Documentation
¶
Overview ¶
Input-artifact disposition observability.
The disposition resolver and the materializer are pure — they RETURN the winning precedence layer and any degradation fact as typed values; the caller logs and emits. This file is the canonical event vocabulary the shared run-loop helper ResolveInputArtifacts emits on behalf of every caller (cmd / devstack / a headless consumer that supplies an emit closure), so operators can see which disposition fired, why (which layer won), and that the runtime never silently overrode them.
newruncontext.go — the shared production RunContext factory.
A headless embedder that wants to run a single goal after assembling a stack previously had to hand-build a `planner.RunContext` field by field, re-deriving the memory / skills / artifact / streaming projections the run-loop drivers already compose. NewRunContext is the ONE factory that turns stack-derived subsystem handles into a fully-projected RunContext, composing the SAME projection helpers the drivers use (FetchMemoryBlocks, ProjectSkillsDirectory, ResolveInputArtifacts, the bus chunk publisher) — never a second hand-rolled construction site. The one-call Stack.RunOnce runner is its first consumer; an embedder building its own RunSpec composes it directly.
Import direction: this file may import `tools` / `llm` (the projection + streaming surfaces) in addition to the package's existing `planner` / `memory` / `skills` / `artifacts` / `events` imports — none of those import this package, so no cycle. The factory stays out of `steering`: it produces the RunContext (RunSpec.Base); the caller wraps it in a RunSpec with the planner / executor / max-steps it owns.
Package runctx exports the RunContext-population helpers a run-loop driver applies between "task spawned" and "planner.Next" — the code that turns subsystem state (memory, skills, artifacts, the terminal Finish) into what the planner actually sees.
The settled contract is that the planner never imports runtime internals: everything it can see arrives through `planner.RunContext`. The projection code that POPULATES RunContext is therefore the runtime's half of the planner contract. Previously that half lived as unexported `package main` helpers in `cmd/harbor`, hand-duplicated in `harbortest/devstack` (the mirror tax — the SDK friction audit found a THIRD drifting copy of the keyword shaper). This package is the promotion that makes the contract real for every caller: `cmd/harbor`, the devstack test kit, and a headless SDK consumer building its own RunSpec all compose the SAME projections.
Import direction (recorded in the phase plan): `internal/runtime/*` may import `planner` / `memory` / `skills` / `artifacts`; `internal/planner` gains NO new imports — in particular no `memory` import.
All five helpers are pure functions (or functions over their explicit dependencies) with no package-level mutable state — they are trivially safe for concurrent use.
executed the deprecation notice: the `ExtractSkillKeywords` query shaper is DELETED along with the raw-Search `<skills_context>` injection path it served; the `skills.Directory` view (projected via ProjectSkillsDirectory) is the canonical producer.
Index ¶
- Constants
- func DispositionHints(raw map[string]string) map[string]planner.AttachmentDisposition
- func ExtractAssistantAnswer(fin planner.Finish) string
- func FetchMemoryBlocks(ctx context.Context, store memory.MemoryStore, id identity.Quadruple, ...) (*planner.MemoryBlocks, error)
- func FinishAnswerEnvelope(fin planner.Finish, traj *planner.Trajectory, ...) (planner.AnswerEnvelope, error)
- func NewRunContext(ctx context.Context, src Sources, q identity.Quadruple, goal string, ...) (planner.RunContext, error)
- func ProjectMemoryBlocks(patch memory.LLMContextPatch) *planner.MemoryBlocks
- func ProjectSkillsContext(ranked []skills.RankedSkill) []any
- func ProjectSkillsDirectory(views []skills.SkillView) []any
- func ResolveInputArtifacts(ctx context.Context, store artifacts.ArtifactStore, q identity.Quadruple, ...) []planner.InputArtifactView
- type InputArtifactOptions
- type InputDispositionResolvedPayload
- type Option
- type Sources
Constants ¶
const EventTypeInputDispositionResolved events.EventType = "task.input_disposition.resolved"
EventTypeInputDispositionResolved is published once per input artifact when the run loop resolves the attachment's disposition. The payload carries the resolved disposition, the precedence layer that won (caller_hint / agent_policy / runtime_default), and — when the resolved disposition could not be honoured — the typed degradation fact (e.g. an unknown `tool:<name>`, or `inline` for a non-image MIME). A resolved `provider_native` is honoured as-is; the provider upload itself is observable via the `llm.provider_file.uploaded` event.
Variables ¶
This section is empty.
Functions ¶
func DispositionHints ¶ added in v1.4.0
func DispositionHints(raw map[string]string) map[string]planner.AttachmentDisposition
DispositionHints converts the string-typed per-attachment hint map a task record carries (`tasks.Task.InputArtifactDispositions` — the Protocol carrier, validated at the edge via `planner.ParseDisposition`) into the typed hint map InputArtifactOptions.Hints expects. Nil in, nil out.
func ExtractAssistantAnswer ¶
ExtractAssistantAnswer pulls the planner's natural-language answer out of a terminal Finish for the memory.AddTurn writeback and the `planner.AnswerEnvelope` projection (promoted by the runtime promotion). The react planner's FinishGoal carries Payload = map[string]any{"answer": "<the LLM's answer>"}. Other planners may shape Payload differently; we accept any string-valued "answer" key and otherwise fall back to a Sprintf so something always lands in memory (matching CLAUDE.md §5 fail-loud — silent "nothing written" would lose the run's outcome).
func FetchMemoryBlocks ¶ added in v1.4.0
func FetchMemoryBlocks( ctx context.Context, store memory.MemoryStore, id identity.Quadruple, query string, recall memory.RecallSettings, logger *slog.Logger, ) (*planner.MemoryBlocks, error)
FetchMemoryBlocks fetches the session's memory patch, optionally recalls semantically similar past turns for the given query, and projects both into the planner's MemoryBlocks. This is the single production home for the memory step of run-loop RunContext population.
When recall is disabled (RecallSettings.Enabled == false) the output is identical to calling GetLLMContext + ProjectMemoryBlocks directly — the External tier remains nil and SearchTurns is never called.
When recall is enabled:
- SearchTurns is called with the session-scoped identity triple (RunID zeroed) and the caller-supplied query.
- Turns scoring below RecallSettings.MinScore are dropped.
- Turns whose UserMessage is already present in the patch's RecentTurns window are deduped out.
- Each remaining turn's user and assistant text are capped at recalledTurnTextCap bytes (with a "…[truncated]" marker).
- The resulting slice is marshalled as map["recalled_turns"][]map[string]any and stored in MemoryBlocks.External alongside the Conversation tier.
An error from GetLLMContext or SearchTurns is returned unwrapped so the caller can fail the run loudly via MarkFailed(runtime_fetch_error). There is no silent fallback to rolling-summary-only on a recall error.
A Debug line is emitted on the supplied logger when recall fires (count + top score) so operators can see retrieval activity without seeing turn content. A nil logger falls back to slog.Default().
func FinishAnswerEnvelope ¶ added in v1.10.0
func FinishAnswerEnvelope(fin planner.Finish, traj *planner.Trajectory, schema *planner.OutputSchemaValidator) (planner.AnswerEnvelope, error)
FinishAnswerEnvelope builds the canonical planner.AnswerEnvelope from a terminal Finish, applying output-schema validation at the ONE shared site. The three call sites (assemble.RunOnce, the cmd/harbor per-task driver, the devstack twin) consume it so the answer envelope — and, on a schema-constrained goal Finish, the validated AnswerPayload — is byte-identical wherever a run is hosted.
Behaviour:
- schema == nil, OR fin.Reason != FinishGoal → the plain three-key envelope ({answer, finish_reason, tool_calls_seen}), byte-identical to a no-schema run. A non-goal terminal (a steering CANCEL surfaced as Finish{Cancelled}, a NoPath / deadline / constraints terminal) was never asked to produce a schema-shaped answer, so it is NEVER validated and NEVER returns ErrOutputInvalid.
- schema != nil AND fin.Reason == FinishGoal → the terminal payload is captured to its exact bytes at the validation site (never a lossy string round-trip) and validated against the compiled schema. On success AnswerPayload carries the exact validated bytes and Answer carries their string rendering. On a capture or validation failure the returned error wraps planner.ErrOutputInvalid — a loud, typed failure, never a silent fallback to unvalidated text (CLAUDE.md §13). A caller mapping this to a task terminal stamps planner.TaskErrorCodeOutputInvalid.
The trajectory is read only for planner.CountToolInvocations; a nil trajectory yields ToolCallsSeen == 0.
func NewRunContext ¶ added in v1.8.0
func NewRunContext( ctx context.Context, src Sources, q identity.Quadruple, goal string, opts ...Option, ) (planner.RunContext, error)
NewRunContext projects the stack-derived sources into a fully-formed planner.RunContext for the run identified by q and driven toward goal. It is the single production home for run-loop RunContext population that a one-call runner or a headless RunSpec builder composes; the per-task drivers keep their own population bodies (they thread control-plane projections this factory deliberately omits) but call the SAME underlying helpers, so the projected memory / skills / input-artifact / streaming surface is identical. The read-only cross-turn session-artifact manifest the dev drivers attach is one of those control-plane projections this factory omits; a headless run that needs input artifacts resolves them explicitly via WithInputArtifacts.
Identity is mandatory and fails loud: an incomplete quadruple returns a wrapped identity error rather than a half-scoped RunContext. Memory and skills are SESSION-scoped (the fetch quadruple zeroes RunID) so a run inherits the session's accumulated state; a fetch error is returned unwrapped enough for the caller to fail the run loudly (no silent degradation, CLAUDE.md §5/§13).
func ProjectMemoryBlocks ¶
func ProjectMemoryBlocks(patch memory.LLMContextPatch) *planner.MemoryBlocks
ProjectMemoryBlocks shapes a memory.LLMContextPatch into the JSON-encodable map the planner's `<read_only_conversation_memory>` wrapper renders. Returns nil when the patch is empty — the wrapper is omitted entirely. Only the Conversation tier is populated here; callers that enable semantic recall (via FetchMemoryBlocks) will additionally populate the External tier with retrieved turns.
func ProjectSkillsContext ¶
func ProjectSkillsContext(ranked []skills.RankedSkill) []any
ProjectSkillsContext shapes a []skills.RankedSkill into the []any the planner's `<skills_context>` wrapper renders ( the reasoning-replay knob). Each element is a small map carrying the body fields the LLM consumes (name / title / description / steps). An empty input returns nil so the wrapper is omitted.
func ProjectSkillsDirectory ¶
ProjectSkillsDirectory shapes a `skills.Directory.View` snapshot into the []any the planner's `<skills_context>` wrapper renders (, executing the deprecation notice: the keyword-shaped raw-Search injection path and its `ExtractSkillKeywords` helper are deleted; the Directory's bounded, pinned-then-recent, capability-filtered, redacted browse window is the canonical producer). Each element is the compact `skills.SkillView` projection (name / title / trigger / task_type / pinned) — full skill content stays behind the `skill_get` meta-tool. An empty input returns nil so the wrapper is omitted.
func ResolveInputArtifacts ¶
func ResolveInputArtifacts( ctx context.Context, store artifacts.ArtifactStore, q identity.Quadruple, ids []string, logger *slog.Logger, opts InputArtifactOptions, ) []planner.InputArtifactView
ResolveInputArtifacts pre-fetches the metadata (+ bytes for inline dispositions) for every operator-uploaded artifact ID on a task, producing the `planner.InputArtifactView` slice the run loop hands to the planner's first turn (promoted by the runtime promotion). The synchronous pre-resolution keeps the planner's prompt assembly I/O-free.
each view's `Disposition` is resolved here via the pure `planner.ResolveDisposition` (per-attachment caller hint > per-agent policy map > runtime default) + capability normalisation via `planner.EffectiveDisposition`. This helper is a THIN caller — the precedence logic lives in `internal/planner`; the helper adds only the I/O (byte fetch for inline) and the logging/emission of the returned winning-layer + degradation facts (the resolver is pure and never logs).
Failures are bounded:
- Nil artifact store with non-empty IDs → empty slice + Warn log (the LLM still sees a text-only prompt; the operator can re- attach after wiring the store). Avoids a hard fail-loud here because the artifact-store dependency is genuinely optional in some dev postures.
- `GetRef` not-found / errored → skip that ID + Warn (the rest of the slice survives; the artifact may have been GC'd between spawn and run).
- `Get` (bytes fetch) errored on an inline disposition → keep the entry but leave Bytes nil. The materializer falls back to a stub-text part for missing-bytes inlines.
The scope on every store call is the run's identity tuple — the artifact store enforces tenant isolation on read. A nil logger defaults to slog.Default() so the Warn paths above stay loud for every caller.
Types ¶
type InputArtifactOptions ¶ added in v1.4.0
type InputArtifactOptions struct {
// Hints maps an artifact ID to its per-attachment caller hint —
// the top precedence layer. The dev run loop populates it from
// `tasks.Task.InputArtifactDispositions` (the Protocol
// `disposition` carrier); a headless caller constructs it
// directly (or skips this helper entirely and sets
// `InputArtifactView.Disposition` on hand-built views).
Hints map[string]planner.AttachmentDisposition
// Policy is the per-agent disposition policy map — the middle
// precedence layer (`harbor.yaml` `multimodal.disposition`, or a
// programmatic `planner.DispositionPolicy`).
Policy planner.DispositionPolicy
// Catalog, when non-nil, validates `tool:<name>` dispositions
// against the run's visible tool set: an unknown name degrades
// to `ref` with a logged + emitted degradation fact.
Catalog planner.ToolCatalogView
// Emit, when non-nil, publishes one
// [EventTypeInputDispositionResolved] event per resolved
// artifact (the dev run loop passes its identity-stamping
// emitter). Nil skips emission; the slog records still land.
Emit func(events.Event)
}
InputArtifactOptions carries the disposition inputs to ResolveInputArtifacts. The zero value reproduces the pre-84b behaviour exactly: no hints, no policy (the runtime default resolves `image/*` → inline, everything else → ref), no catalog validation of `tool:<name>` values, and no event emission.
type InputDispositionResolvedPayload ¶ added in v1.4.0
type InputDispositionResolvedPayload struct {
events.SafeSealed `json:"-"`
// Identity is the run's identity quadruple.
Identity identity.Quadruple `json:"identity"`
// TaskID is the task whose input artifact was resolved.
TaskID string `json:"task_id"`
// ArtifactID is the input artifact's content-addressed id.
ArtifactID string `json:"artifact_id"`
// MIME is the artifact's IANA media type.
MIME string `json:"mime"`
// Disposition is the EFFECTIVE disposition the materializer will
// honour (`ref` / `inline` / `provider_native` / `tool:<name>`).
Disposition string `json:"disposition"`
// Layer is the precedence layer that won the resolution
// (`caller_hint` / `agent_policy` / `runtime_default`).
Layer string `json:"layer"`
// Degraded is true when the resolved disposition could not be
// honoured and fell back to `ref`.
Degraded bool `json:"degraded"`
// DegradedFrom is the disposition that was requested when
// Degraded is true ("" otherwise).
DegradedFrom string `json:"degraded_from,omitempty"`
// DegradationReason is the typed cause when Degraded is true
// (`unknown_tool` / `inline_unsupported_mime` /
// `invalid_disposition`).
DegradationReason string `json:"degradation_reason,omitempty"`
// Tool is the unknown tool name when DegradationReason is
// `unknown_tool`.
Tool string `json:"tool,omitempty"`
// OccurredAt is the resolution wall-clock instant.
OccurredAt time.Time `json:"occurred_at"`
}
InputDispositionResolvedPayload is the `task.input_disposition.resolved` event payload. SafePayload — the fields are artifact ref / MIME / disposition / layer metadata plus the identity quadruple; no operator content and no tool arguments, so the audit redactor bypasses it and subscribers keep typed access (the `llm.image.materialized` precedent).
type Option ¶ added in v1.8.0
type Option func(*runContextConfig)
Option configures a NewRunContext call. The functional-option shape keeps the factory signature stable as new per-run knobs land.
func WithDispositionPolicy ¶ added in v1.8.0
func WithDispositionPolicy(policy planner.DispositionPolicy) Option
WithDispositionPolicy supplies the per-agent disposition policy map (the middle precedence layer).
func WithInputArtifactDispositions ¶ added in v1.8.0
WithInputArtifactDispositions supplies the per-attachment disposition hint map (artifact ID → "inline"/"ref"/"tool:<name>") — the top precedence layer of disposition resolution.
func WithInputArtifacts ¶ added in v1.8.0
WithInputArtifacts pre-resolves the operator-uploaded artifact IDs into the run's first-turn multimodal inputs (the materializer renders them as Content.Parts). Without it the run is text-only.
type Sources ¶ added in v1.8.0
type Sources struct {
// Memory is the session-scoped memory store. When non-nil, the
// session's rolling-summary + recent turns are projected into
// RunContext.MemoryBlocks; semantic recall fires when MemoryRecall
// is enabled.
Memory memory.MemoryStore
MemoryRecall memory.RecallSettings
// SkillsDirectory is the bounded, capability-filtered browse window
// projected into RunContext.SkillsContext. When non-nil its View is
// taken under the run's identity scope and the run's visible-tool
// set (derived from Catalog + GrantedScopes).
SkillsDirectory *skills.Directory
// Catalog is the production tool catalog. When non-nil it is
// projected into the planner-facing schema-only RunContext.Catalog
// view under the run's identity + granted-scope filter.
Catalog tools.ToolCatalog
// Artifacts is the artifact store the input-artifact materializer
// reads from (only touched when WithInputArtifacts supplies IDs).
Artifacts artifacts.ArtifactStore
// Bus is the event bus the planner-side telemetry emitter and the
// per-token chunk publisher write through. When nil, RunContext.Emit
// and RunContext.OnChunk are left nil (no streaming surface).
Bus events.EventBus
// Logger receives the projection helpers' Debug/Warn records. Nil
// falls back to slog.Default().
Logger *slog.Logger
// GrantedScopes is the operator-declared authorization-scope list
// threaded into the catalog + skills capability filters. Tools whose
// AuthScopes exceed this set are invisible to the run.
GrantedScopes []string
// PlanningHints is the optional runtime-supplied planning-constraint
// bundle threaded straight onto RunContext.PlanningHints.
PlanningHints *planner.PlanningHints
// LLMOverrides is the optional run-start-resolved LLM-parameter
// bundle. A headless caller with no tenant/agent control plane wired
// leaves it nil (the run uses the configured defaults).
LLMOverrides *planner.LLMOverrides
// Budget is the per-run hard caps (token budget, deadline, hop
// budget) projected onto RunContext.Budget. The zero value disables
// compression and applies the runtime defaults.
Budget planner.Budget
// OutputSchema is the optional, opt-in run-level output schema (raw
// JSON-Schema bytes). When non-empty NewRunContext compiles it ONCE
// and threads the compiled validator onto RunContext.OutputSchema, so
// the terminal answer is validated against it (fail-loud). A compile
// failure fails the run loudly rather than silently degrading to an
// unconstrained run (CLAUDE.md §13). Empty means "no schema".
OutputSchema json.RawMessage
}
Sources carries the stack-derived subsystem handles NewRunContext projects into a planner.RunContext. Every field is OPTIONAL except an identity-complete quadruple (passed separately): a nil subsystem leaves the corresponding RunContext field nil and the planner omits the matching wrapper — exactly as the run-loop drivers behave when a dev stack did not open the subsystem.
assemble.Stack.RunOnce populates Sources from the assembled stack; a headless embedder constructing its own RunSpec populates it by hand.