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
25 changes: 11 additions & 14 deletions docs/architecture/runtime-resume-extraction-ledger.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,11 +310,11 @@ B3(typed retry/reattach branch)仍然 defer,不进入本 PR。
| `agent-run.ts` | Run create 与 backend reservation 之间提交 continuation-start |
| `runtime-continuation-admission.ts` | opaque、runner-bound、one-shot start proof/receipt 及精确 identity 绑定 |
| `runtime-kernel.ts` | continuation dispatch fail closed;仅消费合法的一次性 start proof |
| `session-manager.ts` | claim-only saga、claim-owned child admission defer、proven-abandonment retry、branch/revision 创建前 preflight |
| `session-manager.ts` | claim-only saga、branch/revision 创建前 preflight;历史 linked-child admission descriptor 仅作重启关闭兼容,不再提供 live child retry |
Comment thread
Astro-Han marked this conversation as resolved.
Comment thread
Astro-Han marked this conversation as resolved.
| `runtime-event-read-model.ts` | continuation-start 是消息不可见的 canonical audit fact |
| `runtime-continuation*.test.ts` | lineage、claim、T1、SIGKILL crash matrix |
| `session-manager.test.ts` | proof 防伪、one-shot、provider replay mutation fence 与无重复 user event |
| `session-manager.test.ts` | production-shaped plan/execute/race/failure/stop,以及 linked/legacy child retry 的 V2 claim/start |
| `session-manager.test.ts` | production-shaped plan/execute/race/failure/stop,以及历史 linked-child admission 的重启关闭兼容 |

#### UI 与文档

Expand Down Expand Up @@ -359,8 +359,8 @@ B3(typed retry/reattach branch)仍然 defer,不进入本 PR。
| start writer exact retry | 同一个物理 `event_seq=1` |
| terminal 后追加 immutable event | rejected;exact terminal retry 仍幂等 |
| stop after durable start | provider dispatch 被 fence |
| claim-owned linked child admission 与 generic repair 竞争 | generic repair defer |
| child abandonment retry | 必须证明 deterministic repair start + terminal,字符串篡改 fail closed |
| claim-owned historical linked-child admission closure 与 generic repair 竞争 | generic repair defer |
| historical child abandonment closure | deterministic repair start + terminal,provider 0 次;字符串篡改 fail closed |
| branch/revision 遇到 V1/V2 continuation 或 authority fact | 创建新 Session 前拒绝 |
| SIGKILL after claim/run create/live start/terminal event/terminal header | reopen 后稳定分类;live start/no-terminal 保守 park |

Expand All @@ -369,20 +369,17 @@ B3(typed retry/reattach branch)仍然 defer,不进入本 PR。
- authority-capable `SessionManager + SqliteRuntimeStore` 的协议与 production-shaped 路径已覆盖;
- runtime-host 的 execution-store facade 当前仍以 file RuntimeEvent store 为主,尚未拥有 B2
continuation authority;
- hosted child provider RateLimit retry 只保留 `durable_continuation` 单一准入:continuation
authority 与 safety inspector 任一缺席,都会在任何 claim/Run/T1 之前 fail closed,禁止
静默 fallback;早期的 `legacy_provider_retry` 兼容 lane 已在 host authority lifecycle
integration 完成后移除;
- hosted child provider RateLimit retry 入口已经删除;历史 `linked_child_resume` /
`linked_child_provider_retry` descriptor 只在 startup recovery 中收敛为 durable terminal
fact,provider 调用次数为 0;早期的 `legacy_provider_retry` 兼容 lane 也已移除;
- PR D 必须在同一 storage-root lease 下接入 SQLite authority,并锁定
`claim repair → linked-child admission repair → generic ledger repair → planning` 的 owner 顺序;
`claim repair → historical linked-child admission closure → generic ledger repair → planning`
的 owner 顺序;
- SQLite canonical terminal 可幂等提交;文件型 AgentRun projection 当前是跨存储 saga。确定性
event id 能让单恢复者重试收敛,但两个进程同时 repair 时还没有 append-if-absent/CAS;
PR D 的 lease/fencing 或 projection CAS 是宣称跨进程 exactly-once 前的硬前置;
- 在上述 composition/owner 测试完成前,不能把当前切片描述为 hosted auto-resume 已默认可用。
`legacy_provider_retry` 兼容 lane(仅维持升级前的 provider 429 重试能力,不具有 durable
continuation、跨进程 exactly-once 或 crash-resume 承诺)已在 host authority lifecycle
integration 完成 typed authority composition 后移除;child provider retry 现在只走
durable continuation 准入,组合不完整即 fail closed。
- 在上述 composition/owner 测试完成前,不能把当前切片描述为 hosted auto-resume 已默认可用;
当前也不存在 child provider retry 的 live 或降级准入路径。

### 8.4 明确不进入 PR B

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,6 @@ composite replay 存在时,它是 tool-state 与 provider suffix 的唯一 gat
`buildResumePlanFromRuntimeEvents()` 只服务无 composite boundary 的 legacy 路径,不能在外层
再次把已经安全裁掉的 `definitely_not_dispatched` call 判为 dangling。

`retryChildAgentWithExecution()` 同样通过 `RuntimeContinuationPlanner` 构造 immutable
composite boundary。旧 child 的 `retriedFromRunId/resumedFromRunId` 边从对应 immutable prefix
严格派生临时 V1 segment;新 child retry 持久化 V2 `continuationSource`。该路径不再使用
`readRuntimeEvents()`、`events.length` 或独立的 `buildResumePlanFromRuntimeEvents()` 组装
provider history。

最终同时冻结:

- `providerProjectionVersion = 1`;
Expand Down Expand Up @@ -287,9 +281,9 @@ sequenceDiagram
5. 通过 dedicated writer 提交 `continuation_start_v2`,必须是 target `event_seq=1`;
6. 才允许 append running turn state、reserve backend、标记 running、调用 provider。

linked child 与 legacy child 的 provider retry 也按这套顺序执行:重新验证 boundary/replay、
workspace/background/tool catalog,竞争同一个 SQLite boundary claim,再写 continuation-start。
`retriedFromRunId` 继续承担产品查询与展示语义,但不承担并发执行所有权
linked child 当前通过普通 Session turn 执行,不再有 same-session child AgentRun 的 provider
retry 入口。历史 `linked_child_resume` / `linked_child_provider_retry` descriptor 与
`retriedFromRunId` 只保留重启关闭、查询和展示兼容,不会重新触发 provider

live continuation-start 同时绑定 claim id、boundary digest、immediate source identity/high-water/prefix
digest、replay manifest、provider projection version 和 provider replay digest。V2 AgentRun header 的
Expand Down Expand Up @@ -327,11 +321,10 @@ failed terminal,failureClass 为
另一个进程擅自补 terminal,因为原 provider 可能仍存活。SQLite writer 同时拒绝 terminal 后追加
任何 immutable event,作为最终写侧防线。

linked-child 的 generic admission repair 在发现 target identity 已由 continuation claim 占有时
必须 defer,不能抢先创建一个缺少 V2 source 的同 id Run。只有同时证明 claim target、B2.1
deterministic repair start、deterministic terminal 与 target header 全部一致,才允许把
`continuation_abandoned_before_provider_dispatch` Run 作为 child provider retry source。仅凭
字符串 failureClass 或 V2 header 不构成证明。
历史 linked-child admission closure 在发现 target identity 已由 continuation claim 占有时必须
defer,不能抢先创建一个缺少 V2 source 的同 id Run。没有 claim owner 时,该 closure 只保留旧
descriptor 的 lineage 并物化 durable failed terminal fact,provider 调用数为 0;它不会把 repaired
Run 重新变成 child provider retry source。

canonical continuation authority 读取失败时,best-effort startup 必须隔离整个 session,不允许
退回 generic/legacy repair。否则一个暂时读不到 claim 的 host 可能把 claim-owned target 当成普通
Expand Down Expand Up @@ -377,18 +370,17 @@ lease/fencing 或 append-if-absent 解决。
#### B3:明确延后

本 PR 不实现通用 provider retry、ShellRun reattach、Bash 重放、conversation clone identity
rewrite 或其他 typed continuation branch。authority-capable SessionManager 内已有的
linked-child RateLimit retry 与 B2.1 repair retry 是窄协议,不代表 runtime-host 已获得生产启用
资格。其他能力必须在各自拥有 durable handle/幂等协议后独立设计,不能复用 B2 的普通
continuation claim 来暗示副作用可重跑
rewrite 或其他 typed continuation branch。linked-child RateLimit retry 入口已经删除;B2.1
repair retry 只修复已持久化的 continuation authority,不会重新调用 provider。其他能力必须在
各自拥有 durable handle/幂等协议后独立设计,不能复用 B2 的普通 continuation claim 来暗示
副作用可重跑

兼容约束:早期的 `legacy_provider_retry` lane(只允许 continuation authority 与 safety
inspector 同时缺席的组合,半配置状态 fail closed;执行前重验 immutable immediate-source
replay,但不产生 claim/start,也不能承接
`continuation_abandoned_before_provider_dispatch`)已在 host authority lifecycle
integration 接入 typed SQLite authority owner 后移除。child provider RateLimit retry 现在
只走 durable continuation 准入:authority 或 safety inspector 任一缺席时在 claim/Run/T1
之前 fail closed,没有降级模式。
integration 接入 typed SQLite authority owner 后移除。历史 child admission descriptor 在
恢复时只会被收敛为 durable terminal fact,不存在 provider retry 降级模式。

B3 之前,branch/revision preflight 必须在创建目标 Session 之前拒绝任何 V1/V2
`continuationSource` 与 continuation-start,稳定返回
Expand Down Expand Up @@ -517,7 +509,7 @@ Host owner 使用显式 `opening -> ready -> closing -> closed` 状态机;`clo

```text
continuation claim repair
→ linked-child admission repair
historical linked-child admission closure
→ generic AgentRun ledger repair
→ ordinary continuation planning / auto-resume
```
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/agent-swarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ export interface AgentSwarmResultProjection {

/**
* Bounded presentation/diagnostic facts derived from the canonical settled
* tool result. This is a projection only: child AgentRuns remain the authority
* for child lifecycle and artifacts.
* tool result. This is a projection only: linked child Sessions remain the
* authority for child lifecycle and artifacts.
*/
export function projectAgentSwarmResult(result: AgentSwarmResult): AgentSwarmResultProjection {
let startedItemCount = 0;
Expand Down
20 changes: 0 additions & 20 deletions packages/core/src/runtime-inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,6 @@ export interface UserMessageInput extends MessageContent {
turnOrchestration?: TurnOrchestration;
/** Trusted host-supplied tool protocol override for this run only. */
toolMode?: ToolMode;
parentRunId?: string;
/** Child AgentRun whose durable conversation this child continues. */
resumedFromRunId?: string;
/** Immediate child AgentRun retried without appending another user prompt. */
retriedFromRunId?: string;
agentId?: string;
agentName?: string;
parentTurnId?: string;
Expand All @@ -133,21 +128,6 @@ export interface UserMessageInput extends MessageContent {
origin?: TurnOrigin;
}

export interface AgentSpec {
id: string;
name: string;
systemPrompt: string;
}

export interface ChildAgentTurnInput {
turnId: string;
parentRunId: string;
spec: AgentSpec;
prompt: string;
/** Trusted, preflighted child AgentRun whose RuntimeEvent history is replayed. */
resumedFromRunId?: string;
}

export interface RegenerateTurnInput {
sourceTurnId: string;
turnId?: string;
Expand Down
164 changes: 0 additions & 164 deletions packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2720,116 +2720,6 @@ test('hosted linked child roots share admission, message, terminal, and stop aut
},
});

const resumeAbort = new AbortController();
resumeAbort.abort();
const runsBeforeAbortedResume = await stores.agentRunStore.listSessionRuns(
child.childSessionId,
);
const sendsBeforeAbortedResume = linkedBackends.get(child.childSessionId)?.sendCount;
let abortedResumeReady = 0;
await assert.rejects(
manager.resumeChildAgent(parent.id, {
parentRunId: parentStarted.result.turn.runId,
sourceRunId: child.runId,
prompt: 'must not start',
abortSignal: resumeAbort.signal,
onReady: () => {
abortedResumeReady += 1;
},
}),
{ name: 'AbortError' },
);
assert.equal(
(await stores.agentRunStore.listSessionRuns(child.childSessionId)).length,
runsBeforeAbortedResume.length,
);
assert.equal(linkedBackends.get(child.childSessionId)?.sendCount, sendsBeforeAbortedResume);
assert.equal(abortedResumeReady, 0);

let resumeReadyRunId: string | undefined;
let resumeEventCount = 0;
const resumed = await manager.resumeChildAgent(parent.id, {
parentRunId: parentStarted.result.turn.runId,
sourceRunId: child.runId,
prompt: 'rate limit this resumed child',
onReady: (ready) => {
resumeReadyRunId = ready.runId;
},
onEvent: () => {
resumeEventCount += 1;
},
});
assert.equal(resumed.status, 'failed');
assert.equal(resumed.failureClass, 'RateLimit');
assert.equal(resumed.resumedFromRunId, child.runId);
assert.equal(resumeReadyRunId, resumed.runId);
assert.equal(resumeEventCount, resumed.eventCount);

let retryReadyRunId: string | undefined;
let retryEventCount = 0;
const retried = await manager.retryChildAgent(parent.id, {
parentRunId: parentStarted.result.turn.runId,
sourceRunId: resumed.runId!,
execution: {
kind: 'child_session',
sessionId: child.childSessionId,
currentRunId: resumed.runId,
},
onReady: (ready) => {
retryReadyRunId = ready.runId;
},
onEvent: () => {
retryEventCount += 1;
},
});
assert.equal(retried.status, 'completed');
assert.equal(retried.retriedFromRunId, resumed.runId);
assert.equal(retryReadyRunId, retried.runId);
assert.equal(retryEventCount, retried.eventCount);
const admissions = await stores.agentRunStore.listRootTurnAdmissionsForRecovery(
child.childSessionId,
);
assert.equal(admissions.length, 3);
assert.equal(admissions[1]?.runId, resumed.runId);
assert.ok(admissions[1]?.userMessageId);
assert.deepEqual(admissions[1]?.execution, {
kind: 'linked_child_resume',
agentId: resumed.agentId,
agentName: resumed.agentName,
sourceRunId: child.runId,
});
assert.equal(admissions[2]?.runId, retried.runId);
assert.equal(admissions[2]?.userMessageId, null);
assert.deepEqual(admissions[2]?.execution, {
kind: 'linked_child_provider_retry',
agentId: retried.agentId,
agentName: retried.agentName,
sourceRunId: resumed.runId,
});
const retryMessages = (await stores.sessionStore.readMessages(child.childSessionId)).filter(
(message) => 'turnId' in message && message.turnId === retried.turnId,
);
assert.deepEqual(retryMessages, []);
const durableRetryRun = await stores.agentRunStore.readRun(
child.childSessionId,
retried.runId!,
);
const durableRetrySource = durableRetryRun.continuationSource;
assert.ok(durableRetrySource && 'protocol' in durableRetrySource);
if (!durableRetrySource || !('protocol' in durableRetrySource)) return;
assert.equal(durableRetrySource.sourceRunId, resumed.runId);
assert.equal(durableRetrySource.protocol, 'continuation_source_v2');
const continuationStart = (
await stores.runtimeEventStore.readImmutableRuntimeEvents(
child.childSessionId,
retried.runId!,
)
)[0]?.actions?.continuationStart;
assert.equal(continuationStart?.claimId, durableRetrySource.claimId);
assert.deepEqual(coordinator.readRootState(child.childSessionId), {
kind: 'idle',
});

const callbackAbortController = new AbortController();
const stopClosureObserved = deferred<void>();
stopClosureSignal = stopClosureObserved;
Expand Down Expand Up @@ -2898,60 +2788,6 @@ test('hosted linked child roots share admission, message, terminal, and stop aut
assert.equal(followupState.kind, 'active');
if (followupState.kind !== 'active') return;

await assert.rejects(
manager.resumeChildAgent(parent.id, {
parentRunId: parentStarted.result.turn.runId,
sourceRunId: retried.runId!,
prompt: 'internal resume racing the external follow-up',
}),
(error) => {
assert.ok(error instanceof RuntimeHostedRootConflictError);
assert.equal(error.code, 'session_busy');
assert.deepEqual(error.scope, {
kind: 'session',
sessionId: child.childSessionId,
});
return true;
},
);
assert.equal(drainRequested, false);
await coordinator.stopRoot(followupState);
assert.deepEqual(coordinator.readRootState(child.childSessionId), {
kind: 'idle',
});

const failedResume = await manager.resumeChildAgent(parent.id, {
parentRunId: parentStarted.result.turn.runId,
sourceRunId: retried.runId!,
prompt: 'rate limit one more linked child',
});
assert.equal(failedResume.status, 'failed');
const linkedBackend = linkedBackends.get(child.childSessionId);
assert.ok(linkedBackend);
const runsBeforeAbortedRetry = await stores.agentRunStore.listSessionRuns(child.childSessionId);
const sendsBeforeAbortedRetry = linkedBackend?.sendCount;
const retryAbort = new AbortController();
retryAbort.abort();
let abortedRetryReady = 0;
await assert.rejects(
manager.retryChildAgent(parent.id, {
parentRunId: parentStarted.result.turn.runId,
sourceRunId: failedResume.runId!,
abortSignal: retryAbort.signal,
onReady: () => {
abortedRetryReady += 1;
},
}),
{ name: 'AbortError' },
);
assert.equal(
(await stores.agentRunStore.listSessionRuns(child.childSessionId)).length,
runsBeforeAbortedRetry.length,
);
assert.equal(linkedBackend?.sendCount, sendsBeforeAbortedRetry);
assert.equal(abortedRetryReady, 0);
assert.equal(drainRequested, false);

const abortController = new AbortController();
let joinedInitial: Promise<typeof child> | undefined;
const interrupted = await manager.spawnChildSession(parent.id, {
Expand Down
21 changes: 2 additions & 19 deletions packages/runtime-host/src/server/child-agent-composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,12 @@ import { type SessionManager } from '@maka/runtime/session-manager';

type ChildAgentAuthority = Pick<
SessionManager,
| 'spawnChildAgent'
| 'spawnChildSession'
| 'prepareChildAgentResume'
| 'resumeChildAgent'
| 'retryChildAgent'
| 'listChildAgents'
| 'readChildAgentOutput'
'spawnChildSession' | 'listChildAgents' | 'readChildAgentOutput'
>;

export type HostChildAgentBackendCapabilities = Pick<
ConstructorParameters<typeof AiSdkBackend>[0],
| 'spawnChildAgent'
| 'spawnChildSession'
| 'prepareChildAgentResume'
| 'resumeChildAgent'
| 'retryChildAgent'
| 'listChildAgents'
| 'readChildAgentOutput'
'spawnChildSession' | 'listChildAgents' | 'readChildAgentOutput'
>;

export interface HostChildAgentToolComposition {
Expand Down Expand Up @@ -84,7 +72,6 @@ export function bindHostChildAgentBackend(
parentSessionId: string,
): HostChildAgentBackendCapabilities {
return {
spawnChildAgent: (input) => authority.spawnChildAgent(parentSessionId, input),
spawnChildSession: (input) =>
authority.spawnChildSession(parentSessionId, {
spawnedBy: {
Expand All @@ -100,10 +87,6 @@ export function bindHostChildAgentBackend(
...(input.onReady ? { onReady: input.onReady } : {}),
...(input.onEvent ? { onEvent: input.onEvent } : {}),
}),
prepareChildAgentResume: (sourceRunId) =>
authority.prepareChildAgentResume(parentSessionId, sourceRunId),
resumeChildAgent: (input) => authority.resumeChildAgent(parentSessionId, input),
retryChildAgent: (input) => authority.retryChildAgent(parentSessionId, input),
listChildAgents: () => authority.listChildAgents(parentSessionId),
readChildAgentOutput: (input) => authority.readChildAgentOutput(parentSessionId, input),
};
Expand Down
Loading