@rosetears/aili-pi 0.1.8 → 0.1.10

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.
Files changed (63) hide show
  1. package/README.md +7 -7
  2. package/THIRD_PARTY_NOTICES.md +12 -12
  3. package/docs/persistent-agents.md +114 -0
  4. package/extensions/matrix/index.ts +24 -8
  5. package/extensions/zentui/config.ts +13 -13
  6. package/extensions/zentui/format.ts +15 -1
  7. package/extensions/zentui/gradient.ts +22 -13
  8. package/extensions/zentui/tool-execution.ts +7 -4
  9. package/manifests/adapter-evidence.json +53 -20
  10. package/manifests/capabilities.json +3 -3
  11. package/manifests/live-verification.json +18 -19
  12. package/manifests/provenance.json +12 -12
  13. package/manifests/roles.json +270 -57
  14. package/manifests/sbom.json +1 -128
  15. package/manifests/skill-compatibility.json +57 -61
  16. package/manifests/subagent-provenance.json +8 -15
  17. package/package.json +3 -3
  18. package/roles/agent-evaluator.md +8 -3
  19. package/roles/ai-regression-scout.md +8 -3
  20. package/roles/browser-qa-runner.md +8 -3
  21. package/roles/code-reviewer.md +8 -3
  22. package/roles/code-scout.md +8 -3
  23. package/roles/convergence-reviewer.md +8 -3
  24. package/roles/doc-researcher.md +8 -3
  25. package/roles/e2e-artifact-runner.md +8 -3
  26. package/roles/general.md +49 -0
  27. package/roles/implementer.md +8 -3
  28. package/roles/opensource-sanitizer.md +8 -3
  29. package/roles/plan-auditor.md +8 -3
  30. package/roles/pr-test-analyzer.md +8 -3
  31. package/roles/security-auditor.md +8 -3
  32. package/roles/silent-failure-reviewer.md +8 -3
  33. package/roles/spec-miner.md +8 -3
  34. package/roles/test-coverage-reviewer.md +8 -3
  35. package/roles/test-engineer.md +8 -3
  36. package/roles/web-performance-auditor.md +8 -3
  37. package/roles/web-researcher.md +8 -3
  38. package/scripts/sync-roles.ts +186 -24
  39. package/src/runtime/doctor.ts +38 -10
  40. package/src/runtime/global-resources.ts +5 -1
  41. package/src/runtime/index.ts +2 -2
  42. package/src/runtime/persistent-agents/hub.ts +429 -0
  43. package/src/runtime/persistent-agents/model-selection.ts +363 -0
  44. package/src/runtime/persistent-agents/output-delivery.ts +356 -0
  45. package/src/runtime/persistent-agents/permission.ts +223 -0
  46. package/src/runtime/persistent-agents/policy.ts +311 -0
  47. package/src/runtime/persistent-agents/production.ts +569 -0
  48. package/src/runtime/persistent-agents/runtime.ts +164 -0
  49. package/src/runtime/persistent-agents/sandbox.ts +68 -0
  50. package/src/runtime/persistent-agents/scheduler.ts +190 -0
  51. package/src/runtime/persistent-agents/session-factory.ts +184 -0
  52. package/src/runtime/persistent-agents/storage.ts +775 -0
  53. package/src/runtime/persistent-agents/task-coordinator.ts +460 -0
  54. package/src/runtime/persistent-agents/task-schema.ts +141 -0
  55. package/src/runtime/persistent-agents/types.ts +134 -0
  56. package/src/runtime/persistent-agents/workspace.ts +335 -0
  57. package/src/runtime/registry.ts +28 -32
  58. package/src/runtime/roles.ts +211 -18
  59. package/src/runtime/rose-context.ts +1 -1
  60. package/templates/APPEND_SYSTEM.md +1 -1
  61. package/themes/rose-cyberdeck.json +5 -9
  62. package/upstream/opencode-global-agents.lock.json +1 -1
  63. package/src/runtime/subagents.ts +0 -249
@@ -0,0 +1,164 @@
1
+ import {
2
+ SessionManager,
3
+ type ExtensionAPI,
4
+ type ExtensionContext,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import type { TaskExecutionOutput, TaskExecutorInput } from "./task-coordinator.js";
7
+ import { TaskCoordinator } from "./task-coordinator.js";
8
+ import { HUB_TOOL_SCHEMA, HubService, type HubCaller, type LiveAgentAdapter } from "./hub.js";
9
+ import { TASK_TOOL_SCHEMA } from "./task-schema.js";
10
+ import {
11
+ createChildSessionManager,
12
+ ensureSidecarLayout,
13
+ openChildSessionManager,
14
+ reconcileUnfinishedCoordinator,
15
+ registerChildSession,
16
+ resumeCoordinator,
17
+ type CoordinatorJournal,
18
+ } from "./storage.js";
19
+ import {
20
+ AsyncDeliveryService,
21
+ persistFullAgentOutput,
22
+ readAgentHistory,
23
+ readAgentOutput,
24
+ type ParentDeliveryAdapter,
25
+ } from "./output-delivery.js";
26
+ import type { SidecarLayout } from "./types.js";
27
+
28
+ export interface PersistentRuntimeExecutorInput extends TaskExecutorInput {
29
+ sessionManager: SessionManager;
30
+ }
31
+
32
+ export interface PersistentAgentRuntimeOptions {
33
+ parentSessionPath: string;
34
+ parentId: string;
35
+ cwd: string;
36
+ execute: (input: PersistentRuntimeExecutorInput) => Promise<TaskExecutionOutput>;
37
+ parentDelivery: ParentDeliveryAdapter;
38
+ revive: (agentId: string, sessionManager: SessionManager) => Promise<LiveAgentAdapter>;
39
+ modelHubOperation?: (request: Record<string, unknown>, caller: HubCaller) => Promise<unknown>;
40
+ onRelease?: (agentId: string) => void | Promise<void>;
41
+ }
42
+
43
+ export class PersistentAgentRuntime {
44
+ readonly layout: SidecarLayout;
45
+ readonly journal: CoordinatorJournal;
46
+ readonly task: TaskCoordinator;
47
+ readonly hub: HubService;
48
+ readonly delivery: AsyncDeliveryService;
49
+ private readonly childManagers = new Map<string, SessionManager>();
50
+
51
+ private constructor(
52
+ private readonly options: PersistentAgentRuntimeOptions,
53
+ initialized: { layout: SidecarLayout; journal: CoordinatorJournal },
54
+ ) {
55
+ this.layout = initialized.layout;
56
+ this.journal = initialized.journal;
57
+ this.delivery = new AsyncDeliveryService(this.layout, this.journal, options.parentDelivery);
58
+ this.task = new TaskCoordinator({
59
+ journal: this.journal,
60
+ execute: async (input) => {
61
+ const manager = await this.childManager(input.agentId);
62
+ return await options.execute({ ...input, sessionManager: manager });
63
+ },
64
+ onSettled: async (settlement, fullOutput) => {
65
+ await persistFullAgentOutput(this.layout, settlement.agentId, fullOutput);
66
+ },
67
+ onAsyncSettled: async (settlement, fullOutput) => {
68
+ await this.delivery.complete(settlement, fullOutput);
69
+ },
70
+ });
71
+ this.hub = new HubService({
72
+ journal: this.journal,
73
+ revive: async (agent) => {
74
+ if (!agent.sessionPath) throw new Error(`${agent.id}: no registered child session`);
75
+ const manager = await openChildSessionManager(this.layout, agent.sessionPath);
76
+ this.childManagers.set(agent.id, manager);
77
+ return await options.revive(agent.id, manager);
78
+ },
79
+ cancelJob: async (jobId) => await this.task.cancel(jobId),
80
+ output: async (agent, offset, limit) => await readAgentOutput(this.layout, this.journal, agent.id, offset, limit),
81
+ history: async (agent, offset, limit) => await readAgentHistory(this.layout, this.journal, agent.id, offset, limit),
82
+ model: options.modelHubOperation,
83
+ onRelease: async (agent) => await options.onRelease?.(agent.id),
84
+ });
85
+ }
86
+
87
+ static async create(options: PersistentAgentRuntimeOptions): Promise<PersistentAgentRuntime> {
88
+ const layout = await ensureSidecarLayout(options.parentSessionPath);
89
+ const resumed = await resumeCoordinator(layout, options.parentId);
90
+ const runtime = new PersistentAgentRuntime(options, { layout, journal: resumed.journal });
91
+ await runtime.delivery.recoverPending();
92
+ return runtime;
93
+ }
94
+
95
+ async shutdown(): Promise<void> {
96
+ await this.task.scheduler.close();
97
+ await reconcileUnfinishedCoordinator(this.journal, "graceful-shutdown");
98
+ for (const manager of this.childManagers.values()) void manager;
99
+ this.childManagers.clear();
100
+ }
101
+
102
+ private async childManager(agentId: string): Promise<SessionManager> {
103
+ const existing = this.childManagers.get(agentId);
104
+ if (existing) return existing;
105
+ const record = this.journal.getState().agents[agentId];
106
+ if (!record) throw new Error(`${agentId}: unknown Agent before child session creation`);
107
+ if (record.sessionPath) {
108
+ const manager = await openChildSessionManager(this.layout, record.sessionPath);
109
+ this.childManagers.set(agentId, manager);
110
+ return manager;
111
+ }
112
+ const created = await createChildSessionManager(this.layout, this.options.cwd, agentId);
113
+ await registerChildSession(this.journal, agentId, created.sessionPath);
114
+ this.childManagers.set(agentId, created.sessionManager);
115
+ return created.sessionManager;
116
+ }
117
+ }
118
+
119
+ export interface InternalPersistentToolRegistrationOptions {
120
+ runtimeForContext: (context: ExtensionContext) => Promise<PersistentAgentRuntime>;
121
+ directModelCommand?: (args: string, context: ExtensionContext) => Promise<string>;
122
+ }
123
+
124
+ /**
125
+ * Canonical registration surface shared by production and deterministic tests.
126
+ * It registers only task/hub plus the direct-user model command when configured;
127
+ * no legacy compatibility alias is created.
128
+ */
129
+ export function registerPersistentAgentTools(pi: ExtensionAPI, options: InternalPersistentToolRegistrationOptions): void {
130
+ pi.registerTool({
131
+ name: "task",
132
+ label: "Task",
133
+ description: "Create one or more parent-scoped persistent AILI Agents; omitted agent defaults to general. Use async:false to wait synchronously or async:true for background execution. Never send blocking: it is profile-only internal metadata.",
134
+ parameters: TASK_TOOL_SCHEMA,
135
+ async execute(_toolCallId, params, signal, _onUpdate, context) {
136
+ const runtime = await options.runtimeForContext(context);
137
+ const result = await runtime.task.submit(params, undefined, signal);
138
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
139
+ },
140
+ });
141
+ pi.registerTool({
142
+ name: "hub",
143
+ label: "Hub",
144
+ description: "Inspect and control persistent Agents, jobs, messages, output, history, cancellation, and model requests.",
145
+ parameters: HUB_TOOL_SCHEMA,
146
+ async execute(_toolCallId, params, _signal, _onUpdate, context) {
147
+ const runtime = await options.runtimeForContext(context);
148
+ const result = await runtime.hub.execute(params);
149
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
150
+ },
151
+ });
152
+ if (options.directModelCommand) {
153
+ pi.registerCommand("aili-agent-model", {
154
+ description: "Direct user operation for AILI Agent instance/global/project model overrides",
155
+ handler: async (args, context) => {
156
+ try {
157
+ context.ui.notify(await options.directModelCommand!(args, context), "info");
158
+ } catch (error) {
159
+ context.ui.notify(error instanceof Error ? error.message : String(error), "error");
160
+ }
161
+ },
162
+ });
163
+ }
164
+ }
@@ -0,0 +1,68 @@
1
+ import { lstat } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import { isAbsolute, resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { createBashToolDefinition, type BashOperations, type ToolDefinition } from "@earendil-works/pi-coding-agent";
6
+ import { profileToConfig, readOnlyOverride, type SandboxConfig } from "pi-permission-modes/src/config-load.ts";
7
+ import { createSandboxedBashOps } from "pi-permission-modes/src/sandbox.ts";
8
+ import type { SandboxProfile } from "pi-permission-modes/src/schema.ts";
9
+ import { resolvePermissionModesPackageRoot } from "../package-resolution.js";
10
+
11
+ type SandboxManagerType = Parameters<typeof createSandboxedBashOps>[0];
12
+ let managerPromise: Promise<SandboxManagerType> | undefined;
13
+
14
+ function resolveProfilePath(cwd: string, path: string): string {
15
+ if (path === ".") return cwd;
16
+ if (path.startsWith("./") || path.startsWith("../")) return resolve(cwd, path);
17
+ return isAbsolute(path) ? path : path;
18
+ }
19
+
20
+ async function installedSandboxManager(): Promise<SandboxManagerType> {
21
+ managerPromise ??= (async () => {
22
+ const permissionRoot = resolvePermissionModesPackageRoot();
23
+ const requireFromPermissionModes = createRequire(new URL("package.json", permissionRoot));
24
+ const entry = requireFromPermissionModes.resolve("@anthropic-ai/sandbox-runtime");
25
+ const loaded = await import(pathToFileURL(entry).href) as { SandboxManager?: SandboxManagerType };
26
+ if (!loaded.SandboxManager) throw new Error("sandbox runtime did not export SandboxManager");
27
+ return loaded.SandboxManager;
28
+ })();
29
+ return await managerPromise;
30
+ }
31
+
32
+ export function childSandboxConfig(profile: SandboxProfile, cwd: string): Partial<SandboxConfig> {
33
+ const projected = profileToConfig({
34
+ ...profile,
35
+ allowWrite: (profile.allowWrite ?? []).map((path) => resolveProfilePath(cwd, path)),
36
+ denyWrite: (profile.denyWrite ?? []).map((path) => resolveProfilePath(cwd, path)),
37
+ denyRead: (profile.denyRead ?? []).map((path) => resolveProfilePath(cwd, path)),
38
+ });
39
+ return profile.writable ? projected : readOnlyOverride(projected);
40
+ }
41
+
42
+ export async function childGitMetadataBlocksSandbox(cwd: string): Promise<boolean> {
43
+ try {
44
+ return (await lstat(resolve(cwd, ".git"))).isFile();
45
+ } catch (error) {
46
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ export async function createChildSandboxBash(
52
+ profile: SandboxProfile,
53
+ cwd: string,
54
+ ): Promise<{ operations?: BashOperations; definition?: ToolDefinition; reason?: string }> {
55
+ if (!profile.enabled) return { reason: "sandbox-disabled-by-mode" };
56
+ if (await childGitMetadataBlocksSandbox(cwd)) return { reason: "sandbox-unavailable-for-git-worktree-metadata" };
57
+ try {
58
+ const manager = await installedSandboxManager();
59
+ if (!manager.isSandboxingEnabled()) return { reason: "parent-sandbox-controller-unavailable" };
60
+ const operations = createSandboxedBashOps(manager, childSandboxConfig(profile, cwd));
61
+ return {
62
+ operations,
63
+ definition: createBashToolDefinition(cwd, { operations }) as unknown as ToolDefinition,
64
+ };
65
+ } catch (error) {
66
+ return { reason: `sandbox-load-failed:${error instanceof Error ? error.message : String(error)}` };
67
+ }
68
+ }
@@ -0,0 +1,190 @@
1
+ export const DEFAULT_PARENT_CONCURRENCY = 32;
2
+ export const DEFAULT_AGENT_MAX_RUNTIME_MS = 0;
3
+ export const DEFAULT_AGENT_SOFT_REQUEST_BUDGET = 0;
4
+
5
+ export class ScheduledTaskCancelledError extends Error {
6
+ constructor(readonly jobId: string, readonly beforeStart: boolean) {
7
+ super(`${jobId}: scheduled task cancelled${beforeStart ? " before start" : ""}`);
8
+ this.name = "ScheduledTaskCancelledError";
9
+ }
10
+ }
11
+
12
+ export interface SchedulerPermit {
13
+ readonly ownerJobId: string;
14
+ readonly token: symbol;
15
+ }
16
+
17
+ export interface ScheduledExecutionContext {
18
+ signal: AbortSignal;
19
+ permit: SchedulerPermit;
20
+ maxRuntimeMs: 0;
21
+ softRequestBudget: 0;
22
+ nested: boolean;
23
+ }
24
+
25
+ export interface ScheduledHandle<T> {
26
+ jobId: string;
27
+ result: Promise<T>;
28
+ state(): "queued" | "running" | "settled" | "cancelled";
29
+ }
30
+
31
+ interface QueueEntry<T> {
32
+ jobId: string;
33
+ controller: AbortController;
34
+ run: (context: ScheduledExecutionContext) => Promise<T>;
35
+ onCancelBeforeStart?: () => void | Promise<void>;
36
+ resolve: (result: T) => void;
37
+ reject: (error: unknown) => void;
38
+ state: "queued" | "running" | "settled" | "cancelled";
39
+ permit?: SchedulerPermit;
40
+ }
41
+
42
+ export class FifoTurnScheduler {
43
+ private readonly queue: Array<QueueEntry<unknown>> = [];
44
+ private readonly running = new Map<string, QueueEntry<unknown>>();
45
+ private readonly known = new Set<string>();
46
+ private readonly permitTokens = new Set<symbol>();
47
+ private readonly nestedTails = new Map<symbol, Promise<void>>();
48
+ private closed = false;
49
+
50
+ constructor(readonly capacity = DEFAULT_PARENT_CONCURRENCY) {
51
+ if (!Number.isSafeInteger(capacity) || capacity <= 0) throw new Error("scheduler capacity must be a positive integer");
52
+ }
53
+
54
+ enqueue<T>(
55
+ jobId: string,
56
+ run: (context: ScheduledExecutionContext) => Promise<T>,
57
+ onCancelBeforeStart?: () => void | Promise<void>,
58
+ ): ScheduledHandle<T> {
59
+ if (this.closed) throw new Error("scheduler is closed");
60
+ if (this.known.has(jobId)) throw new Error(`${jobId}: duplicate scheduled job`);
61
+ this.known.add(jobId);
62
+ let resolveResult!: (result: T) => void;
63
+ let rejectResult!: (error: unknown) => void;
64
+ const result = new Promise<T>((resolve, reject) => {
65
+ resolveResult = resolve;
66
+ rejectResult = reject;
67
+ });
68
+ const entry: QueueEntry<T> = {
69
+ jobId,
70
+ controller: new AbortController(),
71
+ run,
72
+ onCancelBeforeStart,
73
+ resolve: resolveResult,
74
+ reject: rejectResult,
75
+ state: "queued",
76
+ };
77
+ this.queue.push(entry as QueueEntry<unknown>);
78
+ this.drain();
79
+ return { jobId, result, state: () => entry.state };
80
+ }
81
+
82
+ runNested<T>(
83
+ jobId: string,
84
+ inheritedPermit: SchedulerPermit,
85
+ run: (context: ScheduledExecutionContext) => Promise<T>,
86
+ ): ScheduledHandle<T> {
87
+ if (this.closed) throw new Error("scheduler is closed");
88
+ if (!this.permitTokens.has(inheritedPermit.token)) throw new Error(`${jobId}: nested execution requires an active ancestor permit`);
89
+ if (this.known.has(jobId)) throw new Error(`${jobId}: duplicate scheduled job`);
90
+ this.known.add(jobId);
91
+ let state: "queued" | "running" | "settled" | "cancelled" = "queued";
92
+ const controller = new AbortController();
93
+ const previous = this.nestedTails.get(inheritedPermit.token) ?? Promise.resolve();
94
+ const operation = previous.then(async () => {
95
+ state = "running";
96
+ return await run({
97
+ signal: controller.signal,
98
+ permit: inheritedPermit,
99
+ maxRuntimeMs: DEFAULT_AGENT_MAX_RUNTIME_MS,
100
+ softRequestBudget: DEFAULT_AGENT_SOFT_REQUEST_BUDGET,
101
+ nested: true,
102
+ });
103
+ });
104
+ const result = operation.then(
105
+ (value) => {
106
+ state = "settled";
107
+ return value;
108
+ },
109
+ (error) => {
110
+ state = controller.signal.aborted ? "cancelled" : "settled";
111
+ throw error;
112
+ },
113
+ );
114
+ this.nestedTails.set(inheritedPermit.token, result.then(() => undefined, () => undefined));
115
+ return { jobId, result, state: () => state };
116
+ }
117
+
118
+ async cancel(jobId: string): Promise<"queued" | "running" | "not-found"> {
119
+ const queuedIndex = this.queue.findIndex((entry) => entry.jobId === jobId);
120
+ if (queuedIndex >= 0) {
121
+ const [entry] = this.queue.splice(queuedIndex, 1);
122
+ entry!.state = "cancelled";
123
+ entry!.controller.abort(new ScheduledTaskCancelledError(jobId, true));
124
+ try {
125
+ await entry!.onCancelBeforeStart?.();
126
+ } finally {
127
+ entry!.reject(new ScheduledTaskCancelledError(jobId, true));
128
+ }
129
+ return "queued";
130
+ }
131
+ const running = this.running.get(jobId);
132
+ if (running) {
133
+ running.controller.abort(new ScheduledTaskCancelledError(jobId, false));
134
+ return "running";
135
+ }
136
+ return "not-found";
137
+ }
138
+
139
+ isPermitActive(permit: SchedulerPermit): boolean {
140
+ return this.permitTokens.has(permit.token);
141
+ }
142
+
143
+ stats(): { active: number; capacity: number; queued: string[]; running: string[]; closed: boolean } {
144
+ return {
145
+ active: this.running.size,
146
+ capacity: this.capacity,
147
+ queued: this.queue.map((entry) => entry.jobId),
148
+ running: [...this.running.keys()],
149
+ closed: this.closed,
150
+ };
151
+ }
152
+
153
+ async close(): Promise<void> {
154
+ this.closed = true;
155
+ for (const entry of [...this.queue]) await this.cancel(entry.jobId);
156
+ for (const entry of this.running.values()) entry.controller.abort(new ScheduledTaskCancelledError(entry.jobId, false));
157
+ }
158
+
159
+ private drain(): void {
160
+ while (!this.closed && this.running.size < this.capacity && this.queue.length > 0) {
161
+ const entry = this.queue.shift()!;
162
+ entry.state = "running";
163
+ const permit: SchedulerPermit = { ownerJobId: entry.jobId, token: Symbol(entry.jobId) };
164
+ entry.permit = permit;
165
+ this.permitTokens.add(permit.token);
166
+ this.running.set(entry.jobId, entry);
167
+ void entry.run({
168
+ signal: entry.controller.signal,
169
+ permit,
170
+ maxRuntimeMs: DEFAULT_AGENT_MAX_RUNTIME_MS,
171
+ softRequestBudget: DEFAULT_AGENT_SOFT_REQUEST_BUDGET,
172
+ nested: false,
173
+ }).then(
174
+ (result) => {
175
+ entry.state = "settled";
176
+ entry.resolve(result);
177
+ },
178
+ (error) => {
179
+ entry.state = entry.controller.signal.aborted ? "cancelled" : "settled";
180
+ entry.reject(error);
181
+ },
182
+ ).finally(() => {
183
+ this.running.delete(entry.jobId);
184
+ this.permitTokens.delete(permit.token);
185
+ this.nestedTails.delete(permit.token);
186
+ this.drain();
187
+ });
188
+ }
189
+ }
190
+ }
@@ -0,0 +1,184 @@
1
+ import {
2
+ createAgentSession,
3
+ DefaultResourceLoader,
4
+ SettingsManager,
5
+ type AgentSession,
6
+ type CreateAgentSessionOptions,
7
+ type ExtensionAPI,
8
+ type ExtensionFactory,
9
+ type InlineExtension,
10
+ type SessionManager,
11
+ } from "@earendil-works/pi-coding-agent";
12
+ import { bashMentionsCredentialPath, isProtectedChildPath } from "../credential-guard.js";
13
+ import { findCredentialMaterial } from "./permission.js";
14
+ import type { ChildPromptAssembly, EffectiveToolPolicy } from "./policy.js";
15
+
16
+ export type ChildPermissionAction = "allow" | "ask" | "deny";
17
+
18
+ export interface ChildApprovalPacket {
19
+ agentId: string;
20
+ jobId?: string;
21
+ toolName: string;
22
+ summary: string;
23
+ }
24
+
25
+ export interface ChildApprovalBridgeOptions {
26
+ agentId: string;
27
+ jobId?: string;
28
+ cwd: string;
29
+ decide: (toolName: string, input: Record<string, unknown>) => ChildPermissionAction | Promise<ChildPermissionAction>;
30
+ requestApproval: (packet: ChildApprovalPacket) => "allow" | "deny" | Promise<"allow" | "deny">;
31
+ }
32
+
33
+ const FILE_TOOLS = new Set(["read", "write", "edit", "ls", "grep", "find"]);
34
+
35
+ function redactedSummary(toolName: string, input: Record<string, unknown>): string {
36
+ if (FILE_TOOLS.has(toolName) && typeof input.path === "string") return `${toolName} ${input.path}`.slice(0, 500);
37
+ if (toolName === "bash" && typeof input.command === "string") {
38
+ const command = input.command
39
+ .replace(/\b(authorization:\s*bearer|bearer)\s+\S+/gi, "$1 <redacted>")
40
+ .replace(/\b(token|secret|password|passwd|api[_-]?key|private[_-]?key)=\S+/gi, "$1=<redacted>")
41
+ .replace(/\s+/g, " ")
42
+ .trim();
43
+ return `bash ${command}`.slice(0, 500);
44
+ }
45
+ return `${toolName} request`;
46
+ }
47
+
48
+ async function credentialDenial(cwd: string, toolName: string, input: Record<string, unknown>): Promise<string | undefined> {
49
+ if (FILE_TOOLS.has(toolName) && typeof input.path === "string" && await isProtectedChildPath(cwd, input.path)) {
50
+ return "AILI child denied credential/auth/private-key path access before approval";
51
+ }
52
+ if (toolName === "bash" && typeof input.command === "string" && bashMentionsCredentialPath(input.command)) {
53
+ return "AILI child denied credential/auth/private-key path access in bash before approval";
54
+ }
55
+ const generic = await findCredentialMaterial(input, cwd);
56
+ if (generic) return `AILI child denied credential/auth/private-key material before approval (${generic.reason})`;
57
+ return undefined;
58
+ }
59
+
60
+ export function createChildApprovalBridge(options: ChildApprovalBridgeOptions): ExtensionFactory {
61
+ return (pi: ExtensionAPI) => {
62
+ pi.on("tool_call", async (event) => {
63
+ const input = (event.input && typeof event.input === "object" ? event.input : {}) as Record<string, unknown>;
64
+ const hardDenial = await credentialDenial(options.cwd, event.toolName, input);
65
+ if (hardDenial) return { block: true, reason: hardDenial };
66
+ let action: ChildPermissionAction;
67
+ try {
68
+ action = await options.decide(event.toolName, input);
69
+ } catch {
70
+ return { block: true, reason: "AILI child permission classification failed closed" };
71
+ }
72
+ if (action === "deny") return { block: true, reason: "AILI child permission policy denied this tool call" };
73
+ if (action === "allow") return undefined;
74
+ try {
75
+ const decision = await options.requestApproval({
76
+ agentId: options.agentId,
77
+ jobId: options.jobId,
78
+ toolName: event.toolName,
79
+ summary: redactedSummary(event.toolName, input),
80
+ });
81
+ return decision === "allow" ? undefined : { block: true, reason: "AILI parent approval denied or dismissed" };
82
+ } catch {
83
+ return { block: true, reason: "AILI parent approval bridge unavailable" };
84
+ }
85
+ });
86
+ };
87
+ }
88
+
89
+ export interface ChildResourceLoaderOptions {
90
+ cwd: string;
91
+ agentDir: string;
92
+ projectTrusted: boolean;
93
+ systemPrompt: string;
94
+ childExtensions?: Array<{ name: string; factory: ExtensionFactory }>;
95
+ topLevelExtensionNames?: string[];
96
+ }
97
+
98
+ export async function createChildResourceLoader(options: ChildResourceLoaderOptions): Promise<{
99
+ loader: DefaultResourceLoader;
100
+ settingsManager: SettingsManager;
101
+ }> {
102
+ const forbidden = new Set(options.topLevelExtensionNames ?? ["aili-top-coordinator", "aili-runtime"]);
103
+ const extensions: InlineExtension[] = [];
104
+ for (const extension of options.childExtensions ?? []) {
105
+ if (forbidden.has(extension.name)) throw new Error(`${extension.name}: top-level Extension cannot be loaded in a child Agent`);
106
+ if (extensions.some((candidate) => candidate.name === extension.name)) throw new Error(`${extension.name}: duplicate child Extension`);
107
+ extensions.push({ name: extension.name, factory: extension.factory, hidden: true });
108
+ }
109
+ const settingsManager = SettingsManager.inMemory({}, { projectTrusted: options.projectTrusted });
110
+ const loader = new DefaultResourceLoader({
111
+ cwd: options.cwd,
112
+ agentDir: options.agentDir,
113
+ settingsManager,
114
+ noExtensions: true,
115
+ noSkills: true,
116
+ noPromptTemplates: true,
117
+ noThemes: true,
118
+ noContextFiles: true,
119
+ systemPrompt: options.systemPrompt,
120
+ extensionFactories: extensions,
121
+ extensionsOverride: (base) => ({
122
+ ...base,
123
+ extensions: base.extensions.filter((extension) => ![...forbidden].some((name) => extension.path === `<inline:${name}>` || extension.path.endsWith(`/${name}`))),
124
+ }),
125
+ });
126
+ await loader.reload();
127
+ const errors = loader.getExtensions().errors;
128
+ if (errors.length > 0) throw new Error(`child resource loader failed: ${errors.map((error) => error.error).join("; ")}`);
129
+ return { loader, settingsManager };
130
+ }
131
+
132
+ export interface CreatePersistentChildSessionOptions {
133
+ cwd: string;
134
+ agentDir: string;
135
+ projectTrusted: boolean;
136
+ sessionManager: SessionManager;
137
+ prompt: ChildPromptAssembly;
138
+ policy: EffectiveToolPolicy;
139
+ childExtensions?: Array<{ name: string; factory: ExtensionFactory }>;
140
+ topLevelExtensionNames?: string[];
141
+ model?: CreateAgentSessionOptions["model"];
142
+ thinkingLevel?: CreateAgentSessionOptions["thinkingLevel"];
143
+ }
144
+
145
+ export interface PersistentChildSessionRuntime {
146
+ session: AgentSession;
147
+ initialMessage: string;
148
+ dispose(): Promise<void>;
149
+ }
150
+
151
+ export async function createPersistentChildSession(options: CreatePersistentChildSessionOptions): Promise<PersistentChildSessionRuntime> {
152
+ const { loader, settingsManager } = await createChildResourceLoader({
153
+ cwd: options.cwd,
154
+ agentDir: options.agentDir,
155
+ projectTrusted: options.projectTrusted,
156
+ systemPrompt: options.prompt.systemPrompt,
157
+ childExtensions: options.childExtensions,
158
+ topLevelExtensionNames: options.topLevelExtensionNames,
159
+ });
160
+ const created = await createAgentSession({
161
+ cwd: options.cwd,
162
+ agentDir: options.agentDir,
163
+ model: options.model,
164
+ thinkingLevel: options.thinkingLevel,
165
+ resourceLoader: loader,
166
+ settingsManager,
167
+ sessionManager: options.sessionManager,
168
+ tools: options.policy.effectiveTools,
169
+ customTools: options.policy.customTools,
170
+ });
171
+ const active = created.session.getActiveToolNames();
172
+ const missing = options.policy.effectiveTools.filter((name) => !active.includes(name));
173
+ if (missing.length > 0) {
174
+ created.session.dispose();
175
+ throw new Error(`child runtime could not activate approved tools: ${missing.join(", ")}`);
176
+ }
177
+ return {
178
+ session: created.session,
179
+ initialMessage: options.prompt.initialMessage,
180
+ async dispose() {
181
+ created.session.dispose();
182
+ },
183
+ };
184
+ }