@poncho-ai/harness 0.25.0 → 0.27.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/memory.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  slugifyStorageComponent,
9
9
  STORAGE_SCHEMA_VERSION,
10
10
  } from "./agent-identity.js";
11
+ import { createRawKVStore, type RawKVStore } from "./kv-store.js";
11
12
 
12
13
  export interface MainMemory {
13
14
  content: string;
@@ -168,25 +169,23 @@ class FileMainMemoryStore implements MemoryStore {
168
169
  }
169
170
  }
170
171
 
171
- abstract class KeyValueMainMemoryStoreBase implements MemoryStore {
172
- protected readonly ttl?: number;
173
- protected readonly memoryFallback: InMemoryMemoryStore;
172
+ class KVBackedMemoryStore implements MemoryStore {
173
+ private readonly kv: RawKVStore;
174
+ private readonly storageKey: string;
175
+ private readonly ttl?: number;
176
+ private readonly memoryFallback: InMemoryMemoryStore;
174
177
 
175
- constructor(ttl?: number) {
178
+ constructor(kv: RawKVStore, storageKey: string, ttl?: number) {
179
+ this.kv = kv;
180
+ this.storageKey = storageKey;
176
181
  this.ttl = ttl;
177
182
  this.memoryFallback = new InMemoryMemoryStore(ttl);
178
183
  }
179
184
 
180
- protected abstract getRaw(key: string): Promise<string | undefined>;
181
- protected abstract setRaw(key: string, value: string): Promise<void>;
182
- protected abstract setRawWithTtl(key: string, value: string, ttl: number): Promise<void>;
183
-
184
- protected async readPayload(key: string): Promise<MainMemoryPayload> {
185
+ private async readPayload(): Promise<MainMemoryPayload> {
185
186
  try {
186
- const raw = await this.getRaw(key);
187
- if (!raw) {
188
- return { main: { ...DEFAULT_MAIN_MEMORY } };
189
- }
187
+ const raw = await this.kv.get(this.storageKey);
188
+ if (!raw) return { main: { ...DEFAULT_MAIN_MEMORY } };
190
189
  const parsed = JSON.parse(raw) as MainMemoryPayload;
191
190
  const content = typeof parsed.main?.content === "string" ? parsed.main.content : "";
192
191
  const updatedAt = typeof parsed.main?.updatedAt === "number" ? parsed.main.updatedAt : 0;
@@ -197,259 +196,30 @@ abstract class KeyValueMainMemoryStoreBase implements MemoryStore {
197
196
  }
198
197
  }
199
198
 
200
- protected async writePayload(key: string, payload: MainMemoryPayload): Promise<void> {
199
+ private async writePayload(payload: MainMemoryPayload): Promise<void> {
201
200
  try {
202
201
  const serialized = JSON.stringify(payload);
203
202
  if (typeof this.ttl === "number") {
204
- await this.setRawWithTtl(key, serialized, Math.max(1, this.ttl));
203
+ await this.kv.setWithTtl(this.storageKey, serialized, Math.max(1, this.ttl));
205
204
  } else {
206
- await this.setRaw(key, serialized);
205
+ await this.kv.set(this.storageKey, serialized);
207
206
  }
208
207
  } catch {
209
- await this.memoryFallback.updateMainMemory({
210
- content: payload.main.content,
211
- });
208
+ await this.memoryFallback.updateMainMemory({ content: payload.main.content });
212
209
  }
213
210
  }
214
211
 
215
212
  async getMainMemory(): Promise<MainMemory> {
216
- const payload = await this.readPayload(this.key());
213
+ const payload = await this.readPayload();
217
214
  return payload.main;
218
215
  }
219
216
 
220
217
  async updateMainMemory(input: { content: string }): Promise<MainMemory> {
221
- const key = this.key();
222
- const payload = await this.readPayload(key);
223
- payload.main = {
224
- content: input.content.trim(),
225
- updatedAt: Date.now(),
226
- };
227
- await this.writePayload(key, payload);
218
+ const payload = await this.readPayload();
219
+ payload.main = { content: input.content.trim(), updatedAt: Date.now() };
220
+ await this.writePayload(payload);
228
221
  return payload.main;
229
222
  }
230
-
231
- protected abstract key(): string;
232
- }
233
-
234
- class UpstashMemoryStore extends KeyValueMainMemoryStoreBase {
235
- private readonly baseUrl: string;
236
- private readonly token: string;
237
- private readonly storageKey: string;
238
-
239
- constructor(options: {
240
- baseUrl: string;
241
- token: string;
242
- storageKey: string;
243
- ttl?: number;
244
- }) {
245
- super(options.ttl);
246
- this.baseUrl = options.baseUrl.replace(/\/+$/, "");
247
- this.token = options.token;
248
- this.storageKey = options.storageKey;
249
- }
250
-
251
- protected key(): string {
252
- return this.storageKey;
253
- }
254
-
255
- private headers(): HeadersInit {
256
- return {
257
- Authorization: `Bearer ${this.token}`,
258
- "Content-Type": "application/json",
259
- };
260
- }
261
-
262
- protected async getRaw(key: string): Promise<string | undefined> {
263
- const response = await fetch(`${this.baseUrl}/get/${encodeURIComponent(key)}`, {
264
- method: "POST",
265
- headers: this.headers(),
266
- });
267
- if (!response.ok) {
268
- return undefined;
269
- }
270
- const payload = (await response.json()) as { result?: string | null };
271
- return payload.result ?? undefined;
272
- }
273
-
274
- protected async setRaw(key: string, value: string): Promise<void> {
275
- await fetch(
276
- `${this.baseUrl}/set/${encodeURIComponent(key)}/${encodeURIComponent(value)}`,
277
- { method: "POST", headers: this.headers() },
278
- );
279
- }
280
-
281
- protected async setRawWithTtl(key: string, value: string, ttl: number): Promise<void> {
282
- await fetch(
283
- `${this.baseUrl}/setex/${encodeURIComponent(key)}/${Math.max(1, ttl)}/${encodeURIComponent(
284
- value,
285
- )}`,
286
- { method: "POST", headers: this.headers() },
287
- );
288
- }
289
- }
290
-
291
- class RedisMemoryStore extends KeyValueMainMemoryStoreBase {
292
- private readonly storageKey: string;
293
- private readonly clientPromise: Promise<
294
- | {
295
- get: (key: string) => Promise<string | null>;
296
- set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
297
- }
298
- | undefined
299
- >;
300
-
301
- constructor(options: {
302
- url: string;
303
- storageKey: string;
304
- ttl?: number;
305
- }) {
306
- super(options.ttl);
307
- this.storageKey = options.storageKey;
308
- this.clientPromise = (async () => {
309
- try {
310
- const redisModule = (await import("redis")) as unknown as {
311
- createClient: (args: { url: string }) => {
312
- connect: () => Promise<unknown>;
313
- get: (key: string) => Promise<string | null>;
314
- set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
315
- };
316
- };
317
- const client = redisModule.createClient({ url: options.url });
318
- await client.connect();
319
- return client;
320
- } catch {
321
- return undefined;
322
- }
323
- })();
324
- }
325
-
326
- protected key(): string {
327
- return this.storageKey;
328
- }
329
-
330
- protected async getRaw(key: string): Promise<string | undefined> {
331
- const client = await this.clientPromise;
332
- if (!client) {
333
- throw new Error("Redis unavailable");
334
- }
335
- const value = await client.get(key);
336
- return value ?? undefined;
337
- }
338
-
339
- protected async setRaw(key: string, value: string): Promise<void> {
340
- const client = await this.clientPromise;
341
- if (!client) {
342
- throw new Error("Redis unavailable");
343
- }
344
- await client.set(key, value);
345
- }
346
-
347
- protected async setRawWithTtl(key: string, value: string, ttl: number): Promise<void> {
348
- const client = await this.clientPromise;
349
- if (!client) {
350
- throw new Error("Redis unavailable");
351
- }
352
- await client.set(key, value, { EX: Math.max(1, ttl) });
353
- }
354
- }
355
-
356
- class DynamoDbMemoryStore extends KeyValueMainMemoryStoreBase {
357
- private readonly storageKey: string;
358
- private readonly table: string;
359
- private readonly clientPromise: Promise<
360
- | {
361
- send: (command: unknown) => Promise<unknown>;
362
- GetItemCommand: new (input: unknown) => unknown;
363
- PutItemCommand: new (input: unknown) => unknown;
364
- }
365
- | undefined
366
- >;
367
-
368
- constructor(options: {
369
- table: string;
370
- storageKey: string;
371
- region?: string;
372
- ttl?: number;
373
- }) {
374
- super(options.ttl);
375
- this.storageKey = options.storageKey;
376
- this.table = options.table;
377
- this.clientPromise = (async () => {
378
- try {
379
- const module = (await import("@aws-sdk/client-dynamodb")) as {
380
- DynamoDBClient: new (input: { region?: string }) => {
381
- send: (command: unknown) => Promise<unknown>;
382
- };
383
- GetItemCommand: new (input: unknown) => unknown;
384
- PutItemCommand: new (input: unknown) => unknown;
385
- };
386
- const client = new module.DynamoDBClient({ region: options.region });
387
- return {
388
- send: client.send.bind(client),
389
- GetItemCommand: module.GetItemCommand,
390
- PutItemCommand: module.PutItemCommand,
391
- };
392
- } catch {
393
- return undefined;
394
- }
395
- })();
396
- }
397
-
398
- protected key(): string {
399
- return this.storageKey;
400
- }
401
-
402
- protected async getRaw(key: string): Promise<string | undefined> {
403
- const client = await this.clientPromise;
404
- if (!client) {
405
- throw new Error("DynamoDB unavailable");
406
- }
407
- const result = (await client.send(
408
- new client.GetItemCommand({
409
- TableName: this.table,
410
- Key: { runId: { S: key } },
411
- }),
412
- )) as {
413
- Item?: {
414
- value?: { S?: string };
415
- };
416
- };
417
- return result.Item?.value?.S;
418
- }
419
-
420
- protected async setRaw(key: string, value: string): Promise<void> {
421
- const client = await this.clientPromise;
422
- if (!client) {
423
- throw new Error("DynamoDB unavailable");
424
- }
425
- await client.send(
426
- new client.PutItemCommand({
427
- TableName: this.table,
428
- Item: {
429
- runId: { S: key },
430
- value: { S: value },
431
- },
432
- }),
433
- );
434
- }
435
-
436
- protected async setRawWithTtl(key: string, value: string, ttl: number): Promise<void> {
437
- const client = await this.clientPromise;
438
- if (!client) {
439
- throw new Error("DynamoDB unavailable");
440
- }
441
- const ttlEpoch = Math.floor(Date.now() / 1000) + Math.max(1, ttl);
442
- await client.send(
443
- new client.PutItemCommand({
444
- TableName: this.table,
445
- Item: {
446
- runId: { S: key },
447
- value: { S: value },
448
- ttl: { N: String(ttlEpoch) },
449
- },
450
- }),
451
- );
452
- }
453
223
  }
454
224
 
455
225
  export const createMemoryStore = (
@@ -459,54 +229,19 @@ export const createMemoryStore = (
459
229
  ): MemoryStore => {
460
230
  const provider = config?.provider ?? "local";
461
231
  const ttl = config?.ttl;
462
- const storageKey = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(
463
- agentId,
464
- )}:memory:main`;
465
232
  const workingDir = options?.workingDir ?? process.cwd();
233
+
466
234
  if (provider === "local") {
467
235
  return new FileMainMemoryStore(workingDir, ttl);
468
236
  }
469
237
  if (provider === "memory") {
470
238
  return new InMemoryMemoryStore(ttl);
471
239
  }
472
- if (provider === "upstash") {
473
- const urlEnv = config?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
474
- const tokenEnv = config?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
475
- const url = process.env[urlEnv] ?? "";
476
- const token = process.env[tokenEnv] ?? "";
477
- if (url && token) {
478
- return new UpstashMemoryStore({
479
- baseUrl: url,
480
- token,
481
- storageKey,
482
- ttl,
483
- });
484
- }
485
- return new InMemoryMemoryStore(ttl);
486
- }
487
- if (provider === "redis") {
488
- const urlEnv = config?.urlEnv ?? "REDIS_URL";
489
- const url = process.env[urlEnv] ?? "";
490
- if (url) {
491
- return new RedisMemoryStore({
492
- url,
493
- storageKey,
494
- ttl,
495
- });
496
- }
497
- return new InMemoryMemoryStore(ttl);
498
- }
499
- if (provider === "dynamodb") {
500
- const table = config?.table ?? process.env.PONCHO_DYNAMODB_TABLE ?? "";
501
- if (table) {
502
- return new DynamoDbMemoryStore({
503
- table,
504
- storageKey,
505
- region: config?.region,
506
- ttl,
507
- });
508
- }
509
- return new InMemoryMemoryStore(ttl);
240
+
241
+ const kv = createRawKVStore(config);
242
+ if (kv) {
243
+ const storageKey = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}:memory:main`;
244
+ return new KVBackedMemoryStore(kv, storageKey, ttl);
510
245
  }
511
246
  return new InMemoryMemoryStore(ttl);
512
247
  };
package/src/state.ts CHANGED
@@ -21,6 +21,15 @@ export interface StateStore {
21
21
  delete(runId: string): Promise<void>;
22
22
  }
23
23
 
24
+ export interface PendingSubagentResult {
25
+ subagentId: string;
26
+ task: string;
27
+ status: "completed" | "error" | "stopped";
28
+ result?: import("@poncho-ai/sdk").RunResult;
29
+ error?: import("@poncho-ai/sdk").AgentFailure;
30
+ timestamp: number;
31
+ }
32
+
24
33
  export interface Conversation {
25
34
  conversationId: string;
26
35
  title: string;
@@ -55,6 +64,13 @@ export interface Conversation {
55
64
  channelId: string;
56
65
  platformThreadId: string;
57
66
  };
67
+ pendingSubagentResults?: PendingSubagentResult[];
68
+ subagentCallbackCount?: number;
69
+ runningCallbackSince?: number;
70
+ lastActivityAt?: number;
71
+ /** Harness-internal message chain preserved across continuation runs.
72
+ * Cleared when a run completes without continuation. */
73
+ _continuationMessages?: Message[];
58
74
  createdAt: number;
59
75
  updatedAt: number;
60
76
  }
@@ -67,6 +83,7 @@ export interface ConversationStore {
67
83
  update(conversation: Conversation): Promise<void>;
68
84
  rename(conversationId: string, title: string): Promise<Conversation | undefined>;
69
85
  delete(conversationId: string): Promise<boolean>;
86
+ appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void>;
70
87
  }
71
88
 
72
89
  export type StateProviderName =
@@ -300,6 +317,14 @@ export class InMemoryConversationStore implements ConversationStore {
300
317
  async delete(conversationId: string): Promise<boolean> {
301
318
  return this.conversations.delete(conversationId);
302
319
  }
320
+
321
+ async appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void> {
322
+ const conversation = this.conversations.get(conversationId);
323
+ if (!conversation) return;
324
+ if (!conversation.pendingSubagentResults) conversation.pendingSubagentResults = [];
325
+ conversation.pendingSubagentResults.push(result);
326
+ conversation.updatedAt = Date.now();
327
+ }
303
328
  }
304
329
 
305
330
  export type ConversationSummary = {
@@ -572,6 +597,16 @@ class FileConversationStore implements ConversationStore {
572
597
  }
573
598
  return removed;
574
599
  }
600
+
601
+ async appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void> {
602
+ await this.ensureLoaded();
603
+ const conversation = await this.get(conversationId);
604
+ if (!conversation) return;
605
+ if (!conversation.pendingSubagentResults) conversation.pendingSubagentResults = [];
606
+ conversation.pendingSubagentResults.push(result);
607
+ conversation.updatedAt = Date.now();
608
+ await this.update(conversation);
609
+ }
575
610
  }
576
611
 
577
612
  type LocalStateFile = {
@@ -689,6 +724,7 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
689
724
  protected readonly ttl?: number;
690
725
  private readonly agentIdPromise: Promise<string>;
691
726
  private readonly ownerLocks = new Map<string, Promise<void>>();
727
+ private readonly appendLocks = new Map<string, Promise<void>>();
692
728
  protected readonly memoryFallback: InMemoryConversationStore;
693
729
 
694
730
  constructor(ttl: number | undefined, workingDir: string, agentId?: string) {
@@ -712,6 +748,19 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
712
748
  }
713
749
  }
714
750
 
751
+ private async withAppendLock(conversationId: string, task: () => Promise<void>): Promise<void> {
752
+ const prev = this.appendLocks.get(conversationId) ?? Promise.resolve();
753
+ const next = prev.then(task, task);
754
+ this.appendLocks.set(conversationId, next);
755
+ try {
756
+ await next;
757
+ } finally {
758
+ if (this.appendLocks.get(conversationId) === next) {
759
+ this.appendLocks.delete(conversationId);
760
+ }
761
+ }
762
+ }
763
+
715
764
  private async namespace(): Promise<string> {
716
765
  const agentId = await this.agentIdPromise;
717
766
  return `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}`;
@@ -945,6 +994,17 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
945
994
  });
946
995
  return true;
947
996
  }
997
+
998
+ async appendSubagentResult(conversationId: string, result: PendingSubagentResult): Promise<void> {
999
+ await this.withAppendLock(conversationId, async () => {
1000
+ const conversation = await this.get(conversationId);
1001
+ if (!conversation) return;
1002
+ if (!conversation.pendingSubagentResults) conversation.pendingSubagentResults = [];
1003
+ conversation.pendingSubagentResults.push(result);
1004
+ conversation.updatedAt = Date.now();
1005
+ await this.update(conversation);
1006
+ });
1007
+ }
948
1008
  }
949
1009
 
950
1010
  class UpstashConversationStore extends KeyValueConversationStoreBase {
@@ -991,23 +1051,28 @@ class UpstashConversationStore extends KeyValueConversationStoreBase {
991
1051
  return (payload.result ?? []).map((v) => v ?? undefined);
992
1052
  },
993
1053
  set: async (key: string, value: string, ttl?: number) => {
994
- const endpoint =
995
- typeof ttl === "number"
996
- ? `${this.baseUrl}/setex/${encodeURIComponent(key)}/${Math.max(
997
- 1,
998
- ttl,
999
- )}/${encodeURIComponent(value)}`
1000
- : `${this.baseUrl}/set/${encodeURIComponent(key)}/${encodeURIComponent(value)}`;
1001
- await fetch(endpoint, {
1054
+ const command = typeof ttl === "number"
1055
+ ? ["SETEX", key, Math.max(1, ttl), value]
1056
+ : ["SET", key, value];
1057
+ const response = await fetch(this.baseUrl, {
1002
1058
  method: "POST",
1003
1059
  headers: this.headers(),
1060
+ body: JSON.stringify(command),
1004
1061
  });
1062
+ if (!response.ok) {
1063
+ const text = await response.text().catch(() => "");
1064
+ console.error(`[store][upstash] SET failed (${response.status}): ${text.slice(0, 200)}`);
1065
+ }
1005
1066
  },
1006
1067
  del: async (key: string) => {
1007
- await fetch(`${this.baseUrl}/del/${encodeURIComponent(key)}`, {
1068
+ const response = await fetch(`${this.baseUrl}/del/${encodeURIComponent(key)}`, {
1008
1069
  method: "POST",
1009
1070
  headers: this.headers(),
1010
1071
  });
1072
+ if (!response.ok) {
1073
+ const text = await response.text().catch(() => "");
1074
+ console.error(`[store][upstash] DEL failed (${response.status}): ${text.slice(0, 200)}`);
1075
+ }
1011
1076
  },
1012
1077
  };
1013
1078
  }
@@ -15,14 +15,18 @@ export interface SubagentSummary {
15
15
  messageCount: number;
16
16
  }
17
17
 
18
+ export interface SubagentSpawnResult {
19
+ subagentId: string;
20
+ }
21
+
18
22
  export interface SubagentManager {
19
23
  spawn(opts: {
20
24
  task: string;
21
25
  parentConversationId: string;
22
26
  ownerId: string;
23
- }): Promise<SubagentResult>;
27
+ }): Promise<SubagentSpawnResult>;
24
28
 
25
- sendMessage(subagentId: string, message: string): Promise<SubagentResult>;
29
+ sendMessage(subagentId: string, message: string): Promise<SubagentSpawnResult>;
26
30
 
27
31
  stop(subagentId: string): Promise<void>;
28
32
 
@@ -1,48 +1,18 @@
1
- import { defineTool, type Message, type ToolDefinition, getTextContent } from "@poncho-ai/sdk";
2
- import type { SubagentManager, SubagentResult } from "./subagent-manager.js";
3
-
4
- const LAST_MESSAGES_TO_RETURN = 10;
5
-
6
- const summarizeResult = (r: SubagentResult): Record<string, unknown> => {
7
- const summary: Record<string, unknown> = {
8
- subagentId: r.subagentId,
9
- status: r.status,
10
- };
11
- if (r.result) {
12
- summary.result = {
13
- status: r.result.status,
14
- response: r.result.response,
15
- steps: r.result.steps,
16
- duration: r.result.duration,
17
- };
18
- }
19
- if (r.error) {
20
- summary.error = r.error;
21
- }
22
- if (r.latestMessages && r.latestMessages.length > 0) {
23
- summary.latestMessages = r.latestMessages
24
- .slice(-LAST_MESSAGES_TO_RETURN)
25
- .map((m: Message) => ({
26
- role: m.role,
27
- content: getTextContent(m).slice(0, 2000),
28
- }));
29
- }
30
- return summary;
31
- };
1
+ import { defineTool, type ToolContext, type ToolDefinition } from "@poncho-ai/sdk";
2
+ import type { SubagentManager } from "./subagent-manager.js";
32
3
 
33
4
  export const createSubagentTools = (
34
5
  manager: SubagentManager,
35
- getConversationId: () => string | undefined,
36
- getOwnerId: () => string,
37
6
  ): ToolDefinition[] => [
38
7
  defineTool({
39
8
  name: "spawn_subagent",
40
9
  description:
41
- "Spawn a subagent to work on a task and wait for it to finish. The subagent is a full copy of " +
42
- "yourself running in its own conversation context with access to the same tools (except memory writes). " +
43
- "This call blocks until the subagent completes and returns its result.\n\n" +
10
+ "Spawn a subagent to work on a task in the background. Returns immediately with a subagent ID. " +
11
+ "The subagent runs independently and its result will be delivered to you as a message in the " +
12
+ "conversation when it completes.\n\n" +
44
13
  "Guidelines:\n" +
45
- "- Use subagents to parallelize work: call spawn_subagent multiple times in one response for independent sub-tasks -- they run concurrently.\n" +
14
+ "- Spawn all needed subagents in a SINGLE response (they run concurrently), then end your turn with a brief message to the user.\n" +
15
+ "- Do NOT spawn more subagents in follow-up steps. Wait for results to be delivered before deciding if more work is needed.\n" +
46
16
  "- Prefer doing work yourself for simple or quick tasks. Spawn subagents for substantial, self-contained work.\n" +
47
17
  "- The subagent has no memory of your conversation -- write thorough, self-contained instructions in the task.",
48
18
  inputSchema: {
@@ -58,29 +28,32 @@ export const createSubagentTools = (
58
28
  required: ["task"],
59
29
  additionalProperties: false,
60
30
  },
61
- handler: async (input) => {
31
+ handler: async (input: Record<string, unknown>, context: ToolContext) => {
62
32
  const task = typeof input.task === "string" ? input.task : "";
63
33
  if (!task.trim()) {
64
34
  return { error: "task is required" };
65
35
  }
66
- const conversationId = getConversationId();
36
+ const conversationId = context.conversationId;
67
37
  if (!conversationId) {
68
38
  return { error: "no active conversation to spawn subagent from" };
69
39
  }
70
- const result = await manager.spawn({
40
+ const ownerId = typeof context.parameters.__ownerId === "string"
41
+ ? context.parameters.__ownerId
42
+ : "anonymous";
43
+ const { subagentId } = await manager.spawn({
71
44
  task: task.trim(),
72
45
  parentConversationId: conversationId,
73
- ownerId: getOwnerId(),
46
+ ownerId,
74
47
  });
75
- return summarizeResult(result);
48
+ return { subagentId, status: "running" };
76
49
  },
77
50
  }),
78
51
 
79
52
  defineTool({
80
53
  name: "message_subagent",
81
54
  description:
82
- "Send a follow-up message to a completed or stopped subagent and wait for it to finish. " +
83
- "This restarts the subagent with the new message and blocks until it completes. " +
55
+ "Send a follow-up message to a completed or stopped subagent. The subagent restarts in the " +
56
+ "background and its result will be delivered to you as a message when it completes. " +
84
57
  "Only works when the subagent is not currently running.",
85
58
  inputSchema: {
86
59
  type: "object",
@@ -103,8 +76,8 @@ export const createSubagentTools = (
103
76
  if (!subagentId || !message.trim()) {
104
77
  return { error: "subagent_id and message are required" };
105
78
  }
106
- const result = await manager.sendMessage(subagentId, message.trim());
107
- return summarizeResult(result);
79
+ const { subagentId: id } = await manager.sendMessage(subagentId, message.trim());
80
+ return { subagentId: id, status: "running" };
108
81
  },
109
82
  }),
110
83
 
@@ -145,8 +118,8 @@ export const createSubagentTools = (
145
118
  properties: {},
146
119
  additionalProperties: false,
147
120
  },
148
- handler: async () => {
149
- const conversationId = getConversationId();
121
+ handler: async (_input: Record<string, unknown>, context: ToolContext) => {
122
+ const conversationId = context.conversationId;
150
123
  if (!conversationId) {
151
124
  return { error: "no active conversation" };
152
125
  }