From d9b841d8754ad20c610b7624dfe2985a03779639 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 22:58:04 +0000 Subject: [PATCH 1/3] chore(examples/worktree-proximity): resolve identity through request.lineage and useAgent() - src/api.ts: add useWorktree() over the runtime's useAgent(); the README no longer claims a hook contract is missing. - event routes: session/start, agent/start, tool/before, tool/after and stop take actor identity from (await agent()).lineage first, then the native agent_id/session_id pair, then the worktree binding, then the derived worktree: fallback. Provenance now records the runtime lineage resolution (native | registry | inferred) alongside derived. - providers/agent-topology.ts: state the actual reason the snapshot is unavailable (providers receive no identity/lineage/state, #459). - README: fix the stale useWorktree()/lineage sentences; point at #457, #458, #459, #460 for what is still missing. - route-unit tests: cover lineage-carried agent/start and tool/before. --- examples/worktree-proximity/README.md | 74 ++++++++++----- examples/worktree-proximity/src/api.ts | 18 +++- .../worktree-proximity/src/event-support.ts | 88 ++++++++++++++++-- .../src/events/agent/start.tsx | 23 ++--- .../src/events/session/start.tsx | 18 ++-- .../worktree-proximity/src/events/stop.tsx | 10 ++- .../src/events/tool/after.tsx | 3 +- .../src/events/tool/before.tsx | 2 +- .../src/providers/agent-topology.ts | 12 ++- examples/worktree-proximity/src/state.ts | 10 ++- .../tests/route-unit/routes.test.ts | 90 ++++++++++++++++++- 11 files changed, 293 insertions(+), 55 deletions(-) diff --git a/examples/worktree-proximity/README.md b/examples/worktree-proximity/README.md index b07cbce5a..9d3904115 100644 --- a/examples/worktree-proximity/README.md +++ b/examples/worktree-proximity/README.md @@ -40,7 +40,7 @@ The application has four planes: - **Providers** — `git-worktree` derives repository, branch, commit, common Git directory, and linked-worktree identity without throwing for expected degradation. `agent-topology` reports that its snapshot is unavailable - before request mounting. + because providers receive no request identity or state handle. - **Events** — canonical shared-runtime routes observe actors, bind worktrees, record or clear intent, detect conflicts, render current-actor context, and publish or admit notices. @@ -59,29 +59,53 @@ same driver. The application never opens a second store from Git identity data; `gitWorktree.commonDir` remains identity evidence only. The issue sketch places a snapshot at `providers.agentTopology.snapshot`, but -providers execute before request state is mounted, so this provider reports -an honest unavailable result and routes read snapshots from -`(await agent()).state.read()` instead. +a provider factory receives only `{ invocation, signal }` — no request +identity, no `lineage`, and no mounted `state` handle +([agent-bundle#459](https://github.com/scriptedalchemy/agent-bundle/issues/459)) — +so this provider reports an honest unavailable result and routes read +snapshots from `(await agent()).state.read()` instead. `worktree()` in `src/api.ts` is the issue-mandated custom Promise API over the -provider value. A `useWorktree()` React-hook variant is recorded unavailable: -the framework exposes no client-hook contract for provider values. +provider value. `useWorktree()` is the hook-shaped variant for Server +Components and synchronous helpers: it reads the same request handle through +the runtime's `useAgent()`, so it follows the identical lease rules and throws +the runtime's `outside-invocation` error outside a request. ## Actor identity and provenance -Every identity claim records whether it came from a native envelope or was -derived: - -- `session/start` observes `session:` as the root actor. -- `agent/start` requires native `agent_id` and `session_id`, records the child, - and records its parent session provenance as native. -- Tool envelopes contain no `agent_id`. A tool event first resolves an active - actor already bound to the event worktree. Without an earlier binding it - uses the explicit derived identity `worktree:` and records that - provenance; it never upgrades the derived identity to native. -- `agent/start` without native identity records an `edgeRefused` event and - renders that parent identity is unavailable. It refuses to fabricate a - topology edge. +Every identity claim records where it came from. `native` is read from the +host envelope (or a `request.lineage` the runtime resolved natively), +`registry` and `inferred` are the runtime lineage registry's own resolutions, +and `derived` is this application's fallback: + +- `session/start` observes `session:` as the root actor, where the root + is `(await agent()).lineage.root` when the runtime resolved a lineage and the + native `session_id` otherwise. +- `agent/start` records the child and its parent from `request.lineage` + (`subagent.id`, `parent`, `resolution`) when the runtime placed the start + below the root — which needs the spawning `Agent`/`Task` `tool/before` to + have passed through the same shared runtime — and from the native `agent_id` + + `session_id` pair otherwise. +- Claude and Codex put the subagent's `agent_id` on every one of its hook + payloads; Cursor gives the child a fresh `conversation_id` that only the + runtime registry can bind to its `subagentStart`. A tool or stop event + therefore resolves its actor in order of evidence: the child named by + `request.lineage` (depth above zero), then the native `agent_id`, then the + active actor already bound to the event worktree, and finally the explicit + derived identity `worktree:`. A carried child the topology has not + seen is observed and bound with the provenance the evidence carried; a + derived identity is never upgraded. A root-level tool envelope (lineage + depth zero) resolves through the worktree binding, so intent stays + attributed per worktree. +- `agent/start` without either lineage or native identity records an + `edgeRefused` event and renders that parent identity is unavailable. It + refuses to fabricate a topology edge. + +`request.lineage` covers the request's own parent, root, depth, and subagent +record. Siblings, children, and other roots are not exposed +([#457](https://github.com/scriptedalchemy/agent-bundle/issues/457)), so this +application keeps its own topology state for the whole-tree view the +coordinator status reports. Unsupported worktree, actor, parent, state, and delivery conditions are rendered as unavailable instead of being replaced with invented evidence. @@ -98,11 +122,15 @@ Notice admission runs once per event invocation in the render scope. Generated event principals in v1 mount host, session, and workspace identity, but not actor identity. Proximity notices therefore target the recipient worktree through `recipient.workspace.root`, while their content and dedupe -keys continue to name the target actor. Actor-directed delivery remains future -work tied to actor-principal mounting in the #99/#233 lineage. +keys continue to name the target actor. Lineage-addressed delivery +(`recipient.conversation` / `recipient.root` matched against +`request.lineage`) is tracked in +[agent-bundle#458](https://github.com/scriptedalchemy/agent-bundle/issues/458). `(await agent()).notices.read()` exposes only deliveries attempted for the -current invocation. The coordinator status therefore reports topology facts -only and does not claim a whole-ledger pending count. +current invocation; publisher-scoped visibility is +[#460](https://github.com/scriptedalchemy/agent-bundle/issues/460). The +coordinator status therefore reports topology facts only and does not claim a +whole-ledger pending count. ## Evidence boundary diff --git a/examples/worktree-proximity/src/api.ts b/examples/worktree-proximity/src/api.ts index 3da19beb9..845ba29bc 100644 --- a/examples/worktree-proximity/src/api.ts +++ b/examples/worktree-proximity/src/api.ts @@ -1,4 +1,4 @@ -import { agent } from '@agent-bundle/runtime'; +import { agent, useAgent } from '@agent-bundle/runtime'; import { z } from 'zod'; export const WorktreeProviderValueSchema = z.discriminatedUnion('state', [ @@ -24,8 +24,7 @@ export const WorktreeProviderValueSchema = z.discriminatedUnion('state', [ export type WorktreeProviderValue = z.output; export type AvailableWorktree = Extract; -export const worktree = async (): Promise => { - const candidate = (await agent()).providers.gitWorktree; +const parseWorktree = (candidate: unknown): WorktreeProviderValue => { const parsed = WorktreeProviderValueSchema.safeParse(candidate); return parsed.success ? parsed.data @@ -34,3 +33,16 @@ export const worktree = async (): Promise => { state: 'unavailable', }; }; + +/** The Promise-shaped accessor over the mounted `git-worktree` provider value. */ +export const worktree = async (): Promise => + parseWorktree((await agent()).providers.gitWorktree); + +/** + * The hook-shaped variant, for Server Components and synchronous helpers + * that cannot `await`. It reads the identical request handle through the + * runtime's `useAgent()`, so every lease rule holds unchanged: outside a + * request it throws the runtime's `outside-invocation` error. + */ +export const useWorktree = (): WorktreeProviderValue => + parseWorktree(useAgent().providers.gitWorktree); diff --git a/examples/worktree-proximity/src/event-support.ts b/examples/worktree-proximity/src/event-support.ts index 3055781d1..6f203995e 100644 --- a/examples/worktree-proximity/src/event-support.ts +++ b/examples/worktree-proximity/src/event-support.ts @@ -1,11 +1,14 @@ -import type { - AgentDocumentNode, - AgentNoticeDelivery, +import { + agent, + type AgentDocumentNode, + type AgentLineage, + type AgentNoticeDelivery, + type Observed, } from '@agent-bundle/runtime'; import type { AvailableWorktree } from './api.js'; import type { TopologyAccess } from './coordination.js'; -import type { TopologyState } from './state.js'; +import type { IdentityProvenance, TopologyState } from './state.js'; export interface EventIdentity { readonly idempotencyKey: string; @@ -19,9 +22,51 @@ export interface ExtractedIntent { export interface ResolvedActor { readonly id: string; - readonly source: 'derived' | 'native'; + readonly source: IdentityProvenance; } +/** A child actor plus the conversation that spawned it, as one envelope names them. */ +export interface CarriedChild extends ResolvedActor { + readonly parentSessionId: string; +} + +/** The conversation lineage the runtime resolved for the current request. */ +export const requestLineage = async (): Promise> => (await agent()).lineage; + +/** + * The subagent a request speaks for, when the runtime's `request.lineage` + * places it below the root. The runtime resolves the same shape on every + * host, so the route never has to know that Claude and Codex spell the child + * `agent_id` while Cursor gives it a fresh `conversation_id`. A root request + * (depth 0) is deliberately not a child; the root actor is observed at + * `session/start`. + */ +export const childFromLineage = (lineage: Observed): CarriedChild | undefined => { + if (lineage.state !== 'available' || lineage.value.depth === 0) return undefined; + return { + id: lineage.value.subagent?.id ?? lineage.value.conversation, + parentSessionId: lineage.value.parent ?? lineage.value.root, + source: lineage.value.resolution, + }; +}; + +/** + * The child actor one envelope carries: the runtime lineage first, then the + * host's own `agent_id` (Claude and Codex put the subagent's id on every one + * of its hook payloads) with the root `session_id` as its parent. `undefined` + * means the envelope names no subagent. + */ +export const carriedChild = async ( + native: Readonly>, +): Promise => { + const fromLineage = childFromLineage(await requestLineage()); + if (fromLineage !== undefined) return fromLineage; + const agentId = nativeString(native, 'agent_id'); + const sessionId = nativeString(native, 'session_id'); + if (agentId === undefined || sessionId === undefined) return undefined; + return { id: agentId, parentSessionId: sessionId, source: 'native' }; +}; + export const nativeString = ( native: Readonly>, key: string, @@ -66,12 +111,45 @@ export const extractIntent = ( }; }; +/** + * The actor a tool or stop envelope belongs to, in order of evidence: the + * child the envelope itself names (runtime lineage, then native `agent_id`), + * the active actor already bound to the event worktree, and finally the + * explicit derived identity `worktree:`. A carried child the topology + * has not seen yet (its `agent/start` was missed) is observed and bound with + * the provenance the evidence carried; a derived actor is never upgraded. + */ export const actorForWorktree = async ( topology: TopologyAccess, worktree: AvailableWorktree, canonical: EventIdentity, + native: Readonly> = {}, ): Promise<{ readonly actor: ResolvedActor; readonly snapshot: TopologyState }> => { const before = await topology.read(); + const carried = await carriedChild(native); + if (carried !== undefined) { + const known = before.state.actors.find((actor) => actor.id === carried.id); + if (known !== undefined) { + return { actor: { id: known.id, source: known.provenance.id }, snapshot: before.state }; + } + await topology.dispatch('actorObserved', { + id: carried.id, + kind: 'child', + parentSessionId: carried.parentSessionId, + provenance: { id: carried.source, parentSessionId: carried.source }, + status: 'active', + }, { + idempotencyKey: `${canonical.idempotencyKey}:carried-actor`, + }); + const boundResult = await topology.dispatch('actorBound', { + actorId: carried.id, + provenance: 'native', + worktreeRoot: worktree.root, + }, { + idempotencyKey: `${canonical.idempotencyKey}:carried-worktree`, + }); + return { actor: { id: carried.id, source: carried.source }, snapshot: boundResult.state }; + } const bound = before.state.actors.find( (actor) => actor.status === 'active' && actor.worktreeRoot === worktree.root && actor.kind === 'child', ) ?? before.state.actors.find( diff --git a/examples/worktree-proximity/src/events/agent/start.tsx b/examples/worktree-proximity/src/events/agent/start.tsx index 0df71385e..d0177b616 100644 --- a/examples/worktree-proximity/src/events/agent/start.tsx +++ b/examples/worktree-proximity/src/events/agent/start.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { worktree } from '../../api.js'; import { withNotices, withTopology } from '../../coordination.js'; -import { deliveryContexts, nativeString } from '../../event-support.js'; +import { carriedChild, deliveryContexts, nativeString } from '../../event-support.js'; export const config = { runtime: 'shared', @@ -24,10 +24,13 @@ export default async function AgentStart({ ); } - const agentId = nativeString(native, 'agent_id'); - const sessionId = nativeString(native, 'session_id'); - if (agentId === undefined || sessionId === undefined) { - const refusal = agentId === undefined + // The runtime's `request.lineage` names the child and its parent when the + // registry resolved this start; the native `agent_id` + root `session_id` + // pair is the fallback. Neither present is a refusal, never a guess. + const child = await carriedChild(native); + if (child === undefined) { + const sessionId = nativeString(native, 'session_id'); + const refusal = nativeString(native, 'agent_id') === undefined ? 'agent/start omitted native agent_id; refused to fabricate a topology edge' : 'agent/start omitted native session_id; refused to fabricate a topology edge'; const topologyResult = await withTopology(async (topology) => { @@ -58,19 +61,19 @@ export default async function AgentStart({ const topologyResult = await withTopology(async (topology) => { await topology.dispatch('actorObserved', { - id: agentId, + id: child.id, kind: 'child', - parentSessionId: sessionId, + parentSessionId: child.parentSessionId, provenance: { - id: 'native', - parentSessionId: 'native', + id: child.source, + parentSessionId: child.source, }, status: 'active', }, { idempotencyKey: `${canonical.idempotencyKey}:actor`, }); await topology.dispatch('actorBound', { - actorId: agentId, + actorId: child.id, provenance: 'native', worktreeRoot: currentWorktree.root, }, { diff --git a/examples/worktree-proximity/src/events/session/start.tsx b/examples/worktree-proximity/src/events/session/start.tsx index 80392a9d1..b0de43550 100644 --- a/examples/worktree-proximity/src/events/session/start.tsx +++ b/examples/worktree-proximity/src/events/session/start.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { worktree } from '../../api.js'; import { withNotices, withTopology } from '../../coordination.js'; -import { deliveryContexts, nativeString } from '../../event-support.js'; +import { deliveryContexts, nativeString, requestLineage } from '../../event-support.js'; export const config = { runtime: 'shared', @@ -23,8 +23,16 @@ export default async function SessionStart({ ); } - const sessionId = nativeString(native, 'session_id'); - if (sessionId === undefined) { + // The runtime's lineage names the root conversation on every host; the + // native `session_id` is the fallback when no lineage was resolved. + const lineage = await requestLineage(); + const root = lineage.state === 'available' + ? { id: lineage.value.root, source: lineage.value.resolution } + : (() => { + const sessionId = nativeString(native, 'session_id'); + return sessionId === undefined ? undefined : { id: sessionId, source: 'native' as const }; + })(); + if (root === undefined) { return ( Root actor unavailable: session/start omitted native session_id. @@ -32,12 +40,12 @@ export default async function SessionStart({ ); } - const actorId = `session:${sessionId}`; + const actorId = `session:${root.id}`; const topologyResult = await withTopology(async (topology) => { await topology.dispatch('actorObserved', { id: actorId, kind: 'root', - provenance: { id: 'native' }, + provenance: { id: root.source }, status: 'active', }, { idempotencyKey: `${canonical.idempotencyKey}:actor`, diff --git a/examples/worktree-proximity/src/events/stop.tsx b/examples/worktree-proximity/src/events/stop.tsx index 20ef7e52d..acf897da3 100644 --- a/examples/worktree-proximity/src/events/stop.tsx +++ b/examples/worktree-proximity/src/events/stop.tsx @@ -6,8 +6,8 @@ import { worktree } from '../api.js'; import { withNotices, withTopology } from '../coordination.js'; import { actorForWorktree, + carriedChild, deliveryContexts, - nativeString, type ResolvedActor, } from '../event-support.js'; @@ -28,11 +28,13 @@ export default async function Stop({ ); } - const nativeActorId = nativeString(native, 'agent_id'); + // A stop names its own actor through the runtime lineage or the native + // `agent_id`; only an anonymous stop falls back to the worktree binding. + const carried = await carriedChild(native); const topologyResult = await withTopology(async (topology): Promise => { - const resolved = nativeActorId === undefined + const resolved: ResolvedActor = carried === undefined ? (await actorForWorktree(topology, currentWorktree, canonical)).actor - : { id: nativeActorId, source: 'native' as const }; + : { id: carried.id, source: carried.source }; await topology.dispatch('actorStopped', { actorId: resolved.id, observedAt: canonical.observedAt, diff --git a/examples/worktree-proximity/src/events/tool/after.tsx b/examples/worktree-proximity/src/events/tool/after.tsx index 72c8bb3c4..7e2ce4709 100644 --- a/examples/worktree-proximity/src/events/tool/after.tsx +++ b/examples/worktree-proximity/src/events/tool/after.tsx @@ -13,6 +13,7 @@ export const config = { export default async function AfterTool({ canonical, + native, }: AgentEventRouteProps) { const currentWorktree = await worktree(); if (currentWorktree.state === 'unavailable') { @@ -23,7 +24,7 @@ export default async function AfterTool({ ); } const topologyResult = await withTopology(async (topology) => { - const resolved = await actorForWorktree(topology, currentWorktree, canonical); + const resolved = await actorForWorktree(topology, currentWorktree, canonical, native); await topology.dispatch('intentRecorded', { actorId: resolved.actor.id, dependencies: [], diff --git a/examples/worktree-proximity/src/events/tool/before.tsx b/examples/worktree-proximity/src/events/tool/before.tsx index 803e175ad..23cef57bc 100644 --- a/examples/worktree-proximity/src/events/tool/before.tsx +++ b/examples/worktree-proximity/src/events/tool/before.tsx @@ -30,7 +30,7 @@ export default async function BeforeTool({ } const intent = extractIntent(native); const topologyResult = await withTopology(async (topology) => { - const { actor } = await actorForWorktree(topology, currentWorktree, canonical); + const { actor } = await actorForWorktree(topology, currentWorktree, canonical, native); const committed = await topology.dispatch('intentRecorded', { actorId: actor.id, dependencies: [...intent.dependencies], diff --git a/examples/worktree-proximity/src/providers/agent-topology.ts b/examples/worktree-proximity/src/providers/agent-topology.ts index 31c47432f..2c8afdc81 100644 --- a/examples/worktree-proximity/src/providers/agent-topology.ts +++ b/examples/worktree-proximity/src/providers/agent-topology.ts @@ -3,10 +3,20 @@ export interface AgentTopologyProviderValue { readonly state: 'unavailable'; } +/** + * The issue sketch places a topology snapshot at + * `providers.agentTopology.snapshot`. A conventional provider factory still + * receives only `{ invocation, signal }` — no request identity, no + * `lineage`, and no mounted `state`/`notices` handles (agent-bundle#459) — + * so it cannot derive that view. Routes read the topology from + * `(await agent()).state` and their own place in the conversation tree from + * `(await agent()).lineage` instead; this provider reports the gap honestly + * rather than opening a second store. + */ export default function agentTopologyProvider(): AgentTopologyProviderValue { return { reason: - 'Topology snapshots are available only from the mounted request state handle; providers execute before that handle is mounted.', + 'Topology snapshots are available only from the mounted request state handle; providers receive no request identity, lineage, or state handle (agent-bundle#459).', state: 'unavailable', }; } diff --git a/examples/worktree-proximity/src/state.ts b/examples/worktree-proximity/src/state.ts index 1bee532b1..d3b06ac63 100644 --- a/examples/worktree-proximity/src/state.ts +++ b/examples/worktree-proximity/src/state.ts @@ -2,7 +2,15 @@ import { defineState } from '@agent-bundle/runtime/state'; import { z } from 'zod'; const nonEmpty = z.string().trim().min(1); -const provenance = z.enum(['native', 'derived']); +/** + * Where an identity claim came from: `native` is read straight from the host + * envelope (or a `request.lineage` the runtime resolved natively), `registry` + * and `inferred` are the runtime lineage registry's own resolutions, and + * `derived` is this application's fallback (`worktree:`). + */ +const provenance = z.enum(['native', 'registry', 'inferred', 'derived']); + +export type IdentityProvenance = z.output; export const ActorSchema = z .object({ diff --git a/examples/worktree-proximity/tests/route-unit/routes.test.ts b/examples/worktree-proximity/tests/route-unit/routes.test.ts index 88dfb8c78..3eba06a1c 100644 --- a/examples/worktree-proximity/tests/route-unit/routes.test.ts +++ b/examples/worktree-proximity/tests/route-unit/routes.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from '@rstest/core'; -import { available } from '@agent-bundle/runtime'; +import { available, type AgentLineage, type Observed } from '@agent-bundle/runtime'; import { createGeneratedRuntimeState, type GeneratedRuntimeState, @@ -66,6 +66,7 @@ const renderEventInput = async ( id: string, worktreeRoot: string, actorId?: string, + lineage?: Observed, ) => { const bindings = await runtimeState.requestBindings(); try { @@ -77,6 +78,7 @@ const renderEventInput = async ( id: `invocation:${id}`, startedAt: `2026-09-01T20:01:${String(sequence).padStart(2, '0')}.000Z`, }, + ...(lineage === undefined ? {} : { lineage }), noticeLedger: bindings.noticeLedger, providers: { gitWorktree: provider(worktreeRoot) }, session: available({ sessionId: 'root-session' }, 'native'), @@ -97,14 +99,26 @@ const renderEvent = ( id: string, worktreeRoot: string, actorId?: string, + lineage?: Observed, ) => renderEventInput( route, eventInput(event, native, id), id, worktreeRoot, actorId, + lineage, ); +/** A runtime-registry lineage placing `id` one level below `root-session`. */ +const childLineage = (id: string): Observed => available({ + conversation: id, + depth: 1, + parent: 'root-session', + resolution: 'registry', + root: 'root-session', + subagent: { id }, +}, 'derived'); + const bindActors = async (): Promise => { await renderEvent( 'event:session/start', @@ -340,6 +354,80 @@ describe('worktree proximity journeys', () => { } }); + it('records the child and its parent from request.lineage when the envelope carries no agent_id', async () => { + await bindActors(); + const rendered = await renderEvent( + 'event:agent/start', + 'agent/start', + { + agent_type: 'implementation', + cwd: worktrees.b, + hook_event_name: 'SubagentStart', + session_id: 'root-session', + }, + 'agent-c:start', + worktrees.b, + undefined, + childLineage('agent-c'), + ); + + expectDocument(rendered).toHaveStatus('success').toHaveNodeKinds(['result']); + + const bindings = await runtimeState.requestBindings(); + try { + const snapshot = await bindings.state.read(); + expect(snapshot.state.refusals).toEqual([]); + expect(snapshot.state.actors).toEqual(expect.arrayContaining([ + expect.objectContaining({ + id: 'agent-c', + kind: 'child', + parentSessionId: 'root-session', + provenance: expect.objectContaining({ id: 'registry', parentSessionId: 'registry' }), + worktreeRoot: worktrees.b, + }), + ])); + } finally { + await bindings.close(); + } + }); + + it('attributes a tool envelope to the lineage child ahead of the worktree binding', async () => { + await bindActors(); + const rendered = await renderEvent( + 'event:tool/before', + 'tool/before', + { + cwd: worktrees.a, + hook_event_name: 'PreToolUse', + session_id: 'root-session', + tool_input: { file_path: 'src/shared.ts' }, + tool_name: 'Edit', + }, + 'intent:c', + worktrees.a, + undefined, + childLineage('agent-c'), + ); + + expectDocument(rendered).toHaveStatus('success'); + expect(rendered.document.value).toEqual({ outcome: 'continue' }); + + const bindings = await runtimeState.requestBindings(); + try { + const snapshot = await bindings.state.read(); + expect(snapshot.state.actors).toEqual(expect.arrayContaining([ + expect.objectContaining({ + id: 'agent-c', + provenance: expect.objectContaining({ id: 'registry', parentSessionId: 'registry' }), + worktreeRoot: worktrees.a, + }), + ])); + expect(snapshot.state.activities.map((activity) => activity.actorId)).toEqual(['agent-c']); + } finally { + await bindings.close(); + } + }); + it('renders the coordinator status from mounted topology state', async () => { await bindActors(); await recordIntent('agent-a', worktrees.a, 'src/shared.ts', 'intent:a'); From 71daa85b7c3ff7ca69a84a1efa6fccabd080861b Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 23:01:16 +0000 Subject: [PATCH 2/3] chore(examples/audiobook-curator): drop the unused @modelcontextprotocol/server dependency The generated MCP server ships its own transport; no module in the example imports the SDK. Lockfile refreshed for the removed importer entry. --- examples/audiobook-curator/package.json | 1 - pnpm-lock.yaml | 3 --- 2 files changed, 4 deletions(-) diff --git a/examples/audiobook-curator/package.json b/examples/audiobook-curator/package.json index a57e96c6e..c2e6ad81a 100644 --- a/examples/audiobook-curator/package.json +++ b/examples/audiobook-curator/package.json @@ -32,7 +32,6 @@ }, "dependencies": { "@agent-bundle/runtime": "workspace:*", - "@modelcontextprotocol/server": "2.0.0", "react": "19.2.8", "zod": "4.5.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index faad98031..f2d7842dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,9 +89,6 @@ importers: '@agent-bundle/runtime': specifier: workspace:* version: link:../../packages/rsc-runtime - '@modelcontextprotocol/server': - specifier: 2.0.0 - version: 2.0.0 react: specifier: 19.2.8 version: 19.2.8 From 7811fcb94af47cb884c9ed7836f115d6da9ebb6e Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 23:06:21 +0000 Subject: [PATCH 3/3] chore(examples/rsc-agent-runtime): take Json* and DevRuntimeProvider types from public exports JsonObject/JsonValue come from the agent-bundle root export and DevRuntimeProvider from agent-bundle/api; both were reached through ../../../../packages/agent-bundle/src/dev before. The remaining deep imports (runtime-protocol, runtime-generation-store, runtime-mcp-registry, the rest of runtime-provider) have no public counterpart yet. --- .../rsc-agent-runtime/src/dev/generation-materializer.ts | 2 +- examples/rsc-agent-runtime/src/dev/provider.ts | 5 ++++- .../rsc-agent-runtime/src/dev/rsbuild-runtime-session.ts | 2 +- examples/rsc-agent-runtime/src/dev/serialize-inspection.ts | 2 +- examples/rsc-agent-runtime/src/runtime/contracts.ts | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/examples/rsc-agent-runtime/src/dev/generation-materializer.ts b/examples/rsc-agent-runtime/src/dev/generation-materializer.ts index 8f7a75b67..b166664f8 100644 --- a/examples/rsc-agent-runtime/src/dev/generation-materializer.ts +++ b/examples/rsc-agent-runtime/src/dev/generation-materializer.ts @@ -18,7 +18,7 @@ import type { } from '../runtime/contracts.js'; import type { DevRuntimePreparedProject } from '../../../../packages/agent-bundle/src/dev/runtime-provider.ts'; import type { DevRuntimeMcpServerDescriptor } from '../../../../packages/agent-bundle/src/dev/runtime-protocol.ts'; -import type { JsonObject, JsonValue } from '../../../../packages/agent-bundle/src/dev/types.ts'; +import type { JsonObject, JsonValue } from 'agent-bundle'; import type { RuntimeGenerationActivationGuard, RuntimeGenerationAsset, diff --git a/examples/rsc-agent-runtime/src/dev/provider.ts b/examples/rsc-agent-runtime/src/dev/provider.ts index d45d7fc69..d41d926cb 100644 --- a/examples/rsc-agent-runtime/src/dev/provider.ts +++ b/examples/rsc-agent-runtime/src/dev/provider.ts @@ -1,7 +1,10 @@ -import type { DevRuntimeProvider, DevRuntimeStartContext } from '../../../../packages/agent-bundle/src/dev/runtime-provider.ts'; +import type { DevRuntimeProvider } from 'agent-bundle/api'; import { RsbuildRuntimeSession } from './rsbuild-runtime-session.js'; +/** The start context the public provider contract hands `start`; the framework exports only the provider type itself. */ +type DevRuntimeStartContext = Parameters[0]; + export const createDevRuntimeProvider = (): DevRuntimeProvider => Object.freeze({ descriptor: Object.freeze({ environmentVariables: Object.freeze([]), diff --git a/examples/rsc-agent-runtime/src/dev/rsbuild-runtime-session.ts b/examples/rsc-agent-runtime/src/dev/rsbuild-runtime-session.ts index a3b1fb5f7..a8a776cb2 100644 --- a/examples/rsc-agent-runtime/src/dev/rsbuild-runtime-session.ts +++ b/examples/rsc-agent-runtime/src/dev/rsbuild-runtime-session.ts @@ -84,7 +84,7 @@ import { type DevRuntimeSurface, type RuntimeVector, } from '../../../../packages/agent-bundle/src/dev/runtime-protocol.ts'; -import type { JsonObject, JsonValue } from '../../../../packages/agent-bundle/src/dev/types.ts'; +import type { JsonObject, JsonValue } from 'agent-bundle'; const descriptor: DevRuntimeDescriptor = Object.freeze({ environmentVariables: Object.freeze([]), diff --git a/examples/rsc-agent-runtime/src/dev/serialize-inspection.ts b/examples/rsc-agent-runtime/src/dev/serialize-inspection.ts index c1f009796..2a9d8f8c7 100644 --- a/examples/rsc-agent-runtime/src/dev/serialize-inspection.ts +++ b/examples/rsc-agent-runtime/src/dev/serialize-inspection.ts @@ -5,7 +5,7 @@ import type { DevRuntimeTraceSpan, DevRuntimeTreeNode, } from '../../../../packages/agent-bundle/src/dev/runtime-protocol.ts'; -import type { JsonObject, JsonValue } from '../../../../packages/agent-bundle/src/dev/types.ts'; +import type { JsonObject, JsonValue } from 'agent-bundle'; const inspectionStartedAt = '1970-01-01T00:00:00.000Z'; const flightPreviewBytes = 32 * 1024; diff --git a/examples/rsc-agent-runtime/src/runtime/contracts.ts b/examples/rsc-agent-runtime/src/runtime/contracts.ts index e103ce5b4..3dd8def27 100644 --- a/examples/rsc-agent-runtime/src/runtime/contracts.ts +++ b/examples/rsc-agent-runtime/src/runtime/contracts.ts @@ -3,7 +3,7 @@ import type { DevRuntimeInspectionEnvelope, DevRuntimeMcpServerDescriptor, } from '../../../../packages/agent-bundle/src/dev/runtime-protocol.ts'; -import type { JsonObject } from '../../../../packages/agent-bundle/src/dev/types.ts'; +import type { JsonObject } from 'agent-bundle'; export interface EditEvent { eventId: string;