minimal-agent 0.11.0 → 0.12.0
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/package.json +4 -1
- package/plugins/workflow-runner/plugin.js +27 -15
- package/plugins/workflow-runner/plugin.ts +34 -20
- package/plugins/workflow-runner/src/agentCommon.js +13 -12
- package/plugins/workflow-runner/src/agentCommon.ts +22 -13
- package/plugins/workflow-runner/src/index.js +5 -3
- package/plugins/workflow-runner/src/index.ts +31 -13
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
- package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.ts +26 -7
- package/plugins/workflow-runner/test/context-files.test.ts +47 -16
- package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/src/cli/print.js +19 -4
- package/src/cli/print.ts +17 -4
- package/src/cli/streamJson.js +19 -4
- package/src/cli/streamJson.ts +17 -4
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configFile.js +2 -21
- package/src/config/configFile.ts +11 -24
- package/src/config/configManagement.js +1 -1
- package/src/config/configManagement.ts +2 -2
- package/src/context/compact.js +4 -0
- package/src/context/compact.ts +4 -0
- package/src/context/contextBundle.js +38 -13
- package/src/context/contextBundle.ts +47 -13
- package/src/harness/stallGuard.js +19 -4
- package/src/harness/stallGuard.ts +30 -7
- package/src/loop.js +95 -11
- package/src/loop.ts +125 -16
- package/src/main.js +8 -1
- package/src/main.tsx +7 -1
- package/src/memory/constants.js +10 -0
- package/src/memory/constants.ts +13 -0
- package/src/memory/episodicMemory.js +274 -2
- package/src/memory/episodicMemory.ts +306 -1
- package/src/memory/fileStore.js +24 -4
- package/src/memory/fileStore.ts +22 -2
- package/src/memory/memoryService.js +79 -12
- package/src/memory/memoryService.ts +108 -9
- package/src/memory/repository.js +415 -85
- package/src/memory/repository.ts +437 -86
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.ts +16 -0
- package/src/plugins/pluginApi.ts +30 -6
- package/src/plugins/pluginRunner.js +6 -2
- package/src/plugins/pluginRunner.ts +12 -7
- package/src/prompts/policyRegistry.js +27 -0
- package/src/prompts/policyRegistry.ts +27 -0
- package/src/prompts/promptAssembly.js +39 -7
- package/src/prompts/promptAssembly.ts +145 -59
- package/src/session/runAgentTurn.js +43 -21
- package/src/session/runAgentTurn.ts +68 -26
- package/src/session/runTurn.js +11 -46
- package/src/session/runTurn.ts +12 -46
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +5 -0
- package/src/session/sessionState.ts +14 -0
- package/src/tools/agent/agent.js +143 -14
- package/src/tools/agent/agent.ts +149 -14
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +5 -0
- package/src/tools/agent/agentTypes.ts +5 -0
- package/src/tools/bash/bash.js +31 -11
- package/src/tools/bash/bash.ts +29 -10
- package/src/tools/capabilities.js +5 -0
- package/src/tools/capabilities.ts +11 -1
- package/src/tools/edit/edit.js +7 -4
- package/src/tools/edit/edit.ts +7 -4
- package/src/tools/edit/multi-edit.js +4 -2
- package/src/tools/edit/multi-edit.ts +4 -2
- package/src/tools/glob/glob.js +156 -40
- package/src/tools/glob/glob.ts +185 -46
- package/src/tools/index.js +35 -17
- package/src/tools/index.ts +81 -20
- package/src/tools/lsp/lsp.js +165 -22
- package/src/tools/lsp/lsp.ts +206 -36
- package/src/tools/powershell/powershell.js +116 -26
- package/src/tools/powershell/powershell.ts +155 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +244 -29
- package/src/tools/read/read.ts +391 -59
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +2 -1
- package/src/tools/shared/fileActivity.ts +3 -0
- package/src/tools/shared/permanentDeleteGuard.js +26 -0
- package/src/tools/shared/permanentDeleteGuard.ts +34 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +79 -28
- package/src/tools/tasks/backgroundTaskService.ts +165 -49
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +64 -14
- package/src/tools/tasks/taskOutput.ts +102 -23
- package/src/tools/tasks/taskStop.js +48 -8
- package/src/tools/tasks/taskStop.ts +69 -9
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +6 -2
- package/src/tools/write/write.ts +6 -2
- package/src/types.ts +12 -0
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- package/src/web/protocol.ts +78 -3
|
@@ -15,11 +15,19 @@
|
|
|
15
15
|
* ============================================================
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
afterAll,
|
|
20
|
+
beforeAll,
|
|
21
|
+
beforeEach,
|
|
22
|
+
describe,
|
|
23
|
+
expect,
|
|
24
|
+
it,
|
|
25
|
+
mock,
|
|
26
|
+
} from 'bun:test';
|
|
27
|
+
|
|
28
|
+
import type { Provider, Tool } from '../../../src/types.js';
|
|
20
29
|
import { execLlmStep } from '../src/stepExecutors/llm.js';
|
|
21
30
|
import { VarStack } from '../src/types.js';
|
|
22
|
-
import type { Provider } from '../../../src/types.js';
|
|
23
31
|
import type { RunContext, StepDef } from '../src/types.js';
|
|
24
32
|
|
|
25
33
|
const fakeProvider: Provider = {
|
|
@@ -67,8 +75,11 @@ interface ExecuteToolCall {
|
|
|
67
75
|
}
|
|
68
76
|
const capturedExecuteToolCalls: ExecuteToolCall[] = [];
|
|
69
77
|
/** mock executeTool 返回值脚本(按调用顺序);用尽则回退到 { ok:true, content:'OK' } */
|
|
70
|
-
let mockToolResults: Array<
|
|
78
|
+
let mockToolResults: Array<
|
|
79
|
+
{ ok: true; content: string } | { ok: false; error: string }
|
|
80
|
+
> = [];
|
|
71
81
|
|
|
82
|
+
import { chat as realChatImport } from '../../../src/llm/client.js';
|
|
72
83
|
// 复用真实的 ALL_TOOLS / getToolByName(llm.ts 的白名单过滤要用),
|
|
73
84
|
// 只把 executeTool 替换成 mock 版本。
|
|
74
85
|
//
|
|
@@ -80,7 +91,6 @@ import {
|
|
|
80
91
|
executeTool as realExecuteTool,
|
|
81
92
|
getToolByName as realGetToolByName,
|
|
82
93
|
} from '../../../src/tools/index.js';
|
|
83
|
-
import { chat as realChatImport } from '../../../src/llm/client.js';
|
|
84
94
|
|
|
85
95
|
// 在 mock 安装前把真实 chat 函数引用拷到值变量,避免 ESM live binding
|
|
86
96
|
// 在 mock.module 后跟着变成 mock 版本(afterAll 复原才能拿到真函数)
|
|
@@ -140,7 +150,10 @@ function installChatMock(): void {
|
|
|
140
150
|
argumentsDelta: turn.argsJson,
|
|
141
151
|
};
|
|
142
152
|
}
|
|
143
|
-
yield {
|
|
153
|
+
yield {
|
|
154
|
+
type: 'done',
|
|
155
|
+
stopReason: turn.kind === 'tool_call' ? 'tool_calls' : 'stop',
|
|
156
|
+
};
|
|
144
157
|
},
|
|
145
158
|
}));
|
|
146
159
|
}
|
|
@@ -156,7 +169,12 @@ describe('workflow-runner context_files (ADR-05 阶段 4)', () => {
|
|
|
156
169
|
mockTurns = [];
|
|
157
170
|
mockTurnIdx = 0;
|
|
158
171
|
mockToolResults = [];
|
|
159
|
-
ctx = {
|
|
172
|
+
ctx = {
|
|
173
|
+
provider: fakeProvider,
|
|
174
|
+
history: [],
|
|
175
|
+
cwd: process.cwd(),
|
|
176
|
+
tools: [],
|
|
177
|
+
};
|
|
160
178
|
});
|
|
161
179
|
|
|
162
180
|
it('pure llm 不下发工具,避免单轮设计类步骤误调工具后撞 maxTurns=1', async () => {
|
|
@@ -175,6 +193,7 @@ describe('workflow-runner context_files (ADR-05 阶段 4)', () => {
|
|
|
175
193
|
});
|
|
176
194
|
|
|
177
195
|
it('case 1: 基本 Read 引用 — tool_call 一轮 + 纯文本一轮', async () => {
|
|
196
|
+
ctx.tools = [{ name: 'Read' } as Tool];
|
|
178
197
|
mockTurns = [
|
|
179
198
|
{
|
|
180
199
|
kind: 'tool_call',
|
|
@@ -190,8 +209,7 @@ describe('workflow-runner context_files (ADR-05 阶段 4)', () => {
|
|
|
190
209
|
id: 'read-and-answer',
|
|
191
210
|
llm: 'please summarize',
|
|
192
211
|
context_files: [{ path: '/tmp/x.txt', hint: 'source data' }],
|
|
193
|
-
//
|
|
194
|
-
// 白名单把下发工具收敛到 Read(验证 loop.ts 的 allowedTools 过滤 + llm.ts 透传)。
|
|
212
|
+
// context_files 经 runQuery(TAOR) 执行;步骤白名单与宿主有效工具集取交集。
|
|
195
213
|
allowed_tools: ['Read'],
|
|
196
214
|
};
|
|
197
215
|
|
|
@@ -212,8 +230,9 @@ describe('workflow-runner context_files (ADR-05 阶段 4)', () => {
|
|
|
212
230
|
expect(parsedArgs.file_path).toBe('/tmp/x.txt');
|
|
213
231
|
|
|
214
232
|
// 第一轮 user message 包含路径和 hint
|
|
215
|
-
const firstUserMsg = (
|
|
216
|
-
.
|
|
233
|
+
const firstUserMsg = (
|
|
234
|
+
capturedChatArgs[0].messages as Array<{ role: string; content: string }>
|
|
235
|
+
).find((m) => m.role === 'user');
|
|
217
236
|
expect(firstUserMsg).toBeDefined();
|
|
218
237
|
expect(firstUserMsg!.content).toContain('/tmp/x.txt');
|
|
219
238
|
expect(firstUserMsg!.content).toContain('source data');
|
|
@@ -225,12 +244,23 @@ describe('workflow-runner context_files (ADR-05 阶段 4)', () => {
|
|
|
225
244
|
});
|
|
226
245
|
|
|
227
246
|
it('case 2: 永不收敛(每轮都调工具)→ 被 maxTurns 严格兜住,不无限循环', async () => {
|
|
247
|
+
ctx.tools = [{ name: 'Read' } as Tool];
|
|
228
248
|
// v0.9 重构移除了 miniReAct 的"重复工具调用早停",改由主 TAOR 引擎(runQuery)的
|
|
229
249
|
// maxTurns 硬上限兜底。本用例验证安全属性:模型每轮都调工具、永不收敛时,chat 调用
|
|
230
250
|
// 次数被 maxTurns 严格限制(不会无限循环),不再依赖已移除的重复检测。
|
|
231
251
|
mockTurns = [
|
|
232
|
-
{
|
|
233
|
-
|
|
252
|
+
{
|
|
253
|
+
kind: 'tool_call',
|
|
254
|
+
callId: 'c1',
|
|
255
|
+
name: 'Read',
|
|
256
|
+
argsJson: '{"file_path":"/a"}',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
kind: 'tool_call',
|
|
260
|
+
callId: 'c2',
|
|
261
|
+
name: 'Read',
|
|
262
|
+
argsJson: '{"file_path":"/a"}',
|
|
263
|
+
},
|
|
234
264
|
];
|
|
235
265
|
mockToolResults = [
|
|
236
266
|
{ ok: true, content: 'A' },
|
|
@@ -246,13 +276,14 @@ describe('workflow-runner context_files (ADR-05 阶段 4)', () => {
|
|
|
246
276
|
|
|
247
277
|
// 不无限循环:chat 调用次数被 maxTurns(=2) 严格兜住。
|
|
248
278
|
// 撞顶后的半截产出不可信,execLlmStep 必须抛错触发 runner retry/halt。
|
|
249
|
-
await expect(
|
|
250
|
-
|
|
251
|
-
);
|
|
279
|
+
await expect(
|
|
280
|
+
drainLlm(execLlmStep(step, new VarStack(), ctx)),
|
|
281
|
+
).rejects.toThrow('达到最大轮数 2 被截断');
|
|
252
282
|
expect(capturedChatArgs.length).toBe(2);
|
|
253
283
|
});
|
|
254
284
|
|
|
255
285
|
it('case 3: output_schema + tools — 走 [json+tools] 前缀', async () => {
|
|
286
|
+
ctx.tools = [{ name: 'Read' } as Tool];
|
|
256
287
|
mockTurns = [
|
|
257
288
|
{
|
|
258
289
|
kind: 'tool_call',
|
|
@@ -25,6 +25,7 @@ describe('框架侧:runCommand 必须带上 host', () => {
|
|
|
25
25
|
expect(call).toContain('host:');
|
|
26
26
|
expect(call).toContain('computerRun');
|
|
27
27
|
expect(call).toContain('browserRun');
|
|
28
|
+
expect(call).toContain('executeTool: options.executeTool');
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
test('PluginContext 用 Pick<ToolCallContext> 绑定,而不是重新声明', async () => {
|
|
@@ -40,9 +41,14 @@ describe('能力常量单一来源', () => {
|
|
|
40
41
|
test('loader 的白名单与 types 的 HostCapability 一致', async () => {
|
|
41
42
|
// 两处不一致的表现是:yaml 写了某个能力、loader 放行、运行时却识别不了,
|
|
42
43
|
// 或反过来 loader 拦掉一个其实支持的能力。都属于静默错。
|
|
43
|
-
const types = await Bun.file(
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const types = await Bun.file(
|
|
45
|
+
new URL('../src/types.ts', import.meta.url),
|
|
46
|
+
).text();
|
|
47
|
+
const declared =
|
|
48
|
+
/export type HostCapability =([^;]+);/.exec(types)?.[1] ?? '';
|
|
49
|
+
const fromTypes = [...declared.matchAll(/'([a-z]+)'/g)]
|
|
50
|
+
.map((m) => m[1])
|
|
51
|
+
.sort();
|
|
46
52
|
expect(fromTypes).toEqual([...VALID_HOST_CAPABILITIES].sort());
|
|
47
53
|
});
|
|
48
54
|
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { describe, expect, it } from 'bun:test';
|
|
7
7
|
|
|
8
|
+
import { ALL_TOOLS } from '../../../src/tools/index.js';
|
|
8
9
|
import type {
|
|
9
10
|
ChatContentPart,
|
|
10
11
|
LlmStreamEvent,
|
|
@@ -16,7 +17,6 @@ import { execAgentStep } from '../src/stepExecutors/agent.js';
|
|
|
16
17
|
import { execLlmStep } from '../src/stepExecutors/llm.js';
|
|
17
18
|
import { VarStack } from '../src/types.js';
|
|
18
19
|
import type { RunContext, StepDef } from '../src/types.js';
|
|
19
|
-
import { ALL_TOOLS } from '../../../src/tools/index.js';
|
|
20
20
|
|
|
21
21
|
const provider: Provider = {
|
|
22
22
|
name: 'test',
|
|
@@ -72,7 +72,11 @@ describe('workflow-runner multimodal images', () => {
|
|
|
72
72
|
const chat = async function* (): AsyncGenerator<LlmStreamEvent> {
|
|
73
73
|
if (round++ === 0) {
|
|
74
74
|
yield { type: 'tool_call_delta', index: 0, id: 'hidden', name: 'Bash' };
|
|
75
|
-
yield {
|
|
75
|
+
yield {
|
|
76
|
+
type: 'tool_call_delta',
|
|
77
|
+
index: 0,
|
|
78
|
+
argumentsDelta: '{"command":"echo no"}',
|
|
79
|
+
};
|
|
76
80
|
yield { type: 'done', stopReason: 'tool_calls' };
|
|
77
81
|
return;
|
|
78
82
|
}
|
|
@@ -90,15 +94,58 @@ describe('workflow-runner multimodal images', () => {
|
|
|
90
94
|
return { ok: true, content: 'unexpected' };
|
|
91
95
|
},
|
|
92
96
|
};
|
|
93
|
-
const result = await drain(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
const result = await drain(
|
|
98
|
+
execAgentStep(
|
|
99
|
+
{ id: 'bounded', agent: '完成任务', allowed_tools: ['Bash'] },
|
|
100
|
+
new VarStack(),
|
|
101
|
+
ctx,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
98
104
|
expect(executed).toBe(0);
|
|
99
105
|
expect(result.raw.text).toBe('已改道。');
|
|
100
106
|
});
|
|
101
107
|
|
|
108
|
+
it('an empty host tool catalog remains empty instead of restoring global tools', async () => {
|
|
109
|
+
let round = 0;
|
|
110
|
+
let executed = 0;
|
|
111
|
+
const chat = async function* (): AsyncGenerator<LlmStreamEvent> {
|
|
112
|
+
if (round++ === 0) {
|
|
113
|
+
yield { type: 'tool_call_delta', index: 0, id: 'hidden', name: 'Read' };
|
|
114
|
+
yield {
|
|
115
|
+
type: 'tool_call_delta',
|
|
116
|
+
index: 0,
|
|
117
|
+
argumentsDelta: '{"file_path":"README.md"}',
|
|
118
|
+
};
|
|
119
|
+
yield { type: 'done', stopReason: 'tool_calls' };
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
yield { type: 'text_delta', delta: '没有可用工具。' };
|
|
123
|
+
yield { type: 'done', stopReason: 'stop' };
|
|
124
|
+
};
|
|
125
|
+
const ctx: RunContext = {
|
|
126
|
+
provider,
|
|
127
|
+
history: [{ role: 'system', content: 'stable' }],
|
|
128
|
+
cwd: process.cwd(),
|
|
129
|
+
tools: [],
|
|
130
|
+
chat,
|
|
131
|
+
executeTool: async () => {
|
|
132
|
+
executed += 1;
|
|
133
|
+
return { ok: true, content: 'unexpected' };
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const result = await drain(
|
|
138
|
+
execAgentStep(
|
|
139
|
+
{ id: 'empty-bound', agent: '完成任务', allowed_tools: ['Read'] },
|
|
140
|
+
new VarStack(),
|
|
141
|
+
ctx,
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(executed).toBe(0);
|
|
146
|
+
expect(result.raw.text).toBe('没有可用工具。');
|
|
147
|
+
});
|
|
148
|
+
|
|
102
149
|
it('execLlmStep 把 images 传成 image_url content block,并支持变量插值', async () => {
|
|
103
150
|
const captured: Message[][] = [];
|
|
104
151
|
const ctx = createContext(captured);
|
|
@@ -195,7 +242,9 @@ describe('workflow-runner multimodal images', () => {
|
|
|
195
242
|
|
|
196
243
|
expect(result.raw.text).toBe('图片可用');
|
|
197
244
|
expect(messagesContainImage(captured[0])).toBe(true);
|
|
198
|
-
expect(ctx.history).toEqual([
|
|
245
|
+
expect(ctx.history).toEqual([
|
|
246
|
+
{ role: 'system', content: 'workflow system prompt' },
|
|
247
|
+
]);
|
|
199
248
|
});
|
|
200
249
|
|
|
201
250
|
it('isolated 多模态步骤之后的普通 agent 不会收到前一步 image_url', async () => {
|
|
@@ -262,7 +311,7 @@ describe('workflow-runner multimodal images', () => {
|
|
|
262
311
|
provider,
|
|
263
312
|
history: [{ role: 'system', content: 'workflow system prompt' }],
|
|
264
313
|
cwd: process.cwd(),
|
|
265
|
-
tools:
|
|
314
|
+
tools: ALL_TOOLS.filter((tool) => tool.name === 'Write'),
|
|
266
315
|
chat,
|
|
267
316
|
executeTool: async () => {
|
|
268
317
|
toolCalled = true;
|
|
@@ -285,7 +334,9 @@ describe('workflow-runner multimodal images', () => {
|
|
|
285
334
|
|
|
286
335
|
expect(toolCalled).toBe(true);
|
|
287
336
|
expect(result.raw.text).toBe('文件已写好。');
|
|
288
|
-
expect(ctx.history).toEqual([
|
|
337
|
+
expect(ctx.history).toEqual([
|
|
338
|
+
{ role: 'system', content: 'workflow system prompt' },
|
|
339
|
+
]);
|
|
289
340
|
expect(captured.length).toBe(2);
|
|
290
341
|
});
|
|
291
342
|
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ALL_TOOLS,
|
|
5
|
+
type LoopEvent,
|
|
6
|
+
type Provider,
|
|
7
|
+
type ToolCallContext,
|
|
8
|
+
type ToolResult,
|
|
9
|
+
createAgentRuntimeContext,
|
|
10
|
+
createSessionState,
|
|
11
|
+
} from '../../../src/plugin-sdk.js';
|
|
12
|
+
import { execToolStep } from '../src/stepExecutors/tool.js';
|
|
13
|
+
import { type RunContext, type StepResult, VarStack } from '../src/types.js';
|
|
14
|
+
|
|
15
|
+
const provider: Provider = {
|
|
16
|
+
name: 'workflow-tool-test',
|
|
17
|
+
baseURL: '',
|
|
18
|
+
apiKey: '',
|
|
19
|
+
model: 'fixture',
|
|
20
|
+
contextWindow: 128_000,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async function collect(
|
|
24
|
+
generator: AsyncGenerator<LoopEvent, StepResult, void>,
|
|
25
|
+
): Promise<{ events: LoopEvent[]; result: StepResult }> {
|
|
26
|
+
const events: LoopEvent[] = [];
|
|
27
|
+
while (true) {
|
|
28
|
+
const next = await generator.next();
|
|
29
|
+
if (next.done) return { events, result: next.value };
|
|
30
|
+
events.push(next.value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeContext(
|
|
35
|
+
executeTool: NonNullable<RunContext['executeTool']>,
|
|
36
|
+
): RunContext {
|
|
37
|
+
const sessionState = createSessionState();
|
|
38
|
+
sessionState.activeWorktree = {
|
|
39
|
+
id: 'wt_test',
|
|
40
|
+
path: '/tmp/workflow-active-worktree',
|
|
41
|
+
branch: 'workflow/test',
|
|
42
|
+
baseSha: 'deadbeef',
|
|
43
|
+
gitRoot: '/tmp/workflow-root',
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
provider,
|
|
47
|
+
history: [],
|
|
48
|
+
cwd: '/tmp/workflow-root',
|
|
49
|
+
tools: ALL_TOOLS,
|
|
50
|
+
sessionState,
|
|
51
|
+
runtimeContext: createAgentRuntimeContext({
|
|
52
|
+
surface: 'workflow',
|
|
53
|
+
profile: 'main',
|
|
54
|
+
cwd: '/tmp/workflow-root',
|
|
55
|
+
workspaceId: 'workspace_test',
|
|
56
|
+
sessionId: 'session_test',
|
|
57
|
+
owner: 'local-owner',
|
|
58
|
+
memoryAccess: 'read-only',
|
|
59
|
+
interactiveAsk: false,
|
|
60
|
+
}),
|
|
61
|
+
promptCache: {
|
|
62
|
+
scopeKey: 'workflow-test-scope',
|
|
63
|
+
conversationKey: 'workflow-test-conversation',
|
|
64
|
+
},
|
|
65
|
+
executeTool,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('workflow deterministic tool runtime', () => {
|
|
70
|
+
test('all registered tools use the same generic deterministic node dispatcher', async () => {
|
|
71
|
+
const dispatched: string[] = [];
|
|
72
|
+
const ctx = makeContext(async (name) => {
|
|
73
|
+
dispatched.push(name);
|
|
74
|
+
return { ok: true, content: `${name} ok` };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
for (const tool of ALL_TOOLS) {
|
|
78
|
+
const { result } = await collect(
|
|
79
|
+
execToolStep(
|
|
80
|
+
{ id: `catalog_${tool.name}`, tool: tool.name, args: {} },
|
|
81
|
+
new VarStack(),
|
|
82
|
+
ctx,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
expect(result.raw).toMatchObject({ content: `${tool.name} ok` });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
expect(dispatched).toEqual(ALL_TOOLS.map((tool) => tool.name));
|
|
89
|
+
expect(new Set(dispatched).size).toBe(ALL_TOOLS.length);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('passes canonical runtime context and follows the active worktree cwd', async () => {
|
|
93
|
+
let captured: ToolCallContext | undefined;
|
|
94
|
+
const ctx = makeContext(async (_name, _args, _signal, context) => {
|
|
95
|
+
captured = context;
|
|
96
|
+
return { ok: true, content: 'ok' };
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await collect(
|
|
100
|
+
execToolStep(
|
|
101
|
+
{ id: 'context', tool: 'Read', args: { file_path: 'README.md' } },
|
|
102
|
+
new VarStack(),
|
|
103
|
+
ctx,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(captured).toBeDefined();
|
|
108
|
+
expect(captured?.provider).toBe(provider);
|
|
109
|
+
expect(captured?.history).toBe(ctx.history);
|
|
110
|
+
expect(captured?.sessionState).toBe(ctx.sessionState);
|
|
111
|
+
expect(captured?.workspaceId).toBe('workspace_test');
|
|
112
|
+
expect(captured?.sessionId).toBe('session_test');
|
|
113
|
+
expect(captured?.memoryAccess).toBe('read-only');
|
|
114
|
+
expect(captured?.cwd).toBe('/tmp/workflow-active-worktree');
|
|
115
|
+
expect(captured?.toolCallId).toBe('wf_context');
|
|
116
|
+
expect(captured?.promptCache).toBe(ctx.promptCache);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('streams tool progress and preserves structured result fields', async () => {
|
|
120
|
+
const metadata = {
|
|
121
|
+
kind: 'process_execution' as const,
|
|
122
|
+
shell: 'bash' as const,
|
|
123
|
+
mode: 'foreground' as const,
|
|
124
|
+
commandKind: 'test' as const,
|
|
125
|
+
status: 'completed' as const,
|
|
126
|
+
exitCode: 0,
|
|
127
|
+
};
|
|
128
|
+
const ctx = makeContext(async (_name, _args, _signal, context) => {
|
|
129
|
+
context?.reportProgress?.('running tests');
|
|
130
|
+
await Promise.resolve();
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
content: 'tests passed',
|
|
134
|
+
metadata,
|
|
135
|
+
recovery: 'none',
|
|
136
|
+
sensitivity: 'local',
|
|
137
|
+
visionNote: 'verification image',
|
|
138
|
+
} satisfies ToolResult;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const { events, result } = await collect(
|
|
142
|
+
execToolStep(
|
|
143
|
+
{ id: 'progress', tool: 'Bash', args: { command: 'bun test' } },
|
|
144
|
+
new VarStack(),
|
|
145
|
+
ctx,
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(events.map((event) => event.type)).toEqual([
|
|
150
|
+
'tool_start',
|
|
151
|
+
'tool_progress',
|
|
152
|
+
'tool_end',
|
|
153
|
+
]);
|
|
154
|
+
expect(events[1]).toMatchObject({
|
|
155
|
+
type: 'tool_progress',
|
|
156
|
+
toolName: 'Bash',
|
|
157
|
+
toolCallId: 'wf_progress',
|
|
158
|
+
message: 'running tests',
|
|
159
|
+
});
|
|
160
|
+
expect(events[2]).toMatchObject({ type: 'tool_end', metadata });
|
|
161
|
+
expect(result.raw).toMatchObject({
|
|
162
|
+
metadata,
|
|
163
|
+
recovery: 'none',
|
|
164
|
+
sensitivity: 'local',
|
|
165
|
+
visionNote: 'verification image',
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
package/src/cli/print.js
CHANGED
|
@@ -27,7 +27,9 @@ import { updateSessionMetadata } from '../context/sessionRegistry.js';
|
|
|
27
27
|
import { workspaceIndexService } from '../context/workspaceIndex.js';
|
|
28
28
|
import { normalizeImageDetail } from '../multimodal.js';
|
|
29
29
|
import { runWithPlugins } from '../plugins/pluginRunner.js';
|
|
30
|
-
import { buildTransientPromptBundle } from '../prompts/system.js';
|
|
30
|
+
import { buildBaseSystemPrompt, buildTransientPromptBundle } from '../prompts/system.js';
|
|
31
|
+
import { getOrCreateSessionState, sessionStateKey } from '../session/sessionState.js';
|
|
32
|
+
import { hydrateSessionWorktree } from '../tools/worktree/sessionWorktree.js';
|
|
31
33
|
import { ALL_TOOLS } from '../tools/index.js';
|
|
32
34
|
import { createPromptCacheContext } from '../llm/promptCache.js';
|
|
33
35
|
import { createAgentRuntimeContext, filterToolsForRuntime, memoryContextFromRuntime, } from '../runtime/agentRuntimeContext.js';
|
|
@@ -232,10 +234,16 @@ export async function runPrintMode(provider, args, initialHistory, options) {
|
|
|
232
234
|
void finalizeOnce().finally(() => process.exit(130));
|
|
233
235
|
});
|
|
234
236
|
try {
|
|
235
|
-
const
|
|
236
|
-
const workspace = await workspaceIndexService.findByPath(
|
|
237
|
+
const workspaceRoot = options.workspace?.cwd ?? process.cwd();
|
|
238
|
+
const workspace = await workspaceIndexService.findByPath(workspaceRoot).catch(() => null);
|
|
237
239
|
const sessionId = options.workspace?.sessionId ?? 'default';
|
|
238
|
-
const
|
|
240
|
+
const sessionState = getOrCreateSessionState(sessionStateKey({
|
|
241
|
+
surface: 'cli', workspaceId: workspace?.id,
|
|
242
|
+
cwd: workspace?.id ? undefined : workspaceRoot, sessionId,
|
|
243
|
+
}));
|
|
244
|
+
await hydrateSessionWorktree({ sessionState, workspaceId: workspace?.id, sessionId, cwd: workspaceRoot });
|
|
245
|
+
const cwd = sessionState.activeWorktree?.path ?? workspaceRoot;
|
|
246
|
+
const conversationKey = `workspace:${workspace?.id ?? workspaceRoot}:session:${sessionId}`;
|
|
239
247
|
const computerRun = options.computerUseMode && options.computerUseMode !== 'disabled'
|
|
240
248
|
? createCliComputerRun({
|
|
241
249
|
mode: options.computerUseMode,
|
|
@@ -259,6 +267,11 @@ export async function runPrintMode(provider, args, initialHistory, options) {
|
|
|
259
267
|
computer: computerRun,
|
|
260
268
|
});
|
|
261
269
|
const turnTools = filterToolsForRuntime(ALL_TOOLS, runtimeContext);
|
|
270
|
+
const stablePrompt = await buildBaseSystemPrompt(cwd, turnTools);
|
|
271
|
+
if (history[0]?.role === 'system')
|
|
272
|
+
history[0] = { ...history[0], content: stablePrompt };
|
|
273
|
+
else
|
|
274
|
+
history.unshift({ role: 'system', content: stablePrompt });
|
|
262
275
|
const transientBundle = await buildTransientPromptBundle({
|
|
263
276
|
cwd,
|
|
264
277
|
tools: turnTools,
|
|
@@ -266,6 +279,7 @@ export async function runPrintMode(provider, args, initialHistory, options) {
|
|
|
266
279
|
surface: 'cli',
|
|
267
280
|
cwd,
|
|
268
281
|
workspaceId: workspace?.id,
|
|
282
|
+
sessionId,
|
|
269
283
|
interactiveAsk: false,
|
|
270
284
|
memoryAccess: runtimeContext.memoryAccess,
|
|
271
285
|
currentUserInput: prompt,
|
|
@@ -282,6 +296,7 @@ export async function runPrintMode(provider, args, initialHistory, options) {
|
|
|
282
296
|
imageMaxLongSidePixel: imageOptions.imageMaxLongSidePixel,
|
|
283
297
|
allowedTools: turnTools.map((tool) => tool.name),
|
|
284
298
|
runtimeContext,
|
|
299
|
+
sessionState,
|
|
285
300
|
...(transientBundle ? { transientBundle } : {}),
|
|
286
301
|
promptCache: createPromptCacheContext(`workspace:${workspace?.id ?? cwd}:session:${sessionId}`),
|
|
287
302
|
memoryContext: memoryContextFromRuntime(runtimeContext),
|
package/src/cli/print.ts
CHANGED
|
@@ -29,7 +29,9 @@ import { workspaceIndexService } from '../context/workspaceIndex.js';
|
|
|
29
29
|
import { normalizeImageDetail } from '../multimodal.js';
|
|
30
30
|
import type { DeliverableManifest, FileActivityMetadata, HarnessDiagnostics, ImageInputSpec, LoopEvent, Message, Provider } from '../types.js';
|
|
31
31
|
import { runWithPlugins } from '../plugins/pluginRunner.js';
|
|
32
|
-
import { buildTransientPromptBundle } from '../prompts/system.js';
|
|
32
|
+
import { buildBaseSystemPrompt, buildTransientPromptBundle } from '../prompts/system.js';
|
|
33
|
+
import { getOrCreateSessionState, sessionStateKey } from '../session/sessionState.js';
|
|
34
|
+
import { hydrateSessionWorktree } from '../tools/worktree/sessionWorktree.js';
|
|
33
35
|
import { ALL_TOOLS } from '../tools/index.js';
|
|
34
36
|
import { createPromptCacheContext } from '../llm/promptCache.js';
|
|
35
37
|
import {
|
|
@@ -325,10 +327,16 @@ export async function runPrintMode(
|
|
|
325
327
|
});
|
|
326
328
|
|
|
327
329
|
try {
|
|
328
|
-
const
|
|
329
|
-
const workspace = await workspaceIndexService.findByPath(
|
|
330
|
+
const workspaceRoot = options.workspace?.cwd ?? process.cwd();
|
|
331
|
+
const workspace = await workspaceIndexService.findByPath(workspaceRoot).catch(() => null);
|
|
330
332
|
const sessionId = options.workspace?.sessionId ?? 'default';
|
|
331
|
-
const
|
|
333
|
+
const sessionState = getOrCreateSessionState(sessionStateKey({
|
|
334
|
+
surface: 'cli', workspaceId: workspace?.id,
|
|
335
|
+
cwd: workspace?.id ? undefined : workspaceRoot, sessionId,
|
|
336
|
+
}));
|
|
337
|
+
await hydrateSessionWorktree({ sessionState, workspaceId: workspace?.id, sessionId, cwd: workspaceRoot });
|
|
338
|
+
const cwd = sessionState.activeWorktree?.path ?? workspaceRoot;
|
|
339
|
+
const conversationKey = `workspace:${workspace?.id ?? workspaceRoot}:session:${sessionId}`;
|
|
332
340
|
const computerRun = options.computerUseMode && options.computerUseMode !== 'disabled'
|
|
333
341
|
? createCliComputerRun({
|
|
334
342
|
mode: options.computerUseMode,
|
|
@@ -352,6 +360,9 @@ export async function runPrintMode(
|
|
|
352
360
|
computer: computerRun,
|
|
353
361
|
});
|
|
354
362
|
const turnTools = filterToolsForRuntime(ALL_TOOLS, runtimeContext);
|
|
363
|
+
const stablePrompt = await buildBaseSystemPrompt(cwd, turnTools);
|
|
364
|
+
if (history[0]?.role === 'system') history[0] = { ...history[0], content: stablePrompt };
|
|
365
|
+
else history.unshift({ role: 'system', content: stablePrompt });
|
|
355
366
|
const transientBundle = await buildTransientPromptBundle({
|
|
356
367
|
cwd,
|
|
357
368
|
tools: turnTools,
|
|
@@ -359,6 +370,7 @@ export async function runPrintMode(
|
|
|
359
370
|
surface: 'cli',
|
|
360
371
|
cwd,
|
|
361
372
|
workspaceId: workspace?.id,
|
|
373
|
+
sessionId,
|
|
362
374
|
interactiveAsk: false,
|
|
363
375
|
memoryAccess: runtimeContext.memoryAccess,
|
|
364
376
|
currentUserInput: prompt,
|
|
@@ -375,6 +387,7 @@ export async function runPrintMode(
|
|
|
375
387
|
imageMaxLongSidePixel: imageOptions.imageMaxLongSidePixel,
|
|
376
388
|
allowedTools: turnTools.map((tool) => tool.name),
|
|
377
389
|
runtimeContext,
|
|
390
|
+
sessionState,
|
|
378
391
|
...(transientBundle ? { transientBundle } : {}),
|
|
379
392
|
promptCache: createPromptCacheContext(
|
|
380
393
|
`workspace:${workspace?.id ?? cwd}:session:${sessionId}`,
|
package/src/cli/streamJson.js
CHANGED
|
@@ -28,7 +28,9 @@ import { saveContext } from '../context/persistContext.js';
|
|
|
28
28
|
import { setAskHandler } from '../askBridge.js';
|
|
29
29
|
import { normalizeImageDetail } from '../multimodal.js';
|
|
30
30
|
import { runWithPlugins } from '../plugins/pluginRunner.js';
|
|
31
|
-
import { buildTransientPromptBundle } from '../prompts/system.js';
|
|
31
|
+
import { buildBaseSystemPrompt, buildTransientPromptBundle } from '../prompts/system.js';
|
|
32
|
+
import { getOrCreateSessionState, sessionStateKey } from '../session/sessionState.js';
|
|
33
|
+
import { hydrateSessionWorktree } from '../tools/worktree/sessionWorktree.js';
|
|
32
34
|
import { ALL_TOOLS } from '../tools/index.js';
|
|
33
35
|
import { createPromptCacheContext } from '../llm/promptCache.js';
|
|
34
36
|
import { createAgentRuntimeContext, filterToolsForRuntime, memoryContextFromRuntime, } from '../runtime/agentRuntimeContext.js';
|
|
@@ -267,10 +269,16 @@ export async function runStreamJsonMode(provider, args, initialHistory, options)
|
|
|
267
269
|
void finalize().finally(() => process.exit(130));
|
|
268
270
|
});
|
|
269
271
|
try {
|
|
270
|
-
const
|
|
271
|
-
const workspace = await workspaceIndexService.findByPath(
|
|
272
|
+
const workspaceRoot = options.workspace?.cwd ?? getWorkingDir();
|
|
273
|
+
const workspace = await workspaceIndexService.findByPath(workspaceRoot).catch(() => null);
|
|
272
274
|
const sessionId = options.workspace?.sessionId ?? 'default';
|
|
273
|
-
const
|
|
275
|
+
const sessionState = getOrCreateSessionState(sessionStateKey({
|
|
276
|
+
surface: 'headless', workspaceId: workspace?.id,
|
|
277
|
+
cwd: workspace?.id ? undefined : workspaceRoot, sessionId,
|
|
278
|
+
}));
|
|
279
|
+
await hydrateSessionWorktree({ sessionState, workspaceId: workspace?.id, sessionId, cwd: workspaceRoot });
|
|
280
|
+
const cwd = sessionState.activeWorktree?.path ?? workspaceRoot;
|
|
281
|
+
const conversationKey = `workspace:${workspace?.id ?? workspaceRoot}:session:${sessionId}`;
|
|
274
282
|
const computerRun = options.computerUseMode && options.computerUseMode !== 'disabled'
|
|
275
283
|
? createCliComputerRun({
|
|
276
284
|
mode: options.computerUseMode,
|
|
@@ -294,6 +302,11 @@ export async function runStreamJsonMode(provider, args, initialHistory, options)
|
|
|
294
302
|
computer: computerRun,
|
|
295
303
|
});
|
|
296
304
|
const turnTools = filterToolsForRuntime(ALL_TOOLS, runtimeContext);
|
|
305
|
+
const stablePrompt = await buildBaseSystemPrompt(cwd, turnTools);
|
|
306
|
+
if (history[0]?.role === 'system')
|
|
307
|
+
history[0] = { ...history[0], content: stablePrompt };
|
|
308
|
+
else
|
|
309
|
+
history.unshift({ role: 'system', content: stablePrompt });
|
|
297
310
|
const transientBundle = await buildTransientPromptBundle({
|
|
298
311
|
cwd,
|
|
299
312
|
tools: turnTools,
|
|
@@ -301,6 +314,7 @@ export async function runStreamJsonMode(provider, args, initialHistory, options)
|
|
|
301
314
|
surface: 'headless',
|
|
302
315
|
cwd,
|
|
303
316
|
workspaceId: workspace?.id,
|
|
317
|
+
sessionId,
|
|
304
318
|
interactiveAsk: control,
|
|
305
319
|
memoryAccess: runtimeContext.memoryAccess,
|
|
306
320
|
currentUserInput: prompt,
|
|
@@ -316,6 +330,7 @@ export async function runStreamJsonMode(provider, args, initialHistory, options)
|
|
|
316
330
|
imageDetail: cliImageOptions.imageDetail,
|
|
317
331
|
imageMaxLongSidePixel: cliImageOptions.imageMaxLongSidePixel,
|
|
318
332
|
runtimeContext,
|
|
333
|
+
sessionState,
|
|
319
334
|
allowedTools: turnTools.map((tool) => tool.name),
|
|
320
335
|
...(transientBundle ? { transientBundle } : {}),
|
|
321
336
|
promptCache: createPromptCacheContext(`workspace:${workspace?.id ?? cwd}:session:${sessionId}`),
|