@oh-my-pi/pi-agent-core 17.0.8 → 17.1.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/CHANGELOG.md +16 -0
- package/dist/types/compaction/compaction.d.ts +1 -1
- package/dist/types/telemetry.d.ts +19 -1
- package/dist/types/types.d.ts +11 -0
- package/package.json +7 -7
- package/src/agent-loop.ts +33 -20
- package/src/agent.ts +16 -0
- package/src/compaction/compaction.ts +26 -22
- package/src/compaction/utils.ts +12 -1
- package/src/telemetry.ts +46 -5
- package/src/types.ts +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.1.0] - 2026-07-24
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added support for tracking Cloudflare AI Gateway cache status (hit, miss, bypass, unknown) on chat spans.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Improved tool execution steering behavior: queued steering now cooperatively signals long-running, non-interruptible tools (via ToolCallContext.steeringSignal) to allow graceful early termination or backgrounding, rather than hard-aborting them.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Fixed an out-of-memory (OOM) crash caused by an infinite loop when a steer or follow-up message was queued on an agent session with an empty transcript.
|
|
18
|
+
- Fixed an issue where switching providers or models on a session could lose compacted history; the agent now correctly falls back to a portable local summary if the new model cannot replay the prior provider's remote-compaction payload.
|
|
19
|
+
- Fixed a compaction failure with Anthropic models where serializing prior assistant reasoning inside <thinking> tags triggered reasoning_extraction refusals.
|
|
20
|
+
|
|
5
21
|
## [17.0.8] - 2026-07-22
|
|
6
22
|
|
|
7
23
|
### Fixed
|
|
@@ -280,7 +280,7 @@ export interface CompactionPreparation {
|
|
|
280
280
|
/** Compaction settions from settings.jsonl */
|
|
281
281
|
settings: CompactionSettings;
|
|
282
282
|
}
|
|
283
|
-
export declare function prepareCompaction(pathEntries: SessionEntry[], settings: CompactionSettings,
|
|
283
|
+
export declare function prepareCompaction(pathEntries: SessionEntry[], settings: CompactionSettings, activeModel?: Model): CompactionPreparation | undefined;
|
|
284
284
|
/**
|
|
285
285
|
* Generate summaries for compaction using prepared data.
|
|
286
286
|
* Returns CompactionResult - SessionManager adds id/parentId when saving.
|
|
@@ -104,7 +104,9 @@ export declare const enum PiGenAIAttr {
|
|
|
104
104
|
GatewayName = "pi.gen_ai.gateway.name",
|
|
105
105
|
GatewayEndpoint = "pi.gen_ai.gateway.endpoint",
|
|
106
106
|
GatewayCallId = "pi.gen_ai.gateway.call_id",
|
|
107
|
-
GatewayRoutedTo = "pi.gen_ai.gateway.routed_to"
|
|
107
|
+
GatewayRoutedTo = "pi.gen_ai.gateway.routed_to",
|
|
108
|
+
/** Cloudflare AI Gateway response-cache status (`cf-aig-cache-status`), never prompt-cache. */
|
|
109
|
+
GatewayResponseCacheStatus = "pi.gen_ai.gateway.response_cache.status"
|
|
108
110
|
}
|
|
109
111
|
/** GenAI operation names — values for {@link GenAIAttr.OperationName}. */
|
|
110
112
|
export declare const GenAIOperation: {
|
|
@@ -413,6 +415,22 @@ export interface GatewayHeaderDetection {
|
|
|
413
415
|
* {@link ProviderResponseMetadata.headers} produces.
|
|
414
416
|
*/
|
|
415
417
|
export declare function detectGatewayFromHeaders(headers: Readonly<Record<string, string>> | undefined): GatewayHeaderDetection | undefined;
|
|
418
|
+
/**
|
|
419
|
+
* Bounded Cloudflare AI Gateway response-cache statuses emitted on
|
|
420
|
+
* {@link PiGenAIAttr.GatewayResponseCacheStatus}. Distinct from provider
|
|
421
|
+
* prompt-cache token counters (`gen_ai.usage.cache_*`).
|
|
422
|
+
*
|
|
423
|
+
* Cloudflare documents `HIT` / `MISS` on `cf-aig-cache-status`; `bypass` covers
|
|
424
|
+
* skip-cache / CDN-aligned BYPASS values. Any other present value is `unknown`.
|
|
425
|
+
*/
|
|
426
|
+
export type GatewayResponseCacheStatus = "hit" | "miss" | "bypass" | "unknown";
|
|
427
|
+
/**
|
|
428
|
+
* Classify Cloudflare AI Gateway `cf-aig-cache-status` into a bounded
|
|
429
|
+
* response-cache status. Returns `undefined` when the allow-listed header is
|
|
430
|
+
* absent so non-Cloudflare traffic stays unaffected. Does not read TTL,
|
|
431
|
+
* skip-cache, custom-key, or other Cloudflare headers.
|
|
432
|
+
*/
|
|
433
|
+
export declare function classifyGatewayResponseCacheStatus(headers: Readonly<Record<string, string>> | undefined): GatewayResponseCacheStatus | undefined;
|
|
416
434
|
export interface ManualChatToolCallTelemetry {
|
|
417
435
|
readonly toolCallId: string;
|
|
418
436
|
readonly toolName: string;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -399,6 +399,15 @@ export interface ToolCallContext {
|
|
|
399
399
|
id: string;
|
|
400
400
|
name: string;
|
|
401
401
|
}>;
|
|
402
|
+
/**
|
|
403
|
+
* Cooperative steering signal: aborted when a queued user/steering message
|
|
404
|
+
* (or an interrupting peer IRC) is detected while this tool batch runs.
|
|
405
|
+
* Unlike the hard abort signal it NEVER kills the tool — long-running
|
|
406
|
+
* tools MAY observe it (via `ctx.toolCall.steeringSignal`) to finish early
|
|
407
|
+
* or background themselves so the message injects promptly; ignoring it is
|
|
408
|
+
* always safe (the message injects at the next batch boundary).
|
|
409
|
+
*/
|
|
410
|
+
steeringSignal?: AbortSignal;
|
|
402
411
|
}
|
|
403
412
|
/** A single tool-call content block emitted by an assistant message. */
|
|
404
413
|
export type AgentToolCall = Extract<AssistantMessage["content"][number], {
|
|
@@ -533,6 +542,7 @@ export type ToolLoadMode = "essential" | "discoverable";
|
|
|
533
542
|
* - bare tier ("read" / "write" / "exec") — static classification.
|
|
534
543
|
* - object form — adds a `reason` (shown in the prompt) and/or `override: true`
|
|
535
544
|
* (force-prompt even in modes that would otherwise auto-approve this tier).
|
|
545
|
+
* `policy: "deny"` blocks the call at the approval gate.
|
|
536
546
|
* - function — dynamic, given parsed args. Returns either form above.
|
|
537
547
|
*
|
|
538
548
|
* Omitted approvals are treated as "exec" by callers that enforce approvals.
|
|
@@ -541,6 +551,7 @@ export type ToolApprovalDecision = ToolTier | {
|
|
|
541
551
|
tier: ToolTier;
|
|
542
552
|
reason?: string;
|
|
543
553
|
override?: boolean;
|
|
554
|
+
policy?: "allow" | "deny" | "prompt";
|
|
544
555
|
};
|
|
545
556
|
export type ToolApproval = ToolApprovalDecision | ((args: unknown) => ToolApprovalDecision);
|
|
546
557
|
/**
|
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": "17.0
|
|
4
|
+
"version": "17.1.0",
|
|
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,12 +35,12 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "17.0
|
|
39
|
-
"@oh-my-pi/pi-catalog": "17.0
|
|
40
|
-
"@oh-my-pi/pi-natives": "17.0
|
|
41
|
-
"@oh-my-pi/pi-utils": "17.0
|
|
42
|
-
"@oh-my-pi/pi-wire": "17.0
|
|
43
|
-
"@oh-my-pi/snapcompact": "17.0
|
|
38
|
+
"@oh-my-pi/pi-ai": "17.1.0",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "17.1.0",
|
|
40
|
+
"@oh-my-pi/pi-natives": "17.1.0",
|
|
41
|
+
"@oh-my-pi/pi-utils": "17.1.0",
|
|
42
|
+
"@oh-my-pi/pi-wire": "17.1.0",
|
|
43
|
+
"@oh-my-pi/snapcompact": "17.1.0",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -1817,13 +1817,18 @@ async function executeToolCalls(
|
|
|
1817
1817
|
const shouldInterruptImmediately = interruptMode !== "wait";
|
|
1818
1818
|
const steeringAbortController = new AbortController();
|
|
1819
1819
|
const ircAbortController = new AbortController();
|
|
1820
|
-
//
|
|
1821
|
-
//
|
|
1822
|
-
//
|
|
1823
|
-
//
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1820
|
+
// Cooperative channel: aborted when queued steering (or an interrupting
|
|
1821
|
+
// peer IRC) is detected mid-batch. Tools receive it via tool context
|
|
1822
|
+
// (`ctx.steeringSignal`) and MAY react — e.g. an auto-backgroundable bash
|
|
1823
|
+
// backgrounds itself so the message injects promptly — but it never kills
|
|
1824
|
+
// anything; ignoring it is always safe.
|
|
1825
|
+
const steeringSoftController = new AbortController();
|
|
1826
|
+
// Interruptible tools (pure waits: hub wait, vibe) observe steering +
|
|
1827
|
+
// external + IRC aborts. Every other tool sees ONLY the external signal:
|
|
1828
|
+
// neither queued steering nor a peer IRC ever hard-kills a partially
|
|
1829
|
+
// side-effecting foreground tool (e.g. `bash`) — those get the cooperative
|
|
1830
|
+
// steeringSignal above, and the message injects at the next boundary.
|
|
1831
|
+
const nonInterruptibleSignal: AbortSignal = signal ?? new AbortController().signal;
|
|
1827
1832
|
const interruptibleSignal: AbortSignal = signal
|
|
1828
1833
|
? AbortSignal.any([signal, steeringAbortController.signal, ircAbortController.signal])
|
|
1829
1834
|
: AbortSignal.any([steeringAbortController.signal, ircAbortController.signal]);
|
|
@@ -1889,13 +1894,16 @@ async function executeToolCalls(
|
|
|
1889
1894
|
}
|
|
1890
1895
|
}
|
|
1891
1896
|
if (steeringQueued) {
|
|
1892
|
-
// Queued steering
|
|
1893
|
-
//
|
|
1897
|
+
// Queued steering hard-aborts only interruptible waits and raises the
|
|
1898
|
+
// cooperative soft signal for everything else: the boundary dequeue
|
|
1899
|
+
// below injects the message as soon as running tools finish (or
|
|
1900
|
+
// background themselves), and not-yet-started tools are skipped.
|
|
1894
1901
|
// Idempotent — a second steer poll after the abort is a no-op.
|
|
1895
1902
|
if (!steeringAbortController.signal.aborted) {
|
|
1896
1903
|
interruptState.triggered = true;
|
|
1897
1904
|
interruptState.source = steeringSource ?? "unknown";
|
|
1898
1905
|
steeringAbortController.abort();
|
|
1906
|
+
steeringSoftController.abort();
|
|
1899
1907
|
}
|
|
1900
1908
|
return;
|
|
1901
1909
|
}
|
|
@@ -1903,11 +1911,13 @@ async function executeToolCalls(
|
|
|
1903
1911
|
// must not re-abort, and (unlike steering above) never re-consume a queue.
|
|
1904
1912
|
if (interruptState.triggered) return;
|
|
1905
1913
|
if (hasIrcInterrupts && (await hasIrcInterrupts())) {
|
|
1906
|
-
// Peer IRC
|
|
1907
|
-
//
|
|
1914
|
+
// Peer IRC hard-aborts interruptible waits only; foreground tools keep
|
|
1915
|
+
// running (no partial side effects) but get the cooperative soft
|
|
1916
|
+
// signal so backgroundable work can step aside for the peer message.
|
|
1908
1917
|
interruptState.triggered = true;
|
|
1909
1918
|
interruptState.source = "irc";
|
|
1910
1919
|
ircAbortController.abort();
|
|
1920
|
+
steeringSoftController.abort();
|
|
1911
1921
|
}
|
|
1912
1922
|
};
|
|
1913
1923
|
|
|
@@ -2090,12 +2100,17 @@ async function executeToolCalls(
|
|
|
2090
2100
|
: effectiveArgs;
|
|
2091
2101
|
record.args = executionArgs;
|
|
2092
2102
|
|
|
2103
|
+
// The cooperative steering signal rides the loop-owned
|
|
2104
|
+
// ToolCallContext (surfacing as `ctx.toolCall.steeringSignal`):
|
|
2105
|
+
// AgentToolContext itself is app-built via declaration merging, so
|
|
2106
|
+
// the loop cannot construct or extend one structurally.
|
|
2093
2107
|
const toolContext = getToolContext
|
|
2094
2108
|
? getToolContext({
|
|
2095
2109
|
batchId,
|
|
2096
2110
|
index,
|
|
2097
2111
|
total: toolCalls.length,
|
|
2098
2112
|
toolCalls: toolCallInfos,
|
|
2113
|
+
steeringSignal: steeringSoftController.signal,
|
|
2099
2114
|
})
|
|
2100
2115
|
: undefined;
|
|
2101
2116
|
const rawResult = await tool.execute(
|
|
@@ -2237,16 +2252,14 @@ async function executeToolCalls(
|
|
|
2237
2252
|
}
|
|
2238
2253
|
}
|
|
2239
2254
|
|
|
2240
|
-
// While
|
|
2241
|
-
//
|
|
2242
|
-
//
|
|
2243
|
-
//
|
|
2244
|
-
// promptly. Gated on immediate-interrupt
|
|
2245
|
-
// checkSteering is idempotent (no-op once triggered).
|
|
2255
|
+
// While tool calls are in flight, queued steering or interrupting IRC would
|
|
2256
|
+
// otherwise wait out the tools' own window. Poll only non-consuming queues:
|
|
2257
|
+
// detection hard-aborts interruptible waits, soft-signals cooperative tools
|
|
2258
|
+
// (auto-background bash), and skips not-yet-started tools, so the boundary
|
|
2259
|
+
// dequeue below injects the message promptly. Gated on immediate-interrupt
|
|
2260
|
+
// mode; checkSteering is idempotent (no-op once triggered).
|
|
2246
2261
|
const watchSteeringWhileRunning =
|
|
2247
|
-
shouldInterruptImmediately &&
|
|
2248
|
-
(hasSteeringMessages !== undefined || hasIrcInterrupts !== undefined) &&
|
|
2249
|
-
records.some(record => record.interruptible);
|
|
2262
|
+
shouldInterruptImmediately && (hasSteeringMessages !== undefined || hasIrcInterrupts !== undefined);
|
|
2250
2263
|
const steeringWatchTimer = watchSteeringWhileRunning
|
|
2251
2264
|
? setInterval(() => void checkSteering(), STEERING_INTERRUPT_POLL_MS)
|
|
2252
2265
|
: undefined;
|
package/src/agent.ts
CHANGED
|
@@ -1047,6 +1047,22 @@ export class Agent {
|
|
|
1047
1047
|
|
|
1048
1048
|
const messages = this.#state.messages;
|
|
1049
1049
|
if (messages.length === 0) {
|
|
1050
|
+
// An empty transcript has nothing to resume, but a queued steer/follow-up
|
|
1051
|
+
// must still be delivered as the opening turn — mirroring the assistant-tail
|
|
1052
|
+
// branch below. Throwing here leaves the message undeliverable, and idle-drain
|
|
1053
|
+
// callers (AgentSession#scheduleQueuedMessageDrain) re-arm continue() on every
|
|
1054
|
+
// microtask because hasQueuedMessages() never clears, spinning an unbounded
|
|
1055
|
+
// allocation loop until OOM (issue #6344).
|
|
1056
|
+
const queuedSteering = this.#dequeueSteeringMessages();
|
|
1057
|
+
if (queuedSteering.length > 0) {
|
|
1058
|
+
await this.#runLoop(queuedSteering, { skipInitialSteeringPoll: true });
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const queuedFollowUp = this.#dequeueFollowUpMessages();
|
|
1062
|
+
if (queuedFollowUp.length > 0) {
|
|
1063
|
+
await this.#runLoop(queuedFollowUp);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1050
1066
|
throw new Error("No messages to continue from");
|
|
1051
1067
|
}
|
|
1052
1068
|
if (messages[messages.length - 1].role === "assistant") {
|
|
@@ -1130,33 +1130,37 @@ export interface CompactionPreparation {
|
|
|
1130
1130
|
}
|
|
1131
1131
|
|
|
1132
1132
|
/**
|
|
1133
|
-
* Whether a prior compaction's
|
|
1134
|
-
*
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1133
|
+
* Whether a prior remote compaction's provider-native replay can still be read
|
|
1134
|
+
* by the active model — the model that assembles the request context on every
|
|
1135
|
+
* turn. A local compaction (no remote preserve) always can: it holds a real
|
|
1136
|
+
* textual summary. A remote compaction (V2 or V1) only can when the active model
|
|
1137
|
+
* shares the blob's provider AND remote replay is still enabled; otherwise the
|
|
1138
|
+
* active model's encoder drops the payload (see `getOpenAIResponsesHistoryPayload`)
|
|
1139
|
+
* and only the opaque placeholder summary survives, so the caller must re-expand
|
|
1140
|
+
* the originals into a portable local summary rather than strand that history.
|
|
1141
|
+
*
|
|
1142
|
+
* Judged against the ACTIVE model, not the compaction candidate set: a role
|
|
1143
|
+
* model (e.g. `modelRoles.smol`) that still maps to the blob's provider does not
|
|
1144
|
+
* let the active model replay it, so keying reuse on "any candidate shares the
|
|
1145
|
+
* provider" left a provider-switched session permanently context-less (#6343).
|
|
1139
1146
|
*/
|
|
1140
|
-
function
|
|
1147
|
+
function remotePreserveReusable(
|
|
1141
1148
|
preserveData: Record<string, unknown> | undefined,
|
|
1142
|
-
|
|
1149
|
+
activeModel: Model,
|
|
1143
1150
|
settings: CompactionSettings,
|
|
1144
1151
|
): boolean {
|
|
1145
1152
|
const remote = getCompactionV2PreserveData(preserveData) ?? getPreservedOpenAiRemoteCompactionData(preserveData);
|
|
1146
1153
|
if (!remote) return true;
|
|
1147
1154
|
if (settings.remoteEnabled === false) return false;
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
if (v2Ok || shouldUseOpenAiRemoteCompaction(model)) return true;
|
|
1152
|
-
}
|
|
1153
|
-
return false;
|
|
1155
|
+
if (remote.provider !== activeModel.provider) return false;
|
|
1156
|
+
const v2Ok = settings.remoteStreamingV2Enabled !== false && shouldUseCompactionV2Streaming(activeModel);
|
|
1157
|
+
return v2Ok || shouldUseOpenAiRemoteCompaction(activeModel);
|
|
1154
1158
|
}
|
|
1155
1159
|
|
|
1156
1160
|
export function prepareCompaction(
|
|
1157
1161
|
pathEntries: SessionEntry[],
|
|
1158
1162
|
settings: CompactionSettings,
|
|
1159
|
-
|
|
1163
|
+
activeModel?: Model,
|
|
1160
1164
|
): CompactionPreparation | undefined {
|
|
1161
1165
|
if (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === "compaction") {
|
|
1162
1166
|
return undefined;
|
|
@@ -1165,13 +1169,13 @@ export function prepareCompaction(
|
|
|
1165
1169
|
let prevCompactionIndex = -1;
|
|
1166
1170
|
for (let i = pathEntries.length - 1; i >= 0; i--) {
|
|
1167
1171
|
if (pathEntries[i].type !== "compaction") continue;
|
|
1168
|
-
// Skip a prior remote compaction (V2 or V1) whose provider-native replay
|
|
1169
|
-
//
|
|
1170
|
-
//
|
|
1171
|
-
//
|
|
1172
|
-
//
|
|
1172
|
+
// Skip a prior remote compaction (V2 or V1) whose provider-native replay the
|
|
1173
|
+
// active model cannot read: its summary is only an opaque placeholder, so
|
|
1174
|
+
// re-expand its original messages and summarize them locally rather than
|
|
1175
|
+
// stranding that history. compact() still reuses the payload when the active
|
|
1176
|
+
// model can replay it (same provider, remote enabled).
|
|
1173
1177
|
const entry = pathEntries[i] as CompactionEntry;
|
|
1174
|
-
if (
|
|
1178
|
+
if (activeModel && !remotePreserveReusable(entry.preserveData, activeModel, settings)) {
|
|
1175
1179
|
continue;
|
|
1176
1180
|
}
|
|
1177
1181
|
prevCompactionIndex = i;
|
|
@@ -1502,7 +1506,7 @@ export async function compact(
|
|
|
1502
1506
|
// summarization so a successful remote compaction never pays for a second,
|
|
1503
1507
|
// redundant LLM round. If a LATER compaction cannot reuse this payload,
|
|
1504
1508
|
// prepareCompaction re-expands the original messages and summarizes them
|
|
1505
|
-
// locally then (see
|
|
1509
|
+
// locally then (see remotePreserveReusable).
|
|
1506
1510
|
const usedTokens = getCompactionV2PreserveData(preserveData)?.usedTokens ?? 0;
|
|
1507
1511
|
summary =
|
|
1508
1512
|
"Remote compaction preserved provider-native history for this session." +
|
package/src/compaction/utils.ts
CHANGED
|
@@ -234,10 +234,21 @@ export function serializeConversation(messages: Message[], dialect?: Dialect): s
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
if (dialect) {
|
|
237
|
+
// Claude's classifier refuses inputs that reproduce the model's own
|
|
238
|
+
// reasoning as text ("reasoning_extraction"), and the anthropic dialect
|
|
239
|
+
// otherwise renders thinking verbatim inside <thinking> tags. Reasoning is
|
|
240
|
+
// ephemeral and low-signal for a summary, so drop it from Anthropic-target
|
|
241
|
+
// summary input. Other dialects (e.g. Harmony) carry reasoning natively in
|
|
242
|
+
// their transcript format and keep it.
|
|
243
|
+
const dropThinking = dialect === "anthropic";
|
|
237
244
|
const processed: Message[] = [];
|
|
238
245
|
for (const msg of messages) {
|
|
239
246
|
if (msg.role === "assistant") {
|
|
240
|
-
const content = msg.content.filter(
|
|
247
|
+
const content = msg.content.filter(
|
|
248
|
+
block =>
|
|
249
|
+
(block.type !== "toolCall" || !uselessCallIds.has(block.id)) &&
|
|
250
|
+
(!dropThinking || block.type !== "thinking"),
|
|
251
|
+
);
|
|
241
252
|
if (content.length > 0) processed.push(content.length === msg.content.length ? msg : { ...msg, content });
|
|
242
253
|
continue;
|
|
243
254
|
}
|
package/src/telemetry.ts
CHANGED
|
@@ -157,6 +157,8 @@ export const enum PiGenAIAttr {
|
|
|
157
157
|
GatewayEndpoint = "pi.gen_ai.gateway.endpoint",
|
|
158
158
|
GatewayCallId = "pi.gen_ai.gateway.call_id",
|
|
159
159
|
GatewayRoutedTo = "pi.gen_ai.gateway.routed_to",
|
|
160
|
+
/** Cloudflare AI Gateway response-cache status (`cf-aig-cache-status`), never prompt-cache. */
|
|
161
|
+
GatewayResponseCacheStatus = "pi.gen_ai.gateway.response_cache.status",
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
/** GenAI operation names — values for {@link GenAIAttr.OperationName}. */
|
|
@@ -1278,17 +1280,56 @@ export function detectGatewayFromHeaders(
|
|
|
1278
1280
|
return undefined;
|
|
1279
1281
|
}
|
|
1280
1282
|
|
|
1283
|
+
/**
|
|
1284
|
+
* Bounded Cloudflare AI Gateway response-cache statuses emitted on
|
|
1285
|
+
* {@link PiGenAIAttr.GatewayResponseCacheStatus}. Distinct from provider
|
|
1286
|
+
* prompt-cache token counters (`gen_ai.usage.cache_*`).
|
|
1287
|
+
*
|
|
1288
|
+
* Cloudflare documents `HIT` / `MISS` on `cf-aig-cache-status`; `bypass` covers
|
|
1289
|
+
* skip-cache / CDN-aligned BYPASS values. Any other present value is `unknown`.
|
|
1290
|
+
*/
|
|
1291
|
+
export type GatewayResponseCacheStatus = "hit" | "miss" | "bypass" | "unknown";
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* Classify Cloudflare AI Gateway `cf-aig-cache-status` into a bounded
|
|
1295
|
+
* response-cache status. Returns `undefined` when the allow-listed header is
|
|
1296
|
+
* absent so non-Cloudflare traffic stays unaffected. Does not read TTL,
|
|
1297
|
+
* skip-cache, custom-key, or other Cloudflare headers.
|
|
1298
|
+
*/
|
|
1299
|
+
export function classifyGatewayResponseCacheStatus(
|
|
1300
|
+
headers: Readonly<Record<string, string>> | undefined,
|
|
1301
|
+
): GatewayResponseCacheStatus | undefined {
|
|
1302
|
+
if (!headers) return undefined;
|
|
1303
|
+
const raw = headers["cf-aig-cache-status"];
|
|
1304
|
+
if (raw == null) return undefined;
|
|
1305
|
+
switch (raw.trim().toLowerCase()) {
|
|
1306
|
+
case "hit":
|
|
1307
|
+
return "hit";
|
|
1308
|
+
case "miss":
|
|
1309
|
+
return "miss";
|
|
1310
|
+
case "bypass":
|
|
1311
|
+
return "bypass";
|
|
1312
|
+
default:
|
|
1313
|
+
return "unknown";
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1281
1317
|
function applyGatewayAttributes(
|
|
1282
1318
|
span: Span,
|
|
1283
1319
|
headers: Readonly<Record<string, string>> | undefined,
|
|
1284
1320
|
baseUrl: string | undefined,
|
|
1285
1321
|
): void {
|
|
1286
1322
|
const gateway = detectGatewayFromHeaders(headers);
|
|
1287
|
-
if (
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1323
|
+
if (gateway) {
|
|
1324
|
+
span.setAttribute(PiGenAIAttr.GatewayName, gateway.name);
|
|
1325
|
+
if (baseUrl) span.setAttribute(PiGenAIAttr.GatewayEndpoint, baseUrl);
|
|
1326
|
+
if (gateway.callId) span.setAttribute(PiGenAIAttr.GatewayCallId, gateway.callId);
|
|
1327
|
+
if (gateway.routedTo) span.setAttribute(PiGenAIAttr.GatewayRoutedTo, gateway.routedTo);
|
|
1328
|
+
}
|
|
1329
|
+
const responseCacheStatus = classifyGatewayResponseCacheStatus(headers);
|
|
1330
|
+
if (responseCacheStatus) {
|
|
1331
|
+
span.setAttribute(PiGenAIAttr.GatewayResponseCacheStatus, responseCacheStatus);
|
|
1332
|
+
}
|
|
1292
1333
|
}
|
|
1293
1334
|
|
|
1294
1335
|
interface AppliedCostEstimate {
|
package/src/types.ts
CHANGED
|
@@ -458,6 +458,15 @@ export interface ToolCallContext {
|
|
|
458
458
|
index: number;
|
|
459
459
|
total: number;
|
|
460
460
|
toolCalls: Array<{ id: string; name: string }>;
|
|
461
|
+
/**
|
|
462
|
+
* Cooperative steering signal: aborted when a queued user/steering message
|
|
463
|
+
* (or an interrupting peer IRC) is detected while this tool batch runs.
|
|
464
|
+
* Unlike the hard abort signal it NEVER kills the tool — long-running
|
|
465
|
+
* tools MAY observe it (via `ctx.toolCall.steeringSignal`) to finish early
|
|
466
|
+
* or background themselves so the message injects promptly; ignoring it is
|
|
467
|
+
* always safe (the message injects at the next batch boundary).
|
|
468
|
+
*/
|
|
469
|
+
steeringSignal?: AbortSignal;
|
|
461
470
|
}
|
|
462
471
|
|
|
463
472
|
/** A single tool-call content block emitted by an assistant message. */
|
|
@@ -610,11 +619,14 @@ export type ToolLoadMode = "essential" | "discoverable";
|
|
|
610
619
|
* - bare tier ("read" / "write" / "exec") — static classification.
|
|
611
620
|
* - object form — adds a `reason` (shown in the prompt) and/or `override: true`
|
|
612
621
|
* (force-prompt even in modes that would otherwise auto-approve this tier).
|
|
622
|
+
* `policy: "deny"` blocks the call at the approval gate.
|
|
613
623
|
* - function — dynamic, given parsed args. Returns either form above.
|
|
614
624
|
*
|
|
615
625
|
* Omitted approvals are treated as "exec" by callers that enforce approvals.
|
|
616
626
|
*/
|
|
617
|
-
export type ToolApprovalDecision =
|
|
627
|
+
export type ToolApprovalDecision =
|
|
628
|
+
| ToolTier
|
|
629
|
+
| { tier: ToolTier; reason?: string; override?: boolean; policy?: "allow" | "deny" | "prompt" };
|
|
618
630
|
export type ToolApproval = ToolApprovalDecision | ((args: unknown) => ToolApprovalDecision);
|
|
619
631
|
|
|
620
632
|
/**
|