@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.
package/src/providers.ts CHANGED
@@ -1,7 +1,22 @@
1
1
  import { rm } from "node:fs/promises";
2
2
  import type { SessionEntry } from "@earendil-works/pi-coding-agent";
3
3
  import { SessionManager } from "@earendil-works/pi-coding-agent";
4
- import type { SubagentMode, SubagentProviderName } from "./types.ts";
4
+ import type {
5
+ ContextInheritance,
6
+ SubagentMode,
7
+ SubagentProviderName,
8
+ } from "./types.ts";
9
+
10
+ const INHERITED_COMPACTION_CUSTOM_TYPE =
11
+ "pi-subagent/inherited-compaction-summary";
12
+ const INHERITED_BRANCH_CUSTOM_TYPE =
13
+ "pi-subagent/inherited-branch-summary";
14
+ const COMPACTION_SUMMARY_PREFIX =
15
+ "The conversation history before this point was compacted into the following summary:\n\n<summary>\n";
16
+ const COMPACTION_SUMMARY_SUFFIX = "\n</summary>";
17
+ const BRANCH_SUMMARY_PREFIX =
18
+ "The following is a summary of a branch that this conversation came back from:\n\n<summary>\n";
19
+ const BRANCH_SUMMARY_SUFFIX = "</summary>";
5
20
 
6
21
  export interface SessionView {
7
22
  getBranch(): SessionEntry[];
@@ -10,6 +25,7 @@ export interface SessionView {
10
25
  getSessionFile(): string | undefined;
11
26
  getSessionId(): string;
12
27
  getEntries(): SessionEntry[];
28
+ buildContextEntries(): SessionEntry[];
13
29
  appendCustomEntry(customType: string, data?: unknown): string;
14
30
  }
15
31
 
@@ -27,7 +43,11 @@ export interface ChildProvider {
27
43
  name: SubagentProviderName;
28
44
  inheritsParentContext: boolean;
29
45
  supportsContinuable: boolean;
30
- prepare(parent: ProviderParent, mode: SubagentMode): Promise<PreparedChildSession>;
46
+ prepare(
47
+ parent: ProviderParent,
48
+ mode: SubagentMode,
49
+ context?: ContextInheritance,
50
+ ): Promise<PreparedChildSession>;
31
51
  }
32
52
 
33
53
  async function removeOwnedSession(path: string | undefined): Promise<void> {
@@ -68,33 +88,195 @@ export function completedTurnBoundaryId(entries: readonly SessionEntry[]): strin
68
88
  return undefined;
69
89
  }
70
90
 
71
- function forkedSession(parent: ProviderParent): PreparedChildSession {
91
+ interface CompletedTurnSpan {
92
+ start: number;
93
+ end: number;
94
+ }
95
+
96
+ function isSummaryEntry(entry: SessionEntry): boolean {
97
+ return (
98
+ entry.type === "compaction"
99
+ || entry.type === "branch_summary"
100
+ || (
101
+ entry.type === "message"
102
+ && (
103
+ entry.message.role === "compactionSummary"
104
+ || entry.message.role === "branchSummary"
105
+ )
106
+ )
107
+ );
108
+ }
109
+
110
+ function startsModelTurn(entry: SessionEntry): boolean {
111
+ if (entry.type === "custom_message") return true;
112
+ if (entry.type !== "message") return false;
113
+ switch (entry.message.role) {
114
+ case "user":
115
+ case "custom":
116
+ return true;
117
+ case "bashExecution":
118
+ return !entry.message.excludeFromContext;
119
+ default:
120
+ return false;
121
+ }
122
+ }
123
+
124
+ function completedTurnSpans(
125
+ entries: readonly SessionEntry[],
126
+ ): {
127
+ turns: CompletedTurnSpan[];
128
+ latestSummary?: number;
129
+ } {
130
+ const turns: CompletedTurnSpan[] = [];
131
+ let latestSummary: number | undefined;
132
+ let currentStart: number | undefined;
133
+ let currentEnd: number | undefined;
134
+ const finishCurrent = () => {
135
+ if (currentStart !== undefined && currentEnd !== undefined) {
136
+ turns.push({ start: currentStart, end: currentEnd });
137
+ }
138
+ currentStart = undefined;
139
+ currentEnd = undefined;
140
+ };
141
+
142
+ for (let index = 0; index < entries.length; index++) {
143
+ const entry = entries[index]!;
144
+ if (isSummaryEntry(entry)) {
145
+ finishCurrent();
146
+ latestSummary = index;
147
+ continue;
148
+ }
149
+ if (startsModelTurn(entry)) {
150
+ finishCurrent();
151
+ currentStart = index;
152
+ continue;
153
+ }
154
+ if (entry.type !== "message") continue;
155
+ if (
156
+ currentStart === undefined
157
+ && (
158
+ entry.message.role === "assistant"
159
+ || entry.message.role === "toolResult"
160
+ )
161
+ ) {
162
+ currentStart = latestSummary ?? 0;
163
+ }
164
+ if (
165
+ entry.message.role === "assistant"
166
+ && entry.message.stopReason !== "toolUse"
167
+ ) {
168
+ currentEnd = index;
169
+ }
170
+ }
171
+ finishCurrent();
172
+ return {
173
+ turns,
174
+ ...(latestSummary !== undefined ? { latestSummary } : {}),
175
+ };
176
+ }
177
+
178
+ export function completedContextEntries(
179
+ entries: readonly SessionEntry[],
180
+ context: Exclude<ContextInheritance, { mode: "fresh" }>,
181
+ ): SessionEntry[] {
182
+ const { turns, latestSummary } = completedTurnSpans(entries);
183
+ const end = Math.max(
184
+ turns.at(-1)?.end ?? -1,
185
+ latestSummary ?? -1,
186
+ );
187
+ if (end < 0) return [];
188
+ let start = 0;
189
+ if (context.mode === "last_n_completed") {
190
+ if (turns.length >= context.completedTurns) {
191
+ start = turns[turns.length - context.completedTurns]!.start;
192
+ }
193
+ }
194
+ return entries
195
+ .slice(start, end + 1)
196
+ .map((entry) => structuredClone(entry));
197
+ }
198
+
199
+ function appendInheritedEntry(
200
+ session: SessionManager,
201
+ entry: SessionEntry,
202
+ ): void {
203
+ if (entry.type === "message") {
204
+ if (entry.message.role === "compactionSummary") {
205
+ session.appendCustomMessageEntry(
206
+ INHERITED_COMPACTION_CUSTOM_TYPE,
207
+ `${COMPACTION_SUMMARY_PREFIX}${entry.message.summary}${COMPACTION_SUMMARY_SUFFIX}`,
208
+ false,
209
+ { tokensBefore: entry.message.tokensBefore },
210
+ );
211
+ return;
212
+ }
213
+ if (entry.message.role === "branchSummary") {
214
+ session.appendCustomMessageEntry(
215
+ INHERITED_BRANCH_CUSTOM_TYPE,
216
+ `${BRANCH_SUMMARY_PREFIX}${entry.message.summary}${BRANCH_SUMMARY_SUFFIX}`,
217
+ false,
218
+ { fromId: entry.message.fromId },
219
+ );
220
+ return;
221
+ }
222
+ session.appendMessage(
223
+ structuredClone(
224
+ entry.message,
225
+ ) as Parameters<SessionManager["appendMessage"]>[0],
226
+ );
227
+ return;
228
+ }
229
+ if (entry.type === "custom_message") {
230
+ session.appendCustomMessageEntry(
231
+ entry.customType,
232
+ structuredClone(entry.content),
233
+ entry.display,
234
+ structuredClone(entry.details),
235
+ );
236
+ return;
237
+ }
238
+ if (entry.type === "compaction") {
239
+ session.appendCustomMessageEntry(
240
+ INHERITED_COMPACTION_CUSTOM_TYPE,
241
+ `${COMPACTION_SUMMARY_PREFIX}${entry.summary}${COMPACTION_SUMMARY_SUFFIX}`,
242
+ false,
243
+ { tokensBefore: entry.tokensBefore },
244
+ );
245
+ return;
246
+ }
247
+ if (entry.type === "branch_summary") {
248
+ session.appendCustomMessageEntry(
249
+ INHERITED_BRANCH_CUSTOM_TYPE,
250
+ `${BRANCH_SUMMARY_PREFIX}${entry.summary}${BRANCH_SUMMARY_SUFFIX}`,
251
+ false,
252
+ { fromId: entry.fromId },
253
+ );
254
+ }
255
+ }
256
+
257
+ function forkedSession(
258
+ parent: ProviderParent,
259
+ context: Exclude<ContextInheritance, { mode: "fresh" }>,
260
+ ): PreparedChildSession {
72
261
  const parentFile = parent.sessionManager.getSessionFile();
73
- const boundaryId = completedTurnBoundaryId(parent.sessionManager.getBranch());
74
- if (!boundaryId) return freshSession(parent);
262
+ const inherited = completedContextEntries(
263
+ parent.sessionManager.buildContextEntries(),
264
+ context,
265
+ );
266
+ if (inherited.length === 0) return freshSession(parent);
75
267
  if (!parentFile) {
76
268
  throw new Error(
77
269
  "fork provider cannot copy completed history from an ephemeral parent session; use spawn instead",
78
270
  );
79
271
  }
80
-
81
- const clone = SessionManager.open(
82
- parentFile,
83
- parent.sessionManager.getSessionDir(),
84
- parent.sessionManager.getCwd(),
85
- );
86
- if (!clone.getEntry(boundaryId)) return freshSession(parent);
87
- const childFile = clone.createBranchedSession(boundaryId);
88
- if (!childFile) return freshSession(parent);
89
- const sessionManager = SessionManager.open(
90
- childFile,
91
- parent.sessionManager.getSessionDir(),
92
- parent.sessionManager.getCwd(),
93
- );
272
+ const prepared = freshSession(parent);
273
+ for (const entry of inherited) {
274
+ appendInheritedEntry(prepared.sessionManager, entry);
275
+ }
94
276
  return {
95
- sessionManager,
96
- seedMessageCount: sessionManager.buildSessionContext().messages.length,
97
- rollback: () => removeOwnedSession(childFile),
277
+ ...prepared,
278
+ seedMessageCount:
279
+ prepared.sessionManager.buildSessionContext().messages.length,
98
280
  };
99
281
  }
100
282
 
@@ -103,7 +285,14 @@ export class SpawnProvider implements ChildProvider {
103
285
  readonly inheritsParentContext = false;
104
286
  readonly supportsContinuable = true;
105
287
 
106
- prepare(parent: ProviderParent, _mode: SubagentMode): Promise<PreparedChildSession> {
288
+ prepare(
289
+ parent: ProviderParent,
290
+ _mode: SubagentMode,
291
+ context: ContextInheritance = { mode: "fresh" },
292
+ ): Promise<PreparedChildSession> {
293
+ if (context.mode !== "fresh") {
294
+ throw new Error("spawn provider requires fresh context");
295
+ }
107
296
  return Promise.resolve(freshSession(parent));
108
297
  }
109
298
  }
@@ -111,13 +300,17 @@ export class SpawnProvider implements ChildProvider {
111
300
  export class ForkProvider implements ChildProvider {
112
301
  readonly name = "fork";
113
302
  readonly inheritsParentContext = true;
114
- readonly supportsContinuable = false;
303
+ readonly supportsContinuable = true;
115
304
 
116
- async prepare(parent: ProviderParent, mode: SubagentMode): Promise<PreparedChildSession> {
117
- if (mode === "continuable") {
118
- throw new Error("fork provider is one-shot only; use spawn for continuable background work");
305
+ async prepare(
306
+ parent: ProviderParent,
307
+ _mode: SubagentMode,
308
+ context: ContextInheritance = { mode: "all_completed" },
309
+ ): Promise<PreparedChildSession> {
310
+ if (context.mode === "fresh") {
311
+ throw new Error("fork provider requires inherited context");
119
312
  }
120
- return forkedSession(parent);
313
+ return forkedSession(parent, context);
121
314
  }
122
315
  }
123
316
 
package/src/render.ts CHANGED
@@ -4,26 +4,28 @@ import type { DelegationDetails, ParentMessageDetails } from "./types.ts";
4
4
  import { formatUsage } from "./result.ts";
5
5
 
6
6
  export function renderDelegationCall(
7
- args: { agent?: string; description?: string; prompt?: string; run_in_background?: boolean },
7
+ args: {
8
+ agent?: string;
9
+ task_name?: string;
10
+ description?: string;
11
+ prompt?: string;
12
+ context?: { mode?: string };
13
+ },
8
14
  theme: {
9
15
  fg(color: any, text: string): string;
10
16
  bold(text: string): string;
11
17
  },
12
18
  provider: "spawn" | "fork",
19
+ runtimeMode: "foreground" | "background" = "background",
13
20
  ): Text {
14
- const mode =
15
- provider === "fork"
16
- ? "fork · foreground"
17
- : args.run_in_background === false
18
- ? "spawn · foreground"
19
- : args.run_in_background === true
20
- ? "spawn · background"
21
- : "spawn · configured default";
21
+ const contextMode = provider === "fork" ? "all_completed" : (args.context?.mode ?? "fresh");
22
+ const mode = `${contextMode} · ${runtimeMode}`;
22
23
  let text =
23
24
  theme.fg("toolTitle", theme.bold(provider === "fork" ? "subagent_fork " : "subagent ")) +
24
25
  theme.fg("accent", args.agent ?? "…") +
25
26
  theme.fg("muted", ` [${mode}]`);
26
27
  if (args.description) text += `\n ${theme.fg("dim", args.description)}`;
28
+ if (args.task_name) text += `\n ${theme.fg("dim", `path: ${args.task_name}`)}`;
27
29
  return new Text(text, 0, 0);
28
30
  }
29
31
 
@@ -37,6 +39,10 @@ export function renderDelegationResult(
37
39
  },
38
40
  ): Text | Container {
39
41
  if (!details) return new Text(content || "(no output)", 0, 0);
42
+ const taskPath = details.taskPath || details.agentId;
43
+ const contextMode =
44
+ details.context?.mode
45
+ ?? (details.provider === "fork" ? "all_completed" : "fresh");
40
46
  const running = options.isPartial || details.status === "starting" || details.status === "running";
41
47
  const icon = running
42
48
  ? theme.fg("warning", "◌")
@@ -45,8 +51,8 @@ export function renderDelegationResult(
45
51
  : theme.fg("success", "✓");
46
52
  const header = `${icon} ${theme.fg("toolTitle", theme.bold(details.agent))} ${theme.fg(
47
53
  "muted",
48
- `[${details.provider}/${details.mode}]`,
49
- )} ${theme.fg("dim", details.agentId)}`;
54
+ `[${details.provider}/${details.mode}/${contextMode}]`,
55
+ )} ${theme.fg("accent", taskPath)} ${theme.fg("dim", details.agentId)}`;
50
56
 
51
57
  if (!options.expanded) {
52
58
  const lines = [header, theme.fg("muted", details.label)];
@@ -95,14 +101,15 @@ export function renderParentMessage(
95
101
  },
96
102
  ): Text | Markdown {
97
103
  if (expanded) return new Markdown(content, outputPad, 0, getMarkdownTheme());
98
- const kind = details?.kind === "report" ? "report" : "settled";
99
- const icon = details?.stopReason && details.stopReason !== "completed" ? "◐" : "●";
100
104
  const label = details?.label ? ` — ${details.label}` : "";
101
105
  return new Text(
102
- theme.fg("accent", icon) +
106
+ theme.fg("accent", "●") +
103
107
  " " +
104
- theme.fg("toolTitle", theme.bold(`subagent ${kind}`)) +
105
- theme.fg("muted", ` ${details?.childAgentId ?? "unknown"}${label}`),
108
+ theme.fg("toolTitle", theme.bold("subagent report")) +
109
+ theme.fg(
110
+ "muted",
111
+ ` ${details?.taskPath ?? details?.childAgentId ?? "unknown"}${label}`,
112
+ ),
106
113
  outputPad,
107
114
  0,
108
115
  );
@@ -0,0 +1,173 @@
1
+ export interface BackgroundRunPermit {
2
+ release(): void;
3
+ }
4
+
5
+ export interface BackgroundRunAcquireOptions {
6
+ signal?: AbortSignal;
7
+ waitForCapacity?: boolean;
8
+ }
9
+
10
+ export class BackgroundConcurrencyLimitError extends Error {
11
+ constructor(readonly limit: number) {
12
+ super(
13
+ `background subagent concurrency limit (${limit}) is already in use; wait for an active background run to finish`,
14
+ );
15
+ this.name = "BackgroundConcurrencyLimitError";
16
+ }
17
+ }
18
+
19
+ interface BackgroundRunWaiter {
20
+ resolve: (permit: BackgroundRunPermit) => void;
21
+ reject: (error: Error) => void;
22
+ signal?: AbortSignal;
23
+ onAbort?: () => void;
24
+ }
25
+
26
+ export class BackgroundRunLimiter {
27
+ private active = 0;
28
+ private readonly waiters: BackgroundRunWaiter[] = [];
29
+ private closedError: Error | undefined;
30
+
31
+ constructor(private limit = 4) {
32
+ validateLimit(limit);
33
+ }
34
+
35
+ get activeCount(): number {
36
+ return this.active;
37
+ }
38
+
39
+ get pendingCount(): number {
40
+ return this.waiters.length;
41
+ }
42
+
43
+ configure(limit: number): void {
44
+ validateLimit(limit);
45
+ if (limit === this.limit) return;
46
+ this.limit = limit;
47
+ this.drain();
48
+ }
49
+
50
+ acquire(
51
+ options: BackgroundRunAcquireOptions = {},
52
+ ): Promise<BackgroundRunPermit> {
53
+ if (this.closedError) return Promise.reject(this.closedError);
54
+ if (options.signal?.aborted) {
55
+ return Promise.reject(abortReason(options.signal));
56
+ }
57
+ if (this.waiters.length === 0 && this.active < this.limit) {
58
+ this.active += 1;
59
+ return Promise.resolve(this.createPermit());
60
+ }
61
+ if (options.waitForCapacity === false) {
62
+ return Promise.reject(new BackgroundConcurrencyLimitError(this.limit));
63
+ }
64
+
65
+ return new Promise<BackgroundRunPermit>((resolve, reject) => {
66
+ const waiter: BackgroundRunWaiter = {
67
+ resolve,
68
+ reject,
69
+ ...(options.signal ? { signal: options.signal } : {}),
70
+ };
71
+ if (options.signal) {
72
+ waiter.onAbort = () => {
73
+ const index = this.waiters.indexOf(waiter);
74
+ if (index >= 0) this.waiters.splice(index, 1);
75
+ reject(abortReason(options.signal!));
76
+ this.drain();
77
+ };
78
+ options.signal.addEventListener("abort", waiter.onAbort, { once: true });
79
+ }
80
+ this.waiters.push(waiter);
81
+ });
82
+ }
83
+
84
+ close(error = new Error("background subagent scheduler is shutting down")): void {
85
+ if (this.closedError) return;
86
+ this.closedError = error;
87
+ for (const waiter of this.waiters.splice(0)) {
88
+ this.removeAbortListener(waiter);
89
+ waiter.reject(error);
90
+ }
91
+ }
92
+
93
+ private createPermit(): BackgroundRunPermit {
94
+ let released = false;
95
+ return {
96
+ release: () => {
97
+ if (released) return;
98
+ released = true;
99
+ this.active = Math.max(0, this.active - 1);
100
+ this.drain();
101
+ },
102
+ };
103
+ }
104
+
105
+ private drain(): void {
106
+ while (this.waiters.length > 0) {
107
+ const waiter = this.waiters[0]!;
108
+ if (waiter.signal?.aborted) {
109
+ this.waiters.shift();
110
+ this.removeAbortListener(waiter);
111
+ waiter.reject(abortReason(waiter.signal));
112
+ continue;
113
+ }
114
+ if (this.closedError) {
115
+ this.waiters.shift();
116
+ this.removeAbortListener(waiter);
117
+ waiter.reject(this.closedError);
118
+ continue;
119
+ }
120
+ if (this.active >= this.limit) return;
121
+ this.waiters.shift();
122
+ this.removeAbortListener(waiter);
123
+ this.active += 1;
124
+ waiter.resolve(this.createPermit());
125
+ }
126
+ }
127
+
128
+ private removeAbortListener(waiter: BackgroundRunWaiter): void {
129
+ if (waiter.signal && waiter.onAbort) {
130
+ waiter.signal.removeEventListener("abort", waiter.onAbort);
131
+ }
132
+ }
133
+ }
134
+
135
+ export class AgentOperationQueue {
136
+ private readonly tails = new Map<string, Promise<void>>();
137
+
138
+ async run<T>(agentId: string, operation: () => Promise<T>): Promise<T> {
139
+ const predecessor = this.tails.get(agentId) ?? Promise.resolve();
140
+ let release!: () => void;
141
+ const gate = new Promise<void>((resolve) => {
142
+ release = resolve;
143
+ });
144
+ const tail = predecessor.catch(() => {}).then(() => gate);
145
+ this.tails.set(agentId, tail);
146
+
147
+ await predecessor.catch(() => {});
148
+ try {
149
+ return await operation();
150
+ } finally {
151
+ release();
152
+ if (this.tails.get(agentId) === tail) this.tails.delete(agentId);
153
+ }
154
+ }
155
+
156
+ async waitForIdle(): Promise<void> {
157
+ while (this.tails.size > 0) {
158
+ await Promise.all([...this.tails.values()].map((tail) => tail.catch(() => {})));
159
+ }
160
+ }
161
+ }
162
+
163
+ function validateLimit(limit: number): void {
164
+ if (!Number.isSafeInteger(limit) || limit < 1) {
165
+ throw new Error("background run limit must be a positive safe integer");
166
+ }
167
+ }
168
+
169
+ function abortReason(signal: AbortSignal): Error {
170
+ return signal.reason instanceof Error
171
+ ? signal.reason
172
+ : new Error(signal.reason ? String(signal.reason) : "background run scheduling aborted");
173
+ }
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),
@@ -40,56 +70,32 @@ function forkDelegationFields(agentNames?: readonly string[]) {
40
70
  };
41
71
  }
42
72
 
43
- function createDelegationParameters(
44
- enableRunInBackground: boolean,
45
- agentNames?: readonly string[],
46
- ) {
73
+ export function delegationParameters(agentNames?: readonly string[]) {
47
74
  const fields = delegationFields(agentNames);
48
75
  return Type.Object(
49
- enableRunInBackground
50
- ? {
51
- ...fields,
52
- run_in_background: Type.Optional(
53
- Type.Boolean({
54
- description:
55
- "Run as a continuable background child. The spawn provider defaults this from configuration.",
56
- }),
57
- ),
58
- }
59
- : fields,
76
+ {
77
+ ...fields,
78
+ context: Type.Optional(ContextParameters),
79
+ },
60
80
  { additionalProperties: false },
61
81
  );
62
82
  }
63
83
 
64
- export const ForegroundDelegationParameters = createDelegationParameters(false);
65
-
66
- export const DelegationParameters = createDelegationParameters(true);
67
-
68
- export function delegationParameters(
69
- enableRunInBackground: boolean,
70
- agentNames?: readonly string[],
71
- ) {
72
- if (agentNames === undefined) {
73
- return enableRunInBackground ? DelegationParameters : ForegroundDelegationParameters;
74
- }
75
- return createDelegationParameters(enableRunInBackground, agentNames);
76
- }
77
-
78
- export const ForkDelegationParameters = Type.Object(
79
- forkDelegationFields(),
80
- { additionalProperties: false },
81
- );
82
-
83
84
  export function forkDelegationParameters(agentNames?: readonly string[]) {
84
- if (agentNames === undefined) return ForkDelegationParameters;
85
- return Type.Object(forkDelegationFields(agentNames), { additionalProperties: false });
85
+ const fields = forkDelegationFields(agentNames);
86
+ return Type.Object(
87
+ fields,
88
+ { additionalProperties: false },
89
+ );
86
90
  }
87
91
 
88
92
  export const SendMessageParameters = Type.Object(
89
93
  {
90
94
  subagent_id: Type.String({
91
- description: "Durable agent id of a direct continuable child",
95
+ description:
96
+ "Readable absolute/relative task path or durable id of a direct continuable child",
92
97
  minLength: 1,
98
+ maxLength: 4096,
93
99
  }),
94
100
  message: Type.String({
95
101
  description: "Message to enqueue as the child's next FIFO turn",
@@ -99,11 +105,43 @@ export const SendMessageParameters = Type.Object(
99
105
  { additionalProperties: false },
100
106
  );
101
107
 
108
+ export const FollowupTaskParameters = Type.Object(
109
+ {
110
+ subagent_id: Type.String({
111
+ description:
112
+ "Readable absolute/relative task path or durable id of a direct continuable child",
113
+ minLength: 1,
114
+ maxLength: 4096,
115
+ }),
116
+ },
117
+ { additionalProperties: false },
118
+ );
119
+
120
+ export const DEFAULT_WAIT_AGENT_TIMEOUT_MS = 30_000;
121
+ export const MAX_WAIT_AGENT_TIMEOUT_MS = 120_000;
122
+
123
+ export const WaitAgentParameters = Type.Object(
124
+ {
125
+ timeout_ms: Type.Optional(
126
+ Type.Integer({
127
+ description:
128
+ "Maximum event-driven wait in milliseconds before returning a timeout",
129
+ minimum: 0,
130
+ maximum: MAX_WAIT_AGENT_TIMEOUT_MS,
131
+ default: DEFAULT_WAIT_AGENT_TIMEOUT_MS,
132
+ }),
133
+ ),
134
+ },
135
+ { additionalProperties: false },
136
+ );
137
+
102
138
  export const InterruptParameters = Type.Object(
103
139
  {
104
140
  agent_id: Type.String({
105
- description: "Agent id of a live child or deeper descendant whose current turn should stop",
141
+ description:
142
+ "Readable absolute/relative task path or durable id of a live descendant whose current turn should stop",
106
143
  minLength: 1,
144
+ maxLength: 4096,
107
145
  }),
108
146
  },
109
147
  { additionalProperties: false },