agent

package
v0.0.0-...-ad859cb Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 60 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultAgentID = "main"

DefaultAgentID is the registry key for the default agent. It is the internal identifier for the generic default agent instance used when no specific agent is targeted (e.g., unrouted channel messages).

View Source
const NotificationAdminBroadcast = "*admin*"

NotificationAdminBroadcast is the sentinel Recipient value used when a notification could not be routed to a specific user and must reach every admin connection instead (W-7 fallback).

Variables

View Source
var (
	ErrDepthLimitExceeded   = errors.New("sub-turn depth limit exceeded")
	ErrInvalidSubTurnConfig = errors.New("invalid sub-turn config")
	ErrConcurrencyTimeout   = errors.New("timeout waiting for concurrency slot")
)
View Source
var ErrReloadNotConfigured = errors.New("reload not configured")

ErrReloadNotConfigured is returned by TriggerReload when no reload function has been registered. This is normal in unit-test environments where the full gateway reload pipeline is not wired. Production always configures the reload function during startup, so callers outside tests should treat this as unexpected and log accordingly.

Functions

func AbandonedWritesSuppressed

func AbandonedWritesSuppressed() int64

AbandonedWritesSuppressed returns the current value of the omnipus_abandoned_writes_suppressed_total counter.

func GetSSRFChecker

func GetSSRFChecker(al *AgentLoop) *security.SSRFChecker

GetSSRFChecker returns the singleton SSRFChecker built from the SSRF policy config at startup (SEC-24). Returns nil when SSRF protection is disabled (sandbox.ssrf.enabled = false in config.json). Gateway handlers that make outbound HTTP calls (e.g. the skills installer) should pass this to their HTTP client constructors so allow_internal is honored consistently.

func GetTaskStore

func GetTaskStore(al *AgentLoop) *taskstore.TaskStore

GetTaskStore returns the shared TaskStore (may be nil in tests).

func RecoverOrphanedToolCalls

func RecoverOrphanedToolCalls(
	store session.SessionStore,
	sessionKey string,
	auditLog *audit.Logger,
) []providers.Message

RecoverOrphanedToolCalls inspects the tail of the session's message history. If orphaned tool calls are found (assistant message with tool_calls but no subsequent tool result), it:

  1. Appends a synthetic system message to the transcript documenting the ungraceful shutdown recovery (FR-069).
  2. Returns the rebuilt history with the orphaned assistant message removed (FR-088), so the next LLM call does not observe the half-completed turn.
  3. Emits audit events for each orphaned tool call.

Parameters:

  • store: the session store for the agent.
  • sessionKey: the session whose history to inspect and repair.
  • auditLog: optional audit logger; if nil, events are skipped.

Returns the cleaned history slice (all messages except the orphaned assistant turn). If no orphaned calls are found, returns the original history unchanged.

Safe to call on every session load — it is idempotent: orphans that already have a synthetic turn_canceled_restart system message are skipped.

func RegisterBuiltinHook

func RegisterBuiltinHook(name string, factory BuiltinHookFactory) error

RegisterBuiltinHook registers a named in-process hook factory for config-driven mounting.

func SpawnSubTurn

func SpawnSubTurn(ctx context.Context, cfg SubTurnConfig) (*tools.ToolResult, error)

SpawnSubTurn is the exported entry point for tools to spawn sub-turns. It retrieves AgentLoop and parent turnState from context and delegates to spawnSubTurn.

func TurnStateFromContext

func TurnStateFromContext(ctx context.Context) *turnState

TurnStateFromContext retrieves turnState from context (exported for tools)

func WithAgentLoop

func WithAgentLoop(ctx context.Context, al *AgentLoop) context.Context

WithAgentLoop injects AgentLoop into context for tool access

func WithScheduledJobContext

func WithScheduledJobContext(ctx context.Context, jobID, jobName string) context.Context

WithScheduledJobContext returns a child context carrying the schedule identity. Call this in the cron fire path (pkg/gateway/schedules.go RunScheduled) before calling ProcessScheduled so the auto-deny audit entry can include the job id and name.

Types

type ActiveTurnInfo

type ActiveTurnInfo struct {
	TurnID       string
	AgentID      string
	SessionKey   string
	Channel      string
	ChatID       string
	UserMessage  string
	Phase        TurnPhase
	Iteration    int
	StartedAt    time.Time
	Depth        int
	ParentTurnID string
	ChildTurnIDs []string
}

type AdmissionController

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

AdmissionController is a soft-cap gate for concurrent session workers.

Phase 1: gates inbound user-message dispatch only. The counter tracks unique active scopes (one per spawned session worker) — not per-turn, so a single chatty session cannot pin admission slots indefinitely. Subagent spawn and task-executor dispatch paths are NOT gated; see the v0.2 follow-up issue for resource-aware admission that covers those paths as well.

func (*AdmissionController) ActiveScopes

func (a *AdmissionController) ActiveScopes() int

ActiveScopes returns the current count of active scopes (worker goroutines that hold an admission slot). Used in tests and observability.

func (*AdmissionController) SoftCap

func (a *AdmissionController) SoftCap() int

SoftCap returns the configured soft cap value.

func (*AdmissionController) TryAdmit

func (a *AdmissionController) TryAdmit(scope string) (bool, func())

TryAdmit atomically claims a slot for scope. Returns (true, release) when the scope is admitted; release MUST be called (typically via defer) when the scope's worker exits.

If scope is already active (follow-up turn in an existing session), the call always succeeds without consuming an additional slot — the slot was already claimed when the worker was first spawned.

Returns (false, nil) when the softCap is reached and scope is a new scope.

type AgentContextDefinition

type AgentContextDefinition struct {
	Source AgentDefinitionSource  `json:"source,omitempty"`
	Agent  *AgentPromptDefinition `json:"agent,omitempty"`
	Soul   *SoulDefinition        `json:"soul,omitempty"`
	User   *UserDefinition        `json:"user,omitempty"`
}

AgentContextDefinition captures the workspace agent definition in a runtime-friendly shape.

type AgentDefinitionSource

type AgentDefinitionSource string

AgentDefinitionSource identifies which agent bootstrap file produced the definition.

const (
	// AgentDefinitionSourceAgent indicates the new AGENT.md format.
	AgentDefinitionSourceAgent AgentDefinitionSource = "AGENT.md"
	// AgentDefinitionSourceAgents indicates the legacy AGENTS.md format.
	AgentDefinitionSourceAgents AgentDefinitionSource = "AGENTS.md"
)

type AgentFrontmatter

type AgentFrontmatter struct {
	Name        string         `json:"name"`
	Description string         `json:"description"`
	Tools       []string       `json:"tools,omitempty"`
	Model       string         `json:"model,omitempty"`
	MaxTurns    *int           `json:"maxTurns,omitempty"`
	Skills      []string       `json:"skills,omitempty"`
	MCPServers  []string       `json:"mcpServers,omitempty"`
	Fields      map[string]any `json:"fields,omitempty"`
}

AgentFrontmatter holds machine-readable AGENT.md configuration.

Known fields are exposed directly for convenience. Fields keeps the full parsed frontmatter so future refactors can read additional keys without changing the loader contract again.

type AgentInstance

type AgentInstance struct {
	ID                        string
	Name                      string
	Model                     string
	Fallbacks                 []string
	Workspace                 string
	MaxIterations             int
	MaxTokens                 int
	Temperature               float64
	ThinkingLevel             ThinkingLevel
	ContextWindow             int
	SummarizeMessageThreshold int
	SummarizeTokenPercent     int
	Provider                  providers.LLMProvider
	Sessions                  session.SessionStore
	ContextBuilder            *ContextBuilder
	Tools                     *tools.ToolRegistry
	Subagents                 *config.SubagentsConfig
	SkillsFilter              []string
	Candidates                []providers.FallbackCandidate

	// TimeoutSeconds is the per-turn hard timeout. 0 = disabled.
	// Populated from AgentDefaults.TimeoutSeconds; per-agent override if available.
	TimeoutSeconds int

	// AgentType is the resolved type string ("core", "custom", "system") used by
	// FilterToolsByPolicy at LLM-call assembly time (FR-003, FR-041). Set once at
	// construction; never mutated after creation.
	AgentType string

	// IsRoutingDefault records whether this agent was marked Default=true in
	// its AgentConfig at construction time. Used by GetDefaultAgent to make the
	// per-agent routing-default flag the single canonical source of truth (F3),
	// so SPA WebSocket chat and channel routing both converge on the same agent.
	// Read-only after construction.
	IsRoutingDefault bool

	// Router is non-nil when model routing is configured and the light model
	// was successfully resolved. It scores each incoming message and decides
	// whether to route to LightCandidates or stay with Candidates.
	Router *routing.Router
	// LightCandidates holds the resolved provider candidates for the light model.
	// Pre-computed at agent creation to avoid repeated model_list lookups at runtime.
	LightCandidates []providers.FallbackCandidate
	// LightProvider is the concrete provider instance for the configured light model.
	// It is only used when routing selects the light tier for a turn.
	LightProvider providers.LLMProvider
	// contains filtered or unexported fields
}

AgentInstance represents a fully configured agent with its own workspace, session manager, context builder, and tool registry.

func NewAgentInstance

func NewAgentInstance(
	agentCfg *config.AgentConfig,
	defaults *config.AgentDefaults,
	cfg *config.Config,
	provider providers.LLMProvider,
) *AgentInstance

NewAgentInstance creates an agent instance from config.

func (*AgentInstance) Close

func (a *AgentInstance) Close() error

Close releases resources held by the agent's session store.

func (*AgentInstance) LoadToolPolicy

func (a *AgentInstance) LoadToolPolicy() *tools.ToolPolicyCfg

LoadToolPolicy returns the current tool policy snapshot for this agent. Returns nil when no policy has been stored (defaults to allow-all at call sites). Safe for concurrent access (atomic load, FR-020).

func (*AgentInstance) SetAgentType

func (a *AgentInstance) SetAgentType(agentType string)

SetAgentType updates the resolved agent type. Called by the registry to upgrade runtime-seeded core agents (e.g., Ava, Main) that may not have Type set in config.

func (*AgentInstance) StoreToolPolicy

func (a *AgentInstance) StoreToolPolicy(p *tools.ToolPolicyCfg)

StoreToolPolicy atomically replaces the agent's tool policy (FR-020). Called by ReloadProviderAndConfig on config PUT to propagate the new policy without rebuilding the agent registry. Passing nil resets to allow-all. Safe for concurrent access with ongoing turn assembly.

type AgentLoop

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

func AgentLoopFromContext

func AgentLoopFromContext(ctx context.Context) *AgentLoop

AgentLoopFromContext retrieves AgentLoop from context

func NewAgentLoop

func NewAgentLoop(
	cfg *config.Config,
	msgBus *bus.MessageBus,
	provider providers.LLMProvider,
) (*AgentLoop, error)

NewAgentLoop constructs an AgentLoop from the given config, message bus, and LLM provider. Returns (*AgentLoop, nil) on success. Returns (nil, *RecapModelBootError) when AutoRecapEnabled is true and the recap model fails the allow-list gate (FR-029a) — callers should treat this as a fatal configuration error and abort boot.

func (*AgentLoop) AgentForSession

func (al *AgentLoop) AgentForSession(sessionID string) (*AgentInstance, error)

AgentForSession resolves the AgentInstance responsible for the given session. FR-026.

func (*AgentLoop) ApplyAgentModel

func (al *AgentLoop) ApplyAgentModel(agentID, model string) (string, error)

ApplyAgentModel switches a live agent instance to a new primary model in place — rebuilding its provider, candidate chain, and thinking level under the instance lock WITHOUT recreating the instance. This preserves the agent's in-memory conversation state and avoids a config hot-reload that would drop the WebSocket (#73). The new model must already be persisted to config (the REST handler writes config.json first) so resolution observes it. Returns the previous primary model. Shared by the switch_model tool and the PUT /api/v1/agents/{id} model-change path.

func (*AgentLoop) AuditLogger

func (al *AgentLoop) AuditLogger() *audit.Logger

AuditLogger returns the audit logger, or nil if audit logging is disabled. Used by gateway handlers that need to log policy changes.

func (*AgentLoop) BootstrapRecapPass

func (al *AgentLoop) BootstrapRecapPass(ctx context.Context)

BootstrapRecapPass (FR-032, FR-032a): on gateway start, scans the shared session store for sessions that lack a retro and are older than 30 minutes, and enqueues a CloseSession("bootstrap") for each.

Early-returns if AutoRecapEnabled or BootstrapRecapEnabled is false. Rate-limits starts to GetBootstrapRecapMaxPerMinute per minute. Caps total estimated cost at GetBootstrapRecapDailyBudgetUSD.

Sessions are a gateway-wide resource, NOT per-agent — so the sessions directory is walked exactly once. Each session's owning agent is resolved via AgentForSession before auditing so the audit entry reflects reality.

func (*AgentLoop) Close

func (al *AgentLoop) Close()

Close releases resources held by agent session stores. Call after Stop.

func (*AgentLoop) CloseSession

func (al *AgentLoop) CloseSession(sessionID, trigger string)

CloseSession triggers an async session-end recap if AutoRecapEnabled is set and this sessionID has not already been claimed. Idempotent: duplicate calls for the same sessionID are silently dropped (FR-027).

func (*AgentLoop) ContextBuilderRegistry

func (al *AgentLoop) ContextBuilderRegistry() *ContextBuilderRegistry

ContextBuilderRegistry returns the registry used to broadcast system-prompt cache invalidation when operator config changes (FR-061). Always non-nil after NewAgentLoop.

func (*AgentLoop) Continue

func (al *AgentLoop) Continue(ctx context.Context, sessionKey, channel, chatID string) (string, error)

Continue resumes an idle agent by dequeuing any pending steering messages and running them through the agent loop. This is used when the agent's last message was from the assistant (i.e., it has stopped processing) and the user has since enqueued steering messages.

If no steering messages are pending, it returns an empty string.

func (*AgentLoop) EmitNotification

func (al *AgentLoop) EmitNotification(p NotificationPayload)

EmitNotification publishes a user-facing notification onto the event bus so the recipient's SPA WebSocket connections receive a notification frame (#264). The WS forwarder filters delivery by Recipient (==wsConn.userID), so the payload is not broadcast to every authenticated tab. Safe to call from any goroutine — the bus drops to a full subscriber rather than blocking.

func (*AgentLoop) EmitWhatsAppPairing

func (al *AgentLoop) EmitWhatsAppPairing(channelID string, status channels.PairingStatus, qr, message string)

EmitWhatsAppPairing publishes a WhatsApp native/QR pairing update (QR code or status) onto the event bus so every connected SPA WebSocket client receives a whatsapp_pairing frame (#283). Safe to call from a channel's own goroutine — the bus drops to a full subscriber rather than blocking. Wired into the WhatsApp native channel at gateway boot via SetPairingObserver.

func (*AgentLoop) EventDrops

func (al *AgentLoop) EventDrops(kind EventKind) int64

EventDrops returns the number of dropped events for the given kind.

func (*AgentLoop) ExecProxy

func (al *AgentLoop) ExecProxy() *security.ExecProxy

ExecProxy returns the SEC-28 SSRF proxy for exec child processes, or nil when the proxy is disabled or failed to bind. Used by gateway handlers that report the proxy status and by tests that exercise the proxy lifecycle.

func (*AgentLoop) GetActiveAgentIDs

func (al *AgentLoop) GetActiveAgentIDs() []string

GetActiveAgentIDs returns the IDs of all agents that currently have an active turn. Used by the REST API to report real-time agent status.

func (*AgentLoop) GetActiveTurn

func (al *AgentLoop) GetActiveTurn() *ActiveTurnInfo

func (*AgentLoop) GetActiveTurnBySession

func (al *AgentLoop) GetActiveTurnBySession(sessionKey string) *ActiveTurnInfo

func (*AgentLoop) GetActiveTurnHookForSession

func (al *AgentLoop) GetActiveTurnHookForSession(sessionID string) TurnCancelHook

GetActiveTurnHookForSession returns a TurnCancelHook for the active turn belonging to the given transcript session ID, or nil if none is active. Used by handleCancel to atomically claim the turn and register the post-cancel callback (FR-10, FR-11, FR-15).

H1: When multiple turns share the same transcriptSessionID (a root turn plus one or more sub-turns), the root turn (depth==0 / parentTurnID=="") is preferred so the cancel handler targets the outermost scope. The first match in the sync.Map iteration is returned only as a last-resort fallback (defensive; should not occur in normal operation).

func (*AgentLoop) GetAgentStore

func (al *AgentLoop) GetAgentStore(agentID string) *session.UnifiedStore

GetAgentStore returns the UnifiedStore for a given agent, or nil if not found or if the agent's session store is not a UnifiedStore. Use GetSessionStore() for creating new sessions; GetAgentStore is kept for legacy per-agent session access.

func (*AgentLoop) GetChannelManager

func (al *AgentLoop) GetChannelManager() *channels.Manager

GetChannelManager returns the current channel manager under the read lock (may be nil before channels start, e.g. during onboarding). Exported for pkg/gateway: REST handlers inspect runtime channel state (e.g. FailedChannels), and the scheduled runner validates that a deliver=true target channel is registered before publishing (M2). Set after construction via SetChannelManager, so callers must tolerate nil and re-fetch at use time.

func (*AgentLoop) GetConfig

func (al *AgentLoop) GetConfig() *config.Config

GetConfig returns the current config (thread-safe)

func (*AgentLoop) GetCurrentSession

func (al *AgentLoop) GetCurrentSession(agentID string) (string, bool)

GetCurrentSession returns the active session ID for the given agent, and whether one was found. Used by the WebSocket lazy-CAS logic (FR-024).

func (*AgentLoop) GetMediaRefsDropped

func (al *AgentLoop) GetMediaRefsDropped() int64

GetMediaRefsDropped returns the cumulative count of media refs that were dropped because they could not be resolved (unknown ref or file missing on disk). Safe for concurrent access; incremented on the hot turn path.

func (*AgentLoop) GetMediaStore

func (al *AgentLoop) GetMediaStore() media.MediaStore

GetMediaStore returns the currently injected media store. Callers that serve media over HTTP must use this getter (not a cached reference) because the store is replaced on every restartServices — a cached pointer goes stale.

func (*AgentLoop) GetRegistry

func (al *AgentLoop) GetRegistry() *AgentRegistry

GetRegistry returns the current registry (thread-safe)

func (*AgentLoop) GetSessionActiveAgent

func (al *AgentLoop) GetSessionActiveAgent(sessionID string) (string, bool)

GetSessionActiveAgent returns the agent that the handoff tool last switched the given session to. Returns ("", false) if no handoff override is active for this session_id.

func (*AgentLoop) GetSessionStore

func (al *AgentLoop) GetSessionStore() *session.UnifiedStore

GetSessionStore returns the shared UnifiedStore for new sessions. May be nil in tests or when the shared sessions directory could not be initialized.

func (*AgentLoop) GetStartupInfo

func (al *AgentLoop) GetStartupInfo() map[string]any

GetStartupInfo returns information about loaded tools and skills for logging.

func (*AgentLoop) HardAbort

func (al *AgentLoop) HardAbort(sessionKey string) error

HardAbort immediately cancels the running agent loop for the given session, cascading the cancellation to all child SubTurns. This is a destructive operation that terminates execution without waiting for graceful cleanup.

Use this when the user explicitly requests immediate termination (e.g., "stop now", "abort"). For graceful interruption that allows the agent to finish the current tool and summarize, use Steer() instead.

func (*AgentLoop) HydrateAgentHistoryFromTranscript

func (al *AgentLoop) HydrateAgentHistoryFromTranscript(sessionID string) error

HydrateAgentHistoryFromTranscript reads the transcript for sessionID and rebuilds each owning agent's session.SessionStore history under the key "agent:<agentID>:session:<sessionID>".

The mapping is best-effort: messages with unknown roles or unresolvable agent IDs are skipped. SubTurn entries (orchestrator hand-offs) are ignored at this layer — they are reconstructed by the agent loop's own subturn machinery on demand.

func (*AgentLoop) InjectFollowUp

func (al *AgentLoop) InjectFollowUp(msg providers.Message) error

InjectFollowUp enqueues a message to be automatically processed after the current turn completes. Unlike Steer(), which interrupts the current execution, InjectFollowUp waits for the current turn to finish naturally before processing the message.

This is useful for: - Automated workflows that need to chain multiple turns - Background tasks that should run after the main task completes - Scheduled follow-up actions

The message will be processed via Continue() when the agent becomes idle.

func (*AgentLoop) InjectSteering

func (al *AgentLoop) InjectSteering(msg providers.Message) error

InjectSteering is an alias for Steer() to match the design document naming. It injects a steering message into the currently running agent loop.

func (*AgentLoop) InterruptByChannelChat

func (al *AgentLoop) InterruptByChannelChat(channel, chatID, hint string) error

InterruptByChannelChat gracefully cancels the active root turn (depth==0) whose channel and chatID match the supplied values, then cascades to all sub-turns that share the same transcriptSessionID via InterruptSession.

This is the correct cancellation path for Tier B (text-parsing) channels: inbound messages from those channels carry no explicit SessionID so a direct InterruptSession call would match nothing. Sub-turns inherit their parent's transcriptSessionID but NOT channel/chatID (they are created with empty values), so matching by channel+chatID alone misses them. The two-step strategy — find root by channel+chatID, then cascade by sessionID — covers both parent and all sub-turns.

Returns nil whether or not any matching turn was found — "no active turn" is a valid no-op. Returns a non-nil error only when channel or chatID is empty.

func (*AgentLoop) InterruptGraceful

func (al *AgentLoop) InterruptGraceful(hint string) error

func (*AgentLoop) InterruptHard

func (al *AgentLoop) InterruptHard() error

func (*AgentLoop) InterruptSession

func (al *AgentLoop) InterruptSession(sessionID, hint string) (descendants []string, err error)

InterruptSession gracefully cancels the parent turn AND every sub-turn sharing the given sessionID (transcriptSessionID match). FR-6, FR-10, FR-12a, FR-15.

The cascade walks activeTurnStates and, for each matching turnState, spawns a goroutine that calls requestGracefulInterrupt AND providerCancel in parallel so the in-flight LLM HTTP request is aborted immediately (FR-12a) rather than waiting for the stream to drain naturally within the 3s graceful window.

Returns the list of turn IDs that received the cancel signal (parent + sub-turns). The cancel handler includes this in the turn_canceled audit/transcript entry. Returns an error only if sessionID is empty. A session with no active turns is not an error — cancel handlers treat it as a no-op (was_fired=false).

func (*AgentLoop) InterruptSessionHard

func (al *AgentLoop) InterruptSessionHard(sessionID, hint string) (descendants []string, err error)

InterruptSessionHard escalates a previously-graceful cancel to a hard abort for every turn matching sessionID. Called at t=3s after InterruptSession per FR-11. See InterruptHard for the legacy single-turn path; this function is session-scoped.

Returns the list of turn IDs that received the hard-abort signal.

func (*AgentLoop) ListAllSessions

func (al *AgentLoop) ListAllSessions() ([]*session.UnifiedMeta, []error)

ListAllSessions returns sessions from the shared store merged with legacy per-agent stores, deduplicated and sorted by UpdatedAt descending. The second return value collects per-store errors so callers can distinguish "no sessions" from "all stores failed". Callers should surface partial errors as warnings rather than treating the entire response as a failure.

func (*AgentLoop) MountHook

func (al *AgentLoop) MountHook(reg HookRegistration) error

MountHook registers an in-process hook on the agent loop.

func (*AgentLoop) MountProcessHook

func (al *AgentLoop) MountProcessHook(ctx context.Context, name string, opts ProcessHookOptions) error

func (*AgentLoop) MutateConfig

func (al *AgentLoop) MutateConfig(fn func(*config.Config) error) error

MutateConfig acquires the agent loop write lock and calls fn with the live *config.Config pointer. This serializes sysagent mutations with all REST readers that go through GetConfig (which holds RLock). fn must not call GetConfig or SwapConfig — deadlock would result.

The caller (typically Deps.WithConfig) is responsible for snapshotting and rolling back cfg fields if fn or the subsequent SaveConfig fails.

func (*AgentLoop) ProcessDirect

func (al *AgentLoop) ProcessDirect(
	ctx context.Context,
	content, sessionKey string,
) (string, error)

func (*AgentLoop) ProcessDirectWithChannel

func (al *AgentLoop) ProcessDirectWithChannel(
	ctx context.Context,
	content, sessionKey, channel, chatID string,
) (string, error)

func (*AgentLoop) ProcessHeartbeat

func (al *AgentLoop) ProcessHeartbeat(
	ctx context.Context,
	content, channel, chatID string,
) (string, error)

ProcessHeartbeat processes a heartbeat request without session history. Each heartbeat is independent and doesn't accumulate context.

func (*AgentLoop) ProcessScheduled

func (al *AgentLoop) ProcessScheduled(
	ctx context.Context,
	ownerAgentID, sessionID, content, channel, chatID string,
) (string, error)

ProcessScheduled runs a fired schedule's message as ownerAgentID against the concrete pre-created sessionID (issue #264, W-1). It is the dedicated headless entry point for the cron → agent fire path and deliberately differs from the human message path:

  • It pins ownerAgentID directly via runAgentLoop — it does NOT consult routing or the sessionActiveAgent handoff map, so a human switching agents in this session cannot hijack the scheduled run, and a missing/disabled owner is a hard error (never a default-agent fallback, the core #264 bug).
  • It passes the concrete sessionID as TranscriptSessionID so the turn registers under it (GetActiveTurnHookForSession matches by transcriptSessionID) and RequestCancel(CancelScope{SessionID}) can abort it on a caller-imposed deadline. The session key is the per-owner "agent:<owner>:session:<id>" form, collision-free across isolated runs.
  • It sets AutoDenyAsk so any `ask`-policy tool call is denied without blocking for approval (FR-009) — no operator is present.

The caller (the Wave-2 gateway runner) resolves the owner + picks the session per session_mode and supplies a concrete sessionID; it imposes the deadline on ctx and calls RequestCancel on timeout. ProcessScheduled only guarantees owner-pinning, cancellability, and prompt return.

Returns the agent's reply and a non-nil error on run failure. An aborted (canceled/deadline) run returns a context-derived error promptly.

func (*AgentLoop) PromptGuard

func (al *AgentLoop) PromptGuard() *security.PromptGuard

PromptGuard returns the SEC-25 prompt-injection guard. Always non-nil after NewAgentLoop — even when no config field is set, the factory returns a medium-strictness guard. Used by runTurn and by gateway status handlers.

func (*AgentLoop) RateLimiter

func (al *AgentLoop) RateLimiter() *security.RateLimiterRegistry

RateLimiter returns the SEC-26 rate limiter registry. Always non-nil after NewAgentLoop. Used by runTurn for per-agent limit checks and by gateway handlers that report the current rate limit / cost status.

func (*AgentLoop) RecordLastChannel

func (al *AgentLoop) RecordLastChannel(channel string) error

RecordLastChannel records the last active channel for this workspace. This uses the atomic state save mechanism to prevent data loss on crash.

func (*AgentLoop) RecordLastChatID

func (al *AgentLoop) RecordLastChatID(chatID string) error

RecordLastChatID records the last active chat ID for this workspace. This uses the atomic state save mechanism to prevent data loss on crash.

func (*AgentLoop) RegisterIdleTicker

func (al *AgentLoop) RegisterIdleTicker(sessionID string, cancel context.CancelFunc)

RegisterIdleTicker stores a cancel function for the idle ticker of a session. Calling it again for the same session replaces the previous cancel without canceling it — use resetIdleTicker for the reset path.

func (*AgentLoop) RegisterTool

func (al *AgentLoop) RegisterTool(tool tools.Tool)

func (*AgentLoop) ReloadProviderAndConfig

func (al *AgentLoop) ReloadProviderAndConfig(
	ctx context.Context,
	provider providers.LLMProvider,
	cfg *config.Config,
) error

ReloadProviderAndConfig atomically swaps the provider and config with proper synchronization. It uses a context to allow timeout control from the caller. Returns an error if the reload fails or context is canceled.

func (*AgentLoop) RequestCancel

func (al *AgentLoop) RequestCancel(
	ctx context.Context,
	scope CancelScope,
	canceller CancelCanceller,
	hooks CancelHooks,
) (CancelOutcome, error)

RequestCancel is the canonical cancel entry point. All four cancel surfaces (web SPA, Tier A /cancel command, Tier B text-parsing channels, CLI) call this method.

It performs the entire cancel state machine:

  • abuse-detection record
  • ClaimCancel atomic first-cancel-wins check
  • turn_cancel_attempt audit emission (always, even for no-op cancels)
  • graceful cascade via InterruptSession / providerCancel
  • approval auto-deny (via hooks.CancelPendingApprovals)
  • cancel_stage frame emission (via hooks.SendStageFrame)
  • session status → interrupted (via hooks.SetSessionInterrupted)
  • transcript MarkLastEntryTruncated + turn_canceled entry on Finish
  • turn_canceled audit on Finish
  • 3s timer → hard abort (InterruptSessionHard)
  • 5s timer → detached / MarkAbandoned + turn_cancel_stuck audit

Returns:

  • CancelOutcome{Fired: true, Descendants, TurnID} on a successful claim
  • CancelOutcome{Fired: false} when no active turn matches OR ClaimCancel found cancelFired==true (double-cancel race)
  • error only for parameter validation failures (empty scope)

func (*AgentLoop) RequestCancelByChannelChat

func (al *AgentLoop) RequestCancelByChannelChat(ctx context.Context, channelName, chatID, userID string) error

RequestCancelByChannelChat is a primitive-argument adapter for RequestCancel used by the channels.CancelInterceptor interface. It resolves the session by (channel, chatID) so Tier B text-parsing channels can fire the full cancel state machine without knowing the session ID.

Returns nil when no matching turn exists (no-op). Returns a non-nil error only when channel or chatID is empty.

func (*AgentLoop) RequestCancelForSession

func (al *AgentLoop) RequestCancelForSession(ctx context.Context, sessionID, userID, channel string) (bool, error)

RequestCancelForSession is a primitive-argument adapter for RequestCancel used by the commands.AgentLoopInterface. It avoids importing pkg/agent types in pkg/commands (which would create a circular dependency) by accepting only primitive string arguments.

sessionID must be non-empty. Returns (fired, nil) on success; fired is true when an active turn was claimed.

func (*AgentLoop) ResolveSessionStore

func (al *AgentLoop) ResolveSessionStore(sessionID string) *session.UnifiedStore

ResolveSessionStore finds which UnifiedStore owns the given sessionID. Checks the shared store first, then the main agent's legacy store, then all other per-agent stores. Returns nil if the session cannot be found.

func (*AgentLoop) Run

func (al *AgentLoop) Run(ctx context.Context) error

func (*AgentLoop) SandboxBackend

func (al *AgentLoop) SandboxBackend() sandbox.SandboxBackend

SandboxBackend returns the active sandbox backend, or nil if sandboxing is disabled. Used by gateway handlers that report sandbox status.

func (*AgentLoop) SetAllowGodMode

func (al *AgentLoop) SetAllowGodMode(allow bool)

SetAllowGodMode sets the god-mode opt-in flag (latch 2). Must be called before WireTier13Deps so the coercion logic picks up the correct value. If called after WireTier13Deps, the change takes effect on the next hot-reload.

func (*AgentLoop) SetAppliedSandboxMode

func (al *AgentLoop) SetAppliedSandboxMode(mode sandbox.Mode)

SetAppliedSandboxMode stores the mode that the kernel sandbox actually applied at boot (from SandboxApplyResult.Mode). Must be called from the gateway boot path after applySandbox returns successfully, before WireTier13Deps and wireExecToolDeps run, so that ExecToolDeps.SandboxMode reflects the true runtime enforcement level rather than the config file value.

func (*AgentLoop) SetChannelManager

func (al *AgentLoop) SetChannelManager(cm *channels.Manager)

func (*AgentLoop) SetCurrentSession

func (al *AgentLoop) SetCurrentSession(agentID, sessionID string)

SetCurrentSession records the active session ID for the given agent. Used by the WebSocket lazy-CAS logic (FR-024).

func (*AgentLoop) SetMediaStore

func (al *AgentLoop) SetMediaStore(s media.MediaStore)

SetMediaStore injects a MediaStore for media lifecycle management.

func (*AgentLoop) SetReloadFunc

func (al *AgentLoop) SetReloadFunc(fn func() error)

SetReloadFunc sets the callback function for triggering config reload.

func (*AgentLoop) SetSteeringMode

func (al *AgentLoop) SetSteeringMode(mode SteeringMode)

SetSteeringMode updates the steering mode.

func (*AgentLoop) SetSysagentDeps

func (al *AgentLoop) SetSysagentDeps(deps *systools.Deps)

SetSysagentDeps stores the system.* tool dependencies for use by hot-reload. Call WireSysagentDeps after this to immediately register the tools.

func (*AgentLoop) SetToolApprover

func (al *AgentLoop) SetToolApprover(a PolicyApprover)

SetToolApprover injects the gateway's policy-level approval implementation into the loop (FR-011). Must be called before any turns start; safe from any goroutine. Passing nil clears the approver (ask-policy tools treated as allow — open gate).

func (*AgentLoop) SetTranscriber

func (al *AgentLoop) SetTranscriber(t voice.Transcriber)

SetTranscriber injects a voice transcriber for agent-level audio transcription.

func (*AgentLoop) Steer

func (al *AgentLoop) Steer(msg providers.Message) error

Steer enqueues a user message to be injected into the currently running agent loop. The message will be picked up after the current tool finishes executing, causing any remaining tool calls in the batch to be skipped.

func (*AgentLoop) SteeringMode

func (al *AgentLoop) SteeringMode() SteeringMode

SteeringMode returns the current steering mode.

func (*AgentLoop) Stop

func (al *AgentLoop) Stop()

func (*AgentLoop) SubscribeEvents

func (al *AgentLoop) SubscribeEvents(buffer int) EventSubscription

SubscribeEvents registers a subscriber for agent-loop events.

func (*AgentLoop) SwapConfig

func (al *AgentLoop) SwapConfig(newCfg *config.Config)

SwapConfig atomically replaces the in-memory config with the supplied, fully-initialized *config.Config (credentials resolved, sensitive values registered). Callers are responsible for calling credentials.ResolveBundle and cfg.RegisterSensitiveValues before SwapConfig — this method only does the atomic pointer swap.

func (*AgentLoop) TriggerReload

func (al *AgentLoop) TriggerReload() error

TriggerReload triggers a config reload so the in-memory config picks up changes written to disk by safeUpdateConfigJSON. Called by REST handlers after persisting config changes (agent create/update, token rotate, etc.).

Concurrency: the underlying reloadFunc (set in gateway.go) is guarded by an atomic CompareAndSwap that serializes concurrent calls — only one reload can be in flight at a time. A second concurrent call returns an error ("reload already in progress") rather than queuing a second reload.

func (*AgentLoop) UnmountHook

func (al *AgentLoop) UnmountHook(name string)

UnmountHook removes a previously registered in-process hook.

func (*AgentLoop) UnsubscribeEvents

func (al *AgentLoop) UnsubscribeEvents(id uint64)

UnsubscribeEvents removes a previously registered event subscriber.

func (*AgentLoop) WaitForActiveRequests

func (al *AgentLoop) WaitForActiveRequests()

WaitForActiveRequests blocks until all in-flight LLM calls tracked by activeRequests have completed. Used by the graceful shutdown sequence to ensure active turns finish before the process exits.

func (*AgentLoop) WireSysagentDeps

func (al *AgentLoop) WireSysagentDeps(deps *systools.Deps)

WireSysagentDeps registers all 41 system.* tools on every agent in the current registry (FR-001, FR-002). Mirrors the WireTier13Deps pattern: called once at boot after NewAgentLoop, and again on hot-reload. The deps pointer is stashed so hot-reload can re-apply the wiring on the rebuilt registry.

Per-agent policy (seeded via coreagent.SeedConfig) governs which agents may actually invoke these tools at LLM-call time — this registration is the supply side; policy is the demand filter.

func (*AgentLoop) WireTier13Deps

func (al *AgentLoop) WireTier13Deps(deps Tier13Deps)

WireTier13Deps registers the web_serve, workspace.shell, and workspace.shell_bg tools into every non-system agent's tool registry using the shared infrastructure instances created once at gateway boot. Called from gateway.go after NewAgentLoop and after the Tier13Deps registries (DevServerRegistry, ServedSubdirs, EgressProxy) are constructed. The "Tier13" name is historical — Tier 1 (static serve) and Tier 3 (dev-server proxy) used to live in two separate tools (serve_workspace and run_in_workspace); both are now subsumed by web_serve, but the deps struct keeps the legacy name for cross-package callers.

Mirrors the wireExecToolDeps pattern: post-creation injection so the heavy singleton objects (EgressProxy, DevServerRegistry) are not re-created per agent. Nil fields in deps skip the corresponding tool registration (graceful degradation when preview is disabled or Tier 3 unsupported).

type AgentLoopSpawner

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

AgentLoopSpawner implements tools.SubTurnSpawner interface. This allows tools to spawn sub-turns without circular dependency.

func NewSubTurnSpawner

func NewSubTurnSpawner(al *AgentLoop) *AgentLoopSpawner

NewSubTurnSpawner creates a SubTurnSpawner for the given AgentLoop.

func (*AgentLoopSpawner) SpawnSubTurn

func (s *AgentLoopSpawner) SpawnSubTurn(
	ctx context.Context,
	cfg tools.SubTurnConfig,
) (*tools.ToolResult, error)

SpawnSubTurn implements tools.SubTurnSpawner interface.

type AgentPromptDefinition

type AgentPromptDefinition struct {
	Path           string           `json:"path"`
	Raw            string           `json:"raw"`
	Body           string           `json:"body"`
	RawFrontmatter string           `json:"raw_frontmatter,omitempty"`
	Frontmatter    AgentFrontmatter `json:"frontmatter"`
}

AgentPromptDefinition represents the parsed AGENT.md or AGENTS.md prompt file.

type AgentRegistry

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

AgentRegistry manages multiple agent instances and routes messages to them.

func NewAgentRegistry

func NewAgentRegistry(
	cfg *config.Config,
	provider providers.LLMProvider,
) *AgentRegistry

NewAgentRegistry creates a registry from config, instantiating all agents.

func (*AgentRegistry) CanSpawnSubagent

func (r *AgentRegistry) CanSpawnSubagent(parentAgentID, targetAgentID string) bool

CanSpawnSubagent checks if parentAgentID is allowed to spawn targetAgentID.

func (*AgentRegistry) Close

func (r *AgentRegistry) Close()

Close releases resources held by all registered agents and clears the map (M9).

func (*AgentRegistry) ForEachTool

func (r *AgentRegistry) ForEachTool(name string, fn func(tools.Tool))

ForEachTool calls fn for every tool registered under the given name across all agents. This is useful for propagating dependencies (e.g. MediaStore) to tools after registry construction.

func (*AgentRegistry) GetAgent

func (r *AgentRegistry) GetAgent(agentID string) (*AgentInstance, bool)

GetAgent returns the agent instance for a given ID.

func (*AgentRegistry) GetAgentName

func (r *AgentRegistry) GetAgentName(agentID string) (string, bool)

GetAgentName returns the display name for agentID and true if the agent exists in the registry. It satisfies the tools.AgentRegistryReader interface used by HandoffTool to avoid an import cycle.

func (*AgentRegistry) GetDefaultAgent

func (r *AgentRegistry) GetDefaultAgent() *AgentInstance

GetDefaultAgent returns the default agent instance.

Resolution order (canonical — matches channel routing's resolveDefaultAgentID):

  1. An agent whose config has Default==true (the per-agent routing-default flag set by SeedConfig / the Agents-screen "star"). Deterministic: if multiple agents somehow carry Default==true (operator error — F11 repairs this at boot), the one with the lexicographically smallest ID wins.
  2. The configurable override from config.Agents.Defaults.DefaultAgentID, when the named agent exists in the registry.
  3. The built-in "main" sentinel agent.
  4. The lexicographically first registered agent (deterministic fallback, M10).

func (*AgentRegistry) ListAgentIDs

func (r *AgentRegistry) ListAgentIDs() []string

ListAgentIDs returns all registered agent IDs.

func (*AgentRegistry) ResolveRoute

func (r *AgentRegistry) ResolveRoute(input routing.RouteInput) routing.ResolvedRoute

ResolveRoute determines which agent handles the message.

func (*AgentRegistry) SetDefaultAgentOverride

func (r *AgentRegistry) SetDefaultAgentOverride(agentID string)

SetDefaultAgentOverride sets the agent ID to use as the default agent. When set, GetDefaultAgent returns this agent instead of the "main" agent.

type ApprovalDecision

type ApprovalDecision struct {
	Verdict ApprovalVerdict `json:"verdict"`
	Reason  string          `json:"reason,omitempty"`
}

ApprovalDecision is the result of a ToolApprover.ApproveTool call. Use IsApproved() to check whether execution is permitted.

func Deny

func Deny(reason string) ApprovalDecision

Deny constructs a denial ApprovalDecision with a guaranteed non-empty reason.

func (ApprovalDecision) IsApproved

func (d ApprovalDecision) IsApproved() bool

IsApproved returns true when the verdict permits tool execution.

type ApprovalVerdict

type ApprovalVerdict string

ApprovalVerdict is a typed string enum for the outcome of a tool approval request.

const (
	// VerdictAllow approves the tool call for this invocation only.
	VerdictAllow ApprovalVerdict = "allow"
	// VerdictDeny denies the tool call.
	VerdictDeny ApprovalVerdict = "deny"
	// VerdictAlways approves the tool call and remembers the preference for this session.
	VerdictAlways ApprovalVerdict = "always"
)

type BackgroundProcessKillPayload

type BackgroundProcessKillPayload struct {
	PID             int
	MaxSeconds      int
	TerminatedClean bool
}

BackgroundProcessKillPayload describes a background process that was force-killed.

type BuiltinHookFactory

type BuiltinHookFactory func(ctx context.Context, spec config.BuiltinHookConfig) (any, error)

BuiltinHookFactory constructs an in-process hook from config.

type CancelCanceller

type CancelCanceller struct {
	UserID  string // e.g. "@alice", "user_abc123"
	Channel string // factory ID: "web" | "cli" | "telegram" | "slack" | ...
}

CancelCanceller is the identity of who issued the cancel. Used for audit attribution and abuse detection.

type CancelHooks

type CancelHooks struct {
	// SendStageFrame is called at each timer stage transition
	// (stage values: "graceful", "hard", "detached").
	SendStageFrame func(sessionID, stage string)

	// CancelPendingApprovals auto-denies pending approvals on the canceled
	// session (FR-7). Called once at graceful stage.
	CancelPendingApprovals func(sessionID, reason string)

	// SetSessionInterrupted updates the session meta.json Status to interrupted.
	// Called once at graceful stage.
	SetSessionInterrupted func(sessionID string)
}

CancelHooks lets callers inject transport-specific side-effects. All fields are optional; nil hooks are silently skipped.

type CancelOutcome

type CancelOutcome struct {
	Fired       bool     // true if a turn was actually targeted (ClaimCancel succeeded)
	Descendants []string // turn IDs canceled (parent + sub-turns)
	TurnID      string   // root turn ID; empty when Fired is false
}

CancelOutcome is returned to the caller after a cancel attempt.

type CancelScope

type CancelScope struct {
	SessionID string // non-empty → cancel the session directly
	Channel   string // Tier B: factory ID, e.g. "telegram"
	ChatID    string // Tier B: platform chat identifier
}

CancelScope identifies what to cancel. Exactly one of SessionID or (Channel + ChatID) must be set.

  • SessionID is preferred when known (web SPA, CLI, Tier A /cancel).
  • Channel + ChatID is used by Tier B channels that carry no SessionID; RequestCancel resolves the session internally by walking activeTurnStates.

type CompactionRetryPayload

type CompactionRetryPayload struct {
	DroppedMessages   int
	RemainingMessages int
}

CompactionRetryPayload describes context compaction triggered during a timeout recovery.

type ContextBuilder

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

func NewContextBuilder

func NewContextBuilder(workspace string) *ContextBuilder

func (*ContextBuilder) AddAssistantMessage

func (cb *ContextBuilder) AddAssistantMessage(
	messages []providers.Message,
	content string,
) []providers.Message

AddAssistantMessage appends an assistant message to the message slice. The toolCalls parameter was previously unused and has been removed (M5).

func (*ContextBuilder) AddToolResult

func (cb *ContextBuilder) AddToolResult(
	messages []providers.Message,
	toolCallID, toolName, result string,
) []providers.Message

func (*ContextBuilder) BuildMessages

func (cb *ContextBuilder) BuildMessages(
	history []providers.Message,
	summary string,
	currentMessage string,
	media []string,
	channel, chatID, senderID, senderDisplayName string,
	activeSkills ...string,
) []providers.Message

func (*ContextBuilder) BuildSystemPrompt

func (cb *ContextBuilder) BuildSystemPrompt() string

func (*ContextBuilder) BuildSystemPromptWithCache

func (cb *ContextBuilder) BuildSystemPromptWithCache() string

BuildSystemPromptWithCache returns the cached system prompt if available and source files haven't changed, otherwise builds and caches it. Source file changes are detected via mtime checks (cheap stat calls).

func (*ContextBuilder) EnvironmentProvider

func (cb *ContextBuilder) EnvironmentProvider() envcontext.Provider

EnvironmentProvider returns the currently-configured provider (may be nil). Exposed for subturn inheritance assertions and test doubles.

func (*ContextBuilder) GetEnvironmentContext

func (cb *ContextBuilder) GetEnvironmentContext() string

GetEnvironmentContext renders the env preamble for the current process/ agent. Returns an empty string when no provider is wired, which preserves legacy behavior and makes the parts[0] insertion a no-op in tests that do not exercise env awareness. The real body is installed by lane E via envcontext.Render.

func (*ContextBuilder) GetSkillsInfo

func (cb *ContextBuilder) GetSkillsInfo() map[string]any

GetSkillsInfo returns information about loaded skills.

func (*ContextBuilder) InvalidateCache

func (cb *ContextBuilder) InvalidateCache()

InvalidateCache clears the cached system prompt. Normally not needed because the cache auto-invalidates via mtime checks, but this is useful for tests or explicit reload commands.

func (*ContextBuilder) ListSkillNames

func (cb *ContextBuilder) ListSkillNames() []string

func (*ContextBuilder) LoadAgentDefinition

func (cb *ContextBuilder) LoadAgentDefinition() AgentContextDefinition

LoadAgentDefinition parses the workspace agent bootstrap files.

It prefers the new AGENT.md format and its paired SOUL.md file. When the structured files are absent, it falls back to the legacy AGENTS.md layout so the current runtime can transition incrementally.

func (*ContextBuilder) LoadBootstrapFiles

func (cb *ContextBuilder) LoadBootstrapFiles() string

LoadBootstrapFiles loads the bootstrap files using a fresh LoadAgentDefinition call. Callers that already hold an AgentContextDefinition should use loadBootstrapFilesWithDef to avoid a redundant disk read.

func (*ContextBuilder) Memory

func (cb *ContextBuilder) Memory() *MemoryStore

Memory returns the MemoryStore backing this ContextBuilder. Used by NewAgentInstance to wire memory tools with the same store instance.

func (*ContextBuilder) ResolveSkillName

func (cb *ContextBuilder) ResolveSkillName(name string) (string, bool)

func (*ContextBuilder) WithAgentInfo

func (cb *ContextBuilder) WithAgentInfo(id, name string) *ContextBuilder

func (*ContextBuilder) WithEnvironmentProvider

func (cb *ContextBuilder) WithEnvironmentProvider(p envcontext.Provider) *ContextBuilder

WithEnvironmentProvider wires an envcontext.Provider into the builder so BuildSystemPrompt renders the ## Environment preamble as parts[0]. Fix A (FR-057). Subagents share the same ContextBuilder pointer as their parent, so any provider set here is inherited automatically (see subturn.go).

func (*ContextBuilder) WithResourcesInjector

func (cb *ContextBuilder) WithResourcesInjector(fn func() string) *ContextBuilder

WithResourcesInjector sets a callback that provides additional context sections to inject into the system prompt (e.g., available tools catalog for Ava).

func (*ContextBuilder) WithSplitOnMarker

func (cb *ContextBuilder) WithSplitOnMarker(enabled bool) *ContextBuilder

func (*ContextBuilder) WithToolDiscovery

func (cb *ContextBuilder) WithToolDiscovery(useBM25, useRegex bool) *ContextBuilder

type ContextBuilderRegistry

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

ContextBuilderRegistry is a thin, concurrency-safe registry that maps an agent ID to its ContextBuilder. It is the broadcast channel for config-change invalidation (FR-061): when an operator writes a sandbox or dev-mode-bypass setting, the REST handler calls InvalidateAllContextBuilders so every agent's next turn picks up the new preamble without a restart.

Stale entries (agents that have been deleted but whose Unregister was never called) are harmless: InvalidateCache on a no-longer-used ContextBuilder is a cheap no-op.

func NewContextBuilderRegistry

func NewContextBuilderRegistry() *ContextBuilderRegistry

NewContextBuilderRegistry returns an empty, ready-to-use registry.

func (*ContextBuilderRegistry) InvalidateAllContextBuilders

func (r *ContextBuilderRegistry) InvalidateAllContextBuilders()

InvalidateAllContextBuilders iterates every registered ContextBuilder and calls InvalidateCache on each. This forces a full rebuild of the system prompt (including the env preamble) on the next turn. The iteration is lock-free (sync.Map.Range) and safe for concurrent reads and writes.

func (*ContextBuilderRegistry) Register

func (r *ContextBuilderRegistry) Register(agentID string, cb *ContextBuilder)

Register associates agentID with cb. Replaces any existing entry for the same ID (e.g. after a hot-reload creates a new ContextBuilder for an existing agent).

func (*ContextBuilderRegistry) Unregister

func (r *ContextBuilderRegistry) Unregister(agentID string)

Unregister removes the entry for agentID. Safe to call when the entry does not exist.

type ContextCompressPayload

type ContextCompressPayload struct {
	Reason            ContextCompressReason
	DroppedMessages   int
	RemainingMessages int
}

ContextCompressPayload describes a forced history compression.

type ContextCompressReason

type ContextCompressReason string

ContextCompressReason identifies why emergency compression ran.

const (
	// ContextCompressReasonProactive indicates compression before the first LLM call.
	ContextCompressReasonProactive ContextCompressReason = "proactive_budget"
	// ContextCompressReasonRetry indicates compression during context-error retry handling.
	ContextCompressReasonRetry ContextCompressReason = "llm_retry"
)

type EmptyResponseRetryPayload

type EmptyResponseRetryPayload struct {
	Attempt    int
	MaxRetries int
}

EmptyResponseRetryPayload describes a retry triggered by an empty LLM response.

type ErrorPayload

type ErrorPayload struct {
	Stage   string
	Message string
}

ErrorPayload describes an execution error inside the agent loop.

type Event

type Event struct {
	Kind    EventKind `json:"Kind"`
	Time    time.Time `json:"Time"`
	Meta    EventMeta `json:"Meta"`
	Payload any       `json:"Payload"`
}

Event is the structured envelope broadcast by the agent EventBus.

type EventBus

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

EventBus is a lightweight multi-subscriber broadcaster for agent-loop events.

func NewEventBus

func NewEventBus() *EventBus

NewEventBus creates a new in-process event broadcaster.

func (*EventBus) Close

func (b *EventBus) Close()

Close closes all subscriber channels and stops future broadcasts.

func (*EventBus) Dropped

func (b *EventBus) Dropped(kind EventKind) int64

Dropped returns the number of dropped events for a given kind.

func (*EventBus) Emit

func (b *EventBus) Emit(evt Event)

Emit broadcasts an event to all current subscribers without blocking. When a subscriber channel is full, the event is dropped for that subscriber.

func (*EventBus) Subscribe

func (b *EventBus) Subscribe(buffer int) EventSubscription

Subscribe registers a new subscriber with the requested channel buffer size. A non-positive buffer uses the default size.

func (*EventBus) Unsubscribe

func (b *EventBus) Unsubscribe(id uint64)

Unsubscribe removes a subscriber and closes its channel.

type EventKind

type EventKind uint8

EventKind identifies a structured agent-loop event.

MarshalJSON uses a value receiver and UnmarshalJSON uses a pointer receiver — the standard Go JSON codec pair. recvcheck is suppressed here because MarshalJSON cannot use a pointer receiver without breaking fmt.Stringer for value instances (e.g. range-loop variables).

const (
	// EventKindTurnStart is emitted when a turn begins processing.
	EventKindTurnStart EventKind = iota
	// EventKindTurnEnd is emitted when a turn finishes, successfully or with an error.
	EventKindTurnEnd
	// EventKindLLMRequest is emitted before a provider chat request is made.
	EventKindLLMRequest
	// EventKindLLMDelta is emitted when a streaming provider yields a partial delta.
	EventKindLLMDelta
	// EventKindLLMResponse is emitted after a provider chat response is received.
	EventKindLLMResponse
	// EventKindLLMRetry is emitted when an LLM request is retried.
	EventKindLLMRetry
	// EventKindContextCompress is emitted when session history is forcibly compressed.
	EventKindContextCompress
	// EventKindSessionSummarize is emitted when asynchronous summarization completes.
	EventKindSessionSummarize
	// EventKindToolExecStart is emitted immediately before a tool executes.
	EventKindToolExecStart
	// EventKindToolExecEnd is emitted immediately after a tool finishes executing.
	EventKindToolExecEnd
	// EventKindToolExecSkipped is emitted when a queued tool call is skipped.
	EventKindToolExecSkipped
	// EventKindSteeringInjected is emitted when queued steering is injected into context.
	EventKindSteeringInjected
	// EventKindFollowUpQueued is emitted when an async tool queues a follow-up system message.
	EventKindFollowUpQueued
	// EventKindInterruptReceived is emitted when a soft interrupt message is accepted.
	EventKindInterruptReceived
	// EventKindSubTurnSpawn is emitted when a sub-turn is spawned.
	EventKindSubTurnSpawn
	// EventKindSubTurnEnd is emitted when a sub-turn finishes.
	EventKindSubTurnEnd
	// EventKindSubTurnResultDelivered is emitted when a sub-turn result is delivered.
	EventKindSubTurnResultDelivered
	// EventKindSubTurnOrphan is emitted when a sub-turn result cannot be delivered.
	EventKindSubTurnOrphan
	// EventKindError is emitted when a turn encounters an execution error.
	EventKindError
	// EventKindTurnTimeout is emitted when a turn exceeds its configured timeout.
	EventKindTurnTimeout
	// EventKindEmptyResponseRetry is emitted when the LLM returns an empty response and a retry is attempted.
	EventKindEmptyResponseRetry
	// EventKindCompactionRetry is emitted when context compaction is triggered due to a timeout.
	EventKindCompactionRetry
	// EventKindBackgroundProcessKill is emitted when a background process is force-killed after exceeding its timeout.
	EventKindBackgroundProcessKill
	// EventKindRateLimit is emitted when an agent LLM or tool call is denied by a rate limit (SEC-26).
	EventKindRateLimit
	// EventKindWhatsAppPairing is emitted when the WhatsApp native channel produces
	// a linked-device pairing update (QR code or status) to surface in the SPA (#283).
	EventKindWhatsAppPairing
	// EventKindNotification is emitted when a user-facing notification is raised
	// (e.g. a scheduled run failed). Delivered live only to the recipient user's
	// WebSocket connections (#264).
	EventKindNotification
)

func (EventKind) MarshalJSON

func (k EventKind) MarshalJSON() ([]byte, error)

MarshalJSON emits the canonical string form of an EventKind so that subprocess hooks (and any other JSON consumer) see e.g. "tool_exec_start" rather than the underlying uint8 index 8. Without this, the wire payload for hook.event notifications was an integer that subprocess authors had to map manually — and the index would silently shift if a new EventKind was added in the middle of the enum.

Regression guard for #164.

func (EventKind) String

func (k EventKind) String() string

String returns the stable string form of an EventKind.

func (*EventKind) UnmarshalJSON

func (k *EventKind) UnmarshalJSON(data []byte) error

UnmarshalJSON parses the canonical string form back into an EventKind. Round-trips with MarshalJSON. Returns an error for unknown names so a typo or stale schema gets surfaced instead of silently mapping to EventKindTurnStart (the zero value).

type EventMeta

type EventMeta struct {
	AgentID      string `json:"AgentID"`
	TurnID       string `json:"TurnID"`
	ParentTurnID string `json:"ParentTurnID"`
	SessionKey   string `json:"SessionKey"`
	Iteration    int    `json:"Iteration"`
	TracePath    string `json:"TracePath"`
	Source       string `json:"Source"`
}

EventMeta contains correlation fields shared by all agent-loop events.

type EventObserver

type EventObserver interface {
	OnEvent(ctx context.Context, evt Event) error
}

type EventSubscription

type EventSubscription struct {
	ID uint64
	C  <-chan Event
}

EventSubscription identifies a subscriber channel returned by EventBus.Subscribe.

type FollowUpQueuedPayload

type FollowUpQueuedPayload struct {
	SourceTool string
	Channel    string
	ChatID     string
	ContentLen int
}

FollowUpQueuedPayload describes an async follow-up queued back into the inbound bus.

type HookAction

type HookAction string
const (
	HookActionContinue  HookAction = "continue"
	HookActionModify    HookAction = "modify"
	HookActionDenyTool  HookAction = "deny_tool"
	HookActionAbortTurn HookAction = "abort_turn"
	HookActionHardAbort HookAction = "hard_abort"
)

type HookDecision

type HookDecision struct {
	Action HookAction `json:"action"`
	Reason string     `json:"reason,omitempty"`
}

type HookManager

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

func NewHookManager

func NewHookManager(eventBus *EventBus) *HookManager

func (*HookManager) AfterLLM

func (*HookManager) AfterTool

func (*HookManager) ApproveTool

func (hm *HookManager) ApproveTool(ctx context.Context, req *ToolApprovalRequest) ApprovalDecision

func (*HookManager) BeforeLLM

func (*HookManager) BeforeTool

func (*HookManager) Close

func (hm *HookManager) Close()

func (*HookManager) ConfigureTimeouts

func (hm *HookManager) ConfigureTimeouts(observer, interceptor, approval time.Duration)

func (*HookManager) Mount

func (hm *HookManager) Mount(reg HookRegistration) error

func (*HookManager) Unmount

func (hm *HookManager) Unmount(name string)

type HookRegistration

type HookRegistration struct {
	Name     string
	Priority int
	Source   HookSource
	Hook     any
}

func NamedHook

func NamedHook(name string, hook any) HookRegistration

type HookSource

type HookSource uint8
const (
	HookSourceInProcess HookSource = iota
	HookSourceProcess
)

type InterruptKind

type InterruptKind string
const (
	InterruptKindSteering InterruptKind = "steering"
	InterruptKindGraceful InterruptKind = "graceful"
	InterruptKindHard     InterruptKind = "hard_abort"
)

type InterruptReceivedPayload

type InterruptReceivedPayload struct {
	Kind       InterruptKind
	Role       string
	ContentLen int
	QueueDepth int
	HintLen    int
}

InterruptReceivedPayload describes accepted turn-control input.

type LLMDeltaPayload

type LLMDeltaPayload struct {
	ContentDeltaLen   int
	ReasoningDeltaLen int
}

LLMDeltaPayload describes a streamed LLM delta.

type LLMHookRequest

type LLMHookRequest struct {
	Meta             EventMeta                  `json:"meta"`
	Model            string                     `json:"model"`
	Messages         []providers.Message        `json:"messages,omitempty"`
	Tools            []providers.ToolDefinition `json:"tools,omitempty"`
	Options          map[string]any             `json:"options,omitempty"`
	Channel          string                     `json:"channel,omitempty"`
	ChatID           string                     `json:"chat_id,omitempty"`
	GracefulTerminal bool                       `json:"graceful_terminal,omitempty"`
}

func (*LLMHookRequest) Clone

func (r *LLMHookRequest) Clone() *LLMHookRequest

type LLMHookResponse

type LLMHookResponse struct {
	Meta     EventMeta              `json:"meta"`
	Model    string                 `json:"model"`
	Response *providers.LLMResponse `json:"response,omitempty"`
	Channel  string                 `json:"channel,omitempty"`
	ChatID   string                 `json:"chat_id,omitempty"`
}

func (*LLMHookResponse) Clone

func (r *LLMHookResponse) Clone() *LLMHookResponse

type LLMInterceptor

type LLMInterceptor interface {
	BeforeLLM(ctx context.Context, req *LLMHookRequest) (*LLMHookRequest, HookDecision, error)
	AfterLLM(ctx context.Context, resp *LLMHookResponse) (*LLMHookResponse, HookDecision, error)
}

type LLMRequestPayload

type LLMRequestPayload struct {
	Model         string
	MessagesCount int
	ToolsCount    int
	MaxTokens     int
	Temperature   float64
}

LLMRequestPayload describes an outbound LLM request.

type LLMResponsePayload

type LLMResponsePayload struct {
	ContentLen   int
	ToolCalls    int
	HasReasoning bool
}

LLMResponsePayload describes an inbound LLM response.

type LLMRetryPayload

type LLMRetryPayload struct {
	Attempt    int
	MaxRetries int
	Reason     string
	Error      string
	Backoff    time.Duration
}

LLMRetryPayload describes a retry of an LLM request.

type LongTermEntry

type LongTermEntry struct {
	Timestamp time.Time
	Category  MemoryCategory
	Content   string
}

LongTermEntry is a single parsed entry from MEMORY.md.

type MemoryCategory

type MemoryCategory string

MemoryCategory is the closed set of categories an agent may tag a long-term memory entry with. Keeping it typed (rather than a free-form string) makes the domain explicit at every call site and catches drift at compile time.

const (
	CategoryKeyDecision   MemoryCategory = "key_decision"
	CategoryReference     MemoryCategory = "reference"
	CategoryLessonLearned MemoryCategory = "lesson_learned"
	// CategoryLegacy is assigned to entries parsed from pre-structured
	// MEMORY.md files (no ts/cat header). Not valid on write.
	CategoryLegacy MemoryCategory = "legacy"
	// CategoryLastSession / CategoryRetro are synthetic categories applied
	// to entries surfaced through SearchEntries from non-MEMORY.md sources.
	CategoryLastSession MemoryCategory = "last_session"
	CategoryRetro       MemoryCategory = "retro"
)

func ParseMemoryCategory

func ParseMemoryCategory(s string) (MemoryCategory, error)

ParseMemoryCategory validates and returns a typed category from a string. Accepts only the three AppendLongTerm-legal values; anything else is an error so callers can't silently persist "garbage" as cat=garbage.

type MemoryStore

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

MemoryStore manages persistent memory for the agent. - Long-term memory: memory/MEMORY.md - Daily notes: memory/YYYYMM/YYYYMMDD.md - Last session: memory/sessions/LAST_SESSION.md - Retrospectives: memory/sessions/YYYY-MM-DD/<sessionID>_retro.md

func NewMemoryStore

func NewMemoryStore(workspace string) *MemoryStore

NewMemoryStore creates a new MemoryStore with the given workspace path. It ensures the memory directory exists.

func (*MemoryStore) AppendLongTerm

func (ms *MemoryStore) AppendLongTerm(content, category string) error

AppendLongTerm appends a new entry to MEMORY.md under advisory flock. FR-001: category must be one of key_decision | reference | lesson_learned. FR-002: content must be non-empty, ≤ 4096 runes, and must not contain "<!--". FR-003: NUL bytes are stripped silently.

func (*MemoryStore) AppendRetro

func (ms *MemoryStore) AppendRetro(sessionID string, r Retro) error

AppendRetro writes a structured retrospective to memory/sessions/<YYYY-MM-DD>/<sessionID>_retro.md. FR-009: uses advisory flock. sessionID is validated via validation.EntityID.

func (*MemoryStore) AppendToday

func (ms *MemoryStore) AppendToday(content string) error

AppendToday appends content to today's daily note. If the file doesn't exist, it creates a new file with a date header.

func (*MemoryStore) GetMemoryContext

func (ms *MemoryStore) GetMemoryContext() string

GetMemoryContext returns formatted memory context for the agent system prompt. FR-019: includes LAST_SESSION.md before long-term memory. FR-020: budgets MEMORY.md content at 12000 runes; falls back to newest N entries.

func (*MemoryStore) GetRecentDailyNotes

func (ms *MemoryStore) GetRecentDailyNotes(days int) string

GetRecentDailyNotes returns daily notes from the last N days. Contents are joined with "---" separator.

func (*MemoryStore) ReadLastSession

func (ms *MemoryStore) ReadLastSession() (string, error)

ReadLastSession returns the contents of LAST_SESSION.md, or empty string if absent. FR-008.

func (*MemoryStore) ReadLongTerm

func (ms *MemoryStore) ReadLongTerm() string

ReadLongTerm reads the long-term memory (MEMORY.md). Returns empty string if the file doesn't exist.

func (*MemoryStore) ReadLongTermEntries

func (ms *MemoryStore) ReadLongTermEntries() ([]LongTermEntry, error)

ReadLongTermEntries parses MEMORY.md into typed LongTermEntry values. Results are cached mtime-keyed; the cache is reused when the file has not changed. FR-004: returns newest-first. FR-006: legacy MEMORY.md (no separators) → single entry with cat=legacy, ts=<file mtime>.

func (*MemoryStore) ReadRetros

func (ms *MemoryStore) ReadRetros(daysBack int) ([]Retro, error)

ReadRetros returns structured Retro records from the last daysBack days. Clamps daysBack to 1..365. Files that don't parse are silently skipped. FR-010.

func (*MemoryStore) ReadToday

func (ms *MemoryStore) ReadToday() string

ReadToday reads today's daily note. Returns empty string if the file doesn't exist.

func (*MemoryStore) SearchEntries

func (ms *MemoryStore) SearchEntries(query string, limit int) ([]LongTermEntry, error)

SearchEntries performs a case-insensitive literal substring search across: - MEMORY.md entries - LAST_SESSION.md (as a single entry with cat=last_session) - retrospectives from the last 30 days Results are newest-first. limit defaults to 20 if ≤ 0, max 50. FR-005: no regex — literal substring match only.

func (*MemoryStore) SweepRetros

func (ms *MemoryStore) SweepRetros(retentionDays int) (int, error)

SweepRetros deletes retro files whose enclosing date directory is older than retentionDays days. Returns the count of deleted files. FR-031.

func (*MemoryStore) WriteLastSession

func (ms *MemoryStore) WriteLastSession(content string) error

WriteLastSession atomically writes content to memory/sessions/LAST_SESSION.md. FR-007.

func (*MemoryStore) WriteLongTerm

func (ms *MemoryStore) WriteLongTerm(content string) error

WriteLongTerm writes content to the long-term memory file (MEMORY.md).

type MemoryStoreAdapter

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

MemoryStoreAdapter wraps *MemoryStore to implement tools.MemoryAccess. The adapter converts between the agent-side types (LongTermEntry, Retro) and the tools-side mirror types (tools.MemoryEntry, tools.MemoryRetro), keeping pkg/agent and pkg/tools import-cycle-free.

func NewMemoryStoreAdapter

func NewMemoryStoreAdapter(ms *MemoryStore) *MemoryStoreAdapter

NewMemoryStoreAdapter wraps ms in a tools.MemoryAccess implementation.

func (*MemoryStoreAdapter) AppendLongTerm

func (a *MemoryStoreAdapter) AppendLongTerm(content, category string) error

AppendLongTerm delegates to MemoryStore.AppendLongTerm (MemoryWriter).

func (*MemoryStoreAdapter) AppendRetro

func (a *MemoryStoreAdapter) AppendRetro(sessionID string, r tools.MemoryRetro) error

AppendRetro converts tools.MemoryRetro to agent.Retro and delegates. The Trigger field crosses a type boundary here: pkg/tools sees it as a free string (mirror-struct to avoid cycles), pkg/agent keeps it typed so a future refactor can add exhaustive-switch checks on triggers.

func (*MemoryStoreAdapter) SearchEntries

func (a *MemoryStoreAdapter) SearchEntries(query string, limit int) ([]tools.MemoryEntry, error)

SearchEntries delegates to MemoryStore.SearchEntries and converts results. Category crosses the same type boundary as Trigger in AppendRetro.

type NotificationPayload

type NotificationPayload struct {
	// Recipient is the username the notification is for, or
	// NotificationAdminBroadcast to fan out to all admins.
	Recipient        string `json:"recipient"`
	ID               string `json:"id"`
	NotificationType string `json:"notification_type"`
	Title            string `json:"title"`
	Body             string `json:"body,omitempty"`
	Severity         string `json:"severity"`
	Read             bool   `json:"read"`
	CreatedAtMs      int64  `json:"created_at_ms"`
	ScheduleID       string `json:"schedule_id,omitempty"`
	SessionID        string `json:"session_id,omitempty"`
	AgentID          string `json:"agent_id,omitempty"`
}

NotificationPayload carries a user-facing notification for the live WS push (#264). It is delivered ONLY to connections whose userID equals Recipient (or, when Recipient == NotificationAdminBroadcast, to admin-role connections).

type PolicyApprovalReq

type PolicyApprovalReq struct {
	ToolCallID    string
	ToolName      string
	Args          map[string]any
	AgentID       string
	SessionID     string
	TurnID        string
	RequiresAdmin bool
}

PolicyApprovalReq carries the fields needed to create and broadcast an approval.

type PolicyApprover

type PolicyApprover interface {
	RequestApproval(ctx context.Context, req PolicyApprovalReq) (approved bool, denialReason string)
}

PolicyApprover is implemented by the gateway to wire the central approval registry and WebSocket broadcast into the agent loop (FR-011, FR-082).

This is distinct from the hooks.ToolApprover interface (which is for hook-based interactive approval). PolicyApprover governs the policy-level ask gate from FilterToolsByPolicy, using the in-process approvalRegistryV2.

RequestApproval MUST:

  1. Create a pending approval entry in the registry.
  2. Emit a tool_approval_required WS frame (scoped to session owner, FR-073).
  3. Block until the user approves/denies, the timeout fires, the queue is saturated, or the gateway shuts down.
  4. Return (true, "") on approve; (false, reason) otherwise.

denialReason matches the Reason field from ApprovalOutcome: "user", "timeout", "saturated", "restart", "cancel", "batch_short_circuit".

type ProcessHook

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

func NewProcessHook

func NewProcessHook(ctx context.Context, name string, opts ProcessHookOptions) (*ProcessHook, error)

func (*ProcessHook) AfterLLM

func (ph *ProcessHook) AfterLLM(
	ctx context.Context,
	resp *LLMHookResponse,
) (*LLMHookResponse, HookDecision, error)

func (*ProcessHook) AfterTool

func (*ProcessHook) ApproveTool

func (ph *ProcessHook) ApproveTool(ctx context.Context, req *ToolApprovalRequest) (ApprovalDecision, error)

func (*ProcessHook) BeforeLLM

func (ph *ProcessHook) BeforeLLM(
	ctx context.Context,
	req *LLMHookRequest,
) (*LLMHookRequest, HookDecision, error)

func (*ProcessHook) BeforeTool

func (*ProcessHook) Close

func (ph *ProcessHook) Close() error

func (*ProcessHook) OnEvent

func (ph *ProcessHook) OnEvent(ctx context.Context, evt Event) error

type ProcessHookOptions

type ProcessHookOptions struct {
	Command       []string
	Dir           string
	Env           []string
	Observe       bool
	ObserveKinds  []string
	InterceptLLM  bool
	InterceptTool bool
	ApproveTool   bool
}

type RateLimitPayload

type RateLimitPayload struct {
	Scope             string  `json:"scope"`
	Resource          string  `json:"resource"` // "llm_call" or "tool_call"
	PolicyRule        string  `json:"policy_rule"`
	RetryAfterSeconds float64 `json:"retry_after_seconds"`
	AgentID           string  `json:"agent_id,omitempty"`
	ChatID            string  `json:"chat_id,omitempty"`
	Tool              string  `json:"tool,omitempty"`
}

RateLimitPayload describes a rate-limit denial for an LLM or tool call (SEC-26). ChatID is required so the WebSocket event forwarder can route the frame to the correct connection via matchesChatID — a rate-limit denial is meaningless without the chat context it applies to.

type RecapModelBootError

type RecapModelBootError struct {
	Model     string
	AllowList []string
}

RecapModelBootError is returned by NewAgentLoop when AutoRecapEnabled is true and the resolved recap model is not in the cheap-model allow-list (FR-029a). Callers should map this to a non-zero exit code and log the message.

func (*RecapModelBootError) Error

func (e *RecapModelBootError) Error() string

type RecapTrigger

type RecapTrigger string

RecapTrigger is the closed set of triggers recorded on a Retro. Keeping this typed means a future refactor cannot quietly introduce a fourth source without the type system noticing.

const (
	TriggerExplicit  RecapTrigger = "explicit"
	TriggerLazy      RecapTrigger = "lazy"
	TriggerIdle      RecapTrigger = "idle"
	TriggerBootstrap RecapTrigger = "bootstrap"
	TriggerJoined    RecapTrigger = "joined"
)

type Retro

type Retro struct {
	Timestamp        time.Time
	Trigger          RecapTrigger
	Fallback         bool
	FallbackReason   string
	Recap            string
	WentWell         []string
	NeedsImprovement []string
}

Retro is a structured retrospective record.

type ScheduledJobInfo

type ScheduledJobInfo struct {
	JobID   string
	JobName string
}

ScheduledJobInfo carries the schedule/job identity that ProcessScheduled callers inject into the run context via WithScheduledJobContext. The auto-deny path reads it so the emitted audit entry names the responsible schedule (F-13 / O-3 observability requirement, issue #342).

type ServedEntry

type ServedEntry struct {
	// AgentID is the ID of the agent that owns this registration.
	AgentID string
	// AbsDir is the canonicalised absolute path to the served directory.
	// It is within the agent's workspace (validated by the web_serve tool).
	AbsDir string
	// Deadline is when this registration expires.
	Deadline time.Time
}

ServedEntry holds a single web_serve static-mode registration.

type ServedSubdirs

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

ServedSubdirs is the process-wide registry of active web_serve static-mode registrations. The zero value is not usable — call NewServedSubdirs.

func NewServedSubdirs

func NewServedSubdirs() *ServedSubdirs

NewServedSubdirs creates a registry and launches the 30-second janitor. Call Stop when the gateway shuts down so the goroutine exits cleanly.

func (*ServedSubdirs) ActiveForAgent

func (s *ServedSubdirs) ActiveForAgent(agentID string) (token string, deadline time.Time, ok bool)

ActiveForAgent returns the token and deadline of the currently active registration for agentID, and ok=true if one exists and has not expired. Returns ("", zero, false) otherwise.

func (*ServedSubdirs) Evict

func (s *ServedSubdirs) Evict(agentID string)

Evict removes all registrations for the given agentID. Called on agent deletion so URLs stop resolving immediately rather than waiting for the janitor.

func (*ServedSubdirs) Lookup

func (s *ServedSubdirs) Lookup(token string) *ServedEntry

Lookup returns the ServedEntry for the given token, or nil if the token is unknown or expired. An expired-but-not-yet-janitor-cleaned entry is treated as missing so callers always see consistent state.

func (*ServedSubdirs) Register

func (s *ServedSubdirs) Register(
	agentID, absDir string,
	duration time.Duration,
) (token string, deadline time.Time, err error)

Register creates a new web_serve static-mode registration for agentID pointing at absDir with a lifetime of duration. Any previous registration for agentID is atomically replaced (per-agent cap).

Returns the token (for embedding in the URL) and the registration's expiry time.

func (*ServedSubdirs) SetOnEvict

func (s *ServedSubdirs) SetOnEvict(fn func(tokens []string))

SetOnEvict installs a callback that is invoked (outside the registry lock) whenever tokens are evicted. Gateway wires this to purgeFirstServedTokensBulk so the audit firstServedTokens set stays in sync with the registry (F-9). Must be called before any concurrent access.

func (*ServedSubdirs) Stop

func (s *ServedSubdirs) Stop()

Stop signals the janitor goroutine to exit and waits for it to return. Safe to call multiple times (subsequent calls are no-ops — stopCh is already closed and the janitor has already exited).

type SessionSummarizePayload

type SessionSummarizePayload struct {
	SummarizedMessages int
	KeptMessages       int
	SummaryLen         int
	OmittedOversized   bool
}

SessionSummarizePayload describes a completed async session summarization.

type SoulDefinition

type SoulDefinition struct {
	Path    string `json:"path"`
	Content string `json:"content"`
}

SoulDefinition represents the resolved SOUL.md file linked to the agent.

type SteeringInjectedPayload

type SteeringInjectedPayload struct {
	Count           int
	TotalContentLen int
}

SteeringInjectedPayload describes steering messages appended before the next LLM call.

type SteeringMode

type SteeringMode string

SteeringMode controls how queued steering messages are dequeued.

const (
	// SteeringOneAtATime dequeues only the first queued message per poll.
	SteeringOneAtATime SteeringMode = "one-at-a-time"
	// SteeringAll drains the entire queue in a single poll.
	SteeringAll SteeringMode = "all"
	// MaxQueueSize number of possible messages in the Steering Queue
	MaxQueueSize = 10
)

type SubTurnConfig

type SubTurnConfig struct {
	Model        string
	Tools        []tools.Tool
	SystemPrompt string
	MaxTokens    int

	// Async controls the result delivery mechanism:
	//
	// When Async = false (synchronous sub-turn):
	//   - The caller blocks until the sub-turn completes
	//   - The result is ONLY returned via the function return value
	//   - The result is NOT delivered to the parent's pendingResults channel
	//   - This prevents double delivery: caller gets result immediately, no need for channel
	//   - Use case: When the caller needs the result immediately to continue execution
	//   - Example: A tool that needs to process the sub-turn result before returning
	//
	// When Async = true (asynchronous sub-turn):
	//   - The sub-turn runs in the background (still blocks the caller, but semantically async)
	//   - The result is delivered to the parent's pendingResults channel
	//   - The result is ALSO returned via the function return value (for consistency)
	//   - The parent turn can poll pendingResults in later iterations to process results
	//   - Use case: Fire-and-forget operations, or when results are processed in batches
	//   - Example: Spawning multiple sub-turns in parallel and collecting results later
	//
	// IMPORTANT: The Async flag does NOT make the call non-blocking. It only controls
	// whether the result is delivered via the channel. For true non-blocking execution,
	// the caller must spawn the sub-turn in a separate goroutine.
	Async bool

	// Critical indicates this SubTurn's result is important and should continue
	// running even after the parent turn finishes gracefully.
	//
	// When parent finishes gracefully (Finish(false)):
	//   - Critical=true: SubTurn continues running, delivers result as orphan
	//   - Critical=false: SubTurn exits gracefully without error
	//
	// When parent finishes with hard abort (Finish(true)):
	//   - All SubTurns are canceled regardless of Critical flag
	Critical bool

	// Timeout is the maximum duration for this SubTurn.
	// If the SubTurn runs longer than this, it will be canceled.
	// Default is 5 minutes (defaultSubTurnTimeout) if not specified.
	Timeout time.Duration

	// MaxContextRunes limits the context size (in runes) passed to the SubTurn.
	// This prevents context window overflow by truncating message history before LLM calls.
	//
	// Values:
	//   0  = Auto-calculate based on model's ContextWindow * 0.75 (default, recommended)
	//   -1 = No limit (disable soft truncation, rely only on hard context errors)
	//   >0 = Use specified rune limit
	//
	// The soft limit acts as a first line of defense before hitting the provider's
	// hard context window limit. When exceeded, older messages are intelligently
	// truncated while preserving system messages and recent context.
	MaxContextRunes int

	// ActualSystemPrompt is injected as the true 'system' role message for the childAgent.
	// The legacy SystemPrompt field is actually used as the first 'user' message (task description).
	ActualSystemPrompt string

	// InitialMessages preloads the ephemeral session history before the agent loop starts.
	// Used by evaluator-optimizer patterns to pass the full worker context across multiple iterations.
	InitialMessages []providers.Message

	// InitialTokenBudget is a shared atomic counter for tracking remaining tokens.
	// If set, the SubTurn will inherit this budget and deduct tokens after each LLM call.
	// If nil, the SubTurn will inherit the parent's tokenBudget (if any).
	// Used by team tool to enforce token limits across all team members.
	InitialTokenBudget *atomic.Int64

	// TaskLabel is the optional human-readable label for the sub-turn task.
	// Populated by the spawn tool from its "label" argument (FR-H-004).
	// Used in SubTurnSpawnPayload.TaskLabel for the WS subagent_start frame.
	TaskLabel string
}

SubTurnConfig configures the execution of a child sub-turn.

Usage Examples:

Synchronous sub-turn (Async=false):

cfg := SubTurnConfig{
    Model: "gpt-4o-mini",
    SystemPrompt: "Analyze this code",
    Async: false,  // Result returned immediately
}
result, err := SpawnSubTurn(ctx, cfg)
// Use result directly here
processResult(result)

Asynchronous sub-turn (Async=true):

cfg := SubTurnConfig{
    Model: "gpt-4o-mini",
    SystemPrompt: "Background analysis",
    Async: true,  // Result delivered to channel
}
result, err := SpawnSubTurn(ctx, cfg)
// Result also available in parent's pendingResults channel
// Parent turn will poll and process it in a later iteration

type SubTurnEndPayload

type SubTurnEndPayload struct {
	AgentID string
	Status  SubTurnStatus
	// SpanID is "span_" + ParentSpawnCallID, matching the corresponding SubTurnSpawnPayload.
	SpanID string
	// ParentSpawnCallID is the ToolCall.ID of the spawn tool call that triggered this sub-turn.
	ParentSpawnCallID session.ToolCallID
	// DurationMS is the wall-clock duration of the sub-turn in milliseconds.
	DurationMS int64
	// ChatID is needed so the WS forwarder can route this event to the right connection.
	ChatID string
	// SessionID is the transcript-store session ID for this turn.
	SessionID string
}

SubTurnEndPayload describes the completion of a child turn. FR-H-004: carries span_id, status, duration_ms for the WS forwarder.

type SubTurnOrphanPayload

type SubTurnOrphanPayload struct {
	ParentTurnID string
	ChildTurnID  string
	Reason       string
}

SubTurnOrphanPayload describes a sub-turn result that could not be delivered.

type SubTurnResultDeliveredPayload

type SubTurnResultDeliveredPayload struct {
	TargetChannel string
	TargetChatID  string
	ContentLen    int
}

SubTurnResultDeliveredPayload describes delivery of a sub-turn result.

type SubTurnSpawnPayload

type SubTurnSpawnPayload struct {
	AgentID      string
	Label        string
	ParentTurnID string
	// SpanID is "span_" + ParentSpawnCallID (deterministic, derivable from persisted data).
	SpanID string
	// ParentSpawnCallID is the ToolCall.ID of the spawn tool call that triggered this sub-turn.
	// This is the correlation anchor for the subagent span.
	ParentSpawnCallID session.ToolCallID
	// TaskLabel is the human-readable label for the sub-turn task (from spawn tool's label param).
	TaskLabel string
	// ChatID is needed so the WS forwarder can route this event to the right connection.
	ChatID string
	// SessionID is the transcript-store session ID for this turn.
	SessionID string
}

SubTurnSpawnPayload describes the creation of a child turn. FR-H-004: carries span_id, parent_call_id, task_label, agent_id for the WS forwarder.

type SubTurnStatus

type SubTurnStatus string

SubTurnStatus describes the terminal state of a sub-turn. Using a named type prevents accidental use of arbitrary strings at call sites. JSON marshaling is identical to a plain string.

const (
	// SubTurnStatusSuccess indicates the sub-turn completed normally.
	SubTurnStatusSuccess SubTurnStatus = "success"
	// SubTurnStatusError indicates the sub-turn ended with an error.
	SubTurnStatusError SubTurnStatus = "error"
	// SubTurnStatusCancelled indicates the sub-turn was explicitly canceled by the user.
	//
	//nolint:misspell // wire value "cancelled" matches frontend TS union in src/store/chat.ts, src/lib/ws.ts
	SubTurnStatusCancelled SubTurnStatus = "cancelled"
	// SubTurnStatusInterrupted indicates the sub-turn was interrupted by its parent.
	SubTurnStatusInterrupted SubTurnStatus = "interrupted"
	// SubTurnStatusTimeout indicates the sub-turn exceeded its configured timeout.
	SubTurnStatusTimeout SubTurnStatus = "timeout"
)

type TaskExecutor

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

TaskExecutor runs queued tasks by dispatching them to agent sessions.

func GetTaskExecutor

func GetTaskExecutor(al *AgentLoop) *TaskExecutor

GetTaskExecutor returns the shared TaskExecutor (may be nil in tests).

func (*TaskExecutor) CheckQueuedTasks

func (te *TaskExecutor) CheckQueuedTasks(ctx context.Context)

CheckQueuedTasks picks up the highest-priority queued task per agent and starts it. Called by the heartbeat service.

func (*TaskExecutor) ExecuteTask

func (te *TaskExecutor) ExecuteTask(ctx context.Context, taskID string) error

ExecuteTask starts executing the task identified by taskID. It updates the task's status to "running" and dispatches it to the agent in a goroutine.

func (*TaskExecutor) Stop

func (te *TaskExecutor) Stop()

Stop cancels all running task goroutines.

type ThinkingLevel

type ThinkingLevel string

ThinkingLevel controls how the provider sends thinking parameters.

  • "adaptive": sends {thinking: {type: "adaptive"}} + output_config.effort (Claude 4.6+)
  • "low"/"medium"/"high"/"xhigh": sends {thinking: {type: "enabled", budget_tokens: N}} (all models)
  • "off": disables thinking
const (
	ThinkingOff      ThinkingLevel = "off"
	ThinkingLow      ThinkingLevel = "low"
	ThinkingMedium   ThinkingLevel = "medium"
	ThinkingHigh     ThinkingLevel = "high"
	ThinkingXHigh    ThinkingLevel = "xhigh"
	ThinkingAdaptive ThinkingLevel = "adaptive"
)

type Tier13Deps

type Tier13Deps struct {
	// ServedSubdirs is the process-wide web_serve static-mode registration map.
	// Non-nil when the gateway has initialized it at boot.
	ServedSubdirs *ServedSubdirs

	// EgressProxy is the shared Tier 2 / Tier 3 egress HTTP/HTTPS proxy.
	// Non-nil when sandbox.NewEgressProxy succeeded at boot.
	EgressProxy *sandbox.EgressProxy

	// DevServerRegistry is the process-wide web_serve dev-mode registration map.
	// Non-nil when the gateway has initialized it at boot.
	DevServerRegistry *sandbox.DevServerRegistry

	// GatewayBaseURL is the base URL (scheme + host + port) of the running
	// gateway's MAIN listener, e.g. "http://localhost:5000".
	//
	// Deprecated: kept for one release for replay safety on transcripts that
	// embedded URLs minted before the two-port topology landed (FR-021). Use
	// GatewayPreviewBaseURL for new tool URL emission. Will be removed after
	// 2026-Q3.
	GatewayBaseURL string

	// GatewayPreviewBaseURL is the base URL of the gateway's PREVIEW
	// listener, e.g. "http://localhost:3001" or "https://preview.acme.com".
	// Sourced from cfg.Gateway.PreviewOrigin when set, otherwise computed
	// from cfg.Gateway.Host + cfg.Gateway.PreviewPort at boot.
	//
	// web_serve and workspace.shell_bg use this to build absolute preview
	// URLs returned in tool results — web_serve emits /preview/<agent>/<token>/,
	// workspace.shell_bg emits /dev/<agent>/<token>/. The preview origin is
	// browser-cross-origin to the SPA's main origin, providing the T-01
	// mitigation (parent.localStorage access throws SecurityError).
	GatewayPreviewBaseURL string
}

Tier13Deps carries the shared singletons required to register the Tier 1 (web_serve static mode), Tier 2 (build_static), and Tier 3 (web_serve dev mode) tools for non-system agents.

All fields are nullable — a nil registry / proxy means the corresponding tool is not registered (graceful degradation when the gateway skips Tier 2/3 setup, e.g. in unit tests that only need Tier 1).

type ToolApprovalRequest

type ToolApprovalRequest struct {
	Meta      EventMeta      `json:"meta"`
	Tool      string         `json:"tool"`
	Arguments map[string]any `json:"arguments,omitempty"`
	Channel   string         `json:"channel,omitempty"`
	ChatID    string         `json:"chat_id,omitempty"`
	// SessionID is the transcript-store session for the turn requesting approval.
	// Carried on exec_approval_request/expired frames so the SPA can scope them.
	SessionID string `json:"session_id,omitempty"`
}

func (*ToolApprovalRequest) Clone

type ToolApprover

type ToolApprover interface {
	ApproveTool(ctx context.Context, req *ToolApprovalRequest) (ApprovalDecision, error)
}

type ToolCallHookRequest

type ToolCallHookRequest struct {
	Meta      EventMeta      `json:"meta"`
	Tool      string         `json:"tool"`
	Arguments map[string]any `json:"arguments,omitempty"`
	Channel   string         `json:"channel,omitempty"`
	ChatID    string         `json:"chat_id,omitempty"`
}

func (*ToolCallHookRequest) Clone

type ToolExecEndPayload

type ToolExecEndPayload struct {
	ToolCallID session.ToolCallID
	ChatID     string
	// SessionID is the transcript-store session ID for this turn.
	SessionID  string
	Tool       string
	Duration   time.Duration
	ForLLMLen  int
	ForUserLen int
	IsError    bool
	Async      bool
	// Result is the tool's ForLLM content, forwarded to the browser via WebSocket
	// so rich tool UIs (e.g., browser screenshot preview) can render the result.
	Result string
	// ParentSpawnCallID is non-empty when this tool call fires inside a sub-turn.
	// It equals the parent spawn tool call's ToolCall.ID (FR-H-002).
	// The WebSocket forwarder propagates this as parent_call_id on outbound frames (FR-H-005).
	ParentSpawnCallID session.ToolCallID
	// AgentID is the agent executing this tool call.
	// FR-I-008: live tool_call_result frames must carry agent_id to match replay frame parity.
	AgentID string
}

ToolExecEndPayload describes the outcome of a tool execution.

type ToolExecSkippedPayload

type ToolExecSkippedPayload struct {
	Tool   string
	Reason string
}

ToolExecSkippedPayload describes a skipped tool call.

type ToolExecStartPayload

type ToolExecStartPayload struct {
	ToolCallID session.ToolCallID
	ChatID     string
	// SessionID is the transcript-store session ID for this turn.
	SessionID string
	Tool      string
	Arguments map[string]any
	// ParentSpawnCallID is non-empty when this tool call fires inside a sub-turn.
	// It equals the parent spawn tool call's ToolCall.ID (FR-H-002).
	// The WebSocket forwarder propagates this as parent_call_id on outbound frames (FR-H-005).
	ParentSpawnCallID session.ToolCallID
	// AgentID is the agent executing this tool call.
	// FR-I-008: live tool_call_start frames must carry agent_id to match replay frame parity.
	AgentID string
}

ToolExecStartPayload describes a tool execution request.

type ToolInterceptor

type ToolInterceptor interface {
	BeforeTool(ctx context.Context, call *ToolCallHookRequest) (*ToolCallHookRequest, HookDecision, error)
	AfterTool(ctx context.Context, result *ToolResultHookResponse) (*ToolResultHookResponse, HookDecision, error)
}

type ToolResultHookResponse

type ToolResultHookResponse struct {
	Meta      EventMeta         `json:"meta"`
	Tool      string            `json:"tool"`
	Arguments map[string]any    `json:"arguments,omitempty"`
	Result    *tools.ToolResult `json:"result,omitempty"`
	Duration  time.Duration     `json:"duration"`
	Channel   string            `json:"channel,omitempty"`
	ChatID    string            `json:"chat_id,omitempty"`
}

func (*ToolResultHookResponse) Clone

type TurnCancelHook

type TurnCancelHook interface {
	// IsAlive returns true while the turn has not yet finished.
	IsAlive() bool
	// TurnID returns the turn's unique identifier.
	TurnID() string
	// SetOnCancelFinish registers a callback invoked by Finish() when the turn
	// exits after a cancel. Receives "graceful" or "hard".
	SetOnCancelFinish(fn func(cancelMethod string))
	// ClaimCancel performs the atomic first-cancel-wins check. Returns true
	// if this call is the first to claim the cancel (i.e. cancelFired was false
	// and has now been set to true). Returns false if already canceled.
	ClaimCancel() bool
	// MarkAbandoned sets the abandoned flag so the gateway can stop tracking
	// a stuck goroutine (FR-19, FR-20, FR-21).
	MarkAbandoned()
}

TurnCancelHook is the exported interface that the gateway's cancel handler uses to interact with an active turn. It exposes only the methods needed for the two-stage cancel timer, preventing gateway code from touching unexported turnState fields.

type TurnEndPayload

type TurnEndPayload struct {
	Status          TurnEndStatus
	Iterations      int
	Duration        time.Duration
	FinalContentLen int
	// ChatID is the chat session this turn belongs to.
	// Populated so the WS watchdog can scope orphan detection to the correct connection.
	ChatID string
	// SessionID is the transcript-store session ID for this turn.
	// Carried end-to-end so the WS forwarder can avoid the sessionIDs reverse-lookup.
	SessionID string
	// IsRoot is true when this turn has no parent (parentTurnID == "").
	// The orphan watchdog only arms on root turn-end to avoid spurious interrupts
	// from sibling sub-turn completions.
	IsRoot bool
}

TurnEndPayload describes the completion of a turn.

type TurnEndStatus

type TurnEndStatus string

TurnEndStatus describes the terminal state of a turn.

const (
	// TurnEndStatusCompleted indicates the turn finished normally.
	TurnEndStatusCompleted TurnEndStatus = "completed"
	// TurnEndStatusError indicates the turn ended because of an error.
	TurnEndStatusError TurnEndStatus = "error"
	// TurnEndStatusAborted indicates the turn was hard-aborted and rolled back.
	TurnEndStatusAborted TurnEndStatus = "aborted"
)

type TurnPhase

type TurnPhase string
const (
	TurnPhaseSetup      TurnPhase = "setup"
	TurnPhaseRunning    TurnPhase = "running"
	TurnPhaseTools      TurnPhase = "tools"
	TurnPhaseFinalizing TurnPhase = "finalizing"
	TurnPhaseCompleted  TurnPhase = "completed"
	TurnPhaseAborted    TurnPhase = "aborted"
)

type TurnStartPayload

type TurnStartPayload struct {
	Channel     string
	ChatID      string
	UserMessage string
	MediaCount  int
}

TurnStartPayload describes the start of a turn.

type TurnTimeoutPayload

type TurnTimeoutPayload struct {
	TimeoutSeconds int
	Compacted      bool
	Retried        bool
}

TurnTimeoutPayload describes a turn that exceeded its configured timeout.

type UserDefinition

type UserDefinition struct {
	Path    string `json:"path"`
	Content string `json:"content"`
}

UserDefinition represents the resolved USER.md file linked to the workspace.

type WhatsAppPairingPayload

type WhatsAppPairingPayload struct {
	ChannelID string                 `json:"channel_id"`
	Status    channels.PairingStatus `json:"status"`
	QR        string                 `json:"qr,omitempty"`
	Message   string                 `json:"message,omitempty"`
}

WhatsAppPairingPayload carries a WhatsApp native/QR linked-device pairing update for the SPA (#283). QR is populated only when Status == channels.PairingStatusCode.

Directories

Path Synopsis
Package envcontext provides the "new-office onboarding" preamble that sits above an agent's identity in the system prompt (Fix A, spec v7).
Package envcontext provides the "new-office onboarding" preamble that sits above an agent's identity in the system prompt (Fix A, spec v7).
Package testutil provides shared test infrastructure for all Plan-3 PRs.
Package testutil provides shared test infrastructure for all Plan-3 PRs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL