@losclaws/cli 0.1.1 → 0.1.3
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/README.md +33 -2
- package/package.json +5 -2
- package/src/acp-runtime.js +1083 -0
- package/src/cli.js +179 -3
- package/src/config.js +25 -2
- package/src/inspect.js +1 -1
- package/src/workshop-local.js +785 -0
- package/test/acp-runtime.test.js +231 -0
- package/test/config.test.js +6 -1
- package/test/inspect.test.js +26 -0
- package/test/workshop-local.test.js +83 -0
- package/testdata/mock-acp-agent.js +135 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import { loadConfig } from '../src/config.js';
|
|
9
|
+
import {
|
|
10
|
+
buildTaskPrompt,
|
|
11
|
+
resolveAcpRuntime,
|
|
12
|
+
resolveServerAcpDescriptor,
|
|
13
|
+
runWorkshopAcpTask,
|
|
14
|
+
} from '../src/acp-runtime.js';
|
|
15
|
+
|
|
16
|
+
test('resolveServerAcpDescriptor reads the server-composed task.acp', () => {
|
|
17
|
+
const descriptor = resolveServerAcpDescriptor({
|
|
18
|
+
acp: { key: 'opencode', command: 'opencode', args: ['acp'] },
|
|
19
|
+
});
|
|
20
|
+
assert.deepEqual(descriptor, { key: 'opencode', command: 'opencode', args: ['acp'] });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('resolveAcpRuntime builds a runtime from task.acp', () => {
|
|
24
|
+
const resolved = resolveAcpRuntime({
|
|
25
|
+
task: { acp: { key: 'opencode', command: 'opencode', args: ['acp'], model: 'gpt-5.4' } },
|
|
26
|
+
options: {},
|
|
27
|
+
});
|
|
28
|
+
assert.equal(resolved.name, 'opencode');
|
|
29
|
+
assert.equal(resolved.runtime.command, 'opencode');
|
|
30
|
+
assert.deepEqual(resolved.runtime.args, ['acp']);
|
|
31
|
+
assert.equal(resolved.runtime.model, 'gpt-5.4');
|
|
32
|
+
assert.equal(resolved.source, 'server');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('resolveAcpRuntime throws when the server returns no coder runtime', () => {
|
|
36
|
+
assert.throws(() => resolveAcpRuntime({ task: {}, options: {} }), /no coder runtime/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('buildTaskPrompt includes artifact and skill disclosure', () => {
|
|
40
|
+
const result = buildTaskPrompt({
|
|
41
|
+
role: 'worker',
|
|
42
|
+
task: {
|
|
43
|
+
id: 'tsk_123',
|
|
44
|
+
title: 'Write docs',
|
|
45
|
+
description: 'Update the project guide.',
|
|
46
|
+
inputArtifacts: [{ id: 'art_1' }],
|
|
47
|
+
},
|
|
48
|
+
localState: {
|
|
49
|
+
rootPath: '/repo',
|
|
50
|
+
artifacts: [
|
|
51
|
+
{ id: 'art_1', relativePath: 'artifacts/project/guide.md' },
|
|
52
|
+
],
|
|
53
|
+
skills: [
|
|
54
|
+
{ role: 'worker', path: 'skills/worker/docs-skill' },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
assert.match(result.prompt, /Task title: Write docs/);
|
|
60
|
+
assert.match(result.prompt, /artifacts\/project\/guide.md/);
|
|
61
|
+
assert.match(result.prompt, /skills\/worker\/docs-skill/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('runWorkshopAcpTask executes a real ACP prompt against a local runtime mapping', async () => {
|
|
65
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'losclaws-acp-run-'));
|
|
66
|
+
const metaRoot = path.join(tempRoot, '.losclaws');
|
|
67
|
+
await fs.mkdir(path.join(tempRoot, 'artifacts', 'project'), { recursive: true });
|
|
68
|
+
await fs.mkdir(path.join(tempRoot, 'skills', 'worker', 'docs-skill'), { recursive: true });
|
|
69
|
+
await fs.mkdir(metaRoot, { recursive: true });
|
|
70
|
+
await fs.writeFile(path.join(tempRoot, 'artifacts', 'project', 'guide.md'), 'hello', 'utf8');
|
|
71
|
+
await fs.writeFile(
|
|
72
|
+
path.join(metaRoot, 'state.json'),
|
|
73
|
+
`${JSON.stringify(
|
|
74
|
+
{
|
|
75
|
+
version: 1,
|
|
76
|
+
rootType: 'project',
|
|
77
|
+
rootPath: tempRoot,
|
|
78
|
+
role: 'worker',
|
|
79
|
+
workspace: { id: 'wrk_1', name: 'Workspace', slug: 'workspace', version: 1 },
|
|
80
|
+
project: { id: 'prj_1', name: 'Project', slug: 'project', version: 1 },
|
|
81
|
+
artifacts: [
|
|
82
|
+
{
|
|
83
|
+
id: 'art_1',
|
|
84
|
+
scope: 'project',
|
|
85
|
+
relativePath: 'artifacts/project/guide.md',
|
|
86
|
+
contentKind: 'text',
|
|
87
|
+
mimeType: 'text/plain',
|
|
88
|
+
lastSyncedHash: 'hash',
|
|
89
|
+
remoteRevisionNo: 1,
|
|
90
|
+
remoteVersion: 1,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
skills: [
|
|
94
|
+
{
|
|
95
|
+
name: 'docs-skill',
|
|
96
|
+
role: 'worker',
|
|
97
|
+
path: 'skills/worker/docs-skill',
|
|
98
|
+
source: 'bundled',
|
|
99
|
+
revision: 'bundled',
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
null,
|
|
104
|
+
2,
|
|
105
|
+
)}\n`,
|
|
106
|
+
'utf8',
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const configPath = path.join(tempRoot, 'config.json');
|
|
110
|
+
const state = await loadConfig(configPath);
|
|
111
|
+
const fixturePath = fileURLToPath(new URL('../testdata/mock-acp-agent.js', import.meta.url));
|
|
112
|
+
|
|
113
|
+
const worklogs = [];
|
|
114
|
+
const requestWorkshop = async ({ method, path: requestPath, body }) => {
|
|
115
|
+
if (requestPath === '/api/v1/tasks/tsk_123' && (!method || method === 'GET')) {
|
|
116
|
+
return {
|
|
117
|
+
task: {
|
|
118
|
+
id: 'tsk_123',
|
|
119
|
+
title: 'Write docs',
|
|
120
|
+
description: 'Update the project guide.',
|
|
121
|
+
assigneeRole: 'worker',
|
|
122
|
+
version: 4,
|
|
123
|
+
acp: {
|
|
124
|
+
key: 'mock-agent',
|
|
125
|
+
command: process.execPath,
|
|
126
|
+
args: [fixturePath],
|
|
127
|
+
authMethodId: 'local-auth',
|
|
128
|
+
},
|
|
129
|
+
inputArtifacts: [{ id: 'art_1' }],
|
|
130
|
+
},
|
|
131
|
+
projectId: 'prj_1',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (requestPath === '/api/v1/tasks/tsk_123/worklogs' && method === 'POST') {
|
|
135
|
+
worklogs.push(body);
|
|
136
|
+
return { id: `wl_${worklogs.length}`, version: 4 };
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`Unexpected request: ${method} ${requestPath}`);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const result = await runWorkshopAcpTask({
|
|
142
|
+
requestWorkshop,
|
|
143
|
+
state,
|
|
144
|
+
options: {
|
|
145
|
+
id: 'tsk_123',
|
|
146
|
+
root: tempRoot,
|
|
147
|
+
approvalPolicy: 'auto-allow',
|
|
148
|
+
closeSession: true,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
assert.equal(result.agent.name, 'mock-agent');
|
|
153
|
+
assert.equal(result.acp.stopReason, 'end_turn');
|
|
154
|
+
assert.match(result.acp.sessionId, /^sess_/);
|
|
155
|
+
const logText = await fs.readFile(result.logPath, 'utf8');
|
|
156
|
+
assert.match(logText, /session_update/);
|
|
157
|
+
assert.ok(worklogs.length >= 1, 'expected at least one worklog to be recorded');
|
|
158
|
+
assert.ok(worklogs.every((entry) => typeof entry.updateType === 'string'));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('runWorkshopAcpTask routes a remote permission request through feedback and back', async () => {
|
|
162
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'losclaws-acp-remote-'));
|
|
163
|
+
const metaRoot = path.join(tempRoot, '.losclaws');
|
|
164
|
+
await fs.mkdir(metaRoot, { recursive: true });
|
|
165
|
+
await fs.writeFile(
|
|
166
|
+
path.join(metaRoot, 'state.json'),
|
|
167
|
+
`${JSON.stringify({ version: 1, rootType: 'project', rootPath: tempRoot, role: 'worker', artifacts: [], skills: [] }, null, 2)}\n`,
|
|
168
|
+
'utf8',
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const configPath = path.join(tempRoot, 'config.json');
|
|
172
|
+
const state = await loadConfig(configPath);
|
|
173
|
+
const fixturePath = fileURLToPath(new URL('../testdata/mock-acp-agent.js', import.meta.url));
|
|
174
|
+
|
|
175
|
+
let feedbackRequested = null;
|
|
176
|
+
let feedbackResolved = false;
|
|
177
|
+
const requestWorkshop = async ({ method, path: requestPath, body }) => {
|
|
178
|
+
if (requestPath === '/api/v1/tasks/tsk_remote' && (!method || method === 'GET')) {
|
|
179
|
+
// Once a feedback request exists, report it as submitted so the poll resolves.
|
|
180
|
+
const feedbackSession = feedbackRequested
|
|
181
|
+
? {
|
|
182
|
+
id: 'fb_1',
|
|
183
|
+
status: feedbackResolved ? 'submitted' : 'open',
|
|
184
|
+
kind: 'single_select',
|
|
185
|
+
version: 1,
|
|
186
|
+
entries: feedbackResolved ? [{ id: 'e1', response: { optionId: 'allow' }, body: 'Allow' }] : [],
|
|
187
|
+
}
|
|
188
|
+
: null;
|
|
189
|
+
if (feedbackRequested) {
|
|
190
|
+
feedbackResolved = true;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
task: {
|
|
194
|
+
id: 'tsk_remote',
|
|
195
|
+
title: 'Remote task',
|
|
196
|
+
version: 7,
|
|
197
|
+
acp: { key: 'mock-agent', command: process.execPath, args: [fixturePath], authMethodId: 'local-auth' },
|
|
198
|
+
},
|
|
199
|
+
feedbackSession,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (requestPath === '/api/v1/tasks/tsk_remote/feedback-requests' && method === 'POST') {
|
|
203
|
+
feedbackRequested = body;
|
|
204
|
+
return { version: 8 };
|
|
205
|
+
}
|
|
206
|
+
if (requestPath === '/api/v1/tasks/tsk_remote/worklogs' && method === 'POST') {
|
|
207
|
+
return { id: 'wl', version: 8 };
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`Unexpected request: ${method} ${requestPath}`);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const result = await runWorkshopAcpTask({
|
|
213
|
+
requestWorkshop,
|
|
214
|
+
state,
|
|
215
|
+
options: {
|
|
216
|
+
id: 'tsk_remote',
|
|
217
|
+
root: tempRoot,
|
|
218
|
+
approvalPolicy: 'remote',
|
|
219
|
+
feedbackTimeout: 20000,
|
|
220
|
+
closeSession: true,
|
|
221
|
+
// The mock agent issues a permission request when this env var is set on
|
|
222
|
+
// the spawned coder; --env is merged into the coder's process environment.
|
|
223
|
+
env: { MOCK_ACP_REQUEST_PERMISSION: '1' },
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
assert.equal(result.acp.stopReason, 'end_turn');
|
|
228
|
+
assert.ok(feedbackRequested, 'expected a feedback request to be posted');
|
|
229
|
+
assert.equal(feedbackRequested.kind, 'single_select');
|
|
230
|
+
assert.ok(Array.isArray(feedbackRequested.options) && feedbackRequested.options.length === 2);
|
|
231
|
+
});
|
package/test/config.test.js
CHANGED
|
@@ -6,7 +6,12 @@ import path from 'node:path';
|
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
clearedAuthProfile,
|
|
11
|
+
configForDisplay,
|
|
12
|
+
loadConfig,
|
|
13
|
+
resolveRuntime,
|
|
14
|
+
} from '../src/config.js';
|
|
10
15
|
|
|
11
16
|
test('resolveRuntime includes clawworkshop base URL from profile', () => {
|
|
12
17
|
const runtime = resolveRuntime(
|
package/test/inspect.test.js
CHANGED
|
@@ -26,3 +26,29 @@ error: ignored
|
|
|
26
26
|
|
|
27
27
|
assert.deepEqual(models, ['claude-sonnet-4.6', 'gpt-5.4']);
|
|
28
28
|
});
|
|
29
|
+
|
|
30
|
+
test('parseModelListOutput extracts slash-delimited OpenCode model names', () => {
|
|
31
|
+
const models = parseModelListOutput(`
|
|
32
|
+
opencode/big-pickle
|
|
33
|
+
opencode/deepseek-v4-flash-free
|
|
34
|
+
opencode-go/qwen3.7-plus
|
|
35
|
+
`);
|
|
36
|
+
|
|
37
|
+
assert.deepEqual(models, [
|
|
38
|
+
'opencode-go/qwen3.7-plus',
|
|
39
|
+
'opencode/big-pickle',
|
|
40
|
+
'opencode/deepseek-v4-flash-free',
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('parseModelListOutput ignores shell prompts around OpenCode model output', () => {
|
|
45
|
+
const models = parseModelListOutput(`
|
|
46
|
+
openvscode-server@remoteide:~/workspace/supremelosclaws/losclaws-cli$ opencode models
|
|
47
|
+
Usage: opencode models
|
|
48
|
+
opencode/mimo-v2.5-free
|
|
49
|
+
opencode-go/minimax-m3
|
|
50
|
+
error: ignored
|
|
51
|
+
`);
|
|
52
|
+
|
|
53
|
+
assert.deepEqual(models, ['opencode-go/minimax-m3', 'opencode/mimo-v2.5-free']);
|
|
54
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
import { buildArtifactIndex, summarizeIndex } from '../src/workshop-local.js';
|
|
9
|
+
|
|
10
|
+
test('summarizeIndex returns status counters', () => {
|
|
11
|
+
const summary = summarizeIndex({
|
|
12
|
+
entries: [
|
|
13
|
+
{ status: 'clean' },
|
|
14
|
+
{ status: 'clean' },
|
|
15
|
+
{ status: 'modified' },
|
|
16
|
+
{ status: 'missing' },
|
|
17
|
+
{ status: 'untracked' },
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
assert.deepEqual(summary, {
|
|
22
|
+
clean: 2,
|
|
23
|
+
modified: 1,
|
|
24
|
+
missing: 1,
|
|
25
|
+
untracked: 1,
|
|
26
|
+
total: 5,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('buildArtifactIndex detects modified/missing/untracked artifacts', async () => {
|
|
31
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'losclaws-workshop-local-test-'));
|
|
32
|
+
const artifactsDir = path.join(tempRoot, 'artifacts', 'project');
|
|
33
|
+
await fs.mkdir(artifactsDir, { recursive: true });
|
|
34
|
+
|
|
35
|
+
const cleanPath = path.join(artifactsDir, 'clean.txt');
|
|
36
|
+
const modifiedPath = path.join(artifactsDir, 'modified.txt');
|
|
37
|
+
const untrackedPath = path.join(artifactsDir, 'untracked.txt');
|
|
38
|
+
await fs.writeFile(cleanPath, 'clean', 'utf8');
|
|
39
|
+
await fs.writeFile(modifiedPath, 'new-value', 'utf8');
|
|
40
|
+
await fs.writeFile(untrackedPath, 'untracked', 'utf8');
|
|
41
|
+
|
|
42
|
+
const state = {
|
|
43
|
+
rootPath: tempRoot,
|
|
44
|
+
artifacts: [
|
|
45
|
+
{
|
|
46
|
+
id: 'art_clean',
|
|
47
|
+
relativePath: path.join('artifacts', 'project', 'clean.txt'),
|
|
48
|
+
lastSyncedHash: hashText('clean'),
|
|
49
|
+
remoteRevisionNo: 1,
|
|
50
|
+
remoteVersion: 10,
|
|
51
|
+
contentKind: 'text',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'art_modified',
|
|
55
|
+
relativePath: path.join('artifacts', 'project', 'modified.txt'),
|
|
56
|
+
lastSyncedHash: hashText('old-value'),
|
|
57
|
+
remoteRevisionNo: 2,
|
|
58
|
+
remoteVersion: 20,
|
|
59
|
+
contentKind: 'text',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'art_missing',
|
|
63
|
+
relativePath: path.join('artifacts', 'project', 'missing.txt'),
|
|
64
|
+
lastSyncedHash: hashText('missing'),
|
|
65
|
+
remoteRevisionNo: 3,
|
|
66
|
+
remoteVersion: 30,
|
|
67
|
+
contentKind: 'text',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const index = await buildArtifactIndex(state);
|
|
73
|
+
const byPath = new Map(index.entries.map((entry) => [entry.relativePath.replaceAll('\\', '/'), entry]));
|
|
74
|
+
|
|
75
|
+
assert.equal(byPath.get('artifacts/project/clean.txt').status, 'clean');
|
|
76
|
+
assert.equal(byPath.get('artifacts/project/modified.txt').status, 'modified');
|
|
77
|
+
assert.equal(byPath.get('artifacts/project/missing.txt').status, 'missing');
|
|
78
|
+
assert.equal(byPath.get('artifacts/project/untracked.txt').status, 'untracked');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function hashText(value) {
|
|
82
|
+
return createHash('sha256').update(Buffer.from(value, 'utf8')).digest('hex');
|
|
83
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Readable, Writable } from 'node:stream';
|
|
4
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
5
|
+
|
|
6
|
+
class MockAgent {
|
|
7
|
+
constructor(connection) {
|
|
8
|
+
this.connection = connection;
|
|
9
|
+
this.sessions = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async initialize() {
|
|
13
|
+
return {
|
|
14
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
15
|
+
agentCapabilities: {
|
|
16
|
+
loadSession: true,
|
|
17
|
+
sessionCapabilities: {
|
|
18
|
+
close: {},
|
|
19
|
+
resume: {},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
authMethods: [
|
|
23
|
+
{
|
|
24
|
+
id: 'local-auth',
|
|
25
|
+
name: 'Local auth',
|
|
26
|
+
description: 'Mock authentication method',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
agentInfo: {
|
|
30
|
+
name: 'mock-acp-agent',
|
|
31
|
+
title: 'Mock ACP Agent',
|
|
32
|
+
version: '1.0.0',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async authenticate() {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async newSession() {
|
|
42
|
+
const sessionId = `sess_${Math.random().toString(16).slice(2)}`;
|
|
43
|
+
this.sessions.set(sessionId, true);
|
|
44
|
+
return { sessionId };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async loadSession(params) {
|
|
48
|
+
this.sessions.set(params.sessionId, true);
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async resumeSession(params) {
|
|
53
|
+
this.sessions.set(params.sessionId, true);
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async closeSession() {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async prompt(params) {
|
|
62
|
+
const sessionId = params.sessionId;
|
|
63
|
+
|
|
64
|
+
// Reasoning / plan / tool-call surface so the bridge maps several update types.
|
|
65
|
+
await this.connection.sessionUpdate({
|
|
66
|
+
sessionId,
|
|
67
|
+
update: {
|
|
68
|
+
sessionUpdate: 'agent_thought_chunk',
|
|
69
|
+
content: { type: 'text', text: 'Planning the work.' },
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
await this.connection.sessionUpdate({
|
|
73
|
+
sessionId,
|
|
74
|
+
update: {
|
|
75
|
+
sessionUpdate: 'plan',
|
|
76
|
+
entries: [
|
|
77
|
+
{ content: 'Read the task', status: 'completed', priority: 'high' },
|
|
78
|
+
{ content: 'Make the change', status: 'in_progress', priority: 'high' },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
await this.connection.sessionUpdate({
|
|
83
|
+
sessionId,
|
|
84
|
+
update: {
|
|
85
|
+
sessionUpdate: 'tool_call',
|
|
86
|
+
toolCallId: 'call_1',
|
|
87
|
+
title: 'Edit guide.md',
|
|
88
|
+
kind: 'edit',
|
|
89
|
+
status: 'in_progress',
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Optionally exercise the human-in-the-loop permission round-trip.
|
|
94
|
+
if (process.env.MOCK_ACP_REQUEST_PERMISSION === '1') {
|
|
95
|
+
const decision = await this.connection.requestPermission({
|
|
96
|
+
sessionId,
|
|
97
|
+
toolCall: { toolCallId: 'call_1', title: 'Apply the edit to guide.md', kind: 'edit' },
|
|
98
|
+
options: [
|
|
99
|
+
{ optionId: 'allow', name: 'Allow', kind: 'allow_once' },
|
|
100
|
+
{ optionId: 'deny', name: 'Deny', kind: 'reject_once' },
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
await this.connection.sessionUpdate({
|
|
104
|
+
sessionId,
|
|
105
|
+
update: {
|
|
106
|
+
sessionUpdate: 'agent_message_chunk',
|
|
107
|
+
content: { type: 'text', text: `permission outcome: ${JSON.stringify(decision.outcome)}` },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await this.connection.sessionUpdate({
|
|
113
|
+
sessionId,
|
|
114
|
+
update: {
|
|
115
|
+
sessionUpdate: 'agent_message_chunk',
|
|
116
|
+
content: {
|
|
117
|
+
type: 'text',
|
|
118
|
+
text: `mock-agent received: ${params.prompt[0].text.slice(0, 40)}`,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
stopReason: 'end_turn',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async cancel() {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const stream = acp.ndJsonStream(
|
|
132
|
+
Writable.toWeb(process.stdout),
|
|
133
|
+
Readable.toWeb(process.stdin),
|
|
134
|
+
);
|
|
135
|
+
new acp.AgentSideConnection((connection) => new MockAgent(connection), stream);
|