# VeilCLI — Issues to Fix for Studio PRO Integration

This document lists everything VeilCLI needs to expose/fix for Studio to deliver a PRO-grade experience. Implementation approach is left to the developer — this is a **what**, not a **how**.

All changes are intended to be **additive and backward-compatible** (no breaking changes to existing API consumers).

---

## Group 1 — Chat Mode Cancellation (Interrupt)

### Issue 1.1 — Chat mode has no cancel path

**Symptom:** When a client aborts a streaming chat, the agent keeps running on VeilCLI: tokens continue being spent, tool calls continue executing, state mutates.

**What's needed:**
- Chat sessions must be cancellable the same way tasks are (`POST /tasks/:id/cancel` pattern at [`api/routes/tasks.js:250-265`](../../VeilCli/api/routes/tasks.js) is the reference).
- A public endpoint: `POST /sessions/:id/cancel` that cooperatively stops the active `runLoop` for that session.
- Safe to call when no loop is running (idempotent, returns 200 or 204).

**Note:** The plumbing is 90% in place. `runLoop` at [`core/loop.js:177`](../../VeilCli/core/loop.js#L177) already accepts `cancelSignal` and checks `cancelSignal?.aborted` at lines 212 and 459. The `core/cancel.js` registry exists. Only chat mode never wires them — see `runChat` in [`core/router.js:245`](../../VeilCli/core/router.js#L245), where `cancelSignal: null` is hardcoded.

**Acceptance:**
- Calling `POST /sessions/:id/cancel` while an agent is streaming stops the loop cleanly at the next safe checkpoint.
- No orphaned in-memory state, registry entries, or DB rows.

---

### Issue 1.2 — SSE client-disconnect is silently swallowed

**Symptom:** If a Studio client closes the SSE stream from `POST /agents/:name/chat` (tab closed, refresh, network loss), the agent keeps running. The HTTP socket write throws, gets swallowed by the `try {} catch {}` inside `sendEvent`, and the loop never notices.

**What's needed:**
- When the request's underlying socket closes before the SSE stream ends, the session should be auto-cancelled.
- File involved: [`api/routes/chat.js`](../../VeilCli/api/routes/chat.js) — SSE branch at lines 42-82.

**Open design question (acceptable to defer):**
- Should there be an opt-out for callers that want the agent to keep running even if the client disconnects? If yes, expose a `keepAlive: true` flag in the request body. Default should be auto-cancel.

**Acceptance:**
- Client closes SSE stream → `runLoop` sees cancelled signal within one iteration → stops cleanly.
- No agent continues burning tokens after all SSE subscribers have disconnected.

---

### Issue 1.3 — Cancel leaves dangling tool_calls that break continuation

**Symptom:** If a chat is cancelled between an assistant message containing `tool_calls` and the corresponding `role:'tool'` result messages, the session is left in an invalid state. Any future `runChat` or `continue:true` load of that session will fail with an LLM-provider 400 error — OpenAI-compatible APIs require every `tool_call_id` to be matched by a `role:'tool'` response.

**Cancel points vulnerable to this:**
- `loop.js:459` — mid-tool-dispatch loop. Some tool_calls may have produced results; others haven't.
- Any LLM call that completes just as cancel fires — the resulting assistant message gets persisted but tool execution never starts.

**What's needed:**
- On cancel (and on any other abnormal loop exit), ensure the session's message history remains structurally valid: every `tool_call_id` in the latest assistant message must have a corresponding `role:'tool'` result row.
- Unfulfilled tool_calls should get a placeholder result that the LLM can interpret (e.g., a short string indicating the tool was cancelled — exact wording is a developer choice).

**Acceptance:**
- After any cancel, calling `POST /agents/:name/chat` again with the same `sessionId` (with or without a new message, with or without `continue:true`) does not produce a 400 from the LLM provider.

---

### Issue 1.4 — No serialization of concurrent `runChat` calls on the same session

**Symptom:** VeilCLI currently has no guard preventing two `runChat` calls for the same `sessionId` from entering the loop concurrently. The `running-sessions.js` Set tracks who's running but isn't used as a gate in `runChat` itself.

**Why this matters for interrupt:**
The user clicks Interrupt, then within the same tab immediately types a new message. The cancelled loop may still be winding down (writing cleanup rows, removing itself from `runningSessions`) when the new `runChat` arrives. Race scenarios:
- New `runChat` loads the message history before the cancelled loop has written its placeholder tool results → Issue 1.3 triggers.
- Two `runLoop` invocations for the same session briefly overlap → message ordering corruption, duplicated iterations.

**What's needed:**
- `runChat` must serialize per-session. If a loop is already running (or finishing cleanup) for the given `sessionId`, the new call either waits for completion or rejects with 409.
- Must be correct for any entry point into `runChat`, not just the HTTP route (also: `agent_spawn`, `agent_message` fallback, `agent_send` fallback).

**Acceptance:**
- Rapid interrupt → new-message sequence never produces an inconsistent session state.
- Two concurrent chat requests for the same session never both enter the inference loop.

---

## Group 2 — Orchestration Tracking

### Issue 2.1 — Sessions have no parent link

**Symptom:** The `sessions` table has no `parent_session_id` column (verified in [`migrations/001-initial.sql:9-25`](../../VeilCli/migrations/001-initial.sql#L9) and confirmed through 009). Sessions created via `agent_spawn` are orphaned — there's no DB-level record of which session spawned them.

**What's needed:**
- An additive migration adding a nullable column (or equivalent tracking mechanism) that records the spawning session's ID.
- `agent_spawn` populates this when it creates a new session. Requires passing the sender's session context into the tool's `execute()`.

**Backward compatibility:** existing rows stay NULL. Studio treats NULL as "unknown / root". No backfill required.

**Acceptance:**
- Any new session created via `agent_spawn` can be joined back to its parent with a single SQL lookup.

---

### Issue 2.2 — Inter-agent messages don't record source session

**Symptom:** The `agent_messages` table stores `from_agent` (the sender's agent NAME, a string like `"planner"`), but no `from_session_id`. When an agent has multiple concurrent sessions all messaging the same target, there's no way to distinguish which session sent which message.

**What's needed:**
- An additive column (or equivalent) recording the sender's `sessionId`.
- `agent_message` and `agent_send` tools populate it from the sender's context.

**Acceptance:**
- Every new row in `agent_messages` (after the change) has the sender session recorded alongside the sender agent.

---

### Issue 2.3 — No discrete event for spawn

**Symptom:** When `agent_spawn` creates a new session, VeilCLI emits a generic `session.created` via the event bus. There's no way for a subscriber (Studio) to distinguish "a user opened a session" from "another agent spawned this session." Studio currently has to parse tool_calls in message streams to detect spawns.

**What's needed:**
- A specific event type (e.g., `session.spawned`) emitted when a session is created via `agent_spawn`.
- Payload should include: parent session ID, child session ID, spawning agent, target agent, invocation metadata.
- Fires through the same event bus that's already forwarded to `/ws` subscribers.

**Acceptance:**
- Studio can subscribe to spawn events without parsing tool call contents.

---

### Issue 2.4 — No API for orchestration relationships

**Symptom:** Studio currently reconstructs orchestration relationships by scanning every session's messages at startup. Expensive, race-prone, duplicates logic VeilCLI is the authoritative source of.

**What's needed:**
- A read-only API to query orchestration relationships. At minimum, the full graph (all related sessions + their spawn/message edges), with support for filtering by root session and bounded depth.
- Graph response shape is up to the developer — Studio just needs to be able to render nodes + directed edges with message counts.

**Acceptance:**
- Studio can build the orchestration popup from a single API call with no client-side scanning.
- The response reflects the authoritative DB state including anything added by Issues 2.1, 2.2, 2.3.

---

## Group 3 — Streaming State Visibility

### Issue 3.1 — Clients can't query which sessions are currently streaming

**Symptom:** When a Studio client reconnects (page refresh, new tab), it has no way to know which sessions are currently streaming. The UI shows no spinner until the next `chat:stream` event arrives, which could be tens of seconds later. Meanwhile, the server has the authoritative answer in [`core/running-sessions.js`](../../VeilCli/core/running-sessions.js).

**What's needed:**
- A read-only API endpoint that returns the list of currently-active chat sessions. At minimum: the session IDs.
- Nice-to-have: include agent name and stream start timestamp so Studio can display a proper "streaming for Xs" indicator.

**Acceptance:**
- On reconnect, Studio can restore streaming indicators for the correct sessions instantly.

---

## Priority Ordering (Suggested)

**Tier 1 — Required before Studio ships interrupt fix:**
- 1.1 Chat cancel endpoint
- 1.2 Client-disconnect auto-cancel
- 1.3 Dangling tool_call cleanup on cancel
- 1.4 Per-session runChat serialization

**Tier 2 — Required before Studio ships orchestration cleanup:**
- 2.1 Sessions parent link
- 2.2 Agent messages sender session link
- 2.3 Spawn event
- 2.4 Orchestration query API

**Tier 3 — Required before Studio ships refresh-state fix:**
- 3.1 Active-streams query

All three tiers are independent and can ship in any order. Tier 1 is the most user-visible fix (interrupt is currently cosmetic).

---

## Testing Requirements (Studio-Facing Contracts)

Each fix must be verified with an end-to-end scenario — not just a unit test — because the symptoms only manifest through the full Studio ↔ VeilCLI stack.

- **Interrupt loop:** Start a chat, interrupt mid-tool-call, confirm agent stops within one iteration. Then send a new message in the same session — must succeed without error.
- **Tab close:** Start a chat, close the browser tab, confirm the agent stops within one iteration. Check VeilCLI logs for no zombie activity.
- **Rapid interrupt + new message:** Interrupt and send a new message within the same second. Neither request may produce a corrupted session.
- **Refresh mid-stream:** Start a chat, refresh the Studio page while streaming. On reconnect, the streaming indicator must appear immediately without waiting for the next event.
- **Orchestration correctness:** Run a multi-agent flow with nested spawns and cross-messaging. The orchestration popup must show every session, every edge, with correct direction and counts — no missing nodes, no phantom edges.

---

## Out of Scope for This Document

- Specific SQL schema naming — developer chooses.
- Exact event payload field names — developer chooses.
- Exact endpoint URLs — suggestions above are non-binding.
- Implementation language / framework considerations inside VeilCLI.
- Studio-side changes (tracked separately in Studio's own plan).

---

## Group 4 — Agent Tools API Redesign

**Background:** A full investigation (`.meetings/meeting-006/`) reviewed 9 multi-agent frameworks, production post-mortems from Anthropic / OpenAI / Cognition / Replit / Microsoft, and academic failure taxonomies (MAST, NeurIPS 2025). Two adversarial opinionated reviewers (opus, opposing biases) independently converged on the same API shape after debate. This section captures that design.

**Current state:** VeilCLI exposes three agent-tool primitives — `agent_spawn`, `agent_message`, `agent_send` — each taking raw string messages with no structured fields and no explicit observability or control surface.

**Target state:** Three primitives — `agent_spawn`, `agent_message`, `agent_control` — with `agent_send` removed. `agent_message` gains an async-inform flag that subsumes fire-and-forget semantics without introducing polling. An `agent_control` tool adds state queries and forced stop as a separate surface from communication.

**Backward compatibility:** This is a BREAKING CHANGE for agents that currently use `agent_send` or rely on the old flat-argument shape of `agent_spawn` / `agent_message`. Existing `agent_messages` rows and session data are unaffected. Coordinate migration with agent-prompt updates.

---

### Issue 4.1 — Drop `agent_send`

**Rationale:** Fire-and-forget async is present in only 2 of 9 researched frameworks. Its documented failure modes (re-transmission ambiguity, hidden reasoning loops) have no clean fix without adding a companion `poll` primitive. Rather than adding `poll` (which relocates reasoning loops from the conversation layer to an invisible polling layer — cheaper per iteration, harder to detect), fire-and-forget semantics are absorbed into `agent_message` via an `async_inform` flag (Issue 4.3).

**What's needed:**
- Remove the `agent_send` tool and its schema.
- Remove/archive `tools/agent_send.js`.
- Remove the fire-and-forget branches from the internal queue — all outbound messages now go through the `agent_message` code paths.

**Acceptance:**
- `agent_send` is no longer advertised in the tool registry.
- Any legacy agent prompt that references `agent_send` fails with a clear "tool not found" error (not silently).

---

### Issue 4.2 — Redesign `agent_spawn` with a structured brief

**Why:** Raw-string `message` parameter under-specifies the spawned agent's task. Production research found that ~42% of multi-agent failures originate from specification gaps — insufficient context transferred at the handoff boundary. A minimal structured brief at spawn time is the highest-leverage intervention; it costs little in token overhead and eliminates the category.

**New signature:**

```
agent_spawn({
  agent:             string,      // target agent's name (required)
  instance_name:     string,      // caller-chosen tag for this spawned instance (required)
  objective:         string,      // long-lived context about what this agent is for (required)
  success_criteria?: string,      // how the spawned agent knows it's done
  budget_override?:  object,      // see Issue 4.5 — overrides harness default budget
  initial_message?: {             // optional — if present, immediately runs first turn
    message:       string,        // first user-turn content for the spawned agent
    async_inform?: boolean,       // default false — same semantics as agent_message's flag
  },
})
```

**Return value:**
```
If initial_message is omitted:
  { instance_name, sessionId }
  (session is created with objective encoded; no LLM turn has run yet)

If initial_message present with async_inform=false (default):
  { instance_name, sessionId, response }
  (session is created AND first turn runs synchronously)

If initial_message present with async_inform=true:
  { instance_name, sessionId, notice }
  (session is created, first turn kicked off asynchronously; caller ends its turn)
```

**Field semantics:**
- `agent`: unchanged — names a registered agent.
- `instance_name`: caller's label for this spawn. Used in response payloads, traces, and `agent_control` results so the caller can reason about multiple spawned subagents by name rather than session ID. Must be caller-unique within the caller's session. Not persisted as a primary key — just a convenience label.
- `objective`: free-form prose, encoded into the spawned agent's **system prompt** as long-lived context ("what this agent is for"). Persists across the agent's lifetime.
- `success_criteria`: optional prose, also part of the system prompt — the spawned agent's completion guideline.
- `budget_override`: see Issue 4.5.
- `initial_message`: optional. Distinguishes "create the session now, use it later" from "create and run immediately."
  - Without it: `agent_spawn` creates the session (DB row, system prompt assembled) without running any LLM turn. Useful for pre-provisioning specialists before dispatching tasks.
  - With it: `agent_spawn` creates the session AND runs the first user turn using `initial_message.message` as the user-role content. The `async_inform` flag follows the same semantics as `agent_message` — false blocks for the response, true dispatches asynchronously with a static notice and delivers the eventual response as a user message to the caller's session.

**Why `initial_message` is better than two calls:**
- Single atomic operation — no risk of spawning then failing before sending the message.
- Common shorthand pattern: "spawn this specialist and have it start on X right now."
- Still composable — callers who want just a session handle omit it; callers who want spawn-and-run provide it.

**Behavior:**
- Creates a new session.
- Populates `parent_session_id` (from Issue 2.1) and emits `session.spawned` (from Issue 2.3).
- If `initial_message` is present: enters the runLoop immediately (sync or async per the flag).
- If `initial_message` is absent: no LLM call until the caller later invokes `agent_message` on the returned `sessionId`.

**Acceptance:**
- `agent_spawn` without `initial_message` creates a valid session with zero LLM tokens consumed, returning `{ instance_name, sessionId }`.
- `agent_spawn` with `initial_message` and `async_inform: false` runs the first turn synchronously and returns the response.
- `agent_spawn` with `initial_message` and `async_inform: true` returns the fixed notice immediately; the subagent's response arrives as a user message to the caller's session.
- Validation errors (missing required fields, invalid `async_inform` type, etc.) return a structured error response, not a partial success.

---

### Issue 4.3 — Redesign `agent_message` with `async_inform`

**Why:** The three legitimate use cases for inter-session messaging are (a) synchronous request-response, (b) fire-and-forget where the sender wants the response delivered later as a user-message, (c) follow-up to a busy subagent. All three can be expressed through a single primitive with an async-inform flag — no separate poll, no separate send.

**New signature:**

```
agent_message({
  sessionId:     string,     // target session (required)
  message:       string,     // prose body — no envelope required
  async_inform?: boolean,    // default false
})
```

**Return when `async_inform: false` (default, synchronous):**
```
{ content, instance_name, sessionId, finishReason }
```

**Return when `async_inform: true`:**
```
{ instance_name, sessionId, notice: "<fixed static string — see below>" }
```

**Fixed static `notice` string** (suggested wording — implementer may refine, but it MUST be a fixed template, not generated per call, and MUST be directive):

> "Your message has been queued for subagent `{instance_name}` (session `{sessionId}`). The subagent is processing asynchronously. End your current turn now — do not call additional tools or continue reasoning about this task. When the subagent responds, their output will be delivered to you as a new user message."

The prescriptive phrasing ("end your current turn now — do not call additional tools") is intentional. Ambiguous async notices cause the exact reasoning loops this design is trying to prevent.

**Response delivery path for `async_inform: true`:**
- Caller agent ends its turn normally.
- When subagent produces a response, the response content is injected into the caller's session as a new `role: 'user'` message.
- Injection uses the existing "wake-up-if-idle, enqueue-if-running" mechanism (same as current `agent_send` background watcher). Runtime already supports this.

**Follow-up to a busy subagent:**
- `agent_message` may be called with a `sessionId` whose target is currently in `runningSessions`. The message is queued to `agent_messages` with a correlation_id (existing behavior). The subagent drains it at its next safe injection point.

**Race semantics — outstanding calls between same caller/callee pair:**

| Prior call | New call | Behavior |
|------------|----------|----------|
| `async_inform: true` (still pending) | `async_inform: false` | New call supersedes. The prior promised async-user-message delivery is **cancelled**; caller now blocks, and the subagent's next response is delivered synchronously to the blocking call. Pending correlation_ids are merged. |
| `async_inform: true` (still pending) | `async_inform: true` | New message is appended to the subagent's queue. One single response covers all accumulated messages. |
| `async_inform: false` (still blocking) | anything | Cannot occur — the caller is blocked, so it can't issue another call until the first resolves. |

**Rule in one sentence:** between any caller→callee pair, at most ONE delivery promise is outstanding. Newest `async_inform:false` call wins the delivery channel (synchronous); concurrent `async_inform:true` calls accumulate in the subagent's queue and share one response.

**Acceptance:**
- Sync path behaves like current `agent_message` (correlation_id polling, fallback to `runChat` on idle session).
- Async path returns immediately with the fixed notice string; subagent's eventual response arrives as a user message to the caller's session.
- The race scenarios above are handled deterministically (not left to scheduler chance).

---

### Issue 4.4 — Introduce `agent_control` (observability + forced stop)

**Why:** Communication primitives (`spawn`, `message`) should not mix with observability and control primitives. A caller agent asking "is my subagent stuck?" or "cancel my subagent" is doing something conceptually different from sending a message. Grouping these under a single `agent_control` tool reduces tool-selection burden (one tool to reach for, three actions) while keeping the communication surface clean.

**New tool:**

```
agent_control({
  sessionId: string,
  action:    'get-state' | 'get-summary' | 'stop'
})
```

**Action: `get-state` — generic structured status (cheap, no LLM call)**

```
→ {
    isIdle:           boolean,
    numberOfMessages: int,       // messages produced by the subagent since the caller's last-sent message
    numberOfTools:    int,       // tool calls produced by the subagent since the caller's last-sent message
    instance_name:    string,
    sessionId:        string,
  }
```

**Counter semantics:** "Since the caller's last-sent message" means since the exact last message row that this caller injected into the subagent's session (linear message list). Implementation note: store the latest caller-injected message ID in `agent_messages` and count subsequent session messages per role.

**Action: `get-summary` — AI-generated activity summary (expensive, uses an LLM call)**

```
→ {
    isIdle, numberOfMessages, numberOfTools, instance_name, sessionId,
    summary: string,           // natural-language description of what the subagent is doing
  }
```

**Summary generation:** A separate lightweight summarizer model is called with the subagent's last N messages (including tool_calls). The summarizer is instructed to:
- Describe what the subagent is currently doing.
- Mention any grand-subagents it has spawned (by reading its own `agent_spawn` tool calls).
- Focus on recent activity (last few turns).

This subsumes the "show me the full spawn tree" use case — the summary describes the tree recursively when it walks the subagent's spawn calls.

**Action: `stop` — cooperative cancellation**

```
→ {
    numberOfMessages, numberOfTools, instance_name, sessionId,
  }
```

**Semantics:** Same as `POST /sessions/:id/cancel` (Issue 1.1). Cooperative — loop stops at next iteration boundary. Dangling `tool_calls` get placeholder results (Issue 1.3). After `stop` returns, the session remains valid for continuation via `agent_message` or `agent_spawn` with `continue:true`.

**Acceptance:**
- `get-state` returns in O(1) time with no LLM call.
- `get-summary` returns a human-readable natural-language summary, including grand-subagent activity when present.
- `stop` cleanly terminates the subagent's current turn without leaving the session in an invalid state.

---

### Issue 4.5 — Harness-level budget policy with per-call override

**Why:** Production incidents (GetOnStack: $127/week → $47,000/week loop) make a budget layer non-negotiable. Placing it at the harness level (configurable default policy, per-call override as escape valve) gives governance without forcing every spawner to pick numbers.

**What's needed:**

1. A harness-level default budget policy, configurable globally and per-agent. Minimum fields:
   - `max_tokens`: total tokens the subagent may consume before forced termination.
   - `max_wall_seconds`: real-time ceiling.
   - `max_spawn_depth`: how deep the spawn chain can go before further `agent_spawn` calls are rejected.

2. `agent_spawn` and `agent_message` accept an optional `budget_override` object with the same shape. Missing fields fall back to the default.

3. When a budget is exceeded:
   - The subagent's `runLoop` is cancelled via the existing cancel mechanism (Issue 1.1).
   - Dangling `tool_calls` get placeholder results (Issue 1.3).
   - The session is left queryable via `agent_control` with `isIdle: true` and a terminal state reason.

4. Emit a distinct event on budget breach (e.g., `session.budget_exceeded`) so Studio can surface this in the UI.

**Acceptance:**
- A spawn with `budget_override: { max_tokens: 1000 }` is guaranteed to consume ≤1000 tokens in its inference loop.
- Exceeding the harness default (without override) terminates the session cleanly.
- The configured defaults are discoverable via a settings/config endpoint so operators can tune them.

---

### Issue 4.6 — Trace emission for observability

**Why:** The research line "2026 is the year of harnesses" reflects a real gap: multi-agent systems without trace-level observability cannot be debugged. Trace emission doesn't require agent-facing API surface — it's an operator/admin concern — but it must exist.

**What's needed:**
- Every invocation of `agent_spawn`, `agent_message`, and `agent_control` emits a structured trace event. Minimum fields:
  - `trace_id`, `parent_trace_id` (for chain correlation)
  - `agent_name`, `session_id`, `instance_name`
  - `tool_name`, `tokens_in`, `tokens_out`, `latency_ms`
  - A truncated `envelope_summary` (first N chars of the objective / message / action)
- Traces are emitted to the existing event bus AND to a persistent trace store (table or log).
- An operator-facing endpoint lets Studio (or an admin tool) query a full trace tree:
  ```
  GET /orchestration/trace/:trace_id
  → { events: [...], tree: {root, children: [...]} }
  ```

**Relationship to Issue 2.4:** The orchestration graph API returns structural relationships (parent/child sessions, message edges). The trace API returns temporal event data (what happened, when, how much it cost). They complement each other.

**Acceptance:**
- Every agent-tool invocation appears in the trace store.
- A full spawn chain under any root trace_id can be retrieved via one API call.
- Budget-exceeded events (Issue 4.5) appear in the trace as a distinct event type.

---

### Out-of-API conventions (documented, not enforced)

Some patterns identified as high-leverage do NOT get primitives. They are documented and encoded in agent system prompts / templates instead.

**Judge / verifier pattern:** Agents that produce high-stakes outputs should be instructed (via their system prompt or the orchestrator's system prompt) to spawn a critic subagent to verify the output before returning. Example template:

> "For outputs that modify state or produce artifacts the user will rely on, spawn a verifier subagent: `agent_spawn({ agent: 'critic', instance_name: 'verify-<task>', objective: 'Evaluate whether <artifact> meets <criteria>', success_criteria: 'Clear PASS/FAIL/REVISE verdict with reasoning.' })`. Use the verifier's response to decide whether to revise or return."

**Rationale:** Production data (PwC 10%→70% accuracy uplift from independent verification) argues for making verification convenient. But a dedicated `agent_verify` primitive would be mis-applied (over-invoked, turned into a rubber-stamp) and is not necessary — the pattern composes cleanly from `agent_spawn` + `agent_message`. Instruction gets us most of the benefit; enforcement gets us false confidence.

**Context briefing on spawn:** The structured `objective` + `success_criteria` fields in `agent_spawn` naturally force the caller to articulate intent. This is the mitigation for "implicit decisions accumulating across agents" (Cognition's Flappy Bird failure mode). No further primitive is needed.

---

### Priority and Ordering (Group 4)

This group is **independent of Groups 1-3** and can ship separately. Internal dependencies:

- 4.1 (drop `agent_send`) + 4.3 (new `agent_message`): ship together. Dropping one without the replacement breaks agents.
- 4.2 (new `agent_spawn`): ships independently. Small breaking change to existing agent prompts.
- 4.4 (`agent_control`): can ship independently. Relies on Issue 1.1 (cancel endpoint) for the `stop` action.
- 4.5 (budget policy) + 4.6 (trace emission): ship together or in sequence. Both require changes to the runtime loop's invocation surface but not to the agent-facing API.

**Recommended sequencing:**
1. Groups 1-3 first (fixes, not breaks — unblocks Studio's immediate user-visible bugs).
2. Group 4 after, as a coordinated release: update agent prompts + runtime + API simultaneously.

---

### Testing Requirements (Group 4)

- **Brief structure enforcement:** `agent_spawn` without a required field returns a validation error, not a silent partial success.
- **Async delivery correctness:** After `async_inform: true`, the caller's session receives a user message with the subagent's response exactly once — never lost, never duplicated.
- **Race: async-then-sync:** Send `async_inform: true`, then immediately `async_inform: false` to the same session. The sync call must receive the response; the async user-message delivery must not fire.
- **Race: follow-up to busy subagent:** Send a message to a subagent currently running. Confirm it queues and is delivered at the next safe injection point.
- **`get-summary` accuracy:** Spawn a subagent that itself spawns a grand-subagent. Call `get-summary` on the parent session. Confirm the summary mentions the grand-subagent activity without requiring a second call.
- **Budget enforcement:** Spawn with `budget_override: { max_tokens: 500 }`. Confirm the subagent is terminated at ≤500 tokens with a clean session state.
- **Tool-registry removal:** Confirm `agent_send` is no longer discoverable by agents (appears in no tool list).
- **Spawn without initial_message:** Confirm `agent_spawn` without `initial_message` creates the session and returns without consuming any LLM tokens.
- **Spawn with initial_message:** Confirm `agent_spawn` with `initial_message` runs the first turn using that message as the user-role content, with `objective` remaining in the system prompt (not re-sent as a user message).

---

## Group 5 — Multimodal Attachments

### Issue 5.1 — No support for image, audio, file, or URL inputs in chat

**Symptom:** Studio is currently text-only for chat. Users cannot drop an image, attach a PDF, reference a file, or provide audio input. Every modern AI client offers these; without them, Studio is not competitive.

**Reference:** VeilCLI's own `ATTACHMENTS-PLAN.md` documents this gap and proposes a design. This issue does not re-design — it tracks the requirement.

**What's needed:**
- `POST /agents/:name/chat` accepts an optional `attachments` array in the request body.
- Each attachment carries: a type discriminator (image / audio / video / file / url), a media type (MIME), and either inline base64 `data` or a `url` reference.
- The runtime assembles the LLM-facing multi-part message from the attachments for providers that support multimodal (Claude, GPT-4o, etc.); for providers that don't, return a structured error (see Group 7).
- Attachments are NOT stored as blobs in SQLite (per the current "plain text in DB" policy). URL references are stored; inline base64 is stored as text with a size cap, or offloaded to a blob storage path.
- Audio must be base64 only (OpenRouter constraint already documented in ATTACHMENTS-PLAN.md).
- Images, video, files, URLs support either base64 or URL form.
- A per-agent config flag may restrict which attachment types the agent accepts.

**Acceptance:**
- `POST /agents/:name/chat` with an image attachment returns a response that includes the model's interpretation of the image.
- An attachment type the current model doesn't support returns a structured error code (`MODEL_MODALITY_UNSUPPORTED` or similar), not a silent text-only fallback.
- Session message history preserves attachments as metadata entries so they can be re-rendered on reload.
- The `GET /sessions/:id/messages` response includes attachment references (URL or size hint), not raw base64 blobs, when possible.

---

### Issue 5.2 — Tool outputs can only be strings

**Symptom:** A tool that produces a non-text artifact (generated image, audio clip, structured document) has no clean way to return it. Today it's either stringified JSON or a file path the agent has to parse.

**What's needed:**
- Tool execution result supports structured returns: a string (current behavior, default) OR an object containing text + attachments.
- Tool result attachments flow back into the next LLM call as multi-part content when the model supports it.
- The message store records the structured result, not just the string.

**Acceptance:**
- A tool that returns `{ text: "Here's the chart", attachments: [{ type: 'image', data: '...' }] }` produces a next LLM turn where the model sees both the text and the image.
- Backward compatible: tools that return plain strings continue to work unchanged.

---

## Group 6 — Per-Session Config Override

### Issue 6.1 — LLM parameters are locked at agent-configuration time

**Symptom:** Model, `model_thinking`, temperature, max_tokens, top_p, and similar LLM parameters are set per-agent in the agent's config file. A Studio user cannot say "use Opus for this one session" or "enable extended thinking just for this question." `PATCH /sessions/:id` partially covers this (model, model_thinking, compact settings) but the full LLM param surface is not exposed.

**What's needed:**
- Enumerate which parameters are session-overridable. At minimum: `model`, `model_thinking`, `temperature`, `max_tokens`, `top_p`.
- `PATCH /sessions/:id` accepts overrides for all of them.
- An introspection endpoint (e.g., `GET /sessions/:id/config`) returns the effective config for the session — computed as agent defaults + any session overrides — so Studio can display what's actually in effect.
- Overrides persist with the session. Reloading the session applies them automatically.

**Acceptance:**
- Setting `model: "opus-X"` on a session via PATCH routes the next `runChat` call to that model, regardless of the agent's default.
- `GET /sessions/:id/config` returns `{ effective: {...}, overrides: {...}, agentDefaults: {...} }` so Studio can show which values are overridden.

---

### Issue 6.2 — No per-call LLM param override (optional but valuable)

**Symptom:** Even within a session, a user might want to retry a single turn with a different model or temperature. Today that requires creating a new session or permanently mutating the session's config.

**What's needed:**
- `POST /agents/:name/chat` accepts an optional `overrides` field in the request body with the same shape as the session-level overrides.
- The override applies to THIS call only; the session's persistent config is unchanged.
- Override precedence: per-call override > session override > agent default.

**Acceptance:**
- Sending a chat request with `overrides: { model: "haiku-X" }` uses haiku for that turn even if the session is configured for opus.
- The session's next turn without an override reverts to the session's configured model.

---

## Group 7 — Structured Error Taxonomy

### Issue 7.1 — All errors collapse to generic strings

**Symptom:** Chat SSE currently emits `{ error: err.message, code: err.code || 'INTERNAL_ERROR' }`. The code is almost always `INTERNAL_ERROR`. Studio cannot distinguish "your API key expired" from "the LLM is rate-limiting us" from "your tool threw an exception" — all surface as the same UI treatment.

**What's needed:**
- An enumerated set of error codes covering common failure modes. Minimum proposed set (developer may adjust naming/granularity):

  | Code | Meaning |
  |------|---------|
  | `VALIDATION_ERROR` | Request body missing required fields or wrong types |
  | `AGENT_NOT_FOUND` | Agent name doesn't exist |
  | `SESSION_NOT_FOUND` | Session ID doesn't exist |
  | `SESSION_MISMATCH` | Session belongs to a different agent than claimed |
  | `MODE_NOT_SUPPORTED` | Agent doesn't support the requested mode |
  | `MODEL_UNAVAILABLE` | LLM provider unreachable, auth failed, or model not loaded |
  | `MODEL_MODALITY_UNSUPPORTED` | Model doesn't support the attachment type (Group 5) |
  | `RATE_LIMITED` | Upstream LLM rate-limited us |
  | `CONTEXT_LENGTH_EXCEEDED` | Message history + system prompt > model's context window |
  | `BUDGET_EXCEEDED` | Session or call exceeded configured budget (Group 4.5) |
  | `TOOL_TIMEOUT` | Tool execution exceeded its schema timeout |
  | `TOOL_INVALID_INPUT` | Tool args failed schema validation |
  | `TOOL_INVALID_OUTPUT` | Tool output couldn't be parsed as expected |
  | `CANCELLED` | Clean user cancel (not an error, but emitted through the error channel for UX consistency) |
  | `INTERNAL_ERROR` | Unhandled exception — should be rare and observable |

- Every error response (HTTP 4xx/5xx and SSE `error` events) uses the same structured shape:
  ```
  { code: <enum>, message: <human-readable>, details?: { ... } }
  ```
- The `details` object may carry context-specific fields (e.g., `retryAfter` for `RATE_LIMITED`, `maxTokens` for `CONTEXT_LENGTH_EXCEEDED`, `toolName` for tool errors).
- Document the full enumeration in a published API-error reference so Studio can build a full-coverage error-handling layer.

**Acceptance:**
- Rate-limited upstream API produces `{ code: "RATE_LIMITED", message: "...", details: { retryAfter: 30 } }` (or equivalent), not a generic INTERNAL_ERROR.
- Tool timeout surfaces as `{ code: "TOOL_TIMEOUT", details: { toolName: "bash", timeoutSec: 300 } }`.
- Studio can maintain a single switch-statement on `code` to route UI treatment; no string-matching on `message` is required for correctness.

---

### Issue 7.2 — Errors mid-stream don't include where in the turn they happened

**Symptom:** When an error fires during a streaming response, the client receives `chat:error` but has no structured indication of WHERE in the loop it occurred. Was it the LLM call? A tool execution? Post-processing? Studio can't show a precise failure state to the user.

**What's needed:**
- Error events carry a `phase` field identifying the stage: `inference` (LLM call), `tool_execution`, `tool_dispatch`, `compaction`, `post_processing`.
- When phase is `tool_execution`, include the tool name and call id.
- When phase is `inference`, include the iteration number.

**Acceptance:**
- Studio can render a precise error bubble like "Tool `bash` timed out (call id `call_abc`) on iteration 4" from the error event alone, without parsing the session history.

---

## Updated Priority Ordering

**Tier 1 — Studio immediate unblockers (interrupt + streaming):**
- 1.1, 1.2, 1.3, 1.4 (cancel plumbing)
- 3.1 (active-streams query)
- 7.1 (error taxonomy — so Studio can render the new cancel-related errors properly)

**Tier 2 — Studio orchestration:**
- 2.1, 2.2, 2.3, 2.4 (relationship tracking + graph API)

**Tier 3 — Studio chat parity with modern AI clients:**
- 5.1, 5.2 (multimodal)
- 6.1, 6.2 (per-session/per-call config override)

**Tier 4 — Coordinated agent-tools release (breaking):**
- 4.1 through 4.6 (agent-tools redesign + budget + trace)

**Tier 5 — Quality of life:**
- 7.2 (error phase context)

Tiers are independent — can ship in any order — but the numbering reflects which unblock Studio features first.

---

## Updated Out of Scope

Still deferred to a later cycle (flagged in the meeting-006 analysis as "can be retrofitted additively"):
- Multi-user / access control (per-user session scoping, API key management, per-user rate limits).
- Session tagging / metadata / search (additive JSON column + endpoints).
- Event backfill on reconnect (events-since-cursor endpoint).
- Durable task checkpointing / crash resume.
- Tool result caching / memoization.
- Agent evaluation / testing harness.
