nubos-pilot 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/np-tools/_commands.cjs +2 -0
- package/bin/np-tools/_elision-proxy-entry.cjs +13 -0
- package/bin/np-tools/elision-bench.cjs +67 -0
- package/bin/np-tools/elision-get.cjs +48 -0
- package/bin/np-tools/elision-get.test.cjs +66 -0
- package/bin/np-tools/loop-run-round.cjs +25 -11
- package/bin/np-tools/plan-milestone.cjs +1 -0
- package/bin/np-tools/research-phase.cjs +1 -1
- package/bin/np-tools/spawn-headless.cjs +62 -9
- package/lib/cache-align.cjs +78 -0
- package/lib/cache-align.test.cjs +69 -0
- package/lib/compress.cjs +495 -0
- package/lib/compress.test.cjs +267 -0
- package/lib/config-defaults.cjs +39 -0
- package/lib/config-schema.cjs +40 -4
- package/lib/elision-bench.cjs +409 -0
- package/lib/elision-bench.test.cjs +89 -0
- package/lib/elision-proxy.cjs +158 -0
- package/lib/elision-proxy.test.cjs +243 -0
- package/lib/elision.cjs +163 -0
- package/lib/elision.test.cjs +143 -0
- package/lib/nubosloop.cjs +1 -1
- package/lib/output-steering.cjs +68 -0
- package/lib/output-steering.test.cjs +74 -0
- package/lib/researcher-swarm.cjs +14 -3
- package/lib/runtime/agent-loop.cjs +36 -6
- package/lib/runtime/agent-loop.test.cjs +105 -0
- package/lib/runtime/dispatch.cjs +6 -6
- package/lib/runtime/dispatch.test.cjs +17 -3
- package/lib/runtime/providers/openai-compat.cjs +2 -1
- package/lib/runtime/providers/openai-compat.test.cjs +9 -0
- package/lib/runtime/tools/index.cjs +33 -1
- package/lib/runtime/tools/index.test.cjs +24 -0
- package/lib/schemas/data/elision-entry.v1.json +16 -0
- package/lib/token-cost.cjs +46 -0
- package/lib/token-cost.test.cjs +42 -0
- package/np-tools.cjs +2 -0
- package/package.json +1 -1
|
@@ -89,6 +89,8 @@ const COMMANDS = [
|
|
|
89
89
|
{ name: 'knowledge-index', category: 'Utility', description: 'Build BM25-light index over .nubos-pilot/**/*.md → .nubos-pilot/state/knowledge-index.json', description_de: 'Baut BM25-Light-Index über .nubos-pilot/**/*.md → .nubos-pilot/state/knowledge-index.json' },
|
|
90
90
|
{ name: 'knowledge-search', category: 'Utility', description: 'Query the knowledge index; returns top-N JSON hits (rel_path + lines + score + preview). Pass --task <id> inside a Nubosloop task to record Rule 9 audit evidence', description_de: 'Sucht im Knowledge-Index; liefert Top-N-JSON-Treffer (rel_path + Zeilen + Score + Preview). --task <id> innerhalb eines Nubosloop-Tasks schreibt den Rule-9-Audit-Nachweis' },
|
|
91
91
|
{ name: 'knowledge-stats', category: 'Utility', description: 'Print knowledge-index size + grouping (auto-builds if missing)', description_de: 'Gibt Knowledge-Index-Größe + Gruppierung aus (baut auto bei Fehlen)' },
|
|
92
|
+
{ name: 'elision-get', category: 'Utility', description: 'Retrieve the original text behind a ⟦elided:<hash>⟧ compression marker. Reversible context compression (ADR-Elision). Positional <hash> or --hash; --json for envelope', description_de: 'Holt den Originaltext hinter einem ⟦elided:<hash>⟧ Kompressions-Marker. Reversible Kontext-Kompression (ADR-Elision). Positional <hash> oder --hash; --json für Envelope' },
|
|
93
|
+
{ name: 'elision-bench', category: 'Utility', description: 'Measure elision compression: deterministic fidelity (ratio + critical-line preservation + byte-exact reversibility) over a fixture corpus; --with-model adds answer-equivalence (raw vs compressed). --size <small|medium|large> runs a scaled corpus with a deterministic --holdout control group (--max-cases N) and an estimated token saving (--price-per-mtok <p> [--currency EUR] adds a cost estimate). --json, --tier <name>', description_de: 'Misst die Elision-Kompression: deterministische Fidelity (Ratio + Erhalt kritischer Zeilen + byte-exakte Reversibilität) über ein Fixture-Korpus; --with-model ergänzt Antwort-Äquivalenz (roh vs. komprimiert). --size <small|medium|large> fährt ein skaliertes Korpus mit deterministischer --holdout-Kontrollgruppe (--max-cases N) und geschätzter Token-Ersparnis (--price-per-mtok <p> [--currency EUR] ergänzt eine Kostenschätzung). --json, --tier <name>' },
|
|
92
94
|
{ name: 'context-stats', category: 'Utility', description: 'Aggregated context-budget stats (file counts + bytes per group, knowledge-index size)', description_de: 'Aggregierte Context-Budget-Stats (Dateien/Bytes pro Gruppe, Knowledge-Index-Größe)' },
|
|
93
95
|
{ name: 'session-snapshot-write', category: 'Utility', description: 'Capture session snapshot (current_task + recent commits + open handoffs) for resume', description_de: 'Erfasst Session-Snapshot (current_task + letzte Commits + offene Handoffs) für Resume' },
|
|
94
96
|
{ name: 'session-snapshot-read', category: 'Utility', description: 'Print last session snapshot as JSON', description_de: 'Gibt letzten Session-Snapshot als JSON aus' },
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const proxy = require('../../lib/elision-proxy.cjs');
|
|
4
|
+
|
|
5
|
+
proxy.start({
|
|
6
|
+
cwd: process.env.ELISION_PROXY_CWD || process.cwd(),
|
|
7
|
+
upstream: process.env.ELISION_PROXY_UPSTREAM || undefined,
|
|
8
|
+
}).then(({ baseUrl }) => {
|
|
9
|
+
if (process.send) process.send({ ready: true, baseUrl });
|
|
10
|
+
}).catch((err) => {
|
|
11
|
+
if (process.send) process.send({ ready: false, error: String(err && err.message) });
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const bench = require('../../lib/elision-bench.cjs');
|
|
4
|
+
|
|
5
|
+
function _parseArgs(args) {
|
|
6
|
+
const out = { json: false, withModel: false, tier: 'frontier', size: null, holdout: 0.2, maxCases: null, charsPerToken: null, pricePerMTok: null, currency: null };
|
|
7
|
+
for (let i = 0; i < (args || []).length; i += 1) {
|
|
8
|
+
const a = args[i];
|
|
9
|
+
if (a === '--json') { out.json = true; continue; }
|
|
10
|
+
if (a === '--with-model') { out.withModel = true; continue; }
|
|
11
|
+
if (a === '--tier') { out.tier = args[++i] || out.tier; continue; }
|
|
12
|
+
if (a === '--size') { out.size = args[++i] || out.size; continue; }
|
|
13
|
+
if (a === '--holdout') { const v = parseFloat(args[++i]); if (Number.isFinite(v)) out.holdout = v; continue; }
|
|
14
|
+
if (a === '--max-cases') { const v = parseInt(args[++i], 10); if (Number.isFinite(v)) out.maxCases = v; continue; }
|
|
15
|
+
if (a === '--chars-per-token') { const v = parseFloat(args[++i]); if (Number.isFinite(v)) out.charsPerToken = v; continue; }
|
|
16
|
+
if (a === '--price-per-mtok') { const v = parseFloat(args[++i]); if (Number.isFinite(v)) out.pricePerMTok = v; continue; }
|
|
17
|
+
if (a === '--currency') { out.currency = args[++i] || out.currency; continue; }
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function run(args, ctx) {
|
|
23
|
+
const context = ctx || {};
|
|
24
|
+
const cwd = context.cwd || process.cwd();
|
|
25
|
+
const stdout = context.stdout || process.stdout;
|
|
26
|
+
const parsed = _parseArgs(args || []);
|
|
27
|
+
|
|
28
|
+
if (parsed.size) {
|
|
29
|
+
const scale = bench.runScale({ cwd, size: parsed.size, holdoutRatio: parsed.holdout, maxCases: parsed.maxCases, charsPerToken: parsed.charsPerToken, pricePerMTok: parsed.pricePerMTok, currency: parsed.currency });
|
|
30
|
+
stdout.write((parsed.json ? JSON.stringify({ scale }) : bench.formatScale(scale)) + '\n');
|
|
31
|
+
return scale.summary.invariants_ok ? 0 : 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const fidelity = bench.runFidelity({ cwd });
|
|
35
|
+
let equivalence = null;
|
|
36
|
+
|
|
37
|
+
if (parsed.withModel) {
|
|
38
|
+
const { resolveFromConfig } = require('./resolve-model.cjs');
|
|
39
|
+
const { chat } = require('../../lib/runtime/providers/openai-compat.cjs');
|
|
40
|
+
const res = resolveFromConfig({ agentOrTier: parsed.tier, cwd, format: 'json' });
|
|
41
|
+
if (!res || !res.model || !res.baseUrl) {
|
|
42
|
+
stdout.write((parsed.json ? JSON.stringify({ fidelity, equivalence: null, error: 'no-provider' }) : 'No off-host provider resolved for tier "' + parsed.tier + '" — equivalence skipped.') + '\n');
|
|
43
|
+
return fidelity.summary.invariants_ok ? 0 : 1;
|
|
44
|
+
}
|
|
45
|
+
const provider = { baseUrl: res.baseUrl, apiKeyEnv: res.apiKeyEnv, model: res.model };
|
|
46
|
+
equivalence = await bench.runEquivalence({ chatImpl: chat, provider });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (parsed.json) {
|
|
50
|
+
stdout.write(JSON.stringify({ fidelity, equivalence }) + '\n');
|
|
51
|
+
} else {
|
|
52
|
+
stdout.write(bench.formatReport(fidelity) + '\n');
|
|
53
|
+
if (equivalence) {
|
|
54
|
+
stdout.write('\nAnswer equivalence — ' + equivalence.summary.equivalent + '/' + equivalence.summary.cases
|
|
55
|
+
+ ' equivalent, regressions: ' + (equivalence.summary.regressions.length ? equivalence.summary.regressions.join(', ') : 'none') + '\n');
|
|
56
|
+
for (const c of equivalence.cases) {
|
|
57
|
+
stdout.write(' ' + (c.regression ? '✗' : '✓') + ' ' + c.name + ' (raw '
|
|
58
|
+
+ (c.raw_ok ? 'ok' : 'miss') + ', compressed ' + (c.compressed_ok ? 'ok' : 'miss') + ')\n');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ok = fidelity.summary.invariants_ok && (!equivalence || equivalence.summary.no_regression);
|
|
64
|
+
return ok ? 0 : 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { run, _parseArgs };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
4
|
+
const elision = require('../../lib/elision.cjs');
|
|
5
|
+
|
|
6
|
+
function _parseArgs(args) {
|
|
7
|
+
const out = { hash: null, json: false };
|
|
8
|
+
const positional = [];
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
10
|
+
const a = args[i];
|
|
11
|
+
if (a === '--hash') { out.hash = args[++i] || null; continue; }
|
|
12
|
+
if (a === '--json') { out.json = true; continue; }
|
|
13
|
+
positional.push(a);
|
|
14
|
+
}
|
|
15
|
+
if (out.hash == null) out.hash = positional.join('').trim() || null;
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function run(args, ctx) {
|
|
20
|
+
const context = ctx || {};
|
|
21
|
+
const cwd = context.cwd || process.cwd();
|
|
22
|
+
const stdout = context.stdout || process.stdout;
|
|
23
|
+
const parsed = _parseArgs(args || []);
|
|
24
|
+
if (!parsed.hash) {
|
|
25
|
+
throw new NubosPilotError(
|
|
26
|
+
'elision-get-missing-hash',
|
|
27
|
+
'elision-get requires a hash (positional or --hash)',
|
|
28
|
+
{ hint: 'hash is the 12-char id from a ⟦elided:…⟧ marker' },
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const result = elision.retrieve(parsed.hash, cwd);
|
|
32
|
+
if (parsed.json) {
|
|
33
|
+
stdout.write(JSON.stringify(result));
|
|
34
|
+
return result.status === 'ok' ? 0 : 1;
|
|
35
|
+
}
|
|
36
|
+
if (result.status === 'ok') {
|
|
37
|
+
stdout.write(result.original);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
if (result.status === 'expired') {
|
|
41
|
+
stdout.write('[elision ' + parsed.hash + ' expired — original no longer cached (ttl elapsed)]\n');
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
stdout.write('[elision ' + parsed.hash + ' not found]\n');
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { run, _parseArgs };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const elisionGet = require('./elision-get.cjs');
|
|
10
|
+
const elision = require('../../lib/elision.cjs');
|
|
11
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
12
|
+
|
|
13
|
+
function sandbox() {
|
|
14
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-elisionget-'));
|
|
15
|
+
fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
|
|
16
|
+
return root;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function capture() {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
return { stream: { write: (s) => chunks.push(s) }, text: () => chunks.join('') };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test('ELISIONGET-1: prints the original for a known hash', () => {
|
|
25
|
+
const root = sandbox();
|
|
26
|
+
try {
|
|
27
|
+
const hash = elision.store('the original payload', { type: 'plain' }, root);
|
|
28
|
+
const out = capture();
|
|
29
|
+
const code = elisionGet.run([hash], { cwd: root, stdout: out.stream });
|
|
30
|
+
assert.equal(code, 0);
|
|
31
|
+
assert.equal(out.text(), 'the original payload');
|
|
32
|
+
} finally {
|
|
33
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('ELISIONGET-2: unknown hash exits 1 with a not-found notice', () => {
|
|
38
|
+
const root = sandbox();
|
|
39
|
+
try {
|
|
40
|
+
const out = capture();
|
|
41
|
+
const code = elisionGet.run(['aaaaaaaaaaaa'], { cwd: root, stdout: out.stream });
|
|
42
|
+
assert.equal(code, 1);
|
|
43
|
+
assert.match(out.text(), /not found/);
|
|
44
|
+
} finally {
|
|
45
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('ELISIONGET-3: --json emits the retrieval envelope', () => {
|
|
50
|
+
const root = sandbox();
|
|
51
|
+
try {
|
|
52
|
+
const hash = elision.store('payload', { type: 'plain' }, root);
|
|
53
|
+
const out = capture();
|
|
54
|
+
const code = elisionGet.run([hash, '--json'], { cwd: root, stdout: out.stream });
|
|
55
|
+
assert.equal(code, 0);
|
|
56
|
+
const env = JSON.parse(out.text());
|
|
57
|
+
assert.equal(env.status, 'ok');
|
|
58
|
+
assert.equal(env.original, 'payload');
|
|
59
|
+
} finally {
|
|
60
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('ELISIONGET-4: missing hash throws', () => {
|
|
65
|
+
assert.throws(() => elisionGet.run([], { cwd: process.cwd(), stdout: { write() {} } }), NubosPilotError);
|
|
66
|
+
});
|
|
@@ -6,16 +6,37 @@ const path = require('node:path');
|
|
|
6
6
|
const { NubosPilotError, safeAssign } = require('../../lib/core.cjs');
|
|
7
7
|
const safePath = require('../../lib/safe-path.cjs');
|
|
8
8
|
|
|
9
|
-
function _resolveInsideCwdOrTmp(p, cwd, label, errorCode) {
|
|
10
|
-
return safePath.assertInsideCwdOrTmp(p, cwd, label, errorCode);
|
|
11
|
-
}
|
|
12
9
|
const checkpoint = require('../../lib/checkpoint.cjs');
|
|
13
10
|
const nubosloop = require('../../lib/nubosloop.cjs');
|
|
14
11
|
const messaging = require('../../lib/messaging.cjs');
|
|
12
|
+
const compress = require('../../lib/compress.cjs');
|
|
13
|
+
const elision = require('../../lib/elision.cjs');
|
|
14
|
+
const logger = require('../../lib/logger.cjs').child('loop-run-round');
|
|
15
15
|
const args = require('./_args.cjs');
|
|
16
16
|
|
|
17
17
|
const { TASK_ID_RE } = require('../../lib/ids.cjs');
|
|
18
18
|
|
|
19
|
+
function _resolveInsideCwdOrTmp(p, cwd, label, errorCode) {
|
|
20
|
+
return safePath.assertInsideCwdOrTmp(p, cwd, label, errorCode);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _verifyExcerpt(verifyOutput, cwd) {
|
|
24
|
+
const s = String(verifyOutput);
|
|
25
|
+
const cx = elision.compressionContext(cwd);
|
|
26
|
+
const maxBytes = cx.verifyMaxBytes;
|
|
27
|
+
if (s.length <= maxBytes) return s;
|
|
28
|
+
if (cx.enabled) {
|
|
29
|
+
try {
|
|
30
|
+
const crushed = compress.crushLogToBudget(s, maxBytes);
|
|
31
|
+
if (crushed && crushed.length < s.length) {
|
|
32
|
+
const hash = cx.store ? cx.store(s, 'log') : null;
|
|
33
|
+
return crushed + (hash ? '\n' + compress.marker(hash, 'full verify log') : '');
|
|
34
|
+
}
|
|
35
|
+
} catch (err) { logger.warn('verify compression skipped', { cause: err && err.message }); }
|
|
36
|
+
}
|
|
37
|
+
return '…[truncated head, original ' + s.length + ' bytes — see VERIFY_LOG]\n' + s.slice(-maxBytes);
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
const VALID_PHASES = new Set([
|
|
20
41
|
'preflight',
|
|
21
42
|
'post-researcher',
|
|
@@ -182,16 +203,9 @@ function _runPostExecutor(taskId, list, cwd) {
|
|
|
182
203
|
}
|
|
183
204
|
}
|
|
184
205
|
const green = code === 0;
|
|
185
|
-
const VERIFY_TAIL_BYTES = 2000;
|
|
186
206
|
let verifyExcerpt = null;
|
|
187
207
|
if (verifyOutput) {
|
|
188
|
-
|
|
189
|
-
if (s.length > VERIFY_TAIL_BYTES) {
|
|
190
|
-
verifyExcerpt = '…[truncated head, original ' + s.length + ' bytes — see VERIFY_LOG]\n'
|
|
191
|
-
+ s.slice(-VERIFY_TAIL_BYTES);
|
|
192
|
-
} else {
|
|
193
|
-
verifyExcerpt = s;
|
|
194
|
-
}
|
|
208
|
+
verifyExcerpt = _verifyExcerpt(verifyOutput, cwd);
|
|
195
209
|
}
|
|
196
210
|
const merged = checkpoint.mergeCheckpoint(
|
|
197
211
|
taskId,
|
|
@@ -107,7 +107,7 @@ async function run(args, ctx) {
|
|
|
107
107
|
const tmDetail = textMode.resolveTextModeDetail(cwd);
|
|
108
108
|
|
|
109
109
|
const swarmOpts = swarm.resolveSwarmOpts(cwd);
|
|
110
|
-
const spawnSpecs = swarm.buildSpawnSpecs({ milestone: mNum, milestone_id: mIdStr, goal: def.goal || '' }, swarmOpts.k);
|
|
110
|
+
const spawnSpecs = swarm.buildSpawnSpecs({ milestone: mNum, milestone_id: mIdStr, goal: def.goal || '' }, swarmOpts.k, { cwd });
|
|
111
111
|
|
|
112
112
|
let cacheHit = null;
|
|
113
113
|
let cacheMiss = null;
|
|
@@ -9,8 +9,35 @@ const { NubosPilotError, atomicWriteFileSync, appendJsonl, findProjectRoot } = r
|
|
|
9
9
|
const runContext = require('../../lib/run-context.cjs');
|
|
10
10
|
const safePath = require('../../lib/safe-path.cjs');
|
|
11
11
|
const headlessGuard = require('../../lib/headless-guard.cjs');
|
|
12
|
+
const compress = require('../../lib/compress.cjs');
|
|
13
|
+
const elision = require('../../lib/elision.cjs');
|
|
14
|
+
const elisionProxy = require('../../lib/elision-proxy.cjs');
|
|
15
|
+
const steering = require('../../lib/output-steering.cjs');
|
|
16
|
+
const logger = require('../../lib/logger.cjs').child('spawn-headless');
|
|
12
17
|
const args = require('./_args.cjs');
|
|
13
18
|
|
|
19
|
+
const PROXY_START_TIMEOUT_MS = 5000;
|
|
20
|
+
|
|
21
|
+
function _startCompressionProxy(cwd) {
|
|
22
|
+
const upstream = process.env.ANTHROPIC_BASE_URL || '';
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const proc = child_process.fork(path.join(__dirname, '_elision-proxy-entry.cjs'), [], {
|
|
25
|
+
env: Object.assign({}, process.env, { ELISION_PROXY_CWD: cwd, ELISION_PROXY_UPSTREAM: upstream }),
|
|
26
|
+
stdio: ['ignore', 'ignore', 'inherit', 'ipc'],
|
|
27
|
+
});
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
try { proc.kill(); } catch { /* already gone */ }
|
|
30
|
+
reject(new NubosPilotError('elision-proxy-start-timeout', 'compression proxy did not report ready in time', {}));
|
|
31
|
+
}, PROXY_START_TIMEOUT_MS);
|
|
32
|
+
proc.once('message', (m) => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
if (m && m.ready) resolve({ proc, baseUrl: m.baseUrl });
|
|
35
|
+
else { try { proc.kill(); } catch { /* already gone */ } reject(new NubosPilotError('elision-proxy-start-failed', String(m && m.error), {})); }
|
|
36
|
+
});
|
|
37
|
+
proc.once('error', (err) => { clearTimeout(timer); reject(err); });
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
14
41
|
function _sha256(s) {
|
|
15
42
|
return crypto.createHash('sha256').update(s == null ? '' : String(s)).digest('hex');
|
|
16
43
|
}
|
|
@@ -159,6 +186,20 @@ function _composePrompt(agentBody, userPrompt) {
|
|
|
159
186
|
return agentBody.trimEnd() + '\n\n---\n\n' + userPrompt.trimEnd() + '\n';
|
|
160
187
|
}
|
|
161
188
|
|
|
189
|
+
function _maybeCompressPrompt(prompt, cwd) {
|
|
190
|
+
const none = { prompt, stats: null };
|
|
191
|
+
const cx = elision.compressionContext(cwd);
|
|
192
|
+
if (!cx.enabled) return none;
|
|
193
|
+
try {
|
|
194
|
+
const out = compress.compressPrompt(prompt, { minBlockBytes: cx.minBlockBytes, store: cx.store });
|
|
195
|
+
if (!out || out.stats.blocks_compressed === 0) return none;
|
|
196
|
+
return { prompt: out.text, stats: out.stats };
|
|
197
|
+
} catch (err) {
|
|
198
|
+
logger.warn('compression skipped', { cause: err && err.message });
|
|
199
|
+
return none;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
162
203
|
function _stripFrontmatter(md) {
|
|
163
204
|
if (!md.startsWith('---\n')) return md;
|
|
164
205
|
const end = md.indexOf('\n---\n', 4);
|
|
@@ -304,20 +345,15 @@ async function run(argv, ctx) {
|
|
|
304
345
|
}
|
|
305
346
|
|
|
306
347
|
const agentPath = _resolveAgentPath(agent, cwd);
|
|
307
|
-
|
|
348
|
+
let agentBody = _stripFrontmatter(fs.readFileSync(agentPath, 'utf-8'));
|
|
349
|
+
const _os = elision.compressionContext(cwd).outputSteering;
|
|
350
|
+
if (_os && _os.enabled) agentBody = steering.enrichSystemPrompt(agentBody, _os.profile);
|
|
308
351
|
const userPrompt = _readPromptFile(promptPath, cwd);
|
|
309
|
-
|
|
352
|
+
let composedPrompt = _composePrompt(agentBody, userPrompt);
|
|
310
353
|
const resolvedOutput = _ensureOutputDir(outputPath, cwd);
|
|
311
354
|
|
|
312
355
|
const runId = runContext.getRunId();
|
|
313
356
|
|
|
314
|
-
// ADR-0021: off-host routing. If the agent routes to an openai-compat provider,
|
|
315
|
-
// run the nubos-pilot dispatch loop instead of `claude -p`, writing the result
|
|
316
|
-
// in the same {result,...} envelope `claude --output-format json` produces so the
|
|
317
|
-
// callers (review/extract parseXxxOutput → outer.result) parse it unchanged. The
|
|
318
|
-
// native path below is untouched. run() is async; the CLI dispatcher already
|
|
319
|
-
// awaits returned promises (handleRunResult), and the two in-process callers
|
|
320
|
-
// (review.cjs / extract.cjs) await runReview/runExtract.
|
|
321
357
|
const resolveKind = context.resolveImpl || _defaultResolveKind;
|
|
322
358
|
let routed;
|
|
323
359
|
try { routed = resolveKind(agent, cwd); } catch { routed = { kind: 'native' }; }
|
|
@@ -329,6 +365,9 @@ async function run(argv, ctx) {
|
|
|
329
365
|
});
|
|
330
366
|
}
|
|
331
367
|
|
|
368
|
+
const compression = _maybeCompressPrompt(composedPrompt, cwd);
|
|
369
|
+
composedPrompt = compression.prompt;
|
|
370
|
+
|
|
332
371
|
let lockRoot;
|
|
333
372
|
try { lockRoot = findProjectRoot(cwd); }
|
|
334
373
|
catch { lockRoot = cwd; }
|
|
@@ -344,6 +383,17 @@ async function run(argv, ctx) {
|
|
|
344
383
|
const childEnv = _filterSpawnEnv(process.env);
|
|
345
384
|
Object.assign(childEnv, headlessGuard.childSpawnEnv(process.env));
|
|
346
385
|
|
|
386
|
+
let proxyProc = null;
|
|
387
|
+
if (elisionProxy.proxyEnabled(cwd)) {
|
|
388
|
+
try {
|
|
389
|
+
const started = await _startCompressionProxy(cwd);
|
|
390
|
+
proxyProc = started.proc;
|
|
391
|
+
childEnv.ANTHROPIC_BASE_URL = started.baseUrl;
|
|
392
|
+
} catch (err) {
|
|
393
|
+
logger.warn('compression proxy unavailable; spawning without it', { cause: err && err.message });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
347
397
|
const bin = _claudeBinary();
|
|
348
398
|
const claudeArgs = ['-p', '--output-format', 'json'];
|
|
349
399
|
const startedAt = new Date().toISOString();
|
|
@@ -365,6 +415,7 @@ async function run(argv, ctx) {
|
|
|
365
415
|
{ bin, cause: err && err.code },
|
|
366
416
|
);
|
|
367
417
|
} finally {
|
|
418
|
+
if (proxyProc) { try { proxyProc.kill(); } catch { /* already exited */ } }
|
|
368
419
|
lock.release();
|
|
369
420
|
}
|
|
370
421
|
if (result.error && result.error.code === 'ENOENT') {
|
|
@@ -406,6 +457,7 @@ async function run(argv, ctx) {
|
|
|
406
457
|
tokens_in: claudeMeta.tokens_in == null ? null : claudeMeta.tokens_in,
|
|
407
458
|
tokens_out: claudeMeta.tokens_out == null ? null : claudeMeta.tokens_out,
|
|
408
459
|
payload_parse_ok: claudeMeta.parse_ok,
|
|
460
|
+
compression: compression.stats || null,
|
|
409
461
|
};
|
|
410
462
|
try { appendJsonl(spawnTrailPath, spawnRecord, { maxLineBytes: 16 * 1024, mode: 0o600 }); }
|
|
411
463
|
catch (err) {
|
|
@@ -431,6 +483,7 @@ async function run(argv, ctx) {
|
|
|
431
483
|
tokens_in: spawnRecord.tokens_in,
|
|
432
484
|
tokens_out: spawnRecord.tokens_out,
|
|
433
485
|
payload_parse_ok: spawnRecord.payload_parse_ok,
|
|
486
|
+
compression: compression.stats || null,
|
|
434
487
|
};
|
|
435
488
|
stdout.write(JSON.stringify(payload) + '\n');
|
|
436
489
|
if (exitCode !== 0) return 2;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
|
|
4
|
+
const ISO_RE = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
|
|
5
|
+
const JWT_RE = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/;
|
|
6
|
+
const HEX_RE = /\b[0-9a-f]{32,64}\b/i;
|
|
7
|
+
const VOLATILE = Object.freeze([
|
|
8
|
+
['uuid', UUID_RE],
|
|
9
|
+
['iso_datetime', ISO_RE],
|
|
10
|
+
['jwt', JWT_RE],
|
|
11
|
+
['hex_hash', HEX_RE],
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function _systemText(system) {
|
|
15
|
+
if (typeof system === 'string') return system;
|
|
16
|
+
if (Array.isArray(system)) {
|
|
17
|
+
return system.map((b) => (b && typeof b.text === 'string' ? b.text : '')).join('\n');
|
|
18
|
+
}
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function detectVolatile(system) {
|
|
23
|
+
const text = _systemText(system);
|
|
24
|
+
const findings = [];
|
|
25
|
+
for (const [kind, re] of VOLATILE) {
|
|
26
|
+
const m = text.match(re);
|
|
27
|
+
if (m) findings.push({ kind, sample_len: m[0].length });
|
|
28
|
+
}
|
|
29
|
+
return findings;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _sortKeys(v) {
|
|
33
|
+
if (Array.isArray(v)) return v.map(_sortKeys);
|
|
34
|
+
if (v && typeof v === 'object') {
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const k of Object.keys(v).sort()) out[k] = _sortKeys(v[k]);
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function _byCodepoint(a, b) {
|
|
43
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeTools(tools) {
|
|
47
|
+
return tools
|
|
48
|
+
.map(_sortKeys)
|
|
49
|
+
.map((t) => ({ t, name: String(t && t.name), body: JSON.stringify(t) }))
|
|
50
|
+
.sort((a, b) => _byCodepoint(a.name, b.name) || _byCodepoint(a.body, b.body))
|
|
51
|
+
.map((x) => x.t);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function _anyCacheControl(system, tools) {
|
|
55
|
+
if (Array.isArray(system) && system.some((b) => b && b.cache_control)) return true;
|
|
56
|
+
if (Array.isArray(tools) && tools.some((t) => t && t.cache_control)) return true;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function alignAnthropicBody(body) {
|
|
61
|
+
const findings = detectVolatile(body && body.system);
|
|
62
|
+
if (!body || !Array.isArray(body.tools) || body.tools.length === 0) {
|
|
63
|
+
return { body, findings, applied: false };
|
|
64
|
+
}
|
|
65
|
+
if (_anyCacheControl(body.system, body.tools)) {
|
|
66
|
+
return { body, findings, applied: false };
|
|
67
|
+
}
|
|
68
|
+
const tools = normalizeTools(body.tools);
|
|
69
|
+
const last = tools.length - 1;
|
|
70
|
+
tools[last] = Object.assign({}, tools[last], { cache_control: { type: 'ephemeral' } });
|
|
71
|
+
return { body: Object.assign({}, body, { tools }), findings, applied: true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
detectVolatile,
|
|
76
|
+
normalizeTools,
|
|
77
|
+
alignAnthropicBody,
|
|
78
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const ca = require('./cache-align.cjs');
|
|
7
|
+
|
|
8
|
+
test('CA-1: detectVolatile flags UUID/ISO-date/JWT/hex in the system prompt', () => {
|
|
9
|
+
const sys = 'Run id 550e8400-e29b-41d4-a716-446655440000 at 2026-06-23T10:00:00Z '
|
|
10
|
+
+ 'token eyJhbGc.eyJzdWI.sig hash d41d8cd98f00b204e9800998ecf8427e';
|
|
11
|
+
const kinds = ca.detectVolatile(sys).map((f) => f.kind).sort();
|
|
12
|
+
assert.deepEqual(kinds, ['hex_hash', 'iso_datetime', 'jwt', 'uuid']);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('CA-2: detectVolatile reads an array-form system prompt and is clean when stable', () => {
|
|
16
|
+
assert.deepEqual(ca.detectVolatile([{ type: 'text', text: 'You are a stable agent.' }]), []);
|
|
17
|
+
assert.deepEqual(ca.detectVolatile('plain stable instructions'), []);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('CA-3: normalizeTools sorts tools by name and recursively sorts schema keys; arrays keep order', () => {
|
|
21
|
+
const tools = [
|
|
22
|
+
{ name: 'zeta', description: 'z', input_schema: { type: 'object', required: ['b', 'a'], properties: { y: {}, x: {} } } },
|
|
23
|
+
{ name: 'alpha', description: 'a', input_schema: { properties: { n: {} }, type: 'object' } },
|
|
24
|
+
];
|
|
25
|
+
const out = ca.normalizeTools(tools);
|
|
26
|
+
assert.deepEqual(out.map((t) => t.name), ['alpha', 'zeta']);
|
|
27
|
+
assert.deepEqual(Object.keys(out[1]), ['description', 'input_schema', 'name']);
|
|
28
|
+
assert.deepEqual(Object.keys(out[1].input_schema.properties), ['x', 'y']);
|
|
29
|
+
assert.deepEqual(out[1].input_schema.required, ['b', 'a']);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('CA-4: alignAnthropicBody adds exactly one ephemeral breakpoint on the last tool and is idempotent', () => {
|
|
33
|
+
const body = { system: 'sys', tools: [{ name: 'b' }, { name: 'a' }], messages: [] };
|
|
34
|
+
const r1 = ca.alignAnthropicBody(body);
|
|
35
|
+
assert.equal(r1.applied, true);
|
|
36
|
+
assert.deepEqual(r1.body.tools.map((t) => t.name), ['a', 'b']);
|
|
37
|
+
assert.equal(r1.body.tools[0].cache_control, undefined);
|
|
38
|
+
assert.deepEqual(r1.body.tools[1].cache_control, { type: 'ephemeral' });
|
|
39
|
+
const r2 = ca.alignAnthropicBody(r1.body);
|
|
40
|
+
assert.equal(r2.applied, false, 'second pass is a no-op — breakpoint already present');
|
|
41
|
+
assert.equal(JSON.stringify(r2.body), JSON.stringify(r1.body), 'byte-identical second pass');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('CA-4b: normalizeTools is byte-deterministic regardless of input order (codepoint, not locale)', () => {
|
|
45
|
+
const a = [{ name: 'get_A' }, { name: 'get-a' }, { name: 'get_a' }, { name: 'tool10' }, { name: 'tool2' }];
|
|
46
|
+
const b = a.slice().reverse();
|
|
47
|
+
assert.equal(JSON.stringify(ca.normalizeTools(a)), JSON.stringify(ca.normalizeTools(b)), 'order-independent, stable bytes');
|
|
48
|
+
assert.deepEqual(ca.normalizeTools(a).map((t) => t.name), ['get-a', 'get_A', 'get_a', 'tool10', 'tool2']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('CA-4c: duplicate/degenerate names still order deterministically via body tiebreak', () => {
|
|
52
|
+
const x = [{ name: 'x', id: 2 }, { name: 'x', id: 1 }];
|
|
53
|
+
assert.equal(JSON.stringify(ca.normalizeTools(x)), JSON.stringify(ca.normalizeTools(x.slice().reverse())));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('CA-5: customer-placement-wins — a caller-set cache_control is never disturbed', () => {
|
|
57
|
+
const body = { tools: [{ name: 'z', cache_control: { type: 'ephemeral' } }, { name: 'a' }], messages: [] };
|
|
58
|
+
const r = ca.alignAnthropicBody(body);
|
|
59
|
+
assert.equal(r.applied, false);
|
|
60
|
+
assert.deepEqual(r.body.tools.map((t) => t.name), ['z', 'a'], 'tool order untouched when caller manages caching');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('CA-6: no tools → no normalization, detection still runs', () => {
|
|
64
|
+
const body = { system: 'id 550e8400-e29b-41d4-a716-446655440000', messages: [] };
|
|
65
|
+
const r = ca.alignAnthropicBody(body);
|
|
66
|
+
assert.equal(r.applied, false);
|
|
67
|
+
assert.equal(r.body, body);
|
|
68
|
+
assert.deepEqual(r.findings.map((f) => f.kind), ['uuid']);
|
|
69
|
+
});
|