@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.
- package/README.md +18 -26
- package/bin/agent/AgentModel.d.ts +2 -4
- package/bin/agent/AgentModel.d.ts.map +1 -1
- package/bin/agent/AgentModel.js +2 -26
- package/bin/agent/AgentModel.js.map +1 -1
- package/bin/session/SessionMessages.d.ts +16 -0
- package/bin/session/SessionMessages.d.ts.map +1 -1
- package/bin/session/SessionMessages.js +75 -22
- package/bin/session/SessionMessages.js.map +1 -1
- package/package.json +3 -4
- package/scripts/city-model-tool-loop.test.mjs +142 -299
- package/scripts/linux-bubblewrap-sandbox.test.mjs +40 -123
- package/scripts/multi-agent-plugin-isolation.test.mjs +86 -207
- package/scripts/session-messages.test.mjs +114 -3
- package/scripts/shell-sandbox-env.test.mjs +47 -60
- package/src/agent/AgentModel.ts +2 -29
- package/src/executor/README.md +50 -14
- package/src/session/SessionMessages.ts +95 -25
- package/tsconfig.tsbuildinfo +1 -1
package/src/agent/AgentModel.ts
CHANGED
|
@@ -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
|
|
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
|
|
package/src/executor/README.md
CHANGED
|
@@ -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、
|
|
9
|
-
- `Executor` 管理单次模型执行、上下文超限重试和 Step Plugin
|
|
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
|
|
18
|
-
->
|
|
19
|
-
->
|
|
34
|
+
-> SessionTurn 持久化 User Message
|
|
35
|
+
-> Executor 捕获只读 Session 快照
|
|
36
|
+
-> SessionComposer.compose()
|
|
20
37
|
-> CoreEngineRunner.run()
|
|
21
|
-
-> SessionTurn 接收
|
|
38
|
+
-> SessionTurn 接收 Stream Chunk
|
|
22
39
|
-> SessionMessages 完成 Assistant Message
|
|
23
40
|
```
|
|
24
41
|
|
|
25
|
-
|
|
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
|
-
->
|
|
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/
|
|
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
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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);
|