@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.
- package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.js +0 -2
- package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
- package/bin/executor/messages/SessionMessageCodec.d.ts +0 -12
- package/bin/executor/messages/SessionMessageCodec.d.ts.map +1 -1
- package/bin/executor/messages/SessionMessageCodec.js +0 -16
- package/bin/executor/messages/SessionMessageCodec.js.map +1 -1
- package/bin/session/SessionTitle.d.ts.map +1 -1
- package/bin/session/SessionTitle.js +6 -2
- package/bin/session/SessionTitle.js.map +1 -1
- package/bin/session/SessionTurn.d.ts.map +1 -1
- package/bin/session/SessionTurn.js +3 -0
- package/bin/session/SessionTurn.js.map +1 -1
- package/bin/session/messages/SessionAssistantMessageWriter.d.ts +8 -1
- package/bin/session/messages/SessionAssistantMessageWriter.d.ts.map +1 -1
- package/bin/session/messages/SessionAssistantMessageWriter.js +98 -9
- package/bin/session/messages/SessionAssistantMessageWriter.js.map +1 -1
- package/bin/session/messages/SessionJsonValue.d.ts +10 -0
- package/bin/session/messages/SessionJsonValue.d.ts.map +1 -1
- package/bin/session/messages/SessionJsonValue.js +30 -0
- package/bin/session/messages/SessionJsonValue.js.map +1 -1
- package/bin/session/messages/SessionMessageCodec.d.ts.map +1 -1
- package/bin/session/messages/SessionMessageCodec.js +30 -1
- package/bin/session/messages/SessionMessageCodec.js.map +1 -1
- package/bin/types/session/SessionMessage.d.ts +7 -0
- package/bin/types/session/SessionMessage.d.ts.map +1 -1
- package/package.json +4 -3
- package/scripts/city-model-tool-loop.test.mjs +13 -0
- package/scripts/core-engine-context-compaction.test.mjs +4 -0
- package/scripts/openai-responses-continuation.test.mjs +48 -0
- package/scripts/session-messages.test.mjs +171 -1
- package/src/executor/core-engine/CoreEngineRunner.ts +0 -2
- package/src/executor/messages/SessionMessageCodec.ts +0 -21
- package/src/session/SessionTitle.ts +6 -2
- package/src/session/SessionTurn.ts +2 -0
- package/src/session/messages/SessionAssistantMessageWriter.ts +118 -10
- package/src/session/messages/SessionJsonValue.ts +30 -0
- package/src/session/messages/SessionMessageCodec.ts +37 -1
- package/src/types/session/SessionMessage.ts +7 -0
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
}
|