mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
docs(agent): add approved pico implementation handoff
This commit is contained in:
parent
f3c672245d
commit
4819cc877e
7 changed files with 2298 additions and 232 deletions
|
|
@ -1,220 +0,0 @@
|
|||
# Handoff: outstanding pico decisions
|
||||
|
||||
Remaining additions for `packages/agent/docs/pico/`, to reconcile with `pico-v3.md`,
|
||||
`pico-usage-guide.md` and `pico-work.md` before implementation. Incorporated decisions and rejected
|
||||
or superseded proposals are omitted. Two parts: harness additions, then presentation additions
|
||||
that also require task provenance and typed preview access.
|
||||
|
||||
---
|
||||
|
||||
# Part 1 — Harness
|
||||
|
||||
## 1.1 Preview delivery is coalesced
|
||||
|
||||
Scratch commits are never delayed, but `task_output` delivery to watchers is coalesced per task to
|
||||
a frame interval (the rules in `docs/mobile-handoff/01-harness/04-tool-output/rate-limiting.md`),
|
||||
so a fast stream produces one event per frame instead of one per token. Without this a TUI redraws
|
||||
per token.
|
||||
|
||||
Where: §9.4, next to the tracker paragraph.
|
||||
|
||||
## 1.2 Hook handlers receive the task's scratch
|
||||
|
||||
A handler that waits on a person (approval, a question) must be able to memoize its answer durably,
|
||||
or a crash mid-wait re-asks and a crash after the answer asks again. Give handlers `scratch` and
|
||||
the task id in their context, and state the pattern: read-then-set in one scratch commit, first
|
||||
writer wins; a crash before the answer asks again, a crash after replays to the same answer; the
|
||||
memo retires with the task. This replaces `plugins.md`'s `memoOnce` and invocation-memo API, and it
|
||||
is the same shape the question extension needs.
|
||||
|
||||
```typescript
|
||||
h.hooks.on(toolKind, 'before_tool', async ({ toolName, args, scratch, taskId }, call) => {
|
||||
if (toolName !== 'bash') return;
|
||||
const decision = scratchValue<'allow' | 'deny'>('approval');
|
||||
let d = await scratch(sc => sc.value(decision).get(), call);
|
||||
if (d === undefined) {
|
||||
const answer = await approvals.ask(taskId, args, call); // keyed instance every presentation observes
|
||||
d = await scratch(async sc => {
|
||||
const stored = await sc.value(decision).get();
|
||||
if (stored !== undefined) return stored;
|
||||
sc.value(decision).set(answer);
|
||||
return answer;
|
||||
}, call);
|
||||
}
|
||||
return d === 'allow' ? { args } : { block: { reason: 'denied by user' } };
|
||||
});
|
||||
```
|
||||
|
||||
Where: §8.7 and the guide's Hooks section.
|
||||
|
||||
## 1.3 Budget ownership transfer
|
||||
|
||||
The work plan already flags arbitrary tool-work adoption as unresolved. Give it its own package
|
||||
when the ownership design is settled: adopting a non-delegating tool's in-flight work after the
|
||||
budget expires needs an explicit transfer of effect and sink ownership before the source invocation
|
||||
releases its slot. The job-first path ships first.
|
||||
|
||||
Where: `pico-work.md`.
|
||||
|
||||
---
|
||||
|
||||
# Part 2 — Presentation
|
||||
|
||||
## 2.1 Task provenance is `byTaskId`, not `JobState.origin`
|
||||
|
||||
`origin: { tool, task, callId }` is a kind-specific enum for something the harness knows
|
||||
generically. Replace it with a field the `Tx` fills from the committing invocation:
|
||||
|
||||
```typescript
|
||||
interface Task { …; readonly byTaskId?: Id } // task whose commit created it; absent = created outside any task
|
||||
```
|
||||
|
||||
A tool's job has `byTaskId` = the tool task, so a renderer reads that task, `toolKind.is(t)`, and
|
||||
uses the tool's component. A schedule is a job with `every` in its own state; nothing else marks it.
|
||||
Subagent tasks chain `byTaskId` up to the `run`/`spawn` tool. `Entry.byTaskId` already exists; this
|
||||
makes tasks symmetric. Remove `origin` from `JobState`, from the bash example in §8.3 and from the
|
||||
notice text (derive it from the creating task).
|
||||
|
||||
Where: §5.1, §8.3, §8.6.
|
||||
|
||||
## 2.2 `!cmd` is a client-owned task kind
|
||||
|
||||
User-run bash is initiated by the UI, not by the model or a tool, so it is its own kind owned by
|
||||
the coding agent (or its bash plugin), sharing the exec-into-scratch helper with `jobKind`. Its
|
||||
settlement records the transcript write in the same commit as `settle`, so a client that dies in
|
||||
between leaves nothing to repair. Use `tx.write`: it appends immediately when safe, otherwise queues
|
||||
until the next turn boundary:
|
||||
|
||||
```typescript
|
||||
type UserBashInput = { cmd: string; cwd: string; includeInContext: boolean; limits?: ShellOutputLimits };
|
||||
type UserBashStates = UserBashInput & (
|
||||
| { status: 'planned' }
|
||||
| { status: 'running' }
|
||||
| { status: 'done'; exitCode: number }
|
||||
| { status: 'killed' }
|
||||
| { status: 'lost' }
|
||||
);
|
||||
function userBashInput(state: UserBashStates): UserBashInput {
|
||||
return { cmd: state.cmd, cwd: state.cwd, includeInContext: state.includeInContext,
|
||||
...(state.limits === undefined ? {} : { limits: state.limits }) };
|
||||
}
|
||||
|
||||
const userBashKind = defineTaskKind<UserBashStates>()({
|
||||
kind: 'pi.user_bash',
|
||||
initialStatus: 'planned',
|
||||
roles: { planned: 'start', running: 'inflight', done: 'terminal', killed: 'terminal', lost: 'terminal' },
|
||||
preview: { init: async scratch => (await scratch.value(output).get()) ?? emptyOutput() },
|
||||
|
||||
async execute(task, runtime, call) {
|
||||
await runtime.commit(tx => tx.patch(task, 'running', userBashInput(task.state)), call);
|
||||
const result = await execIntoScratch(runtime, task.state, call); // same helper as jobKind
|
||||
const out = runtime.preview.state;
|
||||
await runtime.commit(tx => {
|
||||
tx.write(userBashKind.entry, {
|
||||
data: { cmd: task.state.cmd, exitCode: result.exitCode, output: out },
|
||||
model: task.state.includeInContext ? [userBashMessage(task.state.cmd, out, result.exitCode)] : undefined,
|
||||
});
|
||||
tx.settle(task, 'done', { ...userBashInput(task.state), exitCode: result.exitCode });
|
||||
}, call);
|
||||
},
|
||||
async recover(task, runtime, call) {
|
||||
await runtime.commit(tx => tx.settle(task, 'lost', userBashInput(task.state)), call);
|
||||
},
|
||||
async abort(task, runtime, call) {
|
||||
await runtime.commit(tx => tx.settle(task, 'killed', userBashInput(task.state)), call);
|
||||
},
|
||||
});
|
||||
|
||||
// the "!" handler
|
||||
await c.commit(tx => tx.task(userBashKind, {
|
||||
background: true, state: { status: 'planned', cmd, cwd, includeInContext, limits },
|
||||
}), call);
|
||||
// Escape → h.abortTask(id, call); the renderer's task_start / task_output / entry cases do the rest
|
||||
```
|
||||
|
||||
This is the pattern for any client-owned "thing that runs and should appear in the transcript": a
|
||||
kind, not a flag on a built-in. Use it as the guide's example of a client-defined task kind; it is
|
||||
better than the reminder.
|
||||
|
||||
Where: the guide's "Writing Kinds".
|
||||
|
||||
## 2.3 Two rendering styles are first-class
|
||||
|
||||
§9.4 presents `apply(view)` diffing as the way to render. It is one of two, and the other is the
|
||||
coding agent's interactive mode unchanged:
|
||||
|
||||
- **Event-driven**: `task_start` on a generation makes a streaming component; `task_output` updates
|
||||
it from the view's preview; `task_start` on a tool makes a tool component; `task_end` retires it;
|
||||
`entry` finalizes. Bookkeeping is one streaming component and one map, as today.
|
||||
- **View-driven**: `apply(view)` diffing against what was last drawn, for clients that attach
|
||||
mid-turn or resnapshot.
|
||||
|
||||
The view is authoritative in both: it is what a client renders on attach, and it has been folded
|
||||
before the listener runs. Rewrite the "The view is authoritative and the events are wake-ups"
|
||||
paragraph accordingly, and lead the guide's Rendering section with the event switch.
|
||||
|
||||
Mapping from `packages/coding-agent/src/modes/interactive/interactive-mode.ts` (`handleEvent`,
|
||||
`renderSessionEntries`):
|
||||
|
||||
| interactive mode | pico |
|
||||
|---|---|
|
||||
| `agent_start` / `turn_start` / `message_start(assistant)` | `task_start`, `generationKind.is(t)` |
|
||||
| `message_update`, including tool components born from streaming tool calls (keyed by call id) | `task_output` on the generation; preview is the partial `AssistantMessage` |
|
||||
| `message_end`; aborted/error → pending tools get error results, else `setArgsComplete` | `entry`, `assistantKind.is(e)` |
|
||||
| `tool_execution_start` / `_update` / `_end` | `task_start` / `task_output` / `task_end` on the tool task; result via `entry`, `toolResultKind.is(e)`, keyed by `e.key` |
|
||||
| `compaction_start` / `_end` | `task_start` / `task_end`, `collapseKind.is(t)`; re-render on the summary `entry` |
|
||||
| `auto_retry_start` / `_end` | `task_update` to/from `retry_wait`; failure on `task_end` `failed` |
|
||||
| `summarization_retry_*` | `task_update` on the collapse task |
|
||||
| `agent_end` / `agent_settled` | after any `task_end`: no live foreground task in the view |
|
||||
| `queue_update` | `inbox` |
|
||||
| `thinking_level_changed` | `value`, `addr === generationKind.config.thinking` |
|
||||
| `session_info_changed` | session watch `value` |
|
||||
| `entry_appended(custom)` | `entry` of a plugin or unregistered kind |
|
||||
| `bash_execution_update` | `task_output` on a `pi.user_bash` task (2.2) |
|
||||
|
||||
One map more than today: tool task id → call id, because tool components are created while the
|
||||
assistant message streams (by call id) and only later associated with their task.
|
||||
|
||||
## 2.4 Typed preview access
|
||||
|
||||
Renderers should not cast. The view offers a typed accessor witnessed by the kind, alongside the
|
||||
raw map the reducer and the wire use:
|
||||
|
||||
```typescript
|
||||
interface ConversationView {
|
||||
readonly previews: ReadonlyMap<Id, JsonValue>; // raw, kind-free
|
||||
preview<S extends TaskStateBase, H extends HookPoints, C extends ConfigSpec, P, R extends TaskRoles<S>>(
|
||||
kind: TaskKind<S, H, C, P, R>, task: Id): P | undefined; // typed; undefined if missing or another kind
|
||||
}
|
||||
const msg = w.view.preview(generationKind, event.task); // AssistantMessage | undefined
|
||||
const out = w.view.preview(toolKind, event.task); // ToolOutputState | undefined
|
||||
```
|
||||
|
||||
`kind.is(task)` already narrows `task.state` in the `task_*` cases.
|
||||
|
||||
Where: §9.4 and every example that currently casts.
|
||||
|
||||
## 2.5 Renderer registry and layouts
|
||||
|
||||
Rendering has two jobs and the client keeps them apart:
|
||||
|
||||
- **Renderers**, one per kind, replaceable: `renderers.entry(kind, previous => …)` and
|
||||
`renderers.task(kind, previous => ({ start, output?, update?, end }))`. Built-ins register the
|
||||
same way, so a plugin replaces or wraps one by re-registering under the same kind and receiving
|
||||
the previous renderer. An unregistered kind gets a generic fallback: model text or collapsed
|
||||
data for entries, kind + status + preview for tasks. Renderers never parent components.
|
||||
- **Layouts**, pure functions over the ordered list of rendered blocks:
|
||||
`Layout = (blocks: Block[], view) => LayoutNode[]`, where a node is a block or a group
|
||||
`{ key, label, collapsed, children }`. Applied in registration order; the client reconciles the
|
||||
tree with the chat container by key and keeps open/closed state by key.
|
||||
|
||||
Grouping tool calls per exchange and collapsing a run until its final answer are each about ten
|
||||
lines of layout, and they compose. Hydration on attach and live rendering share one path: build
|
||||
blocks from `view.entries` and `view.tasks`, run the layouts, reconcile. The non-rendering
|
||||
concerns of today's handler (Escape-handler swaps for retry and compaction, the working indicator,
|
||||
the post-summary re-render, shutdown checks) stay in the mode; a plugin does not get to hijack
|
||||
Escape.
|
||||
|
||||
This is presentation-side only, the contribution shape `plugins.md` already describes for tool
|
||||
renderers, keyed by kind string on the wire and by kind object in code. It belongs in the guide's
|
||||
Rendering section and in `pico-work.md` package 20 as the TUI's parity target.
|
||||
305
packages/agent/docs/pico/pico-rendering.md
Normal file
305
packages/agent/docs/pico/pico-rendering.md
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
# Pico rendering
|
||||
|
||||
This document defines how a presentation renders a replicated Pico conversation. It covers local and
|
||||
remote clients, initial attachment, live streaming, terminal handoff, renderer registration and stable
|
||||
component identity. The harness model remains defined by `pico-v3.md`; the examples and client-facing
|
||||
surface belong in `pico-usage-guide.md`.
|
||||
|
||||
The target is the directness of coding-agent interactive mode and `experimental/mini`, without their
|
||||
hard-coded event vocabulary or duplicate hydration logic.
|
||||
|
||||
## 1. The presentation owns a replica, not agent state
|
||||
|
||||
A presentation owns one `ConversationView`. It does not read storage, parse commits or hold live task,
|
||||
provider, tool or model objects.
|
||||
|
||||
Opening a watch atomically captures the view and registers its listener. The harness buffers changes
|
||||
until delivery starts. Each committed change is folded into the view before listeners observe its
|
||||
events. An in-process client uses the harness-owned view; a remote client applies the same exported
|
||||
kind-free reducer before notifying its UI.
|
||||
|
||||
Delivery over one WebSocket or SSE connection is ordered and gap-free. If delivery fails or a bounded
|
||||
queue overflows, that watch closes. Reconnection opens a new watch and receives a fresh view. There are
|
||||
no cursors, epochs, acknowledgements, deduplication, replay requests or in-place resnapshot protocol.
|
||||
|
||||
The view is authoritative. Events identify the affected portion so a live renderer can avoid scanning
|
||||
the whole view; they are not a second source of presentation state.
|
||||
|
||||
## 2. Local view access
|
||||
|
||||
The raw collections remain serializable. A local wrapper adds indexed and typed access:
|
||||
|
||||
```ts
|
||||
interface ConversationView {
|
||||
readonly conversation: Conversation;
|
||||
readonly entries: readonly Entry[];
|
||||
readonly tasks: readonly Task[];
|
||||
readonly previews: ReadonlyMap<Id, JsonValue>;
|
||||
readonly inbox: readonly Element<QueuedInput>[];
|
||||
readonly values: ReadonlyMap<Address, JsonValue>;
|
||||
readonly context: readonly Id[];
|
||||
readonly faulted: boolean;
|
||||
readonly readAt: Id;
|
||||
|
||||
entry(id: Id): Entry | undefined;
|
||||
task(id: Id): Task | undefined;
|
||||
|
||||
entriesOf<K extends EntryDefinition>(kind: K): readonly EntryOf<K>[];
|
||||
tasksOf<K extends TaskDefinition>(kind: K): readonly TaskOf<K>[];
|
||||
|
||||
preview<S extends TaskStateBase, H extends HookPoints, C extends ConfigSpec, P,
|
||||
R extends TaskRoles<S>>(
|
||||
kind: TaskKind<S, H, C, P, R>, taskId: Id,
|
||||
): P | undefined;
|
||||
}
|
||||
```
|
||||
|
||||
`preview(kind, taskId)` returns `undefined` when the task is absent, has another kind or has no live
|
||||
preview. Built-in and plugin renderers use it without casts. `task(id)` is sufficient to classify a
|
||||
`task_output` event because the reducer has already folded that event before the listener runs.
|
||||
|
||||
A remote wire view may remain arrays and maps encoded as JSON. The wrapper builds its indexes locally;
|
||||
those indexes are not another replicated representation.
|
||||
|
||||
## 3. Two rendering paths
|
||||
|
||||
Both paths use the same registered renderers and component keys. They differ only in how much of the
|
||||
view they visit.
|
||||
|
||||
### 3.1 Hydration
|
||||
|
||||
Hydration runs on first attachment and after opening a replacement watch. It walks the current entries
|
||||
and live tasks and upserts their blocks:
|
||||
|
||||
```ts
|
||||
function hydrate(view: ConversationView): void {
|
||||
for (const entry of view.entries) entryRenderers.render(entry, view);
|
||||
for (const task of view.tasks) taskRenderers.render(task, view);
|
||||
chrome.sync(view);
|
||||
}
|
||||
```
|
||||
|
||||
This is the view-driven path used by Mini. Existing blocks are reconciled by stable key, so a fresh
|
||||
view need not tear down unchanged terminal components.
|
||||
|
||||
### 3.2 Live events
|
||||
|
||||
Normal streaming uses targeted event handling, as interactive mode does:
|
||||
|
||||
```ts
|
||||
function handle(event: ConversationEvent, view: ConversationView): void {
|
||||
switch (event.type) {
|
||||
case "entry":
|
||||
entryRenderers.render(event.entry, view);
|
||||
break;
|
||||
case "task_start":
|
||||
case "task_update":
|
||||
taskRenderers.render(event.task, view);
|
||||
break;
|
||||
case "task_output": {
|
||||
const task = view.task(event.task);
|
||||
if (task) taskRenderers.render(task, view);
|
||||
break;
|
||||
}
|
||||
case "task_end":
|
||||
taskRenderers.end(event.task, view);
|
||||
break;
|
||||
case "inbox":
|
||||
case "value":
|
||||
case "context":
|
||||
chrome.sync(view, event);
|
||||
break;
|
||||
case "fault":
|
||||
case "closed":
|
||||
chrome.connection(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A streamed token therefore updates one task renderer. It does not rebuild every transcript block or
|
||||
run every layout. A client that does not need the optimization may ignore the event payload and call
|
||||
`hydrate(view)` after every notification; it remains correct.
|
||||
|
||||
The renderer callbacks, not a mandatory full-tree diff, are the shared path between hydration and live
|
||||
updates.
|
||||
|
||||
## 4. Renderer registry
|
||||
|
||||
Entry and task renderers are registered by kind. Registration is typed by the kind object even though
|
||||
the wire and internal registry use its string token:
|
||||
|
||||
```ts
|
||||
renderers.entry(noteKind, previous => ({
|
||||
render(entry, host, view) { /* entry is NoteEntry */ },
|
||||
}));
|
||||
|
||||
renderers.task(downloadKind, previous => ({
|
||||
render(task, preview, host, view) { /* typed task and DownloadPreview */ },
|
||||
end(task, host, view) { /* terminal typed task */ },
|
||||
}));
|
||||
```
|
||||
|
||||
The exact component type belongs to the presentation package, not Pico. The common contract is:
|
||||
|
||||
- `render` upserts components through the host using stable keys;
|
||||
- `end` handles terminal live-state cleanup and does not need a retired preview;
|
||||
- renderers do not directly parent components in the transcript container;
|
||||
- registration may replace or wrap `previous`;
|
||||
- built-ins and plugins use the same API;
|
||||
- removing renderer code does not affect the replicated view.
|
||||
|
||||
An unregistered entry kind uses a generic renderer: model text when present, otherwise collapsed
|
||||
kind/data. An unregistered task kind uses kind, status and a collapsed raw preview when present.
|
||||
Unknown kinds must remain visible and must not break hydration.
|
||||
|
||||
The heterogeneous registry may erase renderer types internally. That cast or validation boundary is
|
||||
owned once by the registry; renderer authors do not cast task state, entry data or previews.
|
||||
|
||||
## 5. Renderer host
|
||||
|
||||
The host owns component instances and placement. Renderers only describe or update blocks through it.
|
||||
At minimum it maintains:
|
||||
|
||||
```text
|
||||
entry id -> settled transcript block
|
||||
live task id -> live task block or region
|
||||
tool-call id -> tool component
|
||||
inbox item id -> queued-input component
|
||||
```
|
||||
|
||||
It provides keyed `get`, `upsert`, `move`/`adopt` and `remove` operations. A renderer can update an
|
||||
existing component without knowing which container currently parents it. The host preserves expansion,
|
||||
selection and other local UI state while adopting or moving a block.
|
||||
|
||||
No persistent `tool task id -> tool-call id` map is required. Every tool-task state variant carries
|
||||
its call:
|
||||
|
||||
- on `task_output`, `view.task(event.task).state.call.id` identifies the component;
|
||||
- on `task_end`, the event's terminal task carries the same call;
|
||||
- tool-result entries use their indexed call key.
|
||||
|
||||
A renderer may cache expensive presentation-only products such as parsed Markdown. Such caches are
|
||||
never authoritative and may be discarded on hydration.
|
||||
|
||||
## 6. Stable identity and terminal handoff
|
||||
|
||||
Every rendered object has a stable semantic key:
|
||||
|
||||
| Object | Key |
|
||||
| --- | --- |
|
||||
| transcript entry | entry ID |
|
||||
| live generation | generation task ID |
|
||||
| assistant content block | generation task ID + content index |
|
||||
| tool component | tool-call ID |
|
||||
| live tool preview lookup | tool task ID |
|
||||
| inbox item | list-element/input ID |
|
||||
| compaction, job or plugin task | task ID |
|
||||
|
||||
A component may change ownership without changing its semantic identity.
|
||||
|
||||
### Assistant handoff
|
||||
|
||||
A generation preview renders a partial assistant message under the generation task ID. The final
|
||||
assistant entry has `byTaskId` equal to that generation task. Its renderer adopts the existing live
|
||||
component, applies the immutable final message and then associates the settled transcript position
|
||||
with the entry ID. It does not append a duplicate assistant component.
|
||||
|
||||
If no matching live component exists, as on initial attachment, the entry renderer creates the same
|
||||
settled component directly.
|
||||
|
||||
### Tool handoff
|
||||
|
||||
Tool-call components are born while the assistant preview streams and are keyed immediately by the
|
||||
call ID. A later tool-task start marks that same component running. Tool output updates it from the
|
||||
typed preview. The terminal tool-result entry updates the same call-ID component with immutable final
|
||||
output. No phase creates a second tool component.
|
||||
|
||||
### Other tasks
|
||||
|
||||
A task that has no terminal entry removes or finalizes its live block in `end`. A task kind that
|
||||
requires a durable transcript representation writes that entry in the same commit in which it
|
||||
settles. The view is already folded to the whole commit before listeners run, so no client-visible idle
|
||||
or missing-result state exists between those events.
|
||||
|
||||
## 7. Task provenance required by rendering
|
||||
|
||||
Task creation records generic provenance just as entry creation does:
|
||||
|
||||
```ts
|
||||
interface Task {
|
||||
// ...
|
||||
readonly byTaskId?: Id;
|
||||
}
|
||||
```
|
||||
|
||||
`ConversationTx` fills `byTaskId` from the invocation whose commit created the task; it is absent for
|
||||
tasks created outside a task invocation. Task kinds do not write it themselves.
|
||||
|
||||
This replaces kind-specific provenance such as `JobState.origin`. A job created by a tool has
|
||||
`byTaskId` equal to the tool task. A renderer follows that task and narrows it with `toolKind.is` to
|
||||
select the originating tool renderer. Nested delegated work follows the chain as needed.
|
||||
|
||||
## 8. Structured previews
|
||||
|
||||
A preview is the complete current render model for one live task, not a text delta.
|
||||
|
||||
The generation preview is the full partial `AssistantMessage` assembled from
|
||||
`AssistantMessageFrame`s. It retains ordered content blocks, text, thinking, signatures, partial tool
|
||||
arguments, provider metadata, usage and diagnostics. `task_output.ops` only transports changes; the
|
||||
renderer reads the already-updated message through `view.preview(generationKind, task.id)`.
|
||||
|
||||
A tool preview is the complete current `ToolOutputState`: text or structured content, plugin details,
|
||||
images, usage, delegation, handoff, added tools, termination, diagnostics and truncation metadata. Tool
|
||||
renderers own plugin-specific interpretation. The generic host does not flatten it to text.
|
||||
|
||||
Terminal rendering uses immutable entries or terminal task state because settlement retires the live
|
||||
preview.
|
||||
|
||||
## 9. Layouts are optional presentation policy
|
||||
|
||||
Grouping tool calls, collapsing a run until its final answer and similar structures are useful, but a
|
||||
general composable layout/reconciliation pipeline is not required by the Pico watch or renderer
|
||||
contract.
|
||||
|
||||
A presentation may add layouts above the keyed renderer host. Layouts must preserve block keys and
|
||||
must not make preview delivery depend on rebuilding the complete transcript. They should initially run
|
||||
only for structural changes unless a particular layout explicitly depends on live preview content.
|
||||
|
||||
The first coding-agent client should reach feature parity with direct keyed placement. Add a general
|
||||
layout API only after at least two concrete layouts demonstrate that a shared abstraction is smaller
|
||||
than their local implementations.
|
||||
|
||||
## 10. Client-owned visible work
|
||||
|
||||
A client operation that runs over time and should survive in the transcript is an ordinary task kind,
|
||||
not a presentation-only event stream. For example, `!cmd` is a `pi.user_bash` task owned by the coding
|
||||
agent or bash plugin. It shares process-output helpers with jobs, publishes a typed preview, and writes
|
||||
its terminal entry in the same commit as settlement.
|
||||
|
||||
The presentation starts it with a command and then handles its generic `task_start`, `task_output`,
|
||||
`task_end` and `entry` events. Escape calls `abortTask(id)`. Plugins use exactly the same pattern for
|
||||
client-initiated work.
|
||||
|
||||
## 11. Mapping from interactive mode
|
||||
|
||||
| Interactive-mode event | Pico rendering |
|
||||
| --- | --- |
|
||||
| `agent_start` / `turn_start` / assistant `message_start` | generation `task_start` |
|
||||
| `message_update` | generation `task_output`; read typed assistant preview |
|
||||
| `message_end` | assistant `entry`; adopt by `byTaskId` |
|
||||
| `tool_execution_start` | tool `task_start`; identify component by call ID |
|
||||
| `tool_execution_update` | tool `task_output`; read typed tool preview |
|
||||
| `tool_execution_end` | tool `task_end` plus tool-result `entry` |
|
||||
| `compaction_start` / `_end` | collapse `task_start` / `task_end`; summary `entry` |
|
||||
| retry events | generation or collapse `task_update` |
|
||||
| `agent_end` / `agent_settled` | no live foreground task after the commit |
|
||||
| `queue_update` | `inbox` |
|
||||
| thinking/model change | typed configuration `value` |
|
||||
| custom entry | registered entry renderer or generic fallback |
|
||||
| `bash_execution_update` | `pi.user_bash` `task_output` |
|
||||
|
||||
The generic vocabulary changes dispatch, not component behavior. Compared with interactive mode, Pico
|
||||
removes the second independently authored hydration implementation. Compared with Mini, it replaces
|
||||
hard-coded operation fields with typed task kinds and replaceable renderers while retaining the same
|
||||
small keyed-component model.
|
||||
38
packages/agent/docs/pico/pico-simple-blockers.md
Normal file
38
packages/agent/docs/pico/pico-simple-blockers.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Pico implementation state
|
||||
|
||||
## Authority
|
||||
|
||||
[`pico-simple-handoff.md`](pico-simple-handoff.md) is the sole normative implementation specification.
|
||||
Read it completely before implementation. Other Pico documents and prototypes are historical inputs only.
|
||||
|
||||
Astra reviewer `e5771f` reviewed the complete rewritten specification through multiple iterations and
|
||||
returned explicit `APPROVED`. Keep the reviewer available for implementation follow-up.
|
||||
|
||||
## Git state
|
||||
|
||||
- Branch: `pico`
|
||||
- Branch base when created: `f3c672245`
|
||||
- No production implementation has started.
|
||||
- The documentation preservation commit includes the pre-implementation Pico design cleanups and the
|
||||
approved simple handoff.
|
||||
- Implement and commit one small work package at a time for user review.
|
||||
|
||||
## Next action
|
||||
|
||||
Start WP1 from `pico-simple-handoff.md`: core types and kind witnesses under
|
||||
`packages/agent/src/harness/pico/`, including compile-time tests. Delegate implementation with provider
|
||||
`openai-codex`, model `gpt-5.6-sol`. Run the focused test and full `npm run check`, then use retained Astra
|
||||
for review. Commit only WP1 files after the package boundary is approved.
|
||||
|
||||
## Adopted core
|
||||
|
||||
The harness owns `pending -> running -> terminal`. Task input is immutable typed JSON. A checkpoint is an
|
||||
optional full replacement with `phase: string`. Execute/recover return typed terminal closures applied
|
||||
atomically with outcome and scratch retirement. Cancellation is mark, revoke writes, signal, join, fresh
|
||||
abort, then a restricted abort closure.
|
||||
|
||||
`turn: true` is a trusted replaceable task-kind capability. Background is independent because speculative
|
||||
manual collapse is background plus turn. Non-turn tasks use limited transactions and `accept`/`write`;
|
||||
turn tasks may append model-affecting entries directly. Every missing live kind becomes orphaned at open.
|
||||
Input groups belong to built-in generation/post_tools. V1 is job-first and excludes arbitrary unfinished
|
||||
promise adoption. Tool/job output sharing remains gated.
|
||||
1880
packages/agent/docs/pico/pico-simple-handoff.md
Normal file
1880
packages/agent/docs/pico/pico-simple-handoff.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -4,7 +4,9 @@ Durable agent harness for pi: one session file, any number of conversations, eve
|
|||
recorded as a task that survives a crash, and a view any UI can render.
|
||||
|
||||
**Note**: this guide is about using the harness. `pico-v3.md` is the design and the reference for
|
||||
why things are the way they are.
|
||||
why things are the way they are. Tool/sink/preview interfaces and their examples are provisional:
|
||||
we still need a simpler authoring surface before implementing them. Their required capabilities and
|
||||
storage/cancellation guarantees are retained; the separate steps proposal has not been adopted.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
|
|
@ -629,7 +631,10 @@ const answer = await c.prompt({ input: 'Inspect the parser' }, call);
|
|||
|
||||
The pieces are available separately. `inputId` is always the id of the accepted `pi.inbox` list
|
||||
element, even when idle acceptance places and removes it in the same commit. Generation and
|
||||
`post_tools` carry input ids explicitly, so result lookup never scans the transcript:
|
||||
`post_tools` carry input ids explicitly, so result lookup never scans the transcript. This is built-in
|
||||
harness behavior; a replacement for these built-in kinds must retain their input-result responsibilities.
|
||||
Removing an executable tool does not remove its task kind or the input owner: the built-in tool task
|
||||
writes an unavailable-tool result and the turn continues through `post_tools`.
|
||||
|
||||
```typescript
|
||||
const { inputId } = await c.accept({ input: 'Inspect the parser', requestId: 'req-42' }, call);
|
||||
|
|
@ -766,6 +771,9 @@ drain their own scratch promises and drop late callbacks, never silently recreat
|
|||
|
||||
## Watching
|
||||
|
||||
Tool-facing preview APIs remain provisional alongside the sink. Preview delivery coalescing is
|
||||
deferred; the current per-commit watch behavior is unchanged.
|
||||
|
||||
### The View
|
||||
|
||||
A UI never reads storage or parses commits. It watches a conversation and gets a **view**: a plain
|
||||
|
|
@ -1089,6 +1097,10 @@ anywhere. A violating builder call throws before anything is persisted.
|
|||
|
||||
## Writing Tools
|
||||
|
||||
**Provisional:** the sink, runtime and preview interfaces are preserved below for discussion, not as
|
||||
final implementation instructions. We will simplify them while retaining streaming output, bounded
|
||||
capture, diagnostics, typed details, usage, control outcomes and durable job/child cleanup.
|
||||
|
||||
### The Sink
|
||||
|
||||
A tool's `execute` returns nothing. Everything it produces goes through a sink, so output streams
|
||||
|
|
@ -1258,6 +1270,11 @@ A handler receives the active Call as its final argument and must forward it to
|
|||
harness awaits its actual return, not an abandoned raced promise. A handler may run again after a
|
||||
crash, so its external side effects need their own idempotence.
|
||||
|
||||
Approval/question policy, presentation and durable answer reuse belong to workspace/plugins, using
|
||||
existing scratch or scoped values as appropriate. Pico does not add a separate memo subsystem. How
|
||||
hooks receive task identity and authorized scratch access is still an integration question; the
|
||||
examples do not establish a new hook-payload API.
|
||||
|
||||
## Writing Kinds
|
||||
|
||||
Kinds are how you extend the harness with new behaviour rather than new state. An entry kind is a
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ not a package export. In snippets, `call` is the current `Call` (Chord Context,
|
|||
asynchronous public/runtime operations; `runtime` is the task/tool capability object. Transaction
|
||||
builders do not take another Call.
|
||||
|
||||
**Design status:** the tool/sink/preview interfaces and related examples remain provisional and need
|
||||
simplification before implementation. They preserve the capabilities to cover, not a final authoring
|
||||
surface. Storage, atomic publication, cancellation ownership and bounded-output requirements remain
|
||||
constraints on that redesign. The separate steps/returned-plans proposal has not been adopted.
|
||||
|
||||
## 1. Goals
|
||||
|
||||
**Few concepts, replaceable behaviour.** A session is conversations, immutable entries, durable
|
||||
|
|
@ -1420,7 +1425,10 @@ records `terminated`; an abort of the run, or `abortInput` on a queued item, rec
|
|||
Foreground abort withdraws queued steer and followUp while preserving write and nextRun. Failure
|
||||
and terminate may place safe writes but do not consume queued inputs that require a successor.
|
||||
Exactly one live generation or `post_tools` owns an active group, and ownership transfers in the
|
||||
same commit that settles the previous owner.
|
||||
same commit that settles the previous owner. Input groups/results are built-in harness behavior,
|
||||
not payload fields the generic driver infers from arbitrary plugin tasks. These built-in kinds are
|
||||
registered at open; a compatible replacement retains their input-result responsibilities. A missing
|
||||
tool implementation is handled by the surviving tool task kind, not by orphaning the input owner.
|
||||
|
||||
The list is stored as append/remove/clear operations, not as a rewritten array. Inbox watch events
|
||||
carry the same operations; an idle same-commit append/remove emits none. Memory and SQLite may
|
||||
|
|
@ -1685,7 +1693,9 @@ tool execute:
|
|||
```
|
||||
|
||||
The tool never touches context, siblings or queues. A throwing `before_tool` blocks the tool. An
|
||||
ordinary tool throw is an error result, not a cancellation.
|
||||
ordinary tool throw is an error result, not a cancellation. A missing executable tool is also an
|
||||
ordinary unavailable-tool result: the built-in `tool` kind remains registered and `post_tools` can
|
||||
continue or finish the input group. This differs from a genuinely missing task kind (§6.4).
|
||||
|
||||
```text
|
||||
recover: replay only if the stored policy allows; else an interrupted result from the checkpoint
|
||||
|
|
@ -1929,6 +1939,11 @@ and a crash simply asks again. A plugin kind declares its own points the same wa
|
|||
that replaces a built-in kind keeps its hook names so existing handlers keep working. Commit
|
||||
listeners are not hooks: they observe, never decide, and never await a commit on the same line.
|
||||
|
||||
Approval/question workflows and durable answer reuse belong to the workspace/plugin layer. Pico
|
||||
supplies task identity and existing storage capabilities, not a separate memo or approval subsystem.
|
||||
The exact hook access to task id and authorized scratch remains an integration question; no extra
|
||||
hook-payload fields or answer-replay protocol are established here.
|
||||
|
||||
## 9. API
|
||||
|
||||
### 9.1 Handles
|
||||
|
|
@ -2189,6 +2204,11 @@ code.
|
|||
|
||||
### 9.3 Task and tool integration
|
||||
|
||||
**Provisional API:** the sink, tool runtime and output-state shapes below are the current sketch.
|
||||
Simplify them together with previews before implementing. Preserve streaming text/images, typed
|
||||
details, bounded capture/spill, diagnostics, usage, control outcomes, jobs and cooperative cleanup;
|
||||
this section does not settle the final division of responsibilities or arbitrary-work adoption.
|
||||
|
||||
```ts
|
||||
interface Tool<TParams, TDetails extends JsonValue> {
|
||||
readonly name: string; readonly description: string; readonly parameters: JsonSchema<TParams>;
|
||||
|
|
@ -2273,6 +2293,9 @@ copying the message into `data`.
|
|||
|
||||
### 9.4 Watch
|
||||
|
||||
The tool-facing preview API below is provisional (§9.3). Delivery coalescing is deferred, not an
|
||||
initial implementation requirement; the current per-commit publication rules remain unchanged.
|
||||
|
||||
The transport is the commit stream, gap-free and replayable. The interface is typed events derived
|
||||
from it by the harness, and a view the harness keeps current, so a client renders and never parses
|
||||
commits:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ completed dependency audit. After the remaining conceptual blockers are settled,
|
|||
whole-document review and final work-package decomposition described below. `pico-v3.md` is the
|
||||
reference design; `pico-usage-guide.md` shows the intended surface.
|
||||
|
||||
Next design discussions: evaluate status-indexed task authoring and returned plans separately, then
|
||||
simplify the tool/sink/preview contract. The steps proposal has not been adopted. Groups 10, 14 and 15
|
||||
preserve capability/test coverage, but their current API sketches are not ready-to-implement contracts.
|
||||
Preview delivery coalescing is deferred. Approval/question workflows and durable answer reuse belong
|
||||
to workspace/plugins; only their access to existing task identity/scratch is a Pico integration question.
|
||||
|
||||
## 1. Types and ids
|
||||
|
||||
`Id`, `EntryIdentity`, `EntryBase`, the composable `EntryData` / `ModelProjection` /
|
||||
|
|
@ -345,6 +351,9 @@ Tests:
|
|||
|
||||
## 10. Tools, post_tools, exchanges
|
||||
|
||||
**API redesign pending:** retain the capabilities and behavioral tests below; simplify the tool,
|
||||
sink and preview authoring interfaces together before implementing them.
|
||||
|
||||
The tool kind with the sink (`ToolOutput`, `ToolOutputState`, limits enforced by the sink, `diag`,
|
||||
`delegate`, `handoff`, `addTools`, `terminate`), tool-result entries with structured data plus their
|
||||
materialized model message, `before_tool` (fail-closed) and `after_tool`,
|
||||
|
|
@ -361,7 +370,9 @@ before any handoff/user entry in the settlement commit and appears in the next t
|
|||
a throwing tool → error result, `terminate` still honoured; truncation diag from the sink; a lost
|
||||
accept response is recovered through `acceptance(requestId)`; a duplicate create reports the first
|
||||
receipt without comparing payloads or modes; results remain point-readable after further turns.
|
||||
post_tools handles orphaned tool tasks by writing unavailable-tool results from their common fields.
|
||||
A missing tool implementation produces an ordinary error result through the registered tool task kind;
|
||||
the generation/post_tools input owner remains present. Separately, post_tools handles orphaned tool
|
||||
tasks by writing unavailable-tool results from their common fields.
|
||||
Turn-task entry appends remain immediate; outside-turn model-visible writes queue while inTurn is
|
||||
nonempty and land at post_tools/final boundaries. Data-only entries are never blocked.
|
||||
|
||||
|
|
@ -371,7 +382,9 @@ nonempty and land at post_tools/final boundaries. Data-only entries are never bl
|
|||
user content or a write's entry draft and optional request id; append/remove/clear watch operations;
|
||||
queued/placed/done/unanswered result variants; the three placement points; carried generation/post_tools
|
||||
input groups; `queueInput` and `abortInput`; abort draining steer and followUp while preserving write
|
||||
and nextRun. A write reaches done without an answer in its placement commit.
|
||||
and nextRun. A write reaches done without an answer in its placement commit. Input-group ownership
|
||||
and results are built-in harness behavior, implemented by generation/post_tools; compatible replacements
|
||||
retain that contract. No generic plugin-payload inference or new input-owner protocol is needed.
|
||||
|
||||
Tests: idle append/remove is one commit and emits no inbox event; busy image payload is one append
|
||||
operation; the modes table; steer joins at post_tools but starts a group after a final answer;
|
||||
|
|
@ -403,6 +416,10 @@ cleanup tolerates already-terminal child/job; `stop`; nested children; child's o
|
|||
|
||||
## 14. Jobs and the budget
|
||||
|
||||
**Authoring API provisional:** job-first execution remains the initial supported approach. How tool
|
||||
code starts, observes and delegates work is part of the tool/sink/preview simplification; the current
|
||||
helper/sink signatures are not final.
|
||||
|
||||
`jobKind` on `ExecutionEnv.exec` with output into its scratch, `waitForTask` / `jobOutput`, `bash`
|
||||
delegating first and waiting with the budget, `notify` and the `notice` entry, the `job` tool,
|
||||
schedules.
|
||||
|
|
@ -416,10 +433,15 @@ ignores background recurrence; recover → lost or safe rerun; abort kills non-d
|
|||
|
||||
Remaining integration design: arbitrary-promise budget adoption must explicitly transfer effect and
|
||||
sink ownership before the tool invocation releases. A raced, abandoned promise is not permitted.
|
||||
Preserve the capability (crash outcome lost), but implement job-first execution until transfer is settled.
|
||||
Preserve the capability (crash outcome lost), but do not turn arbitrary-work adoption into an
|
||||
implementation package until the simpler tool contract and ownership transfer are settled.
|
||||
|
||||
## 15. Previews
|
||||
|
||||
**API redesign pending:** retain incremental live output and reconstruction from durable scratch.
|
||||
The tracker/sink-facing API below is a sketch to simplify, not a separate framework to implement first.
|
||||
Delivery coalescing is deferred; do not add a frame scheduler or delay scratch durability for it.
|
||||
|
||||
`runtime.preview` as a Chord tracker per task; the generation applies stream events to a partial
|
||||
message, the tool's preview is its sink state, the job's the same; `preview.init` on attach and
|
||||
reopen; flush after each scratch commit.
|
||||
|
|
@ -549,10 +571,11 @@ global; a kind that changes its shape bumps its own version, which is also the m
|
|||
|
||||
Needed when a client is not JavaScript. Not part of the gate.
|
||||
|
||||
Not packages: permissions and approval policy are plugin territory (`before_tool` can block or
|
||||
rewrite args and may wait for a person, scratch holds the durable memo, values hold whatever the
|
||||
plugin remembers, and a keyed service instance shows the question to every attached presentation),
|
||||
and session migration is a non-issue while this is experimental.
|
||||
Not Pico packages: approval/question policy, presentation, durable answers and their replay are
|
||||
workspace/plugin responsibilities. `before_tool` may block, rewrite args or wait; existing scratch
|
||||
and scoped values provide persistence. The workspace/plugin layer chooses answer keys and lifetimes;
|
||||
Pico's remaining integration question is authorized hook access to task identity/scratch, not a new
|
||||
memo protocol. Session migration remains outside this experimental scope.
|
||||
|
||||
## What comes from the lane harness, and how
|
||||
|
||||
|
|
@ -566,7 +589,7 @@ Copy (under `packages/agent/src/harness/` unless noted):
|
|||
|---|---|---|---|
|
||||
| `ExecutionEnv` / `FileSystem` / `Shell` types, the Node env, capture and spill | `types.ts`, `env/`, `tools/tool-context.ts` (03-execenv) | `pico/env/` | 10, 14 |
|
||||
| shell output limits, `applyShellOutputUpdate`, truncation totals | `utils/` | `pico/env/output.ts` | 10 |
|
||||
| the built-in tools (`read`, `write`, `edit`, `bash`, `image`) | `tools/*.ts` | `pico/tools/`, rewritten to the sink signature | 10, 14 |
|
||||
| the built-in tools (`read`, `write`, `edit`, `bash`, `image`) | `tools/*.ts` | `pico/tools/`, adapted after the tool/output interface is simplified | 10, 14 |
|
||||
| system prompt helpers, skills, context files, templates | `system-prompt.ts`, `skills.ts`, `prompt-templates.ts` | host handlers refresh typed base payloads; pure section renderers format them, without a source-read framework | 9, 20 |
|
||||
| telemetry span helpers | `telemetry.ts` | `pico/telemetry.ts` | 19 |
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue