@poncho-ai/harness 0.32.0 → 0.33.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 +12 -0
- package/dist/index.d.ts +4 -42
- package/dist/index.js +76 -246
- package/package.json +1 -2
- package/src/agent-parser.ts +7 -0
- package/src/config.ts +0 -6
- package/src/harness.ts +69 -215
- package/src/index.ts +0 -1
- package/src/reminder-store.ts +13 -4
- package/src/telemetry.ts +0 -8
- package/test/agent-parser.test.ts +74 -0
- package/test/telemetry.test.ts +0 -21
- package/.turbo/turbo-lint.log +0 -6
- package/.turbo/turbo-test.log +0 -34
- package/src/latitude-capture.ts +0 -48
package/src/harness.ts
CHANGED
|
@@ -37,8 +37,7 @@ import { createSkillTools, normalizeScriptPolicyPath } from "./skill-tools.js";
|
|
|
37
37
|
import { createSearchTools } from "./search-tools.js";
|
|
38
38
|
import { createSubagentTools } from "./subagent-tools.js";
|
|
39
39
|
import type { SubagentManager } from "./subagent-manager.js";
|
|
40
|
-
import {
|
|
41
|
-
import { trace, context as otelContext, SpanStatusCode } from "@opentelemetry/api";
|
|
40
|
+
import { trace, context as otelContext, SpanStatusCode, SpanKind } from "@opentelemetry/api";
|
|
42
41
|
import { NodeTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
|
|
43
42
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
44
43
|
import { normalizeOtlp } from "./telemetry.js";
|
|
@@ -502,25 +501,17 @@ When modifying \`AGENT.md\`, follow these rules strictly:
|
|
|
502
501
|
|
|
503
502
|
## Telemetry Configuration (\`poncho.config.js\`)
|
|
504
503
|
|
|
505
|
-
|
|
504
|
+
Send OpenTelemetry traces to any OTLP-compatible collector (Jaeger, Grafana Tempo, Honeycomb, Datadog, etc.):
|
|
506
505
|
|
|
507
506
|
\`\`\`javascript
|
|
508
507
|
telemetry: {
|
|
509
508
|
enabled: true,
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
path: "your/prompt-path", // optional, defaults to agent name
|
|
514
|
-
},
|
|
509
|
+
otlp: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
510
|
+
// Or with auth headers:
|
|
511
|
+
// otlp: { url: "https://api.honeycomb.io/v1/traces", headers: { "x-honeycomb-team": process.env.HONEYCOMB_API_KEY } },
|
|
515
512
|
},
|
|
516
513
|
\`\`\`
|
|
517
514
|
|
|
518
|
-
- \`apiKeyEnv\` specifies the environment variable name for the Latitude API key (defaults to \`"LATITUDE_API_KEY"\`).
|
|
519
|
-
- \`projectIdEnv\` specifies the environment variable name for the project ID (defaults to \`"LATITUDE_PROJECT_ID"\`).
|
|
520
|
-
- With defaults, you only need \`telemetry: { latitude: {} }\` if the env vars are already named \`LATITUDE_API_KEY\` and \`LATITUDE_PROJECT_ID\`.
|
|
521
|
-
- \`path\` must only contain letters, numbers, hyphens, underscores, dots, and slashes.
|
|
522
|
-
- For a generic OTLP endpoint instead: \`telemetry: { otlp: process.env.OTEL_EXPORTER_OTLP_ENDPOINT }\`.
|
|
523
|
-
|
|
524
515
|
## Credential Configuration Pattern
|
|
525
516
|
|
|
526
517
|
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.
|
|
@@ -545,10 +536,7 @@ export default {
|
|
|
545
536
|
tokenEnv: "UPSTASH_REDIS_REST_TOKEN", // default (falls back to KV_REST_API_TOKEN)
|
|
546
537
|
},
|
|
547
538
|
telemetry: {
|
|
548
|
-
|
|
549
|
-
apiKeyEnv: "LATITUDE_API_KEY", // default
|
|
550
|
-
projectIdEnv: "LATITUDE_PROJECT_ID", // default
|
|
551
|
-
},
|
|
539
|
+
otlp: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
552
540
|
},
|
|
553
541
|
messaging: [{ platform: "slack" }], // reads SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET by default
|
|
554
542
|
}
|
|
@@ -647,11 +635,9 @@ export class AgentHarness {
|
|
|
647
635
|
private lastSkillRefreshAt = 0;
|
|
648
636
|
private readonly activeSkillNames = new Set<string>();
|
|
649
637
|
private readonly registeredMcpToolNames = new Set<string>();
|
|
650
|
-
private latitudeTelemetry?: LatitudeTelemetry;
|
|
651
638
|
private otlpSpanProcessor?: BatchSpanProcessor;
|
|
652
639
|
private otlpTracerProvider?: NodeTracerProvider;
|
|
653
640
|
private hasOtlpExporter = false;
|
|
654
|
-
private insideTelemetryCapture = false;
|
|
655
641
|
private _browserSession?: unknown;
|
|
656
642
|
private _browserMod?: {
|
|
657
643
|
createBrowserTools: (getSession: () => unknown, getConversationId?: () => string) => ToolDefinition[];
|
|
@@ -1357,29 +1343,7 @@ export class AgentHarness {
|
|
|
1357
1343
|
await bridge.discoverTools();
|
|
1358
1344
|
await this.refreshMcpTools("initialize");
|
|
1359
1345
|
|
|
1360
|
-
// Initialize Latitude telemetry once so the OpenTelemetry global state
|
|
1361
|
-
// (context manager, tracer provider, propagator) is set up exactly once.
|
|
1362
|
-
// Creating a new LatitudeTelemetry per run would break on the second call
|
|
1363
|
-
// because @opentelemetry/api silently ignores repeated global registrations.
|
|
1364
1346
|
const telemetryEnabled = config?.telemetry?.enabled !== false;
|
|
1365
|
-
const latitudeBlock = config?.telemetry?.latitude;
|
|
1366
|
-
const latApiKeyEnv = latitudeBlock?.apiKeyEnv ?? "LATITUDE_API_KEY";
|
|
1367
|
-
const latProjectIdEnv = latitudeBlock?.projectIdEnv ?? "LATITUDE_PROJECT_ID";
|
|
1368
|
-
const latitudeApiKey = process.env[latApiKeyEnv];
|
|
1369
|
-
const rawProjectId = process.env[latProjectIdEnv];
|
|
1370
|
-
const latitudeProjectId = rawProjectId ? parseInt(rawProjectId, 10) : undefined;
|
|
1371
|
-
if (telemetryEnabled && latitudeApiKey && latitudeProjectId) {
|
|
1372
|
-
this.latitudeTelemetry = new LatitudeTelemetry(latitudeApiKey);
|
|
1373
|
-
} else if (telemetryEnabled && latitudeBlock && (!latitudeApiKey || !latitudeProjectId)) {
|
|
1374
|
-
const missing: string[] = [];
|
|
1375
|
-
if (!latitudeApiKey) missing.push(`${latApiKeyEnv} env var`);
|
|
1376
|
-
if (!latitudeProjectId) missing.push(`${latProjectIdEnv} env var`);
|
|
1377
|
-
console.warn(
|
|
1378
|
-
`[poncho][telemetry] Latitude telemetry is configured but missing: ${missing.join(", ")}. Traces will NOT be sent.`,
|
|
1379
|
-
);
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
// Generic OTLP trace exporter — works alongside or instead of Latitude.
|
|
1383
1347
|
const otlpConfig = telemetryEnabled ? normalizeOtlp(config?.telemetry?.otlp) : undefined;
|
|
1384
1348
|
if (otlpConfig) {
|
|
1385
1349
|
const exporter = new OTLPTraceExporter({
|
|
@@ -1388,26 +1352,13 @@ export class AgentHarness {
|
|
|
1388
1352
|
});
|
|
1389
1353
|
const processor = new BatchSpanProcessor(exporter);
|
|
1390
1354
|
this.otlpSpanProcessor = processor;
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
const delegate = (globalProvider as unknown as { getDelegate?: () => unknown })
|
|
1397
|
-
.getDelegate?.() ?? globalProvider;
|
|
1398
|
-
if (typeof (delegate as Record<string, unknown>).addSpanProcessor === "function") {
|
|
1399
|
-
(delegate as unknown as { addSpanProcessor(p: BatchSpanProcessor): void }).addSpanProcessor(processor);
|
|
1400
|
-
}
|
|
1401
|
-
console.info(`[poncho][telemetry] OTLP exporter added (piggybacking on Latitude provider) → ${otlpConfig.url}`);
|
|
1402
|
-
} else {
|
|
1403
|
-
const provider = new NodeTracerProvider({
|
|
1404
|
-
spanProcessors: [processor],
|
|
1405
|
-
});
|
|
1406
|
-
provider.register();
|
|
1407
|
-
this.otlpTracerProvider = provider;
|
|
1408
|
-
console.info(`[poncho][telemetry] OTLP exporter active (standalone provider) → ${otlpConfig.url}`);
|
|
1409
|
-
}
|
|
1355
|
+
const provider = new NodeTracerProvider({
|
|
1356
|
+
spanProcessors: [processor],
|
|
1357
|
+
});
|
|
1358
|
+
provider.register();
|
|
1359
|
+
this.otlpTracerProvider = provider;
|
|
1410
1360
|
this.hasOtlpExporter = true;
|
|
1361
|
+
console.info(`[poncho][telemetry] OTLP exporter active → ${otlpConfig.url}`);
|
|
1411
1362
|
}
|
|
1412
1363
|
}
|
|
1413
1364
|
|
|
@@ -1570,16 +1521,6 @@ export class AgentHarness {
|
|
|
1570
1521
|
}
|
|
1571
1522
|
|
|
1572
1523
|
await this.mcpBridge?.stopLocalServers();
|
|
1573
|
-
if (this.latitudeTelemetry) {
|
|
1574
|
-
await this.latitudeTelemetry.shutdown().catch((err) => {
|
|
1575
|
-
console.warn(
|
|
1576
|
-
`[poncho][telemetry] Latitude telemetry shutdown error: ${
|
|
1577
|
-
err instanceof Error ? err.message : String(err)
|
|
1578
|
-
}`,
|
|
1579
|
-
);
|
|
1580
|
-
});
|
|
1581
|
-
this.latitudeTelemetry = undefined;
|
|
1582
|
-
}
|
|
1583
1524
|
if (this.otlpSpanProcessor) {
|
|
1584
1525
|
await this.otlpSpanProcessor.shutdown().catch((err) => {
|
|
1585
1526
|
console.warn(
|
|
@@ -1608,107 +1549,23 @@ export class AgentHarness {
|
|
|
1608
1549
|
}
|
|
1609
1550
|
|
|
1610
1551
|
/**
|
|
1611
|
-
* Wraps the run() generator with
|
|
1612
|
-
*
|
|
1613
|
-
* Streams events in real-time using an event queue pattern.
|
|
1552
|
+
* Wraps the run() generator with an OTel root span (invoke_agent) so all
|
|
1553
|
+
* child spans (LLM calls via AI SDK, tool execution) group under one trace.
|
|
1614
1554
|
*/
|
|
1615
1555
|
async *runWithTelemetry(input: RunInput): AsyncGenerator<AgentEvent> {
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
if (telemetry) {
|
|
1620
|
-
// Latitude capture path — wraps run() inside telemetry.capture().
|
|
1621
|
-
// If OTLP is also configured, spans flow to both via the shared provider.
|
|
1622
|
-
const latProjectIdEnv2 = config?.telemetry?.latitude?.projectIdEnv ?? "LATITUDE_PROJECT_ID";
|
|
1623
|
-
const projectId = parseInt(process.env[latProjectIdEnv2] ?? "", 10) as number;
|
|
1624
|
-
const rawPath = config?.telemetry?.latitude?.path ?? this.parsedAgent?.frontmatter.name ?? 'agent';
|
|
1625
|
-
const path = rawPath.replace(/[^\w\-./]/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '') || 'agent';
|
|
1626
|
-
|
|
1627
|
-
const rawConversationId = input.conversationId ?? (
|
|
1628
|
-
typeof input.parameters?.__activeConversationId === "string"
|
|
1629
|
-
? input.parameters.__activeConversationId
|
|
1630
|
-
: undefined
|
|
1631
|
-
);
|
|
1632
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1633
|
-
const conversationUuid = rawConversationId && UUID_RE.test(rawConversationId)
|
|
1634
|
-
? rawConversationId
|
|
1635
|
-
: undefined;
|
|
1636
|
-
|
|
1637
|
-
console.info(
|
|
1638
|
-
`[poncho][telemetry] Latitude telemetry active – projectId=${projectId}, path="${path}"${conversationUuid ? `, conversation="${conversationUuid}"` : ""}`,
|
|
1639
|
-
);
|
|
1640
|
-
|
|
1641
|
-
const eventQueue: AgentEvent[] = [];
|
|
1642
|
-
let queueResolve: ((value: void) => void) | null = null;
|
|
1643
|
-
let generatorDone = false;
|
|
1644
|
-
let generatorError: Error | null = null;
|
|
1645
|
-
|
|
1646
|
-
const capturePromise = telemetry.capture({ projectId, path, conversationUuid }, async () => {
|
|
1647
|
-
this.insideTelemetryCapture = true;
|
|
1648
|
-
try {
|
|
1649
|
-
for await (const event of this.run(input)) {
|
|
1650
|
-
eventQueue.push(event);
|
|
1651
|
-
if (queueResolve) {
|
|
1652
|
-
const resolve = queueResolve;
|
|
1653
|
-
queueResolve = null;
|
|
1654
|
-
resolve();
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
} catch (error) {
|
|
1658
|
-
generatorError = error as Error;
|
|
1659
|
-
} finally {
|
|
1660
|
-
this.insideTelemetryCapture = false;
|
|
1661
|
-
generatorDone = true;
|
|
1662
|
-
if (queueResolve) {
|
|
1663
|
-
queueResolve();
|
|
1664
|
-
queueResolve = null;
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
});
|
|
1668
|
-
|
|
1669
|
-
try {
|
|
1670
|
-
while (!generatorDone || eventQueue.length > 0) {
|
|
1671
|
-
if (eventQueue.length > 0) {
|
|
1672
|
-
yield eventQueue.shift()!;
|
|
1673
|
-
} else if (!generatorDone) {
|
|
1674
|
-
await new Promise<void>((resolve) => {
|
|
1675
|
-
queueResolve = resolve;
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
if (generatorError) {
|
|
1681
|
-
throw generatorError;
|
|
1682
|
-
}
|
|
1683
|
-
} finally {
|
|
1684
|
-
try {
|
|
1685
|
-
await capturePromise;
|
|
1686
|
-
} finally {
|
|
1687
|
-
try {
|
|
1688
|
-
await telemetry.flush();
|
|
1689
|
-
console.info("[poncho][telemetry] flush completed");
|
|
1690
|
-
} catch (flushErr) {
|
|
1691
|
-
console.error("[poncho][telemetry] flush failed:", flushErr);
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
} else if (this.hasOtlpExporter) {
|
|
1696
|
-
// Standalone OTLP path — create a root span for the agent run so all
|
|
1697
|
-
// child spans (LLM calls via Vercel AI SDK, tool spans) are grouped
|
|
1698
|
-
// under a single trace.
|
|
1699
|
-
const tracer = trace.getTracer("poncho");
|
|
1556
|
+
if (this.hasOtlpExporter) {
|
|
1557
|
+
const tracer = trace.getTracer("gen_ai");
|
|
1700
1558
|
const agentName = this.parsedAgent?.frontmatter.name ?? "agent";
|
|
1701
1559
|
|
|
1702
|
-
const rootSpan = tracer.startSpan(`
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1560
|
+
const rootSpan = tracer.startSpan(`invoke_agent ${agentName}`, {
|
|
1561
|
+
kind: SpanKind.INTERNAL,
|
|
1562
|
+
attributes: {
|
|
1563
|
+
"gen_ai.operation.name": "invoke_agent",
|
|
1564
|
+
...(input.conversationId ? { "gen_ai.conversation.id": input.conversationId } : {}),
|
|
1565
|
+
},
|
|
1566
|
+
});
|
|
1707
1567
|
|
|
1708
|
-
// Bind the root span's context so every async step (including
|
|
1709
|
-
// streamText and tool calls) sees it as the parent span.
|
|
1710
1568
|
const spanContext = trace.setSpan(otelContext.active(), rootSpan);
|
|
1711
|
-
this.insideTelemetryCapture = true;
|
|
1712
1569
|
|
|
1713
1570
|
try {
|
|
1714
1571
|
const gen = this.run(input);
|
|
@@ -1726,7 +1583,6 @@ export class AgentHarness {
|
|
|
1726
1583
|
rootSpan.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
1727
1584
|
throw error;
|
|
1728
1585
|
} finally {
|
|
1729
|
-
this.insideTelemetryCapture = false;
|
|
1730
1586
|
rootSpan.end();
|
|
1731
1587
|
try {
|
|
1732
1588
|
await this.otlpSpanProcessor?.forceFlush();
|
|
@@ -1755,10 +1611,13 @@ export class AgentHarness {
|
|
|
1755
1611
|
if (!this.parsedAgent) {
|
|
1756
1612
|
await this.initialize();
|
|
1757
1613
|
}
|
|
1758
|
-
// Start memory
|
|
1614
|
+
// Start memory + todo fetches early so they overlap with refresh I/O
|
|
1759
1615
|
const memoryPromise = this.memoryStore
|
|
1760
1616
|
? this.memoryStore.getMainMemory()
|
|
1761
1617
|
: undefined;
|
|
1618
|
+
const todosPromise = this.todoStore
|
|
1619
|
+
? this.todoStore.get(input.conversationId ?? "__default__")
|
|
1620
|
+
: undefined;
|
|
1762
1621
|
await this.refreshAgentIfChanged();
|
|
1763
1622
|
await this.refreshSkillsIfChanged();
|
|
1764
1623
|
|
|
@@ -1846,6 +1705,14 @@ Each conversation gets its own browser tab sharing a single browser instance. Ca
|
|
|
1846
1705
|
${boundedMainMemory.trim()}`
|
|
1847
1706
|
: "";
|
|
1848
1707
|
|
|
1708
|
+
const openTodos = (await todosPromise)?.filter(
|
|
1709
|
+
(t) => t.status === "pending" || t.status === "in_progress",
|
|
1710
|
+
) ?? [];
|
|
1711
|
+
const todoContext =
|
|
1712
|
+
openTodos.length > 0
|
|
1713
|
+
? `\n\n## Open Tasks\n\n${openTodos.map((t) => `- [${t.status === "in_progress" ? "IN PROGRESS" : "PENDING"}] ${t.content} (id: ${t.id})`).join("\n")}`
|
|
1714
|
+
: "";
|
|
1715
|
+
|
|
1849
1716
|
const buildSystemPrompt = (): string => {
|
|
1850
1717
|
const agentPrompt = renderCurrentAgentPrompt();
|
|
1851
1718
|
const promptWithSkills = this.skillContextWindow
|
|
@@ -1854,16 +1721,9 @@ ${boundedMainMemory.trim()}`
|
|
|
1854
1721
|
const timeContext = this.reminderStore
|
|
1855
1722
|
? `\n\nCurrent UTC time: ${new Date().toISOString()}`
|
|
1856
1723
|
: "";
|
|
1857
|
-
return `${promptWithSkills}${memoryContext}${timeContext}
|
|
1858
|
-
|
|
1859
|
-
## Execution Integrity
|
|
1860
|
-
|
|
1861
|
-
- Do not claim that you executed a tool unless you actually emitted a tool call in this run.
|
|
1862
|
-
- Do not fabricate "Tool Used" or "Tool Result" logs as plain text.
|
|
1863
|
-
- Never output faux execution transcripts, markdown tool logs, or "Tool Used/Result" sections.
|
|
1864
|
-
- If no suitable tool is available, explicitly say that and ask for guidance.`;
|
|
1724
|
+
return `${promptWithSkills}${memoryContext}${todoContext}${timeContext}`;
|
|
1865
1725
|
};
|
|
1866
|
-
let
|
|
1726
|
+
let systemPrompt = buildSystemPrompt();
|
|
1867
1727
|
let lastPromptFingerprint = `${this.agentFileFingerprint}\n${this.skillFingerprint}`;
|
|
1868
1728
|
|
|
1869
1729
|
const pushEvent = (event: AgentEvent): AgentEvent => {
|
|
@@ -2047,7 +1907,7 @@ ${boundedMainMemory.trim()}`
|
|
|
2047
1907
|
inputSchema: t.inputSchema,
|
|
2048
1908
|
})),
|
|
2049
1909
|
);
|
|
2050
|
-
const requestTokenEstimate = estimateTotalTokens(
|
|
1910
|
+
const requestTokenEstimate = estimateTotalTokens(systemPrompt, messages, toolDefsJsonForEstimate);
|
|
2051
1911
|
yield pushEvent({ type: "model:request", tokens: requestTokenEstimate });
|
|
2052
1912
|
|
|
2053
1913
|
// Convert messages to ModelMessage format
|
|
@@ -2289,7 +2149,7 @@ ${boundedMainMemory.trim()}`
|
|
|
2289
2149
|
// Re-check every N steps to curb runaway context growth in longer runs.
|
|
2290
2150
|
const compactionConfig = resolveCompactionConfig(agent.frontmatter.compaction);
|
|
2291
2151
|
if (compactionConfig.enabled && (step === 1 || step % COMPACTION_CHECK_INTERVAL_STEPS === 0)) {
|
|
2292
|
-
const estimated = estimateTotalTokens(
|
|
2152
|
+
const estimated = estimateTotalTokens(systemPrompt, messages, toolDefsJsonForEstimate);
|
|
2293
2153
|
const lastReportedInput = totalInputTokens > 0 ? totalInputTokens : 0;
|
|
2294
2154
|
const effectiveTokens = Math.max(estimated, lastReportedInput);
|
|
2295
2155
|
|
|
@@ -2314,7 +2174,7 @@ ${boundedMainMemory.trim()}`
|
|
|
2314
2174
|
emittedMessages.pop();
|
|
2315
2175
|
}
|
|
2316
2176
|
}
|
|
2317
|
-
const tokensAfterCompaction = estimateTotalTokens(
|
|
2177
|
+
const tokensAfterCompaction = estimateTotalTokens(systemPrompt, messages, toolDefsJsonForEstimate);
|
|
2318
2178
|
latestContextTokens = tokensAfterCompaction;
|
|
2319
2179
|
toolOutputEstimateSinceModel = 0;
|
|
2320
2180
|
yield pushEvent({
|
|
@@ -2356,14 +2216,14 @@ ${boundedMainMemory.trim()}`
|
|
|
2356
2216
|
|
|
2357
2217
|
const result = await streamText({
|
|
2358
2218
|
model: modelInstance,
|
|
2359
|
-
system:
|
|
2219
|
+
system: systemPrompt,
|
|
2360
2220
|
messages: cachedMessages,
|
|
2361
2221
|
tools,
|
|
2362
2222
|
temperature,
|
|
2363
2223
|
abortSignal: input.abortSignal,
|
|
2364
2224
|
...(typeof maxTokens === "number" ? { maxTokens } : {}),
|
|
2365
2225
|
experimental_telemetry: {
|
|
2366
|
-
isEnabled: telemetryEnabled &&
|
|
2226
|
+
isEnabled: telemetryEnabled && this.hasOtlpExporter,
|
|
2367
2227
|
recordInputs: true,
|
|
2368
2228
|
recordOutputs: true,
|
|
2369
2229
|
},
|
|
@@ -2779,39 +2639,24 @@ ${boundedMainMemory.trim()}`
|
|
|
2779
2639
|
return;
|
|
2780
2640
|
}
|
|
2781
2641
|
|
|
2782
|
-
//
|
|
2783
|
-
type
|
|
2784
|
-
const toolSpans = new Map<string,
|
|
2785
|
-
if (this.
|
|
2642
|
+
// OTel GenAI execute_tool spans for tool call visibility in traces
|
|
2643
|
+
type OtelSpan = ReturnType<ReturnType<typeof trace.getTracer>["startSpan"]>;
|
|
2644
|
+
const toolSpans = new Map<string, OtelSpan>();
|
|
2645
|
+
if (this.hasOtlpExporter) {
|
|
2646
|
+
const tracer = trace.getTracer("gen_ai");
|
|
2786
2647
|
for (const call of approvedCalls) {
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
name: call.name,
|
|
2791
|
-
call: { id: call.id, arguments: call.input },
|
|
2792
|
-
}),
|
|
2793
|
-
);
|
|
2794
|
-
}
|
|
2795
|
-
} else if (this.insideTelemetryCapture && this.hasOtlpExporter) {
|
|
2796
|
-
const tracer = trace.getTracer("poncho");
|
|
2797
|
-
for (const call of approvedCalls) {
|
|
2798
|
-
const span = tracer.startSpan(`tool ${call.name}`, {
|
|
2648
|
+
const toolDef = this.dispatcher.get(call.name);
|
|
2649
|
+
toolSpans.set(call.id, tracer.startSpan(`execute_tool ${call.name}`, {
|
|
2650
|
+
kind: SpanKind.INTERNAL,
|
|
2799
2651
|
attributes: {
|
|
2800
|
-
"
|
|
2801
|
-
"
|
|
2802
|
-
"
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
end(opts: { result: { value: unknown; isError: boolean } }) {
|
|
2807
|
-
if (opts.result.isError) {
|
|
2808
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: String(opts.result.value) });
|
|
2809
|
-
} else {
|
|
2810
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
2811
|
-
}
|
|
2812
|
-
span.end();
|
|
2652
|
+
"gen_ai.operation.name": "execute_tool",
|
|
2653
|
+
"gen_ai.tool.name": call.name,
|
|
2654
|
+
"gen_ai.tool.call.id": call.id,
|
|
2655
|
+
"gen_ai.tool.type": "function",
|
|
2656
|
+
...(toolDef?.description ? { "gen_ai.tool.description": toolDef.description } : {}),
|
|
2657
|
+
"gen_ai.tool.call.arguments": JSON.stringify(call.input),
|
|
2813
2658
|
},
|
|
2814
|
-
});
|
|
2659
|
+
}));
|
|
2815
2660
|
}
|
|
2816
2661
|
}
|
|
2817
2662
|
|
|
@@ -2883,7 +2728,12 @@ ${boundedMainMemory.trim()}`
|
|
|
2883
2728
|
for (const result of batchResults) {
|
|
2884
2729
|
const span = toolSpans.get(result.callId);
|
|
2885
2730
|
if (result.error) {
|
|
2886
|
-
span
|
|
2731
|
+
if (span) {
|
|
2732
|
+
span.setAttribute("error.type", "Error");
|
|
2733
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: result.error });
|
|
2734
|
+
span.recordException(new Error(result.error));
|
|
2735
|
+
span.end();
|
|
2736
|
+
}
|
|
2887
2737
|
yield pushEvent({
|
|
2888
2738
|
type: "tool:error",
|
|
2889
2739
|
tool: result.tool,
|
|
@@ -2917,7 +2767,11 @@ ${boundedMainMemory.trim()}`
|
|
|
2917
2767
|
output: { type: "json", value: { error: result.error } },
|
|
2918
2768
|
});
|
|
2919
2769
|
} else {
|
|
2920
|
-
span
|
|
2770
|
+
if (span) {
|
|
2771
|
+
span.setAttribute("gen_ai.tool.call.result", JSON.stringify(result.output ?? null));
|
|
2772
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
2773
|
+
span.end();
|
|
2774
|
+
}
|
|
2921
2775
|
const serialized = JSON.stringify(result.output ?? null);
|
|
2922
2776
|
const outputTokenEstimate = Math.ceil(serialized.length / 4);
|
|
2923
2777
|
toolOutputEstimateSinceModel += outputTokenEstimate;
|
|
@@ -3041,7 +2895,7 @@ ${boundedMainMemory.trim()}`
|
|
|
3041
2895
|
agent = this.parsedAgent as ParsedAgent;
|
|
3042
2896
|
const currentFingerprint = `${this.agentFileFingerprint}\n${this.skillFingerprint}`;
|
|
3043
2897
|
if (currentFingerprint !== lastPromptFingerprint) {
|
|
3044
|
-
|
|
2898
|
+
systemPrompt = buildSystemPrompt();
|
|
3045
2899
|
lastPromptFingerprint = currentFingerprint;
|
|
3046
2900
|
}
|
|
3047
2901
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,6 @@ export * from "./compaction.js";
|
|
|
4
4
|
export * from "./config.js";
|
|
5
5
|
export * from "./default-tools.js";
|
|
6
6
|
export * from "./harness.js";
|
|
7
|
-
export * from "./latitude-capture.js";
|
|
8
7
|
export * from "./memory.js";
|
|
9
8
|
export * from "./mcp.js";
|
|
10
9
|
export * from "./model-factory.js";
|
package/src/reminder-store.ts
CHANGED
|
@@ -66,11 +66,13 @@ const parseReminderList = (raw: unknown): Reminder[] => {
|
|
|
66
66
|
return raw.filter(isValidReminder);
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
-
/** Remove cancelled reminders older than 7 days.
|
|
69
|
+
/** Remove all fired reminders and cancelled reminders older than 7 days. */
|
|
70
70
|
const pruneStale = (reminders: Reminder[]): Reminder[] => {
|
|
71
71
|
const cutoff = Date.now() - STALE_CANCELLED_MS;
|
|
72
72
|
return reminders.filter(
|
|
73
|
-
(r) =>
|
|
73
|
+
(r) =>
|
|
74
|
+
r.status === "pending" ||
|
|
75
|
+
(r.status === "cancelled" && r.createdAt > cutoff),
|
|
74
76
|
);
|
|
75
77
|
};
|
|
76
78
|
|
|
@@ -85,6 +87,7 @@ class InMemoryReminderStore implements ReminderStore {
|
|
|
85
87
|
private reminders: Reminder[] = [];
|
|
86
88
|
|
|
87
89
|
async list(): Promise<Reminder[]> {
|
|
90
|
+
this.reminders = pruneStale(this.reminders);
|
|
88
91
|
return [...this.reminders];
|
|
89
92
|
}
|
|
90
93
|
|
|
@@ -160,7 +163,10 @@ class FileReminderStore implements ReminderStore {
|
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
async list(): Promise<Reminder[]> {
|
|
163
|
-
|
|
166
|
+
const all = await this.readAll();
|
|
167
|
+
const pruned = pruneStale(all);
|
|
168
|
+
if (pruned.length !== all.length) await this.writeAll(pruned);
|
|
169
|
+
return pruned;
|
|
164
170
|
}
|
|
165
171
|
|
|
166
172
|
async create(input: {
|
|
@@ -245,7 +251,10 @@ class KVBackedReminderStore implements ReminderStore {
|
|
|
245
251
|
}
|
|
246
252
|
|
|
247
253
|
async list(): Promise<Reminder[]> {
|
|
248
|
-
|
|
254
|
+
const all = await this.readAll();
|
|
255
|
+
const pruned = pruneStale(all);
|
|
256
|
+
if (pruned.length !== all.length) await this.writeAll(pruned);
|
|
257
|
+
return pruned;
|
|
249
258
|
}
|
|
250
259
|
|
|
251
260
|
async create(input: {
|
package/src/telemetry.ts
CHANGED
|
@@ -27,12 +27,6 @@ export function normalizeOtlp(opt: OtlpOption | undefined): OtlpConfig | undefin
|
|
|
27
27
|
export interface TelemetryConfig {
|
|
28
28
|
enabled?: boolean;
|
|
29
29
|
otlp?: OtlpOption;
|
|
30
|
-
latitude?: {
|
|
31
|
-
apiKeyEnv?: string;
|
|
32
|
-
projectIdEnv?: string;
|
|
33
|
-
path?: string;
|
|
34
|
-
documentPath?: string;
|
|
35
|
-
};
|
|
36
30
|
handler?: (event: AgentEvent) => Promise<void> | void;
|
|
37
31
|
}
|
|
38
32
|
|
|
@@ -55,8 +49,6 @@ export class TelemetryEmitter {
|
|
|
55
49
|
if (otlp) {
|
|
56
50
|
await this.sendOtlp(event, otlp);
|
|
57
51
|
}
|
|
58
|
-
// Latitude telemetry is handled by LatitudeTelemetry (from
|
|
59
|
-
// @latitude-data/telemetry) via harness.runWithTelemetry().
|
|
60
52
|
// Default behavior in local dev: print concise structured logs.
|
|
61
53
|
// Skip per-token stream logs to keep console output readable.
|
|
62
54
|
if (event.type === "model:chunk") {
|
|
@@ -165,6 +165,80 @@ cron:
|
|
|
165
165
|
`);
|
|
166
166
|
expect(parsed.frontmatter.cron!["job"]!.timezone).toBe("Europe/London");
|
|
167
167
|
});
|
|
168
|
+
|
|
169
|
+
it("parses maxRuns when present", () => {
|
|
170
|
+
const parsed = parseAgentMarkdown(`---
|
|
171
|
+
name: test-agent
|
|
172
|
+
cron:
|
|
173
|
+
job:
|
|
174
|
+
schedule: "0 9 * * *"
|
|
175
|
+
task: "Do something"
|
|
176
|
+
maxRuns: 10
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
# Agent
|
|
180
|
+
`);
|
|
181
|
+
expect(parsed.frontmatter.cron!["job"]!.maxRuns).toBe(10);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("leaves maxRuns undefined when not specified", () => {
|
|
185
|
+
const parsed = parseAgentMarkdown(`---
|
|
186
|
+
name: test-agent
|
|
187
|
+
cron:
|
|
188
|
+
job:
|
|
189
|
+
schedule: "0 9 * * *"
|
|
190
|
+
task: "Do something"
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
# Agent
|
|
194
|
+
`);
|
|
195
|
+
expect(parsed.frontmatter.cron!["job"]!.maxRuns).toBeUndefined();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("floors maxRuns: 0 to 1", () => {
|
|
199
|
+
const parsed = parseAgentMarkdown(`---
|
|
200
|
+
name: test-agent
|
|
201
|
+
cron:
|
|
202
|
+
job:
|
|
203
|
+
schedule: "0 9 * * *"
|
|
204
|
+
task: "Do something"
|
|
205
|
+
maxRuns: 0
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
# Agent
|
|
209
|
+
`);
|
|
210
|
+
expect(parsed.frontmatter.cron!["job"]!.maxRuns).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("ignores negative maxRuns", () => {
|
|
214
|
+
const parsed = parseAgentMarkdown(`---
|
|
215
|
+
name: test-agent
|
|
216
|
+
cron:
|
|
217
|
+
job:
|
|
218
|
+
schedule: "0 9 * * *"
|
|
219
|
+
task: "Do something"
|
|
220
|
+
maxRuns: -5
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
# Agent
|
|
224
|
+
`);
|
|
225
|
+
expect(parsed.frontmatter.cron!["job"]!.maxRuns).toBeUndefined();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("floors non-integer maxRuns to integer", () => {
|
|
229
|
+
const parsed = parseAgentMarkdown(`---
|
|
230
|
+
name: test-agent
|
|
231
|
+
cron:
|
|
232
|
+
job:
|
|
233
|
+
schedule: "0 9 * * *"
|
|
234
|
+
task: "Do something"
|
|
235
|
+
maxRuns: 3.7
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
# Agent
|
|
239
|
+
`);
|
|
240
|
+
expect(parsed.frontmatter.cron!["job"]!.maxRuns).toBe(3);
|
|
241
|
+
});
|
|
168
242
|
});
|
|
169
243
|
|
|
170
244
|
it("parses approval-required with relative script paths", () => {
|
package/test/telemetry.test.ts
CHANGED
|
@@ -15,7 +15,6 @@ describe("telemetry emitter", () => {
|
|
|
15
15
|
|
|
16
16
|
const emitter = new TelemetryEmitter({
|
|
17
17
|
otlp: "https://otel.example.com/v1/logs",
|
|
18
|
-
latitude: { apiKeyEnv: "LATITUDE_API_KEY" },
|
|
19
18
|
});
|
|
20
19
|
|
|
21
20
|
await expect(
|
|
@@ -24,24 +23,4 @@ describe("telemetry emitter", () => {
|
|
|
24
23
|
|
|
25
24
|
global.fetch = originalFetch;
|
|
26
25
|
});
|
|
27
|
-
|
|
28
|
-
it("does not send to latitude custom endpoint (handled by LatitudeTelemetry)", async () => {
|
|
29
|
-
const originalFetch = global.fetch;
|
|
30
|
-
const fetchMock = vi.fn().mockResolvedValue({ ok: true });
|
|
31
|
-
global.fetch = fetchMock as unknown as typeof fetch;
|
|
32
|
-
|
|
33
|
-
const emitter = new TelemetryEmitter({
|
|
34
|
-
latitude: {
|
|
35
|
-
apiKeyEnv: "LATITUDE_API_KEY",
|
|
36
|
-
projectIdEnv: "LATITUDE_PROJECT_ID",
|
|
37
|
-
documentPath: "agents/support-agent/AGENT.md",
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
await emitter.emit({ type: "step:started", step: 2 });
|
|
42
|
-
|
|
43
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
44
|
-
|
|
45
|
-
global.fetch = originalFetch;
|
|
46
|
-
});
|
|
47
26
|
});
|