@bendyline/gezel-inference 1.0.4 → 1.0.5

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.
@@ -1,4 +1,5 @@
1
1
  import type { ExternalToolCall, ImageAttachment, LLMSession, TurnUsage } from '../../providers/types.js';
2
+ import type { ToolCallScreen } from './tool-call-screen.js';
2
3
  export type ResponsesToolKind = 'function' | 'custom';
3
4
  /**
4
5
  * Maps a provider-safe local tool name back to the identity advertised on
@@ -243,6 +244,12 @@ export interface ResponsesSerializerOptions {
243
244
  toolNamespaces?: Record<string, string>;
244
245
  echo?: ResponsesRequestEcho;
245
246
  limits?: ResponsesSerializerLimits;
247
+ /**
248
+ * Withholds calls that name an egress indicator; see tool-call-screen.ts.
249
+ * A withheld call that was already announced closes `incomplete` with no
250
+ * arguments, since Responses clients run a call from its `done` item.
251
+ */
252
+ toolCallScreen?: ToolCallScreen;
246
253
  }
247
254
  /**
248
255
  * Stateful Responses output serializer. Every returned event is an immutable
@@ -259,6 +266,7 @@ export declare class ResponsesStreamSerializer {
259
266
  private readonly toolNamespaces;
260
267
  private readonly echo;
261
268
  private readonly limits;
269
+ private readonly toolCallScreen;
262
270
  private started;
263
271
  private terminal;
264
272
  private sequenceNumber;
@@ -305,6 +313,11 @@ export declare class ResponsesStreamSerializer {
305
313
  private finalizeActive;
306
314
  private finalizeMessage;
307
315
  private finalizeTool;
316
+ /**
317
+ * Close a screened call so no client can run it. One never announced stays
318
+ * out of the output; an announced one ends `incomplete` with no arguments.
319
+ */
320
+ private withholdTool;
308
321
  private reconcileToolCalls;
309
322
  private reconcileFinalText;
310
323
  private markActiveIncomplete;
@@ -64,6 +64,7 @@ export class ResponsesStreamSerializer {
64
64
  toolNamespaces;
65
65
  echo;
66
66
  limits;
67
+ toolCallScreen;
67
68
  started = false;
68
69
  terminal = false;
69
70
  sequenceNumber = 0;
@@ -88,6 +89,7 @@ export class ResponsesStreamSerializer {
88
89
  this.toolNamespaces = options.toolNamespaces ?? {};
89
90
  this.echo = options.echo ?? {};
90
91
  this.limits = { ...DEFAULT_LIMITS, ...options.limits };
92
+ this.toolCallScreen = options.toolCallScreen;
91
93
  }
92
94
  get isTerminal() {
93
95
  return this.terminal;
@@ -162,6 +164,7 @@ export class ResponsesStreamSerializer {
162
164
  const calls = this.echo.parallelToolCalls === false ? toolCalls.slice(0, 1) : toolCalls;
163
165
  events.push(...this.reconcileToolCalls(calls));
164
166
  events.push(...this.reconcileFinalText(text));
167
+ events.push(...this.pushTextDelta(this.toolCallScreen?.notice() ?? ''));
165
168
  events.push(...this.finalizeActive());
166
169
  this.terminal = true;
167
170
  this.completedAt = this.nowSeconds();
@@ -438,6 +441,8 @@ export class ResponsesStreamSerializer {
438
441
  const events = [];
439
442
  const finalRaw = finalArguments ?? state.rawArguments;
440
443
  this.assertToolCapacity(finalRaw.length);
444
+ if (this.toolCallScreen?.blocks(state.localName, finalRaw))
445
+ return this.withholdTool(state);
441
446
  if (!state.announced)
442
447
  events.push(...this.announceTool(state));
443
448
  if (!state.item || state.outputIndex === null)
@@ -503,6 +508,27 @@ export class ResponsesStreamSerializer {
503
508
  }));
504
509
  return events;
505
510
  }
511
+ /**
512
+ * Close a screened call so no client can run it. One never announced stays
513
+ * out of the output; an announced one ends `incomplete` with no arguments.
514
+ */
515
+ withholdTool(state) {
516
+ state.completed = true;
517
+ if (!state.announced || !state.item || state.outputIndex === null)
518
+ return [];
519
+ if (state.item.type === 'function_call')
520
+ state.item.arguments = '';
521
+ else
522
+ state.item.input = '';
523
+ state.item.status = 'incomplete';
524
+ return [
525
+ this.event({
526
+ type: 'response.output_item.done',
527
+ output_index: state.outputIndex,
528
+ item: cloneOutputItem(state.item),
529
+ }),
530
+ ];
531
+ }
506
532
  reconcileToolCalls(calls) {
507
533
  const events = [];
508
534
  const matched = new Set();
@@ -519,6 +545,9 @@ export class ResponsesStreamSerializer {
519
545
  }
520
546
  continue;
521
547
  }
548
+ // Never streamed, so a withheld call need not appear at all.
549
+ if (this.toolCallScreen?.blocks(call.name, call.arguments))
550
+ continue;
522
551
  if (this.active)
523
552
  events.push(...this.finalizeActive());
524
553
  events.push(...this.openTool(call.name, call.id));
@@ -626,6 +655,7 @@ function serializerOptions(model, nowSeconds, options) {
626
655
  ...(options.toolKinds ? { toolKinds: options.toolKinds } : {}),
627
656
  ...(options.toolNamespaces ? { toolNamespaces: options.toolNamespaces } : {}),
628
657
  ...(options.limits ? { limits: options.limits } : {}),
658
+ ...(options.toolCallScreen ? { toolCallScreen: options.toolCallScreen } : {}),
629
659
  echo: {
630
660
  ...options.echo,
631
661
  ...(options.echo?.maxOutputTokens === undefined && options.lengthCapTokens !== undefined
@@ -1,5 +1,6 @@
1
1
  import type { ExternalToolCall, ImageAttachment, LLMSession, SendAndWaitOpts } from '../../providers/types.js';
2
2
  import { type StreamingDiagnostics } from './streaming-telemetry.js';
3
+ import { type ToolCallScreen } from './tool-call-screen.js';
3
4
  /**
4
5
  * One-shot, non-streaming chat completion. Subscribes to usage to fill
5
6
  * the OpenAI `usage` envelope and returns the full response in the
@@ -52,6 +53,8 @@ export interface RunNonStreamingOpts {
52
53
  sendOptions?: Omit<SendAndWaitOpts, 'attachments'>;
53
54
  /** Effective output-token cap — reaching it reports `finish_reason: 'length'`. */
54
55
  lengthCapTokens?: number;
56
+ /** Withholds calls that name an egress indicator; see tool-call-screen.ts. */
57
+ toolCallScreen?: ToolCallScreen;
55
58
  }
56
59
  export declare function runNonStreaming(session: LLMSession, prompt: string, echoModel: string, nowSeconds: () => number, opts?: RunNonStreamingOpts): Promise<NonStreamingChatResult>;
57
60
  /**
@@ -117,6 +120,12 @@ export interface RunStreamingOpts {
117
120
  streamToolCallDeltas?: boolean;
118
121
  /** Tool names supplied by the caller; gates early textual-call detection. */
119
122
  toolCallNames?: readonly string[];
123
+ /**
124
+ * Withholds calls that name an egress indicator; see tool-call-screen.ts.
125
+ * With live tool-call deltas on, each call's closing tail is held back
126
+ * until the call is screened, so the client cannot run it early.
127
+ */
128
+ toolCallScreen?: ToolCallScreen;
120
129
  }
121
130
  export interface RunStreamingResult {
122
131
  content: string;
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { createChatProgressReporter } from './chat-progress.js';
3
3
  import { noteStreamingOutbound, noteStreamingProviderActivity, } from './streaming-telemetry.js';
4
+ import { screenableArgumentsPrefix } from './tool-call-screen.js';
4
5
  import { ToolCallStreamFilter } from './tool-call-stream-filter.js';
5
6
  /**
6
7
  * Truncation detection. An engine that reports its own stop reason
@@ -111,7 +112,7 @@ function streamedJsonObjectEnd(body, start) {
111
112
  return body.length;
112
113
  }
113
114
  export async function runNonStreaming(session, prompt, echoModel, nowSeconds, opts = {}) {
114
- const { attachments, sendOptions, lengthCapTokens } = opts;
115
+ const { attachments, sendOptions, lengthCapTokens, toolCallScreen } = opts;
115
116
  // Wrapped so TypeScript's control-flow narrowing doesn't infer
116
117
  // `usage` as `never` after the post-callback null check — closures
117
118
  // that assign to a captured variable confuse the analyzer.
@@ -127,8 +128,10 @@ export async function runNonStreaming(session, prompt, echoModel, nowSeconds, op
127
128
  ...(attachments && attachments.length > 0 ? { attachments } : {}),
128
129
  }
129
130
  : undefined;
130
- const content = await session.sendAndWait(prompt, sendOpts);
131
- const captured = session.capturedToolCalls?.() ?? [];
131
+ const reply = await session.sendAndWait(prompt, sendOpts);
132
+ const rawCaptured = session.capturedToolCalls?.() ?? [];
133
+ const captured = toolCallScreen ? toolCallScreen.admit(rawCaptured) : rawCaptured;
134
+ const content = `${reply}${toolCallScreen?.notice() ?? ''}`;
132
135
  const hasToolCalls = captured.length > 0;
133
136
  return {
134
137
  id: `chatcmpl-${randomUUID()}`,
@@ -173,7 +176,7 @@ export async function runNonStreaming(session, prompt, echoModel, nowSeconds, op
173
176
  * token).
174
177
  */
175
178
  export async function runStreaming(session, prompt, echoModel, sink, nowSeconds, opts = {}) {
176
- const { attachments, sendOptions, includeReasoning = false, keepaliveIntervalMs = 0, diagnostics, suppressTextualToolCalls = false, includeUsage = false, includeProgress = false, lengthCapTokens, onContentDelta, onReasoningDelta, onToolArgsDelta, streamToolCallDeltas = false, toolCallNames = [], } = opts;
179
+ const { attachments, sendOptions, includeReasoning = false, keepaliveIntervalMs = 0, diagnostics, suppressTextualToolCalls = false, includeUsage = false, includeProgress = false, lengthCapTokens, onContentDelta, onReasoningDelta, onToolArgsDelta, streamToolCallDeltas = false, toolCallNames = [], toolCallScreen, } = opts;
177
180
  const id = `chatcmpl-${randomUUID()}`;
178
181
  if (diagnostics)
179
182
  diagnostics.responseId = id;
@@ -198,6 +201,7 @@ export async function runStreaming(session, prompt, echoModel, sink, nowSeconds,
198
201
  const usageRef = { value: null };
199
202
  let visibleContent = '';
200
203
  let reasoningContent = '';
204
+ const sendableArguments = (received) => toolCallScreen ? screenableArgumentsPrefix(received) : received;
201
205
  const liveToolCalls = [];
202
206
  let activeLiveToolCall;
203
207
  let unnamedToolArguments = '';
@@ -238,7 +242,8 @@ export async function runStreaming(session, prompt, echoModel, sink, nowSeconds,
238
242
  index: meta.index ?? nextLiveToolIndex(),
239
243
  id: meta.id ?? `call_${randomUUID()}`,
240
244
  name,
241
- arguments: argumentsChunk,
245
+ arguments: sendableArguments(argumentsChunk),
246
+ received: argumentsChunk,
242
247
  };
243
248
  liveToolCalls.push(call);
244
249
  activeLiveToolCall = call;
@@ -269,10 +274,15 @@ export async function runStreaming(session, prompt, echoModel, sink, nowSeconds,
269
274
  }, 'tool_calls');
270
275
  return call;
271
276
  };
272
- const appendLiveToolArguments = (call, chunk) => {
277
+ const appendLiveToolArguments = (call, received) => {
278
+ if (!received)
279
+ return;
280
+ call.received += received;
281
+ const next = sendableArguments(call.received);
282
+ const chunk = next.slice(call.arguments.length);
273
283
  if (!chunk)
274
284
  return;
275
- call.arguments += chunk;
285
+ call.arguments = next;
276
286
  enqueueSSE({
277
287
  data: JSON.stringify({
278
288
  id,
@@ -516,9 +526,27 @@ export async function runStreaming(session, prompt, echoModel, sink, nowSeconds,
516
526
  }),
517
527
  }, 'content');
518
528
  }
519
- const captured = session.capturedToolCalls?.() ?? [];
529
+ const rawCaptured = session.capturedToolCalls?.() ?? [];
520
530
  if (diagnostics)
521
- diagnostics.capturedToolCalls = captured.length;
531
+ diagnostics.capturedToolCalls = rawCaptured.length;
532
+ // A withheld call's live stream stops short of its closing tail, so the
533
+ // client is left holding arguments it cannot parse.
534
+ const captured = toolCallScreen ? toolCallScreen.admit(rawCaptured) : rawCaptured;
535
+ const notice = toolCallScreen?.notice();
536
+ if (notice) {
537
+ visibleContent += notice;
538
+ onContentDelta?.(notice);
539
+ await writeSSE({
540
+ data: JSON.stringify({
541
+ id,
542
+ object: 'chat.completion.chunk',
543
+ created,
544
+ model: echoModel,
545
+ choices: [{ index: 0, delta: { content: notice }, finish_reason: null }],
546
+ ...nullUsage,
547
+ }),
548
+ }, 'content');
549
+ }
522
550
  const hasToolCalls = captured.length > 0;
523
551
  if (hasToolCalls) {
524
552
  if (!streamToolCallDeltas || liveToolCalls.length === 0) {
@@ -554,6 +582,17 @@ export async function runStreaming(session, prompt, echoModel, sink, nowSeconds,
554
582
  // any suffix that did not arrive live; never replay the complete
555
583
  // arguments, which would make clients concatenate invalid JSON.
556
584
  const matchedLive = new Set();
585
+ // Take the withheld calls' streams out of the running first, so an
586
+ // admitted call of the same tool name cannot be matched onto one.
587
+ for (const call of rawCaptured) {
588
+ if (captured.includes(call))
589
+ continue;
590
+ const live = liveToolCalls.find((candidate) => !matchedLive.has(candidate) &&
591
+ (candidate.id === call.id ||
592
+ (candidate.name === call.name && call.arguments.startsWith(candidate.arguments))));
593
+ if (live)
594
+ matchedLive.add(live);
595
+ }
557
596
  for (const call of captured) {
558
597
  const live = liveToolCalls.find((candidate) => !matchedLive.has(candidate) &&
559
598
  (candidate.id === call.id || candidate.name === call.name));
@@ -565,6 +604,7 @@ export async function runStreaming(session, prompt, echoModel, sink, nowSeconds,
565
604
  id: call.id,
566
605
  name: call.name,
567
606
  arguments: call.arguments,
607
+ received: call.arguments,
568
608
  });
569
609
  await writeSSE({
570
610
  data: JSON.stringify({
@@ -0,0 +1,28 @@
1
+ import { type EgressIndicator } from '@bendyline/gezel';
2
+ import type { ExternalToolCall } from '@bendyline/gezel/local-loop';
3
+ /**
4
+ * Screens the tool calls a `/v1` turn hands back to the calling app.
5
+ *
6
+ * On these surfaces the app runs the tools, often with a real shell (Codex,
7
+ * OpenCode, Pi on a local model), so the response is the last place Gezel
8
+ * sees a call before it runs. A call naming an egress indicator (core
9
+ * `security/egress-indicators.ts`) never reaches the app as a runnable call;
10
+ * the reply says so instead.
11
+ */
12
+ export interface ToolCallScreen {
13
+ /** The calls the app may run. Records the rest. */
14
+ admit(calls: readonly ExternalToolCall[]): ExternalToolCall[];
15
+ /** True when this call must not reach the app. Records it. */
16
+ blocks(name: string, args: string): boolean;
17
+ /** A line for the visible reply once anything was blocked. */
18
+ notice(): string | undefined;
19
+ }
20
+ export declare function createToolCallScreen(onBlocked?: (tool: string, indicators: EgressIndicator[]) => void): ToolCallScreen;
21
+ /**
22
+ * The part of a live-streamed argument string that is safe to send before
23
+ * the call has been screened: everything but a trailing run of closing
24
+ * brackets and whitespace. A tool call's arguments are a JSON object, which
25
+ * must end in `}`, so a client cannot parse — and so cannot run — a call
26
+ * until the screened tail arrives. Grows monotonically as `received` grows.
27
+ */
28
+ export declare function screenableArgumentsPrefix(received: string): string;
@@ -0,0 +1,43 @@
1
+ import { describeEgressIndicators, findEgressIndicators, findEgressIndicatorsInValue, } from '@bendyline/gezel';
2
+ export function createToolCallScreen(onBlocked) {
3
+ const blocked = [];
4
+ const seen = new Set();
5
+ const blocks = (name, args) => {
6
+ const indicators = indicatorsIn(args);
7
+ if (indicators.length === 0)
8
+ return false;
9
+ // The serializer and the final reconciliation can both see one call.
10
+ const key = `${name}\u0000${args}`;
11
+ if (!seen.has(key)) {
12
+ seen.add(key);
13
+ blocked.push(`${name} (it named ${describeEgressIndicators(indicators)})`);
14
+ onBlocked?.(name, indicators);
15
+ }
16
+ return true;
17
+ };
18
+ return {
19
+ admit: (calls) => calls.filter((call) => !blocks(call.name, call.arguments)),
20
+ blocks,
21
+ notice: () => blocked.length === 0
22
+ ? undefined
23
+ : `\n\n[Gezel stopped ${blocked.length === 1 ? 'a tool call' : `${blocked.length} tool calls`} before ${blocked.length === 1 ? 'it' : 'they'} ran: ${blocked.join('; ')}.]`,
24
+ };
25
+ }
26
+ function indicatorsIn(args) {
27
+ try {
28
+ return findEgressIndicatorsInValue(JSON.parse(args));
29
+ }
30
+ catch {
31
+ return findEgressIndicators(args);
32
+ }
33
+ }
34
+ /**
35
+ * The part of a live-streamed argument string that is safe to send before
36
+ * the call has been screened: everything but a trailing run of closing
37
+ * brackets and whitespace. A tool call's arguments are a JSON object, which
38
+ * must end in `}`, so a client cannot parse — and so cannot run — a call
39
+ * until the screened tail arrives. Grows monotonically as `received` grows.
40
+ */
41
+ export function screenableArgumentsPrefix(received) {
42
+ return received.replace(/[\s}\]]+$/, '');
43
+ }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { createLogger, isLocalProvider } from '@bendyline/gezel';
2
+ import { createLogger, isLocalProvider, toolCallBlockedHistoryEntry, } from '@bendyline/gezel';
3
3
  import { Hono } from 'hono';
4
4
  import { streamSSE } from 'hono/streaming';
5
5
  import { ZodError } from 'zod';
@@ -14,6 +14,7 @@ import { engineFailure } from '../openai-compat/engine-failure.js';
14
14
  import { overlayOpenAiRequestTuning } from '../openai-compat/request-tuning.js';
15
15
  import { createStreamingDiagnostics, snapshotStreamingDiagnostics, } from '../openai-compat/streaming-telemetry.js';
16
16
  import { runNonStreaming, runStreaming } from '../openai-compat/streaming.js';
17
+ import { createToolCallScreen } from '../openai-compat/tool-call-screen.js';
17
18
  import { ChatCompletionRequestSchema, flattenTranscriptIntoPrompt, parseGezelModelRef, resolveModelTarget, translateMessagesWithPrefix, } from '../openai-compat/translate.js';
18
19
  const log = createLogger('v1-chat');
19
20
  // Match ChatManager's user-turn ceilings. Caller-owned app routes create a
@@ -402,6 +403,17 @@ export function v1ChatRoutes(ctx, opts = {}) {
402
403
  ctx.chat.recordExternalUsage?.(target.provider, u);
403
404
  });
404
405
  const appId = c.get('auth')?.appId ?? 'unauthenticated';
406
+ const toolCallScreen = createToolCallScreen((tool, indicators) => {
407
+ const entry = toolCallBlockedHistoryEntry({
408
+ tool,
409
+ surface: 'openai',
410
+ indicators,
411
+ appId,
412
+ ...(externalTurn ? { sessionId: externalTurn.sessionId } : {}),
413
+ });
414
+ log.warn(`${entry.summary} (app ${appId})`);
415
+ void ctx.history?.log(entry).catch(() => { });
416
+ });
405
417
  const logCompletion = () => {
406
418
  void ctx.history
407
419
  ?.log({
@@ -482,6 +494,7 @@ export function v1ChatRoutes(ctx, opts = {}) {
482
494
  ...(opts.keepaliveIntervalMs !== undefined
483
495
  ? { keepaliveIntervalMs: opts.keepaliveIntervalMs }
484
496
  : {}),
497
+ toolCallScreen,
485
498
  ...(opts.streamToolCallDeltas === true ? { streamToolCallDeltas: true } : {}),
486
499
  ...(opts.streamToolCallDeltas === true && externalTools
487
500
  ? { toolCallNames: externalTools.map((tool) => tool.name) }
@@ -574,6 +587,7 @@ export function v1ChatRoutes(ctx, opts = {}) {
574
587
  ...(attachments.length > 0 ? { attachments } : {}),
575
588
  sendOptions: { queue, timeoutMs: turnTimeoutMs },
576
589
  ...(lengthCapTokens !== undefined ? { lengthCapTokens } : {}),
590
+ toolCallScreen,
577
591
  });
578
592
  const choice = response.choices[0];
579
593
  await externalTurn
@@ -1,4 +1,4 @@
1
- import { createLogger } from '@bendyline/gezel';
1
+ import { createLogger, toolCallBlockedHistoryEntry } from '@bendyline/gezel';
2
2
  import { Hono } from 'hono';
3
3
  import { bodyLimit } from 'hono/body-limit';
4
4
  import { streamSSE } from 'hono/streaming';
@@ -11,6 +11,7 @@ import { resolveChatTarget, resolvePinnedModelTarget, } from '../openai-compat/c
11
11
  import { engineFailure } from '../openai-compat/engine-failure.js';
12
12
  import { runResponsesNonStreaming, runResponsesStreaming, } from '../openai-compat/responses-streaming.js';
13
13
  import { ResponsesRequestSchema, translateResponsesRequest, } from '../openai-compat/responses-translate.js';
14
+ import { createToolCallScreen } from '../openai-compat/tool-call-screen.js';
14
15
  import { flattenTranscriptIntoPrompt, parseGezelModelRef, resolveModelTarget, } from '../openai-compat/translate.js';
15
16
  const log = createLogger('v1-responses');
16
17
  const MAX_RESPONSES_BODY_BYTES = 16 * 1024 * 1024;
@@ -180,8 +181,19 @@ export function v1ResponsesRoutes(ctx, opts = {}) {
180
181
  const detachRequestAbort = () => {
181
182
  requestSignal.removeEventListener('abort', abortTurn);
182
183
  };
184
+ const toolCallScreen = createToolCallScreen((tool, indicators) => {
185
+ const entry = toolCallBlockedHistoryEntry({
186
+ tool,
187
+ surface: 'responses',
188
+ indicators,
189
+ appId,
190
+ });
191
+ log.warn(`${entry.summary} (app ${appId})`);
192
+ void ctx.history?.log(entry).catch(() => { });
193
+ });
183
194
  const runnerOptions = {
184
195
  ...(attachments.length > 0 ? { attachments } : {}),
196
+ toolCallScreen,
185
197
  ...(lengthCapTokens !== undefined ? { lengthCapTokens } : {}),
186
198
  toolBindings: translated.toolBindings,
187
199
  toolKinds: translated.toolKinds,
@@ -118,6 +118,8 @@ export interface ImageGenerationMeta {
118
118
  * forward it untouched.
119
119
  */
120
120
  img2imgSkippedReason?: string;
121
+ /** Provider limits or adjustments to the requested image settings. */
122
+ settingsNotes?: string[];
121
123
  }
122
124
  export interface ImageGenerationOutput {
123
125
  png: Buffer;
@@ -1,4 +1,4 @@
1
- import type { CodexPermissionModeCompat, ProviderName } from '@bendyline/gezel';
1
+ import type { CodexPermissionModeCompat, EgressIndicator, ProviderName } from '@bendyline/gezel';
2
2
  import type { ActiveCraftbookStep, CompactionRequester, ExternalToolCall, ExternalToolSpec, LLMSession, ProviderSessionState, TerminalToolPolicy, WireTranscriptEntry } from '@bendyline/gezel/local-loop';
3
3
  import type { McpServerSpec } from '@bendyline/gezel/local-loop';
4
4
  import type { ResolvedModelProfile } from '../model-profile/types.js';
@@ -537,6 +537,12 @@ export interface ToolCallEvent {
537
537
  durationMs: number;
538
538
  success: boolean;
539
539
  errorMessage?: string;
540
+ /**
541
+ * Set when Gezel refused the call before it ran because it named a cloud
542
+ * metadata service, link-local address, data-drop or tunnel host, or
543
+ * namespace tool. The consumer records it in History.
544
+ */
545
+ egressIndicators?: EgressIndicator[];
540
546
  /**
541
547
  * Text returned by the tool when the provider exposes it. Producers must
542
548
  * redact known secrets before firing the event. ChatManager bounds this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/gezel-inference",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Gezel's on-device inference runtime: native engines, local model management, embeddings, knowledge search, and OpenAI-compatible routes.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -73,9 +73,9 @@
73
73
  "!dist/**/__pycache__"
74
74
  ],
75
75
  "dependencies": {
76
- "@bendyline/gezel": "1.2.9",
77
- "@bendyline/gezel-catalog": "1.2.9",
78
- "@bendyline/gezel-client": "1.2.9",
76
+ "@bendyline/gezel": "1.2.10",
77
+ "@bendyline/gezel-catalog": "1.2.10",
78
+ "@bendyline/gezel-client": "1.2.10",
79
79
  "@bendyline/gezel-knowledge": "1.2.9",
80
80
  "hono": "4.13.9",
81
81
  "tar": "7.5.21",