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
|
@@ -4,22 +4,67 @@ import { buildContractSection } from "../pil/agent-operating-contract.js";
|
|
|
4
4
|
import { buildNativeCapabilitiesSection } from "../pil/native-capabilities-workbook.js";
|
|
5
5
|
import { getProviderCapabilities } from "../providers/capabilities.js";
|
|
6
6
|
import { loadCustomInstructions } from "../utils/instructions.js";
|
|
7
|
-
import {
|
|
7
|
+
import { logger } from "../utils/logger.js";
|
|
8
|
+
import { loadUserSettings, loadValidSubAgents, } from "../utils/settings.js";
|
|
8
9
|
import { resolveShell } from "../utils/shell.js";
|
|
9
10
|
import { discoverSkills, formatSkillsForPrompt } from "../utils/skills.js";
|
|
10
|
-
//
|
|
11
|
-
// after session
|
|
12
|
-
//
|
|
11
|
+
// F3a — hard cap on tool rounds per user turn. Reduced 100 → 40
|
|
12
|
+
// after session 526a83cf22df logged 2.44M input tokens over 46 LLM calls
|
|
13
|
+
// with 3 turns (seq 33/81/1) consuming 82% of tokens.
|
|
14
|
+
// Env override allowed range 10..400.
|
|
15
|
+
// Env override allowed range 10..400 (or up to 2000 in agent-first).
|
|
13
16
|
function readMaxToolRoundsFromEnv() {
|
|
14
|
-
const
|
|
17
|
+
const settings = loadUserSettings();
|
|
18
|
+
const agentFirst = settings.agentFirst !== false &&
|
|
19
|
+
process.env.MUONROI_AGENT_FIRST !== "0" &&
|
|
20
|
+
process.env.MUONROI_AGENT_FIRST !== "false";
|
|
21
|
+
const raw = process.env.MUONROI_MAX_TOOL_ROUNDS || settings.maxToolRounds;
|
|
15
22
|
if (!raw)
|
|
16
|
-
return
|
|
23
|
+
return agentFirst ? 200 : 40;
|
|
17
24
|
const n = Number(raw);
|
|
18
25
|
if (!Number.isFinite(n))
|
|
19
|
-
return
|
|
20
|
-
|
|
26
|
+
return agentFirst ? 200 : 40;
|
|
27
|
+
const maxLimit = agentFirst ? 2000 : 400;
|
|
28
|
+
return Math.max(10, Math.min(maxLimit, Math.floor(n)));
|
|
21
29
|
}
|
|
22
30
|
export const MAX_TOOL_ROUNDS = readMaxToolRoundsFromEnv();
|
|
31
|
+
// F3b — HARD cap: absolute non-bumpable ceiling per user turn.
|
|
32
|
+
// Fires AFTER the soft cap has been bumped by the user.
|
|
33
|
+
// Env override allowed range 20..400 (or up to 3000 in agent-first).
|
|
34
|
+
function readHardMaxToolRoundsFromEnv() {
|
|
35
|
+
const settings = loadUserSettings();
|
|
36
|
+
const agentFirst = settings.agentFirst !== false &&
|
|
37
|
+
process.env.MUONROI_AGENT_FIRST !== "0" &&
|
|
38
|
+
process.env.MUONROI_AGENT_FIRST !== "false";
|
|
39
|
+
const raw = process.env.MUONROI_HARD_MAX_TOOL_ROUNDS || settings.hardMaxToolRounds;
|
|
40
|
+
if (!raw)
|
|
41
|
+
return agentFirst ? 300 : 60;
|
|
42
|
+
const n = Number(raw);
|
|
43
|
+
if (!Number.isFinite(n))
|
|
44
|
+
return agentFirst ? 300 : 60;
|
|
45
|
+
const maxLimit = agentFirst ? 3000 : 400;
|
|
46
|
+
return Math.max(20, Math.min(maxLimit, Math.floor(n)));
|
|
47
|
+
}
|
|
48
|
+
export const HARD_MAX_TOOL_ROUNDS = readHardMaxToolRoundsFromEnv();
|
|
49
|
+
// F3c — per-turn LLM call cap: how many streamText() invocations are
|
|
50
|
+
// allowed per user turn.
|
|
51
|
+
// Default 12 (or 100 in agent-first).
|
|
52
|
+
// Env override MUONROI_MAX_LLM_CALLS_PER_TURN, range 3..100 (or up to 500 in agent-first).
|
|
53
|
+
function readMaxLlmCallsPerTurn() {
|
|
54
|
+
const settings = loadUserSettings();
|
|
55
|
+
const agentFirst = settings.agentFirst !== false &&
|
|
56
|
+
process.env.MUONROI_AGENT_FIRST !== "0" &&
|
|
57
|
+
process.env.MUONROI_AGENT_FIRST !== "false";
|
|
58
|
+
const raw = process.env.MUONROI_MAX_LLM_CALLS_PER_TURN || settings.maxLlmCallsPerTurn;
|
|
59
|
+
if (!raw)
|
|
60
|
+
return agentFirst ? 100 : 12;
|
|
61
|
+
const n = Number(raw);
|
|
62
|
+
if (!Number.isFinite(n))
|
|
63
|
+
return agentFirst ? 100 : 12;
|
|
64
|
+
const maxLimit = agentFirst ? 500 : 100;
|
|
65
|
+
return Math.max(3, Math.min(maxLimit, Math.floor(n)));
|
|
66
|
+
}
|
|
67
|
+
export const MAX_LLM_CALLS_PER_TURN = readMaxLlmCallsPerTurn();
|
|
23
68
|
export const VISION_MODEL = "grok-4-1-fast-reasoning";
|
|
24
69
|
export const COMPUTER_MODEL = "grok-4.20-0309-reasoning";
|
|
25
70
|
/**
|
|
@@ -62,9 +107,7 @@ export function detectProjectStack(cwd) {
|
|
|
62
107
|
// Best-effort enrichment: a missing/unreadable cwd simply omits the stack
|
|
63
108
|
// line (the ENVIRONMENT cwd line already surfaces "<unknown>"). Debug-gated
|
|
64
109
|
// so prompt assembly never corrupts the TUI at startup.
|
|
65
|
-
|
|
66
|
-
console.error(`[orchestrator/prompts] detectProjectStack failed for ${cwd}: ${err?.message}`);
|
|
67
|
-
}
|
|
110
|
+
logger.error("orchestrator", `detectProjectStack failed for ${cwd}`, { error: err });
|
|
68
111
|
return "";
|
|
69
112
|
}
|
|
70
113
|
const has = (name) => entries.includes(name);
|
|
@@ -406,15 +449,18 @@ export function buildMcpCapabilityBlock(toolNames) {
|
|
|
406
449
|
}
|
|
407
450
|
const _staticPrefixCache = new Map();
|
|
408
451
|
const STATIC_PREFIX_CACHE_TTL_MS = 300_000; // 5 min — ample; inputs are session-stable
|
|
409
|
-
function staticPrefixCacheKey(cwd, mode, providerId, isChitchat, subagentsHash, subAgent = false) {
|
|
410
|
-
return `${cwd}|${mode}|${providerId}|${isChitchat}|${subagentsHash}|${subAgent}`;
|
|
452
|
+
function staticPrefixCacheKey(cwd, mode, providerId, isChitchat, subagentsHash, subAgent = false, toolTurn = false) {
|
|
453
|
+
return `${cwd}|${mode}|${providerId}|${isChitchat}|${subagentsHash}|${subAgent}|${toolTurn}`;
|
|
411
454
|
}
|
|
412
|
-
function computeStaticPrefix(cwd, mode, subagents, providerId, chitchat, subAgent = false) {
|
|
455
|
+
function computeStaticPrefix(cwd, mode, subagents, providerId, chitchat, subAgent = false, toolTurn = false) {
|
|
413
456
|
const custom = loadCustomInstructions(cwd);
|
|
414
457
|
const customSection = subAgent || !custom
|
|
415
458
|
? ""
|
|
416
459
|
: `\n\nCUSTOM INSTRUCTIONS:\n${custom}\n\nFollow the above alongside standard instructions.\n`;
|
|
417
|
-
|
|
460
|
+
// Tool-turn: skip agent-skills catalog (~2K tokens) and native-capabilities block (~2K tokens).
|
|
461
|
+
// The agent was already shown these in the first call of this turn and does not need
|
|
462
|
+
// to re-read them on every tool round-trip.
|
|
463
|
+
const skillsText = chitchat || subAgent || toolTurn ? "" : formatSkillsForPrompt(discoverSkills(cwd));
|
|
418
464
|
const skillsSection = skillsText ? `\n\n${skillsText}\n` : "";
|
|
419
465
|
const subagentsSection = chitchat ? "" : formatCustomSubagentsPromptSection(subagents ?? loadValidSubAgents());
|
|
420
466
|
let modePrompt = MODE_PROMPTS[mode];
|
|
@@ -430,19 +476,20 @@ function computeStaticPrefix(cwd, mode, subagents, providerId, chitchat, subAgen
|
|
|
430
476
|
modePrompt = modePrompt.replace(/\n- (wallet_|paid_|fetch_payment|schedule_|generate_|computer_|search_x).*/g, "");
|
|
431
477
|
}
|
|
432
478
|
const contractSection = buildContractSection({ chitchat });
|
|
433
|
-
const nativeCapabilitiesSection = buildNativeCapabilitiesSection({ mode, chitchat });
|
|
479
|
+
const nativeCapabilitiesSection = toolTurn ? "" : buildNativeCapabilitiesSection({ mode, chitchat });
|
|
434
480
|
const prefix = `${contractSection}${nativeCapabilitiesSection}${modePrompt}${customSection}${skillsSection}${subagentsSection}`;
|
|
435
481
|
return { prefix };
|
|
436
482
|
}
|
|
437
483
|
export function buildSystemPromptParts(cwd, mode, sandboxMode, planContext, subagents, sandboxSettings, providerId, resumeDigest, options) {
|
|
438
484
|
const chitchat = options?.chitchat === true;
|
|
439
485
|
const subAgent = options?.subAgent ?? false;
|
|
486
|
+
const toolTurn = options?.toolTurn === true;
|
|
440
487
|
const pid = providerId ?? "default";
|
|
441
488
|
// Subagents rarely change mid-session, but when they do we need a cache miss.
|
|
442
489
|
// JSON-stable stringify is fast for typical configs (< 10 entries, no circular refs).
|
|
443
490
|
const subagentsHash = subagents ? JSON.stringify(subagents) : "none";
|
|
444
491
|
// Try cache for the static prefix
|
|
445
|
-
const key = staticPrefixCacheKey(cwd, mode, pid, chitchat, subagentsHash, subAgent);
|
|
492
|
+
const key = staticPrefixCacheKey(cwd, mode, pid, chitchat, subagentsHash, subAgent, toolTurn);
|
|
446
493
|
const now = Date.now();
|
|
447
494
|
const cached = _staticPrefixCache.get(key);
|
|
448
495
|
let staticPrefix;
|
|
@@ -451,7 +498,7 @@ export function buildSystemPromptParts(cwd, mode, sandboxMode, planContext, suba
|
|
|
451
498
|
}
|
|
452
499
|
else {
|
|
453
500
|
// Cache miss — compute and store
|
|
454
|
-
const result = computeStaticPrefix(cwd, mode, subagents, pid, chitchat, subAgent);
|
|
501
|
+
const result = computeStaticPrefix(cwd, mode, subagents, pid, chitchat, subAgent, toolTurn);
|
|
455
502
|
staticPrefix = result.prefix;
|
|
456
503
|
_staticPrefixCache.set(key, {
|
|
457
504
|
prefix: staticPrefix,
|
|
@@ -64,6 +64,8 @@ export interface SubAgentCapState {
|
|
|
64
64
|
cumulative: number;
|
|
65
65
|
/** Configured ceiling. */
|
|
66
66
|
max: number;
|
|
67
|
+
/** Hard cap ceiling (e.g. max * 2). */
|
|
68
|
+
hardMax?: number;
|
|
67
69
|
/** True once `cumulative >= max` (sub-agent should wrap up). */
|
|
68
70
|
exhausted: boolean;
|
|
69
71
|
/** Number of duplicate-output detections (telemetry / tests). */
|
|
@@ -46,7 +46,9 @@ function shortHash(text) {
|
|
|
46
46
|
return createHash("sha1").update(text).digest("hex").slice(0, 12);
|
|
47
47
|
}
|
|
48
48
|
export function compressForCap(state, raw) {
|
|
49
|
-
|
|
49
|
+
const hardCeiling = state.hardMax ?? state.max;
|
|
50
|
+
if (state.exhausted || state.cumulative >= hardCeiling) {
|
|
51
|
+
state.exhausted = true;
|
|
50
52
|
return `[${state.label} tool budget exhausted (${state.cumulative}/${state.max} chars). Further tool calls will return this stub. Summarize findings now and return.]`;
|
|
51
53
|
}
|
|
52
54
|
state.callIndex += 1;
|
|
@@ -68,7 +70,11 @@ export function compressForCap(state, raw) {
|
|
|
68
70
|
}
|
|
69
71
|
const ratio = state.cumulative / state.max;
|
|
70
72
|
let out;
|
|
71
|
-
if (
|
|
73
|
+
if (state.cumulative >= state.max) {
|
|
74
|
+
const trimmed = trimHead(raw, state.highTierChars, state.label);
|
|
75
|
+
out = `${trimmed}\n\n[Warning: ${state.label} tool budget exceeded (${state.cumulative}/${state.max} chars). Please finalize your work and summarize findings now.]`;
|
|
76
|
+
}
|
|
77
|
+
else if (ratio >= state.highTierRatio) {
|
|
72
78
|
out = trimHead(raw, state.highTierChars, state.label);
|
|
73
79
|
}
|
|
74
80
|
else if (ratio >= state.midTierRatio) {
|
|
@@ -78,7 +84,7 @@ export function compressForCap(state, raw) {
|
|
|
78
84
|
out = raw;
|
|
79
85
|
}
|
|
80
86
|
state.cumulative += out.length;
|
|
81
|
-
if (state.cumulative >=
|
|
87
|
+
if (state.cumulative >= hardCeiling)
|
|
82
88
|
state.exhausted = true;
|
|
83
89
|
return out;
|
|
84
90
|
}
|
|
@@ -141,9 +147,11 @@ function wrapInternal(tools, state) {
|
|
|
141
147
|
* cumulative state.
|
|
142
148
|
*/
|
|
143
149
|
export function wrapToolSetWithCap(tools, opts = {}) {
|
|
150
|
+
const max = Math.max(20_000, opts.maxCumulativeChars ?? DEFAULT_MAX_CUMULATIVE_CHARS);
|
|
144
151
|
const state = {
|
|
145
152
|
cumulative: 0,
|
|
146
|
-
max
|
|
153
|
+
max,
|
|
154
|
+
hardMax: max * 2,
|
|
147
155
|
exhausted: false,
|
|
148
156
|
dedupHits: 0,
|
|
149
157
|
seenHashes: new Map(),
|
|
@@ -154,3 +154,10 @@ export declare function compactSubAgentMessages(messages: ReadonlyArray<ModelMes
|
|
|
154
154
|
* Creates a copy of the messages array and the last message to avoid mutating in-place.
|
|
155
155
|
*/
|
|
156
156
|
export declare function applyAnthropicPromptCaching(messages: readonly ModelMessage[], modelId: string): ModelMessage[];
|
|
157
|
+
/**
|
|
158
|
+
* Safely slice message history to keep at most `maxMessages` messages.
|
|
159
|
+
* Preserves the system message(s) at the front and ensures that the slice
|
|
160
|
+
* starts with a "user" message and does not split assistant tool calls and
|
|
161
|
+
* corresponding tool results.
|
|
162
|
+
*/
|
|
163
|
+
export declare function sliceMessageHistory(messages: ReadonlyArray<ModelMessage>, maxMessages?: number): ModelMessage[];
|
|
@@ -326,22 +326,25 @@ function rewriteOlderToolMessage(msg, previewChars, label, keepToolIds, persistA
|
|
|
326
326
|
export function compactSubAgentMessages(messages, opts = {}) {
|
|
327
327
|
const resolved = resolveOpts(opts);
|
|
328
328
|
const { outputPreviewChars, label, envelopeChars } = resolved;
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
|
|
329
|
+
// Step 4: Hard-limit message history sent to the model to prevent token bloating
|
|
330
|
+
// When input (messages + envelope) exceeds 50K characters and messages array is > 30,
|
|
331
|
+
// we slice the history to keep at most 30 messages (preserving system and user start).
|
|
332
|
+
let processedMessages = messages;
|
|
333
333
|
const messagesTotal = cumulativeMessageChars(messages);
|
|
334
334
|
const total = messagesTotal + envelopeChars;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
335
|
+
if (total > 50_000 && messages.length > 30) {
|
|
336
|
+
processedMessages = sliceMessageHistory(messages, 30);
|
|
337
|
+
}
|
|
338
|
+
// Calculate effective thresholds and keep last turns using the processed messages
|
|
339
|
+
const processedMessagesTotal = cumulativeMessageChars(processedMessages);
|
|
340
|
+
const processedTotal = processedMessagesTotal + envelopeChars;
|
|
341
|
+
const { effectiveThresholdChars, effectiveKeepLastTurns } = computeDynamicParams(processedTotal, resolved);
|
|
339
342
|
// No-op: return the input BY REFERENCE (contract above) so `compacted === input`.
|
|
340
|
-
if (
|
|
341
|
-
return
|
|
342
|
-
const keepFrom = findKeepFromIndex(
|
|
343
|
+
if (processedTotal < effectiveThresholdChars)
|
|
344
|
+
return processedMessages;
|
|
345
|
+
const keepFrom = findKeepFromIndex(processedMessages, effectiveKeepLastTurns);
|
|
343
346
|
if (keepFrom <= 0)
|
|
344
|
-
return
|
|
347
|
+
return processedMessages;
|
|
345
348
|
// Walk older messages; rewrite fresh tool results into stubs, super-shrink
|
|
346
349
|
// already-stubbed results (F1), and strip args off older assistant
|
|
347
350
|
// tool-call shells (F1). The 1:1 assistant↔tool pairing required by the AI
|
|
@@ -349,8 +352,8 @@ export function compactSubAgentMessages(messages, opts = {}) {
|
|
|
349
352
|
// structure or count.
|
|
350
353
|
let firstUserSeen = false;
|
|
351
354
|
const out = [];
|
|
352
|
-
for (let i = 0; i <
|
|
353
|
-
const msg =
|
|
355
|
+
for (let i = 0; i < processedMessages.length; i++) {
|
|
356
|
+
const msg = processedMessages[i];
|
|
354
357
|
if (i >= keepFrom) {
|
|
355
358
|
out.push(msg);
|
|
356
359
|
continue;
|
|
@@ -497,4 +500,52 @@ export function applyAnthropicPromptCaching(messages, modelId) {
|
|
|
497
500
|
newMessages[lastIndex] = lastMsg;
|
|
498
501
|
return newMessages;
|
|
499
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* Safely slice message history to keep at most `maxMessages` messages.
|
|
505
|
+
* Preserves the system message(s) at the front and ensures that the slice
|
|
506
|
+
* starts with a "user" message and does not split assistant tool calls and
|
|
507
|
+
* corresponding tool results.
|
|
508
|
+
*/
|
|
509
|
+
export function sliceMessageHistory(messages, maxMessages = 30) {
|
|
510
|
+
if (messages.length <= maxMessages)
|
|
511
|
+
return messages;
|
|
512
|
+
// Find all user message indices (excluding system messages)
|
|
513
|
+
const userIndices = [];
|
|
514
|
+
for (let idx = 0; idx < messages.length; idx++) {
|
|
515
|
+
if (messages[idx]?.role === "user") {
|
|
516
|
+
userIndices.push(idx);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (userIndices.length === 0) {
|
|
520
|
+
return messages;
|
|
521
|
+
}
|
|
522
|
+
// Group messages into turns.
|
|
523
|
+
// Each turn is a range [start, end] inclusive.
|
|
524
|
+
const turns = [];
|
|
525
|
+
for (let idx = 0; idx < userIndices.length; idx++) {
|
|
526
|
+
const start = userIndices[idx];
|
|
527
|
+
const end = idx + 1 < userIndices.length ? userIndices[idx + 1] - 1 : messages.length - 1;
|
|
528
|
+
turns.push({ start, end });
|
|
529
|
+
}
|
|
530
|
+
// Accumulate turns from the end, up to maxMessages
|
|
531
|
+
let keptMessagesCount = 0;
|
|
532
|
+
let keepFromIndex = -1;
|
|
533
|
+
for (let idx = turns.length - 1; idx >= 0; idx--) {
|
|
534
|
+
const turn = turns[idx];
|
|
535
|
+
const turnLength = turn.end - turn.start + 1;
|
|
536
|
+
if (keptMessagesCount === 0 || keptMessagesCount + turnLength <= maxMessages) {
|
|
537
|
+
keptMessagesCount += turnLength;
|
|
538
|
+
keepFromIndex = turn.start;
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (keepFromIndex === -1) {
|
|
545
|
+
return messages;
|
|
546
|
+
}
|
|
547
|
+
const kept = messages.slice(keepFromIndex);
|
|
548
|
+
const systemMessages = messages.filter((m) => m.role === "system");
|
|
549
|
+
return [...systemMessages, ...kept];
|
|
550
|
+
}
|
|
500
551
|
//# sourceMappingURL=subagent-compactor.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
-
import { compactSubAgentMessages, cumulativeMessageChars } from "./subagent-compactor.js";
|
|
2
|
+
import { compactSubAgentMessages, cumulativeMessageChars, sliceMessageHistory } from "./subagent-compactor.js";
|
|
3
3
|
function bigText(label, kb) {
|
|
4
4
|
const block = `${label}:${"x".repeat(kb * 1000)}`;
|
|
5
5
|
return block;
|
|
@@ -400,7 +400,14 @@ describe("subagent-compactor: compactSubAgentMessages", () => {
|
|
|
400
400
|
},
|
|
401
401
|
{
|
|
402
402
|
role: "tool",
|
|
403
|
-
content: [
|
|
403
|
+
content: [
|
|
404
|
+
{
|
|
405
|
+
type: "tool-result",
|
|
406
|
+
toolCallId: "call_low",
|
|
407
|
+
toolName: "read_file",
|
|
408
|
+
output: { type: "text", value: lowValueOutput },
|
|
409
|
+
},
|
|
410
|
+
],
|
|
404
411
|
},
|
|
405
412
|
{
|
|
406
413
|
role: "assistant",
|
|
@@ -408,7 +415,14 @@ describe("subagent-compactor: compactSubAgentMessages", () => {
|
|
|
408
415
|
},
|
|
409
416
|
{
|
|
410
417
|
role: "tool",
|
|
411
|
-
content: [
|
|
418
|
+
content: [
|
|
419
|
+
{
|
|
420
|
+
type: "tool-result",
|
|
421
|
+
toolCallId: "call_err",
|
|
422
|
+
toolName: "read_file",
|
|
423
|
+
output: { type: "text", value: highValueOutputWithError },
|
|
424
|
+
},
|
|
425
|
+
],
|
|
412
426
|
},
|
|
413
427
|
{
|
|
414
428
|
role: "assistant",
|
|
@@ -416,7 +430,14 @@ describe("subagent-compactor: compactSubAgentMessages", () => {
|
|
|
416
430
|
},
|
|
417
431
|
{
|
|
418
432
|
role: "tool",
|
|
419
|
-
content: [
|
|
433
|
+
content: [
|
|
434
|
+
{
|
|
435
|
+
type: "tool-result",
|
|
436
|
+
toolCallId: "call_src",
|
|
437
|
+
toolName: "read_file",
|
|
438
|
+
output: { type: "text", value: highValueSourceOutput },
|
|
439
|
+
},
|
|
440
|
+
],
|
|
420
441
|
},
|
|
421
442
|
// Add a couple of low-value turns to push past threshold and keepLast turns (keepLast=1)
|
|
422
443
|
...toolTurn(9, 10),
|
|
@@ -425,13 +446,13 @@ describe("subagent-compactor: compactSubAgentMessages", () => {
|
|
|
425
446
|
msgs[msgs.length - 1].content[0].toolName = "other_tool";
|
|
426
447
|
const out = compactSubAgentMessages(msgs, { thresholdChars: 10_000, keepLastTurns: 1 });
|
|
427
448
|
// low-value large read_file MUST be elided
|
|
428
|
-
const lowValMsg = out.find(m => m.role === "tool" && Array.isArray(m.content) && m.content[0]?.toolCallId === "call_low");
|
|
449
|
+
const lowValMsg = out.find((m) => m.role === "tool" && Array.isArray(m.content) && m.content[0]?.toolCallId === "call_low");
|
|
429
450
|
expect((lowValMsg?.content)[0].output.value).toMatch(/elided by sub-agent compactor/);
|
|
430
451
|
// high-value large error read_file MUST NOT be elided
|
|
431
|
-
const errValMsg = out.find(m => m.role === "tool" && Array.isArray(m.content) && m.content[0]?.toolCallId === "call_err");
|
|
452
|
+
const errValMsg = out.find((m) => m.role === "tool" && Array.isArray(m.content) && m.content[0]?.toolCallId === "call_err");
|
|
432
453
|
expect((errValMsg?.content)[0].output.value).toBe(highValueOutputWithError);
|
|
433
454
|
// high-value small source read_file MUST NOT be elided
|
|
434
|
-
const srcValMsg = out.find(m => m.role === "tool" && Array.isArray(m.content) && m.content[0]?.toolCallId === "call_src");
|
|
455
|
+
const srcValMsg = out.find((m) => m.role === "tool" && Array.isArray(m.content) && m.content[0]?.toolCallId === "call_src");
|
|
435
456
|
expect((srcValMsg?.content)[0].output.value).toBe(highValueSourceOutput);
|
|
436
457
|
});
|
|
437
458
|
it("T1.1 — strips reasoning parts from old assistant turns when stripOldReasoning is enabled", () => {
|
|
@@ -453,7 +474,12 @@ describe("subagent-compactor: compactSubAgentMessages", () => {
|
|
|
453
474
|
msgs.push({
|
|
454
475
|
role: "tool",
|
|
455
476
|
content: [
|
|
456
|
-
{
|
|
477
|
+
{
|
|
478
|
+
type: "tool-result",
|
|
479
|
+
toolCallId: `call_${i}`,
|
|
480
|
+
toolName: "bash",
|
|
481
|
+
output: { type: "text", value: bigText(`R${i}`, 2) },
|
|
482
|
+
},
|
|
457
483
|
],
|
|
458
484
|
});
|
|
459
485
|
}
|
|
@@ -497,7 +523,12 @@ describe("subagent-compactor: compactSubAgentMessages", () => {
|
|
|
497
523
|
msgs.push({
|
|
498
524
|
role: "tool",
|
|
499
525
|
content: [
|
|
500
|
-
{
|
|
526
|
+
{
|
|
527
|
+
type: "tool-result",
|
|
528
|
+
toolCallId: `call_${i}`,
|
|
529
|
+
toolName: "bash",
|
|
530
|
+
output: { type: "text", value: bigText(`R${i}`, 2) },
|
|
531
|
+
},
|
|
501
532
|
],
|
|
502
533
|
});
|
|
503
534
|
}
|
|
@@ -516,5 +547,90 @@ describe("subagent-compactor: compactSubAgentMessages", () => {
|
|
|
516
547
|
expect(hasReasoning).toBe(true);
|
|
517
548
|
}
|
|
518
549
|
});
|
|
550
|
+
describe("sliceMessageHistory & Step 4 truncation", () => {
|
|
551
|
+
it("preserves system prompt and user start while slicing excess messages", () => {
|
|
552
|
+
const msgs = [
|
|
553
|
+
{ role: "system", content: "system prompt" },
|
|
554
|
+
{ role: "user", content: "first prompt" },
|
|
555
|
+
{ role: "assistant", content: "first answer" },
|
|
556
|
+
{ role: "user", content: "second prompt" },
|
|
557
|
+
{
|
|
558
|
+
role: "assistant",
|
|
559
|
+
content: [{ type: "tool-call", toolCallId: "call-1", toolName: "bash", input: "ls" }],
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
role: "tool",
|
|
563
|
+
content: [{ type: "tool-result", toolCallId: "call-1", toolName: "bash", output: "file1" }],
|
|
564
|
+
},
|
|
565
|
+
{ role: "assistant", content: "second answer" },
|
|
566
|
+
{ role: "user", content: "third prompt" },
|
|
567
|
+
{
|
|
568
|
+
role: "assistant",
|
|
569
|
+
content: [{ type: "tool-call", toolCallId: "call-2", toolName: "bash", input: "pwd" }],
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
role: "tool",
|
|
573
|
+
content: [{ type: "tool-result", toolCallId: "call-2", toolName: "bash", output: "/dir" }],
|
|
574
|
+
},
|
|
575
|
+
{ role: "assistant", content: "third answer" },
|
|
576
|
+
];
|
|
577
|
+
// Slicing to max 5 messages should start cleanly at a user message
|
|
578
|
+
const sliced = sliceMessageHistory(msgs, 5);
|
|
579
|
+
expect(sliced[0].role).toBe("system");
|
|
580
|
+
expect(sliced[1].role).toBe("user");
|
|
581
|
+
// Slicing walked backward to find a user message start that satisfies max 5.
|
|
582
|
+
// Last messages:
|
|
583
|
+
// assistant: third answer (idx 10) -> tool result 2 (idx 9) -> assistant call 2 (idx 8) -> user third prompt (idx 7).
|
|
584
|
+
// That is 4 messages. Since kept must start with user, it stops at idx 7.
|
|
585
|
+
// So kept messages are from idx 7 onwards: [user third prompt, assistant call 2, tool result 2, assistant third answer]
|
|
586
|
+
expect(sliced).toHaveLength(5); // 1 system + 4 kept messages
|
|
587
|
+
expect(sliced[1].content).toBe("third prompt");
|
|
588
|
+
});
|
|
589
|
+
it("does not orphaned tool-calls and tool-results", () => {
|
|
590
|
+
const msgs = [
|
|
591
|
+
{ role: "system", content: "system prompt" },
|
|
592
|
+
{ role: "user", content: "prompt" },
|
|
593
|
+
{
|
|
594
|
+
role: "assistant",
|
|
595
|
+
content: [{ type: "tool-call", toolCallId: "call-99", toolName: "bash", input: "whoami" }],
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
role: "tool",
|
|
599
|
+
content: [{ type: "tool-result", toolCallId: "call-99", toolName: "bash", output: "root" }],
|
|
600
|
+
},
|
|
601
|
+
{ role: "assistant", content: "hello" },
|
|
602
|
+
];
|
|
603
|
+
// Even if maxMessages is 2, it shouldn't split assistant call-99 and tool result-99.
|
|
604
|
+
// Walking backward:
|
|
605
|
+
// - assistant: hello (count 1)
|
|
606
|
+
// - tool: result-99 (count 2, pendingToolCalls = {call-99})
|
|
607
|
+
// - assistant: call-99 (count 3, pendingToolCalls = {})
|
|
608
|
+
// - user: prompt (count 4, clean user start)
|
|
609
|
+
// So it must keep 4 messages + 1 system = 5 messages.
|
|
610
|
+
const sliced = sliceMessageHistory(msgs, 2);
|
|
611
|
+
expect(sliced).toHaveLength(5);
|
|
612
|
+
expect(sliced[1].role).toBe("user");
|
|
613
|
+
});
|
|
614
|
+
it("triggers slicing in compactSubAgentMessages when total exceeds 50K and messages count is > 30", () => {
|
|
615
|
+
const msgs = [
|
|
616
|
+
{ role: "system", content: "system" },
|
|
617
|
+
{ role: "user", content: "start" },
|
|
618
|
+
];
|
|
619
|
+
// Push 40 messages to exceed 30 messages limit
|
|
620
|
+
for (let i = 0; i < 20; i++) {
|
|
621
|
+
msgs.push({ role: "assistant", content: `helper ${i}` });
|
|
622
|
+
msgs.push({ role: "user", content: `next question ${i}` });
|
|
623
|
+
}
|
|
624
|
+
// Generate a massive envelope size or message content to exceed 50K total chars
|
|
625
|
+
const out = compactSubAgentMessages(msgs, {
|
|
626
|
+
envelopeChars: 60_000, // force exceeds 50K
|
|
627
|
+
thresholdChars: 200_000, // high threshold to prevent compaction from running on top
|
|
628
|
+
});
|
|
629
|
+
// The message history should have been sliced (will be much fewer than 42 messages)
|
|
630
|
+
expect(out.length).toBeLessThan(msgs.length);
|
|
631
|
+
expect(out[0].role).toBe("system");
|
|
632
|
+
expect(out[1].role).toBe("user");
|
|
633
|
+
});
|
|
634
|
+
});
|
|
519
635
|
});
|
|
520
636
|
//# sourceMappingURL=subagent-compactor.spec.js.map
|
|
@@ -21,6 +21,7 @@ import type { TurnRunnerDepsBase } from "./turn-runner-deps.js";
|
|
|
21
21
|
* subsequent turns). Method callbacks delegate to Agent private methods.
|
|
22
22
|
*/
|
|
23
23
|
export interface MessageProcessorDeps extends TurnRunnerDepsBase {
|
|
24
|
+
readonly isSubSession?: boolean;
|
|
24
25
|
/** Live messageSeqs array (mutated by push; parallel to messages). */
|
|
25
26
|
readonly messageSeqs: Array<number | null>;
|
|
26
27
|
/** Session bookkeeping. */
|
|
@@ -75,6 +76,7 @@ export interface MessageProcessorDeps extends TurnRunnerDepsBase {
|
|
|
75
76
|
setMessages(messages: ModelMessage[]): void;
|
|
76
77
|
requireProvider(): LegacyProvider;
|
|
77
78
|
emitSubagentStatus(status: SubagentStatus | null): void;
|
|
79
|
+
consultParentSession?: (question: string) => Promise<string>;
|
|
78
80
|
fireHook(input: unknown, signal?: AbortSignal): Promise<{
|
|
79
81
|
blocked: boolean;
|
|
80
82
|
blockingErrors: Array<{
|