Documentation
¶
Overview ¶
Builds the streaming provider.Request from the active model selection and the session's text conversation history.
Bounded, classification-driven auto-retry of transient provider-round failures: the per-turn retry loop around a single provider round, the full-jitter exponential backoff and Retry-After honoring, and the injectable clock/sleep seams that keep backoff, budget, and cancellation testable without wall-clock sleeps. Handoff retries stop before dispatch begins, so completed tool dispatch is never replayed.
Development-only suggest_followup trace: the turn loop's seams call appendSuggestFollowupDebug to record how far an intercepted-followup attempt got, gated by Deps.SuggestFollowupDebug. It complements the TUI-side ghost stages emitted from internal/tui so one session log shows the whole suggest_followup path.
Recognizes a tool call a small/open model emits as assistant text (a JSON object, optionally inside one code fence) instead of as a structured tool call, and turns it into the synthetic streamed tool-call delta the agentic loop already assembles and dispatches (docs/ibm-model-issues.md).
Auto-title helper: asks an injected provider client for a short session title from the opening exchange, building its own narrow streaming request rather than reusing the full turn request builder.
Agentic tool-call iteration for one assistant turn: assemble the streamed tool-call deltas the stream loop hands off, persist the assistant tool-call message, schedule and dispatch the calls through the scheduler and an injected dispatcher seam, persist the tool results, and re-issue the turn until the model returns a final no-tool-call answer.
Package turn runs a single cancellable streamed provider turn: it turns a submitted prompt into a streamed model reply, emits the turn events, records the assistant message, and exposes a cancel handle. Its collaborators (provider client, delta sink, flush hooks, tool-call hook) are injected so the package stays free of internal/tui and internal/bootstrap (no import cycle); bootstrap wires the production bindings.
Index ¶
- Variables
- func BuildCompactionSummaryRequest(sel model.Selection, records []sessions.Record) provider.Request
- func BuildRequest(active *model.ActiveState, store *sessions.Store, systemPrompt string, ...) (req provider.Request, sel model.Selection, source model.Source, ok bool)
- func GenerateTitle(ctx context.Context, client provider.Client, sel model.Selection, ...) string
- func ToolActivityLines(calls []AssembledToolCall) []string
- type AssembledToolCall
- type Deps
- type DispatchFailure
- type FinalFlushError
- type FlushHook
- type IgnoredProviders
- type MoveTarget
- type PendingUserMessage
- type PlannedToolCall
- type RetryPolicy
- type RoundReport
- type Runner
- type Sink
- type StreamIdleInfo
- type ToolCallHandoff
- type ToolCallHook
- type ToolDispatcher
- type ToolLoop
- func (l *ToolLoop) AddUserMessage(text string, parts []sessions.ContentPart) bool
- func (l *ToolLoop) Cancel() (retainedChars int, inFlight bool)
- func (l *ToolLoop) Denied() bool
- func (l *ToolLoop) InFlight() bool
- func (l *ToolLoop) Run(ctx context.Context) error
- func (l *ToolLoop) SetDeadline(deadline time.Time)
- func (l *ToolLoop) TimeLimitReached() bool
- func (l *ToolLoop) Unsent() []PendingUserMessage
- type ToolPlan
- type ToolResult
Constants ¶
This section is empty.
Variables ¶
var ErrNoModelSelected = errors.New("turn: no model selected; refuse the turn")
ErrNoModelSelected is returned by Run when no model is selected: the turn is refused before any provider request or turn event is issued.
Functions ¶
func BuildRequest ¶
func BuildRequest(active *model.ActiveState, store *sessions.Store, systemPrompt string, tools []provider.ToolDefinition) (req provider.Request, sel model.Selection, source model.Source, ok bool)
BuildRequest builds the streaming provider.Request for the active model selection and the session's text conversation history. systemPrompt is the assembled system message — the active mode's base prompt, the environment block, and the loaded AGENTS.md instructions: when non-empty it is prepended as a single synthetic "system" message before the conversation history so each request leads with it. An empty systemPrompt adds no message — defensive only, since production assembly always yields the non-empty base + environment sections. ok is false when no model is selected (ActiveState.Current reports not-selected); the caller must then refuse the turn without issuing a provider request, and req/sel/source are zero. Effort is deliberately left unset here: the caller resolves the supported-effort filter via model.EmitTurnStart and sets the filtered value on the returned request.
tools is the turn's already-assembled, mode-filtered advertised tool set: the caller snapshots it once per user turn and passes the same set on every provider round (initial and tool-call continuation) so the advertised set is stable across the turn while a /mode switch only affects the next turn. It is copied onto the request as is; an empty set leaves provider.Request.Tools empty so ChatCompletion omits the `tools`/`tool_choice` fields.
func GenerateTitle ¶
func GenerateTitle(ctx context.Context, client provider.Client, sel model.Selection, firstUser, firstAssistant string, supportedEffort func(providerID, modelID string) []string) string
GenerateTitle asks client for a short title summarizing the opening exchange (firstUser, plus firstAssistant when non-empty) for the active model sel. It builds a narrow streaming request, since some providers refuse non-streaming ones — no tools, no system prompt or history beyond the title instruction — requesting low effort only when the model supports it. It returns "" on a request or stream error, a cancelled context, a missing stream, or a blank candidate, and never a partial title. Title generation is metadata, so it emits no conversation records and writes nothing.
A nil supportedEffort lookup exposes only no-reasoning.
func ToolActivityLines ¶
func ToolActivityLines(calls []AssembledToolCall) []string
Types ¶
type AssembledToolCall ¶
AssembledToolCall is one complete tool call reconstructed from the streamed per-chunk tool-call fragments the stream loop collects in ToolCallHandoff.ToolCallDeltas. ID identifies the call for tool_call_id correlation; Name is the function name; Arguments is the raw JSON-encoded arguments string exactly as the model produced it.
type Deps ¶
type Deps struct {
Active *model.ActiveState
Store *sessions.Store
Client provider.Client
Sink Sink
Flush FlushHook
FlushUnsupported FlushHook
OnToolCalls ToolCallHook
TurnID string
// A nil SupportedEffort lookup exposes only no-reasoning.
SupportedEffort func(providerID, modelID string) []string
// SuggestFollowup routes an intercepted suggest_followup tool call to the
// TUI: label is the short imperative shown as Tab-to-accept ghost text and prompt is
// the text prefilled when the user accepts. The agentic loop fires it instead of
// dispatching or persisting that call, so the suggestion is purely ephemeral UI state
// and never reaches the session record or transcript. Optional and nil-safe: a
// standalone Runner or a test that does not wire it simply drops the suggestion (the
// call is still omitted from the records).
SuggestFollowup func(label, prompt string)
// OnToolActivity surfaces a tool-call round's about-to-run calls as live, inline
// transcript lines, one formatted line per call in assembled order (see
// ToolActivityLines). The agentic loop fires it once per tool-call round, after the
// calls are assembled and before they are scheduled/dispatched, so the user sees what
// is about to run — and a denied call still shows its line. It is derived purely from
// the already-assembled calls, so it costs no extra provider tokens. Optional and
// nil-safe: a standalone Runner or a test that does not wire it simply emits nothing.
OnToolActivity func(lines []string)
// SealPartial commits a buffered pre-tool reply that OnToolActivity left unsealed. The loop
// calls it after dispatch unless the time limit cancelled the round, which rolls it back.
SealPartial func()
OnEditPatch func(patch string)
// Progress reports the cumulative retained assistant-content rune count as the
// reply streams — an approximate in-flight progress signal, not exact tokens
// (stream usage only arrives in the final chunk). It remains a retained-rune
// callback for tests and any other consumer, but production no longer feeds it to
// the token indicator: that indicator is wired to exact per-round Usage only,
// because retained runes routinely exceed the round's exact output tokens and feeding
// them in would make the ↓ figure climb then drop (see Usage / UsageAcc). May be nil.
// It is called in arrival order on the single stream goroutine, like Sink. In an
// agentic turn ToolLoop wraps this so the reported count is cumulative across provider
// rounds (prior rounds' retained chars plus the current round's), keeping the live
// signal from shrinking when a fresh per-round Runner resets its own count to zero.
Progress func(retainedChars int)
// Usage reports the exact provider token usage as cumulative input/output totals,
// feeding the in-flight indicator's exact figure. It fires once per
// successful round under a ToolLoop and once total for a bare Runner: under a
// ToolLoop (UsageAcc != nil) the loop owns emission and fires it after every round
// with the running turn total, so the ↑ input figure reaches each completed round's
// real prompt cost immediately rather than only at turn end; a bare Runner
// (UsageAcc == nil) fires it once on the natural non-tool completion that records
// model_response. May be nil; it is not called when no usage chunk arrived in any
// round (an interrupted or usage-missing turn). The count it reports is the summed
// turn total (see UsageAcc), so it stays consistent with /status.
Usage func(inputTokens, outputTokens int64)
// UsageAcc accumulates the exact stream usage of every provider round in one
// agentic turn so the final model_response and the Usage indicator report the
// summed turn total rather than only the last round. ToolLoop owns one
// per turn and threads the same pointer through every round's Deps; a standalone
// Runner (no tool-call iteration) leaves it nil and records only its own round.
UsageAcc *model.UsageAccumulator
// SubmitStart is the wall-clock instant the user's input for this turn was
// accepted at the pre-turn boundary (sessionturn.Persister), threaded through
// bootstrap so the final provider round can record one end-to-end turn_latency
// event (user submit → final answer) for the whole agentic turn. The same
// value is carried onto every round's Deps, but only the final no-tool answer
// records it, so a multi-round turn yields exactly one turn_latency. The zero
// value means "not wired" (a standalone Runner or a test that does not exercise
// turn latency), in which case no turn_latency is recorded.
SubmitStart time.Time
// SystemPrompt is the assembled system message — the active mode's base prompt,
// the environment block, and the loaded AGENTS.md instructions. The bootstrap turn
// wiring assembles it per dispatch and passes a stable snapshot here, so every
// provider round in one turn uses the same prompt while a later /mode switch is
// reflected on the next turn. When non-empty it leads each provider request as a
// single synthetic system message before the conversation history; empty adds no
// message.
SystemPrompt string
// Tools is the turn's mode-filtered advertised tool set, assembled once by
// the bootstrap turn wiring per dispatch from the snapshotted active mode and the
// realized registry plus the product-authored `bash` definition. It is threaded
// onto every provider round's request through BuildRequest, so the tool-call
// continuation rounds advertise the same set as the initial round and a /mode switch
// only affects the next user turn. Empty (the default) advertises no tools, so the
// request omits the `tools`/`tool_choice` fields.
Tools []provider.ToolDefinition
// SuggestFollowupDebug gates the development-only suggest_followup trace: when
// true the turn loop appends ordered suggest_followup_debug events (advertised /
// no-call / tool-call-seen / intercepted) to Store so a developer can see how far a
// suggestion got. Off by default (false), so a normal turn records none of them. It
// is append-only and best-effort — a missing diagnostic never affects the turn.
SuggestFollowupDebug bool
// Mode is the active interaction-mode key (plan/build/auto_edits) for this turn,
// stamped onto the suggest_followup debug events alongside the provider/model from Active. It is
// captured by bootstrap at dispatch (the turn package has no mode concept) and is only
// read when SuggestFollowupDebug is on; empty otherwise.
Mode string
// Notice reports a provider/turn request failure as user-facing text (the rendered
// failure line — for an HTTP error the friendly message, optionally with
// the remediation hint line). It is called only
// from the provider-failure paths of Run. The production binding commits it
// as an in-place transcript line tied to the turn (App.CommitProviderError,
// marshaled through the tview queue so it is safe from the background turn
// goroutine) rather than a bottom-pinned notice, so a stale provider error can no
// longer re-render below a later turn. Session-write failures take the
// OnFailureWrite path instead and stay on the transient notice surface.
Notice func(text string)
ToolNotice func(text string)
// RenderNotice renders a provider-failure notice: line 1 is the caller-supplied
// message (for an HTTP error, the friendly line from FriendlyHTTPMessage;
// for transport/body failures, the synthetic surface), with the remediation hint
// appended as line 2 when the (phase, reason, httpStatus) tuple matches the hint
// table. The production binding is tui.RenderProviderRequestFailedNotice; injecting
// it keeps this package free of internal/tui while still using the TUI's hint logic.
RenderNotice func(verbatim, phase, reason string, httpStatus int) string
// RenderRetryNotice renders the terminal provider-failure notice after auto-retry
// gives up, enriching the normal notice with the number of attempts that were made
// while preserving the provider-specific hint behavior. The production binding is
// tui.RenderProviderRequestFailedAfterAttemptsNotice. Nil defaults to the normal
// renderer with a generic "failed after N attempts" prefix on line 1.
RenderRetryNotice func(verbatim, phase, reason string, httpStatus, attempts int) string
// Interrupt seals the in-progress transcript reply as interrupted. It is
// called on a real mid-stream transport drop, and on a user interrupt only
// when the reply was still a held text-tool-call candidate that never reached the
// Sink — so the ESC/CTRL-C path's own seal found no open stream to close. On an
// ordinary user interrupt whose content already streamed, that path seals the reply
// itself and this hook is not called.
Interrupt func()
// Finalize commits a naturally completed reply into the transcript so the
// next turn starts a fresh buffer. It is called only on a successful non-tool
// stream, after the assistant message and model_response are recorded.
Finalize func()
// FinalizeErr reports a failure to deliver what Finalize committed.
FinalizeErr func() error
// OnFailureWrite routes a real atomic-write failure from the
// provider_request_failed flush through the per-turn write-failure
// protocol (sessionfailures.HandleWriteFailure). It may be nil.
OnFailureWrite events.WriteFailureHandler
// Retry is the auto-retry policy for the turn's provider rounds. The
// zero value (Enabled false) is the no-retry behavior: every provider
// failure surfaces once, exactly as before. New applies the per-turn budget
// default when an enabled policy leaves it unset. Production threads it from the
// provider.retry settings block via bootstrap.
Retry RetryPolicy
// RetryBackoff reports that the next retry attempt has been scheduled and the
// retry loop is entering backoff. nextAttempt is 1-based and names the provider
// attempt that will run after the wait; totalAttempts is Retry.MaxAttempts. The
// TUI uses it for the transient "Reconnecting..." indicator. Optional and nil-safe.
RetryBackoff func(nextAttempt, totalAttempts int)
// RetryResume reports that backoff completed and the provider round is about to be
// re-issued, so the TUI can return the activity indicator to its normal working
// state. Optional and nil-safe.
RetryResume func()
// RetryRecovered reports that a provider round completed successfully after one or
// more scheduled retries. retries is the number of retries that were actually
// issued, not the total attempt count. Optional and nil-safe.
RetryRecovered func(retries int)
// RollbackPartial removes the un-sealed in-progress reply line before a
// discard-and-restart retry of a partial provider round, so the
// regenerated answer does not stack on the discarded partial. The backend calls
// it; the UI owns the visible transcript rollback against this seam. Optional and
// nil-safe: New defaults it to a no-op, so a standalone Runner or a turn whose
// stream never partially drops is unaffected.
RollbackPartial func()
IgnoredProviders *IgnoredProviders
ProviderIgnoredNotice func(text string)
// OnProviderStreamIdle reports that the active provider round's stream has gone
// quiet — no chunk for streamIdleThreshold. It fires at most once per provider round,
// on the turn goroutine, after the durable provider_stream_idle event has been
// flushed. Optional and nil-safe: New defaults it to a no-op.
OnProviderStreamIdle func(StreamIdleInfo)
// OnProviderStreamActivity reports that a chunk arrived after an idle crossing. It
// fires at most once per round and only after OnProviderStreamIdle. Optional and
// nil-safe: New defaults it to a no-op.
OnProviderStreamActivity func()
OnUserMessageAdded func(text string, parts []sessions.ContentPart)
// contains filtered or unexported fields
}
Deps gathers the injected collaborators for one provider turn. Active, Store, and Client are required; the remaining hooks default to no-ops when nil (RenderNotice to an identity renderer), so a test opts in only to the behavior it asserts. Sink, Flush, FlushUnsupported, and OnToolCalls are the runtime hooks; Notice, RenderNotice, RenderRetryNotice, Interrupt, Finalize, OnFailureWrite, and the retry UX hooks are failure/status hooks that keep the package free of internal/tui and internal/bootstrap.
type DispatchFailure ¶
DispatchFailure classifies a tool-call failure that happened after scheduling began. Error() must stay safe text only — every failure surface prints it — while Unwrap keeps the original error matchable by errors.Is.
func (*DispatchFailure) Error ¶
func (e *DispatchFailure) Error() string
func (*DispatchFailure) Unwrap ¶
func (e *DispatchFailure) Unwrap() error
type FinalFlushError ¶
type FinalFlushError struct{ Err error }
FinalFlushError wraps a flush failure after the provider stream already completed and model_response was appended. Callers can distinguish it from provider/stream errors when deciding whether the visible stream should be sealed.
func (*FinalFlushError) Error ¶
func (e *FinalFlushError) Error() string
func (*FinalFlushError) Unwrap ¶
func (e *FinalFlushError) Unwrap() error
type FlushHook ¶
type FlushHook func() error
FlushHook makes turn records durable at boundaries. The production binding flushes the real session store and routes atomic-write failures through the per-turn write-failure protocol.
type IgnoredProviders ¶
type IgnoredProviders struct {
// contains filtered or unexported fields
}
func (*IgnoredProviders) Add ¶
func (s *IgnoredProviders) Add(slug string) bool
func (*IgnoredProviders) List ¶
func (s *IgnoredProviders) List() []string
type MoveTarget ¶
type PendingUserMessage ¶
type PendingUserMessage struct {
Text string
Parts []sessions.ContentPart
}
type PlannedToolCall ¶
type PlannedToolCall struct {
Call AssembledToolCall
Deps []string
Targets []string
// ContinueOnError withholds this unit's failure from the scheduler, so concurrent
// siblings drain instead of being cancelled. The loop still reports the error before
// persisting anything.
ContinueOnError bool
// PreRun runs before Run and skips it on error. It is separate from Run because a
// wrapping dispatcher may replace Run, and a planner's guard must survive that.
PreRun func(ctx context.Context) error
Run func(ctx context.Context) (ToolResult, error)
}
type RetryPolicy ¶
type RetryPolicy struct {
Enabled bool
MaxAttempts int
BaseDelay time.Duration
MaxDelay time.Duration
Budget time.Duration
}
RetryPolicy is the resolved auto-retry configuration for one turn. The zero value is disabled, so a Runner built without a policy behaves exactly as a no-retry runner. Production wiring fills it from the provider.retry settings block (config.Cache.ProviderRetry); MaxAttempts is the total number of attempts including the first.
type RoundReport ¶
type Runner ¶
type Runner struct {
// contains filtered or unexported fields
}
Runner runs one streamed provider turn and exposes the cancel handle the landed tui.StreamCanceller needs. A Runner is single-use: create one per turn.
func (*Runner) Cancel ¶
Cancel cancels the in-flight provider request and reports the characters retained for the response so far plus whether a stream was actually in flight, matching the landed tui.StreamCanceller shape (the production wiring adapts it). It is safe to call from another goroutine and is idempotent: once the loop has observed cancellation or completion it returns inFlight=false and cancels nothing.
func (*Runner) Run ¶
Run executes the plain send→reply turn synchronously: build the request, emit the start-of-turn events once (filtering unsupported effort), and then drive one or more provider-round attempts under the retry policy (runWithRetry → runAttempt). A transient provider-round failure with attempts and budget remaining is auto-retried with bounded jittered backoff; a deterministic, user-actionable failure surfaces in one pass with no retry. The per-attempt failure routing and the natural-completion/tool-call paths are documented on runAttempt. It returns ErrNoModelSelected when no model is selected (no provider call), the flush-hook error when the post-unsupported flush fails (no provider call), and the provider/stream error (or nil) of the final attempt. No failure path emits model_response, and tool-call turns hand off to OnToolCalls.
type Sink ¶
type Sink func(delta string)
Sink receives each assistant content delta as it streams in, in arrival order. The production binding pipes it to the transcript via App.AppendStreamDelta.
type StreamIdleInfo ¶
type StreamIdleInfo struct {
Provider string
ModelID string
RoundElapsedMs int64
IdleMs int64
PartialChars int64
}
StreamIdleInfo is the live idle-crossing payload delivered to OnProviderStreamIdle. It mirrors the durable provider_stream_idle event but is handed over in-process so a consumer can react without reading events back.
type ToolCallHandoff ¶
type ToolCallHandoff struct {
AssistantText string
ToolCallDeltas [][]openai.ChatCompletionChunkChoiceDeltaToolCall
Report *RoundReport
UpstreamProvider string
}
ToolCallHandoff carries the completed-turn data a tool-call hook needs to take over agentic iteration. AssistantText is the streamed assistant content so far; ToolCallDeltas are the raw per-chunk tool_call fragments collected in arrival order. Assembling and executing them is the tool-call hook's job.
type ToolCallHook ¶
type ToolCallHook func(ctx context.Context, h ToolCallHandoff) error
ToolCallHook intercepts a completed turn whose response requested tool calls so the agentic loop can iterate without rewriting the stream loop. The default is a no-op.
type ToolDispatcher ¶
type ToolDispatcher interface {
// Plan classifies each call on its own: prepared project-confined structured reads may
// overlap within a contiguous run, and every other call is a serial barrier that keeps
// model-call order across the runs on either side of it.
Plan(calls []AssembledToolCall, maxConcurrency int) ToolPlan
// Dispatch executes one authorized-or-denied call and returns its tool-result
// content. A denied call returns the deny envelope as the content with a nil
// error and ToolResult.Denied set; the loop persists that envelope and its
// denied_by decides what happens next — a user or unclassifiable denial halts
// the turn, a recoverable one (permissions.IsRecoverableDeniedBy) re-issues (see ToolResult).
// A non-nil error is an internal failure that aborts the loop without partial
// continuation.
Dispatch(ctx context.Context, call AssembledToolCall) (ToolResult, error)
}
ToolDispatcher is the injected, turn-level seam the agentic loop runs each assembled tool call through. Production wiring binds it to the real registry/runner and the production authorizer behind DispatchShell/DispatchTool; turn tests inject a fake, so the loop is exercised without bootstrap wiring.
type ToolLoop ¶
type ToolLoop struct {
// Expire fires the time limit immediately while Run has a deadline set; only tests call it.
Expire func()
// contains filtered or unexported fields
}
ToolLoop drives agentic tool-call iteration for one assistant turn. It is the production replacement for a bare Runner.Run when tool dispatch is wired: Run issues the streamed turn, and on a tool-call response the OnToolCalls hook (onToolCalls below) assembles the calls, persists the assistant tool-call message, schedules and dispatches the calls, persists the tool results, and signals continuation so Run re-issues the turn. The base Runner loop is unchanged — the loop re-enters it through the hook rather than rewriting it.
func NewToolLoop ¶
func NewToolLoop(deps Deps, dispatcher ToolDispatcher, maxConcurrency int) *ToolLoop
NewToolLoop builds an agentic loop over the per-turn deps, the injected dispatcher seam, and the scheduler concurrency limit (production threads Cache.AgentMaxConcurrency() here). It wires its own OnToolCalls hook onto deps so every turn it issues hands tool-call responses back to the loop.
func (*ToolLoop) AddUserMessage ¶
func (l *ToolLoop) AddUserMessage(text string, parts []sessions.ContentPart) bool
func (*ToolLoop) Cancel ¶
Cancel cancels the currently active provider stream, when one exists, and reports the retained assistant text for the TUI's stream_interrupted event. Tool dispatch cancellation is owned by the parent context passed to Run.
func (*ToolLoop) Denied ¶
Denied reports whether the turn stopped on a terminal permission refusal — a user denial or an unclassifiable deny envelope — rather than a completed final answer. It is valid after Run returns nil: a terminal denial-stop appends the assistant tool-call record and deny envelope but produces no final assistant answer, so callers gate first-turn completion work (e.g. title generation) on this being false. A recoverable policy denial reports false: that turn re-issued the model and ran on to a real final answer.
func (*ToolLoop) Run ¶
Run drives the turn to a final answer. It issues a fresh single-use Runner per provider round (Runner is single-use, and BuildRequest re-reads the store each round so the appended assistant/tool messages feed the next request). A tool-call round handled by onToolCalls sets continued and re-issues; a final no-tool-call round records model_response and finalizes the stream without setting continued, ending the loop. A round whose dispatched results include a terminal permission denial (a user denial or an unclassifiable envelope) leaves continued false, so that refusal halts the attempt without a follow-up provider round; a round denied only by mode policy continues so the model sees the refusal and can adapt. A round carrying ToolResult.Terminal ends the turn after its results are recorded. Any provider/stream/flush error, or a dispatch or scheduler failure surfaced by onToolCalls, ends the loop and is returned.
func (*ToolLoop) SetDeadline ¶
func (*ToolLoop) TimeLimitReached ¶
func (*ToolLoop) Unsent ¶
func (l *ToolLoop) Unsent() []PendingUserMessage
type ToolPlan ¶
type ToolPlan struct {
Units []PlannedToolCall
}
type ToolResult ¶
type ToolResult struct {
Content string
Denied bool
Terminal bool
WrapUp bool
Notice string
ModelContextImage string
DisplayPatch string
// WriteTarget is the path a dispatcher resolved and authorized for an edit or create that
// then returned exit status 0, and is empty otherwise. It is evidence of a successful
// write, not a record of every filesystem change the call made.
WriteTarget string
DeleteTarget string
MoveTarget MoveTarget
}
ToolResult is the outcome of dispatching one tool call. Content is the tool-message text fed back to the model: captured command output on success, or the synthetic deny envelope when the call was refused. A denial is an in-band result the loop always records; whether it then halts or continues depends on the envelope's denied_by kind (see Denied). An internal dispatch failure is instead reported as a non-nil error from Dispatch, which aborts the loop. The deny envelope and an executed result are recorded identically, exactly as the model should see them; only whether the loop re-issues afterward differs.
Denied marks a permission refusal. When set, the loop persists Content through permissions.DeliverDeny (the deny delivery seam) keyed to the call's tool_call_id, instead of the ordinary tool-result record, so DeliverDeny is the single production delivery path for the deny envelope. The loop then decodes Content's denied_by: a user-denied prompt (denied_by:"user") or an unclassifiable envelope halts the turn so a refusal cannot be fed back as an invitation to retry, try an alternate method, or claim a result without the user; a denial permissions.IsRecoverableDeniedBy accepts is instead re-issued, so the model sees the refusal and can pick a permitted alternative. Content carries the envelope either way, so the model-facing result is byte-identical; the marker selects both the append path and, via the decoded kind, the halt-vs-continue decision, because Content alone cannot distinguish a deny envelope from ordinary tool output.
Terminal marks an ordinary result that ends the turn; it is not a permission denial.
WrapUp marks a recoverable refusal after which the loop allows exactly one more provider round, advertising no tools, for the model's final answer.
Notice is display-only text the loop shows the user for a result that still continues the turn, emitted independently of Terminal. A dispatcher must set it already bounded and redacted, because it reaches the user verbatim.