@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
|
+
}
|
package/dist/loop/run-agent.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
577
|
+
const dispatched = await dispatch(tu);
|
|
573
578
|
results[idx] = dispatched.result;
|
|
574
579
|
if (dispatched.detail !== undefined)
|
|
575
580
|
resultDetails[idx] = dispatched.detail;
|
package/dist/providers/ai-sdk.js
CHANGED
|
@@ -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.
|
|
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.
|
|
51
|
-
"@ai-sdk/openai": "^4.0.
|
|
52
|
-
"@ai-sdk/openai-compatible": "^3.0.
|
|
53
|
-
"@ai-sdk/
|
|
54
|
-
"@ai-sdk/provider
|
|
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.
|
|
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.
|
|
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",
|