bridle package - github.com/CarriedWorldUniverse/bridle - Go Packages

bridle

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 25, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

README

bridle

CI Release Go Reference License

The harness layer of the Nexus funnel/harness split. A small Go library that drives one deliberation turn of one model with one tool surface, emitting a stream of observable events and returning a structured TurnResult.

The funnel imports bridle. Aspects do not import bridle directly.

A bridle controls and directs without being the horse — sits beneath the funnel, governs what the model produces, provider-agnostic.

Status

Implemented and in active use. The original build brief is preserved in docs/2026-05-01-bridle-spec.md; the current code includes direct API providers, headless CLI providers, hook support, MCP plumbing for direct providers, and a local tool runner.

Scope

  • One stable provider interface with direct-API and subprocess-stream implementations.
  • Direct API providers: claude-api, openai-api, bedrock, gemini-api, ollama-local.
  • Headless CLI providers: claude-code, gemini-cli, codex-cli, antigravity-cli, plus claude-pty.
  • Hook surface for model-call, tool-call, step-boundary, and turn-done behavior.
  • Per-turn timing instrumentation (TurnTiming) at the harness seam, broken down by round and tool, so the funnel can observe where a turn spends its wall clock.
  • Shared subprocess plumbing for the CLI providers lives in internal/subprocess.
  • send_comms is just a tool the funnel supplies — bridle has no special case.
  • Funnel owns session JSONL; bridle proposes deltas.

Provider categories

direct-api providers talk to a model API and let bridle own the tool loop. They can consume bridle ToolDefs, run tools through the supplied ToolRunner, fire before/after tool hooks, and use bridle MCP configuration.

subprocess-stream providers spawn a headless CLI that owns its own agent loop and tool execution. Bridle observes the CLI JSON stream, emits model/tool events, and returns a normalized TurnResult; it does not re-run those tool calls through the bridle ToolRunner.

codex-cli uses codex exec --json. Set TurnRequest.Model to the Codex model id to pass --model, or use Model: "default" to let the Codex CLI use its configured default model while still satisfying bridle's required model field. Existing sessions resume via codex exec resume <session-id>.

antigravity-cli drives the agy CLI headless (plain-text, not stream-json), resuming via -c rather than a passed session id, and strips stale conversation warnings from the output.

ollama-local exposes KeepAlive (how long the server keeps a model resident — defaults to 30m so gemma-class models stay warm across quiet gaps), NumCtx (the context window, mapped to options.num_ctx), and an Options map that is merged into the request options on every turn.

Test

go test ./...

Live provider smoke tests are opt-in. For Codex CLI:

BRIDLE_LIVE_CODEX=1 go test ./provider/codexcli -run TestRoundTripLive -count=1

Non-goals

  • Not a framework, not an agent runtime, not a session store. It does one thing: drive one turn.
  • See spec §9 for the full non-goals list.

Reference reading

PydanticAI (iter() / CallToolsNode), Eino (streaming + tool-call normalization), PI (headless JSON streaming), Strands (register_hooks), LangGraph (interrupt/checkpoint).

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrModelRequired = errors.New("bridle: TurnRequest.Model is required")

ErrModelRequired is returned by RunTurn when TurnRequest.Model is empty.

View Source
var ErrToolNameCollision = errors.New("bridle: tool name collision between explicit Tools and MCP-loaded tools")

ErrToolNameCollision is returned by RunTurn when a tool name appears in both TurnRequest.Tools (explicit) and the MCP-loaded tool surface.

Functions

func AppendAssistantText added in v0.1.3

func AppendAssistantText(finalText *string, sessionDelta *[]SessionEvent, providerID ProviderID, text string)

AppendAssistantText accumulates a chunk of model-emitted text into *finalText and appends a matching assistant-role SessionEvent (with Provider set) to *sessionDelta. Does NOT emit a ModelChunk — that's the caller's job when the chunk is observed live during a streaming loop.

Centralises the trio (accumulate + SessionEvent shape) that every direct-API provider's extractResult / subprocess-stream provider's text branch repeats. Concretely, this is the spot where forgetting to set Provider silently broke ParseSessionEvent in three providers; using this helper makes the field impossible to omit.

func EmitAssistantText added in v0.1.3

func EmitAssistantText(sink EventSink, finalText *string, sessionDelta *[]SessionEvent, providerID ProviderID, text string)

EmitAssistantText is AppendAssistantText plus a live ModelChunk emit. Use from subprocess-stream provider parsers (claudecode, geminicli) where the same text is BOTH streamed live AND folded into the final result/session log. Direct-API providers that emit chunks inside their SDK stream loop and lower a separate aggregate in extractResult should use AppendAssistantText in the lowering path and call sink.Emit directly in the stream loop.

func IsProviderErrorKind added in v0.1.1

func IsProviderErrorKind(err error, kind ProviderErrorKind) bool

IsProviderErrorKind reports whether err (or any error in its chain) is a ProviderError with the given kind.

Types

type AfterToolCallCtx

type AfterToolCallCtx struct {
	Call   ToolCall
	Result ToolCallResult
	Step   int
}

AfterToolCallCtx carries context passed to AfterToolCall hooks.

type BeforeModelCallCtx

type BeforeModelCallCtx struct {
	Request *ProviderRequest
	Step    int
}

BeforeModelCallCtx carries context passed to BeforeModelCall hooks.

Request points at the live ProviderRequest that the harness is about to send to the provider — the same struct, not a copy. Hooks may mutate its fields in place (Model, AppendSystemPrompt, Tools, ProviderEnv, Messages, etc.) and the changes apply to the upcoming call. The hook fires once before the initial call (Step=0) and once before every subsequent call inside the tool loop (Step=N), so per-step mutations (escalate model on N, drop a tool once used) work.

Mutating Messages is supported but advanced: by the time the in-loop hook fires, the harness has already appended the assistant tool_use turn and the tool_result blocks for the round just completed.

type BeforeToolCallCtx

type BeforeToolCallCtx struct {
	Call ToolCall
	Step int

	// Deny, when set true by a BeforeToolCall hook, tells the harness to
	// SKIP executing this tool call and instead return Result/Err as the
	// tool_result, then continue the loop so the model can react. Use this
	// (with a returned HookContinue) for per-call permission denials —
	// HookAbort ends the whole turn, which is not what a single-call
	// denial wants. Defaults false: existing hooks are unaffected.
	Deny bool
	// Result is the tool_result JSON payload to hand back when Deny is
	// set. nil is treated as JSON null. Ignored unless Deny is true.
	Result json.RawMessage
	// Err, when non-empty and Deny is set, marks the tool_result as an
	// error string the model sees (mirrors a runner.Run error). Ignored
	// unless Deny is true.
	Err string
}

BeforeToolCallCtx carries context passed to BeforeToolCall hooks.

Permission-deny pattern: a BeforeToolCall hook can refuse a single tool call without ending the turn by setting Deny=true (optionally with Result and/or Err) and returning HookContinue. The harness then SKIPS executing that call (no runner.Run, no MCP dispatch), builds the tool_result from Result/Err, fires AfterToolCall for audit, and continues the loop so the model sees the refusal and can react. Contrast HookAbort, which ends the whole turn — use Deny for refusing one call, HookAbort for killing the turn.

Canonical hook:

h.RegisterBeforeToolCall(func(ctx context.Context, in bridle.BeforeToolCallCtx) (bridle.BeforeToolCallCtx, bridle.HookAction, error) {
	if !policy.Allows(in.Call) {
		in.Deny = true
		in.Err = "permission denied by policy: " + in.Call.Name
	}
	return in, bridle.HookContinue, nil
})

type ContextBudgetWarning added in v0.2.0

type ContextBudgetWarning struct {
	Assembled int       // estimated assembled-prompt token count
	Budget    int       // the ContextPolicy.PromptBudget that was met/exceeded
	TS        time.Time // stamped by the harness at emission; zero outside a harness turn
}

ContextBudgetWarning fires when the assembled prompt's estimated token count meets or exceeds the request's ContextPolicy.PromptBudget (NEX-581). It is observability only — bridle does NOT truncate or hard-fail on it; the funnel decides what to do (log, trim future turns, alert). Assembled is the estimated token count of the prompt the provider received this round; Budget is the policy cap that was crossed. Mirrors MCPServerFailed's stamped-event shape.

type ContextPolicy added in v0.2.0

type ContextPolicy struct {
	// TargetWindow is the desired context window in tokens. 0 = no
	// preference (engine default / model-fixed window). Providers with a
	// per-request window knob (ollama num_ctx) map this; fixed-window
	// providers no-op it.
	TargetWindow int

	// PromptBudget is a soft cap, in tokens, on the assembled prompt
	// before bridle warns. 0 = no budget (no warning ever). Honored by
	// ALL providers — the warning is computed at the harness seam, not
	// in the provider.
	PromptBudget int
}

context.go implements the context contract (NEX-581): context-window sizing is a bridle-owned, per-aspect policy expressed ONCE on the request and mapped to whatever knob each engine exposes — so callers don't reach past bridle to set engine-specific context flags.

The policy has two independent levers:

  • TargetWindow is the desired context window in tokens. Each provider maps it to its engine's mechanism: ollama sets options.num_ctx; fixed-window API providers (claude / openai / bedrock / gemini) no-op it because the window is model-fixed and not a per-request knob; vLLM-via-openai no-ops it too (the window is server-side, --max-model-len, not per-request). CLI providers no-op it.

  • PromptBudget is a soft cap, in tokens, on the assembled prompt size. It is engine-AGNOSTIC: ALL providers honor it. After request assembly, bridle estimates the assembled prompt's token count (the usage contract's estimateTokens over the assembled input) and, when that meets or exceeds PromptBudget, emits a ContextBudgetWarning for the funnel to log/act on. v1 is warn-only: bridle never truncates or hard-fails (truncation policy is future work).

The zero value is "no policy": no num_ctx override (engine defaults hold) and no budget warning. This preserves current behaviour for callers that don't set a policy.

type DoneEvent added in v0.2.0

type DoneEvent struct{ StopReason StreamStopReason }

DoneEvent is Stream's done {stop_reason} event — the terminal, successful-completion signal.

type ErrorClass added in v0.2.0

type ErrorClass string

ErrorClass is the Stream-facing error taxonomy (agora-spec-bridle §3: "auth | rate_limit | overloaded | context_length | schema | network | refusal | provider"). agora's retry policy keys off the class: rate_limit/overloaded/network are retryable with backoff, auth surfaces immediately, context_length routes to the context manager, refusal is non-retryable content surfaced to the turn/approval layer, provider is the residual "something else went wrong" bucket.

T1/T7 adds the TYPE and the mapping-table home (internal/normalize's ProviderErrorClass) so Stream's error{class} event has somewhere to land; per-lane DETECTION of overloaded/context_length/schema/refusal from real wire errors is T3 follow-up work — today's ProviderErrorKind values don't distinguish those four yet.

const (
	ErrorClassAuth          ErrorClass = "auth"
	ErrorClassRateLimit     ErrorClass = "rate_limit"
	ErrorClassOverloaded    ErrorClass = "overloaded"
	ErrorClassContextLength ErrorClass = "context_length"
	ErrorClassSchema        ErrorClass = "schema"
	ErrorClassNetwork       ErrorClass = "network"
	ErrorClassRefusal       ErrorClass = "refusal"
	ErrorClassProvider      ErrorClass = "provider"
)

func ClassifyStreamError added in v0.2.0

func ClassifyStreamError(err error) ErrorClass

ClassifyStreamError derives an ErrorClass from a RunStep/RunTurn error for Stream's error{class} event. Mirrors (but cannot call — see internal/normalize.ProviderErrorClass's doc comment for the import-cycle reason) normalize.ProviderErrorClass's *ProviderError switch. Non-ProviderError errors (context cancellation, a panic converted to error, etc.) fall through to ErrorClassProvider — the residual bucket.

Exported (rather than left package-private, like the rest of Stream's helpers) solely so a cross-package test can assert this switch stays in sync with normalize.ProviderErrorClass without recreating the import cycle that prevents this function from calling that one directly (see stream_test.go's TestClassifyStreamError_ MatchesProviderErrorClass).

type ErrorEvent added in v0.2.0

type ErrorEvent struct {
	Class ErrorClass
	Err   error
}

ErrorEvent is Stream's error {class} event — the terminal, failed-completion signal.

type Event

type Event interface {
	// contains filtered or unexported methods
}

Event is the union type for all observable harness events.

type EventSink

type EventSink interface {
	Emit(Event)
}

EventSink receives events as the turn unfolds.

type Harness

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

Harness drives one deliberation turn with one provider.

func NewHarness

func NewHarness(p Provider) *Harness

NewHarness creates a Harness backed by the given provider.

func (*Harness) RegisterAfterToolCall

func (h *Harness) RegisterAfterToolCall(fn Hook[AfterToolCallCtx]) HookID

RegisterAfterToolCall adds a hook that fires after each tool execution. Returns a HookID that can be passed to UnregisterHook.

func (*Harness) RegisterBeforeModelCall

func (h *Harness) RegisterBeforeModelCall(fn Hook[BeforeModelCallCtx]) HookID

RegisterBeforeModelCall adds a hook that fires before each model invocation. Returns a HookID that can be passed to UnregisterHook.

func (*Harness) RegisterBeforeToolCall

func (h *Harness) RegisterBeforeToolCall(fn Hook[BeforeToolCallCtx]) HookID

RegisterBeforeToolCall adds a hook that fires before each tool execution. Returns a HookID that can be passed to UnregisterHook.

To refuse a single call without ending the turn, set in.Deny=true (with in.Err / in.Result) and return HookContinue — see BeforeToolCallCtx for the permission-deny pattern. Return HookAbort only to kill the whole turn.

func (*Harness) RegisterOnStepBoundary

func (h *Harness) RegisterOnStepBoundary(fn Hook[OnStepBoundaryCtx]) HookID

RegisterOnStepBoundary adds a hook that fires between tool-call rounds. Returns a HookID that can be passed to UnregisterHook.

func (*Harness) RegisterOnTurnDone

func (h *Harness) RegisterOnTurnDone(fn Hook[OnTurnDoneCtx]) HookID

RegisterOnTurnDone adds a hook that fires after the turn completes. Hooks may mutate TurnResult.SessionDelta. Returns a HookID that can be passed to UnregisterHook.

func (*Harness) RunStep added in v0.2.0

func (h *Harness) RunStep(ctx context.Context, preq ProviderRequest, sink EventSink) (presult ProviderResult, err error)

RunStep drives ONE provider round for a direct-API lane, WITHOUT owning a tool-execution loop — additive to RunTurn, which stays untouched (existing nexus/funnel callers are unaffected).

Why this exists (NEX-767 T7 / agora-spec-bridle §2): RunTurn's round loop (run.go) executes tool calls itself and re-invokes the provider with synthesized tool_results, because that's what the nexus funnel wants. agora's Stream contract wants the OPPOSITE for direct-API lanes: hand back complete tool_calls and let agora execute them and decide what happens next. RunStep is the single-round primitive that makes that possible — it reuses runProviderRound (round timing) and enforceToolCallContract (NEX-581 leak detection/repair) so a Stream round gets the same protection every RunTurn round gets, MINUS the loop itself, MINUS tool execution, MINUS the MaxSteps check (agora owns those on its own turn engine).

Subprocess-stream lanes (claude-code) do NOT use RunStep — RunTurn is already single-shot for them (run.go's `if !caps.SupportsCustomTools` break fires before any tool re-invocation), so Stream calls RunTurn unmodified for that category. RunStep is direct-api only.

Callers pass an already-lowered ProviderRequest (Stream builds one from its own Request shape — see stream.go) rather than a TurnRequest, since RunStep skips everything TurnRequest carries for the tool-loop/session/MCP machinery it doesn't run. RunStep has the same recover() boundary Harness.RunTurn has (see harness.go's RunTurn): an unrecovered panic here would otherwise propagate straight out of Registry.Stream's goroutine and crash the whole process — every in-flight turn across every lane, not just the offending one (RunStep/Stream is the seam every model call routes through, so its blast radius is far broader than RunTurn's own panic-isolated funnel path).

func (*Harness) RunTurn

func (h *Harness) RunTurn(ctx context.Context, req TurnRequest, runner ToolRunner, sink EventSink) (result TurnResult, err error)

RunTurn drives one turn: calls the provider, executes tool calls via runner, fires hooks at documented points, and emits events to sink. Cancellation via ctx returns a partial TurnResult with StopReason=aborted. Timing is populated on normal completion and on provider errors; it is zero on context-cancellation aborts. Returns ErrModelRequired if req.Model is empty.

func (*Harness) UnregisterHook added in v0.1.3

func (h *Harness) UnregisterHook(id HookID) bool

UnregisterHook removes the hook with the given id from whichever hook slice it was registered into. Returns true if a hook was removed, false if no hook with that id exists. The zero HookID is never registered and always returns false.

Not safe to call concurrently with RunTurn or with Register*. See HookID for the threading contract.

type Hook

type Hook[T any] func(ctx context.Context, in T) (T, HookAction, error)

Hook is the generic hook signature. T is the mutable context value passed in and returned. Registration order is the execution order.

type HookAction

type HookAction int

HookAction tells the harness what to do after a hook returns.

const (
	HookContinue HookAction = iota
	HookAbort               // end the turn; partial TurnResult returned with StopReason=aborted
)

type HookID added in v0.1.3

type HookID uint64

HookID identifies a registered hook so it can be removed via Harness.UnregisterHook. The zero value is not a valid hook id — every successful Register* call returns a non-zero id.

Hook registration is NOT safe to call concurrently with RunTurn or with itself; mirror the existing assumption that hooks are wired during setup. If you need to swap a hook between turns, do it from a single goroutine while no turn is in flight.

type InboxItem

type InboxItem struct {
	From    string
	Content string
	MsgID   int64
	RawJSON json.RawMessage

	// ThreadRoot is the canonical thread identity for the message
	// (linked-list root id; nexus task #226). The funnel uses it to
	// key per-thread session state so each thread gets its own
	// claude-code jsonl, preventing SessionTail bleed across threads.
	// Zero = legacy/non-chat synthetic item or pre-#226 row.
	ThreadRoot int64

	// Source identifies which trigger channel produced this item.
	// Empty defaults to "chat" (legacy / nexus-chat substrate).
	// agora-side callers set Source="tty" for operator-typed inputs,
	// allowing ReturnHandlers to branch routing (chat → bus reply,
	// tty → panel-only). Future trigger channels add new Source
	// values; consumers default-treat unknown values as "chat" for
	// backward compat.
	Source string
}

InboxItem is a comms message that arrived during the previous turn. The harness folds these into the prompt context before the first model call. Read-only from the harness's perspective.

MsgID is the chat msg_id this item was sourced from. It carries through into the prompt so the model can reference items by id when triaging ("triage(msg_id=17, decision='reply')"). Zero means the item didn't originate from a chat message — it's an internal/synthetic item the funnel injected, and the triage contract doesn't apply.

type JSONSchema added in v0.2.0

type JSONSchema struct {
	Name        string
	Description string
	Schema      json.RawMessage
	Strict      bool
}

JSONSchema constrains a Stream response to a schema (agora-spec-bridle §3, T4 structured-output forcing). T1/T7 threads the TYPE through Request so the wire shape exists; it does not yet enforce anything — no provider is forced into structured-output mode by this facade today. That enforcement (native json-schema mode where capable, else forced single-tool-call-and-unwrap, guaranteeing validate-or- error{class:schema}) is T4 follow-up work.

type LaneID added in v0.2.0

type LaneID string

LaneID identifies a fine-grained model lane — finer than ProviderID.

ProviderID isn't fine-grained enough for the registry: openai.go returns ProviderOpenAI regardless of baseURL, so OpenAI-proper and DeepSeek collide under one ProviderID key (DeepSeek is the same Go Provider pointed at api.deepseek.com). The registry keys catalog rows and Harness bindings on LaneID instead, one level below ProviderID.

const (
	LaneClaudeAPI  LaneID = "claude-api"
	LaneClaudeCode LaneID = "claude-code"
	LaneClaudeSDK  LaneID = "claude-sdk" // NEX-745 (parallel branch) owns the Provider; catalog rows here are placeholders
	LaneOpenAI     LaneID = "openai"
	LaneDeepSeek   LaneID = "deepseek"
	LaneGeminiAPI  LaneID = "gemini-api"
	LaneBedrock    LaneID = "bedrock"
	LaneOllama     LaneID = "ollama"
)

The 7 core lanes the T1 static catalog covers. Uncataloged lanes are a deliberate gap, not an oversight — Registry.Resolve errors "not cataloged" for anything outside this set, which enforces core-lanes-only without needing a separate allowlist.

type MCPClientConfig

type MCPClientConfig struct {
	Servers []MCPServerSpec
}

MCPClientConfig describes how bridle connects to MCP servers and what tool surface the model sees from them. The funnel constructs this; bridle consumes it. Ignored by subprocess-stream providers (SupportsMCP=false).

type MCPServerFailed added in v0.2.0

type MCPServerFailed struct {
	Server string
	Err    error
	TS     time.Time // stamped by the harness at emission; zero outside a harness turn
}

MCPServerFailed fires when an MCP server fails to connect/initialize during turn setup. NEX-596: such a failure is non-fatal — the server's tools are dropped and the turn proceeds with the remaining servers. This event surfaces the dropped server for observability.

type MCPServerSpec

type MCPServerSpec struct {
	Name      string            // local identifier, used in tool-call provenance
	Transport MCPTransport      // stdio | http_sse
	Command   []string          // argv to spawn the server (stdio only)
	URL       string            // server URL (http_sse only)
	Env       map[string]string // environment variables for spawned server (stdio only)
	Header    map[string]string // request headers (http_sse only)
}

MCPServerSpec describes a single MCP server connection.

type MCPTransport

type MCPTransport string

MCPTransport identifies the wire transport for an MCP server connection.

const (
	MCPTransportStdio   MCPTransport = "stdio"
	MCPTransportHTTPSSE MCPTransport = "http_sse"
)

type MessageRole added in v0.2.0

type MessageRole string

MessageRole is the abstract role a Request message carries — the three-role "authority gradient" agora composes with (system > developer > user; agora-spec-prompt §1a). bridle maps these onto each provider's wire shape (agora-spec-bridle §3): native developer role where the API has one; folded into a post-core system block on Anthropic-shaped APIs otherwise. T1/T7 folds BOTH system and developer into one system-prompt block unconditionally (see lowerRoleMessages); per-provider native-developer-role mapping is T5 follow-up work (openai.go only, per the blueprint's ticket slot-in).

const (
	MessageRoleSystem    MessageRole = "system"
	MessageRoleDeveloper MessageRole = "developer"
	MessageRoleUser      MessageRole = "user"
)

type ModelCapabilities added in v0.2.0

type ModelCapabilities struct {
	Tools            bool
	ParallelTools    bool
	Streaming        bool
	ReasoningEffort  bool
	StructuredOutput bool
	PromptCaching    bool
	Vision           bool

	// SystemPromptMode advertises which system-prompt application mode
	// this catalog row was authored against: SystemPromptFull ("full",
	// alias of Replace) or SystemPromptAppend ("append"). claude-code
	// genuinely supports BOTH — the catalog row's "append" is an
	// ADVISORY default recording agora policy (preserve claude-code's
	// built-in prompt), not a hard capability gate. Callers may still
	// pass either mode on TurnRequest/ProviderRequest; this field only
	// documents the lane's typical/recommended choice.
	SystemPromptMode SystemPromptMode
}

ModelCapabilities are model-level capability axes — orthogonal to the per-provider ProviderCapabilities (which describe how a Go Provider executes tool calls: subprocess-stream vs direct-api, MCP support, etc). A model's EFFECTIVE tool support is the AND of both: ProviderCapabilities.SupportsCustomTools && ModelCapabilities.Tools. The remaining fields here have no ProviderCapabilities analogue — they're net-new axes agora needs (agora-spec-bridle §1).

type ModelChunk

type ModelChunk struct {
	Text string
	TS   time.Time // stamped by the harness at emission; zero outside a harness turn
}

ModelChunk carries a streamed text fragment from the model.

type ModelHandle added in v0.2.0

type ModelHandle struct {
	Lane     string
	Provider ProviderID
	Model    string
	Info     ModelInfo
}

ModelHandle is the resolved reference Registry.Resolve returns. Deliberately credential-free: Stream looks up the bound *Harness (and therefore the credentialed Provider) from the Handle's Lane at call time — the handle itself never carries auth/base-url configuration.

type ModelInfo added in v0.2.0

type ModelInfo struct {
	ID              string
	Lane            LaneID
	Provider        ProviderID
	Aliases         []string
	ContextWindow   int // tokens — required; the skills 2% catalog budget + context-manager depend on it
	MaxOutputTokens int
	Capabilities    ModelCapabilities
	Pricing         *ModelPricing    // nil = unknown/unset
	Prompt          *ModelPromptMeta // nil = no prompt-presentation metadata authored yet
}

ModelInfo describes one catalog entry: a specific model on a specific lane. Field-for-field agora-spec-bridle §1. The catalog (see registry_models.toml + NewRegistry) is the only place ModelInfo values are constructed for the 7 core lanes; RegisterAlias adds agora-config-sourced aliases on top without minting new ModelInfo.

type ModelPricing added in v0.2.0

type ModelPricing struct {
	In     float64
	Out    float64
	Cached float64
}

ModelPricing is per-million-token USD pricing. Zero value means pricing is unknown/unset — token-only accounting until populated (agora-spec-bridle §1: "optional pricing {in, out, cached} — enables cost-aware workflow budgets — token-only until present").

type ModelPromptMeta added in v0.2.0

type ModelPromptMeta struct {
	Dialect      *PromptDialect
	RenditionRef string
}

ModelPromptMeta is the optional prompt-presentation metadata for a catalog row (agora-spec-bridle §1: "optional prompt {dialect | rendition_ref}").

type NormalizedSessionEvent

type NormalizedSessionEvent struct {
	Role    SessionRole
	Content string // human-readable text representation
}

NormalizedSessionEvent is a provider-agnostic view of a SessionEvent, used when displaying or processing session history without knowing the provider's wire format.

func ParseSessionEvent

func ParseSessionEvent(e SessionEvent) (NormalizedSessionEvent, error)

ParseSessionEvent returns a normalized view of a session event. For events with RawJSON, it attempts a provider-specific parse; falls back to Content if RawJSON is absent or unrecognized.

type OnStepBoundaryCtx

type OnStepBoundaryCtx struct {
	Step int
}

OnStepBoundaryCtx carries context passed to OnStepBoundary hooks.

type OnTurnDoneCtx

type OnTurnDoneCtx struct {
	Result *TurnResult
}

OnTurnDoneCtx carries context passed to OnTurnDone hooks. Hooks may mutate SessionDelta before it is returned to the funnel.

type PromptDialect added in v0.2.0

type PromptDialect struct {
	ToolIdiom string
	Format    string
	Thinking  string
}

PromptDialect carries model-global presentation knobs — tool idiom, wire format, thinking guidance. Per-core adjustments/renditions live in the agora core package, not here (agora-spec-prompt §2a/§4).

type Provider

type Provider interface {
	Name() ProviderID
	Capabilities() ProviderCapabilities
	RunTurn(ctx context.Context, req ProviderRequest, sink EventSink) (ProviderResult, error)
}

Provider is the interface every model backend must implement. Provider-specific weirdness (streaming, wire format, tool-schema translation) stays inside the implementation; the harness sees a uniform event stream.

type ProviderCapabilities

type ProviderCapabilities struct {
	Category               ProviderCategory
	SupportsCustomTools    bool // funnel can pass arbitrary Tools via TurnRequest
	SupportsBeforeToolCall bool // BeforeToolCall hook fires
	SupportsAfterToolCall  bool // AfterToolCall hook fires
	SupportsMCP            bool // provider consumes TurnRequest.MCP (direct-api only)
}

ProviderCapabilities advertises what a provider supports so the harness and funnel can route turns correctly.

type ProviderCategory

type ProviderCategory string

ProviderCategory classifies how a provider executes tool calls.

const (
	// CategoryDirectAPI — provider talks directly to a model API; bridle owns the tool loop.
	CategoryDirectAPI ProviderCategory = "direct-api"
	// CategorySubprocessStream — provider spawns a subprocess that runs its own agentic loop
	// and emits a structured event stream. The subprocess owns tool execution.
	CategorySubprocessStream ProviderCategory = "subprocess-stream"
)

type ProviderError added in v0.1.1

type ProviderError struct {
	Kind    ProviderErrorKind
	Message string
	Err     error // underlying error (may be nil)
}

ProviderError is a classified provider-level error.

func (*ProviderError) Error added in v0.1.1

func (e *ProviderError) Error() string

func (*ProviderError) Unwrap added in v0.1.1

func (e *ProviderError) Unwrap() error

type ProviderErrorKind added in v0.1.1

type ProviderErrorKind string

ProviderErrorKind classifies a provider-level error so callers can surface a distinct diagnosis string instead of an opaque exit code.

const (
	ProviderErrorAuthFailed   ProviderErrorKind = "auth_failed"
	ProviderErrorRateLimit    ProviderErrorKind = "rate_limit"
	ProviderErrorServerError  ProviderErrorKind = "server_error"
	ProviderErrorNetworkError ProviderErrorKind = "network_error"
	ProviderErrorTimeout      ProviderErrorKind = "timeout"
	ProviderErrorTLSError     ProviderErrorKind = "tls_error"
	// ProviderErrorConfig is a non-transient setup failure: the CLI
	// binary is missing from PATH, a referenced config file/profile is
	// absent, or a required flag/argument is malformed. Retrying is
	// futile — the fix is operator configuration, not a re-run.
	ProviderErrorConfig ProviderErrorKind = "config_error"
	// ProviderErrorCrash is an abnormal subprocess termination distinct
	// from an orderly non-zero exit: a fatal signal (segfault/abort), an
	// out-of-memory kill, or a panic/stack-overflow in the CLI itself.
	// Surfaced separately so operators can tell "the model API rejected
	// us" (auth/rate) from "the CLI process itself died".
	ProviderErrorCrash ProviderErrorKind = "subprocess_crash"
	// ProviderErrorSubprocessExit is the fallback kind used when a
	// subprocess-style provider exited non-zero and no other
	// classification matched. Callers can filter for this via
	// IsProviderErrorKind to handle the generic-failure case
	// distinctly from the more specific classes above.
	ProviderErrorSubprocessExit ProviderErrorKind = "subprocess_exit"
	// ProviderErrorRefusal is a model-level safety refusal (the engine
	// declined to continue the turn) rather than a transport/auth/rate
	// failure. Distinct so callers don't retry a refusal the way they'd
	// retry a transient error — re-sending the same turn to the same
	// model is very unlikely to produce a different outcome. Surfaced by
	// claudesdk when the Agent SDK reports stop_reason "refusal" (bridle
	// spec NEX-745 §7).
	ProviderErrorRefusal ProviderErrorKind = "refusal"
)

type ProviderID

type ProviderID string

ProviderID identifies a model provider.

const (
	ProviderClaude         ProviderID = "claude-api"
	ProviderClaudeCode     ProviderID = "claude-code"
	ProviderClaudeSDK      ProviderID = "claude-sdk"
	ProviderClaudePty      ProviderID = "claude-pty"
	ProviderOllama         ProviderID = "ollama-local"
	ProviderOpenAI         ProviderID = "openai-api"
	ProviderBedrock        ProviderID = "bedrock"
	ProviderGemini         ProviderID = "gemini-api"
	ProviderGeminiCLI      ProviderID = "gemini-cli"
	ProviderCodexCLI       ProviderID = "codex-cli"
	ProviderAntigravityCLI ProviderID = "antigravity-cli"
)

type ProviderMessage

type ProviderMessage struct {
	Role       string // "user" | "assistant" | "tool_result" | "system"
	Content    string
	ToolCallID string           // links a tool_result back to the call that produced it
	ToolName   string           // function-declaration name; required for tool_result on Gemini
	ToolCalls  []ToolInvocation // tool_use blocks for assistant turns; nil on other roles

	// ThinkingBlocks (NEX-320) preserves Claude extended-thinking
	// content blocks across turns. Required by the Anthropic API:
	// subsequent turns whose conversation history is missing the
	// thinking blocks from prior assistant turns get rejected with
	// 400 ("content[].thinking in the thinking mode must be passed
	// back to the API"). Providers without thinking-mode support
	// (OpenAI, Gemini, claudecode subprocess) ignore this field;
	// nil/empty when thinking wasn't engaged or for non-assistant
	// roles. Order is preserved — Anthropic requires thinking blocks
	// appear BEFORE text/tool_use blocks in the reconstructed turn.
	ThinkingBlocks []ThinkingBlock

	// ReasoningContent (NEX-340) is the openai-shape parallel of
	// ThinkingBlocks for DeepSeek reasoner-style models. DeepSeek's
	// `/v1` returns an assistant message with a `reasoning_content`
	// string field (extension to OpenAI Chat Completions wire) on
	// reasoning models like deepseek-v4-pro. Subsequent turns whose
	// assistant history is missing this field get rejected with
	// 400 ("The reasoning_content in the thinking mode must be
	// passed back to the API").
	//
	// Providers without reasoning_content support (vanilla OpenAI,
	// Anthropic, Gemini, claudecode) ignore this field. Empty when
	// the prior turn was a non-reasoning model or for non-assistant
	// roles.
	ReasoningContent string
}

ProviderMessage is a single exchange entry in provider-agnostic form.

For Role == "tool_result", both ToolCallID and ToolName must be set. ToolCallID is the call instance identifier the assistant emitted (used to correlate this result with that specific invocation). ToolName is the function-declaration name that was called (e.g. "send_chat") — some providers (Gemini's FunctionResponse) require it to be present alongside the call id, because their wire format keys responses by declaration name, not by call id. Providers that key only by call id (Anthropic, OpenAI, Ollama) ignore ToolName and the field can be left empty without harm.

For Role == "assistant", ToolCalls carries the structured tool_use blocks the model emitted on this turn. Providers that send assistant history back to the model (claude, openai, gemini, bedrock) MUST reconstruct these as native tool_use blocks; sending only Content as plain text loses the tool-call structure and breaks multi-turn tool conversations on strict providers (Bedrock rejects, Anthropic and OpenAI are lenient but degrade). Content and ToolCalls can both be non-empty — text and tool_use blocks coexist in one assistant turn.

type ProviderRequest

type ProviderRequest struct {
	AspectID           string
	AppendSystemPrompt string
	Session            SessionHandle // for subprocess-stream: resume key; for direct-api: may be empty
	Messages           []ProviderMessage
	Tools              []ToolDef
	ToolChoice         string           // see TurnRequest.ToolChoice
	MCP                *MCPClientConfig // nil = no MCP tools
	MaxSteps           int
	Model              string

	// Cwd is the working directory for subprocess-style providers (see
	// TurnRequest.Cwd). Empty falls through to bridle's host cwd. Direct-
	// API providers ignore this field.
	Cwd string

	// ProviderEnv is per-turn auth/routing env (see TurnRequest.ProviderEnv).
	// Subprocess providers overlay it onto the spawned process's env;
	// direct-API providers read it as auth/base-url config. Empty/nil =
	// provider uses its own default config.
	ProviderEnv map[string]string

	// ToolExecutor is harness-owned (bridle spec NEX-745 §4): the
	// harness's own ToolRunner-backed tool-execution pipeline, wired in
	// for subprocess-stream providers whose agentic loop runs mid-
	// RunTurn and therefore can't wait for run.go's per-round tool loop
	// to service a custom tool call (claudesdk today). Nil for every
	// other provider — direct-api providers already own the loop in
	// run.go and ignore this field; subprocess-stream providers that
	// don't support custom tools (claudecode) never receive one.
	ToolExecutor ToolExecutor

	// Sampling + output control — see TurnRequest field docs.
	// NEX-299 Pass 2. Providers honour what their wire format supports
	// and silently ignore the rest (e.g. Seed openai-only, TopK
	// claude-only).
	Temperature     *float64
	TopP            *float64
	TopK            *int
	Seed            *int
	MaxOutputTokens int
	StopSequences   []string
	ResponseFormat  *ResponseFormat

	// ThinkingBudgetTokens requests Anthropic extended-thinking with this token
	// budget. Anthropic requires it be >= 1024 and < the request's max_tokens.
	// 0 = unset/disabled (provider default, no thinking). Claude-only; other
	// providers ignore it, same as Seed/TopK precedent.
	ThinkingBudgetTokens int

	// Effort mirrors TurnRequest.Effort (see its doc comment for the
	// ladder). Providers translate it to their own reasoning-effort knob
	// where they have one, and silently drop it otherwise.
	Effort string

	// ToolCallStrictness mirrors TurnRequest.ToolCallStrictness so the
	// harness's post-provider tool-call contract step (NEX-581) can read
	// the per-aspect knob from the lowered request. Providers themselves
	// ignore it — the contract lives in run.go's round loop.
	ToolCallStrictness ToolCallStrictness
	// ContextPolicy is the lowered per-aspect context-window policy (the
	// context contract, NEX-581). Providers read ContextPolicy.TargetWindow
	// and map it to their engine knob in applyContextPolicy (ollama →
	// num_ctx); fixed-window providers no-op it. PromptBudget is enforced
	// at the harness seam, not here. Zero value = no policy.
	ContextPolicy ContextPolicy

	// SystemPromptMode selects how AppendSystemPrompt is applied: replace (replaces the
	// base prompt entirely, like --system-prompt); or append (the default).
	//
	// Default/append mode appends to bridle's built-in base prompt; replace mode replaces it.
	// Claude-code v2.1.177 supports both modes — `--system-prompt` / `--append-system-prompt`
	// with the same spill logic as AppendSystemPrompt. Field is zero value (default) = append.
	SystemPromptMode SystemPromptMode
}

ProviderRequest is the harness-internal lowered form of TurnRequest. System prompt is assembled, session tail is flattened to the provider's message format, tools are translated to provider-specific schema, and inbox items are folded in.

type ProviderResult

type ProviderResult struct {
	FinalText    string
	ToolCalls    []ToolInvocation
	StepCount    int
	Usage        Usage
	StopReason   StopReason
	SessionDelta []SessionEvent
	// ResolvedModel is the model id the upstream API actually returned
	// (e.g. "claude-3-5-sonnet-20241022"). May differ from
	// ProviderRequest.Model when per-turn ProviderEnv routed the call
	// elsewhere. Empty when the provider doesn't surface a model id.
	// Flows into TurnResult.ResolvedModel.
	ResolvedModel string
	// ThinkingBlocks (NEX-320) carries Claude extended-thinking content
	// blocks from this turn. The harness threads them into the next
	// turn's ProviderMessage so toClaudeMessages can re-emit them —
	// the Anthropic API requires they survive multi-turn round-trip.
	// Nil/empty when the provider doesn't support thinking mode OR
	// when this turn produced none.
	ThinkingBlocks []ThinkingBlock

	// ReasoningContent (NEX-340) is the openai-shape parallel of
	// ThinkingBlocks for DeepSeek reasoner-style models. The harness
	// threads this into the next IN-TURN step's reconstructed
	// assistant ProviderMessage (run.go tool-loop) so toOpenAIMessages
	// can re-emit it on step 2+. DeepSeek's API rejects in-turn
	// follow-up calls whose assistant{tool_call} reconstruction is
	// missing this field with 400 ("The reasoning_content in the
	// thinking mode must be passed back to the API"). Cross-turn
	// replay flows through SessionDelta instead — see SessionEvent.
	ReasoningContent string
}

ProviderResult is the harness-internal result from one provider turn step.

FinalText is the model's settled assistant text for this round — what downstream consumers (e.g. nexus funnel auto-post to chat) should treat as "what the model said." The harness concatenates FinalText across rounds for direct-API providers (see run.go), because each round is a separate, intentional deliberation.

Subprocess-stream providers that parse multi-event streams must decide what counts as "settled" before populating FinalText. Some models (Claude trained for claudecode) produce a draft → tool → final answer pattern within one subprocess run; the draft is exploratory and should NOT survive into FinalText, or auto-post emits a doubled "draft + rewrite" row (operator chat #951, harrow #944). claudecode handles this by resetting accumulated text on every tool_use, so FinalText ends up containing only post-last-tool text.

Other subprocess-stream providers (geminicli) don't currently apply the same heuristic — that's an explicit per-model judgement, not a missed fix. Revisit if a model exhibits the draft-rewrite pattern without this policy.

type RateLimit added in v0.2.0

type RateLimit struct {
	// Status is the provider's own coarse verdict: "allowed",
	// "allowed_warning", or "rejected" — cheaper for a caller to react to
	// than deriving a threshold from Utilization itself.
	Status string
	// WindowType names which window this reading is for — e.g.
	// "five_hour", "seven_day", "seven_day_opus", "seven_day_sonnet",
	// "seven_day_overage_included", "overage" — or "" when the provider
	// did not specify one.
	WindowType string
	// Utilization is 0-100. 0 covers both "just reset" and "not
	// reported" — the provider does not distinguish them either
	// (agora-spec-bridle's usage convention: a floor, not a
	// billing-grade count).
	Utilization int
	// ResetsAt is when this window resets; the zero Time when unknown.
	ResetsAt time.Time
	// UsingOverage is true once the session has started drawing on paid
	// overage credits rather than the plan's included allowance.
	UsingOverage bool
	TS           time.Time // stamped by the harness at emission; zero outside a harness turn
}

RateLimit fires when the provider signals a change in claude.ai subscription rate-limit state — window utilization or a transition into paid overage. Provider-specific: today only claudesdk emits it (a real claude.ai subscription session); API-key, Bedrock, Vertex and other providers never do, since plan limits do not apply to them. A caller must not treat its absence as "usage is fine" — it means "this provider has no concept of plan usage to report".

type ReasoningChunk added in v0.2.0

type ReasoningChunk struct {
	Text string
	TS   time.Time // stamped by the harness at emission; zero outside a harness turn
}

ReasoningChunk carries a streamed extended-thinking/reasoning-content fragment from the model — the reasoning_delta half of the agora-spec-bridle §2 event vocab (ModelChunk covers text_delta). Populated by claude's ThinkingDelta branch and openai's reasoning_content live-emit (both providers extracted this today but never streamed it before NEX-767 T7).

type ReasoningDeltaEvent added in v0.2.0

type ReasoningDeltaEvent struct{ Text string }

ReasoningDeltaEvent is Stream's reasoning_delta {s} event.

type Registry added in v0.2.0

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

Registry is bridle's facade over the static model catalog plus the caller-wired per-lane Harnesses. The catalog (registry_models.toml) NEVER constructs a Provider — Bind is the seam where deploy-specific credentials/base-URLs enter, wired by the caller once per lane.

func NewRegistry added in v0.2.0

func NewRegistry() *Registry

NewRegistry parses the embedded static catalog and returns an empty (unbound, alias-free) Registry. Panics if registry_models.toml itself fails to parse — see loadCatalog.

func (*Registry) Bind added in v0.2.0

func (r *Registry) Bind(lane string, h *Harness) error

Bind wires a credentialed Provider-backed Harness to a lane. The caller (agora/funnel deploy config) constructs the Harness with whatever creds/base-URL that lane's deploy needs; the registry never constructs Providers itself. Re-Binding a lane replaces its Harness.

func (*Registry) List added in v0.2.0

func (r *Registry) List() []ModelInfo

List returns every cataloged ModelInfo, sorted by lane then id for deterministic output (feeds the TUI %-picker / /model per agora-spec-bridle §1). Each returned ModelInfo is a deep copy (see copyModelInfo) — the catalog's ModelInfo values are backed by a package-level sync.Once slice shared across EVERY Registry instance in the process, so without copying, a caller mutating List()[i].Pricing.In (a pointer) or appending to List()[i].Aliases would corrupt the catalog for every other Registry too.

func (*Registry) RegisterAlias added in v0.2.0

func (r *Registry) RegisterAlias(alias, target string) error

RegisterAlias maps a bare alias onto an already-cataloged "lane/id" target. Errors if the target isn't cataloged — aliasing to nothing is a config bug caught at registration time, not resolution time.

For an identity-scoped override (agora's {identity}-interpolated alias form), use RegisterIdentityAlias instead — it takes alias and identity as SEPARATE parameters, keyed internally by a struct rather than a delimited "alias@identity" string, so it can't collide with a bare alias or another identity's override even when alias/identity values contain "@" (email-shaped identities).

func (*Registry) RegisterIdentityAlias added in v0.2.0

func (r *Registry) RegisterIdentityAlias(alias, identity, target string) error

RegisterIdentityAlias maps an identity-scoped alias override — the same alias name resolving differently per-identity (agora's {identity}-interpolated alias form) — onto an already-cataloged "lane/id" target. Keyed by the (alias, identity) STRUCT, not a delimited string, so it is collision-proof regardless of what characters alias or identity contain (see identityAliasKey). Errors if the target isn't cataloged, same as RegisterAlias.

func (*Registry) Resolve added in v0.2.0

func (r *Registry) Resolve(aliasOrID, identity string) (ModelHandle, error)

Resolve implements the two-phase alias cascade: the identity-scoped override -> the bare alias -> bare "lane/id" -> error. Unresolvable input errors HERE, at call time — never mid-turn (agora-spec-bridle §1). A cataloged model whose lane hasn't been Bound also errors here, for the same reason: resolving to a handle nothing can Stream against is exactly the kind of failure that must surface at session/run start.

func (*Registry) Stream added in v0.2.0

func (r *Registry) Stream(ctx context.Context, handle ModelHandle, req Request) (<-chan StreamEvent, error)

Stream drives one agora turn-engine request against handle's bound lane and returns a channel of normalized StreamEvents (agora-spec- bridle §2). Dispatch is by ProviderCategory: subprocess-stream lanes (claude-code) call the existing Harness.RunTurn unmodified (already single-shot for self-executing providers); direct-api lanes call the new additive Harness.RunStep (single round, no tool loop — agora owns tool execution). Cancellation via ctx aborts the upstream request (both RunTurn and RunStep respect ctx through the provider's own RunTurn call).

The channel is closed when the terminal event (done or error) has been sent — callers should range over it until closed rather than watching for a specific terminal event type.

type Request added in v0.2.0

type Request struct {
	Messages []RoleMessage
	Tools    []ToolDef

	// Effort is the agora reasoning-effort ladder value: low | medium |
	// high | xhigh | max (agora-spec-bridle §3), lowered into
	// ProviderRequest.Effort and translated per-provider (T2). Empty is
	// equivalent to unset (provider/model default).
	Effort string

	MaxTokens int

	// Structured requests schema-constrained output. T1/T7 threads the
	// field through but does not enforce it (see JSONSchema doc) — T4
	// follow-up work.
	Structured *JSONSchema

	// CacheHints marks the stable prefix (system + tools + skills
	// catalog) for provider-side prompt-cache control (agora-spec-bridle
	// §3). T1/T7 threads the field through but does not yet apply any
	// provider cache-control wiring from it — follow-up work.
	CacheHints []string

	// ProviderEnv is per-call auth/routing config, same contract as
	// TurnRequest.ProviderEnv / ProviderRequest.ProviderEnv.
	ProviderEnv map[string]string
}

Request is the agora-facing turn input for Registry.Stream (agora-spec-bridle §2): "{messages, tools, effort, max_tokens, structured, cache_hints}". bridle sees only the FINAL messages+tools for this request — history assembly, compaction, and session persistence are agora-side (agora-spec-bridle §4 non-requirements).

type ResponseFormat added in v0.2.0

type ResponseFormat struct {
	// Type is one of: "text" | "json_object" | "json_schema". Empty
	// defaults to "text" (provider default, free-form).
	Type string

	// Name identifies the schema for the "json_schema" type. OpenAI
	// requires it (max 64 chars, a-z/A-Z/0-9/_/-).
	Name string

	// Description is optional context for the model on what the
	// schema is for. Only used for "json_schema".
	Description string

	// Schema is a JSON Schema describing the expected response.
	// Required when Type == "json_schema". Ignored otherwise.
	Schema json.RawMessage

	// Strict, when Type == "json_schema", turns on OpenAI's strict
	// structured-outputs mode (response is GUARANTEED to match
	// schema). Recommended on for classifier paths where
	// parse-failure costs more than rejection.
	Strict bool
}

ResponseFormat constrains the model's output shape (NEX-299 Pass 2). Maps to OpenAI's response_format union. Claude has no equivalent API-level field today; providers that don't support it silently ignore — callers wanting cross-provider portability should also encode the shape in the system prompt.

type RoleMessage added in v0.2.0

type RoleMessage struct {
	Role    MessageRole
	Content string
}

RoleMessage is one message in a Stream Request, tagged with its abstract role. Ordering within a role is preserved (agora-spec-bridle §3).

type RoundTiming added in v0.2.0

type RoundTiming struct {
	AssemblySecs            float64 // request assembly + BeforeModelCall hooks
	StartupToFirstEventSecs float64 // provider call -> first sink event (CLI lane: spawn+startup+TTFT)
	StreamSecs              float64 // first event -> provider call return
	PromptBytes             int     // marshaled request messages size
	MessageCount            int
	ToolDefCount            int
}

RoundTiming captures where one provider round spent its time and what it sent. Secs floats (not Durations) so the struct marshals readably into TurnFrame JSON downstream.

type SessionEvent

type SessionEvent struct {
	Provider ProviderID  `json:"provider,omitempty"` // who produced this event
	Role     SessionRole `json:"role"`
	Content  string      `json:"content,omitempty"`
	// RawJSON carries provider-specific blocks (tool_use, tool_result, etc.)
	// that don't fit the plain content field. Valid only in conjunction with Provider.
	RawJSON json.RawMessage `json:"raw,omitempty"`
	// ThinkingBlocks (NEX-320 cross-turn) preserves Claude extended-
	// thinking blocks across Deliberate calls — within a single Run the
	// blocks survive via ProviderResult.ThinkingBlocks, but the funnel's
	// SessionTail re-lowering path (run.go lowerRequest) erases them
	// without this field. Anthropic API rejects multi-turn requests
	// whose assistant history is missing thinking blocks from prior
	// thinking-mode turns ("content[].thinking ... must be passed back").
	//
	// Attached to the FIRST assistant SessionEvent of each turn (text
	// or tool_use) — duplicating across all events in the same turn
	// would double-emit the blocks on the wire. lowerRequest carries
	// these onto the corresponding ProviderMessage.
	//
	// omitempty keeps existing JSONL session logs backward-compatible
	// (old entries decode with empty slice; cross-turn replay no-ops
	// when nil).
	ThinkingBlocks []ThinkingBlock `json:"thinking_blocks,omitempty"`

	// ReasoningContent (NEX-340) is the openai-shape parallel of
	// ThinkingBlocks for DeepSeek reasoner-style models. Same shape
	// of requirement: subsequent turns whose assistant history loses
	// the reasoning_content field get rejected with 400
	// ("The reasoning_content in the thinking mode must be passed
	// back to the API"). Attached to the FIRST assistant SessionEvent
	// of each turn so lowerRequest can re-emit on the corresponding
	// ProviderMessage. omitempty preserves back-compat for non-
	// reasoning sessions and non-openai-shape providers.
	ReasoningContent string `json:"reasoning_content,omitempty"`

	// ToolCallID links a tool-result SessionEvent (Role==RoleTool)
	// back to the assistant tool_use that produced it. Required for
	// cross-turn replay through SessionTail: providers that use call-
	// id correlation (Anthropic, OpenAI, Ollama) need this on the
	// reconstructed tool_result ProviderMessage. Empty for non-tool
	// events. Without it, lowerRequest can't pair tool_results with
	// their tool_calls and the API rejects the rebuilt history.
	ToolCallID string `json:"tool_call_id,omitempty"`
}

SessionEvent is a single entry in a session's event log. The harness consumes SessionTail on the way in and proposes SessionDelta on the way out.

type SessionHandle

type SessionHandle struct {
	ID  string // opaque to the funnel; meaningful to the provider
	New bool   // true on the first invocation for this ID; false on continuations
}

SessionHandle is an opaque reference to provider-side session state. The funnel mints handles and maps them to threads; the provider uses the ID to resume state (e.g., --resume <session-id> for subprocess-stream). For direct-api providers, Handle may be empty; state comes from SessionTail.

New tells the provider whether the funnel is initiating a fresh session for this ID (true) or asking it to continue an existing one (false). For subprocess-stream providers like claudecode that maintain their own jsonl files, this is the difference between "create with this id" vs "load existing id". Direct-api providers that derive state from SessionTail can ignore this field.

type SessionRole

type SessionRole string

SessionRole identifies who produced a session event.

const (
	RoleUser      SessionRole = "user"
	RoleAssistant SessionRole = "assistant"
	RoleTool      SessionRole = "tool"
	RoleSystem    SessionRole = "system"
)

type StepBoundary

type StepBoundary struct {
	Step int
	TS   time.Time // stamped by the harness at emission; zero outside a harness turn
}

StepBoundary fires between tool-call rounds. Step 1 = the first round; fires after its results are sent back to the model.

type StopReason

type StopReason string

StopReason explains why a turn ended.

const (
	StopReasonModelDone StopReason = "model_done"
	StopReasonMaxSteps  StopReason = "max_steps"
	StopReasonError     StopReason = "error"
	StopReasonAborted   StopReason = "aborted"
	// StopReasonProcessExit is set when the underlying provider process
	// exited non-zero AFTER producing parseable assistant content. The
	// returned ProviderResult carries whatever the model said before
	// the exit — callers should treat the result as truncated-but-real,
	// not discard it. Common cause: hitting an output-token cap and the
	// CLI surfacing that as a non-zero exit rather than a clean stop.
	StopReasonProcessExit StopReason = "process_exit"

	// StopReasonRefusal is a vocabulary value for a model-declined-to-
	// answer stop (agora-spec-bridle §2 done{stop_reason:refusal};
	// Anthropic's Messages API surfaces this as its own stop_reason on
	// Fable 5, HTTP 200). NEX-767 T1/T7 adds the CONST so Stream's
	// done{refusal} mapping (see stream.go's streamStopReason) exists
	// and is testable; no provider's wire-to-StopReason mapping
	// produces it yet — detecting the real wire signal per lane is T3
	// follow-up work (agora-spec-bridle §3's refusal-handling item).
	StopReasonRefusal StopReason = "refusal"
)

type StreamEvent added in v0.2.0

type StreamEvent interface {
	// contains filtered or unexported methods
}

StreamEvent is the closed union of events Registry.Stream emits on its returned channel — the normalized vocabulary agora-spec-bridle §2 specifies so agora never sees provider wire formats. Mirrors bridle's own Event interface pattern (events.go).

type StreamStopReason added in v0.2.0

type StreamStopReason string

StreamStopReason is the Stream done{} event's terminal-signal vocabulary (agora-spec-bridle §2: "done{stop_reason: end|tool_calls| max_tokens|refusal}"). tool_calls is a NEW terminal signal Stream introduces: bridle's own StopReason enum treats a tool_use round as StopReasonModelDone (non-terminal — RunTurn's loop manages it), so Stream derives StreamStopToolCalls from the round's ToolCalls being non-empty rather than from the underlying StopReason value.

const (
	StreamStopEnd       StreamStopReason = "end"
	StreamStopToolCalls StreamStopReason = "tool_calls"
	StreamStopMaxTokens StreamStopReason = "max_tokens"
	StreamStopRefusal   StreamStopReason = "refusal"
)

type StreamUsage added in v0.2.0

type StreamUsage struct {
	Input     int
	Output    int
	Cached    int
	Reasoning int
}

StreamUsage is the Stream usage{} event payload (agora-spec-bridle §2: "usage {input, output, cached, reasoning}").

type SystemPromptMode added in v0.2.0

type SystemPromptMode string

SystemPromptMode selects how AppendSystemPrompt is applied:

  • SystemPromptAppend (default): Append to bridle's built-in base prompt, like --append-system-prompt. The caller's composed system prompt extends the default framing.

  • SystemPromptReplace: Replace the entire base prompt with the caller's text, like --system-prompt. The default framing is completely hidden; only what the caller specifies appears in the turn. Useful for callers that want to own the system framing entirely and opt out of bridle's built-in rules (e.g., nexus.md).

Zero value = SystemPromptAppend (the default / append mode). Field is named so that its zero value has the safer behavior — opt-in for the override, not opt-out.

const (
	SystemPromptAppend  SystemPromptMode = "" // empty means "append" — keep the base prompt + extend it (default)
	SystemPromptReplace SystemPromptMode = "replace"

	// SystemPromptFull is the agora-spec-bridle §1 vocabulary alias for
	// SystemPromptReplace ("full|append" per the registry's
	// system_prompt_mode capability field). Callers that speak agora's
	// vocabulary can set this directly on TurnRequest/ProviderRequest;
	// call Normalize() before switching on the value so "full" and
	// "replace" are treated identically everywhere.
	SystemPromptFull SystemPromptMode = "full"
)

func (SystemPromptMode) IsValid added in v0.2.0

func (m SystemPromptMode) IsValid() bool

func (SystemPromptMode) Normalize added in v0.2.0

func (m SystemPromptMode) Normalize() SystemPromptMode

Normalize collapses the agora-vocabulary alias onto bridle's existing modes: SystemPromptFull becomes SystemPromptReplace; everything else passes through unchanged. Callers that switch on SystemPromptMode (e.g. claudecode's buildCLIArgs) should call Normalize() first so "full" and "replace" take the same branch without duplicating cases.

type TextDeltaEvent added in v0.2.0

type TextDeltaEvent struct{ Text string }

TextDeltaEvent is Stream's text_delta {s} event.

type ThinkingBlock added in v0.2.0

type ThinkingBlock struct {
	// Type is "thinking" or "redacted_thinking".
	Type string
	// Thinking is the plaintext reasoning content (Type=="thinking" only).
	Thinking string
	// Signature is the server-provided cryptographic signature; must
	// be sent back verbatim. (Type=="thinking" only.)
	Signature string
	// Data is the opaque encrypted payload (Type=="redacted_thinking"
	// only). Anthropic's safety filter swaps in a redacted block when
	// it judges the plaintext shouldn't reach the caller; the API
	// still requires it on round-trip.
	Data string
}

ThinkingBlock is one extended-thinking content block returned by Claude when thinking mode is enabled (NEX-320). Type discriminates "thinking" (plaintext + cryptographic signature) from "redacted_thinking" (opaque encrypted payload Anthropic's safety classifier may swap in). Both must be passed back verbatim on subsequent turns — the signature/data is what the API authenticates against to confirm the block originated from the prior server response.

type ToolCall

type ToolCall struct {
	ID   string
	Name string
	Args json.RawMessage
}

ToolCall is a single invocation the model requested.

type ToolCallEvent added in v0.2.0

type ToolCallEvent struct {
	ID       string
	Name     string
	ArgsJSON json.RawMessage
}

ToolCallEvent is Stream's tool_call {id, name, args_json} event — always a COMPLETE call (agora-spec-bridle §2: "bridle assembles streamed arg fragments"). Parallel calls are emitted in the order ProviderResult.ToolCalls records them, which is the order the provider returned them in.

type ToolCallRepaired added in v0.2.0

type ToolCallRepaired struct {
	// Stage names the contract action: "detected", "repaired", or "retried".
	Stage string
	// Detail is a short label (which token leaked, what repair did).
	Detail string
	TS     time.Time // stamped by the harness at emission; zero outside a harness turn
}

ToolCallRepaired is the observability event emitted when the tool-call contract detected a leak and acted on it (NEX-581). It mirrors the MCPServerFailed event pattern: lightweight, stamped via the timing sink, surfaced so the funnel can log how often engines misbehave.

Stage is one of the toolCallStage* constants. Detail is a short label of what tripped detection / what repair did.

type ToolCallResult

type ToolCallResult struct {
	ID     string
	Result json.RawMessage
	Err    string    // non-empty if the tool runner returned an error
	TS     time.Time // stamped by the harness at emission; zero outside a harness turn
}

ToolCallResult fires after the tool runner returns (or errors).

type ToolCallStart

type ToolCallStart struct {
	ID   string
	Name string
	Args json.RawMessage
	TS   time.Time // stamped by the harness at emission; zero outside a harness turn
}

ToolCallStart fires when the model requests a tool call, before execution.

type ToolCallStrictness added in v0.2.0

type ToolCallStrictness string

ToolCallStrictness controls how the tool-call contract reacts to a detected leak or an unparseable tool call (NEX-581). It is a per-request / per-aspect knob the funnel sets; the harness defaults to the repair-then-retry behavior when the field is empty.

The contract is the "feels dumb" insurance: whatever an engine emits, bridle delivers either a well-formed tool call OR a clean text turn. The strictness knob picks how hard bridle works to get there.

const (
	// ToolCallStrictnessRepairThenRetry is the default. On a detected
	// leak, bridle first attempts a structural repair (no model round
	// trip). If repair can't recover a clean result — and the turn was
	// supposed to call a tool, or the content is still garbled — bridle
	// retries the round ONCE with a tightened instruction. Only after
	// repair+retry exhaust does it surface the best-effort cleaned text,
	// flagged. Builders should use this: never ship a degraded tool call.
	ToolCallStrictnessRepairThenRetry ToolCallStrictness = "repair-then-retry"

	// ToolCallStrictnessTolerant accepts the structurally repaired result
	// without a retry round. Research aspects can use this: a cleaned
	// text turn is acceptable, and the extra round isn't worth the cost.
	ToolCallStrictnessTolerant ToolCallStrictness = "tolerant"
)

type ToolDef

type ToolDef struct {
	Name        string
	Description string
	// InputSchema is a JSON Schema object describing the expected arguments.
	InputSchema json.RawMessage
}

ToolDef describes a tool the model may call.

type ToolExecutor added in v0.2.0

type ToolExecutor interface {
	Execute(ctx context.Context, call ToolCall) (ToolResult, error)
}

ToolExecutor lets a subprocess-stream provider whose agentic loop runs mid-RunTurn (bridle spec NEX-745 §4 — claudesdk today) service a bridle-defined tool call the SAME way direct-api providers do: through the harness's own ToolRunner-backed pipeline, so OnBeforeToolCall / OnAfterToolCall fire with identical semantics and the call is visible on the event sink exactly like any other tool invocation.

The harness supplies this via ProviderRequest.ToolExecutor; it is nil for every provider that doesn't ask for it (every provider except claudesdk today — direct-api providers own their own tool loop in run.go and never read this field, subprocess-stream providers that don't support custom tools, e.g. claudecode, don't get one either).

Invariant (asserted by callers, not by this interface): every ToolCall id passed to Execute gets exactly one ToolResult or error before the provider's RunTurn returns.

type ToolInvocation

type ToolInvocation struct {
	ID     string
	Name   string
	Args   json.RawMessage
	Result json.RawMessage
	Err    string
}

ToolInvocation records a single tool call the model made.

type ToolResult added in v0.2.0

type ToolResult struct {
	Result json.RawMessage
	Err    string
}

ToolResult is what a ToolExecutor returns for one tool call: the tool_result payload, or an error string if the runner failed. Mirrors the shape ToolCallResult carries onto the event sink, minus the id/TS bookkeeping fields the harness owns.

type ToolRunner

type ToolRunner interface {
	Run(ctx context.Context, call ToolCall) (json.RawMessage, error)
}

ToolRunner executes tool calls on behalf of the harness. The funnel supplies the implementation; the harness never owns tools.

type ToolTiming added in v0.2.0

type ToolTiming struct {
	ID   string
	Name string
	Secs float64
}

ToolTiming is one tool call's wall-clock duration.

type TurnDone

type TurnDone struct {
	Result TurnResult
	TS     time.Time // stamped by the harness at emission; zero outside a harness turn
}

TurnDone fires after the turn completes successfully.

type TurnError

type TurnError struct {
	Err   error
	Stage TurnErrorStage
	TS    time.Time // stamped by the harness at emission; zero outside a harness turn
}

TurnError fires when the provider or harness hits a non-recoverable error. Never panics across the harness boundary.

Stage labels the pipeline location where the error surfaced — useful for log routing and dashboards. See TurnErrorStage for the enumerated values bridle emits; consumers MAY observe other strings (forwarded from wire, set by tests). Free-form is intentional.

type TurnErrorStage added in v0.1.3

type TurnErrorStage string

TurnErrorStage names a pipeline location that produced a TurnError. The underlying type is a string so consumers that just log it (or receive forwarded values from the wire) continue to work.

const (
	// TurnErrorStageHarnessRecover — panic trap inside Harness.RunTurn.
	TurnErrorStageHarnessRecover TurnErrorStage = "harness-recover"
	// TurnErrorStageProvider — provider.RunTurn returned a non-nil
	// error before producing a complete result.
	TurnErrorStageProvider TurnErrorStage = "provider"
	// TurnErrorStageRetry — a transient provider error is being
	// retried (claudecode); informational, not terminal.
	TurnErrorStageRetry TurnErrorStage = "retry"
	// TurnErrorStageProviderAPIError — claudecode stream-json reported
	// is_api_error=true; the run continues but the kind is surfaced.
	TurnErrorStageProviderAPIError TurnErrorStage = "provider_api_error"
	// TurnErrorStageSubprocessExit — a subprocess-stream provider
	// exited non-zero and the classifier had no better label.
	TurnErrorStageSubprocessExit TurnErrorStage = "subprocess_exit"
	// TurnErrorStageSubprocessExitPartial — subprocess exited non-zero
	// AFTER producing parseable assistant content; the partial result
	// is preserved with StopReason=process_exit.
	TurnErrorStageSubprocessExitPartial TurnErrorStage = "subprocess_exit_partial"
	// TurnErrorStageStderrOutput — subprocess wrote non-empty stderr
	// on a clean exit; surfaced as a warning, not a failure.
	TurnErrorStageStderrOutput TurnErrorStage = "stderr_output"
	// TurnErrorStageStreamTruncated — the provider's event stream
	// ended without a terminal result event.
	TurnErrorStageStreamTruncated TurnErrorStage = "stream_truncated"
	// TurnErrorStageResumeFallback — a session resume failed because the
	// referenced session was missing/corrupt, and the provider fell back
	// to a fresh session. Informational/warning, not terminal: the turn
	// proceeds without the prior session's context. Distinct from
	// TurnErrorStageRetry (same session, transient error).
	TurnErrorStageResumeFallback TurnErrorStage = "resume_fallback"
)

type TurnRequest

type TurnRequest struct {
	// Identity & framing
	AspectID           string           // who's running (cost/triage/identity attribution)
	AppendSystemPrompt string           // composed by funnel: NEXUS.md + SOUL.md + PRIMER + harness rules
	SystemPromptMode   SystemPromptMode // how AppendSystemPrompt is applied: append (default, zero value) extends claude-code's base prompt; replace swaps it entirely
	Session            SessionHandle    // opaque handle for provider-side state (subprocess-stream: resume key)
	SessionTail        []SessionEvent   // recent events for direct-api providers to lower into the request

	// This turn
	UserMessage string      // the prompt that opens this turn (may be empty for autonomous)
	Inbox       []InboxItem // mid-turn comms accumulated since last turn

	// Tool surface
	Tools []ToolDef        // explicit in-process tool defs
	MCP   *MCPClientConfig // MCP-loaded tools; nil = no MCP tools this turn

	// Provider
	Provider ProviderID // claude-api | openai-api | bedrock | gemini-api | ollama-local | claude-code | gemini-cli | codex-cli | claude-pty
	Model    string     // REQUIRED — provider-specific model id; RunTurn returns ErrModelRequired if empty
	MaxSteps int        // hard cap on tool-call rounds; 0 = unlimited

	// ToolChoice optionally constrains how the model picks tools.
	// Empty string → provider default (typically "auto").
	// "auto" → model decides whether to call a tool.
	// "any" → model must call exactly one tool, free choice of which.
	// "none" → no tools may be called this turn (text only).
	// Any other value → name of a specific tool that must be called.
	// Not all providers honour all values; unsupported values fall back to "auto".
	ToolChoice string

	// Sampling controls (NEX-299 Pass 2). Pointer types so "unset" is
	// distinguishable from "explicitly zero" — providers fall through
	// to their own default when nil.
	//
	//   Temperature: lower = more deterministic. 0 for classifier
	//                tasks (cheap judge); higher for creative.
	//   TopP:        nucleus sampling. Standard across providers.
	//   TopK:        claude-only; openai silently ignores.
	//   Seed:        openai-only deterministic sampling seed;
	//                claude silently ignores. Pair with Temperature=0
	//                for full reproducibility.
	Temperature *float64
	TopP        *float64
	TopK        *int
	Seed        *int

	// ThinkingBudgetTokens requests Anthropic extended-thinking with this token
	// budget. Anthropic requires it be >= 1024 and < the request's max_tokens.
	// 0 = unset/disabled (provider default, no thinking). Claude-only; other
	// providers ignore it, same as Seed/TopK precedent.
	ThinkingBudgetTokens int

	// Effort is the agora reasoning-effort ladder value: low | medium |
	// high | xhigh | max (agora-spec-bridle §3). Empty = provider
	// default (no translation attempted). Providers that can't express
	// a tier at all drop it silently, same as Seed/TopK precedent.
	Effort string

	// MaxOutputTokens caps generation length. 0 = provider default
	// (claude internally falls back to 4096; openai uses its own
	// account-level default). Set non-zero for cost-bounded paths
	// like cheap-judge classifier where verdicts are tiny.
	MaxOutputTokens int

	// StopSequences halt generation on first match. Maps to openai
	// `stop` and claude `stop_sequences`. Empty = no stop sequences.
	StopSequences []string

	// ResponseFormat constrains the model's output shape — most
	// usefully for json_schema strict mode, which guarantees the
	// response matches Schema. Providers that don't support this
	// (claude as of writing) silently ignore. Callers wanting
	// portability should also encode schema requirements in the
	// system prompt. Nil = free-form text (provider default).
	ResponseFormat *ResponseFormat

	// ToolCallStrictness is the per-aspect tool-call contract knob
	// (NEX-581). It controls how hard bridle works to deliver a clean
	// tool call or clean text when an engine leaks raw protocol tokens.
	// Empty = repair-then-retry (the default): builders should keep it
	// strict and never ship a degraded tool call; research aspects can
	// set "tolerant" to accept structurally-repaired text without a
	// retry round.
	ToolCallStrictness ToolCallStrictness

	// Cwd is the working directory for subprocess-style providers.
	// Empty falls through to the bridle host process's cwd. Per-request
	// rather than per-Harness because different aspects sharing one
	// Harness need distinct cwds. For example, claude-code derives its
	// session jsonl path AND its .mcp.json discovery from cwd, so two
	// aspects with the same Harness but overlapping cwds collide
	// sessions and leak MCP identity from one into the other. Codex CLI
	// receives the same value as both process cwd and `codex --cd`.
	// Direct-API providers ignore this field — they have no subprocess
	// to anchor.
	Cwd string

	// ProviderEnv is per-call environment for the provider. Direct-API
	// providers read it as their auth/routing config (commonly
	// ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, OPENAI_API_KEY,
	// OPENAI_BASE_URL); subprocess providers propagate it into the
	// spawned process's env so the same per-turn override pattern
	// applies. nil/empty = use whatever the provider already has on its
	// own (process env, --bare-style flags, etc).
	//
	// Per-call rather than per-process so a single funnel can mix
	// credentials across turns — e.g. main turn against the operator's
	// Anthropic credit pool, judge turn against a DeepSeek-via-
	// Anthropic-shape credential, eval turn against OpenAI. The
	// credential store wires this from aspects.default_*_credential
	// per task #218.
	ProviderEnv map[string]string

	// ContextPolicy is the per-aspect context-window policy (the context
	// contract, NEX-581): a desired window (TargetWindow) and a soft
	// prompt-size budget (PromptBudget), expressed once here and mapped
	// by each provider to its engine knob — or warned on engine-
	// agnostically. Zero value = no policy (engine defaults, no budget
	// warning). See ContextPolicy.
	ContextPolicy ContextPolicy
}

TurnRequest is the complete input for one deliberation turn.

type TurnResult

type TurnResult struct {
	FinalText     string           // model's last assistant text (may be empty for tool-only turns)
	ToolCalls     []ToolInvocation // ordered list of what the model actually did
	StepCount     int
	Usage         Usage
	StopReason    StopReason
	ResolvedModel string         // model id the upstream API reported; empty when unknown
	SessionDelta  []SessionEvent // events to propose to the funnel-owned JSONL
	Timing        TurnTiming     // per-turn timing instrumentation; zero value = not recorded
}

TurnResult is the structured outcome of a completed turn.

ResolvedModel is the model identifier the upstream API actually returned (Anthropic Messages.Model, OpenAI ChatCompletion.Model, claudecode result-event model, etc.). It can differ from TurnRequest.Model when per-turn ProviderEnv routes the call to a different backend — operator pool's Claude credit vs. DeepSeek-via- Anthropic-shape credential vs. OpenAI. Empty when the provider doesn't surface a model id. Callers attributing usage/cost/identity should prefer ResolvedModel when non-empty, fallback to TurnRequest.Model.

type TurnTiming added in v0.2.0

type TurnTiming struct {
	Rounds    []RoundTiming
	Tools     []ToolTiming
	TotalSecs float64
}

TurnTiming aggregates per-turn instrumentation. Zero value = not recorded.

type Usage

type Usage struct {
	InputTokens              int
	OutputTokens             int
	CacheReadInputTokens     int     // Anthropic prompt-cache hit count
	CacheCreationInputTokens int     // tokens written into the prompt cache this turn
	CostUSD                  float64 // provider-reported or estimated; 0 if unknown

	// Estimated is set true when the token counts in this Usage were
	// NOT reported by the engine — they were estimated by bridle's
	// tokenizer as a last-resort floor (the usage contract, NEX-581).
	// A provider that reports real usage leaves this false. The flag
	// rides through addUsage: if ANY round of a turn was estimated, the
	// turn total is flagged Estimated. Consumers (cost accounting) can
	// treat estimated counts as approximate. The guarantee is that a
	// completed turn never has silently-zero usage — it has real
	// counts, or a flagged estimate, never nothing.
	Estimated bool

	// ReasoningTokens is the count of extended-thinking/reasoning
	// tokens the provider billed for this round, when it reports the
	// breakdown separately from OutputTokens (agora-spec-bridle §2
	// usage{input, output, cached, reasoning}). Additive field; 0 for
	// providers/rounds that don't report it (not necessarily "no
	// reasoning happened" — it may just be folded into OutputTokens
	// instead, e.g. Anthropic bills thinking tokens as output tokens
	// today).
	ReasoningTokens int
}

Usage holds token and cost data for a turn.

InputTokens is the count of UNCACHED prompt tokens billed at full rate. CacheReadInputTokens and CacheCreationInputTokens surface claude-api's prompt-caching behavior — the former is read at a discount, the latter is the new content being added to cache. Cache fields are zero for providers that don't expose caching (or don't run a cache-eligible request).

Sum (InputTokens + CacheReadInputTokens + CacheCreationInputTokens) approximates the total prompt size the model received. Use that for context-fullness reasoning; use InputTokens alone for billing estimates of fresh content.

type UsageEvent added in v0.2.0

type UsageEvent struct{ Usage StreamUsage }

UsageEvent is Stream's usage {input, output, cached, reasoning} event — final, per request.

type Warning added in v0.2.0

type Warning struct {
	Kind    string
	Message string
	TS      time.Time // stamped by the harness at emission; zero outside a harness turn
}

Warning fires for a non-fatal, once-per-session-worthy condition the harness or a provider wants to surface without failing the turn — e.g. an unsupported effort tier/knob falling back to a default (agora-spec-bridle §3: "Unsupported tier or knob → drop with a warning event once per session"). bridle itself is stateless and emits every time it hits the condition; dedup (once-per-session) is the consumer's job (agora's TUI, per the blueprint's open question 2).

type WarningEvent added in v0.2.0

type WarningEvent struct {
	Kind    string
	Message string
}

WarningEvent surfaces a bridle.Warning on the Stream channel (not part of agora-spec-bridle §2's core vocabulary list, but Warning events fire mid-stream per §3's effort-translation fallback and would otherwise be silently dropped for Stream consumers).

Directories

Path Synopsis
ctxmap
adapter
Package bridleadapter attaches a ctxmap memory engine to a bridle Harness using only bridle's existing hook seams — no bridle core changes:
Package bridleadapter attaches a ctxmap memory engine to a bridle Harness using only bridle's existing hook seams — no bridle core changes:
distill
Package distill is the in-harness distiller tier: the small local model compresses large tool results BEFORE they reach the harnessed (expensive, remote) model, so that model never spends tokens on raw file dumps, command output, or error walls — the noise that degrades a long agentic context.
Package distill is the in-harness distiller tier: the small local model compresses large tool results BEFORE they reach the harnessed (expensive, remote) model, so that model never spends tokens on raw file dumps, command output, or error walls — the noise that degrades a long agentic context.
embed
Package embed defines the sentence-embedding seam for the reconciler.
Package embed defines the sentence-embedding seam for the reconciler.
extractor
Package extractor defines the fact-extraction seam of ctxmap: proposal types and the pair-verdict vocabulary.
Package extractor defines the fact-extraction seam of ctxmap: proposal types and the pair-verdict vocabulary.
memory
Package memory is the ctxmap engine: the harness-agnostic core that a host harness (bridle, the agora research harness, anything with a turn loop) drives.
Package memory is the ctxmap engine: the harness-agnostic core that a host harness (bridle, the agora research harness, anything with a turn loop) drives.
render
Package render turns store facts into the prompt text blocks (spec §6).
Package render turns store facts into the prompt text blocks (spec §6).
store
Package store is the ctxmap fact store: SQLite-backed, provenance-mandatory.
Package store is the ctxmap fact store: SQLite-backed, provenance-mandatory.
Package fake provides scripted test doubles for the bridle harness.
Package fake provides scripted test doubles for the bridle harness.
internal
mcpclient
Package mcpclient wraps mark3labs/mcp-go to provide the bridle-internal MCP client used by direct-api providers.
Package mcpclient wraps mark3labs/mcp-go to provide the bridle-internal MCP client used by direct-api providers.
normalize
Package normalize provides helpers for mapping provider-specific wire values to bridle's canonical StopReason values.
Package normalize provides helpers for mapping provider-specific wire values to bridle's canonical StopReason values.
subprocess
Package subprocess holds plumbing shared by the subprocess-stream providers (claudecode, codexcli, geminicli): the cancel watcher, env merging, prompt extraction, JSONL stream scanning, and stderr error classification.
Package subprocess holds plumbing shared by the subprocess-stream providers (claudecode, codexcli, geminicli): the cancel watcher, env merging, prompt extraction, JSONL stream scanning, and stderr error classification.
version
Package version holds the build-time version string for any binaries in this repo (today: just stubfunnel; future cmd helpers wire through here).
Package version holds the build-time version string for any binaries in this repo (today: just stubfunnel; future cmd helpers wire through here).
provider
antigravitycli
Package antigravitycli implements the bridle Provider interface using the Antigravity CLI (agy) in headless mode (agy -p).
Package antigravitycli implements the bridle Provider interface using the Antigravity CLI (agy) in headless mode (agy -p).
bedrock
Package bedrock implements the bridle Provider interface for AWS Bedrock using the cross-model Converse API.
Package bedrock implements the bridle Provider interface for AWS Bedrock using the cross-model Converse API.
claude
Package claude implements the bridle Provider interface for the Anthropic Claude API.
Package claude implements the bridle Provider interface for the Anthropic Claude API.
claudecode
Package claudecode implements the bridle Provider interface using the claude-code CLI in headless mode (claude -p --output-format stream-json --verbose).
Package claudecode implements the bridle Provider interface using the claude-code CLI in headless mode (claude -p --output-format stream-json --verbose).
claudepty
Package claudepty implements the bridle Provider interface by spawning acp-claude-pty as a stdio subprocess and speaking ACP to it.
Package claudepty implements the bridle Provider interface by spawning acp-claude-pty as a stdio subprocess and speaking ACP to it.
claudesdk
Package claudesdk implements the bridle Provider interface via a thin TypeScript sidecar (bridle-claude-sidecar) that wraps the official @anthropic-ai/claude-agent-sdk over stdio JSON-lines.
Package claudesdk implements the bridle Provider interface via a thin TypeScript sidecar (bridle-claude-sidecar) that wraps the official @anthropic-ai/claude-agent-sdk over stdio JSON-lines.
codexcli
Package codexcli implements the bridle Provider interface using the Codex CLI in non-interactive mode (`codex exec --json`).
Package codexcli implements the bridle Provider interface using the Codex CLI in non-interactive mode (`codex exec --json`).
gemini
Package gemini implements the bridle Provider interface for the Google Gemini API (Gemini Developer API or Vertex AI), via google.golang.org/genai.
Package gemini implements the bridle Provider interface for the Google Gemini API (Gemini Developer API or Vertex AI), via google.golang.org/genai.
geminicli
Package geminicli implements the bridle Provider interface using the gemini CLI in headless mode (gemini -p --output-format stream-json -y).
Package geminicli implements the bridle Provider interface using the gemini CLI in headless mode (gemini -p --output-format stream-json -y).
ollama
Package ollama implements the bridle Provider interface for a local Ollama server.
Package ollama implements the bridle Provider interface for a local Ollama server.
openai
Package openai implements the bridle Provider interface for the OpenAI API.
Package openai implements the bridle Provider interface for the OpenAI API.
Package stubfunnel is a temporary validation harness for bridle v0.1/patch1.
Package stubfunnel is a temporary validation harness for bridle v0.1/patch1.
Package wset is working-set retention: a context-eviction policy for long agentic sessions that works WITH the trained re-verify-via-tools tendency of modern coding models instead of against it.
Package wset is working-set retention: a context-eviction policy for long agentic sessions that works WITH the trained re-verify-via-tools tendency of modern coding models instead of against it.

Jump to

Keyboard shortcuts

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