mu-harness 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/commands/defaults.js +42 -34
- package/esm/session/agent-session.js +1 -0
- package/esm/session/manager.js +1 -0
- package/esm/session/persist.js +1 -0
- package/esm/session/types.d.ts +1 -0
- package/package.json +3 -3
- package/script/commands/defaults.js +42 -34
- package/script/session/agent-session.js +1 -0
- package/script/session/manager.js +1 -0
- package/script/session/persist.js +1 -0
- package/script/session/types.d.ts +1 -0
package/esm/commands/defaults.js
CHANGED
|
@@ -44,13 +44,16 @@ const estTokens = (chars) => Math.max(1, Math.round(chars / 4));
|
|
|
44
44
|
const GRID_COLS = 20;
|
|
45
45
|
const GRID_ROWS = 10;
|
|
46
46
|
const GRID_CELLS = GRID_COLS * GRID_ROWS;
|
|
47
|
-
const
|
|
48
|
-
const
|
|
47
|
+
const GRID_WIDTH = GRID_COLS * 2 - 1; // glyphs joined by single spaces
|
|
48
|
+
const USED = '⛁'; // a filled context cell (Claude Code's /context glyph)
|
|
49
|
+
const FREEG = '⛶'; // a free cell
|
|
49
50
|
const BUFFER_COLOR = '\x1b[93m'; // bright yellow — the compaction reserve
|
|
50
|
-
// ANSI SGR
|
|
51
|
+
// ANSI SGR codes — mu's TUI text utils are ANSI-aware so these render in the terminal;
|
|
51
52
|
// the companion strips them (plain text), keeping the labelled breakdown readable.
|
|
52
53
|
const RESET = '\x1b[0m';
|
|
53
54
|
const DIM = '\x1b[2m';
|
|
55
|
+
const BOLD = '\x1b[1m';
|
|
56
|
+
const ITALIC_DIM = '\x1b[2;3m';
|
|
54
57
|
const paint = (s, color) => `${color}${s}${RESET}`;
|
|
55
58
|
const fmtTok = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`);
|
|
56
59
|
const pctStr = (n, w) => {
|
|
@@ -59,7 +62,6 @@ const pctStr = (n, w) => {
|
|
|
59
62
|
const p = (n / w) * 100;
|
|
60
63
|
return p > 0 && p < 0.1 ? '<0.1%' : `${p.toFixed(1)}%`;
|
|
61
64
|
};
|
|
62
|
-
const fillColor = (pct) => (pct >= 80 ? '\x1b[31m' : pct >= 50 ? '\x1b[33m' : '\x1b[32m');
|
|
63
65
|
/** Extract a `<tag>…</tag>` block (with tags), or '' when absent. */
|
|
64
66
|
function tagBlock(s, tag) {
|
|
65
67
|
const i = s.indexOf(`<${tag}>`);
|
|
@@ -119,16 +121,16 @@ export const createContextCommand = () => ({
|
|
|
119
121
|
}
|
|
120
122
|
return { n: estTokens(text.length), exact: false };
|
|
121
123
|
};
|
|
122
|
-
// label, text, ANSI colour — one per category, in render order.
|
|
124
|
+
// label, text, ANSI colour — one per category, in render order (descriptive labels à la Claude Code).
|
|
123
125
|
const SPEC = [
|
|
124
|
-
['
|
|
125
|
-
['
|
|
126
|
-
['
|
|
127
|
-
['
|
|
128
|
-
['
|
|
129
|
-
['
|
|
130
|
-
['
|
|
131
|
-
['
|
|
126
|
+
['System prompt', sys.agent, '\x1b[36m'], // cyan — the agent prompt
|
|
127
|
+
['Environment', sys.env, '\x1b[33m'], // yellow — the <env> block
|
|
128
|
+
['Instructions', sys.instructions, '\x1b[34m'], // blue — AGENTS.md / CLAUDE.md
|
|
129
|
+
['Memory', sys.memory, '\x1b[35m'], // magenta — MEMORY.md
|
|
130
|
+
['Tools', `${toolSchemas}\n${sys.toolPrompts}`, '\x1b[31m'], // red — schemas + tool prompts
|
|
131
|
+
['You', byRole('user'), '\x1b[32m'], // green — your messages
|
|
132
|
+
['Agent', byRole('assistant'), '\x1b[94m'], // bright blue — assistant replies
|
|
133
|
+
['Tool results', byRole('tool'), '\x1b[90m'], // grey — tool results
|
|
132
134
|
];
|
|
133
135
|
const measured = await Promise.all(SPEC.map(([, text]) => measure(text)));
|
|
134
136
|
const cats = SPEC.map(([label, , color], i) => ({ label, n: measured[i].n, color })).filter((c) => c.n > 0);
|
|
@@ -138,43 +140,49 @@ export const createContextCommand = () => ({
|
|
|
138
140
|
const buffer = window > 0 ? Math.min(Math.round(window * 0.2), Math.max(0, window - total)) : 0;
|
|
139
141
|
const free = Math.max(0, window - total - buffer);
|
|
140
142
|
const mark = (n) => (exact ? fmtTok(n) : `~${fmtTok(n)}`);
|
|
141
|
-
const
|
|
142
|
-
//
|
|
143
|
-
const
|
|
143
|
+
const model = ctx.session?.model ?? '';
|
|
144
|
+
// Right-hand info column — model, totals, then per-category usage (Claude Code's /context).
|
|
145
|
+
const info = [];
|
|
146
|
+
if (model) {
|
|
147
|
+
info.push(model.split('/').pop() ?? model);
|
|
148
|
+
info.push(paint(model, DIM));
|
|
149
|
+
}
|
|
150
|
+
if (window > 0)
|
|
151
|
+
info.push(paint(`${mark(total)}/${fmtTok(window)} tokens (${pctStr(total, window)})`, DIM));
|
|
152
|
+
info.push('');
|
|
153
|
+
info.push(paint('Estimated usage by category', ITALIC_DIM));
|
|
154
|
+
for (const c of cats) {
|
|
155
|
+
info.push(`${paint(USED, c.color)} ${c.label}: ${paint(`${mark(c.n)} tokens (${pctStr(c.n, window)})`, DIM)}`);
|
|
156
|
+
}
|
|
144
157
|
if (window > 0) {
|
|
145
|
-
|
|
146
|
-
|
|
158
|
+
info.push(`${paint(USED, BUFFER_COLOR)} Compaction buffer: ${paint(`${fmtTok(buffer)} tokens (${pctStr(buffer, window)})`, DIM)}`);
|
|
159
|
+
info.push(`${paint(FREEG, DIM)} Free space: ${paint(`${fmtTok(free)} (${pctStr(free, window)})`, DIM)}`);
|
|
147
160
|
}
|
|
148
|
-
const
|
|
149
|
-
const header = window
|
|
150
|
-
? `context · ${mark(total)} / ${fmtTok(window)} ${paint(`(${pctStr(total, window)})`, fillColor(pctNum))}`
|
|
151
|
-
: `context · ${mark(total)} tokens (no model context window)`;
|
|
152
|
-
const lines = [header, ''];
|
|
161
|
+
const lines = [`${BOLD}Context Usage${RESET}`];
|
|
153
162
|
if (window > 0) {
|
|
154
|
-
//
|
|
163
|
+
// Grid: category cells, then free, with the compaction buffer at the very end.
|
|
155
164
|
const cellTokens = Math.max(1, window / GRID_CELLS);
|
|
156
165
|
const ratio = (n) => (n <= 0 ? 0 : Math.max(1, Math.round(n / cellTokens)));
|
|
157
166
|
const cells = [];
|
|
158
167
|
for (const c of cats)
|
|
159
168
|
for (let i = 0; i < ratio(c.n) && cells.length < GRID_CELLS; i++)
|
|
160
|
-
cells.push(paint(
|
|
169
|
+
cells.push(paint(USED, c.color));
|
|
161
170
|
const bufCells = Math.min(ratio(buffer), Math.max(0, GRID_CELLS - cells.length));
|
|
162
171
|
const freeCells = Math.max(0, GRID_CELLS - cells.length - bufCells);
|
|
163
172
|
for (let i = 0; i < freeCells; i++)
|
|
164
|
-
cells.push(paint(
|
|
173
|
+
cells.push(paint(FREEG, DIM));
|
|
165
174
|
for (let i = 0; i < bufCells; i++)
|
|
166
|
-
cells.push(paint(
|
|
175
|
+
cells.push(paint(USED, BUFFER_COLOR));
|
|
167
176
|
cells.length = GRID_CELLS;
|
|
168
|
-
// Side-by-side: 20×10 grid on the left,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
lines.push(` ${gridRow} ${legend[r] ?? ''}`.trimEnd());
|
|
177
|
+
// Side-by-side: 20×10 grid (space-separated glyphs) on the left, info column on the right.
|
|
178
|
+
for (let r = 0; r < Math.max(GRID_ROWS, info.length); r++) {
|
|
179
|
+
const gridRow = r < GRID_ROWS ? cells.slice(r * GRID_COLS, (r + 1) * GRID_COLS).join(' ') : ' '.repeat(GRID_WIDTH);
|
|
180
|
+
lines.push(`${gridRow} ${info[r] ?? ''}`.trimEnd());
|
|
173
181
|
}
|
|
174
182
|
}
|
|
175
183
|
else {
|
|
176
|
-
for (const l of
|
|
177
|
-
lines.push(
|
|
184
|
+
for (const l of info)
|
|
185
|
+
lines.push(l.trimEnd());
|
|
178
186
|
}
|
|
179
187
|
return { ok: true, output: lines.join('\n') };
|
|
180
188
|
},
|
package/esm/session/manager.js
CHANGED
|
@@ -12,6 +12,7 @@ const onFirstMessage = (session, fire) => {
|
|
|
12
12
|
get tools() {
|
|
13
13
|
return session.tools;
|
|
14
14
|
},
|
|
15
|
+
model: session.model,
|
|
15
16
|
assembleRequest: session.assembleRequest?.bind(session),
|
|
16
17
|
countTokens: session.countTokens?.bind(session),
|
|
17
18
|
contextWindow: session.contextWindow?.bind(session),
|
package/esm/session/persist.js
CHANGED
|
@@ -10,6 +10,7 @@ export const persistTo = (store, session, persisted = 0) => {
|
|
|
10
10
|
get tools() {
|
|
11
11
|
return session.tools;
|
|
12
12
|
},
|
|
13
|
+
model: session.model,
|
|
13
14
|
assembleRequest: session.assembleRequest?.bind(session),
|
|
14
15
|
countTokens: session.countTokens?.bind(session),
|
|
15
16
|
contextWindow: session.contextWindow?.bind(session),
|
package/esm/session/types.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface AssembledRequest {
|
|
|
19
19
|
}
|
|
20
20
|
export interface AgentSession {
|
|
21
21
|
readonly id: string;
|
|
22
|
+
readonly model?: string;
|
|
22
23
|
readonly messages: readonly Message[];
|
|
23
24
|
readonly tools: readonly Tool[];
|
|
24
25
|
/** Assemble the request from the CURRENT in-memory session — what the next turn would send. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mu-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Agent harness: createHarness wires mu-core into a host — XDG paths, model registry, plugins, disk-loaded agents & skills, sub-agents, sessions (JSONL + SQLite catalog), slash commands, permission/approval hooks, an optional scheduler, and a composable TUI chat app",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./script/index.js",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"@swc/wasm-typescript": "^1.15.0",
|
|
24
24
|
"cli-highlight": "^2.1.11",
|
|
25
25
|
"croner": "^9.0.0",
|
|
26
|
-
"mu-core": "^0.
|
|
27
|
-
"mu-tui": "^0.
|
|
26
|
+
"mu-core": "^0.30.0",
|
|
27
|
+
"mu-tui": "^0.30.0",
|
|
28
28
|
"ws": "^8.18.0"
|
|
29
29
|
},
|
|
30
30
|
"_generatedBy": "dnt@dev",
|
|
@@ -51,13 +51,16 @@ const estTokens = (chars) => Math.max(1, Math.round(chars / 4));
|
|
|
51
51
|
const GRID_COLS = 20;
|
|
52
52
|
const GRID_ROWS = 10;
|
|
53
53
|
const GRID_CELLS = GRID_COLS * GRID_ROWS;
|
|
54
|
-
const
|
|
55
|
-
const
|
|
54
|
+
const GRID_WIDTH = GRID_COLS * 2 - 1; // glyphs joined by single spaces
|
|
55
|
+
const USED = '⛁'; // a filled context cell (Claude Code's /context glyph)
|
|
56
|
+
const FREEG = '⛶'; // a free cell
|
|
56
57
|
const BUFFER_COLOR = '\x1b[93m'; // bright yellow — the compaction reserve
|
|
57
|
-
// ANSI SGR
|
|
58
|
+
// ANSI SGR codes — mu's TUI text utils are ANSI-aware so these render in the terminal;
|
|
58
59
|
// the companion strips them (plain text), keeping the labelled breakdown readable.
|
|
59
60
|
const RESET = '\x1b[0m';
|
|
60
61
|
const DIM = '\x1b[2m';
|
|
62
|
+
const BOLD = '\x1b[1m';
|
|
63
|
+
const ITALIC_DIM = '\x1b[2;3m';
|
|
61
64
|
const paint = (s, color) => `${color}${s}${RESET}`;
|
|
62
65
|
const fmtTok = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`);
|
|
63
66
|
const pctStr = (n, w) => {
|
|
@@ -66,7 +69,6 @@ const pctStr = (n, w) => {
|
|
|
66
69
|
const p = (n / w) * 100;
|
|
67
70
|
return p > 0 && p < 0.1 ? '<0.1%' : `${p.toFixed(1)}%`;
|
|
68
71
|
};
|
|
69
|
-
const fillColor = (pct) => (pct >= 80 ? '\x1b[31m' : pct >= 50 ? '\x1b[33m' : '\x1b[32m');
|
|
70
72
|
/** Extract a `<tag>…</tag>` block (with tags), or '' when absent. */
|
|
71
73
|
function tagBlock(s, tag) {
|
|
72
74
|
const i = s.indexOf(`<${tag}>`);
|
|
@@ -126,16 +128,16 @@ const createContextCommand = () => ({
|
|
|
126
128
|
}
|
|
127
129
|
return { n: estTokens(text.length), exact: false };
|
|
128
130
|
};
|
|
129
|
-
// label, text, ANSI colour — one per category, in render order.
|
|
131
|
+
// label, text, ANSI colour — one per category, in render order (descriptive labels à la Claude Code).
|
|
130
132
|
const SPEC = [
|
|
131
|
-
['
|
|
132
|
-
['
|
|
133
|
-
['
|
|
134
|
-
['
|
|
135
|
-
['
|
|
136
|
-
['
|
|
137
|
-
['
|
|
138
|
-
['
|
|
133
|
+
['System prompt', sys.agent, '\x1b[36m'], // cyan — the agent prompt
|
|
134
|
+
['Environment', sys.env, '\x1b[33m'], // yellow — the <env> block
|
|
135
|
+
['Instructions', sys.instructions, '\x1b[34m'], // blue — AGENTS.md / CLAUDE.md
|
|
136
|
+
['Memory', sys.memory, '\x1b[35m'], // magenta — MEMORY.md
|
|
137
|
+
['Tools', `${toolSchemas}\n${sys.toolPrompts}`, '\x1b[31m'], // red — schemas + tool prompts
|
|
138
|
+
['You', byRole('user'), '\x1b[32m'], // green — your messages
|
|
139
|
+
['Agent', byRole('assistant'), '\x1b[94m'], // bright blue — assistant replies
|
|
140
|
+
['Tool results', byRole('tool'), '\x1b[90m'], // grey — tool results
|
|
139
141
|
];
|
|
140
142
|
const measured = await Promise.all(SPEC.map(([, text]) => measure(text)));
|
|
141
143
|
const cats = SPEC.map(([label, , color], i) => ({ label, n: measured[i].n, color })).filter((c) => c.n > 0);
|
|
@@ -145,43 +147,49 @@ const createContextCommand = () => ({
|
|
|
145
147
|
const buffer = window > 0 ? Math.min(Math.round(window * 0.2), Math.max(0, window - total)) : 0;
|
|
146
148
|
const free = Math.max(0, window - total - buffer);
|
|
147
149
|
const mark = (n) => (exact ? fmtTok(n) : `~${fmtTok(n)}`);
|
|
148
|
-
const
|
|
149
|
-
//
|
|
150
|
-
const
|
|
150
|
+
const model = ctx.session?.model ?? '';
|
|
151
|
+
// Right-hand info column — model, totals, then per-category usage (Claude Code's /context).
|
|
152
|
+
const info = [];
|
|
153
|
+
if (model) {
|
|
154
|
+
info.push(model.split('/').pop() ?? model);
|
|
155
|
+
info.push(paint(model, DIM));
|
|
156
|
+
}
|
|
157
|
+
if (window > 0)
|
|
158
|
+
info.push(paint(`${mark(total)}/${fmtTok(window)} tokens (${pctStr(total, window)})`, DIM));
|
|
159
|
+
info.push('');
|
|
160
|
+
info.push(paint('Estimated usage by category', ITALIC_DIM));
|
|
161
|
+
for (const c of cats) {
|
|
162
|
+
info.push(`${paint(USED, c.color)} ${c.label}: ${paint(`${mark(c.n)} tokens (${pctStr(c.n, window)})`, DIM)}`);
|
|
163
|
+
}
|
|
151
164
|
if (window > 0) {
|
|
152
|
-
|
|
153
|
-
|
|
165
|
+
info.push(`${paint(USED, BUFFER_COLOR)} Compaction buffer: ${paint(`${fmtTok(buffer)} tokens (${pctStr(buffer, window)})`, DIM)}`);
|
|
166
|
+
info.push(`${paint(FREEG, DIM)} Free space: ${paint(`${fmtTok(free)} (${pctStr(free, window)})`, DIM)}`);
|
|
154
167
|
}
|
|
155
|
-
const
|
|
156
|
-
const header = window
|
|
157
|
-
? `context · ${mark(total)} / ${fmtTok(window)} ${paint(`(${pctStr(total, window)})`, fillColor(pctNum))}`
|
|
158
|
-
: `context · ${mark(total)} tokens (no model context window)`;
|
|
159
|
-
const lines = [header, ''];
|
|
168
|
+
const lines = [`${BOLD}Context Usage${RESET}`];
|
|
160
169
|
if (window > 0) {
|
|
161
|
-
//
|
|
170
|
+
// Grid: category cells, then free, with the compaction buffer at the very end.
|
|
162
171
|
const cellTokens = Math.max(1, window / GRID_CELLS);
|
|
163
172
|
const ratio = (n) => (n <= 0 ? 0 : Math.max(1, Math.round(n / cellTokens)));
|
|
164
173
|
const cells = [];
|
|
165
174
|
for (const c of cats)
|
|
166
175
|
for (let i = 0; i < ratio(c.n) && cells.length < GRID_CELLS; i++)
|
|
167
|
-
cells.push(paint(
|
|
176
|
+
cells.push(paint(USED, c.color));
|
|
168
177
|
const bufCells = Math.min(ratio(buffer), Math.max(0, GRID_CELLS - cells.length));
|
|
169
178
|
const freeCells = Math.max(0, GRID_CELLS - cells.length - bufCells);
|
|
170
179
|
for (let i = 0; i < freeCells; i++)
|
|
171
|
-
cells.push(paint(
|
|
180
|
+
cells.push(paint(FREEG, DIM));
|
|
172
181
|
for (let i = 0; i < bufCells; i++)
|
|
173
|
-
cells.push(paint(
|
|
182
|
+
cells.push(paint(USED, BUFFER_COLOR));
|
|
174
183
|
cells.length = GRID_CELLS;
|
|
175
|
-
// Side-by-side: 20×10 grid on the left,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
lines.push(` ${gridRow} ${legend[r] ?? ''}`.trimEnd());
|
|
184
|
+
// Side-by-side: 20×10 grid (space-separated glyphs) on the left, info column on the right.
|
|
185
|
+
for (let r = 0; r < Math.max(GRID_ROWS, info.length); r++) {
|
|
186
|
+
const gridRow = r < GRID_ROWS ? cells.slice(r * GRID_COLS, (r + 1) * GRID_COLS).join(' ') : ' '.repeat(GRID_WIDTH);
|
|
187
|
+
lines.push(`${gridRow} ${info[r] ?? ''}`.trimEnd());
|
|
180
188
|
}
|
|
181
189
|
}
|
|
182
190
|
else {
|
|
183
|
-
for (const l of
|
|
184
|
-
lines.push(
|
|
191
|
+
for (const l of info)
|
|
192
|
+
lines.push(l.trimEnd());
|
|
185
193
|
}
|
|
186
194
|
return { ok: true, output: lines.join('\n') };
|
|
187
195
|
},
|
|
@@ -15,6 +15,7 @@ const onFirstMessage = (session, fire) => {
|
|
|
15
15
|
get tools() {
|
|
16
16
|
return session.tools;
|
|
17
17
|
},
|
|
18
|
+
model: session.model,
|
|
18
19
|
assembleRequest: session.assembleRequest?.bind(session),
|
|
19
20
|
countTokens: session.countTokens?.bind(session),
|
|
20
21
|
contextWindow: session.contextWindow?.bind(session),
|
|
@@ -13,6 +13,7 @@ const persistTo = (store, session, persisted = 0) => {
|
|
|
13
13
|
get tools() {
|
|
14
14
|
return session.tools;
|
|
15
15
|
},
|
|
16
|
+
model: session.model,
|
|
16
17
|
assembleRequest: session.assembleRequest?.bind(session),
|
|
17
18
|
countTokens: session.countTokens?.bind(session),
|
|
18
19
|
contextWindow: session.contextWindow?.bind(session),
|
|
@@ -19,6 +19,7 @@ export interface AssembledRequest {
|
|
|
19
19
|
}
|
|
20
20
|
export interface AgentSession {
|
|
21
21
|
readonly id: string;
|
|
22
|
+
readonly model?: string;
|
|
22
23
|
readonly messages: readonly Message[];
|
|
23
24
|
readonly tools: readonly Tool[];
|
|
24
25
|
/** Assemble the request from the CURRENT in-memory session — what the next turn would send. */
|