deterministic

package
v1.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 11, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package deterministic ships Harbor's second concrete Planner (Phase 48 — RFC §6.2 + RFC §11 Q-6 — the iface-validation lens that proves the `internal/planner.Planner` seam is genuinely swappable).

The same Runtime that drives the Phase 45 LLM-driven ReAct concrete drives this deterministic concrete via the identical `Planner` interface, the identical `RunContext` view, and the identical `Decision` sum. NO Runtime change. NO interface change. NO `Decision` shape change. CLAUDE.md §1 property 3 holds.

Decision-tree model

The deterministic planner is configured with an ordered slice of DecisionTreeStep values. Each step has a `Decide(ctx, rc) (Decision, bool, error)` method; the boolean reports whether the step claimed the current `Next` call. On every `Next`, the walker:

  1. Honours `ctx.Err()`.
  2. Validates planner.RunContext.Quadruple (§6 rule 9 + D-001; fail-loudly per §13 with wrapped planner.ErrIdentityRequired).
  3. Observes [planner.RunContext.Control.Cancelled] — returns `Finish{Cancelled}` at the step boundary per RFC §6.3.
  4. Walks the step set in order. First step returning `(decision, true, nil)` wins. Steps returning `(nil, false, nil)` are skipped. A step returning `(_, _, err)` propagates the error wrapped with planner.ErrDeterministicStep (fail-loudly — no silent skip).
  5. If no step claims the call, the walker returns `Finish{NoPath, Metadata["deterministic"]="no_step_matched"}`. A misconfigured tree surfaces as a typed terminal, NEVER a silent loop (§13).

Wake-on-resolution (D-032)

The deterministic planner declares planner.WakePoll via the planner.WakeAware interface. The WatchGroupStep (and the composed SpawnAndAwaitStep after its first invocation) perform a non-blocking receive against the channel returned by tasks.TaskRegistry.WatchGroup:

  • Channel not yet readable → emit `AwaitTask{TaskID: OwnerTaskID}`; the runtime will re-invoke `Next` at the next deterministic boundary.
  • Channel readable → consume the typed tasks.GroupCompletion's `Members` slice and invoke the operator-supplied `OnResolved` callback whose return value is the planner's decision.

The TaskRegistry stays NEUTRAL (D-032): no `WakeMode` field on registry types, no `Supports*` capability protocol. The choice is the planner concrete's; the deterministic concrete picks `poll` because the WakePoll mode is the on-disk proof that the registry's mode-neutral surface accepts a poller.

Concurrent reuse (D-025)

DeterministicPlanner is a reusable artifact: the receiver is read-only after construction. Per-run state lives on the stack and in the planner.RunContext argument. SpawnAndAwaitStep holds an internal `sync.Map` for per-`(SessionID, StepID)` spawn-tracking, keyed so concurrent reuse across runs is safe. `d025_test.go` pins N=128 invocations under `-race`.

Import-graph contract (§13)

The deterministic package MUST NOT import `internal/runtime/...` or `internal/llm/...`. The Phase 42 internal/planner/conformance.TestImportGraph_PlannerDoesNotImportRuntime covers the new package by construction; `scripts/smoke/phase-48.sh` asserts the same via grep on both forbidden prefixes.

Index

Constants

View Source
const DefaultName = "deterministic"

DefaultName is the DeterministicPlanner.Name returned when WithName is not supplied.

Variables

This section is empty.

Functions

This section is empty.

Types

type CallToolStep

type CallToolStep struct {
	// Tool is the tool name registered in the ToolCatalogView. The
	// runtime executor dispatches via Phase 26's catalog.
	Tool string
	// ArgsBuilder constructs the JSON-encoded args payload from the
	// run context. Required; nil → step error.
	ArgsBuilder func(planner.RunContext) (json.RawMessage, error)
	// When is the optional guard. nil → always match. Non-nil → step
	// claims the call only when the guard returns true.
	When func(planner.RunContext) bool
}

CallToolStep is the operator-configured single-tool dispatch step. When `When` is nil (or returns true), the step claims the call and returns a planner.CallTool decision built from the tool name and the args-builder closure.

`ArgsBuilder` is required: a CallToolStep with a nil ArgsBuilder returns a step error from [Decide] (fail-loudly per §13 — a silent default-args behaviour would mask operator bugs).

Phase 83e (D-147) narrowed `planner.CallTool` to `{tool, args}` — the former `Reasoning` field was removed. A deterministic planner emits no provider-side reasoning, so the step carries no reasoning either.

func (*CallToolStep) Decide

Decide implements DecisionTreeStep.

type DecisionTreeStep

type DecisionTreeStep interface {
	Decide(ctx context.Context, rc planner.RunContext) (planner.Decision, bool, error)
}

DecisionTreeStep is the operator-configurable step abstraction the deterministic planner walks per `Next` call. Each step's [Decide(ctx, rc)] returns:

  • `(decision, true, nil)` — the step claims the call; the planner returns the decision verbatim.
  • `(nil, false, nil)` — the step is skipped; the planner advances to the next configured step.
  • `(_, _, err)` — fail-loudly; the planner wraps the error with planner.ErrDeterministicStep and returns it. NO silent skip.

Implementations MUST be safe for concurrent use. The DeterministicPlanner is a reusable artifact (D-025); the same step instance receives N concurrent invocations from N concurrent runs against the shared planner.

type DeterministicPlanner

type DeterministicPlanner struct {
	// contains filtered or unexported fields
}

DeterministicPlanner is Harbor's second concrete Planner. The receiver is read-only after construction; per-call state lives in `ctx` + planner.RunContext. See package godoc for the wake-mode contract and the decision-tree walker semantics.

func NewDeterministicPlanner

func NewDeterministicPlanner(opts ...Option) (*DeterministicPlanner, error)

NewDeterministicPlanner constructs a DeterministicPlanner. Returns wrapped planner.ErrInvalidConfig when:

Fail-loudly per §13: configuration errors surface at construction time, NEVER at `Next` time.

func (*DeterministicPlanner) Name

func (p *DeterministicPlanner) Name() string

Name returns the planner's human-readable identifier.

func (*DeterministicPlanner) Next

Next implements planner.Planner. The flow is documented in the package godoc.

func (*DeterministicPlanner) WakeMode

func (p *DeterministicPlanner) WakeMode() planner.WakeMode

WakeMode declares the planner's wake-on-resolution strategy (D-032 + Phase 48 spec). Deterministic ships the `poll` mode: each `Next` invocation performs a non-blocking receive on its outstanding group's tasks.TaskRegistry.WatchGroup channel; not ready → emit `AwaitTask`, the runtime sleeps the step until the next deterministic boundary; ready → consume `MemberOutcome` and proceed. No LLM, no eager wake — a clean deterministic shape that proves the registry's `WatchGroup` surface is mode-neutral.

type FinishStep

type FinishStep struct {
	// Reason is the terminal reason. MUST be canonical (see
	// planner.IsValidFinishReason).
	Reason planner.FinishReason
	// PayloadBuilder constructs the terminal payload from the run
	// context. nil → nil Payload.
	PayloadBuilder func(planner.RunContext) (any, error)
	// MetadataBuilder constructs the terminal metadata from the run
	// context. nil → nil Metadata (the step still stamps `run_id`
	// when the run has one — see Decide).
	MetadataBuilder func(planner.RunContext) (map[string]any, error)
	// When is the optional guard. nil → always match.
	When func(planner.RunContext) bool
}

FinishStep is the operator-configured terminal step. When `When` is nil (or returns true), the step claims the call and returns a planner.Finish decision built from the configured reason, the payload-builder closure, and the metadata-builder closure.

`Reason` MUST be one of the canonical planner.FinishReason values; an invalid reason surfaces as a step error.

func (*FinishStep) Decide

Decide implements DecisionTreeStep.

type Option

type Option func(*config)

Option configures a DeterministicPlanner at construction time.

func WithName

func WithName(name string) Option

WithName sets the planner's human-readable identifier (audit + observability). Default: DefaultName.

func WithRegistry

func WithRegistry(reg tasks.TaskRegistry) Option

WithRegistry sets the tasks.TaskRegistry handle that group-aware steps poll via tasks.TaskRegistry.WatchGroup. Required when any configured step is a SpawnAndAwaitStep or a WatchGroupStep; NewDeterministicPlanner returns wrapped planner.ErrInvalidConfig if a group-aware step is configured without a registry.

func WithSteps

func WithSteps(steps ...DecisionTreeStep) Option

WithSteps sets the ordered decision-tree step set. At least one step is required at construction time; an empty set surfaces as wrapped planner.ErrInvalidConfig from NewDeterministicPlanner.

type PauseStep

type PauseStep struct {
	// Reason is the pause reason. MUST be canonical (see
	// planner.IsValidPauseReason).
	Reason planner.PauseReason
	// PayloadBuilder constructs the pause payload from the run
	// context. nil → empty Payload map.
	PayloadBuilder func(planner.RunContext) (map[string]any, error)
	// When is the optional guard. nil → always match.
	When func(planner.RunContext) bool
}

PauseStep is the operator-configured pause-request step. When `When` is nil (or returns true), the step claims the call and returns a planner.RequestPause decision built from the configured reason and the payload-builder closure.

`Reason` MUST be one of the canonical planner.PauseReason values; an invalid reason surfaces as a step error.

func (*PauseStep) Decide

Decide implements DecisionTreeStep.

type SpawnAndAwaitStep

type SpawnAndAwaitStep struct {
	// StepID is the step's unique identifier within the planner's
	// configured step set. Used as the per-(SessionID, StepID) key
	// for the spawn-state map. Default: "<spawn-and-await>".
	StepID string
	// Kind is the [tasks.TaskKind] for the spawned member task.
	// Typically [tasks.KindBackground].
	Kind tasks.TaskKind
	// SpecBuilder constructs the [planner.SpawnSpec] from the run
	// context. Required; nil → step error.
	SpecBuilder func(planner.RunContext) (planner.SpawnSpec, error)
	// GroupID is the optional pre-assigned group identifier. Empty
	// → the step creates an ad-hoc group keyed by
	// `(SessionID, StepID)`.
	GroupID tasks.TaskGroupID
	// OnResolved is invoked once the group reaches a terminal
	// state. The returned decision becomes the planner's next
	// decision. Required; nil → step error.
	OnResolved func(planner.RunContext, []tasks.MemberOutcome) (planner.Decision, error)
	// When is the optional guard. nil → always match (claim the
	// call until the step is resolved).
	When func(planner.RunContext) bool
	// contains filtered or unexported fields
}

SpawnAndAwaitStep is the operator-configured spawn-then-await step. The step ships the load-bearing scenario the §13 primitive- with-consumer policy demands for Phase 48: SpawnTask + AwaitTask are emitted by a real concrete planner against a real tasks.TaskRegistry.

Lifecycle (per `(SessionID, StepID)`):

  1. First [Decide] call → claim, emit planner.SpawnTask built from `SpecBuilder`. The step resolves an ad-hoc group via tasks.TaskRegistry.ResolveOrCreateGroup (when `GroupID` is empty) and spawns a member task via tasks.TaskRegistry.Spawn. The step persists the assigned `(GroupID, TaskID)` in its internal sync.Map.
  2. Subsequent [Decide] calls perform a non-blocking receive against the group's tasks.TaskRegistry.WatchGroup channel: - not yet ready → emit planner.AwaitTask{TaskID: ownerTaskID} so the runtime sleeps the step until the next deterministic boundary (WakePoll semantics, D-032). - ready → invoke `OnResolved(rc, members)`; the returned decision flows through. Once OnResolved fires, the step is MARKED resolved — future `Decide` calls with the same `(SessionID, StepID)` SKIP (return `(nil, false, nil)`).

`StepID` MUST be unique within the planner's configured step set. When empty, the step uses `"<spawn-and-await>"` — fine for a single instance but ambiguous if two `SpawnAndAwaitStep` values are configured; operators with multiple group-aware steps SHOULD set distinct StepIDs.

`OnResolved` MUST be safe for concurrent use — the same step instance is shared across N concurrent runs against the planner.

func (*SpawnAndAwaitStep) Decide

Decide implements DecisionTreeStep. See type godoc for the lifecycle semantics.

type WatchGroupStep

type WatchGroupStep struct {
	// GroupID is the pre-existing group identifier.
	GroupID tasks.TaskGroupID
	// OwnerTaskID is the task whose lifecycle the planner reports
	// as the AwaitTask target. Typically the spawn handle's TaskID
	// surfaced by an earlier SpawnTask emission.
	OwnerTaskID tasks.TaskID
	// OnResolved is invoked once the group reaches a terminal
	// state. Required; nil → step error.
	OnResolved func(planner.RunContext, []tasks.MemberOutcome) (planner.Decision, error)
	// When is the optional guard. nil → always match.
	When func(planner.RunContext) bool
	// contains filtered or unexported fields
}

WatchGroupStep is the operator-configured "I am waiting on this pre-existing group" step. Distinct from SpawnAndAwaitStep: this step does NOT spawn — it expects the group to exist already (the runtime engine, or another planner step earlier in the tree, created it). The step performs the WakePoll non-blocking receive per `Decide` call:

  • not yet ready → emit planner.AwaitTask{TaskID: OwnerTaskID}.
  • ready → invoke `OnResolved(rc, members)`; once it fires the step is marked resolved and SKIPS future calls of the same `(SessionID, OwnerTaskID)`.

func (*WatchGroupStep) Decide

Decide implements DecisionTreeStep.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL