From 5503486bdcd4b65f4b58d25be305f24fa5c7f911 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 17 Aug 2026 23:17:50 +0200 Subject: [PATCH] docs(agent): update context design status --- packages/agent/docs/rpc.md | 70 ++++++++++++++++++-------------- packages/agent/docs/telemetry.md | 58 ++++++++++++-------------- 2 files changed, 67 insertions(+), 61 deletions(-) diff --git a/packages/agent/docs/rpc.md b/packages/agent/docs/rpc.md index f9ad2391f..659ea3058 100644 --- a/packages/agent/docs/rpc.md +++ b/packages/agent/docs/rpc.md @@ -1,6 +1,6 @@ # Harness RPC Design Notes -> **Status:** Design input, not a normative contract or implementation handoff. The context references here match `src/harness/context.ts`; RPC integration and context propagation through the harness remain design work. Resolve the open decisions and fold the accepted behavior into `harness.md` before creating a work package. +> **Status:** Design input, not a normative contract. Required trailing `Context` parameters have landed across the harness, sessions, hosted-harness interfaces, server manager, and worker adapters. The current Pi service protocol remains a hand-written projection and still enters through `TODO_CONTEXT`; generic harness/session proxies, request cancellation, and trace-carrier propagation remain design or implementation work. Fold accepted final behavior into `harness.md`. ## Goal @@ -43,7 +43,7 @@ An ordinary proxyable method should: - return a promise; - accept only `JsonValue`-compatible business arguments; - return a `JsonValue`-compatible ordinary result or `void`; -- optionally receive one required invocation context in the declared context position. +- receive one required trailing invocation context when the receiver method is context-aware. `void` is represented by a successful response envelope without a result field. It does not require encoding `undefined` as a JSON value. @@ -53,22 +53,16 @@ Current examples requiring review include `SessionReader.getEntries()`, which re ## Context position -The implemented `Context` has no runtime brand, so a generic proxy must use trusted method metadata to distinguish context first, context last, and no-context methods. Position does not solve method discovery by itself. +Receiver methods use one required trailing `Context`. The implemented `Context` has no runtime brand, so a generic proxy must use trusted method metadata to distinguish context-aware methods from methods with no context. Position does not solve method discovery by itself. -Context first is preferable under a strict JSON argument contract: - -```ts -harness.prompt(context, text); -harness.prompt(context, text, images); -``` - -With context last, an optional argument before context requires an `undefined` array slot: +A trailing context keeps propagation visible and consistent across ordinary receivers and callbacks: ```ts harness.prompt(text, undefined, context); +harness.prompt(messages, context); ``` -`undefined` is not a `JsonValue`. Context first leaves optional business arguments trailing and omittable. The final interface migration must settle this before the signature becomes normative. +The first form exposes a strict JSON issue: after removing the context, the optional image argument still occupies an `undefined` array slot. A generic adapter must normalize omitted trailing business arguments or use an adapter projection with an explicit JSON shape. The current Pi service protocol uses `PromptArguments` as such a projection; it never serializes the harness method's raw argument list. ## Contract checking @@ -96,8 +90,8 @@ Conceptually, the client proxy performs: ```text method invocation -→ use method metadata to remove Context from its declared position, if present -→ validate remaining arguments as JSON +→ use trusted method metadata to remove the required trailing Context, when present +→ normalize omitted optional arguments and validate remaining arguments as JSON → start rpc.client span → inject trace carrier → allocate request ID @@ -114,12 +108,11 @@ interface RpcRequest { target: ObjectReference; method: string; args: JsonValue[]; - contextPosition: "first" | "last" | "none"; trace?: JsonValue; } ``` -`contextPosition` records where the server must insert its fresh local context. A trusted manifest may contain this metadata instead of accepting it from the wire. +A trusted manifest records whether the target method receives a trailing context. Untrusted wire input cannot select context placement. The context object, its values, signal, and telemetry implementation never appear in `args`. @@ -135,7 +128,7 @@ receive request → extract trace carrier into local TelemetryContext → start rpc.server span → derive Context with withTelemetryContext(serverSpan, cancellationContext) -→ insert context at declared position +→ append context for a context-aware target method → invoke exposed real method or adapter override → validate result → return JSON/protocol envelope @@ -143,11 +136,19 @@ receive request A cancel frame aborts only the matching request controller. Disconnect aborts every active request and closes every subscription owned by that connection. -`packages/protocol/src/rpc.ts` already separates a server implementation context from serialized arguments through `RpcImplementation`. It can inform this design, but the current client API has no invocation-context/cancellation/trace integration and the manifest model does not yet cover remote object references or subscriptions. +`packages/protocol/src/rpc.ts` already separates a server implementation context from serialized arguments through `RpcImplementation`. Its implementation context is dispatcher-owned and context-first; an adapter then calls the real receiver with its required trailing `Context`. The current client API has no invocation-context/cancellation/trace integration, and the manifest model does not yet cover remote object references or subscriptions. + +## Current hosted-harness adapter + +The current Pi service protocol is an explicit adapter, not the generic proxy described below. `PiServerHost`, `HostedHarnessHandle`, `HostedHarnessAttachment`, and `HostedHarnessWatch` receive trailing contexts, and `HostedHarnessManager` forwards one context through session discovery, creation, attachment, prompting, watch lifecycle, and close. + +At Pi request ingress, `PiServer` still supplies `TODO_CONTEXT`. The coding-agent worker manager accepts contexts on its hosted interfaces, but worker operation frames carry neither cancellation nor trace metadata and the worker reconstructs calls with `TODO_CONTEXT`. This is interface propagation only, not remote cancellation or telemetry support. ## Remote object identity -`AgentHarness`, `AgentLane`, `Session`, and `SessionTree` are stateful capabilities. They cross RPC boundaries as opaque references: +In the future generic proxy, `AgentHarness`, `AgentLane`, `Session`, and `SessionTree` are stateful capabilities that cross RPC boundaries as opaque references. The current Pi service exposes session IDs and adapter-owned hosted handles instead; it does not expose these domain objects directly. + +Target reference shape: ```ts interface ObjectReference { @@ -272,11 +273,13 @@ type EventFrame = The server adapts host-local `events.on()` into this protocol. The real event bus remains unaware of RPC. -Each event frame carries trace metadata from the context that emitted the event. The client reconstructs a fresh event-delivery context and invokes local listeners under that parent. It does not receive the server's `Context` object or arbitrary context values. +The current watch adapter preserves snapshot-first buffering and forwards an event context through in-process hosted-harness callbacks. The worker event frame and public Pi event envelope do not yet carry trace metadata, so both process boundaries lose the emitting context. -Subscriptions are ongoing invocation-scoped resources. They may retain their creation context and own cancellation controller; ordinary harness/session proxies may not. Aborting the subscription context or disconnecting sends/unconditionally performs unsubscribe. +The final protocol requires each event frame to carry trace metadata from the context that emitted the event. The client reconstructs a fresh event-delivery context and invokes local listeners under that parent. It does not receive the server's `Context` object or arbitrary context values. -The transport must buffer events arriving between server subscription installation and client reference decoding. Sequence numbers plus acknowledgements or a bounded flow-control policy prevent unbounded memory growth. +Subscriptions are ongoing invocation-scoped resources. They may retain their creation context and own cancellation controller; ordinary harness/session proxies may not. Aborting the subscription context or disconnecting must unconditionally unsubscribe. The current hosted interfaces expose contexts for watch creation, start, and unsubscribe, but no cancellation controller is connected to them yet. + +The current protocol buffers events across the snapshot/start handshake but has no sequence numbers, acknowledgements, or bounded backpressure policy. Those mechanisms remain necessary to prevent unbounded memory growth and define delivery guarantees. `watch()` and `watchSession()` additionally require a race-free snapshot handshake: @@ -290,11 +293,11 @@ server installs watcher in buffering mode This preserves the current snapshot-plus-buffer semantics without serializing listener callbacks. -A future client facade may reconstruct the familiar local `Events`/`WatchHandle` experience on top of this protocol. That facade lives in the RPC client package. +The current `PiClient` reconstructs its service-specific `PiLaneWatch` facade on top of `watch`, `startWatch`, and `stopWatch`. A future generic client may reconstruct the domain `Events`/`WatchHandle` experience on top of the richer subscription protocol described here. ## Request cancellation -The adapter maps each invocation's `abortSignal`, when present, to one request ID: +Request cancellation is not implemented in the current Pi client, protocol, server, or worker transport. The final adapter maps each invocation's `abortSignal`, when present, to one request ID: ```text client context.abortSignal aborts @@ -323,7 +326,7 @@ The adapter only maps disconnect to the affected invocation signal. See `telemet ## Telemetry across RPC -RPC telemetry follows: +RPC telemetry is not implemented in the current wire protocol. The target parentage is: ```text client caller @@ -354,18 +357,20 @@ Authentication, authorization, and tenant metadata may be reconstructed as typed ## Interface migration -The initial context change is scaffolding, not proof of RPC behavior. +The required trailing-context migration has landed, but it remains scaffolding rather than proof of RPC behavior. -`TODO_CONTEXT` marks a caller whose real parent is not yet threaded. `BACKGROUND_CONTEXT` is reserved for intentional roots. Temporary implementation shims may ignore context to make the repository compile, but they must not be described as cancellation or telemetry support. +`TODO_CONTEXT` marks a boundary whose real parent cannot yet be reconstructed. Current uses cluster at Pi protocol request ingress and worker RPC ingress. `BACKGROUND_CONTEXT` is reserved for intentional roots. Context-accepting shims that cannot yet transport cancellation or telemetry must not be described as semantic support. Because the superseded harness implementation has been removed, all eventual semantics must target the current harness runtime. Old-runtime propagation changes are not implementation evidence. ## Required tests for the later handoff +Current protocol and server tests cover manifest-based argument/result validation, ordinary Pi service calls, attachment/watch lifecycle, and snapshot-first event buffering. Remaining generic RPC and context coverage includes: + - static rejection of non-JSON ordinary interface members; - runtime rejection of functions, cycles, non-finite numbers, sparse arrays, and class instances; -- context is removed from serialized arguments and reconstructed server-side; -- both supported context positions or the selected final position; +- trailing context is removed from serialized arguments and reconstructed server-side; +- omitted optional arguments are normalized without serializing `undefined`; - ordinary transparent method calls; - nested remote references and stable proxy identity; - adapter override invokes/decorates the real method; @@ -383,9 +388,14 @@ Because the superseded harness implementation has been removed, all eventual sem - drive joiner cancellation does not cancel shared execution; - invocation cancellation writes no durable cancellation state. +## Resolved migration decisions + +- Context-aware receiver methods use one required trailing `Context`. +- `Context`, `AbortSignal`, and `TelemetryContext` objects are never serialized. +- The current Pi service remains an explicit adapter projection rather than a generic `AgentHarness` proxy. + ## Open decisions before `harness.md` -- context argument position on receiver methods; - whether all ordinary results must be JSON-compatible or some interfaces need RPC projections; - static contract-checking API and test location; - object-reference ownership, lifetime, and nested result encoding; diff --git a/packages/agent/docs/telemetry.md b/packages/agent/docs/telemetry.md index 346dce58b..a29b5efd4 100644 --- a/packages/agent/docs/telemetry.md +++ b/packages/agent/docs/telemetry.md @@ -1,10 +1,10 @@ # Invocation Context and Telemetry Design Notes -> **Status:** Design input, not a normative contract or implementation handoff. The context primitives described here match `src/harness/context.ts`; propagation through the harness and the remaining runtime decisions are still design work. Fold the accepted design into `harness.md` when that work lands. `telemetry-schema.md` remains the generated reference for span names and attributes. +> **Status:** Design input, not a normative contract. The context primitives and required trailing `Context` parameters have landed across the harness, sessions, execution capabilities, and hosted-harness adapters. Local propagation is scaffolding rather than proof of complete cancellation or telemetry semantics: most runtime spans, drive ownership, RPC cancellation, and cross-process trace propagation remain design or implementation work. Fold accepted final behavior into `harness.md`. `telemetry-schema.md` remains the generated reference for span names and attributes. ## Goal -`Session`, `SessionTree`, `AgentLane`, and `AgentHarness` must receive invocation-scoped control data explicitly. The same receiver may serve concurrent local callers or RPC clients, so it cannot retain a mutable or default caller context. +`Session`, `SessionTree`, `AgentLane`, and `AgentHarness` receive invocation-scoped control data explicitly through a required trailing `Context` parameter. The same receiver may serve concurrent local callers or RPC clients, so it cannot retain a mutable or default caller context. The invocation context must solve two related problems without `AsyncLocalStorage`: @@ -68,7 +68,7 @@ Every invocation supplies its own context. This prevents concurrent callers from A process-local object representing one ongoing invocation may retain its derived context. Examples are an active drive task or an event subscription. This is different from storing a default context on the shared harness or session receiver. -`AgentHarnessOptions.telemetryContext` must disappear. A harness-level default cannot represent two concurrent callers with different parents. +`AgentHarnessOptions.telemetryContext` has been removed. A harness-level default cannot represent two concurrent callers with different parents. ## Existing typed telemetry remains authoritative @@ -81,21 +81,20 @@ The design retains: - `startAiSpan()`, `startHarnessSpan()`, and `createTypedSpanStarter()`; - adapter conformance behavior. -Starting a harness span must derive the invocation context passed to lower work: +`startAiSpan()` and `startHarnessSpan()` package span derivation by giving their callbacks both the typed span and a derived invocation context: ```ts return startHarnessSpan( - context.telemetryContext, "pi.harness.run", attributes, - async (span) => { - const runContext = withTelemetryContext(span, context); + async (span, runContext) => { return runDrive(runContext); }, + context, ); ``` -A helper may package this pattern by giving its callback both the typed span and the derived invocation context. It still delegates to the existing `TelemetryContext.startSpan()` contract. +The helpers delegate to `context.telemetryContext.startSpan()` and install the callback-owned span in a child context with `withTelemetryContext(span, context)`. Lower work must receive that child context rather than the parent invocation context. Do not mutate a context to install an active span. Do not use a process-global or receiver-global current span. @@ -107,8 +106,8 @@ Explicit propagation supports concurrent sibling calls: await parent.telemetryContext.startSpan({ name: "caller" }, async (callerSpan) => { const callerContext = withTelemetryContext(callerSpan, parent); await Promise.all([ - laneA.drive(callerContext, optionsA), - laneB.drive(callerContext, optionsB), + laneA.drive(optionsA, callerContext), + laneB.drive(optionsB, callerContext), ]); }); ``` @@ -119,21 +118,19 @@ Tests must cross concurrent branches deliberately so accidental receiver-level c ## Callbacks, hooks, and events -Host-local callbacks invoked as part of an operation receive the current invocation context: +Host-local callbacks invoked as part of an operation receive the current invocation context in their declared trailing position: ```ts handler(event, context); -tool.execute(context, invocation, arguments); +tool.execute(toolCallId, params, onUpdate, toolContext, invocation, context); mutation(mutator, context); ``` -A hook span derives a child context before invoking the hook. Hook-triggered lower work therefore remains under `pi.harness.hook`. +Current propagation preserves context through callbacks and gives `before_tool` and `after_tool` handlers a child context derived from `pi.harness.hook`. Extending that span behavior to every hook type remains work; handlers without an installed hook span currently receive the operation context directly. -Events preserve the context that caused each event. A buffered event watcher must buffer `{ event, context }`, not only `event`; otherwise delayed delivery is parented to the wrong caller. Each listener invocation starts `pi.harness.event_handler` from the event context and passes the resulting child context to the listener. +Within the harness process, events preserve the context that caused each event, and buffered event watchers store `{ event, context }` rather than only `event`. Starting `pi.harness.event_handler` from that event context and passing its child context to each listener remains work. Event registration itself is host-local configuration and has no operation parent. -Event registration itself is host-local configuration and has no operation parent. Event delivery has the operation parent. - -Session commits similarly start `pi.session.write` from the invocation that issued the transaction. The mutation callback and its sole commit continue to use the same explicit invocation lineage unless they deliberately derive child spans. +Session mutation callbacks and commits receive the same explicit invocation context. Starting `pi.session.write` from the committing invocation and passing its child context through the storage commit remains work. ## Drive execution and joiners @@ -223,25 +220,19 @@ RPC cancellation and telemetry propagation are independent control-plane channel ## Interface migration scaffolding -`TODO_CONTEXT` is a temporary migration marker, not a semantic root. Use it where a caller has not yet been taught which context to propagate. `BACKGROUND_CONTEXT` means intentionally start without a caller. +Receiver methods now use a required trailing `Context`. Concrete implementations, calls, callback adapters, and object-literal façades have been migrated rather than relying only on interface assignability. -TypeScript permits an implementation method to accept fewer parameters than its interface. Therefore changing public interfaces alone does not find every implementation or concrete-class call. The migration must: +`TODO_CONTEXT` remains a temporary migration marker, not a semantic root. Current uses cluster at unresolved transport and worker boundaries that cannot yet reconstruct a caller context, notably Pi protocol request ingress and worker RPC ingress. `BACKGROUND_CONTEXT` means intentionally start without a caller. -- update concrete method signatures explicitly, even when they temporarily ignore `_context`; -- inventory concrete calls, callback adapters, and object-literal façades; -- grep or check `TODO_CONTEXT` separately; -- eventually remove every unapproved `TODO_CONTEXT` use. - -This migration can make the repository compile before semantics are implemented. Compilation does not prove telemetry or cancellation correctness. +Continue to inventory `TODO_CONTEXT` separately. Replace each transport-boundary use only when the boundary can construct a request-local cancellation context and telemetry parent; substituting `BACKGROUND_CONTEXT` would hide unfinished propagation. Compilation still does not prove telemetry or cancellation correctness. ## Required tests for the later handoff -- immutable typed-value layering, shadowing, and key isolation; -- empty roots expose an undefined abort signal and no-op telemetry; -- parent/child abort composition and sibling cancellation isolation; +Current tests cover immutable typed-value layering and shadowing, distinct empty roots, parent/child abort composition, sibling cancellation isolation, and tool-hook child parentage. Remaining handoff coverage includes: + - crossed concurrent telemetry branches on one shared receiver; -- nested hook, tool, event-handler, and session-write parentage; -- buffered events retain their emitting context; +- every hook type, tools, event handlers, and session writes receive the intended child context; +- buffered events retain their emitting context under delayed delivery; - no receiver-level telemetry default; - pre-aborted invocation starts no external effect; - installer and joiner cancellation isolation; @@ -253,9 +244,14 @@ This migration can make the repository compile before semantics are implemented. - event delivery reconstructs source trace metadata; - missing/malformed trace carriers degrade to no-op/root telemetry without affecting business behavior. +## Resolved migration decisions + +- Receiver methods use one required trailing `Context`. +- Shared harness, lane, session, and tree receivers retain no default invocation context. +- `Context`, `AbortSignal`, and `TelemetryContext` objects are never serialized across RPC boundaries. + ## Open decisions before `harness.md` -- context argument position on receiver methods; - installer-owned, lease-owned, or harness-owned drive execution; - whether telemetry links are required for joiners; - trace-carrier adapter ownership and shape;