@agent-native/core 0.176.5 → 0.176.6-nightly-20260903194620
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/dist/automation/index.d.ts +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/observability/traces.js +140 -18
- package/dist/observability/tracing.d.ts +22 -3
- package/dist/observability/tracing.js +64 -17
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/core-routes-plugin.d.ts +6 -0
- package/dist/server/core-routes-plugin.js +9 -2
- package/dist/server/realtime-token.d.ts +1 -1
- package/package.json +1 -1
|
@@ -186,8 +186,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
|
|
|
186
186
|
duplicate?: undefined;
|
|
187
187
|
eventId?: undefined;
|
|
188
188
|
} | {
|
|
189
|
-
error?: undefined;
|
|
190
189
|
accepted: boolean;
|
|
191
190
|
duplicate: boolean;
|
|
192
191
|
eventId: string;
|
|
192
|
+
error?: undefined;
|
|
193
193
|
}>;
|
|
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
62
62
|
error: string;
|
|
63
63
|
states?: undefined;
|
|
64
64
|
} | {
|
|
65
|
-
error?: undefined;
|
|
66
65
|
states: {
|
|
67
66
|
clientId: number;
|
|
68
67
|
state: string;
|
|
69
68
|
}[];
|
|
69
|
+
error?: undefined;
|
|
70
70
|
}>>;
|
|
71
71
|
/**
|
|
72
72
|
* GET /_agent-native/collab/:docId/users
|
|
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
|
|
|
77
77
|
error: string;
|
|
78
78
|
users?: undefined;
|
|
79
79
|
} | {
|
|
80
|
-
error?: undefined;
|
|
81
80
|
users: {
|
|
82
81
|
clientId: number;
|
|
83
82
|
lastSeen: number;
|
|
84
83
|
}[];
|
|
84
|
+
error?: undefined;
|
|
85
85
|
}>>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { captureError } from "../server/capture-error.js";
|
|
2
2
|
import { getRequestContext } from "../server/request-context.js";
|
|
3
3
|
import { MAX_AI_CONTENT_BYTES, MAX_AI_SPANS_PER_RUN, boundAiContent, emitAiSpanEvent, emitAiTraceEvent, resolveAiError, toAiErrorDetail, toPostHogMessages, } from "./posthog-ai.js";
|
|
4
|
-
import { endAgentSpan, startAgentSpan } from "./tracing.js";
|
|
4
|
+
import { endAgentSpan, startAgentSpan, withAgentSpanContext, } from "./tracing.js";
|
|
5
5
|
import { trackingIdentityProperties } from "./tracking-identity.js";
|
|
6
6
|
function spanId() {
|
|
7
7
|
return `span-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -384,10 +384,9 @@ export async function instrumentAgentLoop(opts) {
|
|
|
384
384
|
// don't have to thread it down by hand.
|
|
385
385
|
const browserSessionId = opts.browserSessionId ?? getRequestContext()?.browserSessionId;
|
|
386
386
|
// Optional OpenTelemetry root span for this run. No-ops unless a host has
|
|
387
|
-
// installed `@opentelemetry/api` and registered a provider. The
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
// for the dashboards an embedding app would build).
|
|
387
|
+
// installed `@opentelemetry/api` and registered a provider. The root is
|
|
388
|
+
// installed as the active context while the loop runs so child tool/model
|
|
389
|
+
// spans have a real parent relationship in the exported trace.
|
|
391
390
|
const otelRunSpanPromise = startAgentSpan("agent.run", {
|
|
392
391
|
"agent.run_id": runId,
|
|
393
392
|
"agent.thread_id": threadId ?? undefined,
|
|
@@ -401,6 +400,7 @@ export async function instrumentAgentLoop(opts) {
|
|
|
401
400
|
? opts.experimentAssignments[0].variantId
|
|
402
401
|
: undefined,
|
|
403
402
|
});
|
|
403
|
+
let otelRunSpan = null;
|
|
404
404
|
const spans = [];
|
|
405
405
|
let toolInvocationCounter = 0;
|
|
406
406
|
// Keyed by counter to handle concurrent calls to the same tool name
|
|
@@ -433,6 +433,82 @@ export async function instrumentAgentLoop(opts) {
|
|
|
433
433
|
/** The call currently streaming, or the last one that streamed — text and
|
|
434
434
|
* usage arriving between calls belong to the call that just finished. */
|
|
435
435
|
const currentRoundTrip = () => modelRoundTrips[modelRoundTrips.length - 1];
|
|
436
|
+
let calculateCost;
|
|
437
|
+
const calculateUsageCost = (callUsage) => {
|
|
438
|
+
if (!calculateCost || !callUsage)
|
|
439
|
+
return undefined;
|
|
440
|
+
try {
|
|
441
|
+
return calculateCost(callUsage.inputTokens, callUsage.outputTokens, callUsage.model, callUsage.cacheReadTokens, callUsage.cacheWriteTokens);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// coercion-ok: cost estimation is enrichment and cannot fail tracing.
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
const pendingOtelModelSpans = new Map();
|
|
449
|
+
const openOtelModelSpans = new Set();
|
|
450
|
+
const modelSpansAwaitingFinalError = new Set();
|
|
451
|
+
const modelSpanAttributes = (index) => {
|
|
452
|
+
const trip = modelRoundTrips[index];
|
|
453
|
+
const callUsage = trip?.usage;
|
|
454
|
+
return {
|
|
455
|
+
"llm.model": callUsage?.model ?? loopOpts.model,
|
|
456
|
+
"llm.call_index": index,
|
|
457
|
+
"llm.stop_reason": trip?.stopReason,
|
|
458
|
+
"llm.input_tokens": callUsage?.inputTokens,
|
|
459
|
+
"llm.output_tokens": callUsage?.outputTokens,
|
|
460
|
+
"llm.cache_read_tokens": callUsage?.cacheReadTokens,
|
|
461
|
+
"llm.cache_write_tokens": callUsage?.cacheWriteTokens,
|
|
462
|
+
"llm.cost_cents_x100": calculateUsageCost(callUsage),
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
const startOtelModelSpan = (index) => {
|
|
466
|
+
const entry = {
|
|
467
|
+
spanPromise: Promise.resolve(null),
|
|
468
|
+
span: null,
|
|
469
|
+
endResult: undefined,
|
|
470
|
+
ended: false,
|
|
471
|
+
};
|
|
472
|
+
entry.spanPromise = startAgentSpan("llm.call", {
|
|
473
|
+
"llm.model": loopOpts.model,
|
|
474
|
+
"llm.call_index": index,
|
|
475
|
+
}, otelRunSpan);
|
|
476
|
+
pendingOtelModelSpans.set(index, entry);
|
|
477
|
+
void entry.spanPromise.then((span) => {
|
|
478
|
+
if (!span || entry.ended)
|
|
479
|
+
return;
|
|
480
|
+
if (entry.endResult) {
|
|
481
|
+
entry.ended = true;
|
|
482
|
+
endAgentSpan(span, entry.endResult);
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
entry.span = span;
|
|
486
|
+
openOtelModelSpans.add(span);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
};
|
|
490
|
+
const finishOtelModelSpan = (index, result) => {
|
|
491
|
+
const entry = pendingOtelModelSpans.get(index);
|
|
492
|
+
if (!entry || entry.ended)
|
|
493
|
+
return;
|
|
494
|
+
entry.endResult = result;
|
|
495
|
+
if (!entry.span)
|
|
496
|
+
return;
|
|
497
|
+
entry.ended = true;
|
|
498
|
+
openOtelModelSpans.delete(entry.span);
|
|
499
|
+
endAgentSpan(entry.span, result);
|
|
500
|
+
};
|
|
501
|
+
const finishAwaitingOtelModelSpans = (finalErrorMessage = null) => {
|
|
502
|
+
for (const tripIndex of modelSpansAwaitingFinalError) {
|
|
503
|
+
finishOtelModelSpan(tripIndex, {
|
|
504
|
+
status: "error",
|
|
505
|
+
errorMessage: finalErrorMessage ?? "Model stream ended before completion.",
|
|
506
|
+
attributes: modelSpanAttributes(tripIndex),
|
|
507
|
+
endTime: modelRoundTrips[tripIndex]?.end,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
modelSpansAwaitingFinalError.clear();
|
|
511
|
+
};
|
|
436
512
|
const modelStreamIntervals = [];
|
|
437
513
|
let modelStreamOpenedAt = null;
|
|
438
514
|
/** Tool span id → the round-trip that requested it. */
|
|
@@ -507,6 +583,7 @@ export async function instrumentAgentLoop(opts) {
|
|
|
507
583
|
// counted as a successful delegated generation. A later clear/done means
|
|
508
584
|
// the wrapper recovered and finished cleanly, so reset in that case.
|
|
509
585
|
if (event.type === "clear" || event.type === "done") {
|
|
586
|
+
finishAwaitingOtelModelSpans();
|
|
510
587
|
runStatus = "success";
|
|
511
588
|
errorMessage = null;
|
|
512
589
|
cutOffReason = null;
|
|
@@ -539,8 +616,13 @@ export async function instrumentAgentLoop(opts) {
|
|
|
539
616
|
// The emitter brackets these itself, so a repeated start or an
|
|
540
617
|
// unmatched end is a no-op here rather than a fabricated interval.
|
|
541
618
|
if (event.status === "start") {
|
|
619
|
+
// A reasonless closure is emitted before the agent loop decides
|
|
620
|
+
// whether to retry. If another attempt starts, the old attempt is
|
|
621
|
+
// definitely final and must not span the retry backoff.
|
|
622
|
+
finishAwaitingOtelModelSpans();
|
|
542
623
|
if (modelStreamOpenedAt === null) {
|
|
543
624
|
modelStreamOpenedAt = Date.now();
|
|
625
|
+
const tripIndex = modelRoundTrips.length;
|
|
544
626
|
modelRoundTrips.push({
|
|
545
627
|
spanId: spanId(),
|
|
546
628
|
start: modelStreamOpenedAt,
|
|
@@ -561,17 +643,25 @@ export async function instrumentAgentLoop(opts) {
|
|
|
561
643
|
: {}),
|
|
562
644
|
assistantText: [],
|
|
563
645
|
});
|
|
646
|
+
startOtelModelSpan(tripIndex);
|
|
564
647
|
}
|
|
565
648
|
}
|
|
566
649
|
else if (modelStreamOpenedAt !== null) {
|
|
567
650
|
const end = Date.now();
|
|
568
651
|
modelStreamIntervals.push({ start: modelStreamOpenedAt, end });
|
|
652
|
+
const tripIndex = modelRoundTrips.length - 1;
|
|
569
653
|
const trip = currentRoundTrip();
|
|
570
654
|
if (trip) {
|
|
571
655
|
trip.end = end;
|
|
572
656
|
if (event.reason)
|
|
573
657
|
trip.stopReason = event.reason;
|
|
574
658
|
}
|
|
659
|
+
if (event.reason === undefined || event.reason === "error") {
|
|
660
|
+
// The engine emits this from a `finally`, before the outer catch
|
|
661
|
+
// has classified a provider error. Defer ending the span so that
|
|
662
|
+
// the real error message wins over a generic stream-ended value.
|
|
663
|
+
modelSpansAwaitingFinalError.add(tripIndex);
|
|
664
|
+
}
|
|
575
665
|
modelStreamOpenedAt = null;
|
|
576
666
|
}
|
|
577
667
|
}
|
|
@@ -604,7 +694,7 @@ export async function instrumentAgentLoop(opts) {
|
|
|
604
694
|
toolCallIdToCounter.set(event.id, counter);
|
|
605
695
|
void startAgentSpan("tool.call", {
|
|
606
696
|
"tool.name": event.tool,
|
|
607
|
-
}).then((span) => {
|
|
697
|
+
}, otelRunSpan).then((span) => {
|
|
608
698
|
if (!span)
|
|
609
699
|
return;
|
|
610
700
|
// If `tool_done` already ran for this call, end the span now with the
|
|
@@ -769,7 +859,8 @@ export async function instrumentAgentLoop(opts) {
|
|
|
769
859
|
? [...loopOpts.messages]
|
|
770
860
|
: loopOpts.messages;
|
|
771
861
|
try {
|
|
772
|
-
|
|
862
|
+
otelRunSpan = await otelRunSpanPromise;
|
|
863
|
+
usage = await withAgentSpanContext(otelRunSpan, () => runAgentLoop({
|
|
773
864
|
...loopOpts,
|
|
774
865
|
runId,
|
|
775
866
|
send: instrumentedSend,
|
|
@@ -782,7 +873,7 @@ export async function instrumentAgentLoop(opts) {
|
|
|
782
873
|
trip.usage = callUsage;
|
|
783
874
|
loopOpts.onUsage?.(callUsage);
|
|
784
875
|
},
|
|
785
|
-
});
|
|
876
|
+
}));
|
|
786
877
|
}
|
|
787
878
|
catch (err) {
|
|
788
879
|
const classification = opts.classifyError?.(err) ?? null;
|
|
@@ -814,6 +905,9 @@ export async function instrumentAgentLoop(opts) {
|
|
|
814
905
|
// model was still running when the run stopped, so the interval closes at
|
|
815
906
|
// the run's end rather than being dropped.
|
|
816
907
|
const failedInsideModelCall = modelStreamOpenedAt !== null;
|
|
908
|
+
const interruptedModelRoundTrip = modelStreamOpenedAt !== null && modelRoundTrips.length > 0
|
|
909
|
+
? modelRoundTrips.length - 1
|
|
910
|
+
: null;
|
|
817
911
|
if (modelStreamOpenedAt !== null) {
|
|
818
912
|
modelStreamIntervals.push({ start: modelStreamOpenedAt, end: runEnd });
|
|
819
913
|
const trip = currentRoundTrip();
|
|
@@ -890,7 +984,6 @@ export async function instrumentAgentLoop(opts) {
|
|
|
890
984
|
let costCentsX100 = 0;
|
|
891
985
|
// Held for the per-generation costs below, which price each round-trip
|
|
892
986
|
// from its own tokens rather than splitting the run total.
|
|
893
|
-
let calculateCost;
|
|
894
987
|
try {
|
|
895
988
|
({ calculateCost } = await import("../usage/store.js"));
|
|
896
989
|
if (usage) {
|
|
@@ -937,8 +1030,8 @@ export async function instrumentAgentLoop(opts) {
|
|
|
937
1030
|
llmCallCount =
|
|
938
1031
|
usage?.llmCalls ??
|
|
939
1032
|
// Compatibility for custom loop implementations that predate the
|
|
940
|
-
// attempt counter:
|
|
941
|
-
1;
|
|
1033
|
+
// attempt counter: observed brackets still count every attempt.
|
|
1034
|
+
(modelRoundTrips.length > 0 ? modelRoundTrips.length : 1);
|
|
942
1035
|
const runUsage = usage ?? {
|
|
943
1036
|
inputTokens: 0,
|
|
944
1037
|
outputTokens: 0,
|
|
@@ -1308,13 +1401,34 @@ export async function instrumentAgentLoop(opts) {
|
|
|
1308
1401
|
createdAt: runStart,
|
|
1309
1402
|
};
|
|
1310
1403
|
writeTraceData(spans, summary, runId, config).catch(() => { });
|
|
1311
|
-
// OpenTelemetry export (no-op unless a provider is registered).
|
|
1312
|
-
//
|
|
1313
|
-
//
|
|
1314
|
-
//
|
|
1404
|
+
// OpenTelemetry export (no-op unless a provider is registered). Bracketed
|
|
1405
|
+
// model calls have already emitted live spans; engines without brackets
|
|
1406
|
+
// get one aggregate generation. End any tool/model spans still open and
|
|
1407
|
+
// then end the run span. Awaited so spans are emitted before return.
|
|
1315
1408
|
try {
|
|
1316
|
-
if (
|
|
1317
|
-
|
|
1409
|
+
if (interruptedModelRoundTrip !== null) {
|
|
1410
|
+
finishOtelModelSpan(interruptedModelRoundTrip, {
|
|
1411
|
+
status: "error",
|
|
1412
|
+
errorMessage: errorMessage ?? "Model stream interrupted before completion.",
|
|
1413
|
+
attributes: modelSpanAttributes(interruptedModelRoundTrip),
|
|
1414
|
+
endTime: runEnd,
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
for (const [tripIndex, trip] of modelRoundTrips.entries()) {
|
|
1418
|
+
if (trip.stopReason && trip.stopReason !== "error") {
|
|
1419
|
+
finishOtelModelSpan(tripIndex, {
|
|
1420
|
+
status: "success",
|
|
1421
|
+
errorMessage: null,
|
|
1422
|
+
attributes: modelSpanAttributes(tripIndex),
|
|
1423
|
+
endTime: trip.end,
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
finishAwaitingOtelModelSpans(errorMessage);
|
|
1428
|
+
await Promise.all([...pendingOtelModelSpans.values()].map((entry) => entry.spanPromise));
|
|
1429
|
+
if (usage && modelRoundTrips.length === 0) {
|
|
1430
|
+
const aggregateLlmSpan = await withAgentSpanContext(otelRunSpan, () => startAgentSpan("llm.call", {}, otelRunSpan));
|
|
1431
|
+
endAgentSpan(aggregateLlmSpan, {
|
|
1318
1432
|
status: runStatus,
|
|
1319
1433
|
errorMessage,
|
|
1320
1434
|
attributes: {
|
|
@@ -1327,6 +1441,13 @@ export async function instrumentAgentLoop(opts) {
|
|
|
1327
1441
|
},
|
|
1328
1442
|
});
|
|
1329
1443
|
}
|
|
1444
|
+
for (const modelSpan of openOtelModelSpans) {
|
|
1445
|
+
endAgentSpan(modelSpan, {
|
|
1446
|
+
status: "error",
|
|
1447
|
+
errorMessage: "Agent run ended before model_stream completed.",
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
openOtelModelSpans.clear();
|
|
1330
1451
|
for (const toolSpan of openOtelToolSpans) {
|
|
1331
1452
|
endAgentSpan(toolSpan, {
|
|
1332
1453
|
status: "error",
|
|
@@ -1334,10 +1455,11 @@ export async function instrumentAgentLoop(opts) {
|
|
|
1334
1455
|
});
|
|
1335
1456
|
}
|
|
1336
1457
|
openOtelToolSpans.clear();
|
|
1337
|
-
endAgentSpan(
|
|
1458
|
+
endAgentSpan(otelRunSpan, {
|
|
1338
1459
|
status: runStatus,
|
|
1339
1460
|
errorMessage,
|
|
1340
1461
|
attributes: {
|
|
1462
|
+
"agent.llm_calls": llmCallCount,
|
|
1341
1463
|
"agent.tool_calls": toolCallCount,
|
|
1342
1464
|
"agent.successful_tools": successfulTools,
|
|
1343
1465
|
"agent.failed_tools": failedTools,
|
|
@@ -36,7 +36,7 @@ export interface AgentSpan {
|
|
|
36
36
|
name?: string;
|
|
37
37
|
message: string;
|
|
38
38
|
}): void;
|
|
39
|
-
end(): void;
|
|
39
|
+
end(endTime?: unknown): void;
|
|
40
40
|
}
|
|
41
41
|
/** OTel `SpanStatusCode` values, inlined so we don't need the api types here. */
|
|
42
42
|
export declare const SPAN_STATUS_OK = 1;
|
|
@@ -44,14 +44,30 @@ export declare const SPAN_STATUS_ERROR = 2;
|
|
|
44
44
|
interface AgentTracer {
|
|
45
45
|
startSpan(name: string, options?: {
|
|
46
46
|
attributes?: Record<string, string | number | boolean>;
|
|
47
|
-
}): AgentSpan;
|
|
47
|
+
}, context?: unknown): AgentSpan;
|
|
48
|
+
}
|
|
49
|
+
interface AgentTraceRuntime {
|
|
50
|
+
tracer: AgentTracer;
|
|
51
|
+
context?: {
|
|
52
|
+
active(): unknown;
|
|
53
|
+
with<T>(context: unknown, callback: () => T): T;
|
|
54
|
+
};
|
|
55
|
+
trace?: {
|
|
56
|
+
setSpan(context: unknown, span: AgentSpan): unknown;
|
|
57
|
+
};
|
|
48
58
|
}
|
|
49
59
|
/**
|
|
50
60
|
* Start a span. When OTel isn't installed (or no provider is registered) this
|
|
51
61
|
* returns `null` and the caller simply skips span bookkeeping — there is no
|
|
52
62
|
* runtime cost beyond the cached null check.
|
|
53
63
|
*/
|
|
54
|
-
export declare function startAgentSpan(name: string, attributes?: Record<string, string | number | boolean | null | undefined
|
|
64
|
+
export declare function startAgentSpan(name: string, attributes?: Record<string, string | number | boolean | null | undefined>, parentSpan?: AgentSpan | null): Promise<AgentSpan | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Run a callback with the supplied span installed as the active OTel span so
|
|
67
|
+
* spans created by the callback become descendants of it. The bridge is
|
|
68
|
+
* deliberately best-effort: telemetry setup must never change agent behavior.
|
|
69
|
+
*/
|
|
70
|
+
export declare function withAgentSpanContext<T>(span: AgentSpan | null, callback: () => T): T;
|
|
55
71
|
/**
|
|
56
72
|
* Finish a span, setting OK/ERROR status and recording the error message when
|
|
57
73
|
* present. Safe to call with `null` (no-op) and never throws.
|
|
@@ -60,6 +76,7 @@ export declare function endAgentSpan(span: AgentSpan | null, result?: {
|
|
|
60
76
|
status?: "success" | "error";
|
|
61
77
|
errorMessage?: string | null;
|
|
62
78
|
attributes?: Record<string, string | number | boolean | null | undefined>;
|
|
79
|
+
endTime?: number;
|
|
63
80
|
}): void;
|
|
64
81
|
/** For tests — reset the cached tracer so a fresh provider can be detected. */
|
|
65
82
|
export declare function __resetAgentTracerCache(): void;
|
|
@@ -69,4 +86,6 @@ export declare function __resetAgentTracerCache(): void;
|
|
|
69
86
|
* to simulate "no tracer available".
|
|
70
87
|
*/
|
|
71
88
|
export declare function __setAgentTracerForTests(tracer: AgentTracer | null): void;
|
|
89
|
+
/** For tests — inject a runtime with an active-context implementation. */
|
|
90
|
+
export declare function __setAgentTraceRuntimeForTests(runtime: AgentTraceRuntime | null): void;
|
|
72
91
|
export {};
|
|
@@ -24,28 +24,36 @@ const TRACER_NAME = "@agent-native/core/agent-loop";
|
|
|
24
24
|
export const SPAN_STATUS_OK = 1;
|
|
25
25
|
export const SPAN_STATUS_ERROR = 2;
|
|
26
26
|
/**
|
|
27
|
-
* Cached
|
|
28
|
-
* "no
|
|
27
|
+
* Cached OTel runtime. `undefined` = not yet resolved; `null` = resolved to
|
|
28
|
+
* "no runtime available" (api package missing or load failed).
|
|
29
29
|
*/
|
|
30
|
-
let
|
|
30
|
+
let cachedRuntime;
|
|
31
31
|
/**
|
|
32
32
|
* Resolve the OpenTelemetry tracer if `@opentelemetry/api` is installed.
|
|
33
33
|
* Returns `null` (cached) when the package is unavailable so callers can
|
|
34
34
|
* branch to a no-op cheaply on every subsequent call.
|
|
35
35
|
*/
|
|
36
|
-
async function
|
|
37
|
-
if (
|
|
38
|
-
return
|
|
36
|
+
async function resolveRuntime() {
|
|
37
|
+
if (cachedRuntime !== undefined)
|
|
38
|
+
return cachedRuntime;
|
|
39
39
|
try {
|
|
40
40
|
// Optional dependency — guarded import. Absent ⇒ no-op everywhere.
|
|
41
41
|
const otel = await import("@opentelemetry/api");
|
|
42
42
|
const tracer = otel?.trace?.getTracer?.(TRACER_NAME);
|
|
43
|
-
|
|
43
|
+
cachedRuntime = tracer
|
|
44
|
+
? {
|
|
45
|
+
tracer: tracer,
|
|
46
|
+
...(otel?.context?.active && otel?.context?.with
|
|
47
|
+
? { context: otel.context }
|
|
48
|
+
: {}),
|
|
49
|
+
...(otel?.trace?.setSpan ? { trace: otel.trace } : {}),
|
|
50
|
+
}
|
|
51
|
+
: null;
|
|
44
52
|
}
|
|
45
53
|
catch {
|
|
46
|
-
|
|
54
|
+
cachedRuntime = null;
|
|
47
55
|
}
|
|
48
|
-
return
|
|
56
|
+
return cachedRuntime;
|
|
49
57
|
}
|
|
50
58
|
/** Drop sentinel/zero token counts so spans aren't cluttered with noise. */
|
|
51
59
|
function pruneAttributes(attributes) {
|
|
@@ -62,19 +70,54 @@ function pruneAttributes(attributes) {
|
|
|
62
70
|
* returns `null` and the caller simply skips span bookkeeping — there is no
|
|
63
71
|
* runtime cost beyond the cached null check.
|
|
64
72
|
*/
|
|
65
|
-
export async function startAgentSpan(name, attributes = {}) {
|
|
66
|
-
const
|
|
67
|
-
if (!
|
|
73
|
+
export async function startAgentSpan(name, attributes = {}, parentSpan = null) {
|
|
74
|
+
const runtime = await resolveRuntime();
|
|
75
|
+
if (!runtime)
|
|
68
76
|
return null;
|
|
69
77
|
try {
|
|
70
|
-
|
|
78
|
+
let parentContext;
|
|
79
|
+
if (parentSpan && runtime.context && runtime.trace) {
|
|
80
|
+
try {
|
|
81
|
+
parentContext = runtime.trace.setSpan(runtime.context.active(), parentSpan);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// coercion-ok: explicit OTel parent setup must never break the agent loop.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return runtime.tracer.startSpan(name, {
|
|
71
88
|
attributes: pruneAttributes(attributes),
|
|
72
|
-
});
|
|
89
|
+
}, parentContext);
|
|
73
90
|
}
|
|
74
91
|
catch {
|
|
75
92
|
return null;
|
|
76
93
|
}
|
|
77
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Run a callback with the supplied span installed as the active OTel span so
|
|
97
|
+
* spans created by the callback become descendants of it. The bridge is
|
|
98
|
+
* deliberately best-effort: telemetry setup must never change agent behavior.
|
|
99
|
+
*/
|
|
100
|
+
export function withAgentSpanContext(span, callback) {
|
|
101
|
+
if (!span)
|
|
102
|
+
return callback();
|
|
103
|
+
const runtime = cachedRuntime;
|
|
104
|
+
if (!runtime?.context || !runtime.trace)
|
|
105
|
+
return callback();
|
|
106
|
+
let callbackEntered = false;
|
|
107
|
+
try {
|
|
108
|
+
const context = runtime.trace.setSpan(runtime.context.active(), span);
|
|
109
|
+
return runtime.context.with(context, () => {
|
|
110
|
+
callbackEntered = true;
|
|
111
|
+
return callback();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (callbackEntered)
|
|
116
|
+
throw error;
|
|
117
|
+
// coercion-ok: optional OTel context setup must never break the agent loop.
|
|
118
|
+
return callback();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
78
121
|
/**
|
|
79
122
|
* Finish a span, setting OK/ERROR status and recording the error message when
|
|
80
123
|
* present. Safe to call with `null` (no-op) and never throws.
|
|
@@ -104,7 +147,7 @@ export function endAgentSpan(span, result = {}) {
|
|
|
104
147
|
}
|
|
105
148
|
finally {
|
|
106
149
|
try {
|
|
107
|
-
span.end();
|
|
150
|
+
span.end(result.endTime);
|
|
108
151
|
}
|
|
109
152
|
catch {
|
|
110
153
|
// ignore
|
|
@@ -113,7 +156,7 @@ export function endAgentSpan(span, result = {}) {
|
|
|
113
156
|
}
|
|
114
157
|
/** For tests — reset the cached tracer so a fresh provider can be detected. */
|
|
115
158
|
export function __resetAgentTracerCache() {
|
|
116
|
-
|
|
159
|
+
cachedRuntime = undefined;
|
|
117
160
|
}
|
|
118
161
|
/**
|
|
119
162
|
* For tests — inject a tracer directly (e.g. an in-memory test provider's
|
|
@@ -121,5 +164,9 @@ export function __resetAgentTracerCache() {
|
|
|
121
164
|
* to simulate "no tracer available".
|
|
122
165
|
*/
|
|
123
166
|
export function __setAgentTracerForTests(tracer) {
|
|
124
|
-
|
|
167
|
+
cachedRuntime = tracer ? { tracer } : null;
|
|
168
|
+
}
|
|
169
|
+
/** For tests — inject a runtime with an active-context implementation. */
|
|
170
|
+
export function __setAgentTraceRuntimeForTests(runtime) {
|
|
171
|
+
cachedRuntime = runtime;
|
|
125
172
|
}
|
|
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
48
48
|
}>;
|
|
49
49
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
50
50
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
51
|
-
error: string;
|
|
52
51
|
ok?: undefined;
|
|
52
|
+
error: string;
|
|
53
53
|
} | {
|
|
54
54
|
error?: undefined;
|
|
55
55
|
ok: boolean;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -37,17 +37,17 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
|
|
|
37
37
|
ok?: undefined;
|
|
38
38
|
status?: undefined;
|
|
39
39
|
} | {
|
|
40
|
-
error?: undefined;
|
|
41
40
|
ok: boolean;
|
|
42
41
|
status: string;
|
|
42
|
+
error?: undefined;
|
|
43
43
|
} | {
|
|
44
44
|
ok?: undefined;
|
|
45
45
|
error: string;
|
|
46
46
|
removed?: undefined;
|
|
47
47
|
} | {
|
|
48
|
-
error?: undefined;
|
|
49
48
|
ok: boolean;
|
|
50
49
|
removed: boolean;
|
|
50
|
+
error?: undefined;
|
|
51
51
|
}>>;
|
|
52
52
|
/**
|
|
53
53
|
* POST /_agent-native/secrets/:key/test — validate an optional candidate value
|
|
@@ -58,13 +58,13 @@ export declare function createTestSecretHandler(): import("h3").EventHandlerWith
|
|
|
58
58
|
error: string;
|
|
59
59
|
note?: undefined;
|
|
60
60
|
} | {
|
|
61
|
-
error?: undefined;
|
|
62
61
|
ok: boolean;
|
|
63
62
|
note?: undefined;
|
|
64
|
-
} | {
|
|
65
63
|
error?: undefined;
|
|
64
|
+
} | {
|
|
66
65
|
ok: boolean;
|
|
67
66
|
note: string;
|
|
67
|
+
error?: undefined;
|
|
68
68
|
} | {
|
|
69
69
|
note?: undefined;
|
|
70
70
|
ok: boolean;
|
|
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
|
|
|
95
95
|
export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
|
|
96
96
|
error: string;
|
|
97
97
|
} | {
|
|
98
|
-
error?: undefined;
|
|
99
98
|
ok: boolean;
|
|
100
99
|
key: string;
|
|
101
|
-
} | {
|
|
102
100
|
error?: undefined;
|
|
101
|
+
} | {
|
|
103
102
|
ok: boolean;
|
|
104
103
|
removed: boolean;
|
|
104
|
+
error?: undefined;
|
|
105
105
|
}>>;
|
|
@@ -113,6 +113,12 @@ export interface DbHealthProbeResult {
|
|
|
113
113
|
* matching".
|
|
114
114
|
*/
|
|
115
115
|
identityMismatch?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* What this runtime believes its own app is (`app.slug ?? app.id`), or
|
|
118
|
+
* `null` when the bundle cannot derive one. A null here is why a
|
|
119
|
+
* mismatch cannot be claimed, and is itself a finding worth reading.
|
|
120
|
+
*/
|
|
121
|
+
runningApp?: string | null;
|
|
116
122
|
};
|
|
117
123
|
/**
|
|
118
124
|
* Hosted-realtime wiring, so a deploy can be verified without signing in.
|
|
@@ -402,6 +402,7 @@ export async function runDbHealthProbe(exec = getDbExec, options = {}) {
|
|
|
402
402
|
// read must never be reported as "nothing recorded".
|
|
403
403
|
let identity;
|
|
404
404
|
let identityMismatch;
|
|
405
|
+
let runningApp;
|
|
405
406
|
if (db) {
|
|
406
407
|
identity = await withHealthDeadline(readDatabaseIdentity(dbExec).catch((err) => ({
|
|
407
408
|
state: "unreadable",
|
|
@@ -409,9 +410,15 @@ export async function runDbHealthProbe(exec = getDbExec, options = {}) {
|
|
|
409
410
|
})), { state: "timeout" });
|
|
410
411
|
// Only "recorded" can ever prove a mismatch — the other three states mean
|
|
411
412
|
// the check couldn't confirm one, not that it confirmed there wasn't.
|
|
413
|
+
// And only a KNOWN running identity can disagree with the recorded one:
|
|
414
|
+
// a hosted bundle that cannot derive its own slug/id must report the gap
|
|
415
|
+
// (`runningApp: null`), not a mismatch that blocks every production
|
|
416
|
+
// cutover — which is exactly what the first crm promotion did.
|
|
417
|
+
runningApp = resolveRunningAppIdentity();
|
|
412
418
|
identityMismatch =
|
|
413
419
|
identity.state === "recorded" &&
|
|
414
|
-
|
|
420
|
+
runningApp !== null &&
|
|
421
|
+
identity.app !== runningApp;
|
|
415
422
|
}
|
|
416
423
|
const database = getDatabaseRuntimeFingerprint();
|
|
417
424
|
// Measured on the connection `SELECT 1` just warmed, so the number reflects
|
|
@@ -449,7 +456,7 @@ export async function runDbHealthProbe(exec = getDbExec, options = {}) {
|
|
|
449
456
|
appName: database.appName,
|
|
450
457
|
authTokenConfigured: database.authTokenConfigured,
|
|
451
458
|
netlifyDatabaseUrlConfigured: database.netlifyDatabaseUrlConfigured,
|
|
452
|
-
...(identity ? { identity, identityMismatch } : {}),
|
|
459
|
+
...(identity ? { identity, identityMismatch, runningApp } : {}),
|
|
453
460
|
},
|
|
454
461
|
...(schema ? { schema } : {}),
|
|
455
462
|
...(pressure ? { pressure } : {}),
|
|
@@ -59,8 +59,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
|
|
|
59
59
|
expiresAt?: undefined;
|
|
60
60
|
ttlSeconds?: undefined;
|
|
61
61
|
} | {
|
|
62
|
-
error?: undefined;
|
|
63
62
|
token: string;
|
|
64
63
|
expiresAt: string;
|
|
65
64
|
ttlSeconds: number;
|
|
65
|
+
error?: undefined;
|
|
66
66
|
}>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.176.
|
|
3
|
+
"version": "0.176.6-nightly-20260903194620",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|