@agent-native/core 0.176.5 → 0.176.6-nightly-20260903200904
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/corpus/README.md +1 -1
- package/corpus/templates/content/app/global.css +1 -0
- package/corpus/templates/content/app/hooks/content-action-refresh.ts +93 -0
- package/corpus/templates/content/app/hooks/use-db-sync.ts +11 -10
- package/dist/client/ClientOnly.js +5 -2
- package/dist/client/app-providers.js +4 -1
- package/dist/client/use-db-sync.d.ts +3 -2
- package/dist/client/use-db-sync.js +10 -3
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/notifications/routes.d.ts +4 -4
- package/dist/observability/routes.d.ts +5 -5
- package/dist/observability/traces.js +140 -18
- package/dist/observability/tracing.d.ts +22 -3
- package/dist/observability/tracing.js +64 -17
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +15 -15
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/server/app-origin-config.js +2 -1
- package/dist/server/auth.js +45 -6
- package/dist/server/beta-opt-out-html.d.ts +1 -1
- package/dist/server/beta-opt-out-html.js +16 -2
- 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/dist/shared/ssr-beta-redirect.d.ts +5 -8
- package/dist/shared/ssr-beta-redirect.js +120 -17
- package/package.json +3 -3
package/corpus/README.md
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
interface ActionQuery {
|
|
2
|
+
queryKey: readonly unknown[];
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
interface ActionEvent {
|
|
6
|
+
source?: string;
|
|
7
|
+
key?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const COMMENT_MUTATIONS = new Set([
|
|
11
|
+
"add-comment",
|
|
12
|
+
"delete-comment",
|
|
13
|
+
"sync-notion-comments",
|
|
14
|
+
"update-comment",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const DOCUMENT_MUTATIONS = new Set([
|
|
18
|
+
"create-and-link-notion-page",
|
|
19
|
+
"delete-document",
|
|
20
|
+
"delete-document-property",
|
|
21
|
+
"duplicate-document-property",
|
|
22
|
+
"edit-document",
|
|
23
|
+
"execute-builder-source-batch",
|
|
24
|
+
"execute-builder-source-execution",
|
|
25
|
+
"import-content-source",
|
|
26
|
+
"migrate-content-database-rows",
|
|
27
|
+
"move-document",
|
|
28
|
+
"mutate-content-database-block",
|
|
29
|
+
"process-builder-body-hydration",
|
|
30
|
+
"pull-builder-doc",
|
|
31
|
+
"pull-document",
|
|
32
|
+
"pull-notion-page",
|
|
33
|
+
"push-builder-doc",
|
|
34
|
+
"push-notion-page",
|
|
35
|
+
"reorder-document-property",
|
|
36
|
+
"resolve-local-folder-conflict",
|
|
37
|
+
"resolve-notion-sync-conflict",
|
|
38
|
+
"restore-document",
|
|
39
|
+
"restore-document-version",
|
|
40
|
+
"set-document-discoverability",
|
|
41
|
+
"set-document-property",
|
|
42
|
+
"set-image-alt-text",
|
|
43
|
+
"sync-local-folder-source",
|
|
44
|
+
"sync-manifest-local-folder-source",
|
|
45
|
+
"transcribe-media",
|
|
46
|
+
"update-document",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const CONTENT_MUTATIONS = new Set([
|
|
50
|
+
...COMMENT_MUTATIONS,
|
|
51
|
+
...DOCUMENT_MUTATIONS,
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
function queryTargetsDocument(query: ActionQuery, documentId: string): boolean {
|
|
55
|
+
if (query.queryKey[0] !== "action") return false;
|
|
56
|
+
if (
|
|
57
|
+
query.queryKey[1] !== "get-document" &&
|
|
58
|
+
query.queryKey[1] !== "list-comments"
|
|
59
|
+
) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const args = query.queryKey[2];
|
|
63
|
+
return (
|
|
64
|
+
!!args &&
|
|
65
|
+
typeof args === "object" &&
|
|
66
|
+
(("id" in args && args.id === documentId) ||
|
|
67
|
+
("documentId" in args && args.documentId === documentId))
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function contentDocumentIdFromPathname(
|
|
72
|
+
pathname: string,
|
|
73
|
+
): string | undefined {
|
|
74
|
+
const match = /^\/page\/([^/]+)\/?$/.exec(pathname);
|
|
75
|
+
return match?.[1] ? decodeURIComponent(match[1]) : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function contentActionInvalidatePredicate(
|
|
79
|
+
pathname: string,
|
|
80
|
+
): (query: ActionQuery, events: readonly ActionEvent[]) => boolean {
|
|
81
|
+
const documentId = contentDocumentIdFromPathname(pathname);
|
|
82
|
+
return (query, events) => {
|
|
83
|
+
if (documentId === undefined || !queryTargetsDocument(query, documentId)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return events.some(
|
|
87
|
+
(event) =>
|
|
88
|
+
event.source === "action" &&
|
|
89
|
+
typeof event.key === "string" &&
|
|
90
|
+
CONTENT_MUTATIONS.has(event.key),
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getBrowserTabId,
|
|
3
|
+
useDbSync as useCoreDbSync,
|
|
4
|
+
} from "@agent-native/core/client/hooks";
|
|
2
5
|
import { useQueryClient } from "@tanstack/react-query";
|
|
3
6
|
|
|
7
|
+
import { contentActionInvalidatePredicate } from "./content-action-refresh";
|
|
8
|
+
|
|
4
9
|
export function useDbSync() {
|
|
5
10
|
const queryClient = useQueryClient();
|
|
11
|
+
const browserTabId = getBrowserTabId();
|
|
6
12
|
|
|
7
13
|
useCoreDbSync({
|
|
8
14
|
queryClient,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
suppressActionInvalidationFor: [
|
|
14
|
-
"process-builder-body-hydration",
|
|
15
|
-
"refresh-content-database-source",
|
|
16
|
-
"refresh-notion-sync-status",
|
|
17
|
-
],
|
|
15
|
+
ignoreSource: browserTabId,
|
|
16
|
+
actionInvalidatePredicate: contentActionInvalidatePredicate(
|
|
17
|
+
typeof window === "undefined" ? "" : window.location.pathname,
|
|
18
|
+
),
|
|
18
19
|
queryKeys: [
|
|
19
20
|
"action",
|
|
20
21
|
"document-sync",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useLayoutEffect, useState } from "react";
|
|
2
|
+
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
2
3
|
/**
|
|
3
4
|
* Renders children only on the client (after hydration).
|
|
4
5
|
*
|
|
@@ -9,7 +10,9 @@ import { useState, useEffect } from "react";
|
|
|
9
10
|
*/
|
|
10
11
|
export function ClientOnly({ children, fallback, }) {
|
|
11
12
|
const [mounted, setMounted] = useState(false);
|
|
12
|
-
|
|
13
|
+
// The static shell and client tree must hand off before paint or the loader
|
|
14
|
+
// flashes again while the authenticated app mounts.
|
|
15
|
+
useBrowserLayoutEffect(() => setMounted(true), []);
|
|
13
16
|
if (!mounted)
|
|
14
17
|
return fallback ?? null;
|
|
15
18
|
return children;
|
|
@@ -57,6 +57,7 @@ import { useEffect, useRef } from "react";
|
|
|
57
57
|
import { useInRouterContext } from "react-router";
|
|
58
58
|
import { isHumanReadableDocumentTitle, normalizeDocumentTitle, } from "../shared/document-title.js";
|
|
59
59
|
import { getSsrBetaRedirectScriptBody } from "../shared/ssr-beta-redirect.js";
|
|
60
|
+
import { agentNativePath } from "./api-path.js";
|
|
60
61
|
import { ClientOnly } from "./ClientOnly.js";
|
|
61
62
|
import { DefaultSpinner } from "./DefaultSpinner.js";
|
|
62
63
|
import { EnvironmentBadge } from "./EnvironmentBadge.js";
|
|
@@ -70,7 +71,9 @@ import { EMBEDDED_THEME_CHANGE_EVENT, applyEmbeddedThemeUpdate, parseEmbeddedThe
|
|
|
70
71
|
import { createAgentNativeServerActionWebMcpRegistration } from "./webmcp.js";
|
|
71
72
|
const DEFAULT_TOASTER = (_jsx(Toaster, { richColors: true, position: "bottom-left", offset: { bottom: 44, left: 32 }, mobileOffset: { bottom: 44, left: 16 } }));
|
|
72
73
|
function EarlyBetaRedirectScript() {
|
|
73
|
-
return (_jsx("script", { "data-agent-native-beta-redirect": "1", dangerouslySetInnerHTML: {
|
|
74
|
+
return (_jsx("script", { "data-agent-native-beta-redirect": "1", dangerouslySetInnerHTML: {
|
|
75
|
+
__html: getSsrBetaRedirectScriptBody(agentNativePath("/_agent-native/auth/session")),
|
|
76
|
+
} }));
|
|
74
77
|
}
|
|
75
78
|
function RoutedAppEnhancements() {
|
|
76
79
|
const isInRouter = useInRouterContext();
|
|
@@ -101,7 +101,8 @@ export declare function subscribeSyncEvents(options: SubscribeSyncEventsOptions)
|
|
|
101
101
|
* value. Use a per-tab ID so the UI ignores its own writes while still
|
|
102
102
|
* picking up changes from other tabs, agents, and scripts.
|
|
103
103
|
* @param options.actionInvalidatePredicate - Optional filter for the broad
|
|
104
|
-
* compatibility invalidate triggered by
|
|
104
|
+
* compatibility invalidate triggered by sync events. The current event batch
|
|
105
|
+
* is provided so apps can preserve action-level targeting. Use this to keep
|
|
105
106
|
* expensive active queries on explicit-refresh semantics while still letting
|
|
106
107
|
* normal source-versioned queries react through `useChangeVersion`.
|
|
107
108
|
* @param options.suppressActionInvalidationFor - Action names whose sync events
|
|
@@ -120,7 +121,7 @@ export declare function useDbSync(options?: {
|
|
|
120
121
|
fallbackInterval?: number;
|
|
121
122
|
pauseWhenHidden?: boolean;
|
|
122
123
|
ignoreSource?: string;
|
|
123
|
-
actionInvalidatePredicate?: (query: Query) => boolean;
|
|
124
|
+
actionInvalidatePredicate?: (query: Query, events: readonly SyncEvent[]) => boolean;
|
|
124
125
|
suppressActionInvalidationFor?: string[];
|
|
125
126
|
}): void;
|
|
126
127
|
/** @deprecated Use useDbSync instead */
|
|
@@ -1174,7 +1174,8 @@ export function subscribeSyncEvents(options) {
|
|
|
1174
1174
|
* value. Use a per-tab ID so the UI ignores its own writes while still
|
|
1175
1175
|
* picking up changes from other tabs, agents, and scripts.
|
|
1176
1176
|
* @param options.actionInvalidatePredicate - Optional filter for the broad
|
|
1177
|
-
* compatibility invalidate triggered by
|
|
1177
|
+
* compatibility invalidate triggered by sync events. The current event batch
|
|
1178
|
+
* is provided so apps can preserve action-level targeting. Use this to keep
|
|
1178
1179
|
* expensive active queries on explicit-refresh semantics while still letting
|
|
1179
1180
|
* normal source-versioned queries react through `useChangeVersion`.
|
|
1180
1181
|
* @param options.suppressActionInvalidationFor - Action names whose sync events
|
|
@@ -1351,7 +1352,10 @@ export function useDbSync(options = {}) {
|
|
|
1351
1352
|
// makes one agent write fan out across unrelated provider reads,
|
|
1352
1353
|
// dashboards, and background status checks. Older apps that still
|
|
1353
1354
|
// need broad compatibility can opt in with a predicate.
|
|
1354
|
-
const
|
|
1355
|
+
const appPredicate = actionInvalidatePredicateRef.current;
|
|
1356
|
+
const predicate = appPredicate
|
|
1357
|
+
? (query) => appPredicate(query, invalidating)
|
|
1358
|
+
: undefined;
|
|
1355
1359
|
invalidateWithoutCancel(predicate ? { predicate } : { queryKey: ["action"] });
|
|
1356
1360
|
}
|
|
1357
1361
|
// Framework-level invalidate: a small, fixed list of query-key
|
|
@@ -1394,7 +1398,10 @@ export function useDbSync(options = {}) {
|
|
|
1394
1398
|
// ["action"] query regardless of what the app opted out of — and
|
|
1395
1399
|
// an app cannot work around it, because both the prefix and this
|
|
1396
1400
|
// call are framework-owned.
|
|
1397
|
-
const
|
|
1401
|
+
const appPredicate = actionInvalidatePredicateRef.current;
|
|
1402
|
+
const predicate = appPredicate
|
|
1403
|
+
? (query) => appPredicate(query, invalidating)
|
|
1404
|
+
: undefined;
|
|
1398
1405
|
invalidateWithoutCancel(predicate ? { predicate } : { queryKey: ["action"] });
|
|
1399
1406
|
}
|
|
1400
1407
|
if (!hasActionEvent || hasFrameworkPrefixEvent) {
|
|
@@ -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
|
}>>;
|
|
@@ -16,18 +16,18 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
|
|
|
16
16
|
error?: undefined;
|
|
17
17
|
ok?: undefined;
|
|
18
18
|
} | {
|
|
19
|
-
count?: undefined;
|
|
20
19
|
updated: number;
|
|
21
20
|
error?: undefined;
|
|
22
21
|
ok?: undefined;
|
|
23
|
-
} | {
|
|
24
22
|
count?: undefined;
|
|
23
|
+
} | {
|
|
25
24
|
updated?: undefined;
|
|
26
25
|
error: string;
|
|
27
26
|
ok?: undefined;
|
|
28
|
-
} | {
|
|
29
27
|
count?: undefined;
|
|
28
|
+
} | {
|
|
30
29
|
updated?: undefined;
|
|
31
|
-
ok: boolean;
|
|
32
30
|
error?: undefined;
|
|
31
|
+
ok: boolean;
|
|
32
|
+
count?: undefined;
|
|
33
33
|
}>>;
|
|
@@ -41,27 +41,27 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
41
41
|
thumbsUpRate: number;
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
|
+
error?: undefined;
|
|
45
|
+
ok?: undefined;
|
|
44
46
|
summary: import("./types.js").TraceSummary;
|
|
45
47
|
spans: import("./types.js").TraceSpan[];
|
|
46
48
|
id?: undefined;
|
|
49
|
+
} | {
|
|
47
50
|
error?: undefined;
|
|
48
51
|
ok?: undefined;
|
|
49
|
-
} | {
|
|
50
52
|
summary?: undefined;
|
|
51
53
|
spans?: undefined;
|
|
52
54
|
id: string;
|
|
53
|
-
error?: undefined;
|
|
54
|
-
ok?: undefined;
|
|
55
55
|
} | {
|
|
56
|
+
ok?: undefined;
|
|
56
57
|
summary?: undefined;
|
|
57
58
|
spans?: undefined;
|
|
58
59
|
id?: undefined;
|
|
59
60
|
error: any;
|
|
60
|
-
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
|
+
error?: undefined;
|
|
62
63
|
summary?: undefined;
|
|
63
64
|
spans?: undefined;
|
|
64
65
|
id?: undefined;
|
|
65
|
-
error?: undefined;
|
|
66
66
|
ok: boolean;
|
|
67
67
|
}>>;
|
|
@@ -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 {};
|