@downcity/agent 1.1.267 → 1.1.270

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 (62) hide show
  1. package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
  2. package/bin/executor/core-engine/CoreEngineRunner.js +1 -2
  3. package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
  4. package/bin/executor/services/ExecutorRecoveryPolicy.d.ts +0 -1
  5. package/bin/executor/services/ExecutorRecoveryPolicy.d.ts.map +1 -1
  6. package/bin/executor/services/ExecutorRecoveryPolicy.js +2 -17
  7. package/bin/executor/services/ExecutorRecoveryPolicy.js.map +1 -1
  8. package/bin/executor/types/SessionRecords.d.ts +5 -7
  9. package/bin/executor/types/SessionRecords.d.ts.map +1 -1
  10. package/bin/executor/types/SessionRecords.js +3 -4
  11. package/bin/executor/types/SessionRecords.js.map +1 -1
  12. package/bin/index.d.ts +1 -1
  13. package/bin/index.d.ts.map +1 -1
  14. package/bin/index.js.map +1 -1
  15. package/bin/session/SessionMessages.d.ts.map +1 -1
  16. package/bin/session/SessionMessages.js +14 -1
  17. package/bin/session/SessionMessages.js.map +1 -1
  18. package/bin/session/SessionSystem.js +6 -6
  19. package/bin/session/SessionSystem.js.map +1 -1
  20. package/bin/session/SessionTitle.d.ts.map +1 -1
  21. package/bin/session/SessionTitle.js +0 -6
  22. package/bin/session/SessionTitle.js.map +1 -1
  23. package/bin/session/SessionTurn.d.ts.map +1 -1
  24. package/bin/session/SessionTurn.js +0 -25
  25. package/bin/session/SessionTurn.js.map +1 -1
  26. package/bin/session/messages/SessionAssistantMessageWriter.d.ts +1 -1
  27. package/bin/session/messages/SessionAssistantMessageWriter.d.ts.map +1 -1
  28. package/bin/session/messages/SessionAssistantMessageWriter.js +153 -12
  29. package/bin/session/messages/SessionAssistantMessageWriter.js.map +1 -1
  30. package/bin/session/messages/SessionJsonValue.d.ts +3 -1
  31. package/bin/session/messages/SessionJsonValue.d.ts.map +1 -1
  32. package/bin/session/messages/SessionJsonValue.js +12 -0
  33. package/bin/session/messages/SessionJsonValue.js.map +1 -1
  34. package/bin/session/messages/SessionMessageCodec.d.ts.map +1 -1
  35. package/bin/session/messages/SessionMessageCodec.js +226 -19
  36. package/bin/session/messages/SessionMessageCodec.js.map +1 -1
  37. package/bin/types/sdk/AgentSessionTurn.d.ts +2 -1
  38. package/bin/types/sdk/AgentSessionTurn.d.ts.map +1 -1
  39. package/bin/types/session/SessionMessage.d.ts +87 -3
  40. package/bin/types/session/SessionMessage.d.ts.map +1 -1
  41. package/package.json +3 -3
  42. package/scripts/agent-session-maintenance.test.mjs +7 -1
  43. package/scripts/executor-failure-result.test.mjs +81 -0
  44. package/scripts/openai-responses-continuation.test.mjs +35 -5
  45. package/scripts/session-messages.test.mjs +287 -1
  46. package/scripts/session-shell-approval.test.mjs +3 -3
  47. package/scripts/session-system-blocks.test.mjs +14 -1
  48. package/scripts/session-turn-failure.test.mjs +101 -0
  49. package/src/executor/core-engine/CoreEngineRunner.ts +1 -5
  50. package/src/executor/services/ExecutorRecoveryPolicy.ts +2 -23
  51. package/src/executor/types/SessionRecords.ts +5 -7
  52. package/src/index.ts +11 -0
  53. package/src/session/SessionMessages.ts +14 -1
  54. package/src/session/SessionSystem.ts +6 -6
  55. package/src/session/SessionTitle.ts +0 -6
  56. package/src/session/SessionTurn.ts +0 -29
  57. package/src/session/messages/SessionAssistantMessageWriter.ts +170 -12
  58. package/src/session/messages/SessionJsonValue.ts +12 -1
  59. package/src/session/messages/SessionMessageCodec.ts +226 -18
  60. package/src/types/sdk/AgentSessionTurn.ts +2 -1
  61. package/src/types/session/SessionMessage.ts +96 -3
  62. package/tsconfig.tsbuildinfo +1 -1
@@ -107,16 +107,15 @@ interface ExecutorRecoveryRunInput {
107
107
  * 执行恢复与重试策略服务。
108
108
  */
109
109
  export class ExecutorRecoveryPolicy {
110
- private readonly session_id: string;
111
110
  private readonly should_compact: ExecutorRecoveryPolicyOptions["should_compact"];
112
111
  private readonly logger: Logger;
113
112
  private retry_count = 0;
114
113
 
115
114
  constructor(options: ExecutorRecoveryPolicyOptions) {
116
- this.session_id = String(options.session_id || "").trim();
115
+ const session_id = String(options.session_id || "").trim();
117
116
  this.should_compact = options.should_compact;
118
117
  this.logger = options.logger;
119
- if (!this.session_id) {
118
+ if (!session_id) {
120
119
  throw new Error("ExecutorRecoveryPolicy requires a non-empty session_id");
121
120
  }
122
121
  }
@@ -161,8 +160,6 @@ export class ExecutorRecoveryPolicy {
161
160
  return this.build_failure_result({
162
161
  error_text:
163
162
  "Context length exceeded and retries failed. Please resend your question.",
164
- fallback_text:
165
- "Context length exceeded and retries failed. Please resend your question.",
166
163
  run_context: input.run_context,
167
164
  });
168
165
  }
@@ -173,7 +170,6 @@ export class ExecutorRecoveryPolicy {
173
170
  });
174
171
  return this.build_failure_result({
175
172
  error_text,
176
- fallback_text: `Execution failed: ${error_text}`,
177
173
  run_context: input.run_context,
178
174
  });
179
175
  }
@@ -185,11 +181,6 @@ export class ExecutorRecoveryPolicy {
185
181
  */
186
182
  error_text: string;
187
183
 
188
- /**
189
- * fallback assistant 消息文本。
190
- */
191
- fallback_text: string;
192
-
193
184
  /**
194
185
  * 当前显式运行上下文。
195
186
  */
@@ -198,18 +189,6 @@ export class ExecutorRecoveryPolicy {
198
189
  return {
199
190
  success: false,
200
191
  error: input.error_text,
201
- assistantMessage: {
202
- id: `a:${this.session_id}:${Date.now()}`,
203
- role: "assistant",
204
- metadata: {
205
- v: 1,
206
- ts: Date.now(),
207
- sessionId: this.session_id,
208
- source: "egress",
209
- kind: "normal",
210
- },
211
- parts: [{ type: "text", text: input.fallback_text }],
212
- },
213
192
  deferredPersistedUserMessages: [
214
193
  ...input.run_context.deferredPersistedUserMessages,
215
194
  ],
@@ -2,10 +2,9 @@
2
2
  * SessionRecord 类型定义。
3
3
  *
4
4
  * 关键点(中文)
5
- * - 这里统一描述 session JSONL 中的持久化 record 结构。
6
- * - record 是 session 内部事实源;其中 message record 继续使用 AI SDK `UIMessage`。
7
- * - action record UI record,不属于 LLM 输入。
8
- * - 这些类型会被 Store、Composer、compact、control UI、task runtime 共同复用。
5
+ * - SessionMessage 是唯一持久化事实,record 只作为 Executor、Composer 和旧 UI 的内部投影。
6
+ * - message record 继续使用 AI SDK `UIMessage`,action record 不属于 LLM 输入。
7
+ * - 这些类型不定义 canonical Session 存储结构。
9
8
  */
10
9
 
11
10
  import type { UIMessage } from "ai";
@@ -15,9 +14,8 @@ import type { JsonObject } from "@/types/common/Json.js";
15
14
  * Session message record 类别。
16
15
  *
17
16
  * 关键点(中文)
18
- * - 持久化存储在 `.downcity/agents/<encodedAgentId>/sessions/<encodedSessionId>/messages/messages.jsonl`
19
- * - record 文件默认包含 message record,或 `type=action` 的 UI 状态记录。
20
- * - compact 会把更早 message record 压缩为一条 `assistant` 摘要 record。
17
+ * - User / Assistant SessionMessage 会在执行边界临时投影为 message record。
18
+ * - Compact Summary 同样投影为一条内部 `assistant` record。
21
19
  * - action 只用于 UI timeline,不进入 LLM 输入
22
20
  */
23
21
  export type SessionMessageKind = "normal" | "summary";
package/src/index.ts CHANGED
@@ -40,14 +40,25 @@ export type {
40
40
  export type {
41
41
  ListSessionMessagesInput,
42
42
  SessionActionMessage,
43
+ SessionAssistantDataPart,
44
+ SessionAssistantDocumentSourcePart,
45
+ SessionAssistantFilePart,
43
46
  SessionAssistantMessage,
44
47
  SessionAssistantMessagePart,
48
+ SessionAssistantSourcePart,
49
+ SessionAssistantStepPart,
50
+ SessionAssistantTextPart,
45
51
  SessionAssistantToolPart,
52
+ SessionAssistantUrlSourcePart,
46
53
  SessionErrorMessage,
47
54
  SessionMessage,
48
55
  SessionMessagePage,
56
+ SessionUserDataPart,
57
+ SessionUserFilePart,
49
58
  SessionUserMessage,
50
59
  SessionUserMessagePart,
60
+ SessionUserTextPart,
61
+ SessionToolApprovalSnapshot,
51
62
  } from "./types/session/SessionMessage.js";
52
63
  export type {
53
64
  SessionContextSnapshot,
@@ -695,7 +695,10 @@ export class SessionMessages {
695
695
  await this.update_assistant_part_serialized(message_id, {
696
696
  ...tool.part,
697
697
  state: "approval-required",
698
- approval,
698
+ approval: {
699
+ approval_id: approval.approval_id,
700
+ request: approval,
701
+ },
699
702
  });
700
703
  });
701
704
  }
@@ -721,6 +724,16 @@ export class SessionMessages {
721
724
  await this.update_assistant_part_serialized(message_id, {
722
725
  ...tool.part,
723
726
  state: input.decision === "approved" ? "running" : "failed",
727
+ approval: {
728
+ ...tool.part.approval,
729
+ approval_id: input.approval_id,
730
+ approved: input.decision === "approved",
731
+ ...(input.decision === "expired"
732
+ ? { reason: "Approval expired" }
733
+ : input.decision === "denied"
734
+ ? { reason: "Approval denied" }
735
+ : {}),
736
+ },
724
737
  ...(input.decision === "approved"
725
738
  ? {}
726
739
  : {
@@ -59,12 +59,12 @@ function create_session_system_block(
59
59
  session: AgentSessionSystemSessionInfo,
60
60
  ): AgentSessionSystemBlock {
61
61
  const content = [
62
- "当前会话上下文:",
63
- `你正在服务 agent "${session.agentId}" session "${session.sessionId}"。`,
64
- `当前项目根目录是 "${session.projectRoot}"。`,
65
- `本会话创建于 ${session.createdAt},参考时区是 ${session.timezone}。`,
66
- "这个创建时间是当前会话的稳定参考时间,不代表每轮运行时的当前时间。",
67
- "如果用户消息中提供了新的当前时间、相对时间或其他动态上下文,应优先使用用户消息中的动态信息。",
62
+ "Current session context:",
63
+ `You are serving agent "${session.agentId}" in session "${session.sessionId}".`,
64
+ `The current project root is "${session.projectRoot}".`,
65
+ `This session was created at ${session.createdAt}, with ${session.timezone} as its reference timezone.`,
66
+ "This creation time is a stable reference for the session and does not represent the current time for every run.",
67
+ "If the user message provides a newer current time, a relative time, or other dynamic context, prioritize the dynamic information from the user message.",
68
68
  ].join("\n");
69
69
  return {
70
70
  source: "session",
@@ -215,12 +215,6 @@ async function generateSessionTitle(input: {
215
215
  "",
216
216
  input.firstUserText,
217
217
  ].join("\n"),
218
- // 标题生成是一次性调用,不参与 Responses continuation。
219
- providerOptions: {
220
- openai: {
221
- store: false,
222
- },
223
- },
224
218
  onError: ({ error }) => {
225
219
  observedStreamError = error;
226
220
  },
@@ -241,10 +241,6 @@ export class SessionTurn {
241
241
  turnId: turn_id,
242
242
  text: "",
243
243
  success: false,
244
- assistantMessage: build_prompt_runtime_error_assistant_message({
245
- session_id: this.session_id,
246
- message,
247
- }),
248
244
  error: message,
249
245
  };
250
246
  active_turn.result = final_result;
@@ -291,10 +287,6 @@ export class SessionTurn {
291
287
  turnId: turn_id,
292
288
  text: "",
293
289
  success: false,
294
- assistantMessage: build_prompt_runtime_error_assistant_message({
295
- session_id: this.session_id,
296
- message: QUEUED_PROMPT_CANCELLED_MESSAGE,
297
- }),
298
290
  error: QUEUED_PROMPT_CANCELLED_MESSAGE,
299
291
  };
300
292
  cancelled_turn.result = final_result;
@@ -630,27 +622,6 @@ function is_assistant_content_chunk(type: string): boolean {
630
622
  );
631
623
  }
632
624
 
633
- function build_prompt_runtime_error_assistant_message(input: {
634
- session_id: string;
635
- message: string;
636
- }): SessionMessageRecordV1 {
637
- return {
638
- id: `a:${input.session_id}:${Date.now()}:${nanoid(6)}`,
639
- role: "assistant",
640
- metadata: {
641
- v: 1,
642
- ts: Date.now(),
643
- sessionId: input.session_id,
644
- source: "egress",
645
- kind: "normal",
646
- extra: {
647
- note: "session_prompt_runtime_error",
648
- },
649
- },
650
- parts: [{ type: "text", text: input.message }],
651
- };
652
- }
653
-
654
625
  function create_deferred<T>(): SessionDeferred<T> {
655
626
  let resolve!: (value: T) => void;
656
627
  const promise = new Promise<T>((inner_resolve) => {
@@ -8,6 +8,7 @@
8
8
  import type { UIMessageChunk } from "ai";
9
9
  import type { SessionMessages } from "@/session/SessionMessages.js";
10
10
  import {
11
+ to_session_json_object,
11
12
  to_session_json_value,
12
13
  to_session_provider_metadata,
13
14
  } from "@/session/messages/SessionJsonValue.js";
@@ -15,6 +16,7 @@ import type {
15
16
  SessionAssistantFilePart,
16
17
  SessionAssistantMessage,
17
18
  SessionAssistantMessagePart,
19
+ SessionAssistantTextPart,
18
20
  SessionAssistantToolPart,
19
21
  } from "@/types/session/SessionMessage.js";
20
22
  import type { SessionToolInputReady } from "@/types/session/SessionTool.js";
@@ -24,7 +26,10 @@ import { generateId } from "@/utils/Id.js";
24
26
  export class SessionAssistantMessageWriter {
25
27
  readonly message_id: string;
26
28
  private readonly recorder: SessionMessages;
27
- private readonly pending_text_parts = new Map<string, "text" | "reasoning">();
29
+ private readonly pending_text_parts = new Map<
30
+ string,
31
+ Pick<SessionAssistantTextPart, "type" | "provider_metadata">
32
+ >();
28
33
  private readonly active_text_part_ids = new Map<string, string>();
29
34
  private write_chain: Promise<void> = Promise.resolve();
30
35
  private closed = false;
@@ -51,7 +56,10 @@ export class SessionAssistantMessageWriter {
51
56
  const type = chunk.type === "text-start" ? "text" : "reasoning";
52
57
  const part_id = this.resolve_text_part_id(type, chunk.id);
53
58
  if (!current.parts.some((part) => part.part_id === part_id)) {
54
- this.pending_text_parts.set(part_id, type);
59
+ this.pending_text_parts.set(part_id, {
60
+ type,
61
+ provider_metadata: to_session_provider_metadata(chunk.providerMetadata),
62
+ });
55
63
  }
56
64
  return;
57
65
  }
@@ -59,7 +67,11 @@ export class SessionAssistantMessageWriter {
59
67
  case "reasoning-delta": {
60
68
  const type = chunk.type === "text-delta" ? "text" : "reasoning";
61
69
  const part_id = this.resolve_text_part_id(type, chunk.id);
62
- await this.ensure_text_part(part_id, type);
70
+ await this.ensure_text_part(
71
+ part_id,
72
+ type,
73
+ to_session_provider_metadata(chunk.providerMetadata),
74
+ );
63
75
  await this.recorder.append_assistant_delta(
64
76
  this.message_id,
65
77
  part_id,
@@ -76,7 +88,12 @@ export class SessionAssistantMessageWriter {
76
88
  if (!part_id) return;
77
89
  const part = current.parts.find((item) => item.part_id === part_id);
78
90
  if (part?.type === "text" || part?.type === "reasoning") {
79
- await this.upsert_part({ ...part, state: "done" });
91
+ const provider_metadata = to_session_provider_metadata(chunk.providerMetadata);
92
+ await this.upsert_part({
93
+ ...part,
94
+ state: "done",
95
+ ...(provider_metadata !== undefined ? { provider_metadata } : {}),
96
+ });
80
97
  } else {
81
98
  this.pending_text_parts.delete(part_id);
82
99
  }
@@ -88,10 +105,14 @@ export class SessionAssistantMessageWriter {
88
105
  const call_provider_metadata = to_session_provider_metadata(
89
106
  chunk.providerMetadata,
90
107
  );
108
+ const tool_metadata = to_session_json_object(chunk.toolMetadata);
91
109
  if (tool) {
92
110
  if (
93
111
  call_provider_metadata === undefined &&
94
- chunk.providerExecuted === undefined
112
+ chunk.providerExecuted === undefined &&
113
+ chunk.title === undefined &&
114
+ tool_metadata === undefined &&
115
+ chunk.dynamic === undefined
95
116
  ) return;
96
117
  await this.upsert_tool(chunk.toolCallId, {
97
118
  tool_name: tool.tool_name,
@@ -102,6 +123,9 @@ export class SessionAssistantMessageWriter {
102
123
  ...(chunk.providerExecuted !== undefined
103
124
  ? { provider_executed: chunk.providerExecuted }
104
125
  : {}),
126
+ ...(chunk.title !== undefined ? { title: chunk.title } : {}),
127
+ ...(tool_metadata !== undefined ? { tool_metadata } : {}),
128
+ ...(chunk.dynamic !== undefined ? { dynamic: chunk.dynamic } : {}),
105
129
  });
106
130
  return;
107
131
  }
@@ -115,6 +139,9 @@ export class SessionAssistantMessageWriter {
115
139
  ...(chunk.providerExecuted !== undefined
116
140
  ? { provider_executed: chunk.providerExecuted }
117
141
  : {}),
142
+ ...(chunk.title !== undefined ? { title: chunk.title } : {}),
143
+ ...(tool_metadata !== undefined ? { tool_metadata } : {}),
144
+ ...(chunk.dynamic !== undefined ? { dynamic: chunk.dynamic } : {}),
118
145
  });
119
146
  return;
120
147
  }
@@ -133,10 +160,14 @@ export class SessionAssistantMessageWriter {
133
160
  const call_provider_metadata = to_session_provider_metadata(
134
161
  chunk.providerMetadata,
135
162
  );
163
+ const tool_metadata = to_session_json_object(chunk.toolMetadata);
136
164
  if (tool && tool.state !== "input-streaming") {
137
165
  if (
138
166
  call_provider_metadata === undefined &&
139
- chunk.providerExecuted === undefined
167
+ chunk.providerExecuted === undefined &&
168
+ chunk.title === undefined &&
169
+ tool_metadata === undefined &&
170
+ chunk.dynamic === undefined
140
171
  ) return;
141
172
  await this.upsert_tool(chunk.toolCallId, {
142
173
  tool_name: tool.tool_name,
@@ -147,6 +178,9 @@ export class SessionAssistantMessageWriter {
147
178
  ...(chunk.providerExecuted !== undefined
148
179
  ? { provider_executed: chunk.providerExecuted }
149
180
  : {}),
181
+ ...(chunk.title !== undefined ? { title: chunk.title } : {}),
182
+ ...(tool_metadata !== undefined ? { tool_metadata } : {}),
183
+ ...(chunk.dynamic !== undefined ? { dynamic: chunk.dynamic } : {}),
150
184
  });
151
185
  return;
152
186
  }
@@ -160,6 +194,9 @@ export class SessionAssistantMessageWriter {
160
194
  ...(chunk.providerExecuted !== undefined
161
195
  ? { provider_executed: chunk.providerExecuted }
162
196
  : {}),
197
+ ...(chunk.title !== undefined ? { title: chunk.title } : {}),
198
+ ...(tool_metadata !== undefined ? { tool_metadata } : {}),
199
+ ...(chunk.dynamic !== undefined ? { dynamic: chunk.dynamic } : {}),
163
200
  });
164
201
  return;
165
202
  }
@@ -167,17 +204,33 @@ export class SessionAssistantMessageWriter {
167
204
  const call_provider_metadata = to_session_provider_metadata(
168
205
  chunk.providerMetadata,
169
206
  );
207
+ const tool_metadata = to_session_json_object(chunk.toolMetadata);
170
208
  await this.upsert_tool(chunk.toolCallId, {
171
209
  tool_name: chunk.toolName,
172
210
  state: "failed",
173
211
  input: to_session_json_value(chunk.input),
174
212
  error: chunk.errorText,
213
+ ...(chunk.input === undefined && "rawInput" in chunk && chunk.rawInput !== undefined
214
+ ? { raw_input: to_session_json_value(chunk.rawInput) }
215
+ : {}),
175
216
  ...(call_provider_metadata !== undefined
176
217
  ? { call_provider_metadata }
177
218
  : {}),
178
219
  ...(chunk.providerExecuted !== undefined
179
220
  ? { provider_executed: chunk.providerExecuted }
180
221
  : {}),
222
+ ...(chunk.title !== undefined ? { title: chunk.title } : {}),
223
+ ...(tool_metadata !== undefined ? { tool_metadata } : {}),
224
+ ...(chunk.dynamic !== undefined ? { dynamic: chunk.dynamic } : {}),
225
+ });
226
+ return;
227
+ }
228
+ case "tool-approval-request": {
229
+ const tool = this.find_tool(chunk.toolCallId);
230
+ await this.upsert_tool(chunk.toolCallId, {
231
+ tool_name: tool?.tool_name || "unknown",
232
+ state: "approval-required",
233
+ approval: { approval_id: chunk.approvalId },
181
234
  });
182
235
  return;
183
236
  }
@@ -190,6 +243,7 @@ export class SessionAssistantMessageWriter {
190
243
  const result_provider_metadata = to_session_provider_metadata(
191
244
  chunk.providerMetadata,
192
245
  );
246
+ const tool_metadata = to_session_json_object(chunk.toolMetadata);
193
247
  await this.upsert_tool(chunk.toolCallId, {
194
248
  tool_name: tool?.tool_name || "unknown",
195
249
  state: "completed",
@@ -200,6 +254,11 @@ export class SessionAssistantMessageWriter {
200
254
  ...(chunk.providerExecuted !== undefined
201
255
  ? { provider_executed: chunk.providerExecuted }
202
256
  : {}),
257
+ ...(tool_metadata !== undefined ? { tool_metadata } : {}),
258
+ ...(chunk.dynamic !== undefined ? { dynamic: chunk.dynamic } : {}),
259
+ ...(chunk.preliminary !== undefined
260
+ ? { preliminary: chunk.preliminary }
261
+ : {}),
203
262
  });
204
263
  return;
205
264
  }
@@ -208,6 +267,7 @@ export class SessionAssistantMessageWriter {
208
267
  const result_provider_metadata = to_session_provider_metadata(
209
268
  chunk.providerMetadata,
210
269
  );
270
+ const tool_metadata = to_session_json_object(chunk.toolMetadata);
211
271
  await this.upsert_tool(chunk.toolCallId, {
212
272
  tool_name: tool?.tool_name || "unknown",
213
273
  state: "failed",
@@ -218,6 +278,8 @@ export class SessionAssistantMessageWriter {
218
278
  ...(chunk.providerExecuted !== undefined
219
279
  ? { provider_executed: chunk.providerExecuted }
220
280
  : {}),
281
+ ...(tool_metadata !== undefined ? { tool_metadata } : {}),
282
+ ...(chunk.dynamic !== undefined ? { dynamic: chunk.dynamic } : {}),
221
283
  });
222
284
  return;
223
285
  }
@@ -227,6 +289,12 @@ export class SessionAssistantMessageWriter {
227
289
  tool_name: tool?.tool_name || "unknown",
228
290
  state: "failed",
229
291
  error: "Tool output denied",
292
+ approval: {
293
+ ...(tool?.approval || {}),
294
+ approval_id:
295
+ tool?.approval?.approval_id || `approval:${chunk.toolCallId}`,
296
+ approved: false,
297
+ },
230
298
  });
231
299
  return;
232
300
  }
@@ -234,10 +302,73 @@ export class SessionAssistantMessageWriter {
234
302
  await this.append_file_part({
235
303
  media_type: chunk.mediaType,
236
304
  url: chunk.url,
305
+ provider_metadata: to_session_provider_metadata(chunk.providerMetadata),
237
306
  });
238
307
  return;
239
- default:
308
+ case "source-url": {
309
+ const part_id = `source:${chunk.sourceId}`;
310
+ const current_part = current.parts.find((part) => part.part_id === part_id);
311
+ const provider_metadata = to_session_provider_metadata(chunk.providerMetadata);
312
+ await this.upsert_part({
313
+ part_id,
314
+ sequence: current_part?.sequence || this.next_part_sequence(),
315
+ type: "source",
316
+ source_type: "url",
317
+ source_id: chunk.sourceId,
318
+ url: chunk.url,
319
+ ...(chunk.title !== undefined ? { title: chunk.title } : {}),
320
+ ...(provider_metadata !== undefined
321
+ ? { provider_metadata }
322
+ : {}),
323
+ });
240
324
  return;
325
+ }
326
+ case "source-document": {
327
+ const part_id = `source:${chunk.sourceId}`;
328
+ const current_part = current.parts.find((part) => part.part_id === part_id);
329
+ const provider_metadata = to_session_provider_metadata(chunk.providerMetadata);
330
+ await this.upsert_part({
331
+ part_id,
332
+ sequence: current_part?.sequence || this.next_part_sequence(),
333
+ type: "source",
334
+ source_type: "document",
335
+ source_id: chunk.sourceId,
336
+ media_type: chunk.mediaType,
337
+ title: chunk.title,
338
+ ...(chunk.filename !== undefined ? { filename: chunk.filename } : {}),
339
+ ...(provider_metadata !== undefined
340
+ ? { provider_metadata }
341
+ : {}),
342
+ });
343
+ return;
344
+ }
345
+ case "start-step":
346
+ await this.upsert_part({
347
+ part_id: `step:${generateId()}`,
348
+ sequence: this.next_part_sequence(),
349
+ type: "step-start",
350
+ });
351
+ return;
352
+ default: {
353
+ if (chunk.type.startsWith("data-")) {
354
+ const data_chunk = chunk as unknown as Record<string, unknown>;
355
+ if (data_chunk.transient === true) return;
356
+ const data_id = typeof data_chunk.id === "string"
357
+ ? data_chunk.id
358
+ : undefined;
359
+ const part_id = `data:${data_id ?? generateId()}`;
360
+ const current_part = current.parts.find((part) => part.part_id === part_id);
361
+ await this.upsert_part({
362
+ part_id,
363
+ sequence: current_part?.sequence || this.next_part_sequence(),
364
+ type: "data",
365
+ data_type: chunk.type,
366
+ data: to_session_json_value(data_chunk.data),
367
+ ...(data_id !== undefined ? { data_id } : {}),
368
+ });
369
+ }
370
+ return;
371
+ }
241
372
  }
242
373
  }
243
374
 
@@ -285,7 +416,10 @@ export class SessionAssistantMessageWriter {
285
416
 
286
417
  /** 把最终结果中的文件补入当前 Assistant,并对流式已写入文件去重。 */
287
418
  async append_file_part(
288
- input: Pick<SessionAssistantFilePart, "filename" | "media_type" | "url">,
419
+ input: Pick<
420
+ SessionAssistantFilePart,
421
+ "filename" | "media_type" | "provider_metadata" | "url"
422
+ >,
289
423
  ): Promise<void> {
290
424
  const filename = String(input.filename || "").trim();
291
425
  const current = this.current_message();
@@ -296,8 +430,17 @@ export class SessionAssistantMessageWriter {
296
430
  part.media_type === input.media_type,
297
431
  );
298
432
  if (existing?.type === "file") {
299
- if (filename && String(existing.filename || "").trim() !== filename) {
300
- await this.upsert_part({ ...existing, filename });
433
+ if (
434
+ (filename && String(existing.filename || "").trim() !== filename) ||
435
+ input.provider_metadata !== undefined
436
+ ) {
437
+ await this.upsert_part({
438
+ ...existing,
439
+ ...(filename ? { filename } : {}),
440
+ ...(input.provider_metadata !== undefined
441
+ ? { provider_metadata: input.provider_metadata }
442
+ : {}),
443
+ });
301
444
  }
302
445
  return;
303
446
  }
@@ -308,6 +451,9 @@ export class SessionAssistantMessageWriter {
308
451
  media_type: input.media_type,
309
452
  url: input.url,
310
453
  ...(filename ? { filename } : {}),
454
+ ...(input.provider_metadata !== undefined
455
+ ? { provider_metadata: input.provider_metadata }
456
+ : {}),
311
457
  });
312
458
  }
313
459
 
@@ -392,6 +538,7 @@ export class SessionAssistantMessageWriter {
392
538
  private async ensure_text_part(
393
539
  part_id: string,
394
540
  type: "text" | "reasoning",
541
+ provider_metadata?: SessionAssistantTextPart["provider_metadata"],
395
542
  ): Promise<void> {
396
543
  const existing = this.current_message().parts.find(
397
544
  (part) => part.part_id === part_id,
@@ -400,10 +547,16 @@ export class SessionAssistantMessageWriter {
400
547
  if (existing.type !== type) {
401
548
  throw new Error(`Assistant Part type changed: ${part_id}`);
402
549
  }
550
+ if (
551
+ (existing.type === "text" || existing.type === "reasoning") &&
552
+ provider_metadata !== undefined
553
+ ) {
554
+ await this.upsert_part({ ...existing, provider_metadata });
555
+ }
403
556
  return;
404
557
  }
405
- const pending_type = this.pending_text_parts.get(part_id);
406
- if (pending_type && pending_type !== type) {
558
+ const pending = this.pending_text_parts.get(part_id);
559
+ if (pending && pending.type !== type) {
407
560
  throw new Error(`Assistant pending Part type changed: ${part_id}`);
408
561
  }
409
562
  await this.upsert_part({
@@ -412,6 +565,11 @@ export class SessionAssistantMessageWriter {
412
565
  type,
413
566
  text: "",
414
567
  state: "streaming",
568
+ ...(provider_metadata !== undefined
569
+ ? { provider_metadata }
570
+ : pending?.provider_metadata !== undefined
571
+ ? { provider_metadata: pending.provider_metadata }
572
+ : {}),
415
573
  });
416
574
  this.pending_text_parts.delete(part_id);
417
575
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { ProviderMetadata } from "ai";
9
- import type { JsonValue } from "@/types/common/Json.js";
9
+ import type { JsonObject, JsonValue } from "@/types/common/Json.js";
10
10
 
11
11
  /** 把任意运行时输入转换为可持久化的 JSON 值。 */
12
12
  export function to_session_json_value(input: unknown): JsonValue {
@@ -25,6 +25,17 @@ export function to_session_json_value(input: unknown): JsonValue {
25
25
  }
26
26
  }
27
27
 
28
+ /** 把任意运行时输入转换为可持久化 JSON 对象,非对象输入返回 undefined。 */
29
+ export function to_session_json_object(input: unknown): JsonObject | undefined {
30
+ if (!is_plain_object(input)) return undefined;
31
+ try {
32
+ const value = JSON.parse(JSON.stringify(input)) as unknown;
33
+ return is_plain_object(value) ? value as JsonObject : undefined;
34
+ } catch {
35
+ return undefined;
36
+ }
37
+ }
38
+
28
39
  /**
29
40
  * 把 AI SDK Provider metadata 规整为可持久化快照。
30
41
  *