@nemus-cli/nemus 0.2.13 → 0.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/CHANGELOG.md +31 -0
- package/README.md +17 -0
- package/dist/cli/ai-prompt.js +13 -25
- package/dist/commands/reflect.js +160 -0
- package/dist/program.js +2 -0
- package/dist/utils/agent-judge.js +137 -0
- package/dist/utils/reflect.js +353 -0
- package/package.json +1 -1
- package/src/cli/ai-prompt.ts +12 -22
- package/src/commands/reflect.ts +172 -0
- package/src/program.ts +2 -0
- package/src/utils/agent-judge.test.ts +88 -0
- package/src/utils/agent-judge.ts +168 -0
- package/src/utils/reflect.test.ts +103 -0
- package/src/utils/reflect.ts +405 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`reflect` now shows live progress and no longer looks hung.** The judge call
|
|
13
|
+
ran synchronously (`execFileSync`), which blocked the event loop, and a
|
|
14
|
+
10-workspace run could hit the 180s cap and die with a raw
|
|
15
|
+
`spawnSync pi ETIMEDOUT`. Now: (1) the gather phase prints a **per-workspace
|
|
16
|
+
line** as each session is read (`✓ 1/3 my-workspace 635 turns · 12 prompts ·
|
|
17
|
+
13 failures`); (2) the judge runs **async** behind a live spinner with elapsed
|
|
18
|
+
seconds; (3) the timeout is raised to 300s, overridable via
|
|
19
|
+
`NEMUS_JUDGE_TIMEOUT_MS`, and a timeout now yields an **actionable** message
|
|
20
|
+
("try a smaller --limit, a faster agent, or raise the cap"); (4) the judge
|
|
21
|
+
prompt is leaner (fewer prompts/errors per session) so a local model actually
|
|
22
|
+
finishes.
|
|
23
|
+
|
|
24
|
+
## [0.3.0] - 2026-08-27
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **`nemus reflect` (alias `retro`) — LLM-as-a-judge retrospective.** Analyzes
|
|
29
|
+
your most recent workspaces (default 10) by reading their agent session
|
|
30
|
+
transcripts, distilling the human prompts + tool failures + tools used, and
|
|
31
|
+
asking your own configured agent (claude/pi/opencode — no API key of ours) to
|
|
32
|
+
recommend concrete setup improvements: which **skills** to add and where,
|
|
33
|
+
missing **AGENTS.md/context** rules, missing **connectivity/smoke tests**, and
|
|
34
|
+
**prompt/workflow** habits to change — each with a priority and a concrete
|
|
35
|
+
example snippet. Reads both Claude and pi transcript formats. Flags:
|
|
36
|
+
`--limit <n>`, `--json` (structured report, same `{ok:false,error}` failure
|
|
37
|
+
contract as the other JSON commands), and `--dry-run` (print the assembled
|
|
38
|
+
corpus + judge prompt without calling the agent). Idea courtesy of
|
|
39
|
+
**@lightpriest** — thank you for the great suggestion!
|
|
40
|
+
|
|
10
41
|
## [0.2.13] - 2026-08-27
|
|
11
42
|
|
|
12
43
|
### Added
|
package/README.md
CHANGED
|
@@ -280,6 +280,23 @@ nemus snapshot save ws # (ss) capture exact branches/commits/dirty state
|
|
|
280
280
|
nemus snapshot restore <id> # (sr)
|
|
281
281
|
```
|
|
282
282
|
|
|
283
|
+
### Reflect — improve your setup over time
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
nemus reflect # (retro) analyze your last 10 workspaces' sessions
|
|
287
|
+
nemus reflect --limit 5 # narrow the window
|
|
288
|
+
nemus reflect --json # structured report for tooling
|
|
289
|
+
nemus reflect --dry-run # show what the judge sees, without calling the agent
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`reflect` reads your recent agent **session transcripts** (Claude + pi), distills
|
|
293
|
+
the prompts you sent, the failures the agent hit, and the tools it used, then asks
|
|
294
|
+
**your own configured agent** (LLM-as-a-judge — no extra API key) to recommend
|
|
295
|
+
concrete improvements: which **skills** to add and where, missing
|
|
296
|
+
**AGENTS.md/context** rules, missing **connectivity/smoke tests**, and
|
|
297
|
+
**prompt/workflow** habits — each with a priority and an example snippet. It's a
|
|
298
|
+
fast retrospective on *how you drive the agent*, so next time works better.
|
|
299
|
+
|
|
283
300
|
### AI assistant
|
|
284
301
|
|
|
285
302
|
```bash
|
package/dist/cli/ai-prompt.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.extractIntent = extractIntent;
|
|
|
41
41
|
exports.run = run;
|
|
42
42
|
exports.main = main;
|
|
43
43
|
const child_process_1 = require("child_process");
|
|
44
|
+
const agent_judge_1 = require("../utils/agent-judge");
|
|
44
45
|
const util_1 = require("util");
|
|
45
46
|
const fs = __importStar(require("fs"));
|
|
46
47
|
const path = __importStar(require("path"));
|
|
@@ -267,35 +268,22 @@ async function extractIntent(prompt) {
|
|
|
267
268
|
];
|
|
268
269
|
result = runExtraction('pi', [...piLean, ...piCore], piCore);
|
|
269
270
|
}
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
const parsed = JSON.parse(jsonStr);
|
|
277
|
-
// Handle different output formats:
|
|
278
|
-
// - Claude: { structured_output: {...} } or { result: "..." }
|
|
279
|
-
// - Pi: may return the object directly or wrap it
|
|
271
|
+
// Unwrap the agent's reply (code fences + the structured_output / result-string
|
|
272
|
+
// / result-object / bare-object envelopes) with the shared parser, so this and
|
|
273
|
+
// the reflect judge can't drift when a new agent shape is learned. The
|
|
274
|
+
// extraction-specific INVOCATION (lean flags + tailored timeout/auth errors)
|
|
275
|
+
// deliberately stays here — those messages are part of the `nemus --` UX.
|
|
280
276
|
let intent;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
intent = JSON.parse(parsed.result);
|
|
287
|
-
}
|
|
288
|
-
catch { /* ignore */ }
|
|
289
|
-
}
|
|
290
|
-
else if (typeof parsed.result === 'object' && parsed.result !== null) {
|
|
291
|
-
intent = parsed.result;
|
|
277
|
+
try {
|
|
278
|
+
const parsed = (0, agent_judge_1.parseAgentJson)(result);
|
|
279
|
+
if (parsed && typeof parsed === 'object')
|
|
280
|
+
intent = parsed;
|
|
292
281
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
intent = parsed;
|
|
282
|
+
catch {
|
|
283
|
+
throw new Error(`Could not extract intent from agent response: ${result.slice(0, 200)}`);
|
|
296
284
|
}
|
|
297
285
|
if (!intent) {
|
|
298
|
-
throw new Error(`Could not extract intent from agent response
|
|
286
|
+
throw new Error(`Could not extract intent from agent response: ${result.slice(0, 200)}`);
|
|
299
287
|
}
|
|
300
288
|
// Coerce/validate field types so malformed model output can't crash the
|
|
301
289
|
// downstream .trim()/sanitize path (a stringly-typed workspaceName or a
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerReflectCommand = registerReflectCommand;
|
|
4
|
+
const logger_1 = require("../utils/logger");
|
|
5
|
+
const output_1 = require("../utils/output");
|
|
6
|
+
const colors_1 = require("../utils/colors");
|
|
7
|
+
const reflect_1 = require("../utils/reflect");
|
|
8
|
+
const agent_judge_1 = require("../utils/agent-judge");
|
|
9
|
+
function registerReflectCommand(parent) {
|
|
10
|
+
parent
|
|
11
|
+
.command('reflect')
|
|
12
|
+
.alias('retro')
|
|
13
|
+
.description('Analyze your recent workspace sessions and suggest skill/prompt/context improvements (LLM-as-a-judge)')
|
|
14
|
+
.option('-n, --limit <n>', 'How many recent workspaces to analyze', '10')
|
|
15
|
+
.option('--json', 'Output the report as JSON')
|
|
16
|
+
.option('--dry-run', 'Print the assembled corpus + judge prompt without calling the agent')
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
await handleReflect(opts);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async function handleReflect(opts) {
|
|
22
|
+
const limit = Math.max(1, Number.parseInt(opts.limit ?? '10', 10) || 10);
|
|
23
|
+
try {
|
|
24
|
+
const showProgress = !opts.json && !opts.dryRun;
|
|
25
|
+
if (showProgress) {
|
|
26
|
+
(0, logger_1.logStep)(`Analyzing your ${(0, colors_1.colorize)(String(limit), 'cyan')} most recent workspaces…`);
|
|
27
|
+
}
|
|
28
|
+
const corpus = await (0, reflect_1.gatherReflectionCorpus)(limit, showProgress ? printProgress : undefined);
|
|
29
|
+
const withSessions = corpus.workspaces.filter((w) => w.session).length;
|
|
30
|
+
const prompt = (0, reflect_1.buildJudgePrompt)(corpus);
|
|
31
|
+
if (opts.dryRun) {
|
|
32
|
+
// No LLM call — surface exactly what the judge would see.
|
|
33
|
+
if (opts.json)
|
|
34
|
+
(0, output_1.outputJson)({ corpus, prompt });
|
|
35
|
+
else {
|
|
36
|
+
process.stdout.write(prompt + '\n');
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (withSessions === 0) {
|
|
41
|
+
const msg = 'No recent agent sessions found to analyze (need Claude/pi session transcripts).';
|
|
42
|
+
if (opts.json)
|
|
43
|
+
(0, output_1.outputJsonError)(msg);
|
|
44
|
+
else
|
|
45
|
+
(0, logger_1.logError)(msg);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
// The judge shells the user's own agent and can take minutes; run it async
|
|
49
|
+
// (non-blocking) so a live spinner shows it's alive, not hung. Timeout is
|
|
50
|
+
// overridable for slow local models.
|
|
51
|
+
const timeoutMs = Number.parseInt(process.env.NEMUS_JUDGE_TIMEOUT_MS ?? '', 10) || undefined;
|
|
52
|
+
const stopSpinner = opts.json
|
|
53
|
+
? () => { }
|
|
54
|
+
: startSpinner(`Judging ${withSessions} session(s) with your configured agent (this can take a minute)…`);
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = await (0, agent_judge_1.runAgentJsonAsync)(prompt, { schema: reflect_1.REFLECT_SCHEMA, timeoutMs });
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
stopSpinner();
|
|
61
|
+
}
|
|
62
|
+
const report = (0, reflect_1.parseReflectionReport)(parsed);
|
|
63
|
+
if (opts.json) {
|
|
64
|
+
(0, output_1.outputJson)({ analyzed: withSessions, workspaces: corpus.workspaces.length, ...report });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
printReport(report, corpus.workspaces.length, withSessions);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const msg = error instanceof Error ? error.message : 'reflect failed';
|
|
71
|
+
if (opts.json)
|
|
72
|
+
(0, output_1.outputJsonError)(msg);
|
|
73
|
+
else {
|
|
74
|
+
(0, logger_1.logError)('Failed to analyze sessions');
|
|
75
|
+
(0, logger_1.logError)(msg);
|
|
76
|
+
}
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const KIND_LABEL = {
|
|
81
|
+
skill: 'Skill',
|
|
82
|
+
context: 'Context/AGENTS.md',
|
|
83
|
+
test: 'Test',
|
|
84
|
+
prompt: 'Prompt',
|
|
85
|
+
connectivity: 'Connectivity',
|
|
86
|
+
workflow: 'Workflow',
|
|
87
|
+
other: 'Other',
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* A minimal stderr spinner with elapsed seconds. Returns a stop() that clears
|
|
91
|
+
* the line. No-op (single log line) when stderr isn't a TTY (piped/CI), so it
|
|
92
|
+
* never pollutes captured output. Kept local + tiny — no new dependency.
|
|
93
|
+
*/
|
|
94
|
+
function startSpinner(text) {
|
|
95
|
+
if (!process.stderr.isTTY) {
|
|
96
|
+
(0, logger_1.logInfo)(text);
|
|
97
|
+
return () => { };
|
|
98
|
+
}
|
|
99
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
100
|
+
const start = Date.now();
|
|
101
|
+
let i = 0;
|
|
102
|
+
const render = () => {
|
|
103
|
+
const secs = Math.floor((Date.now() - start) / 1000);
|
|
104
|
+
process.stderr.write(`\r${(0, colors_1.colorize)(frames[(i = (i + 1) % frames.length)], 'cyan')} ${text} ${(0, colors_1.colorize)(`(${secs}s)`, 'dim')}`);
|
|
105
|
+
};
|
|
106
|
+
render();
|
|
107
|
+
const timer = setInterval(render, 100);
|
|
108
|
+
timer.unref?.(); // never keep the process alive on our account
|
|
109
|
+
return () => {
|
|
110
|
+
clearInterval(timer);
|
|
111
|
+
process.stderr.write('\r' + ' '.repeat(text.length + 24) + '\r');
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** Live per-workspace line during the gather phase (to stderr — stdout stays
|
|
115
|
+
* reserved for the report / JSON). */
|
|
116
|
+
function printProgress(p) {
|
|
117
|
+
const n = (0, colors_1.colorize)(`${p.index + 1}/${p.total}`, 'dim');
|
|
118
|
+
const d = p.digest.session;
|
|
119
|
+
if (!d) {
|
|
120
|
+
process.stderr.write(` ${(0, colors_1.colorize)('·', 'dim')} ${n} ${p.digest.name} ${(0, colors_1.colorize)('— no session', 'dim')}\n`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const failures = `${d.errors.length} ${d.errors.length === 1 ? 'failure' : 'failures'}`;
|
|
124
|
+
const stats = (0, colors_1.colorize)(`${d.turns} turns · ${d.userPrompts.length} prompts · ${failures}`, 'dim');
|
|
125
|
+
process.stderr.write(` ${(0, colors_1.colorize)('✓', 'green')} ${n} ${p.digest.name} ${stats}\n`);
|
|
126
|
+
}
|
|
127
|
+
function priorityBadge(p) {
|
|
128
|
+
if (p === 'high')
|
|
129
|
+
return (0, colors_1.colorize)('● high', 'red');
|
|
130
|
+
if (p === 'medium')
|
|
131
|
+
return (0, colors_1.colorize)('● med', 'yellow');
|
|
132
|
+
return (0, colors_1.colorize)('● low', 'gray');
|
|
133
|
+
}
|
|
134
|
+
function printReport(report, workspaces, analyzed) {
|
|
135
|
+
console.log('');
|
|
136
|
+
console.log((0, colors_1.colorize)(' Reflection', 'bright') + (0, colors_1.colorize)(` (${analyzed} sessions across ${workspaces} workspaces)`, 'dim'));
|
|
137
|
+
console.log((0, colors_1.colorize)(' ' + '─'.repeat(56), 'dim'));
|
|
138
|
+
if (report.summary) {
|
|
139
|
+
console.log('\n ' + report.summary.replace(/\n/g, '\n '));
|
|
140
|
+
}
|
|
141
|
+
if (report.recommendations.length === 0) {
|
|
142
|
+
console.log('\n ' + (0, colors_1.colorize)('No specific recommendations — looks solid.', 'green') + '\n');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// High priority first.
|
|
146
|
+
const order = { high: 0, medium: 1, low: 2 };
|
|
147
|
+
const recs = [...report.recommendations].sort((a, b) => order[a.priority] - order[b.priority]);
|
|
148
|
+
console.log('');
|
|
149
|
+
for (const r of recs) {
|
|
150
|
+
const target = r.target ? (0, colors_1.colorize)(` [${r.target}]`, 'cyan') : '';
|
|
151
|
+
console.log(` ${priorityBadge(r.priority)} ${(0, colors_1.colorize)(KIND_LABEL[r.kind], 'bright')} ${r.title}${target}`);
|
|
152
|
+
if (r.detail)
|
|
153
|
+
console.log(` ${r.detail.replace(/\n/g, '\n ')}`);
|
|
154
|
+
if (r.example) {
|
|
155
|
+
console.log((0, colors_1.colorize)(' example:', 'dim'));
|
|
156
|
+
console.log((0, colors_1.colorize)(r.example.replace(/^/gm, ' '), 'dim'));
|
|
157
|
+
}
|
|
158
|
+
console.log('');
|
|
159
|
+
}
|
|
160
|
+
}
|
package/dist/program.js
CHANGED
|
@@ -89,6 +89,7 @@ const save_context_1 = require("./commands/save-context");
|
|
|
89
89
|
const migrate_1 = require("./commands/migrate");
|
|
90
90
|
const report_bug_1 = require("./commands/report-bug");
|
|
91
91
|
const completion_1 = require("./commands/completion");
|
|
92
|
+
const reflect_1 = require("./commands/reflect");
|
|
92
93
|
(0, create_1.registerCreateCommand)(exports.program);
|
|
93
94
|
(0, list_1.registerListCommand)(exports.program);
|
|
94
95
|
(0, update_1.registerUpdateCommand)(exports.program);
|
|
@@ -113,6 +114,7 @@ const completion_1 = require("./commands/completion");
|
|
|
113
114
|
(0, migrate_1.registerMigrateCommand)(exports.program);
|
|
114
115
|
(0, report_bug_1.registerReportBugCommand)(exports.program);
|
|
115
116
|
(0, completion_1.registerCompletionCommand)(exports.program);
|
|
117
|
+
(0, reflect_1.registerReflectCommand)(exports.program);
|
|
116
118
|
// Register TUI (delegates to existing Ink/React implementation)
|
|
117
119
|
exports.program
|
|
118
120
|
.command('tui')
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.agentAttempts = agentAttempts;
|
|
4
|
+
exports.runAgentRaw = runAgentRaw;
|
|
5
|
+
exports.runAgentRawAsync = runAgentRawAsync;
|
|
6
|
+
exports.runAgentJson = runAgentJson;
|
|
7
|
+
exports.runAgentJsonAsync = runAgentJsonAsync;
|
|
8
|
+
exports.parseAgentJson = parseAgentJson;
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const util_1 = require("util");
|
|
11
|
+
const agent_config_1 = require("./agent-config");
|
|
12
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
13
|
+
/**
|
|
14
|
+
* The ordered invocation attempts for an agent (preferred → fallback), as pure
|
|
15
|
+
* data so both the sync and async runners share ONE flag ladder (and it's
|
|
16
|
+
* unit-testable without spawning anything).
|
|
17
|
+
*/
|
|
18
|
+
function agentAttempts(agentType, prompt, schema) {
|
|
19
|
+
if (agentType === 'claude') {
|
|
20
|
+
const preferred = ['-p', prompt, '--output-format', 'json', '--bare', '--strict-mcp-config', '--disable-slash-commands'];
|
|
21
|
+
if (schema)
|
|
22
|
+
preferred.push('--json-schema', schema);
|
|
23
|
+
// Older claude may reject the newer flags — fall back to the plainest form.
|
|
24
|
+
return [{ cmd: 'claude', args: preferred }, { cmd: 'claude', args: ['-p', prompt] }];
|
|
25
|
+
}
|
|
26
|
+
if (agentType === 'opencode') {
|
|
27
|
+
return [{ cmd: 'opencode', args: ['run', prompt] }];
|
|
28
|
+
}
|
|
29
|
+
// pi (and any other): run as lean as possible so a bloated env can't hang it.
|
|
30
|
+
const piLean = ['--no-extensions', '--no-skills', '--no-prompt-templates', '--no-context-files', '--no-tools', '--no-session'];
|
|
31
|
+
return [{ cmd: 'pi', args: [...piLean, '-p', prompt] }, { cmd: 'pi', args: ['-p', prompt] }];
|
|
32
|
+
}
|
|
33
|
+
function wrapJudgeError(err, agentType) {
|
|
34
|
+
// A timeout is the common failure (big prompt + slow local model), so make it
|
|
35
|
+
// actionable instead of surfacing a raw `spawn … ETIMEDOUT`.
|
|
36
|
+
if (err?.killed || err?.code === 'ETIMEDOUT' || err?.signal === 'SIGTERM' || /ETIMEDOUT/.test(String(err?.message ?? ''))) {
|
|
37
|
+
return new Error(`agent judge (${agentType}) timed out. Try a smaller --limit, a faster agent, or raise the cap with NEMUS_JUDGE_TIMEOUT_MS.`);
|
|
38
|
+
}
|
|
39
|
+
const detail = (err?.stderr || err?.stdout || err?.message || 'unknown error').toString().trim().slice(0, 500);
|
|
40
|
+
return new Error(`agent judge failed (${agentType}): ${detail}`);
|
|
41
|
+
}
|
|
42
|
+
const DEFAULT_TIMEOUT_MS = 300000; // judging N transcripts is heavier than extraction; a big prompt + slow model can run minutes
|
|
43
|
+
const DEFAULT_MAX_BUFFER = 32 * 1024 * 1024;
|
|
44
|
+
/**
|
|
45
|
+
* Invoke the agent with `prompt` and return the raw stdout. Throws a clear error
|
|
46
|
+
* on timeout / non-zero exit. Kept separate from parsing so callers can inspect
|
|
47
|
+
* raw output (e.g. `--dry-run`, debugging).
|
|
48
|
+
*/
|
|
49
|
+
function runAgentRaw(prompt, opts = {}) {
|
|
50
|
+
const agentType = opts.agentType ?? (0, agent_config_1.getPrimaryAgent)().type;
|
|
51
|
+
const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
52
|
+
const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
53
|
+
const exec = opts.exec ??
|
|
54
|
+
((cmd, args, o) => (0, child_process_1.execFileSync)(cmd, args, { encoding: 'utf-8', timeout: o.timeout, maxBuffer: o.maxBuffer }));
|
|
55
|
+
const attempts = agentAttempts(agentType, prompt, opts.schema);
|
|
56
|
+
let lastErr;
|
|
57
|
+
for (const a of attempts) {
|
|
58
|
+
try {
|
|
59
|
+
return exec(a.cmd, a.args, { timeout, maxBuffer });
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
lastErr = err; // try the next (fallback) form
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw wrapJudgeError(lastErr, agentType);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Non-blocking twin of {@link runAgentRaw}. Uses `execFile` (async) so the
|
|
69
|
+
* caller's event loop stays free — letting a spinner/progress UI animate while
|
|
70
|
+
* the judge (which can take minutes) runs. Prefer this in interactive commands.
|
|
71
|
+
*/
|
|
72
|
+
async function runAgentRawAsync(prompt, opts = {}) {
|
|
73
|
+
const agentType = opts.agentType ?? (0, agent_config_1.getPrimaryAgent)().type;
|
|
74
|
+
const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
75
|
+
const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
76
|
+
const exec = opts.execAsync ??
|
|
77
|
+
(async (cmd, args, o) => (await execFileAsync(cmd, args, { encoding: 'utf-8', timeout: o.timeout, maxBuffer: o.maxBuffer })).stdout.toString());
|
|
78
|
+
const attempts = agentAttempts(agentType, prompt, opts.schema);
|
|
79
|
+
let lastErr;
|
|
80
|
+
for (const a of attempts) {
|
|
81
|
+
try {
|
|
82
|
+
return await exec(a.cmd, a.args, { timeout, maxBuffer });
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
lastErr = err; // try the next (fallback) form
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw wrapJudgeError(lastErr, agentType);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Run the agent and parse its reply as JSON, tolerating the shapes different
|
|
92
|
+
* agents emit: `{ structured_output }`, `{ result: "<json>" }`, a ```json fence,
|
|
93
|
+
* or a bare object. Returns `unknown`; callers validate/normalize their shape.
|
|
94
|
+
*/
|
|
95
|
+
function runAgentJson(prompt, opts = {}) {
|
|
96
|
+
const raw = runAgentRaw(prompt, opts);
|
|
97
|
+
return parseAgentJson(raw);
|
|
98
|
+
}
|
|
99
|
+
/** Non-blocking twin of {@link runAgentJson}. */
|
|
100
|
+
async function runAgentJsonAsync(prompt, opts = {}) {
|
|
101
|
+
const raw = await runAgentRawAsync(prompt, opts);
|
|
102
|
+
return parseAgentJson(raw);
|
|
103
|
+
}
|
|
104
|
+
/** Extract a JSON object from an agent's raw stdout. Exported for tests. */
|
|
105
|
+
function parseAgentJson(raw) {
|
|
106
|
+
let text = raw.trim();
|
|
107
|
+
const fence = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
108
|
+
if (fence)
|
|
109
|
+
text = fence[1].trim();
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(text);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Last resort: grab the outermost {...} span.
|
|
116
|
+
const span = text.match(/\{[\s\S]*\}/);
|
|
117
|
+
if (!span)
|
|
118
|
+
throw new Error('agent did not return JSON');
|
|
119
|
+
parsed = JSON.parse(span[0]);
|
|
120
|
+
}
|
|
121
|
+
// Unwrap the common agent envelopes.
|
|
122
|
+
if (parsed && typeof parsed === 'object') {
|
|
123
|
+
if (parsed.structured_output && typeof parsed.structured_output === 'object')
|
|
124
|
+
return parsed.structured_output;
|
|
125
|
+
if (typeof parsed.result === 'string') {
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(parsed.result);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* fall through — result was plain text, return the envelope */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (parsed.result && typeof parsed.result === 'object')
|
|
134
|
+
return parsed.result;
|
|
135
|
+
}
|
|
136
|
+
return parsed;
|
|
137
|
+
}
|