@springbrand/agent-runtime 0.3.0-alpha.2 → 0.3.0-alpha.21
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 +11 -9
- package/src/adapter/cloudflare/agent.ts +233 -179
- package/src/adapter/cloudflare/define-agent.ts +252 -104
- package/src/adapter/cloudflare/index.ts +18 -0
- package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
- package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
- package/src/adapter/cloudflare/schema.ts +21 -2
- package/src/adapter/cloudflare/subagent/definition.ts +25 -13
- package/src/adapter/cloudflare/subagent/runner.ts +43 -50
- package/src/adapter/cloudflare/subagent/tools.ts +4 -4
- package/src/adapter/cloudflare/universal-agent/preparation.ts +3 -3
- package/src/adapter/cloudflare/universal-agent/tools.ts +4 -6
- package/src/adapter/cloudflare/workspace/version-control.ts +1 -1
- package/src/config/activation.ts +13 -15
- package/src/config/agent-context.ts +1 -1
- package/src/config/assembler.ts +48 -31
- package/src/config/assembly-snapshot.ts +1 -1
- package/src/config/definition.ts +1 -1
- package/src/config/harness-runtime.ts +77 -49
- package/src/config/index.ts +1 -1
- package/src/config/model-guidance.ts +20 -0
- package/src/config/ports.ts +57 -7
- package/src/config/{pinning.ts → prepared-runtime.ts} +59 -123
- package/src/config/public-contracts.ts +2 -2
- package/src/config/runtime-config.ts +2 -1
- package/src/context/assembly.ts +1 -1
- package/src/context/extension/assembly.ts +2 -2
- package/src/context/memory/budget/gate.ts +19 -3
- package/src/conversation/harness-resume.ts +89 -238
- package/src/conversation/harness-stream.ts +87 -71
- package/src/conversation/history.ts +154 -152
- package/src/conversation/message/contract.ts +28 -4
- package/src/conversation/message/conversion.ts +46 -5
- package/src/conversation/message/projection.ts +61 -21
- package/src/conversation/session-entry.ts +29 -0
- package/src/conversation/state.ts +23 -2
- package/src/harness/governance.ts +206 -0
- package/src/harness/index.ts +3 -0
- package/src/harness/session-harness.ts +269 -445
- package/src/harness/submission-operation.ts +15 -40
- package/src/index.ts +18 -8
- package/src/lib/artifacts.ts +18 -0
- package/src/lib/execution-level.ts +13 -0
- package/src/lib/prompt.ts +9 -10
- package/src/lib/token-estimate.ts +8 -0
- package/src/model-protocol/openrouter.ts +41 -0
- package/src/models/index.ts +200 -35
- package/src/models/openrouter/messages.ts +13 -8
- package/src/models/openrouter/openai.ts +200 -0
- package/src/models/openrouter/protocol-response.ts +85 -0
- package/src/models/provider-streams.ts +45 -0
- package/src/session/cloudflare/migration.ts +382 -864
- package/src/session/cloudflare/storage.ts +13 -536
- package/src/session/index.ts +22 -35
- package/src/skills/springbrand-worker-website/index.ts +1 -1
- package/src/submission/admission.ts +20 -30
- package/src/submission/contracts.ts +42 -2
- package/src/submission/delivery.ts +1 -1
- package/src/submission/finalization.ts +36 -15
- package/src/submission/lifecycle.ts +28 -15
- package/src/submission/repository.ts +97 -49
- package/src/submission/steer-delivery.ts +32 -76
- package/src/submission/unattended.ts +40 -0
- package/src/telemetry/contract.ts +0 -1
- package/src/telemetry/coordinator.ts +1 -1
- package/src/telemetry/runtime-telemetry.ts +0 -4
- package/src/telemetry/subagent-lifecycle.ts +1 -1
- package/src/telemetry/turn-events.ts +35 -25
- package/src/tools/README.md +44 -0
- package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
- package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
- package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
- package/src/tools/assistant/ask-user.ts +319 -0
- package/src/tools/assistant/candidate.ts +15 -0
- package/src/tools/assistant/followups.ts +34 -0
- package/src/tools/assistant/index.ts +19 -0
- package/src/tools/assistant/memory.ts +73 -0
- package/src/tools/assistant/plan.ts +98 -0
- package/src/tools/{time.ts → assistant/time.ts} +1 -1
- package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
- package/src/tools/{core.ts → codemode/index.ts} +36 -92
- package/src/tools/codemode/workspace-connector.ts +198 -0
- package/src/tools/extension/index.ts +42 -0
- package/src/tools/index.ts +29 -26
- package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
- package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
- package/src/tools/runtime/activation.ts +40 -0
- package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
- package/src/tools/runtime/compiler.ts +246 -0
- package/src/tools/runtime/contracts.ts +140 -0
- package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +149 -39
- package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
- package/src/tools/{interaction → runtime/interaction}/repository.ts +1 -7
- package/src/tools/runtime/result.ts +22 -0
- package/src/tools/runtime/unattended-error.ts +4 -0
- package/src/tools/sandbox/index.ts +228 -0
- package/src/tools/schedule/index.ts +301 -0
- package/src/tools/skill/contracts.ts +20 -0
- package/src/tools/skill/index.ts +99 -0
- package/src/tools/skill/resources.ts +322 -0
- package/src/tools/skill/scripts.ts +168 -0
- package/src/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
- package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
- package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
- package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
- package/src/tools/{temporary-agent → subagent/temporary}/core.ts +2 -3
- package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
- package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
- package/src/tools/web/index.ts +2 -0
- package/src/tools/web/search/api.ts +1322 -0
- package/src/tools/web/search/index.ts +2 -0
- package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
- package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
- package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
- package/src/tools/base.ts +0 -534
- package/src/tools/compiler.ts +0 -327
- package/src/tools/schedule.ts +0 -261
- package/src/tools/skill.ts +0 -565
- package/src/tools/web-search/api.ts +0 -1318
- package/src/tools/web-search/index.ts +0 -2
- package/src/tools/workspace-state-connector.ts +0 -146
- package/src/tools/{approval → runtime/approval}/index.ts +0 -0
- package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
- package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
- /package/src/tools/{workspace-versioning.ts → workspace/versioning.ts} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { RuntimeGatewaySession } from "
|
|
2
|
-
import type { PiToolCandidate } from "
|
|
3
|
-
import { createPiMcpToolCandidate } from "./
|
|
1
|
+
import type { RuntimeGatewaySession } from "../../config/ports";
|
|
2
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
3
|
+
import { createPiMcpToolCandidate } from "./index";
|
|
4
4
|
|
|
5
5
|
const GATEWAY_TOOLS = [
|
|
6
6
|
"search_capabilities",
|
|
@@ -31,22 +31,22 @@ export function createPiGatewayToolCandidates(
|
|
|
31
31
|
requiredExecutionLevel: execute ? "high" : "safe",
|
|
32
32
|
...(execute
|
|
33
33
|
? {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
50
|
: {}),
|
|
51
51
|
},
|
|
52
52
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { RuntimeMcpServer } from "
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import type { RuntimeMcpServer } from "../../config/profile";
|
|
2
|
+
import { createPiDeclaredToolCandidate } from "../adapters/declared";
|
|
3
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
4
4
|
|
|
5
5
|
interface McpCallResult {
|
|
6
6
|
content: unknown[];
|
|
@@ -34,7 +34,7 @@ interface PiMcpCandidatePolicy {
|
|
|
34
34
|
* 单个 Server 的客户端,Server 是对外提供工具的服务。连接持久化、OAuth 和
|
|
35
35
|
* 恢复仍由 Agents SDK 的 MCP Client 管理,本接口只读取和调用。
|
|
36
36
|
*
|
|
37
|
-
* `Host` 与 `Pi` 等项目核心术语见
|
|
37
|
+
* `Host` 与 `Pi` 等项目核心术语见 `src/index.ts`。
|
|
38
38
|
*/
|
|
39
39
|
export interface PiMcpHost {
|
|
40
40
|
/**
|
|
@@ -284,7 +284,7 @@ export function createPiMcpToolCandidate(
|
|
|
284
284
|
* 远端 Server 提供的 annotations 只是提示,不是本地审批边界,所以所有动态
|
|
285
285
|
* MCP 工具继续标为高风险,不能据此绕过审批。
|
|
286
286
|
*
|
|
287
|
-
* `Host`、`Pi`、`Runtime Snapshot` 与“候选工具”见
|
|
287
|
+
* `Host`、`Pi`、`Runtime Snapshot` 与“候选工具”见 `src/index.ts`。
|
|
288
288
|
*/
|
|
289
289
|
export async function createPiMcpToolCandidates(
|
|
290
290
|
host: PiMcpHost,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { PiToolCandidate } from "./contracts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 把 Candidate 上的按需激活声明收成 SessionHarness 可直接消费的选项。
|
|
6
|
+
*
|
|
7
|
+
* `inactiveUntilActivated` 的工具仍编译进 Harness,只是一开始不在活动集里。
|
|
8
|
+
* 其他工具成功返回后通过 `toolsToActivate` 点名,Harness 在 `after_tool`
|
|
9
|
+
* 里 `setActiveTools`,由 Pi Agent Loop 写成 transcript 的 `toolsAdded`。
|
|
10
|
+
*/
|
|
11
|
+
export function harnessToolActivation(candidates: readonly PiToolCandidate[]): {
|
|
12
|
+
readonly activeToolNames: string[];
|
|
13
|
+
readonly onDemandToolNames: readonly string[];
|
|
14
|
+
readonly toolsToActivate: (
|
|
15
|
+
toolName: string,
|
|
16
|
+
result: AgentToolResult<unknown>,
|
|
17
|
+
isError: boolean,
|
|
18
|
+
) => readonly string[];
|
|
19
|
+
} {
|
|
20
|
+
const compiled = candidates.map(({ tool }) => tool.name);
|
|
21
|
+
const onDemand = candidates
|
|
22
|
+
.filter((candidate) => candidate.inactiveUntilActivated === true)
|
|
23
|
+
.map(({ tool }) => tool.name);
|
|
24
|
+
const onDemandSet = new Set(onDemand);
|
|
25
|
+
const activators = new Map(
|
|
26
|
+
candidates.flatMap((candidate) =>
|
|
27
|
+
candidate.toolsToActivate
|
|
28
|
+
? [[candidate.tool.name, candidate.toolsToActivate] as const]
|
|
29
|
+
: [],
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
return {
|
|
33
|
+
activeToolNames: compiled.filter((name) => !onDemandSet.has(name)),
|
|
34
|
+
onDemandToolNames: onDemand,
|
|
35
|
+
toolsToActivate: (toolName, result, isError) => {
|
|
36
|
+
if (isError) return [];
|
|
37
|
+
return activators.get(toolName)?.(result) ?? [];
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
|
-
import type { ExecutionLevel } from "
|
|
2
|
+
import type { ExecutionLevel } from "../../../lib/execution-level";
|
|
3
3
|
|
|
4
4
|
// Approval 的统一定义见 ./index.ts。
|
|
5
|
-
// #region 状态与行类型
|
|
6
5
|
export type ApprovalStatus = "pending" | "approved" | "rejected";
|
|
7
6
|
|
|
8
7
|
export interface StoredApproval {
|
|
@@ -16,6 +15,7 @@ export interface StoredApproval {
|
|
|
16
15
|
executionLevel: ExecutionLevel;
|
|
17
16
|
requiredExecutionLevel: ExecutionLevel;
|
|
18
17
|
inputJson: string;
|
|
18
|
+
confirmationJson?: string | null;
|
|
19
19
|
status: ApprovalStatus;
|
|
20
20
|
createdAt: number;
|
|
21
21
|
decidedAt: number | null;
|
|
@@ -33,6 +33,7 @@ export interface NewApproval {
|
|
|
33
33
|
executionLevel: ExecutionLevel;
|
|
34
34
|
requiredExecutionLevel: ExecutionLevel;
|
|
35
35
|
inputJson: string;
|
|
36
|
+
confirmationJson?: string | null;
|
|
36
37
|
status: ApprovalStatus;
|
|
37
38
|
createdAt: number;
|
|
38
39
|
}
|
|
@@ -46,6 +47,7 @@ export interface PendingApprovalSummary {
|
|
|
46
47
|
executionLevel: ExecutionLevel;
|
|
47
48
|
requiredExecutionLevel: ExecutionLevel;
|
|
48
49
|
inputJson: string;
|
|
50
|
+
confirmationJson?: string | null;
|
|
49
51
|
requestId: string;
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -66,14 +68,13 @@ type ApprovalRow = {
|
|
|
66
68
|
execution_level: string;
|
|
67
69
|
required_level: string;
|
|
68
70
|
input_json: string;
|
|
71
|
+
confirmation_json: string | null;
|
|
69
72
|
status: string;
|
|
70
73
|
created_at: number;
|
|
71
74
|
decided_at: number | null;
|
|
72
75
|
reason: string | null;
|
|
73
76
|
};
|
|
74
77
|
|
|
75
|
-
// #endregion
|
|
76
|
-
// #region 行映射
|
|
77
78
|
// 把 SQLite 的 Approval 行转成 Runtime 对象。
|
|
78
79
|
// find 查到记录后调用,上层因而不需要理解 snake_case 列名。
|
|
79
80
|
// 状态由表内字符串转成联合类型;新增列或状态时必须同步核对类型、SELECT 和映射。
|
|
@@ -89,6 +90,7 @@ function mapRow(row: ApprovalRow): StoredApproval {
|
|
|
89
90
|
executionLevel: row.execution_level as ExecutionLevel,
|
|
90
91
|
requiredExecutionLevel: row.required_level as ExecutionLevel,
|
|
91
92
|
inputJson: row.input_json,
|
|
93
|
+
confirmationJson: row.confirmation_json,
|
|
92
94
|
status: row.status as ApprovalStatus,
|
|
93
95
|
createdAt: row.created_at,
|
|
94
96
|
decidedAt: row.decided_at,
|
|
@@ -96,13 +98,11 @@ function mapRow(row: ApprovalRow): StoredApproval {
|
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
// #endregion
|
|
100
|
-
// #region Repository
|
|
101
101
|
export class ApprovalRepository {
|
|
102
102
|
// 保存当前 Agent SQLite 的查询入口。
|
|
103
103
|
// RuntimeDatabase 构造时调用,HarnessDecisionGate 和 Transcript 随后共享这个 Repository。
|
|
104
104
|
// Approval 必须与所属 Submission 在同一 Agent 存储中查询,不能跨 Durable Object 拼接状态。
|
|
105
|
-
constructor(private readonly sql: SqlTaggedTemplate) {}
|
|
105
|
+
constructor(private readonly sql: SqlTaggedTemplate) { }
|
|
106
106
|
|
|
107
107
|
// 按 executionId 读取一条完整 Approval,不存在时返回 null。
|
|
108
108
|
// HarnessDecisionGate 在请求去重、决策、继续和拒绝时调用,调用方必须处理 null。
|
|
@@ -111,7 +111,7 @@ export class ApprovalRepository {
|
|
|
111
111
|
const row = this.sql<ApprovalRow>`
|
|
112
112
|
SELECT execution_id, submission_id, request_id, source,
|
|
113
113
|
tool_call_id, tool_name, summary,
|
|
114
|
-
execution_level, required_level, input_json,
|
|
114
|
+
execution_level, required_level, input_json, confirmation_json,
|
|
115
115
|
status, created_at, decided_at, reason
|
|
116
116
|
FROM pi_approvals
|
|
117
117
|
WHERE execution_id = ${executionId}
|
|
@@ -128,7 +128,7 @@ export class ApprovalRepository {
|
|
|
128
128
|
INSERT INTO pi_approvals (
|
|
129
129
|
execution_id, submission_id, request_id, source,
|
|
130
130
|
tool_call_id, tool_name, summary,
|
|
131
|
-
execution_level, required_level, input_json,
|
|
131
|
+
execution_level, required_level, input_json, confirmation_json,
|
|
132
132
|
status, created_at
|
|
133
133
|
) VALUES (
|
|
134
134
|
${approval.executionId},
|
|
@@ -141,6 +141,7 @@ export class ApprovalRepository {
|
|
|
141
141
|
${approval.executionLevel},
|
|
142
142
|
${approval.requiredExecutionLevel},
|
|
143
143
|
${approval.inputJson},
|
|
144
|
+
${approval.confirmationJson ?? null},
|
|
144
145
|
${approval.status},
|
|
145
146
|
${approval.createdAt}
|
|
146
147
|
)
|
|
@@ -192,12 +193,13 @@ export class ApprovalRepository {
|
|
|
192
193
|
execution_level: string;
|
|
193
194
|
required_level: string;
|
|
194
195
|
input_json: string;
|
|
196
|
+
confirmation_json: string | null;
|
|
195
197
|
request_id: string;
|
|
196
198
|
}>`
|
|
197
199
|
SELECT approval.execution_id, approval.tool_call_id, approval.source,
|
|
198
200
|
approval.tool_name, approval.summary,
|
|
199
201
|
approval.execution_level, approval.required_level,
|
|
200
|
-
approval.input_json, approval.request_id
|
|
202
|
+
approval.input_json, approval.confirmation_json, approval.request_id
|
|
201
203
|
FROM pi_approvals approval
|
|
202
204
|
JOIN pi_submissions submission
|
|
203
205
|
ON submission.submission_id = approval.submission_id
|
|
@@ -212,7 +214,7 @@ export class ApprovalRepository {
|
|
|
212
214
|
summary: row.summary,
|
|
213
215
|
executionLevel: row.execution_level as ExecutionLevel,
|
|
214
216
|
requiredExecutionLevel: row.required_level as ExecutionLevel,
|
|
215
|
-
inputJson: row.input_json,
|
|
217
|
+
inputJson: row.confirmation_json ?? row.input_json,
|
|
216
218
|
requestId: row.request_id,
|
|
217
219
|
}));
|
|
218
220
|
}
|
|
@@ -271,4 +273,3 @@ export class ApprovalRepository {
|
|
|
271
273
|
}));
|
|
272
274
|
}
|
|
273
275
|
}
|
|
274
|
-
// #endregion
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ToolRegistration } from "@earendil-works/pi-durable";
|
|
3
|
+
import { awaitWithContext } from "@earendil-works/chord/context";
|
|
4
|
+
import { boundDurableToolOutput, spillDurableToolOutput } from "../../context/memory/budget/gate";
|
|
5
|
+
import { artifactId, serializeOutput, sha256Hex, type ArtifactRef, type SpillWorkspace } from "../../lib/artifacts";
|
|
6
|
+
import type { CompileHarnessToolsOptions, PiToolCandidate } from "./contracts";
|
|
7
|
+
import { UnattendedExecutionError } from "./unattended-error";
|
|
8
|
+
|
|
9
|
+
// DO SQLite rows also contain arguments and transcript/recovery envelopes.
|
|
10
|
+
// Keep each serialized result below half the 2 MiB row limit.
|
|
11
|
+
const DURABLE_TOOL_RESULT_MAX_BYTES = 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
const MEDIA_CAPTION_MAX_CHARS = 4 * 1024;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
// 把任意失败值收敛为可安全显示的简短消息。
|
|
18
|
+
// 受治理工具在工具执行或结算抛错时调用它。
|
|
19
|
+
// 读取错误自身也可能抛错,因此最外层必须保留固定回退文本。
|
|
20
|
+
function errorMessage(error: unknown): string {
|
|
21
|
+
try {
|
|
22
|
+
if (error instanceof Error) {
|
|
23
|
+
return error.message || error.name;
|
|
24
|
+
}
|
|
25
|
+
return String(error);
|
|
26
|
+
} catch {
|
|
27
|
+
return "Tool execution failed";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Most Pi adapters keep the original Tool value in details and its serialized
|
|
32
|
+
// model form in one text block. Spill that original once instead of duplicating
|
|
33
|
+
// it inside an AgentToolResult JSON envelope.
|
|
34
|
+
function spillPayload(result: AgentToolResult<unknown>): unknown {
|
|
35
|
+
const text = result.content.length === 1 && result.content[0]?.type === "text"
|
|
36
|
+
? result.content[0].text
|
|
37
|
+
: undefined;
|
|
38
|
+
if (text !== undefined && serializeOutput(result.details).text === text) {
|
|
39
|
+
return result.details;
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function artifactResult(
|
|
45
|
+
result: AgentToolResult<unknown>,
|
|
46
|
+
ref: ArtifactRef,
|
|
47
|
+
): AgentToolResult<unknown> {
|
|
48
|
+
const presentation = result.details !== null && typeof result.details === "object"
|
|
49
|
+
? (result.details as Record<string, unknown>).presentation
|
|
50
|
+
: undefined;
|
|
51
|
+
let captionRemaining = MEDIA_CAPTION_MAX_CHARS;
|
|
52
|
+
const mediaContent = result.content.some((part) => part.type !== "text")
|
|
53
|
+
? result.content.flatMap((part): AgentToolResult<unknown>["content"] => {
|
|
54
|
+
if (part.type !== "text") return [part];
|
|
55
|
+
const text = part.text.slice(0, captionRemaining);
|
|
56
|
+
captionRemaining -= text.length;
|
|
57
|
+
return text ? [{ ...part, text }] : [];
|
|
58
|
+
})
|
|
59
|
+
: [];
|
|
60
|
+
return {
|
|
61
|
+
...result,
|
|
62
|
+
content: [{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: [
|
|
65
|
+
ref.note,
|
|
66
|
+
`Path: ${ref.path}`,
|
|
67
|
+
`Bytes: ${ref.bytes}`,
|
|
68
|
+
`Preview:\n${ref.preview}`,
|
|
69
|
+
].join("\n"),
|
|
70
|
+
}, ...mediaContent],
|
|
71
|
+
details: presentation === undefined ? ref : { ...ref, presentation },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function unavailableArtifactResult(
|
|
76
|
+
result: AgentToolResult<unknown>,
|
|
77
|
+
): Promise<AgentToolResult<unknown>> {
|
|
78
|
+
const { text, ext } = serializeOutput(spillPayload(result));
|
|
79
|
+
const hash = await sha256Hex(text);
|
|
80
|
+
const ref: ArtifactRef = {
|
|
81
|
+
kind: "artifact_ref",
|
|
82
|
+
artifactId: artifactId(hash),
|
|
83
|
+
path: `/scratch/tool-output/${hash.slice(0, 24)}.${ext}`,
|
|
84
|
+
bytes: new TextEncoder().encode(text).byteLength,
|
|
85
|
+
hash,
|
|
86
|
+
workspaceRevision: "unavailable",
|
|
87
|
+
availability: "unavailable",
|
|
88
|
+
preview: text.slice(0, 600),
|
|
89
|
+
note:
|
|
90
|
+
"The Tool output was too large to keep inline, and the Workspace artifact " +
|
|
91
|
+
"could not be verified. The evidence is unavailable; do not treat the preview as complete.",
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
...result,
|
|
95
|
+
content: [{
|
|
96
|
+
type: "text",
|
|
97
|
+
text: [
|
|
98
|
+
ref.note,
|
|
99
|
+
`Artifact ID: ${ref.artifactId}`,
|
|
100
|
+
`Path: ${ref.path}`,
|
|
101
|
+
`Bytes: ${ref.bytes}`,
|
|
102
|
+
`Hash: ${ref.hash}`,
|
|
103
|
+
`Preview:\n${ref.preview}`,
|
|
104
|
+
].join("\n"),
|
|
105
|
+
}],
|
|
106
|
+
details: ref,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function durableResult(
|
|
111
|
+
result: AgentToolResult<unknown>,
|
|
112
|
+
candidate: PiToolCandidate,
|
|
113
|
+
workspace: SpillWorkspace | undefined,
|
|
114
|
+
): Promise<AgentToolResult<unknown>> {
|
|
115
|
+
const spill = (candidate.outputBudget ?? { kind: "spill" }).kind === "spill";
|
|
116
|
+
const serialized = serializeOutput(spillPayload(result));
|
|
117
|
+
let ref = spill
|
|
118
|
+
? await spillDurableToolOutput(spillPayload(result), { workspace })
|
|
119
|
+
: null;
|
|
120
|
+
if (spill && serialized.text.length > 32 * 1024 && !ref) {
|
|
121
|
+
return unavailableArtifactResult(result);
|
|
122
|
+
}
|
|
123
|
+
const bounded = ref
|
|
124
|
+
? artifactResult(result, ref)
|
|
125
|
+
: boundDurableToolOutput(result) as AgentToolResult<unknown>;
|
|
126
|
+
const bytes = new TextEncoder().encode(JSON.stringify(bounded)).byteLength;
|
|
127
|
+
if (bytes <= DURABLE_TOOL_RESULT_MAX_BYTES) return bounded;
|
|
128
|
+
|
|
129
|
+
// Binary blocks cannot be truncated, and this boundary has no image resizer.
|
|
130
|
+
// Preserve the original where possible and fail before any SQLite write.
|
|
131
|
+
if (!spill) ref = await spillDurableToolOutput(spillPayload(result), { workspace });
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Tool output exceeds the 1 MiB durable result limit (${bytes} bytes). ` +
|
|
134
|
+
"Return a smaller result, such as lower-resolution images or fewer media items; the original cannot be included inline. " +
|
|
135
|
+
(ref ? `Original output saved to Workspace: ${ref.path}` : "The original output could not be saved to Workspace."),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Convert the configured candidate surface once at the AgentHarness boundary.
|
|
141
|
+
* Schema, argument preparation, progress, cancellation, replay classification,
|
|
142
|
+
* and output bounds stay attached to the original Tool declaration.
|
|
143
|
+
*/
|
|
144
|
+
export function compileHarnessTools(
|
|
145
|
+
candidates: readonly PiToolCandidate[],
|
|
146
|
+
options: CompileHarnessToolsOptions = {},
|
|
147
|
+
): ToolRegistration<any, any>[] {
|
|
148
|
+
return candidates.map((candidate) => {
|
|
149
|
+
const source = candidate.tool;
|
|
150
|
+
const tool = source.constrainedSampling === false
|
|
151
|
+
? source
|
|
152
|
+
: {
|
|
153
|
+
...source,
|
|
154
|
+
constrainedSampling: source.constrainedSampling ?? {
|
|
155
|
+
type: "json_schema" as const,
|
|
156
|
+
strict: "prefer" as const,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
...tool,
|
|
161
|
+
replay: candidate.retry === "idempotent" &&
|
|
162
|
+
candidate.requiredExecutionLevel === "safe" &&
|
|
163
|
+
!candidate.alwaysRequiresApproval &&
|
|
164
|
+
!candidate.requiredExecutionLevelForInput &&
|
|
165
|
+
!candidate.interaction
|
|
166
|
+
? "safe" as const
|
|
167
|
+
: "unsafe" as const,
|
|
168
|
+
...(candidate.deferLoading ? { deferLoading: true as const } : {}),
|
|
169
|
+
async execute(args, api, context) {
|
|
170
|
+
const toolCallId = api.callId;
|
|
171
|
+
// AgentTool progress is a replacing snapshot (web_search sends its accumulated text), but Pi output only
|
|
172
|
+
// appends and has no replace operation: append a grown snapshot's suffix, and start a new line otherwise.
|
|
173
|
+
let shown = "";
|
|
174
|
+
const onUpdate = (update: AgentToolResult<unknown>) => {
|
|
175
|
+
const text = update.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("");
|
|
176
|
+
if (text && text !== shown) {
|
|
177
|
+
api.output(text.startsWith(shown) ? text.slice(shown.length) : shown ? `\n${text}` : text);
|
|
178
|
+
shown = text;
|
|
179
|
+
}
|
|
180
|
+
if (update.details !== undefined) void api.details(update.details, context).catch(() => {});
|
|
181
|
+
};
|
|
182
|
+
const signal = context.abortSignal ?? new AbortController().signal;
|
|
183
|
+
let result: AgentToolResult<unknown>;
|
|
184
|
+
try {
|
|
185
|
+
await options.authorize?.(candidate, args, {
|
|
186
|
+
toolCallId,
|
|
187
|
+
signal,
|
|
188
|
+
});
|
|
189
|
+
const replay = await options.resolveReplay?.(
|
|
190
|
+
candidate,
|
|
191
|
+
toolCallId,
|
|
192
|
+
args,
|
|
193
|
+
);
|
|
194
|
+
if (replay) return durableResult(replay, candidate, options.workspace);
|
|
195
|
+
signal.throwIfAborted();
|
|
196
|
+
const execution = candidate.confirmation
|
|
197
|
+
? (() => {
|
|
198
|
+
const confirmed = options.resolveConfirmation?.(candidate, toolCallId, args);
|
|
199
|
+
if (!confirmed) throw new Error("User confirmation is required");
|
|
200
|
+
return candidate.confirmation.execute(confirmed.prepared, { intentId: confirmed.intentId, signal });
|
|
201
|
+
})()
|
|
202
|
+
: source.execute(toolCallId, args, signal, (update) => onUpdate(update));
|
|
203
|
+
result = await awaitWithContext(
|
|
204
|
+
Promise.resolve(execution).then((result) => durableResult(result, candidate, options.workspace)),
|
|
205
|
+
context,
|
|
206
|
+
);
|
|
207
|
+
} catch (cause) {
|
|
208
|
+
const message = errorMessage(cause);
|
|
209
|
+
const failed = boundDurableToolOutput({
|
|
210
|
+
content: [{ type: "text", text: message }],
|
|
211
|
+
details: {},
|
|
212
|
+
}) as AgentToolResult<unknown>;
|
|
213
|
+
await options.onSettlement?.({
|
|
214
|
+
toolCallId,
|
|
215
|
+
toolName: source.name,
|
|
216
|
+
args,
|
|
217
|
+
result: failed,
|
|
218
|
+
isError: true,
|
|
219
|
+
cause,
|
|
220
|
+
summary: candidate.summary,
|
|
221
|
+
source: candidate.source,
|
|
222
|
+
});
|
|
223
|
+
if (cause instanceof UnattendedExecutionError) {
|
|
224
|
+
return { ...failed, isError: true, control: { terminate: true as const } };
|
|
225
|
+
}
|
|
226
|
+
throw cause instanceof Error && cause.message === message
|
|
227
|
+
? cause
|
|
228
|
+
: new Error(message, { cause });
|
|
229
|
+
}
|
|
230
|
+
await options.onSettlement?.({
|
|
231
|
+
toolCallId,
|
|
232
|
+
toolName: source.name,
|
|
233
|
+
args,
|
|
234
|
+
result,
|
|
235
|
+
isError: result.details !== null &&
|
|
236
|
+
typeof result.details === "object" &&
|
|
237
|
+
(result.details as { availability?: unknown }).availability ===
|
|
238
|
+
"unavailable",
|
|
239
|
+
summary: candidate.summary,
|
|
240
|
+
source: candidate.source,
|
|
241
|
+
});
|
|
242
|
+
return { ...result, ...(result.terminate ? { control: { terminate: true as const } } : {}) };
|
|
243
|
+
},
|
|
244
|
+
} satisfies ToolRegistration<any, any>;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { TSchema } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { ApprovalReceipt } from "../../submission/contracts";
|
|
4
|
+
import { type BudgetPolicy } from "../../context/memory/budget/gate";
|
|
5
|
+
import { type SpillWorkspace } from "../../lib/artifacts";
|
|
6
|
+
import { type ExecutionLevel } from "../../lib/execution-level";
|
|
7
|
+
|
|
8
|
+
// 本文件沿用 `src/index.ts` 入口定义的 Tool Candidate 和 Tool Settlement 术语。
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 声明一个 Tool 的结果由客户端投递,而不是由 `execute()` 产出。
|
|
13
|
+
*
|
|
14
|
+
* 带这个字段的 Tool 会在执行链里 park 住,等宿主经 `respondToolInteraction` 把响应送进来,
|
|
15
|
+
* 再用 `settle` 把响应映射成这次 `toolCallId` 的权威 ToolResult。它的 `execute` 不会被调用。
|
|
16
|
+
*
|
|
17
|
+
* 这是 AI SDK 里「没有 execute 的 user-interaction tool」的等价物;差别是我们 park 住整个 Turn,
|
|
18
|
+
* 而不是让 tool part 悬空到下一次请求 —— 我们的 transcript 是服务端权威的,悬空的 tool_use
|
|
19
|
+
* 会让下一次模型调用非法。
|
|
20
|
+
*/
|
|
21
|
+
export interface PiToolInteractionSpec {
|
|
22
|
+
/** 结合 Tool 输入校验客户端响应体;不通过则拒绝投递,park 保持不变。 */
|
|
23
|
+
readonly validateResponse: (input: unknown, response: unknown) => boolean;
|
|
24
|
+
/** 把通过校验的响应映射成 ToolResult;缺省是原样回传。 */
|
|
25
|
+
readonly settle?: (
|
|
26
|
+
input: unknown,
|
|
27
|
+
response: unknown,
|
|
28
|
+
) => Omit<AgentToolResult<unknown>, "terminate">;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Trusted Tool metadata used to evaluate a scheduled task grant. */
|
|
32
|
+
export interface PiToolUnattendedExecutionSpec {
|
|
33
|
+
readonly capability: string;
|
|
34
|
+
readonly operation: string;
|
|
35
|
+
readonly objectForInput?: (
|
|
36
|
+
input: unknown,
|
|
37
|
+
) => unknown | Promise<unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 描述一个尚未进入最终 Tool Surface 和结算包装的 Pi 工具。 */
|
|
41
|
+
export type ToolExposureMode = "direct" | "codemode" | "both";
|
|
42
|
+
|
|
43
|
+
/** Host-prepared confirmation travels through the existing durable approval. */
|
|
44
|
+
export interface PiToolConfirmationSpec {
|
|
45
|
+
prepare(input: unknown, context: { intentId: string }): Promise<unknown>;
|
|
46
|
+
execute(prepared: unknown, context: { intentId: string; signal: AbortSignal }): Promise<AgentToolResult<unknown>>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 一份尚未进入最终 Tool Surface 的 Pi 工具及其治理字段。
|
|
51
|
+
*
|
|
52
|
+
* 单个工厂可以写成 `PiToolCandidate<typeof parameters, Details>`,把 Schema 和
|
|
53
|
+
* details 留在类型里。混进同一个 `PiToolCandidate[]` 时走默认擦除形式:
|
|
54
|
+
* `AgentTool` 在两个类型参数上都是不变的,只有默认 `any` 能收进异构列表。
|
|
55
|
+
*/
|
|
56
|
+
export interface PiToolCandidate<
|
|
57
|
+
TParameters extends TSchema = any,
|
|
58
|
+
TDetails = any,
|
|
59
|
+
> {
|
|
60
|
+
/** Presentation only, resolved at the model boundary; never changes approved execution inputs. */
|
|
61
|
+
readonly modelGuidance?: () => Promise<{ description: string; systemPrompt?: string }>;
|
|
62
|
+
readonly confirmation?: PiToolConfirmationSpec;
|
|
63
|
+
readonly owner: string;
|
|
64
|
+
readonly tool: AgentTool<TParameters, TDetails>;
|
|
65
|
+
/** @internal Send the complete schema but hide it until provider Tool Search finds it. */
|
|
66
|
+
readonly deferLoading?: true;
|
|
67
|
+
/** Omitted means visible both directly and through Code Mode. */
|
|
68
|
+
readonly exposureMode?: ToolExposureMode;
|
|
69
|
+
/** Conservative maximum used in the stable Runtime descriptor. */
|
|
70
|
+
readonly requiredExecutionLevel: ExecutionLevel;
|
|
71
|
+
/** Trusted parameter-level policy, evaluated before approval or dispatch. */
|
|
72
|
+
readonly requiredExecutionLevelForInput?: (
|
|
73
|
+
input: unknown,
|
|
74
|
+
) => ExecutionLevel | Promise<ExecutionLevel>;
|
|
75
|
+
/**
|
|
76
|
+
* 无条件需要人来决定:不看执行档位矩阵,每次调用都 park 出审批。
|
|
77
|
+
*
|
|
78
|
+
* 档位差是「够不够格自己跑」,不是「该不该问人」。花用户钱、改变账户
|
|
79
|
+
* 状态这类工具,一旦 Agent 档位被 `allow_level` 永久拉高就会静默执行,
|
|
80
|
+
* 那是失效不是降级。`deny` 仍然优先,被 deny 的工具根本不会注入。
|
|
81
|
+
*/
|
|
82
|
+
readonly alwaysRequiresApproval?: boolean;
|
|
83
|
+
/** Declares the concrete grant identity derived from actual Tool input. */
|
|
84
|
+
readonly unattendedExecution?: PiToolUnattendedExecutionSpec;
|
|
85
|
+
/** Explicitly forbids this Tool from unattended Scheduled Runs. */
|
|
86
|
+
readonly unattendedMode?: "forbidden";
|
|
87
|
+
/** Recovery may replay this Tool Call only when the candidate guarantees convergence. Omitted means non-idempotent. */
|
|
88
|
+
readonly retry?: "idempotent" | "non-idempotent";
|
|
89
|
+
readonly summary?: string;
|
|
90
|
+
readonly source?: ApprovalReceipt["source"];
|
|
91
|
+
readonly interaction?: PiToolInteractionSpec;
|
|
92
|
+
/** Large payloads spill by default; protocol-shaped results opt into structure. */
|
|
93
|
+
readonly outputBudget?: BudgetPolicy;
|
|
94
|
+
/**
|
|
95
|
+
* 编进 Harness 工具表,但不进入初始 `setActiveTools`。
|
|
96
|
+
* 之后由其他 Tool 的 `toolsToActivate` 经 `setActiveTools` 写入 transcript `toolsAdded`。
|
|
97
|
+
*/
|
|
98
|
+
readonly inactiveUntilActivated?: true;
|
|
99
|
+
/**
|
|
100
|
+
* 本次成功结果要把哪些已编译工具加入活动集。未知名字会被忽略。
|
|
101
|
+
* 替代已删除的 `AgentToolResult.addedToolNames`。
|
|
102
|
+
*/
|
|
103
|
+
readonly toolsToActivate?: (
|
|
104
|
+
result: AgentToolResult<unknown>,
|
|
105
|
+
) => readonly string[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface CompileHarnessToolsOptions {
|
|
109
|
+
readonly workspace?: SpillWorkspace;
|
|
110
|
+
readonly resolveConfirmation?: (candidate: PiToolCandidate, toolCallId: string, args: unknown) => { prepared: unknown; intentId: string };
|
|
111
|
+
/** Recheck live authorization at the effect boundary, including safe replay. */
|
|
112
|
+
readonly authorize?: (
|
|
113
|
+
candidate: PiToolCandidate,
|
|
114
|
+
args: unknown,
|
|
115
|
+
invocation: { readonly toolCallId: string; readonly signal: AbortSignal },
|
|
116
|
+
) => void | Promise<void>;
|
|
117
|
+
/** Return an already decided interaction result without invoking the handler. */
|
|
118
|
+
readonly resolveReplay?: (
|
|
119
|
+
candidate: PiToolCandidate,
|
|
120
|
+
toolCallId: string,
|
|
121
|
+
args: unknown,
|
|
122
|
+
) => AgentToolResult<unknown> | undefined | Promise<AgentToolResult<unknown> | undefined>;
|
|
123
|
+
/** Optional business projection; Pi remains the authoritative Tool result. */
|
|
124
|
+
readonly onSettlement?: (
|
|
125
|
+
call: SettledPiToolCall,
|
|
126
|
+
) => void | Promise<void>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 描述一次已进入持久化结算边界的 Pi 工具调用。 */
|
|
130
|
+
export interface SettledPiToolCall {
|
|
131
|
+
readonly toolCallId: string;
|
|
132
|
+
readonly toolName: string;
|
|
133
|
+
readonly args: unknown;
|
|
134
|
+
readonly result: AgentToolResult<unknown>;
|
|
135
|
+
readonly isError: boolean;
|
|
136
|
+
/** Diagnostic-only original error; callers must not persist or expose it. */
|
|
137
|
+
readonly cause?: unknown;
|
|
138
|
+
readonly summary?: string;
|
|
139
|
+
readonly source?: ApprovalReceipt["source"];
|
|
140
|
+
}
|