Documentation
¶
Overview ¶
Package runner orchestrates one agent session end-to-end.
It is the keystone of v0.5.0 — F.2.8 (daemon wire-up) calls Runner.Run for every claimed QueuedWork and the function does not return until the session is fully terminated (a result has been posted, the worktree torn down, and the heartbeat stopped).
The package wires the Wave 2/2b building blocks into the per-session main loop:
┌───────────────────────────────────────────────────────────────┐ │ runner.Run(ctx, qw) │ │ │ │ 1. Translate QueuedWork → agent.Spec (spec_translation.go) │ │ 2. Resolve Provider via Registry (registry.go) │ │ 3. Provision worktree (runtime/worktree) │ │ 4. Compose env (blocklist applied) (runtime/env) │ │ 5. Build MCP stdio config tmpfile (runtime/mcp) │ │ 6. Render system+user prompt (prompt) │ │ 7. Spawn provider (provider/...) │ │ 8. Start heartbeat pulser (runtime/heartbeat) │ │ 9. Stream events → state.json + post (runtime/state) │ │ 10. Wait for terminal event (or cancel) │ │ 11. Tail recovery: steering → backstop (steering/backstop) │ │ 12. Post result (result.Poster) │ │ 13. Teardown │ └───────────────────────────────────────────────────────────────┘
The package exports a small, public surface so rensei-tui can embed the runner without depending on private daemon plumbing:
- Runner — long-lived per-daemon orchestrator built once via New, then Runner.Run called per session.
- QueuedWork — input contract; mirrors the platform Redis payload plus the resolved-profile knob.
- Result — terminal output; identical to agent.Result today but kept distinct for forward compatibility (so future runner wave hooks can extend it without touching the agent package).
- Registry — provider resolution; configurable so test code can swap stubs in.
Failure modes follow F.1.1 §5 verbatim:
- Worktree provisioning retries 3 times with 15s delay. Lost ownership during retry short-circuits the run.
- Heartbeat 3-strike trips runtime/heartbeat.ErrLostOwnership; the runner cancels the provider via Handle.Stop and records FailureMode "lost-ownership".
- Provider Spawn errors map to FailureMode "spawn-failed".
- ErrorEvents on the stream before any terminal ResultEvent map to FailureMode "provider-error".
- Steering failure falls through to backstop; backstop failure records its diagnostics on Result.BackstopReport.
Two-stage tail recovery on agent completion (F.0.1 §1):
- Stage 1 (steering): when the provider supports message injection or session resume and the session ended without a PR, the runner sends a per-provider templated steering prompt asking the agent to commit/push/PR.
- Stage 2 (backstop): when steering did not produce a PR (or the provider does not support steering), the runner runs a deterministic git workflow (`git add -A` with the path-exclude list, `git commit`, `git push`, `gh pr create`).
The path-exclude list in [backstop.go] is ported verbatim from the legacy TS at agentfactory/packages/core/src/orchestrator/session-backstop.ts:57-95.
Telemetry ¶
Every Event is mirrored three ways:
- Appended to <worktree>/.agent/events.jsonl (audit trail).
- State-store snapshot updated for crash recovery.
- Logger.Debug for operator dashboards.
Posting per-event activity to the platform is left to F.5; today's runner only posts the terminal Result (via result.Poster.Post).
Package runner provider_view.go — adapter that exposes the in-process AgentRuntime registry as the read-only daemon.ProviderRegistry view consumed by the /api/daemon/providers* HTTP handler. Wave 9 / A1.
The adapter lives in the runner package (not daemon) so daemon stays free of a runner import — daemon.ProviderRegistry is the interface, runner.NewProviderView builds the concrete view from a *Registry.
Index ¶
- Constants
- Variables
- func IsBudgetExceeded(err error) bool
- func RequiresWorkResult(workType string) bool
- type BudgetCap
- type BudgetEnforcer
- func (e *BudgetEnforcer) CheckDuration(now time.Time) *BudgetExceededError
- func (e *BudgetEnforcer) Enabled() bool
- func (e *BudgetEnforcer) ObserveEvent(ev agent.Event) *BudgetExceededError
- func (e *BudgetEnforcer) Report(now time.Time) *BudgetReport
- func (e *BudgetEnforcer) WithDurationCap(parent context.Context) (context.Context, context.CancelFunc)
- type BudgetExceededError
- type BudgetReport
- type CompletionContract
- type CompletionField
- type CompletionFieldType
- type CredentialProvider
- type KitDetector
- type LinearStatusTransition
- type MCPConfigPath
- type Options
- type PostSessionDecision
- type ProviderNotRegisteredError
- type ProviderView
- type QueuedWork
- type Registry
- func (r *Registry) Names() []agent.ProviderName
- func (r *Registry) Register(p agent.Provider) error
- func (r *Registry) Resolve(name agent.ProviderName) (agent.Provider, error)
- func (r *Registry) SelectProvider(profile ResolvedModelProfile) (agent.Provider, error)
- func (r *Registry) Shutdown(ctx context.Context) error
- type ResolvedModelProfile
- type ResolvedProfile
- type Result
- type Runner
- type RuntimeCredentials
- type SpecInputs
Constants ¶
const ( // FailureWorktreeProvision indicates the worktree manager could // not provision a worktree after MaxSpawnRetries attempts. Often // preceded by a transient git-conflict error or a lost-ownership // short-circuit (see FailureLostOwnership). FailureWorktreeProvision = "worktree-provision" // FailurePromptRender indicates the prompt builder rejected the // QueuedWork (typically because the caller passed empty issue // context). Permanent — retrying without changing the input // will fail the same way. FailurePromptRender = "prompt-render" // FailureProviderResolve indicates the runner could not resolve // the requested provider name in its registry. Permanent. Often // indicates a misconfigured ResolvedProfile.Provider. FailureProviderResolve = "provider-resolve" // FailureSpawn indicates Provider.Spawn returned an error before // the events channel opened (e.g. CLI binary missing, app-server // unreachable). Wraps agent.ErrSpawnFailed. FailureSpawn = "spawn-failed" // FailureProviderError indicates the provider emitted an // ErrorEvent before any terminal ResultEvent. The error message // surfaces via Result.Error. FailureProviderError = "provider-error" // FailureSilentExit indicates the provider closed the events // channel without emitting either a ResultEvent or an ErrorEvent. // The runner synthesizes a failure record for these cases. FailureSilentExit = "silent-exit" // FailureLostOwnership indicates the per-session heartbeat tripped // its 3-strike threshold mid-session (or the worktree manager // detected ownership loss between retries). The runner cancels // the provider via Handle.Stop and tears down without backstop. FailureLostOwnership = "lost-ownership" // FailureTimeout indicates ctx was cancelled before the session // terminated. Surfaces when the daemon's per-session deadline // expires. FailureTimeout = "timeout" // FailureBackstop indicates the deterministic git backstop ran // but failed to push or open a PR; diagnostics live on // Result.BackstopReport.Diagnostics. FailureBackstop = "backstop-failed" // FailureKitProvision indicates a kit toolchain-install command or a // post_acquire hook exited non-zero before the agent spawned (Seam 2: // "no partial toolchain"). The session aborts; the agent never starts. // The failing command + exit code surface via Result.Error. FailureKitProvision = "kit-provision" // FailureAgentBlocked indicates the agent terminated normally but // DELIBERATELY declined to do the work — it judged the spec ambiguous, // the preconditions unmet, or the task outside its remit, and said so // instead of producing code. This is distinct from a crash // (FailureProviderError), a silent exit (FailureSilentExit), or a // budget/timeout cut-off: the agent made a reasoned decision to stop. // // The signal is structural — the agent emits an explicit decline // marker ("WORK_RESULT:blocked" or "AGENT_BLOCKED: <reason>") which // the runner scans for. A blocked result MUST NOT trigger the empty- // branch backstop or steering (there is nothing to recover), and the // platform side should surface it as a needs-clarification outcome // rather than re-dispatching the identical context — re-dispatch only // re-runs the agent into the same wall while re-billing the full // context-assembly prefix. FailureAgentBlocked = "agent-blocked" )
Failure-mode classification constants for [Result.FailureMode].
The values are stable wire strings so platform-side dashboards and Linear comments can dispatch off them without scraping log lines. Add new values at the bottom; never repurpose an existing one.
const ( // DefaultMaxSessionDuration is the upper-bound timeout the runner // applies to ctx when [Options.MaxSessionDuration] is zero. Two // hours matches the legacy TS MAX_SESSION_DURATION constant. DefaultMaxSessionDuration = 2 * time.Hour // DefaultEventBufferSize is the buffered-channel size used to // decouple event mirroring from the provider goroutine. A small // number keeps memory bounded; spikes block the provider for at // most one event before backpressure kicks in. DefaultEventBufferSize = 64 )
Default values for Options. Exposed for tests and operator debugging; production daemons override via Options.
const ( WorkTypeResearch = "research" WorkTypeBacklogCreation = "backlog-creation" WorkTypeBacklogGroomer = "backlog-groomer" WorkTypeDevelopmentStr = "development" WorkTypeInflight = "inflight" WorkTypeQAStr = "qa" WorkTypeAcceptance = "acceptance" WorkTypeRefinement = "refinement" WorkTypeRefinementCoordination = "refinement-coordination" WorkTypeMerge = "merge" WorkTypeSecurity = "security" WorkTypeImprovementLoop = "improvement-loop" WorkTypeOutcomeAuditor = "outcome-auditor" WorkTypeGAReadiness = "ga-readiness" WorkTypeDocumentationSteward = "documentation-steward" WorkTypeOperationalScannerVercel = "operational-scanner-vercel" WorkTypeOperationalScannerAudit = "operational-scanner-audit" WorkTypeOperationalScannerCI = "operational-scanner-ci" WorkTypeCoordination = "coordination" WorkTypeInflightCoordination = "inflight-coordination" )
Known agent work-type names. Verbatim mirror of AgentWorkType from agentfactory/packages/core/src/orchestrator/work-types.ts so any new work type the platform adds shows up here as a missing entry on `make test` (TestWorkTypeStatusMappings_Exhaustive).
Stable string constants; never repurpose an existing value.
const FailureBudgetExceeded = "budget-exceeded"
FailureBudgetExceeded classifies a session that hit a stage-budget cap (REN-1485 / REN-1487 Phase 2 acceptance criterion #4). The per-cap details live on Result.BudgetReport.
Variables ¶
var AllWorkTypes = []string{ WorkTypeResearch, WorkTypeBacklogCreation, WorkTypeBacklogGroomer, WorkTypeDevelopmentStr, WorkTypeInflight, WorkTypeQAStr, WorkTypeAcceptance, WorkTypeRefinement, WorkTypeRefinementCoordination, WorkTypeMerge, WorkTypeSecurity, WorkTypeImprovementLoop, WorkTypeOutcomeAuditor, WorkTypeGAReadiness, WorkTypeDocumentationSteward, WorkTypeOperationalScannerVercel, WorkTypeOperationalScannerAudit, WorkTypeOperationalScannerCI, WorkTypeCoordination, WorkTypeInflightCoordination, }
AllWorkTypes lists every recognised work type. Used by exhaustive mapping tests (every entry must appear in workTypeCompleteStatus and workTypeFailStatus, even when the value is empty). Order matches the declaration above; tests do not depend on order.
Functions ¶
func IsBudgetExceeded ¶
IsBudgetExceeded reports whether err wraps a *BudgetExceededError. Convenience helper for the runner's classification fork.
func RequiresWorkResult ¶
RequiresWorkResult reports whether the contract for this work type includes FieldWorkResult as a required field. Drives the diagnostic-comment branch in the post-session block when the agent exits without emitting a WORK_RESULT marker.
Types ¶
type BudgetCap ¶
type BudgetCap string
BudgetCap names which cap was breached. Surfaces in Result.BudgetReport.CapBreached so dashboards can group breaches by kind.
type BudgetEnforcer ¶
type BudgetEnforcer struct {
// contains filtered or unexported fields
}
BudgetEnforcer is a per-session counter + cap checker. Constructed once per Run with the session's StageBudget; mutated only via Track* methods (concurrency-safe via atomic counters + a sync.Mutex for the breach record).
Wall-clock enforcement is delegated to context.WithTimeout — the constructor returns a derived ctx that fires when MaxDurationSeconds elapses. Token + sub-agent enforcement is observation-driven: every agent.Event the runner streams flows through ObserveEvent, which returns a non-nil error when the cap is breached. The runner sees the error, classifies the failure as budget-exceeded, and stops the provider.
When the dispatch carries no StageBudget (legacy path) New returns a no-op Enforcer whose Track* methods always return nil — the runner can call them unconditionally.
func NewBudgetEnforcer ¶
func NewBudgetEnforcer(b *prompt.StageBudget, now time.Time) *BudgetEnforcer
NewBudgetEnforcer constructs an enforcer for the given budget. A nil budget produces a disabled enforcer (no caps, no enforcement) so callers can use one code path for legacy + stage dispatch.
func (*BudgetEnforcer) CheckDuration ¶
func (e *BudgetEnforcer) CheckDuration(now time.Time) *BudgetExceededError
CheckDuration returns a non-nil *BudgetExceededError when the wall-clock cap has been breached. The runner calls this from the post-loop classification path so a context-deadline-exceeded surfaces as the canonical budget breach reason instead of FailureTimeout. Returns nil when no cap is configured or the cap has not yet tripped.
func (*BudgetEnforcer) Enabled ¶
func (e *BudgetEnforcer) Enabled() bool
Enabled reports whether the enforcer has any caps to enforce.
func (*BudgetEnforcer) ObserveEvent ¶
func (e *BudgetEnforcer) ObserveEvent(ev agent.Event) *BudgetExceededError
ObserveEvent updates the running counters from an agent.Event and returns a non-nil *BudgetExceededError when the event tripped a cap. Callers should treat the error as a clean cancellation: stop the provider, classify the failure, write WORK_RESULT.
The enforcer continues to track counters after a breach — the caller may receive the same error again on subsequent events. This is intentional: the runner's first response is to cancel the stream context, but events buffered by the provider may still flow until the channel closes.
func (*BudgetEnforcer) Report ¶
func (e *BudgetEnforcer) Report(now time.Time) *BudgetReport
Report returns the per-session enforcement record. Safe to call at any point; values reflect the latest observations + breach state. Always returns a non-nil report — the runner attaches it to Result.BudgetReport unconditionally so dashboards can show "no budget enforced" sessions distinctly from "budget OK" ones.
func (*BudgetEnforcer) WithDurationCap ¶
func (e *BudgetEnforcer) WithDurationCap(parent context.Context) (context.Context, context.CancelFunc)
WithDurationCap returns a derived context that automatically cancels when the wall-clock budget elapses. Returns the input ctx unchanged when no duration cap is configured. The returned cancel is always non-nil and safe to defer.
type BudgetExceededError ¶
BudgetExceededError is returned by ObserveEvent / CheckDuration when a cap has been tripped. The runner classifies the failure as FailureBudgetExceeded and stops the provider cleanly.
func (*BudgetExceededError) Error ¶
func (e *BudgetExceededError) Error() string
Error satisfies the error interface.
type BudgetReport ¶
type BudgetReport struct {
// Enforced is true when the runner had a non-nil StageBudget to
// enforce. False when the session was dispatched with no budget
// (legacy path) — the report is then a no-op observation record.
Enforced bool `json:"enforced"`
// Limits captures the configured caps the runner was enforcing.
// All-zero means "no caps set, proceed unbounded."
Limits prompt.StageBudget `json:"limits"`
// ObservedSubAgents counts the Task tool invocations seen across
// the session.
ObservedSubAgents int `json:"observedSubAgents"`
// ObservedTokens is the cumulative input+output token count
// observed across all turns. Sourced from per-turn ResultEvent.Cost
// and the final terminal CostData.
ObservedTokens int64 `json:"observedTokens"`
// ObservedDurationSeconds is the wall-clock the session ran for at
// terminal time (or at the breach point).
ObservedDurationSeconds int `json:"observedDurationSeconds"`
// CapBreached names which cap tripped. Empty when the session
// completed within budget.
CapBreached BudgetCap `json:"capBreached,omitempty"`
// BreachDetail is the human-readable "<cap> exceeded: observed=X,
// limit=Y" string. Empty when no breach.
BreachDetail string `json:"breachDetail,omitempty"`
}
BudgetReport is the per-session enforcement report. Always present when the session was dispatched with a non-nil StageBudget; nil otherwise (legacy `agent.dispatch_to_queue` work has no budget to report on). Surfaced on Result.BudgetReport so the platform's WORK_RESULT consumer can render the breach reason without scraping log lines.
type CompletionContract ¶
type CompletionContract struct {
WorkType string
Required []CompletionField
Optional []CompletionField
}
CompletionContract enumerates the required + optional fields a session of the given work type must produce.
func GetCompletionContract ¶
func GetCompletionContract(workType string) (CompletionContract, bool)
GetCompletionContract returns the completion contract for a work type. Returns the contract and true on hit; returns the zero CompletionContract and false for unknown work types (caller treats as no contract — best-effort completion).
type CompletionField ¶
type CompletionField struct {
Type CompletionFieldType
Label string
BackstopCapable bool
}
CompletionField is a single required or optional field in a completion contract. BackstopCapable=true means the runner can fill the field deterministically post-session (e.g. push a branch). Fields that require agent judgement (work_result) are NOT backstop-capable.
type CompletionFieldType ¶
type CompletionFieldType string
CompletionFieldType is the discriminator for the field set every contract enumerates. Verbatim mirror of CompletionFieldType from the legacy TS file.
const ( FieldPRURL CompletionFieldType = "pr_url" FieldBranchPushed CompletionFieldType = "branch_pushed" FieldCommitsPresent CompletionFieldType = "commits_present" FieldWorkResult CompletionFieldType = "work_result" FieldIssueUpdated CompletionFieldType = "issue_updated" FieldCommentPosted CompletionFieldType = "comment_posted" FieldSubIssuesCreated CompletionFieldType = "sub_issues_created" FieldPRMerged CompletionFieldType = "pr_merged" FieldPRMergedOrEnqueued CompletionFieldType = "pr_merged_or_enqueued" )
Field-type constants. Stable wire strings — never repurpose an existing one.
type CredentialProvider ¶
type CredentialProvider func(context.Context) (RuntimeCredentials, error)
CredentialProvider returns the freshest worker runtime credentials available to the caller. Implementations should be cheap and concurrency-safe.
type KitDetector ¶ added in v0.10.0
type KitDetector func(repoRoot, targetOS string) ([]kit.ManifestView, error)
KitDetector resolves the ordered kit manifests that apply to a worktree at repoRoot for targetOS (foundation → framework → project). Returns an empty slice when no kit applies. Implemented by KitRegistry.DetectForRepo.
type LinearStatusTransition ¶
type LinearStatusTransition struct {
// WorkType is the agent work type the decision was made for.
WorkType string `json:"workType,omitempty"`
// WorkResult is the parsed marker driving the transition
// ("passed" | "failed" | "unknown" | "").
WorkResult string `json:"workResult,omitempty"`
// TargetStatus is the Linear workflow-state name the runner
// attempted to transition to. Empty when no transition was
// attempted.
TargetStatus string `json:"targetStatus,omitempty"`
// Attempted is true when the runner called UpdateIssueStatus.
Attempted bool `json:"attempted,omitempty"`
// Succeeded is true when UpdateIssueStatus returned nil.
Succeeded bool `json:"succeeded,omitempty"`
// Reason is a short identifier from PostSessionDecision.Reason
// ("passed", "failed", "unknown", "completed-non-sensitive",
// "deferred-merge-queue", "no-mapping", ...).
Reason string `json:"reason,omitempty"`
// Error is the human-readable error message when the transition
// failed. Empty on success.
Error string `json:"error,omitempty"`
// DiagnosticPosted is true when the runner posted the
// "missing WORK_RESULT" diagnostic comment to Linear (i.e. the
// Reason was "unknown" and the comment post succeeded).
DiagnosticPosted bool `json:"diagnosticPosted,omitempty"`
}
LinearStatusTransition records the runner's post-session attempt to transition the Linear issue's workflow state. Built from resolveTargetStatus and the UpdateIssueStatus call result.
type MCPConfigPath ¶
type MCPConfigPath struct {
Path string
Cleanup func()
}
MCPConfigPath wraps the runtime/mcp.Builder output so the loop has a single path-and-cleanup pair to thread through Spec construction + teardown. The cleanup closure is no-op when no MCP servers were requested, matching mcp.Builder.Build semantics.
type Options ¶
type Options struct {
// Registry is the provider registry the runner consults on each
// Run to resolve QueuedWork.ResolvedProfile.Provider. Required.
Registry *Registry
// WorktreeManager owns clone/teardown of per-session worktrees.
// Required.
WorktreeManager *worktree.Manager
// Poster posts the terminal Result back to the platform.
// Required.
Poster *result.Poster
// CredentialProvider supplies the freshest platform worker credentials
// for long-running child sessions. Heartbeats call it before every tick so
// they can pick up daemon-side runtime-token refreshes.
CredentialProvider CredentialProvider
// EnvComposer builds the agent subprocess env. Defaults to
// env.NewComposer().
EnvComposer *env.Composer
// MCPBuilder builds the per-session MCP stdio config tmpfile.
// Defaults to mcp.NewBuilder().
MCPBuilder *mcp.Builder
// StateStore writes .agent/state.json snapshots. Defaults to
// state.NewStore().
StateStore *state.Store
// PromptBuilder renders the (system, user) prompt pair.
// Defaults to &prompt.Builder{}.
PromptBuilder *prompt.Builder
// HTTPClient is forwarded to the heartbeat pulser for
// /api/sessions/<id>/lock-refresh calls. Defaults to a 30s-timeout
// http.Client.
HTTPClient *http.Client
// Logger receives Debug/Info/Warn lines describing each step.
// Defaults to slog.Default().
Logger *slog.Logger
// Now is injected for deterministic tests. Defaults to time.Now.
Now func() time.Time
// MaxSessionDuration is the per-Run upper-bound on ctx. Zero
// falls back to DefaultMaxSessionDuration. Negative disables the
// runner-side timeout (caller is responsible for ctx expiry).
MaxSessionDuration time.Duration
// PreserveWorktreeOnFailure keeps the worktree on disk after a
// failed Run for debugging. Defaults to true in v0.5.0 per F.1.1
// §10 Q7 — flip to false after smoke-harness confidence is high.
PreserveWorktreeOnFailure bool
// PreserveWorktreeAlways keeps the worktree on disk after every
// Run regardless of outcome. Used by tests that need to inspect
// .agent/events.jsonl or state.json post-Run, and by debug
// builds that want zero auto-cleanup. Defaults to false.
PreserveWorktreeAlways bool
// SkipBackstop disables the deterministic backstop entirely.
// Used by tests that don't have a real git worktree.
SkipBackstop bool
// SkipSteering disables the steering stage of tail recovery.
// Used by tests that need a deterministic recovery flow.
SkipSteering bool
// SkipPostSession disables the post-session Linear state-transition
// block (REN-1467 / loop.go step 11b). Tests that don't have a
// platform mock with /api/issue-tracker-proxy support, or that
// want to assert on the pre-transition Result envelope, set this
// to skip the block entirely. Production daemons leave it false.
SkipPostSession bool
// HeartbeatInterval overrides the per-session heartbeat cadence.
// Zero falls back to runtime/heartbeat.DefaultInterval.
HeartbeatInterval time.Duration
// KitSkillSources is the optional slice of Kit skill contributions to
// inject into each dispatched agent's system prompt and tool surface.
// Populated by the daemon at runner construction time from the active
// Kits in the KitRegistry (via internal/kit.LoadSkills). When nil or
// empty no Kit skill injection occurs — the runner behaves as before
// this feature was added (additive/cardinal rule 1 compliant).
//
// Each element describes one active Kit's skill file list (paths
// relative to the Kit's manifest directory) and priority for
// ordering the merged system-prompt append block.
KitSkillSources []kit.KitSkillSource
// KitDetector resolves the kit manifests that apply to a provisioned
// worktree, ordered foundation → framework → project. The daemon
// wires this to KitRegistry.DetectForRepo over its active kits; nil
// disables kit toolchain provisioning entirely (the runner behaves
// exactly as before K1). Computed inside the runner because the runner
// owns the worktree path (K1.4); mirrors how KitSkillSources is
// daemon-populated.
KitDetector KitDetector
// KitTargetOS overrides the OS the kit toolchain demand is composed
// for. Empty falls back to the host OS (kit.MustResolveOS). The daemon
// sets this to the SANDBOX OS ("linux") for cloud-targeted sessions so
// install scripts match the sandbox, not the dispatching host (OD-2).
KitTargetOS string
}
Options carries the long-lived configuration a Runner needs.
Required fields: Registry, WorktreeManager, Poster. The remaining fields have sensible defaults so simple consumers can call New(Options{Registry: r, WorktreeManager: m, Poster: p}) without having to plumb every collaborator.
type PostSessionDecision ¶
type PostSessionDecision struct {
WorkType string
WorkResult string // "passed" | "failed" | "unknown" | ""
TargetStatus string // Linear workflow-state name, e.g. "Finished"
ShouldTransition bool
PostDiagnostic bool
Deferred bool
Reason string
}
PostSessionDecision is the typed outcome of resolveTargetStatus. Callers use it to drive the actual side effects:
- TargetStatus non-empty + ShouldTransition true: call UpdateIssueStatus(issueID, TargetStatus).
- PostDiagnostic true: post the "missing WORK_RESULT" comment to Linear (do NOT transition).
- Deferred true: log "deferred to merge queue" and skip transition.
Reason is a free-form short identifier surfaced in logs ("passed", "failed", "unknown", "agent-failed", "completed-non-sensitive", "deferred-merge-queue", "no-mapping").
type ProviderNotRegisteredError ¶
type ProviderNotRegisteredError struct {
// RequestedID is the ProviderID from the ResolvedModelProfile.
RequestedID string
// Registered is the snapshot of provider names at call time.
Registered []string
}
ProviderNotRegisteredError is returned by SelectProvider when the requested provider family is not registered on this host. It is a structured error (not a plain string) so daemon dispatch code can log the exact requested + available set in one log line.
func (*ProviderNotRegisteredError) Error ¶
func (e *ProviderNotRegisteredError) Error() string
type ProviderView ¶
type ProviderView struct {
// contains filtered or unexported fields
}
ProviderView wraps a *Registry and satisfies daemon.ProviderRegistry. Construct via NewProviderView. Read-only and safe for concurrent use.
func NewProviderView ¶
func NewProviderView(reg *Registry) *ProviderView
NewProviderView returns a ProviderView backed by reg. Pass the result to daemon.Options.ProviderRegistry to expose the runner's registered AgentRuntime providers via the daemon's HTTP control API.
func (*ProviderView) Capabilities ¶
func (v *ProviderView) Capabilities(name string) (map[string]any, bool)
Capabilities returns the typed capability struct serialised to a flat map[string]any for the named provider, or (nil, false) when the provider is not registered. The map shape matches the JSON encoding of agent.Capabilities so the wire shape on /api/daemon/providers satisfies the contract in afclient/provider_types.go.
func (*ProviderView) Names ¶
func (v *ProviderView) Names() []string
Names returns the sorted list of registered provider names as plain strings (the daemon.ProviderRegistry contract). The underlying Registry.Names() returns []agent.ProviderName which is just a typed alias; we widen the wire shape here.
type QueuedWork ¶
type QueuedWork struct {
prompt.QueuedWork
// ResolvedProfile carries the model-profile knobs the platform
// resolved before queueing this work. The runner reads
// ResolvedProfile.Provider to select which provider implementation
// runs the session.
ResolvedProfile ResolvedProfile `json:"resolvedProfile,omitempty"`
// Branch is the working branch name the runner should use when
// provisioning the worktree. Empty falls back to "agent/<sessionID>".
Branch string `json:"branch,omitempty"`
// WorkerID is the daemon worker that claimed this session. Used
// for ownership probes inside the worktree retry loop and as the
// {workerId} in the heartbeat refresh body. Required.
WorkerID string `json:"workerId,omitempty"`
// AuthToken is the worker's bearer token used for platform API
// calls (heartbeat, result post). The daemon resolves this from
// the registration store; the runner just forwards it.
AuthToken string `json:"-"`
// PlatformURL is the base URL of the platform (e.g.
// "https://platform.example.com" or "http://127.0.0.1:3010"). The runner
// forwards this to result.Poster + heartbeat.Pulser. Required.
PlatformURL string `json:"-"`
}
QueuedWork is the runner's input contract — the per-session payload the daemon hands to Runner.Run. It embeds the prompt package's prompt.QueuedWork (which carries the issue/identifier/context the prompt builder consumes) and adds the runner-specific knobs the orchestrator needs (resolved profile, branch, worker id).
Wire shape: matches the platform Redis session payload at "agent:session:<id>" verbatim. F.1.1 §1 + the live payload observed during F.2.7 (REN2-1) drive the field set.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry resolves agent.ProviderName values to their corresponding agent.Provider instances. The runner builds one Registry at daemon startup (per F.2.8 wire-up) and consults it on every Run.
The zero value of Registry is unusable — callers must build one via NewRegistry which seeds the map.
Concurrency: Registry is safe for concurrent reads. Registration is only safe before the runner starts dispatching Runs; treat it as build-once, read-many.
func NewRegistry ¶
func NewRegistry() *Registry
NewRegistry constructs an empty Registry. Use Registry.Register to add providers.
func (*Registry) Names ¶
func (r *Registry) Names() []agent.ProviderName
Names returns the sorted list of registered provider names. Useful for daemon-startup logging and the `donmai agent providers` admin command.
func (*Registry) Register ¶
Register adds p under its declared Name. Calling Register with a different instance under an existing name overwrites the earlier entry — daemon startup decides whether duplicate registration is fatal.
Returns an error when p is nil or its Name is empty (a programmer error: every Provider implementation must declare a name).
func (*Registry) Resolve ¶
Resolve returns the registered Provider for name, or agent.ErrNoProvider when no provider is registered. The error is wrapped with the requested name so callers can log forensics.
func (*Registry) SelectProvider ¶
func (r *Registry) SelectProvider(profile ResolvedModelProfile) (agent.Provider, error)
SelectProvider resolves a provider implementation from the registry using the fully-rendered ResolvedModelProfile. It is the preferred dispatch entry point when the platform has already resolved the profile end-to-end; callers that only have a raw ProviderName should use Registry.Resolve directly.
Lookup order:
- profile.ProviderID (exact match against registered provider names)
- If unregistered, return a descriptive *ProviderNotRegisteredError
An empty ProviderID falls back to agent.ProviderClaude for backwards compatibility with dispatches that arrive before the platform ships the enriched profile.
Returns (nil, *ProviderNotRegisteredError) when the requested provider is not registered on this host. The error carries the ProviderID and the names currently registered so the caller can log a useful diagnostic.
func (*Registry) Shutdown ¶
Shutdown calls Provider.Shutdown on every registered provider and returns the joined error if any failed. Daemon drain calls this once on graceful exit so long-lived child processes (codex app-server) terminate cleanly.
A nil error means every provider's Shutdown returned nil. When multiple providers fail, the returned error is errors.Join'd so callers see all failures.
type ResolvedModelProfile ¶
type ResolvedModelProfile struct {
// ID is the model_profile row UUID from the platform's model_profiles
// table (e.g. "mp_01jt5..."). Used for audit logging and for
// cross-referencing the platform's profile management API.
ID string `json:"id"`
// ProviderID is the canonical provider family identifier
// (e.g. "claude", "codex", "gemini", "ollama"). Matches the
// agent.ProviderName enum; the daemon calls SelectProvider(profile)
// which converts this to agent.ProviderName internally.
ProviderID string `json:"providerId"`
// Model is the model variant within the provider family
// (e.g. "claude-opus-4-7", "gpt-4o-2025-04"). Empty falls back to
// the provider's built-in default model.
Model string `json:"model"`
// Mode is the reasoning-effort/speed tier string the platform
// resolved (e.g. "xhigh", "high", "medium", "low"). Maps onto
// agent.EffortLevel; empty falls back to the provider default.
Mode string `json:"mode,omitempty"`
// Context is the context-window size in tokens the platform requires
// for this dispatch (e.g. 1_000_000 for claude-3-7-sonnet-1m).
// Zero means "use the model default".
Context int `json:"context,omitempty"`
// MaxOutputTokens is the per-response output-token budget the
// platform resolver picked from the model catalog row. Zero means
// "use the model default".
MaxOutputTokens int `json:"maxOutputTokens,omitempty"`
}
ResolvedModelProfile is the fully-rendered provider/model specification the platform passes with each dispatch (ADR-2026-05-12-worktype-and-model- profile-routing). The Go daemon receives this from the platform as part of the poll response and uses it to select the correct provider implementation instead of falling back to the local-config default.
JSON tags follow the platform-side camelCase wire shape so the struct can be embedded directly in HTTP request/response bodies.
Relationship to ResolvedProfile: ResolvedModelProfile is the richer, platform-resolved shape carrying ID + context window + output-token caps. ResolvedProfile (runner/types.go) is the legacy shape used by QueuedWork. When a dispatch includes a ResolvedModelProfile it supersedes the ResolvedProfile fields; detailToQueuedWork (afcli/agent_run.go) bridges the two shapes.
func (ResolvedModelProfile) ToResolvedProfile ¶
func (p ResolvedModelProfile) ToResolvedProfile() ResolvedProfile
ToResolvedProfile converts a ResolvedModelProfile into the legacy runner.ResolvedProfile shape so callers can merge it into a QueuedWork without knowing both types. Fields not present in the legacy shape (Context, MaxOutputTokens) are injected via ProviderConfig so providers that honor extended knobs can consume them.
type ResolvedProfile ¶
type ResolvedProfile struct {
// Provider names the provider family that should run the session
// (claude/codex/stub for v0.5.0). When empty the runner falls
// back to the legacy `Runner` field, then to agent.ProviderClaude.
Provider agent.ProviderName `json:"provider,omitempty"`
// Runner is the legacy field name some platform deployments use
// for the same value. The runner reads Provider first and falls
// back to Runner so an in-flight wire-shape transition does not
// break dispatch.
Runner string `json:"runner,omitempty"`
// Model identifies the model variant within the provider family
// (e.g. "claude-sonnet-4-5"). Empty falls back to the provider
// default.
Model string `json:"model,omitempty"`
// Effort is the normalized reasoning-effort tier the provider
// should pass through to its native knob. Honored by providers
// with SupportsReasoningEffort=true.
Effort agent.EffortLevel `json:"effort,omitempty"`
// CredentialID identifies the credential entry the daemon should
// resolve into provider-native auth (e.g. ANTHROPIC_API_KEY) and
// inject via Spec.Env.
CredentialID string `json:"credentialId,omitempty"`
// ProviderConfig carries provider-specific knobs from the matched
// model profile. Forwarded into agent.Spec.ProviderConfig.
ProviderConfig map[string]any `json:"providerConfig,omitempty"`
}
ResolvedProfile names the profile knobs the platform resolved for this session. Mirrors F.1.1 §4 ResolvedProfile shape.
JSON tags follow the platform-side camelCase wire shape (consumed by the daemon poll handler).
type Result ¶
type Result struct {
agent.Result
// SessionID is the platform-side session UUID this result
// belongs to. Echoed for caller convenience.
SessionID string `json:"sessionId,omitempty"`
// IssueIdentifier is the human-readable issue identifier (e.g.
// "REN-1459"). Echoed for log correlation.
IssueIdentifier string `json:"issueIdentifier,omitempty"`
// StartedAt is the unix-ms timestamp when [Runner.Run] entered
// step 1 of the loop.
StartedAt int64 `json:"startedAt,omitempty"`
// FinishedAt is the unix-ms timestamp when [Runner.Run] returned
// the Result (after teardown).
FinishedAt int64 `json:"finishedAt,omitempty"`
// SteeringTriggered reports whether tail-recovery stage 1 fired.
SteeringTriggered bool `json:"steeringTriggered,omitempty"`
// PostSessionWarnings collects non-fatal warnings raised by the
// post-session block (REN-1467) — e.g. "Linear updateIssueStatus
// failed: …" or "diagnostic comment post failed: …". These are
// strictly observability — they do NOT change the session's
// terminal Status. Surface them in operator dashboards so a
// silently-failed transition is visible.
PostSessionWarnings []string `json:"postSessionWarnings,omitempty"`
// LinearStatusTransition records the result of the post-session
// Linear status-update attempt (REN-1467). Empty when no
// transition was attempted (non-result-sensitive type with no
// mapping, or marker was unknown). Non-nil even on failure so the
// caller can correlate dashboard signals to runner logs.
LinearStatusTransition *LinearStatusTransition `json:"linearStatusTransition,omitempty"`
// BudgetReport captures the per-stage budget enforcement record
// (REN-1485 / REN-1487 Phase 2 acceptance criterion #4). Non-nil
// for every Run; the .Enforced flag distinguishes
// stage-dispatched work (caps configured) from legacy work
// (caps absent). When a cap was breached .CapBreached + .BreachDetail
// surface the reason; the session's Status is "failed" with
// FailureMode=FailureBudgetExceeded.
BudgetReport *BudgetReport `json:"budgetReport,omitempty"`
}
Result is the terminal output of a Runner.Run call.
Today it is a thin alias around agent.Result with the addition of runner-internal fields (StartedAt, FinishedAt) and a direct field echo of the platform-relevant identifiers so callers do not have to thread the QueuedWork through their result handler. Forward-compat: new runner-wave hooks can extend Result without touching the agent/types.go contract.
type Runner ¶
type Runner struct {
// contains filtered or unexported fields
}
Runner is the long-lived per-daemon orchestrator. Build one via New at daemon startup and call Runner.Run for every claimed QueuedWork.
Runner is safe for concurrent use across sessions: every method holds only per-Run state via locals; collaborators (Registry, WorktreeManager, etc.) are documented as concurrency-safe by their own packages.
func New ¶
New constructs a Runner from opts. Returns an error when any required collaborator is missing.
func (*Runner) Run ¶
Run orchestrates one session end-to-end. It does not return until the session has reached a terminal state (success, failure, or cancellation) and the result has been posted.
The returned Result is always non-nil — on a fatal error the Result.Status is "failed" and Result.FailureMode classifies the reason. Callers should log err and inspect Result for the platform-relevant fields (PR URL, cost, etc).
Cancellation: ctx is honored at every step. A cancelled ctx short-circuits the loop with FailureMode "timeout"; the runner still attempts result.Post + teardown so platform state stays consistent.
type RuntimeCredentials ¶
RuntimeCredentials are the bearer-token credentials needed for session heartbeats and status posts.
type SpecInputs ¶
type SpecInputs struct {
// Cwd is the worktree path the worktree manager just provisioned.
Cwd string
// Prompt is the rendered user prompt (from prompt.Builder.Build).
Prompt string
// SystemPromptAppend is the rendered system-append block from the
// prompt builder; threaded into Spec.SystemPromptAppend for
// providers that consume it.
SystemPromptAppend string
// InitialContext is large/volatile session context (e.g. recalled
// agent memory) the runner routes through Spec.InitialContext so it
// rides the first turn's input rather than the re-sent system-prompt
// prefix. Empty unless the resolved provider declares
// Capabilities.SupportsTurnInputContext.
InitialContext string
// MCPServers is the list of MCP stdio configs the runtime/mcp
// builder produced. Empty when no plugins are enabled.
MCPServers []agent.MCPServerConfig
// Env is the merged session env (output of runtime/env.Compose,
// already rebuilt as a map for Spec.Env).
Env map[string]string
// Autonomous mirrors the daemon's session-mode flag — true for
// agent sessions invoked from the work queue.
Autonomous bool
}
SpecInputs are the per-session inputs the [translateSpec] helper merges with the QueuedWork to produce an agent.Spec. Splitting these out keeps spec_translation pure (no I/O, no platform calls) and makes the loop easy to test in isolation.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package access is the pure, DB-free OSS enforcement mirror of the platform's narrow-only model-access semantics (platform/src/lib/billing/access-policy.ts).
|
Package access is the pure, DB-free OSS enforcement mirror of the platform's narrow-only model-access semantics (platform/src/lib/billing/access-policy.ts). |