@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,407 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2026 Causyne.
|
|
3
|
+
* SPDX-License-Identifier: BUSL-1.1
|
|
4
|
+
*
|
|
5
|
+
* Hollywood Standard Screenplay & Production Shot List Formatter.
|
|
6
|
+
* Enforces industry Courier 12pt specifications, 1-page-per-minute timing,
|
|
7
|
+
* director's shot list grids, and voice-over (VO) timing synchronization.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type ScreenplayElementType =
|
|
11
|
+
| 'scene_heading'
|
|
12
|
+
| 'action'
|
|
13
|
+
| 'character'
|
|
14
|
+
| 'parenthetical'
|
|
15
|
+
| 'dialogue'
|
|
16
|
+
| 'transition';
|
|
17
|
+
|
|
18
|
+
export interface ScreenplayElement {
|
|
19
|
+
type: ScreenplayElementType;
|
|
20
|
+
text: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ScreenplaySpec {
|
|
24
|
+
title: string;
|
|
25
|
+
author?: string;
|
|
26
|
+
contact?: string;
|
|
27
|
+
elements: ScreenplayElement[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ProductionShot {
|
|
31
|
+
shotNumber: number | string;
|
|
32
|
+
scene?: string;
|
|
33
|
+
framing: 'EWS' | 'WS' | 'MWS' | 'MS' | 'MCU' | 'CU' | 'ECU' | 'AERIAL' | 'MACRO';
|
|
34
|
+
angle?: 'Eye-level' | 'Low-angle' | 'High-angle' | 'Dutch' | 'Over-the-shoulder' | 'POV';
|
|
35
|
+
movement?: 'Static' | 'Pan' | 'Tilt' | 'Dolly' | 'Tracking' | 'Crane' | 'Handheld';
|
|
36
|
+
lens?: string; // e.g. "24mm", "35mm", "50mm", "85mm"
|
|
37
|
+
subject: string;
|
|
38
|
+
action: string;
|
|
39
|
+
audioOrSfx?: string;
|
|
40
|
+
durationSeconds: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface VoBlock {
|
|
44
|
+
id: string;
|
|
45
|
+
timecode: string; // e.g. "00-10s"
|
|
46
|
+
text: string;
|
|
47
|
+
targetDurationSeconds: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface VoAnalysisResult {
|
|
51
|
+
blocks: Array<{
|
|
52
|
+
id: string;
|
|
53
|
+
timecode: string;
|
|
54
|
+
text: string;
|
|
55
|
+
wordCount: number;
|
|
56
|
+
targetDurationSeconds: number;
|
|
57
|
+
wpm: number;
|
|
58
|
+
status: 'optimal' | 'too_fast' | 'too_slow';
|
|
59
|
+
}>;
|
|
60
|
+
totalWords: number;
|
|
61
|
+
totalDurationSeconds: number;
|
|
62
|
+
averageWpm: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate industry-standard Hollywood Screenplay HTML + CSS.
|
|
67
|
+
* Courier 12pt, 1.5in left margin, centered character cues, constrained dialogue width.
|
|
68
|
+
*/
|
|
69
|
+
export function formatScreenplayHtml(spec: ScreenplaySpec): { html: string; css: string } {
|
|
70
|
+
const css = `
|
|
71
|
+
/* Hollywood Industry Standard Screenplay Layout */
|
|
72
|
+
@page {
|
|
73
|
+
size: letter portrait;
|
|
74
|
+
margin: 1.0in 1.0in 1.0in 1.5in;
|
|
75
|
+
@top-right {
|
|
76
|
+
content: counter(page) ".";
|
|
77
|
+
font-family: "Courier Prime", "Courier Final Draft", Courier, monospace;
|
|
78
|
+
font-size: 10pt;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
@page :first {
|
|
82
|
+
@top-right { content: normal; }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
html {
|
|
86
|
+
-webkit-print-color-adjust: exact;
|
|
87
|
+
print-color-adjust: exact;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
body {
|
|
91
|
+
margin: 0;
|
|
92
|
+
padding: 0;
|
|
93
|
+
font-family: "Courier Prime", "Courier Final Draft", Courier, monospace;
|
|
94
|
+
font-size: 12pt;
|
|
95
|
+
line-height: 1.0;
|
|
96
|
+
color: #000000;
|
|
97
|
+
background: #ffffff;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.script-title-page {
|
|
101
|
+
page-break-after: always;
|
|
102
|
+
height: 9.0in;
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
align-items: center;
|
|
107
|
+
text-align: center;
|
|
108
|
+
}
|
|
109
|
+
.script-title {
|
|
110
|
+
font-size: 18pt;
|
|
111
|
+
font-weight: bold;
|
|
112
|
+
letter-spacing: 0.1em;
|
|
113
|
+
text-transform: uppercase;
|
|
114
|
+
margin-bottom: 24pt;
|
|
115
|
+
}
|
|
116
|
+
.script-by {
|
|
117
|
+
font-size: 12pt;
|
|
118
|
+
margin-bottom: 12pt;
|
|
119
|
+
}
|
|
120
|
+
.script-author {
|
|
121
|
+
font-size: 12pt;
|
|
122
|
+
text-transform: uppercase;
|
|
123
|
+
margin-bottom: 48pt;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.element-scene_heading {
|
|
127
|
+
font-weight: bold;
|
|
128
|
+
text-transform: uppercase;
|
|
129
|
+
margin-top: 18pt;
|
|
130
|
+
margin-bottom: 10pt;
|
|
131
|
+
page-break-after: avoid;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.element-action {
|
|
135
|
+
margin-bottom: 12pt;
|
|
136
|
+
line-height: 1.25;
|
|
137
|
+
text-align: left;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.element-character {
|
|
141
|
+
text-transform: uppercase;
|
|
142
|
+
margin-left: 2.2in;
|
|
143
|
+
margin-top: 14pt;
|
|
144
|
+
margin-bottom: 0;
|
|
145
|
+
page-break-after: avoid;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.element-parenthetical {
|
|
149
|
+
margin-left: 1.6in;
|
|
150
|
+
width: 2.5in;
|
|
151
|
+
margin-top: 0;
|
|
152
|
+
margin-bottom: 0;
|
|
153
|
+
page-break-after: avoid;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.element-dialogue {
|
|
157
|
+
margin-left: 1.0in;
|
|
158
|
+
width: 3.5in;
|
|
159
|
+
line-height: 1.2;
|
|
160
|
+
margin-top: 2pt;
|
|
161
|
+
margin-bottom: 12pt;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.element-transition {
|
|
165
|
+
text-transform: uppercase;
|
|
166
|
+
text-align: right;
|
|
167
|
+
margin-top: 14pt;
|
|
168
|
+
margin-bottom: 14pt;
|
|
169
|
+
page-break-after: avoid;
|
|
170
|
+
}
|
|
171
|
+
`.trim();
|
|
172
|
+
|
|
173
|
+
let bodyContent = '';
|
|
174
|
+
|
|
175
|
+
if (spec.title) {
|
|
176
|
+
bodyContent += `
|
|
177
|
+
<div class="script-title-page">
|
|
178
|
+
<div class="script-title">${escapeHtml(spec.title)}</div>
|
|
179
|
+
<div class="script-by">written by</div>
|
|
180
|
+
<div class="script-author">${escapeHtml(spec.author || 'Anonymous')}</div>
|
|
181
|
+
${spec.contact ? `<div style="margin-top: auto; font-size: 10pt;">${escapeHtml(spec.contact)}</div>` : ''}
|
|
182
|
+
</div>`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
bodyContent += '<div class="script-body">';
|
|
186
|
+
for (const el of spec.elements) {
|
|
187
|
+
const escaped = escapeHtml(el.text);
|
|
188
|
+
switch (el.type) {
|
|
189
|
+
case 'scene_heading':
|
|
190
|
+
bodyContent += `\n<div class="element-scene_heading">${escaped}</div>`;
|
|
191
|
+
break;
|
|
192
|
+
case 'action':
|
|
193
|
+
bodyContent += `\n<div class="element-action">${escaped}</div>`;
|
|
194
|
+
break;
|
|
195
|
+
case 'character':
|
|
196
|
+
bodyContent += `\n<div class="element-character">${escaped}</div>`;
|
|
197
|
+
break;
|
|
198
|
+
case 'parenthetical':
|
|
199
|
+
bodyContent += `\n<div class="element-parenthetical">(${escaped.replace(/^\(|\)$/g, '')})</div>`;
|
|
200
|
+
break;
|
|
201
|
+
case 'dialogue':
|
|
202
|
+
bodyContent += `\n<div class="element-dialogue">${escaped}</div>`;
|
|
203
|
+
break;
|
|
204
|
+
case 'transition':
|
|
205
|
+
bodyContent += `\n<div class="element-transition">${escaped}</div>`;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
bodyContent += '\n</div>';
|
|
210
|
+
|
|
211
|
+
const html = `<!DOCTYPE html>
|
|
212
|
+
<html lang="en">
|
|
213
|
+
<head>
|
|
214
|
+
<meta charset="UTF-8">
|
|
215
|
+
<title>${escapeHtml(spec.title)}</title>
|
|
216
|
+
<style>
|
|
217
|
+
${css}
|
|
218
|
+
</style>
|
|
219
|
+
</head>
|
|
220
|
+
<body>
|
|
221
|
+
${bodyContent}
|
|
222
|
+
</body>
|
|
223
|
+
</html>`;
|
|
224
|
+
|
|
225
|
+
return { html, css };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Format Production Shot List as print-ready HTML table.
|
|
230
|
+
*/
|
|
231
|
+
export function formatShotListHtml(title: string, shots: ProductionShot[]): { html: string; css: string } {
|
|
232
|
+
const css = `
|
|
233
|
+
@page {
|
|
234
|
+
size: A4 landscape;
|
|
235
|
+
margin: 15mm;
|
|
236
|
+
@bottom-right {
|
|
237
|
+
content: "Page " counter(page) " of " counter(pages);
|
|
238
|
+
font-size: 9pt;
|
|
239
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
body {
|
|
243
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
244
|
+
font-size: 9.5pt;
|
|
245
|
+
color: #111827;
|
|
246
|
+
margin: 0;
|
|
247
|
+
padding: 0;
|
|
248
|
+
}
|
|
249
|
+
.header {
|
|
250
|
+
margin-bottom: 16px;
|
|
251
|
+
border-bottom: 2px solid #0f172a;
|
|
252
|
+
padding-bottom: 8px;
|
|
253
|
+
display: flex;
|
|
254
|
+
justify-content: space-between;
|
|
255
|
+
align-items: baseline;
|
|
256
|
+
}
|
|
257
|
+
.title {
|
|
258
|
+
font-size: 16pt;
|
|
259
|
+
font-weight: 700;
|
|
260
|
+
letter-spacing: -0.02em;
|
|
261
|
+
}
|
|
262
|
+
.subtitle {
|
|
263
|
+
font-size: 10pt;
|
|
264
|
+
color: #4b5563;
|
|
265
|
+
}
|
|
266
|
+
table {
|
|
267
|
+
width: 100%;
|
|
268
|
+
border-collapse: collapse;
|
|
269
|
+
margin-top: 8px;
|
|
270
|
+
}
|
|
271
|
+
th {
|
|
272
|
+
background: #0f172a;
|
|
273
|
+
color: #ffffff;
|
|
274
|
+
font-weight: 600;
|
|
275
|
+
text-align: left;
|
|
276
|
+
padding: 8px 10px;
|
|
277
|
+
font-size: 8.5pt;
|
|
278
|
+
text-transform: uppercase;
|
|
279
|
+
letter-spacing: 0.05em;
|
|
280
|
+
}
|
|
281
|
+
td {
|
|
282
|
+
padding: 8px 10px;
|
|
283
|
+
border-bottom: 1px solid #e2e8f0;
|
|
284
|
+
vertical-align: top;
|
|
285
|
+
line-height: 1.4;
|
|
286
|
+
}
|
|
287
|
+
tr:nth-child(even) td {
|
|
288
|
+
background: #f8fafc;
|
|
289
|
+
}
|
|
290
|
+
tr {
|
|
291
|
+
page-break-inside: avoid;
|
|
292
|
+
}
|
|
293
|
+
.badge {
|
|
294
|
+
display: inline-block;
|
|
295
|
+
padding: 2px 6px;
|
|
296
|
+
border-radius: 4px;
|
|
297
|
+
font-size: 7.5pt;
|
|
298
|
+
font-weight: 600;
|
|
299
|
+
background: #e0e7ff;
|
|
300
|
+
color: #3730a3;
|
|
301
|
+
}
|
|
302
|
+
`.trim();
|
|
303
|
+
|
|
304
|
+
let totalDuration = 0;
|
|
305
|
+
const rows = shots
|
|
306
|
+
.map((s) => {
|
|
307
|
+
totalDuration += s.durationSeconds;
|
|
308
|
+
return `
|
|
309
|
+
<tr>
|
|
310
|
+
<td style="font-weight: bold; width: 40px;">${escapeHtml(String(s.shotNumber))}</td>
|
|
311
|
+
<td style="width: 70px;">${escapeHtml(s.scene || '-')}</td>
|
|
312
|
+
<td style="width: 70px;"><span class="badge">${escapeHtml(s.framing)}</span></td>
|
|
313
|
+
<td style="width: 90px;">${escapeHtml(s.angle || 'Eye-level')}</td>
|
|
314
|
+
<td style="width: 90px;">${escapeHtml(s.movement || 'Static')}</td>
|
|
315
|
+
<td style="width: 60px;">${escapeHtml(s.lens || '-')}</td>
|
|
316
|
+
<td><strong>${escapeHtml(s.subject)}</strong> — ${escapeHtml(s.action)}</td>
|
|
317
|
+
<td style="width: 160px; font-size: 8.5pt; color: #475569;">${escapeHtml(s.audioOrSfx || '-')}</td>
|
|
318
|
+
<td style="text-align: right; font-weight: 600; width: 50px;">${s.durationSeconds}s</td>
|
|
319
|
+
</tr>`;
|
|
320
|
+
})
|
|
321
|
+
.join('');
|
|
322
|
+
|
|
323
|
+
const html = `<!DOCTYPE html>
|
|
324
|
+
<html lang="en">
|
|
325
|
+
<head>
|
|
326
|
+
<meta charset="UTF-8">
|
|
327
|
+
<title>${escapeHtml(title)} - Shot List</title>
|
|
328
|
+
<style>${css}</style>
|
|
329
|
+
</head>
|
|
330
|
+
<body>
|
|
331
|
+
<div class="header">
|
|
332
|
+
<div class="title">${escapeHtml(title)} — Production Shot List</div>
|
|
333
|
+
<div class="subtitle">Total Shots: ${shots.length} | Est. Duration: ${totalDuration}s (${(totalDuration / 60).toFixed(1)} min)</div>
|
|
334
|
+
</div>
|
|
335
|
+
<table>
|
|
336
|
+
<thead>
|
|
337
|
+
<tr>
|
|
338
|
+
<th>Shot</th>
|
|
339
|
+
<th>Scene</th>
|
|
340
|
+
<th>Framing</th>
|
|
341
|
+
<th>Angle</th>
|
|
342
|
+
<th>Movement</th>
|
|
343
|
+
<th>Lens</th>
|
|
344
|
+
<th>Subject & Action Description</th>
|
|
345
|
+
<th>Audio / SFX Cue</th>
|
|
346
|
+
<th style="text-align: right;">Dur</th>
|
|
347
|
+
</tr>
|
|
348
|
+
</thead>
|
|
349
|
+
<tbody>
|
|
350
|
+
${rows}
|
|
351
|
+
</tbody>
|
|
352
|
+
</table>
|
|
353
|
+
</body>
|
|
354
|
+
</html>`;
|
|
355
|
+
|
|
356
|
+
return { html, css };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Analyze Voice-Over / Dubbing blocks for Word-Per-Minute (WPM) cadence.
|
|
361
|
+
* Target: 130-155 WPM for natural documentary/commercial narration.
|
|
362
|
+
*/
|
|
363
|
+
export function analyzeVoiceOverTiming(blocks: VoBlock[]): VoAnalysisResult {
|
|
364
|
+
let totalWords = 0;
|
|
365
|
+
let totalDuration = 0;
|
|
366
|
+
|
|
367
|
+
const analyzed = blocks.map((b) => {
|
|
368
|
+
const words = b.text.trim().split(/\s+/).filter(Boolean).length;
|
|
369
|
+
totalWords += words;
|
|
370
|
+
totalDuration += b.targetDurationSeconds;
|
|
371
|
+
|
|
372
|
+
const minutes = b.targetDurationSeconds / 60;
|
|
373
|
+
const wpm = minutes > 0 ? Math.round(words / minutes) : 0;
|
|
374
|
+
|
|
375
|
+
let status: 'optimal' | 'too_fast' | 'too_slow' = 'optimal';
|
|
376
|
+
if (wpm > 165) status = 'too_fast';
|
|
377
|
+
else if (wpm < 115) status = 'too_slow';
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
id: b.id,
|
|
381
|
+
timecode: b.timecode,
|
|
382
|
+
text: b.text,
|
|
383
|
+
wordCount: words,
|
|
384
|
+
targetDurationSeconds: b.targetDurationSeconds,
|
|
385
|
+
wpm,
|
|
386
|
+
status
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const avgMinutes = totalDuration / 60;
|
|
391
|
+
const averageWpm = avgMinutes > 0 ? Math.round(totalWords / avgMinutes) : 0;
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
blocks: analyzed,
|
|
395
|
+
totalWords,
|
|
396
|
+
totalDurationSeconds: totalDuration,
|
|
397
|
+
averageWpm
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function escapeHtml(str: string): string {
|
|
402
|
+
return String(str || '')
|
|
403
|
+
.replace(/&/g, '&')
|
|
404
|
+
.replace(/</g, '<')
|
|
405
|
+
.replace(/>/g, '>')
|
|
406
|
+
.replace(/"/g, '"');
|
|
407
|
+
}
|
package/src/entrypoint.js
CHANGED
|
@@ -194,6 +194,19 @@ program
|
|
|
194
194
|
await execCommand(command);
|
|
195
195
|
});
|
|
196
196
|
|
|
197
|
+
program
|
|
198
|
+
.command('acp')
|
|
199
|
+
.description('Start Causyne as an Agent Client Protocol (ACP) server over stdio')
|
|
200
|
+
.action(async () => {
|
|
201
|
+
const { AcpServer } = await import('./acp/server.js');
|
|
202
|
+
const server = new AcpServer({
|
|
203
|
+
input: process.stdin,
|
|
204
|
+
output: process.stdout,
|
|
205
|
+
agentVersion: getPackageVersion()
|
|
206
|
+
});
|
|
207
|
+
server.start();
|
|
208
|
+
});
|
|
209
|
+
|
|
197
210
|
// Setup fallback for missing commands
|
|
198
211
|
program.on('command:*', function () {
|
|
199
212
|
console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' '));
|
package/src/execSafe.js
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
import { exec, execSync as nodeExecSync } from 'node:child_process';
|
|
2
|
-
|
|
3
2
|
// Compatibility wrapper, NOT a security boundary. These APIs invoke a shell.
|
|
4
3
|
// Callers must enforce approval and validate input; prefer execFile with argv.
|
|
5
4
|
export function execSync(command, options) {
|
|
6
|
-
|
|
5
|
+
return nodeExecSync(command, options);
|
|
7
6
|
}
|
|
8
|
-
|
|
9
7
|
export function execAsync(command, options = {}) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
exec(command, options, (error, stdout, stderr) => {
|
|
10
|
+
if (error) {
|
|
11
|
+
Object.assign(error, { stdout, stderr });
|
|
12
|
+
reject(error);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
resolve(typeof stdout === 'string' ? stdout : String(stdout ?? ''));
|
|
16
|
+
});
|
|
19
17
|
});
|
|
20
|
-
});
|
|
21
18
|
}
|
package/src/execSafe.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
exec,
|
|
3
|
+
execSync as nodeExecSync,
|
|
4
|
+
type ExecOptions,
|
|
5
|
+
type ExecSyncOptions
|
|
6
|
+
} from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
// Compatibility wrapper, NOT a security boundary. These APIs invoke a shell.
|
|
9
|
+
// Callers must enforce approval and validate input; prefer execFile with argv.
|
|
10
|
+
export function execSync(command: string, options?: ExecSyncOptions): Buffer | string {
|
|
11
|
+
return nodeExecSync(command, options);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function execAsync(command: string, options: ExecOptions = {}): Promise<string> {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
exec(command, options, (error, stdout, stderr) => {
|
|
17
|
+
if (error) {
|
|
18
|
+
Object.assign(error, { stdout, stderr });
|
|
19
|
+
reject(error);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
resolve(typeof stdout === 'string' ? stdout : String(stdout ?? ''));
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
package/src/modelRouter.js
CHANGED
|
@@ -1,145 +1,78 @@
|
|
|
1
1
|
import { UTILITY_MODEL_BY_PROVIDER } from './modelCatalogue.js';
|
|
2
|
-
|
|
3
|
-
// Hybrid model routing.
|
|
4
|
-
//
|
|
5
|
-
// Not every call an agent makes is reasoning. Condensing a dropped transcript,
|
|
6
|
-
// classifying an intent, filtering a log, extracting keywords — these are
|
|
7
|
-
// utility work, and paying a flagship rate for them is pure waste. The research
|
|
8
|
-
// spec puts the saving at the difference between the two rate cards, which for
|
|
9
|
-
// OpenAI is $5/$30 against $0.75/$4.50: roughly 6x on work that never needed
|
|
10
|
-
// the better model.
|
|
11
|
-
//
|
|
12
|
-
// THREE RULES THIS MODULE EXISTS TO ENFORCE, all of them things a naive router
|
|
13
|
-
// gets wrong:
|
|
14
|
-
//
|
|
15
|
-
// 1. NEVER CHANGE PROVIDER. Causyne is BYOK. The user holds one key. A router
|
|
16
|
-
// that sends utility work to "whatever is cheapest on the market" calls an
|
|
17
|
-
// endpoint there is no credential for and the task fails — having spent
|
|
18
|
-
// the tokens that got it there.
|
|
19
|
-
//
|
|
20
|
-
// 2. NEVER CHANGE THE PRIVACY POSTURE. Muse's cheap tier is `-contributor`,
|
|
21
|
-
// discounted because the vendor trains on the content. Routing into it to
|
|
22
|
-
// save money would opt the user into training silently. Cost is not a
|
|
23
|
-
// reason to make that choice for someone.
|
|
24
|
-
//
|
|
25
|
-
// 3. NEVER GUESS. A provider with no published second tier routes to nothing
|
|
26
|
-
// and pays full rate. An invented model id becomes a 404 mid-task, or
|
|
27
|
-
// worse, a real model at a price nobody predicted.
|
|
28
|
-
//
|
|
29
|
-
// The router returns a DESCRIPTION of where a call should go. It never
|
|
30
|
-
// constructs a provider and never performs a call, so it stays trivially
|
|
31
|
-
// testable and cannot become a second place where credentials are handled.
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* What a call is for.
|
|
35
|
-
*
|
|
36
|
-
* `reasoning` — planning, code, architecture, tool selection, anything the
|
|
37
|
-
* user's result quality depends on. Always the configured model.
|
|
38
|
-
* `utility` — mechanical text work whose output is consumed by the machine
|
|
39
|
-
* rather than by the user's judgement.
|
|
40
|
-
*
|
|
41
|
-
* @typedef {'reasoning' | 'utility'} RouteClass
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @typedef {Object} Route
|
|
46
|
-
* @property {string} provider The provider to call. Always the configured one.
|
|
47
|
-
* @property {string} model The model id to request.
|
|
48
|
-
* @property {boolean} routed True when this differs from the configured model.
|
|
49
|
-
* @property {string} reason Why, in one line. Written to the audit trail.
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
2
|
/**
|
|
53
3
|
* Utility work that is cheap to get wrong and expensive to run on a flagship.
|
|
54
4
|
* Anything not listed is treated as reasoning — the safe default is to spend
|
|
55
5
|
* more, never to silently downgrade work the user is judging.
|
|
56
|
-
*
|
|
57
|
-
* @type {ReadonlySet<string>}
|
|
58
6
|
*/
|
|
59
7
|
export const UTILITY_KINDS = new Set([
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
8
|
+
'condense', // cortex/autoCondenser semantic pass
|
|
9
|
+
'keywords', // plan-cache intent extraction
|
|
10
|
+
'classify', // intent / archetype disambiguation
|
|
11
|
+
'filter', // log and output filtering
|
|
12
|
+
'summarise' // non-decision summaries
|
|
65
13
|
]);
|
|
66
|
-
|
|
67
14
|
/**
|
|
68
15
|
* Decide where a call of a given kind should go.
|
|
69
16
|
*
|
|
70
|
-
* @param
|
|
71
|
-
*
|
|
72
|
-
* @
|
|
73
|
-
* Either a RouteClass or one of UTILITY_KINDS.
|
|
74
|
-
* @returns {Route}
|
|
17
|
+
* @param config The live agent config, as loaded from `~/.causyne/config.json`.
|
|
18
|
+
* @param kind Either a RouteClass or one of UTILITY_KINDS.
|
|
19
|
+
* @returns The resolved route description.
|
|
75
20
|
*/
|
|
76
21
|
export function resolveRoute(config, kind) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
22
|
+
const provider = String(config?.provider || '').toLowerCase();
|
|
23
|
+
const configured = String(config?.model || '');
|
|
24
|
+
const stay = {
|
|
25
|
+
provider,
|
|
26
|
+
model: configured,
|
|
27
|
+
routed: false,
|
|
28
|
+
reason: 'reasoning work runs on the configured model'
|
|
29
|
+
};
|
|
30
|
+
if (!provider || !configured) {
|
|
31
|
+
return { ...stay, reason: 'no provider or model configured' };
|
|
32
|
+
}
|
|
33
|
+
const isUtility = kind === 'utility' || UTILITY_KINDS.has(String(kind));
|
|
34
|
+
if (!isUtility)
|
|
35
|
+
return stay;
|
|
36
|
+
if (config?.disableModelRouting === true) {
|
|
37
|
+
return { ...stay, reason: 'model routing disabled by config' };
|
|
38
|
+
}
|
|
39
|
+
// An explicit choice by the user outranks the table, including a deliberate
|
|
40
|
+
// choice of a training tier — they made it, we did not make it for them.
|
|
41
|
+
const override = config?.modelRouting?.['utility'];
|
|
42
|
+
if (override) {
|
|
43
|
+
return {
|
|
44
|
+
provider,
|
|
45
|
+
model: String(override),
|
|
46
|
+
routed: String(override) !== configured,
|
|
47
|
+
reason: 'utility route set explicitly in config'
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const cheap = UTILITY_MODEL_BY_PROVIDER[provider];
|
|
51
|
+
if (!cheap) {
|
|
52
|
+
return { ...stay, reason: `no published utility tier for ${provider}; paying full rate` };
|
|
53
|
+
}
|
|
54
|
+
if (cheap === configured) {
|
|
55
|
+
return { ...stay, reason: 'already on this provider\'s utility tier' };
|
|
56
|
+
}
|
|
57
|
+
// Rule 2. The configured model already carries a privacy posture; a route may
|
|
58
|
+
// not alter it in either direction.
|
|
59
|
+
if (/-contributor$/.test(configured) !== /-contributor$/.test(cheap)) {
|
|
60
|
+
return { ...stay, reason: 'a route may not change the training-data posture' };
|
|
61
|
+
}
|
|
103
62
|
return {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
63
|
+
provider,
|
|
64
|
+
model: cheap,
|
|
65
|
+
routed: true,
|
|
66
|
+
reason: `utility work routed to ${provider}'s cheaper tier`
|
|
108
67
|
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const cheap = UTILITY_MODEL_BY_PROVIDER[provider];
|
|
112
|
-
if (!cheap) {
|
|
113
|
-
return { ...stay, reason: `no published utility tier for ${provider}; paying full rate` };
|
|
114
|
-
}
|
|
115
|
-
if (cheap === configured) {
|
|
116
|
-
return { ...stay, reason: 'already on this provider\'s utility tier' };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Rule 2. The configured model already carries a privacy posture; a route may
|
|
120
|
-
// not alter it in either direction.
|
|
121
|
-
if (/-contributor$/.test(configured) !== /-contributor$/.test(cheap)) {
|
|
122
|
-
return { ...stay, reason: 'a route may not change the training-data posture' };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
provider,
|
|
127
|
-
model: cheap,
|
|
128
|
-
routed: true,
|
|
129
|
-
reason: `utility work routed to ${provider}'s cheaper tier`
|
|
130
|
-
};
|
|
131
68
|
}
|
|
132
|
-
|
|
133
69
|
/**
|
|
134
70
|
* A config object for the routed call, leaving everything except the model
|
|
135
71
|
* untouched — same provider, same key, same base URL.
|
|
136
|
-
*
|
|
137
|
-
* @param {Record<string, any>} config
|
|
138
|
-
* @param {RouteClass|string} kind
|
|
139
|
-
* @returns {{config: Record<string, any>, route: Route}}
|
|
140
72
|
*/
|
|
141
73
|
export function routedConfig(config, kind) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
74
|
+
const route = resolveRoute(config, kind);
|
|
75
|
+
if (!route.routed)
|
|
76
|
+
return { config, route };
|
|
77
|
+
return { config: { ...config, model: route.model }, route };
|
|
145
78
|
}
|