@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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +28 -0
- package/dist/index.d.ts +41 -8
- package/dist/index.js +603 -359
- package/package.json +2 -2
- package/src/config.ts +4 -0
- package/src/harness.ts +41 -24
- package/src/kv-store.ts +216 -0
- package/src/memory.ts +26 -291
- package/src/state.ts +74 -9
- package/src/subagent-manager.ts +6 -2
- package/src/subagent-tools.ts +21 -48
- package/src/todo-tools.ts +363 -0
- package/.turbo/turbo-lint.log +0 -6
- package/.turbo/turbo-test.log +0 -135
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
203
|
+
await this.kv.setWithTtl(this.storageKey, serialized, Math.max(1, this.ttl));
|
|
205
204
|
} else {
|
|
206
|
-
await this.
|
|
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(
|
|
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
|
|
222
|
-
|
|
223
|
-
payload
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
|
|
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
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
}
|
package/src/subagent-manager.ts
CHANGED
|
@@ -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<
|
|
27
|
+
}): Promise<SubagentSpawnResult>;
|
|
24
28
|
|
|
25
|
-
sendMessage(subagentId: string, message: string): Promise<
|
|
29
|
+
sendMessage(subagentId: string, message: string): Promise<SubagentSpawnResult>;
|
|
26
30
|
|
|
27
31
|
stop(subagentId: string): Promise<void>;
|
|
28
32
|
|
package/src/subagent-tools.ts
CHANGED
|
@@ -1,48 +1,18 @@
|
|
|
1
|
-
import { defineTool, type
|
|
2
|
-
import type { SubagentManager
|
|
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
|
|
42
|
-
"
|
|
43
|
-
"
|
|
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
|
-
"-
|
|
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 =
|
|
36
|
+
const conversationId = context.conversationId;
|
|
67
37
|
if (!conversationId) {
|
|
68
38
|
return { error: "no active conversation to spawn subagent from" };
|
|
69
39
|
}
|
|
70
|
-
const
|
|
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
|
|
46
|
+
ownerId,
|
|
74
47
|
});
|
|
75
|
-
return
|
|
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
|
|
83
|
-
"
|
|
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
|
|
107
|
-
return
|
|
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 =
|
|
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
|
}
|