aichat

module
v0.15.6 Latest Latest
Warning

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

Go to latest
Published: Nov 7, 2026 License: Apache-2.0

README

aichat

Shared AI provider layer and Bubble Tea chat kit for Sneat, DataTug and future products. Apache-2.0.

Package What it owns
ai Chat request, normalised streaming event model, LLMProvider, tool calling (Tool/ToolCall/ToolResult) and reasoning
ai/agent Tool-calling agent Loop over an ai.LLMProvider: executes Handlers, feeds results back, itself an ai.LLMProvider
ai/decision Decision schema (evolvable) with three provenances (calibrated, self-reported, deterministic), Provider, the first-decider-wins Chain (stops at an exhausted allowance, a spent budget or a misconfigured engine by default); scored candidates (ScoredProvider), the named SelectionPolicy with one Actionable rule: a decision is actionable only with a positive verdict stamped by the package's own judges (a calibrated selection, a deterministic rule match, an uncalibrated decision accepted by the policy's explicit AcceptUncalibratedAt, or a policy-less chain's floor), never by omission, never from the wire or storage (Chain.Rejudge), and never a side-effectful interaction without positive, backed confidence; and Report/Trace of which engine answered, with per-attempt usage and latency. The default thresholds are PROVISIONAL: re-measure them on your corpus and the exact model id
ai/session Entity refs, focus/selection/sidebar, pending/previous action
ai/cloudproto Product-neutral wire protocol + SSE codec for an AI cloud boundary (e.g. api.sneat.cloud)
ai/openaicompat LLMProvider over the OpenAI Chat Completions streaming API (plain net/http)
ai/openairesponses LLMProvider over OpenAI's Responses API (plain net/http, item-based input/output, same Config shape as ai/openaicompat)
ai/anthropic LLMProvider over the Anthropic Messages streaming API (plain net/http, prompt caching)
ai/cloud LLMProvider client for the ai/cloudproto cloud boundary; Decider() returns its separate decision.Provider role (also a ScoredProvider, over POST ai/score), plus Usage. Decision-route errors map to the engine-neutral sentinels (invalid, auth, quota, misconfigured) and carry Retry-After; "no such route" is told from a mistyped base URL and remembered (ResetCapabilities)
ai/decision/rules Deterministic, table-driven decision.Provider — no regex/NLP engine; a match is declared deterministic (decision.Deterministic), so it is always actionable under any selection policy, durable included, and never costs an engine call
ai/decision/llmdecider decision.Provider backed by one structured LLM inference: an LLM decider with uncalibrated confidences (decisions and scored candidates), a fallback or emulator behind a real decision model
ai/decision/typesafe Client for TypeSafe AI's System One API (the Jev decision model): decision.Provider and decision.ScoredProvider with calibrated probabilities; the model id is required (pin a version)
ai/decision/compose Engine combinators over decision.Provider: Single, Fallback, Hedged, Race, a circuit Breaker (hedge-cancelled primaries are tallied slow, not failed; a quota refusal never fails over to a paid backup unless OnQuota) and Budget, a cap on how often a paid backup is called (ErrBudget)
ai/ctxmgr Context Manager: token-budgeted, cache-stable ai.ContextBlock selection
ai/aiconfig Product config (cloud/byok, decision chain) and Build wiring
ai/diag Per-turn diagnostics record (diag.Turn) and Debug-level logging
ai/internal/retry Unexported retry-before-first-byte helper (+ Retry-After honouring) shared by the HTTP adapters
ai/internal/sse Unexported SSE line-scanner (LF/CRLF/bare-CR) shared by every SSE reader in the module
tui Messages shared across the tui/* packages (e.g. AddToSidebarMsg)
Markdown rendering Moved to tuigoff/pkg/mdrender: tuigoff owns rendering, aichat composes it
Transcript, sidebar, grid block Moved to tuigoff/pkg/transcript, pkg/sidebar, pkg/gridblock: tuigoff renders, aichat composes. session.EntityRef is an alias of tuigoff's entity.Ref
tui/stream Pumps an ai.LLMProvider stream into Bubble Tea messages without buffering
tui/chatshell The reusable chat screen composing the packages above; a product plugs in a Handler, and optionally a SidePanel (replaces the sidebar), Overlays (modal dialogs), GlobalKeys, content-only top-bar/hints-bar providers (WithTopBarProvider/WithHintsProvider), and mouse support (WithMouse/SetMouseEnabled: wheel-scrolls the transcript)

Using the decision chain safely

Four things a product must know before it acts on ai/decision answers:

  • Only a judged decision is actionable. Decision.Actionable() is true only when the package's own judges (Chain, compose.WithPolicy, decision.Deterministic) stamped a positive verdict in an unexported field. The exported Outcome is informational and is never believed from a provider, a remote engine or storage. A side-effectful interaction (confirmation, rejection, correction, cancellation, undo) needs, with or without a policy, an InteractionConfidence above 0 and at least the durable bar (0.90); a calibrated claim also needs InteractionScores, because a remote engine's calibrated flag is only a claim.
  • A stored or replayed decision is NOT actionable and has lost its provenance class (a replayed rule decision reads self_reported). Re-judge it with Chain.Rejudge(d, taxonomy) (it is then an ordinary decision under your policy or floor, never deterministic), or re-run your rules.
  • A stopped chain is not "nobody decided". At an exhausted allowance (ErrQuota), a spent budget (ErrBudget) or a misconfigured engine the chain returns ok=false with Trace.StoppedBy and Trace.Err() set, so a paid backup is never billed silently. Check Trace.StoppedBy BEFORE escalating to your paid main LLM. Put rules (deterministic providers) before the engines: a stop skips everything after it.
  • A verdict belongs to the content it judged. Editing a judged decision's module, intent, interaction, confidences or Calibrated makes it not actionable again, json.Unmarshal into a judged value discards its verdict, and Chain.Rejudge never upgrades a refusal recorded in Outcome (a policy-less chain knows only its floor: rejudge with the chain that carries your policy). Fields outside that list (Scores, Slots, Reference, ...) are not bound: re-judge after changing them.
  • A paid backup needs a cap. TypeSafe documents one 429 (a rate limit) and no way to tell a spent Jev account from it, so every 429 stays transient and a Fallback(Breaker(jev), Breaker(llm)) sends all traffic to the LLM once the account is spent (a probe: 5 Jev calls and 50 paid LLM calls in 50 turns). Wrap the backup in compose.NewBudget (inside its Breaker), or set decision.backupBudget: {maxCalls, per} in aiconfig. For anonymous or public traffic run the calibrated engine alone (no paid backup) or with a budgeted backup. The cap counts admissions, not billed requests (a hedge-started backup that loses still consumed one), its window is fixed (a burst across a boundary can admit twice the cap), a spent budget is never failed over from (OnQuota does not apply) and always beats an abstention or an uncertain answer, so the chain stops; a spent budget in a race halts the race.

Upgrade notes

  • Usage is reported on every attempt. Trace.Attempts[i].Usage (and Report.Attempts) now carries what the engine billed for EVERY upstream call, not only an abstained or scored one: a decided answer, an answer the policy judged uncertain, and a call that failed after a response that carried a usage object. typesafe.Client reports one attempt per call from DecideTraced and the new ScoreTraced (outcome decided, abstained or error, with latency), and compose (Single, Fallback, Hedged, Race, Breaker, Budget) and decision.Chain keep it. A call whose response reported no usage, a network error or a leg still running when it was cancelled, has Usage == nil (cost unknown), never zero. A host that meters or caps spend can sum Attempts[].Usage from the trace; typesafe.CallEvent.Usage is likewise set on a failed call whose body carried usage. A report that used to carry no attempts for a decided answer or an error now carries one, so code that counted len(Attempts) as "abstentions" should filter on Outcome.

Products own scopes, intents, prompts, actions and controls. This module owns only what every product needs to talk to models and render chat the same way — including how it looks: github.com/tuigoff/tuigoff/pkg/theme is the single place colour, padding and border decisions live, and every tui/* package (and every product built on tui/chatshell) gets that look as the default, with zero styling code of its own. A product supplies content only — message text, hint labels ([]theme.Hint), top-bar title/context/menu items ([]theme.MenuItem) — never a colour literal. See spec/features/tui-kit for the full contract.

See docs/tui.md for the tui/* keyboard map and sidebar reference.

Directories

Path Synopsis
ai
Package ai is the product-neutral AI contract shared by Sneat, DataTug and future products: chat requests, the normalised streaming event model, and the LLMProvider interface every adapter (the ai/cloud client, OpenAI-compatible, Anthropic) implements.
Package ai is the product-neutral AI contract shared by Sneat, DataTug and future products: chat requests, the normalised streaming event model, and the LLMProvider interface every adapter (the ai/cloud client, OpenAI-compatible, Anthropic) implements.
agent
Package agent implements a tool-calling agent loop over ai.LLMProvider: it streams a model's response, executes any tool calls the model asked for via caller-supplied Handlers, feeds the results back, and repeats until the model stops calling tools or a configured limit is hit.
Package agent implements a tool-calling agent loop over ai.LLMProvider: it streams a model's response, executes any tool calls the model asked for via caller-supplied Handlers, feeds the results back, and repeats until the model stops calling tools or a configured limit is hit.
aiconfig
Package aiconfig loads and applies the product-level configuration for which LLM and decision providers a product wires up: cloud or BYOK (bring your own key, connecting directly to the provider, never via the cloud), and which deciders run before/instead of the cloud's decision service.
Package aiconfig loads and applies the product-level configuration for which LLM and decision providers a product wires up: cloud or BYOK (bring your own key, connecting directly to the provider, never via the cloud), and which deciders run before/instead of the cloud's decision service.
anthropic
Package anthropic is an ai.LLMProvider for the Anthropic Messages API, implemented directly over net/http -- no vendor SDK.
Package anthropic is an ai.LLMProvider for the Anthropic Messages API, implemented directly over net/http -- no vendor SDK.
clientctx
Package clientctx holds reusable, privacy-bounded caller context helpers.
Package clientctx holds reusable, privacy-bounded caller context helpers.
cloud
Package cloud is the client for the cloudproto protocol (see ai/cloudproto): it implements ai.LLMProvider (chat) and exposes a separate decision.Provider via Decider() that is also a decision.ScoredProvider (POST ai/score), plus a Usage lookup.
Package cloud is the client for the cloudproto protocol (see ai/cloudproto): it implements ai.LLMProvider (chat) and exposes a separate decision.Provider via Decider() that is also a decision.ScoredProvider (POST ai/score), plus a Usage lookup.
cloudproto
Package cloudproto is the small, product-neutral wire protocol between clients and an AI cloud boundary.
Package cloudproto is the small, product-neutral wire protocol between clients and an AI cloud boundary.
ctxmgr
Package ctxmgr selects which ai.ContextBlock values go into a ChatRequest, balancing two goals: keep the LLM within a token budget, and keep the provider-cached prefix (the leading static blocks a provider such as Anthropic prompt-caches) as stable as possible across turns so caching actually pays off.
Package ctxmgr selects which ai.ContextBlock values go into a ChatRequest, balancing two goals: keep the LLM within a token budget, and keep the provider-cached prefix (the leading static blocks a provider such as Anthropic prompt-caches) as stable as possible across turns so caching actually pays off.
decision
Package decision defines the DecisionProvider contract and the Chain that runs providers in order until one decides.
Package decision defines the DecisionProvider contract and the Chain that runs providers in order until one decides.
decision/compose
Package compose holds engine combinators over decision.Provider (and decision.ScoredProvider): Single, Fallback, Hedged and Race, plus Breaker, a circuit breaker that stops calling an engine that is down, and Budget, a cap on how often an engine (typically a paid backup) is called.
Package compose holds engine combinators over decision.Provider (and decision.ScoredProvider): Single, Fallback, Hedged and Race, plus Breaker, a circuit breaker that stops calling an engine that is down, and Budget, a cap on how often an engine (typically a paid backup) is called.
decision/llmdecider
Package llmdecider is a decision.Provider that makes ONE structured inference against an ai.LLMProvider to produce a decision.Decision.
Package llmdecider is a decision.Provider that makes ONE structured inference against an ai.LLMProvider to produce a decision.Decision.
decision/rules
Package rules is a deterministic, table-driven decision.Provider.
Package rules is a deterministic, table-driven decision.Provider.
decision/typesafe
Package typesafe is a client for TypeSafe AI's System One API (the "Jev" decision model) and a decision.Provider / decision.ScoredProvider built on it.
Package typesafe is a client for TypeSafe AI's System One API (the "Jev" decision model) and a decision.Provider / decision.ScoredProvider built on it.
diag
Package diag is the shared diagnostics record for one conversational turn: which path handled it, what the decision chain did, which LLM answered, timing and usage.
Package diag is the shared diagnostics record for one conversational turn: which path handled it, what the decision chain did, which LLM answered, timing and usage.
internal/retry
Package retry is a tiny internal helper shared by the LLMProvider HTTP adapters (ai/openaicompat, ai/anthropic, ai/cloud).
Package retry is a tiny internal helper shared by the LLMProvider HTTP adapters (ai/openaicompat, ai/anthropic, ai/cloud).
internal/sse
Package sse is a tiny internal helper shared by every SSE reader in this module (ai/openaicompat, ai/anthropic, ai/cloudproto): a bufio.SplitFunc that behaves like bufio.ScanLines but also accepts a bare '\r' (old Mac-style line endings), which some SSE producers/proxies still emit.
Package sse is a tiny internal helper shared by every SSE reader in this module (ai/openaicompat, ai/anthropic, ai/cloudproto): a bufio.SplitFunc that behaves like bufio.ScanLines but also accepts a bare '\r' (old Mac-style line endings), which some SSE producers/proxies still emit.
openaicompat
Package openaicompat is an ai.LLMProvider for the OpenAI Chat Completions streaming API (and any API-compatible endpoint), implemented directly over net/http -- no vendor SDK.
Package openaicompat is an ai.LLMProvider for the OpenAI Chat Completions streaming API (and any API-compatible endpoint), implemented directly over net/http -- no vendor SDK.
openairesponses
Package openairesponses is an ai.LLMProvider over OpenAI's Responses API (POST {base}/responses, stream:true), implemented directly over net/http -- no vendor SDK.
Package openairesponses is an ai.LLMProvider over OpenAI's Responses API (POST {base}/responses, stream:true), implemented directly over net/http -- no vendor SDK.
session
Package session holds the product-neutral conversational working state that helps resolve requests such as "move it to Friday": the focused entity, the current selection, sidebar pins, and the pending and previous actions.
Package session holds the product-neutral conversational working state that helps resolve requests such as "move it to Friday": the focused entity, the current selection, sidebar pins, and the pending and previous actions.
tui
Package tui holds the small set of messages shared across the strongo/aichat Bubble Tea chat kit's sub-packages (tui/stream, tui/chatshell) so a leaf component can ask the shell to do something (e.g.
Package tui holds the small set of messages shared across the strongo/aichat Bubble Tea chat kit's sub-packages (tui/stream, tui/chatshell) so a leaf component can ask the shell to do something (e.g.
chatshell
Package chatshell composes tuigoff's pkg/transcript, pkg/sidebar, pkg/focus and pkg/gridblock with aichat's tui/stream into the reusable chat screen every aichat product runs: history + composer + sidebar + top/status bars, with a split pane when the terminal is wide enough (>= 104 columns, matching DataTug's splitEnabled).
Package chatshell composes tuigoff's pkg/transcript, pkg/sidebar, pkg/focus and pkg/gridblock with aichat's tui/stream into the reusable chat screen every aichat product runs: history + composer + sidebar + top/status bars, with a split pane when the terminal is wide enough (>= 104 columns, matching DataTug's splitEnabled).
stream
Package stream pumps an ai.LLMProvider's iter.Seq2[ai.Event, error] into Bubble Tea messages without buffering the response: each event arrives as an EventMsg carrying the tea.Cmd that re-arms the pump for the next event (the "channel re-arm" pattern DataTug chat used for HTTP/LLM streaming).
Package stream pumps an ai.LLMProvider's iter.Seq2[ai.Event, error] into Bubble Tea messages without buffering the response: each event arrives as an EventMsg carrying the tea.Cmd that re-arms the pump for the next event (the "channel re-arm" pattern DataTug chat used for HTTP/LLM streaming).

Jump to

Keyboard shortcuts

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