@springbrand/agent-runtime 0.2.0-alpha.27 → 0.2.0-alpha.29
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 +2 -1
- package/src/index.ts +6 -0
- package/src/pi/runtime-adapter/execution.ts +38 -1
- package/src/pi/tool/declared.ts +121 -0
- package/src/pi/tool/index.ts +3 -0
- package/src/pi/tool/mcp.ts +37 -49
- package/src/runtime.ts +30 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springbrand/agent-runtime",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.29",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
".": "./src/index.ts",
|
|
14
14
|
"./adapter/cloudflare": "./src/adapter/cloudflare/index.ts",
|
|
15
15
|
"./contracts": "./src/kernel/public-contracts.ts",
|
|
16
|
+
"./declared-tool": "./src/pi/tool/declared.ts",
|
|
16
17
|
"./models": "./src/lib/model-catalog.ts"
|
|
17
18
|
},
|
|
18
19
|
"dependencies": {
|
package/src/index.ts
CHANGED
|
@@ -112,6 +112,12 @@ export type {
|
|
|
112
112
|
SettledPiToolCall,
|
|
113
113
|
} from "./pi/tool";
|
|
114
114
|
export { basePiToolCandidates } from "./pi/tool";
|
|
115
|
+
export { createPiDeclaredToolCandidate } from "./pi/tool";
|
|
116
|
+
export type {
|
|
117
|
+
PiDeclaredTool,
|
|
118
|
+
PiDeclaredToolCallContext,
|
|
119
|
+
PiDeclaredToolPolicy,
|
|
120
|
+
} from "./pi/tool";
|
|
115
121
|
export {
|
|
116
122
|
createMemoryTools,
|
|
117
123
|
memoryPiToolCandidate,
|
|
@@ -210,6 +210,41 @@ function projectToolResultsForModel(
|
|
|
210
210
|
return changed ? projected : [...messages];
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
function orderLateToolResultsBeforeTurnAbortMarker(
|
|
214
|
+
messages: readonly AgentMessage[],
|
|
215
|
+
): AgentMessage[] {
|
|
216
|
+
const ordered: AgentMessage[] = [];
|
|
217
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
218
|
+
const message = messages[index]!;
|
|
219
|
+
const previous = ordered.at(-1);
|
|
220
|
+
if (
|
|
221
|
+
message.role !== "custom" ||
|
|
222
|
+
message.customType !== "turn-aborted" ||
|
|
223
|
+
previous?.role !== "assistant"
|
|
224
|
+
) {
|
|
225
|
+
ordered.push(message);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const pending = new Set(
|
|
229
|
+
previous.content.flatMap((part) =>
|
|
230
|
+
part.type === "toolCall" ? [part.id] : []
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
let cursor = index + 1;
|
|
234
|
+
while (true) {
|
|
235
|
+
const result = messages[cursor];
|
|
236
|
+
if (result?.role !== "toolResult" || !pending.has(result.toolCallId)) {
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
ordered.push(result);
|
|
240
|
+
cursor += 1;
|
|
241
|
+
}
|
|
242
|
+
ordered.push(message);
|
|
243
|
+
index = cursor - 1;
|
|
244
|
+
}
|
|
245
|
+
return ordered;
|
|
246
|
+
}
|
|
247
|
+
|
|
213
248
|
class PiTurnAdapter {
|
|
214
249
|
private piInstance?: PiCore;
|
|
215
250
|
private abortRequested = false;
|
|
@@ -304,7 +339,9 @@ class PiTurnAdapter {
|
|
|
304
339
|
transformContext: async (messages, signal) => {
|
|
305
340
|
const ctx = await this.opts.transformContext(messages, signal);
|
|
306
341
|
return transformMessages(
|
|
307
|
-
|
|
342
|
+
orderLateToolResultsBeforeTurnAbortMarker(
|
|
343
|
+
projectToolResultsForModel(ctx, selfBounded),
|
|
344
|
+
) as Message[],
|
|
308
345
|
this.opts.pi.model,
|
|
309
346
|
) as AgentMessage[];
|
|
310
347
|
},
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { PiToolCandidate } from "./compiler";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 一个由 Runtime 之外声明、由 Runtime 之外执行的工具。
|
|
6
|
+
*
|
|
7
|
+
* MCP Server、Connector Gateway、Extension 以及 Host 自己的远程能力,交给
|
|
8
|
+
* Runtime 的东西是同一份:一个名字、一段说明、一份参数 Schema,加上一次调用。
|
|
9
|
+
* 每种来源各自把这四样拼成 Tool Candidate 时,容易在 Schema 缺省、标签兜底或
|
|
10
|
+
* 治理字段上出现细微差别,于是同一个 Runtime 里不同来源的工具行为不一致。
|
|
11
|
+
*
|
|
12
|
+
* 这里只承担那份共同的拼装。连接、凭据、幂等、计费和结果语义仍属于声明方:
|
|
13
|
+
* 本模块不认识任何来源的业务概念,也不替声明方决定失败该不该重试。
|
|
14
|
+
*
|
|
15
|
+
* 本模块只引入类型,因此可经 `@springbrand/agent-runtime/declared-tool` 单独导入:
|
|
16
|
+
* 声明方在 Node 下也能装配自己的工具,不必为此加载 Workers 运行时。
|
|
17
|
+
*
|
|
18
|
+
* `Pi`、`Tool Candidate` 与 `Runtime` 等核心术语见 `../../index.ts`。
|
|
19
|
+
*/
|
|
20
|
+
export interface PiDeclaredTool {
|
|
21
|
+
/** 声明方自己的工具名,用于结果详情与诊断,不一定是模型看到的名字。 */
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly title?: string;
|
|
24
|
+
readonly description?: string;
|
|
25
|
+
/** 原始 JSON Schema;缺省时按开放对象处理。 */
|
|
26
|
+
readonly inputSchema?: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 一次调用的身份。
|
|
31
|
+
*
|
|
32
|
+
* `toolCallId` 是 Runtime 持久化并在恢复后重放的那一个,所以要求至多一次外部
|
|
33
|
+
* 副作用的声明方可以据此派生稳定的幂等键。中止信号原样透传:没有信号时不替调用
|
|
34
|
+
* 方凭空造一个,否则它无法分辨调用是否真的可被取消。
|
|
35
|
+
*/
|
|
36
|
+
export interface PiDeclaredToolCallContext {
|
|
37
|
+
readonly toolCallId: string;
|
|
38
|
+
readonly signal?: AbortSignal;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 声明方对一个工具的治理与归属主张。
|
|
43
|
+
*
|
|
44
|
+
* 模型可见名与执行级别都由声明方给出,而不是从远端声明里读:远端标注是提示,
|
|
45
|
+
* 不是本地审批边界。
|
|
46
|
+
*/
|
|
47
|
+
export interface PiDeclaredToolPolicy<Reply = unknown> {
|
|
48
|
+
readonly owner: string;
|
|
49
|
+
readonly modelName: string;
|
|
50
|
+
readonly requiredExecutionLevel: PiToolCandidate["requiredExecutionLevel"];
|
|
51
|
+
readonly requiredExecutionLevelForInput?: PiToolCandidate["requiredExecutionLevelForInput"];
|
|
52
|
+
readonly direct?: true;
|
|
53
|
+
readonly source?: PiToolCandidate["source"];
|
|
54
|
+
readonly summary?: string;
|
|
55
|
+
/**
|
|
56
|
+
* 把一次调用的返回值投影成模型可读的结果。
|
|
57
|
+
*
|
|
58
|
+
* 缺省是 JSON 文本加同一份详情,适用于返回结构化事实的远程调用。需要保留
|
|
59
|
+
* 图片等原生内容块的来源在此接管,因为只有它知道自己的内容协议。
|
|
60
|
+
*/
|
|
61
|
+
readonly project?: (reply: Reply) => AgentToolResult<unknown>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 作用:把一次调用的返回值收敛为模型可读的默认结果。
|
|
65
|
+
// 调用:声明方未提供 project 时,工具 execute 在拿到返回值后调用。
|
|
66
|
+
// 原因:字符串按原样呈现,其余序列化为 JSON,详情保持原值,
|
|
67
|
+
// 这样 Host 侧的结构化事实不会在默认路径上丢形状。
|
|
68
|
+
function jsonResult(reply: unknown): AgentToolResult<unknown> {
|
|
69
|
+
return {
|
|
70
|
+
content: [{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: typeof reply === "string" ? reply : JSON.stringify(reply) ?? "",
|
|
73
|
+
}],
|
|
74
|
+
details: reply,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 把一份外部声明和一次调用变成 Tool Candidate。
|
|
80
|
+
*
|
|
81
|
+
* 各来源的候选工厂在映射自己发现或编译出的每个工具时调用它。参数 Schema 会被
|
|
82
|
+
* 克隆后交给模型,避免声明方后续改动同一对象影响已装配的 Runtime。
|
|
83
|
+
*
|
|
84
|
+
* 调用时会带上本次 Tool Call 的身份:恢复后重放的是同一个 `toolCallId`,需要做到
|
|
85
|
+
* 至多一次外部副作用的声明方由此可以派生稳定的幂等键,而不必另建一套调用身份。
|
|
86
|
+
*
|
|
87
|
+
* 校验、审批、结算与恢复仍由 Runtime 统一负责,本函数不做任何一项;它也不捕获
|
|
88
|
+
* 调用抛出的错误,失败该如何解释属于声明方的语义。
|
|
89
|
+
*/
|
|
90
|
+
export function createPiDeclaredToolCandidate<Reply = unknown>(
|
|
91
|
+
declared: PiDeclaredTool,
|
|
92
|
+
call: (
|
|
93
|
+
input: Readonly<Record<string, unknown>>,
|
|
94
|
+
context: PiDeclaredToolCallContext,
|
|
95
|
+
) => Promise<Reply>,
|
|
96
|
+
policy: PiDeclaredToolPolicy<Reply>,
|
|
97
|
+
): PiToolCandidate {
|
|
98
|
+
const label = declared.title ?? declared.name;
|
|
99
|
+
const project = policy.project ?? jsonResult;
|
|
100
|
+
const tool: AgentTool<any, unknown> = {
|
|
101
|
+
name: policy.modelName,
|
|
102
|
+
label,
|
|
103
|
+
description: declared.description ?? label,
|
|
104
|
+
parameters: structuredClone(
|
|
105
|
+
declared.inputSchema ?? { type: "object", additionalProperties: true },
|
|
106
|
+
) as AgentTool["parameters"],
|
|
107
|
+
execute: async (toolCallId, input, signal) =>
|
|
108
|
+
project(await call(input as Record<string, unknown>, { toolCallId, signal })),
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
owner: policy.owner,
|
|
112
|
+
tool,
|
|
113
|
+
summary: policy.summary ?? label,
|
|
114
|
+
requiredExecutionLevel: policy.requiredExecutionLevel,
|
|
115
|
+
...(policy.requiredExecutionLevelForInput
|
|
116
|
+
? { requiredExecutionLevelForInput: policy.requiredExecutionLevelForInput }
|
|
117
|
+
: {}),
|
|
118
|
+
...(policy.direct ? { direct: policy.direct } : {}),
|
|
119
|
+
...(policy.source ? { source: policy.source } : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
package/src/pi/tool/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
createPiToolGovernance,
|
|
4
4
|
} from "./compiler";
|
|
5
5
|
import { listExtensionsPiToolCandidate } from "./core";
|
|
6
|
+
import { createPiDeclaredToolCandidate } from "./declared";
|
|
6
7
|
import { createPiMcpToolCandidates } from "./mcp";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -15,6 +16,7 @@ import { createPiMcpToolCandidates } from "./mcp";
|
|
|
15
16
|
export const piTools = Object.freeze({
|
|
16
17
|
compile: compilePiTools,
|
|
17
18
|
createGovernance: createPiToolGovernance,
|
|
19
|
+
createDeclaredCandidate: createPiDeclaredToolCandidate,
|
|
18
20
|
createMcpCandidates: createPiMcpToolCandidates,
|
|
19
21
|
listExtensionsCandidate: listExtensionsPiToolCandidate,
|
|
20
22
|
});
|
|
@@ -24,6 +26,7 @@ export * from "./base";
|
|
|
24
26
|
export * from "./compiler";
|
|
25
27
|
export * from "./core";
|
|
26
28
|
export * from "./core-host";
|
|
29
|
+
export * from "./declared";
|
|
27
30
|
export * from "./gateway";
|
|
28
31
|
export * from "./mcp";
|
|
29
32
|
export * from "./schedule";
|
package/src/pi/tool/mcp.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
1
|
import type { MCPClientManager } from "agents/mcp/client";
|
|
3
2
|
import type { RuntimeMcpServer } from "../../kernel/profile";
|
|
4
3
|
import type { PiToolCandidate } from "./compiler";
|
|
4
|
+
import { createPiDeclaredToolCandidate } from "./declared";
|
|
5
5
|
|
|
6
6
|
interface McpCallResult {
|
|
7
7
|
content: unknown[];
|
|
@@ -217,7 +217,12 @@ function errorMessage(result: McpCallResult): string {
|
|
|
217
217
|
: "MCP tool call failed";
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* 共享 MCP 结果投影;Gateway 与用户配置的 Remote MCP 均经过此边界。
|
|
222
|
+
*
|
|
223
|
+
* 名字、说明、Schema 与治理字段的拼装交给声明式工具工厂,这里只保留 MCP 自己
|
|
224
|
+
* 的内容协议:`_meta` 剥离、原生图片保留,以及 `isError` 抛错。
|
|
225
|
+
*/
|
|
221
226
|
export function createPiMcpToolCandidate(
|
|
222
227
|
mcpTool: PiDiscoveredMcpTool,
|
|
223
228
|
callTool: (
|
|
@@ -226,55 +231,38 @@ export function createPiMcpToolCandidate(
|
|
|
226
231
|
) => Promise<unknown>,
|
|
227
232
|
policy: PiMcpCandidatePolicy,
|
|
228
233
|
): PiToolCandidate {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
234
|
+
return createPiDeclaredToolCandidate(
|
|
235
|
+
{
|
|
236
|
+
name: mcpTool.name,
|
|
237
|
+
title: mcpTool.title ?? mcpTool.annotations?.title,
|
|
238
|
+
description: mcpTool.description,
|
|
239
|
+
inputSchema: mcpTool.inputSchema,
|
|
240
|
+
},
|
|
241
|
+
(input, { signal }) => callTool(input, signal),
|
|
242
|
+
{
|
|
243
|
+
owner: policy.owner,
|
|
244
|
+
modelName: policy.modelName,
|
|
245
|
+
requiredExecutionLevel: policy.requiredExecutionLevel,
|
|
246
|
+
requiredExecutionLevelForInput: policy.requiredExecutionLevelForInput,
|
|
247
|
+
source: "action",
|
|
248
|
+
project: (reply) => {
|
|
249
|
+
const result = normalizeCallResult(reply);
|
|
250
|
+
if (result.isError) throw new Error(errorMessage(result));
|
|
251
|
+
const structuredContent = result.structuredContent === undefined
|
|
252
|
+
? undefined
|
|
253
|
+
: publicMcpValue(result.structuredContent);
|
|
254
|
+
const content = resultContent({ ...result, structuredContent });
|
|
255
|
+
return {
|
|
256
|
+
content,
|
|
257
|
+
details: {
|
|
258
|
+
kind: "mcp",
|
|
259
|
+
toolName: mcpTool.name,
|
|
260
|
+
output: structuredContent ?? content,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
244
263
|
},
|
|
245
|
-
) as AgentTool["parameters"],
|
|
246
|
-
execute: async (_toolCallId, args, signal) => {
|
|
247
|
-
const result = normalizeCallResult(
|
|
248
|
-
await callTool(args as Record<string, unknown>, signal),
|
|
249
|
-
);
|
|
250
|
-
if (result.isError) throw new Error(errorMessage(result));
|
|
251
|
-
const structuredContent = result.structuredContent === undefined
|
|
252
|
-
? undefined
|
|
253
|
-
: publicMcpValue(result.structuredContent);
|
|
254
|
-
const content = resultContent({ ...result, structuredContent });
|
|
255
|
-
return {
|
|
256
|
-
content,
|
|
257
|
-
details: {
|
|
258
|
-
kind: "mcp",
|
|
259
|
-
toolName: mcpTool.name,
|
|
260
|
-
output: structuredContent ?? content,
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
264
|
},
|
|
264
|
-
|
|
265
|
-
return {
|
|
266
|
-
owner: policy.owner,
|
|
267
|
-
tool,
|
|
268
|
-
summary: label,
|
|
269
|
-
requiredExecutionLevel: policy.requiredExecutionLevel,
|
|
270
|
-
...(policy.requiredExecutionLevelForInput
|
|
271
|
-
? {
|
|
272
|
-
requiredExecutionLevelForInput:
|
|
273
|
-
policy.requiredExecutionLevelForInput,
|
|
274
|
-
}
|
|
275
|
-
: {}),
|
|
276
|
-
source: "action",
|
|
277
|
-
};
|
|
265
|
+
);
|
|
278
266
|
}
|
|
279
267
|
|
|
280
268
|
/**
|
package/src/runtime.ts
CHANGED
|
@@ -2560,6 +2560,36 @@ export abstract class AgentRuntimeKernel<
|
|
|
2560
2560
|
return { kind: "settled" };
|
|
2561
2561
|
case "resume-turn": {
|
|
2562
2562
|
const last = (await this.transcript.canonicalMessages()).at(-1);
|
|
2563
|
+
if (last?.role === "assistant" && last.stopReason === "toolUse") {
|
|
2564
|
+
// Tool input 会在真正执行前持久化。恢复状态仍为 idle 证明这些
|
|
2565
|
+
// canonical Tool Call 还没越过 attempt 边界,包括非幂等 Tool 也可以安全首次执行。
|
|
2566
|
+
const unstartedCalls = last.content.filter((part) =>
|
|
2567
|
+
part.type === "toolCall"
|
|
2568
|
+
);
|
|
2569
|
+
if (unstartedCalls.length > 0) {
|
|
2570
|
+
for (const call of unstartedCalls) {
|
|
2571
|
+
try {
|
|
2572
|
+
if (
|
|
2573
|
+
!await adapter.retryTool({
|
|
2574
|
+
toolName: call.name,
|
|
2575
|
+
toolCallId: call.id,
|
|
2576
|
+
input: call.arguments,
|
|
2577
|
+
})
|
|
2578
|
+
) {
|
|
2579
|
+
return {
|
|
2580
|
+
kind: "unresumable",
|
|
2581
|
+
reason:
|
|
2582
|
+
`SpringBrand could not resume this turn: Tool "${call.name}" is no longer available.`,
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
} catch {
|
|
2586
|
+
// The governed Tool persisted its bounded error ToolResult.
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
decision = await this.materializeRecoveredToolResults(submission);
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2563
2593
|
// Pi 的 agentLoopContinue 拒绝从 assistant 消息继续。悬空的尾部 assistant
|
|
2564
2594
|
// 意味着这条 transcript 续不下去了 —— 判断出来就必须收尾,不能默默退场。
|
|
2565
2595
|
return last?.role === "user" || last?.role === "toolResult"
|