@springbrand/agent-runtime 0.1.3-alpha.3 → 0.1.3-alpha.4
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 +1 -1
- 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 +15 -0
- package/src/db/submission.repo.ts +29 -0
- package/src/index.ts +7 -17
- package/src/kernel/approval-lifecycle.ts +41 -6
- package/src/kernel/bindings.ts +37 -0
- package/src/kernel/interaction-lifecycle.ts +395 -0
- package/src/kernel/public-contracts.ts +2 -0
- package/src/kernel/recoverable-chat-agent.ts +10 -2
- 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 +102 -0
- package/src/kernel/state.ts +8 -1
- package/src/kernel/submission-lifecycle.ts +30 -0
- package/src/pi/runtime-adapter/assembly.ts +13 -1
- package/src/pi/runtime-adapter/execution.ts +109 -9
- package/src/pi/runtime-adapter/index.ts +9 -3
- package/src/pi/runtime-adapter/recovery.ts +188 -1
- package/src/pi/tool/base.ts +62 -9
- package/src/pi/tool/compiler.ts +34 -0
- package/src/pi/tool/gateway.ts +54 -0
- package/src/pi/tool/index.ts +1 -0
- package/src/pi/tool/mcp.ts +93 -64
- 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.ts +246 -113
- package/src/{plugins.ts → runtime-assembler.ts} +65 -282
- package/src/runtime.ts +440 -159
|
@@ -5,6 +5,13 @@ import {
|
|
|
5
5
|
type PiApprovalOutcome,
|
|
6
6
|
type PiToolApproval,
|
|
7
7
|
} from "./approval";
|
|
8
|
+
import {
|
|
9
|
+
cancelPiToolInteraction,
|
|
10
|
+
respondPiToolInteraction,
|
|
11
|
+
type PiToolInteraction,
|
|
12
|
+
type PiToolInteractionCancelReason,
|
|
13
|
+
type PiToolInteractionOutcome,
|
|
14
|
+
} from "./interaction";
|
|
8
15
|
|
|
9
16
|
/**
|
|
10
17
|
* 本文件实现 Pi Tool、审批、续跑和终态里程碑的纯恢复状态机。
|
|
@@ -52,12 +59,28 @@ export type PiToolRecoveryMilestone =
|
|
|
52
59
|
type: "approval";
|
|
53
60
|
approval: PiToolApproval;
|
|
54
61
|
})
|
|
62
|
+
| (RecoveryIdentity & {
|
|
63
|
+
type: "interaction";
|
|
64
|
+
interaction: PiToolInteraction;
|
|
65
|
+
})
|
|
55
66
|
| (RecoveryIdentity & {
|
|
56
67
|
type: "continuation";
|
|
57
68
|
continuationKey: string;
|
|
58
69
|
phase: "pending" | "committed";
|
|
59
70
|
timestamp: number;
|
|
60
71
|
})
|
|
72
|
+
/**
|
|
73
|
+
* 一次被承认的装配迁移:这条 Submission 停在人机等待期间换了装配。
|
|
74
|
+
*
|
|
75
|
+
* `assemblyRevision` 是迁移前那份(所以它自己通过身份校验),
|
|
76
|
+
* `nextAssemblyRevision` 是迁移后那份。重放读到它之前按旧 revision 校验
|
|
77
|
+
* 身份,读到之后按新的。同一条 Submission 允许发生多次。
|
|
78
|
+
*/
|
|
79
|
+
| (RecoveryIdentity & {
|
|
80
|
+
type: "assembly-repin";
|
|
81
|
+
nextAssemblyRevision: string;
|
|
82
|
+
timestamp: number;
|
|
83
|
+
})
|
|
61
84
|
| (RecoveryIdentity & {
|
|
62
85
|
type: "terminal";
|
|
63
86
|
phase: "intent" | "committed";
|
|
@@ -93,6 +116,7 @@ export interface PiToolRecoveryState {
|
|
|
93
116
|
assemblyRevision?: string;
|
|
94
117
|
toolCalls: Readonly<Record<string, RecoveredToolCall>>;
|
|
95
118
|
approvals: Readonly<Record<string, PiToolApproval>>;
|
|
119
|
+
interactions: Readonly<Record<string, PiToolInteraction>>;
|
|
96
120
|
continuations: Readonly<
|
|
97
121
|
Record<string, "pending" | "committed">
|
|
98
122
|
>;
|
|
@@ -113,6 +137,11 @@ export type PiToolRecoveryPlan =
|
|
|
113
137
|
executionId: string;
|
|
114
138
|
chargeRecoveryBudget: false;
|
|
115
139
|
}
|
|
140
|
+
| {
|
|
141
|
+
kind: "parked-interaction";
|
|
142
|
+
interactionId: string;
|
|
143
|
+
chargeRecoveryBudget: false;
|
|
144
|
+
}
|
|
116
145
|
| {
|
|
117
146
|
kind: "retry-tool";
|
|
118
147
|
toolCallId: string;
|
|
@@ -168,6 +197,19 @@ export interface RecoveredPiApprovalDecision {
|
|
|
168
197
|
outcome: PiApprovalOutcome;
|
|
169
198
|
}
|
|
170
199
|
|
|
200
|
+
/**
|
|
201
|
+
* 表示恢复路径应用一次 interaction 响应或取消后需要追加的里程碑和动作。
|
|
202
|
+
*
|
|
203
|
+
* Runtime 收到 respond 或 cancel 命令时同时持久化 `milestones` 并处理 `outcome`。
|
|
204
|
+
*
|
|
205
|
+
* 与审批不同的是,settle 分支的 `tool-result` 里程碑带 `needsContinuation: true`:
|
|
206
|
+
* interaction 没有自己的续跑键,续跑完全依赖既有的 `tool:<id>:settled` 路径。
|
|
207
|
+
*/
|
|
208
|
+
export interface RecoveredPiToolInteractionSettlement {
|
|
209
|
+
milestones: PiToolRecoveryMilestone[];
|
|
210
|
+
outcome: PiToolInteractionOutcome;
|
|
211
|
+
}
|
|
212
|
+
|
|
171
213
|
// #endregion
|
|
172
214
|
|
|
173
215
|
// #region 里程碑编码与校验
|
|
@@ -256,6 +298,28 @@ function isApproval(value: unknown): value is PiToolApproval {
|
|
|
256
298
|
);
|
|
257
299
|
}
|
|
258
300
|
|
|
301
|
+
// 校验从持久 JSON 读出的值是否是完整的 Pi Tool interaction 记录。
|
|
302
|
+
// 里程碑解码器处理 interaction 事件时调用它。
|
|
303
|
+
// 状态采用封闭枚举,未知协议值会让本次重放停在可信前缀 —— 与 isApproval 同一口径。
|
|
304
|
+
function isInteraction(value: unknown): value is PiToolInteraction {
|
|
305
|
+
return (
|
|
306
|
+
isRecord(value) &&
|
|
307
|
+
typeof value.interactionId === "string" &&
|
|
308
|
+
typeof value.requestId === "string" &&
|
|
309
|
+
typeof value.toolCallId === "string" &&
|
|
310
|
+
typeof value.toolName === "string" &&
|
|
311
|
+
typeof value.inputJson === "string" &&
|
|
312
|
+
typeof value.createdAt === "number" &&
|
|
313
|
+
(value.respondedAt === undefined ||
|
|
314
|
+
typeof value.respondedAt === "number") &&
|
|
315
|
+
(value.responseJson === undefined ||
|
|
316
|
+
typeof value.responseJson === "string") &&
|
|
317
|
+
(value.status === "pending" ||
|
|
318
|
+
value.status === "responded" ||
|
|
319
|
+
value.status === "cancelled")
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
259
323
|
/**
|
|
260
324
|
* 把一个 Pi Tool 恢复里程碑编码成可持久化的 JSON 正文。
|
|
261
325
|
*
|
|
@@ -315,6 +379,10 @@ export function decodePiToolRecoveryMilestone(
|
|
|
315
379
|
return isApproval(value.approval)
|
|
316
380
|
? (value as PiToolRecoveryMilestone)
|
|
317
381
|
: null;
|
|
382
|
+
case "interaction":
|
|
383
|
+
return isInteraction(value.interaction)
|
|
384
|
+
? (value as PiToolRecoveryMilestone)
|
|
385
|
+
: null;
|
|
318
386
|
case "continuation":
|
|
319
387
|
return typeof value.continuationKey === "string" &&
|
|
320
388
|
(value.phase === "pending" ||
|
|
@@ -322,6 +390,12 @@ export function decodePiToolRecoveryMilestone(
|
|
|
322
390
|
typeof value.timestamp === "number"
|
|
323
391
|
? (value as PiToolRecoveryMilestone)
|
|
324
392
|
: null;
|
|
393
|
+
case "assembly-repin":
|
|
394
|
+
return typeof value.nextAssemblyRevision === "string" &&
|
|
395
|
+
value.nextAssemblyRevision.length > 0 &&
|
|
396
|
+
typeof value.timestamp === "number"
|
|
397
|
+
? (value as PiToolRecoveryMilestone)
|
|
398
|
+
: null;
|
|
325
399
|
case "terminal":
|
|
326
400
|
return (value.phase === "intent" ||
|
|
327
401
|
value.phase === "committed") &&
|
|
@@ -390,6 +464,7 @@ export function replayPiToolRecovery(
|
|
|
390
464
|
): PiToolRecoveryState {
|
|
391
465
|
const tools: Record<string, RecoveredToolCall> = {};
|
|
392
466
|
const approvals: Record<string, PiToolApproval> = {};
|
|
467
|
+
const interactions: Record<string, PiToolInteraction> = {};
|
|
393
468
|
const continuations: Record<
|
|
394
469
|
string,
|
|
395
470
|
"pending" | "committed"
|
|
@@ -398,6 +473,7 @@ export function replayPiToolRecovery(
|
|
|
398
473
|
let state: PiToolRecoveryState = {
|
|
399
474
|
toolCalls: tools,
|
|
400
475
|
approvals,
|
|
476
|
+
interactions,
|
|
401
477
|
continuations,
|
|
402
478
|
canonicalToolResults: results,
|
|
403
479
|
};
|
|
@@ -488,6 +564,27 @@ export function replayPiToolRecovery(
|
|
|
488
564
|
}
|
|
489
565
|
break;
|
|
490
566
|
}
|
|
567
|
+
case "interaction": {
|
|
568
|
+
const previous = interactions[milestone.interaction.interactionId];
|
|
569
|
+
// 与 approval 同一口径:pending 可以前进到一个结局,但已结局的记录不能被翻转。
|
|
570
|
+
// 同状态重复记录允许刷新完整持久快照,同时保持结果不变。
|
|
571
|
+
if (
|
|
572
|
+
!previous ||
|
|
573
|
+
previous.status === "pending" ||
|
|
574
|
+
previous.status === milestone.interaction.status
|
|
575
|
+
) {
|
|
576
|
+
interactions[milestone.interaction.interactionId] =
|
|
577
|
+
milestone.interaction;
|
|
578
|
+
}
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
case "assembly-repin":
|
|
582
|
+
// 身份校验已经按迁移前的 revision 通过;从这里往后的里程碑属于新装配。
|
|
583
|
+
state = {
|
|
584
|
+
...state,
|
|
585
|
+
assemblyRevision: milestone.nextAssemblyRevision,
|
|
586
|
+
};
|
|
587
|
+
break;
|
|
491
588
|
case "continuation":
|
|
492
589
|
// committed 是单调终态,迟到的 pending 记录不能让续跑再次被调度。
|
|
493
590
|
if (
|
|
@@ -529,6 +626,7 @@ export function replayPiToolRecovery(
|
|
|
529
626
|
...state,
|
|
530
627
|
toolCalls: tools,
|
|
531
628
|
approvals,
|
|
629
|
+
interactions,
|
|
532
630
|
continuations,
|
|
533
631
|
canonicalToolResults: results,
|
|
534
632
|
};
|
|
@@ -574,7 +672,8 @@ function continuationPlan(
|
|
|
574
672
|
*
|
|
575
673
|
* Runtime 恢复适配器在每次状态变化后调用它,并只执行返回计划对应的一个动作。
|
|
576
674
|
*
|
|
577
|
-
*
|
|
675
|
+
* 优先级固定为终态、已决定审批、待审批、待响应 interaction、已结算 Tool、未结算 Tool,
|
|
676
|
+
* 不能调换到越过终态或用户许可。
|
|
578
677
|
*/
|
|
579
678
|
export function planPiToolRecovery(
|
|
580
679
|
state: PiToolRecoveryState,
|
|
@@ -619,6 +718,18 @@ export function planPiToolRecovery(
|
|
|
619
718
|
chargeRecoveryBudget: false,
|
|
620
719
|
};
|
|
621
720
|
}
|
|
721
|
+
// 仍等客户端投递结果的 interaction 同样保持 parked。
|
|
722
|
+
// 已响应或已取消的 interaction 不在这里出现:它们在写入本记录的同一批里程碑中
|
|
723
|
+
// 一并写了 tool-result,因此直接落到下面「已结算 Tool 补齐续跑」那条既有路径,
|
|
724
|
+
// 不需要 interaction 专属的续跑键。
|
|
725
|
+
for (const interaction of Object.values(state.interactions)) {
|
|
726
|
+
if (interaction.status !== "pending") continue;
|
|
727
|
+
return {
|
|
728
|
+
kind: "parked-interaction",
|
|
729
|
+
interactionId: interaction.interactionId,
|
|
730
|
+
chargeRecoveryBudget: false,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
622
733
|
|
|
623
734
|
// 已结算 Tool 先补齐续跑;未结算 Tool 只有声明幂等时才自动重试。
|
|
624
735
|
for (const tool of Object.values(state.toolCalls)) {
|
|
@@ -724,6 +835,42 @@ export function commitPiRecoveryContinuation(
|
|
|
724
835
|
|
|
725
836
|
// #endregion
|
|
726
837
|
|
|
838
|
+
// #region 装配迁移
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* 为一次停在人机等待期间发生的装配迁移创建里程碑。
|
|
842
|
+
*
|
|
843
|
+
* 宿主换完装配、准备把 Submission 重新 pin 到新装配上时调用它;返回的里程碑
|
|
844
|
+
* 必须与 `pinAssembly` 的写入落在同一个事务里,否则重启后里程碑与 Submission
|
|
845
|
+
* 会各自指向一半的迁移。
|
|
846
|
+
*
|
|
847
|
+
* 已经处在目标 revision(重复调用)或还没有任何里程碑(没有身份要迁移)时返回
|
|
848
|
+
* null:这两种情况下重放本来就得到正确的身份,多写一条只会造出假历史。
|
|
849
|
+
*/
|
|
850
|
+
export function stagePiAssemblyRepin(
|
|
851
|
+
state: PiToolRecoveryState,
|
|
852
|
+
nextAssemblyRevision: string,
|
|
853
|
+
timestamp: number,
|
|
854
|
+
): PiToolRecoveryMilestone | null {
|
|
855
|
+
if (
|
|
856
|
+
!state.turnId ||
|
|
857
|
+
!state.assemblyRevision ||
|
|
858
|
+
state.assemblyRevision === nextAssemblyRevision
|
|
859
|
+
) {
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
return {
|
|
863
|
+
version: 1,
|
|
864
|
+
turnId: state.turnId,
|
|
865
|
+
assemblyRevision: state.assemblyRevision,
|
|
866
|
+
type: "assembly-repin",
|
|
867
|
+
nextAssemblyRevision,
|
|
868
|
+
timestamp,
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// #endregion
|
|
873
|
+
|
|
727
874
|
// #region 恢复审批决定
|
|
728
875
|
|
|
729
876
|
/**
|
|
@@ -790,3 +937,99 @@ export function applyRecoveredPiApprovalDecision(
|
|
|
790
937
|
}
|
|
791
938
|
|
|
792
939
|
// #endregion
|
|
940
|
+
|
|
941
|
+
// #region 恢复 interaction 结算
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* 在恢复状态上记录一条新 park 的 interaction。
|
|
945
|
+
*
|
|
946
|
+
* 执行链进入 park 前调用,返回的里程碑让重启后的重放知道这次 Tool 调用在等客户端,
|
|
947
|
+
* 从而规划成 `parked-interaction` 而不是去重试 Tool。
|
|
948
|
+
*/
|
|
949
|
+
export function recordRecoveredPiToolInteraction(
|
|
950
|
+
state: PiToolRecoveryState,
|
|
951
|
+
interaction: PiToolInteraction,
|
|
952
|
+
): PiToolRecoveryMilestone[] {
|
|
953
|
+
if (!state.turnId || !state.assemblyRevision) {
|
|
954
|
+
throw new Error("Pi Tool interaction is missing its Turn revision");
|
|
955
|
+
}
|
|
956
|
+
return [{
|
|
957
|
+
version: 1,
|
|
958
|
+
turnId: state.turnId,
|
|
959
|
+
assemblyRevision: state.assemblyRevision,
|
|
960
|
+
type: "interaction",
|
|
961
|
+
interaction,
|
|
962
|
+
}];
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* 在恢复状态上应用一次客户端响应或取消,并生成需要持久化的里程碑。
|
|
967
|
+
*
|
|
968
|
+
* Runtime 收到 `respondToolInteraction` 或判定取消时调用它,再按返回结果更新表和恢复日志。
|
|
969
|
+
*
|
|
970
|
+
* 两种结局形状相同 —— 都写 interaction 记录 + 一条权威 `tool-result`。重复投递返回空里程碑。
|
|
971
|
+
*/
|
|
972
|
+
export function applyRecoveredPiToolInteractionSettlement(
|
|
973
|
+
state: PiToolRecoveryState,
|
|
974
|
+
input:
|
|
975
|
+
| {
|
|
976
|
+
interactionId: string;
|
|
977
|
+
kind: "respond";
|
|
978
|
+
response: unknown;
|
|
979
|
+
toolResult: ToolResultMessage;
|
|
980
|
+
settledAt: number;
|
|
981
|
+
}
|
|
982
|
+
| {
|
|
983
|
+
interactionId: string;
|
|
984
|
+
kind: "cancel";
|
|
985
|
+
reason: PiToolInteractionCancelReason;
|
|
986
|
+
settledAt: number;
|
|
987
|
+
toolResult?: ToolResultMessage;
|
|
988
|
+
},
|
|
989
|
+
): RecoveredPiToolInteractionSettlement {
|
|
990
|
+
const interaction = state.interactions[input.interactionId];
|
|
991
|
+
if (!interaction) {
|
|
992
|
+
throw new Error(`Unknown Pi Tool interaction ${input.interactionId}`);
|
|
993
|
+
}
|
|
994
|
+
const transition = input.kind === "respond"
|
|
995
|
+
? respondPiToolInteraction(interaction, {
|
|
996
|
+
response: input.response,
|
|
997
|
+
toolResult: input.toolResult,
|
|
998
|
+
respondedAt: input.settledAt,
|
|
999
|
+
})
|
|
1000
|
+
: cancelPiToolInteraction(interaction, {
|
|
1001
|
+
reason: input.reason,
|
|
1002
|
+
cancelledAt: input.settledAt,
|
|
1003
|
+
...(input.toolResult ? { toolResult: input.toolResult } : {}),
|
|
1004
|
+
});
|
|
1005
|
+
const outcome = transition.outcome;
|
|
1006
|
+
// 已结束的 interaction 只返回 noop;这里不应再追加任何持久记录。
|
|
1007
|
+
if (outcome.kind !== "settle") {
|
|
1008
|
+
return { milestones: [], outcome };
|
|
1009
|
+
}
|
|
1010
|
+
if (!state.turnId || !state.assemblyRevision) {
|
|
1011
|
+
throw new Error("Pi Tool interaction is missing its Turn revision");
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const identity: RecoveryIdentity = {
|
|
1015
|
+
version: 1,
|
|
1016
|
+
turnId: state.turnId,
|
|
1017
|
+
assemblyRevision: state.assemblyRevision,
|
|
1018
|
+
};
|
|
1019
|
+
return {
|
|
1020
|
+
milestones: [
|
|
1021
|
+
{ ...identity, type: "interaction", interaction: transition.interaction },
|
|
1022
|
+
// needsContinuation 必须为 true:interaction 没有自己的续跑键,
|
|
1023
|
+
// 唤醒原 Turn 全靠 planPiToolRecovery 里 `tool:<id>:settled` 那条既有分支。
|
|
1024
|
+
{
|
|
1025
|
+
...identity,
|
|
1026
|
+
type: "tool-result",
|
|
1027
|
+
toolResult: outcome.toolResult,
|
|
1028
|
+
needsContinuation: true,
|
|
1029
|
+
},
|
|
1030
|
+
],
|
|
1031
|
+
outcome,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// #endregion
|