@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/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 { LatitudeTelemetry } from "@latitude-data/telemetry";
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
- When configuring Latitude telemetry, use **exactly** these field names:
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
- latitude: {
511
- apiKeyEnv: "LATITUDE_API_KEY", // env var name (default)
512
- projectIdEnv: "LATITUDE_PROJECT_ID", // env var name (default)
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
- latitude: {
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
- if (this.latitudeTelemetry) {
1393
- // Latitude already registered a global TracerProvider (v1.x) — add our
1394
- // processor to it so every span flows to both destinations.
1395
- const globalProvider = trace.getTracerProvider();
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 telemetry capture for complete trace coverage.
1612
- * Supports Latitude, generic OTLP, or both simultaneously.
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
- const config = this.loadedConfig;
1617
- const telemetry = this.latitudeTelemetry;
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(`agent.run ${agentName}`);
1703
- rootSpan.setAttribute("poncho.agent.name", agentName);
1704
- if (input.conversationId) {
1705
- rootSpan.setAttribute("poncho.conversation.id", input.conversationId);
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 fetch early so it overlaps with refresh I/O
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 integrityPrompt = buildSystemPrompt();
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(integrityPrompt, messages, toolDefsJsonForEstimate);
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(integrityPrompt, messages, toolDefsJsonForEstimate);
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(integrityPrompt, messages, toolDefsJsonForEstimate);
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: integrityPrompt,
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 && !!(this.latitudeTelemetry || this.hasOtlpExporter),
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
- // Create telemetry tool spans so tool calls appear in traces
2783
- type ToolSpanHandle = { end: (opts: { result: { value: unknown; isError: boolean } }) => void };
2784
- const toolSpans = new Map<string, ToolSpanHandle>();
2785
- if (this.insideTelemetryCapture && this.latitudeTelemetry) {
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
- toolSpans.set(
2788
- call.id,
2789
- this.latitudeTelemetry.span.tool({
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
- "poncho.tool.name": call.name,
2801
- "poncho.tool.call_id": call.id,
2802
- "poncho.tool.arguments": JSON.stringify(call.input),
2803
- },
2804
- });
2805
- toolSpans.set(call.id, {
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?.end({ result: { value: result.error, isError: true } });
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?.end({ result: { value: result.output ?? null, isError: false } });
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
- integrityPrompt = buildSystemPrompt();
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";
@@ -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. Fired reminders are deleted immediately on fire. */
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) => r.status === "pending" || r.createdAt > cutoff,
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
- return this.readAll();
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
- return this.readAll();
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", () => {
@@ -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
  });
@@ -1,6 +0,0 @@
1
-
2
- > @poncho-ai/harness@0.11.2 lint /Users/cesar/Dev/latitude/poncho-ai/packages/harness
3
- > eslint src/
4
-
5
- sh: eslint: command not found
6
-  ELIFECYCLE  Command failed.