@poncho-ai/harness 0.24.0 → 0.26.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/dist/index.js CHANGED
@@ -1402,7 +1402,8 @@ if (result.compacted) {
1402
1402
  When \`memory.enabled\` is true in \`poncho.config.js\`, the harness enables a simple memory model:
1403
1403
 
1404
1404
  - A single persistent main memory document is loaded at run start and interpolated into the system prompt under \`## Persistent Memory\`.
1405
- - \`memory_main_update\` can replace or append to that document. The tool description instructs the model to proactively evaluate each turn whether durable memory should be updated.
1405
+ - \`memory_main_write\` overwrites the entire memory document (for initial writes or full rewrites).
1406
+ - \`memory_main_edit\` performs targeted string-replacement edits on memory (find \`old_str\`, replace with \`new_str\`), mirroring \`edit_file\` semantics. The tool description instructs the model to proactively evaluate each turn whether durable memory should be updated.
1406
1407
  - \`conversation_recall\` can search recent prior conversations (keyword scoring) when historical context is relevant (\`as we discussed\`, \`last time\`, etc.).
1407
1408
 
1408
1409
  \`\`\`javascript
@@ -1420,9 +1421,10 @@ export default {
1420
1421
 
1421
1422
  Available memory tools:
1422
1423
 
1423
- - \`memory_main_get\`
1424
- - \`memory_main_update\`
1425
- - \`conversation_recall\`
1424
+ - \`memory_main_get\` \u2014 read the current memory document
1425
+ - \`memory_main_write\` \u2014 overwrite the entire memory document
1426
+ - \`memory_main_edit\` \u2014 edit memory via exact string replacement (\`old_str\` / \`new_str\`)
1427
+ - \`conversation_recall\` \u2014 search past conversations
1426
1428
  `,
1427
1429
  "configuration": `# Configuration & Security
1428
1430
 
@@ -2028,6 +2030,8 @@ var ponchoDocsTool = defineTool({
2028
2030
 
2029
2031
  // src/harness.ts
2030
2032
  import { randomUUID as randomUUID3 } from "crypto";
2033
+ import { readFile as readFile8 } from "fs/promises";
2034
+ import { resolve as resolve10 } from "path";
2031
2035
  import { getTextContent as getTextContent3 } from "@poncho-ai/sdk";
2032
2036
 
2033
2037
  // src/upload-store.ts
@@ -2342,6 +2346,142 @@ var createUploadStore = async (config, workingDir) => {
2342
2346
  import { mkdir as mkdir3, readFile as readFile5, rename, writeFile as writeFile4 } from "fs/promises";
2343
2347
  import { dirname as dirname2, resolve as resolve6 } from "path";
2344
2348
  import { defineTool as defineTool2 } from "@poncho-ai/sdk";
2349
+
2350
+ // src/kv-store.ts
2351
+ var UpstashKVStore = class {
2352
+ baseUrl;
2353
+ token;
2354
+ constructor(baseUrl, token) {
2355
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
2356
+ this.token = token;
2357
+ }
2358
+ headers() {
2359
+ return { Authorization: `Bearer ${this.token}`, "Content-Type": "application/json" };
2360
+ }
2361
+ async get(key) {
2362
+ const response = await fetch(`${this.baseUrl}/get/${encodeURIComponent(key)}`, {
2363
+ method: "POST",
2364
+ headers: this.headers()
2365
+ });
2366
+ if (!response.ok) return void 0;
2367
+ const payload = await response.json();
2368
+ return payload.result ?? void 0;
2369
+ }
2370
+ async set(key, value) {
2371
+ await fetch(
2372
+ `${this.baseUrl}/set/${encodeURIComponent(key)}/${encodeURIComponent(value)}`,
2373
+ { method: "POST", headers: this.headers() }
2374
+ );
2375
+ }
2376
+ async setWithTtl(key, value, ttl) {
2377
+ await fetch(
2378
+ `${this.baseUrl}/setex/${encodeURIComponent(key)}/${Math.max(1, ttl)}/${encodeURIComponent(value)}`,
2379
+ { method: "POST", headers: this.headers() }
2380
+ );
2381
+ }
2382
+ };
2383
+ var RedisKVStore = class {
2384
+ clientPromise;
2385
+ constructor(url) {
2386
+ this.clientPromise = (async () => {
2387
+ try {
2388
+ const redisModule = await import("redis");
2389
+ const client = redisModule.createClient({ url });
2390
+ await client.connect();
2391
+ return client;
2392
+ } catch {
2393
+ return void 0;
2394
+ }
2395
+ })();
2396
+ }
2397
+ async get(key) {
2398
+ const client = await this.clientPromise;
2399
+ if (!client) throw new Error("Redis unavailable");
2400
+ const value = await client.get(key);
2401
+ return value ?? void 0;
2402
+ }
2403
+ async set(key, value) {
2404
+ const client = await this.clientPromise;
2405
+ if (!client) throw new Error("Redis unavailable");
2406
+ await client.set(key, value);
2407
+ }
2408
+ async setWithTtl(key, value, ttl) {
2409
+ const client = await this.clientPromise;
2410
+ if (!client) throw new Error("Redis unavailable");
2411
+ await client.set(key, value, { EX: Math.max(1, ttl) });
2412
+ }
2413
+ };
2414
+ var DynamoDbKVStore = class {
2415
+ table;
2416
+ clientPromise;
2417
+ constructor(table, region) {
2418
+ this.table = table;
2419
+ this.clientPromise = (async () => {
2420
+ try {
2421
+ const module = await import("@aws-sdk/client-dynamodb");
2422
+ const client = new module.DynamoDBClient({ region });
2423
+ return {
2424
+ send: client.send.bind(client),
2425
+ GetItemCommand: module.GetItemCommand,
2426
+ PutItemCommand: module.PutItemCommand
2427
+ };
2428
+ } catch {
2429
+ return void 0;
2430
+ }
2431
+ })();
2432
+ }
2433
+ async get(key) {
2434
+ const client = await this.clientPromise;
2435
+ if (!client) throw new Error("DynamoDB unavailable");
2436
+ const result = await client.send(
2437
+ new client.GetItemCommand({ TableName: this.table, Key: { runId: { S: key } } })
2438
+ );
2439
+ return result.Item?.value?.S;
2440
+ }
2441
+ async set(key, value) {
2442
+ const client = await this.clientPromise;
2443
+ if (!client) throw new Error("DynamoDB unavailable");
2444
+ await client.send(
2445
+ new client.PutItemCommand({
2446
+ TableName: this.table,
2447
+ Item: { runId: { S: key }, value: { S: value } }
2448
+ })
2449
+ );
2450
+ }
2451
+ async setWithTtl(key, value, ttl) {
2452
+ const client = await this.clientPromise;
2453
+ if (!client) throw new Error("DynamoDB unavailable");
2454
+ const ttlEpoch = Math.floor(Date.now() / 1e3) + Math.max(1, ttl);
2455
+ await client.send(
2456
+ new client.PutItemCommand({
2457
+ TableName: this.table,
2458
+ Item: { runId: { S: key }, value: { S: value }, ttl: { N: String(ttlEpoch) } }
2459
+ })
2460
+ );
2461
+ }
2462
+ };
2463
+ var createRawKVStore = (config) => {
2464
+ const provider = config?.provider ?? "local";
2465
+ if (provider === "upstash") {
2466
+ const urlEnv = config?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
2467
+ const tokenEnv = config?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
2468
+ const url = process.env[urlEnv] ?? "";
2469
+ const token = process.env[tokenEnv] ?? "";
2470
+ if (url && token) return new UpstashKVStore(url, token);
2471
+ }
2472
+ if (provider === "redis") {
2473
+ const urlEnv = config?.urlEnv ?? "REDIS_URL";
2474
+ const url = process.env[urlEnv] ?? "";
2475
+ if (url) return new RedisKVStore(url);
2476
+ }
2477
+ if (provider === "dynamodb") {
2478
+ const table = config?.table ?? process.env.PONCHO_DYNAMODB_TABLE ?? "";
2479
+ if (table) return new DynamoDbKVStore(table, config?.region);
2480
+ }
2481
+ return void 0;
2482
+ };
2483
+
2484
+ // src/memory.ts
2345
2485
  var DEFAULT_MAIN_MEMORY = {
2346
2486
  content: "",
2347
2487
  updatedAt: 0
@@ -2384,14 +2524,9 @@ var InMemoryMemoryStore = class {
2384
2524
  return this.mainMemory;
2385
2525
  }
2386
2526
  async updateMainMemory(input) {
2387
- const now2 = Date.now();
2388
- const existing = await this.getMainMemory();
2389
- const nextContent = input.mode === "append" && existing.content ? `${existing.content}
2390
-
2391
- ${input.content}`.trim() : input.content;
2392
2527
  this.mainMemory = {
2393
- content: nextContent.trim(),
2394
- updatedAt: now2
2528
+ content: input.content.trim(),
2529
+ updatedAt: Date.now()
2395
2530
  };
2396
2531
  return this.mainMemory;
2397
2532
  }
@@ -2449,31 +2584,29 @@ var FileMainMemoryStore = class {
2449
2584
  }
2450
2585
  async updateMainMemory(input) {
2451
2586
  await this.ensureLoaded();
2452
- const existing = await this.getMainMemory();
2453
- const nextContent = input.mode === "append" && existing.content ? `${existing.content}
2454
-
2455
- ${input.content}`.trim() : input.content;
2456
2587
  this.mainMemory = {
2457
- content: nextContent.trim(),
2588
+ content: input.content.trim(),
2458
2589
  updatedAt: Date.now()
2459
2590
  };
2460
2591
  await this.persist();
2461
2592
  return this.mainMemory;
2462
2593
  }
2463
2594
  };
2464
- var KeyValueMainMemoryStoreBase = class {
2595
+ var KVBackedMemoryStore = class {
2596
+ kv;
2597
+ storageKey;
2465
2598
  ttl;
2466
2599
  memoryFallback;
2467
- constructor(ttl) {
2600
+ constructor(kv, storageKey, ttl) {
2601
+ this.kv = kv;
2602
+ this.storageKey = storageKey;
2468
2603
  this.ttl = ttl;
2469
2604
  this.memoryFallback = new InMemoryMemoryStore(ttl);
2470
2605
  }
2471
- async readPayload(key) {
2606
+ async readPayload() {
2472
2607
  try {
2473
- const raw = await this.getRaw(key);
2474
- if (!raw) {
2475
- return { main: { ...DEFAULT_MAIN_MEMORY } };
2476
- }
2608
+ const raw = await this.kv.get(this.storageKey);
2609
+ if (!raw) return { main: { ...DEFAULT_MAIN_MEMORY } };
2477
2610
  const parsed = JSON.parse(raw);
2478
2611
  const content = typeof parsed.main?.content === "string" ? parsed.main.content : "";
2479
2612
  const updatedAt = typeof parsed.main?.updatedAt === "number" ? parsed.main.updatedAt : 0;
@@ -2483,204 +2616,32 @@ var KeyValueMainMemoryStoreBase = class {
2483
2616
  return { main };
2484
2617
  }
2485
2618
  }
2486
- async writePayload(key, payload) {
2619
+ async writePayload(payload) {
2487
2620
  try {
2488
2621
  const serialized = JSON.stringify(payload);
2489
2622
  if (typeof this.ttl === "number") {
2490
- await this.setRawWithTtl(key, serialized, Math.max(1, this.ttl));
2623
+ await this.kv.setWithTtl(this.storageKey, serialized, Math.max(1, this.ttl));
2491
2624
  } else {
2492
- await this.setRaw(key, serialized);
2625
+ await this.kv.set(this.storageKey, serialized);
2493
2626
  }
2494
2627
  } catch {
2495
- await this.memoryFallback.updateMainMemory({
2496
- content: payload.main.content,
2497
- mode: "replace"
2498
- });
2628
+ await this.memoryFallback.updateMainMemory({ content: payload.main.content });
2499
2629
  }
2500
2630
  }
2501
2631
  async getMainMemory() {
2502
- const payload = await this.readPayload(this.key());
2632
+ const payload = await this.readPayload();
2503
2633
  return payload.main;
2504
2634
  }
2505
2635
  async updateMainMemory(input) {
2506
- const key = this.key();
2507
- const payload = await this.readPayload(key);
2508
- const nextContent = input.mode === "append" && payload.main.content ? `${payload.main.content}
2509
-
2510
- ${input.content}`.trim() : input.content;
2511
- payload.main = {
2512
- content: nextContent.trim(),
2513
- updatedAt: Date.now()
2514
- };
2515
- await this.writePayload(key, payload);
2636
+ const payload = await this.readPayload();
2637
+ payload.main = { content: input.content.trim(), updatedAt: Date.now() };
2638
+ await this.writePayload(payload);
2516
2639
  return payload.main;
2517
2640
  }
2518
2641
  };
2519
- var UpstashMemoryStore = class extends KeyValueMainMemoryStoreBase {
2520
- baseUrl;
2521
- token;
2522
- storageKey;
2523
- constructor(options) {
2524
- super(options.ttl);
2525
- this.baseUrl = options.baseUrl.replace(/\/+$/, "");
2526
- this.token = options.token;
2527
- this.storageKey = options.storageKey;
2528
- }
2529
- key() {
2530
- return this.storageKey;
2531
- }
2532
- headers() {
2533
- return {
2534
- Authorization: `Bearer ${this.token}`,
2535
- "Content-Type": "application/json"
2536
- };
2537
- }
2538
- async getRaw(key) {
2539
- const response = await fetch(`${this.baseUrl}/get/${encodeURIComponent(key)}`, {
2540
- method: "POST",
2541
- headers: this.headers()
2542
- });
2543
- if (!response.ok) {
2544
- return void 0;
2545
- }
2546
- const payload = await response.json();
2547
- return payload.result ?? void 0;
2548
- }
2549
- async setRaw(key, value) {
2550
- await fetch(
2551
- `${this.baseUrl}/set/${encodeURIComponent(key)}/${encodeURIComponent(value)}`,
2552
- { method: "POST", headers: this.headers() }
2553
- );
2554
- }
2555
- async setRawWithTtl(key, value, ttl) {
2556
- await fetch(
2557
- `${this.baseUrl}/setex/${encodeURIComponent(key)}/${Math.max(1, ttl)}/${encodeURIComponent(
2558
- value
2559
- )}`,
2560
- { method: "POST", headers: this.headers() }
2561
- );
2562
- }
2563
- };
2564
- var RedisMemoryStore = class extends KeyValueMainMemoryStoreBase {
2565
- storageKey;
2566
- clientPromise;
2567
- constructor(options) {
2568
- super(options.ttl);
2569
- this.storageKey = options.storageKey;
2570
- this.clientPromise = (async () => {
2571
- try {
2572
- const redisModule = await import("redis");
2573
- const client = redisModule.createClient({ url: options.url });
2574
- await client.connect();
2575
- return client;
2576
- } catch {
2577
- return void 0;
2578
- }
2579
- })();
2580
- }
2581
- key() {
2582
- return this.storageKey;
2583
- }
2584
- async getRaw(key) {
2585
- const client = await this.clientPromise;
2586
- if (!client) {
2587
- throw new Error("Redis unavailable");
2588
- }
2589
- const value = await client.get(key);
2590
- return value ?? void 0;
2591
- }
2592
- async setRaw(key, value) {
2593
- const client = await this.clientPromise;
2594
- if (!client) {
2595
- throw new Error("Redis unavailable");
2596
- }
2597
- await client.set(key, value);
2598
- }
2599
- async setRawWithTtl(key, value, ttl) {
2600
- const client = await this.clientPromise;
2601
- if (!client) {
2602
- throw new Error("Redis unavailable");
2603
- }
2604
- await client.set(key, value, { EX: Math.max(1, ttl) });
2605
- }
2606
- };
2607
- var DynamoDbMemoryStore = class extends KeyValueMainMemoryStoreBase {
2608
- storageKey;
2609
- table;
2610
- clientPromise;
2611
- constructor(options) {
2612
- super(options.ttl);
2613
- this.storageKey = options.storageKey;
2614
- this.table = options.table;
2615
- this.clientPromise = (async () => {
2616
- try {
2617
- const module = await import("@aws-sdk/client-dynamodb");
2618
- const client = new module.DynamoDBClient({ region: options.region });
2619
- return {
2620
- send: client.send.bind(client),
2621
- GetItemCommand: module.GetItemCommand,
2622
- PutItemCommand: module.PutItemCommand
2623
- };
2624
- } catch {
2625
- return void 0;
2626
- }
2627
- })();
2628
- }
2629
- key() {
2630
- return this.storageKey;
2631
- }
2632
- async getRaw(key) {
2633
- const client = await this.clientPromise;
2634
- if (!client) {
2635
- throw new Error("DynamoDB unavailable");
2636
- }
2637
- const result = await client.send(
2638
- new client.GetItemCommand({
2639
- TableName: this.table,
2640
- Key: { runId: { S: key } }
2641
- })
2642
- );
2643
- return result.Item?.value?.S;
2644
- }
2645
- async setRaw(key, value) {
2646
- const client = await this.clientPromise;
2647
- if (!client) {
2648
- throw new Error("DynamoDB unavailable");
2649
- }
2650
- await client.send(
2651
- new client.PutItemCommand({
2652
- TableName: this.table,
2653
- Item: {
2654
- runId: { S: key },
2655
- value: { S: value }
2656
- }
2657
- })
2658
- );
2659
- }
2660
- async setRawWithTtl(key, value, ttl) {
2661
- const client = await this.clientPromise;
2662
- if (!client) {
2663
- throw new Error("DynamoDB unavailable");
2664
- }
2665
- const ttlEpoch = Math.floor(Date.now() / 1e3) + Math.max(1, ttl);
2666
- await client.send(
2667
- new client.PutItemCommand({
2668
- TableName: this.table,
2669
- Item: {
2670
- runId: { S: key },
2671
- value: { S: value },
2672
- ttl: { N: String(ttlEpoch) }
2673
- }
2674
- })
2675
- );
2676
- }
2677
- };
2678
2642
  var createMemoryStore = (agentId, config, options) => {
2679
2643
  const provider = config?.provider ?? "local";
2680
2644
  const ttl = config?.ttl;
2681
- const storageKey = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(
2682
- agentId
2683
- )}:memory:main`;
2684
2645
  const workingDir = options?.workingDir ?? process.cwd();
2685
2646
  if (provider === "local") {
2686
2647
  return new FileMainMemoryStore(workingDir, ttl);
@@ -2688,44 +2649,10 @@ var createMemoryStore = (agentId, config, options) => {
2688
2649
  if (provider === "memory") {
2689
2650
  return new InMemoryMemoryStore(ttl);
2690
2651
  }
2691
- if (provider === "upstash") {
2692
- const urlEnv = config?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
2693
- const tokenEnv = config?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
2694
- const url = process.env[urlEnv] ?? "";
2695
- const token = process.env[tokenEnv] ?? "";
2696
- if (url && token) {
2697
- return new UpstashMemoryStore({
2698
- baseUrl: url,
2699
- token,
2700
- storageKey,
2701
- ttl
2702
- });
2703
- }
2704
- return new InMemoryMemoryStore(ttl);
2705
- }
2706
- if (provider === "redis") {
2707
- const urlEnv = config?.urlEnv ?? "REDIS_URL";
2708
- const url = process.env[urlEnv] ?? "";
2709
- if (url) {
2710
- return new RedisMemoryStore({
2711
- url,
2712
- storageKey,
2713
- ttl
2714
- });
2715
- }
2716
- return new InMemoryMemoryStore(ttl);
2717
- }
2718
- if (provider === "dynamodb") {
2719
- const table = config?.table ?? process.env.PONCHO_DYNAMODB_TABLE ?? "";
2720
- if (table) {
2721
- return new DynamoDbMemoryStore({
2722
- table,
2723
- storageKey,
2724
- region: config?.region,
2725
- ttl
2726
- });
2727
- }
2728
- return new InMemoryMemoryStore(ttl);
2652
+ const kv = createRawKVStore(config);
2653
+ if (kv) {
2654
+ const storageKey = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}:memory:main`;
2655
+ return new KVBackedMemoryStore(kv, storageKey, ttl);
2729
2656
  }
2730
2657
  return new InMemoryMemoryStore(ttl);
2731
2658
  };
@@ -2775,19 +2702,14 @@ var createMemoryTools = (store, options) => {
2775
2702
  }
2776
2703
  }),
2777
2704
  defineTool2({
2778
- name: "memory_main_update",
2779
- description: "Update persistent main memory when new stable preferences, long-term goals, or durable facts appear. Proactively evaluate every turn whether memory should be updated, and avoid storing ephemeral details.",
2705
+ name: "memory_main_write",
2706
+ description: "Overwrite the entire persistent main memory document. Use for initial writes or full rewrites. Prefer memory_main_edit for targeted changes to existing memory.",
2780
2707
  inputSchema: {
2781
2708
  type: "object",
2782
2709
  properties: {
2783
- mode: {
2784
- type: "string",
2785
- enum: ["replace", "append"],
2786
- description: "replace overwrites memory; append adds content to the end"
2787
- },
2788
2710
  content: {
2789
2711
  type: "string",
2790
- description: "The memory content to write"
2712
+ description: "The full memory content to write"
2791
2713
  }
2792
2714
  },
2793
2715
  required: ["content"],
@@ -2798,8 +2720,50 @@ var createMemoryTools = (store, options) => {
2798
2720
  if (!content) {
2799
2721
  throw new Error("content is required");
2800
2722
  }
2801
- const mode = input.mode === "append" || input.mode === "replace" ? input.mode : "replace";
2802
- const memory = await store.updateMainMemory({ content, mode });
2723
+ const memory = await store.updateMainMemory({ content });
2724
+ return { ok: true, memory };
2725
+ }
2726
+ }),
2727
+ defineTool2({
2728
+ name: "memory_main_edit",
2729
+ description: "Edit persistent main memory by replacing an exact string match with new content. The old_str must match exactly one location in memory. Use an empty new_str to delete matched content. Proactively evaluate every turn whether memory should be updated.",
2730
+ inputSchema: {
2731
+ type: "object",
2732
+ properties: {
2733
+ old_str: {
2734
+ type: "string",
2735
+ description: "The exact text to find and replace (must be unique in memory). Include surrounding context if needed to ensure uniqueness."
2736
+ },
2737
+ new_str: {
2738
+ type: "string",
2739
+ description: "The replacement text (use empty string to delete the matched content)"
2740
+ }
2741
+ },
2742
+ required: ["old_str", "new_str"],
2743
+ additionalProperties: false
2744
+ },
2745
+ handler: async (input) => {
2746
+ const oldStr = typeof input.old_str === "string" ? input.old_str : "";
2747
+ const newStr = typeof input.new_str === "string" ? input.new_str : "";
2748
+ if (!oldStr) {
2749
+ throw new Error("old_str must not be empty.");
2750
+ }
2751
+ const current = await store.getMainMemory();
2752
+ const content = current.content;
2753
+ const first = content.indexOf(oldStr);
2754
+ if (first === -1) {
2755
+ throw new Error(
2756
+ "old_str not found in memory. Make sure it matches exactly, including whitespace and line breaks."
2757
+ );
2758
+ }
2759
+ const last = content.lastIndexOf(oldStr);
2760
+ if (first !== last) {
2761
+ throw new Error(
2762
+ "old_str appears multiple times in memory. Please provide more context to ensure a unique match."
2763
+ );
2764
+ }
2765
+ const newContent = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
2766
+ const memory = await store.updateMainMemory({ content: newContent });
2803
2767
  return { ok: true, memory };
2804
2768
  }
2805
2769
  }),
@@ -2861,6 +2825,269 @@ ${item.content}`, query)
2861
2825
  ];
2862
2826
  };
2863
2827
 
2828
+ // src/todo-tools.ts
2829
+ import { mkdir as mkdir4, readFile as readFile6, rename as rename2, writeFile as writeFile5 } from "fs/promises";
2830
+ import { dirname as dirname3, resolve as resolve7 } from "path";
2831
+ import { defineTool as defineTool3 } from "@poncho-ai/sdk";
2832
+ var VALID_STATUSES = ["pending", "in_progress", "completed"];
2833
+ var VALID_PRIORITIES = ["high", "medium", "low"];
2834
+ var TODOS_DIRECTORY = "todos";
2835
+ var writeJsonAtomic2 = async (filePath, payload) => {
2836
+ await mkdir4(dirname3(filePath), { recursive: true });
2837
+ const tmpPath = `${filePath}.tmp`;
2838
+ await writeFile5(tmpPath, JSON.stringify(payload, null, 2), "utf8");
2839
+ await rename2(tmpPath, filePath);
2840
+ };
2841
+ var parseTodoList = (raw) => {
2842
+ if (!Array.isArray(raw)) return [];
2843
+ return raw.filter(
2844
+ (item) => typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.content === "string"
2845
+ );
2846
+ };
2847
+ var generateId = () => (globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`).slice(0, 8);
2848
+ var InMemoryTodoStore = class {
2849
+ store = /* @__PURE__ */ new Map();
2850
+ async get(conversationId) {
2851
+ return this.store.get(conversationId) ?? [];
2852
+ }
2853
+ async set(conversationId, todos) {
2854
+ this.store.set(conversationId, todos);
2855
+ }
2856
+ };
2857
+ var FileTodoStore = class {
2858
+ workingDir;
2859
+ todosDir = "";
2860
+ constructor(workingDir) {
2861
+ this.workingDir = workingDir;
2862
+ }
2863
+ async ensureTodosDir() {
2864
+ if (this.todosDir) return this.todosDir;
2865
+ const identity = await ensureAgentIdentity(this.workingDir);
2866
+ this.todosDir = resolve7(getAgentStoreDirectory(identity), TODOS_DIRECTORY);
2867
+ await mkdir4(this.todosDir, { recursive: true });
2868
+ return this.todosDir;
2869
+ }
2870
+ async filePath(conversationId) {
2871
+ const dir = await this.ensureTodosDir();
2872
+ return resolve7(dir, `${slugifyStorageComponent(conversationId)}.json`);
2873
+ }
2874
+ async get(conversationId) {
2875
+ try {
2876
+ const fp = await this.filePath(conversationId);
2877
+ const raw = await readFile6(fp, "utf8");
2878
+ return parseTodoList(JSON.parse(raw));
2879
+ } catch {
2880
+ return [];
2881
+ }
2882
+ }
2883
+ async set(conversationId, todos) {
2884
+ const fp = await this.filePath(conversationId);
2885
+ await writeJsonAtomic2(fp, todos);
2886
+ }
2887
+ };
2888
+ var KVBackedTodoStore = class {
2889
+ kv;
2890
+ baseKey;
2891
+ ttl;
2892
+ memoryFallback = new InMemoryTodoStore();
2893
+ constructor(kv, baseKey, ttl) {
2894
+ this.kv = kv;
2895
+ this.baseKey = baseKey;
2896
+ this.ttl = ttl;
2897
+ }
2898
+ keyFor(conversationId) {
2899
+ return `${this.baseKey}:${slugifyStorageComponent(conversationId)}`;
2900
+ }
2901
+ async get(conversationId) {
2902
+ try {
2903
+ const raw = await this.kv.get(this.keyFor(conversationId));
2904
+ if (!raw) return [];
2905
+ return parseTodoList(JSON.parse(raw));
2906
+ } catch {
2907
+ return this.memoryFallback.get(conversationId);
2908
+ }
2909
+ }
2910
+ async set(conversationId, todos) {
2911
+ try {
2912
+ const serialized = JSON.stringify(todos);
2913
+ const key = this.keyFor(conversationId);
2914
+ if (typeof this.ttl === "number") {
2915
+ await this.kv.setWithTtl(key, serialized, Math.max(1, this.ttl));
2916
+ } else {
2917
+ await this.kv.set(key, serialized);
2918
+ }
2919
+ } catch {
2920
+ await this.memoryFallback.set(conversationId, todos);
2921
+ }
2922
+ }
2923
+ };
2924
+ var createTodoStore = (agentId, config, options) => {
2925
+ const provider = config?.provider ?? "local";
2926
+ const ttl = config?.ttl;
2927
+ const workingDir = options?.workingDir ?? process.cwd();
2928
+ if (provider === "local") {
2929
+ return new FileTodoStore(workingDir);
2930
+ }
2931
+ if (provider === "memory") {
2932
+ return new InMemoryTodoStore();
2933
+ }
2934
+ const kv = createRawKVStore(config);
2935
+ if (kv) {
2936
+ const baseKey = `poncho:${STORAGE_SCHEMA_VERSION}:${slugifyStorageComponent(agentId)}:todos`;
2937
+ return new KVBackedTodoStore(kv, baseKey, ttl);
2938
+ }
2939
+ return new InMemoryTodoStore();
2940
+ };
2941
+ var createTodoTools = (store) => {
2942
+ const resolveKey = (context) => context.conversationId || context.runId;
2943
+ return [
2944
+ defineTool3({
2945
+ name: "todo_list",
2946
+ description: "List all todo items for the current conversation. Use this to check progress and plan next steps.",
2947
+ inputSchema: {
2948
+ type: "object",
2949
+ properties: {
2950
+ status: {
2951
+ type: "string",
2952
+ enum: VALID_STATUSES,
2953
+ description: "Filter by status (omit to list all)"
2954
+ }
2955
+ },
2956
+ additionalProperties: false
2957
+ },
2958
+ handler: async (input, context) => {
2959
+ const key = resolveKey(context);
2960
+ let todos = await store.get(key);
2961
+ const status = typeof input.status === "string" ? input.status : void 0;
2962
+ if (status && VALID_STATUSES.includes(status)) {
2963
+ todos = todos.filter((t) => t.status === status);
2964
+ }
2965
+ return { todos, count: todos.length };
2966
+ }
2967
+ }),
2968
+ defineTool3({
2969
+ name: "todo_add",
2970
+ description: "Add a new todo item for the current conversation. Use proactively for complex multi-step tasks (3+ steps).",
2971
+ inputSchema: {
2972
+ type: "object",
2973
+ properties: {
2974
+ content: {
2975
+ type: "string",
2976
+ description: "Description of the task"
2977
+ },
2978
+ status: {
2979
+ type: "string",
2980
+ enum: VALID_STATUSES,
2981
+ description: "Initial status (default: pending)"
2982
+ },
2983
+ priority: {
2984
+ type: "string",
2985
+ enum: VALID_PRIORITIES,
2986
+ description: "Priority level (default: medium)"
2987
+ }
2988
+ },
2989
+ required: ["content"],
2990
+ additionalProperties: false
2991
+ },
2992
+ handler: async (input, context) => {
2993
+ const content = typeof input.content === "string" ? input.content.trim() : "";
2994
+ if (!content) throw new Error("content is required");
2995
+ const status = typeof input.status === "string" && VALID_STATUSES.includes(input.status) ? input.status : "pending";
2996
+ const priority = typeof input.priority === "string" && VALID_PRIORITIES.includes(input.priority) ? input.priority : "medium";
2997
+ const now2 = Date.now();
2998
+ const todo = {
2999
+ id: generateId(),
3000
+ content,
3001
+ status,
3002
+ priority,
3003
+ createdAt: now2,
3004
+ updatedAt: now2
3005
+ };
3006
+ const key = resolveKey(context);
3007
+ const todos = await store.get(key);
3008
+ todos.push(todo);
3009
+ await store.set(key, todos);
3010
+ return { todo, todos };
3011
+ }
3012
+ }),
3013
+ defineTool3({
3014
+ name: "todo_update",
3015
+ description: "Update an existing todo item's status, content, or priority. Mark tasks in_progress when starting and completed when done.",
3016
+ inputSchema: {
3017
+ type: "object",
3018
+ properties: {
3019
+ id: {
3020
+ type: "string",
3021
+ description: "ID of the todo to update"
3022
+ },
3023
+ status: {
3024
+ type: "string",
3025
+ enum: VALID_STATUSES,
3026
+ description: "New status"
3027
+ },
3028
+ content: {
3029
+ type: "string",
3030
+ description: "New content/description"
3031
+ },
3032
+ priority: {
3033
+ type: "string",
3034
+ enum: VALID_PRIORITIES,
3035
+ description: "New priority level"
3036
+ }
3037
+ },
3038
+ required: ["id"],
3039
+ additionalProperties: false
3040
+ },
3041
+ handler: async (input, context) => {
3042
+ const id = typeof input.id === "string" ? input.id : "";
3043
+ if (!id) throw new Error("id is required");
3044
+ const key = resolveKey(context);
3045
+ const todos = await store.get(key);
3046
+ const todo = todos.find((t) => t.id === id);
3047
+ if (!todo) throw new Error(`Todo with id "${id}" not found`);
3048
+ if (typeof input.status === "string" && VALID_STATUSES.includes(input.status)) {
3049
+ todo.status = input.status;
3050
+ }
3051
+ if (typeof input.content === "string" && input.content.trim()) {
3052
+ todo.content = input.content.trim();
3053
+ }
3054
+ if (typeof input.priority === "string" && VALID_PRIORITIES.includes(input.priority)) {
3055
+ todo.priority = input.priority;
3056
+ }
3057
+ todo.updatedAt = Date.now();
3058
+ await store.set(key, todos);
3059
+ return { todo, todos };
3060
+ }
3061
+ }),
3062
+ defineTool3({
3063
+ name: "todo_remove",
3064
+ description: "Remove a todo item by ID.",
3065
+ inputSchema: {
3066
+ type: "object",
3067
+ properties: {
3068
+ id: {
3069
+ type: "string",
3070
+ description: "ID of the todo to remove"
3071
+ }
3072
+ },
3073
+ required: ["id"],
3074
+ additionalProperties: false
3075
+ },
3076
+ handler: async (input, context) => {
3077
+ const id = typeof input.id === "string" ? input.id : "";
3078
+ if (!id) throw new Error("id is required");
3079
+ const key = resolveKey(context);
3080
+ const todos = await store.get(key);
3081
+ const index = todos.findIndex((t) => t.id === id);
3082
+ if (index === -1) throw new Error(`Todo with id "${id}" not found`);
3083
+ const [removed] = todos.splice(index, 1);
3084
+ await store.set(key, todos);
3085
+ return { removed, todos };
3086
+ }
3087
+ })
3088
+ ];
3089
+ };
3090
+
2864
3091
  // src/mcp.ts
2865
3092
  var McpHttpError = class extends Error {
2866
3093
  status;
@@ -3355,8 +3582,8 @@ var createModelProvider = (provider, config) => {
3355
3582
  };
3356
3583
 
3357
3584
  // src/skill-context.ts
3358
- import { readFile as readFile6, readdir as readdir2, stat } from "fs/promises";
3359
- import { dirname as dirname3, resolve as resolve7, normalize } from "path";
3585
+ import { readFile as readFile7, readdir as readdir2, stat } from "fs/promises";
3586
+ import { dirname as dirname4, resolve as resolve8, normalize } from "path";
3360
3587
  import YAML3 from "yaml";
3361
3588
  var DEFAULT_SKILL_DIRS = ["skills"];
3362
3589
  var resolveSkillDirs = (workingDir, extraPaths) => {
@@ -3368,7 +3595,7 @@ var resolveSkillDirs = (workingDir, extraPaths) => {
3368
3595
  }
3369
3596
  }
3370
3597
  }
3371
- return dirs.map((d) => resolve7(workingDir, d));
3598
+ return dirs.map((d) => resolve8(workingDir, d));
3372
3599
  };
3373
3600
  var FRONTMATTER_PATTERN3 = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
3374
3601
  var asRecord2 = (value) => typeof value === "object" && value !== null ? value : {};
@@ -3437,7 +3664,7 @@ var collectSkillManifests = async (directory) => {
3437
3664
  const entries = await readdir2(directory, { withFileTypes: true });
3438
3665
  const files = [];
3439
3666
  for (const entry of entries) {
3440
- const fullPath = resolve7(directory, entry.name);
3667
+ const fullPath = resolve8(directory, entry.name);
3441
3668
  let isDir = entry.isDirectory();
3442
3669
  let isFile = entry.isFile();
3443
3670
  if (entry.isSymbolicLink()) {
@@ -3472,13 +3699,13 @@ var loadSkillMetadata = async (workingDir, extraSkillPaths) => {
3472
3699
  const seen = /* @__PURE__ */ new Set();
3473
3700
  for (const manifest of allManifests) {
3474
3701
  try {
3475
- const content = await readFile6(manifest, "utf8");
3702
+ const content = await readFile7(manifest, "utf8");
3476
3703
  const parsed = parseSkillFrontmatter(content);
3477
3704
  if (parsed && !seen.has(parsed.name)) {
3478
3705
  seen.add(parsed.name);
3479
3706
  skills.push({
3480
3707
  ...parsed,
3481
- skillDir: dirname3(manifest),
3708
+ skillDir: dirname4(manifest),
3482
3709
  skillPath: manifest
3483
3710
  });
3484
3711
  }
@@ -3517,7 +3744,7 @@ ${xmlSkills}
3517
3744
  };
3518
3745
  var escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
3519
3746
  var loadSkillInstructions = async (skill) => {
3520
- const content = await readFile6(skill.skillPath, "utf8");
3747
+ const content = await readFile7(skill.skillPath, "utf8");
3521
3748
  const match = content.match(FRONTMATTER_PATTERN3);
3522
3749
  return match ? match[2].trim() : content.trim();
3523
3750
  };
@@ -3526,11 +3753,11 @@ var readSkillResource = async (skill, relativePath) => {
3526
3753
  if (normalized.startsWith("..") || normalized.startsWith("/")) {
3527
3754
  throw new Error("Path must be relative and within the skill directory");
3528
3755
  }
3529
- const fullPath = resolve7(skill.skillDir, normalized);
3756
+ const fullPath = resolve8(skill.skillDir, normalized);
3530
3757
  if (!fullPath.startsWith(skill.skillDir)) {
3531
3758
  throw new Error("Path escapes the skill directory");
3532
3759
  }
3533
- return await readFile6(fullPath, "utf8");
3760
+ return await readFile7(fullPath, "utf8");
3534
3761
  };
3535
3762
  var MAX_INSTRUCTIONS_PER_SKILL = 1200;
3536
3763
  var loadSkillContext = async (workingDir) => {
@@ -3647,16 +3874,16 @@ function convertSchema(schema) {
3647
3874
  }
3648
3875
 
3649
3876
  // src/skill-tools.ts
3650
- import { defineTool as defineTool3 } from "@poncho-ai/sdk";
3877
+ import { defineTool as defineTool4 } from "@poncho-ai/sdk";
3651
3878
  import { access as access2, readdir as readdir3, stat as stat2 } from "fs/promises";
3652
- import { extname, normalize as normalize2, resolve as resolve8, sep as sep2 } from "path";
3879
+ import { extname, normalize as normalize2, resolve as resolve9, sep as sep2 } from "path";
3653
3880
  import { pathToFileURL } from "url";
3654
3881
  import { createJiti as createJiti2 } from "jiti";
3655
3882
  var createSkillTools = (skills, options) => {
3656
3883
  const skillsByName = new Map(skills.map((skill) => [skill.name, skill]));
3657
3884
  const knownNames = skills.length > 0 ? skills.map((skill) => skill.name).join(", ") : "(none)";
3658
3885
  return [
3659
- defineTool3({
3886
+ defineTool4({
3660
3887
  name: "activate_skill",
3661
3888
  description: `Load the full instructions for an available skill. Use this when a user's request matches a skill's description. Available skills: ${knownNames}`,
3662
3889
  inputSchema: {
@@ -3693,7 +3920,7 @@ var createSkillTools = (skills, options) => {
3693
3920
  }
3694
3921
  }
3695
3922
  }),
3696
- defineTool3({
3923
+ defineTool4({
3697
3924
  name: "deactivate_skill",
3698
3925
  description: "Deactivate a previously activated skill and update scoped tool availability.",
3699
3926
  inputSchema: {
@@ -3722,7 +3949,7 @@ var createSkillTools = (skills, options) => {
3722
3949
  }
3723
3950
  }
3724
3951
  }),
3725
- defineTool3({
3952
+ defineTool4({
3726
3953
  name: "list_active_skills",
3727
3954
  description: "List currently active skills with scoped MCP tools.",
3728
3955
  inputSchema: {
@@ -3734,7 +3961,7 @@ var createSkillTools = (skills, options) => {
3734
3961
  activeSkills: options?.onListActiveSkills ? options.onListActiveSkills() : []
3735
3962
  })
3736
3963
  }),
3737
- defineTool3({
3964
+ defineTool4({
3738
3965
  name: "read_skill_resource",
3739
3966
  description: `Read a file from a skill's directory (references, scripts, assets). Use relative paths from the skill root. Available skills: ${knownNames}`,
3740
3967
  inputSchema: {
@@ -3774,7 +4001,7 @@ var createSkillTools = (skills, options) => {
3774
4001
  }
3775
4002
  }
3776
4003
  }),
3777
- defineTool3({
4004
+ defineTool4({
3778
4005
  name: "list_skill_scripts",
3779
4006
  description: `List JavaScript/TypeScript script files available under a skill directory (recursive). Available skills: ${knownNames}`,
3780
4007
  inputSchema: {
@@ -3809,7 +4036,7 @@ var createSkillTools = (skills, options) => {
3809
4036
  }
3810
4037
  }
3811
4038
  }),
3812
- defineTool3({
4039
+ defineTool4({
3813
4040
  name: "run_skill_script",
3814
4041
  description: `Run a JavaScript/TypeScript module in a skill or project directory. Uses default export function or named run/main/handler function. Available skills: ${knownNames}`,
3815
4042
  inputSchema: {
@@ -3906,7 +4133,7 @@ var collectScriptFiles = async (directory) => {
3906
4133
  if (entry.name === "node_modules") {
3907
4134
  continue;
3908
4135
  }
3909
- const fullPath = resolve8(directory, entry.name);
4136
+ const fullPath = resolve9(directory, entry.name);
3910
4137
  let isDir = entry.isDirectory();
3911
4138
  let isFile = entry.isFile();
3912
4139
  if (entry.isSymbolicLink()) {
@@ -3945,8 +4172,8 @@ var normalizeScriptPolicyPath = (relativePath) => {
3945
4172
  };
3946
4173
  var resolveScriptPath = (baseDir, relativePath) => {
3947
4174
  const normalized = normalizeScriptPolicyPath(relativePath);
3948
- const fullPath = resolve8(baseDir, normalized);
3949
- if (!fullPath.startsWith(`${resolve8(baseDir)}${sep2}`) && fullPath !== resolve8(baseDir)) {
4175
+ const fullPath = resolve9(baseDir, normalized);
4176
+ if (!fullPath.startsWith(`${resolve9(baseDir)}${sep2}`) && fullPath !== resolve9(baseDir)) {
3950
4177
  throw new Error("Script path must stay inside the allowed directory");
3951
4178
  }
3952
4179
  const extension = extname(fullPath).toLowerCase();
@@ -4020,7 +4247,7 @@ var extractRunnableFunction = (value) => {
4020
4247
  };
4021
4248
 
4022
4249
  // src/subagent-tools.ts
4023
- import { defineTool as defineTool4, getTextContent as getTextContent2 } from "@poncho-ai/sdk";
4250
+ import { defineTool as defineTool5, getTextContent as getTextContent2 } from "@poncho-ai/sdk";
4024
4251
  var LAST_MESSAGES_TO_RETURN = 10;
4025
4252
  var summarizeResult = (r) => {
4026
4253
  const summary = {
@@ -4047,7 +4274,7 @@ var summarizeResult = (r) => {
4047
4274
  return summary;
4048
4275
  };
4049
4276
  var createSubagentTools = (manager, getConversationId, getOwnerId) => [
4050
- defineTool4({
4277
+ defineTool5({
4051
4278
  name: "spawn_subagent",
4052
4279
  description: "Spawn a subagent to work on a task and wait for it to finish. The subagent is a full copy of yourself running in its own conversation context with access to the same tools (except memory writes). This call blocks until the subagent completes and returns its result.\n\nGuidelines:\n- Use subagents to parallelize work: call spawn_subagent multiple times in one response for independent sub-tasks -- they run concurrently.\n- Prefer doing work yourself for simple or quick tasks. Spawn subagents for substantial, self-contained work.\n- The subagent has no memory of your conversation -- write thorough, self-contained instructions in the task.",
4053
4280
  inputSchema: {
@@ -4078,7 +4305,7 @@ var createSubagentTools = (manager, getConversationId, getOwnerId) => [
4078
4305
  return summarizeResult(result);
4079
4306
  }
4080
4307
  }),
4081
- defineTool4({
4308
+ defineTool5({
4082
4309
  name: "message_subagent",
4083
4310
  description: "Send a follow-up message to a completed or stopped subagent and wait for it to finish. This restarts the subagent with the new message and blocks until it completes. Only works when the subagent is not currently running.",
4084
4311
  inputSchema: {
@@ -4106,7 +4333,7 @@ var createSubagentTools = (manager, getConversationId, getOwnerId) => [
4106
4333
  return summarizeResult(result);
4107
4334
  }
4108
4335
  }),
4109
- defineTool4({
4336
+ defineTool5({
4110
4337
  name: "stop_subagent",
4111
4338
  description: "Stop a running subagent. The subagent's conversation is preserved but it will stop processing. Use this to cancel work that is no longer needed.",
4112
4339
  inputSchema: {
@@ -4129,7 +4356,7 @@ var createSubagentTools = (manager, getConversationId, getOwnerId) => [
4129
4356
  return { message: `Subagent "${subagentId}" has been stopped.` };
4130
4357
  }
4131
4358
  }),
4132
- defineTool4({
4359
+ defineTool5({
4133
4360
  name: "list_subagents",
4134
4361
  description: "List all subagents that have been spawned in this conversation. Returns each subagent's ID, original task, current status, and message count. Use this to look up subagent IDs before calling message_subagent or stop_subagent.",
4135
4362
  inputSchema: {
@@ -4677,6 +4904,7 @@ var AgentHarness = class _AgentHarness {
4677
4904
  uploadStore;
4678
4905
  skillContextWindow = "";
4679
4906
  memoryStore;
4907
+ todoStore;
4680
4908
  loadedConfig;
4681
4909
  loadedSkills = [];
4682
4910
  skillFingerprint = "";
@@ -4688,6 +4916,7 @@ var AgentHarness = class _AgentHarness {
4688
4916
  _browserSession;
4689
4917
  _browserMod;
4690
4918
  parsedAgent;
4919
+ agentFileFingerprint = "";
4691
4920
  mcpBridge;
4692
4921
  subagentManager;
4693
4922
  resolveToolAccess(toolName) {
@@ -4784,6 +5013,10 @@ var AgentHarness = class _AgentHarness {
4784
5013
  get frontmatter() {
4785
5014
  return this.parsedAgent?.frontmatter;
4786
5015
  }
5016
+ async getTodos(conversationId) {
5017
+ if (!this.todoStore) return [];
5018
+ return this.todoStore.get(conversationId);
5019
+ }
4787
5020
  listActiveSkills() {
4788
5021
  return [...this.activeSkillNames].sort();
4789
5022
  }
@@ -4800,20 +5033,17 @@ var AgentHarness = class _AgentHarness {
4800
5033
  return this.parsedAgent?.frontmatter.approvalRequired?.scripts ?? [];
4801
5034
  }
4802
5035
  getRequestedMcpPatterns() {
4803
- const skillPatterns = /* @__PURE__ */ new Set();
5036
+ const patterns = new Set(this.getAgentMcpIntent());
4804
5037
  for (const skillName of this.activeSkillNames) {
4805
5038
  const skill = this.loadedSkills.find((entry) => entry.name === skillName);
4806
5039
  if (!skill) {
4807
5040
  continue;
4808
5041
  }
4809
5042
  for (const pattern of skill.allowedTools.mcp) {
4810
- skillPatterns.add(pattern);
5043
+ patterns.add(pattern);
4811
5044
  }
4812
5045
  }
4813
- if (skillPatterns.size > 0) {
4814
- return [...skillPatterns];
4815
- }
4816
- return this.getAgentMcpIntent();
5046
+ return [...patterns];
4817
5047
  }
4818
5048
  getRequestedScriptPatterns() {
4819
5049
  const patterns = new Set(this.getAgentScriptIntent());
@@ -4829,20 +5059,17 @@ var AgentHarness = class _AgentHarness {
4829
5059
  return [...patterns];
4830
5060
  }
4831
5061
  getRequestedMcpApprovalPatterns() {
4832
- const skillPatterns = /* @__PURE__ */ new Set();
5062
+ const patterns = new Set(this.getAgentMcpApprovalPatterns());
4833
5063
  for (const skillName of this.activeSkillNames) {
4834
5064
  const skill = this.loadedSkills.find((entry) => entry.name === skillName);
4835
5065
  if (!skill) {
4836
5066
  continue;
4837
5067
  }
4838
5068
  for (const pattern of skill.approvalRequired.mcp) {
4839
- skillPatterns.add(pattern);
5069
+ patterns.add(pattern);
4840
5070
  }
4841
5071
  }
4842
- if (skillPatterns.size > 0) {
4843
- return [...skillPatterns];
4844
- }
4845
- return this.getAgentMcpApprovalPatterns();
5072
+ return [...patterns];
4846
5073
  }
4847
5074
  getRequestedScriptApprovalPatterns() {
4848
5075
  const patterns = new Set(this.getAgentScriptApprovalPatterns());
@@ -4973,13 +5200,54 @@ var AgentHarness = class _AgentHarness {
4973
5200
  );
4974
5201
  }
4975
5202
  static SKILL_REFRESH_DEBOUNCE_MS = 3e3;
4976
- async refreshSkillsIfChanged() {
5203
+ /**
5204
+ * Re-read AGENT.md and update the parsed agent when the file has changed
5205
+ * on disk. Returns `true` when the agent was actually re-parsed.
5206
+ *
5207
+ * Preserves the agent identity (id) across reloads so conversation
5208
+ * continuity isn't broken.
5209
+ */
5210
+ async refreshAgentIfChanged() {
4977
5211
  if (this.environment !== "development") {
4978
- return;
5212
+ return false;
4979
5213
  }
4980
- const elapsed = Date.now() - this.lastSkillRefreshAt;
4981
- if (this.lastSkillRefreshAt > 0 && elapsed < _AgentHarness.SKILL_REFRESH_DEBOUNCE_MS) {
4982
- return;
5214
+ try {
5215
+ const agentFilePath = resolve10(this.workingDir, "AGENT.md");
5216
+ const rawContent = await readFile8(agentFilePath, "utf8");
5217
+ if (rawContent === this.agentFileFingerprint) {
5218
+ return false;
5219
+ }
5220
+ const parsed = parseAgentMarkdown(rawContent);
5221
+ if (!parsed.frontmatter.id && this.parsedAgent?.frontmatter.id) {
5222
+ parsed.frontmatter.id = this.parsedAgent.frontmatter.id;
5223
+ }
5224
+ this.parsedAgent = parsed;
5225
+ this.agentFileFingerprint = rawContent;
5226
+ return true;
5227
+ } catch (error) {
5228
+ console.warn(
5229
+ `[poncho][agent] Failed to refresh AGENT.md in development mode: ${error instanceof Error ? error.message : String(error)}`
5230
+ );
5231
+ return false;
5232
+ }
5233
+ }
5234
+ /**
5235
+ * Re-scan skill directories and update metadata, tools, and context window
5236
+ * when skills have changed on disk. Returns `true` when the skill set was
5237
+ * actually updated.
5238
+ *
5239
+ * @param force - bypass the time-based debounce (used for mid-run refreshes
5240
+ * after the agent may have written new skill files).
5241
+ */
5242
+ async refreshSkillsIfChanged(force = false) {
5243
+ if (this.environment !== "development") {
5244
+ return false;
5245
+ }
5246
+ if (!force) {
5247
+ const elapsed = Date.now() - this.lastSkillRefreshAt;
5248
+ if (this.lastSkillRefreshAt > 0 && elapsed < _AgentHarness.SKILL_REFRESH_DEBOUNCE_MS) {
5249
+ return false;
5250
+ }
4983
5251
  }
4984
5252
  this.lastSkillRefreshAt = Date.now();
4985
5253
  try {
@@ -4989,22 +5257,35 @@ var AgentHarness = class _AgentHarness {
4989
5257
  );
4990
5258
  const nextFingerprint = this.buildSkillFingerprint(latestSkills);
4991
5259
  if (nextFingerprint === this.skillFingerprint) {
4992
- return;
5260
+ return false;
4993
5261
  }
4994
5262
  this.loadedSkills = latestSkills;
4995
5263
  this.skillContextWindow = buildSkillContextWindow(latestSkills);
4996
5264
  this.skillFingerprint = nextFingerprint;
4997
5265
  this.registerSkillTools(latestSkills);
4998
- this.activeSkillNames.clear();
5266
+ const latestSkillNames = new Set(latestSkills.map((s) => s.name));
5267
+ for (const name of this.activeSkillNames) {
5268
+ if (!latestSkillNames.has(name)) {
5269
+ this.activeSkillNames.delete(name);
5270
+ }
5271
+ }
5272
+ if (this.mcpBridge) {
5273
+ await this.mcpBridge.discoverTools();
5274
+ }
4999
5275
  await this.refreshMcpTools("skills:changed");
5276
+ return true;
5000
5277
  } catch (error) {
5001
5278
  console.warn(
5002
5279
  `[poncho][skills] Failed to refresh skills in development mode: ${error instanceof Error ? error.message : String(error)}`
5003
5280
  );
5281
+ return false;
5004
5282
  }
5005
5283
  }
5006
5284
  async initialize() {
5007
- this.parsedAgent = await parseAgentFile(this.workingDir);
5285
+ const agentFilePath = resolve10(this.workingDir, "AGENT.md");
5286
+ const agentRawContent = await readFile8(agentFilePath, "utf8");
5287
+ this.parsedAgent = parseAgentMarkdown(agentRawContent);
5288
+ this.agentFileFingerprint = agentRawContent;
5008
5289
  const identity = await ensureAgentIdentity(this.workingDir);
5009
5290
  if (!this.parsedAgent.frontmatter.id) {
5010
5291
  this.parsedAgent.frontmatter.id = identity.id;
@@ -5025,8 +5306,8 @@ var AgentHarness = class _AgentHarness {
5025
5306
  this.skillContextWindow = buildSkillContextWindow(skillMetadata);
5026
5307
  this.skillFingerprint = this.buildSkillFingerprint(skillMetadata);
5027
5308
  this.registerSkillTools(skillMetadata);
5309
+ const agentId = this.parsedAgent.frontmatter.id ?? this.parsedAgent.frontmatter.name;
5028
5310
  if (memoryConfig?.enabled) {
5029
- const agentId = this.parsedAgent.frontmatter.id ?? this.parsedAgent.frontmatter.name;
5030
5311
  this.memoryStore = createMemoryStore(
5031
5312
  agentId,
5032
5313
  memoryConfig,
@@ -5038,6 +5319,13 @@ var AgentHarness = class _AgentHarness {
5038
5319
  })
5039
5320
  );
5040
5321
  }
5322
+ const stateConfig = resolveStateConfig(config);
5323
+ this.todoStore = createTodoStore(agentId, stateConfig, { workingDir: this.workingDir });
5324
+ for (const tool of createTodoTools(this.todoStore)) {
5325
+ if (this.isToolEnabled(tool.name)) {
5326
+ this.registerIfMissing(tool);
5327
+ }
5328
+ }
5041
5329
  if (config?.browser) {
5042
5330
  await this.initBrowserTools(config).catch((e) => {
5043
5331
  console.warn(
@@ -5077,14 +5365,14 @@ var AgentHarness = class _AgentHarness {
5077
5365
  const filePath = pathResolve(stateDir, `${sessionId}.json`);
5078
5366
  return {
5079
5367
  async save(json) {
5080
- const { mkdir: mkdir5, writeFile: writeFile6 } = await import("fs/promises");
5081
- await mkdir5(stateDir, { recursive: true });
5082
- await writeFile6(filePath, json, "utf8");
5368
+ const { mkdir: mkdir6, writeFile: writeFile7 } = await import("fs/promises");
5369
+ await mkdir6(stateDir, { recursive: true });
5370
+ await writeFile7(filePath, json, "utf8");
5083
5371
  },
5084
5372
  async load() {
5085
- const { readFile: readFile8 } = await import("fs/promises");
5373
+ const { readFile: readFile10 } = await import("fs/promises");
5086
5374
  try {
5087
- return await readFile8(filePath, "utf8");
5375
+ return await readFile10(filePath, "utf8");
5088
5376
  } catch {
5089
5377
  return void 0;
5090
5378
  }
@@ -5150,7 +5438,7 @@ var AgentHarness = class _AgentHarness {
5150
5438
  let browserMod;
5151
5439
  try {
5152
5440
  const { existsSync } = await import("fs");
5153
- const { join, dirname: dirname5 } = await import("path");
5441
+ const { join, dirname: dirname6 } = await import("path");
5154
5442
  const { pathToFileURL: pathToFileURL2 } = await import("url");
5155
5443
  let searchDir = this.workingDir;
5156
5444
  let entryPath;
@@ -5160,7 +5448,7 @@ var AgentHarness = class _AgentHarness {
5160
5448
  entryPath = candidate;
5161
5449
  break;
5162
5450
  }
5163
- const parent = dirname5(searchDir);
5451
+ const parent = dirname6(searchDir);
5164
5452
  if (parent === searchDir) break;
5165
5453
  searchDir = parent;
5166
5454
  }
@@ -5248,9 +5536,9 @@ var AgentHarness = class _AgentHarness {
5248
5536
  for await (const event of this.run(input)) {
5249
5537
  eventQueue.push(event);
5250
5538
  if (queueResolve) {
5251
- const resolve10 = queueResolve;
5539
+ const resolve12 = queueResolve;
5252
5540
  queueResolve = null;
5253
- resolve10();
5541
+ resolve12();
5254
5542
  }
5255
5543
  }
5256
5544
  } catch (error) {
@@ -5269,8 +5557,8 @@ var AgentHarness = class _AgentHarness {
5269
5557
  if (eventQueue.length > 0) {
5270
5558
  yield eventQueue.shift();
5271
5559
  } else if (!generatorDone) {
5272
- await new Promise((resolve10) => {
5273
- queueResolve = resolve10;
5560
+ await new Promise((resolve12) => {
5561
+ queueResolve = resolve12;
5274
5562
  });
5275
5563
  }
5276
5564
  }
@@ -5308,13 +5596,14 @@ var AgentHarness = class _AgentHarness {
5308
5596
  await this.initialize();
5309
5597
  }
5310
5598
  const memoryPromise = this.memoryStore ? this.memoryStore.getMainMemory() : void 0;
5599
+ await this.refreshAgentIfChanged();
5311
5600
  await this.refreshSkillsIfChanged();
5312
5601
  this._currentRunConversationId = input.conversationId;
5313
5602
  const ownerParam = input.parameters?.__ownerId;
5314
5603
  if (typeof ownerParam === "string") {
5315
5604
  this._currentRunOwnerId = ownerParam;
5316
5605
  }
5317
- const agent = this.parsedAgent;
5606
+ let agent = this.parsedAgent;
5318
5607
  const runId = `run_${randomUUID3()}`;
5319
5608
  const start = now();
5320
5609
  const maxSteps = agent.frontmatter.limits?.maxSteps ?? 50;
@@ -5325,11 +5614,11 @@ var AgentHarness = class _AgentHarness {
5325
5614
  const messages = [...input.messages ?? []];
5326
5615
  const inputMessageCount = messages.length;
5327
5616
  const events = [];
5328
- const systemPrompt = renderAgentPrompt(agent, {
5617
+ const renderCurrentAgentPrompt = () => renderAgentPrompt(this.parsedAgent, {
5329
5618
  parameters: input.parameters,
5330
5619
  runtime: {
5331
5620
  runId,
5332
- agentId: agent.frontmatter.id ?? agent.frontmatter.name,
5621
+ agentId: this.parsedAgent.frontmatter.id ?? this.parsedAgent.frontmatter.name,
5333
5622
  environment: this.environment,
5334
5623
  workingDir: this.workingDir
5335
5624
  }
@@ -5357,9 +5646,6 @@ Browser sessions (cookies, localStorage, login state) are automatically saved an
5357
5646
 
5358
5647
  ### Tabs and resources
5359
5648
  Each conversation gets its own browser tab sharing a single browser instance. Call \`browser_close\` when done to free the tab. If you don't close it, the tab stays open and the user can continue interacting with it.` : "";
5360
- const promptWithSkills = this.skillContextWindow ? `${systemPrompt}${developmentContext}
5361
-
5362
- ${this.skillContextWindow}${browserContext}` : `${systemPrompt}${developmentContext}${browserContext}`;
5363
5649
  const mainMemory = await memoryPromise;
5364
5650
  const boundedMainMemory = mainMemory && mainMemory.content.length > 4e3 ? `${mainMemory.content.slice(0, 4e3)}
5365
5651
  ...[truncated]` : mainMemory?.content;
@@ -5367,7 +5653,12 @@ ${this.skillContextWindow}${browserContext}` : `${systemPrompt}${developmentCont
5367
5653
  ## Persistent Memory
5368
5654
 
5369
5655
  ${boundedMainMemory.trim()}` : "";
5370
- const integrityPrompt = `${promptWithSkills}${memoryContext}
5656
+ const buildSystemPrompt = () => {
5657
+ const agentPrompt = renderCurrentAgentPrompt();
5658
+ const promptWithSkills = this.skillContextWindow ? `${agentPrompt}${developmentContext}
5659
+
5660
+ ${this.skillContextWindow}${browserContext}` : `${agentPrompt}${developmentContext}${browserContext}`;
5661
+ return `${promptWithSkills}${memoryContext}
5371
5662
 
5372
5663
  ## Execution Integrity
5373
5664
 
@@ -5375,6 +5666,10 @@ ${boundedMainMemory.trim()}` : "";
5375
5666
  - Do not fabricate "Tool Used" or "Tool Result" logs as plain text.
5376
5667
  - Never output faux execution transcripts, markdown tool logs, or "Tool Used/Result" sections.
5377
5668
  - If no suitable tool is available, explicitly say that and ask for guidance.`;
5669
+ };
5670
+ let integrityPrompt = buildSystemPrompt();
5671
+ let lastPromptFingerprint = `${this.agentFileFingerprint}
5672
+ ${this.skillFingerprint}`;
5378
5673
  const pushEvent = (event) => {
5379
5674
  events.push(event);
5380
5675
  return event;
@@ -5797,8 +6092,8 @@ ${textContent}` };
5797
6092
  let timer;
5798
6093
  nextPart = await Promise.race([
5799
6094
  fullStreamIterator.next(),
5800
- new Promise((resolve10) => {
5801
- timer = setTimeout(() => resolve10(null), timeout);
6095
+ new Promise((resolve12) => {
6096
+ timer = setTimeout(() => resolve12(null), timeout);
5802
6097
  })
5803
6098
  ]);
5804
6099
  clearTimeout(timer);
@@ -6106,6 +6401,19 @@ ${textContent}` };
6106
6401
  content: JSON.stringify(toolResultsForModel),
6107
6402
  metadata: toolMsgMeta
6108
6403
  });
6404
+ if (this.environment === "development") {
6405
+ const agentChanged = await this.refreshAgentIfChanged();
6406
+ const skillsChanged = await this.refreshSkillsIfChanged(true);
6407
+ if (agentChanged || skillsChanged) {
6408
+ agent = this.parsedAgent;
6409
+ const currentFingerprint = `${this.agentFileFingerprint}
6410
+ ${this.skillFingerprint}`;
6411
+ if (currentFingerprint !== lastPromptFingerprint) {
6412
+ integrityPrompt = buildSystemPrompt();
6413
+ lastPromptFingerprint = currentFingerprint;
6414
+ }
6415
+ }
6416
+ }
6109
6417
  yield pushEvent({
6110
6418
  type: "step:completed",
6111
6419
  step,
@@ -6294,8 +6602,8 @@ var LatitudeCapture = class {
6294
6602
 
6295
6603
  // src/state.ts
6296
6604
  import { randomUUID as randomUUID4 } from "crypto";
6297
- import { mkdir as mkdir4, readFile as readFile7, readdir as readdir4, rename as rename2, rm as rm3, writeFile as writeFile5 } from "fs/promises";
6298
- import { dirname as dirname4, resolve as resolve9 } from "path";
6605
+ import { mkdir as mkdir5, readFile as readFile9, readdir as readdir4, rename as rename3, rm as rm3, writeFile as writeFile6 } from "fs/promises";
6606
+ import { dirname as dirname5, resolve as resolve11 } from "path";
6299
6607
  var DEFAULT_OWNER = "local-owner";
6300
6608
  var LOCAL_STATE_FILE = "state.json";
6301
6609
  var CONVERSATIONS_DIRECTORY = "conversations";
@@ -6310,11 +6618,11 @@ var toStoreIdentity = async ({
6310
6618
  }
6311
6619
  return { name: ensured.name, id: agentId };
6312
6620
  };
6313
- var writeJsonAtomic2 = async (filePath, payload) => {
6314
- await mkdir4(dirname4(filePath), { recursive: true });
6621
+ var writeJsonAtomic3 = async (filePath, payload) => {
6622
+ await mkdir5(dirname5(filePath), { recursive: true });
6315
6623
  const tmpPath = `${filePath}.tmp`;
6316
- await writeFile5(tmpPath, JSON.stringify(payload, null, 2), "utf8");
6317
- await rename2(tmpPath, filePath);
6624
+ await writeFile6(tmpPath, JSON.stringify(payload, null, 2), "utf8");
6625
+ await rename3(tmpPath, filePath);
6318
6626
  };
6319
6627
  var formatUtcTimestamp = (value) => new Date(value).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
6320
6628
  var normalizeTitle = (title) => {
@@ -6488,8 +6796,8 @@ var FileConversationStore = class {
6488
6796
  agentId: this.agentId
6489
6797
  });
6490
6798
  const agentDir = getAgentStoreDirectory(identity);
6491
- const conversationsDir = resolve9(agentDir, CONVERSATIONS_DIRECTORY);
6492
- const indexPath = resolve9(conversationsDir, LOCAL_CONVERSATION_INDEX_FILE);
6799
+ const conversationsDir = resolve11(agentDir, CONVERSATIONS_DIRECTORY);
6800
+ const indexPath = resolve11(conversationsDir, LOCAL_CONVERSATION_INDEX_FILE);
6493
6801
  this.paths = { conversationsDir, indexPath };
6494
6802
  return this.paths;
6495
6803
  }
@@ -6499,13 +6807,13 @@ var FileConversationStore = class {
6499
6807
  schemaVersion: STORAGE_SCHEMA_VERSION,
6500
6808
  conversations: Array.from(this.conversations.values()).sort((a, b) => b.updatedAt - a.updatedAt)
6501
6809
  };
6502
- await writeJsonAtomic2(indexPath, payload);
6810
+ await writeJsonAtomic3(indexPath, payload);
6503
6811
  }
6504
6812
  async readConversationFile(fileName) {
6505
6813
  const { conversationsDir } = await this.resolvePaths();
6506
- const filePath = resolve9(conversationsDir, fileName);
6814
+ const filePath = resolve11(conversationsDir, fileName);
6507
6815
  try {
6508
- const raw = await readFile7(filePath, "utf8");
6816
+ const raw = await readFile9(filePath, "utf8");
6509
6817
  return JSON.parse(raw);
6510
6818
  } catch {
6511
6819
  return void 0;
@@ -6551,7 +6859,7 @@ var FileConversationStore = class {
6551
6859
  this.loaded = true;
6552
6860
  const { indexPath } = await this.resolvePaths();
6553
6861
  try {
6554
- const raw = await readFile7(indexPath, "utf8");
6862
+ const raw = await readFile9(indexPath, "utf8");
6555
6863
  const parsed = JSON.parse(raw);
6556
6864
  for (const conversation of parsed.conversations ?? []) {
6557
6865
  this.conversations.set(conversation.conversationId, conversation);
@@ -6574,9 +6882,9 @@ var FileConversationStore = class {
6574
6882
  const { conversationsDir } = await this.resolvePaths();
6575
6883
  const existing = this.conversations.get(conversation.conversationId);
6576
6884
  const fileName = existing?.fileName ?? this.resolveConversationFileName(conversation);
6577
- const filePath = resolve9(conversationsDir, fileName);
6885
+ const filePath = resolve11(conversationsDir, fileName);
6578
6886
  this.writing = this.writing.then(async () => {
6579
- await writeJsonAtomic2(filePath, conversation);
6887
+ await writeJsonAtomic3(filePath, conversation);
6580
6888
  this.conversations.set(conversation.conversationId, {
6581
6889
  conversationId: conversation.conversationId,
6582
6890
  title: conversation.title,
@@ -6672,7 +6980,7 @@ var FileConversationStore = class {
6672
6980
  if (removed) {
6673
6981
  this.writing = this.writing.then(async () => {
6674
6982
  if (existing) {
6675
- await rm3(resolve9(conversationsDir, existing.fileName), { force: true });
6983
+ await rm3(resolve11(conversationsDir, existing.fileName), { force: true });
6676
6984
  }
6677
6985
  await this.writeIndex();
6678
6986
  });
@@ -6702,7 +7010,7 @@ var FileStateStore = class {
6702
7010
  workingDir: this.workingDir,
6703
7011
  agentId: this.agentId
6704
7012
  });
6705
- this.filePath = resolve9(getAgentStoreDirectory(identity), LOCAL_STATE_FILE);
7013
+ this.filePath = resolve11(getAgentStoreDirectory(identity), LOCAL_STATE_FILE);
6706
7014
  }
6707
7015
  isExpired(state) {
6708
7016
  return typeof this.ttlMs === "number" && Date.now() - state.updatedAt > this.ttlMs;
@@ -6714,7 +7022,7 @@ var FileStateStore = class {
6714
7022
  }
6715
7023
  this.loaded = true;
6716
7024
  try {
6717
- const raw = await readFile7(this.filePath, "utf8");
7025
+ const raw = await readFile9(this.filePath, "utf8");
6718
7026
  const parsed = JSON.parse(raw);
6719
7027
  for (const state of parsed.states ?? []) {
6720
7028
  this.states.set(state.runId, state);
@@ -6727,7 +7035,7 @@ var FileStateStore = class {
6727
7035
  states: Array.from(this.states.values())
6728
7036
  };
6729
7037
  this.writing = this.writing.then(async () => {
6730
- await writeJsonAtomic2(this.filePath, payload);
7038
+ await writeJsonAtomic3(this.filePath, payload);
6731
7039
  });
6732
7040
  await this.writing;
6733
7041
  }
@@ -7441,7 +7749,7 @@ var TelemetryEmitter = class {
7441
7749
  };
7442
7750
 
7443
7751
  // src/index.ts
7444
- import { defineTool as defineTool5 } from "@poncho-ai/sdk";
7752
+ import { defineTool as defineTool6 } from "@poncho-ai/sdk";
7445
7753
  export {
7446
7754
  AgentHarness,
7447
7755
  InMemoryConversationStore,
@@ -7471,7 +7779,7 @@ export {
7471
7779
  createSubagentTools,
7472
7780
  createUploadStore,
7473
7781
  createWriteTool,
7474
- defineTool5 as defineTool,
7782
+ defineTool6 as defineTool,
7475
7783
  deriveUploadKey,
7476
7784
  ensureAgentIdentity,
7477
7785
  estimateTokens,