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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## 0.135.0

### workerFromBackend honors `continuity: 'resume'` on the bridge backend

The backend-derived worker seam re-attaches cli-bridge sessions (#694, the chat-transport resume executor's bridge arm).
A bridge session id IS the harness conversation key — cli-bridge maps it to the CLI's own resume (opencode `-s <id>`, claude `--resume`) — so a resume spawn is real session re-attachment, never a fresh session wearing a `resume` stamp.

- `workerFromBackend` records the session id each supervised bridge spawn was bound to, keyed by the worker id the Scope assigned. A `continuity: 'resume'` spawn binds the prior worker's recorded session id (`spawnContext.resume.ofWorker`) instead of deriving a fresh one, so the new worker continues the exact harness conversation. A fresh → resume → resume chain stays on ONE session.
- The record is process-local by construction, which matches the kernel's documented resume boundary: a prior process's workers are not resume targets.
- Fail-loud contract unchanged everywhere else: a non-bridge backend still refuses a resume spawn (its executors have no re-attachable session), and a resume of a worker this seam never bound a session for refuses by name. Every refusal throws BEFORE a worker exists, so the kernel never ledgers `continuity: 'resume'` over a session that was not re-attached.
- `runGraph` handed a `backend: 'bridge'` config now honors a delegates edge's `continuity: 'resume'` natively — the revise-edge pattern (write fresh, revise by resuming the writer's session) runs without a custom `makeWorkerAgent`.
- FIX, found by the live proof: the derived external session id now scopes by the spawning manager node. Assignment ordinals restart at `ordinal:0` under every manager, so the unscoped digest mapped worker 1 of EVERY run — and of every sibling manager — to ONE bridge session id; on a bridge with a persistent session store, a `'fresh'` spawn then silently continued a foreign run's harness conversation (measured live: three separate runs shared one claude conversation, `turns: 6`). Durable recovery within a run is unchanged (a replay re-issues the same manager node id and assignments). Migration: a journaled run from an older version replayed under this version derives DIFFERENT session ids and will not re-attach its old harness sessions.

## 0.134.4

- Consume Core 0.8.0, Eval 0.145.11, Interface 0.52.0, Knowledge 7.2.6, Profile Materialize 0.14.2, and Sandbox 0.26.1 as one compatible dependency set.
Expand Down
2 changes: 1 addition & 1 deletion docs/api/primitive-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

# Primitive catalog — the never-stale anti-reinvention inventory

> **GENERATED** from `@tangle-network/agent-runtime@0.134.9` and `@tangle-network/agent-eval@0.145.11` by `scripts/gen-primitive-catalog.mjs`. Do NOT hand-edit — run `pnpm run docs:api`. This is the mechanical companion to the JUDGMENT in `canonical-api.md` (§2 decision table + §1.5 AgentProfile law): that doc says WHICH primitive to reach for and what NOT to build; this catalog proves WHAT exists. Per-symbol signatures + `file:line` live in the per-module pages under `docs/api/`.
> **GENERATED** from `@tangle-network/agent-runtime@0.135.0` and `@tangle-network/agent-eval@0.145.11` by `scripts/gen-primitive-catalog.mjs`. Do NOT hand-edit — run `pnpm run docs:api`. This is the mechanical companion to the JUDGMENT in `canonical-api.md` (§2 decision table + §1.5 AgentProfile law): that doc says WHICH primitive to reach for and what NOT to build; this catalog proves WHAT exists. Per-symbol signatures + `file:line` live in the per-module pages under `docs/api/`.

## 1. agent-runtime — own public surface

Expand Down
10 changes: 10 additions & 0 deletions docs/api/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -26035,6 +26035,16 @@ construction, so a caller may hand back something the run only learns later —
`supervise()` gives a traced run's workers their trace context without ordering the span recorder
ahead of the worker seam.

Continuity: the `bridge` backend honors `continuity: 'resume'` by session re-attachment. A
bridge session id IS the harness conversation key (cli-bridge maps it to the CLI's own resume —
opencode `-s <id>`, claude `--resume`), so this seam records the session id each supervised
spawn was bound to, keyed by the worker id the Scope assigned, and a resume spawn binds the
prior worker's recorded session id instead of deriving a fresh one. The record is process-local
by construction, which matches the kernel's resume boundary (a prior process's workers are not
resume targets). Every other backend keeps failing loud: their executors have no re-attachable
session, and accepting the spawn would ledger `continuity: 'resume'` over a brand-new session —
a stamp asserting something that never happened.

#### Parameters

##### backend
Expand Down
2 changes: 1 addition & 1 deletion docs/canonical-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Generated signatures and the complete export list live in docs/api/.
Run pnpm docs:freshness after editing this file. -->

> **Version 0.134.9.**
> **Version 0.135.0.**
> [`docs/api/primitive-catalog.md`](./api/primitive-catalog.md) lists every export and import path.
> `agent-eval` must satisfy `>=0.145.11 <0.146.0`.
> `sandbox` must satisfy `>=0.26.1 <0.27.0`.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tangle-network/agent-runtime",
"version": "0.134.9",
"version": "0.135.0",
"description": "Shared task-lifecycle skeleton for agents: a recursive loop kernel for chat turns, one-shot tasks, and multi-attempt loops, with trace capture and eval-gated self-improvement. Domain behavior lives in adapters; scoring and ship-gates in @tangle-network/agent-eval.",
"homepage": "https://github.com/tangle-network/agent-runtime#readme",
"repository": {
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,8 +548,9 @@ export {
} from './supervise/budget'
// The chat-transport leaf (#721): a worker that IS a model conversation on a bare
// OpenAI-compatible /v1/chat/completions endpoint — no sandbox. Ships with its session store and
// the continuity-honoring `makeWorkerAgent` seam (the resume consumer `workerFromBackend`
// refuses to be), so conversation graphs and chat-shot loops compose from data.
// the continuity-honoring `makeWorkerAgent` seam. `workerFromBackend` resumes only bridge-backed
// workers; this leaf resumes its own sessions, so conversation graphs and chat-shot loops
// compose from data on any transport.
export {
type ChatCompletionsTransport,
type ChatSessionStore,
Expand Down
87 changes: 75 additions & 12 deletions src/runtime/supervise/supervise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,16 @@ import { WORKER_TRACE_PROPAGATION } from './worker-trace'
* construction, so a caller may hand back something the run only learns later — which is exactly how
* `supervise()` gives a traced run's workers their trace context without ordering the span recorder
* ahead of the worker seam.
*
* Continuity: the `bridge` backend honors `continuity: 'resume'` by session re-attachment. A
* bridge session id IS the harness conversation key (cli-bridge maps it to the CLI's own resume —
* opencode `-s <id>`, claude `--resume`), so this seam records the session id each supervised
* spawn was bound to, keyed by the worker id the Scope assigned, and a resume spawn binds the
* prior worker's recorded session id instead of deriving a fresh one. The record is process-local
* by construction, which matches the kernel's resume boundary (a prior process's workers are not
* resume targets). Every other backend keeps failing loud: their executors have no re-attachable
* session, and accepting the spawn would ledger `continuity: 'resume'` over a brand-new session —
* a stamp asserting something that never happened.
*/
export function workerFromBackend(
backend: ExecutorConfig,
Expand All @@ -150,37 +160,53 @@ export function workerFromBackend(
const capturedBackend = captureReusableExecutorConfig(backend, 'workerFromBackend')
const unscopedNamespace = randomUUID()
let unscopedOrdinal = 0
const bridgeSessionByWorker = new Map<string, string>()
return (rawProfile, spawnContext) => {
const parsed = agentProfileSchema.safeParse(rawProfile)
if (!parsed.success) {
throw new ValidationError(`workerFromBackend: invalid AgentProfile: ${parsed.error.message}`)
}
const profile = parsed.data
assertBackendProfileMaterialization(profile, capturedBackend, 'workerFromBackend')
// Fail closed on a resume this seam cannot honor: workerFromBackend creates a NEW executor
// per spawn and has no session re-attachment, so accepting a 'resume' spawn would ledger
// `continuity: 'resume'` over a brand-new session — a stamp asserting something that never
// happened. Custom makeWorkerAgent seams that re-attach sessions are the resume consumers.
if (spawnContext?.continuity === 'resume') {
throw new ValidationError(
'workerFromBackend: this backend seam does not re-attach sessions and cannot honor ' +
"continuity: 'resume' — provide a makeWorkerAgent that resumes (it receives " +
"spawnContext.resume.ofWorker), or use continuity: 'fresh'",
)
}
// Resolved BEFORE any worker exists, so a resume this seam cannot honor fails the spawn
// itself and the kernel never ledgers a `continuity: 'resume'` stamp for it.
const resumeSessionId = bridgeResumeSessionId(
capturedBackend,
spawnContext,
bridgeSessionByWorker,
)
const name = profile.name ?? 'worker'
// A Scope assignment is stable across reconstruction. Direct callers that omit that context
// still get isolation, but only Scope-backed calls claim durable external-session recovery.
const assignmentId =
spawnContext?.assignmentId ?? `unscoped:${unscopedNamespace}:${unscopedOrdinal++}`
// The derived execution id scopes by the spawning manager: assignment ordinals restart at
// `ordinal:0` under every manager, so an unscoped digest maps worker 1 of EVERY run (and of
// every sibling manager) to one external session — on a bridge with a persistent session
// store, a 'fresh' spawn then silently continues a foreign run's harness conversation.
// Durable recovery is preserved: a replay of the same run re-issues the same manager node id
// and the same assignments, so it derives the same session ids.
const executionScope = spawnContext?.parentNodeId
const boundBackend = bindReusableExecutorExecutionId(
capturedBackend,
externalExecutionId('supervised-worker', { assignmentId }),
resumeSessionId ??
externalExecutionId('supervised-worker', {
assignmentId,
...(executionScope === undefined ? {} : { scope: executionScope }),
}),
)
const boundSessionId = boundBackend.backend === 'bridge' ? boundBackend.sessionId : undefined
const baseFactory = createExecutor(boundBackend)
// Carry the configured factory into Scope. It is built only AFTER reservation with the real
// child signal/context, so a rejected or already-completed keyed spawn creates no executor.
const executorFactory = (spec: AgentSpec, ctx: ExecutorContext) => {
// Record the bridge session this worker is bound to under its Scope-assigned worker id,
// so a later resume of the node can re-attach it. A resumed worker records the SAME
// session under its own id, which is what keeps a fresh → resume → resume chain on one
// harness conversation.
if (boundSessionId !== undefined && ctx.node?.nodeId !== undefined) {
bridgeSessionByWorker.set(ctx.node.nodeId, boundSessionId)
}
// Caller-supplied seams sit UNDER the per-child seams the Scope seeds, so the scope's
// recursion and trace context always win on a key collision.
const extraSeams = seams?.()
Expand All @@ -207,6 +233,43 @@ function externalExecutionId(kind: string, identity: unknown): string {
return `${kind}-${digest.slice('sha256:'.length)}`
}

/**
* The bridge session id a `'resume'` spawn re-attaches, or `undefined` for a fresh spawn.
* Every refusal throws BEFORE a worker exists, which is what keeps the kernel's continuity
* ledger true: a `'resume'` stamp can only appear over a session that was actually re-attached.
*/
function bridgeResumeSessionId(
backend: ExecutorConfig,
spawnContext: WorkerSpawnContext | undefined,
sessions: ReadonlyMap<string, string>,
): string | undefined {
if (spawnContext?.continuity !== 'resume') return undefined
if (backend.backend !== 'bridge') {
throw new ValidationError(
`workerFromBackend: the '${backend.backend}' backend seam does not re-attach sessions and ` +
"cannot honor continuity: 'resume' — only the 'bridge' backend resumes here (cli-bridge " +
'keys the harness conversation by session id). Provide a makeWorkerAgent that resumes ' +
"(it receives spawnContext.resume.ofWorker), or use continuity: 'fresh'",
)
}
const ofWorker = spawnContext.resume?.ofWorker
if (ofWorker === undefined) {
throw new ValidationError(
"workerFromBackend: a 'resume' spawn carries no resume lineage — the kernel stamps " +
'spawnContext.resume for ledgered spawns; a direct caller must pass { ofWorker, sequence }',
)
}
const prior = sessions.get(ofWorker)
if (prior === undefined) {
throw new ValidationError(
`workerFromBackend: no recorded bridge session for worker '${ofWorker}' — this seam ` +
're-attaches only sessions it bound in this process (the kernel resume boundary). ' +
"Spawn the node fresh first, or use continuity: 'fresh'",
)
}
return prior
}

/**
* The `trace-unpropagated` declaration for a worker backend, or `undefined` when the backend HAS a
* propagation channel. The census (`WORKER_TRACE_PROPAGATION`) says WHETHER a backend propagates;
Expand Down
10 changes: 5 additions & 5 deletions src/testing/fixtures/agent-improvement-proposal.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"changedSurfaces": ["prompt"],
"digest": "sha256:bdcb2a49dec1a39ec71d01f845da6a2c6e55282805e271eff52a1b176a938601",
"digest": "sha256:2c580eafb8ac241bcb7a5d90ed4d400dc469e70776be38e92d0ca3cca8b1b3ba",
"evaluation": {
"decision": {
"contributingChecks": [
Expand Down Expand Up @@ -4882,7 +4882,7 @@
],
"metadata": {
"fixture": "agent-improvement-proposal",
"runtimeVersion": "0.134.9"
"runtimeVersion": "0.135.0"
},
"objectives": [
{
Expand Down Expand Up @@ -4993,8 +4993,8 @@
"baselineContentHash": "sha256:5c21ee53e513fc604cb09754e21c392b24a424da0ef37dbf8f1ee4a8a0b08f09",
"candidateContentHash": "sha256:60fcbb1c728194bd51d7d19cb732d1c3f1881dce7e0a6266b41c8b98cfd65693",
"kind": "agent-eval-loop",
"recordDigest": "sha256:b6bb5f018d1f2d5210293918085073c1fa2f78fc21faf01364dddf8560af69ac",
"runId": "agent-runtime-0.134.9-proposal-fixture",
"recordDigest": "sha256:cb02f870cf34f127846fa273bcd957b888dcbe8b6021f049272d2bafe021a8f5",
"runId": "agent-runtime-0.135.0-proposal-fixture",
"schema": "agent-candidate-experiment"
}
},
Expand All @@ -5021,5 +5021,5 @@
],
"kind": "agent-improvement-proposal",
"proposedAt": "2026-07-10T01:00:00.000Z",
"runId": "agent-runtime-0.134.9-proposal-fixture"
"runId": "agent-runtime-0.135.0-proposal-fixture"
}
6 changes: 3 additions & 3 deletions src/testing/fixtures/agent-profile-improvement-proposal.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"changedSurfaces": ["prompt", "skills"],
"digest": "sha256:5bf45cc86b707dbae89345609e3ee8fe0c4e7444f15bf4fbcc9d3369f5ae9274",
"digest": "sha256:fcf087fac77b1e1cc64c7735aa120df7cd66366d595ea903c156d04449eb9412",
"evaluation": {
"decision": {
"contributingChecks": [
Expand Down Expand Up @@ -1715,7 +1715,7 @@
],
"metadata": {
"fixture": "agent-profile-improvement-proposal",
"runtimeVersion": "0.134.9"
"runtimeVersion": "0.135.0"
},
"objectives": [
{
Expand Down Expand Up @@ -1826,7 +1826,7 @@
"baselineContentHash": "sha256:21c495a37c418c10bde64fbaa188beddeed31f1f051ea60a6a6582a9ee0db704",
"candidateContentHash": "sha256:103f77bc8481601eef1ad5fe6ba84a40dffabc3a44f421f8c8559121edab84e9",
"kind": "agent-eval-loop",
"recordDigest": "sha256:15c4af0c9eca66190ac1690ecf4fcc5ec400919e947f310354d230d3a90d3406",
"recordDigest": "sha256:1615f2b547311bbba123dea28a11004d09b60191bcbab4637f0364fb39975868",
"runId": "profile-improvement-1",
"schema": "agent-profile-improvement-experiment"
}
Expand Down
Loading