@poncho-ai/harness 0.11.2 → 0.12.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 +11 -0
- package/dist/index.d.ts +53 -1
- package/dist/index.js +583 -133
- package/package.json +2 -2
- package/src/config.ts +10 -0
- package/src/harness.ts +191 -23
- package/src/index.ts +1 -0
- package/src/upload-store.ts +387 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"redis": "^5.10.0",
|
|
31
31
|
"yaml": "^2.4.0",
|
|
32
32
|
"zod": "^3.22.0",
|
|
33
|
-
"@poncho-ai/sdk": "0.
|
|
33
|
+
"@poncho-ai/sdk": "1.0.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/mustache": "^4.2.6",
|
package/src/config.ts
CHANGED
|
@@ -23,6 +23,15 @@ export interface StorageConfig {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface UploadsConfig {
|
|
27
|
+
provider?: "local" | "vercel-blob" | "s3";
|
|
28
|
+
/** Vercel Blob access mode. Must match the store's configuration. Defaults to "public". */
|
|
29
|
+
access?: "public" | "private";
|
|
30
|
+
bucket?: string;
|
|
31
|
+
region?: string;
|
|
32
|
+
endpoint?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
export type BuiltInToolToggles = {
|
|
27
36
|
list_directory?: boolean;
|
|
28
37
|
read_file?: boolean;
|
|
@@ -67,6 +76,7 @@ export interface PonchoConfig extends McpConfig {
|
|
|
67
76
|
/** Extra directories (relative to project root) to scan for skills.
|
|
68
77
|
* `skills/` and `.poncho/skills/` are always scanned. */
|
|
69
78
|
skillPaths?: string[];
|
|
79
|
+
uploads?: UploadsConfig;
|
|
70
80
|
build?: {
|
|
71
81
|
vercel?: Record<string, unknown>;
|
|
72
82
|
docker?: Record<string, unknown>;
|
package/src/harness.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type {
|
|
3
3
|
AgentEvent,
|
|
4
|
+
ContentPart,
|
|
5
|
+
FileContentPart,
|
|
4
6
|
Message,
|
|
5
7
|
RunInput,
|
|
6
8
|
RunResult,
|
|
9
|
+
TextContentPart,
|
|
7
10
|
ToolContext,
|
|
8
11
|
ToolDefinition,
|
|
9
12
|
} from "@poncho-ai/sdk";
|
|
13
|
+
import { getTextContent } from "@poncho-ai/sdk";
|
|
14
|
+
import type { UploadStore } from "./upload-store.js";
|
|
15
|
+
import { PONCHO_UPLOAD_SCHEME, deriveUploadKey } from "./upload-store.js";
|
|
10
16
|
import { parseAgentFile, renderAgentPrompt, type ParsedAgent } from "./agent-parser.js";
|
|
11
17
|
import { loadPonchoConfig, resolveMemoryConfig, type PonchoConfig } from "./config.js";
|
|
12
18
|
import { createDefaultTools, createWriteTool } from "./default-tools.js";
|
|
@@ -44,6 +50,7 @@ export interface HarnessOptions {
|
|
|
44
50
|
approvalId: string;
|
|
45
51
|
}) => Promise<boolean> | boolean;
|
|
46
52
|
modelProvider?: ModelProviderFactory;
|
|
53
|
+
uploadStore?: UploadStore;
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
export interface HarnessRunOutput {
|
|
@@ -257,6 +264,7 @@ export class AgentHarness {
|
|
|
257
264
|
private readonly modelProviderInjected: boolean;
|
|
258
265
|
private readonly dispatcher = new ToolDispatcher();
|
|
259
266
|
private readonly approvalHandler?: HarnessOptions["approvalHandler"];
|
|
267
|
+
readonly uploadStore?: UploadStore;
|
|
260
268
|
private skillContextWindow = "";
|
|
261
269
|
private memoryStore?: MemoryStore;
|
|
262
270
|
private loadedConfig?: PonchoConfig;
|
|
@@ -329,6 +337,7 @@ export class AgentHarness {
|
|
|
329
337
|
this.modelProviderInjected = !!options.modelProvider;
|
|
330
338
|
this.modelProvider = options.modelProvider ?? createModelProvider("anthropic");
|
|
331
339
|
this.approvalHandler = options.approvalHandler;
|
|
340
|
+
this.uploadStore = options.uploadStore;
|
|
332
341
|
|
|
333
342
|
if (options.toolDefinitions?.length) {
|
|
334
343
|
this.dispatcher.registerMany(options.toolDefinitions);
|
|
@@ -706,6 +715,10 @@ export class AgentHarness {
|
|
|
706
715
|
const start = now();
|
|
707
716
|
const maxSteps = agent.frontmatter.limits?.maxSteps ?? 50;
|
|
708
717
|
const timeoutMs = (agent.frontmatter.limits?.timeout ?? 300) * 1000;
|
|
718
|
+
const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
|
|
719
|
+
const softDeadlineMs = platformMaxDurationSec > 0
|
|
720
|
+
? platformMaxDurationSec * 800
|
|
721
|
+
: 0;
|
|
709
722
|
const messages: Message[] = [...(input.messages ?? [])];
|
|
710
723
|
const events: AgentEvent[] = [];
|
|
711
724
|
|
|
@@ -763,11 +776,42 @@ ${boundedMainMemory.trim()}`
|
|
|
763
776
|
agentId: agent.frontmatter.id ?? agent.frontmatter.name,
|
|
764
777
|
});
|
|
765
778
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
779
|
+
if (input.files && input.files.length > 0) {
|
|
780
|
+
const parts: ContentPart[] = [
|
|
781
|
+
{ type: "text", text: input.task } satisfies TextContentPart,
|
|
782
|
+
];
|
|
783
|
+
for (const file of input.files) {
|
|
784
|
+
if (this.uploadStore) {
|
|
785
|
+
const buf = Buffer.from(file.data, "base64");
|
|
786
|
+
const key = deriveUploadKey(buf, file.mediaType);
|
|
787
|
+
const ref = await this.uploadStore.put(key, buf, file.mediaType);
|
|
788
|
+
parts.push({
|
|
789
|
+
type: "file",
|
|
790
|
+
data: ref,
|
|
791
|
+
mediaType: file.mediaType,
|
|
792
|
+
filename: file.filename,
|
|
793
|
+
} satisfies FileContentPart);
|
|
794
|
+
} else {
|
|
795
|
+
parts.push({
|
|
796
|
+
type: "file",
|
|
797
|
+
data: file.data,
|
|
798
|
+
mediaType: file.mediaType,
|
|
799
|
+
filename: file.filename,
|
|
800
|
+
} satisfies FileContentPart);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
messages.push({
|
|
804
|
+
role: "user",
|
|
805
|
+
content: parts,
|
|
806
|
+
metadata: { timestamp: now(), id: randomUUID() },
|
|
807
|
+
});
|
|
808
|
+
} else {
|
|
809
|
+
messages.push({
|
|
810
|
+
role: "user",
|
|
811
|
+
content: input.task,
|
|
812
|
+
metadata: { timestamp: now(), id: randomUUID() },
|
|
813
|
+
});
|
|
814
|
+
}
|
|
771
815
|
|
|
772
816
|
let responseText = "";
|
|
773
817
|
let totalInputTokens = 0;
|
|
@@ -791,6 +835,19 @@ ${boundedMainMemory.trim()}`
|
|
|
791
835
|
});
|
|
792
836
|
return;
|
|
793
837
|
}
|
|
838
|
+
if (softDeadlineMs > 0 && now() - start > softDeadlineMs) {
|
|
839
|
+
const result: RunResult = {
|
|
840
|
+
status: "completed",
|
|
841
|
+
response: responseText,
|
|
842
|
+
steps: step - 1,
|
|
843
|
+
tokens: { input: totalInputTokens, output: totalOutputTokens, cached: 0 },
|
|
844
|
+
duration: now() - start,
|
|
845
|
+
continuation: true,
|
|
846
|
+
maxSteps,
|
|
847
|
+
};
|
|
848
|
+
yield pushEvent({ type: "run:completed", runId, result });
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
794
851
|
|
|
795
852
|
const stepStart = now();
|
|
796
853
|
yield pushEvent({ type: "step:started", step });
|
|
@@ -818,13 +875,13 @@ ${boundedMainMemory.trim()}`
|
|
|
818
875
|
}
|
|
819
876
|
|
|
820
877
|
// Convert messages to ModelMessage format
|
|
821
|
-
const
|
|
822
|
-
(msg): ModelMessage[] => {
|
|
878
|
+
const convertMessage = async (msg: Message): Promise<ModelMessage[]> => {
|
|
823
879
|
if (msg.role === "tool") {
|
|
824
880
|
// Tool messages are provider-sensitive; skip malformed historical records
|
|
825
881
|
// instead of failing the entire run continuation.
|
|
882
|
+
const textContent = typeof msg.content === "string" ? msg.content : getTextContent(msg);
|
|
826
883
|
try {
|
|
827
|
-
const parsed: unknown = JSON.parse(
|
|
884
|
+
const parsed: unknown = JSON.parse(textContent);
|
|
828
885
|
if (!Array.isArray(parsed)) {
|
|
829
886
|
return [];
|
|
830
887
|
}
|
|
@@ -881,12 +938,13 @@ ${boundedMainMemory.trim()}`
|
|
|
881
938
|
if (msg.role === "assistant") {
|
|
882
939
|
// Assistant messages may contain serialized tool calls from previous runs.
|
|
883
940
|
// Keep only valid tool-call records to avoid broken continuation payloads.
|
|
941
|
+
const assistantText = typeof msg.content === "string" ? msg.content : getTextContent(msg);
|
|
884
942
|
try {
|
|
885
|
-
const parsed: unknown = JSON.parse(
|
|
943
|
+
const parsed: unknown = JSON.parse(assistantText);
|
|
886
944
|
if (typeof parsed === "object" && parsed !== null) {
|
|
887
945
|
const parsedRecord = parsed as { text?: unknown; tool_calls?: unknown };
|
|
888
946
|
if (!Array.isArray(parsedRecord.tool_calls)) {
|
|
889
|
-
return [{ role: "assistant" as const, content:
|
|
947
|
+
return [{ role: "assistant" as const, content: assistantText }];
|
|
890
948
|
}
|
|
891
949
|
const textPart = typeof parsedRecord.text === "string" ? parsedRecord.text : "";
|
|
892
950
|
const validToolCalls = parsedRecord.tool_calls
|
|
@@ -922,18 +980,115 @@ ${boundedMainMemory.trim()}`
|
|
|
922
980
|
} catch {
|
|
923
981
|
// Not JSON, treat as regular assistant text.
|
|
924
982
|
}
|
|
925
|
-
return [{ role: "assistant" as const, content:
|
|
983
|
+
return [{ role: "assistant" as const, content: assistantText }];
|
|
926
984
|
}
|
|
927
985
|
|
|
928
|
-
if (msg.role === "
|
|
986
|
+
if (msg.role === "system") {
|
|
929
987
|
return [{
|
|
930
|
-
role:
|
|
931
|
-
content: msg.content,
|
|
988
|
+
role: "system" as const,
|
|
989
|
+
content: typeof msg.content === "string" ? msg.content : getTextContent(msg),
|
|
932
990
|
}];
|
|
933
991
|
}
|
|
934
992
|
|
|
993
|
+
if (msg.role === "user") {
|
|
994
|
+
if (typeof msg.content === "string") {
|
|
995
|
+
return [{ role: "user" as const, content: msg.content }];
|
|
996
|
+
}
|
|
997
|
+
// Convert ContentPart[] to Vercel AI SDK UserContent.
|
|
998
|
+
// Only specific media types can be sent as binary attachments —
|
|
999
|
+
// everything else is inlined as text or skipped gracefully.
|
|
1000
|
+
const MODEL_IMAGE_TYPES = new Set([
|
|
1001
|
+
"image/jpeg", "image/png", "image/gif", "image/webp",
|
|
1002
|
+
]);
|
|
1003
|
+
const MODEL_FILE_TYPES = new Set([
|
|
1004
|
+
"application/pdf",
|
|
1005
|
+
]);
|
|
1006
|
+
const isTextBasedMime = (mt: string): boolean =>
|
|
1007
|
+
mt.startsWith("text/") ||
|
|
1008
|
+
mt === "application/json" ||
|
|
1009
|
+
mt === "application/xml" ||
|
|
1010
|
+
mt === "application/x-yaml" ||
|
|
1011
|
+
mt.endsWith("+json") ||
|
|
1012
|
+
mt.endsWith("+xml");
|
|
1013
|
+
|
|
1014
|
+
const userContent = await Promise.all(
|
|
1015
|
+
msg.content.map(async (part) => {
|
|
1016
|
+
if (part.type === "text") {
|
|
1017
|
+
return { type: "text" as const, text: part.text };
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const isSupportedImage = MODEL_IMAGE_TYPES.has(part.mediaType);
|
|
1021
|
+
const isSupportedFile = MODEL_FILE_TYPES.has(part.mediaType);
|
|
1022
|
+
|
|
1023
|
+
// Text-based files: inline the content so the model can read it
|
|
1024
|
+
if (!isSupportedImage && !isSupportedFile && isTextBasedMime(part.mediaType)) {
|
|
1025
|
+
let textContent: string;
|
|
1026
|
+
try {
|
|
1027
|
+
if (part.data.startsWith(PONCHO_UPLOAD_SCHEME) && this.uploadStore) {
|
|
1028
|
+
const buf = await this.uploadStore.get(part.data);
|
|
1029
|
+
textContent = buf.toString("utf8");
|
|
1030
|
+
} else if (part.data.startsWith("https://") || part.data.startsWith("http://")) {
|
|
1031
|
+
const resp = await fetch(part.data);
|
|
1032
|
+
textContent = await resp.text();
|
|
1033
|
+
} else {
|
|
1034
|
+
textContent = Buffer.from(part.data, "base64").toString("utf8");
|
|
1035
|
+
}
|
|
1036
|
+
} catch {
|
|
1037
|
+
textContent = "(could not read file)";
|
|
1038
|
+
}
|
|
1039
|
+
const label = part.filename ?? part.mediaType;
|
|
1040
|
+
return { type: "text" as const, text: `[File: ${label}]\n${textContent}` };
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Unsupported binary formats (e.g. AVIF, HEIC, MP4): skip with a note
|
|
1044
|
+
if (!isSupportedImage && !isSupportedFile) {
|
|
1045
|
+
const label = part.filename ?? part.mediaType;
|
|
1046
|
+
return {
|
|
1047
|
+
type: "text" as const,
|
|
1048
|
+
text: `[Attached file: ${label} (${part.mediaType}) — this format is not supported by the model and was skipped]`,
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Always resolve to base64 so the model doesn't need to
|
|
1053
|
+
// fetch URLs itself (which fails for private blob stores).
|
|
1054
|
+
let resolvedData: string;
|
|
1055
|
+
if (part.data.startsWith(PONCHO_UPLOAD_SCHEME) && this.uploadStore) {
|
|
1056
|
+
const buf = await this.uploadStore.get(part.data);
|
|
1057
|
+
resolvedData = buf.toString("base64");
|
|
1058
|
+
} else if (part.data.startsWith("https://") || part.data.startsWith("http://")) {
|
|
1059
|
+
if (this.uploadStore) {
|
|
1060
|
+
const buf = await this.uploadStore.get(part.data);
|
|
1061
|
+
resolvedData = buf.toString("base64");
|
|
1062
|
+
} else {
|
|
1063
|
+
const resp = await fetch(part.data);
|
|
1064
|
+
resolvedData = Buffer.from(await resp.arrayBuffer()).toString("base64");
|
|
1065
|
+
}
|
|
1066
|
+
} else {
|
|
1067
|
+
resolvedData = part.data;
|
|
1068
|
+
}
|
|
1069
|
+
if (isSupportedImage) {
|
|
1070
|
+
return {
|
|
1071
|
+
type: "image" as const,
|
|
1072
|
+
image: resolvedData,
|
|
1073
|
+
mediaType: part.mediaType,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
return {
|
|
1077
|
+
type: "file" as const,
|
|
1078
|
+
data: resolvedData,
|
|
1079
|
+
mediaType: part.mediaType,
|
|
1080
|
+
filename: part.filename,
|
|
1081
|
+
};
|
|
1082
|
+
}),
|
|
1083
|
+
);
|
|
1084
|
+
return [{ role: "user" as const, content: userContent }];
|
|
1085
|
+
}
|
|
1086
|
+
|
|
935
1087
|
return [];
|
|
936
|
-
}
|
|
1088
|
+
};
|
|
1089
|
+
const coreMessages: ModelMessage[] = (
|
|
1090
|
+
await Promise.all(trimMessageWindow(messages).map(convertMessage))
|
|
1091
|
+
).flat();
|
|
937
1092
|
|
|
938
1093
|
const modelName = agent.frontmatter.model?.name ?? "claude-opus-4-5";
|
|
939
1094
|
const temperature = agent.frontmatter.model?.temperature ?? 0.2;
|
|
@@ -1319,14 +1474,27 @@ ${boundedMainMemory.trim()}`
|
|
|
1319
1474
|
}
|
|
1320
1475
|
}
|
|
1321
1476
|
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1477
|
+
if (softDeadlineMs > 0) {
|
|
1478
|
+
const result: RunResult = {
|
|
1479
|
+
status: "completed",
|
|
1480
|
+
response: responseText,
|
|
1481
|
+
steps: maxSteps,
|
|
1482
|
+
tokens: { input: totalInputTokens, output: totalOutputTokens, cached: 0 },
|
|
1483
|
+
duration: now() - start,
|
|
1484
|
+
continuation: true,
|
|
1485
|
+
maxSteps,
|
|
1486
|
+
};
|
|
1487
|
+
yield pushEvent({ type: "run:completed", runId, result });
|
|
1488
|
+
} else {
|
|
1489
|
+
yield pushEvent({
|
|
1490
|
+
type: "run:error",
|
|
1491
|
+
runId,
|
|
1492
|
+
error: {
|
|
1493
|
+
code: "MAX_STEPS_EXCEEDED",
|
|
1494
|
+
message: `Run reached maximum of ${maxSteps} steps`,
|
|
1495
|
+
},
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1330
1498
|
}
|
|
1331
1499
|
|
|
1332
1500
|
async runToCompletion(input: RunInput): Promise<HarnessRunOutput> {
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from "./schema-converter.js";
|
|
|
11
11
|
export * from "./skill-context.js";
|
|
12
12
|
export * from "./skill-tools.js";
|
|
13
13
|
export * from "./state.js";
|
|
14
|
+
export * from "./upload-store.js";
|
|
14
15
|
export * from "./telemetry.js";
|
|
15
16
|
export * from "./tool-dispatcher.js";
|
|
16
17
|
export { defineTool } from "@poncho-ai/sdk";
|