@downcity/agent 1.1.189 → 1.1.196

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 (112) hide show
  1. package/bin/agent/local/Agent.d.ts.map +1 -1
  2. package/bin/agent/local/Agent.js +5 -2
  3. package/bin/agent/local/Agent.js.map +1 -1
  4. package/bin/agent/local/services/AgentAssemblyService.d.ts.map +1 -1
  5. package/bin/agent/local/services/AgentAssemblyService.js +2 -2
  6. package/bin/agent/local/services/AgentAssemblyService.js.map +1 -1
  7. package/bin/agent/local/services/AgentSessionManager.d.ts +5 -0
  8. package/bin/agent/local/services/AgentSessionManager.d.ts.map +1 -1
  9. package/bin/agent/local/services/AgentSessionManager.js +3 -0
  10. package/bin/agent/local/services/AgentSessionManager.js.map +1 -1
  11. package/bin/executor/Executor.d.ts +4 -0
  12. package/bin/executor/Executor.d.ts.map +1 -1
  13. package/bin/executor/Executor.js +40 -1
  14. package/bin/executor/Executor.js.map +1 -1
  15. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +9 -0
  16. package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
  17. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts.map +1 -1
  18. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +1 -0
  19. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
  20. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts +2 -0
  21. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
  22. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +49 -5
  23. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
  24. package/bin/executor/composer/history/jsonl/JsonlSessionHistoryComposer.d.ts +1 -1
  25. package/bin/executor/composer/history/jsonl/JsonlSessionHistoryComposer.d.ts.map +1 -1
  26. package/bin/executor/composer/history/jsonl/JsonlSessionHistoryComposer.js +2 -0
  27. package/bin/executor/composer/history/jsonl/JsonlSessionHistoryComposer.js.map +1 -1
  28. package/bin/executor/messages/SessionMessageCodec.d.ts +1 -1
  29. package/bin/executor/messages/SessionMessageCodec.d.ts.map +1 -1
  30. package/bin/executor/messages/SessionMessageCodec.js +6 -1
  31. package/bin/executor/messages/SessionMessageCodec.js.map +1 -1
  32. package/bin/executor/store/history/SessionHistoryStore.d.ts +22 -0
  33. package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
  34. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts +7 -0
  35. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
  36. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +53 -0
  37. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
  38. package/bin/executor/tools/plugin/PluginToolBridge.d.ts +4 -8
  39. package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
  40. package/bin/executor/tools/plugin/PluginToolBridge.js +6 -21
  41. package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
  42. package/bin/executor/tools/plugin/PluginToolDefinition.d.ts +7 -19
  43. package/bin/executor/tools/plugin/PluginToolDefinition.d.ts.map +1 -1
  44. package/bin/executor/tools/plugin/PluginToolDefinition.js +29 -18
  45. package/bin/executor/tools/plugin/PluginToolDefinition.js.map +1 -1
  46. package/bin/executor/types/SessionMessages.d.ts +15 -2
  47. package/bin/executor/types/SessionMessages.d.ts.map +1 -1
  48. package/bin/executor/types/SessionMessages.js +9 -1
  49. package/bin/executor/types/SessionMessages.js.map +1 -1
  50. package/bin/index.d.ts +1 -0
  51. package/bin/index.d.ts.map +1 -1
  52. package/bin/index.js.map +1 -1
  53. package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
  54. package/bin/plugin/core/PluginRegistry.js +0 -4
  55. package/bin/plugin/core/PluginRegistry.js.map +1 -1
  56. package/bin/session/browse/Browse.d.ts.map +1 -1
  57. package/bin/session/browse/Browse.js +22 -0
  58. package/bin/session/browse/Browse.js.map +1 -1
  59. package/bin/session/services/SessionStateService.d.ts +21 -1
  60. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  61. package/bin/session/services/SessionStateService.js +101 -11
  62. package/bin/session/services/SessionStateService.js.map +1 -1
  63. package/bin/session/services/SessionTurnService.d.ts.map +1 -1
  64. package/bin/session/services/SessionTurnService.js +9 -0
  65. package/bin/session/services/SessionTurnService.js.map +1 -1
  66. package/bin/session/services/SessionViewService.d.ts.map +1 -1
  67. package/bin/session/services/SessionViewService.js +42 -8
  68. package/bin/session/services/SessionViewService.js.map +1 -1
  69. package/bin/types/agent/SessionTypes.d.ts +22 -1
  70. package/bin/types/agent/SessionTypes.d.ts.map +1 -1
  71. package/bin/types/executor/SessionRunContext.d.ts +10 -0
  72. package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
  73. package/bin/types/plugin/PluginToolRuntime.d.ts +75 -0
  74. package/bin/types/plugin/PluginToolRuntime.d.ts.map +1 -0
  75. package/bin/types/plugin/PluginToolRuntime.js +9 -0
  76. package/bin/types/plugin/PluginToolRuntime.js.map +1 -0
  77. package/bin/types/sdk/AgentSessionEvent.d.ts +2 -1
  78. package/bin/types/sdk/AgentSessionEvent.d.ts.map +1 -1
  79. package/bin/types/sdk/AgentSessionOperation.d.ts +99 -0
  80. package/bin/types/sdk/AgentSessionOperation.d.ts.map +1 -0
  81. package/bin/types/sdk/AgentSessionOperation.js +10 -0
  82. package/bin/types/sdk/AgentSessionOperation.js.map +1 -0
  83. package/package.json +3 -3
  84. package/scripts/agent-ready.test.mjs +168 -0
  85. package/scripts/plugin-tool-bridge.test.mjs +114 -10
  86. package/scripts/session-operation-message.test.mjs +182 -0
  87. package/src/agent/local/Agent.ts +5 -2
  88. package/src/agent/local/services/AgentAssemblyService.ts +2 -3
  89. package/src/agent/local/services/AgentSessionManager.ts +8 -0
  90. package/src/executor/Executor.ts +49 -1
  91. package/src/executor/composer/compaction/SessionCompactionComposer.ts +10 -0
  92. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +1 -0
  93. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +64 -5
  94. package/src/executor/composer/history/jsonl/JsonlSessionHistoryComposer.ts +5 -1
  95. package/src/executor/messages/SessionMessageCodec.ts +11 -2
  96. package/src/executor/store/history/SessionHistoryStore.ts +27 -0
  97. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +66 -0
  98. package/src/executor/tools/plugin/PluginToolBridge.ts +10 -26
  99. package/src/executor/tools/plugin/PluginToolDefinition.ts +39 -22
  100. package/src/executor/types/SessionMessages.ts +26 -2
  101. package/src/index.ts +7 -0
  102. package/src/plugin/core/PluginRegistry.ts +0 -4
  103. package/src/session/browse/Browse.ts +24 -0
  104. package/src/session/services/SessionStateService.ts +125 -11
  105. package/src/session/services/SessionTurnService.ts +8 -0
  106. package/src/session/services/SessionViewService.ts +47 -10
  107. package/src/types/agent/SessionTypes.ts +22 -1
  108. package/src/types/executor/SessionRunContext.ts +20 -0
  109. package/src/types/plugin/PluginToolRuntime.ts +86 -0
  110. package/src/types/sdk/AgentSessionEvent.ts +2 -0
  111. package/src/types/sdk/AgentSessionOperation.ts +125 -0
  112. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Agent Session operation 类型定义。
3
+ *
4
+ * 关键点(中文)
5
+ * - operation 描述 session 内部正在发生的辅助操作,不代表 assistant 正文。
6
+ * - operation 可以同时作为订阅事件发布,也可以作为 UIMessage 持久化到 JSONL。
7
+ * - operation message 只用于前端时间线与调试,不应进入 LLM 输入。
8
+ */
9
+ import type { JsonValue } from "../../types/common/Json.js";
10
+ /**
11
+ * Session operation 当前状态。
12
+ */
13
+ export type AgentSessionOperationStatus = "started" | "progress" | "finished" | "skipped" | "failed";
14
+ /**
15
+ * Session operation 名称。
16
+ *
17
+ * 说明(中文)
18
+ * - 内置名称用于 SDK 与前端约定常见辅助操作。
19
+ * - 字符串扩展允许 plugin 或宿主运行时声明自己的 operation。
20
+ */
21
+ export type AgentSessionOperationName = "compacting" | "model-switching" | "history-forking" | (string & {});
22
+ /**
23
+ * Session operation 记录。
24
+ */
25
+ export interface AgentSessionOperationRecord {
26
+ /**
27
+ * 同一个 operation 生命周期内稳定复用的 ID。
28
+ */
29
+ operationId: string;
30
+ /**
31
+ * 当前 operation 名称。
32
+ */
33
+ name: AgentSessionOperationName;
34
+ /**
35
+ * 当前 operation 状态。
36
+ */
37
+ status: AgentSessionOperationStatus;
38
+ /**
39
+ * 当前 operation 关联的 turn 标识。
40
+ *
41
+ * 说明(中文)
42
+ * - turn 内触发的 operation 应写入该字段。
43
+ * - session 级 operation 可以省略。
44
+ */
45
+ turnId?: string;
46
+ /**
47
+ * 面向用户展示的短文本。
48
+ */
49
+ label?: string;
50
+ /**
51
+ * 机器可读原因。
52
+ *
53
+ * 示例(中文)
54
+ * - `under_budget`:未达到压缩阈值。
55
+ * - `summary_fallback`:摘要生成失败后使用降级摘要。
56
+ */
57
+ reason?: string;
58
+ /**
59
+ * 当前 operation 进度,范围 0 到 1。
60
+ *
61
+ * 说明(中文)
62
+ * - 无法估算进度时省略。
63
+ */
64
+ progress?: number;
65
+ /**
66
+ * operation 完成后产生的轻量结果。
67
+ */
68
+ result?: JsonValue;
69
+ /**
70
+ * operation 失败时的错误文本。
71
+ */
72
+ error?: string;
73
+ /**
74
+ * 是否建议前端展示。
75
+ *
76
+ * 说明(中文)
77
+ * - 默认视为 `true`。
78
+ * - 调试或审计类辅助事件可显式写入 `false`。
79
+ */
80
+ visible?: boolean;
81
+ }
82
+ /**
83
+ * Session operation 订阅事件。
84
+ */
85
+ export interface AgentSessionOperationEvent extends AgentSessionOperationRecord {
86
+ /**
87
+ * 当前事件类型。
88
+ */
89
+ type: "operation";
90
+ /**
91
+ * 当前 session 唯一标识。
92
+ */
93
+ sessionId: string;
94
+ }
95
+ /**
96
+ * Session operation 发布回调。
97
+ */
98
+ export type AgentSessionOperationCallback = (operation: AgentSessionOperationEvent) => Promise<void>;
99
+ //# sourceMappingURL=AgentSessionOperation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AgentSessionOperation.d.ts","sourceRoot":"","sources":["../../../src/types/sdk/AgentSessionOperation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAExD;;GAEG;AACH,MAAM,MAAM,2BAA2B,GACnC,SAAS,GACT,UAAU,GACV,UAAU,GACV,SAAS,GACT,QAAQ,CAAC;AAEb;;;;;;GAMG;AACH,MAAM,MAAM,yBAAyB,GACjC,YAAY,GACZ,iBAAiB,GACjB,iBAAiB,GACjB,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,IAAI,EAAE,yBAAyB,CAAC;IAEhC;;OAEG;IACH,MAAM,EAAE,2BAA2B,CAAC;IAEpC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,0BAA2B,SAAQ,2BAA2B;IAC7E;;OAEG;IACH,IAAI,EAAE,WAAW,CAAC;IAElB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,MAAM,6BAA6B,GAAG,CAC1C,SAAS,EAAE,0BAA0B,KAClC,OAAO,CAAC,IAAI,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Agent Session operation 类型定义。
3
+ *
4
+ * 关键点(中文)
5
+ * - operation 描述 session 内部正在发生的辅助操作,不代表 assistant 正文。
6
+ * - operation 可以同时作为订阅事件发布,也可以作为 UIMessage 持久化到 JSONL。
7
+ * - operation message 只用于前端时间线与调试,不应进入 LLM 输入。
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=AgentSessionOperation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AgentSessionOperation.js","sourceRoot":"","sources":["../../../src/types/sdk/AgentSessionOperation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@downcity/agent",
3
- "version": "1.1.189",
3
+ "version": "1.1.196",
4
4
  "type": "module",
5
5
  "description": "Downcity Agent 运行时 — 单 Agent 执行壳与本机 RPC 能力",
6
6
  "main": "./bin/index.js",
@@ -17,8 +17,8 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@ai-sdk/openai-compatible": "^2.0.48",
20
- "@downcity/shell": "^0.1.61",
21
- "@downcity/type": "0.1.94",
20
+ "@downcity/shell": "^0.1.66",
21
+ "@downcity/type": "0.1.99",
22
22
  "ai": "^6.0.193",
23
23
  "commander": "^15.0.0",
24
24
  "dotenv": "^17.4.2",
@@ -0,0 +1,168 @@
1
+ /**
2
+ * @file 验证 session.prompt 会等待当前 Agent 后台能力 ready。
3
+ *
4
+ * 关键点(中文)
5
+ * - 这里走编译后的公开 SDK,覆盖宿主真实入口。
6
+ * - plugin lifecycle 未完成前,session.prompt 不应进入模型执行。
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import fs from "node:fs/promises";
14
+
15
+ import { MockLanguageModelV3 } from "ai/test";
16
+ import { Agent } from "../bin/index.js";
17
+ import { createPlugin } from "../bin/plugin/core/PluginActionFactory.js";
18
+
19
+ function create_deferred() {
20
+ let resolve;
21
+ const promise = new Promise((inner_resolve) => {
22
+ resolve = inner_resolve;
23
+ });
24
+ return {
25
+ promise,
26
+ resolve,
27
+ };
28
+ }
29
+
30
+ async function is_settled(promise) {
31
+ const marker = {};
32
+ const result = await Promise.race([
33
+ promise.then(
34
+ () => true,
35
+ () => true,
36
+ ),
37
+ new Promise((resolve) => setTimeout(() => resolve(marker), 0)),
38
+ ]);
39
+ return result !== marker;
40
+ }
41
+
42
+ function create_stream_text_result(text) {
43
+ return {
44
+ stream: new ReadableStream({
45
+ start(controller) {
46
+ controller.enqueue({
47
+ type: "stream-start",
48
+ warnings: [],
49
+ });
50
+ controller.enqueue({
51
+ type: "text-start",
52
+ id: "text_1",
53
+ });
54
+ controller.enqueue({
55
+ type: "text-delta",
56
+ id: "text_1",
57
+ delta: text,
58
+ });
59
+ controller.enqueue({
60
+ type: "text-end",
61
+ id: "text_1",
62
+ });
63
+ controller.enqueue({
64
+ type: "finish",
65
+ finishReason: {
66
+ unified: "stop",
67
+ raw: "stop",
68
+ },
69
+ usage: {
70
+ inputTokens: {
71
+ total: 0,
72
+ noCache: 0,
73
+ cacheRead: 0,
74
+ cacheWrite: 0,
75
+ },
76
+ outputTokens: {
77
+ total: 0,
78
+ text: 0,
79
+ reasoning: 0,
80
+ },
81
+ },
82
+ });
83
+ controller.close();
84
+ },
85
+ }),
86
+ };
87
+ }
88
+
89
+ test("session.prompt waits for agent background ready before model execution", async () => {
90
+ const agent_path = await fs.mkdtemp(
91
+ path.join(os.tmpdir(), "downcity-agent-ready-"),
92
+ );
93
+ const lifecycle_ready = create_deferred();
94
+ let model_stream_calls = 0;
95
+
96
+ const blocking_plugin = createPlugin({
97
+ name: "blocking",
98
+ title: "Blocking",
99
+ description: "Blocks lifecycle start until the test releases it",
100
+ lifecycle: {
101
+ start: async () => {
102
+ await lifecycle_ready.promise;
103
+ },
104
+ },
105
+ });
106
+ const model = new MockLanguageModelV3({
107
+ modelId: "agent-ready-model",
108
+ doStream: async () => {
109
+ model_stream_calls += 1;
110
+ return create_stream_text_result("ready");
111
+ },
112
+ doGenerate: async () => ({
113
+ content: [
114
+ {
115
+ type: "text",
116
+ text: "Ready title",
117
+ },
118
+ ],
119
+ finishReason: {
120
+ unified: "stop",
121
+ raw: "stop",
122
+ },
123
+ usage: {
124
+ inputTokens: {
125
+ total: 0,
126
+ noCache: 0,
127
+ cacheRead: 0,
128
+ cacheWrite: 0,
129
+ },
130
+ outputTokens: {
131
+ total: 0,
132
+ text: 0,
133
+ reasoning: 0,
134
+ },
135
+ },
136
+ warnings: [],
137
+ }),
138
+ });
139
+ const agent = new Agent({
140
+ id: "ready_agent",
141
+ path: agent_path,
142
+ plugins: [blocking_plugin],
143
+ model,
144
+ });
145
+
146
+ try {
147
+ const session = await agent.session_collection().create_session({
148
+ sessionId: "ready_session",
149
+ });
150
+ const prompt_promise = session.prompt({
151
+ query: "hello",
152
+ });
153
+
154
+ assert.equal(await is_settled(prompt_promise), false);
155
+ assert.equal(model_stream_calls, 0);
156
+
157
+ lifecycle_ready.resolve();
158
+ const turn = await prompt_promise;
159
+ const result = await turn.finished;
160
+
161
+ assert.equal(result.success, true);
162
+ assert.equal(model_stream_calls, 1);
163
+ } finally {
164
+ lifecycle_ready.resolve();
165
+ await agent.dispose();
166
+ }
167
+ });
168
+
@@ -15,8 +15,8 @@ import fs from "node:fs/promises";
15
15
  import {
16
16
  invokePluginCallTool,
17
17
  invokePluginReadTool,
18
- setPluginToolRuntime,
19
18
  } from "../bin/executor/tools/plugin/PluginToolBridge.js";
19
+ import { createPluginTools } from "../bin/executor/tools/plugin/PluginToolDefinition.js";
20
20
  import { withSessionRunScope } from "../bin/executor/SessionRunScope.js";
21
21
  import { createAction, createPlugin } from "../bin/plugin/core/PluginActionFactory.js";
22
22
  import { createAgentPluginRegistry } from "../bin/agent/local/AgentPluginFactory.js";
@@ -38,7 +38,7 @@ test("invokePluginCallTool returns absolute paths for materialized file parts",
38
38
  );
39
39
  const bytes = Buffer.from("png-bytes-for-plugin-tool", "utf8");
40
40
 
41
- setPluginToolRuntime({
41
+ const plugins = {
42
42
  list: () => [],
43
43
  read: () => ({ plugins: [] }),
44
44
  availability: async () => ({ enabled: true, available: true, reasons: [] }),
@@ -64,14 +64,17 @@ test("invokePluginCallTool returns absolute paths for materialized file parts",
64
64
  resolve: async () => {
65
65
  throw new Error("not implemented");
66
66
  },
67
- });
67
+ };
68
68
 
69
69
  const run_context = create_run_context(project_root);
70
70
  const result = await withSessionRunScope({ runContext: run_context }, () =>
71
71
  invokePluginCallTool({
72
- plugin: "image",
73
- action: "image_result",
74
- payload: { job_id: "img_1" },
72
+ plugins,
73
+ input: {
74
+ plugin: "image",
75
+ action: "image_result",
76
+ payload: { job_id: "img_1" },
77
+ },
75
78
  }),
76
79
  );
77
80
 
@@ -93,7 +96,7 @@ test("invokePluginCallTool returns absolute paths for materialized file parts",
93
96
  });
94
97
 
95
98
  test("invokePluginReadTool returns plugin action metadata", async () => {
96
- setPluginToolRuntime({
99
+ const plugins = {
97
100
  list: () => [],
98
101
  read: () => ({
99
102
  name: "image",
@@ -130,11 +133,14 @@ test("invokePluginReadTool returns plugin action metadata", async () => {
130
133
  resolve: async () => {
131
134
  throw new Error("not implemented");
132
135
  },
133
- });
136
+ };
134
137
 
135
138
  const result = await invokePluginReadTool({
136
- plugin: "image",
137
- action: "image_create",
139
+ plugins,
140
+ input: {
141
+ plugin: "image",
142
+ action: "image_create",
143
+ },
138
144
  });
139
145
 
140
146
  assert.equal(result.success, true);
@@ -174,6 +180,7 @@ test("PluginRegistry validates action payload with metadata schema", async () =>
174
180
  });
175
181
  const registry = createAgentPluginRegistry({
176
182
  plugins: [plugin],
183
+ plugin_instances: new Map(),
177
184
  get_context: () => ({ rootPath: process.cwd() }),
178
185
  });
179
186
 
@@ -197,3 +204,100 @@ test("PluginRegistry validates action payload with metadata schema", async () =>
197
204
  assert.equal(valid.success, true);
198
205
  assert.equal(valid.data.text, "hello");
199
206
  });
207
+
208
+ test("createPluginTools binds plugin_call to the current registry", async () => {
209
+ function create_owner_registry(owner) {
210
+ const plugin = createPlugin({
211
+ name: "skill",
212
+ title: `Skill ${owner}`,
213
+ description: "Owner scoped skill plugin",
214
+ actions: {
215
+ lookup: createAction({
216
+ description: "Return registry owner",
217
+ execute: async () => ({
218
+ success: true,
219
+ data: { owner },
220
+ message: owner,
221
+ }),
222
+ }),
223
+ },
224
+ });
225
+ return createAgentPluginRegistry({
226
+ plugins: [plugin],
227
+ plugin_instances: new Map(),
228
+ get_context: () => ({ rootPath: process.cwd() }),
229
+ });
230
+ }
231
+
232
+ const registry_a = create_owner_registry("agent_a");
233
+ const registry_b = create_owner_registry("agent_b");
234
+ const tools_a = createPluginTools({ plugins: registry_a });
235
+ const tools_b = createPluginTools({ plugins: registry_b });
236
+
237
+ const result_a = await tools_a.plugin_call.execute({
238
+ plugin: "skill",
239
+ action: "lookup",
240
+ payload: { name: "anything" },
241
+ });
242
+ const result_b = await tools_b.plugin_call.execute({
243
+ plugin: "skill",
244
+ action: "lookup",
245
+ payload: { name: "anything" },
246
+ });
247
+
248
+ assert.equal(result_a.success, true);
249
+ assert.equal(result_a.data.value.owner, "agent_a");
250
+ assert.equal(result_b.success, true);
251
+ assert.equal(result_b.data.value.owner, "agent_b");
252
+ });
253
+
254
+ test("PluginRegistry keeps plugin ready after action business failure", async () => {
255
+ let call_count = 0;
256
+ const plugin = createPlugin({
257
+ name: "skill",
258
+ title: "Skill",
259
+ description: "Retryable skill plugin",
260
+ actions: {
261
+ lookup: createAction({
262
+ description: "Fail once then succeed",
263
+ execute: async () => {
264
+ call_count += 1;
265
+ if (call_count === 1) {
266
+ return {
267
+ success: false,
268
+ error: "Skill not found: missing",
269
+ message: "Skill not found: missing",
270
+ };
271
+ }
272
+ return {
273
+ success: true,
274
+ data: { loaded: true },
275
+ message: "loaded",
276
+ };
277
+ },
278
+ }),
279
+ },
280
+ });
281
+ const registry = createAgentPluginRegistry({
282
+ plugins: [plugin],
283
+ plugin_instances: new Map(),
284
+ get_context: () => ({ rootPath: process.cwd() }),
285
+ });
286
+
287
+ const failed = await registry.runAction({
288
+ plugin: "skill",
289
+ action: "lookup",
290
+ payload: { name: "missing" },
291
+ });
292
+ assert.equal(failed.success, false);
293
+ assert.equal(registry.status("skill").state, "ready");
294
+
295
+ const retry = await registry.runAction({
296
+ plugin: "skill",
297
+ action: "lookup",
298
+ payload: { name: "exists" },
299
+ });
300
+ assert.equal(retry.success, true);
301
+ assert.equal(retry.data.loaded, true);
302
+ assert.equal(registry.status("skill").state, "ready");
303
+ });
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @file 验证 operation message 的持久化、模型输入过滤与 timeline 投影。
3
+ *
4
+ * 关键点(中文)
5
+ * - operation message 会作为 UIMessage 写入 JSONL。
6
+ * - history composer 组装 LLM 输入时必须过滤 operation message。
7
+ * - timeline view 可以把 operation message 投影成前端可识别的 operation 事件。
8
+ */
9
+
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+ import fs from "node:fs/promises";
15
+
16
+ import { JsonlSessionHistoryStore } from "../bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js";
17
+ import { JsonlSessionHistoryComposer } from "../bin/executor/composer/history/jsonl/JsonlSessionHistoryComposer.js";
18
+ import { toSessionTimelineEvents } from "../bin/session/browse/Browse.js";
19
+ import { Agent } from "../bin/index.js";
20
+
21
+ test("operation message is persisted but filtered from LLM history", async () => {
22
+ const root_path = await fs.mkdtemp(
23
+ path.join(os.tmpdir(), "downcity-agent-operation-message-"),
24
+ );
25
+ const session_id = "operation_session";
26
+ const store = new JsonlSessionHistoryStore({
27
+ rootPath: root_path,
28
+ agentId: "operation_agent",
29
+ sessionId: session_id,
30
+ });
31
+ const composer = new JsonlSessionHistoryComposer({ store });
32
+
33
+ await store.append(
34
+ store.userText({
35
+ text: "hello",
36
+ metadata: { sessionId: session_id },
37
+ id: "u:1",
38
+ }),
39
+ );
40
+ await store.append(
41
+ store.operation({
42
+ operation: {
43
+ operationId: "op-1",
44
+ name: "compacting",
45
+ status: "started",
46
+ label: "Compacting session history",
47
+ turnId: "turn-1",
48
+ },
49
+ metadata: { sessionId: session_id },
50
+ id: "op:1",
51
+ }),
52
+ );
53
+ await store.append(
54
+ store.assistantText({
55
+ text: "world",
56
+ metadata: { sessionId: session_id },
57
+ id: "a:1",
58
+ }),
59
+ );
60
+
61
+ const persisted_messages = await store.list();
62
+ assert.equal(persisted_messages.length, 3);
63
+ assert.equal(persisted_messages[1].metadata.kind, "operation");
64
+ assert.equal(persisted_messages[1].metadata.operation.operationId, "op-1");
65
+
66
+ const model_messages = await composer.prepare({
67
+ query: "",
68
+ tools: {},
69
+ system: [],
70
+ model: {},
71
+ retryCount: 0,
72
+ });
73
+ assert.deepEqual(
74
+ model_messages.map((message) => message.id),
75
+ ["u:1", "a:1"],
76
+ );
77
+
78
+ const timeline_events = toSessionTimelineEvents(persisted_messages[1]);
79
+ assert.equal(timeline_events.length, 1);
80
+ assert.equal(timeline_events[0].role, "operation");
81
+ assert.equal(timeline_events[0].operationId, "op-1");
82
+ assert.equal(timeline_events[0].operationName, "compacting");
83
+ assert.equal(timeline_events[0].operationStatus, "started");
84
+ assert.equal(timeline_events[0].text, "Compacting session history");
85
+ });
86
+
87
+ test("session.set persists and publishes model-switching operations", async () => {
88
+ const agent_path = await fs.mkdtemp(
89
+ path.join(os.tmpdir(), "downcity-agent-model-switching-"),
90
+ );
91
+ const agent = new Agent({
92
+ id: "model_switching_agent",
93
+ path: agent_path,
94
+ });
95
+ try {
96
+ const session = await agent.session_collection().create_session({
97
+ sessionId: "model_switching_session",
98
+ });
99
+ const events = [];
100
+ const unsubscribe = session.subscribe((event) => {
101
+ if (event.type === "operation") events.push(event);
102
+ });
103
+
104
+ await session.set({
105
+ model: {
106
+ modelId: "test-model",
107
+ provider: "test",
108
+ },
109
+ });
110
+ unsubscribe();
111
+
112
+ assert.deepEqual(
113
+ events.map((event) => `${event.name}:${event.status}`),
114
+ ["model-switching:started", "model-switching:finished"],
115
+ );
116
+
117
+ const history = await session.history({ view: "timeline" });
118
+ const operation_events = history.items.filter(
119
+ (item) => item.role === "operation",
120
+ );
121
+ assert.deepEqual(
122
+ operation_events.map((event) => `${event.operationName}:${event.operationStatus}`),
123
+ ["model-switching:started", "model-switching:finished"],
124
+ );
125
+ } finally {
126
+ await agent.dispose();
127
+ }
128
+ });
129
+
130
+ test("session.fork persists and publishes history-forking operations on source session", async () => {
131
+ const agent_path = await fs.mkdtemp(
132
+ path.join(os.tmpdir(), "downcity-agent-history-forking-"),
133
+ );
134
+ const agent = new Agent({
135
+ id: "history_forking_agent",
136
+ path: agent_path,
137
+ });
138
+ try {
139
+ const session = await agent.session_collection().create_session({
140
+ sessionId: "history_forking_session",
141
+ });
142
+ await session.set({
143
+ model: {
144
+ modelId: "fork-test-model",
145
+ provider: "test",
146
+ },
147
+ });
148
+ await session.appendUserMessage({ text: "source message" });
149
+
150
+ const events = [];
151
+ const unsubscribe = session.subscribe((event) => {
152
+ if (event.type === "operation" && event.name === "history-forking") {
153
+ events.push(event);
154
+ }
155
+ });
156
+ const forked = await session.fork();
157
+ unsubscribe();
158
+
159
+ assert.deepEqual(
160
+ events.map((event) => `${event.name}:${event.status}`),
161
+ ["history-forking:started", "history-forking:finished"],
162
+ );
163
+ assert.notEqual(forked.id, session.id);
164
+
165
+ const source_history = await session.history({ view: "timeline" });
166
+ const source_fork_operations = source_history.items.filter(
167
+ (item) => item.operationName === "history-forking",
168
+ );
169
+ assert.deepEqual(
170
+ source_fork_operations.map((event) => event.operationStatus),
171
+ ["started", "finished"],
172
+ );
173
+
174
+ const forked_history = await forked.history({ view: "timeline" });
175
+ const forked_model_operations = forked_history.items.filter(
176
+ (item) => item.operationName === "model-switching",
177
+ );
178
+ assert.equal(forked_model_operations.length, 2);
179
+ } finally {
180
+ await agent.dispose();
181
+ }
182
+ });
@@ -90,14 +90,14 @@ export class Agent {
90
90
  this.instruction = assembly.instruction;
91
91
  this.shell = assembly.shell;
92
92
 
93
- this.sessionManager = this.create_session_manager(assembly);
94
- session_manager_ref = this.sessionManager;
95
93
  // 关键点(中文):构造完成即触发后台能力启动;调用方可 `await agent.ready()` 等待。
96
94
  this.backgroundService = new AgentBackgroundService({
97
95
  logger: this.logger,
98
96
  agent_context: this.agentContext,
99
97
  get_shell: () => this.shell,
100
98
  });
99
+ this.sessionManager = this.create_session_manager(assembly);
100
+ session_manager_ref = this.sessionManager;
101
101
  }
102
102
 
103
103
  /**
@@ -269,6 +269,9 @@ export class Agent {
269
269
  get_agent_context: () => this.agentContext ?? assembly.agent_context,
270
270
  get_instruction: () => this.instruction,
271
271
  plugin_instances: assembly.plugin_instances,
272
+ ensure_agent_ready: async () => {
273
+ await this.backgroundService.ready();
274
+ },
272
275
  default_model: this.defaultModel,
273
276
  SessionClass: this.SessionClass,
274
277
  });