Documentation
¶
Overview ¶
Package backend is the native Daintree Assistant backend client — the CLI's ONLY model gateway. It replaces the direct provider model client for assistant turns and utility tasks.
The CLI is a thin local runtime: it stores the visible conversation, exposes and executes local function tools, and ships structured startup/runtime/turn context. The backend owns the system prompt, developer instructions, skill/runbook selection, model choice, prompt assembly, and the utility-model prompts — and it reaches every model THROUGH OPENROUTER, using the caller's own key on a per-request basis. Model names that appear in this repo are OpenRouter route ids, never direct provider integrations. The wire contract here is Daintree-native (NOT OpenAI-compatible) and strict: the request schema rejects system/developer messages and unknown fields, so these structs deliberately emit only the fields the backend accepts.
Reference: ../assistant-backend/docs/DAINTREE_API.md and the pydantic models in ../assistant-backend/src/daintree_assistant_server/contracts/.
Index ¶
- Constants
- Variables
- func AllowsUnverifiedSignIn(baseURL string) bool
- func AnyToMap(v any) map[string]any
- func CoreTaskIDs() []string
- func ParseEndpointList(raw string) []string
- func PrivacyDescription(mode string, caps *Capabilities) string
- func ScrubKey(text, key string) string
- func WithoutRetry(ctx context.Context) context.Context
- func WorkflowTaskIDs() []string
- type AgentRosterSnapshot
- type AgentSnapshot
- type Backend
- type Capabilities
- type CheckpointInput
- type CheckpointOutput
- type Client
- func (c *Client) BaseURL() string
- func (c *Client) Capabilities(ctx context.Context) (Capabilities, error)
- func (c *Client) Health(ctx context.Context) error
- func (c *Client) Ready(ctx context.Context) error
- func (c *Client) Respond(ctx context.Context, req RespondRequest) (RespondResponse, error)
- func (c *Client) RespondStream(ctx context.Context, req RespondRequest, cb StreamCallbacks) (RespondResult, error)
- func (c *Client) RunTask(ctx context.Context, req TaskRequest) (TaskResult, error)
- func (c *Client) VerifyKey(ctx context.Context) (KeyVerification, error)
- func (c *Client) Version(ctx context.Context) (Version, error)
- type ClientConfig
- type ClientInfo
- type CostEvent
- type CostReportingCaps
- type CurrentWorktreeSnapshot
- type DisplayInfo
- type DistilledFact
- type EndpointChoice
- type Envelope
- type EnvelopeError
- type Error
- func (e *Error) Error() string
- func (e *Error) IsAuth() bool
- func (e *Error) IsConnect() bool
- func (e *Error) IsContract() bool
- func (e *Error) IsProtocolMismatch() bool
- func (e *Error) IsProviderAccount() bool
- func (e *Error) IsRateLimited() bool
- func (e *Error) IsReportable() bool
- func (e *Error) IsRoutingDeadEnd() bool
- func (e *Error) IsUpstreamAuth() bool
- func (e *Error) ProviderAccountReason() string
- type ExtractJSONOutput
- type ExtractionVerdictInput
- type ExtractionVerdictOutput
- type FunctionCall
- type FunctionDef
- type Generation
- type JudgeOutput
- type KeyVerification
- type MCPInfo
- type MCPServer
- type Memories
- type MemoryDistillInput
- type MemoryDistillOutput
- type Message
- type MultipleChoiceQuestionOut
- type NodePatchOut
- type OpenTerminal
- type Prelude
- type PreludeExecution
- type PreludeToolCall
- type PreludeToolResult
- type ProjectSnapshot
- type ProtocolRange
- type RecommendedActionOut
- type ResourcePatchOut
- type RespondCapsBlock
- type RespondInput
- type RespondMessage
- type RespondRequest
- type RespondResponse
- type RespondResult
- type RespondSession
- type RetryInfo
- type RetryPolicy
- type Routing
- type RoutingCapsBlock
- type RoutingSelectable
- type RuntimeContext
- type Selection
- type SelectorMeta
- type SkillRef
- type SkillStepConsistencyInput
- type SkillsBlock
- type StartupContext
- type StreamCallbacks
- type StreamDelta
- type StreamDone
- type StreamMeta
- type StreamStatus
- type Swappable
- func (s *Swappable) BaseURL() string
- func (s *Swappable) Capabilities(ctx context.Context) (Capabilities, error)
- func (s *Swappable) Current() Backend
- func (s *Swappable) Health(ctx context.Context) error
- func (s *Swappable) Ready(ctx context.Context) error
- func (s *Swappable) RespondStream(ctx context.Context, req RespondRequest, cb StreamCallbacks) (RespondResult, error)
- func (s *Swappable) RunTask(ctx context.Context, req TaskRequest) (TaskResult, error)
- func (s *Swappable) Swap(b Backend) Backend
- func (s *Swappable) VerifyKey(ctx context.Context) (KeyVerification, error)
- func (s *Swappable) Version(ctx context.Context) (Version, error)
- type TaskAvailability
- type TaskOutputError
- type TaskRequest
- type TaskResult
- type TaskRunner
- type TaskTraceInfo
- type TerminalExtractJSONInput
- type TerminalExtractTextInput
- type TerminalJudgeInput
- type TerminalState
- type TerminalSummarizeInput
- type TextOutput
- type Tool
- type ToolCall
- type ToolCallDelta
- type ToolChoiceNamed
- type TransportMarks
- type TurnContext
- type TurnCost
- type TurnTimings
- type Usage
- type Version
- type WatcherClassifyInput
- type WatcherClassifyOutput
- type WorkflowBlockerOut
- type WorkflowDigest
- type WorkflowEdgeOut
- type WorkflowNodeOut
- type WorkflowPatchOut
- type WorkflowPlanInput
- type WorkflowPlanOutput
- type WorkflowReconcileInput
- type WorkflowReconcileOutput
- type WorkflowResumeDigestInput
- type WorkflowResumeDigestOutput
- type WorkflowResumeItem
- type WorkflowSnapshot
- type WorkflowSnapshotBlocker
- type WorkflowSnapshotNode
- type WorkflowSnapshotResource
- type WorkflowToolInfo
- type WorktreeSnapshot
Constants ¶
const ( // MaxWorkflowDigests caps how many workflow digests ride one turn context. MaxWorkflowDigests = 5 // MaxWorkflowStateBytes caps the serialized workflow_state block; whole // trailing digests are dropped (never a partial cut) until it fits. MaxWorkflowStateBytes = 16384 )
Per-field rune limits for WorkflowDigest (mirror the backend max_length constraints) plus the digest-list caps the rollout contract fixes.
const ( // The caller's provider account. Deterministic — their settings, their fix. CodeProviderInvalidAPIKey = "provider_invalid_api_key" // 401 upstream CodeProviderInsufficientCredit = "provider_insufficient_credits" // 402 upstream CodeProviderKeyForbidden = "provider_key_forbidden" // 403 upstream // Routing. Deterministic: the policy is fixed, so an immediate replay re-derives // the same empty endpoint pool. The backend fails closed here rather than quietly // relaxing the privacy floor to find a route. CodeUpstreamNoCompliantProvider = "upstream_no_compliant_provider" // 503 // Transient upstream conditions. Worth replaying while no visible content has // committed. CodeUpstreamRateLimited = "upstream_rate_limited" // 429 CodeUpstreamTimeout = "upstream_timeout" // 504 // Our bug, not the caller's: we sent something the provider would not accept, or it // answered with something we could not parse. Permanent either way. CodeUpstreamRequestRejected = "upstream_request_rejected" // 502 CodeUpstreamProtocolError = "upstream_protocol_error" // 502 // The pre-split catch-all. The backend still emits it, but now ONLY for a stream // error it could not map to anything above — a genuine "we don't know", which is // why it stays retryable on the stream path. CodeUpstreamError = "upstream_error" // 502 )
The backend's stable upstream-failure codes. Every one of these names a DIFFERENT problem with a different fix, which is the entire reason they exist: they used to collapse into one 502 `upstream_error`, so a tester whose balance had run out was told their credentials were rejected and dutifully replaced a perfectly good key.
Two properties are load-bearing for this package, and neither is on the wire:
- Retryability. The backend knows it per code and does NOT serialise it, so isRetriable classifies from the code here. Getting it wrong is expensive in both directions — replaying a deterministic verdict burns the whole retry budget to re-derive the same answer, and failing to replay a transient one turns a blip into a failed turn.
- Whose problem it is. The account codes are the caller's own OpenRouter settings; the routing code is their policy; the two "rejected/protocol" codes are ours. Only distinct codes can produce distinct advice, which is what internal/agent/session.go renders.
The HTTP statuses are listed for orientation only. NEVER classify on status alone: a mid-stream SSE error carries HTTPStatus 0 (the 200 was already committed), so the same condition reaches this package with and without its status depending only on how far the request got.
const ( // PrivacyNoTraining routes only to endpoints that do not collect or train on // request data. It is a no-training guarantee and NOT a no-retention one; an // endpoint in this pool may still hold a request transiently. PrivacyNoTraining = "no_training" // PrivacyZDR adds OpenRouter's zero-data-retention filter on top. It narrows the // eligible pool substantially. PrivacyZDR = "zdr" )
Privacy modes. The wording a user SEES comes from the backend's capabilities (`routing.privacy_description`), never from here — see PrivacyDescription.
const ( SortThroughput = "throughput" // fastest tokens/second, explicitly at the cost of price SortPrice = "price" // cheapest wins SortLatency = "latency" // lowest time-to-first-token )
Endpoint sort axes.
const ( TaskCheckpoint = "checkpoint" TaskMemoryDistill = "memory_distill" TaskWatcherClassify = "watcher_classify" TaskTerminalJudge = "terminal_judge" TaskTerminalSummarize = "terminal_summarize" TaskTerminalExtractText = "terminal_extract_text" TaskTerminalExtractJSON = "terminal_extract_json" TaskExtractionVerdict = "extraction_verdict" TaskSkillStepConsistency = "skill_step_consistency" )
Task IDs the backend exposes via /v1/daintree/tasks. Use these constants rather than string literals at call sites. Availability against a live backend is verified by CheckTasks (surfaced by /doctor and the debug log) — see below.
const ( TaskWorkflowPlan = "workflow_plan" TaskWorkflowReconcile = "workflow_reconcile" TaskWorkflowResumeDigest = "workflow_resume_digest" )
Workflow-intelligence task IDs (server-owned prompts/models; the CLI sends data only). Availability is gated by DAINTREE_WORKFLOW_INTELLIGENCE and, at runtime, by Capabilities.Tasks — a backend without these tasks rejects them and the caller degrades gracefully.
const DefaultBaseURL = "https://assistant.daintree.org"
DefaultBaseURL is the deployed backend — the endpoint a fresh install signs in to.
The backend requires authentication in EVERY environment: each request carries the caller's own API key as the bearer token, and that key is also the upstream credential funding the turn's model calls (the server holds none of its own). There is no unauthenticated mode to fall back to, so a CLI without a stored key cannot talk to any endpoint — see internal/credentials and the login flow in internal/cli/login.go.
const FinishReasonLength = "length"
FinishReasonLength is the provider finish reason for a generation cut off by the output-token cap. The agent loop uses it to diagnose a parse-failed final tool call as truncation (re-issue the amputated work) rather than a JSON syntax slip (re-encode the same call).
const KeyPurposeNotice = "OpenRouter bills this key for every model call, including background supervision."
KeyPurposeNotice is what a user is told, at the moment they are asked for a key, about WHAT the key is and WHO it bills.
It is here rather than duplicated in each prompt because both sign-in surfaces have to say the same thing, and this particular sentence is one a tester acts on: they are about to paste a credential that spends their own money, and nothing else in the flow tells them so. Someone who thinks they are entering a Daintree account password will paste the wrong thing, and — worse — will not know to watch their balance.
The billing clause leads because it is the half a reader acts on. The backend holds no provider credential of its own; the key travels with every request and funds every model call the session makes, including the ones background supervision fires while nobody is watching — which is the spend a tester would otherwise never think to expect.
Kept SHORT on purpose. The cockpit sheet wraps it into a bounded number of rows and ELLIPSIZES past them, and a disclosure cut off mid-sentence at 40 columns is worse than a terser one that always lands whole. Each surface adds its own framing around this sentence; the sentence itself stays the shared, load-bearing part.
const LocalBaseURL = "http://127.0.0.1:8473"
LocalBaseURL is the local development backend (`python -m daintree_assistant_server` from ../assistant-backend). Offered as an explicit choice at login, and still the value DAINTREE_BACKEND_URL is usually pointed at for e2e tests and benchmarks. It needs a key too — "local" changes where requests go, not whether they authenticate.
const MaxEndpointList = 24
MaxEndpointList caps an allow/deny list, matching the backend's own bound. Generous (a model is served by a couple of dozen endpoints) but bounded, because these strings are forwarded upstream verbatim.
const ProtocolVersion = 2
ProtocolVersion is the Daintree wire protocol the CLI speaks. The backend advertises a supported range via /version and /v1/daintree/capabilities; a mismatch yields HTTP 426.
const RespondOp = "respond"
RespondOp is the CostEvent.Op value for a conversation turn. Every other value is a utility task, named by its task id.
Variables ¶
var EndpointChoices = []EndpointChoice{ {Label: "Official", URL: DefaultBaseURL, Note: "the deployed Daintree backend"}, {Label: "Custom", URL: "", Note: "enter your own URL"}, {Label: "Local", URL: LocalBaseURL, Note: "a backend you are running yourself"}, }
EndpointChoices is the offered endpoint menu, shared by the startup login flow (internal/cli) and the cockpit's `/login` sheet (internal/ui) so the two surfaces cannot drift into offering different endpoints.
var ErrBackendIncompatible = errors.New("this Daintree backend did not answer the key-verification request — it may be out of date, or a proxy may be intercepting it")
ErrBackendIncompatible is a remote endpoint failing a capability the release contract requires. Distinct from ErrKeyRejected because the fix is the opposite: nothing is wrong with the key and re-pasting it will not help. Sign-in surfaces it as an endpoint problem so a tester doesn't go hunting for a credential problem they don't have.
The wording names both plausible causes rather than only the first. From the client there is no way to tell an obsolete deployment from a proxy, CDN, or captive portal eating the route — and for the hosted service "update the deployment" is not something the tester can act on, whereas "try without the proxy" often is.
var ErrKeyRejected = errors.New("the provider rejected this API key")
ErrKeyRejected is the definite verdict: the upstream provider does not accept this key. A sentinel rather than a bare message so both sign-in surfaces can render it as the actionable thing it is ("this key doesn't work") instead of burying it inside a generic "could not verify <url>" wrapper, which reads like a connectivity problem.
var ErrVerifyUnsupported = errors.New("backend does not support key verification")
ErrVerifyUnsupported reports a backend that does not serve the key-verification route at all — as opposed to serving it and answering. Sign-in fails on it for any REMOTE endpoint (an obsolete deployment or an intercepting proxy is a compatibility failure) and downgrades it to a warning only for loopback, so the local development loop keeps working. See CheckSignIn and AllowsUnverifiedSignIn.
Functions ¶
func AllowsUnverifiedSignIn ¶
AllowsUnverifiedSignIn reports whether an endpoint may be signed in to WITHOUT proving the key works upstream — i.e. whether a missing /v1/daintree/auth/verify route downgrades to a warning instead of failing sign-in outright (see CheckSignIn).
Only a LOOPBACK endpoint qualifies. The reasoning is about which way the test fails when it is wrong:
- The obvious formulation, "is this the official endpoint?", fails OPEN. Its alias surface is unbounded — `:443`, an empty port, a trailing DNS root dot, an IDNA spelling, userinfo — and every spelling the check does not anticipate silently takes the LENIENT path and persists an unverified, spendable key against a remote host. Getting that check wrong is a security bug.
- "Is this loopback?" fails CLOSED. There is no `evil.com` spelling that parses to 127.0.0.1, an unparseable URL is treated as remote, and the worst outcome of a miss is that a developer's own backend has to serve one more route.
So every REMOTE endpoint — official, staging, or custom — is held to the full contract, and the lenient path exists only for the `python -m daintree_assistant_server` development loop, where there is no network to intercept and no third party to trust.
func AnyToMap ¶
AnyToMap converts an arbitrary JSON-serializable value (e.g. a digest) into the map[string]any a task input carries. nil on failure — task inputs are best-effort context, never worth failing the call over.
func CoreTaskIDs ¶
func CoreTaskIDs() []string
CoreTaskIDs returns the always-required task ids (a copy — callers must not mutate the manifest).
func ParseEndpointList ¶
ParseEndpointList splits a comma-separated env value into slugs, dropping empties and trimming whitespace so `"deepinfra, together"` works as typed.
func PrivacyDescription ¶
func PrivacyDescription(mode string, caps *Capabilities) string
PrivacyDescription returns the accurate one-line description of a privacy mode, preferring the BACKEND's own wording from capabilities.
The fallback exists only for the case where capabilities could not be read; it is worded to the same standard the backend holds itself to. The distinction it protects is the whole reason the modes are named rather than boolean: no-training and no-retention are different promises, and saying "does not store" for `no_training` would be a claim about someone's data that is not true.
func ScrubKey ¶
ScrubKey removes every occurrence of a secret from text destined for a human.
A backend we do not control can echo the Authorization header into an error body, and that text reaches the cockpit sheet and the 0600 debug log. The cockpit renders on the NORMAL screen buffer, so a leaked key would persist in the host's scrollback long after the session. Cheap insurance at the one boundary where untrusted text meets a known secret.
func WithoutRetry ¶
WithoutRetry returns a context whose backend calls make exactly ONE attempt.
It exists for DIAGNOSTICS. `/doctor` and the doctor subcommand ask "is the hop up right now?", and the answer must be immediate: with the patient default budget, a probe against a refused socket would silently spend its whole timeout retrying and report the same failure seconds later. Retrying there would also mask the exact condition the probe exists to surface. Everything else — turns, tasks, the boot handshake — wants the retries.
func WorkflowTaskIDs ¶
func WorkflowTaskIDs() []string
WorkflowTaskIDs returns the workflow-intelligence task ids (a copy).
Types ¶
type AgentRosterSnapshot ¶
type AgentRosterSnapshot struct {
Agents []AgentSnapshot `json:"agents"`
Complete bool `json:"complete"`
AvailabilityComplete bool `json:"availability_complete"`
TotalCount int `json:"total_count"`
}
AgentRosterSnapshot is one authoritative direct-agent registry read. Agents is not omitempty so a successful empty read remains [] rather than disappearing.
type AgentSnapshot ¶
type AgentSnapshot struct {
ID string `json:"id"`
DisplayName string `json:"display_name,omitempty"`
Source string `json:"source"`
Availability string `json:"availability,omitempty"`
Installed *bool `json:"installed,omitempty"`
Launchable *bool `json:"launchable,omitempty"`
Pinned *bool `json:"pinned,omitempty"`
ToolbarVisible *bool `json:"toolbar_visible,omitempty"`
}
AgentSnapshot preserves the exact registered identifier and required provenance. Pointer booleans retain the registry's tri-state semantics; absent availability remains unknown.
type Backend ¶
type Backend interface {
RespondStream(ctx context.Context, req RespondRequest, cb StreamCallbacks) (RespondResult, error)
RunTask(ctx context.Context, req TaskRequest) (TaskResult, error)
Capabilities(ctx context.Context) (Capabilities, error)
VerifyKey(ctx context.Context) (KeyVerification, error)
Version(ctx context.Context) (Version, error)
Health(ctx context.Context) error
Ready(ctx context.Context) error
BaseURL() string
}
Backend is the full client surface the app depends on (satisfied by *Client, and trivially by a fake in tests). Holding the app's dependency as this interface lets tests inject a fake backend without a live server.
type Capabilities ¶
type Capabilities struct {
ServerVersion string `json:"server_version"`
Protocol ProtocolRange `json:"protocol"`
Respond RespondCapsBlock `json:"respond"`
// Routing reports the ACTIVE endpoint-routing posture and the values a client may
// select. The description is served rather than composed locally so a client
// cannot invent its own privacy wording — the difference between "does not train
// on" and "does not store" is a claim about the user's data, and only one of them
// is true under the default mode.
Routing RoutingCapsBlock `json:"routing"`
Skills struct {
CatalogRevision string `json:"catalog_revision"`
ManualResolve bool `json:"manual_resolve"`
} `json:"skills"`
Tasks []string `json:"tasks"`
Limits struct {
RequestBytes int `json:"request_bytes"`
Tools int `json:"tools"`
} `json:"limits"`
}
Capabilities is the GET /v1/daintree/capabilities body — protocol range, limits, stream events, and available task ids.
type CheckpointInput ¶
type CheckpointInput struct {
Transcript string `json:"transcript"`
}
CheckpointInput compacts a transcript into a structured checkpoint.
type CheckpointOutput ¶
type CheckpointOutput struct {
Goal string `json:"goal"`
// Standing user instructions/constraints stated mid-conversation ("never close
// terminals", "always branch first") — preserved near-verbatim by the backend's
// checkpoint task so an explicit instruction survives compaction.
UserDirectives []string `json:"user_directives"`
ActiveTerminals []string `json:"active_terminals"`
ActiveWatchers []string `json:"active_watchers"`
WorkflowRunIDs []string `json:"workflow_run_ids"`
Decisions []string `json:"decisions"`
// The loop-prevention register: approaches tried and failed (with why), so the
// post-compaction assistant does not repeat them.
FailedApproaches []string `json:"failed_approaches"`
PendingToolState []string `json:"pending_tool_state"`
NextActions []string `json:"next_actions"`
OpenQuestions []string `json:"open_questions"`
ApprovalsGrants []string `json:"approvals_grants"`
PreservedIDs []string `json:"preserved_ids"`
}
CheckpointOutput is the structured compaction checkpoint.
func RunCheckpoint ¶
func RunCheckpoint(ctx context.Context, r TaskRunner, in CheckpointInput) (CheckpointOutput, error)
RunCheckpoint compacts a transcript into a structured checkpoint. Deliberately NO structurally-empty validate hook: the agent's compaction path (see agent/checkpoint.go) treats an all-empty checkpoint as a legitimate best-effort degradation — a prose/degenerate model reply must never block compaction, and its validateCheckpoint pass mines the load-bearing IDs into the empty object afterward. Only a wire round with NO output at all is rejected (by runTyped).
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is the native Daintree backend HTTP client. It speaks the Daintree-native protocol (NOT OpenAI), streams the respond endpoint as named SSE events, and runs server-owned utility tasks. It is safe for concurrent use.
func NewClient ¶
func NewClient(cfg ClientConfig) *Client
NewClient builds a Client. An empty BaseURL falls back to DefaultBaseURL.
func (*Client) BaseURL ¶
BaseURL returns the configured backend base URL (for diagnostics / doctor).
func (*Client) Capabilities ¶
func (c *Client) Capabilities(ctx context.Context) (Capabilities, error)
Capabilities fetches the backend's capability descriptor. Cache the result — refresh only on startup, reconnect, or /doctor.
func (*Client) Health ¶
Health probes liveness. Returns nil when the backend reports ok.
The path is /health, NOT /healthz. Both are served by the same handler, but only /health is routed on the deployed edge — /healthz there returns a Google 404 page from the load balancer, which the CLI would report as "backend UNREACHABLE" against a perfectly healthy backend (observed 2026-08-08; the backend repo hit the same trap in its release smoke test, assistant-backend 15264f1).
func (*Client) Ready ¶
Ready probes /readyz (readiness: config, secrets, prompts, catalog, provider). Returns nil only when the backend reports ready (a 503 surfaces as *Error).
func (*Client) Respond ¶
func (c *Client) Respond(ctx context.Context, req RespondRequest) (RespondResponse, error)
Respond runs a non-streaming generation round (used for tests / simple callers).
func (*Client) RespondStream ¶
func (c *Client) RespondStream(ctx context.Context, req RespondRequest, cb StreamCallbacks) (RespondResult, error)
RespondStream runs one generation round against /v1/daintree/respond as a named-event SSE stream and returns the accumulated result. It forces generation.stream = true. A failure before the backend opens the SSE response arrives as an ordinary JSON error; upstream connection/generation failures after the eager meta event arrive as terminal SSE error events — both surface as *Error. The caller owns cancellation via ctx.
Transient failures (connect errors, 5xx/gateway statuses, rate limits, and the mid-stream upstream/truncation errors the backend surfaces after the 200) are retried with exponential backoff per the client's RetryPolicy — but ONLY while no visible content has streamed yet, since a replay after the user has seen tokens would duplicate them. Replays are safe and near-free: the conversation prefix is unchanged and the backend prefix-caches it.
func (*Client) RunTask ¶
func (c *Client) RunTask(ctx context.Context, req TaskRequest) (TaskResult, error)
RunTask runs a server-owned utility task against /v1/daintree/tasks. The CLI sends task DATA only; the backend owns the prompt, model, schema, and output mode. Decode TaskResult.Output into the task-specific output struct.
func (*Client) VerifyKey ¶
func (c *Client) VerifyKey(ctx context.Context) (KeyVerification, error)
VerifyKey asks the backend whether the configured key actually works upstream.
This is the ONLY meaningful validity check available. The backend authenticates STRUCTURALLY — it holds no upstream credential — so /v1/daintree/capabilities answers 200 for any well-formed string. Without this call, a wrong key is discovered on the first real turn rather than at sign-in.
The CLI must never probe the provider itself: it holds no provider client by design (that is what keeps prompts, model choice, and credentials on the server), and the key becomes a subscription key later, at which point only the backend can resolve it.
type ClientConfig ¶
type ClientConfig struct {
BaseURL string
APIKey string
HTTPClient *http.Client
ClientInfo ClientInfo
// Retry tunes transient-failure retries for every backend call. The zero value
// selects DefaultRetryPolicy (10 attempts settling into a 10–15s poll — the
// backend owns provider retries; this covers only the CLI↔backend hop). Set
// Retry.MaxAttempts to 1 to disable retries.
Retry RetryPolicy
// OnRetry, if set, is invoked just before each backoff sleep when a transient
// failure will be retried — on the streamed respond turn AND on the JSON
// endpoints (tasks / capabilities / health / ready). Observability only; it must
// not block. RetryInfo.Op names which call is being replayed.
OnRetry func(RetryInfo)
// OnTask, if set, is invoked after every RunTask round trip (success or failure).
// Observability only — it must not block. Without it the utility tasks are the
// one backend surface a session log cannot see: a /compact's checkpoint +
// memory_distill calls (and every watcher classify/judge/extract) would leave no
// trace at all.
OnTask func(TaskTraceInfo)
// OnCost, if set, is invoked once for every billed upstream call this client makes
// — every respond turn AND every utility task. Observability only; it must not
// block.
//
// It lives on the CLIENT rather than on the agent session because a session sees
// only turns. A day of orchestration also spends money on dozens of
// terminal.summarize / terminal.extract / watcher-classify tasks, fired from tools,
// watchers and compaction alike — real spend on the user's own key, outside any
// turn. One hook at the layer every call passes through is the only way to count
// all of it without every future caller remembering to.
OnCost func(CostEvent)
// RoutingPreference, if set, is read for every request this client makes — turns AND
// utility tasks. It lives on the client for the same reason OnCost does: a task
// sends the caller's content upstream just as a turn does, and a privacy choice that
// covered only the visible path would be the most misleading kind of half-measure.
// A zero Routing means "no preference" and omits the block.
RoutingPreference func() Routing
}
ClientConfig configures a Client. APIKey is REQUIRED for every real endpoint: the backend authenticates in all environments (local included) and the bearer token is also the upstream credential funding the turn. An empty key sends no Authorization header and is useful only for the unauthenticated probes — it is left permitted so `doctor` can still reach /healthz while signed out. HTTPClient defaults to one with NO global timeout (a streamed turn can run for minutes; cancellation is via context).
type ClientInfo ¶
type ClientInfo struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Platform string `json:"platform,omitempty"`
}
ClientInfo identifies the CLI build for the backend's telemetry.
type CostEvent ¶
type CostEvent struct {
// Op is "respond" for a turn, or the task id ("terminal_summarize", "checkpoint", …).
Op string
// Amount is USD for the whole request, or nil when nothing was reported.
// nil means UNKNOWN. It never means zero.
Amount *float64
// Complete is false when this request ran work whose cost could not be measured, so
// Amount is a floor rather than a sum. Set from the backend's own `cost.complete`,
// and forced false when an earlier retried attempt of the same call already billed.
Complete bool
// CachedTokens/PromptTokens back the prompt-cache hit ratio. They cover the MAIN
// completion only — the selector's usage is not exposed per call — so the ratio is
// about the main call, not the whole request, and consumers should say so. It rides
// here because it EXPLAINS the spend beside it: the backend's byte-stable prompt
// assembly exists to keep ~18k tokens of tool schemas cached, and a collapse in this
// ratio is the first symptom of a regression that costs the user money directly.
CachedTokens int
PromptTokens int
}
CostEvent is one billed backend REQUEST, reported to ClientConfig.OnCost.
One event is not one provider call: a single turn can bill the skill selector, a repair pass, a losing speculative generation and the main completion. Amount is the request's total across all of them, which is the number the caller is charged.
It is emitted even when Amount is nil — "this request happened and reported no cost" is the fact that turns a session total into a LOWER BOUND, and an accumulator that only heard about the requests carrying numbers would present a partial sum as a receipt.
type CostReportingCaps ¶
type CostReportingCaps struct {
Field string `json:"field"`
Currency string `json:"currency"`
// Components names the sub-fields of the cost block ("total", "main", "selector",
// "complete").
Components []string `json:"components"`
StreamEvent string `json:"stream_event"`
// AbsentWhenUnknown states the rule the client must implement rather than infer:
// the block is omitted rather than zero-filled when nothing was reported.
AbsentWhenUnknown bool `json:"absent_when_unknown"`
// TotalMayBeIncomplete states that `complete: false` can occur, i.e. that a total
// is sometimes a floor.
TotalMayBeIncomplete bool `json:"total_may_be_incomplete"`
}
CostReportingCaps describes the backend's cost-reporting contract.
type CurrentWorktreeSnapshot ¶
type CurrentWorktreeSnapshot struct {
Current *WorktreeSnapshot `json:"current"`
}
CurrentWorktreeSnapshot preserves the three states of the live read. A nil Runtime Worktree means the read was unavailable; {"current":null} is a successful, definitive "none selected" response; a non-nil Current carries the full useful snapshot.
type DisplayInfo ¶
type DisplayInfo struct {
Columns int `json:"columns,omitempty"`
ContentWidth int `json:"content_width,omitempty"`
}
DisplayInfo is the client's live render geometry. content_width is the load-bearing value — the measure the assistant's own markdown is wrapped at — and the backend shapes the response contract (prose length, whether a pipe table can fit) around it; columns is the raw terminal, sent so the model can answer questions about the window it is running in without inferring one number from the other. Both are cells, and both are optional on the backend: a surface that knows its wrap width but not its window (a future non-cockpit publisher) sends content_width alone rather than a guessed pair.
Bounded by displayWidthMax to mirror the backend's validation: the request is validated BEFORE it is used, so an absurd width from a confused terminal probe would 422 the whole turn rather than degrade to a default.
func NewDisplayInfo ¶
func NewDisplayInfo(columns, contentWidth int) *DisplayInfo
NewDisplayInfo builds the wire block from a measured geometry, clamping both values into the range the backend validates. Returns nil when the content width is unmeasured or unusable, so an unknown surface omits the block entirely instead of asserting a width nobody measured.
type DistilledFact ¶
type DistilledFact struct {
Fact string `json:"fact"`
Kind string `json:"kind"` // "semantic" | "episodic"
}
DistilledFact is one distilled memory with its kind.
type EndpointChoice ¶
type EndpointChoice struct {
Label string
// URL is empty for "Custom", which prompts for one instead.
URL string
Note string
}
EndpointChoice is one selectable endpoint in a sign-in menu.
type Envelope ¶
type Envelope struct {
Error EnvelopeError `json:"error"`
RetryAfter string `json:"retry_after,omitempty"`
}
Envelope is the stable Daintree error envelope. It is delivered pre-stream as a JSON body and mid-stream as a terminal SSE `error` event with the same shape.
type EnvelopeError ¶
type EnvelopeError struct {
// OpenAI taxonomy, "_error"-suffixed:
// invalid_request_error|authentication_error|rate_limit_error|api_error
Type string `json:"type"`
Code string `json:"code"` // stable machine code, e.g. system_messages_not_allowed
Message string `json:"message"` // human-readable detail
Param string `json:"param"` // offending field path, when applicable
}
EnvelopeError is the inner error object.
type Error ¶
type Error struct {
HTTPStatus int
Type string
Code string
Message string
Param string
RetryAfter time.Duration
Stream bool
// RequestID is the backend's X-Request-Id for the failing call, when it sent one.
// It is what makes "report this as a bug" actionable: the two codes that mean our
// bug (CodeUpstreamRequestRejected, CodeUpstreamProtocolError) are undiagnosable
// without it, since the useful detail is in the server's log, not the client's.
RequestID string
}
Error is a backend failure surfaced to the agent loop. HTTPStatus is 0 for a mid-stream SSE error (the 200 was already committed). RetryAfter is set from the Retry-After header on an HTTP error or the top-level retry_after field on an SSE error. Stream is true when the failure arrived as a terminal `error` event after the meta event (vs. a pre-stream JSON error).
func (*Error) IsAuth ¶
IsAuth reports a 401/403 raised at OUR door — the bearer token was missing or structurally malformed. The fix is the header: sign in again.
The provider account codes are deliberately EXCLUDED even though they share those statuses. `provider_invalid_api_key` is also a 401, and telling someone whose key the provider revoked to "check you pasted it in full" sends them round a re-entry loop that cannot work. Same status, opposite advice — so the code decides, not the status.
func (*Error) IsConnect ¶
IsConnect reports that the backend was unreachable — a connection-level failure (dial refused/timeout), not an HTTP response. It is the most common local-dev failure (the backend isn't running) and deserves a connectivity message, not a "model error". Set at the only two construction sites in client.go.
func (*Error) IsContract ¶
IsContract reports a 400 — a CLI contract bug (forbidden role, reserved tool, invalid schema/name, unknown field). The message+param tell you what to fix.
func (*Error) IsProtocolMismatch ¶
IsProtocolMismatch reports a 426 — the CLI's protocol_version is unsupported.
func (*Error) IsProviderAccount ¶
IsProviderAccount reports one of the three post-split account codes specifically — i.e. IsUpstreamAuth minus the legacy catch-all. Consumers that branch per code (to say "out of credit" rather than "key rejected") gate on this, because the legacy code cannot tell them which of the three it was.
func (*Error) IsRateLimited ¶
IsRateLimited reports an upstream/model rate limit (429).
The type check was previously "rate_limit", which the backend never emits — it sends the "_error"-suffixed OpenAI taxonomy, so that comparison was dead and the 429/code checks were carrying the whole function.
func (*Error) IsReportable ¶
IsReportable reports the two codes whose only useful action is a bug report carrying RequestID: the exchange between Daintree and the provider was malformed in one direction or the other, and nothing the caller can change about their account, their key or their routing policy affects it.
The two are grouped for that reason alone, NOT because they share a culprit — and a message must not claim they do. `upstream_request_rejected` means the provider judged OUR request body malformed, which is almost always our bug. `upstream_protocol_error` means the provider answered with something unparseable, which is usually a provider or compatibility problem. Same next step, opposite direction of fault.
func (*Error) IsRoutingDeadEnd ¶
IsRoutingDeadEnd reports that no upstream endpoint satisfied the active routing policy. Not an outage and not a bug: the eligible pool was empty, which a stricter privacy mode or a narrow endpoint allowlist can cause on its own. Surfacing it as a generic upstream failure would hide the one thing that fixes it.
func (*Error) IsUpstreamAuth ¶
IsUpstreamAuth reports a well-formed key that the UPSTREAM provider then rejected. IsAuth means "fix your header"; this means "fix your account" — a revoked key, an empty balance, or a key not permitted to use this model. Without the split, a funding problem would read as a broken login.
The legacy 502 `upstream_error` form is still recognised so a CLI pointed at an older backend keeps its correct message rather than falling through to "Model error".
func (*Error) ProviderAccountReason ¶
ProviderAccountReason is a short clause naming WHICH account problem this is, for a caller composing its own sentence around it ("… but the provider " + reason). Empty when the error is not one of the three, including for the legacy `upstream_error` blob — which genuinely could not tell them apart, and must not be made to look as if it could.
type ExtractJSONOutput ¶
type ExtractJSONOutput struct {
Result json.RawMessage `json:"result"`
}
ExtractJSONOutput wraps the extracted structured value.
func RunTerminalExtractJSON ¶
func RunTerminalExtractJSON(ctx context.Context, r TaskRunner, in TerminalExtractJSONInput, schema map[string]any) (ExtractJSONOutput, error)
RunTerminalExtractJSON extracts a structured value; an optional result schema constrains the extraction (the backend treats a non-conforming value as a parse failure and retries).
type ExtractionVerdictInput ¶
type ExtractionVerdictInput struct {
Result string `json:"result"`
Condition string `json:"condition"`
}
ExtractionVerdictInput judges whether an extracted result satisfies a condition.
type ExtractionVerdictOutput ¶
ExtractionVerdictOutput is the settle/verdict judgement.
func RunExtractionVerdict ¶
func RunExtractionVerdict(ctx context.Context, r TaskRunner, in ExtractionVerdictInput) (ExtractionVerdictOutput, error)
RunExtractionVerdict judges whether an extracted result satisfies a condition. No validate hook: the backend ExtractionVerdictOutput defaults reason to "", so {pass:false, reason:""} is a schema-valid failing judgement (see RunTerminalJudge).
type FunctionCall ¶
FunctionCall is the name + raw-JSON-string arguments of a tool call.
type FunctionDef ¶
type FunctionDef struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Parameters json.RawMessage `json:"parameters,omitempty"`
}
FunctionDef is the name/description/JSON-schema parameters of a tool.
type Generation ¶
type Generation struct {
Temperature *float64 `json:"temperature,omitempty"`
MaxTokens *int `json:"max_tokens,omitempty"`
Stop []string `json:"stop,omitempty"`
ResponseFormat string `json:"response_format,omitempty"` // "text" | "json_object"
Stream bool `json:"stream"`
}
Generation carries only the generation params the backend supports — it validates with extra="forbid", so any unknown key is rejected. Stream is a plain bool (no omitempty) so the streaming intent is always explicit.
type JudgeOutput ¶
type JudgeOutput struct {
Reason string `json:"reason"`
Confidence float64 `json:"confidence"`
Matched bool `json:"matched"`
}
JudgeOutput is the yes/no judge verdict (terminal_judge + skill consistency).
func RunSkillStepConsistency ¶
func RunSkillStepConsistency(ctx context.Context, r TaskRunner, in SkillStepConsistencyInput) (JudgeOutput, error)
RunSkillStepConsistency judges whether one skill-step advance is consistent. No validate hook — same reasoning as RunTerminalJudge (shared JudgeOutput schema).
func RunTerminalJudge ¶
func RunTerminalJudge(ctx context.Context, r TaskRunner, in TerminalJudgeInput) (JudgeOutput, error)
RunTerminalJudge answers one yes/no question about a terminal. No validate hook: the AUTHORITATIVE backend schema (contracts/tasks.py JudgeOutput) defaults reason to "" and matched to false, so {matched:false, confidence:0, reason:""} is a schema-valid NEGATIVE verdict — rejecting it client-side would turn a legitimate "no" into a task error. Only the no-output-at-all wire round is rejected (by runTyped). Same posture for skill_step_consistency below.
type KeyVerification ¶
type KeyVerification struct {
// Valid is the provider's answer. False means a definite rejection — not
// "we couldn't check", which surfaces as an error from VerifyKey instead.
Valid bool `json:"valid"`
Detail string `json:"detail"`
// Usable answers a DIFFERENT question from Valid: not "does the provider recognise
// this credential" but "can the account behind it actually fund a turn". A key with
// a spent balance is valid and unusable, and a client reading only Valid shows a
// successful sign-in and then fails on the first real request with what looks like
// an unrelated error.
//
// A POINTER because an older backend omits the field entirely, and a plain bool
// would decode that absence as `false` — declaring every key on every older
// deployment unusable. nil means "not reported"; fall back to LimitRemaining.
Usable *bool `json:"usable"`
// Reason is the stable machine-readable outcome — `ok`, `provider_rejected`,
// `credits_exhausted`. Branch on this, never on Detail, which is prose.
Reason string `json:"reason"`
// Label is the provider's own name for the key, when it exposes one — useful for
// confirming the RIGHT key was pasted, not just a working one.
Label string `json:"label"`
// LimitRemaining is credit left on the key when the provider reports it. A pointer
// so "not reported" stays distinct from a genuine zero, which is worth warning about.
LimitRemaining *float64 `json:"limit_remaining"`
IsFreeTier bool `json:"is_free_tier"`
}
KeyVerification is the backend's verdict on the caller's key.
func CheckSignIn ¶
func CheckSignIn(ctx context.Context, c *Client) (verification KeyVerification, warning string, err error)
CheckSignIn is the shared sign-in verification both entry points run — the startup flow (internal/cli) and the cockpit's `/login` (internal/app) — so the two can't diverge on what "verified" means.
It answers two DIFFERENT questions, in order, because they fail differently:
- Is this a Daintree backend we can talk to, and is the header well-formed? (`/v1/daintree/capabilities` — a hard gate. A wrong URL or a mangled key stops here.)
- Does the provider actually accept this key? (`/v1/daintree/auth/verify` — the only check that can catch a key that is well-formed but wrong, revoked, or unfunded.)
The second is a hard gate too, and a backend that does not serve the route AT ALL is a COMPATIBILITY FAILURE for every REMOTE endpoint — official, staging, or custom. The deployed backend has served /v1/daintree/auth/verify since 2026-08, so its absence means an obsolete deployment or something intercepting the request; warning through would hand a tester exactly what verification exists to prevent — a well-formed but wrong / revoked / unfunded key, persisted, failing only on the first real turn.
The single exception is a LOOPBACK endpoint (`AllowsUnverifiedSignIn`), where the lenient warning path survives so the `python -m daintree_assistant_server` development loop keeps working. The predicate is deliberately "is this local?" rather than "is this official?" — see AllowsUnverifiedSignIn for why the latter fails open.
A non-empty `warning` means the sign-in was PERSISTED but something about it is worth saying out loud. Exactly three cases produce one:
- an unverifiable LOOPBACK backend (above);
- a provider we could not REACH — never a rejection, because "we could not check" must not be reported as "your key is bad", which sends the user hunting for a problem they do not have. Cancellation and timeout are the exception: they are hard failures, since neither is evidence about the key nor consent to persist an unverified one;
- a recognised key with no credit left — a real, fixable state, where refusing the sign-in would leave no way to configure the CLI while topping the account up.
func (KeyVerification) IsUsable ¶
func (v KeyVerification) IsUsable() bool
IsUsable reports whether the account behind a VALID key can actually fund a turn.
The backend answers this directly now (`usable`, with a stable `reason`), and its answer is the one to trust: it judges conservatively — only a positively reported zero-or-negative balance counts as exhausted, so an unlimited or pay-as-you-go key, which reports no limit at all, stays usable.
The LimitRemaining fallback is for a backend that predates the field. It cannot simply be deleted in favour of the new one: `usable` is a pointer precisely because absent must not decode as false, and treating "not reported" as "unusable" would warn every user of an older deployment that their working key has no credit.
type MCPInfo ¶
type MCPInfo struct {
Connected bool `json:"connected"`
Transport string `json:"transport,omitempty"`
ToolCount *int `json:"tool_count,omitempty"`
Status string `json:"status,omitempty"`
}
MCPInfo is a coarse connectivity summary for the primary MCP surface.
type MCPServer ¶
type MCPServer struct {
Name string `json:"name"`
Transport string `json:"transport,omitempty"`
Status string `json:"status,omitempty"`
ToolCount *int `json:"tool_count,omitempty"`
Description string `json:"description,omitempty"`
Instructions string `json:"instructions,omitempty"`
}
MCPServer is one MCP server the CLI is connected to, as the CLI reports it. The backend renders the name/description/instructions as inert, escape-neutralized data so they cannot inject instructions.
type Memories ¶
type Memories struct {
Pinned []string `json:"pinned,omitempty"`
Relevant []string `json:"relevant,omitempty"`
}
Memories splits recalled context into pinned (durable) and relevant (per-turn BM25 recall) buckets.
type MemoryDistillInput ¶
type MemoryDistillInput struct {
Transcript string `json:"transcript"`
}
MemoryDistillInput distills durable facts from a discarded transcript.
type MemoryDistillOutput ¶
type MemoryDistillOutput struct {
Facts []DistilledFact `json:"facts"`
}
MemoryDistillOutput is the distilled fact list.
func RunMemoryDistill ¶
func RunMemoryDistill(ctx context.Context, r TaskRunner, in MemoryDistillInput) (MemoryDistillOutput, error)
RunMemoryDistill distills durable facts from a discarded transcript. No validate hook: an empty fact list is a legitimate result (nothing durable to keep).
type Message ¶
type Message struct {
Role string `json:"role"`
Content json.RawMessage `json:"content,omitempty"`
Name string `json:"name,omitempty"`
// ReasoningContent is the assistant turn's chain-of-thought, replayed verbatim.
// DeepSeek REQUIRES it on every subsequent request for any assistant turn that
// performed a tool call (omitting it 400s the whole request); it is optional and
// ignored for assistant turns without tool calls. omitempty so a non-thinking turn
// (the default posture) sends nothing and the wire is byte-identical to before.
ReasoningContent string `json:"reasoning_content,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
}
Message is one visible-conversation message. Content is raw JSON so it can be a string, a multimodal parts array, or an explicit null (an assistant tool-call turn) — exactly mirroring the local wire encoder. Roles are user/assistant/tool ONLY; the converter rejects system/developer before a request is built.
type MultipleChoiceQuestionOut ¶
type MultipleChoiceQuestionOut struct {
Question string `json:"question"`
Options []string `json:"options,omitempty"`
AllowOther bool `json:"allow_other,omitempty"`
}
MultipleChoiceQuestionOut is a finite user decision the backend recommends asking (rendered through user.askMultipleChoice by the model, never directly).
type NodePatchOut ¶
type NodePatchOut struct {
NodeID string `json:"node_id"`
Status *string `json:"status,omitempty"`
Title *string `json:"title,omitempty"`
LastError *string `json:"last_error,omitempty"`
Note *string `json:"note,omitempty"`
}
NodePatchOut is one node mutation in a backend patch (identity: node_id). The mutable fields are POINTERS because the backend model declares them `... | None` (contracts/tasks.py NodePatchOut): JSON null / omitted means "leave unchanged" and must stay distinguishable from an explicit "" — a returned last_error:"" CLEARS a previous error, which a value string would silently swallow.
type OpenTerminal ¶
type OpenTerminal struct {
ID string `json:"id"`
Kind string `json:"kind,omitempty"`
WorktreeID string `json:"worktree_id,omitempty"`
Title string `json:"title,omitempty"`
AgentID string `json:"agent_id,omitempty"`
AgentState string `json:"agent_state,omitempty"`
WaitingReason string `json:"waiting_reason,omitempty"`
ExitCode *int `json:"exit_code,omitempty"`
}
OpenTerminal is one live Daintree terminal in the per-turn inventory the CLI attaches to the runtime block, so the model always sees the open-terminal roster as inert data instead of tool-calling terminal.list mid-turn to discover it. Metadata only — never terminal output. The list fields (id/kind/worktree/title/agent) come from a single terminal.list; AgentState/WaitingReason/ExitCode are refreshed from one no-output terminal.getStatus. ExitCode is a pointer because 0 is a meaningful clean exit that must be distinguishable from "no exit code".
func (OpenTerminal) Clamp ¶
func (t OpenTerminal) Clamp() OpenTerminal
Clamp returns a copy with every string field truncated to its backend max_length, so a long agent-controlled value can never trip the backend's pre-sanitization length validation and 422 the request. ids are short terminal-<uuid> values far under the limit, so clamping the id cannot collapse two distinct terminals in practice.
type Prelude ¶
type Prelude struct {
ToolExecutions []PreludeExecution `json:"tool_executions"`
}
Prelude is optional skill-load metadata the backend still emits. The client decodes but never replays or renders it; it is vestigial pending a coordinated server-side drop.
type PreludeExecution ¶
type PreludeExecution struct {
Call PreludeToolCall `json:"call"`
Result PreludeToolResult `json:"result"`
DisplayName string `json:"display_name"`
}
PreludeExecution is one skill-load call + its result, with a display name. Part of the vestigial Prelude metadata — decoded but not rendered by the client.
type PreludeToolCall ¶
type PreludeToolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
} `json:"function"`
}
PreludeToolCall mirrors a ToolCall for the synthetic skill-load exchange.
type PreludeToolResult ¶
type PreludeToolResult struct {
Role string `json:"role"`
ToolCallID string `json:"tool_call_id"`
Content string `json:"content"`
}
PreludeToolResult mirrors a tool-result message for the synthetic exchange.
type ProjectSnapshot ¶
type ProjectSnapshot struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Path string `json:"path,omitempty"`
Status string `json:"status,omitempty"`
DaintreeConfigPresent *bool `json:"daintree_config_present,omitempty"`
InRepoSettings *bool `json:"in_repo_settings,omitempty"`
}
ProjectSnapshot is the deliberately narrow subset of project.getCurrent that is useful on ordinary turns. The boolean pointers preserve unknown/false/true.
type ProtocolRange ¶
ProtocolRange is the inclusive supported protocol-version range.
type RecommendedActionOut ¶
type RecommendedActionOut struct {
Label string `json:"label"`
ToolName string `json:"tool_name,omitempty"`
Args map[string]any `json:"args,omitempty"`
Risk string `json:"risk,omitempty"`
RequiresConfirmation bool `json:"requires_confirmation,omitempty"`
NodeID string `json:"node_id,omitempty"`
}
RecommendedActionOut is the backend's suggested next tool call. It carries ZERO execution authority — the client validates the tool exists and dispatch still gates every mutation.
type ResourcePatchOut ¶
type ResourcePatchOut struct {
ResourceID string `json:"resource_id"`
Status *string `json:"status,omitempty"`
NodeID *string `json:"node_id,omitempty"`
Label *string `json:"label,omitempty"`
}
ResourcePatchOut is one resource mutation in a backend patch (identity: resource_id — the id the client supplied in the reconcile snapshot). Same pointer-nullability contract as NodePatchOut (backend ResourcePatchOut).
type RespondCapsBlock ¶
type RespondCapsBlock struct {
Endpoint string `json:"endpoint"`
Model string `json:"model"`
Streaming bool `json:"streaming"`
StreamEvents []string `json:"stream_events"`
SystemMessagesAccepted bool `json:"system_messages_accepted"`
MaxActiveSkills int `json:"max_active_skills"`
MetadataTransport string `json:"metadata_transport"`
// CostReporting is present when this backend reports what each request charged the
// caller. Absent on an older deployment — which the CLI handles without needing to
// ask, since an unreported cost is already indistinguishable from a backend that
// reports none, and both are rendered as "unknown". Advertised here so `/doctor`
// can name the contract rather than leave a tester guessing why /cost is empty.
CostReporting *CostReportingCaps `json:"cost_reporting"`
// DisplayContext reports that this backend accepts `runtime.display` — the client's
// terminal geometry — and shapes its response contract around it. It is a GATE, not
// a nicety: the backend validates `runtime` with extra="forbid", so sending the block
// to a deployment that predates it 422s the WHOLE turn before the model ever runs.
// False/absent on an older backend, which is why the CLI withholds the geometry until
// a handshake says otherwise (App.PromptContext). Delete the gate once no such
// deployment is reachable.
DisplayContext bool `json:"display_context"`
}
RespondCapsBlock is the respond-endpoint capability summary.
type RespondInput ¶
type RespondInput struct {
Messages []Message `json:"messages"`
Tools []Tool `json:"tools,omitempty"`
ToolChoice any `json:"tool_choice,omitempty"` // "auto"|"none"|"required" | ToolChoiceNamed
}
RespondInput is the visible conversation plus the client's current tool inventory. Stable discovery belongs in RespondRequest.Startup and must never be inserted here. Messages must be non-empty and carry only user/assistant/tool roles.
type RespondMessage ¶
type RespondMessage struct {
Role string `json:"role"`
Content string `json:"content"`
ReasoningContent string `json:"reasoning_content,omitempty"`
ToolCalls []ToolCall `json:"tool_calls"`
}
RespondMessage is the assistant message in a non-streaming response. ReasoningContent is present only when thinking is active (exclude_none on the server), so a non-thinking response decodes identically to before.
type RespondRequest ¶
type RespondRequest struct {
ProtocolVersion int `json:"protocol_version"`
Session RespondSession `json:"session"`
State *string `json:"state,omitempty"`
Startup StartupContext `json:"startup"`
Input RespondInput `json:"input"`
Runtime *RuntimeContext `json:"runtime,omitempty"`
Turn *TurnContext `json:"turn,omitempty"`
Selection *Selection `json:"selection,omitempty"`
Generation *Generation `json:"generation,omitempty"`
Client *ClientInfo `json:"client,omitempty"`
// Routing is the caller's endpoint-selection preference. Omitted by almost every
// request, which is what keeps the server default in force. See routing.go.
Routing *Routing `json:"routing,omitempty"`
}
RespondRequest is the single request body for the generation endpoint. The backend validates it with extra="forbid" at the top level, so every field here must be one the backend knows; optional sub-objects are pointers with omitempty so an absent one is never sent as null. Startup is the required value exception and therefore always serializes, including as {} when discovery is unavailable.
type RespondResponse ¶
type RespondResponse struct {
ProtocolVersion int `json:"protocol_version"`
RequestID string `json:"request_id"`
Model string `json:"model"`
Message RespondMessage `json:"message"`
FinishReason string `json:"finish_reason"`
Usage Usage `json:"usage"`
// Cost is the whole request's spend. Absent ⇒ unknown, never free. See TurnCost.
Cost *TurnCost `json:"cost"`
// Timings is where the request's wall clock went, by phase. Absent ⇒ the backend
// does not report timings. See TurnTimings.
Timings *TurnTimings `json:"timings"`
Skills SkillsBlock `json:"skills"`
State string `json:"state"`
CatalogRevision string `json:"catalog_revision"`
PromptVersion string `json:"prompt_version"`
Warnings []string `json:"warnings"`
}
RespondResponse is the non-streaming response body. (The CLI streams in normal operation; this exists for completeness and tests.)
type RespondResult ¶
type RespondResult struct {
Meta StreamMeta
Message RespondMessage
FinishReason string
Usage Usage
// Cost is the turn's total spend, carried up from the terminal `done` event. nil
// when the backend reported none — the caller must not read that as zero.
Cost *TurnCost
// Timings is the server-side phase breakdown, carried up from the terminal `done`
// event. On a RETRIED call it describes the WINNING attempt only (each attempt is a
// separate request with its own clock), so a client-measured round duration that
// exceeds Timings.TotalMs is the expected shape, not a contradiction.
Timings *TurnTimings
// Transport is the CLIENT-side latency of the attempt that produced this result:
// dial, TLS, upload, first byte back. It is the other half of Timings — the part
// measured before the server's clock starts and after it stops — and the two are
// meant to be read together. nil when the attempt never reached the wire. See
// transport.go.
Transport *TransportMarks
}
RespondResult is the accumulated outcome of a streamed respond call: the meta event, the assembled assistant message (content + tool calls), the finish reason, and usage. The agent loop reads State/Skills off Meta and appends Message to history.
func (RespondResult) HasToolCalls ¶
func (r RespondResult) HasToolCalls() bool
HasToolCalls reports whether the assistant asked to run any tools.
type RespondSession ¶
type RespondSession struct {
ID string `json:"id"`
TurnID string `json:"turn_id"`
InstructionRevision int `json:"instruction_revision,omitempty"`
Round int `json:"round,omitempty"`
}
RespondSession identifies the conversation and turn so the backend's skill state and selector cadence have a stable key. All four fields are accepted; instruction_revision/round default to 0 and are omitted when zero.
type RetryInfo ¶
RetryInfo is handed to ClientConfig.OnRetry (and StreamCallbacks.OnRetry) just before each backoff sleep, for observability and for the cockpit's "retrying…" cue. Attempt is 0-based: 0 is the first failure (about to make the first retry). MaxAttempts is the policy's total budget, so a renderer can say "2 of 10" without reaching into the client. Op names the failing call ("respond", or the JSON method+path) — with every endpoint retried now, a log line without it can't tell a stalled turn from a stalled utility task.
type RetryPolicy ¶
type RetryPolicy struct {
MaxAttempts int // total attempts INCLUDING the first; 1 = no retries
BaseDelay time.Duration // backoff for the first retry (doubles thereafter)
MaxDelay time.Duration // cap on a single backoff (0 = uncapped)
// MaxElapsed caps the wall clock of the whole retried call — attempts plus
// backoff sleeps — so slow failures can't multiply the attempt budget into
// minutes. 0 = unbounded (the attempt count is then the only limit).
MaxElapsed time.Duration
}
RetryPolicy tunes transient-failure retries for RespondStream. The zero value is not usable directly — NewClient substitutes DefaultRetryPolicy when MaxAttempts is 0. Set MaxAttempts to 1 to disable retries entirely (a single attempt).
func DefaultRetryPolicy ¶
func DefaultRetryPolicy() RetryPolicy
DefaultRetryPolicy is the production policy: 10 attempts total, exponential from 500ms and settling into a 10–15s poll — enough to ride out a backend restart on the CLI↔backend hop (the backend still owns its own provider retries).
type Routing ¶
type Routing struct {
Privacy string `json:"privacy,omitempty"`
Sort string `json:"sort,omitempty"`
Only []string `json:"only,omitempty"`
Ignore []string `json:"ignore,omitempty"`
}
Routing is the optional `routing` block on a respond request. The zero value means "no preference" and serializes to nothing — the server default applies, which is what almost every request wants.
func (Routing) IsDefault ¶
IsDefault reports whether this policy is what the caller would get by expressing no preference. Used to decide whether the posture is worth announcing: an explicit setting that happens to match the default is not news.
func (Routing) IsZero ¶
IsZero reports whether this expresses no preference at all, in which case the block is omitted from the request rather than sent as an empty object.
type RoutingCapsBlock ¶
type RoutingCapsBlock struct {
PrivacyMode string `json:"privacy_mode"`
PrivacyDescription string `json:"privacy_description"`
Sort string `json:"sort"`
// ClientSelectable is absent on a backend that does not accept a `routing` block,
// which is how the CLI knows not to send one.
ClientSelectable *RoutingSelectable `json:"client_selectable"`
}
RoutingCapsBlock is the backend's advertised routing posture.
type RoutingSelectable ¶
type RoutingSelectable struct {
Field string `json:"field"`
Privacy []string `json:"privacy"`
Sort []string `json:"sort"`
EndpointLists []string `json:"endpoint_lists"`
MaxEndpoints int `json:"max_endpoints"`
}
RoutingSelectable enumerates what a client may choose.
type RuntimeContext ¶
type RuntimeContext struct {
PermissionTier string `json:"permission_tier,omitempty"`
MCP *MCPInfo `json:"mcp,omitempty"`
MCPServers []MCPServer `json:"mcp_servers,omitempty"`
SchedulerActive bool `json:"scheduler_active"`
Worktree *CurrentWorktreeSnapshot `json:"worktree,omitempty"`
OpenTerminals []OpenTerminal `json:"open_terminals,omitempty"`
// Display is how wide the reply will actually render. Omitted when the CLI has no
// terminal to measure, which the backend answers with its own default width — so
// an absent block means "unknown", never "narrow".
Display *DisplayInfo `json:"display,omitempty"`
}
RuntimeContext is the CLI-reported environment. The backend renders it as inert data in the per-request (uncached) prompt tail. scheduler_active defaults to true on the backend, so it is sent WITHOUT omitempty — an inactive scheduler must be representable as an explicit false.
type Selection ¶
type Selection struct {
Policy string `json:"policy,omitempty"` // "new_instruction" | "always"
Force bool `json:"force,omitempty"`
}
Selection controls the backend's skill-selection cadence. policy "new_instruction" (the default) re-runs selection on a new turn / interjection / missing-state; "always" forces it every round.
type SelectorMeta ¶
type SelectorMeta struct {
Ran bool `json:"ran"`
Degraded bool `json:"degraded"`
TaskType string `json:"task_type,omitempty"`
Confidence *float64 `json:"confidence,omitempty"`
Usage *Usage `json:"usage,omitempty"`
Reason string `json:"reason,omitempty"`
}
SelectorMeta is the skill selector's telemetry for the turn.
type SkillRef ¶
SkillRef identifies one active/loaded skill. Skills are UNVERSIONED by design (change-busting rides the catalog content hash), so the backend's SkillRef carries only id + title — there is no version field to decode.
type SkillStepConsistencyInput ¶
type SkillStepConsistencyInput struct {
SkillID string `json:"skill_id"`
CompletedStep int `json:"completed_step"`
Status string `json:"status,omitempty"`
RequestedNext string `json:"requested_next,omitempty"`
CurrentStepBefore int `json:"current_step_before"`
CurrentStepAfter int `json:"current_step_after"`
RunStatusBefore string `json:"run_status_before,omitempty"`
RunStatusAfter string `json:"run_status_after,omitempty"`
Notes string `json:"notes,omitempty"`
ProgressBefore []map[string]any `json:"progress_before,omitempty"`
ProgressAfter []map[string]any `json:"progress_after,omitempty"`
}
SkillStepConsistencyInput judges whether one skill-step advance is consistent.
type SkillsBlock ¶
type SkillsBlock struct {
Active []SkillRef `json:"active"`
NewlyLoaded []SkillRef `json:"newly_loaded"`
Prelude Prelude `json:"prelude"`
Selector SelectorMeta `json:"selector"`
}
SkillsBlock is the dynamic-skill outcome for a turn. NONE of it is folded into the conversation: NewlyLoaded rides the eager OnSkillLoaded callback to the diagnostic sinks (run log, --json, debug trace), Selector is read by the debug trace, and Active/Prelude are decoded off the wire but unused — nothing reports the active set. The backend no longer injects anything into the upstream transcript — a newly-active skill reaches the model as its body in a "# Loaded skills" system message (plain context), so Prelude is now vestigial metadata the client neither replays nor renders.
type StartupContext ¶
type StartupContext struct {
Project *ProjectSnapshot `json:"project,omitempty"`
AgentRoster *AgentRosterSnapshot `json:"agent_roster,omitempty"`
ProjectInstructions string `json:"project_instructions,omitempty"`
}
StartupContext is the stable, cache-friendly Daintree snapshot collected while the splash animation is visible. It is a required value on every generation request: an unavailable discovery serializes as {}, never as null. The backend places it before the visible conversation and treats every value as inert, untrusted project data.
type StreamCallbacks ¶
type StreamCallbacks struct {
// OnRawMeta fires immediately when each HTTP attempt's SSE meta event is
// decoded. Unlike OnMeta, it is a transport-observation hook: retries can make
// it fire more than once for one RespondStream call, and callers must not use it
// to adopt state or perform visible side effects. It exists so diagnostics can
// distinguish actual meta arrival from the retry-safe committed OnMeta callback.
OnRawMeta func(StreamMeta)
// OnMeta carries the refreshed state token, the skills outcome, and version
// markers. Client.RespondStream defers it until the attempt commits so retries
// cannot duplicate stateful side effects.
OnMeta func(StreamMeta)
// OnSkillLoaded fires as soon as a meta event reports newly-loaded skills,
// before the upstream model needs to produce content. Client.RespondStream
// de-duplicates identical refs across retry attempts, so a retry cannot record the
// same load twice. DIAGNOSTIC ONLY — no consumer renders it in the transcript.
OnSkillLoaded func([]SkillRef)
// OnContent fires for each visible content fragment, in order.
OnContent func(string)
// OnReasoning fires for each chain-of-thought fragment (DeepSeek thinking mode),
// in order, before the first content fragment. Optional; the parser accumulates
// reasoning into the final message regardless. Empty stream when thinking is off.
OnReasoning func(string)
// OnStatus fires for each `status` event — once, with phase "thinking", the
// instant chain-of-thought begins. Optional; never fires when thinking is off.
OnStatus func(StreamStatus)
// OnToolCallDelta fires for each raw tool-call fragment (optional; the parser
// accumulates these internally regardless).
//
// REPLAY CONTRACT: RespondStream retries transient failures that occur before any
// content streams, and a failed attempt may already have emitted tool-call
// fragments — so this callback can fire for fragments from an attempt that is then
// discarded and replayed. Treat the RETURNED RespondResult.Message.ToolCalls (built
// from the final attempt's own fresh accumulator) as authoritative; do NOT execute
// or accumulate tool calls off these raw fragments across the call.
OnToolCallDelta func(ToolCallDelta)
// OnRetry fires just before each backoff sleep when Client.RespondStream is about
// to replay a transient failure. It exists so the caller can show a live cue: the
// retry budget can now span a minute of wall clock, and an unexplained spinner is
// indistinguishable from a hang. Observational only — it must not block, and it
// never fires from the stream parser (only from the retry loop above it).
OnRetry func(RetryInfo)
}
StreamCallbacks receives streamed events as they arrive. All are optional. The final assembled message + usage are returned by the stream parser regardless; these callbacks exist for live UI (token streaming, surfacing newly-loaded skills up front). They are invoked synchronously on the reader goroutine.
type StreamDelta ¶
type StreamDelta struct {
Content string `json:"content,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
ToolCalls []ToolCallDelta `json:"tool_calls,omitempty"`
}
StreamDelta is one streamed chunk: visible content, chain-of-thought, and/or OpenAI-style tool-call delta fragments (accumulated in sse.go). ReasoningContent fragments stream before the first Content fragment (DeepSeek thinking mode) and are concatenated the same way Content is; they only appear when thinking is active.
type StreamDone ¶
type StreamDone struct {
FinishReason string `json:"finish_reason"`
Usage Usage `json:"usage"`
// Cost rides the TERMINAL event because that is the earliest it can be known:
// OpenRouter reports a stream's usage only in its final chunk. Absent ⇒ unknown.
Cost *TurnCost `json:"cost"`
// Timings rides the terminal event for the same reason cost does: `meta` is emitted
// BEFORE the model is opened — which is exactly what makes meta useful — so it
// cannot know generation or total. A client logging a turn reads this, never meta.
Timings *TurnTimings `json:"timings"`
}
StreamDone terminates a successful stream. usage is always present (the backend dumps it without omit), with zeros when the upstream reported nothing.
type StreamMeta ¶
type StreamMeta struct {
ProtocolVersion int `json:"protocol_version"`
RequestID string `json:"request_id"`
Model string `json:"model"`
Skills SkillsBlock `json:"skills"`
State string `json:"state"`
CatalogRevision string `json:"catalog_revision"`
PromptVersion string `json:"prompt_version"`
Warnings []string `json:"warnings"`
}
StreamMeta is the FIRST streamed event — always before any token. It carries the refreshed opaque state token (resend on the next request), the skills outcome (active set + newly-loaded titles the client surfaces), and version markers.
type StreamStatus ¶
type StreamStatus struct {
Phase string `json:"phase"`
}
StreamStatus is the optional `status` event the backend emits once, the instant chain-of-thought begins (phase "thinking"). It never appears when thinking is off. Unknown future phase values are ignorable.
type Swappable ¶
type Swappable struct {
// contains filtered or unexported fields
}
Swappable is a Backend whose underlying client can be replaced at runtime, safely, while other goroutines are mid-call.
It exists for one reason: `/login` re-authenticates WITHOUT a restart. The backend client is captured in a lot of long-lived places — agent.Session's deps, the watcher engine, the async coordinator, the workflow layer — and several of those run on their own goroutines (the 3s scheduler tick, the 1s coordinator tick, autonomous wake turns). Reassigning a plain `App.Backend` field under all that is a data race, and threading a guarded accessor through four subsystems would touch every call site.
Instead the app hands out ONE Swappable at construction and never replaces the reference. Every consumer keeps the pointer it already has; a swap changes only what the wrapper delegates to, so the next call — from any goroutine — lands on the new client. Nothing downstream needs to know this happened.
An in-flight call keeps running against the client it started on. That is the correct behaviour, not a compromise: a streaming turn cannot be moved to a different endpoint halfway through, and cutting it off would corrupt the transcript. The swap takes effect from the next call.
func NewSwappable ¶
NewSwappable wraps an initial client. b must not be nil.
func (*Swappable) Capabilities ¶
func (s *Swappable) Capabilities(ctx context.Context) (Capabilities, error)
func (*Swappable) Current ¶
Current returns the delegate, for callers that need the concrete client (diagnostics).
func (*Swappable) RespondStream ¶
func (s *Swappable) RespondStream(ctx context.Context, req RespondRequest, cb StreamCallbacks) (RespondResult, error)
func (*Swappable) RunTask ¶
func (s *Swappable) RunTask(ctx context.Context, req TaskRequest) (TaskResult, error)
func (*Swappable) Swap ¶
Swap replaces the delegate and returns the previous one. Calls already in flight finish against the old client; every call after this lands on the new one.
type TaskAvailability ¶
type TaskAvailability struct {
// Reported is false when the backend returned a SUCCESSFUL capabilities
// response that advertised no tasks at all. That is a broken backend, not a
// warming one, and it is NOT excused: /v1/daintree/capabilities sits behind
// require_ready, which raises 503 BEFORE the handler runs, and a 200 always
// fills `tasks` from task_runner.task_ids(). So an empty list means the task
// registry itself is empty and every task call will 404.
//
// The genuine "cannot verify" case is a capabilities FETCH ERROR, which never
// reaches this function — callers branch on that first.
Reported bool
// Missing are required ids the backend did NOT advertise, sorted. Non-empty
// while Reported means real drift: those calls will 404 at the moment of use.
Missing []string
// Required is how many ids were checked (core, plus the workflow set when the
// workflow-intelligence flag is on).
Required int
}
TaskAvailability is the result of comparing the ids this CLI sends against the ids a live backend advertises.
func CheckTasks ¶
func CheckTasks(caps Capabilities, includeWorkflow bool) TaskAvailability
CheckTasks compares the task ids this CLI depends on against the ids a live backend advertises in Capabilities.Tasks. It assumes the capabilities fetch SUCCEEDED — a fetch error is the caller's "cannot verify" branch and must not reach here. includeWorkflow adds the flag-gated workflow-intelligence ids, which are only required when DAINTREE_WORKFLOW_INTELLIGENCE is on — a backend without them is perfectly healthy for a CLI that will never call them.
Extra server-side tasks are deliberately ignored: the backend may legitimately expose more than this CLI knows about, and that is forward compatibility, not drift.
func (TaskAvailability) OK ¶
func (a TaskAvailability) OK() bool
OK reports whether every required task is available. An empty inventory is a FAILURE, not an excuse: see Reported.
type TaskOutputError ¶
TaskOutputError reports a task round that "succeeded" on the wire but produced a result the typed caller cannot use: no output at all, or a decode that yielded a structurally empty required result. It is typed so callers can distinguish a bad task result from transport failures, and so a zero-valued struct is never silently treated as a real answer.
func (*TaskOutputError) Error ¶
func (e *TaskOutputError) Error() string
type TaskRequest ¶
type TaskRequest struct {
Task string `json:"task"`
RequestID string `json:"request_id,omitempty"`
Input map[string]any `json:"input,omitempty"`
ResultSchema map[string]any `json:"result_schema,omitempty"` // only terminal_extract_json
// Routing carries the SAME endpoint preference a turn sends, and carrying it here is
// not a nicety. A task ships the caller's content upstream exactly as a turn does —
// terminal tails, conversation transcripts, memories — so a privacy choice honoured
// only on /respond would be kept precisely where the user can see it and dropped
// everywhere else. Stamped by the client for every task (see Client.RunTask), so a
// new task call site cannot forget it.
Routing *Routing `json:"routing,omitempty"`
}
TaskRequest is the named utility-task envelope. Clients send task DATA only — the backend owns the prompt, model, schema, and output mode. extra="forbid" on the backend rejects any attempt to smuggle messages/system/developer here.
type TaskResult ¶
type TaskResult struct {
ID string `json:"id"`
Object string `json:"object"`
Task string `json:"task"`
Model string `json:"model"`
Output json.RawMessage `json:"output"`
FinishReason string `json:"finish_reason"`
Usage Usage `json:"usage"`
PromptVersion string `json:"prompt_version"`
}
TaskResult is the typed utility-task response. Output is raw JSON decoded by the caller into the task-specific output struct.
type TaskRunner ¶
type TaskRunner interface {
RunTask(ctx context.Context, req TaskRequest) (TaskResult, error)
}
TaskRunner is the narrow seam the typed task helpers depend on (satisfied by *Client and trivially by a fake). Keeping the helpers free functions over this interface keeps the app/daemon adapters testable without a live backend.
type TaskTraceInfo ¶
type TaskTraceInfo struct {
Task string
Duration time.Duration
InputBytes int
OutputBytes int
Err error
}
TaskTraceInfo describes one completed RunTask round trip for the OnTask hook. Err is nil on success; sizes are the serialized envelope input and the raw task output (bounded facts for a log line — never the payloads themselves).
type TerminalExtractJSONInput ¶
type TerminalExtractJSONInput struct {
TerminalIDs []string `json:"terminal_ids,omitempty"`
Instruction string `json:"instruction"`
Tail string `json:"tail,omitempty"`
}
TerminalExtractJSONInput extracts a structured value from terminal output. The optional schema travels in TaskRequest.result_schema, not here.
type TerminalExtractTextInput ¶
type TerminalExtractTextInput struct {
TerminalIDs []string `json:"terminal_ids,omitempty"`
Instruction string `json:"instruction"`
Tail string `json:"tail,omitempty"`
}
TerminalExtractTextInput extracts free text from terminal output.
type TerminalJudgeInput ¶
type TerminalJudgeInput struct {
Goal string `json:"goal,omitempty"`
Question string `json:"question"`
TerminalState TerminalState `json:"terminal_state"`
Tail string `json:"tail,omitempty"`
}
TerminalJudgeInput answers one yes/no question about a terminal.
type TerminalState ¶
type TerminalState struct {
AgentState string `json:"agent_state,omitempty"`
RuntimeStatus string `json:"runtime_status,omitempty"`
WaitingReason string `json:"waiting_reason,omitempty"`
LastOutputAt string `json:"last_output_at,omitempty"`
}
TerminalState is the deterministic terminal snapshot shared by several tasks.
type TerminalSummarizeInput ¶
type TerminalSummarizeInput struct {
Purpose string `json:"purpose,omitempty"`
Tail string `json:"tail,omitempty"`
}
TerminalSummarizeInput produces a terse factual summary of terminal output.
type TextOutput ¶
type TextOutput struct {
Text string `json:"text"`
}
TextOutput is a plain-text task result (summarize / extract_text).
func RunTerminalExtractText ¶
func RunTerminalExtractText(ctx context.Context, r TaskRunner, in TerminalExtractTextInput) (TextOutput, error)
RunTerminalExtractText extracts free text from terminal output. No validate hook — same reasoning as RunTerminalSummarize (empty extracted text can be legitimate).
func RunTerminalSummarize ¶
func RunTerminalSummarize(ctx context.Context, r TaskRunner, in TerminalSummarizeInput) (TextOutput, error)
RunTerminalSummarize produces a terse summary of terminal output. No validate hook: `{"text": ""}` is indistinguishable post-decode from an intentionally empty answer (e.g. extracting from a blank terminal), so only the missing-output case — covered by runTyped — is rejected.
type Tool ¶
type Tool struct {
Type string `json:"type"` // "function"
Function FunctionDef `json:"function"`
}
Tool is a function tool definition offered to the backend. The backend bounds the total tool bytes, schema depth, and property count, and rejects reserved names (skill__find/skill__load/daintree_internal__*).
type ToolCall ¶
type ToolCall struct {
ID string `json:"id"`
Type string `json:"type"` // always "function"
Function FunctionCall `json:"function"`
}
ToolCall is one function call the model emitted (or one replayed in history).
type ToolCallDelta ¶
type ToolCallDelta struct {
Index *int `json:"index"`
ID string `json:"id,omitempty"`
Type string `json:"type,omitempty"`
Function struct {
Name string `json:"name,omitempty"`
Arguments string `json:"arguments,omitempty"`
} `json:"function"`
}
ToolCallDelta is one streamed tool-call fragment, passed through verbatim from the upstream model. Fragments for the same call share an index; id/name arrive once and argument text streams in pieces.
type ToolChoiceNamed ¶
type ToolChoiceNamed struct {
Name string `json:"name"`
}
ToolChoiceNamed is the {"name": ...} flattened form of forcing a specific tool.
type TransportMarks ¶
type TransportMarks struct {
// ConnectMs is call start → a usable connection. On a REUSED connection this is the
// time to check one out of the pool: near zero, and the reason a session's first
// turn can look far worse than every turn after it. nil when no connection was
// established at all.
ConnectMs *int64
// Reused reports whether the connection came from the pool, and is nil when there
// was no connection to have an opinion about. It is the caveat that makes every
// other field readable: a cold dial and a pooled checkout measure different things,
// and comparing them without it invents a regression that never happened.
Reused *bool
// DNSMs is the name lookup's own duration, nil when none happened (a pooled
// connection, an IP literal, or a hit inside the resolver's cache).
DNSMs *int64
// TLSMs is the handshake's own duration, nil when none happened. On a cold
// connection to a remote endpoint this and DNSMs are usually most of ConnectMs.
TLSMs *int64
// RequestSentMs is call start → the transport reporting our request written. That is
// NOT "when they had it", and not even reliably "when the kernel had all of it": the
// hook fires before the transport's final buffer flush, and a warm round measured
// 0 ms here while shipping ~200 KB of prompt, because the body fit in the socket
// buffer. Treat it as a lower bound on our own send time.
RequestSentMs *int64
// FirstByteMs is call start → the first byte of the response, and the only mark that
// is round-trip complete. Subtracting the server's own preparation_ms from
// (FirstByteMs - RequestSentMs) leaves the real network cost — upload transmission
// included, which is where a large uncached prompt actually shows up.
FirstByteMs *int64
}
TransportMarks is one HTTP attempt's client-side latency in milliseconds. Every field is measured on OUR side of the wire.
ConnectMs, RequestSentMs and FirstByteMs are elapsed from the START of the call, so they nest and the reader subtracts. DNSMs and TLSMs are the opposite — each is that STAGE's own duration, because that is what the httptrace hooks bracket and inventing a common origin for them would be presenting arithmetic as measurement.
Every field is a pointer, and for the same reason the backend's phase timings are: a stage that did not happen must be ABSENT, never 0. That is not hypothetical here — a failed attempt legitimately produces a partial set (a DNS failure resolves nothing and connects to nothing), and a `FirstByteMs: 0` on a request that never got a response would read as the fastest turn ever recorded.
type TurnContext ¶
type TurnContext struct {
Goal string `json:"goal,omitempty"`
IsWake bool `json:"is_wake,omitempty"`
WorkflowRuns []string `json:"workflow_runs,omitempty"`
// AsyncOperations are the live runtime-owned async invocations (one
// pre-formatted line each), re-read every round so the model always sees its
// own in-flight async work — and never re-issues it after a compaction.
AsyncOperations []string `json:"async_operations,omitempty"`
Memories *Memories `json:"memories,omitempty"`
// ResumedWatchers are the titles of live watchers adopted from a prior owner
// at ownership boot (project-scoped supervision resumed automatically) —
// surfaced once, on the first turn. Replaces the pre-supervisor
// session_ended_watchers field; the backend contract renames in lockstep.
ResumedWatchers []string `json:"resumed_watchers,omitempty"`
// WorkflowState carries compact digests of the ACTIVE client-owned workflow
// graphs (the workflow-intelligence layer), re-read every round like the
// async ledger, so the model always sees what it already planned, did, and
// is waiting on — and never redoes completed work after a compaction or
// wake. Populated ONLY when workflow intelligence is enabled
// (DAINTREE_WORKFLOW_INTELLIGENCE=1): the backend validates TurnContext with
// extra="forbid", so a backend without the matching contract must never see
// the field (omitempty keeps the wire byte-identical when the feature is off).
WorkflowState []WorkflowDigest `json:"workflow_state,omitempty"`
}
TurnContext is the per-turn context the old footer message used to carry as prose; the backend now takes it as structured data and renders the footer.
type TurnCost ¶
type TurnCost struct {
Total float64 `json:"total"`
Main *float64 `json:"main"`
Selector *float64 `json:"selector"`
// Complete is a pointer only so an older backend's omission can default to TRUE
// (the backend's own default) instead of decoding as "incomplete" and marking every
// turn a lower bound. Read it through IsComplete.
Complete *bool `json:"complete"`
}
TurnCost is what a whole /respond request charged the caller, in USD, across every upstream call it made: the skill selector, its repair pass, a losing speculative generation, the main completion, and a re-rolled round the user never saw.
Two rules a client must IMPLEMENT rather than infer, and both exist to stop a session accumulator from quietly under-reporting someone's bill:
- The whole block is ABSENT when nothing was reported. Absent means unknown, never free.
- `complete: false` means a call that RAN did not report its cost, so Total is a floor rather than a sum. (A turn that SKIPPED the selector stays complete: no call happened, so nothing is missing.) One case is structurally unobservable — a speculative generation cancelled mid-flight was billed, but OpenRouter reports usage only in a stream's final chunk, which a cancelled stream never sends.
The practical consequence is one rule: render a session total as a lower bound if ANY turn in it was incomplete or reported no cost at all. These figures are for proportion and trend; the OpenRouter dashboard is the authority on the actual bill.
func (*TurnCost) IsComplete ¶
IsComplete reports whether Total is a full sum rather than a floor. Absent ⇒ true, matching the backend's default for a field it only started sending recently.
type TurnTimings ¶
type TurnTimings struct {
// SelectionMs is the skill-selector call, including a parse-repair round trip when
// one ran. Absent on a tool-continuation round, where selection is skipped by
// design — so its absence across a turn's later rounds is the healthy shape.
SelectionMs *int `json:"selection_ms"`
// DocsMs is the documentation lookup, when the selector asked for one.
DocsMs *int `json:"docs_ms"`
// PreparationMs is request-in → upstream request built: selection, docs, state
// verification and prompt assembly. The share of the wait the backend owns outright,
// and the number to read against UpstreamOpenMs when asking where a slow turn went.
PreparationMs *int `json:"preparation_ms"`
// UpstreamOpenMs is request-in → the model's first event; mostly prefill. A SMALL
// value next to a large SelectionMs means a winning speculation hid the open, not a
// fast model. Absent on a non-streamed call, where one await covers opening and
// generating and splitting it would be a guess presented as a measurement.
UpstreamOpenMs *int `json:"upstream_open_ms"`
// ThinkingMs is the first chain-of-thought fragment → the first visible token.
// Absent on every normal turn: the whole interactive surface is non-thinking. A
// value here means that posture changed.
ThinkingMs *int `json:"thinking_ms"`
// FirstOutputMs is request-in → the first visible token. The headline number — what
// a user means by "it started answering".
FirstOutputMs *int `json:"first_output_ms"`
// GenerationMs is the first visible token → complete. Pure generation.
GenerationMs *int `json:"generation_ms"`
// TotalMs is the whole server-side wait for this ONE request. A retried round bills
// and measures per attempt, so this covers the winning attempt only — which is why a
// client-observed round duration can legitimately exceed it.
TotalMs *int `json:"total_ms"`
}
TurnTimings is where a request's wall clock went, by phase, measured SERVER-side around real awaits (so each figure includes the queueing and network the user actually waited through, not the provider's own latency accounting).
Every field is a POINTER, and that is the contract rather than Go pedantry. The backend serializes with exclude_none, so a phase that did not happen is a MISSING key — never 0. A selector that never ran and a selector that answered instantly are different facts, and decoding the first as zero merges them into a lie that reads like a measurement. PreparationMs and TotalMs are the two the backend promises on every turn; they are pointers anyway so that a backend WITHOUT this block (the deployed one, until this ships) cannot produce a log line claiming a 0 ms turn.
The phases do NOT sum to TotalMs. They overlap deliberately: a speculative upstream stream opens while the selector is still running, so SelectionMs and UpstreamOpenMs can cover the same wall clock. Read each as "how long did this part take", never as a partition — and never render them as a stacked bar.
func (*TurnTimings) Any ¶
func (t *TurnTimings) Any() bool
Any reports whether the block carries at least one measured phase. A backend that sends `timings` but populates nothing (or an older one that omits it entirely) must not produce an empty timings record in a log or a caller's accounting.
func (*TurnTimings) UnmarshalJSON ¶
func (t *TurnTimings) UnmarshalJSON(data []byte) error
UnmarshalJSON decodes the block and NEVER returns an error. That is not laziness about malformed input — it is the whole point.
These numbers are telemetry. They arrive on the terminal `done` event, which the SSE parser decodes strictly: one `json.Unmarshal` failure anywhere in that event aborts the stream, and the turn — already generated, already streamed to the user, already BILLED to their key — fails. Letting a diagnostic field have that power is indefensible, and the failure is not hypothetical: every field is a `*int`, so the backend dropping a single `round()` and reporting `5775.3` would kill every turn, as would any future string-valued field. A phase we cannot parse is reported the same way as a phase that did not happen — absent — which is exactly the right answer.
Fields are parsed INDEPENDENTLY, so one bad value costs only its own phase rather than the whole block.
type Usage ¶
type Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
CachedTokens int `json:"cached_tokens"`
// Cost is what this ONE call charged the caller, in USD — their own key funds every
// upstream call, so it is their money. A POINTER because nil means "the provider
// reported nothing", which is emphatically not "free": coercing it to 0 would
// quietly under-report a running total. Never compute this client-side; the router
// knows which of ~24 endpoints served the call and what cache discount applied, and
// anything we derived from a token price would be a guess presented as a bill.
//
// On a /respond body this covers the MAIN completion only — the turn total is the
// separate `cost` block. On a /tasks result it IS that task's total (a task may
// still make two calls when a malformed response needs a repair pass).
Cost *float64 `json:"cost"`
}
Usage is the token accounting the backend reports.
type Version ¶
type Version struct {
ServerVersion string `json:"server_version"`
BuildSHA string `json:"build_sha"`
Protocol ProtocolRange `json:"protocol"`
}
Version is the GET /version body.
type WatcherClassifyInput ¶
type WatcherClassifyInput struct {
Goal string `json:"goal,omitempty"`
TerminalState TerminalState `json:"terminal_state"`
PreviousClassification string `json:"previous_classification,omitempty"`
Tail string `json:"tail,omitempty"`
}
WatcherClassifyInput classifies a terminal's recent state for the watcher.
type WatcherClassifyOutput ¶
type WatcherClassifyOutput struct {
Classification string `json:"classification"`
Confidence float64 `json:"confidence"`
Summary string `json:"summary"`
Evidence []string `json:"evidence"`
RecommendedAction string `json:"recommendedAction"`
}
WatcherClassifyOutput is the watcher classification verdict.
func RunWatcherClassify ¶
func RunWatcherClassify(ctx context.Context, r TaskRunner, in WatcherClassifyInput) (WatcherClassifyOutput, error)
RunWatcherClassify classifies a terminal's recent state.
type WorkflowBlockerOut ¶
type WorkflowBlockerOut struct {
ID string `json:"id,omitempty"`
Summary string `json:"summary"`
NodeID string `json:"node_id,omitempty"`
Kind string `json:"kind,omitempty"`
}
WorkflowBlockerOut is one blocker addition in a backend patch. Summary is the required field; ID may be null (the client mints one on apply).
type WorkflowDigest ¶
type WorkflowDigest struct {
ID string `json:"id"`
Goal string `json:"goal"`
Status string `json:"status"`
Progress string `json:"progress,omitempty"`
ActiveNodes []string `json:"active_nodes,omitempty"`
Resources []string `json:"resources,omitempty"`
Blockers []string `json:"blockers,omitempty"`
NextAction string `json:"next_action,omitempty"`
LastEvent string `json:"last_event,omitempty"`
}
WorkflowDigest is one bounded, prompt-ready summary of a workflow graph. Field names/limits MUST mirror the backend's WorkflowDigest pydantic model (contracts/extensions.py): the backend validates BEFORE it sanitizes, so an over-limit field would 422 the whole turn — the CLI clamps first (Clamp).
func CapWorkflowDigests ¶
func CapWorkflowDigests(in []WorkflowDigest) []WorkflowDigest
CapWorkflowDigests enforces the digest-list contract: clamp every digest, keep at most MaxWorkflowDigests, then drop WHOLE trailing digests until the serialized block fits MaxWorkflowStateBytes. Order is the caller's ranking (most relevant first), so the tail is always the casualty.
func (WorkflowDigest) Clamp ¶
func (d WorkflowDigest) Clamp() WorkflowDigest
Clamp returns a copy with every string bounded to its backend max_length so a verbose graph field can never 422 the turn.
type WorkflowEdgeOut ¶
WorkflowEdgeOut is one dependency edge as the backend emits it: source → target ("target depends on source"). NEVER from/to — that spelling drifted once and silently dropped every edge.
type WorkflowNodeOut ¶
type WorkflowNodeOut struct {
ID string `json:"id"`
Title string `json:"title"`
Kind string `json:"kind"`
Status string `json:"status,omitempty"`
DependsOn []string `json:"depends_on,omitempty"`
ToolName string `json:"tool_name,omitempty"`
ToolArgs map[string]any `json:"tool_args,omitempty"`
Risk string `json:"risk,omitempty"`
RequiresConfirm bool `json:"requires_confirm,omitempty"`
ExpectedEvidence []string `json:"expected_evidence,omitempty"`
AsyncPolicy string `json:"async_policy,omitempty"`
Owner string `json:"owner,omitempty"`
}
WorkflowNodeOut is one planned node as the backend emits it (snake_case wire form; converted + validated locally before storage).
type WorkflowPatchOut ¶
type WorkflowPatchOut struct {
BaseRevision *int64 `json:"base_revision,omitempty"`
NewStatus string `json:"new_status,omitempty"`
NodeUpdates []NodePatchOut `json:"node_updates,omitempty"`
AddNodes []WorkflowNodeOut `json:"add_nodes,omitempty"`
AddEdges []WorkflowEdgeOut `json:"add_edges,omitempty"`
ResourceUpdates []ResourcePatchOut `json:"resource_updates,omitempty"`
AddBlockers []WorkflowBlockerOut `json:"add_blockers,omitempty"`
ResolveBlockers []string `json:"resolve_blockers,omitempty"`
Rationale string `json:"rationale,omitempty"`
}
WorkflowPatchOut is the backend's proposed graph mutation set. UNTRUSTED: the client converts it to a local Patch, applies it to a copy, and validates the result before anything commits. Mirrors the backend WorkflowPatchOut exactly — the backend patch model has NO add_resources/add_evidence ops (resources/evidence are client-observed facts, not model suggestions).
type WorkflowPlanInput ¶
type WorkflowPlanInput struct {
Goal string `json:"goal"`
Scope string `json:"scope,omitempty"`
RuntimeSummary map[string]any `json:"runtime_summary,omitempty"`
ToolInventory []WorkflowToolInfo `json:"tool_inventory,omitempty"`
ActiveSkillIDs []string `json:"active_skill_ids,omitempty"`
ExistingWorkflow *WorkflowSnapshot `json:"existing_workflow,omitempty"`
RelevantMemories []string `json:"relevant_memories,omitempty"`
OpenResources []map[string]any `json:"open_resources,omitempty"`
Constraints []string `json:"constraints,omitempty"`
}
WorkflowPlanInput asks the backend to turn a goal + context into an executable graph. Field names mirror the backend WorkflowPlanInput pydantic model (contracts/tasks.py).
type WorkflowPlanOutput ¶
type WorkflowPlanOutput struct {
Goal string `json:"goal"`
Status string `json:"status,omitempty"`
SuccessCriteria []string `json:"success_criteria,omitempty"`
Assumptions []string `json:"assumptions,omitempty"`
Nodes []WorkflowNodeOut `json:"nodes"`
Edges []WorkflowEdgeOut `json:"edges,omitempty"`
NextAction *RecommendedActionOut `json:"next_action,omitempty"`
UserQuestion *MultipleChoiceQuestionOut `json:"user_question,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Confidence float64 `json:"confidence,omitempty"`
}
WorkflowPlanOutput is the typed plan the backend returns.
func RunWorkflowPlan ¶
func RunWorkflowPlan(ctx context.Context, r TaskRunner, in WorkflowPlanInput) (WorkflowPlanOutput, error)
RunWorkflowPlan runs workflow_plan.
type WorkflowReconcileInput ¶
type WorkflowReconcileInput struct {
Workflow *WorkflowSnapshot `json:"workflow"`
RecentEvents []map[string]any `json:"recent_events,omitempty"`
RuntimeSummary map[string]any `json:"runtime_summary,omitempty"`
QueueEvents []map[string]any `json:"queue_events,omitempty"`
LatestUserMessage string `json:"latest_user_message,omitempty"`
Reason string `json:"reason"`
ToolInventory []WorkflowToolInfo `json:"tool_inventory,omitempty"`
}
WorkflowReconcileInput hands the backend the CURRENT graph + recent evidence and asks for a safe patch.
type WorkflowReconcileOutput ¶
type WorkflowReconcileOutput struct {
Patch WorkflowPatchOut `json:"patch"`
NextAction *RecommendedActionOut `json:"next_action,omitempty"`
UserQuestion *MultipleChoiceQuestionOut `json:"user_question,omitempty"`
Summary string `json:"summary,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Confidence float64 `json:"confidence,omitempty"`
}
WorkflowReconcileOutput is the typed reconcile result.
func RunWorkflowReconcile ¶
func RunWorkflowReconcile(ctx context.Context, r TaskRunner, in WorkflowReconcileInput) (WorkflowReconcileOutput, error)
RunWorkflowReconcile runs workflow_reconcile.
type WorkflowResumeDigestInput ¶
type WorkflowResumeDigestInput struct {
Workflows []map[string]any `json:"workflows,omitempty"`
RuntimeSummary map[string]any `json:"runtime_summary,omitempty"`
QueueDigest []map[string]any `json:"queue_digest,omitempty"`
UserMessage string `json:"user_message,omitempty"`
}
WorkflowResumeDigestInput asks the backend to rank the active workflows and produce a compact resume package (the "where were we?" surface).
type WorkflowResumeDigestOutput ¶
type WorkflowResumeDigestOutput struct {
Ranked []WorkflowResumeItem `json:"ranked"`
SuggestedFocusWorkflowID string `json:"suggested_focus_workflow_id,omitempty"`
AssistantContextLines []string `json:"assistant_context_lines,omitempty"`
}
WorkflowResumeDigestOutput is the typed resume package.
func RunWorkflowResumeDigest ¶
func RunWorkflowResumeDigest(ctx context.Context, r TaskRunner, in WorkflowResumeDigestInput) (WorkflowResumeDigestOutput, error)
RunWorkflowResumeDigest runs workflow_resume_digest.
type WorkflowResumeItem ¶
type WorkflowResumeItem struct {
WorkflowID string `json:"workflow_id"`
Rank int `json:"rank"`
Headline string `json:"headline"`
Status string `json:"status,omitempty"`
Summary string `json:"summary,omitempty"`
Blockers []string `json:"blockers,omitempty"`
RecommendedAction *RecommendedActionOut `json:"recommended_action,omitempty"`
}
WorkflowResumeItem is one ranked resume entry.
type WorkflowSnapshot ¶
type WorkflowSnapshot struct {
ID string `json:"id"`
Revision int64 `json:"revision"`
Goal string `json:"goal"`
Status string `json:"status"`
Nodes []WorkflowSnapshotNode `json:"nodes"`
Edges []WorkflowEdgeOut `json:"edges"`
Resources []WorkflowSnapshotResource `json:"resources"`
Blockers []WorkflowSnapshotBlocker `json:"blockers"`
}
WorkflowSnapshot is the snake_case graph snapshot the reconcile task input carries (and the plan task's existing_workflow). Built explicitly from the local Graph — never a JSON round-trip of the camelCase local shape — because the backend's cycle detection and terminal-status guard read exactly these keys (node id/status/depends_on, edge source/target); drifted keys are silently NOT read and those safety checks go dark.
type WorkflowSnapshotBlocker ¶
type WorkflowSnapshotBlocker struct {
ID string `json:"id"`
Summary string `json:"summary"`
NodeID string `json:"node_id"`
Kind string `json:"kind"`
}
WorkflowSnapshotBlocker is one OPEN blocker in the reconcile snapshot. Its id is what the backend's resolve_blockers references back.
type WorkflowSnapshotNode ¶
type WorkflowSnapshotNode struct {
ID string `json:"id"`
Title string `json:"title"`
Kind string `json:"kind"`
Status string `json:"status"`
DependsOn []string `json:"depends_on"`
ToolName string `json:"tool_name,omitempty"`
}
WorkflowSnapshotNode is one node in the reconcile snapshot. DependsOn must carry EVERY pre-existing dependency (the local layer unions its depends_on lists with its explicit edges) so backend cycle detection sees the full picture.
type WorkflowSnapshotResource ¶
type WorkflowSnapshotResource struct {
ID string `json:"id"`
Kind string `json:"kind"`
Status string `json:"status"`
NodeID string `json:"node_id"`
Label string `json:"label"`
}
WorkflowSnapshotResource is one tracked resource in the reconcile snapshot. Its id is the LOCAL resource id — the backend's resource_updates patches reference it back as resource_id.
type WorkflowToolInfo ¶
WorkflowToolInfo is one tool-inventory entry for planning/reconciliation: just enough for the backend to recommend REAL, callable tools (never the full schema — the plan prompt doesn't need it and the payload stays small).
type WorktreeSnapshot ¶
type WorktreeSnapshot struct {
ID string `json:"id,omitempty"`
Path string `json:"path,omitempty"`
Branch string `json:"branch,omitempty"`
IsMain bool `json:"is_main"`
IssueNumber *int `json:"issue_number,omitempty"`
IssueTitle string `json:"issue_title,omitempty"`
PRNumber *int `json:"pr_number,omitempty"`
PRTitle string `json:"pr_title,omitempty"`
PRURL string `json:"pr_url,omitempty"`
Status string `json:"status,omitempty"`
LastCommit string `json:"last_commit,omitempty"`
}
WorktreeSnapshot is the useful subset of worktree.getCurrent, normalized and bounded by the CLI before it reaches the backend's strict request validator.