@downcity/agent 1.1.262 → 1.1.265

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 (40) hide show
  1. package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
  2. package/bin/executor/core-engine/CoreEngineRunner.js +0 -2
  3. package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
  4. package/bin/executor/messages/SessionMessageCodec.d.ts +0 -12
  5. package/bin/executor/messages/SessionMessageCodec.d.ts.map +1 -1
  6. package/bin/executor/messages/SessionMessageCodec.js +0 -16
  7. package/bin/executor/messages/SessionMessageCodec.js.map +1 -1
  8. package/bin/session/SessionTitle.d.ts.map +1 -1
  9. package/bin/session/SessionTitle.js +6 -2
  10. package/bin/session/SessionTitle.js.map +1 -1
  11. package/bin/session/SessionTurn.d.ts.map +1 -1
  12. package/bin/session/SessionTurn.js +3 -0
  13. package/bin/session/SessionTurn.js.map +1 -1
  14. package/bin/session/messages/SessionAssistantMessageWriter.d.ts +8 -1
  15. package/bin/session/messages/SessionAssistantMessageWriter.d.ts.map +1 -1
  16. package/bin/session/messages/SessionAssistantMessageWriter.js +98 -9
  17. package/bin/session/messages/SessionAssistantMessageWriter.js.map +1 -1
  18. package/bin/session/messages/SessionJsonValue.d.ts +10 -0
  19. package/bin/session/messages/SessionJsonValue.d.ts.map +1 -1
  20. package/bin/session/messages/SessionJsonValue.js +30 -0
  21. package/bin/session/messages/SessionJsonValue.js.map +1 -1
  22. package/bin/session/messages/SessionMessageCodec.d.ts.map +1 -1
  23. package/bin/session/messages/SessionMessageCodec.js +30 -1
  24. package/bin/session/messages/SessionMessageCodec.js.map +1 -1
  25. package/bin/types/session/SessionMessage.d.ts +7 -0
  26. package/bin/types/session/SessionMessage.d.ts.map +1 -1
  27. package/package.json +4 -3
  28. package/scripts/city-model-tool-loop.test.mjs +13 -0
  29. package/scripts/core-engine-context-compaction.test.mjs +4 -0
  30. package/scripts/openai-responses-continuation.test.mjs +48 -0
  31. package/scripts/session-messages.test.mjs +171 -1
  32. package/src/executor/core-engine/CoreEngineRunner.ts +0 -2
  33. package/src/executor/messages/SessionMessageCodec.ts +0 -21
  34. package/src/session/SessionTitle.ts +6 -2
  35. package/src/session/SessionTurn.ts +2 -0
  36. package/src/session/messages/SessionAssistantMessageWriter.ts +118 -10
  37. package/src/session/messages/SessionJsonValue.ts +30 -0
  38. package/src/session/messages/SessionMessageCodec.ts +37 -1
  39. package/src/types/session/SessionMessage.ts +7 -0
  40. package/tsconfig.tsbuildinfo +1 -1
@@ -7,7 +7,10 @@
7
7
 
8
8
  import type { UIMessageChunk } from "ai";
9
9
  import type { SessionMessages } from "@/session/SessionMessages.js";
10
- import { to_session_json_value } from "@/session/messages/SessionJsonValue.js";
10
+ import {
11
+ to_session_json_value,
12
+ to_session_provider_metadata,
13
+ } from "@/session/messages/SessionJsonValue.js";
11
14
  import type {
12
15
  SessionAssistantFilePart,
13
16
  SessionAssistantMessage,
@@ -80,14 +83,41 @@ export class SessionAssistantMessageWriter {
80
83
  this.active_text_part_ids.delete(source_part_id);
81
84
  return;
82
85
  }
83
- case "tool-input-start":
84
- if (this.find_tool(chunk.toolCallId)) return;
86
+ case "tool-input-start": {
87
+ const tool = this.find_tool(chunk.toolCallId);
88
+ const call_provider_metadata = to_session_provider_metadata(
89
+ chunk.providerMetadata,
90
+ );
91
+ if (tool) {
92
+ if (
93
+ call_provider_metadata === undefined &&
94
+ chunk.providerExecuted === undefined
95
+ ) return;
96
+ await this.upsert_tool(chunk.toolCallId, {
97
+ tool_name: tool.tool_name,
98
+ state: tool.state,
99
+ ...(call_provider_metadata !== undefined
100
+ ? { call_provider_metadata }
101
+ : {}),
102
+ ...(chunk.providerExecuted !== undefined
103
+ ? { provider_executed: chunk.providerExecuted }
104
+ : {}),
105
+ });
106
+ return;
107
+ }
85
108
  await this.upsert_tool(chunk.toolCallId, {
86
109
  tool_name: chunk.toolName,
87
110
  state: "input-streaming",
88
111
  input_text: "",
112
+ ...(call_provider_metadata !== undefined
113
+ ? { call_provider_metadata }
114
+ : {}),
115
+ ...(chunk.providerExecuted !== undefined
116
+ ? { provider_executed: chunk.providerExecuted }
117
+ : {}),
89
118
  });
90
119
  return;
120
+ }
91
121
  case "tool-input-delta": {
92
122
  const tool = this.find_tool(chunk.toolCallId);
93
123
  if (tool && tool.state !== "input-streaming") return;
@@ -100,45 +130,103 @@ export class SessionAssistantMessageWriter {
100
130
  }
101
131
  case "tool-input-available": {
102
132
  const tool = this.find_tool(chunk.toolCallId);
103
- if (tool && tool.state !== "input-streaming") return;
133
+ const call_provider_metadata = to_session_provider_metadata(
134
+ chunk.providerMetadata,
135
+ );
136
+ if (tool && tool.state !== "input-streaming") {
137
+ if (
138
+ call_provider_metadata === undefined &&
139
+ chunk.providerExecuted === undefined
140
+ ) return;
141
+ await this.upsert_tool(chunk.toolCallId, {
142
+ tool_name: tool.tool_name,
143
+ state: tool.state,
144
+ ...(call_provider_metadata !== undefined
145
+ ? { call_provider_metadata }
146
+ : {}),
147
+ ...(chunk.providerExecuted !== undefined
148
+ ? { provider_executed: chunk.providerExecuted }
149
+ : {}),
150
+ });
151
+ return;
152
+ }
104
153
  await this.upsert_tool(chunk.toolCallId, {
105
154
  tool_name: chunk.toolName,
106
155
  state: "ready",
107
156
  input: to_session_json_value(chunk.input),
157
+ ...(call_provider_metadata !== undefined
158
+ ? { call_provider_metadata }
159
+ : {}),
160
+ ...(chunk.providerExecuted !== undefined
161
+ ? { provider_executed: chunk.providerExecuted }
162
+ : {}),
108
163
  });
109
164
  return;
110
165
  }
111
- case "tool-input-error":
166
+ case "tool-input-error": {
167
+ const call_provider_metadata = to_session_provider_metadata(
168
+ chunk.providerMetadata,
169
+ );
112
170
  await this.upsert_tool(chunk.toolCallId, {
113
171
  tool_name: chunk.toolName,
114
172
  state: "failed",
115
173
  input: to_session_json_value(chunk.input),
116
174
  error: chunk.errorText,
175
+ ...(call_provider_metadata !== undefined
176
+ ? { call_provider_metadata }
177
+ : {}),
178
+ ...(chunk.providerExecuted !== undefined
179
+ ? { provider_executed: chunk.providerExecuted }
180
+ : {}),
117
181
  });
118
182
  return;
183
+ }
119
184
  case "tool-output-available": {
120
185
  const tool = this.find_tool(chunk.toolCallId);
121
186
  if (
122
187
  tool?.state === "failed" &&
123
188
  (tool.error === "Approval denied" || tool.error === "Approval expired")
124
189
  ) return;
190
+ const result_provider_metadata = to_session_provider_metadata(
191
+ chunk.providerMetadata,
192
+ );
125
193
  await this.upsert_tool(chunk.toolCallId, {
126
194
  tool_name: tool?.tool_name || "unknown",
127
195
  state: "completed",
128
196
  output: to_session_json_value(chunk.output),
197
+ ...(result_provider_metadata !== undefined
198
+ ? { result_provider_metadata }
199
+ : {}),
200
+ ...(chunk.providerExecuted !== undefined
201
+ ? { provider_executed: chunk.providerExecuted }
202
+ : {}),
203
+ });
204
+ return;
205
+ }
206
+ case "tool-output-error": {
207
+ const tool = this.find_tool(chunk.toolCallId);
208
+ const result_provider_metadata = to_session_provider_metadata(
209
+ chunk.providerMetadata,
210
+ );
211
+ await this.upsert_tool(chunk.toolCallId, {
212
+ tool_name: tool?.tool_name || "unknown",
213
+ state: "failed",
214
+ error: chunk.errorText,
215
+ ...(result_provider_metadata !== undefined
216
+ ? { result_provider_metadata }
217
+ : {}),
218
+ ...(chunk.providerExecuted !== undefined
219
+ ? { provider_executed: chunk.providerExecuted }
220
+ : {}),
129
221
  });
130
222
  return;
131
223
  }
132
- case "tool-output-error":
133
224
  case "tool-output-denied": {
134
225
  const tool = this.find_tool(chunk.toolCallId);
135
226
  await this.upsert_tool(chunk.toolCallId, {
136
227
  tool_name: tool?.tool_name || "unknown",
137
228
  state: "failed",
138
- error:
139
- chunk.type === "tool-output-error"
140
- ? chunk.errorText
141
- : "Tool output denied",
229
+ error: "Tool output denied",
142
230
  });
143
231
  return;
144
232
  }
@@ -158,6 +246,26 @@ export class SessionAssistantMessageWriter {
158
246
  await this.recorder.update_assistant_part(this.message_id, part);
159
247
  }
160
248
 
249
+ /**
250
+ * 用 AI SDK 最终 UIMessage 中的 Tool 快照校准流式写入结果。
251
+ *
252
+ * 关键点(中文):最终快照只覆盖实际存在的字段;缺失 metadata 时继续保留
253
+ * 流式阶段已经写入的 Provider 快照。
254
+ */
255
+ async reconcile_final_tool_part(
256
+ part: SessionAssistantToolPart,
257
+ ): Promise<void> {
258
+ await this.enqueue_write(async () => {
259
+ const current = this.find_tool(part.tool_call_id);
260
+ await this.upsert_part({
261
+ ...(current || {}),
262
+ ...part,
263
+ part_id: current?.part_id || `tool:${part.tool_call_id}`,
264
+ sequence: current?.sequence || this.next_part_sequence(),
265
+ });
266
+ });
267
+ }
268
+
161
269
  /** Executor 在调用 Tool 实现前写入完整输入。 */
162
270
  async prepare_tool_input(input: SessionToolInputReady): Promise<void> {
163
271
  await this.enqueue_write(async () => {
@@ -5,6 +5,7 @@
5
5
  * 避免 Tool、User Data 等外部输入污染 canonical Message。
6
6
  */
7
7
 
8
+ import type { ProviderMetadata } from "ai";
8
9
  import type { JsonValue } from "@/types/common/Json.js";
9
10
 
10
11
  /** 把任意运行时输入转换为可持久化的 JSON 值。 */
@@ -23,3 +24,32 @@ export function to_session_json_value(input: unknown): JsonValue {
23
24
  return String(input);
24
25
  }
25
26
  }
27
+
28
+ /**
29
+ * 把 AI SDK Provider metadata 规整为可持久化快照。
30
+ *
31
+ * 关键点(中文)
32
+ * - ProviderMetadata 顶层是 provider ID 到 JSON 对象的映射。
33
+ * - 新 metadata 代表完整快照,不在这里猜测 Provider 的字段合并语义。
34
+ * - 非法 metadata 返回 undefined,由调用方保留已经持久化的旧快照。
35
+ */
36
+ export function to_session_provider_metadata(
37
+ input: unknown,
38
+ ): ProviderMetadata | undefined {
39
+ if (!is_plain_object(input)) return undefined;
40
+ try {
41
+ const value = JSON.parse(JSON.stringify(input)) as unknown;
42
+ if (!is_plain_object(value)) return undefined;
43
+ if (!Object.values(value).every(is_plain_object)) return undefined;
44
+ return value as ProviderMetadata;
45
+ } catch {
46
+ return undefined;
47
+ }
48
+ }
49
+
50
+ /** 判断未知值是否为普通对象。 */
51
+ function is_plain_object(input: unknown): input is Record<string, unknown> {
52
+ if (!input || typeof input !== "object" || Array.isArray(input)) return false;
53
+ const prototype = Object.getPrototypeOf(input);
54
+ return prototype === Object.prototype || prototype === null;
55
+ }
@@ -8,7 +8,10 @@
8
8
  */
9
9
 
10
10
  import type { UIMessage } from "ai";
11
- import { to_session_json_value } from "@/session/messages/SessionJsonValue.js";
11
+ import {
12
+ to_session_json_value,
13
+ to_session_provider_metadata,
14
+ } from "@/session/messages/SessionJsonValue.js";
12
15
  import type {
13
16
  SessionAssistantMessage,
14
17
  SessionAssistantMessagePart,
@@ -161,6 +164,12 @@ export function from_ui_assistant_parts(
161
164
  }
162
165
  if (type === "dynamic-tool" || type.startsWith("tool-")) {
163
166
  const state = String(candidate.state || "");
167
+ const call_provider_metadata = to_session_provider_metadata(
168
+ candidate.callProviderMetadata,
169
+ );
170
+ const result_provider_metadata = to_session_provider_metadata(
171
+ candidate.resultProviderMetadata,
172
+ );
164
173
  return [{
165
174
  part_id: String(candidate.toolCallId || `tool:${index + 1}`),
166
175
  sequence: index + 1,
@@ -189,6 +198,15 @@ export function from_ui_assistant_parts(
189
198
  ? { output: to_session_json_value(candidate.output) }
190
199
  : {}),
191
200
  ...(candidate.errorText ? { error: String(candidate.errorText) } : {}),
201
+ ...(call_provider_metadata !== undefined
202
+ ? { call_provider_metadata }
203
+ : {}),
204
+ ...(result_provider_metadata !== undefined
205
+ ? { result_provider_metadata }
206
+ : {}),
207
+ ...(typeof candidate.providerExecuted === "boolean"
208
+ ? { provider_executed: candidate.providerExecuted }
209
+ : {}),
192
210
  }];
193
211
  }
194
212
  return [];
@@ -251,6 +269,14 @@ function to_ui_assistant_parts(
251
269
  throw new Error(`Unsupported Assistant part: ${part.type}`);
252
270
  }
253
271
  const input = part.input ?? parse_json(part.input_text);
272
+ const call_provider_fields = {
273
+ ...(part.call_provider_metadata !== undefined
274
+ ? { callProviderMetadata: part.call_provider_metadata }
275
+ : {}),
276
+ ...(part.provider_executed !== undefined
277
+ ? { providerExecuted: part.provider_executed }
278
+ : {}),
279
+ };
254
280
  if (part.state === "completed") {
255
281
  return {
256
282
  type: "dynamic-tool",
@@ -259,6 +285,10 @@ function to_ui_assistant_parts(
259
285
  state: "output-available",
260
286
  input,
261
287
  output: part.output,
288
+ ...call_provider_fields,
289
+ ...(part.result_provider_metadata !== undefined
290
+ ? { resultProviderMetadata: part.result_provider_metadata }
291
+ : {}),
262
292
  };
263
293
  }
264
294
  if (part.state === "failed") {
@@ -269,6 +299,10 @@ function to_ui_assistant_parts(
269
299
  state: "output-error",
270
300
  input,
271
301
  errorText: part.error || "Tool failed",
302
+ ...call_provider_fields,
303
+ ...(part.result_provider_metadata !== undefined
304
+ ? { resultProviderMetadata: part.result_provider_metadata }
305
+ : {}),
272
306
  };
273
307
  }
274
308
  if (part.state === "approval-required") {
@@ -279,6 +313,7 @@ function to_ui_assistant_parts(
279
313
  state: "approval-requested",
280
314
  input,
281
315
  approval: { id: part.approval?.approval_id || `approval:${part.tool_call_id}` },
316
+ ...call_provider_fields,
282
317
  };
283
318
  }
284
319
  return {
@@ -287,6 +322,7 @@ function to_ui_assistant_parts(
287
322
  toolCallId: part.tool_call_id,
288
323
  state: part.state === "input-streaming" ? "input-streaming" : "input-available",
289
324
  input,
325
+ ...call_provider_fields,
290
326
  };
291
327
  }) as UIMessage["parts"];
292
328
  }
@@ -5,6 +5,7 @@
5
5
  * 均为内部 part,不提升为顶层消息。
6
6
  */
7
7
 
8
+ import type { ProviderMetadata } from "ai";
8
9
  import type { JsonObject, JsonValue } from "@/types/common/Json.js";
9
10
  import type { SessionApproval } from "@/types/session/SessionApproval.js";
10
11
 
@@ -133,6 +134,12 @@ export interface SessionAssistantToolPart {
133
134
  output?: JsonValue;
134
135
  /** 工具失败信息。 */
135
136
  error?: string;
137
+ /** 工具调用阶段由 AI SDK Provider 返回的可序列化 metadata。 */
138
+ call_provider_metadata?: ProviderMetadata;
139
+ /** 工具结果阶段由 AI SDK Provider 返回的可序列化 metadata。 */
140
+ result_provider_metadata?: ProviderMetadata;
141
+ /** 当前工具是否由模型 Provider 直接执行。 */
142
+ provider_executed?: boolean;
136
143
  /** 当前 Tool 等待或已经处理过的完整审批快照。 */
137
144
  approval?: SessionApproval;
138
145
  }