pi-agenticoding 0.1.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/LICENSE +21 -0
- package/README.md +240 -0
- package/agenticoding.test.ts +2079 -0
- package/handoff/command.ts +36 -0
- package/handoff/compact.ts +35 -0
- package/handoff/tool.ts +151 -0
- package/index.ts +149 -0
- package/ledger/rehydration.ts +94 -0
- package/ledger/store.ts +82 -0
- package/ledger/tools.ts +166 -0
- package/package.json +21 -0
- package/spawn/index.ts +487 -0
- package/spawn/renderer.ts +809 -0
- package/spawn/shared.ts +34 -0
- package/state.ts +108 -0
- package/system-prompt.ts +59 -0
- package/test-loader.mjs +32 -0
- package/watchdog.ts +65 -0
package/spawn/shared.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type ThinkingValue = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
2
|
+
export type SpawnOutcome = "running" | "success" | "aborted" | "error";
|
|
3
|
+
|
|
4
|
+
export type SpawnResultDetails = {
|
|
5
|
+
depth: number;
|
|
6
|
+
model: string;
|
|
7
|
+
thinking: ThinkingValue;
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
outcome: SpawnOutcome;
|
|
10
|
+
stats?: Record<string, number>;
|
|
11
|
+
statsUnavailable?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type AssistantMessageLike = {
|
|
15
|
+
role: string;
|
|
16
|
+
content?: { type: string; text?: string }[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns all text blocks from the last assistant message, joined by newlines.
|
|
21
|
+
*/
|
|
22
|
+
export function getLastAssistantText(messages: AssistantMessageLike[]): string {
|
|
23
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
24
|
+
const msg = messages[i];
|
|
25
|
+
if (msg.role !== "assistant") continue;
|
|
26
|
+
const text = (msg.content ?? [])
|
|
27
|
+
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
28
|
+
.map((block) => block.text ?? "")
|
|
29
|
+
.join("\n")
|
|
30
|
+
.trim();
|
|
31
|
+
if (text) return text;
|
|
32
|
+
}
|
|
33
|
+
return "";
|
|
34
|
+
}
|
package/state.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared mutable state for the agenticoding extension.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth that all modules read/write through.
|
|
5
|
+
* Mutable by design — this is session-scoped imperative state.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
export interface AgenticodingState {
|
|
11
|
+
/** Compact ledger entries keyed by kebab-case name */
|
|
12
|
+
ledger: Map<string, string>;
|
|
13
|
+
|
|
14
|
+
/** Monotonically increasing epoch, set on first ledger_add */
|
|
15
|
+
epoch: number;
|
|
16
|
+
|
|
17
|
+
/** Last context usage percent from getContextUsage() */
|
|
18
|
+
lastContextPercent: number | null;
|
|
19
|
+
|
|
20
|
+
/** Handoff task queued by the tool until the compaction hook consumes it. */
|
|
21
|
+
pendingHandoff: { task: string; source: "tool" } | null;
|
|
22
|
+
|
|
23
|
+
/** User-requested handoff that must result in a real tool-driven compaction. */
|
|
24
|
+
pendingRequestedHandoff: {
|
|
25
|
+
direction: string;
|
|
26
|
+
enforcementAttempts: number;
|
|
27
|
+
toolCalled: boolean;
|
|
28
|
+
} | null;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Published child agent sessions keyed by toolCallId.
|
|
32
|
+
* Lifecycle: executeSpawn publishes → renderSpawnResult claims via get+delete.
|
|
33
|
+
* This is only the render handoff queue, not the full live-session registry.
|
|
34
|
+
*/
|
|
35
|
+
childSessions: Map<string, AgentSession>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* All live child agent sessions keyed by toolCallId, including claimed ones.
|
|
39
|
+
* Reset/teardown aborts this registry so claimed children cannot outlive /new or UI disposal.
|
|
40
|
+
*
|
|
41
|
+
* INVARIANT: This Map is never replaced — only cleared via .clear().
|
|
42
|
+
* NestedAgentSessionComponent holds a direct reference and depends on it
|
|
43
|
+
* staying valid. If you change this, update attachSession in spawn/renderer.ts.
|
|
44
|
+
*/
|
|
45
|
+
liveChildSessions: Map<string, AgentSession>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generation counter for child-session ownership.
|
|
49
|
+
* Increment on /new so stale child updates/results cannot touch fresh state.
|
|
50
|
+
*/
|
|
51
|
+
childSessionEpoch: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Create a fresh state instance. Call reset() on /new. */
|
|
55
|
+
export function createState(): AgenticodingState {
|
|
56
|
+
const childSessions = new Map<string, AgentSession>();
|
|
57
|
+
const liveChildSessions = new Map<string, AgentSession>();
|
|
58
|
+
const state: AgenticodingState = {
|
|
59
|
+
ledger: new Map(),
|
|
60
|
+
epoch: 0,
|
|
61
|
+
lastContextPercent: null,
|
|
62
|
+
pendingHandoff: null,
|
|
63
|
+
pendingRequestedHandoff: null,
|
|
64
|
+
childSessions,
|
|
65
|
+
liveChildSessions,
|
|
66
|
+
childSessionEpoch: 0,
|
|
67
|
+
};
|
|
68
|
+
// Prevent replacement — NestedAgentSessionComponent holds direct references
|
|
69
|
+
// to both maps and depends on reference stability. Only .clear() and .delete()
|
|
70
|
+
// are valid — assigning a new Map would silently break session lifecycle.
|
|
71
|
+
Object.defineProperty(state, 'childSessions', {
|
|
72
|
+
get: () => childSessions,
|
|
73
|
+
set: () => { throw new Error('childSessions cannot be replaced — use .clear() instead'); },
|
|
74
|
+
enumerable: true,
|
|
75
|
+
configurable: false,
|
|
76
|
+
});
|
|
77
|
+
Object.defineProperty(state, 'liveChildSessions', {
|
|
78
|
+
get: () => liveChildSessions,
|
|
79
|
+
set: () => { throw new Error('liveChildSessions cannot be replaced — use .clear() instead'); },
|
|
80
|
+
enumerable: true,
|
|
81
|
+
configurable: false,
|
|
82
|
+
});
|
|
83
|
+
return state;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Reset all state. Used on /new or session reset. */
|
|
87
|
+
export function resetState(state: AgenticodingState): void {
|
|
88
|
+
state.childSessionEpoch++;
|
|
89
|
+
state.ledger.clear();
|
|
90
|
+
state.epoch = 0;
|
|
91
|
+
state.lastContextPercent = null;
|
|
92
|
+
state.pendingHandoff = null;
|
|
93
|
+
state.pendingRequestedHandoff = null;
|
|
94
|
+
abortAndClearChildSessions(state);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Abort all active child sessions and clear both registries. Called on /new (session reset). */
|
|
98
|
+
export function abortAndClearChildSessions(state: AgenticodingState): void {
|
|
99
|
+
const seen = new Map<any, string>(); // session → first id (for logging)
|
|
100
|
+
for (const [id, session] of [...state.childSessions.entries(), ...state.liveChildSessions.entries()]) {
|
|
101
|
+
if (!seen.has(session)) seen.set(session, id);
|
|
102
|
+
}
|
|
103
|
+
state.childSessions.clear();
|
|
104
|
+
state.liveChildSessions.clear();
|
|
105
|
+
for (const [session, id] of seen) {
|
|
106
|
+
session.abort().catch(e => console.warn("[spawn] abort failed:", id, e));
|
|
107
|
+
}
|
|
108
|
+
}
|
package/system-prompt.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context management system prompt primer.
|
|
3
|
+
*
|
|
4
|
+
* Injected via before_agent_start into the system prompt.
|
|
5
|
+
* Teaches the LLM about spawn, ledger, and handoff primitives.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const CONTEXT_PRIMER = `
|
|
9
|
+
## Context management
|
|
10
|
+
|
|
11
|
+
One context, one job. Research is one job. Planning is one job. Execution
|
|
12
|
+
is one job. When the job changes, call the handoff tool.
|
|
13
|
+
|
|
14
|
+
### The primacy-zone heuristic
|
|
15
|
+
You use long context unevenly. Performance can degrade as context grows —
|
|
16
|
+
even far from the window limit. Treat the first ~30% as a practical heuristic
|
|
17
|
+
for keeping the current job near the front of attention. The system tells you
|
|
18
|
+
exact context usage after each turn, and watchdog reminders may be injected
|
|
19
|
+
before LLM calls when context is past the heuristic. Watchdog reminders are
|
|
20
|
+
advisory only.
|
|
21
|
+
|
|
22
|
+
### Spawn — isolate noise
|
|
23
|
+
Delegate isolated work to child agents. They are trusted extensions of you,
|
|
24
|
+
with their own context and the same authority. You receive only condensed
|
|
25
|
+
results. Parent context stays at orchestration level. Siblings run in parallel.
|
|
26
|
+
|
|
27
|
+
### Ledger — sparse continuity cache
|
|
28
|
+
Continuously maintain the ledger while you work. After meaningful reads,
|
|
29
|
+
research, analysis, decisions, or milestones, either update/refine a ledger
|
|
30
|
+
entry now or consciously skip because nothing reusable was learned. Prefer
|
|
31
|
+
refining existing entries over creating many tiny ones. The current ledger
|
|
32
|
+
listing is available above. Reference entries by name; fetch via ledger_get on
|
|
33
|
+
demand. Never pre-load bodies.
|
|
34
|
+
|
|
35
|
+
### Handoff — complete the picture, then continue cleanly
|
|
36
|
+
When the job changes, or when context is noisy past the ~30% heuristic, use
|
|
37
|
+
handoff to finish extracting what matters from the current context before the
|
|
38
|
+
cut. Save reusable state to the ledger when useful, then draft a handoff brief
|
|
39
|
+
that preserves the important knowledge still missing from the ledger.
|
|
40
|
+
Handoff compacts the active session around that brief so the next turn starts
|
|
41
|
+
in a clean context with the right picture already in view. Full history remains
|
|
42
|
+
in the session file for the user.
|
|
43
|
+
|
|
44
|
+
Use any structure that keeps the next work unambiguous. Include the current
|
|
45
|
+
state, important findings, unresolved questions, failed paths worth avoiding,
|
|
46
|
+
next steps, refs, constraints, and spawn ideas when useful. The handoff should
|
|
47
|
+
help the next context start well without re-deriving what you already learned.
|
|
48
|
+
|
|
49
|
+
### Rules
|
|
50
|
+
- Maintain the ledger as a constant working process; do not wait for handoff
|
|
51
|
+
- Cache reusable state in the ledger; handoff should also capture the missing context that has not been recorded yet
|
|
52
|
+
- Prefer refining existing entries over creating many tiny ones
|
|
53
|
+
- After meaningful work, either update/refine the ledger or intentionally skip
|
|
54
|
+
- Reference ledger entries by name when useful; fetch bodies on demand
|
|
55
|
+
- Use spawn to delegate isolated subtasks when it helps; parent orchestrates and merges results
|
|
56
|
+
- Call handoff at job boundaries: research→execution, planning→execution
|
|
57
|
+
- Past ~30%, consider handoff when the phase is done or context noise is hurting focus
|
|
58
|
+
- After handoff, ledger_get entries as needed — not all at once
|
|
59
|
+
`.trim();
|
package/test-loader.mjs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const PACKAGE_ROOT = "/Users/ofri/.nvm/versions/node/v24.14.1/lib/node_modules/@earendil-works/pi-coding-agent";
|
|
6
|
+
const PACKAGE_ALIASES = {
|
|
7
|
+
"@earendil-works/pi-coding-agent": `${PACKAGE_ROOT}/dist/index.js`,
|
|
8
|
+
"@earendil-works/pi-ai": `${PACKAGE_ROOT}/node_modules/@earendil-works/pi-ai/dist/index.js`,
|
|
9
|
+
"@earendil-works/pi-tui": `${PACKAGE_ROOT}/node_modules/@earendil-works/pi-tui/dist/index.js`,
|
|
10
|
+
"@earendil-works/pi-agent-core": `${PACKAGE_ROOT}/node_modules/@earendil-works/pi-agent-core/dist/index.js`,
|
|
11
|
+
typebox: `${PACKAGE_ROOT}/node_modules/typebox/build/index.mjs`,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function resolve(specifier, context, defaultResolve) {
|
|
15
|
+
const packagePath = PACKAGE_ALIASES[specifier];
|
|
16
|
+
if (packagePath) {
|
|
17
|
+
return defaultResolve(pathToFileURL(packagePath).href, context, defaultResolve);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if ((specifier.startsWith("./") || specifier.startsWith("../")) && specifier.endsWith(".js") && context.parentURL) {
|
|
21
|
+
const parentPath = fileURLToPath(context.parentURL);
|
|
22
|
+
const tsPath = path.resolve(path.dirname(parentPath), specifier.slice(0, -3) + ".ts");
|
|
23
|
+
try {
|
|
24
|
+
await access(tsPath);
|
|
25
|
+
return defaultResolve(pathToFileURL(tsPath).href, context, defaultResolve);
|
|
26
|
+
} catch {
|
|
27
|
+
// fall through
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return defaultResolve(specifier, context, defaultResolve);
|
|
32
|
+
}
|
package/watchdog.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watchdog: advisory primacy-zone reminder.
|
|
3
|
+
*
|
|
4
|
+
* Exposes nudge text generation and records the latest context usage at
|
|
5
|
+
* `agent_end` for UI/state purposes. Actual reminder injection happens in the
|
|
6
|
+
* `context` hook so it can appear before every LLM call in the same agent run.
|
|
7
|
+
*
|
|
8
|
+
* Never force-disengages — the watchdog is advisory only.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import type { AgenticodingState } from "./state.js";
|
|
13
|
+
|
|
14
|
+
/** Build a nudge string with the exact percent interpolated. */
|
|
15
|
+
export function buildNudge(percent: number): string {
|
|
16
|
+
const pct = Math.round(percent);
|
|
17
|
+
|
|
18
|
+
if (pct >= 70) {
|
|
19
|
+
return `Context at ${pct}% — deep in the degraded zone. Compaction may trigger soon
|
|
20
|
+
(emergency summarization at ~90%). Prefer a deliberate handoff now: save
|
|
21
|
+
reusable state to the ledger, draft a clear next-task brief, and call handoff.`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (pct >= 50) {
|
|
25
|
+
return `Context at ${pct}% — well past the primacy-zone heuristic. If the current job is
|
|
26
|
+
done or the context is noisy, consider a handoff soon. Save reusable state to
|
|
27
|
+
the ledger and draft a concise but sufficiently detailed brief for what comes
|
|
28
|
+
next.`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 30-50%
|
|
32
|
+
return `Context at ${pct}% — past the primacy-zone heuristic. One context, one job.
|
|
33
|
+
If you're mid-job and still clear, continue. If the current phase is complete
|
|
34
|
+
or the context is noisy, consider a handoff and draft a clear brief for what
|
|
35
|
+
comes next.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register the watchdog's `agent_end` handler.
|
|
40
|
+
*
|
|
41
|
+
* Must be called from the extension factory in index.ts after state creation.
|
|
42
|
+
*/
|
|
43
|
+
export function registerWatchdog(pi: ExtensionAPI, state: AgenticodingState): void {
|
|
44
|
+
pi.on("agent_end", async (_event: unknown, ctx: ExtensionContext) => {
|
|
45
|
+
const requestedHandoff = state.pendingRequestedHandoff;
|
|
46
|
+
if (requestedHandoff) {
|
|
47
|
+
requestedHandoff.enforcementAttempts += 1;
|
|
48
|
+
if (!requestedHandoff.toolCalled) {
|
|
49
|
+
state.pendingRequestedHandoff = null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Primacy-zone nudge ──────────────────────────────────────
|
|
54
|
+
const usage = ctx.getContextUsage();
|
|
55
|
+
|
|
56
|
+
// Null usage / null percent — right after compaction, before next LLM response.
|
|
57
|
+
if (!usage || usage.percent === null) {
|
|
58
|
+
state.lastContextPercent = null;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
state.lastContextPercent = usage.percent;
|
|
63
|
+
|
|
64
|
+
});
|
|
65
|
+
}
|