@inflexa-ai/harness 0.35.7 → 0.35.8

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.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * OpenTelemetry GenAI spans for the agent loop: `invoke_agent` around a run of
3
+ * `runAgent`, and `execute_tool` around each tool call that it dispatches. The AI
4
+ * SDK traces the model calls (`providers/ai-sdk.ts`). It never sees the loop or
5
+ * the tools, because the harness runs both itself.
6
+ */
7
+ import type { ToolCallPart } from "ai";
8
+ import type { AgentSession } from "../auth/types.js";
9
+ import type { ToolOutcome } from "./tool-outcome.js";
10
+ interface AgentRun {
11
+ readonly finish: {
12
+ readonly reason: string;
13
+ readonly cappedOut: boolean;
14
+ };
15
+ }
16
+ /** Run one agent loop under an `invoke_agent` span. */
17
+ export declare function traceAgentRun<T extends AgentRun>(agentName: string, session: AgentSession, run: () => Promise<T>): Promise<T>;
18
+ /** Run one tool call under an `execute_tool` span that records how the call ended. */
19
+ export declare function traceToolCall<T>(call: Pick<ToolCallPart, "toolName" | "toolCallId">, run: () => Promise<T>, outcomeOf: (value: T) => ToolOutcome): Promise<T>;
20
+ export {};
@@ -0,0 +1,53 @@
1
+ /**
2
+ * OpenTelemetry GenAI spans for the agent loop: `invoke_agent` around a run of
3
+ * `runAgent`, and `execute_tool` around each tool call that it dispatches. The AI
4
+ * SDK traces the model calls (`providers/ai-sdk.ts`). It never sees the loop or
5
+ * the tools, because the harness runs both itself.
6
+ */
7
+ import { SpanStatusCode, trace } from "@opentelemetry/api";
8
+ /** Run one agent loop under an `invoke_agent` span. */
9
+ export function traceAgentRun(agentName, session, run) {
10
+ const attributes = { "gen_ai.operation.name": "invoke_agent", "gen_ai.agent.name": agentName, ...runFrameAttributes(session) };
11
+ return withSpan(`invoke_agent ${agentName}`, attributes, async (span) => {
12
+ const result = await run();
13
+ span.setAttributes({ "gen_ai.response.finish_reasons": [result.finish.reason], "inflexa.agent.capped_out": result.finish.cappedOut });
14
+ return result;
15
+ });
16
+ }
17
+ /** Run one tool call under an `execute_tool` span that records how the call ended. */
18
+ export function traceToolCall(call, run, outcomeOf) {
19
+ const attributes = { "gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": call.toolName, "gen_ai.tool.call.id": call.toolCallId };
20
+ return withSpan(`execute_tool ${call.toolName}`, attributes, async (span) => {
21
+ const value = await run();
22
+ const outcome = outcomeOf(value);
23
+ span.setAttribute("inflexa.tool.outcome", outcome);
24
+ if (outcome === "error")
25
+ span.setStatus({ code: SpanStatusCode.ERROR });
26
+ return value;
27
+ });
28
+ }
29
+ function withSpan(name, attributes, run) {
30
+ return trace.getTracer("@inflexa-ai/harness").startActiveSpan(name, { attributes }, async (span) => {
31
+ try {
32
+ return await run(span);
33
+ }
34
+ catch (err) {
35
+ // A cancellation is the caller's decision, not a failure.
36
+ const cancelled = err instanceof Error && err.name === "AbortError";
37
+ if (!cancelled) {
38
+ span.setStatus({ code: SpanStatusCode.ERROR });
39
+ if (err instanceof Error)
40
+ span.recordException(err);
41
+ }
42
+ throw err;
43
+ }
44
+ finally {
45
+ span.end();
46
+ }
47
+ });
48
+ }
49
+ function runFrameAttributes({ runFrame }) {
50
+ if (runFrame === undefined)
51
+ return {};
52
+ return { "inflexa.run_id": runFrame.runId, ...(runFrame.stepId === undefined ? {} : { "inflexa.step_id": runFrame.stepId }) };
53
+ }
@@ -13,6 +13,7 @@ import { DEFAULT_REASONING } from "../providers/reasoning.js";
13
13
  import { resultStep } from "./run-step.js";
14
14
  import { AskRejectedError, UnavailableAsk } from "../tools/approval/contract.js";
15
15
  import { isToolError, readToolResultImages } from "../tools/define-tool.js";
16
+ import { traceAgentRun, traceToolCall } from "./genai-spans.js";
16
17
  import { addChatUsage, hasReportedUsage, recordAgentRun } from "./metrics.js";
17
18
  import { computeDetail, computeResultDetail } from "./tool-detail.js";
18
19
  import { toolOutcomeForOutputType } from "./tool-outcome.js";
@@ -22,7 +23,10 @@ export const DEFAULT_STEP_NAME_FORMATTER = {
22
23
  llm: (i) => `llm-${i}`,
23
24
  tool: (name, id) => `tool-${name}-${id}`,
24
25
  };
25
- export async function runAgent(agent, initial, session, opts) {
26
+ export function runAgent(agent, initial, session, opts) {
27
+ return traceAgentRun(agent.id, session, () => runAgentLoop(agent, initial, session, opts));
28
+ }
29
+ async function runAgentLoop(agent, initial, session, opts) {
26
30
  const { provider, signal, emit, runStep } = opts;
27
31
  const formatStepName = opts.formatStepName ?? DEFAULT_STEP_NAME_FORMATTER;
28
32
  const configuredFatalLoopError = opts.isFatalLoopError ?? (() => false);
@@ -524,6 +528,7 @@ function appendDeferredImages(messages, results, deferredImages) {
524
528
  * is genuinely fast. A bracket inside the step would report a body that did not run.
525
529
  */
526
530
  async function dispatchTools(toolUses, toolsById, toolCtx, isFatalLoopError, runStep, toolStepName, encoding) {
531
+ const dispatch = (tu) => traceToolCall(tu, () => dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError, encoding), (dispatched) => outcomeOf(dispatched.result));
527
532
  const results = new Array(toolUses.length);
528
533
  // The array starts with holes, which read as `undefined`. The element type
529
534
  // says so, thus it agrees with what `settleRound` accepts. Every index is in
@@ -550,7 +555,7 @@ async function dispatchTools(toolUses, toolsById, toolCtx, isFatalLoopError, run
550
555
  const stepName = toolStepName(tu.toolName, tu.toolCallId);
551
556
  return runStep(stepName, () => {
552
557
  stableSpan(stepName, `tool:${tu.toolName}`, { [ATTR_INFLEXA_TOOL_USE_ID]: tu.toolCallId });
553
- return dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError, encoding);
558
+ return dispatch(tu);
554
559
  }).then((settled) => {
555
560
  durations[idx] = elapsedMs(startedAt);
556
561
  const dispatched = readDispatchedStep(settled);
@@ -561,7 +566,7 @@ async function dispatchTools(toolUses, toolsById, toolCtx, isFatalLoopError, run
561
566
  }));
562
567
  for (const { tu, idx } of workflowTools) {
563
568
  const startedAt = performance.now();
564
- const dispatched = await dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError, encoding);
569
+ const dispatched = await dispatch(tu);
565
570
  results[idx] = dispatched.result;
566
571
  if (dispatched.detail !== undefined)
567
572
  resultDetails[idx] = dispatched.detail;
@@ -569,7 +574,7 @@ async function dispatchTools(toolUses, toolsById, toolCtx, isFatalLoopError, run
569
574
  }
570
575
  for (const { tu, idx } of inlineTools) {
571
576
  const startedAt = performance.now();
572
- const dispatched = await dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError, encoding);
577
+ const dispatched = await dispatch(tu);
573
578
  results[idx] = dispatched.result;
574
579
  if (dispatched.detail !== undefined)
575
580
  resultDetails[idx] = dispatched.detail;
@@ -2,6 +2,7 @@ import { streamText, wrapLanguageModel, } from "ai";
2
2
  import { createAnthropic } from "@ai-sdk/anthropic";
3
3
  import { createOpenAI } from "@ai-sdk/openai";
4
4
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
5
+ import { OpenTelemetry } from "@ai-sdk/otel";
5
6
  import { APICallError } from "@ai-sdk/provider";
6
7
  import { retryWithExponentialBackoff } from "@ai-sdk/provider-utils";
7
8
  import { ResultAsync, err, ok } from "neverthrow";
@@ -431,6 +432,11 @@ export function createAiSdkProvider(deps) {
431
432
  const maxRetries = deps.maxRetries ?? RETRY_MAX_RETRIES;
432
433
  const requestTimeoutMs = deps.requestTimeoutMs;
433
434
  const requestedModelId = requestedModelIdOf(deps.model);
435
+ // Each model call emits OpenTelemetry GenAI spans through the AI SDK, with no
436
+ // prompt or completion text on them. The integration rides on each call
437
+ // instead of the global registry, so the harness never traces the embedder's
438
+ // own AI SDK calls.
439
+ const telemetry = { integrations: new OpenTelemetry(), recordInputs: false, recordOutputs: false };
434
440
  routeSdkWarningsTo(logger);
435
441
  /**
436
442
  * Record a call that the envelope gave up on.
@@ -484,6 +490,7 @@ export function createAiSdkProvider(deps) {
484
490
  maxRetries: 0,
485
491
  headers,
486
492
  abortSignal: signal,
493
+ telemetry,
487
494
  // `firstChunkMs` bounds the wait for the first content chunk,
488
495
  // and `chunkMs` bounds each later gap. Thus each silent interval
489
496
  // carries the one configured bound, and a turn that writes
@@ -560,6 +567,7 @@ export function createAiSdkProvider(deps) {
560
567
  maxRetries: 0,
561
568
  headers,
562
569
  abortSignal: signal,
570
+ telemetry,
563
571
  // The fetch guard bounds the wait until the headers. Past the
564
572
  // headers the SDK owns two bounds: `firstChunkMs` covers the wait
565
573
  // for the first content chunk, and `chunkMs` covers each later
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.35.7",
3
+ "version": "0.35.8",
4
4
  "description": "Host-agnostic bioinformatics agent harness — hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -47,11 +47,12 @@
47
47
  "typecheck": "tsc --noEmit"
48
48
  },
49
49
  "dependencies": {
50
- "@ai-sdk/anthropic": "^4.0.38",
51
- "@ai-sdk/openai": "^4.0.39",
52
- "@ai-sdk/openai-compatible": "^3.0.30",
53
- "@ai-sdk/provider": "^4.0.7",
54
- "@ai-sdk/provider-utils": "5.0.27",
50
+ "@ai-sdk/anthropic": "^4.0.53",
51
+ "@ai-sdk/openai": "^4.0.66",
52
+ "@ai-sdk/openai-compatible": "^3.0.48",
53
+ "@ai-sdk/otel": "^1.0.99",
54
+ "@ai-sdk/provider": "^4.0.14",
55
+ "@ai-sdk/provider-utils": "5.0.40",
55
56
  "@anthropic-ai/sdk": "^0.116.0",
56
57
  "@dbos-inc/dbos-sdk": "^4.23.6",
57
58
  "@fontsource-variable/space-grotesk": "5.1.1",
@@ -68,7 +69,7 @@
68
69
  "@opentelemetry/sdk-trace-node": "^2.9.0",
69
70
  "@opentelemetry/semantic-conventions": "^1.43.0",
70
71
  "ag-grid-community": "36.1.0",
71
- "ai": "^7.0.61",
72
+ "ai": "^7.0.99",
72
73
  "cheerio": "^1.2.0",
73
74
  "dockerode": "^4.0.12",
74
75
  "echarts": "6.1.0",
@@ -94,7 +95,7 @@
94
95
  "uuid": "^11.1.1"
95
96
  },
96
97
  "devDependencies": {
97
- "@ai-sdk/amazon-bedrock": "^5.0.74",
98
+ "@ai-sdk/amazon-bedrock": "^5.0.82",
98
99
  "@eslint/js": "^10.0.1",
99
100
  "@types/bun": "latest",
100
101
  "@types/dockerode": "^3.3.47",