@downcity/agent 1.1.259 → 1.1.260
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agent/AgentModel.d.ts +2 -4
- package/bin/agent/AgentModel.d.ts.map +1 -1
- package/bin/agent/AgentModel.js +2 -26
- package/bin/agent/AgentModel.js.map +1 -1
- package/package.json +3 -4
- package/scripts/city-model-tool-loop.test.mjs +142 -299
- package/scripts/linux-bubblewrap-sandbox.test.mjs +40 -123
- package/scripts/multi-agent-plugin-isolation.test.mjs +86 -207
- package/scripts/shell-sandbox-env.test.mjs +47 -60
- package/src/agent/AgentModel.ts +2 -29
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file 验证 Linux
|
|
3
|
-
*
|
|
4
|
-
* 关键点(中文)
|
|
5
|
-
* - 测试编译后的 bin 输出,避免测试文件进入 package 源码导出面。
|
|
6
|
-
* - 不启动真实 `bwrap`,只锁住路径挂载、网络开关与 shell 调用参数。
|
|
2
|
+
* @file 验证 Linux Bubblewrap 参数对统一 Sandbox 策略的映射。
|
|
7
3
|
*/
|
|
8
4
|
|
|
9
5
|
import test from "node:test";
|
|
@@ -12,140 +8,61 @@ import fs from "node:fs/promises";
|
|
|
12
8
|
import os from "node:os";
|
|
13
9
|
import path from "node:path";
|
|
14
10
|
|
|
15
|
-
import {
|
|
11
|
+
import { build_linux_bubblewrap_args } from "@downcity/shell/sandbox/LinuxBubblewrap.js";
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
const root = await fs.mkdtemp(path.join(os.tmpdir(), "downcity-bwrap-"));
|
|
19
|
-
const projectRoot = path.join(root, "project");
|
|
20
|
-
const writablePath = path.join(projectRoot, ".downcity");
|
|
21
|
-
const shellDir = path.join(writablePath, "shell", "sh_test");
|
|
22
|
-
const sandboxDir = path.join(projectRoot, ".downcity", "sandbox");
|
|
23
|
-
const tmpDir = path.join(sandboxDir, "tmp");
|
|
24
|
-
const cacheDir = path.join(sandboxDir, ".cache");
|
|
25
|
-
|
|
26
|
-
await fs.mkdir(shellDir, { recursive: true });
|
|
27
|
-
await fs.mkdir(tmpDir, { recursive: true });
|
|
28
|
-
await fs.mkdir(cacheDir, { recursive: true });
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
root,
|
|
32
|
-
projectRoot,
|
|
33
|
-
writablePath,
|
|
34
|
-
shellDir,
|
|
35
|
-
sandboxDir,
|
|
36
|
-
tmpDir,
|
|
37
|
-
cacheDir,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function createParams(fixture, overrides = {}) {
|
|
42
|
-
return {
|
|
43
|
-
executionId: "sh_test",
|
|
44
|
-
executionDir: fixture.shellDir,
|
|
45
|
-
cmd: "printf hello",
|
|
46
|
-
cwd: fixture.projectRoot,
|
|
47
|
-
actualCwd: fixture.projectRoot,
|
|
48
|
-
shellPath: "/bin/sh",
|
|
49
|
-
login: true,
|
|
50
|
-
baseEnv: {
|
|
51
|
-
PATH: "/usr/bin:/bin",
|
|
52
|
-
LANG: "C.UTF-8",
|
|
53
|
-
DC_SESSION_ID: "session_test",
|
|
54
|
-
},
|
|
55
|
-
config: {
|
|
56
|
-
backend: "linux-bubblewrap",
|
|
57
|
-
rootPath: fixture.projectRoot,
|
|
58
|
-
sandboxDir: fixture.sandboxDir,
|
|
59
|
-
homeDir: fixture.sandboxDir,
|
|
60
|
-
tmpDir: fixture.tmpDir,
|
|
61
|
-
cacheDir: fixture.cacheDir,
|
|
62
|
-
envAllowlist: ["PATH", "LANG"],
|
|
63
|
-
writablePaths: [fixture.projectRoot, fixture.sandboxDir],
|
|
64
|
-
networkMode: "off",
|
|
65
|
-
},
|
|
66
|
-
...overrides,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function hasArg(args, value) {
|
|
71
|
-
return args.includes(value);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function hasOptionPair(args, option, sourcePath, targetPath = sourcePath) {
|
|
13
|
+
function has_option_pair(args, option, source_path, target_path = source_path) {
|
|
75
14
|
for (let index = 0; index < args.length - 2; index += 1) {
|
|
76
15
|
if (
|
|
77
16
|
args[index] === option &&
|
|
78
|
-
args[index + 1] ===
|
|
79
|
-
args[index + 2] ===
|
|
80
|
-
)
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function hasOptionValue(args, option, value) {
|
|
88
|
-
for (let index = 0; index < args.length - 1; index += 1) {
|
|
89
|
-
if (args[index] === option && args[index + 1] === value) {
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
17
|
+
args[index + 1] === source_path &&
|
|
18
|
+
args[index + 2] === target_path
|
|
19
|
+
) return true;
|
|
92
20
|
}
|
|
93
21
|
return false;
|
|
94
22
|
}
|
|
95
23
|
|
|
96
|
-
test("Linux
|
|
97
|
-
const
|
|
98
|
-
try {
|
|
99
|
-
const args = buildLinuxBubblewrapArgs(createParams(fixture));
|
|
100
|
-
|
|
101
|
-
assert.equal(hasArg(args, "--die-with-parent"), true);
|
|
102
|
-
assert.equal(hasArg(args, "--unshare-pid"), true);
|
|
103
|
-
assert.equal(hasArg(args, "--unshare-net"), true);
|
|
104
|
-
assert.equal(hasOptionPair(args, "--bind", fixture.projectRoot), true);
|
|
105
|
-
assert.equal(hasOptionPair(args, "--bind", fixture.sandboxDir), false);
|
|
106
|
-
assert.equal(hasOptionPair(args, "--ro-bind", fixture.projectRoot), false);
|
|
107
|
-
assert.equal(hasOptionValue(args, "--dir", fixture.writablePath), false);
|
|
108
|
-
assert.deepEqual(args.slice(-5), [
|
|
109
|
-
"--chdir",
|
|
110
|
-
fixture.projectRoot,
|
|
111
|
-
"/bin/sh",
|
|
112
|
-
"-lc",
|
|
113
|
-
"printf hello",
|
|
114
|
-
]);
|
|
115
|
-
} finally {
|
|
116
|
-
await fs.rm(fixture.root, { recursive: true, force: true });
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("Linux bubblewrap args keep root writable when sandbox writablePaths includes root", async () => {
|
|
121
|
-
const fixture = await createSandboxFixture();
|
|
24
|
+
test("Linux maps read paths to ro-bind and workspace to bind", async () => {
|
|
25
|
+
const fixture_root = await fs.mkdtemp(path.join(os.tmpdir(), "downcity-bwrap-"));
|
|
122
26
|
try {
|
|
123
|
-
const
|
|
124
|
-
|
|
27
|
+
const project_root = path.join(fixture_root, "project");
|
|
28
|
+
const tool_root = path.join(fixture_root, "tool");
|
|
29
|
+
await fs.mkdir(project_root, { recursive: true });
|
|
30
|
+
await fs.mkdir(tool_root, { recursive: true });
|
|
31
|
+
const request = {
|
|
32
|
+
execution_id: "sh_test",
|
|
33
|
+
execution_dir: path.join(project_root, ".downcity", "shell", "sh_test"),
|
|
34
|
+
cmd: "printf hello",
|
|
35
|
+
cwd: project_root,
|
|
36
|
+
shell_path: "/bin/sh",
|
|
37
|
+
login: true,
|
|
38
|
+
base_env: { PATH: "/usr/bin:/bin" },
|
|
39
|
+
policy: {
|
|
125
40
|
backend: "linux-bubblewrap",
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
41
|
+
root_path: project_root,
|
|
42
|
+
sandbox_dir: path.join(project_root, ".downcity", "sandbox"),
|
|
43
|
+
home_dir: path.join(project_root, ".downcity", "sandbox"),
|
|
44
|
+
tmp_dir: path.join(project_root, ".downcity", "sandbox", "tmp"),
|
|
45
|
+
cache_dir: path.join(project_root, ".downcity", "sandbox", ".cache"),
|
|
46
|
+
env_allowlist: ["PATH"],
|
|
47
|
+
read_only_paths: [tool_root],
|
|
48
|
+
read_write_paths: [project_root],
|
|
49
|
+
network_mode: "off",
|
|
50
|
+
fingerprint: "policy_test",
|
|
134
51
|
},
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
assert.equal(
|
|
139
|
-
assert.equal(
|
|
140
|
-
assert.equal(
|
|
52
|
+
};
|
|
53
|
+
const args = build_linux_bubblewrap_args(request);
|
|
54
|
+
assert.equal(args.includes("--unshare-net"), true);
|
|
55
|
+
assert.equal(has_option_pair(args, "--ro-bind", tool_root), true);
|
|
56
|
+
assert.equal(has_option_pair(args, "--bind", tool_root), false);
|
|
57
|
+
assert.equal(has_option_pair(args, "--bind", project_root), true);
|
|
141
58
|
assert.deepEqual(args.slice(-5), [
|
|
142
59
|
"--chdir",
|
|
143
|
-
|
|
60
|
+
project_root,
|
|
144
61
|
"/bin/sh",
|
|
145
|
-
"-
|
|
62
|
+
"-lc",
|
|
146
63
|
"printf hello",
|
|
147
64
|
]);
|
|
148
65
|
} finally {
|
|
149
|
-
await fs.rm(
|
|
66
|
+
await fs.rm(fixture_root, { recursive: true, force: true });
|
|
150
67
|
}
|
|
151
68
|
});
|
|
@@ -1,161 +1,100 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file 验证多个 Agent 经 session.prompt 执行 plugin_call 时保持 registry 隔离。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - 两个 Session 并发进入模型 tool loop,验证异步作用域不会造成 registry 串线。
|
|
7
|
-
* - 第二次模型请求必须收到各自 plugin action 返回的 owner。
|
|
4
|
+
* 两个 CityModel 并发进入原生 LanguageModelV3 tool loop,第二次模型调用必须只
|
|
5
|
+
* 收到各自 Agent plugin action 返回的 owner。
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
8
|
import test from "node:test";
|
|
11
9
|
import assert from "node:assert/strict";
|
|
12
|
-
import http from "node:http";
|
|
13
10
|
import os from "node:os";
|
|
14
11
|
import path from "node:path";
|
|
15
12
|
import fs from "node:fs/promises";
|
|
13
|
+
import { MockLanguageModelV3 } from "ai/test";
|
|
16
14
|
|
|
17
15
|
import { Agent } from "../bin/index.js";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
createPlugin,
|
|
21
|
-
} from "../bin/plugin/core/PluginActionFactory.js";
|
|
22
|
-
import { CITY_MODEL_INVOKER, CITY_MODEL_KIND } from "@downcity/type";
|
|
16
|
+
import { createAction, createPlugin } from "../bin/plugin/core/PluginActionFactory.js";
|
|
17
|
+
import { CITY_MODEL_KIND } from "@downcity/type";
|
|
23
18
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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();
|
|
19
|
+
/** 构造 AI SDK V3 usage。 */
|
|
20
|
+
function create_usage() {
|
|
21
|
+
return {
|
|
22
|
+
inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
|
|
23
|
+
outputTokens: { total: 1, text: 1, reasoning: 0 },
|
|
24
|
+
};
|
|
38
25
|
}
|
|
39
26
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
27
|
+
/** 返回普通文本模型流。 */
|
|
28
|
+
function create_text_stream(text) {
|
|
29
|
+
return {
|
|
30
|
+
stream: new ReadableStream({
|
|
31
|
+
start(controller) {
|
|
32
|
+
controller.enqueue({ type: "stream-start", warnings: [] });
|
|
33
|
+
controller.enqueue({ type: "text-start", id: "text_1" });
|
|
34
|
+
controller.enqueue({ type: "text-delta", id: "text_1", delta: text });
|
|
35
|
+
controller.enqueue({ type: "text-end", id: "text_1" });
|
|
36
|
+
controller.enqueue({
|
|
37
|
+
type: "finish",
|
|
38
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
39
|
+
usage: create_usage(),
|
|
40
|
+
});
|
|
41
|
+
controller.close();
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
53
45
|
}
|
|
54
46
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
},
|
|
86
|
-
"[DONE]",
|
|
87
|
-
]);
|
|
47
|
+
/** 返回一次 plugin_call tool call。 */
|
|
48
|
+
function create_plugin_call_stream(model_id) {
|
|
49
|
+
const tool_input = JSON.stringify({
|
|
50
|
+
plugin: "skill",
|
|
51
|
+
action: "lookup",
|
|
52
|
+
payload: {},
|
|
53
|
+
});
|
|
54
|
+
const call_id = `call_${model_id}`;
|
|
55
|
+
return {
|
|
56
|
+
stream: new ReadableStream({
|
|
57
|
+
start(controller) {
|
|
58
|
+
controller.enqueue({ type: "stream-start", warnings: [] });
|
|
59
|
+
controller.enqueue({ type: "tool-input-start", id: call_id, toolName: "plugin_call" });
|
|
60
|
+
controller.enqueue({ type: "tool-input-delta", id: call_id, delta: tool_input });
|
|
61
|
+
controller.enqueue({ type: "tool-input-end", id: call_id });
|
|
62
|
+
controller.enqueue({
|
|
63
|
+
type: "tool-call",
|
|
64
|
+
toolCallId: call_id,
|
|
65
|
+
toolName: "plugin_call",
|
|
66
|
+
input: tool_input,
|
|
67
|
+
});
|
|
68
|
+
controller.enqueue({
|
|
69
|
+
type: "finish",
|
|
70
|
+
finishReason: { unified: "tool-calls", raw: "tool_calls" },
|
|
71
|
+
usage: create_usage(),
|
|
72
|
+
});
|
|
73
|
+
controller.close();
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
88
77
|
}
|
|
89
78
|
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
],
|
|
79
|
+
/** 创建原生 LanguageModelV3 CityModel。 */
|
|
80
|
+
function create_test_model(model_id, model_requests) {
|
|
81
|
+
let request_count = 0;
|
|
82
|
+
const language_model = new MockLanguageModelV3({
|
|
83
|
+
modelId: model_id,
|
|
84
|
+
doStream: async (options) => {
|
|
85
|
+
if (!Array.isArray(options.tools) || options.tools.length === 0) {
|
|
86
|
+
return create_text_stream(`Title ${model_id}`);
|
|
87
|
+
}
|
|
88
|
+
request_count += 1;
|
|
89
|
+
const requests = model_requests.get(model_id) ?? [];
|
|
90
|
+
requests.push(options);
|
|
91
|
+
model_requests.set(model_id, requests);
|
|
92
|
+
return request_count === 1
|
|
93
|
+
? create_plugin_call_stream(model_id)
|
|
94
|
+
: create_text_stream("done");
|
|
136
95
|
},
|
|
137
|
-
|
|
138
|
-
|
|
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({
|
|
96
|
+
});
|
|
97
|
+
return Object.assign(language_model, {
|
|
159
98
|
id: model_id,
|
|
160
99
|
name: model_id,
|
|
161
100
|
description: "Multi-agent plugin isolation model",
|
|
@@ -163,19 +102,10 @@ function create_test_model(base_url, model_id) {
|
|
|
163
102
|
tags: [],
|
|
164
103
|
meta: {},
|
|
165
104
|
kind: CITY_MODEL_KIND,
|
|
166
|
-
[CITY_MODEL_INVOKER]: {
|
|
167
|
-
connection: () => ({
|
|
168
|
-
base_url,
|
|
169
|
-
api_key: "test_key",
|
|
170
|
-
model_id,
|
|
171
|
-
}),
|
|
172
|
-
},
|
|
173
105
|
});
|
|
174
106
|
}
|
|
175
107
|
|
|
176
|
-
/**
|
|
177
|
-
* 创建返回固定 owner 的 skill plugin。
|
|
178
|
-
*/
|
|
108
|
+
/** 创建返回固定 owner 的 skill plugin。 */
|
|
179
109
|
function create_owner_plugin(owner, executed_owners) {
|
|
180
110
|
return createPlugin({
|
|
181
111
|
name: "skill",
|
|
@@ -200,63 +130,19 @@ function create_owner_plugin(owner, executed_owners) {
|
|
|
200
130
|
test("multiple session prompts use only their owning Agent plugin registry", async () => {
|
|
201
131
|
const model_requests = new Map();
|
|
202
132
|
const executed_owners = [];
|
|
203
|
-
const
|
|
204
|
-
|
|
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
|
-
);
|
|
133
|
+
const root_a = await fs.mkdtemp(path.join(os.tmpdir(), "downcity-plugin-isolation-a-"));
|
|
134
|
+
const root_b = await fs.mkdtemp(path.join(os.tmpdir(), "downcity-plugin-isolation-b-"));
|
|
249
135
|
const agent_a = new Agent({
|
|
250
136
|
id: "agent_a",
|
|
251
137
|
path: root_a,
|
|
252
138
|
plugins: [create_owner_plugin("agent_a", executed_owners)],
|
|
253
|
-
model: create_test_model(
|
|
139
|
+
model: create_test_model("model_a", model_requests),
|
|
254
140
|
});
|
|
255
141
|
const agent_b = new Agent({
|
|
256
142
|
id: "agent_b",
|
|
257
143
|
path: root_b,
|
|
258
144
|
plugins: [create_owner_plugin("agent_b", executed_owners)],
|
|
259
|
-
model: create_test_model(
|
|
145
|
+
model: create_test_model("model_b", model_requests),
|
|
260
146
|
});
|
|
261
147
|
|
|
262
148
|
try {
|
|
@@ -266,24 +152,17 @@ test("multiple session prompts use only their owning Agent plugin registry", asy
|
|
|
266
152
|
session_a.prompt({ query: "Call your skill plugin" }),
|
|
267
153
|
session_b.prompt({ query: "Call your skill plugin" }),
|
|
268
154
|
]);
|
|
269
|
-
const [result_a, result_b] = await Promise.all([
|
|
270
|
-
turn_a.finished,
|
|
271
|
-
turn_b.finished,
|
|
272
|
-
]);
|
|
155
|
+
const [result_a, result_b] = await Promise.all([turn_a.finished, turn_b.finished]);
|
|
273
156
|
|
|
274
157
|
assert.equal(result_a.success, true);
|
|
275
158
|
assert.equal(result_b.success, true);
|
|
276
159
|
assert.deepEqual([...executed_owners].sort(), ["agent_a", "agent_b"]);
|
|
277
|
-
for (const [
|
|
278
|
-
[
|
|
279
|
-
["model_b", "agent_b"],
|
|
280
|
-
]) {
|
|
281
|
-
const requests = model_requests.get(model) || [];
|
|
160
|
+
for (const [model_id, owner] of [["model_a", "agent_a"], ["model_b", "agent_b"]]) {
|
|
161
|
+
const requests = model_requests.get(model_id) ?? [];
|
|
282
162
|
assert.equal(requests.length, 2);
|
|
283
|
-
assert.match(JSON.stringify(requests[1].
|
|
163
|
+
assert.match(JSON.stringify(requests[1].prompt), new RegExp(owner));
|
|
284
164
|
}
|
|
285
165
|
} finally {
|
|
286
166
|
await Promise.all([agent_a.dispose(), agent_b.dispose()]);
|
|
287
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
288
167
|
}
|
|
289
168
|
});
|