@rosetears/aili-pi 0.1.9 → 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 (57) hide show
  1. package/README.md +6 -6
  2. package/THIRD_PARTY_NOTICES.md +12 -12
  3. package/docs/persistent-agents.md +114 -0
  4. package/manifests/adapter-evidence.json +53 -20
  5. package/manifests/capabilities.json +3 -3
  6. package/manifests/live-verification.json +18 -19
  7. package/manifests/provenance.json +12 -12
  8. package/manifests/roles.json +270 -57
  9. package/manifests/sbom.json +1 -128
  10. package/manifests/skill-compatibility.json +57 -61
  11. package/manifests/subagent-provenance.json +8 -15
  12. package/package.json +3 -3
  13. package/roles/agent-evaluator.md +8 -3
  14. package/roles/ai-regression-scout.md +8 -3
  15. package/roles/browser-qa-runner.md +8 -3
  16. package/roles/code-reviewer.md +8 -3
  17. package/roles/code-scout.md +8 -3
  18. package/roles/convergence-reviewer.md +8 -3
  19. package/roles/doc-researcher.md +8 -3
  20. package/roles/e2e-artifact-runner.md +8 -3
  21. package/roles/general.md +49 -0
  22. package/roles/implementer.md +8 -3
  23. package/roles/opensource-sanitizer.md +8 -3
  24. package/roles/plan-auditor.md +8 -3
  25. package/roles/pr-test-analyzer.md +8 -3
  26. package/roles/security-auditor.md +8 -3
  27. package/roles/silent-failure-reviewer.md +8 -3
  28. package/roles/spec-miner.md +8 -3
  29. package/roles/test-coverage-reviewer.md +8 -3
  30. package/roles/test-engineer.md +8 -3
  31. package/roles/web-performance-auditor.md +8 -3
  32. package/roles/web-researcher.md +8 -3
  33. package/scripts/sync-roles.ts +186 -24
  34. package/src/runtime/doctor.ts +38 -10
  35. package/src/runtime/global-resources.ts +5 -1
  36. package/src/runtime/index.ts +2 -2
  37. package/src/runtime/persistent-agents/hub.ts +429 -0
  38. package/src/runtime/persistent-agents/model-selection.ts +363 -0
  39. package/src/runtime/persistent-agents/output-delivery.ts +356 -0
  40. package/src/runtime/persistent-agents/permission.ts +223 -0
  41. package/src/runtime/persistent-agents/policy.ts +311 -0
  42. package/src/runtime/persistent-agents/production.ts +569 -0
  43. package/src/runtime/persistent-agents/runtime.ts +164 -0
  44. package/src/runtime/persistent-agents/sandbox.ts +68 -0
  45. package/src/runtime/persistent-agents/scheduler.ts +190 -0
  46. package/src/runtime/persistent-agents/session-factory.ts +184 -0
  47. package/src/runtime/persistent-agents/storage.ts +775 -0
  48. package/src/runtime/persistent-agents/task-coordinator.ts +460 -0
  49. package/src/runtime/persistent-agents/task-schema.ts +141 -0
  50. package/src/runtime/persistent-agents/types.ts +134 -0
  51. package/src/runtime/persistent-agents/workspace.ts +335 -0
  52. package/src/runtime/registry.ts +28 -32
  53. package/src/runtime/roles.ts +211 -18
  54. package/src/runtime/rose-context.ts +1 -1
  55. package/templates/APPEND_SYSTEM.md +1 -1
  56. package/upstream/opencode-global-agents.lock.json +1 -1
  57. package/src/runtime/subagents.ts +0 -249
@@ -0,0 +1,311 @@
1
+ import type { AgentSession, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ BUNDLED_ROLE_SELECTORS,
4
+ loadRoleProfiles,
5
+ type RoleProfile,
6
+ } from "../roles.js";
7
+
8
+ const LEGACY_OR_TOP_LEVEL_ONLY_TOOLS = new Set(["subagent", "aili_task"]);
9
+ const BUILTIN_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
10
+ const CHILD_BRIDGE_TOOL_NAMES = new Set(["task", "hub"]);
11
+
12
+ export interface ParentToolSnapshot {
13
+ active: string[];
14
+ definitions: Map<string, ToolDefinition>;
15
+ }
16
+
17
+ export interface UnavailableTool {
18
+ name: string;
19
+ reason: "parent-inactive" | "parent-definition-missing" | "child-unloadable" | "role-ceiling" | "hard-guard" | "call-narrowing" | "spawn-policy";
20
+ }
21
+
22
+ export interface EffectiveToolPolicy {
23
+ effectiveTools: string[];
24
+ customTools: ToolDefinition[];
25
+ unavailable: UnavailableTool[];
26
+ parentActiveHashInput: string[];
27
+ }
28
+
29
+ export interface ComputeEffectiveToolsInput {
30
+ parent: ParentToolSnapshot;
31
+ childLoadable: Iterable<string>;
32
+ childDefinitions?: Map<string, ToolDefinition>;
33
+ role: RoleProfile;
34
+ callTools?: string[];
35
+ hardDenied?: Iterable<string>;
36
+ currentDepth: number;
37
+ configuredMaxDepth?: number;
38
+ }
39
+
40
+ export function captureParentToolSnapshot(session: Pick<AgentSession, "getActiveToolNames" | "getToolDefinition">): ParentToolSnapshot {
41
+ const active = [...new Set(session.getActiveToolNames())];
42
+ const definitions = new Map<string, ToolDefinition>();
43
+ for (const name of active) {
44
+ const definition = session.getToolDefinition(name);
45
+ if (definition) definitions.set(name, definition);
46
+ }
47
+ return { active, definitions };
48
+ }
49
+
50
+ export interface SpawnDecision {
51
+ allowed: boolean;
52
+ target: string;
53
+ depth: number;
54
+ async: false;
55
+ reason?: "unknown-selector" | "self-recursion" | "role-disallowed" | "depth-exceeded";
56
+ }
57
+
58
+ export function evaluateSpawn(
59
+ role: RoleProfile,
60
+ target: string,
61
+ currentDepth: number,
62
+ configuredMaxDepth = 2,
63
+ ): SpawnDecision {
64
+ const nextDepth = currentDepth + 1;
65
+ if (!(BUNDLED_ROLE_SELECTORS as readonly string[]).includes(target)) {
66
+ return { allowed: false, target, depth: nextDepth, async: false, reason: "unknown-selector" };
67
+ }
68
+ if (target === role.selector) {
69
+ return { allowed: false, target, depth: nextDepth, async: false, reason: "self-recursion" };
70
+ }
71
+ if (!role.spawns.includes(target)) {
72
+ return { allowed: false, target, depth: nextDepth, async: false, reason: "role-disallowed" };
73
+ }
74
+ const effectiveMaxDepth = Math.min(4, Math.max(0, configuredMaxDepth));
75
+ if (nextDepth > effectiveMaxDepth) {
76
+ return { allowed: false, target, depth: nextDepth, async: false, reason: "depth-exceeded" };
77
+ }
78
+ return { allowed: true, target, depth: nextDepth, async: false };
79
+ }
80
+
81
+ export function computeEffectiveTools(input: ComputeEffectiveToolsInput): EffectiveToolPolicy {
82
+ const childLoadable = new Set(input.childLoadable);
83
+ const hardDenied = new Set([...LEGACY_OR_TOP_LEVEL_ONLY_TOOLS, ...(input.hardDenied ?? [])]);
84
+ const roleCeiling = input.role.toolPolicy === "inherit-parent" ? undefined : new Set(input.role.tools);
85
+ const callCeiling = input.callTools ? new Set(input.callTools) : undefined;
86
+ const unavailable: UnavailableTool[] = [];
87
+ const effectiveTools: string[] = [];
88
+
89
+ const report = (name: string, reason: UnavailableTool["reason"]) => {
90
+ if (!unavailable.some((item) => item.name === name && item.reason === reason)) unavailable.push({ name, reason });
91
+ };
92
+
93
+ for (const name of input.parent.active) {
94
+ if (roleCeiling && !roleCeiling.has(name)) {
95
+ report(name, "role-ceiling");
96
+ continue;
97
+ }
98
+ if (hardDenied.has(name)) {
99
+ report(name, "hard-guard");
100
+ continue;
101
+ }
102
+ if (callCeiling && !callCeiling.has(name)) {
103
+ report(name, "call-narrowing");
104
+ continue;
105
+ }
106
+ if (name === "task") {
107
+ const hasSpawn = input.role.spawns.some((target) => evaluateSpawn(input.role, target, input.currentDepth, input.configuredMaxDepth).allowed);
108
+ if (!hasSpawn) {
109
+ report(name, "spawn-policy");
110
+ continue;
111
+ }
112
+ }
113
+ if (CHILD_BRIDGE_TOOL_NAMES.has(name) && !input.childDefinitions?.has(name)) {
114
+ report(name, "child-unloadable");
115
+ continue;
116
+ }
117
+ if (!input.parent.definitions.has(name) && !input.childDefinitions?.has(name) && !BUILTIN_TOOL_NAMES.has(name)) {
118
+ report(name, "parent-definition-missing");
119
+ continue;
120
+ }
121
+ if (!childLoadable.has(name)) {
122
+ report(name, "child-unloadable");
123
+ continue;
124
+ }
125
+ effectiveTools.push(name);
126
+ }
127
+
128
+ for (const name of roleCeiling ?? []) {
129
+ if (!input.parent.active.includes(name)) report(name, "parent-inactive");
130
+ }
131
+ for (const name of callCeiling ?? []) {
132
+ if (!input.parent.active.includes(name)) report(name, "parent-inactive");
133
+ }
134
+
135
+ const customTools = effectiveTools
136
+ .filter((name) => !BUILTIN_TOOL_NAMES.has(name))
137
+ .map((name) => input.childDefinitions?.get(name) ?? input.parent.definitions.get(name))
138
+ .filter((definition): definition is ToolDefinition => definition !== undefined);
139
+ return {
140
+ effectiveTools,
141
+ customTools,
142
+ unavailable,
143
+ parentActiveHashInput: [...input.parent.active].sort(),
144
+ };
145
+ }
146
+
147
+ export interface TrustedContextResource {
148
+ kind: "rule" | "skill" | "context" | "shared";
149
+ path: string;
150
+ content: string;
151
+ trusted: boolean;
152
+ }
153
+
154
+ export interface ChildPromptInput {
155
+ runtimeEnvelope: string;
156
+ role: RoleProfile;
157
+ task: string;
158
+ context?: string;
159
+ cwd: string;
160
+ workspace: { mode: "shared" | "isolated"; root: string; diagnostic?: string };
161
+ resources?: TrustedContextResource[];
162
+ approvedPlanRef?: string;
163
+ sharedRefs?: string[];
164
+ }
165
+
166
+ export interface ChildPromptAssembly {
167
+ systemPrompt: string;
168
+ initialMessage: string;
169
+ includedResources: Array<{ kind: TrustedContextResource["kind"]; path: string }>;
170
+ diagnostics: string[];
171
+ }
172
+
173
+ function section(title: string, body: string | undefined): string[] {
174
+ if (!body?.trim()) return [];
175
+ return [`## ${title}`, "", body.trim(), ""];
176
+ }
177
+
178
+ export function assembleChildPrompt(input: ChildPromptInput): ChildPromptAssembly {
179
+ if (!input.task.trim()) throw new Error("child task must be non-empty");
180
+ const trusted = (input.resources ?? []).filter((resource) => resource.trusted);
181
+ const untrusted = (input.resources ?? []).filter((resource) => !resource.trusted);
182
+ const systemPrompt = [
183
+ ...section("AILI persistent Agent runtime", input.runtimeEnvelope),
184
+ ...section("Selected role profile", input.role.prompt),
185
+ ...section("Workspace", `mode: ${input.workspace.mode}\nroot: ${input.workspace.root}${input.workspace.diagnostic ? `\ndiagnostic: ${input.workspace.diagnostic}` : ""}`),
186
+ ...trusted.flatMap((resource) => section(`Trusted ${resource.kind}: ${resource.path}`, resource.content)),
187
+ "The parent conversation is not part of this child context. Use only the explicit assignment/context and trusted resources above.",
188
+ ].join("\n").trim();
189
+ const initialMessage = [
190
+ ...section("Assignment", input.task),
191
+ ...section("Explicit context", input.context),
192
+ ...section("Current working directory", input.cwd),
193
+ ...section("Approved plan reference", input.approvedPlanRef),
194
+ ...section("Shared references", input.sharedRefs?.join("\n")),
195
+ ].join("\n").trim();
196
+ return {
197
+ systemPrompt,
198
+ initialMessage,
199
+ includedResources: trusted.map(({ kind, path }) => ({ kind, path })),
200
+ diagnostics: untrusted.map((resource) => `${resource.kind}:${resource.path}: excluded because project/resource trust is inactive`),
201
+ };
202
+ }
203
+
204
+ export interface TurnPolicyAudit {
205
+ selector: string;
206
+ profileHash: string;
207
+ sourceHash: string;
208
+ profileVersion: number;
209
+ runtimeAdapterVersion: number;
210
+ effectiveTools: string[];
211
+ parentActiveTools: string[];
212
+ unavailableTools: UnavailableTool[];
213
+ depth: number;
214
+ provider?: string;
215
+ model?: string;
216
+ thinking?: string;
217
+ }
218
+
219
+ export interface TurnRuntimeHandle {
220
+ dispose(): void | Promise<void>;
221
+ }
222
+
223
+ export interface PrepareTurnInput<T extends TurnRuntimeHandle> {
224
+ selector: string;
225
+ parent: ParentToolSnapshot;
226
+ childLoadable: Iterable<string>;
227
+ childDefinitions?: Map<string, ToolDefinition>;
228
+ callTools?: string[];
229
+ hardDenied?: Iterable<string>;
230
+ depth: number;
231
+ configuredMaxDepth?: number;
232
+ modelAudit?: { provider?: string; model?: string; thinking?: string };
233
+ loadProfiles?: () => Promise<RoleProfile[]>;
234
+ build: (role: RoleProfile, policy: EffectiveToolPolicy, audit: TurnPolicyAudit) => Promise<T>;
235
+ }
236
+
237
+ export class TurnBoundaryPolicyManager<T extends TurnRuntimeHandle> {
238
+ private current?: { handle: T; key: string; audit: TurnPolicyAudit };
239
+ private running = false;
240
+
241
+ markRunning(): void {
242
+ if (this.running) throw new Error("Agent already has an active turn");
243
+ if (!this.current) throw new Error("Agent runtime is not prepared");
244
+ this.running = true;
245
+ }
246
+
247
+ markSettled(): void {
248
+ this.running = false;
249
+ }
250
+
251
+ getAudit(): TurnPolicyAudit | undefined {
252
+ return this.current ? structuredClone(this.current.audit) : undefined;
253
+ }
254
+
255
+ async prepareAtTurnBoundary(input: PrepareTurnInput<T>): Promise<{ handle: T; audit: TurnPolicyAudit; rebuilt: boolean }> {
256
+ if (this.running) throw new Error("cannot hot reload policy during an in-flight turn");
257
+ const profiles = await (input.loadProfiles ?? loadRoleProfiles)();
258
+ const role = profiles.find((candidate) => candidate.selector === input.selector);
259
+ if (!role) throw new Error(`${input.selector}: selected role profile is unavailable or invalid`);
260
+ const policy = computeEffectiveTools({
261
+ parent: input.parent,
262
+ childLoadable: input.childLoadable,
263
+ childDefinitions: input.childDefinitions,
264
+ role,
265
+ callTools: input.callTools,
266
+ hardDenied: input.hardDenied,
267
+ currentDepth: input.depth,
268
+ configuredMaxDepth: input.configuredMaxDepth,
269
+ });
270
+ const audit: TurnPolicyAudit = {
271
+ selector: role.selector,
272
+ profileHash: role.profileHash,
273
+ sourceHash: role.sourceHash,
274
+ profileVersion: role.profileVersion,
275
+ runtimeAdapterVersion: role.runtimeAdapterVersion,
276
+ effectiveTools: policy.effectiveTools,
277
+ parentActiveTools: [...input.parent.active],
278
+ unavailableTools: policy.unavailable,
279
+ depth: input.depth,
280
+ provider: input.modelAudit?.provider,
281
+ model: input.modelAudit?.model,
282
+ thinking: input.modelAudit?.thinking,
283
+ };
284
+ const key = JSON.stringify({
285
+ selector: audit.selector,
286
+ profileHash: audit.profileHash,
287
+ sourceHash: audit.sourceHash,
288
+ tools: audit.effectiveTools,
289
+ parent: audit.parentActiveTools,
290
+ depth: audit.depth,
291
+ provider: audit.provider,
292
+ model: audit.model,
293
+ thinking: audit.thinking,
294
+ });
295
+ if (this.current?.key === key) return { handle: this.current.handle, audit: structuredClone(audit), rebuilt: false };
296
+
297
+ if (this.current) {
298
+ await this.current.handle.dispose();
299
+ this.current = undefined;
300
+ }
301
+ const handle = await input.build(role, policy, audit);
302
+ this.current = { handle, key, audit };
303
+ return { handle, audit: structuredClone(audit), rebuilt: true };
304
+ }
305
+
306
+ async dispose(): Promise<void> {
307
+ if (this.current) await this.current.handle.dispose();
308
+ this.current = undefined;
309
+ this.running = false;
310
+ }
311
+ }