@causyne/agent 1.10.21 → 1.11.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/native-skills/media/professional-document-publishing/SKILL.md +44 -0
- package/native-skills/media/screenplay-and-shotlist-forge/SKILL.md +42 -0
- package/package.json +1 -1
- package/src/acp/client.js +235 -0
- package/src/acp/client.ts +282 -0
- package/src/acp/peerProfiles.js +122 -0
- package/src/acp/peerProfiles.ts +128 -0
- package/src/acp/protocol.js +54 -0
- package/src/acp/protocol.ts +434 -0
- package/src/acp/server.js +609 -0
- package/src/acp/server.ts +676 -0
- package/src/agentLoop.d.ts +43 -0
- package/src/agentLoop.js +142 -8
- package/src/commands.js +203 -0
- package/src/compiledSkills.js +10 -0
- package/src/documentEngine/antiSlopAuditor.js +156 -0
- package/src/documentEngine/antiSlopAuditor.ts +221 -0
- package/src/documentEngine/documentForge.js +443 -0
- package/src/documentEngine/documentForge.ts +519 -0
- package/src/documentEngine/screenplayFormatter.js +336 -0
- package/src/documentEngine/screenplayFormatter.ts +407 -0
- package/src/entrypoint.js +13 -0
- package/src/execSafe.js +10 -13
- package/src/execSafe.ts +25 -0
- package/src/modelRouter.js +56 -123
- package/src/modelRouter.ts +134 -0
- package/src/observer/advisory.js +164 -0
- package/src/observer/advisory.ts +171 -0
- package/src/observer/coldObserver.js +462 -0
- package/src/observer/coldObserver.ts +540 -0
- package/src/observer/evidence.js +435 -0
- package/src/observer/evidence.ts +499 -0
- package/src/perception.js +303 -118
- package/src/perception.ts +387 -0
- package/src/procSignal.js +15 -16
- package/src/procSignal.ts +31 -0
- package/src/promptMentions.js +170 -0
- package/src/promptMentions.ts +212 -0
- package/src/sandbox.js +1 -1
- package/src/textDiff.js +117 -109
- package/src/textDiff.ts +169 -0
- package/src/toolPacks.js +5 -2
- package/src/tools/documentForge.js +215 -0
- package/src/tools/documentForge.ts +265 -0
- package/src/tools/peerAgent.js +102 -0
- package/src/tools/peerAgent.ts +116 -0
- package/src/tools/registry.js +3 -1
- package/src/tui/completion.js +97 -41
- package/src/tui/prompt.js +17 -17
- package/src/uncertainty.js +93 -205
- package/src/uncertainty.ts +255 -0
- package/src/utils/paths.js +15 -18
- package/src/utils/paths.ts +36 -0
- package/src/vfeTracker.js +122 -181
- package/src/vfeTracker.ts +242 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: professional-document-publishing
|
|
3
|
+
description: Generate publication-grade PDF documents, grant proposals, financial sheets, and technical reports using HTML/CSS Paged Media and Headless Chrome. Enforces visual quality inspection, under 2MB compliance, and mathematical integrity.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Professional Document Publishing & Harness
|
|
7
|
+
|
|
8
|
+
Frontier LLMs and efficient models produce equally stunning, publication-ready documents when guided by this harness methodology.
|
|
9
|
+
|
|
10
|
+
Never assemble print layouts through raw Python scripts. Always author in semantic HTML5 plus CSS Paged Media, compile via Headless Chrome, and visually inspect the rendered output.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. Prepare structured data and schemas.
|
|
15
|
+
- Keep numeric budget and scheduling data in structured JSON format.
|
|
16
|
+
- Run math validation with `document_forge` (`action: "audit_budget"`).
|
|
17
|
+
- **Done when:** all numbers are validated against the single source of truth.
|
|
18
|
+
|
|
19
|
+
2. Compile document markup and styles.
|
|
20
|
+
- Author page layouts in HTML using standard CSS `@page` rules (`size: A4 portrait; margin: 20mm 18mm 18mm 18mm;`).
|
|
21
|
+
- Use `@page cover { margin: 0; }` for full-bleed cover banners without white borders.
|
|
22
|
+
- Force `-webkit-print-color-adjust: exact; print-color-adjust: exact;` so backgrounds and gradients render faithfully.
|
|
23
|
+
- Prevent mid-table row cuts with `tr { page-break-inside: avoid; }`.
|
|
24
|
+
- Prevent orphan headers with `h1, h2, h3 { page-break-after: avoid; }`.
|
|
25
|
+
- Compile via `document_forge` (`action: "compile_document"`).
|
|
26
|
+
- **Done when:** HTML markup and CSS paged styles are written to disk.
|
|
27
|
+
|
|
28
|
+
3. Render to vector PDF via Headless Chrome.
|
|
29
|
+
- Invoke `document_forge` (`action: "render_pdf"`, `sourceHtmlPath`, `outputPdfPath`).
|
|
30
|
+
- **Done when:** vector PDF file exists on disk with non-zero size.
|
|
31
|
+
|
|
32
|
+
4. Inspect pages visually and audit compliance.
|
|
33
|
+
- Extract page images via `document_forge` (`action: "inspect_pages"`).
|
|
34
|
+
- Verify page fill density: flag and repair any pages with less than 25% text coverage.
|
|
35
|
+
- Confirm PDF file size is below 2,048 KB for portal compliance.
|
|
36
|
+
- Audit text content for long em-dashes and AI clichés with `document_forge` (`action: "audit_text"`).
|
|
37
|
+
- **Done when:** all pages pass visual density check and size constraints.
|
|
38
|
+
|
|
39
|
+
## Verification
|
|
40
|
+
|
|
41
|
+
- `document_forge` returns `success: true`.
|
|
42
|
+
- Output PDF file size is non-zero and under 2,048 KB.
|
|
43
|
+
- Extracted page images show no clipped text or orphaned headers.
|
|
44
|
+
- Mathematical totals match across all sections.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: screenplay-and-shotlist-forge
|
|
3
|
+
description: Format professional Hollywood screenplays in Courier 12pt, create director production shot lists, and build voiceover timing cue sheets with Words-Per-Minute pacing analysis. Use for movie scripts, video storytelling, documentary shotlists, and audio dubbing timelines.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Screenplay & Production Shot List Forge
|
|
7
|
+
|
|
8
|
+
High-end cinematic storytelling and video production require standardized industry formatting so that directors, cinematographers, and editors can immediately execute on set and in the edit suite.
|
|
9
|
+
|
|
10
|
+
## Specifications
|
|
11
|
+
|
|
12
|
+
- **Screenplay Typography:** Courier 12pt monospaced font, exactly 6 lines per inch vertical rhythm.
|
|
13
|
+
- **Screenplay Margins:** Left margin 1.5 inches; right, top, and bottom margins 1.0 inch.
|
|
14
|
+
- **Pacing Standard:** Voiceover narration moves at 130 to 155 Words-Per-Minute for natural documentary cadence.
|
|
15
|
+
|
|
16
|
+
## Workflow
|
|
17
|
+
|
|
18
|
+
1. Format screenplay elements and layout.
|
|
19
|
+
- Author scenes using `document_forge` (`action: "format_screenplay"`, `screenplaySpec`).
|
|
20
|
+
- Format scene headings in uppercase, character cues indented 2.2 inches, and dialogue width constrained to 3.5 inches.
|
|
21
|
+
- **Done when:** script HTML and CSS with Courier 12pt specifications are generated.
|
|
22
|
+
|
|
23
|
+
2. Build director's production shot list.
|
|
24
|
+
- Generate production shot lists with `document_forge` (`action: "format_shotlist"`, `shotList`).
|
|
25
|
+
- Assign camera framing (EWS, WS, MS, CU, ECU), lenses, movement, and audio cues.
|
|
26
|
+
- **Done when:** structured shot list grid is compiled with cumulative duration estimates.
|
|
27
|
+
|
|
28
|
+
3. Analyze voiceover timing and narration pacing.
|
|
29
|
+
- Audit narration pacing with `document_forge` (`action: "analyze_vo"`, `voBlocks`).
|
|
30
|
+
- Ensure word density stays within 130 to 155 WPM for optimal documentary pacing.
|
|
31
|
+
- **Done when:** pacing status is confirmed as optimal across all audio blocks.
|
|
32
|
+
|
|
33
|
+
4. Render and verify publication files.
|
|
34
|
+
- Render scripts or shot lists to vector PDF via `document_forge` (`action: "render_pdf"`).
|
|
35
|
+
- **Done when:** output PDF files exist and are verified visually.
|
|
36
|
+
|
|
37
|
+
## Verification
|
|
38
|
+
|
|
39
|
+
- `document_forge` returns `success: true`.
|
|
40
|
+
- Screenplay HTML contains Courier 12pt typography and 1.5-inch left margin.
|
|
41
|
+
- Shot list table summarizes total shots and total runtime.
|
|
42
|
+
- Voiceover analysis confirms pacing within target WPM limits.
|
package/package.json
CHANGED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { ACP_ERROR_CODES, ACP_METHODS, ACP_PROTOCOL_VERSION, isJsonRpcRequest, isJsonRpcResponse } from './protocol.js';
|
|
3
|
+
const MAX_STDERR_CHARS = 4000;
|
|
4
|
+
const MAX_UPDATE_CHARS = 200_000;
|
|
5
|
+
export class AcpClient {
|
|
6
|
+
options;
|
|
7
|
+
child = null;
|
|
8
|
+
buffer = '';
|
|
9
|
+
stderrTail = '';
|
|
10
|
+
pending = new Map();
|
|
11
|
+
nextId = 1;
|
|
12
|
+
closed = false;
|
|
13
|
+
exitError = null;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
get command() {
|
|
18
|
+
return this.options.command;
|
|
19
|
+
}
|
|
20
|
+
async start() {
|
|
21
|
+
if (this.child)
|
|
22
|
+
throw new Error('client already started');
|
|
23
|
+
const args = this.options.args ?? [];
|
|
24
|
+
const child = spawn(this.options.command, args, {
|
|
25
|
+
cwd: this.options.cwd,
|
|
26
|
+
env: this.options.env ?? process.env,
|
|
27
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
28
|
+
windowsHide: true
|
|
29
|
+
});
|
|
30
|
+
this.child = child;
|
|
31
|
+
child.stdout.setEncoding('utf8');
|
|
32
|
+
child.stdout.on('data', (chunk) => {
|
|
33
|
+
this.buffer += chunk;
|
|
34
|
+
void this.drain();
|
|
35
|
+
});
|
|
36
|
+
child.stderr.setEncoding('utf8');
|
|
37
|
+
child.stderr.on('data', (chunk) => {
|
|
38
|
+
this.stderrTail = `${this.stderrTail}${chunk}`.slice(-MAX_STDERR_CHARS);
|
|
39
|
+
});
|
|
40
|
+
child.on('error', (error) => this.failAll(new Error(`agent process failed: ${error.message}`)));
|
|
41
|
+
child.on('exit', (code, signal) => {
|
|
42
|
+
const detail = this.stderrTail.trim() || `exit code ${code ?? 'null'}${signal ? ` signal ${signal}` : ''}`;
|
|
43
|
+
this.failAll(new Error(`agent process exited: ${detail}`));
|
|
44
|
+
});
|
|
45
|
+
const params = {
|
|
46
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
47
|
+
clientCapabilities: {
|
|
48
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
49
|
+
terminal: false,
|
|
50
|
+
session: { notices: false }
|
|
51
|
+
},
|
|
52
|
+
clientInfo: { name: 'Causyne Agent', version: this.options.clientVersion ?? '0.0.0' }
|
|
53
|
+
};
|
|
54
|
+
return await this.request(ACP_METHODS.INITIALIZE, params, 30_000);
|
|
55
|
+
}
|
|
56
|
+
async newSession(cwd, mcpServers = []) {
|
|
57
|
+
const result = await this.request(ACP_METHODS.NEW_SESSION, {
|
|
58
|
+
cwd,
|
|
59
|
+
mcpServers
|
|
60
|
+
}, 30_000);
|
|
61
|
+
if (!result || typeof result.sessionId !== 'string' || !result.sessionId) {
|
|
62
|
+
throw new Error('agent did not return a session id');
|
|
63
|
+
}
|
|
64
|
+
return result.sessionId;
|
|
65
|
+
}
|
|
66
|
+
async prompt(sessionId, text) {
|
|
67
|
+
const blocks = [{ type: 'text', text }];
|
|
68
|
+
let collected = '';
|
|
69
|
+
const collector = (update) => {
|
|
70
|
+
if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
|
|
71
|
+
collected = `${collected}${update.content.text}`.slice(0, MAX_UPDATE_CHARS);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const previous = this.updateListener;
|
|
75
|
+
this.updateListener = (sessionIdFromEvent, update) => {
|
|
76
|
+
previous?.(sessionIdFromEvent, update);
|
|
77
|
+
if (sessionIdFromEvent === sessionId)
|
|
78
|
+
collector(update);
|
|
79
|
+
};
|
|
80
|
+
try {
|
|
81
|
+
const response = await this.request(ACP_METHODS.PROMPT, {
|
|
82
|
+
sessionId,
|
|
83
|
+
prompt: blocks
|
|
84
|
+
}, this.options.promptTimeoutMs ?? 15 * 60 * 1000);
|
|
85
|
+
return {
|
|
86
|
+
stopReason: response?.stopReason ?? 'end_turn',
|
|
87
|
+
text: collected
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
this.updateListener = previous;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
notifyCancel(sessionId) {
|
|
95
|
+
this.write({
|
|
96
|
+
jsonrpc: '2.0',
|
|
97
|
+
method: ACP_METHODS.CANCEL,
|
|
98
|
+
params: { sessionId }
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async close() {
|
|
102
|
+
if (this.closed)
|
|
103
|
+
return;
|
|
104
|
+
this.closed = true;
|
|
105
|
+
const child = this.child;
|
|
106
|
+
this.failAll(new Error('client closed'));
|
|
107
|
+
if (!child)
|
|
108
|
+
return;
|
|
109
|
+
await new Promise((resolve) => {
|
|
110
|
+
const timer = setTimeout(() => {
|
|
111
|
+
child.kill('SIGKILL');
|
|
112
|
+
resolve();
|
|
113
|
+
}, 1500);
|
|
114
|
+
child.once('exit', () => {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
resolve();
|
|
117
|
+
});
|
|
118
|
+
child.kill('SIGTERM');
|
|
119
|
+
});
|
|
120
|
+
this.child = null;
|
|
121
|
+
}
|
|
122
|
+
updateListener = null;
|
|
123
|
+
write(message) {
|
|
124
|
+
if (!this.child || this.closed)
|
|
125
|
+
return;
|
|
126
|
+
try {
|
|
127
|
+
this.child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// A dead pipe surfaces through the exit handler.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async drain() {
|
|
134
|
+
let index = this.buffer.indexOf('\n');
|
|
135
|
+
while (index >= 0) {
|
|
136
|
+
const line = this.buffer.slice(0, index).trim();
|
|
137
|
+
this.buffer = this.buffer.slice(index + 1);
|
|
138
|
+
if (line)
|
|
139
|
+
await this.handleLine(line);
|
|
140
|
+
index = this.buffer.indexOf('\n');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async handleLine(line) {
|
|
144
|
+
let parsed;
|
|
145
|
+
try {
|
|
146
|
+
parsed = JSON.parse(line);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (isJsonRpcResponse(parsed)) {
|
|
152
|
+
this.settle(parsed);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (isJsonRpcRequest(parsed)) {
|
|
156
|
+
await this.handleServerRequest(parsed);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const method = parsed.method;
|
|
160
|
+
if (method === ACP_METHODS.UPDATE) {
|
|
161
|
+
const params = parsed.params;
|
|
162
|
+
if (params && typeof params.sessionId === 'string' && params.update) {
|
|
163
|
+
this.updateListener?.(params.sessionId, params.update);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async handleServerRequest(request) {
|
|
168
|
+
if (request.method === ACP_METHODS.REQUEST_PERMISSION) {
|
|
169
|
+
const handler = this.options.permissionHandler;
|
|
170
|
+
if (!handler) {
|
|
171
|
+
this.write({
|
|
172
|
+
jsonrpc: '2.0',
|
|
173
|
+
id: request.id,
|
|
174
|
+
result: { outcome: { outcome: 'cancelled' } }
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const outcome = await handler(request.params);
|
|
180
|
+
this.write({ jsonrpc: '2.0', id: request.id, result: { outcome } });
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
this.write({ jsonrpc: '2.0', id: request.id, result: { outcome: { outcome: 'cancelled' } } });
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.write({
|
|
188
|
+
jsonrpc: '2.0',
|
|
189
|
+
id: request.id,
|
|
190
|
+
error: { code: ACP_ERROR_CODES.METHOD_NOT_FOUND, message: `Unsupported client method: ${request.method}` }
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
settle(response) {
|
|
194
|
+
if (response.id === null || response.id === undefined)
|
|
195
|
+
return;
|
|
196
|
+
const pending = this.pending.get(response.id);
|
|
197
|
+
if (!pending)
|
|
198
|
+
return;
|
|
199
|
+
clearTimeout(pending.timer);
|
|
200
|
+
this.pending.delete(response.id);
|
|
201
|
+
if (response.error) {
|
|
202
|
+
pending.reject(new Error(`${pending.method} failed: ${response.error.message}`));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
pending.resolve(response.result);
|
|
206
|
+
}
|
|
207
|
+
request(method, params, timeoutMs) {
|
|
208
|
+
if (!this.child || this.closed)
|
|
209
|
+
return Promise.reject(new Error('agent process is not running'));
|
|
210
|
+
if (this.exitError)
|
|
211
|
+
return Promise.reject(this.exitError);
|
|
212
|
+
const id = this.nextId++;
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
const timer = setTimeout(() => {
|
|
215
|
+
this.pending.delete(id);
|
|
216
|
+
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
217
|
+
}, timeoutMs);
|
|
218
|
+
this.pending.set(id, {
|
|
219
|
+
resolve: (value) => resolve(value),
|
|
220
|
+
reject,
|
|
221
|
+
timer,
|
|
222
|
+
method
|
|
223
|
+
});
|
|
224
|
+
this.write({ jsonrpc: '2.0', id, method, params });
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
failAll(error) {
|
|
228
|
+
this.exitError = error;
|
|
229
|
+
for (const [id, pending] of this.pending) {
|
|
230
|
+
clearTimeout(pending.timer);
|
|
231
|
+
pending.reject(error);
|
|
232
|
+
this.pending.delete(id);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
3
|
+
import {
|
|
4
|
+
ACP_ERROR_CODES,
|
|
5
|
+
ACP_METHODS,
|
|
6
|
+
ACP_PROTOCOL_VERSION,
|
|
7
|
+
isJsonRpcRequest,
|
|
8
|
+
isJsonRpcResponse
|
|
9
|
+
} from './protocol.js';
|
|
10
|
+
import type {
|
|
11
|
+
ContentBlock,
|
|
12
|
+
InitializeRequest,
|
|
13
|
+
InitializeResult,
|
|
14
|
+
JsonRpcNotification,
|
|
15
|
+
JsonRpcRequest,
|
|
16
|
+
JsonRpcResponse,
|
|
17
|
+
PromptResponse,
|
|
18
|
+
RequestPermissionRequest,
|
|
19
|
+
RequestPermissionResponse,
|
|
20
|
+
SessionNotification,
|
|
21
|
+
SessionUpdate
|
|
22
|
+
} from './protocol.js';
|
|
23
|
+
|
|
24
|
+
export interface AcpClientOptions {
|
|
25
|
+
command: string;
|
|
26
|
+
args?: string[];
|
|
27
|
+
cwd: string;
|
|
28
|
+
env?: NodeJS.ProcessEnv;
|
|
29
|
+
clientVersion?: string;
|
|
30
|
+
promptTimeoutMs?: number;
|
|
31
|
+
permissionHandler?: (request: RequestPermissionRequest) => Promise<RequestPermissionResponse['outcome']>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AcpPromptResult {
|
|
35
|
+
stopReason: PromptResponse['stopReason'];
|
|
36
|
+
text: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface PendingRequest {
|
|
40
|
+
resolve: (value: unknown) => void;
|
|
41
|
+
reject: (error: Error) => void;
|
|
42
|
+
timer: NodeJS.Timeout;
|
|
43
|
+
method: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const MAX_STDERR_CHARS = 4000;
|
|
47
|
+
const MAX_UPDATE_CHARS = 200_000;
|
|
48
|
+
|
|
49
|
+
export class AcpClient {
|
|
50
|
+
private readonly options: AcpClientOptions;
|
|
51
|
+
private child: ChildProcessWithoutNullStreams | null = null;
|
|
52
|
+
private buffer = '';
|
|
53
|
+
private stderrTail = '';
|
|
54
|
+
private readonly pending = new Map<string | number, PendingRequest>();
|
|
55
|
+
private nextId = 1;
|
|
56
|
+
private closed = false;
|
|
57
|
+
private exitError: Error | null = null;
|
|
58
|
+
|
|
59
|
+
constructor(options: AcpClientOptions) {
|
|
60
|
+
this.options = options;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get command(): string {
|
|
64
|
+
return this.options.command;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async start(): Promise<InitializeResult> {
|
|
68
|
+
if (this.child) throw new Error('client already started');
|
|
69
|
+
const args = this.options.args ?? [];
|
|
70
|
+
const child = spawn(this.options.command, args, {
|
|
71
|
+
cwd: this.options.cwd,
|
|
72
|
+
env: this.options.env ?? process.env,
|
|
73
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
74
|
+
windowsHide: true
|
|
75
|
+
});
|
|
76
|
+
this.child = child;
|
|
77
|
+
|
|
78
|
+
child.stdout.setEncoding('utf8');
|
|
79
|
+
child.stdout.on('data', (chunk: string) => {
|
|
80
|
+
this.buffer += chunk;
|
|
81
|
+
void this.drain();
|
|
82
|
+
});
|
|
83
|
+
child.stderr.setEncoding('utf8');
|
|
84
|
+
child.stderr.on('data', (chunk: string) => {
|
|
85
|
+
this.stderrTail = `${this.stderrTail}${chunk}`.slice(-MAX_STDERR_CHARS);
|
|
86
|
+
});
|
|
87
|
+
child.on('error', (error) => this.failAll(new Error(`agent process failed: ${error.message}`)));
|
|
88
|
+
child.on('exit', (code, signal) => {
|
|
89
|
+
const detail = this.stderrTail.trim() || `exit code ${code ?? 'null'}${signal ? ` signal ${signal}` : ''}`;
|
|
90
|
+
this.failAll(new Error(`agent process exited: ${detail}`));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const params: InitializeRequest = {
|
|
94
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
95
|
+
clientCapabilities: {
|
|
96
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
97
|
+
terminal: false,
|
|
98
|
+
session: { notices: false }
|
|
99
|
+
},
|
|
100
|
+
clientInfo: { name: 'Causyne Agent', version: this.options.clientVersion ?? '0.0.0' }
|
|
101
|
+
};
|
|
102
|
+
return await this.request<InitializeResult>(ACP_METHODS.INITIALIZE, params, 30_000);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async newSession(cwd: string, mcpServers: unknown[] = []): Promise<string> {
|
|
106
|
+
const result = await this.request<{ sessionId?: unknown }>(ACP_METHODS.NEW_SESSION, {
|
|
107
|
+
cwd,
|
|
108
|
+
mcpServers
|
|
109
|
+
}, 30_000);
|
|
110
|
+
if (!result || typeof result.sessionId !== 'string' || !result.sessionId) {
|
|
111
|
+
throw new Error('agent did not return a session id');
|
|
112
|
+
}
|
|
113
|
+
return result.sessionId;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async prompt(sessionId: string, text: string): Promise<AcpPromptResult> {
|
|
117
|
+
const blocks: ContentBlock[] = [{ type: 'text', text }];
|
|
118
|
+
let collected = '';
|
|
119
|
+
const collector = (update: SessionUpdate): void => {
|
|
120
|
+
if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
|
|
121
|
+
collected = `${collected}${update.content.text}`.slice(0, MAX_UPDATE_CHARS);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const previous = this.updateListener;
|
|
125
|
+
this.updateListener = (sessionIdFromEvent, update) => {
|
|
126
|
+
previous?.(sessionIdFromEvent, update);
|
|
127
|
+
if (sessionIdFromEvent === sessionId) collector(update);
|
|
128
|
+
};
|
|
129
|
+
try {
|
|
130
|
+
const response = await this.request<PromptResponse>(ACP_METHODS.PROMPT, {
|
|
131
|
+
sessionId,
|
|
132
|
+
prompt: blocks
|
|
133
|
+
}, this.options.promptTimeoutMs ?? 15 * 60 * 1000);
|
|
134
|
+
return {
|
|
135
|
+
stopReason: response?.stopReason ?? 'end_turn',
|
|
136
|
+
text: collected
|
|
137
|
+
};
|
|
138
|
+
} finally {
|
|
139
|
+
this.updateListener = previous;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
notifyCancel(sessionId: string): void {
|
|
144
|
+
this.write({
|
|
145
|
+
jsonrpc: '2.0',
|
|
146
|
+
method: ACP_METHODS.CANCEL,
|
|
147
|
+
params: { sessionId }
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async close(): Promise<void> {
|
|
152
|
+
if (this.closed) return;
|
|
153
|
+
this.closed = true;
|
|
154
|
+
const child = this.child;
|
|
155
|
+
this.failAll(new Error('client closed'));
|
|
156
|
+
if (!child) return;
|
|
157
|
+
await new Promise<void>((resolve) => {
|
|
158
|
+
const timer = setTimeout(() => {
|
|
159
|
+
child.kill('SIGKILL');
|
|
160
|
+
resolve();
|
|
161
|
+
}, 1500);
|
|
162
|
+
child.once('exit', () => {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
resolve();
|
|
165
|
+
});
|
|
166
|
+
child.kill('SIGTERM');
|
|
167
|
+
});
|
|
168
|
+
this.child = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private updateListener: ((sessionId: string, update: SessionUpdate) => void) | null = null;
|
|
172
|
+
|
|
173
|
+
private write(message: JsonRpcRequest<unknown> | JsonRpcResponse | JsonRpcNotification<unknown>): void {
|
|
174
|
+
if (!this.child || this.closed) return;
|
|
175
|
+
try {
|
|
176
|
+
this.child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
177
|
+
} catch {
|
|
178
|
+
// A dead pipe surfaces through the exit handler.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private async drain(): Promise<void> {
|
|
183
|
+
let index = this.buffer.indexOf('\n');
|
|
184
|
+
while (index >= 0) {
|
|
185
|
+
const line = this.buffer.slice(0, index).trim();
|
|
186
|
+
this.buffer = this.buffer.slice(index + 1);
|
|
187
|
+
if (line) await this.handleLine(line);
|
|
188
|
+
index = this.buffer.indexOf('\n');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async handleLine(line: string): Promise<void> {
|
|
193
|
+
let parsed: unknown;
|
|
194
|
+
try {
|
|
195
|
+
parsed = JSON.parse(line);
|
|
196
|
+
} catch {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (isJsonRpcResponse(parsed)) {
|
|
200
|
+
this.settle(parsed);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (isJsonRpcRequest(parsed)) {
|
|
204
|
+
await this.handleServerRequest(parsed);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const method = (parsed as { method?: unknown }).method;
|
|
208
|
+
if (method === ACP_METHODS.UPDATE) {
|
|
209
|
+
const params = (parsed as { params?: SessionNotification }).params;
|
|
210
|
+
if (params && typeof params.sessionId === 'string' && params.update) {
|
|
211
|
+
this.updateListener?.(params.sessionId, params.update);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async handleServerRequest(request: JsonRpcRequest): Promise<void> {
|
|
217
|
+
if (request.method === ACP_METHODS.REQUEST_PERMISSION) {
|
|
218
|
+
const handler = this.options.permissionHandler;
|
|
219
|
+
if (!handler) {
|
|
220
|
+
this.write({
|
|
221
|
+
jsonrpc: '2.0',
|
|
222
|
+
id: request.id,
|
|
223
|
+
result: { outcome: { outcome: 'cancelled' } } satisfies RequestPermissionResponse
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const outcome = await handler(request.params as unknown as RequestPermissionRequest);
|
|
229
|
+
this.write({ jsonrpc: '2.0', id: request.id, result: { outcome } });
|
|
230
|
+
} catch {
|
|
231
|
+
this.write({ jsonrpc: '2.0', id: request.id, result: { outcome: { outcome: 'cancelled' } } });
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
this.write({
|
|
236
|
+
jsonrpc: '2.0',
|
|
237
|
+
id: request.id,
|
|
238
|
+
error: { code: ACP_ERROR_CODES.METHOD_NOT_FOUND, message: `Unsupported client method: ${request.method}` }
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private settle(response: JsonRpcResponse): void {
|
|
243
|
+
if (response.id === null || response.id === undefined) return;
|
|
244
|
+
const pending = this.pending.get(response.id);
|
|
245
|
+
if (!pending) return;
|
|
246
|
+
clearTimeout(pending.timer);
|
|
247
|
+
this.pending.delete(response.id);
|
|
248
|
+
if (response.error) {
|
|
249
|
+
pending.reject(new Error(`${pending.method} failed: ${response.error.message}`));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
pending.resolve(response.result);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private request<T>(method: string, params: unknown, timeoutMs: number): Promise<T> {
|
|
256
|
+
if (!this.child || this.closed) return Promise.reject(new Error('agent process is not running'));
|
|
257
|
+
if (this.exitError) return Promise.reject(this.exitError);
|
|
258
|
+
const id = this.nextId++;
|
|
259
|
+
return new Promise<T>((resolve, reject) => {
|
|
260
|
+
const timer = setTimeout(() => {
|
|
261
|
+
this.pending.delete(id);
|
|
262
|
+
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
263
|
+
}, timeoutMs);
|
|
264
|
+
this.pending.set(id, {
|
|
265
|
+
resolve: (value) => resolve(value as T),
|
|
266
|
+
reject,
|
|
267
|
+
timer,
|
|
268
|
+
method
|
|
269
|
+
});
|
|
270
|
+
this.write({ jsonrpc: '2.0', id, method, params });
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private failAll(error: Error): void {
|
|
275
|
+
this.exitError = error;
|
|
276
|
+
for (const [id, pending] of this.pending) {
|
|
277
|
+
clearTimeout(pending.timer);
|
|
278
|
+
pending.reject(error);
|
|
279
|
+
this.pending.delete(id);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|