@downcity/agent 1.1.258 → 1.1.260

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.
@@ -3,51 +3,24 @@
3
3
  *
4
4
  * 关键点(中文)
5
5
  * - Agent 与 Session 对外统一接收 AgentModel,executor 内部只处理 AI SDK LanguageModel。
6
- * - CityModel 保留模型目录信息;运行时连接信息通过隐藏协议提供。
7
- * - 这里直接创建 OpenAI-compatible LanguageModel,不再保留旧的 text/stream 反向适配。
6
+ * - CityModel 自身实现 LanguageModelV3,executor 可以直接调用。
8
7
  * - 这里不依赖 @downcity/city,只依赖 @downcity/type 的共享协议。
9
8
  */
10
9
 
11
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
12
10
  import {
13
- CITY_MODEL_INVOKER,
14
11
  isCityModel,
15
- type CityModel,
16
12
  } from "@downcity/type";
17
13
  import type { LanguageModel } from "ai";
18
14
 
19
- /** 已完成转换的 CityModel 与 LanguageModel 对应关系。 */
20
- const normalized_models = new WeakMap<object, LanguageModel>();
21
-
22
15
  /**
23
16
  * Agent SDK 可接受的模型输入。
24
17
  */
25
- export type AgentModel = LanguageModel | CityModel;
26
-
27
- /**
28
- * 将 CityModel 的 hidden connection 转换为 AI SDK LanguageModel。
29
- */
30
- function cityModelToLanguageModel(model: CityModel): LanguageModel {
31
- const connection = model[CITY_MODEL_INVOKER].connection();
32
- const provider = createOpenAICompatible({
33
- name: "downcity",
34
- baseURL: connection.base_url,
35
- apiKey: connection.api_key,
36
- });
37
- return provider.languageModel(connection.model_id) as LanguageModel;
38
- }
18
+ export type AgentModel = LanguageModel;
39
19
 
40
20
  /**
41
21
  * 将 Agent 可接受的模型输入归一为 AI SDK LanguageModel。
42
22
  */
43
23
  export function normalizeAgentModel(model: AgentModel): LanguageModel {
44
- if (isCityModel(model)) {
45
- const cached = normalized_models.get(model);
46
- if (cached) return cached;
47
- const normalized = cityModelToLanguageModel(model);
48
- normalized_models.set(model, normalized);
49
- return normalized;
50
- }
51
24
  return model;
52
25
  }
53
26
 
@@ -2,37 +2,73 @@
2
2
 
3
3
  `executor/` 是 Session 内部的模型与 Tool Loop 执行内核。普通 SDK 用户只通过 `Session` 调用它。
4
4
 
5
+ 完整 Session Runtime 设计见 [`docs/session-runtime-architecture.md`](../../../../docs/session-runtime-architecture.md)。
6
+
5
7
  ## 边界
6
8
 
7
- - `SessionTurn` 拥有输入队列、Turn Handle、取消信号与 Message 收口。
8
- - `SessionComposer` 根据只读 Session 快照组装 system、history 和 tools。
9
- - `Executor` 管理单次模型执行、上下文超限重试和 Step Plugin lease
10
- - `CoreEngineRunner` 执行 `streamText()`、Tool Loop、续写与内存上下文折叠。
9
+ - `SessionTurn` 拥有输入队列、Turn Handle、取消信号与 Assistant Message 收口。
10
+ - `SessionComposer` 根据只读 Session 快照组装 system、messages 和 tools。
11
+ - `Executor` 管理单次模型执行、上下文超限重试和 Step Plugin Lease
12
+ - `CoreEngineRunner` 执行 `streamText()`、Tool Loop、续写和内存上下文折叠。
11
13
  - `SessionMessages` 是 Message 唯一事实源;Executor 不写文件、不持有 Store。
12
14
 
13
- ## 调用链
15
+ ## 执行关系
16
+
17
+ ```mermaid
18
+ flowchart LR
19
+ Turn["SessionTurn"] --> Executor
20
+ Executor --> Composer["SessionComposer"]
21
+ Composer --> Input["system + messages + tools"]
22
+ Executor --> Engine["CoreEngineRunner"]
23
+ Input --> Engine
24
+ Engine --> Model["LanguageModel"]
25
+ Engine --> Tools["Tools / Plugins"]
26
+ Engine -->|"Chunk / Callback"| Turn
27
+ Turn --> Messages["SessionMessages"]
28
+ ```
29
+
30
+ 每个模型 Step 前,`SessionTurn` 先消费排队的 Steer 和状态 Command。Executor 随后捕获最新 effective state,并调用 Composer 生成完整 Step 输入。
14
31
 
15
32
  ```text
16
33
  Session.prompt()
17
- -> SessionTurn 写入 User Message
18
- -> SessionComposer.compose(readonly snapshot)
19
- -> Executor.run()
34
+ -> SessionTurn 持久化 User Message
35
+ -> Executor 捕获只读 Session 快照
36
+ -> SessionComposer.compose()
20
37
  -> CoreEngineRunner.run()
21
- -> SessionTurn 接收 stream chunk
38
+ -> SessionTurn 接收 Stream Chunk
22
39
  -> SessionMessages 完成 Assistant Message
23
40
  ```
24
41
 
25
- 每个模型 Step 前,`SessionTurn` 消费排队的 steer 和状态命令,Composer 再基于最新 effective state 生成完整 Step 输入。
42
+ ## Composer 去向
43
+
44
+ 过去的四个独立 Composer 不再构成执行管线:
45
+
46
+ | 旧能力 | 当前归属 |
47
+ | --- | --- |
48
+ | `SystemComposer` | `DefaultSessionComposer.compose()` + `SessionSystem` |
49
+ | `HistoryComposer` | `SessionMessages.context_snapshot()` + `SessionMessageCodec` |
50
+ | `ContextComposer` 的 tools | `Session.create_compose_input()` + `SessionComposer.compose()` |
51
+ | `ContextComposer` 的 Step Callback | `SessionTurn` + `CoreEngineRunner` |
52
+ | `ContextComposer` 的 fallback Assistant | `CoreEngineRunner` + `ExecutorRecoveryPolicy` |
53
+ | `CompactionComposer` | `SessionComposer.compact()` + `should_compact()` |
54
+
55
+ 统一 Composer 只回答两个策略问题:
56
+
57
+ 1. 当前 Step 的 system、messages 和 tools 是什么?
58
+ 2. 当前只读 Message 快照应生成什么压缩计划?
59
+
60
+ Queue 消费、Turn 控制、Message 写入、Mutation 发布和 Segment 提交都不是 Composer 职责。
26
61
 
27
62
  ## Compaction
28
63
 
29
64
  ```text
30
- SessionComposer.compact(snapshot)
65
+ SessionComposer.compact(readonly snapshot)
31
66
  -> SessionCompactionPlan
32
- -> SessionMessages.compact_active(plan)
67
+ -> Session.commit_compaction_plan()
68
+ -> SessionMessages.compact_active()
33
69
  ```
34
70
 
35
- Composer 可以调用模型生成 Summary,但不能修改 Message、Metadata 或发布事件。持久化提交始终由 Session 完成。
71
+ Composer 可以调用模型生成 Summary,但不能修改 Message、Metadata 或发布事件。持久化提交始终由 Session 领域完成。
36
72
 
37
73
  ## 目录
38
74
 
@@ -40,7 +76,7 @@ Composer 可以调用模型生成 Summary,但不能修改 Message、Metadata
40
76
  executor/
41
77
  Executor.ts
42
78
  core-engine/ 模型与 Tool Loop
43
- composer/system/ 默认 system prompt 领域实现
79
+ composer/system/ 可复用的默认 system prompt 领域实现
44
80
  messages/ AI SDK 消息转换
45
81
  services/ 执行恢复策略
46
82
  tools/ Tool 运行辅助
@@ -2,6 +2,8 @@
2
2
  * SessionMessages:Session Message 的唯一领域入口。
3
3
  *
4
4
  * 完整 Message 与 Assistant 草稿先持久化,成功后再发布实时 Mutation。
5
+ * 同一 Assistant Message 的流式、审批与关闭操作共享一条写队列,保证 revision
6
+ * 从最新快照严格递增。
5
7
  */
6
8
 
7
9
  import type { UIMessage } from "ai";
@@ -65,6 +67,8 @@ export class SessionMessages {
65
67
  private readonly store: JsonlSessionMessageStore;
66
68
  private readonly publish: SessionMessagesOptions["publish"];
67
69
  private readonly messages_by_id = new Map<string, SessionMessage>();
70
+ /** 按 Assistant Message 隔离的完整写事务链。 */
71
+ private readonly assistant_write_chains = new Map<string, Promise<void>>();
68
72
  private initialized = false;
69
73
 
70
74
  constructor(options: SessionMessagesOptions) {
@@ -514,6 +518,18 @@ export class SessionMessages {
514
518
  delta: string,
515
519
  ): Promise<void> {
516
520
  if (!delta) return;
521
+ await this.enqueue_assistant_write(message_id, async () => {
522
+ await this.append_assistant_delta_serialized(message_id, part_id, type, delta);
523
+ });
524
+ }
525
+
526
+ /** 在 Assistant 写队列内追加文本 delta。 */
527
+ private async append_assistant_delta_serialized(
528
+ message_id: string,
529
+ part_id: string,
530
+ type: "text" | "reasoning",
531
+ delta: string,
532
+ ): Promise<void> {
517
533
  const current = require_message([...this.messages_by_id.values()], message_id, "assistant");
518
534
  require_streaming_assistant(current);
519
535
  const part = current.parts.find((item) => item.part_id === part_id);
@@ -551,6 +567,16 @@ export class SessionMessages {
551
567
  async update_assistant_part(
552
568
  message_id: string,
553
569
  part: SessionAssistantMessagePart,
570
+ ): Promise<void> {
571
+ await this.enqueue_assistant_write(message_id, async () => {
572
+ await this.update_assistant_part_serialized(message_id, part);
573
+ });
574
+ }
575
+
576
+ /** 在 Assistant 写队列内提交完整 Part 快照。 */
577
+ private async update_assistant_part_serialized(
578
+ message_id: string,
579
+ part: SessionAssistantMessagePart,
554
580
  ): Promise<void> {
555
581
  const current = require_message([...this.messages_by_id.values()], message_id, "assistant");
556
582
  require_streaming_assistant(current);
@@ -588,6 +614,16 @@ export class SessionMessages {
588
614
  async complete_assistant_message(
589
615
  message_id: string,
590
616
  status: "completed" | "stopped" | "failed",
617
+ ): Promise<void> {
618
+ await this.enqueue_assistant_write(message_id, async () => {
619
+ await this.complete_assistant_message_serialized(message_id, status);
620
+ });
621
+ }
622
+
623
+ /** 在 Assistant 写队列内关闭草稿并写入 Active。 */
624
+ private async complete_assistant_message_serialized(
625
+ message_id: string,
626
+ status: "completed" | "stopped" | "failed",
591
627
  ): Promise<void> {
592
628
  const current = require_message([...this.messages_by_id.values()], message_id, "assistant");
593
629
  require_streaming_assistant(current);
@@ -645,16 +681,22 @@ export class SessionMessages {
645
681
 
646
682
  /** 把完整审批请求写入对应的流式 Tool Part。 */
647
683
  async request_tool_approval(approval: SessionApproval): Promise<void> {
648
- const tool = this.require_streaming_tool(approval.tool_call_id);
649
- if (tool.part.state !== "ready") {
650
- throw new Error(
651
- `Tool approval requires ready input: ${approval.tool_call_id} (${tool.part.state})`,
652
- );
653
- }
654
- await this.update_assistant_part(tool.message_id, {
655
- ...tool.part,
656
- state: "approval-required",
657
- approval,
684
+ const { message_id } = this.require_streaming_tool(approval.tool_call_id);
685
+ await this.enqueue_assistant_write(message_id, async () => {
686
+ const tool = this.require_streaming_tool(approval.tool_call_id);
687
+ if (tool.message_id !== message_id) {
688
+ throw new Error(`Tool Assistant Message changed: ${approval.tool_call_id}`);
689
+ }
690
+ if (tool.part.state !== "ready") {
691
+ throw new Error(
692
+ `Tool approval requires ready input: ${approval.tool_call_id} (${tool.part.state})`,
693
+ );
694
+ }
695
+ await this.update_assistant_part_serialized(message_id, {
696
+ ...tool.part,
697
+ state: "approval-required",
698
+ approval,
699
+ });
658
700
  });
659
701
  }
660
702
 
@@ -667,24 +709,52 @@ export class SessionMessages {
667
709
  /** 当前审批关联的 Tool Call。 */
668
710
  tool_call_id: string;
669
711
  }): Promise<void> {
670
- const tool = this.require_streaming_tool(input.tool_call_id);
671
- if (tool.part.approval?.approval_id !== input.approval_id) {
672
- throw new Error(`Tool approval identity mismatch: ${input.approval_id}`);
673
- }
674
- await this.update_assistant_part(tool.message_id, {
675
- ...tool.part,
676
- state: input.decision === "approved" ? "running" : "failed",
677
- ...(input.decision === "approved"
678
- ? {}
679
- : {
680
- error:
681
- input.decision === "expired"
682
- ? "Approval expired"
683
- : "Approval denied",
684
- }),
712
+ const { message_id } = this.require_streaming_tool(input.tool_call_id);
713
+ await this.enqueue_assistant_write(message_id, async () => {
714
+ const tool = this.require_streaming_tool(input.tool_call_id);
715
+ if (tool.message_id !== message_id) {
716
+ throw new Error(`Tool Assistant Message changed: ${input.tool_call_id}`);
717
+ }
718
+ if (tool.part.approval?.approval_id !== input.approval_id) {
719
+ throw new Error(`Tool approval identity mismatch: ${input.approval_id}`);
720
+ }
721
+ await this.update_assistant_part_serialized(message_id, {
722
+ ...tool.part,
723
+ state: input.decision === "approved" ? "running" : "failed",
724
+ ...(input.decision === "approved"
725
+ ? {}
726
+ : {
727
+ error:
728
+ input.decision === "expired"
729
+ ? "Approval expired"
730
+ : "Approval denied",
731
+ }),
732
+ });
685
733
  });
686
734
  }
687
735
 
736
+ /**
737
+ * 串行执行同一 Assistant Message 的完整写事务。
738
+ *
739
+ * 失败事务不会阻塞后续写入;链尾仅用于排序,真实错误仍返回给调用方。
740
+ */
741
+ private async enqueue_assistant_write<T>(
742
+ message_id: string,
743
+ operation: () => Promise<T>,
744
+ ): Promise<T> {
745
+ const previous = this.assistant_write_chains.get(message_id) || Promise.resolve();
746
+ const result = previous.then(operation, operation);
747
+ const chain = result.then(() => undefined, () => undefined);
748
+ this.assistant_write_chains.set(message_id, chain);
749
+ try {
750
+ return await result;
751
+ } finally {
752
+ if (this.assistant_write_chains.get(message_id) === chain) {
753
+ this.assistant_write_chains.delete(message_id);
754
+ }
755
+ }
756
+ }
757
+
688
758
  /** 查找当前流式 Assistant 中的 Tool Part,否则抛出明确错误。 */
689
759
  private require_streaming_tool(tool_call_id: string): SessionStreamingToolLocation {
690
760
  const tool = this.find_streaming_tool(tool_call_id);