muonroi-cli 1.8.0 → 1.8.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/LICENSE +21 -21
- package/README.md +122 -122
- package/dist/src/agent-harness/mock-model.js +112 -4
- package/dist/src/cli/cost-forensics.js +17 -14
- package/dist/src/council/prompts.js +20 -20
- package/dist/src/flow/compaction/index.d.ts +1 -0
- package/dist/src/flow/compaction/index.js +4 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/index.js +4 -2
- package/dist/src/lsp/manager.js +11 -3
- package/dist/src/lsp/manager.test.js +39 -0
- package/dist/src/mcp/auto-setup.js +0 -8
- package/dist/src/mcp/oauth-callback.js +2 -2
- package/dist/src/mcp/parse-headers.test.js +14 -14
- package/dist/src/mcp/self-verify-jobs.d.ts +1 -0
- package/dist/src/mcp/self-verify-runner.js +1 -1
- package/dist/src/mcp/setup-guide-text.js +74 -74
- package/dist/src/mcp/smoke.test.js +43 -43
- package/dist/src/ops/doctor.js +7 -7
- package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +1 -0
- package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
- package/dist/src/orchestrator/__tests__/prompts.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/prompts.test.js +88 -0
- package/dist/src/orchestrator/__tests__/sub-session-delegation.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/sub-session-delegation.test.js +507 -0
- package/dist/src/orchestrator/auto-commit.js +9 -7
- package/dist/src/orchestrator/message-processor.d.ts +2 -0
- package/dist/src/orchestrator/message-processor.js +7 -0
- package/dist/src/orchestrator/orchestrator.d.ts +3 -0
- package/dist/src/orchestrator/orchestrator.js +362 -28
- package/dist/src/orchestrator/pending-calls.js +2 -1
- package/dist/src/orchestrator/preprocessor.js +3 -2
- package/dist/src/orchestrator/prompts.d.ts +8 -0
- package/dist/src/orchestrator/prompts.js +65 -18
- package/dist/src/orchestrator/sub-agent-cap.d.ts +2 -0
- package/dist/src/orchestrator/sub-agent-cap.js +12 -4
- package/dist/src/orchestrator/subagent-compactor.d.ts +7 -0
- package/dist/src/orchestrator/subagent-compactor.js +65 -14
- package/dist/src/orchestrator/subagent-compactor.spec.js +125 -9
- package/dist/src/orchestrator/tool-engine.d.ts +2 -0
- package/dist/src/orchestrator/tool-engine.js +80 -21
- package/dist/src/orchestrator/turn-runner-deps.d.ts +2 -0
- package/dist/src/pil/__tests__/llm-classify.test.js +79 -1
- package/dist/src/pil/layer1-intent.js +37 -37
- package/dist/src/pil/layer2_5-ponytail.js +8 -8
- package/dist/src/pil/llm-classify.d.ts +11 -0
- package/dist/src/pil/llm-classify.js +193 -0
- package/dist/src/product-loop/done-gate.js +3 -3
- package/dist/src/product-loop/loop-driver.js +18 -18
- package/dist/src/product-loop/progress-snapshot.js +4 -4
- package/dist/src/providers/mcp-vision-bridge.js +48 -48
- package/dist/src/reporter/index.js +1 -1
- package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
- package/dist/src/scaffold/bb-quality-gate.js +5 -5
- package/dist/src/scaffold/continuation-prompt.js +60 -60
- package/dist/src/scaffold/init-new.js +453 -453
- package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
- package/dist/src/self-qa/agentic-loop.js +11 -2
- package/dist/src/storage/__tests__/hierarchical-session.test.d.ts +1 -0
- package/dist/src/storage/__tests__/hierarchical-session.test.js +161 -0
- package/dist/src/storage/__tests__/migrations.test.js +2 -2
- package/dist/src/storage/__tests__/sweep-stale-pending.test.js +2 -2
- package/dist/src/storage/index.d.ts +1 -1
- package/dist/src/storage/index.js +1 -1
- package/dist/src/storage/interaction-log.js +5 -5
- package/dist/src/storage/migrations.js +135 -123
- package/dist/src/storage/session-experience-store.js +4 -4
- package/dist/src/storage/sessions.js +43 -43
- package/dist/src/storage/transcript-response-entry.test.js +18 -5
- package/dist/src/storage/transcript.d.ts +2 -0
- package/dist/src/storage/transcript.js +223 -97
- package/dist/src/storage/usage.js +34 -17
- package/dist/src/storage/workspaces.js +12 -12
- package/dist/src/tools/native-tools.js +10 -1
- package/dist/src/tools/registry.d.ts +1 -0
- package/dist/src/tools/registry.js +30 -2
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/slash/council-inspect.js +4 -4
- package/dist/src/ui/slash/expand.js +14 -1
- package/dist/src/ui/use-app-logic.js +0 -0
- package/dist/src/utils/__tests__/compaction-caps.test.js +9 -9
- package/dist/src/utils/__tests__/llm-deadline.test.js +16 -4
- package/dist/src/utils/__tests__/logger.test.d.ts +1 -0
- package/dist/src/utils/__tests__/logger.test.js +115 -0
- package/dist/src/utils/__tests__/settings-agent-first.test.d.ts +1 -0
- package/dist/src/utils/__tests__/settings-agent-first.test.js +48 -0
- package/dist/src/utils/clipboard-image.js +23 -23
- package/dist/src/utils/install-manager.d.ts +1 -0
- package/dist/src/utils/install-manager.js +76 -7
- package/dist/src/utils/install-manager.test.js +43 -1
- package/dist/src/utils/logger.d.ts +28 -0
- package/dist/src/utils/logger.js +171 -0
- package/dist/src/utils/settings.d.ts +9 -1
- package/dist/src/utils/settings.js +59 -22
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/utils/skills.js +3 -3
- package/dist/src/utils/update-checker.test.js +5 -1
- package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
- package/package.json +1 -1
|
@@ -23,7 +23,8 @@ import { statusBarStore } from "../ui/status-bar/store.js";
|
|
|
23
23
|
import { appendCostLog } from "../usage/cost-log.js";
|
|
24
24
|
import { appendDecisionLog } from "../usage/decision-log.js";
|
|
25
25
|
import { projectCostUSD, sanitizeInputTokens } from "../usage/estimator.js";
|
|
26
|
-
import {
|
|
26
|
+
import { logger } from "../utils/logger.js";
|
|
27
|
+
import { getAutoCompactThresholdPct, getCouncilRounds, getCurrentModel, getCurrentShellSettings, getModeSpecificModel, getRoleModel, getRoleModels, isAutoCompactAfterTurnEnabled, isCouncilMultiProviderPreferred, isProviderDisabled, loadUserSettings, } from "../utils/settings.js";
|
|
27
28
|
import { runSideQuestion } from "../utils/side-question.js";
|
|
28
29
|
import { buildVerifyDetectPrompt, normalizeVerifyRecipe } from "../verify/entrypoint.js";
|
|
29
30
|
import { runVerifyOrchestration } from "../verify/orchestrator.js";
|
|
@@ -38,7 +39,7 @@ import { DelegationManager } from "./delegations.js";
|
|
|
38
39
|
import { loadFlowResumeDigest } from "./flow-resume.js";
|
|
39
40
|
import { MessageProcessor } from "./message-processor.js";
|
|
40
41
|
import { lastPersistedSeq } from "./message-seq.js";
|
|
41
|
-
import { buildSystemPrompt, MAX_TOOL_ROUNDS } from "./prompts.js";
|
|
42
|
+
import { buildSystemPrompt, HARD_MAX_TOOL_ROUNDS, MAX_TOOL_ROUNDS } from "./prompts.js";
|
|
42
43
|
import { getReadPathBudgetCap, ReadPathBudget } from "./read-path-budget.js";
|
|
43
44
|
import { withStreamRetry } from "./retry-stream.js";
|
|
44
45
|
import { StreamRunner } from "./stream-runner.js";
|
|
@@ -136,6 +137,7 @@ export class Agent {
|
|
|
136
137
|
/** UI-registered live-queue steer drain; see Agent.setSteerDrain. */
|
|
137
138
|
steerDrain = null;
|
|
138
139
|
maxToolRounds;
|
|
140
|
+
hardMaxToolRounds;
|
|
139
141
|
mode = "agent";
|
|
140
142
|
modelId;
|
|
141
143
|
maxTokens;
|
|
@@ -240,7 +242,10 @@ export class Agent {
|
|
|
240
242
|
this.setApiKey(apiKey, baseURL);
|
|
241
243
|
}
|
|
242
244
|
this.schedules = new ScheduleManager(() => this.bash.getCwd(), () => this.modelId);
|
|
243
|
-
|
|
245
|
+
const settings = loadUserSettings();
|
|
246
|
+
this.maxToolRounds = maxToolRounds || settings.maxToolRounds || MAX_TOOL_ROUNDS;
|
|
247
|
+
const baseHardMax = settings.hardMaxToolRounds || HARD_MAX_TOOL_ROUNDS;
|
|
248
|
+
this.hardMaxToolRounds = Math.max(Math.floor(this.maxToolRounds * 1.5), baseHardMax);
|
|
244
249
|
const envMax = Number(process.env.MUONROI_MAX_TOKENS);
|
|
245
250
|
this.maxTokens = Number.isFinite(envMax) && envMax > 0 ? envMax : 16_384;
|
|
246
251
|
this.batchApi = options.batchApi ?? false;
|
|
@@ -267,7 +272,7 @@ export class Agent {
|
|
|
267
272
|
warmMcpClients(filterMcpServersByMessage(loadMcpServers(), ""));
|
|
268
273
|
}
|
|
269
274
|
catch (err) {
|
|
270
|
-
|
|
275
|
+
logger.error("orchestrator", "MCP pre-warm skipped", { error: err });
|
|
271
276
|
}
|
|
272
277
|
})();
|
|
273
278
|
if (options.persistSession !== false) {
|
|
@@ -424,6 +429,10 @@ export class Agent {
|
|
|
424
429
|
getMessages() {
|
|
425
430
|
return this.messages;
|
|
426
431
|
}
|
|
432
|
+
setMessages(messages) {
|
|
433
|
+
this.messages = messages;
|
|
434
|
+
this.messageSeqs = messages.map(() => null);
|
|
435
|
+
}
|
|
427
436
|
async listSchedules() {
|
|
428
437
|
return this.schedules.list();
|
|
429
438
|
}
|
|
@@ -1212,6 +1221,20 @@ export class Agent {
|
|
|
1212
1221
|
if (this._compactionStats.count >= 2) {
|
|
1213
1222
|
keepRecentTokens = Math.floor(keepRecentTokens * 0.85);
|
|
1214
1223
|
}
|
|
1224
|
+
// For sub-sessions, reduce keepRecentTokens to 8000 to keep active history light
|
|
1225
|
+
try {
|
|
1226
|
+
if (this.session) {
|
|
1227
|
+
const { getDatabase } = require("../storage/db.js");
|
|
1228
|
+
const db = getDatabase();
|
|
1229
|
+
const row = db.prepare("SELECT parent_session_id FROM sessions WHERE id = ?").get(this.session.id);
|
|
1230
|
+
if (row?.parent_session_id) {
|
|
1231
|
+
keepRecentTokens = 8000;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
catch {
|
|
1236
|
+
// Ignored
|
|
1237
|
+
}
|
|
1215
1238
|
return {
|
|
1216
1239
|
reserveTokens: Math.max(this.maxTokens, DEFAULT_RESERVE_TOKENS),
|
|
1217
1240
|
keepRecentTokens,
|
|
@@ -1276,7 +1299,23 @@ export class Agent {
|
|
|
1276
1299
|
const keptSeqs = this.messageSeqs.slice(preparation.firstKeptIndex);
|
|
1277
1300
|
const firstKeptSeq = keptSeqs.find((seq) => seq !== null) ?? getNextMessageSequence(this.session.id);
|
|
1278
1301
|
const compactStartedAt = Date.now();
|
|
1279
|
-
const
|
|
1302
|
+
const isSubSession = await (async () => {
|
|
1303
|
+
try {
|
|
1304
|
+
if (!this.session)
|
|
1305
|
+
return false;
|
|
1306
|
+
const { getDatabase } = await import("../storage/db.js");
|
|
1307
|
+
const db = getDatabase();
|
|
1308
|
+
const row = db.prepare("SELECT parent_session_id FROM sessions WHERE id = ?").get(this.session.id);
|
|
1309
|
+
return !!row?.parent_session_id;
|
|
1310
|
+
}
|
|
1311
|
+
catch {
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
})();
|
|
1315
|
+
const customInstructions = isSubSession
|
|
1316
|
+
? "This is a temporary sub-session. Under sub-sessions, it is CRITICAL to preserve active files being worked on, compiler/linter error states, and exact line coordinates in the summary. Do not omit details of files edited, tests run, or compiler diagnostics, as the model needs this specific context to continue working without re-reading the files."
|
|
1317
|
+
: undefined;
|
|
1318
|
+
const { summary, usage: compactUsage } = await generateCompactionSummary(provider, compactModelId, preparation, customInstructions, signal);
|
|
1280
1319
|
// Record compaction call in cost-log — bypasses recordUsage because
|
|
1281
1320
|
// compaction returns usage separately and isn't routed through the
|
|
1282
1321
|
// status-bar / usage event pipeline (intentional: it's overhead, not user spend).
|
|
@@ -1414,7 +1453,7 @@ export class Agent {
|
|
|
1414
1453
|
});
|
|
1415
1454
|
}
|
|
1416
1455
|
log(true, `over-threshold (${tokens} >= ${minMeaningfulTokens})`, { tokens, thresholdPct, minMeaningfulTokens });
|
|
1417
|
-
await this.compactForContext(provider, system, contextWindow, signal, this.getCompactionSettings(contextWindow), true).catch((err) =>
|
|
1456
|
+
await this.compactForContext(provider, system, contextWindow, signal, this.getCompactionSettings(contextWindow), true).catch((err) => logger.warn("orchestrator", "compaction failed", { error: err }));
|
|
1418
1457
|
}
|
|
1419
1458
|
// ========================================================================
|
|
1420
1459
|
// Council system — delegated to CouncilManager (Phase 12.1-02)
|
|
@@ -1473,7 +1512,7 @@ export class Agent {
|
|
|
1473
1512
|
runDir = nodePath.join(flowDir, "runs", runId);
|
|
1474
1513
|
}
|
|
1475
1514
|
catch (err) {
|
|
1476
|
-
|
|
1515
|
+
logger.error("router", "runDir resolution failed (decisions.lock will be skipped)", { error: err });
|
|
1477
1516
|
}
|
|
1478
1517
|
try {
|
|
1479
1518
|
const gen = runCouncil(topic, this.modelId, this.messages, this.session?.id, llm, this.councilManager.createQuestionResponder(), this.councilManager.createPreflightResponder(), processMessageFn, {
|
|
@@ -2005,6 +2044,9 @@ export class Agent {
|
|
|
2005
2044
|
get maxToolRounds() {
|
|
2006
2045
|
return self.maxToolRounds;
|
|
2007
2046
|
},
|
|
2047
|
+
get hardMaxToolRounds() {
|
|
2048
|
+
return self.hardMaxToolRounds;
|
|
2049
|
+
},
|
|
2008
2050
|
get maxTokens() {
|
|
2009
2051
|
return self.maxTokens;
|
|
2010
2052
|
},
|
|
@@ -2097,32 +2139,255 @@ export class Agent {
|
|
|
2097
2139
|
fireHook(input, signal) {
|
|
2098
2140
|
return executeEventHooks(input, this.bash.getCwd(), signal);
|
|
2099
2141
|
}
|
|
2142
|
+
estimateConversationChars() {
|
|
2143
|
+
let count = 0;
|
|
2144
|
+
for (const m of this.messages) {
|
|
2145
|
+
if (typeof m.content === "string") {
|
|
2146
|
+
count += m.content.length;
|
|
2147
|
+
}
|
|
2148
|
+
else if (Array.isArray(m.content)) {
|
|
2149
|
+
for (const p of m.content) {
|
|
2150
|
+
if (p.type === "text" && p.text) {
|
|
2151
|
+
count += p.text.length;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
return count;
|
|
2157
|
+
}
|
|
2100
2158
|
// ========================================================================
|
|
2101
2159
|
// processMessage — main streaming turn loop (PIL enrichment, routing, LLM
|
|
2102
2160
|
// stream, tool execution, compaction, hooks, observer notifications)
|
|
2103
2161
|
// ========================================================================
|
|
2104
2162
|
async *processMessage(userMessage, observer, images) {
|
|
2105
|
-
const
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
//
|
|
2109
|
-
|
|
2163
|
+
const threshold = Number(process.env.MUONROI_SILENT_ROTATION_THRESHOLD) || 80000;
|
|
2164
|
+
const currentChars = this.estimateConversationChars();
|
|
2165
|
+
logger.debug("orchestrator", "Checking silent session rotation threshold", { currentChars, threshold });
|
|
2166
|
+
// 1. Run classifier to decide execution route
|
|
2167
|
+
let routeAction = "DIRECT_ANSWER";
|
|
2168
|
+
try {
|
|
2169
|
+
const { classifySubSessionAction } = await import("../pil/llm-classify.js");
|
|
2170
|
+
const routeResult = await classifySubSessionAction(this.requireProvider(), this.modelId, userMessage);
|
|
2171
|
+
if (routeResult) {
|
|
2172
|
+
routeAction = routeResult.action;
|
|
2173
|
+
logger.info("orchestrator", "Routing action selected for user message", {
|
|
2174
|
+
action: routeAction,
|
|
2175
|
+
confidence: routeResult.confidence,
|
|
2176
|
+
reason: routeResult.reason,
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
catch (err) {
|
|
2181
|
+
logger.error("orchestrator", "Routing classification failed, falling back to DIRECT_ANSWER", { error: err });
|
|
2182
|
+
}
|
|
2183
|
+
const shouldRotate = currentChars > threshold || routeAction === "ROTATE_SESSION";
|
|
2184
|
+
if (shouldRotate && this.session && this.sessionStore) {
|
|
2185
|
+
logger.info("orchestrator", "Triggering silent session rotation", {
|
|
2186
|
+
parentSessionId: this.session.id,
|
|
2187
|
+
currentChars,
|
|
2188
|
+
});
|
|
2189
|
+
yield { type: "content", content: "\n⋯ Xoay vòng session ngầm để tối ưu hóa context (Anti-Mù active)...\n" };
|
|
2190
|
+
const parentSessionId = this.session.id;
|
|
2191
|
+
try {
|
|
2192
|
+
const path = await import("node:path");
|
|
2193
|
+
const flowDir = path.join(this.bash.getCwd(), ".muonroi-flow");
|
|
2194
|
+
const { deliberateCompact } = await import("../flow/compaction/index.js");
|
|
2195
|
+
const { getDatabase } = await import("../storage/db.js");
|
|
2196
|
+
const { appendCompaction, getNextMessageSequence } = await import("../storage/transcript.js");
|
|
2197
|
+
const cr = await deliberateCompact(flowDir, this.messages, "", 4096, this.requireProvider(), this.modelId);
|
|
2198
|
+
const newSession = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
|
|
2199
|
+
const db = getDatabase();
|
|
2200
|
+
db.prepare("UPDATE sessions SET parent_session_id = ? WHERE id = ?").run(parentSessionId, newSession.id);
|
|
2201
|
+
const summaryMessage = createCompactionSummaryMessage(cr.summary);
|
|
2202
|
+
const nextSeq = getNextMessageSequence(newSession.id);
|
|
2203
|
+
appendCompaction(newSession.id, nextSeq, cr.summary, cr.tokensBeforeCompress);
|
|
2204
|
+
this.session = this.sessionStore.getRequiredSession(newSession.id);
|
|
2205
|
+
this.messages = [summaryMessage];
|
|
2206
|
+
this.messageSeqs = [null];
|
|
2207
|
+
this.sessionStore.touchSession(newSession.id, this.bash.getCwd());
|
|
2208
|
+
logger.info("orchestrator", "Silent session rotation completed successfully", {
|
|
2209
|
+
parentSessionId,
|
|
2210
|
+
newSessionId: newSession.id,
|
|
2211
|
+
summaryLength: cr.summary.length,
|
|
2212
|
+
tokensBeforeCompress: cr.tokensBeforeCompress,
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
catch (err) {
|
|
2216
|
+
logger.error("orchestrator", "silent session rotation failed", { error: err });
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
let isSubSessionForked = false;
|
|
2220
|
+
let parentSessionId = null;
|
|
2221
|
+
let subSessionId = null;
|
|
2222
|
+
if (routeAction === "SPAWN_SUB_SESSION" && this.session && this.sessionStore) {
|
|
2223
|
+
yield { type: "content", content: "\n⋯ Đang khởi tạo sub-session ngầm để xử lý tác vụ...\n" };
|
|
2224
|
+
parentSessionId = this.session.id;
|
|
2225
|
+
try {
|
|
2226
|
+
const { loadLatestCompaction, getNextMessageSequence, appendCompaction, loadTranscriptState } = await import("../storage/transcript.js");
|
|
2227
|
+
const { getDatabase } = await import("../storage/db.js");
|
|
2228
|
+
const db = getDatabase();
|
|
2229
|
+
// Check if there is already an active child session for this parent session
|
|
2230
|
+
const activeSubSession = db
|
|
2231
|
+
.prepare("SELECT id, updated_at FROM sessions WHERE parent_session_id = ? AND status = 'active' ORDER BY updated_at DESC LIMIT 1")
|
|
2232
|
+
.get(parentSessionId);
|
|
2233
|
+
let shouldResume = false;
|
|
2234
|
+
if (activeSubSession) {
|
|
2235
|
+
const updatedAt = new Date(activeSubSession.updated_at).getTime();
|
|
2236
|
+
const now = Date.now();
|
|
2237
|
+
const diffMins = (now - updatedAt) / (60 * 1000);
|
|
2238
|
+
if (diffMins > 15) {
|
|
2239
|
+
// Stale: mark as abandoned
|
|
2240
|
+
db.prepare("UPDATE sessions SET status = 'abandoned', updated_at = ? WHERE id = ?").run(new Date().toISOString(), activeSubSession.id);
|
|
2241
|
+
logger.info("orchestrator", "Stale sub-session found, marked as abandoned", {
|
|
2242
|
+
parentSessionId,
|
|
2243
|
+
subSessionId: activeSubSession.id,
|
|
2244
|
+
diffMins,
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
else {
|
|
2248
|
+
shouldResume = true;
|
|
2249
|
+
subSessionId = activeSubSession.id;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
if (shouldResume && subSessionId) {
|
|
2253
|
+
yield { type: "content", content: "\n⋯ Phát hiện sub-session trước đó bị gián đoạn, đang khôi phục...\n" };
|
|
2254
|
+
this.session = this.sessionStore.getRequiredSession(subSessionId);
|
|
2255
|
+
const childState = loadTranscriptState(subSessionId);
|
|
2256
|
+
this.messages = childState.messages;
|
|
2257
|
+
this.messageSeqs = childState.seqs;
|
|
2258
|
+
this.sessionStore.touchSession(subSessionId, this.bash.getCwd());
|
|
2259
|
+
isSubSessionForked = true;
|
|
2260
|
+
logger.info("orchestrator", "Resumed child sub-session successfully", {
|
|
2261
|
+
parentSessionId,
|
|
2262
|
+
subSessionId,
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
else {
|
|
2266
|
+
const latest = loadLatestCompaction(parentSessionId);
|
|
2267
|
+
const newSession = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
|
|
2268
|
+
db.prepare("UPDATE sessions SET parent_session_id = ? WHERE id = ?").run(parentSessionId, newSession.id);
|
|
2269
|
+
subSessionId = newSession.id;
|
|
2270
|
+
let seedMessages = [];
|
|
2271
|
+
let seedSeqs = [];
|
|
2272
|
+
if (latest?.summary) {
|
|
2273
|
+
const summaryMsg = createCompactionSummaryMessage(latest.summary);
|
|
2274
|
+
seedMessages = [summaryMsg];
|
|
2275
|
+
seedSeqs = [null];
|
|
2276
|
+
const nextSeq = getNextMessageSequence(subSessionId);
|
|
2277
|
+
appendCompaction(subSessionId, nextSeq, latest.summary, latest.tokensBefore);
|
|
2278
|
+
}
|
|
2279
|
+
else {
|
|
2280
|
+
seedMessages = [...this.messages];
|
|
2281
|
+
seedSeqs = [...this.messageSeqs];
|
|
2282
|
+
}
|
|
2283
|
+
// Add sub-session overlay system message
|
|
2284
|
+
const overlayMessage = {
|
|
2285
|
+
role: "system",
|
|
2286
|
+
content: `You are executing a sub-task delegated by the Main Session in an isolated, temporary Sub-Session.\n` +
|
|
2287
|
+
`Your goal is to satisfy the user's request: "${userMessage}"\n\n` +
|
|
2288
|
+
`Your Operating Boundaries & Rules:\n` +
|
|
2289
|
+
`1. You have full access to tools (bash, edit_file, read_file, grep, etc.). Execute them as needed to build, debug, and verify the work.\n` +
|
|
2290
|
+
`2. Stay strictly focused on completing the request. Do not engage in social chat or pleasantries.\n` +
|
|
2291
|
+
`3. Once the goal is achieved and verified, you MUST return your final response using the 'respond_general' (or final answer) tool.\n` +
|
|
2292
|
+
`4. Your final response MUST contain a structured summary (Key Changes, Verification Details, Result Summary).\n` +
|
|
2293
|
+
`5. All intermediate tool calls, raw tool outputs, and diagnostic traces will remain isolated inside this sub-session and will not bloat the parent session.`,
|
|
2294
|
+
};
|
|
2295
|
+
seedMessages.push(overlayMessage);
|
|
2296
|
+
seedSeqs.push(null);
|
|
2297
|
+
this.session = this.sessionStore.getRequiredSession(subSessionId);
|
|
2298
|
+
this.messages = seedMessages;
|
|
2299
|
+
this.messageSeqs = seedSeqs;
|
|
2300
|
+
this.sessionStore.touchSession(subSessionId, this.bash.getCwd());
|
|
2301
|
+
isSubSessionForked = true;
|
|
2302
|
+
logger.info("orchestrator", "Forked child sub-session successfully", {
|
|
2303
|
+
parentSessionId,
|
|
2304
|
+
subSessionId,
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
catch (err) {
|
|
2309
|
+
logger.error("orchestrator", "Forking child sub-session failed, falling back to main session", { error: err });
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
let processor = new MessageProcessor(this._buildMessageProcessorDeps());
|
|
2110
2313
|
const autoCommitOn = isAutoCommitEnabled();
|
|
2111
2314
|
const cwd = this.bash.getCwd();
|
|
2112
2315
|
const dirtyBefore = autoCommitOn ? await snapshotDirtyPaths(cwd) : new Set();
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
const
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2316
|
+
try {
|
|
2317
|
+
let attempts = 0;
|
|
2318
|
+
const maxAttempts = 3;
|
|
2319
|
+
const messagesSnapshot = [...this.messages];
|
|
2320
|
+
const seqsSnapshot = [...this.messageSeqs];
|
|
2321
|
+
while (attempts < maxAttempts) {
|
|
2322
|
+
try {
|
|
2323
|
+
attempts++;
|
|
2324
|
+
yield* processor.run(userMessage, observer, images);
|
|
2325
|
+
break;
|
|
2326
|
+
}
|
|
2327
|
+
catch (err) {
|
|
2328
|
+
if (isSubSessionForked && isTransientError(err) && attempts < maxAttempts && subSessionId) {
|
|
2329
|
+
logger.warn("orchestrator", "Transient error in sub-session, retrying", {
|
|
2330
|
+
attempt: attempts,
|
|
2331
|
+
error: err,
|
|
2332
|
+
});
|
|
2333
|
+
yield {
|
|
2334
|
+
type: "content",
|
|
2335
|
+
content: `\n⚠️ Có lỗi mạng hoặc rate limit xảy ra trong sub-session (lần thử ${attempts}/${maxAttempts}). Đang thử lại...\n`,
|
|
2336
|
+
};
|
|
2337
|
+
this.messages = [...messagesSnapshot];
|
|
2338
|
+
this.messageSeqs = [...seqsSnapshot];
|
|
2339
|
+
processor = new MessageProcessor(this._buildMessageProcessorDeps());
|
|
2340
|
+
}
|
|
2341
|
+
else {
|
|
2342
|
+
throw err;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
if (autoCommitOn) {
|
|
2347
|
+
const auto = await maybeAutoCommitTurn({ cwd, dirtyBefore, userMessage }).catch((err) => {
|
|
2348
|
+
logger.error("orchestrator", "auto-commit unexpected failure", { error: err });
|
|
2349
|
+
return { committed: false };
|
|
2350
|
+
});
|
|
2351
|
+
if (auto.committed) {
|
|
2352
|
+
yield {
|
|
2353
|
+
type: "content",
|
|
2354
|
+
content: `\n✓ Auto-committed ${auto.fileCount} file(s) → ${auto.sha} (${AUTO_COMMIT_ATTRIBUTION})\n`,
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
finally {
|
|
2360
|
+
if (isSubSessionForked && parentSessionId && this.sessionStore) {
|
|
2361
|
+
try {
|
|
2362
|
+
const finalMessages = salvageSubSessionOutput(this.messages);
|
|
2363
|
+
// Restore parent session
|
|
2364
|
+
this.session = this.sessionStore.getRequiredSession(parentSessionId);
|
|
2365
|
+
const { loadTranscriptState } = await import("../storage/transcript.js");
|
|
2366
|
+
const parentState = loadTranscriptState(parentSessionId);
|
|
2367
|
+
this.messages = parentState.messages;
|
|
2368
|
+
this.messageSeqs = parentState.seqs;
|
|
2369
|
+
const userModelMessage = {
|
|
2370
|
+
role: "user",
|
|
2371
|
+
content: userMessage,
|
|
2372
|
+
};
|
|
2373
|
+
if (finalMessages.length > 0) {
|
|
2374
|
+
this.appendCompletedTurn(userModelMessage, finalMessages);
|
|
2375
|
+
logger.info("orchestrator", "Absorbed sub-session outcome into parent session", {
|
|
2376
|
+
parentSessionId,
|
|
2377
|
+
subSessionId,
|
|
2378
|
+
absorbedMessagesCount: finalMessages.length,
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
else {
|
|
2382
|
+
logger.warn("orchestrator", "No assistant messages found to absorb from sub-session", {
|
|
2383
|
+
parentSessionId,
|
|
2384
|
+
subSessionId,
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
catch (err) {
|
|
2389
|
+
logger.error("orchestrator", "Failed to absorb sub-session final summary", { error: err });
|
|
2390
|
+
}
|
|
2126
2391
|
}
|
|
2127
2392
|
}
|
|
2128
2393
|
}
|
|
@@ -2163,6 +2428,9 @@ export class Agent {
|
|
|
2163
2428
|
get maxToolRounds() {
|
|
2164
2429
|
return self.maxToolRounds;
|
|
2165
2430
|
},
|
|
2431
|
+
get hardMaxToolRounds() {
|
|
2432
|
+
return self.hardMaxToolRounds;
|
|
2433
|
+
},
|
|
2166
2434
|
get batchApi() {
|
|
2167
2435
|
return self.batchApi;
|
|
2168
2436
|
},
|
|
@@ -2257,6 +2525,39 @@ export class Agent {
|
|
|
2257
2525
|
},
|
|
2258
2526
|
requireProvider: () => self.requireProvider(),
|
|
2259
2527
|
emitSubagentStatus: (s) => self.emitSubagentStatus(s),
|
|
2528
|
+
consultParentSession: async (question) => {
|
|
2529
|
+
if (!self.session) {
|
|
2530
|
+
return "No active session found to consult.";
|
|
2531
|
+
}
|
|
2532
|
+
const { getDatabase } = await import("../storage/db.js");
|
|
2533
|
+
const db = getDatabase();
|
|
2534
|
+
const row = db.prepare("SELECT parent_session_id FROM sessions WHERE id = ?").get(self.session.id);
|
|
2535
|
+
if (!row?.parent_session_id) {
|
|
2536
|
+
return "No parent session found to consult.";
|
|
2537
|
+
}
|
|
2538
|
+
const parentSessionId = row.parent_session_id;
|
|
2539
|
+
const { loadTranscriptState } = await import("../storage/transcript.js");
|
|
2540
|
+
const parentState = loadTranscriptState(parentSessionId);
|
|
2541
|
+
const { serializeConversation } = await import("./compaction.js");
|
|
2542
|
+
const serializedParent = serializeConversation(parentState.messages);
|
|
2543
|
+
const systemPrompt = `You are the Main Session of an AI coding assistant.\n` +
|
|
2544
|
+
`A child sub-session has been spawned to execute a sub-task and is requesting your advice/guidance.\n` +
|
|
2545
|
+
`Below is your conversation history (Main Session Context):\n\n` +
|
|
2546
|
+
`${serializedParent}\n\n` +
|
|
2547
|
+
`Provide clear, actionable guidance to resolve the child's query.`;
|
|
2548
|
+
const { generateText } = await import("ai");
|
|
2549
|
+
const provider = self.requireProvider();
|
|
2550
|
+
const modelId = self.modelId;
|
|
2551
|
+
const runtime = resolveModelRuntime(provider, modelId);
|
|
2552
|
+
const result = await generateText({
|
|
2553
|
+
model: runtime.model,
|
|
2554
|
+
system: systemPrompt,
|
|
2555
|
+
prompt: `Child Sub-session is stuck. Question:\n${question}`,
|
|
2556
|
+
temperature: 0.2,
|
|
2557
|
+
...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
|
|
2558
|
+
});
|
|
2559
|
+
return result.text;
|
|
2560
|
+
},
|
|
2260
2561
|
fireHook: (input, signal) => self.fireHook(input, signal),
|
|
2261
2562
|
consumeBackgroundNotifications: () => self.consumeBackgroundNotifications(),
|
|
2262
2563
|
initOAuthProvider: () => self._initOAuthProvider(),
|
|
@@ -2285,21 +2586,21 @@ export class Agent {
|
|
|
2285
2586
|
return await h(info);
|
|
2286
2587
|
}
|
|
2287
2588
|
catch (err) {
|
|
2288
|
-
|
|
2589
|
+
logger.error("orchestrator", "askToolLoopContinue crashed", { error: err });
|
|
2289
2590
|
return "stop";
|
|
2290
2591
|
}
|
|
2291
2592
|
},
|
|
2292
2593
|
askSafetyOverride: async (info) => {
|
|
2293
2594
|
const h = self._safetyOverrideHandler;
|
|
2294
2595
|
if (!h) {
|
|
2295
|
-
|
|
2596
|
+
logger.warn("orchestrator", `askSafetyOverride called but no handler registered — blocking ${info.kind}: ${info.reason}`);
|
|
2296
2597
|
return { action: "block" };
|
|
2297
2598
|
}
|
|
2298
2599
|
try {
|
|
2299
2600
|
return await h(info);
|
|
2300
2601
|
}
|
|
2301
2602
|
catch (err) {
|
|
2302
|
-
|
|
2603
|
+
logger.error("orchestrator", "askSafetyOverride crashed", { error: err });
|
|
2303
2604
|
return { action: "block" };
|
|
2304
2605
|
}
|
|
2305
2606
|
},
|
|
@@ -2475,4 +2776,37 @@ export class Agent {
|
|
|
2475
2776
|
}
|
|
2476
2777
|
}
|
|
2477
2778
|
}
|
|
2779
|
+
function isTransientError(err) {
|
|
2780
|
+
if (!(err instanceof Error))
|
|
2781
|
+
return false;
|
|
2782
|
+
const msg = err.message.toLowerCase();
|
|
2783
|
+
return (msg.includes("fetch failed") ||
|
|
2784
|
+
msg.includes("rate limit") ||
|
|
2785
|
+
msg.includes("429") ||
|
|
2786
|
+
msg.includes("503") ||
|
|
2787
|
+
msg.includes("502") ||
|
|
2788
|
+
msg.includes("network") ||
|
|
2789
|
+
msg.includes("timeout") ||
|
|
2790
|
+
msg.includes("econnreset") ||
|
|
2791
|
+
msg.includes("econnrefused"));
|
|
2792
|
+
}
|
|
2793
|
+
function salvageSubSessionOutput(messages) {
|
|
2794
|
+
let lastAsstIdx = -1;
|
|
2795
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2796
|
+
if (messages[i].role === "assistant") {
|
|
2797
|
+
lastAsstIdx = i;
|
|
2798
|
+
break;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
const finalMessages = [];
|
|
2802
|
+
if (lastAsstIdx >= 0) {
|
|
2803
|
+
finalMessages.push(messages[lastAsstIdx]);
|
|
2804
|
+
for (let i = lastAsstIdx + 1; i < messages.length; i++) {
|
|
2805
|
+
if (messages[i].role === "tool") {
|
|
2806
|
+
finalMessages.push(messages[i]);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
return finalMessages;
|
|
2811
|
+
}
|
|
2478
2812
|
//# sourceMappingURL=orchestrator.js.map
|
|
@@ -22,6 +22,7 @@ import * as crypto from "node:crypto";
|
|
|
22
22
|
import { promises as fs } from "node:fs";
|
|
23
23
|
import * as path from "node:path";
|
|
24
24
|
import { getSessionDir } from "../storage/session-dir.js";
|
|
25
|
+
import { logger } from "../utils/logger.js";
|
|
25
26
|
// ─── stableCallId ─────────────────────────────────────────────────────────────
|
|
26
27
|
/**
|
|
27
28
|
* Derives a deterministic call_id from (turnId, toolName, input).
|
|
@@ -160,7 +161,7 @@ export function createPendingCallsLog(sessionId) {
|
|
|
160
161
|
void finalExists;
|
|
161
162
|
}
|
|
162
163
|
catch (err) {
|
|
163
|
-
|
|
164
|
+
logger.warn("orchestrator", `reconcile: could not clean staged path ${tmp}`, { error: err });
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
// Mark the entry as abandoned in the log.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runPipeline } from "../pil/pipeline.js";
|
|
2
2
|
import { getSessionLastTask, recordSessionLastTask, resolveCeiling } from "./scope-ceiling.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
3
4
|
export async function* prepareTurnContext(deps, userMessage, _budgetOverride) {
|
|
4
5
|
// PIL: enrich prompt before pushing to messages (D-01, D-03, D-04)
|
|
5
6
|
// Promise.race timeout of 200ms is inside runPipeline — fail-open guaranteed
|
|
@@ -31,7 +32,7 @@ export async function* prepareTurnContext(deps, userMessage, _budgetOverride) {
|
|
|
31
32
|
llmFallback = createLlmClassifier(deps.requireProvider(), deps.modelId);
|
|
32
33
|
}
|
|
33
34
|
catch (err) {
|
|
34
|
-
|
|
35
|
+
logger.error("pil", "LLM fallback wiring failed", { error: err });
|
|
35
36
|
}
|
|
36
37
|
// Model-driven clarification proposer (for discovery interview).
|
|
37
38
|
// The actual task model (via the same provider + modelId) generates the
|
|
@@ -42,7 +43,7 @@ export async function* prepareTurnContext(deps, userMessage, _budgetOverride) {
|
|
|
42
43
|
clarificationProposer = createModelClarificationProposer(deps.requireProvider(), deps.modelId);
|
|
43
44
|
}
|
|
44
45
|
catch (err) {
|
|
45
|
-
|
|
46
|
+
logger.error("pil", "clarification proposer wiring failed", { error: err });
|
|
46
47
|
}
|
|
47
48
|
pilCtxResolved = await runPipeline(userMessage, {
|
|
48
49
|
resumeDigest: deps.getResumeDigest(),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AgentMode, TaskRequest } from "../types/index.js";
|
|
2
2
|
import { type CustomSubagentConfig, type SandboxMode, type SandboxSettings } from "../utils/settings.js";
|
|
3
3
|
export declare const MAX_TOOL_ROUNDS: number;
|
|
4
|
+
export declare const HARD_MAX_TOOL_ROUNDS: number;
|
|
5
|
+
export declare const MAX_LLM_CALLS_PER_TURN: number;
|
|
4
6
|
export declare const VISION_MODEL = "grok-4-1-fast-reasoning";
|
|
5
7
|
export declare const COMPUTER_MODEL = "grok-4.20-0309-reasoning";
|
|
6
8
|
/**
|
|
@@ -61,6 +63,12 @@ export interface SystemPromptOptions {
|
|
|
61
63
|
* and can't run the full toolset anyway. Cuts ~6K tokens per sub-agent turn.
|
|
62
64
|
*/
|
|
63
65
|
subAgent?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* When true (tool-turn, i.e. second+ LLM call in the same user-turn tool
|
|
68
|
+
* loop), skip native-capabilities and skills sections that were already
|
|
69
|
+
* shown in the first call. Cuts ~4K tokens per tool round-trip.
|
|
70
|
+
*/
|
|
71
|
+
toolTurn?: boolean;
|
|
64
72
|
}
|
|
65
73
|
/**
|
|
66
74
|
* Render the LIVE per-turn MCP tool roster as a system-prompt block.
|