@springbrand/agent-runtime 0.1.3-alpha.0 → 0.1.3-alpha.10
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 +12 -3
- package/src/adapter/cloudflare/index.ts +56 -0
- package/src/adapter/cloudflare/resources/runtime-resources.ts +89 -0
- package/src/adapter/cloudflare/sandbox/adapter.ts +1513 -0
- package/src/adapter/cloudflare/sandbox/id.ts +23 -0
- package/src/adapter/cloudflare/sandbox/policy.ts +15 -0
- package/src/adapter/cloudflare/subagent/definition.ts +574 -0
- package/src/adapter/cloudflare/subagent/runner.ts +175 -0
- package/src/adapter/cloudflare/subagent/tools.ts +254 -0
- package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
- package/src/adapter/cloudflare/universal-agent/preparation.ts +277 -0
- package/src/adapter/cloudflare/universal-agent/tools.ts +80 -0
- package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
- package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -0
- package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
- package/src/agent-tool-runtime.ts +152 -0
- package/src/db/agent-tool.repo.ts +27 -0
- package/src/db/index.ts +33 -0
- package/src/db/interaction.repo.ts +185 -0
- package/src/db/schema.ts +25 -1
- package/src/db/submission.repo.ts +63 -1
- package/src/index.ts +61 -27
- package/src/kernel/approval-lifecycle.ts +41 -6
- package/src/kernel/bindings.ts +99 -12
- package/src/kernel/extensions.ts +1 -1
- package/src/kernel/interaction-lifecycle.ts +395 -0
- package/src/kernel/profile.ts +3 -4
- package/src/kernel/public-contracts.ts +2 -0
- package/src/kernel/recoverable-chat-agent.ts +104 -6
- package/src/kernel/runtime-assembly-view.ts +37 -0
- package/src/kernel/runtime-assembly.ts +41 -0
- package/src/kernel/runtime-config.ts +4 -0
- package/src/kernel/runtime-load.ts +191 -0
- package/src/kernel/state.ts +12 -1
- package/src/kernel/submission-lifecycle.ts +33 -2
- package/src/layers/orchestration/temporary-agent/core.ts +12 -1
- package/src/layers/orchestration/temporary-agent/runner.ts +1 -67
- package/src/lib/mcp.ts +7 -3
- package/src/lib/prompt.ts +1 -1
- package/src/lib/telemetry-dev.ts +7 -4
- package/src/pi/assembly/context.ts +4 -6
- package/src/pi/assembly/extensions.ts +11 -22
- package/src/pi/assembly/snapshot.ts +17 -9
- package/src/pi/message/contract.ts +7 -0
- package/src/pi/message/conversion.ts +9 -1
- package/src/pi/runtime-adapter/assembly.ts +42 -97
- package/src/pi/runtime-adapter/execution.ts +216 -21
- package/src/pi/runtime-adapter/index.ts +24 -8
- package/src/pi/runtime-adapter/models.ts +382 -35
- package/src/pi/runtime-adapter/recovery.ts +188 -1
- package/src/pi/runtime-adapter/transcript.ts +61 -3
- package/src/pi/tool/ai-adapter.ts +58 -1
- package/src/pi/tool/base.ts +190 -12
- package/src/pi/tool/compiler.ts +39 -33
- package/src/pi/tool/core-host.ts +28 -30
- package/src/pi/tool/core.ts +37 -124
- package/src/pi/tool/gateway.ts +54 -0
- package/src/pi/tool/index.ts +2 -0
- package/src/pi/tool/mcp.ts +98 -70
- package/src/pi/tool/schedule.ts +86 -20
- package/src/pi/tool/skill.ts +126 -420
- package/src/pi/tool/subagent.ts +14 -2
- package/src/pi/tool/web-fetch.ts +281 -0
- package/src/pi/tool/web-search/api.ts +34 -18
- package/src/pi/tool/web-search/web-search.ts +0 -1
- package/src/pi/tool/workspace-revision.ts +64 -0
- package/src/pi/tool/workspace-sandbox.ts +105 -263
- package/src/pi/turn/index.ts +20 -0
- package/src/pi/turn/interaction.ts +181 -0
- package/src/pi/turn/tool-recovery.ts +244 -1
- package/src/runtime-agent-context.ts +112 -0
- package/src/runtime-agent.ts +568 -321
- package/src/runtime-assembler.ts +797 -0
- package/src/runtime-definition.ts +175 -0
- package/src/runtime.ts +840 -208
- package/src/tool-registry.ts +143 -0
- package/src/workspace-versioning.ts +46 -0
- package/src/plugins.ts +0 -1033
package/src/kernel/bindings.ts
CHANGED
|
@@ -186,6 +186,17 @@ export interface WorkspacePort {
|
|
|
186
186
|
glob(pattern: string): Promise<WorkspaceFileInfo[]>;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/**
|
|
190
|
+
* 向 Runtime 提供已组装的 Workspace Code Mode 执行能力。
|
|
191
|
+
*
|
|
192
|
+
* @remarks
|
|
193
|
+
* Host Adapter 负责绑定 Durable Object、Worker Loader、网络出口和
|
|
194
|
+
* Workspace;Tool Surface 只把这个已授权 Port 转成 `execute` Tool。
|
|
195
|
+
*/
|
|
196
|
+
export interface RuntimeCodeExecutionPort {
|
|
197
|
+
execute(input: { code: string }): Promise<unknown>;
|
|
198
|
+
}
|
|
199
|
+
|
|
189
200
|
/**
|
|
190
201
|
* 汇总一个已限定作用域的 Workspace 用量。
|
|
191
202
|
*
|
|
@@ -283,9 +294,7 @@ export interface RuntimeMemoryPort {
|
|
|
283
294
|
* 按标签覆盖一块热记忆。
|
|
284
295
|
*
|
|
285
296
|
* @remarks
|
|
286
|
-
* Host
|
|
287
|
-
*
|
|
288
|
-
* TODO(待确认): 确认后续写入方是否仍需通过这个公开 Port 提交热记忆。
|
|
297
|
+
* `set_context` 在校验标签和 token 预算后调用,Host 负责按当前 Agent 与会话持久化。
|
|
289
298
|
*/
|
|
290
299
|
set(label: string, content: string): Promise<void>;
|
|
291
300
|
}
|
|
@@ -513,6 +522,41 @@ export interface RuntimeSandboxPort {
|
|
|
513
522
|
|
|
514
523
|
// #region Runtime 服务端口
|
|
515
524
|
|
|
525
|
+
/** Gateway MCP 发现后可供 Pi 装配的安全 Tool 描述。 */
|
|
526
|
+
export interface RuntimeGatewayMcpTool {
|
|
527
|
+
readonly name: string;
|
|
528
|
+
readonly title?: string;
|
|
529
|
+
readonly description?: string;
|
|
530
|
+
readonly inputSchema?: Record<string, unknown>;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Gateway safe Catalog 中经过认证的 Action Risk。 */
|
|
534
|
+
export type RuntimeGatewayActionRisk = "none" | "high";
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* 一次 Agent Runtime 独占的 Gateway MCP 逻辑 Session。
|
|
538
|
+
*
|
|
539
|
+
* Runtime Grant 由实现闭包持有;本接口只暴露发现、调用和可信风险解析,
|
|
540
|
+
* 因此 Grant 不会进入 Profile、Snapshot 描述或 transcript。
|
|
541
|
+
*/
|
|
542
|
+
export interface RuntimeGatewaySession {
|
|
543
|
+
readonly tools: readonly RuntimeGatewayMcpTool[];
|
|
544
|
+
callTool(
|
|
545
|
+
name: string,
|
|
546
|
+
input: Readonly<Record<string, unknown>>,
|
|
547
|
+
signal?: AbortSignal,
|
|
548
|
+
): Promise<unknown>;
|
|
549
|
+
resolveCapabilityRisk(
|
|
550
|
+
reference: string,
|
|
551
|
+
): Promise<RuntimeGatewayActionRisk>;
|
|
552
|
+
close(): Promise<void>;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Host 在 Runtime 启动时用它 mint Grant 并打开唯一 Gateway MCP Session。 */
|
|
556
|
+
export interface RuntimeGatewayPort {
|
|
557
|
+
open(runtimeId: string): Promise<RuntimeGatewaySession>;
|
|
558
|
+
}
|
|
559
|
+
|
|
516
560
|
/**
|
|
517
561
|
* 描述一条模型可见的定时任务。
|
|
518
562
|
*
|
|
@@ -540,6 +584,14 @@ export interface RuntimeScheduleUpdate {
|
|
|
540
584
|
tz?: string;
|
|
541
585
|
}
|
|
542
586
|
|
|
587
|
+
/** 定时任务可切换到的已授权 User Agent 摘要。 */
|
|
588
|
+
export interface RuntimeScheduleAgentSummary {
|
|
589
|
+
id: string;
|
|
590
|
+
name: string;
|
|
591
|
+
description: string | null;
|
|
592
|
+
isDefault: boolean;
|
|
593
|
+
}
|
|
594
|
+
|
|
543
595
|
/**
|
|
544
596
|
* 让 Runtime 通过 Host 管理当前用户的定时任务。
|
|
545
597
|
*
|
|
@@ -573,6 +625,10 @@ export interface RuntimeSchedulePort {
|
|
|
573
625
|
): Promise<{ ok: boolean }>;
|
|
574
626
|
pause(id: string): Promise<{ ok: boolean }>;
|
|
575
627
|
resume(id: string): Promise<{ ok: boolean }>;
|
|
628
|
+
/** 列出当前用户可用于执行定时任务的 User Agent。 */
|
|
629
|
+
listAgents(): Promise<RuntimeScheduleAgentSummary[]>;
|
|
630
|
+
/** 更换一条定时任务的执行 Agent。 */
|
|
631
|
+
changeAgent(id: string, userAgentId: string): Promise<{ ok: boolean }>;
|
|
576
632
|
/**
|
|
577
633
|
* 按 ID 取消一条定时任务。
|
|
578
634
|
*
|
|
@@ -674,6 +730,22 @@ export interface RuntimeModelUsageEvent {
|
|
|
674
730
|
};
|
|
675
731
|
}
|
|
676
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
|
+
|
|
677
749
|
export interface RuntimeToolSettlementEvent {
|
|
678
750
|
readonly eventId: string;
|
|
679
751
|
readonly submissionId: string;
|
|
@@ -705,6 +777,17 @@ export interface RuntimeTurnEventsPort {
|
|
|
705
777
|
): Promise<void>;
|
|
706
778
|
/** 投递 Assistant 或 Compaction 的原始 provider usage。 */
|
|
707
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>;
|
|
708
791
|
/** 投递不含参数与结果的 Tool 最终结算事件。 */
|
|
709
792
|
onToolSettled?(event: RuntimeToolSettlementEvent): Promise<void>;
|
|
710
793
|
onApproval?(input: {
|
|
@@ -780,13 +863,15 @@ export interface RuntimeModelEndpoint {
|
|
|
780
863
|
headers?: Readonly<Record<string, string>>;
|
|
781
864
|
baseURL: string;
|
|
782
865
|
models: readonly string[];
|
|
866
|
+
/** OpenRouter upstream provider pins keyed by model ID; pinned requests never fall back. */
|
|
867
|
+
openRouterProviderPins?: Readonly<Record<string, string>>;
|
|
783
868
|
}
|
|
784
869
|
|
|
785
870
|
/**
|
|
786
871
|
* 向 Runtime 提供本次装配可用的模型端点。
|
|
787
872
|
*
|
|
788
873
|
* @remarks
|
|
789
|
-
* Provider Plugin
|
|
874
|
+
* Provider Plugin 准备它,Builder 在提交前校验,Pi Adapter 在激活与每次解析模型时读取。
|
|
790
875
|
*
|
|
791
876
|
* Runtime 只依赖已解析结果,不知道环境变量或业务模型配置的来源。
|
|
792
877
|
*/
|
|
@@ -800,7 +885,7 @@ export interface RuntimeProviderPort {
|
|
|
800
885
|
* 向 Runtime 提供 Cloudflare 执行平台上的可授权能力。
|
|
801
886
|
*
|
|
802
887
|
* @remarks
|
|
803
|
-
* Platform Plugin
|
|
888
|
+
* Platform Plugin 准备它,Workspace Codemode、Browser 工具、遥测和工具门卫按需使用。
|
|
804
889
|
*
|
|
805
890
|
* Worker Loader 与网络出口由 Host 选择,使 Dynamic Worker 只获得已授权绑定;术语见 `../index.ts`。
|
|
806
891
|
*/
|
|
@@ -837,15 +922,15 @@ export interface RuntimePlatformPort {
|
|
|
837
922
|
}
|
|
838
923
|
|
|
839
924
|
/**
|
|
840
|
-
*
|
|
925
|
+
* 汇总本次装配已配置的 Skill 来源。
|
|
841
926
|
*
|
|
842
927
|
* @remarks
|
|
843
928
|
* Builder 在生成候选 Snapshot 时创建,Pi 组装在生成 Skill 工具时读取。
|
|
844
929
|
*
|
|
845
|
-
* catalog
|
|
930
|
+
* catalog、来源与脚本策略一起保留,避免装配期重新访问来源。
|
|
846
931
|
*/
|
|
847
932
|
export interface RuntimeSkillBindings {
|
|
848
|
-
/**
|
|
933
|
+
/** Configured external sources and their isolated script policy. */
|
|
849
934
|
sources: readonly RuntimeSkillSourceBinding[];
|
|
850
935
|
}
|
|
851
936
|
|
|
@@ -853,9 +938,9 @@ export interface RuntimeSkillBindings {
|
|
|
853
938
|
* 限定一个 Skill 脚本可使用的网络、Workspace 和工具。
|
|
854
939
|
*
|
|
855
940
|
* @remarks
|
|
856
|
-
* Skill Plugin
|
|
941
|
+
* Skill Plugin 在准备来源时设置,Pi Skill 工具在决定是否注入脚本执行能力时读取。
|
|
857
942
|
*
|
|
858
|
-
*
|
|
943
|
+
* 这是 Config 已解析的执行策略,执行期不应自动放宽。
|
|
859
944
|
*/
|
|
860
945
|
export interface RuntimeSkillScriptPolicy {
|
|
861
946
|
network: "none" | "full";
|
|
@@ -864,12 +949,12 @@ export interface RuntimeSkillScriptPolicy {
|
|
|
864
949
|
}
|
|
865
950
|
|
|
866
951
|
/**
|
|
867
|
-
*
|
|
952
|
+
* 把一个已配置 Skill 来源与其脚本策略放在同一快照中。
|
|
868
953
|
*
|
|
869
954
|
* @remarks
|
|
870
955
|
* Builder 为每个 Skill 贡献创建,Pi 组装和 Skill 工具按名称查找并读取。
|
|
871
956
|
*
|
|
872
|
-
*
|
|
957
|
+
* 名称、内容来源与脚本策略不分开存放,脚本 runner 才能按 Skill 名找到对应策略。
|
|
873
958
|
*/
|
|
874
959
|
export interface RuntimeSkillSourceBinding {
|
|
875
960
|
name: string;
|
|
@@ -890,6 +975,8 @@ export interface RuntimeSkillSourceBinding {
|
|
|
890
975
|
export interface RuntimeBindings {
|
|
891
976
|
provider: RuntimeProviderPort;
|
|
892
977
|
platform: RuntimePlatformPort;
|
|
978
|
+
/** Secret-capability binding; implementations must keep Runtime Grant in closure state. */
|
|
979
|
+
gateway?: RuntimeGatewayPort;
|
|
893
980
|
workspace?: WorkspacePort;
|
|
894
981
|
memory?: RuntimeMemoryPort;
|
|
895
982
|
skills: RuntimeSkillBindings;
|
package/src/kernel/extensions.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface RuntimeExtensionContextDefinition {
|
|
|
43
43
|
* 把 Extension 的 manifest 与待加载源码放在同一份配置里。
|
|
44
44
|
*
|
|
45
45
|
* @remarks
|
|
46
|
-
* extension Plugin
|
|
46
|
+
* extension Plugin 在准备结果中提供它,Pi Extension 适配器在候选 Runtime 装配时加载。
|
|
47
47
|
*
|
|
48
48
|
* manifest 与源码一起传递,可避免授权信息与实际执行代码在不同查找步骤中错位。
|
|
49
49
|
*
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isTerminalSubmissionStatus,
|
|
3
|
+
type RuntimeDatabase,
|
|
4
|
+
type StoredToolInteraction,
|
|
5
|
+
type SubmissionStatus,
|
|
6
|
+
} from "../db";
|
|
7
|
+
import type {
|
|
8
|
+
PiDurableMutation,
|
|
9
|
+
PiRecoveryCommand,
|
|
10
|
+
PiRecoveryDecision,
|
|
11
|
+
PiRuntimeAdapter,
|
|
12
|
+
PiToolInteraction,
|
|
13
|
+
PiToolInteractionCancelReason,
|
|
14
|
+
} from "../pi/runtime-adapter";
|
|
15
|
+
import type { ToolResultMessage } from "@earendil-works/pi-ai";
|
|
16
|
+
|
|
17
|
+
// #region 类型约定
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 提供 interaction 流程定位一次持久化提交所需的最小信息。
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* 与 `ApprovalSubmission` 同形。这里只要求 park/续跑真正需要的字段,
|
|
24
|
+
* 避免 interaction 流程依赖完整提交模型。
|
|
25
|
+
*/
|
|
26
|
+
export interface InteractionSubmission {
|
|
27
|
+
readonly submissionId: string;
|
|
28
|
+
readonly requestId: string;
|
|
29
|
+
readonly assemblyRevision: string;
|
|
30
|
+
readonly status: SubmissionStatus;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 表示已经绑定到持久化提交的完整 interaction 记录。
|
|
35
|
+
*/
|
|
36
|
+
export interface InteractionRecord extends PiToolInteraction {
|
|
37
|
+
readonly submissionId: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 描述一次响应或取消要写入的 ToolResult 载荷。
|
|
42
|
+
*
|
|
43
|
+
* `content` / `details` 由 Tool 自己的 `settle` 映射产出 —— 本流程不解释响应体语义。
|
|
44
|
+
* 取消不带载荷,由纯函数层用固定文案兜底。
|
|
45
|
+
*/
|
|
46
|
+
export interface InteractionSettlementPayload {
|
|
47
|
+
readonly content: ToolResultMessage["content"];
|
|
48
|
+
readonly details: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 宿主用这些依赖把 interaction 状态机接入数据库、Pi 恢复计算和 Turn 续跑。
|
|
52
|
+
interface InteractionLifecycleOptions<
|
|
53
|
+
TSubmission extends InteractionSubmission,
|
|
54
|
+
> {
|
|
55
|
+
readonly db: RuntimeDatabase;
|
|
56
|
+
readonly pi: PiRuntimeAdapter;
|
|
57
|
+
readonly findSubmission: (submissionId: string) => TSubmission | null;
|
|
58
|
+
readonly applyRecoveryMutations: (
|
|
59
|
+
submission: TSubmission,
|
|
60
|
+
mutations: readonly PiDurableMutation[],
|
|
61
|
+
) => boolean;
|
|
62
|
+
readonly materializeRecoveredToolResults: (
|
|
63
|
+
submission: TSubmission,
|
|
64
|
+
) => Promise<PiRecoveryDecision>;
|
|
65
|
+
/** 没有内存等待器时(DO 睡过一觉)把原 Turn 重新拉起来。 */
|
|
66
|
+
readonly resumeSubmission: (submissionId: string) => Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* pending 集合发生变化后通知宿主重算活动状态投影。
|
|
69
|
+
*
|
|
70
|
+
* park 和结算都会触发。宿主的 needs-input 是 pending 行的纯函数,
|
|
71
|
+
* 没有这个回调,park 的那一刻不会有人去重算 —— 侧栏要等下一次广播才翻牌。
|
|
72
|
+
*/
|
|
73
|
+
readonly onInteractionsChanged?: () => Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// #endregion
|
|
77
|
+
|
|
78
|
+
// #region interaction 流程 API
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 管理「结果由客户端提供」的 Tool 从 park、等待、响应到续跑的完整过程。
|
|
82
|
+
*
|
|
83
|
+
* @remarks
|
|
84
|
+
* 与 `ApprovalLifecycle` 结构同源但显著更小 —— 不需要 `allow_level` 的 Host 协调、
|
|
85
|
+
* 不需要批准后 `retryTool`、不需要执行档位矩阵。两者的重复是**当前有意接受的**:
|
|
86
|
+
* 审批那条路已经跑在线上,等 interaction 跑稳后再考虑抽公共核,届时改的是这两个类,
|
|
87
|
+
* 不是恢复状态机。
|
|
88
|
+
*
|
|
89
|
+
* 续跑不走 interaction 专属的调度键:响应会同时写下一条权威 `tool-result` 里程碑,
|
|
90
|
+
* 因此复用 `planPiToolRecovery` 中既有的 `tool:<id>:settled` 分支。
|
|
91
|
+
*/
|
|
92
|
+
export class InteractionLifecycle<
|
|
93
|
+
TSubmission extends InteractionSubmission,
|
|
94
|
+
> {
|
|
95
|
+
private readonly waiters = new Map<string, () => void>();
|
|
96
|
+
|
|
97
|
+
constructor(
|
|
98
|
+
private readonly options: InteractionLifecycleOptions<TSubmission>,
|
|
99
|
+
) {}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 用工具调用标识查找唯一一条仍待响应的 interaction。
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* `Runtime.respondToolInteraction` 调用它 —— 前端只有 `toolCallId`。
|
|
106
|
+
* 调用方必须把 `null` 当作没有安全匹配,不应猜测要处理哪条记录。
|
|
107
|
+
*/
|
|
108
|
+
findPending(toolCallId: string): InteractionRecord | null {
|
|
109
|
+
const stored = this.options.db.interactions
|
|
110
|
+
.findPendingByToolCallId(toolCallId);
|
|
111
|
+
return stored ? toRecord(stored) : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 按 interactionId 读取一条完整记录,不存在时返回 null。
|
|
116
|
+
*/
|
|
117
|
+
read(interactionId: string): InteractionRecord | null {
|
|
118
|
+
const stored = this.options.db.interactions.find(interactionId);
|
|
119
|
+
return stored ? toRecord(stored) : null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 持久化一条 interaction,并暂停当前工具执行直到客户端投递响应。
|
|
124
|
+
*
|
|
125
|
+
* @remarks
|
|
126
|
+
* 执行适配器在 Tool 声明了 `interaction` 时调用它。
|
|
127
|
+
* 记录与恢复里程碑先在事务中落盘,再建立内存等待,保证重启仍能恢复 park 状态。
|
|
128
|
+
*/
|
|
129
|
+
async request(
|
|
130
|
+
submission: TSubmission,
|
|
131
|
+
interaction: PiToolInteraction,
|
|
132
|
+
signal?: AbortSignal,
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
const pending = this.options.db.transaction(() =>
|
|
135
|
+
this.ensurePending(submission, interaction)
|
|
136
|
+
);
|
|
137
|
+
// 必须在 wait 之前通知:wait 会一直挂到客户端投递,之后再通知就晚了一个回合。
|
|
138
|
+
await this.options.onInteractionsChanged?.();
|
|
139
|
+
return this.wait(pending, signal);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 应用客户端的首次响应,结算对应 Tool 并唤醒原 Turn。
|
|
144
|
+
*
|
|
145
|
+
* @remarks
|
|
146
|
+
* `Runtime.respondToolInteraction` 在校验并映射出 ToolResult 后调用它。
|
|
147
|
+
* 不存在、已结束或提交已终态都返回 `{ ok: false }`,不抛 —— 口径对齐 `ApprovalLifecycle.decide`。
|
|
148
|
+
*/
|
|
149
|
+
async respond(
|
|
150
|
+
interactionId: string,
|
|
151
|
+
response: unknown,
|
|
152
|
+
payload: InteractionSettlementPayload,
|
|
153
|
+
): Promise<{ ok: boolean }> {
|
|
154
|
+
return this.settle(interactionId, {
|
|
155
|
+
kind: "respond",
|
|
156
|
+
response,
|
|
157
|
+
result: payload,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 在用户没有作答的情况下结束一条 interaction 并让 Turn 继续。
|
|
163
|
+
*
|
|
164
|
+
* @remarks
|
|
165
|
+
* park 期间收到用户消息时调用(`user_replied_freeform`),提交进终态时也调用。
|
|
166
|
+
* 取消同样产出稳定 ToolResult,不是 Tool 失败 —— 否则模型会收到 tool_error 并倾向重试。
|
|
167
|
+
*/
|
|
168
|
+
async cancel(
|
|
169
|
+
interactionId: string,
|
|
170
|
+
reason: PiToolInteractionCancelReason,
|
|
171
|
+
): Promise<{ ok: boolean }> {
|
|
172
|
+
return this.settle(interactionId, { kind: "cancel", reason });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 把某次提交下仍待响应的全部 interaction 一次性取消。
|
|
177
|
+
*
|
|
178
|
+
* @remarks
|
|
179
|
+
* `dispatchMessage` 发现 park 期间来了用户消息时、以及提交进终态时调用。
|
|
180
|
+
* 返回是否真的取消过记录,调用方据此决定要不要广播。
|
|
181
|
+
*/
|
|
182
|
+
async cancelPendingForSubmission(
|
|
183
|
+
submissionId: string,
|
|
184
|
+
reason: PiToolInteractionCancelReason,
|
|
185
|
+
): Promise<boolean> {
|
|
186
|
+
const pending = this.options.db.interactions
|
|
187
|
+
.listPendingForSubmission(submissionId);
|
|
188
|
+
if (pending.length === 0) return false;
|
|
189
|
+
let cancelled = false;
|
|
190
|
+
for (const interactionId of pending) {
|
|
191
|
+
const result = await this.cancel(interactionId, reason);
|
|
192
|
+
cancelled ||= result.ok;
|
|
193
|
+
}
|
|
194
|
+
return cancelled;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 在同步终态事务里把某次提交下仍待响应的 interaction 全部取消。
|
|
199
|
+
*
|
|
200
|
+
* @remarks
|
|
201
|
+
* `Runtime.commitTerminalOutcome` 在提交终态的 `transaction` 内调用,因此必须是同步的。
|
|
202
|
+
* 这里只推进状态和里程碑,**不** materialize、**不** 续跑 —— Turn 正在结束,
|
|
203
|
+
* 调用方负责在同一事务内调用 `materializeRecoveredToolResultsSync`,口径对齐审批的 `rejectPending`。
|
|
204
|
+
*
|
|
205
|
+
* 返回是否真的取消过记录;没有 pending 时返回 false,调用方可据此跳过 materialize。
|
|
206
|
+
*/
|
|
207
|
+
cancelPendingForSubmissionSync(
|
|
208
|
+
submission: TSubmission,
|
|
209
|
+
reason: PiToolInteractionCancelReason = "submission_terminal",
|
|
210
|
+
): boolean {
|
|
211
|
+
const pending = this.options.db.interactions
|
|
212
|
+
.listPendingForSubmission(submission.submissionId);
|
|
213
|
+
if (pending.length === 0) return false;
|
|
214
|
+
for (const interactionId of pending) {
|
|
215
|
+
const decision = this.decideRecovery(submission, {
|
|
216
|
+
kind: "interaction-settlement",
|
|
217
|
+
interactionId,
|
|
218
|
+
settlement: { kind: "cancel", reason },
|
|
219
|
+
});
|
|
220
|
+
this.options.applyRecoveryMutations(submission, decision.mutations);
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 判断某次提交当前是否停在等待客户端响应上。
|
|
227
|
+
*
|
|
228
|
+
* @remarks
|
|
229
|
+
* `dispatchMessage` 用它决定要不要把 delivery 提升成 steer —— 否则 enqueue 的消息
|
|
230
|
+
* 要等这个 Turn 结束,而这个 Turn 正在等一个不会来的答案,死锁。
|
|
231
|
+
*/
|
|
232
|
+
hasPendingForSubmission(submissionId: string): boolean {
|
|
233
|
+
return this.options.db.interactions
|
|
234
|
+
.listPendingForSubmission(submissionId).length > 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// #endregion
|
|
238
|
+
|
|
239
|
+
// #region 内部
|
|
240
|
+
|
|
241
|
+
// 作用:把响应或取消统一走同一条持久化 + 唤醒路径。
|
|
242
|
+
// 调用:`respond` 和 `cancel`。
|
|
243
|
+
// 原因:两种结局的差别只在 ToolResult 从哪来,状态推进和续跑必须完全一致。
|
|
244
|
+
private async settle(
|
|
245
|
+
interactionId: string,
|
|
246
|
+
settlement:
|
|
247
|
+
| {
|
|
248
|
+
kind: "respond";
|
|
249
|
+
response: unknown;
|
|
250
|
+
result: InteractionSettlementPayload;
|
|
251
|
+
}
|
|
252
|
+
| { kind: "cancel"; reason: PiToolInteractionCancelReason },
|
|
253
|
+
): Promise<{ ok: boolean }> {
|
|
254
|
+
const stored = this.read(interactionId);
|
|
255
|
+
if (!stored) return { ok: false };
|
|
256
|
+
const submission = this.options.findSubmission(stored.submissionId);
|
|
257
|
+
if (!submission || isTerminalSubmissionStatus(submission.status)) {
|
|
258
|
+
return { ok: false };
|
|
259
|
+
}
|
|
260
|
+
if (stored.status !== "pending") return { ok: false };
|
|
261
|
+
|
|
262
|
+
const decision = this.decideRecovery(submission, {
|
|
263
|
+
kind: "interaction-settlement",
|
|
264
|
+
interactionId,
|
|
265
|
+
settlement,
|
|
266
|
+
});
|
|
267
|
+
if (!decision.applied) return { ok: false };
|
|
268
|
+
this.options.db.transaction(() => {
|
|
269
|
+
this.options.applyRecoveryMutations(submission, decision.mutations);
|
|
270
|
+
});
|
|
271
|
+
const persisted = this.read(interactionId);
|
|
272
|
+
if (!persisted || persisted.status === "pending") {
|
|
273
|
+
return { ok: false };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 把刚写下的权威 tool-result 落成 settlement 行 —— park 住的执行链醒来后
|
|
277
|
+
// 只认 findToolSettlement,不读里程碑。这一步必须早于唤醒。
|
|
278
|
+
await this.options.materializeRecoveredToolResults(submission);
|
|
279
|
+
// 先重算活动状态再唤醒:唤醒会把 Turn 一路跑下去,晚通知就会让「等待回复」
|
|
280
|
+
// 在续跑期间多挂一段时间。
|
|
281
|
+
await this.options.onInteractionsChanged?.();
|
|
282
|
+
|
|
283
|
+
const waiter = this.waiters.get(interactionId);
|
|
284
|
+
if (waiter) {
|
|
285
|
+
waiter();
|
|
286
|
+
return { ok: true };
|
|
287
|
+
}
|
|
288
|
+
// 没有内存等待器 = DO 在 park 期间睡过一觉,原 Turn 的执行栈已经没了。
|
|
289
|
+
// 走持久恢复把它重新拉起来,里程碑里的 tool-result 会被当成已结算结果复用。
|
|
290
|
+
await this.options.resumeSubmission(submission.submissionId);
|
|
291
|
+
return { ok: true };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 作用:读取一个 Submission 的全部恢复里程碑正文。
|
|
295
|
+
// 原因:持久化历史必须作为恢复决策的唯一输入。
|
|
296
|
+
private milestoneBodies(submissionId: string): string[] {
|
|
297
|
+
return this.options.db.milestones.listBodies(submissionId);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 作用:请求 Pi 根据当前里程碑给出下一步。
|
|
301
|
+
// 原因:所有路径共用同一决策入口,避免各自改写恢复状态。
|
|
302
|
+
private decideRecovery(
|
|
303
|
+
submission: TSubmission,
|
|
304
|
+
command: PiRecoveryCommand = { kind: "inspect" },
|
|
305
|
+
): PiRecoveryDecision {
|
|
306
|
+
return this.options.pi.decideRecovery({
|
|
307
|
+
milestoneBodies: this.milestoneBodies(submission.submissionId),
|
|
308
|
+
identity: {
|
|
309
|
+
turnId: submission.submissionId,
|
|
310
|
+
assemblyRevision: submission.assemblyRevision,
|
|
311
|
+
},
|
|
312
|
+
command,
|
|
313
|
+
now: Date.now(),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 作用:复用或创建一条 pending interaction。
|
|
318
|
+
// 调用:`request` 在准入事务内调用。
|
|
319
|
+
// 原因:已存在记录的字段冲突必须报错,不能把响应投给另一次调用。
|
|
320
|
+
private ensurePending(
|
|
321
|
+
submission: TSubmission,
|
|
322
|
+
interaction: PiToolInteraction,
|
|
323
|
+
): InteractionRecord {
|
|
324
|
+
const existing = this.read(interaction.interactionId);
|
|
325
|
+
if (existing) {
|
|
326
|
+
if (
|
|
327
|
+
existing.requestId !== submission.requestId ||
|
|
328
|
+
existing.toolCallId !== interaction.toolCallId ||
|
|
329
|
+
existing.toolName !== interaction.toolName ||
|
|
330
|
+
existing.inputJson !== interaction.inputJson
|
|
331
|
+
) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Conflicting durable Tool interaction: ${interaction.toolCallId}`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return existing;
|
|
337
|
+
}
|
|
338
|
+
// 表行和恢复里程碑都由 record-interaction 这一条命令产出,
|
|
339
|
+
// 不在这里直接 insert —— 两份状态分叉正是恢复最难查的一类 bug。
|
|
340
|
+
const decision = this.decideRecovery(submission, {
|
|
341
|
+
kind: "record-interaction",
|
|
342
|
+
interaction,
|
|
343
|
+
});
|
|
344
|
+
this.options.applyRecoveryMutations(submission, decision.mutations);
|
|
345
|
+
return { ...interaction, submissionId: submission.submissionId };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 作用:等待当前进程里的客户端响应。
|
|
349
|
+
// 原因:内存 Promise 只负责唤醒当前请求,数据库仍负责持久状态以支持重启。
|
|
350
|
+
private wait(
|
|
351
|
+
interaction: InteractionRecord,
|
|
352
|
+
signal?: AbortSignal,
|
|
353
|
+
): Promise<void> {
|
|
354
|
+
if (interaction.status !== "pending") return Promise.resolve();
|
|
355
|
+
return new Promise((resolve, reject) => {
|
|
356
|
+
const abort = () => {
|
|
357
|
+
this.waiters.delete(interaction.interactionId);
|
|
358
|
+
reject(signal?.reason ?? new Error("Tool interaction aborted"));
|
|
359
|
+
};
|
|
360
|
+
if (signal?.aborted) {
|
|
361
|
+
abort();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
365
|
+
this.waiters.set(interaction.interactionId, () => {
|
|
366
|
+
signal?.removeEventListener("abort", abort);
|
|
367
|
+
this.waiters.delete(interaction.interactionId);
|
|
368
|
+
resolve();
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// #endregion
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 把持久行转成流程内部使用的记录。
|
|
377
|
+
// find / findPending 查到记录后调用,上层因而只看 Pi 的 interaction 形状。
|
|
378
|
+
function toRecord(stored: StoredToolInteraction): InteractionRecord {
|
|
379
|
+
return {
|
|
380
|
+
interactionId: stored.interactionId,
|
|
381
|
+
submissionId: stored.submissionId,
|
|
382
|
+
requestId: stored.requestId,
|
|
383
|
+
toolCallId: stored.toolCallId,
|
|
384
|
+
toolName: stored.toolName,
|
|
385
|
+
inputJson: stored.inputJson,
|
|
386
|
+
status: stored.status,
|
|
387
|
+
createdAt: stored.createdAt,
|
|
388
|
+
...(stored.respondedAt === null
|
|
389
|
+
? {}
|
|
390
|
+
: { respondedAt: stored.respondedAt }),
|
|
391
|
+
...(stored.responseJson === null
|
|
392
|
+
? {}
|
|
393
|
+
: { responseJson: stored.responseJson }),
|
|
394
|
+
};
|
|
395
|
+
}
|
package/src/kernel/profile.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type ThinkingEffort =
|
|
|
22
22
|
* 保存一次装配选定的内存开关与上下文预算。
|
|
23
23
|
*
|
|
24
24
|
* @remarks
|
|
25
|
-
* memory Plugin
|
|
25
|
+
* memory Plugin 在准备结果中提供它,Runtime 在构建上下文时读取。
|
|
26
26
|
*
|
|
27
27
|
* 预算与开关放在同一个 Profile 中,确保一次 Snapshot 只使用一套内存配置。
|
|
28
28
|
*
|
|
@@ -32,14 +32,13 @@ export interface RuntimeMemoryProfile {
|
|
|
32
32
|
enabled: boolean;
|
|
33
33
|
memoryTokens: number;
|
|
34
34
|
preferencesTokens: number;
|
|
35
|
-
compactAfterTokens: number;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
/**
|
|
39
38
|
* 指明本次装配允许连接的一个 MCP 服务。
|
|
40
39
|
*
|
|
41
40
|
* @remarks
|
|
42
|
-
* connector Plugin
|
|
41
|
+
* connector Plugin 在准备结果中加入它,Pi 工具装配只选择 Host 已就绪且 URL 匹配的连接。
|
|
43
42
|
*
|
|
44
43
|
* 这里只保留名称和 URL;连接生命周期、凭据与重试仍由 Host 管理。
|
|
45
44
|
*
|
|
@@ -71,7 +70,7 @@ export interface RuntimeDenyPolicy {
|
|
|
71
70
|
* 保存一次装配中真正会影响执行的参数。
|
|
72
71
|
*
|
|
73
72
|
* @remarks
|
|
74
|
-
* `RuntimeBuilder` 在所有 Plugin
|
|
73
|
+
* `RuntimeBuilder` 在所有 Plugin 声明合并完成后生成它,Kernel 只通过已提交的 Snapshot 读取。
|
|
75
74
|
*
|
|
76
75
|
* 它不包含业务身份、归属键或存储地址,因为这些选择属于 Host,不应被冻结成执行参数。
|
|
77
76
|
*
|