chat

package
v0.8.1 Latest Latest
Warning

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

Go to latest
Published: Sep 16, 2026 License: MIT Imports: 33 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultMaxSessions = 200

DefaultMaxSessions is how many recorded sessions Save keeps once MaxSessions is unset: the decided default (Task 20) balancing "the picker stays useful" against "~/.config/nib/sessions/*.json accumulating forever, each holding a full transcript".

View Source
const ModelListTimeout = 3 * time.Second

ModelListTimeout bounds the endpoint lookup behind /model and /models. Both front ends run that lookup on the goroutine that draws the prompt, so an endpoint that accepts the connection and then never answers would otherwise freeze the UI with no way out.

It is deliberately short. In the TUI this is the only synchronous network call on the Update goroutine, so the whole interface, Ctrl+C included, stops responding for as long as it runs: the bound is a responsiveness budget, not a patience budget. A listing that a local endpoint cannot produce in three seconds is a listing the user is better off not waiting for, and the degraded outcome is mild (a switch that goes through unverified, never a refusal).

Variables

This section is empty.

Functions

func BashGrantPrefix added in v0.2.0

func BashGrantPrefix(argsJSON string) (string, bool)

BashGrantPrefix derives the prefix-grant key for a bash tool call: the script's first word, when the script is a single simple command. ok is false when no safe prefix can be derived (compound or chaining commands, unparseable args) — the UI then offers a whole-tool grant instead.

func ContextBudget added in v0.8.1

func ContextBudget(cfg types.CompactionConfig, window int) int

ContextBudget is the window minus the reserve held back for the response, where the reserve is never allowed to claim more than a quarter of the window.

Exported so the TUI's context badge can be drawn against the same number auto-compaction triggers on. A badge that budgeted against the raw window disagreed with the moment compaction actually fires, which is the one thing the badge exists to predict.

The clamp is not the percentage reserve the spec rejected. The reserve stays a flat cfg.ReserveTokens for every window of 4×ReserveTokens or more — 16384 and up at the 4096 default — and the trigger is still Threshold × budget, not a percentage of the window. The clamp bites only where the flat number is incoherent relative to the window it is being subtracted from: without it a 4096-token model reserves its entire window, the budget is 0, and auto- compaction switches OFF for the model that overflows soonest. Worse, once a window is learned from an overflow error, LEARNING a real 4096 window would be what disabled compaction for the model that just overflowed — the exact inverse of the point of learning it. The default is applied HERE as well as in config.Load, the same way shouldAutoCompact defaults Threshold at its use site. An embedder calling chat.NewSession directly never passes through config.Load, and an unset ReserveTokens would then reserve nothing — which is precisely the failure this budget exists to prevent, arriving through the one door nobody watches.

It lands BEFORE the quarter-window clamp, so the two stay coherent: a small window still clamps the default (a 8192-token model reserves 2048, not 4096) rather than the clamp being bypassed by a zero.

func FormatModelList added in v0.5.0

func FormatModelList(models []string, current string) string

FormatModelList renders a model listing, marking the current model. Order is the endpoint's own; the caller decides whether to sort.

func FormatSessionSummary added in v0.7.0

func FormatSessionSummary(u SessionUsage) string

FormatSessionSummary renders the one-line spend report shown when a session ends. It returns "" when nothing was spent, so a session that exits before its first turn prints nothing rather than a row of zeroes.

Turns and tokens are not in lockstep and the wording does not claim they are: every run is counted for tokens, including one the user interrupted, while Turns counts only completed exchanges. A session showing spend against fewer turns is reporting the truth, not a miscount.

Callers must not send this to stdout in TUI mode: stdout carries the shell-capture line, and a summary there would be pasted into the user's command line. RunTUI writes it to /dev/tty and RunCLI to stderr.

func FormatToolCall

func FormatToolCall(name, argsJSON string) string

FormatToolCall renders a tool call's arguments as a compact, human-readable summary for display, replacing raw JSON. argsJSON is the marshaled arguments object. Known tools get a purpose-built one-liner (see toolFormatters); any other tool (MCP servers, plugins) falls back to an aligned key/value args card (see ToolArgRows). If argsJSON is not a JSON object the input is returned unchanged.

func FormatToolResult added in v0.8.0

func FormatToolResult(name, result string) string

FormatToolResult renders a tool's output for human reading. A tool with a formatter gets purpose-built text; a JSON object with no formatter (any MCP tool, or a future built-in) degrades to flattened key/value rows — the same fallback FormatToolCall already gives arguments; a result that is not JSON at all passes through unchanged.

func GrantScope added in v0.2.0

func GrantScope(name, argsJSON string) (scope, prefix string)

GrantScope describes what choosing "always allow" covers for this call: scope is the user-facing wording, prefix the grant key ("" = whole tool). Only bash gets prefix grants; every other tool (including bash_background) is granted whole.

func HumanTokens

func HumanTokens(n int) string

HumanTokens formats a token count compactly (e.g. 47200 → "47.2k").

func HumanTokensOrZero added in v0.7.0

func HumanTokensOrZero(n int) string

HumanTokensOrZero is HumanTokens with a floor of "0". HumanTokens renders "" for a zero count so a caller can drop the segment entirely, which suits a count that stands alone; it does not suit a fixed shape with a slot for every direction. A provider that reports no completion tokens would otherwise leave a hole where a number belongs.

Exported because the TUI's footer badge prints that same fixed shape from package tui and must not grow a second copy of this rule: one formatter, one guard (TestFormatSessionSummaryFillsZeroSegments and its badge twin).

func IsReadOnly added in v0.3.0

func IsReadOnly(name, argsJSON string, cmds readOnlyCommands) bool

IsReadOnly reports whether a tool call only observes state and is therefore safe to auto-approve in the default prompt mode. It is deliberately conservative: unknown tools and any non-trivial bash return false. cmds is the read-only bash command set (built-ins plus user config).

func PreviewResult

func PreviewResult(name, s string, maxLines int) string

PreviewResult formats a tool result for compact display: it renders the result for human reading (FormatToolResult — a purpose-built formatter when name has one, flattened rows for an unrecognized JSON object, or the raw text unchanged), trims surrounding whitespace, then truncates to at most maxLines lines, appending a "… N more lines" note when it had to cut. Returns "" for empty/whitespace input. maxLines <= 0 means no line limit.

Types

type AgentEvent

type AgentEvent struct {
	ID     string
	Type   string // agent type name (e.g. "explore"); empty for generic
	Task   string
	Status AgentStatus
	Result string
	Err    error
	// Populated on completion/failure events (zero otherwise):
	ToolCount   int           // tools the sub-agent executed
	TotalTokens int           // cumulative tokens consumed across the run
	Elapsed     time.Duration // wall-clock from spawn to completion
}

AgentEvent is emitted on sub-agent lifecycle changes (spawn/complete/fail).

func (AgentEvent) StatsSuffix

func (ev AgentEvent) StatsSuffix() string

StatsSuffix renders the trailing run-stats summary for a completed sub-agent, e.g. " · 3 tools · 12.4k tokens · 1m 03s". Segments whose value is zero or unknown are omitted; returns "" when nothing is known.

type AgentStatus

type AgentStatus string

AgentStatus mirrors cogito's sub-agent lifecycle states for UI consumption, decoupling the UI from the cogito type.

const (
	AgentStatusRunning   AgentStatus = "running"
	AgentStatusCompleted AgentStatus = "completed"
	AgentStatusFailed    AgentStatus = "failed"
)

type ArgRow added in v0.2.0

type ArgRow struct {
	Key   string
	Value string // first line of the value
	// HiddenLines counts the value's truncated lines (0 = single-line value).
	HiddenLines int
}

ArgRow is one key/value line of the fallback args card. Nested objects are flattened to dotted keys, so a card is always a flat list of rows.

func ToolArgRows added in v0.2.0

func ToolArgRows(name, argsJSON string) ([]ArgRow, bool)

ToolArgRows returns the args-card rows for a tool call, for callers that style keys and values separately (the TUI approval block). ok is false when the tool has a purpose-built formatter or the arguments are not a JSON object — render FormatToolCall's string instead.

func (ArgRow) ValueDisplay added in v0.2.0

func (r ArgRow) ValueDisplay() string

ValueDisplay renders the value with its hidden-line hint, e.g. "The login flow redirects to… (+12 lines)".

type AskRequest

type AskRequest struct {
	Question string
	Options  []string // optional multiple-choice options
	// MultiSelect, when true, lets the user pick several options (checkbox);
	// otherwise it's a single choice (radio). Only meaningful with Options.
	MultiSelect bool
}

AskRequest is a question the agent wants to ask the user.

type Callbacks

type Callbacks struct {
	OnStatus    func(status string)
	OnReasoning func(reasoning string)
	// OnStream, when set, receives live token-level deltas during generation
	// (reasoning/answer/tool-selection) so a UI can render progress as it
	// happens. Setting it opts the session into cogito's streaming path; the
	// existing step-boundary callbacks (OnReasoning/OnStatus/OnToolResult) still
	// fire afterwards. Optional — leave nil for the non-streaming path.
	//
	// Caveat worth knowing before you opt in: it currently zeroes token
	// accounting. cogito accumulates streaming usage from StreamEvent.Usage on
	// the done event, and its bundled clients never populate that field, so
	// Session.Usage reports 0 tokens for every streamed turn. nib's own CLI and
	// TUI do not set this, so the shipped binary is unaffected; an embedder that
	// sets it trades the token counter for live deltas until cogito's clients
	// request usage from the API.
	//
	// This is the only under-count an embedder opts into, not the only one the
	// counter has: SessionUsage's doc carries the known list, and all of them
	// under-report rather than invent spend.
	OnStream   func(ev StreamEvent)
	OnToolCall func(req ToolCallRequest) ToolCallResponse
	// OnStepContent is called with the assistant text that accompanied a tool
	// selection ("I'll search for X now…") at the step boundary, before the
	// selected tools run — so a UI can commit the commentary in chronological
	// order relative to OnToolResult. Never fires with empty content and never
	// for the turn's final reply (that arrives via OnResponse). Optional.
	OnStepContent func(content string)
	OnResponse    func(response string)
	OnError       func(err error)
	// OnToolResult is called after a tool finishes, with its output. Optional.
	OnToolResult func(res ToolResult)
	// OnAgentEvent is called on sub-agent lifecycle changes. Optional.
	OnAgentEvent func(ev AgentEvent)
	// OnAskUser is called when the agent asks the user a question (ask_user tool).
	// It blocks until the user answers and returns the answer.
	OnAskUser func(req AskRequest) string
	// OnScheduleWakeup is called when the agent schedules an in-session wake-up
	// (schedule_wakeup tool). It returns immediately with a confirmation; the
	// host re-engages the agent with the note once the delay elapses.
	OnScheduleWakeup func(req WakeupRequest) string
	// OnCronCreate registers a cron job and returns a confirmation (incl. its id).
	OnCronCreate func(req CronRequest) string
	// OnCronList returns a human-readable listing of active cron jobs.
	OnCronList func() string
	// OnCronDelete cancels a cron job by id and returns a confirmation.
	OnCronDelete func(id string) string
	// OnCompactDone is called after the conversation is compacted, with the
	// approximate token counts before and after. Optional.
	OnCompactDone func(before, after int)
	// OnPruneDone is called when tool-output pruning stubs results it had not
	// stubbed before, with how many results were replaced on this pass and the
	// approximate tokens that freed. It fires on the transition, not on every
	// LLM call: a call that re-applies existing stubs is silent. Optional.
	OnPruneDone func(results, freed int)
	// OnParked is called when the live run parks: the assistant has produced a
	// reply but cogito keeps the loop alive because background work (sub-agents
	// or shell jobs) is still pending or because the user may inject a follow-up.
	// reply is the assistant's text at the park point (may be empty). The host
	// can finalize the assistant turn in the transcript and unlock the composer
	// so the user can keep chatting (their input is injected into this same run).
	// May fire multiple times across one run. Optional.
	OnParked func(reply string)
	// OnResumed is called when an injected message wakes a parked run. The host
	// can re-lock the composer and show the working indicator again. May fire
	// multiple times across one run. Optional.
	OnResumed func()
}

Callbacks defines the interface for UI interactions.

type ContentPart added in v0.4.1

type ContentPart struct {
	Kind        PartKind
	DataURI     string
	AudioFormat string
}

ContentPart is nib's native multimodal part, satisfying cogito.TypedMultimedia so SendMessage can hand image/audio/video parts to the fragment. DataURI is a base64 data: URI (e.g. data:image/png;base64,...). AudioFormat is the audio container (e.g. "wav") used for the input_audio wire form.

func (ContentPart) Data added in v0.4.1

func (p ContentPart) Data() string

Data returns the raw base64 payload (no data: prefix) for input_audio; "" for image/video (which travel via URL()).

func (ContentPart) Format added in v0.4.1

func (p ContentPart) Format() string

Format returns the audio container for input_audio; "" for image/video.

func (ContentPart) MediaKind added in v0.4.1

func (p ContentPart) MediaKind() cogito.MediaKind

MediaKind maps to cogito's media kind.

func (ContentPart) URL added in v0.4.1

func (p ContentPart) URL() string

URL returns the data URI (used for image_url / video_url parts).

type CronRequest

type CronRequest struct {
	Expr      string
	Prompt    string
	Recurring bool
	Durable   bool
}

CronRequest is a recurring/one-shot job the agent registers (cron tool).

type FriendlyError added in v0.4.1

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

FriendlyError wraps a noisy backend error with a short, actionable message while preserving the original via Unwrap (so errors.Is/As still work).

func (*FriendlyError) Error added in v0.4.1

func (e *FriendlyError) Error() string

func (*FriendlyError) Unwrap added in v0.4.1

func (e *FriendlyError) Unwrap() error

type Message

type Message struct {
	Role    string
	Content string
}

Message represents a chat message.

type PartKind added in v0.4.1

type PartKind int
const (
	PartImage PartKind = iota
	PartAudio
	PartVideo
)

type Session

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

Session represents a chat session with the AI assistant

func NewSession

func NewSession(ctx context.Context, cfg types.Config, callbacks Callbacks, transports ...mcp.Transport) (*Session, error)

NewSession creates a new chat session.

If cfg.TraceDir is set and the recorder cannot be opened, NewSession fails rather than returning a session that quietly records nothing — a caller that asked for a trace should not have to discover later that it never got one. app.Run preflights the directory, so in practice only embedders calling this directly reach the failure.

func (*Session) AgentLog

func (s *Session) AgentLog(agentID string) string

AgentLog returns the captured activity log for a sub-agent (for the agent_logs tool / UI inspection).

func (*Session) AgentManager

func (s *Session) AgentManager() *cogito.AgentManager

AgentManager exposes the sub-agent registry so the UI can list and detach agents.

func (*Session) AutoApprove added in v0.8.0

func (s *Session) AutoApprove() bool

AutoApprove reports whether every tool call is currently auto-approved.

func (*Session) ClearGoal

func (s *Session) ClearGoal()

ClearGoal removes the active session goal.

func (*Session) ClearHistory

func (s *Session) ClearHistory()

func (*Session) Close

func (s *Session) Close() error

Close closes the session and cleans up resources

func (*Session) CompactHistory

func (s *Session) CompactHistory() (before, after int, err error)

CompactHistory summarizes the older portion of the conversation via the LLM and rebuilds the fragment as [summary] + recent tail, keeping the display copy consistent. It returns byte/4 token estimates of the conversation before and after compaction (before==after signals a no-op). On summary failure it returns the error WITHOUT mutating session state (atomic swap).

func (*Session) ContextTokens

func (s *Session) ContextTokens() int

ContextTokens reports the current conversation size in tokens for display: the last request's reported prompt tokens, or a byte/4 estimate when the backend hasn't reported usage yet (e.g. before the first turn). This is the same signal the auto-compaction trigger watches.

func (*Session) ContextWindow added in v0.8.1

func (s *Session) ContextWindow() int

ContextWindow reports the context window this session is actually budgeting against: the one learned from a backend overflow error when it belongs to the model in use, otherwise the configured MaxContextTokens.

It exists for display. The TUI cannot read s.compaction.MaxContextTokens and call it the window, because a learned window silently replaces it — a session configured for 400k against a model that really serves 262k would draw a badge claiming plenty of room while compaction fires.

func (*Session) EstimatedUsage added in v0.8.0

func (s *Session) EstimatedUsage() SessionUsage

EstimatedUsage derives a byte/4 estimate of the session's prompt and completion tokens from the current conversation (estimateUsageSplit), mirroring the fallback ContextTokens already applies for the context badge. It exists for a session whose real counter is unhelpful — chiefly a streamed one: cogito's bundled clients never populate StreamEvent.Usage, so every streamed turn adds zero to Usage() (see this file's doc comment).

This method always estimates from the fragment, regardless of what Usage() holds; deciding whether to prefer the measured figure over this one is the caller's job (the TUI's usageBadge does it). Deliberately kept OFF the Usage() / Close() path: only Usage() feeds trace.WriteUsage's usage.json, so an estimate can never land under those measured field names, which the package doc calls a contract benchmark harnesses read. A caller that displays this figure must mark it as an estimate (theme.UsageEstimatedPrefix) rather than presenting it as measured spend.

The byte/4 proxy is not guaranteed to be a floor the way Usage() is: content that compresses well under a real BPE tokenizer (long runs of whitespace or repeated characters) can make the true count lower than this guess. That is acceptable only because every caller marks the figure as an estimate rather than folding it into SessionUsage's own never-an-overstatement contract.

func (*Session) ExportHistory added in v0.4.0

func (s *Session) ExportHistory() []openai.ChatCompletionMessage

ExportHistory returns a copy of the full conversation messages (the same []openai.ChatCompletionMessage that backs the model context), suitable for JSON serialization and persistence. Feed the result back via types.Config.InitialHistory to resume the conversation losslessly — the model then continues with real memory of it, not a summary.

The returned slice is a copy, so mutating it (or its serialization) never touches the live session. It EXCLUDES the system prompt: s.messages only ever records user/assistant turns (the system prompt is regenerated per model/locale and re-applied to the fragment on every turn), so there is no system message to strip. Safe to call from another goroutine while a turn is running; it takes the same lock SendMessage holds while appending.

func (*Session) GetMessages

func (s *Session) GetMessages() []Message

GetMessages returns all messages in the conversation

func (*Session) Goal

func (s *Session) Goal() string

Goal returns the active session goal, or "" if none.

func (*Session) Inject

func (s *Session) Inject(msg string) bool

Inject delivers msg into the live run's message-injection channel, waking a parked loop so the assistant continues in the SAME run. Non-blocking: returns false when there is no live run, the channel is full, or msg is empty. Used for mid-run user follow-ups, shell-job completions, and scheduled wake-ups.

func (*Session) InjectUser added in v0.1.1

func (s *Session) InjectUser(msg string) bool

InjectUser delivers a user-typed follow-up into the live run (see Inject), and additionally tracks it: if the run returns before consuming it (the model never saw it), the text is reported by TakeUndelivered so the caller can re-dispatch it as a fresh turn instead of losing it to the end-of-run drain. System notices (shell-job completions, wake-ups) keep using Inject — re-running a stale notice as a fresh turn would re-trigger finished work.

func (*Session) Interrupt

func (s *Session) Interrupt()

Interrupt cancels the in-flight turn (and any sub-agents spawned within it), leaving the session alive. Safe to call when no turn is running.

func (*Session) KillAgent

func (s *Session) KillAgent(id string) bool

KillAgent cancels a running sub-agent by id (its context is cancelled, which stops the agent and any LLM call it has in flight). Returns false when the id is unknown. Safe to call on an already-finished agent.

func (*Session) ListModels added in v0.5.0

func (s *Session) ListModels(ctx context.Context) ([]string, error)

ListModels returns the model IDs the configured endpoint advertises, so a UI can offer them for /model. A failing endpoint surfaces as an error rather than an empty list.

func (*Session) LoadSkill

func (s *Session) LoadSkill(name string) (string, error)

LoadSkill appends a named skill's instructions to the session system prompt (eager load via /skill), so subsequent turns include it without a load_skill tool call. Returns a short notice for the transcript.

func (*Session) Model added in v0.5.0

func (s *Session) Model() string

Model returns the model the session is currently using. Safe to call from another goroutine while a turn is running.

func (*Session) PrefixWarm added in v0.5.0

func (s *Session) PrefixWarm() bool

PrefixWarm reports whether this session has already issued a request that SHOULD have prefilled its prompt prefix on the server — not that the server's cache still holds it. A LocalAI restart or a KV eviction leaves this stale-true. That direction is deliberate: a stale true costs a missing "preparing" label, while a false negative would only cost a redundant one, so callers should treat it as a hint for labelling and never as a guarantee.

It is set only where the model was genuinely reached — a completed SendMessage turn or a successful Warm — and never by the paths that bail before that (an attachments failure, or an all-blocked attachment set with no text). It is safe to call from another goroutine while a turn is running.

SetModel resets it to false: unlike a restart or an eviction, a switch is a cold prefix the session knows about. Nothing else resets it, and a rebuilt session is a new *Session that starts false.

func (*Session) ReconcileMCPServers

func (s *Session) ReconcileMCPServers(desired map[string]types.MCPServer) error

ReconcileMCPServers connects newly-desired config MCP servers and closes ones no longer desired (or whose command/args changed). Connect failures are logged and skipped so one bad server never breaks the session. Called from Reload at turn start (deferred while background sub-agents run), so closing a client session here cannot race with a detached agent still using it.

func (*Session) Reload

func (s *Session) Reload(cfg types.Config) error

Reload re-wires every reloadable part of the session from cfg. It closes MCP client sessions and mutates session state read by the turn goroutine and by detached sub-agents, so it must run at turn start in the turn goroutine and is deferred while background sub-agents run (see applyPendingReload). It must not run concurrently with a running turn or a live detached agent.

func (*Session) RunLive

func (s *Session) RunLive() bool

RunLive reports whether a run is currently in flight (between SendMessage start and return), including while it is parked. The TUI uses this as the authoritative signal for whether a typed message should be queued into the live run or start a new turn, rather than its own loading/parked UI flags which can briefly desync across park/resume events.

func (*Session) SendMessage

func (s *Session) SendMessage(text string, parts ...ContentPart) (string, error)

func (*Session) SendWithAttachments added in v0.4.1

func (s *Session) SendWithAttachments(ctx context.Context, text string, files []string,
	overrides map[string]attachments.Override) (string, []attachments.Blocked, error)

SendWithAttachments resolves treatments for files against the active model's capabilities, runs conversion/transcription, and sends one multimodal turn. Returns any blocked files (media the active model can't accept) for the caller to surface.

func (*Session) SetAutoApprove added in v0.8.0

func (s *Session) SetAutoApprove(on bool)

SetAutoApprove turns the session-wide approve-everything switch on or off at runtime (the /yolo toggle). It is deliberately atomic rather than guarded by historyMu, which is held across whole tool calls.

It does not touch allowedTools or allowedBashPrefixes: those are narrower grants the user minted explicitly, and revoking them as a side effect of flipping this switch would be a surprise. While active, it bypasses the external-influence approval prompt but still permits PreToolUse hooks to enforce their policies.

func (*Session) SetGoal

func (s *Session) SetGoal(goal string)

SetGoal sets (or replaces) the active session goal. While a goal is set, a turn re-runs until the model calls goal_done or the user interrupts. Call between turns, not during a live run: the goal_done tool is wired at the start of a turn, so arming a goal mid-run would not expose goal_done.

func (*Session) SetModel added in v0.5.0

func (s *Session) SetModel(name string)

SetModel switches the session to a different model, rebuilding the LLM client the same way NewSession does: same endpoint and credentials, same per-request metadata and reasoning effort, same trace wrapper when tracing is on. Relabelling the existing client would not switch anything, and rebuilding without re-applying that configuration would silently drop it.

Conversation history is kept: nib is built around persistent context, and /compact exists when history needs trimming. Sub-agents follow automatically, because newAgentLLM resolves against the session model. The prefix-warm flag does not: the new model has never seen this session's prefix.

A turn already in flight finishes on the client it started with (see currentLLM); the switch applies from the next turn. Safe to call from another goroutine while a turn is running.

func (*Session) SetShellJobs

func (s *Session) SetShellJobs(jobs *wizmcp.ShellJobs)

SetShellJobs wires the shared shell-job registry into the session so the pending-work predicate keeps a run parked while a backgrounded shell command is still running, and so finished shell jobs inject a completion notice into the live run. Registers the completion hook. Call once at setup.

func (*Session) SetSkills

func (s *Session) SetSkills(skills []types.Skill) error

SetSkills rebuilds the in-memory skills MCP server so load_skill advertises the given skills, swapping its client. An empty list tears the server down. Called from Reload at turn start (deferred while background sub-agents run), so closing the old skills client cannot race with a detached agent.

func (*Session) SwitchModel added in v0.5.0

func (s *Session) SwitchModel(ctx context.Context, name string) (string, error)

SwitchModel is the checked entry point behind /model <name>: it validates the name against what the endpoint advertises and only then calls SetModel. It returns the notice to show the user, or an error to show instead. Both front ends go through it, so the policy and its wording cannot drift between them.

SetModel takes no error by design, so a typo would otherwise switch happily and surface a turn later as a 404 from the backend, with nothing pointing at the cause. Validating here turns that into an immediate message that names the models the endpoint does serve.

A lookup that fails does NOT veto the switch. The list is a convenience, and a user asking for a different model may well be asking precisely because something is wrong with the endpoint right now; refusing would leave them stuck. The same goes for an endpoint that answers with an empty list. Both cases switch and say the name went unverified.

func (*Session) TakeUndelivered added in v0.1.1

func (s *Session) TakeUndelivered() []string

TakeUndelivered returns (and clears) user-typed follow-ups that were injected into a run that ended before consuming them. Call after a run returns to re-dispatch them as fresh turns.

func (*Session) ToolCallDenied

func (s *Session) ToolCallDenied(req ToolCallRequest) bool

ToolCallDenied reports whether the given tool call would be denied (used to verify PreToolUse hook gating end-to-end).

func (*Session) Usage added in v0.7.0

func (s *Session) Usage() SessionUsage

Usage returns what this session has spent so far. Safe to call from any goroutine, which the TUI does on every render.

func (*Session) Warm added in v0.5.0

func (s *Session) Warm(ctx context.Context) error

Warm issues the same request the next SendMessage would build — same system prompt, same tool schemas, same model — capped at one output token, so the server prefills and caches the prompt prefix.

It does not touch s.fragment or s.messages and fires no callbacks: nothing enters the transcript and the UI never sees it. It honors ctx, so a user who sends a message mid-prime cancels it rather than queueing behind it.

The prefix must match the real request to hit the cache, which is why this builds through toolOptions rather than assembling its own list. Note that a nil error only means the request was accepted — the server may or may not have retained the prefix.

type SessionRecord added in v0.8.0

type SessionRecord struct {
	ID       string                         `json:"id"`
	Title    string                         `json:"title"`
	Cwd      string                         `json:"cwd"`
	Model    string                         `json:"model"`
	Created  time.Time                      `json:"created"`
	Updated  time.Time                      `json:"updated"`
	Messages []openai.ChatCompletionMessage `json:"messages"`
}

SessionRecord is one recorded conversation: enough to repopulate types.Config.InitialHistory and resume losslessly (see Session.ExportHistory), plus the metadata the /resume picker lists by (Title, Cwd, Updated, message count).

type SessionStore added in v0.8.0

type SessionStore struct {
	Dir string
	// MaxSessions caps how many session files Save keeps after pruning; 0
	// (the zero value, so a bare NewSessionStore(dir) needs no extra wiring)
	// means DefaultMaxSessions. Set this after construction — e.g. from
	// types.Config.SessionRetention — to make the cap configurable.
	MaxSessions int
}

SessionStore persists SessionRecords as one JSON file per session under Dir. The caller picks Dir; the TUI (tui/model.go's NewModel) and the --resume flag (app/app.go's applyResumeFlag) both root it at the per-user BaseDir (~/.config/nib/sessions by default) rather than the process's cwd the way loop.Registry's loops.json is — cwd-relative would put every project's sessions in a different, mutually invisible folder, which defeats /resume --all (there would be nothing outside the current folder to widen to) and makes the Cwd field below pointless (every session in one folder would share the same Cwd by construction). Dir is created on first Save; List and Load tolerate it not existing yet.

func NewSessionStore added in v0.8.0

func NewSessionStore(dir string) *SessionStore

NewSessionStore returns a store rooted at dir. dir is not created until the first Save.

func (*SessionStore) Delete added in v0.8.0

func (s *SessionStore) Delete(id string) error

Delete removes one stored session by id — the /resume picker's delete affordance (tui/resume.go). A missing file is not an error: the caller (already showing a list built from an earlier List() call) may be acting on a session a concurrent prune or another delete already removed, and that is exactly the outcome it wanted anyway.

func (*SessionStore) List added in v0.8.0

func (s *SessionStore) List(cwd string) ([]SessionRecord, error)

List returns every recorded session, newest (Updated) first. cwd == "" returns every session regardless of where it was started; a non-empty cwd filters to sessions whose stored Cwd matches exactly (so a session started in a subdirectory of cwd is excluded — the caller widens with "" to see it, matching /resume --all).

A file that fails to read or parse is skipped, not fatal: one corrupt session must not make the whole picker unusable. A missing directory (nothing recorded yet) returns an empty list, not an error.

func (*SessionStore) Load added in v0.8.0

func (s *SessionStore) Load(id string) (SessionRecord, error)

Load reads one session by id. A missing or corrupt file is an error here (unlike List, which skips a corrupt file rather than failing the whole listing) — a direct `/resume <id>` naming a bad session has no other session to fall back to, so the caller must be told.

func (*SessionStore) Save added in v0.8.0

func (s *SessionStore) Save(rec SessionRecord) error

Save writes rec atomically: marshal to a temp file created in the SAME directory as the destination, then os.Rename over it. Same-directory matters — os.Rename is only atomic within a filesystem, and a temp directory elsewhere (e.g. os.TempDir()) could be a different one — and the rename itself means a crash mid-write leaves either the old file intact or the new one complete, never a truncated one.

type SessionUsage added in v0.7.0

type SessionUsage struct {
	PromptTokens     int `json:"prompt_tokens"`
	CompletionTokens int `json:"completion_tokens"`
	TotalTokens      int `json:"total_tokens"`
	Turns            int `json:"turns"`
}

SessionUsage is a snapshot of what a session has spent: the LLM calls it made, including sub-agents and compaction summaries, plus the number of user turns those calls served.

Every gap found so far under-reports rather than invents spend, so a figure here is a floor and never an overstatement. That direction is the contract; the list below is what is known, not a proof that nothing else leaks:

  • Streaming. cogito reads streamed usage from StreamEvent.Usage on the done event and its bundled clients never populate it, so a session that sets Callbacks.OnStream counts zero. nib's CLI and TUI do not set it.
  • A failed sub-agent. cogito keeps a sub-agent's fragment only on success, so whatever a failure burned before dying has nowhere to be read from.
  • A resumed sub-agent. send_agent_message to an agent that already finished runs a fresh ExecuteTools and reassigns agent.Fragment in place, with no status transition and so no agent callback: emitAgentEvent, the one place sub-agent spend is folded in, never fires for that run and its tokens are counted nowhere.

The JSON tags are load-bearing: the usage.json written beside a trace is read by benchmark harnesses, so the field names are a contract, not decoration.

type StreamEvent added in v0.3.1

type StreamEvent struct {
	Kind     string // delta kind
	Content  string // text delta, for reasoning/content
	ToolName string // tool name, for tool_call (first chunk only)
	ToolArgs string // streamed argument fragment, for tool_call
}

StreamEvent is a single live delta during generation, forwarded only when a consumer sets Callbacks.OnStream (which opts the session into cogito's streaming path). It lets a UI render reasoning/answer/tool-selection as the model produces them, instead of only at step boundaries. Kind is one of "reasoning", "content", "tool_call", "tool_result", "status", "done", "error".

type ToolCallRequest

type ToolCallRequest struct {
	Name      string
	Arguments string
	Reasoning string
	AgentID   string // non-empty when the requesting caller is a sub-agent
	// ExternalSources identifies untrusted data still present in the active
	// conversation. A non-empty value forces consequential calls through the
	// approval gate unless session-wide auto-approval is active.
	ExternalSources []string
}

ToolCallRequest contains information about a tool the agent wants to run.

type ToolCallResponse

type ToolCallResponse struct {
	Approved    bool
	Adjustment  string
	AlwaysAllow bool
	// AlwaysPrefix narrows an AlwaysAllow grant for the bash tool to scripts
	// whose first word matches (e.g. "git" → simple `git …` commands run
	// without prompting). Empty means the grant covers the whole tool.
	// Ignored unless AlwaysAllow is set.
	AlwaysPrefix string
	// AllowAllTurn, when set together with Approved, approves every remaining
	// tool call for the rest of the current turn (incl. sub-agents) w/o prompting.
	AllowAllTurn bool
}

ToolCallResponse represents the user's decision on a tool call.

type ToolResult

type ToolResult struct {
	Name      string
	Result    string
	Arguments string // marshaled JSON of the call's arguments, for display
	AgentID   string // non-empty when the tool was run by a sub-agent
}

ToolResult is the outcome of a tool execution, surfaced to the UI after the tool runs.

type UnservedModelError added in v0.5.0

type UnservedModelError struct {
	Name    string   // the name the user asked for
	Models  []string // what the endpoint does serve
	Current string   // the session's model, marked in the listing
}

UnservedModelError is what SwitchModel returns when the endpoint's listing does not contain the requested name.

It keeps the headline and the listing separate instead of pre-joining them into one message, because the two halves want different rendering: the TUI word-wraps error text, which strips the listing's indent column and truncates long model IDs, while a fenced block survives verbatim. A front end with no such distinction can just use Error().

func (*UnservedModelError) Error added in v0.5.0

func (e *UnservedModelError) Error() string

func (*UnservedModelError) Headline added in v0.5.0

func (e *UnservedModelError) Headline() string

Headline is the one-line explanation, safe to wrap.

func (*UnservedModelError) Listing added in v0.5.0

func (e *UnservedModelError) Listing() string

Listing is the column-aligned model list, which must NOT be re-wrapped.

type WakeupRequest

type WakeupRequest struct {
	DelaySeconds int
	Prompt       string // payload to re-run on wake (slash command or prompt)
	Reason       string // one-line "what I'm waiting for", shown to the user
	// Poll marks a wake-up whose only purpose is to poll background work already
	// in flight (a sub-agent or shell job). Such a wake-up is auto-cancelled if
	// that work finishes first, since the run is then resumed with the result
	// automatically — leaving the poll tick to re-dispatch the finished task.
	// Reminders and self-paced loop steps leave this false so they always fire.
	Poll bool
}

WakeupRequest asks the host to re-engage the agent after a delay — an in-session reminder/cron the agent schedules for itself. In a self-paced loop, Prompt carries the task to repeat; the host re-resolves and re-runs it when the delay elapses.

Jump to

Keyboard shortcuts

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