@springbrand/agent-runtime 0.1.3-alpha.3 → 0.1.3-alpha.4
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 +1 -1
- 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 +15 -0
- package/src/db/submission.repo.ts +29 -0
- package/src/index.ts +7 -17
- package/src/kernel/approval-lifecycle.ts +41 -6
- package/src/kernel/bindings.ts +37 -0
- package/src/kernel/interaction-lifecycle.ts +395 -0
- package/src/kernel/public-contracts.ts +2 -0
- package/src/kernel/recoverable-chat-agent.ts +10 -2
- 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 +102 -0
- package/src/kernel/state.ts +8 -1
- package/src/kernel/submission-lifecycle.ts +30 -0
- package/src/pi/runtime-adapter/assembly.ts +13 -1
- package/src/pi/runtime-adapter/execution.ts +109 -9
- package/src/pi/runtime-adapter/index.ts +9 -3
- package/src/pi/runtime-adapter/recovery.ts +188 -1
- package/src/pi/tool/base.ts +62 -9
- package/src/pi/tool/compiler.ts +34 -0
- package/src/pi/tool/gateway.ts +54 -0
- package/src/pi/tool/index.ts +1 -0
- package/src/pi/tool/mcp.ts +93 -64
- 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.ts +246 -113
- package/src/{plugins.ts → runtime-assembler.ts} +65 -282
- package/src/runtime.ts +440 -159
package/src/pi/tool/compiler.ts
CHANGED
|
@@ -13,14 +13,48 @@ import {
|
|
|
13
13
|
|
|
14
14
|
// #region Public contracts
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* 声明一个 Tool 的结果由客户端投递,而不是由 `execute()` 产出。
|
|
18
|
+
*
|
|
19
|
+
* 带这个字段的 Tool 会在执行链里 park 住,等宿主经 `respondToolInteraction` 把响应送进来,
|
|
20
|
+
* 再用 `settle` 把响应映射成这次 `toolCallId` 的权威 ToolResult。它的 `execute` 不会被调用。
|
|
21
|
+
*
|
|
22
|
+
* 这是 AI SDK 里「没有 execute 的 user-interaction tool」的等价物;差别是我们 park 住整个 Turn,
|
|
23
|
+
* 而不是让 tool part 悬空到下一次请求 —— 我们的 transcript 是服务端权威的,悬空的 tool_use
|
|
24
|
+
* 会让下一次模型调用非法。
|
|
25
|
+
*/
|
|
26
|
+
export interface PiToolInteractionSpec {
|
|
27
|
+
/** 校验客户端响应体;不通过则拒绝投递,park 保持不变。 */
|
|
28
|
+
readonly validateResponse: (response: unknown) => boolean;
|
|
29
|
+
/** 把通过校验的响应映射成 ToolResult;缺省是原样回传。 */
|
|
30
|
+
readonly settle?: (
|
|
31
|
+
input: unknown,
|
|
32
|
+
response: unknown,
|
|
33
|
+
) => AgentToolResult<unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
16
36
|
/** 描述一个尚未进入最终 Tool Surface 和结算包装的 Pi 工具。 */
|
|
17
37
|
export interface PiToolCandidate {
|
|
18
38
|
readonly owner: string;
|
|
19
39
|
readonly authorized: boolean;
|
|
20
40
|
readonly tool: AgentTool<any, any>;
|
|
41
|
+
/** Conservative maximum used in the stable Runtime descriptor. */
|
|
21
42
|
readonly requiredExecutionLevel: ExecutionLevel;
|
|
43
|
+
/** Trusted parameter-level policy, evaluated before approval or dispatch. */
|
|
44
|
+
readonly requiredExecutionLevelForInput?: (
|
|
45
|
+
input: unknown,
|
|
46
|
+
) => ExecutionLevel | Promise<ExecutionLevel>;
|
|
47
|
+
/**
|
|
48
|
+
* 无条件需要人来决定:不看执行档位矩阵,每次调用都 park 出审批。
|
|
49
|
+
*
|
|
50
|
+
* 档位差是「够不够格自己跑」,不是「该不该问人」。花用户钱、改变账户
|
|
51
|
+
* 状态这类工具,一旦 Agent 档位被 `allow_level` 永久拉高就会静默执行,
|
|
52
|
+
* 那是失效不是降级。`deny` 仍然优先,被 deny 的工具根本不会注入。
|
|
53
|
+
*/
|
|
54
|
+
readonly alwaysRequiresApproval?: boolean;
|
|
22
55
|
readonly summary?: string;
|
|
23
56
|
readonly source?: ApprovalReceipt["source"];
|
|
57
|
+
readonly interaction?: PiToolInteractionSpec;
|
|
24
58
|
}
|
|
25
59
|
|
|
26
60
|
/** 控制候选 Pi 工具如何被编译为可执行工具集。 */
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { RuntimeGatewaySession } from "../../kernel/bindings";
|
|
2
|
+
import type { PiToolCandidate } from "./compiler";
|
|
3
|
+
import { createPiMcpToolCandidate } from "./mcp";
|
|
4
|
+
|
|
5
|
+
const GATEWAY_TOOLS = [
|
|
6
|
+
"search_capabilities",
|
|
7
|
+
"execute_capability",
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
/** Builds the two platform Gateway tools from one authenticated logical MCP Session. */
|
|
11
|
+
export function createPiGatewayToolCandidates(
|
|
12
|
+
session: RuntimeGatewaySession,
|
|
13
|
+
): PiToolCandidate[] {
|
|
14
|
+
const tools = new Map(session.tools.map((tool) => [tool.name, tool]));
|
|
15
|
+
if (
|
|
16
|
+
tools.size !== GATEWAY_TOOLS.length ||
|
|
17
|
+
GATEWAY_TOOLS.some((name) => !tools.has(name))
|
|
18
|
+
) {
|
|
19
|
+
throw new Error("Connector Gateway returned an invalid MCP catalog");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return GATEWAY_TOOLS.map((name) => {
|
|
23
|
+
const tool = tools.get(name)!;
|
|
24
|
+
const execute = name === "execute_capability";
|
|
25
|
+
return createPiMcpToolCandidate(
|
|
26
|
+
{ ...tool, serverId: "gateway" },
|
|
27
|
+
(input, signal) => session.callTool(name, input, signal),
|
|
28
|
+
{
|
|
29
|
+
owner: "gateway",
|
|
30
|
+
modelName: name,
|
|
31
|
+
requiredExecutionLevel: execute ? "high" : "safe",
|
|
32
|
+
...(execute
|
|
33
|
+
? {
|
|
34
|
+
requiredExecutionLevelForInput: async (input: unknown) => {
|
|
35
|
+
const reference = input && typeof input === "object" &&
|
|
36
|
+
!Array.isArray(input)
|
|
37
|
+
? (input as Record<string, unknown>).name
|
|
38
|
+
: undefined;
|
|
39
|
+
if (typeof reference !== "string" || !reference) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Gateway Capability Reference is not verified",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return (await session.resolveCapabilityRisk(reference)) ===
|
|
45
|
+
"none"
|
|
46
|
+
? "safe" as const
|
|
47
|
+
: "high" as const;
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
: {}),
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
}
|
package/src/pi/tool/index.ts
CHANGED
package/src/pi/tool/mcp.ts
CHANGED
|
@@ -9,6 +9,22 @@ interface McpCallResult {
|
|
|
9
9
|
isError?: boolean;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export interface PiDiscoveredMcpTool {
|
|
13
|
+
readonly serverId: string;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly title?: string;
|
|
16
|
+
readonly description?: string;
|
|
17
|
+
readonly inputSchema?: unknown;
|
|
18
|
+
readonly annotations?: { readonly title?: string } & Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PiMcpCandidatePolicy {
|
|
22
|
+
readonly owner: string;
|
|
23
|
+
readonly modelName: string;
|
|
24
|
+
readonly requiredExecutionLevel: PiToolCandidate["requiredExecutionLevel"];
|
|
25
|
+
readonly requiredExecutionLevelForInput?: PiToolCandidate["requiredExecutionLevelForInput"];
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
/**
|
|
13
29
|
* Pi 的 MCP 适配器所需的最小 Host 能力。
|
|
14
30
|
*
|
|
@@ -200,6 +216,67 @@ function errorMessage(result: McpCallResult): string {
|
|
|
200
216
|
: "MCP tool call failed";
|
|
201
217
|
}
|
|
202
218
|
|
|
219
|
+
/** 共享 MCP 结果投影;Gateway 与用户配置的 Remote MCP 均经过此边界。 */
|
|
220
|
+
export function createPiMcpToolCandidate(
|
|
221
|
+
mcpTool: PiDiscoveredMcpTool,
|
|
222
|
+
callTool: (
|
|
223
|
+
input: Readonly<Record<string, unknown>>,
|
|
224
|
+
signal?: AbortSignal,
|
|
225
|
+
) => Promise<unknown>,
|
|
226
|
+
policy: PiMcpCandidatePolicy,
|
|
227
|
+
): PiToolCandidate {
|
|
228
|
+
const label =
|
|
229
|
+
mcpTool.title ??
|
|
230
|
+
mcpTool.annotations?.title ??
|
|
231
|
+
mcpTool.name;
|
|
232
|
+
const tool: AgentTool<any, {
|
|
233
|
+
kind: "mcp";
|
|
234
|
+
toolName: string;
|
|
235
|
+
}> = {
|
|
236
|
+
name: policy.modelName,
|
|
237
|
+
label,
|
|
238
|
+
description: mcpTool.description ?? label,
|
|
239
|
+
parameters: structuredClone(
|
|
240
|
+
mcpTool.inputSchema ?? {
|
|
241
|
+
type: "object",
|
|
242
|
+
additionalProperties: true,
|
|
243
|
+
},
|
|
244
|
+
) as AgentTool["parameters"],
|
|
245
|
+
execute: async (_toolCallId, args, signal) => {
|
|
246
|
+
const result = normalizeCallResult(
|
|
247
|
+
await callTool(args as Record<string, unknown>, signal),
|
|
248
|
+
);
|
|
249
|
+
if (result.isError) throw new Error(errorMessage(result));
|
|
250
|
+
const structuredContent = result.structuredContent === undefined
|
|
251
|
+
? undefined
|
|
252
|
+
: publicMcpValue(result.structuredContent);
|
|
253
|
+
const content = resultContent({ ...result, structuredContent });
|
|
254
|
+
return {
|
|
255
|
+
content,
|
|
256
|
+
details: {
|
|
257
|
+
kind: "mcp",
|
|
258
|
+
toolName: mcpTool.name,
|
|
259
|
+
output: structuredContent ?? content,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
owner: policy.owner,
|
|
266
|
+
authorized: true,
|
|
267
|
+
tool,
|
|
268
|
+
summary: label,
|
|
269
|
+
requiredExecutionLevel: policy.requiredExecutionLevel,
|
|
270
|
+
...(policy.requiredExecutionLevelForInput
|
|
271
|
+
? {
|
|
272
|
+
requiredExecutionLevelForInput:
|
|
273
|
+
policy.requiredExecutionLevelForInput,
|
|
274
|
+
}
|
|
275
|
+
: {}),
|
|
276
|
+
source: "action",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
203
280
|
/**
|
|
204
281
|
* 把当前已连接且获授权的 MCP 工具转换成 Pi 候选工具。
|
|
205
282
|
*
|
|
@@ -249,71 +326,23 @@ export function createPiMcpToolCandidates(
|
|
|
249
326
|
state: "ready",
|
|
250
327
|
})
|
|
251
328
|
.filter((tool) => selectedIds.has(tool.serverId))
|
|
252
|
-
.map((mcpTool)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
toolName: string;
|
|
260
|
-
}> = {
|
|
261
|
-
name: modelVisibleName(mcpTool.serverId, mcpTool.name),
|
|
262
|
-
label,
|
|
263
|
-
description: mcpTool.description ?? label,
|
|
264
|
-
parameters: structuredClone(
|
|
265
|
-
mcpTool.inputSchema ?? {
|
|
266
|
-
type: "object",
|
|
267
|
-
additionalProperties: true,
|
|
268
|
-
},
|
|
269
|
-
) as AgentTool["parameters"],
|
|
270
|
-
// 作用:执行模型选中的远端 MCP 工具并返回公开结果。
|
|
271
|
-
// 调用:Pi 在参数校验和统一工具治理通过后调用。
|
|
272
|
-
// 原因:调用交给 Agents SDK,并在边界处统一处理错误、
|
|
273
|
-
// 私有元数据和非原生内容。
|
|
274
|
-
execute: async (_toolCallId, args, signal) => {
|
|
275
|
-
const result = normalizeCallResult(
|
|
276
|
-
await host.mcp.callTool(
|
|
277
|
-
{
|
|
278
|
-
serverId: mcpTool.serverId,
|
|
279
|
-
name: mcpTool.name,
|
|
280
|
-
arguments: args as Record<string, unknown>,
|
|
281
|
-
},
|
|
282
|
-
undefined,
|
|
283
|
-
{ signal },
|
|
284
|
-
),
|
|
285
|
-
);
|
|
286
|
-
if (result.isError) {
|
|
287
|
-
throw new Error(errorMessage(result));
|
|
288
|
-
}
|
|
289
|
-
const structuredContent =
|
|
290
|
-
result.structuredContent === undefined
|
|
291
|
-
? undefined
|
|
292
|
-
: publicMcpValue(result.structuredContent);
|
|
293
|
-
const content = resultContent({
|
|
294
|
-
...result,
|
|
295
|
-
structuredContent,
|
|
296
|
-
});
|
|
297
|
-
return {
|
|
298
|
-
content,
|
|
299
|
-
details: {
|
|
300
|
-
kind: "mcp",
|
|
301
|
-
toolName: mcpTool.name,
|
|
302
|
-
output: structuredContent ?? content,
|
|
303
|
-
},
|
|
304
|
-
};
|
|
329
|
+
.map((mcpTool) => createPiMcpToolCandidate(
|
|
330
|
+
mcpTool,
|
|
331
|
+
(args, signal) => host.mcp.callTool(
|
|
332
|
+
{
|
|
333
|
+
serverId: mcpTool.serverId,
|
|
334
|
+
name: mcpTool.name,
|
|
335
|
+
arguments: args,
|
|
305
336
|
},
|
|
306
|
-
|
|
307
|
-
|
|
337
|
+
undefined,
|
|
338
|
+
{ signal },
|
|
339
|
+
),
|
|
340
|
+
{
|
|
308
341
|
owner: `mcp:${mcpTool.serverId}`,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
// MCP annotations are supplied by the remote server and are not an
|
|
313
|
-
// authorization boundary. Until the Host supplies trusted per-method
|
|
314
|
-
// policy, every dynamic MCP method must take the approval path.
|
|
342
|
+
modelName: modelVisibleName(mcpTool.serverId, mcpTool.name),
|
|
343
|
+
// Remote MCP annotations are untrusted; all user-configured methods
|
|
344
|
+
// retain the existing high-risk approval policy.
|
|
315
345
|
requiredExecutionLevel: "high",
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
});
|
|
346
|
+
},
|
|
347
|
+
));
|
|
319
348
|
}
|
package/src/pi/turn/index.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* 本目录统一使用以下术语:
|
|
8
8
|
*
|
|
9
9
|
* - **审批(approval)**:Tool handler 运行前持久化的用户许可;`pending` 状态会暂停 Turn。
|
|
10
|
+
* - **交互(interaction)**:结果由客户端投递而非 `execute()` 产出的 Tool 调用;`pending` 状态同样暂停 Turn。
|
|
11
|
+
* 与审批的差别只在响应体:审批是三态枚举,交互是任意 JSON。
|
|
10
12
|
* - **里程碑(milestone)**:属于同一个 `turnId` 和 `assemblyRevision` 的持久 JSON 记录。
|
|
11
13
|
* - **结算结果(settled ToolResult)**:已持久化且恢复时必须复用、不能再次执行副作用的 Tool 结果。
|
|
12
14
|
* - **续跑(continuation)**:审批或 Tool 结算后恢复原 Turn 的动作;先记 `pending`,再记 `committed`。
|
|
@@ -21,12 +23,16 @@ import {
|
|
|
21
23
|
parkPiToolApproval,
|
|
22
24
|
requiresPiToolApproval,
|
|
23
25
|
} from "./approval";
|
|
26
|
+
import { parkPiToolInteraction } from "./interaction";
|
|
24
27
|
import {
|
|
25
28
|
applyRecoveredPiApprovalDecision,
|
|
29
|
+
applyRecoveredPiToolInteractionSettlement,
|
|
26
30
|
commitPiRecoveryContinuation,
|
|
27
31
|
encodePiToolRecoveryMilestone,
|
|
28
32
|
planPiToolRecovery,
|
|
33
|
+
recordRecoveredPiToolInteraction,
|
|
29
34
|
replayPiToolRecovery,
|
|
35
|
+
stagePiAssemblyRepin,
|
|
30
36
|
stagePiRecoveryContinuation,
|
|
31
37
|
} from "./tool-recovery";
|
|
32
38
|
|
|
@@ -51,12 +57,26 @@ export const piApproval = Object.freeze({
|
|
|
51
57
|
*/
|
|
52
58
|
export const piRecovery = Object.freeze({
|
|
53
59
|
applyApprovalDecision: applyRecoveredPiApprovalDecision,
|
|
60
|
+
applyInteractionSettlement: applyRecoveredPiToolInteractionSettlement,
|
|
54
61
|
commitContinuation: commitPiRecoveryContinuation,
|
|
55
62
|
encodeMilestone: encodePiToolRecoveryMilestone,
|
|
56
63
|
plan: planPiToolRecovery,
|
|
64
|
+
recordInteraction: recordRecoveredPiToolInteraction,
|
|
57
65
|
replay: replayPiToolRecovery,
|
|
66
|
+
stageAssemblyRepin: stagePiAssemblyRepin,
|
|
58
67
|
stageContinuation: stagePiRecoveryContinuation,
|
|
59
68
|
});
|
|
60
69
|
|
|
70
|
+
/**
|
|
71
|
+
* 提供 Pi Tool interaction(结果由客户端提供的 Tool)的 park 入口。
|
|
72
|
+
*
|
|
73
|
+
* 执行适配器发现 Tool 声明了 `interaction` 后调用 `park` 生成待响应状态;
|
|
74
|
+
* 响应与取消是有状态操作,走 `piRecovery.applyInteractionSettlement`,不放在这个门面里。
|
|
75
|
+
*/
|
|
76
|
+
export const piInteraction = Object.freeze({
|
|
77
|
+
park: parkPiToolInteraction,
|
|
78
|
+
});
|
|
79
|
+
|
|
61
80
|
export * from "./approval";
|
|
81
|
+
export * from "./interaction";
|
|
62
82
|
export * from "./tool-recovery";
|
|
@@ -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
|
+
}
|