@poncho-ai/harness 0.2.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 ADDED
@@ -0,0 +1,3015 @@
1
+ // src/agent-parser.ts
2
+ import { randomUUID } from "crypto";
3
+ import { readFile } from "fs/promises";
4
+ import { resolve } from "path";
5
+ import Mustache from "mustache";
6
+ import YAML from "yaml";
7
+ var FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
8
+ var asRecord = (value) => typeof value === "object" && value !== null ? value : {};
9
+ var asNumberOrUndefined = (value) => typeof value === "number" ? value : void 0;
10
+ var parseAgentMarkdown = (content) => {
11
+ const match = content.match(FRONTMATTER_PATTERN);
12
+ if (!match) {
13
+ throw new Error(
14
+ "Invalid AGENT.md: expected YAML frontmatter wrapped in --- markers."
15
+ );
16
+ }
17
+ const parsedYaml = YAML.parse(match[1]) ?? {};
18
+ const parsed = asRecord(parsedYaml);
19
+ if (typeof parsed.name !== "string" || parsed.name.trim() === "") {
20
+ throw new Error("Invalid AGENT.md: frontmatter requires a non-empty `name`.");
21
+ }
22
+ const modelValue = asRecord(parsed.model);
23
+ const limitsValue = asRecord(parsed.limits);
24
+ const frontmatter = {
25
+ name: parsed.name,
26
+ description: typeof parsed.description === "string" ? parsed.description : void 0,
27
+ model: Object.keys(modelValue).length > 0 ? {
28
+ provider: typeof modelValue.provider === "string" ? modelValue.provider : "anthropic",
29
+ name: typeof modelValue.name === "string" ? modelValue.name : "claude-opus-4-5",
30
+ temperature: asNumberOrUndefined(modelValue.temperature),
31
+ maxTokens: asNumberOrUndefined(modelValue.maxTokens)
32
+ } : void 0,
33
+ limits: Object.keys(limitsValue).length > 0 ? {
34
+ maxSteps: asNumberOrUndefined(limitsValue.maxSteps),
35
+ timeout: asNumberOrUndefined(limitsValue.timeout)
36
+ } : void 0
37
+ };
38
+ return {
39
+ frontmatter,
40
+ body: match[2].trim()
41
+ };
42
+ };
43
+ var parseAgentFile = async (workingDir) => {
44
+ const filePath = resolve(workingDir, "AGENT.md");
45
+ const content = await readFile(filePath, "utf8");
46
+ return parseAgentMarkdown(content);
47
+ };
48
+ var renderAgentPrompt = (agent, context = {}) => {
49
+ const renderContext = {
50
+ name: agent.frontmatter.name,
51
+ description: agent.frontmatter.description ?? "",
52
+ runtime: {
53
+ workingDir: context.runtime?.workingDir ?? process.cwd(),
54
+ agentId: context.runtime?.agentId ?? agent.frontmatter.name,
55
+ runId: context.runtime?.runId ?? `run_${randomUUID()}`,
56
+ environment: context.runtime?.environment ?? "development"
57
+ },
58
+ parameters: context.parameters ?? {}
59
+ };
60
+ return Mustache.render(agent.body, renderContext).trim();
61
+ };
62
+
63
+ // src/config.ts
64
+ import { access } from "fs/promises";
65
+ import { resolve as resolve2 } from "path";
66
+ var resolveTtl = (ttl, key) => {
67
+ if (typeof ttl === "number") {
68
+ return ttl;
69
+ }
70
+ if (ttl && typeof ttl === "object" && typeof ttl[key] === "number") {
71
+ return ttl[key];
72
+ }
73
+ return void 0;
74
+ };
75
+ var resolveStateConfig = (config) => {
76
+ if (config?.storage) {
77
+ return {
78
+ provider: config.storage.provider,
79
+ url: config.storage.url,
80
+ token: config.storage.token,
81
+ table: config.storage.table,
82
+ region: config.storage.region,
83
+ ttl: resolveTtl(config.storage.ttl, "conversations")
84
+ };
85
+ }
86
+ return config?.state;
87
+ };
88
+ var resolveMemoryConfig = (config) => {
89
+ if (config?.storage) {
90
+ return {
91
+ enabled: config.storage.memory?.enabled ?? config.memory?.enabled,
92
+ provider: config.storage.provider,
93
+ url: config.storage.url,
94
+ token: config.storage.token,
95
+ table: config.storage.table,
96
+ region: config.storage.region,
97
+ ttl: resolveTtl(config.storage.ttl, "memory"),
98
+ maxRecallConversations: config.storage.memory?.maxRecallConversations ?? config.memory?.maxRecallConversations
99
+ };
100
+ }
101
+ return config?.memory;
102
+ };
103
+ var loadPonchoConfig = async (workingDir) => {
104
+ const filePath = resolve2(workingDir, "poncho.config.js");
105
+ try {
106
+ await access(filePath);
107
+ } catch {
108
+ return void 0;
109
+ }
110
+ const imported = await import(`${filePath}?t=${Date.now()}`);
111
+ return imported.default;
112
+ };
113
+
114
+ // src/default-tools.ts
115
+ import { mkdir, readdir, readFile as readFile2, writeFile } from "fs/promises";
116
+ import { dirname, resolve as resolve3, sep } from "path";
117
+ import { defineTool } from "@poncho-ai/sdk";
118
+ var resolveSafePath = (workingDir, inputPath) => {
119
+ const base = resolve3(workingDir);
120
+ const target = resolve3(base, inputPath);
121
+ if (target === base || target.startsWith(`${base}${sep}`)) {
122
+ return target;
123
+ }
124
+ throw new Error("Access denied: path must stay inside the working directory.");
125
+ };
126
+ var createDefaultTools = (workingDir) => [
127
+ defineTool({
128
+ name: "list_directory",
129
+ description: "List files and folders at a path",
130
+ inputSchema: {
131
+ type: "object",
132
+ properties: {
133
+ path: {
134
+ type: "string",
135
+ description: "Directory path relative to working directory"
136
+ }
137
+ },
138
+ required: ["path"],
139
+ additionalProperties: false
140
+ },
141
+ handler: async (input) => {
142
+ const path = typeof input.path === "string" ? input.path : ".";
143
+ const resolved = resolveSafePath(workingDir, path);
144
+ const entries = await readdir(resolved, { withFileTypes: true });
145
+ return entries.map((entry) => ({
146
+ name: entry.name,
147
+ type: entry.isDirectory() ? "directory" : "file"
148
+ }));
149
+ }
150
+ }),
151
+ defineTool({
152
+ name: "read_file",
153
+ description: "Read UTF-8 text file contents",
154
+ inputSchema: {
155
+ type: "object",
156
+ properties: {
157
+ path: {
158
+ type: "string",
159
+ description: "File path relative to working directory"
160
+ }
161
+ },
162
+ required: ["path"],
163
+ additionalProperties: false
164
+ },
165
+ handler: async (input) => {
166
+ const path = typeof input.path === "string" ? input.path : "";
167
+ const resolved = resolveSafePath(workingDir, path);
168
+ const content = await readFile2(resolved, "utf8");
169
+ return { path, content };
170
+ }
171
+ })
172
+ ];
173
+ var createWriteTool = (workingDir) => defineTool({
174
+ name: "write_file",
175
+ description: "Write UTF-8 text file contents (create or overwrite)",
176
+ inputSchema: {
177
+ type: "object",
178
+ properties: {
179
+ path: {
180
+ type: "string",
181
+ description: "File path relative to working directory"
182
+ },
183
+ content: {
184
+ type: "string",
185
+ description: "Text content to write"
186
+ }
187
+ },
188
+ required: ["path", "content"],
189
+ additionalProperties: false
190
+ },
191
+ handler: async (input) => {
192
+ const path = typeof input.path === "string" ? input.path : "";
193
+ const content = typeof input.content === "string" ? input.content : "";
194
+ const resolved = resolveSafePath(workingDir, path);
195
+ await mkdir(dirname(resolved), { recursive: true });
196
+ await writeFile(resolved, content, "utf8");
197
+ return { path, written: true };
198
+ }
199
+ });
200
+
201
+ // src/harness.ts
202
+ import { randomUUID as randomUUID2 } from "crypto";
203
+
204
+ // src/latitude-capture.ts
205
+ var sanitizePath = (value) => value.trim().replace(/[^a-zA-Z0-9\-_/\.]/g, "-").replace(/-+/g, "-");
206
+ var LatitudeCapture = class {
207
+ apiKey;
208
+ telemetryPromise;
209
+ projectId;
210
+ path;
211
+ constructor(config) {
212
+ this.apiKey = config?.apiKey ?? process.env.LATITUDE_API_KEY;
213
+ if (!this.apiKey) {
214
+ return;
215
+ }
216
+ const rawProjectId = config?.projectId ?? process.env.LATITUDE_PROJECT_ID;
217
+ const projectIdNumber = typeof rawProjectId === "number" ? rawProjectId : rawProjectId ? Number.parseInt(rawProjectId, 10) : Number.NaN;
218
+ this.projectId = Number.isFinite(projectIdNumber) ? projectIdNumber : void 0;
219
+ const rawPath = config?.path ?? process.env.LATITUDE_PATH ?? process.env.LATITUDE_DOCUMENT_PATH ?? config?.defaultPath;
220
+ this.path = rawPath ? sanitizePath(rawPath) : void 0;
221
+ }
222
+ async initializeTelemetry() {
223
+ if (!this.apiKey) {
224
+ return void 0;
225
+ }
226
+ try {
227
+ const [{ LatitudeTelemetry }, AnthropicSdk, { default: OpenAI2 }] = await Promise.all([
228
+ import("@latitude-data/telemetry"),
229
+ import("@anthropic-ai/sdk"),
230
+ import("openai")
231
+ ]);
232
+ const disableAnthropicInstrumentation = process.env.LATITUDE_DISABLE_ANTHROPIC_INSTRUMENTATION === "true";
233
+ return new LatitudeTelemetry(this.apiKey, {
234
+ instrumentations: {
235
+ ...disableAnthropicInstrumentation ? {} : { anthropic: AnthropicSdk },
236
+ openai: OpenAI2
237
+ }
238
+ });
239
+ } catch {
240
+ return void 0;
241
+ }
242
+ }
243
+ async capture(fn) {
244
+ if (!this.apiKey || !this.projectId || !this.path) {
245
+ return await fn();
246
+ }
247
+ if (!this.telemetryPromise) {
248
+ this.telemetryPromise = this.initializeTelemetry();
249
+ }
250
+ const telemetry = await this.telemetryPromise;
251
+ if (!telemetry) {
252
+ return await fn();
253
+ }
254
+ try {
255
+ return await telemetry.capture(
256
+ {
257
+ projectId: this.projectId,
258
+ path: this.path
259
+ },
260
+ fn
261
+ );
262
+ } catch {
263
+ return await fn();
264
+ }
265
+ }
266
+ };
267
+
268
+ // src/memory.ts
269
+ import { createHash } from "crypto";
270
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
271
+ import { basename, dirname as dirname2, resolve as resolve4 } from "path";
272
+ import { homedir } from "os";
273
+ import { defineTool as defineTool2 } from "@poncho-ai/sdk";
274
+ var DEFAULT_MAIN_MEMORY = {
275
+ content: "",
276
+ updatedAt: 0
277
+ };
278
+ var LOCAL_MEMORY_FILE = "local-memory.json";
279
+ var getStateDirectory = () => {
280
+ const cwd = process.cwd();
281
+ const home = homedir();
282
+ const isServerless = process.env.VERCEL === "1" || process.env.VERCEL_ENV !== void 0 || process.env.VERCEL_URL !== void 0 || process.env.AWS_LAMBDA_FUNCTION_NAME !== void 0 || process.env.AWS_EXECUTION_ENV?.includes("AWS_Lambda") === true || process.env.LAMBDA_TASK_ROOT !== void 0 || process.env.NOW_REGION !== void 0 || cwd.startsWith("/var/task") || home.startsWith("/var/task") || process.env.SERVERLESS === "1";
283
+ if (isServerless) {
284
+ return "/tmp/.poncho/state";
285
+ }
286
+ return resolve4(homedir(), ".poncho", "state");
287
+ };
288
+ var projectScopedMemoryPath = (workingDir) => {
289
+ const projectName = basename(workingDir).replace(/[^a-zA-Z0-9_-]+/g, "-") || "project";
290
+ const projectHash = createHash("sha256").update(workingDir).digest("hex").slice(0, 12);
291
+ return resolve4(getStateDirectory(), `${projectName}-${projectHash}-${LOCAL_MEMORY_FILE}`);
292
+ };
293
+ var scoreText = (text, query) => {
294
+ const normalized = query.trim().toLowerCase();
295
+ const tokens = normalized.split(/\s+/).filter(Boolean);
296
+ if (tokens.length === 0) {
297
+ return 0;
298
+ }
299
+ const haystack = text.toLowerCase();
300
+ let score = haystack.includes(normalized) ? 5 : 0;
301
+ for (const token of tokens) {
302
+ if (haystack.includes(token)) {
303
+ score += 1;
304
+ }
305
+ }
306
+ return score;
307
+ };
308
+ var InMemoryMemoryStore = class {
309
+ mainMemory = { ...DEFAULT_MAIN_MEMORY };
310
+ ttlMs;
311
+ constructor(ttlSeconds) {
312
+ this.ttlMs = typeof ttlSeconds === "number" ? ttlSeconds * 1e3 : void 0;
313
+ }
314
+ isExpired(updatedAt) {
315
+ return typeof this.ttlMs === "number" && Date.now() - updatedAt > this.ttlMs;
316
+ }
317
+ async getMainMemory() {
318
+ if (this.mainMemory.updatedAt > 0 && this.isExpired(this.mainMemory.updatedAt)) {
319
+ this.mainMemory = { ...DEFAULT_MAIN_MEMORY };
320
+ }
321
+ return this.mainMemory;
322
+ }
323
+ async updateMainMemory(input) {
324
+ const now2 = Date.now();
325
+ const existing = await this.getMainMemory();
326
+ const nextContent = input.mode === "append" && existing.content ? `${existing.content}
327
+
328
+ ${input.content}`.trim() : input.content;
329
+ this.mainMemory = {
330
+ content: nextContent.trim(),
331
+ updatedAt: now2
332
+ };
333
+ return this.mainMemory;
334
+ }
335
+ };
336
+ var FileMainMemoryStore = class {
337
+ filePath;
338
+ ttlMs;
339
+ loaded = false;
340
+ writing = Promise.resolve();
341
+ mainMemory = { ...DEFAULT_MAIN_MEMORY };
342
+ constructor(workingDir, ttlSeconds) {
343
+ this.filePath = projectScopedMemoryPath(workingDir);
344
+ this.ttlMs = typeof ttlSeconds === "number" ? ttlSeconds * 1e3 : void 0;
345
+ }
346
+ isExpired(updatedAt) {
347
+ return typeof this.ttlMs === "number" && Date.now() - updatedAt > this.ttlMs;
348
+ }
349
+ async ensureLoaded() {
350
+ if (this.loaded) {
351
+ return;
352
+ }
353
+ this.loaded = true;
354
+ try {
355
+ const raw = await readFile3(this.filePath, "utf8");
356
+ const parsed = JSON.parse(raw);
357
+ const content = typeof parsed.main?.content === "string" ? parsed.main.content : "";
358
+ const updatedAt = typeof parsed.main?.updatedAt === "number" ? parsed.main.updatedAt : 0;
359
+ this.mainMemory = { content, updatedAt };
360
+ } catch {
361
+ }
362
+ }
363
+ async persist() {
364
+ const payload = { main: this.mainMemory };
365
+ this.writing = this.writing.then(async () => {
366
+ await mkdir2(dirname2(this.filePath), { recursive: true });
367
+ await writeFile2(this.filePath, JSON.stringify(payload, null, 2), "utf8");
368
+ });
369
+ await this.writing;
370
+ }
371
+ async getMainMemory() {
372
+ await this.ensureLoaded();
373
+ if (this.mainMemory.updatedAt > 0 && this.isExpired(this.mainMemory.updatedAt)) {
374
+ this.mainMemory = { ...DEFAULT_MAIN_MEMORY };
375
+ await this.persist();
376
+ }
377
+ return this.mainMemory;
378
+ }
379
+ async updateMainMemory(input) {
380
+ await this.ensureLoaded();
381
+ const existing = await this.getMainMemory();
382
+ const nextContent = input.mode === "append" && existing.content ? `${existing.content}
383
+
384
+ ${input.content}`.trim() : input.content;
385
+ this.mainMemory = {
386
+ content: nextContent.trim(),
387
+ updatedAt: Date.now()
388
+ };
389
+ await this.persist();
390
+ return this.mainMemory;
391
+ }
392
+ };
393
+ var KeyValueMainMemoryStoreBase = class {
394
+ ttl;
395
+ memoryFallback;
396
+ constructor(ttl) {
397
+ this.ttl = ttl;
398
+ this.memoryFallback = new InMemoryMemoryStore(ttl);
399
+ }
400
+ async readPayload(key) {
401
+ try {
402
+ const raw = await this.getRaw(key);
403
+ if (!raw) {
404
+ return { main: { ...DEFAULT_MAIN_MEMORY } };
405
+ }
406
+ const parsed = JSON.parse(raw);
407
+ const content = typeof parsed.main?.content === "string" ? parsed.main.content : "";
408
+ const updatedAt = typeof parsed.main?.updatedAt === "number" ? parsed.main.updatedAt : 0;
409
+ return { main: { content, updatedAt } };
410
+ } catch {
411
+ const main = await this.memoryFallback.getMainMemory();
412
+ return { main };
413
+ }
414
+ }
415
+ async writePayload(key, payload) {
416
+ try {
417
+ const serialized = JSON.stringify(payload);
418
+ if (typeof this.ttl === "number") {
419
+ await this.setRawWithTtl(key, serialized, Math.max(1, this.ttl));
420
+ } else {
421
+ await this.setRaw(key, serialized);
422
+ }
423
+ } catch {
424
+ await this.memoryFallback.updateMainMemory({
425
+ content: payload.main.content,
426
+ mode: "replace"
427
+ });
428
+ }
429
+ }
430
+ async getMainMemory() {
431
+ const payload = await this.readPayload(this.key());
432
+ return payload.main;
433
+ }
434
+ async updateMainMemory(input) {
435
+ const key = this.key();
436
+ const payload = await this.readPayload(key);
437
+ const nextContent = input.mode === "append" && payload.main.content ? `${payload.main.content}
438
+
439
+ ${input.content}`.trim() : input.content;
440
+ payload.main = {
441
+ content: nextContent.trim(),
442
+ updatedAt: Date.now()
443
+ };
444
+ await this.writePayload(key, payload);
445
+ return payload.main;
446
+ }
447
+ };
448
+ var UpstashMemoryStore = class extends KeyValueMainMemoryStoreBase {
449
+ baseUrl;
450
+ token;
451
+ storageKey;
452
+ constructor(options) {
453
+ super(options.ttl);
454
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
455
+ this.token = options.token;
456
+ this.storageKey = options.storageKey;
457
+ }
458
+ key() {
459
+ return this.storageKey;
460
+ }
461
+ headers() {
462
+ return {
463
+ Authorization: `Bearer ${this.token}`,
464
+ "Content-Type": "application/json"
465
+ };
466
+ }
467
+ async getRaw(key) {
468
+ const response = await fetch(`${this.baseUrl}/get/${encodeURIComponent(key)}`, {
469
+ method: "POST",
470
+ headers: this.headers()
471
+ });
472
+ if (!response.ok) {
473
+ return void 0;
474
+ }
475
+ const payload = await response.json();
476
+ return payload.result ?? void 0;
477
+ }
478
+ async setRaw(key, value) {
479
+ await fetch(
480
+ `${this.baseUrl}/set/${encodeURIComponent(key)}/${encodeURIComponent(value)}`,
481
+ { method: "POST", headers: this.headers() }
482
+ );
483
+ }
484
+ async setRawWithTtl(key, value, ttl) {
485
+ await fetch(
486
+ `${this.baseUrl}/setex/${encodeURIComponent(key)}/${Math.max(1, ttl)}/${encodeURIComponent(
487
+ value
488
+ )}`,
489
+ { method: "POST", headers: this.headers() }
490
+ );
491
+ }
492
+ };
493
+ var RedisMemoryStore = class extends KeyValueMainMemoryStoreBase {
494
+ storageKey;
495
+ clientPromise;
496
+ constructor(options) {
497
+ super(options.ttl);
498
+ this.storageKey = options.storageKey;
499
+ this.clientPromise = (async () => {
500
+ try {
501
+ const redisModule = await import("redis");
502
+ const client = redisModule.createClient({ url: options.url });
503
+ await client.connect();
504
+ return client;
505
+ } catch {
506
+ return void 0;
507
+ }
508
+ })();
509
+ }
510
+ key() {
511
+ return this.storageKey;
512
+ }
513
+ async getRaw(key) {
514
+ const client = await this.clientPromise;
515
+ if (!client) {
516
+ throw new Error("Redis unavailable");
517
+ }
518
+ const value = await client.get(key);
519
+ return value ?? void 0;
520
+ }
521
+ async setRaw(key, value) {
522
+ const client = await this.clientPromise;
523
+ if (!client) {
524
+ throw new Error("Redis unavailable");
525
+ }
526
+ await client.set(key, value);
527
+ }
528
+ async setRawWithTtl(key, value, ttl) {
529
+ const client = await this.clientPromise;
530
+ if (!client) {
531
+ throw new Error("Redis unavailable");
532
+ }
533
+ await client.set(key, value, { EX: Math.max(1, ttl) });
534
+ }
535
+ };
536
+ var DynamoDbMemoryStore = class extends KeyValueMainMemoryStoreBase {
537
+ storageKey;
538
+ table;
539
+ clientPromise;
540
+ constructor(options) {
541
+ super(options.ttl);
542
+ this.storageKey = options.storageKey;
543
+ this.table = options.table;
544
+ this.clientPromise = (async () => {
545
+ try {
546
+ const module = await import("@aws-sdk/client-dynamodb");
547
+ const client = new module.DynamoDBClient({ region: options.region });
548
+ return {
549
+ send: client.send.bind(client),
550
+ GetItemCommand: module.GetItemCommand,
551
+ PutItemCommand: module.PutItemCommand
552
+ };
553
+ } catch {
554
+ return void 0;
555
+ }
556
+ })();
557
+ }
558
+ key() {
559
+ return this.storageKey;
560
+ }
561
+ async getRaw(key) {
562
+ const client = await this.clientPromise;
563
+ if (!client) {
564
+ throw new Error("DynamoDB unavailable");
565
+ }
566
+ const result = await client.send(
567
+ new client.GetItemCommand({
568
+ TableName: this.table,
569
+ Key: { runId: { S: key } }
570
+ })
571
+ );
572
+ return result.Item?.value?.S;
573
+ }
574
+ async setRaw(key, value) {
575
+ const client = await this.clientPromise;
576
+ if (!client) {
577
+ throw new Error("DynamoDB unavailable");
578
+ }
579
+ await client.send(
580
+ new client.PutItemCommand({
581
+ TableName: this.table,
582
+ Item: {
583
+ runId: { S: key },
584
+ value: { S: value }
585
+ }
586
+ })
587
+ );
588
+ }
589
+ async setRawWithTtl(key, value, ttl) {
590
+ const client = await this.clientPromise;
591
+ if (!client) {
592
+ throw new Error("DynamoDB unavailable");
593
+ }
594
+ const ttlEpoch = Math.floor(Date.now() / 1e3) + Math.max(1, ttl);
595
+ await client.send(
596
+ new client.PutItemCommand({
597
+ TableName: this.table,
598
+ Item: {
599
+ runId: { S: key },
600
+ value: { S: value },
601
+ ttl: { N: String(ttlEpoch) }
602
+ }
603
+ })
604
+ );
605
+ }
606
+ };
607
+ var createMemoryStore = (agentId, config, options) => {
608
+ const provider = config?.provider ?? "local";
609
+ const ttl = config?.ttl;
610
+ const storageKey = `poncho:memory:${agentId}:main`;
611
+ const workingDir = options?.workingDir ?? process.cwd();
612
+ if (provider === "local") {
613
+ return new FileMainMemoryStore(workingDir, ttl);
614
+ }
615
+ if (provider === "memory") {
616
+ return new InMemoryMemoryStore(ttl);
617
+ }
618
+ if (provider === "upstash") {
619
+ const url = config?.url ?? process.env.UPSTASH_REDIS_REST_URL ?? process.env.KV_REST_API_URL ?? "";
620
+ const token = config?.token ?? process.env.UPSTASH_REDIS_REST_TOKEN ?? process.env.KV_REST_API_TOKEN ?? "";
621
+ if (url && token) {
622
+ return new UpstashMemoryStore({
623
+ baseUrl: url,
624
+ token,
625
+ storageKey,
626
+ ttl
627
+ });
628
+ }
629
+ return new InMemoryMemoryStore(ttl);
630
+ }
631
+ if (provider === "redis") {
632
+ const url = config?.url ?? process.env.REDIS_URL ?? "";
633
+ if (url) {
634
+ return new RedisMemoryStore({
635
+ url,
636
+ storageKey,
637
+ ttl
638
+ });
639
+ }
640
+ return new InMemoryMemoryStore(ttl);
641
+ }
642
+ if (provider === "dynamodb") {
643
+ const table = config?.table ?? process.env.PONCHO_DYNAMODB_TABLE ?? "";
644
+ if (table) {
645
+ return new DynamoDbMemoryStore({
646
+ table,
647
+ storageKey,
648
+ region: config?.region,
649
+ ttl
650
+ });
651
+ }
652
+ return new InMemoryMemoryStore(ttl);
653
+ }
654
+ return new InMemoryMemoryStore(ttl);
655
+ };
656
+ var asRecallCorpus = (raw) => {
657
+ if (!Array.isArray(raw)) {
658
+ return [];
659
+ }
660
+ return raw.map((item) => {
661
+ if (!item || typeof item !== "object") {
662
+ return void 0;
663
+ }
664
+ const record = item;
665
+ const conversationId = typeof record.conversationId === "string" ? record.conversationId : "";
666
+ const title = typeof record.title === "string" ? record.title : "Conversation";
667
+ const updatedAt = typeof record.updatedAt === "number" ? record.updatedAt : 0;
668
+ const content = typeof record.content === "string" ? record.content : "";
669
+ if (!conversationId || !content) {
670
+ return void 0;
671
+ }
672
+ return { conversationId, title, updatedAt, content };
673
+ }).filter((item) => Boolean(item));
674
+ };
675
+ var buildRecallSnippet = (content, query, maxChars = 360) => {
676
+ const normalized = query.trim().toLowerCase();
677
+ const index = content.toLowerCase().indexOf(normalized);
678
+ if (index === -1) {
679
+ return content.slice(0, maxChars);
680
+ }
681
+ const start = Math.max(0, index - 120);
682
+ const end = Math.min(content.length, index + normalized.length + 180);
683
+ return content.slice(start, end);
684
+ };
685
+ var createMemoryTools = (store, options) => {
686
+ const maxRecallConversations = Math.max(1, options?.maxRecallConversations ?? 20);
687
+ return [
688
+ defineTool2({
689
+ name: "memory_main_get",
690
+ description: "Get the current persistent main memory document.",
691
+ inputSchema: {
692
+ type: "object",
693
+ properties: {},
694
+ additionalProperties: false
695
+ },
696
+ handler: async () => {
697
+ const memory = await store.getMainMemory();
698
+ return { memory };
699
+ }
700
+ }),
701
+ defineTool2({
702
+ name: "memory_main_update",
703
+ 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.",
704
+ inputSchema: {
705
+ type: "object",
706
+ properties: {
707
+ mode: {
708
+ type: "string",
709
+ enum: ["replace", "append"],
710
+ description: "replace overwrites memory; append adds content to the end"
711
+ },
712
+ content: {
713
+ type: "string",
714
+ description: "The memory content to write"
715
+ }
716
+ },
717
+ required: ["content"],
718
+ additionalProperties: false
719
+ },
720
+ handler: async (input) => {
721
+ const content = typeof input.content === "string" ? input.content.trim() : "";
722
+ if (!content) {
723
+ throw new Error("content is required");
724
+ }
725
+ const mode = input.mode === "append" || input.mode === "replace" ? input.mode : "replace";
726
+ const memory = await store.updateMainMemory({ content, mode });
727
+ return { ok: true, memory };
728
+ }
729
+ }),
730
+ defineTool2({
731
+ name: "conversation_recall",
732
+ description: "Recall relevant snippets from previous conversations when prior context is likely important (for example: 'as we discussed', 'last time', or ambiguous references).",
733
+ inputSchema: {
734
+ type: "object",
735
+ properties: {
736
+ query: {
737
+ type: "string",
738
+ description: "Search query for past conversation recall"
739
+ },
740
+ limit: {
741
+ type: "number",
742
+ description: "Maximum snippets to return"
743
+ },
744
+ excludeConversationId: {
745
+ type: "string",
746
+ description: "Optional conversation id to exclude from recall"
747
+ }
748
+ },
749
+ required: ["query"],
750
+ additionalProperties: false
751
+ },
752
+ handler: async (input, context) => {
753
+ const query = typeof input.query === "string" ? input.query.trim() : "";
754
+ if (!query) {
755
+ throw new Error("query is required");
756
+ }
757
+ const limit = Math.max(
758
+ 1,
759
+ Math.min(5, typeof input.limit === "number" ? input.limit : 3)
760
+ );
761
+ const excludeConversationId = typeof input.excludeConversationId === "string" ? input.excludeConversationId : "";
762
+ const corpus = asRecallCorpus(context.parameters.__conversationRecallCorpus).slice(
763
+ 0,
764
+ maxRecallConversations
765
+ );
766
+ const results = corpus.filter(
767
+ (item) => excludeConversationId ? item.conversationId !== excludeConversationId : true
768
+ ).map((item) => ({
769
+ ...item,
770
+ score: scoreText(`${item.title}
771
+ ${item.content}`, query)
772
+ })).filter((item) => item.score > 0).sort((a, b) => {
773
+ if (b.score === a.score) {
774
+ return b.updatedAt - a.updatedAt;
775
+ }
776
+ return b.score - a.score;
777
+ }).slice(0, limit).map((item) => ({
778
+ conversationId: item.conversationId,
779
+ title: item.title,
780
+ updatedAt: item.updatedAt,
781
+ snippet: buildRecallSnippet(item.content, query)
782
+ }));
783
+ return { results };
784
+ }
785
+ })
786
+ ];
787
+ };
788
+
789
+ // src/mcp.ts
790
+ import { WebSocket } from "ws";
791
+ var WebSocketMcpRpcClient = class {
792
+ ws;
793
+ url;
794
+ timeoutMs;
795
+ reconnectAttempts;
796
+ reconnectDelayMs;
797
+ idCounter = 1;
798
+ pending = /* @__PURE__ */ new Map();
799
+ opened = false;
800
+ constructor(url, timeoutMs = 1e4, reconnectAttempts = 3, reconnectDelayMs = 500) {
801
+ this.url = url;
802
+ this.timeoutMs = timeoutMs;
803
+ this.reconnectAttempts = reconnectAttempts;
804
+ this.reconnectDelayMs = reconnectDelayMs;
805
+ }
806
+ attachHandlers(ws) {
807
+ ws.on("open", () => {
808
+ this.opened = true;
809
+ });
810
+ ws.on("message", (data) => {
811
+ try {
812
+ const message = JSON.parse(String(data));
813
+ if (typeof message.id !== "number") {
814
+ return;
815
+ }
816
+ const pending = this.pending.get(message.id);
817
+ if (!pending) {
818
+ return;
819
+ }
820
+ clearTimeout(pending.timer);
821
+ this.pending.delete(message.id);
822
+ if (message.error) {
823
+ pending.reject(new Error(message.error.message ?? "MCP RPC error"));
824
+ } else {
825
+ pending.resolve(message.result);
826
+ }
827
+ } catch {
828
+ }
829
+ });
830
+ ws.on("close", () => {
831
+ this.opened = false;
832
+ for (const [, pending] of this.pending) {
833
+ clearTimeout(pending.timer);
834
+ pending.reject(new Error("MCP websocket closed"));
835
+ }
836
+ this.pending.clear();
837
+ });
838
+ }
839
+ async openSocket() {
840
+ const ws = new WebSocket(this.url);
841
+ this.ws = ws;
842
+ this.attachHandlers(ws);
843
+ await new Promise((resolvePromise, rejectPromise) => {
844
+ const timeout = setTimeout(() => {
845
+ rejectPromise(new Error("MCP websocket open timeout"));
846
+ }, this.timeoutMs);
847
+ ws.once("open", () => {
848
+ clearTimeout(timeout);
849
+ resolvePromise();
850
+ });
851
+ ws.once("error", (error) => {
852
+ clearTimeout(timeout);
853
+ rejectPromise(error);
854
+ });
855
+ });
856
+ }
857
+ async waitUntilOpen() {
858
+ if (this.opened && this.ws && this.ws.readyState === WebSocket.OPEN) {
859
+ return;
860
+ }
861
+ let lastError;
862
+ for (let attempt = 1; attempt <= this.reconnectAttempts; attempt += 1) {
863
+ try {
864
+ await this.openSocket();
865
+ return;
866
+ } catch (error) {
867
+ lastError = error;
868
+ await new Promise(
869
+ (resolvePromise) => setTimeout(resolvePromise, this.reconnectDelayMs * attempt)
870
+ );
871
+ }
872
+ }
873
+ throw lastError instanceof Error ? lastError : new Error("Unable to connect to remote MCP websocket");
874
+ }
875
+ async request(method, params) {
876
+ await this.waitUntilOpen();
877
+ const id = this.idCounter++;
878
+ const payload = JSON.stringify({
879
+ jsonrpc: "2.0",
880
+ id,
881
+ method,
882
+ params: params ?? {}
883
+ });
884
+ const resultPromise = new Promise((resolvePromise, rejectPromise) => {
885
+ const timer = setTimeout(() => {
886
+ this.pending.delete(id);
887
+ rejectPromise(new Error(`MCP websocket timeout for method ${method}`));
888
+ }, this.timeoutMs);
889
+ this.pending.set(id, { resolve: resolvePromise, reject: rejectPromise, timer });
890
+ });
891
+ const socket = this.ws;
892
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
893
+ throw new Error("MCP websocket is not connected");
894
+ }
895
+ socket.send(payload);
896
+ return await resultPromise;
897
+ }
898
+ async listTools() {
899
+ const result = await this.request("tools/list");
900
+ const value = result?.tools;
901
+ return Array.isArray(value) ? value : [];
902
+ }
903
+ async callTool(name, input) {
904
+ const result = await this.request("tools/call", { name, arguments: input });
905
+ return result?.result ?? result?.content ?? result;
906
+ }
907
+ async close() {
908
+ this.ws?.close();
909
+ this.ws = void 0;
910
+ this.opened = false;
911
+ }
912
+ };
913
+ var LocalMcpBridge = class {
914
+ remoteServers;
915
+ rpcClients = /* @__PURE__ */ new Map();
916
+ constructor(config) {
917
+ this.remoteServers = (config?.mcp ?? []).filter(
918
+ (entry) => typeof entry.url === "string"
919
+ );
920
+ }
921
+ async loadTools() {
922
+ const tools = [];
923
+ for (const remoteServer of this.remoteServers) {
924
+ const name = remoteServer.name ?? remoteServer.url;
925
+ const client = this.rpcClients.get(name);
926
+ if (!client) {
927
+ continue;
928
+ }
929
+ try {
930
+ const discovered = await client.listTools();
931
+ tools.push(...this.toToolDefinitions(name, discovered, client));
932
+ } catch {
933
+ }
934
+ }
935
+ return tools;
936
+ }
937
+ async startLocalServers() {
938
+ for (const server of this.remoteServers) {
939
+ const name = server.name ?? server.url;
940
+ this.rpcClients.set(
941
+ name,
942
+ new WebSocketMcpRpcClient(
943
+ server.url,
944
+ server.timeoutMs ?? 1e4,
945
+ server.reconnectAttempts ?? 3,
946
+ server.reconnectDelayMs ?? 500
947
+ )
948
+ );
949
+ }
950
+ }
951
+ async stopLocalServers() {
952
+ for (const [, client] of this.rpcClients) {
953
+ await client.close();
954
+ }
955
+ this.rpcClients.clear();
956
+ }
957
+ listServers() {
958
+ return [...this.remoteServers];
959
+ }
960
+ listRemoteServers() {
961
+ return this.remoteServers;
962
+ }
963
+ async checkRemoteConnectivity() {
964
+ const checks = [];
965
+ for (const remote of this.remoteServers) {
966
+ try {
967
+ if (remote.url.startsWith("http://") || remote.url.startsWith("https://")) {
968
+ const response = await fetch(remote.url, { method: "HEAD" });
969
+ checks.push({ url: remote.url, ok: response.ok });
970
+ } else {
971
+ checks.push({ url: remote.url, ok: true });
972
+ }
973
+ } catch (error) {
974
+ checks.push({
975
+ url: remote.url,
976
+ ok: false,
977
+ error: error instanceof Error ? error.message : "Unknown connectivity error"
978
+ });
979
+ }
980
+ }
981
+ return checks;
982
+ }
983
+ toSerializableConfig() {
984
+ return { mcp: [...this.remoteServers] };
985
+ }
986
+ getLocalServers() {
987
+ return [];
988
+ }
989
+ toToolDefinitions(serverName, tools, client) {
990
+ return tools.map((tool) => ({
991
+ name: `${serverName}:${tool.name}`,
992
+ description: tool.description ?? `MCP tool ${tool.name} from ${serverName}`,
993
+ inputSchema: tool.inputSchema ?? {
994
+ type: "object",
995
+ properties: {}
996
+ },
997
+ handler: async (input) => await client.callTool(tool.name, input)
998
+ }));
999
+ }
1000
+ };
1001
+
1002
+ // src/anthropic-client.ts
1003
+ import Anthropic from "@anthropic-ai/sdk";
1004
+ var toAnthropicMessages = (messages) => messages.flatMap((message) => {
1005
+ if (message.role === "system") {
1006
+ return [];
1007
+ }
1008
+ if (message.role === "tool") {
1009
+ return [{ role: "user", content: message.content }];
1010
+ }
1011
+ return [{ role: message.role, content: message.content }];
1012
+ });
1013
+ var AnthropicModelClient = class {
1014
+ client;
1015
+ latitudeCapture;
1016
+ constructor(apiKey, options) {
1017
+ this.client = new Anthropic({
1018
+ apiKey: apiKey ?? process.env.ANTHROPIC_API_KEY
1019
+ });
1020
+ this.latitudeCapture = options?.latitudeCapture;
1021
+ }
1022
+ async *generateStream(input) {
1023
+ let stream;
1024
+ try {
1025
+ stream = await (this.latitudeCapture?.capture(
1026
+ async () => this.client.messages.stream({
1027
+ model: input.modelName,
1028
+ max_tokens: input.maxTokens ?? 1024,
1029
+ temperature: input.temperature ?? 0.2,
1030
+ system: input.systemPrompt,
1031
+ messages: toAnthropicMessages(input.messages),
1032
+ tools: input.tools.map((tool) => ({
1033
+ name: tool.name,
1034
+ description: tool.description,
1035
+ input_schema: tool.inputSchema
1036
+ }))
1037
+ })
1038
+ ) ?? this.client.messages.stream({
1039
+ model: input.modelName,
1040
+ max_tokens: input.maxTokens ?? 1024,
1041
+ temperature: input.temperature ?? 0.2,
1042
+ system: input.systemPrompt,
1043
+ messages: toAnthropicMessages(input.messages),
1044
+ tools: input.tools.map((tool) => ({
1045
+ name: tool.name,
1046
+ description: tool.description,
1047
+ input_schema: tool.inputSchema
1048
+ }))
1049
+ }));
1050
+ } catch (error) {
1051
+ const maybeStatus = error.status;
1052
+ if (maybeStatus === 404) {
1053
+ throw new Error(
1054
+ `Anthropic model not found: ${input.modelName}. Update AGENT.md frontmatter model.name to a valid model (for example: claude-opus-4-5).`
1055
+ );
1056
+ }
1057
+ throw error;
1058
+ }
1059
+ let text = "";
1060
+ for await (const event of stream) {
1061
+ if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && typeof event.delta.text === "string" && event.delta.text.length > 0) {
1062
+ text += event.delta.text;
1063
+ yield { type: "chunk", content: event.delta.text };
1064
+ }
1065
+ }
1066
+ const response = await stream.finalMessage();
1067
+ const toolCalls = [];
1068
+ for (const block of response.content) {
1069
+ if (block.type === "text") {
1070
+ if (text.length === 0 && block.text) {
1071
+ text = block.text;
1072
+ }
1073
+ }
1074
+ if (block.type === "tool_use") {
1075
+ toolCalls.push({
1076
+ id: block.id,
1077
+ name: block.name,
1078
+ input: block.input ?? {}
1079
+ });
1080
+ }
1081
+ }
1082
+ yield {
1083
+ type: "final",
1084
+ response: {
1085
+ text,
1086
+ toolCalls,
1087
+ usage: {
1088
+ input: response.usage.input_tokens,
1089
+ output: response.usage.output_tokens
1090
+ },
1091
+ rawContent: response.content
1092
+ }
1093
+ };
1094
+ }
1095
+ async generate(input) {
1096
+ let finalResponse;
1097
+ for await (const event of this.generateStream(input)) {
1098
+ if (event.type === "final") {
1099
+ finalResponse = event.response;
1100
+ }
1101
+ }
1102
+ if (!finalResponse) {
1103
+ throw new Error("Anthropic response ended without final payload");
1104
+ }
1105
+ return finalResponse;
1106
+ }
1107
+ };
1108
+
1109
+ // src/openai-client.ts
1110
+ import OpenAI from "openai";
1111
+ var toOpenAiMessages = (systemPrompt, messages) => {
1112
+ const mapped = [{ role: "system", content: systemPrompt }];
1113
+ for (const message of messages) {
1114
+ if (message.role === "system") {
1115
+ continue;
1116
+ }
1117
+ if (message.role === "tool") {
1118
+ mapped.push({
1119
+ role: "user",
1120
+ content: `Tool result context: ${message.content}`
1121
+ });
1122
+ continue;
1123
+ }
1124
+ mapped.push({ role: message.role, content: message.content });
1125
+ }
1126
+ return mapped;
1127
+ };
1128
+ var OpenAiModelClient = class {
1129
+ client;
1130
+ latitudeCapture;
1131
+ constructor(apiKey, options) {
1132
+ this.client = new OpenAI({
1133
+ apiKey: apiKey ?? process.env.OPENAI_API_KEY ?? "missing-openai-key"
1134
+ });
1135
+ this.latitudeCapture = options?.latitudeCapture;
1136
+ }
1137
+ async *generateStream(input) {
1138
+ const stream = await (this.latitudeCapture?.capture(
1139
+ async () => this.client.chat.completions.create({
1140
+ model: input.modelName,
1141
+ temperature: input.temperature ?? 0.2,
1142
+ max_tokens: input.maxTokens ?? 1024,
1143
+ messages: toOpenAiMessages(input.systemPrompt, input.messages),
1144
+ tools: input.tools.map((tool) => ({
1145
+ type: "function",
1146
+ function: {
1147
+ name: tool.name,
1148
+ description: tool.description,
1149
+ parameters: tool.inputSchema
1150
+ }
1151
+ })),
1152
+ tool_choice: "auto",
1153
+ stream: true,
1154
+ stream_options: { include_usage: true }
1155
+ })
1156
+ ) ?? this.client.chat.completions.create({
1157
+ model: input.modelName,
1158
+ temperature: input.temperature ?? 0.2,
1159
+ max_tokens: input.maxTokens ?? 1024,
1160
+ messages: toOpenAiMessages(input.systemPrompt, input.messages),
1161
+ tools: input.tools.map((tool) => ({
1162
+ type: "function",
1163
+ function: {
1164
+ name: tool.name,
1165
+ description: tool.description,
1166
+ parameters: tool.inputSchema
1167
+ }
1168
+ })),
1169
+ tool_choice: "auto",
1170
+ stream: true,
1171
+ stream_options: { include_usage: true }
1172
+ }));
1173
+ let text = "";
1174
+ let inputTokens = 0;
1175
+ let outputTokens = 0;
1176
+ const toolCallsByIndex = /* @__PURE__ */ new Map();
1177
+ for await (const chunk of stream) {
1178
+ if (chunk.usage) {
1179
+ inputTokens = chunk.usage.prompt_tokens ?? inputTokens;
1180
+ outputTokens = chunk.usage.completion_tokens ?? outputTokens;
1181
+ }
1182
+ const delta = chunk.choices[0]?.delta;
1183
+ if (delta?.content) {
1184
+ text += delta.content;
1185
+ yield { type: "chunk", content: delta.content };
1186
+ }
1187
+ for (const toolCall of delta?.tool_calls ?? []) {
1188
+ const index = toolCall.index ?? 0;
1189
+ const current = toolCallsByIndex.get(index) ?? {
1190
+ id: toolCall.id ?? `tool_call_${index}`,
1191
+ name: "",
1192
+ argumentsJson: ""
1193
+ };
1194
+ if (toolCall.id) {
1195
+ current.id = toolCall.id;
1196
+ }
1197
+ if (toolCall.function?.name) {
1198
+ current.name = toolCall.function.name;
1199
+ }
1200
+ if (toolCall.function?.arguments) {
1201
+ current.argumentsJson += toolCall.function.arguments;
1202
+ }
1203
+ toolCallsByIndex.set(index, current);
1204
+ }
1205
+ }
1206
+ const toolCalls = Array.from(toolCallsByIndex.values()).filter((call) => call.name.length > 0).map((call) => {
1207
+ let parsedInput = {};
1208
+ if (call.argumentsJson.trim().length > 0) {
1209
+ try {
1210
+ parsedInput = JSON.parse(call.argumentsJson);
1211
+ } catch {
1212
+ parsedInput = { raw: call.argumentsJson };
1213
+ }
1214
+ }
1215
+ return {
1216
+ id: call.id,
1217
+ name: call.name,
1218
+ input: parsedInput
1219
+ };
1220
+ });
1221
+ yield {
1222
+ type: "final",
1223
+ response: {
1224
+ text,
1225
+ toolCalls,
1226
+ usage: {
1227
+ input: inputTokens,
1228
+ output: outputTokens
1229
+ },
1230
+ rawContent: []
1231
+ }
1232
+ };
1233
+ }
1234
+ async generate(input) {
1235
+ let finalResponse;
1236
+ for await (const event of this.generateStream(input)) {
1237
+ if (event.type === "final") {
1238
+ finalResponse = event.response;
1239
+ }
1240
+ }
1241
+ if (!finalResponse) {
1242
+ throw new Error("OpenAI response ended without final payload");
1243
+ }
1244
+ return finalResponse;
1245
+ }
1246
+ };
1247
+
1248
+ // src/model-factory.ts
1249
+ var createModelClient = (provider, options) => {
1250
+ const normalized = (provider ?? "anthropic").toLowerCase();
1251
+ if (normalized === "openai") {
1252
+ return new OpenAiModelClient(void 0, options);
1253
+ }
1254
+ return new AnthropicModelClient(void 0, options);
1255
+ };
1256
+
1257
+ // src/skill-context.ts
1258
+ import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
1259
+ import { dirname as dirname3, resolve as resolve5, normalize } from "path";
1260
+ import YAML2 from "yaml";
1261
+ var DEFAULT_SKILL_DIRS = ["skills"];
1262
+ var resolveSkillDirs = (workingDir, extraPaths) => {
1263
+ const dirs = [...DEFAULT_SKILL_DIRS];
1264
+ if (extraPaths) {
1265
+ for (const p of extraPaths) {
1266
+ if (!dirs.includes(p)) {
1267
+ dirs.push(p);
1268
+ }
1269
+ }
1270
+ }
1271
+ return dirs.map((d) => resolve5(workingDir, d));
1272
+ };
1273
+ var FRONTMATTER_PATTERN2 = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
1274
+ var asRecord2 = (value) => typeof value === "object" && value !== null ? value : {};
1275
+ var parseSkillFrontmatter = (content) => {
1276
+ const match = content.match(FRONTMATTER_PATTERN2);
1277
+ if (!match) {
1278
+ return void 0;
1279
+ }
1280
+ const parsedYaml = YAML2.parse(match[1]) ?? {};
1281
+ const parsed = asRecord2(parsedYaml);
1282
+ const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
1283
+ if (!name) {
1284
+ return void 0;
1285
+ }
1286
+ const description = typeof parsed.description === "string" ? parsed.description.trim() : "";
1287
+ const allowedToolsValue = parsed["allowed-tools"];
1288
+ const allowedTools = typeof allowedToolsValue === "string" ? allowedToolsValue.split(/\s+/).map((tool) => tool.trim()).filter((tool) => tool.length > 0) : [];
1289
+ const legacyToolsValue = parsed.tools;
1290
+ const legacyTools = Array.isArray(legacyToolsValue) ? legacyToolsValue.filter((tool) => typeof tool === "string") : [];
1291
+ const tools = allowedTools.length > 0 ? allowedTools : legacyTools;
1292
+ return { name, description, tools };
1293
+ };
1294
+ var collectSkillManifests = async (directory) => {
1295
+ const entries = await readdir2(directory, { withFileTypes: true });
1296
+ const files = [];
1297
+ for (const entry of entries) {
1298
+ const fullPath = resolve5(directory, entry.name);
1299
+ if (entry.isDirectory()) {
1300
+ files.push(...await collectSkillManifests(fullPath));
1301
+ continue;
1302
+ }
1303
+ if (entry.isFile() && entry.name.toLowerCase() === "skill.md") {
1304
+ files.push(fullPath);
1305
+ }
1306
+ }
1307
+ return files;
1308
+ };
1309
+ var loadSkillMetadata = async (workingDir, extraSkillPaths) => {
1310
+ const skillDirs = resolveSkillDirs(workingDir, extraSkillPaths);
1311
+ const allManifests = [];
1312
+ for (const dir of skillDirs) {
1313
+ try {
1314
+ allManifests.push(...await collectSkillManifests(dir));
1315
+ } catch {
1316
+ }
1317
+ }
1318
+ const skills = [];
1319
+ const seen = /* @__PURE__ */ new Set();
1320
+ for (const manifest of allManifests) {
1321
+ try {
1322
+ const content = await readFile4(manifest, "utf8");
1323
+ const parsed = parseSkillFrontmatter(content);
1324
+ if (parsed && !seen.has(parsed.name)) {
1325
+ seen.add(parsed.name);
1326
+ skills.push({
1327
+ ...parsed,
1328
+ skillDir: dirname3(manifest),
1329
+ skillPath: manifest
1330
+ });
1331
+ }
1332
+ } catch {
1333
+ }
1334
+ }
1335
+ return skills;
1336
+ };
1337
+ var buildSkillContextWindow = (skills) => {
1338
+ if (skills.length === 0) {
1339
+ return "";
1340
+ }
1341
+ const xmlSkills = skills.map((skill) => {
1342
+ const lines = [
1343
+ " <skill>",
1344
+ ` <name>${escapeXml(skill.name)}</name>`
1345
+ ];
1346
+ if (skill.description) {
1347
+ lines.push(
1348
+ ` <description>${escapeXml(skill.description)}</description>`
1349
+ );
1350
+ }
1351
+ lines.push(" </skill>");
1352
+ return lines.join("\n");
1353
+ }).join("\n");
1354
+ return `<available_skills description="Skills the agent can use. Use the activate_skill tool to load full instructions for a skill when a user's request matches its description.">
1355
+ ${xmlSkills}
1356
+ </available_skills>`;
1357
+ };
1358
+ var escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1359
+ var loadSkillInstructions = async (skill) => {
1360
+ const content = await readFile4(skill.skillPath, "utf8");
1361
+ const match = content.match(FRONTMATTER_PATTERN2);
1362
+ return match ? match[2].trim() : content.trim();
1363
+ };
1364
+ var readSkillResource = async (skill, relativePath) => {
1365
+ const normalized = normalize(relativePath);
1366
+ if (normalized.startsWith("..") || normalized.startsWith("/")) {
1367
+ throw new Error("Path must be relative and within the skill directory");
1368
+ }
1369
+ const fullPath = resolve5(skill.skillDir, normalized);
1370
+ if (!fullPath.startsWith(skill.skillDir)) {
1371
+ throw new Error("Path escapes the skill directory");
1372
+ }
1373
+ return await readFile4(fullPath, "utf8");
1374
+ };
1375
+ var MAX_INSTRUCTIONS_PER_SKILL = 1200;
1376
+ var loadSkillContext = async (workingDir) => {
1377
+ const metadata = await loadSkillMetadata(workingDir);
1378
+ const entries = [];
1379
+ for (const skill of metadata) {
1380
+ try {
1381
+ const instructions = await loadSkillInstructions(skill);
1382
+ const trimmed = instructions.length > MAX_INSTRUCTIONS_PER_SKILL ? `${instructions.slice(0, MAX_INSTRUCTIONS_PER_SKILL)}...` : instructions;
1383
+ entries.push({ ...skill, instructions: trimmed });
1384
+ } catch {
1385
+ entries.push({ ...skill, instructions: "" });
1386
+ }
1387
+ }
1388
+ return entries;
1389
+ };
1390
+
1391
+ // src/skill-tools.ts
1392
+ import { defineTool as defineTool3 } from "@poncho-ai/sdk";
1393
+ import { access as access2, readdir as readdir3 } from "fs/promises";
1394
+ import { extname, normalize as normalize2, resolve as resolve6, sep as sep2 } from "path";
1395
+ import { pathToFileURL } from "url";
1396
+ import { createJiti } from "jiti";
1397
+ var createSkillTools = (skills) => {
1398
+ if (skills.length === 0) {
1399
+ return [];
1400
+ }
1401
+ const skillsByName = new Map(skills.map((skill) => [skill.name, skill]));
1402
+ const knownNames = skills.map((skill) => skill.name).join(", ");
1403
+ return [
1404
+ defineTool3({
1405
+ name: "activate_skill",
1406
+ description: `Load the full instructions for an available skill. Use this when a user's request matches a skill's description. Available skills: ${knownNames}`,
1407
+ inputSchema: {
1408
+ type: "object",
1409
+ properties: {
1410
+ name: {
1411
+ type: "string",
1412
+ description: "Name of the skill to activate"
1413
+ }
1414
+ },
1415
+ required: ["name"],
1416
+ additionalProperties: false
1417
+ },
1418
+ handler: async (input) => {
1419
+ const name = typeof input.name === "string" ? input.name.trim() : "";
1420
+ const skill = skillsByName.get(name);
1421
+ if (!skill) {
1422
+ return {
1423
+ error: `Unknown skill: "${name}". Available skills: ${knownNames}`
1424
+ };
1425
+ }
1426
+ try {
1427
+ const instructions = await loadSkillInstructions(skill);
1428
+ return {
1429
+ skill: name,
1430
+ instructions: instructions || "(no instructions provided)"
1431
+ };
1432
+ } catch (err) {
1433
+ return {
1434
+ error: `Failed to load skill "${name}": ${err instanceof Error ? err.message : String(err)}`
1435
+ };
1436
+ }
1437
+ }
1438
+ }),
1439
+ defineTool3({
1440
+ name: "read_skill_resource",
1441
+ description: `Read a file from a skill's directory (references, scripts, assets). Use relative paths from the skill root. Available skills: ${knownNames}`,
1442
+ inputSchema: {
1443
+ type: "object",
1444
+ properties: {
1445
+ skill: {
1446
+ type: "string",
1447
+ description: "Name of the skill"
1448
+ },
1449
+ path: {
1450
+ type: "string",
1451
+ description: "Relative path to the file within the skill directory (e.g. references/REFERENCE.md)"
1452
+ }
1453
+ },
1454
+ required: ["skill", "path"],
1455
+ additionalProperties: false
1456
+ },
1457
+ handler: async (input) => {
1458
+ const name = typeof input.skill === "string" ? input.skill.trim() : "";
1459
+ const path = typeof input.path === "string" ? input.path.trim() : "";
1460
+ const skill = skillsByName.get(name);
1461
+ if (!skill) {
1462
+ return {
1463
+ error: `Unknown skill: "${name}". Available skills: ${knownNames}`
1464
+ };
1465
+ }
1466
+ if (!path) {
1467
+ return { error: "Path is required" };
1468
+ }
1469
+ try {
1470
+ const content = await readSkillResource(skill, path);
1471
+ return { skill: name, path, content };
1472
+ } catch (err) {
1473
+ return {
1474
+ error: `Failed to read "${path}" from skill "${name}": ${err instanceof Error ? err.message : String(err)}`
1475
+ };
1476
+ }
1477
+ }
1478
+ }),
1479
+ defineTool3({
1480
+ name: "list_skill_scripts",
1481
+ description: `List JavaScript/TypeScript script files available under a skill's scripts directory. Available skills: ${knownNames}`,
1482
+ inputSchema: {
1483
+ type: "object",
1484
+ properties: {
1485
+ skill: {
1486
+ type: "string",
1487
+ description: "Name of the skill"
1488
+ }
1489
+ },
1490
+ required: ["skill"],
1491
+ additionalProperties: false
1492
+ },
1493
+ handler: async (input) => {
1494
+ const name = typeof input.skill === "string" ? input.skill.trim() : "";
1495
+ const skill = skillsByName.get(name);
1496
+ if (!skill) {
1497
+ return {
1498
+ error: `Unknown skill: "${name}". Available skills: ${knownNames}`
1499
+ };
1500
+ }
1501
+ try {
1502
+ const scripts = await listSkillScripts(skill);
1503
+ return {
1504
+ skill: name,
1505
+ scripts
1506
+ };
1507
+ } catch (err) {
1508
+ return {
1509
+ error: `Failed to list scripts for skill "${name}": ${err instanceof Error ? err.message : String(err)}`
1510
+ };
1511
+ }
1512
+ }
1513
+ }),
1514
+ defineTool3({
1515
+ name: "run_skill_script",
1516
+ description: `Run a JavaScript/TypeScript module in a skill's scripts directory. Uses default export function or named run/main/handler function. Available skills: ${knownNames}`,
1517
+ inputSchema: {
1518
+ type: "object",
1519
+ properties: {
1520
+ skill: {
1521
+ type: "string",
1522
+ description: "Name of the skill"
1523
+ },
1524
+ script: {
1525
+ type: "string",
1526
+ description: "Relative path under scripts/ (e.g. scripts/summarize.ts or summarize.ts)"
1527
+ },
1528
+ input: {
1529
+ type: "object",
1530
+ description: "Optional JSON input payload passed to the script function"
1531
+ }
1532
+ },
1533
+ required: ["skill", "script"],
1534
+ additionalProperties: false
1535
+ },
1536
+ handler: async (input) => {
1537
+ const name = typeof input.skill === "string" ? input.skill.trim() : "";
1538
+ const script = typeof input.script === "string" ? input.script.trim() : "";
1539
+ const payload = typeof input.input === "object" && input.input !== null ? input.input : {};
1540
+ const skill = skillsByName.get(name);
1541
+ if (!skill) {
1542
+ return {
1543
+ error: `Unknown skill: "${name}". Available skills: ${knownNames}`
1544
+ };
1545
+ }
1546
+ if (!script) {
1547
+ return { error: "Script path is required" };
1548
+ }
1549
+ try {
1550
+ const scriptPath = resolveSkillScriptPath(skill, script);
1551
+ await access2(scriptPath);
1552
+ const fn = await loadRunnableScriptFunction(scriptPath);
1553
+ const output = await fn(payload, {
1554
+ skill: name,
1555
+ skillDir: skill.skillDir,
1556
+ scriptPath
1557
+ });
1558
+ return {
1559
+ skill: name,
1560
+ script,
1561
+ output
1562
+ };
1563
+ } catch (err) {
1564
+ return {
1565
+ error: `Failed to run script "${script}" in skill "${name}": ${err instanceof Error ? err.message : String(err)}`
1566
+ };
1567
+ }
1568
+ }
1569
+ })
1570
+ ];
1571
+ };
1572
+ var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"]);
1573
+ var listSkillScripts = async (skill) => {
1574
+ const scriptsRoot = resolve6(skill.skillDir, "scripts");
1575
+ try {
1576
+ await access2(scriptsRoot);
1577
+ } catch {
1578
+ return [];
1579
+ }
1580
+ const scripts = await collectScriptFiles(scriptsRoot);
1581
+ return scripts.map((fullPath) => `scripts/${fullPath.slice(scriptsRoot.length + 1).split(sep2).join("/")}`).sort();
1582
+ };
1583
+ var collectScriptFiles = async (directory) => {
1584
+ const entries = await readdir3(directory, { withFileTypes: true });
1585
+ const files = [];
1586
+ for (const entry of entries) {
1587
+ const fullPath = resolve6(directory, entry.name);
1588
+ if (entry.isDirectory()) {
1589
+ files.push(...await collectScriptFiles(fullPath));
1590
+ continue;
1591
+ }
1592
+ if (entry.isFile()) {
1593
+ const extension = extname(fullPath).toLowerCase();
1594
+ if (SCRIPT_EXTENSIONS.has(extension)) {
1595
+ files.push(fullPath);
1596
+ }
1597
+ }
1598
+ }
1599
+ return files;
1600
+ };
1601
+ var resolveSkillScriptPath = (skill, relativePath) => {
1602
+ const normalized = normalize2(relativePath);
1603
+ if (normalized.startsWith("..") || normalized.startsWith("/")) {
1604
+ throw new Error("Script path must be relative and within the skill directory");
1605
+ }
1606
+ const normalizedWithPrefix = normalized.startsWith("scripts/") ? normalized : `scripts/${normalized}`;
1607
+ const fullPath = resolve6(skill.skillDir, normalizedWithPrefix);
1608
+ const scriptsRoot = resolve6(skill.skillDir, "scripts");
1609
+ if (!fullPath.startsWith(`${scriptsRoot}${sep2}`) && fullPath !== scriptsRoot) {
1610
+ throw new Error("Script path must stay inside the scripts directory");
1611
+ }
1612
+ const extension = extname(fullPath).toLowerCase();
1613
+ if (!SCRIPT_EXTENSIONS.has(extension)) {
1614
+ throw new Error(
1615
+ `Unsupported script extension "${extension || "(none)"}". Allowed: ${[...SCRIPT_EXTENSIONS].join(", ")}`
1616
+ );
1617
+ }
1618
+ return fullPath;
1619
+ };
1620
+ var loadRunnableScriptFunction = async (scriptPath) => {
1621
+ const loaded = await loadScriptModule(scriptPath);
1622
+ const fn = extractRunnableFunction(loaded);
1623
+ if (!fn) {
1624
+ throw new Error(
1625
+ "Script module must export a function (default export or named run/main/handler)"
1626
+ );
1627
+ }
1628
+ return fn;
1629
+ };
1630
+ var loadScriptModule = async (scriptPath) => {
1631
+ try {
1632
+ return await import(pathToFileURL(scriptPath).href);
1633
+ } catch {
1634
+ const jiti = createJiti(import.meta.url, { interopDefault: true });
1635
+ return await jiti.import(scriptPath);
1636
+ }
1637
+ };
1638
+ var extractRunnableFunction = (value) => {
1639
+ if (typeof value === "function") {
1640
+ return value;
1641
+ }
1642
+ if (Array.isArray(value) || typeof value !== "object" || value === null) {
1643
+ return void 0;
1644
+ }
1645
+ const module = value;
1646
+ const defaultValue = module.default;
1647
+ if (typeof defaultValue === "function") {
1648
+ return defaultValue;
1649
+ }
1650
+ if (typeof module.run === "function") {
1651
+ return module.run;
1652
+ }
1653
+ if (typeof module.main === "function") {
1654
+ return module.main;
1655
+ }
1656
+ if (typeof module.handler === "function") {
1657
+ return module.handler;
1658
+ }
1659
+ if (defaultValue && typeof defaultValue === "object" && !Array.isArray(defaultValue)) {
1660
+ const inner = defaultValue;
1661
+ if (typeof inner.run === "function") {
1662
+ return inner.run;
1663
+ }
1664
+ if (typeof inner.main === "function") {
1665
+ return inner.main;
1666
+ }
1667
+ if (typeof inner.handler === "function") {
1668
+ return inner.handler;
1669
+ }
1670
+ }
1671
+ return void 0;
1672
+ };
1673
+
1674
+ // src/tool-dispatcher.ts
1675
+ var ToolDispatcher = class {
1676
+ tools = /* @__PURE__ */ new Map();
1677
+ register(tool) {
1678
+ this.tools.set(tool.name, tool);
1679
+ }
1680
+ registerMany(tools) {
1681
+ for (const tool of tools) {
1682
+ this.register(tool);
1683
+ }
1684
+ }
1685
+ list() {
1686
+ return [...this.tools.values()];
1687
+ }
1688
+ get(name) {
1689
+ return this.tools.get(name);
1690
+ }
1691
+ async execute(call, context) {
1692
+ const definition = this.tools.get(call.name);
1693
+ if (!definition) {
1694
+ return {
1695
+ callId: call.id,
1696
+ tool: call.name,
1697
+ error: `Tool not found: ${call.name}`
1698
+ };
1699
+ }
1700
+ try {
1701
+ const output = await definition.handler(call.input, context);
1702
+ return {
1703
+ callId: call.id,
1704
+ tool: call.name,
1705
+ output
1706
+ };
1707
+ } catch (error) {
1708
+ return {
1709
+ callId: call.id,
1710
+ tool: call.name,
1711
+ error: error instanceof Error ? error.message : "Unknown tool error"
1712
+ };
1713
+ }
1714
+ }
1715
+ async executeBatch(calls, context) {
1716
+ return Promise.all(calls.map(async (call) => this.execute(call, context)));
1717
+ }
1718
+ };
1719
+
1720
+ // src/harness.ts
1721
+ var now = () => Date.now();
1722
+ var MAX_CONTEXT_MESSAGES = 40;
1723
+ var trimMessageWindow = (messages) => messages.length <= MAX_CONTEXT_MESSAGES ? messages : messages.slice(messages.length - MAX_CONTEXT_MESSAGES);
1724
+ var DEVELOPMENT_MODE_CONTEXT = `## Development Mode Context
1725
+
1726
+ You are running locally in development mode. Treat this as an editable agent workspace.
1727
+
1728
+ When users ask about customization:
1729
+ - Explain and edit \`poncho.config.js\` for model/provider, storage+memory, auth, telemetry, and MCP settings.
1730
+ - Help create or update local skills under \`skills/<skill-name>/SKILL.md\`.
1731
+ - For executable skills, add JavaScript/TypeScript scripts under \`skills/<skill-name>/scripts/\` and run them via \`run_skill_script\`.
1732
+ - For setup, skills, MCP, auth, storage, telemetry, or "how do I..." questions, proactively read \`README.md\` with \`read_file\` before answering.
1733
+ - Prefer quoting concrete commands and examples from \`README.md\` over guessing.
1734
+ - Keep edits minimal, preserve unrelated settings/code, and summarize what changed.`;
1735
+ var AgentHarness = class {
1736
+ workingDir;
1737
+ environment;
1738
+ modelClient;
1739
+ dispatcher = new ToolDispatcher();
1740
+ approvalHandler;
1741
+ skillContextWindow = "";
1742
+ memoryStore;
1743
+ parsedAgent;
1744
+ mcpBridge;
1745
+ getConfiguredToolFlag(config, name) {
1746
+ const defaults = config?.tools?.defaults;
1747
+ const environment = this.environment ?? "development";
1748
+ const envOverrides = config?.tools?.byEnvironment?.[environment];
1749
+ return envOverrides?.[name] ?? defaults?.[name];
1750
+ }
1751
+ isBuiltInToolEnabled(config, name) {
1752
+ if (name === "write_file") {
1753
+ const allowedByEnvironment = this.shouldEnableWriteTool();
1754
+ const configured = this.getConfiguredToolFlag(config, "write_file");
1755
+ return allowedByEnvironment && configured !== false;
1756
+ }
1757
+ if (name === "list_directory") {
1758
+ const configured = this.getConfiguredToolFlag(config, "list_directory");
1759
+ return configured !== false;
1760
+ }
1761
+ if (name === "read_file") {
1762
+ const configured = this.getConfiguredToolFlag(config, "read_file");
1763
+ return configured !== false;
1764
+ }
1765
+ return true;
1766
+ }
1767
+ registerIfMissing(tool) {
1768
+ if (!this.dispatcher.get(tool.name)) {
1769
+ this.dispatcher.register(tool);
1770
+ }
1771
+ }
1772
+ registerConfiguredBuiltInTools(config) {
1773
+ for (const tool of createDefaultTools(this.workingDir)) {
1774
+ if (this.isBuiltInToolEnabled(config, tool.name)) {
1775
+ this.registerIfMissing(tool);
1776
+ }
1777
+ }
1778
+ if (this.isBuiltInToolEnabled(config, "write_file")) {
1779
+ this.registerIfMissing(createWriteTool(this.workingDir));
1780
+ }
1781
+ }
1782
+ shouldEnableWriteTool() {
1783
+ const override = process.env.PONCHO_FS_WRITE?.toLowerCase();
1784
+ if (override === "1" || override === "true" || override === "yes") {
1785
+ return true;
1786
+ }
1787
+ if (override === "0" || override === "false" || override === "no") {
1788
+ return false;
1789
+ }
1790
+ return this.environment !== "production";
1791
+ }
1792
+ constructor(options = {}) {
1793
+ this.workingDir = options.workingDir ?? process.cwd();
1794
+ this.environment = options.environment ?? "development";
1795
+ this.modelClient = createModelClient("anthropic");
1796
+ this.approvalHandler = options.approvalHandler;
1797
+ if (options.toolDefinitions?.length) {
1798
+ this.dispatcher.registerMany(options.toolDefinitions);
1799
+ }
1800
+ }
1801
+ async initialize() {
1802
+ this.parsedAgent = await parseAgentFile(this.workingDir);
1803
+ const config = await loadPonchoConfig(this.workingDir);
1804
+ this.registerConfiguredBuiltInTools(config);
1805
+ const provider = this.parsedAgent.frontmatter.model?.provider ?? "anthropic";
1806
+ const memoryConfig = resolveMemoryConfig(config);
1807
+ const latitudeCapture = new LatitudeCapture({
1808
+ apiKey: config?.telemetry?.latitude?.apiKey ?? process.env.LATITUDE_API_KEY,
1809
+ projectId: config?.telemetry?.latitude?.projectId ?? process.env.LATITUDE_PROJECT_ID,
1810
+ path: config?.telemetry?.latitude?.path ?? config?.telemetry?.latitude?.documentPath ?? process.env.LATITUDE_PATH ?? process.env.LATITUDE_DOCUMENT_PATH,
1811
+ defaultPath: `agents/${this.parsedAgent.frontmatter.name}/model-call`
1812
+ });
1813
+ this.modelClient = createModelClient(provider, { latitudeCapture });
1814
+ const bridge = new LocalMcpBridge(config);
1815
+ this.mcpBridge = bridge;
1816
+ const extraSkillPaths = config?.skillPaths;
1817
+ const skillMetadata = await loadSkillMetadata(this.workingDir, extraSkillPaths);
1818
+ this.skillContextWindow = buildSkillContextWindow(skillMetadata);
1819
+ this.dispatcher.registerMany(createSkillTools(skillMetadata));
1820
+ if (memoryConfig?.enabled) {
1821
+ this.memoryStore = createMemoryStore(
1822
+ this.parsedAgent.frontmatter.name,
1823
+ memoryConfig,
1824
+ { workingDir: this.workingDir }
1825
+ );
1826
+ this.dispatcher.registerMany(
1827
+ createMemoryTools(this.memoryStore, {
1828
+ maxRecallConversations: memoryConfig.maxRecallConversations
1829
+ })
1830
+ );
1831
+ }
1832
+ await bridge.startLocalServers();
1833
+ this.dispatcher.registerMany(await bridge.loadTools());
1834
+ }
1835
+ async shutdown() {
1836
+ await this.mcpBridge?.stopLocalServers();
1837
+ }
1838
+ listTools() {
1839
+ return this.dispatcher.list();
1840
+ }
1841
+ async *run(input) {
1842
+ if (!this.parsedAgent) {
1843
+ await this.initialize();
1844
+ }
1845
+ const agent = this.parsedAgent;
1846
+ const runId = `run_${randomUUID2()}`;
1847
+ const start = now();
1848
+ const maxSteps = agent.frontmatter.limits?.maxSteps ?? 50;
1849
+ const timeoutMs = (agent.frontmatter.limits?.timeout ?? 300) * 1e3;
1850
+ const messages = [...input.messages ?? []];
1851
+ const events = [];
1852
+ const systemPrompt = renderAgentPrompt(agent, {
1853
+ parameters: input.parameters,
1854
+ runtime: {
1855
+ runId,
1856
+ agentId: agent.frontmatter.name,
1857
+ environment: this.environment,
1858
+ workingDir: this.workingDir
1859
+ }
1860
+ });
1861
+ const developmentContext = this.environment === "development" ? `
1862
+
1863
+ ${DEVELOPMENT_MODE_CONTEXT}` : "";
1864
+ const promptWithSkills = this.skillContextWindow ? `${systemPrompt}${developmentContext}
1865
+
1866
+ ${this.skillContextWindow}` : `${systemPrompt}${developmentContext}`;
1867
+ const mainMemory = this.memoryStore ? await this.memoryStore.getMainMemory() : void 0;
1868
+ const boundedMainMemory = mainMemory && mainMemory.content.length > 4e3 ? `${mainMemory.content.slice(0, 4e3)}
1869
+ ...[truncated]` : mainMemory?.content;
1870
+ const memoryContext = boundedMainMemory && boundedMainMemory.trim().length > 0 ? `
1871
+ ## Persistent Memory
1872
+
1873
+ ${boundedMainMemory.trim()}` : "";
1874
+ const integrityPrompt = `${promptWithSkills}${memoryContext}
1875
+
1876
+ ## Execution Integrity
1877
+
1878
+ - Do not claim that you executed a tool unless you actually emitted a tool call in this run.
1879
+ - Do not fabricate "Tool Used" or "Tool Result" logs as plain text.
1880
+ - Never output faux execution transcripts, markdown tool logs, or "Tool Used/Result" sections.
1881
+ - If no suitable tool is available, explicitly say that and ask for guidance.`;
1882
+ const pushEvent = (event) => {
1883
+ events.push(event);
1884
+ return event;
1885
+ };
1886
+ yield pushEvent({
1887
+ type: "run:started",
1888
+ runId,
1889
+ agentId: agent.frontmatter.name
1890
+ });
1891
+ messages.push({
1892
+ role: "user",
1893
+ content: input.task,
1894
+ metadata: { timestamp: now(), id: randomUUID2() }
1895
+ });
1896
+ let responseText = "";
1897
+ let totalInputTokens = 0;
1898
+ let totalOutputTokens = 0;
1899
+ for (let step = 1; step <= maxSteps; step += 1) {
1900
+ if (now() - start > timeoutMs) {
1901
+ yield pushEvent({
1902
+ type: "run:error",
1903
+ runId,
1904
+ error: {
1905
+ code: "TIMEOUT",
1906
+ message: `Run exceeded timeout of ${Math.floor(timeoutMs / 1e3)}s`
1907
+ }
1908
+ });
1909
+ return;
1910
+ }
1911
+ const stepStart = now();
1912
+ yield pushEvent({ type: "step:started", step });
1913
+ yield pushEvent({ type: "model:request", tokens: 0 });
1914
+ const modelCallInput = {
1915
+ modelName: agent.frontmatter.model?.name ?? "claude-opus-4-5",
1916
+ temperature: agent.frontmatter.model?.temperature,
1917
+ maxTokens: agent.frontmatter.model?.maxTokens,
1918
+ systemPrompt: integrityPrompt,
1919
+ messages: trimMessageWindow(messages),
1920
+ tools: this.dispatcher.list()
1921
+ };
1922
+ let modelResponse;
1923
+ let streamedAnyChunk = false;
1924
+ if (this.modelClient.generateStream) {
1925
+ for await (const streamEvent of this.modelClient.generateStream(modelCallInput)) {
1926
+ if (streamEvent.type === "chunk" && streamEvent.content.length > 0) {
1927
+ streamedAnyChunk = true;
1928
+ yield pushEvent({ type: "model:chunk", content: streamEvent.content });
1929
+ }
1930
+ if (streamEvent.type === "final") {
1931
+ modelResponse = streamEvent.response;
1932
+ }
1933
+ }
1934
+ } else {
1935
+ modelResponse = await this.modelClient.generate(modelCallInput);
1936
+ }
1937
+ if (!modelResponse) {
1938
+ throw new Error("Model response ended without final payload");
1939
+ }
1940
+ totalInputTokens += modelResponse.usage.input;
1941
+ totalOutputTokens += modelResponse.usage.output;
1942
+ if (!streamedAnyChunk && modelResponse.text) {
1943
+ yield pushEvent({ type: "model:chunk", content: modelResponse.text });
1944
+ }
1945
+ yield pushEvent({
1946
+ type: "model:response",
1947
+ usage: {
1948
+ input: modelResponse.usage.input,
1949
+ output: modelResponse.usage.output,
1950
+ cached: 0
1951
+ }
1952
+ });
1953
+ if (modelResponse.toolCalls.length === 0) {
1954
+ responseText = modelResponse.text;
1955
+ yield pushEvent({
1956
+ type: "step:completed",
1957
+ step,
1958
+ duration: now() - stepStart
1959
+ });
1960
+ const result = {
1961
+ status: "completed",
1962
+ response: responseText,
1963
+ steps: step,
1964
+ tokens: {
1965
+ input: totalInputTokens,
1966
+ output: totalOutputTokens,
1967
+ cached: 0
1968
+ },
1969
+ duration: now() - start
1970
+ };
1971
+ yield pushEvent({ type: "run:completed", runId, result });
1972
+ return;
1973
+ }
1974
+ const toolContext = {
1975
+ runId,
1976
+ agentId: agent.frontmatter.name,
1977
+ step,
1978
+ workingDir: this.workingDir,
1979
+ parameters: input.parameters ?? {}
1980
+ };
1981
+ const toolResultsForModel = [];
1982
+ const approvedCalls = [];
1983
+ for (const call of modelResponse.toolCalls) {
1984
+ yield pushEvent({ type: "tool:started", tool: call.name, input: call.input });
1985
+ const definition = this.dispatcher.get(call.name);
1986
+ if (definition?.requiresApproval) {
1987
+ const approvalId = `approval_${randomUUID2()}`;
1988
+ yield pushEvent({
1989
+ type: "tool:approval:required",
1990
+ tool: call.name,
1991
+ input: call.input,
1992
+ approvalId
1993
+ });
1994
+ const approved = this.approvalHandler ? await this.approvalHandler({
1995
+ tool: call.name,
1996
+ input: call.input,
1997
+ runId,
1998
+ step,
1999
+ approvalId
2000
+ }) : false;
2001
+ if (!approved) {
2002
+ yield pushEvent({
2003
+ type: "tool:approval:denied",
2004
+ approvalId,
2005
+ reason: "No approval handler granted execution"
2006
+ });
2007
+ yield pushEvent({
2008
+ type: "tool:error",
2009
+ tool: call.name,
2010
+ error: "Tool execution denied by approval policy",
2011
+ recoverable: true
2012
+ });
2013
+ toolResultsForModel.push({
2014
+ type: "tool_result",
2015
+ tool_use_id: call.id,
2016
+ content: "Tool error: Tool execution denied by approval policy"
2017
+ });
2018
+ continue;
2019
+ }
2020
+ yield pushEvent({ type: "tool:approval:granted", approvalId });
2021
+ }
2022
+ approvedCalls.push({
2023
+ id: call.id,
2024
+ name: call.name,
2025
+ input: call.input
2026
+ });
2027
+ }
2028
+ const batchStart = now();
2029
+ const batchResults = approvedCalls.length > 0 ? await this.dispatcher.executeBatch(approvedCalls, toolContext) : [];
2030
+ for (const result of batchResults) {
2031
+ if (result.error) {
2032
+ yield pushEvent({
2033
+ type: "tool:error",
2034
+ tool: result.tool,
2035
+ error: result.error,
2036
+ recoverable: true
2037
+ });
2038
+ toolResultsForModel.push({
2039
+ type: "tool_result",
2040
+ tool_use_id: result.callId,
2041
+ content: `Tool error: ${result.error}`
2042
+ });
2043
+ } else {
2044
+ yield pushEvent({
2045
+ type: "tool:completed",
2046
+ tool: result.tool,
2047
+ output: result.output,
2048
+ duration: now() - batchStart
2049
+ });
2050
+ toolResultsForModel.push({
2051
+ type: "tool_result",
2052
+ tool_use_id: result.callId,
2053
+ content: JSON.stringify(result.output ?? null)
2054
+ });
2055
+ }
2056
+ }
2057
+ messages.push({
2058
+ role: "assistant",
2059
+ content: modelResponse.text || `[tool calls: ${modelResponse.toolCalls.length}]`,
2060
+ metadata: { timestamp: now(), id: randomUUID2(), step }
2061
+ });
2062
+ messages.push({
2063
+ role: "tool",
2064
+ content: JSON.stringify(toolResultsForModel),
2065
+ metadata: { timestamp: now(), id: randomUUID2(), step }
2066
+ });
2067
+ yield pushEvent({
2068
+ type: "step:completed",
2069
+ step,
2070
+ duration: now() - stepStart
2071
+ });
2072
+ }
2073
+ yield {
2074
+ type: "run:error",
2075
+ runId,
2076
+ error: {
2077
+ code: "MAX_STEPS_EXCEEDED",
2078
+ message: `Run reached maximum of ${maxSteps} steps`
2079
+ }
2080
+ };
2081
+ }
2082
+ async runToCompletion(input) {
2083
+ const events = [];
2084
+ let runId = "";
2085
+ let finalResult;
2086
+ const messages = [...input.messages ?? []];
2087
+ messages.push({ role: "user", content: input.task });
2088
+ for await (const event of this.run(input)) {
2089
+ events.push(event);
2090
+ if (event.type === "run:started") {
2091
+ runId = event.runId;
2092
+ }
2093
+ if (event.type === "run:completed") {
2094
+ finalResult = event.result;
2095
+ messages.push({
2096
+ role: "assistant",
2097
+ content: event.result.response ?? ""
2098
+ });
2099
+ }
2100
+ if (event.type === "run:error") {
2101
+ finalResult = {
2102
+ status: "error",
2103
+ response: event.error.message,
2104
+ steps: 0,
2105
+ tokens: { input: 0, output: 0, cached: 0 },
2106
+ duration: 0
2107
+ };
2108
+ }
2109
+ }
2110
+ return {
2111
+ runId,
2112
+ events,
2113
+ messages,
2114
+ result: finalResult ?? {
2115
+ status: "error",
2116
+ response: "Run ended unexpectedly",
2117
+ steps: 0,
2118
+ tokens: { input: 0, output: 0, cached: 0 },
2119
+ duration: 0
2120
+ }
2121
+ };
2122
+ }
2123
+ };
2124
+
2125
+ // src/state.ts
2126
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
2127
+ import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
2128
+ import { basename as basename2, dirname as dirname4, resolve as resolve7 } from "path";
2129
+ import { homedir as homedir2 } from "os";
2130
+ var DEFAULT_OWNER = "local-owner";
2131
+ var CONVERSATIONS_STATE_KEY = "__poncho_conversations__";
2132
+ var LOCAL_CONVERSATIONS_FILE = "local-conversations.json";
2133
+ var LOCAL_STATE_FILE = "local-state.json";
2134
+ var getStateDirectory2 = () => {
2135
+ const cwd = process.cwd();
2136
+ const home = homedir2();
2137
+ const isServerless = process.env.VERCEL === "1" || process.env.VERCEL_ENV !== void 0 || process.env.VERCEL_URL !== void 0 || process.env.AWS_LAMBDA_FUNCTION_NAME !== void 0 || process.env.AWS_EXECUTION_ENV?.includes("AWS_Lambda") === true || process.env.LAMBDA_TASK_ROOT !== void 0 || process.env.NOW_REGION !== void 0 || cwd.startsWith("/var/task") || home.startsWith("/var/task") || process.env.SERVERLESS === "1";
2138
+ if (isServerless) {
2139
+ return "/tmp/.poncho/state";
2140
+ }
2141
+ return resolve7(homedir2(), ".poncho", "state");
2142
+ };
2143
+ var projectScopedFilePath = (workingDir, suffix) => {
2144
+ const projectName = basename2(workingDir).replace(/[^a-zA-Z0-9_-]+/g, "-") || "project";
2145
+ const projectHash = createHash2("sha256").update(workingDir).digest("hex").slice(0, 12);
2146
+ return resolve7(getStateDirectory2(), `${projectName}-${projectHash}-${suffix}`);
2147
+ };
2148
+ var normalizeTitle = (title) => {
2149
+ return title && title.trim().length > 0 ? title.trim() : "New conversation";
2150
+ };
2151
+ var InMemoryStateStore = class {
2152
+ store = /* @__PURE__ */ new Map();
2153
+ ttlMs;
2154
+ constructor(ttlSeconds) {
2155
+ this.ttlMs = typeof ttlSeconds === "number" ? ttlSeconds * 1e3 : void 0;
2156
+ }
2157
+ isExpired(state) {
2158
+ return typeof this.ttlMs === "number" && Date.now() - state.updatedAt > this.ttlMs;
2159
+ }
2160
+ async get(runId) {
2161
+ const state = this.store.get(runId);
2162
+ if (!state) {
2163
+ return void 0;
2164
+ }
2165
+ if (this.isExpired(state)) {
2166
+ this.store.delete(runId);
2167
+ return void 0;
2168
+ }
2169
+ return state;
2170
+ }
2171
+ async set(state) {
2172
+ this.store.set(state.runId, { ...state, updatedAt: Date.now() });
2173
+ }
2174
+ async delete(runId) {
2175
+ this.store.delete(runId);
2176
+ }
2177
+ };
2178
+ var UpstashStateStore = class {
2179
+ baseUrl;
2180
+ token;
2181
+ ttl;
2182
+ constructor(baseUrl, token, ttl) {
2183
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
2184
+ this.token = token;
2185
+ this.ttl = ttl;
2186
+ }
2187
+ headers() {
2188
+ return {
2189
+ Authorization: `Bearer ${this.token}`,
2190
+ "Content-Type": "application/json"
2191
+ };
2192
+ }
2193
+ async get(runId) {
2194
+ const response = await fetch(`${this.baseUrl}/get/${encodeURIComponent(runId)}`, {
2195
+ method: "POST",
2196
+ headers: this.headers()
2197
+ });
2198
+ if (!response.ok) {
2199
+ return void 0;
2200
+ }
2201
+ const payload = await response.json();
2202
+ if (!payload.result) {
2203
+ return void 0;
2204
+ }
2205
+ return JSON.parse(payload.result);
2206
+ }
2207
+ async set(state) {
2208
+ const serialized = JSON.stringify({ ...state, updatedAt: Date.now() });
2209
+ const path = typeof this.ttl === "number" ? `${this.baseUrl}/setex/${encodeURIComponent(state.runId)}/${Math.max(
2210
+ 1,
2211
+ this.ttl
2212
+ )}/${encodeURIComponent(serialized)}` : `${this.baseUrl}/set/${encodeURIComponent(state.runId)}/${encodeURIComponent(
2213
+ serialized
2214
+ )}`;
2215
+ await fetch(path, { method: "POST", headers: this.headers() });
2216
+ }
2217
+ async delete(runId) {
2218
+ await fetch(`${this.baseUrl}/del/${encodeURIComponent(runId)}`, {
2219
+ method: "POST",
2220
+ headers: this.headers()
2221
+ });
2222
+ }
2223
+ };
2224
+ var InMemoryConversationStore = class {
2225
+ conversations = /* @__PURE__ */ new Map();
2226
+ ttlMs;
2227
+ constructor(ttlSeconds) {
2228
+ this.ttlMs = typeof ttlSeconds === "number" ? ttlSeconds * 1e3 : void 0;
2229
+ }
2230
+ isExpired(updatedAt) {
2231
+ return typeof this.ttlMs === "number" && Date.now() - updatedAt > this.ttlMs;
2232
+ }
2233
+ purgeExpired() {
2234
+ for (const [conversationId, conversation] of this.conversations.entries()) {
2235
+ if (this.isExpired(conversation.updatedAt)) {
2236
+ this.conversations.delete(conversationId);
2237
+ }
2238
+ }
2239
+ }
2240
+ async list(ownerId = DEFAULT_OWNER) {
2241
+ this.purgeExpired();
2242
+ return Array.from(this.conversations.values()).filter((conversation) => conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
2243
+ }
2244
+ async get(conversationId) {
2245
+ this.purgeExpired();
2246
+ return this.conversations.get(conversationId);
2247
+ }
2248
+ async create(ownerId = DEFAULT_OWNER, title) {
2249
+ const now2 = Date.now();
2250
+ const conversation = {
2251
+ conversationId: globalThis.crypto?.randomUUID?.() ?? `${now2}-${Math.random()}`,
2252
+ title: normalizeTitle(title),
2253
+ messages: [],
2254
+ ownerId,
2255
+ tenantId: null,
2256
+ createdAt: now2,
2257
+ updatedAt: now2
2258
+ };
2259
+ this.conversations.set(conversation.conversationId, conversation);
2260
+ return conversation;
2261
+ }
2262
+ async update(conversation) {
2263
+ this.conversations.set(conversation.conversationId, {
2264
+ ...conversation,
2265
+ updatedAt: Date.now()
2266
+ });
2267
+ }
2268
+ async rename(conversationId, title) {
2269
+ const existing = await this.get(conversationId);
2270
+ if (!existing) {
2271
+ return void 0;
2272
+ }
2273
+ const updated = {
2274
+ ...existing,
2275
+ title: normalizeTitle(title || existing.title),
2276
+ updatedAt: Date.now()
2277
+ };
2278
+ this.conversations.set(conversationId, updated);
2279
+ return updated;
2280
+ }
2281
+ async delete(conversationId) {
2282
+ return this.conversations.delete(conversationId);
2283
+ }
2284
+ };
2285
+ var FileConversationStore = class {
2286
+ filePath;
2287
+ conversations = /* @__PURE__ */ new Map();
2288
+ loaded = false;
2289
+ writing = Promise.resolve();
2290
+ constructor(workingDir) {
2291
+ this.filePath = projectScopedFilePath(workingDir, LOCAL_CONVERSATIONS_FILE);
2292
+ }
2293
+ async ensureLoaded() {
2294
+ if (this.loaded) {
2295
+ return;
2296
+ }
2297
+ this.loaded = true;
2298
+ try {
2299
+ const raw = await readFile5(this.filePath, "utf8");
2300
+ const parsed = JSON.parse(raw);
2301
+ for (const conversation of parsed.conversations ?? []) {
2302
+ this.conversations.set(conversation.conversationId, conversation);
2303
+ }
2304
+ } catch {
2305
+ }
2306
+ }
2307
+ async persist() {
2308
+ const payload = {
2309
+ conversations: Array.from(this.conversations.values())
2310
+ };
2311
+ this.writing = this.writing.then(async () => {
2312
+ await mkdir3(dirname4(this.filePath), { recursive: true });
2313
+ await writeFile3(this.filePath, JSON.stringify(payload, null, 2), "utf8");
2314
+ });
2315
+ await this.writing;
2316
+ }
2317
+ async list(ownerId = DEFAULT_OWNER) {
2318
+ await this.ensureLoaded();
2319
+ return Array.from(this.conversations.values()).filter((conversation) => conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
2320
+ }
2321
+ async get(conversationId) {
2322
+ await this.ensureLoaded();
2323
+ return this.conversations.get(conversationId);
2324
+ }
2325
+ async create(ownerId = DEFAULT_OWNER, title) {
2326
+ await this.ensureLoaded();
2327
+ const now2 = Date.now();
2328
+ const conversation = {
2329
+ conversationId: randomUUID3(),
2330
+ title: normalizeTitle(title),
2331
+ messages: [],
2332
+ ownerId,
2333
+ tenantId: null,
2334
+ createdAt: now2,
2335
+ updatedAt: now2
2336
+ };
2337
+ this.conversations.set(conversation.conversationId, conversation);
2338
+ await this.persist();
2339
+ return conversation;
2340
+ }
2341
+ async update(conversation) {
2342
+ await this.ensureLoaded();
2343
+ this.conversations.set(conversation.conversationId, {
2344
+ ...conversation,
2345
+ updatedAt: Date.now()
2346
+ });
2347
+ await this.persist();
2348
+ }
2349
+ async rename(conversationId, title) {
2350
+ await this.ensureLoaded();
2351
+ const existing = this.conversations.get(conversationId);
2352
+ if (!existing) {
2353
+ return void 0;
2354
+ }
2355
+ const updated = {
2356
+ ...existing,
2357
+ title: normalizeTitle(title || existing.title),
2358
+ updatedAt: Date.now()
2359
+ };
2360
+ this.conversations.set(conversationId, updated);
2361
+ await this.persist();
2362
+ return updated;
2363
+ }
2364
+ async delete(conversationId) {
2365
+ await this.ensureLoaded();
2366
+ const removed = this.conversations.delete(conversationId);
2367
+ if (removed) {
2368
+ await this.persist();
2369
+ }
2370
+ return removed;
2371
+ }
2372
+ };
2373
+ var FileStateStore = class {
2374
+ filePath;
2375
+ states = /* @__PURE__ */ new Map();
2376
+ ttlMs;
2377
+ loaded = false;
2378
+ writing = Promise.resolve();
2379
+ constructor(workingDir, ttlSeconds) {
2380
+ this.filePath = projectScopedFilePath(workingDir, LOCAL_STATE_FILE);
2381
+ this.ttlMs = typeof ttlSeconds === "number" ? ttlSeconds * 1e3 : void 0;
2382
+ }
2383
+ isExpired(state) {
2384
+ return typeof this.ttlMs === "number" && Date.now() - state.updatedAt > this.ttlMs;
2385
+ }
2386
+ async ensureLoaded() {
2387
+ if (this.loaded) {
2388
+ return;
2389
+ }
2390
+ this.loaded = true;
2391
+ try {
2392
+ const raw = await readFile5(this.filePath, "utf8");
2393
+ const parsed = JSON.parse(raw);
2394
+ for (const state of parsed.states ?? []) {
2395
+ this.states.set(state.runId, state);
2396
+ }
2397
+ } catch {
2398
+ }
2399
+ }
2400
+ async persist() {
2401
+ const payload = {
2402
+ states: Array.from(this.states.values())
2403
+ };
2404
+ this.writing = this.writing.then(async () => {
2405
+ await mkdir3(dirname4(this.filePath), { recursive: true });
2406
+ await writeFile3(this.filePath, JSON.stringify(payload, null, 2), "utf8");
2407
+ });
2408
+ await this.writing;
2409
+ }
2410
+ async get(runId) {
2411
+ await this.ensureLoaded();
2412
+ const state = this.states.get(runId);
2413
+ if (!state) {
2414
+ return void 0;
2415
+ }
2416
+ if (this.isExpired(state)) {
2417
+ this.states.delete(runId);
2418
+ await this.persist();
2419
+ return void 0;
2420
+ }
2421
+ return state;
2422
+ }
2423
+ async set(state) {
2424
+ await this.ensureLoaded();
2425
+ this.states.set(state.runId, { ...state, updatedAt: Date.now() });
2426
+ await this.persist();
2427
+ }
2428
+ async delete(runId) {
2429
+ await this.ensureLoaded();
2430
+ this.states.delete(runId);
2431
+ await this.persist();
2432
+ }
2433
+ };
2434
+ var KeyValueConversationStoreBase = class {
2435
+ memoryFallback;
2436
+ ttl;
2437
+ constructor(ttl) {
2438
+ this.ttl = ttl;
2439
+ this.memoryFallback = new InMemoryConversationStore(ttl);
2440
+ }
2441
+ async readAllConversations() {
2442
+ try {
2443
+ const raw = await this.getRaw(CONVERSATIONS_STATE_KEY);
2444
+ if (!raw) {
2445
+ return [];
2446
+ }
2447
+ const parsed = JSON.parse(raw);
2448
+ return Array.isArray(parsed.conversations) ? parsed.conversations : [];
2449
+ } catch {
2450
+ return await this.memoryFallback.list(DEFAULT_OWNER);
2451
+ }
2452
+ }
2453
+ async writeAllConversations(conversations) {
2454
+ const payload = JSON.stringify({ conversations });
2455
+ try {
2456
+ if (typeof this.ttl === "number") {
2457
+ await this.setRawWithTtl(CONVERSATIONS_STATE_KEY, payload, Math.max(1, this.ttl));
2458
+ } else {
2459
+ await this.setRaw(CONVERSATIONS_STATE_KEY, payload);
2460
+ }
2461
+ } catch {
2462
+ for (const conversation of conversations) {
2463
+ await this.memoryFallback.update(conversation);
2464
+ }
2465
+ }
2466
+ }
2467
+ async list(ownerId = DEFAULT_OWNER) {
2468
+ const conversations = await this.readAllConversations();
2469
+ return conversations.filter((conversation) => conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
2470
+ }
2471
+ async get(conversationId) {
2472
+ const conversations = await this.readAllConversations();
2473
+ return conversations.find((conversation) => conversation.conversationId === conversationId);
2474
+ }
2475
+ async create(ownerId = DEFAULT_OWNER, title) {
2476
+ const now2 = Date.now();
2477
+ const conversation = {
2478
+ conversationId: globalThis.crypto?.randomUUID?.() ?? `${now2}-${Math.random()}`,
2479
+ title: normalizeTitle(title),
2480
+ messages: [],
2481
+ ownerId,
2482
+ tenantId: null,
2483
+ createdAt: now2,
2484
+ updatedAt: now2
2485
+ };
2486
+ const conversations = await this.readAllConversations();
2487
+ conversations.push(conversation);
2488
+ await this.writeAllConversations(conversations);
2489
+ return conversation;
2490
+ }
2491
+ async update(conversation) {
2492
+ const conversations = await this.readAllConversations();
2493
+ const next = conversations.map(
2494
+ (item) => item.conversationId === conversation.conversationId ? { ...conversation, updatedAt: Date.now() } : item
2495
+ );
2496
+ await this.writeAllConversations(next);
2497
+ }
2498
+ async rename(conversationId, title) {
2499
+ const conversations = await this.readAllConversations();
2500
+ let updated;
2501
+ const next = conversations.map((item) => {
2502
+ if (item.conversationId !== conversationId) {
2503
+ return item;
2504
+ }
2505
+ updated = {
2506
+ ...item,
2507
+ title: normalizeTitle(title || item.title),
2508
+ updatedAt: Date.now()
2509
+ };
2510
+ return updated;
2511
+ });
2512
+ if (!updated) {
2513
+ return void 0;
2514
+ }
2515
+ await this.writeAllConversations(next);
2516
+ return updated;
2517
+ }
2518
+ async delete(conversationId) {
2519
+ const conversations = await this.readAllConversations();
2520
+ const next = conversations.filter((item) => item.conversationId !== conversationId);
2521
+ if (next.length === conversations.length) {
2522
+ return false;
2523
+ }
2524
+ if (next.length === 0) {
2525
+ try {
2526
+ await this.delRaw(CONVERSATIONS_STATE_KEY);
2527
+ } catch {
2528
+ await this.writeAllConversations(next);
2529
+ }
2530
+ return true;
2531
+ }
2532
+ await this.writeAllConversations(next);
2533
+ return true;
2534
+ }
2535
+ };
2536
+ var UpstashConversationStore = class extends KeyValueConversationStoreBase {
2537
+ baseUrl;
2538
+ token;
2539
+ constructor(baseUrl, token, ttl) {
2540
+ super(ttl);
2541
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
2542
+ this.token = token;
2543
+ }
2544
+ headers() {
2545
+ return {
2546
+ Authorization: `Bearer ${this.token}`,
2547
+ "Content-Type": "application/json"
2548
+ };
2549
+ }
2550
+ async getRaw(key) {
2551
+ const response = await fetch(`${this.baseUrl}/get/${encodeURIComponent(key)}`, {
2552
+ method: "POST",
2553
+ headers: this.headers()
2554
+ });
2555
+ if (!response.ok) {
2556
+ return void 0;
2557
+ }
2558
+ const payload = await response.json();
2559
+ return payload.result ?? void 0;
2560
+ }
2561
+ async setRaw(key, value) {
2562
+ await fetch(
2563
+ `${this.baseUrl}/set/${encodeURIComponent(key)}/${encodeURIComponent(value)}`,
2564
+ { method: "POST", headers: this.headers() }
2565
+ );
2566
+ }
2567
+ async setRawWithTtl(key, value, ttl) {
2568
+ await fetch(
2569
+ `${this.baseUrl}/setex/${encodeURIComponent(key)}/${Math.max(1, ttl)}/${encodeURIComponent(
2570
+ value
2571
+ )}`,
2572
+ { method: "POST", headers: this.headers() }
2573
+ );
2574
+ }
2575
+ async delRaw(key) {
2576
+ await fetch(`${this.baseUrl}/del/${encodeURIComponent(key)}`, {
2577
+ method: "POST",
2578
+ headers: this.headers()
2579
+ });
2580
+ }
2581
+ };
2582
+ var RedisLikeStateStore = class {
2583
+ memoryFallback;
2584
+ ttl;
2585
+ clientPromise;
2586
+ constructor(url, ttl) {
2587
+ this.ttl = ttl;
2588
+ this.memoryFallback = new InMemoryStateStore(ttl);
2589
+ this.clientPromise = (async () => {
2590
+ try {
2591
+ const redisModule = await import("redis");
2592
+ const client = redisModule.createClient({ url });
2593
+ await client.connect();
2594
+ return client;
2595
+ } catch {
2596
+ return void 0;
2597
+ }
2598
+ })();
2599
+ }
2600
+ async get(runId) {
2601
+ const client = await this.clientPromise;
2602
+ if (!client) {
2603
+ return await this.memoryFallback.get(runId);
2604
+ }
2605
+ const raw = await client.get(runId);
2606
+ return raw ? JSON.parse(raw) : void 0;
2607
+ }
2608
+ async set(state) {
2609
+ const client = await this.clientPromise;
2610
+ if (!client) {
2611
+ await this.memoryFallback.set(state);
2612
+ return;
2613
+ }
2614
+ const serialized = JSON.stringify({ ...state, updatedAt: Date.now() });
2615
+ if (typeof this.ttl === "number") {
2616
+ await client.set(state.runId, serialized, { EX: Math.max(1, this.ttl) });
2617
+ return;
2618
+ }
2619
+ await client.set(state.runId, serialized);
2620
+ }
2621
+ async delete(runId) {
2622
+ const client = await this.clientPromise;
2623
+ if (!client) {
2624
+ await this.memoryFallback.delete(runId);
2625
+ return;
2626
+ }
2627
+ await client.del(runId);
2628
+ }
2629
+ };
2630
+ var RedisLikeConversationStore = class extends KeyValueConversationStoreBase {
2631
+ clientPromise;
2632
+ constructor(url, ttl) {
2633
+ super(ttl);
2634
+ this.clientPromise = (async () => {
2635
+ try {
2636
+ const redisModule = await import("redis");
2637
+ const client = redisModule.createClient({ url });
2638
+ await client.connect();
2639
+ return client;
2640
+ } catch {
2641
+ return void 0;
2642
+ }
2643
+ })();
2644
+ }
2645
+ async getRaw(key) {
2646
+ const client = await this.clientPromise;
2647
+ if (!client) {
2648
+ throw new Error("Redis unavailable");
2649
+ }
2650
+ const value = await client.get(key);
2651
+ return value ?? void 0;
2652
+ }
2653
+ async setRaw(key, value) {
2654
+ const client = await this.clientPromise;
2655
+ if (!client) {
2656
+ throw new Error("Redis unavailable");
2657
+ }
2658
+ await client.set(key, value);
2659
+ }
2660
+ async setRawWithTtl(key, value, ttl) {
2661
+ const client = await this.clientPromise;
2662
+ if (!client) {
2663
+ throw new Error("Redis unavailable");
2664
+ }
2665
+ await client.set(key, value, { EX: Math.max(1, ttl) });
2666
+ }
2667
+ async delRaw(key) {
2668
+ const client = await this.clientPromise;
2669
+ if (!client) {
2670
+ throw new Error("Redis unavailable");
2671
+ }
2672
+ await client.del(key);
2673
+ }
2674
+ };
2675
+ var DynamoDbStateStore = class {
2676
+ memoryFallback;
2677
+ table;
2678
+ ttl;
2679
+ clientPromise;
2680
+ constructor(table, region, ttl) {
2681
+ this.table = table;
2682
+ this.ttl = ttl;
2683
+ this.memoryFallback = new InMemoryStateStore(ttl);
2684
+ this.clientPromise = (async () => {
2685
+ try {
2686
+ const module = await import("@aws-sdk/client-dynamodb");
2687
+ return {
2688
+ send: module.DynamoDBClient ? new module.DynamoDBClient({ region }).send.bind(
2689
+ new module.DynamoDBClient({ region })
2690
+ ) : async () => ({}),
2691
+ GetItemCommand: module.GetItemCommand,
2692
+ PutItemCommand: module.PutItemCommand,
2693
+ DeleteItemCommand: module.DeleteItemCommand
2694
+ };
2695
+ } catch {
2696
+ return void 0;
2697
+ }
2698
+ })();
2699
+ }
2700
+ async get(runId) {
2701
+ const client = await this.clientPromise;
2702
+ if (!client) {
2703
+ return await this.memoryFallback.get(runId);
2704
+ }
2705
+ const result = await client.send(
2706
+ new client.GetItemCommand({
2707
+ TableName: this.table,
2708
+ Key: { runId: { S: runId } }
2709
+ })
2710
+ );
2711
+ const raw = result.Item?.value?.S;
2712
+ return raw ? JSON.parse(raw) : void 0;
2713
+ }
2714
+ async set(state) {
2715
+ const client = await this.clientPromise;
2716
+ if (!client) {
2717
+ await this.memoryFallback.set(state);
2718
+ return;
2719
+ }
2720
+ const updatedState = { ...state, updatedAt: Date.now() };
2721
+ const ttlEpoch = typeof this.ttl === "number" ? Math.floor(Date.now() / 1e3) + Math.max(1, this.ttl) : void 0;
2722
+ await client.send(
2723
+ new client.PutItemCommand({
2724
+ TableName: this.table,
2725
+ Item: {
2726
+ runId: { S: state.runId },
2727
+ value: { S: JSON.stringify(updatedState) },
2728
+ ...typeof ttlEpoch === "number" ? { ttl: { N: String(ttlEpoch) } } : {}
2729
+ }
2730
+ })
2731
+ );
2732
+ }
2733
+ async delete(runId) {
2734
+ const client = await this.clientPromise;
2735
+ if (!client) {
2736
+ await this.memoryFallback.delete(runId);
2737
+ return;
2738
+ }
2739
+ await client.send(
2740
+ new client.DeleteItemCommand({
2741
+ TableName: this.table,
2742
+ Key: { runId: { S: runId } }
2743
+ })
2744
+ );
2745
+ }
2746
+ };
2747
+ var DynamoDbConversationStore = class extends KeyValueConversationStoreBase {
2748
+ table;
2749
+ clientPromise;
2750
+ constructor(table, region, ttl) {
2751
+ super(ttl);
2752
+ this.table = table;
2753
+ this.clientPromise = (async () => {
2754
+ try {
2755
+ const module = await import("@aws-sdk/client-dynamodb");
2756
+ const client = new module.DynamoDBClient({ region });
2757
+ return {
2758
+ send: client.send.bind(client),
2759
+ GetItemCommand: module.GetItemCommand,
2760
+ PutItemCommand: module.PutItemCommand,
2761
+ DeleteItemCommand: module.DeleteItemCommand
2762
+ };
2763
+ } catch {
2764
+ return void 0;
2765
+ }
2766
+ })();
2767
+ }
2768
+ async getRaw(key) {
2769
+ const client = await this.clientPromise;
2770
+ if (!client) {
2771
+ throw new Error("DynamoDB unavailable");
2772
+ }
2773
+ const result = await client.send(
2774
+ new client.GetItemCommand({
2775
+ TableName: this.table,
2776
+ Key: { runId: { S: key } }
2777
+ })
2778
+ );
2779
+ return result.Item?.value?.S;
2780
+ }
2781
+ async setRaw(key, value) {
2782
+ const client = await this.clientPromise;
2783
+ if (!client) {
2784
+ throw new Error("DynamoDB unavailable");
2785
+ }
2786
+ await client.send(
2787
+ new client.PutItemCommand({
2788
+ TableName: this.table,
2789
+ Item: {
2790
+ runId: { S: key },
2791
+ value: { S: value }
2792
+ }
2793
+ })
2794
+ );
2795
+ }
2796
+ async setRawWithTtl(key, value, ttl) {
2797
+ const client = await this.clientPromise;
2798
+ if (!client) {
2799
+ throw new Error("DynamoDB unavailable");
2800
+ }
2801
+ const ttlEpoch = Math.floor(Date.now() / 1e3) + Math.max(1, ttl);
2802
+ await client.send(
2803
+ new client.PutItemCommand({
2804
+ TableName: this.table,
2805
+ Item: {
2806
+ runId: { S: key },
2807
+ value: { S: value },
2808
+ ttl: { N: String(ttlEpoch) }
2809
+ }
2810
+ })
2811
+ );
2812
+ }
2813
+ async delRaw(key) {
2814
+ const client = await this.clientPromise;
2815
+ if (!client) {
2816
+ throw new Error("DynamoDB unavailable");
2817
+ }
2818
+ await client.send(
2819
+ new client.DeleteItemCommand({
2820
+ TableName: this.table,
2821
+ Key: { runId: { S: key } }
2822
+ })
2823
+ );
2824
+ }
2825
+ };
2826
+ var createStateStore = (config, options) => {
2827
+ const provider = config?.provider ?? "local";
2828
+ const ttl = config?.ttl;
2829
+ const workingDir = options?.workingDir ?? process.cwd();
2830
+ if (provider === "local") {
2831
+ return new FileStateStore(workingDir, ttl);
2832
+ }
2833
+ if (provider === "memory") {
2834
+ return new InMemoryStateStore(ttl);
2835
+ }
2836
+ if (provider === "upstash") {
2837
+ const url = config?.url ?? process.env.UPSTASH_REDIS_REST_URL ?? "";
2838
+ const token = config?.token ?? process.env.UPSTASH_REDIS_REST_TOKEN ?? "";
2839
+ if (url && token) {
2840
+ return new UpstashStateStore(url, token, ttl);
2841
+ }
2842
+ return new InMemoryStateStore(ttl);
2843
+ }
2844
+ if (provider === "redis") {
2845
+ const url = config?.url ?? process.env.REDIS_URL ?? "";
2846
+ if (url) {
2847
+ return new RedisLikeStateStore(url, ttl);
2848
+ }
2849
+ return new InMemoryStateStore(ttl);
2850
+ }
2851
+ if (provider === "dynamodb") {
2852
+ const table = config?.table ?? process.env.PONCHO_DYNAMODB_TABLE ?? "";
2853
+ if (table) {
2854
+ return new DynamoDbStateStore(table, config?.region, ttl);
2855
+ }
2856
+ return new InMemoryStateStore(ttl);
2857
+ }
2858
+ return new InMemoryStateStore(ttl);
2859
+ };
2860
+ var createConversationStore = (config, options) => {
2861
+ const provider = config?.provider ?? "local";
2862
+ const ttl = config?.ttl;
2863
+ const workingDir = options?.workingDir ?? process.cwd();
2864
+ if (provider === "local") {
2865
+ return new FileConversationStore(workingDir);
2866
+ }
2867
+ if (provider === "memory") {
2868
+ return new InMemoryConversationStore(ttl);
2869
+ }
2870
+ if (provider === "upstash") {
2871
+ const url = config?.url ?? (process.env.UPSTASH_REDIS_REST_URL || process.env.KV_REST_API_URL || "");
2872
+ const token = config?.token ?? (process.env.UPSTASH_REDIS_REST_TOKEN || process.env.KV_REST_API_TOKEN || "");
2873
+ if (url && token) {
2874
+ return new UpstashConversationStore(url, token, ttl);
2875
+ }
2876
+ return new InMemoryConversationStore(ttl);
2877
+ }
2878
+ if (provider === "redis") {
2879
+ const url = config?.url ?? process.env.REDIS_URL ?? "";
2880
+ if (url) {
2881
+ return new RedisLikeConversationStore(url, ttl);
2882
+ }
2883
+ return new InMemoryConversationStore(ttl);
2884
+ }
2885
+ if (provider === "dynamodb") {
2886
+ const table = config?.table ?? process.env.PONCHO_DYNAMODB_TABLE ?? "";
2887
+ if (table) {
2888
+ return new DynamoDbConversationStore(table, config?.region, ttl);
2889
+ }
2890
+ return new InMemoryConversationStore(ttl);
2891
+ }
2892
+ return new InMemoryConversationStore(ttl);
2893
+ };
2894
+
2895
+ // src/telemetry.ts
2896
+ var TelemetryEmitter = class {
2897
+ config;
2898
+ constructor(config) {
2899
+ this.config = config;
2900
+ }
2901
+ async emit(event) {
2902
+ if (this.config?.enabled === false) {
2903
+ return;
2904
+ }
2905
+ if (this.config?.handler) {
2906
+ await this.config.handler(event);
2907
+ return;
2908
+ }
2909
+ if (this.config?.otlp) {
2910
+ await this.sendOtlp(event);
2911
+ }
2912
+ if (this.config?.latitude?.apiKey) {
2913
+ await this.sendLatitude(event);
2914
+ }
2915
+ process.stdout.write(`[event] ${event.type} ${JSON.stringify(event)}
2916
+ `);
2917
+ }
2918
+ async sendOtlp(event) {
2919
+ const endpoint = this.config?.otlp;
2920
+ if (!endpoint) {
2921
+ return;
2922
+ }
2923
+ try {
2924
+ await fetch(endpoint, {
2925
+ method: "POST",
2926
+ headers: { "Content-Type": "application/json" },
2927
+ body: JSON.stringify({
2928
+ resourceLogs: [
2929
+ {
2930
+ scopeLogs: [
2931
+ {
2932
+ logRecords: [
2933
+ {
2934
+ timeUnixNano: String(Date.now() * 1e6),
2935
+ severityText: "INFO",
2936
+ body: { stringValue: event.type },
2937
+ attributes: [
2938
+ {
2939
+ key: "event.payload",
2940
+ value: { stringValue: JSON.stringify(event) }
2941
+ }
2942
+ ]
2943
+ }
2944
+ ]
2945
+ }
2946
+ ]
2947
+ }
2948
+ ]
2949
+ })
2950
+ });
2951
+ } catch {
2952
+ }
2953
+ }
2954
+ async sendLatitude(event) {
2955
+ const apiKey = this.config?.latitude?.apiKey;
2956
+ if (!apiKey) {
2957
+ return;
2958
+ }
2959
+ const projectId = this.config?.latitude?.projectId ?? process.env.LATITUDE_PROJECT_ID;
2960
+ const path = this.config?.latitude?.path ?? process.env.LATITUDE_PATH;
2961
+ const documentPath = this.config?.latitude?.documentPath ?? process.env.LATITUDE_DOCUMENT_PATH;
2962
+ try {
2963
+ await fetch("https://api.latitude.so/v1/telemetry/events", {
2964
+ method: "POST",
2965
+ headers: {
2966
+ "Content-Type": "application/json",
2967
+ Authorization: `Bearer ${apiKey}`
2968
+ },
2969
+ body: JSON.stringify({
2970
+ type: event.type,
2971
+ payload: event,
2972
+ timestamp: Date.now(),
2973
+ projectId,
2974
+ path,
2975
+ documentPath
2976
+ })
2977
+ });
2978
+ } catch {
2979
+ }
2980
+ }
2981
+ };
2982
+
2983
+ // src/index.ts
2984
+ import { defineTool as defineTool4 } from "@poncho-ai/sdk";
2985
+ export {
2986
+ AgentHarness,
2987
+ InMemoryConversationStore,
2988
+ InMemoryStateStore,
2989
+ LatitudeCapture,
2990
+ LocalMcpBridge,
2991
+ OpenAiModelClient,
2992
+ TelemetryEmitter,
2993
+ ToolDispatcher,
2994
+ buildSkillContextWindow,
2995
+ createConversationStore,
2996
+ createDefaultTools,
2997
+ createMemoryStore,
2998
+ createMemoryTools,
2999
+ createModelClient,
3000
+ createSkillTools,
3001
+ createStateStore,
3002
+ createWriteTool,
3003
+ defineTool4 as defineTool,
3004
+ loadPonchoConfig,
3005
+ loadSkillContext,
3006
+ loadSkillInstructions,
3007
+ loadSkillMetadata,
3008
+ parseAgentFile,
3009
+ parseAgentMarkdown,
3010
+ readSkillResource,
3011
+ renderAgentPrompt,
3012
+ resolveMemoryConfig,
3013
+ resolveSkillDirs,
3014
+ resolveStateConfig
3015
+ };