@springbrand/agent-runtime 0.1.3-alpha.0 → 0.1.3-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -3
- package/src/adapter/cloudflare/index.ts +56 -0
- package/src/adapter/cloudflare/resources/runtime-resources.ts +89 -0
- package/src/adapter/cloudflare/sandbox/adapter.ts +1513 -0
- package/src/adapter/cloudflare/sandbox/id.ts +23 -0
- package/src/adapter/cloudflare/sandbox/policy.ts +15 -0
- package/src/adapter/cloudflare/subagent/definition.ts +574 -0
- package/src/adapter/cloudflare/subagent/runner.ts +175 -0
- package/src/adapter/cloudflare/subagent/tools.ts +254 -0
- package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
- package/src/adapter/cloudflare/universal-agent/preparation.ts +277 -0
- package/src/adapter/cloudflare/universal-agent/tools.ts +80 -0
- package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
- package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -0
- package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
- package/src/agent-tool-runtime.ts +152 -0
- package/src/db/agent-tool.repo.ts +27 -0
- package/src/db/index.ts +33 -0
- package/src/db/interaction.repo.ts +185 -0
- package/src/db/schema.ts +25 -1
- package/src/db/submission.repo.ts +63 -1
- package/src/index.ts +61 -27
- package/src/kernel/approval-lifecycle.ts +41 -6
- package/src/kernel/bindings.ts +99 -12
- package/src/kernel/extensions.ts +1 -1
- package/src/kernel/interaction-lifecycle.ts +395 -0
- package/src/kernel/profile.ts +3 -4
- package/src/kernel/public-contracts.ts +2 -0
- package/src/kernel/recoverable-chat-agent.ts +104 -6
- package/src/kernel/runtime-assembly-view.ts +37 -0
- package/src/kernel/runtime-assembly.ts +41 -0
- package/src/kernel/runtime-config.ts +4 -0
- package/src/kernel/runtime-load.ts +191 -0
- package/src/kernel/state.ts +12 -1
- package/src/kernel/submission-lifecycle.ts +33 -2
- package/src/layers/orchestration/temporary-agent/core.ts +12 -1
- package/src/layers/orchestration/temporary-agent/runner.ts +1 -67
- package/src/lib/mcp.ts +7 -3
- package/src/lib/prompt.ts +1 -1
- package/src/lib/telemetry-dev.ts +7 -4
- package/src/pi/assembly/context.ts +4 -6
- package/src/pi/assembly/extensions.ts +11 -22
- package/src/pi/assembly/snapshot.ts +17 -9
- package/src/pi/message/contract.ts +7 -0
- package/src/pi/message/conversion.ts +9 -1
- package/src/pi/runtime-adapter/assembly.ts +42 -97
- package/src/pi/runtime-adapter/execution.ts +216 -21
- package/src/pi/runtime-adapter/index.ts +24 -8
- package/src/pi/runtime-adapter/models.ts +382 -35
- package/src/pi/runtime-adapter/recovery.ts +188 -1
- package/src/pi/runtime-adapter/transcript.ts +61 -3
- package/src/pi/tool/ai-adapter.ts +58 -1
- package/src/pi/tool/base.ts +190 -12
- package/src/pi/tool/compiler.ts +39 -33
- package/src/pi/tool/core-host.ts +28 -30
- package/src/pi/tool/core.ts +37 -124
- package/src/pi/tool/gateway.ts +54 -0
- package/src/pi/tool/index.ts +2 -0
- package/src/pi/tool/mcp.ts +98 -70
- package/src/pi/tool/schedule.ts +86 -20
- package/src/pi/tool/skill.ts +126 -420
- package/src/pi/tool/subagent.ts +14 -2
- package/src/pi/tool/web-fetch.ts +281 -0
- package/src/pi/tool/web-search/api.ts +34 -18
- package/src/pi/tool/web-search/web-search.ts +0 -1
- package/src/pi/tool/workspace-revision.ts +64 -0
- package/src/pi/tool/workspace-sandbox.ts +105 -263
- package/src/pi/turn/index.ts +20 -0
- package/src/pi/turn/interaction.ts +181 -0
- package/src/pi/turn/tool-recovery.ts +244 -1
- package/src/runtime-agent-context.ts +112 -0
- package/src/runtime-agent.ts +568 -321
- package/src/runtime-assembler.ts +797 -0
- package/src/runtime-definition.ts +175 -0
- package/src/runtime.ts +840 -208
- package/src/tool-registry.ts +143 -0
- package/src/workspace-versioning.ts +46 -0
- package/src/plugins.ts +0 -1033
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { ToolResultMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 表示一个 Pi Tool 在等待客户端投递结果时的持久 park 记录。
|
|
5
|
+
*
|
|
6
|
+
* Runtime 在带 `interaction` 声明的 Tool 进入执行链时保存它,恢复路径按 `interactionId` 读取并应用一次响应。
|
|
7
|
+
*
|
|
8
|
+
* Tool 输入随记录一起保存,才能在 Durable Object 重建后把响应映射回原始调用,而不是重新推断它。
|
|
9
|
+
*/
|
|
10
|
+
export interface PiToolInteraction {
|
|
11
|
+
interactionId: string;
|
|
12
|
+
requestId: string;
|
|
13
|
+
toolCallId: string;
|
|
14
|
+
toolName: string;
|
|
15
|
+
inputJson: string;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
status: "pending" | "responded" | "cancelled";
|
|
18
|
+
respondedAt?: number;
|
|
19
|
+
responseJson?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 说明一次 interaction 为什么在没有拿到用户选择的情况下结束。
|
|
24
|
+
*
|
|
25
|
+
* 目前只有一种:用户没有点选项,而是直接在聊天里发了消息。
|
|
26
|
+
* 新增成员时必须同步更新 `cancelText` 与解码器的枚举校验。
|
|
27
|
+
*/
|
|
28
|
+
export type PiToolInteractionCancelReason =
|
|
29
|
+
| "user_replied_freeform"
|
|
30
|
+
| "submission_terminal";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 描述一次 interaction 状态转换要求调用方采取的动作。
|
|
34
|
+
*
|
|
35
|
+
* `park` 表示继续等待,`settle` 表示已得到权威 ToolResult 可以续跑,`noop` 表示重复投递应被忽略。
|
|
36
|
+
*
|
|
37
|
+
* 状态转换与 I/O 分开,使同一套幂等规则同时服务在线执行和崩溃恢复 —— 与审批同构。
|
|
38
|
+
*/
|
|
39
|
+
export type PiToolInteractionOutcome =
|
|
40
|
+
| { kind: "park"; interactionId: string; requestId: string }
|
|
41
|
+
| {
|
|
42
|
+
kind: "settle";
|
|
43
|
+
interactionId: string;
|
|
44
|
+
requestId: string;
|
|
45
|
+
toolCallId: string;
|
|
46
|
+
toolResult: ToolResultMessage;
|
|
47
|
+
}
|
|
48
|
+
| { kind: "noop"; interactionId: string };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 把 interaction 的新状态和对应动作绑定为一个不可拆分的结果。
|
|
52
|
+
*
|
|
53
|
+
* 在线执行与恢复调用方应同时消费 `interaction` 和 `outcome`,不要只持久化其中一半。
|
|
54
|
+
*/
|
|
55
|
+
export interface PiToolInteractionTransition {
|
|
56
|
+
interaction: PiToolInteraction;
|
|
57
|
+
outcome: PiToolInteractionOutcome;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 用户改为自由发言时写进 ToolResult 的提示语。
|
|
62
|
+
*
|
|
63
|
+
* 刻意**不包含用户原文** —— 紧随其后的那条用户消息里就有,复制进来模型会读到两遍。
|
|
64
|
+
* 这段文字是模型判断「那句话算不算答案」的唯一依据,改动前先想清楚它会怎么读。
|
|
65
|
+
*/
|
|
66
|
+
const CANCEL_TEXT: Record<PiToolInteractionCancelReason, string> = {
|
|
67
|
+
user_replied_freeform:
|
|
68
|
+
"The user did not pick an option. They replied in the chat instead — " +
|
|
69
|
+
"read their next message and treat it as the answer if it addresses " +
|
|
70
|
+
"the question; otherwise follow whatever they actually asked for.",
|
|
71
|
+
submission_terminal:
|
|
72
|
+
"The turn ended before the user answered. Do not assume any option was chosen.",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 创建一条待响应的 interaction,并告诉调用方暂停当前 Tool。
|
|
77
|
+
*
|
|
78
|
+
* 执行适配器确认 Tool 声明了 `interaction` 后调用它,再把返回的记录持久化并等待客户端投递。
|
|
79
|
+
*
|
|
80
|
+
* 此处不填写响应时间或响应体,保证新记录只有一个明确的 `pending` 起点。
|
|
81
|
+
*/
|
|
82
|
+
export function parkPiToolInteraction(
|
|
83
|
+
input: Omit<
|
|
84
|
+
PiToolInteraction,
|
|
85
|
+
"status" | "respondedAt" | "responseJson"
|
|
86
|
+
>,
|
|
87
|
+
): PiToolInteractionTransition {
|
|
88
|
+
return {
|
|
89
|
+
interaction: { ...input, status: "pending" },
|
|
90
|
+
outcome: {
|
|
91
|
+
kind: "park",
|
|
92
|
+
interactionId: input.interactionId,
|
|
93
|
+
requestId: input.requestId,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 把客户端的首次响应应用到待响应记录。
|
|
100
|
+
*
|
|
101
|
+
* 在线 RPC 与恢复路径在读到持久记录后调用它;已经结束的记录返回 `noop`,调用方不得再次结算 Tool。
|
|
102
|
+
*
|
|
103
|
+
* `toolResult` 由调用方按 Tool 自己的 `settle` 映射产出 —— 本函数不解释响应体语义,只负责状态单调前进。
|
|
104
|
+
*/
|
|
105
|
+
export function respondPiToolInteraction(
|
|
106
|
+
interaction: PiToolInteraction,
|
|
107
|
+
input: {
|
|
108
|
+
response: unknown;
|
|
109
|
+
toolResult: ToolResultMessage;
|
|
110
|
+
respondedAt: number;
|
|
111
|
+
},
|
|
112
|
+
): PiToolInteractionTransition {
|
|
113
|
+
if (interaction.status !== "pending") {
|
|
114
|
+
return {
|
|
115
|
+
interaction,
|
|
116
|
+
outcome: { kind: "noop", interactionId: interaction.interactionId },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
interaction: {
|
|
121
|
+
...interaction,
|
|
122
|
+
status: "responded",
|
|
123
|
+
respondedAt: input.respondedAt,
|
|
124
|
+
responseJson: JSON.stringify(input.response),
|
|
125
|
+
},
|
|
126
|
+
outcome: {
|
|
127
|
+
kind: "settle",
|
|
128
|
+
interactionId: interaction.interactionId,
|
|
129
|
+
requestId: interaction.requestId,
|
|
130
|
+
toolCallId: interaction.toolCallId,
|
|
131
|
+
toolResult: input.toolResult,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 在用户没有作答的情况下结束一条 interaction。
|
|
138
|
+
*
|
|
139
|
+
* `dispatchMessage` 发现 park 期间来了用户消息时调用,Submission 进终态收尾时也调用。
|
|
140
|
+
*
|
|
141
|
+
* 取消同样产出 `isError: false` 的稳定 ToolResult —— 它不是 Tool 失败,而是一个「没选」的已处理结果;
|
|
142
|
+
* 改成异常会让模型收到 tool_error 并倾向于重试,那恰恰是我们不想要的。
|
|
143
|
+
*/
|
|
144
|
+
export function cancelPiToolInteraction(
|
|
145
|
+
interaction: PiToolInteraction,
|
|
146
|
+
input: {
|
|
147
|
+
reason: PiToolInteractionCancelReason;
|
|
148
|
+
cancelledAt: number;
|
|
149
|
+
toolResult?: ToolResultMessage;
|
|
150
|
+
},
|
|
151
|
+
): PiToolInteractionTransition {
|
|
152
|
+
if (interaction.status !== "pending") {
|
|
153
|
+
return {
|
|
154
|
+
interaction,
|
|
155
|
+
outcome: { kind: "noop", interactionId: interaction.interactionId },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const toolResult: ToolResultMessage = input.toolResult ?? {
|
|
159
|
+
role: "toolResult",
|
|
160
|
+
toolCallId: interaction.toolCallId,
|
|
161
|
+
toolName: interaction.toolName,
|
|
162
|
+
content: [{ type: "text", text: CANCEL_TEXT[input.reason] }],
|
|
163
|
+
details: { cancelled: true, reason: input.reason },
|
|
164
|
+
isError: false,
|
|
165
|
+
timestamp: input.cancelledAt,
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
interaction: {
|
|
169
|
+
...interaction,
|
|
170
|
+
status: "cancelled",
|
|
171
|
+
respondedAt: input.cancelledAt,
|
|
172
|
+
},
|
|
173
|
+
outcome: {
|
|
174
|
+
kind: "settle",
|
|
175
|
+
interactionId: interaction.interactionId,
|
|
176
|
+
requestId: interaction.requestId,
|
|
177
|
+
toolCallId: interaction.toolCallId,
|
|
178
|
+
toolResult,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -5,6 +5,13 @@ import {
|
|
|
5
5
|
type PiApprovalOutcome,
|
|
6
6
|
type PiToolApproval,
|
|
7
7
|
} from "./approval";
|
|
8
|
+
import {
|
|
9
|
+
cancelPiToolInteraction,
|
|
10
|
+
respondPiToolInteraction,
|
|
11
|
+
type PiToolInteraction,
|
|
12
|
+
type PiToolInteractionCancelReason,
|
|
13
|
+
type PiToolInteractionOutcome,
|
|
14
|
+
} from "./interaction";
|
|
8
15
|
|
|
9
16
|
/**
|
|
10
17
|
* 本文件实现 Pi Tool、审批、续跑和终态里程碑的纯恢复状态机。
|
|
@@ -52,12 +59,28 @@ export type PiToolRecoveryMilestone =
|
|
|
52
59
|
type: "approval";
|
|
53
60
|
approval: PiToolApproval;
|
|
54
61
|
})
|
|
62
|
+
| (RecoveryIdentity & {
|
|
63
|
+
type: "interaction";
|
|
64
|
+
interaction: PiToolInteraction;
|
|
65
|
+
})
|
|
55
66
|
| (RecoveryIdentity & {
|
|
56
67
|
type: "continuation";
|
|
57
68
|
continuationKey: string;
|
|
58
69
|
phase: "pending" | "committed";
|
|
59
70
|
timestamp: number;
|
|
60
71
|
})
|
|
72
|
+
/**
|
|
73
|
+
* 一次被承认的装配迁移:这条 Submission 停在人机等待期间换了装配。
|
|
74
|
+
*
|
|
75
|
+
* `assemblyRevision` 是迁移前那份(所以它自己通过身份校验),
|
|
76
|
+
* `nextAssemblyRevision` 是迁移后那份。重放读到它之前按旧 revision 校验
|
|
77
|
+
* 身份,读到之后按新的。同一条 Submission 允许发生多次。
|
|
78
|
+
*/
|
|
79
|
+
| (RecoveryIdentity & {
|
|
80
|
+
type: "assembly-repin";
|
|
81
|
+
nextAssemblyRevision: string;
|
|
82
|
+
timestamp: number;
|
|
83
|
+
})
|
|
61
84
|
| (RecoveryIdentity & {
|
|
62
85
|
type: "terminal";
|
|
63
86
|
phase: "intent" | "committed";
|
|
@@ -93,6 +116,7 @@ export interface PiToolRecoveryState {
|
|
|
93
116
|
assemblyRevision?: string;
|
|
94
117
|
toolCalls: Readonly<Record<string, RecoveredToolCall>>;
|
|
95
118
|
approvals: Readonly<Record<string, PiToolApproval>>;
|
|
119
|
+
interactions: Readonly<Record<string, PiToolInteraction>>;
|
|
96
120
|
continuations: Readonly<
|
|
97
121
|
Record<string, "pending" | "committed">
|
|
98
122
|
>;
|
|
@@ -113,6 +137,11 @@ export type PiToolRecoveryPlan =
|
|
|
113
137
|
executionId: string;
|
|
114
138
|
chargeRecoveryBudget: false;
|
|
115
139
|
}
|
|
140
|
+
| {
|
|
141
|
+
kind: "parked-interaction";
|
|
142
|
+
interactionId: string;
|
|
143
|
+
chargeRecoveryBudget: false;
|
|
144
|
+
}
|
|
116
145
|
| {
|
|
117
146
|
kind: "retry-tool";
|
|
118
147
|
toolCallId: string;
|
|
@@ -168,6 +197,19 @@ export interface RecoveredPiApprovalDecision {
|
|
|
168
197
|
outcome: PiApprovalOutcome;
|
|
169
198
|
}
|
|
170
199
|
|
|
200
|
+
/**
|
|
201
|
+
* 表示恢复路径应用一次 interaction 响应或取消后需要追加的里程碑和动作。
|
|
202
|
+
*
|
|
203
|
+
* Runtime 收到 respond 或 cancel 命令时同时持久化 `milestones` 并处理 `outcome`。
|
|
204
|
+
*
|
|
205
|
+
* 与审批不同的是,settle 分支的 `tool-result` 里程碑带 `needsContinuation: true`:
|
|
206
|
+
* interaction 没有自己的续跑键,续跑完全依赖既有的 `tool:<id>:settled` 路径。
|
|
207
|
+
*/
|
|
208
|
+
export interface RecoveredPiToolInteractionSettlement {
|
|
209
|
+
milestones: PiToolRecoveryMilestone[];
|
|
210
|
+
outcome: PiToolInteractionOutcome;
|
|
211
|
+
}
|
|
212
|
+
|
|
171
213
|
// #endregion
|
|
172
214
|
|
|
173
215
|
// #region 里程碑编码与校验
|
|
@@ -256,6 +298,28 @@ function isApproval(value: unknown): value is PiToolApproval {
|
|
|
256
298
|
);
|
|
257
299
|
}
|
|
258
300
|
|
|
301
|
+
// 校验从持久 JSON 读出的值是否是完整的 Pi Tool interaction 记录。
|
|
302
|
+
// 里程碑解码器处理 interaction 事件时调用它。
|
|
303
|
+
// 状态采用封闭枚举,未知协议值会让本次重放停在可信前缀 —— 与 isApproval 同一口径。
|
|
304
|
+
function isInteraction(value: unknown): value is PiToolInteraction {
|
|
305
|
+
return (
|
|
306
|
+
isRecord(value) &&
|
|
307
|
+
typeof value.interactionId === "string" &&
|
|
308
|
+
typeof value.requestId === "string" &&
|
|
309
|
+
typeof value.toolCallId === "string" &&
|
|
310
|
+
typeof value.toolName === "string" &&
|
|
311
|
+
typeof value.inputJson === "string" &&
|
|
312
|
+
typeof value.createdAt === "number" &&
|
|
313
|
+
(value.respondedAt === undefined ||
|
|
314
|
+
typeof value.respondedAt === "number") &&
|
|
315
|
+
(value.responseJson === undefined ||
|
|
316
|
+
typeof value.responseJson === "string") &&
|
|
317
|
+
(value.status === "pending" ||
|
|
318
|
+
value.status === "responded" ||
|
|
319
|
+
value.status === "cancelled")
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
259
323
|
/**
|
|
260
324
|
* 把一个 Pi Tool 恢复里程碑编码成可持久化的 JSON 正文。
|
|
261
325
|
*
|
|
@@ -315,6 +379,10 @@ export function decodePiToolRecoveryMilestone(
|
|
|
315
379
|
return isApproval(value.approval)
|
|
316
380
|
? (value as PiToolRecoveryMilestone)
|
|
317
381
|
: null;
|
|
382
|
+
case "interaction":
|
|
383
|
+
return isInteraction(value.interaction)
|
|
384
|
+
? (value as PiToolRecoveryMilestone)
|
|
385
|
+
: null;
|
|
318
386
|
case "continuation":
|
|
319
387
|
return typeof value.continuationKey === "string" &&
|
|
320
388
|
(value.phase === "pending" ||
|
|
@@ -322,6 +390,12 @@ export function decodePiToolRecoveryMilestone(
|
|
|
322
390
|
typeof value.timestamp === "number"
|
|
323
391
|
? (value as PiToolRecoveryMilestone)
|
|
324
392
|
: null;
|
|
393
|
+
case "assembly-repin":
|
|
394
|
+
return typeof value.nextAssemblyRevision === "string" &&
|
|
395
|
+
value.nextAssemblyRevision.length > 0 &&
|
|
396
|
+
typeof value.timestamp === "number"
|
|
397
|
+
? (value as PiToolRecoveryMilestone)
|
|
398
|
+
: null;
|
|
325
399
|
case "terminal":
|
|
326
400
|
return (value.phase === "intent" ||
|
|
327
401
|
value.phase === "committed") &&
|
|
@@ -390,6 +464,7 @@ export function replayPiToolRecovery(
|
|
|
390
464
|
): PiToolRecoveryState {
|
|
391
465
|
const tools: Record<string, RecoveredToolCall> = {};
|
|
392
466
|
const approvals: Record<string, PiToolApproval> = {};
|
|
467
|
+
const interactions: Record<string, PiToolInteraction> = {};
|
|
393
468
|
const continuations: Record<
|
|
394
469
|
string,
|
|
395
470
|
"pending" | "committed"
|
|
@@ -398,6 +473,7 @@ export function replayPiToolRecovery(
|
|
|
398
473
|
let state: PiToolRecoveryState = {
|
|
399
474
|
toolCalls: tools,
|
|
400
475
|
approvals,
|
|
476
|
+
interactions,
|
|
401
477
|
continuations,
|
|
402
478
|
canonicalToolResults: results,
|
|
403
479
|
};
|
|
@@ -488,6 +564,27 @@ export function replayPiToolRecovery(
|
|
|
488
564
|
}
|
|
489
565
|
break;
|
|
490
566
|
}
|
|
567
|
+
case "interaction": {
|
|
568
|
+
const previous = interactions[milestone.interaction.interactionId];
|
|
569
|
+
// 与 approval 同一口径:pending 可以前进到一个结局,但已结局的记录不能被翻转。
|
|
570
|
+
// 同状态重复记录允许刷新完整持久快照,同时保持结果不变。
|
|
571
|
+
if (
|
|
572
|
+
!previous ||
|
|
573
|
+
previous.status === "pending" ||
|
|
574
|
+
previous.status === milestone.interaction.status
|
|
575
|
+
) {
|
|
576
|
+
interactions[milestone.interaction.interactionId] =
|
|
577
|
+
milestone.interaction;
|
|
578
|
+
}
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
case "assembly-repin":
|
|
582
|
+
// 身份校验已经按迁移前的 revision 通过;从这里往后的里程碑属于新装配。
|
|
583
|
+
state = {
|
|
584
|
+
...state,
|
|
585
|
+
assemblyRevision: milestone.nextAssemblyRevision,
|
|
586
|
+
};
|
|
587
|
+
break;
|
|
491
588
|
case "continuation":
|
|
492
589
|
// committed 是单调终态,迟到的 pending 记录不能让续跑再次被调度。
|
|
493
590
|
if (
|
|
@@ -529,6 +626,7 @@ export function replayPiToolRecovery(
|
|
|
529
626
|
...state,
|
|
530
627
|
toolCalls: tools,
|
|
531
628
|
approvals,
|
|
629
|
+
interactions,
|
|
532
630
|
continuations,
|
|
533
631
|
canonicalToolResults: results,
|
|
534
632
|
};
|
|
@@ -574,7 +672,8 @@ function continuationPlan(
|
|
|
574
672
|
*
|
|
575
673
|
* Runtime 恢复适配器在每次状态变化后调用它,并只执行返回计划对应的一个动作。
|
|
576
674
|
*
|
|
577
|
-
*
|
|
675
|
+
* 优先级固定为终态、已决定审批、待审批、待响应 interaction、已结算 Tool、未结算 Tool,
|
|
676
|
+
* 不能调换到越过终态或用户许可。
|
|
578
677
|
*/
|
|
579
678
|
export function planPiToolRecovery(
|
|
580
679
|
state: PiToolRecoveryState,
|
|
@@ -619,6 +718,18 @@ export function planPiToolRecovery(
|
|
|
619
718
|
chargeRecoveryBudget: false,
|
|
620
719
|
};
|
|
621
720
|
}
|
|
721
|
+
// 仍等客户端投递结果的 interaction 同样保持 parked。
|
|
722
|
+
// 已响应或已取消的 interaction 不在这里出现:它们在写入本记录的同一批里程碑中
|
|
723
|
+
// 一并写了 tool-result,因此直接落到下面「已结算 Tool 补齐续跑」那条既有路径,
|
|
724
|
+
// 不需要 interaction 专属的续跑键。
|
|
725
|
+
for (const interaction of Object.values(state.interactions)) {
|
|
726
|
+
if (interaction.status !== "pending") continue;
|
|
727
|
+
return {
|
|
728
|
+
kind: "parked-interaction",
|
|
729
|
+
interactionId: interaction.interactionId,
|
|
730
|
+
chargeRecoveryBudget: false,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
622
733
|
|
|
623
734
|
// 已结算 Tool 先补齐续跑;未结算 Tool 只有声明幂等时才自动重试。
|
|
624
735
|
for (const tool of Object.values(state.toolCalls)) {
|
|
@@ -724,6 +835,42 @@ export function commitPiRecoveryContinuation(
|
|
|
724
835
|
|
|
725
836
|
// #endregion
|
|
726
837
|
|
|
838
|
+
// #region 装配迁移
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* 为一次停在人机等待期间发生的装配迁移创建里程碑。
|
|
842
|
+
*
|
|
843
|
+
* 宿主换完装配、准备把 Submission 重新 pin 到新装配上时调用它;返回的里程碑
|
|
844
|
+
* 必须与 `pinAssembly` 的写入落在同一个事务里,否则重启后里程碑与 Submission
|
|
845
|
+
* 会各自指向一半的迁移。
|
|
846
|
+
*
|
|
847
|
+
* 已经处在目标 revision(重复调用)或还没有任何里程碑(没有身份要迁移)时返回
|
|
848
|
+
* null:这两种情况下重放本来就得到正确的身份,多写一条只会造出假历史。
|
|
849
|
+
*/
|
|
850
|
+
export function stagePiAssemblyRepin(
|
|
851
|
+
state: PiToolRecoveryState,
|
|
852
|
+
nextAssemblyRevision: string,
|
|
853
|
+
timestamp: number,
|
|
854
|
+
): PiToolRecoveryMilestone | null {
|
|
855
|
+
if (
|
|
856
|
+
!state.turnId ||
|
|
857
|
+
!state.assemblyRevision ||
|
|
858
|
+
state.assemblyRevision === nextAssemblyRevision
|
|
859
|
+
) {
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
return {
|
|
863
|
+
version: 1,
|
|
864
|
+
turnId: state.turnId,
|
|
865
|
+
assemblyRevision: state.assemblyRevision,
|
|
866
|
+
type: "assembly-repin",
|
|
867
|
+
nextAssemblyRevision,
|
|
868
|
+
timestamp,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// #endregion
|
|
873
|
+
|
|
727
874
|
// #region 恢复审批决定
|
|
728
875
|
|
|
729
876
|
/**
|
|
@@ -790,3 +937,99 @@ export function applyRecoveredPiApprovalDecision(
|
|
|
790
937
|
}
|
|
791
938
|
|
|
792
939
|
// #endregion
|
|
940
|
+
|
|
941
|
+
// #region 恢复 interaction 结算
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* 在恢复状态上记录一条新 park 的 interaction。
|
|
945
|
+
*
|
|
946
|
+
* 执行链进入 park 前调用,返回的里程碑让重启后的重放知道这次 Tool 调用在等客户端,
|
|
947
|
+
* 从而规划成 `parked-interaction` 而不是去重试 Tool。
|
|
948
|
+
*/
|
|
949
|
+
export function recordRecoveredPiToolInteraction(
|
|
950
|
+
state: PiToolRecoveryState,
|
|
951
|
+
interaction: PiToolInteraction,
|
|
952
|
+
): PiToolRecoveryMilestone[] {
|
|
953
|
+
if (!state.turnId || !state.assemblyRevision) {
|
|
954
|
+
throw new Error("Pi Tool interaction is missing its Turn revision");
|
|
955
|
+
}
|
|
956
|
+
return [{
|
|
957
|
+
version: 1,
|
|
958
|
+
turnId: state.turnId,
|
|
959
|
+
assemblyRevision: state.assemblyRevision,
|
|
960
|
+
type: "interaction",
|
|
961
|
+
interaction,
|
|
962
|
+
}];
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* 在恢复状态上应用一次客户端响应或取消,并生成需要持久化的里程碑。
|
|
967
|
+
*
|
|
968
|
+
* Runtime 收到 `respondToolInteraction` 或判定取消时调用它,再按返回结果更新表和恢复日志。
|
|
969
|
+
*
|
|
970
|
+
* 两种结局形状相同 —— 都写 interaction 记录 + 一条权威 `tool-result`。重复投递返回空里程碑。
|
|
971
|
+
*/
|
|
972
|
+
export function applyRecoveredPiToolInteractionSettlement(
|
|
973
|
+
state: PiToolRecoveryState,
|
|
974
|
+
input:
|
|
975
|
+
| {
|
|
976
|
+
interactionId: string;
|
|
977
|
+
kind: "respond";
|
|
978
|
+
response: unknown;
|
|
979
|
+
toolResult: ToolResultMessage;
|
|
980
|
+
settledAt: number;
|
|
981
|
+
}
|
|
982
|
+
| {
|
|
983
|
+
interactionId: string;
|
|
984
|
+
kind: "cancel";
|
|
985
|
+
reason: PiToolInteractionCancelReason;
|
|
986
|
+
settledAt: number;
|
|
987
|
+
toolResult?: ToolResultMessage;
|
|
988
|
+
},
|
|
989
|
+
): RecoveredPiToolInteractionSettlement {
|
|
990
|
+
const interaction = state.interactions[input.interactionId];
|
|
991
|
+
if (!interaction) {
|
|
992
|
+
throw new Error(`Unknown Pi Tool interaction ${input.interactionId}`);
|
|
993
|
+
}
|
|
994
|
+
const transition = input.kind === "respond"
|
|
995
|
+
? respondPiToolInteraction(interaction, {
|
|
996
|
+
response: input.response,
|
|
997
|
+
toolResult: input.toolResult,
|
|
998
|
+
respondedAt: input.settledAt,
|
|
999
|
+
})
|
|
1000
|
+
: cancelPiToolInteraction(interaction, {
|
|
1001
|
+
reason: input.reason,
|
|
1002
|
+
cancelledAt: input.settledAt,
|
|
1003
|
+
...(input.toolResult ? { toolResult: input.toolResult } : {}),
|
|
1004
|
+
});
|
|
1005
|
+
const outcome = transition.outcome;
|
|
1006
|
+
// 已结束的 interaction 只返回 noop;这里不应再追加任何持久记录。
|
|
1007
|
+
if (outcome.kind !== "settle") {
|
|
1008
|
+
return { milestones: [], outcome };
|
|
1009
|
+
}
|
|
1010
|
+
if (!state.turnId || !state.assemblyRevision) {
|
|
1011
|
+
throw new Error("Pi Tool interaction is missing its Turn revision");
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const identity: RecoveryIdentity = {
|
|
1015
|
+
version: 1,
|
|
1016
|
+
turnId: state.turnId,
|
|
1017
|
+
assemblyRevision: state.assemblyRevision,
|
|
1018
|
+
};
|
|
1019
|
+
return {
|
|
1020
|
+
milestones: [
|
|
1021
|
+
{ ...identity, type: "interaction", interaction: transition.interaction },
|
|
1022
|
+
// needsContinuation 必须为 true:interaction 没有自己的续跑键,
|
|
1023
|
+
// 唤醒原 Turn 全靠 planPiToolRecovery 里 `tool:<id>:settled` 那条既有分支。
|
|
1024
|
+
{
|
|
1025
|
+
...identity,
|
|
1026
|
+
type: "tool-result",
|
|
1027
|
+
toolResult: outcome.toolResult,
|
|
1028
|
+
needsContinuation: true,
|
|
1029
|
+
},
|
|
1030
|
+
],
|
|
1031
|
+
outcome,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// #endregion
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolRunStatus,
|
|
3
|
+
ChatCapableAgentClass,
|
|
4
|
+
RunAgentToolOptions,
|
|
5
|
+
} from "agents";
|
|
6
|
+
import type { ExecutionLevel } from "./lib/execution-level";
|
|
7
|
+
import type { RuntimeConfigUpdateResult } from "./kernel/runtime-config";
|
|
8
|
+
import type {
|
|
9
|
+
TemporaryAgentRequest,
|
|
10
|
+
TemporaryAgentRunContext,
|
|
11
|
+
} from "./layers/orchestration/temporary-agent/core";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 描述 Agent 寻址路径中的一级父子节点。
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Cloudflare Agents SDK 用 `parentPath` 暴露根节点到直接父节点的路径。
|
|
18
|
+
*
|
|
19
|
+
* 调用方通常只读取,不应自行构造后写回 SDK。
|
|
20
|
+
*/
|
|
21
|
+
export interface RuntimeAgentPathStep {
|
|
22
|
+
className: string;
|
|
23
|
+
name: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 表示一次 Agent Tool 执行返回给 Runtime 的稳定字段。
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Subagent 端口在调用 `RuntimeAgentConfigContext.runAgentTool` 后读取它。
|
|
31
|
+
*
|
|
32
|
+
* 这里只保留 Runtime 需要的结果,避免把 SDK 内部运行对象暴露给应用。
|
|
33
|
+
*/
|
|
34
|
+
export interface RuntimeAgentToolResult {
|
|
35
|
+
status: string;
|
|
36
|
+
runId: string;
|
|
37
|
+
output?: unknown;
|
|
38
|
+
summary?: string;
|
|
39
|
+
error?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type RuntimeAgentRole = "primary" | "temporary";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 这是应用创建 Config 时唯一可以接触的 Agent 实例能力。
|
|
46
|
+
*
|
|
47
|
+
* 身份通过实时 getter 暴露。
|
|
48
|
+
*/
|
|
49
|
+
export interface RuntimeAgentConfigContext<
|
|
50
|
+
Env extends Cloudflare.Env = Cloudflare.Env,
|
|
51
|
+
> {
|
|
52
|
+
/** 读取当前 Durable Object 的状态句柄。 */
|
|
53
|
+
readonly ctx: DurableObjectState;
|
|
54
|
+
/** 读取当前 Worker 的环境绑定。 */
|
|
55
|
+
readonly env: Env;
|
|
56
|
+
/** 读取当前 Agent 实例名称。 */
|
|
57
|
+
readonly name: string;
|
|
58
|
+
/** 读取从根节点到直接父节点的 Agent 路径。 */
|
|
59
|
+
readonly parentPath: readonly RuntimeAgentPathStep[];
|
|
60
|
+
/** Runtime 根据 Agent facet 父子关系自动判定,不是外部配置项。 */
|
|
61
|
+
readonly role: RuntimeAgentRole;
|
|
62
|
+
/**
|
|
63
|
+
* 通过当前 Agent 调用一个受 SDK 管理的子 Agent Tool。
|
|
64
|
+
*
|
|
65
|
+
* @remarks
|
|
66
|
+
* subagent 端口在父 Agent 需要保留子运行状态、事件和取消边界时调用。
|
|
67
|
+
*/
|
|
68
|
+
runAgentTool(
|
|
69
|
+
agentClass: ChatCapableAgentClass,
|
|
70
|
+
options: RunAgentToolOptions<unknown>,
|
|
71
|
+
): Promise<RuntimeAgentToolResult>;
|
|
72
|
+
/**
|
|
73
|
+
* 清理符合年龄和状态条件的 SDK 子运行记录。
|
|
74
|
+
*/
|
|
75
|
+
clearAgentToolRuns(options: {
|
|
76
|
+
olderThan: number;
|
|
77
|
+
status: AgentToolRunStatus[];
|
|
78
|
+
}): Promise<void>;
|
|
79
|
+
/** 读取当前已装配 User Agent 的执行档位。 */
|
|
80
|
+
executionLevel(): ExecutionLevel;
|
|
81
|
+
/**
|
|
82
|
+
* 在当前 Session Runtime 中执行一个仅对本次调用存活的临时 Agent。
|
|
83
|
+
*/
|
|
84
|
+
runTemporaryAgent(
|
|
85
|
+
request: TemporaryAgentRequest,
|
|
86
|
+
context: TemporaryAgentRunContext,
|
|
87
|
+
): Promise<string>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface RuntimeAgentPlanningContext<
|
|
91
|
+
Env extends Cloudflare.Env = Cloudflare.Env,
|
|
92
|
+
Command = never,
|
|
93
|
+
Change = never,
|
|
94
|
+
> extends RuntimeAgentConfigContext<Env> {
|
|
95
|
+
/** 仅可变 Definition 提供;Host Tool 用它复用生成类的更新与重载编排。 */
|
|
96
|
+
readonly updateConfig?: (
|
|
97
|
+
command: Command,
|
|
98
|
+
) => Promise<RuntimeConfigUpdateResult<Change>>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 装配、Hook 与 Port 准备共用的 Agent 上下文。 */
|
|
102
|
+
export type RuntimeAssemblyContext<
|
|
103
|
+
Env extends Cloudflare.Env = Cloudflare.Env,
|
|
104
|
+
Command = never,
|
|
105
|
+
Change = never,
|
|
106
|
+
> = RuntimeAgentPlanningContext<Env, Command, Change>;
|
|
107
|
+
|
|
108
|
+
export type RuntimeAgentContext<
|
|
109
|
+
Env extends Cloudflare.Env = Cloudflare.Env,
|
|
110
|
+
Command = never,
|
|
111
|
+
Change = never,
|
|
112
|
+
> = RuntimeAgentPlanningContext<Env, Command, Change>;
|