@linxiraos/pi-agent-core 1.1.27 → 1.2.0

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/src/telemetry.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * OpenTelemetry instrumentation for the agent loop.
3
3
  *
4
4
  * Implements the OpenTelemetry GenAI semantic conventions
5
- * (https://opentelemetry.io/docs/specs/semconv/gen-ai/) plus `omp.gen_ai.*`
5
+ * (https://opentelemetry.io/docs/specs/semconv/gen-ai/) plus `zeta.gen_ai.*`
6
6
  * extension attributes for run summaries, dashboard summaries, and cost hints
7
7
  * that are useful to downstream observability UIs.
8
8
  *
@@ -126,47 +126,47 @@ export const enum OpenAIAttr {
126
126
 
127
127
  /** Project extension attributes. Kept out of the reserved `gen_ai.*` namespace. */
128
128
  export const enum OmpGenAIAttr {
129
- AgentStepNumber = "omp.gen_ai.agent.step.number",
130
- AgentStepCount = "omp.gen_ai.agent.step.count",
131
- RequestReasoningEffort = "omp.gen_ai.request.reasoning.effort",
132
- RequestToolChoice = "omp.gen_ai.request.tool.choice",
133
- RequestAvailableTools = "omp.gen_ai.request.available_tools",
134
- RequestMessages = "omp.gen_ai.request.messages",
135
- ResponseText = "omp.gen_ai.response.text",
136
- ResponseToolCalls = "omp.gen_ai.response.tool_calls",
137
- ResponseUpstreamProvider = "omp.gen_ai.response.upstream_provider",
138
- UsageTotalTokens = "omp.gen_ai.usage.total_tokens",
139
- UsageServerSideTools = "omp.gen_ai.usage.server_tool_requests",
140
- CostEstimatedUsd = "omp.gen_ai.cost.estimated_usd",
141
- CostInputUsd = "omp.gen_ai.cost.input_usd",
142
- CostOutputUsd = "omp.gen_ai.cost.output_usd",
143
- CostUnavailableReason = "omp.gen_ai.cost.unavailable_reason",
144
- ToolStatus = "omp.gen_ai.tool.status",
145
- ToolCallIntent = "omp.gen_ai.tool.call.intent",
146
- HandoffFromAgentName = "omp.gen_ai.handoff.from_agent.name",
147
- HandoffFromAgentId = "omp.gen_ai.handoff.from_agent.id",
148
- HandoffToAgentName = "omp.gen_ai.handoff.to_agent.name",
149
- HandoffToAgentId = "omp.gen_ai.handoff.to_agent.id",
129
+ AgentStepNumber = "zeta.gen_ai.agent.step.number",
130
+ AgentStepCount = "zeta.gen_ai.agent.step.count",
131
+ RequestReasoningEffort = "zeta.gen_ai.request.reasoning.effort",
132
+ RequestToolChoice = "zeta.gen_ai.request.tool.choice",
133
+ RequestAvailableTools = "zeta.gen_ai.request.available_tools",
134
+ RequestMessages = "zeta.gen_ai.request.messages",
135
+ ResponseText = "zeta.gen_ai.response.text",
136
+ ResponseToolCalls = "zeta.gen_ai.response.tool_calls",
137
+ ResponseUpstreamProvider = "zeta.gen_ai.response.upstream_provider",
138
+ UsageTotalTokens = "zeta.gen_ai.usage.total_tokens",
139
+ UsageServerSideTools = "zeta.gen_ai.usage.server_tool_requests",
140
+ CostEstimatedUsd = "zeta.gen_ai.cost.estimated_usd",
141
+ CostInputUsd = "zeta.gen_ai.cost.input_usd",
142
+ CostOutputUsd = "zeta.gen_ai.cost.output_usd",
143
+ CostUnavailableReason = "zeta.gen_ai.cost.unavailable_reason",
144
+ ToolStatus = "zeta.gen_ai.tool.status",
145
+ ToolCallIntent = "zeta.gen_ai.tool.call.intent",
146
+ HandoffFromAgentName = "zeta.gen_ai.handoff.from_agent.name",
147
+ HandoffFromAgentId = "zeta.gen_ai.handoff.from_agent.id",
148
+ HandoffToAgentName = "zeta.gen_ai.handoff.to_agent.name",
149
+ HandoffToAgentId = "zeta.gen_ai.handoff.to_agent.id",
150
150
  // Marks chat spans emitted outside the agent loop (compaction, handoff, branch
151
151
  // summary, image inspection, …). Lets dashboards split oneshot cost / latency
152
152
  // from main-turn cost without overloading the semconv `gen_ai.operation.name`.
153
- OneshotKind = "omp.gen_ai.oneshot.kind",
153
+ OneshotKind = "zeta.gen_ai.oneshot.kind",
154
154
  // Gateway / proxy (LiteLLM, Helicone, Portkey, …) — populated when a known
155
155
  // gateway header pattern is detected on the upstream response. The base
156
156
  // `gen_ai.provider.name` continues to track the *upstream* provider (e.g.
157
157
  // `anthropic`) that the gateway routed to.
158
- GatewayName = "omp.gen_ai.gateway.name",
159
- GatewayEndpoint = "omp.gen_ai.gateway.endpoint",
160
- GatewayCallId = "omp.gen_ai.gateway.call_id",
161
- GatewayRoutedTo = "omp.gen_ai.gateway.routed_to",
158
+ GatewayName = "zeta.gen_ai.gateway.name",
159
+ GatewayEndpoint = "zeta.gen_ai.gateway.endpoint",
160
+ GatewayCallId = "zeta.gen_ai.gateway.call_id",
161
+ GatewayRoutedTo = "zeta.gen_ai.gateway.routed_to",
162
162
  /** Cloudflare AI Gateway response-cache status (`cf-aig-cache-status`), never prompt-cache. */
163
- GatewayResponseCacheStatus = "omp.gen_ai.gateway.response_cache.status",
163
+ GatewayResponseCacheStatus = "zeta.gen_ai.gateway.response_cache.status",
164
164
  /** Caller-level reason a judgment ran (`find`, `ttsr`, `judge_batch`, …). */
165
- JudgmentPurpose = "omp.gen_ai.judgment.purpose",
165
+ JudgmentPurpose = "zeta.gen_ai.judgment.purpose",
166
166
  /** Questions the caller asked in one judgment request. */
167
- JudgmentQuestions = "omp.gen_ai.judgment.questions",
167
+ JudgmentQuestions = "zeta.gen_ai.judgment.questions",
168
168
  /** Questions answered from the local judgment cache instead of the provider. */
169
- JudgmentCachedQuestions = "omp.gen_ai.judgment.cached_questions",
169
+ JudgmentCachedQuestions = "zeta.gen_ai.judgment.cached_questions",
170
170
  }
171
171
 
172
172
  /** GenAI operation names — values for {@link GenAIAttr.OperationName}. */
@@ -220,9 +220,9 @@ export interface CostEstimatorContext {
220
220
 
221
221
  /**
222
222
  * Cost estimator result.
223
- * { usd: number } — cost is known; emitted as omp.gen_ai.cost.estimated_usd
223
+ * { usd: number } — cost is known; emitted as zeta.gen_ai.cost.estimated_usd
224
224
  * { unavailable: string } — cost is intentionally unknown; emitted as
225
- * omp.gen_ai.cost.unavailable_reason
225
+ * zeta.gen_ai.cost.unavailable_reason
226
226
  * undefined — no opinion; nothing emitted
227
227
  */
228
228
  export type CostEstimate =
@@ -271,7 +271,7 @@ export interface ChatUsageEvent {
271
271
  *
272
272
  * Use this to reconcile gateway-issued ids (e.g. `x-litellm-call-id`) with
273
273
  * downstream billing / spend dashboards. Known gateway patterns are also
274
- * auto-stamped on the chat span as `omp.gen_ai.gateway.*` attributes.
274
+ * auto-stamped on the chat span as `zeta.gen_ai.gateway.*` attributes.
275
275
  */
276
276
  readonly headers: Readonly<Record<string, string>> | undefined;
277
277
  }
@@ -836,15 +836,27 @@ function serializeRequestMessagesForTelemetry(
836
836
  ): string | undefined {
837
837
  const serializer = telemetry.config.contentSerializer?.requestMessages;
838
838
  if (serializer) return callContentSerializer(telemetry, "requestMessages", () => serializer(request));
839
+ const systemParts = normalizeSystemPromptParts(request.systemPrompt);
840
+ const requestMessages = request.messages ?? [];
841
+ const total = systemParts.length + requestMessages.length;
842
+ if (total === 0) return undefined;
843
+ // Summarize only what survives the message cap; the rest is counted.
839
844
  const messages: TelemetryMessageSummary[] = [];
840
- for (const text of normalizeSystemPromptParts(request.systemPrompt))
845
+ for (const text of systemParts) {
846
+ if (messages.length === MAX_TELEMETRY_MESSAGE_COUNT) break;
841
847
  messages.push({ role: "system", content: summarizeTelemetryValue(text) });
842
- if (request.messages) {
843
- for (const message of request.messages) {
844
- messages.push({ role: message.role, content: summarizeTelemetryValue(message.content) });
845
- }
846
848
  }
847
- return messages.length === 0 ? undefined : stringifyJsonAttribute(limitTelemetryMessages(messages));
849
+ for (const message of requestMessages) {
850
+ if (messages.length === MAX_TELEMETRY_MESSAGE_COUNT) break;
851
+ messages.push({ role: message.role, content: summarizeTelemetryValue(message.content) });
852
+ }
853
+ if (total > MAX_TELEMETRY_MESSAGE_COUNT) {
854
+ messages.push({
855
+ role: "system",
856
+ content: { kind: "truncated", omittedMessages: total - MAX_TELEMETRY_MESSAGE_COUNT },
857
+ });
858
+ }
859
+ return stringifyJsonAttribute(messages);
848
860
  }
849
861
 
850
862
  function serializeResponseTextForTelemetry(telemetry: AgentTelemetry, message: AssistantMessage): string | undefined {
@@ -1022,17 +1034,6 @@ function callContentSerializer(
1022
1034
  }
1023
1035
  }
1024
1036
 
1025
- function limitTelemetryMessages(messages: readonly TelemetryMessageSummary[]): TelemetryMessageSummary[] {
1026
- const limited = messages.slice(0, MAX_TELEMETRY_MESSAGE_COUNT);
1027
- if (messages.length > MAX_TELEMETRY_MESSAGE_COUNT) {
1028
- limited.push({
1029
- role: "system",
1030
- content: { kind: "truncated", omittedMessages: messages.length - MAX_TELEMETRY_MESSAGE_COUNT },
1031
- });
1032
- }
1033
- return limited;
1034
- }
1035
-
1036
1037
  function limitTelemetryToolCalls(toolCalls: readonly TelemetryToolCallSummary[]): TelemetryToolCallSummary[] {
1037
1038
  const limited = toolCalls.slice(0, MAX_TELEMETRY_ARRAY_ITEMS);
1038
1039
  if (toolCalls.length > MAX_TELEMETRY_ARRAY_ITEMS) {
@@ -1788,10 +1789,10 @@ export interface InstrumentedChatSpanOptions {
1788
1789
  /** Step index recorded on the span; defaults to `-1` for non-loop calls. */
1789
1790
  readonly stepNumber?: number;
1790
1791
  /**
1791
- * Tag stamped onto `omp.gen_ai.oneshot.kind`. Values used by the agent:
1792
+ * Tag stamped onto `zeta.gen_ai.oneshot.kind`. Values used by the agent:
1792
1793
  * `compaction_summary`, `compaction_short_summary`, `compaction_turn_prefix`,
1793
- * `handoff`, `branch_summary`, `image_question`. Free-form to allow callers
1794
- * outside this package to add new kinds without bumping the helper.
1794
+ * `handoff`, `branch_summary`, `image_question`, `skill_description`. Free-form
1795
+ * to allow callers outside this package to add new kinds without bumping the helper.
1795
1796
  */
1796
1797
  readonly oneshotKind?: string;
1797
1798
  /** Extra span attributes applied verbatim. */
@@ -2116,33 +2117,33 @@ export function fireOnRunEnd(telemetry: AgentTelemetry, summary: AgentRunSummary
2116
2117
  }
2117
2118
  }
2118
2119
 
2119
- /** Aggregate `omp.gen_ai.agent.*` attributes stamped on the `invoke_agent` span. */
2120
+ /** Aggregate `zeta.gen_ai.agent.*` attributes stamped on the `invoke_agent` span. */
2120
2121
  export const enum OmpGenAIAggregateAttr {
2121
- ChatsCount = "omp.gen_ai.agent.chats.count",
2122
- ChatsTotalLatencyMs = "omp.gen_ai.agent.chats.total_latency_ms",
2123
- ChatsStopReasonPrefix = "omp.gen_ai.agent.chats.stop_reason.",
2124
- ToolsCount = "omp.gen_ai.agent.tools.count",
2125
- ToolsOkCount = "omp.gen_ai.agent.tools.ok.count",
2126
- ToolsErrorCount = "omp.gen_ai.agent.tools.error.count",
2127
- ToolsSkippedCount = "omp.gen_ai.agent.tools.skipped.count",
2128
- ToolsBlockedCount = "omp.gen_ai.agent.tools.blocked.count",
2129
- ToolsTimeoutCount = "omp.gen_ai.agent.tools.timeout.count",
2130
- ToolsAbortedCount = "omp.gen_ai.agent.tools.aborted.count",
2131
- ToolsTotalLatencyMs = "omp.gen_ai.agent.tools.total_latency_ms",
2132
- ToolsInvoked = "omp.gen_ai.agent.tools.invoked",
2133
- ToolsAvailable = "omp.gen_ai.agent.tools.available",
2134
- ToolsUnused = "omp.gen_ai.agent.tools.unused",
2135
- UsageInputTokensTotal = "omp.gen_ai.agent.usage.input_tokens.total",
2136
- UsageOutputTokensTotal = "omp.gen_ai.agent.usage.output_tokens.total",
2137
- UsageCacheReadInputTokensTotal = "omp.gen_ai.agent.usage.cache_read.input_tokens.total",
2138
- UsageCacheCreationInputTokensTotal = "omp.gen_ai.agent.usage.cache_creation.input_tokens.total",
2139
- UsageReasoningOutputTokensTotal = "omp.gen_ai.agent.usage.reasoning.output_tokens.total",
2140
- UsageTotalTokensTotal = "omp.gen_ai.agent.usage.total_tokens.total",
2141
- CostEstimatedUsdTotal = "omp.gen_ai.agent.cost.estimated_usd.total",
2142
- ErrorsCount = "omp.gen_ai.agent.errors.count",
2143
- }
2144
-
2145
- /** Stamp the aggregate `omp.gen_ai.agent.*` attributes on the given span. */
2122
+ ChatsCount = "zeta.gen_ai.agent.chats.count",
2123
+ ChatsTotalLatencyMs = "zeta.gen_ai.agent.chats.total_latency_ms",
2124
+ ChatsStopReasonPrefix = "zeta.gen_ai.agent.chats.stop_reason.",
2125
+ ToolsCount = "zeta.gen_ai.agent.tools.count",
2126
+ ToolsOkCount = "zeta.gen_ai.agent.tools.ok.count",
2127
+ ToolsErrorCount = "zeta.gen_ai.agent.tools.error.count",
2128
+ ToolsSkippedCount = "zeta.gen_ai.agent.tools.skipped.count",
2129
+ ToolsBlockedCount = "zeta.gen_ai.agent.tools.blocked.count",
2130
+ ToolsTimeoutCount = "zeta.gen_ai.agent.tools.timeout.count",
2131
+ ToolsAbortedCount = "zeta.gen_ai.agent.tools.aborted.count",
2132
+ ToolsTotalLatencyMs = "zeta.gen_ai.agent.tools.total_latency_ms",
2133
+ ToolsInvoked = "zeta.gen_ai.agent.tools.invoked",
2134
+ ToolsAvailable = "zeta.gen_ai.agent.tools.available",
2135
+ ToolsUnused = "zeta.gen_ai.agent.tools.unused",
2136
+ UsageInputTokensTotal = "zeta.gen_ai.agent.usage.input_tokens.total",
2137
+ UsageOutputTokensTotal = "zeta.gen_ai.agent.usage.output_tokens.total",
2138
+ UsageCacheReadInputTokensTotal = "zeta.gen_ai.agent.usage.cache_read.input_tokens.total",
2139
+ UsageCacheCreationInputTokensTotal = "zeta.gen_ai.agent.usage.cache_creation.input_tokens.total",
2140
+ UsageReasoningOutputTokensTotal = "zeta.gen_ai.agent.usage.reasoning.output_tokens.total",
2141
+ UsageTotalTokensTotal = "zeta.gen_ai.agent.usage.total_tokens.total",
2142
+ CostEstimatedUsdTotal = "zeta.gen_ai.agent.cost.estimated_usd.total",
2143
+ ErrorsCount = "zeta.gen_ai.agent.errors.count",
2144
+ }
2145
+
2146
+ /** Stamp the aggregate `zeta.gen_ai.agent.*` attributes on the given span. */
2146
2147
  function applyAggregateAttributes(span: Span, summary: AgentRunSummary, coverage: AgentRunCoverage): void {
2147
2148
  span.setAttribute(OmpGenAIAggregateAttr.ChatsCount, summary.chats.total);
2148
2149
  span.setAttribute(OmpGenAIAggregateAttr.ChatsTotalLatencyMs, summary.chats.totalLatencyMs);
@@ -0,0 +1,38 @@
1
+ import { type ToolCall, validateToolArguments } from "@linxiraos/pi-ai";
2
+ import type { AgentTool } from "./types";
3
+
4
+ /**
5
+ * Where a call's `__parseError`/`__rawJson` keys came from.
6
+ * - `"parsed"`: a provider stream parsed the arguments, so `__parseError` is a
7
+ * genuine parse-failure sentinel and is never handed to a tool.
8
+ * - `"payload"`: the arguments are a payload the model wrote (eval tool bridge,
9
+ * `xd://` device content), so those keys are forged and only stripped.
10
+ */
11
+ export type ToolArgumentSource = "parsed" | "payload";
12
+
13
+ /**
14
+ * Validate a call's arguments against `tool`'s schema, honoring
15
+ * {@link AgentTool.lenientArgValidation}: on a schema mismatch a lenient tool
16
+ * receives its raw arguments minus the internal `__parseError`/`__rawJson`
17
+ * sentinels. Malformed JSON from a provider stream is never lenient.
18
+ *
19
+ * @throws the validation error when the tool is strict or the arguments failed to parse.
20
+ */
21
+ export function validateAgentToolArguments(
22
+ tool: AgentTool,
23
+ toolCall: ToolCall,
24
+ source: ToolArgumentSource = "parsed",
25
+ ): ToolCall["arguments"] {
26
+ try {
27
+ return validateToolArguments(tool, toolCall);
28
+ } catch (error) {
29
+ const args: unknown = toolCall.arguments;
30
+ if (!tool.lenientArgValidation) throw error;
31
+ if (args === null || typeof args !== "object" || Array.isArray(args)) return args as ToolCall["arguments"];
32
+ if (source === "parsed" && "__parseError" in args) throw error;
33
+ const fallback = { ...(args as Record<string, unknown>) };
34
+ delete fallback.__parseError;
35
+ delete fallback.__rawJson;
36
+ return fallback;
37
+ }
38
+ }
package/src/types.ts CHANGED
@@ -328,6 +328,16 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
328
328
  * background.
329
329
  */
330
330
  hasBackgroundCompletions?: () => boolean | Promise<boolean>;
331
+ /**
332
+ * Peeks whether a passive aside (an extension or user message queued with
333
+ * `deliverAs: "aside"`) is waiting for injection at the next boundary.
334
+ *
335
+ * Same rules as {@link hasBackgroundCompletions}: non-consuming, ends only
336
+ * *interruptible* waits, never raises {@link ToolCallContext.steeringSignal}.
337
+ * The host still injects the aside once at the boundary; ordinary running
338
+ * tools finish first.
339
+ */
340
+ hasQueuedAsides?: () => boolean | Promise<boolean>;
331
341
 
332
342
  /**
333
343
  * Returns follow-up messages to process after the agent would otherwise stop.
@@ -341,7 +351,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
341
351
  * Returns non-interrupting "aside" messages to inject at a step boundary.
342
352
  *
343
353
  * Polled after each tool batch (before the next LLM call) AND at the yield
344
- * check. Unlike steering, these NEVER abort in-flight tools — they are passive
354
+ * check. Unlike steering, these never abort foreground tools (only an
355
+ * interruptible `wait` ends early, via the peek hooks) — they are passive
345
356
  * notifications (e.g. background-job completions, late LSP diagnostics) that
346
357
  * should reach the model between requests without waiting for the agent to
347
358
  * fully stop. Returned messages are appended to the context with normal