pi-famulus 0.1.2 → 0.1.4
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/README.md +3 -1
- package/package.json +31 -12
- package/src/agents/builtins.ts +5 -22
- package/src/agents/definition.ts +16 -12
- package/src/bash-override.ts +8 -21
- package/src/behavior-guidelines.ts +40 -15
- package/src/clock.ts +5 -0
- package/src/comms/comms.ts +7 -3
- package/src/comms/tools.ts +11 -36
- package/src/config.ts +25 -1
- package/src/format.ts +83 -5
- package/src/index.ts +155 -30
- package/src/manager-client.ts +265 -114
- package/src/monitor.ts +14 -33
- package/src/notify.ts +7 -7
- package/src/prompts.generated.ts +183 -0
- package/src/shell-exec.ts +6 -2
- package/src/subagent/admission.ts +224 -0
- package/src/subagent/agent-records.ts +66 -3
- package/src/subagent/child-bash.ts +65 -56
- package/src/subagent/model-spec.ts +134 -16
- package/src/subagent/overrun.ts +191 -0
- package/src/subagent/pi-runtime.ts +118 -33
- package/src/subagent/registry.ts +235 -34
- package/src/subagent/runner.ts +565 -99
- package/src/subagent/tool.ts +111 -61
- package/src/subagent/types.ts +71 -7
- package/src/subagent/usage.ts +48 -0
- package/src/task-tools.ts +15 -25
- package/src/thinking-levels.ts +26 -0
- package/src/tui/message-renderers.ts +52 -2
- package/src/wake-delivery.ts +137 -0
- package/src/wake.ts +270 -7
- package/src/work-index.ts +4 -0
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
* allowlist plus injected custom tools (child bash, M4 comms), and wraps it
|
|
12
12
|
* into a ChildSessionAdapter.
|
|
13
13
|
*
|
|
14
|
-
* Child sessions
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* by construction.
|
|
14
|
+
* Child sessions discover user resources and built-in extensions normally.
|
|
15
|
+
* A session-local event bus identifies child loaders before the famulus
|
|
16
|
+
* extension initializes, preventing recursive parent-tool registration.
|
|
18
17
|
*/
|
|
19
18
|
import type {
|
|
19
|
+
ExtensionAPI,
|
|
20
20
|
ExtensionContext,
|
|
21
21
|
ModelRegistry,
|
|
22
22
|
ToolDefinition,
|
|
@@ -28,7 +28,8 @@ import {
|
|
|
28
28
|
type ModelCandidate,
|
|
29
29
|
} from "./model-spec";
|
|
30
30
|
import { turnsFromMessages } from "./conversation";
|
|
31
|
-
import
|
|
31
|
+
import { createTokenUsageAccumulator, normalizedTokenUsage } from "./usage";
|
|
32
|
+
import type { ChildRunRequest, ChildSessionAdapter, ChildSessionEvent, CreateSessionFn } from "./types";
|
|
32
33
|
|
|
33
34
|
/** Structural subset of the pi module namespace we rely on. */
|
|
34
35
|
type PiModule = typeof import("@earendil-works/pi-coding-agent");
|
|
@@ -37,6 +38,34 @@ type PiAgentSession = Awaited<ReturnType<PiModule["createAgentSession"]>>["sessi
|
|
|
37
38
|
|
|
38
39
|
type Model = NonNullable<ExtensionContext["model"]>;
|
|
39
40
|
|
|
41
|
+
export function createPiUsageAdapter() {
|
|
42
|
+
const totals = createTokenUsageAccumulator();
|
|
43
|
+
return {
|
|
44
|
+
tokenUsage: () => totals.snapshot(),
|
|
45
|
+
adapt(event: {
|
|
46
|
+
type: string;
|
|
47
|
+
toolCallId?: string;
|
|
48
|
+
message?: {
|
|
49
|
+
role?: string;
|
|
50
|
+
usage?: { input?: unknown; output?: unknown; cacheRead?: unknown; cacheWrite?: unknown };
|
|
51
|
+
};
|
|
52
|
+
}): ChildSessionEvent {
|
|
53
|
+
const message = event.message;
|
|
54
|
+
let usage: ReturnType<typeof normalizedTokenUsage> | undefined;
|
|
55
|
+
if (event.type === "message_end" && message?.usage) {
|
|
56
|
+
usage = normalizedTokenUsage(message.usage);
|
|
57
|
+
totals.add(usage);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
type: event.type,
|
|
61
|
+
...(typeof event.toolCallId === "string" ? { toolCallId: event.toolCallId } : {}),
|
|
62
|
+
...(typeof message?.role === "string" ? { role: message.role } : {}),
|
|
63
|
+
...(usage ? { usage } : {}),
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
40
69
|
export interface PiRuntimeDeps {
|
|
41
70
|
/** Parent session model registry (ctx.modelRegistry). */
|
|
42
71
|
getModelRegistry: () => ModelRegistry | null;
|
|
@@ -58,6 +87,10 @@ export interface PiRuntimeDeps {
|
|
|
58
87
|
getScopedModels?: () => readonly { model: Model; thinkingLevel?: string }[];
|
|
59
88
|
/** Working directory for child sessions. */
|
|
60
89
|
getCwd: () => string;
|
|
90
|
+
/** Global resource directory; defaults to pi.getAgentDir(). */
|
|
91
|
+
getAgentDir?: () => string;
|
|
92
|
+
/** Preserve the parent's project trust decision. */
|
|
93
|
+
getProjectTrusted?: () => boolean;
|
|
61
94
|
/**
|
|
62
95
|
* Custom tools injected into every child session (e.g. the no-background
|
|
63
96
|
* bash variant, M4 contact_supervisor). Called per child request.
|
|
@@ -137,13 +170,13 @@ function resolveModel(deps: PiRuntimeDeps, req: ChildRunRequest): ResolvedModel
|
|
|
137
170
|
if (!res.ok) throw new Error(modelResolutionError(req.model, res));
|
|
138
171
|
const model = findModel(res.provider, res.id);
|
|
139
172
|
if (!model) throw new Error(`model ${res.provider}/${res.id} is not usable in this session`);
|
|
140
|
-
return { model, thinkingOverride: res.thinking };
|
|
173
|
+
return { model, thinkingOverride: res.thinking, warning: res.warning };
|
|
141
174
|
}
|
|
142
175
|
if (req.agent.model !== undefined) {
|
|
143
176
|
const res = resolveModelSpec(req.agent.model, candidates);
|
|
144
177
|
if (res.ok) {
|
|
145
178
|
const model = findModel(res.provider, res.id);
|
|
146
|
-
if (model) return { model, thinkingOverride: res.thinking };
|
|
179
|
+
if (model) return { model, thinkingOverride: res.thinking, warning: res.warning };
|
|
147
180
|
}
|
|
148
181
|
return {
|
|
149
182
|
model: deps.getParentModel(),
|
|
@@ -156,11 +189,17 @@ function resolveModel(deps: PiRuntimeDeps, req: ChildRunRequest): ResolvedModel
|
|
|
156
189
|
/** Wrap a real AgentSession into the pi-free ChildSessionAdapter. */
|
|
157
190
|
function wrapSession(
|
|
158
191
|
session: PiAgentSession,
|
|
159
|
-
extras: { warning?: string; resolvedModel?: string } = {},
|
|
192
|
+
extras: { warning?: string; resolvedModel?: string; effectiveThinkingLevel?: string } = {},
|
|
160
193
|
): ChildSessionAdapter {
|
|
194
|
+
const usageAdapter = createPiUsageAdapter();
|
|
195
|
+
let disposal: Promise<void> | undefined;
|
|
196
|
+
// Children receive all queued steering messages in one drain/turn rather
|
|
197
|
+
// than the interactive one-at-a-time default, which can strand wake bursts.
|
|
198
|
+
session.setSteeringMode("all");
|
|
161
199
|
return {
|
|
162
200
|
...(extras.warning !== undefined ? { warning: extras.warning } : {}),
|
|
163
201
|
...(extras.resolvedModel !== undefined ? { resolvedModel: extras.resolvedModel } : {}),
|
|
202
|
+
...(extras.effectiveThinkingLevel !== undefined ? { effectiveThinkingLevel: extras.effectiveThinkingLevel } : {}),
|
|
164
203
|
// pi ≥0.99 returns QueuedInputDisposition ("handled" | "queued") from
|
|
165
204
|
// input methods; the adapter contract stays Promise<void>.
|
|
166
205
|
prompt: async (text) => { await session.prompt(text); },
|
|
@@ -180,33 +219,64 @@ function wrapSession(
|
|
|
180
219
|
};
|
|
181
220
|
},
|
|
182
221
|
getConversation: () => turnsFromMessages(session.messages),
|
|
222
|
+
tokenUsage: () => usageAdapter.tokenUsage(),
|
|
183
223
|
getActiveToolNames: () => session.getActiveToolNames(),
|
|
184
224
|
getSystemPrompt: () => session.systemPrompt,
|
|
185
225
|
isStreaming: () => session.isStreaming,
|
|
186
|
-
|
|
187
|
-
|
|
226
|
+
getSteeringMode: () => session.steeringMode,
|
|
227
|
+
subscribe: (listener) => session.subscribe((event) => {
|
|
228
|
+
listener(usageAdapter.adapt(event as Parameters<typeof usageAdapter.adapt>[0]));
|
|
229
|
+
}),
|
|
230
|
+
dispose: () => disposal ??= (async () => {
|
|
231
|
+
try {
|
|
232
|
+
await session.abort();
|
|
233
|
+
await session.extensionRunner.emit({ type: "session_shutdown", reason: "quit" });
|
|
234
|
+
} finally {
|
|
235
|
+
session.dispose();
|
|
236
|
+
}
|
|
237
|
+
})(),
|
|
188
238
|
};
|
|
189
239
|
}
|
|
190
240
|
|
|
191
|
-
|
|
192
|
-
|
|
241
|
+
const CHILD_ROLE_CHANNEL = "pi-famulus:child-role";
|
|
242
|
+
|
|
243
|
+
/** The loader's bus answers synchronously, before any extension initialization. */
|
|
244
|
+
export function isFamulusChildSession(pi: Pick<ExtensionAPI, "events">): boolean {
|
|
245
|
+
if (!pi.events) {
|
|
246
|
+
throw new Error("pi-famulus requires the extension event bus to identify child sessions safely");
|
|
247
|
+
}
|
|
248
|
+
const probe = { child: false };
|
|
249
|
+
pi.events.emit(CHILD_ROLE_CHANNEL, probe);
|
|
250
|
+
return probe.child;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Keep configuration scopes and paths intact; never persist child overrides. */
|
|
254
|
+
export async function createChildResources(input: {
|
|
193
255
|
cwd: string;
|
|
194
256
|
agentDir: string;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
257
|
+
projectTrusted: boolean;
|
|
258
|
+
}) {
|
|
259
|
+
const pi = await importPi();
|
|
260
|
+
const settingsManager = pi.SettingsManager.create(input.cwd, input.agentDir, {
|
|
261
|
+
projectTrusted: input.projectTrusted,
|
|
262
|
+
});
|
|
263
|
+
const eventBus = pi.createEventBus();
|
|
264
|
+
eventBus.on(CHILD_ROLE_CHANNEL, (probe) => {
|
|
265
|
+
if (probe && typeof probe === "object" && "child" in probe) probe.child = true;
|
|
266
|
+
});
|
|
267
|
+
const resourceLoader = new pi.DefaultResourceLoader({
|
|
202
268
|
cwd: input.cwd,
|
|
203
269
|
agentDir: input.agentDir,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
270
|
+
settingsManager,
|
|
271
|
+
eventBus,
|
|
272
|
+
extensionFactories: [
|
|
273
|
+
{ name: "codemode", factory: pi.createCodemodeExtension(), builtin: true, replaceable: true },
|
|
274
|
+
{ name: "tool-search", factory: pi.createToolSearchExtension(), builtin: true, replaceable: true },
|
|
275
|
+
{ name: "mcp", factory: pi.createMcpExtension(), builtin: true, replaceable: true },
|
|
276
|
+
],
|
|
277
|
+
});
|
|
278
|
+
await resourceLoader.reload();
|
|
279
|
+
return { resourceLoader, settingsManager };
|
|
210
280
|
}
|
|
211
281
|
|
|
212
282
|
export function childSessionCreateOptions(input: {
|
|
@@ -222,13 +292,18 @@ export function childSessionCreateOptions(input: {
|
|
|
222
292
|
model: unknown;
|
|
223
293
|
thinkingLevel: unknown;
|
|
224
294
|
tools?: string[];
|
|
295
|
+
excludeTools?: string[];
|
|
225
296
|
customTools?: unknown[];
|
|
226
297
|
modelRuntime?: unknown;
|
|
227
298
|
resourceLoader?: unknown;
|
|
228
299
|
} {
|
|
229
|
-
const tools = input.tools
|
|
230
|
-
const customTools = input.customTools?.
|
|
300
|
+
const tools = input.tools !== undefined ? [...input.tools] : undefined;
|
|
301
|
+
const customTools = input.customTools?.filter((customTool) => {
|
|
302
|
+
const name = (customTool as { name?: unknown }).name;
|
|
303
|
+
return name !== "bash" || tools === undefined || tools.includes("bash");
|
|
304
|
+
}).map((customTool) => {
|
|
231
305
|
const tool = customTool as { name?: unknown; promptGuidelines?: string[] };
|
|
306
|
+
if (tool.name === "bash") return { ...tool, defaultActive: false };
|
|
232
307
|
if (tool.name === "contact_supervisor") {
|
|
233
308
|
return {
|
|
234
309
|
...tool,
|
|
@@ -247,7 +322,11 @@ export function childSessionCreateOptions(input: {
|
|
|
247
322
|
cwd: input.cwd,
|
|
248
323
|
model: input.model,
|
|
249
324
|
thinkingLevel: input.thinkingLevel,
|
|
250
|
-
...(tools ? { tools } : {}),
|
|
325
|
+
...(tools !== undefined ? { tools } : {}),
|
|
326
|
+
// pi 1.0.4+ otherwise retains MCP tools behind non-MCP allowlists.
|
|
327
|
+
...(tools !== undefined && !tools.some((name) => name.startsWith("mcp__"))
|
|
328
|
+
? { excludeTools: ["mcp__*"] }
|
|
329
|
+
: {}),
|
|
251
330
|
...(customTools && customTools.length > 0 ? { customTools } : {}),
|
|
252
331
|
...(input.modelRuntime ? { modelRuntime: input.modelRuntime } : {}),
|
|
253
332
|
...(input.resourceLoader ? { resourceLoader: input.resourceLoader } : {}),
|
|
@@ -267,31 +346,37 @@ export function createPiSessionFn(deps: PiRuntimeDeps): CreateSessionFn {
|
|
|
267
346
|
req.agent.thinking ??
|
|
268
347
|
deps.getParentThinkingLevel();
|
|
269
348
|
const cwd = deps.getCwd();
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
349
|
+
const agentDir = deps.getAgentDir?.() ?? pi.getAgentDir();
|
|
350
|
+
const { resourceLoader, settingsManager } = await createChildResources({
|
|
351
|
+
cwd,
|
|
352
|
+
agentDir,
|
|
353
|
+
projectTrusted: deps.getProjectTrusted?.() ?? false,
|
|
354
|
+
});
|
|
274
355
|
const customTools = deps.customTools?.(req) ?? [];
|
|
275
356
|
const modelRuntime = deps.getModelRuntime?.();
|
|
276
357
|
const options = childSessionCreateOptions({
|
|
277
358
|
cwd,
|
|
278
359
|
model: resolved.model,
|
|
279
360
|
thinkingLevel,
|
|
280
|
-
tools: req.agent.tools
|
|
361
|
+
tools: req.agent.tools,
|
|
281
362
|
customTools: customTools.length > 0 ? customTools : undefined,
|
|
282
363
|
modelRuntime,
|
|
283
364
|
resourceLoader,
|
|
284
365
|
});
|
|
285
366
|
const { session } = await pi.createAgentSession({
|
|
286
367
|
...options,
|
|
368
|
+
agentDir,
|
|
369
|
+
settingsManager,
|
|
287
370
|
sessionManager: pi.SessionManager.inMemory(cwd),
|
|
288
371
|
} as never);
|
|
372
|
+
await session.bindExtensions({});
|
|
289
373
|
const resolvedModel = resolved.model
|
|
290
374
|
? `${resolved.model.provider}/${resolved.model.id}`
|
|
291
375
|
: undefined;
|
|
292
376
|
return wrapSession(session, {
|
|
293
377
|
...(resolved.warning !== undefined ? { warning: resolved.warning } : {}),
|
|
294
378
|
...(resolvedModel !== undefined ? { resolvedModel } : {}),
|
|
379
|
+
...(typeof thinkingLevel === "string" ? { effectiveThinkingLevel: thinkingLevel } : {}),
|
|
295
380
|
});
|
|
296
381
|
};
|
|
297
382
|
}
|