@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.
Files changed (35) hide show
  1. package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
  2. package/bin/executor/core-engine/CoreEngineRunner.js +18 -0
  3. package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
  4. package/bin/executor/core-engine/CoreEngineUiStreamCollector.d.ts +1 -1
  5. package/bin/executor/core-engine/CoreEngineUiStreamCollector.d.ts.map +1 -1
  6. package/bin/executor/core-engine/CoreEngineUiStreamCollector.js +6 -4
  7. package/bin/executor/core-engine/CoreEngineUiStreamCollector.js.map +1 -1
  8. package/bin/executor/types/SessionRun.d.ts +14 -1
  9. package/bin/executor/types/SessionRun.d.ts.map +1 -1
  10. package/bin/session/SessionMessages.d.ts +2 -0
  11. package/bin/session/SessionMessages.d.ts.map +1 -1
  12. package/bin/session/SessionMessages.js +21 -0
  13. package/bin/session/SessionMessages.js.map +1 -1
  14. package/bin/session/SessionTurn.d.ts.map +1 -1
  15. package/bin/session/SessionTurn.js +20 -7
  16. package/bin/session/SessionTurn.js.map +1 -1
  17. package/bin/session/messages/SessionAssistantMessageWriter.d.ts +21 -8
  18. package/bin/session/messages/SessionAssistantMessageWriter.d.ts.map +1 -1
  19. package/bin/session/messages/SessionAssistantMessageWriter.js +116 -23
  20. package/bin/session/messages/SessionAssistantMessageWriter.js.map +1 -1
  21. package/bin/types/executor/SessionRunContext.d.ts +21 -1
  22. package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
  23. package/package.json +3 -3
  24. package/scripts/core-engine-ui-stream-collector.test.mjs +38 -0
  25. package/scripts/executor-failure-result.test.mjs +104 -1
  26. package/scripts/session-messages.test.mjs +161 -2
  27. package/scripts/session-turn-failure.test.mjs +108 -1
  28. package/src/executor/core-engine/CoreEngineRunner.ts +23 -0
  29. package/src/executor/core-engine/CoreEngineUiStreamCollector.ts +6 -4
  30. package/src/executor/types/SessionRun.ts +20 -1
  31. package/src/session/SessionMessages.ts +31 -0
  32. package/src/session/SessionTurn.ts +19 -7
  33. package/src/session/messages/SessionAssistantMessageWriter.ts +143 -26
  34. package/src/types/executor/SessionRunContext.ts +26 -0
  35. package/tsconfig.tsbuildinfo +1 -1
@@ -31,7 +31,10 @@ export class SessionAssistantMessageWriter {
31
31
  Pick<SessionAssistantTextPart, "type" | "provider_metadata">
32
32
  >();
33
33
  private readonly active_text_part_ids = new Map<string, string>();
34
+ private readonly current_step_part_ids = new Set<string>();
34
35
  private write_chain: Promise<void> = Promise.resolve();
36
+ private step_index = 0;
37
+ private step_active = false;
35
38
  private closed = false;
36
39
 
37
40
  constructor(recorder: SessionMessages, message_id: string) {
@@ -46,6 +49,70 @@ export class SessionAssistantMessageWriter {
46
49
  });
47
50
  }
48
51
 
52
+ /** 建立一个独立模型 UI stream 的 canonical step 作用域。 */
53
+ async begin_step(): Promise<void> {
54
+ await this.enqueue_write(async () => {
55
+ if (this.closed) throw new Error("Assistant Message writer is closed");
56
+ if (this.step_active) {
57
+ throw new Error("Assistant canonical step is already active");
58
+ }
59
+ this.step_index += 1;
60
+ this.step_active = true;
61
+ this.current_step_part_ids.clear();
62
+ this.pending_text_parts.clear();
63
+ this.active_text_part_ids.clear();
64
+ });
65
+ }
66
+
67
+ /**
68
+ * 校验当前 step 的最终快照并原子补充 metadata。
69
+ *
70
+ * 最终快照不能创建、删除或重排 Part;任何不一致都表示 canonical chunk
71
+ * 链路不完整,必须让当前 Turn 失败。
72
+ */
73
+ async finish_step(parts: SessionAssistantMessagePart[]): Promise<void> {
74
+ await this.enqueue_write(async () => {
75
+ if (!this.step_active) {
76
+ throw new Error("Assistant canonical step is not active");
77
+ }
78
+ const current = this.current_message();
79
+ const current_step_parts = current.parts.filter(
80
+ (part) =>
81
+ this.current_step_part_ids.has(part.part_id) &&
82
+ part.type !== "step-start",
83
+ );
84
+ const final_step_parts = parts.filter((part) => part.type !== "step-start");
85
+ if (current_step_parts.length !== final_step_parts.length) {
86
+ throw this.step_snapshot_error(
87
+ `part count ${current_step_parts.length} != ${final_step_parts.length}`,
88
+ );
89
+ }
90
+
91
+ const merged_parts = new Map<string, SessionAssistantMessagePart>();
92
+ for (let index = 0; index < current_step_parts.length; index += 1) {
93
+ const current_part = current_step_parts[index];
94
+ const final_part = final_step_parts[index];
95
+ merged_parts.set(
96
+ current_part.part_id,
97
+ this.merge_step_part(current_part, final_part, index),
98
+ );
99
+ }
100
+ await this.recorder.commit_assistant_step(
101
+ this.message_id,
102
+ current.parts.map((part) => merged_parts.get(part.part_id) || part),
103
+ );
104
+ this.reset_step_state();
105
+ });
106
+ }
107
+
108
+ /** 释放异常结束的 step 作用域并保留已经写入的 canonical Parts。 */
109
+ async abort_step(): Promise<void> {
110
+ await this.enqueue_write(async () => {
111
+ if (!this.step_active) return;
112
+ this.reset_step_state();
113
+ });
114
+ }
115
+
49
116
  /** 在当前 Assistant writer 的单写者队列中应用原始 chunk。 */
50
117
  private async apply_chunk_serialized(chunk: UIMessageChunk): Promise<void> {
51
118
  if (this.closed) throw new Error("Assistant Message writer is closed");
@@ -306,7 +373,7 @@ export class SessionAssistantMessageWriter {
306
373
  });
307
374
  return;
308
375
  case "source-url": {
309
- const part_id = `source:${chunk.sourceId}`;
376
+ const part_id = `source:${this.step_index}:${chunk.sourceId}`;
310
377
  const current_part = current.parts.find((part) => part.part_id === part_id);
311
378
  const provider_metadata = to_session_provider_metadata(chunk.providerMetadata);
312
379
  await this.upsert_part({
@@ -324,7 +391,7 @@ export class SessionAssistantMessageWriter {
324
391
  return;
325
392
  }
326
393
  case "source-document": {
327
- const part_id = `source:${chunk.sourceId}`;
394
+ const part_id = `source:${this.step_index}:${chunk.sourceId}`;
328
395
  const current_part = current.parts.find((part) => part.part_id === part_id);
329
396
  const provider_metadata = to_session_provider_metadata(chunk.providerMetadata);
330
397
  await this.upsert_part({
@@ -356,7 +423,9 @@ export class SessionAssistantMessageWriter {
356
423
  const data_id = typeof data_chunk.id === "string"
357
424
  ? data_chunk.id
358
425
  : undefined;
359
- const part_id = `data:${data_id ?? generateId()}`;
426
+ const part_id = data_id
427
+ ? `data:${this.step_index}:${data_id}`
428
+ : `data:${generateId()}`;
360
429
  const current_part = current.parts.find((part) => part.part_id === part_id);
361
430
  await this.upsert_part({
362
431
  part_id,
@@ -375,26 +444,7 @@ export class SessionAssistantMessageWriter {
375
444
  /** 写入一个完整 Assistant part。 */
376
445
  async upsert_part(part: SessionAssistantMessagePart): Promise<void> {
377
446
  await this.recorder.update_assistant_part(this.message_id, part);
378
- }
379
-
380
- /**
381
- * 用 AI SDK 最终 UIMessage 中的 Tool 快照校准流式写入结果。
382
- *
383
- * 关键点(中文):最终快照只覆盖实际存在的字段;缺失 metadata 时继续保留
384
- * 流式阶段已经写入的 Provider 快照。
385
- */
386
- async reconcile_final_tool_part(
387
- part: SessionAssistantToolPart,
388
- ): Promise<void> {
389
- await this.enqueue_write(async () => {
390
- const current = this.find_tool(part.tool_call_id);
391
- await this.upsert_part({
392
- ...(current || {}),
393
- ...part,
394
- part_id: current?.part_id || `tool:${part.tool_call_id}`,
395
- sequence: current?.sequence || this.next_part_sequence(),
396
- });
397
- });
447
+ if (this.step_active) this.current_step_part_ids.add(part.part_id);
398
448
  }
399
449
 
400
450
  /** Executor 在调用 Tool 实现前写入完整输入。 */
@@ -426,6 +476,7 @@ export class SessionAssistantMessageWriter {
426
476
  const existing = current.parts.find(
427
477
  (part) =>
428
478
  part.type === "file" &&
479
+ (!this.step_active || this.current_step_part_ids.has(part.part_id)) &&
429
480
  part.url === input.url &&
430
481
  part.media_type === input.media_type,
431
482
  );
@@ -500,6 +551,73 @@ export class SessionAssistantMessageWriter {
500
551
  );
501
552
  }
502
553
 
554
+ /** 校验并合并同一位置的 canonical Part 与 step 最终快照。 */
555
+ private merge_step_part(
556
+ current_part: SessionAssistantMessagePart,
557
+ final_part: SessionAssistantMessagePart,
558
+ index: number,
559
+ ): SessionAssistantMessagePart {
560
+ if (current_part.type !== final_part.type) {
561
+ throw this.step_snapshot_error(
562
+ `part ${index + 1} type ${current_part.type} != ${final_part.type}`,
563
+ );
564
+ }
565
+ if (
566
+ (current_part.type === "text" || current_part.type === "reasoning") &&
567
+ (final_part.type === "text" || final_part.type === "reasoning")
568
+ ) {
569
+ if (current_part.text !== final_part.text) {
570
+ throw this.step_snapshot_error(`part ${index + 1} text differs`);
571
+ }
572
+ } else if (current_part.type === "tool" && final_part.type === "tool") {
573
+ if (current_part.tool_call_id !== final_part.tool_call_id) {
574
+ throw this.step_snapshot_error(`part ${index + 1} tool_call_id differs`);
575
+ }
576
+ } else if (current_part.type === "file" && final_part.type === "file") {
577
+ if (
578
+ current_part.url !== final_part.url ||
579
+ current_part.media_type !== final_part.media_type
580
+ ) {
581
+ throw this.step_snapshot_error(`part ${index + 1} file identity differs`);
582
+ }
583
+ } else if (current_part.type === "source" && final_part.type === "source") {
584
+ if (
585
+ current_part.source_type !== final_part.source_type ||
586
+ current_part.source_id !== final_part.source_id
587
+ ) {
588
+ throw this.step_snapshot_error(`part ${index + 1} source identity differs`);
589
+ }
590
+ } else if (current_part.type === "data" && final_part.type === "data") {
591
+ if (
592
+ current_part.data_type !== final_part.data_type ||
593
+ current_part.data_id !== final_part.data_id
594
+ ) {
595
+ throw this.step_snapshot_error(`part ${index + 1} data identity differs`);
596
+ }
597
+ }
598
+ return {
599
+ ...current_part,
600
+ ...final_part,
601
+ part_id: current_part.part_id,
602
+ sequence: current_part.sequence,
603
+ } as SessionAssistantMessagePart;
604
+ }
605
+
606
+ /** 构造不包含正文与工具输出的结构化 step 快照错误。 */
607
+ private step_snapshot_error(detail: string): Error {
608
+ return new Error(
609
+ `Assistant canonical step ${this.step_index} snapshot mismatch: ${detail}`,
610
+ );
611
+ }
612
+
613
+ /** 清理当前 step 的临时关联状态。 */
614
+ private reset_step_state(): void {
615
+ this.step_active = false;
616
+ this.current_step_part_ids.clear();
617
+ this.pending_text_parts.clear();
618
+ this.active_text_part_ids.clear();
619
+ }
620
+
503
621
  /** 计算下一个不可变 Part 顺序号。 */
504
622
  private next_part_sequence(): number {
505
623
  return this.current_message().parts.reduce(
@@ -531,7 +649,7 @@ export class SessionAssistantMessageWriter {
531
649
  type: "text" | "reasoning",
532
650
  chunk_id: string,
533
651
  ): string {
534
- return `${type}:${chunk_id}`;
652
+ return `${this.step_index}:${type}:${chunk_id}`;
535
653
  }
536
654
 
537
655
  /** 在首个有效 Delta 到达时才固定文本 Part 的真实顺序。 */
@@ -603,8 +721,7 @@ export class SessionAssistantMessageWriter {
603
721
  status: "completed" | "stopped" | "failed",
604
722
  ): Promise<void> {
605
723
  if (this.closed) return;
606
- this.pending_text_parts.clear();
607
- this.active_text_part_ids.clear();
724
+ this.reset_step_state();
608
725
  await this.recorder.complete_assistant_message(this.message_id, status);
609
726
  this.closed = true;
610
727
  }
@@ -15,6 +15,9 @@ import type {
15
15
  import type {
16
16
  SessionAssistantStepCallback,
17
17
  SessionUiMessageChunkCallback,
18
+ SessionUiMessageStepAbortCallback,
19
+ SessionUiMessageStepFinishCallback,
20
+ SessionUiMessageStepStartCallback,
18
21
  } from "@/executor/types/SessionRun.js";
19
22
  import type { SessionUserMessageV1 } from "@/executor/types/SessionRecords.js";
20
23
  import type { FileUIPart } from "ai";
@@ -107,6 +110,29 @@ export interface SessionRunContext {
107
110
  */
108
111
  onUiMessageChunkCallback?: SessionUiMessageChunkCallback;
109
112
 
113
+ /**
114
+ * 单个模型 UI stream 开始回调。
115
+ *
116
+ * 关键点(中文):Session writer 用它建立独立 step 作用域,确保重复 chunk id
117
+ * 不会跨模型调用复用同一个 canonical Part。
118
+ */
119
+ on_ui_message_step_start?: SessionUiMessageStepStartCallback;
120
+
121
+ /**
122
+ * 单个模型 UI stream 完成快照回调。
123
+ *
124
+ * 关键点(中文):最终快照只校验当前 step 的顺序并补充 metadata,不能创建、
125
+ * 删除或重排 canonical Part。
126
+ */
127
+ on_ui_message_step_finish?: SessionUiMessageStepFinishCallback;
128
+
129
+ /**
130
+ * 单个模型 UI stream 异常结束回调。
131
+ *
132
+ * 关键点(中文):用于释放 step 作用域;已经持久化的流式 Part 继续保留。
133
+ */
134
+ on_ui_message_step_abort?: SessionUiMessageStepAbortCallback;
135
+
110
136
  /** Tool 实现开始执行前提交完整输入的顺序屏障。 */
111
137
  on_tool_input_ready?: (input: SessionToolInputReady) => Promise<void>;
112
138