planner

package
v1.19.1 Latest Latest
Warning

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

Go to latest
Published: Jul 24, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Attachment disposition policy.

Disposition is a *choice* that belongs to the developer / operator / planner, not a fact the runtime hardcodes. Previously the per-MIME `switch` in `materializeOne` was the sole authority over how an uploaded attachment reached the model. This file turns that mechanism into declared policy:

  • AttachmentDisposition is the enum (`ref` / `inline` / `provider_native` / `tool:<name>`).
  • DispositionPolicy is the per-agent per-MIME map + default.
  • ResolveDisposition is the pure precedence resolver: per-attachment caller hint > per-agent policy map > runtime default. The layers are semantic; the carriers are adapters — the Protocol input-artifact `disposition` field is one carrier of the hint (direct `InputArtifactView.Disposition` construction by a headless library consumer is the other), and `harbor.yaml` is one carrier of the policy map (programmatic DispositionPolicy construction is the other).
  • EffectiveDisposition applies runtime capability constraints to a resolved disposition and *returns* any degradation as a typed fact — the resolver and materializer are pure; the CALLER (run loop, devstack, or a consumer's own loop) logs and emits.

The runtime default reproduces the legacy per-MIME dispatch byte-for-byte: `image/*` resolves `inline` (the sub-threshold DataURL fast path), everything else resolves `ref` (the `ArtifactStub` + `Fetch.Tool` hint the planner drives via native tool-calling).

Multimodal first-turn materialization.

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 of the multimodal design: 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.

Harbor ships the interface + the sum + the views + a stub finish.Planner that always returns Finish{Reason: Goal}. Harbor ships the reference ReAct concrete; Harbor ships the deterministic concrete. The conformance harness skeleton 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: 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: 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; the `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 —
	TaskErrorCodeCancelled = "cancelled"
	// TaskErrorCodeOutputInvalid marks a schema-constrained run whose
	// terminal answer failed output-schema validation after the
	// correction budget was spent — the task-shaped mirror of
	// [ErrOutputInvalid]. A run-loop driver stamps it on BOTH failure
	// shapes: a RunLoop.Run error whose chain carries the schema-retry /
	// downgrade exhaustion, and an edge-validation ErrOutputInvalid on a
	// returned goal Finish. A run carrying an output schema NEVER reaches
	// MarkComplete with an unvalidated envelope — it fails with this code
	// instead (CLAUDE.md §13: no silent fallback to unvalidated text).
	TaskErrorCodeOutputInvalid = "output_invalid"
)

Terminal task-error codes a run-loop driver stamps on `tasks.TaskError.Code` when a run ends without FinishGoal (exported for run-loop drivers).

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

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

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

	// EventTypePlannerRepairExhausted — emitted by the
	// 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
	// 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).
	EventTypePlannerMaxStepsExceeded events.EventType = "planner.max_steps_exceeded"

	// EventTypeTrajectoryCompressed — emitted by the
	// 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).
	EventTypeTrajectoryCompressed events.EventType = "trajectory.compressed"

	// EventTypeTrajectoryCompressionFailed — emitted by the
	// 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).
	EventTypeTrajectoryCompressionFailed events.EventType = "trajectory.compression_failed"

	// EventTypePlannerRepairGuidanceInjected — emitted by the
	// 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).
	EventTypePlannerRepairGuidanceInjected events.EventType = "planner.repair_guidance_injected"

	// EventTypePlannerActionExtraFieldDropped — emitted by the
	// repair loop when an incoming action object carried a field the
	// narrowed action schema no longer recognises (`reasoning`
	// / `thought`). 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. Harbor registers the constants with the events package's canonical registry so future concretes (ReAct, Deterministic, etc.) can emit without re-registering.

Most payload structs land at the phase that first emits each type — Harbor 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).

Harbor 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).

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 — 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 — 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). 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 — 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 — 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 — 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 — 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 — 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 — a branch requested
	// a pause mid-execution. The unified pause/resume primitive lands;
	// until then, the parallel executor MUST fail loud
	// per RFC §6.2's parallel-pause-atomicity contract. The unified pause primitive
	// upgrades this path to a checkpointed atomic pause.
	ErrParallelPauseUnsupported = errors.New("planner: CallParallel pause-mid-execution not supported by the unified pause primitive at V1")

	// ErrMemoryBlockUnserializable — 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 — 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. The earlier stub ErrTrajectoryNotImplemented is retired — Harbor 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.

View Source
var ErrInvalidDisposition = errors.New("planner: invalid attachment disposition")

ErrInvalidDisposition is returned by ParseDisposition for a value outside the disposition grammar (`ref` / `inline` / `provider_native` / `tool:<name>`).

View Source
var ErrOutputInvalid = errors.New("planner: terminal output failed output-schema validation")

ErrOutputInvalid is the typed sentinel a run-level structured-output run returns when the terminal answer fails validation against the caller-supplied output schema after the correction budget is spent. It wraps the underlying schema-validation error (and, when the generation-steering retry loop was engaged and exhausted, the github.com/hurtener/Harbor/internal/llm.ErrRetryExhausted chain). A schema-constrained run NEVER falls back to unvalidated text — the caller sees this error instead (CLAUDE.md §13). Callers compare via errors.Is.

Functions

func CountToolInvocations added in v1.9.0

func CountToolInvocations(t *Trajectory) int

CountToolInvocations counts the tool invocations ATTEMPTED across t's steps — the counting rule behind AnswerEnvelope.ToolCallsSeen. A CallTool step counts as one invocation; a CallParallel step counts as len(Branches) (the runtime dispatches N tools concurrently, not one); a Batch step counts as len(Tools) (its spawns are not tool dispatches); SpawnTask, AwaitTask, and any other step value count as zero — they are runtime decisions the planner made, never tool dispatches. A nil Trajectory counts as zero.

The Console Tasks-page tool_count uses the SAME per-decision counting rule (DecisionInvocationCount) but differs on the failure axis: the run loop skips its dispatch hook when the executor errors, so tool_count counts SUCCESSFUL dispatches only, while a failed dispatch still appends its step to the trajectory and is counted here. A run with three CallTool steps of which one failed reports ToolCallsSeen == 3 and tool_count == 2 — by design, not drift.

Producers call this on a LIVE, in-process trajectory, whose Step.Action values are the concrete Decision shapes the run loop appended. A trajectory rehydrated from serialized bytes carries map-shaped Actions (see Trajectory.Serialize) and would count 0 — any future cross-process-resume producer must re-type Actions or count before serialization, deliberately, not by accident.

This replaces an earlier len(t.Steps) reading, which silently miscounted in both directions: one CallParallel step holding N branches undercounted (reported 1 instead of N), and a SpawnTask / AwaitTask step overcounted (reported a dispatch that never happened).

func DecisionInvocationCount added in v1.9.0

func DecisionInvocationCount(action any) int

DecisionInvocationCount returns the number of tool invocations a single decision represents: 1 for CallTool, len(Branches) for CallParallel, len(Tools) for Batch (its Spawns contribute zero, matching SpawnTask's rule — a spawn is never a tool invocation), 0 for SpawnTask / AwaitTask / TaskStatusQuery / CancelTask / RequestPause / Finish / any other value (including a trajectory step restored from JSON without being re-typed through the Decision sum — e.g. a bare map[string]any). Typed-nil pointers count as zero.

action is typed `any`, not Decision: trajectory.Step.Action is `any` to avoid a planner<->trajectory import cycle (see Step's godoc); the type switch here is the seam that recovers the concrete Decision shape when the caller has one (a live, in-process Step.Action, or a Decision passed directly from a run-loop dispatch site).

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 safety pass), so the simpler walker is sufficient. A future estimator that structurally walks the trajectory is a later refinement; the TokenEstimator functional-option seam is the unwind point.

func EffectiveDisposition added in v1.4.0

func EffectiveDisposition(resolved AttachmentDisposition, mime string, catalog ToolCatalogView) (AttachmentDisposition, *DispositionDegradation)

EffectiveDisposition applies runtime capability constraints to a resolved disposition, returning the disposition the materializer will honour plus a typed degradation fact when they differ (nil when honoured as-is). Pure function — the caller logs/emits the fact.

Constraints at V1.1:

  • `provider_native` is honoured as-is — the materializer emits a `ProviderNative`-flagged typed part and the LLM driver performs the provider upload inside `Complete`. A provider without upload support for the modality degrades at the DRIVER (the `ArtifactStub` universal degradation, with a logged notice) — not here.
  • `tool:<name>` degrades to ref when the catalog view does not resolve the name (DegradationUnknownTool). A nil catalog skips the check — the forced tool is kept verbatim (the hint on the emitted stub is advisory to the LLM; a headless caller without a catalog view still gets the forced routing).
  • `inline` degrades to ref for non-`image/*` MIMEs (DegradationInlineUnsupportedMIME).
  • a non-grammar value degrades to ref (DegradationInvalidDisposition).

A zero disposition passes through unchanged — it selects the legacy materializer dispatch and there is nothing to degrade.

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 ResolveDisposition added in v1.4.0

ResolveDisposition is the pure precedence resolver: per-attachment caller hint > per-agent policy map > runtime default. It returns the resolved disposition AND the layer that won, so the caller can log/emit which disposition fired and why.

Pure function — no I/O, no logging, no state. The run loop (`cmd/harbor` + the devstack mirror) is a thin caller; a headless library consumer calls the same function, or skips it entirely by setting `InputArtifactView.Disposition` directly (the consumer IS the top-precedence layer).

The hint is trusted verbatim (carriers validate via ParseDisposition at their edge); capability constraints (unknown tool, non-image inline) are applied by EffectiveDisposition, which returns typed degradation facts.

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 ( ). 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 previously, so carrying no canonical `source` key) resolving to a real provenance instead of "unknown" — no back-fill migration needed.

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. 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 actual tool invocations the run
	// dispatched — computed via CountToolInvocations, NOT
	// len(Trajectory.Steps). A CallTool step counts once; a
	// CallParallel step counts once PER branch (the runtime dispatches
	// N tools concurrently, not one); SpawnTask and AwaitTask steps
	// count zero — they are runtime decisions, never tool dispatches.
	//
	// This field's semantics were redefined from an earlier len(Steps)
	// reading, which silently miscounted: undercounting a CallParallel
	// step (1 instead of N branches) and overcounting a SpawnTask /
	// AwaitTask step (a dispatch that never happened). This is a
	// value-semantics change on an SDK-visible field — embedders
	// comparing ToolCallsSeen against a fixed expectation should
	// re-check it after upgrading.
	ToolCallsSeen int `json:"tool_calls_seen"`
	// AnswerPayload is the schema-validated raw-JSON terminal answer of
	// a structured-output run — the run the caller opened with
	// assemble.WithOutputSchema. It is ADDITIVE and omitted (`omitempty`)
	// on plain runs, so the three pre-existing keys and their byte-shape
	// are untouched (the golden encoding test still pins them). When
	// present it carries the exact validated bytes captured at the
	// validation site (never a lossy string round-trip); Answer carries
	// the same payload's string rendering for backward compatibility.
	//
	// A structured-output run whose terminal answer failed validation
	// after the correction budget never reaches an envelope at all — the
	// runner returns ErrOutputInvalid instead (CLAUDE.md §13: no silent
	// fallback to unvalidated text).
	AnswerPayload json.RawMessage `json:"answer_payload,omitempty"`
}

AnswerEnvelope is the canonical JSON shape `tasks.TaskResult.Value` carries when a run-loop driver completes a task from a Finish with reason FinishGoal:

{"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 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.

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. 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 (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.

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 AttachmentDisposition added in v1.4.0

type AttachmentDisposition string

AttachmentDisposition declares how an uploaded input artifact is handed to the model. The zero value ("") means "no disposition declared" — the materializer falls back to the legacy per-MIME dispatch, so a headless consumer that never sets the field gets the default behaviour unchanged.

const (
	// DispositionRef emits an `ArtifactStub` by reference with the
	// existing `Fetch.Tool` hint — the developer-controllable
	// "process it myself via a tool" path. The runtime default for
	// every non-image MIME.
	DispositionRef AttachmentDisposition = "ref"
	// DispositionInline inlines the artifact bytes as a DataURL part.
	// V1.1 supports inline for `image/*` only (the sub-threshold
	// vision fast path); other MIMEs degrade to ref with a returned
	// [DispositionDegradation]. The runtime default for `image/*`.
	DispositionInline AttachmentDisposition = "inline"
	// DispositionProviderNative hands the artifact to the provider's
	// own understanding: the materializer emits a typed part with the
	// `ProviderNative` flag set, and the LLM driver uploads the
	// content to the provider's file surface inside `Complete`,
	// rewriting the part to an opaque `file_id` reference (RFC §6.5).
	// Opt-in only — never the runtime default. A provider without
	// upload support for the modality degrades loudly to the
	// `ArtifactStub` reference at the driver.
	DispositionProviderNative AttachmentDisposition = "provider_native"
)

func DefaultDisposition added in v1.4.0

func DefaultDisposition(mime string) AttachmentDisposition

DefaultDisposition is the runtime-default layer: the built-in map that reproduces the legacy materializer dispatch. `image/*` → DispositionInline (the sub-threshold DataURL fast path); everything else → DispositionRef (the `ArtifactStub` + `Fetch.Tool` path). Exported so a headless consumer can read the default the runtime would apply.

func DispositionTool added in v1.4.0

func DispositionTool(name string) AttachmentDisposition

DispositionTool builds the parametrised `tool:<name>` disposition that forces the named catalog tool via the `ArtifactStub.Fetch.Tool` mechanism (e.g. `DispositionTool("pdf.extract")`).

func ParseDisposition added in v1.4.0

func ParseDisposition(s string) (AttachmentDisposition, error)

ParseDisposition validates a wire / config carrier string against the disposition grammar and returns the typed value. The carriers (Protocol `disposition` hint, `harbor.yaml` map values) call this at their edge so the runtime only ever sees canonical values; a headless consumer constructing AttachmentDisposition directly may skip it (garbage degrades loudly at EffectiveDisposition).

`internal/config/validate.go::validateMultimodal` mirrors this grammar locally (the import direction forbids config → planner); `TestParseDisposition_ConfigGrammarLockstep` pins the two in lockstep.

func (AttachmentDisposition) IsZero added in v1.4.0

func (d AttachmentDisposition) IsZero() bool

IsZero reports whether no disposition was declared. A zero disposition selects the legacy per-MIME materializer dispatch.

func (AttachmentDisposition) ToolName added in v1.4.0

func (d AttachmentDisposition) ToolName() (string, bool)

ToolName returns the forced tool name and true when d is a `tool:<name>` disposition with a non-empty name.

func (AttachmentDisposition) Valid added in v1.4.0

func (d AttachmentDisposition) Valid() bool

Valid reports whether d is a canonical disposition value: one of the three fixed values or a `tool:<name>` with a non-empty name. The zero value is NOT valid — callers that accept "unset" check AttachmentDisposition.IsZero first.

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 Batch added in v1.16.0

type Batch struct {
	// Tools are the catalog-tool branches, dispatched concurrently
	// and joined per Join (nil collapses to JoinAll, matching the
	// native-parallel path).
	Tools []CallTool
	// Spawns are the task spawns; every entry's Spec.RetainTurn is
	// false.
	Spawns []SpawnTask
	// Join governs ONLY Tools. It is nil (JoinAll) when Tools is
	// empty — a spawns-only Batch carries no join.
	Join *JoinSpec
}

Batch groups zero-or-more catalog-tool branches with zero-or-more task spawns projected from ONE native multi-call LLM response — the shape a projector constructs when a model batches a `_spawn_task` call alongside an ordinary tool call (or alongside other spawns) in a single response.

Batch is a distinct fourth dispatch shape, NOT a widening of CallParallel: tool-invocation accounting counts Tools and Spawns separately (a spawn is never a tool invocation — see DecisionInvocationCount, which returns len(Tools) for a Batch and counts Spawns as zero). Reserved-control terminal/blocking names (`_finish` / `_await_task`) are never Batch members; only catalog tools and non-retain-turn spawns are.

Invariants (enforced by NewBatch, failing loud on violation):

  • len(Tools)+len(Spawns) >= 2. A single-branch would-be Batch is degenerate; producers construct the plain CallTool / SpawnTask / CallParallel shape instead (one representation per semantic).
  • Every Spawns[i].Spec.RetainTurn is false. A turn-retaining spawn inside a non-blocking multi-dispatch is a contradiction.

func NewBatch added in v1.16.0

func NewBatch(tools []CallTool, spawns []SpawnTask, join *JoinSpec) (Batch, error)

NewBatch validates and constructs a Batch, failing loud (wrapping ErrInvalidDecision) on a degenerate batch (fewer than two combined Tools+Spawns branches) or any retain-turn spawn. Every producer of a Batch — the React projector today, future concrete planners — routes through this constructor so the structural invariants hold at every call site.

NewBatch validates STRUCTURAL invariants only. Semantic checks that need projection context (e.g. FailFast disagreement across auto-grouped spawns) live at the producing projector, and the operator-configured breadth cap lives at the dispatch edge.

type BatchObservation added in v1.16.0

type BatchObservation struct {
	// Tools is the per-tool-branch outcome slice, index-aligned to
	// [Batch.Tools]. It reuses [ParallelBranchObservation] verbatim —
	// the tool half of a Batch dispatches through the SAME executor path
	// a [CallParallel] does, so its per-branch observation shape is
	// identical.
	Tools []ParallelBranchObservation `json:"tools,omitempty"`

	// Spawns is the per-spawn-branch REGISTRATION outcome slice,
	// index-aligned to [Batch.Spawns].
	Spawns []BatchSpawnObservation `json:"spawns,omitempty"`
}

BatchObservation is the aggregate observation a runtime ToolExecutor produces when it dispatches a Batch decision. A batch has two heterogeneous halves — catalog-tool branches and task spawns — and this observation carries both, each index-aligned to the originating Batch.Tools / Batch.Spawns declaration order REGARDLESS of the order the branches or spawns actually complete in. The caller MUST index or look up by CallID / Index to reconstruct per-`call_id` replies — never range a Go map — because provider tool-calling wire contracts require exactly one reply per `call_id`, some of them order-sensitive.

The shape lives in `internal/planner` (not the runtime executor's package) for the same reason ParallelObservation does: BOTH sides of the round-trip consume it — the production ToolExecutor produces it as the step's Observation / LLMObservation, and the React prompt builder reads it back to emit the per-branch `role:"tool"` messages. Both packages already import `internal/planner`, so 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 the embedded tool `Value`s hold only JSON-encodable results.

type BatchSpawnObservation added in v1.16.0

type BatchSpawnObservation struct {
	// CallID is the provider-assigned tool-call identifier sourced from
	// the originating [Batch.Spawns][Index].CallID (the native
	// `_spawn_task` call's id). The prompt builder stamps it onto the
	// matching `role:"tool"` message's ToolCallID. Empty when the spawn
	// carried no provider id (a programmatic spawn); the renderer then
	// falls back to a deterministic synthetic id keyed on Index.
	CallID string `json:"call_id,omitempty"`

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

	// TaskID is the registered task's id on a successful registration.
	// Empty on a registry reject.
	TaskID string `json:"task_id,omitempty"`

	// GroupID is the registered task's group id on a successful
	// registration — either the spawn's explicit group or the one
	// auto-created group the executor resolves for a batch's ungrouped
	// spawns. Empty on a registry reject, and empty for a single
	// ungrouped spawn that kept the ad-hoc single-member-group path.
	GroupID string `json:"group_id,omitempty"`

	// Error is the spawn's registration failure message. Non-empty only
	// on a registry reject; empty on success.
	Error string `json:"error,omitempty"`
}

BatchSpawnObservation is one Batch.Spawns entry's REGISTRATION outcome. "Complete" here means REGISTERED, not terminal: a spawn's eventual result arrives later via the task registry's group-watch path, never through this observation — so a spawn is never reported as "not done yet". Exactly one of (TaskID+GroupID) or Error is populated: a successful registration carries the assigned task id and its group id; a registry reject (depth-cap exceeded, sealed group, malformed request) carries that spawn's error as a value, so every `call_id` is answered even when one spawn is rejected.

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 (seam; consumer —).
	// Zero means no token-budget enforcement; the trajectory
	// grows unbounded (until the 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). 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. 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 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; Harbor ships the executor).

Branches share the same step-level pause/cancel atomicity contract see the 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
	// 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; the planner does not block on the call.

The action shape is intentionally narrow — `{tool, args}` only. An earlier phase 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, never a schema field.

type CancelTask added in v1.16.0

type CancelTask struct {
	// TaskID is the descendant to cancel.
	TaskID tasks.TaskID
	// Reason is the human-readable cancellation reason recorded on the
	// emitted task.cancelled event.
	Reason string
}

CancelTask cancels one background task THIS run spawned — the model's own judgment applied to work it started (e.g. cancelling the losing branches of a fan-out once the first answered). A non-terminal, non-blocking decision dispatched like CallTool; the planner observes the {task_id, cancelled} outcome on its next turn.

TaskID must reach the calling run's own task by walking the parent-task chain upward — a run can cancel only its own descendants, never a sibling run's tasks in the same session. A cancel on a task the run spawned under an isolate propagation policy still succeeds: a direct cancel is never gated on the target's own propagation mode — isolate only detaches a task from an ANCESTOR's cascade, never from a direct cancel by the run that spawned it or by the operator.

type ChunkKind added in v1.2.0

type ChunkKind string

ChunkKind is a sealed enum for the streaming-chunk kind. Values: ChunkContent (model output text), ChunkReasoning (thinking trace — 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 (one MaybeCompress call per step, gated on Budget.TokenBudget > 0 and a configured runner).

Reusable artifact: 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 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).** A partial quadruple returns wrapped llm.ErrIdentityMissing — parity with the react/repair planner's identity-rejection sentinel.

**Reusable.** 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.

Harbor 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. The shapes ship per RFC §6.2:

  • CallTool: invoke one tool with structured args.
  • CallParallel: invoke N tools in parallel with a join spec.
  • Batch: one native multi-call response mixing catalog-tool branches with non-retain-turn task spawns.
  • SpawnTask: spawn a background task (retain-turn or non-retain-turn).
  • AwaitTask: block the planner until a spawned task resolves.
  • TaskStatusQuery: observe the state of tasks this run spawned.
  • CancelTask: cancel one task this run spawned.
  • SteerTask: steer one background task this run spawned.
  • PauseTask: pause one background task this run spawned.
  • ResumeTask: resume one paused background task this run spawned.
  • 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 further 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. 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. registered the event type; Harbor 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, persisted VERBATIM. Because DecisionPayload embeds SafeSealed (a SafePayload), the event bus SKIPS the audit redactor for it — the trace a sink persists is byte-identical to the one the enricher captured. Reconstruction of a run's reasoning channel relies on that fidelity, so the trace is treated as operator-only debug data, never routed to an untrusted surface. `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 DispositionDegradation added in v1.4.0

type DispositionDegradation struct {
	// From is the disposition that was requested / resolved.
	From AttachmentDisposition
	// To is the disposition actually applied (always ref at V1.1).
	To AttachmentDisposition
	// Reason is the typed degradation cause.
	Reason DispositionDegradationReason
	// Tool carries the unknown tool name when Reason is
	// [DegradationUnknownTool].
	Tool string
}

DispositionDegradation is the typed degradation fact EffectiveDisposition returns when a resolved disposition cannot be honoured. The resolver never logs — the caller (run loop, devstack, or a consumer's own loop) logs and emits the fact (fail-loud, never a silent drop — CLAUDE.md §13).

type DispositionDegradationReason added in v1.4.0

type DispositionDegradationReason string

DispositionDegradationReason is the typed vocabulary of why a resolved disposition could not be honoured and degraded to DispositionRef.

const (
	// DegradationUnknownTool — a `tool:<name>` disposition named a
	// tool absent from the run's catalog view. Degrades to ref so the
	// run proceeds; the caller logs the warning (revisit as a hard
	// error if operators prefer — plan §risks).
	DegradationUnknownTool DispositionDegradationReason = "unknown_tool"
	// DegradationInlineUnsupportedMIME — `inline` resolved for a
	// non-`image/*` MIME; V1.1 inlines images only.
	DegradationInlineUnsupportedMIME DispositionDegradationReason = "inline_unsupported_mime"
	// DegradationInvalidDisposition — a directly-constructed
	// disposition value outside the grammar reached resolution.
	DegradationInvalidDisposition DispositionDegradationReason = "invalid_disposition"
)

type DispositionLayer added in v1.4.0

type DispositionLayer string

DispositionLayer names the precedence layer that resolved an attachment's disposition — returned by ResolveDisposition so the caller can record which layer won (the audit criterion: operators see the runtime never silently overrode them).

const (
	// DispositionLayerCallerHint — the per-attachment caller hint won
	// (the Protocol `disposition` field, or a directly-set
	// `InputArtifactView.Disposition` upstream of the resolver).
	DispositionLayerCallerHint DispositionLayer = "caller_hint"
	// DispositionLayerAgentPolicy — the per-agent policy map won
	// (`harbor.yaml` `multimodal.disposition`, or a programmatic
	// [DispositionPolicy]).
	DispositionLayerAgentPolicy DispositionLayer = "agent_policy"
	// DispositionLayerRuntimeDefault — the built-in runtime default
	// won: `image/*` → inline, everything else → ref (byte-for-byte
	// the legacy per-MIME dispatch).
	DispositionLayerRuntimeDefault DispositionLayer = "runtime_default"
)

type DispositionPolicy added in v1.4.0

type DispositionPolicy struct {
	// ByMIME maps a MIME key to its disposition. Keys are an exact
	// IANA type (`application/pdf`) or a family wildcard
	// (`image/*`).
	ByMIME map[string]AttachmentDisposition
	// Default is the agent-wide disposition applied when no ByMIME
	// key matches. Zero means "no agent default" — resolution falls
	// through to the runtime default layer.
	Default AttachmentDisposition
}

DispositionPolicy is the per-agent disposition policy: a per-MIME map plus an agent-wide default. The type is the programmatic seam — `internal/config` merely decodes the `harbor.yaml` `multimodal.disposition` block into it (see DispositionPolicyFromConfig); a headless library consumer constructs the value directly. Both construction paths produce the same semantics.

Lookup order within the policy layer: exact MIME match (`application/pdf`) > family wildcard (`application/*`) > Default. A zero policy (nil map, zero Default) contributes nothing and resolution falls through to the runtime default.

func DispositionPolicyFromConfig added in v1.4.0

func DispositionPolicyFromConfig(cfg config.MultimodalConfig) (DispositionPolicy, error)

DispositionPolicyFromConfig decodes the YAML `multimodal.disposition` block into the planner-homed DispositionPolicy. The config block is a thin carrier: the literal `*` key decodes onto Default; every other key (exact media type or `type/*` family wildcard) decodes onto ByMIME. Programmatic DispositionPolicy construction produces the same value with no config file — that is the seam this projection adapts onto.

Values are re-validated via ParseDisposition defense-in-depth behind `config.Validate` (the loader's validateMultimodal mirrors the grammar locally); a non-grammar value fails loud with ErrInvalidDisposition rather than decoding a policy that silently drops entries (CLAUDE.md §13).

func (DispositionPolicy) IsZero added in v1.4.0

func (p DispositionPolicy) IsZero() bool

IsZero reports whether the policy contributes nothing (no map entries and no default).

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). 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 (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).

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

Payload's contract on an output-schema run (WithOutputSchema / the per-task output_schema field, validated at the shared internal/runtime/runctx envelope builder): a `string` payload is treated as raw JSON TEXT, never as a plain Go string to be quoted for the caller — this is the react terminal-answer reality, where `resp.Content` IS the model's JSON encoding of the answer. A plain Go string like `"done"` is NOT valid JSON and fails loud with a hint naming the fix (quote it, or return a structured payload instead). Structured payloads (a map, a struct, any non-string/[]byte/ json.RawMessage Go value) are marshaled via encoding/json. A json.RawMessage or []byte payload is captured verbatim, bytes unchanged.

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). Harbor 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
	// Disposition is the RESOLVED attachment disposition:
	// how this artifact is handed to the model. The run loop
	// resolves it via [ResolveDisposition] + [EffectiveDisposition]
	// (per-attachment caller hint > per-agent policy map > runtime
	// default); a headless library consumer constructing its own
	// views sets it directly — the consumer IS the top-precedence
	// layer. The zero value selects the pre-84b per-MIME dispatch in
	// the materializer, so existing consumers are byte-for-byte
	// unchanged.
	Disposition AttachmentDisposition
}

InputArtifactView is a pre-resolved multimodal input artifact the planner consumes on its first turn. 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.
	// Harbor 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. Harbor ships the constants;
	// Harbor 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. Harbor ships the executor; Harbor ships the shape so concretes can compile against it.

type LLMOverrides added in v1.5.0

type LLMOverrides struct {
	Model                *string
	Temperature          *float64
	MaxTokens            *int
	ReasoningEffort      *string
	ExtraInstructions    *string
	SystemPromptOverride *string
	BasePromptLayer      *string
	UserPromptLayer      *string
}

LLMOverrides is the per-run bundle of run-start-resolved LLM-parameter overrides carried on RunContext.LLMOverrides. Every field is optional (a nil pointer / zero-value means "no override for this dimension — use the agent/config default"). The Runtime resolves the bundle once at run start and the planner reads it when building each LLM request; the bundle is immutable for the run.

  • Model overrides the model name the request asks for (validated at set time against the configured `ModelProfiles`).
  • Temperature / MaxTokens / ReasoningEffort override the corresponding `llm.CompleteRequest` fields.
  • ExtraInstructions is ADDITIVE: it is appended to the agent's system prompt (rendered into the `<additional_guidance>` section), never a replacement of the base prompt.
  • SystemPromptOverride is a full REPLACE of the agent's base system prompt (the session layer's affordance, distinct from the additive ExtraInstructions). When both are set, the base prompt is replaced AND the additive guidance still appends. An empty string is a valid override (it clears the base prompt for that run); nil = no replace.
  • BasePromptLayer is the durable, operator-owned base prompt layer resolved from the agent's active config at run start. When set it is the run's base system prompt (overriding the agent's configured default base); nil = inherit the configured default. A session SystemPromptOverride takes precedence over it for its single message.
  • UserPromptLayer is the durable, optional user-instruction layer resolved from the agent's active config at run start. When set it composes ABOVE the base in a distinctly-framed, lower-trust position — it can extend the operator's guidance but never precede, replace, or weaken the base. A session SystemPromptOverride replaces the whole base+user spine for its single message.

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).

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

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. 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 and memory strategies 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 — 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 OutputSchemaValidator added in v1.9.0

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

OutputSchemaValidator is the per-run compiled output schema a structured-output run carries on RunContext.OutputSchema. It bundles the raw JSON-Schema bytes (rendered into the terminal completion's llm.ResponseFormat so the provider constrains generation) with the compiled validator (used both by the generation-steering planner's per-turn llm.CompleteRequest.Validator and by the runtime-edge final validation, so the schema is compiled EXACTLY ONCE per run).

Concurrent reuse: the value is immutable after CompileOutputSchema returns — the compiled schema is read-only, and OutputSchemaValidator.Validate is safe for concurrent invocation. It is per-run state carried on the RunContext, never a field on the shared planner artifact.

func CompileOutputSchema added in v1.9.0

func CompileOutputSchema(raw json.RawMessage) (*OutputSchemaValidator, error)

CompileOutputSchema compiles a caller-supplied JSON-Schema document into a reusable per-run validator. A nil / empty / whitespace-only schema is a loud error (never a silent no-op, CLAUDE.md §13); an un-parseable or un-compilable schema is a loud error naming the fault. The compilation happens once per run at the runtime edge; the result threads onto RunContext.OutputSchema.

func (*OutputSchemaValidator) Raw added in v1.9.0

Raw returns the schema bytes to render into the terminal completion's llm.ResponseFormat.JSONSchema. The returned slice is the validator's own copy; callers must not mutate it.

func (*OutputSchemaValidator) Validate added in v1.9.0

func (v *OutputSchemaValidator) Validate(payload json.RawMessage) error

Validate checks a candidate terminal-answer payload against the compiled schema. An empty payload is treated as JSON null so a schema requiring an object fails loud rather than passing vacuously. A non-JSON payload fails loud (the model emitted text where structured output was demanded). A nil validator passes (no schema constrained this run).

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
	// 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. 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 PauseTask added in v1.17.0

type PauseTask struct {
	// TaskID is the descendant to pause.
	TaskID tasks.TaskID
	// Reason is the human-readable pause reason carried onto the
	// descendant's pause record.
	Reason string
}

PauseTask pauses one background task THIS run spawned — the model parking work it started so it can be resumed later. A non-terminal, non-blocking decision dispatched like CallTool; the planner observes the {task_id, paused} outcome on its next turn.

Pausing a descendant drives that descendant through the Runtime's unified pause/resume primitive (the ONE pause path shared with HITL, tool-side OAuth, and operator/Console PAUSE) — it introduces no new pause mechanism. Pausing a descendant NEVER pauses the run issuing the verb: only the resolved descendant parks. Descendant-scope + human-supremacy invariant: identical to SteerTask — a run pauses only its own descendants; the operator reaches any task.

The observed `paused` bool means the PAUSE control was ENQUEUED onto a live descendant — it is false ONLY when the descendant has already finished (its run ended, its inbox retired). It is NOT a pause-state-transition signal: the dispatch edge does not inspect the descendant's pause state, so a redundant pause of an already-paused descendant still reports paused:true and coalesces downstream through the unified primitive (the descendant's own RunLoop parks once); there is no parent-observable "no transition" outcome.

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: 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.
	// 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.
	// 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.
	// 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.
	// 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
	// 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 (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. Empty Driver defaults to "react" — the V1 reference planner — so a config that omits the planner block boots unchanged from the earlier hardcoded path. The boundary copy matches the OAuth-provider 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. 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.

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 the 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) 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.

An earlier phase renamed this type from `PlanningHints` to free that name for the richer runtime-supplied PlanningHints struct rendered into the `<planning_constraints>` prompt section. 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. 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. 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. 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
	// 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 ( ). Each counter tracks one class of LLM-output-format failure the runtime had to repair:

  • FinishRepair — a `_finish` action that failed schema 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 the 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 the per-step repair leaves open.

**Concurrent-reuse.** 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 (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).

Harbor ships the payload with the emit; the 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. 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). Harbor 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 ResumeTask added in v1.17.0

type ResumeTask struct {
	// TaskID is the descendant to resume.
	TaskID tasks.TaskID
	// Directive is optional steering guidance injected on resume via the
	// unified pause/resume primitive's resume-payload seam.
	Directive string
}

ResumeTask resumes one paused background task THIS run spawned, optionally injecting a steering directive on resume. A non-terminal, non-blocking decision dispatched like CallTool; the planner observes the {task_id, resumed} outcome on its next turn.

Resuming a descendant releases it through the SAME unified pause/resume primitive PauseTask parks it with — no new mechanism. A non-empty Directive rides the resume through the primitive's existing resume-payload seam so the released descendant sees the guidance as it continues. Descendant-scope + human-supremacy invariant: identical to SteerTask / PauseTask.

The observed `resumed` bool means the RESUME control was ENQUEUED onto a live descendant — it is false ONLY when the descendant has already finished (its run ended, its inbox retired). It is NOT a resume-state-transition signal: the dispatch edge does not inspect the descendant's pause state, so a redundant resume of a descendant that is NOT paused still reports resumed:true. Such a spurious resume is not harmless downstream — the descendant's own RunLoop applies the RESUME exactly as it would an operator's mistaken resume, surfacing a no-outstanding-pause condition that ends that descendant's run loud (inherited pause/resume-primitive semantics, never a parent-observable outcome). Resume only a descendant you actually paused.

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. Harbor closes
	// the split (serialisable half + handle registry); Harbor
	// 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. Harbor closes the fail-loudly Serialize
	// contract; Harbor 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 the schema-repair pipeline had
	// to fix an LLM-output-format failure, and resets on a clean turn at
	// that surface. 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 concurrent-reuse
	// concurrent-reuse contract (CLAUDE.md §5 + §13).
	RepairCounters *RepairCounters

	// PlanningHints carries runtime-supplied planning constraints the
	// ReAct prompt builder renders into the `<planning_constraints>`
	// section (anchor; body). 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.
	// 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). Harbor 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; the planner is the
	// caller responsible for upgrading inline bytes to refs at the
	// LLM edge.
	Artifacts artifacts.ArtifactStore

	// InputArtifacts 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); 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. 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

	// LLMOverrides carries the per-run, run-start-resolved LLM-parameter
	// overrides the Runtime applies to this run's LLM requests: the model
	// name, sampling temperature, per-call token ceiling, reasoning-effort
	// hint, and an ADDITIVE extra-instructions block appended to the
	// agent's system prompt. Nil means "no overrides" — the planner uses
	// the agent/config defaults (the bound `llm.model`, the provider
	// defaults, the agent's base system prompt).
	//
	// The Runtime resolves these ONCE at run start from the effective
	// override layers (an admin-set tenant default today; a per-session
	// next-turn override above it later) and pins the snapshot here for
	// the whole run — a swap at any layer lands on the NEXT run, never
	// mid-flight (the same next-turn-only invariant the compiled-artifact
	// concurrent-reuse contract requires: a running planner's parameters
	// are fixed for the run; per-run state lives on rc, never on the
	// shared planner artifact). The planner reads only.
	LLMOverrides *LLMOverrides

	// MemoryBlocks carries the pre-fetched, identity-scoped memory
	// blobs the planner injects into the system prompt as UNTRUSTED-
	// framed `<read_only_*_memory>` sections. 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. 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 (
	// 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: 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,
	// 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. 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: 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 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
	// never on the planner struct.
	DiscoveredTools []string

	// PendingToolCalls 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.
	PendingToolCalls []ToolCallDeferred

	// OnPendingToolCalls (— 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 (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 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 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: the manifest is per-run state on rc,
	// never on the shared planner artifact.
	SessionArtifacts []ArtifactManifestEntry

	// OutputSchema is the optional, opt-in run-level output schema the
	// caller supplied (via the assemble.WithOutputSchema run option).
	// When non-nil the run is a structured-output run: the terminal
	// answer must validate against this schema. A generation-steering
	// planner (the ReAct driver) renders OutputSchema.Raw() into its
	// terminal completion's ResponseFormat and installs a
	// schema-Validator so the corrective retry-with-feedback loop
	// engages; every planner's terminal Finish is additionally validated
	// against this schema at the runtime edge (planner-agnostic, no
	// capability ceremony). Nil means "no schema" — the run is plain
	// text exactly as before.
	//
	// The schema is compiled ONCE per run at the runtime edge and the
	// compiled validator threads here; the planner reads only. Per-run
	// state on rc, never on the shared planner artifact.
	OutputSchema *OutputSchemaValidator
}

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. 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 these 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 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. Harbor ships the production surface; Harbor declares the planner- facing shape so the planner package compiles without importing internal/skills (parallel fork at).

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.
	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
	// PropagateOnCancel controls whether this spawned task survives a
	// cancellation cascade from an ANCESTOR task. Empty (the default)
	// maps to `tasks.PropagateCascade`: the task is swept when a task
	// above it in the parent chain is cancelled with a cascade policy.
	// `tasks.PropagateIsolate` detaches the task from that cascade — it
	// keeps running when its parent is cancelled. Isolate never detaches
	// a task from a DIRECT cancel: the operator (via the Protocol) and
	// the spawning run itself (via CancelTask) both reach an
	// isolate-marked task at any time. Any other value is rejected loud
	// at the dispatch edge. This is the model's brake on the parallelism
	// it spawns — paired with the observation/cancel meta-tools so
	// detached work is never unobservable or unstoppable.
	PropagateOnCancel string
}

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.

The shape carries only the fields the planner needs to specify; the Runtime fills the rest (Identity, IdempotencyKey, NotifyOnComplete). PropagateOnCancel became planner-controllable once the model gained the task observation/cancel meta-tools to manage detached work. Future phases MAY extend this shape with additional planner-controlled fields.

type SpawnTask

type SpawnTask struct {
	Kind    tasks.TaskKind
	Spec    SpawnSpec
	GroupID tasks.TaskGroupID
	// CallID is the provider-assigned tool-call identifier of the
	// native `_spawn_task` call this spawn was projected from,
	// mirroring CallTool.CallID. Batch dispatch keys each spawn's
	// observation by it so every native tool_call_id — spawn calls
	// included — is answered. Empty for programmatic (non-native)
	// spawn emissions, exactly like CallTool.CallID; the projector
	// stamps it when partitioning a native multi-call response.
	CallID string
}

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 (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 SteerTask added in v1.17.0

type SteerTask struct {
	// TaskID is the descendant to steer.
	TaskID tasks.TaskID
	// Directive is the free-text steering guidance enqueued onto the
	// descendant's per-sub-run steering inbox.
	Directive string
}

SteerTask steers one background task THIS run spawned — the model applying mid-flight guidance to work it started, without waiting on or cancelling it. A non-terminal, non-blocking decision the Runtime executor dispatches like CallTool; the planner observes the {task_id, steered} outcome on its next turn.

The Directive is enqueued onto the target sub-run's own steering inbox — the SAME inbox an operator's steering targets — so the steered descendant sees the guidance at its next planner-step boundary. Descendant-scope + human-supremacy invariant: TaskID must reach the calling run's own task by walking the parent-task chain upward, so a run steers ONLY the tasks it spawned (directly or transitively), never a sibling run's tasks or its own task; the operator's control surface always supersedes and reaches any task. Steering a terminal descendant returns {steered: false}, not an error (idempotent-on-terminal, mirroring CancelTask).

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: earlier, this type was named TrajectoryStep at the planner-package level. The rename to Step is part of the subpackage relocation; no external consumers of TrajectoryStep existed in an earlier revision.

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 the concurrent-reuse contract; the summariser is called under the run's ctx from MaybeCompress).

The production implementation is the LLM-backed TrajectorySummariser in internal/llm/summarizer: it binds an LLM client + a versioned compaction prompt, invokes llm.LLMClient.Complete over the trajectory's planner-facing projection (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 ( summariser output). Earlier this was TrajectorySummary at the planner-package level; the subpackage rename to Summary is part of

type TaskStatusQuery added in v1.16.0

type TaskStatusQuery struct {
	// TaskIDs are the tasks to report on; nil/empty means every task
	// this run has spawned, including nested descendants.
	TaskIDs []tasks.TaskID
}

TaskStatusQuery observes the state of the background tasks THIS run spawned — a non-terminal, non-blocking decision the Runtime executor dispatches like CallTool, appending a trajectory step the planner reads on its next turn. A model that fanned out several explorations uses it to poll which have resolved before deciding whether to await or cancel the rest.

TaskIDs names the tasks to report; an empty/nil slice means "every task this run has spawned, directly or transitively" (the run's whole descendant subtree). Every explicitly-named id is scope-checked: the executor resolves ONLY tasks whose parent-task chain reaches the calling run's own task, never an arbitrary session task — one out-of-scope id fails the whole call rather than silently omitting it.

The shape is deliberately NOT named TaskStatus: that identifier is the tasks-package lifecycle enum (pending/running/…/cancelled). This is the planner-facing query decision.

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).

Estimator errors propagate; an trajectory.ErrUnserializable from Serialize is the typical failure mode and is surfaced verbatim (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. 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. 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. Harbor 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).

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. 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 an 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.

Harbor 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).

type TrajectorySummary

type TrajectorySummary = trajectory.Summary

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

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 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). 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.
	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 (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 (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 (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 (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 (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).
Package repair ships Harbor's reusable salvage / schema-repair / graceful-failure / multi-action-salvage ladder for planner steps (RFC §6.2).
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