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.
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 Phase-39 `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 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
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 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 Phase-39 `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).