@poncho-ai/harness 0.11.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +17 -0
- package/dist/index.d.ts +60 -1
- package/dist/index.js +660 -134
- package/package.json +2 -2
- package/src/agent-parser.ts +76 -0
- package/src/config.ts +10 -0
- package/src/harness.ts +215 -24
- package/src/index.ts +1 -0
- package/src/upload-store.ts +387 -0
- package/test/agent-parser.test.ts +118 -0
package/dist/index.js
CHANGED
|
@@ -67,6 +67,59 @@ var matchesSlashPattern = (value, pattern) => {
|
|
|
67
67
|
var FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
|
|
68
68
|
var asRecord = (value) => typeof value === "object" && value !== null ? value : {};
|
|
69
69
|
var asNumberOrUndefined = (value) => typeof value === "number" ? value : void 0;
|
|
70
|
+
var CRON_EXPRESSION_PATTERN = /^(\S+\s+){4}\S+$/;
|
|
71
|
+
var validateCronExpression = (expr, path) => {
|
|
72
|
+
if (!CRON_EXPRESSION_PATTERN.test(expr.trim())) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Invalid cron expression at ${path}: "${expr}". Expected 5-field cron format (minute hour day month weekday).`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var KNOWN_TIMEZONES = (() => {
|
|
79
|
+
try {
|
|
80
|
+
return new Set(Intl.supportedValuesOf("timeZone"));
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
var validateTimezone = (tz, path) => {
|
|
86
|
+
if (KNOWN_TIMEZONES && !KNOWN_TIMEZONES.has(tz)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Invalid timezone at ${path}: "${tz}". Expected an IANA timezone string (e.g. "America/New_York", "UTC").`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var parseCronJobs = (value) => {
|
|
93
|
+
const raw = asRecord(value);
|
|
94
|
+
const keys = Object.keys(raw);
|
|
95
|
+
if (keys.length === 0) return void 0;
|
|
96
|
+
const jobs = {};
|
|
97
|
+
for (const jobName of keys) {
|
|
98
|
+
const jobValue = asRecord(raw[jobName]);
|
|
99
|
+
const path = `AGENT.md frontmatter cron.${jobName}`;
|
|
100
|
+
if (typeof jobValue.schedule !== "string" || jobValue.schedule.trim() === "") {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Invalid ${path}: "schedule" is required and must be a non-empty string.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (typeof jobValue.task !== "string" || jobValue.task.trim() === "") {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Invalid ${path}: "task" is required and must be a non-empty string.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
validateCronExpression(jobValue.schedule, path);
|
|
111
|
+
const timezone = typeof jobValue.timezone === "string" && jobValue.timezone.trim() ? jobValue.timezone.trim() : void 0;
|
|
112
|
+
if (timezone) {
|
|
113
|
+
validateTimezone(timezone, path);
|
|
114
|
+
}
|
|
115
|
+
jobs[jobName] = {
|
|
116
|
+
schedule: jobValue.schedule.trim(),
|
|
117
|
+
task: jobValue.task,
|
|
118
|
+
timezone
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return jobs;
|
|
122
|
+
};
|
|
70
123
|
var parseAgentMarkdown = (content) => {
|
|
71
124
|
const match = content.match(FRONTMATTER_PATTERN);
|
|
72
125
|
if (!match) {
|
|
@@ -144,7 +197,8 @@ var parseAgentMarkdown = (content) => {
|
|
|
144
197
|
approvalRequired: approvalRequired.mcp.length > 0 || approvalRequired.scripts.length > 0 ? {
|
|
145
198
|
mcp: approvalRequired.mcp.length > 0 ? approvalRequired.mcp : void 0,
|
|
146
199
|
scripts: approvalRequired.scripts.length > 0 ? approvalRequired.scripts : void 0
|
|
147
|
-
} : void 0
|
|
200
|
+
} : void 0,
|
|
201
|
+
cron: parseCronJobs(parsed.cron)
|
|
148
202
|
};
|
|
149
203
|
return {
|
|
150
204
|
frontmatter,
|
|
@@ -412,10 +466,313 @@ var createWriteTool = (workingDir) => defineTool({
|
|
|
412
466
|
|
|
413
467
|
// src/harness.ts
|
|
414
468
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
469
|
+
import { getTextContent } from "@poncho-ai/sdk";
|
|
470
|
+
|
|
471
|
+
// src/upload-store.ts
|
|
472
|
+
import { createHash as createHash2 } from "crypto";
|
|
473
|
+
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile3, rm } from "fs/promises";
|
|
474
|
+
import { createRequire } from "module";
|
|
475
|
+
import { resolve as resolve5 } from "path";
|
|
476
|
+
var tryImport = async (mod, workingDir) => {
|
|
477
|
+
try {
|
|
478
|
+
return await import(
|
|
479
|
+
/* webpackIgnore: true */
|
|
480
|
+
mod
|
|
481
|
+
);
|
|
482
|
+
} catch {
|
|
483
|
+
if (workingDir) {
|
|
484
|
+
const require2 = createRequire(resolve5(workingDir, "package.json"));
|
|
485
|
+
const resolved = require2.resolve(mod);
|
|
486
|
+
return await import(
|
|
487
|
+
/* webpackIgnore: true */
|
|
488
|
+
resolved
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
throw new Error(`Cannot find module "${mod}"`);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
var PONCHO_UPLOAD_SCHEME = "poncho-upload://";
|
|
495
|
+
var CachedUploadStore = class {
|
|
496
|
+
inner;
|
|
497
|
+
cache = /* @__PURE__ */ new Map();
|
|
498
|
+
maxEntries;
|
|
499
|
+
ttlMs;
|
|
500
|
+
constructor(inner, maxEntries = 64, ttlMs = 10 * 60 * 1e3) {
|
|
501
|
+
this.inner = inner;
|
|
502
|
+
this.maxEntries = maxEntries;
|
|
503
|
+
this.ttlMs = ttlMs;
|
|
504
|
+
}
|
|
505
|
+
async put(key, data, mediaType) {
|
|
506
|
+
const ref = `${PONCHO_UPLOAD_SCHEME}${key}`;
|
|
507
|
+
const now2 = Date.now();
|
|
508
|
+
this.cache.set(ref, { data, ts: now2 });
|
|
509
|
+
this.cache.set(key, { data, ts: now2 });
|
|
510
|
+
this.evict();
|
|
511
|
+
this.inner.put(key, data, mediaType).catch((err) => {
|
|
512
|
+
console.error("[poncho] background upload failed:", err instanceof Error ? err.message : err);
|
|
513
|
+
});
|
|
514
|
+
return ref;
|
|
515
|
+
}
|
|
516
|
+
async get(urlOrKey) {
|
|
517
|
+
const cached = this.cache.get(urlOrKey);
|
|
518
|
+
if (cached && Date.now() - cached.ts < this.ttlMs) {
|
|
519
|
+
return cached.data;
|
|
520
|
+
}
|
|
521
|
+
return this.inner.get(urlOrKey);
|
|
522
|
+
}
|
|
523
|
+
async delete(urlOrKey) {
|
|
524
|
+
this.cache.delete(urlOrKey);
|
|
525
|
+
return this.inner.delete(urlOrKey);
|
|
526
|
+
}
|
|
527
|
+
evict() {
|
|
528
|
+
if (this.cache.size <= this.maxEntries) return;
|
|
529
|
+
let oldest;
|
|
530
|
+
let oldestTs = Infinity;
|
|
531
|
+
for (const [k, v] of this.cache) {
|
|
532
|
+
if (v.ts < oldestTs) {
|
|
533
|
+
oldestTs = v.ts;
|
|
534
|
+
oldest = k;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (oldest) this.cache.delete(oldest);
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
var deriveUploadKey = (data, mediaType) => {
|
|
541
|
+
const hash = createHash2("sha256").update(data).digest("hex").slice(0, 24);
|
|
542
|
+
const ext = mimeToExt(mediaType);
|
|
543
|
+
return `${hash}${ext}`;
|
|
544
|
+
};
|
|
545
|
+
var MIME_EXT_MAP = {
|
|
546
|
+
"image/jpeg": ".jpg",
|
|
547
|
+
"image/png": ".png",
|
|
548
|
+
"image/gif": ".gif",
|
|
549
|
+
"image/webp": ".webp",
|
|
550
|
+
"image/svg+xml": ".svg",
|
|
551
|
+
"application/pdf": ".pdf",
|
|
552
|
+
"text/plain": ".txt",
|
|
553
|
+
"text/csv": ".csv",
|
|
554
|
+
"text/html": ".html",
|
|
555
|
+
"application/json": ".json",
|
|
556
|
+
"video/mp4": ".mp4",
|
|
557
|
+
"video/webm": ".webm",
|
|
558
|
+
"audio/mpeg": ".mp3",
|
|
559
|
+
"audio/wav": ".wav"
|
|
560
|
+
};
|
|
561
|
+
var mimeToExt = (mediaType) => MIME_EXT_MAP[mediaType] ?? `.${mediaType.split("/").pop() ?? "bin"}`;
|
|
562
|
+
var LocalUploadStore = class {
|
|
563
|
+
uploadsDir;
|
|
564
|
+
constructor(workingDir) {
|
|
565
|
+
this.uploadsDir = resolve5(workingDir, ".poncho", "uploads");
|
|
566
|
+
}
|
|
567
|
+
async put(_key, data, mediaType) {
|
|
568
|
+
const key = deriveUploadKey(data, mediaType);
|
|
569
|
+
const filePath = resolve5(this.uploadsDir, key);
|
|
570
|
+
await mkdir2(this.uploadsDir, { recursive: true });
|
|
571
|
+
await writeFile3(filePath, data);
|
|
572
|
+
return `${PONCHO_UPLOAD_SCHEME}${key}`;
|
|
573
|
+
}
|
|
574
|
+
async get(urlOrKey) {
|
|
575
|
+
const key = urlOrKey.startsWith(PONCHO_UPLOAD_SCHEME) ? urlOrKey.slice(PONCHO_UPLOAD_SCHEME.length) : urlOrKey;
|
|
576
|
+
return readFile4(resolve5(this.uploadsDir, key));
|
|
577
|
+
}
|
|
578
|
+
async delete(urlOrKey) {
|
|
579
|
+
const key = urlOrKey.startsWith(PONCHO_UPLOAD_SCHEME) ? urlOrKey.slice(PONCHO_UPLOAD_SCHEME.length) : urlOrKey;
|
|
580
|
+
await rm(resolve5(this.uploadsDir, key), { force: true });
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
var VercelBlobUploadStore = class {
|
|
584
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
585
|
+
sdk;
|
|
586
|
+
workingDir;
|
|
587
|
+
access;
|
|
588
|
+
constructor(workingDir, access3 = "public") {
|
|
589
|
+
this.workingDir = workingDir;
|
|
590
|
+
this.access = access3;
|
|
591
|
+
}
|
|
592
|
+
async loadSdk() {
|
|
593
|
+
if (this.sdk) return this.sdk;
|
|
594
|
+
try {
|
|
595
|
+
this.sdk = await tryImport("@vercel/blob", this.workingDir);
|
|
596
|
+
return this.sdk;
|
|
597
|
+
} catch {
|
|
598
|
+
throw new Error(
|
|
599
|
+
'uploads: vercel-blob provider requires the "@vercel/blob" package. Install it with: pnpm add @vercel/blob'
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
async put(key, data, mediaType) {
|
|
604
|
+
const sdk = await this.loadSdk();
|
|
605
|
+
await sdk.put(key, data, {
|
|
606
|
+
access: this.access,
|
|
607
|
+
contentType: mediaType,
|
|
608
|
+
addRandomSuffix: false,
|
|
609
|
+
allowOverwrite: true
|
|
610
|
+
});
|
|
611
|
+
return `${PONCHO_UPLOAD_SCHEME}${key}`;
|
|
612
|
+
}
|
|
613
|
+
async get(urlOrKey) {
|
|
614
|
+
let pathname = urlOrKey;
|
|
615
|
+
if (urlOrKey.startsWith(PONCHO_UPLOAD_SCHEME)) {
|
|
616
|
+
pathname = urlOrKey.slice(PONCHO_UPLOAD_SCHEME.length);
|
|
617
|
+
} else if (urlOrKey.startsWith("https://") || urlOrKey.startsWith("http://")) {
|
|
618
|
+
pathname = new URL(urlOrKey).pathname.slice(1);
|
|
619
|
+
}
|
|
620
|
+
if (this.access === "private") {
|
|
621
|
+
const sdk2 = await this.loadSdk();
|
|
622
|
+
const result = await sdk2.get(pathname, { access: "private" });
|
|
623
|
+
if (!result || result.statusCode !== 200) {
|
|
624
|
+
throw new Error(`uploads: failed to fetch private blob "${pathname}": ${result?.statusCode ?? "not found"}`);
|
|
625
|
+
}
|
|
626
|
+
const chunks = [];
|
|
627
|
+
const reader = result.stream.getReader();
|
|
628
|
+
for (; ; ) {
|
|
629
|
+
const { done, value } = await reader.read();
|
|
630
|
+
if (done) break;
|
|
631
|
+
chunks.push(value);
|
|
632
|
+
}
|
|
633
|
+
return Buffer.concat(chunks);
|
|
634
|
+
}
|
|
635
|
+
const sdk = await this.loadSdk();
|
|
636
|
+
const blob = await sdk.head(pathname);
|
|
637
|
+
const response = await fetch(blob.url);
|
|
638
|
+
if (!response.ok) {
|
|
639
|
+
throw new Error(`uploads: failed to fetch blob "${pathname}": ${response.status}`);
|
|
640
|
+
}
|
|
641
|
+
return Buffer.from(await response.arrayBuffer());
|
|
642
|
+
}
|
|
643
|
+
async delete(urlOrKey) {
|
|
644
|
+
const sdk = await this.loadSdk();
|
|
645
|
+
await sdk.del(urlOrKey);
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
var S3UploadStore = class {
|
|
649
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
650
|
+
s3Sdk;
|
|
651
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
652
|
+
presignerSdk;
|
|
653
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
654
|
+
client;
|
|
655
|
+
bucket;
|
|
656
|
+
region;
|
|
657
|
+
endpoint;
|
|
658
|
+
workingDir;
|
|
659
|
+
constructor(bucket, region, endpoint, workingDir) {
|
|
660
|
+
this.bucket = bucket;
|
|
661
|
+
this.region = region;
|
|
662
|
+
this.endpoint = endpoint;
|
|
663
|
+
this.workingDir = workingDir;
|
|
664
|
+
}
|
|
665
|
+
async ensureClient() {
|
|
666
|
+
if (this.client) return;
|
|
667
|
+
try {
|
|
668
|
+
this.s3Sdk = await tryImport("@aws-sdk/client-s3", this.workingDir);
|
|
669
|
+
this.presignerSdk = await tryImport("@aws-sdk/s3-request-presigner", this.workingDir);
|
|
670
|
+
} catch {
|
|
671
|
+
throw new Error(
|
|
672
|
+
'uploads: s3 provider requires "@aws-sdk/client-s3" and "@aws-sdk/s3-request-presigner". Install with: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner'
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
this.client = new this.s3Sdk.S3Client({
|
|
676
|
+
region: this.region ?? process.env.AWS_REGION ?? "us-east-1",
|
|
677
|
+
...this.endpoint ? { endpoint: this.endpoint, forcePathStyle: true } : {}
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
async put(key, data, mediaType) {
|
|
681
|
+
await this.ensureClient();
|
|
682
|
+
await this.client.send(
|
|
683
|
+
new this.s3Sdk.PutObjectCommand({
|
|
684
|
+
Bucket: this.bucket,
|
|
685
|
+
Key: key,
|
|
686
|
+
Body: data,
|
|
687
|
+
ContentType: mediaType
|
|
688
|
+
})
|
|
689
|
+
);
|
|
690
|
+
const url = await this.presignerSdk.getSignedUrl(
|
|
691
|
+
this.client,
|
|
692
|
+
new this.s3Sdk.GetObjectCommand({ Bucket: this.bucket, Key: key }),
|
|
693
|
+
{ expiresIn: 7 * 24 * 60 * 60 }
|
|
694
|
+
);
|
|
695
|
+
return url;
|
|
696
|
+
}
|
|
697
|
+
async get(urlOrKey) {
|
|
698
|
+
if (urlOrKey.startsWith("https://") || urlOrKey.startsWith("http://")) {
|
|
699
|
+
const response = await fetch(urlOrKey);
|
|
700
|
+
if (!response.ok) {
|
|
701
|
+
throw new Error(`uploads: failed to fetch S3 object at ${urlOrKey}: ${response.status}`);
|
|
702
|
+
}
|
|
703
|
+
return Buffer.from(await response.arrayBuffer());
|
|
704
|
+
}
|
|
705
|
+
await this.ensureClient();
|
|
706
|
+
const result = await this.client.send(
|
|
707
|
+
new this.s3Sdk.GetObjectCommand({ Bucket: this.bucket, Key: urlOrKey })
|
|
708
|
+
);
|
|
709
|
+
if (!result.Body) throw new Error(`uploads: empty body for S3 key ${urlOrKey}`);
|
|
710
|
+
return Buffer.from(await result.Body.transformToByteArray());
|
|
711
|
+
}
|
|
712
|
+
async delete(urlOrKey) {
|
|
713
|
+
await this.ensureClient();
|
|
714
|
+
const key = urlOrKey.startsWith("https://") ? new URL(urlOrKey).pathname.slice(1) : urlOrKey;
|
|
715
|
+
await this.client.send(
|
|
716
|
+
new this.s3Sdk.DeleteObjectCommand({ Bucket: this.bucket, Key: key })
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
var warn = (msg) => {
|
|
721
|
+
console.warn(`[poncho] \u26A0 ${msg}`);
|
|
722
|
+
};
|
|
723
|
+
var createUploadStore = async (config, workingDir) => {
|
|
724
|
+
const provider = config?.provider ?? "local";
|
|
725
|
+
if (provider === "vercel-blob") {
|
|
726
|
+
if (!process.env.BLOB_READ_WRITE_TOKEN) {
|
|
727
|
+
warn(
|
|
728
|
+
"uploads: vercel-blob configured but BLOB_READ_WRITE_TOKEN not found in environment. Falling back to local filesystem.\n Make sure BLOB_READ_WRITE_TOKEN is set in your .env file or environment."
|
|
729
|
+
);
|
|
730
|
+
return new LocalUploadStore(workingDir);
|
|
731
|
+
}
|
|
732
|
+
const store = new VercelBlobUploadStore(workingDir, config?.access ?? "public");
|
|
733
|
+
try {
|
|
734
|
+
await store.loadSdk();
|
|
735
|
+
console.log("[poncho] uploads: using vercel-blob store");
|
|
736
|
+
return new CachedUploadStore(store);
|
|
737
|
+
} catch {
|
|
738
|
+
warn(
|
|
739
|
+
'uploads: vercel-blob configured but "@vercel/blob" package is not installed. Falling back to local filesystem.\n Run `poncho build <target>` to auto-add it, or install manually: pnpm add @vercel/blob'
|
|
740
|
+
);
|
|
741
|
+
return new LocalUploadStore(workingDir);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (provider === "s3") {
|
|
745
|
+
const bucket = config?.bucket ?? process.env.PONCHO_UPLOADS_BUCKET;
|
|
746
|
+
if (!process.env.AWS_ACCESS_KEY_ID || !bucket) {
|
|
747
|
+
const missing = !process.env.AWS_ACCESS_KEY_ID ? "AWS_ACCESS_KEY_ID" : "bucket (config.uploads.bucket or PONCHO_UPLOADS_BUCKET)";
|
|
748
|
+
warn(
|
|
749
|
+
`uploads: s3 configured but ${missing} not found in environment. Falling back to local filesystem.`
|
|
750
|
+
);
|
|
751
|
+
return new LocalUploadStore(workingDir);
|
|
752
|
+
}
|
|
753
|
+
const store = new S3UploadStore(
|
|
754
|
+
bucket,
|
|
755
|
+
config?.region ?? process.env.AWS_REGION,
|
|
756
|
+
config?.endpoint ?? process.env.PONCHO_UPLOADS_ENDPOINT,
|
|
757
|
+
workingDir
|
|
758
|
+
);
|
|
759
|
+
try {
|
|
760
|
+
await store.ensureClient();
|
|
761
|
+
console.log(`[poncho] uploads: using s3 store (bucket: ${bucket})`);
|
|
762
|
+
return new CachedUploadStore(store);
|
|
763
|
+
} catch {
|
|
764
|
+
warn(
|
|
765
|
+
"uploads: s3 configured but AWS SDK packages are not installed. Falling back to local filesystem.\n Run `poncho build <target>` to auto-add them, or install manually: pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner"
|
|
766
|
+
);
|
|
767
|
+
return new LocalUploadStore(workingDir);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return new LocalUploadStore(workingDir);
|
|
771
|
+
};
|
|
415
772
|
|
|
416
773
|
// src/memory.ts
|
|
417
|
-
import { mkdir as
|
|
418
|
-
import { dirname as dirname2, resolve as
|
|
774
|
+
import { mkdir as mkdir3, readFile as readFile5, rename, writeFile as writeFile4 } from "fs/promises";
|
|
775
|
+
import { dirname as dirname2, resolve as resolve6 } from "path";
|
|
419
776
|
import { defineTool as defineTool2 } from "@poncho-ai/sdk";
|
|
420
777
|
var DEFAULT_MAIN_MEMORY = {
|
|
421
778
|
content: "",
|
|
@@ -423,9 +780,9 @@ var DEFAULT_MAIN_MEMORY = {
|
|
|
423
780
|
};
|
|
424
781
|
var LOCAL_MEMORY_FILE = "memory.json";
|
|
425
782
|
var writeJsonAtomic = async (filePath, payload) => {
|
|
426
|
-
await
|
|
783
|
+
await mkdir3(dirname2(filePath), { recursive: true });
|
|
427
784
|
const tmpPath = `${filePath}.tmp`;
|
|
428
|
-
await
|
|
785
|
+
await writeFile4(tmpPath, JSON.stringify(payload, null, 2), "utf8");
|
|
429
786
|
await rename(tmpPath, filePath);
|
|
430
787
|
};
|
|
431
788
|
var scoreText = (text, query) => {
|
|
@@ -487,7 +844,7 @@ var FileMainMemoryStore = class {
|
|
|
487
844
|
return;
|
|
488
845
|
}
|
|
489
846
|
const identity = await ensureAgentIdentity(this.workingDir);
|
|
490
|
-
this.filePath =
|
|
847
|
+
this.filePath = resolve6(getAgentStoreDirectory(identity), LOCAL_MEMORY_FILE);
|
|
491
848
|
}
|
|
492
849
|
isExpired(updatedAt) {
|
|
493
850
|
return typeof this.ttlMs === "number" && Date.now() - updatedAt > this.ttlMs;
|
|
@@ -499,7 +856,7 @@ var FileMainMemoryStore = class {
|
|
|
499
856
|
}
|
|
500
857
|
this.loaded = true;
|
|
501
858
|
try {
|
|
502
|
-
const raw = await
|
|
859
|
+
const raw = await readFile5(this.filePath, "utf8");
|
|
503
860
|
const parsed = JSON.parse(raw);
|
|
504
861
|
const content = typeof parsed.main?.content === "string" ? parsed.main.content : "";
|
|
505
862
|
const updatedAt = typeof parsed.main?.updatedAt === "number" ? parsed.main.updatedAt : 0;
|
|
@@ -1385,8 +1742,8 @@ var createModelProvider = (provider) => {
|
|
|
1385
1742
|
};
|
|
1386
1743
|
|
|
1387
1744
|
// src/skill-context.ts
|
|
1388
|
-
import { readFile as
|
|
1389
|
-
import { dirname as dirname3, resolve as
|
|
1745
|
+
import { readFile as readFile6, readdir as readdir2, stat } from "fs/promises";
|
|
1746
|
+
import { dirname as dirname3, resolve as resolve7, normalize } from "path";
|
|
1390
1747
|
import YAML3 from "yaml";
|
|
1391
1748
|
var DEFAULT_SKILL_DIRS = ["skills"];
|
|
1392
1749
|
var resolveSkillDirs = (workingDir, extraPaths) => {
|
|
@@ -1398,7 +1755,7 @@ var resolveSkillDirs = (workingDir, extraPaths) => {
|
|
|
1398
1755
|
}
|
|
1399
1756
|
}
|
|
1400
1757
|
}
|
|
1401
|
-
return dirs.map((d) =>
|
|
1758
|
+
return dirs.map((d) => resolve7(workingDir, d));
|
|
1402
1759
|
};
|
|
1403
1760
|
var FRONTMATTER_PATTERN3 = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
|
|
1404
1761
|
var asRecord2 = (value) => typeof value === "object" && value !== null ? value : {};
|
|
@@ -1467,7 +1824,7 @@ var collectSkillManifests = async (directory) => {
|
|
|
1467
1824
|
const entries = await readdir2(directory, { withFileTypes: true });
|
|
1468
1825
|
const files = [];
|
|
1469
1826
|
for (const entry of entries) {
|
|
1470
|
-
const fullPath =
|
|
1827
|
+
const fullPath = resolve7(directory, entry.name);
|
|
1471
1828
|
let isDir = entry.isDirectory();
|
|
1472
1829
|
let isFile = entry.isFile();
|
|
1473
1830
|
if (entry.isSymbolicLink()) {
|
|
@@ -1502,7 +1859,7 @@ var loadSkillMetadata = async (workingDir, extraSkillPaths) => {
|
|
|
1502
1859
|
const seen = /* @__PURE__ */ new Set();
|
|
1503
1860
|
for (const manifest of allManifests) {
|
|
1504
1861
|
try {
|
|
1505
|
-
const content = await
|
|
1862
|
+
const content = await readFile6(manifest, "utf8");
|
|
1506
1863
|
const parsed = parseSkillFrontmatter(content);
|
|
1507
1864
|
if (parsed && !seen.has(parsed.name)) {
|
|
1508
1865
|
seen.add(parsed.name);
|
|
@@ -1547,7 +1904,7 @@ ${xmlSkills}
|
|
|
1547
1904
|
};
|
|
1548
1905
|
var escapeXml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1549
1906
|
var loadSkillInstructions = async (skill) => {
|
|
1550
|
-
const content = await
|
|
1907
|
+
const content = await readFile6(skill.skillPath, "utf8");
|
|
1551
1908
|
const match = content.match(FRONTMATTER_PATTERN3);
|
|
1552
1909
|
return match ? match[2].trim() : content.trim();
|
|
1553
1910
|
};
|
|
@@ -1556,11 +1913,11 @@ var readSkillResource = async (skill, relativePath) => {
|
|
|
1556
1913
|
if (normalized.startsWith("..") || normalized.startsWith("/")) {
|
|
1557
1914
|
throw new Error("Path must be relative and within the skill directory");
|
|
1558
1915
|
}
|
|
1559
|
-
const fullPath =
|
|
1916
|
+
const fullPath = resolve7(skill.skillDir, normalized);
|
|
1560
1917
|
if (!fullPath.startsWith(skill.skillDir)) {
|
|
1561
1918
|
throw new Error("Path escapes the skill directory");
|
|
1562
1919
|
}
|
|
1563
|
-
return await
|
|
1920
|
+
return await readFile6(fullPath, "utf8");
|
|
1564
1921
|
};
|
|
1565
1922
|
var MAX_INSTRUCTIONS_PER_SKILL = 1200;
|
|
1566
1923
|
var loadSkillContext = async (workingDir) => {
|
|
@@ -1651,7 +2008,7 @@ function convertSchema(schema) {
|
|
|
1651
2008
|
// src/skill-tools.ts
|
|
1652
2009
|
import { defineTool as defineTool3 } from "@poncho-ai/sdk";
|
|
1653
2010
|
import { access as access2, readdir as readdir3, stat as stat2 } from "fs/promises";
|
|
1654
|
-
import { extname, normalize as normalize2, resolve as
|
|
2011
|
+
import { extname, normalize as normalize2, resolve as resolve8, sep as sep2 } from "path";
|
|
1655
2012
|
import { pathToFileURL } from "url";
|
|
1656
2013
|
import { createJiti as createJiti2 } from "jiti";
|
|
1657
2014
|
var createSkillTools = (skills, options) => {
|
|
@@ -1908,7 +2265,7 @@ var collectScriptFiles = async (directory) => {
|
|
|
1908
2265
|
if (entry.name === "node_modules") {
|
|
1909
2266
|
continue;
|
|
1910
2267
|
}
|
|
1911
|
-
const fullPath =
|
|
2268
|
+
const fullPath = resolve8(directory, entry.name);
|
|
1912
2269
|
let isDir = entry.isDirectory();
|
|
1913
2270
|
let isFile = entry.isFile();
|
|
1914
2271
|
if (entry.isSymbolicLink()) {
|
|
@@ -1947,8 +2304,8 @@ var normalizeScriptPolicyPath = (relativePath) => {
|
|
|
1947
2304
|
};
|
|
1948
2305
|
var resolveScriptPath = (baseDir, relativePath) => {
|
|
1949
2306
|
const normalized = normalizeScriptPolicyPath(relativePath);
|
|
1950
|
-
const fullPath =
|
|
1951
|
-
if (!fullPath.startsWith(`${
|
|
2307
|
+
const fullPath = resolve8(baseDir, normalized);
|
|
2308
|
+
if (!fullPath.startsWith(`${resolve8(baseDir)}${sep2}`) && fullPath !== resolve8(baseDir)) {
|
|
1952
2309
|
throw new Error("Script path must stay inside the allowed directory");
|
|
1953
2310
|
}
|
|
1954
2311
|
const extension = extname(fullPath).toLowerCase();
|
|
@@ -2256,6 +2613,25 @@ You can extend your own capabilities by creating custom JavaScript/TypeScript sc
|
|
|
2256
2613
|
- Script entries outside \`./scripts/\` must also appear in \`allowed-tools\`.
|
|
2257
2614
|
- Keep MCP server connection details (\`url\`, auth env vars) in \`poncho.config.js\` only.
|
|
2258
2615
|
|
|
2616
|
+
## Cron Jobs
|
|
2617
|
+
|
|
2618
|
+
Users can define scheduled tasks in \`AGENT.md\` frontmatter:
|
|
2619
|
+
|
|
2620
|
+
\`\`\`yaml
|
|
2621
|
+
cron:
|
|
2622
|
+
daily-report:
|
|
2623
|
+
schedule: "0 9 * * *" # Standard 5-field cron expression
|
|
2624
|
+
timezone: "America/New_York" # Optional IANA timezone (default: UTC)
|
|
2625
|
+
task: "Generate the daily sales report"
|
|
2626
|
+
\`\`\`
|
|
2627
|
+
|
|
2628
|
+
- Each cron job triggers an autonomous agent run with the specified task, creating a fresh conversation.
|
|
2629
|
+
- In \`poncho dev\`, jobs run via an in-process scheduler and appear in the web UI sidebar (prefixed with \`[cron]\`).
|
|
2630
|
+
- For Vercel: \`poncho build vercel\` generates \`vercel.json\` cron entries. Set \`CRON_SECRET\` = \`PONCHO_AUTH_TOKEN\`.
|
|
2631
|
+
- Jobs can also be triggered manually: \`GET /api/cron/<jobName>\`.
|
|
2632
|
+
- To carry context across cron runs, enable memory.
|
|
2633
|
+
- **IMPORTANT**: When adding a new cron job, always PRESERVE all existing cron jobs. Never remove or overwrite existing jobs unless the user explicitly asks you to replace or delete them. Read the full current \`cron:\` block before editing, and append the new job alongside the existing ones.
|
|
2634
|
+
|
|
2259
2635
|
## When users ask about customization:
|
|
2260
2636
|
|
|
2261
2637
|
- Explain and edit \`poncho.config.js\` for model/provider, storage+memory, auth, telemetry, and MCP settings.
|
|
@@ -2283,6 +2659,7 @@ var AgentHarness = class {
|
|
|
2283
2659
|
modelProviderInjected;
|
|
2284
2660
|
dispatcher = new ToolDispatcher();
|
|
2285
2661
|
approvalHandler;
|
|
2662
|
+
uploadStore;
|
|
2286
2663
|
skillContextWindow = "";
|
|
2287
2664
|
memoryStore;
|
|
2288
2665
|
loadedConfig;
|
|
@@ -2345,10 +2722,14 @@ var AgentHarness = class {
|
|
|
2345
2722
|
this.modelProviderInjected = !!options.modelProvider;
|
|
2346
2723
|
this.modelProvider = options.modelProvider ?? createModelProvider("anthropic");
|
|
2347
2724
|
this.approvalHandler = options.approvalHandler;
|
|
2725
|
+
this.uploadStore = options.uploadStore;
|
|
2348
2726
|
if (options.toolDefinitions?.length) {
|
|
2349
2727
|
this.dispatcher.registerMany(options.toolDefinitions);
|
|
2350
2728
|
}
|
|
2351
2729
|
}
|
|
2730
|
+
get frontmatter() {
|
|
2731
|
+
return this.parsedAgent?.frontmatter;
|
|
2732
|
+
}
|
|
2352
2733
|
listActiveSkills() {
|
|
2353
2734
|
return [...this.activeSkillNames].sort();
|
|
2354
2735
|
}
|
|
@@ -2626,9 +3007,9 @@ var AgentHarness = class {
|
|
|
2626
3007
|
for await (const event of this.run(input)) {
|
|
2627
3008
|
eventQueue.push(event);
|
|
2628
3009
|
if (queueResolve) {
|
|
2629
|
-
const
|
|
3010
|
+
const resolve10 = queueResolve;
|
|
2630
3011
|
queueResolve = null;
|
|
2631
|
-
|
|
3012
|
+
resolve10();
|
|
2632
3013
|
}
|
|
2633
3014
|
}
|
|
2634
3015
|
} catch (error) {
|
|
@@ -2646,8 +3027,8 @@ var AgentHarness = class {
|
|
|
2646
3027
|
if (eventQueue.length > 0) {
|
|
2647
3028
|
yield eventQueue.shift();
|
|
2648
3029
|
} else if (!generatorDone) {
|
|
2649
|
-
await new Promise((
|
|
2650
|
-
queueResolve =
|
|
3030
|
+
await new Promise((resolve10) => {
|
|
3031
|
+
queueResolve = resolve10;
|
|
2651
3032
|
});
|
|
2652
3033
|
}
|
|
2653
3034
|
}
|
|
@@ -2671,6 +3052,8 @@ var AgentHarness = class {
|
|
|
2671
3052
|
const start = now();
|
|
2672
3053
|
const maxSteps = agent.frontmatter.limits?.maxSteps ?? 50;
|
|
2673
3054
|
const timeoutMs = (agent.frontmatter.limits?.timeout ?? 300) * 1e3;
|
|
3055
|
+
const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
|
|
3056
|
+
const softDeadlineMs = platformMaxDurationSec > 0 ? platformMaxDurationSec * 800 : 0;
|
|
2674
3057
|
const messages = [...input.messages ?? []];
|
|
2675
3058
|
const events = [];
|
|
2676
3059
|
const systemPrompt = renderAgentPrompt(agent, {
|
|
@@ -2718,11 +3101,42 @@ ${boundedMainMemory.trim()}` : "";
|
|
|
2718
3101
|
runId,
|
|
2719
3102
|
agentId: agent.frontmatter.id ?? agent.frontmatter.name
|
|
2720
3103
|
});
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
3104
|
+
if (input.files && input.files.length > 0) {
|
|
3105
|
+
const parts = [
|
|
3106
|
+
{ type: "text", text: input.task }
|
|
3107
|
+
];
|
|
3108
|
+
for (const file of input.files) {
|
|
3109
|
+
if (this.uploadStore) {
|
|
3110
|
+
const buf = Buffer.from(file.data, "base64");
|
|
3111
|
+
const key = deriveUploadKey(buf, file.mediaType);
|
|
3112
|
+
const ref = await this.uploadStore.put(key, buf, file.mediaType);
|
|
3113
|
+
parts.push({
|
|
3114
|
+
type: "file",
|
|
3115
|
+
data: ref,
|
|
3116
|
+
mediaType: file.mediaType,
|
|
3117
|
+
filename: file.filename
|
|
3118
|
+
});
|
|
3119
|
+
} else {
|
|
3120
|
+
parts.push({
|
|
3121
|
+
type: "file",
|
|
3122
|
+
data: file.data,
|
|
3123
|
+
mediaType: file.mediaType,
|
|
3124
|
+
filename: file.filename
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
messages.push({
|
|
3129
|
+
role: "user",
|
|
3130
|
+
content: parts,
|
|
3131
|
+
metadata: { timestamp: now(), id: randomUUID3() }
|
|
3132
|
+
});
|
|
3133
|
+
} else {
|
|
3134
|
+
messages.push({
|
|
3135
|
+
role: "user",
|
|
3136
|
+
content: input.task,
|
|
3137
|
+
metadata: { timestamp: now(), id: randomUUID3() }
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
2726
3140
|
let responseText = "";
|
|
2727
3141
|
let totalInputTokens = 0;
|
|
2728
3142
|
let totalOutputTokens = 0;
|
|
@@ -2744,6 +3158,19 @@ ${boundedMainMemory.trim()}` : "";
|
|
|
2744
3158
|
});
|
|
2745
3159
|
return;
|
|
2746
3160
|
}
|
|
3161
|
+
if (softDeadlineMs > 0 && now() - start > softDeadlineMs) {
|
|
3162
|
+
const result2 = {
|
|
3163
|
+
status: "completed",
|
|
3164
|
+
response: responseText,
|
|
3165
|
+
steps: step - 1,
|
|
3166
|
+
tokens: { input: totalInputTokens, output: totalOutputTokens, cached: 0 },
|
|
3167
|
+
duration: now() - start,
|
|
3168
|
+
continuation: true,
|
|
3169
|
+
maxSteps
|
|
3170
|
+
};
|
|
3171
|
+
yield pushEvent({ type: "run:completed", runId, result: result2 });
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
2747
3174
|
const stepStart = now();
|
|
2748
3175
|
yield pushEvent({ type: "step:started", step });
|
|
2749
3176
|
yield pushEvent({ type: "model:request", tokens: 0 });
|
|
@@ -2765,102 +3192,182 @@ ${boundedMainMemory.trim()}` : "";
|
|
|
2765
3192
|
inputSchema: jsonSchemaToZod(tool.inputSchema)
|
|
2766
3193
|
};
|
|
2767
3194
|
}
|
|
2768
|
-
const
|
|
2769
|
-
(msg)
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
3195
|
+
const convertMessage = async (msg) => {
|
|
3196
|
+
if (msg.role === "tool") {
|
|
3197
|
+
const textContent = typeof msg.content === "string" ? msg.content : getTextContent(msg);
|
|
3198
|
+
try {
|
|
3199
|
+
const parsed = JSON.parse(textContent);
|
|
3200
|
+
if (!Array.isArray(parsed)) {
|
|
3201
|
+
return [];
|
|
3202
|
+
}
|
|
3203
|
+
const toolResults = parsed.filter((item) => {
|
|
3204
|
+
if (typeof item !== "object" || item === null) {
|
|
3205
|
+
return false;
|
|
2775
3206
|
}
|
|
2776
|
-
const
|
|
2777
|
-
|
|
3207
|
+
const row = item;
|
|
3208
|
+
return typeof row.tool_use_id === "string" && typeof row.tool_name === "string" && typeof row.content === "string";
|
|
3209
|
+
});
|
|
3210
|
+
if (toolResults.length === 0) {
|
|
3211
|
+
return [];
|
|
3212
|
+
}
|
|
3213
|
+
return [{
|
|
3214
|
+
role: "tool",
|
|
3215
|
+
content: toolResults.map((tr) => {
|
|
3216
|
+
if (tr.content.startsWith("Tool error:")) {
|
|
3217
|
+
return {
|
|
3218
|
+
type: "tool-result",
|
|
3219
|
+
toolCallId: tr.tool_use_id,
|
|
3220
|
+
toolName: tr.tool_name,
|
|
3221
|
+
output: { type: "text", value: tr.content }
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
try {
|
|
3225
|
+
const resultValue = JSON.parse(tr.content);
|
|
3226
|
+
return {
|
|
3227
|
+
type: "tool-result",
|
|
3228
|
+
toolCallId: tr.tool_use_id,
|
|
3229
|
+
toolName: tr.tool_name,
|
|
3230
|
+
output: { type: "json", value: resultValue }
|
|
3231
|
+
};
|
|
3232
|
+
} catch {
|
|
3233
|
+
return {
|
|
3234
|
+
type: "tool-result",
|
|
3235
|
+
toolCallId: tr.tool_use_id,
|
|
3236
|
+
toolName: tr.tool_name,
|
|
3237
|
+
output: { type: "text", value: tr.content }
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
})
|
|
3241
|
+
}];
|
|
3242
|
+
} catch {
|
|
3243
|
+
return [];
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
if (msg.role === "assistant") {
|
|
3247
|
+
const assistantText = typeof msg.content === "string" ? msg.content : getTextContent(msg);
|
|
3248
|
+
try {
|
|
3249
|
+
const parsed = JSON.parse(assistantText);
|
|
3250
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
3251
|
+
const parsedRecord = parsed;
|
|
3252
|
+
if (!Array.isArray(parsedRecord.tool_calls)) {
|
|
3253
|
+
return [{ role: "assistant", content: assistantText }];
|
|
3254
|
+
}
|
|
3255
|
+
const textPart = typeof parsedRecord.text === "string" ? parsedRecord.text : "";
|
|
3256
|
+
const validToolCalls = parsedRecord.tool_calls.filter((tc) => {
|
|
3257
|
+
if (typeof tc !== "object" || tc === null) {
|
|
2778
3258
|
return false;
|
|
2779
3259
|
}
|
|
2780
|
-
const
|
|
2781
|
-
return typeof
|
|
2782
|
-
})
|
|
2783
|
-
|
|
3260
|
+
const toolCall = tc;
|
|
3261
|
+
return typeof toolCall.id === "string" && typeof toolCall.name === "string" && toolCall.input !== null && typeof toolCall.input === "object";
|
|
3262
|
+
}).map((tc) => ({
|
|
3263
|
+
type: "tool-call",
|
|
3264
|
+
toolCallId: tc.id,
|
|
3265
|
+
toolName: tc.name,
|
|
3266
|
+
input: tc.input
|
|
3267
|
+
}));
|
|
3268
|
+
if (textPart.length === 0 && validToolCalls.length === 0) {
|
|
2784
3269
|
return [];
|
|
2785
3270
|
}
|
|
2786
3271
|
return [{
|
|
2787
|
-
role: "
|
|
2788
|
-
content:
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
toolCallId: tr.tool_use_id,
|
|
2793
|
-
toolName: tr.tool_name,
|
|
2794
|
-
output: { type: "text", value: tr.content }
|
|
2795
|
-
};
|
|
2796
|
-
}
|
|
2797
|
-
try {
|
|
2798
|
-
const resultValue = JSON.parse(tr.content);
|
|
2799
|
-
return {
|
|
2800
|
-
type: "tool-result",
|
|
2801
|
-
toolCallId: tr.tool_use_id,
|
|
2802
|
-
toolName: tr.tool_name,
|
|
2803
|
-
output: { type: "json", value: resultValue }
|
|
2804
|
-
};
|
|
2805
|
-
} catch {
|
|
2806
|
-
return {
|
|
2807
|
-
type: "tool-result",
|
|
2808
|
-
toolCallId: tr.tool_use_id,
|
|
2809
|
-
toolName: tr.tool_name,
|
|
2810
|
-
output: { type: "text", value: tr.content }
|
|
2811
|
-
};
|
|
2812
|
-
}
|
|
2813
|
-
})
|
|
3272
|
+
role: "assistant",
|
|
3273
|
+
content: [
|
|
3274
|
+
...textPart.length > 0 ? [{ type: "text", text: textPart }] : [],
|
|
3275
|
+
...validToolCalls
|
|
3276
|
+
]
|
|
2814
3277
|
}];
|
|
2815
|
-
} catch {
|
|
2816
|
-
return [];
|
|
2817
3278
|
}
|
|
3279
|
+
} catch {
|
|
2818
3280
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
3281
|
+
return [{ role: "assistant", content: assistantText }];
|
|
3282
|
+
}
|
|
3283
|
+
if (msg.role === "system") {
|
|
3284
|
+
return [{
|
|
3285
|
+
role: "system",
|
|
3286
|
+
content: typeof msg.content === "string" ? msg.content : getTextContent(msg)
|
|
3287
|
+
}];
|
|
3288
|
+
}
|
|
3289
|
+
if (msg.role === "user") {
|
|
3290
|
+
if (typeof msg.content === "string") {
|
|
3291
|
+
return [{ role: "user", content: msg.content }];
|
|
3292
|
+
}
|
|
3293
|
+
const MODEL_IMAGE_TYPES = /* @__PURE__ */ new Set([
|
|
3294
|
+
"image/jpeg",
|
|
3295
|
+
"image/png",
|
|
3296
|
+
"image/gif",
|
|
3297
|
+
"image/webp"
|
|
3298
|
+
]);
|
|
3299
|
+
const MODEL_FILE_TYPES = /* @__PURE__ */ new Set([
|
|
3300
|
+
"application/pdf"
|
|
3301
|
+
]);
|
|
3302
|
+
const isTextBasedMime = (mt) => mt.startsWith("text/") || mt === "application/json" || mt === "application/xml" || mt === "application/x-yaml" || mt.endsWith("+json") || mt.endsWith("+xml");
|
|
3303
|
+
const userContent = await Promise.all(
|
|
3304
|
+
msg.content.map(async (part) => {
|
|
3305
|
+
if (part.type === "text") {
|
|
3306
|
+
return { type: "text", text: part.text };
|
|
3307
|
+
}
|
|
3308
|
+
const isSupportedImage = MODEL_IMAGE_TYPES.has(part.mediaType);
|
|
3309
|
+
const isSupportedFile = MODEL_FILE_TYPES.has(part.mediaType);
|
|
3310
|
+
if (!isSupportedImage && !isSupportedFile && isTextBasedMime(part.mediaType)) {
|
|
3311
|
+
let textContent;
|
|
3312
|
+
try {
|
|
3313
|
+
if (part.data.startsWith(PONCHO_UPLOAD_SCHEME) && this.uploadStore) {
|
|
3314
|
+
const buf = await this.uploadStore.get(part.data);
|
|
3315
|
+
textContent = buf.toString("utf8");
|
|
3316
|
+
} else if (part.data.startsWith("https://") || part.data.startsWith("http://")) {
|
|
3317
|
+
const resp = await fetch(part.data);
|
|
3318
|
+
textContent = await resp.text();
|
|
3319
|
+
} else {
|
|
3320
|
+
textContent = Buffer.from(part.data, "base64").toString("utf8");
|
|
2831
3321
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
}).map((tc) => ({
|
|
2835
|
-
type: "tool-call",
|
|
2836
|
-
toolCallId: tc.id,
|
|
2837
|
-
toolName: tc.name,
|
|
2838
|
-
input: tc.input
|
|
2839
|
-
}));
|
|
2840
|
-
if (textPart.length === 0 && validToolCalls.length === 0) {
|
|
2841
|
-
return [];
|
|
3322
|
+
} catch {
|
|
3323
|
+
textContent = "(could not read file)";
|
|
2842
3324
|
}
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
...textPart.length > 0 ? [{ type: "text", text: textPart }] : [],
|
|
2847
|
-
...validToolCalls
|
|
2848
|
-
]
|
|
2849
|
-
}];
|
|
3325
|
+
const label = part.filename ?? part.mediaType;
|
|
3326
|
+
return { type: "text", text: `[File: ${label}]
|
|
3327
|
+
${textContent}` };
|
|
2850
3328
|
}
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
3329
|
+
if (!isSupportedImage && !isSupportedFile) {
|
|
3330
|
+
const label = part.filename ?? part.mediaType;
|
|
3331
|
+
return {
|
|
3332
|
+
type: "text",
|
|
3333
|
+
text: `[Attached file: ${label} (${part.mediaType}) \u2014 this format is not supported by the model and was skipped]`
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
let resolvedData;
|
|
3337
|
+
if (part.data.startsWith(PONCHO_UPLOAD_SCHEME) && this.uploadStore) {
|
|
3338
|
+
const buf = await this.uploadStore.get(part.data);
|
|
3339
|
+
resolvedData = buf.toString("base64");
|
|
3340
|
+
} else if (part.data.startsWith("https://") || part.data.startsWith("http://")) {
|
|
3341
|
+
if (this.uploadStore) {
|
|
3342
|
+
const buf = await this.uploadStore.get(part.data);
|
|
3343
|
+
resolvedData = buf.toString("base64");
|
|
3344
|
+
} else {
|
|
3345
|
+
const resp = await fetch(part.data);
|
|
3346
|
+
resolvedData = Buffer.from(await resp.arrayBuffer()).toString("base64");
|
|
3347
|
+
}
|
|
3348
|
+
} else {
|
|
3349
|
+
resolvedData = part.data;
|
|
3350
|
+
}
|
|
3351
|
+
if (isSupportedImage) {
|
|
3352
|
+
return {
|
|
3353
|
+
type: "image",
|
|
3354
|
+
image: resolvedData,
|
|
3355
|
+
mediaType: part.mediaType
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
return {
|
|
3359
|
+
type: "file",
|
|
3360
|
+
data: resolvedData,
|
|
3361
|
+
mediaType: part.mediaType,
|
|
3362
|
+
filename: part.filename
|
|
3363
|
+
};
|
|
3364
|
+
})
|
|
3365
|
+
);
|
|
3366
|
+
return [{ role: "user", content: userContent }];
|
|
2862
3367
|
}
|
|
2863
|
-
|
|
3368
|
+
return [];
|
|
3369
|
+
};
|
|
3370
|
+
const coreMessages = (await Promise.all(trimMessageWindow(messages).map(convertMessage))).flat();
|
|
2864
3371
|
const modelName = agent.frontmatter.model?.name ?? "claude-opus-4-5";
|
|
2865
3372
|
const temperature = agent.frontmatter.model?.temperature ?? 0.2;
|
|
2866
3373
|
const maxTokens = agent.frontmatter.model?.maxTokens;
|
|
@@ -2907,8 +3414,8 @@ ${boundedMainMemory.trim()}` : "";
|
|
|
2907
3414
|
let timer;
|
|
2908
3415
|
const nextChunk = await Promise.race([
|
|
2909
3416
|
textIterator.next(),
|
|
2910
|
-
new Promise((
|
|
2911
|
-
timer = setTimeout(() =>
|
|
3417
|
+
new Promise((resolve10) => {
|
|
3418
|
+
timer = setTimeout(() => resolve10(null), timeout);
|
|
2912
3419
|
})
|
|
2913
3420
|
]);
|
|
2914
3421
|
clearTimeout(timer);
|
|
@@ -3183,14 +3690,27 @@ ${boundedMainMemory.trim()}` : "";
|
|
|
3183
3690
|
return;
|
|
3184
3691
|
}
|
|
3185
3692
|
}
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3693
|
+
if (softDeadlineMs > 0) {
|
|
3694
|
+
const result = {
|
|
3695
|
+
status: "completed",
|
|
3696
|
+
response: responseText,
|
|
3697
|
+
steps: maxSteps,
|
|
3698
|
+
tokens: { input: totalInputTokens, output: totalOutputTokens, cached: 0 },
|
|
3699
|
+
duration: now() - start,
|
|
3700
|
+
continuation: true,
|
|
3701
|
+
maxSteps
|
|
3702
|
+
};
|
|
3703
|
+
yield pushEvent({ type: "run:completed", runId, result });
|
|
3704
|
+
} else {
|
|
3705
|
+
yield pushEvent({
|
|
3706
|
+
type: "run:error",
|
|
3707
|
+
runId,
|
|
3708
|
+
error: {
|
|
3709
|
+
code: "MAX_STEPS_EXCEEDED",
|
|
3710
|
+
message: `Run reached maximum of ${maxSteps} steps`
|
|
3711
|
+
}
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3194
3714
|
}
|
|
3195
3715
|
async runToCompletion(input) {
|
|
3196
3716
|
const events = [];
|
|
@@ -3270,8 +3790,8 @@ var LatitudeCapture = class {
|
|
|
3270
3790
|
|
|
3271
3791
|
// src/state.ts
|
|
3272
3792
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
3273
|
-
import { mkdir as
|
|
3274
|
-
import { dirname as dirname4, resolve as
|
|
3793
|
+
import { mkdir as mkdir4, readFile as readFile7, readdir as readdir4, rename as rename2, rm as rm2, writeFile as writeFile5 } from "fs/promises";
|
|
3794
|
+
import { dirname as dirname4, resolve as resolve9 } from "path";
|
|
3275
3795
|
var DEFAULT_OWNER = "local-owner";
|
|
3276
3796
|
var LOCAL_STATE_FILE = "state.json";
|
|
3277
3797
|
var CONVERSATIONS_DIRECTORY = "conversations";
|
|
@@ -3287,9 +3807,9 @@ var toStoreIdentity = async ({
|
|
|
3287
3807
|
return { name: ensured.name, id: agentId };
|
|
3288
3808
|
};
|
|
3289
3809
|
var writeJsonAtomic2 = async (filePath, payload) => {
|
|
3290
|
-
await
|
|
3810
|
+
await mkdir4(dirname4(filePath), { recursive: true });
|
|
3291
3811
|
const tmpPath = `${filePath}.tmp`;
|
|
3292
|
-
await
|
|
3812
|
+
await writeFile5(tmpPath, JSON.stringify(payload, null, 2), "utf8");
|
|
3293
3813
|
await rename2(tmpPath, filePath);
|
|
3294
3814
|
};
|
|
3295
3815
|
var formatUtcTimestamp = (value) => new Date(value).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
@@ -3450,8 +3970,8 @@ var FileConversationStore = class {
|
|
|
3450
3970
|
agentId: this.agentId
|
|
3451
3971
|
});
|
|
3452
3972
|
const agentDir = getAgentStoreDirectory(identity);
|
|
3453
|
-
const conversationsDir =
|
|
3454
|
-
const indexPath =
|
|
3973
|
+
const conversationsDir = resolve9(agentDir, CONVERSATIONS_DIRECTORY);
|
|
3974
|
+
const indexPath = resolve9(conversationsDir, LOCAL_CONVERSATION_INDEX_FILE);
|
|
3455
3975
|
this.paths = { conversationsDir, indexPath };
|
|
3456
3976
|
return this.paths;
|
|
3457
3977
|
}
|
|
@@ -3465,9 +3985,9 @@ var FileConversationStore = class {
|
|
|
3465
3985
|
}
|
|
3466
3986
|
async readConversationFile(fileName) {
|
|
3467
3987
|
const { conversationsDir } = await this.resolvePaths();
|
|
3468
|
-
const filePath =
|
|
3988
|
+
const filePath = resolve9(conversationsDir, fileName);
|
|
3469
3989
|
try {
|
|
3470
|
-
const raw = await
|
|
3990
|
+
const raw = await readFile7(filePath, "utf8");
|
|
3471
3991
|
return JSON.parse(raw);
|
|
3472
3992
|
} catch {
|
|
3473
3993
|
return void 0;
|
|
@@ -3509,7 +4029,7 @@ var FileConversationStore = class {
|
|
|
3509
4029
|
this.loaded = true;
|
|
3510
4030
|
const { indexPath } = await this.resolvePaths();
|
|
3511
4031
|
try {
|
|
3512
|
-
const raw = await
|
|
4032
|
+
const raw = await readFile7(indexPath, "utf8");
|
|
3513
4033
|
const parsed = JSON.parse(raw);
|
|
3514
4034
|
for (const conversation of parsed.conversations ?? []) {
|
|
3515
4035
|
this.conversations.set(conversation.conversationId, conversation);
|
|
@@ -3522,7 +4042,7 @@ var FileConversationStore = class {
|
|
|
3522
4042
|
const { conversationsDir } = await this.resolvePaths();
|
|
3523
4043
|
const existing = this.conversations.get(conversation.conversationId);
|
|
3524
4044
|
const fileName = existing?.fileName ?? this.resolveConversationFileName(conversation);
|
|
3525
|
-
const filePath =
|
|
4045
|
+
const filePath = resolve9(conversationsDir, fileName);
|
|
3526
4046
|
this.writing = this.writing.then(async () => {
|
|
3527
4047
|
await writeJsonAtomic2(filePath, conversation);
|
|
3528
4048
|
this.conversations.set(conversation.conversationId, {
|
|
@@ -3601,7 +4121,7 @@ var FileConversationStore = class {
|
|
|
3601
4121
|
if (removed) {
|
|
3602
4122
|
this.writing = this.writing.then(async () => {
|
|
3603
4123
|
if (existing) {
|
|
3604
|
-
await
|
|
4124
|
+
await rm2(resolve9(conversationsDir, existing.fileName), { force: true });
|
|
3605
4125
|
}
|
|
3606
4126
|
await this.writeIndex();
|
|
3607
4127
|
});
|
|
@@ -3631,7 +4151,7 @@ var FileStateStore = class {
|
|
|
3631
4151
|
workingDir: this.workingDir,
|
|
3632
4152
|
agentId: this.agentId
|
|
3633
4153
|
});
|
|
3634
|
-
this.filePath =
|
|
4154
|
+
this.filePath = resolve9(getAgentStoreDirectory(identity), LOCAL_STATE_FILE);
|
|
3635
4155
|
}
|
|
3636
4156
|
isExpired(state) {
|
|
3637
4157
|
return typeof this.ttlMs === "number" && Date.now() - state.updatedAt > this.ttlMs;
|
|
@@ -3643,7 +4163,7 @@ var FileStateStore = class {
|
|
|
3643
4163
|
}
|
|
3644
4164
|
this.loaded = true;
|
|
3645
4165
|
try {
|
|
3646
|
-
const raw = await
|
|
4166
|
+
const raw = await readFile7(this.filePath, "utf8");
|
|
3647
4167
|
const parsed = JSON.parse(raw);
|
|
3648
4168
|
for (const state of parsed.states ?? []) {
|
|
3649
4169
|
this.states.set(state.runId, state);
|
|
@@ -4319,9 +4839,13 @@ export {
|
|
|
4319
4839
|
InMemoryStateStore,
|
|
4320
4840
|
LatitudeCapture,
|
|
4321
4841
|
LocalMcpBridge,
|
|
4842
|
+
LocalUploadStore,
|
|
4843
|
+
PONCHO_UPLOAD_SCHEME,
|
|
4844
|
+
S3UploadStore,
|
|
4322
4845
|
STORAGE_SCHEMA_VERSION,
|
|
4323
4846
|
TelemetryEmitter,
|
|
4324
4847
|
ToolDispatcher,
|
|
4848
|
+
VercelBlobUploadStore,
|
|
4325
4849
|
buildAgentDirectoryName,
|
|
4326
4850
|
buildSkillContextWindow,
|
|
4327
4851
|
createConversationStore,
|
|
@@ -4331,8 +4855,10 @@ export {
|
|
|
4331
4855
|
createModelProvider,
|
|
4332
4856
|
createSkillTools,
|
|
4333
4857
|
createStateStore,
|
|
4858
|
+
createUploadStore,
|
|
4334
4859
|
createWriteTool,
|
|
4335
4860
|
defineTool4 as defineTool,
|
|
4861
|
+
deriveUploadKey,
|
|
4336
4862
|
ensureAgentIdentity,
|
|
4337
4863
|
generateAgentId,
|
|
4338
4864
|
getAgentStoreDirectory,
|