mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
docs(agent): specify Pico5 harness API
This commit is contained in:
parent
c8e4a5a552
commit
b02eef4186
3 changed files with 712 additions and 108 deletions
|
|
@ -15,10 +15,13 @@ facades, membranes, document routing, view projection, events, or clone chains.
|
|||
|
||||
## 1. Records, cursors, and memory tables
|
||||
|
||||
Implement IDs, sequences, conversations, entries, inputs, live/terminal tasks,
|
||||
document metadata, storage writes, cursors, and detached `MemoryStorage` tables.
|
||||
Implement IDs, sequences, Session metadata with stable root ID, conversations,
|
||||
entries, inputs, live/terminal tasks, document metadata, storage writes, cursors,
|
||||
and detached `MemoryStorage` tables.
|
||||
|
||||
Test mixed atomic commits, rollback, detached reads/writes, cursor boundaries,
|
||||
Test atomic root-metadata/conversation creation, missing or dangling root
|
||||
metadata rejection, mixed atomic commits, rollback, detached reads/writes,
|
||||
cursor boundaries,
|
||||
fork-aware entry scans through deep ancestor caps, head lookup,
|
||||
entry-to-commit lookup, and full task replacement.
|
||||
|
||||
|
|
@ -170,8 +173,10 @@ Implement durable abort marks, signal/join/fresh-abort invocation, owned
|
|||
conversation creation, subtree traversal, background behavior, and idle waits.
|
||||
|
||||
Test commit rejection after a run task is marked, crashes at every abort stage,
|
||||
close precedence over a previously marked task, deep ownership trees, and atomic
|
||||
retirement of task-scoped documents.
|
||||
close precedence over a previously marked task, deep ownership trees, atomic
|
||||
retirement of task-scoped documents, default non-inheritance, inheritance from
|
||||
the current committed tail, an empty source conversation, document fork
|
||||
policies, and explicit model/section seed overrides.
|
||||
|
||||
## 18. Inputs and positional inbox
|
||||
|
||||
|
|
@ -222,14 +227,18 @@ progress, owned APIs, interrupted/replay-safe recovery, result entries,
|
|||
post-tools joining, controls, and boundaries using fake tools.
|
||||
|
||||
Test recovery from every phase, both stored/current replay-policy directions,
|
||||
and atomic assistant/tool/post-tools settlement. Use a fake generation
|
||||
successor; package 22 replaces it and reruns integration.
|
||||
default and overridden bounds, streamed-content fallback, progress replacement
|
||||
and coalesced commit settlement, drain-before-terminal ordering,
|
||||
abort/close with buffered output, invocation-bound owned handles, and atomic
|
||||
assistant/tool/post-tools settlement. Use a fake
|
||||
generation successor; package 22 replaces it and reruns integration.
|
||||
|
||||
## 22. Generation task
|
||||
|
||||
Implement preparation, request intent, durable throttled partials, attempts,
|
||||
retry policy, response classification, continuation, and deferred polling/
|
||||
cancellation with the faux provider.
|
||||
cancellation through pi-ai's exported `Models` interface. Do not add a Pico
|
||||
model adapter. The faux test double implements that same interface.
|
||||
|
||||
Test every phase before and after reopen, aborted partial conversion, overflow
|
||||
through a fake collapse kind, input settlement, and no visible-undurable update.
|
||||
|
|
@ -254,13 +263,32 @@ state or entries, and plugin handler recovery.
|
|||
|
||||
## 25. Harness integration
|
||||
|
||||
Expose conversation/input/task handles, watches, dynamic registries, close,
|
||||
suspend, service withdrawal/client detach, and product wiring. Implement the v1
|
||||
Implement the exact Pico3-shaped public surface in specification §2.2:
|
||||
`Harness.open/resume/suspend/close`, lifecycle gates, root/create/lookup
|
||||
conversations, ordered `write` handles, send/input handles, conversation-bound
|
||||
commits, fork/collapse/reset/abort/idle, typed task wait/abort, generic document
|
||||
access, registries, and structural conversation watches. Do not restore Pico3's
|
||||
namespace router, fixed document accessors, semantic view events, or manual Chord
|
||||
view bridge.
|
||||
|
||||
Expose service withdrawal/client detach and product wiring. Implement the v1
|
||||
host-plugin reload path as stop admission, close/join, dispose, rebuild with all
|
||||
new definitions, reopen/migrate, and resume. Test that closing seals commit and
|
||||
get-or-create admission, lets storage settlement for already-flushed admitted
|
||||
commits finish despite caller cancellation, writes no abort or terminal outcome, starts no fresh abort
|
||||
invocation, and does not run old and new generations concurrently.
|
||||
commits finish despite caller cancellation, writes no abort or terminal outcome,
|
||||
starts no fresh abort invocation, and does not run old and new generations
|
||||
concurrently.
|
||||
|
||||
Run all package-specific tests and the repository check. Verify a local
|
||||
Test stable persisted root identity; atomic conversation/config/section/input
|
||||
creation; fork seed overrides; concrete-entry forks; collapse task-ID return;
|
||||
busy reset admission and later placement; mark-only versus signalling abort;
|
||||
conversation abort/join with surviving passive writes and background tasks;
|
||||
quiescence with eligible work; listener initial/future delivery and isolation;
|
||||
and runtime registration between open and resume without resurrection of a task
|
||||
settled during open.
|
||||
|
||||
Compile-test every §2.2 signature and every usage sequence shown in slides
|
||||
20–26. The erased registry test must include a concrete task with narrowed
|
||||
input, multiple checkpoint phases, and custom hooks. Run all package-specific
|
||||
tests and the repository check. Verify a local
|
||||
coding-agent turn and a reopened interrupted turn, then stop for final review.
|
||||
|
|
|
|||
|
|
@ -132,89 +132,92 @@ Recovery records them as interrupted, then retries only when safe.</code></pre>
|
|||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="7"><div class="frame wide">
|
||||
<h2>What Lane got right</h2>
|
||||
<pre><code>TX 10 operation = { phase: "requesting", requestId: "r1" }
|
||||
<span class="comment">// durable intent</span>
|
||||
|
||||
provider request runs
|
||||
<span class="comment">// process may die here</span>
|
||||
|
||||
TX 11 assistant entry + operation = { phase: "tools", calls: [...] }
|
||||
<span class="comment">// durable outcome</span></code></pre>
|
||||
<p class="subtitle step">Atomic transactions and explicit restart points were correct.</p>
|
||||
<h2>Lane solved a narrower problem well</h2>
|
||||
<div class="grid2">
|
||||
<div class="panel step"><h3>Session</h3><ul class="tight"><li>immutable entry tree</li><li>typed current values and lists</li><li>atomic commits</li><li>usage ledger</li></ul></div>
|
||||
<div class="panel step"><h3>AgentLane</h3><ul class="tight"><li>one configured branch</li><li>queued input</li><li>at most one core operation</li><li>durable restart state</li></ul></div>
|
||||
</div>
|
||||
<p class="subtitle step">Lane is our durable conversation runtime. One operation is an agent run, compaction, or navigation.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="8"><div class="frame wide">
|
||||
<h2>Lane’s state machine was one fixed machine</h2>
|
||||
<pre class="code-small"><code><span class="kw">type</span> <span class="type">OperationState</span> =
|
||||
| Starting | Checkpoint | AssistantReady | AssistantEffectPending
|
||||
| AssistantRetryWait | Tools | DeferredSuspended | DeferredEffectPending
|
||||
| SummaryDeciding | SummaryReady | SummaryEffectPending
|
||||
| SummaryRetryWait | NavigationReadyToCommit;
|
||||
<h2>Products need independently registered durable tasks</h2>
|
||||
<pre><code>generation G7
|
||||
├─ tool T8 ───────────────┐
|
||||
├─ tool T9 ───────────────┴─ post-tools P10 ─ generation G11
|
||||
└─ collapse C12 <span class="comment">independent background work</span>
|
||||
|
||||
<span class="kw">switch</span> (state.at) {
|
||||
<span class="comment">// one core driver understands every operation and recovery edge</span>
|
||||
}</code></pre>
|
||||
<p class="subtitle step">Adding a first-class durable operation kind required changing the core graph and recovery.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="9"><div class="frame wide">
|
||||
<h2>Lane’s application storage was values + lists</h2>
|
||||
<pre><code>value<T> set or replace the complete value
|
||||
list<T> append one element or delete the whole list
|
||||
entry append immutable transcript content
|
||||
transaction commit all writes atomically</code></pre>
|
||||
<p class="subtitle step">Good storage primitives. Poor extension programming model for nested shared state.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="10"><div class="frame wide">
|
||||
<h2>Canvas with a Lane value: naive split read/write</h2>
|
||||
<pre class="code-small"><code><span class="kw">const</span> Canvas = value<<span class="type">CanvasState</span>>(<span class="str">"app.canvas"</span>);
|
||||
|
||||
<span class="kw">const</span> stored = <span class="kw">await</span> session.getValue(Canvas, context);
|
||||
<span class="kw">const</span> next = stored?.value ?? { strokes: [] };
|
||||
next.strokes.push(stroke);
|
||||
<span class="kw">await</span> session.setValue(Canvas, next, context);</code></pre>
|
||||
background job J20 ─ poll ─ output ─ reschedule
|
||||
plugin task K30 ─ owned conversation ─ child tasks</code></pre>
|
||||
<ul class="compact steps">
|
||||
<li class="step">Read, copy, and serialize the complete canvas per stroke.</li>
|
||||
<li class="step">Use Lane’s existing <code class="inline">session.mutate()</code> to serialize read-modify-write.</li>
|
||||
<li class="step">Maintain a separate replicated UI copy.</li>
|
||||
<li class="step">Lane already supports multiple lanes and parallel tools.</li>
|
||||
<li class="step">Missing: plugin-defined tasks with their own checkpoints, recovery, cancellation, and dependencies.</li>
|
||||
</ul>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="11"><div class="frame wide">
|
||||
<h2>Canvas with a Lane list</h2>
|
||||
<pre class="code-small"><code><span class="kw">const</span> CanvasOps = list<<span class="type">CanvasOp</span>>(<span class="str">"app.canvas.ops"</span>);
|
||||
<span class="kw">await</span> session.appendList(CanvasOps, {
|
||||
type: <span class="str">"stroke.added"</span>, stroke,
|
||||
}, context);
|
||||
<section class="slide" data-slide="9"><div class="frame wide">
|
||||
<h2>New first-class operation kinds require core changes</h2>
|
||||
<pre class="code-small"><code><span class="comment">// selected states from the fixed core union</span>
|
||||
<span class="kw">type</span> <span class="type">OperationState</span> =
|
||||
| Starting | AssistantEffectPending | AssistantRetryWait
|
||||
| Tools | DeferredEffectPending
|
||||
| SummaryEffectPending | SummaryRetryWait
|
||||
| NavigationReadyToCommit;
|
||||
|
||||
<span class="comment">// The extension now owns:</span>
|
||||
foldOperations(); checkpoint(); compact(); restore(); publishToClients();</code></pre>
|
||||
<p class="subtitle step">Small writes—but every extension starts building its own document store.</p>
|
||||
<span class="kw">switch</span> (state.at) {
|
||||
<span class="comment">// one driver owns every phase, recovery edge, and terminal path</span>
|
||||
}</code></pre>
|
||||
<p class="subtitle step">A plugin job can store markers—but first-class scheduling means editing Lane itself.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="10"><div class="frame wide">
|
||||
<h2>Concrete example: a durable plugin job</h2>
|
||||
<pre class="code-small"><code>download(url), independent of an agent run
|
||||
1 persist intent + stable request ID
|
||||
2 start remote work; persist returned handle
|
||||
3 poll handle + checkpoint bounded progress
|
||||
4 restart: poll, deduplicate if supported, or report interruption
|
||||
5 abort and clean up
|
||||
6 persist a terminal result for dependent jobs
|
||||
|
||||
Lane tools already have checkpoints, memos, cancellation, and recovery.
|
||||
Independent jobs still need plugin-owned scheduling,
|
||||
recovery, cancellation, and dependency handling.</code></pre>
|
||||
<p class="subtitle step">That is a second task runtime hidden inside an extension.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="11"><div class="frame wide">
|
||||
<h2>Values and lists are storage—not documents</h2>
|
||||
<table class="table"><thead><tr><th>Lane primitive</th><th>Canvas write</th><th>Application must add</th></tr></thead><tbody>
|
||||
<tr class="step"><td><code class="inline">value<Canvas></code></td><td><code class="inline">session.mutate()</code>, copy and replace full canvas</td><td>nested deltas and replica updates</td></tr>
|
||||
<tr class="step"><td><code class="inline">list<CanvasOp></code></td><td>append one small stroke operation</td><td>fold, base, compact, restore, hydrate</td></tr>
|
||||
</tbody></table>
|
||||
<p class="subtitle step">Both are valid primitives. Neither is a tracked JSON document with a committed change stream.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="12"><div class="frame wide">
|
||||
<h2>Durability and UI replication split</h2>
|
||||
<pre class="code-small"><code><span class="kw">await</span> session.setValue(ReviewValue, next, context); <span class="comment">// durable copy</span>
|
||||
<h2>Application replication is a second pipeline</h2>
|
||||
<pre><code>Lane application value/list application-owned Chord/UI mirror
|
||||
──────────────────────────── ─────────────────────────────────
|
||||
atomic durable writes materialized nested document
|
||||
current reads + recovery ────────→ snapshots + incremental updates
|
||||
serialized mutation line subscriptions + remote hydration
|
||||
|
||||
uiState.state.review = next; <span class="comment">// second mutable copy</span>
|
||||
uiState.publish(context); <span class="comment">// second publication</span>
|
||||
|
||||
<span class="comment">// On restart: read storage, reconstruct uiState, avoid races.</span></code></pre>
|
||||
<p class="subtitle step">Every facet owns the seam between storage and Chord.</p>
|
||||
for every application:
|
||||
define the mapping, startup hydration, ordering, and failure handling</code></pre>
|
||||
<p class="subtitle step">Lane exposes its own snapshots/events; arbitrary application values do not become ReplicatedState.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="13"><div class="frame wide">
|
||||
<h2>Lane lacked entry-anchored application-state history</h2>
|
||||
<pre><code>conversation A: e1 ─ e2 ─ e3 ─ e4
|
||||
└── fork B at e3
|
||||
<h2>Forking cannot recover application state at an entry</h2>
|
||||
<pre><code>source: canvas A ── e1 ── add B ── e2 ── delete A ── e3
|
||||
|
||||
transcript at e3: available
|
||||
current plan-mode value: available
|
||||
plan-mode value at e3: <span class="bad">not available</span>
|
||||
review comments at e3: <span class="bad">not available</span></code></pre>
|
||||
<p class="subtitle step">Tree forks copy current application state; branch forks exclude it. Neither reconstructs state at e3.</p>
|
||||
fork requested at e2 after e3 exists
|
||||
|
||||
Lane branch fork transcript through e2; application values/lists excluded
|
||||
Lane tree fork whole entry tree; <span class="warn">current</span> application state: only B
|
||||
needed for product transcript through e2; application state: A + B</code></pre>
|
||||
<p class="subtitle step">Current values have no replaced/deleted history to reconstruct the state at e2.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="14"><div class="frame wide">
|
||||
|
|
@ -293,6 +296,127 @@ many independently attached presentations</code></pre>
|
|||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="20"><div class="frame wide">
|
||||
<h2>The specified Pico5 host API</h2>
|
||||
<pre class="code-small"><code><span class="comment">// Normative public surface; implementation is package 25.</span>
|
||||
<span class="kw">const</span> storage: <span class="type">Storage</span> = <span class="kw">await</span> openAppStorage(context);
|
||||
<span class="kw">const</span> options: <span class="type">HarnessOptions</span> = {
|
||||
models, <span class="comment">// pi-ai Models</span>
|
||||
tools: [read, bash],
|
||||
taskKinds: [Reindex],
|
||||
documents: [PlanDoc, JobOutputDoc],
|
||||
sections,
|
||||
root: { model: { provider: <span class="str">"anthropic"</span>, modelId: <span class="str">"claude-sonnet-4-5"</span> } },
|
||||
};
|
||||
|
||||
<span class="kw">const</span> harness = <span class="kw">await</span> Harness.open(storage, options, context);
|
||||
harness.resume();
|
||||
<span class="kw">const</span> conversation = <span class="kw">await</span> harness.root(context);</code></pre>
|
||||
<p class="subtitle step">Definitions are installed before migration, recovery, and task dispatch.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="21"><div class="frame wide">
|
||||
<h2>Send input and await its durable answer</h2>
|
||||
<pre class="code-small"><code><span class="kw">const</span> input = <span class="kw">await</span> conversation.send({
|
||||
content: <span class="str">"Summarize the repository."</span>,
|
||||
requestId: <span class="str">"slack:1719432.0021"</span>,
|
||||
whenBusy: <span class="str">"followUp"</span>,
|
||||
}, context);
|
||||
|
||||
<span class="kw">const</span> settled = <span class="kw">await</span> input.wait(context);
|
||||
<span class="kw">if</span> (settled.status === <span class="str">"done"</span>) {
|
||||
console.log(<span class="str">"answer entry"</span>, settled.answer);
|
||||
} <span class="kw">else</span> {
|
||||
console.log(<span class="str">"unanswered"</span>, settled.reason);
|
||||
}</code></pre>
|
||||
<p class="subtitle step">Send returns after durable admission. Cancelling wait does not withdraw the input.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="22"><div class="frame wide">
|
||||
<h2>Write without starting another turn</h2>
|
||||
<pre class="code-small"><code><span class="kw">const</span> first = <span class="kw">await</span> conversation.write({
|
||||
kind: <span class="str">"app.note"</span>, data: { text: <span class="str">"Build passed"</span> },
|
||||
}, context);
|
||||
<span class="kw">const</span> second = <span class="kw">await</span> conversation.write({
|
||||
kind: <span class="str">"app.note"</span>, data: { text: <span class="str">"Deploy started"</span> },
|
||||
}, context);
|
||||
|
||||
<span class="kw">const</span> [a, b] = <span class="kw">await</span> Promise.all([
|
||||
first.wait(context), second.wait(context),
|
||||
]);
|
||||
console.log(a.entry, b.entry); <span class="comment">// passive writes settle with entry, not answer</span></code></pre>
|
||||
<ul class="compact steps">
|
||||
<li class="step">Idle: append immediately.</li>
|
||||
<li class="step">Busy: queue, then append at a safe boundary in input-ID order.</li>
|
||||
<li class="step">A passive write never triggers generation.</li>
|
||||
</ul>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="23"><div class="frame wide">
|
||||
<h2>Create, wait for, or abort a custom task</h2>
|
||||
<pre class="code-small"><code><span class="kw">const</span> ref = <span class="kw">await</span> conversation.commit(
|
||||
tx => tx.createTask(Reindex, { path: <span class="str">"/repo"</span> }, {
|
||||
background: <span class="kw">true</span>, <span class="comment">// conversationId defaults to this handle</span>
|
||||
}),
|
||||
context,
|
||||
);
|
||||
|
||||
<span class="comment">// Request durable cancellation, then observe terminal settlement.</span>
|
||||
<span class="kw">await</span> harness.abortTask(ref.id, context);
|
||||
<span class="kw">const</span> task = <span class="kw">await</span> harness.waitForTask(ref, context);
|
||||
console.log(task.state.outcome);</code></pre>
|
||||
<p class="subtitle step">Dependencies use terminal task records; no separate receipt store.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="24"><div class="frame wide">
|
||||
<h2>Fork at an entry and inspect historical state</h2>
|
||||
<pre class="code-small"><code><span class="comment">// PlanDoc is rewindable; checkpointEntryId is visible in the parent.</span>
|
||||
<span class="kw">const</span> child = <span class="kw">await</span> conversation.fork(checkpointEntryId, {}, context);
|
||||
|
||||
<span class="kw">const</span> historicalPlan = <span class="kw">await</span> harness.snapshotAsOf(
|
||||
PlanDoc, conversation.id, checkpointEntryId, context,
|
||||
); <span class="comment">// undefined when no instance existed at that commit</span>
|
||||
|
||||
<span class="kw">const</span> childPlan = <span class="kw">await</span> harness.snapshot(PlanDoc, {
|
||||
scope: <span class="str">"conversation"</span>, conversationId: child.id,
|
||||
}, context); <span class="comment">// normal access may initialize</span></code></pre>
|
||||
<p class="subtitle step"><code class="inline">asOf</code>, <code class="inline">current</code>, or <code class="inline">initial</code> determines the child document.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="25"><div class="frame wide">
|
||||
<h2>Observe the conversation as committed state</h2>
|
||||
<pre class="code-small"><code><span class="kw">const</span> watch = <span class="kw">await</span> conversation.watch(context);
|
||||
render(watch.value);
|
||||
|
||||
watch.start((view, ops, revision, _context) => {
|
||||
render(view);
|
||||
console.log(revision, ops);
|
||||
});
|
||||
|
||||
<span class="comment">// A Session facet may forward the same committed ops.</span>
|
||||
<span class="kw">function</span> disposeUi() { <span class="comment">// on UI teardown</span>
|
||||
watch.stop();
|
||||
}</code></pre>
|
||||
<p class="subtitle step">Capture and listener registration are atomic; close stops the watch.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="26"><div class="frame wide">
|
||||
<h2>Close and reopen without aborting work</h2>
|
||||
<pre class="code-small"><code><span class="kw">await</span> harness.close(context);
|
||||
<span class="comment">// Close causes no abort/terminal write. Admitted commits settle;</span>
|
||||
<span class="comment">// remaining work and checkpoints stay durable.</span>
|
||||
|
||||
<span class="kw">const</span> reopenedStorage = <span class="kw">await</span> openAppStorage(context);
|
||||
<span class="kw">const</span> recovered =
|
||||
<span class="kw">await</span> Harness.open(reopenedStorage, options, context);
|
||||
recovered.resume();
|
||||
|
||||
<span class="kw">const</span> recoveredConversation =
|
||||
<span class="kw">await</span> recovered.conversation(conversation.id, context);
|
||||
<span class="comment">// Old handles/sources are closed; reacquire from recovered.</span></code></pre>
|
||||
<p class="subtitle step">Plugin reload also disposes old facets and constructs the new definition set before reopen.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="27"><div class="frame wide">
|
||||
<h2>Four durable concepts</h2>
|
||||
<table class="table"><thead><tr><th>Concept</th><th>What it stores</th><th>Lifetime</th></tr></thead><tbody>
|
||||
<tr class="step"><td class="accent">Entry</td><td>Transcript and permanent facts</td><td>Session history</td></tr>
|
||||
|
|
@ -302,7 +426,7 @@ many independently attached presentations</code></pre>
|
|||
</tbody></table>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="21"><div class="frame wide">
|
||||
<section class="slide" data-slide="28"><div class="frame wide">
|
||||
<h2>Conversations are cheap branches</h2>
|
||||
<pre><code>Conversation A
|
||||
entries: e1 ─ e2 ─ e3 ─ e4
|
||||
|
|
@ -314,7 +438,7 @@ head entry → model context begins here; old entries remain stored</code
|
|||
<p class="subtitle step">No transcript copy. No whole-Session tree in memory.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="22"><div class="frame wide">
|
||||
<section class="slide" data-slide="29"><div class="frame wide">
|
||||
<h2>A document declares its semantics</h2>
|
||||
<pre class="code-small"><code><span class="kw">type</span> <span class="type">Stroke</span> = { color: <span class="type">string</span>; points: { x: <span class="type">number</span>; y: <span class="type">number</span> }[] };
|
||||
<span class="kw">type</span> <span class="type">CanvasState</span> = { strokes: <span class="type">Stroke</span>[] };
|
||||
|
|
@ -329,7 +453,7 @@ head entry → model context begins here; old entries remain stored</code
|
|||
<p class="subtitle step">Scope defines lifetime. Only conversation documents choose history and fork.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="23"><div class="frame wide">
|
||||
<section class="slide" data-slide="30"><div class="frame wide">
|
||||
<h2>Conversation history and fork are explicit</h2>
|
||||
<table class="table"><thead><tr><th>Setting</th><th>Meaning</th></tr></thead><tbody>
|
||||
<tr class="step"><td><code class="inline">history: latest</code></td><td>Only current value matters; old records can be reclaimed after a base.</td></tr>
|
||||
|
|
@ -340,7 +464,7 @@ head entry → model context begins here; old entries remain stored</code
|
|||
</tbody></table>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="24"><div class="frame wide">
|
||||
<section class="slide" data-slide="31"><div class="frame wide">
|
||||
<h2>One mutation, one authority</h2>
|
||||
<pre class="code-small"><code><span class="kw">const</span> target = { scope: <span class="str">"session"</span> } <span class="kw">as const</span>;
|
||||
|
||||
|
|
@ -356,7 +480,7 @@ head entry → model context begins here; old entries remain stored</code
|
|||
</div>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="25"><div class="frame wide">
|
||||
<section class="slide" data-slide="32"><div class="frame wide">
|
||||
<h2>Chord: the live delivery half</h2>
|
||||
<ul class="compact steps">
|
||||
<li class="step"><span class="accent">Facet</span> — one feature’s setup unit.</li>
|
||||
|
|
@ -367,7 +491,7 @@ head entry → model context begins here; old entries remain stored</code
|
|||
<p class="subtitle step">Chord transports state. Pico5 commits it.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="26"><div class="frame wide">
|
||||
<section class="slide" data-slide="33"><div class="frame wide">
|
||||
<h2>Canvas service: acquire once</h2>
|
||||
<pre class="code-tight"><code><span class="kw">interface</span> <span class="type">CanvasService</span> {
|
||||
<span class="kw">readonly</span> state: ReplicatedState<<span class="type">CanvasState</span> | <span class="kw">null</span>>;
|
||||
|
|
@ -381,7 +505,7 @@ head entry → model context begins here; old entries remain stored</code
|
|||
<span class="comment">// documentReplicatedState is the small Pico5→Chord adapter.</span></code></pre>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="27"><div class="frame wide">
|
||||
<section class="slide" data-slide="34"><div class="frame wide">
|
||||
<h2>Canvas service: provide durable methods</h2>
|
||||
<pre class="code-tight"><code><span class="kw">const</span> CanvasFacet = defineFacet({
|
||||
id: <span class="str">"app.canvas/session"</span>,
|
||||
|
|
@ -400,7 +524,7 @@ head entry → model context begins here; old entries remain stored</code
|
|||
<p class="subtitle step">The method resolves after durability—not after every UI callback.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="28"><div class="frame wide">
|
||||
<section class="slide" data-slide="35"><div class="frame wide">
|
||||
<h2>Consumers see snapshots, then updates</h2>
|
||||
<pre class="code-small"><code><span class="kw">const</span> CanvasConsumer = defineFacet({
|
||||
id: <span class="str">"app.canvas/consumer"</span>,
|
||||
|
|
@ -421,7 +545,7 @@ head entry → model context begins here; old entries remain stored</code
|
|||
<p class="subtitle step">Late join: current value. Connected client: ordered committed updates.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="29"><div class="frame wide">
|
||||
<section class="slide" data-slide="36"><div class="frame wide">
|
||||
<h2>A generation task commits what clients see</h2>
|
||||
<pre class="code-small"><code><span class="comment">// Schematic: LiveDoc is the conversation-scoped presentation document.</span>
|
||||
<span class="comment">// Read provider frames off the Session line.</span>
|
||||
|
|
@ -438,7 +562,7 @@ head entry → model context begins here; old entries remain stored</code
|
|||
<p class="subtitle step">Storage and every Chord client receive the same committed operations.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="30"><div class="frame wide">
|
||||
<section class="slide" data-slide="37"><div class="frame wide">
|
||||
<h2>Task kinds replace the fixed graph</h2>
|
||||
<pre class="code-small"><code><span class="kw">type</span> <span class="type">GenerationState</span> =
|
||||
| { phase: <span class="str">"prepare"</span> }
|
||||
|
|
@ -455,7 +579,7 @@ head entry → model context begins here; old entries remain stored</code
|
|||
<p class="subtitle step">Core scheduler knows tasks. Each kind knows its own phases.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="31"><div class="frame wide">
|
||||
<section class="slide" data-slide="38"><div class="frame wide">
|
||||
<h2>The generation task graph grows by commits</h2>
|
||||
<pre class="code-tight"><code><span class="comment">Selected business commits; reservation/preparation/progress omitted</span>
|
||||
|
||||
|
|
@ -475,7 +599,7 @@ F ordinary post-tools settlement:
|
|||
<p class="subtitle step">The scheduler knows dependencies. Generation, tools, and post-tools create the graph.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="32"><div class="frame wide">
|
||||
<section class="slide" data-slide="39"><div class="frame wide">
|
||||
<h2>The effect sandwich stays</h2>
|
||||
<pre><code><span class="comment">successful response with no tool calls</span>
|
||||
|
||||
|
|
@ -488,7 +612,7 @@ commit append assistant entry E
|
|||
replace task with completed result { entryId: E }</code></pre>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="33"><div class="frame wide">
|
||||
<section class="slide" data-slide="40"><div class="frame wide">
|
||||
<h2>Private task state ≠ presentation state</h2>
|
||||
<div class="grid2">
|
||||
<div class="panel step"><h3>Task record</h3><pre class="code-small"><code>{
|
||||
|
|
@ -505,7 +629,7 @@ commit append assistant entry E
|
|||
</div>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="34"><div class="frame wide">
|
||||
<section class="slide" data-slide="41"><div class="frame wide">
|
||||
<h2>Task scope carries bounded output</h2>
|
||||
<pre class="code-tight"><code><span class="kw">type</span> <span class="type">JobOutputState</span> = { stdout: <span class="type">string</span>; chunks: <span class="type">number</span> };
|
||||
|
||||
|
|
@ -525,7 +649,7 @@ watch.start((value, ops) => consume(value, ops));
|
|||
<p class="subtitle step">The producer bounds stdout on append; checkpoints only bound replay cost.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="35"><div class="frame wide">
|
||||
<section class="slide" data-slide="42"><div class="frame wide">
|
||||
<h2>Abort is durable and semantic</h2>
|
||||
<pre><code>conversation C
|
||||
├─ generation G7 ──owns──→ sub-conversation D ── tool T9
|
||||
|
|
@ -540,7 +664,7 @@ crash anywhere → durable marks remain → reopen continues abort</code></pre>
|
|||
<p class="subtitle step">Clients observe committed consequences; process-local abort signals are not replicated state.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="36"><div class="frame wide">
|
||||
<section class="slide" data-slide="43"><div class="frame wide">
|
||||
<h2>Close is not abort</h2>
|
||||
<table class="table"><thead><tr><th>Operation</th><th>Durable writes</th><th>Distribution and reopen</th></tr></thead><tbody>
|
||||
<tr class="step"><td>Cancel a Context</td><td>None by itself</td><td>Requests cancellation of one call/wait; remote propagation depends on transport. Committed work is not undone.</td></tr>
|
||||
|
|
@ -550,7 +674,7 @@ crash anywhere → durable marks remain → reopen continues abort</code></pre>
|
|||
<p class="subtitle step">Disconnecting a client does not silently abort shared work.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="37"><div class="frame wide">
|
||||
<section class="slide" data-slide="44"><div class="frame wide">
|
||||
<h2>Hooks and tools remain extensible</h2>
|
||||
<div class="grid2">
|
||||
<div class="panel step"><h3>Hooks</h3><ul class="tight"><li>declared by task kind</li><li>replacement chains or first decision</li><li>may rerun before their consuming commit</li><li>memos persist idempotency receipts</li></ul></div>
|
||||
|
|
@ -559,7 +683,7 @@ crash anywhere → durable marks remain → reopen continues abort</code></pre>
|
|||
<p class="subtitle step">V1: client facets may reload; Session-side plugin code closes and reopens the Harness.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="38"><div class="frame wide">
|
||||
<section class="slide" data-slide="45"><div class="frame wide">
|
||||
<h2>Target contract: open pi-ai PR #9548</h2>
|
||||
<pre class="code-tight"><code><span class="comment">// e10 — complete baseline</span>
|
||||
{ role: <span class="str">"system"</span>, content: basePrompt,
|
||||
|
|
@ -577,7 +701,7 @@ crash anywhere → durable marks remain → reopen continues abort</code></pre>
|
|||
<p class="subtitle step">Replay in transcript order; stored rendered sections remain authoritative.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="39"><div class="frame wide">
|
||||
<section class="slide" data-slide="46"><div class="frame wide">
|
||||
<h2>The conversation view is a mount</h2>
|
||||
<pre class="code-small"><code><span class="kw">type</span> <span class="type">ConversationView</span> = {
|
||||
conversation: <span class="type">Conversation</span>;
|
||||
|
|
@ -590,7 +714,7 @@ crash anywhere → durable marks remain → reopen continues abort</code></pre>
|
|||
<p class="subtitle step">Path prefixing, not a second tracker or semantic projection.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="40"><div class="frame wide">
|
||||
<section class="slide" data-slide="47"><div class="frame wide">
|
||||
<h2>Status is state, not an event</h2>
|
||||
<pre><code>live.collapse = { stage: "summarizing", through: 920 }
|
||||
live.collapse = { stage: "retrying", retryAt: 1730000000 }
|
||||
|
|
@ -602,7 +726,7 @@ late client during retry:
|
|||
<p class="subtitle step">A “started” event cannot help a client that attaches later.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="41"><div class="frame wide">
|
||||
<section class="slide" data-slide="48"><div class="frame wide">
|
||||
<h2>Storage is one contract, two products</h2>
|
||||
<div class="grid2">
|
||||
<div class="panel step"><h3>SQLite</h3><ul class="tight"><li>long-running/cloud Sessions</li><li>one SQL transaction per commit</li><li>indexed base + delta ranges</li><li>current task rows</li></ul></div>
|
||||
|
|
@ -611,7 +735,7 @@ late client during retry:
|
|||
<p class="subtitle step">Backends receive the same entries, task records, document ops, and candidate values.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="42"><div class="frame wide">
|
||||
<section class="slide" data-slide="49"><div class="frame wide">
|
||||
<h2>Build it in reviewable slices</h2>
|
||||
<div class="flow">
|
||||
<div class="node step"><span class="num">1</span><br>storage<br><span class="small">memory · SQLite · JSONL</span></div><div class="arrow step">→</div>
|
||||
|
|
@ -622,7 +746,7 @@ late client during retry:
|
|||
<p class="subtitle step">Each slice has intrinsic conformance tests and stops for sign-off.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="43"><div class="frame wide">
|
||||
<section class="slide" data-slide="50"><div class="frame wide">
|
||||
<h2>The deliberate sharp edges</h2>
|
||||
<div class="grid2">
|
||||
<div class="panel step"><h3>Ownership</h3><ul class="tight"><li>draft/proxy escape</li><li>inserted and duplicated aliases</li><li>table read after table write</li><li>large values in terminal task records</li></ul></div>
|
||||
|
|
@ -631,7 +755,7 @@ late client during retry:
|
|||
<p class="subtitle step">Stable definition IDs, mounted paths, and fork settings are persisted protocol.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="44"><div class="frame wide">
|
||||
<section class="slide" data-slide="51"><div class="frame wide">
|
||||
<h2>Footguns: drafts have one owner</h2>
|
||||
<pre class="code-small"><code><span class="kw">let</span> escaped: <span class="type">CanvasState</span> | <span class="kw">undefined</span>;
|
||||
<span class="kw">await</span> session.commit(<span class="kw">async</span> tx => {
|
||||
|
|
@ -651,7 +775,7 @@ escaped!.strokes.push(otherStroke); <span class="bad">// wrong: transaction ende
|
|||
<p class="subtitle step">Do not retain drafts, nested proxies, bound methods, or assigned mutable aliases.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="45"><div class="frame wide">
|
||||
<section class="slide" data-slide="52"><div class="frame wide">
|
||||
<h2>Footguns: do not block or outrun the line</h2>
|
||||
<pre class="code-tight"><code><span class="comment">// Independent mistake 1: external effect on the Session line.</span>
|
||||
<span class="kw">await</span> session.commit(<span class="kw">async</span> _tx => requestModel(context), context);
|
||||
|
|
@ -676,7 +800,7 @@ callback failure before flush → <span class="good">baseline resto
|
|||
<p class="subtitle step">Also remember: normal document reads may create, and IDs/paths are protocol.</p>
|
||||
</div></section>
|
||||
|
||||
<section class="slide" data-slide="46"><div class="frame center">
|
||||
<section class="slide" data-slide="53"><div class="frame center">
|
||||
<p class="big">Entries preserve history.</p>
|
||||
<p class="big step" style="margin-top:24px">Tasks preserve work.</p>
|
||||
<p class="big step" style="margin-top:24px">Documents preserve <span class="good">current truth</span>.</p>
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@ import type { Context, JsonValue } from "@earendil-works/chord";
|
|||
import type { Op } from "@earendil-works/chord/delta";
|
||||
import type {
|
||||
Message as ModelMessage,
|
||||
Models,
|
||||
TextContent,
|
||||
Tool,
|
||||
ToolReference,
|
||||
ToolResultMessage,
|
||||
UserMessage,
|
||||
} from "@earendil-works/pi-ai";
|
||||
|
||||
type JsonObject = { [key: string]: JsonValue };
|
||||
|
|
@ -168,6 +171,278 @@ Views carry raw active entries. UI reduction and model-context reduction are
|
|||
separate consumers. Older stored history is available through cursor-based
|
||||
entry scans.
|
||||
|
||||
### 2.2 Public Harness surface
|
||||
|
||||
This is the v1 host-facing API. Pico5 is not implemented yet, but implementations
|
||||
must expose this shape rather than inventing a different facade during package
|
||||
25.
|
||||
|
||||
```ts
|
||||
type ModelRef = {
|
||||
readonly provider: string;
|
||||
readonly modelId: string;
|
||||
};
|
||||
|
||||
type UserInput = UserMessage["content"];
|
||||
|
||||
type SendInput = {
|
||||
readonly content: UserInput;
|
||||
readonly requestId?: string;
|
||||
readonly whenBusy?: "steer" | "followUp" | "reject";
|
||||
};
|
||||
|
||||
type SectionSeed =
|
||||
| { readonly key: string; readonly value: JsonValue }
|
||||
| { readonly key: string; readonly remove: true };
|
||||
|
||||
type ConversationSpec = {
|
||||
readonly parent?: { readonly conversationId: Id; readonly at: Id };
|
||||
readonly model?: ModelRef;
|
||||
readonly sections?: readonly SectionSeed[];
|
||||
};
|
||||
|
||||
type AnyTaskKind = {
|
||||
readonly definition: {
|
||||
readonly name: string;
|
||||
readonly version: number;
|
||||
readonly initial: unknown;
|
||||
readonly phases: Readonly<Record<string, unknown>>;
|
||||
readonly abort: unknown;
|
||||
readonly migrate?: unknown;
|
||||
readonly hooks?: object;
|
||||
};
|
||||
};
|
||||
|
||||
type HarnessOptions = {
|
||||
readonly models: Models; // the pi-ai Models interface
|
||||
readonly tools?: readonly ToolRegistration[];
|
||||
readonly taskKinds?: readonly AnyTaskKind[];
|
||||
readonly documents?: readonly AnyDocToken[];
|
||||
readonly sections?: readonly SystemSection<JsonValue>[];
|
||||
readonly plugins?: Readonly<Record<string, PluginHandler>>;
|
||||
readonly processHost?: ProcessHost;
|
||||
readonly now?: () => number;
|
||||
readonly root?: Omit<ConversationSpec, "parent">;
|
||||
readonly onReport?: (error: unknown) => void;
|
||||
};
|
||||
|
||||
interface EntryKind<E extends Entry = Entry> {
|
||||
readonly kind: string;
|
||||
is(entry: Entry | undefined): entry is E;
|
||||
}
|
||||
|
||||
function defineEntry<E extends Entry>(kind: string): EntryKind<E>;
|
||||
|
||||
type ContextView = {
|
||||
readonly head: Entry | undefined;
|
||||
readonly entries: readonly Entry[];
|
||||
readonly messages: readonly RequestMessage[];
|
||||
};
|
||||
|
||||
type SettledInput = Input & {
|
||||
readonly status: "done" | "unanswered";
|
||||
};
|
||||
|
||||
interface InputHandle {
|
||||
readonly id: Id;
|
||||
result(context: Context): Promise<Input | undefined>;
|
||||
wait(context: Context): Promise<SettledInput>;
|
||||
abort(
|
||||
context: Context,
|
||||
): Promise<"aborted" | "already_placed" | "not_found">;
|
||||
}
|
||||
|
||||
type SettledTask<R> = Task<JsonValue, JsonValue, R> & {
|
||||
readonly state: TerminalTask<R>;
|
||||
};
|
||||
|
||||
interface ConversationWatch {
|
||||
readonly value: ConversationView;
|
||||
readonly revision: number;
|
||||
start(listener: (
|
||||
value: ConversationView,
|
||||
ops: readonly Op[],
|
||||
revision: number,
|
||||
context: Context,
|
||||
) => void): void;
|
||||
stop(): void;
|
||||
}
|
||||
|
||||
type HooksOf<K> = K extends TaskKind<infer _I, infer _S, infer _R, infer H>
|
||||
? H
|
||||
: never;
|
||||
|
||||
interface ConversationHandle {
|
||||
readonly id: Id;
|
||||
send(input: SendInput, context: Context): Promise<InputHandle>;
|
||||
write(entry: EntryDraft, context: Context): Promise<InputHandle>;
|
||||
commit<T>(
|
||||
change: (tx: Tx) => T | Promise<T>,
|
||||
context: Context,
|
||||
): Promise<T>;
|
||||
context(context: Context): Promise<ContextView>;
|
||||
fork(
|
||||
at: Id,
|
||||
spec: Omit<ConversationSpec, "parent">,
|
||||
context: Context,
|
||||
): Promise<ConversationHandle>;
|
||||
collapse(instructions: string | undefined, context: Context): Promise<Id>;
|
||||
reset(handoff: string | undefined, context: Context): Promise<void>;
|
||||
abort(context: Context): Promise<void>;
|
||||
waitForIdle(context: Context): Promise<void>;
|
||||
hooks<K extends AnyTaskKind>(
|
||||
owner: string,
|
||||
kind: K,
|
||||
handlers: Partial<HooksOf<K>>,
|
||||
options?: { readonly subtree?: boolean },
|
||||
): () => void;
|
||||
watch(context: Context): Promise<ConversationWatch>;
|
||||
}
|
||||
|
||||
interface Harness extends Session {
|
||||
resume(): void;
|
||||
suspend(context: Context): Promise<void>;
|
||||
quiescent(): boolean;
|
||||
hold(): () => void;
|
||||
|
||||
registerDocument(token: AnyDocToken): () => void;
|
||||
registerTaskKind(kind: AnyTaskKind): () => void;
|
||||
registerTool(tool: ToolRegistration): () => void;
|
||||
registerSection(section: SystemSection<JsonValue>): () => void;
|
||||
registerEntryKind(kind: EntryKind): () => void;
|
||||
hooks<K extends AnyTaskKind>(
|
||||
owner: string,
|
||||
kind: K,
|
||||
handlers: Partial<HooksOf<K>>,
|
||||
): () => void;
|
||||
|
||||
root(context: Context): Promise<ConversationHandle>;
|
||||
onConversation(listener: (conversation: ConversationHandle) => void): () => void;
|
||||
conversation(id: Id, context: Context): Promise<ConversationHandle | undefined>;
|
||||
createConversation(
|
||||
spec: ConversationSpec & { readonly input?: UserInput },
|
||||
context: Context,
|
||||
): Promise<ConversationHandle>;
|
||||
entries(
|
||||
query: EntryQuery,
|
||||
cursor: EntryCursor | undefined,
|
||||
limit: number,
|
||||
context: Context,
|
||||
): Promise<Page<Entry, EntryCursor>>;
|
||||
|
||||
getTask(id: Id, context: Context): Promise<Task<JsonValue, JsonValue, JsonValue> | undefined>;
|
||||
abortInput(
|
||||
id: Id,
|
||||
context: Context,
|
||||
conversationId?: Id,
|
||||
): Promise<"aborted" | "already_placed" | "not_found">;
|
||||
abortTask(id: Id, context: Context): Promise<"marked" | "terminal">;
|
||||
markTask(id: Id, context: Context): Promise<"marked" | "terminal">;
|
||||
waitForTask<R>(ref: TaskRef<R>, context: Context): Promise<SettledTask<R>>;
|
||||
waitForIdle(context: Context): Promise<void>;
|
||||
}
|
||||
|
||||
declare const Harness: {
|
||||
open(
|
||||
storage: Storage,
|
||||
options: HarnessOptions,
|
||||
context: Context,
|
||||
): Promise<Harness>;
|
||||
};
|
||||
```
|
||||
|
||||
This intentionally retains the useful Pico3 host shape. It removes Pico3's
|
||||
fixed `rewindable()`/`sticky()` accessors, namespace router, semantic view events,
|
||||
and manual Chord view bridge. Typed Pico5 documents and the structural
|
||||
conversation watch replace those surfaces. `write()` now returns an
|
||||
`InputHandle`, because a busy passive write may remain queued before it receives
|
||||
an entry ID.
|
||||
|
||||
`Harness.open()` installs built-in definitions followed by supplied documents,
|
||||
task kinds, tools, sections, and plugins. It then completes document migration,
|
||||
changes surviving `running` tasks to `pending`, migrates task records, and
|
||||
settles unknown or unmigratable live kinds as `orphaned` before resolving. IDs
|
||||
and names must be unique. No handler dispatches during open. Dynamic registration is available
|
||||
after open and does not resurrect a task already settled by that pass.
|
||||
|
||||
Storage contains one stable root conversation ID in Session metadata. Empty
|
||||
storage creates the conversation and metadata together from `options.root`;
|
||||
reopen reads that ID rather than inferring it from a conversation scan.
|
||||
`options.root` never overwrites existing state. `root()` returns the stored
|
||||
handle. A conversation with no configured model produces a durable `no_model`
|
||||
generation failure.
|
||||
|
||||
`resume()` is idempotent while running and only enables scheduling. It does not
|
||||
repeat open-time reconciliation. `suspend()` is terminal for that Harness
|
||||
instance and follows the close semantics below; `resume()` after suspend/close
|
||||
rejects. `quiescent()` means no task invocation is currently executing; eligible
|
||||
or delayed durable tasks may still exist. `hold()` is available only while
|
||||
quiescent and pauses reservation until its idempotent release function runs.
|
||||
|
||||
Runtime registration rejects duplicate IDs/names and returns an idempotent
|
||||
function that unregisters only that exact token. It supports declaration
|
||||
movement during normal operation, not replacement of executing plugin code;
|
||||
section 7.4 governs code reload.
|
||||
|
||||
Conversation creation atomically commits the conversation, built-in
|
||||
configuration and section seeds, and optional input admission. For a fork,
|
||||
omitted model/section values follow their document definitions' fork policies;
|
||||
provided values override those forked built-in values in the same commit. Other
|
||||
documents follow their own definitions without special handling.
|
||||
|
||||
A `ConversationHandle.commit()` is a Session commit bound to that conversation.
|
||||
`tx.createTask()` defaults `TaskOptions.conversationId` to the bound conversation.
|
||||
Generic Session-wide document operations remain available directly on `Harness`
|
||||
because `Harness extends Session`.
|
||||
|
||||
`fork()` requires a concrete visible parent entry and applies section 3.7.
|
||||
`collapse()` returns the newly admitted background collapse task ID, not its
|
||||
future summary entry. `reset()` durably admits a passive self-head reset or
|
||||
handoff write and then resolves; while busy, placement follows section 6 and may
|
||||
occur later. Observe its placement through the conversation watch. An idle wait
|
||||
does not guarantee placement of queued passive writes.
|
||||
|
||||
`markTask()` only commits `abortRequested`; it neither signals nor joins an
|
||||
active invocation. The scheduler notices the mark on its next drain.
|
||||
`abortTask()` also signals and joins an active run before starting the abort
|
||||
invocation. `ConversationHandle.abort()` withdraws queued non-write inputs, marks
|
||||
non-background tasks in the conversation and owned subtree, signals them, and
|
||||
resolves only after that subtree is ordinarily idle. Passive writes and
|
||||
background tasks survive. Conversation idle means no non-background live task
|
||||
in that conversation or its ownership subtree; Harness idle applies the same
|
||||
rule Session-wide. Pending dependency- or deadline-blocked work is still live
|
||||
and therefore not idle. Cancelling an idle wait aborts only that waiter.
|
||||
|
||||
`onConversation()` synchronously visits the currently loaded committed
|
||||
conversations in ascending ID order, then reports each later creation after its
|
||||
commit. Listener failures go to `onReport` and do not stop other listeners. Its
|
||||
idempotent disposer removes the listener; Harness close removes all remaining
|
||||
listeners.
|
||||
|
||||
`send()` and `write()` return after durable admission, not settlement. `send`
|
||||
creates a user message with the admission timestamp. `whenBusy` defaults to
|
||||
`followUp`. `write()` uses the ordered passive path in section 6 and never starts
|
||||
generation. A completed passive write identifies its entry in `Input.entry`; a
|
||||
completed send identifies the answer in `Input.answer`. Cancelling `wait()` only
|
||||
cancels that wait. It does not withdraw or abort the input; `InputHandle.abort()`
|
||||
is the explicit queued-input withdrawal operation.
|
||||
|
||||
`abortTask()` durably requests cancellation and returns `marked` after the mark
|
||||
is committed, any active run invocation has joined, and an abort invocation has
|
||||
been scheduled; it does not await terminal settlement. `waitForTask()` observes
|
||||
the terminal receipt. Aborting an already terminal task returns `terminal`; an
|
||||
unknown ID rejects. Explicit task abort includes a background task. Cancelling a
|
||||
task or idle wait does not abort work.
|
||||
|
||||
`ConversationHandle.watch()` atomically captures the current structural view and
|
||||
subscribes to later complete Session commits. It has the same stopped-start,
|
||||
unbounded pre-start buffering, listener isolation, and close ownership as
|
||||
`watchDoc()`. It carries no semantic events and owns no second tracker.
|
||||
|
||||
`close()` is equivalent to `suspend()` for v1: it stops reservation, signals and
|
||||
joins invocations, closes watches and storage, and writes no task outcome. Handles
|
||||
belong to that open Harness and must be reacquired after reopen.
|
||||
|
||||
## 3. Documents
|
||||
|
||||
### 3.1 Definitions
|
||||
|
|
@ -710,7 +985,7 @@ settles, the scheduler rereads the task and applies the first matching rule:
|
|||
6. Checkpoint unchanged: write terminal `faulted` because no durable progress
|
||||
was made.
|
||||
|
||||
On open, orphan reconciliation changes surviving `running` tasks back to
|
||||
On open, running-task reconciliation changes surviving `running` tasks back to
|
||||
`pending`, preserving their checkpoint and abort mark. Task migration then runs
|
||||
before dispatch. One callback handles every supported older version; newer or
|
||||
unmigratable live tasks become orphaned.
|
||||
|
|
@ -886,6 +1161,89 @@ semantic event channel; current UI status is document state.
|
|||
|
||||
### 7.2 Tools
|
||||
|
||||
```ts
|
||||
type ToolControl = {
|
||||
readonly addTools?: readonly string[];
|
||||
readonly terminate?: true;
|
||||
readonly handoff?: string;
|
||||
};
|
||||
|
||||
type ToolExecutionResult = {
|
||||
readonly content?: ToolResultMessage["content"];
|
||||
readonly isError?: boolean;
|
||||
readonly details?: JsonValue;
|
||||
readonly control?: ToolControl;
|
||||
};
|
||||
|
||||
interface OwnedConversationHandle {
|
||||
readonly id: Id;
|
||||
send(input: SendInput, context: Context): Promise<InputHandle>;
|
||||
abort(context: Context): Promise<void>;
|
||||
waitForIdle(context: Context): Promise<void>;
|
||||
}
|
||||
|
||||
interface ToolExecutionApi extends DocumentObserver {
|
||||
readonly taskId: Id;
|
||||
readonly conversationId: Id;
|
||||
readonly callId: string;
|
||||
stream(chunk: string | Uint8Array): void;
|
||||
commit<T>(change: (tx: Tx) => T | Promise<T>, context: Context): Promise<T>;
|
||||
progress(value: JsonObject, context: Context): Promise<void>;
|
||||
memo<T extends JsonValue>(name: string, context: Context): Promise<T | undefined>;
|
||||
memo<T extends JsonValue>(name: string, candidate: T, context: Context): Promise<T>;
|
||||
createTask<I, S extends { phase: string }, R, H extends object>(
|
||||
kind: TaskKind<I, S, R, H>,
|
||||
input: I,
|
||||
options: Omit<TaskOptions, "conversationId">,
|
||||
context: Context,
|
||||
): Promise<TaskRef<R>>;
|
||||
getTask<R>(ref: TaskRef<R>, context: Context): Promise<Task<JsonValue, JsonValue, R> | undefined>;
|
||||
waitForTask<R>(ref: TaskRef<R>, context: Context): Promise<SettledTask<R>>;
|
||||
createConversation(
|
||||
spec: Omit<ConversationSpec, "parent"> & { readonly inherit?: boolean },
|
||||
context: Context,
|
||||
): Promise<OwnedConversationHandle>;
|
||||
}
|
||||
|
||||
type PluginHandler = (
|
||||
input: JsonValue,
|
||||
api: ToolExecutionApi,
|
||||
context: Context,
|
||||
) => Promise<JsonValue>;
|
||||
|
||||
type ToolRegistration = Tool & {
|
||||
readonly replay?: "safe" | "unsafe";
|
||||
readonly output?: {
|
||||
readonly maxBytes?: number;
|
||||
readonly maxLines?: number;
|
||||
readonly retain?: "head" | "tail";
|
||||
};
|
||||
execute(
|
||||
args: JsonValue,
|
||||
api: ToolExecutionApi,
|
||||
context: Context,
|
||||
): Promise<ToolExecutionResult>;
|
||||
};
|
||||
```
|
||||
|
||||
Omitted `replay` is `unsafe`. Omitted output bounds are 64 KiB, 200 lines, and
|
||||
`retain: "head"`. `stream()` synchronously accepts UTF-8 output into that
|
||||
invocation-owned bounded buffer and throws after invocation end. Throttled
|
||||
commits publish the retained output and dropped byte/line counts in the tool
|
||||
presentation document. If `execute()` omits `content`, the final retained stream
|
||||
becomes one text content item; no stream becomes an empty content list. Explicit
|
||||
text in explicit result content is bounded by the same limits before transcript
|
||||
persistence; non-text content is retained as declared by its pi-ai type.
|
||||
|
||||
`progress(value)` replaces the invocation's complete JSON `progress` payload; it
|
||||
does not merge keys. Its promise resolves after the corresponding or coalesced
|
||||
document commit. During normal settlement, accepted output updates drain before
|
||||
the tool-result entry and terminal task record commit. Abort and close obey
|
||||
invocation and Session admission gates: uncommitted buffered updates may be
|
||||
discarded, while admitted commits settle. Pre-flush cancellation or callback
|
||||
failure does not poison the Session. Post-flush, checkpoint, or storage failure
|
||||
follows the fatal Session rule.
|
||||
|
||||
Tools are dynamically registered declarations with name, description, JSON
|
||||
schema, replay policy, and execute function. A tool call is accepted only if it
|
||||
was offered in the request's effective system/tool history. Arguments are
|
||||
|
|
@ -902,8 +1260,25 @@ A tool executes in a durable task. It may:
|
|||
- create and wait for tasks;
|
||||
- create owned conversations;
|
||||
- observe documents for which it has a token/reference;
|
||||
- mutate authorized documents through `commit()`;
|
||||
- return bounded model content and separate diagnostic details.
|
||||
|
||||
Tool and plugin operations use the invoking task's admission and
|
||||
invocation-lifetime gates. Task creation defaults to that task's conversation.
|
||||
Trusted document access follows sections 3.3 and 9.2; there is no additional
|
||||
document subtree authorization layer. `createConversation()` records the
|
||||
invoking task as owner and returns an invocation-bound handle. `inherit` defaults
|
||||
to `false`. When true, creation atomically selects the invoking conversation's
|
||||
newest committed visible entry as `parent.at`; if no entry exists, it creates no
|
||||
history parent. Conversation documents then apply section 3.7 at that entry, and
|
||||
explicit model/section seeds override their inherited built-in values. With no
|
||||
parent, documents initialize normally. That handle's
|
||||
operations reject after the invocation ends. An `InputHandle` returned by its
|
||||
`send()` is invocation-bound in the same way; the admitted input itself remains
|
||||
durable after those methods reject. Invocation-owned document watches stop when
|
||||
the invocation ends. A
|
||||
plugin handler receives the same API; `callId` is its durable invocation key.
|
||||
|
||||
A tool result may request `addTools`, `terminate`, or `handoff`. Post-tools
|
||||
applies added tool names to configured loadout, uses a final boundary for
|
||||
terminate/handoff, and writes a headed handoff entry when requested.
|
||||
|
|
@ -917,6 +1292,18 @@ tool terminal outcomes retain their tool-result entry ID for post-tools.
|
|||
|
||||
### 7.3 System sections and dynamic tools
|
||||
|
||||
```ts
|
||||
interface SystemSection<T extends JsonValue = JsonValue> {
|
||||
readonly key: string;
|
||||
render(value: T): string;
|
||||
}
|
||||
|
||||
function defineSystemSection<T extends JsonValue>(definition: {
|
||||
readonly key: string;
|
||||
render(value: T): string;
|
||||
}): SystemSection<T>;
|
||||
```
|
||||
|
||||
Pico stores prompt and tool changes directly as PR #9548 `SystemMessage` values
|
||||
at their transcript positions:
|
||||
|
||||
|
|
@ -1011,6 +1398,43 @@ isolation and is not a v1 promise.
|
|||
|
||||
## 8. Built-in tasks
|
||||
|
||||
```ts
|
||||
type ProcessSpec = {
|
||||
readonly command: string;
|
||||
readonly args: readonly string[];
|
||||
readonly cwd: string;
|
||||
readonly env?: Readonly<Record<string, string>>;
|
||||
};
|
||||
|
||||
type ProcessStatus =
|
||||
| {
|
||||
readonly status: "running";
|
||||
readonly stdout: string;
|
||||
readonly stderr: string;
|
||||
readonly droppedStdout: number;
|
||||
readonly droppedStderr: number;
|
||||
}
|
||||
| {
|
||||
readonly status: "exited";
|
||||
readonly exitCode: number;
|
||||
readonly stdout: string;
|
||||
readonly stderr: string;
|
||||
readonly droppedStdout: number;
|
||||
readonly droppedStderr: number;
|
||||
}
|
||||
| { readonly status: "unknown" };
|
||||
|
||||
interface ProcessHost {
|
||||
start(key: string, spec: ProcessSpec, context: Context): Promise<void>;
|
||||
status(key: string, context: Context): Promise<ProcessStatus>;
|
||||
kill(
|
||||
key: string,
|
||||
signal: "SIGTERM" | "SIGKILL",
|
||||
context: Context,
|
||||
): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
The initial implementation provides:
|
||||
|
||||
| kind | responsibility |
|
||||
|
|
@ -1022,6 +1446,14 @@ The initial implementation provides:
|
|||
| job | run a process, persist bounded output, delay or reschedule |
|
||||
| plugin | run a registered durable handler when needed |
|
||||
|
||||
Generation uses `HarnessOptions.models` without a Pico-specific model adapter. It
|
||||
resolves `models.getModel(ref.provider, ref.modelId)`, builds a pi-ai `Context`
|
||||
from the prepared prompt/messages/tools, and calls `models.streamSimple()` with
|
||||
the task invocation's abort signal and configured reasoning/options. Deferred
|
||||
continuation calls `models.fetchDeferred()` and `models.cancelDeferred()` with
|
||||
that same model and signal. Missing models and synchronous/streamed pi-ai errors
|
||||
are classified into the durable generation outcomes below.
|
||||
|
||||
Generation and tool progress are throttled durable document commits. A crash may
|
||||
lose only the uncommitted throttle window. Recovery converts committed partials
|
||||
to normal interrupted/aborted transcript entries, clears presentation state,
|
||||
|
|
@ -1143,6 +1575,14 @@ Entry appends/head changes and every changed mounted document are included in
|
|||
the same publication. The mount owns no tracker and performs no semantic
|
||||
projection. Its revision is contiguous per view.
|
||||
|
||||
`ConversationHandle.watch()` exposes that mount directly. Capture of
|
||||
`ConversationView`, revision, and listener registration is atomic on the Session
|
||||
line. Like `watchDoc()`, it returns stopped, buffers without a fixed bound until
|
||||
`start()` or `stop()`, drains in order, isolates listener failures, and stops on
|
||||
Harness close. Chord Session facets may forward the captured value and committed
|
||||
ops through services, but that product wiring is not part of the Harness facade
|
||||
and must not add another tracker or semantic event envelope.
|
||||
|
||||
## 10. Storage contract
|
||||
|
||||
Ordered scans are cursor-based. Exact identity lookups are keyed.
|
||||
|
|
@ -1194,7 +1634,12 @@ type StoredDocument = {
|
|||
];
|
||||
};
|
||||
|
||||
type SessionMetadata = {
|
||||
readonly rootConversationId: Id;
|
||||
};
|
||||
|
||||
type StorageWrite =
|
||||
| { readonly type: "session.metadata"; readonly value: SessionMetadata }
|
||||
| { readonly type: "conversation"; readonly value: Conversation }
|
||||
| { readonly type: "entry"; readonly value: Entry }
|
||||
| { readonly type: "task"; readonly value: Task<JsonValue, JsonValue, JsonValue> }
|
||||
|
|
@ -1218,6 +1663,7 @@ interface Storage {
|
|||
commit(writes: readonly StorageWrite[], context: Context): Promise<Seq>;
|
||||
mintId(): Id;
|
||||
|
||||
metadata(context: Context): Promise<SessionMetadata | undefined>;
|
||||
conversation(id: Id, context: Context): Promise<Conversation | undefined>;
|
||||
scanConversations(cursor: ConversationCursor | undefined, limit: number, context: Context): Promise<Page<Conversation, ConversationCursor>>;
|
||||
|
||||
|
|
@ -1238,6 +1684,11 @@ interface Storage {
|
|||
}
|
||||
```
|
||||
|
||||
`metadata()` is absent only for empty storage. The first root creation commits
|
||||
its conversation and metadata together. Existing records with missing metadata,
|
||||
or metadata naming a missing conversation, make open fail rather than guessing a
|
||||
root.
|
||||
|
||||
`EntryQuery` supports conversation ancestry, a strict `before` entry cursor,
|
||||
kind filtering, and `withHead`. Document queries support definition, scope,
|
||||
instance, current/as-of membership, and retired membership. Task queries support
|
||||
|
|
@ -1335,8 +1786,9 @@ These are contracts, not invitations to add defensive machinery:
|
|||
- **Schema stability:** definition IDs, mounted document IDs, and visible paths
|
||||
are persisted/public protocol. Value migration cannot rename an ID; an ID
|
||||
change requires explicit copy and retirement.
|
||||
- **Unstarted watches:** `watchDoc()` buffers every committed update until
|
||||
`start()` or `stop()`. Delaying both can consume unbounded memory.
|
||||
- **Unstarted watches:** `watchDoc()` and `ConversationHandle.watch()` buffer
|
||||
every committed update until `start()` or `stop()`. Delaying both can consume
|
||||
unbounded memory.
|
||||
- **Durable progress cadence:** clients see only committed progress. A crash may
|
||||
lose the current uncommitted throttle window.
|
||||
- **Large terminal results:** terminal task records remain queryable. Put large
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue