@poncho-ai/harness 0.14.0 → 0.14.2
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/.turbo/turbo-lint.log +6 -0
- package/.turbo/turbo-test.log +137 -0
- package/CHANGELOG.md +26 -0
- package/dist/index.d.ts +79 -49
- package/dist/index.js +199 -113
- package/package.json +2 -2
- package/src/config.ts +13 -8
- package/src/harness.ts +195 -115
- package/src/latitude-capture.ts +9 -10
- package/src/memory.ts +8 -13
- package/src/model-factory.ts +13 -6
- package/src/state.ts +18 -12
- package/src/telemetry.ts +2 -2
- package/test/telemetry.test.ts +3 -3
package/src/harness.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
type MemoryStore,
|
|
23
23
|
} from "./memory.js";
|
|
24
24
|
import { LocalMcpBridge } from "./mcp.js";
|
|
25
|
-
import { createModelProvider, getModelContextWindow, type ModelProviderFactory } from "./model-factory.js";
|
|
25
|
+
import { createModelProvider, getModelContextWindow, type ModelProviderFactory, type ProviderConfig } from "./model-factory.js";
|
|
26
26
|
import { buildSkillContextWindow, loadSkillMetadata } from "./skill-context.js";
|
|
27
27
|
import { streamText, type ModelMessage } from "ai";
|
|
28
28
|
import { addPromptCacheBreakpoints } from "./prompt-cache.js";
|
|
@@ -37,20 +37,13 @@ import {
|
|
|
37
37
|
matchesSlashPattern,
|
|
38
38
|
normalizeRelativeScriptPattern,
|
|
39
39
|
} from "./tool-policy.js";
|
|
40
|
-
import { ToolDispatcher } from "./tool-dispatcher.js";
|
|
40
|
+
import { ToolDispatcher, type ToolCall, type ToolExecutionResult } from "./tool-dispatcher.js";
|
|
41
41
|
import { ensureAgentIdentity } from "./agent-identity.js";
|
|
42
42
|
|
|
43
43
|
export interface HarnessOptions {
|
|
44
44
|
workingDir?: string;
|
|
45
45
|
environment?: "development" | "staging" | "production";
|
|
46
46
|
toolDefinitions?: ToolDefinition[];
|
|
47
|
-
approvalHandler?: (request: {
|
|
48
|
-
tool: string;
|
|
49
|
-
input: Record<string, unknown>;
|
|
50
|
-
runId: string;
|
|
51
|
-
step: number;
|
|
52
|
-
approvalId: string;
|
|
53
|
-
}) => Promise<boolean> | boolean;
|
|
54
47
|
modelProvider?: ModelProviderFactory;
|
|
55
48
|
uploadStore?: UploadStore;
|
|
56
49
|
}
|
|
@@ -374,18 +367,51 @@ When configuring Latitude telemetry, use **exactly** these field names:
|
|
|
374
367
|
telemetry: {
|
|
375
368
|
enabled: true,
|
|
376
369
|
latitude: {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
path: "your/prompt-path",
|
|
370
|
+
apiKeyEnv: "LATITUDE_API_KEY", // env var name (default)
|
|
371
|
+
projectIdEnv: "LATITUDE_PROJECT_ID", // env var name (default)
|
|
372
|
+
path: "your/prompt-path", // optional, defaults to agent name
|
|
380
373
|
},
|
|
381
374
|
},
|
|
382
375
|
\`\`\`
|
|
383
376
|
|
|
384
|
-
-
|
|
385
|
-
-
|
|
377
|
+
- \`apiKeyEnv\` specifies the environment variable name for the Latitude API key (defaults to \`"LATITUDE_API_KEY"\`).
|
|
378
|
+
- \`projectIdEnv\` specifies the environment variable name for the project ID (defaults to \`"LATITUDE_PROJECT_ID"\`).
|
|
379
|
+
- With defaults, you only need \`telemetry: { latitude: {} }\` if the env vars are already named \`LATITUDE_API_KEY\` and \`LATITUDE_PROJECT_ID\`.
|
|
386
380
|
- \`path\` must only contain letters, numbers, hyphens, underscores, dots, and slashes.
|
|
387
381
|
- For a generic OTLP endpoint instead: \`telemetry: { otlp: process.env.OTEL_EXPORTER_OTLP_ENDPOINT }\`.
|
|
388
|
-
|
|
382
|
+
|
|
383
|
+
## Credential Configuration Pattern
|
|
384
|
+
|
|
385
|
+
All credentials in \`poncho.config.js\` use the **env var name** pattern (\`*Env\` fields). Config specifies which environment variable to read — never the secret itself. Sensible defaults mean zero config when using conventional env var names.
|
|
386
|
+
|
|
387
|
+
\`\`\`javascript
|
|
388
|
+
// poncho.config.js — credentials use *Env fields with defaults
|
|
389
|
+
export default {
|
|
390
|
+
// Model provider API keys (optional, defaults shown)
|
|
391
|
+
providers: {
|
|
392
|
+
anthropic: { apiKeyEnv: "ANTHROPIC_API_KEY" },
|
|
393
|
+
openai: { apiKeyEnv: "OPENAI_API_KEY" },
|
|
394
|
+
},
|
|
395
|
+
auth: {
|
|
396
|
+
required: true,
|
|
397
|
+
tokenEnv: "PONCHO_AUTH_TOKEN", // default
|
|
398
|
+
},
|
|
399
|
+
storage: {
|
|
400
|
+
provider: "upstash",
|
|
401
|
+
urlEnv: "UPSTASH_REDIS_REST_URL", // default (falls back to KV_REST_API_URL)
|
|
402
|
+
tokenEnv: "UPSTASH_REDIS_REST_TOKEN", // default (falls back to KV_REST_API_TOKEN)
|
|
403
|
+
},
|
|
404
|
+
telemetry: {
|
|
405
|
+
latitude: {
|
|
406
|
+
apiKeyEnv: "LATITUDE_API_KEY", // default
|
|
407
|
+
projectIdEnv: "LATITUDE_PROJECT_ID", // default
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
messaging: [{ platform: "slack" }], // reads SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET by default
|
|
411
|
+
}
|
|
412
|
+
\`\`\`
|
|
413
|
+
|
|
414
|
+
Since all fields have defaults, you only need to specify \`*Env\` when your env var name differs from the convention.
|
|
389
415
|
|
|
390
416
|
## When users ask about customization:
|
|
391
417
|
|
|
@@ -400,6 +426,7 @@ telemetry: {
|
|
|
400
426
|
- To scope tools to a skill: keep server config in \`poncho.config.js\`, add desired \`allowed-tools\`/ \`approval-required\` patterns in that skill's \`SKILL.md\`, and remove global \`AGENT.md\` patterns if you do not want global availability.
|
|
401
427
|
- Do not invent unsupported top-level config keys (for example \`model\` in \`poncho.config.js\`). Keep existing config structure unless README/spec explicitly says otherwise.
|
|
402
428
|
- Keep \`poncho.config.js\` valid JavaScript and preserve existing imports/types/comments. If there is a JSDoc type import, do not rewrite it to a different package name.
|
|
429
|
+
- Credentials always use \`*Env\` fields (env var names), never raw \`process.env.*\` values. For example, use \`apiKeyEnv: "MY_KEY"\` not \`apiKey: process.env.MY_KEY\`.
|
|
403
430
|
- Preferred MCP config shape in \`poncho.config.js\`:
|
|
404
431
|
\`mcp: [{ name: "linear", url: "https://mcp.linear.app/mcp", auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" } }]\`
|
|
405
432
|
- If shell/CLI access exists, you can use \`poncho mcp add --url ... --name ... --auth-bearer-env ...\`, then \`poncho mcp tools list <server>\` and \`poncho mcp tools select <server>\`.
|
|
@@ -414,7 +441,6 @@ export class AgentHarness {
|
|
|
414
441
|
private modelProvider: ModelProviderFactory;
|
|
415
442
|
private readonly modelProviderInjected: boolean;
|
|
416
443
|
private readonly dispatcher = new ToolDispatcher();
|
|
417
|
-
private readonly approvalHandler?: HarnessOptions["approvalHandler"];
|
|
418
444
|
readonly uploadStore?: UploadStore;
|
|
419
445
|
private skillContextWindow = "";
|
|
420
446
|
private memoryStore?: MemoryStore;
|
|
@@ -500,7 +526,6 @@ export class AgentHarness {
|
|
|
500
526
|
this.environment = options.environment ?? "development";
|
|
501
527
|
this.modelProviderInjected = !!options.modelProvider;
|
|
502
528
|
this.modelProvider = options.modelProvider ?? createModelProvider("anthropic");
|
|
503
|
-
this.approvalHandler = options.approvalHandler;
|
|
504
529
|
this.uploadStore = options.uploadStore;
|
|
505
530
|
|
|
506
531
|
if (options.toolDefinitions?.length) {
|
|
@@ -767,10 +792,8 @@ export class AgentHarness {
|
|
|
767
792
|
this.registerConfiguredBuiltInTools(config);
|
|
768
793
|
const provider = this.parsedAgent.frontmatter.model?.provider ?? "anthropic";
|
|
769
794
|
const memoryConfig = resolveMemoryConfig(config);
|
|
770
|
-
// Only create modelProvider if one wasn't injected (for production use)
|
|
771
|
-
// Tests can inject a mock modelProvider via constructor options
|
|
772
795
|
if (!this.modelProviderInjected) {
|
|
773
|
-
this.modelProvider = createModelProvider(provider);
|
|
796
|
+
this.modelProvider = createModelProvider(provider, config?.providers);
|
|
774
797
|
}
|
|
775
798
|
const bridge = new LocalMcpBridge(config);
|
|
776
799
|
this.mcpBridge = bridge;
|
|
@@ -802,14 +825,13 @@ export class AgentHarness {
|
|
|
802
825
|
// Creating a new LatitudeTelemetry per run would break on the second call
|
|
803
826
|
// because @opentelemetry/api silently ignores repeated global registrations.
|
|
804
827
|
const telemetryEnabled = config?.telemetry?.enabled !== false;
|
|
805
|
-
const latitudeApiKey = config?.telemetry?.latitude?.apiKey;
|
|
806
|
-
const rawProjectId = config?.telemetry?.latitude?.projectId;
|
|
807
|
-
const latitudeProjectId = typeof rawProjectId === 'string' ? parseInt(rawProjectId, 10) : rawProjectId;
|
|
808
828
|
const latitudeBlock = config?.telemetry?.latitude;
|
|
829
|
+
const latApiKeyEnv = latitudeBlock?.apiKeyEnv ?? "LATITUDE_API_KEY";
|
|
830
|
+
const latProjectIdEnv = latitudeBlock?.projectIdEnv ?? "LATITUDE_PROJECT_ID";
|
|
831
|
+
const latitudeApiKey = process.env[latApiKeyEnv];
|
|
832
|
+
const rawProjectId = process.env[latProjectIdEnv];
|
|
833
|
+
const latitudeProjectId = rawProjectId ? parseInt(rawProjectId, 10) : undefined;
|
|
809
834
|
if (telemetryEnabled && latitudeApiKey && latitudeProjectId) {
|
|
810
|
-
// Surface genuine OTLP export failures. Suppress "duplicate registration"
|
|
811
|
-
// errors that fire when the dev server re-initializes the harness — the
|
|
812
|
-
// first registration persists and telemetry keeps working.
|
|
813
835
|
diag.setLogger(
|
|
814
836
|
{
|
|
815
837
|
error: (msg, ...args) => {
|
|
@@ -826,16 +848,10 @@ export class AgentHarness {
|
|
|
826
848
|
this.latitudeTelemetry = new LatitudeTelemetry(latitudeApiKey, { disableBatch: true });
|
|
827
849
|
} else if (telemetryEnabled && latitudeBlock && (!latitudeApiKey || !latitudeProjectId)) {
|
|
828
850
|
const missing: string[] = [];
|
|
829
|
-
if (!latitudeApiKey) missing.push(
|
|
830
|
-
if (!latitudeProjectId) missing.push(
|
|
831
|
-
const unknownKeys = Object.keys(latitudeBlock).filter(
|
|
832
|
-
(k) => !["apiKey", "projectId", "path", "documentPath"].includes(k),
|
|
833
|
-
);
|
|
834
|
-
const hint = unknownKeys.length > 0
|
|
835
|
-
? ` (found unknown key${unknownKeys.length > 1 ? "s" : ""}: ${unknownKeys.join(", ")} – did you mean "apiKey"?)`
|
|
836
|
-
: "";
|
|
851
|
+
if (!latitudeApiKey) missing.push(`${latApiKeyEnv} env var`);
|
|
852
|
+
if (!latitudeProjectId) missing.push(`${latProjectIdEnv} env var`);
|
|
837
853
|
console.warn(
|
|
838
|
-
`[poncho][telemetry] Latitude telemetry is configured but missing: ${missing.join(", ")}
|
|
854
|
+
`[poncho][telemetry] Latitude telemetry is configured but missing: ${missing.join(", ")}. Traces will NOT be sent.`,
|
|
839
855
|
);
|
|
840
856
|
}
|
|
841
857
|
}
|
|
@@ -867,8 +883,8 @@ export class AgentHarness {
|
|
|
867
883
|
const telemetry = this.latitudeTelemetry;
|
|
868
884
|
|
|
869
885
|
if (telemetry) {
|
|
870
|
-
const
|
|
871
|
-
const projectId = (
|
|
886
|
+
const latProjectIdEnv2 = config?.telemetry?.latitude?.projectIdEnv ?? "LATITUDE_PROJECT_ID";
|
|
887
|
+
const projectId = parseInt(process.env[latProjectIdEnv2] ?? "", 10) as number;
|
|
872
888
|
const rawPath = config?.telemetry?.latitude?.path ?? this.parsedAgent?.frontmatter.name ?? 'agent';
|
|
873
889
|
// Sanitize path for Latitude's DOCUMENT_PATH_REGEXP: /^([\w-]+\/)*([\w-.])+$/
|
|
874
890
|
const path = rawPath.replace(/[^\w\-./]/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '') || 'agent';
|
|
@@ -963,6 +979,7 @@ export class AgentHarness {
|
|
|
963
979
|
? platformMaxDurationSec * 800
|
|
964
980
|
: 0;
|
|
965
981
|
const messages: Message[] = [...(input.messages ?? [])];
|
|
982
|
+
const inputMessageCount = messages.length;
|
|
966
983
|
const events: AgentEvent[] = [];
|
|
967
984
|
|
|
968
985
|
const systemPrompt = renderAgentPrompt(agent, {
|
|
@@ -1024,41 +1041,43 @@ ${boundedMainMemory.trim()}`
|
|
|
1024
1041
|
contextWindow,
|
|
1025
1042
|
});
|
|
1026
1043
|
|
|
1027
|
-
if (input.
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1044
|
+
if (input.task != null) {
|
|
1045
|
+
if (input.files && input.files.length > 0) {
|
|
1046
|
+
const parts: ContentPart[] = [
|
|
1047
|
+
{ type: "text", text: input.task } satisfies TextContentPart,
|
|
1048
|
+
];
|
|
1049
|
+
for (const file of input.files) {
|
|
1050
|
+
if (this.uploadStore) {
|
|
1051
|
+
const buf = Buffer.from(file.data, "base64");
|
|
1052
|
+
const key = deriveUploadKey(buf, file.mediaType);
|
|
1053
|
+
const ref = await this.uploadStore.put(key, buf, file.mediaType);
|
|
1054
|
+
parts.push({
|
|
1055
|
+
type: "file",
|
|
1056
|
+
data: ref,
|
|
1057
|
+
mediaType: file.mediaType,
|
|
1058
|
+
filename: file.filename,
|
|
1059
|
+
} satisfies FileContentPart);
|
|
1060
|
+
} else {
|
|
1061
|
+
parts.push({
|
|
1062
|
+
type: "file",
|
|
1063
|
+
data: file.data,
|
|
1064
|
+
mediaType: file.mediaType,
|
|
1065
|
+
filename: file.filename,
|
|
1066
|
+
} satisfies FileContentPart);
|
|
1067
|
+
}
|
|
1049
1068
|
}
|
|
1069
|
+
messages.push({
|
|
1070
|
+
role: "user",
|
|
1071
|
+
content: parts,
|
|
1072
|
+
metadata: { timestamp: now(), id: randomUUID() },
|
|
1073
|
+
});
|
|
1074
|
+
} else {
|
|
1075
|
+
messages.push({
|
|
1076
|
+
role: "user",
|
|
1077
|
+
content: input.task,
|
|
1078
|
+
metadata: { timestamp: now(), id: randomUUID() },
|
|
1079
|
+
});
|
|
1050
1080
|
}
|
|
1051
|
-
messages.push({
|
|
1052
|
-
role: "user",
|
|
1053
|
-
content: parts,
|
|
1054
|
-
metadata: { timestamp: now(), id: randomUUID() },
|
|
1055
|
-
});
|
|
1056
|
-
} else {
|
|
1057
|
-
messages.push({
|
|
1058
|
-
role: "user",
|
|
1059
|
-
content: input.task,
|
|
1060
|
-
metadata: { timestamp: now(), id: randomUUID() },
|
|
1061
|
-
});
|
|
1062
1081
|
}
|
|
1063
1082
|
|
|
1064
1083
|
let responseText = "";
|
|
@@ -1346,9 +1365,7 @@ ${boundedMainMemory.trim()}`
|
|
|
1346
1365
|
const modelInstance = this.modelProvider(modelName);
|
|
1347
1366
|
const cachedMessages = addPromptCacheBreakpoints(coreMessages, modelInstance);
|
|
1348
1367
|
|
|
1349
|
-
// Stream response using Vercel AI SDK with telemetry enabled
|
|
1350
1368
|
const telemetryEnabled = this.loadedConfig?.telemetry?.enabled !== false;
|
|
1351
|
-
const latitudeApiKey = this.loadedConfig?.telemetry?.latitude?.apiKey;
|
|
1352
1369
|
|
|
1353
1370
|
const result = await streamText({
|
|
1354
1371
|
model: modelInstance,
|
|
@@ -1359,7 +1376,7 @@ ${boundedMainMemory.trim()}`
|
|
|
1359
1376
|
abortSignal: input.abortSignal,
|
|
1360
1377
|
...(typeof maxTokens === "number" ? { maxTokens } : {}),
|
|
1361
1378
|
experimental_telemetry: {
|
|
1362
|
-
isEnabled: telemetryEnabled && !!
|
|
1379
|
+
isEnabled: telemetryEnabled && !!this.latitudeTelemetry,
|
|
1363
1380
|
},
|
|
1364
1381
|
});
|
|
1365
1382
|
// Stream text chunks — enforce overall run timeout per chunk.
|
|
@@ -1597,47 +1614,35 @@ ${boundedMainMemory.trim()}`
|
|
|
1597
1614
|
input: call.input,
|
|
1598
1615
|
approvalId,
|
|
1599
1616
|
});
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
error: "Tool execution denied by approval policy",
|
|
1630
|
-
recoverable: true,
|
|
1631
|
-
});
|
|
1632
|
-
toolResultsForModel.push({
|
|
1633
|
-
type: "tool_result",
|
|
1634
|
-
tool_use_id: call.id,
|
|
1635
|
-
tool_name: runtimeToolName,
|
|
1636
|
-
content: "Tool error: Tool execution denied by approval policy",
|
|
1637
|
-
});
|
|
1638
|
-
continue;
|
|
1639
|
-
}
|
|
1640
|
-
yield pushEvent({ type: "tool:approval:granted", approvalId });
|
|
1617
|
+
|
|
1618
|
+
const assistantContent = JSON.stringify({
|
|
1619
|
+
text: fullText,
|
|
1620
|
+
tool_calls: toolCalls.map(tc => ({
|
|
1621
|
+
id: tc.id,
|
|
1622
|
+
name: exposedToolNames.get(tc.name) ?? tc.name,
|
|
1623
|
+
input: tc.input,
|
|
1624
|
+
})),
|
|
1625
|
+
});
|
|
1626
|
+
const assistantMsg: Message = {
|
|
1627
|
+
role: "assistant",
|
|
1628
|
+
content: assistantContent,
|
|
1629
|
+
metadata: { timestamp: now(), id: randomUUID(), step },
|
|
1630
|
+
};
|
|
1631
|
+
const deltaMessages = [...messages.slice(inputMessageCount), assistantMsg];
|
|
1632
|
+
yield pushEvent({
|
|
1633
|
+
type: "tool:approval:checkpoint",
|
|
1634
|
+
approvalId,
|
|
1635
|
+
tool: runtimeToolName,
|
|
1636
|
+
toolCallId: call.id,
|
|
1637
|
+
input: call.input,
|
|
1638
|
+
checkpointMessages: deltaMessages,
|
|
1639
|
+
pendingToolCalls: toolCalls.map(tc => ({
|
|
1640
|
+
id: tc.id,
|
|
1641
|
+
name: exposedToolNames.get(tc.name) ?? tc.name,
|
|
1642
|
+
input: tc.input,
|
|
1643
|
+
})),
|
|
1644
|
+
});
|
|
1645
|
+
return;
|
|
1641
1646
|
}
|
|
1642
1647
|
approvedCalls.push({
|
|
1643
1648
|
id: call.id,
|
|
@@ -1790,12 +1795,87 @@ ${boundedMainMemory.trim()}`
|
|
|
1790
1795
|
}
|
|
1791
1796
|
}
|
|
1792
1797
|
|
|
1798
|
+
async executeTools(
|
|
1799
|
+
calls: ToolCall[],
|
|
1800
|
+
context: ToolContext,
|
|
1801
|
+
): Promise<ToolExecutionResult[]> {
|
|
1802
|
+
return this.dispatcher.executeBatch(calls, context);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
async *continueFromToolResult(input: {
|
|
1806
|
+
messages: Message[];
|
|
1807
|
+
toolResults: Array<{ callId: string; toolName: string; result?: unknown; error?: string }>;
|
|
1808
|
+
conversationId?: string;
|
|
1809
|
+
parameters?: Record<string, unknown>;
|
|
1810
|
+
abortSignal?: AbortSignal;
|
|
1811
|
+
}): AsyncGenerator<AgentEvent> {
|
|
1812
|
+
const messages = [...input.messages];
|
|
1813
|
+
const lastMsg = messages[messages.length - 1];
|
|
1814
|
+
if (!lastMsg || lastMsg.role !== "assistant") {
|
|
1815
|
+
throw new Error("continueFromToolResult: last message must be an assistant message with tool calls");
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
let allToolCalls: Array<{ id: string; name: string; input: Record<string, unknown> }> = [];
|
|
1819
|
+
try {
|
|
1820
|
+
const parsed = JSON.parse(typeof lastMsg.content === "string" ? lastMsg.content : "");
|
|
1821
|
+
allToolCalls = parsed.tool_calls ?? [];
|
|
1822
|
+
} catch {
|
|
1823
|
+
throw new Error("continueFromToolResult: could not parse tool calls from last assistant message");
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
const providedMap = new Map(
|
|
1827
|
+
input.toolResults.map(r => [r.callId, r]),
|
|
1828
|
+
);
|
|
1829
|
+
const toolResultsForModel: Array<{
|
|
1830
|
+
type: "tool_result";
|
|
1831
|
+
tool_use_id: string;
|
|
1832
|
+
tool_name: string;
|
|
1833
|
+
content: string;
|
|
1834
|
+
}> = [];
|
|
1835
|
+
|
|
1836
|
+
for (const tc of allToolCalls) {
|
|
1837
|
+
const provided = providedMap.get(tc.id);
|
|
1838
|
+
if (provided) {
|
|
1839
|
+
toolResultsForModel.push({
|
|
1840
|
+
type: "tool_result",
|
|
1841
|
+
tool_use_id: tc.id,
|
|
1842
|
+
tool_name: provided.toolName,
|
|
1843
|
+
content: provided.error
|
|
1844
|
+
? `Tool error: ${provided.error}`
|
|
1845
|
+
: JSON.stringify(provided.result ?? null),
|
|
1846
|
+
});
|
|
1847
|
+
} else {
|
|
1848
|
+
toolResultsForModel.push({
|
|
1849
|
+
type: "tool_result",
|
|
1850
|
+
tool_use_id: tc.id,
|
|
1851
|
+
tool_name: tc.name,
|
|
1852
|
+
content: "Tool error: Tool execution deferred (pending approval checkpoint)",
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
messages.push({
|
|
1858
|
+
role: "tool",
|
|
1859
|
+
content: JSON.stringify(toolResultsForModel),
|
|
1860
|
+
metadata: { timestamp: Date.now(), id: randomUUID() },
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
yield* this.runWithTelemetry({
|
|
1864
|
+
messages,
|
|
1865
|
+
conversationId: input.conversationId,
|
|
1866
|
+
parameters: input.parameters,
|
|
1867
|
+
abortSignal: input.abortSignal,
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1793
1871
|
async runToCompletion(input: RunInput): Promise<HarnessRunOutput> {
|
|
1794
1872
|
const events: AgentEvent[] = [];
|
|
1795
1873
|
let runId = "";
|
|
1796
1874
|
let finalResult: RunResult | undefined;
|
|
1797
1875
|
const messages: Message[] = [...(input.messages ?? [])];
|
|
1798
|
-
|
|
1876
|
+
if (input.task != null) {
|
|
1877
|
+
messages.push({ role: "user", content: input.task });
|
|
1878
|
+
}
|
|
1799
1879
|
|
|
1800
1880
|
for await (const event of this.runWithTelemetry(input)) {
|
|
1801
1881
|
events.push(event);
|
package/src/latitude-capture.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export interface LatitudeCaptureConfig {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
apiKeyEnv?: string;
|
|
14
|
+
projectIdEnv?: string;
|
|
15
15
|
path?: string;
|
|
16
16
|
defaultPath?: string;
|
|
17
17
|
}
|
|
@@ -26,15 +26,14 @@ export class LatitudeCapture {
|
|
|
26
26
|
private readonly path?: string;
|
|
27
27
|
|
|
28
28
|
constructor(config?: LatitudeCaptureConfig) {
|
|
29
|
-
|
|
29
|
+
const apiKeyEnv = config?.apiKeyEnv ?? "LATITUDE_API_KEY";
|
|
30
|
+
this.apiKey = process.env[apiKeyEnv];
|
|
30
31
|
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
? Number.parseInt(rawProjectId, 10)
|
|
37
|
-
: Number.NaN;
|
|
32
|
+
const projectIdEnv = config?.projectIdEnv ?? "LATITUDE_PROJECT_ID";
|
|
33
|
+
const rawProjectId = process.env[projectIdEnv];
|
|
34
|
+
const projectIdNumber = rawProjectId
|
|
35
|
+
? Number.parseInt(rawProjectId, 10)
|
|
36
|
+
: Number.NaN;
|
|
38
37
|
this.projectId = Number.isFinite(projectIdNumber) ? projectIdNumber : undefined;
|
|
39
38
|
|
|
40
39
|
const rawPath =
|
package/src/memory.ts
CHANGED
|
@@ -17,8 +17,8 @@ export interface MainMemory {
|
|
|
17
17
|
export interface MemoryConfig {
|
|
18
18
|
enabled?: boolean;
|
|
19
19
|
provider?: StateProviderName;
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
urlEnv?: string;
|
|
21
|
+
tokenEnv?: string;
|
|
22
22
|
table?: string;
|
|
23
23
|
region?: string;
|
|
24
24
|
ttl?: number;
|
|
@@ -495,16 +495,10 @@ export const createMemoryStore = (
|
|
|
495
495
|
return new InMemoryMemoryStore(ttl);
|
|
496
496
|
}
|
|
497
497
|
if (provider === "upstash") {
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
"";
|
|
503
|
-
const token =
|
|
504
|
-
config?.token ??
|
|
505
|
-
process.env.UPSTASH_REDIS_REST_TOKEN ??
|
|
506
|
-
process.env.KV_REST_API_TOKEN ??
|
|
507
|
-
"";
|
|
498
|
+
const urlEnv = config?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
|
|
499
|
+
const tokenEnv = config?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
|
|
500
|
+
const url = process.env[urlEnv] ?? "";
|
|
501
|
+
const token = process.env[tokenEnv] ?? "";
|
|
508
502
|
if (url && token) {
|
|
509
503
|
return new UpstashMemoryStore({
|
|
510
504
|
baseUrl: url,
|
|
@@ -516,7 +510,8 @@ export const createMemoryStore = (
|
|
|
516
510
|
return new InMemoryMemoryStore(ttl);
|
|
517
511
|
}
|
|
518
512
|
if (provider === "redis") {
|
|
519
|
-
const
|
|
513
|
+
const urlEnv = config?.urlEnv ?? "REDIS_URL";
|
|
514
|
+
const url = process.env[urlEnv] ?? "";
|
|
520
515
|
if (url) {
|
|
521
516
|
return new RedisMemoryStore({
|
|
522
517
|
url,
|
package/src/model-factory.ts
CHANGED
|
@@ -49,23 +49,30 @@ export const getModelContextWindow = (modelName: string): number => {
|
|
|
49
49
|
return best ? MODEL_CONTEXT_WINDOWS[best]! : DEFAULT_CONTEXT_WINDOW;
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
export interface ProviderConfig {
|
|
53
|
+
openai?: { apiKeyEnv?: string };
|
|
54
|
+
anthropic?: { apiKeyEnv?: string };
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
/**
|
|
53
|
-
* Creates a model provider factory for the specified AI provider
|
|
54
|
-
*
|
|
55
|
-
*
|
|
58
|
+
* Creates a model provider factory for the specified AI provider.
|
|
59
|
+
* API keys are read from environment variables; override the env var
|
|
60
|
+
* name via the `providers` config in `poncho.config.js`.
|
|
56
61
|
*/
|
|
57
|
-
export const createModelProvider = (provider?: string): ModelProviderFactory => {
|
|
62
|
+
export const createModelProvider = (provider?: string, config?: ProviderConfig): ModelProviderFactory => {
|
|
58
63
|
const normalized = (provider ?? "anthropic").toLowerCase();
|
|
59
64
|
|
|
60
65
|
if (normalized === "openai") {
|
|
66
|
+
const apiKeyEnv = config?.openai?.apiKeyEnv ?? "OPENAI_API_KEY";
|
|
61
67
|
const openai = createOpenAI({
|
|
62
|
-
apiKey: process.env
|
|
68
|
+
apiKey: process.env[apiKeyEnv],
|
|
63
69
|
});
|
|
64
70
|
return (modelName: string) => openai(modelName);
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
const apiKeyEnv = config?.anthropic?.apiKeyEnv ?? "ANTHROPIC_API_KEY";
|
|
67
74
|
const anthropic = createAnthropic({
|
|
68
|
-
apiKey: process.env
|
|
75
|
+
apiKey: process.env[apiKeyEnv],
|
|
69
76
|
});
|
|
70
77
|
return (modelName: string) => anthropic(modelName);
|
|
71
78
|
};
|
package/src/state.ts
CHANGED
|
@@ -30,7 +30,11 @@ export interface Conversation {
|
|
|
30
30
|
approvalId: string;
|
|
31
31
|
runId: string;
|
|
32
32
|
tool: string;
|
|
33
|
+
toolCallId?: string;
|
|
33
34
|
input: Record<string, unknown>;
|
|
35
|
+
checkpointMessages?: Message[];
|
|
36
|
+
baseMessageCount?: number;
|
|
37
|
+
pendingToolCalls?: Array<{ id: string; name: string; input: Record<string, unknown> }>;
|
|
34
38
|
}>;
|
|
35
39
|
ownerId: string;
|
|
36
40
|
tenantId: string | null;
|
|
@@ -57,8 +61,8 @@ export type StateProviderName =
|
|
|
57
61
|
export interface StateConfig {
|
|
58
62
|
provider?: StateProviderName;
|
|
59
63
|
ttl?: number;
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
urlEnv?: string;
|
|
65
|
+
tokenEnv?: string;
|
|
62
66
|
table?: string;
|
|
63
67
|
region?: string;
|
|
64
68
|
}
|
|
@@ -1162,15 +1166,18 @@ export const createStateStore = (
|
|
|
1162
1166
|
return new InMemoryStateStore(ttl);
|
|
1163
1167
|
}
|
|
1164
1168
|
if (provider === "upstash") {
|
|
1165
|
-
const
|
|
1166
|
-
const
|
|
1169
|
+
const urlEnv = config?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
|
|
1170
|
+
const tokenEnv = config?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
|
|
1171
|
+
const url = process.env[urlEnv] ?? "";
|
|
1172
|
+
const token = process.env[tokenEnv] ?? "";
|
|
1167
1173
|
if (url && token) {
|
|
1168
1174
|
return new UpstashStateStore(url, token, ttl);
|
|
1169
1175
|
}
|
|
1170
1176
|
return new InMemoryStateStore(ttl);
|
|
1171
1177
|
}
|
|
1172
1178
|
if (provider === "redis") {
|
|
1173
|
-
const
|
|
1179
|
+
const urlEnv = config?.urlEnv ?? "REDIS_URL";
|
|
1180
|
+
const url = process.env[urlEnv] ?? "";
|
|
1174
1181
|
if (url) {
|
|
1175
1182
|
return new RedisLikeStateStore(url, ttl);
|
|
1176
1183
|
}
|
|
@@ -1200,19 +1207,18 @@ export const createConversationStore = (
|
|
|
1200
1207
|
return new InMemoryConversationStore(ttl);
|
|
1201
1208
|
}
|
|
1202
1209
|
if (provider === "upstash") {
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const token =
|
|
1207
|
-
config?.token ??
|
|
1208
|
-
(process.env.UPSTASH_REDIS_REST_TOKEN || process.env.KV_REST_API_TOKEN || "");
|
|
1210
|
+
const urlEnv = config?.urlEnv ?? (process.env.UPSTASH_REDIS_REST_URL ? "UPSTASH_REDIS_REST_URL" : "KV_REST_API_URL");
|
|
1211
|
+
const tokenEnv = config?.tokenEnv ?? (process.env.UPSTASH_REDIS_REST_TOKEN ? "UPSTASH_REDIS_REST_TOKEN" : "KV_REST_API_TOKEN");
|
|
1212
|
+
const url = process.env[urlEnv] ?? "";
|
|
1213
|
+
const token = process.env[tokenEnv] ?? "";
|
|
1209
1214
|
if (url && token) {
|
|
1210
1215
|
return new UpstashConversationStore(url, token, workingDir, ttl, options?.agentId);
|
|
1211
1216
|
}
|
|
1212
1217
|
return new InMemoryConversationStore(ttl);
|
|
1213
1218
|
}
|
|
1214
1219
|
if (provider === "redis") {
|
|
1215
|
-
const
|
|
1220
|
+
const urlEnv = config?.urlEnv ?? "REDIS_URL";
|
|
1221
|
+
const url = process.env[urlEnv] ?? "";
|
|
1216
1222
|
if (url) {
|
|
1217
1223
|
return new RedisLikeConversationStore(url, workingDir, ttl, options?.agentId);
|
|
1218
1224
|
}
|
package/src/telemetry.ts
CHANGED
package/test/telemetry.test.ts
CHANGED
|
@@ -15,7 +15,7 @@ describe("telemetry emitter", () => {
|
|
|
15
15
|
|
|
16
16
|
const emitter = new TelemetryEmitter({
|
|
17
17
|
otlp: "https://otel.example.com/v1/logs",
|
|
18
|
-
latitude: {
|
|
18
|
+
latitude: { apiKeyEnv: "LATITUDE_API_KEY" },
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
await expect(
|
|
@@ -32,8 +32,8 @@ describe("telemetry emitter", () => {
|
|
|
32
32
|
|
|
33
33
|
const emitter = new TelemetryEmitter({
|
|
34
34
|
latitude: {
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
apiKeyEnv: "LATITUDE_API_KEY",
|
|
36
|
+
projectIdEnv: "LATITUDE_PROJECT_ID",
|
|
37
37
|
documentPath: "agents/support-agent/AGENT.md",
|
|
38
38
|
},
|
|
39
39
|
});
|