@oai404iao/pi-subagent 0.3.0 → 0.4.0

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.
@@ -0,0 +1,146 @@
1
+ import type {
2
+ SubagentDescriptor,
3
+ SubagentTask,
4
+ } from "./types.ts";
5
+
6
+ export const ROOT_TASK_PATH = "/root";
7
+ export const MAX_TASK_NAME_LENGTH = 64;
8
+ export const MAX_TASK_PATH_LENGTH = 4096;
9
+
10
+ const TASK_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
11
+ const AGENT_ID_PATTERN =
12
+ /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
13
+
14
+ function pathSegments(path: string): string[] {
15
+ if (path.length > MAX_TASK_PATH_LENGTH) {
16
+ throw new Error(
17
+ `task path exceeds ${MAX_TASK_PATH_LENGTH} characters`,
18
+ );
19
+ }
20
+ if (!path.startsWith("/") || path.endsWith("/") || path.includes("//")) {
21
+ throw new Error(`task path must be a canonical absolute path under ${ROOT_TASK_PATH}`);
22
+ }
23
+ const segments = path.slice(1).split("/");
24
+ if (segments[0] !== "root") {
25
+ throw new Error(`task path must be rooted at ${ROOT_TASK_PATH}`);
26
+ }
27
+ return segments;
28
+ }
29
+
30
+ export function validateTaskName(value: string): string {
31
+ if (!TASK_NAME_PATTERN.test(value)) {
32
+ throw new Error(
33
+ "task_name must contain 1-64 lowercase ASCII letters, digits, hyphens, or underscores and start with a letter or digit",
34
+ );
35
+ }
36
+ if (value === "root") {
37
+ throw new Error(`task_name "${value}" is reserved`);
38
+ }
39
+ return value;
40
+ }
41
+
42
+ export function validateTaskPath(path: string): string {
43
+ if (path === ROOT_TASK_PATH) return path;
44
+ const segments = pathSegments(path);
45
+ for (let index = 1; index < segments.length; index++) {
46
+ validateTaskName(segments[index]!);
47
+ }
48
+ return path;
49
+ }
50
+
51
+ export function taskPath(parentPath: string, name: string): string {
52
+ validateTaskPath(parentPath);
53
+ validateTaskName(name);
54
+ const path = `${parentPath}/${name}`;
55
+ return validateTaskPath(path);
56
+ }
57
+
58
+ export function descriptorTaskPath(
59
+ descriptor: SubagentDescriptor,
60
+ ): string {
61
+ return descriptor.task.path;
62
+ }
63
+
64
+ export function descriptorTask(
65
+ descriptor: SubagentDescriptor,
66
+ ): SubagentTask {
67
+ return { ...descriptor.task };
68
+ }
69
+
70
+ export function validateDescriptorTask(task: SubagentTask): SubagentTask {
71
+ const name = validateTaskName(task.name);
72
+ const path = validateTaskPath(task.path);
73
+ if (path === ROOT_TASK_PATH || path.split("/").at(-1) !== name) {
74
+ throw new Error("task.path must end with task.name");
75
+ }
76
+ return { name, path };
77
+ }
78
+
79
+ export function slugTaskName(value: string): string {
80
+ const normalized = value
81
+ .normalize("NFKD")
82
+ .replace(/\p{Mark}/gu, "")
83
+ .toLowerCase()
84
+ .replace(/[^a-z0-9_-]+/g, "-")
85
+ .replace(/[-_]{2,}/g, "-")
86
+ .replace(/^[-_]+|[-_]+$/g, "")
87
+ .slice(0, MAX_TASK_NAME_LENGTH)
88
+ .replace(/[-_]+$/g, "");
89
+ const candidate = !normalized || normalized === "root" ? "task" : normalized;
90
+ return validateTaskName(candidate);
91
+ }
92
+
93
+ export function numberedTaskName(base: string, ordinal: number): string {
94
+ validateTaskName(base);
95
+ if (!Number.isSafeInteger(ordinal) || ordinal < 2) {
96
+ throw new Error("task name ordinal must be an integer of at least 2");
97
+ }
98
+ const suffix = `-${ordinal}`;
99
+ const prefix = base
100
+ .slice(0, MAX_TASK_NAME_LENGTH - suffix.length)
101
+ .replace(/[-_]+$/g, "");
102
+ return validateTaskName(`${prefix || "task"}${suffix}`);
103
+ }
104
+
105
+ export function resolveTaskPath(
106
+ basePath: string,
107
+ reference: string,
108
+ ): string {
109
+ validateTaskPath(basePath);
110
+ if (!reference.trim() || reference !== reference.trim()) {
111
+ throw new Error("task path reference must be a non-empty trimmed string");
112
+ }
113
+ if (reference.length > MAX_TASK_PATH_LENGTH) {
114
+ throw new Error(
115
+ `task path reference exceeds ${MAX_TASK_PATH_LENGTH} characters`,
116
+ );
117
+ }
118
+ if (reference.includes("//") || (reference.length > 1 && reference.endsWith("/"))) {
119
+ throw new Error("task path reference contains an empty segment");
120
+ }
121
+
122
+ const absolute = reference.startsWith("/");
123
+ const segments = absolute ? ["root"] : pathSegments(basePath);
124
+ const input = absolute ? reference.slice(1).split("/") : reference.split("/");
125
+ if (absolute && input.shift() !== "root") {
126
+ throw new Error(`absolute task paths must be rooted at ${ROOT_TASK_PATH}`);
127
+ }
128
+ for (const segment of input) {
129
+ if (!segment || segment === ".") continue;
130
+ if (segment === "..") {
131
+ if (segments.length === 1) {
132
+ throw new Error(`task path reference cannot escape ${ROOT_TASK_PATH}`);
133
+ }
134
+ segments.pop();
135
+ continue;
136
+ }
137
+ validateTaskName(segment);
138
+ segments.push(segment);
139
+ }
140
+ const resolved = `/${segments.join("/")}`;
141
+ return validateTaskPath(resolved);
142
+ }
143
+
144
+ export function isAgentId(value: string): boolean {
145
+ return AGENT_ID_PATTERN.test(value);
146
+ }
package/src/types.ts CHANGED
@@ -1,20 +1,36 @@
1
1
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import type { Usage } from "@earendil-works/pi-ai";
3
3
 
4
+ /** Current on-disk subagent descriptor version. */
5
+ export const DESCRIPTOR_VERSION = 4;
6
+
4
7
  export type AgentScope = "user" | "project" | "both";
5
- export type AgentSource = "bundled" | "user" | "project";
6
- export type ReportDelivery = "wakeup" | "quiet";
8
+ /** Sources that runtime discovery is allowed to activate. */
9
+ export type AgentSource = "user" | "project";
10
+ /**
11
+ * Single execution mode. `foreground` waits for every child's final answer;
12
+ * `background` starts durable mailbox children.
13
+ */
14
+ export type RuntimeMode = "foreground" | "background";
7
15
  export type SubagentMode = "one-shot" | "continuable";
8
16
  export type SubagentProviderName = "spawn" | "fork";
9
17
  export type SubagentStopReason = "completed" | "aborted" | "error" | "max-tokens";
18
+ export type ContextInheritance =
19
+ | { mode: "fresh" }
20
+ | { mode: "all_completed" }
21
+ | { mode: "last_n_completed"; completedTurns: number };
22
+
23
+ export interface SubagentTask {
24
+ name: string;
25
+ path: string;
26
+ }
10
27
 
11
28
  export interface SubagentSettings {
12
29
  agentScope: AgentScope;
13
- syncBundledAgents: boolean;
14
30
  maxDepth: number;
15
- enableRunInBackground: boolean;
16
- defaultBackground: boolean;
17
- reportDelivery: ReportDelivery;
31
+ runtimeMode: RuntimeMode;
32
+ maxConcurrentBackgroundRuns: number;
33
+ maxIdleRuntimes: number;
18
34
  inheritExtensions: boolean;
19
35
  openAIIdentity: boolean;
20
36
  maxOutputBytes: number;
@@ -48,18 +64,16 @@ export interface ResolvedModel {
48
64
 
49
65
  export interface SubagentRuntimeSnapshot {
50
66
  agentScope: AgentScope;
51
- syncBundledAgents: boolean;
52
67
  maxDepth: number;
53
- enableRunInBackground: boolean;
54
- defaultBackground: boolean;
55
- reportDelivery: ReportDelivery;
68
+ runtimeMode: RuntimeMode;
69
+ maxConcurrentBackgroundRuns: number;
70
+ maxIdleRuntimes: number;
56
71
  inheritExtensions: boolean;
57
72
  openAIIdentity: boolean;
58
73
  maxOutputBytes: number;
59
74
  }
60
75
 
61
- export interface SubagentDescriptor {
62
- version: 2;
76
+ interface SubagentDescriptorBase {
63
77
  mode: SubagentMode;
64
78
  provider: SubagentProviderName;
65
79
  label: string;
@@ -80,15 +94,24 @@ export interface SubagentDescriptor {
80
94
  runtime: SubagentRuntimeSnapshot;
81
95
  }
82
96
 
97
+ export interface SubagentDescriptor extends SubagentDescriptorBase {
98
+ version: typeof DESCRIPTOR_VERSION;
99
+ task: SubagentTask;
100
+ context: ContextInheritance;
101
+ }
102
+
83
103
  export interface SubagentUsage extends Usage {
84
104
  turns: number;
85
105
  }
86
106
 
87
107
  export interface SubagentRunResult {
88
108
  agentId: string;
109
+ turnId: string;
89
110
  piSessionId?: string;
90
111
  sessionFile?: string;
91
112
  output: string;
113
+ outputTruncated?: boolean;
114
+ omittedBytes?: number;
92
115
  stopReason: SubagentStopReason;
93
116
  usage: SubagentUsage;
94
117
  }
@@ -102,9 +125,12 @@ export interface TraceItem {
102
125
  export interface DelegationDetails {
103
126
  kind: "delegation";
104
127
  agentId: string;
128
+ taskPath: string;
129
+ turnId?: string;
105
130
  piSessionId?: string;
106
131
  provider: SubagentProviderName;
107
132
  mode: SubagentMode;
133
+ context: ContextInheritance;
108
134
  agent: string;
109
135
  label: string;
110
136
  depth: number;
@@ -118,18 +144,30 @@ export interface DelegationDetails {
118
144
 
119
145
  export interface ControlDetails {
120
146
  kind: "control";
121
- action: "send" | "interrupt" | "list" | "report";
147
+ action: "send" | "followup" | "wait" | "interrupt" | "list" | "report";
122
148
  agentId?: string;
149
+ taskPath?: string;
150
+ messageId?: string;
151
+ turnId?: string;
152
+ pendingMessages?: number;
153
+ claimedMessages?: number;
154
+ completionIds?: string[];
155
+ timedOut?: boolean;
156
+ unreadUpdates?: number;
123
157
  }
124
158
 
125
159
  export interface CatalogChild {
126
160
  kind: "child";
127
161
  agentId: string;
128
162
  parentAgentId: string;
163
+ taskPath: string;
164
+ parentTaskPath: string;
129
165
  depth: number;
130
166
  descriptor: SubagentDescriptor;
131
167
  sessionFile?: string;
132
168
  status: "running" | "idle" | "ready";
169
+ pendingMessages: number;
170
+ unreadUpdates: number;
133
171
  }
134
172
 
135
173
  export interface CatalogDiagnostic {
@@ -144,10 +182,10 @@ export interface CatalogDiagnostic {
144
182
  export type CatalogEntry = CatalogChild | CatalogDiagnostic;
145
183
 
146
184
  export interface ParentMessageDetails {
147
- kind: "report" | "settled";
185
+ kind: "report";
148
186
  childAgentId: string;
187
+ taskPath?: string;
149
188
  label: string;
150
- stopReason?: SubagentStopReason;
151
189
  truncated?: boolean;
152
190
  }
153
191