@senpi/runtime 3.0.6-dev.runtime-phase-2.20260630134327 → 3.0.6-dev.runtime-phase-2.20260630134410
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/actions/llm-decision/anthropic-llm-decision.d.ts +5 -9
- package/dist/actions/llm-decision/anthropic-llm-decision.d.ts.map +1 -1
- package/dist/actions/llm-decision/anthropic-llm-decision.js +6 -86
- package/dist/actions/llm-decision/anthropic-llm-decision.js.map +1 -1
- package/dist/actions/llm-decision/factory.d.ts +0 -6
- package/dist/actions/llm-decision/factory.d.ts.map +1 -1
- package/dist/actions/llm-decision/factory.js +0 -2
- package/dist/actions/llm-decision/factory.js.map +1 -1
- package/dist/actions/llm-decision/openclaw-llm-decision.d.ts.map +1 -1
- package/dist/actions/llm-decision/openclaw-llm-decision.js +20 -0
- package/dist/actions/llm-decision/openclaw-llm-decision.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -11
- package/dist/index.js.map +1 -1
- package/dist/recording/wiring/emitter-factory.d.ts +0 -5
- package/dist/recording/wiring/emitter-factory.d.ts.map +1 -1
- package/dist/recording/wiring/emitter-factory.js +0 -18
- package/dist/recording/wiring/emitter-factory.js.map +1 -1
- package/dist/recording/wiring/index.d.ts +1 -1
- package/dist/recording/wiring/index.d.ts.map +1 -1
- package/dist/recording/wiring/index.js +1 -1
- package/dist/recording/wiring/index.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +4 -53
- package/dist/runtime/runtime-schema.d.ts.map +1 -1
- package/dist/runtime/runtime-schema.js +4 -4
- package/dist/runtime/runtime-schema.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +9 -15
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/senpi/client.d.ts +10 -0
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +69 -49
- package/dist/senpi/client.js.map +1 -1
- package/dist/telemetry/metrics.d.ts +47 -0
- package/dist/telemetry/metrics.d.ts.map +1 -0
- package/dist/telemetry/metrics.js +154 -0
- package/dist/telemetry/metrics.js.map +1 -0
- package/dist/telemetry/otel-sdk.d.ts +21 -33
- package/dist/telemetry/otel-sdk.d.ts.map +1 -1
- package/dist/telemetry/otel-sdk.js +77 -50
- package/dist/telemetry/otel-sdk.js.map +1 -1
- package/dist/telemetry/tracing.d.ts +2 -1
- package/dist/telemetry/tracing.d.ts.map +1 -1
- package/dist/telemetry/tracing.js +1 -1
- package/dist/telemetry/tracing.js.map +1 -1
- package/dist/tracking/spans/constants.d.ts +4 -11
- package/dist/tracking/spans/constants.d.ts.map +1 -1
- package/dist/tracking/spans/constants.js +4 -23
- package/dist/tracking/spans/constants.js.map +1 -1
- package/dist/tracking/spans/gen-ai.d.ts +70 -0
- package/dist/tracking/spans/gen-ai.d.ts.map +1 -0
- package/dist/tracking/spans/gen-ai.js +330 -0
- package/dist/tracking/spans/gen-ai.js.map +1 -0
- package/dist/tracking/spans/index.d.ts +6 -8
- package/dist/tracking/spans/index.d.ts.map +1 -1
- package/dist/tracking/spans/index.js +6 -11
- package/dist/tracking/spans/index.js.map +1 -1
- package/dist/tracking/spans/redact-pii.d.ts +18 -20
- package/dist/tracking/spans/redact-pii.d.ts.map +1 -1
- package/dist/tracking/spans/redact-pii.js +30 -29
- package/dist/tracking/spans/redact-pii.js.map +1 -1
- package/dist/tracking/spans/span-hooks.d.ts +0 -17
- package/dist/tracking/spans/span-hooks.d.ts.map +1 -1
- package/dist/tracking/spans/span-hooks.js +407 -207
- package/dist/tracking/spans/span-hooks.js.map +1 -1
- package/package.json +1 -1
- package/dist/tracking/spans/fan-out-span-transport.d.ts +0 -22
- package/dist/tracking/spans/fan-out-span-transport.d.ts.map +0 -1
- package/dist/tracking/spans/fan-out-span-transport.js +0 -49
- package/dist/tracking/spans/fan-out-span-transport.js.map +0 -1
- package/dist/tracking/spans/otel/fit-row-budget.d.ts +0 -22
- package/dist/tracking/spans/otel/fit-row-budget.d.ts.map +0 -1
- package/dist/tracking/spans/otel/fit-row-budget.js +0 -91
- package/dist/tracking/spans/otel/fit-row-budget.js.map +0 -1
- package/dist/tracking/spans/otel/map-span-to-trace-row.d.ts +0 -16
- package/dist/tracking/spans/otel/map-span-to-trace-row.d.ts.map +0 -1
- package/dist/tracking/spans/otel/map-span-to-trace-row.js +0 -227
- package/dist/tracking/spans/otel/map-span-to-trace-row.js.map +0 -1
- package/dist/tracking/spans/otel/recording-span-transport.d.ts +0 -43
- package/dist/tracking/spans/otel/recording-span-transport.d.ts.map +0 -1
- package/dist/tracking/spans/otel/recording-span-transport.js +0 -67
- package/dist/tracking/spans/otel/recording-span-transport.js.map +0 -1
- package/dist/tracking/spans/otel/span-ids.d.ts +0 -22
- package/dist/tracking/spans/otel/span-ids.d.ts.map +0 -1
- package/dist/tracking/spans/otel/span-ids.js +0 -32
- package/dist/tracking/spans/otel/span-ids.js.map +0 -1
- package/dist/tracking/spans/plugin-span-tracker.d.ts +0 -9
- package/dist/tracking/spans/plugin-span-tracker.d.ts.map +0 -1
- package/dist/tracking/spans/plugin-span-tracker.js +0 -115
- package/dist/tracking/spans/plugin-span-tracker.js.map +0 -1
- package/dist/tracking/spans/span-tracker.d.ts +0 -133
- package/dist/tracking/spans/span-tracker.d.ts.map +0 -1
- package/dist/tracking/spans/span-tracker.js +0 -721
- package/dist/tracking/spans/span-tracker.js.map +0 -1
- package/dist/tracking/spans/span-transport.d.ts +0 -71
- package/dist/tracking/spans/span-transport.d.ts.map +0 -1
- package/dist/tracking/spans/span-transport.js +0 -240
- package/dist/tracking/spans/span-transport.js.map +0 -1
- package/dist/tracking/spans/types.d.ts +0 -101
- package/dist/tracking/spans/types.d.ts.map +0 -1
- package/dist/tracking/spans/types.js +0 -6
- package/dist/tracking/spans/types.js.map +0 -1
|
@@ -1,13 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* registerSpanHooks: wires the nine OpenClaw plugin hooks (llm_input/llm_output,
|
|
3
|
+
* before/after_tool_call, the four subagent_* hooks, agent_end) to real OTel gen_ai
|
|
4
|
+
* spans via the shared `getTracer()`:
|
|
5
|
+
* - llm_input → llm_output → `chat` (CLIENT)
|
|
6
|
+
* - tool_call → `execute_tool` (INTERNAL), parented to the model span
|
|
7
|
+
* - subagent_* lifecycle → `invoke_agent` (INTERNAL), parented to the model span
|
|
8
|
+
* - agent_end → ends any still-open spans for the run
|
|
9
|
+
*
|
|
10
|
+
* Open and finalize are separate callbacks, so we can't use `withSpan`; we `startSpan`
|
|
11
|
+
* and hold the handle in a session-keyed registry until the finalize hook, recording
|
|
12
|
+
* errors directly on the held span (it's never the active span, so `logger.error` won't).
|
|
13
|
+
*
|
|
14
|
+
* Identity rides as `senpi.*` attributes from the `X-Senpi-*` headers (ALS doesn't cross
|
|
15
|
+
* the loopback fetch). The trading trace is joined by the `traceparent` header when present
|
|
16
|
+
* (nesting under `signal.process`), else by a `senpi.correlation_id` attribute. Handlers
|
|
17
|
+
* never throw.
|
|
18
|
+
*/
|
|
19
|
+
import { SpanKind, context as otelContext, propagation, trace, } from "@opentelemetry/api";
|
|
20
|
+
import { getTracer } from "../../telemetry/tracing.js";
|
|
1
21
|
import { HEADER_ADDRESS, HEADER_CORRELATION_ID, HEADER_RECIPE_NAME, HEADER_RUNTIME_ID, HEADER_RUNTIME_VERSION, } from "./constants.js";
|
|
22
|
+
import { GEN_AI, errorTypeOf, firstNumber, normalizeGenAiMessages, recordSpanError, setDefinedAttributes, setGenAiContentAttribute, toFinishReasons, } from "./gen-ai.js";
|
|
23
|
+
import { elapsedSeconds, recordExecuteToolDuration, recordInvokeAgentDuration, recordOperationDuration, recordTokenUsage, } from "../../telemetry/metrics.js";
|
|
2
24
|
import { createLogger } from "../../utils/logger.js";
|
|
3
|
-
import { sanitizePayloadValue } from "./sanitize-payload.js";
|
|
4
25
|
const logger = createLogger("[senpi-core][span-hooks]");
|
|
5
26
|
export function registerSpanHooks(options) {
|
|
6
|
-
const { api
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
27
|
+
const { api } = options;
|
|
28
|
+
const tracer = getTracer();
|
|
29
|
+
// Held model spans, keyed by (sessionKey, runId). The tool/subagent spans parent
|
|
30
|
+
// off the model span for the same session/run.
|
|
31
|
+
const modelSpans = new Map();
|
|
32
|
+
// Held tool spans, keyed by (sessionKey, runId, toolCallId|toolName seq).
|
|
33
|
+
const toolSpans = new Map();
|
|
34
|
+
// Held subagent spans, keyed by childSessionKey (sessionKey/runId = the host).
|
|
35
|
+
const subagentSpans = new Map();
|
|
36
|
+
let toolSeq = 0;
|
|
37
|
+
// Tool hooks often arrive with a ctx that lacks sessionKey/runId. Remember
|
|
38
|
+
// (agentId → sessionKey/runId) from llm_input, plus the last-seen pair, so tool
|
|
39
|
+
// hooks can resolve their host session.
|
|
11
40
|
const sessionByAgentId = new Map();
|
|
12
41
|
let lastActive = null;
|
|
13
42
|
const rememberSession = (agentCtx, sessionKey, runId) => {
|
|
@@ -32,10 +61,15 @@ export function registerSpanHooks(options) {
|
|
|
32
61
|
}
|
|
33
62
|
return lastActive;
|
|
34
63
|
};
|
|
35
|
-
|
|
64
|
+
/** Build a parent Context for tool/subagent spans from the host model span (if any). */
|
|
65
|
+
const parentContextForModel = (sessionKey, runId) => {
|
|
66
|
+
const model = modelSpans.get(sessionLlmKey(sessionKey, runId));
|
|
67
|
+
return model ? trace.setSpan(otelContext.active(), model.span) : otelContext.active();
|
|
68
|
+
};
|
|
69
|
+
// ─── LLM (`chat` span) ───────────────────────────────────────────────────
|
|
36
70
|
api.on("llm_input", (event, agentCtx) => {
|
|
37
71
|
try {
|
|
38
|
-
const
|
|
72
|
+
const identity = readIdentity(agentCtx);
|
|
39
73
|
const sessionKey = readSessionKey(agentCtx, event);
|
|
40
74
|
const runId = readRunId(agentCtx, event);
|
|
41
75
|
if (!sessionKey || !runId) {
|
|
@@ -43,13 +77,43 @@ export function registerSpanHooks(options) {
|
|
|
43
77
|
return;
|
|
44
78
|
}
|
|
45
79
|
rememberSession(agentCtx, sessionKey, runId);
|
|
46
|
-
|
|
80
|
+
const key = sessionLlmKey(sessionKey, runId);
|
|
81
|
+
// A duplicate llm_input for the same (session, run) means a new turn opened
|
|
82
|
+
// without the previous one closing — end the prior span before replacing it.
|
|
83
|
+
const prior = modelSpans.get(key);
|
|
84
|
+
if (prior)
|
|
85
|
+
endSpan(prior.span);
|
|
86
|
+
const ev = asRecord(event) ?? {};
|
|
87
|
+
const requestModel = nonEmptyString(ev.model);
|
|
88
|
+
const provider = nonEmptyString(ev.provider);
|
|
89
|
+
const span = tracer.startSpan(`chat ${requestModel ?? "unknown"}`, { kind: SpanKind.CLIENT },
|
|
90
|
+
// Parent off the trading trace when the traceparent header threaded through.
|
|
91
|
+
parentContextFromHeaders(agentCtx));
|
|
92
|
+
const held = {
|
|
93
|
+
span,
|
|
47
94
|
sessionKey,
|
|
48
95
|
runId,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
96
|
+
startMs: Date.now(),
|
|
97
|
+
provider,
|
|
98
|
+
model: requestModel,
|
|
99
|
+
};
|
|
100
|
+
modelSpans.set(key, held);
|
|
101
|
+
setDefinedAttributes(span, {
|
|
102
|
+
[GEN_AI.OPERATION_NAME]: "chat",
|
|
103
|
+
[GEN_AI.PROVIDER_NAME]: provider,
|
|
104
|
+
[GEN_AI.REQUEST_MODEL]: requestModel,
|
|
105
|
+
[GEN_AI.REQUEST_TEMPERATURE]: firstNumber(ev.temperature),
|
|
106
|
+
[GEN_AI.REQUEST_MAX_TOKENS]: firstNumber(ev.maxTokens),
|
|
107
|
+
...identityAttributes(identity),
|
|
52
108
|
});
|
|
109
|
+
// Content (redacted + capped). Stamping is async and the span ends in llm_output,
|
|
110
|
+
// so track the promise for the finalize to await — else a fast round-trip could end
|
|
111
|
+
// the span before the input content lands.
|
|
112
|
+
held.pending = Promise.allSettled([
|
|
113
|
+
setGenAiContentAttribute(span, GEN_AI.SYSTEM_INSTRUCTIONS, buildSystemInstructions(ev)),
|
|
114
|
+
setGenAiContentAttribute(span, GEN_AI.INPUT_MESSAGES, buildInputMessages(ev)),
|
|
115
|
+
setGenAiContentAttribute(span, GEN_AI.TOOL_DEFINITIONS, ev.tools),
|
|
116
|
+
]);
|
|
53
117
|
}
|
|
54
118
|
catch (err) {
|
|
55
119
|
logger.warn("llm_input handler failed", { err: errMsg(err) });
|
|
@@ -57,7 +121,6 @@ export function registerSpanHooks(options) {
|
|
|
57
121
|
});
|
|
58
122
|
api.on("llm_output", (event, agentCtx) => {
|
|
59
123
|
try {
|
|
60
|
-
const ctx = readContext(agentCtx);
|
|
61
124
|
const sessionKey = readSessionKey(agentCtx, event);
|
|
62
125
|
const runId = readRunId(agentCtx, event);
|
|
63
126
|
if (!sessionKey || !runId) {
|
|
@@ -65,47 +128,88 @@ export function registerSpanHooks(options) {
|
|
|
65
128
|
return;
|
|
66
129
|
}
|
|
67
130
|
rememberSession(agentCtx, sessionKey, runId);
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
sessionKey,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
131
|
+
const held = modelSpans.get(sessionLlmKey(sessionKey, runId));
|
|
132
|
+
if (!held) {
|
|
133
|
+
logger.debug("llm_output: no open chat span", { sessionKey, runId });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const span = held.span;
|
|
137
|
+
modelSpans.delete(sessionLlmKey(sessionKey, runId));
|
|
138
|
+
const ev = asRecord(event) ?? {};
|
|
139
|
+
const usage = asRecord(ev.usage);
|
|
140
|
+
// Token field naming varies by provider/wrapper; try every known spelling.
|
|
141
|
+
const inputTokens = firstNumber(usage?.input_tokens, usage?.prompt_tokens, usage?.promptTokens, usage?.input);
|
|
142
|
+
const outputTokens = firstNumber(usage?.output_tokens, usage?.completion_tokens, usage?.completionTokens, usage?.output);
|
|
143
|
+
setDefinedAttributes(span, {
|
|
144
|
+
[GEN_AI.RESPONSE_MODEL]: nonEmptyString(ev.model),
|
|
145
|
+
[GEN_AI.RESPONSE_FINISH_REASONS]: toFinishReasons(ev.finishReason ?? ev.stopReason),
|
|
146
|
+
[GEN_AI.USAGE_INPUT_TOKENS]: inputTokens,
|
|
147
|
+
[GEN_AI.USAGE_OUTPUT_TOKENS]: outputTokens,
|
|
77
148
|
});
|
|
149
|
+
const error = ev.error;
|
|
150
|
+
const errored = error !== undefined && error !== null;
|
|
151
|
+
const dims = { provider: held.provider, model: held.model };
|
|
152
|
+
recordTokenUsage(dims, { input: inputTokens, output: outputTokens });
|
|
153
|
+
recordOperationDuration(elapsedSeconds(held.startMs), {
|
|
154
|
+
...dims,
|
|
155
|
+
errorType: errored ? errorTypeOf(error) : undefined,
|
|
156
|
+
});
|
|
157
|
+
const outputMessages = buildOutputMessages(ev);
|
|
158
|
+
void (async () => {
|
|
159
|
+
try {
|
|
160
|
+
if (held.pending)
|
|
161
|
+
await held.pending;
|
|
162
|
+
await setGenAiContentAttribute(span, GEN_AI.OUTPUT_MESSAGES, outputMessages);
|
|
163
|
+
if (errored)
|
|
164
|
+
recordSpanError(span, error);
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
try {
|
|
168
|
+
span.end();
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
/* best effort */
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
})();
|
|
78
175
|
}
|
|
79
176
|
catch (err) {
|
|
80
177
|
logger.warn("llm_output handler failed", { err: errMsg(err) });
|
|
81
178
|
}
|
|
82
179
|
});
|
|
83
|
-
// ─── Tool
|
|
84
|
-
// OpenClaw's tool-hook context often omits sessionKey/runId. Fall back to
|
|
85
|
-
// (agentId → session/run) learned from the most recent llm_input, then to
|
|
86
|
-
// the last-active pair, matching opik's strategy.
|
|
180
|
+
// ─── Tool (`execute_tool` span) ────────────────────────────────────────────
|
|
87
181
|
api.on("before_tool_call", (event, toolCtx) => {
|
|
88
182
|
try {
|
|
89
|
-
const
|
|
183
|
+
const identity = readIdentity(toolCtx);
|
|
90
184
|
const resolved = resolveToolSession(toolCtx);
|
|
91
185
|
if (!resolved) {
|
|
92
|
-
logger.debug("before_tool_call
|
|
186
|
+
logger.debug("before_tool_call session unresolved (no llm_input seen yet); skipping");
|
|
93
187
|
return;
|
|
94
188
|
}
|
|
95
|
-
const
|
|
96
|
-
const toolName = nonEmptyString(
|
|
97
|
-
const toolCallId = nonEmptyString(
|
|
189
|
+
const ev = asRecord(event);
|
|
190
|
+
const toolName = nonEmptyString(ev?.toolName) ?? nonEmptyString(ev?.name) ?? "unknown";
|
|
191
|
+
const toolCallId = nonEmptyString(ev?.toolCallId) ??
|
|
98
192
|
nonEmptyString(asRecord(toolCtx)?.toolCallId) ??
|
|
99
193
|
null;
|
|
100
|
-
|
|
194
|
+
const key = toolKey(resolved.sessionKey, resolved.runId, toolCallId, toolName, ++toolSeq);
|
|
195
|
+
const span = tracer.startSpan(`execute_tool ${toolName}`, { kind: SpanKind.INTERNAL }, parentContextForModel(resolved.sessionKey, resolved.runId));
|
|
196
|
+
const held = {
|
|
197
|
+
span,
|
|
101
198
|
sessionKey: resolved.sessionKey,
|
|
102
199
|
runId: resolved.runId,
|
|
103
|
-
|
|
104
|
-
toolCallId,
|
|
200
|
+
startMs: Date.now(),
|
|
105
201
|
toolName,
|
|
106
|
-
|
|
107
|
-
|
|
202
|
+
};
|
|
203
|
+
toolSpans.set(key, held);
|
|
204
|
+
setDefinedAttributes(span, {
|
|
205
|
+
[GEN_AI.OPERATION_NAME]: "execute_tool",
|
|
206
|
+
[GEN_AI.TOOL_NAME]: toolName,
|
|
207
|
+
[GEN_AI.TOOL_CALL_ID]: toolCallId ?? undefined,
|
|
208
|
+
...identityAttributes(identity),
|
|
108
209
|
});
|
|
210
|
+
// Args stamping is async; the finalize (after_tool_call) awaits this so a fast
|
|
211
|
+
// tool doesn't end the span before the args land.
|
|
212
|
+
held.pending = setGenAiContentAttribute(span, GEN_AI.TOOL_CALL_ARGUMENTS, ev?.params ?? ev?.input);
|
|
109
213
|
}
|
|
110
214
|
catch (err) {
|
|
111
215
|
logger.warn("before_tool_call handler failed", { err: errMsg(err) });
|
|
@@ -113,69 +217,93 @@ export function registerSpanHooks(options) {
|
|
|
113
217
|
});
|
|
114
218
|
api.on("after_tool_call", (event, toolCtx) => {
|
|
115
219
|
try {
|
|
116
|
-
const ctx = readContext(toolCtx);
|
|
117
220
|
const resolved = resolveToolSession(toolCtx);
|
|
118
221
|
if (!resolved) {
|
|
119
|
-
logger.debug("after_tool_call
|
|
222
|
+
logger.debug("after_tool_call session unresolved; skipping");
|
|
120
223
|
return;
|
|
121
224
|
}
|
|
122
|
-
const
|
|
123
|
-
const toolName = nonEmptyString(
|
|
124
|
-
const toolCallId = nonEmptyString(
|
|
225
|
+
const ev = asRecord(event);
|
|
226
|
+
const toolName = nonEmptyString(ev?.toolName) ?? nonEmptyString(ev?.name) ?? "unknown";
|
|
227
|
+
const toolCallId = nonEmptyString(ev?.toolCallId) ??
|
|
125
228
|
nonEmptyString(asRecord(toolCtx)?.toolCallId) ??
|
|
126
229
|
null;
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
230
|
+
const foundKey = findToolKey(toolSpans, resolved.sessionKey, resolved.runId, toolCallId, toolName);
|
|
231
|
+
if (!foundKey) {
|
|
232
|
+
logger.debug("after_tool_call: no open tool span", { toolName, toolCallId });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const held = toolSpans.get(foundKey);
|
|
236
|
+
const span = held.span;
|
|
237
|
+
toolSpans.delete(foundKey);
|
|
238
|
+
const error = ev?.error;
|
|
239
|
+
const errored = error !== undefined && error !== null;
|
|
240
|
+
const result = ev?.result;
|
|
241
|
+
recordExecuteToolDuration(elapsedSeconds(held.startMs), {
|
|
242
|
+
toolName: held.toolName,
|
|
243
|
+
errorType: errored ? errorTypeOf(error) : undefined,
|
|
139
244
|
});
|
|
245
|
+
void (async () => {
|
|
246
|
+
try {
|
|
247
|
+
if (held.pending)
|
|
248
|
+
await held.pending;
|
|
249
|
+
if (errored) {
|
|
250
|
+
recordSpanError(span, error);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
await setGenAiContentAttribute(span, GEN_AI.TOOL_CALL_RESULT, result);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
try {
|
|
258
|
+
span.end();
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
/* best effort */
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
})();
|
|
140
265
|
}
|
|
141
266
|
catch (err) {
|
|
142
267
|
logger.warn("after_tool_call handler failed", { err: errMsg(err) });
|
|
143
268
|
}
|
|
144
269
|
});
|
|
145
|
-
// ─── Subagent (four
|
|
270
|
+
// ─── Subagent (`invoke_agent` span, four-hook lifecycle) ───────────────────
|
|
146
271
|
api.on("subagent_spawning", (event, subagentCtx) => {
|
|
147
272
|
try {
|
|
148
|
-
const
|
|
149
|
-
const
|
|
273
|
+
const identity = readIdentity(subagentCtx);
|
|
274
|
+
const ev = asRecord(event);
|
|
150
275
|
const ctxObj = asRecord(subagentCtx);
|
|
151
276
|
const hostSessionKey = nonEmptyString(ctxObj?.requesterSessionKey) ??
|
|
152
277
|
nonEmptyString(ctxObj?.sessionKey) ??
|
|
153
278
|
"";
|
|
154
|
-
const hostRunId = nonEmptyString(ctxObj?.runId) ??
|
|
155
|
-
|
|
156
|
-
"";
|
|
157
|
-
const childSessionKey = nonEmptyString(evObj?.childSessionKey) ??
|
|
158
|
-
nonEmptyString(ctxObj?.childSessionKey);
|
|
279
|
+
const hostRunId = nonEmptyString(ctxObj?.runId) ?? nonEmptyString(ctxObj?.requesterRunId) ?? "";
|
|
280
|
+
const childSessionKey = nonEmptyString(ev?.childSessionKey) ?? nonEmptyString(ctxObj?.childSessionKey);
|
|
159
281
|
if (!childSessionKey) {
|
|
160
282
|
logger.warn("subagent_spawning missing childSessionKey; skipping span");
|
|
161
283
|
return;
|
|
162
284
|
}
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
hostRunId
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
285
|
+
const prior = subagentSpans.get(childSessionKey);
|
|
286
|
+
if (prior)
|
|
287
|
+
endSpan(prior.span);
|
|
288
|
+
const agentName = nonEmptyString(ev?.label) ?? nonEmptyString(ev?.agentId) ?? "unknown";
|
|
289
|
+
const span = tracer.startSpan(`invoke_agent ${agentName}`, { kind: SpanKind.INTERNAL }, hostSessionKey && hostRunId
|
|
290
|
+
? parentContextForModel(hostSessionKey, hostRunId)
|
|
291
|
+
: otelContext.active());
|
|
292
|
+
// sessionKey/runId carry the HOST run so the agent_end sweep can reach this span.
|
|
293
|
+
subagentSpans.set(childSessionKey, {
|
|
294
|
+
span,
|
|
295
|
+
sessionKey: hostSessionKey,
|
|
296
|
+
runId: hostRunId,
|
|
297
|
+
startMs: Date.now(),
|
|
298
|
+
agentName,
|
|
299
|
+
});
|
|
300
|
+
setDefinedAttributes(span, {
|
|
301
|
+
[GEN_AI.OPERATION_NAME]: "invoke_agent",
|
|
302
|
+
[GEN_AI.AGENT_NAME]: agentName,
|
|
303
|
+
"senpi.subagent.mode": nonEmptyString(ev?.mode),
|
|
304
|
+
"senpi.subagent.requester": nonEmptyString(ev?.requester),
|
|
305
|
+
"senpi.subagent.thread_requested": typeof ev?.threadRequested === "boolean" ? ev.threadRequested : undefined,
|
|
306
|
+
...identityAttributes(identity),
|
|
179
307
|
});
|
|
180
308
|
}
|
|
181
309
|
catch (err) {
|
|
@@ -184,23 +312,20 @@ export function registerSpanHooks(options) {
|
|
|
184
312
|
});
|
|
185
313
|
api.on("subagent_spawned", (event, subagentCtx) => {
|
|
186
314
|
try {
|
|
187
|
-
const
|
|
188
|
-
const evObj = asRecord(event);
|
|
315
|
+
const ev = asRecord(event);
|
|
189
316
|
const ctxObj = asRecord(subagentCtx);
|
|
190
|
-
const childSessionKey = nonEmptyString(
|
|
191
|
-
nonEmptyString(ctxObj?.childSessionKey);
|
|
317
|
+
const childSessionKey = nonEmptyString(ev?.childSessionKey) ?? nonEmptyString(ctxObj?.childSessionKey);
|
|
192
318
|
if (!childSessionKey)
|
|
193
319
|
return;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}, ctx);
|
|
320
|
+
const held = subagentSpans.get(childSessionKey);
|
|
321
|
+
if (!held)
|
|
322
|
+
return;
|
|
323
|
+
const span = held.span;
|
|
324
|
+
setDefinedAttributes(span, {
|
|
325
|
+
"senpi.subagent.run_id": nonEmptyString(ev?.runId) ?? nonEmptyString(ctxObj?.runId),
|
|
326
|
+
"senpi.subagent.mode": nonEmptyString(ev?.mode),
|
|
327
|
+
"senpi.subagent.thread_requested": typeof ev?.threadRequested === "boolean" ? ev.threadRequested : undefined,
|
|
328
|
+
});
|
|
204
329
|
}
|
|
205
330
|
catch (err) {
|
|
206
331
|
logger.warn("subagent_spawned handler failed", { err: errMsg(err) });
|
|
@@ -208,31 +333,23 @@ export function registerSpanHooks(options) {
|
|
|
208
333
|
});
|
|
209
334
|
api.on("subagent_delivery_target", (event, subagentCtx) => {
|
|
210
335
|
try {
|
|
211
|
-
const
|
|
212
|
-
const evObj = asRecord(event);
|
|
336
|
+
const ev = asRecord(event);
|
|
213
337
|
const ctxObj = asRecord(subagentCtx);
|
|
214
|
-
const childSessionKey = nonEmptyString(
|
|
215
|
-
nonEmptyString(ctxObj?.childSessionKey);
|
|
338
|
+
const childSessionKey = nonEmptyString(ev?.childSessionKey) ?? nonEmptyString(ctxObj?.childSessionKey);
|
|
216
339
|
if (!childSessionKey)
|
|
217
340
|
return;
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
expectsCompletionMessage: typeof evObj?.expectsCompletionMessage === "boolean"
|
|
231
|
-
? evObj.expectsCompletionMessage
|
|
232
|
-
: undefined,
|
|
233
|
-
},
|
|
234
|
-
},
|
|
235
|
-
}, ctx);
|
|
341
|
+
const held = subagentSpans.get(childSessionKey);
|
|
342
|
+
if (!held)
|
|
343
|
+
return;
|
|
344
|
+
const span = held.span;
|
|
345
|
+
const origin = asRecord(ev?.requesterOrigin) ?? {};
|
|
346
|
+
setDefinedAttributes(span, {
|
|
347
|
+
"senpi.subagent.delivery.channel": nonEmptyString(origin.channel),
|
|
348
|
+
"senpi.subagent.delivery.account_id": nonEmptyString(origin.accountId),
|
|
349
|
+
"senpi.subagent.delivery.to": nonEmptyString(origin.to),
|
|
350
|
+
"senpi.subagent.delivery.child_run_id": nonEmptyString(ev?.childRunId),
|
|
351
|
+
"senpi.subagent.delivery.spawn_mode": nonEmptyString(ev?.spawnMode),
|
|
352
|
+
});
|
|
236
353
|
}
|
|
237
354
|
catch (err) {
|
|
238
355
|
logger.warn("subagent_delivery_target handler failed", { err: errMsg(err) });
|
|
@@ -240,79 +357,192 @@ export function registerSpanHooks(options) {
|
|
|
240
357
|
});
|
|
241
358
|
api.on("subagent_ended", (event, subagentCtx) => {
|
|
242
359
|
try {
|
|
243
|
-
const
|
|
244
|
-
const evObj = asRecord(event);
|
|
360
|
+
const ev = asRecord(event);
|
|
245
361
|
const ctxObj = asRecord(subagentCtx);
|
|
246
|
-
const childSessionKey = nonEmptyString(
|
|
362
|
+
const childSessionKey = nonEmptyString(ev?.targetSessionKey) ??
|
|
247
363
|
nonEmptyString(ctxObj?.childSessionKey) ??
|
|
248
|
-
nonEmptyString(
|
|
364
|
+
nonEmptyString(ev?.childSessionKey);
|
|
249
365
|
if (!childSessionKey)
|
|
250
366
|
return;
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
367
|
+
const held = subagentSpans.get(childSessionKey);
|
|
368
|
+
if (!held)
|
|
369
|
+
return;
|
|
370
|
+
const span = held.span;
|
|
371
|
+
subagentSpans.delete(childSessionKey);
|
|
372
|
+
const error = ev?.error;
|
|
373
|
+
const errored = error !== undefined && error !== null;
|
|
374
|
+
recordInvokeAgentDuration(elapsedSeconds(held.startMs), {
|
|
375
|
+
agentName: held.agentName,
|
|
376
|
+
errorType: errored ? errorTypeOf(error) : undefined,
|
|
377
|
+
});
|
|
378
|
+
if (errored) {
|
|
379
|
+
recordSpanError(span, error);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
setDefinedAttributes(span, {
|
|
383
|
+
"senpi.subagent.outcome": nonEmptyString(ev?.outcome),
|
|
384
|
+
"senpi.subagent.reason": nonEmptyString(ev?.reason),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
span.end();
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
/* best effort */
|
|
392
|
+
}
|
|
263
393
|
}
|
|
264
394
|
catch (err) {
|
|
265
395
|
logger.warn("subagent_ended handler failed", { err: errMsg(err) });
|
|
266
396
|
}
|
|
267
397
|
});
|
|
268
|
-
// ─── Agent end (sweep
|
|
398
|
+
// ─── Agent end (orphan sweep) ──────────────────────────────────────────────
|
|
269
399
|
api.on("agent_end", (event, agentCtx) => {
|
|
270
400
|
try {
|
|
271
|
-
const ctx = readContext(agentCtx);
|
|
272
401
|
const sessionKey = readSessionKey(agentCtx, event);
|
|
273
402
|
const runId = readRunId(agentCtx, event);
|
|
274
403
|
if (!sessionKey || !runId) {
|
|
275
|
-
logger.warn("agent_end missing sessionKey/runId; skipping
|
|
404
|
+
logger.warn("agent_end missing sessionKey/runId; skipping sweep");
|
|
276
405
|
return;
|
|
277
406
|
}
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
407
|
+
const ev = asRecord(event);
|
|
408
|
+
const outcomeError = ev?.error;
|
|
409
|
+
// End any span still open under this (session, run): the model span, its tool
|
|
410
|
+
// children, and any subagents it hosted. Matching is by the stored host
|
|
411
|
+
// session/run, so nothing leaks past the run's end (incl. subagents, which have
|
|
412
|
+
// no other guaranteed terminator).
|
|
413
|
+
const llmKey = sessionLlmKey(sessionKey, runId);
|
|
414
|
+
const model = modelSpans.get(llmKey);
|
|
415
|
+
if (model) {
|
|
416
|
+
modelSpans.delete(llmKey);
|
|
417
|
+
const errored = outcomeError !== undefined && outcomeError !== null;
|
|
418
|
+
recordOperationDuration(elapsedSeconds(model.startMs), {
|
|
419
|
+
provider: model.provider,
|
|
420
|
+
model: model.model,
|
|
421
|
+
errorType: errored ? errorTypeOf(outcomeError) : undefined,
|
|
422
|
+
});
|
|
423
|
+
if (errored)
|
|
424
|
+
recordSpanError(model.span, outcomeError);
|
|
425
|
+
endSpan(model.span);
|
|
426
|
+
}
|
|
427
|
+
for (const [k, held] of toolSpans) {
|
|
428
|
+
if (held.sessionKey === sessionKey && held.runId === runId) {
|
|
429
|
+
toolSpans.delete(k);
|
|
430
|
+
endSpan(held.span);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
for (const [k, held] of subagentSpans) {
|
|
434
|
+
if (held.sessionKey === sessionKey && held.runId === runId) {
|
|
435
|
+
subagentSpans.delete(k);
|
|
436
|
+
endSpan(held.span);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
286
439
|
}
|
|
287
440
|
catch (err) {
|
|
288
441
|
logger.warn("agent_end handler failed", { err: errMsg(err) });
|
|
289
442
|
}
|
|
290
443
|
});
|
|
291
444
|
}
|
|
292
|
-
// ───
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
445
|
+
// ─── Span key helpers ───────────────────────────────────────────────────────
|
|
446
|
+
const SEP = " ";
|
|
447
|
+
function sessionLlmKey(sessionKey, runId) {
|
|
448
|
+
return `${sessionKey}${SEP}${runId}${SEP}llm`;
|
|
449
|
+
}
|
|
450
|
+
function toolKey(sessionKey, runId, toolCallId, toolName, seq) {
|
|
451
|
+
return toolCallId
|
|
452
|
+
? `${sessionKey}${SEP}${runId}${SEP}toolcall${SEP}${toolCallId}`
|
|
453
|
+
: `${sessionKey}${SEP}${runId}${SEP}tool${SEP}${toolName}${SEP}${seq}`;
|
|
454
|
+
}
|
|
455
|
+
/** Find the open tool span key within one (session, run): by toolCallId first, else by tool name. */
|
|
456
|
+
function findToolKey(toolSpans, sessionKey, runId, toolCallId, toolName) {
|
|
457
|
+
const prefix = `${sessionKey}${SEP}${runId}${SEP}`;
|
|
458
|
+
if (toolCallId) {
|
|
459
|
+
const suffix = `${SEP}toolcall${SEP}${toolCallId}`;
|
|
460
|
+
for (const key of toolSpans.keys()) {
|
|
461
|
+
if (key.startsWith(prefix) && key.endsWith(suffix))
|
|
462
|
+
return key;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const namePart = `${SEP}tool${SEP}${toolName}${SEP}`;
|
|
466
|
+
for (const key of toolSpans.keys()) {
|
|
467
|
+
if (key.startsWith(prefix) && key.includes(namePart))
|
|
468
|
+
return key;
|
|
296
469
|
}
|
|
297
470
|
return undefined;
|
|
298
471
|
}
|
|
299
|
-
function
|
|
300
|
-
|
|
472
|
+
function endSpan(span) {
|
|
473
|
+
try {
|
|
474
|
+
span.end();
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
/* best effort */
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// ─── Identity + trace correlation ─────────────────────────────────────────────
|
|
481
|
+
const ADDRESS_RE = /^0x[a-f0-9]{40}$/i;
|
|
482
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
483
|
+
/** Cap on a free-form header value stamped as an attribute (defence vs oversized header). */
|
|
484
|
+
const MAX_HEADER_ATTR_CHARS = 256;
|
|
485
|
+
function clampHeader(v) {
|
|
486
|
+
return v.length > MAX_HEADER_ATTR_CHARS ? v.slice(0, MAX_HEADER_ATTR_CHARS) : v;
|
|
301
487
|
}
|
|
302
488
|
/**
|
|
303
|
-
*
|
|
304
|
-
*
|
|
489
|
+
* Map the senpi identity headers to `senpi.*` span attributes (keys match the on-site
|
|
490
|
+
* identity bag; the headers carry the runtime id as `senpi.runtime.id`, vs on-site
|
|
491
|
+
* `senpi.runtime`). Headers are caller-influenceable, so join keys are validated — a
|
|
492
|
+
* malformed address / correlation id is dropped, not stamped as garbage.
|
|
305
493
|
*/
|
|
306
|
-
function
|
|
494
|
+
function identityAttributes(identity) {
|
|
495
|
+
const attrs = {};
|
|
496
|
+
if (identity.address) {
|
|
497
|
+
const lowered = identity.address.toLowerCase();
|
|
498
|
+
if (ADDRESS_RE.test(lowered))
|
|
499
|
+
attrs["senpi.strategy.address"] = lowered;
|
|
500
|
+
}
|
|
501
|
+
if (identity.recipeName)
|
|
502
|
+
attrs["senpi.recipe"] = clampHeader(identity.recipeName);
|
|
503
|
+
if (identity.runtimeVersion)
|
|
504
|
+
attrs["senpi.recipe.version"] = clampHeader(identity.runtimeVersion);
|
|
505
|
+
if (identity.runtimeId)
|
|
506
|
+
attrs["senpi.runtime.id"] = clampHeader(identity.runtimeId);
|
|
507
|
+
// Fallback join key to the trading trace when traceparent didn't thread through.
|
|
508
|
+
if (identity.correlationId && UUID_RE.test(identity.correlationId)) {
|
|
509
|
+
attrs["senpi.correlation_id"] = identity.correlationId;
|
|
510
|
+
}
|
|
511
|
+
return attrs;
|
|
512
|
+
}
|
|
513
|
+
function readIdentity(agentCtx) {
|
|
307
514
|
const headers = readHeaders(agentCtx);
|
|
308
515
|
return {
|
|
309
|
-
correlationId: headers[HEADER_CORRELATION_ID]
|
|
310
|
-
address: headers[HEADER_ADDRESS]
|
|
311
|
-
recipeName: headers[HEADER_RECIPE_NAME]
|
|
312
|
-
runtimeId: headers[HEADER_RUNTIME_ID]
|
|
313
|
-
runtimeVersion: headers[HEADER_RUNTIME_VERSION]
|
|
516
|
+
correlationId: headers[HEADER_CORRELATION_ID],
|
|
517
|
+
address: headers[HEADER_ADDRESS],
|
|
518
|
+
recipeName: headers[HEADER_RECIPE_NAME],
|
|
519
|
+
runtimeId: headers[HEADER_RUNTIME_ID],
|
|
520
|
+
runtimeVersion: headers[HEADER_RUNTIME_VERSION],
|
|
314
521
|
};
|
|
315
522
|
}
|
|
523
|
+
/** Extract a W3C traceparent from the request headers → a parent Context (else active). */
|
|
524
|
+
function parentContextFromHeaders(agentCtx) {
|
|
525
|
+
try {
|
|
526
|
+
const headers = readHeaders(agentCtx);
|
|
527
|
+
if (headers.traceparent === undefined)
|
|
528
|
+
return otelContext.active();
|
|
529
|
+
return propagation.extract(otelContext.active(), headers);
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
return otelContext.active();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// ─── Field readers ────────────────────────────────────────────────────────────
|
|
536
|
+
function asRecord(v) {
|
|
537
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
538
|
+
return v;
|
|
539
|
+
}
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
function nonEmptyString(v) {
|
|
543
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
544
|
+
}
|
|
545
|
+
/** Pull request headers out of agentCtx; several common locations are checked. */
|
|
316
546
|
function readHeaders(agentCtx) {
|
|
317
547
|
const ctxObj = asRecord(agentCtx);
|
|
318
548
|
if (!ctxObj)
|
|
@@ -328,7 +558,6 @@ function readHeaders(agentCtx) {
|
|
|
328
558
|
if (!candidate)
|
|
329
559
|
continue;
|
|
330
560
|
for (const [k, v] of Object.entries(candidate)) {
|
|
331
|
-
// HTTP headers should be case-insensitive; normalize to lowercase.
|
|
332
561
|
const lk = k.toLowerCase();
|
|
333
562
|
if (typeof v === "string" && v.length > 0 && out[lk] === undefined) {
|
|
334
563
|
out[lk] = v;
|
|
@@ -347,61 +576,32 @@ function readSessionKey(ctx, event) {
|
|
|
347
576
|
nonEmptyString(asRecord(event)?.sessionId));
|
|
348
577
|
}
|
|
349
578
|
function readRunId(ctx, event) {
|
|
350
|
-
return
|
|
351
|
-
nonEmptyString(asRecord(event)?.runId));
|
|
579
|
+
return nonEmptyString(asRecord(ctx)?.runId) ?? nonEmptyString(asRecord(event)?.runId);
|
|
352
580
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// back of injected context and the real user prompt gets buried.
|
|
361
|
-
return {
|
|
362
|
-
system: sanitizePayloadValue(ev.systemPrompt ?? ev.system),
|
|
363
|
-
prompt: sanitizePayloadValue(ev.prompt),
|
|
364
|
-
messages: sanitizePayloadValue(ev.historyMessages ?? ev.messages),
|
|
365
|
-
model: ev.model,
|
|
366
|
-
provider: ev.provider,
|
|
367
|
-
imagesCount: ev.imagesCount,
|
|
368
|
-
temperature: ev.temperature,
|
|
369
|
-
maxTokens: ev.maxTokens,
|
|
370
|
-
tools: ev.tools,
|
|
371
|
-
};
|
|
581
|
+
// ─── gen_ai content builders ──────────────────────────────────────────────────
|
|
582
|
+
/** System instructions as `[{type:"text", content}]` per the gen_ai shape. */
|
|
583
|
+
function buildSystemInstructions(ev) {
|
|
584
|
+
const system = ev.systemPrompt ?? ev.system;
|
|
585
|
+
if (typeof system !== "string" || system.length === 0)
|
|
586
|
+
return undefined;
|
|
587
|
+
return [{ type: "text", content: system }];
|
|
372
588
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
output: {
|
|
380
|
-
assistantTexts: sanitizePayloadValue(ev.assistantTexts),
|
|
381
|
-
lastAssistant: sanitizePayloadValue(ev.lastAssistant),
|
|
382
|
-
content: sanitizePayloadValue(ev.content),
|
|
383
|
-
finishReason: ev.finishReason ?? ev.stopReason,
|
|
384
|
-
},
|
|
385
|
-
model: nonEmptyString(ev.model) ?? null,
|
|
386
|
-
// Token field naming varies by provider and by how OpenClaw shapes the
|
|
387
|
-
// usage object. OpenClaw normalizes most hook-path traffic to `input` /
|
|
388
|
-
// `output` (see normalizeUsage in session-cost-usage-*.js), but raw
|
|
389
|
-
// SDK shapes leak through on edge paths and direct-SDK callers. We try
|
|
390
|
-
// every known spelling so the count is captured regardless:
|
|
391
|
-
// Anthropic SDK → input_tokens / output_tokens
|
|
392
|
-
// OpenAI SDK → prompt_tokens / completion_tokens (snake_case is canonical)
|
|
393
|
-
// Wrapper layers → promptTokens / completionTokens (some abstractions)
|
|
394
|
-
// OpenClaw / Gemini → input / output (normalized)
|
|
395
|
-
tokensIn: firstNumber(usage?.input_tokens, usage?.prompt_tokens, usage?.promptTokens, usage?.input),
|
|
396
|
-
tokensOut: firstNumber(usage?.output_tokens, usage?.completion_tokens, usage?.completionTokens, usage?.output),
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
function firstNumber(...values) {
|
|
400
|
-
for (const v of values) {
|
|
401
|
-
if (typeof v === "number" && Number.isFinite(v))
|
|
402
|
-
return v;
|
|
589
|
+
/** Input messages: the history (or messages), with any standalone `prompt` as a user turn. */
|
|
590
|
+
function buildInputMessages(ev) {
|
|
591
|
+
const history = ev.historyMessages ?? ev.messages;
|
|
592
|
+
const messages = Array.isArray(history) ? [...history] : history ? [history] : [];
|
|
593
|
+
if (typeof ev.prompt === "string" && ev.prompt.length > 0) {
|
|
594
|
+
messages.push({ role: "user", content: ev.prompt });
|
|
403
595
|
}
|
|
404
|
-
return
|
|
596
|
+
return normalizeGenAiMessages(messages, "user");
|
|
597
|
+
}
|
|
598
|
+
/** Output messages: the assistant text(s) / content, with the finish reason on the message. */
|
|
599
|
+
function buildOutputMessages(ev) {
|
|
600
|
+
const content = ev.assistantTexts ?? ev.lastAssistant ?? ev.content;
|
|
601
|
+
if (content === undefined || content === null)
|
|
602
|
+
return undefined;
|
|
603
|
+
const finish = ev.finishReason ?? ev.stopReason;
|
|
604
|
+
return normalizeGenAiMessages([{ role: "assistant", content, ...(typeof finish === "string" ? { finish_reason: finish } : {}) }], "assistant");
|
|
405
605
|
}
|
|
406
606
|
function errMsg(err) {
|
|
407
607
|
return err instanceof Error ? err.message : String(err);
|