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
118 changes: 118 additions & 0 deletions examples/worktree-proximity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Worktree proximity

This advanced composition reference coordinates one root task and two child
agents working in linked worktrees of the same Git repository. Application
code records topology and current intent, detects path or dependency overlap,
warns the actor handling the current event, and publishes a durable notice
addressed to the other actor. The notice ledger attempts delivery on that
actor's next admitted event. No daemon and no native directed-message API are
required.

This example is intentionally not part of the newcomer path.

## Scenario

1. A `session/start` event records the root actor.
2. Two `agent/start` events bind native child actor IDs to distinct worktrees.
3. `tool/before` records current path and dependency intent.
4. The pure proximity domain compares active intents from different worktrees.
5. A conflict renders an `Agent.Context` warning with an `outcome: continue`
result and publishes a recipient-scoped notice.
6. The other actor's next event admits the pending notice, changes its
evidence-backed state to `attempted`, and renders its content as context.
7. `tool/after` records an empty current intent, and `stop` marks the actor
stopped.

The demonstration dependency convention is a `deps:` string in tool input:

```json
{ "file_path": "src/shared.ts", "deps": "deps:react,zod" }
```

`Write`, `Edit`, and `Read` contribute `file_path`. Dependency names are
trimmed and compared case-insensitively. Paths are normalized to
repository-relative slash-separated paths by the domain module.

## Architecture

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.
- **Events** — canonical shared-runtime routes observe actors, bind worktrees,
record or clear intent, detect conflicts, render current-actor context, and
publish or admit notices.
- **State and notices** — one workspace-durable topology definition and the
framework notice definition share the generated runtime's SQLite driver.
Routes use only the mounted `(await agent()).state` and
`(await agent()).notices` handles; SQLite supplies cross-process durability
and idempotency without a daemon.
- **Domain** — `src/domain/proximity.ts` contains all collision decisions and
performs no I/O.

The generated runtime owns the durable root. It mounts SQLite at
`$AGENT_BUNDLE_PLUGIN_ROOT/state`, with the generated artifact root as the
fallback anchor, and mounts topology state and the notice ledger over that
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.

`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.

## 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.

Unsupported worktree, actor, parent, state, and delivery conditions are
rendered as unavailable instead of being replaced with invented evidence.

## Framework primitive wiring

Generated route workers mount the extracted `src/state.ts` definition and the
notice ledger into every request scope. `withTopology` and `withNotices` are
small capability adapters over those real handles. If a surface has no
mounted handle, they return an unavailable result and the route renders that
reason as `Agent.Context`; there is no fallback write path.

Notice admission runs once per event invocation in the render scope.
Recipient matching uses the actor identity mounted in that request, and
`(await agent()).notices.read()` exposes only deliveries attempted for that
invocation. The coordinator status therefore reports topology facts only and
does not claim a whole-ledger pending count.

## Evidence boundary

The route-unit suite is in-process real-renderer evidence: it compiles the
manifest, mounts the real state and notice handles, renders the event routes,
and exercises the documented journeys against one shared durable runtime
owner. The multi-process artifact/contract suite is the next slice and has
not run here. Neither level is proof that Claude, Codex, or another commercial
host dispatches a hook, preserves its envelope, or displays projected context
in production.

## External-driver boundary

Version 1 connects no external driver adapter and claims none. A future
external adapter must pass the framework state-driver conformance suite
before any “integrated” claim. The generated runtime's SQLite driver is the
only durable driver used by this example.
11 changes: 11 additions & 0 deletions examples/worktree-proximity/agent-bundle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from 'agent-bundle/config';

export default defineConfig({
plugin: {
description: 'Coordinate related agents across linked worktrees without a daemon.',
name: 'worktree-proximity',
version: '1.0.0',
},
runtime: { node: '22.19.0' },
targets: ['claude', 'codex'],
});
33 changes: 33 additions & 0 deletions examples/worktree-proximity/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@agent-bundle-example/worktree-proximity",
"version": "1.0.0",
"private": true,
"description": "An advanced worktree-proximity coordination reference for agent-bundle.",
"type": "module",
"engines": {
"node": ">=22.19.0"
},
"files": [
"artifact",
"README.md"
],
"scripts": {
"build": "agent-bundle build --output artifact",
"check": "pnpm validate && pnpm build && pnpm typecheck && pnpm test && pnpm test:routes",
"dev": "agent-bundle dev",
"test": "rstest tests --exclude 'tests/route-unit/**'",
"test:routes": "rstest --config rstest.route-unit.config.ts",
"typecheck": "tsc -p tsconfig.json --noEmit",
"validate": "agent-bundle validate"
},
"dependencies": {
"@agent-bundle/runtime": "workspace:*",
"react": "19.2.8",
"zod": "4.4.3"
},
"devDependencies": {
"@rstest/core": "0.11.10",
"@types/react": "19.2.18",
"agent-bundle": "workspace:*"
}
}
4 changes: 4 additions & 0 deletions examples/worktree-proximity/rstest.route-unit.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineConfig } from '@rstest/core';
import { agentBundleRstest } from 'agent-bundle/rstest';

export default defineConfig(await agentBundleRstest());
36 changes: 36 additions & 0 deletions examples/worktree-proximity/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { agent } from '@agent-bundle/runtime';
import { z } from 'zod';

export const WorktreeProviderValueSchema = z.discriminatedUnion('state', [
z
.object({
branch: z.string().min(1),
commonDir: z.string().min(1),
head: z.string().min(1),
isLinkedWorktree: z.boolean(),
root: z.string().min(1),
source: z.enum(['native-cwd', 'process-cwd']),
state: z.literal('available'),
})
.strict(),
z
.object({
reason: z.string().min(1),
state: z.literal('unavailable'),
})
.strict(),
]);

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 parsed = WorktreeProviderValueSchema.safeParse(candidate);
return parsed.success
? parsed.data
: {
reason: 'The git-worktree provider did not expose a valid worktree identity.',
state: 'unavailable',
};
};
73 changes: 73 additions & 0 deletions examples/worktree-proximity/src/coordination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
agent,
type AgentStateHandle,
} from '@agent-bundle/runtime';
import type { AgentNoticesHandle } from '@agent-bundle/runtime/notices';

import {
type TopologyEvents,
type TopologyState,
} from './state.js';

export type TopologyAccess =
Pick<AgentStateHandle<TopologyState, TopologyEvents>, 'dispatch' | 'read'>;

export type CapabilityResult<T> =
| {
readonly state: 'available';
readonly value: T;
}
| {
readonly reason: string;
readonly state: 'unavailable';
};

export const withTopology = async <T>(
operation: (topology: TopologyAccess) => Promise<T>,
): Promise<CapabilityResult<T>> => {
const context = await agent();
if (context.state === undefined) {
return {
reason: 'Topology state unavailable: this request has no mounted state handle.',
state: 'unavailable',
};
}
try {
return {
state: 'available',
value: await operation(
context.state as AgentStateHandle<TopologyState, TopologyEvents>,
),
};
} catch (error) {
return {
reason:
`Topology state unavailable: ${error instanceof Error ? error.message : String(error)}`,
state: 'unavailable',
};
}
};

export const withNotices = async <T>(
operation: (notices: AgentNoticesHandle) => Promise<T>,
): Promise<CapabilityResult<T>> => {
const context = await agent();
if (context.notices === undefined) {
return {
reason: 'Directed notices unavailable: this request has no mounted notice handle.',
state: 'unavailable',
};
}
try {
return {
state: 'available',
value: await operation(context.notices),
};
} catch (error) {
return {
reason:
`Directed notices unavailable: ${error instanceof Error ? error.message : String(error)}`,
state: 'unavailable',
};
}
};
88 changes: 88 additions & 0 deletions examples/worktree-proximity/src/domain/proximity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { TopologyState } from '../state.js';

export interface ProximityIntent {
readonly actorId: string;
readonly dependencies: readonly string[];
readonly paths: readonly string[];
}

export interface ProximityConflict {
readonly actorId: string;
readonly summary: string;
}

const normalizeSegments = (value: string): string => {
const segments: string[] = [];
for (const segment of value.replaceAll('\\', '/').split('/')) {
if (segment === '' || segment === '.') continue;
if (segment === '..' && segments.length > 0 && segments.at(-1) !== '..') {
segments.pop();
} else {
segments.push(segment);
}
}
return segments.join('/');
};

const normalizePath = (value: string, worktreeRoot: string): string => {
const path = value.replaceAll('\\', '/');
const root = worktreeRoot.replaceAll('\\', '/').replace(/\/+$/u, '');
const relative = path === root
? '.'
: path.startsWith(`${root}/`)
? path.slice(root.length + 1)
: path;
return normalizeSegments(relative);
};

const normalizeDependency = (value: string): string => value.trim().toLowerCase();

export const findProximity = (
snapshot: TopologyState,
currentWorktree: string,
intent: ProximityIntent,
): readonly ProximityConflict[] => {
const currentPaths = new Set(intent.paths.map((path) => normalizePath(path, currentWorktree)).filter(Boolean));
const currentDependencies = new Set(
intent.dependencies.map(normalizeDependency).filter((dependency) => dependency !== ''),
);
const actors = new Map(snapshot.actors.map((actor) => [actor.id, actor]));
const conflicts: ProximityConflict[] = [];

for (const activity of snapshot.activities) {
if (activity.actorId === intent.actorId) continue;
const actor = actors.get(activity.actorId);
if (
actor === undefined
|| actor.status !== 'active'
|| actor.worktreeRoot === undefined
|| actor.worktreeRoot === currentWorktree
) {
continue;
}

const sharedPath = activity.paths
.map((path) => normalizePath(path, actor.worktreeRoot!))
.find((path) => currentPaths.has(path));
if (sharedPath !== undefined) {
conflicts.push({
actorId: actor.id,
summary:
`Worktrees ${currentWorktree} and ${actor.worktreeRoot} both intend to change path ${sharedPath}.`,
});
}

const sharedDependency = activity.dependencies
.map(normalizeDependency)
.find((dependency) => currentDependencies.has(dependency));
if (sharedDependency !== undefined) {
conflicts.push({
actorId: actor.id,
summary:
`Worktrees ${currentWorktree} and ${actor.worktreeRoot} both depend on ${sharedDependency}.`,
});
}
}

return conflicts;
};
Loading
Loading