Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion examples/audiobook-curator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
},
"dependencies": {
"@agent-bundle/runtime": "workspace:*",
"@modelcontextprotocol/server": "2.0.0",
"react": "19.2.8",
"zod": "4.5.4"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion examples/rsc-agent-runtime/src/dev/provider.ts
Original file line number Diff line number Diff line change
@@ -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<DevRuntimeProvider['start']>[0];

export const createDevRuntimeProvider = (): DevRuntimeProvider => Object.freeze({
descriptor: Object.freeze({
environmentVariables: Object.freeze([]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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([]),
Expand Down
2 changes: 1 addition & 1 deletion examples/rsc-agent-runtime/src/dev/serialize-inspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion examples/rsc-agent-runtime/src/runtime/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
74 changes: 51 additions & 23 deletions examples/worktree-proximity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:<session_id>` 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:<root>` 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:<root>` 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:<root>`. 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.
Expand All @@ -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

Expand Down
18 changes: 15 additions & 3 deletions examples/worktree-proximity/src/api.ts
Original file line number Diff line number Diff line change
@@ -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', [
Expand All @@ -24,8 +24,7 @@ export const WorktreeProviderValueSchema = z.discriminatedUnion('state', [
export type WorktreeProviderValue = z.output<typeof WorktreeProviderValueSchema>;
export type AvailableWorktree = Extract<WorktreeProviderValue, { state: 'available' }>;

export const worktree = async (): Promise<WorktreeProviderValue> => {
const candidate = (await agent()).providers.gitWorktree;
const parseWorktree = (candidate: unknown): WorktreeProviderValue => {
const parsed = WorktreeProviderValueSchema.safeParse(candidate);
return parsed.success
? parsed.data
Expand All @@ -34,3 +33,16 @@ export const worktree = async (): Promise<WorktreeProviderValue> => {
state: 'unavailable',
};
};

/** The Promise-shaped accessor over the mounted `git-worktree` provider value. */
export const worktree = async (): Promise<WorktreeProviderValue> =>
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);
88 changes: 83 additions & 5 deletions examples/worktree-proximity/src/event-support.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Observed<AgentLineage>> => (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<AgentLineage>): 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<Record<string, unknown>>,
): Promise<CarriedChild | undefined> => {
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<Record<string, unknown>>,
key: string,
Expand Down Expand Up @@ -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:<root>`. 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<Record<string, unknown>> = {},
): 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(
Expand Down
23 changes: 13 additions & 10 deletions examples/worktree-proximity/src/events/agent/start.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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) => {
Expand Down Expand Up @@ -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,
}, {
Expand Down
Loading
Loading