bazis 0.99.2 → 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.
@@ -327,14 +327,14 @@ function isSettlementResult(value: unknown): value is { readonly status: "record
327
327
  } catch { return false; }
328
328
  }
329
329
 
330
- 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 };
331
331
 
332
332
  async function awaitHook<T>(operation: Promise<T>, signal: AbortSignal | undefined, timeoutMs: number | undefined): Promise<HookOutcome<T>> {
333
333
  // Observe both branches even after the caller's deadline wins: a late hook
334
334
  // rejection must never become an unhandled process-level rejection.
335
335
  const observed: Promise<HookOutcome<T>> = operation
336
336
  .then((value): HookOutcome<T> => ({ kind: "value", value }))
337
- .catch((): HookOutcome<T> => ({ kind: "failed" }));
337
+ .catch((error: unknown): HookOutcome<T> => ({ kind: "failed", error }));
338
338
  if (signal?.aborted) return { kind: "cancelled" };
339
339
  let timer: ReturnType<typeof setTimeout> | undefined;
340
340
  let detach: (() => void) | undefined;
@@ -646,6 +646,7 @@ export class AgentToolExecutor {
646
646
  tier: this.isPlatformHook(record) ? "platform" : "application", order: registration.order ?? 0,
647
647
  outcome: "failed",
648
648
  };
649
+ let hookError: unknown;
649
650
  const postEffect = state.executeStarted;
650
651
  const configuredTimeoutMs = registration.timeoutMs ?? 5_000;
651
652
  const remainingMs = !postEffect && state.deadlineUnixMs !== undefined
@@ -683,6 +684,7 @@ export class AgentToolExecutor {
683
684
  const activated = await awaitHook(record.activation.activateAsync(scope), controller.signal, boundedTimeoutMs);
684
685
  if (activated.kind !== "value") {
685
686
  if (activated.kind === "timeout") controller.abort();
687
+ if (activated.kind === "failed") hookError = activated.error;
686
688
  projection = { ...projection, outcome: activated.kind === "timeout" ? "timed-out" : activated.kind === "cancelled" ? (state.callerSignal?.aborted ? "cancelled" : "timed-out") : "failed" };
687
689
  terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement handler was unavailable.", metadata, startedAt);
688
690
  } else {
@@ -695,6 +697,7 @@ export class AgentToolExecutor {
695
697
  const outcome = await awaitHook(Promise.resolve((handler.settle as (value: AgentToolSettlementEventV1, context: AgentToolHookContextV1) => unknown)(event, context)), controller.signal, invocationTimeoutMs);
696
698
  if (outcome.kind === "timeout") controller.abort();
697
699
  if (outcome.kind !== "value") {
700
+ if (outcome.kind === "failed") hookError = outcome.error;
698
701
  projection = { ...projection, outcome: outcome.kind === "timeout" ? "timed-out" : outcome.kind === "cancelled" ? (state.callerSignal?.aborted ? "cancelled" : "timed-out") : "failed" };
699
702
  terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement failed.", metadata, startedAt);
700
703
  } else if (!isSettlementResult(outcome.value)) {
@@ -707,17 +710,20 @@ export class AgentToolExecutor {
707
710
  }
708
711
  }
709
712
  }
710
- } catch {
713
+ } catch (error) {
714
+ hookError = error;
711
715
  projection = { ...projection, outcome: "failed" };
712
716
  terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement failed.", metadata, startedAt);
713
717
  } finally {
714
718
  detach?.();
715
719
  const disposeError = await this.disposeScope(scope, this.scopeDisposeTimeoutMs);
716
720
  if (disposeError !== undefined) {
721
+ hookError ??= disposeError;
717
722
  projection = { ...projection, outcome: "failed" };
718
723
  terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement disposal failed.", metadata, startedAt);
719
724
  }
720
725
  }
726
+ this.logHookFailure(projection, call, event.agentName, hookError);
721
727
  const evidenceRemainingMs = postEffect || state.deadlineUnixMs === undefined
722
728
  ? registration.timeoutMs ?? 5_000
723
729
  : Math.max(0, Math.min(registration.timeoutMs ?? 5_000, state.deadlineUnixMs - Date.now()));
@@ -744,6 +750,7 @@ export class AgentToolExecutor {
744
750
  if (registration.kind !== "observer") continue;
745
751
  const scope = this.services.createScope();
746
752
  let outcome: AgentToolAuditHookProjectionV1["outcome"] = "failed";
753
+ let hookError: unknown;
747
754
  const timeoutMs = registration.timeoutMs ?? 5_000;
748
755
  const controller = new AbortController();
749
756
  const detach = attachAbortForwarding(options.signal, controller);
@@ -752,6 +759,7 @@ export class AgentToolExecutor {
752
759
  const activated = await awaitHook(record.activation.activateAsync(scope), controller.signal, timeoutMs);
753
760
  if (activated.kind !== "value") {
754
761
  if (activated.kind === "timeout") controller.abort();
762
+ if (activated.kind === "failed") hookError = activated.error;
755
763
  outcome = activated.kind === "timeout" ? "timed-out" : activated.kind === "cancelled" ? "cancelled" : "failed";
756
764
  } else {
757
765
  const handler = activated.value as { observe?: unknown };
@@ -764,15 +772,22 @@ export class AgentToolExecutor {
764
772
  Math.max(1, context.deadlineUnixMs - Date.now()),
765
773
  );
766
774
  if (observed.kind === "timeout" || observed.kind === "value") controller.abort();
775
+ if (observed.kind === "failed") hookError = observed.error;
767
776
  outcome = observed.kind === "value" ? "recorded" : observed.kind === "timeout" ? "timed-out" : observed.kind === "cancelled" ? "cancelled" : "failed";
768
777
  }
769
778
  }
770
- } catch {
779
+ } catch (error) {
780
+ hookError = error;
771
781
  outcome = controller.signal.aborted && options.signal?.aborted ? "cancelled" : "failed";
772
782
  } finally {
773
783
  detach?.();
774
- 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
+ }
775
789
  }
790
+ this.logHookFailure({ kind: registration.kind, id: registration.id, version: registration.version, outcome }, { id: settlement.callId, name: settlement.toolName }, settlement.agentName, hookError);
776
791
  await this.recordObserverHook(settlement, registration, record, outcome, timeoutMs);
777
792
  }
778
793
  }
@@ -1086,6 +1101,7 @@ export class AgentToolExecutor {
1086
1101
  tier: isPlatform(record) ? "platform" : "application", order: registration.order ?? 0,
1087
1102
  outcome: "failed",
1088
1103
  };
1104
+ let hookError: unknown;
1089
1105
  const configuredTimeoutMs = registration.timeoutMs ?? 5_000;
1090
1106
  const remainingMs = executionState.deadlineUnixMs === undefined
1091
1107
  ? undefined : Math.max(0, executionState.deadlineUnixMs - Date.now());
@@ -1102,6 +1118,7 @@ export class AgentToolExecutor {
1102
1118
  const activated = await awaitHook(record.activation.activateAsync(scope), controller.signal, timeoutMs);
1103
1119
  if (activated.kind !== "value") {
1104
1120
  if (activated.kind === "timeout") controller.abort();
1121
+ if (activated.kind === "failed") hookError = activated.error;
1105
1122
  projection = { ...projection, outcome: activated.kind === "timeout" ? "timed-out" : activated.kind === "cancelled" ? "cancelled" : "failed" };
1106
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);
1107
1124
  } else {
@@ -1113,27 +1130,31 @@ export class AgentToolExecutor {
1113
1130
  if (outcome.kind === "timeout") controller.abort();
1114
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); }
1115
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); }
1116
- 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); }
1117
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); }
1118
1135
  else {
1119
1136
  const decision = outcome.value;
1120
- 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 } : {}) };
1121
- 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);
1122
1140
  }
1123
1141
  }
1124
1142
  }
1125
1143
  }
1126
- } catch {
1144
+ } catch (error) {
1145
+ hookError = error;
1127
1146
  projection = { ...projection, outcome: "failed" };
1128
1147
  terminal = this.errorResult(call, options.signal?.aborted ? "TOOL_HOOK_CANCELLED" : "TOOL_HOOK_FAILED", "Agent Tool enforcement hook failed.", metadata, startedAt);
1129
1148
  } finally {
1130
1149
  detach?.();
1131
1150
  const disposeError = await this.disposeScope(scope, this.scopeDisposeTimeoutMs);
1132
1151
  if (disposeError !== undefined) {
1152
+ hookError ??= disposeError;
1133
1153
  projection = { ...projection, outcome: "failed" };
1134
1154
  terminal = this.errorResult(call, "TOOL_HOOK_FAILED", "Agent Tool enforcement hook disposal failed.", metadata, startedAt);
1135
1155
  }
1136
1156
  }
1157
+ this.logHookFailure(projection, call, agentName, hookError);
1137
1158
  const evidenceRemainingMs = executionState.deadlineUnixMs === undefined
1138
1159
  ? registration.timeoutMs ?? 5_000
1139
1160
  : Math.max(0, Math.min(registration.timeoutMs ?? 5_000, executionState.deadlineUnixMs - Date.now()));
@@ -2040,6 +2061,20 @@ export class AgentToolExecutor {
2040
2061
  }
2041
2062
 
2042
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
+
2043
2078
  /**
2044
2079
  * A tool's own failure: a ToolError message reaches the model as written;
2045
2080
  * anything else is replaced by `fallback` and logged with its stack.
@@ -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; }
@@ -108,6 +108,13 @@ details stay out of the LLM — and goes to the application log with its stack
108
108
  call failed`. The LLM connectors pass the application logger; a runtime created
109
109
  by hand takes `options.logger`.
110
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
+
111
118
  Retries are opt-in. `write` and `external` tools need an idempotency key before
112
119
  the executor repeats them. Runtime-generated keys include both the invocation id
113
120
  and the unique model call id; `toolExecution.idempotencyKeyForCall` gives a
@@ -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.2" }, 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,6 @@
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
4
  import { LOGGER } from "../../kernel/logging/Logger";
5
5
  import { reader, requireValue } from "../connectorConfig";
6
6
  import { InfraError, type InfraConnector } from "../InfraConnector";
@@ -66,6 +66,19 @@ export const agentRuntimeProviders: readonly ProviderDefinition[] = Object.freez
66
66
  const options = services.has(AGENT_RUNTIME_OPTIONS) ? services.resolve(AGENT_RUNTIME_OPTIONS) : {};
67
67
  return new AgentRuntime(services, model, { ...options, logger: options.logger ?? services.tryResolve(LOGGER) });
68
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
+ }))),
69
82
  ]);
70
83
 
71
84
  export function llmConnect<T extends LlmConfigShape>(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.99.2",
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",