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: 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
packetcodeengine binary. For Ollama, a daemon answering onlocalhost: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.
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).
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.
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).
"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:
- The picker's badge for your row reads ready and Launch is enabled.
- The first turn streams: assistant text appears progressively and the context ring on the composer's context strip moves.
- The statusline under the input box prints
ctx … · in … · out …once a turn has completed (src/lib/usageStatusline.ts:7). - 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 |
Related#
- 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.