muonroi-cli 1.6.1 → 1.6.2
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/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/orchestrator/subagent-compactor.d.ts +10 -0
- package/dist/src/orchestrator/subagent-compactor.js +14 -0
- package/dist/src/orchestrator/subagent-compactor.spec.js +54 -0
- package/dist/src/storage/tool-results.js +23 -0
- package/dist/src/storage/tool-results.test.d.ts +1 -0
- package/dist/src/storage/tool-results.test.js +48 -0
- package/dist/src/ui/__tests__/markdown-render.test.js +17 -0
- package/dist/src/ui/markdown-render.js +12 -0
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const PACKAGE_VERSION = "1.6.
|
|
1
|
+
export declare const PACKAGE_VERSION = "1.6.2";
|
|
2
2
|
export declare const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// AUTO-GENERATED by scripts/sync-version.cjs. DO NOT EDIT BY HAND.
|
|
2
2
|
// Sourced from package.json at build time so it survives bun --compile bundling.
|
|
3
|
-
export const PACKAGE_VERSION = "1.6.
|
|
3
|
+
export const PACKAGE_VERSION = "1.6.2";
|
|
4
4
|
export const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
|
|
5
5
|
//# sourceMappingURL=version.js.map
|
|
@@ -111,6 +111,16 @@ export declare const SUBAGENT_COMPACT_DEFAULT_KEEP_LAST = 3;
|
|
|
111
111
|
* the native contract + native-capabilities tell the agent to rely on for "task finished?" and
|
|
112
112
|
* rehydrate during long meta conversations about CLI/PIL/compaction/EE. */
|
|
113
113
|
export declare const IMPORTANT_TOOL_NAMES: readonly ["read_file", "grep", "lsp", "bash", "ee_query", "usage_forensics", "selfverify_start", "selfverify_result", "selfverify_status"];
|
|
114
|
+
/**
|
|
115
|
+
* MCP tool prefixes whose results are an AUTHORITATIVE source the agent is
|
|
116
|
+
* explicitly steered to fetch FIRST and ground on (the ECOSYSTEM_DOCS_NUDGE in
|
|
117
|
+
* src/gsd/directives.ts). Eliding them defeats the nudge — the agent calls the
|
|
118
|
+
* ecosystem docs, then compaction discards them and it goes blind on the very
|
|
119
|
+
* source it was told to trust (session 584ba476c07a: mcp_muonroi-docs__setup_guide
|
|
120
|
+
* + bb_recipe_list elided, ee_unavailable, 0 rehydrated → "partially blind").
|
|
121
|
+
* Keep their results verbatim so the agent stays grounded across the session.
|
|
122
|
+
*/
|
|
123
|
+
export declare const HIGH_VALUE_MCP_PREFIXES: readonly ["mcp_muonroi-docs__"];
|
|
114
124
|
/**
|
|
115
125
|
* Heuristic: keep full (no stub) for high-signal tool results.
|
|
116
126
|
* Signals: allowlist tool + (error/todo/plan/keyfile/large output or explicit keep list).
|
|
@@ -73,6 +73,16 @@ export const IMPORTANT_TOOL_NAMES = [
|
|
|
73
73
|
"selfverify_result",
|
|
74
74
|
"selfverify_status",
|
|
75
75
|
];
|
|
76
|
+
/**
|
|
77
|
+
* MCP tool prefixes whose results are an AUTHORITATIVE source the agent is
|
|
78
|
+
* explicitly steered to fetch FIRST and ground on (the ECOSYSTEM_DOCS_NUDGE in
|
|
79
|
+
* src/gsd/directives.ts). Eliding them defeats the nudge — the agent calls the
|
|
80
|
+
* ecosystem docs, then compaction discards them and it goes blind on the very
|
|
81
|
+
* source it was told to trust (session 584ba476c07a: mcp_muonroi-docs__setup_guide
|
|
82
|
+
* + bb_recipe_list elided, ee_unavailable, 0 rehydrated → "partially blind").
|
|
83
|
+
* Keep their results verbatim so the agent stays grounded across the session.
|
|
84
|
+
*/
|
|
85
|
+
export const HIGH_VALUE_MCP_PREFIXES = ["mcp_muonroi-docs__"];
|
|
76
86
|
/**
|
|
77
87
|
* Heuristic: keep full (no stub) for high-signal tool results.
|
|
78
88
|
* Signals: allowlist tool + (error/todo/plan/keyfile/large output or explicit keep list).
|
|
@@ -86,6 +96,10 @@ export function isHighValueToolResult(toolName, preview, explicitKeepIds, toolCa
|
|
|
86
96
|
// work/findings. Truncating it causes the agent to think it lost its answer.
|
|
87
97
|
if (name.startsWith("respond_"))
|
|
88
98
|
return true;
|
|
99
|
+
// Authoritative ecosystem-docs MCP results: the agent is nudged to fetch these
|
|
100
|
+
// FIRST, so eliding them strands it (session 584ba476c07a). Keep verbatim.
|
|
101
|
+
if (HIGH_VALUE_MCP_PREFIXES.some((p) => name.startsWith(p)))
|
|
102
|
+
return true;
|
|
89
103
|
if (IMPORTANT_TOOL_NAMES.includes(name)) {
|
|
90
104
|
const p = preview.toLowerCase();
|
|
91
105
|
if (/error|fail|todo|plan|done|✔|blocked|critical/.test(p))
|
|
@@ -110,6 +110,60 @@ describe("subagent-compactor: compactSubAgentMessages", () => {
|
|
|
110
110
|
expect(out[out.length - i]).toBe(msgs[msgs.length - i]);
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
|
+
it("keeps an OLDER authoritative muonroi-docs MCP result verbatim while eliding low-value peers (session 584ba476c07a)", () => {
|
|
114
|
+
// History: an early muonroi-docs setup_guide (older than keepLast=3) + many
|
|
115
|
+
// low-value tool turns. The ecosystem doc must survive compaction so the
|
|
116
|
+
// agent stays grounded on the source it was nudged to fetch first.
|
|
117
|
+
const docsValue = bigText("ECOSYSTEM_DOCS", 6); // ~6kb authoritative payload
|
|
118
|
+
const msgs = [
|
|
119
|
+
{ role: "system", content: "You are the agent." },
|
|
120
|
+
{ role: "user", content: "ecosystem question" },
|
|
121
|
+
{
|
|
122
|
+
role: "assistant",
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: "tool-call",
|
|
126
|
+
toolCallId: "call_docs",
|
|
127
|
+
toolName: "mcp_muonroi-docs__setup_guide",
|
|
128
|
+
input: JSON.stringify({ component: "ecosystem" }),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
role: "tool",
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: "tool-result",
|
|
137
|
+
toolCallId: "call_docs",
|
|
138
|
+
toolName: "mcp_muonroi-docs__setup_guide",
|
|
139
|
+
output: { type: "text", value: docsValue },
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
// Pile on low-value turns to push well past threshold and make the docs turn "old".
|
|
145
|
+
for (let i = 1; i <= 10; i++) {
|
|
146
|
+
const t = toolTurn(i, 10);
|
|
147
|
+
t[1].content[0].toolName = "mcp_filesystem__list_directory"; // low-value MCP
|
|
148
|
+
t[0].content[0].toolName = "mcp_filesystem__list_directory";
|
|
149
|
+
msgs.push(...t);
|
|
150
|
+
}
|
|
151
|
+
const out = compactSubAgentMessages(msgs);
|
|
152
|
+
expect(out).not.toBe(msgs); // compaction fired
|
|
153
|
+
// The muonroi-docs result is kept verbatim (full payload, no stub).
|
|
154
|
+
const docsMsg = out.find((m) => m.role === "tool" &&
|
|
155
|
+
Array.isArray(m.content) &&
|
|
156
|
+
m.content[0]?.toolName === "mcp_muonroi-docs__setup_guide");
|
|
157
|
+
const docsOut = (docsMsg?.content)[0].output.value;
|
|
158
|
+
expect(docsOut).toBe(docsValue);
|
|
159
|
+
expect(docsOut).not.toMatch(/elided by/);
|
|
160
|
+
// A low-value filesystem MCP peer from an OLD turn IS stubbed.
|
|
161
|
+
const stubbed = out.some((m) => m.role === "tool" &&
|
|
162
|
+
Array.isArray(m.content) &&
|
|
163
|
+
typeof m.content[0]?.output?.value === "string" &&
|
|
164
|
+
m.content[0].output.value.includes("elided by"));
|
|
165
|
+
expect(stubbed).toBe(true);
|
|
166
|
+
});
|
|
113
167
|
it("rewrites older tool-result parts with elision stub", () => {
|
|
114
168
|
const msgs = buildHistory(10, 10);
|
|
115
169
|
// Neutralize tool so the basic elision test is not affected by high-value auto-keep (idea 1).
|
|
@@ -31,6 +31,29 @@ export function extractToolResultFromOutput(output) {
|
|
|
31
31
|
output: String(output.value),
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
// MCP tool results: `{ type: "content", value: [{ type: "text", text }, ...] }`
|
|
35
|
+
// (see cap-tool-result.ts). Before this branch, extraction returned null, so
|
|
36
|
+
// persisted output_json was the raw envelope with NO `success` field — on
|
|
37
|
+
// reload the renderer read `toolResult.success` as undefined and displayed
|
|
38
|
+
// "Error" for a SUCCESSFUL call (session 63f2d542b772: 50 muonroi-docs calls,
|
|
39
|
+
// 0 DB failures, all shown as "Error"). Flatten the text parts so it round-
|
|
40
|
+
// trips as a real ToolResult. A genuinely failed MCP call throws → the SDK
|
|
41
|
+
// records an `error-text` part, handled above, so content == success here.
|
|
42
|
+
if ("type" in output && output.type === "content" && "value" in output && Array.isArray(output.value)) {
|
|
43
|
+
const parts = output.value;
|
|
44
|
+
const text = parts
|
|
45
|
+
.filter((p) => !!p &&
|
|
46
|
+
typeof p === "object" &&
|
|
47
|
+
p.type === "text" &&
|
|
48
|
+
typeof p.text === "string")
|
|
49
|
+
.map((p) => p.text)
|
|
50
|
+
.join("\n");
|
|
51
|
+
const nonText = parts.length - parts.filter((p) => p?.type === "text").length;
|
|
52
|
+
return {
|
|
53
|
+
success: true,
|
|
54
|
+
output: text || (nonText > 0 ? `[${nonText} non-text MCP part(s)]` : "(empty MCP result)"),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
34
57
|
return null;
|
|
35
58
|
}
|
|
36
59
|
export function getOutputKind(output) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { extractToolResultFromOutput, isOutputSuccess } from "./tool-results.js";
|
|
3
|
+
describe("extractToolResultFromOutput", () => {
|
|
4
|
+
it("passes through a native ToolResult shape", () => {
|
|
5
|
+
const r = extractToolResultFromOutput({ success: true, output: "hi" });
|
|
6
|
+
expect(r).toMatchObject({ success: true, output: "hi" });
|
|
7
|
+
});
|
|
8
|
+
it("treats error-text as a failure", () => {
|
|
9
|
+
const r = extractToolResultFromOutput({ type: "error-text", value: "boom" });
|
|
10
|
+
expect(r).toMatchObject({ success: false, error: "boom" });
|
|
11
|
+
});
|
|
12
|
+
it("flattens an MCP content envelope into a successful ToolResult (session 63f2d542b772)", () => {
|
|
13
|
+
// MCP tools return { type: "content", value: [{ type: "text", text }] }.
|
|
14
|
+
// Before the fix this returned null, so the persisted output_json had no
|
|
15
|
+
// `success` field and the renderer showed "Error" for a successful call.
|
|
16
|
+
const out = {
|
|
17
|
+
type: "content",
|
|
18
|
+
value: [
|
|
19
|
+
{ type: "text", text: "package list" },
|
|
20
|
+
{ type: "text", text: "more" },
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
const r = extractToolResultFromOutput(out);
|
|
24
|
+
expect(r).toMatchObject({ success: true, output: "package list\nmore" });
|
|
25
|
+
});
|
|
26
|
+
it("round-trips through JSON so the renderer reads success=true (the actual bug)", () => {
|
|
27
|
+
// transcript.loadStoredToolResults does JSON.parse(output_json) and the
|
|
28
|
+
// renderer reads `.success`. Simulate persist→load and assert it is NOT
|
|
29
|
+
// misread as an error.
|
|
30
|
+
const mcpOutput = { type: "content", value: [{ type: "text", text: "## Muonroi.Core\nNuGet: Muonroi.Core" }] };
|
|
31
|
+
const persisted = JSON.stringify(extractToolResultFromOutput(mcpOutput));
|
|
32
|
+
const loaded = JSON.parse(persisted);
|
|
33
|
+
const rendered = loaded.success ? loaded.output || "Success" : loaded.error || "Error";
|
|
34
|
+
expect(rendered).toContain("Muonroi.Core");
|
|
35
|
+
expect(rendered).not.toBe("Error");
|
|
36
|
+
});
|
|
37
|
+
it("describes a non-text-only MCP content result instead of dropping to Error", () => {
|
|
38
|
+
const out = { type: "content", value: [{ type: "image", data: "..." }] };
|
|
39
|
+
const r = extractToolResultFromOutput(out);
|
|
40
|
+
expect(r?.success).toBe(true);
|
|
41
|
+
expect(r?.output).toMatch(/non-text MCP part/);
|
|
42
|
+
});
|
|
43
|
+
it("isOutputSuccess still treats content envelopes as success", () => {
|
|
44
|
+
expect(isOutputSuccess({ type: "content", value: [] })).toBe(true);
|
|
45
|
+
expect(isOutputSuccess({ type: "error-text", value: "x" })).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
//# sourceMappingURL=tool-results.test.js.map
|
|
@@ -39,6 +39,23 @@ describe("parseInline — marker concealment", () => {
|
|
|
39
39
|
expect(text(parseInline("a **partial answer", t))).toBe("a **partial answer");
|
|
40
40
|
expect(text(parseInline("trailing `code", t))).toBe("trailing `code");
|
|
41
41
|
});
|
|
42
|
+
it("does NOT treat intra-word underscores as emphasis (identifiers stay intact)", () => {
|
|
43
|
+
// Session 584ba476c07a rendered `mcp_filesystem__list_directory` as
|
|
44
|
+
// "mcpfilesystemlistdirectory" — underscores eaten as italic/bold.
|
|
45
|
+
expect(text(parseInline("mcp_filesystem__list_directory", t))).toBe("mcp_filesystem__list_directory");
|
|
46
|
+
expect(text(parseInline("a snake_case name", t))).toBe("a snake_case name");
|
|
47
|
+
expect(text(parseInline("call mcp_muonroi-docs__setup_guide first", t))).toBe("call mcp_muonroi-docs__setup_guide first");
|
|
48
|
+
// None of these should be emphasized.
|
|
49
|
+
expect(parseInline("mcp_filesystem__list_directory", t).some((s) => s.italic || s.bold)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
it("still emphasizes underscores at word boundaries", () => {
|
|
52
|
+
expect(text(parseInline("an _italic_ word", t))).toBe("an italic word");
|
|
53
|
+
expect(parseInline("an _italic_ word", t).find((s) => s.text === "italic")?.italic).toBe(true);
|
|
54
|
+
expect(text(parseInline("a __bold__ word", t))).toBe("a bold word");
|
|
55
|
+
expect(parseInline("a __bold__ word", t).find((s) => s.text === "bold")?.bold).toBe(true);
|
|
56
|
+
// Underscore emphasis adjacent to punctuation still works.
|
|
57
|
+
expect(parseInline("(_em_)", t).find((s) => s.text === "em")?.italic).toBe(true);
|
|
58
|
+
});
|
|
42
59
|
it("never leaves ** ` ### markers in styled segments", () => {
|
|
43
60
|
const sample = "**A** and `b` and ***c*** and [d](http://e) and ~~f~~";
|
|
44
61
|
const out = text(parseInline(sample, t));
|
|
@@ -77,6 +77,18 @@ export function parseInline(line, t, base = {}) {
|
|
|
77
77
|
const inner = line.slice(i + m.open.length, close);
|
|
78
78
|
if (inner.length === 0)
|
|
79
79
|
continue;
|
|
80
|
+
// CommonMark: underscore does NOT open/close emphasis intra-word, so
|
|
81
|
+
// identifiers like `mcp_filesystem__list_directory` or `snake_case` keep
|
|
82
|
+
// their underscores instead of being eaten as italic/bold (session
|
|
83
|
+
// 584ba476c07a rendered "mcpfilesystemlistdirectory"). Asterisk markers
|
|
84
|
+
// keep intraword behaviour. Reject when a word char hugs the marker on the
|
|
85
|
+
// word-internal side.
|
|
86
|
+
if (m.open[0] === "_") {
|
|
87
|
+
const before = i > 0 ? line[i - 1] : "";
|
|
88
|
+
const after = line[close + m.close.length] ?? "";
|
|
89
|
+
if (/[A-Za-z0-9]/.test(before) || /[A-Za-z0-9]/.test(after))
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
80
92
|
flushPlain(i);
|
|
81
93
|
const isBold = "bold" in m && m.bold;
|
|
82
94
|
const isItalic = "italic" in m && m.italic;
|