bazis 0.99.1 → 0.99.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,8 @@ import { assertAgentToolHookIdV1, getAgentModuleContributionsV1, type AgentModul
7
7
  import type { AgentToolBeforeEffectEventV1, AgentToolEnforcementDecisionV1, AgentToolHookContextV1, AgentToolRequiredHookRefV1, AgentToolAuditHookProjectionV1, AgentToolSettlementEventV1 } from "./AgentToolHooks";
8
8
  import { agentModelJson } from "./internal/AgentModelBinding";
9
9
  import { redactSensitive, type SensitiveRedactionOptions } from "../../library/redaction";
10
- import { AgentSetupError, AgentToolExecutionError, AgentToolPreCommitError } from "./errors";
10
+ import { AgentSetupError, AgentToolExecutionError, AgentToolPreCommitError, ToolError } from "./errors";
11
+ import type { Logger } from "../kernel/logging/Logger";
11
12
  import type { AgentRegistry, ToolDefinition } from "./AgentRegistry";
12
13
  import type { JsonObject, JsonValue, AgentToolCall, AgentToolResult } from "./semantic";
13
14
  import { agentToolCall, agentToolResult, normalizeJsonValue } from "./semantic";
@@ -326,14 +327,14 @@ function isSettlementResult(value: unknown): value is { readonly status: "record
326
327
  } catch { return false; }
327
328
  }
328
329
 
329
- type HookOutcome<T> = { readonly kind: "value"; readonly value: T } | { readonly kind: "timeout" } | { readonly kind: "cancelled" } | { readonly kind: "failed" };
330
+ type HookOutcome<T> = { readonly kind: "value"; readonly value: T } | { readonly kind: "timeout" } | { readonly kind: "cancelled" } | { readonly kind: "failed"; readonly error?: unknown };
330
331
 
331
332
  async function awaitHook<T>(operation: Promise<T>, signal: AbortSignal | undefined, timeoutMs: number | undefined): Promise<HookOutcome<T>> {
332
333
  // Observe both branches even after the caller's deadline wins: a late hook
333
334
  // rejection must never become an unhandled process-level rejection.
334
335
  const observed: Promise<HookOutcome<T>> = operation
335
336
  .then((value): HookOutcome<T> => ({ kind: "value", value }))
336
- .catch((): HookOutcome<T> => ({ kind: "failed" }));
337
+ .catch((error: unknown): HookOutcome<T> => ({ kind: "failed", error }));
337
338
  if (signal?.aborted) return { kind: "cancelled" };
338
339
  let timer: ReturnType<typeof setTimeout> | undefined;
339
340
  let detach: (() => void) | undefined;
@@ -430,6 +431,8 @@ export class AgentToolExecutor {
430
431
  private readonly services: ServiceProvider,
431
432
  private readonly registry: AgentRegistry,
432
433
  options: AgentToolExecutorOptions = {},
434
+ /** Where unexpected tool failures go with their stack; the model sees a generic message. */
435
+ private readonly logger?: Logger,
433
436
  ) {
434
437
  assertNonNegativeInteger(options.defaultTimeoutMs, "AgentToolExecutor.defaultTimeoutMs");
435
438
  assertPositiveInteger(options.scopeDisposeTimeoutMs, "AgentToolExecutor.scopeDisposeTimeoutMs");
@@ -643,6 +646,7 @@ export class AgentToolExecutor {
643
646
  tier: this.isPlatformHook(record) ? "platform" : "application", order: registration.order ?? 0,
644
647
  outcome: "failed",
645
648
  };
649
+ let hookError: unknown;
646
650
  const postEffect = state.executeStarted;
647
651
  const configuredTimeoutMs = registration.timeoutMs ?? 5_000;
648
652
  const remainingMs = !postEffect && state.deadlineUnixMs !== undefined
@@ -680,6 +684,7 @@ export class AgentToolExecutor {
680
684
  const activated = await awaitHook(record.activation.activateAsync(scope), controller.signal, boundedTimeoutMs);
681
685
  if (activated.kind !== "value") {
682
686
  if (activated.kind === "timeout") controller.abort();
687
+ if (activated.kind === "failed") hookError = activated.error;
683
688
  projection = { ...projection, outcome: activated.kind === "timeout" ? "timed-out" : activated.kind === "cancelled" ? (state.callerSignal?.aborted ? "cancelled" : "timed-out") : "failed" };
684
689
  terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement handler was unavailable.", metadata, startedAt);
685
690
  } else {
@@ -692,6 +697,7 @@ export class AgentToolExecutor {
692
697
  const outcome = await awaitHook(Promise.resolve((handler.settle as (value: AgentToolSettlementEventV1, context: AgentToolHookContextV1) => unknown)(event, context)), controller.signal, invocationTimeoutMs);
693
698
  if (outcome.kind === "timeout") controller.abort();
694
699
  if (outcome.kind !== "value") {
700
+ if (outcome.kind === "failed") hookError = outcome.error;
695
701
  projection = { ...projection, outcome: outcome.kind === "timeout" ? "timed-out" : outcome.kind === "cancelled" ? (state.callerSignal?.aborted ? "cancelled" : "timed-out") : "failed" };
696
702
  terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement failed.", metadata, startedAt);
697
703
  } else if (!isSettlementResult(outcome.value)) {
@@ -704,17 +710,20 @@ export class AgentToolExecutor {
704
710
  }
705
711
  }
706
712
  }
707
- } catch {
713
+ } catch (error) {
714
+ hookError = error;
708
715
  projection = { ...projection, outcome: "failed" };
709
716
  terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement failed.", metadata, startedAt);
710
717
  } finally {
711
718
  detach?.();
712
719
  const disposeError = await this.disposeScope(scope, this.scopeDisposeTimeoutMs);
713
720
  if (disposeError !== undefined) {
721
+ hookError ??= disposeError;
714
722
  projection = { ...projection, outcome: "failed" };
715
723
  terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement disposal failed.", metadata, startedAt);
716
724
  }
717
725
  }
726
+ this.logHookFailure(projection, call, event.agentName, hookError);
718
727
  const evidenceRemainingMs = postEffect || state.deadlineUnixMs === undefined
719
728
  ? registration.timeoutMs ?? 5_000
720
729
  : Math.max(0, Math.min(registration.timeoutMs ?? 5_000, state.deadlineUnixMs - Date.now()));
@@ -741,6 +750,7 @@ export class AgentToolExecutor {
741
750
  if (registration.kind !== "observer") continue;
742
751
  const scope = this.services.createScope();
743
752
  let outcome: AgentToolAuditHookProjectionV1["outcome"] = "failed";
753
+ let hookError: unknown;
744
754
  const timeoutMs = registration.timeoutMs ?? 5_000;
745
755
  const controller = new AbortController();
746
756
  const detach = attachAbortForwarding(options.signal, controller);
@@ -749,6 +759,7 @@ export class AgentToolExecutor {
749
759
  const activated = await awaitHook(record.activation.activateAsync(scope), controller.signal, timeoutMs);
750
760
  if (activated.kind !== "value") {
751
761
  if (activated.kind === "timeout") controller.abort();
762
+ if (activated.kind === "failed") hookError = activated.error;
752
763
  outcome = activated.kind === "timeout" ? "timed-out" : activated.kind === "cancelled" ? "cancelled" : "failed";
753
764
  } else {
754
765
  const handler = activated.value as { observe?: unknown };
@@ -761,15 +772,22 @@ export class AgentToolExecutor {
761
772
  Math.max(1, context.deadlineUnixMs - Date.now()),
762
773
  );
763
774
  if (observed.kind === "timeout" || observed.kind === "value") controller.abort();
775
+ if (observed.kind === "failed") hookError = observed.error;
764
776
  outcome = observed.kind === "value" ? "recorded" : observed.kind === "timeout" ? "timed-out" : observed.kind === "cancelled" ? "cancelled" : "failed";
765
777
  }
766
778
  }
767
- } catch {
779
+ } catch (error) {
780
+ hookError = error;
768
781
  outcome = controller.signal.aborted && options.signal?.aborted ? "cancelled" : "failed";
769
782
  } finally {
770
783
  detach?.();
771
- if (await this.disposeScope(scope, this.scopeDisposeTimeoutMs) !== undefined) outcome = "failed";
784
+ const disposeError = await this.disposeScope(scope, this.scopeDisposeTimeoutMs);
785
+ if (disposeError !== undefined) {
786
+ hookError ??= disposeError;
787
+ outcome = "failed";
788
+ }
772
789
  }
790
+ this.logHookFailure({ kind: registration.kind, id: registration.id, version: registration.version, outcome }, { id: settlement.callId, name: settlement.toolName }, settlement.agentName, hookError);
773
791
  await this.recordObserverHook(settlement, registration, record, outcome, timeoutMs);
774
792
  }
775
793
  }
@@ -1083,6 +1101,7 @@ export class AgentToolExecutor {
1083
1101
  tier: isPlatform(record) ? "platform" : "application", order: registration.order ?? 0,
1084
1102
  outcome: "failed",
1085
1103
  };
1104
+ let hookError: unknown;
1086
1105
  const configuredTimeoutMs = registration.timeoutMs ?? 5_000;
1087
1106
  const remainingMs = executionState.deadlineUnixMs === undefined
1088
1107
  ? undefined : Math.max(0, executionState.deadlineUnixMs - Date.now());
@@ -1099,6 +1118,7 @@ export class AgentToolExecutor {
1099
1118
  const activated = await awaitHook(record.activation.activateAsync(scope), controller.signal, timeoutMs);
1100
1119
  if (activated.kind !== "value") {
1101
1120
  if (activated.kind === "timeout") controller.abort();
1121
+ if (activated.kind === "failed") hookError = activated.error;
1102
1122
  projection = { ...projection, outcome: activated.kind === "timeout" ? "timed-out" : activated.kind === "cancelled" ? "cancelled" : "failed" };
1103
1123
  terminal = this.errorResult(call, activated.kind === "timeout" ? "TOOL_HOOK_TIMEOUT" : activated.kind === "cancelled" ? "TOOL_HOOK_CANCELLED" : "TOOL_HOOK_UNAVAILABLE", "Agent Tool enforcement handler was unavailable.", metadata, startedAt);
1104
1124
  } else {
@@ -1110,27 +1130,31 @@ export class AgentToolExecutor {
1110
1130
  if (outcome.kind === "timeout") controller.abort();
1111
1131
  if (outcome.kind === "timeout") { projection = { ...projection, outcome: "timed-out" }; terminal = this.errorResult(call, "TOOL_HOOK_TIMEOUT", "Agent Tool enforcement hook timed out.", metadata, startedAt); }
1112
1132
  else if (outcome.kind === "cancelled") { projection = { ...projection, outcome: "cancelled" }; terminal = this.errorResult(call, "TOOL_HOOK_CANCELLED", "Agent Tool enforcement hook was cancelled.", metadata, startedAt); }
1113
- else if (outcome.kind === "failed") { terminal = this.errorResult(call, "TOOL_HOOK_FAILED", "Agent Tool enforcement hook failed.", metadata, startedAt); }
1133
+ else if (outcome.kind === "failed") { hookError = outcome.error; terminal = this.errorResult(call, "TOOL_HOOK_FAILED", "Agent Tool enforcement hook failed.", metadata, startedAt); }
1114
1134
  else if (!isHookDecision(outcome.value)) { projection = { ...projection, outcome: "invalid-result" }; terminal = this.errorResult(call, "TOOL_HOOK_DECISION_INVALID", "Agent Tool enforcement hook returned an invalid decision.", metadata, startedAt); }
1115
1135
  else {
1116
1136
  const decision = outcome.value;
1117
- projection = { ...projection, outcome: decision.decision === "allow" ? "allowed" : "denied", ...(decision.decision === "deny" && decision.reasonCode ? { reasonCode: decision.reasonCode } : {}), ...(decision.evidence ? { evidence: redactSensitive(normalizeHookEvidence(decision.evidence), this.hookRedaction) as JsonObject } : {}) };
1118
- if (decision.decision === "deny") terminal = this.deniedResult(call, "TOOL_HOOK_DENIED", "Tool execution was denied by an enforcement hook.", {}, metadata, startedAt);
1137
+ projection = { ...projection, outcome: decision.decision === "allow" ? "allowed" : "denied", ...(decision.decision === "deny" && decision.reasonCode ? { reasonCode: decision.reasonCode } : {}), ...(decision.decision === "deny" && decision.message ? { message: redactSensitive(decision.message) as string } : {}), ...(decision.evidence ? { evidence: redactSensitive(normalizeHookEvidence(decision.evidence), this.hookRedaction) as JsonObject } : {}) };
1138
+ // The hook's message is meant for the model, like a ToolError; reasonCode helps it branch.
1139
+ if (decision.decision === "deny") terminal = this.deniedResult(call, "TOOL_HOOK_DENIED", decision.message ?? "Tool execution was denied by an enforcement hook.", decision.reasonCode ? { reasonCode: decision.reasonCode } : {}, metadata, startedAt);
1119
1140
  }
1120
1141
  }
1121
1142
  }
1122
1143
  }
1123
- } catch {
1144
+ } catch (error) {
1145
+ hookError = error;
1124
1146
  projection = { ...projection, outcome: "failed" };
1125
1147
  terminal = this.errorResult(call, options.signal?.aborted ? "TOOL_HOOK_CANCELLED" : "TOOL_HOOK_FAILED", "Agent Tool enforcement hook failed.", metadata, startedAt);
1126
1148
  } finally {
1127
1149
  detach?.();
1128
1150
  const disposeError = await this.disposeScope(scope, this.scopeDisposeTimeoutMs);
1129
1151
  if (disposeError !== undefined) {
1152
+ hookError ??= disposeError;
1130
1153
  projection = { ...projection, outcome: "failed" };
1131
1154
  terminal = this.errorResult(call, "TOOL_HOOK_FAILED", "Agent Tool enforcement hook disposal failed.", metadata, startedAt);
1132
1155
  }
1133
1156
  }
1157
+ this.logHookFailure(projection, call, agentName, hookError);
1134
1158
  const evidenceRemainingMs = executionState.deadlineUnixMs === undefined
1135
1159
  ? registration.timeoutMs ?? 5_000
1136
1160
  : Math.max(0, Math.min(registration.timeoutMs ?? 5_000, executionState.deadlineUnixMs - Date.now()));
@@ -1530,15 +1554,12 @@ export class AgentToolExecutor {
1530
1554
  );
1531
1555
  });
1532
1556
  const code = this.interruptionCode(error, tool, executionState);
1557
+ if (code.endsWith("_OUTCOME_UNKNOWN")) {
1558
+ this.logger?.error("Agent tool outcome unknown", { agent: agentName ?? null, tool: call.name, callId: call.id, code, phase: executionState.phase });
1559
+ }
1533
1560
  return this.errorResult(call, code, executionState.timedOut ? "Tool execution timed out." : errorMessageOf(error), this.attemptMetadata(metadata, attempt), startedAt, { phase: executionState.phase });
1534
1561
  }
1535
- result = this.errorResult(
1536
- call,
1537
- "TOOL_EXECUTION_FAILED",
1538
- errorMessageOf(error),
1539
- this.attemptMetadata(metadata, attempt),
1540
- startedAt,
1541
- );
1562
+ result = this.failedResult(call, agentName, "TOOL_EXECUTION_FAILED", error, "Tool execution failed.", this.attemptMetadata(metadata, attempt), startedAt);
1542
1563
  } finally {
1543
1564
  detach?.();
1544
1565
  }
@@ -1582,13 +1603,7 @@ export class AgentToolExecutor {
1582
1603
  ? await contributed[0]!.activation.activateAsync(scope)
1583
1604
  : await scope.resolveAsync(tool.target);
1584
1605
  } catch (error) {
1585
- return this.errorResult(
1586
- call,
1587
- "TOOL_RESOLVE_FAILED",
1588
- `Tool resolve failed: ${errorMessageOf(error)}`,
1589
- this.attemptMetadata(metadata, attempt),
1590
- startedAt,
1591
- );
1606
+ return this.failedResult(call, agentName, "TOOL_RESOLVE_FAILED", error, "Tool could not be created.", this.attemptMetadata(metadata, attempt), startedAt);
1592
1607
  }
1593
1608
 
1594
1609
  if (signal.aborted) {
@@ -1638,12 +1653,14 @@ export class AgentToolExecutor {
1638
1653
  const explicitlyPreCommit = error instanceof AgentToolPreCommitError;
1639
1654
  const outcomeUnknown = !explicitlyPreCommit
1640
1655
  && (tool.metadata.sideEffect === "write" || tool.metadata.sideEffect === "external");
1641
- return this.errorResult(
1656
+ return this.failedResult(
1642
1657
  call,
1658
+ agentName,
1643
1659
  outcomeUnknown ? "TOOL_EXECUTION_FAILED_OUTCOME_UNKNOWN" : "TOOL_EXECUTION_FAILED",
1660
+ error,
1644
1661
  outcomeUnknown
1645
- ? `Tool "${tool.metadata.name}" threw after side-effect execution started; retry safety is unknown: ${errorMessageOf(error)}`
1646
- : errorMessageOf(error),
1662
+ ? `Tool "${tool.metadata.name}" threw after side-effect execution started; retry safety is unknown.`
1663
+ : "Tool execution failed.",
1647
1664
  this.attemptMetadata(metadata, attempt),
1648
1665
  startedAt,
1649
1666
  outcomeUnknown ? { sideEffect: tool.metadata.sideEffect, phase: "execute" } : undefined,
@@ -1664,13 +1681,7 @@ export class AgentToolExecutor {
1664
1681
  return this.postExecutionOutcome(
1665
1682
  call,
1666
1683
  tool,
1667
- this.errorResult(
1668
- call,
1669
- "TOOL_OUTPUT_INVALID",
1670
- `Tool output processing failed: ${errorMessageOf(error)}`,
1671
- this.attemptMetadata(metadata, attempt),
1672
- startedAt,
1673
- ),
1684
+ this.failedResult(call, agentName, "TOOL_OUTPUT_INVALID", error, "Tool output processing failed.", this.attemptMetadata(metadata, attempt), startedAt),
1674
1685
  metadata,
1675
1686
  startedAt,
1676
1687
  attempt,
@@ -1692,13 +1703,7 @@ export class AgentToolExecutor {
1692
1703
  return this.postExecutionOutcome(
1693
1704
  call,
1694
1705
  tool,
1695
- this.errorResult(
1696
- call,
1697
- "TOOL_RESULT_INVALID",
1698
- errorMessageOf(error),
1699
- this.attemptMetadata(metadata, attempt),
1700
- startedAt,
1701
- ),
1706
+ this.failedResult(call, agentName, "TOOL_RESULT_INVALID", error, "Tool result is invalid.", this.attemptMetadata(metadata, attempt), startedAt),
1702
1707
  metadata,
1703
1708
  startedAt,
1704
1709
  attempt,
@@ -2056,6 +2061,41 @@ export class AgentToolExecutor {
2056
2061
  }
2057
2062
 
2058
2063
 
2064
+ /** A hook that failed, timed out or answered nonsense; a cancelled run is not a failure. */
2065
+ private logHookFailure(
2066
+ hook: Pick<AgentToolAuditHookProjectionV1, "kind" | "id" | "version" | "outcome">,
2067
+ call: Pick<AgentToolCall, "id" | "name">,
2068
+ agentName: string | undefined,
2069
+ error: unknown,
2070
+ ): void {
2071
+ if (hook.outcome !== "failed" && hook.outcome !== "timed-out" && hook.outcome !== "invalid-result") return;
2072
+ this.logger?.error("Agent tool hook failed", {
2073
+ hook: `${hook.kind}:${hook.id}@${hook.version}`, outcome: hook.outcome, agent: agentName ?? null, tool: call.name, callId: call.id,
2074
+ ...(error === undefined ? {} : { error: redactSensitive(error) }),
2075
+ });
2076
+ }
2077
+
2078
+ /**
2079
+ * A tool's own failure: a ToolError message reaches the model as written;
2080
+ * anything else is replaced by `fallback` and logged with its stack.
2081
+ */
2082
+ private failedResult(
2083
+ call: AgentToolCall,
2084
+ agentName: string | undefined,
2085
+ code: string,
2086
+ error: unknown,
2087
+ fallback: string,
2088
+ metadata: JsonObject,
2089
+ startedAt: number,
2090
+ details?: JsonObject,
2091
+ ): AgentToolResult {
2092
+ if (error instanceof ToolError) {
2093
+ return this.errorResult(call, code, error.message, metadata, startedAt, details);
2094
+ }
2095
+ this.logger?.error("Agent tool failed", { agent: agentName ?? null, tool: call.name, callId: call.id, code, error: redactSensitive(error) });
2096
+ return this.errorResult(call, code, fallback, metadata, startedAt, details);
2097
+ }
2098
+
2059
2099
  private errorResult(
2060
2100
  call: AgentToolCall,
2061
2101
  code: string,
@@ -36,4 +36,4 @@ export interface AgentToolSettlementResultV1 { readonly status: "recorded"; read
36
36
  export interface AgentToolSettlementHookV1 { settle(event: AgentToolSettlementEventV1, context: AgentToolHookContextV1): AgentToolSettlementResultV1 | Promise<AgentToolSettlementResultV1>; }
37
37
  export interface AgentToolObserverEventV1 { readonly type: "agent-tool.observer/v1"; readonly settlement: AgentToolSettlementEventV1; }
38
38
  export interface AgentToolObserverHookV1 { observe(event: AgentToolObserverEventV1, context: AgentToolHookContextV1): void | Promise<void>; }
39
- export interface AgentToolAuditHookProjectionV1 extends AgentToolHookRefV1 { readonly tier: "platform" | "application"; readonly order: number; readonly outcome: "allowed" | "denied" | "recorded" | "failed" | "timed-out" | "cancelled" | "invalid-result"; readonly reasonCode?: string; readonly evidence?: JsonObject; }
39
+ export interface AgentToolAuditHookProjectionV1 extends AgentToolHookRefV1 { readonly tier: "platform" | "application"; readonly order: number; readonly outcome: "allowed" | "denied" | "recorded" | "failed" | "timed-out" | "cancelled" | "invalid-result"; readonly reasonCode?: string; readonly message?: string; readonly evidence?: JsonObject; }
@@ -100,6 +100,21 @@ Tool input and output are validated by the model validator
100
100
  otherwise. Audit entries redact `input`, `result` and execution `metadata`
101
101
  by default using `bazis/library/redaction`.
102
102
 
103
+ A tool reports a failure to the model with `throw new ToolError("Product
104
+ kiwi is no longer sold")`: the message reaches the model as written. Any other
105
+ exception from `execute` reaches the model as `Tool execution failed.` — internal
106
+ details stay out of the LLM — and goes to the application log with its stack
107
+ (`Agent tool failed`). Failures of the model itself are logged as `Agent model
108
+ call failed`. The LLM connectors pass the application logger; a runtime created
109
+ by hand takes `options.logger`.
110
+
111
+ An enforcement hook that denies a call explains why with `message` — it reaches
112
+ the model as written, with `reasonCode` in `error.details`, and both go to the
113
+ audit. A hook that throws, times out or returns an invalid answer is logged as
114
+ `Agent tool hook failed` with the error. `llmConnect` and `llmRouter` build
115
+ `AgentRuntime` at start, so wrong runtime options or enforcement hooks without
116
+ an audit sink stop the application with a configuration error.
117
+
103
118
  Retries are opt-in. `write` and `external` tools need an idempotency key before
104
119
  the executor repeats them. Runtime-generated keys include both the invocation id
105
120
  and the unique model call id; `toolExecution.idempotencyKeyForCall` gives a
@@ -22,12 +22,25 @@ export class AgentToolExecutionError extends Error {
22
22
  }
23
23
  }
24
24
 
25
+ /**
26
+ * A failure a tool reports to the model on purpose: the message reaches the
27
+ * model as written, so the model can explain it or try another way. Any other
28
+ * exception from `execute` reaches the model as "Tool execution failed." and
29
+ * goes to the application log with its stack.
30
+ */
31
+ export class ToolError extends Error {
32
+ constructor(message: string) {
33
+ super(message);
34
+ this.name = "ToolError";
35
+ }
36
+ }
37
+
25
38
  /**
26
39
  * Explicit assertion by a tool that execution failed before any side effect
27
40
  * could commit. Only this typed error keeps write/external failures eligible
28
- * for an idempotency-gated retry.
41
+ * for an idempotency-gated retry. Its message reaches the model like a ToolError.
29
42
  */
30
- export class AgentToolPreCommitError extends Error {
43
+ export class AgentToolPreCommitError extends ToolError {
31
44
  constructor(message: string) {
32
45
  super(message);
33
46
  this.name = "AgentToolPreCommitError";
@@ -15,6 +15,7 @@ export {
15
15
  AgentSetupError,
16
16
  AgentToolExecutionError,
17
17
  AgentToolPreCommitError,
18
+ ToolError,
18
19
  } from "./errors";
19
20
  export {
20
21
  AGENT_RUNTIME_OPTIONS,
@@ -3,6 +3,7 @@ import { describeTool } from "../Tool.contract";
3
3
  import type { ServiceProvider } from "../../di";
4
4
  import { getGeneratedOpenApiMetadata } from "../../http/OpenApi/generatedOpenApiRegistry";
5
5
  import { redactSensitive, redactSensitiveText } from "../../../library/redaction";
6
+ import type { Logger } from "../../kernel/logging/Logger";
6
7
  import { AgentRuntimeError } from "../errors";
7
8
  import { validateAgentJsonSchema } from "./AgentJsonSchema";
8
9
  import type { AgentDefinition, AgentRegistry } from "../AgentRegistry";
@@ -80,6 +81,11 @@ export interface AgentRuntimeOptions {
80
81
  readonly contextBuilder?: AgentContextBuilder;
81
82
  readonly contextLimits?: AgentContextLimits;
82
83
  readonly toolExecutorOptions?: AgentToolExecutorOptions;
84
+ /**
85
+ * Where failures of the model and unexpected tool failures go, with their
86
+ * stack. The LLM connectors pass the application logger.
87
+ */
88
+ readonly logger?: Logger;
83
89
  }
84
90
 
85
91
  export interface AgentRuntimeInvokeOptions {
@@ -503,6 +509,7 @@ export class AgentExecutionDriver {
503
509
  readonly defaultTimeoutMs?: number;
504
510
  private readonly defaultContextLimits?: AgentContextLimits;
505
511
  private readonly contextBuilder: AgentContextBuilder;
512
+ private readonly logger?: Logger;
506
513
 
507
514
  constructor(
508
515
  services: ServiceProvider,
@@ -521,7 +528,8 @@ export class AgentExecutionDriver {
521
528
  : options.timeoutMs ?? DEFAULT_MODEL_TIMEOUT_MS;
522
529
  this.defaultContextLimits = normalizeAgentContextLimits(options.contextLimits, "AgentRuntime.contextLimits");
523
530
  this.contextBuilder = options.contextBuilder ?? new DefaultAgentContextBuilder();
524
- this.toolExecutor = new AgentToolExecutor(services, registry, options.toolExecutorOptions);
531
+ this.logger = options.logger;
532
+ this.toolExecutor = new AgentToolExecutor(services, registry, options.toolExecutorOptions, options.logger);
525
533
  }
526
534
 
527
535
  async invoke(agentName: string, options: AgentRuntimeInvokeOptions = {}): Promise<AgentRuntimeResult> {
@@ -1122,6 +1130,7 @@ export class AgentExecutionDriver {
1122
1130
  try {
1123
1131
  return { ok: true, value: normalizeProviderResponse(rawResponse) };
1124
1132
  } catch (error) {
1133
+ this.logger?.error("Agent model call failed", { agent: agentName, invocationId: request.invocationId, code: "AGENT_PROVIDER_RESPONSE_INVALID", error: redactSensitive(error) });
1125
1134
  return {
1126
1135
  ok: false,
1127
1136
  error: runtimeError("AGENT_PROVIDER_RESPONSE_INVALID", `Model provider returned an invalid response: ${errorMessageOf(error)}`),
@@ -1133,6 +1142,9 @@ export class AgentExecutionDriver {
1133
1142
  : error instanceof AgentRuntimeAbortError
1134
1143
  ? "AGENT_ABORTED"
1135
1144
  : "AGENT_PROVIDER_FAILED";
1145
+ if (code !== "AGENT_ABORTED") {
1146
+ this.logger?.error("Agent model call failed", { agent: agentName, invocationId: request.invocationId, code, error: redactSensitive(error) });
1147
+ }
1136
1148
  return { ok: false, error: runtimeError(code, errorMessageOf(error)) };
1137
1149
  } finally {
1138
1150
  acceptingText = false;
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
232
232
  if (method === "account/updated") this.modelCache = undefined;
233
233
  }, () => { this.clearLogin(); this.modelCache = undefined; });
234
234
  const initialized = await client.request("initialize", {
235
- clientInfo: { name: "bazis", title: "Bazis", version: "0.99.1" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.99.3" }, capabilities: { experimentalApi: true },
236
236
  }, 15_000, signal);
237
237
  if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
238
238
  client.notify("initialized");
@@ -1,6 +1,7 @@
1
1
  import type { AppConfig, ConfigRegistry, Secret } from "../../kernel";
2
2
  import { AGENT_MODEL_PROVIDER, AGENT_RUNTIME_OPTIONS, AgentRuntime, type AgentModelProvider } from "../../agent";
3
- import { DI, SERVICE_PROVIDER, type ProviderDefinition } from "../../di";
3
+ import { DI, DiError, HOSTED_SERVICE, SERVICE_PROVIDER, type ProviderDefinition } from "../../di";
4
+ import { LOGGER } from "../../kernel/logging/Logger";
4
5
  import { reader, requireValue } from "../connectorConfig";
5
6
  import { InfraError, type InfraConnector } from "../InfraConnector";
6
7
  import { identifyConnector } from "../connectorIdentity";
@@ -62,8 +63,22 @@ export function resolveLlmConnectionOptions<T extends LlmConfigShape>(
62
63
  export const agentRuntimeProviders: readonly ProviderDefinition[] = Object.freeze([
63
64
  DI.singleton(DI.factoryProviderWithResolver(AgentRuntime, [AGENT_MODEL_PROVIDER], (resolver, model: AgentModelProvider) => {
64
65
  const services = resolver.resolve(SERVICE_PROVIDER);
65
- return new AgentRuntime(services, model, services.has(AGENT_RUNTIME_OPTIONS) ? services.resolve(AGENT_RUNTIME_OPTIONS) : {});
66
+ const options = services.has(AGENT_RUNTIME_OPTIONS) ? services.resolve(AGENT_RUNTIME_OPTIONS) : {};
67
+ return new AgentRuntime(services, model, { ...options, logger: options.logger ?? services.tryResolve(LOGGER) });
66
68
  })),
69
+ // Builds the runtime at start: wrong options or hooks without an audit sink
70
+ // stop the application instead of failing every agent request.
71
+ DI.singleton(DI.factoryProviderWithResolver(HOSTED_SERVICE, [], (resolver) => ({
72
+ start() {
73
+ try {
74
+ resolver.resolve(AgentRuntime);
75
+ } catch (error) {
76
+ if (error instanceof DiError) throw error;
77
+ throw new DiError(`AgentRuntime: ${error instanceof Error ? error.message : String(error)}`);
78
+ }
79
+ },
80
+ stop() {},
81
+ }))),
67
82
  ]);
68
83
 
69
84
  export function llmConnect<T extends LlmConfigShape>(
package/index.ts CHANGED
@@ -83,6 +83,7 @@ export {
83
83
  AgentRuntimeError,
84
84
  AgentSemanticError,
85
85
  AgentToolExecutionError,
86
+ ToolError,
86
87
  ApproximateAgentContextTokenEstimator,
87
88
  DefaultAgentContextBuilder,
88
89
  Tool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.99.1",
3
+ "version": "0.99.3",
4
4
  "description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Alexander Alenin",