Documentation
¶
Overview ¶
Package protocol is the shared vocabulary between the dcode daemon and its clients. It holds no logic and performs no I/O: both sides import it so the wire contract has exactly one definition.
Spec: docs/specs/architecture/client-server-protocol/202608072240-*.
Index ¶
- Constants
- func HTTPStatus(code string) int
- type ApprovalDecision
- type ApprovalRequest
- type ApprovalResolved
- type Completion
- type ContextBand
- type CreateSessionRequest
- type Error
- type Event
- type EventType
- type MessageDelta
- type MessageReasoning
- type PlanItem
- type PlanUpdated
- type Progress
- type RenameSessionRequest
- type ResolveApprovalRequest
- type Session
- type SessionCompacted
- type SessionRenamed
- type SessionResumed
- type SessionState
- type SteerRequest
- type SubmitTurnRequest
- type ToolCompleted
- type ToolRequested
- type TurnCompleted
- type TurnImage
- type TurnStarted
- type TurnSteered
- type UndoResult
- type Usage
Constants ¶
const ( CodeSessionNotFound = "session_not_found" CodeTurnAlreadyActive = "turn_already_active" // CodeNoActiveTurn answers a correction aimed at a turn that is not // running. Distinct from turn_already_active and its exact mirror: one says // "wait", the other says "there is nothing to correct — send it as a // message". Collapsing them would leave the client guessing which. CodeNoActiveTurn = "no_active_turn" CodeInvalidInput = "invalid_input" CodeApprovalResolved = "approval_already_resolved" CodeApprovalExpired = "approval_expired" CodeEventsExpired = "events_expired" CodeWorkspaceInvalid = "workspace_invalid" CodeMaxSessionsReached = "max_sessions_reached" CodeInternal = "internal" )
Error codes. Section 8 of the planning spec.
const ( PlanPending = "pending" PlanActive = "active" PlanDone = "done" PlanBlocked = "blocked" )
Plan item statuses.
const ( StopDone = "done" StopInterrupted = "interrupted" StopMaxIterations = "max_iterations" StopRepeatLoop = "repeat_loop" StopMaxTokens = "max_tokens" StopError = "error" // StopUnverified and StopIncomplete are RESULTS, not errors. They are the // honest state of work delivered without a check having passed. // // Treating them as failure would create exactly the wrong incentive: the // easy way out becomes switching the checking off. StopUnverified = "unverified" StopIncomplete = "incomplete" )
Turn stop reasons, carried in TurnCompleted.Reason.
const ( // ProgressRounds is the turn against its iteration ceiling. ProgressRounds = "rounds" // ProgressInFlight is how many tool calls are running together, against // the concurrency ceiling this session allows. ProgressInFlight = "in_flight" // ProgressArguments is a tool call still arriving from the model, counted // in bytes of its arguments. // // Bytes rather than lines: what has landed is a fragment of JSON, and // counting lines inside half an escaped string counts something that is // not there yet. It has no total — the model does not say how long the // call will be, and a denominator nobody sent is one somebody would trust. ProgressArguments = "arguments" // ProgressFiles is a scan working through files. Total is known when the // tool had the list before it started and absent when it is still walking. ProgressFiles = "files" )
What a Progress counts. A closed set, so a client can say it in the reader's language instead of printing the daemon's.
Only what something actually emits is declared. A kind nobody writes is a promise on a versioned surface that no code keeps.
const Version = "v1"
Version is the protocol version carried in the URL prefix.
Variables ¶
This section is empty.
Functions ¶
func HTTPStatus ¶
HTTPStatus returns the status for a code, or 500 for an unmapped one.
Types ¶
type ApprovalDecision ¶
type ApprovalDecision string
ApprovalDecision is the user's answer to a boundary crossing.
const ( ApprovalAllow ApprovalDecision = "allow" ApprovalAllowSession ApprovalDecision = "allow_session" ApprovalDeny ApprovalDecision = "deny" // The two answers that outlive the session, because some questions are // about the project rather than about the moment. Asking again next week // about a decision already made is how a prompt becomes something people // dismiss without reading — and a prompt nobody reads protects nobody. // // They are recorded in the USER's config root, never in the workspace: a // grant living inside a project would let a repository arrive // pre-approved, so cloning something would permit it before anyone read a // line of it. ApprovalAllowProject ApprovalDecision = "allow_project" ApprovalAllowAlways ApprovalDecision = "allow_always" )
func (ApprovalDecision) Grants ¶
func (d ApprovalDecision) Grants() bool
Grants reports whether a decision permits the crossing at all.
func (ApprovalDecision) Remembered ¶
func (d ApprovalDecision) Remembered() bool
Remembered reports whether a decision outlives the session, and how widely.
Named rather than compared inline, so the two places that persist a grant and the one that renders it cannot drift into disagreeing about which answers are standing ones.
func (ApprovalDecision) Valid ¶
func (d ApprovalDecision) Valid() bool
Valid reports whether d is a decision the server accepts.
type ApprovalRequest ¶
type ApprovalRequest struct {
ApprovalID string `json:"approval_id"`
TurnID string `json:"turn_id"`
ToolCallID string `json:"tool_call_id"`
Tool string `json:"tool"`
Command string `json:"command,omitempty"`
BoundaryCrossed string `json:"boundary_crossed"`
ExpiresAt time.Time `json:"expires_at"`
// Reason says why in a sentence, and Rule names the pattern that raised the
// question when one did. Consent to a rule nobody can see is consent to
// nothing, and Rule is also what an "allow for the session" answer is
// remembered against.
Reason string `json:"reason,omitempty"`
Rule string `json:"rule,omitempty"`
}
ApprovalRequest is emitted when execution crosses the sandbox boundary. The turn blocks until it is resolved or ExpiresAt passes, which denies.
type ApprovalResolved ¶
type ApprovalResolved struct {
ApprovalID string `json:"approval_id"`
Decision ApprovalDecision `json:"decision"`
}
ApprovalResolved records the decision that unblocked a turn.
type Completion ¶
type Completion struct {
// Verification is the single-criterion seal: clean, passed, failed,
// stale or unavailable.
Verification string `json:"verification"`
// Met and Unmet name the criteria, so a client can show which.
Met []string `json:"met,omitempty"`
Unmet []string `json:"unmet,omitempty"`
// unmet, and shown differently: nothing was learned about them.
Unavailable []string `json:"unavailable,omitempty"`
// TouchedProtected are paths that are part of how the work is measured
// and were written this turn. Never omitted when present.
TouchedProtected []string `json:"touched_protected,omitempty"`
}
Completion is the state of the done criteria when a turn ended.
type ContextBand ¶ added in v0.5.0
ContextBand is the context crossing a threshold on the way to being summarised.
The model is told this already, and has been for a while. The PERSON was not, so the summary arrived as a line saying it had happened — after the fact, with no warning that it was coming and no way to finish a thought first.
Fraction is of the BUDGET, not of the window: the budget is the space before compaction, so 0.80 means eighty per cent of the way to a summary. Against the window it would be a number about a limit that never arrives.
type CreateSessionRequest ¶
type CreateSessionRequest struct {
Workspace string `json:"workspace"`
Model string `json:"model,omitempty"`
SandboxMode string `json:"sandbox_mode,omitempty"`
// Resume names a recorded session whose conversation this one continues.
//
// A new session either way: the old one ended with the client that ran it,
// and nothing survives what created it. What carries over is the history,
// rebuilt from the record — not the approvals, which were consent given in
// a moment that has passed, and not the background processes, which died
// with the session that started them.
Resume string `json:"resume,omitempty"`
}
CreateSessionRequest opens a session. Workspace must be absolute.
type Error ¶
Error is the wire error. Code is the stable machine identifier; Message is human text in English. Localisation, if it ever exists, hangs off Code and never off Message.
type Event ¶
type Event struct {
Seq uint64 `json:"seq"`
SessionID string `json:"session_id"`
Type EventType `json:"type"`
At time.Time `json:"at"`
Payload json.RawMessage `json:"payload"`
}
Event is the envelope every observable fact travels in. Seq is per session, monotonic from 1, never reused and never gapped.
At is the only non-deterministic field; golden comparisons zero it.
type EventType ¶
type EventType string
EventType identifies the payload shape of an Event.
const ( EventSessionCreated EventType = "session.created" // EventSessionResumed opens the conversation this session continues. // // The events after it happened somewhere else, and a marker is what keeps // the transcript honest about that: without one, a replayed conversation // reads as work this session did. It also carries the only thing the reader // cannot reconstruct from the events themselves — which session they came // from. EventSessionResumed EventType = "session.resumed" EventTurnStarted EventType = "turn.started" EventMessageDelta EventType = "message.delta" // EventMessageReasoning carries the model's thinking, which is not its // answer. It is an event so a client can show it and so replaying a session // looks like having watched it — but it never enters the history the model // is sent, because a model that reads its own reasoning back as something // it said out loud starts defending it. EventMessageReasoning EventType = "message.reasoning" EventToolRequested EventType = "tool.requested" EventApprovalRequired EventType = "tool.approval_required" EventApprovalResolved EventType = "tool.approval_resolved" EventToolCompleted EventType = "tool.completed" // EventTurnSteered is the person correcting a turn already under way. // // Its own event rather than a message delta: a correction the transcript // cannot tell apart from the model's own words is a turn nobody can audit // afterwards, and "why did it change direction" is the first question asked // of any run that went sideways. EventTurnSteered EventType = "turn.steered" EventTurnCompleted EventType = "turn.completed" EventPlanUpdated EventType = "plan.updated" // EventProgress answers "how far along", and it is the only event here // that is not a fact worth replaying on its own. // // One event rather than one per question. A tool counting files and a turn // counting rounds are the same question asked of different subjects, and // adding a versioned surface twice for one kind of question is how it comes // out crooked — the second one always answers slightly differently. // // It joins the log and the record like message.delta does: chatty, batched // rather than flushed, and carrying a Seq. Giving it no Seq would have been // the alternative, and it would have put a gap in the one property the // record is built on. EventProgress EventType = "progress" // EventSessionRenamed is a name a person gave this conversation. // // An event rather than a file beside the record, and the record is where it // belongs for one reason above the others: a name for a conversation that // no longer exists is nothing. Pruning removes the transcript, and a name // stored anywhere else would outlive what it named — a listing full of // titles for sessions nobody can open. // // It also keeps the count at one. A second store beside the log is a second // thing that can disagree with the first, and this protocol's whole shape // is that every observable fact travels the same way. EventSessionRenamed EventType = "session.renamed" EventSessionCompacted EventType = "session.compacted" EventContextBand EventType = "context.band" EventSessionError EventType = "session.error" )
Event types. Payload shapes are defined in section 5.1 of the planning spec.
type MessageDelta ¶
MessageDelta is a fragment of model text.
type MessageReasoning ¶
MessageReasoning is a fragment of the model's thinking.
type PlanItem ¶
type PlanItem struct {
ID int `json:"id"`
Text string `json:"text"`
Status string `json:"status"`
Blocked string `json:"blocked,omitempty"`
}
PlanItem is one entry of the session plan, maintained by the plan tool.
type PlanUpdated ¶
type PlanUpdated struct {
Items []PlanItem `json:"items"`
}
PlanUpdated is the payload of EventPlanUpdated.
type Progress ¶ added in v0.2.0
type Progress struct {
TurnID string `json:"turn_id"`
// ToolCallID names the call this is about; empty means the turn itself.
ToolCallID string `json:"tool_call_id,omitempty"`
// Name is the tool, sent only while a call is still ARRIVING — before
// tool.requested exists to carry it. A subject that does not exist yet
// has to name itself, or the report has nowhere to land.
Name string `json:"name,omitempty"`
Kind string `json:"kind"`
Done int `json:"done"`
Total int `json:"total,omitempty"`
}
Progress is how far along something running has got.
Done without Total is honest: a scan that has not finished enumerating knows how many it has seen and not how many there are. A client shows the count alone rather than inventing a denominator.
Kind comes from the closed set below rather than being a word to print. The daemon's language is not the reader's, and a client that renders the payload's text shows the wrong one to half its users.
Never part of the context the model is sent: it is a person's window, the same rule StartedAt already carries, and a count that changes between two runs of one session is exactly what ADR-03 forbids in a prefix.
type RenameSessionRequest ¶ added in v0.2.0
type RenameSessionRequest struct {
Name string `json:"name"`
}
RenameSessionRequest names a conversation. An empty name restores the title derived from the first question, which is the way back rather than a second command for undoing.
type ResolveApprovalRequest ¶
type ResolveApprovalRequest struct {
Decision ApprovalDecision `json:"decision"`
}
ResolveApprovalRequest answers a pending approval. First writer wins.
type Session ¶
type Session struct {
ID string `json:"id"`
State SessionState `json:"state"`
Workspace string `json:"workspace"`
Model string `json:"model"`
SandboxMode string `json:"sandbox_mode"`
CreatedAt time.Time `json:"created_at"`
LastSeq uint64 `json:"last_seq"`
// FirstSeq is the earliest event the session still holds. It is not always
// 1: continuing a long conversation puts every carried event in the log,
// and retention drops the oldest. A client that assumes 1 asks for events
// that are gone and is refused, so the session it asked for never opens.
FirstSeq uint64 `json:"first_seq,omitempty"`
// ContextWindow is the model's window in tokens. The client needs it to
// turn a token count into the percentage a person can act on; without it,
// "12400 tokens" answers nothing.
ContextWindow int `json:"context_window,omitempty"`
}
Session is the server-owned session record. Clients hold no session state of their own beyond scroll position, panel visibility and their input queue.
type SessionCompacted ¶
type SessionCompacted struct {
FromSeq uint64 `json:"from_seq"`
ToSeq uint64 `json:"to_seq"`
// Messages is how many were replaced by the summary, and Kept how many
// survived it. "Earlier history was summarised" says that something
// happened; these say how much, which is the difference between a
// notice and an answer.
Messages int `json:"messages,omitempty"`
Kept int `json:"kept,omitempty"`
}
SessionCompacted records a context compaction.
type SessionRenamed ¶ added in v0.2.0
type SessionRenamed struct {
Name string `json:"name"`
}
SessionRenamed is the name a person gave, which beats the one derived from the first question. Empty gives the derived title back.
type SessionResumed ¶
type SessionResumed struct {
SourceID string `json:"source_id"`
Turns int `json:"turns"`
StartedAt time.Time `json:"started_at,omitempty"`
}
SessionResumed names the conversation the events after it came from.
Turns is what happened there, counted before the replay rather than by the reader, so a client that joins late reads the same number as one that was there.
type SessionState ¶
type SessionState string
SessionState is the lifecycle state of a session.
const ( SessionStateIdle SessionState = "idle" SessionStateRunning SessionState = "running" SessionStateBlocked SessionState = "blocked" SessionStateClosed SessionState = "closed" )
func (SessionState) Valid ¶
func (s SessionState) Valid() bool
Valid reports whether s is a state the server can be in.
type SteerRequest ¶
type SteerRequest struct {
Text string `json:"text"`
}
TurnStarted announces an accepted input. SteerRequest is what the person says to a turn already running.
type SubmitTurnRequest ¶
type SubmitTurnRequest struct {
Text string `json:"text"`
// Images are pictures shown with this turn, base64 encoded with their
// media type. Sent by value rather than as paths: the daemon may be on
// another machine, and a path only means something where it was typed.
Images []TurnImage `json:"images,omitempty"`
}
SubmitTurnRequest submits user input. Rejected with turn_already_active if a turn is already running: one turn per session.
type ToolCompleted ¶
type ToolCompleted struct {
ToolCallID string `json:"tool_call_id"`
OK bool `json:"ok"`
Output string `json:"output"`
Truncated bool `json:"truncated"`
// What the call did, stated once by the tool that knows.
//
// A client that parses Output to rebuild these numbers breaks silently
// the day the wording changes, and every client has to reimplement the
// same parsing. All optional: a tool reports what applies to it.
Lines int `json:"lines,omitempty"`
Files int `json:"files,omitempty"`
Added int `json:"added,omitempty"`
Removed int `json:"removed,omitempty"`
ExitCode int `json:"exit_code,omitempty"`
HasExit bool `json:"has_exit,omitempty"`
DurationMS int `json:"duration_ms,omitempty"`
// StartedAt and FinishedAt are the REAL execution order (RN-3.3).
//
// A duration cannot answer which of two concurrent calls went first,
// and results arrive in emission order rather than the order they ran.
// A client showing a batch has a person looking at it, and "which of
// these happened first" is a reasonable thing for them to ask.
//
// They live HERE and never in the context sent to the model: a
// timestamp in the prefix would make two runs of the same session
// differ, which ADR-03 forbids. The event is a person's window; the
// context is not.
StartedAt time.Time `json:"started_at,omitempty"`
FinishedAt time.Time `json:"finished_at,omitempty"`
// Diff is the unified diff of a change. Present only for tools that
// modify a file, and never part of what the model was sent.
Diff string `json:"diff,omitempty"`
}
ToolCompleted carries the result of an execution.
type ToolRequested ¶
type ToolRequested struct {
TurnID string `json:"turn_id"`
ToolCallID string `json:"tool_call_id"`
Name string `json:"name"`
Input json.RawMessage `json:"input"`
}
ToolRequested announces a tool call before policy evaluation.
type TurnCompleted ¶
type TurnCompleted struct {
TurnID string `json:"turn_id"`
Reason string `json:"reason"`
// Usage is what the turn cost. Absent when the provider did not report
// it, which is why it is a pointer: zero tokens and unknown tokens are
// different facts and a client shows them differently.
Usage *Usage `json:"usage,omitempty"`
// Completion is what was and was not checked. Absent when the turn had
// no definition of done, which is why it is a pointer: "nothing to
// check" and "checked, all met" are different facts.
//
// It travels on the wire because it is the guarantee that survives a
// model claiming success in prose. The text can lie; this cannot.
Completion *Completion `json:"completion,omitempty"`
}
TurnCompleted ends a turn with one of the Stop* reasons.
type TurnStarted ¶
type TurnStarted struct {
TurnID string `json:"turn_id"`
// Text is what the user asked for.
//
// It rides here because a turn starts BECAUSE of it, and because
// nothing else carried it: the log held the model's side of a
// conversation and none of the questions. A transcript could not be
// read, a session could not be titled, and a client attaching to a
// session already under way saw answers to questions it never saw.
Text string `json:"text,omitempty"`
}
Payloads for the remaining event types.
type TurnSteered ¶
TurnSteered is what the person said while the turn was running, and the round it landed at.
type UndoResult ¶
UndoResult is what putting the last turn back changed, and what it would not touch.
Refused is per file rather than all-or-nothing: seven files changed and one edited by hand should still give six back, and naming the one that stayed is more useful than refusing everything on account of it.
type Usage ¶
type Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
CacheReadTokens int `json:"cache_read_tokens,omitempty"`
CacheWriteTokens int `json:"cache_write_tokens,omitempty"`
// ContextTokens is what the assembled context costs NOW — not summed,
// and stated by the daemon rather than derived by a client.
//
// It is the same estimate the compaction trigger reads, deliberately:
// a meter and a threshold that disagree are worse than either alone,
// because the person sees the summary happen at a number the screen
// never showed. Provider counts could not give this — the two families
// disagree about whether input_tokens already includes the cached
// prefix, so the same context reads differently depending on who
// answered.
ContextTokens int `json:"context_tokens,omitempty"`
}
Usage is the token accounting for a turn.
InputTokens is CUMULATIVE across the turn's rounds: every round re-sends the context, so a forty-round turn sums forty readings of it. That is the right number for what a turn COST and the wrong one for how full the context is — a client that divided it by the window showed `ctx 175%`.