@springbrand/agent-runtime 0.3.0-alpha.7 → 0.3.0-alpha.9
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 -2
- package/src/adapter/cloudflare/agent.ts +4 -7
- package/src/adapter/cloudflare/define-agent.ts +2 -0
- package/src/adapter/cloudflare/schema.ts +3 -0
- package/src/adapter/cloudflare/subagent/definition.ts +4 -4
- package/src/adapter/cloudflare/subagent/runner.ts +1 -1
- package/src/adapter/cloudflare/subagent/tools.ts +2 -2
- 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/agent-context.ts +1 -1
- package/src/config/assembler.ts +17 -31
- package/src/config/assembly-snapshot.ts +1 -1
- package/src/config/definition.ts +1 -1
- package/src/config/model-guidance.ts +1 -1
- package/src/config/ports.ts +5 -15
- package/src/config/public-contracts.ts +2 -2
- package/src/context/extension/assembly.ts +2 -2
- package/src/context/memory/budget/gate.ts +1 -1
- package/src/conversation/state.ts +1 -1
- package/src/index.ts +7 -7
- package/src/lib/execution-level.ts +13 -0
- package/src/lib/prompt.ts +3 -5
- package/src/models/index.ts +1 -0
- package/src/session/cloudflare/migration.ts +29 -8
- package/src/skills/springbrand-worker-website/index.ts +1 -1
- package/src/submission/admission.ts +4 -0
- package/src/submission/delivery.ts +1 -1
- package/src/submission/finalization.ts +1 -1
- package/src/submission/repository.ts +8 -1
- package/src/telemetry/coordinator.ts +1 -1
- package/src/telemetry/subagent-lifecycle.ts +1 -1
- package/src/telemetry/turn-events.ts +1 -1
- 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} +2 -2
- package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +2 -2
- package/src/tools/assistant/ask-user.ts +317 -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 -49
- 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 +28 -26
- package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
- package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
- package/src/tools/{approval → runtime/approval}/repository.ts +3 -9
- package/src/tools/{compiler.ts → runtime/compiler.ts} +24 -155
- package/src/tools/runtime/contracts.ts +117 -0
- package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +36 -61
- 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/sandbox/index.ts +228 -0
- package/src/tools/{schedule.ts → schedule/index.ts} +50 -49
- 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 +1 -1
- 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 +1320 -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 -542
- 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/{unattended-error.ts → runtime/unattended-error.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,17 +1,14 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
HookHandler,
|
|
4
|
-
} from "@earendil-works/pi-agent-core";
|
|
5
|
-
import type { ExecutionLevel } from "../lib/execution-level";
|
|
1
|
+
import type { AgentToolResult, HookHandler } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ExecutionLevel } from "../../lib/execution-level";
|
|
6
3
|
import {
|
|
7
4
|
EXECUTION_LEVELS,
|
|
5
|
+
minExecutionLevel,
|
|
8
6
|
requiresExecutionApproval,
|
|
9
|
-
} from "
|
|
10
|
-
import type {
|
|
7
|
+
} from "../../lib/execution-level";
|
|
8
|
+
import type { RuntimeTelemetryCoordinator } from "../../telemetry/coordinator";
|
|
11
9
|
import type { ApprovalRepository } from "./approval";
|
|
10
|
+
import type { PiToolCandidate } from "./contracts";
|
|
12
11
|
import type { ToolInteractionRepository } from "./interaction";
|
|
13
|
-
import type { RuntimeTelemetryCoordinator } from "../telemetry/coordinator";
|
|
14
|
-
import type { RuntimeUnattendedExecutionGrant } from "../submission/contracts";
|
|
15
12
|
import { UnattendedExecutionError } from "./unattended-error";
|
|
16
13
|
|
|
17
14
|
export interface HarnessDecisionSubmission {
|
|
@@ -19,7 +16,10 @@ export interface HarnessDecisionSubmission {
|
|
|
19
16
|
readonly requestId: string;
|
|
20
17
|
readonly executionLevel: ExecutionLevel;
|
|
21
18
|
readonly origin?: "user" | "scheduled";
|
|
22
|
-
|
|
19
|
+
/** Maximum level allowed by the owning Schedule Definition. */
|
|
20
|
+
readonly scheduledMaxExecutionLevel?: ExecutionLevel;
|
|
21
|
+
/** @deprecated Retained for rolling upgrades; ignored by the decision gate. */
|
|
22
|
+
readonly unattendedExecutionGrant?: unknown;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export interface HarnessDecisionGateOptions {
|
|
@@ -41,17 +41,8 @@ export interface HarnessDecisionGateOptions {
|
|
|
41
41
|
readonly requiredExecutionLevel: ExecutionLevel;
|
|
42
42
|
readonly signal: AbortSignal;
|
|
43
43
|
}) => void | Promise<void>;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
capability: string;
|
|
47
|
-
operation: string;
|
|
48
|
-
object?: unknown;
|
|
49
|
-
toolCallId: string;
|
|
50
|
-
toolName: string;
|
|
51
|
-
input: unknown;
|
|
52
|
-
requiredExecutionLevel: ExecutionLevel;
|
|
53
|
-
signal: AbortSignal;
|
|
54
|
-
}) => void | Promise<void>;
|
|
44
|
+
/** @deprecated Scheduled authorization is local and never calls this hook. */
|
|
45
|
+
readonly authorizeUnattendedExecution?: (input: unknown) => void | Promise<void>;
|
|
55
46
|
readonly onUnattendedFailure?: (reason: string) => void | Promise<void>;
|
|
56
47
|
readonly unattendedFailure?: () => string | null | undefined;
|
|
57
48
|
}
|
|
@@ -105,11 +96,9 @@ export class HarnessDecisionGate {
|
|
|
105
96
|
if (!(error instanceof UnattendedExecutionError)) throw error;
|
|
106
97
|
return { block: { reason: error.message, terminate: true } };
|
|
107
98
|
}
|
|
99
|
+
const effectiveLevel = this.effectiveExecutionLevel();
|
|
108
100
|
const needsApproval = candidate.alwaysRequiresApproval ||
|
|
109
|
-
requiresExecutionApproval(
|
|
110
|
-
this.options.submission.executionLevel,
|
|
111
|
-
requiredExecutionLevel,
|
|
112
|
-
);
|
|
101
|
+
requiresExecutionApproval(effectiveLevel, requiredExecutionLevel);
|
|
113
102
|
const executionId = `${this.options.submission.submissionId}:${event.toolCallId}`;
|
|
114
103
|
let unattendedAuthorized = false;
|
|
115
104
|
try {
|
|
@@ -240,6 +229,14 @@ export class HarnessDecisionGate {
|
|
|
240
229
|
if (failure) throw new UnattendedExecutionError(failure);
|
|
241
230
|
}
|
|
242
231
|
|
|
232
|
+
private effectiveExecutionLevel(): ExecutionLevel {
|
|
233
|
+
const submission = this.options.submission;
|
|
234
|
+
const ceiling = submission.origin === "scheduled"
|
|
235
|
+
? submission.scheduledMaxExecutionLevel ?? "high"
|
|
236
|
+
: submission.executionLevel;
|
|
237
|
+
return minExecutionLevel(submission.executionLevel, ceiling);
|
|
238
|
+
}
|
|
239
|
+
|
|
243
240
|
private async resolveExecutionLevel(candidate: PiToolCandidate, input: unknown): Promise<ExecutionLevel> {
|
|
244
241
|
this.assertUnattendedActive();
|
|
245
242
|
try {
|
|
@@ -253,7 +250,9 @@ export class HarnessDecisionGate {
|
|
|
253
250
|
} catch (error) {
|
|
254
251
|
if (this.options.submission.origin !== "scheduled") throw error;
|
|
255
252
|
const reason = error instanceof Error ? error.message : String(error);
|
|
256
|
-
return this.denyUnattended(
|
|
253
|
+
return this.denyUnattended(
|
|
254
|
+
`SCHEDULED_EXECUTION_RISK_CHECK_FAILED: ${candidate.tool.name}: ${reason}`,
|
|
255
|
+
);
|
|
257
256
|
}
|
|
258
257
|
}
|
|
259
258
|
|
|
@@ -265,52 +264,28 @@ export class HarnessDecisionGate {
|
|
|
265
264
|
): Promise<boolean> {
|
|
266
265
|
if (this.options.submission.origin !== "scheduled") return false;
|
|
267
266
|
this.assertUnattendedActive();
|
|
268
|
-
const spec = candidate.unattendedExecution;
|
|
269
267
|
if (candidate.interaction) {
|
|
270
268
|
return this.denyUnattended(
|
|
271
|
-
`
|
|
269
|
+
`SCHEDULED_EXECUTION_REQUIRES_INTERACTION: ${candidate.tool.name}`,
|
|
272
270
|
);
|
|
273
271
|
}
|
|
274
272
|
if (candidate.alwaysRequiresApproval) {
|
|
275
273
|
return this.denyUnattended(
|
|
276
|
-
`
|
|
274
|
+
`SCHEDULED_EXECUTION_ALWAYS_REQUIRES_APPROVAL: ${candidate.tool.name}`,
|
|
277
275
|
);
|
|
278
276
|
}
|
|
279
|
-
if (
|
|
277
|
+
if (candidate.unattendedMode === "forbidden") {
|
|
280
278
|
return this.denyUnattended(
|
|
281
|
-
`
|
|
279
|
+
`SCHEDULED_EXECUTION_FORBIDDEN: ${candidate.tool.name}`,
|
|
282
280
|
);
|
|
283
281
|
}
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
await this.options.onUnattendedFailure?.(reason);
|
|
290
|
-
throw new UnattendedExecutionError(reason);
|
|
291
|
-
}
|
|
292
|
-
try {
|
|
293
|
-
await authorize({
|
|
294
|
-
grant,
|
|
295
|
-
capability: spec.capability,
|
|
296
|
-
operation: spec.operation,
|
|
297
|
-
...(spec.objectForInput
|
|
298
|
-
? { object: await spec.objectForInput(input) }
|
|
299
|
-
: {}),
|
|
300
|
-
toolCallId: invocation.toolCallId,
|
|
301
|
-
toolName: candidate.tool.name,
|
|
302
|
-
input,
|
|
303
|
-
requiredExecutionLevel,
|
|
304
|
-
signal: invocation.signal,
|
|
305
|
-
});
|
|
306
|
-
return true;
|
|
307
|
-
} catch (error) {
|
|
308
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
309
|
-
const message =
|
|
310
|
-
`Unattended execution denied for ${spec.capability}.${spec.operation}: ${reason}`;
|
|
311
|
-
await this.options.onUnattendedFailure?.(message);
|
|
312
|
-
throw new UnattendedExecutionError(message, { cause: error });
|
|
282
|
+
const effectiveLevel = this.effectiveExecutionLevel();
|
|
283
|
+
if (requiresExecutionApproval(effectiveLevel, requiredExecutionLevel)) {
|
|
284
|
+
return this.denyUnattended(
|
|
285
|
+
`SCHEDULED_EXECUTION_LEVEL_EXCEEDED: ${candidate.tool.name} requires ${requiredExecutionLevel}; effective level is ${effectiveLevel}`,
|
|
286
|
+
);
|
|
313
287
|
}
|
|
288
|
+
return true;
|
|
314
289
|
}
|
|
315
290
|
|
|
316
291
|
private async denyUnattended(reason: string): Promise<never> {
|
|
@@ -322,7 +297,7 @@ export class HarnessDecisionGate {
|
|
|
322
297
|
const intentId = `${this.options.submission.submissionId}:${toolCallId}`;
|
|
323
298
|
const approval = this.options.approvals.find(intentId);
|
|
324
299
|
if (!approval || approval.status !== "approved" || !approval.confirmationJson ||
|
|
325
|
-
|
|
300
|
+
approval.toolName !== candidate.tool.name || approval.inputJson !== stableInput(args)) {
|
|
326
301
|
throw new Error("Matching user confirmation is required");
|
|
327
302
|
}
|
|
328
303
|
return { prepared: JSON.parse(approval.confirmationJson), intentId };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { ApprovalDecision } from "
|
|
2
|
-
import type { RuntimeTelemetryCoordinator } from "
|
|
1
|
+
import type { ApprovalDecision } from "../../submission/contracts";
|
|
2
|
+
import type { RuntimeTelemetryCoordinator } from "../../telemetry/coordinator";
|
|
3
3
|
import type { ApprovalRepository } from "./approval/repository";
|
|
4
|
-
import type { PiToolInteractionSpec } from "./
|
|
4
|
+
import type { PiToolInteractionSpec } from "./contracts";
|
|
5
5
|
import type { StoredToolInteraction, ToolInteractionRepository } from "./interaction/repository";
|
|
6
6
|
|
|
7
7
|
/** Human decisions own validation and durable facts; the caller resumes Pi. */
|
|
@@ -12,7 +12,7 @@ export class HarnessHumanDecisions {
|
|
|
12
12
|
readonly transaction: <T>(run: () => T) => T;
|
|
13
13
|
readonly isSubmissionActive: (submissionId: string) => boolean;
|
|
14
14
|
readonly telemetry: Pick<RuntimeTelemetryCoordinator, "capture">;
|
|
15
|
-
}) {}
|
|
15
|
+
}) { }
|
|
16
16
|
|
|
17
17
|
isWaitingForApproval(submissionId: string): boolean {
|
|
18
18
|
return this.options.approvals.listPendingForSubmission(submissionId).length > 0;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
2
|
|
|
3
3
|
// Tool Interaction 的统一定义见 ./index.ts。
|
|
4
|
-
// #region 状态与行类型
|
|
5
4
|
export type ToolInteractionStatus = "pending" | "responded" | "cancelled";
|
|
6
5
|
|
|
7
6
|
export interface StoredToolInteraction {
|
|
@@ -41,8 +40,6 @@ type ToolInteractionRow = {
|
|
|
41
40
|
responded_at: number | null;
|
|
42
41
|
};
|
|
43
42
|
|
|
44
|
-
// #endregion
|
|
45
|
-
// #region 行映射
|
|
46
43
|
// 把 SQLite 的 Interaction 行转成 Runtime 对象。
|
|
47
44
|
// find 与 findPending* 查到记录后调用,上层因而不需要理解 snake_case 列名。
|
|
48
45
|
// 状态由表内字符串转成联合类型;新增列或状态时必须同步核对类型、SELECT 和映射。
|
|
@@ -61,13 +58,11 @@ function mapRow(row: ToolInteractionRow): StoredToolInteraction {
|
|
|
61
58
|
};
|
|
62
59
|
}
|
|
63
60
|
|
|
64
|
-
// #endregion
|
|
65
|
-
// #region Repository
|
|
66
61
|
export class ToolInteractionRepository {
|
|
67
62
|
// 保存当前 Agent SQLite 的查询入口。
|
|
68
63
|
// RuntimeDatabase 构造时调用,InteractionLifecycle 随后共享这个 Repository。
|
|
69
64
|
// Interaction 必须与所属 Submission 在同一 Agent 存储中查询,不能跨 Durable Object 拼接状态。
|
|
70
|
-
constructor(private readonly sql: SqlTaggedTemplate) {}
|
|
65
|
+
constructor(private readonly sql: SqlTaggedTemplate) { }
|
|
71
66
|
|
|
72
67
|
// 按 interactionId 读取一条完整 Interaction,不存在时返回 null。
|
|
73
68
|
// InteractionLifecycle 在请求去重、响应、取消和收尾时调用,调用方必须处理 null。
|
|
@@ -182,4 +177,3 @@ export class ToolInteractionRepository {
|
|
|
182
177
|
`.map(mapRow);
|
|
183
178
|
}
|
|
184
179
|
}
|
|
185
|
-
// #endregion
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { serializeOutput } from "../../lib/artifacts";
|
|
3
|
+
|
|
4
|
+
export function result<T>(details: T): AgentToolResult<T> {
|
|
5
|
+
return {
|
|
6
|
+
content: [{ type: "text", text: serializeOutput(details).text }],
|
|
7
|
+
details,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
// 作用:向 Pi 报告一次 Workspace 或 Sandbox 操作已经开始。
|
|
11
|
+
// 调用:可能访问存储或容器的 execute 方法在进入 Port 前调用。
|
|
12
|
+
// 原因:复用统一的 running 结果形状,
|
|
13
|
+
// 让长操作有进度反馈而不复制回调协议。
|
|
14
|
+
export function running(
|
|
15
|
+
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
16
|
+
text: string,
|
|
17
|
+
): void {
|
|
18
|
+
onUpdate?.({
|
|
19
|
+
content: [{ type: "text", text }],
|
|
20
|
+
details: { status: "running" },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { RuntimeSandboxPort, WorkspacePort } from "../../config/ports";
|
|
4
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
5
|
+
import { result, running } from "../runtime/result";
|
|
6
|
+
|
|
7
|
+
/** 从 {@link WorkspacePort} 生成标准 workspace 文件 Tool 集(read / write / edit …)。 */
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const sandboxCommandParameters = {
|
|
12
|
+
command: Type.String({
|
|
13
|
+
minLength: 1,
|
|
14
|
+
maxLength: 32_768,
|
|
15
|
+
description: "Shell command to run in the isolated Linux Sandbox.",
|
|
16
|
+
}),
|
|
17
|
+
cwd: Type.Optional(Type.String({
|
|
18
|
+
maxLength: 4_096,
|
|
19
|
+
description: "Sandbox working directory. Omit to use the Sandbox default.",
|
|
20
|
+
})),
|
|
21
|
+
stdin: Type.Optional(Type.String({
|
|
22
|
+
maxLength: 65_536,
|
|
23
|
+
description: "Optional text to pass to the command on standard input.",
|
|
24
|
+
})),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const sandboxExecParameters = Type.Object({
|
|
28
|
+
...sandboxCommandParameters,
|
|
29
|
+
timeoutMs: Type.Optional(
|
|
30
|
+
Type.Integer({
|
|
31
|
+
minimum: 1,
|
|
32
|
+
maximum: 60_000,
|
|
33
|
+
description: "Maximum command runtime in milliseconds, from 1 to 60000.",
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
}, { additionalProperties: false });
|
|
37
|
+
|
|
38
|
+
const sandboxStartParameters = Type.Object(
|
|
39
|
+
sandboxCommandParameters,
|
|
40
|
+
{ additionalProperties: false },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const sandboxProcessParameters = Type.Object({
|
|
44
|
+
id: Type.String({
|
|
45
|
+
minLength: 1,
|
|
46
|
+
maxLength: 128,
|
|
47
|
+
description: "Process ID returned by sandbox_start_process.",
|
|
48
|
+
}),
|
|
49
|
+
}, { additionalProperties: false });
|
|
50
|
+
|
|
51
|
+
const sandboxPublishParameters = Type.Object({
|
|
52
|
+
paths: Type.Array(Type.String({
|
|
53
|
+
minLength: 1,
|
|
54
|
+
maxLength: 4_096,
|
|
55
|
+
description: "Sandbox file path to publish.",
|
|
56
|
+
}), {
|
|
57
|
+
minItems: 1,
|
|
58
|
+
maxItems: 100,
|
|
59
|
+
description: "Files to copy from the temporary Sandbox into the persistent Workspace.",
|
|
60
|
+
}),
|
|
61
|
+
}, { additionalProperties: false });
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 为现有 RuntimeSandboxPort 创建 Pi 命令、进程和文件发布工具。
|
|
65
|
+
*
|
|
66
|
+
* Worker 的 Sandbox Plugin 在容器能力和持久 Workspace 都可用时调用它。调用方
|
|
67
|
+
* 应传入已绑定当前会话的 Port;前台命令适合一次性执行,后台进程适合需要持续
|
|
68
|
+
* 运行的服务,二者不能互换结果语义。
|
|
69
|
+
*
|
|
70
|
+
* 这里的 “Sandbox” 指 Cloudflare Sandbox SDK 提供的隔离 Linux 容器;本层只
|
|
71
|
+
* 面向项目自己的 Port。命令限制、cwd 校验、工作区装载、超时、取消、清理和
|
|
72
|
+
* 冲突安全发布仍在 Port 内。容器文件不会自动写回 Workspace,只有 publish
|
|
73
|
+
* 工具显式选中的文件才进入持久视图;执行、启动、停止和发布继续走高风险审批。
|
|
74
|
+
*
|
|
75
|
+
* `Pi`、`Port` 与“候选工具”等项目核心术语见 `src/index.ts`。
|
|
76
|
+
*/
|
|
77
|
+
export function sandboxPiToolCandidates(
|
|
78
|
+
sandbox: RuntimeSandboxPort,
|
|
79
|
+
): PiToolCandidate[] {
|
|
80
|
+
const exec: AgentTool<typeof sandboxExecParameters> = {
|
|
81
|
+
name: "sandbox_exec",
|
|
82
|
+
label: "Run command",
|
|
83
|
+
description:
|
|
84
|
+
"Run a foreground command in this Chat's isolated Linux Sandbox. Files stay temporary until sandbox_publish_files is called.",
|
|
85
|
+
parameters: sandboxExecParameters,
|
|
86
|
+
// 作用:在 Sandbox 中运行一次前台命令并等待完整退出结果。
|
|
87
|
+
// 调用:Pi 执行构建、测试或其他会结束的命令时调用。
|
|
88
|
+
// 原因:输入和 AbortSignal 原样交给 Port,返回值只投影公开字段,
|
|
89
|
+
// 避免容器内部句柄进入对话。
|
|
90
|
+
async execute(_toolCallId, input, signal, onUpdate) {
|
|
91
|
+
signal?.throwIfAborted();
|
|
92
|
+
running(onUpdate, "Running the Sandbox command.");
|
|
93
|
+
const output = await sandbox.exec(input, signal);
|
|
94
|
+
return result({
|
|
95
|
+
success: output.success,
|
|
96
|
+
stdout: output.stdout,
|
|
97
|
+
stderr: output.stderr,
|
|
98
|
+
exitCode: output.exitCode,
|
|
99
|
+
stdoutTruncated: output.stdoutTruncated,
|
|
100
|
+
stderrTruncated: output.stderrTruncated,
|
|
101
|
+
workspacePersistence: "explicit_publish_required",
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const startProcess: AgentTool<typeof sandboxStartParameters> = {
|
|
107
|
+
name: "sandbox_start_process",
|
|
108
|
+
label: "Start process",
|
|
109
|
+
description:
|
|
110
|
+
"Start a long-running background process in this Chat's isolated Linux Sandbox.",
|
|
111
|
+
parameters: sandboxStartParameters,
|
|
112
|
+
// 作用:在 Sandbox 中启动一个长期运行的后台进程并返回进程标识。
|
|
113
|
+
// 调用:Pi 启动开发服务器、监听器或其他不能等待退出的程序时调用。
|
|
114
|
+
// 原因:Cloudflare 官方接口把长期进程与 exec 分开;
|
|
115
|
+
// 这里只公开后续查日志和停止所需字段。
|
|
116
|
+
async execute(_toolCallId, input, signal, onUpdate) {
|
|
117
|
+
signal?.throwIfAborted();
|
|
118
|
+
running(onUpdate, "Starting the Sandbox process.");
|
|
119
|
+
const process = await sandbox.startProcess(input);
|
|
120
|
+
return result({
|
|
121
|
+
id: process.id,
|
|
122
|
+
command: process.command,
|
|
123
|
+
status: process.status,
|
|
124
|
+
...(process.exitCode === undefined
|
|
125
|
+
? {}
|
|
126
|
+
: { exitCode: process.exitCode }),
|
|
127
|
+
workspacePersistence: "explicit_publish_required",
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const processLogs: AgentTool<typeof sandboxProcessParameters> = {
|
|
133
|
+
name: "sandbox_process_logs",
|
|
134
|
+
label: "Read process logs",
|
|
135
|
+
description:
|
|
136
|
+
"Read bounded stdout and stderr logs for one background Sandbox process.",
|
|
137
|
+
parameters: sandboxProcessParameters,
|
|
138
|
+
// 作用:读取一个 Sandbox 后台进程当前的有界日志。
|
|
139
|
+
// 调用:Pi 拿到 startProcess 返回的 id 后检查启动或运行状态时调用。
|
|
140
|
+
// 原因:日志截断由 Port 统一处理并显式回传标记,
|
|
141
|
+
// 避免适配层再次缓存不断增长的输出。
|
|
142
|
+
async execute(_toolCallId, { id }, signal, onUpdate) {
|
|
143
|
+
signal?.throwIfAborted();
|
|
144
|
+
running(onUpdate, "Reading the Sandbox process logs.");
|
|
145
|
+
const logs = await sandbox.getProcessLogs(id);
|
|
146
|
+
return result({
|
|
147
|
+
id: logs.id,
|
|
148
|
+
stdout: logs.stdout,
|
|
149
|
+
stderr: logs.stderr,
|
|
150
|
+
stdoutTruncated: logs.stdoutTruncated,
|
|
151
|
+
stderrTruncated: logs.stderrTruncated,
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const stopProcess: AgentTool<typeof sandboxProcessParameters> = {
|
|
157
|
+
name: "sandbox_stop_process",
|
|
158
|
+
label: "Stop process",
|
|
159
|
+
description: "Stop one background Sandbox process.",
|
|
160
|
+
parameters: sandboxProcessParameters,
|
|
161
|
+
// 作用:停止一个由当前 Sandbox 管理的后台进程。
|
|
162
|
+
// 调用:Pi 不再需要该进程或需要清理运行环境时调用。
|
|
163
|
+
// 原因:只把进程 id 交给 Port,
|
|
164
|
+
// 进程归属检查和实际清理由 Sandbox 边界统一负责。
|
|
165
|
+
async execute(_toolCallId, { id }, signal, onUpdate) {
|
|
166
|
+
signal?.throwIfAborted();
|
|
167
|
+
running(onUpdate, "Stopping the Sandbox process.");
|
|
168
|
+
const output = await sandbox.stopProcess(id);
|
|
169
|
+
return result({ stopped: output.stopped });
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const publishFiles: AgentTool<typeof sandboxPublishParameters> = {
|
|
174
|
+
name: "sandbox_publish_files",
|
|
175
|
+
label: "Publish files",
|
|
176
|
+
description:
|
|
177
|
+
"Publish named files from the temporary Sandbox into the scoped Workspace. Shared files remain read-only.",
|
|
178
|
+
parameters: sandboxPublishParameters,
|
|
179
|
+
// 作用:把明确列出的 Sandbox 文件发布到持久 Workspace。
|
|
180
|
+
// 调用:Pi 确认命令产物需要保留到会话文件区时调用。
|
|
181
|
+
// 原因:显式发布避免临时容器状态被误当成已持久化;
|
|
182
|
+
// 冲突、共享写保护和失败码继续由 Port 决定。
|
|
183
|
+
async execute(_toolCallId, { paths }, signal, onUpdate) {
|
|
184
|
+
signal?.throwIfAborted();
|
|
185
|
+
running(onUpdate, "Publishing Sandbox files to the Workspace.");
|
|
186
|
+
const output = await sandbox.publishFiles(paths);
|
|
187
|
+
return result({
|
|
188
|
+
files: output.files,
|
|
189
|
+
skipped: output.skipped,
|
|
190
|
+
failed: output.failed.map(({ path, errorCode }) => ({
|
|
191
|
+
path,
|
|
192
|
+
errorCode,
|
|
193
|
+
})),
|
|
194
|
+
bytes: output.bytes,
|
|
195
|
+
workspacePersistence: "published",
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return [
|
|
201
|
+
{
|
|
202
|
+
owner: "sandbox",
|
|
203
|
+
requiredExecutionLevel: "high",
|
|
204
|
+
tool: exec,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
owner: "sandbox",
|
|
208
|
+
requiredExecutionLevel: "high",
|
|
209
|
+
tool: startProcess,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
owner: "sandbox",
|
|
213
|
+
requiredExecutionLevel: "safe",
|
|
214
|
+
retry: "idempotent",
|
|
215
|
+
tool: processLogs,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
owner: "sandbox",
|
|
219
|
+
requiredExecutionLevel: "high",
|
|
220
|
+
tool: stopProcess,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
owner: "sandbox",
|
|
224
|
+
requiredExecutionLevel: "high",
|
|
225
|
+
tool: publishFiles,
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from "
|
|
7
|
-
import {
|
|
8
|
-
import type {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "../
|
|
12
|
-
import type { ScheduleSpec } from "../submission/contracts";
|
|
13
|
-
import { serializeOutput } from "../lib/artifacts";
|
|
14
|
-
import type { PiToolCandidate } from "./compiler";
|
|
1
|
+
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { Type, fauxToolCall, validateToolArguments, type TSchema } from "@earendil-works/pi-ai";
|
|
3
|
+
import {
|
|
4
|
+
toScheduleRuleSpec,
|
|
5
|
+
validateScheduleRuleSpec,
|
|
6
|
+
} from "../../adapter/cloudflare/scheduling/schedule-rule";
|
|
7
|
+
import { isValidTimeZone } from "../../adapter/cloudflare/scheduling/zoned-cron";
|
|
8
|
+
import type { RuntimeSchedulePort, RuntimeSchedulePreparation } from "../../config/ports";
|
|
9
|
+
import { serializeOutput } from "../../lib/artifacts";
|
|
10
|
+
import type { ScheduleSpec } from "../../submission/contracts";
|
|
11
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
15
12
|
|
|
16
13
|
const scheduleTriggerParameters = Type.Union([
|
|
17
14
|
Type.Object({
|
|
@@ -62,7 +59,8 @@ const scheduleTriggerParameters = Type.Union([
|
|
|
62
59
|
});
|
|
63
60
|
|
|
64
61
|
const scheduleParameters = Type.Object({
|
|
65
|
-
prompt: Type.String({
|
|
62
|
+
prompt: Type.String({
|
|
63
|
+
minLength: 1, maxLength: 1000, pattern: "\\S",
|
|
66
64
|
description:
|
|
67
65
|
"The standalone instruction to run when the schedule fires. The future turn has no context from this chat, so make it self-contained.",
|
|
68
66
|
}),
|
|
@@ -260,40 +258,43 @@ export function schedulePiToolCandidates(
|
|
|
260
258
|
if (name !== "schedule" && name !== "update_schedule") return [toolCandidate];
|
|
261
259
|
const confirmation = schedule.confirmation;
|
|
262
260
|
if (!confirmation) return [];
|
|
263
|
-
return [{
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
261
|
+
return [{
|
|
262
|
+
...toolCandidate, alwaysRequiresApproval: true, confirmation: {
|
|
263
|
+
async prepare(args, { intentId }) {
|
|
264
|
+
const validated = validateToolArguments(toolCandidate.tool, fauxToolCall(name, args as Record<string, unknown>));
|
|
265
|
+
const input = validated as { id?: string; prompt?: string; trigger?: ScheduleSpec["trigger"]; label?: string; timezone?: string };
|
|
266
|
+
if (input.timezone !== undefined && !isValidTimeZone(input.timezone)) throw new Error("A valid IANA timezone is required");
|
|
267
|
+
if (input.trigger?.kind === "local" && input.timezone) {
|
|
268
|
+
const error = validateScheduleRuleSpec(toScheduleRuleSpec(input.trigger, input.timezone));
|
|
269
|
+
if (error) throw new Error(error.message);
|
|
270
|
+
}
|
|
271
|
+
const prepared = await confirmation.prepare({
|
|
272
|
+
intentId, ...(name === "update_schedule" ? { id: input.id } : {}),
|
|
273
|
+
...(input.prompt !== undefined ? { prompt: input.prompt } : {}),
|
|
274
|
+
...(input.trigger !== undefined ? { trigger: input.trigger } : {}),
|
|
275
|
+
...(input.label !== undefined ? { label: input.label } : {}),
|
|
276
|
+
...(input.timezone !== undefined ? { tz: input.timezone } : {}),
|
|
277
|
+
});
|
|
278
|
+
if (prepared.id !== input.id) throw new Error("Host preparation changed the target Task ID");
|
|
279
|
+
const fullInput = { prompt: prepared.prompt, label: prepared.label, trigger: prepared.trigger, timezone: prepared.timezone };
|
|
280
|
+
void validateToolArguments({ ...toolCandidate.tool, parameters: scheduleParameters }, fauxToolCall(name, fullInput));
|
|
281
|
+
if (!prepared.label?.trim() || !isValidTimeZone(prepared.timezone) || !prepared.candidateId?.trim() || !prepared.candidateVersion?.trim() || !prepared.authorization?.version?.trim() || !prepared.authorization.scope?.trim() || !Array.isArray(prepared.authorization.missing)) {
|
|
282
|
+
throw new Error("Host preparation is incomplete");
|
|
283
|
+
}
|
|
284
|
+
if (prepared.trigger.kind === "local") {
|
|
285
|
+
const error = validateScheduleRuleSpec(toScheduleRuleSpec(prepared.trigger, prepared.timezone));
|
|
286
|
+
if (error) throw new Error(error.message);
|
|
287
|
+
}
|
|
288
|
+
return prepared;
|
|
289
|
+
},
|
|
290
|
+
async execute(prepared, context) {
|
|
291
|
+
const preview = prepared as RuntimeSchedulePreparation;
|
|
292
|
+
if (preview.authorization.missing.length) throw new Error(`Missing authorization: ${preview.authorization.missing.join("; ")}`);
|
|
293
|
+
const { id } = await confirmation.commit(preview, context);
|
|
294
|
+
return result({ [name === "schedule" ? "scheduled" : "updated"]: true, id, task: { label: preview.label, prompt: preview.prompt, trigger: preview.trigger, timezone: preview.timezone } });
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
}];
|
|
297
298
|
});
|
|
298
299
|
}
|
|
299
300
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type SkillSource } from "agents/skills";
|
|
2
|
+
import type { RuntimeSkillScriptPolicy, WorkspacePort } from "../../config/ports";
|
|
3
|
+
import type { RuntimeSkillBodyTransform } from "../../config/definition";
|
|
4
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
5
|
+
|
|
6
|
+
/** A configured Skill source and its script capabilities. */
|
|
7
|
+
export interface PiSkillBinding {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly description: string;
|
|
10
|
+
readonly source: SkillSource;
|
|
11
|
+
readonly script?: RuntimeSkillScriptPolicy;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Host capabilities available while creating Skill tools. */
|
|
15
|
+
export interface SkillPiToolOptions {
|
|
16
|
+
readonly loader?: WorkerLoader;
|
|
17
|
+
readonly workspace?: WorkspacePort;
|
|
18
|
+
readonly tools?: readonly PiToolCandidate[];
|
|
19
|
+
readonly transformSkillBody?: RuntimeSkillBodyTransform;
|
|
20
|
+
}
|