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

## 0.193.0

### Omitted code-mode deadlines no longer stop work

`CodeModeOptions.timeoutMs` now accepts `null`, and an omitted value no longer creates a
60-second deadline.
Code-mode programs continue until the manager cancels them or a caller supplies a positive
`timeoutMs`.
Consumers that require a wall-clock limit must now declare it explicitly.

### Live children can settle without a clock

`SuperviseOptions.childSettleGraceMs` and `SupervisorOpts.childSettleGraceMs` now accept `null`.
That value keeps the join open until every child settles or the caller cancels the run.
An explicit run deadline still bounds the join.
Omitted and zero values retain immediate teardown.

## 0.192.0

### Provider date snapshots retain model identity
Expand Down
6 changes: 3 additions & 3 deletions api-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,7 @@
"CliSeam": "type 69bfe2571bab",
"CliWorktreeBridgeSeam": "type 81fa5d344141",
"CliWorktreeSeam": "type d5be6307c356",
"CodeModeOptions": "type 7968eb5eb81b",
"CodeModeOptions": "type 846ed388a825",
"CodeModeRunner": "type 644646a508d0",
"CodexForkBoundary": "type 5335e473148a",
"CodexRolloutIdentity": "type 13675db20eb0",
Expand Down Expand Up @@ -1573,7 +1573,7 @@
"StructuralRolloutPolicy": "type d372912ef050",
"StructuralRolloutResult": "type 9248b72cae04",
"SuperviseDispatchOptions": "type 55e71c9c1e6b",
"SuperviseOptions": "type 0792601cf1d0",
"SuperviseOptions": "type 51029d9e2b96",
"SuperviseOptionsForDispatch": "type 7d89526c1040",
"SuperviseRegistry": "type 4fd60c297f74",
"SuperviseRegistryTable": "type cc1468cd50c1",
Expand All @@ -1586,7 +1586,7 @@
"SupervisorFinalizer": "type f8628e65536f",
"SupervisorNodeContext": "type b8e545bbb355",
"SupervisorNodeContextSeed": "type da1e69b5be66",
"SupervisorOpts": "type 97f0a700e6a3",
"SupervisorOpts": "type 11a618807bda",
"SupervisorProfile": "type 217ef5d5fe3f",
"SupervisorSpanAttributes": "type 1a1e9cd7e217",
"SupervisorSpanOptions": "type 1f63a31dfa75",
Expand Down
9 changes: 4 additions & 5 deletions docs/api/durable.md
Original file line number Diff line number Diff line change
Expand Up @@ -1507,12 +1507,11 @@ Compose the re-entry instruction for an unmet contract, or return `'stop'` to en

##### childSettleGraceMs?

> `readonly` `optional` **childSettleGraceMs?**: `number`
> `readonly` `optional` **childSettleGraceMs?**: `number` \| `null`

How long live children may keep running after the ROOT DRIVER FAILED, before the join barrier
cascades the abort into them. A root that died did not make its children unhealthy: a child
mid-unit holds work already paid for, and an immediate cascade discards everything it has not
yet written. Bounded by the run's own deadline. Omit/`0` = immediate teardown.
How long live children may keep running after the root driver returns or fails, before the join
barrier cascades the abort into them. `null` waits until children settle or the caller cancels.
An explicit run deadline always wins. Omit/`0` = immediate teardown.

###### Inherited from

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.192.5` and `@tangle-network/agent-eval@0.173.0` 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.193.0` and `@tangle-network/agent-eval@0.173.0` 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
41 changes: 19 additions & 22 deletions docs/api/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -11784,14 +11784,14 @@ Where model-written code runs. THE isolation boundary — see the module doc: th

`Readonly`\<`Record`\<`string`, (`args`) => `Promise`\<`unknown`\>\>\>

The granted operations, already deadline-gated and result-detached by the caller. The
The granted operations, already cancellation-gated and result-detached by the caller. The
runner exposes these to the program as `api.<name>` and adds nothing else reachable.

###### signal

`AbortSignal`

Aborts when the whole-program deadline passes or the manager scope cancels.
Aborts when the manager cancels or a caller-authored deadline passes.

###### Returns

Expand All @@ -11805,11 +11805,10 @@ Aborts when the whole-program deadline passes or the manager scope cancels.

##### timeoutMs?

> `readonly` `optional` **timeoutMs?**: `number`
> `readonly` `optional` **timeoutMs?**: `number` \| `null`

Whole-program deadline per `execute` call. Default 60_000. After it passes, the running
program's next `api` call fails closed, so a runaway loop cannot keep spawning workers the
model can no longer see.
Optional caller-authored deadline for one `execute` call. Omit it to run until the manager
cancels. A declared deadline aborts the runner and refuses later `api` calls.

***

Expand Down Expand Up @@ -13491,12 +13490,11 @@ Compose the re-entry instruction for an unmet contract, or return `'stop'` to en

##### childSettleGraceMs?

> `readonly` `optional` **childSettleGraceMs?**: `number`
> `readonly` `optional` **childSettleGraceMs?**: `number` \| `null`

How long live children may keep running after the ROOT DRIVER FAILED, before the join barrier
cascades the abort into them. A root that died did not make its children unhealthy: a child
mid-unit holds work already paid for, and an immediate cascade discards everything it has not
yet written. Bounded by the run's own deadline. Omit/`0` = immediate teardown.
How long live children may keep running after the root driver returns or fails, before the join
barrier cascades the abort into them. `null` waits until children settle or the caller cancels.
An explicit run deadline always wins. Omit/`0` = immediate teardown.

###### Inherited from

Expand Down Expand Up @@ -17788,12 +17786,11 @@ Compose the re-entry instruction for an unmet contract, or return `'stop'` to en

##### childSettleGraceMs?

> `readonly` `optional` **childSettleGraceMs?**: `number`
> `readonly` `optional` **childSettleGraceMs?**: `number` \| `null`

How long live children may keep running after the ROOT DRIVER FAILED, before the join barrier
cascades the abort into them. A root that died did not make its children unhealthy: a child
mid-unit holds work already paid for, and an immediate cascade discards everything it has not
yet written. Bounded by the run's own deadline. Omit/`0` = immediate teardown.
How long live children may keep running after the root driver returns or fails, before the join
barrier cascades the abort into them. `null` waits until children settle or the caller cancels.
An explicit run deadline always wins. Omit/`0` = immediate teardown.

##### resolveDriveHarness?

Expand Down Expand Up @@ -20286,13 +20283,13 @@ trips the supervisor to `no-winner` rather than restarting forever.

##### childSettleGraceMs?

> `readonly` `optional` **childSettleGraceMs?**: `number`
> `readonly` `optional` **childSettleGraceMs?**: `number` \| `null`

How long live children may keep running after the ROOT DRIVER FAILED, before the join barrier
cascades the abort into them (#741). A root that dies did not make its children unhealthy: a
child mid-unit holds work already paid for, and killing it instantly discards everything it has
not yet written. The window applies ONLY to a driver failure on an un-cancelled run, and never
extends past the run's own deadline. Omit/`0` = the historical immediate teardown.
How long live children may keep running after the root driver returns or fails, before the join
barrier cascades the abort into them (#741). A child mid-unit holds work already paid for, and
killing it instantly discards everything it has not yet written. `null` waits until children
settle or the caller cancels. An explicit run deadline always wins. Omit/`0` = immediate
teardown.

##### resume?

Expand Down
18 changes: 8 additions & 10 deletions docs/api/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -886,12 +886,11 @@ Compose the re-entry instruction for an unmet contract, or return `'stop'` to en

##### childSettleGraceMs?

> `readonly` `optional` **childSettleGraceMs?**: `number`
> `readonly` `optional` **childSettleGraceMs?**: `number` \| `null`

How long live children may keep running after the ROOT DRIVER FAILED, before the join barrier
cascades the abort into them. A root that died did not make its children unhealthy: a child
mid-unit holds work already paid for, and an immediate cascade discards everything it has not
yet written. Bounded by the run's own deadline. Omit/`0` = immediate teardown.
How long live children may keep running after the root driver returns or fails, before the join
barrier cascades the abort into them. `null` waits until children settle or the caller cancels.
An explicit run deadline always wins. Omit/`0` = immediate teardown.

###### Inherited from

Expand Down Expand Up @@ -1738,12 +1737,11 @@ Compose the re-entry instruction for an unmet contract, or return `'stop'` to en

##### childSettleGraceMs?

> `readonly` `optional` **childSettleGraceMs?**: `number`
> `readonly` `optional` **childSettleGraceMs?**: `number` \| `null`

How long live children may keep running after the ROOT DRIVER FAILED, before the join barrier
cascades the abort into them. A root that died did not make its children unhealthy: a child
mid-unit holds work already paid for, and an immediate cascade discards everything it has not
yet written. Bounded by the run's own deadline. Omit/`0` = immediate teardown.
How long live children may keep running after the root driver returns or fails, before the join
barrier cascades the abort into them. `null` waits until children settle or the caller cancels.
An explicit run deadline always wins. Omit/`0` = immediate teardown.

###### Inherited from

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.192.5.**
> **Version 0.193.0.**
> [`docs/api/primitive-catalog.md`](./api/primitive-catalog.md) lists every export and import path.
> `agent-eval` must satisfy `>=0.173.0 <0.174.0`.
> `sandbox` must satisfy `>=0.36.4 <0.38.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.192.5",
"version": "0.193.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
55 changes: 32 additions & 23 deletions src/runtime/supervise/code-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,10 @@ export function renderCodeModeApi(
export interface CodeModeRunner {
run(args: {
readonly code: string
/** The granted operations, already deadline-gated and result-detached by the caller. The
/** The granted operations, already cancellation-gated and result-detached by the caller. The
* runner exposes these to the program as `api.<name>` and adds nothing else reachable. */
readonly bindings: Readonly<Record<string, (args: unknown) => Promise<unknown>>>
/** Aborts when the whole-program deadline passes or the manager scope cancels. */
/** Aborts when the manager cancels or a caller-authored deadline passes. */
readonly signal: AbortSignal
}): Promise<{ readonly result: unknown; readonly logs: ReadonlyArray<string> }>
}
Expand Down Expand Up @@ -240,10 +240,9 @@ function detach(value: unknown): unknown {
// ── The two tools ───────────────────────────────────────────────────────────────

export interface CodeModeOptions {
/** Whole-program deadline per `execute` call. Default 60_000. After it passes, the running
* program's next `api` call fails closed, so a runaway loop cannot keep spawning workers the
* model can no longer see. */
readonly timeoutMs?: number
/** Optional caller-authored deadline for one `execute` call. Omit it to run until the manager
* cancels. A declared deadline aborts the runner and refuses later `api` calls. */
readonly timeoutMs?: number | null
}

/**
Expand All @@ -266,7 +265,12 @@ export function codeModeSupervisorTools(
'codeModeSupervisorTools: a CodeModeRunner is required (no default) — pass unsafeInProcessRunner() for trusted output, or a jailed runner for untrusted models',
)
}
const timeoutMs = options.timeoutMs ?? 60_000
const timeoutMs = options.timeoutMs ?? null
if (timeoutMs !== null && (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0)) {
throw new ValidationError(
'codeModeSupervisorTools: timeoutMs must be a positive safe integer or null',
)
}

const faces = (context: SupervisorToolInvocationContext): ReadonlyArray<CoordinationToolFace> =>
context
Expand Down Expand Up @@ -315,25 +319,30 @@ export function codeModeSupervisorTools(
// runner is what isolates. See the module doc.
assertAuthoredCode(code, { context: `code mode (${context.nodeId})` })

// The whole-program deadline: a local controller linked to the manager scope signal and a
// timer. After it fires, every binding fails closed, so no api call lands post-deadline;
// the listener is removed in finally so it never outlives this execute call.
const deadline = new AbortController()
const onScopeAbort = () => deadline.abort(abortReason(context.signal))
const timer = setTimeout(
() =>
deadline.abort(
new ValidationError(`code mode: program timed out after ${timeoutMs}ms`),
),
timeoutMs,
)
if (context.signal.aborted) deadline.abort(abortReason(context.signal))
// Link execution to the manager's cancellation signal. Add a timer only when the caller
// declared one. After either signal fires, every binding fails closed.
const execution = new AbortController()
let timedOut = false
const onScopeAbort = () => execution.abort(abortReason(context.signal))
const timer =
timeoutMs === null
? null
: setTimeout(() => {
if (!execution.signal.aborted) {
timedOut = true
execution.abort(
new ValidationError(`code mode: program timed out after ${timeoutMs}ms`),
)
}
}, timeoutMs)
if (context.signal.aborted) execution.abort(abortReason(context.signal))
else context.signal.addEventListener('abort', onScopeAbort, { once: true })

const bindings: Record<string, (args: unknown) => Promise<unknown>> = {}
for (const [wire, member] of Object.entries(CODE_CALLABLE_VERBS)) {
bindings[wire] = async (args: unknown) => {
if (deadline.signal.aborted) {
if (execution.signal.aborted) {
if (!timedOut) throw abortReason(execution.signal)
throw new ValidationError(
`code mode: the execute deadline passed; api.${wire} is refused so no work outlives the call`,
)
Expand All @@ -342,9 +351,9 @@ export function codeModeSupervisorTools(
}
}
try {
return await runner.run({ code, bindings, signal: deadline.signal })
return await runner.run({ code, bindings, signal: execution.signal })
} finally {
clearTimeout(timer)
if (timer !== null) clearTimeout(timer)
context.signal.removeEventListener('abort', onScopeAbort)
}
},
Expand Down
9 changes: 4 additions & 5 deletions src/runtime/supervise/supervise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1290,12 +1290,11 @@ export interface SuperviseOptions {
* owes and reports how many workers passed the check. */
readonly onUnmetContract?: OnUnmetContract
/**
* How long live children may keep running after the ROOT DRIVER FAILED, before the join barrier
* cascades the abort into them. A root that died did not make its children unhealthy: a child
* mid-unit holds work already paid for, and an immediate cascade discards everything it has not
* yet written. Bounded by the run's own deadline. Omit/`0` = immediate teardown.
* How long live children may keep running after the root driver returns or fails, before the join
* barrier cascades the abort into them. `null` waits until children settle or the caller cancels.
* An explicit run deadline always wins. Omit/`0` = immediate teardown.
*/
readonly childSettleGraceMs?: number
readonly childSettleGraceMs?: number | null
/** Resolve one custom external-harness session per trusted manager identity. Use this instead of
* `driveHarness` when recursive managers must be independently steerable. */
readonly resolveDriveHarness?: ResolveDriveHarness
Expand Down
16 changes: 9 additions & 7 deletions src/runtime/supervise/supervisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,7 @@ function wrapJournalForBreaker(journal: SpawnJournal, breaker: IntensityBreaker)
async function drainLiveChildren(
scope: Scope<unknown>,
controller: AbortController,
settleGraceMs = 0,
settleGraceMs: number | null = 0,
): Promise<ReadonlyArray<UnconfirmedTeardown>> {
// Armed wait-states count here even though they are deliberately excluded from `inFlight`: a
// wait holds no executor, but it DOES hold a live timer, so a run that returns without
Expand All @@ -1075,13 +1075,13 @@ async function drainLiveChildren(
// children finish in the meantime. Exactly ONE cursor reader either way: the grace never races
// `next()`, it only decides when the abort lands.
let graceTimer: ReturnType<typeof setTimeout> | undefined
if (settleGraceMs > 0 && !controller.signal.aborted) {
if (settleGraceMs !== null && settleGraceMs > 0 && !controller.signal.aborted) {
graceTimer = setTimeout(
() => controller.abort('root driver failed; child settle grace expired'),
settleGraceMs,
)
graceTimer.unref?.()
} else if (!controller.signal.aborted) {
} else if (settleGraceMs !== null && !controller.signal.aborted) {
// Same event as the grace-timer branch above, so it carries the same named reason: one
// path stating why and the other going silent is what put identical deaths in two
// different diagnostic buckets.
Expand Down Expand Up @@ -1127,13 +1127,15 @@ function describeUnconfirmed(scope: Scope<unknown>): string {
}`
}

/** The settle window a failed driver's children actually get: the caller's grace, never past the
* run's own deadline. `0` (the default) keeps the historical immediate cascade. */
/** The settle window a driver's children actually get: the caller's grace, never past an explicit
* run deadline. `null` means no timer. `0` (the default) cascades immediately. */
function boundedSettleGrace(
graceMs: number | undefined,
graceMs: number | null | undefined,
deadlineAtMs: number,
now: () => number,
): number {
): number | null {
if (graceMs === null && deadlineAtMs <= 0) return null
if (graceMs === null) return Math.max(0, deadlineAtMs - now())
if (graceMs === undefined || graceMs <= 0) return 0
if (deadlineAtMs <= 0) return graceMs
return Math.max(0, Math.min(graceMs, deadlineAtMs - now()))
Expand Down
Loading