karajan-code 1.21.1 → 1.22.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/package.json +1 -1
- package/src/agents/host-agent.js +38 -0
- package/src/mcp/orphan-guard.js +47 -0
- package/src/mcp/server-handlers.js +24 -10
- package/src/mcp/server.js +2 -1
- package/src/orchestrator/iteration-stages.js +32 -5
- package/src/orchestrator/post-loop-stages.js +14 -2
- package/src/orchestrator/pre-loop-stages.js +15 -0
- package/src/orchestrator/reviewer-fallback.js +7 -1
- package/src/orchestrator/solomon-escalation.js +10 -1
- package/src/orchestrator.js +41 -5
- package/src/roles/coder-role.js +12 -2
- package/src/session-store.js +3 -2
- package/src/utils/agent-detect.js +21 -0
package/package.json
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host Agent — delegates task execution to the MCP host AI via elicitation.
|
|
3
|
+
*
|
|
4
|
+
* Instead of spawning a subprocess, returns the prompt to the host AI
|
|
5
|
+
* (Claude, Codex, etc.) for direct execution. The host has full access
|
|
6
|
+
* to the codebase and tools — no subprocess overhead.
|
|
7
|
+
*
|
|
8
|
+
* Used when: the MCP host IS the same agent configured for a role.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { BaseAgent } from "./base-agent.js";
|
|
12
|
+
|
|
13
|
+
export class HostAgent extends BaseAgent {
|
|
14
|
+
constructor(config, logger, { askHost }) {
|
|
15
|
+
super("host", config, logger);
|
|
16
|
+
this._askHost = askHost;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async runTask(task) {
|
|
20
|
+
const { prompt, onOutput } = task;
|
|
21
|
+
|
|
22
|
+
if (!this._askHost) {
|
|
23
|
+
return { ok: false, output: "", error: "Host agent has no askHost callback" };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (onOutput) onOutput({ stream: "info", line: "[host-agent] Delegating to host AI..." });
|
|
27
|
+
|
|
28
|
+
const answer = await this._askHost(prompt);
|
|
29
|
+
|
|
30
|
+
if (!answer) {
|
|
31
|
+
return { ok: false, output: "", error: "Host AI declined or returned no response" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (onOutput) onOutput({ stream: "info", line: "[host-agent] Host AI completed task" });
|
|
35
|
+
|
|
36
|
+
return { ok: true, output: answer, exitCode: 0 };
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/mcp/orphan-guard.js
CHANGED
|
@@ -22,6 +22,53 @@ export function setupOrphanGuard({ intervalMs = DEFAULT_INTERVAL_MS, exitFn = ()
|
|
|
22
22
|
return { timer, parentPid };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
const DEFAULT_MEMORY_CHECK_MS = 30_000;
|
|
26
|
+
const DEFAULT_WARN_HEAP_MB = 512;
|
|
27
|
+
const DEFAULT_CRITICAL_HEAP_MB = 768;
|
|
28
|
+
|
|
29
|
+
export function setupMemoryWatchdog({
|
|
30
|
+
intervalMs = DEFAULT_MEMORY_CHECK_MS,
|
|
31
|
+
warnHeapMb = DEFAULT_WARN_HEAP_MB,
|
|
32
|
+
criticalHeapMb = DEFAULT_CRITICAL_HEAP_MB,
|
|
33
|
+
onWarn = null,
|
|
34
|
+
onCritical = null,
|
|
35
|
+
exitFn = () => process.exit(1)
|
|
36
|
+
} = {}) {
|
|
37
|
+
const warnBytes = warnHeapMb * 1024 * 1024;
|
|
38
|
+
const criticalBytes = criticalHeapMb * 1024 * 1024;
|
|
39
|
+
let warned = false;
|
|
40
|
+
|
|
41
|
+
const timer = setInterval(() => {
|
|
42
|
+
const { heapUsed, rss } = process.memoryUsage();
|
|
43
|
+
|
|
44
|
+
if (heapUsed >= criticalBytes) {
|
|
45
|
+
if (global.gc) {
|
|
46
|
+
try { global.gc(); } catch { /* --expose-gc not set */ }
|
|
47
|
+
const after = process.memoryUsage().heapUsed;
|
|
48
|
+
if (after < criticalBytes) return; // GC freed enough
|
|
49
|
+
}
|
|
50
|
+
const msg = `Memory critical: heap ${(heapUsed / 1024 / 1024).toFixed(0)}MB / rss ${(rss / 1024 / 1024).toFixed(0)}MB — exiting to prevent OOM`;
|
|
51
|
+
if (onCritical) onCritical(msg);
|
|
52
|
+
else process.stderr.write(`[karajan-mcp] ${msg}\n`);
|
|
53
|
+
clearInterval(timer);
|
|
54
|
+
exitFn();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (heapUsed >= warnBytes && !warned) {
|
|
59
|
+
warned = true;
|
|
60
|
+
const msg = `Memory warning: heap ${(heapUsed / 1024 / 1024).toFixed(0)}MB / rss ${(rss / 1024 / 1024).toFixed(0)}MB (critical at ${criticalHeapMb}MB)`;
|
|
61
|
+
if (onWarn) onWarn(msg);
|
|
62
|
+
else process.stderr.write(`[karajan-mcp] ${msg}\n`);
|
|
63
|
+
} else if (heapUsed < warnBytes) {
|
|
64
|
+
warned = false;
|
|
65
|
+
}
|
|
66
|
+
}, intervalMs);
|
|
67
|
+
timer.unref();
|
|
68
|
+
|
|
69
|
+
return { timer };
|
|
70
|
+
}
|
|
71
|
+
|
|
25
72
|
export function setupVersionWatcher({ pkgPath, currentVersion, exitFn = () => process.exit(0) } = {}) {
|
|
26
73
|
if (!pkgPath) return null;
|
|
27
74
|
|
|
@@ -287,22 +287,36 @@ export async function handleResumeDirect(a, server, extra) {
|
|
|
287
287
|
const config = await buildConfig(a);
|
|
288
288
|
const logger = createLogger(config.output.log_level, "mcp");
|
|
289
289
|
|
|
290
|
+
const projectDir = await resolveProjectDir(server);
|
|
291
|
+
const runLog = createRunLog(projectDir);
|
|
292
|
+
runLog.logText(`[kj_resume] started — session="${a.sessionId}"`);
|
|
293
|
+
|
|
290
294
|
const emitter = new EventEmitter();
|
|
291
295
|
emitter.on("progress", buildProgressHandler(server));
|
|
296
|
+
emitter.on("progress", (event) => runLog.logEvent(event));
|
|
292
297
|
const progressNotifier = buildProgressNotifier(extra);
|
|
293
298
|
if (progressNotifier) emitter.on("progress", progressNotifier);
|
|
294
299
|
|
|
295
300
|
const askQuestion = buildAskQuestion(server);
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
301
|
+
try {
|
|
302
|
+
const result = await resumeFlow({
|
|
303
|
+
sessionId: a.sessionId,
|
|
304
|
+
answer: a.answer || null,
|
|
305
|
+
config,
|
|
306
|
+
logger,
|
|
307
|
+
flags: a,
|
|
308
|
+
emitter,
|
|
309
|
+
askQuestion
|
|
310
|
+
});
|
|
311
|
+
const ok = !result.paused && (result.approved !== false);
|
|
312
|
+
runLog.logText(`[kj_resume] finished — ok=${ok}`);
|
|
313
|
+
return { ok, ...result };
|
|
314
|
+
} catch (err) {
|
|
315
|
+
runLog.logText(`[kj_resume] failed: ${err.message}`);
|
|
316
|
+
throw err;
|
|
317
|
+
} finally {
|
|
318
|
+
runLog.close();
|
|
319
|
+
}
|
|
306
320
|
}
|
|
307
321
|
|
|
308
322
|
function buildDirectEmitter(server, runLog, extra) {
|
package/src/mcp/server.js
CHANGED
|
@@ -50,9 +50,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
// --- Orphan process protection + version watcher ---
|
|
53
|
-
import { setupOrphanGuard, setupVersionWatcher } from "./orphan-guard.js";
|
|
53
|
+
import { setupOrphanGuard, setupVersionWatcher, setupMemoryWatchdog } from "./orphan-guard.js";
|
|
54
54
|
setupOrphanGuard();
|
|
55
55
|
setupVersionWatcher({ pkgPath: PKG_PATH, currentVersion: LOADED_VERSION });
|
|
56
|
+
setupMemoryWatchdog();
|
|
56
57
|
|
|
57
58
|
const transport = new StdioServerTransport();
|
|
58
59
|
await mcpServer.connect(transport);
|
|
@@ -256,8 +256,14 @@ async function handleTddFailure({ tddEval, config, logger, emitter, eventBase, s
|
|
|
256
256
|
|
|
257
257
|
export async function runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration, askQuestion }) {
|
|
258
258
|
logger.setContext({ iteration, stage: "tdd" });
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
let tddDiff, untrackedFiles;
|
|
260
|
+
try {
|
|
261
|
+
tddDiff = await generateDiff({ baseRef: session.session_start_sha });
|
|
262
|
+
untrackedFiles = await getUntrackedFiles();
|
|
263
|
+
} catch (err) {
|
|
264
|
+
logger.warn(`TDD diff generation failed: ${err.message}`);
|
|
265
|
+
return { action: "continue", stageResult: { ok: false, summary: `TDD check failed: ${err.message}` } };
|
|
266
|
+
}
|
|
261
267
|
const tddEval = evaluateTddPolicy(tddDiff, config.development, untrackedFiles);
|
|
262
268
|
await addCheckpoint(session, {
|
|
263
269
|
stage: "tdd-policy",
|
|
@@ -366,7 +372,13 @@ export async function runSonarStage({ config, logger, emitter, eventBase, sessio
|
|
|
366
372
|
const sonarRole = new SonarRole({ config, logger, emitter });
|
|
367
373
|
await sonarRole.init({ iteration });
|
|
368
374
|
const sonarStart = Date.now();
|
|
369
|
-
|
|
375
|
+
let sonarOutput;
|
|
376
|
+
try {
|
|
377
|
+
sonarOutput = await sonarRole.run();
|
|
378
|
+
} catch (err) {
|
|
379
|
+
logger.warn(`Sonar threw: ${err.message}`);
|
|
380
|
+
sonarOutput = { ok: false, result: { error: err.message }, summary: `Sonar error: ${err.message}` };
|
|
381
|
+
}
|
|
370
382
|
trackBudget({ role: "sonar", provider: "sonar", result: sonarOutput, duration_ms: Date.now() - sonarStart });
|
|
371
383
|
const sonarResult = sonarOutput.result;
|
|
372
384
|
|
|
@@ -438,7 +450,13 @@ export async function runSonarCloudStage({ config, logger, emitter, eventBase, s
|
|
|
438
450
|
|
|
439
451
|
const { runSonarCloudScan } = await import("../sonar/cloud-scanner.js");
|
|
440
452
|
const scanStart = Date.now();
|
|
441
|
-
|
|
453
|
+
let result;
|
|
454
|
+
try {
|
|
455
|
+
result = await runSonarCloudScan(config);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
logger.warn(`SonarCloud threw: ${err.message}`);
|
|
458
|
+
result = { ok: false, error: err.message };
|
|
459
|
+
}
|
|
442
460
|
trackBudget({ role: "sonarcloud", provider: "sonarcloud", result: { ok: result.ok }, duration_ms: Date.now() - scanStart });
|
|
443
461
|
|
|
444
462
|
await addCheckpoint(session, {
|
|
@@ -550,7 +568,13 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
550
568
|
})
|
|
551
569
|
);
|
|
552
570
|
|
|
553
|
-
|
|
571
|
+
let diff;
|
|
572
|
+
try {
|
|
573
|
+
diff = await fetchReviewDiff(session, logger);
|
|
574
|
+
} catch (err) {
|
|
575
|
+
logger.warn(`Review diff generation failed: ${err.message}`);
|
|
576
|
+
return { approved: false, blocking_issues: [{ description: `Diff generation failed: ${err.message}` }], non_blocking_suggestions: [], summary: `Reviewer failed: cannot generate diff — ${err.message}`, confidence: 0 };
|
|
577
|
+
}
|
|
554
578
|
const reviewerOnOutput = ({ stream, line }) => {
|
|
555
579
|
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "reviewer" }, {
|
|
556
580
|
message: line,
|
|
@@ -575,6 +599,9 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
575
599
|
trackBudget({ role: "reviewer", provider: reviewer, model: reviewerRole.model, result, duration_ms: Date.now() - reviewerStart });
|
|
576
600
|
}
|
|
577
601
|
});
|
|
602
|
+
} catch (err) {
|
|
603
|
+
logger.warn(`Reviewer threw: ${err.message}`);
|
|
604
|
+
reviewerExec = { execResult: { ok: false, error: err.message }, attempts: [{ reviewer: reviewerRole.provider, result: { ok: false, error: err.message } }] };
|
|
578
605
|
} finally {
|
|
579
606
|
reviewerStall.stop();
|
|
580
607
|
}
|
|
@@ -16,7 +16,13 @@ export async function runTesterStage({ config, logger, emitter, eventBase, sessi
|
|
|
16
16
|
const tester = new TesterRole({ config, logger, emitter });
|
|
17
17
|
await tester.init({ task, iteration });
|
|
18
18
|
const testerStart = Date.now();
|
|
19
|
-
|
|
19
|
+
let testerOutput;
|
|
20
|
+
try {
|
|
21
|
+
testerOutput = await tester.run({ task, diff });
|
|
22
|
+
} catch (err) {
|
|
23
|
+
logger.warn(`Tester threw: ${err.message}`);
|
|
24
|
+
testerOutput = { ok: false, summary: `Tester error: ${err.message}`, result: { error: err.message } };
|
|
25
|
+
}
|
|
20
26
|
trackBudget({
|
|
21
27
|
role: "tester",
|
|
22
28
|
provider: config?.roles?.tester?.provider || coderRole.provider,
|
|
@@ -90,7 +96,13 @@ export async function runSecurityStage({ config, logger, emitter, eventBase, ses
|
|
|
90
96
|
const security = new SecurityRole({ config, logger, emitter });
|
|
91
97
|
await security.init({ task, iteration });
|
|
92
98
|
const securityStart = Date.now();
|
|
93
|
-
|
|
99
|
+
let securityOutput;
|
|
100
|
+
try {
|
|
101
|
+
securityOutput = await security.run({ task, diff });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
logger.warn(`Security threw: ${err.message}`);
|
|
104
|
+
securityOutput = { ok: false, summary: `Security error: ${err.message}`, result: { error: err.message } };
|
|
105
|
+
}
|
|
94
106
|
trackBudget({
|
|
95
107
|
role: "security",
|
|
96
108
|
provider: config?.roles?.security?.provider || coderRole.provider,
|
|
@@ -69,6 +69,9 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
|
|
|
69
69
|
let triageOutput;
|
|
70
70
|
try {
|
|
71
71
|
triageOutput = await triage.run({ task: session.task, onOutput: triageStall.onOutput });
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger.warn(`Triage threw: ${err.message}`);
|
|
74
|
+
triageOutput = { ok: false, summary: `Triage error: ${err.message}`, result: { error: err.message } };
|
|
72
75
|
} finally {
|
|
73
76
|
triageStall.stop();
|
|
74
77
|
}
|
|
@@ -159,6 +162,9 @@ export async function runResearcherStage({ config, logger, emitter, eventBase, s
|
|
|
159
162
|
let researchOutput;
|
|
160
163
|
try {
|
|
161
164
|
researchOutput = await researcher.run({ task: session.task, onOutput: researcherStall.onOutput });
|
|
165
|
+
} catch (err) {
|
|
166
|
+
logger.warn(`Researcher threw: ${err.message}`);
|
|
167
|
+
researchOutput = { ok: false, summary: `Researcher error: ${err.message}`, result: { error: err.message } };
|
|
162
168
|
} finally {
|
|
163
169
|
researcherStall.stop();
|
|
164
170
|
}
|
|
@@ -281,6 +287,9 @@ export async function runArchitectStage({ config, logger, emitter, eventBase, se
|
|
|
281
287
|
discoverResult,
|
|
282
288
|
triageLevel
|
|
283
289
|
});
|
|
290
|
+
} catch (err) {
|
|
291
|
+
logger.warn(`Architect threw: ${err.message}`);
|
|
292
|
+
architectOutput = { ok: false, summary: `Architect error: ${err.message}`, result: { error: err.message } };
|
|
284
293
|
} finally {
|
|
285
294
|
architectStall.stop();
|
|
286
295
|
}
|
|
@@ -380,6 +389,9 @@ export async function runPlannerStage({ config, logger, emitter, eventBase, sess
|
|
|
380
389
|
let planResult;
|
|
381
390
|
try {
|
|
382
391
|
planResult = await planRole.execute({ task, onOutput: plannerStall.onOutput });
|
|
392
|
+
} catch (err) {
|
|
393
|
+
logger.warn(`Planner threw: ${err.message}`);
|
|
394
|
+
planResult = { ok: false, result: { error: err.message }, summary: `Planner error: ${err.message}` };
|
|
383
395
|
} finally {
|
|
384
396
|
plannerStall.stop();
|
|
385
397
|
}
|
|
@@ -453,6 +465,9 @@ export async function runDiscoverStage({ config, logger, emitter, eventBase, ses
|
|
|
453
465
|
let discoverOutput;
|
|
454
466
|
try {
|
|
455
467
|
discoverOutput = await discover.run({ task: session.task, mode, onOutput: discoverStall.onOutput });
|
|
468
|
+
} catch (err) {
|
|
469
|
+
logger.warn(`Discover threw: ${err.message}`);
|
|
470
|
+
discoverOutput = { ok: false, summary: `Discover error: ${err.message}`, result: { error: err.message } };
|
|
456
471
|
} finally {
|
|
457
472
|
discoverStall.stop();
|
|
458
473
|
}
|
|
@@ -16,7 +16,13 @@ export async function runReviewerWithFallback({ reviewerName, config, logger, em
|
|
|
16
16
|
const role = new ReviewerRole({ config: reviewerConfig, logger, emitter, createAgentFn: createAgent });
|
|
17
17
|
await role.init();
|
|
18
18
|
for (let attempt = 1; attempt <= retries + 1; attempt += 1) {
|
|
19
|
-
|
|
19
|
+
let execResult;
|
|
20
|
+
try {
|
|
21
|
+
execResult = await role.execute(reviewInput);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
logger.warn(`Reviewer ${name} attempt ${attempt} threw: ${err.message}`);
|
|
24
|
+
execResult = { ok: false, result: { error: err.message }, summary: `Reviewer error: ${err.message}` };
|
|
25
|
+
}
|
|
20
26
|
if (onAttemptResult) {
|
|
21
27
|
await onAttemptResult({ reviewer: name, result: execResult.result });
|
|
22
28
|
}
|
|
@@ -19,7 +19,16 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
|
|
|
19
19
|
|
|
20
20
|
const solomon = new SolomonRole({ config, logger, emitter });
|
|
21
21
|
await solomon.init({ task: conflict.task || session.task, iteration });
|
|
22
|
-
|
|
22
|
+
let ruling;
|
|
23
|
+
try {
|
|
24
|
+
ruling = await solomon.run({ conflict });
|
|
25
|
+
} catch (err) {
|
|
26
|
+
logger.warn(`Solomon threw: ${err.message}`);
|
|
27
|
+
return escalateToHuman({
|
|
28
|
+
askQuestion, session, emitter, eventBase, stage, iteration,
|
|
29
|
+
conflict: { ...conflict, solomonReason: `Solomon error: ${err.message}` }
|
|
30
|
+
});
|
|
31
|
+
}
|
|
23
32
|
|
|
24
33
|
emitProgress(
|
|
25
34
|
emitter,
|
package/src/orchestrator.js
CHANGED
|
@@ -958,14 +958,45 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
|
|
|
958
958
|
return { action: "return", result };
|
|
959
959
|
}
|
|
960
960
|
|
|
961
|
-
async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults }) {
|
|
961
|
+
async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task }) {
|
|
962
|
+
// Escalate to Solomon / human before giving up
|
|
963
|
+
const solomonResult = await invokeSolomon({
|
|
964
|
+
config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
|
|
965
|
+
iteration: config.max_iterations,
|
|
966
|
+
conflict: {
|
|
967
|
+
stage: "max_iterations",
|
|
968
|
+
task,
|
|
969
|
+
iterationCount: config.max_iterations,
|
|
970
|
+
maxIterations: config.max_iterations,
|
|
971
|
+
history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached without reviewer approval" }]
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
if (solomonResult.action === "continue") {
|
|
976
|
+
if (solomonResult.humanGuidance) {
|
|
977
|
+
session.last_reviewer_feedback = `User guidance: ${solomonResult.humanGuidance}`;
|
|
978
|
+
}
|
|
979
|
+
session.reviewer_retry_count = 0;
|
|
980
|
+
await saveSession(session);
|
|
981
|
+
return { approved: false, sessionId: session.id, reason: "max_iterations_extended", humanGuidance: solomonResult.humanGuidance };
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (solomonResult.action === "pause") {
|
|
985
|
+
return { paused: true, sessionId: session.id, question: solomonResult.question, context: "max_iterations" };
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (solomonResult.action === "subtask") {
|
|
989
|
+
return { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "max_iterations_subtask" };
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Solomon also couldn't resolve — fail
|
|
962
993
|
session.budget = budgetSummary();
|
|
963
994
|
await markSessionStatus(session, "failed");
|
|
964
995
|
emitProgress(
|
|
965
996
|
emitter,
|
|
966
997
|
makeEvent("session:end", { ...eventBase, stage: "done" }, {
|
|
967
998
|
status: "fail",
|
|
968
|
-
message: "Max iterations reached",
|
|
999
|
+
message: "Max iterations reached (Solomon could not resolve)",
|
|
969
1000
|
detail: { approved: false, reason: "max_iterations", iterations: config.max_iterations, stages: stageResults, budget: budgetSummary() }
|
|
970
1001
|
})
|
|
971
1002
|
);
|
|
@@ -978,7 +1009,7 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
978
1009
|
const refactorerRole = resolveRole(config, "refactorer");
|
|
979
1010
|
const pipelineFlags = resolvePipelineFlags(config);
|
|
980
1011
|
const repeatDetector = new RepeatDetector({ threshold: getRepeatThreshold(config) });
|
|
981
|
-
const coderRoleInstance = new CoderRole({ config, logger, emitter, createAgentFn: createAgent });
|
|
1012
|
+
const coderRoleInstance = new CoderRole({ config, logger, emitter, createAgentFn: createAgent, askHost: askQuestion });
|
|
982
1013
|
const startedAt = Date.now();
|
|
983
1014
|
const eventBase = { sessionId: null, iteration: 0, stage: null, startedAt };
|
|
984
1015
|
const { budgetTracker, budgetLimit, budgetSummary, trackBudget } = createBudgetManager({ config, emitter, eventBase });
|
|
@@ -1109,7 +1140,7 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1109
1140
|
if (iterResult.action === "retry") { i -= 1; }
|
|
1110
1141
|
}
|
|
1111
1142
|
|
|
1112
|
-
return handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config, stageResults: ctx.stageResults });
|
|
1143
|
+
return handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config, stageResults: ctx.stageResults, logger, askQuestion, task });
|
|
1113
1144
|
}
|
|
1114
1145
|
|
|
1115
1146
|
export async function resumeFlow({ sessionId, answer, config, logger, flags = {}, emitter = null, askQuestion = null }) {
|
|
@@ -1162,5 +1193,10 @@ export async function resumeFlow({ sessionId, answer, config, logger, flags = {}
|
|
|
1162
1193
|
await saveSession(session);
|
|
1163
1194
|
|
|
1164
1195
|
// Re-run the flow with the existing session context
|
|
1165
|
-
|
|
1196
|
+
try {
|
|
1197
|
+
return await runFlow({ task, config: sessionConfig, logger, flags, emitter, askQuestion });
|
|
1198
|
+
} catch (err) {
|
|
1199
|
+
await markSessionStatus(session, "failed");
|
|
1200
|
+
throw err;
|
|
1201
|
+
}
|
|
1166
1202
|
}
|
package/src/roles/coder-role.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { BaseRole } from "./base-role.js";
|
|
2
2
|
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
3
|
import { buildCoderPrompt } from "../prompts/coder.js";
|
|
4
|
+
import { isHostAgent } from "../utils/agent-detect.js";
|
|
5
|
+
import { HostAgent } from "../agents/host-agent.js";
|
|
4
6
|
|
|
5
7
|
function resolveProvider(config) {
|
|
6
8
|
return (
|
|
@@ -11,9 +13,10 @@ function resolveProvider(config) {
|
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export class CoderRole extends BaseRole {
|
|
14
|
-
constructor({ config, logger, emitter = null, createAgentFn = null }) {
|
|
16
|
+
constructor({ config, logger, emitter = null, createAgentFn = null, askHost = null }) {
|
|
15
17
|
super({ name: "coder", config, logger, emitter });
|
|
16
18
|
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
19
|
+
this._askHost = askHost;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
async execute(input) {
|
|
@@ -22,7 +25,14 @@ export class CoderRole extends BaseRole {
|
|
|
22
25
|
: input || {};
|
|
23
26
|
|
|
24
27
|
const provider = resolveProvider(this.config);
|
|
25
|
-
const
|
|
28
|
+
const useHost = this._askHost && isHostAgent(provider);
|
|
29
|
+
const agent = useHost
|
|
30
|
+
? new HostAgent(this.config, this.logger, { askHost: this._askHost })
|
|
31
|
+
: this._createAgent(provider, this.config, this.logger);
|
|
32
|
+
|
|
33
|
+
if (useHost) {
|
|
34
|
+
this.logger.info(`Host-as-coder: delegating to host AI (skipping ${provider} subprocess)`);
|
|
35
|
+
}
|
|
26
36
|
|
|
27
37
|
const prompt = buildCoderPrompt({
|
|
28
38
|
task: task || this.context?.task || "",
|
package/src/session-store.js
CHANGED
|
@@ -83,8 +83,9 @@ export async function loadMostRecentSession() {
|
|
|
83
83
|
|
|
84
84
|
export async function resumeSessionWithAnswer(sessionId, answer) {
|
|
85
85
|
const session = await loadSession(sessionId);
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
const resumable = new Set(["paused", "running", "failed", "stopped"]);
|
|
87
|
+
if (!resumable.has(session.status)) {
|
|
88
|
+
throw new Error(`Session ${sessionId} cannot be resumed (status: ${session.status})`);
|
|
88
89
|
}
|
|
89
90
|
const pausedState = session.paused_state;
|
|
90
91
|
if (!pausedState) {
|
|
@@ -30,4 +30,25 @@ export async function detectAvailableAgents() {
|
|
|
30
30
|
return results;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Detect which AI agent is the current MCP host (if any).
|
|
35
|
+
* Returns the agent name ("claude", "codex", etc.) or null if not inside an agent.
|
|
36
|
+
*/
|
|
37
|
+
export function detectHostAgent() {
|
|
38
|
+
if (process.env.CLAUDECODE === "1" || process.env.CLAUDE_CODE === "1") return "claude";
|
|
39
|
+
if (process.env.CODEX_CLI === "1" || process.env.CODEX === "1") return "codex";
|
|
40
|
+
if (process.env.GEMINI_CLI === "1") return "gemini";
|
|
41
|
+
if (process.env.OPENCODE === "1") return "opencode";
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if a given provider matches the current host agent.
|
|
47
|
+
* When true, we can skip subprocess spawning and delegate to the host.
|
|
48
|
+
*/
|
|
49
|
+
export function isHostAgent(provider) {
|
|
50
|
+
const host = detectHostAgent();
|
|
51
|
+
return host !== null && host === provider;
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
export { KNOWN_AGENTS };
|