@downcity/agent 1.1.270 → 1.1.276
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 +18 -0
- package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
- package/bin/executor/core-engine/CoreEngineUiStreamCollector.d.ts +1 -1
- package/bin/executor/core-engine/CoreEngineUiStreamCollector.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineUiStreamCollector.js +6 -4
- package/bin/executor/core-engine/CoreEngineUiStreamCollector.js.map +1 -1
- package/bin/executor/types/SessionRun.d.ts +14 -1
- package/bin/executor/types/SessionRun.d.ts.map +1 -1
- package/bin/session/SessionMessages.d.ts +2 -0
- package/bin/session/SessionMessages.d.ts.map +1 -1
- package/bin/session/SessionMessages.js +21 -0
- package/bin/session/SessionMessages.js.map +1 -1
- package/bin/session/SessionTurn.d.ts.map +1 -1
- package/bin/session/SessionTurn.js +20 -7
- package/bin/session/SessionTurn.js.map +1 -1
- package/bin/session/messages/SessionAssistantMessageWriter.d.ts +21 -8
- package/bin/session/messages/SessionAssistantMessageWriter.d.ts.map +1 -1
- package/bin/session/messages/SessionAssistantMessageWriter.js +116 -23
- package/bin/session/messages/SessionAssistantMessageWriter.js.map +1 -1
- package/bin/types/executor/SessionRunContext.d.ts +21 -1
- package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
- package/package.json +3 -3
- package/scripts/core-engine-ui-stream-collector.test.mjs +38 -0
- package/scripts/executor-failure-result.test.mjs +104 -1
- package/scripts/session-messages.test.mjs +161 -2
- package/scripts/session-turn-failure.test.mjs +108 -1
- package/src/executor/core-engine/CoreEngineRunner.ts +23 -0
- package/src/executor/core-engine/CoreEngineUiStreamCollector.ts +6 -4
- package/src/executor/types/SessionRun.ts +20 -1
- package/src/session/SessionMessages.ts +31 -0
- package/src/session/SessionTurn.ts +19 -7
- package/src/session/messages/SessionAssistantMessageWriter.ts +143 -26
- package/src/types/executor/SessionRunContext.ts +26 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -230,6 +230,7 @@ test("Streaming Tool Provider metadata 在输入和输出状态间完整保留",
|
|
|
230
230
|
turn_id: "turn-provider-metadata",
|
|
231
231
|
segment_index: 1,
|
|
232
232
|
});
|
|
233
|
+
await writer.begin_step();
|
|
233
234
|
await writer.apply_chunk({
|
|
234
235
|
type: "tool-input-start",
|
|
235
236
|
toolCallId: "call_1",
|
|
@@ -256,7 +257,7 @@ test("Streaming Tool Provider metadata 在输入和输出状态间完整保留",
|
|
|
256
257
|
providerExecuted: true,
|
|
257
258
|
providerMetadata: { openai: { resultId: "result_1" } },
|
|
258
259
|
});
|
|
259
|
-
await writer.
|
|
260
|
+
await writer.finish_step([{
|
|
260
261
|
part_id: "call_1",
|
|
261
262
|
sequence: 1,
|
|
262
263
|
type: "tool",
|
|
@@ -267,7 +268,7 @@ test("Streaming Tool Provider metadata 在输入和输出状态间完整保留",
|
|
|
267
268
|
output: "ok",
|
|
268
269
|
provider_executed: true,
|
|
269
270
|
call_provider_metadata: { openai: { itemId: "fc_final" } },
|
|
270
|
-
});
|
|
271
|
+
}]);
|
|
271
272
|
await writer.complete();
|
|
272
273
|
|
|
273
274
|
const assistant = (await read_jsonl(file_path))[0];
|
|
@@ -820,6 +821,7 @@ test("不同模型 step 重复使用 Text chunk ID 时仍保持真实 Part 顺
|
|
|
820
821
|
const { recorder, file_path } = await create_recorder("reused-text-id-order-test");
|
|
821
822
|
const writer = await recorder.open_assistant_message({ turn_id: "turn-1", segment_index: 1 });
|
|
822
823
|
|
|
824
|
+
await writer.begin_step();
|
|
823
825
|
await writer.apply_chunk({ type: "text-start", id: "txt-0" });
|
|
824
826
|
await writer.apply_chunk({ type: "text-delta", id: "txt-0", delta: "first" });
|
|
825
827
|
await writer.apply_chunk({ type: "text-end", id: "txt-0" });
|
|
@@ -831,7 +833,27 @@ test("不同模型 step 重复使用 Text chunk ID 时仍保持真实 Part 顺
|
|
|
831
833
|
input: { query: "first" },
|
|
832
834
|
});
|
|
833
835
|
await writer.apply_chunk({ type: "tool-output-available", toolCallId: "call-1", output: "one" });
|
|
836
|
+
await writer.finish_step([
|
|
837
|
+
{
|
|
838
|
+
part_id: "text-1",
|
|
839
|
+
sequence: 1,
|
|
840
|
+
type: "text",
|
|
841
|
+
text: "first",
|
|
842
|
+
state: "done",
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
part_id: "call-1",
|
|
846
|
+
sequence: 2,
|
|
847
|
+
type: "tool",
|
|
848
|
+
tool_call_id: "call-1",
|
|
849
|
+
tool_name: "search",
|
|
850
|
+
state: "completed",
|
|
851
|
+
input: { query: "first" },
|
|
852
|
+
output: "one",
|
|
853
|
+
},
|
|
854
|
+
]);
|
|
834
855
|
|
|
856
|
+
await writer.begin_step();
|
|
835
857
|
await writer.apply_chunk({ type: "text-start", id: "txt-0" });
|
|
836
858
|
await writer.apply_chunk({ type: "text-delta", id: "txt-0", delta: "second" });
|
|
837
859
|
await writer.apply_chunk({ type: "text-end", id: "txt-0" });
|
|
@@ -843,6 +865,25 @@ test("不同模型 step 重复使用 Text chunk ID 时仍保持真实 Part 顺
|
|
|
843
865
|
input: { query: "second" },
|
|
844
866
|
});
|
|
845
867
|
await writer.apply_chunk({ type: "tool-output-available", toolCallId: "call-2", output: "two" });
|
|
868
|
+
await writer.finish_step([
|
|
869
|
+
{
|
|
870
|
+
part_id: "text-1",
|
|
871
|
+
sequence: 1,
|
|
872
|
+
type: "text",
|
|
873
|
+
text: "second",
|
|
874
|
+
state: "done",
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
part_id: "call-2",
|
|
878
|
+
sequence: 2,
|
|
879
|
+
type: "tool",
|
|
880
|
+
tool_call_id: "call-2",
|
|
881
|
+
tool_name: "search",
|
|
882
|
+
state: "completed",
|
|
883
|
+
input: { query: "second" },
|
|
884
|
+
output: "two",
|
|
885
|
+
},
|
|
886
|
+
]);
|
|
846
887
|
await writer.complete();
|
|
847
888
|
|
|
848
889
|
const assistant = (await read_jsonl(file_path))[0];
|
|
@@ -855,10 +896,58 @@ test("不同模型 step 重复使用 Text chunk ID 时仍保持真实 Part 顺
|
|
|
855
896
|
assert.notEqual(assistant.parts[0].part_id, assistant.parts[2].part_id);
|
|
856
897
|
});
|
|
857
898
|
|
|
899
|
+
test("不同模型 step 重复 Source 与 Data ID 时创建独立 canonical Parts", async () => {
|
|
900
|
+
const { recorder, file_path } = await create_recorder("reused-structured-id-order-test");
|
|
901
|
+
const writer = await recorder.open_assistant_message({ turn_id: "turn-1", segment_index: 1 });
|
|
902
|
+
|
|
903
|
+
for (const step of [1, 2]) {
|
|
904
|
+
await writer.begin_step();
|
|
905
|
+
await writer.apply_chunk({
|
|
906
|
+
type: "source-url",
|
|
907
|
+
sourceId: "source-1",
|
|
908
|
+
url: `https://example.com/${step}`,
|
|
909
|
+
});
|
|
910
|
+
await writer.apply_chunk({
|
|
911
|
+
type: "data-status",
|
|
912
|
+
id: "status-1",
|
|
913
|
+
data: { step },
|
|
914
|
+
});
|
|
915
|
+
await writer.finish_step([
|
|
916
|
+
{
|
|
917
|
+
part_id: "source-1",
|
|
918
|
+
sequence: 1,
|
|
919
|
+
type: "source",
|
|
920
|
+
source_type: "url",
|
|
921
|
+
source_id: "source-1",
|
|
922
|
+
url: `https://example.com/${step}`,
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
part_id: "data-1",
|
|
926
|
+
sequence: 2,
|
|
927
|
+
type: "data",
|
|
928
|
+
data_type: "data-status",
|
|
929
|
+
data: { step },
|
|
930
|
+
data_id: "status-1",
|
|
931
|
+
},
|
|
932
|
+
]);
|
|
933
|
+
}
|
|
934
|
+
await writer.complete();
|
|
935
|
+
|
|
936
|
+
const assistant = (await read_jsonl(file_path))[0];
|
|
937
|
+
assert.deepEqual(
|
|
938
|
+
assistant.parts.map((part) => part.type),
|
|
939
|
+
["source", "data", "source", "data"],
|
|
940
|
+
);
|
|
941
|
+
assert.deepEqual(assistant.parts.map((part) => part.sequence), [1, 2, 3, 4]);
|
|
942
|
+
assert.notEqual(assistant.parts[0].part_id, assistant.parts[2].part_id);
|
|
943
|
+
assert.notEqual(assistant.parts[1].part_id, assistant.parts[3].part_id);
|
|
944
|
+
});
|
|
945
|
+
|
|
858
946
|
test("空 Text Start 不会抢占后续 Tool 的真实顺序", async () => {
|
|
859
947
|
const { recorder, events, file_path } = await create_recorder("deferred-text-order-test");
|
|
860
948
|
const writer = await recorder.open_assistant_message({ turn_id: "turn-1", segment_index: 1 });
|
|
861
949
|
|
|
950
|
+
await writer.begin_step();
|
|
862
951
|
await writer.apply_chunk({ type: "reasoning-start", id: "reasoning-0" });
|
|
863
952
|
await writer.apply_chunk({ type: "reasoning-delta", id: "reasoning-0", delta: "先执行命令" });
|
|
864
953
|
await writer.apply_chunk({ type: "reasoning-end", id: "reasoning-0" });
|
|
@@ -881,6 +970,32 @@ test("空 Text Start 不会抢占后续 Tool 的真实顺序", async () => {
|
|
|
881
970
|
});
|
|
882
971
|
await writer.apply_chunk({ type: "text-delta", id: "text-0", delta: "命令执行完成" });
|
|
883
972
|
await writer.apply_chunk({ type: "text-end", id: "text-0" });
|
|
973
|
+
await writer.finish_step([
|
|
974
|
+
{
|
|
975
|
+
part_id: "reasoning-1",
|
|
976
|
+
sequence: 1,
|
|
977
|
+
type: "reasoning",
|
|
978
|
+
text: "先执行命令",
|
|
979
|
+
state: "done",
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
part_id: "call-1",
|
|
983
|
+
sequence: 2,
|
|
984
|
+
type: "tool",
|
|
985
|
+
tool_call_id: "call-1",
|
|
986
|
+
tool_name: "shell_exec",
|
|
987
|
+
state: "completed",
|
|
988
|
+
input: { cmd: "pwd" },
|
|
989
|
+
output: { success: true },
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
part_id: "text-1",
|
|
993
|
+
sequence: 3,
|
|
994
|
+
type: "text",
|
|
995
|
+
text: "命令执行完成",
|
|
996
|
+
state: "done",
|
|
997
|
+
},
|
|
998
|
+
]);
|
|
884
999
|
await writer.complete();
|
|
885
1000
|
|
|
886
1001
|
const assistant = (await read_jsonl(file_path))[0];
|
|
@@ -902,6 +1017,50 @@ test("空 Text Start 不会抢占后续 Tool 的真实顺序", async () => {
|
|
|
902
1017
|
);
|
|
903
1018
|
});
|
|
904
1019
|
|
|
1020
|
+
test("step 最终快照缺少 canonical Tool chunk 时拒绝猜测顺序", async () => {
|
|
1021
|
+
const { recorder, events, file_path } = await create_recorder("final-reconcile-order-test");
|
|
1022
|
+
const writer = await recorder.open_assistant_message({ turn_id: "turn-1", segment_index: 1 });
|
|
1023
|
+
|
|
1024
|
+
await writer.begin_step();
|
|
1025
|
+
await writer.apply_chunk({ type: "text-start", id: "text-0" });
|
|
1026
|
+
await writer.apply_chunk({ type: "text-delta", id: "text-0", delta: "最终结论" });
|
|
1027
|
+
await writer.apply_chunk({ type: "text-end", id: "text-0" });
|
|
1028
|
+
const streamed_text_part_id = events.find(
|
|
1029
|
+
(event) => event.variant === "part" && event.type === "text",
|
|
1030
|
+
).part.part_id;
|
|
1031
|
+
|
|
1032
|
+
await assert.rejects(
|
|
1033
|
+
writer.finish_step([
|
|
1034
|
+
{
|
|
1035
|
+
part_id: "call-1",
|
|
1036
|
+
sequence: 1,
|
|
1037
|
+
type: "tool",
|
|
1038
|
+
tool_call_id: "call-1",
|
|
1039
|
+
tool_name: "shell_exec",
|
|
1040
|
+
state: "completed",
|
|
1041
|
+
input: { cmd: "pwd" },
|
|
1042
|
+
output: { success: true },
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
part_id: "text-1",
|
|
1046
|
+
sequence: 2,
|
|
1047
|
+
type: "text",
|
|
1048
|
+
text: "最终结论",
|
|
1049
|
+
state: "done",
|
|
1050
|
+
},
|
|
1051
|
+
]),
|
|
1052
|
+
/snapshot mismatch: part count 1 != 2/,
|
|
1053
|
+
);
|
|
1054
|
+
await writer.abort_step();
|
|
1055
|
+
await writer.fail("canonical snapshot mismatch");
|
|
1056
|
+
|
|
1057
|
+
const assistant = (await read_jsonl(file_path))[0];
|
|
1058
|
+
assert.equal(assistant.status, "failed");
|
|
1059
|
+
assert.deepEqual(assistant.parts.map((part) => part.type), ["text"]);
|
|
1060
|
+
assert.deepEqual(assistant.parts.map((part) => part.sequence), [1]);
|
|
1061
|
+
assert.equal(assistant.parts[0].part_id, streamed_text_part_id);
|
|
1062
|
+
});
|
|
1063
|
+
|
|
905
1064
|
test("重启时将 Assistant 草稿收口为 stopped,并将运行中 Action 标记失败", async () => {
|
|
906
1065
|
const { recorder, file_path, assistant_message_file_path } = await create_recorder("recovery-test");
|
|
907
1066
|
await recorder.append_user_message({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file 验证 Turn
|
|
2
|
+
* @file 验证 Turn 失败收口与最终 Assistant 快照顺序。
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import assert from "node:assert/strict";
|
|
@@ -99,3 +99,110 @@ test("Provider 在部分输出后失败时保留 failed Assistant 并追加 Erro
|
|
|
99
99
|
assert.equal(page.items[1].parts[0].text, "partial response");
|
|
100
100
|
assert.equal(page.items[2].message, "stream interrupted");
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
test("Turn 使用 step canonical chunks 保持 Tool 与最终正文顺序", async () => {
|
|
104
|
+
const { messages, turn } = await create_turn_harness(async (run_context) => {
|
|
105
|
+
await run_context.on_ui_message_step_start();
|
|
106
|
+
await run_context.onUiMessageChunkCallback({
|
|
107
|
+
type: "tool-input-start",
|
|
108
|
+
toolCallId: "call-1",
|
|
109
|
+
toolName: "shell_exec",
|
|
110
|
+
});
|
|
111
|
+
await run_context.onUiMessageChunkCallback({
|
|
112
|
+
type: "tool-input-available",
|
|
113
|
+
toolCallId: "call-1",
|
|
114
|
+
toolName: "shell_exec",
|
|
115
|
+
input: { cmd: "pwd" },
|
|
116
|
+
});
|
|
117
|
+
await run_context.onUiMessageChunkCallback({
|
|
118
|
+
type: "tool-output-available",
|
|
119
|
+
toolCallId: "call-1",
|
|
120
|
+
output: { success: true },
|
|
121
|
+
});
|
|
122
|
+
await run_context.onUiMessageChunkCallback({ type: "text-start", id: "text-1" });
|
|
123
|
+
await run_context.onUiMessageChunkCallback({
|
|
124
|
+
type: "text-delta",
|
|
125
|
+
id: "text-1",
|
|
126
|
+
delta: "最终结论",
|
|
127
|
+
});
|
|
128
|
+
await run_context.onUiMessageChunkCallback({ type: "text-end", id: "text-1" });
|
|
129
|
+
const assistant_message = {
|
|
130
|
+
id: "assistant-1",
|
|
131
|
+
role: "assistant",
|
|
132
|
+
parts: [
|
|
133
|
+
{
|
|
134
|
+
type: "dynamic-tool",
|
|
135
|
+
toolCallId: "call-1",
|
|
136
|
+
toolName: "shell_exec",
|
|
137
|
+
state: "output-available",
|
|
138
|
+
input: { cmd: "pwd" },
|
|
139
|
+
output: { success: true },
|
|
140
|
+
},
|
|
141
|
+
{ type: "text", text: "最终结论", state: "done" },
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
await run_context.on_ui_message_step_finish(assistant_message);
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
assistantMessage: assistant_message,
|
|
148
|
+
deferredPersistedUserMessages: [],
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const handle = await turn.prompt({ query: "diagnose" });
|
|
153
|
+
const result = await handle.finished;
|
|
154
|
+
const page = await messages.list_messages();
|
|
155
|
+
const assistant = page.items.find((message) => message.type === "assistant");
|
|
156
|
+
|
|
157
|
+
assert.equal(result.success, true);
|
|
158
|
+
assert.deepEqual(assistant.parts.map((part) => part.type), ["tool", "text"]);
|
|
159
|
+
assert.deepEqual(assistant.parts.map((part) => part.sequence), [1, 2]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("Turn 在 step 最终快照出现未流式写入的 Tool 时失败", async () => {
|
|
163
|
+
const { messages, turn } = await create_turn_harness(async (run_context) => {
|
|
164
|
+
await run_context.on_ui_message_step_start();
|
|
165
|
+
await run_context.onUiMessageChunkCallback({ type: "text-start", id: "text-1" });
|
|
166
|
+
await run_context.onUiMessageChunkCallback({
|
|
167
|
+
type: "text-delta",
|
|
168
|
+
id: "text-1",
|
|
169
|
+
delta: "最终结论",
|
|
170
|
+
});
|
|
171
|
+
await run_context.onUiMessageChunkCallback({ type: "text-end", id: "text-1" });
|
|
172
|
+
try {
|
|
173
|
+
await run_context.on_ui_message_step_finish({
|
|
174
|
+
id: "assistant-1",
|
|
175
|
+
role: "assistant",
|
|
176
|
+
parts: [
|
|
177
|
+
{
|
|
178
|
+
type: "dynamic-tool",
|
|
179
|
+
toolCallId: "call-1",
|
|
180
|
+
toolName: "shell_exec",
|
|
181
|
+
state: "output-available",
|
|
182
|
+
input: { cmd: "pwd" },
|
|
183
|
+
output: { success: true },
|
|
184
|
+
},
|
|
185
|
+
{ type: "text", text: "最终结论", state: "done" },
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
await run_context.on_ui_message_step_abort();
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
error: error.message,
|
|
193
|
+
deferredPersistedUserMessages: [],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const handle = await turn.prompt({ query: "diagnose" });
|
|
199
|
+
const result = await handle.finished;
|
|
200
|
+
const page = await messages.list_messages();
|
|
201
|
+
const assistant = page.items.find((message) => message.type === "assistant");
|
|
202
|
+
|
|
203
|
+
assert.equal(result.success, false);
|
|
204
|
+
assert.match(result.error, /snapshot mismatch/);
|
|
205
|
+
assert.equal(assistant.status, "failed");
|
|
206
|
+
assert.deepEqual(assistant.parts.map((part) => part.type), ["text"]);
|
|
207
|
+
assert.equal(page.items.at(-1).type, "error");
|
|
208
|
+
});
|
|
@@ -298,7 +298,13 @@ export class CoreEngineRunner {
|
|
|
298
298
|
last_observed_stream_error = undefined;
|
|
299
299
|
let step_assistant_ui_message: SessionMessageRecordV1;
|
|
300
300
|
let executed_steps: StepResult<Record<string, Tool>>[];
|
|
301
|
+
let canonical_step_started = false;
|
|
302
|
+
let canonical_step_finished = false;
|
|
301
303
|
try {
|
|
304
|
+
if (input.run_context.on_ui_message_step_start) {
|
|
305
|
+
await input.run_context.on_ui_message_step_start();
|
|
306
|
+
canonical_step_started = true;
|
|
307
|
+
}
|
|
302
308
|
const result = streamText({
|
|
303
309
|
model: step_inputs.model,
|
|
304
310
|
system,
|
|
@@ -326,6 +332,13 @@ export class CoreEngineRunner {
|
|
|
326
332
|
abortSignal: input.run_context.abortSignal,
|
|
327
333
|
});
|
|
328
334
|
|
|
335
|
+
if (input.run_context.on_ui_message_step_finish) {
|
|
336
|
+
await input.run_context.on_ui_message_step_finish(
|
|
337
|
+
step_assistant_ui_message,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
canonical_step_finished = true;
|
|
341
|
+
|
|
329
342
|
final_assistant_ui_message = mergeAssistantUiMessages(
|
|
330
343
|
final_assistant_ui_message,
|
|
331
344
|
step_assistant_ui_message,
|
|
@@ -336,6 +349,13 @@ export class CoreEngineRunner {
|
|
|
336
349
|
message_state.appendRuntimeSessionMessage(step_assistant_ui_message);
|
|
337
350
|
executed_steps = await result.steps;
|
|
338
351
|
} catch (error) {
|
|
352
|
+
if (
|
|
353
|
+
canonical_step_started &&
|
|
354
|
+
!canonical_step_finished &&
|
|
355
|
+
input.run_context.on_ui_message_step_abort
|
|
356
|
+
) {
|
|
357
|
+
await input.run_context.on_ui_message_step_abort();
|
|
358
|
+
}
|
|
339
359
|
const compact_error = this.should_compact_on_error(error)
|
|
340
360
|
? error
|
|
341
361
|
: last_observed_stream_error;
|
|
@@ -557,6 +577,7 @@ export class CoreEngineRunner {
|
|
|
557
577
|
return {
|
|
558
578
|
success: true,
|
|
559
579
|
assistantMessage: final_message,
|
|
580
|
+
assistant_file_parts: [...input.run_context.pendingAssistantFileParts],
|
|
560
581
|
...(compact_required ? { compact_required: true } : {}),
|
|
561
582
|
deferredPersistedUserMessages: [
|
|
562
583
|
...input.run_context.deferredPersistedUserMessages,
|
|
@@ -578,6 +599,7 @@ export class CoreEngineRunner {
|
|
|
578
599
|
success: false,
|
|
579
600
|
error: error_text,
|
|
580
601
|
...(stopped_message ? { assistantMessage: stopped_message } : {}),
|
|
602
|
+
assistant_file_parts: [...input.run_context.pendingAssistantFileParts],
|
|
581
603
|
...(compact_required ? { compact_required: true } : {}),
|
|
582
604
|
deferredPersistedUserMessages: [
|
|
583
605
|
...input.run_context.deferredPersistedUserMessages,
|
|
@@ -601,6 +623,7 @@ export class CoreEngineRunner {
|
|
|
601
623
|
return {
|
|
602
624
|
success: false,
|
|
603
625
|
error: error_text,
|
|
626
|
+
assistant_file_parts: [...input.run_context.pendingAssistantFileParts],
|
|
604
627
|
...(compact_required ? { compact_required: true } : {}),
|
|
605
628
|
deferredPersistedUserMessages: [
|
|
606
629
|
...input.run_context.deferredPersistedUserMessages,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 关键点(中文)
|
|
5
5
|
* - 必须完整消费 AI SDK UI stream,才能稳定触发 `onFinish`。
|
|
6
6
|
* - 优先使用结构化 `responseMessage`;缺失时才回退到纯文本。
|
|
7
|
-
* - UI chunk
|
|
7
|
+
* - UI chunk 回调负责上层 canonical message 写入,失败必须终止本轮收敛。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { streamText } from "ai";
|
|
@@ -54,6 +54,7 @@ export async function collectFinalAssistantMessageFromUiStream(params: {
|
|
|
54
54
|
let streamedAssistantMessage: SessionMessageRecordV1 | null = null;
|
|
55
55
|
let uiFinishSummary: JsonObject | null = null;
|
|
56
56
|
let streamed_text = "";
|
|
57
|
+
let callback_failed = false;
|
|
57
58
|
|
|
58
59
|
const uiStream = params.result.toUIMessageStream<SessionMessageRecordV1>({
|
|
59
60
|
// 关键点(中文):SDK stream 需要 reasoning 旁路事件时可直接消费;最终落盘仍由 responseMessage 收敛。
|
|
@@ -91,12 +92,13 @@ export async function collectFinalAssistantMessageFromUiStream(params: {
|
|
|
91
92
|
if (typeof params.onUiMessageChunkCallback !== "function") continue;
|
|
92
93
|
try {
|
|
93
94
|
await params.onUiMessageChunkCallback(chunk);
|
|
94
|
-
} catch {
|
|
95
|
-
|
|
95
|
+
} catch (error) {
|
|
96
|
+
callback_failed = true;
|
|
97
|
+
throw error;
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
} catch (error) {
|
|
99
|
-
if (!params.abortSignal?.aborted) {
|
|
101
|
+
if (callback_failed || !params.abortSignal?.aborted) {
|
|
100
102
|
throw error;
|
|
101
103
|
}
|
|
102
104
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - 输出暴露可选 assistantMessage(UIMessage)。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { Tool, UIMessageChunk } from "ai";
|
|
10
|
+
import type { FileUIPart, Tool, UIMessageChunk } from "ai";
|
|
11
11
|
import type {
|
|
12
12
|
SessionRecordV1,
|
|
13
13
|
SessionMessageRecordV1,
|
|
@@ -81,6 +81,17 @@ export type SessionUiMessageChunkCallback = (
|
|
|
81
81
|
chunk: SessionUiMessageChunk,
|
|
82
82
|
) => Promise<void>;
|
|
83
83
|
|
|
84
|
+
/** 单个模型 UI stream 开始前的 canonical step 回调。 */
|
|
85
|
+
export type SessionUiMessageStepStartCallback = () => Promise<void>;
|
|
86
|
+
|
|
87
|
+
/** 单个模型 UI stream 完成后的 canonical step 快照回调。 */
|
|
88
|
+
export type SessionUiMessageStepFinishCallback = (
|
|
89
|
+
message: SessionMessageRecordV1,
|
|
90
|
+
) => Promise<void>;
|
|
91
|
+
|
|
92
|
+
/** 单个模型 UI stream 未完成时的 canonical step 清理回调。 */
|
|
93
|
+
export type SessionUiMessageStepAbortCallback = () => Promise<void>;
|
|
94
|
+
|
|
84
95
|
/**
|
|
85
96
|
* Session 执行结果。
|
|
86
97
|
*/
|
|
@@ -104,6 +115,14 @@ export interface SessionRunResult {
|
|
|
104
115
|
*/
|
|
105
116
|
assistantMessage?: SessionMessageRecordV1 | null;
|
|
106
117
|
|
|
118
|
+
/**
|
|
119
|
+
* 工具运行期显式生成、并在 Assistant 末尾持久化的文件 Parts。
|
|
120
|
+
*
|
|
121
|
+
* 关键点(中文):该字段与聚合 `assistantMessage` 分离,Session 不需要从最终
|
|
122
|
+
* UIMessage 反推哪些文件来自工具通道。
|
|
123
|
+
*/
|
|
124
|
+
assistant_file_parts?: FileUIPart[];
|
|
125
|
+
|
|
107
126
|
/**
|
|
108
127
|
* 本轮执行结束后待写入长期历史的 user 消息。
|
|
109
128
|
*
|
|
@@ -610,6 +610,37 @@ export class SessionMessages {
|
|
|
610
610
|
} as SessionMutation, message);
|
|
611
611
|
}
|
|
612
612
|
|
|
613
|
+
/** @internal 原子提交当前 Assistant step 的 metadata 快照。 */
|
|
614
|
+
async commit_assistant_step(
|
|
615
|
+
message_id: string,
|
|
616
|
+
parts: SessionAssistantMessagePart[],
|
|
617
|
+
): Promise<void> {
|
|
618
|
+
await this.enqueue_assistant_write(message_id, async () => {
|
|
619
|
+
const current = require_message([...this.messages_by_id.values()], message_id, "assistant");
|
|
620
|
+
require_streaming_assistant(current);
|
|
621
|
+
if (
|
|
622
|
+
parts.length !== current.parts.length ||
|
|
623
|
+
parts.some((part, index) =>
|
|
624
|
+
part.part_id !== current.parts[index]?.part_id ||
|
|
625
|
+
part.sequence !== current.parts[index]?.sequence
|
|
626
|
+
)
|
|
627
|
+
) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
`Assistant step changed canonical Part identity: ${message_id}`,
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
const created_at = Date.now();
|
|
633
|
+
const message: SessionAssistantMessage = {
|
|
634
|
+
...current,
|
|
635
|
+
revision: current.revision + 1,
|
|
636
|
+
updated_at: created_at,
|
|
637
|
+
parts: structuredClone(parts),
|
|
638
|
+
};
|
|
639
|
+
await this.store.write_assistant_message(message);
|
|
640
|
+
this.accept_message(message);
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
613
644
|
/** @internal 收口 Assistant Message。 */
|
|
614
645
|
async complete_assistant_message(
|
|
615
646
|
message_id: string,
|
|
@@ -471,6 +471,18 @@ export class SessionTurn {
|
|
|
471
471
|
const writer = await ensure_assistant_writer();
|
|
472
472
|
await writer.apply_chunk(chunk);
|
|
473
473
|
},
|
|
474
|
+
on_ui_message_step_start: async () => {
|
|
475
|
+
const writer = await ensure_assistant_writer();
|
|
476
|
+
await writer.begin_step();
|
|
477
|
+
},
|
|
478
|
+
on_ui_message_step_finish: async (message) => {
|
|
479
|
+
const writer = await ensure_assistant_writer();
|
|
480
|
+
await writer.finish_step(from_ui_assistant_parts(message.parts));
|
|
481
|
+
},
|
|
482
|
+
on_ui_message_step_abort: async () => {
|
|
483
|
+
const writer = assistant_writer_ref.current;
|
|
484
|
+
if (writer) await writer.abort_step();
|
|
485
|
+
},
|
|
474
486
|
on_tool_input_ready: async (tool_input) => {
|
|
475
487
|
const writer = await ensure_assistant_writer();
|
|
476
488
|
await writer.prepare_tool_input(tool_input);
|
|
@@ -511,12 +523,8 @@ export class SessionTurn {
|
|
|
511
523
|
: [];
|
|
512
524
|
const final_assistant_writer = assistant_writer_ref.current;
|
|
513
525
|
if (final_assistant_writer) {
|
|
514
|
-
for (const part of
|
|
515
|
-
if (part.type === "file")
|
|
516
|
-
await final_assistant_writer.append_file_part(part);
|
|
517
|
-
} else if (part.type === "tool") {
|
|
518
|
-
await final_assistant_writer.reconcile_final_tool_part(part);
|
|
519
|
-
}
|
|
526
|
+
for (const part of from_ui_assistant_parts(result.assistant_file_parts)) {
|
|
527
|
+
if (part.type === "file") await final_assistant_writer.append_file_part(part);
|
|
520
528
|
}
|
|
521
529
|
if (input.abort_signal?.aborted) {
|
|
522
530
|
await final_assistant_writer.stop();
|
|
@@ -618,7 +626,11 @@ function is_assistant_content_chunk(type: string): boolean {
|
|
|
618
626
|
type === "reasoning-delta" ||
|
|
619
627
|
type === "reasoning-end" ||
|
|
620
628
|
type.startsWith("tool-") ||
|
|
621
|
-
type === "file"
|
|
629
|
+
type === "file" ||
|
|
630
|
+
type === "source-url" ||
|
|
631
|
+
type === "source-document" ||
|
|
632
|
+
type === "start-step" ||
|
|
633
|
+
type.startsWith("data-")
|
|
622
634
|
);
|
|
623
635
|
}
|
|
624
636
|
|