@downcity/agent 1.1.232 → 1.1.236
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/local/Agent.d.ts +1 -1
- package/bin/agent/local/Agent.js +2 -2
- package/bin/agent/local/Agent.js.map +1 -1
- package/bin/agent/local/services/AgentSessions.d.ts +4 -4
- package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
- package/bin/agent/local/services/AgentSessions.js +8 -8
- package/bin/agent/local/services/AgentSessions.js.map +1 -1
- package/bin/agent/remote/RemoteSession.d.ts +2 -0
- package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
- package/bin/agent/remote/RemoteSession.js +4 -0
- package/bin/agent/remote/RemoteSession.js.map +1 -1
- package/bin/agent/remote/RemoteTransport.d.ts +2 -0
- package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +1 -0
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +11 -0
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +1 -0
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +3 -0
- package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
- package/bin/executor/Executor.d.ts +20 -2
- package/bin/executor/Executor.d.ts.map +1 -1
- package/bin/executor/Executor.js +102 -18
- package/bin/executor/Executor.js.map +1 -1
- package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts +6 -0
- package/bin/executor/composer/compaction/SessionCompactionComposer.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts +0 -9
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js +4 -38
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.js.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts +1 -3
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +7 -59
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
- package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts +38 -0
- package/bin/executor/core-engine/CoreEngineContextCompaction.d.ts.map +1 -0
- package/bin/executor/core-engine/CoreEngineContextCompaction.js +392 -0
- package/bin/executor/core-engine/CoreEngineContextCompaction.js.map +1 -0
- package/bin/executor/core-engine/CoreEngineMessageState.d.ts +15 -0
- package/bin/executor/core-engine/CoreEngineMessageState.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineMessageState.js +20 -0
- package/bin/executor/core-engine/CoreEngineMessageState.js.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.d.ts +9 -5
- package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
- package/bin/executor/core-engine/CoreEngineRunner.js +146 -35
- package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
- package/bin/executor/store/history/SessionHistoryStore.d.ts +4 -10
- package/bin/executor/store/history/SessionHistoryStore.d.ts.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +1 -3
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
- package/bin/executor/types/SessionRun.d.ts +8 -0
- package/bin/executor/types/SessionRun.d.ts.map +1 -1
- package/bin/index.d.ts +1 -1
- package/bin/index.d.ts.map +1 -1
- package/bin/index.js +1 -1
- package/bin/index.js.map +1 -1
- package/bin/plugin/core/PluginLocalExecution.js +3 -3
- package/bin/plugin/core/PluginLocalExecution.js.map +1 -1
- package/bin/plugin/core/PluginRegistry.d.ts +20 -2
- package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
- package/bin/plugin/core/PluginRegistry.js +100 -3
- package/bin/plugin/core/PluginRegistry.js.map +1 -1
- package/bin/rpc/Client.d.ts +4 -0
- package/bin/rpc/Client.d.ts.map +1 -1
- package/bin/rpc/Client.js +11 -0
- package/bin/rpc/Client.js.map +1 -1
- package/bin/session/Session.d.ts +7 -3
- package/bin/session/Session.d.ts.map +1 -1
- package/bin/session/Session.js +26 -19
- package/bin/session/Session.js.map +1 -1
- package/bin/session/SessionSystemBuilder.d.ts.map +1 -1
- package/bin/session/SessionSystemBuilder.js +3 -1
- package/bin/session/SessionSystemBuilder.js.map +1 -1
- package/bin/session/recorder/SessionRecorderCompaction.d.ts.map +1 -1
- package/bin/session/recorder/SessionRecorderCompaction.js +102 -60
- package/bin/session/recorder/SessionRecorderCompaction.js.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.d.ts +11 -7
- package/bin/session/runtime/SessionPromptRuntime.d.ts.map +1 -1
- package/bin/session/runtime/SessionPromptRuntime.js +22 -24
- package/bin/session/runtime/SessionPromptRuntime.js.map +1 -1
- package/bin/session/services/SessionStateService.d.ts +10 -6
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js +29 -20
- package/bin/session/services/SessionStateService.js.map +1 -1
- package/bin/session/services/SessionTurnService.d.ts +13 -3
- package/bin/session/services/SessionTurnService.d.ts.map +1 -1
- package/bin/session/services/SessionTurnService.js +90 -7
- package/bin/session/services/SessionTurnService.js.map +1 -1
- package/bin/types/agent/SessionActor.d.ts +8 -0
- package/bin/types/agent/SessionActor.d.ts.map +1 -1
- package/bin/types/executor/SessionRunContext.d.ts +16 -8
- package/bin/types/executor/SessionRunContext.d.ts.map +1 -1
- package/bin/types/plugin/PluginRuntime.d.ts +36 -4
- package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
- package/bin/types/plugin/PluginState.d.ts +10 -0
- package/bin/types/plugin/PluginState.d.ts.map +1 -1
- package/bin/types/rpc/RpcProtocol.d.ts +10 -0
- package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
- package/bin/types/runtime/agent/AgentContext.d.ts +2 -2
- package/bin/types/runtime/agent/AgentContext.js +1 -1
- package/bin/types/session/SessionLocalState.d.ts +2 -2
- package/bin/types/session/SessionOptions.d.ts +5 -5
- package/bin/types/session/SessionOptions.d.ts.map +1 -1
- package/bin/types/session/SessionQueueCommand.d.ts +68 -0
- package/bin/types/session/SessionQueueCommand.d.ts.map +1 -0
- package/bin/types/session/SessionQueueCommand.js +10 -0
- package/bin/types/session/SessionQueueCommand.js.map +1 -0
- package/bin/utils/logger/Logger.d.ts +7 -9
- package/bin/utils/logger/Logger.d.ts.map +1 -1
- package/bin/utils/logger/Logger.js +11 -9
- package/bin/utils/logger/Logger.js.map +1 -1
- package/package.json +3 -3
- package/scripts/core-engine-context-compaction.test.mjs +411 -0
- package/scripts/logger-isolation.test.mjs +55 -0
- package/scripts/multi-agent-plugin-isolation.test.mjs +289 -0
- package/scripts/plugin-tool-bridge.test.mjs +52 -0
- package/scripts/remote-session-reconnect.test.mjs +18 -0
- package/scripts/session-compaction-context-window.test.mjs +27 -26
- package/scripts/session-config-turn-boundary.test.mjs +80 -26
- package/scripts/session-prompt-runtime.test.mjs +23 -15
- package/scripts/session-recorder.test.mjs +15 -24
- package/src/agent/local/Agent.ts +2 -2
- package/src/agent/local/services/AgentSessions.ts +10 -10
- package/src/agent/remote/RemoteSession.ts +5 -0
- package/src/agent/remote/RemoteTransport.ts +2 -0
- package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +15 -0
- package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +4 -0
- package/src/executor/Executor.ts +111 -19
- package/src/executor/composer/compaction/SessionCompactionComposer.ts +7 -0
- package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionComposer.ts +4 -57
- package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +8 -66
- package/src/executor/core-engine/CoreEngineContextCompaction.ts +477 -0
- package/src/executor/core-engine/CoreEngineMessageState.ts +29 -0
- package/src/executor/core-engine/CoreEngineRunner.ts +200 -48
- package/src/executor/store/history/SessionHistoryStore.ts +4 -12
- package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +1 -3
- package/src/executor/types/SessionRun.ts +9 -0
- package/src/index.ts +1 -1
- package/src/plugin/core/PluginLocalExecution.ts +3 -3
- package/src/plugin/core/PluginRegistry.ts +116 -3
- package/src/rpc/Client.ts +12 -0
- package/src/session/Session.ts +28 -20
- package/src/session/SessionSystemBuilder.ts +4 -1
- package/src/session/recorder/SessionRecorderCompaction.ts +113 -65
- package/src/session/runtime/SessionPromptRuntime.ts +31 -46
- package/src/session/services/SessionStateService.ts +34 -25
- package/src/session/services/SessionTurnService.ts +97 -8
- package/src/types/agent/SessionActor.ts +9 -0
- package/src/types/executor/SessionRunContext.ts +17 -8
- package/src/types/plugin/PluginRuntime.ts +38 -4
- package/src/types/plugin/PluginState.ts +10 -0
- package/src/types/rpc/RpcProtocol.ts +11 -0
- package/src/types/runtime/agent/AgentContext.ts +2 -2
- package/src/types/session/SessionLocalState.ts +2 -2
- package/src/types/session/SessionOptions.ts +6 -6
- package/src/types/session/SessionQueueCommand.ts +78 -0
- package/src/utils/logger/Logger.ts +10 -10
- package/tsconfig.tsbuildinfo +1 -1
- package/bin/types/session/SessionConfigMutation.d.ts +0 -68
- package/bin/types/session/SessionConfigMutation.d.ts.map +0 -1
- package/bin/types/session/SessionConfigMutation.js +0 -10
- package/bin/types/session/SessionConfigMutation.js.map +0 -1
- package/src/types/session/SessionConfigMutation.ts +0 -78
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 验证多个 Agent 经 session.prompt 执行 plugin_call 时保持 registry 隔离。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - 先创建 Agent A,再创建 Agent B,锁住历史全局 runtime 的覆盖顺序。
|
|
6
|
+
* - 两个 Session 并发进入模型 tool loop,验证异步作用域不会造成 registry 串线。
|
|
7
|
+
* - 第二次模型请求必须收到各自 plugin action 返回的 owner。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import http from "node:http";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import fs from "node:fs/promises";
|
|
16
|
+
|
|
17
|
+
import { Agent } from "../bin/index.js";
|
|
18
|
+
import {
|
|
19
|
+
createAction,
|
|
20
|
+
createPlugin,
|
|
21
|
+
} from "../bin/plugin/core/PluginActionFactory.js";
|
|
22
|
+
import { CITY_MODEL_INVOKER, CITY_MODEL_KIND } from "@downcity/type";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 写入一组 OpenAI-compatible SSE chunk。
|
|
26
|
+
*/
|
|
27
|
+
function write_openai_sse(response, chunks) {
|
|
28
|
+
response.writeHead(200, {
|
|
29
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
30
|
+
"cache-control": "no-cache",
|
|
31
|
+
connection: "keep-alive",
|
|
32
|
+
});
|
|
33
|
+
for (const chunk of chunks) {
|
|
34
|
+
const payload = typeof chunk === "string" ? chunk : JSON.stringify(chunk);
|
|
35
|
+
response.write(`data: ${payload}\n\n`);
|
|
36
|
+
}
|
|
37
|
+
response.end();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 读取 HTTP JSON 请求体。
|
|
42
|
+
*/
|
|
43
|
+
async function read_json_body(request) {
|
|
44
|
+
const raw = await new Promise((resolve, reject) => {
|
|
45
|
+
let data = "";
|
|
46
|
+
request.on("data", (chunk) => {
|
|
47
|
+
data += chunk;
|
|
48
|
+
});
|
|
49
|
+
request.on("end", () => resolve(data));
|
|
50
|
+
request.on("error", reject);
|
|
51
|
+
});
|
|
52
|
+
return JSON.parse(String(raw || "{}"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 返回普通文本模型流。
|
|
57
|
+
*/
|
|
58
|
+
function write_text_response(response, input) {
|
|
59
|
+
write_openai_sse(response, [
|
|
60
|
+
{
|
|
61
|
+
id: input.id,
|
|
62
|
+
object: "chat.completion.chunk",
|
|
63
|
+
created: 1,
|
|
64
|
+
model: input.model,
|
|
65
|
+
choices: [
|
|
66
|
+
{
|
|
67
|
+
index: 0,
|
|
68
|
+
delta: { role: "assistant", content: input.text },
|
|
69
|
+
finish_reason: null,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: input.id,
|
|
75
|
+
object: "chat.completion.chunk",
|
|
76
|
+
created: 1,
|
|
77
|
+
model: input.model,
|
|
78
|
+
choices: [
|
|
79
|
+
{
|
|
80
|
+
index: 0,
|
|
81
|
+
delta: {},
|
|
82
|
+
finish_reason: "stop",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
"[DONE]",
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 返回一次 plugin_call tool call。
|
|
92
|
+
*/
|
|
93
|
+
function write_plugin_call_response(response, model) {
|
|
94
|
+
write_openai_sse(response, [
|
|
95
|
+
{
|
|
96
|
+
id: `chatcmpl_${model}_tool`,
|
|
97
|
+
object: "chat.completion.chunk",
|
|
98
|
+
created: 1,
|
|
99
|
+
model,
|
|
100
|
+
choices: [
|
|
101
|
+
{
|
|
102
|
+
index: 0,
|
|
103
|
+
delta: { role: "assistant" },
|
|
104
|
+
finish_reason: null,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: `chatcmpl_${model}_tool`,
|
|
110
|
+
object: "chat.completion.chunk",
|
|
111
|
+
created: 1,
|
|
112
|
+
model,
|
|
113
|
+
choices: [
|
|
114
|
+
{
|
|
115
|
+
index: 0,
|
|
116
|
+
delta: {
|
|
117
|
+
tool_calls: [
|
|
118
|
+
{
|
|
119
|
+
index: 0,
|
|
120
|
+
id: `call_${model}`,
|
|
121
|
+
type: "function",
|
|
122
|
+
function: {
|
|
123
|
+
name: "plugin_call",
|
|
124
|
+
arguments: JSON.stringify({
|
|
125
|
+
plugin: "skill",
|
|
126
|
+
action: "lookup",
|
|
127
|
+
payload: {},
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
finish_reason: null,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: `chatcmpl_${model}_tool`,
|
|
139
|
+
object: "chat.completion.chunk",
|
|
140
|
+
created: 1,
|
|
141
|
+
model,
|
|
142
|
+
choices: [
|
|
143
|
+
{
|
|
144
|
+
index: 0,
|
|
145
|
+
delta: {},
|
|
146
|
+
finish_reason: "tool_calls",
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
"[DONE]",
|
|
151
|
+
]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 创建绑定测试 HTTP 服务的 CityModel。
|
|
156
|
+
*/
|
|
157
|
+
function create_test_model(base_url, model_id) {
|
|
158
|
+
return Object.freeze({
|
|
159
|
+
id: model_id,
|
|
160
|
+
name: model_id,
|
|
161
|
+
description: "Multi-agent plugin isolation model",
|
|
162
|
+
modalities: ["text", "stream"],
|
|
163
|
+
tags: [],
|
|
164
|
+
meta: {},
|
|
165
|
+
kind: CITY_MODEL_KIND,
|
|
166
|
+
[CITY_MODEL_INVOKER]: {
|
|
167
|
+
connection: () => ({
|
|
168
|
+
base_url,
|
|
169
|
+
api_key: "test_key",
|
|
170
|
+
model_id,
|
|
171
|
+
}),
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 创建返回固定 owner 的 skill plugin。
|
|
178
|
+
*/
|
|
179
|
+
function create_owner_plugin(owner, executed_owners) {
|
|
180
|
+
return createPlugin({
|
|
181
|
+
name: "skill",
|
|
182
|
+
title: `Skill ${owner}`,
|
|
183
|
+
description: "Return the owning Agent id",
|
|
184
|
+
actions: {
|
|
185
|
+
lookup: createAction({
|
|
186
|
+
description: "Return registry owner",
|
|
187
|
+
execute: async () => {
|
|
188
|
+
executed_owners.push(owner);
|
|
189
|
+
return {
|
|
190
|
+
success: true,
|
|
191
|
+
data: { owner },
|
|
192
|
+
message: owner,
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
}),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
test("multiple session prompts use only their owning Agent plugin registry", async () => {
|
|
201
|
+
const model_requests = new Map();
|
|
202
|
+
const executed_owners = [];
|
|
203
|
+
const server = http.createServer(async (request, response) => {
|
|
204
|
+
const url = new URL(String(request.url || "/"), "http://127.0.0.1");
|
|
205
|
+
if (
|
|
206
|
+
request.method !== "POST" ||
|
|
207
|
+
url.pathname !== "/v1/ai/chat/completions"
|
|
208
|
+
) {
|
|
209
|
+
response.writeHead(404);
|
|
210
|
+
response.end("not found");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const body = await read_json_body(request);
|
|
215
|
+
const model = String(body.model || "");
|
|
216
|
+
if (!Array.isArray(body.tools)) {
|
|
217
|
+
write_text_response(response, {
|
|
218
|
+
id: `chatcmpl_${model}_title`,
|
|
219
|
+
model,
|
|
220
|
+
text: `Title ${model}`,
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const requests = model_requests.get(model) || [];
|
|
226
|
+
requests.push(body);
|
|
227
|
+
model_requests.set(model, requests);
|
|
228
|
+
if (requests.length === 1) {
|
|
229
|
+
write_plugin_call_response(response, model);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
write_text_response(response, {
|
|
233
|
+
id: `chatcmpl_${model}_done`,
|
|
234
|
+
model,
|
|
235
|
+
text: "done",
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
240
|
+
const address = server.address();
|
|
241
|
+
assert.ok(address && typeof address === "object");
|
|
242
|
+
const base_url = `http://127.0.0.1:${String(address.port)}/v1/ai`;
|
|
243
|
+
const root_a = await fs.mkdtemp(
|
|
244
|
+
path.join(os.tmpdir(), "downcity-plugin-isolation-a-"),
|
|
245
|
+
);
|
|
246
|
+
const root_b = await fs.mkdtemp(
|
|
247
|
+
path.join(os.tmpdir(), "downcity-plugin-isolation-b-"),
|
|
248
|
+
);
|
|
249
|
+
const agent_a = new Agent({
|
|
250
|
+
id: "agent_a",
|
|
251
|
+
path: root_a,
|
|
252
|
+
plugins: [create_owner_plugin("agent_a", executed_owners)],
|
|
253
|
+
model: create_test_model(base_url, "model_a"),
|
|
254
|
+
});
|
|
255
|
+
const agent_b = new Agent({
|
|
256
|
+
id: "agent_b",
|
|
257
|
+
path: root_b,
|
|
258
|
+
plugins: [create_owner_plugin("agent_b", executed_owners)],
|
|
259
|
+
model: create_test_model(base_url, "model_b"),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const session_a = await agent_a.sessions.create({ sessionId: "session_a" });
|
|
264
|
+
const session_b = await agent_b.sessions.create({ sessionId: "session_b" });
|
|
265
|
+
const [turn_a, turn_b] = await Promise.all([
|
|
266
|
+
session_a.prompt({ query: "Call your skill plugin" }),
|
|
267
|
+
session_b.prompt({ query: "Call your skill plugin" }),
|
|
268
|
+
]);
|
|
269
|
+
const [result_a, result_b] = await Promise.all([
|
|
270
|
+
turn_a.finished,
|
|
271
|
+
turn_b.finished,
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
assert.equal(result_a.success, true);
|
|
275
|
+
assert.equal(result_b.success, true);
|
|
276
|
+
assert.deepEqual([...executed_owners].sort(), ["agent_a", "agent_b"]);
|
|
277
|
+
for (const [model, owner] of [
|
|
278
|
+
["model_a", "agent_a"],
|
|
279
|
+
["model_b", "agent_b"],
|
|
280
|
+
]) {
|
|
281
|
+
const requests = model_requests.get(model) || [];
|
|
282
|
+
assert.equal(requests.length, 2);
|
|
283
|
+
assert.match(JSON.stringify(requests[1].messages), new RegExp(owner));
|
|
284
|
+
}
|
|
285
|
+
} finally {
|
|
286
|
+
await Promise.all([agent_a.dispose(), agent_b.dispose()]);
|
|
287
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
288
|
+
}
|
|
289
|
+
});
|
|
@@ -313,3 +313,55 @@ test("PluginRegistry keeps plugin ready after action business failure", async ()
|
|
|
313
313
|
assert.equal(retry.data.loaded, true);
|
|
314
314
|
assert.equal(registry.status("skill").status, "ready");
|
|
315
315
|
});
|
|
316
|
+
|
|
317
|
+
test("PluginRegistry delays lifecycle stop until the active execution lease is released", async () => {
|
|
318
|
+
let lifecycle_active = false;
|
|
319
|
+
let stop_count = 0;
|
|
320
|
+
const plugin = createPlugin({
|
|
321
|
+
name: "leased-plugin",
|
|
322
|
+
title: "Leased Plugin",
|
|
323
|
+
description: "Keeps runtime resources alive for an active Session step",
|
|
324
|
+
lifecycle: {
|
|
325
|
+
start: async () => {
|
|
326
|
+
lifecycle_active = true;
|
|
327
|
+
},
|
|
328
|
+
stop: async () => {
|
|
329
|
+
lifecycle_active = false;
|
|
330
|
+
stop_count += 1;
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
actions: {
|
|
334
|
+
status: createAction({
|
|
335
|
+
description: "Read lifecycle state",
|
|
336
|
+
execute: async () => ({
|
|
337
|
+
success: lifecycle_active,
|
|
338
|
+
data: { lifecycle_active },
|
|
339
|
+
}),
|
|
340
|
+
}),
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
const registry = createAgentPluginRegistry({
|
|
344
|
+
plugins: [plugin],
|
|
345
|
+
plugin_instances: new Map(),
|
|
346
|
+
get_context: () => ({ rootPath: process.cwd() }),
|
|
347
|
+
});
|
|
348
|
+
await registry.startAll();
|
|
349
|
+
const lease = registry.execution_view().acquire();
|
|
350
|
+
|
|
351
|
+
assert.equal(await registry.unregister("leased-plugin"), true);
|
|
352
|
+
assert.equal(registry.has("leased-plugin"), false);
|
|
353
|
+
assert.equal(lifecycle_active, true);
|
|
354
|
+
assert.equal(stop_count, 0);
|
|
355
|
+
|
|
356
|
+
const result = await lease.runAction({
|
|
357
|
+
plugin: "leased-plugin",
|
|
358
|
+
action: "status",
|
|
359
|
+
});
|
|
360
|
+
assert.equal(result.success, true);
|
|
361
|
+
assert.equal(result.data.lifecycle_active, true);
|
|
362
|
+
|
|
363
|
+
await lease.release();
|
|
364
|
+
await lease.release();
|
|
365
|
+
assert.equal(lifecycle_active, false);
|
|
366
|
+
assert.equal(stop_count, 1);
|
|
367
|
+
});
|
|
@@ -102,3 +102,21 @@ test("RemoteSession switches models by stable id without local model instances",
|
|
|
102
102
|
/does not accept a local model instance/,
|
|
103
103
|
);
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
test("RemoteSession queues compact through its transport", async () => {
|
|
107
|
+
const compacted_session_ids = [];
|
|
108
|
+
const transport = {
|
|
109
|
+
async compact(session_id) {
|
|
110
|
+
compacted_session_ids.push(session_id);
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
const session = new RemoteSession(transport, {
|
|
114
|
+
agentId: "agent_test",
|
|
115
|
+
sessionId: "session_test",
|
|
116
|
+
messageCount: 0,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await session.compact();
|
|
120
|
+
|
|
121
|
+
assert.deepEqual(compacted_session_ids, ["session_test"]);
|
|
122
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file 验证 CityModel
|
|
2
|
+
* @file 验证 CityModel 上下文窗口元数据与 compact 的明确触发语义。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
5
|
* - CityModel 目录元数据需要在归一化前读取。
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
6
|
+
* - compact 不再根据字符/token 预估自动触发。
|
|
7
|
+
* - 只有明确 force 或超限恢复才会调用 history store。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import assert from "node:assert/strict";
|
|
@@ -53,6 +53,7 @@ async function capture_compact_input(options = {}) {
|
|
|
53
53
|
model: {},
|
|
54
54
|
system: [],
|
|
55
55
|
retryCount: options.retry_count ?? 0,
|
|
56
|
+
force: true,
|
|
56
57
|
...(options.context_window !== undefined
|
|
57
58
|
? { context_window: options.context_window }
|
|
58
59
|
: {}),
|
|
@@ -68,28 +69,28 @@ test("CityModel exposes its configured context window to Agent", () => {
|
|
|
68
69
|
);
|
|
69
70
|
})
|
|
70
71
|
|
|
71
|
-
test("
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
assert.equal(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
context_window: 256000,
|
|
79
|
-
retry_count: 1,
|
|
80
|
-
});
|
|
81
|
-
assert.equal(retry.maxInputTokensApprox, 102400);
|
|
82
|
-
})
|
|
72
|
+
test("compact input carries only an explicit force request", async () => {
|
|
73
|
+
const compact_input = await capture_compact_input({ context_window: 256000 });
|
|
74
|
+
assert.equal(compact_input.force, true);
|
|
75
|
+
assert.equal("maxInputTokensApprox" in compact_input, false);
|
|
76
|
+
assert.equal("compactRatio" in compact_input, false);
|
|
77
|
+
assert.equal("keepLastMessages" in compact_input, false);
|
|
78
|
+
});
|
|
83
79
|
|
|
84
|
-
test("
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
test("normal composer run does not call history store without force", async () => {
|
|
81
|
+
let called = false;
|
|
82
|
+
const composer = new JsonlSessionCompactionComposer();
|
|
83
|
+
const result = await composer.run({
|
|
84
|
+
historyStore: {
|
|
85
|
+
compact: async () => {
|
|
86
|
+
called = true;
|
|
87
|
+
return { compacted: true };
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
model: {},
|
|
91
|
+
system: [],
|
|
92
|
+
retryCount: 0,
|
|
88
93
|
});
|
|
89
|
-
assert.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
test("compact keeps the existing 128k fallback without model metadata", async () => {
|
|
93
|
-
const compact_input = await capture_compact_input();
|
|
94
|
-
assert.equal(compact_input.maxInputTokensApprox, 128000);
|
|
95
|
-
})
|
|
94
|
+
assert.deepEqual(result, { compacted: false, reason: "not_requested" });
|
|
95
|
+
assert.equal(called, false);
|
|
96
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file 验证 Agent
|
|
2
|
+
* @file 验证 Agent 配置与 steer 在 Session step 检查点统一生效。
|
|
3
3
|
*
|
|
4
4
|
* 关键点(中文)
|
|
5
|
-
* -
|
|
5
|
+
* - 第一次 provider 请求保持阻塞,用来制造真实的运行中配置修改窗口。
|
|
6
6
|
* - instruction、env 与 plugin registry 修改不能改变已经开始的请求。
|
|
7
|
-
* -
|
|
7
|
+
* - config 与 steer 在同一个 Session step 检查点提交,并继续使用同一个 turn id。
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import test from "node:test";
|
|
@@ -58,33 +58,39 @@ function create_stream_text_result(text) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
test("running Agent config changes
|
|
61
|
+
test("running Agent config changes and steer apply at the same Session step checkpoint", async () => {
|
|
62
62
|
const agent_path = await fs.mkdtemp(
|
|
63
63
|
path.join(os.tmpdir(), "downcity-agent-config-turn-boundary-"),
|
|
64
64
|
);
|
|
65
|
-
const
|
|
66
|
-
const
|
|
65
|
+
const first_provider_request_started = create_deferred();
|
|
66
|
+
const release_first_provider_request = create_deferred();
|
|
67
67
|
const provider_prompts = [];
|
|
68
|
-
let
|
|
68
|
+
let provider_request_count = 0;
|
|
69
|
+
let plugin_stop_count = 0;
|
|
69
70
|
|
|
70
71
|
const model = new MockLanguageModelV3({
|
|
71
72
|
modelId: "config-turn-boundary-model",
|
|
72
73
|
doStream: async (options) => {
|
|
73
74
|
const has_tools = Array.isArray(options.tools) && options.tools.length > 0;
|
|
74
75
|
if (!has_tools) return create_stream_text_result("Session title");
|
|
75
|
-
|
|
76
|
+
provider_request_count += 1;
|
|
76
77
|
provider_prompts.push(JSON.stringify(options.prompt));
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
await
|
|
78
|
+
if (provider_request_count === 1) {
|
|
79
|
+
first_provider_request_started.resolve();
|
|
80
|
+
await release_first_provider_request.promise;
|
|
80
81
|
}
|
|
81
|
-
return create_stream_text_result(`done:${String(
|
|
82
|
+
return create_stream_text_result(`done:${String(provider_request_count)}`);
|
|
82
83
|
},
|
|
83
84
|
});
|
|
84
85
|
const runtime_plugin = createPlugin({
|
|
85
86
|
name: "runtime-config",
|
|
86
87
|
title: "Runtime Config",
|
|
87
88
|
description: "Provides a system block for turn-boundary tests",
|
|
89
|
+
lifecycle: {
|
|
90
|
+
stop: async () => {
|
|
91
|
+
plugin_stop_count += 1;
|
|
92
|
+
},
|
|
93
|
+
},
|
|
88
94
|
system: (context) => `plugin-env:${context.env.TURN_ENV || "missing"}`,
|
|
89
95
|
actions: {
|
|
90
96
|
ping: createAction({
|
|
@@ -107,22 +113,25 @@ test("running Agent config changes become effective together at the next model t
|
|
|
107
113
|
sessionId: "config_turn_boundary_session",
|
|
108
114
|
});
|
|
109
115
|
const first_turn = await session.prompt({ query: "first" });
|
|
110
|
-
await
|
|
116
|
+
await first_provider_request_started.promise;
|
|
111
117
|
|
|
112
118
|
agent.setInstruction(["instruction:new"]);
|
|
113
119
|
agent.patchEnv({ TURN_ENV: "new" });
|
|
114
120
|
await agent.plugins.unregister("runtime-config");
|
|
121
|
+
const steer_turn_promise = session.prompt({ query: "steer" });
|
|
115
122
|
|
|
123
|
+
assert.equal(plugin_stop_count, 0);
|
|
116
124
|
assert.equal(provider_prompts.length, 1);
|
|
117
125
|
assert.match(provider_prompts[0], /instruction:old/);
|
|
118
126
|
assert.match(provider_prompts[0], /plugin-env:old/);
|
|
119
127
|
assert.doesNotMatch(provider_prompts[0], /instruction:new/);
|
|
120
128
|
|
|
121
|
-
|
|
129
|
+
release_first_provider_request.resolve();
|
|
130
|
+
const steer_turn = await steer_turn_promise;
|
|
122
131
|
assert.equal((await first_turn.finished).success, true);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
assert.equal(
|
|
132
|
+
assert.equal(steer_turn.id, first_turn.id);
|
|
133
|
+
assert.equal((await steer_turn.finished).success, true);
|
|
134
|
+
assert.equal(plugin_stop_count, 1);
|
|
126
135
|
assert.equal(provider_prompts.length, 2);
|
|
127
136
|
assert.match(provider_prompts[1], /instruction:new/);
|
|
128
137
|
assert.doesNotMatch(provider_prompts[1], /instruction:old/);
|
|
@@ -138,14 +147,14 @@ test("running Agent config changes become effective together at the next model t
|
|
|
138
147
|
"Agent plugin runtime-config unregistered",
|
|
139
148
|
]);
|
|
140
149
|
} finally {
|
|
141
|
-
|
|
150
|
+
release_first_provider_request.resolve();
|
|
142
151
|
await agent.dispose();
|
|
143
152
|
}
|
|
144
153
|
});
|
|
145
154
|
|
|
146
|
-
test("running session model changes
|
|
155
|
+
test("running session model changes apply with steer at the next Session step", async () => {
|
|
147
156
|
const agent_path = await fs.mkdtemp(
|
|
148
|
-
path.join(os.tmpdir(), "downcity-session-
|
|
157
|
+
path.join(os.tmpdir(), "downcity-session-step-boundary-"),
|
|
149
158
|
);
|
|
150
159
|
const old_model_started = create_deferred();
|
|
151
160
|
const release_old_model = create_deferred();
|
|
@@ -183,7 +192,7 @@ test("running session model changes keep the current provider and switch the nex
|
|
|
183
192
|
},
|
|
184
193
|
});
|
|
185
194
|
const agent = new Agent({
|
|
186
|
-
id: "
|
|
195
|
+
id: "session_step_boundary_agent",
|
|
187
196
|
path: agent_path,
|
|
188
197
|
model: old_model,
|
|
189
198
|
plugins: [runtime_plugin],
|
|
@@ -191,20 +200,20 @@ test("running session model changes keep the current provider and switch the nex
|
|
|
191
200
|
|
|
192
201
|
try {
|
|
193
202
|
const session = await agent.sessions.create({
|
|
194
|
-
sessionId: "
|
|
203
|
+
sessionId: "session_step_boundary_session",
|
|
195
204
|
});
|
|
196
205
|
const first_turn = await session.prompt({ query: "first" });
|
|
197
206
|
await old_model_started.promise;
|
|
198
207
|
|
|
199
208
|
await session.set({ model: new_model });
|
|
209
|
+
const steer_turn_promise = session.prompt({ query: "continue" });
|
|
200
210
|
assert.deepEqual(model_calls, ["old-model"]);
|
|
201
211
|
|
|
202
212
|
release_old_model.resolve();
|
|
213
|
+
const steer_turn = await steer_turn_promise;
|
|
203
214
|
assert.equal((await first_turn.finished).success, true);
|
|
204
|
-
assert.
|
|
205
|
-
|
|
206
|
-
const second_turn = await session.prompt({ query: "second" });
|
|
207
|
-
assert.equal((await second_turn.finished).success, true);
|
|
215
|
+
assert.equal(steer_turn.id, first_turn.id);
|
|
216
|
+
assert.equal((await steer_turn.finished).success, true);
|
|
208
217
|
assert.deepEqual(model_calls, ["old-model", "new-model"]);
|
|
209
218
|
|
|
210
219
|
const messages = await session.messages();
|
|
@@ -222,3 +231,48 @@ test("running session model changes keep the current provider and switch the nex
|
|
|
222
231
|
await agent.dispose();
|
|
223
232
|
}
|
|
224
233
|
});
|
|
234
|
+
|
|
235
|
+
test("config remains effective when its action message cannot be persisted", async () => {
|
|
236
|
+
const agent_path = await fs.mkdtemp(
|
|
237
|
+
path.join(os.tmpdir(), "downcity-config-action-observability-"),
|
|
238
|
+
);
|
|
239
|
+
const model_calls = [];
|
|
240
|
+
const old_model = new MockLanguageModelV3({
|
|
241
|
+
modelId: "old-observability-model",
|
|
242
|
+
doStream: async () => {
|
|
243
|
+
model_calls.push("old");
|
|
244
|
+
return create_stream_text_result("old");
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
const new_model = new MockLanguageModelV3({
|
|
248
|
+
modelId: "new-observability-model",
|
|
249
|
+
doStream: async () => {
|
|
250
|
+
model_calls.push("new");
|
|
251
|
+
return create_stream_text_result("new");
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
const agent = new Agent({
|
|
255
|
+
id: "config_action_observability_agent",
|
|
256
|
+
path: agent_path,
|
|
257
|
+
model: old_model,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const session = await agent.sessions.create({
|
|
262
|
+
sessionId: "config_action_observability_session",
|
|
263
|
+
});
|
|
264
|
+
session.stateService.emit_action_event = async () => {
|
|
265
|
+
throw new Error("action store unavailable");
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
await session.set({ model: new_model });
|
|
269
|
+
const turn = await session.prompt({ query: "use configured model" });
|
|
270
|
+
assert.equal((await turn.finished).success, true);
|
|
271
|
+
assert.deepEqual(model_calls, ["new", "new"]);
|
|
272
|
+
|
|
273
|
+
const messages = await session.messages();
|
|
274
|
+
assert.equal(messages.items.some((message) => message.type === "action"), false);
|
|
275
|
+
} finally {
|
|
276
|
+
await agent.dispose();
|
|
277
|
+
}
|
|
278
|
+
});
|