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
4 changes: 3 additions & 1 deletion docs/agent-managed-compute/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ The work is ordered to prove the two-agent atom before adding scale.
- Extend the existing run record with revisions, ownership generation, commands, provider references, and coordination events.
- Add durable adapters with conditional writes.
- Persist dispatch intent before provider creation.
Partially done: `startRetainedRun` blocks on its required `onAdmission` hook after creation and after dispatch, and `recoverRetainedRun` rebuilds a run from the pre-dispatch record.
Partially done: `startRetainedRun` persists admission after creation and dispatch.
`startRetainedRunInEnvironment` applies the same boundary to a fresh session in an existing environment.
`recoverRetainedRun` rebuilds a run from the pre-dispatch record.
The pre-creation intent record remains open.
- Add a durable provider-command outbox with coordinator generation and command sequence.
- Rebuild budget reservations and interaction state on restart.
Expand Down
4 changes: 3 additions & 1 deletion docs/api/primitive-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ Import from `@tangle-network/agent-runtime/intelligence` — 166 exports.

### Execution kernel — recursive atom, supervision, executors, round-synchronous loop

Import from `@tangle-network/agent-runtime/kernel` — 755 exports.
Import from `@tangle-network/agent-runtime/kernel` — 757 exports.

| Symbol | Kind | Summary |
|---|---|---|
Expand Down Expand Up @@ -715,6 +715,7 @@ Import from `@tangle-network/agent-runtime/kernel` — 755 exports.
| `settledWorkerOut` | function | What a settled worker exposes as its output artifact (the blob the brain's |
| `spendFromUsageEvents` | function | Fold a normalized `UsageEvent` array into a `Spend`. Tokens and usd are separate |
| `startRetainedRun` | function | Dispatch one detached, replayable run and return only after exact durable |
| `startRetainedRunInEnvironment` | function | Dispatch a fresh retained session inside an existing provider environment. |
| `stopSentinel` | function | A unique, attributable stop sentinel for a node (ralph-loop style). Deterministic from the |
| `streamAgentTurn` | function | Run ONE agent turn on any backend kind and stream its events. Yields the |
| `structuralRollout` | function | Build the structuralRollout `Strategy`: k shots → score each by the frozen visible |
Expand Down Expand Up @@ -987,6 +988,7 @@ Import from `@tangle-network/agent-runtime/kernel` — 755 exports.
| `SpawnJournal` | interface | The spawn-tree event source (mirrors `ConversationJournal`'s begin/append/load shape). |
| `Spend` | interface | Conserved spend, reconciled from the normalized `UsageEvent` stream. Tokens and usd |
| `SpendGap` | interface | One journaled node whose usage accounting is incomplete — the named gap behind a `false` |
| `StartRetainedRunInEnvironmentOptions` | interface | A fresh retained session inside a provider environment that already exists. |
| `StartRetainedRunOptions` | interface | A retained start is retry-safe only when environment and turn keys are explicit. |
| `SteerableRootHandle` | interface | A Runtime-minted root handle that can deliver raw steering or answers to a live manager inbox. |
| `SteerableSandboxSession` | interface | What the steerable session exposes to its executor: the usage stream plus the live reads. |
Expand Down
95 changes: 93 additions & 2 deletions docs/api/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -920,7 +920,7 @@ Manager-scoped assignment identity, including deterministic ids for unkeyed sibl

###### Inherited from

[`NodeSnapshot`](#nodesnapshot).[`identity`](#identity-7)
[`NodeSnapshot`](#nodesnapshot).[`identity`](#identity-8)

##### materialization?

Expand Down Expand Up @@ -6290,6 +6290,73 @@ so every process derives the same values.

***

### StartRetainedRunInEnvironmentOptions

**`Stable`**

A fresh retained session inside a provider environment that already exists.

#### Properties

##### provider

> `readonly` **provider**: `AgentEnvironmentProvider`

##### environment

> `readonly` **environment**: `object`

###### id

> `readonly` **id**: `string`

Stable provider environment identifier used by `provider.get`.

###### idempotencyKey

> `readonly` **idempotencyKey**: `string`

Original environment key retained for deterministic run identity and recovery records.

##### turn

> `readonly` **turn**: `AgentTurnInput` & `object`

###### Type Declaration

###### turnId

> **turnId**: `string`

##### identity?

> `readonly` `optional` **identity?**: `object`

Explicit fresh-session coordinates. When omitted, the runtime mints them
from `(environment.idempotencyKey, turn.turnId)`.

###### sessionId

> `readonly` **sessionId**: `string`

###### executionId

> `readonly` **executionId**: `string`

##### onAdmission

> `readonly` **onAdmission**: [`RetainedRunAdmissionHook`](#retainedrunadmissionhook)

##### now?

> `readonly` `optional` **now?**: () => `number`

###### Returns

`number`

***

### ReconnectRetainedRunOptions

**`Stable`**
Expand Down Expand Up @@ -14446,7 +14513,7 @@ Stable identity of this manager's coordination stream.

###### Inherited from

[`SupervisorNodeContext`](#supervisornodecontext).[`identity`](#identity-2)
[`SupervisorNodeContext`](#supervisornodecontext).[`identity`](#identity-3)

##### assignmentId?

Expand Down Expand Up @@ -22787,6 +22854,30 @@ whose exact reference a crash would lose.

***

### startRetainedRunInEnvironment()

> **startRetainedRunInEnvironment**(`options`): `Promise`\<[`RetainedRunHandle`](#retainedrunhandle)\>

**`Stable`**

Dispatch a fresh retained session inside an existing provider environment.

This operation reuses only the environment. It does not append to a prior
harness chat and does not claim native conversation continuity. The caller
must use `RetainedRunHandle.continueNative` for a verified same-chat turn.

#### Parameters

##### options

[`StartRetainedRunInEnvironmentOptions`](#startretainedruninenvironmentoptions)

#### Returns

`Promise`\<[`RetainedRunHandle`](#retainedrunhandle)\>

***

### recoverRetainedRun()

> **recoverRetainedRun**(`options`): `Promise`\<[`RecoverRetainedRunResult`](#recoverretainedrunresult)\>
Expand Down
4 changes: 2 additions & 2 deletions docs/api/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ Product authority over every steer/answer instruction (the filter seam). `runGra

###### Inherited from

[`RunGraphOptions`](runtime.md#rungraphoptions).[`now`](runtime.md#now-7)
[`RunGraphOptions`](runtime.md#rungraphoptions).[`now`](runtime.md#now-8)

##### otel?

Expand Down Expand Up @@ -1187,7 +1187,7 @@ Give the supervisor brain a chapter-lifecycle on its OWN context window (router

###### Inherited from

[`SuperviseOptions`](runtime.md#superviseoptions).[`now`](runtime.md#now-13)
[`SuperviseOptions`](runtime.md#superviseoptions).[`now`](runtime.md#now-14)

##### allowedModels?

Expand Down
1 change: 1 addition & 0 deletions docs/canonical-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ A general "loop" primitive is the single most common modelling error in this rep
| Run **agent-eval fixture folders** through Runtime `runAgentRounds` | agent-eval fixture loading/planning, then `loopCampaignDispatch(...)`: `/kernel`; it starts the Runtime cell inside Eval's paid-call lifecycle | a one-off `runCampaign` dispatch, or attaching a completed `LoopResult` after paid work already ran |
| Run a **recursive `supervise()` tree** through an agent-eval profile matrix | `superviseDispatch({ toTask, toSuperviseOptions, ... })`: `/kernel`; it admits the tree through Eval before Runtime spends, then records its receipt only when Runtime proves one model. Mixed or unknown trees fail instead of being relabelled. | a Lab receipt mapper, a second scheduler, or attaching a completed `SupervisedResult` after paid work already ran |
| Run + **resume** ONE persistent box across turns | `openSandboxRun(client, opts, deliverable)`: `/kernel` | a per-domain `new Sandbox`+`box.fs.read`+delete copy |
| Start a retry-safe detached run in a new environment, or a fresh harness chat in one existing environment | `startRetainedRun(...)` or `startRetainedRunInEnvironment(...)`: `/kernel`; both persist exact coordinates before and after dispatch, while only `continueNative(...)` may claim same-chat continuity | calling `provider.create/get/dispatch` directly, reusing an environment as proof of chat continuity, or appending to an unverified native session |
| Run **ONE agent turn** on any substrate: box (`streamPrompt`), cli-bridge/router `Executor`, or in-process chat backend: as ONE normalized `RuntimeStreamEvent` stream with a guaranteed terminal result+usage event; opt into in-stream `tool_call`/`tool_result` with `preserveToolParts`, or tap the raw sandbox events with `onRawEvent` | `streamAgentTurn(backend, prompt, { signal, timeoutMs, preserveToolParts?, onRawEvent? })` + `collectAgentTurn(stream)`: `/kernel` | a per-provider stream→event mapper zoo, a hand-faked box around a non-box executor, or raw fetch leaking through the turn abstraction |
| Use an exact profile and Runtime executor where `runAgentTaskStream` or a conversation expects an `AgentExecutionBackend` | `createProfileExecutionBackend({ profile, executor: createExecutor(config) })`: root `.`; the adapter preserves conversation authorization, recursion-depth, and trace headers | a provider-specific backend constructor or an adapter that reads a second model/prompt configuration |
| Pick the **execution transport a driven loop runs on** (`sandbox` box / cli-bridge / router) from a product flag | `resolveSandboxClient({ backend })`: `/kernel` | a per-product `if (backend === 'router') …` branch re-wiring `createExecutor` + `inlineSandboxClient` |
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,10 @@ export {
type RetainedRunSnapshot,
reconnectRetainedRun,
recoverRetainedRun,
type StartRetainedRunInEnvironmentOptions,
type StartRetainedRunOptions,
startRetainedRun,
startRetainedRunInEnvironment,
} from './retained-run'
// Router requests are an internal transport adapter. Public execution always enters through an
// exact AgentProfile (`createExecutor` + `streamAgentTurn`); callers may configure only the
Expand Down
24 changes: 18 additions & 6 deletions src/runtime/retained-run-binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,19 +264,31 @@ export function assertEventBinding(
controlRef: AgentExactRunControlRef,
): void {
const transport = isRecord(source) ? source : undefined
const record = isRecord(source.data) ? source.data : undefined
const payload = isRecord(source.data) ? source.data : undefined
const providerEvent = isRecord(source.providerEvent) ? source.providerEvent : undefined
const values = [transport, record, providerEvent].filter(
(value): value is Record<string, unknown> => value !== undefined,
)
for (const value of values) {
const bindings = [
{ value: transport, checkSessionId: true },
{
value: payload,
// The canonical session.updated payload identifies the harness-native chat.
// It is event content, not the retained provider session coordinate.
checkSessionId: source.type !== 'session.updated',
},
{ value: providerEvent, checkSessionId: true },
]
for (const { value, checkSessionId } of bindings) {
if (value === undefined) continue
if (value.runId !== undefined && value.runId !== controlRef.runId) {
throw new Error('provider returned an event for another retained run')
}
if (value.executionId !== undefined && value.executionId !== controlRef.executionId) {
throw new Error('provider returned an event for another retained execution')
}
if (value.sessionId !== undefined && value.sessionId !== controlRef.sessionId) {
if (
checkSessionId &&
value.sessionId !== undefined &&
value.sessionId !== controlRef.sessionId
) {
throw new Error('provider returned an event for another retained session')
}
}
Expand Down
97 changes: 89 additions & 8 deletions src/runtime/retained-run-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
AgentExactRunControlRefSchema,
} from '@tangle-network/agent-interface'
import type {
AgentEnvironment,
AgentEnvironmentCapabilities,
AgentEnvironmentProvider,
AgentSession,
Expand All @@ -22,6 +23,7 @@ import type {
RetainedRunAdmission,
RetainedRunAdmissionHook,
RetainedRunHandle,
StartRetainedRunInEnvironmentOptions,
StartRetainedRunOptions,
} from './retained-run-types'
import { freshTurnInput } from './turn-input'
Expand Down Expand Up @@ -107,22 +109,101 @@ export async function startRetainedRun(
throw new Error(`provider "${options.provider.name}" does not expose detached session control`)
}

// The environment admission fires only for a dispatch-capable environment:
// an environment destroyed above must never leave a durable record.
return dispatchRetainedRun({
provider: options.provider,
environment,
environmentIdempotencyKey: options.environment.idempotencyKey,
turn: options.turn,
identity,
onAdmission: options.onAdmission,
capabilities,
now: options.now,
})
}

/**
* Dispatch a fresh retained session inside an existing provider environment.
*
* This operation reuses only the environment. It does not append to a prior
* harness chat and does not claim native conversation continuity. The caller
* must use `RetainedRunHandle.continueNative` for a verified same-chat turn.
*
* @stable
*/
export async function startRetainedRunInEnvironment(
options: StartRetainedRunInEnvironmentOptions,
): Promise<RetainedRunHandle> {
assertStableText(options.environment.id, 'retained environment id')
assertStableText(options.environment.idempotencyKey, 'environment idempotency key')
assertStableText(options.turn.turnId, 'turn idempotency key')
if (options.identity !== undefined) {
assertStableText(options.identity.sessionId, 'retained session id')
assertStableText(options.identity.executionId, 'retained execution id')
}
if (typeof options.onAdmission !== 'function') {
throw new Error('startRetainedRunInEnvironment requires an awaited onAdmission durability hook')
}
const identity =
options.identity ??
mintRetainedIdentity(options.environment.idempotencyKey, options.turn.turnId)
const capabilities = await assertRetainedCapabilities(options.provider)
if (!options.provider.get) {
throw new Error(`provider "${options.provider.name}" cannot reconstruct an environment by id`)
}
const environment = await options.provider.get(options.environment.id)
if (!environment) {
throw new Error(
`provider "${options.provider.name}" no longer holds environment "${options.environment.id}"`,
)
}
if (environment.id !== options.environment.id || environment.provider !== options.provider.name) {
throw new Error('provider reconstructed a different retained environment')
}
if (!environment.dispatch || !environment.session) {
throw new Error(`provider "${options.provider.name}" does not expose detached session control`)
}

return dispatchRetainedRun({
provider: options.provider,
environment,
environmentIdempotencyKey: options.environment.idempotencyKey,
turn: options.turn,
identity,
onAdmission: options.onAdmission,
capabilities,
now: options.now,
})
}

interface DispatchRetainedRunOptions {
readonly provider: AgentEnvironmentProvider
readonly environment: AgentEnvironment
readonly environmentIdempotencyKey: string
readonly turn: StartRetainedRunOptions['turn']
readonly identity: { readonly sessionId: string; readonly executionId: string }
readonly onAdmission: RetainedRunAdmissionHook
readonly capabilities: AgentEnvironmentCapabilities
readonly now?: () => number
}

async function dispatchRetainedRun(
options: DispatchRetainedRunOptions,
): Promise<RetainedRunHandle> {
const { environment, identity } = options
// The environment admission fires only for a dispatch-capable environment.
await admitDurably(options.onAdmission, {
phase: 'environment',
provider: options.provider.name,
environmentId: environment.id,
idempotencyKey: options.environment.idempotencyKey,
idempotencyKey: options.environmentIdempotencyKey,
turnId: options.turn.turnId,
sessionId: identity.sessionId,
executionId: identity.executionId,
})

// Once dispatch begins, its outcome may be unknown to this process. Keep the
// idempotently-created environment so a retry or reconnect can recover the
// retained operation instead of destroying work that may already be live.
const reference = await environment.dispatch(
// environment so a retry or reconnect can recover work that may already be live.
const reference = await environment.dispatch!(
freshTurnInput(options.turn, {
turnId: options.turn.turnId,
detach: true,
Expand Down Expand Up @@ -168,14 +249,14 @@ export async function startRetainedRun(
await admitDurably(options.onAdmission, {
phase: 'dispatched',
controlRef: freezeControlRef(exact.controlRef),
idempotencyKey: options.environment.idempotencyKey,
idempotencyKey: options.environmentIdempotencyKey,
turnId: options.turn.turnId,
})
return createRetainedRunHandle(
environment,
exact.session,
exact.controlRef,
capabilities,
options.capabilities,
options.now,
)
}
Expand Down
Loading