PacketBenchDocs

Agents & conversations#

A conversation is PacketBench's structured exchange with an API provider: you pick a row in the agent picker, type a prompt, and the app streams back assistant text, tool calls, permission prompts and reviewable diffs. Nine rows sit behind that picker across three transports, and they all drive the same chrome — the row decides who answers, not what the surface can do. This page walks the loop: choosing a row, setting the posture, sending the turn, watching the tools, answering a prompt, and reviewing what changed.

The Agents view with the conversation sidebar, a streaming transcript and the composer's context strip

The Agents view: the conversation list on the left, the transcript in the middle, and the composer with its context strip and posture chip at the bottom.

Before you start#

  • An API key for at least one provider, in the OS keyring via Settings → Providers and Models — see Install & first run.
  • A project path: a local folder, or an SSH server with a remote path. The agent's tools all run inside that scope.
  • For PacketCode (ACP), a reachable packetcode engine binary. For Ollama, a daemon answering on localhost:11434.

No API-agent row uses an interactive subscription login — every keyed row authenticates with an API key you supply, which is why the inline Log in button is gone from this surface (composer/Composer.tsx:549). Interactive claude login still exists in Settings for PTY CLI sessions, a different execution model entirely (Workspaces & terminals).

Choose a row in the agent picker#

The picker is the Provider dropdown on the launch composer. It renders three labelled groups from PROVIDER_GROUPS (src/components/agents/composer/utils.ts:62); the rows come from API_PROVIDERS (src/lib/api-models.ts:45). Nine are live.

Group Row Transport Credential
Anthropic Claude Agent SDK (API) Node sidecar Anthropic API key
Anthropic Claude (API) In-process Rust Anthropic API key
OpenAI OpenAI (API) In-process Rust OpenAI API key
OpenAI OpenAI Agents SDK (API) Node sidecar OpenAI API key
Other OpenRouter In-process Rust OpenRouter API key
Other MiniMax (Token Plan) In-process Rust MiniMax API key
Other Ollama (Local) In-process Rust None — a local daemon
Other PacketCode (ACP) ACP subprocess None — the engine's own
Other Custom endpoint (OpenAI-compatible) In-process Rust Optional

Model lists live on the same rows and move faster than this page can; read src/lib/api-models.ts for the current set. Three rows carry no static models on purpose — Ollama and the custom endpoint fetch theirs at runtime, and PacketCode enumerates the engine's own. That row used to seed Anthropic ids so the picker would render something, which was a guess at another program's configuration and 404'd against an engine with no Anthropic provider (src/lib/api-models.ts:177). Picking a row also resets the model to that row's first entry.

Two ids do not mean what they say#

Persisted conversations store the internal AgentCli id verbatim and resume with it, so two ids are frozen past their meaning.

api-claude-oauth is not an OAuth row. Since July 2026 it is the Claude Agent SDK on the api-key-anthropic keyring entry; only the credential changed (src/lib/api-models.ts:47), so its badge probes the Anthropic keyring rather than the claude-oauth credential file (src/stores/agentTaskStore.ts:305). That OAuth probe is not dead; it still gates PTY CLI launches.

api-openai-codex is Removed — withdrawn in July 2026 with its sidecar provider and registry entry (agentTaskStore.ts:202) and deliberately not aliased onto a live row, because an alias silently moves a conversation onto different credentials. Conversations on it stay readable and refuse new turns with a system message rather than failing silently. Never derive a backend provider id by stripping api- from a row id: it is wrong for the default row, it bills the wrong key, and a repository fence fails the build if you try.

The auth badge decides whether Launch is live#

Every row carries a live auth badge, refreshed on provider-auth:changed and re-probed when you open the dropdown (hooks/useProviderAuthStatus.ts:38). Not-ready rows dim but stay selectable. Launch is enabled only for a ready badge, and its disabled tooltip is the backend's own hint verbatim rather than a fixed "add a key" sentence — a not-ready badge is not always a missing key. Ollama reports service_down when nothing answers on localhost:11434, the custom endpoint reports missing_key when no base URL is set, and PacketCode the same when its engine binary cannot be found (Composer.tsx:554). With nothing selected and no pinned provider, the launcher probes seven rows in order and picks the first ready credential, falling back to api-minimax on MiniMax-M3 (src/components/views/AgentsView.tsx:29).

Set the posture before you launch#

The launch composer's Advanced accordion holds three controls; it stays collapsed while all three sit at their defaults. The four mode segments come from MODE_META (composer/utils.ts:20), and launchConversation maps them onto the two flags the backend receives (src/lib/launchConversation.ts:119).

Mode Description shown Launches with
Agent "Full tools — read, write, run commands" permissionMode: "auto"
Ask "Read-only — no edits or commands" planMode: true
Manual "Every risky tool requires your approval" permissionMode: "ask_for_risky"
Plan "Produce a structured plan first, then execute" planMode: true

Ask and Plan launch identically; they differ in what you do with the turn, not in what the backend is told. The four buttons also cannot express deny_all or allow_all — those are reachable only from the mode chip inside a running conversation.

A profile contributes the system prompt, an allowed-tools list, the memory flag, and optionally a pinned model that overrides the launcher's selection (launchConversation.ts:104); the mode wins over the profile for plan and permission posture. Three built-ins ship — Default (all tools, memory on), Scout (read-only tools, plan mode) and Reviewer (read-only tools, plan mode, memory off) — at src/stores/profileStore.ts:23. They can be cloned but not edited, and picking one pins it as the global default.

The Local / Worktree segment decides where edits land (ComposerModePicker.tsx:13). Worktree provisions <project>/.pkt-worktrees/<conversationId> on a fresh pkt/<conversationId> branch off HEAD and stamps that provenance onto the conversation so the work can be landed or discarded later (launchConversation.ts:216). It renders only for a local project and only on the launch composer — a running conversation cannot be moved into a worktree afterwards.

Warning

If worktree provisioning fails the launch still proceeds, in the project root, with no worktree stamped. That silently revokes the isolation you asked for, so the fallback is reported through the launch-error toast and not only to the console (src/lib/launchConversation.ts:236).

Send the turn#

The composer is one component with a launch variant and a chat variant, so @ and / behave identically whether you are starting an agent or replying to one (composer/Composer.tsx:98). The keyboard handler is shared too.

Keys Effect
Enter Send, or launch
Shift+Enter Newline
Ctrl+Enter Send, from anywhere in the box
@ File mention, rooted at this session's project
/ Slash commands, prompt templates and project skills
Shift+Tab Cycle the permission posture — chat variant only
/ Recall your own prior turns — chat variant only
Esc Dismiss a popover, or abandon a recalled turn

While a popover is open, Enter and Tab always either pick the highlighted row or dismiss an empty list, so a half-typed @query is never sent by the keystroke that was trying to complete it (buildComposerKeyboardHandler.ts:84). Images paste or drag into either variant, capped at 5 MB each. The mic button is the same control in both shells — see Dictation & analytics.

Nine builtin slash commands exist in the chat variant — /plan, /permissions, /model, /compact, /review, /history, /clear, /new and /help (composer/slashCommandSource.tsx:72) — and they deliberately shadow an engine command of the same name, because /model and /permissions are how the composer's own pickers open from the keyboard. /compact trims the local transcript view only, keeping the last four messages and saying so; the backend's context is untouched (slashCommandHandlers.ts:146). Project commands are Markdown files under <project>/.packetbench/commands/, overriding same-named globals. @ inserts the literal path text — nothing resolves it into file content, so the agent still calls read_file — and neither trigger opens where the session cannot serve it.

What the system prompt is made of#

At launch the app assembles one system prompt from three layers, lowest first (src/stores/agentTaskStore.ts:1099): the AGENTS.md / CLAUDE.md cascade (~/.claude/, then every candidate from the git root down to the working directory, capped at 32 KiB and skipped for SSH conversations); the memory brief, gated on the conversation's memory flag; then the profile's prompt, last, so it wins conflicts of intent.

The result is frozen onto the conversation and replayed on every later turn, so the memory brief is composed once and never recomposed mid-session (agentTaskStore.ts:2176). The MCP trust snapshot freezes at the same moment — which is why editing Settings mid-session cannot widen a running agent's authority — and cost guardrails can refuse the launch outright. A message typed while a turn is streaming is queued, not dropped, and carries no attachments, so staged images are held for the next live send.

Watch the tools it runs#

An in-process API agent gets six workspace tools — read_file, write_file, edit_file, list_directory, bash, grep — plus web_fetch, spawn_subagent, create_pull_request, three task tools, whatever mcp__* tools the trust snapshot allows, any agent_* custom agents, and three gh_* GitHub readers (src-tauri/src/core/tool_runtime.rs:121). A profile's allowed-tools list filters that set before the model sees it. Every path resolves against the project root and is refused if it escapes, and web_fetch runs from the PacketBench process rather than through SSH, blocking loopback, private and link-local addresses (src-tauri/src/core/tool_web.rs:1).

Warning

edit_file is not available on an SSH-backed conversation. A remote edit needs a read-modify-write round trip the approval gate cannot preview, so it fails closed rather than being half-wired (tool_runtime.rs:295); the agent is told to use read_file plus a full write_file instead.

Read, search and list calls never render individually. They collapse into one exploration rollup — "Explored 3 files, 2 searches, 1 listing" — which is both their live representation and their settled summary, so settling does not swap dozens of cards for one and destroy your scroll position (ExplorationRollupCard.tsx:35). Everything else gets a card: bash shows the command and the real exit code; spawn_subagent its task and summary; task_list the checklist; three or more settled file edits collapse into one "Edited N files" card, and a single edit becomes a chip that deep-links into the review surface.

Transcript density is one global setting — Summary, Normal, Verbose — cycled with Ctrl/Cmd+Shift+O. Normal collapses every card except the task list; Summary hides extended thinking and disables expansion; Verbose mounts cards expanded with raw tool inputs (src/hooks/useAgentTabHoists.ts:58).

What the session is pointed at lives on the composer's context strip rather than the header: project, SSH host, git branch with a dirty dot and a behind-upstream count polled every 30 seconds, an MCP count, and the context-window ring (composer/ContextStrip.tsx:47). The MCP chip is read-only on purpose — PacketBench has no per-session MCP consent toggle, so a clickable chip would be a false affordance (ContextStrip.tsx:44).

Answer a permission prompt#

Postures inside a running conversation are richer than the four launcher buttons. The mode chip on the composer row derives its label from the conversation's (planMode, permissionMode) pair; approveWrites is an orthogonal flag that survives every mode change untouched (agentModeChipUtils.ts:117).

Posture Chip description Sends
Default "Full tools — read, write, run commands" permissionMode: "auto"
Plan "Read-only exploration; no edits or commands" planMode: true
Manual "Every risky tool requires your approval" ask_for_risky
Deny "Risky tools are refused without prompting" deny_all
Yolo "Allow-all — never prompt for permissions" allow_all

Risky means bash, write_file and edit_file (src-tauri/src/commands/api_agent.rs:2005). The chevron beside the chip — also opened by /permissions — picks any offered posture directly and toggles Approve writes, "Confirm each file write before it lands".

The chip shows only the postures the backend will honour: capabilitiesFor intersects PacketBench's five with what an ACP engine advertised, because picking a posture outside that set is a silent no-op — Rust drops the unaccepted override and the session lands on the engine's own default (src/lib/agentCapabilities.ts:166). Where the derived posture is not offered, the chip stops claiming one and renders the provider's own word, or a neutral Provider default.

Warning

The Shift+Tab cycle is not narrowed the same way. The chip computes its "Shift+Tab → …" tooltip with nextModeIn, restricted to the offered set (AgentModeChip.tsx:171), but the handler it calls cycles the unrestricted five (AgentChatPane.tsx:276). On a restricted session, cycling can land on a posture the popover does not list and the backend will refuse. Pick the posture from the popover instead.

Not every tool call becomes a prompt#

Before a request can block you it is classified into one of three tiers (src/lib/approvalTiers.ts:9): read (Read, Glob, Grep, LS, NotebookRead, TodoWrite, BashOutput, read_file, list_directory, grep), edit in project (a file edit whose every path resolves inside the project), and blocking — shell, network, out-of-project writes, and anything that cannot be positively classified, including MCP tools and unknown names. The posture then decides which tiers prompt:

Posture read edit in project blocking
Plan, Deny prompt prompt prompt
Manual auto-allow prompt prompt
Default, Yolo auto-allow auto-allow prompt

A prompt for Grep teaches people to click Allow without reading, which is why reads never raise one (approvalTiers.ts:14). Auto-allowed calls still reach the provenance audit trail; they only skip the queue. Evidence that crossed a trust boundary earlier in the turn forces a prompt regardless of tier (src/lib/provenance.ts:351).

The decision itself#

An approval card renders inline in the transcript at the point in the turn where it happened. Allow is a split button: the left half allows once, and the chevron opens three scopes — Allow once, Allow for this session (labelled "(no rule saved)"), and, where a rule can be derived, Always allow <pattern>, which appends the pattern to this conversation's allowedTools and takes effect on the next turn (PermissionPrompt.tsx:276). That allowlist is per conversation; there is no project or global rule store for it.

Deny sits at equal visual weight, because this is a safety gate and PacketBench does not know which answer is right (PermissionPrompt.tsx:336). with reason… turns the denial into steering text folded into the synthetic tool result, so the model is redirected rather than stalled. A bash command matching a conservative destructive-pattern list gets a Destructive badge and a sentence naming the consequence, because a colour change alone does not slow a reflexive click (src/components/agents/permissionPatternHint.ts:67).

Warning

"Allow for this session" keeps no memory on Claude rows. The Agent SDK provider resolves allow_always exactly as allow_once (agent-sidecar/src/providers/anthropic.ts:1069); only the OpenAI Agents SDK provider remembers the tool for the rest of the session. On a Claude row the same tool will ask again.

Two or more queued permissions add a rollup above the first card — "N pending tool permissions" with Allow all and Deny all (PendingApprovalsRollup.tsx:29). Pending file edits are deliberately not mixed in; they belong to the review surface's Keep/Undo pair.

Y answers the head of the queue and N denies it. Exactly one document-level handler owns that pair; it ignores modifier keys and anything typed into a field, and in a workspace mosaic it is armed only for the focused tile — two live handlers would mean every open tile answering one keypress (chat/PendingApprovalsSection.tsx:22). Permission prompts outrank pending edits, so the review bar's own Y/N stays passive while any permission waits. Out of view, a floating "N pending · Y allow · N deny" pill, an amber header count and a Needs you sidebar section all point back at it.

Cancel N on the composer row is distinct from Stop: it drains the parked prompts as denied — the model receives "User cancelled this tool" for each and keeps generating — where Stop kills the whole turn.

Approving a plan#

In plan mode, a settled turn containing a ## Plan, ## Files to change or ## Steps heading grows an approval menu (planDetection.ts:10) offering Approve and execute (auto), Approve and accept edits (auto, approve-writes off), Approve, review each edit (ask_for_risky) and Keep planning. The first two apply near-identical policy — approve-writes is already off by default — so what separates them is the instruction sent to the model, not an enforced guarantee. Approval lifts plan mode first, then applies the posture, then sends, because sidecar sessions map set_plan_mode(false) onto permission mode "default": a posture applied earlier would be reset on the backend while the frontend kept the requested one, a silent permission desync (src/stores/agentPlanStore.ts:118).

Review what the agent changed#

There is one review surface, reachable three ways: the header diffstat chip, a per-file chip in the transcript, and the review bar above the composer. All three open ReviewSurface, a single multibuffer pane covering every file the conversation touched (review/ReviewSurface.tsx:46), and it mounts only for API conversations. The review bar reads N files · +X/-Y · Review, turns amber while gated edits await a decision, and once the conversation settles grows a Finish → Commit… button (review/ReviewBar.tsx:35).

Sections come in two kinds and share one verb pair, Keep and Undo; blocking Allow/Deny prompts stay in the permission cards and are never mixed in (ReviewSurface.tsx:52). A pending section is an edit the agent is still parked on. An applied section is one already on disk, with a per-file Viewed checkbox that collapses it and resets the moment that file is edited again — the same reset GitHub applies (src/stores/reviewStore.ts:29).

Every section is per-hunk: each hunk carries a keep toggle, and the primary button relabels to Keep 2/5 for a partial selection. Keeping a strict subset merges the accepted hunks into new content before applying; keeping none degrades to a reject, so the model is told the truth rather than that an edit landed (ReviewSurface.tsx:387). Edit baselines are held in memory only (src/stores/editBaselineStore.ts:21): after an app restart applied sections degrade to a read-only disk-versus-proposed preview and per-hunk Undo is gone.

Hovering any diff row offers a + that opens a one-line comment box. Queued comments show as chips above the composer and, on the next send, fold into the prompt as a File comments: preamble of path:line — text anchors; the diff is already in the history, so the anchors are enough (agentTaskStore.ts:1308).

Markdown preview, the editor, applied-file review and Undo all read local disk, so on an SSH-backed conversation — where projectPath is the remote path — those controls stay visible and disabled with the tooltip "Not yet available for SSH workspaces" rather than being hidden or pointed at an unrelated local file (src/lib/remoteConversation.ts:5).

Landing or discarding the work#

A conversation launched in worktree mode has one endings surface, opened from Finish → Commit… or Open Git ending (src/components/workspace/WorktreeLifecycleBar.tsx:17). Merge back squash-merges into the root checkout, force-deletes the branch and removes the worktree — disabled for SSH conversations, where remote commit, push and pull are not implemented. Create PR pushes the branch and opens a draft PR. Keep for later leaves the worktree alone. Discard removes it, escalating to a second confirm when the tree is dirty.

Deleting the conversation also discards its worktree. The confirm names the path, the branch, and first, because it is the only unrecoverable consequence, any uncommitted changes; a failed dirty-check is disclosed as possibly-dirty rather than as clean (src/lib/conversationWorktreeDisclosure.ts:1).

Verify#

The loop is working when all four of these hold:

  1. The picker's badge for your row reads ready and Launch is enabled.
  2. The first turn streams: assistant text appears progressively and the context ring on the composer's context strip moves.
  3. The statusline under the input box prints ctx … · in … · out … once a turn has completed (src/lib/usageStatusline.ts:7).
  4. A conversation started in worktree mode shows a pkt/<id> branch on the context strip, not your working branch.

There is no dollar figure by default. PacketBench removed its cost reporting surface on 2026-07-31; cost is still measured and still drives the budget guardrails, it is not displayed, and the $ segment ships behind an opt-in that defaults off (src/lib/usageStatusline.ts:12).

Conversations persist to ~/.packetbench/conversations/<id>.json, one file each, on a 500 ms debounce (src/stores/agentConversationPersistence.ts:48). Close the app, reopen it, and the transcript is there.

If it does not work#

What you see What happened
Launch is disabled with a hint The credential probe is not ready. The tooltip is the backend's own remedy — a missing key, a stopped Ollama daemon, an unset base URL, or an unreachable engine binary
"Sidecar restarted — please resend your message…" The Node sidecar died and the supervisor fanned an error out to every owned session. Resend; restarts are capped at three in 60 seconds — see Agent event contract
The conversation refuses new turns and says so It is on a retired provider id. Start a new conversation on a live row; the old transcript stays readable
A worktree launch fell back to the project root Provisioning failed. The agent is running in your main checkout — stop it before it writes
The posture chip says Provider default PacketBench's override was refused. Pick a posture the popover lists
Preview, editor, file open or Undo are greyed out The conversation is SSH-backed and those surfaces read local disk
  • Core concepts — conversation, agent row, profile and worktree, defined once.
  • Agent event contract — the api-agent:* events every transport emits.
  • Workspaces & terminals — the other execution model, and conversation tiles in a mosaic.
  • Flight Deck — attempts, which are ordinary API conversations run in parallel.
  • MCP hub — where a conversation's tools come from.
  • Memory — what gets injected into the system prompt at session start.
  • Settings — auto-failover, auto-archive, cost guardrails, the default MCP set.