mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
docs(agent): define coherent attachment work package
This commit is contained in:
parent
b7277a566a
commit
9a8b75b1f8
2 changed files with 819 additions and 4 deletions
|
|
@ -3465,8 +3465,11 @@ Every package implements its named concern end to end and tests its normal path,
|
|||
|---|---|---|---|
|
||||
| WP00 (complete) | Reconciled acceptance/hooks, harvested runtime1 scenarios, switched the public factory, and deleted runtime1 without adding execution behavior. | Approved redesign and durability handoffs were present. | [Runtime1 removal](work-packages/00-runtime1-removal.md) |
|
||||
| WP01 (complete) | Replaced retained register/custom-state APIs with bound values/lists across Session, Memory, JSONL, SQLite, instrumentation, conformance, and public application access. | WP00 | [Bound values and lists](work-packages/01-bound-values-lists.md) |
|
||||
| WP02 | Implement atomic prompt/skill/template acceptance plus coherent attachment, phase-aware open-operation status, fully hydrated owned inspection state, and gap-free lane watch snapshots without starting execution. | WP01 | [Atomic acceptance and coherent attachment](work-packages/02-atomic-run-acceptance.md) |
|
||||
|
||||
The old Types, Session/Memory, JSONL, and SQLite rows described work already present in the repository. Their remaining cross-backend delta is WP01; they are not separate future packages.
|
||||
The old Types, Session/Memory, JSONL, and SQLite rows described work already present in the repository. Their remaining cross-backend delta was WP01; they are not separate future packages.
|
||||
|
||||
The former R1 row is likewise no longer a separate candidate. Its public types, lane/config shell, restore inventory, hook/event primitives, breakpoint/effect gate, neutral assistant/tool blocks, and direct primitive coverage are already present. WP02 takes atomic acceptance. The remaining lane-owned drive pass, expected-id fencing, same-id joining, deadline yield, and task lifecycle move into R2, where they first own real work; do not build an owner-only intermediate package. The harness-specific checkpoint update callback shape and its compatibility test remain R4 work.
|
||||
|
||||
## Future package candidates
|
||||
|
||||
|
|
@ -3476,10 +3479,9 @@ These rows preserve requirements, not frozen package boundaries. Create their ha
|
|||
|---|---|---|---|
|
||||
| S3 | **Search** | The standalone `SessionSearchService` plus repo catch-up utilities (§2.8): core entry search as `SessionSearch<T>`, session-level ranked results, optional `searchEntries?: SessionSearch<TEntryHit>`, `remove()`, the `SessionSearchSyncTarget` cursor/index-batch contract, sync enumeration and catch-up outside the service contract, debounced notify as a utility, `(sessionId, storeGeneration)` cursor keys, and the reference SQLite FTS5 implementation working over any backend's repository through the sync utility. | Cursor catch-up from empty against existing sessions, idempotent re-index after crash mid-batch, notify-utility/sweep equivalence, sessions-vs-entries queries and ranking, removal and reconciliation, shared-index multi-process discipline. |
|
||||
| S4 | **Dev TUI and Client** | A minimal `AgentClient` over one lane — `LaneSnapshot` plus `watch()` events, `prompt`/`steer`/`followUp`/`abort`/`resume`/`cancelQueued`, `pi.lane.lastResult` read — and a throwaway alt-screen TUI on `packages/tui`: transcript from snapshot and events, input box, status/queue display, abort key. Built first against a scripted fake client on the shared types; binds to the real harness as runtime packages land. Not final. | Compiles; fake-client smoke test. No durability obligations. |
|
||||
| R1 | **Runtime shell and execution primitives** | Extend the public types with `OperationRequest`, admission/drive/cancellation/inspection results, `OperationMismatch`, `AgentHarnessToolInvocation`, and harness-specific tool-update checkpoint options. Implement lane/settings mutation lines; one drive-pass `ActiveOperation` slot per lane; expected-id fencing, same-operation drive joining, coherent `inspectExecution`, safe deadline-yield plumbing, `ActionInfo` breakpoint barrier, `EffectGate` and its internal abort control flow, projection-existence restore checks, hook/event primitives, restore inventory, registry lookup plumbing, and fault/close plumbing. Also implement the independently testable behavior-only blocks under `packages/agent/src/harness/execution/` from §5.7; `agent-loop.ts` remains unchanged. | Type surface clean; coherent inspection; expected-id mismatch/latest-result lookup; one same-id pass installed/joined; stale-id drive isolation; safe yield; breakpoint and effect-gate coverage; task lifecycle; hook aggregation; ordered passive event buffering; restore without history reads, list reads, or reference audits; direct assistant/tool block tests; old `AgentToolUpdateCallback` remains unchanged while the harness callback carries `checkpoint:true`. |
|
||||
| R2 | **Minimal no-tool run** | Compose R1's assistant block with generic hook-free prompt `accept`, optional caller-supplied operation id, prompt expansion, atomic `starting` acceptance without task installation, pending-capture placement, driver-owned `before_drive`/`before_run` settlement, and `prompt` as `accept`+`drive`; add projection-only restore with consumption-time dereference, captured request options/thinking and model preflight, payload/response hooks, one generation intent/effect/settlement with per-event frame appends (synchronous enqueue, latest-promise settlement fencing) and atomic frame-list deletion, usage, terminal cleanup plus `pi.lane.lastResult`, results, basic events/telemetry, and stable breakpoint descriptors. | Acceptance returns before any drive work; crash between accept and drive resumes the initial state; prompt convenience and explicit accept+drive produce byte-identical writes/events; supplied operation id reaches `pi.op.meta`/events/result; successful run with final assistant fields; one frame append per convertible event with no per-frame storage await and only the latest promise retained; settlement awaits the latest frame write and deletes the list atomically; frame-append storage failure faults before `after_response`; caller rejection and adapter-reported failures; exact ordering; terminal cleanup; same-id drive join during the accept/drive gap; close at every boundary; invalid references rejected at consumption; re-enable coding-agent's one-shot and worker-owned Harness prompt tests. |
|
||||
| R2 | **Minimal no-tool drive** | Starting from WP02's durable `starting` operation, add the first real lane-owned `ActiveOperation` pass with expected-id fencing, same-id joining, latest-result lookup, safe deadline yields, stable `ActionInfo` breakpoints, and `EffectGate`; run driver-owned `before_drive`/`before_run`, make `prompt`/`skill`/`promptFromTemplate` compose `accept`+`drive`, and execute one assistant generation through captured context/options, payload/response hooks, intent/effect/settlement, synchronous per-event frame appends with latest-promise fencing, atomic frame-list deletion, usage, terminal cleanup plus `pi.lane.lastResult`, results, events, and telemetry. | Accepted `starting` close/reopen/drive; prompt convenience and explicit accept+drive produce byte-identical writes/events; one same-id pass installed/joined and stale ids isolated; safe yields and stable descriptors; successful run with final assistant fields; one frame append per convertible event with no per-frame storage await and only the latest promise retained; settlement awaits the latest frame write and deletes the list atomically; frame-append failure faults before `after_response`; caller rejection and adapter-reported failures; exact ordering; supplied operation id propagates through `run_end` and `pi.lane.lastResult`; terminal cleanup; close at every boundary; invalid references rejected at consumption; re-enable coding-agent's one-shot and worker-owned Harness prompt tests. |
|
||||
| R3 | **Generation recovery and retry** | Activation-only unknown-effect recovery that reduces committed frames into the synthetic zero-usage partial error under the reserved ids and then classifies ordinarily (retry or failure drain), ordinary stop/error/deferred classification, provider-compliant `aborted`, failure-drain foundation, and both retry-wait policies: explicit drive returns `waiting/notBefore`, while convenience drive may own the abort-aware timer. Define stable breakpoint descriptors for retry timers and recovery commits. Overflow classification remains explicitly unimplemented until R9. | Every generation state before/after reopen; accept/close/reopen/drive; recovery with no frames, partial frames, and authoritative end-frame content; synthetic settlement always commits under reserved ids with the interruption warning and deletes the frame list; interrupted partial tool calls never execute; `after_response` never runs for synthetic settlement; caps/backoff; no timer under `waitForRetry:false`; due and future `notBefore`; deadline before retry; convenience/explicit-drive durable equivalence; timer abort-first/start-first; stable descriptors; stop/error/aborted/deferred classification; missing identities. |
|
||||
| R4 | **Tools** | Compose R1's tool phases with bound context, invocation-scoped memos, `pi.op.tool_args`, scalar `pendingToolOutput(operationId, invocationId)` checkpoints, `outcome_ready` staging in `pi.pending.entry`, completion-order outcome durability, source-ordered materialization, orphan recovery/replay, sequential/parallel promises, blocked/genuine-length outcomes, hooks/events/usage, and stable descriptors. The built-in bash tool emits live updates at 100 ms and requests distinct checkpoints at most every two seconds. | Stable invocation id/memos across safe replay; every planned/effect-pending/outcome-ready/completed state; parallel out-of-order staging without replay; synchronous update callback with the latest delivery and latest checkpoint write awaited before `after_tool`; `tool_end` before staging; every requested checkpoint enqueued synchronously with only the latest promise reference retained and no write dropped or coalesced; tool-selected cadence/duplicate suppression; late-write fencing after settlement; JSONL checkpoint growth/compaction; safe replay deletes old checkpoint; unsafe recovery with/without checkpoint; batch-wide identity preflight starts no effect when any captured tool is missing; identity-free unsafe orphan settlement precedes later context resolution; a missing first safe orphan starts/resolves nothing later; current sequential override serializes recovered calls before later identity suspension; source-ordered materialization; termination and abort-ready states. |
|
||||
| R4 | **Tools** | Add the harness-specific tool-update checkpoint options/callback while leaving pi-ai's `AgentToolUpdateCallback` unchanged, then compose §5.7's neutral tool phases with bound context, invocation-scoped memos, `pi.op.tool_args`, scalar `pendingToolOutput(operationId, invocationId)` checkpoints, `outcome_ready` staging in `pi.pending.entry`, completion-order outcome durability, source-ordered materialization, orphan recovery/replay, sequential/parallel promises, blocked/genuine-length outcomes, hooks/events/usage, and stable descriptors. The built-in bash tool emits live updates at 100 ms and requests distinct checkpoints at most every two seconds. | Stable invocation id/memos across safe replay; every planned/effect-pending/outcome-ready/completed state; parallel out-of-order staging without replay; synchronous update callback with the latest delivery and latest checkpoint write awaited before `after_tool`; `tool_end` before staging; every requested checkpoint enqueued synchronously with only the latest promise reference retained and no write dropped or coalesced; tool-selected cadence/duplicate suppression; late-write fencing after settlement; JSONL checkpoint growth/compaction; safe replay deletes old checkpoint; unsafe recovery with/without checkpoint; batch-wide identity preflight starts no effect when any captured tool is missing; identity-free unsafe orphan settlement precedes later context resolution; a missing first safe orphan starts/resolves nothing later; current sequential override serializes recovered calls before later identity suspension; source-ordered materialization; termination and abort-ready states. |
|
||||
| R5 | **Inbox, configuration, and writes** | `nextRun`/steer/follow-up via `pendingEntry(id)` values, pending-payload consumption-time dereference, `cancelQueued` triage (`not_found`), durable drain markers, checkpoint consumption with pending-value deletion, immediate total config setters, deferred tree writes, adjustments, and stable breakpoint descriptors for the checkpoint boundaries introduced here. | Capture/cancel/consume races, repeated cancellation answering `not_found`, invalid queue payload kinds and missing pending values rejected at consumption, one-at-a-time crash after one drain, pending-value/entry exclusivity at every boundary, custom-write continuation, config-step race, writes surviving reopen. |
|
||||
| R6 | **Abort, close, and failure drain** | Add expected-id `requestAbort` as commit/signalling only. Compose `abort()` from inspection, marker, and same-id drive. Implement orthogonal control, per-phase reconciliation including planned/effect-pending/outcome-ready tools, best-effort deferred cancellation, waiters/run-when-idle, controlled-crash close, complete terminal deletion of memos/checkpoints/staged outcomes, external-finalization stop, and stable descriptors. | Cancellation with/without live pass; idempotency/fencing; abort at every tool state; restored interruption incorporates latest checkpoint without replay; restored cancellation synthesizes aborted responses preserving frame-reduced partial content and deleting the list; outcome-ready values survive and materialize; checkpoint/memo/frame cleanup; parked barriers; deferred cancellation; close races including a non-cooperative started tool that cannot block close and parallel retained promises that are all observed without later starts or unhandled rejection; external finalization fences late scalar/list writes; failure drain. |
|
||||
| R7 | **Deferred provider redemption** | One poll per drive pass carrying `pollDeferred:true`; `resume()` composes inspection and that drive option. Add deferred consumption-time dereference and provider identity preflight. Preserve copied configuration/options, per-poll hooks, exact source lineage/equality, poll frame persistence with fresh intent after unknown poll deleting the abandoned frame list, mismatch-to-error, ready tools, advancement of R6 cancellation, and stable poll descriptors. | `pollDeferred:false` returns waiting without work; one permit performs at most one poll; invalid/missing/mismatched durable handles; repeated pending, ready/error/aborted/mismatch, crash positions, poll frame cleanup on every settlement and replacement, unknown-poll snapshot without permit, convenience/primitive equivalence, no cap/backoff/loop, newest-handle cancellation. |
|
||||
|
|
|
|||
813
packages/agent/docs/work-packages/02-atomic-run-acceptance.md
Normal file
813
packages/agent/docs/work-packages/02-atomic-run-acceptance.md
Normal file
|
|
@ -0,0 +1,813 @@
|
|||
# WP02 — Atomic acceptance and coherent attachment
|
||||
|
||||
## Status
|
||||
|
||||
Implementation-ready; Phase A not started. `harness.md` remains normative, but its current attachment, suspension, snapshot, and manual-progress sections must be revised by Phase A of this package before implementation begins.
|
||||
|
||||
This handoff expects a clean implementation baseline: all discarded pre-handoff WP02 source and test changes have been explicitly removed. If a future session finds that implementation still present, stop before Phase A and restore these exact tracked paths to `HEAD`:
|
||||
|
||||
- `packages/agent/src/harness/runtime2/harness.ts`
|
||||
- `packages/agent/src/harness/runtime2/lane.ts`
|
||||
- `packages/agent/src/harness/runtime2/types.ts`
|
||||
- `packages/agent/test/harness/runtime2/harness.test.ts`
|
||||
- `packages/agent/test/harness/runtime2/lane.test.ts`
|
||||
|
||||
Delete the untracked `packages/agent/test/harness/runtime2/accept.test.ts`. Leave `packages/agent/docs/harness.md` and this handoff in place. Use only explicit per-path operations permitted by repository git-safety rules. Do not preserve, patch forward, or mine the discarded implementation.
|
||||
|
||||
## Goal
|
||||
|
||||
Deliver one effect-free lifecycle boundary:
|
||||
|
||||
```text
|
||||
idle lane
|
||||
→ atomic prompt/skill/template acceptance
|
||||
→ durable open operation
|
||||
→ close or process loss
|
||||
→ coherent attachment
|
||||
→ immediately complete, gap-free watch snapshot
|
||||
```
|
||||
|
||||
After `AgentHarness.create(options, context)` succeeds:
|
||||
|
||||
- every configured lane has one coherent process-local projection;
|
||||
- every open operation is reported as `resumable`, `blocked`, or `aborting`;
|
||||
- `inspectExecution(context)` and the first `watch(context)` read owned memory only;
|
||||
- the first watch snapshot can render committed transcript, queues, pending writes, unfinished assistant output, running-tool checkpoints, retry/deferred state, manual action, control, leaf, and last result without replaying old lifecycle events;
|
||||
- attachment starts no hook, provider, tool, timer, breakpoint, drive owner, or application callback;
|
||||
- future drive passes still reread and fence durable state and never use attachment display hydration as transition authority.
|
||||
|
||||
WP02 also implements atomic prompt/skill/template acceptance into payload-free `starting`. It does not implement `drive()`, provider generation, hooks, tool effects, retries, deferred polling, cancellation procedures, or manual action execution.
|
||||
|
||||
## Decision trace
|
||||
|
||||
### 1. The old suspension type mixed unrelated concepts
|
||||
|
||||
Problem:
|
||||
|
||||
```ts
|
||||
reason: "crash" | "deferred" | "missing_identities"
|
||||
```
|
||||
|
||||
`crash` described how process ownership disappeared. `deferred` described durable provider state. `missing_identities` described a current process registry blocker. The `crash` branch could also contain `deferred` and `missing`, so application code could not make an exhaustive decision from `reason`.
|
||||
|
||||
Concrete trace:
|
||||
|
||||
```text
|
||||
run is deferred
|
||||
→ process closes
|
||||
→ reopen reports reason:"crash" with optional deferred
|
||||
→ application must inspect both reason and optional fields
|
||||
```
|
||||
|
||||
Decision: delete `SuspendedOperation`. Report open operations with one process-relative status that directly controls application behavior:
|
||||
|
||||
- `resumable`: no process owner, and the next real drive can make durable progress;
|
||||
- `blocked`: no process owner, and the immediate next durable transition requires a currently missing model/tool identity;
|
||||
- `aborting`: durable `cancel_requested`; cancellation reconciliation is next;
|
||||
- `running`: a process-local drive owner exists; used by live inspection, never returned by attachment.
|
||||
|
||||
Idle is `current: null` and is not an open-operation status.
|
||||
|
||||
### 2. Attachment and drive have different authority
|
||||
|
||||
Problem: treating attachment hydration as transition authority creates stale parallel state. Treating attachment as projection-only produces incomplete public inspection and forces an artificial classification drive.
|
||||
|
||||
Decision:
|
||||
|
||||
- attachment fully reconstructs and validates the exact state-directed data required by `open`, `inspectExecution(context)`, and an immediate `watch(context)` snapshot;
|
||||
- owned memory is authoritative for process-local inspection and event publication;
|
||||
- every future drive pass independently enters the lane mutation line, rereads current durable ownership/control/phase, fences the expected operation id, dereferences its own transition inputs, checks current identities, commits intent before effects, and rereads before settlement;
|
||||
- attachment data is never passed into a durable transition.
|
||||
|
||||
### 3. The first watch must be complete before resume
|
||||
|
||||
Required application order:
|
||||
|
||||
```ts
|
||||
const { harness, open } = await AgentHarness.create(options, context);
|
||||
const watch = await harness.watch(context);
|
||||
render(watch.snapshot);
|
||||
watch.start((event, eventContext) => renderEvent(event, eventContext));
|
||||
// Only now may the application call resume(context) or manual progression methods.
|
||||
```
|
||||
|
||||
Problem: if watch lazily reads durable partials after resume starts, snapshot and events can race. If it omits pre-attachment partials, the UI cannot render interrupted provider/tool work.
|
||||
|
||||
Decision: `create(options, context)` performs the bounded hydration. `watch(context)` atomically captures owned memory and begins buffering before it returns. It performs no storage read. The initial snapshot contains durable partial fallback; live process partials replace it after a future drive owns work. Old message/tool lifecycle events are never replayed.
|
||||
|
||||
### 4. Missing identities are phase-aware and advisory
|
||||
|
||||
Problem: comparing every lane-configured identity against registries at attachment can label identity-free orphan recovery or cancellation as blocked.
|
||||
|
||||
Decision: one pure phase classifier derives the identity requirement of the immediate next durable transition. `blocked` means:
|
||||
|
||||
> Calling `resume(context)` now cannot commit any durable progress because that next transition requires a missing current-process identity.
|
||||
|
||||
Inspection computes this advisory status from owned durable phase plus current in-process registries. A future drive recomputes it at the actual transition boundary. Registry replacement can change `blocked` to `resumable` without a durable write or automatic start.
|
||||
|
||||
### 5. Malformed required attachment state faults attachment
|
||||
|
||||
Problem: returning a harness whose first watch later discovers a missing required queue/deferred/tool reference violates the coherent-memory invariant.
|
||||
|
||||
Decision:
|
||||
|
||||
- missing/mismatched required projection or public-snapshot references reject `create()` with one `HarnessFault`;
|
||||
- no partially attached harness is returned;
|
||||
- absence of optional assistant frame lists and optional tool checkpoints is valid and merely omits the partial field;
|
||||
- drive-only references not represented in attachment inspection remain consumption-time validations.
|
||||
|
||||
### 6. Normal users do not manage operation ids
|
||||
|
||||
Low-level `drive({ operationId })` and `requestAbort(operationId)` retain expected-id fencing so a stale hosted wake for operation A cannot act on later operation B.
|
||||
|
||||
Normal applications use `prompt()`, `skill()`, `compact()`, `navigateTree()`, `resume()`, and `abort()` without ids and pass the trailing invocation `Context` required by the current API. After attachment, the application calls `resume(context)` on a lane reported `resumable` or `aborting`; it repairs registries before resuming a `blocked` lane.
|
||||
|
||||
### 7. Manual mode returns progress instead of parking public promises
|
||||
|
||||
Problem: a manual-mode convenience promise that remains unresolved behind a breakpoint requires polling, a waiter API, or a second controller object.
|
||||
|
||||
Decision: add `action_required` to the existing operation outcome families. In manual mode:
|
||||
|
||||
- `resume(context)` and operation convenience calls return at the first parked breakpoint;
|
||||
- `executeAction(context)` releases exactly one parked action and returns the next `ResumeResult` action or non-action outcome;
|
||||
- `runToCompletion(context)` repeatedly releases actions and cannot return `action_required`;
|
||||
- `peekAction(context)` remains immediate nonblocking inspection;
|
||||
- automatic mode never returns `action_required`.
|
||||
|
||||
WP02 lands these public types and stubs. The first real drive package implements their behavior.
|
||||
|
||||
### 8. Invocation context makes telemetry and RPC boundaries explicit
|
||||
|
||||
Problem: one harness or lane can serve concurrent local callers and RPC requests. A receiver-level or ambient context would let one invocation overwrite another's telemetry parent or cancellation authority. RPC also needs a place to reconstruct request-local authority without adding transport fields to every business method.
|
||||
|
||||
Concrete trace:
|
||||
|
||||
```text
|
||||
client invocation context
|
||||
→ rpc.client injects a trace carrier and maps abort to request cancellation
|
||||
→ rpc.server creates a request AbortController and extracts a local telemetry parent
|
||||
→ adapter constructs a fresh server Context
|
||||
→ harness call, Session commit, and emitted events receive that Context
|
||||
```
|
||||
|
||||
Decision:
|
||||
|
||||
- every current public harness/lane operation receives one explicit trailing `context: Context`;
|
||||
- `context.telemetryContext` supplies the invocation parent for harness/session spans, and derived child contexts preserve parentage across concurrent asynchronous branches without ambient state;
|
||||
- `context.abortSignal` is process-local invocation cancellation; it never implies durable `cancel_requested`;
|
||||
- shared harness, lane, Session, and SessionTree receivers never retain a caller context;
|
||||
- acceptance passes its invocation context to Session reads/mutation/commit, faults, and emitted events; buffered events retain `{ event, context }` so delayed local handlers and future RPC event frames preserve the source telemetry lineage;
|
||||
- RPC adapters remove Context from serialized business arguments, carry required trace/cancellation data as control-plane metadata, and reconstruct a fresh local Context at the receiving boundary; the Context object, `AbortSignal`, and `TelemetryContext` are never serialized as business arguments or stored durably;
|
||||
- whether specific adapter-managed typed values may cross as control-plane metadata remains an open RPC design decision;
|
||||
- WP02 documents and preserves that boundary but does not implement RPC transport, carrier encoding, distributed cancellation, new telemetry spans, or final RPC parameter-position policy.
|
||||
|
||||
### 9. Rejected alternatives
|
||||
|
||||
Do not implement:
|
||||
|
||||
- `resumeManual()` or wrong-mode throws;
|
||||
- a separate execution/controller object;
|
||||
- polling `peekAction()`;
|
||||
- `waitForAction()` ceremony;
|
||||
- `reason: "ready"`;
|
||||
- redundant `status: "suspended"` inside a suspended descriptor;
|
||||
- `crash` as a user-action classification;
|
||||
- attach-time broad history/value scans;
|
||||
- attachment data as drive transition input;
|
||||
- compatibility aliases for `SuspendedOperation` or the old convenience result shapes.
|
||||
|
||||
## End-state public contract
|
||||
|
||||
Phase A must first place these normative shapes in `harness.md`. Phase B then updates source types.
|
||||
|
||||
### Operation status and attachment result
|
||||
|
||||
```ts
|
||||
export type OperationStatus = "running" | "resumable" | "blocked" | "aborting";
|
||||
|
||||
export type OpenOperation = {
|
||||
lane: string;
|
||||
operationId: string;
|
||||
kind: "run" | "compaction" | "navigation";
|
||||
startedAt: number;
|
||||
} & (
|
||||
| { status: "resumable" | "aborting"; missing?: never }
|
||||
| { status: "blocked"; missing: MissingIdentityInfo }
|
||||
);
|
||||
|
||||
export type CurrentOperationInfo = {
|
||||
id: string;
|
||||
kind: "run" | "compaction" | "navigation";
|
||||
startedAt: number;
|
||||
} & (
|
||||
| { status: "running" | "resumable" | "aborting"; missing?: never }
|
||||
| { status: "blocked"; missing: MissingIdentityInfo }
|
||||
);
|
||||
|
||||
export interface LaneInfo {
|
||||
name: string;
|
||||
leafId: string | null;
|
||||
operation: CurrentOperationInfo | null;
|
||||
}
|
||||
|
||||
export interface SessionSnapshot {
|
||||
lanes: LaneInfo[];
|
||||
faulted: boolean;
|
||||
}
|
||||
|
||||
export interface AgentHarnessConstructor {
|
||||
create<TContext extends object | undefined = object | undefined>(
|
||||
options: AgentHarnessOptions<TContext>,
|
||||
context: Context,
|
||||
): Promise<{ harness: AgentHarness<TContext>; open: OpenOperation[] }>;
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `open` contains exactly one item per lane with a durable current operation;
|
||||
- idle lanes are omitted;
|
||||
- attachment never returns `running`;
|
||||
- `missing` is present exactly when status is `blocked`;
|
||||
- `open` is an observation and may become stale after return;
|
||||
- `inspectExecution(context)` and snapshots derive current status again from owned state and current registries.
|
||||
|
||||
### Phase-aware identity requirement
|
||||
|
||||
Add one exhaustive classifier over `Operation` plus the current time, defined for the behavior of normal `resume()` (which always owns one deferred-poll permit):
|
||||
|
||||
```ts
|
||||
export type IdentityRequirement =
|
||||
| { kind: "model"; provider: string; modelId: string }
|
||||
| { kind: "tools"; names: string[] }
|
||||
| null;
|
||||
|
||||
export function nextResumeIdentityRequirement(
|
||||
operation: Operation,
|
||||
now: number,
|
||||
): IdentityRequirement;
|
||||
```
|
||||
|
||||
The classifier is pure for fixed arguments. Low-level drive options do not alter attachment status; hosted `drive()` performs its own options-aware preflight.
|
||||
|
||||
Minimum classification table:
|
||||
|
||||
| Current durable state | Immediate requirement |
|
||||
|---|---|
|
||||
| `control.cancel_requested` | none; status is `aborting` |
|
||||
| run `starting` | none |
|
||||
| checkpoint with steer/follow-up/writes to apply | none |
|
||||
| checkpoint `need_assistant` after inbox is empty | current lane model |
|
||||
| checkpoint `may_finish` after inbox is empty | none |
|
||||
| assistant/summary generation `ready` | captured model |
|
||||
| retry wait with `notBefore > now` | none; `resume()` may own the timer and status remains `resumable` |
|
||||
| retry wait with `notBefore <= now` | captured model |
|
||||
| unowned assistant/summary `effect_pending` | none; unknown-effect recovery is first |
|
||||
| deferred suspended or deferred `effect_pending` | captured model; `resume()` owns one poll/replacement-fetch permit |
|
||||
| planned tool batch before any call starts | captured active tools |
|
||||
| safe tool replay immediately requiring one tool | that captured tool |
|
||||
| unsafe orphan synthesis, outcome materialization, failure drain | none |
|
||||
| structural deciding and navigation ready-to-commit | none |
|
||||
|
||||
The implementation must exhaust every current `OperationState` variant. Do not infer requirements from the lane's full active-tool list when the next transition uses a captured batch subset or no identity.
|
||||
|
||||
### Manual progress outcomes
|
||||
|
||||
```ts
|
||||
export interface ActionRequired {
|
||||
kind: "action_required";
|
||||
action: ActionInfo;
|
||||
}
|
||||
|
||||
export type RunOutcome = ExistingRunOutcome | ActionRequired;
|
||||
export type CompactionOutcome = ExistingCompactionOutcome | ActionRequired;
|
||||
export type NavigationOutcome = ExistingNavigationOutcome | ActionRequired;
|
||||
|
||||
export type RunOperationOutcome = { operation: "run"; runId: string } & RunOutcome;
|
||||
export type CompactionOperationOutcome = {
|
||||
operation: "compaction";
|
||||
runId: string;
|
||||
} & CompactionOutcome;
|
||||
export type NavigationOperationOutcome = {
|
||||
operation: "navigation";
|
||||
runId: string;
|
||||
} & NavigationOutcome;
|
||||
|
||||
export type ResumeOutcome =
|
||||
| RunOperationOutcome
|
||||
| CompactionOperationOutcome
|
||||
| NavigationOperationOutcome;
|
||||
|
||||
export type SettledResumeOutcome = Exclude<ResumeOutcome, { kind: "action_required" }>;
|
||||
|
||||
export type RunResult = Result<RunOperationOutcome, ExistingRunAdmissionErrors>;
|
||||
export type CompactionResult = Result<CompactionOperationOutcome, ExistingCompactionAdmissionErrors>;
|
||||
export type NavigationResult = Result<NavigationOperationOutcome, ExistingNavigationAdmissionErrors>;
|
||||
export type ResumeResult = Result<ResumeOutcome, NothingToResume | Closed>;
|
||||
|
||||
export type DriveOutcome =
|
||||
| ExistingDriveOutcome
|
||||
| { kind: "action_required"; operationId: string; action: ActionInfo };
|
||||
```
|
||||
|
||||
Post-acceptance missing identities use the existing suspended/waiting outcome branch, not both that branch and `Err(MissingIdentities)`. `MissingIdentities` remains an acceptance error.
|
||||
|
||||
```ts
|
||||
interface AgentLane {
|
||||
resume(context: Context): Promise<ResumeResult>;
|
||||
executeAction(context: Context): Promise<ResumeResult>;
|
||||
runToCompletion(
|
||||
context: Context,
|
||||
): Promise<Result<SettledResumeOutcome, NothingToResume | Closed>>;
|
||||
peekAction(context: Context): Promise<ActionInfo | undefined>;
|
||||
}
|
||||
```
|
||||
|
||||
WP02 changes types and keeps these execution methods as `SliceNotImplemented`. The first drive package implements the behavior and updates convenience tests.
|
||||
|
||||
### Complete lane snapshot
|
||||
|
||||
```ts
|
||||
export interface LaneSnapshot {
|
||||
lane: string;
|
||||
transcript: Entry[];
|
||||
leafId: string | null;
|
||||
operation: null | {
|
||||
id: string;
|
||||
kind: "run" | "compaction" | "navigation";
|
||||
startedAt: number;
|
||||
status: OperationStatus;
|
||||
missing?: MissingIdentityInfo;
|
||||
action?: ActionInfo;
|
||||
retry?: { attempt: number; maxAttempts: number; nextAttemptAt: number };
|
||||
deferred?: { handle: DeferredHandle; poll: number };
|
||||
drained?: { steer: QueuedItem[]; followUp: QueuedItem[] };
|
||||
streamingMessage?: AssistantMessage;
|
||||
runningTools: {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: unknown;
|
||||
partialResult?: AgentToolResult<unknown>;
|
||||
}[];
|
||||
};
|
||||
queues: { steer: QueuedItem[]; followUp: QueuedItem[]; nextRun: QueuedItem[] };
|
||||
pendingWrites: {
|
||||
entryId: string;
|
||||
type: EntryType;
|
||||
customType?: string;
|
||||
message?: AgentMessage;
|
||||
data?: JsonValue;
|
||||
}[];
|
||||
faulted: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
Snapshot rules:
|
||||
|
||||
- `transcript` contains committed entries only;
|
||||
- `streamingMessage` is a display projection, not a committed transcript entry;
|
||||
- `runningTools[].partialResult` is a display projection from live state or durable checkpoint;
|
||||
- live partials take precedence over attachment fallback;
|
||||
- outcome-ready/materialized tools are not reported as running;
|
||||
- `action` exists only while a live manual breakpoint is parked;
|
||||
- `retry`, `deferred`, and `drained` are descriptive details, not scheduling instructions;
|
||||
- attachment and watch replay no pre-attachment lifecycle events.
|
||||
|
||||
## End-state attachment hydration
|
||||
|
||||
Attachment hydrates each lane into owned immutable process-local memory under the `Context` supplied to `AgentHarness.create(options, context)`. Projection restore, public-state hydration, and any missing-main-config seed commit pass that same context to every Session operation. Reads are exact or bounded by current state. WP02 adds no attachment-specific cancellation semantics beyond the behavior already provided by Session operations receiving `context.abortSignal`; `nextResumeIdentityRequirement(operation, now)` remains context-free and pure.
|
||||
|
||||
### Required reads — missing/mismatched faults `create()`
|
||||
|
||||
| Data | Read |
|
||||
|---|---|
|
||||
| lane projection | `laneLeaf`, `laneConfig`, `laneState`, optional `laneLastResult` |
|
||||
| open operation projection | `operationMeta`, `operationState` |
|
||||
| transcript | one compaction-bounded branch scan from current leaf |
|
||||
| pending queues | every `pendingEntry(id)` named by `pendingNextRun` and run inbox steer/follow-up |
|
||||
| pending writes | every `pendingEntry(id)` named by run inbox writes |
|
||||
| abort drain | every `pendingEntry(id)` named by drained steer/follow-up |
|
||||
| deferred display | exact deferred source message carrying its handle |
|
||||
| running tool display | batch assistant entry and exact persisted arguments for each `effect_pending` call represented in `runningTools`; planned calls have not started, and outcome-ready/completed calls are not running |
|
||||
|
||||
Required message positions must contain messages of the required role/state. Required pending values must have the expected pending kind. Duplicate/missing lineage and impossible typed relationships fault attachment.
|
||||
|
||||
### Optional reads — absence omits display fallback
|
||||
|
||||
| Data | Read |
|
||||
|---|---|
|
||||
| interrupted assistant partial | bounded pages from exact `pendingAssistantFrames(operationId, responseEntryId)` |
|
||||
| running tool checkpoint | exact `pendingToolOutput(operationId, invocationId)` |
|
||||
|
||||
A storage read failure still faults. A valid empty frame list or absent checkpoint is not corruption.
|
||||
|
||||
### Not hydrated for public inspection
|
||||
|
||||
Do not read preparation values, invocation memos, staged outcomes, or other drive-only references unless a snapshot field introduced by the normative spec directly represents them. Their consuming procedure validates them during drive.
|
||||
|
||||
### Startup complexity
|
||||
|
||||
Per lane:
|
||||
|
||||
- fixed projection point reads;
|
||||
- one compaction-bounded transcript scan;
|
||||
- O(ids named by the current lane/operation) exact value/entry reads;
|
||||
- bounded assistant frame pages and exact optional checkpoints;
|
||||
- no history-wide value scans, usage ledger reads, provider context construction, effects, hooks, tools, timers, or callbacks.
|
||||
|
||||
## Phase A — Amend `harness.md` before code
|
||||
|
||||
This phase is mandatory and has its own review stop. Do not edit runtime source until it passes review.
|
||||
|
||||
### Part 0, terminology, and invocation context
|
||||
|
||||
- Replace `SuspendedOperation` glossary/provenance entries with `OperationStatus` and `OpenOperation`.
|
||||
- Define `running`, `resumable`, `blocked`, `aborting`, and idle once.
|
||||
- State that status is process-relative observation, while durable operation phase/control remains recovery truth.
|
||||
- Remove language equating every unowned operation with `status: "suspended"`.
|
||||
- Motivate explicit Context with the two accepted requirements: correct telemetry parentage for concurrent asynchronous work, and a transport-agnostic RPC boundary for reconstructed request telemetry/cancellation.
|
||||
- Document the existing source contract: every public harness/lane operation takes a trailing `context: Context`; hooks and event listeners receive the invocation context as their final argument; Session reads/writes receive that context.
|
||||
- State that shared harness/lane/Session/SessionTree receivers retain no caller context. Context is invocation-scoped authority and propagation, not durable operation data: acceptance must not persist a `Context` or add it to the write set.
|
||||
- Specify the RPC boundary principle: an adapter maps a request trace carrier and request cancellation/disconnect into a fresh local Context, strips/inserts Context outside serialized business arguments, and never serializes the Context object, `AbortSignal`, or `TelemetryContext` as business arguments. Whether specific adapter-managed typed values cross as control-plane metadata remains open in `rpc.md`.
|
||||
- Distinguish `context.abortSignal` from durable abort: invocation or RPC cancellation must not write `cancel_requested`.
|
||||
- Keep the existing `AgentHarnessOptions.telemetryContext` option unchanged in WP02, but do not describe it as the invocation span parent; its removal belongs to the pending context/telemetry design.
|
||||
- Keep trailing parameter position and propagation behavior aligned with current source for WP02. Broader RPC transport, carrier encoding, and any future context-position migration remain open in non-normative `rpc.md` and are not decided by WP02.
|
||||
|
||||
### §3.3 — Restore validation
|
||||
|
||||
Replace the blanket "nothing else" language with two explicit attachment layers:
|
||||
|
||||
1. projection restore: exact lane/operation point values;
|
||||
2. public-state hydration: bounded state-directed reads required for coherent `open`, inspection, and initial watch.
|
||||
|
||||
Amend the trusted-values doctrine explicitly: attachment does not schema-validate trusted stored objects, but it does validate existence, address/kind discriminants, and entry type/role relationships required to construct public state. Specify required-reference fault behavior and legal optional absence. Preserve consumption-time validation for drive-only references.
|
||||
|
||||
### §3.6 — Acceptance
|
||||
|
||||
Retain the atomic run acceptance algorithm and exact transaction. Replace references to caller entries with request prompt entries. State that accepted-undriven `starting` is `resumable`: its immediate `before_run` consumption and checkpoint commit are identity-free durable progress.
|
||||
|
||||
### §3.11 and §4.6 — Queues and abort
|
||||
|
||||
- Remove `SuspendedOperation.aborting`.
|
||||
- Put drained payloads in `LaneSnapshot.operation.drained` and `AbortResult`.
|
||||
- Require attachment hydration of every queued, pending-write, and abort-drained `pendingEntry(id)` represented by the snapshot.
|
||||
|
||||
### §4.1 — Ownership and manual progress
|
||||
|
||||
- Replace suspended-owner language with four-way `OperationStatus`.
|
||||
- Add the rule that a manual breakpoint returns `action_required` instead of leaving the public drive/convenience promise unresolved.
|
||||
- Same-operation callers observing a parked pass return the same current action without releasing it.
|
||||
- Automatic mode crosses barriers and never returns `action_required`.
|
||||
|
||||
### §4.4 — Attachment and missing identities
|
||||
|
||||
Rewrite attachment as:
|
||||
|
||||
```text
|
||||
inventory
|
||||
→ projection restore
|
||||
→ bounded public-state hydration and validation
|
||||
→ phase-aware status classification
|
||||
→ publish complete harness + open[]
|
||||
```
|
||||
|
||||
Enumerate exact required and optional reads from the End-state attachment hydration section above. Fix the stale "five point lookups" count to include optional `laneLastResult` without calling it a recovery input. Remove `reason: "crash"` reconstruction.
|
||||
|
||||
Add the exhaustive identity requirement table and state that attachment classification is advisory while drive preflight is authoritative.
|
||||
|
||||
### §4.5 — Drive and recovery
|
||||
|
||||
- Keep expected-id fencing and durable rereads.
|
||||
- State that `resume(context)` inspects and drives the current operation without exposing ids.
|
||||
- Remove `MissingIdentities` from `ResumeResult` errors; use the existing suspended/waiting outcome.
|
||||
- Preserve one deferred poll permit for `resume(context)`.
|
||||
- Add `action_required` pass behavior, while leaving implementation to the next package.
|
||||
|
||||
### §5.1 — Public result surface
|
||||
|
||||
Apply the exact operation-outcome and action-required types above. Ensure convenience operation results are branches of `ResumeOutcome`, so manual progression moves from `prompt()`/`compact()` to `executeAction()` without conversion helpers. Add trailing `context: Context` to every normative method signature, and align the lane tree view with the current `readonly sessionTree: SessionTree` property rather than the stale `readonly session` name.
|
||||
|
||||
### §5.2 — Harness creation
|
||||
|
||||
- Specify `AgentHarness.create(options, context)` with trailing `context: Context` and require projection reads, hydration reads, and any initial configuration seed commit to receive it.
|
||||
- Change `{ harness, suspended }` to `{ harness, open }`.
|
||||
- Delete `SuspendedOperation`.
|
||||
- Add `OpenOperation` and the status rules.
|
||||
- State that `open` is an attachment observation, not a scheduler reservation.
|
||||
|
||||
### §5.4 — Snapshots and watches
|
||||
|
||||
- Apply the complete snapshot shape above.
|
||||
- Require `watch(context)` to capture owned memory and start buffering atomically, with no storage read.
|
||||
- Specify one lane-local observation gate ordering state publication plus synchronous event-batch enqueue against watcher registration plus synchronous memory capture. Event-bus enqueue binds each event's invocation context together with its recipient set immediately rather than consulting later listeners at delivery time. Therefore a watcher is either registered before publication and receives the `(event, context)` batch after an old snapshot, or registers after publication/enqueue and receives a new snapshot without that old batch.
|
||||
- Explain why the pair is required: local event handlers derive telemetry from the emitting invocation, while an RPC subscription injects that same source lineage into each event frame and reconstructs a fresh client-side delivery context. Subscription-establishment context must not replace event-source context.
|
||||
- Specify transcript, partial-message, checkpoint, queue, drained, retry, deferred, action, and live-over-durable precedence.
|
||||
- Remove restored crash descriptors and lifecycle replay implications.
|
||||
|
||||
### Part 8 roadmap
|
||||
|
||||
- Mark WP02 in progress and rename its outcome to atomic acceptance plus coherent attachment/watch.
|
||||
- Move real `action_required`, `executeAction`, `runToCompletion`, and convenience-drive behavior into the first no-tool drive package.
|
||||
- Keep provider generation, retry ownership, deferred polling, tools, cancellation procedures, and full session watch in their owning packages.
|
||||
|
||||
### Part 9 invariants and races
|
||||
|
||||
Amend or add:
|
||||
|
||||
- projection restore versus public-state hydration versus drive consumption;
|
||||
- successful attachment publishes no incomplete lane;
|
||||
- required attachment reference corruption faults once; optional frame/checkpoint absence is valid;
|
||||
- `blocked` is phase-aware and never prevents identity-free progress;
|
||||
- `open`/snapshot status is advisory and drive rereads durable state;
|
||||
- initial watch snapshot plus buffered events has no gap;
|
||||
- no pre-attachment lifecycle replay;
|
||||
- automatic mode never returns `action_required`;
|
||||
- no public execution promise remains parked invisibly behind a manual barrier;
|
||||
- expected-id primitive fencing remains unchanged;
|
||||
- shared receivers never retain invocation Context, and concurrent calls preserve independent telemetry/cancellation lineage;
|
||||
- Context and its values are neither durable data nor serialized business arguments;
|
||||
- buffered events retain `{ event, context }`, not only event payloads;
|
||||
- invocation/RPC cancellation does not become durable cancellation;
|
||||
- attachment/resume, registration/resume, close/attach, snapshot/resume, and external-finalization races.
|
||||
|
||||
### Phase A review stop
|
||||
|
||||
After editing only `harness.md` and this handoff:
|
||||
|
||||
1. Run a Terra librarian contradiction audit.
|
||||
2. Run a Fable work-product review.
|
||||
3. Resolve every finding.
|
||||
4. Re-read all changed normative sections together.
|
||||
5. Do not begin Phase B until the user approves the normative diff.
|
||||
|
||||
## Phase B — Implementation
|
||||
|
||||
### Public types
|
||||
|
||||
Modify `packages/agent/src/harness/agent-harness.ts`:
|
||||
|
||||
- remove `SuspendedOperation` and exports;
|
||||
- add `OperationStatus`, `OpenOperation`, `IdentityRequirement`, and `ActionRequired`;
|
||||
- change `CurrentOperationInfo`, `LaneInfo`, `LaneSnapshot`, and `SessionSnapshot`;
|
||||
- change `AgentHarness.create(options, context)` result to `open`;
|
||||
- make convenience operation values operation-tagged branches of `ResumeOutcome`;
|
||||
- remove post-acceptance `MissingIdentities` from `ResumeResult` errors;
|
||||
- add `DriveOutcome.action_required`;
|
||||
- change `executeAction(context)` and `runToCompletion(context)` result signatures;
|
||||
- preserve every existing trailing `context: Context` parameter, including expected-id primitives, and do not persist `Context`.
|
||||
|
||||
Update public type tests before runtime code.
|
||||
|
||||
### Runtime source layout
|
||||
|
||||
Add focused modules rather than growing `lane.ts` into a generic reducer:
|
||||
|
||||
- `packages/agent/src/harness/runtime2/classify.ts`
|
||||
- exhaustive `nextResumeIdentityRequirement(operation, now)`;
|
||||
- status derivation from operation, local owner presence, and current registries.
|
||||
- `packages/agent/src/harness/runtime2/hydrate.ts`
|
||||
- bounded attachment reads under the `create(options, context)` context;
|
||||
- required-reference validation;
|
||||
- immutable presentation snapshot construction;
|
||||
- optional frame/checkpoint fallback.
|
||||
- `packages/agent/src/harness/runtime2/accept.ts` only if extracting the run acceptance planner materially clarifies `Lane`; do not introduce a future operation context.
|
||||
|
||||
Modify:
|
||||
|
||||
- `runtime2/types.ts` to hold the complete owned hydrated lane projection;
|
||||
- `runtime2/restore.ts` to keep projection restoration direct and separate from public hydration;
|
||||
- `runtime2/harness.ts` to hydrate all lanes before constructing/publishing the harness and return `open`;
|
||||
- `runtime2/lane.ts` to implement acceptance, memory-only inspection, and memory-only `watch(context)`;
|
||||
- `runtime2/index.ts` exports only as needed.
|
||||
|
||||
### Atomic run acceptance
|
||||
|
||||
Implement atomic acceptance under the approved Phase A contract.
|
||||
|
||||
`accept(request, context)` performs state-independent normalization of the `OperationRequest` before `Lane.command(plan, context)`:
|
||||
|
||||
- prompt strings/images;
|
||||
- supplied message or message array;
|
||||
- explicit skill formatting;
|
||||
- prompt-template formatting;
|
||||
- pending assistant rejection;
|
||||
- unknown skill/template errors;
|
||||
- supplied or minted operation id and request prompt entry ids.
|
||||
|
||||
Keep the public prompt overloads exactly `[text, images | undefined, context]` and `[messageOrMessages, context]`, but leave both convenience implementations as `SliceNotImplemented`; WP02 must not turn `prompt()` into acceptance-only behavior.
|
||||
|
||||
The accepting context is process-local call authority, not durable operation data. Pass it through `Lane.command(plan, context)`, `session.mutate(lane, callback, context)`, every Session read/commit, and fault handling as `onFault(cause, context)`. Do not add it to operation metadata or any write.
|
||||
|
||||
Inside that one lane command:
|
||||
|
||||
1. reject busy;
|
||||
2. capture all current `pendingNextRun` ids;
|
||||
3. phase-independent admission identity preflight against the captured config/registries;
|
||||
4. read and validate captured pending messages;
|
||||
5. reject zero placed messages;
|
||||
6. parent captured next-run entries before request prompt entries;
|
||||
7. commit exactly once;
|
||||
8. publish owned hydrated state from `CommitResult`.
|
||||
|
||||
Exact writes:
|
||||
|
||||
```text
|
||||
insert captured nextRun message entries
|
||||
insert request prompt entries
|
||||
delete captured pendingEntry values
|
||||
set laneLeaf
|
||||
set operationMeta
|
||||
set operationState(run starting)
|
||||
set laneState(current operation, pendingNextRun=[])
|
||||
```
|
||||
|
||||
Post-commit events carry the exact accepting invocation context through `onEvent(event, context)` while the bus remains open:
|
||||
|
||||
```text
|
||||
run_start
|
||||
for each placed message:
|
||||
message_start
|
||||
message_end
|
||||
entry_added
|
||||
queue_update if nextRun was captured
|
||||
accept resolves
|
||||
```
|
||||
|
||||
Acceptance updates the owned transcript, leaf, operation projection, queues, and snapshot fields in the same publication. It starts no drive or effect.
|
||||
|
||||
### Memory-only inspection and watch
|
||||
|
||||
`inspectExecution(context)` derives current status from owned operation/control, current local owner state, the pure identity classifier, and current registries. It performs no storage read.
|
||||
|
||||
`watch(context)` uses the Phase A observation protocol, not the current asynchronous `HarnessEventBus.watchFromSnapshot()` ordering by itself. One lane-local observation gate serializes synchronous owned-state publication plus event-batch enqueue against synchronous watcher registration plus memory capture. Event enqueue snapshots each event's invocation context together with recipients immediately; `WatchHandle.start(listener)` later delivers `(event, context)`. Event handlers still run later and outside lane/storage mutation lines. Since capture is memory-only, watch returns a coherent clone without a gap or duplicate. Event payload mutation cannot alter owned state.
|
||||
|
||||
The main harness inherits lane `watch(context)`. `watchSession(context)` may remain later work, but all public session-snapshot type changes land now.
|
||||
|
||||
### Out-of-scope execution methods
|
||||
|
||||
`drive`, `resume`, `prompt`, `skill`, `promptFromTemplate`, `compact`, `navigateTree`, abort/queues, `executeAction`, and `runToCompletion` keep throwing `SliceNotImplemented` where WP02 does not already implement their non-executing primitive. Their updated signatures compile against future behavior.
|
||||
|
||||
Do not add `ActiveOperation`, effect gates, breakpoint execution, hooks, providers, tools, retry timers, deferred polls, terminal settlement, or usage.
|
||||
|
||||
## Required tests
|
||||
|
||||
All runtime2 tests use `BACKGROUND_CONTEXT` by default and pass it explicitly to public harness/lane and Session calls. Public type tests cover hook/listener context signatures. Runtime context-propagation cases use a distinct derived `Context` and assert object-identical delivery to acceptance event and fault boundaries without durable serialization.
|
||||
|
||||
### Public type contract
|
||||
|
||||
- `SuspendedOperation` is absent.
|
||||
- `OpenOperation` and status unions narrow exhaustively.
|
||||
- convenience operation values are assignable to `ResumeOutcome`.
|
||||
- `action_required` narrows across run/compaction/navigation and drive outcomes.
|
||||
- `executeAction(context)`/`runToCompletion(context)` signatures match the normative contract.
|
||||
- prompt overloads have the exact trailing-Context tuple shapes; `Context` is not assignable to the images or message positions.
|
||||
|
||||
### Atomic acceptance
|
||||
|
||||
Implement coverage for:
|
||||
|
||||
- text-only, images-only, text-plus-images, supplied message arrays;
|
||||
- pending assistant rejection;
|
||||
- skill with/without instructions and prompt-template arguments;
|
||||
- unknown resources and empty input write nothing;
|
||||
- supplied/minted operation ids;
|
||||
- exact transaction, parent chain, metadata, payload-free `starting`, settings snapshot, `CommitResult` materialization;
|
||||
- pending-next-run capture and deletion;
|
||||
- missing model/tools, busy structural/run operations;
|
||||
- one concurrent accept winner;
|
||||
- commit publication/failure and close races;
|
||||
- exact post-commit events carrying the accepting `Context`;
|
||||
- no callback/effect invocation.
|
||||
|
||||
### Attachment statuses
|
||||
|
||||
Construct every durable phase directly and assert:
|
||||
|
||||
- idle omitted from `open`;
|
||||
- starting and identity-free phases are resumable;
|
||||
- model boundary with missing model is blocked;
|
||||
- planned captured tool boundary with missing tool is blocked in captured order;
|
||||
- unowned effect-pending recovery is resumable despite missing identities;
|
||||
- safe replay requiring a missing tool is blocked;
|
||||
- unsafe recovery/outcome materialization is resumable;
|
||||
- deferred poll boundary is blocked only when its required model is missing;
|
||||
- cancel requested is aborting and never blocked;
|
||||
- replacing registries changes later inspection blocked/resumable without a durable write;
|
||||
- attachment never reports running.
|
||||
|
||||
### Required hydration and corruption
|
||||
|
||||
For each required reference category:
|
||||
|
||||
- valid data appears in owned memory and first snapshot;
|
||||
- missing value, wrong pending kind, wrong entry type/role, or impossible relationship rejects create with one `HarnessFault`;
|
||||
- no partial harness is returned;
|
||||
- attachment commits nothing for an already configured session;
|
||||
- no hook/provider/tool/timer/application callback runs.
|
||||
|
||||
### Optional partial recovery
|
||||
|
||||
- empty/missing assistant frame list omits streaming fallback;
|
||||
- one and many frame pages reduce in sequence order;
|
||||
- frame read failure faults attachment;
|
||||
- absent tool checkpoint omits partial result;
|
||||
- present checkpoint populates the represented running tool;
|
||||
- live partial later overrides durable fallback without changing transcript.
|
||||
|
||||
### Watch snapshots and races
|
||||
|
||||
- first snapshot includes complete transcript, queues, pending writes, drained payloads, retry/deferred details, streaming message, running tools, leaf/result/control/status;
|
||||
- unfinished assistant/tool partials are not committed transcript entries;
|
||||
- pre-attachment message/tool lifecycle events are not replayed;
|
||||
- watcher registered before concurrent acceptance publication receives either snapshot-before-plus-events or snapshot-after without a gap/duplicate;
|
||||
- a watcher started after emission receives each buffered event with the object-identical emitting Context, never the watch/start invocation context;
|
||||
- caller mutation of snapshot/event payloads does not mutate owned memory;
|
||||
- watch performs no storage read;
|
||||
- close/fault behavior matches event-bus lifecycle.
|
||||
|
||||
### Drive authority boundary
|
||||
|
||||
Without implementing drive, add direct classifier/hydration tests proving:
|
||||
|
||||
- attachment status is advisory;
|
||||
- later durable ownership changes make stale `open` harmless;
|
||||
- no hydrated display object is stored in a durable operation value;
|
||||
- expected-id primitive types remain mandatory.
|
||||
|
||||
## Files
|
||||
|
||||
### Add
|
||||
|
||||
- `packages/agent/src/harness/runtime2/classify.ts`
|
||||
- `packages/agent/src/harness/runtime2/hydrate.ts`
|
||||
- `packages/agent/test/harness/runtime2/accept.test.ts`
|
||||
- focused hydration/watch tests if `harness.test.ts` would become oversized.
|
||||
|
||||
### Modify
|
||||
|
||||
- `packages/agent/docs/harness.md` — Phase A first
|
||||
- `packages/agent/docs/work-packages/02-atomic-run-acceptance.md`
|
||||
- `packages/agent/src/harness/agent-harness.ts`
|
||||
- `packages/agent/src/harness/runtime2/harness.ts`
|
||||
- `packages/agent/src/harness/runtime2/lane.ts`
|
||||
- `packages/agent/src/harness/runtime2/restore.ts`
|
||||
- `packages/agent/src/harness/runtime2/types.ts`
|
||||
- `packages/agent/test/harness/runtime2/harness.test.ts`
|
||||
- `packages/agent/test/harness/runtime2/lane.test.ts`
|
||||
- `packages/agent/test/harness/runtime2/restore.test.ts`
|
||||
- `packages/agent/test/harness/types.test.ts`
|
||||
- test utilities only where controlled/instrumented reads require them.
|
||||
|
||||
No backend, storage schema, telemetry schema, coding-agent, or changelog change is expected. Stop for boundary review if one becomes necessary. On `dev`, defer the changelog entry.
|
||||
|
||||
## Validation
|
||||
|
||||
After Phase A docs:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
```
|
||||
|
||||
After Phase B code:
|
||||
|
||||
```bash
|
||||
cd packages/agent
|
||||
node "$(git rev-parse --show-toplevel)/node_modules/vitest/dist/cli.js" --run \
|
||||
test/harness/runtime2/accept.test.ts \
|
||||
test/harness/runtime2/harness.test.ts \
|
||||
test/harness/runtime2/lane.test.ts \
|
||||
test/harness/runtime2/restore.test.ts \
|
||||
test/harness/types.test.ts
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
git diff --check
|
||||
npm run check
|
||||
./test.sh
|
||||
```
|
||||
|
||||
If subprocess tests fail only because ignored workspace `dist` artifacts are stale, obtain explicit user authorization before `npm run build`, then rerun `./test.sh`.
|
||||
|
||||
Report runtime2 source line counts. The rebased clean pre-WP02 baseline is 970 lines; treat growth above 1,900 runtime2 source lines as a design review trigger, not a compression target.
|
||||
|
||||
## Review checkpoints
|
||||
|
||||
1. **Phase A:** Terra contradiction audit, then Fable normative/handoff review. Resolve all findings and obtain user approval.
|
||||
2. **Phase B midpoint:** review public types, classifier table, and hydration matrix before acceptance/watch implementation.
|
||||
3. **Final:** Fable implementation review after focused tests and `npm run check`; resolve findings before full validation.
|
||||
|
||||
Keep the same Fable subagent alive through the Phase A discussion so it retains the decision context.
|
||||
|
||||
## Stop condition
|
||||
|
||||
Stop when:
|
||||
|
||||
- `harness.md` contains the approved status, attachment, watch, and manual-progress contract;
|
||||
- prompt/skill/template acceptance commits exactly once into payload-free `starting`;
|
||||
- successful create publishes only fully hydrated coherent lane memory and accurate `open[]` statuses;
|
||||
- required attachment corruption faults and optional partial absence remains legal;
|
||||
- immediate watch is read-free, gap-free, and UI-complete;
|
||||
- no execution effect or owner is introduced;
|
||||
- focused tests, `npm run check`, and full tests pass;
|
||||
- runtime2 line count and deferred changelog are reported.
|
||||
|
||||
Do not begin the first real drive package. `drive`, convenience execution, action progression, hooks, providers, tools, retries, deferred polling, cancellation procedures, and terminal settlement remain the next reviewed package.
|
||||
Loading…
Add table
Add a link
Reference in a new issue