@downcity/agent 1.1.276 → 1.1.278
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/agent/Agent.d.ts.map +1 -1
- package/bin/agent/Agent.js +0 -1
- package/bin/agent/Agent.js.map +1 -1
- package/bin/agent/AgentSessions.d.ts +0 -4
- package/bin/agent/AgentSessions.d.ts.map +1 -1
- package/bin/agent/AgentSessions.js +2 -17
- package/bin/agent/AgentSessions.js.map +1 -1
- package/bin/session/Session.d.ts +11 -1
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +61 -24
- package/bin/session/Session.js.map +1 -1
- package/bin/session/messages/SessionAssistantMessageWriter.d.ts +6 -1
- package/bin/session/messages/SessionAssistantMessageWriter.d.ts.map +1 -1
- package/bin/session/messages/SessionAssistantMessageWriter.js +52 -15
- package/bin/session/messages/SessionAssistantMessageWriter.js.map +1 -1
- package/bin/session/messages/SessionToolPartGate.d.ts +23 -0
- package/bin/session/messages/SessionToolPartGate.d.ts.map +1 -0
- package/bin/session/messages/SessionToolPartGate.js +64 -0
- package/bin/session/messages/SessionToolPartGate.js.map +1 -0
- package/bin/session/storage/Instruction.d.ts +14 -0
- package/bin/session/storage/Instruction.d.ts.map +1 -0
- package/bin/session/storage/Instruction.js +44 -0
- package/bin/session/storage/Instruction.js.map +1 -0
- package/bin/session/storage/Paths.d.ts +8 -0
- package/bin/session/storage/Paths.d.ts.map +1 -1
- package/bin/session/storage/Paths.js +10 -0
- package/bin/session/storage/Paths.js.map +1 -1
- package/bin/types/agent/SessionActor.d.ts +2 -0
- package/bin/types/agent/SessionActor.d.ts.map +1 -1
- package/bin/types/session/SessionInstruction.d.ts +22 -0
- package/bin/types/session/SessionInstruction.d.ts.map +1 -0
- package/bin/types/session/SessionInstruction.js +9 -0
- package/bin/types/session/SessionInstruction.js.map +1 -0
- package/bin/types/session/SessionOptions.d.ts +6 -2
- package/bin/types/session/SessionOptions.d.ts.map +1 -1
- package/bin/types/session/SessionQueue.d.ts +2 -18
- package/bin/types/session/SessionQueue.d.ts.map +1 -1
- package/bin/types/session/SessionTool.d.ts +9 -0
- package/bin/types/session/SessionTool.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/session-config-turn-boundary.test.mjs +156 -5
- package/scripts/session-messages.test.mjs +180 -4
- package/scripts/session-tool-part-gate.test.mjs +58 -0
- package/src/agent/Agent.ts +0 -1
- package/src/agent/AgentSessions.ts +2 -21
- package/src/session/Session.ts +68 -26
- package/src/session/messages/SessionAssistantMessageWriter.ts +65 -14
- package/src/session/messages/SessionToolPartGate.ts +67 -0
- package/src/session/storage/Instruction.ts +59 -0
- package/src/session/storage/Paths.ts +18 -0
- package/src/types/agent/SessionActor.ts +3 -0
- package/src/types/session/SessionInstruction.ts +24 -0
- package/src/types/session/SessionOptions.ts +6 -2
- package/src/types/session/SessionQueue.ts +9 -29
- package/src/types/session/SessionTool.ts +10 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionQueue.d.ts","sourceRoot":"","sources":["../../../src/types/session/SessionQueue.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,
|
|
1
|
+
{"version":3,"file":"SessionQueue.d.ts","sourceRoot":"","sources":["../../../src/types/session/SessionQueue.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAChF,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,iCAAiC,CAAC;AACnF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AAE9E,mCAAmC;AACnC,MAAM,WAAW,wBAAwB;IACvC,4BAA4B;IAC5B,IAAI,EAAE,eAAe,CAAC;IACtB,mBAAmB;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,MAAM,EAAE,0BAA0B,CAAC;IACnC,8BAA8B;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8BAA8B;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,yCAAyC;AACzC,MAAM,WAAW,sBAAsB;IACrC,4BAA4B;IAC5B,IAAI,EAAE,WAAW,CAAC;IAClB,mBAAmB;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,2BAA2B;IAC3B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,8CAA8C;AAC9C,MAAM,WAAW,0BAA0B;IACzC,+BAA+B;IAC/B,IAAI,EAAE,eAAe,CAAC;IACtB,mBAAmB;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,+BAA+B;IAC/B,OAAO,EAAE,2BAA2B,CAAC;CACtC;AAED,mBAAmB;AACnB,MAAM,WAAW,0BAA0B;IACzC,uBAAuB;IACvB,IAAI,EAAE,SAAS,CAAC;IAChB,mBAAmB;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,+BAA+B;AAC/B,MAAM,MAAM,mBAAmB,GAC3B,wBAAwB,GACxB,sBAAsB,GACtB,0BAA0B,GAC1B,0BAA0B,CAAC;AAE/B,gDAAgD;AAChD,MAAM,MAAM,mBAAmB,GAAG;IAC9B,mBAAmB;IACnB,IAAI,EAAE,KAAK,CAAC;IACZ,gBAAgB;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B,GACC;IACE,uBAAuB;IACvB,IAAI,EAAE,SAAS,CAAC;IAChB,gBAAgB;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,OAAO,EAAE,2BAA2B,CAAC;CACtC,CAAC;AAEN,wBAAwB;AACxB,MAAM,WAAW,oBAAoB,CAAC,CAAC;IACrC,sBAAsB;IACtB,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,oBAAoB;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;CAC7B;AAED,mCAAmC;AACnC,MAAM,WAAW,mBAAmB;IAClC,uBAAuB;IACvB,IAAI,EAAE,QAAQ,CAAC;IACf,wBAAwB;IACxB,KAAK,EAAE,uBAAuB,CAAC;IAC/B,0CAA0C;IAC1C,eAAe,EAAE,oBAAoB,CAAC,sBAAsB,CAAC,CAAC;CAC/D;AAED,6BAA6B;AAC7B,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,GAAG,mBAAmB,CAAC"}
|
|
@@ -20,4 +20,13 @@ export interface SessionToolInputReady {
|
|
|
20
20
|
/** 已完成解析的 Tool 输入。 */
|
|
21
21
|
input: unknown;
|
|
22
22
|
}
|
|
23
|
+
/** 等待单个 canonical Tool Part 到达的异步句柄。 */
|
|
24
|
+
export interface SessionToolPartWaiter {
|
|
25
|
+
/** Tool Part 成功持久化后完成,异常结束时拒绝。 */
|
|
26
|
+
promise: Promise<void>;
|
|
27
|
+
/** Tool Part 已按 canonical 顺序持久化时释放等待。 */
|
|
28
|
+
resolve: () => void;
|
|
29
|
+
/** 当前 step 或 Gate 异常结束时拒绝等待。 */
|
|
30
|
+
reject: (error: Error) => void;
|
|
31
|
+
}
|
|
23
32
|
//# sourceMappingURL=SessionTool.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionTool.d.ts","sourceRoot":"","sources":["../../../src/types/session/SessionTool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAElF,wCAAwC;AACxC,MAAM,WAAW,4BAA4B;IAC3C,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,yBAAyB;IACzB,IAAI,EAAE,wBAAwB,CAAC;CAChC;AAED,oCAAoC;AACpC,MAAM,WAAW,qBAAqB;IACpC,uBAAuB;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB;IACtB,KAAK,EAAE,OAAO,CAAC;CAChB"}
|
|
1
|
+
{"version":3,"file":"SessionTool.d.ts","sourceRoot":"","sources":["../../../src/types/session/SessionTool.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAElF,wCAAwC;AACxC,MAAM,WAAW,4BAA4B;IAC3C,sCAAsC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,yBAAyB;IACzB,IAAI,EAAE,wBAAwB,CAAC;CAChC;AAED,oCAAoC;AACpC,MAAM,WAAW,qBAAqB;IACpC,uBAAuB;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB;IACtB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,wCAAwC;AACxC,MAAM,WAAW,qBAAqB;IACpC,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,yCAAyC;IACzC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,gCAAgC;IAChC,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAChC"}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
5
|
* - 第一次 provider 请求保持阻塞,用来制造真实的运行中配置修改窗口。
|
|
6
|
-
* - instruction
|
|
6
|
+
* - Agent instruction 修改不能改变已有 Session;env 与 plugin registry 修改在 step 检查点生效。
|
|
7
7
|
* - config 与 steer 在同一个 Session step 检查点提交,并继续使用同一个 turn id。
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -58,7 +58,7 @@ function create_stream_text_result(text) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
test("
|
|
61
|
+
test("Agent instruction changes only affect newly created Sessions", async () => {
|
|
62
62
|
const agent_path = await fs.mkdtemp(
|
|
63
63
|
path.join(os.tmpdir(), "downcity-agent-config-turn-boundary-"),
|
|
64
64
|
);
|
|
@@ -133,8 +133,8 @@ test("running Agent config changes and steer apply at the same Session step chec
|
|
|
133
133
|
assert.equal((await steer_turn.finished).success, true);
|
|
134
134
|
assert.equal(plugin_stop_count, 1);
|
|
135
135
|
assert.equal(provider_prompts.length, 2);
|
|
136
|
-
assert.match(provider_prompts[1], /instruction:
|
|
137
|
-
assert.doesNotMatch(provider_prompts[1], /instruction:
|
|
136
|
+
assert.match(provider_prompts[1], /instruction:old/);
|
|
137
|
+
assert.doesNotMatch(provider_prompts[1], /instruction:new/);
|
|
138
138
|
assert.doesNotMatch(provider_prompts[1], /plugin-env:old/);
|
|
139
139
|
|
|
140
140
|
const messages = await session.messages();
|
|
@@ -142,16 +142,167 @@ test("running Agent config changes and steer apply at the same Session step chec
|
|
|
142
142
|
.filter((message) => message.type === "action" && message.status === "completed")
|
|
143
143
|
.map((message) => message.title);
|
|
144
144
|
assert.deepEqual(completed_actions, [
|
|
145
|
-
"Agent instruction updated",
|
|
146
145
|
"Agent environment updated",
|
|
147
146
|
"Agent plugin runtime-config unregistered",
|
|
148
147
|
]);
|
|
148
|
+
|
|
149
|
+
// 未显式 snapshot 的 Session 重新装载时使用 Agent 当前 instruction。
|
|
150
|
+
await agent.sessions.clear_messages(session.id);
|
|
151
|
+
const restored_session = await agent.sessions.get(session.id);
|
|
152
|
+
const restored_system = await restored_session.system();
|
|
153
|
+
const restored_system_text = restored_system.blocks
|
|
154
|
+
.map((block) => block.content)
|
|
155
|
+
.join("\n");
|
|
156
|
+
assert.match(restored_system_text, /instruction:new/);
|
|
157
|
+
assert.doesNotMatch(restored_system_text, /instruction:old/);
|
|
158
|
+
|
|
159
|
+
const new_session = await agent.sessions.create({
|
|
160
|
+
sessionId: "config_turn_boundary_new_session",
|
|
161
|
+
});
|
|
162
|
+
const new_system = await new_session.system();
|
|
163
|
+
const new_system_text = new_system.blocks
|
|
164
|
+
.map((block) => block.content)
|
|
165
|
+
.join("\n");
|
|
166
|
+
assert.match(new_system_text, /instruction:new/);
|
|
167
|
+
assert.doesNotMatch(new_system_text, /instruction:old/);
|
|
149
168
|
} finally {
|
|
150
169
|
release_first_provider_request.resolve();
|
|
151
170
|
await agent.dispose();
|
|
152
171
|
}
|
|
153
172
|
});
|
|
154
173
|
|
|
174
|
+
test("Session snapshot explicitly persists instruction.md", async () => {
|
|
175
|
+
const agent_path = await fs.mkdtemp(
|
|
176
|
+
path.join(os.tmpdir(), "downcity-agent-instruction-restart-"),
|
|
177
|
+
);
|
|
178
|
+
const model = new MockLanguageModelV3({ modelId: "instruction-restart-model" });
|
|
179
|
+
const first_agent = new Agent({
|
|
180
|
+
id: "instruction_restart_agent",
|
|
181
|
+
path: agent_path,
|
|
182
|
+
model,
|
|
183
|
+
instruction: ["instruction:old"],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const session = await first_agent.sessions.create({
|
|
188
|
+
sessionId: "instruction_restart_session",
|
|
189
|
+
});
|
|
190
|
+
const first_system = await session.system();
|
|
191
|
+
assert.match(
|
|
192
|
+
first_system.blocks.map((block) => block.content).join("\n"),
|
|
193
|
+
/instruction:old/,
|
|
194
|
+
);
|
|
195
|
+
await session.snapshot();
|
|
196
|
+
|
|
197
|
+
const instruction_path = path.join(
|
|
198
|
+
agent_path,
|
|
199
|
+
".downcity",
|
|
200
|
+
"agents",
|
|
201
|
+
"instruction_restart_agent",
|
|
202
|
+
"sessions",
|
|
203
|
+
"instruction_restart_session",
|
|
204
|
+
"instruction.md",
|
|
205
|
+
);
|
|
206
|
+
assert.equal(await fs.readFile(instruction_path, "utf8"), "instruction:old");
|
|
207
|
+
|
|
208
|
+
await fs.writeFile(instruction_path, "instruction:manual", "utf8");
|
|
209
|
+
await session.snapshot();
|
|
210
|
+
assert.equal(await fs.readFile(instruction_path, "utf8"), "instruction:old");
|
|
211
|
+
} finally {
|
|
212
|
+
await first_agent.dispose();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const restarted_agent = new Agent({
|
|
216
|
+
id: "instruction_restart_agent",
|
|
217
|
+
path: agent_path,
|
|
218
|
+
model,
|
|
219
|
+
instruction: ["instruction:new"],
|
|
220
|
+
});
|
|
221
|
+
try {
|
|
222
|
+
const restored_session = await restarted_agent.sessions.get(
|
|
223
|
+
"instruction_restart_session",
|
|
224
|
+
);
|
|
225
|
+
const restored_system = await restored_session.system();
|
|
226
|
+
const restored_system_text = restored_system.blocks
|
|
227
|
+
.map((block) => block.content)
|
|
228
|
+
.join("\n");
|
|
229
|
+
assert.match(restored_system_text, /instruction:old/);
|
|
230
|
+
assert.doesNotMatch(restored_system_text, /instruction:new/);
|
|
231
|
+
} finally {
|
|
232
|
+
await restarted_agent.dispose();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const instruction_path = path.join(
|
|
236
|
+
agent_path,
|
|
237
|
+
".downcity",
|
|
238
|
+
"agents",
|
|
239
|
+
"instruction_restart_agent",
|
|
240
|
+
"sessions",
|
|
241
|
+
"instruction_restart_session",
|
|
242
|
+
"instruction.md",
|
|
243
|
+
);
|
|
244
|
+
await fs.rm(instruction_path);
|
|
245
|
+
|
|
246
|
+
const fallback_agent = new Agent({
|
|
247
|
+
id: "instruction_restart_agent",
|
|
248
|
+
path: agent_path,
|
|
249
|
+
model,
|
|
250
|
+
instruction: ["instruction:new"],
|
|
251
|
+
});
|
|
252
|
+
try {
|
|
253
|
+
const fallback_session = await fallback_agent.sessions.get(
|
|
254
|
+
"instruction_restart_session",
|
|
255
|
+
);
|
|
256
|
+
const fallback_system = await fallback_session.system();
|
|
257
|
+
const fallback_system_text = fallback_system.blocks
|
|
258
|
+
.map((block) => block.content)
|
|
259
|
+
.join("\n");
|
|
260
|
+
assert.match(fallback_system_text, /instruction:new/);
|
|
261
|
+
assert.doesNotMatch(fallback_system_text, /instruction:old/);
|
|
262
|
+
} finally {
|
|
263
|
+
await fallback_agent.dispose();
|
|
264
|
+
await fs.rm(agent_path, { recursive: true, force: true });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("empty Session snapshot suppresses Agent instruction after restart", async () => {
|
|
269
|
+
const agent_path = await fs.mkdtemp(
|
|
270
|
+
path.join(os.tmpdir(), "downcity-agent-empty-instruction-snapshot-"),
|
|
271
|
+
);
|
|
272
|
+
const model = new MockLanguageModelV3({ modelId: "empty-snapshot-model" });
|
|
273
|
+
const first_agent = new Agent({
|
|
274
|
+
id: "empty_snapshot_agent",
|
|
275
|
+
path: agent_path,
|
|
276
|
+
model,
|
|
277
|
+
});
|
|
278
|
+
try {
|
|
279
|
+
const session = await first_agent.sessions.create({
|
|
280
|
+
sessionId: "empty_snapshot_session",
|
|
281
|
+
});
|
|
282
|
+
await session.snapshot();
|
|
283
|
+
} finally {
|
|
284
|
+
await first_agent.dispose();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const restarted_agent = new Agent({
|
|
288
|
+
id: "empty_snapshot_agent",
|
|
289
|
+
path: agent_path,
|
|
290
|
+
model,
|
|
291
|
+
instruction: ["instruction:must-not-appear"],
|
|
292
|
+
});
|
|
293
|
+
try {
|
|
294
|
+
const session = await restarted_agent.sessions.get("empty_snapshot_session");
|
|
295
|
+
const system = await session.system();
|
|
296
|
+
assert.doesNotMatch(
|
|
297
|
+
system.blocks.map((block) => block.content).join("\n"),
|
|
298
|
+
/instruction:must-not-appear/,
|
|
299
|
+
);
|
|
300
|
+
} finally {
|
|
301
|
+
await restarted_agent.dispose();
|
|
302
|
+
await fs.rm(agent_path, { recursive: true, force: true });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
155
306
|
test("running session model changes apply with steer at the next Session step", async () => {
|
|
156
307
|
const agent_path = await fs.mkdtemp(
|
|
157
308
|
path.join(os.tmpdir(), "downcity-session-step-boundary-"),
|
|
@@ -49,6 +49,24 @@ class PausingAssistantMessageStore extends JsonlSessionMessageStore {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** 可让下一次 Assistant 草稿更新失败,用于验证持久化失败边界。 */
|
|
53
|
+
class FailingAssistantMessageStore extends JsonlSessionMessageStore {
|
|
54
|
+
next_assistant_error = null;
|
|
55
|
+
|
|
56
|
+
fail_next_assistant_write(message) {
|
|
57
|
+
this.next_assistant_error = new Error(message);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async write_assistant_message(message) {
|
|
61
|
+
const error = this.next_assistant_error;
|
|
62
|
+
if (error) {
|
|
63
|
+
this.next_assistant_error = null;
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
await super.write_assistant_message(message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
52
70
|
async function create_recorder(
|
|
53
71
|
session_id = "session-recorder-test",
|
|
54
72
|
create_store = (options) => new JsonlSessionMessageStore(options),
|
|
@@ -703,7 +721,7 @@ test("Approval Broker 只接受已经准备完整输入的 Tool", async () => {
|
|
|
703
721
|
);
|
|
704
722
|
assert.equal(approval_broker.list().length, 0);
|
|
705
723
|
|
|
706
|
-
|
|
724
|
+
const tool_input_ready = writer.prepare_tool_input({
|
|
707
725
|
tool_call_id: "call-ready-barrier",
|
|
708
726
|
tool_name: "shell_exec",
|
|
709
727
|
input: {
|
|
@@ -712,6 +730,14 @@ test("Approval Broker 只接受已经准备完整输入的 Tool", async () => {
|
|
|
712
730
|
reason: "Inspect requested desktop files",
|
|
713
731
|
},
|
|
714
732
|
});
|
|
733
|
+
await writer.flush();
|
|
734
|
+
assert.deepEqual(recorder.get_message(writer.message_id).parts, []);
|
|
735
|
+
await writer.apply_chunk({
|
|
736
|
+
type: "tool-input-start",
|
|
737
|
+
toolCallId: "call-ready-barrier",
|
|
738
|
+
toolName: "shell_exec",
|
|
739
|
+
});
|
|
740
|
+
await tool_input_ready;
|
|
715
741
|
const approval_handle = await approval_broker.request(approval_input);
|
|
716
742
|
|
|
717
743
|
const tool = recorder.get_message(writer.message_id).parts[0];
|
|
@@ -734,7 +760,7 @@ test("Approval Broker 只接受已经准备完整输入的 Tool", async () => {
|
|
|
734
760
|
events
|
|
735
761
|
.filter((event) => event.variant === "part" && event.type === "tool")
|
|
736
762
|
.map((event) => event.part.state),
|
|
737
|
-
["ready", "approval-required", "failed"],
|
|
763
|
+
["input-streaming", "ready", "approval-required", "failed"],
|
|
738
764
|
);
|
|
739
765
|
});
|
|
740
766
|
|
|
@@ -749,6 +775,11 @@ test("流式更新与 Approval 共享 Assistant revision 写队列", async () =>
|
|
|
749
775
|
turn_id: "turn-approval-race",
|
|
750
776
|
segment_index: 1,
|
|
751
777
|
});
|
|
778
|
+
await writer.apply_chunk({
|
|
779
|
+
type: "tool-input-start",
|
|
780
|
+
toolCallId: "call-approval-race",
|
|
781
|
+
toolName: "shell_exec",
|
|
782
|
+
});
|
|
752
783
|
await writer.prepare_tool_input({
|
|
753
784
|
tool_call_id: "call-approval-race",
|
|
754
785
|
tool_name: "shell_exec",
|
|
@@ -789,7 +820,7 @@ test("流式更新与 Approval 共享 Assistant revision 写队列", async () =>
|
|
|
789
820
|
const assistant = recorder.get_message(writer.message_id);
|
|
790
821
|
const tool = assistant.parts.find((part) => part.type === "tool");
|
|
791
822
|
const text_part = assistant.parts.find((part) => part.type === "text");
|
|
792
|
-
assert.equal(assistant.revision,
|
|
823
|
+
assert.equal(assistant.revision, 7);
|
|
793
824
|
assert.equal(tool.state, "approval-required");
|
|
794
825
|
assert.equal(tool.approval.approval_id, approval_handle.approval_id);
|
|
795
826
|
assert.equal(tool.input.cmd, "ls -la ~");
|
|
@@ -798,7 +829,7 @@ test("流式更新与 Approval 共享 Assistant revision 写队列", async () =>
|
|
|
798
829
|
events
|
|
799
830
|
.filter((event) => event.message_id === writer.message_id)
|
|
800
831
|
.map((event) => event.revision),
|
|
801
|
-
[1, 2, 3, 4, 5, 6],
|
|
832
|
+
[1, 2, 3, 4, 5, 6, 7],
|
|
802
833
|
);
|
|
803
834
|
assert.equal(
|
|
804
835
|
events.some(
|
|
@@ -1017,6 +1048,151 @@ test("空 Text Start 不会抢占后续 Tool 的真实顺序", async () => {
|
|
|
1017
1048
|
);
|
|
1018
1049
|
});
|
|
1019
1050
|
|
|
1051
|
+
test("Tool 执行先到时等待 stream 固定 canonical Part 顺序", async () => {
|
|
1052
|
+
const { recorder, file_path } = await create_recorder("pending-tool-input-order-test");
|
|
1053
|
+
const writer = await recorder.open_assistant_message({
|
|
1054
|
+
turn_id: "turn-pending-tool-input",
|
|
1055
|
+
segment_index: 1,
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
await writer.begin_step();
|
|
1059
|
+
const tool_input_ready = writer.prepare_tool_input({
|
|
1060
|
+
tool_call_id: "call-pending",
|
|
1061
|
+
tool_name: "search",
|
|
1062
|
+
input: { query: "downcity" },
|
|
1063
|
+
});
|
|
1064
|
+
await writer.flush();
|
|
1065
|
+
assert.deepEqual(recorder.get_message(writer.message_id).parts, []);
|
|
1066
|
+
|
|
1067
|
+
await writer.apply_chunk({ type: "text-start", id: "text-0" });
|
|
1068
|
+
await writer.apply_chunk({ type: "text-delta", id: "text-0", delta: "第一段" });
|
|
1069
|
+
await writer.apply_chunk({ type: "text-end", id: "text-0" });
|
|
1070
|
+
await writer.apply_chunk({ type: "text-start", id: "text-1" });
|
|
1071
|
+
await writer.apply_chunk({ type: "text-delta", id: "text-1", delta: "第二段" });
|
|
1072
|
+
await writer.apply_chunk({ type: "text-end", id: "text-1" });
|
|
1073
|
+
await writer.apply_chunk({
|
|
1074
|
+
type: "tool-input-start",
|
|
1075
|
+
toolCallId: "call-pending",
|
|
1076
|
+
toolName: "search",
|
|
1077
|
+
});
|
|
1078
|
+
await tool_input_ready;
|
|
1079
|
+
await writer.apply_chunk({
|
|
1080
|
+
type: "tool-input-available",
|
|
1081
|
+
toolCallId: "call-pending",
|
|
1082
|
+
toolName: "search",
|
|
1083
|
+
input: { query: "downcity" },
|
|
1084
|
+
});
|
|
1085
|
+
await writer.apply_chunk({
|
|
1086
|
+
type: "tool-output-available",
|
|
1087
|
+
toolCallId: "call-pending",
|
|
1088
|
+
output: "result",
|
|
1089
|
+
});
|
|
1090
|
+
await writer.finish_step([
|
|
1091
|
+
{
|
|
1092
|
+
part_id: "text-final-0",
|
|
1093
|
+
sequence: 1,
|
|
1094
|
+
type: "text",
|
|
1095
|
+
text: "第一段",
|
|
1096
|
+
state: "done",
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
part_id: "text-final-1",
|
|
1100
|
+
sequence: 2,
|
|
1101
|
+
type: "text",
|
|
1102
|
+
text: "第二段",
|
|
1103
|
+
state: "done",
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
part_id: "call-pending",
|
|
1107
|
+
sequence: 3,
|
|
1108
|
+
type: "tool",
|
|
1109
|
+
tool_call_id: "call-pending",
|
|
1110
|
+
tool_name: "search",
|
|
1111
|
+
state: "completed",
|
|
1112
|
+
input: { query: "downcity" },
|
|
1113
|
+
output: "result",
|
|
1114
|
+
},
|
|
1115
|
+
]);
|
|
1116
|
+
await writer.complete();
|
|
1117
|
+
|
|
1118
|
+
const assistant = (await read_jsonl(file_path))[0];
|
|
1119
|
+
assert.deepEqual(assistant.parts.map((part) => part.type), ["text", "text", "tool"]);
|
|
1120
|
+
assert.deepEqual(assistant.parts.map((part) => part.sequence), [1, 2, 3]);
|
|
1121
|
+
assert.deepEqual(assistant.parts[2].input, { query: "downcity" });
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
test("step abort 与 writer close 会拒绝未释放的 Tool 输入屏障", async () => {
|
|
1125
|
+
const { recorder } = await create_recorder("pending-tool-input-cleanup-test");
|
|
1126
|
+
const aborted_writer = await recorder.open_assistant_message({
|
|
1127
|
+
turn_id: "turn-aborted-tool-input",
|
|
1128
|
+
segment_index: 1,
|
|
1129
|
+
});
|
|
1130
|
+
await aborted_writer.begin_step();
|
|
1131
|
+
const aborted_input = assert.rejects(
|
|
1132
|
+
aborted_writer.prepare_tool_input({
|
|
1133
|
+
tool_call_id: "call-aborted",
|
|
1134
|
+
tool_name: "search",
|
|
1135
|
+
input: { query: "aborted" },
|
|
1136
|
+
}),
|
|
1137
|
+
/canonical step was aborted: call-aborted/,
|
|
1138
|
+
);
|
|
1139
|
+
await aborted_writer.flush();
|
|
1140
|
+
await aborted_writer.abort_step();
|
|
1141
|
+
await aborted_input;
|
|
1142
|
+
await aborted_writer.fail("aborted");
|
|
1143
|
+
|
|
1144
|
+
const closed_writer = await recorder.open_assistant_message({
|
|
1145
|
+
turn_id: "turn-closed-tool-input",
|
|
1146
|
+
segment_index: 2,
|
|
1147
|
+
});
|
|
1148
|
+
const closed_input = assert.rejects(
|
|
1149
|
+
closed_writer.prepare_tool_input({
|
|
1150
|
+
tool_call_id: "call-closed",
|
|
1151
|
+
tool_name: "search",
|
|
1152
|
+
input: { query: "closed" },
|
|
1153
|
+
}),
|
|
1154
|
+
/writer closed with status stopped: call-closed/,
|
|
1155
|
+
);
|
|
1156
|
+
await closed_writer.flush();
|
|
1157
|
+
await closed_writer.stop();
|
|
1158
|
+
await closed_input;
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
test("Tool Part 持久化失败时不会释放对应执行等待", async () => {
|
|
1162
|
+
const { recorder, store } = await create_recorder(
|
|
1163
|
+
"tool-part-persistence-gate-test",
|
|
1164
|
+
(options) => new FailingAssistantMessageStore(options),
|
|
1165
|
+
);
|
|
1166
|
+
const writer = await recorder.open_assistant_message({
|
|
1167
|
+
turn_id: "turn-tool-part-persistence",
|
|
1168
|
+
segment_index: 1,
|
|
1169
|
+
});
|
|
1170
|
+
await writer.begin_step();
|
|
1171
|
+
const tool_input = assert.rejects(
|
|
1172
|
+
writer.prepare_tool_input({
|
|
1173
|
+
tool_call_id: "call-persistence-failed",
|
|
1174
|
+
tool_name: "search",
|
|
1175
|
+
input: { query: "downcity" },
|
|
1176
|
+
}),
|
|
1177
|
+
/canonical step was aborted: call-persistence-failed/,
|
|
1178
|
+
);
|
|
1179
|
+
|
|
1180
|
+
store.fail_next_assistant_write("disk full");
|
|
1181
|
+
await assert.rejects(
|
|
1182
|
+
writer.apply_chunk({
|
|
1183
|
+
type: "tool-input-start",
|
|
1184
|
+
toolCallId: "call-persistence-failed",
|
|
1185
|
+
toolName: "search",
|
|
1186
|
+
}),
|
|
1187
|
+
/disk full/,
|
|
1188
|
+
);
|
|
1189
|
+
assert.deepEqual(recorder.get_message(writer.message_id).parts, []);
|
|
1190
|
+
|
|
1191
|
+
await writer.abort_step();
|
|
1192
|
+
await tool_input;
|
|
1193
|
+
await writer.fail("disk full");
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1020
1196
|
test("step 最终快照缺少 canonical Tool chunk 时拒绝猜测顺序", async () => {
|
|
1021
1197
|
const { recorder, events, file_path } = await create_recorder("final-reconcile-order-test");
|
|
1022
1198
|
const writer = await recorder.open_assistant_message({ turn_id: "turn-1", segment_index: 1 });
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 验证 canonical Tool Part Gate 按 Tool Call 独立协调等待与释放。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
|
|
8
|
+
import { SessionToolPartGate } from "../bin/session/messages/SessionToolPartGate.js";
|
|
9
|
+
|
|
10
|
+
test("不同 Tool Call 独立释放,不形成全局执行锁", async () => {
|
|
11
|
+
const gate = new SessionToolPartGate();
|
|
12
|
+
let tool_a_ready = false;
|
|
13
|
+
let tool_b_ready = false;
|
|
14
|
+
const tool_a = gate.wait_until_available("call-a").then(() => {
|
|
15
|
+
tool_a_ready = true;
|
|
16
|
+
});
|
|
17
|
+
const tool_b = gate.wait_until_available("call-b").then(() => {
|
|
18
|
+
tool_b_ready = true;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
gate.mark_available("call-a");
|
|
22
|
+
await tool_a;
|
|
23
|
+
assert.equal(tool_a_ready, true);
|
|
24
|
+
assert.equal(tool_b_ready, false);
|
|
25
|
+
|
|
26
|
+
gate.mark_available("call-b");
|
|
27
|
+
await tool_b;
|
|
28
|
+
assert.equal(tool_b_ready, true);
|
|
29
|
+
await gate.wait_until_available("call-a");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("reject_pending 只清理当前等待,Gate 可用于后续 step", async () => {
|
|
33
|
+
const gate = new SessionToolPartGate();
|
|
34
|
+
const aborted = assert.rejects(
|
|
35
|
+
gate.wait_until_available("call-aborted"),
|
|
36
|
+
/step aborted: call-aborted/,
|
|
37
|
+
);
|
|
38
|
+
gate.reject_pending("step aborted");
|
|
39
|
+
await aborted;
|
|
40
|
+
|
|
41
|
+
const next_step = gate.wait_until_available("call-next");
|
|
42
|
+
gate.mark_available("call-next");
|
|
43
|
+
await next_step;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("close 拒绝当前及后续 Tool Part 等待", async () => {
|
|
47
|
+
const gate = new SessionToolPartGate();
|
|
48
|
+
const pending = assert.rejects(
|
|
49
|
+
gate.wait_until_available("call-pending"),
|
|
50
|
+
/writer closed: call-pending/,
|
|
51
|
+
);
|
|
52
|
+
gate.close("writer closed");
|
|
53
|
+
await pending;
|
|
54
|
+
await assert.rejects(
|
|
55
|
+
gate.wait_until_available("call-future"),
|
|
56
|
+
/writer closed/,
|
|
57
|
+
);
|
|
58
|
+
});
|
package/src/agent/Agent.ts
CHANGED
|
@@ -159,7 +159,6 @@ export class Agent {
|
|
|
159
159
|
setInstruction(input: string | string[]): void {
|
|
160
160
|
const next_instruction = normalizeInstructionInput(input);
|
|
161
161
|
this.instruction.splice(0, this.instruction.length, ...next_instruction);
|
|
162
|
-
this.sessions.broadcast_instruction(this.instruction, generateId());
|
|
163
162
|
}
|
|
164
163
|
|
|
165
164
|
/**
|
|
@@ -146,23 +146,6 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
|
|
|
146
146
|
return this.list_executing_session_ids().length;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
/**
|
|
150
|
-
* 把 Agent instruction 修改广播到已有 Session 的统一输入队列。
|
|
151
|
-
*/
|
|
152
|
-
broadcast_instruction(instruction: string[], command_id: string): void {
|
|
153
|
-
const instruction_blocks = createInstructionSystemBlocks(
|
|
154
|
-
instruction,
|
|
155
|
-
this.project_root,
|
|
156
|
-
);
|
|
157
|
-
for (const session of this.sessions_by_id.values()) {
|
|
158
|
-
session.enqueue_agent_command({
|
|
159
|
-
type: "instruction",
|
|
160
|
-
command_id,
|
|
161
|
-
instruction_blocks,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
149
|
/**
|
|
167
150
|
* 把 Agent env 修改广播到已有 Session 的统一输入队列。
|
|
168
151
|
*/
|
|
@@ -198,9 +181,7 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
|
|
|
198
181
|
* 获取或创建一个 session runtime port。
|
|
199
182
|
*/
|
|
200
183
|
runtime(session_id: string): SessionPort {
|
|
201
|
-
return this.get_or_create_session({
|
|
202
|
-
session_id,
|
|
203
|
-
}).getRuntimePort();
|
|
184
|
+
return this.get_or_create_session({ session_id }).getRuntimePort();
|
|
204
185
|
}
|
|
205
186
|
|
|
206
187
|
/**
|
|
@@ -436,7 +417,7 @@ export class AgentSessions implements AgentSessionsContract<AgentSession> {
|
|
|
436
417
|
sessionId: resolved_session_id,
|
|
437
418
|
tools: this.tools,
|
|
438
419
|
logger: this.logger,
|
|
439
|
-
|
|
420
|
+
instruction_system_blocks: this.load_instruction_system_blocks(),
|
|
440
421
|
getAgentEnv: () => this.get_agent_env(),
|
|
441
422
|
getAgentModel: () => this.get_agent_model(),
|
|
442
423
|
get_agent_plugins: () => this.get_agent_plugins(),
|