@springbrand/agent-runtime 0.1.3-alpha.5 → 0.1.3-alpha.7
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 +9 -3
- package/src/adapter/cloudflare/index.ts +0 -5
- package/src/adapter/cloudflare/resources/runtime-resources.ts +14 -11
- package/src/adapter/cloudflare/subagent/tools.ts +2 -4
- package/src/kernel/bindings.ts +33 -6
- package/src/kernel/recoverable-chat-agent.ts +12 -0
- package/src/kernel/runtime-load.ts +89 -0
- package/src/lib/mcp.ts +7 -3
- package/src/pi/assembly/context.ts +3 -3
- package/src/pi/assembly/extensions.ts +11 -22
- package/src/pi/assembly/snapshot.ts +1 -1
- package/src/pi/runtime-adapter/assembly.ts +0 -8
- package/src/pi/tool/base.ts +0 -2
- package/src/pi/tool/compiler.ts +0 -1
- package/src/pi/tool/core.ts +0 -3
- package/src/pi/tool/mcp.ts +3 -4
- package/src/pi/tool/schedule.ts +0 -1
- package/src/pi/tool/skill.ts +55 -41
- package/src/pi/tool/subagent.ts +0 -2
- package/src/pi/tool/web-fetch.ts +0 -1
- package/src/pi/tool/web-search/web-search.ts +0 -1
- package/src/pi/tool/workspace-sandbox.ts +0 -7
- package/src/runtime-agent.ts +28 -28
- package/src/runtime-assembler.ts +12 -8
- package/src/runtime-definition.ts +2 -0
- package/src/runtime.ts +76 -14
- package/src/adapter/cloudflare/universal-agent/definition.ts +0 -71
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springbrand/agent-runtime",
|
|
3
|
-
"version": "0.1.3-alpha.
|
|
3
|
+
"version": "0.1.3-alpha.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -22,9 +22,15 @@
|
|
|
22
22
|
"@cloudflare/think": "0.15.0",
|
|
23
23
|
"@earendil-works/pi-agent-core": "0.83.0",
|
|
24
24
|
"@earendil-works/pi-ai": "0.83.0",
|
|
25
|
-
"agents": "^0.19.0",
|
|
26
|
-
"ai": "^7.0.0",
|
|
27
25
|
"lodash-es": "^4.18.1",
|
|
28
26
|
"zod": "^4.4.3"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"agents": "^0.19.0",
|
|
30
|
+
"ai": "^7.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"agents": "0.19.0",
|
|
34
|
+
"ai": "7.0.37"
|
|
29
35
|
}
|
|
30
36
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { r2, type SkillSource } from "agents/skills";
|
|
2
2
|
import type { RuntimeDegradation } from "../../../kernel/degradation";
|
|
3
3
|
import type { RuntimeExtensionConfig } from "../../../kernel/extensions";
|
|
4
|
+
import { withRuntimeLoadTimeout } from "../../../kernel/runtime-load";
|
|
4
5
|
import type { RuntimeExtensionContribution } from "../../../runtime-definition";
|
|
5
6
|
|
|
6
7
|
export interface CloudflareRuntimeExtensionSource {
|
|
@@ -38,17 +39,19 @@ export async function readVerifiedR2Text(
|
|
|
38
39
|
sourceRef: string,
|
|
39
40
|
sourceHash: string,
|
|
40
41
|
): Promise<string> {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
return withRuntimeLoadTimeout("extension-source:r2", async () => {
|
|
43
|
+
const object = await bucket.get(sourceRef);
|
|
44
|
+
if (!object) throw new Error(`Runtime content not found: ${sourceRef}`);
|
|
45
|
+
const source = await object.text();
|
|
46
|
+
const digest = await crypto.subtle.digest(
|
|
47
|
+
"SHA-256",
|
|
48
|
+
new TextEncoder().encode(source),
|
|
49
|
+
);
|
|
50
|
+
if (hex(digest) !== sourceHash) {
|
|
51
|
+
throw new Error(`Runtime content hash mismatch: ${sourceRef}`);
|
|
52
|
+
}
|
|
53
|
+
return source;
|
|
54
|
+
});
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
export async function prepareRuntimeExtensions(
|
|
@@ -104,7 +104,6 @@ function executeCandidate(options: {
|
|
|
104
104
|
};
|
|
105
105
|
return {
|
|
106
106
|
owner: "subagent-execute",
|
|
107
|
-
authorized: true,
|
|
108
107
|
requiredExecutionLevel: "safe",
|
|
109
108
|
tool,
|
|
110
109
|
};
|
|
@@ -181,7 +180,6 @@ function fetchCandidate(options: {
|
|
|
181
180
|
};
|
|
182
181
|
return {
|
|
183
182
|
owner: "subagent-fetch",
|
|
184
|
-
authorized: true,
|
|
185
183
|
requiredExecutionLevel: "safe",
|
|
186
184
|
tool,
|
|
187
185
|
};
|
|
@@ -242,11 +240,11 @@ export function createCloudflareSubAgentTools(options: {
|
|
|
242
240
|
]
|
|
243
241
|
: [...workspace, fetchCandidate(options)];
|
|
244
242
|
|
|
245
|
-
// 子 Agent 是独立的嵌套 Tool surface
|
|
243
|
+
// 子 Agent 是独立的嵌套 Tool surface:先移除会递归委派的候选,
|
|
246
244
|
// compiler 只负责执行治理和结算。
|
|
247
245
|
const deny = new Set(DENIED_TOOLS);
|
|
248
246
|
const mounted = candidates.filter(
|
|
249
|
-
(candidate) =>
|
|
247
|
+
(candidate) => !deny.has(candidate.tool.name),
|
|
250
248
|
);
|
|
251
249
|
assertSafeExecutionLevel(mounted);
|
|
252
250
|
|
package/src/kernel/bindings.ts
CHANGED
|
@@ -730,6 +730,22 @@ export interface RuntimeModelUsageEvent {
|
|
|
730
730
|
};
|
|
731
731
|
}
|
|
732
732
|
|
|
733
|
+
/**
|
|
734
|
+
* 一次 Tool 调用刚开始,还不知道它会怎么结束。
|
|
735
|
+
*
|
|
736
|
+
* @remarks
|
|
737
|
+
* 结算事件刻意不带参数,因为它只用来记账;开始事件必须带,因为它唯一的用处
|
|
738
|
+
* 就是让 Host 立刻知道「正在写的是什么」——只有工具名的话,「write 开始了」
|
|
739
|
+
* 和「一个网页正在生成」之间的差别就丢了,而后者才是盯着屏幕的人要看的。
|
|
740
|
+
* 参数本来就随 Tool 输入里程碑落库,这里没有多暴露任何东西。
|
|
741
|
+
*/
|
|
742
|
+
export interface RuntimeToolStartEvent {
|
|
743
|
+
readonly submissionId: string;
|
|
744
|
+
readonly toolCallId: string;
|
|
745
|
+
readonly toolName: string;
|
|
746
|
+
readonly args: unknown;
|
|
747
|
+
}
|
|
748
|
+
|
|
733
749
|
export interface RuntimeToolSettlementEvent {
|
|
734
750
|
readonly eventId: string;
|
|
735
751
|
readonly submissionId: string;
|
|
@@ -761,6 +777,17 @@ export interface RuntimeTurnEventsPort {
|
|
|
761
777
|
): Promise<void>;
|
|
762
778
|
/** 投递 Assistant 或 Compaction 的原始 provider usage。 */
|
|
763
779
|
onModelUsage?(event: RuntimeModelUsageEvent): Promise<void>;
|
|
780
|
+
/**
|
|
781
|
+
* 投递一次 Tool 开始执行的事件,带上它的参数。
|
|
782
|
+
*
|
|
783
|
+
* @remarks
|
|
784
|
+
* 只在这次调用第一次落下 durable intent 时投递:恢复重放不会再报一次开始,
|
|
785
|
+
* 否则一个早就结束的 Tool 会在几分钟后再宣布自己「正在跑」。
|
|
786
|
+
*
|
|
787
|
+
* 这条是活性信号而不是账,所以不进 durable outbox:迟到的开始事件本身就是
|
|
788
|
+
* 错的,投递失败就让它丢掉,最终状态由 `onToolSettled` 兜底。
|
|
789
|
+
*/
|
|
790
|
+
onToolStart?(event: RuntimeToolStartEvent): Promise<void>;
|
|
764
791
|
/** 投递不含参数与结果的 Tool 最终结算事件。 */
|
|
765
792
|
onToolSettled?(event: RuntimeToolSettlementEvent): Promise<void>;
|
|
766
793
|
onApproval?(input: {
|
|
@@ -893,15 +920,15 @@ export interface RuntimePlatformPort {
|
|
|
893
920
|
}
|
|
894
921
|
|
|
895
922
|
/**
|
|
896
|
-
*
|
|
923
|
+
* 汇总本次装配已配置的 Skill 来源。
|
|
897
924
|
*
|
|
898
925
|
* @remarks
|
|
899
926
|
* Builder 在生成候选 Snapshot 时创建,Pi 组装在生成 Skill 工具时读取。
|
|
900
927
|
*
|
|
901
|
-
* catalog
|
|
928
|
+
* catalog、来源与脚本策略一起保留,避免装配期重新访问来源。
|
|
902
929
|
*/
|
|
903
930
|
export interface RuntimeSkillBindings {
|
|
904
|
-
/**
|
|
931
|
+
/** Configured external sources and their isolated script policy. */
|
|
905
932
|
sources: readonly RuntimeSkillSourceBinding[];
|
|
906
933
|
}
|
|
907
934
|
|
|
@@ -911,7 +938,7 @@ export interface RuntimeSkillBindings {
|
|
|
911
938
|
* @remarks
|
|
912
939
|
* Skill Plugin 在准备来源时设置,Pi Skill 工具在决定是否注入脚本执行能力时读取。
|
|
913
940
|
*
|
|
914
|
-
*
|
|
941
|
+
* 这是 Config 已解析的执行策略,执行期不应自动放宽。
|
|
915
942
|
*/
|
|
916
943
|
export interface RuntimeSkillScriptPolicy {
|
|
917
944
|
network: "none" | "full";
|
|
@@ -920,12 +947,12 @@ export interface RuntimeSkillScriptPolicy {
|
|
|
920
947
|
}
|
|
921
948
|
|
|
922
949
|
/**
|
|
923
|
-
*
|
|
950
|
+
* 把一个已配置 Skill 来源与其脚本策略放在同一快照中。
|
|
924
951
|
*
|
|
925
952
|
* @remarks
|
|
926
953
|
* Builder 为每个 Skill 贡献创建,Pi 组装和 Skill 工具按名称查找并读取。
|
|
927
954
|
*
|
|
928
|
-
*
|
|
955
|
+
* 名称、内容来源与脚本策略不分开存放,脚本 runner 才能按 Skill 名找到对应策略。
|
|
929
956
|
*/
|
|
930
957
|
export interface RuntimeSkillSourceBinding {
|
|
931
958
|
name: string;
|
|
@@ -353,7 +353,13 @@ export abstract class RecoverableChatAgent<
|
|
|
353
353
|
* 清理延后执行,为断线重连的客户端保留终态流片段。
|
|
354
354
|
*/
|
|
355
355
|
protected completeRecoverableStream(streamId: string): void {
|
|
356
|
+
const completedRequestId = this.resumableStream.activeRequestId;
|
|
356
357
|
this.resumableStream.complete(streamId);
|
|
358
|
+
this.pendingResumeConnections.clear();
|
|
359
|
+
if (completedRequestId === this.continuation.activeRequestId) {
|
|
360
|
+
this.continuation.activeRequestId = null;
|
|
361
|
+
this.continuation.activeConnectionId = null;
|
|
362
|
+
}
|
|
357
363
|
void this.ensureStreamCleanupScheduled();
|
|
358
364
|
}
|
|
359
365
|
|
|
@@ -366,7 +372,13 @@ export abstract class RecoverableChatAgent<
|
|
|
366
372
|
* 流错误标志与业务失败分开,因为续传协议和提交生命周期的责任不同。
|
|
367
373
|
*/
|
|
368
374
|
protected failRecoverableStream(streamId: string): void {
|
|
375
|
+
const erroredRequestId = this.resumableStream.activeRequestId;
|
|
369
376
|
this.resumableStream.markError(streamId);
|
|
377
|
+
this.pendingResumeConnections.clear();
|
|
378
|
+
if (erroredRequestId === this.continuation.activeRequestId) {
|
|
379
|
+
this.continuation.activeRequestId = null;
|
|
380
|
+
this.continuation.activeConnectionId = null;
|
|
381
|
+
}
|
|
370
382
|
void this.ensureStreamCleanupScheduled();
|
|
371
383
|
}
|
|
372
384
|
|
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
import type { RuntimeLoadPhase, RuntimeLoadState } from "./state";
|
|
2
2
|
|
|
3
|
+
/** Hard ceiling for one Host-owned Runtime definition phase. */
|
|
4
|
+
export const RUNTIME_LOAD_TIMEOUT_MS = 180_000;
|
|
5
|
+
|
|
6
|
+
/** Hard ceiling for one optional external capability load. */
|
|
7
|
+
export const RUNTIME_CAPABILITY_LOAD_TIMEOUT_MS = 60_000;
|
|
8
|
+
|
|
9
|
+
const reportedFailures = new WeakSet<object>();
|
|
10
|
+
|
|
11
|
+
function errorText(error: unknown): string {
|
|
12
|
+
return error instanceof Error ? error.message : String(error);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Emit one stable structured record for a Runtime load failure. */
|
|
16
|
+
export function logRuntimeLoadFailure(
|
|
17
|
+
step: string,
|
|
18
|
+
error: unknown,
|
|
19
|
+
details: {
|
|
20
|
+
reason?: "error" | "timeout";
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
durationMs?: number;
|
|
23
|
+
} = {},
|
|
24
|
+
): void {
|
|
25
|
+
if (typeof error === "object" && error !== null) {
|
|
26
|
+
if (reportedFailures.has(error)) return;
|
|
27
|
+
reportedFailures.add(error);
|
|
28
|
+
}
|
|
29
|
+
console.error(
|
|
30
|
+
"[runtime-load:failed]",
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
step,
|
|
33
|
+
reason: details.reason ?? "error",
|
|
34
|
+
...(details.timeoutMs !== undefined
|
|
35
|
+
? { timeoutMs: details.timeoutMs }
|
|
36
|
+
: {}),
|
|
37
|
+
...(details.durationMs !== undefined
|
|
38
|
+
? { durationMs: details.durationMs }
|
|
39
|
+
: {}),
|
|
40
|
+
error: errorText(error),
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Stop waiting for one Runtime load boundary and report any failure once. */
|
|
46
|
+
export async function withRuntimeLoadTimeout<T>(
|
|
47
|
+
step: string,
|
|
48
|
+
load: () => T | PromiseLike<T>,
|
|
49
|
+
options: {
|
|
50
|
+
timeoutMs?: number;
|
|
51
|
+
onLateResult?: (value: T) => void | Promise<void>;
|
|
52
|
+
} = {},
|
|
53
|
+
): Promise<T> {
|
|
54
|
+
const timeoutMs = options.timeoutMs ?? RUNTIME_CAPABILITY_LOAD_TIMEOUT_MS;
|
|
55
|
+
const startedAt = Date.now();
|
|
56
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
57
|
+
let timedOut = false;
|
|
58
|
+
const operation = Promise.resolve().then(load);
|
|
59
|
+
|
|
60
|
+
if (options.onLateResult) {
|
|
61
|
+
void operation.then(async (value) => {
|
|
62
|
+
if (timedOut) await options.onLateResult!(value);
|
|
63
|
+
}, () => undefined).catch((error) => {
|
|
64
|
+
logRuntimeLoadFailure(`${step}:late-cleanup`, error);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
return await Promise.race([
|
|
70
|
+
operation,
|
|
71
|
+
new Promise<never>((_, reject) => {
|
|
72
|
+
timeout = setTimeout(() => {
|
|
73
|
+
timedOut = true;
|
|
74
|
+
reject(new Error(
|
|
75
|
+
`Runtime load step "${step}" timed out after ${timeoutMs}ms`,
|
|
76
|
+
));
|
|
77
|
+
}, timeoutMs);
|
|
78
|
+
}),
|
|
79
|
+
]);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
logRuntimeLoadFailure(step, error, {
|
|
82
|
+
reason: timedOut ? "timeout" : "error",
|
|
83
|
+
timeoutMs,
|
|
84
|
+
durationMs: Date.now() - startedAt,
|
|
85
|
+
});
|
|
86
|
+
throw error;
|
|
87
|
+
} finally {
|
|
88
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
3
92
|
/**
|
|
4
93
|
* 装载状态机与宿主之间的全部接触面。
|
|
5
94
|
*
|
package/src/lib/mcp.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RetryOptions } from "agents";
|
|
2
|
+
import { withRuntimeLoadTimeout } from "../kernel/runtime-load";
|
|
2
3
|
|
|
3
4
|
const MCP_RETRY: RetryOptions = {
|
|
4
5
|
maxAttempts: 3,
|
|
@@ -63,9 +64,12 @@ export async function connectConfiguredMcpServers(
|
|
|
63
64
|
const servers = [...pending.values()];
|
|
64
65
|
const settled = await Promise.allSettled(
|
|
65
66
|
servers.map((server) =>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
withRuntimeLoadTimeout(
|
|
68
|
+
`mcp:${server.name}:connect`,
|
|
69
|
+
() => host.addMcpServer(server.name, server.url, {
|
|
70
|
+
retry: MCP_RETRY,
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
69
73
|
),
|
|
70
74
|
);
|
|
71
75
|
const failed = settled.flatMap((result, index) =>
|
|
@@ -367,7 +367,7 @@ async function readMemory(
|
|
|
367
367
|
return { blocks, degradations };
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
//
|
|
370
|
+
// 作用:把已配置 Skill 的持久化 catalog 整理成提示词。
|
|
371
371
|
// 调用:系统上下文装配在需要告诉模型当前可用 Skill 时调用。
|
|
372
372
|
// 原因:catalog 已在发布和绑定时固定,装配期不得为此访问远端内容来源。
|
|
373
373
|
function renderSkillCatalog(
|
|
@@ -379,8 +379,8 @@ function renderSkillCatalog(
|
|
|
379
379
|
if (bindings.length === 0) return null;
|
|
380
380
|
|
|
381
381
|
return [
|
|
382
|
-
"
|
|
383
|
-
"Use an
|
|
382
|
+
"AVAILABLE SKILLS",
|
|
383
|
+
"Use an available Skill when its description matches. Read its instructions or files only through the Skill tools.",
|
|
384
384
|
"",
|
|
385
385
|
...bindings.map(
|
|
386
386
|
({ name, description }) => `- ${name}: ${description}`,
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
RuntimeExtensionPermissions,
|
|
12
12
|
} from "../../kernel/extensions";
|
|
13
13
|
import type { RuntimeDegradation } from "../../kernel/degradation";
|
|
14
|
+
import { withRuntimeLoadTimeout } from "../../kernel/runtime-load";
|
|
14
15
|
import { sanitizeExtensionName } from "../../lib/extension-name";
|
|
15
16
|
import type { PiToolCandidate } from "../tool/compiler";
|
|
16
17
|
|
|
@@ -42,9 +43,6 @@ export interface PiLoadedExtension {
|
|
|
42
43
|
|
|
43
44
|
export interface AssemblePiExtensionsOptions {
|
|
44
45
|
readonly extensions: readonly RuntimeExtensionConfig[];
|
|
45
|
-
readonly published: ReadonlySet<string>;
|
|
46
|
-
readonly enabled: ReadonlySet<string>;
|
|
47
|
-
readonly authorized: ReadonlySet<string>;
|
|
48
46
|
readonly load: (
|
|
49
47
|
extension: RuntimeExtensionConfig,
|
|
50
48
|
) => Promise<LoadedRuntimeExtension>;
|
|
@@ -237,7 +235,7 @@ const EXTENSION_HOOK_NAMES = [
|
|
|
237
235
|
* 通过 Cloudflare ExtensionManager 加载一个 Extension,并只暴露 Runtime 需要的描述和执行能力。
|
|
238
236
|
*
|
|
239
237
|
* @remarks
|
|
240
|
-
* Runtime
|
|
238
|
+
* Runtime 准备阶段会对已配置 Extension 调用,资源发布的 smoke test 也会用它校验候选源码;调用方必须提供 WorkerLoader,并在权限需要时提供 Host 绑定工厂。
|
|
241
239
|
*
|
|
242
240
|
* discovery 使用剔除权限的 manifest,有外部访问的执行 isolate 则延迟到首次 Tool 执行时创建。这是为了防止 `describe()` 在授权和审批前获得网络或 Host 能力,不能随意合并两条加载路径。
|
|
243
241
|
*
|
|
@@ -267,9 +265,12 @@ export async function loadPiExtension(
|
|
|
267
265
|
const { permissions: _ignored, ...manifestWithoutPermissions } =
|
|
268
266
|
extension.manifest;
|
|
269
267
|
try {
|
|
270
|
-
await
|
|
271
|
-
|
|
272
|
-
|
|
268
|
+
await withRuntimeLoadTimeout(
|
|
269
|
+
`extension:${name}:discover`,
|
|
270
|
+
() => discovery.load(
|
|
271
|
+
manifestWithoutPermissions as ExtensionManifest,
|
|
272
|
+
extension.source,
|
|
273
|
+
),
|
|
273
274
|
);
|
|
274
275
|
} catch (cause) {
|
|
275
276
|
// Upstream assumes describe() yields an array and throws an opaque TypeError
|
|
@@ -507,20 +508,17 @@ export function extensionToolRequiredExecutionLevel(
|
|
|
507
508
|
}
|
|
508
509
|
|
|
509
510
|
/**
|
|
510
|
-
*
|
|
511
|
+
* 把当前已配置的 Extension 装配成 Runtime 能力。
|
|
511
512
|
*
|
|
512
513
|
* @remarks
|
|
513
|
-
* Runtime 准备阶段和资源 smoke test
|
|
514
|
+
* Runtime 准备阶段和资源 smoke test 调用;调用方传入已选 Extension 及已确定加载策略的 `load`。
|
|
514
515
|
*
|
|
515
|
-
*
|
|
516
|
+
* 单个加载失败记为 degradation 而不影响其他项。
|
|
516
517
|
*
|
|
517
518
|
* 核心术语见包入口 `index.ts`。
|
|
518
519
|
*/
|
|
519
520
|
export async function assemblePiExtensions({
|
|
520
521
|
extensions,
|
|
521
|
-
published,
|
|
522
|
-
enabled,
|
|
523
|
-
authorized,
|
|
524
522
|
load,
|
|
525
523
|
}: AssemblePiExtensionsOptions): Promise<PiExtensionAssembly> {
|
|
526
524
|
const loadedExtensions: PiLoadedExtension[] = [];
|
|
@@ -530,14 +528,6 @@ export async function assemblePiExtensions({
|
|
|
530
528
|
|
|
531
529
|
for (const extension of extensions) {
|
|
532
530
|
const name = extension.manifest.name.trim();
|
|
533
|
-
if (
|
|
534
|
-
!published.has(name) ||
|
|
535
|
-
!enabled.has(name) ||
|
|
536
|
-
!authorized.has(name)
|
|
537
|
-
) {
|
|
538
|
-
continue;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
531
|
let loaded: LoadedRuntimeExtension;
|
|
542
532
|
try {
|
|
543
533
|
loaded = await load(extension);
|
|
@@ -608,7 +598,6 @@ export async function assemblePiExtensions({
|
|
|
608
598
|
});
|
|
609
599
|
candidates.push(Object.freeze({
|
|
610
600
|
owner: `extension:${name}@${extension.manifest.version}`,
|
|
611
|
-
authorized: true,
|
|
612
601
|
requiredExecutionLevel,
|
|
613
602
|
summary: description,
|
|
614
603
|
tool,
|
|
@@ -392,16 +392,8 @@ async function preparePiAssembly(
|
|
|
392
392
|
options: PreparePiAssemblyOptions,
|
|
393
393
|
): Promise<PreparedPiAssembly> {
|
|
394
394
|
const { snapshot } = options;
|
|
395
|
-
const extensionNames = new Set(
|
|
396
|
-
snapshot.pi.extensions.map((extension) =>
|
|
397
|
-
extension.manifest.name.trim()
|
|
398
|
-
),
|
|
399
|
-
);
|
|
400
395
|
const extensions = await assemblePiExtensions({
|
|
401
396
|
extensions: snapshot.pi.extensions,
|
|
402
|
-
published: extensionNames,
|
|
403
|
-
enabled: extensionNames,
|
|
404
|
-
authorized: extensionNames,
|
|
405
397
|
load: async (extension) => {
|
|
406
398
|
const workspacePermission =
|
|
407
399
|
extension.manifest.permissions?.workspace ?? "none";
|
package/src/pi/tool/base.ts
CHANGED
|
@@ -144,7 +144,6 @@ function result<T>(details: T): AgentToolResult<T> {
|
|
|
144
144
|
function candidate<T extends TSchema>(tool: AgentTool<T>): PiToolCandidate {
|
|
145
145
|
return {
|
|
146
146
|
owner: "runtime-base",
|
|
147
|
-
authorized: true,
|
|
148
147
|
requiredExecutionLevel: "safe",
|
|
149
148
|
source: "action",
|
|
150
149
|
tool,
|
|
@@ -269,7 +268,6 @@ export function memoryPiToolCandidate(
|
|
|
269
268
|
};
|
|
270
269
|
return {
|
|
271
270
|
owner: "runtime-memory",
|
|
272
|
-
authorized: true,
|
|
273
271
|
requiredExecutionLevel: "safe",
|
|
274
272
|
source: "action",
|
|
275
273
|
tool,
|
package/src/pi/tool/compiler.ts
CHANGED
|
@@ -36,7 +36,6 @@ export interface PiToolInteractionSpec {
|
|
|
36
36
|
/** 描述一个尚未进入最终 Tool Surface 和结算包装的 Pi 工具。 */
|
|
37
37
|
export interface PiToolCandidate {
|
|
38
38
|
readonly owner: string;
|
|
39
|
-
readonly authorized: boolean;
|
|
40
39
|
readonly tool: AgentTool<any, any>;
|
|
41
40
|
/** Conservative maximum used in the stable Runtime descriptor. */
|
|
42
41
|
readonly requiredExecutionLevel: ExecutionLevel;
|
package/src/pi/tool/core.ts
CHANGED
|
@@ -51,7 +51,6 @@ export function browserQuickActionPiToolCandidates(
|
|
|
51
51
|
return Object.entries(createQuickActionTools({ browser })).map(
|
|
52
52
|
([name, tool]) => ({
|
|
53
53
|
owner: "core:browser",
|
|
54
|
-
authorized: true,
|
|
55
54
|
requiredExecutionLevel: "low",
|
|
56
55
|
tool: aiToolToPi(name, tool, {
|
|
57
56
|
label: BROWSER_TOOL_LABELS[name] ?? name,
|
|
@@ -94,7 +93,6 @@ export function listExtensionsPiToolCandidate(
|
|
|
94
93
|
};
|
|
95
94
|
return {
|
|
96
95
|
owner: "core:extensions",
|
|
97
|
-
authorized: true,
|
|
98
96
|
requiredExecutionLevel: "low",
|
|
99
97
|
tool,
|
|
100
98
|
};
|
|
@@ -174,7 +172,6 @@ export function codeExecutionPiToolCandidate(
|
|
|
174
172
|
};
|
|
175
173
|
return {
|
|
176
174
|
owner: "core:codemode",
|
|
177
|
-
authorized: true,
|
|
178
175
|
requiredExecutionLevel: "high",
|
|
179
176
|
source: "codemode",
|
|
180
177
|
summary: "Run JavaScript with network and configured connector access",
|
package/src/pi/tool/mcp.ts
CHANGED
|
@@ -263,7 +263,6 @@ export function createPiMcpToolCandidate(
|
|
|
263
263
|
};
|
|
264
264
|
return {
|
|
265
265
|
owner: policy.owner,
|
|
266
|
-
authorized: true,
|
|
267
266
|
tool,
|
|
268
267
|
summary: label,
|
|
269
268
|
requiredExecutionLevel: policy.requiredExecutionLevel,
|
|
@@ -278,15 +277,15 @@ export function createPiMcpToolCandidate(
|
|
|
278
277
|
}
|
|
279
278
|
|
|
280
279
|
/**
|
|
281
|
-
*
|
|
280
|
+
* 把当前已配置且 ready 的 MCP 工具转换成 Pi 候选工具。
|
|
282
281
|
*
|
|
283
282
|
* Tool Surface 在 Runtime 准备时调用它。调用方应传入
|
|
284
283
|
* Agent Host,以及同一 Runtime 快照中的 MCP 配置;未就绪、未配置或重复 URL
|
|
285
284
|
* 的连接不会进入模型工具目录。
|
|
286
285
|
*
|
|
287
286
|
* “候选工具”是进入 Pi 编译和统一治理前的内部描述。连接、凭据和恢复由 Host
|
|
288
|
-
* 与 Agents SDK
|
|
289
|
-
* 远端 Server 提供的 annotations
|
|
287
|
+
* 与 Agents SDK 管理;本函数只做动态连接状态过滤、Schema 转接和公开结果投影。
|
|
288
|
+
* 远端 Server 提供的 annotations 只是提示,不是本地审批边界,所以所有动态
|
|
290
289
|
* MCP 工具继续标为高风险,不能据此绕过审批。
|
|
291
290
|
*
|
|
292
291
|
* `Host`、`Pi`、`Runtime Snapshot` 与“候选工具”见 `../../index.ts`。
|
package/src/pi/tool/schedule.ts
CHANGED
package/src/pi/tool/skill.ts
CHANGED
|
@@ -13,9 +13,10 @@ import type {
|
|
|
13
13
|
import { aiToolToPi } from "./ai-adapter";
|
|
14
14
|
import type { PiToolCandidate } from "./compiler";
|
|
15
15
|
|
|
16
|
-
/**
|
|
16
|
+
/** A configured Skill source and its script capabilities. */
|
|
17
17
|
export interface PiSkillBinding {
|
|
18
18
|
readonly name: string;
|
|
19
|
+
readonly description: string;
|
|
19
20
|
readonly source: SkillSource;
|
|
20
21
|
readonly script?: RuntimeSkillScriptPolicy;
|
|
21
22
|
}
|
|
@@ -46,42 +47,23 @@ function scriptTools(
|
|
|
46
47
|
})) as ToolSet;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
function authorizedSource(binding: PiSkillBinding): SkillSource {
|
|
52
|
-
const { name, source } = binding;
|
|
50
|
+
function catalogSkillSource(binding: PiSkillBinding): SkillSource {
|
|
51
|
+
const source = binding.source;
|
|
53
52
|
return {
|
|
54
|
-
id:
|
|
53
|
+
id: source.id,
|
|
55
54
|
get fingerprint() {
|
|
56
|
-
return
|
|
57
|
-
},
|
|
58
|
-
async list() {
|
|
59
|
-
return (await source.list()).filter((skill) => skill.name === name);
|
|
60
|
-
},
|
|
61
|
-
async load(requested) {
|
|
62
|
-
if (requested !== name) return null;
|
|
63
|
-
try {
|
|
64
|
-
const skill = await source.load(requested);
|
|
65
|
-
return skill?.name === name ? skill : null;
|
|
66
|
-
} catch (cause) {
|
|
67
|
-
throw new Error(`Skill unavailable: ${name}`, { cause });
|
|
68
|
-
}
|
|
55
|
+
return source.fingerprint;
|
|
69
56
|
},
|
|
57
|
+
// Runtime configuration already resolved the catalogue; Skill content stays lazy.
|
|
58
|
+
list: async () => [{
|
|
59
|
+
name: binding.name,
|
|
60
|
+
description: binding.description,
|
|
61
|
+
sourceId: source.id,
|
|
62
|
+
}],
|
|
63
|
+
load: (name) => source.load(name),
|
|
70
64
|
...(source.readResource
|
|
71
|
-
? {
|
|
72
|
-
|
|
73
|
-
if (requested !== name) return null;
|
|
74
|
-
try {
|
|
75
|
-
const resource = await source.readResource!(requested, path);
|
|
76
|
-
return resource?.path === path ? resource : null;
|
|
77
|
-
} catch (cause) {
|
|
78
|
-
throw new Error(
|
|
79
|
-
`Skill resource unavailable: ${name}/${path}`,
|
|
80
|
-
{ cause },
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
}
|
|
65
|
+
? { readResource: (name: string, path: string) =>
|
|
66
|
+
source.readResource!(name, path) }
|
|
85
67
|
: {}),
|
|
86
68
|
...(source.refresh ? { refresh: () => source.refresh!() } : {}),
|
|
87
69
|
};
|
|
@@ -123,6 +105,9 @@ const SKILL_TOOL_LABELS: Readonly<Record<string, string>> = {
|
|
|
123
105
|
run_skill_script: "Run Skill script",
|
|
124
106
|
};
|
|
125
107
|
|
|
108
|
+
const SKILL_ENTRY_READ_GUIDANCE =
|
|
109
|
+
"SKILL.md contains the Skill instructions; use activate_skill instead.";
|
|
110
|
+
|
|
126
111
|
/** Create Pi candidates from the official Agents SDK SkillRegistry tools. */
|
|
127
112
|
export async function skillPiToolCandidates(
|
|
128
113
|
bindings: readonly PiSkillBinding[],
|
|
@@ -131,17 +116,46 @@ export async function skillPiToolCandidates(
|
|
|
131
116
|
if (bindings.length === 0) return [];
|
|
132
117
|
|
|
133
118
|
const registry = new SkillRegistry(
|
|
134
|
-
bindings.map(
|
|
119
|
+
bindings.map(catalogSkillSource),
|
|
135
120
|
scriptRunner(bindings, options),
|
|
136
121
|
);
|
|
137
122
|
await registry.load();
|
|
138
123
|
|
|
139
|
-
return Object.entries(registry.tools()).map(([name, tool]) =>
|
|
140
|
-
|
|
141
|
-
authorized: true,
|
|
142
|
-
requiredExecutionLevel: name === "run_skill_script" ? "high" : "safe",
|
|
143
|
-
tool: aiToolToPi(name, tool, {
|
|
124
|
+
return Object.entries(registry.tools()).map(([name, tool]) => {
|
|
125
|
+
const adapted = aiToolToPi(name, tool, {
|
|
144
126
|
label: SKILL_TOOL_LABELS[name] ?? name,
|
|
145
|
-
|
|
146
|
-
|
|
127
|
+
...(name === "read_skill_resource"
|
|
128
|
+
? {
|
|
129
|
+
description:
|
|
130
|
+
"Read a bundled resource listed by activate_skill. Do not pass SKILL.md; activate_skill returns the Skill instructions.",
|
|
131
|
+
}
|
|
132
|
+
: {}),
|
|
133
|
+
});
|
|
134
|
+
if (name === "read_skill_resource") {
|
|
135
|
+
const execute = adapted.execute;
|
|
136
|
+
adapted.execute = (toolCallId, input, signal) => {
|
|
137
|
+
const target = input as { name?: unknown; path?: unknown };
|
|
138
|
+
const parts = typeof target.path === "string"
|
|
139
|
+
? target.path.split("/")
|
|
140
|
+
: [];
|
|
141
|
+
if (
|
|
142
|
+
target.path === "SKILL.md" ||
|
|
143
|
+
(target.name === undefined &&
|
|
144
|
+
parts.length === 2 &&
|
|
145
|
+
parts[1] === "SKILL.md")
|
|
146
|
+
) {
|
|
147
|
+
return Promise.resolve({
|
|
148
|
+
content: [{ type: "text", text: SKILL_ENTRY_READ_GUIDANCE }],
|
|
149
|
+
details: SKILL_ENTRY_READ_GUIDANCE,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return execute(toolCallId, input, signal);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
owner: "runtime-skill",
|
|
157
|
+
requiredExecutionLevel: name === "run_skill_script" ? "high" : "safe",
|
|
158
|
+
tool: adapted,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
147
161
|
}
|
package/src/pi/tool/subagent.ts
CHANGED
|
@@ -91,7 +91,6 @@ export function subagentPiToolCandidates(
|
|
|
91
91
|
};
|
|
92
92
|
return {
|
|
93
93
|
owner: `subagent:${type.name}`,
|
|
94
|
-
authorized: true,
|
|
95
94
|
requiredExecutionLevel: "safe",
|
|
96
95
|
tool,
|
|
97
96
|
};
|
|
@@ -142,7 +141,6 @@ export function subagentPiToolCandidates(
|
|
|
142
141
|
};
|
|
143
142
|
candidates.push({
|
|
144
143
|
owner: "subagent:background",
|
|
145
|
-
authorized: true,
|
|
146
144
|
requiredExecutionLevel: "low",
|
|
147
145
|
summary: "Dispatch a background sub-agent",
|
|
148
146
|
source: "action",
|
package/src/pi/tool/web-fetch.ts
CHANGED
|
@@ -288,13 +288,11 @@ export function workspacePiToolCandidates(
|
|
|
288
288
|
return [
|
|
289
289
|
...[read, write, edit, list, find, grep, remove].map((tool) => ({
|
|
290
290
|
owner: "workspace",
|
|
291
|
-
authorized: true,
|
|
292
291
|
requiredExecutionLevel: "safe" as const,
|
|
293
292
|
tool,
|
|
294
293
|
})),
|
|
295
294
|
{
|
|
296
295
|
owner: "workspace",
|
|
297
|
-
authorized: true,
|
|
298
296
|
requiredExecutionLevel: "high" as const,
|
|
299
297
|
summary: "Run a Bash script over Workspace files",
|
|
300
298
|
source: "action" as const,
|
|
@@ -474,31 +472,26 @@ export function sandboxPiToolCandidates(
|
|
|
474
472
|
return [
|
|
475
473
|
{
|
|
476
474
|
owner: "sandbox",
|
|
477
|
-
authorized: true,
|
|
478
475
|
requiredExecutionLevel: "high",
|
|
479
476
|
tool: exec,
|
|
480
477
|
},
|
|
481
478
|
{
|
|
482
479
|
owner: "sandbox",
|
|
483
|
-
authorized: true,
|
|
484
480
|
requiredExecutionLevel: "high",
|
|
485
481
|
tool: startProcess,
|
|
486
482
|
},
|
|
487
483
|
{
|
|
488
484
|
owner: "sandbox",
|
|
489
|
-
authorized: true,
|
|
490
485
|
requiredExecutionLevel: "safe",
|
|
491
486
|
tool: processLogs,
|
|
492
487
|
},
|
|
493
488
|
{
|
|
494
489
|
owner: "sandbox",
|
|
495
|
-
authorized: true,
|
|
496
490
|
requiredExecutionLevel: "high",
|
|
497
491
|
tool: stopProcess,
|
|
498
492
|
},
|
|
499
493
|
{
|
|
500
494
|
owner: "sandbox",
|
|
501
|
-
authorized: true,
|
|
502
495
|
requiredExecutionLevel: "high",
|
|
503
496
|
tool: publishFiles,
|
|
504
497
|
},
|
package/src/runtime-agent.ts
CHANGED
|
@@ -44,6 +44,11 @@ import {
|
|
|
44
44
|
} from "./tool-registry";
|
|
45
45
|
import type { RuntimeAssemblyView } from "./kernel/runtime-assembly-view";
|
|
46
46
|
import type { RuntimeConfigUpdateResult } from "./kernel/runtime-config";
|
|
47
|
+
import {
|
|
48
|
+
logRuntimeLoadFailure,
|
|
49
|
+
RUNTIME_LOAD_TIMEOUT_MS,
|
|
50
|
+
withRuntimeLoadTimeout,
|
|
51
|
+
} from "./kernel/runtime-load";
|
|
47
52
|
import { AgentToolRuntimeKernel } from "./agent-tool-runtime";
|
|
48
53
|
import type {
|
|
49
54
|
RuntimeAgentConfigContext,
|
|
@@ -388,20 +393,6 @@ function parseTemporaryAgentLaunch<Config>(
|
|
|
388
393
|
*
|
|
389
394
|
* 生成类把首次加载、并发去重、按 key 重载和失败重试收口到一个入口。
|
|
390
395
|
*/
|
|
391
|
-
export function defineRuntimeAgent<
|
|
392
|
-
Env extends Cloudflare.Env,
|
|
393
|
-
Config,
|
|
394
|
-
Command,
|
|
395
|
-
Change,
|
|
396
|
-
>(
|
|
397
|
-
definition: MutableRuntimeAgentDefinition<Env, Config, Command, Change>,
|
|
398
|
-
): RuntimeAgentClass<Env, Command, Change>;
|
|
399
|
-
export function defineRuntimeAgent<
|
|
400
|
-
Env extends Cloudflare.Env = Cloudflare.Env,
|
|
401
|
-
Config = unknown,
|
|
402
|
-
>(
|
|
403
|
-
definition: ReadonlyRuntimeAgentDefinition<Env, Config>,
|
|
404
|
-
): RuntimeAgentClass<Env>;
|
|
405
396
|
export function defineRuntimeAgent<
|
|
406
397
|
Env extends Cloudflare.Env = Cloudflare.Env,
|
|
407
398
|
Config = unknown,
|
|
@@ -587,14 +578,18 @@ export function defineRuntimeAgent<
|
|
|
587
578
|
const pending = (async () => {
|
|
588
579
|
this.runtimeLoad.begin();
|
|
589
580
|
const loadContext = this.createDefinitionContext();
|
|
590
|
-
const loaded = await (
|
|
591
|
-
definition.load
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
581
|
+
const loaded = await withRuntimeLoadTimeout(
|
|
582
|
+
"definition.load",
|
|
583
|
+
() => (
|
|
584
|
+
definition.load as (
|
|
585
|
+
context: RuntimeAgentPlanningContext<Env, Command, Change>,
|
|
586
|
+
requestedRuntimeKey?: string,
|
|
587
|
+
) => Promise<LoadedRuntime<Config>>
|
|
588
|
+
)(
|
|
589
|
+
loadContext,
|
|
590
|
+
requestedRuntimeKey,
|
|
591
|
+
),
|
|
592
|
+
{ timeoutMs: RUNTIME_LOAD_TIMEOUT_MS },
|
|
598
593
|
);
|
|
599
594
|
const resolvedRuntimeKey = loaded.runtimeKey.trim();
|
|
600
595
|
if (!resolvedRuntimeKey) {
|
|
@@ -603,12 +598,16 @@ export function defineRuntimeAgent<
|
|
|
603
598
|
const context = this.createDefinitionContext({
|
|
604
599
|
value: loaded.config,
|
|
605
600
|
});
|
|
606
|
-
let tools = normalizeToolAssembly(await (
|
|
607
|
-
definition.tools
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
601
|
+
let tools = normalizeToolAssembly(await withRuntimeLoadTimeout(
|
|
602
|
+
"definition.tools",
|
|
603
|
+
() => (
|
|
604
|
+
definition.tools as (
|
|
605
|
+
context: RuntimeAgentPlanningContext<Env, Command, Change>,
|
|
606
|
+
config: Config,
|
|
607
|
+
) => ToolRegistry | ToolAssemblyResult | Promise<ToolRegistry | ToolAssemblyResult>
|
|
608
|
+
)(context, loaded.config),
|
|
609
|
+
{ timeoutMs: RUNTIME_LOAD_TIMEOUT_MS },
|
|
610
|
+
));
|
|
612
611
|
let hooks = resolveDefinitionHooks(context);
|
|
613
612
|
if (this.role === "temporary") {
|
|
614
613
|
this.temporaryDispose = tools.dispose;
|
|
@@ -631,6 +630,7 @@ export function defineRuntimeAgent<
|
|
|
631
630
|
// Host 的会话列表把装配后的第一次投影当作「facet 已就绪」的信号。
|
|
632
631
|
await this.broadcastApprovals();
|
|
633
632
|
})().catch((error) => {
|
|
633
|
+
logRuntimeLoadFailure("runtime", error);
|
|
634
634
|
this.runtimeLoad.fail();
|
|
635
635
|
throw error;
|
|
636
636
|
});
|
package/src/runtime-assembler.ts
CHANGED
|
@@ -45,6 +45,7 @@ import type {
|
|
|
45
45
|
RuntimeToolSurfacePolicy,
|
|
46
46
|
} from "./runtime-definition";
|
|
47
47
|
import type { ToolRegistry } from "./tool-registry";
|
|
48
|
+
import { withRuntimeLoadTimeout } from "./kernel/runtime-load";
|
|
48
49
|
import {
|
|
49
50
|
basePiToolCandidates,
|
|
50
51
|
memoryPiToolCandidate,
|
|
@@ -168,7 +169,7 @@ interface ToolSurface {
|
|
|
168
169
|
readonly degradations: readonly RuntimeDegradation[];
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
// 唯一 Tool Surface seam
|
|
172
|
+
// 唯一 Tool Surface seam:只读取已配置 Port、Resource 和 Agent policy,
|
|
172
173
|
// 统一生成可见 Tool,并在一处做名称、执行档位与冲突校验。
|
|
173
174
|
async function createToolSurface(
|
|
174
175
|
input: ToolSurfaceInput,
|
|
@@ -177,7 +178,6 @@ async function createToolSurface(
|
|
|
177
178
|
const allowsTool = input.policy?.allowsTool;
|
|
178
179
|
const allowsExtension = input.policy?.allowsExtension;
|
|
179
180
|
const visible = (candidate: PiToolCandidate) =>
|
|
180
|
-
candidate.authorized &&
|
|
181
181
|
!deny.has(candidate.tool.name) &&
|
|
182
182
|
allowsTool?.(candidate.tool.name) !== false;
|
|
183
183
|
const browserCandidates = input.platform.browser
|
|
@@ -207,12 +207,12 @@ async function createToolSurface(
|
|
|
207
207
|
|
|
208
208
|
return {
|
|
209
209
|
surface: Object.freeze({
|
|
210
|
-
finalize(
|
|
210
|
+
finalize(candidates: readonly PiToolCandidate[]) {
|
|
211
211
|
const tools = new Map<string, PiToolCandidate>();
|
|
212
212
|
|
|
213
213
|
for (const candidate of [
|
|
214
214
|
...staticCandidates,
|
|
215
|
-
...
|
|
215
|
+
...candidates,
|
|
216
216
|
]) {
|
|
217
217
|
const name = requiredName(candidate.tool.name, "Pi Tool");
|
|
218
218
|
if (!visible(candidate)) continue;
|
|
@@ -576,7 +576,6 @@ export function toolRegistryPiToolCandidates(
|
|
|
576
576
|
const normalized = requiredName(name, "Platform Tool");
|
|
577
577
|
candidates.push(Object.freeze({
|
|
578
578
|
owner: "platform",
|
|
579
|
-
authorized: true,
|
|
580
579
|
requiredExecutionLevel: spec.requiredExecutionLevel,
|
|
581
580
|
...(spec.alwaysRequiresApproval
|
|
582
581
|
? { alwaysRequiresApproval: true }
|
|
@@ -633,6 +632,7 @@ function hooksToTurnEventsPort<
|
|
|
633
632
|
if (
|
|
634
633
|
!hooks.onTurnEnd &&
|
|
635
634
|
!hooks.onModelUsage &&
|
|
635
|
+
!hooks.onToolStart &&
|
|
636
636
|
!hooks.onToolSettled &&
|
|
637
637
|
!hooks.onApproval &&
|
|
638
638
|
!hooks.onActivityChanged &&
|
|
@@ -645,6 +645,7 @@ function hooksToTurnEventsPort<
|
|
|
645
645
|
? (messages) => hooks.onTurnEnd!(ctx, messages)
|
|
646
646
|
: async () => undefined,
|
|
647
647
|
...(hooks.onModelUsage ? { onModelUsage: hooks.onModelUsage } : {}),
|
|
648
|
+
...(hooks.onToolStart ? { onToolStart: hooks.onToolStart } : {}),
|
|
648
649
|
...(hooks.onToolSettled ? { onToolSettled: hooks.onToolSettled } : {}),
|
|
649
650
|
...(hooks.onApproval ? { onApproval: hooks.onApproval } : {}),
|
|
650
651
|
...(hooks.onActivityChanged
|
|
@@ -675,9 +676,12 @@ export async function assembleRuntimeSnapshot<
|
|
|
675
676
|
const provider = resources.provider;
|
|
676
677
|
let profile = settings.profile;
|
|
677
678
|
if (input.hooks?.profileOverrides) {
|
|
678
|
-
const overrides = await
|
|
679
|
-
|
|
680
|
-
input.
|
|
679
|
+
const overrides = await withRuntimeLoadTimeout(
|
|
680
|
+
"profile-overrides",
|
|
681
|
+
() => input.hooks!.profileOverrides!(
|
|
682
|
+
input.ctx,
|
|
683
|
+
input.config,
|
|
684
|
+
),
|
|
681
685
|
);
|
|
682
686
|
if (overrides.systemPrompt !== undefined) {
|
|
683
687
|
profile = { ...profile, systemPrompt: overrides.systemPrompt };
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
RuntimePlatformPort,
|
|
7
7
|
RuntimeProviderPort,
|
|
8
8
|
RuntimeSkillScriptPolicy,
|
|
9
|
+
RuntimeToolStartEvent,
|
|
9
10
|
RuntimeTurnMessage,
|
|
10
11
|
} from "./kernel/bindings";
|
|
11
12
|
import type {
|
|
@@ -135,6 +136,7 @@ export interface RuntimeAgentHooks<
|
|
|
135
136
|
messages: readonly RuntimeTurnMessage[],
|
|
136
137
|
) => Promise<void>;
|
|
137
138
|
readonly onModelUsage?: (event: RuntimeModelUsageEvent) => Promise<void>;
|
|
139
|
+
readonly onToolStart?: (event: RuntimeToolStartEvent) => Promise<void>;
|
|
138
140
|
readonly onToolSettled?: (event: RuntimeToolSettlementEvent) => Promise<void>;
|
|
139
141
|
readonly onApproval?: (input: ApprovalHookInput) => Promise<void>;
|
|
140
142
|
readonly onActivityChanged?: (
|
package/src/runtime.ts
CHANGED
|
@@ -45,7 +45,10 @@ import type {
|
|
|
45
45
|
RuntimeState,
|
|
46
46
|
RuntimeTurnState,
|
|
47
47
|
} from "./kernel/state";
|
|
48
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
RuntimeLoadTracker,
|
|
50
|
+
withRuntimeLoadTimeout,
|
|
51
|
+
} from "./kernel/runtime-load";
|
|
49
52
|
import {
|
|
50
53
|
prepareRuntimeCandidate,
|
|
51
54
|
type RuntimeAssemblyInput,
|
|
@@ -175,6 +178,16 @@ function json(value: unknown): string {
|
|
|
175
178
|
return JSON.stringify(value);
|
|
176
179
|
}
|
|
177
180
|
|
|
181
|
+
async function closeRuntimeGatewaySession(
|
|
182
|
+
session: RuntimeGatewaySession | undefined,
|
|
183
|
+
step: string,
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
if (!session) return;
|
|
186
|
+
await withRuntimeLoadTimeout(step, () => session.close()).catch(
|
|
187
|
+
() => undefined,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
function assistantUsageEvent(
|
|
179
192
|
eventId: string,
|
|
180
193
|
submissionId: string,
|
|
@@ -530,8 +543,15 @@ export abstract class AgentRuntimeKernel<
|
|
|
530
543
|
let gatewaySession: RuntimeGatewaySession | undefined;
|
|
531
544
|
if (candidateSnapshot.bindings.gateway) {
|
|
532
545
|
try {
|
|
533
|
-
gatewaySession = await
|
|
534
|
-
|
|
546
|
+
gatewaySession = await withRuntimeLoadTimeout(
|
|
547
|
+
"gateway.open",
|
|
548
|
+
() => candidateSnapshot.bindings.gateway!.open(
|
|
549
|
+
`${this.ctx.id.toString()}:${crypto.randomUUID()}`,
|
|
550
|
+
),
|
|
551
|
+
{
|
|
552
|
+
onLateResult: (session) =>
|
|
553
|
+
closeRuntimeGatewaySession(session, "gateway.close.late"),
|
|
554
|
+
},
|
|
535
555
|
);
|
|
536
556
|
} catch {
|
|
537
557
|
gatewayDegradations.push({
|
|
@@ -557,7 +577,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
557
577
|
),
|
|
558
578
|
});
|
|
559
579
|
} catch (error) {
|
|
560
|
-
await gatewaySession
|
|
580
|
+
await closeRuntimeGatewaySession(gatewaySession, "gateway.close.failed");
|
|
561
581
|
throw error;
|
|
562
582
|
}
|
|
563
583
|
const revision = await this.hash(prepared.revisionDescriptor);
|
|
@@ -569,18 +589,23 @@ export abstract class AgentRuntimeKernel<
|
|
|
569
589
|
this.submissions.isBusy();
|
|
570
590
|
const parked = contested && this.db.everyUnfinishedSubmissionParked();
|
|
571
591
|
if (contested && !parked) {
|
|
572
|
-
await gatewaySession
|
|
592
|
+
await closeRuntimeGatewaySession(gatewaySession, "gateway.close.contested");
|
|
573
593
|
throw new Error(
|
|
574
594
|
"Cannot reload Runtime while a revision-pinned Pi Turn is active",
|
|
575
595
|
);
|
|
576
596
|
}
|
|
577
597
|
const repin = parked
|
|
578
|
-
? await
|
|
598
|
+
? await withRuntimeLoadTimeout(
|
|
599
|
+
"parked-submission.repin",
|
|
600
|
+
() => this.prepareParkedSubmissionRepin(prepared, revision),
|
|
601
|
+
)
|
|
579
602
|
: undefined;
|
|
580
603
|
let activated = false;
|
|
581
604
|
const previousGatewaySession = this.runtimeGatewaySession;
|
|
582
605
|
try {
|
|
583
|
-
for (const guard of candidate.commitGuards)
|
|
606
|
+
for (const [index, guard] of candidate.commitGuards.entries()) {
|
|
607
|
+
await withRuntimeLoadTimeout(`commit-guard:${index}`, guard);
|
|
608
|
+
}
|
|
584
609
|
if (candidateSnapshot.bindings.platform.telemetryConsole) {
|
|
585
610
|
installConsoleSink();
|
|
586
611
|
}
|
|
@@ -594,13 +619,19 @@ export abstract class AgentRuntimeKernel<
|
|
|
594
619
|
} catch (error) {
|
|
595
620
|
repin?.abort();
|
|
596
621
|
if (activated && previous) this.pi.activate(previous);
|
|
597
|
-
await gatewaySession
|
|
622
|
+
await closeRuntimeGatewaySession(gatewaySession, "gateway.close.aborted");
|
|
598
623
|
throw error;
|
|
599
624
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
625
|
+
if (repin) {
|
|
626
|
+
await withRuntimeLoadTimeout(
|
|
627
|
+
"parked-submission.interrupt",
|
|
628
|
+
() => repin.interrupt(),
|
|
629
|
+
).catch(() => undefined);
|
|
630
|
+
}
|
|
631
|
+
await closeRuntimeGatewaySession(
|
|
632
|
+
previousGatewaySession,
|
|
633
|
+
"gateway.close.previous",
|
|
634
|
+
);
|
|
604
635
|
|
|
605
636
|
const degradations = prepared.degradations;
|
|
606
637
|
if (degradations.length > 0) {
|
|
@@ -1474,12 +1505,41 @@ export abstract class AgentRuntimeKernel<
|
|
|
1474
1505
|
kind: "record-tool-input",
|
|
1475
1506
|
record: input,
|
|
1476
1507
|
});
|
|
1477
|
-
|
|
1508
|
+
const appended = this.db.transaction(() =>
|
|
1478
1509
|
this.applyPiRecoveryMutations(
|
|
1479
1510
|
submission,
|
|
1480
1511
|
decision.mutations,
|
|
1481
1512
|
),
|
|
1482
1513
|
);
|
|
1514
|
+
if (appended) this.announceToolStart(submission.submissionId, input);
|
|
1515
|
+
return appended;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// 作用:把「这个 Tool 开始跑了」连同参数投给 Host。
|
|
1519
|
+
// 调用:durable intent 第一次落库之后,Tool 真正执行之前。
|
|
1520
|
+
// 原因:人在等的是「正在写一个网页」,不是三十秒后的「write 完成了」。
|
|
1521
|
+
//
|
|
1522
|
+
// 只在第一次落库时发:恢复重放会再走一遍这条路径,那时 Tool 可能早就结束了,
|
|
1523
|
+
// 再宣布一次「开始」比不宣布更糟。也不进 durable outbox —— 这是活性信号,
|
|
1524
|
+
// 补投一条迟到的开始事件没有意义,投失败就算了,终态有 onToolSettled 兜底。
|
|
1525
|
+
private announceToolStart(
|
|
1526
|
+
submissionId: string,
|
|
1527
|
+
input: PiToolInputRecord,
|
|
1528
|
+
): void {
|
|
1529
|
+
const turnEvents = this.turnEventsPort();
|
|
1530
|
+
if (!turnEvents?.onToolStart) return;
|
|
1531
|
+
const delivery = turnEvents.onToolStart({
|
|
1532
|
+
submissionId,
|
|
1533
|
+
toolCallId: input.toolCallId,
|
|
1534
|
+
toolName: input.toolName,
|
|
1535
|
+
args: input.input,
|
|
1536
|
+
}).catch((error: unknown) => {
|
|
1537
|
+
console.warn(
|
|
1538
|
+
"[runtime-tool-start:degraded]",
|
|
1539
|
+
json({ toolCallId: input.toolCallId, error: errorText(error) }),
|
|
1540
|
+
);
|
|
1541
|
+
});
|
|
1542
|
+
this.ctx.waitUntil(delivery);
|
|
1483
1543
|
}
|
|
1484
1544
|
|
|
1485
1545
|
// 作用:持久化一次 Tool 结果,并推进相应的恢复里程碑。
|
|
@@ -2099,7 +2159,9 @@ export abstract class AgentRuntimeKernel<
|
|
|
2099
2159
|
continuation: recovery,
|
|
2100
2160
|
assistantOrdinal,
|
|
2101
2161
|
onRecord: (record) =>
|
|
2102
|
-
this.
|
|
2162
|
+
this.migratedSubmissions.has(submissionId)
|
|
2163
|
+
? undefined
|
|
2164
|
+
: this.persistAndBroadcastRecord(turn, record),
|
|
2103
2165
|
onTerminal: (intent) => {
|
|
2104
2166
|
terminalIntent = intent;
|
|
2105
2167
|
this.appendTerminalIntent(
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defineRuntimeAgent,
|
|
3
|
-
type MutableRuntimeAgentDefinition,
|
|
4
|
-
type ReadonlyRuntimeAgentDefinition,
|
|
5
|
-
type RuntimeAgentClass,
|
|
6
|
-
type RuntimeAgentInstance,
|
|
7
|
-
} from "../../../runtime-agent";
|
|
8
|
-
|
|
9
|
-
export type CloudflareUniversalAgentInstance<
|
|
10
|
-
Env extends Cloudflare.Env = Cloudflare.Env,
|
|
11
|
-
Command = never,
|
|
12
|
-
Change = never,
|
|
13
|
-
> = RuntimeAgentInstance<Env, Command, Change> & {
|
|
14
|
-
revokeUserConnections(): Promise<void>;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export interface CloudflareUniversalAgentClass<
|
|
18
|
-
Env extends Cloudflare.Env = Cloudflare.Env,
|
|
19
|
-
Command = never,
|
|
20
|
-
Change = never,
|
|
21
|
-
> {
|
|
22
|
-
new (
|
|
23
|
-
ctx: DurableObjectState,
|
|
24
|
-
env: Env,
|
|
25
|
-
): CloudflareUniversalAgentInstance<Env, Command, Change>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function defineCloudflareUniversalAgent<
|
|
29
|
-
Env extends Cloudflare.Env,
|
|
30
|
-
Config,
|
|
31
|
-
Command,
|
|
32
|
-
Change,
|
|
33
|
-
>(
|
|
34
|
-
definition: MutableRuntimeAgentDefinition<Env, Config, Command, Change>,
|
|
35
|
-
): CloudflareUniversalAgentClass<Env, Command, Change>;
|
|
36
|
-
export function defineCloudflareUniversalAgent<
|
|
37
|
-
Env extends Cloudflare.Env = Cloudflare.Env,
|
|
38
|
-
Config = unknown,
|
|
39
|
-
>(
|
|
40
|
-
definition: ReadonlyRuntimeAgentDefinition<Env, Config>,
|
|
41
|
-
): CloudflareUniversalAgentClass<Env>;
|
|
42
|
-
export function defineCloudflareUniversalAgent<
|
|
43
|
-
Env extends Cloudflare.Env,
|
|
44
|
-
Config,
|
|
45
|
-
Command,
|
|
46
|
-
Change,
|
|
47
|
-
>(
|
|
48
|
-
definition:
|
|
49
|
-
| MutableRuntimeAgentDefinition<Env, Config, Command, Change>
|
|
50
|
-
| ReadonlyRuntimeAgentDefinition<Env, Config>,
|
|
51
|
-
): CloudflareUniversalAgentClass<Env, Command, Change> {
|
|
52
|
-
const Base = defineRuntimeAgent(
|
|
53
|
-
definition as MutableRuntimeAgentDefinition<
|
|
54
|
-
Env,
|
|
55
|
-
Config,
|
|
56
|
-
Command,
|
|
57
|
-
Change
|
|
58
|
-
>,
|
|
59
|
-
) as RuntimeAgentClass<Env, Command, Change>;
|
|
60
|
-
const Generated = {
|
|
61
|
-
[definition.name]: class extends Base {
|
|
62
|
-
async revokeUserConnections(): Promise<void> {
|
|
63
|
-
await this.stopTurn(undefined, "Session ended");
|
|
64
|
-
for (const connection of this.getConnections()) {
|
|
65
|
-
connection.close(1008, "Session ended");
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
}[definition.name];
|
|
70
|
-
return Generated as CloudflareUniversalAgentClass<Env, Command, Change>;
|
|
71
|
-
}
|