shariq-pi-extensions 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 +94 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/docs/ARCHITECTURE.md +54 -0
- package/docs/DEVELOPMENT.md +88 -0
- package/docs/EXTENSIONS.md +89 -0
- package/extensions/antigravity-provider/LICENSE +21 -0
- package/extensions/antigravity-provider/README.md +23 -0
- package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
- package/extensions/antigravity-provider/antigravity/index.ts +67 -0
- package/extensions/antigravity-provider/antigravity/models.ts +184 -0
- package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
- package/extensions/antigravity-provider/index.ts +3 -0
- package/extensions/ask-user/README.md +21 -0
- package/extensions/ask-user/index.ts +417 -0
- package/extensions/background-terminals/README.md +43 -0
- package/extensions/background-terminals/index.ts +400 -0
- package/extensions/background-terminals/src/manager.ts +480 -0
- package/extensions/background-terminals/src/output-buffer.ts +110 -0
- package/extensions/background-terminals/src/presentation.ts +74 -0
- package/extensions/background-terminals/src/types.ts +47 -0
- package/extensions/background-terminals/src/ui.ts +478 -0
- package/extensions/context-usage/LICENSE +21 -0
- package/extensions/context-usage/README.md +20 -0
- package/extensions/context-usage/index.ts +2006 -0
- package/extensions/context-usage/lib/ansi.ts +60 -0
- package/extensions/context-usage/lib/boundary.ts +27 -0
- package/extensions/context-usage/lib/chat.ts +190 -0
- package/extensions/context-usage/lib/config.ts +30 -0
- package/extensions/context-usage/lib/fmt.ts +59 -0
- package/extensions/context-usage/lib/style.ts +260 -0
- package/extensions/copy-all/README.md +15 -0
- package/extensions/copy-all/index.ts +72 -0
- package/extensions/factory-provider/API_KEYS.md +17 -0
- package/extensions/factory-provider/AUTH.md +15 -0
- package/extensions/factory-provider/README.md +90 -0
- package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
- package/extensions/factory-provider/UPDATING.md +97 -0
- package/extensions/factory-provider/factory/api-keys.ts +407 -0
- package/extensions/factory-provider/factory/auth.ts +132 -0
- package/extensions/factory-provider/factory/constants.ts +15 -0
- package/extensions/factory-provider/factory/dashboard.ts +363 -0
- package/extensions/factory-provider/factory/droid.ts +84 -0
- package/extensions/factory-provider/factory/limits.ts +342 -0
- package/extensions/factory-provider/factory/models.ts +283 -0
- package/extensions/factory-provider/factory/responses.ts +331 -0
- package/extensions/factory-provider/factory/warm-theme.ts +53 -0
- package/extensions/factory-provider/factory/websocket.ts +130 -0
- package/extensions/factory-provider/index.ts +204 -0
- package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
- package/extensions/firecrawl-web/README.md +31 -0
- package/extensions/firecrawl-web/auth.ts +92 -0
- package/extensions/firecrawl-web/client.ts +238 -0
- package/extensions/firecrawl-web/index.ts +96 -0
- package/extensions/firecrawl-web/output.ts +125 -0
- package/extensions/git-info/README.md +25 -0
- package/extensions/git-info/index.ts +162 -0
- package/extensions/git-info/src/git.ts +206 -0
- package/extensions/git-info/src/ui.ts +291 -0
- package/extensions/goal/README.md +44 -0
- package/extensions/goal/constants.ts +4 -0
- package/extensions/goal/extension.ts +1064 -0
- package/extensions/goal/index.ts +2 -0
- package/extensions/goal/types.ts +56 -0
- package/extensions/goal/ui.ts +334 -0
- package/extensions/orchestration/README.md +58 -0
- package/extensions/orchestration/dashboard.ts +192 -0
- package/extensions/orchestration/engine.ts +706 -0
- package/extensions/orchestration/index.ts +243 -0
- package/extensions/orchestration/prompts.ts +124 -0
- package/extensions/orchestration/protocol.ts +116 -0
- package/extensions/orchestration/settings-ui.ts +41 -0
- package/extensions/orchestration/settings.ts +59 -0
- package/extensions/orchestration/storage.ts +49 -0
- package/extensions/orchestration/types.ts +105 -0
- package/extensions/performance-status/README.md +5 -0
- package/extensions/performance-status/index.ts +171 -0
- package/extensions/pi-memory/README.md +53 -0
- package/extensions/pi-memory/index.ts +326 -0
- package/extensions/pi-memory/src/config.ts +75 -0
- package/extensions/pi-memory/src/database.ts +617 -0
- package/extensions/pi-memory/src/extraction.ts +178 -0
- package/extensions/pi-memory/src/project.ts +80 -0
- package/extensions/pi-memory/src/projection.ts +59 -0
- package/extensions/pi-memory/src/redaction.ts +60 -0
- package/extensions/pi-memory/src/retrieval.ts +30 -0
- package/extensions/pi-memory/src/service.ts +194 -0
- package/extensions/pi-memory/src/session.ts +146 -0
- package/extensions/pi-memory/src/types.ts +82 -0
- package/extensions/shared/README.md +18 -0
- package/extensions/shared/activity-status.ts +31 -0
- package/extensions/shared/child-session.ts +150 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +99 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/tui-dashboard.ts +139 -0
- package/extensions/shell-shortcuts/README.md +17 -0
- package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
- package/extensions/shell-shortcuts/index.ts +6 -0
- package/extensions/subagents/README.md +89 -0
- package/extensions/subagents/index.ts +1386 -0
- package/extensions/subagents/src/backend.ts +69 -0
- package/extensions/subagents/src/backends/pi.ts +781 -0
- package/extensions/subagents/src/catalog.ts +85 -0
- package/extensions/subagents/src/config.ts +241 -0
- package/extensions/subagents/src/context.ts +57 -0
- package/extensions/subagents/src/coordinator.ts +64 -0
- package/extensions/subagents/src/domain.ts +294 -0
- package/extensions/subagents/src/format.ts +74 -0
- package/extensions/subagents/src/manager.ts +813 -0
- package/extensions/subagents/src/prompt.ts +94 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +48 -0
- package/extensions/subagents/src/ui/takeover.ts +695 -0
- package/extensions/subagents/src/ui/transcript.ts +182 -0
- package/extensions/subagents/src/worktree.ts +175 -0
- package/extensions/web-fetch/README.md +13 -0
- package/extensions/web-fetch/index.ts +441 -0
- package/package.json +117 -0
- package/skills/background-terminals/SKILL.md +42 -0
- package/skills/orchestration/SKILL.md +33 -0
- package/skills/subagents/SKILL.md +89 -0
- package/themes/ember-warm-dark.json +85 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ReasoningEffort } from "../subagents/src/domain.ts";
|
|
2
|
+
|
|
3
|
+
export const ORCHESTRATION_ROLES = [
|
|
4
|
+
"orchestrator",
|
|
5
|
+
"explorer",
|
|
6
|
+
"frontend",
|
|
7
|
+
"backend",
|
|
8
|
+
"general",
|
|
9
|
+
"reviewer",
|
|
10
|
+
] as const;
|
|
11
|
+
export type OrchestrationRole = (typeof ORCHESTRATION_ROLES)[number];
|
|
12
|
+
|
|
13
|
+
export interface RoleSettings {
|
|
14
|
+
model?: string;
|
|
15
|
+
thinking: ReasoningEffort;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface OrchestrationSettings {
|
|
19
|
+
version: 1;
|
|
20
|
+
maxWorkersPerProject: 10;
|
|
21
|
+
roles: Record<OrchestrationRole, RoleSettings>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type TaskStatus =
|
|
25
|
+
| "pending"
|
|
26
|
+
| "implementing"
|
|
27
|
+
| "reviewing"
|
|
28
|
+
| "integrating"
|
|
29
|
+
| "completed"
|
|
30
|
+
| "blocked"
|
|
31
|
+
| "cancelled";
|
|
32
|
+
|
|
33
|
+
export interface OrchestrationTask {
|
|
34
|
+
id: string;
|
|
35
|
+
title: string;
|
|
36
|
+
description: string;
|
|
37
|
+
role: Exclude<OrchestrationRole, "orchestrator" | "reviewer">;
|
|
38
|
+
dependencies: string[];
|
|
39
|
+
acceptanceCriteria: string[];
|
|
40
|
+
status: TaskStatus;
|
|
41
|
+
workerAgentId?: string;
|
|
42
|
+
reviewerAgentId?: string;
|
|
43
|
+
fixRounds: number;
|
|
44
|
+
lastWorkerSettledAt?: number;
|
|
45
|
+
lastReviewerSettledAt?: number;
|
|
46
|
+
reviewSummary?: string;
|
|
47
|
+
filesChanged?: string[];
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type RunStatus =
|
|
52
|
+
| "planning"
|
|
53
|
+
| "awaiting-approval"
|
|
54
|
+
| "running"
|
|
55
|
+
| "paused"
|
|
56
|
+
| "blocked"
|
|
57
|
+
| "completed"
|
|
58
|
+
| "cancelled"
|
|
59
|
+
| "interrupted";
|
|
60
|
+
|
|
61
|
+
export interface OrchestrationRun {
|
|
62
|
+
id: string;
|
|
63
|
+
objective: string;
|
|
64
|
+
cwd: string;
|
|
65
|
+
projectKey: string;
|
|
66
|
+
gitBacked: boolean;
|
|
67
|
+
status: RunStatus;
|
|
68
|
+
summary: string;
|
|
69
|
+
tasks: OrchestrationTask[];
|
|
70
|
+
orchestratorAgentId?: string;
|
|
71
|
+
orchestratorHandledAt?: number;
|
|
72
|
+
finalReviewerAgentId?: string;
|
|
73
|
+
finalReviewerHandledAt?: number;
|
|
74
|
+
finalReviewPassed?: boolean;
|
|
75
|
+
finalReviewSummary?: string;
|
|
76
|
+
lastCoordinatedTaskState?: string;
|
|
77
|
+
createdAt: number;
|
|
78
|
+
updatedAt: number;
|
|
79
|
+
completedAt?: number;
|
|
80
|
+
error?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface PlannedTask {
|
|
84
|
+
id: string;
|
|
85
|
+
title: string;
|
|
86
|
+
description: string;
|
|
87
|
+
role: OrchestrationTask["role"];
|
|
88
|
+
dependencies?: string[];
|
|
89
|
+
acceptanceCriteria?: string[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface OrchestratorDecision {
|
|
93
|
+
decision: "continue" | "complete" | "blocked";
|
|
94
|
+
summary: string;
|
|
95
|
+
tasks?: PlannedTask[];
|
|
96
|
+
blocker?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ReviewDecision {
|
|
100
|
+
passed: boolean;
|
|
101
|
+
summary: string;
|
|
102
|
+
smallFixesApplied?: string[];
|
|
103
|
+
findings?: string[];
|
|
104
|
+
validation?: string[];
|
|
105
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Performance Status
|
|
2
|
+
|
|
3
|
+
Adds one compact footer-area status row for the current or latest assistant response.
|
|
4
|
+
|
|
5
|
+
It reports TPS, time to first token, elapsed response time, response output tokens, and the active tool. Live token counts and TPS are estimates until the provider returns final usage. Final TPS uses conventional decode throughput: provider-reported output tokens divided by first-token-to-message-completion time. TTFT and total elapsed time separately expose provider latency, prefill, and hidden reasoning.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
interface GenerationMetrics {
|
|
7
|
+
requestStartedAt?: number;
|
|
8
|
+
firstTokenAt?: number;
|
|
9
|
+
completedAt?: number;
|
|
10
|
+
streamedChars: number;
|
|
11
|
+
exactOutputTokens?: number;
|
|
12
|
+
activeTool?: string;
|
|
13
|
+
phase: "idle" | "waiting" | "generating" | "tool" | "done";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const STATUS_ID = "generation-performance";
|
|
17
|
+
const RENDER_INTERVAL_MS = 100;
|
|
18
|
+
|
|
19
|
+
function duration(seconds: number) {
|
|
20
|
+
if (!Number.isFinite(seconds) || seconds < 0) return "—";
|
|
21
|
+
return seconds < 10 ? `${seconds.toFixed(1)}s` : `${Math.round(seconds)}s`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function tokenCount(tokens: number, estimated = false) {
|
|
25
|
+
const value = tokens < 1_000
|
|
26
|
+
? `${Math.max(0, Math.round(tokens))}`
|
|
27
|
+
: `${(tokens / 1_000).toFixed(1)}k`;
|
|
28
|
+
return `${estimated ? "~" : ""}${value}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function values(metrics: GenerationMetrics, now = Date.now()) {
|
|
32
|
+
const elapsed = metrics.requestStartedAt
|
|
33
|
+
? Math.max(0, (Math.min(metrics.completedAt ?? now, now) - metrics.requestStartedAt) / 1_000)
|
|
34
|
+
: undefined;
|
|
35
|
+
const ttft = metrics.requestStartedAt && metrics.firstTokenAt
|
|
36
|
+
? Math.max(0, (metrics.firstTokenAt - metrics.requestStartedAt) / 1_000)
|
|
37
|
+
: undefined;
|
|
38
|
+
const estimatedTokens = metrics.streamedChars / 4;
|
|
39
|
+
const outputTokens = metrics.exactOutputTokens ?? estimatedTokens;
|
|
40
|
+
// Conventional TPS measures decode throughput from first streamed token to
|
|
41
|
+
// completion. TTFT and elapsed time separately expose provider latency,
|
|
42
|
+
// prefill, and hidden reasoning before visible streaming begins.
|
|
43
|
+
const generationSeconds = metrics.firstTokenAt
|
|
44
|
+
? Math.max(0, ((metrics.completedAt ?? now) - metrics.firstTokenAt) / 1_000)
|
|
45
|
+
: undefined;
|
|
46
|
+
const tps = generationSeconds && generationSeconds > 0.05
|
|
47
|
+
? outputTokens / generationSeconds
|
|
48
|
+
: undefined;
|
|
49
|
+
return {
|
|
50
|
+
elapsed,
|
|
51
|
+
ttft,
|
|
52
|
+
outputTokens,
|
|
53
|
+
estimated: metrics.exactOutputTokens === undefined,
|
|
54
|
+
tps,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function formatPerformanceStatus(
|
|
59
|
+
metrics: GenerationMetrics,
|
|
60
|
+
ctx: ExtensionContext,
|
|
61
|
+
now = Date.now(),
|
|
62
|
+
) {
|
|
63
|
+
const theme = ctx.ui.theme;
|
|
64
|
+
const measured = values(metrics, now);
|
|
65
|
+
const label = metrics.phase === "waiting"
|
|
66
|
+
? "waiting"
|
|
67
|
+
: metrics.phase === "generating"
|
|
68
|
+
? "generating"
|
|
69
|
+
: metrics.phase === "tool"
|
|
70
|
+
? `tool ${metrics.activeTool ?? "running"}`
|
|
71
|
+
: "last response";
|
|
72
|
+
const parts = [
|
|
73
|
+
`${theme.fg("accent", "◆")} ${theme.fg(metrics.phase === "done" ? "muted" : "accent", label)}`,
|
|
74
|
+
measured.tps === undefined
|
|
75
|
+
? undefined
|
|
76
|
+
: theme.fg("text", `TPS ${measured.estimated ? "~" : ""}${Math.round(measured.tps)} tok/s`),
|
|
77
|
+
measured.ttft === undefined ? undefined : theme.fg("muted", `TTFT ${duration(measured.ttft)}`),
|
|
78
|
+
measured.elapsed === undefined ? undefined : theme.fg("muted", duration(measured.elapsed)),
|
|
79
|
+
measured.outputTokens > 0
|
|
80
|
+
? theme.fg("muted", `${tokenCount(measured.outputTokens, measured.estimated)} out`)
|
|
81
|
+
: undefined,
|
|
82
|
+
].filter((part): part is string => Boolean(part));
|
|
83
|
+
return parts.join(theme.fg("dim", " · "));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default function performanceStatus(pi: ExtensionAPI) {
|
|
87
|
+
const metrics: GenerationMetrics = { streamedChars: 0, phase: "idle" };
|
|
88
|
+
let renderTimer: ReturnType<typeof setTimeout> | undefined;
|
|
89
|
+
let lastRenderAt = 0;
|
|
90
|
+
|
|
91
|
+
const render = (ctx: ExtensionContext, immediate = false) => {
|
|
92
|
+
if (!ctx.hasUI || metrics.phase === "idle") return;
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
const remaining = RENDER_INTERVAL_MS - (now - lastRenderAt);
|
|
95
|
+
if (!immediate && remaining > 0) {
|
|
96
|
+
if (!renderTimer) {
|
|
97
|
+
renderTimer = setTimeout(() => {
|
|
98
|
+
renderTimer = undefined;
|
|
99
|
+
lastRenderAt = Date.now();
|
|
100
|
+
ctx.ui.setStatus(STATUS_ID, formatPerformanceStatus(metrics, ctx));
|
|
101
|
+
}, remaining);
|
|
102
|
+
renderTimer.unref?.();
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (renderTimer) clearTimeout(renderTimer);
|
|
107
|
+
renderTimer = undefined;
|
|
108
|
+
lastRenderAt = now;
|
|
109
|
+
ctx.ui.setStatus(STATUS_ID, formatPerformanceStatus(metrics, ctx, now));
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
113
|
+
metrics.requestStartedAt = Date.now();
|
|
114
|
+
metrics.firstTokenAt = undefined;
|
|
115
|
+
metrics.completedAt = undefined;
|
|
116
|
+
metrics.streamedChars = 0;
|
|
117
|
+
metrics.exactOutputTokens = undefined;
|
|
118
|
+
metrics.activeTool = undefined;
|
|
119
|
+
metrics.phase = "waiting";
|
|
120
|
+
render(ctx, true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
pi.on("message_update", (event, ctx) => {
|
|
124
|
+
if (event.message.role !== "assistant") return;
|
|
125
|
+
const update = event.assistantMessageEvent;
|
|
126
|
+
if (update.type === "text_delta" || update.type === "thinking_delta" || update.type === "toolcall_delta") {
|
|
127
|
+
metrics.firstTokenAt ??= Date.now();
|
|
128
|
+
metrics.streamedChars += update.delta.length;
|
|
129
|
+
metrics.phase = "generating";
|
|
130
|
+
render(ctx);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (update.type === "done") {
|
|
134
|
+
metrics.firstTokenAt ??= metrics.requestStartedAt ?? Date.now();
|
|
135
|
+
metrics.completedAt = Date.now();
|
|
136
|
+
metrics.exactOutputTokens = update.message.usage.output;
|
|
137
|
+
metrics.phase = "done";
|
|
138
|
+
render(ctx, true);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
pi.on("message_end", (event, ctx) => {
|
|
143
|
+
if (event.message.role !== "assistant") return;
|
|
144
|
+
metrics.completedAt ??= Date.now();
|
|
145
|
+
metrics.exactOutputTokens = event.message.usage.output;
|
|
146
|
+
metrics.phase = "done";
|
|
147
|
+
render(ctx, true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
151
|
+
metrics.activeTool = event.toolName;
|
|
152
|
+
metrics.phase = "tool";
|
|
153
|
+
render(ctx, true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
pi.on("tool_execution_end", (_event, ctx) => {
|
|
157
|
+
metrics.activeTool = undefined;
|
|
158
|
+
metrics.phase = metrics.completedAt ? "done" : "generating";
|
|
159
|
+
render(ctx, true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
pi.on("session_start", (_event, ctx) => {
|
|
163
|
+
if (ctx.hasUI) ctx.ui.setStatus(STATUS_ID, undefined);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
167
|
+
if (renderTimer) clearTimeout(renderTimer);
|
|
168
|
+
renderTimer = undefined;
|
|
169
|
+
if (ctx.hasUI) ctx.ui.setStatus(STATUS_ID, undefined);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# pi-memory
|
|
2
|
+
|
|
3
|
+
Pi-native durable memory built only from work performed inside Pi.
|
|
4
|
+
|
|
5
|
+
## Boundaries
|
|
6
|
+
|
|
7
|
+
- Captures Pi session messages, project identity, compaction summaries, explicit memory edits, and only an allowlist of Pi-native coding/terminal/subagent tool events.
|
|
8
|
+
- Does not ingest web/browser/search, screenshot/OCR, screen, audio, camera, application-observation, Chronicle, Skysight, or passive computer-recording tool events.
|
|
9
|
+
- Uses SQLite as the source of truth. The two generated `MEMORY.md` surfaces are read-only projections for people:
|
|
10
|
+
- `<agent-dir>/pi-memory/global/MEMORY.md`
|
|
11
|
+
- `<agent-dir>/pi-memory/projects/<project-slug>-<project-hash-prefix>/MEMORY.md`
|
|
12
|
+
- Runs only in TUI/RPC sessions, so headless children and print/JSON jobs do not create memory.
|
|
13
|
+
- Persists its extraction provider, model, and reasoning in `<agent-dir>/pi-memory/config.json`; `/memory-model` changes all three. The initial default is `openai-codex/gpt-5.6-luna` at `max` reasoning.
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
- `src/database.ts` — schema, FTS5 search, ranking, leases, retries, provenance, and lifecycle.
|
|
18
|
+
- `src/session.ts` — branch-aware Pi session capture with bounded tool output.
|
|
19
|
+
- `src/extraction.ts` — no-tool structured extraction through Pi's model registry.
|
|
20
|
+
- `src/retrieval.ts` — scoped retrieval and character-bounded estimated context injection.
|
|
21
|
+
- `src/projection.ts` — generated global/project Markdown views.
|
|
22
|
+
- `src/service.ts` — lifecycle and queue ownership.
|
|
23
|
+
|
|
24
|
+
The completed bootstrap import is detached from its former sources. Imported knowledge is owned by Pi Memory as `bootstrap-import` provenance with Pi-local opaque identifiers. Runtime code never scans or reads another agent's memory directories.
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
- `/memory-status`
|
|
29
|
+
- `/memory-model` (interactive provider → model → reasoning selector)
|
|
30
|
+
- `/memory-model <provider>/<model> <reasoning>`
|
|
31
|
+
- `/memory-search <query>`
|
|
32
|
+
- `/memory-review [query]`
|
|
33
|
+
- `/memory-save [global|project] <text>`
|
|
34
|
+
- `/memory-correct <id> <replacement text>`
|
|
35
|
+
- `/memory-forget <id>`
|
|
36
|
+
- `/memory-rebuild`
|
|
37
|
+
- `/memory-process`
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
- `pi_memory_search`
|
|
42
|
+
- `pi_memory_read`
|
|
43
|
+
- `pi_memory_save`
|
|
44
|
+
- `pi_memory_correct`
|
|
45
|
+
- `pi_memory_forget`
|
|
46
|
+
- `pi_memory_status`
|
|
47
|
+
|
|
48
|
+
## Validation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm run check
|
|
52
|
+
npm test
|
|
53
|
+
```
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import {
|
|
5
|
+
defaultConfig,
|
|
6
|
+
MEMORY_REASONING_LEVELS,
|
|
7
|
+
saveMemoryModelConfig,
|
|
8
|
+
type MemoryModelConfig,
|
|
9
|
+
type MemoryReasoningLevel,
|
|
10
|
+
} from "./src/config.ts";
|
|
11
|
+
import { formatMemoryContext, selectForInjection } from "./src/retrieval.ts";
|
|
12
|
+
import { MemoryService } from "./src/service.ts";
|
|
13
|
+
import { MEMORY_KINDS, type MemoryKind, type MemoryRecord, type MemoryScope } from "./src/types.ts";
|
|
14
|
+
|
|
15
|
+
function toolText(text: string, details: Record<string, unknown> = {}) {
|
|
16
|
+
return { content: [{ type: "text" as const, text }], details };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatSearchResults(results: MemoryRecord[]): string {
|
|
20
|
+
if (results.length === 0) return "No matching memories.";
|
|
21
|
+
return results.map((memory) =>
|
|
22
|
+
`${memory.memoryId} ${memory.scope}/${memory.kind} ${memory.title}\n${memory.content}`,
|
|
23
|
+
).join("\n\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MEMORY_TOOL_NAMES = new Set([
|
|
27
|
+
"pi_memory_search",
|
|
28
|
+
"pi_memory_read",
|
|
29
|
+
"pi_memory_save",
|
|
30
|
+
"pi_memory_correct",
|
|
31
|
+
"pi_memory_forget",
|
|
32
|
+
"pi_memory_status",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export default function piMemory(pi: ExtensionAPI) {
|
|
36
|
+
const config = defaultConfig();
|
|
37
|
+
const service = new MemoryService(config);
|
|
38
|
+
|
|
39
|
+
const active = (ctx: ExtensionContext): boolean => config.enabledModes.has(ctx.mode);
|
|
40
|
+
const projectFor = (ctx: Pick<ExtensionContext, "cwd">) => service.project(ctx.cwd);
|
|
41
|
+
|
|
42
|
+
pi.registerTool({
|
|
43
|
+
name: "pi_memory_search",
|
|
44
|
+
label: "Search Pi Memory",
|
|
45
|
+
description: "Search durable global and current-project memories. Returns at most the requested number of concise records.",
|
|
46
|
+
promptSnippet: "Search durable Pi memories for deeper historical context",
|
|
47
|
+
promptGuidelines: ["Use pi_memory_search when auto-injected memory is insufficient and past Pi work could materially help."],
|
|
48
|
+
parameters: Type.Object({
|
|
49
|
+
query: Type.String({ minLength: 1, description: "What to find" }),
|
|
50
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 30 })),
|
|
51
|
+
}),
|
|
52
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
53
|
+
signal?.throwIfAborted();
|
|
54
|
+
const project = projectFor(ctx);
|
|
55
|
+
const results = service.search(params.query, project.id, params.limit ?? 10);
|
|
56
|
+
service.database.recordAccess(results.map((memory) => memory.memoryId));
|
|
57
|
+
return toolText(formatSearchResults(results), { count: results.length, memoryIds: results.map((memory) => memory.memoryId) });
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
pi.registerTool({
|
|
62
|
+
name: "pi_memory_read",
|
|
63
|
+
label: "Read Pi Memory",
|
|
64
|
+
description: "Read one durable memory by exact ID, including scope, provenance, confidence, and timestamps.",
|
|
65
|
+
parameters: Type.Object({ memoryId: Type.String({ minLength: 1 }) }),
|
|
66
|
+
async execute(_id, params, signal) {
|
|
67
|
+
signal?.throwIfAborted();
|
|
68
|
+
const memory = service.database.getMemory(params.memoryId);
|
|
69
|
+
if (!memory || memory.status !== "active") throw new Error(`Active memory not found: ${params.memoryId}`);
|
|
70
|
+
service.database.recordAccess([memory.memoryId]);
|
|
71
|
+
const sources = service.database.listSources(memory.memoryId);
|
|
72
|
+
return toolText(JSON.stringify({ ...memory, sources }, null, 2), { memoryId: memory.memoryId, sourceCount: sources.length });
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
pi.registerTool({
|
|
77
|
+
name: "pi_memory_save",
|
|
78
|
+
label: "Save Pi Memory",
|
|
79
|
+
description: "Save one explicit durable memory. Use global only for stable cross-project preferences; otherwise use project.",
|
|
80
|
+
parameters: Type.Object({
|
|
81
|
+
scope: StringEnum(["global", "project"] as const),
|
|
82
|
+
kind: StringEnum(MEMORY_KINDS),
|
|
83
|
+
title: Type.String({ minLength: 1, maxLength: 160 }),
|
|
84
|
+
content: Type.String({ minLength: 1, maxLength: 4000 }),
|
|
85
|
+
tags: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 80 }), { maxItems: 16 })),
|
|
86
|
+
importance: Type.Optional(Type.Integer({ minimum: 1, maximum: 5 })),
|
|
87
|
+
}),
|
|
88
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
89
|
+
signal?.throwIfAborted();
|
|
90
|
+
const memory = service.save({
|
|
91
|
+
scope: params.scope,
|
|
92
|
+
project: projectFor(ctx),
|
|
93
|
+
kind: params.kind,
|
|
94
|
+
title: params.title,
|
|
95
|
+
content: params.content,
|
|
96
|
+
...(params.tags ? { tags: params.tags } : {}),
|
|
97
|
+
...(params.importance ? { importance: params.importance } : {}),
|
|
98
|
+
});
|
|
99
|
+
return toolText(`Saved ${memory.scope} memory ${memory.memoryId}: ${memory.title}`, { memoryId: memory.memoryId });
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
pi.registerTool({
|
|
104
|
+
name: "pi_memory_correct",
|
|
105
|
+
label: "Correct Pi Memory",
|
|
106
|
+
description: "Correct an existing durable memory by exact memory ID. Use only when the user corrects stored information.",
|
|
107
|
+
parameters: Type.Object({
|
|
108
|
+
memoryId: Type.String({ minLength: 1 }),
|
|
109
|
+
title: Type.Optional(Type.String({ minLength: 1, maxLength: 160 })),
|
|
110
|
+
content: Type.Optional(Type.String({ minLength: 1, maxLength: 4000 })),
|
|
111
|
+
tags: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 80 }), { maxItems: 16 })),
|
|
112
|
+
importance: Type.Optional(Type.Integer({ minimum: 1, maximum: 5 })),
|
|
113
|
+
}),
|
|
114
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
115
|
+
signal?.throwIfAborted();
|
|
116
|
+
const memory = service.correct(params.memoryId, {
|
|
117
|
+
project: projectFor(ctx),
|
|
118
|
+
...(params.title ? { title: params.title } : {}),
|
|
119
|
+
...(params.content ? { content: params.content } : {}),
|
|
120
|
+
...(params.tags ? { tags: params.tags } : {}),
|
|
121
|
+
...(params.importance ? { importance: params.importance } : {}),
|
|
122
|
+
});
|
|
123
|
+
if (!memory) throw new Error(`Active memory not found: ${params.memoryId}`);
|
|
124
|
+
return toolText(`Corrected memory ${memory.memoryId}: ${memory.title}`, { memoryId: memory.memoryId });
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
pi.registerTool({
|
|
129
|
+
name: "pi_memory_forget",
|
|
130
|
+
label: "Forget Pi Memory",
|
|
131
|
+
description: "Soft-delete one durable memory by exact ID. Use only on an explicit user request to forget it.",
|
|
132
|
+
parameters: Type.Object({ memoryId: Type.String({ minLength: 1 }) }),
|
|
133
|
+
async execute(_id, params, signal) {
|
|
134
|
+
signal?.throwIfAborted();
|
|
135
|
+
if (!service.forget(params.memoryId)) throw new Error(`Active memory not found: ${params.memoryId}`);
|
|
136
|
+
return toolText(`Forgot memory ${params.memoryId}.`, { memoryId: params.memoryId });
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
pi.registerTool({
|
|
141
|
+
name: "pi_memory_status",
|
|
142
|
+
label: "Pi Memory Status",
|
|
143
|
+
description: "Show Pi Memory counts, queue state, and storage paths.",
|
|
144
|
+
parameters: Type.Object({}),
|
|
145
|
+
async execute() {
|
|
146
|
+
const status = service.status();
|
|
147
|
+
return toolText(JSON.stringify(status, null, 2), status);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
pi.registerCommand("memory-status", {
|
|
152
|
+
description: "Show Pi Memory status",
|
|
153
|
+
handler: async (_args, ctx) => ctx.ui.notify(JSON.stringify(service.status()), "info"),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
pi.registerCommand("memory-model", {
|
|
157
|
+
description: "Select the provider, model, and reasoning level used only for Pi Memory extraction",
|
|
158
|
+
handler: async (args, ctx) => {
|
|
159
|
+
const available = ctx.modelRegistry.getAvailable();
|
|
160
|
+
let selected: MemoryModelConfig | undefined;
|
|
161
|
+
const direct = args.trim().match(/^(\S+)\/(\S+)\s+(off|minimal|low|medium|high|xhigh|max)$/);
|
|
162
|
+
|
|
163
|
+
if (direct) {
|
|
164
|
+
const [, provider, model, reasoning] = direct;
|
|
165
|
+
const match = available.find((candidate) => candidate.provider === provider && candidate.id === model);
|
|
166
|
+
if (!match) {
|
|
167
|
+
ctx.ui.notify(`Unavailable memory model: ${provider}/${model}`, "error");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (!match.reasoning && reasoning !== "off") {
|
|
171
|
+
ctx.ui.notify(`${provider}/${model} does not support reasoning; select off.`, "error");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
selected = { provider: provider!, model: model!, reasoning: reasoning as MemoryReasoningLevel };
|
|
175
|
+
} else if (args.trim()) {
|
|
176
|
+
ctx.ui.notify("Usage: /memory-model [provider/model reasoning]", "warning");
|
|
177
|
+
return;
|
|
178
|
+
} else {
|
|
179
|
+
if (!ctx.hasUI) {
|
|
180
|
+
ctx.ui.notify("Usage: /memory-model <provider/model> <reasoning>", "warning");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const current = config.extractionModel;
|
|
184
|
+
const providers = [...new Set(available.map((model) => model.provider))].sort();
|
|
185
|
+
providers.sort((a, b) => Number(b === current.provider) - Number(a === current.provider));
|
|
186
|
+
const provider = await ctx.ui.select("Pi Memory extraction provider", providers);
|
|
187
|
+
if (!provider) return;
|
|
188
|
+
|
|
189
|
+
const providerModels = available.filter((model) => model.provider === provider);
|
|
190
|
+
providerModels.sort((a, b) => Number(b.id === current.model && provider === current.provider) - Number(a.id === current.model && provider === current.provider) || a.id.localeCompare(b.id));
|
|
191
|
+
const modelId = await ctx.ui.select("Pi Memory extraction model", providerModels.map((model) => model.id));
|
|
192
|
+
if (!modelId) return;
|
|
193
|
+
const model = providerModels.find((candidate) => candidate.id === modelId)!;
|
|
194
|
+
|
|
195
|
+
const levels = model.reasoning ? [...MEMORY_REASONING_LEVELS] : ["off" as const];
|
|
196
|
+
levels.sort((a, b) => Number(b === current.reasoning) - Number(a === current.reasoning));
|
|
197
|
+
const reasoning = await ctx.ui.select("Pi Memory extraction reasoning", levels);
|
|
198
|
+
if (!reasoning) return;
|
|
199
|
+
selected = { provider, model: modelId, reasoning: reasoning as MemoryReasoningLevel };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
config.extractionModel = selected;
|
|
203
|
+
saveMemoryModelConfig(config.root, selected);
|
|
204
|
+
ctx.ui.notify(`Pi Memory extraction: ${selected.provider}/${selected.model} (${selected.reasoning})`, "info");
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
pi.registerCommand("memory-search", {
|
|
209
|
+
description: "Search Pi Memory: /memory-search <query>",
|
|
210
|
+
handler: async (args, ctx) => {
|
|
211
|
+
const query = args.trim();
|
|
212
|
+
if (!query) { ctx.ui.notify("Usage: /memory-search <query>", "warning"); return; }
|
|
213
|
+
const results = service.search(query, projectFor(ctx).id, 10);
|
|
214
|
+
service.database.recordAccess(results.map((memory) => memory.memoryId));
|
|
215
|
+
ctx.ui.notify(formatSearchResults(results).slice(0, 8_000), "info");
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
pi.registerCommand("memory-review", {
|
|
220
|
+
description: "Review high-priority global and current-project memories",
|
|
221
|
+
handler: async (args, ctx) => {
|
|
222
|
+
const project = projectFor(ctx);
|
|
223
|
+
const query = args.trim();
|
|
224
|
+
const memories = query ? service.search(query, project.id, 30) : service.review(project.id, 30);
|
|
225
|
+
ctx.ui.notify(formatSearchResults(memories).slice(0, 12_000), "info");
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
pi.registerCommand("memory-save", {
|
|
230
|
+
description: "Save a memory: /memory-save [global|project] <text>",
|
|
231
|
+
handler: async (args, ctx) => {
|
|
232
|
+
const match = args.trim().match(/^(?:(global|project)\s+)?([\s\S]+)$/);
|
|
233
|
+
if (!match) { ctx.ui.notify("Usage: /memory-save [global|project] <text>", "warning"); return; }
|
|
234
|
+
const scope = (match[1] ?? "project") as MemoryScope;
|
|
235
|
+
const content = match[2]!.trim();
|
|
236
|
+
const memory = service.save({
|
|
237
|
+
scope,
|
|
238
|
+
project: projectFor(ctx),
|
|
239
|
+
kind: "reference",
|
|
240
|
+
title: content.split(/[.!?\n]/, 1)[0]!.slice(0, 140),
|
|
241
|
+
content,
|
|
242
|
+
});
|
|
243
|
+
ctx.ui.notify(`Saved ${scope} memory ${memory.memoryId}`, "info");
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
pi.registerCommand("memory-correct", {
|
|
248
|
+
description: "Correct memory content: /memory-correct <id> <replacement text>",
|
|
249
|
+
handler: async (args, ctx) => {
|
|
250
|
+
const match = args.trim().match(/^(\S+)\s+([\s\S]+)$/);
|
|
251
|
+
if (!match) { ctx.ui.notify("Usage: /memory-correct <id> <replacement text>", "warning"); return; }
|
|
252
|
+
const memory = service.correct(match[1]!, { project: projectFor(ctx), content: match[2]!.trim() });
|
|
253
|
+
ctx.ui.notify(memory ? `Corrected ${memory.memoryId}` : `Active memory not found: ${match[1]}`, memory ? "info" : "warning");
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
pi.registerCommand("memory-forget", {
|
|
258
|
+
description: "Forget a memory by exact ID: /memory-forget <id>",
|
|
259
|
+
handler: async (args, ctx) => {
|
|
260
|
+
const memoryId = args.trim();
|
|
261
|
+
if (!memoryId) { ctx.ui.notify("Usage: /memory-forget <id>", "warning"); return; }
|
|
262
|
+
const forgotten = service.forget(memoryId);
|
|
263
|
+
ctx.ui.notify(forgotten ? `Forgot ${memoryId}` : `Active memory not found: ${memoryId}`, forgotten ? "info" : "warning");
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
pi.registerCommand("memory-rebuild", {
|
|
268
|
+
description: "Rebuild the Pi Memory search index and Markdown projections",
|
|
269
|
+
handler: async (_args, ctx) => {
|
|
270
|
+
service.rebuild();
|
|
271
|
+
ctx.ui.notify("Pi Memory search index and projections rebuilt.", "info");
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
pi.registerCommand("memory-process", {
|
|
276
|
+
description: "Capture this session and process pending memory jobs",
|
|
277
|
+
handler: async (_args, ctx) => {
|
|
278
|
+
if (!active(ctx)) { ctx.ui.notify("Memory capture is disabled in this Pi mode.", "warning"); return; }
|
|
279
|
+
service.enqueueCurrentSession(ctx, projectFor(ctx));
|
|
280
|
+
await service.processPending(ctx, 10);
|
|
281
|
+
ctx.ui.notify(`Memory processing finished. ${service.status().memories} active memories.`, "info");
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
286
|
+
if (!active(ctx)) {
|
|
287
|
+
pi.setActiveTools(pi.getActiveTools().filter((name) => !MEMORY_TOOL_NAMES.has(name)));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
service.initialize(ctx.cwd);
|
|
291
|
+
ctx.ui.setStatus("pi-memory", `memory ${service.status().memories}`);
|
|
292
|
+
void service.processPending(ctx, 1);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
296
|
+
if (!active(ctx)) return;
|
|
297
|
+
const project = projectFor(ctx);
|
|
298
|
+
const memories = selectForInjection(service.database, event.prompt, project.id, {
|
|
299
|
+
maxResults: config.maxInjectedMemories,
|
|
300
|
+
maxCharacters: config.maxInjectedCharacters,
|
|
301
|
+
});
|
|
302
|
+
if (memories.length === 0) return;
|
|
303
|
+
service.database.recordAccess(memories.map((memory) => memory.memoryId));
|
|
304
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${formatMemoryContext(memories)}` };
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
308
|
+
if (!active(ctx)) return;
|
|
309
|
+
service.enqueueCurrentSession(ctx, projectFor(ctx));
|
|
310
|
+
void service.processPending(ctx, 1).then(() => {
|
|
311
|
+
if (ctx.hasUI) ctx.ui.setStatus("pi-memory", `memory ${service.status().memories}`);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
pi.on("session_before_compact", async (_event, ctx) => {
|
|
316
|
+
if (!active(ctx)) return;
|
|
317
|
+
service.enqueueCurrentSession(ctx, projectFor(ctx));
|
|
318
|
+
void service.processPending(ctx, 1);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
322
|
+
if (active(ctx)) service.enqueueCurrentSession(ctx, projectFor(ctx));
|
|
323
|
+
if (ctx.hasUI) ctx.ui.setStatus("pi-memory", undefined);
|
|
324
|
+
await service.shutdown();
|
|
325
|
+
});
|
|
326
|
+
}
|