@oai404iao/pi-subagent 0.3.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,21 +109,55 @@ 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);
82
135
 
83
- export function forkDelegationParameters(agentNames?: readonly string[]) {
84
- if (agentNames === undefined) return ForkDelegationParameters;
85
- return Type.Object(forkDelegationFields(agentNames), { additionalProperties: false });
136
+ export const ForkDelegationParameters =
137
+ createForkDelegationParameters(true);
138
+
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
156
  subagent_id: Type.String({
91
- description: "Durable agent id of a direct continuable child",
157
+ description:
158
+ "Readable absolute/relative task path or durable id of a direct continuable child",
92
159
  minLength: 1,
160
+ maxLength: 4096,
93
161
  }),
94
162
  message: Type.String({
95
163
  description: "Message to enqueue as the child's next FIFO turn",
@@ -99,11 +167,43 @@ export const SendMessageParameters = Type.Object(
99
167
  { additionalProperties: false },
100
168
  );
101
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
+
102
200
  export const InterruptParameters = Type.Object(
103
201
  {
104
202
  agent_id: Type.String({
105
- description: "Agent 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",
106
205
  minLength: 1,
206
+ maxLength: 4096,
107
207
  }),
108
208
  },
109
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,18 +2,33 @@ 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;
19
34
  openAIIdentity: boolean;
@@ -38,7 +53,7 @@ export interface AgentSnapshot {
38
53
  model?: string;
39
54
  thinking?: ThinkingLevel;
40
55
  systemPrompt: string;
41
- source: AgentSource;
56
+ source: AgentSnapshotSource;
42
57
  }
43
58
 
44
59
  export interface ResolvedModel {
@@ -48,18 +63,19 @@ export interface ResolvedModel {
48
63
 
49
64
  export interface SubagentRuntimeSnapshot {
50
65
  agentScope: AgentScope;
51
- syncBundledAgents: boolean;
52
66
  maxDepth: number;
53
67
  enableRunInBackground: boolean;
54
68
  defaultBackground: boolean;
69
+ maxConcurrentBackgroundRuns: number;
70
+ maxIdleRuntimes: number;
71
+ backgroundProtocol: BackgroundProtocol;
55
72
  reportDelivery: ReportDelivery;
56
73
  inheritExtensions: boolean;
57
74
  openAIIdentity: boolean;
58
75
  maxOutputBytes: number;
59
76
  }
60
77
 
61
- export interface SubagentDescriptor {
62
- version: 2;
78
+ interface SubagentDescriptorBase {
63
79
  mode: SubagentMode;
64
80
  provider: SubagentProviderName;
65
81
  label: string;
@@ -80,15 +96,32 @@ export interface SubagentDescriptor {
80
96
  runtime: SubagentRuntimeSnapshot;
81
97
  }
82
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
+
83
113
  export interface SubagentUsage extends Usage {
84
114
  turns: number;
85
115
  }
86
116
 
87
117
  export interface SubagentRunResult {
88
118
  agentId: string;
119
+ turnId: string;
89
120
  piSessionId?: string;
90
121
  sessionFile?: string;
91
122
  output: string;
123
+ outputTruncated?: boolean;
124
+ omittedBytes?: number;
92
125
  stopReason: SubagentStopReason;
93
126
  usage: SubagentUsage;
94
127
  }
@@ -102,9 +135,12 @@ export interface TraceItem {
102
135
  export interface DelegationDetails {
103
136
  kind: "delegation";
104
137
  agentId: string;
138
+ taskPath: string;
139
+ turnId?: string;
105
140
  piSessionId?: string;
106
141
  provider: SubagentProviderName;
107
142
  mode: SubagentMode;
143
+ context: ContextInheritance;
108
144
  agent: string;
109
145
  label: string;
110
146
  depth: number;
@@ -118,18 +154,30 @@ export interface DelegationDetails {
118
154
 
119
155
  export interface ControlDetails {
120
156
  kind: "control";
121
- action: "send" | "interrupt" | "list" | "report";
157
+ action: "send" | "followup" | "wait" | "interrupt" | "list" | "report";
122
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;
123
167
  }
124
168
 
125
169
  export interface CatalogChild {
126
170
  kind: "child";
127
171
  agentId: string;
128
172
  parentAgentId: string;
173
+ taskPath: string;
174
+ parentTaskPath: string;
129
175
  depth: number;
130
176
  descriptor: SubagentDescriptor;
131
177
  sessionFile?: string;
132
178
  status: "running" | "idle" | "ready";
179
+ pendingMessages: number;
180
+ unreadUpdates: number;
133
181
  }
134
182
 
135
183
  export interface CatalogDiagnostic {
@@ -146,6 +194,7 @@ export type CatalogEntry = CatalogChild | CatalogDiagnostic;
146
194
  export interface ParentMessageDetails {
147
195
  kind: "report" | "settled";
148
196
  childAgentId: string;
197
+ taskPath?: string;
149
198
  label: string;
150
199
  stopReason?: SubagentStopReason;
151
200
  truncated?: boolean;