little-coder 1.5.1 → 1.6.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/.pi/extensions/read-guard/index.ts +153 -0
- package/.pi/extensions/read-guard/read-guard.test.ts +189 -0
- package/CHANGELOG.md +12 -0
- package/README.md +5 -3
- package/package.json +1 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { harnessIntervention } from "../_shared/intervention.ts";
|
|
3
|
+
|
|
4
|
+
// Harness intervention: trim a `read` result that would overflow the context window.
|
|
5
|
+
//
|
|
6
|
+
// little-coder drives SMALL local models with small context windows
|
|
7
|
+
// (`context_limit` is 32768 in .pi/settings.json, and the live window is often
|
|
8
|
+
// less). pi's built-in `read` returns up to ~2000 lines in a single tool result
|
|
9
|
+
// — for a small model that one result can blow past the remaining budget, evict
|
|
10
|
+
// earlier conversation, and wreck the run. That's exactly the class of failure
|
|
11
|
+
// the harness-intervention layer exists to catch (cf. thinking-budget cap,
|
|
12
|
+
// write-guard redirect, turn-cap).
|
|
13
|
+
//
|
|
14
|
+
// When a read result would push context usage past the window, we replace it
|
|
15
|
+
// with only the file's first HEAD_LINES lines plus a message telling the model
|
|
16
|
+
// why it was trimmed and to use those lines to understand the structure, then
|
|
17
|
+
// locate what it needs with grep/find or a targeted read (offset/limit) — rather
|
|
18
|
+
// than re-reading the whole file. The user sees one uniform "harness
|
|
19
|
+
// intervention: …" line, like every other intervention.
|
|
20
|
+
//
|
|
21
|
+
// Why `tool_result`, not `tool_call`: a `tool_call` handler can only `block`
|
|
22
|
+
// with a `reason` string (no file content) or mutate `input.limit` (lines but no
|
|
23
|
+
// message). Delivering BOTH the first 30 lines AND an explanation in one result
|
|
24
|
+
// requires `tool_result`, whose return value replaces the content the model sees
|
|
25
|
+
// (ToolResultEventResult.content). The full file is still read from disk (pi
|
|
26
|
+
// already caps that at ~2000 lines) but the oversized text never reaches the LLM
|
|
27
|
+
// context because we swap it out before it lands.
|
|
28
|
+
|
|
29
|
+
export const HEAD_LINES = 30;
|
|
30
|
+
|
|
31
|
+
// When current context usage is unknown (e.g. right after compaction
|
|
32
|
+
// getContextUsage().tokens is null), fall back to "a single file should never
|
|
33
|
+
// eat more than this fraction of the whole window".
|
|
34
|
+
export const FALLBACK_FRACTION = 0.5;
|
|
35
|
+
|
|
36
|
+
// Tokens to keep in reserve below the window before we call a read an overflow.
|
|
37
|
+
// 0 = trim only on literal overflow; raise it to trim slightly earlier and leave
|
|
38
|
+
// the model headroom to act on the 30 lines.
|
|
39
|
+
export const RESERVE = 0;
|
|
40
|
+
|
|
41
|
+
/** chars→tokens estimate. Same 3.5 ratio as thinking-budget's charsToTokens /
|
|
42
|
+
* local/context_manager.estimate_tokens. */
|
|
43
|
+
export function estimateTokens(chars: number): number {
|
|
44
|
+
return Math.ceil(chars / 3.5);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** First `n` lines of `text`, preserving pi's `cat -n` line-number prefixes so
|
|
48
|
+
* the model keeps a real structural view. Safe when text has fewer than n. */
|
|
49
|
+
export function firstLines(text: string, n: number): string {
|
|
50
|
+
return text.split("\n").slice(0, n).join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function countLines(text: string): number {
|
|
54
|
+
if (text === "") return 0;
|
|
55
|
+
return text.split("\n").length;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Decide whether a read result should be trimmed because keeping it whole would
|
|
60
|
+
* exceed the context window.
|
|
61
|
+
*
|
|
62
|
+
* - Nothing to trim if the result is already <= headN lines, or we have no window.
|
|
63
|
+
* - With a known current token count: trim when current + est would cross the
|
|
64
|
+
* window (less RESERVE) — the literal "will result in exceeding the window".
|
|
65
|
+
* - With unknown current usage: trim when the result alone exceeds
|
|
66
|
+
* FALLBACK_FRACTION of the window.
|
|
67
|
+
*/
|
|
68
|
+
export function shouldTrimRead(a: {
|
|
69
|
+
contentChars: number;
|
|
70
|
+
currentTokens: number | null;
|
|
71
|
+
contextWindow: number;
|
|
72
|
+
lineCount: number;
|
|
73
|
+
headN: number;
|
|
74
|
+
}): boolean {
|
|
75
|
+
if (!a.contextWindow) return false;
|
|
76
|
+
if (a.lineCount <= a.headN) return false;
|
|
77
|
+
const est = estimateTokens(a.contentChars);
|
|
78
|
+
if (a.currentTokens == null) {
|
|
79
|
+
return est > a.contextWindow * FALLBACK_FRACTION;
|
|
80
|
+
}
|
|
81
|
+
return a.currentTokens + est > a.contextWindow - RESERVE;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Message appended below the 30 lines, addressed to the model. Leads with the
|
|
85
|
+
* consequence and the directive. */
|
|
86
|
+
export function trimmedReadMessage(a: {
|
|
87
|
+
shownLines: number;
|
|
88
|
+
totalLines: number;
|
|
89
|
+
estTokens: number;
|
|
90
|
+
contextWindow: number;
|
|
91
|
+
}): string {
|
|
92
|
+
return (
|
|
93
|
+
`⚠️ This file is too large to read in full — reading all ${a.totalLines} lines ` +
|
|
94
|
+
`(~${a.estTokens} tokens) would exceed the remaining context window ` +
|
|
95
|
+
`(${a.contextWindow} tokens). Only the first ${a.shownLines} lines are shown above.\n` +
|
|
96
|
+
`\n` +
|
|
97
|
+
`Use these ${a.shownLines} lines to understand the file's structure, then narrow down ` +
|
|
98
|
+
`instead of reading the whole thing:\n` +
|
|
99
|
+
` • search for what you need with \`grep\` (by content) or \`find\` (by name), then\n` +
|
|
100
|
+
` • \`read\` only the relevant range with \`offset\` and \`limit\`.\n` +
|
|
101
|
+
`\n` +
|
|
102
|
+
`Do NOT re-read this file in full — it will be trimmed again.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type TextOrImage = { type: string; text?: string };
|
|
107
|
+
|
|
108
|
+
export default function (pi: ExtensionAPI) {
|
|
109
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
110
|
+
if (String((event as any).toolName ?? "").toLowerCase() !== "read") return;
|
|
111
|
+
if ((event as any).isError) return;
|
|
112
|
+
|
|
113
|
+
const content = (((event as any).content ?? []) as TextOrImage[]);
|
|
114
|
+
if (content.length === 0) return;
|
|
115
|
+
// Text-only: an image read can't be line-trimmed, leave it alone.
|
|
116
|
+
if (content.some((c) => c.type !== "text")) return;
|
|
117
|
+
const text = content.map((c) => c.text ?? "").join("");
|
|
118
|
+
|
|
119
|
+
// getContextUsage may be absent on older SDK builds; without a window we
|
|
120
|
+
// can't judge overflow, so leave the result untouched.
|
|
121
|
+
const usage =
|
|
122
|
+
typeof ctx.getContextUsage === "function" ? ctx.getContextUsage() : undefined;
|
|
123
|
+
if (!usage?.contextWindow) return;
|
|
124
|
+
|
|
125
|
+
const lineCount = countLines(text);
|
|
126
|
+
if (
|
|
127
|
+
!shouldTrimRead({
|
|
128
|
+
contentChars: text.length,
|
|
129
|
+
currentTokens: usage.tokens,
|
|
130
|
+
contextWindow: usage.contextWindow,
|
|
131
|
+
lineCount,
|
|
132
|
+
headN: HEAD_LINES,
|
|
133
|
+
})
|
|
134
|
+
) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const head = firstLines(text, HEAD_LINES);
|
|
139
|
+
const msg = trimmedReadMessage({
|
|
140
|
+
shownLines: HEAD_LINES,
|
|
141
|
+
totalLines: lineCount,
|
|
142
|
+
estTokens: estimateTokens(text.length),
|
|
143
|
+
contextWindow: usage.contextWindow,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
harnessIntervention(
|
|
147
|
+
ctx,
|
|
148
|
+
"a read would have overflowed the context window — showed only the file's first 30 lines and told the model to search it instead.",
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return { content: [{ type: "text" as const, text: head + "\n\n" + msg }] };
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import setupReadGuard, {
|
|
3
|
+
HEAD_LINES,
|
|
4
|
+
FALLBACK_FRACTION,
|
|
5
|
+
estimateTokens,
|
|
6
|
+
firstLines,
|
|
7
|
+
countLines,
|
|
8
|
+
shouldTrimRead,
|
|
9
|
+
trimmedReadMessage,
|
|
10
|
+
} from "./index.ts";
|
|
11
|
+
|
|
12
|
+
// ── pure helpers ────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
describe("estimateTokens", () => {
|
|
15
|
+
it("uses the 3.5 chars/token ratio, rounding up", () => {
|
|
16
|
+
expect(estimateTokens(0)).toBe(0);
|
|
17
|
+
expect(estimateTokens(1)).toBe(1); // ceil(1/3.5)
|
|
18
|
+
expect(estimateTokens(35)).toBe(10);
|
|
19
|
+
expect(estimateTokens(36)).toBe(11); // ceil(36/3.5)
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("firstLines", () => {
|
|
24
|
+
const sample = Array.from({ length: 100 }, (_, i) => `${i + 1}\tline ${i + 1}`).join("\n");
|
|
25
|
+
|
|
26
|
+
it("returns the first n lines and preserves cat -n prefixes", () => {
|
|
27
|
+
const out = firstLines(sample, 30);
|
|
28
|
+
expect(countLines(out)).toBe(30);
|
|
29
|
+
expect(out.startsWith("1\tline 1")).toBe(true);
|
|
30
|
+
expect(out.endsWith("30\tline 30")).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("is safe when the text has fewer than n lines", () => {
|
|
34
|
+
expect(firstLines("a\nb", 30)).toBe("a\nb");
|
|
35
|
+
expect(firstLines("", 30)).toBe("");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("countLines", () => {
|
|
40
|
+
it("counts newline-separated lines, with empty string as zero", () => {
|
|
41
|
+
expect(countLines("")).toBe(0);
|
|
42
|
+
expect(countLines("one")).toBe(1);
|
|
43
|
+
expect(countLines("one\ntwo\nthree")).toBe(3);
|
|
44
|
+
expect(countLines("trailing\n")).toBe(2); // trailing newline => empty final line
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("shouldTrimRead", () => {
|
|
49
|
+
const base = { contextWindow: 32768, headN: HEAD_LINES };
|
|
50
|
+
|
|
51
|
+
it("trims when current tokens + estimate would exceed the window", () => {
|
|
52
|
+
// 100k chars ≈ 28572 tokens; with 10000 already used that crosses 32768.
|
|
53
|
+
expect(
|
|
54
|
+
shouldTrimRead({ ...base, contentChars: 100_000, currentTokens: 10_000, lineCount: 2000 }),
|
|
55
|
+
).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("does not trim when the result comfortably fits", () => {
|
|
59
|
+
expect(
|
|
60
|
+
shouldTrimRead({ ...base, contentChars: 4_000, currentTokens: 1_000, lineCount: 200 }),
|
|
61
|
+
).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("never trims when the result is <= headN lines", () => {
|
|
65
|
+
expect(
|
|
66
|
+
shouldTrimRead({ ...base, contentChars: 1_000_000, currentTokens: 30_000, lineCount: HEAD_LINES }),
|
|
67
|
+
).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("falls back to a window fraction when current usage is unknown (null)", () => {
|
|
71
|
+
const window = 10_000;
|
|
72
|
+
const overChars = Math.ceil(window * FALLBACK_FRACTION * 3.5) + 100; // est just over half
|
|
73
|
+
const underChars = Math.floor(window * FALLBACK_FRACTION * 3.5) - 100; // est just under half
|
|
74
|
+
expect(
|
|
75
|
+
shouldTrimRead({ contextWindow: window, headN: HEAD_LINES, currentTokens: null, contentChars: overChars, lineCount: 2000 }),
|
|
76
|
+
).toBe(true);
|
|
77
|
+
expect(
|
|
78
|
+
shouldTrimRead({ contextWindow: window, headN: HEAD_LINES, currentTokens: null, contentChars: underChars, lineCount: 2000 }),
|
|
79
|
+
).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns false when there is no context window to judge against", () => {
|
|
83
|
+
expect(
|
|
84
|
+
shouldTrimRead({ contextWindow: 0, headN: HEAD_LINES, currentTokens: 1, contentChars: 1_000_000, lineCount: 2000 }),
|
|
85
|
+
).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("trimmedReadMessage", () => {
|
|
90
|
+
it("explains the trim and directs to grep/find + targeted read", () => {
|
|
91
|
+
const msg = trimmedReadMessage({ shownLines: 30, totalLines: 2000, estTokens: 28572, contextWindow: 32768 });
|
|
92
|
+
expect(msg).toContain("too large");
|
|
93
|
+
expect(msg).toContain("first 30 lines");
|
|
94
|
+
expect(msg).toContain("grep");
|
|
95
|
+
expect(msg).toContain("find");
|
|
96
|
+
expect(msg).toContain("offset");
|
|
97
|
+
expect(msg).toContain("limit");
|
|
98
|
+
expect(msg).toContain("Do NOT re-read");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── tool_result handler ─────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function getToolResultHandler() {
|
|
105
|
+
let handler: ((event: any, ctx: any) => any) | undefined;
|
|
106
|
+
const pi = {
|
|
107
|
+
on(name: string, h: (event: any, ctx: any) => any) {
|
|
108
|
+
if (name === "tool_result") handler = h;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
setupReadGuard(pi as any);
|
|
112
|
+
if (!handler) throw new Error("read-guard did not register a tool_result handler");
|
|
113
|
+
return handler;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function makeCtx(usage: { tokens: number | null; contextWindow: number } | undefined) {
|
|
117
|
+
const notifies: string[] = [];
|
|
118
|
+
return {
|
|
119
|
+
notifies,
|
|
120
|
+
ui: { notify: (m: string) => notifies.push(m) },
|
|
121
|
+
getContextUsage: () => (usage ? { ...usage, percent: null } : undefined),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// A read result whose text is `lines` numbered lines, ~chars wide each.
|
|
126
|
+
function bigReadEvent(lines: number, width = 80) {
|
|
127
|
+
const text = Array.from({ length: lines }, (_, i) => `${i + 1}\t${"x".repeat(width)}`).join("\n");
|
|
128
|
+
return { toolName: "read", isError: false, content: [{ type: "text", text }] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
describe("read-guard tool_result handler", () => {
|
|
132
|
+
it("trims an oversized read to 30 lines + a directive and fires one intervention", async () => {
|
|
133
|
+
const handler = getToolResultHandler();
|
|
134
|
+
const ctx = makeCtx({ tokens: 20_000, contextWindow: 32768 });
|
|
135
|
+
const result = await handler(bigReadEvent(2000), ctx);
|
|
136
|
+
|
|
137
|
+
expect(result?.content).toHaveLength(1);
|
|
138
|
+
const out = result.content[0].text as string;
|
|
139
|
+
// first 30 lines preserved (and only those), then the directive
|
|
140
|
+
expect(out.startsWith("1\t")).toBe(true);
|
|
141
|
+
expect(out).not.toContain("\n31\t"); // line 31's content must be gone
|
|
142
|
+
const [headPart] = out.split("⚠️");
|
|
143
|
+
expect(countLines(headPart.trimEnd())).toBe(30);
|
|
144
|
+
expect(out).toContain("grep");
|
|
145
|
+
expect(ctx.notifies).toHaveLength(1);
|
|
146
|
+
expect(ctx.notifies[0]).toMatch(/harness intervention:.*first 30 lines/i);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("leaves a read that fits the window untouched", async () => {
|
|
150
|
+
const handler = getToolResultHandler();
|
|
151
|
+
const ctx = makeCtx({ tokens: 1_000, contextWindow: 32768 });
|
|
152
|
+
const result = await handler(bigReadEvent(50), ctx);
|
|
153
|
+
expect(result).toBeUndefined();
|
|
154
|
+
expect(ctx.notifies).toHaveLength(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("ignores error results", async () => {
|
|
158
|
+
const handler = getToolResultHandler();
|
|
159
|
+
const ctx = makeCtx({ tokens: 30_000, contextWindow: 32768 });
|
|
160
|
+
const ev = { ...bigReadEvent(2000), isError: true };
|
|
161
|
+
expect(await handler(ev, ctx)).toBeUndefined();
|
|
162
|
+
expect(ctx.notifies).toHaveLength(0);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("ignores results that contain an image block (can't line-trim an image)", async () => {
|
|
166
|
+
const handler = getToolResultHandler();
|
|
167
|
+
const ctx = makeCtx({ tokens: 30_000, contextWindow: 32768 });
|
|
168
|
+
const ev = {
|
|
169
|
+
toolName: "read",
|
|
170
|
+
isError: false,
|
|
171
|
+
content: [{ type: "image", data: "…", mimeType: "image/png" }],
|
|
172
|
+
};
|
|
173
|
+
expect(await handler(ev, ctx)).toBeUndefined();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("ignores non-read tools", async () => {
|
|
177
|
+
const handler = getToolResultHandler();
|
|
178
|
+
const ctx = makeCtx({ tokens: 30_000, contextWindow: 32768 });
|
|
179
|
+
const ev = { ...bigReadEvent(2000), toolName: "bash" };
|
|
180
|
+
expect(await handler(ev, ctx)).toBeUndefined();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("does nothing when context usage is unavailable", async () => {
|
|
184
|
+
const handler = getToolResultHandler();
|
|
185
|
+
const ctx = makeCtx(undefined);
|
|
186
|
+
expect(await handler(bigReadEvent(2000), ctx)).toBeUndefined();
|
|
187
|
+
expect(ctx.notifies).toHaveLength(0);
|
|
188
|
+
});
|
|
189
|
+
});
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to little-coder are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and little-coder's public interface (CLI, providers, tools, skills) follows semver starting at `v0.0.1` post-rename.
|
|
4
4
|
|
|
5
|
+
## [v1.6.0] — 2026-05-23
|
|
6
|
+
|
|
7
|
+
A new harness intervention for small-context models: oversized file reads no longer blow the context window. little-coder targets local models with small windows (`context_limit` is 32768, and the live window is often less), but pi's built-in `read` returns up to ~2000 lines in a single tool result — enough for one read to evict the conversation and derail the run. The harness now catches that read before it lands and replaces it with the file's head plus a "search, don't slurp" directive, surfaced through the same one-voice `harness intervention: …` line as the thinking-budget cap, write-guard redirect, and turn-cap.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`read-guard` extension — trims a Read that would overflow the context window.** On the `tool_result` event, when a successful `read`'s content would push context usage past the window (`ctx.getContextUsage().tokens + estimate(result) > contextWindow`, estimated at the same 3.5 chars/token ratio as the thinking-budget cap), the harness replaces the result with **only the file's first 30 lines** followed by a message that explains the trim and directs the model to use those lines to understand the file's structure, then narrow down — locate what it needs with `grep`/`find` or a targeted `read` (`offset`/`limit`) — rather than re-reading the whole file. The full file is still read from disk (pi already caps that at ~2000 lines), but the oversized text never reaches the model's context because the result content is swapped before it lands. `tool_result` (not `tool_call`) is used precisely because it can deliver both the 30 lines and the explanation in one result — a `tool_call` block can only return a `reason` string, and mutating `input.limit` gives lines but no message. When current usage is unknown (e.g. right after compaction, `tokens` is null), it falls back to trimming any single read that alone exceeds half the window. Image reads and error results are left untouched. New extension at `.pi/extensions/read-guard/`, auto-discovered by the launcher.
|
|
11
|
+
|
|
12
|
+
### Notes for upgraders
|
|
13
|
+
- No CLI flag, `models.json` shape, `.pi/settings.json`, or per-model-profile schema changes. The new extension auto-loads like every other `.pi/extensions/*/index.ts`, and only changes behaviour when a read would otherwise overflow the context window — normal reads pass through untouched. The threshold reads pi's live `getContextUsage()`, so it scales with whatever context window the active model reports.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
5
17
|
## [v1.5.1] — 2026-05-22
|
|
6
18
|
|
|
7
19
|
A branding release — no behaviour changes. little-coder now wears the v1.0 brand book: the warm **paper / ink / honey** palette (`#F2EBDC` · `#1A1410` · `#E15A1F`), the `lc▌` block-cursor mark, and IBM Plex Mono. The "ready to type" cursor is the punchline — it ties the CLI heritage into the identity without saying so.
|
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
# little-coder
|
|
2
5
|
|
|
3
6
|
**A coding agent tuned for small local models, built on top of [pi](https://pi.dev).**
|
|
4
7
|
|
|
5
|
-

|
|
6
|
-
|
|
7
8
|
The research story behind all this — why scaffold–model fit matters, how a 9.7 B Qwen beat frontier entries on Aider Polyglot, and what the load-bearing mechanisms actually do — is written up on Substack: **[*Honey, I Shrunk the Coding Agent*](https://open.substack.com/pub/itayinbarr/p/honey-i-shrunk-the-coding-agent)**. Start there if you want the "why"; stay here for the "how".
|
|
8
9
|
|
|
9
10
|
## How it relates to pi
|
|
@@ -298,10 +299,11 @@ The benchmarks harness (`benchmarks/`) is dev-only and not shipped with the npm
|
|
|
298
299
|
little-coder/
|
|
299
300
|
├── .pi/
|
|
300
301
|
│ ├── settings.json # per-model profiles + benchmark_overrides (terminal_bench, gaia)
|
|
301
|
-
│ └── extensions/ #
|
|
302
|
+
│ └── extensions/ # 23 TypeScript extensions, auto-discovered by pi
|
|
302
303
|
│ ├── branding/ # little-coder startup header + terminal title (replaces pi's built-in)
|
|
303
304
|
│ ├── llama-cpp-provider/ # data-driven provider registration from models.json — ships llamacpp, ollama, lmstudio (+ user override file)
|
|
304
305
|
│ ├── write-guard/ # Write refuses on existing files; rewrites root-bare /foo.md paths to cwd
|
|
306
|
+
│ ├── read-guard/ # trims a Read that would overflow the context window to its first 30 lines + a search-instead directive
|
|
305
307
|
│ ├── extra-tools/ # glob, webfetch, websearch (pi ships grep/find)
|
|
306
308
|
│ ├── skill-inject/ # per-turn tool-skill selection (error > recency > intent)
|
|
307
309
|
│ ├── knowledge-inject/ # algorithm cheat-sheet scoring (word=1.0, bigram=2.0, threshold=2.0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "little-coder",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "A pi-based coding agent optimized for small local language models. Reproduces the whitepaper's scaffold-model-fit adaptations as pi extensions.",
|
|
5
5
|
"homepage": "https://github.com/itayinbarr/little-coder",
|
|
6
6
|
"repository": {
|