@oh-my-pi/pi-agent-core 15.1.2 → 15.1.3
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/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/dist/types/compaction/branch-summarization.d.ts +6 -0
- package/dist/types/compaction/compaction.d.ts +13 -0
- package/dist/types/run-collector.d.ts +0 -10
- package/dist/types/telemetry.d.ts +38 -1
- package/package.json +4 -4
- package/src/agent-loop.ts +1 -1
- package/src/compaction/branch-summarization.ts +8 -2
- package/src/compaction/compaction.ts +23 -5
- package/src/run-collector.ts +15 -12
- package/src/telemetry.ts +113 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.1.3] - 2026-05-17
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added optional `telemetry` support to `generateSummary`, `generateHandoff`, `generateBranchSummary`, and `compact` options so compaction, handoff, and branch summary one-shot LLM calls can emit OpenTelemetry chat telemetry when enabled
|
|
9
|
+
- Added shared oneshot telemetry instrumentation for compaction, handoff, and branch summary calls, tagging spans with `pi.gen_ai.oneshot.kind` values such as `compaction_summary`, `compaction_short_summary`, `compaction_turn_prefix`, `handoff`, and `branch_summary`
|
|
10
|
+
|
|
5
11
|
## [15.1.2] - 2026-05-15
|
|
6
12
|
### Added
|
|
7
13
|
|
package/README.md
CHANGED
|
@@ -279,7 +279,7 @@ const agent = new Agent({
|
|
|
279
279
|
|
|
280
280
|
## Tools
|
|
281
281
|
|
|
282
|
-
Define tools using `AgentTool` with a Zod parameter schema (via `z` from `@oh-my-pi/pi-ai`).
|
|
282
|
+
Define tools using `AgentTool` with a Zod parameter schema (via `z` from `@oh-my-pi/pi-ai`).
|
|
283
283
|
|
|
284
284
|
```typescript
|
|
285
285
|
import { z } from "@oh-my-pi/pi-ai";
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* a summary of the branch being left so context isn't lost.
|
|
6
6
|
*/
|
|
7
7
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import { type AgentTelemetry } from "../telemetry";
|
|
8
9
|
import type { AgentMessage } from "../types";
|
|
9
10
|
import type { ReadonlySessionManager, SessionEntry } from "./entries";
|
|
10
11
|
import { type ConvertToLlm } from "./messages";
|
|
@@ -51,6 +52,11 @@ export interface GenerateBranchSummaryOptions {
|
|
|
51
52
|
metadata?: Record<string, unknown>;
|
|
52
53
|
/** Convert app-specific messages before serializing the branch summary prompt. */
|
|
53
54
|
convertToLlm?: ConvertToLlm;
|
|
55
|
+
/**
|
|
56
|
+
* Optional telemetry handle. When provided, the branch summary LLM call is
|
|
57
|
+
* wrapped in an OTEL chat span tagged with `pi.gen_ai.oneshot.kind = "branch_summary"`.
|
|
58
|
+
*/
|
|
59
|
+
telemetry?: AgentTelemetry;
|
|
54
60
|
}
|
|
55
61
|
/**
|
|
56
62
|
* Collect entries that should be summarized when navigating from one position to another.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* and after compaction the session is reloaded.
|
|
6
6
|
*/
|
|
7
7
|
import { type MessageAttribution, type Model, type Usage } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import { type AgentTelemetry } from "../telemetry";
|
|
8
9
|
import type { AgentMessage, AgentTool } from "../types";
|
|
9
10
|
import type { SessionEntry } from "./entries";
|
|
10
11
|
import { type ConvertToLlm } from "./messages";
|
|
@@ -107,6 +108,13 @@ export interface SummaryOptions {
|
|
|
107
108
|
initiatorOverride?: MessageAttribution;
|
|
108
109
|
metadata?: Record<string, unknown>;
|
|
109
110
|
convertToLlm?: ConvertToLlm;
|
|
111
|
+
/**
|
|
112
|
+
* Optional telemetry handle. When provided, every LLM call emitted during
|
|
113
|
+
* compaction is wrapped in an OTEL chat span tagged with
|
|
114
|
+
* `pi.gen_ai.oneshot.kind` (`compaction_summary`, `compaction_short_summary`,
|
|
115
|
+
* or `compaction_turn_prefix`). `undefined` keeps the call paths zero-cost.
|
|
116
|
+
*/
|
|
117
|
+
telemetry?: AgentTelemetry;
|
|
110
118
|
}
|
|
111
119
|
export declare function generateSummary(currentMessages: AgentMessage[], model: Model, reserveTokens: number, apiKey: string, signal?: AbortSignal, customInstructions?: string, previousSummary?: string, options?: SummaryOptions): Promise<string>;
|
|
112
120
|
export interface HandoffOptions {
|
|
@@ -118,6 +126,11 @@ export interface HandoffOptions {
|
|
|
118
126
|
convertToLlm?: ConvertToLlm;
|
|
119
127
|
initiatorOverride?: MessageAttribution;
|
|
120
128
|
metadata?: Record<string, unknown>;
|
|
129
|
+
/**
|
|
130
|
+
* Optional telemetry handle. When provided, the handoff LLM call is
|
|
131
|
+
* wrapped in an OTEL chat span tagged with `pi.gen_ai.oneshot.kind = "handoff"`.
|
|
132
|
+
*/
|
|
133
|
+
telemetry?: AgentTelemetry;
|
|
121
134
|
}
|
|
122
135
|
export declare function renderHandoffPrompt(customInstructions?: string): string;
|
|
123
136
|
export declare function generateHandoff(messages: AgentMessage[], model: Model, apiKey: string, options: HandoffOptions, signal?: AbortSignal): Promise<string>;
|
|
@@ -108,16 +108,6 @@ export interface AgentRunCoverage {
|
|
|
108
108
|
readonly modelsUsed: readonly string[];
|
|
109
109
|
readonly providersUsed: readonly string[];
|
|
110
110
|
}
|
|
111
|
-
/**
|
|
112
|
-
* Per-invocation event buffer. Constructed unconditionally inside
|
|
113
|
-
* {@link resolveTelemetry}; cost is one allocation per `agentLoop` call.
|
|
114
|
-
*
|
|
115
|
-
* Methods are intentionally non-throwing — telemetry must never turn a
|
|
116
|
-
* successful agent run into a failed one. WeakMap keys keep span-state
|
|
117
|
-
* lookups bounded; if a finish path is somehow reached without a matching
|
|
118
|
-
* begin (provider crash, tracer swap mid-run), the corresponding record is
|
|
119
|
-
* still emitted with `latencyMs: 0` rather than throwing.
|
|
120
|
-
*/
|
|
121
111
|
export declare class AgentRunCollector {
|
|
122
112
|
#private;
|
|
123
113
|
/** True once `markRunEnded()` has been called for this invocation. */
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* registered, `@opentelemetry/api` returns a no-op tracer and all calls are
|
|
23
23
|
* cheap pass-throughs.
|
|
24
24
|
*/
|
|
25
|
-
import { type AssistantMessage, type Message, type Model, type ServiceTier, type StopReason, type ToolChoice, type Usage } from "@oh-my-pi/pi-ai";
|
|
25
|
+
import { type Api, type AssistantMessage, type Context, type Message, type Model, type ServiceTier, type SimpleStreamOptions, type StopReason, type ToolChoice, type Usage } from "@oh-my-pi/pi-ai";
|
|
26
26
|
import { type Attributes, type AttributeValue, type Span, SpanKind, SpanStatusCode, type Tracer, trace } from "@opentelemetry/api";
|
|
27
27
|
import { AgentRunCollector, type AgentRunCoverage, type AgentRunSummary, type ToolStatus } from "./run-collector";
|
|
28
28
|
import type { AgentTool } from "./types";
|
|
@@ -99,6 +99,7 @@ export declare const enum PiGenAIAttr {
|
|
|
99
99
|
HandoffFromAgentId = "pi.gen_ai.handoff.from_agent.id",
|
|
100
100
|
HandoffToAgentName = "pi.gen_ai.handoff.to_agent.name",
|
|
101
101
|
HandoffToAgentId = "pi.gen_ai.handoff.to_agent.id",
|
|
102
|
+
OneshotKind = "pi.gen_ai.oneshot.kind",
|
|
102
103
|
GatewayName = "pi.gen_ai.gateway.name",
|
|
103
104
|
GatewayEndpoint = "pi.gen_ai.gateway.endpoint",
|
|
104
105
|
GatewayCallId = "pi.gen_ai.gateway.call_id",
|
|
@@ -433,6 +434,42 @@ export interface ManualChatTelemetryOptions {
|
|
|
433
434
|
readonly endSpan?: boolean;
|
|
434
435
|
}
|
|
435
436
|
export declare function recordManualChatTelemetry(telemetry: AgentTelemetry | undefined, options: ManualChatTelemetryOptions): Promise<Span | undefined>;
|
|
437
|
+
/**
|
|
438
|
+
* Options accepted by {@link instrumentedCompleteSimple}. Mirrors the
|
|
439
|
+
* `streamAssistantResponse` chat-span lifecycle for oneshot LLM calls
|
|
440
|
+
* (compaction summaries, handoff document, branch summary, inspect_image).
|
|
441
|
+
*/
|
|
442
|
+
export interface InstrumentedChatSpanOptions {
|
|
443
|
+
readonly telemetry: AgentTelemetry | undefined;
|
|
444
|
+
/** Optional explicit parent span. Defaults to `context.active()`. */
|
|
445
|
+
readonly parent?: Span;
|
|
446
|
+
/** Step index recorded on the span; defaults to `-1` for non-loop calls. */
|
|
447
|
+
readonly stepNumber?: number;
|
|
448
|
+
/**
|
|
449
|
+
* Tag stamped onto `pi.gen_ai.oneshot.kind`. Values used by the agent:
|
|
450
|
+
* `compaction_summary`, `compaction_short_summary`, `compaction_turn_prefix`,
|
|
451
|
+
* `handoff`, `branch_summary`, `inspect_image`. Free-form to allow callers
|
|
452
|
+
* outside this package to add new kinds without bumping the helper.
|
|
453
|
+
*/
|
|
454
|
+
readonly oneshotKind?: string;
|
|
455
|
+
/** Extra span attributes applied verbatim. */
|
|
456
|
+
readonly attributes?: Attributes;
|
|
457
|
+
/**
|
|
458
|
+
* Override for the underlying {@link completeSimple} call. Defaults to
|
|
459
|
+
* `completeSimple` from `@oh-my-pi/pi-ai`. Use to retain a test injection
|
|
460
|
+
* seam while still going through the chat-span lifecycle.
|
|
461
|
+
*/
|
|
462
|
+
readonly completeImpl?: <TApi extends Api>(model: Model<TApi>, ctx: Context, options: SimpleStreamOptions) => Promise<AssistantMessage>;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Wrap a {@link completeSimple} round-trip with the same chat-span lifecycle
|
|
466
|
+
* the agent loop uses for streamed turns: `startChatSpan` → run inside the
|
|
467
|
+
* active span → `finishChatSpan` on success, `failChatSpan` on throw.
|
|
468
|
+
*
|
|
469
|
+
* Short-circuits when `telemetry` is `undefined` so cost / overhead stays at
|
|
470
|
+
* zero for installations without an OTEL SDK.
|
|
471
|
+
*/
|
|
472
|
+
export declare function instrumentedCompleteSimple<TApi extends Api>(model: Model<TApi>, ctx: Context, options: SimpleStreamOptions, span: InstrumentedChatSpanOptions): Promise<AssistantMessage>;
|
|
436
473
|
/**
|
|
437
474
|
* Start an `execute_tool` span representing one tool invocation. Parented
|
|
438
475
|
* under the supplied `invoke_agent` span by default — pass `parent` to
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-agent-core",
|
|
4
|
-
"version": "15.1.
|
|
4
|
+
"version": "15.1.3",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "15.1.
|
|
39
|
-
"@oh-my-pi/pi-natives": "15.1.
|
|
40
|
-
"@oh-my-pi/pi-utils": "15.1.
|
|
38
|
+
"@oh-my-pi/pi-ai": "15.1.3",
|
|
39
|
+
"@oh-my-pi/pi-natives": "15.1.3",
|
|
40
|
+
"@oh-my-pi/pi-utils": "15.1.3",
|
|
41
41
|
"@opentelemetry/api": "^1.9.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
9
|
-
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
10
9
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
10
|
+
import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
|
|
11
11
|
import type { AgentMessage } from "../types";
|
|
12
12
|
import { estimateTokens } from "./compaction";
|
|
13
13
|
import type { ReadonlySessionManager, SessionEntry } from "./entries";
|
|
@@ -81,6 +81,11 @@ export interface GenerateBranchSummaryOptions {
|
|
|
81
81
|
metadata?: Record<string, unknown>;
|
|
82
82
|
/** Convert app-specific messages before serializing the branch summary prompt. */
|
|
83
83
|
convertToLlm?: ConvertToLlm;
|
|
84
|
+
/**
|
|
85
|
+
* Optional telemetry handle. When provided, the branch summary LLM call is
|
|
86
|
+
* wrapped in an OTEL chat span tagged with `pi.gen_ai.oneshot.kind = "branch_summary"`.
|
|
87
|
+
*/
|
|
88
|
+
telemetry?: AgentTelemetry;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
// ============================================================================
|
|
@@ -299,10 +304,11 @@ export async function generateBranchSummary(
|
|
|
299
304
|
];
|
|
300
305
|
|
|
301
306
|
// Call LLM for summarization
|
|
302
|
-
const response = await
|
|
307
|
+
const response = await instrumentedCompleteSimple(
|
|
303
308
|
model,
|
|
304
309
|
{ systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
|
|
305
310
|
{ apiKey, signal, maxTokens: 2048, metadata },
|
|
311
|
+
{ telemetry: options.telemetry, oneshotKind: "branch_summary" },
|
|
306
312
|
);
|
|
307
313
|
|
|
308
314
|
// Check if aborted or errored
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
type AssistantMessage,
|
|
10
|
-
completeSimple,
|
|
11
10
|
Effort,
|
|
12
11
|
type Message,
|
|
13
12
|
type MessageAttribution,
|
|
@@ -16,6 +15,7 @@ import {
|
|
|
16
15
|
} from "@oh-my-pi/pi-ai";
|
|
17
16
|
import { countTokens } from "@oh-my-pi/pi-natives";
|
|
18
17
|
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
18
|
+
import { type AgentTelemetry, instrumentedCompleteSimple } from "../telemetry";
|
|
19
19
|
import type { AgentMessage, AgentTool } from "../types";
|
|
20
20
|
import type { CompactionEntry, SessionEntry } from "./entries";
|
|
21
21
|
import { type ConvertToLlm, convertToLlm, createBranchSummaryMessage, createCustomMessage } from "./messages";
|
|
@@ -514,6 +514,13 @@ export interface SummaryOptions {
|
|
|
514
514
|
initiatorOverride?: MessageAttribution;
|
|
515
515
|
metadata?: Record<string, unknown>;
|
|
516
516
|
convertToLlm?: ConvertToLlm;
|
|
517
|
+
/**
|
|
518
|
+
* Optional telemetry handle. When provided, every LLM call emitted during
|
|
519
|
+
* compaction is wrapped in an OTEL chat span tagged with
|
|
520
|
+
* `pi.gen_ai.oneshot.kind` (`compaction_summary`, `compaction_short_summary`,
|
|
521
|
+
* or `compaction_turn_prefix`). `undefined` keeps the call paths zero-cost.
|
|
522
|
+
*/
|
|
523
|
+
telemetry?: AgentTelemetry;
|
|
517
524
|
}
|
|
518
525
|
|
|
519
526
|
export async function generateSummary(
|
|
@@ -570,7 +577,7 @@ export async function generateSummary(
|
|
|
570
577
|
return remote.summary;
|
|
571
578
|
}
|
|
572
579
|
|
|
573
|
-
const response = await
|
|
580
|
+
const response = await instrumentedCompleteSimple(
|
|
574
581
|
model,
|
|
575
582
|
{ systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
|
|
576
583
|
{
|
|
@@ -581,6 +588,7 @@ export async function generateSummary(
|
|
|
581
588
|
initiatorOverride: options?.initiatorOverride,
|
|
582
589
|
metadata: options?.metadata,
|
|
583
590
|
},
|
|
591
|
+
{ telemetry: options?.telemetry, oneshotKind: "compaction_summary" },
|
|
584
592
|
);
|
|
585
593
|
|
|
586
594
|
if (response.stopReason === "error") {
|
|
@@ -608,6 +616,11 @@ export interface HandoffOptions {
|
|
|
608
616
|
convertToLlm?: ConvertToLlm;
|
|
609
617
|
initiatorOverride?: MessageAttribution;
|
|
610
618
|
metadata?: Record<string, unknown>;
|
|
619
|
+
/**
|
|
620
|
+
* Optional telemetry handle. When provided, the handoff LLM call is
|
|
621
|
+
* wrapped in an OTEL chat span tagged with `pi.gen_ai.oneshot.kind = "handoff"`.
|
|
622
|
+
*/
|
|
623
|
+
telemetry?: AgentTelemetry;
|
|
611
624
|
}
|
|
612
625
|
|
|
613
626
|
export function renderHandoffPrompt(customInstructions?: string): string {
|
|
@@ -635,7 +648,7 @@ export async function generateHandoff(
|
|
|
635
648
|
},
|
|
636
649
|
];
|
|
637
650
|
|
|
638
|
-
const response = await
|
|
651
|
+
const response = await instrumentedCompleteSimple(
|
|
639
652
|
model,
|
|
640
653
|
{
|
|
641
654
|
systemPrompt: options.systemPrompt,
|
|
@@ -650,6 +663,7 @@ export async function generateHandoff(
|
|
|
650
663
|
initiatorOverride: options.initiatorOverride,
|
|
651
664
|
metadata: options.metadata,
|
|
652
665
|
},
|
|
666
|
+
{ telemetry: options.telemetry, oneshotKind: "handoff" },
|
|
653
667
|
);
|
|
654
668
|
|
|
655
669
|
if (response.stopReason === "error") {
|
|
@@ -694,7 +708,7 @@ async function generateShortSummary(
|
|
|
694
708
|
return remote.summary;
|
|
695
709
|
}
|
|
696
710
|
|
|
697
|
-
const response = await
|
|
711
|
+
const response = await instrumentedCompleteSimple(
|
|
698
712
|
model,
|
|
699
713
|
{
|
|
700
714
|
systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT],
|
|
@@ -708,6 +722,7 @@ async function generateShortSummary(
|
|
|
708
722
|
initiatorOverride: options?.initiatorOverride,
|
|
709
723
|
metadata: options?.metadata,
|
|
710
724
|
},
|
|
725
|
+
{ telemetry: options?.telemetry, oneshotKind: "compaction_short_summary" },
|
|
711
726
|
);
|
|
712
727
|
|
|
713
728
|
if (response.stopReason === "error") {
|
|
@@ -889,6 +904,7 @@ export async function compact(
|
|
|
889
904
|
initiatorOverride: options?.initiatorOverride,
|
|
890
905
|
metadata: options?.metadata,
|
|
891
906
|
convertToLlm: options?.convertToLlm,
|
|
907
|
+
telemetry: options?.telemetry,
|
|
892
908
|
};
|
|
893
909
|
|
|
894
910
|
let preserveData = withOpenAiRemoteCompactionPreserveData(previousPreserveData, undefined);
|
|
@@ -978,6 +994,7 @@ export async function compact(
|
|
|
978
994
|
remoteEndpoint: summaryOptions.remoteEndpoint,
|
|
979
995
|
initiatorOverride: summaryOptions.initiatorOverride,
|
|
980
996
|
metadata: summaryOptions.metadata,
|
|
997
|
+
telemetry: summaryOptions.telemetry,
|
|
981
998
|
},
|
|
982
999
|
);
|
|
983
1000
|
|
|
@@ -1023,7 +1040,7 @@ async function generateTurnPrefixSummary(
|
|
|
1023
1040
|
},
|
|
1024
1041
|
];
|
|
1025
1042
|
|
|
1026
|
-
const response = await
|
|
1043
|
+
const response = await instrumentedCompleteSimple(
|
|
1027
1044
|
model,
|
|
1028
1045
|
{ systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
|
|
1029
1046
|
{
|
|
@@ -1034,6 +1051,7 @@ async function generateTurnPrefixSummary(
|
|
|
1034
1051
|
initiatorOverride: options?.initiatorOverride,
|
|
1035
1052
|
metadata: options?.metadata,
|
|
1036
1053
|
},
|
|
1054
|
+
{ telemetry: options?.telemetry, oneshotKind: "compaction_turn_prefix" },
|
|
1037
1055
|
);
|
|
1038
1056
|
|
|
1039
1057
|
if (response.stopReason === "error") {
|
package/src/run-collector.ts
CHANGED
|
@@ -139,9 +139,12 @@ interface ToolStart {
|
|
|
139
139
|
* begin (provider crash, tracer swap mid-run), the corresponding record is
|
|
140
140
|
* still emitted with `latencyMs: 0` rather than throwing.
|
|
141
141
|
*/
|
|
142
|
+
const kChatStart = Symbol("agent.run-collector.chatStart");
|
|
143
|
+
const kToolStart = Symbol("agent.run-collector.toolStart");
|
|
144
|
+
type SpanWithChatStart = Span & { [kChatStart]?: ChatStart };
|
|
145
|
+
type SpanWithToolStart = Span & { [kToolStart]?: ToolStart };
|
|
146
|
+
|
|
142
147
|
export class AgentRunCollector {
|
|
143
|
-
readonly #chatStarts = new WeakMap<Span, ChatStart>();
|
|
144
|
-
readonly #toolStarts = new WeakMap<Span, ToolStart>();
|
|
145
148
|
readonly #chats: ChatRecord[] = [];
|
|
146
149
|
readonly #tools: ToolRecord[] = [];
|
|
147
150
|
readonly #availableTools = new Set<string>();
|
|
@@ -179,12 +182,12 @@ export class AgentRunCollector {
|
|
|
179
182
|
init: { readonly stepNumber: number; readonly model: Model; readonly provider?: string },
|
|
180
183
|
): void {
|
|
181
184
|
const provider = init.provider ?? init.model.provider;
|
|
182
|
-
|
|
185
|
+
(span as SpanWithChatStart)[kChatStart] = {
|
|
183
186
|
stepNumber: init.stepNumber,
|
|
184
187
|
startedAtMs: performance.now(),
|
|
185
188
|
model: init.model.id,
|
|
186
189
|
provider,
|
|
187
|
-
}
|
|
190
|
+
};
|
|
188
191
|
this.#modelsUsed.add(init.model.id);
|
|
189
192
|
if (provider) this.#providersUsed.add(provider);
|
|
190
193
|
}
|
|
@@ -197,8 +200,8 @@ export class AgentRunCollector {
|
|
|
197
200
|
readonly costUnavailableReason: string | undefined;
|
|
198
201
|
},
|
|
199
202
|
): void {
|
|
200
|
-
const start =
|
|
201
|
-
|
|
203
|
+
const start = (span as SpanWithChatStart)[kChatStart];
|
|
204
|
+
(span as SpanWithChatStart)[kChatStart] = undefined;
|
|
202
205
|
const usage = message.usage;
|
|
203
206
|
// Public surface: `inputTokens` is the total cost-bearing input the
|
|
204
207
|
// provider charged for, so it must include cache_read + cache_write.
|
|
@@ -237,8 +240,8 @@ export class AgentRunCollector {
|
|
|
237
240
|
* appear in the run summary.
|
|
238
241
|
*/
|
|
239
242
|
failChat(span: Span, fields: { readonly errorType: string }): void {
|
|
240
|
-
const start =
|
|
241
|
-
|
|
243
|
+
const start = (span as SpanWithChatStart)[kChatStart];
|
|
244
|
+
(span as SpanWithChatStart)[kChatStart] = undefined;
|
|
242
245
|
this.#chats.push({
|
|
243
246
|
stepNumber: start?.stepNumber ?? -1,
|
|
244
247
|
model: start?.model ?? "",
|
|
@@ -258,17 +261,17 @@ export class AgentRunCollector {
|
|
|
258
261
|
}
|
|
259
262
|
|
|
260
263
|
beginTool(span: Span, init: { readonly toolCallId: string; readonly toolName: string }): void {
|
|
261
|
-
|
|
264
|
+
(span as SpanWithToolStart)[kToolStart] = {
|
|
262
265
|
toolCallId: init.toolCallId,
|
|
263
266
|
toolName: init.toolName,
|
|
264
267
|
startedAtMs: performance.now(),
|
|
265
|
-
}
|
|
268
|
+
};
|
|
266
269
|
this.#invokedTools.add(init.toolName);
|
|
267
270
|
}
|
|
268
271
|
|
|
269
272
|
endTool(span: Span, fields: { readonly status: ToolStatus; readonly errorType: string | undefined }): void {
|
|
270
|
-
const start =
|
|
271
|
-
|
|
273
|
+
const start = (span as SpanWithToolStart)[kToolStart];
|
|
274
|
+
(span as SpanWithToolStart)[kToolStart] = undefined;
|
|
272
275
|
this.#tools.push({
|
|
273
276
|
toolCallId: start?.toolCallId ?? "",
|
|
274
277
|
toolName: start?.toolName ?? "",
|
package/src/telemetry.ts
CHANGED
|
@@ -24,10 +24,14 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import {
|
|
27
|
+
type Api,
|
|
27
28
|
type AssistantMessage,
|
|
29
|
+
type Context,
|
|
30
|
+
completeSimple,
|
|
28
31
|
type Message,
|
|
29
32
|
type Model,
|
|
30
33
|
type ServiceTier,
|
|
34
|
+
type SimpleStreamOptions,
|
|
31
35
|
type StopReason,
|
|
32
36
|
shouldSendServiceTier,
|
|
33
37
|
type ToolChoice,
|
|
@@ -139,6 +143,10 @@ export const enum PiGenAIAttr {
|
|
|
139
143
|
HandoffFromAgentId = "pi.gen_ai.handoff.from_agent.id",
|
|
140
144
|
HandoffToAgentName = "pi.gen_ai.handoff.to_agent.name",
|
|
141
145
|
HandoffToAgentId = "pi.gen_ai.handoff.to_agent.id",
|
|
146
|
+
// Marks chat spans emitted outside the agent loop (compaction, handoff, branch
|
|
147
|
+
// summary, image inspection, …). Lets dashboards split oneshot cost / latency
|
|
148
|
+
// from main-turn cost without overloading the semconv `gen_ai.operation.name`.
|
|
149
|
+
OneshotKind = "pi.gen_ai.oneshot.kind",
|
|
142
150
|
// Gateway / proxy (LiteLLM, Helicone, Portkey, …) — populated when a known
|
|
143
151
|
// gateway header pattern is detected on the upstream response. The base
|
|
144
152
|
// `gen_ai.provider.name` continues to track the *upstream* provider (e.g.
|
|
@@ -1573,6 +1581,111 @@ export async function recordManualChatTelemetry(
|
|
|
1573
1581
|
return span;
|
|
1574
1582
|
}
|
|
1575
1583
|
|
|
1584
|
+
/**
|
|
1585
|
+
* Options accepted by {@link instrumentedCompleteSimple}. Mirrors the
|
|
1586
|
+
* `streamAssistantResponse` chat-span lifecycle for oneshot LLM calls
|
|
1587
|
+
* (compaction summaries, handoff document, branch summary, inspect_image).
|
|
1588
|
+
*/
|
|
1589
|
+
export interface InstrumentedChatSpanOptions {
|
|
1590
|
+
readonly telemetry: AgentTelemetry | undefined;
|
|
1591
|
+
/** Optional explicit parent span. Defaults to `context.active()`. */
|
|
1592
|
+
readonly parent?: Span;
|
|
1593
|
+
/** Step index recorded on the span; defaults to `-1` for non-loop calls. */
|
|
1594
|
+
readonly stepNumber?: number;
|
|
1595
|
+
/**
|
|
1596
|
+
* Tag stamped onto `pi.gen_ai.oneshot.kind`. Values used by the agent:
|
|
1597
|
+
* `compaction_summary`, `compaction_short_summary`, `compaction_turn_prefix`,
|
|
1598
|
+
* `handoff`, `branch_summary`, `inspect_image`. Free-form to allow callers
|
|
1599
|
+
* outside this package to add new kinds without bumping the helper.
|
|
1600
|
+
*/
|
|
1601
|
+
readonly oneshotKind?: string;
|
|
1602
|
+
/** Extra span attributes applied verbatim. */
|
|
1603
|
+
readonly attributes?: Attributes;
|
|
1604
|
+
/**
|
|
1605
|
+
* Override for the underlying {@link completeSimple} call. Defaults to
|
|
1606
|
+
* `completeSimple` from `@oh-my-pi/pi-ai`. Use to retain a test injection
|
|
1607
|
+
* seam while still going through the chat-span lifecycle.
|
|
1608
|
+
*/
|
|
1609
|
+
readonly completeImpl?: <TApi extends Api>(
|
|
1610
|
+
model: Model<TApi>,
|
|
1611
|
+
ctx: Context,
|
|
1612
|
+
options: SimpleStreamOptions,
|
|
1613
|
+
) => Promise<AssistantMessage>;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* Wrap a {@link completeSimple} round-trip with the same chat-span lifecycle
|
|
1618
|
+
* the agent loop uses for streamed turns: `startChatSpan` → run inside the
|
|
1619
|
+
* active span → `finishChatSpan` on success, `failChatSpan` on throw.
|
|
1620
|
+
*
|
|
1621
|
+
* Short-circuits when `telemetry` is `undefined` so cost / overhead stays at
|
|
1622
|
+
* zero for installations without an OTEL SDK.
|
|
1623
|
+
*/
|
|
1624
|
+
export async function instrumentedCompleteSimple<TApi extends Api>(
|
|
1625
|
+
model: Model<TApi>,
|
|
1626
|
+
ctx: Context,
|
|
1627
|
+
options: SimpleStreamOptions,
|
|
1628
|
+
span: InstrumentedChatSpanOptions,
|
|
1629
|
+
): Promise<AssistantMessage> {
|
|
1630
|
+
const { telemetry, parent, oneshotKind } = span;
|
|
1631
|
+
const stepNumber = span.stepNumber ?? -1;
|
|
1632
|
+
const reasoning = options.reasoning;
|
|
1633
|
+
const chatSpan = startChatSpan(telemetry, model, {
|
|
1634
|
+
parent,
|
|
1635
|
+
stepNumber,
|
|
1636
|
+
request: {
|
|
1637
|
+
maxTokens: options.maxTokens,
|
|
1638
|
+
temperature: options.temperature,
|
|
1639
|
+
topP: options.topP,
|
|
1640
|
+
topK: options.topK,
|
|
1641
|
+
presencePenalty: options.presencePenalty,
|
|
1642
|
+
serviceTier: options.serviceTier,
|
|
1643
|
+
reasoningEffort: typeof reasoning === "string" ? reasoning : undefined,
|
|
1644
|
+
toolChoice: options.toolChoice,
|
|
1645
|
+
tools: ctx.tools,
|
|
1646
|
+
systemPrompt: ctx.systemPrompt,
|
|
1647
|
+
messages: ctx.messages,
|
|
1648
|
+
},
|
|
1649
|
+
});
|
|
1650
|
+
if (chatSpan) {
|
|
1651
|
+
if (oneshotKind) chatSpan.setAttribute(PiGenAIAttr.OneshotKind, oneshotKind);
|
|
1652
|
+
if (span.attributes) chatSpan.setAttributes(span.attributes);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// Wrap the user-supplied onResponse so we always capture response headers
|
|
1656
|
+
// for the cost / gateway hooks without stealing them from the caller.
|
|
1657
|
+
let capturedHeaders: Readonly<Record<string, string>> | undefined;
|
|
1658
|
+
const userOnResponse = options.onResponse;
|
|
1659
|
+
const captureOnResponse: NonNullable<SimpleStreamOptions["onResponse"]> = (response, modelInfo) => {
|
|
1660
|
+
capturedHeaders = response.headers;
|
|
1661
|
+
return userOnResponse?.(response, modelInfo);
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
try {
|
|
1665
|
+
return await runInActiveSpan(chatSpan, async () => {
|
|
1666
|
+
const complete = span.completeImpl ?? completeSimple;
|
|
1667
|
+
const message = await complete(model, ctx, {
|
|
1668
|
+
...options,
|
|
1669
|
+
onResponse: captureOnResponse,
|
|
1670
|
+
});
|
|
1671
|
+
await finishChatSpan(telemetry, chatSpan, message, {
|
|
1672
|
+
stepNumber,
|
|
1673
|
+
serviceTier: options.serviceTier,
|
|
1674
|
+
responseHeaders: capturedHeaders,
|
|
1675
|
+
baseUrl: model.baseUrl,
|
|
1676
|
+
});
|
|
1677
|
+
return message;
|
|
1678
|
+
});
|
|
1679
|
+
} catch (err) {
|
|
1680
|
+
failChatSpan(telemetry, chatSpan, {
|
|
1681
|
+
errorObject: err,
|
|
1682
|
+
responseHeaders: capturedHeaders,
|
|
1683
|
+
baseUrl: model.baseUrl,
|
|
1684
|
+
});
|
|
1685
|
+
throw err;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1576
1689
|
/**
|
|
1577
1690
|
* Start an `execute_tool` span representing one tool invocation. Parented
|
|
1578
1691
|
* under the supplied `invoke_agent` span by default — pass `parent` to
|