Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
6d155f3
feat(dev): trace entry contract and bounded TraceHub for the unified …
ScriptedAlchemy Sep 5, 2026
f9b2c40
feat(workbench): correlate route workspace traces
ScriptedAlchemy Sep 5, 2026
8d07e96
feat(dev): fan runtime and logs into trace
ScriptedAlchemy Sep 5, 2026
ee6b592
feat: publish route invocation kernel traces
ScriptedAlchemy Sep 5, 2026
b219481
feat(dev): add unified trace hub routes
ScriptedAlchemy Sep 5, 2026
c5fb87f
feat(dev): lower Workbench MCP session traces onto the unified trace …
ScriptedAlchemy Sep 5, 2026
52e2026
docs: explain unified Workbench trace
ScriptedAlchemy Sep 5, 2026
6fc96f4
feat(workbench): Trace page as a correlated live timeline over /api/t…
ScriptedAlchemy Sep 5, 2026
dcd4222
feat(dev): host-invoked hook receipts on the trace (#600 PR 2, lane T7)
ScriptedAlchemy Sep 5, 2026
3f57098
integrate T1: trace hub routes
ScriptedAlchemy Sep 5, 2026
118e2cb
integrate T2: route invocation + kernel traces
ScriptedAlchemy Sep 5, 2026
ee0119f
integrate T3
ScriptedAlchemy Sep 5, 2026
3449806
integrate T4
ScriptedAlchemy Sep 5, 2026
1da8148
integrate T7
ScriptedAlchemy Sep 5, 2026
121df4a
integrate T5
ScriptedAlchemy Sep 5, 2026
d1d3430
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
170deed
integrate T6
ScriptedAlchemy Sep 5, 2026
9617bc0
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
2ca4ae8
integrate T8
ScriptedAlchemy Sep 5, 2026
91897af
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
2540fd6
integrate: TraceHub requires projectRoot in lane tests
ScriptedAlchemy Sep 5, 2026
d3fba52
Merge remote-tracking branch 'origin/main' into wb600-pr2-trace
ScriptedAlchemy Sep 5, 2026
7cb34c6
docs(zh): document unified Workbench trace
ScriptedAlchemy Sep 5, 2026
4fcdff2
feat(dev): wire unified server trace
ScriptedAlchemy Sep 5, 2026
5c50805
Rewrite Trace acceptance for the live /trace page and pin heading mat…
ScriptedAlchemy Sep 5, 2026
4a1ba29
integrate W1
ScriptedAlchemy Sep 5, 2026
696a73b
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
fe78286
integrate W4
ScriptedAlchemy Sep 5, 2026
55a4dc3
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
6dbde2c
integrate W3
ScriptedAlchemy Sep 5, 2026
a93bee3
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
4263f04
W2: browser wiring for unified trace (#600 PR 2)
ScriptedAlchemy Sep 5, 2026
b338d9a
fix trace summary signal quality
ScriptedAlchemy Sep 5, 2026
67e2526
integrate W2
ScriptedAlchemy Sep 5, 2026
91e1960
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
8d46942
integrate W5
ScriptedAlchemy Sep 5, 2026
e2b8e5b
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
dcef330
fix(dev): stop fabricating route-invocation provider timings
ScriptedAlchemy Sep 5, 2026
a3cce28
fix(dev): pin route invocations to a leased epoch (AB8239)
ScriptedAlchemy Sep 5, 2026
de055e5
fix(dev): rewrite only module specifiers in the route invocation chil…
ScriptedAlchemy Sep 5, 2026
fef0bbe
integrate A4
ScriptedAlchemy Sep 5, 2026
912f1b7
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
5efc4b0
integrate A3
ScriptedAlchemy Sep 5, 2026
502e64f
fix merged telemetry/lease test fixtures
ScriptedAlchemy Sep 5, 2026
0d5925b
integrate A1
ScriptedAlchemy Sep 5, 2026
5102943
fix: carry runtime MCP trace correlation
ScriptedAlchemy Sep 5, 2026
536f589
integrate W6
ScriptedAlchemy Sep 5, 2026
2f56483
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
5de5f1c
feat: execute routes from compiled artifacts
ScriptedAlchemy Sep 5, 2026
1b7d78e
integrate A2
ScriptedAlchemy Sep 5, 2026
61c8a32
integrate A2: args check after prepared lease
ScriptedAlchemy Sep 5, 2026
68a938e
integrate A2: trace type through the invocations contract; unit-rende…
ScriptedAlchemy Sep 5, 2026
4dbe4df
integration test: observed provider telemetry from the compiled worker
ScriptedAlchemy Sep 5, 2026
83b28ed
changeset: PR 2a execution parity + provenance
ScriptedAlchemy Sep 5, 2026
0723fc1
Merge origin/main (#635, #640, #636) into wb600-pr2a-exec
ScriptedAlchemy Sep 5, 2026
fdd8932
fix workbench invocation epoch state parity
ScriptedAlchemy Sep 5, 2026
c44ef46
integrate A5
ScriptedAlchemy Sep 5, 2026
fd46e89
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
58ec27b
Merge remote-tracking branch 'origin/main' into wb600-pr2a-exec
ScriptedAlchemy Sep 5, 2026
a89aa6c
changeset: name #643
ScriptedAlchemy Sep 5, 2026
2f2ad57
fix(dev): retain state across epoch rebuilds
ScriptedAlchemy Sep 5, 2026
7fcc028
feat(workbench): select invocation surfaces
ScriptedAlchemy Sep 5, 2026
8957f3b
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
796096b
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
c61cdb9
integrate A6 (surface model) and A9 (dev state root)
ScriptedAlchemy Sep 5, 2026
a12423d
fix stateful test helper call; runtime-environment docs follow the pr…
ScriptedAlchemy Sep 5, 2026
657d7f1
Report route invocation outcome separately from execution status (#64…
ScriptedAlchemy Sep 5, 2026
1a1a46f
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
44bfc7d
integrate A7 (outcome vs execution status)
ScriptedAlchemy Sep 5, 2026
db99d4a
project isolated renders by the route's default surface; port exit fi…
ScriptedAlchemy Sep 5, 2026
6c15152
test: cover route invocation acceptance fixtures
ScriptedAlchemy Sep 5, 2026
551130d
fix workbench route review findings
ScriptedAlchemy Sep 5, 2026
564b1e6
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
d9aeaf5
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
1b451a5
integrate A10 (review fixes) and A8 (six acceptance fixtures); keep t…
ScriptedAlchemy Sep 5, 2026
18739c3
Merge origin/main (#604 manifest v2, #634, #645, #646, #649) into wb6…
ScriptedAlchemy Sep 5, 2026
629738f
Deslop pass for #643 (lane A11)
ScriptedAlchemy Sep 5, 2026
4908c01
drop LANE-NOTES
ScriptedAlchemy Sep 5, 2026
527878b
test: give the compiled-artifact dev-server suites a load-tolerant bu…
ScriptedAlchemy Sep 5, 2026
e1cf6d3
test: keep the parity fixture's handler marker out of the watched sou…
ScriptedAlchemy Sep 6, 2026
d04ff7c
Merge remote-tracking branch 'origin/main' into wb600-pr2a-exec
ScriptedAlchemy Sep 6, 2026
55be005
test(workbench): exact heading locators through expectHeading (#632)
ScriptedAlchemy Sep 6, 2026
25db456
merge: integrate route execution and live trace
ScriptedAlchemy Sep 6, 2026
79ed2cb
Merge remote-tracking branch 'origin/main' into wb600-pr2a-exec
ScriptedAlchemy Sep 6, 2026
d1a7282
fix: review round 2 — Workbench-only worker input validation, host se…
ScriptedAlchemy Sep 6, 2026
a47f9f8
fix(workbench): loaded invocation host wins over the preflight defaul…
ScriptedAlchemy Sep 6, 2026
c5f4114
Merge branch 'lane/wb600-pr2b-b0' into wb600-pr2-trace
ScriptedAlchemy Sep 6, 2026
bcb1b4a
drop LANE-NOTES
ScriptedAlchemy Sep 6, 2026
05778fa
Merge branch 'wb600-pr2a-exec' into wb600-pr2-trace
ScriptedAlchemy Sep 6, 2026
7f2dd25
fix(workbench): the preflight default repairs an invalid host selecti…
ScriptedAlchemy Sep 6, 2026
f1eba9c
Merge remote-tracking branch 'origin/main' into wb600-pr2a-exec
ScriptedAlchemy Sep 6, 2026
ccdc075
Merge branch 'wb600-pr2a-exec' into wb600-pr2-trace
ScriptedAlchemy Sep 6, 2026
8496883
Merge origin/main (PR 2a squash 3afcdc9d87, #648) into wb600-pr2-trace
ScriptedAlchemy Sep 6, 2026
7c6fa97
feat(workbench): stream live route invocations
ScriptedAlchemy Sep 6, 2026
0f3aa1e
Merge branch 'lane/wb600-pr2b-b1' into wb600-pr2-trace
ScriptedAlchemy Sep 6, 2026
bad25c8
drop LANE-NOTES
ScriptedAlchemy Sep 6, 2026
6c14c45
test(workbench): accept live trace flows
ScriptedAlchemy Sep 6, 2026
9099fc3
Merge branch 'lane/wb600-pr2b-b2' into wb600-pr2-trace
ScriptedAlchemy Sep 6, 2026
ba6a8c3
drop LANE-NOTES
ScriptedAlchemy Sep 6, 2026
5522449
Merge remote-tracking branch 'origin/main' into lane/wb600-pr2b-b3
ScriptedAlchemy Sep 6, 2026
9635379
chore(workbench): merge and deslop live trace
ScriptedAlchemy Sep 6, 2026
74c7482
Merge branch 'lane/wb600-pr2b-b3' into wb600-pr2-trace
ScriptedAlchemy Sep 6, 2026
690e94d
drop LANE-NOTES
ScriptedAlchemy Sep 6, 2026
ed1b0f3
Merge remote-tracking branch 'origin/main' into wb600-pr2-trace
ScriptedAlchemy Sep 6, 2026
aebe042
changeset: name PR #666
ScriptedAlchemy Sep 6, 2026
fe06da0
fix trace review findings
ScriptedAlchemy Sep 6, 2026
1349ee8
drop LANE-NOTES
ScriptedAlchemy Sep 6, 2026
632d180
Merge remote-tracking branch 'origin/main' into wb600-pr2-trace
ScriptedAlchemy Sep 6, 2026
686fa7a
fix(trace): round-2 review — drop runId from trace lowering, stale ru…
ScriptedAlchemy Sep 6, 2026
ba29609
docs(trace): drop the stale runtime correlationId prose; runtime.even…
ScriptedAlchemy Sep 6, 2026
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
5 changes: 5 additions & 0 deletions .changeset/wb600-live-trace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'agent-bundle': minor
---

Add the Workbench Trace page and live development tracing through `/api/trace`, `/api/trace/stream`, and `/api/trace/receipts`; stream route runs with `POST /api/routes/invocations` using `stream: true` and `GET /api/routes/invocations/<id>/stream`, cancel them with `POST /api/routes/invocations/<id>/cancel`, and report cancelled runs with `status: 'cancelled'`. `route.invocation` project events now also fire when an invocation starts, with a running record (no `completedAt`/`outcome`); narrow on `status` before reading completion fields. `createEventTracer` called without `observer` now reads the process-local observer on every emission (and `enabled` reflects it live) instead of being permanently disabled, so a tracer created before `installEventTraceObserver` starts emitting once one is installed. Add diagnostics `AB8240`–`AB8243`, `AB8247`–`AB8249`, and `AB8256`. (#666)
4 changes: 4 additions & 0 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,13 @@ even when no error diagnostic was reported.
| `AB8215`–`AB8218` | Workbench read-only host discovery route (`/api/discovery`): `AB8215` invalid path, `AB8216` query string or non-`GET` method (400/405), `AB8217` report over the 16 MiB response limit (413), `AB8218` discovery not available (503). |
| `AB8219`–`AB8223` | Workbench live MCP probe route (user-initiated, read-only initialize + tools/list): `AB8219` invalid path, `AB8220` invalid request/method, `AB8221` probe target not found, `AB8222` response over the 16 MiB budget, `AB8223` probe unavailable. |
| `AB8233`–`AB8235` | Workbench browser-side strict decoders rejecting a dev-server response: `AB8233` lifecycle replay, `AB8234` host discovery, `AB8235` MCP probe report. |
| `AB8243` | Workbench browser-side strict decoder rejecting a `/api/trace` replay or NDJSON stream frame (unknown `source`, malformed correlation, unsafe text, or a cursor the reply does not account for). It sits between the server-side trace routes (`AB8240`–`AB8242`) and the hook receipt route (`AB8247`–`AB8249`); `AB8244`–`AB8246` are unassigned. |
| `AB8231`–`AB8232`, `AB8236`–`AB8238` | Workbench route invocation service (`/api/routes/invocations`): `AB8231` unknown route or invocation id, `AB8232` no published build / invocation manifest unavailable, `AB8236` render child timed out or crashed, `AB8237` malformed invocation request, and `AB8238` unknown fixture id. (`AB8233`–`AB8235` were already assigned to browser-side decoders.) |
| `AB8240`–`AB8242` | Workbench unified trace routes (`/api/trace`, `/api/trace/stream`): `AB8240` invalid `after` cursor (400), `AB8241` cursor ahead of the current trace sequence (409), and `AB8242` trace routes unavailable before composition or during shutdown (404/503). |
| `AB8247`–`AB8249` | Workbench hook receipt route (`POST /api/trace/receipts`, posted by a generated hook wrapper of the dev plugin): `AB8247` receipt refused — peer not loopback, `Origin` header present, missing or wrong bearer token (403), or receipts closed (409); `AB8248` malformed receipt — query string, non-object body, unknown key, out-of-range enum, or unbounded field (400, the message names the field); `AB8249` receipt over the 16 KiB limit (413). |
| `AB8239` | Workbench route invocation service (`/api/routes/invocations`): the published manifest digest or source revision moved while the request waited for a concurrency slot (409). Retry against the current revision so the recorded `manifestDigest`/`sourceRevision` cannot describe a different build than the one that ran. |
| `AB8250`–`AB8255` | Workbench production route execution: `AB8250` no published compiler artifact is available, `AB8251` the selected route has no executable in the published artifact, `AB8252` compiled CLI projection or event preflight preparation failed, `AB8253` a selected CLI command does not project onto the canonical operation id, `AB8254` a projected `cli:<command>` id was used instead of its canonical `tool:<server>/<tool>` id plus CLI surface, and `AB8255` an event route with compiled preflight was submitted without a concrete host surface. Rebuild the project for `AB8250`/`AB8251`; fix the reported projection or preflight failure for `AB8252`; use the command or canonical operation named by `AB8253`/`AB8254`; select a generated host wrapper for `AB8255`. |
| `AB8256` | Workbench route invocation cancellation (`POST /api/routes/invocations/<id>/cancel`): the invocation is already final (409). Reload the final invocation instead of cancelling it. |
| `AB8110`–`AB8113` | Workbench standalone MCP Inspector routes (`/api/inspector/status`, `/api/inspector/launch`): `AB8110` invalid path, `AB8111` invalid request shape or query, `AB8112` the Inspector could not be launched (spawn failure, exit before publishing a URL, or the 30 s startup budget elapsed; 502), `AB8113` routes not available (404 when the launcher is not composed, 503 after shutdown). |
| `AB8120`–`AB8123` | Workbench route manifest (`/api/routes/manifest`): `AB8120` invalid path, `AB8121` not available (404/409/503), `AB8122` query string on the request, `AB8123` the browser client could not decode the response (see below). |
| `AB8024`–`AB8025` | Live host MCP proxy: epoch drift behind a host connection and dev-server unavailability (see below). |
Expand Down
120 changes: 81 additions & 39 deletions packages/agent-bundle/src/adapters/hook-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,8 +667,14 @@ const eventRouteHookWrapperSource = (
// then) retires the durable lineage journal itself, so roots never outlive
// their session; only projects whose state is workspace-durable have one.
const retiresLineage = standalone && durableLineage && route.event === 'session/end';
// The deferred executor is spawned by a preflight wrapper that already
// holds this execution's tracer and receipt; it traces into a disabled
// tracer so one host invocation yields one receipt (#600).
const projectBindings = [
'createCanonicalEventProps',
'createEventTracer',
'eventTraceExecution',
...(deferredExecution ? [] : ['openEventTraceReceipt']),
...(standalone ? ['projectEventDocument'] : []),
'validateNativeEventEnvelope',
];
Expand Down Expand Up @@ -790,25 +796,34 @@ const eventRouteHookWrapperSource = (
'};',
]
: []),
'const runStandalone = async (native, signal, observation, preflight) => {',
'const runStandalone = async (native, signal, observation, preflight, trace, receipt) => {',
' const resolved = createCanonicalEventProps(canonicalEvent, native, target, nativeEvent, capabilityRevision, signal, observation);',
...(retiresLineage ? [' await retireLineage(native, resolved.canonical.idempotencyKey, resolved.canonical.observedAt);'] : []),
' const sessionId = typeof native.session_id === "string" ? native.session_id : typeof native.conversation_id === "string" ? native.conversation_id : undefined;',
' const workspaceRoot = typeof native.cwd === "string" ? native.cwd : Array.isArray(native.workspace_roots) && typeof native.workspace_roots[0] === "string" ? native.workspace_roots[0] : undefined;',
// Standalone hooks hold no registry, so lineage is what the payload proves — plus, on Codex, what the
// thread's own rollout named in the payload records (docs/audits/2026-09-03-host-lineage-matrix.md, #423).
' const lineage = target === "claude" || target === "codex" || target === "cursor" ? await resolveStandaloneLineage(target, native) : unavailable("no-subagent-events");',
' const document = await runAgentRequest({',
' host: available({ name: target }, "native"),',
' invocation: { artifactEpoch, hostContractRevision: capabilityRevision, kind: "event", operationId: `event:${canonicalEvent}`, surface: canonicalEvent },',
' lineage,',
' plugin: pluginRoot.identity,',
' ...(sessionId === undefined ? {} : { session: available({ sessionId }, "native") }),',
' signal,',
' receipt?.lineage(lineage);',
' trace.renderStart();',
' let document;',
' try {',
' document = await runAgentRequest({',
' host: available({ name: target }, "native"),',
' invocation: { artifactEpoch, hostContractRevision: capabilityRevision, kind: "event", operationId: `event:${canonicalEvent}`, surface: canonicalEvent },',
' lineage,',
' plugin: pluginRoot.identity,',
' ...(sessionId === undefined ? {} : { session: available({ sessionId }, "native") }),',
' signal,',
// A hook's stdout is its host envelope: no terminal, never probed (#511).
' terminal: available({ hostSurface: "hook", sharesTarget: false, stderr: { color: "none", kind: "none" }, stdout: { color: "none", kind: "none" } }, "derived"),',
' ...(workspaceRoot === undefined ? {} : { workspace: available({ root: workspaceRoot }, "native") }),',
' }, async () => renderStandalone({ kind: "event", props: { event: canonicalEvent, payload: { canonical: resolved.canonical, native: resolved.native, ...(preflight === undefined ? {} : { preflight }) } } }, signal));',
' terminal: available({ hostSurface: "hook", sharesTarget: false, stderr: { color: "none", kind: "none" }, stdout: { color: "none", kind: "none" } }, "derived"),',
' ...(workspaceRoot === undefined ? {} : { workspace: available({ root: workspaceRoot }, "native") }),',
' }, async () => renderStandalone({ kind: "event", props: { event: canonicalEvent, payload: { canonical: resolved.canonical, native: resolved.native, ...(preflight === undefined ? {} : { preflight }) } } }, signal));',
' } catch (error) {',
' trace.failure("render", error);',
' throw error;',
' }',
' trace.renderFinish();',
' return projectEventDocument(document, canonicalEvent, target, nativeEvent, native);',
'};',
]
Expand Down Expand Up @@ -836,25 +851,44 @@ const eventRouteHookWrapperSource = (
' const preflight = undefined;',
]),
' const native = validateNativeEventEnvelope(nativeInput, { canonicalEvent, nativeEvent, target });',
' const execution = eventTraceExecution({ event: canonicalEvent, host: target, nativeEvent });',
deferredExecution
? ' const receipt = undefined;'
: ' const receipt = await openEventTraceReceipt({ anchor: import.meta.url, env: process.env, execution });',
' const trace = createEventTracer({ execution, ...(receipt === undefined ? {} : { observer: receipt.observer }) });',
' receipt?.identity(native);',
' const controller = new AbortController();',
' let output;',
' if (runtimeMode === "standalone") {',
' try {',
' if (runtimeMode === "standalone") {',
...(standalone
? [' output = await runStandalone(native, controller.signal, observation, preflight);']
: [' fail("standalone runtime was not compiled");']),
' } else {',
' try {',
' output = await requestEventRuntime({ artifactEpoch, endpointId, event: canonicalEvent, hostContractRevision: capabilityRevision, native, observedAt: observation?.observedAt, preflight, sequence: observation?.sequence, signal: controller.signal, target, timeoutMs });',
' } catch (error) {',
? [
' trace.executeStart("standalone");',
' output = await runStandalone(native, controller.signal, observation, preflight, trace, receipt);',
]
: [' fail("standalone runtime was not compiled");']),
' } else {',
' trace.executeStart("shared");',
' try {',
' output = await requestEventRuntime({ artifactEpoch, endpointId, event: canonicalEvent, hostContractRevision: capabilityRevision, native, observedAt: observation?.observedAt, preflight, sequence: observation?.sequence, signal: controller.signal, target, timeoutMs });',
' } catch (error) {',
...(standalone
? [
' if (!(fallbackMode === "standalone" && error instanceof EventRuntimeTransportError && error.code === "runtime-unavailable")) throw error;',
' output = await runStandalone(native, controller.signal, observation, preflight);',
' if (!(fallbackMode === "standalone" && error instanceof EventRuntimeTransportError && error.code === "runtime-unavailable")) throw error;',
' trace.executeStart("standalone");',
' output = await runStandalone(native, controller.signal, observation, preflight, trace, receipt);',
]
: [' throw error;']),
: [' throw error;']),
' }',
' }',
' if (output !== undefined) process.stdout.write(JSON.stringify(output));',
' } catch (error) {',
// A no-op once a phase already attributed the failure: the tracer is terminal after `failure`.
' trace.failure("execute", error);',
' throw error;',
' } finally {',
' await receipt?.send();',
' }',
' if (output !== undefined) process.stdout.write(JSON.stringify(output));',
'};',
'if (import.meta.main) {',
' await run().catch((error) => {',
Expand Down Expand Up @@ -883,6 +917,7 @@ const eventRoutePreflightWrapperSource = (
'createEventTracer',
'eventTraceExecution',
'executeEventPreflight',
'openEventTraceReceipt',
'projectEventPreflightResult',
'validateNativeEventEnvelope',
];
Expand Down Expand Up @@ -943,26 +978,33 @@ const eventRoutePreflightWrapperSource = (
' try { parsed = JSON.parse(input.toString("utf8")); } catch { fail("stdin must contain exactly one JSON value"); }',
' const controller = new AbortController();',
' const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(timeoutMs)]);',
' const { gate, native, projected, props, trace } = await prepareRouteInvocation(parsed, signal);',
' if (gate !== "execute" && gate.outcome !== "execute") {',
' if (projected !== undefined) process.stdout.write(JSON.stringify(projected));',
' return;',
' }',
' trace.executeStart(runtimeMode);',
' const executionInput = Buffer.from(JSON.stringify({ native, observedAt: props.canonical.observedAt, ...(gate === "execute" ? {} : { preflight: gate.data }), sequence: props.canonical.sequence }));',
' const terminationSignals = ["SIGHUP", "SIGINT", "SIGTERM"];',
' const terminate = () => controller.abort();',
' for (const terminationSignal of terminationSignals) process.once(terminationSignal, terminate);',
' let output;',
' const execution = eventTraceExecution({ event: canonicalEvent, host: target, nativeEvent });',
' const receipt = await openEventTraceReceipt({ anchor: import.meta.url, env: process.env, execution });',
' try {',
' output = await runExecutor(executionInput, controller.signal);',
' } catch (error) {',
' trace.failure("execute", error);',
' throw error;',
' const { gate, native, projected, props, trace } = await prepareRouteInvocation(parsed, signal, receipt?.observer);',
' receipt?.identity(native);',
' if (gate !== "execute" && gate.outcome !== "execute") {',
' if (projected !== undefined) process.stdout.write(JSON.stringify(projected));',
' return;',
' }',
' trace.executeStart(runtimeMode);',
' const executionInput = Buffer.from(JSON.stringify({ native, observedAt: props.canonical.observedAt, ...(gate === "execute" ? {} : { preflight: gate.data }), sequence: props.canonical.sequence }));',
' const terminationSignals = ["SIGHUP", "SIGINT", "SIGTERM"];',
' const terminate = () => controller.abort();',
' for (const terminationSignal of terminationSignals) process.once(terminationSignal, terminate);',
' let output;',
' try {',
' output = await runExecutor(executionInput, controller.signal);',
' } catch (error) {',
' trace.failure("execute", error);',
' throw error;',
' } finally {',
' for (const terminationSignal of terminationSignals) process.off(terminationSignal, terminate);',
' }',
' if (output.length > 0) process.stdout.write(output);',
' } finally {',
' for (const terminationSignal of terminationSignals) process.off(terminationSignal, terminate);',
' await receipt?.send();',
' }',
' if (output.length > 0) process.stdout.write(output);',
'};',
'if (import.meta.main) {',
' await run().catch((error) => {',
Expand Down
4 changes: 4 additions & 0 deletions packages/agent-bundle/src/contracts/invocations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ export type {
RouteInvocationProviderStatus,
RouteInvocationRequest,
RouteInvocationStatus,
RunningRouteInvocation,
RouteInvocationSummary,
RouteInvocationSurface,
RouteInvocationTiming,
} from '../dev/routes/route-invocation.ts';
export type {
RouteInvocation,
RouteInvocationResponse,
RouteInvocationStart,
RouteInvocationStreamMessage,
RunningRouteInvocationResponse,
} from '../dev/routes/route-invocation-result.ts';
export type { EventTraceEvent } from '../events/trace.ts';
4 changes: 4 additions & 0 deletions packages/agent-bundle/src/contracts/mcp-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ export type {
McpSessionInspectorConfig,
McpSessionOperation,
McpSessionTraceEntry,
McpSessionTraceMeta,
McpSessionTraceReplayGap,
} from '../dev/mcp-session/mcp-session-protocol.ts';

/** MCP `params._meta` key used to correlate a tool call with its Workbench invocation. */
export const mcpCorrelationMetaKey = 'agent-bundle/correlationId';

/** The host targets a Workbench MCP session may bind. */
export const MCP_SESSION_TARGETS = Object.freeze(['claude', 'codex', 'cursor', 'portable'] as const);

Expand Down
16 changes: 16 additions & 0 deletions packages/agent-bundle/src/contracts/trace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Browser-consumable contract surface for the Workbench unified trace
* (`GET /api/trace`, `GET /api/trace/stream`). The source vocabulary is
* dependency-free runtime code; the entry shapes are type-only.
*/
export { isTraceReplayGap, isTraceSource, traceSources } from '../dev/trace/trace-entry.ts';
export type {
TraceCorrelation,
TraceEntry,
TraceEntryInput,
TraceMessage,
TraceReplay,
TraceReplayGap,
TraceSource,
TraceStatus,
} from '../dev/trace/trace-entry.ts';
17 changes: 17 additions & 0 deletions packages/agent-bundle/src/core/loopback-origin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* A serialized loopback HTTP origin — `http://127.0.0.1:<port>` or
* `http://[::1]:<port>` with nothing after the authority. The one shape the
* dev lock publishes, the host MCP proxy dials, and a generated hook wrapper
* may post a trace receipt to.
*/
export const isLoopbackHttpOrigin = (value: unknown): value is string => {
if (typeof value !== 'string') return false;
try {
const parsed = new URL(value);
return parsed.protocol === 'http:'
&& (parsed.hostname === '127.0.0.1' || parsed.hostname === '[::1]')
&& parsed.origin === value;
} catch {
return false;
}
};
Loading
Loading