@poncho-ai/harness 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +14 -0
- package/.turbo/turbo-test.log +22 -0
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/dist/index.d.ts +416 -0
- package/dist/index.js +3015 -0
- package/package.json +53 -0
- package/src/agent-parser.ts +127 -0
- package/src/anthropic-client.ts +134 -0
- package/src/config.ts +141 -0
- package/src/default-tools.ts +89 -0
- package/src/harness.ts +522 -0
- package/src/index.ts +17 -0
- package/src/latitude-capture.ts +108 -0
- package/src/local-tools.ts +108 -0
- package/src/mcp.ts +287 -0
- package/src/memory.ts +700 -0
- package/src/model-client.ts +44 -0
- package/src/model-factory.ts +14 -0
- package/src/openai-client.ts +169 -0
- package/src/skill-context.ts +259 -0
- package/src/skill-tools.ts +357 -0
- package/src/state.ts +1017 -0
- package/src/telemetry.ts +108 -0
- package/src/tool-dispatcher.ts +69 -0
- package/test/agent-parser.test.ts +39 -0
- package/test/harness.test.ts +716 -0
- package/test/mcp.test.ts +82 -0
- package/test/memory.test.ts +50 -0
- package/test/model-factory.test.ts +16 -0
- package/test/state.test.ts +43 -0
- package/test/telemetry.test.ts +57 -0
- package/tsconfig.json +8 -0
package/src/harness.ts
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
AgentEvent,
|
|
4
|
+
Message,
|
|
5
|
+
RunInput,
|
|
6
|
+
RunResult,
|
|
7
|
+
ToolContext,
|
|
8
|
+
ToolDefinition,
|
|
9
|
+
} from "@poncho-ai/sdk";
|
|
10
|
+
import { parseAgentFile, renderAgentPrompt, type ParsedAgent } from "./agent-parser.js";
|
|
11
|
+
import { loadPonchoConfig, resolveMemoryConfig, type PonchoConfig } from "./config.js";
|
|
12
|
+
import { createDefaultTools, createWriteTool } from "./default-tools.js";
|
|
13
|
+
import { LatitudeCapture } from "./latitude-capture.js";
|
|
14
|
+
import {
|
|
15
|
+
createMemoryStore,
|
|
16
|
+
createMemoryTools,
|
|
17
|
+
type MemoryStore,
|
|
18
|
+
} from "./memory.js";
|
|
19
|
+
import { LocalMcpBridge } from "./mcp.js";
|
|
20
|
+
import type { ModelClient, ModelResponse } from "./model-client.js";
|
|
21
|
+
import { createModelClient } from "./model-factory.js";
|
|
22
|
+
import { buildSkillContextWindow, loadSkillMetadata } from "./skill-context.js";
|
|
23
|
+
import { createSkillTools } from "./skill-tools.js";
|
|
24
|
+
import { ToolDispatcher } from "./tool-dispatcher.js";
|
|
25
|
+
|
|
26
|
+
export interface HarnessOptions {
|
|
27
|
+
workingDir?: string;
|
|
28
|
+
environment?: "development" | "staging" | "production";
|
|
29
|
+
toolDefinitions?: ToolDefinition[];
|
|
30
|
+
approvalHandler?: (request: {
|
|
31
|
+
tool: string;
|
|
32
|
+
input: Record<string, unknown>;
|
|
33
|
+
runId: string;
|
|
34
|
+
step: number;
|
|
35
|
+
approvalId: string;
|
|
36
|
+
}) => Promise<boolean> | boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface HarnessRunOutput {
|
|
40
|
+
runId: string;
|
|
41
|
+
result: RunResult;
|
|
42
|
+
events: AgentEvent[];
|
|
43
|
+
messages: Message[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const now = (): number => Date.now();
|
|
47
|
+
const MAX_CONTEXT_MESSAGES = 40;
|
|
48
|
+
|
|
49
|
+
const trimMessageWindow = (messages: Message[]): Message[] =>
|
|
50
|
+
messages.length <= MAX_CONTEXT_MESSAGES
|
|
51
|
+
? messages
|
|
52
|
+
: messages.slice(messages.length - MAX_CONTEXT_MESSAGES);
|
|
53
|
+
|
|
54
|
+
const DEVELOPMENT_MODE_CONTEXT = `## Development Mode Context
|
|
55
|
+
|
|
56
|
+
You are running locally in development mode. Treat this as an editable agent workspace.
|
|
57
|
+
|
|
58
|
+
When users ask about customization:
|
|
59
|
+
- Explain and edit \`poncho.config.js\` for model/provider, storage+memory, auth, telemetry, and MCP settings.
|
|
60
|
+
- Help create or update local skills under \`skills/<skill-name>/SKILL.md\`.
|
|
61
|
+
- For executable skills, add JavaScript/TypeScript scripts under \`skills/<skill-name>/scripts/\` and run them via \`run_skill_script\`.
|
|
62
|
+
- For setup, skills, MCP, auth, storage, telemetry, or "how do I..." questions, proactively read \`README.md\` with \`read_file\` before answering.
|
|
63
|
+
- Prefer quoting concrete commands and examples from \`README.md\` over guessing.
|
|
64
|
+
- Keep edits minimal, preserve unrelated settings/code, and summarize what changed.`;
|
|
65
|
+
|
|
66
|
+
export class AgentHarness {
|
|
67
|
+
private readonly workingDir: string;
|
|
68
|
+
private readonly environment: HarnessOptions["environment"];
|
|
69
|
+
private modelClient: ModelClient;
|
|
70
|
+
private readonly dispatcher = new ToolDispatcher();
|
|
71
|
+
private readonly approvalHandler?: HarnessOptions["approvalHandler"];
|
|
72
|
+
private skillContextWindow = "";
|
|
73
|
+
private memoryStore?: MemoryStore;
|
|
74
|
+
|
|
75
|
+
private parsedAgent?: ParsedAgent;
|
|
76
|
+
private mcpBridge?: LocalMcpBridge;
|
|
77
|
+
|
|
78
|
+
private getConfiguredToolFlag(
|
|
79
|
+
config: PonchoConfig | undefined,
|
|
80
|
+
name: keyof NonNullable<NonNullable<PonchoConfig["tools"]>["defaults"]>,
|
|
81
|
+
): boolean | undefined {
|
|
82
|
+
const defaults = config?.tools?.defaults;
|
|
83
|
+
const environment = this.environment ?? "development";
|
|
84
|
+
const envOverrides = config?.tools?.byEnvironment?.[environment];
|
|
85
|
+
return envOverrides?.[name] ?? defaults?.[name];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private isBuiltInToolEnabled(config: PonchoConfig | undefined, name: string): boolean {
|
|
89
|
+
if (name === "write_file") {
|
|
90
|
+
const allowedByEnvironment = this.shouldEnableWriteTool();
|
|
91
|
+
const configured = this.getConfiguredToolFlag(config, "write_file");
|
|
92
|
+
return allowedByEnvironment && configured !== false;
|
|
93
|
+
}
|
|
94
|
+
if (name === "list_directory") {
|
|
95
|
+
const configured = this.getConfiguredToolFlag(config, "list_directory");
|
|
96
|
+
return configured !== false;
|
|
97
|
+
}
|
|
98
|
+
if (name === "read_file") {
|
|
99
|
+
const configured = this.getConfiguredToolFlag(config, "read_file");
|
|
100
|
+
return configured !== false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private registerIfMissing(tool: ToolDefinition): void {
|
|
106
|
+
if (!this.dispatcher.get(tool.name)) {
|
|
107
|
+
this.dispatcher.register(tool);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private registerConfiguredBuiltInTools(config: PonchoConfig | undefined): void {
|
|
112
|
+
for (const tool of createDefaultTools(this.workingDir)) {
|
|
113
|
+
if (this.isBuiltInToolEnabled(config, tool.name)) {
|
|
114
|
+
this.registerIfMissing(tool);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (this.isBuiltInToolEnabled(config, "write_file")) {
|
|
118
|
+
this.registerIfMissing(createWriteTool(this.workingDir));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private shouldEnableWriteTool(): boolean {
|
|
123
|
+
const override = process.env.PONCHO_FS_WRITE?.toLowerCase();
|
|
124
|
+
if (override === "1" || override === "true" || override === "yes") {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (override === "0" || override === "false" || override === "no") {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return this.environment !== "production";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
constructor(options: HarnessOptions = {}) {
|
|
134
|
+
this.workingDir = options.workingDir ?? process.cwd();
|
|
135
|
+
this.environment = options.environment ?? "development";
|
|
136
|
+
this.modelClient = createModelClient("anthropic");
|
|
137
|
+
this.approvalHandler = options.approvalHandler;
|
|
138
|
+
|
|
139
|
+
if (options.toolDefinitions?.length) {
|
|
140
|
+
this.dispatcher.registerMany(options.toolDefinitions);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async initialize(): Promise<void> {
|
|
145
|
+
this.parsedAgent = await parseAgentFile(this.workingDir);
|
|
146
|
+
const config = await loadPonchoConfig(this.workingDir);
|
|
147
|
+
this.registerConfiguredBuiltInTools(config);
|
|
148
|
+
const provider = this.parsedAgent.frontmatter.model?.provider ?? "anthropic";
|
|
149
|
+
const memoryConfig = resolveMemoryConfig(config);
|
|
150
|
+
const latitudeCapture = new LatitudeCapture({
|
|
151
|
+
apiKey:
|
|
152
|
+
config?.telemetry?.latitude?.apiKey ?? process.env.LATITUDE_API_KEY,
|
|
153
|
+
projectId:
|
|
154
|
+
config?.telemetry?.latitude?.projectId ??
|
|
155
|
+
process.env.LATITUDE_PROJECT_ID,
|
|
156
|
+
path:
|
|
157
|
+
config?.telemetry?.latitude?.path ??
|
|
158
|
+
config?.telemetry?.latitude?.documentPath ??
|
|
159
|
+
process.env.LATITUDE_PATH ??
|
|
160
|
+
process.env.LATITUDE_DOCUMENT_PATH,
|
|
161
|
+
defaultPath: `agents/${this.parsedAgent.frontmatter.name}/model-call`,
|
|
162
|
+
});
|
|
163
|
+
this.modelClient = createModelClient(provider, { latitudeCapture });
|
|
164
|
+
const bridge = new LocalMcpBridge(config);
|
|
165
|
+
this.mcpBridge = bridge;
|
|
166
|
+
const extraSkillPaths = config?.skillPaths;
|
|
167
|
+
const skillMetadata = await loadSkillMetadata(this.workingDir, extraSkillPaths);
|
|
168
|
+
this.skillContextWindow = buildSkillContextWindow(skillMetadata);
|
|
169
|
+
this.dispatcher.registerMany(createSkillTools(skillMetadata));
|
|
170
|
+
if (memoryConfig?.enabled) {
|
|
171
|
+
this.memoryStore = createMemoryStore(
|
|
172
|
+
this.parsedAgent.frontmatter.name,
|
|
173
|
+
memoryConfig,
|
|
174
|
+
{ workingDir: this.workingDir },
|
|
175
|
+
);
|
|
176
|
+
this.dispatcher.registerMany(
|
|
177
|
+
createMemoryTools(this.memoryStore, {
|
|
178
|
+
maxRecallConversations: memoryConfig.maxRecallConversations,
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
await bridge.startLocalServers();
|
|
183
|
+
this.dispatcher.registerMany(await bridge.loadTools());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async shutdown(): Promise<void> {
|
|
187
|
+
await this.mcpBridge?.stopLocalServers();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
listTools(): ToolDefinition[] {
|
|
191
|
+
return this.dispatcher.list();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async *run(input: RunInput): AsyncGenerator<AgentEvent> {
|
|
195
|
+
if (!this.parsedAgent) {
|
|
196
|
+
await this.initialize();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const agent = this.parsedAgent as ParsedAgent;
|
|
200
|
+
const runId = `run_${randomUUID()}`;
|
|
201
|
+
const start = now();
|
|
202
|
+
const maxSteps = agent.frontmatter.limits?.maxSteps ?? 50;
|
|
203
|
+
const timeoutMs = (agent.frontmatter.limits?.timeout ?? 300) * 1000;
|
|
204
|
+
const messages: Message[] = [...(input.messages ?? [])];
|
|
205
|
+
const events: AgentEvent[] = [];
|
|
206
|
+
|
|
207
|
+
const systemPrompt = renderAgentPrompt(agent, {
|
|
208
|
+
parameters: input.parameters,
|
|
209
|
+
runtime: {
|
|
210
|
+
runId,
|
|
211
|
+
agentId: agent.frontmatter.name,
|
|
212
|
+
environment: this.environment,
|
|
213
|
+
workingDir: this.workingDir,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
const developmentContext =
|
|
217
|
+
this.environment === "development" ? `\n\n${DEVELOPMENT_MODE_CONTEXT}` : "";
|
|
218
|
+
const promptWithSkills = this.skillContextWindow
|
|
219
|
+
? `${systemPrompt}${developmentContext}\n\n${this.skillContextWindow}`
|
|
220
|
+
: `${systemPrompt}${developmentContext}`;
|
|
221
|
+
const mainMemory = this.memoryStore
|
|
222
|
+
? await this.memoryStore.getMainMemory()
|
|
223
|
+
: undefined;
|
|
224
|
+
const boundedMainMemory =
|
|
225
|
+
mainMemory && mainMemory.content.length > 4000
|
|
226
|
+
? `${mainMemory.content.slice(0, 4000)}\n...[truncated]`
|
|
227
|
+
: mainMemory?.content;
|
|
228
|
+
const memoryContext =
|
|
229
|
+
boundedMainMemory && boundedMainMemory.trim().length > 0
|
|
230
|
+
? `
|
|
231
|
+
## Persistent Memory
|
|
232
|
+
|
|
233
|
+
${boundedMainMemory.trim()}`
|
|
234
|
+
: "";
|
|
235
|
+
const integrityPrompt = `${promptWithSkills}${memoryContext}
|
|
236
|
+
|
|
237
|
+
## Execution Integrity
|
|
238
|
+
|
|
239
|
+
- Do not claim that you executed a tool unless you actually emitted a tool call in this run.
|
|
240
|
+
- Do not fabricate "Tool Used" or "Tool Result" logs as plain text.
|
|
241
|
+
- Never output faux execution transcripts, markdown tool logs, or "Tool Used/Result" sections.
|
|
242
|
+
- If no suitable tool is available, explicitly say that and ask for guidance.`;
|
|
243
|
+
|
|
244
|
+
const pushEvent = (event: AgentEvent): AgentEvent => {
|
|
245
|
+
events.push(event);
|
|
246
|
+
return event;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
yield pushEvent({
|
|
250
|
+
type: "run:started",
|
|
251
|
+
runId,
|
|
252
|
+
agentId: agent.frontmatter.name,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
messages.push({
|
|
256
|
+
role: "user",
|
|
257
|
+
content: input.task,
|
|
258
|
+
metadata: { timestamp: now(), id: randomUUID() },
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
let responseText = "";
|
|
262
|
+
let totalInputTokens = 0;
|
|
263
|
+
let totalOutputTokens = 0;
|
|
264
|
+
|
|
265
|
+
for (let step = 1; step <= maxSteps; step += 1) {
|
|
266
|
+
if (now() - start > timeoutMs) {
|
|
267
|
+
yield pushEvent({
|
|
268
|
+
type: "run:error",
|
|
269
|
+
runId,
|
|
270
|
+
error: {
|
|
271
|
+
code: "TIMEOUT",
|
|
272
|
+
message: `Run exceeded timeout of ${Math.floor(timeoutMs / 1000)}s`,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const stepStart = now();
|
|
279
|
+
yield pushEvent({ type: "step:started", step });
|
|
280
|
+
yield pushEvent({ type: "model:request", tokens: 0 });
|
|
281
|
+
|
|
282
|
+
const modelCallInput = {
|
|
283
|
+
modelName: agent.frontmatter.model?.name ?? "claude-opus-4-5",
|
|
284
|
+
temperature: agent.frontmatter.model?.temperature,
|
|
285
|
+
maxTokens: agent.frontmatter.model?.maxTokens,
|
|
286
|
+
systemPrompt: integrityPrompt,
|
|
287
|
+
messages: trimMessageWindow(messages),
|
|
288
|
+
tools: this.dispatcher.list(),
|
|
289
|
+
};
|
|
290
|
+
let modelResponse: ModelResponse | undefined;
|
|
291
|
+
let streamedAnyChunk = false;
|
|
292
|
+
|
|
293
|
+
if (this.modelClient.generateStream) {
|
|
294
|
+
for await (const streamEvent of this.modelClient.generateStream(modelCallInput)) {
|
|
295
|
+
if (streamEvent.type === "chunk" && streamEvent.content.length > 0) {
|
|
296
|
+
streamedAnyChunk = true;
|
|
297
|
+
yield pushEvent({ type: "model:chunk", content: streamEvent.content });
|
|
298
|
+
}
|
|
299
|
+
if (streamEvent.type === "final") {
|
|
300
|
+
modelResponse = streamEvent.response;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
modelResponse = await this.modelClient.generate(modelCallInput);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!modelResponse) {
|
|
308
|
+
throw new Error("Model response ended without final payload");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
totalInputTokens += modelResponse.usage.input;
|
|
312
|
+
totalOutputTokens += modelResponse.usage.output;
|
|
313
|
+
|
|
314
|
+
if (!streamedAnyChunk && modelResponse.text) {
|
|
315
|
+
yield pushEvent({ type: "model:chunk", content: modelResponse.text });
|
|
316
|
+
}
|
|
317
|
+
yield pushEvent({
|
|
318
|
+
type: "model:response",
|
|
319
|
+
usage: {
|
|
320
|
+
input: modelResponse.usage.input,
|
|
321
|
+
output: modelResponse.usage.output,
|
|
322
|
+
cached: 0,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (modelResponse.toolCalls.length === 0) {
|
|
327
|
+
responseText = modelResponse.text;
|
|
328
|
+
yield pushEvent({
|
|
329
|
+
type: "step:completed",
|
|
330
|
+
step,
|
|
331
|
+
duration: now() - stepStart,
|
|
332
|
+
});
|
|
333
|
+
const result: RunResult = {
|
|
334
|
+
status: "completed",
|
|
335
|
+
response: responseText,
|
|
336
|
+
steps: step,
|
|
337
|
+
tokens: {
|
|
338
|
+
input: totalInputTokens,
|
|
339
|
+
output: totalOutputTokens,
|
|
340
|
+
cached: 0,
|
|
341
|
+
},
|
|
342
|
+
duration: now() - start,
|
|
343
|
+
};
|
|
344
|
+
yield pushEvent({ type: "run:completed", runId, result });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const toolContext: ToolContext = {
|
|
349
|
+
runId,
|
|
350
|
+
agentId: agent.frontmatter.name,
|
|
351
|
+
step,
|
|
352
|
+
workingDir: this.workingDir,
|
|
353
|
+
parameters: input.parameters ?? {},
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const toolResultsForModel: Array<{
|
|
357
|
+
type: "tool_result";
|
|
358
|
+
tool_use_id: string;
|
|
359
|
+
content: string;
|
|
360
|
+
}> = [];
|
|
361
|
+
|
|
362
|
+
const approvedCalls: Array<{
|
|
363
|
+
id: string;
|
|
364
|
+
name: string;
|
|
365
|
+
input: Record<string, unknown>;
|
|
366
|
+
}> = [];
|
|
367
|
+
|
|
368
|
+
for (const call of modelResponse.toolCalls) {
|
|
369
|
+
yield pushEvent({ type: "tool:started", tool: call.name, input: call.input });
|
|
370
|
+
const definition = this.dispatcher.get(call.name);
|
|
371
|
+
if (definition?.requiresApproval) {
|
|
372
|
+
const approvalId = `approval_${randomUUID()}`;
|
|
373
|
+
yield pushEvent({
|
|
374
|
+
type: "tool:approval:required",
|
|
375
|
+
tool: call.name,
|
|
376
|
+
input: call.input,
|
|
377
|
+
approvalId,
|
|
378
|
+
});
|
|
379
|
+
const approved = this.approvalHandler
|
|
380
|
+
? await this.approvalHandler({
|
|
381
|
+
tool: call.name,
|
|
382
|
+
input: call.input,
|
|
383
|
+
runId,
|
|
384
|
+
step,
|
|
385
|
+
approvalId,
|
|
386
|
+
})
|
|
387
|
+
: false;
|
|
388
|
+
if (!approved) {
|
|
389
|
+
yield pushEvent({
|
|
390
|
+
type: "tool:approval:denied",
|
|
391
|
+
approvalId,
|
|
392
|
+
reason: "No approval handler granted execution",
|
|
393
|
+
});
|
|
394
|
+
yield pushEvent({
|
|
395
|
+
type: "tool:error",
|
|
396
|
+
tool: call.name,
|
|
397
|
+
error: "Tool execution denied by approval policy",
|
|
398
|
+
recoverable: true,
|
|
399
|
+
});
|
|
400
|
+
toolResultsForModel.push({
|
|
401
|
+
type: "tool_result",
|
|
402
|
+
tool_use_id: call.id,
|
|
403
|
+
content: "Tool error: Tool execution denied by approval policy",
|
|
404
|
+
});
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
yield pushEvent({ type: "tool:approval:granted", approvalId });
|
|
408
|
+
}
|
|
409
|
+
approvedCalls.push({
|
|
410
|
+
id: call.id,
|
|
411
|
+
name: call.name,
|
|
412
|
+
input: call.input,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
const batchStart = now();
|
|
416
|
+
const batchResults =
|
|
417
|
+
approvedCalls.length > 0
|
|
418
|
+
? await this.dispatcher.executeBatch(approvedCalls, toolContext)
|
|
419
|
+
: [];
|
|
420
|
+
|
|
421
|
+
for (const result of batchResults) {
|
|
422
|
+
if (result.error) {
|
|
423
|
+
yield pushEvent({
|
|
424
|
+
type: "tool:error",
|
|
425
|
+
tool: result.tool,
|
|
426
|
+
error: result.error,
|
|
427
|
+
recoverable: true,
|
|
428
|
+
});
|
|
429
|
+
toolResultsForModel.push({
|
|
430
|
+
type: "tool_result",
|
|
431
|
+
tool_use_id: result.callId,
|
|
432
|
+
content: `Tool error: ${result.error}`,
|
|
433
|
+
});
|
|
434
|
+
} else {
|
|
435
|
+
yield pushEvent({
|
|
436
|
+
type: "tool:completed",
|
|
437
|
+
tool: result.tool,
|
|
438
|
+
output: result.output,
|
|
439
|
+
duration: now() - batchStart,
|
|
440
|
+
});
|
|
441
|
+
toolResultsForModel.push({
|
|
442
|
+
type: "tool_result",
|
|
443
|
+
tool_use_id: result.callId,
|
|
444
|
+
content: JSON.stringify(result.output ?? null),
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
messages.push({
|
|
450
|
+
role: "assistant",
|
|
451
|
+
content: modelResponse.text || `[tool calls: ${modelResponse.toolCalls.length}]`,
|
|
452
|
+
metadata: { timestamp: now(), id: randomUUID(), step },
|
|
453
|
+
});
|
|
454
|
+
messages.push({
|
|
455
|
+
role: "tool",
|
|
456
|
+
content: JSON.stringify(toolResultsForModel),
|
|
457
|
+
metadata: { timestamp: now(), id: randomUUID(), step },
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
yield pushEvent({
|
|
461
|
+
type: "step:completed",
|
|
462
|
+
step,
|
|
463
|
+
duration: now() - stepStart,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
yield {
|
|
468
|
+
type: "run:error",
|
|
469
|
+
runId,
|
|
470
|
+
error: {
|
|
471
|
+
code: "MAX_STEPS_EXCEEDED",
|
|
472
|
+
message: `Run reached maximum of ${maxSteps} steps`,
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async runToCompletion(input: RunInput): Promise<HarnessRunOutput> {
|
|
478
|
+
const events: AgentEvent[] = [];
|
|
479
|
+
let runId = "";
|
|
480
|
+
let finalResult: RunResult | undefined;
|
|
481
|
+
const messages: Message[] = [...(input.messages ?? [])];
|
|
482
|
+
messages.push({ role: "user", content: input.task });
|
|
483
|
+
|
|
484
|
+
for await (const event of this.run(input)) {
|
|
485
|
+
events.push(event);
|
|
486
|
+
if (event.type === "run:started") {
|
|
487
|
+
runId = event.runId;
|
|
488
|
+
}
|
|
489
|
+
if (event.type === "run:completed") {
|
|
490
|
+
finalResult = event.result;
|
|
491
|
+
messages.push({
|
|
492
|
+
role: "assistant",
|
|
493
|
+
content: event.result.response ?? "",
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
if (event.type === "run:error") {
|
|
497
|
+
finalResult = {
|
|
498
|
+
status: "error",
|
|
499
|
+
response: event.error.message,
|
|
500
|
+
steps: 0,
|
|
501
|
+
tokens: { input: 0, output: 0, cached: 0 },
|
|
502
|
+
duration: 0,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
runId,
|
|
509
|
+
events,
|
|
510
|
+
messages,
|
|
511
|
+
result:
|
|
512
|
+
finalResult ??
|
|
513
|
+
({
|
|
514
|
+
status: "error",
|
|
515
|
+
response: "Run ended unexpectedly",
|
|
516
|
+
steps: 0,
|
|
517
|
+
tokens: { input: 0, output: 0, cached: 0 },
|
|
518
|
+
duration: 0,
|
|
519
|
+
} satisfies RunResult),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from "./agent-parser.js";
|
|
2
|
+
export * from "./config.js";
|
|
3
|
+
export * from "./default-tools.js";
|
|
4
|
+
export * from "./harness.js";
|
|
5
|
+
export * from "./latitude-capture.js";
|
|
6
|
+
export * from "./memory.js";
|
|
7
|
+
export * from "./mcp.js";
|
|
8
|
+
export * from "./model-client.js";
|
|
9
|
+
export * from "./model-factory.js";
|
|
10
|
+
export * from "./openai-client.js";
|
|
11
|
+
export * from "./skill-context.js";
|
|
12
|
+
export * from "./skill-tools.js";
|
|
13
|
+
export * from "./state.js";
|
|
14
|
+
export * from "./telemetry.js";
|
|
15
|
+
export * from "./tool-dispatcher.js";
|
|
16
|
+
export { defineTool } from "@poncho-ai/sdk";
|
|
17
|
+
export type { ToolDefinition } from "@poncho-ai/sdk";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export interface LatitudeCaptureConfig {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
projectId?: string | number;
|
|
4
|
+
path?: string;
|
|
5
|
+
defaultPath?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const sanitizePath = (value: string): string =>
|
|
9
|
+
value
|
|
10
|
+
.trim()
|
|
11
|
+
.replace(/[^a-zA-Z0-9\-_/\.]/g, "-")
|
|
12
|
+
.replace(/-+/g, "-");
|
|
13
|
+
|
|
14
|
+
export class LatitudeCapture {
|
|
15
|
+
private readonly apiKey?: string;
|
|
16
|
+
private telemetryPromise?: Promise<
|
|
17
|
+
| {
|
|
18
|
+
capture: <T>(
|
|
19
|
+
context: { projectId: number; path: string },
|
|
20
|
+
fn: () => Promise<T>,
|
|
21
|
+
) => Promise<T>;
|
|
22
|
+
}
|
|
23
|
+
| undefined
|
|
24
|
+
>;
|
|
25
|
+
private readonly projectId?: number;
|
|
26
|
+
private readonly path?: string;
|
|
27
|
+
|
|
28
|
+
constructor(config?: LatitudeCaptureConfig) {
|
|
29
|
+
this.apiKey = config?.apiKey ?? process.env.LATITUDE_API_KEY;
|
|
30
|
+
if (!this.apiKey) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const rawProjectId = config?.projectId ?? process.env.LATITUDE_PROJECT_ID;
|
|
35
|
+
const projectIdNumber =
|
|
36
|
+
typeof rawProjectId === "number"
|
|
37
|
+
? rawProjectId
|
|
38
|
+
: rawProjectId
|
|
39
|
+
? Number.parseInt(rawProjectId, 10)
|
|
40
|
+
: Number.NaN;
|
|
41
|
+
this.projectId = Number.isFinite(projectIdNumber) ? projectIdNumber : undefined;
|
|
42
|
+
const rawPath =
|
|
43
|
+
config?.path ??
|
|
44
|
+
process.env.LATITUDE_PATH ??
|
|
45
|
+
process.env.LATITUDE_DOCUMENT_PATH ??
|
|
46
|
+
config?.defaultPath;
|
|
47
|
+
this.path = rawPath ? sanitizePath(rawPath) : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async initializeTelemetry(): Promise<
|
|
51
|
+
| {
|
|
52
|
+
capture: <T>(
|
|
53
|
+
context: { projectId: number; path: string },
|
|
54
|
+
fn: () => Promise<T>,
|
|
55
|
+
) => Promise<T>;
|
|
56
|
+
}
|
|
57
|
+
| undefined
|
|
58
|
+
> {
|
|
59
|
+
if (!this.apiKey) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const [{ LatitudeTelemetry }, AnthropicSdk, { default: OpenAI }] = await Promise.all([
|
|
64
|
+
import("@latitude-data/telemetry"),
|
|
65
|
+
import("@anthropic-ai/sdk"),
|
|
66
|
+
import("openai"),
|
|
67
|
+
]);
|
|
68
|
+
const disableAnthropicInstrumentation =
|
|
69
|
+
process.env.LATITUDE_DISABLE_ANTHROPIC_INSTRUMENTATION === "true";
|
|
70
|
+
return new LatitudeTelemetry(this.apiKey, {
|
|
71
|
+
instrumentations: {
|
|
72
|
+
...(disableAnthropicInstrumentation
|
|
73
|
+
? {}
|
|
74
|
+
: { anthropic: AnthropicSdk as unknown }),
|
|
75
|
+
openai: OpenAI as unknown,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
} catch {
|
|
79
|
+
// If instrumentation setup fails, skip Latitude capture and run normally.
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async capture<T>(fn: () => Promise<T>): Promise<T> {
|
|
85
|
+
if (!this.apiKey || !this.projectId || !this.path) {
|
|
86
|
+
return await fn();
|
|
87
|
+
}
|
|
88
|
+
if (!this.telemetryPromise) {
|
|
89
|
+
this.telemetryPromise = this.initializeTelemetry();
|
|
90
|
+
}
|
|
91
|
+
const telemetry = await this.telemetryPromise;
|
|
92
|
+
if (!telemetry) {
|
|
93
|
+
return await fn();
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
return await telemetry.capture(
|
|
97
|
+
{
|
|
98
|
+
projectId: this.projectId,
|
|
99
|
+
path: this.path,
|
|
100
|
+
},
|
|
101
|
+
fn,
|
|
102
|
+
);
|
|
103
|
+
} catch {
|
|
104
|
+
// Telemetry must never break runtime model calls.
|
|
105
|
+
return await fn();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|