planner

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: 17 Imported by: 0

Documentation

Overview

Multimodal first-turn materialization (Round-7 F11 / D-166).

The Playground composer's chat-attach control uploads files via `artifacts.put` and the operator clicks Send. The runtime carries the artifact IDs onto the task (`tasks.Task.InputArtifactIDs`); the run loop pre-resolves each entry into a `planner.InputArtifactView` on the `RunContext` and hands the planner a synchronous, ready-to- render slice. `MaterializeInputContent` is the per-MIME dispatcher the planner calls when assembling its first-turn user message:

  • `image/*` → `llm.ImagePart{DataURL: data:<mime>;base64,<bytes>}` (Path 1 from D-166: bytes inline at the LLM edge so vision- capable providers actually see the image). The base64 encoding is bounded by the operator's upload (the runtime ArtifactStore itself caps each artifact size; the materializer is a pass-through).
  • `application/pdf` → `llm.FilePart{Artifact: &llm.ArtifactStub{...}}` by reference. Providers that support PDF native (Anthropic today) consume the ref via the bifrost driver's existing translatation; providers without PDF support see the canonical `ArtifactStub` JSON description (graceful degradation, RFC §6.5).
  • `audio/*` → `llm.AudioPart{Artifact: &llm.ArtifactStub{...}}` by reference. Same graceful-degradation rule as PDF.
  • everything else → bare `ArtifactStub` text block on the user message — the LLM sees the ref + MIME + size + (optional) `Fetch.Tool` pointer and routes to a matching tool via the catalog. The operator gets multimodal-as-routing-hint for free (e.g. "I uploaded a CSV, please summarise it").

The optional `Fetch.Tool` pointer on every emitted `ArtifactStub` is populated from the supplied `ToolCatalogView`: the first tool whose `HandlesMIME` matches the artifact's MIME wins. Operators register an audio.transcribe tool with `HandlesMIME: ["audio/*"]` once and the LLM gets an explicit "use this tool for this ref" hint — no LLM-side guesswork.

Package planner ships Harbor's swappable reasoning-policy seam.

The Runtime owns mechanism (sessions, runs, tasks, events, streaming, pause/resume, artifacts, tool execution, memory injection); the Planner owns policy (next-action selection, tool choice, finish detection). The contract is a single interface:

type Planner interface {
    Next(ctx context.Context, run RunContext) (Decision, error)
}

`Decision` is a sealed sum-type with six shapes (CallTool, CallParallel, SpawnTask, AwaitTask, RequestPause, Finish — see decision.go). The Runtime executes the decision; the Planner never reaches into Runtime internals. Tools, memory, skills, artifacts, pause/resume, steering — every capability the planner can read is reachable through `RunContext`, the only surface the planner sees.

Phase 42 ships the interface + the sum + the views + a stub finish.Planner that always returns Finish{Reason: Goal}. Phase 45 ships the reference ReAct concrete; Phase 48 ships the deterministic concrete. The conformance harness skeleton (Phase 49) lives in internal/planner/conformance/.

Import-graph contract (binding — see CLAUDE.md §1 + §13): `internal/planner/...` MUST NOT import `internal/runtime/...`. The conformance/importgraph_test.go walks every Go file under the planner subtree and fails the build on a `internal/runtime/...` import.

Concurrent-reuse contract (D-025): every concrete Planner MUST be safe to share across N concurrent goroutines. Per-run state lives in `ctx` + `RunContext`, never on the receiver. See concurrent_test.go for the N=128 reuse test the stub planner passes.

Wake-on-resolution contract (D-032): when a planner emits a `SpawnTask` without retain-turn, it MUST consume `tasks.TaskRegistry.WatchGroup` to learn when the group resolves. The three modes (push / poll / hybrid) are documented at the `internal/tasks/groups.go` package godoc; Phase 42's `WakeMode` enum + optional `WakeAware` interface let concretes declare which mode they use so the conformance pack can assert the round-trip.

Index

Constants

View Source
const (
	// TaskErrorCodeRunLoopError marks a run whose RunLoop.Run returned
	// a non-cancellation error.
	TaskErrorCodeRunLoopError = "runloop_error"
	// TaskErrorCodeCancelled marks a run whose RunLoop.Run surfaced
	// context.Canceled. The FSM has no auto-cancelled status (Cancel
	// is the external-caller surface and requires a reason); Failed
	// with this code is the closest terminal match — see D-098.
	TaskErrorCodeCancelled = "cancelled"
)

Terminal task-error codes a run-loop driver stamps on `tasks.TaskError.Code` when a run ends without FinishGoal (Phase 106 / D-098; named and exported by Phase 110a).

View Source
const (
	// EventTypePlannerDecision — emitted by a planner concrete after
	// each Next call. Payload (defined at Phase 45) carries the
	// Decision shape + reasoning hash + step latency.
	EventTypePlannerDecision events.EventType = "planner.decision"

	// EventTypePlannerFinish — emitted when a planner returns
	// Finish{}. Payload (defined at Phase 45) carries the
	// FinishReason + terminal metadata.
	EventTypePlannerFinish events.EventType = "planner.finish"

	// EventTypePlannerError — emitted when Planner.Next returns an
	// error. Payload (defined at Phase 45) carries the error code +
	// message + step index.
	EventTypePlannerError events.EventType = "planner.error"

	// EventTypePlannerRepairExhausted — emitted by the Phase 44
	// repair loop on the graceful-failure path: after
	// `max_consecutive_arg_failures` consecutive arg-validation
	// failures OR `repair_attempts` exceeded, the loop returns
	// Finish{Reason: NoPath, Metadata["followup"]=true} AND emits
	// this event so operators see the failure loudly. The event is
	// the load-bearing surface that distinguishes Harbor's graceful
	// failure from the silent-degradation pattern banned by §13.
	EventTypePlannerRepairExhausted events.EventType = "planner.repair_exhausted"

	// EventTypePlannerMaxStepsExceeded — emitted by the Phase 45
	// ReAct planner's MaxSteps circuit breaker. When a planner step
	// observes a trajectory whose step count is ≥ the configured
	// MaxSteps, the planner returns
	// Finish{Reason: NoPath, Metadata["max_steps_exceeded"]=true}
	// AND emits this event before returning so operators see the
	// circuit-breaker fire loudly. Companion to repair_exhausted —
	// same fail-loudly shape, different graceful-failure source
	// (repair loop vs. planner-side step cap). D-051.
	EventTypePlannerMaxStepsExceeded events.EventType = "planner.max_steps_exceeded"

	// EventTypeTrajectoryCompressed — emitted by the Phase 46
	// CompressionRunner when the trajectory summariser successfully
	// produces a compaction artefact. Payload
	// (TrajectoryCompressedPayload, SafePayload) carries the run's
	// identity quadruple + step count + token estimate at the moment
	// of compression. The success-path companion to
	// trajectory.compression_failed (the fail-loudly surface). D-055.
	EventTypeTrajectoryCompressed events.EventType = "trajectory.compressed"

	// EventTypeTrajectoryCompressionFailed — emitted by the Phase 46
	// CompressionRunner when the summariser returns an error, the
	// estimator fails, or the summariser returns (nil, nil). Payload
	// (TrajectoryCompressionFailedPayload, SafePayload) carries the
	// identity + step count + token estimate + error code + truncated
	// error message. The load-bearing fail-loudly observability surface
	// for compression failures (§13 — silent degradation banned). D-055.
	EventTypeTrajectoryCompressionFailed events.EventType = "trajectory.compression_failed"

	// EventTypePlannerRepairGuidanceInjected — emitted by the Phase 83c
	// ReAct prompt builder each turn it merges an escalating repair-
	// guidance block into the system prompt because a
	// [RunContext.RepairCounters] field tripped. Payload
	// (RepairGuidanceInjectedPayload, SafePayload) carries the run
	// identity + the tier (`reminder` / `warning` / `critical`) + the
	// counter name (`finish` / `args` / `multi_action`) + the counter
	// value at render time. The emit lets the Console / operator see
	// when the LLM is struggling to produce well-formed output across
	// steps — the across-step companion to `planner.repair_exhausted`
	// (the per-step terminal). D-145.
	EventTypePlannerRepairGuidanceInjected events.EventType = "planner.repair_guidance_injected"

	// EventTypePlannerActionExtraFieldDropped — emitted by the Phase 44
	// repair loop when an incoming action object carried a field the
	// Phase 83e-narrowed action schema no longer recognises (`reasoning`
	// / `thought` — D-147). The loop strips the field and emits this
	// event for telemetry; it is a soft signal, NOT a failure — the
	// runtime fails open on extra fields for backward compatibility with
	// older trained models. Payload (ActionExtraFieldDroppedPayload,
	// SafePayload) carries the run identity + the dropped field name.
	EventTypePlannerActionExtraFieldDropped events.EventType = "planner.action_extra_field_dropped"
)

Planner-emitted event types. Phase 42 registers the constants with the events package's canonical registry so future concretes (Phase 45 ReAct, Phase 48 Deterministic, etc.) can emit without re-registering.

Most payload structs land at the phase that first emits each type — Phase 42 ships only the type-name registration for `planner.decision` / `planner.finish` / `planner.error`, since the stub finish.Planner does not emit. Decoupling type registration from payload definition matches the events-package convention (RFC §6.2 / events/events.go §RegisterEventType).

Phase 44 adds `planner.repair_exhausted` AND its typed payload — the repair loop is the first emitter, so the payload ships in the same PR (CLAUDE.md §13 fail-loudly principle: the emit is the observability surface that makes graceful failure NOT silent).

View Source
const AbsoluteMaxParallel = 50

AbsoluteMaxParallel is the system cap on CallParallel branch counts (RFC §6.2 — settled). Any CallParallel with more branches fails the whole call with ErrParallelCapExceeded BEFORE execution. The cap is defence in depth — the planner's PlanningNudges.MaxParallel is the soft cap; this constant is the hard cap that protects the runtime from a runaway emission (a buggy LLM emitting 1000 branches).

D-056 — Phase 47 settles the value at 50.

Variables

View Source
var (
	// ErrNilTrajectory is the fail-loud sentinel returned by
	// [CompressionRunner.MaybeCompress] (and [DefaultTokenEstimator])
	// when the supplied trajectory pointer is nil. Distinct from the
	// "no budget set" / "under threshold" no-op paths — nil is a
	// composition error (the caller passed a wrong value); the runner
	// surfaces it loudly rather than treating it as "skip".
	ErrNilTrajectory = errors.New("planner: compression refuses nil trajectory")

	// ErrEmptySummary is the fail-loud sentinel returned by
	// [CompressionRunner.MaybeCompress] when the [Summariser] returns
	// (nil, nil). The summariser contract is "return a non-nil
	// summary on success OR a non-nil error"; returning (nil, nil) is
	// a bug, not a recovery state. The runner refuses to stamp a nil
	// summary; the call surfaces the contract violation loudly.
	ErrEmptySummary = errors.New("planner: summariser returned (nil, nil) — contract violation")
)

Sentinel errors. Use errors.Is.

View Source
var (
	// ErrPlannerClosed — operations against a planner whose Close()
	// has been called. Reserved for future planner concretes that
	// hold lifecycle resources (HTTP client pools, LLM driver
	// sessions, etc.).
	ErrPlannerClosed = errors.New("planner: planner closed")

	// ErrInvalidDecision — the planner returned a Decision the
	// Runtime cannot dispatch (unknown FinishReason / PauseReason,
	// CallTool with empty Tool name, CallParallel with zero
	// branches). The Runtime rejects with this sentinel before
	// dispatching the decision.
	ErrInvalidDecision = errors.New("planner: invalid decision")

	// ErrRepairExhausted (Phase 44) — surfaced by the
	// `internal/planner/repair.RepairLoop` for callers that want to
	// inspect the graceful-failure path before the loop's terminal
	// `Finish{NoPath}` is dispatched. The loop's `Run` returns
	// (Decision, nil) on graceful failure — Finish IS the success path
	// — but the wrapped sentinel is available via the Finish.Metadata
	// `repair_error` slot for observability sinks that prefer error-
	// shaped reads. Compare via `errors.Is`. The fail-loudly emit
	// (`planner.repair_exhausted`) is the canonical observability
	// surface; this sentinel is the secondary read surface.
	ErrRepairExhausted = errors.New("planner: schema repair exhausted")

	// ErrIdentityRequired (Phase 48) — a concrete planner observed a
	// `RunContext.Quadruple` missing one of the four scope components
	// (tenant / user / session / run). Identity is mandatory at every
	// planner boundary (§6 rule 9 + D-001). Concrete planners SHOULD
	// wrap this sentinel with their context so the runtime executor
	// can surface a precise failure (e.g. `fmt.Errorf("%w (missing
	// run_id)", planner.ErrIdentityRequired)`). The deterministic
	// planner is the first emitter; future concretes that enforce
	// identity at Next boundary consume the same sentinel.
	ErrIdentityRequired = errors.New("planner: identity required (tenant/user/session/run)")

	// ErrInvalidConfig (Phase 48) — a concrete planner's constructor
	// rejected the supplied configuration (empty step set, missing
	// dependency, contradictory options). The fail-loudly contract:
	// a malformed configuration MUST surface at construction time,
	// NEVER at Next time. Wrapping per-concrete with structural
	// context (`fmt.Errorf("%w: WithSteps required at least one
	// step", planner.ErrInvalidConfig)`) is the recommended pattern.
	ErrInvalidConfig = errors.New("planner: invalid configuration")

	// ErrDeterministicStep (Phase 48) — a `DecisionTreeStep` inside
	// the deterministic planner's walker returned a non-nil error.
	// The planner wraps the step's error with this sentinel so
	// callers can distinguish a structural step failure from a
	// transport-level error returned by other concretes. Fail-loudly
	// per §13 — the walker does NOT skip a failing step (a silent
	// skip would mask operator bugs in the tree).
	ErrDeterministicStep = errors.New("planner/deterministic: step returned error")
	// ErrParallelCapExceeded (Phase 47, D-056) — a CallParallel was
	// emitted with more branches than AbsoluteMaxParallel. The runtime
	// parallel executor rejects with this sentinel BEFORE any branch
	// dispatches — atomic-setup-validation discipline per RFC §6.2.
	// Fail-loudly per §13; never silently truncates the branch list.
	ErrParallelCapExceeded = errors.New("planner: CallParallel branch count exceeds absolute_max_parallel")

	// ErrParallelInvalidJoin (Phase 47, D-056) — a CallParallel was
	// emitted with a JoinSpec whose shape is malformed (e.g. JoinN
	// with N ≤ 0 or N > len(Branches), or an unknown JoinKind). The
	// executor rejects at setup time, before any branch dispatches.
	ErrParallelInvalidJoin = errors.New("planner: CallParallel join spec invalid")

	// ErrParallelBranchInvalidArgs (Phase 47, D-056) — atomic-setup
	// validation: ANY branch's args fail the descriptor's validator,
	// the whole CallParallel fails before execution. The wrapped error
	// names the offending branch index + the upstream validator error.
	ErrParallelBranchInvalidArgs = errors.New("planner: CallParallel branch failed atomic-setup validation")

	// ErrParallelPauseUnsupported (Phase 47, D-056) — a branch requested
	// a pause mid-execution. The unified pause/resume primitive lands
	// at Phase 50; until then, the parallel executor MUST fail loud
	// per RFC §6.2's parallel-pause-atomicity contract. Phase 50
	// upgrades this path to a checkpointed atomic pause.
	ErrParallelPauseUnsupported = errors.New("planner: CallParallel pause-mid-execution not supported until Phase 50 unified pause primitive")

	// ErrMemoryBlockUnserializable (Phase 83d, D-146) — a value in
	// `RunContext.MemoryBlocks` (External / Conversation) or in
	// `RunContext.SkillsContext` could not be encoded to compact JSON
	// for injection into the ReAct system prompt. The prompt builder
	// fails loudly with this sentinel — it NEVER degrades to an empty
	// `<read_only_*_memory>` wrapper or silently drops the tier. This
	// closes the silent-context-loss failure mode (CLAUDE.md §5 +
	// §13). The wrapped error names the offending tier / index and the
	// upstream `json.Marshal` error. Compare via `errors.Is`.
	ErrMemoryBlockUnserializable = errors.New("planner: memory block is not JSON-serialisable")

	// ErrParallelThresholdUnmet (Phase 47, D-056) — a JoinN parallel
	// call completed but fewer than N branches succeeded. This is a
	// runtime-execution outcome, NOT an invalid decision: the
	// CallParallel was well-formed and passed atomic-setup validation;
	// the branches simply failed at invoke time. Distinguished from
	// ErrInvalidDecision so callers doing errors.Is can tell a
	// malformed emission apart from a runtime branch shortfall. The
	// wrapped error joins every failed branch's error.
	ErrParallelThresholdUnmet = errors.New("planner: CallParallel JoinN threshold not met")
)

Sentinel errors. Callers compare via errors.Is.

Trajectory-shaped sentinels (ErrUnserializable, ErrToolContextLost) live in the canonical subpackage internal/planner/trajectory and are re-exported as type aliases at trajectory.go in this package. Pre-Phase-43 stub ErrTrajectoryNotImplemented is retired — Phase 43 ships the real fail-loudly Serialize contract that replaces it.

View Source
var (
	// ErrDriverUnknown — Resolve was called with a name no driver has
	// registered for. The error message lists the registered driver
	// names so the operator sees the typo.
	ErrDriverUnknown = errors.New("planner: driver not registered")
	// ErrDriverEmptyName — Register was called with an empty driver
	// name. Build-time configuration bug.
	ErrDriverEmptyName = errors.New("planner: driver registration: empty name")
	// ErrDriverNilFactory — Register was called with a nil Factory.
	// Build-time configuration bug.
	ErrDriverNilFactory = errors.New("planner: driver registration: nil factory")
	// ErrDriverDuplicate — Register was called twice for the same
	// driver name. Build-time configuration bug (typically a
	// double-blank-import).
	ErrDriverDuplicate = errors.New("planner: driver registration: duplicate name")
)

Sentinel errors specific to the registry. Driver-internal errors continue to use the driver's own sentinels.

Functions

func DefaultTokenEstimator

func DefaultTokenEstimator(tr *Trajectory) (int, error)

DefaultTokenEstimator is the chars/4 estimator backed by trajectory.Trajectory.Serialize. Used when WithTokenEstimator is not set.

A nil trajectory returns (0, ErrNilTrajectory) — the estimator fails closed rather than returning a meaningless zero estimate.

The chars/4 algorithm under-counts multimodal content compared to the LLM-edge estimator (which adds 256 tokens per non-text part); trajectories don't typically carry multimodal parts directly in LLMContext (heavy content is upstream of the trajectory per the D-026 safety pass), so the simpler walker is sufficient at Phase 46. A future estimator that structurally walks the trajectory is a Phase 47+ refinement; the TokenEstimator functional-option seam is the unwind point.

func IsValidFinishReason

func IsValidFinishReason(r FinishReason) bool

IsValidFinishReason reports whether r is one of the canonical finish reasons.

func IsValidPauseReason

func IsValidPauseReason(r PauseReason) bool

IsValidPauseReason reports whether r is one of the four canonical pause reasons. Used by the conformance pack to verify RequestPause.Reason is well-formed.

func IsValidReasoningReplayMode

func IsValidReasoningReplayMode(m ReasoningReplayMode) bool

IsValidReasoningReplayMode reports whether m is one of the canonical replay modes. The empty string is accepted — it is the unset sentinel that resolves to `ReasoningReplayNever`. Config validation uses this to reject typo'd values loudly pre-boot.

func IsValidWakeMode

func IsValidWakeMode(m WakeMode) bool

IsValidWakeMode reports whether m is one of the three canonical modes. The conformance pack uses this to validate WakeAware implementations.

func MaterializeInputContent

func MaterializeInputContent(goal string, artifacts []InputArtifactView, catalog ToolCatalogView) llm.Content

MaterializeInputContent assembles the first-turn LLM `Content` from the user goal text plus a slice of pre-resolved input artifacts. Returns a text-only `Content` when `artifacts` is empty (the existing text-only path is unchanged); otherwise returns a `Content{Parts: [...]}` with one PartText for the goal and one part per artifact, dispatched by MIME.

`catalog` is consulted for the `Fetch.Tool` annotation on emitted `ArtifactStub`s; pass a nil-or-empty view if MIME routing should be left to the LLM's catalog discovery.

Pure function — no I/O, no goroutines, no state. The Bytes slot on each InputArtifactView is consumed read-only; the materializer never mutates the slice or its contents.

func MustRegister

func MustRegister(name string, factory Factory)

MustRegister wraps Register and panics on error. The typical driver-side idiom: `init() { planner.MustRegister("react", New) }`. A duplicate-name panic at init signals a build bug (probably two drivers chose the same canonical name); the panic message names the offending driver so the operator's stack trace points at the fix.

func Register

func Register(name string, factory Factory) error

Register installs a Factory under name. Drivers self-register from their package init(); the binary entry point (`cmd/harbor/main.go`) blank-imports each driver to fire the registration.

Re-registering the same name returns `ErrDriverDuplicate` (the caller, an init() function, should panic on this — it signals a build mis-configuration). The function does NOT panic itself so the test suite can exercise the duplicate-name path without bringing the process down.

func RegisteredDrivers

func RegisteredDrivers() []string

RegisteredDrivers returns a sorted list of registered driver names. Useful for boot-log output and `planner.Resolve` error messages.

func ResolveProvenance added in v1.2.0

func ResolveProvenance(src map[string]any) string

ResolveProvenance derives the human-readable origin string the manifest shows for an artifact, from its opaque `Source` map (Phase 107f — D-176). Resolution order:

  1. The canonical `source` key when it is a non-empty string (e.g. "user_upload", "tool", "flow").
  2. Otherwise a `tool` name key → "tool: <name>".
  3. Otherwise a `flow` name key → "flow: <name>".
  4. Otherwise a `producer` key → that value verbatim.
  5. Otherwise "unknown".

The else-chain keeps EXISTING artifacts (put before Phase 107f, so carrying no canonical `source` key) resolving to a real provenance instead of "unknown" — no back-fill migration needed (D-176).

func TaskErrorCodeForFinish added in v1.3.0

func TaskErrorCodeForFinish(reason FinishReason) string

TaskErrorCodeForFinish maps a non-goal terminal FinishReason (NoPath, Cancelled, DeadlineExceeded, ConstraintsConflict, ...) to the `tasks.TaskError.Code` a run-loop driver stamps when the run reached Finish without satisfying the goal. The mapping is the reason's string verbatim — exactly the pre-110a behaviour — so the Console / operator sees WHY the run ended without a goal.

Types

type ActionExtraFieldDroppedPayload

type ActionExtraFieldDroppedPayload struct {
	events.SafeSealed
	Identity   identity.Quadruple
	Field      string
	OccurredAt time.Time
}

ActionExtraFieldDroppedPayload is the typed payload for EventTypePlannerActionExtraFieldDropped (Phase 83e — D-147). SafePayload — every field is operator-visible debug data, never secret-shaped:

  • `Identity` is the run's identity quadruple.
  • `Field` is the name of the dropped extra field (`reasoning` or `thought`) — the parser strips fields the narrowed `{tool, args}` action schema no longer recognises.

The event is a SOFT telemetry signal: extra fields are stripped, the step proceeds. It is NOT a fail-loudly surface — the runtime fails OPEN on extra fields for backward compatibility with older trained models. The captured thinking trace flows through the provider channel onto `trajectory.Step.ReasoningTrace` instead.

type AnswerEnvelope added in v1.3.0

type AnswerEnvelope struct {
	// Answer is the assistant's final answer text extracted from the
	// Finish payload.
	Answer string `json:"answer"`
	// FinishReason is the planner's Finish.Reason, verbatim
	// (string-typed on the wire).
	FinishReason string `json:"finish_reason"`
	// ToolCallsSeen is the number of trajectory steps the run
	// accumulated (the tool-dispatch count the operator sees).
	ToolCallsSeen int `json:"tool_calls_seen"`
}

AnswerEnvelope is the canonical JSON shape `tasks.TaskResult.Value` carries when a run-loop driver completes a task from a Finish with reason FinishGoal (Phase 106; named and exported by Phase 110a):

{"answer": "<llm text>", "finish_reason": "<FinishReason>", "tool_calls_seen": <int>}

Producers: the per-task run-loop drivers (cmd/harbor + harbortest/devstack) marshal it on MarkComplete. Consumers: the `tasks.get` Protocol projector reads it into `result_inline`, and `internal/runtime/dispatch`'s AwaitTask / retain-turn SpawnTask observation parses it back for the awaiting planner.

The encoding is byte-compatible with the Phase 106 map-literal shape (keys in the same order; pinned by the golden test in answer_envelope_test.go). Forward-compatible — future phases extend with new keys; never break existing ones.

Phase 110a (D-194) named and exported what was an implicit cmd↔cmd wire contract (the SDK friction audit's Pattern 1 / P3 finding): one `package main` file marshalled what another parsed, with no named type anywhere. Home rationale (import direction, recorded in the phase plan): the envelope is the projection of Finish — it carries FinishReason verbatim — and both producers (run-loop drivers) and consumers (`internal/runtime/dispatch`, Protocol projectors) already import `internal/planner`; homing it in `internal/tasks` would force a new tasks→planner import edge (tasks is planner-free today).

type ArtifactManifestEntry added in v1.2.0

type ArtifactManifestEntry struct {
	// Ref is the content-addressed artifact identifier the model passes
	// to `artifact_fetch` to read the artifact's bytes.
	Ref string
	// Filename is the operator-supplied or producer-supplied original
	// filename, if known. May be empty.
	Filename string
	// MIME is the artifact's IANA media type (metadata for the model).
	MIME string
	// SizeBytes is the artifact's byte length (metadata, not the bytes).
	SizeBytes int64
	// Provenance is the human-readable origin of the artifact, resolved
	// by the run loop from the artifact's `Source` map (canonical
	// `source` key else the tool/producer/flow name else "unknown").
	Provenance string
}

ArtifactManifestEntry is one metadata-only row in the session-artifact manifest (Phase 107f — D-176). The run loop builds the slice from `ArtifactStore.List` scoped to the run's `(tenant, user, session)` triple and sets it on `RunContext.SessionArtifacts`; the ReAct planner renders the rows into the read-only `<session_artifacts>` prompt block so the model can `artifact_fetch` any listed `Ref`.

The shape carries NO bytes — content stays in the store and is fetched on demand via the `artifact_fetch` meta-tool (D-026 heavy-content discipline). `Provenance` is the run-loop-resolved origin string (e.g. "user_upload", "tool: web_search", "flow: ...") — see the run loop's provenance resolver.

func BuildArtifactManifest added in v1.2.0

func BuildArtifactManifest(refs []artifacts.ArtifactRef) []ArtifactManifestEntry

BuildArtifactManifest maps a session's listed artifact refs onto the metadata-only ArtifactManifestEntry slice the planner renders into the read-only `<session_artifacts>` prompt block (Phase 107f — D-176).

It is the SINGLE source of the run-loop ↔ devstack manifest build so the production dev run loop and the test harness cannot diverge (CLAUDE.md §17.6 parity). Both call sites list `ArtifactStore.List` scoped to the run's `(tenant, user, session)` triple and hand the returned refs here.

Ordering is stable newest-first: the artifact's `created_at` provenance stamp (when present) primary, descending; the content- addressed `ID` secondary, ascending, as a deterministic tiebreaker so the map-iteration-order non-determinism of `ArtifactStore.List` never leaks into the prompt (a stable prefix preserves KV-cache windows).

The FULL slice is returned; the renderer ([renderSessionArtifacts] in the react package) caps the rendered rows and appends an explicit "+K more" line on overflow (AC-6) — never a silent truncation. A nil / empty input returns nil so the planner omits the block entirely.

type AwaitTask

type AwaitTask struct {
	TaskID tasks.TaskID
}

AwaitTask blocks the planner until the named task reaches a terminal state. The Runtime's executor watches the task's lifecycle and re-invokes Next with the MemberOutcome surfaced in the next trajectory step.

type BackgroundMemberOutcome

type BackgroundMemberOutcome = trajectory.BackgroundMemberOutcome

BackgroundMemberOutcome is the per-member outcome inside a BackgroundResult.

type BackgroundResult

type BackgroundResult = trajectory.BackgroundResult

BackgroundResult is the planner's projection of a resolved non-retain-turn task group.

type Budget

type Budget struct {
	// Deadline is the absolute wall-clock deadline for the run. Zero
	// value means no deadline. The Runtime's ctx is set to expire at
	// Deadline; the planner SHOULD honour ctx.Err() between long
	// phases of work.
	Deadline time.Time
	// HopBudget is the maximum number of planner steps remaining.
	// Negative means no cap. Decrements per planner step.
	HopBudget int
	// CostCap is the maximum LLM cost (USD-equivalent micros) for
	// the run. Zero means no cap. The Runtime's Governance subsystem
	// enforces; the planner reads.
	CostCap int64
	// CostSpent is the cost accumulated so far this run. Same units
	// as CostCap.
	CostSpent int64
	// TokenBudget is the maximum estimated token count the planner-
	// observed trajectory may carry before the runtime invokes the
	// trajectory summariser (Phase 46 seam; Phase 111e consumer —
	// D-202). Zero means no token-budget enforcement; the trajectory
	// grows unbounded (until the D-026 context-window safety net
	// backstops it).
	//
	// The runtime's CompressionRunner reads this field: the steering
	// RunLoop calls [CompressionRunner.MaybeCompress] at each step
	// boundary when TokenBudget > 0 and a runner is configured on the
	// run spec. When the trajectory's estimate exceeds the budget the
	// configured [Summariser] (production: the LLM-backed
	// TrajectorySummariser in internal/llm/summarizer) produces a
	// [TrajectorySummary] that replaces the raw step history in
	// subsequent prompt builds (RFC §6.2, brief 02 §4, D-055). One
	// compression per run at V1.1.x (the runner's Summary != nil
	// idempotence). Production wiring: the `planner.token_budget`
	// config knob projects here via the per-task run-loop drivers.
	// Compression is a runtime concern; the planner sees only the
	// compacted view via the trajectory summary.
	TokenBudget int
}

Budget carries the per-run hard caps the planner observes. The Runtime enforces them outside the planner — the planner reads to make budget-aware decisions (e.g. choose a cheaper model when CostRemaining is low).

type BudgetHints

type BudgetHints struct {
	// MaxSteps is the advisory planner-step ceiling. Nil = no hint.
	MaxSteps *int
	// MaxCostUSD is the advisory cost ceiling, in USD. Nil = no hint.
	MaxCostUSD *float64
	// MaxLatencyMS is the advisory wall-clock latency ceiling, in
	// milliseconds. Nil = no hint.
	MaxLatencyMS *int64
}

BudgetHints carries the optional budget caps a runtime may surface to the planner through PlanningHints.Budget (Phase 83c — D-145). Every field is a pointer: nil means "no cap for this dimension", so a partial BudgetHints renders only the dimensions it pins. The caps are advisory prompt content — the runtime's Governance subsystem (Phase 47+) and Budget enforce the hard caps; the planner reads BudgetHints to make budget-aware decisions.

type CallParallel

type CallParallel struct {
	Branches []CallTool
	Join     *JoinSpec
}

CallParallel invokes N tools concurrently with a JoinSpec describing how the Runtime merges results. Atomic setup validation: any branch's invalid args fails the whole call before execution (RFC §6.2; Phase 47 ships the executor).

Branches share the same step-level pause/cancel atomicity contract — see Phase 47's plan.

type CallTool

type CallTool struct {
	// Tool is the name registered in the ToolCatalogView.
	Tool string
	// Args is the JSON-encoded argument payload matching the tool's
	// ArgsSchema. Validation happens at the catalog edge; an invalid
	// payload produces `tools.ErrToolInvalidArgs` from dispatch.
	Args json.RawMessage
	// CallID is the provider-assigned tool-call identifier (Phase
	// 107c / D-167 — native tool-calling). Empty for prompt-
	// engineered CallTool emissions; non-empty when the call
	// originated from a native ToolCall. Round-trips on the
	// RoleTool message's ToolCallID field.
	CallID string
}

CallTool invokes one tool with structured args. The Runtime dispatches via the production ToolCatalog + ToolPolicy (Phase 26 + 26a); the planner does not block on the call.

The action shape is intentionally narrow — `{tool, args}` only. Phase 83e (D-147) dropped the former `Reasoning` field: the model emits the action JSON, and the provider-side thinking trace is captured separately on `trajectory.Step.ReasoningTrace` via `llm.CompleteResponse.Reasoning`. Reasoning is captured content, not part of the structured decision; replaying it into prompts is an operator-controlled per-agent knob (D-148), never a schema field.

type ChunkKind added in v1.2.0

type ChunkKind string

ChunkKind is a sealed enum for the streaming-chunk kind (Phase 107). Values: ChunkContent (model output text), ChunkReasoning (thinking trace — Phase 107a renders).

const (
	ChunkContent   ChunkKind = "content"
	ChunkReasoning ChunkKind = "reasoning"
)

type CompressionOption

type CompressionOption func(*CompressionRunner)

CompressionOption configures a CompressionRunner at construction.

func WithTokenEstimator

func WithTokenEstimator(est TokenEstimator) CompressionOption

WithTokenEstimator overrides DefaultTokenEstimator. Tests use this to inject a deterministic counter; production wiring uses the default.

A nil estimator is a no-op (the option is dropped) — defensive against callers wiring options in a loop.

type CompressionRunner

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

CompressionRunner drives the "estimate → optional summariser → stamp Trajectory.Summary" loop the runtime invokes between planner steps. The production cadence is the steering RunLoop's step boundary (Phase 111e — one MaybeCompress call per step, gated on Budget.TokenBudget > 0 and a configured runner).

Reusable artifact (D-025): one constructed instance is safe to share across N concurrent runs; per-call state lives entirely in `ctx` + RunContext + the per-call serialised bytes. The receiver is read-only after construction.

**Idempotent on `tr.Summary != nil`.** A second call with an already-stamped trajectory returns nil without invoking the summariser — this short-circuit IS the V1.1.x "one compression per run, no auto-cascade" scope fence (RFC §6.5; re-compaction cadence is the recorded D-202 follow-up). The layer that owns a future re-compaction policy clears the summary before re-invoking.

func NewCompressionRunner

func NewCompressionRunner(summariser Summariser, opts ...CompressionOption) *CompressionRunner

NewCompressionRunner constructs a CompressionRunner from the supplied Summariser + options.

**Panics on nil summariser** — composition error caught at boot, matching [react.New]'s nil-client behaviour. Operators that need a "no-op runner" should pass a no-op Summariser implementation, not nil; nil is a contract violation surfaced as a panic.

func (*CompressionRunner) MaybeCompress

func (r *CompressionRunner) MaybeCompress(
	ctx context.Context,
	rc RunContext,
	tr *Trajectory,
) error

MaybeCompress is the runner's entrypoint. The flow (in order):

  1. Honour `ctx.Err()` at entry; return verbatim if cancelled.
  2. Reject missing identity (wrapped llm.ErrIdentityMissing).
  3. Defensive nil guard: nil `tr` returns wrapped ErrNilTrajectory.
  4. Already-compressed short-circuit: when `tr.Summary != nil`, return nil without calling the summariser (idempotent).
  5. Compute the token estimate via the configured estimator. Estimator errors propagate (with the `trajectory.compression_failed` emit carrying error code "estimator_error").
  6. Budget check: when `rc.Budget.TokenBudget <= 0`, return nil (no budget enforced).
  7. When `estimate <= rc.Budget.TokenBudget`, return nil (below threshold).
  8. Invoke `summariser.Summarise(ctx, rc, tr)`. A non-nil error → emit `trajectory.compression_failed` with code "summariser_error", return the error wrapped. A nil `*TrajectorySummary` with nil error → wrapped ErrEmptySummary + emit with code "empty_summary".
  9. Stamp `tr.Summary = result` and emit `trajectory.compressed` carrying the identity + before/after step count + token estimate.
  10. Return nil.

**Fail-loudly (CLAUDE.md §13).** Every error path emits the failure event before returning; there is no silent fall-through to raw history. The success-path emit pairs with the failure-path emit so compression is observable in both directions.

**Identity is mandatory (§6 rule 9 + D-001).** A partial quadruple returns wrapped llm.ErrIdentityMissing — parity with the react/repair planner's identity-rejection sentinel.

**Reusable (D-025).** Safe to invoke concurrently against a single shared runner from N goroutines. Per-call state lives in ctx + rc + the per-call serialised bytes; the receiver is read-only.

type ControlSignals

type ControlSignals struct {
	// Cancelled is true when a CANCEL control event was observed for
	// this run. The Planner SHOULD return Finish{Reason: Cancelled}
	// at the next step boundary.
	Cancelled bool
	// PauseRequested is true when a PAUSE control event was observed.
	// The Planner SHOULD return RequestPause{Reason: AwaitInput} at
	// the next step boundary.
	PauseRequested bool
	// InjectedContext carries INJECT_CONTEXT payloads accumulated
	// since the last planner step. The Planner SHOULD merge them
	// into its prompt construction.
	InjectedContext []map[string]any
	// UserMessages carries USER_MESSAGE payloads accumulated since
	// the last planner step.
	UserMessages []string
	// RedirectGoal is non-empty when a REDIRECT control event
	// updated the goal between planner steps.
	RedirectGoal string
}

ControlSignals carries the steering observations the planner sees at step start. The Runtime owns the inbox; the Planner reads.

Phase 42 ships a minimal struct — the unified pause/resume primitive + steering subsystem (later phases) populate the fields. Concrete signals (Cancel, Pause, Approve, Reject, InjectContext, Redirect, UserMessage, Prioritize, External) are observed via the typed slices; planners react in their Next implementation.

type Decision

type Decision interface {
	// contains filtered or unexported methods
}

Decision is the sealed sum-type a planner returns from Next. Six shapes ship at Phase 42 (RFC §6.2):

  • CallTool: invoke one tool with structured args.
  • CallParallel: invoke N tools in parallel with a join spec.
  • SpawnTask: spawn a background task (retain-turn or non-retain-turn).
  • AwaitTask: block the planner until a spawned task resolves.
  • RequestPause: pause the run for approval / input / external event.
  • Finish: terminal decision with a reason + payload.

The interface is sealed via the unexported `isDecision()` marker — adding a seventh shape requires editing this file. The predecessor's "magic strings as next_node" anti-pattern is explicitly rejected here (RFC §6.2 settled decisions); each shape is its own Go type.

`NoOp` is deliberately absent (resolves brief 02 Q-5). Wait-for- steering and trajectory-summarisation are Runtime short-circuits, not planner decisions.

type DecisionPayload

type DecisionPayload struct {
	events.SafeSealed
	Identity       identity.Quadruple
	DecisionKind   string
	Tool           string
	ReasoningChars int
	ReasoningTrace string
	OccurredAt     time.Time
}

DecisionPayload is the typed payload for EventTypePlannerDecision. Phase 42 registered the event type; Phase 83e (D-147) ships the first emitter (the ReAct planner) AND the payload — satisfying the §13 primitive-with-consumer rule for the long-registered type. SafePayload — every field is operator-visible debug data:

  • `Identity` is the run's identity quadruple.
  • `DecisionKind` is the resolved Decision shape name (`CallTool`, `CallParallel`, `Finish`, `SpawnTask`, `AwaitTask`, `RequestPause`).
  • `Tool` is the tool name when DecisionKind is `CallTool`; empty otherwise.
  • `ReasoningChars` is the rune count of the captured reasoning trace — a scannable size signal that never carries raw content.
  • `ReasoningTrace` is the provider-side thinking trace captured for the step (Phase 83e). Reasoning can be sensitive; the event is published onto the bus where the audit redactor processes it before any sink persists it (CLAUDE.md §7). `inspect-runs` surfaces it as `steps[].reasoning_trace`.

The emit is the observability surface that lets `harbor inspect-runs` reconstruct a run's reasoning channel from the event stream.

type ErrToolContextLost

type ErrToolContextLost = trajectory.ErrToolContextLost

ErrToolContextLost is the fail-loudly sentinel returned by HandleRegistry.Get on a missing handle. Re-exported from the canonical subpackage.

type ErrUnserializable

type ErrUnserializable = trajectory.ErrUnserializable

ErrUnserializable is the fail-loudly sentinel returned by Trajectory.Serialize on a non-JSON-encodable leaf. Re-exported from the canonical subpackage; use errors.As to extract the Field path.

type Factory

type Factory func(cfg PlannerConfig, deps FactoryDeps) (Planner, error)

Factory builds a Planner from a PlannerConfig + FactoryDeps. Drivers self-register one Factory each via init() → MustRegister.

A factory MUST fail closed on missing required deps (drivers that need an LLM MUST reject a nil `deps.LLM`); the V1 `react` driver's constructor panics on nil LLM per its existing contract. Custom drivers MUST honour the same fail-loud contract — silent fallback is forbidden per §13.

type FactoryDeps

type FactoryDeps struct {
	// LLM is the composed LLM client (retry → downgrade → corrections →
	// safety → driver per D-043). The V1 `react` driver consumes this
	// directly; drivers that don't use an LLM ignore the field.
	LLM llm.LLMClient
}

FactoryDeps bundles the shared collaborators every planner driver consumes at construction. The dev stack constructs the LLM client ONCE (one provider per binary; see `cmd/harbor/cmd_dev.go::bootDevStack`) and passes the same instance into every factory call.

Future drivers may need additional collaborators (a tasks subsystem handle for the deterministic planner, a sub-planner registry for the supervisor planner). Add fields here as drivers land; keep the boundary narrow so a planner driver never reaches into runtime internals (the §13 import-graph rule for `internal/planner/...`).

type FailureRecord

type FailureRecord = trajectory.FailureRecord

FailureRecord is the structured-failure projection (Phase 44 repair).

type Finish

type Finish struct {
	Reason   FinishReason
	Payload  any
	Metadata map[string]any
}

Finish is the terminal decision. The Runtime maps FinishReason → Protocol `task.completed` / `task.failed` payloads; `Payload` carries the planner's terminal observation (a summary string, a structured answer, an ArtifactRef — heavy payloads MUST be ArtifactRef-shaped per D-026).

`Reason` MUST be one of the canonical values (see IsValidFinishReason). The Runtime rejects an invalid reason with ErrInvalidDecision.

type FinishReason

type FinishReason string

FinishReason is the planner-side enum for the terminal reason a run finished. The Runtime maps FinishReason → Protocol `task.completed` / `task.failed` payloads.

const (
	// FinishGoal — the planner satisfied the user goal. The
	// canonical success terminal.
	FinishGoal FinishReason = "goal"
	// FinishNoPath — the planner could not find a path to the goal
	// (schema repair exhausted, no tool satisfies the requirement,
	// constraint conflict). Phase 44 emits this from the repair
	// pipeline's graceful-failure path.
	FinishNoPath FinishReason = "no_path"
	// FinishCancelled — the run was cancelled (CANCEL control event,
	// parent task cascade, deadline expiration honoured early).
	FinishCancelled FinishReason = "cancelled"
	// FinishDeadlineExceeded — the run hit its Budget.Deadline.
	FinishDeadlineExceeded FinishReason = "deadline_exceeded"
	// FinishConstraintsConflict — the run terminated because a
	// constraint conflict could not be resolved (operator denied an
	// approval; budget cap reached during a required tool call).
	FinishConstraintsConflict FinishReason = "constraints_conflict"
)

Finish reasons.

type HandleID

type HandleID = trajectory.HandleID

HandleID is the opaque key for a non-serialisable tool-context handle. Re-exported from the canonical subpackage.

type HandleRegistry

type HandleRegistry = trajectory.HandleRegistry

HandleRegistry holds the non-serialisable half of ToolContext. V1 ships a process-local driver; the distributed driver is a post-V1 RFC concern.

func NewProcessLocalRegistry

func NewProcessLocalRegistry() HandleRegistry

NewProcessLocalRegistry constructs the V1 process-local HandleRegistry driver.

type InputArtifactView

type InputArtifactView struct {
	// ID is the content-addressed artifact identifier.
	ID string
	// MIME is the artifact's IANA media type. Drives the per-MIME
	// dispatcher in the materializer.
	MIME string
	// SizeBytes is the artifact's byte length (metadata, not the bytes).
	SizeBytes int64
	// Filename is the operator-supplied original filename, if known.
	// Surfaced to the LLM as a hint when the provider supports it.
	Filename string
	// Bytes is the materialized payload, populated by the run loop
	// for `image/*` MIMEs (the Path 1 inline-bytes case). Nil for
	// every other MIME (the LLM sees an ArtifactStub ref instead).
	Bytes []byte
}

InputArtifactView is a pre-resolved multimodal input artifact the planner consumes on its first turn. Round-7 F11 / D-166 — the run loop reads the operator-supplied IDs from `tasks.Task.InputArtifactIDs`, looks each one up in the ArtifactStore, and (for `image/*` MIMEs) pre-fetches the bytes so the planner can construct `llm.ImagePart` with `DataURL` inline without async I/O. For non-image MIMEs the `Bytes` slot stays nil — the planner emits an `ArtifactStub` text block instead, and the LLM routes to whichever tool advertises the MIME via `tools.Tool.HandlesMIME`.

The shape is intentionally narrow: the planner's prompt assembly is synchronous, so every field it needs lives here. Future expansion (e.g. summaries, audio waveform thumbnails) extends the struct rather than reaching back into the artifact store.

type JoinKind

type JoinKind string

JoinKind enumerates the parallel-result merge strategies.

const (
	// JoinAll waits for every branch to terminate before producing
	// the merged observation. The default.
	JoinAll JoinKind = "all"
	// JoinFirstSuccess returns the first successful branch; the rest
	// are cancelled. Failures are NOT cancelled until all branches
	// have terminated.
	JoinFirstSuccess JoinKind = "first_success"
	// JoinKeyed produces a keyed merge over the branches; the
	// MergeKeys slice gives the deterministic ordering.
	JoinKeyed JoinKind = "keyed"
	// JoinN waits for N branches to succeed, then cancels the
	// remaining branches. JoinSpec.N carries the threshold; the
	// executor validates 0 < N ≤ len(Branches) at setup time and
	// fails the call with ErrParallelInvalidJoin when out of range.
	// D-056 — Phase 47 introduces JoinN as the third explicit join
	// shape (JoinAll / JoinFirstSuccess / JoinN); JoinKeyed remains
	// a documented future surface (a future runtime phase merges
	// outputs by key).
	JoinN JoinKind = "n"
)

Join kinds.

type JoinSpec

type JoinSpec struct {
	// Kind is the join strategy. Phase 42 ships the constants;
	// Phase 47 ships the implementations.
	Kind JoinKind
	// MergeKeys is the deterministic merge ordering (only meaningful
	// for JoinKeyed).
	MergeKeys []string
	// N is the success threshold for JoinN — the executor waits until
	// N branches succeed, then cancels the remaining branches. Ignored
	// for any Kind other than JoinN. Values ≤ 0 fall back to JoinAll
	// semantics (the executor validates this at setup time).
	N int
}

JoinSpec describes how the Runtime merges N CallParallel branch results into a single observation the planner sees in the next trajectory step. Phase 47 ships the executor; Phase 42 ships the shape so concretes can compile against it.

type MaxStepsExceededPayload

type MaxStepsExceededPayload struct {
	events.SafeSealed
	Identity      identity.Quadruple
	MaxSteps      int
	StepsObserved int
	LastTool      string
	OccurredAt    time.Time
}

MaxStepsExceededPayload is the typed payload for EventTypePlannerMaxStepsExceeded. SafePayload — every field is operator-visible debug data, not secret-shaped:

  • `Identity` is the run's identity quadruple.
  • `MaxSteps` is the configured circuit-breaker cap that fired.
  • `StepsObserved` is the trajectory step count at the moment the breaker fired (always ≥ MaxSteps; equality is the typical case when the breaker is the load-bearing terminator).
  • `LastTool` is the most-recently-dispatched tool name (from the last trajectory step's Action), or empty when the trajectory was empty AND MaxSteps == 0 (a degenerate config).

Phase 45 ships the payload alongside the emit site; the emit is the load-bearing fail-loudly surface that makes the circuit breaker not silent (§13). D-051.

type MemoryBlocks

type MemoryBlocks struct {
	// External is the long-term / retrieved memory tier. Rendered into
	// the `<read_only_external_memory>` prompt section. Nil to omit.
	External any
	// Conversation is the short-term / session memory tier. Rendered
	// into the `<read_only_conversation_memory>` prompt section. Nil
	// to omit.
	Conversation any
}

MemoryBlocks carries the two memory tiers the ReAct planner injects into its system prompt with UNTRUSTED anti-prompt-injection framing (Phase 83d — D-146). Both fields are optional: a nil tier is omitted from the prompt entirely (no empty wrapper is rendered).

The fields are typed `any` deliberately. The Runtime's MemoryStore (Phase 23) and memory strategies (Phase 24) produce free-form structured blobs whose shape is policy-dependent — a struct, a map, a slice of entries. The prompt builder compact-JSON-encodes whatever is supplied; a value `json.Marshal` rejects fails loudly with ErrMemoryBlockUnserializable rather than degrading to an empty wrapper.

Identity contract: the Runtime MUST have already filtered each blob to the run's `(tenant, user, session)` scope before populating this struct. The prompt builder never re-applies identity filtering — it renders exactly what it is handed (RFC §6.6, CLAUDE.md §6).

type MemoryView

type MemoryView interface {
	// Snapshot returns the memory entries visible to the planner
	// step. The map shape is intentionally opaque at Phase 42 — the
	// production MemoryView adapter (later wave) defines the keying
	// convention. Empty map + nil error is the no-memory case.
	Snapshot(ctx context.Context) (map[string]any, error)
}

MemoryView is the planner-facing read view over the declared-policy memory snapshot. The Runtime constructs a MemoryView at planner-step start from the production MemoryStore + scoping policy; the Planner reads the snapshot, never queries the store directly.

type ParallelBranchObservation added in v1.2.0

type ParallelBranchObservation struct {
	// CallID is the provider-assigned tool-call identifier sourced from
	// the originating `CallParallel.Branches[Index].CallID`. The prompt
	// builder stamps it onto the matching `RoleTool` message's
	// ToolCallID so the assistant `tool_calls[i]` and its answer pair
	// up. Empty when the branch carried no provider ID (the renderer
	// then falls back to a deterministic synthetic ID keyed on Index).
	CallID string `json:"call_id,omitempty"`

	// Tool is the branch's tool name (same as CallParallel.Branches[Index].Tool).
	Tool string `json:"tool,omitempty"`

	// Index is the branch's position in the originating
	// `CallParallel.Branches` slice. The deterministic merge key: the
	// renderer pairs each branch's observation to its assistant
	// tool-call by Index regardless of CallID collisions.
	Index int `json:"index"`

	// Value is the branch's success result. For the raw aggregate it is
	// the untruncated tool value; for the LLM-facing aggregate it is the
	// D-026 projected form (heavy results replaced by an artifact-stub
	// summary). Nil on failure.
	Value any `json:"value,omitempty"`

	// Error is the branch's failure message. Non-empty only on failure
	// (resolve miss, args-validation failure, or tool Invoke error).
	Error string `json:"error,omitempty"`
}

ParallelBranchObservation is one branch's outcome inside a ParallelObservation. Exactly one of `Value` (success) or `Error` (failure) is populated — never both.

type ParallelObservation added in v1.2.0

type ParallelObservation struct {
	// Branches is the per-branch outcome slice in branch-index order.
	Branches []ParallelBranchObservation `json:"branches,omitempty"`
}

ParallelObservation is the aggregate observation a runtime ToolExecutor produces when it dispatches a CallParallel decision (Phase 107d — D-169). It carries one ParallelBranchObservation per branch, in branch-index order (JoinAll semantics), so the planner's trajectory replay can decompose it back into the N native `role:"tool"` messages the provider wire contract requires — exactly one answer per `tool_call_id`.

The shape lives in `internal/planner` (not the runtime executor's package) because BOTH sides of the round-trip consume it: the dev `ToolExecutor` (cmd/harbor) produces it as the step's Observation / LLMObservation, and the React prompt builder (`internal/planner/react`) reads it back to emit the per-branch `RoleTool` messages. Both packages already import `internal/planner`; placing the type here avoids a new cross-package dependency.

JSON-encodable: the trajectory persists Step.Observation / Step.LLMObservation across checkpoints, so every field carries a JSON tag and `Value` holds only JSON-encodable tool results.

type PauseReason

type PauseReason string

PauseReason is the planner-side enum mirroring RFC §6.3's pause taxonomy. The unified pause/resume primitive package (later phase) MAY canonicalise via a typedef bridge; the enum values match the canonical strings exactly.

const (
	// PauseApprovalRequired — a human needs to approve a planner-
	// chosen tool call before execution.
	PauseApprovalRequired PauseReason = "approval_required"
	// PauseAwaitInput — the planner needs additional input from the
	// user / supervisor before continuing.
	PauseAwaitInput PauseReason = "await_input"
	// PauseExternalEvent — the run is waiting on an external event
	// (webhook, scheduled trigger, A2A callback).
	PauseExternalEvent PauseReason = "external_event"
	// PauseConstraintsConflict — the planner detected a constraint
	// conflict (budget vs. tool requirement, identity scope mismatch)
	// that requires operator resolution.
	PauseConstraintsConflict PauseReason = "constraints_conflict"
)

Pause reasons (RFC §6.3 — settled).

type Planner

type Planner interface {
	Next(ctx context.Context, run RunContext) (Decision, error)
}

Planner is the swappable reasoning-policy contract. Implementations MUST be safe to share across N concurrent goroutines (D-025): a shared Planner instance receives many calls; per-run state lives in `ctx` + the `RunContext` argument, never on the receiver.

`Next` returns ONE Decision per call. The Runtime executes the decision and re-invokes Next with the resulting trajectory. The Runtime owns the loop; the Planner owns the policy.

func Resolve

func Resolve(ctx context.Context, cfg PlannerConfig, deps FactoryDeps) (Planner, error)

Resolve constructs the Planner for cfg by dispatching to the driver named in `cfg.Driver`. Returns wrapped `ErrDriverUnknown` when the driver has not registered; otherwise delegates to the driver's Factory (whose own validation surfaces fail-closed errors).

`ctx` is honoured for the construction itself; drivers should observe `ctx.Err()` between long phases of work (e.g. HTTP discovery probes — none for V1 react).

type PlannerConfig

type PlannerConfig struct {
	// Driver names the registered planner driver to resolve. Required;
	// the validator rejects empty + unknown values pre-boot.
	Driver string

	// MaxSteps is the optional circuit-breaker step cap. Zero =
	// driver default.
	MaxSteps int

	// ExtraGuidance is operator-supplied domain-specific guidance for
	// the rendered prompt's <additional_guidance> section (Phase 83a).
	// The V1 `react` driver maps it onto `react.WithSystemPromptExtra`;
	// other drivers ignore it. Empty by default.
	ExtraGuidance string

	// ReasoningReplay is the agent-configured reasoning-replay mode
	// (Phase 83e — D-148). Empty string resolves to
	// `ReasoningReplayNever` — replay is OFF unless an operator opts
	// in. The dev stack maps `config.PlannerConfig.ReasoningReplay`
	// onto this field; the react factory passes it to
	// `react.WithReasoningReplay`. The validator rejects non-canonical
	// values pre-boot.
	ReasoningReplay ReasoningReplayMode

	// MaxToolExamplesPerTool caps how many curated examples the ReAct
	// prompt's <available_tools> section renders per tool (Phase 83b —
	// D-144). Zero resolves to the react driver's default of 3; the
	// react factory passes it to `react.WithMaxToolExamplesPerTool`.
	// Other drivers ignore it.
	MaxToolExamplesPerTool int

	// ParallelToolCalls is the optional native-parallel tool-call knob
	// (Phase 107d — D-169). Nil (the default) resolves to `true` — the
	// react driver emits a native `CallParallel` for N>1 tool-calls in
	// one response. A non-nil `false` selects the Phase 107c
	// serialization fallback. The react factory passes the resolved
	// value to `react.WithParallelToolCalls` only when non-nil (the
	// planner's own default is already `true`). Other drivers ignore it.
	ParallelToolCalls *bool

	// Extra is the driver-specific extras map. Reserved for future
	// drivers' per-flow knobs; unused by the V1 `react` driver.
	Extra map[string]string
}

PlannerConfig is the boundary type the registry exposes to drivers. It mirrors `config.PlannerConfig` (the operator-facing YAML shape) but lives in `internal/planner` so concrete drivers can depend on it without forcing the `internal/config` package to import driver internals. The dev stack maps the YAML struct onto this struct at the boundary (D-095 precedent).

`MaxSteps` is the planner-side circuit-breaker cap. Zero (the loader-side and registry-side default) means "use the driver's internal default" — e.g. `react.DefaultMaxSteps` (12). Negative is rejected at the validator edge (`internal/config/validate.go`).

`Extra` is the per-driver opaque config map reserved for future per-flow knobs (e.g. a deterministic planner's scripted step sequence, a supervisor planner's sub-agent list). The V1 `react` driver ignores it. Future drivers consume their entries from `Extra` without changing the boundary's signature.

func ConfigFromOperator added in v1.3.0

func ConfigFromOperator(cfg config.PlannerConfig) PlannerConfig

ConfigFromOperator maps the operator-facing `config.PlannerConfig` onto the registry-facing `planner.PlannerConfig` boundary (D-103). Empty Driver defaults to "react" — the V1 reference planner — so a config that omits the planner block boots unchanged from the pre-D-103 hardcoded path. The boundary copy matches the D-095 OAuth-provider precedent (`internal/config` keeps its own struct shape; the subsystem owns the projection).

Three `config.PlannerConfig` fields are deliberately NOT part of this projection (they are run-loop / executor knobs, not planner- driver boundary fields — the parity test names each exclusion):

  • `SkillsContextMax` — resolved via `config.PlannerConfig.SkillsContextMaxResolved()` and consumed by the run loop's skill retrieval, not the planner driver.
  • `AbsoluteMaxSpawnDepth` — resolved via `config.PlannerConfig.SpawnDepthCap()` and consumed by the tool executor's SpawnTask depth clamp.
  • `PlanningHints` — projected via `HintsFromConfig` onto `RunContext.PlanningHints` per run, not onto the driver config.

type PlanningHints

type PlanningHints struct {
	// Constraints is free-form constraint text rendered verbatim.
	Constraints string
	// PreferredOrder lists tool names the planner should prefer to
	// invoke in the given order.
	PreferredOrder []string
	// ParallelGroups lists groups of tool names that may be invoked
	// in parallel — each inner slice is one independent group.
	ParallelGroups [][]string
	// DisallowTools lists tool names the planner must NOT invoke.
	DisallowTools []string
	// PreferredTools lists tool names the planner should prefer when
	// multiple tools satisfy the same goal.
	PreferredTools []string
	// Budget carries advisory budget caps. Nil = no budget hints.
	Budget *BudgetHints
}

PlanningHints carries runtime-supplied planning constraints the ReAct prompt builder renders into the `<planning_constraints>` section of the system prompt (Phase 83c — D-145). It is the operator/runtime steering surface: tenant-specific policy, or guiding the planner around a known-bad path, expressed as prompt content rather than Go code (brief 13 §2.5).

Every field is optional. An empty field is omitted from the rendered section entirely — never emitted as an empty line. A PlanningHints whose every field is empty renders the empty string, so the prompt builder omits the `<planning_constraints>` section.

PlanningHints is distinct from PlanningNudges (RunContext.Hints): nudges are the legacy parallel/transport hints the planner MAY honour; PlanningHints is the richer runtime-steering surface rendered directly into the prompt.

func HintsFromConfig added in v1.3.0

func HintsFromConfig(cfg config.PlannerPlanningHintsCfg) *PlanningHints

HintsFromConfig projects the YAML `config.PlannerPlanningHintsCfg` onto a `*PlanningHints`. Returns nil when the YAML block is empty — the per-task run loop then hands the planner a nil PlanningHints and the `<planning_constraints>` prompt wrapper is omitted entirely.

V1.1 projects only the two YAML-exposed fields (Constraints + PreferredTools). The richer Go-struct fields on PlanningHints (ParallelGroups, DisallowTools, Budget) remain reachable through a custom planner Option but not via harbor.yaml; see Phase 83f's plan risks/open-questions section.

type PlanningNudges

type PlanningNudges struct {
	// MaxParallel hints the maximum CallParallel branch count the
	// planner should produce. The Runtime's system cap
	// (absolute_max_parallel = 50, per RFC §6.2 / Phase 47) wins.
	// Zero means no hint.
	MaxParallel int
	// PreferTransport hints the planner should prefer tools of the
	// given transport kind when multiple tools satisfy the same
	// goal. Empty means no preference.
	PreferTransport string
}

PlanningNudges are caller-provided nudges the planner MAY honour. The Runtime hard caps win in every case.

Phase 83c renamed this type from `PlanningHints` to free that name for the richer runtime-supplied PlanningHints struct rendered into the `<planning_constraints>` prompt section (D-145). PlanningNudges stays the type of RunContext.Hints — the legacy parallel/transport nudge surface; PlanningHints is the new operator-steering surface.

type ReasoningReplayMode

type ReasoningReplayMode string

ReasoningReplayMode controls whether the ReAct planner re-injects a prior step's captured reasoning trace into the next turn's prompt (Phase 83e — D-148). The predecessor never replayed; Harbor's stance is never-replay default for ALL models, with a per-agent operator opt-in. V1 ships exactly two modes — no `provider_native` mode, because Bifrost's docs do not address signed-thinking-block round- trips.

const (
	// ReasoningReplayNever — the trajectory renderer emits prior
	// `{tool, args}` JSON only; captured reasoning is never re-injected
	// into prompts. The default for ALL models.
	ReasoningReplayNever ReasoningReplayMode = "never"
	// ReasoningReplayText — the trajectory renderer prepends each prior
	// step's captured reasoning trace as a text block ABOVE the prior
	// `{tool, args}` JSON in the assistant turn. Per-agent operator
	// opt-in for workloads that benefit from CoT continuity.
	ReasoningReplayText ReasoningReplayMode = "text"
)

Reasoning replay modes (D-148). The zero value is deliberately the empty string, which `EffectiveReasoningReplay` resolves to `ReasoningReplayNever` — replay is OFF unless an operator opts in.

func EffectiveReasoningReplay

func EffectiveReasoningReplay(rc RunContext, configured ReasoningReplayMode) ReasoningReplayMode

EffectiveReasoningReplay resolves the replay mode in effect for a planner step (D-148). The per-run `RunContext.ReasoningReplay` override wins when non-nil; otherwise the agent-configured `configured` value applies. An empty `configured` value — or an empty override — resolves to `ReasoningReplayNever`: replay is OFF unless an operator explicitly opted in. Any non-canonical value also resolves to `ReasoningReplayNever` (fail-closed; config validation already rejects bad values pre-boot, so this is defence in depth).

type RepairCounters

type RepairCounters struct {
	// FinishRepair counts consecutive `_finish` actions that failed
	// Phase 44 validation. Reset to 0 on a clean finish.
	FinishRepair int
	// ArgsRepair counts consecutive tool-call actions whose args
	// failed schema validation. Reset to 0 on a clean single action.
	ArgsRepair int
	// MultiAction counts consecutive turns that emitted more than one
	// JSON action block. Reset to 0 on a clean single action.
	MultiAction int
}

RepairCounters carries the per-run, across-step failure counters that drive the ReAct planner's escalating repair guidance (Phase 83c — D-145). Each counter tracks one class of LLM-output-format failure the runtime had to repair:

  • FinishRepair — a `_finish` action that failed Phase 44 validation (malformed finish args, missing answer).
  • ArgsRepair — a tool-call action whose args failed the tool's schema validation.
  • MultiAction — the LLM emitted more than one JSON action block in a single turn (multi-action / multi-JSON emission).

The runtime increments the matching counter when Phase 44's repair pipeline had to fix an output, and resets it when a clean turn lands at that surface (a clean finish resets FinishRepair; a clean single-action-with-valid-args turn resets ArgsRepair AND MultiAction). The ReAct prompt builder reads the counters per turn and merges escalating `reminder → warning → critical` guidance into the system prompt for that turn only — closing the across-step feedback loop Phase 44's per-step repair leaves open (brief 13 §2.2).

**Concurrent-reuse (D-145 + D-025).** RepairCounters lives on the per-run RunContext, NEVER on the `ReActPlanner` struct. The runtime constructs one RepairCounters per run and threads the same pointer through every per-step RunContext; the shared planner artifact stays immutable. A counter field on the planner would be a §13-forbidden mutable-state-on-a-compiled-artifact bug.

**Parallel-branch failures do NOT increment these counters.** A `parallel` plan whose branches fail at tool execution is a tool-execution failure, not an LLM-output-format failure — the counters track only the latter (Phase 83c non-goal; see repair_guidance.go).

type RepairExhaustedPayload

type RepairExhaustedPayload struct {
	events.SafeSealed
	Identity               identity.Quadruple
	Attempts               int
	ConsecutiveArgFailures int
	Reasons                []string
	OccurredAt             time.Time
}

RepairExhaustedPayload is the typed payload for EventTypePlannerRepairExhausted. SafePayload — every field is operator-visible debug data, not secret-shaped:

  • `Identity` is the run's identity quadruple.
  • `Attempts` is the total LLM re-asks the loop burned before giving up (1-based; 1 means the loop made the initial call, observed the failure, and exited without re-asking).
  • `ConsecutiveArgFailures` is the consecutive-arg-failure counter value at the moment of exhaustion. When it equals `RepairLoop.cfg.MaxConsecutiveArgFailures`, the storm-guard path fired; when it is less, the `RepairAttempts` budget exhausted.
  • `Reasons` is the truncated chain of validator failures seen across attempts (each entry capped to 256 chars by the loop).

Phase 44 ships the payload with the emit; Phase 49's conformance pack asserts the round-trip.

type RepairGuidanceInjectedPayload

type RepairGuidanceInjectedPayload struct {
	events.SafeSealed
	Identity   identity.Quadruple
	Tier       string
	Counter    string
	Count      int
	OccurredAt time.Time
}

RepairGuidanceInjectedPayload is the typed payload for EventTypePlannerRepairGuidanceInjected (Phase 83c — D-145). SafePayload — every field is operator-visible debug data, never secret-shaped:

  • `Identity` is the run's identity quadruple.
  • `Tier` is the escalation tier the builder rendered: `reminder` (counter == 1), `warning` (counter == 2), `critical` (counter >= 3).
  • `Counter` names the tripped counter: `finish`, `args`, or `multi_action`.
  • `Count` is the RepairCounters field value at render time.

The emit is the observability surface that lets the Console show an operator when the LLM is repeatedly producing malformed output — the across-step companion to `planner.repair_exhausted` (the per-step terminal). Phase 83c ships the payload alongside the first emitter (the ReAct prompt builder).

type RequestPause

type RequestPause struct {
	Reason  PauseReason
	Payload map[string]any
}

RequestPause asks the Runtime to pause the run for an external signal. The unified pause/resume primitive (later phase) drives the pause coordinator; the planner only signals "I need a pause" via this decision (RFC §3.3 + §6.3).

`Reason` MUST be one of the four canonical values (see IsValidPauseReason). The Runtime rejects an invalid reason with ErrInvalidDecision before the pause is issued.

`Payload` is sanitised and depth/size-bounded by the Runtime's pauseresume coordinator (RFC §6.3 — depth ≤ 6, ≤ 64 keys, etc.) before serialisation.

type ResumeHint

type ResumeHint = trajectory.ResumeHint

ResumeHint signals a resume continuation.

type RunContext

type RunContext struct {
	// Quadruple is the (tenant, user, session, run) identity scope.
	// The triple (Identity field) is the isolation boundary; RunID is
	// the per-execution scope inside a session.
	Quadruple identity.Quadruple

	// Query is the user-facing query that started the run. Set once
	// at run start; never mutated.
	Query string

	// Goal is the current planner-visible goal. May be redirected by
	// a REDIRECT control signal; the Runtime updates the field
	// between planner steps.
	Goal string

	// LLMContext is the visible-to-LLM context (memories, system
	// notes, prior turn summaries). The Runtime populates from the
	// MemoryView + steering injections; the Planner reads only.
	LLMContext map[string]any

	// ToolContext is the tool-only handle bundle. Phase 43 closes
	// the split (serialisable half + handle registry); Phase 42
	// ships the skeleton.
	ToolContext ToolContext

	// Trajectory is the append-only execution log. The Planner reads
	// the history (compaction artefact, prior steps); the Runtime
	// appends each step. Phase 43 closes the fail-loudly Serialize
	// contract; Phase 42 ships the type skeleton with a stub Serialize
	// that returns ErrTrajectoryNotImplemented.
	Trajectory *Trajectory

	// Hints are caller-provided ordering / parallel / budget nudges.
	// Planners MAY honour them; the Runtime enforces the hard caps
	// (system absolute_max_parallel, identity-tier budget) outside
	// the planner.
	Hints PlanningNudges

	// RepairCounters carries the per-run across-step failure counters
	// the runtime increments when Phase 44's schema-repair pipeline had
	// to fix an LLM-output-format failure, and resets on a clean turn at
	// that surface (Phase 83c — D-145). Nil means "no augmentation": the
	// ReAct prompt builder renders no repair guidance.
	//
	// The pointer is owned by the runtime, which constructs ONE
	// RepairCounters per run and threads the SAME pointer through every
	// per-step RunContext. The counters live here — on the per-run
	// scope — and NEVER on the planner struct: a mutable counter field
	// on the shared `ReActPlanner` artifact would violate the D-025
	// concurrent-reuse contract (CLAUDE.md §5 + §13). See D-145.
	RepairCounters *RepairCounters

	// PlanningHints carries runtime-supplied planning constraints the
	// ReAct prompt builder renders into the `<planning_constraints>`
	// section (Phase 83a anchor; Phase 83c body — D-145). Nil means "no
	// hints": the optional section is omitted from the prompt entirely.
	//
	// Unlike Hints (caller nudges the planner MAY honour), PlanningHints
	// is operator/runtime steering: tenant-specific policy, or guiding
	// the planner around a known-bad path, without forking the prompt
	// (brief 13 §2.5). The Runtime populates it from the per-run options;
	// the planner reads only.
	PlanningHints *PlanningHints

	// Catalog is the planner-facing tool view (schemas only — never
	// Descriptors). Phase 26 ships the production ToolCatalog; the
	// runtime engine phase wires a ToolCatalogView adapter.
	Catalog ToolCatalogView

	// Memory is the declared-policy memory view. RESERVED for future
	// planner drivers that pull memory mid-run — no shipped planner
	// concrete reads it (the react planner consumes the pre-fetched
	// `MemoryBlocks` instead; see the type godoc + SDK friction audit
	// §3). Assemblers populate `MemoryBlocks`; wiring this view is
	// optional until a driver consumes it.
	Memory MemoryView

	// Skills is the skills subsystem's search/get surface. RESERVED
	// for future planner drivers that search skills mid-run — no
	// shipped planner concrete reads it (the react planner consumes
	// the pre-fetched `SkillsContext` instead). Assemblers populate
	// `SkillsContext`; wiring this view is optional until a driver
	// consumes it.
	Skills SkillLookup

	// Artifacts is the artifact store. Heavy outputs MUST round-trip
	// through ArtifactRef per D-022 + D-026; the planner is the
	// caller responsible for upgrading inline bytes to refs at the
	// LLM edge.
	Artifacts artifacts.ArtifactStore

	// InputArtifacts (Round-7 F11 / D-166) carry the operator-uploaded
	// multimodal inputs the run consumes on its FIRST planner turn —
	// pre-resolved by the run loop (no async I/O inside the planner).
	// The planner's first-turn user-content builder routes each entry
	// by MIME: `image/*` materializes as `llm.ImagePart{DataURL}` with
	// inline base64 bytes (Path 1, D-166); everything else stays as
	// an `ArtifactStub` the LLM routes through the tool catalog.
	// Empty on text-only turns AND on every turn after the first.
	InputArtifacts []InputArtifactView

	// Control is the accumulated steering signals (control events
	// observed since the last planner step). The Planner reads;
	// the Runtime owns the inbox.
	Control ControlSignals

	// Budget is the per-run hard caps: deadline, hop budget, cost cap.
	// The Runtime enforces them; the Planner observes them to make
	// budget-aware decisions.
	Budget Budget

	// Clock is the (typically `time.Now`) clock the Planner reads. A
	// controllable clock lets tests fix time across a planner step.
	Clock func() time.Time

	// Emit publishes onto the event bus with the run's identity
	// quadruple already attached. The Planner uses Emit to surface
	// planner-side telemetry (`planner.decision`, `planner.finish`,
	// `planner.error` — see events.go). May be nil in tests; concretes
	// MUST nil-check.
	Emit func(events.Event)

	// ReasoningReplay is a per-run override for the agent's reasoning-
	// replay policy (Phase 83e — D-148). When nil, the planner uses
	// the agent's configured `config.PlannerConfig.ReasoningReplay`.
	// When non-nil, this value wins — letting a tenant-specific or
	// run-specific policy override the per-agent default (e.g. an
	// operator disabling replay for a cost-sensitive run). The Runtime
	// populates it from the per-run options; the planner reads only.
	ReasoningReplay *ReasoningReplayMode

	// MemoryBlocks carries the pre-fetched, identity-scoped memory
	// blobs the planner injects into the system prompt as UNTRUSTED-
	// framed `<read_only_*_memory>` sections (Phase 83d — D-146). The
	// Runtime populates this from the MemoryStore per its declared
	// scoping policy; the planner only renders. Nil means "no memory
	// to inject" — the wrappers are omitted entirely. The Runtime is
	// responsible for filtering the blob to the run's identity BEFORE
	// it reaches this field; the prompt builder never re-applies
	// identity filtering (RFC §6.6).
	MemoryBlocks *MemoryBlocks

	// SkillsContext carries pre-retrieved skill bodies the runtime
	// resolved for this run (Phase 83d — D-146). The planner renders
	// them into a single UNTRUSTED-framed `<skills_context>` section.
	// Nil/empty means "no skills to inject" — the section is omitted.
	// The element type is `any` for V1: callers may supply
	// `planner.Skill` values, maps, or operator-defined structs; the
	// renderer compact-JSON-encodes whatever is passed. The runtime,
	// not the planner, decides which skills land here (RFC §6.7).
	SkillsContext []any

	// OnReasoning is a per-step callback the Planner invokes with the
	// provider-side reasoning trace captured by the LLM call (Phase 83m
	// item 8). The Runtime sets it on each per-step RunContext so the
	// runloop can copy the trace onto `trajectory.Step.ReasoningTrace`
	// when it appends the step. May be nil — a planner that finds no
	// callback skips the emission silently (no observability surface
	// wired this run).
	//
	// Why a side-channel rather than a field on Decision: the Decision
	// sum is the planner→runtime instruction contract (CallTool,
	// CallParallel, SpawnTask, AwaitTask, RequestPause, Finish) and
	// must stay narrow so future planner concretes (Deterministic,
	// Workflow, Plan-Execute) implement it cleanly without populating
	// a field most planners never produce. Reasoning is per-step
	// observation, not per-step instruction — this seam matches.
	//
	// Concurrent-reuse (D-025): the callback closure is captured per
	// run on the runloop's stack; the planner reads it from rc, never
	// from itself. N concurrent runs see N independent closures.
	OnReasoning func(string)

	// OnAssistantContent is a per-step callback the Planner invokes
	// with the natural-language content the assistant emitted
	// alongside its tool_call on this step. The Runtime sets it on
	// each per-step RunContext so the runloop copies the preamble
	// onto `trajectory.Step.AssistantPreamble` when it appends the
	// step; the prompt builder then replays it as the assistant
	// message's content on subsequent turns so the model retains
	// its narrative thread.
	//
	// Under native tool-calling the response carries both a
	// `tool_calls` block AND a natural-language `content` field.
	// The Decision sum only encodes the tool_call; this callback
	// is the side-channel that ferries `content` onto the step.
	//
	// Same shape as OnReasoning: nil-safe, per-run closure (D-025),
	// invoked by the planner after each LLM call.
	OnAssistantContent func(string)

	// OnChunk is the per-step streaming callback the Planner invokes
	// per token delta from the LLM provider (Phase 107). The Runtime
	// sets it on each per-step RunContext so the runloop publishes
	// `llm.completion.chunk` events. May be nil — a planner without
	// streaming wired skips the emission silently.
	//
	// Concurrent-reuse (D-025): same pattern as OnReasoning — per-run
	// closure on the stack, never on the shared planner artifact.
	OnChunk func(delta string, done bool, kind ChunkKind)

	// DiscoveredTools (Phase 107c / D-167) is the per-run list of
	// tool names the LLM discovered via meta-tools during this run.
	// The React planner reads this to add discovered tools to
	// subsequent turns' Tools[] declarations. Stack-local-per-run
	// (D-025) — never on the planner struct.
	DiscoveredTools []string

	// PendingToolCalls (Phase 107c / D-167) carries remaining
	// serialized native ToolCalls when the LLM emits N>1 calls in
	// one response (AC-19 serialization fallback). The planner
	// consumes PendingToolCalls before consulting the LLM again.
	// Stack-local-per-run (D-025).
	PendingToolCalls []ToolCallDeferred

	// OnPendingToolCalls (Phase 107c / D-167 — AC-19 + AC-19a) is the
	// per-step callback the planner invokes BEFORE returning a
	// Decision to surface the post-step `PendingToolCalls` queue back
	// to the runloop. rc is passed BY VALUE to Next; without this
	// bridge any append to `rc.PendingToolCalls` inside Next dies
	// with the planner's stack frame, and the AC-19 multi-ToolCall
	// serialisation fallback becomes dead code. The runloop captures
	// a stack-local slice via the closure (D-025: per-run, never on
	// the planner artifact) and writes it back into `spec.Base` so
	// the next iteration's value-copy carries the queue forward.
	//
	// Nil callback is a no-op (tests that exercise Next directly
	// without a runloop). Operators should never set it; the
	// runloop owns the closure.
	OnPendingToolCalls func([]ToolCallDeferred)

	// SessionArtifacts (Phase 107f — D-176) carries the pre-resolved,
	// identity-scoped session-artifact manifest the planner renders into
	// a read-only `<session_artifacts>` prompt block, so the model stays
	// aware of uploads and tool-materialised artifacts across turns and
	// can `artifact_fetch` any of them by ref.
	//
	// The run loop lists `ArtifactStore.List` scoped to the run's
	// `(tenant, user, session)` triple each turn and maps each ref into
	// a metadata-only entry (the D-166 run-loop pre-resolution pattern —
	// the planner does NO I/O; it reads only from rc). Nil/empty means
	// "no manifest": the planner omits the `<session_artifacts>` block
	// entirely (no fabricated rows). Newest-first; the renderer caps the
	// rendered rows and appends an explicit "+K more" line on overflow
	// (never a silent truncation, CLAUDE.md §17.6).
	//
	// Concurrent-reuse (D-025): the manifest is per-run state on rc,
	// never on the shared planner artifact.
	SessionArtifacts []ArtifactManifestEntry
}

RunContext is the only surface the Planner sees. All fields are either value types, narrow read interfaces, or function closures — never concrete Runtime structs. The Runtime constructs a fresh RunContext per planner step; reading from `ctx` for cancellation, `Quadruple` for identity, and the view interfaces for tools / memory / skills / artifacts is the entire API surface.

The Runtime (assembler) is responsible for:

  • Wiring `Catalog` to a visibility-filtered ToolCatalogView.
  • Populating `MemoryBlocks` + `SkillsContext` with pre-fetched, identity-scoped content (Phase 83d — D-146). These are the fields the shipped planner concretes actually read; see the note on `Memory` / `Skills` below.
  • Wiring `Artifacts` to the production ArtifactStore.
  • Populating `Control` with the accumulated steering signals.
  • Setting `Budget` from the per-run options.
  • Providing `Clock` (typically `time.Now`).
  • Providing `Emit` that publishes onto the EventBus with the run's identity quadruple attached.

The `Memory` (MemoryView) and `Skills` (SkillLookup) view fields are reserved for future planner drivers that pull memory / skills mid-run: NO shipped planner concrete reads them today (the react planner consumes only the pre-fetched `MemoryBlocks` / `SkillsContext` — SDK friction audit, docs/notes/sdk-friction-audit.md §3). Assemblers should populate the 83d fields; wiring the views is optional until a driver consumes them.

The Planner is responsible for:

  • Reading from RunContext, never writing back.
  • Returning a Decision (one of six shapes — see decision.go).
  • Never blocking on the Runtime's internals. Long-running work ALWAYS goes through SpawnTask / AwaitTask, not via a goroutine spawned inside Next.

type Skill

type Skill struct {
	// ID is the skill's stable identifier (provider-namespaced).
	ID string
	// Name is the human-readable name.
	Name string
	// Description is the one-line summary the planner shows the LLM.
	Description string
	// Body is the skill's prompt-injection content.
	Body string
	// Tags categorise the skill for filtering / search.
	Tags []string
}

Skill is the planner-facing projection of a skill record. The production internal/skills package (Phase 37+) defines the full record shape; the planner only needs the Name / Description / Body to compose an LLM prompt and the optional ToolTemplates for auto-instantiated tools.

type SkillLookup

type SkillLookup interface {
	// Search returns up to `limit` skills matching `query`. Empty
	// slice + nil error is the no-match case.
	Search(ctx context.Context, query string, limit int) ([]SkillResult, error)

	// Get returns the full skill by id, or (nil, nil) on miss.
	Get(ctx context.Context, id string) (*Skill, error)
}

SkillLookup is the planner-facing read view over the skills subsystem. Phase 37 ships the production surface; Phase 42 declares the planner- facing shape so the planner package compiles without importing internal/skills (parallel fork at Wave 8 Stage A).

type SkillResult

type SkillResult struct {
	Skill
	// Score is the search backend's relevance score, in [0.0, 1.0].
	// Higher is more relevant.
	Score float64
}

SkillResult is the search projection — a hit with a relevance score.

type Source

type Source = trajectory.Source

Source records a citation / provenance entry. Re-exported.

type SpawnSpec

type SpawnSpec struct {
	// Description is the human-readable task description (audit +
	// observability).
	Description string
	// Query is the goal / prompt the spawned task should pursue.
	Query string
	// Priority is the task scheduling priority (-1000..1000). Zero
	// is the default mid-priority.
	Priority int
	// RetainTurn blocks the foreground turn on the spawned task's
	// group resolution. When true the planner WILL re-enter Next
	// only after the group reaches a terminal state. When false the
	// planner returns control to the runtime; the runtime consumes
	// WatchGroup to re-invoke the planner on resolution (D-032).
	RetainTurn bool
	// FailFast applies when SpawnTask creates a fresh group: cancels
	// remaining members when the first fails. Ignored when joining
	// an existing GroupID.
	FailFast bool
}

SpawnSpec is the planner-facing spawn descriptor. The Runtime maps it into a `tasks.SpawnRequest` (or `tasks.SpawnToolRequest`) at dispatch time; identity is filled from the run's quadruple.

At Phase 42 the shape carries only the fields the planner needs to specify; the Runtime fills the rest (Identity, IdempotencyKey, PropagateOnCancel, NotifyOnComplete). Future phases MAY extend this shape with additional planner-controlled fields.

type SpawnTask

type SpawnTask struct {
	Kind    tasks.TaskKind
	Spec    SpawnSpec
	GroupID tasks.TaskGroupID
}

SpawnTask spawns a background task. When `Spec.RetainTurn` is true the foreground turn blocks on the spawned task's group; when false the planner returns control to the runtime and consumes `tasks.TaskRegistry.WatchGroup` to learn when the group resolves (D-032 wake-on-resolution contract).

`GroupID` is optional — when empty, the runtime creates an ad-hoc single-member group; when non-empty, the task joins the existing group (cross-task fan-in pattern).

type SteeringInjection

type SteeringInjection = trajectory.SteeringInjection

SteeringInjection records a steering event the planner observed.

type Step

type Step = trajectory.Step

Step is the trajectory's per-step shape (action + observation + failure + streams). Re-exported from the canonical subpackage.

Note: pre-Phase-43, this type was named TrajectoryStep at the planner-package level. The Phase 43 rename to Step is part of the subpackage relocation; no external consumers of TrajectoryStep existed pre-Phase-43.

type StreamChunk

type StreamChunk = trajectory.StreamChunk

StreamChunk captures one chunk of a streaming output.

type Summariser

type Summariser interface {
	Summarise(ctx context.Context, rc RunContext, tr *Trajectory) (*TrajectorySummary, error)
}

Summariser is the runtime-side interface a configured compaction driver implements. The CompressionRunner calls Summariser.Summarise when the trajectory's token estimate exceeds Budget.TokenBudget.

Fail-loudly contract (CLAUDE.md §13): an error from Summarise propagates verbatim through CompressionRunner.MaybeCompress — no silent fall-through to raw history. Returning (nil, nil) is also a contract violation; the runner surfaces ErrEmptySummary so the bug is loud, not silent.

Implementations MUST be safe for concurrent use across runs (the runner is a reusable artifact per D-025; the summariser is called under the run's ctx from MaybeCompress).

The production implementation is the LLM-backed TrajectorySummariser in internal/llm/summarizer (Phase 111e, D-202): it binds an LLM client + a versioned compaction prompt, invokes llm.LLMClient.Complete over the trajectory's planner-facing projection (Phase 35 structured-output JSON-schema mode with the existing downgrade ladder), and parses the response into the five TrajectorySummary fields. The production call site is the steering RunLoop's step loop, which calls CompressionRunner.MaybeCompress at each step boundary when Budget.TokenBudget > 0 — wired from the `planner.token_budget` config knob by the runtime assembly.

type Summary

type Summary = trajectory.Summary

Summary is the trajectory's compaction artefact (Phase 46 summariser output). Pre-Phase-43 this was TrajectorySummary at the planner-package level; the subpackage rename to Summary is part of Phase 43.

type TokenEstimator

type TokenEstimator func(tr *Trajectory) (int, error)

TokenEstimator is the runner's pluggable token-count function. The default implementation (DefaultTokenEstimator) walks trajectory.Trajectory.Serialize bytes and returns `len/4 + 1` — mirroring internal/llm/tokens.go's `chars4Estimator` so the two estimators agree (single surface; no parallel implementation per §13). D-055.

Estimator errors propagate; an trajectory.ErrUnserializable from Serialize is the typical failure mode and is surfaced verbatim (Phase 43 fail-loudly contract).

type ToolCallDeferred added in v1.2.0

type ToolCallDeferred struct {
	Name   string
	Args   json.RawMessage
	CallID string
}

ToolCallDeferred is a pending native tool-call the planner will dispatch on the next step (AC-19 serialization fallback).

type ToolCatalogView

type ToolCatalogView interface {
	// Resolve returns the Tool by name and a presence bool. The Tool
	// value carries schemas, transport kind, side-effect class, and
	// cost / latency hints — everything the planner needs to make a
	// CallTool decision without reaching into the descriptor.
	Resolve(name string) (tools.Tool, bool)

	// List returns every tool visible to the run. The slice ordering
	// is the catalog's natural order (typically registration order);
	// planners that need a stable ordering MUST sort the result.
	List() []tools.Tool
}

ToolCatalogView is the planner-facing read view over the production ToolCatalog (Phase 26). The view exposes schemas only — never ToolDescriptors — so the planner cannot dispatch tools directly. The Runtime owns dispatch; the Planner returns CallTool decisions.

Implementations MUST already apply visibility filtering — the planner sees the set of tools the run's identity may call, not the full catalog.

type ToolContext

type ToolContext = trajectory.ToolContext

ToolContext is the split serialisable / handle-registry tool-handle bundle. Re-exported from the canonical subpackage.

type Trajectory

type Trajectory = trajectory.Trajectory

Trajectory is re-exported from the canonical subpackage.

type TrajectoryCompressedPayload

type TrajectoryCompressedPayload struct {
	events.SafeSealed
	Identity      identity.Quadruple
	StepsBefore   int
	StepsAfter    int
	TokenEstimate int
	OccurredAt    time.Time
}

TrajectoryCompressedPayload is the typed payload for EventTypeTrajectoryCompressed (Phase 46). SafePayload — every field is operator-visible debug data, not secret-shaped:

  • `Identity` is the run's identity quadruple.
  • `StepsBefore` is the trajectory step count when compression ran.
  • `StepsAfter` is the step count after compression. Phase 46 does NOT truncate the Steps slice; the runner only stamps Summary. `StepsAfter == StepsBefore` in V1; the field exists so future phases that truncate (free memory; trade observability for footprint) extend the schema without a payload-version bump.
  • `TokenEstimate` is the estimator's count at the moment the budget was breached.

The emit is the success-path observability surface that pairs with trajectory.compression_failed for the failure path; together they make compression observable in both directions (§13 fail-loudly). D-055.

type TrajectoryCompressionFailedPayload

type TrajectoryCompressionFailedPayload struct {
	events.SafeSealed
	Identity      identity.Quadruple
	StepsObserved int
	TokenEstimate int
	ErrorCode     string
	ErrorMessage  string
	OccurredAt    time.Time
}

TrajectoryCompressionFailedPayload is the typed payload for EventTypeTrajectoryCompressionFailed (Phase 46). SafePayload — the fields carry operator-visible debug data:

  • `Identity` is the run's identity quadruple.
  • `StepsObserved` is the trajectory step count at the moment of the failure.
  • `TokenEstimate` is the estimator's count when the failure happened (zero when the estimator itself failed).
  • `ErrorCode` classifies the failure into one of three buckets: `summariser_error` (the Summariser returned a non-nil error), `empty_summary` (the Summariser returned (nil, nil) — contract violation), `estimator_error` (the TokenEstimator returned an error, typically a Phase 43 ErrUnserializable surfaced through DefaultTokenEstimator's Serialize call).
  • `ErrorMessage` is the truncated original error message (capped at 256 chars to keep audit payloads bounded). Never carries raw trajectory content.

Phase 46 ships the payload + the emit; the bus subscribers observe the failure end-to-end. The emit is the load-bearing fail-loudly observability surface (§13 — silent degradation banned). D-055.

type TrajectorySummary

type TrajectorySummary = trajectory.Summary

TrajectorySummary is the canonical name for the trajectory's compaction artefact. Aliased onto the Phase 43 trajectory.Summary struct: same shape, the alias matches RFC §6.2 + the master-plan Phase 46 vocabulary so callers outside the trajectory package use the RFC name. Five fields per RFC §6.2: `Goals`, `Facts`, `Pending`, `LastOutputDigest`, `Note`. D-055.

type WakeAware

type WakeAware interface {
	WakeMode() WakeMode
}

WakeAware is the OPTIONAL interface a planner concrete may implement to declare its non-retain-turn wake strategy. The conformance pack (Phase 49) asserts the round-trip for the declared mode; observability + the Console surface the value.

A planner that does not implement WakeAware is treated as WakePush by the conformance pack (the safe default — no polling burden).

Implementations MUST return a constant value for the lifetime of the planner instance. WakeMode is identity, not capability — a concrete picks one mode at construction time.

type WakeMode

type WakeMode string

WakeMode names a planner concrete's chosen wake-on-resolution strategy. The constants match the canonical names documented at `internal/tasks/groups.go`.

const (
	// WakePush — the planner subscribes to WatchGroup; the runtime
	// re-invokes Next on group resolution. Lowest latency, lowest
	// LLM cost (no in-flight polls). Phase 45 ReAct uses WakePush.
	WakePush WakeMode = "push"

	// WakePoll — the planner skips WatchGroup and re-checks group
	// status deterministically on its own cadence. No subscription
	// required. Suits deterministic / workflow planners (Phase 48+).
	WakePoll WakeMode = "poll"

	// WakeHybrid — the main planner subscribes via WatchGroup (push)
	// AND a sidecar (small LLM / templater) polls intermediate state
	// to emit user-facing progress events between push deliveries.
	WakeHybrid WakeMode = "hybrid"
)

Wake modes (D-032 — settled).

func ResolveWakeMode

func ResolveWakeMode(p Planner) WakeMode

ResolveWakeMode returns the effective WakeMode for a planner instance: the planner's own WakeMode() if it implements WakeAware, otherwise WakePush as the documented default.

Callers (the conformance pack, observability emitters) use this helper to avoid scattering type assertions.

func (WakeMode) String

func (m WakeMode) String() string

String returns the canonical string form of the wake mode.

Directories

Path Synopsis
Package conformance ships the planner conformance pack.
Package conformance ships the planner conformance pack.
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).
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).
Package finish ships Harbor's stub Planner — a planner that always returns Finish{Reason: Goal}.
Package finish ships Harbor's stub Planner — a planner that always returns Finish{Reason: Goal}.
Package react ships Harbor's reference LLM-driven planner concrete (Phase 45 — RFC §6.2 + RFC §3.2 — the first concrete sitting on the `internal/planner.Planner` seam).
Package react ships Harbor's reference LLM-driven planner concrete (Phase 45 — RFC §6.2 + RFC §3.2 — the first concrete sitting on the `internal/planner.Planner` seam).
Package repair ships Harbor's reusable salvage / schema-repair / graceful-failure / multi-action-salvage ladder for planner steps (RFC §6.2, Phase 44 — see docs/plans/phase-44-schema-repair.md).
Package repair ships Harbor's reusable salvage / schema-repair / graceful-failure / multi-action-salvage ladder for planner steps (RFC §6.2, Phase 44 — see docs/plans/phase-44-schema-repair.md).
Package trajectory ships Harbor's append-only planner-execution log and the fail-loudly serialise contract that closes the predecessor's silent-context-loss bug.
Package trajectory ships Harbor's append-only planner-execution log and the fail-loudly serialise contract that closes the predecessor's silent-context-loss bug.

Jump to

Keyboard shortcuts

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