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.
@@ -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
+ }
@@ -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();
@@ -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
+ }