Documentation
¶
Overview ¶
Package host implements the embedded assistant-host protocol.
The transport is stdio NDJSON — one JSON object per line: stdin carries the inbound command stream (first line = SessionDescriptor, every subsequent line = a HostCommand); stdout carries the outbound HostEvent stream; stderr carries human-readable diagnostics. Protocol JSON is NEVER written to stderr (and diagnostics are NEVER written to stdout) — Daintree Zod-validates stdout line-by-line and rejects unknown shapes.
This file is the pure protocol layer: the version constant, the verbatim string vocabularies, the wire event/command types with their JSON shapes, the severity map, and the descriptor/command parsers. No I/O.
The wire contract — event names, command names, field names, vocabulary values, PROTOCOL_VERSION — is the one hard external contract and must stay byte-for-byte with Daintree.
Index ¶
- Constants
- Variables
- func Run(ctx context.Context, factory AppFactory) int
- func Serve(ctx context.Context, factory AppFactory, in io.Reader, out, errw io.Writer)
- type App
- type AppFactory
- type AppHooks
- type AppParams
- type AuditResult
- type AuditSeverity
- type Bridge
- func (b *Bridge) AssistantCancelled(string)
- func (b *Bridge) AssistantEnd(content, _ string)
- func (b *Bridge) AssistantStart()
- func (b *Bridge) AssistantToken(chunk string)
- func (b *Bridge) Confirm(ctx context.Context, req ConfirmRequest) bool
- func (b *Bridge) Error(message string)
- func (b *Bridge) Info(string)
- func (b *Bridge) Interjection(string)
- func (b *Bridge) Interrupt()
- func (b *Bridge) ModelRateLimited()
- func (b *Bridge) Phase(domain.RunPhase)
- func (b *Bridge) ResolveApproval(approvalID string, decision ConfirmationDecision)
- func (b *Bridge) SettlePendingApprovals(decision ConfirmationDecision)
- func (b *Bridge) SettleTurn(outcome TurnOutcomeClass)
- func (b *Bridge) SkillLoaded([]string)
- func (b *Bridge) StartExchange()
- func (b *Bridge) ToolBatch([]agent.BatchedToolCall)
- func (b *Bridge) ToolCall(ev agent.ToolCallEvent)
- func (b *Bridge) ToolProgress(string, string)
- func (b *Bridge) ToolResult(ev agent.ToolResultEvent)
- func (b *Bridge) ToolState(string, agent.ToolState)
- func (b *Bridge) TurnPrompt(string)
- func (b *Bridge) Usage(agent.UsageEvent)
- func (b *Bridge) Warn(string)
- type BridgeOptions
- type ConfirmRequest
- type ConfirmationDecision
- type EvApprovalDecided
- type EvApprovalRequested
- type EvError
- type EvReady
- type EvShutdown
- type EvToolSettled
- type EvToolStarted
- type EvTurnEnd
- type EvTurnStart
- type EvTurnToken
- type Host
- type HostCommand
- type HostCommandType
- type HostEvent
- type HostShutdownReason
- type PostFunc
- type RiskOfFunc
- type SessionDescriptor
- type TurnOutcomeClass
- type TurnRole
Constants ¶
const ( // DefaultApprovalTimeoutMs is the unanswered-confirm auto-timeout (5 min). DefaultApprovalTimeoutMs = 5 * 60_000 // 300000 )
Approval/redaction constants.
const ProtocolVersion = 2
PROTOCOL_VERSION is the wire-format version. MUST equal Daintree's ASSISTANT_HOST_PROTOCOL_VERSION; Daintree Zod-rejects an unrecognized version.
It is 2: the transport is stdio NDJSON line frames. The framing is a breaking change for any consumer of an older format, so the version moves in lockstep.
Variables ¶
var ErrAppFactoryUnset = errors.New("host: App factory not yet wired (cockpit/cli wave)")
ErrAppFactoryUnset is returned by the placeholder factory until the concrete App is wired. It lets `host --stdio` build green now and boot-fail cleanly (host:error bootstrap-error) rather than panic.
Functions ¶
func Run ¶
func Run(ctx context.Context, factory AppFactory) int
Run is the os-wired entry the CLI's `host --stdio` subcommand calls. It checks the stdio precondition (stdin must be a usable command stream), then Serves over os.Stdin/os.Stdout/os.Stderr. factory is the App builder.
Returns an exit code only when the precondition fails or Serve unwinds without exiting; on the normal path teardown calls os.Exit directly.
func Serve ¶
Serve runs the embedded host over the given streams until teardown exits the process. It is the headless protocol core: in = command stdin (NDJSON, first line = descriptor), out = event stdout (NDJSON), errw = diagnostics stderr. factory builds the App for the booted session (the cockpit/cli wave provides the concrete factory). Serve normally never returns — teardown calls os.Exit after flushing — but it returns if Run unwinds without exiting (e.g. tests injecting a non-exiting exit func).
Types ¶
type App ¶
type App interface {
// SetHooks installs the agent event sink (bridge.sink) and the tool-confirm
// hook (bridge.Confirm). Must be called before ConnectMCP/StartScheduler so a
// wake or early tool call is bridged.
SetHooks(hooks AppHooks)
// ConnectMCP attempts the MCP connection. Best-effort: a degraded MCP is NOT a
// boot failure (it surfaces in prompt context + tool results), so the returned
// error is informational only — the host logs it to stderr and proceeds.
ConnectMCP(ctx context.Context) error
// StartScheduler starts the daemon (watchers/timers tick in-host). onAttention
// receives each surfaced attention burst; the host filters for actionable wakes.
StartScheduler(onAttention func(events []domain.QueueEvent))
// RearmAttention durably re-arms delivered-but-unhandled attention events
// (nulls their notifiedAt in the project store) so the NEXT owner's notify
// pass re-digests and re-delivers them. Teardown calls it with whatever is
// left in pendingWake — a burst a shutdown/hibernate-cancelled wake turn
// requeued, or one that was queued but never started: those events were
// already marked notified when they were handed to this process, and the
// in-memory queue dies with it — without the durable re-arm the wake would
// be silently lost across the restart. Best-effort (the host only logs a
// failure).
RearmAttention(ids []string) error
// Session is the turn engine driven by prompt/wake.
Session() *agent.Session
// RiskOf looks up a tool's risk class for the danger hint (false if unknown).
RiskOf(toolName string) (domain.RiskClass, bool)
// Config is the resolved runtime config (used for debug-log start).
Config() config.AppConfig
// Shutdown tears down the runtime (best-effort; the host exits regardless).
Shutdown(ctx context.Context) error
}
App is the SEAM to the full assistant runtime. The host depends on this interface and the cockpit/cli wave fills it with the concrete App. The surface it needs: wire the agent event sink + confirm hook, connect MCP best-effort, start the daemon, drive the session, and shut down.
The host NEVER touches the DB, models, or tools directly — everything flows through this seam, so this package compiles in isolation against the providers that already exist (agent/config/domain).
type AppFactory ¶
AppFactory builds the App for a booted session. MCP url/token/tier/projectId come from env via loadConfig, NOT the descriptor. The cockpit/cli wave provides the concrete factory; the host stores it so it can be tested with a fake.
type AppHooks ¶
type AppHooks struct {
// AgentEvents is the bridge sink (agent.EventSink). The session emits through it.
AgentEvents agent.EventSink
// Confirm is the tool-confirm hook: a mutating tool calls it and blocks until
// the approval is decided (true) / rejected / times out (false).
Confirm func(ctx context.Context, req ConfirmRequest) bool
}
AppHooks bundles the hooks the host installs on the App.
type AppParams ¶
type AppParams struct {
SessionID string // appSessionId: resume id when resuming, else session id
ProjectPath string // descriptor.cwd → overrides.projectPath
ProjectInstructions string // loaded DAINTREE.md content → overrides
}
AppParams is the descriptor-derived input to AppFactory. appSessionId is resumeSessionId ?? sessionId (so resumed conversation state replays).
type AuditResult ¶
type AuditResult string
AuditResult is the settled-tool result classification (7 values).
const ( AuditSuccess AuditResult = "success" AuditError AuditResult = "error" AuditConfirmationPending AuditResult = "confirmation-pending" AuditDedup AuditResult = "dedup" AuditCollision AuditResult = "collision" AuditRateLimited AuditResult = "rate_limited" )
type AuditSeverity ¶
type AuditSeverity string
AuditSeverity is the severity bucket for an audited result (5 values). Note "critical" is in the set for Daintree parity but is never produced by SeverityForResult.
const ( SeverityInfo AuditSeverity = "info" SeverityNotice AuditSeverity = "notice" SeverityWarning AuditSeverity = "warning" SeverityErrorSev AuditSeverity = "error" SeverityCritical AuditSeverity = "critical" )
func SeverityForResult ¶
func SeverityForResult(r AuditResult) AuditSeverity
SeverityForResult maps an AuditResult to its AuditSeverity (unknown → error).
type Bridge ¶
type Bridge struct {
// contains filtered or unexported fields
}
Bridge adapts the in-process agent EventSink + tool-confirm hook into wire HostEvents. It owns the single-turn lifecycle, approvals, redaction, and audit mapping. The agent loop runs Send() on another goroutine and calls the sink methods concurrently, so all mutable state is guarded by mu. No transport dependency — events go through the injected Post.
func NewBridge ¶
func NewBridge(opts BridgeOptions) *Bridge
NewBridge builds a Bridge with defaults filled.
func (*Bridge) AssistantCancelled ¶
func (*Bridge) AssistantEnd ¶
AssistantEnd closes the turn: "answered" if content is non-blank else "unknown". (reasoning is not forwarded over the host protocol.)
func (*Bridge) AssistantStart ¶
func (b *Bridge) AssistantStart()
func (*Bridge) AssistantToken ¶
func (*Bridge) Confirm ¶
func (b *Bridge) Confirm(ctx context.Context, req ConfirmRequest) bool
Confirm is the tool-confirm hook: mint an approval, emit approval:requested, and block until decided / timed out / drained. Returns true iff approved. Runs on the agent's dispatch goroutine; the command loop resolves via decide/drain.
The emitted approval:requested carries the request's display context — risk class (passed through verbatim, so a per-confirm override survives), the human-readable consequence, and a redacted args summary (redactArgs, the same helper tool:started uses) — so Daintree's timeline matches a local cockpit approval. Empty fields are omitted by the event encoder.
func (*Bridge) Interjection ¶
Interjection has no host-protocol channel: the Daintree parent already holds the text it sent as the mid-turn prompt (handlePrompt routes it to InjectPrompt while busy), so echoing it back would be redundant. Dropped, like Phase.
func (*Bridge) Interrupt ¶
func (b *Bridge) Interrupt()
Interrupt is the display side of an interrupt: latch interrupted, stop forwarding the in-flight turn, close it "agent-stuck". No-op without an active turn.
func (*Bridge) ModelRateLimited ¶
func (b *Bridge) ModelRateLimited()
ModelRateLimited is a live cockpit health cue with no host-protocol channel — dropped. The "Model rate-limited" reply still flows through the normal turn text.
func (*Bridge) ResolveApproval ¶
func (b *Bridge) ResolveApproval(approvalID string, decision ConfirmationDecision)
ResolveApproval settles an outstanding approval (decide / timeout / drain). No-op if not pending. Emits approval:decided and unblocks the Confirm caller.
func (*Bridge) SettlePendingApprovals ¶
func (b *Bridge) SettlePendingApprovals(decision ConfirmationDecision)
SettlePendingApprovals rejects (default) every outstanding approval. Used on interrupt + teardown drain so a parked dispatch never strands busy.
func (*Bridge) SettleTurn ¶
func (b *Bridge) SettleTurn(outcome TurnOutcomeClass)
SettleTurn closes any dangling assistant turn (no-op if already closed). Called in the prompt/wake finally.
func (*Bridge) SkillLoaded ¶
SkillLoaded has no host-protocol channel (the parent doesn't surface the assistant's internal skill loads); dropped, like Interjection.
func (*Bridge) StartExchange ¶
func (b *Bridge) StartExchange()
StartExchange resets per-turn state and emits a zero-duration user turn (start+end at the same ts). Prompt text is NOT carried — Daintree originated it.
func (*Bridge) ToolBatch ¶
func (b *Bridge) ToolBatch([]agent.BatchedToolCall)
ToolBatch/ToolState/ToolProgress are live-footer-only in the loop; the host protocol keys off the concrete tool:started/tool:settled events, so the in-tool substep stream has no host channel and is dropped.
func (*Bridge) ToolCall ¶
func (b *Bridge) ToolCall(ev agent.ToolCallEvent)
func (*Bridge) ToolProgress ¶
func (*Bridge) ToolResult ¶
func (b *Bridge) ToolResult(ev agent.ToolResultEvent)
func (*Bridge) TurnPrompt ¶
TurnPrompt has no host-protocol channel — Daintree originated the prompt, so the bridge drops it (it's persisted for /explain by the run-event sink).
func (*Bridge) Usage ¶
func (b *Bridge) Usage(agent.UsageEvent)
Usage is not forwarded over the host protocol (token/cost stays in-process).
type BridgeOptions ¶
type BridgeOptions struct {
SessionID string
Post PostFunc
RiskOf RiskOfFunc // default: always unknown
Now func() int64 // default: domain.NowMS
ApprovalTimeoutMs int // default: DefaultApprovalTimeoutMs (0 disables the timer)
}
BridgeOptions configures a Bridge.
type ConfirmRequest ¶
type ConfirmRequest struct {
ToolName string
Summary string
RiskClass domain.RiskClass
Consequence string
// RawArgs is the raw JSON args string the model emitted. The bridge redacts it
// (redactArgs) before emitting the wire event — it never crosses verbatim.
RawArgs string
}
ConfirmRequest is the confirm payload the host bridges to an approval. The App adapts its tool-context ConfirmRequest (tools.ConfirmRequest) into this when calling the installed hook. Beyond the tool name + summary, it carries the display context the approval:requested wire event surfaces so Daintree's timeline matches a local cockpit approval: the risk class, the human-readable consequence, and the raw args (redacted by the bridge before they cross the wire). RiskClass is passed through (not re-derived from the registry) so a tool's explicit per-confirm override — e.g. grant.create electing RiskSystem — reaches the UI verbatim.
Intentional scope boundary: this bridge deliberately drops NeedsTypedConfirm. The embedded host does not render its own approval sheet — it delegates the decision to its external caller (Daintree's orchestration UI), which owns the approval UX. The typed-confirm friction (issue #210) is enforced on the surfaces that DO render the sheet — the cockpit and the classic REPL — not here. The risk-class label travels on RiskClass above so the external timeline can still display it.
type ConfirmationDecision ¶
type ConfirmationDecision string
ConfirmationDecision is the outcome of an approval (3 values).
const ( DecisionApproved ConfirmationDecision = "approved" DecisionRejected ConfirmationDecision = "rejected" DecisionTimeout ConfirmationDecision = "timeout" )
type EvApprovalDecided ¶
type EvApprovalDecided struct {
ApprovalID string
Decision ConfirmationDecision
DecidedAt int64
}
EvApprovalDecided — approval:decided.
type EvApprovalRequested ¶
type EvApprovalRequested struct {
ApprovalID string
ToolID string
Summary string
RequestedAt int64
TurnID string
RiskClass domain.RiskClass
Consequence string
ArgsSummary string
}
EvApprovalRequested — approval:requested. turnId optional. riskClass, consequence, and argsSummary are optional display context (parity with a local cockpit approval); each is omitted from the wire object when empty.
type EvReady ¶
EvReady — host:ready. resumedSessionId only set when the descriptor carried a resumeSessionId.
type EvShutdown ¶
type EvShutdown struct {
Reason HostShutdownReason
ResumeSessionID string
}
EvShutdown — host:shutdown. resumeSessionId optional. Emitted FIRST in teardown.
type EvToolSettled ¶
type EvToolSettled struct {
ToolCallID string
ToolID string
DurationMs int64
Result AuditResult
Severity AuditSeverity
ErrorCode string
TurnID string
// AsyncID marks an ACCEPTED-but-still-running async operation (asy_…): the
// call settled but the work continues in the background, so a host must NOT
// render it as a finished success (the cockpit shows it as a distinct yellow
// pending state). Empty for every ordinary synchronous result.
AsyncID string
}
EvToolSettled — tool:settled. errorCode + turnId + asyncId optional.
type EvToolStarted ¶
type EvToolStarted struct {
ToolCallID string
ToolID string
ArgsSummary string
StartedAt int64
TurnID string
Danger bool
}
EvToolStarted — tool:started. turnId optional.
type EvTurnEnd ¶
type EvTurnEnd struct {
TurnID string
EndedAt int64
Outcome TurnOutcomeClass
}
EvTurnEnd — turn:end. Outcome optional.
type EvTurnStart ¶
EvTurnStart — turn:start.
type Host ¶
type Host struct {
// contains filtered or unexported fields
}
Host owns the transport, the descriptor handshake, the command loop, the wake reactor, boot, and teardown. It is driven by Run(ctx). Process-level state (busy/ready/pendingWake/…) is owned by the single command-loop goroutine, EXCEPT the Bridge's own state (the agent loop calls the sink concurrently and uses its own mutex). The command loop MUST NOT block on a running Send — send runs on a worker goroutine while the loop keeps servicing interrupt/approval:decide (the key structural property of the event loop).
type HostCommand ¶
type HostCommand struct {
Type HostCommandType
SessionID string
// prompt
Text string
// approval:decide
ApprovalID string
Decision string
}
HostCommand is a decoded inbound command. Only the fields relevant to the arm are populated; the rest are zero. SessionID is always required.
func ParseCommand ¶
func ParseCommand(line []byte) (HostCommand, error)
ParseCommand decodes + validates an inbound command line: object with a string sessionId, then a per-type field check. An unknown type or a missing required field → errNotCommand (drop).
type HostCommandType ¶
type HostCommandType string
HostCommandType is the inbound command discriminator.
const ( CmdPrompt HostCommandType = "prompt" CmdApprovalDecide HostCommandType = "approval:decide" CmdInterrupt HostCommandType = "interrupt" CmdHibernate HostCommandType = "hibernate" CmdShutdown HostCommandType = "shutdown" )
type HostEvent ¶
type HostEvent interface {
// contains filtered or unexported methods
}
HostEvent is the outbound (host → Daintree) wire union. Every event carries a "type" discriminator + "sessionId"; the Go encoder writes each as one NDJSON line on stdout. Concrete event structs implement encode() which injects "type" and "sessionId" so callers never repeat them. Field names are verbatim wire.
type HostShutdownReason ¶
type HostShutdownReason string
HostShutdownReason is why teardown ran (4 values). "revoke" is kept for Daintree parity even though index/teardown never emits it.
const ( ShutdownHibernate HostShutdownReason = "hibernate" ShutdownRevoke HostShutdownReason = "revoke" ShutdownError HostShutdownReason = "error" ShutdownExit HostShutdownReason = "exit" )
type PostFunc ¶
type PostFunc func(HostEvent)
PostFunc is the transport sink the bridge writes events through. Injected so the bridge has no transport dependency (tests + the NDJSON owner share it).
type RiskOfFunc ¶
RiskOfFunc looks up a tool's risk class for the danger hint. The bool is false when the tool is unknown.
type SessionDescriptor ¶
type SessionDescriptor struct {
SessionID string `json:"sessionId"`
WindowID int64 `json:"windowId"`
ProjectID string `json:"projectId"`
Cwd string `json:"cwd"`
Tier string `json:"tier"`
ProtocolVersion int `json:"protocolVersion"`
ResumeSessionID string `json:"resumeSessionId,omitempty"`
}
SessionDescriptor is the non-secret handshake Daintree sends as the very first inbound line. windowId/projectId/tier are validated here but the live binding values come from env via loadConfig — a leaked descriptor cannot re-bind.
func ParseDescriptor ¶
func ParseDescriptor(line []byte) (SessionDescriptor, error)
ParseDescriptor decodes + validates a descriptor line: the six required fields must be present with the right JSON types; resumeSessionId is optional and NOT type-checked. A failing line yields an error (caller emits host:error code bad-descriptor + teardown).
type TurnOutcomeClass ¶
type TurnOutcomeClass string
TurnOutcomeClass classifies how a turn ended (12 values).
const ( OutcomeAnswered TurnOutcomeClass = "answered" OutcomeHedged TurnOutcomeClass = "hedged" OutcomeRefused TurnOutcomeClass = "refused" OutcomeDocsEmpty TurnOutcomeClass = "docs-empty" OutcomeTierRejected TurnOutcomeClass = "tier-rejected" OutcomeMcpNotReady TurnOutcomeClass = "mcp-not-ready" OutcomeAgentStuck TurnOutcomeClass = "agent-stuck" OutcomeToolError TurnOutcomeClass = "tool-error" OutcomeReasoningLoop TurnOutcomeClass = "reasoning-loop" OutcomeHibernateResumeStale TurnOutcomeClass = "hibernate-resume-stale" OutcomeCancelled TurnOutcomeClass = "cancelled" OutcomeUnknown TurnOutcomeClass = "unknown" )