Added nono profiles & orca integration - Added some extensions to start working on

This commit is contained in:
jb
2026-08-24 10:23:38 -04:00
parent 8bcb82813c
commit 10f64b593b
83 changed files with 21840 additions and 1353 deletions
+31
View File
@@ -0,0 +1,31 @@
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
type Theme = ExtensionContext["ui"]["theme"];
interface ActivityCounts {
running: number;
done: number;
failed: number;
}
const SQUARE = "■";
export function formatActivityStatus(
theme: Theme,
label: "subagents" | "workflows",
counts: ActivityCounts,
) {
const parts: string[] = [];
if (counts.running > 0) {
parts.push(theme.fg("warning", `${SQUARE} ${counts.running} running`));
}
if (counts.done > 0) {
parts.push(theme.fg("success", `${SQUARE} ${counts.done} done`));
}
if (counts.failed > 0) {
parts.push(theme.fg("error", `${SQUARE} ${counts.failed} failed`));
}
parts.push(theme.fg("accent", `/${label}`) + theme.fg("dim", " to view"));
return `${theme.fg("muted", `${label}:`)} ${parts.join(theme.fg("dim", " · "))}`;
}
+270
View File
@@ -0,0 +1,270 @@
import assert from "node:assert/strict";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import * as path from "node:path";
import test from "node:test";
import {
createAgentSession,
DefaultResourceLoader,
defineTool,
ProjectTrustStore,
SessionManager,
SettingsManager,
type SessionShutdownEvent,
} from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import {
bindChildSessionExtensions,
CHILD_EXCLUDED_TOOL_NAMES,
childToolPolicy,
createChildResources,
resolveStandaloneChildProjectTrust,
shutdownAndDisposeChildSession,
type DisposableChildSession,
} from "./child-session.ts";
async function withTempDir(run: (directory: string) => Promise<void>) {
const directory = await mkdtemp(path.join(tmpdir(), "pi-child-policy-"));
try {
await run(directory);
} finally {
await rm(directory, { recursive: true, force: true });
}
}
test("child denylist keeps extension and workflow structured tools available", async () => {
await withTempDir(async (directory) => {
let starts = 0;
let shutdowns = 0;
const settingsManager = SettingsManager.inMemory(undefined, {
projectTrusted: false,
});
const inlineLoader = new DefaultResourceLoader({
cwd: directory,
agentDir: path.join(directory, "inline-agent"),
settingsManager,
extensionFactories: [
(pi) => {
pi.on("session_start", () => {
starts++;
});
pi.on("session_shutdown", () => {
shutdowns++;
});
for (const name of [
"fixture_extension_tool",
...CHILD_EXCLUDED_TOOL_NAMES,
]) {
pi.registerTool({
name,
label: name,
description: name,
parameters: Type.Object({}),
async execute() {
return {
content: [{ type: "text", text: "ok" }],
details: {},
};
},
});
}
},
],
});
await inlineLoader.reload();
const structuredOutput = defineTool({
name: "structured_output",
label: "Structured Output",
description: "fixture structured result",
parameters: Type.Object({ value: Type.String() }),
async execute(_id, params) {
return {
content: [{ type: "text", text: params.value }],
details: {},
};
},
});
const { session } = await createAgentSession({
cwd: directory,
agentDir: path.join(directory, "inline-agent"),
resourceLoader: inlineLoader,
settingsManager,
sessionManager: SessionManager.inMemory(directory),
customTools: [structuredOutput],
...childToolPolicy(),
});
await bindChildSessionExtensions(session);
assert.deepEqual(
[...CHILD_EXCLUDED_TOOL_NAMES],
[
"subagent_spawn",
"subagent_wait",
"subagent_cancel",
"subagent_check",
"subagent_list",
"workflow",
"ask_user",
],
);
const allTools = new Set(session.getAllTools().map((tool) => tool.name));
const activeTools = new Set(session.getActiveToolNames());
assert.equal(starts, 1);
assert.equal(allTools.has("fixture_extension_tool"), true);
assert.equal(activeTools.has("fixture_extension_tool"), true);
assert.equal(allTools.has("structured_output"), true);
assert.equal(activeTools.has("structured_output"), true);
for (const denied of CHILD_EXCLUDED_TOOL_NAMES) {
assert.equal(allTools.has(denied), false, `${denied} should be denied`);
assert.equal(
activeTools.has(denied),
false,
`${denied} should be inactive`,
);
}
for (const builtin of ["read", "bash", "edit", "write"]) {
assert.equal(
activeTools.has(builtin),
true,
`${builtin} should stay active`,
);
}
await Promise.all([
shutdownAndDisposeChildSession(session),
shutdownAndDisposeChildSession(session),
]);
assert.equal(shutdowns, 1);
});
});
test("resource loading gates project extensions but retains global extensions", async () => {
await withTempDir(async (directory) => {
const cwd = path.join(directory, "project");
const agentDir = path.join(directory, "agent");
await mkdir(path.join(cwd, ".pi", "extensions"), { recursive: true });
await mkdir(path.join(agentDir, "extensions"), { recursive: true });
const extensionSource = (name: string) => `
export default function (pi) {
pi.registerTool({
name: ${JSON.stringify(name)}, label: ${JSON.stringify(name)},
description: "fixture", parameters: { type: "object", properties: {} },
async execute() { return { content: [{ type: "text", text: "ok" }] }; }
});
}
`;
await writeFile(
path.join(agentDir, "extensions", "global.ts"),
extensionSource("global_fixture"),
);
await writeFile(
path.join(cwd, ".pi", "extensions", "project.ts"),
extensionSource("project_fixture"),
);
const untrusted = await createChildResources({
cwd,
agentDir,
projectTrusted: false,
});
const untrustedTools = untrusted.loader
.getExtensions()
.extensions.flatMap((extension) => [...extension.tools.keys()]);
assert.equal(untrustedTools.includes("global_fixture"), true);
assert.equal(untrustedTools.includes("project_fixture"), false);
const trusted = await createChildResources({
cwd,
agentDir,
projectTrusted: true,
});
const trustedTools = trusted.loader
.getExtensions()
.extensions.flatMap((extension) => [...extension.tools.keys()]);
assert.equal(trustedTools.includes("global_fixture"), true);
assert.equal(trustedTools.includes("project_fixture"), true);
});
});
test("alternate standalone cwd only uses explicit saved trust", async () => {
await withTempDir(async (directory) => {
const parentCwd = path.join(directory, "parent");
const childCwd = path.join(directory, "alternate");
const agentDir = path.join(directory, "agent");
await mkdir(parentCwd, { recursive: true });
await mkdir(childCwd, { recursive: true });
assert.equal(
resolveStandaloneChildProjectTrust({
parentCwd,
childCwd: parentCwd,
parentTrusted: true,
agentDir,
}),
true,
);
assert.equal(
resolveStandaloneChildProjectTrust({
parentCwd,
childCwd,
parentTrusted: true,
agentDir,
}),
false,
);
new ProjectTrustStore(agentDir).set(childCwd, true);
assert.equal(
resolveStandaloneChildProjectTrust({
parentCwd,
childCwd,
parentTrusted: false,
agentDir,
}),
true,
);
});
});
test("shutdown helper balances hooks and disposal despite errors", async () => {
let emits = 0;
let disposals = 0;
const session: DisposableChildSession = {
extensionRunner: {
hasHandlers: () => true,
async emit(event: SessionShutdownEvent) {
emits++;
assert.deepEqual(event, { type: "session_shutdown", reason: "quit" });
throw new Error("fixture shutdown failure");
},
},
dispose() {
disposals++;
},
};
await Promise.all([
shutdownAndDisposeChildSession(session),
shutdownAndDisposeChildSession(session),
shutdownAndDisposeChildSession(session),
]);
assert.equal(emits, 1);
assert.equal(disposals, 1);
});
test("shutdown helper bounds a stuck hook before disposal", async () => {
let disposals = 0;
const session: DisposableChildSession = {
extensionRunner: {
hasHandlers: () => true,
emit: () => new Promise(() => {}),
},
dispose() {
disposals++;
},
};
await shutdownAndDisposeChildSession(session, { timeoutMs: 10 });
assert.equal(disposals, 1);
});
+148
View File
@@ -0,0 +1,148 @@
import * as path from "node:path";
import {
DefaultResourceLoader,
getAgentDir,
ProjectTrustStore,
SettingsManager,
type AgentSession,
type SessionShutdownEvent,
} from "@earendil-works/pi-coding-agent";
const CHILD_SHUTDOWN_TIMEOUT_MS = 5_000;
/** Tools that headless children must not receive. Everything else stays enabled. */
export const CHILD_EXCLUDED_TOOL_NAMES = [
"subagent_spawn",
"subagent_wait",
"subagent_cancel",
"subagent_check",
"subagent_list",
"workflow",
"ask_user",
] as const;
/** Fresh SDK options avoid turning the denylist into an accidental allowlist. */
export function childToolPolicy() {
return { excludeTools: [...CHILD_EXCLUDED_TOOL_NAMES] };
}
export interface ChildResourceOptions {
cwd: string;
projectTrusted: boolean;
appendSystemPrompt?: string[];
agentDir?: string;
}
/** Load normal global/package resources and trust-gated project resources. */
export async function createChildResources(options: ChildResourceOptions) {
const agentDir = options.agentDir ?? getAgentDir();
const settingsManager = SettingsManager.create(options.cwd, agentDir, {
projectTrusted: options.projectTrusted,
});
const loader = new DefaultResourceLoader({
cwd: options.cwd,
agentDir,
settingsManager,
...(options.appendSystemPrompt
? { appendSystemPrompt: options.appendSystemPrompt }
: {}),
});
await loader.reload();
return { loader, settingsManager };
}
/**
* Same-directory children inherit the live parent decision. An alternate cwd
* is trusted only when Pi's persisted trust store explicitly trusts it (or a
* containing directory); unreadable/invalid trust data fails closed.
*/
export function resolveStandaloneChildProjectTrust(options: {
parentCwd: string;
childCwd: string;
parentTrusted: boolean;
agentDir?: string;
}) {
if (path.resolve(options.childCwd) === path.resolve(options.parentCwd)) {
return options.parentTrusted;
}
try {
const trustStore = new ProjectTrustStore(options.agentDir ?? getAgentDir());
return trustStore.get(options.childCwd) === true;
} catch {
return false;
}
}
/** Start child extension session hooks/resources in headless print mode. */
export async function bindChildSessionExtensions(
session: Pick<AgentSession, "bindExtensions">,
) {
await session.bindExtensions({ mode: "print" });
}
interface ChildExtensionRunner {
hasHandlers(eventType: string): boolean;
emit(event: SessionShutdownEvent): Promise<unknown>;
}
export interface DisposableChildSession {
readonly extensionRunner: ChildExtensionRunner;
dispose(): void;
}
const childShutdowns = new WeakMap<object, Promise<void>>();
function waitBounded(operation: Promise<unknown>, timeoutMs: number) {
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<void>((resolve) => {
timer = setTimeout(resolve, timeoutMs);
});
return Promise.race([
operation.then(
() => undefined,
() => undefined,
),
timeout,
])
.catch(() => {})
.finally(() => {
if (timer) clearTimeout(timer);
});
}
/**
* Emit child session_shutdown once, then dispose once. Hook failures and a
* bounded hook deadline never prevent disposal.
*/
export function shutdownAndDisposeChildSession(
session: DisposableChildSession,
options: { timeoutMs?: number } = {},
) {
const existing = childShutdowns.get(session);
if (existing) return existing;
const shutdown = (async () => {
try {
if (session.extensionRunner.hasHandlers("session_shutdown")) {
await waitBounded(
session.extensionRunner.emit({
type: "session_shutdown",
reason: "quit",
}),
options.timeoutMs ?? CHILD_SHUTDOWN_TIMEOUT_MS,
);
}
} catch {
// Extension runner inspection/emission is best-effort during teardown.
} finally {
try {
session.dispose();
} catch {
// Disposal is terminal and must remain idempotent for callers.
}
}
})();
childShutdowns.set(session, shutdown);
return shutdown;
}
@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
contextPercent,
formatContextUtilization,
} from "./context-utilization.ts";
test("formats current context occupancy against model capacity", () => {
assert.equal(
formatContextUtilization({ tokens: 26_040, contextWindow: 372_000 }),
"7%/372k",
);
});
test("formats the latest post-compaction usage rather than prior cumulative usage", () => {
const latestUsage = { tokens: 18_000, contextWindow: 200_000 };
assert.equal(formatContextUtilization(latestUsage), "9%/200k");
});
test("clamps over-capacity and nonsensical token values", () => {
assert.equal(
contextPercent({ tokens: 500_000, contextWindow: 200_000 }),
100,
);
assert.equal(
formatContextUtilization({
tokens: Number.POSITIVE_INFINITY,
contextWindow: 200_000,
}),
"?%/200k",
);
assert.equal(
formatContextUtilization({ tokens: -1, contextWindow: 200_000 }),
"?%/200k",
);
});
test("handles missing usage or capacity without NaN or Infinity", () => {
assert.equal(
formatContextUtilization({ tokens: null, contextWindow: 372_000 }),
"?%/372k",
);
assert.equal(formatContextUtilization({ tokens: 12_000 }), "");
assert.equal(
formatContextUtilization({ tokens: 12_000, contextWindow: 0 }),
"",
);
});
+47
View File
@@ -0,0 +1,47 @@
/** Compact, defensive context-window utilization formatting for child agents. */
export interface ContextUtilization {
/** Current conversation context occupancy; null while it is unknown after compaction. */
tokens?: number | null;
/** Capacity of the model currently serving the conversation. */
contextWindow?: number | null;
}
function usableTokens(value: number | null | undefined) {
return typeof value === "number" && Number.isFinite(value) && value >= 0
? value
: undefined;
}
function usableCapacity(value: number | null | undefined) {
return typeof value === "number" && Number.isFinite(value) && value > 0
? value
: undefined;
}
export function contextPercent(usage: ContextUtilization) {
const tokens = usableTokens(usage.tokens);
const capacity = usableCapacity(usage.contextWindow);
if (tokens === undefined || capacity === undefined) return undefined;
return Math.round(Math.min(100, Math.max(0, (tokens / capacity) * 100)));
}
export function formatCompactTokens(count: number) {
if (count < 1000) return Math.round(count).toString();
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
if (count < 1000000) return `${Math.round(count / 1000)}k`;
return `${(count / 1000000).toFixed(1)}M`;
}
/**
* Render `%/capacity`. If occupancy is temporarily unknown (notably directly
* after compaction), retain the useful model capacity as `?%/capacity`. If no
* valid capacity is available, omit the statistic because no percentage can
* be computed.
*/
export function formatContextUtilization(usage: ContextUtilization) {
const capacity = usableCapacity(usage.contextWindow);
if (capacity === undefined) return "";
const percent = contextPercent(usage);
return `${percent === undefined ? "?" : percent}%/${formatCompactTokens(capacity)}`;
}
+117
View File
@@ -0,0 +1,117 @@
import assert from "node:assert/strict";
import test from "node:test";
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import {
createToolCallTimeoutGuard,
runWithToolCallTimeout,
CHILD_TOOL_CALL_TIMEOUT_MS,
ToolCallTimeoutError,
} from "./tool-call-timeout.ts";
test("the production timeout error names the tool and three-minute limit", () => {
assert.equal(
new ToolCallTimeoutError("fixture_tool", CHILD_TOOL_CALL_TIMEOUT_MS)
.message,
'Tool call "fixture_tool" timed out after 3 minutes.',
);
});
test("a hung tool call fails clearly and receives an abort signal", async () => {
let executionSignal: AbortSignal | undefined;
await assert.rejects(
runWithToolCallTimeout("hung_fixture", 10, undefined, (signal) => {
executionSignal = signal;
return new Promise(() => {});
}),
(error: unknown) => {
assert.equal(error instanceof ToolCallTimeoutError, true);
assert.equal(
error instanceof Error ? error.message : "",
'Tool call "hung_fixture" timed out after 10 ms.',
);
return true;
},
);
assert.equal(executionSignal?.aborted, true);
assert.equal(executionSignal?.reason instanceof ToolCallTimeoutError, true);
});
test("parent cancellation still stops the timeout wrapper immediately", async () => {
const controller = new AbortController();
const reason = new Error("cancelled fixture");
const pending = runWithToolCallTimeout(
"hung_fixture",
60_000,
controller.signal,
() => new Promise(() => {}),
);
controller.abort(reason);
await assert.rejects(pending, (error: unknown) => error === reason);
});
test("the guard wraps each definition once and can discover later tools", () => {
const definitions = new Map<string, ToolDefinition>();
const createDefinition = (name: string): ToolDefinition => ({
name,
label: name,
description: name,
parameters: Type.Object({}),
async execute() {
return { content: [{ type: "text", text: "done" }], details: {} };
},
});
const first = createDefinition("first");
definitions.set(first.name, first);
const registry = {
getAllTools: () => [...definitions.keys()].map((name) => ({ name })),
getToolDefinition: (name: string) => definitions.get(name),
};
const guard = createToolCallTimeoutGuard(10);
const firstExecute = first.execute;
guard.apply(registry);
const firstWrappedExecute = first.execute;
assert.notEqual(firstWrappedExecute, firstExecute);
const second = createDefinition("second");
const secondExecute = second.execute;
definitions.set(second.name, second);
guard.apply(registry);
assert.equal(first.execute, firstWrappedExecute);
assert.notEqual(second.execute, secondExecute);
});
test("successful and terminating tool results pass through unchanged", async () => {
const result = {
content: [{ type: "text" as const, text: "recorded" }],
details: { value: "fixture" },
terminate: true,
};
assert.equal(
await runWithToolCallTimeout(
"structured_output",
10,
undefined,
async () => result,
),
result,
);
});
test("the timeout is fresh for each tool call, not shared across calls", async () => {
const execute = () =>
runWithToolCallTimeout("slow_fixture", 100, undefined, async () => {
await new Promise((resolve) => setTimeout(resolve, 60));
return "done";
});
assert.equal(await execute(), "done");
assert.equal(await execute(), "done");
});
+104
View File
@@ -0,0 +1,104 @@
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
export const CHILD_TOOL_CALL_TIMEOUT_MS = 3 * 60 * 1_000;
interface ToolRegistry {
getAllTools(): Array<{ name: string }>;
getToolDefinition(name: string): ToolDefinition | undefined;
}
function formatTimeout(timeoutMs: number) {
if (timeoutMs % 60_000 === 0) {
const minutes = timeoutMs / 60_000;
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
}
if (timeoutMs % 1_000 === 0) {
const seconds = timeoutMs / 1_000;
return `${seconds} second${seconds === 1 ? "" : "s"}`;
}
return `${timeoutMs} ms`;
}
export class ToolCallTimeoutError extends Error {
constructor(toolName: string, timeoutMs: number) {
super(
`Tool call "${toolName}" timed out after ${formatTimeout(timeoutMs)}.`,
);
this.name = "ToolCallTimeoutError";
}
}
export async function runWithToolCallTimeout<T>(
toolName: string,
timeoutMs: number,
signal: AbortSignal | undefined,
execute: (signal: AbortSignal) => Promise<T>,
) {
const timeoutController = new AbortController();
const executionSignal = signal
? AbortSignal.any([signal, timeoutController.signal])
: timeoutController.signal;
const timeoutError = new ToolCallTimeoutError(toolName, timeoutMs);
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => {
reject(timeoutError);
timeoutController.abort(timeoutError);
}, timeoutMs);
});
let removeAbortListener: (() => void) | undefined;
const aborted = new Promise<never>((_resolve, reject) => {
if (!signal) return;
const onAbort = () => {
reject(
signal.reason instanceof Error
? signal.reason
: new Error(`Tool call "${toolName}" was aborted.`),
);
};
if (signal.aborted) {
onAbort();
return;
}
signal.addEventListener("abort", onAbort, { once: true });
removeAbortListener = () => signal.removeEventListener("abort", onAbort);
});
try {
return await Promise.race([execute(executionSignal), timeout, aborted]);
} finally {
if (timer) clearTimeout(timer);
removeAbortListener?.();
}
}
/**
* Wrap every currently registered child tool with an independent execution
* timeout. Calling apply() again is safe and picks up tools registered later.
*/
export function createToolCallTimeoutGuard(
timeoutMs = CHILD_TOOL_CALL_TIMEOUT_MS,
) {
const wrapped = new WeakSet<ToolDefinition>();
const wrap = (definition: ToolDefinition) => {
if (wrapped.has(definition)) return;
wrapped.add(definition);
const execute = definition.execute;
definition.execute = async (toolCallId, params, signal, onUpdate, ctx) =>
runWithToolCallTimeout(definition.name, timeoutMs, signal, (signal) =>
execute.call(definition, toolCallId, params, signal, onUpdate, ctx),
);
};
return {
apply(session: ToolRegistry) {
for (const { name } of session.getAllTools()) {
const definition = session.getToolDefinition(name);
if (definition) wrap(definition);
}
},
};
}