Documentation
¶
Overview ¶
Package serve provides an HTTP/WebSocket server for managing multiple agent sessions through a web dashboard.
Index ¶
- Variables
- func NewServer(manager *Manager, opts ...ServerOption) http.Handler
- type AskData
- type Attachment
- type AttachmentDTO
- type AutomationCallback
- type AutomationMCPServer
- type AutomationRunRequest
- type AutomationRunResponse
- type BashCompleteData
- type BashJobEndData
- type BashJobInitData
- type BashJobOutputData
- type BashJobStartData
- type CallbackPending
- type CommandData
- type CommandDequeuedData
- type CommandQueuedData
- type CommandResult
- type CompactionEndData
- type ConfigChangeData
- type ContextUpdateData
- type ConversationMessage
- type CreateOpts
- type DeltaData
- type Event
- type GoalChangeData
- type GoalEndData
- type GoalIterationData
- type InitData
- type LiveToolInitData
- type MCPChangeData
- type MCPSummary
- type ManagedSession
- type Manager
- func (m *Manager) Cancel(sessionID string) error
- func (m *Manager) CancelBashJob(sessionID, jobID string) error
- func (m *Manager) CancelSubagent(sessionID, jobID string) error
- func (m *Manager) CancelWithDiscardedSteers(sessionID string) ([]core.SteerItem, error)
- func (m *Manager) CloseSession(id string) error
- func (m *Manager) CreateAutomationRun(req AutomationRunRequest) (sessionID string, created bool, err error)
- func (m *Manager) CreateSession(opts CreateOpts) (*ManagedSession, error)
- func (m *Manager) Delete(id string) error
- func (m *Manager) ExecCommand(sessionID, rawCommand, id string) (*CommandResult, error)
- func (m *Manager) FileScanner() *files.Scanner
- func (m *Manager) Get(id string) (*ManagedSession, bool)
- func (m *Manager) InvalidateFileCache(cwd string)
- func (m *Manager) List() []SessionInfo
- func (m *Manager) MarkSessionRead(id string, throughSeq uint64, namespace string) error
- func (m *Manager) PromoteSubagent(sessionID, jobID string) error
- func (m *Manager) ReconfigureSession(sessionID, modelSpec, thinking string) (map[string]string, error)
- func (m *Manager) ResumeSession(id string) (*ManagedSession, error)
- func (m *Manager) Send(sessionID, text string, atts []Attachment, steerID, msgID string) (action, id string, descriptors []attachment.Descriptor, err error)
- func (m *Manager) SetCompactAt(sessionID string, tokens int) (int, error)
- func (m *Manager) SetPermissionMode(sessionID, modeStr string) (string, error)
- func (m *Manager) SetTitle(sessionID, title string) (string, error)
- func (m *Manager) Shutdown()
- func (m *Manager) SteerSubagent(sessionID, jobID, text string) (bool, error)
- func (m *Manager) ToggleMCPServer(anchor *ManagedSession, params mcpDisableParams, server string) (mcpToggleResult, error)
- func (m *Manager) Version() release.Result
- type ManagerConfig
- type MessageEndData
- type PendingSteerData
- type PermissionData
- type PromptResolvedData
- type RateLimitData
- type RealtimeAPIKeyFunc
- type RunEndData
- type RunTokensData
- type ServerOption
- func WithAllowedHosts(hosts []string) ServerOption
- func WithAuthToken(token string, secureCookie bool) ServerOption
- func WithAutomationToken(token string) ServerOption
- func WithDeviceAuthentication() ServerOption
- func WithDeviceStorePath(path string) ServerOption
- func WithRealtimeClientSecretBroker(key RealtimeAPIKeyFunc, client *http.Client) ServerOption
- type SessionCostData
- type SessionInfo
- type SessionState
- type StateChangeData
- type SteerData
- type SubagentCompleteData
- type SubagentCountData
- type SubagentEndData
- type SubagentEventData
- type SubagentInitData
- type SubagentStartData
- type SubagentSummary
- type SubagentUsageData
- type TasksUpdateData
- type ToolCallDeltaData
- type ToolCallStreamingData
- type ToolEndData
- type ToolStartData
- type ToolUpdateData
- type UserMessageData
Constants ¶
This section is empty.
Variables ¶
var ( ErrNotFound = errors.New("session not found") ErrBusy = errors.New("session is busy") ErrInvalidCWD = errors.New("invalid working directory") ErrInvalidModel = errors.New("invalid model") ErrInvalidThinking = errors.New("invalid thinking level") ErrInvalidPermissionMode = errors.New("invalid permission mode") ErrNoMCP = errors.New("session has no MCP servers") ErrInvalidAttentionCursor = errors.New("invalid attention cursor") ErrStaleAttentionNamespace = errors.New("stale attention namespace") )
ErrAutomationIndexUnavailable reports that the idempotency index could not be rebuilt from disk. Keyed requests are refused while it holds: answering them with an empty index would silently execute a redelivered webhook twice.
var ErrAutomationInvalidMCP = errors.New("invalid mcp_servers")
ErrAutomationInvalidMCP reports a per-run MCP server the request may not ask for (currently: a name an operator-configured server already owns). It is a 400: the caller has to pick another name, retrying changes nothing.
var ErrAutomationNotLive = errors.New("session is not loaded")
ErrAutomationNotLive reports that the session exists and belongs to the automation token, but is not currently loaded — so it cannot have a pending interaction to answer.
var ErrAutomationTooManySessions = errors.New("too many loaded sessions")
ErrAutomationTooManySessions reports that resuming a saved session would push the resident set past maxAutomationLoadedSessions.
var ErrBadAttachment = errors.New("bad attachment")
ErrBadAttachment wraps any attachment validation failure; the wrapping error message names the offending attachment and is safe to surface as a 400.
var ( // ErrScopeInvalid is returned for a scope that isn't session/project/global. ErrScopeInvalid = errors.New("invalid scope") )
Errors returned by the MCP toggle path, mapped to HTTP status by the handler.
Functions ¶
Types ¶
type AskData ¶
type AskData struct {
ID string `json:"id"`
RunGen uint64 `json:"run_gen,omitempty"`
Questions []bus.AskQuestion `json:"questions"`
}
AskData is a pending ask_user request.
type Attachment ¶
type Attachment struct {
Name string `json:"name"`
Mime string `json:"mime"`
Data string `json:"data"` // base64 standard encoding
}
Attachment is a file uploaded inline (base64) with a /send request.
type AttachmentDTO ¶
type AttachmentDTO struct {
ID string `json:"id"`
Name string `json:"name"`
Mime string `json:"mime"`
Size int64 `json:"size"`
Kind string `json:"kind"`
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
}
AttachmentDTO is the public metadata returned for attachments created by a send. The URL is served by the attachment endpoint.
type AutomationCallback ¶ added in v0.20.0
type AutomationCallback struct {
SessionID string `json:"session_id"`
Status string `json:"status"` // done | failed | needs_input
Title string `json:"title"`
Summary string `json:"summary"`
URL string `json:"url"`
Error string `json:"error,omitempty"`
// Pending describes what the run is blocked on. Only set for needs_input,
// so a machine caller can answer it through the scoped interaction
// endpoints instead of only learning that a human is needed.
Pending *CallbackPending `json:"pending,omitempty"`
Timestamp string `json:"timestamp"` // RFC3339
}
AutomationCallback is the JSON body POSTed to an automation session's callback_url. It is deliberately small: a status, a hint of what happened and a link back to the session, which holds the full transcript.
type AutomationMCPServer ¶ added in v0.20.0
type AutomationMCPServer struct {
Name string `json:"name"`
URL string `json:"url"`
Headers map[string]string `json:"headers,omitempty"`
// Command/Args/Env exist only so a caller that sends one gets a 400 instead
// of a silently url-only server. They never start anything.
Command json.RawMessage `json:"command,omitempty"`
Args json.RawMessage `json:"args,omitempty"`
Env json.RawMessage `json:"env,omitempty"`
}
AutomationMCPServer is one per-run MCP server: a remote endpoint the session connects to for the duration of its life.
URL-based ONLY, by design. A command-based entry would let a remote caller execute a local program with the automation token as its only credential — authority far beyond "start a session". Decoding is strict about it: a "command" (or "args"/"env") key is an error, not something quietly ignored.
type AutomationRunRequest ¶ added in v0.20.0
type AutomationRunRequest struct {
Prompt string `json:"prompt"`
Model string `json:"model"`
CWD string `json:"cwd"`
Title string `json:"title"`
Origin string `json:"origin"`
// IdempotencyKey deduplicates retries: webhooks redeliver, and a redelivery
// must not spawn a second session.
IdempotencyKey string `json:"idempotency_key"`
// CallbackURL/CallbackSecret configure the outbound completion callback:
// where to POST when the run settles, and the optional HMAC secret the
// receiver verifies it with (see callback.go).
CallbackURL string `json:"callback_url"`
CallbackSecret string `json:"callback_secret"`
// MCPServers attaches session-scoped MCP servers to the run, so the agent
// can call the caller's own tools. URL-based only — see AutomationMCPServer.
MCPServers []AutomationMCPServer `json:"mcp_servers"`
}
AutomationRunRequest is the body of POST /api/automation/runs: create a session and send it a first prompt in one call.
type AutomationRunResponse ¶ added in v0.20.0
type AutomationRunResponse struct {
SessionID string `json:"session_id"`
URL string `json:"url"`
// Created is false when an idempotency key matched an existing session.
Created bool `json:"created"`
}
AutomationRunResponse identifies the session the run landed in.
type BashCompleteData ¶
type BashCompleteData struct {
JobID string `json:"job_id"`
OwnerAgentID string `json:"owner_agent_id,omitempty"`
Command string `json:"command"`
Status string `json:"status"`
Text string `json:"text"`
}
BashCompleteData is sent when an async background bash job finishes and its formatted result is reinjected into the conversation.
type BashJobEndData ¶
type BashJobInitData ¶
type BashJobInitData struct {
JobID string `json:"job_id"`
OwnerAgentID string `json:"owner_agent_id,omitempty"`
Command string `json:"command"`
CWD string `json:"cwd"`
Status string `json:"status"`
Output string `json:"output"`
}
BashJobInitData restores a live/recent background command after reconnect.
type BashJobOutputData ¶
type BashJobStartData ¶
type CallbackPending ¶ added in v0.20.0
type CallbackPending struct {
Kind string `json:"kind"` // question | permission
ID string `json:"id"`
Questions []bus.AskQuestion `json:"questions,omitempty"`
Tool string `json:"tool,omitempty"`
Summary string `json:"summary,omitempty"`
}
CallbackPending is the interaction a needs_input callback is blocked on. It mirrors the bus event that raised it: a question carries its questions, a permission carries the tool and a human-readable summary of its arguments.
type CommandData ¶
type CommandData struct {
Command string `json:"command"`
Messages []core.AgentMessage `json:"messages,omitempty"` // compact sends updated messages
HistoryTruncated bool `json:"history_truncated,omitempty"`
}
CommandData is sent when a slash command is executed.
type CommandDequeuedData ¶
type CommandDequeuedData struct {
ID string `json:"id"`
Raw string `json:"raw"`
Executed bool `json:"executed"`
Err string `json:"err,omitempty"`
}
CommandDequeuedData is sent when a queued command barrier leaves the queue (after execution, cancellation, or a permanent failure). The client removes the matching chip by ID. Err is set when it left because execution failed.
type CommandQueuedData ¶
CommandQueuedData is sent when a slash command is enqueued as a barrier in the unified queue rail (issued while the session was busy). The client renders a queued command chip keyed by ID, distinct from a queued message chip.
type CommandResult ¶
type CommandResult struct {
OK bool `json:"ok"`
Message string `json:"message"`
NewSessionID string `json:"newSessionId,omitempty"`
// Queued marks a command whose OUTCOME IS NOT IN THIS RESPONSE. Two shapes:
// enqueued as a barrier in the unified queue rail (issued while the session
// was busy), where ID is the queued chip's authoritative ID so the client
// reconciles its optimistic command chip by it; or accepted and started now
// (/compact, /prepare-compact) with no ID, since no command_dequeued will
// follow. Either way the result arrives as WS events, never here.
Queued bool `json:"queued,omitempty"`
ID string `json:"id,omitempty"`
}
CommandResult is the response from executing a slash command.
type CompactionEndData ¶ added in v0.28.0
type CompactionEndData struct {
Marker *core.AgentMessage `json:"marker,omitempty"`
}
CompactionEndData carries the durable display marker that TreeSyncer records as the compaction entry.
type ConfigChangeData ¶
type ConfigChangeData struct {
Model string `json:"model,omitempty"`
Provider string `json:"provider,omitempty"`
Thinking string `json:"thinking,omitempty"`
PermissionMode string `json:"permission_mode,omitempty"`
PathScope string `json:"path_scope,omitempty"`
// CompactAt carries a compaction-threshold change only. Pointer because 0
// ("compact at the model window") is a real setting, not "unchanged".
CompactAt *int `json:"compact_at,omitempty"`
// ContextWindow carries a model switch's new input window, the denominator
// for every context percentage the client shows.
ContextWindow int `json:"context_window,omitempty"`
}
ConfigChangeData is sent when model/thinking/permissions/path scope change.
type ContextUpdateData ¶
type ContextUpdateData struct {
ContextPercent int `json:"context_percent"`
}
ContextUpdateData carries the current context usage percentage.
type ConversationMessage ¶
type ConversationMessage struct {
ID string `json:"id"`
Role string `json:"role"`
Source string `json:"source,omitempty"`
Timestamp time.Time `json:"timestamp,omitempty"`
Text string `json:"text,omitempty"`
Truncated bool `json:"truncated,omitempty"`
Omitted bool `json:"omitted,omitempty"`
OmittedBlocks int `json:"omitted_blocks,omitempty"`
Tool string `json:"tool,omitempty"`
Action string `json:"action,omitempty"`
Target string `json:"target,omitempty"`
Status string `json:"status,omitempty"`
Attachments []AttachmentDTO `json:"attachments,omitempty"`
}
ConversationMessage is the owner-facing transcript DTO. Tool activity is projected into role=tool items; tool result output remains available only through the explicit detail query.
type CreateOpts ¶
type CreateOpts struct {
Model string `json:"model"`
Thinking string `json:"thinking"`
PermissionMode string `json:"permission_mode"`
Title string `json:"title"`
CWD string `json:"cwd"`
// Origin records who created the session ("user" when empty). Free-form so
// automation callers can label their integration, e.g. "linear-webhook".
Origin string `json:"origin"`
// contains filtered or unexported fields
}
CreateOpts configures a new session.
type DeltaData ¶
type DeltaData struct {
Delta string `json:"delta"`
}
DeltaData carries a streaming text delta.
type Event ¶
type Event struct {
Type string `json:"type"`
Data any `json:"data,omitempty"`
Seq uint64 `json:"seq,omitempty"`
}
Event is a JSON-serializable event sent to WebSocket clients.
type GoalChangeData ¶
type GoalChangeData struct {
Active bool `json:"active"`
Objective string `json:"objective,omitempty"`
WorkDir string `json:"work_dir,omitempty"`
Iteration int `json:"iteration"`
Stalled int `json:"stalled"`
}
GoalChangeData is sent when goal mode activates or deactivates.
type GoalEndData ¶
type GoalEndData struct {
Reason string `json:"reason"`
}
GoalEndData is sent when a goal loop ends.
type GoalIterationData ¶
type GoalIterationData struct {
Iteration int `json:"iteration"`
Satisfied bool `json:"satisfied"`
Feedback string `json:"feedback,omitempty"`
}
GoalIterationData is sent after the verifier judges a goal iteration.
type InitData ¶
type InitData struct {
// ServerInstance identifies the process that owns this runtime incarnation.
// AttentionNamespace, not this value, scopes the bus-sequence read cursor.
ServerInstance string `json:"server_instance"`
// AttentionNamespace identifies the ordered runtime incarnation that owns
// bus-sequence read cursors. It changes on a close/resume even within one
// server process, whose bus sequence then starts again at zero.
AttentionNamespace string `json:"attention_namespace,omitempty"`
Messages []core.AgentMessage `json:"messages"`
State string `json:"state"`
ContextPercent int `json:"context_percent"`
ContextWindow int `json:"context_window,omitempty"`
CompactAt int `json:"compact_at,omitempty"`
CompactAtMin int `json:"compact_at_min,omitempty"`
PermissionMode string `json:"permission_mode"`
PathScope string `json:"path_scope,omitempty"`
PendingPermission *PermissionData `json:"pending_permission,omitempty"`
PendingAsk *AskData `json:"pending_ask,omitempty"`
Tasks any `json:"tasks,omitempty"`
GoalActive bool `json:"goal_active,omitempty"`
GoalObjective string `json:"goal_objective,omitempty"`
GoalWorkDir string `json:"goal_work_dir,omitempty"`
GoalIteration int `json:"goal_iteration,omitempty"`
GoalStalled int `json:"goal_stalled,omitempty"`
GoalVerifying bool `json:"goal_verifying,omitempty"`
Compacting bool `json:"compacting,omitempty"`
AutoVerifying bool `json:"auto_verifying,omitempty"`
StreamingText string `json:"streaming_text,omitempty"`
StreamingThinking string `json:"streaming_thinking,omitempty"`
LiveTools []LiveToolInitData `json:"live_tools,omitempty"`
RunTokensUp int `json:"run_tokens_up"`
RunTokensDown int `json:"run_tokens_down"`
RunStartedAtMs int64 `json:"run_started_at_ms,omitempty"`
PendingSteers []PendingSteerData `json:"pending_steers,omitempty"`
CostUSD float64 `json:"cost_usd,omitempty"`
Subagents []SubagentInitData `json:"subagents,omitempty"`
// SubagentOutcomes restores terminal child cards after reconnect/restart.
// It is separate from live Subagents because terminal jobs do not belong in
// the Live Dock.
SubagentOutcomes []SubagentEndData `json:"subagent_outcomes,omitempty"`
BashJobs []BashJobInitData `json:"bash_jobs,omitempty"`
LastSeq uint64 `json:"last_seq,omitempty"`
HistoryTruncated bool `json:"history_truncated,omitempty"`
HistoryBefore string `json:"history_before,omitempty"`
// DeltaBase is the validated tree entry requested by since_msg. When set,
// Messages is a complete suffix after this entry and clients append it.
DeltaBase string `json:"delta_base,omitempty"`
}
InitData is sent on WebSocket connect with the full session state.
type LiveToolInitData ¶ added in v0.23.0
type LiveToolInitData struct {
ToolCallID string `json:"tool_call_id"`
ToolName string `json:"tool_name"`
Args map[string]any `json:"args,omitempty"`
// Status is "generating" (arguments still streaming) or "running".
Status string `json:"status"`
// StartedAtMs is when the call first appeared, epoch milliseconds (same
// encoding as RunStartedAtMs), so the row's elapsed timer resumes instead
// of restarting at zero.
StartedAtMs int64 `json:"started_at_ms,omitempty"`
}
LiveToolInitData is one tool call that is still generating its arguments or still executing when the snapshot is taken. Such a call may not be in the message history yet — it is written there only when its assistant message closes, its result only when the tool ends — so without this a client that switches away and back mid-call rebuilds a row it cannot name and falls back to a generic "Calling", or (for a long bash) loses the row entirely. When the message did close, the call IS in history but still lacks its result, phase and start anchor; withLiveTools patches that row instead of duplicating it.
Shape mirrors ToolStartData on purpose: the client reconstructs the row with the same reducer path a live tool_call_start/tool_start would have taken, and dedups by ToolCallID so a live event arriving after the snapshot updates the restored row instead of duplicating it.
type MCPChangeData ¶
type MCPChangeData struct {
Total int `json:"total"`
Ready int `json:"ready"`
Disabled int `json:"disabled"`
Unhealthy int `json:"unhealthy"`
Pending int `json:"pending"`
}
MCPChangeData carries the rolled-up MCP summary counts for the status-line indicator, matching the MCPSummary fields. An open panel re-fetches full per-server detail from GET /api/sessions/{id}/mcp when this arrives.
type MCPSummary ¶
type MCPSummary struct {
Total int `json:"total"`
Ready int `json:"ready"`
Disabled int `json:"disabled"`
Unhealthy int `json:"unhealthy"`
Pending int `json:"pending"`
}
MCPSummary is the glanceable MCP health for the status line. Total counts all configured servers (including disabled ones); Disabled is the count in the intentionally-off state (neutral, not an alarm); Unhealthy counts only servers that are enabled yet failed/exited (the alert color); Pending counts servers mid-transition (desired differs from applied). The indicator shows whenever Total > 0 and turns to an alert color only when Unhealthy > 0.
type ManagedSession ¶
type ManagedSession struct {
// Immutable after construction.
ID string `json:"id"`
CWD string `json:"cwd"`
Created time.Time `json:"created"`
// Origin is who created the session ("user" for a human, or a caller-chosen
// label for automation). Mirrors session.Session metadata.
Origin string `json:"origin"`
Title string
TitleSource string // "manual" | "auto" | "" (legacy=auto); see session.TitleSource
Updated time.Time
// contains filtered or unexported fields
}
ManagedSession wraps a bus.SessionRuntime with metadata for the web dashboard.
func (*ManagedSession) History ¶
func (s *ManagedSession) History() []core.AgentMessage
History returns a copy of the session's conversation messages.
func (*ManagedSession) MCPStatus ¶
func (s *ManagedSession) MCPStatus() []mcp.ControllerStatus
MCPStatus returns the policy-decorated health snapshot of this session's MCP servers (empty if none are configured). Each entry carries the applied enabled state, desired-enabled, the scopes that veto it, and any pending action.
func (*ManagedSession) RestartMCPServer ¶
func (s *ManagedSession) RestartMCPServer(name string) (mcp.ServerStatus, error)
RestartMCPServer restarts a single MCP server for this session and re-syncs the tool registry with its (possibly changed) tool set. Other servers are untouched. Returns ErrNoMCP if the session has no MCP manager, mcp.ErrUnknownServer for a name it doesn't manage, mcp.ErrServerDisabled for a disabled server (enable it first), or ErrBusy if the session is running or awaiting a permission decision.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns all active sessions.
func NewManager ¶
func NewManager(ctx context.Context, cfg ManagerConfig) *Manager
NewManager creates a Manager. The context controls the lifetime of all agent runs — cancelling it aborts every active session.
func (*Manager) CancelBashJob ¶
CancelBashJob cancels a session-scoped background bash job.
func (*Manager) CancelSubagent ¶
CancelSubagent requests cancellation of a single (async) subagent job belonging to a session, without aborting the parent run.
func (*Manager) CancelWithDiscardedSteers ¶ added in v0.26.0
CancelWithDiscardedSteers aborts the running agent and returns the queued steers atomically discarded by that operation.
func (*Manager) CloseSession ¶ added in v0.22.0
CloseSession unloads an active session from memory, leaving it on disk where it lists as "saved" and can be reopened with ResumeSession.
This is what "close" means to a user: the conversation stops occupying a live runtime (agent, MCP connections, bridges) but stays in the list and loses nothing. Closing a session that is already only on disk is a no-op, so the action is idempotent from any client.
Refused with ErrBusy unless the session is fully quiescent: not running, not awaiting a permission decision, and with no background work (async subagents, bash jobs, verifiers) still in flight. StateIdle alone is not enough — closing cancels the session context, which would kill that work and lose its output.
Concurrency. The close is admitted under the state lock (DoIfQuiescent), which is the same lock a run-start takes, so a /send cannot slip between the check and the teardown: either it starts a run first (and the close is refused), or it finds the session already marked closing. The ID stays reserved in m.resuming until the teardown finishes, so a concurrent ResumeSession cannot build a second runtime from disk while the old one is still flushing.
Runs under automationMu like Delete, so a close cannot interleave with the automation check-create-send-register sequence. Lock order: automationMu → m.mu.
func (*Manager) CreateAutomationRun ¶ added in v0.20.0
func (m *Manager) CreateAutomationRun(req AutomationRunRequest) (sessionID string, created bool, err error)
CreateAutomationRun creates a session for an external caller and sends it the first prompt. It returns the session ID and whether it was created now; a repeated idempotency key resolves to the existing session without creating a duplicate or re-sending the prompt.
The whole check-create-send-record sequence runs under automationMu so two simultaneous retries of the same webhook cannot both pass the check, and so a concurrent Delete cannot drop the index entry we are about to write.
The key is committed after success, never rolled back: it is written to the session metadata (and indexed) only once the first prompt was accepted. A failed send therefore leaves an inert, keyless session — nothing deletes a session another client may already be using, and neither a retry in this process nor an index rebuilt after a restart can resolve the key to it.
func (*Manager) CreateSession ¶
func (m *Manager) CreateSession(opts CreateOpts) (*ManagedSession, error)
CreateSession creates a new agent session.
func (*Manager) Delete ¶
Delete aborts any running agent, closes resources, and removes the session.
It runs under automationMu (the same guard as CreateAutomationRun's check-create-send-register sequence) so a delete cannot interleave with a run creation and leave an idempotency key pointing at a session that is already gone. Lock order is automationMu → m.mu, as in CreateAutomationRun.
func (*Manager) ExecCommand ¶
func (m *Manager) ExecCommand(sessionID, rawCommand, id string) (*CommandResult, error)
ExecCommand executes a slash command in a session. id is the client-minted stable ID for the optimistic chip when the command is enqueued as a barrier (busy session, PolicyQueue); it is ignored when the command runs immediately.
func (*Manager) FileScanner ¶
FileScanner returns the shared file scanner instance.
func (*Manager) Get ¶
func (m *Manager) Get(id string) (*ManagedSession, bool)
Get returns a managed session by ID.
func (*Manager) InvalidateFileCache ¶
InvalidateFileCache invalidates the file scanner cache for a given CWD. Called after successful file edits to keep file suggestions fresh.
func (*Manager) List ¶
func (m *Manager) List() []SessionInfo
List returns info for all sessions, sorted by updated time descending.
func (*Manager) MarkSessionRead ¶ added in v0.27.0
MarkSessionRead advances a runtime's process-local bus read cursor. Runtime identity, namespace, the bus sequence cap, and the cursor mutation share unseenMu so a close/resume cannot redirect a request from an old runtime onto its replacement.
func (*Manager) PromoteSubagent ¶
PromoteSubagent flips a running sync subagent job to async, unblocking its parent's blocking tool call while the child keeps running in the background. Returns ErrNotFound if the session/job doesn't exist, and propagates subagent.ErrNotSync / subagent.ErrNotRunning otherwise so callers can map them to specific responses.
func (*Manager) ReconfigureSession ¶
func (m *Manager) ReconfigureSession(sessionID, modelSpec, thinking string) (map[string]string, error)
ReconfigureSession changes the model and/or thinking level of a session. Only allowed when the session is idle (not running).
func (*Manager) ResumeSession ¶
func (m *Manager) ResumeSession(id string) (*ManagedSession, error)
ResumeSession loads a saved session from disk and creates a full runtime.
func (*Manager) Send ¶
func (m *Manager) Send(sessionID, text string, atts []Attachment, steerID, msgID string) (action, id string, descriptors []attachment.Descriptor, err error)
Send delivers a user message (with optional attachments) to a session. If idle: starts a new agent run via bus. If running/permission: steers the running agent (attachments not allowed there). steerID, when non-empty, is the client-minted stable ID for the queued message: the client shows its optimistic chip under that same ID, so there is no window where a chip lacks an authoritative identity (closes the double-send and cancel-vs-in-flight races). When empty (e.g. CLI), the handler mints one. msgID plays the same role for a direct send: the client mints it for its optimistic echo, and the user message enters the conversation under it, so the UserMessageAppended broadcast dedups against that echo instead of doubling the message on the sending client. A malformed or already-used msgID is replaced by a server-minted one rather than rejected (see validClientID): the prompt must reach the agent either way.
Returns the action taken ("send" or "steer") and the effective ID the message was accepted under — the chip ID for a steer, the message ID for a direct send — so the caller can reconcile its optimistic view by identity even when the server re-minted it.
func (*Manager) SetCompactAt ¶
SetCompactAt changes a session's soft compaction threshold (tokens), so the agent compacts once context passes it rather than waiting for the full model window. 0 restores the window-based default. Like model/thinking this reconfigures the agent, so it is only allowed while the session is idle.
func (*Manager) SetPermissionMode ¶
SetPermissionMode changes the permission mode for a session via bus command.
func (*Manager) SetTitle ¶
SetTitle renames a session, marking the title as manually set so background auto-titling won't overwrite it, and persists the change immediately.
func (*Manager) Shutdown ¶
func (m *Manager) Shutdown()
Shutdown synchronously flushes every active session to disk. Call it after the HTTP server has stopped accepting requests and before the process exits, so a turn that finished just before shutdown is persisted even though the async RunEnded→TreeSynced→save chain may not have drained.
A SIGTERM cancels the root context, which cancels each in-flight run. Before flushing we wait — bounded by shutdownDrainBudget across the whole process — for active sessions to leave the running/permission state, so a snapshot captures the complete final turn rather than a partial one. If the budget expires we flush regardless (best effort beats losing the turn entirely).
func (*Manager) SteerSubagent ¶
SteerSubagent queues a message for inter-step delivery to the running child agent of a subagent job. Returns ErrNotFound if the session or job doesn't exist. The bool reports whether the message was actually queued (false if the job has no live child agent yet or has already finished).
func (*Manager) ToggleMCPServer ¶
func (m *Manager) ToggleMCPServer(anchor *ManagedSession, params mcpDisableParams, server string) (mcpToggleResult, error)
ToggleMCPServer applies a disable/enable preference for one server in one scope, persisting it (project/global) and fanning it out to every affected open session in the process:
- session: only the anchor session,
- project: every open session whose canonical cwd matches the anchor's,
- global: every open session in the process.
The persisted scope also governs future session startups. Returns how many sessions applied the change now vs deferred it to quiescence. anchor must be a session that has the server configured (the caller validates that via GET).
type ManagerConfig ¶
type ManagerConfig struct {
ProviderFactory func(model core.Model) (core.Provider, error)
Transcriber core.Transcriber // optional; enables POST /api/transcribe
UsagePoller *usage.MultiPoller // optional; enables GET /api/usage
PushStore *push.Store // optional; enables Web Push
PushDispatcher *push.Dispatcher // optional; enables Web Push
DefaultModel core.Model
WorkspaceRoot string
MoaCfg core.MoaConfig
// AuxiliaryModelResolver resolves auto-title/session-brief settings against
// normal completion credentials. A nil resolver leaves auto unavailable;
// explicit specs continue to work through the core resolver.
AuxiliaryModelResolver func(spec string) (core.Model, bool, error)
// ConfigLoader loads configuration for an individual session CWD. When
// nil, core.LoadMoaConfig preserves the normal global/project lookup.
ConfigLoader func(cwd string) core.MoaConfig
SessionBaseDir string // root for session stores; empty = default
// SchedulePath overrides the durable schedules file. Empty stores it beside
// the session base directory.
SchedulePath string
ReleaseInfo release.Info
UpdateChecker *release.Checker
UpdateCheckEnabled bool
}
ManagerConfig configures a Manager.
type MessageEndData ¶
type MessageEndData struct {
Text string `json:"text"`
MsgID string `json:"msg_id,omitempty"`
InputTokens int `json:"input_tokens,omitempty"`
OutputTokens int `json:"output_tokens,omitempty"`
}
MessageEndData carries the full assistant text on message completion and the provider-reported usage for that message.
type PendingSteerData ¶
type PendingSteerData struct {
ID string `json:"id"`
Text string `json:"text"`
Command bool `json:"command,omitempty"`
Images int `json:"images,omitempty"`
}
PendingSteerData is one queued (not yet delivered) item in the unified queue rail, with its authoritative ID so a reconnecting client reconciles its optimistic chip by ID instead of by text. Command marks a queued slash-command barrier (Text holds its raw command line, e.g. "/compact") so the client renders a command chip; Images is the number of image blocks a queued message carries (0 for a plain-text steer or a command) so the chip can show an attachment badge.
type PermissionData ¶
type PermissionData struct {
ID string `json:"id"`
RunGen uint64 `json:"run_gen,omitempty"`
ToolName string `json:"tool_name"`
Args map[string]any `json:"args"`
AllowPattern string `json:"allow_pattern,omitempty"`
}
PermissionData is a pending permission request.
type PromptResolvedData ¶ added in v0.28.0
type PromptResolvedData struct {
ID string `json:"id"`
}
PromptResolvedData identifies the prompt cleared by a resolution event.
type RateLimitData ¶
type RateLimitData struct {
Status string `json:"status,omitempty"`
RepresentativeClaim string `json:"representative_claim,omitempty"`
OnOverage bool `json:"on_overage"`
FiveHourPct int `json:"five_hour_pct"`
SevenDayPct int `json:"seven_day_pct"`
OveragePct int `json:"overage_pct"`
}
RateLimitData carries the provider's per-request rate-limit state: plan-window utilization (as percentages, to match /api/usage) and whether this request was served from extra usage.
type RealtimeAPIKeyFunc ¶
RealtimeAPIKeyFunc returns only a normal OpenAI API key. ok must be false for missing credentials and OAuth credentials.
type RunEndData ¶
type RunEndData struct {
Text string `json:"text"`
RunGen uint64 `json:"run_gen"`
Cancelled bool `json:"cancelled,omitempty"`
HasError bool `json:"has_error,omitempty"`
}
RunEndData carries the final assistant text when a run completes.
type RunTokensData ¶
RunTokensData carries the current run's estimated logical input/output traffic.
type ServerOption ¶
type ServerOption func(*serverOptions)
ServerOption configures optional NewServer behavior.
func WithAllowedHosts ¶
func WithAllowedHosts(hosts []string) ServerOption
WithAllowedHosts adds extra hostnames accepted by the anti DNS-rebinding Host check (on top of localhost and any IP literal). Use it for named hosts such as a Tailscale MagicDNS name.
func WithAuthToken ¶
func WithAuthToken(token string, secureCookie bool) ServerOption
WithAuthToken enables opt-in shared-token authentication. An empty token leaves the server unauthenticated (current behavior). secureCookie marks the session cookie Secure and should be true only when served over TLS.
func WithAutomationToken ¶ added in v0.20.0
func WithAutomationToken(token string) ServerOption
WithAutomationToken enables the Automation API with its own shared secret, separate from the browser token. An empty token leaves the automation routes disabled entirely (they answer 404), so the API is fail-closed even on localhost.
func WithDeviceAuthentication ¶
func WithDeviceAuthentication() ServerOption
WithDeviceAuthentication enables Pulse device pairing and credentials when Serve is embedded without a shared token. The CLI enables it by default; tests and other embedders opt in explicitly.
func WithDeviceStorePath ¶
func WithDeviceStorePath(path string) ServerOption
WithDeviceStorePath overrides the private device credential store. It is primarily useful for embedded deployments and tests; normal Serve uses ~/.config/moa/devices.json (or MOA_CONFIG_DIR/devices.json).
func WithRealtimeClientSecretBroker ¶
func WithRealtimeClientSecretBroker(key RealtimeAPIKeyFunc, client *http.Client) ServerOption
WithRealtimeClientSecretBroker supplies the narrowly scoped capability used to mint OpenAI Realtime client secrets. The key is never retained by Serve.
type SessionCostData ¶
type SessionCostData struct {
CostUSD float64 `json:"cost_usd"`
}
SessionCostData carries the accumulated session spend (main run + subagents).
type SessionInfo ¶
type SessionInfo struct {
ID string `json:"id"`
Title string `json:"title"`
State SessionState `json:"state"`
Model string `json:"model"`
Provider string `json:"provider"`
Thinking string `json:"thinking"`
CWD string `json:"cwd"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
Origin string `json:"origin,omitempty"` // who created it; omitted for ordinary user sessions
Error string `json:"error,omitempty"`
Unseen bool `json:"unseen"`
UnseenSeq uint64 `json:"unseen_seq,omitempty"`
ServerInstance string `json:"server_instance"`
AttentionNamespace string `json:"attention_namespace,omitempty"`
UntrustedMCP bool `json:"untrusted_mcp,omitempty"`
// MCP summarizes this session's MCP servers for the status line: a count and
// whether any is unhealthy, so the indicator can appear only when servers
// exist and turn red when one has failed or exited. The full per-server
// detail is fetched on demand from GET /api/sessions/{id}/mcp. Omitted when
// the session has no MCP servers.
MCP *MCPSummary `json:"mcp,omitempty"`
ContextPercent int `json:"context_percent"` // 0-100, -1 if unknown
// ContextWindow is the model's usable input window in tokens — the
// denominator ContextPercent is measured against, and the scale CompactAt
// is expressed on, so a UI can show the limit as a percentage of the ring.
ContextWindow int `json:"context_window,omitempty"`
// CompactAt is the soft compaction threshold in tokens; 0 means the session
// compacts only when it approaches ContextWindow.
CompactAt int `json:"compact_at,omitempty"`
// CompactAtMin is the lowest threshold the engine honors, in tokens. A UI
// picking a threshold must not offer below it — the engine would raise the
// value and compact somewhere other than where the control says.
CompactAtMin int `json:"compact_at_min,omitempty"`
PermissionMode string `json:"permission_mode"` // "yolo", "ask", "auto"
CostUSD float64 `json:"cost_usd"` // accumulated session spend (main run + subagents)
Activity *attention.SessionActivity `json:"activity,omitempty"`
// CacheExpiresAt is when the Anthropic prompt cache for this session goes
// cold (last run + cache TTL). Zero/omitted when not applicable (no run yet,
// or a non-Anthropic model that doesn't use TTL-based prompt caching). The
// UI warns once this time has passed that a new message pays a cache write.
CacheExpiresAt time.Time `json:"cache_expires_at,omitzero"`
// RunStartedAt is when the in-progress run began; zero/omitted when idle.
// The UI anchors the activity-indicator elapsed counter to it so the counter
// stays correct across reconnects. Only meaningful while State is running or
// permission.
RunStartedAt time.Time `json:"run_started_at,omitzero"`
// BriefAttempting/BriefProgress are the cheap LLM-generated status prose:
// what the session is attempting and how it's going. Prose that can age;
// the actionable state is State/PermissionMode above (derived live), never
// baked into this prose. BriefUpdated is the freshness stamp. Empty until
// the first brief is generated. See brief.go.
BriefAttempting string `json:"brief_attempting,omitempty"`
BriefProgress string `json:"brief_progress,omitempty"`
BriefUpdated time.Time `json:"brief_updated,omitzero"`
}
SessionInfo is the public representation returned by List/Get endpoints.
type SessionState ¶
type SessionState string
SessionState describes the current state of a managed session.
const ( StateIdle SessionState = "idle" // waiting for user input StateRunning SessionState = "running" // agent is executing StatePermission SessionState = "permission" // blocked on permission approval StateError SessionState = "error" // last run errored (still usable) StateSaved SessionState = "saved" // on disk but not loaded into memory )
type StateChangeData ¶
StateChangeData is sent when the session state changes.
type SteerData ¶
type SteerData struct {
ID string `json:"id,omitempty"`
MsgID string `json:"msg_id,omitempty"`
Text string `json:"text"`
Content []core.Content `json:"content,omitempty"`
Custom map[string]any `json:"custom,omitempty"`
}
SteerData is sent when the user steers a running agent. Content carries the injected message's blocks when the steer had attachments, so clients render the thumbnails live instead of only after a reload; a text-only steer travels in Text alone.
type SubagentCompleteData ¶
type SubagentCompleteData struct {
JobID string `json:"job_id"`
Task string `json:"task"`
Status string `json:"status"`
Text string `json:"text"`
}
SubagentCompleteData is sent when an async subagent finishes.
type SubagentCountData ¶
type SubagentCountData struct {
Count int `json:"count"`
}
SubagentCountData is sent when async subagent jobs start/finish.
type SubagentEndData ¶
type SubagentEndData struct {
JobID string `json:"job_id"`
Task string `json:"task,omitempty"`
Async bool `json:"async"`
Status string `json:"status"`
// Result is present for completed children. Error is present for failures;
// cancellation intentionally has neither, so it cannot masquerade as a
// successful empty result.
Result string `json:"result,omitempty"`
Error string `json:"error,omitempty"`
// Excerpt says Result/Error was bounded for this WebSocket/init payload;
// the Conversation action still opens the complete persisted transcript.
Excerpt bool `json:"excerpt,omitempty"`
FinishedAtMs int64 `json:"finished_at_ms,omitempty"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
CostUSD float64 `json:"cost_usd"`
}
SubagentEndData is sent when a subagent finishes, carrying its usage/cost.
type SubagentEventData ¶
SubagentEventData wraps a single translated bus event from a subagent child, namespaced by JobID. Event is produced by re-applying wsEventFromBus to the inner (already-typed) bus event — same shape as a top-level WS event.
type SubagentInitData ¶
type SubagentInitData struct {
JobID string `json:"job_id"`
OriginToolCallID string `json:"origin_tool_call_id,omitempty"`
Task string `json:"task"`
Title string `json:"title,omitempty"`
Model string `json:"model"`
Thinking string `json:"thinking"`
Status string `json:"status"`
Async bool `json:"async"`
Messages []core.AgentMessage `json:"messages"`
// StartedAtMs is the child's start time as epoch milliseconds (same
// encoding as InitData.RunStartedAtMs), so a reconnecting client resumes
// its live elapsed timer instead of restarting it. Omitted when unknown.
StartedAtMs int64 `json:"started_at_ms,omitempty"`
// InputTokens/OutputTokens/CostUSD are the child's accumulated usage/cost
// so far, so live cost doesn't reset to zero after a reconnect. Omitted
// (zero) until the child has closed at least one message.
InputTokens int `json:"input_tokens,omitempty"`
OutputTokens int `json:"output_tokens,omitempty"`
CostUSD float64 `json:"cost_usd,omitempty"`
// ContextPercent restores the child's own context reading after a
// reconnect (0-100, -1 unknown) — see SubagentUsageData.ContextPercent.
ContextPercent int `json:"context_percent"`
// AccentIndex is the subagent's stable per-session creation ordinal, used
// by the client to derive a deterministic accent color that survives
// reconnects (instead of one derived from array/map position).
AccentIndex int `json:"accent_index"`
}
SubagentInitData describes one live subagent job for reconnecting clients (WS init snapshot), so a client that connects mid-run sees the agent tray and its accumulated transcript instead of starting empty.
type SubagentStartData ¶
type SubagentStartData struct {
JobID string `json:"job_id"`
OriginToolCallID string `json:"origin_tool_call_id,omitempty"`
Task string `json:"task"`
Title string `json:"title,omitempty"`
Model string `json:"model"`
Thinking string `json:"thinking"`
Async bool `json:"async"`
// StartedAtMs is the child's start time as epoch milliseconds (same
// encoding as InitData.RunStartedAtMs), so the client can compute live
// elapsed time (now - StartedAtMs). Omitted when unknown.
StartedAtMs int64 `json:"started_at_ms,omitempty"`
// AccentIndex is the subagent's stable per-session creation ordinal (see
// SubagentInitData.AccentIndex). Never omitted: 0 is a valid ordinal
// (the session's first subagent).
AccentIndex int `json:"accent_index"`
}
SubagentStartData is sent when a subagent (sync or async) begins.
type SubagentSummary ¶
type SubagentSummary struct {
JobID string `json:"job_id"`
Task string `json:"task"`
Title string `json:"title,omitempty"`
Model string `json:"model,omitempty"`
Thinking string `json:"thinking,omitempty"`
Status string `json:"status"`
Async bool `json:"async"`
StartedAt time.Time `json:"started_at,omitempty"`
FinishedAt time.Time `json:"finished_at,omitempty"`
Source string `json:"source"`
// Usage/cost/context of the CHILD, so a client reopening a finished
// subagent shows the same figures its live view showed. ContextPercent is
// -1 when unknown; the tokens and cost are omitted when zero.
InputTokens int `json:"input_tokens,omitempty"`
OutputTokens int `json:"output_tokens,omitempty"`
CostUSD float64 `json:"cost_usd,omitempty"`
ContextPercent int `json:"context_percent"`
}
SubagentSummary describes a subagent available to an owner-authorized client, including the delegated task.
type SubagentUsageData ¶
type SubagentUsageData struct {
JobID string `json:"job_id"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
CostUSD float64 `json:"cost_usd"`
// ContextPercent is how full the CHILD's own window is (0-100), or -1 when
// its model has no known window. Sent unconditionally (no omitempty): 0 is
// a real reading, and a client that fell back to the parent's percentage
// would be showing a number about a different agent.
ContextPercent int `json:"context_percent"`
}
SubagentUsageData carries a subagent's accumulated usage/cost while it is still running, emitted each time the child closes a message. It lets the client show live tokens/cost before the terminal subagent_end. The cost is computed the same way subagent_end computes its total, so the live value stays consistent with the final one.
type TasksUpdateData ¶
type TasksUpdateData struct {
Tasks any `json:"tasks"`
}
TasksUpdateData carries the full task list after a change.
type ToolCallDeltaData ¶
type ToolCallDeltaData struct {
ToolCallID string `json:"tool_call_id"`
Args map[string]any `json:"args"`
}
ToolCallDeltaData carries incrementally-parsed tool call arguments.
type ToolCallStreamingData ¶
type ToolCallStreamingData struct {
ToolCallID string `json:"tool_call_id"`
ToolName string `json:"tool_name"`
}
ToolStartData is sent when a tool execution begins. ToolCallStreamingData is sent when the LLM starts generating a tool call.
type ToolEndData ¶
type ToolEndData struct {
ToolCallID string `json:"tool_call_id"`
ToolName string `json:"tool_name"`
IsError bool `json:"is_error"`
Rejected bool `json:"rejected"`
Result string `json:"result"`
}
ToolEndData is sent when a tool execution completes.
type ToolStartData ¶
type ToolStartData struct {
ToolCallID string `json:"tool_call_id"`
ToolName string `json:"tool_name"`
Args map[string]any `json:"args"`
// StartLine is the real 1-based file line where an edit's oldText starts,
// so the frontend diff preview shows real line numbers before the tool
// result arrives. 0 when unknown (frontend numbers from 1). Edit tool only.
StartLine int `json:"start_line,omitempty"`
}
type ToolUpdateData ¶
ToolUpdateData carries streaming tool output.
type UserMessageData ¶ added in v0.21.0
type UserMessageData struct {
MsgID string `json:"msg_id,omitempty"`
Text string `json:"text,omitempty"`
Content []core.Content `json:"content,omitempty"`
Custom map[string]any `json:"custom,omitempty"`
}
UserMessageData is sent when a user prompt starts a new run, so every connected client (not just the one that issued it) renders the message live. Content carries the message's blocks for a structured send (attachments + text); Text carries a plain-text prompt. Clients dedup by MsgID against their own optimistic echo and against the reconnect snapshot.
Source Files
¶
- attachments.go
- attachments_download.go
- attachstore.go
- attachstore_unix.go
- attention.go
- automation.go
- automation_interact.go
- automation_mcp.go
- autotitle.go
- brief.go
- cacheclock.go
- callback.go
- commands.go
- compact_at.go
- compress.go
- conversation.go
- device_auth.go
- device_lock_unix.go
- events.go
- files.go
- fs_complete.go
- generate.go
- guardian_ws.go
- history.go
- manager.go
- mcp_toggle.go
- model_preferences.go
- persist.go
- pulse_auth_api.go
- push.go
- realtime_client_secret.go
- route_auth.go
- scheduler.go
- secrets.go
- sendfile.go
- server.go
- session_config.go
- session_lifecycle.go
- static_assets.go
- subagent_conversation.go
- subagent_models.go
- unread_results.go
- usage_cache.go
- ws.go