@oai404iao/pi-subagent 0.2.0 → 0.4.0-alpha.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.
package/src/schemas.ts CHANGED
@@ -17,6 +17,15 @@ function agentNameParameter(agentNames?: readonly string[]) {
17
17
  function delegationFields(agentNames?: readonly string[]) {
18
18
  return {
19
19
  agent: agentNameParameter(agentNames),
20
+ task_name: Type.Optional(
21
+ Type.String({
22
+ description:
23
+ "Stable readable child path segment; lowercase letters, digits, hyphens, and underscores",
24
+ pattern: "^[a-z0-9][a-z0-9_-]{0,63}$",
25
+ minLength: 1,
26
+ maxLength: 64,
27
+ }),
28
+ ),
20
29
  description: Type.String({
21
30
  description: "Short 3-5 word display label for the delegated task",
22
31
  minLength: 1,
@@ -29,6 +38,27 @@ function delegationFields(agentNames?: readonly string[]) {
29
38
  };
30
39
  }
31
40
 
41
+ const ContextParameters = Type.Object(
42
+ {
43
+ mode: StringEnum(
44
+ ["fresh", "all_completed", "last_n_completed"] as const,
45
+ {
46
+ description:
47
+ "Parent context inherited once when the child is created",
48
+ },
49
+ ),
50
+ completed_turns: Type.Optional(
51
+ Type.Integer({
52
+ description:
53
+ "Number of completed parent turns for last_n_completed",
54
+ minimum: 1,
55
+ maximum: 100,
56
+ }),
57
+ ),
58
+ },
59
+ { additionalProperties: false },
60
+ );
61
+
32
62
  function forkDelegationFields(agentNames?: readonly string[]) {
33
63
  return {
34
64
  ...delegationFields(agentNames),
@@ -45,10 +75,14 @@ function createDelegationParameters(
45
75
  agentNames?: readonly string[],
46
76
  ) {
47
77
  const fields = delegationFields(agentNames);
78
+ const contextualFields = {
79
+ ...fields,
80
+ context: Type.Optional(ContextParameters),
81
+ };
48
82
  return Type.Object(
49
83
  enableRunInBackground
50
84
  ? {
51
- ...fields,
85
+ ...contextualFields,
52
86
  run_in_background: Type.Optional(
53
87
  Type.Boolean({
54
88
  description:
@@ -56,7 +90,7 @@ function createDelegationParameters(
56
90
  }),
57
91
  ),
58
92
  }
59
- : fields,
93
+ : contextualFields,
60
94
  { additionalProperties: false },
61
95
  );
62
96
  }
@@ -75,19 +109,56 @@ export function delegationParameters(
75
109
  return createDelegationParameters(enableRunInBackground, agentNames);
76
110
  }
77
111
 
78
- export const ForkDelegationParameters = Type.Object(
79
- forkDelegationFields(),
80
- { additionalProperties: false },
81
- );
112
+ function createForkDelegationParameters(
113
+ enableRunInBackground: boolean,
114
+ agentNames?: readonly string[],
115
+ ) {
116
+ const fields = forkDelegationFields(agentNames);
117
+ return Type.Object(
118
+ enableRunInBackground
119
+ ? {
120
+ ...fields,
121
+ run_in_background: Type.Optional(
122
+ Type.Boolean({
123
+ description:
124
+ "Run as a continuable inherited-context background child. Fork remains foreground by default.",
125
+ }),
126
+ ),
127
+ }
128
+ : fields,
129
+ { additionalProperties: false },
130
+ );
131
+ }
132
+
133
+ export const ForegroundForkDelegationParameters =
134
+ createForkDelegationParameters(false);
135
+
136
+ export const ForkDelegationParameters =
137
+ createForkDelegationParameters(true);
82
138
 
83
- export function forkDelegationParameters(agentNames?: readonly string[]) {
84
- if (agentNames === undefined) return ForkDelegationParameters;
85
- return Type.Object(forkDelegationFields(agentNames), { additionalProperties: false });
139
+ export function forkDelegationParameters(
140
+ agentNames?: readonly string[],
141
+ enableRunInBackground = true,
142
+ ) {
143
+ if (agentNames === undefined) {
144
+ return enableRunInBackground
145
+ ? ForkDelegationParameters
146
+ : ForegroundForkDelegationParameters;
147
+ }
148
+ return createForkDelegationParameters(
149
+ enableRunInBackground,
150
+ agentNames,
151
+ );
86
152
  }
87
153
 
88
154
  export const SendMessageParameters = Type.Object(
89
155
  {
90
- subagent_id: Type.String({ description: "Durable id of a direct continuable child", minLength: 1 }),
156
+ subagent_id: Type.String({
157
+ description:
158
+ "Readable absolute/relative task path or durable id of a direct continuable child",
159
+ minLength: 1,
160
+ maxLength: 4096,
161
+ }),
91
162
  message: Type.String({
92
163
  description: "Message to enqueue as the child's next FIFO turn",
93
164
  minLength: 1,
@@ -96,11 +167,43 @@ export const SendMessageParameters = Type.Object(
96
167
  { additionalProperties: false },
97
168
  );
98
169
 
170
+ export const FollowupTaskParameters = Type.Object(
171
+ {
172
+ subagent_id: Type.String({
173
+ description:
174
+ "Readable absolute/relative task path or durable id of a direct mailbox-v2 continuable child",
175
+ minLength: 1,
176
+ maxLength: 4096,
177
+ }),
178
+ },
179
+ { additionalProperties: false },
180
+ );
181
+
182
+ export const DEFAULT_WAIT_AGENT_TIMEOUT_MS = 30_000;
183
+ export const MAX_WAIT_AGENT_TIMEOUT_MS = 120_000;
184
+
185
+ export const WaitAgentParameters = Type.Object(
186
+ {
187
+ timeout_ms: Type.Optional(
188
+ Type.Integer({
189
+ description:
190
+ "Maximum event-driven wait in milliseconds before returning a timeout",
191
+ minimum: 0,
192
+ maximum: MAX_WAIT_AGENT_TIMEOUT_MS,
193
+ default: DEFAULT_WAIT_AGENT_TIMEOUT_MS,
194
+ }),
195
+ ),
196
+ },
197
+ { additionalProperties: false },
198
+ );
199
+
99
200
  export const InterruptParameters = Type.Object(
100
201
  {
101
202
  agent_id: Type.String({
102
- description: "Id of a live child or deeper descendant whose current turn should stop",
203
+ description:
204
+ "Readable absolute/relative task path or durable id of a live descendant whose current turn should stop",
103
205
  minLength: 1,
206
+ maxLength: 4096,
104
207
  }),
105
208
  },
106
209
  { additionalProperties: false },
@@ -0,0 +1,188 @@
1
+ import type {
2
+ SubagentDescriptor,
3
+ SubagentTask,
4
+ } from "./types.ts";
5
+
6
+ export const ROOT_TASK_PATH = "/root";
7
+ export const LEGACY_TASK_NAMESPACE = ".legacy";
8
+ export const MAX_TASK_NAME_LENGTH = 64;
9
+ export const MAX_TASK_PATH_LENGTH = 4096;
10
+
11
+ const TASK_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
12
+ const AGENT_ID_PATTERN =
13
+ /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
14
+
15
+ function pathSegments(path: string): string[] {
16
+ if (path.length > MAX_TASK_PATH_LENGTH) {
17
+ throw new Error(
18
+ `task path exceeds ${MAX_TASK_PATH_LENGTH} characters`,
19
+ );
20
+ }
21
+ if (!path.startsWith("/") || path.endsWith("/") || path.includes("//")) {
22
+ throw new Error(`task path must be a canonical absolute path under ${ROOT_TASK_PATH}`);
23
+ }
24
+ const segments = path.slice(1).split("/");
25
+ if (segments[0] !== "root") {
26
+ throw new Error(`task path must be rooted at ${ROOT_TASK_PATH}`);
27
+ }
28
+ return segments;
29
+ }
30
+
31
+ export function validateTaskName(value: string): string {
32
+ if (!TASK_NAME_PATTERN.test(value)) {
33
+ throw new Error(
34
+ "task_name must contain 1-64 lowercase ASCII letters, digits, hyphens, or underscores and start with a letter or digit",
35
+ );
36
+ }
37
+ if (value === "root" || value === LEGACY_TASK_NAMESPACE) {
38
+ throw new Error(`task_name "${value}" is reserved`);
39
+ }
40
+ return value;
41
+ }
42
+
43
+ export function validateTaskPath(path: string): string {
44
+ if (path === ROOT_TASK_PATH) return path;
45
+ const segments = pathSegments(path);
46
+ for (let index = 1; index < segments.length; index++) {
47
+ const segment = segments[index]!;
48
+ if (
49
+ index === 1
50
+ && segment === LEGACY_TASK_NAMESPACE
51
+ ) {
52
+ const legacyId = segments[index + 1];
53
+ if (!legacyId || !AGENT_ID_PATTERN.test(legacyId)) {
54
+ throw new Error(
55
+ `legacy task paths must use ${ROOT_TASK_PATH}/${LEGACY_TASK_NAMESPACE}/<agent-id>`,
56
+ );
57
+ }
58
+ index++;
59
+ continue;
60
+ }
61
+ validateTaskName(segment);
62
+ }
63
+ return path;
64
+ }
65
+
66
+ export function taskPath(parentPath: string, name: string): string {
67
+ validateTaskPath(parentPath);
68
+ validateTaskName(name);
69
+ const path = `${parentPath}/${name}`;
70
+ return validateTaskPath(path);
71
+ }
72
+
73
+ export function legacyTaskPath(agentId: string): string {
74
+ if (!AGENT_ID_PATTERN.test(agentId)) {
75
+ throw new Error("legacy task path requires a UUIDv7 agent id");
76
+ }
77
+ return `${ROOT_TASK_PATH}/${LEGACY_TASK_NAMESPACE}/${agentId}`;
78
+ }
79
+
80
+ export function descriptorTaskPath(
81
+ descriptor: SubagentDescriptor,
82
+ ): string {
83
+ return descriptor.version === 3
84
+ ? descriptor.task.path
85
+ : legacyTaskPath(descriptor.agentId);
86
+ }
87
+
88
+ export function descriptorTask(
89
+ descriptor: SubagentDescriptor,
90
+ ): SubagentTask {
91
+ if (descriptor.version === 3) return { ...descriptor.task };
92
+ return {
93
+ name: descriptor.agentId,
94
+ path: legacyTaskPath(descriptor.agentId),
95
+ };
96
+ }
97
+
98
+ export function validateDescriptorTask(task: SubagentTask): SubagentTask {
99
+ const name = validateTaskName(task.name);
100
+ const path = validateTaskPath(task.path);
101
+ if (path === ROOT_TASK_PATH || path.split("/").at(-1) !== name) {
102
+ throw new Error("task.path must end with task.name");
103
+ }
104
+ return { name, path };
105
+ }
106
+
107
+ export function slugTaskName(value: string): string {
108
+ const normalized = value
109
+ .normalize("NFKD")
110
+ .replace(/\p{Mark}/gu, "")
111
+ .toLowerCase()
112
+ .replace(/[^a-z0-9_-]+/g, "-")
113
+ .replace(/[-_]{2,}/g, "-")
114
+ .replace(/^[-_]+|[-_]+$/g, "")
115
+ .slice(0, MAX_TASK_NAME_LENGTH)
116
+ .replace(/[-_]+$/g, "");
117
+ const candidate =
118
+ !normalized || normalized === "root" || normalized === LEGACY_TASK_NAMESPACE
119
+ ? "task"
120
+ : normalized;
121
+ return validateTaskName(candidate);
122
+ }
123
+
124
+ export function numberedTaskName(base: string, ordinal: number): string {
125
+ validateTaskName(base);
126
+ if (!Number.isSafeInteger(ordinal) || ordinal < 2) {
127
+ throw new Error("task name ordinal must be an integer of at least 2");
128
+ }
129
+ const suffix = `-${ordinal}`;
130
+ const prefix = base
131
+ .slice(0, MAX_TASK_NAME_LENGTH - suffix.length)
132
+ .replace(/[-_]+$/g, "");
133
+ return validateTaskName(`${prefix || "task"}${suffix}`);
134
+ }
135
+
136
+ export function resolveTaskPath(
137
+ basePath: string,
138
+ reference: string,
139
+ ): string {
140
+ validateTaskPath(basePath);
141
+ if (!reference.trim() || reference !== reference.trim()) {
142
+ throw new Error("task path reference must be a non-empty trimmed string");
143
+ }
144
+ if (reference.length > MAX_TASK_PATH_LENGTH) {
145
+ throw new Error(
146
+ `task path reference exceeds ${MAX_TASK_PATH_LENGTH} characters`,
147
+ );
148
+ }
149
+ if (reference.includes("//") || (reference.length > 1 && reference.endsWith("/"))) {
150
+ throw new Error("task path reference contains an empty segment");
151
+ }
152
+
153
+ const absolute = reference.startsWith("/");
154
+ const segments = absolute ? ["root"] : pathSegments(basePath);
155
+ const input = absolute ? reference.slice(1).split("/") : reference.split("/");
156
+ if (absolute && input.shift() !== "root") {
157
+ throw new Error(`absolute task paths must be rooted at ${ROOT_TASK_PATH}`);
158
+ }
159
+ for (const segment of input) {
160
+ if (!segment || segment === ".") continue;
161
+ if (segment === "..") {
162
+ if (segments.length === 1) {
163
+ throw new Error(`task path reference cannot escape ${ROOT_TASK_PATH}`);
164
+ }
165
+ segments.pop();
166
+ continue;
167
+ }
168
+ if (segment === LEGACY_TASK_NAMESPACE) {
169
+ segments.push(segment);
170
+ continue;
171
+ }
172
+ if (
173
+ segments.at(-1) === LEGACY_TASK_NAMESPACE
174
+ && AGENT_ID_PATTERN.test(segment)
175
+ ) {
176
+ segments.push(segment);
177
+ continue;
178
+ }
179
+ validateTaskName(segment);
180
+ segments.push(segment);
181
+ }
182
+ const resolved = `/${segments.join("/")}`;
183
+ return validateTaskPath(resolved);
184
+ }
185
+
186
+ export function isAgentId(value: string): boolean {
187
+ return AGENT_ID_PATTERN.test(value);
188
+ }
package/src/types.ts CHANGED
@@ -2,20 +2,36 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import type { Usage } from "@earendil-works/pi-ai";
3
3
 
4
4
  export type AgentScope = "user" | "project" | "both";
5
- export type AgentSource = "bundled" | "user" | "project";
5
+ /** Sources that runtime discovery is allowed to activate. */
6
+ export type AgentSource = "user" | "project";
7
+ /** `bundled` is retained only for reading descriptors written by older releases. */
8
+ export type AgentSnapshotSource = AgentSource | "bundled";
6
9
  export type ReportDelivery = "wakeup" | "quiet";
10
+ export type BackgroundProtocol = "legacy" | "mailbox-v2";
7
11
  export type SubagentMode = "one-shot" | "continuable";
8
12
  export type SubagentProviderName = "spawn" | "fork";
9
13
  export type SubagentStopReason = "completed" | "aborted" | "error" | "max-tokens";
14
+ export type ContextInheritance =
15
+ | { mode: "fresh" }
16
+ | { mode: "all_completed" }
17
+ | { mode: "last_n_completed"; completedTurns: number };
18
+
19
+ export interface SubagentTask {
20
+ name: string;
21
+ path: string;
22
+ }
10
23
 
11
24
  export interface SubagentSettings {
12
25
  agentScope: AgentScope;
13
- syncBundledAgents: boolean;
14
26
  maxDepth: number;
15
27
  enableRunInBackground: boolean;
16
28
  defaultBackground: boolean;
29
+ maxConcurrentBackgroundRuns: number;
30
+ maxIdleRuntimes: number;
31
+ backgroundProtocol?: BackgroundProtocol;
17
32
  reportDelivery: ReportDelivery;
18
33
  inheritExtensions: boolean;
34
+ openAIIdentity: boolean;
19
35
  maxOutputBytes: number;
20
36
  }
21
37
 
@@ -37,7 +53,7 @@ export interface AgentSnapshot {
37
53
  model?: string;
38
54
  thinking?: ThinkingLevel;
39
55
  systemPrompt: string;
40
- source: AgentSource;
56
+ source: AgentSnapshotSource;
41
57
  }
42
58
 
43
59
  export interface ResolvedModel {
@@ -47,21 +63,29 @@ export interface ResolvedModel {
47
63
 
48
64
  export interface SubagentRuntimeSnapshot {
49
65
  agentScope: AgentScope;
50
- syncBundledAgents: boolean;
51
66
  maxDepth: number;
52
67
  enableRunInBackground: boolean;
53
68
  defaultBackground: boolean;
69
+ maxConcurrentBackgroundRuns: number;
70
+ maxIdleRuntimes: number;
71
+ backgroundProtocol: BackgroundProtocol;
54
72
  reportDelivery: ReportDelivery;
55
73
  inheritExtensions: boolean;
74
+ openAIIdentity: boolean;
56
75
  maxOutputBytes: number;
57
76
  }
58
77
 
59
- export interface SubagentDescriptor {
60
- version: 1;
78
+ interface SubagentDescriptorBase {
61
79
  mode: SubagentMode;
62
80
  provider: SubagentProviderName;
63
81
  label: string;
64
- parentSessionId: string;
82
+ /** Durable pi-subagent control identity, independent of provider wire ids. */
83
+ agentId: string;
84
+ /** Durable control identity of the delegating pi-subagent. */
85
+ parentAgentId: string;
86
+ /** Pi session lookup key of the delegating agent; never used as a wire id. */
87
+ parentPiSessionId: string;
88
+ /** Path of the parent's session file at delegation time (attribute, not identity). */
65
89
  parentSessionFile?: string;
66
90
  depth: number;
67
91
  cwd: string;
@@ -72,14 +96,32 @@ export interface SubagentDescriptor {
72
96
  runtime: SubagentRuntimeSnapshot;
73
97
  }
74
98
 
99
+ export interface SubagentDescriptorV2 extends SubagentDescriptorBase {
100
+ version: 2;
101
+ }
102
+
103
+ export interface SubagentDescriptorV3 extends SubagentDescriptorBase {
104
+ version: 3;
105
+ task: SubagentTask;
106
+ context: ContextInheritance;
107
+ }
108
+
109
+ export type SubagentDescriptor =
110
+ | SubagentDescriptorV2
111
+ | SubagentDescriptorV3;
112
+
75
113
  export interface SubagentUsage extends Usage {
76
114
  turns: number;
77
115
  }
78
116
 
79
117
  export interface SubagentRunResult {
80
- id: string;
118
+ agentId: string;
119
+ turnId: string;
120
+ piSessionId?: string;
81
121
  sessionFile?: string;
82
122
  output: string;
123
+ outputTruncated?: boolean;
124
+ omittedBytes?: number;
83
125
  stopReason: SubagentStopReason;
84
126
  usage: SubagentUsage;
85
127
  }
@@ -92,9 +134,13 @@ export interface TraceItem {
92
134
 
93
135
  export interface DelegationDetails {
94
136
  kind: "delegation";
95
- id: string;
137
+ agentId: string;
138
+ taskPath: string;
139
+ turnId?: string;
140
+ piSessionId?: string;
96
141
  provider: SubagentProviderName;
97
142
  mode: SubagentMode;
143
+ context: ContextInheritance;
98
144
  agent: string;
99
145
  label: string;
100
146
  depth: number;
@@ -108,23 +154,35 @@ export interface DelegationDetails {
108
154
 
109
155
  export interface ControlDetails {
110
156
  kind: "control";
111
- action: "send" | "interrupt" | "list" | "report";
112
- id?: string;
157
+ action: "send" | "followup" | "wait" | "interrupt" | "list" | "report";
158
+ agentId?: string;
159
+ taskPath?: string;
160
+ messageId?: string;
161
+ turnId?: string;
162
+ pendingMessages?: number;
163
+ claimedMessages?: number;
164
+ completionIds?: string[];
165
+ timedOut?: boolean;
166
+ unreadUpdates?: number;
113
167
  }
114
168
 
115
169
  export interface CatalogChild {
116
170
  kind: "child";
117
- id: string;
118
- parentId: string;
171
+ agentId: string;
172
+ parentAgentId: string;
173
+ taskPath: string;
174
+ parentTaskPath: string;
119
175
  depth: number;
120
176
  descriptor: SubagentDescriptor;
121
177
  sessionFile?: string;
122
178
  status: "running" | "idle" | "ready";
179
+ pendingMessages: number;
180
+ unreadUpdates: number;
123
181
  }
124
182
 
125
183
  export interface CatalogDiagnostic {
126
184
  kind: "diagnostic";
127
- id: string;
185
+ piSessionId: string;
128
186
  reason: "corrupt" | "unavailable";
129
187
  sessionFile?: string;
130
188
  parentSessionFile?: string;
@@ -135,7 +193,8 @@ export type CatalogEntry = CatalogChild | CatalogDiagnostic;
135
193
 
136
194
  export interface ParentMessageDetails {
137
195
  kind: "report" | "settled";
138
- childId: string;
196
+ childAgentId: string;
197
+ taskPath?: string;
139
198
  label: string;
140
199
  stopReason?: SubagentStopReason;
141
200
  truncated?: boolean;