Documentation
¶
Overview ¶
Package agent is the use-case heart of mecatl: the streaming agent loop that ties the ports together. It records the user prompt, calls the LLMProvider, streams assistant deltas, dispatches tool calls (read-parallel / mutate-serial) through the permission policy and hook lifecycle, pauses on permission asks, compacts the history at the context-window threshold, and emits a single ordered stream of session.Events terminating in a result.
Import rule: this package imports ONLY engine/session, engine/port, engine/tool, engine/governance, engine/prompt, engine/team (the domain coordination substrate for agent teams), and the standard library. Adapters are injected as ports; the loop never names a concrete adapter or the api layer. (Tests may import adapters.)
Index ¶
- Constants
- Variables
- func DefaultChildLimits() session.Limits
- func MemberSessionID(teamID, member string) session.SessionID
- func MemberToolNames() map[string]struct{}
- func MemberTools(t *team.Team, self string, hooks port.HookRunner) []tool.Tool
- func NewInspectMemberTool(store port.SessionStore) tool.Tool
- func NewInspectMemberToolWithOwnership(store port.SessionStore, ownershipEnforced bool) tool.Tool
- func NewInspectSubagentTool(store port.SessionStore) tool.Tool
- func NewInspectSubagentToolWithOwnership(store port.SessionStore, ownershipEnforced bool) tool.Tool
- func NewParallelTool(childEngine *Engine, forker tool.EnvironmentForker, opts ...ParallelOption) tool.Tool
- func NewPlanAwareScheduleTool(base tool.Tool, mgr port.ScheduleManager) tool.Tool
- func NewPresentPlanTool() tool.Tool
- func NewScheduleQueryTool(mgr port.ScheduleManager) tool.Tool
- func NewScheduleTool(mgr port.ScheduleManager) tool.Tool
- func NewShellStatusTool() tool.Tool
- func NewShellTool() tool.Tool
- func NewSubagentStatusTool() tool.Tool
- func NewSubagentTool(childEngine *Engine, opts ...SubagentOption) tool.Tool
- func NewTeamTool(factory TeamMemberEngineFactory, opts ...TeamOption) tool.Tool
- func ParseReflectionOutcome(in learning.Input, raw []byte, limits ReflectionLimits) (learning.Outcome, error)
- func RejectDuplicateJSONKeys(raw []byte) error
- func RemapAuxiliaryUsage(ctx context.Context, diag port.Diagnostics, purpose session.UsageKind, ...) session.AuxiliaryUsage
- func RunGuardrailCheck(ctx context.Context, engine *Engine, prompt string) (string, session.AuxiliaryUsage, error)
- func RunModelRouter(ctx context.Context, engine *Engine, req ModelRouteRequest) (category string, usage session.AuxiliaryUsage, missReason string, ok bool)
- func StripLoneCodeFence(s string) string
- func TeamStop(o TeamOutcome) session.StopReason
- func TightenTeamTokenBudget(serverBudget, request int) int
- func UtilityEngineUsage(kind session.UsageKind, identity session.ProviderModelID, ...) session.AuxiliaryUsage
- func ValidateApprovalResolution(ask session.PendingAsk, resolution ApprovalResolution) error
- type AgentMeta
- type ApprovalResolution
- type ArtifactHandle
- type AskResolution
- type BranchJudge
- type BranchSummary
- type CascadeCompactor
- type ChildAskReview
- type ChildAskReviewRequest
- type ChildAskReviewer
- type Compactor
- type DelegationTightening
- type Deps
- type Engine
- func (e *Engine) Capabilities() port.ProviderCapabilities
- func (e *Engine) CompactSession(ctx context.Context, sess *session.Session) (ManualCompactionResult, error)
- func (e *Engine) ContextWindow() int
- func (e *Engine) HasTool(name string) bool
- func (e *Engine) Model() string
- func (e *Engine) PrepareAfterAuthorization(ctx context.Context, sess *session.Session, env tool.Environment, ...) (*PreparedRun, error)
- func (e *Engine) PrepareAuthorizationContinuation(ctx context.Context, sess *session.Session, env tool.Environment, ...) (*PreparedRun, error)
- func (e *Engine) ResumeApproval(ctx context.Context, sess *session.Session, env tool.Environment, askID string, ...) *Run
- func (e *Engine) ResumeApprovalWith(ctx context.Context, sess *session.Session, env tool.Environment, askID string, ...) *Run
- func (e *Engine) RetryFailedStep(ctx context.Context, sess *session.Session, env tool.Environment) *Run
- func (e *Engine) Run(ctx context.Context, sess *session.Session, env tool.Environment, ...) *Run
- func (e *Engine) SteerEnabled() bool
- type EngineAskReviewerOption
- type EngineJudgeOption
- type EvidenceReflector
- func (r *EvidenceReflector) Reflect(ctx context.Context, in learning.Input) (learning.Outcome, session.AuxiliaryUsage, error)
- func (r *EvidenceReflector) ReflectProjection(ctx context.Context, projection learning.Projection) (learning.Outcome, session.AuxiliaryUsage, error)
- func (r *EvidenceReflector) RequestTokenEstimate(in learning.Input) (int, error)
- type GuardrailReviewFailure
- type GuardrailReviewTerminalFailure
- type HeuristicCompactor
- type HeuristicTokenCounter
- type InspectMemberTool
- type InspectSubagentTool
- type LRUForkReaper
- type ManualCompactionResult
- type MemberBuild
- type MemberDisposition
- type MemberEngine
- type MemberOutcome
- type MemberSpec
- type MemberStopReason
- type ModelRouteCategory
- type ModelRouteRequest
- type ModelRouteResult
- type ModelTarget
- type ParallelOption
- func WithAutoMerge(m tool.EnvironmentMerger) ParallelOption
- func WithMaxBranches(n int) ParallelOption
- func WithParallelChildLimits(l session.Limits) ParallelOption
- func WithParallelChildMode(m session.PermissionMode) ParallelOption
- func WithParallelChildSessionPrefix(p string) ParallelOption
- func WithParallelConcurrency(n int) ParallelOption
- func WithParallelEngineFactory(f func(ModelTarget) (*Engine, bool)) ParallelOption
- func WithParallelJudge(j BranchJudge) ParallelOption
- func WithParallelProvider(provider string) ParallelOption
- func WithParallelSelectorResolver(r SubagentSelectorResolver) ParallelOption
- func WithParallelStore(store port.SessionStore) ParallelOption
- func WithParallelSubagentStopHook(h port.HookRunner) ParallelOption
- func WithWinnerReaper(s PreservedForkStore) ParallelOption
- type ParallelTool
- func (t *ParallelTool) Execute(ctx context.Context, call session.ToolCall, env tool.Environment) (session.ToolResult, error)
- func (t *ParallelTool) ExecuteWithParent(ctx context.Context, call session.ToolCall, env tool.Environment, ...) (session.ToolResult, error)
- func (t *ParallelTool) MutatesParent(call session.ToolCall) bool
- func (*ParallelTool) ReadOnly() bool
- func (*ParallelTool) Spec() tool.ToolSpec
- type PermissionReviewPolicyProvider
- type PlanApprovalReceipt
- type PlanApprovalStore
- type PreparedReviewEvidence
- type PreparedRun
- type PreparedRunTransition
- type PreservedForkStore
- type ReflectionLimits
- type ResolvedModelSelector
- type ResumeApprovalOptions
- type ReviewAssessment
- type ReviewCaller
- type ReviewCapacity
- type ReviewConcern
- type ReviewDetail
- type ReviewDetailSink
- type ReviewEvidence
- type ReviewEvidenceMeta
- type ReviewEvidencePreparation
- type ReviewEvidencePreparer
- type ReviewEvidenceRequest
- type ReviewEvidenceSource
- type ReviewEvidenceUse
- type ReviewFailureCode
- type ReviewFailureRecorder
- type ReviewGrantStore
- type ReviewJob
- type ReviewMetadataProvider
- type ReviewMissingEvidence
- type ReviewPolicyProvider
- type ReviewPrincipalFact
- type ReviewTarget
- type ReviewTrajectoryFact
- type Run
- func (r *Run) Approve(askID string, v session.ApprovalVerdict) error
- func (r *Run) Cancel()
- func (r *Run) CancelChild(childID string) bool
- func (r *Run) CancelSteer() (SteerOutcome, error)
- func (r *Run) EnqueueSteer(text string, parts []session.Content) (SteerOutcome, error)
- func (r *Run) EnqueueSteerWithMessageID(text string, parts []session.Content, messageID string) (SteerOutcome, error)
- func (r *Run) Events() <-chan session.Event
- func (r *Run) Outcome() RunOutcome
- func (r *Run) ResolveApproval(resolution ApprovalResolution) error
- func (r *Run) ResolveOrdinaryAsk(askID string, v session.ApprovalVerdict) AskResolution
- func (r *Run) ResolvePlanAsk(askID string, verdict session.ApprovalVerdict) AskResolution
- func (r *Run) RetractPermissionAsk(askID string) bool
- func (r *Run) RunID() string
- func (r *Run) ValidateRemoteApprovalIntent(askID, reviewID string, kind session.GuardrailApprovalKind, ...) error
- type RunOutcome
- type RunRequest
- type ScheduleQueryTool
- type ScheduleTool
- type ShellStatusTool
- func (*ShellStatusTool) Execute(_ context.Context, call session.ToolCall, _ tool.Environment) (session.ToolResult, error)
- func (t *ShellStatusTool) ExecuteWithParent(ctx context.Context, call session.ToolCall, env tool.Environment, ...) (session.ToolResult, error)
- func (*ShellStatusTool) ReadOnly() bool
- func (*ShellStatusTool) Spec() tool.ToolSpec
- type ShellTool
- func (t ShellTool) Execute(ctx context.Context, in session.ToolCall, env tool.Environment) (session.ToolResult, error)
- func (t ShellTool) ExecuteWithParent(ctx context.Context, in session.ToolCall, env tool.Environment, ...) (session.ToolResult, error)
- func (ShellTool) ReadOnly() bool
- func (ShellTool) Spec() tool.ToolSpec
- type SteerOutcome
- type SubagentModelRouter
- type SubagentOption
- func WithAgentEngines(engines map[string]*Engine, meta []AgentMeta) SubagentOption
- func WithAgentModelEngineFactory(f func(agentName string, target ModelTarget) (*Engine, bool)) SubagentOption
- func WithAgentWritableEngineFactory(f func(agentName string) (*Engine, bool)) SubagentOption
- func WithAgentWritableModelEngineFactory(f func(agentName string, target ModelTarget) (*Engine, bool)) SubagentOption
- func WithChildForker(f tool.EnvironmentForker) SubagentOption
- func WithChildLimits(l session.Limits) SubagentOption
- func WithChildMode(m session.PermissionMode) SubagentOption
- func WithChildSessionPrefix(p string) SubagentOption
- func WithMaxConcurrentChildren(n int) SubagentOption
- func WithPinnedAgents(names []string) SubagentOption
- func WithRoutableAgents(names []string) SubagentOption
- func WithSharedChildWorkspace(view func(tool.Workspace) tool.Workspace) SubagentOption
- func WithSubagentEngineFactory(f func(ModelTarget) (*Engine, bool)) SubagentOption
- func WithSubagentNoFSNote() SubagentOption
- func WithSubagentOwnershipEnforced(ownershipEnforced bool) SubagentOption
- func WithSubagentProvider(provider string) SubagentOption
- func WithSubagentReadLedgerFactory(factory func() tool.ReadLedger) SubagentOption
- func WithSubagentSelectorResolver(r SubagentSelectorResolver) SubagentOption
- func WithSubagentShellDisabledNote(reason string) SubagentOption
- func WithSubagentStopHook(h port.HookRunner) SubagentOption
- func WithSubagentStore(store port.SessionStore) SubagentOption
- func WithWritableChildEngine(e *Engine) SubagentOption
- func WithWritableEngineFactory(f func(ModelTarget) (*Engine, bool)) SubagentOption
- type SubagentSelectorResolver
- type SubagentStatusTool
- func (*SubagentStatusTool) Execute(_ context.Context, call session.ToolCall, _ tool.Environment) (session.ToolResult, error)
- func (t *SubagentStatusTool) ExecuteWithParent(ctx context.Context, call session.ToolCall, env tool.Environment, ...) (session.ToolResult, error)
- func (*SubagentStatusTool) ReadOnly() bool
- func (*SubagentStatusTool) Spec() tool.ToolSpec
- type SubagentTool
- func (t *SubagentTool) Execute(ctx context.Context, call session.ToolCall, env tool.Environment) (session.ToolResult, error)
- func (t *SubagentTool) ExecuteObserved(ctx context.Context, call session.ToolCall, env tool.Environment, ...) (session.ToolResult, error)
- func (t *SubagentTool) ExecuteWithParent(ctx context.Context, call session.ToolCall, env tool.Environment, ...) (session.ToolResult, error)
- func (t *SubagentTool) MutatesParent(call session.ToolCall) bool
- func (*SubagentTool) ReadOnly() bool
- func (t *SubagentTool) Spec() tool.ToolSpec
- type Supervisor
- func (s *Supervisor) AddMember(ctx context.Context, spec MemberSpec) error
- func (s *Supervisor) CancelMember(name string) bool
- func (s *Supervisor) Close()
- func (s *Supervisor) MemberModel(name string) string
- func (s *Supervisor) MemberRouting(name string) (category, model, reason string)
- func (s *Supervisor) Run(ctx context.Context, sink func(TeamEvent)) TeamOutcome
- type SupervisorOption
- func WithForker(f tool.EnvironmentForker) SupervisorOption
- func WithMaxRounds(n int) SupervisorOption
- func WithMemberErrorRetries(n int) SupervisorOption
- func WithMemberLiveness(liveness port.SessionLiveness) SupervisorOption
- func WithMemberSessionPrefix(p string) SupervisorOption
- func WithMemberStore(store port.SessionStore) SupervisorOption
- func WithMemberTurnBudget(n int) SupervisorOption
- func WithReadOnlyForker(f tool.EnvironmentForker) SupervisorOption
- func WithRootAuthority(authority session.Authority) SupervisorOption
- func WithTeamConcurrency(n int) SupervisorOption
- func WithTeamGoal(goal string) SupervisorOption
- func WithTeamHooks(h port.HookRunner) SupervisorOption
- func WithTeamLimits(l session.Limits) SupervisorOption
- func WithTeamMode(m session.PermissionMode) SupervisorOption
- func WithTeamOwner(owner *session.Principal) SupervisorOption
- func WithTeamReadLedgerFactory(factory func() tool.ReadLedger) SupervisorOption
- func WithTeamSharedBaseWorkspace(view func(tool.Workspace) tool.Workspace) SupervisorOption
- func WithTeamTokenBudget(n int) SupervisorOption
- func WithUntrustedGoal(untrusted bool) SupervisorOption
- type TeamEvent
- type TeamMemberArg
- type TeamMemberEngineFactory
- type TeamOption
- func WithTeamSelectorResolver(r SubagentSelectorResolver) TeamOption
- func WithTeamToolForker(f tool.EnvironmentForker) TeamOption
- func WithTeamToolHooks(h port.HookRunner) TeamOption
- func WithTeamToolReadLedgerFactory(factory func() tool.ReadLedger) TeamOption
- func WithTeamToolReadOnlyForker(f tool.EnvironmentForker) TeamOption
- func WithTeamToolSharedBaseWorkspace(view func(tool.Workspace) tool.Workspace) TeamOption
- func WithTeamToolStore(s port.SessionStore) TeamOption
- func WithTeamToolTokenBudget(n int) TeamOption
- type TeamOutcome
- type TeamTool
- func (t *TeamTool) Execute(ctx context.Context, call session.ToolCall, env tool.Environment) (session.ToolResult, error)
- func (t *TeamTool) ExecuteObserved(ctx context.Context, call session.ToolCall, env tool.Environment, ...) (session.ToolResult, error)
- func (t *TeamTool) ExecuteWithParent(ctx context.Context, call session.ToolCall, env tool.Environment, ...) (session.ToolResult, error)
- func (*TeamTool) ReadOnly() bool
- func (*TeamTool) Spec() tool.ToolSpec
- type TokenCounter
- type ToolReviewRequest
- type ToolReviewResult
- type ToolReviewer
- type UserModelReviewer
Constants ¶
const ( // SubagentSessionPrefix prefixes flat-fleet Subagent children: // "subagent-<callID>" (SubagentTool's default; WithChildSessionPrefix // overrides the stem). SubagentSessionPrefix = "subagent-" // ParallelSessionPrefix prefixes Parallel fork-join branches: // "parallel-<callID>-<i>" (ParallelTool's default; // WithParallelChildSessionPrefix overrides the stem). Issue #30: the // InspectSubagent prefix gate ALSO consumes this prefix, so a persisted branch // (WithParallelStore) is loadable by its surfaced "branch id:". ParallelSessionPrefix = "parallel-" // TeamSessionPrefix prefixes team members: "team-<teamID>-<member>" — // MemberSessionID's scheme (memberSessionIDPrefix aliases this constant). TeamSessionPrefix = "team-" // ShellCmdJobPrefix prefixes background-Shell job ids: "bashcmd-<callID>". // Unlike the three delegation prefixes above it names NO session (the job // is a bare process, not a child loop), so the InspectSubagent prefix gate // and the child-session retention GC must NOT learn it — it exists only so // the job-id spelling has one source. ShellCmdJobPrefix = "bashcmd-" )
The delegation families' child-session id PREFIXES — the single exported source for the id-minting convention. Each constant is the literal prefix of a child SESSION id; the three are disjoint, and every consumer derives from them rather than re-spelling the literals: the minting sites (SubagentTool's default child prefix, ParallelTool's default branch prefix, MemberSessionID's team scheme), the InspectSubagent prefix gate (subagent- ∪ parallel-, issue #30), and the composition layer's child-session retention GC (internal/app/childgc.go's childSessionPrefixes). A deployment overriding a prefix (WithChildSessionPrefix / WithParallelChildSessionPrefix / WithMemberSessionPrefix) departs from this convention and from everything keyed on it — see those options' docs.
const ( // RouterMissDegenerateInput: a fast fail-soft miss on a leaf-guard input — a nil // classifier engine, an empty category list, or a blank task prompt. No classifier // call was made. RouterMissDegenerateInput = "degenerate-input" // RouterMissClassifierError: the classifier run ended StopError (the provider/run // failed) — fail-soft inherit. RouterMissClassifierError = "classifier-error" // RouterMissCancelled means the caller's context was observed cancelled. RouterMissCancelled = "cancelled" // RouterMissTimeout means a caller, operation, or typed classifier deadline was observed. RouterMissTimeout = "timeout" // RouterMissLowConfidence means a valid classifier result was below a configured threshold. RouterMissLowConfidence = "low-confidence" // RouterMissInputOverLimit means a local classifier input bound rejected the request. RouterMissInputOverLimit = "input-over-limit" // RouterMissCapacityTimeout means bounded classifier queue capacity expired while the caller remained active. RouterMissCapacityTimeout = "capacity-timeout" // RouterMissBadVerdict: the classifier's output was not a single JSON object, was // unparseable JSON, or named an empty category — the whole-output-single-object parse // rejected it (fail-soft, defeats a forged verdict echoed inside the fenced prompt). RouterMissBadVerdict = "bad-verdict" // RouterMissUnknownCategory: the verdict named a category NOT in the offered list (a // hallucination) — the membership check rejected it (fail-soft). RouterMissUnknownCategory = "unknown-category" )
Router miss-reason constants (issue #287): the SPECIFIC common outcome a classifier backend observed when it did NOT yield a routed model. They enumerate the CLOSED set owned by the engine; composition maps backend-local mechanisms into these values before invoking the callback. Empty ("") is the success sentinel.
NOTE: the Deps.SubagentModelRouter closure (the COMPOSITION half) may return ADDITIONAL, OPEN-SET free-form reasons for its own category-mapping misses (e.g. "category-selector-empty (category=…)", "category-target-unresolvable (category=… selector=…)", and the dispatch closure's "empty-model" fallback). Those are NOT in this enum — the full string is INFORMATIONAL for the operator log. routingReasonPayload reduces the two known composition shapes to static codes before an event is emitted and substitutes a generic code for every other open-set value; callers never branch on the free-form text.
All reasons are metadata ONLY — never the task prompt or the classifier's output (gauntlet #7).
const DefaultAskReviewMaxDenies = 3
DefaultAskReviewMaxDenies is the breaker threshold applied when Deps.ChildAskReviewMaxDenies is unset (NewEngine maps <=0 to this): after this many CONSECUTIVE non-allow outcomes (denies, failures, timeouts) within one run, further asks skip the reviewer and fall through to auto-deny — bounding reviewer spend on a run whose children keep proposing disallowed commands. An allow resets the count; an abstention (ErrNotReviewable) does not affect it.
Exported so the cmd/ flag declarations (mecated/mecatui/mecatequi) reference a single named const for their --subagent-ask-reviewer-max-denies default instead of an inline literal that could drift from this value. The drift guard lives in engine/agent/runbounds_drift_test.go.
const DefaultPreservedForkCap = 8
DefaultPreservedForkCap is the number of preserved winner forks an LRUForkReaper keeps when constructed with a non-positive cap. Small by design: preserved forks are full workspace copies, so the default trades a little inspectability headroom for a tight disk bound.
const MinSubagentRunTokens = 25_000
MinSubagentRunTokens is the floor applied to per-call MaxRunTokensOverride. The system prompt + AGENTS.md + project instructions are replayed on every turn, costing ~20k+ tokens on the very first turn of a typical workspace run. A model-supplied budget below this floor would stop the child before it can complete even one useful turn, which is never the model's intent. The floor is safe because the operator ceiling still wins via the tighten-only fold in effectiveMaxRunTokens: min(override, Deps.MaxRunTokens) — clamping the override UP to 25k never raises the effective budget above the operator bound.
Exported so tests and external callers can reference the floor value without hard-coding the magic number.
const PlanApprovedProceedText = "Plan approved by operator. Proceed with execution."
PlanApprovedProceedText is the harness-framed proceed message injected (by Wave 4's ApprovePlan service seam) as ordinary recorded history when an operator approves a presented plan, signalling the model to begin execution. It is event-silent (it is a recorded user message, NOT a diagnostics line — the loop's "exactly THREE lines" invariant holds) and mirrors the harness-note framing of the background-completion notices. Exported so the composition/service layer (which owns the proceed injection) can reference the exact text without re-stringing it; the text itself is a stable contract the model reads as the proceed signal.
const ScheduleQueryToolName = "ScheduleQuery"
ScheduleQueryToolName is the catalog name of the READ-ONLY scheduled-task query tool (list/inspect). Exported: the composition root references it for the permission floor Allow (defaultRules) and the catalog registration.
const ScheduleToolName = "Schedule"
ScheduleToolName is the catalog name of the MUTATING scheduled-task management tool (create/pause/resume/delete/fire). Exported: the composition root references it for the permission floor Allow (defaultRules) and the catalog registration.
Variables ¶
var ( // ErrReflectionLimits reports an invalid or explicitly unbounded limit set. ErrReflectionLimits = errors.New("agent: invalid reflection limits") // ErrReflectionOutput reports strict output decoding, bounds, or evidence failure. ErrReflectionOutput = errors.New("agent: invalid reflection output") // ErrReflectionProvider reports a provider start or stream failure. ErrReflectionProvider = errors.New("agent: reflection provider failure") )
var ( // ErrApprovalNotPending means the ask is stale, unknown, or already resolved. ErrApprovalNotPending = errors.New("approval is not pending") // ErrApprovalIntentMismatch means the reply does not match the pending ask's identity or scope. ErrApprovalIntentMismatch = errors.New("approval intent does not match pending ask") // ErrApprovalUnsupported means the registered ask has an unsupported origin or guardrail kind. ErrApprovalUnsupported = errors.New("approval kind or origin is unsupported") // ErrApprovalGrantIneligible means the requested verdict is invalid for this approval kind. ErrApprovalGrantIneligible = errors.New("approval grant is ineligible") )
Approval resolution errors are stable categories for hosts. Callers should use errors.Is rather than parse error text.
var ( // ErrMemberNameRequired is returned by AddMember when the spec name is empty. // It is a bad-request (caller) error. ErrMemberNameRequired = errors.New("agent: team member name is required") // ErrMemberAlreadyAdded is returned by AddMember when the supervisor already // holds a member of that name (a duplicate at the supervisor layer, distinct // from team.ErrMemberExists at the aggregate layer). It is a bad-request error. ErrMemberAlreadyAdded = errors.New("agent: member already added") // ErrSupervisorClosed is returned when enrolment is attempted after Close or Run // has claimed the supervisor lifecycle. ErrSupervisorClosed = errors.New("agent: team supervisor is closed") // ErrNoForker is returned by AddMember when a Mutating member is requested but // no EnvironmentForker is configured. This is a server MISCONFIGURATION (the // composition root did not wire a forker), not a bad client request. ErrNoForker = errors.New("agent: Mutating member requires a configured EnvironmentForker") // ErrForkWorkspace wraps a failure to fork a Mutating member's workspace. It is // an I/O / internal fault. ErrForkWorkspace = errors.New("agent: fork member workspace") // ErrNilEngine is returned by AddMember when the member-engine factory returns // a nil Engine. It is a server-internal fault (a broken factory). ErrNilEngine = errors.New("agent: member engine factory returned nil") // ErrReadOnlyMemberMutating is returned by AddMember when a BASE-SHARING member's // catalog contains a workspace-mutating tool. It is a server MISCONFIGURATION of // the member's catalog, not a bad client request. It is gated on base-sharing // (neither Mutating nor read-only-isolated): a worktree-isolated read-only member // is exempt exactly like a mutating one, because its mutating-classified Shell // lands in its own throwaway worktree, never the shared base. ErrReadOnlyMemberMutating = errors.New("agent: read-only member given workspace-mutating tool") // ErrReadOnlyShellNoForker is returned by AddMember when a member's factory marked // it read-only-isolated (MemberBuild.IsolateReadOnly — it put Shell into a // non-mutating member's catalog) but no read-only forker is configured. It is a // should-never-happen server MIS-WIRE assertion: the composition layer only sets // IsolateReadOnly when the read-only forker is wired, so this guards the two from // drifting apart. ErrReadOnlyShellNoForker = errors.New("agent: read-only-isolated member requires a configured read-only EnvironmentForker") )
AddMember failure-class sentinels. They let a caller (e.g. the gRPC adapter) classify an enrolment failure into the right wire status instead of collapsing every failure to "invalid argument". The team-aggregate failures (duplicate / reserved / too-many members) are NOT re-wrapped here — callers test those with errors.Is against the team package's own sentinels (team.ErrMemberExists, team.ErrReservedName, team.ErrTooManyMembers), which AddMember already wraps via %w through s.team.AddMember.
var ErrCompactionWouldOrphan = errors.New("agent: compaction would orphan a tool result or dangle a tool call")
ErrCompactionWouldOrphan is the sentinel a Compactor returns when the only history it could produce would be tool-pairing-invalid (an orphaned tool result or a dangling tool call). The loop treats it like any other compaction failure — keep the original history and continue without compaction — rather than emitting a history that draws a provider HTTP 400 and bricks the session. Compactors return the ORIGINAL messages alongside this error so a caller that ignores the sentinel still gets a safe slice.
var ErrNotAwaiting = errors.New("agent: session is not awaiting the given approval")
ErrNotAwaiting is the terminal cause driveFromAwaiting fails with when it is asked to resume a session that is NOT parked on a pending ask (no PendingAsk), or whose pending ask does not match the askID the resume targets. It never silently completes: a mismatch ends the run as StopError carrying this cause so a stale/duplicate Approve cannot drive an unexpected session to a clean terminal.
var ErrNotReviewable = errors.New("agent: child ask not reviewable by this reviewer")
ErrNotReviewable is the sentinel a ChildAskReviewer returns to ABSTAIN from a particular ask (for example a reviewer that only judges shell commands, asked to review a different tool). It is distinct from a genuine review failure: an abstention falls through to the default headless auto-deny WITHOUT counting against the consecutive-failure breaker, so a reviewer that legitimately cannot judge some asks never silences review for the asks it can judge. Every OTHER error counts toward Deps.ChildAskReviewMaxDenies.
Functions ¶
func DefaultChildLimits ¶
DefaultChildLimits returns the default per-child/per-member stop conditions a Subagent tool (and a team member, via WithTeamLimits) runs under when the caller does not override them. The composition layer uses it as the per-field FALLBACK when deriving a def's session.Limits from its maxTurns/maxToolCalls: a zero def field inherits the matching default here, so a def that sets neither is bounded exactly as before.
func MemberSessionID ¶
MemberSessionID derives the COLLISION-FREE session id for one team member, namespaced by the team id: "team-<teamID>-<member>". It is the SINGLE source of truth for the member-session id scheme — the Team tool seeds the supervisor's member-session prefix from it, and the InspectMemberTool derives an id with it — so the producer (the supervisor, which saves the session) and the consumer (the inspect tool, which loads it) cannot drift. Because teamID is the parent call id (Team tool) or the server-assigned "team-<NewID()>" (gRPC path), two concurrent teams sharing a member name still get distinct ids.
CONTRACT — teamID MUST be the EXACT team id published on the wire: the value on EvTeamStart.TeamID, the Team tool's call id, and the CreateTeam/CreateTeamResponse team_id. Pass it VERBATIM — never normalised, trimmed, or re-prefixed. The string is intentionally NOT canonicalised here: the gRPC path's published team id is itself "team-<NewID()>", so the saved id is "team-team-<NewID()>-<member>" — that double "team-" is CORRECT and load-bearing, because the only caller that derives an inspect id (InspectMemberTool) passes the SAME published "team-<NewID()>" string, so producer and consumer agree byte-for-byte. The Team-tool path publishes the parent call id as the team id (no "team-" of its own), so its saved id is "team-<callID>-<member>". Both paths are pinned by round-trip tests (TestMemberSessionIDRoundTripsTeamToolPath / ...GRPCPath). Changing the published team-id string would change these saved ids — do not normalise it to "fix" the double prefix.
func MemberToolNames ¶
func MemberToolNames() map[string]struct{}
MemberToolNames returns the set of coordination-tool names MemberTools installs into every member's catalog. These tools report ReadOnly() == false because they mutate TEAM state, but they NEVER touch the workspace, so they are safe for a read-only (base-sharing) member. The supervisor uses this set to distinguish a member's coordination tools from genuine WORKSPACE-mutating tools (Edit / Write / non-read-only Shell) when it enforces the read-only-member invariant in AddMember. It is kept in lock-step with MemberTools by construction: it derives the names from MemberTools over a throwaway team.
func MemberTools ¶
MemberTools returns the coordination tools bound to the shared team t and the member name self. The composition root registers these into every member's catalog (including the lead's) so team coordination is always available to a member even when an agent definition otherwise restricts its tools. hooks, when non-nil, fires the TaskCreated / TaskCompleted lifecycle gates from AddTask / CompleteTask (a Block outcome vetoes the action); pass nil to disable them.
func NewInspectMemberTool ¶
func NewInspectMemberTool(store port.SessionStore) tool.Tool
NewInspectMemberTool constructs the legacy-compatible InspectMember tool over a session store. Without a verified-caller request edge, ownership enforcement stays disabled.
func NewInspectMemberToolWithOwnership ¶
func NewInspectMemberToolWithOwnership(store port.SessionStore, ownershipEnforced bool) tool.Tool
NewInspectMemberToolWithOwnership constructs InspectMember with the request edge's ownership policy. When enforcement is enabled, only the owner with the same (Issuer, Subject) pair may read a persisted member transcript.
func NewInspectSubagentTool ¶
func NewInspectSubagentTool(store port.SessionStore) tool.Tool
NewInspectSubagentTool constructs the legacy-compatible InspectSubagent tool over a session store. Without a verified-caller request edge, ownership enforcement stays disabled.
func NewInspectSubagentToolWithOwnership ¶
func NewInspectSubagentToolWithOwnership(store port.SessionStore, ownershipEnforced bool) tool.Tool
NewInspectSubagentToolWithOwnership constructs InspectSubagent with the request edge's ownership policy. When enforcement is enabled, only the owner with the same (Issuer, Subject) pair may read a persisted child transcript.
func NewParallelTool ¶
func NewParallelTool(childEngine *Engine, forker tool.EnvironmentForker, opts ...ParallelOption) tool.Tool
NewParallelTool constructs the Parallel fan-out tool over a pre-built child *Engine and an EnvironmentForker. The composition root builds childEngine with the SCOPED child catalog and a non-interactive policy (see NewSubagentTool's guidance); the child catalog MUST NOT contain Parallel or Subagent (so a branch cannot fan out further). childEngine and forker must be non-nil; NewParallelTool panics otherwise, because a Parallel tool with no child loop or no isolation seam is a composition-root programming error.
func NewPlanAwareScheduleTool ¶
NewPlanAwareScheduleTool wraps the MUTATING Schedule tool for a PLAN-MODE session's catalog (the AC4.3 gate). The default mutating tool reports ReadOnly()==false, so the plan-mode catalog projection (engine/tool/catalog.go Available(ModePlan)) would hide the WHOLE tool — including the read-leaning create plan mode must keep (a schedule CREATE does not itself mutate the workspace; the FIRE's posture is pinned at create-time by the Mutating/Mode invariant). The plan-aware variant reports ReadOnly()==true (so the plan-mode projection advertises it) and hard-denies the mutating shapes per call with the plan-mode deny reason BEFORE the base tool runs — the read-leaning create (mutating:false) drives through unchanged. The read-only ScheduleQueryTool needs no wrapper: it is ReadOnly()==true, so the plan-mode projection advertises it as-is.
The wrapper's ReadOnly()==true is sound because the ONLY call shapes it lets through are the read-leaning ones: none mutate the workspace (a CREATE writes the schedule REGISTRY, not the tree; a read-leaning schedule's FIRE runs in plan mode), so no admitted plan-mode call can mutate. mgr is the SAME ScheduleManager the base drives — the fire gate reads the schedule's pinned Mutating posture from it (the fire verb carries no mutating flag of its own). The non-plan engine keeps the DEFAULT tool (the read/mutate serialization contract is unchanged).
func NewPresentPlanTool ¶
NewPresentPlanTool constructs the PresentPlan tool — the signalling affordance the model calls once it has presented a complete plan for operator approval. The tool is read-only and signaling-only; in plan mode the dispatcher intercepts it by name (presentPlanToolName) and surfaces the ask, so Execute is only reached on a misroute.
func NewScheduleQueryTool ¶
func NewScheduleQueryTool(mgr port.ScheduleManager) tool.Tool
NewScheduleQueryTool constructs the read-only Schedule query tool over the injected port.ScheduleManager. mgr must be non-nil; NewScheduleQueryTool panics otherwise (the same composition-root programming-error contract as NewScheduleTool). Composition registers it alongside the mutating Schedule tool when the session's store backs a ScheduleStore.
func NewScheduleTool ¶
func NewScheduleTool(mgr port.ScheduleManager) tool.Tool
NewScheduleTool constructs the mutating Schedule tool over the injected port.ScheduleManager. mgr must be non-nil; NewScheduleTool panics otherwise (a composition-root programming error — the tool has nothing to drive without the seam). Composition registers the tool ONLY when the session's store backs a ScheduleStore (the scheduleStore() != nil gate), so a nil manager never reaches the catalog.
func NewShellStatusTool ¶ added in v0.15.0
NewShellStatusTool constructs the ShellStatus tool. It is stateless: all state lives in the per-run registry handed down via parentCaps.
func NewShellTool ¶ added in v0.15.0
NewShellTool constructs the Shell tool. The runner is NOT captured here — it is read off the tool.Environment at Execute time (issue #462). The composition root registers the returned tool ONLY when a runner is available for the namespace; without one, the catalog has no Shell and the agent runs shell-less. Background calls additionally require the bound runner to implement tool.CommandStreamer (they decline honestly when it does not).
func NewSubagentStatusTool ¶
NewSubagentStatusTool constructs the SubagentStatus tool. It is stateless: all state lives in the per-run registry handed down via parentCaps.
func NewSubagentTool ¶
func NewSubagentTool(childEngine *Engine, opts ...SubagentOption) tool.Tool
NewSubagentTool constructs the Subagent tool tool over a pre-built child *Engine.
The composition root (cmd/mecated, WP11) is responsible for building childEngine with the SCOPED child catalog and policy. The recommended, deterministic wiring is:
- Catalog: a read-only explorer set — Read, Grep, Glob ONLY. It MUST NOT contain the Subagent tool (otherwise a subagent could spawn subagents — infinite recursion) and SHOULD NOT contain mutating tools (Edit/Write/non-RO Shell): the default explorer subagent cannot mutate the workspace.
- Policy: allow-all over those read-only tools (e.g. permpolicy.NewPolicy([]governance.Rule{{Effect: governance.Allow}})), so the child never produces a permission "ask". Subagents are one-shot and non-interactive — there is no human on the other end of a child run.
Even with that wiring, Execute defends the non-interactive invariant: if the child loop ever pauses on a permission ask, Execute auto-resolves it as DENY so the child can never block waiting for a human. This keeps the subagent deterministic regardless of the policy it is given.
childEngine must be non-nil; NewSubagentTool panics otherwise, because a Subagent tool with no child loop to delegate to is a programming error at the composition root.
func NewTeamTool ¶
func NewTeamTool(factory TeamMemberEngineFactory, opts ...TeamOption) tool.Tool
NewTeamTool constructs the Team tool over a per-member engine factory. factory must be non-nil; NewTeamTool panics otherwise (a composition-root programming error — a Team tool with no way to build member engines cannot run a team).
func ParseReflectionOutcome ¶
func ParseReflectionOutcome(in learning.Input, raw []byte, limits ReflectionLimits) (learning.Outcome, error)
ParseReflectionOutcome strictly parses and validates one reflector response.
func RejectDuplicateJSONKeys ¶ added in v0.16.0
RejectDuplicateJSONKeys validates one complete JSON value and rejects duplicate object keys at every nesting depth before typed decoding can apply last-key-wins.
func RemapAuxiliaryUsage ¶ added in v0.16.0
func RemapAuxiliaryUsage(ctx context.Context, diag port.Diagnostics, purpose session.UsageKind, in session.AuxiliaryUsage) session.AuxiliaryUsage
RemapAuxiliaryUsage confines a producer result to the caller-owned purpose while preserving every reported model attribution and its spend.
func RunGuardrailCheck ¶
func RunGuardrailCheck(ctx context.Context, engine *Engine, prompt string) (string, session.AuxiliaryUsage, error)
RunGuardrailCheck drives a dedicated, tool-less one-turn Engine over a fully assembled checker prompt and returns its raw final text. A run failure or cancellation is an error (never a fabricated reply), so a caller can treat a failed check distinctly from a verdict. The drive is bounded by a hard timeout derived from ctx.
engine must be a tool-less checker Engine that fires no hooks and carries no nested reviewer, so a check can never recurse or call a tool. It returns an error on a nil engine rather than panicking, since it is a leaf helper.
func RunModelRouter ¶
func RunModelRouter(ctx context.Context, engine *Engine, req ModelRouteRequest) (category string, usage session.AuxiliaryUsage, missReason string, ok bool)
RunModelRouter drives a dedicated, tool-less one-turn classifier Engine over a fenced classification prompt and returns the chosen CATEGORY NAME plus purpose-attributed router usage. It mirrors RunGuardrailCheck: bounded by modelRouterTimeout, drained under a zero-capability child posture (role "model-router"), and FAIL-SOFT — failure, cancellation, deadline expiry, an invalid verdict, or an unknown category returns ("", observed usage, canonical miss reason, false) so the caller inherits the default model. It never returns a Go error; callers may inspect the common miss reason without parsing provider error text.
Usage is returned on EVERY path including early-return degenerate inputs (zero usage) and fail-soft miss paths (whatever was spent before the failure) so the caller can always fold it unconditionally.
engine must be a tool-less classifier Engine that fires no hooks and carries no nested reviewer/router (built via the composition's childEngineDepsForProvider recipe), so a classification can never recurse or call a tool. A nil engine, an empty category list, or a blank task prompt is a fail-soft miss (ok=false), never a panic — it is a leaf helper on the fast path.
func StripLoneCodeFence ¶
StripLoneCodeFence removes a single surrounding ```…``` fence (optionally language-tagged) from s, returning the inner text trimmed; if s is not a lone fenced block it is returned unchanged.
func TeamStop ¶
func TeamStop(o TeamOutcome) session.StopReason
TeamStop maps a TeamOutcome to its terminal StopReason — the SINGLE quiescent/budget/round-cap rule teamStop applies for EvTeamEnd, exported so the wire layers can stamp the same string-passthrough stop onto the terminal RunTeam outcome frame without duplicating the rule (issue #36).
func TightenTeamTokenBudget ¶
TightenTeamTokenBudget folds a per-request team token budget into the server-configured one, TIGHTEN-ONLY (issue #36): a non-positive request inherits the server budget verbatim; a positive request applies only when it is LOWER than the server's bound (with a 0 server budget meaning "unlimited", so any positive request tightens it). It delegates to the single tightenLimit algorithm the per-call Subagent/Team overrides use — the caller (the wire CreateTeam handlers) can therefore never loosen the operator's ceiling.
func UtilityEngineUsage ¶ added in v0.16.0
func UtilityEngineUsage(kind session.UsageKind, identity session.ProviderModelID, sess *session.Session) session.AuxiliaryUsage
UtilityEngineUsage returns every model call made by a utility engine. Its main call is attributed from composition; nested utility work such as tier-4 compaction retains the exact attribution already recorded on the utility session. The owning caller remaps every returned bucket to its fixed purpose.
func ValidateApprovalResolution ¶ added in v0.16.0
func ValidateApprovalResolution(ask session.PendingAsk, resolution ApprovalResolution) error
ValidateApprovalResolution checks a resolution against the exact pending ask.
Types ¶
type AgentMeta ¶
type AgentMeta struct {
// Name is the agent def's routing key (the value the model passes as `agent`).
Name string
// Description is the one-line summary the model uses to choose a specialist.
Description string
// Provider is the concrete provider used by this specialist.
Provider string
// Limits are the per-def session stop conditions the child session runs under
// when this agent is selected. The composition root derives them from the def's
// maxTurns/maxToolCalls (per-field falling back to the Subagent tool's default
// limits), so a def with no limits carries the same bound as the default
// explorer. A zero Limits value is treated as "no per-def override" — Execute
// then uses the Subagent tool's default limits, exactly as the no-`agent` path does.
Limits session.Limits
// AuthorityCeiling is the resolved read-only definition tool ceiling. It is
// honoured only when Managed is true; lower tiers can never establish a ceiling.
AuthorityCeiling governance.CapabilitySet
// WritableAuthorityCeiling is the corresponding direct-write ceiling used only
// for a fresh mode:"read-write" delegation to this definition.
WritableAuthorityCeiling governance.CapabilitySet
Managed bool
}
AgentMeta is the plain (name, description) summary of one registered agent definition, surfaced in the Subagent tool's Spec().Description for progressive disclosure. It is a layering-clean value type: the composition root translates the agents adapter's Registry into a []AgentMeta + a map[string]*Engine and injects both via WithAgentEngines, so engine/agent never imports the agents adapter.
type ApprovalResolution ¶ added in v0.16.0
type ApprovalResolution struct {
AskID string
ReviewID string
Kind session.GuardrailApprovalKind
Verdict session.ApprovalVerdict
}
ApprovalResolution atomically identifies and resolves one registered approval. ReviewID and Kind must exactly acknowledge a guardrail-scoped ask; ordinary permission asks leave both empty. VerdictAllowAlways is ineligible for result release, which supports only release-once or deny.
type ArtifactHandle ¶ added in v0.13.0
type ArtifactHandle string
ArtifactHandle is an opaque handle for a preserved delegation artifact. It is deliberately distinct from the server's placement-selector protocol: artifact discovery never grants placement authority, and no placement API accepts this value.
type AskResolution ¶ added in v0.15.0
type AskResolution uint8
AskResolution reports the atomic result of resolving an ordinary permission ask through Run.ResolveOrdinaryAsk.
const ( // AskResolutionNotPending means the ask is unknown or already resolved. AskResolutionNotPending AskResolution = iota // AskResolutionResolved means this call delivered the verdict exactly once. AskResolutionResolved // AskResolutionPlanOriginated means the ask belongs to the dedicated plan // resolution choreography and remains pending. AskResolutionPlanOriginated // AskResolutionNotPlan means the pending ask is ordinary and was not consumed. AskResolutionNotPlan )
type BranchJudge ¶
type BranchJudge interface {
// Judge picks the winner among candidates, guided by criteria. It returns the
// 0-based position WITHIN candidates (not a branch index) and a short rationale.
// Implementations MUST be non-interactive and bounded. ParallelTool treats a nil
// error with an out-of-range index — or any error — as "fall back to the first
// successful branch", so a judge must never be load-bearing for correctness.
Judge(ctx context.Context, candidates []BranchSummary, criteria string) (winner int, rationale string, usage session.AuxiliaryUsage, err error)
}
BranchJudge selects a winning branch from candidate summaries. It is the reusable selection seam for the Parallel "judge"/"best" strategy (and, later, a team tournament finish — AGENT-TEAMS-SPIKE §8.7). It is an interface so engine/agent never imports an adapter (the default impl runs an *Engine injected by the composition root) and so a non-LLM scorer can be substituted in tests/future.
func NewEngineJudge ¶
func NewEngineJudge(engine *Engine, opts ...EngineJudgeOption) BranchJudge
NewEngineJudge constructs the default LLM-over-child-Engine BranchJudge. engine must be non-nil; it is the dedicated judge Engine the composition root builds (its own catalog/provider, distinct from the branch child Engine so their LLM calls never interleave). It panics on a nil engine — a judge with no loop to run is a composition-root programming error.
type BranchSummary ¶
type BranchSummary struct {
// Label is the branch's stable, human-meaningful tag (e.g. "branch-2").
Label string
// Summary is the branch's terminal result text.
Summary string
// Failed reports whether the branch failed. ParallelTool only ever passes
// SUCCESSFUL candidates to a judge (you cannot pick a crashed branch), so this
// is false in the default flow; the field exists so a future caller (e.g. a
// team tournament) can pass the full set if it wants.
Failed bool
}
BranchSummary is the compact, transcript-free view of one candidate branch the judge scores. It is exactly the information a human would get from the joined summary — Label + Summary (and whether the branch Failed) — never the branch's intermediate transcript, so judging preserves Parallel's context-isolation guarantee.
type CascadeCompactor ¶
type CascadeCompactor struct {
// Counter sizes the history between tiers; nil → HeuristicTokenCounter.
Counter TokenCounter
// BudgetTokens is the target the cascade reduces toward. Tiers stop running
// once the history is at or below it. Zero means "run every deterministic
// tier once" (snip→strip→collapse), which is the offline default.
BudgetTokens int
// KeepLastTurns overrides cascadeKeepLastTurns when > 0.
KeepLastTurns int
// StripToolBodyChars overrides cascadeStripToolBodyChars when > 0.
StripToolBodyChars int
// MaxCollapseChars overrides cascadeMaxCollapseChars when > 0.
MaxCollapseChars int
// LLM, when non-nil, enables tier 4 (LLM summary of the oldest segment). When
// nil the cascade stops at tier 3 and never makes a model call.
LLM port.LLMProvider
// ProviderModel is the exact provider/model selected for the tier-4 call.
ProviderModel session.ProviderModelID
// Model is the model identifier passed to the LLM on the tier-4 summary call.
Model string
// SummaryMaxTokens is the SOFT size budget for the tier-4 summary, expressed
// in the summariser PROMPT (the trailing instruction), not enforced — the
// model may overshoot and the cascade accepts it (fail-open). It is NOT a
// port.LLMRequest field: the request stays provider-neutral, so the budget
// rides as prompt text only. Zero means defaultSummaryMaxTokens.
SummaryMaxTokens int
}
CascadeCompactor is a tiered, cheapest-first Compactor (harness pattern 5, doc 07 §4 / doc 08 #12). It applies up to four tiers in order, stopping as soon as the history fits the budget:
- snip — drop the oldest low-value turns (assistant/tool pairs and old user chatter) outside the preserved head (system + goal) and tail (recent K).
- strip — truncate large tool-result bodies to a small budget, eliding the rest with a marker (the lightest-touch "tool-result clearing").
- collapse — replace large file-read tool results with a path+size pointer, dropping the body entirely (the model can re-read on demand).
- summarize — if still over budget AND an LLMProvider is injected, ask the model for a compact STRUCTURED summary of the oldest segment (the section-locked summarizerSystemPrompt template) and replace it. With no LLM injected the cascade STOPS at tier 3, remaining fully deterministic and offline-testable.
Across every tier it preserves the system prompt, the user goal, all touched file paths (as a synthesised summary message), and the recent tail — back-snapped to recent USER turns (snapCutToRecentUserTurn) so the most-recent user instruction survives verbatim instead of being summarised into the middle (prior art: Codex, gemini-cli; docs/harnesses/07-context-and-mcp.md §4, 03-claude-code-architecture.md, 08-design-considerations.md §12). It drops file bodies, verbose tool output, and old stack traces — the doc-08 "what to preserve / what to drop" contract.
func (CascadeCompactor) Compact ¶
func (c CascadeCompactor) Compact(ctx context.Context, conv *session.Conversation) ([]session.Message, string, session.AuxiliaryUsage, error)
Compact implements Compactor by running the tiered cascade. It always returns a reduced (or equal) history and a human-readable summary of what each tier did; it returns an error only when tier-4 summarisation fails — an LLM call error or an EMPTY summary (tiers 1–3 cannot fail) — or when the assembled history would orphan a tool pairing (ErrCompactionWouldOrphan). In both error cases the ORIGINAL history is returned alongside the error (abort-to-original).
type ChildAskReview ¶
type ChildAskReview struct {
// Allowed reports the verdict. An allow approves THIS ONE call only — it is
// never learned as a rule, so an identical later command is reviewed again.
Allowed bool
// Reason is the reviewer's short rationale. On a deny it is folded (clamped)
// into the message the child model sees and into the operator audit line; on
// an allow it rides the audit line.
Reason string
}
ChildAskReview is one reviewer verdict over a child's permission ask.
type ChildAskReviewRequest ¶
type ChildAskReviewRequest struct {
// Ask is the child's pending permission ask under review.
Ask session.PendingAsk
// Isolated reports whether the command would run inside an isolated,
// throwaway worktree/fork (its filesystem effects confined to a disposable
// copy of the workspace; the process, network, and absolute paths are NOT
// isolated). A reviewer may weigh this when judging filesystem-mutating
// commands.
Isolated bool
}
ChildAskReviewRequest is the input to one review. It is a struct (not a positional argument list) so future inputs — a child role, a workspace hint — can be added without breaking external reviewer implementations.
type ChildAskReviewer ¶
type ChildAskReviewer interface {
Review(ctx context.Context, req ChildAskReviewRequest) (ChildAskReview, session.AuxiliaryUsage, error)
}
ChildAskReviewer adjudicates a child agent's (subagent / team member / parallel branch) permission ask that the harness could not resolve from its static rules and that no attached human can answer — the alternative to a blanket auto-deny on a non-interactive run.
CONSUMER CONTRACT:
- It is consulted ONLY for a headless, NON-configured ask: a deliberately configured "ask" rule always demands a human and is never delegated here, and an interactive run surfaces the ask to its client instead.
- Each call is bounded by a 30-second deadline (the ctx carries it); a reviewer that has not answered by then is treated as a failure.
- An allow grants the ONE call under review — nothing is learned.
- Return ErrNotReviewable to ABSTAIN (falls through to auto-deny, does NOT count against the breaker). EVERY OTHER error — and any unparseable or ambiguous verdict — falls through to auto-deny AND counts toward the consecutive-failure breaker (Deps.ChildAskReviewMaxDenies). A reviewer is therefore FAIL-SAFE and never load-bearing for safety.
- Implementations MUST be non-interactive and safe for concurrent calls.
It is an interface so the engine never depends on a concrete reviewer: the default implementation runs an injected one-turn Engine, and a consumer may substitute its own policy engine.
func NewEngineAskReviewer ¶
func NewEngineAskReviewer(engine *Engine, opts ...EngineAskReviewerOption) ChildAskReviewer
NewEngineAskReviewer constructs the default LLM-over-Engine ChildAskReviewer. engine must be non-nil; it is the dedicated, tool-less reviewer Engine the caller builds. It panics on a nil engine — a reviewer with no loop to run is a programming error.
type Compactor ¶
type Compactor interface {
Compact(ctx context.Context, conv *session.Conversation) (compacted []session.Message, summary string, usage session.AuxiliaryUsage, err error)
}
Compactor compresses a Conversation that has grown past the context-window threshold into a shorter, semantically-equivalent history. It is a seam (ARCHITECTURE.md §8, gauntlet #12): the default implementation is a pure, offline heuristic, but an LLM-backed summariser can be slotted in behind the same interface.
Compact returns the replacement message slice, a human-readable summary of what was dropped (surfaced on the compaction Event), and an error only on a genuine failure. Implementations MUST preserve file paths, decisions, and unresolved questions while dropping large tool-output bodies and stale file contents.
type DelegationTightening ¶
type DelegationTightening struct {
Tools []string `json:"tools,omitempty"`
RemainingDelegationDepth *int `json:"remaining_delegation_depth,omitempty"`
FileSystem *bool `json:"filesystem,omitempty"`
DirectWrite *bool `json:"direct_write,omitempty"`
}
DelegationTightening is the optional per-call capability reduction requested for a Subagent. Nil fields inherit the already-derived value.
type Deps ¶
type Deps struct {
// LLM is the model-call seam.
LLM port.LLMProvider
// Catalog is the tool registry; the loop reads Specs(mode) and Lookup(name).
Catalog *tool.Catalog
// Policy evaluates each tool call (deny → ask → allow).
Policy port.PermissionPolicy
// AuthorityEvaluator authorizes executions for sessions carrying a derived
// authority set. Bound sessions require an evaluator; composition selects either
// enforcement or the explicit noop evaluator when it mints bound sessions.
AuthorityEvaluator port.AuthorityEvaluator
// Hooks runs the PreToolUse / PostToolUse lifecycle hooks.
Hooks port.HookRunner
// ToolReviewer performs contextual action and inbound review when configured.
ToolReviewer ToolReviewer
// ReviewEvidencePreparer mints authority-bound finite evidence inventories for
// each contextual review. A nil preparer yields an explicitly incomplete inventory.
ReviewEvidencePreparer ReviewEvidencePreparer
// ReviewDetails receives bounded live-only human review detail.
ReviewDetails ReviewDetailSink
// ReviewTaskWindow selects the last K accepted genuine root prompts supplied as
// principal task facts. Values are clamped to 1..3; zero defaults to 1.
ReviewTaskWindow int
// PlanApprovals owns process-local, single-use plan approval receipts.
PlanApprovals PlanApprovalStore
// Store persists session state (optional; nil disables persistence).
Store port.SessionStore
// SessionLiveness is the optional lifecycle-exclusion seam for engine-owned
// child sessions. The parent run registers each delegation child before it can
// queue or drive and releases it on every terminal or pre-start-abort path.
// Composition may back it with both process-local tracking and SessionLease;
// registration failure prevents the child from becoming runnable.
SessionLiveness port.SessionLiveness
// Clock supplies wall time for tool-call timing (optional; nil → no timing).
Clock port.Clock
// ToolCallRecorder records tool-execution observability — the tool-call audit
// seam (optional; nil → no recording).
ToolCallRecorder port.ToolCallRecorder
// Sink, when non-nil, also receives every Event the loop emits, in addition
// to the Run.Events() channel which is always the primary surface.
Sink port.EventSink
// EnableDurableEvidence emits debugger-only request manifests and accepts
// sanitized provider-attempt observations. It is opt-in because constructing
// that evidence requires hashing/counting, contexts, maps, and slices; hosts
// should enable it only when their relay persists events to a durable EventLog
// for later inspection. Sink presence is not a durability signal.
EnableDurableEvidence bool
// Diagnostics is the general-purpose operational logging seam (optional; nil →
// port.NopDiagnostics, applied in NewEngine, so the engine never nil-panics and
// stays silent when no sink is injected). It is DISTINCT from ToolCallRecorder
// (the per-tool audit seam) and Sink (the model's conversation stream).
Diagnostics port.Diagnostics
// LearningMode and LearningObserver install the optional completed-trajectory
// observation seam. Observation is synchronous and runs once after each eligible
// clean completion. The zero mode, Off, or a nil observer is inert.
LearningMode learning.Mode
LearningObserver learning.Observer
// Compactor compresses history at the threshold; nil → HeuristicCompactor.
Compactor Compactor
// TokenCounter estimates history size for the compaction trigger (and is
// shared with the Compactor); nil → HeuristicTokenCounter.
TokenCounter TokenCounter
// Instructions assembles the project-instruction messages recorded once at
// the start of a run; nil → prompt.RootAssembler (root-only AGENTS.md /
// CLAUDE.md, the v1 default).
Instructions prompt.InstructionAssembler
ProjectInstructionMaxBytes int
// CommandExpander rewrites a raw user prompt into the text the model sees,
// expanding slash-command invocations (e.g. "/review foo.go") against the
// workspace before the prompt is recorded; nil → prompt.NoopExpander (no
// expansion, the v1 default).
CommandExpander prompt.CommandExpander
// PromptConfig seeds the cache-stable system prompt (role/tone/safety). The
// loop fills in Tools and the volatile Env per turn.
PromptConfig prompt.Config
// PromptBuilder assembles the layered system prompt each turn. nil →
// prompt.Build (the v1 default: coding-agent role/tone/safety + tool
// inventory), byte-identical to v0.0.1. A host that needs a fully host-owned
// system prompt (e.g. a non-coding agent) supplies a prompt.Builder here; it
// receives the same Config the loop builds (Tools + volatile Env and live
// operator profile filled per turn) and returns the Layered prompt. Only the
// MAIN loop's buildRequest routes through this field — the compaction summarizer
// (cascade.go) builds its own prompt.Layered directly and is unaffected.
PromptBuilder prompt.Builder
// OperatorProfileSource supplies full durable user facts. When non-nil it is
// read before every model request; failures are fail-soft and retain the last
// good snapshot within the run.
OperatorProfileSource prompt.OperatorProfileSource
// Model is the provider model identifier sent on every request.
Model string
// ProviderModel is the exact immutable provider/model identity selected by
// composition for this engine's primary LLM calls. Utility-result collection uses
// it to attribute this engine's main usage; nested auxiliary usage retains its
// producer's own attribution. The request itself remains provider-neutral.
ProviderModel session.ProviderModelID
// ContextWindow returns the model's context window in tokens, resolved LIVE at
// the point of use (the compaction check / Engine.ContextWindow) rather than
// frozen at construction — so a post-construction live-catalog refresh self-
// corrects on the next turn without rebuilding the engine. A nil closure OR a
// <=0 return DISABLES compaction (preserving the old "zero disables"). The
// closure is a stdlib func value; it is built in composition (internal/app)
// over the FIXED (provider, model) so the engine itself imports no adapter.
ContextWindow func() int
// CompactionRatio overrides defaultCompactionRatio when in (0,1].
CompactionRatio float64
// Role is the operator-facing label this engine logs under in diagnostics: the
// empty string for the MAIN engine (correlated by session only), or a non-empty
// role for a child/subagent engine (e.g. "task" for the Subagent tool, a team
// member's name, or a fork-branch label) so interleaved child diagnostics are
// readable. It is read once per run when binding the run-scoped Diagnostics (see
// drive): empty → only the "session" key; set → "session"+"agent" keys. It is
// NOT plumbed into Sink/ToolCallRecorder (those stay off for children) and never
// reaches the model.
Role string
// MaxNoProgressNudges bounds how many times the loop injects a continuation
// ("please continue") user message after a completed turn that produced NEITHER a
// tool call NOR meaningful assistant text (a reasoning-only / empty turn). It is
// the blast-radius knob for the no-progress handler. Semantics (applied in
// NewEngine): a ZERO value (the default; existing Deps built without it) uses the
// safety-net default defaultNoProgressNudges (2); a NEGATIVE value DISABLES nudging
// entirely (a no-progress turn terminates immediately with StopNoProgress, the old
// behaviour minus the silent StopEndTurn mislabel); a positive value overrides the
// default. It is a loop concern, exactly like CompactionRatio — NOT a
// port.LLMRequest field (the request stays provider-neutral). Composition plumbs it
// from Config so it is operator-tunable; children inherit the default.
MaxNoProgressNudges int
// Interactive reports whether a HUMAN approver is attached to this engine's runs:
// true for the bidi Converse / HTTP-SSE surfaces where a client can answer a
// permission ask, false for a headless/in-process run (the demo, a RunTeam with no
// attached client). It is composition's knowledge of whether an approval UI exists,
// set on the MAIN engine only and read once per run (drive) to (a) install the
// child-ask router so a subagent's ask can be SURFACED to the human, and (b) tell a
// subagent posture whether to surface (interactive) or auto-deny (headless). It is a
// plain bool — NOT a port.LLMRequest field and never reaches the model. Child engines
// leave it false (a child never surfaces further). DEFAULT false (fail-safe: a run
// with no declared approver auto-denies a subagent ask rather than hanging).
Interactive bool
// PlanModeAutoApprove is an OPT-IN, OPERATOR-TIER-ONLY, DEFAULT-OFF flag that
// tells the engine to SURFACE a plan-approval ask (PresentPlan) even when headless
// (no human approver attached), so the composition layer's Service can auto-resolve
// it via ApprovePlan without operator interaction. It is DELIBERATELY ONLY the
// PresentPlan gate — a non-plan ask (policy/hook) is still headless-auto-denied.
// DEFAULT false (fail-safe: a headless plan ask is auto-denied like every other ask).
// It is a plain bool — NOT a port.LLMRequest field and never reaches the model.
// Child engines inherit it from the parent (so a Subagent/team child's plan ask also
// parks rather than auto-denies), allowing composition to auto-approve at the Service
// layer.
PlanModeAutoApprove bool
// ChildAskReviewer, when non-nil, reviews a child agent's (subagent / team
// member / parallel branch) permission ask that the harness could not resolve
// statically and that no attached human can answer — instead of blanket-denying
// it. It is consulted ONLY for a headless run with no interactive approver, and
// never for a deliberately-configured "ask" rule (that always demands a human).
// An allow grants the one call only (nothing is learned); a deny — or any review
// failure, timeout, or ambiguous verdict — keeps the call denied, so a reviewer
// is fail-safe and never load-bearing for safety. Reviews are bounded by a
// 30-second deadline and serialised through a per-run consecutive-failure
// breaker (ChildAskReviewMaxDenies). DEFAULT nil: a headless ask is
// blanket-denied, the long-standing behaviour. Set this on the engine whose runs
// have no human approver; an engine whose runs surface asks to a client leaves
// it unused (the surface path wins).
ChildAskReviewer ChildAskReviewer
// ChildAskReviewMaxDenies is the per-run circuit-breaker threshold for
// ChildAskReviewer: after this many CONSECUTIVE non-allow review outcomes (deny
// verdicts, failures, timeouts, ambiguous verdicts — an abstention via
// ErrNotReviewable does not count) within one run, further asks skip the reviewer
// and fall through to auto-deny, bounding reviewer spend. An allow resets the
// count. <=0 (the default) uses a built-in threshold of 3. Only consulted when
// ChildAskReviewer is set.
ChildAskReviewMaxDenies int
// MaxRunTokens is the loop-level token ceiling for one run: when lifetime main
// usage since the Run's immutable baseline crosses this value, the loop
// terminates CLEANLY at the next turn boundary with session.StopBudget. It is the shared runaway brake the AGENT-TEAMS-SPIKE named the
// missing token budget — checked in drive Step 2, so it serves EVERY engine: main +
// Subagent + Team member + lead synthesis + Fork branch. Semantics: 0 (the default;
// existing Deps built without it) DISABLES the budget (behaviour byte-identical to
// before); a positive value is the ceiling. It is a turn-BOUNDARY check (never a
// mid-stream abort), so an in-flight turn always completes and
// no-replay-after-first-chunk holds; the terminal is non-error, so the session ends
// COMPLETED and stays Reopen-recoverable (mirrors StopNoProgress exactly). It is a
// loop concern, like CompactionRatio / MaxNoProgressNudges — NOT a port.LLMRequest
// field (the request stays provider-neutral). Composition plumbs it from Config so it
// is operator-tunable; children INHERIT it (childEngineDepsForProvider keeps it) and a
// per-call override may only TIGHTEN it.
MaxRunTokens int
// SubagentModelRouter, when non-nil, is the OPT-IN semantic delegated-model
// router. Its configured backend, classifier model, and optional threshold remain
// available when pin/fork/resume/family gates skip Route. Route returns a typed
// backend-neutral result: Category and Model are validated candidates, OK says
// whether the route was accepted, Confidence is optional backend-native evidence,
// and Usage is folded into the parent session on every classifier call.
//
// Composition owns the classifier, taxonomy, category-to-model mapping, and
// same-provider target construction. The engine owns routing eligibility, the
// per-run three-miss breaker, bounded delegation-start evidence, and fail-soft
// fallback. Candidate fields never assert which model actually ran; the enclosing
// delegation payload's Model and accepted-route fields remain authoritative.
// DEFAULT nil means no configured router and produces no RoutingDecision. Child
// engines force it nil (the no-nesting recursion guard). The callback receives the
// run context so cancellation bounds classification; it is never a port.LLMRequest
// field.
SubagentModelRouter *SubagentModelRouter
// ProgressiveTools, when true, enables progressive tool disclosure
// (pattern 9): the per-turn request advertises lightweight specs for tools
// implementing tool.Disclosable plus a built-in ToolSearch tool the model
// uses to hydrate a full spec on demand. The DEFAULT (false) sends every
// tool's full spec every turn, exactly as v1 does. The ToolSearch tool is
// registered into the catalog by NewEngine only when this is enabled.
ProgressiveTools bool
// DeliveryQueue, when non-nil, is the DURABLE per-session pending-delivery queue
// the loop's turn-boundary drain reads (fire-result delivery). The fire path
// (composition) enqueues a rendered fire-result note
// for an origin session that is BUSY or AWAITING (it cannot drive a delivery run
// without colliding); the loop drains the pending notes at Step 2a, BEFORE
// BeginTurn — the SAME turn-boundary seam injectBackgroundNotice uses — recording
// each as an ordinary harness-framed user continuation (recordContinuation) and
// marking it delivered via MarkDelivered (the session-scoped exactly-once ledger).
//
// The drain is registered on the MAIN + per-session engines ONLY — never on child
// engines (a child origin degrades to pull-only with a WARN at the fire path). nil
// (the default) is the byte-identical no-delivery path: nothing drains, the
// fire path's enqueue is a no-op against a nil queue. It is a port (port.DeliveryQueue),
// so the agent package imports no concrete adapter.
DeliveryQueue port.DeliveryQueue
// EnableSteer, when true, arms each Run with an in-memory, best-effort
// steer inbox (steer-while-running, issue #512): an operator-supplied
// instruction enqueued mid-run that the loop drains at the next turn
// boundary (Step 2a, the same seam injectBackgroundNotice /
// drainPendingDelivery use) and records as an ordinary user continuation
// via recordContinuation, so it replays to the model and flows through
// compaction / session.ValidateToolPairing / event-log rehydration
// unchanged. It is a plain loop-concern bool — NOT a port.LLMRequest field
// and never reaches the model as anything but an ordinary recorded user
// message. DEFAULT false (the zero value): no inbox is armed and the drain
// is a strict no-op, byte-identical to the pre-steer posture. Composition
// plumbs it from Config; child engines inherit the shared deps.
// A pending (un-drained) steer is in-memory only and lost with the run —
// never persisted (see steer.go).
EnableSteer bool
}
Deps are the injected ports and configuration a single Engine is built from. Every field is a port (an interface) or plain config, so the agent package never depends on a concrete adapter. The composition root wires real or fake adapters in.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine builds Runs from a fixed set of ports. It is safe for concurrent use: each Run owns its own goroutine and state, and the injected ports are expected to be concurrency-safe (the provided adapters are). One Engine typically backs the whole process; the API layer (WP10) calls Run per prompt.
func NewEngine ¶
NewEngine constructs an Engine from deps, applying defaults for the optional Compactor and CompactionRatio.
func (*Engine) Capabilities ¶
func (e *Engine) Capabilities() port.ProviderCapabilities
Capabilities reports the multimodal input capabilities of the Engine's LLM provider, so a surface adapter can advertise them and gate unsupported prompt content. It is a pure pass-through to the injected provider.
func (*Engine) CompactSession ¶
func (e *Engine) CompactSession(ctx context.Context, sess *session.Session) (ManualCompactionResult, error)
CompactSession runs the configured Compactor once at a session turn boundary, regardless of the automatic compaction threshold. It creates no conversation turn. Empty, identical, and non-reducing candidates are successful no-ops. Invalid tool pairing and compactor failures are returned without mutation.
func (*Engine) ContextWindow ¶
ContextWindow reports the model's context window in tokens, resolved LIVE via Deps.ContextWindow at the point of call, or 0 when unknown/unset/disabled. The team supervisor reads it from each member's engine so a forwarded turn.end can carry the denominator for the per-member context meter in the f6 agents overlay (the resolver lives in private deps).
func (*Engine) HasTool ¶
HasTool reports whether a tool with the given registered name is present in the Engine's catalog. It is the read-only seam a surface adapter uses to report capabilities (e.g. memory/skills/bash availability) from the BUILT catalog rather than a static config flag, so the report can never claim a feature the engine did not register. It is nil-safe: a nil catalog yields false. It exposes only presence, never the concrete tool, keeping the agent package free of any adapter dependency.
func (*Engine) Model ¶
Model returns the provider model identifier this engine sends on every request (deps.Model). It is the read-only seam a delegation emitter uses to surface the child's resolved model on its start event, independent of how the model was chosen (inherited default, agent-def pin, per-call override, or the opt-in router). The string is bare metadata (a model id); the engine stays model-string-only — no adapter/proto type crosses here. Used by the Subagent / Parallel / Team delegation emit sites to populate the generic Model field (issue #112).
func (*Engine) PrepareAfterAuthorization ¶ added in v0.15.0
func (e *Engine) PrepareAfterAuthorization(ctx context.Context, sess *session.Session, env tool.Environment, authorization session.ExternalAuthorization, callID session.ToolCallID, results []session.ToolResult, resolution session.AuthorizationResolution) (*PreparedRun, error)
PrepareAfterAuthorization prepares the ordinary model loop after a terminal external-authorization outcome has been durably paired. Results must be the exact ordered ToolResults already recorded on sess. The caller must register Run before Start. It may present another authorization because its sole production caller has already authenticated the owner at the Service boundary.
func (*Engine) PrepareAuthorizationContinuation ¶ added in v0.15.0
func (e *Engine) PrepareAuthorizationContinuation(ctx context.Context, sess *session.Session, env tool.Environment, pending session.PendingAuthorization, resolution session.AuthorizationResolution) (*PreparedRun, error)
PrepareAuthorizationContinuation prepares an already durably claimed external-authorization continuation without executing it. The caller must register Run before Start. It may present another authorization because its sole production caller has already authenticated the owner at the Service boundary.
func (*Engine) ResumeApproval ¶
func (e *Engine) ResumeApproval(ctx context.Context, sess *session.Session, env tool.Environment, askID string, verdict session.ApprovalVerdict) *Run
ResumeApproval resumes an awaiting permission decision with default host capabilities.
func (*Engine) ResumeApprovalWith ¶ added in v0.15.0
func (e *Engine) ResumeApprovalWith(ctx context.Context, sess *session.Session, env tool.Environment, askID string, verdict session.ApprovalVerdict, opts ResumeApprovalOptions) *Run
ResumeApprovalWith resumes an awaiting permission decision with explicit host capabilities that may be needed by the continued tool call.
func (*Engine) RetryFailedStep ¶
func (e *Engine) RetryFailedStep(ctx context.Context, sess *session.Session, env tool.Environment) *Run
RetryFailedStep resumes the failed model step without submitting another user prompt. Persisted conversation and tool state are reused, while live turn-0 instructions and system prompt inputs are re-resolved by the normal request builder. sess must carry durable failed-step retry intent prepared by the host.
func (*Engine) Run ¶
func (e *Engine) Run(ctx context.Context, sess *session.Session, env tool.Environment, req RunRequest) *Run
Run starts processing req against sess with the exact supplied environment and returns immediately with a handle to the background run.
func (*Engine) SteerEnabled ¶
SteerEnabled reports whether this engine arms its Runs with the mid-run steer inbox (Deps.EnableSteer, steer-while-running issue #512). It is the read-only seam composition reads to advertise the feature — the ServerCapabilities.steer bit reads the SAME wired knob the runs consult, so the advertisement can never claim a steer path the engine did not arm (single-source, never recomputed per sink).
type EngineAskReviewerOption ¶
type EngineAskReviewerOption func(*engineAskReviewer)
EngineAskReviewerOption configures an engineAskReviewer.
func WithAskReviewPolicy ¶
func WithAskReviewPolicy(p string) EngineAskReviewerOption
WithAskReviewPolicy overrides the trusted policy rubric the default reviewer applies. Empty/whitespace is ignored (the built-in rubric stands).
type EngineJudgeOption ¶
type EngineJudgeOption func(*engineJudge)
EngineJudgeOption configures an engineJudge.
func WithJudgeLimits ¶
func WithJudgeLimits(l session.Limits) EngineJudgeOption
WithJudgeLimits overrides the judge run's stop conditions (default defaultChildLimits).
func WithJudgeSessionPrefix ¶
func WithJudgeSessionPrefix(p string) EngineJudgeOption
WithJudgeSessionPrefix sets the prefix used to derive the judge session id (default "fork-judge").
type EvidenceReflector ¶
type EvidenceReflector struct {
// contains filtered or unexported fields
}
EvidenceReflector performs exactly one provider-neutral, zero-tool model turn for an admitted reflection input. It never enters the Engine loop.
func NewEvidenceReflector ¶
func NewEvidenceReflector(provider port.LLMProvider, identity session.ProviderModelID, counter TokenCounter, limits ReflectionLimits) (*EvidenceReflector, error)
NewEvidenceReflector constructs a bounded reflector with the exact composition-selected provider/model identity used for returned usage. A nil counter selects the dependency-free heuristic counter.
func (*EvidenceReflector) Reflect ¶
func (r *EvidenceReflector) Reflect(ctx context.Context, in learning.Input) (learning.Outcome, session.AuxiliaryUsage, error)
Reflect implements learning.Reflector. Inputs without any host-supplied or structurally detected signal abstain without spending a provider call.
func (*EvidenceReflector) ReflectProjection ¶ added in v0.14.0
func (r *EvidenceReflector) ReflectProjection(ctx context.Context, projection learning.Projection) (learning.Outcome, session.AuxiliaryUsage, error)
ReflectProjection performs reflection across a restart-safe boundary that accepts only the bounded canonical learning projection. It rejects projections that are not reproducible from their own content-addressed evidence metadata.
func (*EvidenceReflector) RequestTokenEstimate ¶
func (r *EvidenceReflector) RequestTokenEstimate(in learning.Input) (int, error)
RequestTokenEstimate returns the selected-model estimate for the exact bounded request Reflect would send, plus the configured maximum output tokens.
type GuardrailReviewFailure ¶ added in v0.16.0
type GuardrailReviewFailure interface {
GuardrailReviewFailureCode() ReviewFailureCode
}
GuardrailReviewFailure classifies a review error without exposing its text. Unknown/custom errors are projected as provider_failure by consumers.
type GuardrailReviewTerminalFailure ¶ added in v0.16.0
type GuardrailReviewTerminalFailure interface {
GuardrailReviewTerminalFailure() bool
}
GuardrailReviewTerminalFailure classifies a review error as an authority-relevant terminal evidence failure. Implementations return true only when enforcement must fail closed; ordinary provider or transport failures must not use it.
type HeuristicCompactor ¶
type HeuristicCompactor struct {
// MaxToolBodyChars overrides maxToolBodyChars when > 0.
MaxToolBodyChars int
// KeepLastTurns overrides keepLastTurns when > 0.
KeepLastTurns int
}
HeuristicCompactor is the default, network-free Compactor. It keeps the system prompt and the user goal, synthesises a single summary message that lists every file path touched so far, truncates large tool-result bodies, and preserves the last keepLastTurns messages verbatim — back-snapped to recent USER turns (snapCutToRecentUserTurn) so the most-recent user instruction survives verbatim rather than being summarised away (prior art: Codex, gemini-cli keep the recent user turns verbatim; docs/harnesses/07-context-and-mcp.md §4 "what to preserve", 03-claude-code-architecture.md "first user message is summarized away", 08-design-considerations.md §12). It performs no LLM call, so it is fully deterministic and testable offline (gauntlet #12-lite).
func (HeuristicCompactor) Compact ¶
func (h HeuristicCompactor) Compact(_ context.Context, conv *session.Conversation) ([]session.Message, string, session.AuxiliaryUsage, error)
Compact implements Compactor with the heuristic described on HeuristicCompactor.
type HeuristicTokenCounter ¶
type HeuristicTokenCounter struct {
// CharsPerToken overrides charsPerToken when > 0.
CharsPerToken int
}
HeuristicTokenCounter is the default, dependency-free TokenCounter. It divides byte length by charsPerToken and adds a small fixed overhead per message and per tool call so a history of many short messages is not undercounted. It performs no network or tokenizer-table lookup, so it is fully deterministic and testable offline. It replaces the inline 4-chars/token estimate the loop used before the TokenCounter seam existed.
func (HeuristicTokenCounter) Count ¶
func (h HeuristicTokenCounter) Count(text string) int
Count implements TokenCounter for a single string.
func (HeuristicTokenCounter) CountMessages ¶
func (h HeuristicTokenCounter) CountMessages(msgs []session.Message) int
CountMessages implements TokenCounter for a conversation slice, summing the text/reasoning/tool bodies (divided by the chars-per-token ratio) plus the fixed per-message and per-tool-call framing overhead.
type InspectMemberTool ¶
type InspectMemberTool struct {
// contains filtered or unexported fields
}
InspectMemberTool reads one persisted team member session via a port.SessionStore and returns a BOUNDED rendering of its conversation as its ToolResult. It is PULL and read-only.
func (*InspectMemberTool) Execute ¶
func (t *InspectMemberTool) Execute(ctx context.Context, call session.ToolCall, _ tool.Environment) (session.ToolResult, error)
Execute loads the member's persisted session (id derived via the SHARED MemberSessionID helper, so it cannot drift from the supervisor's save id) and renders a bounded transcript. An unknown id is a model-addressable error (the team may not have run, or the member never started).
func (*InspectMemberTool) ReadOnly ¶
func (*InspectMemberTool) ReadOnly() bool
ReadOnly reports that InspectMember only READS the store (no workspace mutation), so the dispatcher may run it read-parallel.
func (*InspectMemberTool) Spec ¶
func (*InspectMemberTool) Spec() tool.ToolSpec
Spec returns the model-facing specification for the InspectMember tool.
type InspectSubagentTool ¶
type InspectSubagentTool struct {
// contains filtered or unexported fields
}
InspectSubagentTool reads one persisted subagent OR Parallel-branch session via a port.SessionStore and returns a BOUNDED rendering of its conversation as its ToolResult. It is PULL and read-only. The agent_id it takes IS the session id verbatim — no derivation — so the id from a Subagent result's 'agentId:' line OR a Parallel result's 'branch id:' line loads directly (issue #30).
func (*InspectSubagentTool) Execute ¶
func (t *InspectSubagentTool) Execute(ctx context.Context, call session.ToolCall, _ tool.Environment) (session.ToolResult, error)
Execute loads the subagent's persisted session by its agent id (the id IS the session id, verbatim — no derivation) and renders a bounded transcript. An unknown id is a model-addressable error (the subagent may not have run yet).
func (*InspectSubagentTool) ReadOnly ¶
func (*InspectSubagentTool) ReadOnly() bool
ReadOnly reports that InspectSubagent only READS the store (no workspace mutation), so the dispatcher may run it read-parallel.
func (*InspectSubagentTool) Spec ¶
func (*InspectSubagentTool) Spec() tool.ToolSpec
Spec returns the model-facing specification for the InspectSubagent tool.
type LRUForkReaper ¶
type LRUForkReaper struct {
// contains filtered or unexported fields
}
LRUForkReaper is a process-scoped, bounded PreservedForkStore: it keeps the most-recent cap preserved winner forks and reaps the OLDEST when a new winner exceeds the cap (invoking that fork's captured cleanup). This bounds the disk a run can accumulate from preserved winners while keeping the most recent winners inspectable. It is safe for concurrent use.
A non-positive cap is normalised to DefaultPreservedForkCap. A reaper is NOT required for Parallel to work — without one, ParallelTool falls back to the original behaviour (winner forks are preserved forever); the reaper is the bound.
func NewLRUForkReaper ¶
func NewLRUForkReaper(capacity int) *LRUForkReaper
NewLRUForkReaper constructs a bounded reaper that keeps at most cap preserved winner forks (the most recent). A non-positive cap uses DefaultPreservedForkCap.
func (*LRUForkReaper) Close ¶ added in v0.13.0
func (r *LRUForkReaper) Close()
Close reaps every retained fork and waits for eviction cleanups detached before closure. It is safe to call concurrently with Preserve and is idempotent. Once closed, a reaper never retains another fork: Preserve instead invokes its supplied cleanup immediately, and Close does not wait for that later work. Cleanup runs outside the mutex.
func (*LRUForkReaper) Len ¶
func (r *LRUForkReaper) Len() int
Len reports how many preserved forks the reaper currently retains. Test/diagnostic only.
func (*LRUForkReaper) Preserve ¶
func (r *LRUForkReaper) Preserve(handle ArtifactHandle, cleanup func() error)
Preserve records a winner fork and reaps the oldest beyond the cap. Re-preserving the same handle refreshes its recency (and adopts the new cleanup) rather than double-counting. A nil cleanup is ignored. After Close, cleanup runs immediately. Cleanup runs OUTSIDE the lock so a slow filesystem teardown does not serialize concurrent Parallel calls.
type ManualCompactionResult ¶
ManualCompactionResult describes one out-of-band compaction attempt. Archive and Summary are populated only when Changed is true and are suitable for the existing compaction notice and durable archive events.
type MemberBuild ¶
type MemberBuild struct {
// Engine is the member's loop engine. It must be non-nil; AddMember rejects a
// nil Engine with ErrNilEngine.
Engine *Engine
// Provider is the concrete provider backing Engine.
Provider string
// Mode is the optional per-member permission mode. Empty => the team default.
Mode session.PermissionMode
// Limits are the OPTIONAL per-member, per-round stop conditions resolved from the
// member's agent definition (its maxTurns/maxToolCalls). A zero Limits field means
// "use the supervisor's team default" (s.limits / WithTeamLimits) for THAT field,
// exactly as an empty Mode falls back to s.mode — the factory is the only place
// that resolves a def to limits, so the mapping stays in the composition layer and
// the supervisor stays agnostic of agent definitions. A wholly zero Limits leaves
// the member on the team default, unchanged.
Limits session.Limits
// Close, if non-nil, tears down resources the factory opened for THIS member —
// specifically the inline per-agent MCP manager(s) connected for the member's
// agent definition (a reference entry opens nothing, so it contributes no Close).
// The supervisor composes it with the member's fork cleanup so it runs on every
// teardown path (cleanupAll, a failed/stopped member, a rejected enrolment).
Close func() error
// MCPToolNames are the names of MCP tools the factory added to this member's
// catalog (from its def's mcpServers). MCP tools report ReadOnly()==false but
// touch only the remote server, never the workspace, so the supervisor EXEMPTS
// them from the read-only-member workspace-mutating-tool backstop — exactly like
// the team coordination tools. Empty when the def scopes no MCP servers.
MCPToolNames []string
// IsolateReadOnly tells the supervisor this is a NON-mutating member that the
// factory nonetheless gave Shell (i.e. it put a workspace-mutating shell into a
// read-only member's catalog because a read-only forker is available). When true
// the supervisor runs the member in an isolated git WORKTREE (via s.roForker) so
// its mutating-classified Shell lands in a throwaway checkout, never the shared
// base — and the read-only-member workspace-mutating-tool backstop EXEMPTS it
// (the guard gates on base-sharing, and an isolated member is not base-sharing).
// It is false for a base-sharing read-only member (no shell) and for a Mutating
// member (the Mutating flag already drives its force-copy fork). The factory must
// set it true ONLY when it actually added Shell to a non-mutating catalog AND a
// read-only forker is available; setting it without a wired forker trips
// ErrReadOnlyShellNoForker.
IsolateReadOnly bool
}
MemberBuild is what the per-member engine factory returns: the constructed *Engine plus the OPTIONAL per-member permission mode resolved from the member's agent definition. An empty Mode means "use the team-wide default" (WithTeamMode / s.mode); a non-empty Mode (e.g. session.ModePlan from a def's permissionMode) overrides it for THIS member's session only. The mode lives here, not on MemberSpec, because the factory is the only place that resolves a def to a mode — keeping the spec a pure request value and the supervisor agnostic of agent definitions (the registry→mode mapping stays in the composition layer).
type MemberDisposition ¶
type MemberDisposition string
MemberDisposition is the terminal disposition of one team member at the end of a Run: a closed enum (done / stopped), never free-form text. It is a SUPERVISOR verdict, not member-authored content, so it sidesteps the redaction question.
const ( // DispositionDone is a member that finished cleanly (idle, no-progress, or any // non-error terminal whose session re-opened successfully). DispositionDone MemberDisposition = "done" // DispositionStopped is a member that ended non-resumably or exhausted its budget. DispositionStopped MemberDisposition = "stopped" )
type MemberEngine ¶
type MemberEngine func(spec MemberSpec, routedModel string) MemberBuild
MemberEngine builds the per-member engine (and its optional permission mode) from its spec. The composition root supplies it; it is expected to capture the shared *team.Team so the member's catalog includes MemberTools(team, spec.Name) (always available to a member, even under a restrictive agent definition) plus the member's scoped base tools, model, and policy.
Catalog shaping follows the three-tier workspace policy (see the package doc):
- A read-only member that the factory CANNOT isolate (no read-only forker / runner wired) shares the base workspace, so it must get NO mutating tool (no Edit/Write/Shell) and MemberBuild.IsolateReadOnly stays false.
- A read-only member the factory CAN isolate gets Read/Grep/Glob PLUS Shell (but NOT Edit/Write) and sets MemberBuild.IsolateReadOnly=true, so the supervisor runs it in a throwaway git worktree (s.roForker) where its Shell is confined.
- A Mutating member (which runs in an isolated force-copy fork via s.forker) may get Edit/Write/Shell.
Shell is workspace-aware: ShellTool.Execute runs the command with the member's (forked) Workspace.Root() as the working directory, so an isolated member's Shell runs in its OWN worktree/fork, never the shared parent base — which is why an isolated member MAY be given Shell while a base-sharing read-only member must not.
routedModel is the OPT-IN router's concrete model for an undefined member. For provider-aware selections (explicit or automatic), spec.Selector carries the complete target and evidence to the same factory. A defined member without an explicit selector keeps its own model.
type MemberOutcome ¶
type MemberOutcome struct {
// Name is the member name.
Name string
// LastText is the member's most recent terminal assistant text.
LastText string
// Stopped reports whether the supervisor descheduled the member before the team
// finished (its last run failed or was cancelled, or it exhausted its lifetime turn
// budget), so it ran no further rounds.
Stopped bool
// Disposition is the member's TERMINAL disposition (done / stopped). It is the
// closed-enum form of Stopped: Disposition == DispositionStopped iff Stopped.
Disposition MemberDisposition
// Reason is WHY a stopped member stopped (error / cancelled / budget); empty for a
// done member.
Reason MemberStopReason
// ErrorRounds is how many of this member's rounds ended in session.StopError,
// whether it was RETRIED through them or finally benched by them (issue #318).
// It is the disposition-HONESTY signal: a bounded retry means
// a member can fail a round and still finish, and such a member reports
// DispositionDone with no Reason — so without this count a transient failure would
// be invisible to the caller and the run would read as silently clean. It is a
// COUNT, deliberately not a new MemberDisposition value: the disposition is a closed
// enum mirrored on the proto wire, and "done" is still the honest terminal.
//
// It is INDEPENDENT of the terminal: it counts errored rounds over the member's whole
// LIFETIME (the counter is monotonic — that monotonicity is the retry cap's
// termination proof), so it does NOT follow from Reason and Reason does not follow
// from it. 0 exactly when the member never had an errored round. A member benched by
// its errors has >=1 alongside Stopped/StopReasonError, but so can one benched for
// cancellation or budget: a member that failed round 1, was recovered and retried,
// then was cancelled in round 2 reports Reason "cancelled" with ErrorRounds 1. Read
// the two together, never one from the other.
ErrorRounds int
// Completed holds this member's completed-task descriptions (clamped), captured in
// outcome() from the shared task list. It feeds the ledger-rich deliverable
// fallback so a degraded report can state what each member actually finished — the
// "honest gaps" data the incident wanted. Empty for a member that completed no task.
Completed []string
// Lead reports whether this member is the coordinating lead. The structured
// deliverable fallback SKIPS the lead's LastText: after the synthesis turn the
// lead's last words ARE the synthesis (empty, truncated, or the rejected refusal
// the fallback exists to replace), so echoing them would re-surface the very text
// the fallback discarded. The lead's disposition still appears.
Lead bool
}
MemberOutcome summarises one member at the end of a Run.
type MemberSpec ¶
type MemberSpec struct {
// Name is the unique member handle peers address messages to.
Name string
// AgentType is the optional agent-definition name this member adopts; passed
// through to the engine factory and recorded on the team roster.
AgentType string
// Lead marks the coordinating member. The lead is NOT auto-assigned tasks (it
// coordinates); it runs on its initial prompt and whenever it has messages.
Lead bool
// Mutating requests a self-contained force-copied fork (own `.git`) for this
// member, with Edit/Write/Shell. A read-only member (the default) either runs in
// an isolated git WORKTREE with a shell for inspection (when a read-only forker is
// wired — the factory sets MemberBuild.IsolateReadOnly) or, failing that, shares
// the base workspace with no shell. See the package "Workspace policy" doc.
Mutating bool
// InitialPrompt is the member's first-turn input, run in round 0 (typically the
// lead's top-level task, or a teammate's role briefing).
InitialPrompt string
// Selector is the composition-validated explicit target, if one was requested.
Selector *ResolvedModelSelector
}
MemberSpec describes a member to enrol before running the team.
type MemberStopReason ¶
type MemberStopReason string
MemberStopReason is WHY a stopped member stopped: a closed enum. Empty/unspecified for a member that finished cleanly (DispositionDone).
const ( // StopReasonError is a run that failed (StopError) or a session that could not be // returned to idle — both the internal-fault class. A failed run is RECOVERED // (issue #318), so this reason does not imply the session is undrivable; only // memberRT.nonResumable says that. StopReasonError MemberStopReason = "error" // StopReasonCancelled is a member ended by ctx cancellation. StopReasonCancelled MemberStopReason = "cancelled" // StopReasonBudget is a member that exhausted its lifetime turn budget (its session // stays resumable; it is merely non-schedulable). StopReasonBudget MemberStopReason = "budget" )
type ModelRouteCategory ¶
type ModelRouteCategory struct {
// Name is the routing key the classifier must echo back as its verdict (and the
// key composition maps to a concrete model). A verdict naming a category NOT in
// the request's list is a hallucination → ok=false (fail-soft).
Name string
// Description is the one-line summary the classifier reads to choose. Operators
// are told to make these clear and distinct (the classifier's only signal).
Description string
}
ModelRouteCategory is one routing category the classifier chooses among: a NAME (the verdict value) and a one-line DESCRIPTION the classifier reads to decide. It is a layering-clean value type — the composition root translates the operator's `models.router.categories` taxonomy into these and supplies the category→model mapping itself; engine/agent never sees the per-category model selector.
type ModelRouteRequest ¶
type ModelRouteRequest struct {
// TaskPrompt is the Subagent call's `prompt` — model-authored and possibly
// peer-injected → UNTRUSTED. It is wrapped in a governance.UntrustedFence (framing
// neutralised) by buildModelRoutePrompt so it can neither forge the verdict nor
// fabricate a fresh classifier instruction.
TaskPrompt string
// Categories are the routing choices (name + description), rendered in the CLEAR
// (operator-authored, trusted). A verdict must name one of these.
Categories []ModelRouteCategory
// Default is the category name the classifier is told to choose when no category
// clearly fits. It is advisory to the classifier; the caller's own fallback (the
// inherited model on ok=false) is the real safety net.
Default string
}
ModelRouteRequest is the input to one classification: the (model-authored, UNTRUSTED) task prompt being routed, the available categories, and the default category the classifier should fall back to when none clearly fits.
type ModelRouteResult ¶ added in v0.15.0
type ModelRouteResult struct {
Category string
Provider string
Model string
Usage session.AuxiliaryUsage
Reason string
OK bool
Confidence *float64
}
ModelRouteResult is one backend-neutral classifier result. Category and target are validated candidates even when OK is false; OK alone says whether the route was accepted.
type ModelTarget ¶ added in v0.17.0
ModelTarget is a provider/model target selected for delegated work. An empty Provider preserves the caller's contextual provider.
type ParallelOption ¶
type ParallelOption func(*ParallelTool)
ParallelOption configures a ParallelTool.
func WithAutoMerge ¶
func WithAutoMerge(m tool.EnvironmentMerger) ParallelOption
WithAutoMerge injects the OPTIONAL tool.EnvironmentMerger that auto-merges a SINGLE-BRANCH join=first winner's diff back into the parent workspace after the run. It is the composition-owned, opt-in (--parallel-auto-merge, default OFF) capability that lets a delegated implementer's edits actually land without a manual copy/merge step. nil (the default) keeps the no-auto-merge boundary.
The merger fires ONLY for a single-branch run (len(tasks)==1) with a successful winner under join=first/judge. Multi-branch runs and join=all NEVER auto-merge (the no-auto-merge boundary stays for fan-out). The merge is a POST-RUN step before the winner cleanup is handed to the reaper and before Execute returns, so ReadOnly() stays true for read-only fan-out; a merge-completing CALL is excluded from the concurrent read batch via MutatesParent (dispatch-serial — see parentMutatingCaller), so it never overlaps a sibling parent read, and cross-run merge-vs-merge is serialized by the shared SerializingMerger. On a conflict Execute returns a tool error with the ephemeral fork path, which may already be gone if graceful shutdown began. See tool.EnvironmentMerger and the forker.Merger adapter.
func WithMaxBranches ¶
func WithMaxBranches(n int) ParallelOption
WithMaxBranches caps the number of branches a single Parallel call may fan out to (default defaultMaxBranches). A non-positive value is ignored.
func WithParallelChildLimits ¶
func WithParallelChildLimits(l session.Limits) ParallelOption
WithParallelChildLimits overrides the per-branch stop conditions (default defaultChildLimits).
func WithParallelChildMode ¶
func WithParallelChildMode(m session.PermissionMode) ParallelOption
WithParallelChildMode sets the permission mode each child branch session runs under. Without it a branch inherits an auto or yolo parent's mode and otherwise runs in session.ModeDefault (see effectiveChildMode); an explicit session.ModeDefault keeps default even under an auto or yolo parent.
func WithParallelChildSessionPrefix ¶
func WithParallelChildSessionPrefix(p string) ParallelOption
WithParallelChildSessionPrefix sets the prefix used to derive child branch SessionIDs (default "parallel"). Child ids are of the form "<prefix>-<callID>-<i>".
func WithParallelConcurrency ¶
func WithParallelConcurrency(n int) ParallelOption
WithParallelConcurrency bounds how many branches run simultaneously (default defaultParallelConcurrency). A non-positive value is ignored.
func WithParallelEngineFactory ¶
func WithParallelEngineFactory(f func(ModelTarget) (*Engine, bool)) ParallelOption
WithParallelEngineFactory mints per-branch engines on provider/model targets. Automatic routing declines fall back to the shared branch engine.
func WithParallelJudge ¶
func WithParallelJudge(j BranchJudge) ParallelOption
WithParallelJudge injects the BranchJudge used by the "judge"/"best" join strategy (nil disables judging — the strategy then returns a model-addressable error). The default build wires an engineJudge over a dedicated, tool-less read-only child Engine; see internal/app.
func WithParallelProvider ¶ added in v0.17.0
func WithParallelProvider(provider string) ParallelOption
WithParallelProvider supplies the inherited branch provider for observability.
func WithParallelSelectorResolver ¶ added in v0.17.0
func WithParallelSelectorResolver(r SubagentSelectorResolver) ParallelOption
WithParallelSelectorResolver injects composition-owned provider/model resolution.
func WithParallelStore ¶
func WithParallelStore(store port.SessionStore) ParallelOption
WithParallelStore injects the optional session store each branch's child session is best-effort persisted to after its run (issue #30), so the PULL InspectSubagent tool can later load a branch's transcript by the "branch id:" line the Parallel result surfaces. Nil disables persistence. Mirrors SubagentTool's WithSubagentStore / persistChild discipline exactly (the closest structural sibling — both take a *session.Session): the save is advisory and a failure is swallowed (no diagnostic). Branch ids share the SHARED store with subagent and team-member ids on a DISJOINT prefix ("parallel-"), so no collision engineering is needed. The tool consumes the port.SessionStore interface, never a concrete adapter, so no layering rule is crossed.
func WithParallelSubagentStopHook ¶
func WithParallelSubagentStopHook(h port.HookRunner) ParallelOption
WithParallelSubagentStopHook injects the HookRunner that fires SubagentStop when a branch run finishes (best-effort; nil disables it).
func WithWinnerReaper ¶
func WithWinnerReaper(s PreservedForkStore) ParallelOption
WithWinnerReaper injects a bounded PreservedForkStore that caps how many PRESERVED winner forks (join=first / join=judge) survive at once: a new winner beyond the cap reaps the oldest. nil (the default) preserves winner forks indefinitely. See NewLRUForkReaper for the default bounded implementation.
type ParallelTool ¶
type ParallelTool struct {
// contains filtered or unexported fields
}
ParallelTool is the fork-join fan-out tool (harness pattern 8). When executed it forks N ISOLATED child workspaces from the parent's workspace (via the injected tool.EnvironmentForker), runs one CHILD agent loop per branch in PARALLEL (bounded by a worker limit) over the injected child *Engine — each with its own fresh Session, tighter Limits, and the child Engine's scoped catalog — drains every child's Event stream internally, and JOINS the results into a SINGLE session.ToolResult that summarizes all branches.
Like SubagentTool (gauntlet #7), the parent NEVER observes any child's intermediate tool.call / tool.result / message.delta / permission.ask events: each child's stream is drained entirely inside Execute and only the terminal summary folds back. A child permission ask is auto-denied so children stay non-interactive.
Isolation: each branch runs in its OWN forked workspace, so even a child wired with mutating tools writes only to its fork — parallel writes are SAFE because they are isolated (this is strictly safer than concurrent SubagentTool calls, which share the base). v1 does NOT auto-merge: Execute returns the per-branch summaries and the child workspace ROOT paths so a human or the parent can inspect/merge the forked trees. cleanup tears each fork down after its summary has been captured.
The child Engine is built by the composition root with a scoped catalog (no Parallel, no Subagent — children cannot fan out further) exactly as for SubagentTool.
func (*ParallelTool) Execute ¶
func (t *ParallelTool) Execute(ctx context.Context, call session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute forks N isolated child workspaces (one per task), runs a child loop in each IN PARALLEL bounded by the worker limit, drains every child stream internally, cleans up each fork, and returns ONE ToolResult that joins all branch summaries. The whole operation is bounded by the parent ctx: cancelling it cancels every in-flight branch. A branch that fails is reported in the joined summary without aborting the others; the call returns a harness-level error only for a setup failure (invalid args / cap exceeded).
func (*ParallelTool) ExecuteWithParent ¶
func (t *ParallelTool) ExecuteWithParent(ctx context.Context, call session.ToolCall, env tool.Environment, emit func(session.Event), caps parentCaps) (session.ToolResult, error)
ExecuteWithParent is the childCapableTool seam: it runs Parallel like Execute but threads the PARENT's caps (interactivity + surface back-channel) into each branch's posture AND the parent emit closure — so a Parallel run projects its REDACTED parallel.* group observability stream (start / per-branch / end) onto the parent event channel, exactly as Subagent projects subagent.*.
func (*ParallelTool) MutatesParent ¶
func (t *ParallelTool) MutatesParent(call session.ToolCall) bool
MutatesParent implements the optional parentMutatingCaller seam (FIX C): it reports whether THIS specific call will auto-merge a single branch's fork diff back into the PARENT workspace. ReadOnly() stays true so multi-branch / join=all fan-out keeps batching in parallel; a call for which this returns true is excluded from the concurrent read batch (dispatch-serial via runOne) so its post-run merge never overlaps a sibling parent Read/Grep/Glob. It returns true ONLY for a call that will ACTUALLY merge: the auto-merger wired, exactly ONE task, and join in {first,judge} (autoMergeWinner only merges a single-branch winner). A malformed/unparseable args payload returns false (the call errors later anyway, and never merges).
It keys on the TASK count (len(nonEmptyTasks)==1) whereas autoMergeWinner keys on the RESULT count (len(results)==1); these differ only when the lone branch fails to START (1 task, 0 results → no merge). That makes MutatesParent a deliberate OVER-approximation: at worst it flushes a non-merging single-branch call serially instead of in the read batch — a throughput cost on a rare path, never a correctness gap (it never UNDER-declares a call that will merge).
func (*ParallelTool) ReadOnly ¶
func (*ParallelTool) ReadOnly() bool
ReadOnly reports that the Parallel tool is read-only with respect to the PARENT's shared workspace, which lets the parent dispatcher run it concurrently with other read-only tools (read-parallel / mutate-serial; see dispatch.go).
INVARIANT — this is the same invariant SubagentTool documents, but Parallel makes it strictly safer: every child branch runs in its OWN forked workspace, never the shared base. So the child's filesystem-mutating tools (Edit / Write) land in the isolated fork and CANNOT race on, or mutate, the parent's shared base. Shell is now workspace-aware (see app.buildParallelChildEngine): ShellTool.Execute passes the per-branch forked Workspace.Root() to its CommandRunner as the working directory, so a branch's Shell runs in its OWN fork — its DEFAULT cwd is the fork, not the shared parent base. (Residual: unlike path-scoped Edit/Write, Shell can still escape its cwd via absolute paths or `cd`; that is the inherent Shell trust model, the same as the main session. What the fix guarantees is that no ACCIDENTAL shared-base mutation happens — a branch's relative-path Shell lands in the fork.) That is why ReadOnly() can safely return true even for mutating (Edit/Write/Shell) children — for the SAME reason SubagentTool.ReadOnly() stays true: each tool isolates its mutating child so the child's writes never touch the shared parent base. Isolation, not catalog read-only-ness, is the boundary (a Subagent child with Shell runs in its OWN git worktree exactly as a Parallel branch runs in its own force-copy). The remaining distinction is only WHICH tools the child gets: a Parallel branch keeps Edit/Write (it is meant to IMPLEMENT in its fork), while a Subagent child drops them and is shell-only (a read-only explorer that may run git/build/test but cannot edit the project).
ReadOnly() stays true for read-only fan-out; a merge-completing CALL (single-branch join=first/judge with the merger wired) is excluded from the concurrent read batch via MutatesParent (dispatch-serial — see parentMutatingCaller), and cross-run merge-vs-merge is serialized by the shared SerializingMerger.
func (*ParallelTool) Spec ¶
func (*ParallelTool) Spec() tool.ToolSpec
Spec returns the model-facing specification for the Parallel tool.
type PermissionReviewPolicyProvider ¶ added in v0.16.0
type PermissionReviewPolicyProvider interface {
GuardrailPermissionReviewEligible(session.ToolCall) bool
}
PermissionReviewPolicyProvider reports whether an exact child Shell call has enabled, enforcing, matching action-review coverage. Advisory, skipped, and unmatched rules must return false. The engine never consults it for a yolo-mode child.
type PlanApprovalReceipt ¶ added in v0.16.0
type PlanApprovalReceipt struct {
// Ref is the opaque, non-empty receipt identity.
Ref string
// SessionID binds the receipt to exactly one session.
SessionID session.SessionID
// Call identifies the PresentPlan call the operator approved.
Call session.ToolCallID
// TargetMode is the successfully entered execution mode.
TargetMode session.PermissionMode
}
PlanApprovalReceipt is a bounded process-local capability proving that an operator approved a presented plan and the session successfully transitioned to the exact target mode. It contains no plan body or tool arguments.
type PlanApprovalStore ¶ added in v0.16.0
type PlanApprovalStore interface {
RecordPlanApproval(PlanApprovalReceipt)
ConsumePlanApproval(session.SessionID) (PlanApprovalReceipt, bool)
}
PlanApprovalStore is the optional composition-owned process-local plan receipt seam. Record is called only after the approved mode transition succeeds. Consume must be synchronized and atomically return and remove at most one exact session receipt for the next accepted root execution prompt. Lifecycle cleanup is caller-owned: composition must discard pending receipts when a session is closed, cleared, or otherwise replaced.
type PreparedReviewEvidence ¶ added in v0.16.0
type PreparedReviewEvidence struct {
Source ReviewEvidenceSource
Evidence []ReviewEvidenceMeta
Complete bool
Close func()
}
PreparedReviewEvidence owns one review-local finite capability set. Close is called by the Engine after any immediate human wait and binding revalidation.
type PreparedRun ¶ added in v0.15.0
type PreparedRun struct {
// contains filtered or unexported fields
}
PreparedRun is a fully initialized, inert Run. Start releases its execution goroutine exactly once, allowing a host to register the run before a durably parked protected call can execute.
func (*PreparedRun) Abort ¶ added in v0.15.0
func (p *PreparedRun) Abort() PreparedRunTransition
Abort closes an inert prepared run without executing its body and reports whether it aborted, was a duplicate abort, or lost to Start.
func (*PreparedRun) Run ¶ added in v0.15.0
func (p *PreparedRun) Run() *Run
Run returns the inert run handle for registration before Start.
func (*PreparedRun) Start ¶ added in v0.15.0
func (p *PreparedRun) Start() (*Run, PreparedRunTransition)
Start releases an inert prepared run and reports whether it started, was a duplicate start, or lost to Abort.
type PreparedRunTransition ¶ added in v0.15.0
type PreparedRunTransition string
PreparedRunTransition reports the observed state transition of Start or Abort.
const ( // PreparedRunStarted means Start won and released execution. PreparedRunStarted PreparedRunTransition = "started" // PreparedRunAborted means Abort won and closed the inert run. PreparedRunAborted PreparedRunTransition = "aborted" // PreparedRunDuplicateStart means Start was called again after starting. PreparedRunDuplicateStart PreparedRunTransition = "duplicate_start" // PreparedRunDuplicateAbort means Abort was called again after aborting. PreparedRunDuplicateAbort PreparedRunTransition = "duplicate_abort" // PreparedRunStartAfterAbort means Start lost because Abort already won. PreparedRunStartAfterAbort PreparedRunTransition = "start_after_abort" // PreparedRunAbortAfterStart means Abort lost because Start already won. PreparedRunAbortAfterStart PreparedRunTransition = "abort_after_start" )
type PreservedForkStore ¶
type PreservedForkStore interface {
// Preserve records a winning fork under an opaque artifact handle and retains
// its private cleanup capability until eviction. The handle is safe to surface;
// the physical root remains private to the Environment and cleanup closure.
Preserve(handle ArtifactHandle, cleanup func() error)
}
PreservedForkStore is the seam the Parallel tool uses to retain a winning branch's PRESERVED fork (join=first / join=judge) under a BOUNDED policy. A winner's fork is intentionally not torn down at the end of the call — its contents are the deliverable, inspectable/mergeable by the operator — but without a bound every winner over the process lifetime would leak a fork dir, growing disk without limit. A PreservedForkStore caps how many preserved forks survive at once: when a new winner pushes the count past the cap, the OLDEST preserved fork is reaped (its captured cleanup invoked).
LAYERING: this lives in engine/agent (application layer) so ParallelTool can be injected with it without importing an adapter. The cleanup func is the SAME teardown the forker handed runBranch — the reaper simply defers calling it (for the winner) until eviction, instead of the caller dropping it on the floor.
Implementations must be safe for concurrent use: several Parallel calls can finish concurrently and each preserves at most one winner.
type ReflectionLimits ¶
type ReflectionLimits struct {
InputBytes int
Events int
Existing int
Candidates int
EvidencePerCandidate int
OutputBytes int
Tokens int
Timeout time.Duration
}
ReflectionLimits are explicit resource bounds for one direct reflection call. Zero selects a conservative default; negative values and values above the hard ceilings are rejected.
type ResolvedModelSelector ¶ added in v0.17.0
type ResolvedModelSelector struct {
Target ModelTarget
ActualProvider string
ProviderBearing bool
ExplicitRouterCategory string
}
ResolvedModelSelector is composition's validated interpretation of one explicit delegation selector. ProviderBearing distinguishes a contextual bare model from a selector that explicitly carries provider intent (including pair aliases).
type ResumeApprovalOptions ¶ added in v0.15.0
type ResumeApprovalOptions struct {
// CanPresentAuthorization permits this resumed main run to present a newly
// required external authorization. The zero value fails closed.
CanPresentAuthorization bool
}
ResumeApprovalOptions configures ResumeApproval, the FOURTH, awaiting-ONLY run-entry seam: it re-enters the loop AT a parked permission ask on a session that is in StateAwaiting (typically loaded fresh from a snapshot after the process that parked the ask died), applies verdict to the pending tool call, closes out any unanswered sibling calls on the same trailing assistant message, then continues the loop to completion. It mirrors Engine.Run's Run-construction preamble exactly (same events/asks/cancel/ctx/hardAbort/serial/diag/children/router discipline) but launches driveFromAwaiting instead of drive.
It is DISTINCT from the three existing terminal-recovery seams (Reopen / Interrupt / Recover, all via resetToIdle at a turn boundary): those zero the Counters and clear the pending ask; this preserves both via the awaiting-only session.ResumeWith seam (the live loop's own resume path), so the re-entered run continues the SAME logical turn with its spend and limits intact. It adds NO new aggregate transition verb (ResumeWith already exists); the "fourth seam" is the run-entry orchestration here + Service.resumeFromAwaiting, not a domain change.
PRECONDITION FAILURES surface ON THE RUN, not as a return value (ResumeApproval only ever returns a live *Run): if sess is not in StateAwaiting, or its pending ask does not match askID, or the trailing assistant message carries no matching tool call, driveFromAwaiting terminates the returned run with session.StopError carrying a cause matching errors.Is(_, ErrNotAwaiting). A consumer reads this off the run's terminal EvResult (Stop == StopError, the Error field), exactly like any other terminal — it never silently completes. The pending tool executes EXACTLY ONCE on the allow path and NOT AT ALL on a precondition failure or a deny.
type ReviewAssessment ¶ added in v0.16.0
type ReviewAssessment string
ReviewAssessment is the reviewer's three-state decision.
const ( ReviewAcceptable ReviewAssessment = "acceptable" ReviewProhibited ReviewAssessment = "prohibited" ReviewUnresolved ReviewAssessment = "unresolved" )
Contextual review assessments.
type ReviewCaller ¶ added in v0.16.0
ReviewCaller describes the reviewed caller's effective authority.
type ReviewCapacity ¶ added in v0.16.0
type ReviewCapacity struct {
MaxEvidenceHandles int
MaxEvidenceBytes int64
MaxTrajectoryFacts int
MaxTrajectoryBytes int64
}
ReviewCapacity reports the applicable count and byte capacities.
type ReviewConcern ¶ added in v0.16.0
type ReviewConcern struct{ Ref, Category, Rationale, SourceRef string }
ReviewConcern is one validated reviewer finding.
type ReviewDetail ¶ added in v0.16.0
type ReviewDetail struct {
RootSessionID, SessionID session.SessionID
ReviewID, Concern, SourceDisplay, NextAction string
}
ReviewDetail is transient, owner-authorized human display detail. RootSessionID is the owning delegation-root run; SessionID is the reviewed root or child. Sinks must scope child relations and cleanup to RootSessionID, never infer a child to be its own root.
type ReviewDetailSink ¶ added in v0.16.0
type ReviewDetailSink interface {
PublishReviewDetail(context.Context, ReviewDetail)
}
ReviewDetailSink publishes transient review detail under the explicit root-run ownership carried by ReviewDetail.RootSessionID.
type ReviewEvidence ¶ added in v0.16.0
type ReviewEvidence struct {
Handle, Kind, Version string
Continuation string
Complete bool
Content string
}
ReviewEvidence is one bounded evidence preview.
type ReviewEvidenceMeta ¶ added in v0.16.0
type ReviewEvidenceMeta struct {
Handle, Kind, Display, Version string
// Continuation is the next opaque handle for the same versioned evidence
// object. Empty means this page is final.
Continuation string
Complete bool
}
ReviewEvidenceMeta advertises one review-local evidence handle.
type ReviewEvidencePreparation ¶ added in v0.16.0
type ReviewEvidencePreparation struct {
Request ToolReviewRequest
Environment tool.Environment
Owner *session.Principal
Result *session.ToolResult
// Authorize verifies one evidence-source call against the originating run's
// effective catalog, mode, and bound authority. Preparers must call it before
// touching backend metadata or content; model/reviewer input cannot replace it.
Authorize func(context.Context, session.ToolCall) error
}
ReviewEvidencePreparation is the trusted, immutable input used to mint a finite evidence inventory. Owner comes from the admitted Session, never from reviewer or model output. Result is non-nil only for an inbound review.
type ReviewEvidencePreparer ¶ added in v0.16.0
type ReviewEvidencePreparer interface {
PrepareReviewEvidence(context.Context, ReviewEvidencePreparation) (PreparedReviewEvidence, error)
}
ReviewEvidencePreparer mints finite evidence capabilities under the exact admitted environment and caller authority. Implementations must reject an unsupported backend rather than reading through Workspace.Root as a host path.
type ReviewEvidenceRequest ¶ added in v0.16.0
type ReviewEvidenceRequest struct{ ReviewID, Handle, Version string }
ReviewEvidenceRequest names one advertised review-local capability.
type ReviewEvidenceSource ¶ added in v0.16.0
type ReviewEvidenceSource interface {
ReadReviewEvidence(context.Context, ReviewEvidenceRequest) (ReviewEvidence, error)
}
ReviewEvidenceSource resolves review-local evidence capabilities.
type ReviewEvidenceUse ¶ added in v0.16.0
ReviewEvidenceUse records how read evidence supports an assessment.
type ReviewFailureCode ¶ added in v0.16.0
type ReviewFailureCode string
ReviewFailureCode is a closed, machine-safe classification for review failures. It is safe to project across event, detail, and health boundaries; implementations must never derive it from provider/model text.
const ( ReviewFailureProviderFailure ReviewFailureCode = "provider_failure" ReviewFailureTimeout ReviewFailureCode = "timeout" ReviewFailureBlankAssessment ReviewFailureCode = "blank_assessment" ReviewFailureMalformedAssessment ReviewFailureCode = "malformed_assessment" ReviewFailureInvalidAssessment ReviewFailureCode = "invalid_assessment" ReviewFailureMissingSubmit ReviewFailureCode = "missing_submit" ReviewFailureEvidenceFailure ReviewFailureCode = "evidence_failure" )
Contextual review failure codes.
type ReviewFailureRecorder ¶ added in v0.16.0
type ReviewFailureRecorder interface {
RecordGuardrailReviewFailure(ToolReviewResult, error)
}
ReviewFailureRecorder receives engine-side preparation failures that occur before ToolReviewer.Review can be called.
type ReviewGrantStore ¶ added in v0.16.0
type ReviewGrantStore interface {
GrantDigest(request ToolReviewRequest) (digest string, eligible bool)
AllowsGrant(digest string) bool
ArmGrant(digest, sessionID string)
}
ReviewGrantStore is the optional exact-action repeat-grant seam. GrantDigest must bind the exact session, environment revision, caller authority, effective call, target, and every eligible versioned dependency under a purpose-separated keyed digest; false means repeat is ineligible. AllowsGrant tests that digest. ArmGrant stores only that digest for the named session and must not widen its scope or persist it beyond the implementation's documented session lifetime. ArmGrant must be a bounded, non-blocking publication and must not call back into the engine; final grant publication may invoke it while the principal-revision mutex is held to make validation and publication atomic.
type ReviewJob ¶ added in v0.16.0
type ReviewJob string
ReviewJob identifies the contextual review direction.
type ReviewMetadataProvider ¶ added in v0.16.0
type ReviewMetadataProvider interface {
GuardrailReviewMetadata(toolName string, job ReviewJob) (ruleID, ruleOrigin, providerID, modelID string)
}
ReviewMetadataProvider projects machine-safe metadata for the rule selected by toolName and job. Empty return values mean no metadata is available.
type ReviewMissingEvidence ¶ added in v0.16.0
type ReviewMissingEvidence struct{ Ref, Kind, Handle, Reason string }
ReviewMissingEvidence describes decision-relevant unavailable evidence.
type ReviewPolicyProvider ¶ added in v0.16.0
type ReviewPolicyProvider interface {
GuardrailReviewPolicy(toolName string, job ReviewJob, operationalFailure bool) (applies, enforce bool)
}
ReviewPolicyProvider declares configured applicability and enforcement. The toolName and job identify the candidate rule; operationalFailure selects its checker-down posture. The returns report whether review applies and whether a finding or unresolved assessment is enforced. A ToolReviewer without this capability retains action-only enforcement. Report the operator's configured enforcement independent of permission mode: the engine demotes it to advisory for each executing yolo-mode session, including delegated children that share the reviewer.
type ReviewPrincipalFact ¶ added in v0.16.0
ReviewPrincipalFact carries one harness-established principal fact.
type ReviewTarget ¶ added in v0.16.0
type ReviewTarget struct{ Kind, Display, DestinationID string }
ReviewTarget identifies the destination implicated by a review.
type ReviewTrajectoryFact ¶ added in v0.16.0
type ReviewTrajectoryFact struct {
Call session.ToolCallID
Ref, Direction, DataClass, TargetID, Decision string
SessionID, Tool, ApprovalOrigin, ApprovalKind, ReviewID string
}
ReviewTrajectoryFact carries one harness-established root-run fact.
type Run ¶
type Run struct {
// contains filtered or unexported fields
}
Run is an asynchronous agent execution.
func (*Run) Approve ¶
func (r *Run) Approve(askID string, v session.ApprovalVerdict) error
Approve is the compatibility helper for ordinary permission and legacy action approvals. Result release requires ResolveApproval's explicit review acknowledgement.
func (*Run) Cancel ¶
func (r *Run) Cancel()
Cancel aborts the in-flight run while preserving ordinary child join-and-drain behavior. It is a no-op for an already durably parked authorization, whose resumable handoff point must remain intact.
func (*Run) CancelChild ¶
CancelChild requests cancellation of ONE child of this run, addressed by its child session id (the `agentId:` trailer on the Subagent result / the overlay ChildID — the single handle convention; no per-family interpretation). It is the per-child mirror of Approve's routing role: a client frame addressed at a child, routed by the parent Run.
It is idempotent and safe from any goroutine; an unknown or already-done id returns false (the finished-as-you-pressed race is benign). On a live child it: marks the registry entry clientCancelled (so the Subagent terminal renders "[subagent cancelled by user]" rather than a generic cancel), SNAPSHOTS the child's surfaced askIDs (without clearing — see below), cancels the child's per-call context OUTSIDE the registry lock (unwinding a mid-drive turn, a gate wait, or a parked askRegistry.await alike), then EAGERLY retracts the snapshot via the shared retractAsksVia: each askID is unregistered from the parent's childAskRouter atomically with its permission.retract emit (one emitMu section) — so a racing late approval falls through to the parent's own registry and dies as an unknown-ask no-op (fail-safe ordering), while the client dismisses its modal promptly, ahead of the child's unwind. The unregister's answered-vs-pending gate also means an ask whose verdict was JUST routed (route deleted the router entry first) does not draw a spurious retract.
The eager retract is BEST-EFFORT only — it runs on the caller's goroutine, unsynchronized with the run's terminate path, so it can lose a scheduling race to the seal (then its emitMu section sees sealed and skips, gate untouched). The GUARANTEED leg is the child's own registry terminal (markDoneResult — the retraction chokepoint, pre-doneCh-close ⇒ pre-seal), which takes the un-cleared set and retracts whatever this path didn't deliver; the atomic unregister gate keeps the two legs exactly-once.
func (*Run) CancelSteer ¶
func (r *Run) CancelSteer() (SteerOutcome, error)
CancelSteer is the wire-facing steer-cancel entry point: a live run's Service routes an operator steer_cancel here. It retracts the PENDING (un-drained) steer and reports the authoritative SteerOutcome (retracted/none_pending) — a steer that already drained at a turn boundary is ordinary recorded history and cannot be retracted (the cancel reports none_pending then: the drain won).
func (*Run) EnqueueSteer ¶
EnqueueSteer enqueues a text, media, or mixed steer atomically without a correlation id. It is the compatibility entry point for engine consumers that do not need a committed-echo watermark. It reports the authoritative SteerOutcome (accepted/appended/too_late), never an error for the ordinary too-late race.
func (*Run) EnqueueSteerWithMessageID ¶ added in v0.15.0
func (r *Run) EnqueueSteerWithMessageID(text string, parts []session.Content, messageID string) (SteerOutcome, error)
EnqueueSteerWithMessageID enqueues a text, media, or mixed steer together with the client-minted message id that identifies its committed echo. The id is part of the mutex-guarded pending bundle, so a drain observes the exact watermark that belonged to the content it took. When multiple steers append before a drain, the latest id becomes the bundle watermark.
func (*Run) Events ¶
Events returns the channel of domain Events for this run. It closes after a terminal result or a durable external-authorization park.
func (*Run) Outcome ¶ added in v0.15.0
func (r *Run) Outcome() RunOutcome
Outcome reports why this Run's event stream closed.
func (*Run) ResolveApproval ¶ added in v0.16.0
func (r *Run) ResolveApproval(resolution ApprovalResolution) error
ResolveApproval validates purpose, review identity, verdict eligibility, and pending identity while holding the ask registry lock, then submits the verdict exactly once. A failed validation leaves the ask registered and unresolved.
func (*Run) ResolveOrdinaryAsk ¶ added in v0.15.0
func (r *Run) ResolveOrdinaryAsk(askID string, v session.ApprovalVerdict) AskResolution
ResolveOrdinaryAsk atomically resolves one pending non-plan permission ask owned by this run or surfaced from a child. It reports plan-originated asks without consuming them, leaving the dedicated plan-resolution choreography authoritative. Unknown and already-resolved ask IDs report not pending.
Approve remains the source-compatible all-origin wrapper for callers that do not need to distinguish these outcomes.
func (*Run) ResolvePlanAsk ¶ added in v0.16.0
func (r *Run) ResolvePlanAsk(askID string, verdict session.ApprovalVerdict) AskResolution
ResolvePlanAsk atomically consumes only a root plan-originated pending ask. A child ask cannot authorize the root plan, regardless of its tool name.
func (*Run) RetractPermissionAsk ¶ added in v0.13.0
RetractPermissionAsk withdraws this run's own pending permission ask without resolving it and emits one permission.retract event. It returns false when the ask is unknown or already resolved. The run remains parked until its host cancels it; this narrow seam lets a lease-owning host retract local delivery while preserving an already-durable awaiting snapshot for a successor.
func (*Run) RunID ¶
RunID reports the opaque, host-minted identity of this run, or "" when the host supplied none.
It exists so a caller holding a *Run can ASK which run it holds, rather than inferring it from the session aggregate. That distinction matters for stale controls: a control addressed at a specific run must be compared against the run it would actually affect, and a session's aggregate is a step removed from that (it names the session's CURRENT run, which after a terminal race may not be the one the caller is holding).
func (*Run) ValidateRemoteApprovalIntent ¶ added in v0.16.0
func (r *Run) ValidateRemoteApprovalIntent(askID, reviewID string, kind session.GuardrailApprovalKind, verdict session.ApprovalVerdict) error
ValidateRemoteApprovalIntent is retained for source compatibility. Hosts must use ResolveApproval for an atomic validate-and-submit operation.
type RunOutcome ¶ added in v0.15.0
type RunOutcome int32
RunOutcome records why a Run's event stream closed. Its zero value means the run is still active.
const ( // RunOutcomeUnknown means the run is still active. RunOutcomeUnknown RunOutcome = iota // RunOutcomeCompleted means the run emitted a terminal EvResult. RunOutcomeCompleted // RunOutcomeAuthorizationPending means the run durably parked without an // EvResult or terminal session stop. RunOutcomeAuthorizationPending )
type RunRequest ¶
type RunRequest struct {
// Text is the (possibly empty) text of the user prompt. Command expansion and the
// UserPromptSubmit hook operate on Text only; the media Parts pass through untouched
// and are recorded verbatim on the user message. Text may be empty when Parts
// carries the content.
Text string
// Parts carries non-text media (image/audio) alongside Text. nil for a text-only
// prompt. The media passes through to the engine untouched.
Parts []session.Content
// CanPresentAuthorization permits this main run to present a required external
// authorization. The zero value fails closed.
CanPresentAuthorization bool
// MaxRunTokensOverride, when > 0, is a per-run TIGHTEN-ONLY override of the engine's
// Deps.MaxRunTokens budget: the effective ceiling for THIS run is the lower of the
// two non-zero values (a per-call ceiling may make the run stricter than the operator
// default, never looser — mirroring the per-call limit tighten-only discipline). 0
// (the default) inherits the engine's Deps.MaxRunTokens unchanged.
MaxRunTokensOverride int
// ExtraTools are run-scoped tools layered OVER the engine's catalog for THIS run
// only: their specs are advertised to the model this run and they are dispatchable,
// but they are never registered into the shared catalog (so concurrent runs of the
// same engine never see them, and the engine is not mutated). A name collision with a
// catalog tool resolves to the EXTRA tool (the run-scoped overlay wins) for THIS run.
// The Subagent tool uses this to inject the synthetic SubmitResult deliverable tool for a
// structured-output child. Every ExtraTool MUST be ReadOnly (it is dispatched on the
// read-parallel path); a structured-output SubmitResult records into a per-run sink
// and performs no workspace mutation, so it is read-only.
ExtraTools []tool.Tool
// RunID is the opaque, host-minted identity of THIS run.
//
// It does two things and nothing else. Every event this run emits is stamped
// with it at Run.emit/emitOrAbort, beside the existing Seq stamp, so no relay,
// transport, or persistence path downstream can omit it. And when
// AskIDDiscriminator is empty it also SUPPLIES the ask discriminator, which
// is what "a durable host passes its own RunID" means —
// so a durable host sets ONE field, not two carrying the same value.
//
// HOST CONTRACT (inherited from AskIDDiscriminator, because it feeds it): the
// value must be UNIQUE per run-ATTEMPT and STABLE across processes for the
// SAME attempt, or the CWE-863 askID replay guard weakens. It should be
// colon-free; a colon-bearing value still stamps events fine but cannot serve
// as an ask discriminator (the askID grammar would be ambiguous), so the run
// falls back to the process-global serial for asks and logs a WARN.
//
// Empty (the zero value) is the legacy behaviour exactly: events carry an
// empty RunID and asks use the process-global serial. mecatui, mecademo, and
// tests pass nothing and are unaffected.
//
// The loop's licence over this value is deliberately narrow: STAMP it, and
// DERIVE the ask discriminator from it. It must never be branched on, logged,
// sent to a provider, or used to reach storage.
RunID string
// AskIDDiscriminator, when non-empty, supplies the colon-free HOST namespace
// of every askID minted this run. Live issuance appends a per-run `.aN`
// occurrence token to the opaque call component, making repeated approvals for
// one call distinct while retaining the exact host suffix and four-component
// colon grammar. The host value remains reconstructable from persisted state.
// The host MUST supply a value that is (a) UNIQUE per run-ATTEMPT and (b)
// STABLE across processes for the SAME attempt — this preserves the CWE-863
// replay guard the process-global serial provides (the Interrupt/re-mint
// scenario documented on newAskID): a stale verdict for a retracted ask must
// never resolve a re-minted ask of a different attempt. (c) It MUST be
// colon-free to keep the askID grammar unambiguous; a value containing a colon
// is IGNORED and the run falls back to the process-global "r<serial>" (a WARN
// is logged) rather than minting an ambiguous id. Empty (the zero value) keeps
// the legacy "r<serial>" behavior with no change — mecatui, tests, and
// in-memory hosts pass nothing and are unaffected. A durable host (e.g. a downstream consumer)
// passes its own RunID. Same opt-in RunRequest seam pattern as
// MaxRunTokensOverride/ExtraTools.
//
// FOOTGUN GUARD: after startRun the RESOLVED value (this when valid, else the
// "r<serial>" fallback) lives on Run.askDiscriminator. askID minting (newAskID,
// in authorize) MUST read r.askDiscriminator — NEVER this raw, un-validated
// r.req.AskIDDiscriminator, which may be empty or colon-bearing and would
// bypass the colon/empty fallback.
AskIDDiscriminator string
// contains filtered or unexported fields
}
RunRequest is the single request shape a caller threads into Engine.Run. It carries the user prompt (text and/or non-text media parts) PLUS the run-scoped overrides that live on the Run, never on the shared Engine.Deps, so a per-call knob (a tighter token ceiling, a synthetic deliverable tool) works on a SHARED child engine WITHOUT minting a fresh engine or mutating the engine other concurrent runs share. The zero value of the override fields is the legacy run (no override, no extras).
type ScheduleQueryTool ¶
type ScheduleQueryTool struct {
// contains filtered or unexported fields
}
ScheduleQueryTool is the model-facing READ-ONLY scheduled-task query tool (list/inspect). It shares the injected port.ScheduleManager with the mutating ScheduleTool but reports ReadOnly()==true, so its calls join the read-parallel batch — the AC1.4 partition (a mutating verb can never fan out from here because this tool carries none).
func (*ScheduleQueryTool) Execute ¶
func (t *ScheduleQueryTool) Execute(ctx context.Context, call session.ToolCall, _ tool.Environment) (session.ToolResult, error)
Execute dispatches on the read-only verb (list/inspect). A mutating verb is a model-addressable unknown-verb error (this tool carries none).
func (*ScheduleQueryTool) ReadOnly ¶
func (*ScheduleQueryTool) ReadOnly() bool
ReadOnly reports true — list/inspect never mutate the workspace or the schedule registry, so they are parallel-safe (the read-half of the AC1.4 partition).
func (*ScheduleQueryTool) Spec ¶
func (*ScheduleQueryTool) Spec() tool.ToolSpec
Spec returns the model-facing specification for the read-only Schedule query tool.
type ScheduleTool ¶
type ScheduleTool struct {
// contains filtered or unexported fields
}
ScheduleTool is the model-facing MUTATING scheduled-task management tool (create/pause/resume/delete/fire). It consumes the consumer-local port.ScheduleManager seam composition injects (the server Service's schedule methods) — the SAME validated create-seam the REST/gRPC handlers ride, never a second path, so a schedule created in-chat is indistinguishable from an API-created one (one store, one truth).
READONLY PARTITION (the dispatch invariant — AGENTS.md): the read-only verbs (list/inspect) live on the SEPARATE ScheduleQueryTool (ReadOnly()==true, read-parallel); this tool carries ONLY the mutating verbs and reports ReadOnly()==false, so every mutating Schedule call serialises on the mutate path and NEVER runs concurrently with a sibling read. The per-verb split is realised as two catalog entries over the one ScheduleManager because a single tool's ReadOnly() takes no args — pinned by TestScheduleTool_ReadOnlyPartition.
func (*ScheduleTool) Execute ¶
func (t *ScheduleTool) Execute(ctx context.Context, call session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute dispatches on the verb, mapping the call args onto the injected ScheduleManager and rendering the result as model-readable text. A verb-level error (an unknown schedule, a rejected create, a fire overlap) is a MODEL-ADDRESSABLE ToolResult (IsError), not a harness-level error — the loop records it and lets the model react.
func (*ScheduleTool) ReadOnly ¶
func (*ScheduleTool) ReadOnly() bool
ReadOnly reports false — this tool carries ONLY the mutating verbs (create/pause/resume/delete/fire), so every call serialises on the mutate path and never overlaps a sibling read (the dispatch invariant). The read-only list/inspect verbs live on the ScheduleQueryTool (ReadOnly()==true). See the type doc.
func (*ScheduleTool) Spec ¶
func (*ScheduleTool) Spec() tool.ToolSpec
Spec returns the model-facing specification for the mutating Schedule tool.
type ShellStatusTool ¶ added in v0.15.0
type ShellStatusTool struct{}
ShellStatusTool reports the LIVE state of this run's background Shell jobs, collects a finished job's result, and cancels a live job. It reaches the parent run's child registry through parentCaps (the childCapableTool seam) — the same seam SubagentStatus uses, filtered to the bash-cmd family. It is registered wherever the agent Shell tool is (the composition root's main catalogs), never in child catalogs.
func (*ShellStatusTool) Execute ¶ added in v0.15.0
func (*ShellStatusTool) Execute(_ context.Context, call session.ToolCall, _ tool.Environment) (session.ToolResult, error)
Execute is the caps-less path (plain Execute, no parent run threaded): there is no registry to read, which is an honest model-addressable error — this tool is only meaningful inside a run that registers its children.
func (*ShellStatusTool) ExecuteWithParent ¶ added in v0.15.0
func (t *ShellStatusTool) ExecuteWithParent(ctx context.Context, call session.ToolCall, env tool.Environment, _ func(session.Event), caps parentCaps) (session.ToolResult, error)
ExecuteWithParent is the childCapableTool seam: it receives the parent run's capabilities and reads/collects/cancels through caps.children, filtered to the bash-cmd family (bashCmdFamiliesOnly).
func (*ShellStatusTool) ReadOnly ¶ added in v0.15.0
func (*ShellStatusTool) ReadOnly() bool
ReadOnly reports that ShellStatus never mutates the workspace: its reads hit the in-memory registry, and cancel only signals the job's context (the kill is the job drive's own ctx reaction). The dispatcher may therefore run it read-parallel — which is also what lets a wait_ms park overlap other tools in the same turn.
func (*ShellStatusTool) Spec ¶ added in v0.15.0
func (*ShellStatusTool) Spec() tool.ToolSpec
Spec returns the model-facing specification for the ShellStatus tool.
type ShellTool ¶ added in v0.15.0
type ShellTool struct{}
ShellTool is the agent-loop Shell tool: the foreground path is byte-identical to the fstools Shell body's orchestration (same arg validation, timeout ctx, runner.Run, combined-output shaping, exit-code error), and background:true detaches the command as a run-scoped background job on the parent run's child registry (the childCapableTool seam). It lives in engine/agent — not the fstools adapter — because the background half needs the child registry, which is an agent-package type.
The tool registers under tool.ShellToolName ("Shell"): the permission evaluator special-cases that literal name (resolveShell / planModeDecision / LearnableRule), so a second tool name would silently bypass the bash gate. Statically non-read-only: whether a specific command is read-only is governance's job, not this tool's.
The runner is read off the tool.Environment at Execute time (issue #462), NOT captured at construction: a bound runner is part of the per-namespace Environment (main session, or a forked child whose runner is bound to the child namespace), so the command's cwd always matches the workspace the tool executes against, never a stale shared parent base. A namespace with no shell (env.CommandRunner == nil) surfaces ErrNoShell honestly. The composition root decides whether to REGISTER a Shell tool at all based on runner availability; a shell-less catalog simply omits Shell.
func (ShellTool) Execute ¶ added in v0.15.0
func (t ShellTool) Execute(ctx context.Context, in session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute runs a FOREGROUND Shell call. A background:true call must arrive via the childCapableTool seam (ExecuteWithParent), which owns the child registry; on the plain Execute path it gets an honest error, never a silent foreground fallback (the model was promised detached delivery).
func (ShellTool) ExecuteWithParent ¶ added in v0.15.0
func (t ShellTool) ExecuteWithParent(ctx context.Context, in session.ToolCall, env tool.Environment, _ func(session.Event), caps parentCaps) (session.ToolResult, error)
ExecuteWithParent is the childCapableTool seam. The foreground half is the same as Execute; the background half registers a run-scoped job on the parent run's child registry (cancelled at run end by its drain), detaches the drive, and returns the started-result immediately. It emits NO events — a background Shell job is not a delegation family; the started-result, the registry (notice/status/collect), and the stored result are the only channels.
type SteerOutcome ¶
type SteerOutcome string
SteerOutcome is the closed-enum result of a steer inbox transition (enqueue or cancel). The engine is AUTHORITATIVE: the client cannot observe the exact drain moment across stream latency, so the outcome reports what actually happened to the steer rather than letting the client guess. It is an ENUM, not stacked booleans — AGENTS.md's "a third provenance ⇒ extract an enum" discipline (a bool pair cannot express accepted-vs-appended-vs-too_late).
const ( // SteerAccepted means the steer was parked in the single pending slot (the // slot was empty). It will drain at the next turn boundary unless retracted // first. SteerAccepted SteerOutcome = "accepted" // SteerAppended means the steer found the pending slot OCCUPIED and was // MERGED into it: the pending bundle's text grew by "\n\n"+text (it still // drains as ONE bundle). Append is the DEFAULT for a second steer on the // occupied slot (round-3 rework; its outcome distinguished from accepted — // the new pending bundle — so the client can render "merged onto pending" // honestly). Replacing the pending bundle is the explicit cancel path // (steer_cancel, then a fresh steer); append and replace are distinct. SteerAppended SteerOutcome = "appended" // SteerRetracted means a cancel found a pending steer and retracted it; the // run drains nothing for it and the next turn sees no injected message. SteerRetracted SteerOutcome = "retracted" // SteerNonePending means a cancel found the slot EMPTY — there was nothing // to retract (no steer was pending, or the run is terminal). SteerNonePending SteerOutcome = "none_pending" // SteerTooLate means the steer arrived after the run went terminal (the // inbox is closed) — it is never parked and never silently drained into a // finished turn. The service layer promotes a too_late steer to a fresh // follow-up run; the engine only reports the outcome. SteerTooLate SteerOutcome = "too_late" )
type SubagentModelRouter ¶ added in v0.15.0
type SubagentModelRouter struct {
Backend string
ClassifierModel string
MinimumConfidence *float64
Route func(context.Context, string) ModelRouteResult
}
SubagentModelRouter configures delegated-model classification and exposes stable configured metadata even when a delegation gate skips Route.
type SubagentOption ¶
type SubagentOption func(*SubagentTool)
SubagentOption configures a SubagentTool.
func WithAgentEngines ¶
func WithAgentEngines(engines map[string]*Engine, meta []AgentMeta) SubagentOption
WithAgentEngines injects the per-definition child engines (keyed by agent name) and their (name, description) metadata for progressive disclosure. The composition root builds each engine with a SCOPED, read-only catalog (the Subagent read-only invariant is preserved — see ReadOnly) and the def's resolved model/prompt, then passes the map and a name-sorted meta slice here.
engines and meta should describe the same set of names; meta drives the Spec enumeration while engines drives routing. A nil/empty map leaves Subagent with only the default explorer (no behaviour change). It is the agent-package boundary the agents adapter never crosses: only plain map + structs flow in.
func WithAgentModelEngineFactory ¶
func WithAgentModelEngineFactory(f func(agentName string, target ModelTarget) (*Engine, bool)) SubagentOption
WithAgentModelEngineFactory rebuilds a named specialist on a provider/model target while preserving its scoped tools, prompt, and limits. Bare model overrides use the specialist's provider; automatic routing may select another provider.
func WithAgentWritableEngineFactory ¶
func WithAgentWritableEngineFactory(f func(agentName string) (*Engine, bool)) SubagentOption
WithAgentWritableEngineFactory injects the composition-supplied factory that mints a WRITABLE child engine for a mode:"read-write"+`agent` call (a writable named specialist). Given an agent name it REBUILDS the named specialist's scoped engine (catalog/prompt/hooks/memory) with allowMutating=true on the def's resolved provider/model through the SAME contamination-safe per-provider path the startup engines use (buildAgentDefEngine → newChildEngineForProvider re-derives Compactor/TokenCounter/Env.Model/ContextWindow), using the MAIN session's command runner (direct-write parity — no fork, no copy, no merge-back); its Edit/Write/Shell mutate the REAL parent workspace in place, exactly as the main agent does, and git is the rollback layer. The pre-built agentEngines map is NEVER mutated (a fresh engine is minted per call). It returns (engine, true) for a known reference-only-MCP def and (nil, false) for an unknown agent or a def with INLINE MCP servers (a v1 scope limit — the inline manager's live session has no process-lifetime owner on a per-call engine; selectChildEngine surfaces the model-addressable error). Reference-only MCP servers ARE supported (they borrow the process-lifetime mainMgr, no new connection).
nil (the default, and ALWAYS on the no-FS path) leaves Subagent without writable- specialist support: a mode:"read-write"+`agent` call then errors with a clear "not supported in this deployment" message from validateMode. Explicit read-write+`agent`+ `model` arguments together are OUT OF SCOPE for v1 (validateMode rejects them before this factory is ever consulted); a router-selected model is handled by the separate WithAgentWritableModelEngineFactory. It is layering- clean: the closure takes a string and returns *Engine — both agent-layer types — and no adapter/proto/server type crosses (same shape as WithAgentModelEngineFactory).
func WithAgentWritableModelEngineFactory ¶
func WithAgentWritableModelEngineFactory(f func(agentName string, target ModelTarget) (*Engine, bool)) SubagentOption
WithAgentWritableModelEngineFactory rebuilds a writable named specialist on a routed provider/model target. A decline falls back to the default writable specialist.
func WithChildForker ¶
func WithChildForker(f tool.EnvironmentForker) SubagentOption
WithChildForker injects the workspace-isolation seam each child run forks before executing. The composition root wires it ONLY when the child catalog includes Shell (the read-only explorer's shell), so the child's mutating-classified Shell lands in a throwaway git worktree, never the shared parent base — preserving Subagent's read-parallel safety (see ReadOnly). It should be the forker's DEFAULT mode (git worktree: shares the base repo's `.git` ⇒ full history for git log/show). When the forker is nil (the default), the child shares the parent content backend through any composition-supplied authority-narrowing Workspace view. A fork failure on this path is a tool error, not a silent fallback.
func WithChildLimits ¶
func WithChildLimits(l session.Limits) SubagentOption
WithChildLimits overrides the subagent's stop conditions. Use it to make a child even tighter (or, rarely, looser) than the defaults.
func WithChildMode ¶
func WithChildMode(m session.PermissionMode) SubagentOption
WithChildMode sets the permission mode the child session runs under. Without it the child inherits an auto or yolo parent's mode and otherwise runs in session.ModeDefault (see effectiveChildMode); an explicit session.ModeDefault keeps default even under an auto or yolo parent. session.ModePlan additionally hides any non-read-only tools from the child at the catalog level.
func WithChildSessionPrefix ¶
func WithChildSessionPrefix(p string) SubagentOption
WithChildSessionPrefix sets the prefix used to derive child SessionIDs (default "subagent"). Child ids are of the form "<prefix>-<callID>".
func WithMaxConcurrentChildren ¶
func WithMaxConcurrentChildren(n int) SubagentOption
WithMaxConcurrentChildren bounds how many Subagent children may run CONCURRENTLY — forking AND forker-less (default defaultMaxConcurrentChildren). It is the single fan-out brake on the dispatcher's read-parallel batch: N Subagent calls in one turn each block on the gate, so at most cap children run at once. A value < 1 is clamped to 1 (a zero-capacity gate would deadlock).
func WithPinnedAgents ¶
func WithPinnedAgents(names []string) SubagentOption
WithPinnedAgents injects the composition-computed SET of agent-def names that expressed model intent (ANY non-empty `model:`, including explicit `inherit`). The set is used only to attribute an unrouted named delegation as RoutingReasonAgentDefPinned. It is separate from WithRoutableAgents because a def may be unroutable for reasons other than a model pin (for example a provider switch or inline MCP server); those cases report router-disabled. nil/empty is safe and preserves the pre-option default.
func WithRoutableAgents ¶
func WithRoutableAgents(names []string) SubagentOption
WithRoutableAgents injects the composition-computed SET of agent-def names eligible for the OPT-IN model router (issue #286): a def that expressed NO model intent (absent `model:`), does not switch provider away from the parent, and has no inline MCP servers. An `agent`-named delegation to one of these — read-only, with the agent+model factory wired — is CLASSIFIED and its SCOPED engine rebuilt on the routed model (fail-soft to the pre-built def engine). Absence from this set does NOT itself mean pinned: provider-switched and inline-MCP defs are also ineligible. WithPinnedAgents carries the narrower attribution set. nil/empty (the default) means NO def routes — byte-identical to pre-#286. It is layering-clean: only def NAME strings flow in. It is inert when the router is off (routeTask nil) — wiring it unconditionally is safe.
func WithSharedChildWorkspace ¶
func WithSharedChildWorkspace(view func(tool.Workspace) tool.Workspace) SubagentOption
WithSharedChildWorkspace injects a capability-narrowing view for base-sharing children. The function receives the actual parent Workspace and must preserve its content backend; it exists so child authority can be stricter than a posture-relaxed main-session wrapper without reconstructing storage from Root.
func WithSubagentEngineFactory ¶
func WithSubagentEngineFactory(f func(ModelTarget) (*Engine, bool)) SubagentOption
WithSubagentEngineFactory mints a fresh child on a resolved provider/model target. A bare per-call model has an empty Provider; composition uses the parent's provider. A declined explicit selection errors, while automatic routing falls back.
func WithSubagentNoFSNote ¶
func WithSubagentNoFSNote() SubagentOption
WithSubagentNoFSNote tells the Subagent tool's Spec() that this session has NO FILESYSTEM (the "no-fs" session profile), replacing the WHOLE tool-surface description: under no-FS the spec must not claim Read/Grep/Glob, a worktree shell, kept file changes (Parallel — absent under no-FS), or "a quick read you can do with Read/Grep" — it describes the real child surface instead (MCP tools, memory, web fetch; no file access, no shell). DISTINCT from WithSubagentShellDisabledNote (issue #40), which swaps only the shell clause and keeps the read-only file-tool claims that are still true on that path. Without this option the description stays byte-identical to the default one.
func WithSubagentOwnershipEnforced ¶
func WithSubagentOwnershipEnforced(ownershipEnforced bool) SubagentOption
WithSubagentOwnershipEnforced records whether the request edge verifies caller identity. Enabled deployments require a resume caller to match the persisted child owner; disabled deployments retain legacy ownerless compatibility.
func WithSubagentProvider ¶ added in v0.17.0
func WithSubagentProvider(provider string) SubagentOption
WithSubagentProvider supplies the concrete provider used by the inherited child engine so start metadata remains truthful when no explicit selector is present.
func WithSubagentReadLedgerFactory ¶ added in v0.14.0
func WithSubagentReadLedgerFactory(factory func() tool.ReadLedger) SubagentOption
WithSubagentReadLedgerFactory injects the mandatory fresh child-ledger factory.
func WithSubagentSelectorResolver ¶ added in v0.17.0
func WithSubagentSelectorResolver(r SubagentSelectorResolver) SubagentOption
WithSubagentSelectorResolver injects composition-owned alias, provider, and explicit router-category resolution.
func WithSubagentShellDisabledNote ¶
func WithSubagentShellDisabledNote(reason string) SubagentOption
WithSubagentShellDisabledNote tells the Subagent tool's Spec() that NO child gets a shell on this workspace and why. With it set, the spec's "plus a full shell in an isolated, throwaway git worktree …" clause is REPLACED by an honest read-only-only description carrying the reason, so the model never plans build/test/git delegation the child cannot perform. The composition root sets it ONLY when the workspace-trust gate withheld the shell (issue #40) — a shell-less deployment (--no-bash / empty shell) keeps the default description unchanged. An empty reason is a no-op (the default, byte-identical description).
func WithSubagentStopHook ¶
func WithSubagentStopHook(h port.HookRunner) SubagentOption
WithSubagentStopHook injects the HookRunner that fires the SubagentStop hook when a child run finishes. It is best-effort: a hook error or block never fails the Subagent call. Passing nil disables the hook.
func WithSubagentStore ¶
func WithSubagentStore(store port.SessionStore) SubagentOption
WithSubagentStore injects the optional session store each child session is best-effort persisted to after its run (ALL terminals: clean, limit-stopped, structured-output-exhausted, errored, cancelled/timed out — the final state after any structured-output re-drives). Nil disables persistence. Mirrors the team supervisor's WithMemberStore/persistMember discipline: the save is advisory and a failure is swallowed (no diagnostic). The tool consumes the port.SessionStore interface, never a concrete adapter, so no layering rule is crossed.
func WithWritableChildEngine ¶
func WithWritableChildEngine(e *Engine) SubagentOption
WithWritableChildEngine injects the child *Engine a mode:"read-write" Subagent call runs on: a WRITABLE explorer whose catalog includes Edit/Write (the composition root builds it over the read-only explorer catalog + Edit + Write, using the MAIN session's command runner). It is SEPARATE from the read-only childEngine; a read-write call selects this engine instead, so the read-only fan-out path is byte-identical when read-write is never used. A read-write child runs DIRECTLY against the parent workspace — no fork, no copy, no merge-back; git is the rollback layer. nil (the default, and the no-FS path) leaves writable subagents unwired (a read-write arg then errors).
func WithWritableEngineFactory ¶
func WithWritableEngineFactory(f func(ModelTarget) (*Engine, bool)) SubagentOption
WithWritableEngineFactory rebuilds a direct-write explorer on a provider/model target. Bare model overrides inherit the parent provider; routed targets retain their provider. A routed miss falls back to the default writable explorer.
type SubagentSelectorResolver ¶ added in v0.17.0
type SubagentSelectorResolver func(provider, model string) (ResolvedModelSelector, error)
SubagentSelectorResolver resolves call-level provider/model syntax without exposing provider registries or router configuration to engine core.
type SubagentStatusTool ¶
type SubagentStatusTool struct{}
SubagentStatusTool reports the LIVE state of this run's delegated children and collects background subagent results. It reaches the parent run's child registry through parentCaps (the childCapableTool seam) — no store, no adapter, no layering cost. It is registered wherever Subagent is (the composition root's main catalogs), never in child catalogs.
func (*SubagentStatusTool) Execute ¶
func (*SubagentStatusTool) Execute(_ context.Context, call session.ToolCall, _ tool.Environment) (session.ToolResult, error)
Execute is the caps-less path (plain Execute, no parent run threaded): there is no registry to read, which is an honest model-addressable error — this tool is only meaningful inside a run that registers its children.
func (*SubagentStatusTool) ExecuteWithParent ¶
func (t *SubagentStatusTool) ExecuteWithParent(ctx context.Context, call session.ToolCall, env tool.Environment, _ func(session.Event), caps parentCaps) (session.ToolResult, error)
ExecuteWithParent is the childCapableTool seam: it receives the parent run's capabilities and reads/collects from caps.children.
func (*SubagentStatusTool) ReadOnly ¶
func (*SubagentStatusTool) ReadOnly() bool
ReadOnly reports that SubagentStatus only reads the in-memory registry (no workspace mutation), so the dispatcher may run it read-parallel — which is also what lets a wait_ms park overlap other tools in the same turn.
func (*SubagentStatusTool) Spec ¶
func (*SubagentStatusTool) Spec() tool.ToolSpec
Spec returns the model-facing specification for the SubagentStatus tool.
type SubagentTool ¶
type SubagentTool struct {
// contains filtered or unexported fields
}
SubagentTool is the subagent delegation tool (gauntlet #7). It is a tool.Tool that, when executed, spins up a CHILD agent loop with its own fresh Session, its own (tighter) Limits, and a SCOPED tool catalog — supplied by the injected child *Engine — runs it to completion, and returns ONLY the child's final summary string as a single ToolResult.
Workspace: when a child forker is wired (WithChildForker — the composition root wires it iff the child catalog includes Shell), each child runs in its OWN isolated git WORKTREE (shares the base repo's `.git` ⇒ full history) so the explorer's shell can inspect (git log/show, cat, build, test) without its writes touching the shared parent workspace; the worktree is torn down after the child drains. Without a forker the child has no Shell and runs against the parent workspace, exactly as it originally did. Either way Subagent stays read-parallel-safe (see ReadOnly).
Context isolation is the whole point: the parent never observes the child's intermediate tool.call / tool.result / message.delta events. The child's Event stream is drained entirely inside Execute; only the terminal result text folds back into the parent conversation. This keeps a noisy "search → read N files → summarize" investigation from bloating the main context window.
The child Engine is built by the composition root (cmd/mecated, WP11) with a read-only explorer catalog (Read, Grep, Glob) that NEVER includes the Subagent tool itself — so a subagent cannot recurse — and an allow-all policy over those read-only tools so the child never needs to prompt a human. See NewSubagentTool.
Resume: when a store is wired (WithSubagentStore), a Subagent call carrying `resume` CONTINUES a previously-run child by its persisted id (the result trailer's `agentId:` line) — the prior conversation is reloaded and its terminal state recovered (completed →Reopen, cancelled→Interrupt, failed→Recover), then it runs on the default explorer engine in a FRESH workspace fork (the original worktree is gone; a staleness note is prepended). An in-flight guard rejects a concurrent run on the same id.
func (*SubagentTool) Execute ¶
func (t *SubagentTool) Execute(ctx context.Context, call session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute runs one subagent: it builds a FRESH child Session (own conversation, own Limits, its configured mode) — or, on `resume`, reloads the persisted child session and recovers its terminal state (completed→Reopen, cancelled→Interrupt, failed→Recover) before driving it in a NEW workspace fork — runs the child loop via the injected child Engine, drains the child's entire Event stream internally, and returns only the child's final summary text as a single ToolResult. The parent therefore never observes the child's intermediate events (gauntlet #7).
The child run is bounded by the parent ctx: cancelling the parent cancels the child. Any permission ask the child raises is auto-denied so the child is non-interactive. When the child finishes, the SubagentStop hook fires best-effort.
func (*SubagentTool) ExecuteObserved ¶
func (t *SubagentTool) ExecuteObserved(ctx context.Context, call session.ToolCall, env tool.Environment, emit func(session.Event)) (session.ToolResult, error)
ExecuteObserved runs the subagent like Execute but, when emit is non-nil, forwards a REDACTED, metadata-only projection of the child's activity to the parent run's event stream via the three subagent.* events. emit only sequences and channels events; it never touches the parent's Conversation, so this is orthogonal to context isolation (gauntlet #7): the child's CONTENT still never enters the parent context. It is the observableTool seam the dispatcher calls.
func (*SubagentTool) ExecuteWithParent ¶
func (t *SubagentTool) ExecuteWithParent(ctx context.Context, call session.ToolCall, env tool.Environment, emit func(session.Event), caps parentCaps) (session.ToolResult, error)
ExecuteWithParent is the childCapableTool seam: it runs the subagent like ExecuteObserved but threads the PARENT's capabilities (interactivity + the surface back-channel) into the child posture, so a child Shell ask that A1/A2 did not auto-resolve is SURFACED to the human (interactive) or auto-denied with the accurate message + operator diagnostic (headless).
func (*SubagentTool) MutatesParent ¶
func (t *SubagentTool) MutatesParent(call session.ToolCall) bool
MutatesParent implements the optional parentMutatingCaller seam: it reports whether THIS specific call will mutate the PARENT workspace. ReadOnly() stays true so read-only Subagent fan-out keeps batching in parallel; a call for which this returns true is excluded from the concurrent read batch (dispatch-serial, flushed alone via runOne) so the writable child's IN-PLACE Edit/Write/Shell against the real tree never overlaps a sibling parent Read/Grep/Glob — a torn read. This is the LOAD-BEARING correctness fix for direct-write: a mode:"read-write" child mutates the real workspace DURING its run (no fork, no merge), so the dispatcher MUST keep it mutate-serial — independent of any merger (there is none). It returns true ONLY for a call that will ACTUALLY run writable: mode:"read-write" with the writable child engine wired (the writable explorer) OR the agent writable factory wired (a writable named specialist mutates the real tree too); the two are OR'd so a deployment wiring either form keeps its writable calls dispatch-serial. A malformed/unparseable args payload returns false (the call errors later anyway, and never writes).
func (*SubagentTool) ReadOnly ¶
func (*SubagentTool) ReadOnly() bool
ReadOnly reports that the Subagent tool is read-only, which lets the parent's dispatcher run Subagent CONCURRENTLY with other read-only tools that share the same Workspace (read-parallel / mutate-serial; see dispatch.go).
INVARIANT — what keeps this safe is WORKSPACE ISOLATION, not catalog read-only-ness. A Subagent child may now WRITE via Shell (the read-only explorer's shell — git, build, test, cat), but when a child forker is wired (childForker != nil — the composition root wires it iff the child catalog has Shell) the child runs in an ISOLATED git WORKTREE, so its writes land in a throwaway checkout and NEVER touch the shared parent workspace the parent's other read-only calls race over. The read-parallel guarantee therefore holds exactly as before: no two concurrent dispatched tools ever mutate the same tree.
The only surface a worktree child shares with the parent is the `.git` object DB/refs (git-locked for concurrent access; config-driven code-execution vectors — hooks/pager/fsmonitor/external-diff — neutralized by the sandboxed runner's gitenv env in the composition layer), and a detached-HEAD worktree's stray commit is dangling and gc-able. A fork FAILURE is surfaced as a tool error, never a silent fallback to the shared ws (which WOULD break this), so the invariant cannot be violated by a degraded fork.
When NO forker is wired the child has no Shell (the catalog stays a pure read-only explorer) and runs against the shared ws — also safe, by catalog read-only-ness, exactly as it always was. Either way Subagent is read-parallel-safe and ReadOnly() honestly returns true.
ReadOnly() stays true for read-only fan-out; a mode:"read-write" CALL mutates the parent workspace IN PLACE during the run (direct-write — no fork, no merge), so it is excluded from the concurrent read batch via MutatesParent (dispatch-serial, run alone — see parentMutatingCaller) so its in-place edits never overlap a sibling parent read.
func (*SubagentTool) Spec ¶
func (t *SubagentTool) Spec() tool.ToolSpec
Spec returns the model-facing specification for the Subagent tool. When named agent definitions are configured, their names+descriptions are appended to the description (progressive disclosure, like the Skill tool enumerates skills) so the model can choose a specialist via the optional `agent` arg.
type Supervisor ¶
type Supervisor struct {
// contains filtered or unexported fields
}
Supervisor orchestrates one agent team. Build it with NewSupervisor, enrol members with AddMember (before Run), then call Run.
func NewSupervisor ¶
func NewSupervisor(t *team.Team, base tool.Environment, factory MemberEngine, opts ...SupervisorOption) *Supervisor
NewSupervisor constructs a team supervisor over a shared team, a base workspace, and a per-member engine factory. team, base, and factory must be non-nil; NewSupervisor panics otherwise (a composition-root programming error).
func (*Supervisor) AddMember ¶
func (s *Supervisor) AddMember(ctx context.Context, spec MemberSpec) error
AddMember enrols a member: it registers it on the team roster, builds its engine, selects its workspace per the three-tier policy (the shared base for a base-sharing read-only member; a worktree fork for a read-only-isolated member; a force-copy fork for a Mutating one), constructs its session, and records it. It must be called before Run. A Mutating member without s.forker, or a read-only-isolated member without s.roForker, is an error.
func (*Supervisor) CancelMember ¶
func (s *Supervisor) CancelMember(name string) bool
CancelMember requests cancellation of ONE member by name: it fires the member's per-member cancel, which unwinds a mid-drive turn (the drive ctx derives from the member ctx → the existing StopCancelled classification in runTurn de-schedules it, releasing its tasks) or, for a member idle between rounds, is caught by planRound's up-front ctx check before the next round plans it (D5: de-schedule, never skip-turn). It returns false for an unknown name; cancelling an already-stopped member is a harmless no-op (its ctx just goes unobserved). Safe from any goroutine: members/ cancel are immutable after AddMember (which must precede Run).
It is the seam BOTH cancel paths share: the Converse-path Team tool reaches members through the parent registry's CancelChild (whose registered cancel IS this member cancel), and the RunTeam-path `CancelTeammate` unary (D4, issue #29 — server.Service.CancelTeammate) calls it directly.
func (*Supervisor) Close ¶ added in v0.16.0
func (s *Supervisor) Close()
Close cancels an active run and waits until every enrolled member has stopped and its resources have been torn down. Event callbacks may still be draining; Run waits for every ordered callback before it returns. Close is safe to call repeatedly, concurrently, and from an event callback.
Member factories and cleanup callbacks must not synchronously call Close: they are part of the resource construction/destruction that Close joins, so recursive teardown cannot complete.
A supervisor is single-use: AddMember after Close or after Run starts returns ErrSupervisorClosed, and a second Run returns an empty outcome.
func (*Supervisor) MemberModel ¶
func (s *Supervisor) MemberModel(name string) string
MemberModel returns the concrete MODEL id the named member's engine actually runs on ("" for an unknown member). It is the read-only seam the Team tool uses to surface the member's resolved model on the EvTeamStart roster — independent of how it was chosen (inherited default member model, agent-def pin, or the opt-in router). Captured at AddMember (the engine is built once and reused). Bare metadata, never member content. When the router classified the member, MemberModel == the routed model. See issue #112.
func (*Supervisor) MemberRouting ¶
func (s *Supervisor) MemberRouting(name string) (category, model, reason string)
MemberRouting returns the OPT-IN model router's bare-metadata classification (category, model id) for a member by name (both empty when the member was not routed or is unknown), plus the bare-metadata REASON it was not routed (empty on a routed hit — issue #397). It is the read-back seam the Team tool uses to project routed metadata onto the EvTeamStart roster — captured once at AddMember, never member content. Safe to call after AddMember (the fields are immutable once set).
func (*Supervisor) Run ¶
func (s *Supervisor) Run(ctx context.Context, sink func(TeamEvent)) TeamOutcome
Run drives the team to quiescence (or the round cap), invoking sink for every member event as it is produced, and returns the outcome. sink may be nil. The run is bounded by ctx: cancelling it stops scheduling further rounds and lets the in-flight round finish. Forked member workspaces are cleaned up on return.
type SupervisorOption ¶
type SupervisorOption func(*Supervisor)
SupervisorOption configures a Supervisor.
func WithForker ¶
func WithForker(f tool.EnvironmentForker) SupervisorOption
WithForker injects the workspace-isolation seam used to fork a Mutating member's workspace (force-copy: own `.git`). It is required only if any member is Mutating.
func WithMaxRounds ¶
func WithMaxRounds(n int) SupervisorOption
WithMaxRounds caps the number of scheduling rounds (default defaultMaxRounds). A non-positive value is ignored.
func WithMemberErrorRetries ¶
func WithMemberErrorRetries(n int) SupervisorOption
WithMemberErrorRetries sets how many times a member whose round ended in session.StopError — and whose session the supervisor then RECOVERED successfully — is left SCHEDULABLE for a later round instead of being benched (default defaultMemberErrorRetries = 1; issue #318). A retried member releases its in-progress task claim (so it, or a peer, can re-claim the work) and is force-scheduled for exactly one turn even when it holds no message and no claimable task. Once its errored-round count EXCEEDS this cap it is benched exactly as before this: stopped, MemberStopReason StopReasonError, tasks released, registry entry closed. 0 disables retry entirely (the previous release's behaviour); a negative value is ignored.
A member whose RECOVERY itself failed (memberRT.nonResumable) is NEVER retried regardless of this cap — its session cannot be driven at all — and neither is a cancelled or turn-budget-exhausted member (those are not transient failures).
func WithMemberLiveness ¶
func WithMemberLiveness(liveness port.SessionLiveness) SupervisorOption
WithMemberLiveness injects the maintenance exclusion used for team-member sessions. Run acquires it for every member before scheduling begins and cleanupAll releases each hold after the between-round and synthesis lifecycle has ended. Acquisition failure safely skips that member. This capability is independent of parent permission/ask capabilities, so a direct Service RunTeam can remain zero-capability.
func WithMemberSessionPrefix ¶
func WithMemberSessionPrefix(p string) SupervisorOption
WithMemberSessionPrefix sets the prefix used to derive member session ids (default "team"). Ids are of the form "<prefix>-<member>".
func WithMemberStore ¶
func WithMemberStore(store port.SessionStore) SupervisorOption
WithMemberStore injects the optional session store the supervisor uses to persist each member session for out-of-band inspection. Nil disables persistence. The supervisor consumes the port.SessionStore interface, never a concrete adapter, so no layering rule is crossed.
func WithMemberTurnBudget ¶
func WithMemberTurnBudget(n int) SupervisorOption
WithMemberTurnBudget sets the cumulative LIFETIME turn cap each member may spend across all rounds (default defaultMemberTurnBudget). Unlike WithTeamLimits — whose counters session.Reopen resets every round — this budget accumulates across rounds and is the only ceiling on a member's total turn spend. A member that exhausts it is stopped (its in-progress tasks released) exactly like a failed member, so a member that never finishes its work cannot loop to the round cap unbounded. A non-positive value disables the budget (n == 0 means "no lifetime cap").
func WithReadOnlyForker ¶
func WithReadOnlyForker(f tool.EnvironmentForker) SupervisorOption
WithReadOnlyForker injects the workspace-isolation seam used to fork a read-only member that the factory granted a shell (MemberBuild.IsolateReadOnly). It should be the forker's DEFAULT mode (git worktree: shares the base repo's `.git`), so an inspect-only member gets full history cheaply. It is required only if the factory marks any read-only member IsolateReadOnly; without it such a member trips ErrReadOnlyShellNoForker.
func WithRootAuthority ¶
func WithRootAuthority(authority session.Authority) SupervisorOption
WithRootAuthority supplies the composed root set for members of a directly server-created team. It is ignored for a team created from a parent run; child derivation remains outside this option.
func WithTeamConcurrency ¶
func WithTeamConcurrency(n int) SupervisorOption
WithTeamConcurrency bounds how many member turns run simultaneously within a scheduling round (default defaultTeamConcurrency). A non-positive value is ignored.
func WithTeamGoal ¶
func WithTeamGoal(goal string) SupervisorOption
WithTeamGoal sets the team's top-level objective. It is rendered as the team's TRUSTED top-level instruction into every member's round-0 turn and into the lead's synthesis prompt (the goal IS the member's genuine job; its provenance is the principal, never a peer). It is still governance.NeutraliseFraming'd on render so it cannot forge a fence/header. Use WithUntrustedGoal(true) to re-fence it as UNTRUSTED data when a deployment may interpolate untrusted end-user text into the goal. Empty is legal (the gRPC default before the goal field is supplied).
func WithTeamHooks ¶
func WithTeamHooks(h port.HookRunner) SupervisorOption
WithTeamHooks injects the HookRunner that fires the TeammateIdle lifecycle hook when a member goes idle after a turn (best-effort; nil disables it). It is the same runner the composition root should pass to MemberTools for the TaskCreated / TaskCompleted gates, so a team's lifecycle hooks all flow through one runner.
func WithTeamLimits ¶
func WithTeamLimits(l session.Limits) SupervisorOption
WithTeamLimits overrides the per-member, per-round stop conditions (default defaultChildLimits). Reopen resets these counters each round, so they bound one turn-loop, not the member's whole life.
func WithTeamMode ¶
func WithTeamMode(m session.PermissionMode) SupervisorOption
WithTeamMode sets the permission mode each member session runs under (default session.ModeDefault).
func WithTeamOwner ¶
func WithTeamOwner(owner *session.Principal) SupervisorOption
WithTeamOwner attributes members of a directly server-created team to the verified caller that created it. It is ignored for a team created from a parent run, where caps.owner carries the authoritative inheritance.
It supplies the OWNER ONLY. The gRPC path deliberately runs with zero parent caps — no ask surfacing and no child-ask adjudicator — and that posture is preserved: this option must never become a general caps channel.
func WithTeamReadLedgerFactory ¶ added in v0.14.0
func WithTeamReadLedgerFactory(factory func() tool.ReadLedger) SupervisorOption
WithTeamReadLedgerFactory injects the mandatory fresh member-ledger factory.
func WithTeamSharedBaseWorkspace ¶
func WithTeamSharedBaseWorkspace(view func(tool.Workspace) tool.Workspace) SupervisorOption
WithTeamSharedBaseWorkspace injects a capability-narrowing view for base-sharing members. It receives the actual base Workspace and must preserve its content backend rather than reconstructing storage from Root.
func WithTeamTokenBudget ¶
func WithTeamTokenBudget(n int) SupervisorOption
WithTeamTokenBudget sets the TEAM-WIDE cumulative token budget (input+output, session.Usage.TotalTokens) summed across ALL members and ALL rounds, including the lead's synthesis turn in the final accounting. It is checked at the ROUND boundary only (before planRound): the in-flight round always completes, so the overshoot is bounded by concurrency × one round's per-member spend (each drive itself bounded by per-round Limits and any Deps.MaxRunTokens). When it trips the TEAM stops scheduling — members are NOT individually stopped (no new MemberStopReason) and the lead's synthesis turn still runs (the report is the deliverable). It is ORTHOGONAL to the per-engine Deps.MaxRunTokens ceiling, which bounds one member drive and resets on Reopen each round. 0 (the default) disables it; a negative value is ignored.
func WithUntrustedGoal ¶
func WithUntrustedGoal(untrusted bool) SupervisorOption
WithUntrustedGoal marks the team goal as UNTRUSTED, so it is fenced as data rather than rendered as the team's trusted instruction. Use it ONLY when the goal may contain untrusted end-user text (a relay / multi-tenant front door). Default (no option) = trusted. The trust DECISION is a composition concern (where provenance is known); the supervisor is pure mechanism and only takes the bool.
type TeamEvent ¶
type TeamEvent struct {
// Member is the name of the member whose session produced Event.
Member string
// MemberSessionID is the producing member's child SESSION id (MemberSessionID:
// "team-<teamID>-<member>") — the uniform cancel/inspect handle, forwarded onto
// the team.member projection so a client can address the member (CancelChild)
// without deriving the id grammar. The same for every event of a given member.
MemberSessionID string
// MemberIncarnation binds internal durable event correlation to this member lifetime.
MemberIncarnation session.IncarnationID
// Event is the underlying session Event (turn.start, tool.call, result, ...).
Event session.Event
// ContextWindow is the producing member engine's context window in tokens
// (Engine.ContextWindow), the denominator for the per-member context meter. It
// is the same for every event of a given member; projectTeamEvent forwards it
// onto the turn.end projection (0 when the member's engine has no window set).
ContextWindow int
}
TeamEvent tags a member session Event with the member that produced it, for the multiplexed team event stream the caller observes.
type TeamMemberArg ¶
type TeamMemberArg struct {
// Name is the member's unique handle peers address messages to.
Name string `json:"name"`
// Role is the member's role briefing — its first-turn instruction. It becomes
// the member's InitialPrompt.
Role string `json:"role"`
// Mutating requests a self-contained copied workspace (own `.git`) with
// edit/write/shell tools (Edit/Write/Shell). A read-only member (the default,
// false) runs in an isolated throwaway git worktree with full shell for
// INSPECTION (git log/show, cat, build, test) but no Edit/Write. Neither tier is
// merged back into the base.
Mutating bool `json:"mutating,omitempty"`
// Agent optionally names a configured specialist definition.
Agent string `json:"agent,omitempty"`
// Provider and Model form this member's optional explicit selector.
Provider string `json:"provider,omitempty"`
Model string `json:"model,omitempty"`
}
TeamMemberArg is one roster entry the model supplies in a Team call. It maps 1:1 onto MemberSpec: Name → Name, Role → InitialPrompt, Mutating → Mutating.
type TeamMemberEngineFactory ¶
type TeamMemberEngineFactory func(t *team.Team, spec MemberSpec, routedModel string) MemberBuild
TeamMemberEngineFactory builds a team member's engine (as a MemberBuild carrying the constructed *Engine plus the member's optional per-member permission mode) from the shared team and the member spec. It is the SINGLE canonical factory shape consumed by every team entry point — the gRPC CreateTeam path (server.Config.MemberEngine) AND the Team tool — so the per-member engine wiring lives in one place (internal/app) and cannot drift between the two paths. The composition root supplies it; it is expected to capture nothing (the team is passed per-call) and to shape the member's catalog per the three-tier workspace policy (a base-sharing read-only member must NOT be handed workspace-mutating tools; a read-only-isolated member may have Shell and sets IsolateReadOnly; a Mutating member may have Edit/Write/Shell) plus the team coordination tools (MemberTools). Returning a MemberBuild (rather than a bare *Engine) is how a member's agent-definition permissionMode reaches the supervisor's per-member session — it is the exact shape server.MemberEngineFactory has, so one factory serves both paths.
routedModel is the OPT-IN model router's concrete model id for an UNDEFINED member, "" otherwise. When a selector is resolved (explicitly or by routing), spec.Selector carries the full provider/model target and its evidence to the same factory. A defined member without an explicit selector uses its own def.
type TeamOption ¶
type TeamOption func(*TeamTool)
TeamOption configures a TeamTool.
func WithTeamSelectorResolver ¶ added in v0.17.0
func WithTeamSelectorResolver(r SubagentSelectorResolver) TeamOption
WithTeamSelectorResolver injects composition-owned provider/model resolution.
func WithTeamToolForker ¶
func WithTeamToolForker(f tool.EnvironmentForker) TeamOption
WithTeamToolForker injects the workspace forker used to isolate a Mutating member's workspace (force-copy). It is required only if the model forms a Mutating roster.
func WithTeamToolHooks ¶
func WithTeamToolHooks(h port.HookRunner) TeamOption
WithTeamToolHooks injects the HookRunner threaded into the Supervisor (and, by the composition root, the member coordination tools) so a team's lifecycle hooks flow through one runner. nil disables them.
func WithTeamToolReadLedgerFactory ¶ added in v0.14.0
func WithTeamToolReadLedgerFactory(factory func() tool.ReadLedger) TeamOption
WithTeamToolReadLedgerFactory injects the fresh ledger factory for members.
func WithTeamToolReadOnlyForker ¶
func WithTeamToolReadOnlyForker(f tool.EnvironmentForker) TeamOption
WithTeamToolReadOnlyForker injects the workspace forker (the worktree-default mode) used to isolate a read-only member that the factory granted a shell. It is required only if the factory marks a read-only member IsolateReadOnly.
func WithTeamToolSharedBaseWorkspace ¶
func WithTeamToolSharedBaseWorkspace(view func(tool.Workspace) tool.Workspace) TeamOption
WithTeamToolSharedBaseWorkspace injects the capability-narrowing Workspace view the Team tool forwards to its Supervisor for base-sharing members.
func WithTeamToolStore ¶
func WithTeamToolStore(s port.SessionStore) TeamOption
WithTeamToolStore injects the session store the Team tool threads into the supervisor (WithMemberStore) to persist member sessions for out-of-band inspection. nil disables persistence. The Team tool consumes the port.SessionStore interface, never a concrete adapter (layering holds).
func WithTeamToolTokenBudget ¶
func WithTeamToolTokenBudget(n int) TeamOption
WithTeamToolTokenBudget sets the operator-configured team-wide token budget the tool threads into the supervisor (WithTeamTokenBudget). A per-call max_team_tokens arg may only TIGHTEN it (tightenLimit semantics). 0 disables.
type TeamOutcome ¶
type TeamOutcome struct {
// Rounds is the number of scheduling rounds that ran work.
Rounds int
// Quiescent reports whether the team reached genuine completion (all tasks
// done, mailboxes empty, no member still working) versus stopping because a
// round planned no work while tasks remained (a stuck dependency / deadlock).
Quiescent bool
// Members holds each member's terminal summary in enrolment order.
Members []MemberOutcome
// Report is the LEAD's consolidated synthesis — the team's deliverable, produced
// by a final synthesis turn in Run after the scheduling loop. It is the value the
// Team tool returns as its ToolResult. It is empty when synthesis could not run
// (no lead, lead stopped, or the lead produced no text); the caller then renders
// the degraded joinTeamFallback concatenation instead.
Report string
// Findings is the team's findings ledger at the end of the Run, snapshotted in
// append order — the PRIMARY deterministic, member-authored data the degraded
// deliverable leads with. It is the same data buildSynthesisSources reads
// (s.team.Findings()), captured onto the outcome so BOTH the Team-tool deliverable
// path and the gRPC RunTeam consumer get the rich fallback without reaching into
// the live *team.Team. Bodies are clamped identically to the wire/observability
// projection (projectTeamFindingsSnapshot).
Findings []session.TeamFindingSnapshot
// BudgetExhausted reports that the team-wide token budget (WithTeamTokenBudget)
// crossed at a round boundary and no further round was scheduled — the in-flight
// round and the lead's synthesis still completed. false when no budget was set or it
// never crossed.
BudgetExhausted bool
// Usage is the supervisor-accumulated team total — Σ per-drive EvResult.Usage across
// all members and rounds, synthesis included; equal by construction to the TeamTool
// sink's turn.end sum, which remains authoritative for the EvTeamEnd payload.
Usage session.Usage
}
TeamOutcome is the result of a team Run.
type TeamTool ¶
type TeamTool struct {
// contains filtered or unexported fields
}
TeamTool forms a team of coordinating subagents in-process (see teamsupervisor.go). When executed it builds a fresh team.Team, enrols the model-supplied roster (synthesizing the first member as the lead), drives the existing Supervisor to quiescence over the SAME base workspace as the parent, and returns the team's deliverable as a single ToolResult. The deliverable resolves through the three-tier deliverable() chain: (1) the lead's consolidated synthesis when it is a usable report (non-empty AND not a non-deliverable — isNonDeliverable rejects a bare refusal over a populated ledger), (2) a ledger-rich structured fallback (findings grouped by member, per-member disposition/reason/completed-tasks/last-text), (3) an honest floor when even the ledger is empty. A non-convergence header is prepended when the team did NOT converge. The ToolResult is therefore NEVER a bare refusal or empty.
It is the team analogue of SubagentTool/ParallelTool, with two deliberate differences:
- ReadOnly() == false. A team spawns Mutating members and is long-lived and stateful, so the dispatcher must SERIALISE it (mutate-serial) rather than run it read-parallel like Subagent. (Mutating members run in isolated forks, but the supervisor's member maps are not safe to drive alongside other tools.)
- Member activity is OBSERVABLE and fuller. Via the observableTool seam, each member event is projected to the parent run's stream as a team.member event carrying the member's message text and BOUNDED tool previews — a team is meant to be watched. permission.ask is dropped; every preview is capped.
Context isolation holds exactly as for Subagent/Fork: the per-member transcripts are never written to the parent Session's Conversation. Only the lead's synthesis (the ToolResult) folds back, so the LLM's context stays summary-only. (The PULL InspectMember tool may later pull ONE member's transcript on the parent's deliberate request — still not auto-injection.)
The composition root injects the member-engine factory, the workspace Forker, and the team hooks runner (mirroring Service.CreateTeam's wiring) so this tool drives the SAME Supervisor the gRPC team path drives.
func (*TeamTool) Execute ¶
func (t *TeamTool) Execute(ctx context.Context, call session.ToolCall, env tool.Environment) (session.ToolResult, error)
Execute runs a team with no observability (the emit == nil path): the team's member activity is not forwarded, only the lead's consolidated report is returned. Existing non-observing callers are unaffected by the observability seam.
func (*TeamTool) ExecuteObserved ¶
func (t *TeamTool) ExecuteObserved(ctx context.Context, call session.ToolCall, env tool.Environment, emit func(session.Event)) (session.ToolResult, error)
ExecuteObserved runs a team like Execute but, when emit is non-nil, forwards a BOUNDED projection of member activity to the parent run's stream via the three team.* events. emit only sequences and channels events; it never touches the parent's Conversation, so member content still never enters the parent context (only the lead's synthesis ToolResult does). It is the observableTool seam the dispatcher calls.
func (*TeamTool) ExecuteWithParent ¶
func (t *TeamTool) ExecuteWithParent(ctx context.Context, call session.ToolCall, env tool.Environment, emit func(session.Event), caps parentCaps) (session.ToolResult, error)
ExecuteWithParent is the childCapableTool seam: it runs the team like ExecuteObserved but threads the PARENT's capabilities (interactivity + surface back-channel) into the supervisor, so a member's permission ask that A2 (isolation auto-approve) did not resolve is SURFACED to the human (interactive parent) or auto-denied with the accurate message + operator diagnostic (headless). The in-loop Team tool's goal stays trusted; only the ask resolution posture changes.
func (*TeamTool) ReadOnly ¶
ReadOnly reports that the Team tool is NOT read-only, so the parent dispatcher SERIALISES it (mutate-serial) — it never runs concurrently with another tool. A team is long-lived, stateful, and may spawn Mutating members; its Supervisor drives unsynchronised member state, so it must not race the parent's other tool calls. This is the deliberate opposite of SubagentTool/ParallelTool, which are read-parallel.
type TokenCounter ¶
type TokenCounter interface {
// Count returns the estimated token count of a single string.
Count(text string) int
// CountMessages returns the estimated token count of a conversation slice,
// summing each message's text/reasoning/tool bodies plus the per-message and
// per-role framing overhead the provider adds on the wire.
CountMessages(msgs []session.Message) int
}
TokenCounter estimates how many model tokens a piece of text or a slice of conversation messages occupies. It is a seam (ARCHITECTURE.md §8, gauntlet #12): the loop and the compaction cascade consume it to decide when to compact and which segments to drop, while the concrete tokenizer (a dependency-free heuristic by default, an optional tiktoken-backed adapter in production) is injected from the composition root.
Implementations MUST be deterministic and SHOULD be cheap: Count/CountMessages run on every turn. A counter is never required to be exact — the loop only needs the estimate to be in the right ballpark — but it must be stable, since an unstable estimate would make compaction non-reproducible.
type ToolReviewRequest ¶ added in v0.16.0
type ToolReviewRequest struct {
ReviewID string
Job ReviewJob
Event governance.HookEvent
EffectiveCall session.ToolCall
PrincipalFacts []ReviewPrincipalFact
PrincipalFactsComplete bool
Caller ReviewCaller
Environment session.EnvironmentRef
Target ReviewTarget
Evidence []ReviewEvidenceMeta
EvidenceComplete bool
Trajectory []ReviewTrajectoryFact
TrajectoryComplete bool
Capacity ReviewCapacity
}
ToolReviewRequest is the complete harness-owned contextual review input.
type ToolReviewResult ¶ added in v0.16.0
type ToolReviewResult struct {
Assessment ReviewAssessment
Concerns []ReviewConcern
Evidence []ReviewEvidenceUse
Missing []ReviewMissingEvidence
}
ToolReviewResult is one completed contextual assessment. Its zero value is unresolved: a nil error never makes an empty Assessment acceptable.
type ToolReviewer ¶ added in v0.16.0
type ToolReviewer interface {
Review(context.Context, ToolReviewRequest, ReviewEvidenceSource) (ToolReviewResult, session.AuxiliaryUsage, error)
}
ToolReviewer performs one contextual tool review and returns all model usage incurred by the review, including partial usage returned with an error.
type UserModelReviewer ¶
type UserModelReviewer struct {
// contains filtered or unexported fields
}
UserModelReviewer extracts durable operator facts from completed trajectories. Observe consumes the owned trajectory directly and requires only the injected child engine. The legacy Review method additionally requires a SessionStore so it can load history by session ID.
func NewUserModelObserver ¶
func NewUserModelObserver(engine *Engine) *UserModelReviewer
NewUserModelObserver constructs the standard trajectory observer. It needs no SessionStore because learning.Trajectory already owns the completed history.
func NewUserModelReviewer ¶
func NewUserModelReviewer(store port.SessionStore, engine *Engine) *UserModelReviewer
NewUserModelReviewer constructs the legacy ID-based reviewer. Its SessionStore is used only by Review; automatic trajectory observation should use NewUserModelObserver instead.
func (*UserModelReviewer) Observe ¶
func (r *UserModelReviewer) Observe(ctx context.Context, tr learning.Trajectory) error
Observe implements learning.Observer from an owned completed-trajectory snapshot. Unlike the legacy Review method it needs no SessionStore reload and therefore observes exactly the history that completed.
func (*UserModelReviewer) Review ¶
func (r *UserModelReviewer) Review(ctx context.Context, sessionID string) error
Review loads the transcript of the just-finished session sessionID, runs the extraction child loop, and discards everything but completion (the child's RememberUser calls land in the user-model store as a side effect). Review returns an error when this reviewer was constructed by NewUserModelObserver, because the legacy ID-based path has no SessionStore in that configuration. It is otherwise best-effort: an empty/unreadable transcript, or a child that writes nothing, is a clean no-op. It returns an error only for genuinely surprising faults (e.g. a store load error) so a caller logging it has something to log; a fail-soft caller may ignore it.
It runs against a workspace-less child (the extraction needs no filesystem — the only tool is RememberUser, which ignores its Workspace). ctx bounds the run.
Source Files
¶
- actionreview.go
- askadjudicator.go
- authority_delegation.go
- authority_disclosure.go
- auxiliary_usage.go
- cascade.go
- child_environment.go
- childregistry.go
- compaction.go
- dispatch.go
- evidencereflector.go
- fence.go
- forkjudge.go
- forkreaper.go
- guardrailcheck.go
- hooks.go
- inboundreview.go
- loop.go
- manual_compaction.go
- modelrouter.go
- parallel.go
- permission.go
- presentplan.go
- request_manifest.go
- review.go
- scheduletool.go
- sessionorigin.go
- shellstatus.go
- shelltool.go
- steer.go
- structuredoutput.go
- subagent.go
- subagentinspect.go
- subagentstatus.go
- tailbuffer.go
- teaminspect.go
- teamsupervisor.go
- teamtool.go
- teamtools.go
- tokencount.go
- usermodelreview.go