@springbrand/agent-runtime 0.1.3-alpha.2 → 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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/db/agent-tool.repo.ts +27 -0
  3. package/src/db/index.ts +33 -0
  4. package/src/db/interaction.repo.ts +185 -0
  5. package/src/db/schema.ts +15 -0
  6. package/src/db/submission.repo.ts +29 -0
  7. package/src/index.ts +8 -17
  8. package/src/kernel/approval-lifecycle.ts +41 -6
  9. package/src/kernel/bindings.ts +37 -0
  10. package/src/kernel/interaction-lifecycle.ts +395 -0
  11. package/src/kernel/public-contracts.ts +2 -0
  12. package/src/kernel/recoverable-chat-agent.ts +10 -2
  13. package/src/kernel/runtime-assembly-view.ts +37 -0
  14. package/src/kernel/runtime-assembly.ts +41 -0
  15. package/src/kernel/runtime-config.ts +4 -0
  16. package/src/kernel/runtime-load.ts +102 -0
  17. package/src/kernel/state.ts +8 -1
  18. package/src/kernel/submission-lifecycle.ts +30 -0
  19. package/src/lib/telemetry-dev.ts +7 -4
  20. package/src/pi/runtime-adapter/assembly.ts +13 -1
  21. package/src/pi/runtime-adapter/execution.ts +109 -9
  22. package/src/pi/runtime-adapter/index.ts +10 -3
  23. package/src/pi/runtime-adapter/models.ts +162 -16
  24. package/src/pi/runtime-adapter/recovery.ts +188 -1
  25. package/src/pi/tool/base.ts +62 -9
  26. package/src/pi/tool/compiler.ts +34 -0
  27. package/src/pi/tool/gateway.ts +54 -0
  28. package/src/pi/tool/index.ts +1 -0
  29. package/src/pi/tool/mcp.ts +93 -64
  30. package/src/pi/turn/index.ts +20 -0
  31. package/src/pi/turn/interaction.ts +181 -0
  32. package/src/pi/turn/tool-recovery.ts +244 -1
  33. package/src/runtime-agent.ts +246 -113
  34. package/src/{plugins.ts → runtime-assembler.ts} +65 -282
  35. package/src/runtime.ts +454 -162
@@ -0,0 +1,181 @@
1
+ import type { ToolResultMessage } from "@earendil-works/pi-ai";
2
+
3
+ /**
4
+ * 表示一个 Pi Tool 在等待客户端投递结果时的持久 park 记录。
5
+ *
6
+ * Runtime 在带 `interaction` 声明的 Tool 进入执行链时保存它,恢复路径按 `interactionId` 读取并应用一次响应。
7
+ *
8
+ * Tool 输入随记录一起保存,才能在 Durable Object 重建后把响应映射回原始调用,而不是重新推断它。
9
+ */
10
+ export interface PiToolInteraction {
11
+ interactionId: string;
12
+ requestId: string;
13
+ toolCallId: string;
14
+ toolName: string;
15
+ inputJson: string;
16
+ createdAt: number;
17
+ status: "pending" | "responded" | "cancelled";
18
+ respondedAt?: number;
19
+ responseJson?: string;
20
+ }
21
+
22
+ /**
23
+ * 说明一次 interaction 为什么在没有拿到用户选择的情况下结束。
24
+ *
25
+ * 目前只有一种:用户没有点选项,而是直接在聊天里发了消息。
26
+ * 新增成员时必须同步更新 `cancelText` 与解码器的枚举校验。
27
+ */
28
+ export type PiToolInteractionCancelReason =
29
+ | "user_replied_freeform"
30
+ | "submission_terminal";
31
+
32
+ /**
33
+ * 描述一次 interaction 状态转换要求调用方采取的动作。
34
+ *
35
+ * `park` 表示继续等待,`settle` 表示已得到权威 ToolResult 可以续跑,`noop` 表示重复投递应被忽略。
36
+ *
37
+ * 状态转换与 I/O 分开,使同一套幂等规则同时服务在线执行和崩溃恢复 —— 与审批同构。
38
+ */
39
+ export type PiToolInteractionOutcome =
40
+ | { kind: "park"; interactionId: string; requestId: string }
41
+ | {
42
+ kind: "settle";
43
+ interactionId: string;
44
+ requestId: string;
45
+ toolCallId: string;
46
+ toolResult: ToolResultMessage;
47
+ }
48
+ | { kind: "noop"; interactionId: string };
49
+
50
+ /**
51
+ * 把 interaction 的新状态和对应动作绑定为一个不可拆分的结果。
52
+ *
53
+ * 在线执行与恢复调用方应同时消费 `interaction` 和 `outcome`,不要只持久化其中一半。
54
+ */
55
+ export interface PiToolInteractionTransition {
56
+ interaction: PiToolInteraction;
57
+ outcome: PiToolInteractionOutcome;
58
+ }
59
+
60
+ /**
61
+ * 用户改为自由发言时写进 ToolResult 的提示语。
62
+ *
63
+ * 刻意**不包含用户原文** —— 紧随其后的那条用户消息里就有,复制进来模型会读到两遍。
64
+ * 这段文字是模型判断「那句话算不算答案」的唯一依据,改动前先想清楚它会怎么读。
65
+ */
66
+ const CANCEL_TEXT: Record<PiToolInteractionCancelReason, string> = {
67
+ user_replied_freeform:
68
+ "The user did not pick an option. They replied in the chat instead — " +
69
+ "read their next message and treat it as the answer if it addresses " +
70
+ "the question; otherwise follow whatever they actually asked for.",
71
+ submission_terminal:
72
+ "The turn ended before the user answered. Do not assume any option was chosen.",
73
+ };
74
+
75
+ /**
76
+ * 创建一条待响应的 interaction,并告诉调用方暂停当前 Tool。
77
+ *
78
+ * 执行适配器确认 Tool 声明了 `interaction` 后调用它,再把返回的记录持久化并等待客户端投递。
79
+ *
80
+ * 此处不填写响应时间或响应体,保证新记录只有一个明确的 `pending` 起点。
81
+ */
82
+ export function parkPiToolInteraction(
83
+ input: Omit<
84
+ PiToolInteraction,
85
+ "status" | "respondedAt" | "responseJson"
86
+ >,
87
+ ): PiToolInteractionTransition {
88
+ return {
89
+ interaction: { ...input, status: "pending" },
90
+ outcome: {
91
+ kind: "park",
92
+ interactionId: input.interactionId,
93
+ requestId: input.requestId,
94
+ },
95
+ };
96
+ }
97
+
98
+ /**
99
+ * 把客户端的首次响应应用到待响应记录。
100
+ *
101
+ * 在线 RPC 与恢复路径在读到持久记录后调用它;已经结束的记录返回 `noop`,调用方不得再次结算 Tool。
102
+ *
103
+ * `toolResult` 由调用方按 Tool 自己的 `settle` 映射产出 —— 本函数不解释响应体语义,只负责状态单调前进。
104
+ */
105
+ export function respondPiToolInteraction(
106
+ interaction: PiToolInteraction,
107
+ input: {
108
+ response: unknown;
109
+ toolResult: ToolResultMessage;
110
+ respondedAt: number;
111
+ },
112
+ ): PiToolInteractionTransition {
113
+ if (interaction.status !== "pending") {
114
+ return {
115
+ interaction,
116
+ outcome: { kind: "noop", interactionId: interaction.interactionId },
117
+ };
118
+ }
119
+ return {
120
+ interaction: {
121
+ ...interaction,
122
+ status: "responded",
123
+ respondedAt: input.respondedAt,
124
+ responseJson: JSON.stringify(input.response),
125
+ },
126
+ outcome: {
127
+ kind: "settle",
128
+ interactionId: interaction.interactionId,
129
+ requestId: interaction.requestId,
130
+ toolCallId: interaction.toolCallId,
131
+ toolResult: input.toolResult,
132
+ },
133
+ };
134
+ }
135
+
136
+ /**
137
+ * 在用户没有作答的情况下结束一条 interaction。
138
+ *
139
+ * `dispatchMessage` 发现 park 期间来了用户消息时调用,Submission 进终态收尾时也调用。
140
+ *
141
+ * 取消同样产出 `isError: false` 的稳定 ToolResult —— 它不是 Tool 失败,而是一个「没选」的已处理结果;
142
+ * 改成异常会让模型收到 tool_error 并倾向于重试,那恰恰是我们不想要的。
143
+ */
144
+ export function cancelPiToolInteraction(
145
+ interaction: PiToolInteraction,
146
+ input: {
147
+ reason: PiToolInteractionCancelReason;
148
+ cancelledAt: number;
149
+ toolResult?: ToolResultMessage;
150
+ },
151
+ ): PiToolInteractionTransition {
152
+ if (interaction.status !== "pending") {
153
+ return {
154
+ interaction,
155
+ outcome: { kind: "noop", interactionId: interaction.interactionId },
156
+ };
157
+ }
158
+ const toolResult: ToolResultMessage = input.toolResult ?? {
159
+ role: "toolResult",
160
+ toolCallId: interaction.toolCallId,
161
+ toolName: interaction.toolName,
162
+ content: [{ type: "text", text: CANCEL_TEXT[input.reason] }],
163
+ details: { cancelled: true, reason: input.reason },
164
+ isError: false,
165
+ timestamp: input.cancelledAt,
166
+ };
167
+ return {
168
+ interaction: {
169
+ ...interaction,
170
+ status: "cancelled",
171
+ respondedAt: input.cancelledAt,
172
+ },
173
+ outcome: {
174
+ kind: "settle",
175
+ interactionId: interaction.interactionId,
176
+ requestId: interaction.requestId,
177
+ toolCallId: interaction.toolCallId,
178
+ toolResult,
179
+ },
180
+ };
181
+ }
@@ -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
- * 优先级固定为终态、已决定审批、待审批、已结算 Tool、未结算 Tool,不能调换到越过终态或用户许可。
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