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
|
@@ -76,6 +76,7 @@ import { visionToolsNeeded } from "../tools/vision-gate.js";
|
|
|
76
76
|
import { recordTurnTrace } from "../ui/slash/debug.js";
|
|
77
77
|
import { statusBarStore } from "../ui/status-bar/store.js";
|
|
78
78
|
import { appendDecisionLog } from "../usage/decision-log.js";
|
|
79
|
+
import { logger } from "../utils/logger.js";
|
|
79
80
|
import { openUrl } from "../utils/open-url.js";
|
|
80
81
|
import { appendAudit, toolNeedsApproval } from "../utils/permission-mode.js";
|
|
81
82
|
import { getAutoCouncilConfidence, getAutoCouncilMinRoles, getProviderStallRetries, getProviderStallTimeoutMs, getRoleModels, getSteerInjectionEnabled, getTopLevelCompactKeepLast, getTopLevelCompactThresholdChars, getTopLevelToolBudgetChars, isAutoCouncilEnabled, loadMcpServers, } from "../utils/settings.js";
|
|
@@ -86,7 +87,7 @@ import { humanizeApiError, isAuthenticationError, isContextLimitError, summarize
|
|
|
86
87
|
import { buildGroundingFootnote, findUnverifiedClaims } from "./grounding-check.js";
|
|
87
88
|
import { buildInterruptedTurnNote } from "./interrupted-turn.js";
|
|
88
89
|
import { stableCallId } from "./pending-calls.js";
|
|
89
|
-
import { buildMcpCapabilityBlock } from "./prompts.js";
|
|
90
|
+
import { buildMcpCapabilityBlock, MAX_LLM_CALLS_PER_TURN, } from "./prompts.js";
|
|
90
91
|
import { extractProviderOptionsShape } from "./provider-options-shape.js";
|
|
91
92
|
import { wrapToolSetWithReadBudget } from "./read-path-budget.js";
|
|
92
93
|
import { containsEncryptedReasoning, sanitizeModelMessages } from "./reasoning.js";
|
|
@@ -194,7 +195,7 @@ export function rewriteSafetyApprovedToolResults(messages) {
|
|
|
194
195
|
}
|
|
195
196
|
import { stripDsmlMarkup } from "./message-processor.js";
|
|
196
197
|
export async function* executeToolEngine(args) {
|
|
197
|
-
let { deps, stepRouterPhase, phase2Runtime, runtime, modelInfo, _debugSteps, _ceilingHit, userMessage, pilCtx, turnModelId, turnProvider, _stepCeiling, userModelMessage, userEnrichedMessage, signal, observer, taskHash, provider, system, routerStore, attemptedOverflowRecovery, patternLoopForceHalt, userWriteAheadSeq, streamRetryCount, MAX_STREAM_RETRIES, subagents, systemParts, playwrightGuidance, _hasResponseTools, _pilResponseTools, patternLoopInjectCount, agentLoopDecisionCount, MAX_AGENT_LOOP_DECISIONS, _naturalCeiling, _ceilingTaskType, _ceilingSize, textToolReSteerCount, MAX_TEXT_TOOL_RESTEER, turnStartMs, _debugOn, _debugTurnId, _pilEnrichmentDeltaSnapshot, isChitchat, } = args;
|
|
198
|
+
let { deps, stepRouterPhase, phase2Runtime, runtime, modelInfo, _debugSteps, _ceilingHit, userMessage, pilCtx, turnModelId, turnProvider, _stepCeiling, userModelMessage, userEnrichedMessage, signal, observer, taskHash, provider, system, routerStore, attemptedOverflowRecovery, patternLoopForceHalt, userWriteAheadSeq, streamRetryCount, MAX_STREAM_RETRIES, subagents, systemParts, toolTurnSystem, playwrightGuidance, _hasResponseTools, _pilResponseTools, patternLoopInjectCount, agentLoopDecisionCount, MAX_AGENT_LOOP_DECISIONS, _naturalCeiling, _ceilingTaskType, _ceilingSize, textToolReSteerCount, MAX_TEXT_TOOL_RESTEER, turnStartMs, _debugOn, _debugTurnId, _pilEnrichmentDeltaSnapshot, isChitchat, } = args;
|
|
198
199
|
// Put all extracted code here:
|
|
199
200
|
let stallTriggered = false;
|
|
200
201
|
// Time-to-first-byte stall RE-PROMPT: some providers (observed:
|
|
@@ -219,6 +220,11 @@ export async function* executeToolEngine(args) {
|
|
|
219
220
|
// Bounded by the same maxStallRetries cap so a persistently-dead provider
|
|
220
221
|
// still falls through to the partial-answer rescue.
|
|
221
222
|
let midLoopStallRetryCount = 0;
|
|
223
|
+
// F3c — per-turn LLM call cap: counts every streamText() invocation
|
|
224
|
+
// (tool round-trip, stall re-prompt, stream retry) and hard-aborts
|
|
225
|
+
// the turn when exceeded. Prevents the session 526a83cf22df pattern
|
|
226
|
+
// where 3 user messages burnt 82% of 2.44M tokens in 36 LLM calls.
|
|
227
|
+
let llmCallsThisTurn = 0;
|
|
222
228
|
// Live-queue steering: messages the user typed mid-turn are drained at a
|
|
223
229
|
// prepareStep boundary and accumulated here, then re-appended (deduped) to
|
|
224
230
|
// the messages returned for each subsequent step. Loop-persistent so they
|
|
@@ -395,7 +401,7 @@ export async function* executeToolEngine(args) {
|
|
|
395
401
|
});
|
|
396
402
|
}
|
|
397
403
|
catch (emitErr) {
|
|
398
|
-
|
|
404
|
+
logger.error("orchestrator", "stall-reprompt telemetry failed", { error: emitErr });
|
|
399
405
|
}
|
|
400
406
|
try {
|
|
401
407
|
if (deps.session) {
|
|
@@ -411,7 +417,7 @@ export async function* executeToolEngine(args) {
|
|
|
411
417
|
}
|
|
412
418
|
}
|
|
413
419
|
catch (logErr) {
|
|
414
|
-
|
|
420
|
+
logger.error("orchestrator", "stall-reprompt log failed", { error: logErr });
|
|
415
421
|
}
|
|
416
422
|
return backoffMs;
|
|
417
423
|
};
|
|
@@ -434,11 +440,19 @@ export async function* executeToolEngine(args) {
|
|
|
434
440
|
let _pendingStructuredResponse = null;
|
|
435
441
|
let _pendingStructuredResponseLen = -1;
|
|
436
442
|
try {
|
|
443
|
+
const { getDatabase } = await import("../storage/db.js");
|
|
444
|
+
const db = getDatabase();
|
|
445
|
+
const row = db.prepare("SELECT parent_session_id FROM sessions WHERE id = ?").get(deps.session.id);
|
|
446
|
+
const isSubSession = !!row?.parent_session_id;
|
|
447
|
+
let contextWindow = modelInfo?.contextWindow || 0;
|
|
448
|
+
if (isSubSession && contextWindow > 0) {
|
|
449
|
+
contextWindow = Math.min(45000, contextWindow);
|
|
450
|
+
}
|
|
437
451
|
const settings = attemptedOverflowRecovery
|
|
438
|
-
? relaxCompactionSettings(deps.getCompactionSettings(
|
|
439
|
-
: deps.getCompactionSettings(
|
|
440
|
-
if (
|
|
441
|
-
await deps.compactForContext(provider, system,
|
|
452
|
+
? relaxCompactionSettings(deps.getCompactionSettings(contextWindow))
|
|
453
|
+
: deps.getCompactionSettings(contextWindow);
|
|
454
|
+
if (contextWindow) {
|
|
455
|
+
await deps.compactForContext(provider, system, contextWindow, signal, settings, attemptedOverflowRecovery);
|
|
442
456
|
}
|
|
443
457
|
// Vision-tool gate: for vision-proxy (text-only) models the registry
|
|
444
458
|
// adds 3 image tools (~500-700 tok) on every turn. Drop them when the
|
|
@@ -457,6 +471,7 @@ export async function* executeToolEngine(args) {
|
|
|
457
471
|
listDelegations: () => deps.listDelegations(),
|
|
458
472
|
modelId: turnModelId,
|
|
459
473
|
includeVisionTools,
|
|
474
|
+
consultParentSession: deps.consultParentSession,
|
|
460
475
|
});
|
|
461
476
|
// Top-level cumulative cap state. We accumulate the raw tool set
|
|
462
477
|
// (base + MCP + PIL response tools) across the assembly below,
|
|
@@ -532,7 +547,7 @@ export async function* executeToolEngine(args) {
|
|
|
532
547
|
});
|
|
533
548
|
}
|
|
534
549
|
catch (err) {
|
|
535
|
-
|
|
550
|
+
logger.error("mcp", "buildMcpToolSet failed, proceeding with builtins only", { error: err });
|
|
536
551
|
}
|
|
537
552
|
if (mcpBundle) {
|
|
538
553
|
closeMcp = mcpBundle.close;
|
|
@@ -596,7 +611,7 @@ export async function* executeToolEngine(args) {
|
|
|
596
611
|
// Apply the top-level cumulative cap once over the fully-assembled
|
|
597
612
|
// raw tool set. State is per-turn; each turn gets a fresh budget.
|
|
598
613
|
const topLevelCap = wrapToolSetWithCap(rawToolSet, {
|
|
599
|
-
maxCumulativeChars: getTopLevelToolBudgetChars(),
|
|
614
|
+
maxCumulativeChars: getTopLevelToolBudgetChars(deps.maxToolRounds),
|
|
600
615
|
midTierRatio: 0.5,
|
|
601
616
|
highTierRatio: 0.8,
|
|
602
617
|
label: "top-level",
|
|
@@ -669,9 +684,12 @@ export async function* executeToolEngine(args) {
|
|
|
669
684
|
// cheap-model cost) layered UNDER the tool-use playbook so the
|
|
670
685
|
// CRITICAL tool rules stay at the very front. Both fixed per turn, so
|
|
671
686
|
// they stay inside the cached prefix.
|
|
687
|
+
// F3c — tool-turn: use reduced system prompt (skip
|
|
688
|
+
// native-capabilities + skills already shown in first call).
|
|
689
|
+
const activeSystem = llmCallsThisTurn > 0 && toolTurnSystem ? toolTurnSystem : system;
|
|
672
690
|
const systemWithWorkbook = shouldInjectCheapModelWorkbook(runtime.modelInfo)
|
|
673
|
-
? injectCheapModelWorkbook(
|
|
674
|
-
:
|
|
691
|
+
? injectCheapModelWorkbook(activeSystem, pilCtx.taskType)
|
|
692
|
+
: activeSystem;
|
|
675
693
|
const systemWithPlaybook = shouldInjectCheapModelPlaybook(runtime.modelInfo)
|
|
676
694
|
? injectCheapModelPlaybook(systemWithWorkbook)
|
|
677
695
|
: systemWithWorkbook;
|
|
@@ -761,6 +779,8 @@ export async function* executeToolEngine(args) {
|
|
|
761
779
|
// stepCountIs(maxToolRounds) fires and the user sees the TUI freeze
|
|
762
780
|
// (session 7dcf8fd7d6a4 hit exactly 100 rounds → looked like a crash).
|
|
763
781
|
let _lastFinishReason = null;
|
|
782
|
+
// F3b — track hard cap hit for visible toast after stream ends.
|
|
783
|
+
let _hardCapHit = false;
|
|
764
784
|
// Phase B4: compact older tool_result parts before each top-level
|
|
765
785
|
// step once cumulative message chars exceed the configured threshold.
|
|
766
786
|
// The compactor preserves system + first user verbatim and keeps the
|
|
@@ -864,7 +884,7 @@ export async function* executeToolEngine(args) {
|
|
|
864
884
|
}
|
|
865
885
|
}
|
|
866
886
|
catch (err) {
|
|
867
|
-
|
|
887
|
+
logger.error("orchestrator", "loop auto-decision failed", { error: err });
|
|
868
888
|
}
|
|
869
889
|
}
|
|
870
890
|
return deps.askToolLoopContinue ? await deps.askToolLoopContinue(info) : "stop";
|
|
@@ -910,6 +930,14 @@ export async function* executeToolEngine(args) {
|
|
|
910
930
|
// _ceilingHit and _ceilingHitAtStep are kept for telemetry: a
|
|
911
931
|
// crossing event is logged for forensics, but no action is taken.
|
|
912
932
|
const dynamicStopWhen = (async (state) => {
|
|
933
|
+
// F3b — HARD cap: absolute non-bumpable ceiling per user turn.
|
|
934
|
+
// Fires AFTER the soft cap (maxToolRounds) has been bumped by the
|
|
935
|
+
// user. Prevents runaway sessions (session 526a83cf22df: 16 LLM
|
|
936
|
+
// calls for a single user message, 2.44M total input tokens).
|
|
937
|
+
if (state.steps.length > deps.hardMaxToolRounds) {
|
|
938
|
+
_hardCapHit = true;
|
|
939
|
+
return true;
|
|
940
|
+
}
|
|
913
941
|
// Terminal response tool: a `respond_*` call IS the model's final
|
|
914
942
|
// structured answer (its `execute` is identity — the payload lives
|
|
915
943
|
// in the tool-call args). `shouldHaltOnResponseTool` decides if the
|
|
@@ -1007,6 +1035,17 @@ export async function* executeToolEngine(args) {
|
|
|
1007
1035
|
const stall = createStallWatchdog(getProviderStallTimeoutMs(), () => {
|
|
1008
1036
|
stallTriggered = true;
|
|
1009
1037
|
});
|
|
1038
|
+
// F3c — hard-cap LLM calls per turn before this streamText()
|
|
1039
|
+
if (++llmCallsThisTurn > MAX_LLM_CALLS_PER_TURN) {
|
|
1040
|
+
stall.dispose();
|
|
1041
|
+
yield {
|
|
1042
|
+
type: "error",
|
|
1043
|
+
content: `Turn aborted: reached the limit of ${MAX_LLM_CALLS_PER_TURN} LLM calls for this message. Try a narrower request or break your task into smaller steps.`,
|
|
1044
|
+
isAuthError: false,
|
|
1045
|
+
};
|
|
1046
|
+
yield { type: "done" };
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1010
1049
|
const result = streamText({
|
|
1011
1050
|
model: runtime.model,
|
|
1012
1051
|
system: systemForModel,
|
|
@@ -1022,12 +1061,18 @@ export async function* executeToolEngine(args) {
|
|
|
1022
1061
|
// See src/orchestrator/tool-args-repair.ts for the transforms.
|
|
1023
1062
|
experimental_repairToolCall: repairToolCallHook,
|
|
1024
1063
|
prepareStep: ({ stepNumber: sn, messages: stepMessages }) => {
|
|
1025
|
-
// A new step's provider request is about to go out — reset the
|
|
1026
|
-
// per-step chunk counter. Fires after the previous step's chunks
|
|
1027
|
-
// were counted (prepareStep runs post tool-execution), so if THIS
|
|
1028
|
-
// step's request wedges before any byte, chunksThisStep stays 0 and
|
|
1029
|
-
// the mid-loop dead-socket continuation can safely resume from here.
|
|
1030
1064
|
chunksThisStep = 0;
|
|
1065
|
+
if (deps.isSubSession) {
|
|
1066
|
+
logger.info("orchestrator", "Sub-session executing tool round", {
|
|
1067
|
+
stepNumber: sn,
|
|
1068
|
+
maxToolRounds: deps.maxToolRounds,
|
|
1069
|
+
});
|
|
1070
|
+
deps.emitSubagentStatus({
|
|
1071
|
+
agent: "sub-session",
|
|
1072
|
+
description: `Running sub-session task...`,
|
|
1073
|
+
detail: `[Sub-Session] Executing tool round ${sn + 1} of ${deps.maxToolRounds}...`,
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1031
1076
|
// --- Live-queue steering injection ---------------------------
|
|
1032
1077
|
// Drain the UI steer queue ONCE per prepareStep call (sn >= 1),
|
|
1033
1078
|
// accumulate into pendingSteers, and graft pendingSteers onto the
|
|
@@ -1058,7 +1103,7 @@ export async function* executeToolEngine(args) {
|
|
|
1058
1103
|
});
|
|
1059
1104
|
}
|
|
1060
1105
|
catch (emitErr) {
|
|
1061
|
-
|
|
1106
|
+
logger.error("orchestrator", "steer-inject telemetry failed", { error: emitErr });
|
|
1062
1107
|
}
|
|
1063
1108
|
}
|
|
1064
1109
|
const baseRes = (() => {
|
|
@@ -1338,7 +1383,7 @@ export async function* executeToolEngine(args) {
|
|
|
1338
1383
|
});
|
|
1339
1384
|
}
|
|
1340
1385
|
catch (err) {
|
|
1341
|
-
|
|
1386
|
+
logger.error("orchestrator", "failed to emit llm-done", { error: err });
|
|
1342
1387
|
}
|
|
1343
1388
|
deps.setCurrentCallId("");
|
|
1344
1389
|
// Rec #1 persisted forensics: onFinish fires once per top-level turn,
|
|
@@ -1349,7 +1394,7 @@ export async function* executeToolEngine(args) {
|
|
|
1349
1394
|
persistSessionExperience(deps.session?.id ?? null, getSessionExperienceCounts());
|
|
1350
1395
|
}
|
|
1351
1396
|
catch (err) {
|
|
1352
|
-
|
|
1397
|
+
logger.error("orchestrator", "persistSessionExperience failed", { error: err });
|
|
1353
1398
|
}
|
|
1354
1399
|
},
|
|
1355
1400
|
});
|
|
@@ -2749,6 +2794,15 @@ export async function* executeToolEngine(args) {
|
|
|
2749
2794
|
catch {
|
|
2750
2795
|
/* fail-open */
|
|
2751
2796
|
}
|
|
2797
|
+
// F3b — surface hard-cap stop (absolute ceiling, cannot be bumped).
|
|
2798
|
+
if (_hardCapHit) {
|
|
2799
|
+
yield {
|
|
2800
|
+
type: "content",
|
|
2801
|
+
content: `\n\n[Hard limit reached: Agent hit the absolute step ceiling of ${deps.hardMaxToolRounds} steps. ` +
|
|
2802
|
+
`This turn cannot continue — too many LLM round-trips. ` +
|
|
2803
|
+
`Start a new turn or break your task into smaller steps.]\n`,
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2752
2806
|
// Surface the round-cap stop so the user knows why the agent halted
|
|
2753
2807
|
// (session 7dcf8fd7d6a4 hit stepCountIs(100) silently, looked like a
|
|
2754
2808
|
// crash). AI SDK reports finishReason='tool-calls' when the step cap
|
|
@@ -3075,5 +3129,10 @@ export async function* executeToolEngine(args) {
|
|
|
3075
3129
|
catch (err) {
|
|
3076
3130
|
throw err;
|
|
3077
3131
|
}
|
|
3132
|
+
finally {
|
|
3133
|
+
if (deps.isSubSession) {
|
|
3134
|
+
deps.emitSubagentStatus(null);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3078
3137
|
}
|
|
3079
3138
|
//# sourceMappingURL=tool-engine.js.map
|
|
@@ -23,6 +23,8 @@ export interface TurnRunnerDepsBase {
|
|
|
23
23
|
readonly bash: BashTool;
|
|
24
24
|
readonly mode: AgentMode;
|
|
25
25
|
readonly maxToolRounds: number;
|
|
26
|
+
/** Absolute non-bumpable ceiling per turn — fires after the user bumps past maxToolRounds. */
|
|
27
|
+
readonly hardMaxToolRounds: number;
|
|
26
28
|
readonly schedules: import("../tools/schedule.js").ScheduleManager;
|
|
27
29
|
readonly sendTelegramFile: ((filePath: string) => Promise<ToolResult>) | null;
|
|
28
30
|
getCompactedThisTurn(): boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
2
2
|
import { installMockModel, textOnlyStream } from "../../agent-harness/mock-model.js";
|
|
3
3
|
import { loadCatalog } from "../../models/registry.js";
|
|
4
|
-
import { createLlmClassifier } from "../llm-classify.js";
|
|
4
|
+
import { classifySubSessionAction, createLlmClassifier } from "../llm-classify.js";
|
|
5
5
|
describe("createLlmClassifier (PIL Layer 1 Pass 4)", () => {
|
|
6
6
|
beforeAll(async () => {
|
|
7
7
|
await loadCatalog();
|
|
@@ -194,4 +194,82 @@ describe("createLlmClassifier (PIL Layer 1 Pass 4)", () => {
|
|
|
194
194
|
expect((await bareClassify("fix it"))?.deliverableKind).toBeNull();
|
|
195
195
|
});
|
|
196
196
|
});
|
|
197
|
+
describe("classifySubSessionAction", () => {
|
|
198
|
+
let cleanup = null;
|
|
199
|
+
afterEach(() => {
|
|
200
|
+
cleanup?.();
|
|
201
|
+
cleanup = null;
|
|
202
|
+
});
|
|
203
|
+
it("correctly parses SPAWN_SUB_SESSION from model response", async () => {
|
|
204
|
+
const handle = installMockModel({
|
|
205
|
+
fixture: { stream: textOnlyStream("SPAWN_SUB_SESSION,0.98,Requires writing a test suite") },
|
|
206
|
+
});
|
|
207
|
+
cleanup = handle.uninstall;
|
|
208
|
+
const factory = (() => handle.model);
|
|
209
|
+
const result = await classifySubSessionAction(factory, "deepseek-v4-flash", "fix all compile errors");
|
|
210
|
+
expect(result).not.toBeNull();
|
|
211
|
+
expect(result?.action).toBe("SPAWN_SUB_SESSION");
|
|
212
|
+
expect(result?.confidence).toBe(0.98);
|
|
213
|
+
expect(result?.reason).toBe("Requires writing a test suite");
|
|
214
|
+
});
|
|
215
|
+
it("correctly parses DIRECT_ANSWER from model response", async () => {
|
|
216
|
+
const handle = installMockModel({ fixture: { stream: textOnlyStream("DIRECT_ANSWER,0.95,Simple query") } });
|
|
217
|
+
cleanup = handle.uninstall;
|
|
218
|
+
const factory = (() => handle.model);
|
|
219
|
+
const result = await classifySubSessionAction(factory, "deepseek-v4-flash", "explain database index");
|
|
220
|
+
expect(result?.action).toBe("DIRECT_ANSWER");
|
|
221
|
+
expect(result?.confidence).toBe(0.95);
|
|
222
|
+
});
|
|
223
|
+
it("correctly routes greetings, thanks, or simple math via heuristic without calling LLM", async () => {
|
|
224
|
+
const factory = (() => {
|
|
225
|
+
throw new Error("Should not be called because of heuristic short-circuit");
|
|
226
|
+
});
|
|
227
|
+
const resultMath = await classifySubSessionAction(factory, "deepseek-v4-flash", "2+2");
|
|
228
|
+
expect(resultMath?.action).toBe("DIRECT_ANSWER");
|
|
229
|
+
expect(resultMath?.confidence).toBe(0.99);
|
|
230
|
+
expect(resultMath?.reason).toContain("heuristic (simple math)");
|
|
231
|
+
const resultGreeting = await classifySubSessionAction(factory, "deepseek-v4-flash", "hello");
|
|
232
|
+
expect(resultGreeting?.action).toBe("DIRECT_ANSWER");
|
|
233
|
+
expect(resultGreeting?.confidence).toBe(0.99);
|
|
234
|
+
expect(resultGreeting?.reason).toContain("heuristic (greeting)");
|
|
235
|
+
const resultThanks = await classifySubSessionAction(factory, "deepseek-v4-flash", "thank you");
|
|
236
|
+
expect(resultThanks?.action).toBe("DIRECT_ANSWER");
|
|
237
|
+
expect(resultThanks?.confidence).toBe(0.99);
|
|
238
|
+
expect(resultThanks?.reason).toContain("heuristic (thanks)");
|
|
239
|
+
});
|
|
240
|
+
it("does not route greetings/thanks via heuristic if they contain other text (fuzzy bypass prevention)", async () => {
|
|
241
|
+
const handle = installMockModel({ fixture: { stream: textOnlyStream("SPAWN_SUB_SESSION,0.95,Delete file task") } });
|
|
242
|
+
cleanup = handle.uninstall;
|
|
243
|
+
const factory = (() => handle.model);
|
|
244
|
+
const result = await classifySubSessionAction(factory, "deepseek-v4-flash", "hello, delete file X");
|
|
245
|
+
expect(result?.action).toBe("SPAWN_SUB_SESSION");
|
|
246
|
+
});
|
|
247
|
+
it("bypasses heuristic when MUONROI_DISABLE_HEURISTIC_ROUTING is set to 1", async () => {
|
|
248
|
+
process.env.MUONROI_DISABLE_HEURISTIC_ROUTING = "1";
|
|
249
|
+
const handle = installMockModel({ fixture: { stream: textOnlyStream("SPAWN_SUB_SESSION,0.95,Forced class") } });
|
|
250
|
+
cleanup = handle.uninstall;
|
|
251
|
+
try {
|
|
252
|
+
const factory = (() => handle.model);
|
|
253
|
+
const result = await classifySubSessionAction(factory, "deepseek-v4-flash", "hello");
|
|
254
|
+
expect(result?.action).toBe("SPAWN_SUB_SESSION");
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
delete process.env.MUONROI_DISABLE_HEURISTIC_ROUTING;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
it("correctly parses ROTATE_SESSION from model response", async () => {
|
|
261
|
+
const handle = installMockModel({ fixture: { stream: textOnlyStream("ROTATE_SESSION,0.90,New topic") } });
|
|
262
|
+
cleanup = handle.uninstall;
|
|
263
|
+
const factory = (() => handle.model);
|
|
264
|
+
const result = await classifySubSessionAction(factory, "deepseek-v4-flash", "switch topic");
|
|
265
|
+
expect(result?.action).toBe("ROTATE_SESSION");
|
|
266
|
+
});
|
|
267
|
+
it("returns null when the reply cannot be parsed", async () => {
|
|
268
|
+
const handle = installMockModel({ fixture: { stream: textOnlyStream("bad response") } });
|
|
269
|
+
cleanup = handle.uninstall;
|
|
270
|
+
const factory = (() => handle.model);
|
|
271
|
+
const result = await classifySubSessionAction(factory, "deepseek-v4-flash", "test");
|
|
272
|
+
expect(result).toBeNull();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
197
275
|
//# sourceMappingURL=llm-classify.test.js.map
|
|
@@ -1243,40 +1243,40 @@ export async function layer1Intent(ctx, opts = {}) {
|
|
|
1243
1243
|
// touch existing files.
|
|
1244
1244
|
// 0.7 confidence threshold for Pass 2 keyword override remains
|
|
1245
1245
|
// unchanged (HIGH_CONF_THRESHOLD_PASS2 above).
|
|
1246
|
-
const brainRaw = await classifyViaBrain(`You are a multilingual prompt classifier. The user's prompt may be in English, Vietnamese, or a mix of both.
|
|
1247
|
-
Classify the prompt's INTENT (not its language). Reply with TWO lowercase words separated by a comma: <category>,<style>
|
|
1248
|
-
|
|
1249
|
-
Category — pick ONE (listed in neutral order, no precedence):
|
|
1250
|
-
analyze — explain / inspect / review existing code (giải thích, phân tích, review)
|
|
1251
|
-
debug — fix a bug or investigate failure (sửa lỗi, fix bug, lỗi, traceback)
|
|
1252
|
-
generate — create new code/file or add new behavior (tạo, sinh code, viết function mới, thêm)
|
|
1253
|
-
refactor — restructure existing code (tái cấu trúc, refactor)
|
|
1254
|
-
plan — design / roadmap / architecture (kế hoạch, thiết kế, kiến trúc)
|
|
1255
|
-
documentation — write docs/comments (viết docs, comment, jsdoc)
|
|
1256
|
-
general — chitchat OR unclear / ambiguous coding intent
|
|
1257
|
-
|
|
1258
|
-
Rules (Phase 4 4P-2 disambiguation):
|
|
1259
|
-
- Only return refactor when the user EXPLICITLY uses one of: rename, restructure, reorganize, extract, inline, move, migrate, reshape — applied to EXISTING code WITHOUT adding new behavior.
|
|
1260
|
-
- Feature additions ('add flag', 'thêm', 'create endpoint', 'thêm option'), changing a DEFAULT value, adding tests, or improving coverage are 'generate' — NOT refactor.
|
|
1261
|
-
- 'improve', 'change', 'update', 'modify', 'đổi', 'cải thiện' alone do NOT imply refactor — pick the specific category by what the change actually does.
|
|
1262
|
-
- When the request is ambiguous, prefer 'general' over guessing refactor.
|
|
1263
|
-
|
|
1264
|
-
Negative examples (NOT refactor):
|
|
1265
|
-
- "đổi default --max-tool-rounds 8 sang 12" → generate
|
|
1266
|
-
- "improve test coverage" → generate
|
|
1267
|
-
- "tại sao X trả empty" → analyze
|
|
1268
|
-
- "fix CI failing" → debug
|
|
1269
|
-
|
|
1270
|
-
Style — pick ONE:
|
|
1271
|
-
concise (ngắn gọn) | balanced (cân bằng) | detailed (chi tiết)
|
|
1272
|
-
|
|
1273
|
-
Examples:
|
|
1274
|
-
"Refactor this function" → refactor,balanced
|
|
1275
|
-
"tại sao test fail" → debug,balanced
|
|
1276
|
-
"thiết kế hệ thống auth" → plan,detailed
|
|
1277
|
-
"thêm flag --foo" → generate,concise
|
|
1278
|
-
"hi" → general,concise
|
|
1279
|
-
|
|
1246
|
+
const brainRaw = await classifyViaBrain(`You are a multilingual prompt classifier. The user's prompt may be in English, Vietnamese, or a mix of both.
|
|
1247
|
+
Classify the prompt's INTENT (not its language). Reply with TWO lowercase words separated by a comma: <category>,<style>
|
|
1248
|
+
|
|
1249
|
+
Category — pick ONE (listed in neutral order, no precedence):
|
|
1250
|
+
analyze — explain / inspect / review existing code (giải thích, phân tích, review)
|
|
1251
|
+
debug — fix a bug or investigate failure (sửa lỗi, fix bug, lỗi, traceback)
|
|
1252
|
+
generate — create new code/file or add new behavior (tạo, sinh code, viết function mới, thêm)
|
|
1253
|
+
refactor — restructure existing code (tái cấu trúc, refactor)
|
|
1254
|
+
plan — design / roadmap / architecture (kế hoạch, thiết kế, kiến trúc)
|
|
1255
|
+
documentation — write docs/comments (viết docs, comment, jsdoc)
|
|
1256
|
+
general — chitchat OR unclear / ambiguous coding intent
|
|
1257
|
+
|
|
1258
|
+
Rules (Phase 4 4P-2 disambiguation):
|
|
1259
|
+
- Only return refactor when the user EXPLICITLY uses one of: rename, restructure, reorganize, extract, inline, move, migrate, reshape — applied to EXISTING code WITHOUT adding new behavior.
|
|
1260
|
+
- Feature additions ('add flag', 'thêm', 'create endpoint', 'thêm option'), changing a DEFAULT value, adding tests, or improving coverage are 'generate' — NOT refactor.
|
|
1261
|
+
- 'improve', 'change', 'update', 'modify', 'đổi', 'cải thiện' alone do NOT imply refactor — pick the specific category by what the change actually does.
|
|
1262
|
+
- When the request is ambiguous, prefer 'general' over guessing refactor.
|
|
1263
|
+
|
|
1264
|
+
Negative examples (NOT refactor):
|
|
1265
|
+
- "đổi default --max-tool-rounds 8 sang 12" → generate
|
|
1266
|
+
- "improve test coverage" → generate
|
|
1267
|
+
- "tại sao X trả empty" → analyze
|
|
1268
|
+
- "fix CI failing" → debug
|
|
1269
|
+
|
|
1270
|
+
Style — pick ONE:
|
|
1271
|
+
concise (ngắn gọn) | balanced (cân bằng) | detailed (chi tiết)
|
|
1272
|
+
|
|
1273
|
+
Examples:
|
|
1274
|
+
"Refactor this function" → refactor,balanced
|
|
1275
|
+
"tại sao test fail" → debug,balanced
|
|
1276
|
+
"thiết kế hệ thống auth" → plan,detailed
|
|
1277
|
+
"thêm flag --foo" → generate,concise
|
|
1278
|
+
"hi" → general,concise
|
|
1279
|
+
|
|
1280
1280
|
Prompt: "${ctx.raw.slice(0, 500)}"`, 1500);
|
|
1281
1281
|
if (brainRaw) {
|
|
1282
1282
|
pass3LegacyTaskSucceeded = true;
|
|
@@ -1327,9 +1327,9 @@ Prompt: "${ctx.raw.slice(0, 500)}"`, 1500);
|
|
|
1327
1327
|
if (pass3LegacyTaskAttempted) {
|
|
1328
1328
|
legacyBrainAttempted = true;
|
|
1329
1329
|
pass3LegacyStyleAttempted = true;
|
|
1330
|
-
const brainRawStyle = await classifyViaBrain(`Detect the user's preferred output style. The prompt may be EN or VN.
|
|
1331
|
-
Reply with ONE word: concise (ngắn gọn) | balanced (bình thường) | detailed (chi tiết).
|
|
1332
|
-
|
|
1330
|
+
const brainRawStyle = await classifyViaBrain(`Detect the user's preferred output style. The prompt may be EN or VN.
|
|
1331
|
+
Reply with ONE word: concise (ngắn gọn) | balanced (bình thường) | detailed (chi tiết).
|
|
1332
|
+
|
|
1333
1333
|
Prompt: "${ctx.raw.slice(0, 300)}"`, 800);
|
|
1334
1334
|
if (brainRawStyle) {
|
|
1335
1335
|
pass3LegacyStyleSucceeded = true;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { truncateToBudget } from "./budget.js";
|
|
2
2
|
import { isPonytailModeEnabled } from "./config.js";
|
|
3
|
-
const PONYTAIL_INSTRUCTION = `[LAZY SENIOR / PONYTAIL MODE ACTIVE]
|
|
4
|
-
"The best code is the code you never wrote." You MUST adhere to the following Decision Ladder:
|
|
5
|
-
1. Standard Library first.
|
|
6
|
-
2. Native platform features next.
|
|
7
|
-
3. 1-line solution next.
|
|
8
|
-
4. YAGNI: Do not write it if not strictly necessary.
|
|
9
|
-
5. Smallest correct change.
|
|
10
|
-
|
|
3
|
+
const PONYTAIL_INSTRUCTION = `[LAZY SENIOR / PONYTAIL MODE ACTIVE]
|
|
4
|
+
"The best code is the code you never wrote." You MUST adhere to the following Decision Ladder:
|
|
5
|
+
1. Standard Library first.
|
|
6
|
+
2. Native platform features next.
|
|
7
|
+
3. 1-line solution next.
|
|
8
|
+
4. YAGNI: Do not write it if not strictly necessary.
|
|
9
|
+
5. Smallest correct change.
|
|
10
|
+
|
|
11
11
|
DOCUMENTATION RULE: If you bypass a complex/scalable pattern in favor of a simple one to adhere to this rule, you MUST add a comment in the code starting exactly with "// Intentional simplification: [Reason]".`;
|
|
12
12
|
export async function layer2_5Ponytail(ctx) {
|
|
13
13
|
// If task is not coding/action-related (e.g., chitchat), we might not need it, but pipeline.ts already skips layers if taskType is null.
|
|
@@ -76,3 +76,14 @@ export type LlmClassifyFn = (prompt: string, signal?: AbortSignal) => Promise<Ll
|
|
|
76
76
|
* fail-open (keep prior taskType, do not block the turn).
|
|
77
77
|
*/
|
|
78
78
|
export declare function createLlmClassifier(factory: ProviderFactory, modelId: string): LlmClassifyFn;
|
|
79
|
+
export type SubSessionAction = "DIRECT_ANSWER" | "ROTATE_SESSION" | "SPAWN_SUB_SESSION";
|
|
80
|
+
export interface SubSessionRouteResult {
|
|
81
|
+
action: SubSessionAction;
|
|
82
|
+
confidence: number;
|
|
83
|
+
reason: string;
|
|
84
|
+
}
|
|
85
|
+
export declare function classifySubSessionActionHeuristic(prompt: string): SubSessionRouteResult | null;
|
|
86
|
+
export declare function classifySubSessionAction(factory: ProviderFactory, modelId: string, prompt: string, contextInfo?: {
|
|
87
|
+
currentChars: number;
|
|
88
|
+
threshold: number;
|
|
89
|
+
}, signal?: AbortSignal): Promise<SubSessionRouteResult | null>;
|