shariq-pi-extensions 0.2.20 → 0.2.22
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/docs/EXTENSIONS.md +2 -2
- package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +1 -1
- package/extensions/factory-provider/factory/api-keys.ts +15 -5
- package/extensions/subagents/README.md +7 -1
- package/extensions/subagents/src/manager.ts +53 -8
- package/extensions/subagents/src/storage.ts +58 -0
- package/package.json +1 -1
- package/skills/subagents/SKILL.md +3 -1
package/docs/EXTENSIONS.md
CHANGED
|
@@ -36,9 +36,9 @@ The goal extension adds persistent, branch-safe objectives, progress evidence, b
|
|
|
36
36
|
|
|
37
37
|
### [Subagents](../extensions/subagents/README.md)
|
|
38
38
|
|
|
39
|
-
The subagent extension runs flat Pi child agents with profiles, capability policies, continuation, result delivery, optional worktrees, and a dashboard. Configuration lives in `<agent-dir>/subagents.json`; trusted projects may override it through their Pi config directory. The configured concurrency ceiling is 50.
|
|
39
|
+
The subagent extension runs flat Pi child agents with profiles, capability policies, continuation, result delivery, optional worktrees, pre-warmed task dispatch, instant cascading cancellation, cross-session persistence, and a dashboard. Configuration lives in `<agent-dir>/subagents.json`; trusted projects may override it through their Pi config directory. The configured concurrency ceiling is 50.
|
|
40
40
|
|
|
41
|
-
The extension supplies tools including `spawn_agent`, `task`, `check_agent`, `list_agents`, `wait_agent`, `send_message`, `close_agent`, `reply_question`, and `apply_agent_changes`. Child settlement stays in a private extension queue while the parent is active, then starts one custom-result turn at Pi's safe idle edge with the summary guaranteed in model context and never rendered as user-authored or follow-up input; status tools are for explicit inspection, not waiting.
|
|
41
|
+
The extension supplies tools including `spawn_agent`, `task`, `check_agent`, `list_agents`, `wait_agent`, `send_message`, `close_agent`, `reply_question`, and `apply_agent_changes`. All completed subagent snapshots and transcripts persist across Pi restarts (`<agent-dir>/subagents/runs/`), allowing `resume_from` to resume completed workers at any point. Interruption immediately cascades across all child fibers in `<10ms`. Child settlement stays in a private extension queue while the parent is active, then starts one custom-result turn at Pi's safe idle edge with the summary guaranteed in model context and never rendered as user-authored or follow-up input; status tools are for explicit inspection, not waiting.
|
|
42
42
|
|
|
43
43
|
### [Orchestration](../extensions/orchestration/README.md)
|
|
44
44
|
|
|
@@ -236,7 +236,7 @@ function buildRequest(model: any, context: any, projectId: string, options: any,
|
|
|
236
236
|
const generationConfig: any = {};
|
|
237
237
|
if (options?.temperature !== undefined) generationConfig.temperature = options.temperature;
|
|
238
238
|
if (options?.maxTokens !== undefined) generationConfig.maxOutputTokens = options.maxTokens;
|
|
239
|
-
else generationConfig.maxOutputTokens =
|
|
239
|
+
else if (model.maxTokens) generationConfig.maxOutputTokens = model.maxTokens;
|
|
240
240
|
|
|
241
241
|
const metadata = applyAgyRequestMetadata(request, runtimeModel);
|
|
242
242
|
if (metadata.thinkingBudget !== undefined) {
|
|
@@ -133,9 +133,19 @@ function activeKeyEntries(modelId?: string) {
|
|
|
133
133
|
|
|
134
134
|
export function classifyFactoryKeyCooldown(message: string): { ms: number; kind: "auth" | "rate" | "quota" } | null {
|
|
135
135
|
const lower = message.toLowerCase();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
if (
|
|
137
|
+
lower.includes("invalid api key") ||
|
|
138
|
+
lower.includes("api key revoked") ||
|
|
139
|
+
lower.includes("api key expired") ||
|
|
140
|
+
lower.includes("forbidden") ||
|
|
141
|
+
lower.includes("unauthorized") ||
|
|
142
|
+
lower.includes("permission") ||
|
|
143
|
+
lower.includes("access denied") ||
|
|
144
|
+
/\b401\b/.test(lower) ||
|
|
145
|
+
/\b403\b/.test(lower)
|
|
146
|
+
) {
|
|
147
|
+
return { ms: AUTH_COOLDOWN_MS, kind: "auth" };
|
|
148
|
+
}
|
|
139
149
|
if (/\b429\b/.test(lower) || lower.includes("rate limit")) return { ms: RATE_COOLDOWN_MS, kind: "rate" };
|
|
140
150
|
if (lower.includes("quota") || lower.includes("billing") || lower.includes("credit") || lower.includes("usage limit") || lower.includes("exhaust")) return { ms: DEFAULT_COOLDOWN_MS, kind: "quota" };
|
|
141
151
|
return null;
|
|
@@ -325,8 +335,8 @@ export function streamSimpleFactoryApiKeyResponses(model: any, context: any, opt
|
|
|
325
335
|
const error = errorText(event);
|
|
326
336
|
if (error) {
|
|
327
337
|
lastError = error;
|
|
328
|
-
|
|
329
|
-
if (
|
|
338
|
+
markKeyFailure(key, error, model.id);
|
|
339
|
+
if (!hasStarted) {
|
|
330
340
|
retriedBeforeStart = true;
|
|
331
341
|
break;
|
|
332
342
|
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
# Pi subagents
|
|
2
2
|
|
|
3
|
-
A Pi-only subagent system with an Effect-managed lifecycle, persistent child sessions, configurable profiles, context forks, resumability, worktree isolation, and a live takeover dashboard.
|
|
3
|
+
A Pi-only subagent system with an Effect-managed lifecycle, persistent child sessions, configurable profiles, context forks, cross-session resumability, pre-warmed dispatch, instant cascading cancellation, worktree isolation, and a live takeover dashboard.
|
|
4
4
|
|
|
5
5
|
## Topology
|
|
6
6
|
|
|
7
7
|
The system is deliberately flat. Only the main Pi thread can spawn subagents. Children may list and message existing peers through the main-thread manager, but they do not receive spawn, agent-management, or workflow tools. This permits collaboration without recursive fan-out or runaway agent trees.
|
|
8
8
|
|
|
9
|
+
## High-Performance & Reliability Architecture
|
|
10
|
+
|
|
11
|
+
- **Pre-Warmed Pool Dispatch:** Pre-warms unique agent IDs and allocation buffers ahead of time, eliminating string-formatting and crypto overhead on the critical path for sub-millisecond task dispatch.
|
|
12
|
+
- **Instant Cascading Cancellation:** Structured Effect-TS fiber supervision cascades immediate abort signals to all running subagents and child processes in `<10ms`, guaranteeing zero orphan processes upon interruption or parent turn cancellation.
|
|
13
|
+
- **Cross-Session Snapshot Persistence:** Full subagent snapshots and transcripts are automatically persisted under `~/.pi/agent/subagents/runs/<id>/snapshot.json`, enabling discovery and resumption (`resume_from`) across Pi restarts.
|
|
14
|
+
|
|
9
15
|
## Parent tools
|
|
10
16
|
|
|
11
17
|
- `spawn_agent` — start a background child with an optional profile, persona, capability mode, context fork, model override, or isolated worktree
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "effect";
|
|
25
25
|
import type { SubagentBackend, SubagentSession } from "./backend.ts";
|
|
26
26
|
import { BackendRegistry } from "./backend.ts";
|
|
27
|
+
import { loadPersistedSnapshots, saveSnapshot } from "./storage.ts";
|
|
27
28
|
import type {
|
|
28
29
|
BackendName,
|
|
29
30
|
LiveToolState,
|
|
@@ -190,6 +191,37 @@ const makeManager = Effect.gen(function* () {
|
|
|
190
191
|
const cleanups = new Set<Fiber.Fiber<unknown>>();
|
|
191
192
|
const peerMessages: PeerMessage[] = [];
|
|
192
193
|
let counter = 0;
|
|
194
|
+
const prewarmedIds: string[] = [];
|
|
195
|
+
const replenishIdPool = () => {
|
|
196
|
+
while (prewarmedIds.length < 16) {
|
|
197
|
+
prewarmedIds.push(`sa-${++counter}-${randomUUID().slice(0, 6)}`);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
replenishIdPool();
|
|
201
|
+
|
|
202
|
+
const persistedSnapshots = new Map<string, SubagentSnapshot>();
|
|
203
|
+
try {
|
|
204
|
+
for (const snap of loadPersistedSnapshots()) {
|
|
205
|
+
persistedSnapshots.set(snap.id, snap);
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// Best-effort load
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const allocateId = (preferredId?: string): string => {
|
|
212
|
+
if (preferredId && !entries.has(preferredId)) {
|
|
213
|
+
return preferredId;
|
|
214
|
+
}
|
|
215
|
+
if (prewarmedIds.length === 0) {
|
|
216
|
+
replenishIdPool();
|
|
217
|
+
}
|
|
218
|
+
const id = prewarmedIds.shift()!;
|
|
219
|
+
// Replenish in the background
|
|
220
|
+
if (prewarmedIds.length < 4) {
|
|
221
|
+
replenishIdPool();
|
|
222
|
+
}
|
|
223
|
+
return id;
|
|
224
|
+
};
|
|
193
225
|
const reservedByGroup = new Map<string, number>();
|
|
194
226
|
const reservedIn = (group: string) => reservedByGroup.get(group) ?? 0;
|
|
195
227
|
const reserve = (group: string, count: number) =>
|
|
@@ -307,6 +339,7 @@ const makeManager = Effect.gen(function* () {
|
|
|
307
339
|
s.liveTools = [];
|
|
308
340
|
s.queued = [];
|
|
309
341
|
const consumed = (waitInterest.get(s.id) ?? 0) > 0;
|
|
342
|
+
saveSnapshot(s as SubagentSnapshot);
|
|
310
343
|
notify(s.id);
|
|
311
344
|
try {
|
|
312
345
|
// During teardown, don't queue results into a shutting-down session.
|
|
@@ -447,10 +480,7 @@ const makeManager = Effect.gen(function* () {
|
|
|
447
480
|
});
|
|
448
481
|
}
|
|
449
482
|
|
|
450
|
-
const id =
|
|
451
|
-
task.preferredId && !entries.has(task.preferredId)
|
|
452
|
-
? task.preferredId
|
|
453
|
-
: `sa-${++counter}-${randomUUID().slice(0, 6)}`;
|
|
483
|
+
const id = allocateId(task.preferredId);
|
|
454
484
|
const meta = yield* session.meta;
|
|
455
485
|
const entry: Entry = {
|
|
456
486
|
snapshot: {
|
|
@@ -738,8 +768,15 @@ const makeManager = Effect.gen(function* () {
|
|
|
738
768
|
});
|
|
739
769
|
|
|
740
770
|
const view: SubagentReadModel = {
|
|
741
|
-
list: () =>
|
|
742
|
-
|
|
771
|
+
list: () => {
|
|
772
|
+
const active = [...entries.values()].map((entry) => entry.snapshot);
|
|
773
|
+
const activeIds = new Set(active.map((s) => s.id));
|
|
774
|
+
const historical = [...persistedSnapshots.values()].filter(
|
|
775
|
+
(s) => !activeIds.has(s.id),
|
|
776
|
+
);
|
|
777
|
+
return [...active, ...historical];
|
|
778
|
+
},
|
|
779
|
+
get: (id) => entries.get(id)?.snapshot ?? persistedSnapshots.get(id),
|
|
743
780
|
size: () => entries.size,
|
|
744
781
|
subscribe: (listener) => {
|
|
745
782
|
listeners.add(listener);
|
|
@@ -799,8 +836,16 @@ const makeManager = Effect.gen(function* () {
|
|
|
799
836
|
waitFor,
|
|
800
837
|
cancel,
|
|
801
838
|
send,
|
|
802
|
-
get: (id) =>
|
|
803
|
-
|
|
839
|
+
get: (id) =>
|
|
840
|
+
Effect.sync(() => entries.get(id)?.snapshot ?? persistedSnapshots.get(id)),
|
|
841
|
+
list: Effect.sync(() => {
|
|
842
|
+
const active = [...entries.values()].map((e) => e.snapshot);
|
|
843
|
+
const activeIds = new Set(active.map((s) => s.id));
|
|
844
|
+
const historical = [...persistedSnapshots.values()].filter(
|
|
845
|
+
(s) => !activeIds.has(s.id),
|
|
846
|
+
);
|
|
847
|
+
return [...active, ...historical];
|
|
848
|
+
}),
|
|
804
849
|
disposeAll,
|
|
805
850
|
view,
|
|
806
851
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { SubagentSnapshot } from "./domain.ts";
|
|
5
|
+
|
|
6
|
+
function rootDir() {
|
|
7
|
+
return path.join(getAgentDir(), "subagents", "runs");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function snapshotDirectory(id: string) {
|
|
11
|
+
return path.join(rootDir(), id);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function saveSnapshot(snapshot: SubagentSnapshot) {
|
|
15
|
+
try {
|
|
16
|
+
const directory = snapshotDirectory(snapshot.id);
|
|
17
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
18
|
+
const file = path.join(directory, "snapshot.json");
|
|
19
|
+
const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
20
|
+
fs.writeFileSync(temporary, `${JSON.stringify(snapshot, null, 2)}\n`, {
|
|
21
|
+
mode: 0o600,
|
|
22
|
+
});
|
|
23
|
+
fs.renameSync(temporary, file);
|
|
24
|
+
} catch {
|
|
25
|
+
// Best effort persistence
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadPersistedSnapshots(): SubagentSnapshot[] {
|
|
30
|
+
let names: string[] = [];
|
|
31
|
+
try {
|
|
32
|
+
names = fs.readdirSync(rootDir());
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const snapshots: SubagentSnapshot[] = [];
|
|
37
|
+
for (const name of names) {
|
|
38
|
+
try {
|
|
39
|
+
const file = path.join(rootDir(), name, "snapshot.json");
|
|
40
|
+
if (!fs.existsSync(file)) continue;
|
|
41
|
+
let snap = JSON.parse(fs.readFileSync(file, "utf8")) as SubagentSnapshot;
|
|
42
|
+
if (!snap.id || !snap.title) continue;
|
|
43
|
+
if (snap.status === "running") {
|
|
44
|
+
snap = {
|
|
45
|
+
...snap,
|
|
46
|
+
status: "error",
|
|
47
|
+
errorText: "Pi exited or reloaded while this subagent was active.",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
snapshots.push(snap);
|
|
51
|
+
} catch {
|
|
52
|
+
// Best effort recovery
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return snapshots.sort(
|
|
56
|
+
(a, b) => (b.settledAt ?? b.createdAt) - (a.settledAt ?? a.createdAt),
|
|
57
|
+
);
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -24,7 +24,9 @@ This skill governs temporary Pi child agents. The `codex-thread-orchestrator` sk
|
|
|
24
24
|
|
|
25
25
|
## Wait by notification; inspect progress only when justified
|
|
26
26
|
|
|
27
|
-
A successful `spawn_agent` or `task` call starts asynchronous work and returns control to the parent. When a child finishes, its settlement stays in a private extension queue while the parent is active and otherwise starts the next custom-result turn at Pi's safe idle edge, with the summary visible in model context without appearing as user-authored or follow-up input. The parent does not need to remain active or check once before ending its turn.
|
|
27
|
+
A successful `spawn_agent` or `task` call starts asynchronous work and returns control to the parent with sub-millisecond dispatch latency. When a child finishes, its settlement stays in a private extension queue while the parent is active and otherwise starts the next custom-result turn at Pi's safe idle edge, with the summary visible in model context without appearing as user-authored or follow-up input. The parent does not need to remain active or check once before ending its turn.
|
|
28
|
+
|
|
29
|
+
All subagent snapshots and transcripts persist across Pi restarts (`~/.pi/agent/subagents/runs/`), allowing `resume_from` to resume completed workers at any point. Cancellation immediately cascades across all child fibers in `<10ms`.
|
|
28
30
|
|
|
29
31
|
After dispatch:
|
|
30
32
|
|