xo-harness 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +17 -0
  2. package/dist/internal/harness/index.d.ts +3 -0
  3. package/dist/internal/harness/index.js +3 -0
  4. package/dist/internal/harness/message.d.ts +1 -2
  5. package/dist/internal/harness/message.js +45 -19
  6. package/dist/internal/harness/report-diff.d.ts +46 -0
  7. package/dist/internal/harness/report-diff.js +62 -0
  8. package/dist/internal/harness/report.d.ts +2 -0
  9. package/dist/internal/harness/report.js +10 -0
  10. package/dist/internal/harness/shadow.d.ts +9 -2
  11. package/dist/internal/harness/shadow.js +5 -1
  12. package/dist/internal/harness/task-supervisor.js +21 -6
  13. package/dist/internal/harness/tool-delivery.d.ts +17 -0
  14. package/dist/internal/harness/tool-delivery.js +53 -0
  15. package/dist/internal/harness/tool-policy.d.ts +52 -0
  16. package/dist/internal/harness/tool-policy.js +22 -0
  17. package/dist/internal/harness/tool-runtime.d.ts +4 -0
  18. package/dist/internal/harness/tool-runtime.js +82 -1
  19. package/dist/internal/harness/tools.d.ts +22 -1
  20. package/dist/internal/harness/voice-session.d.ts +9 -1
  21. package/dist/internal/harness/voice-session.js +22 -2
  22. package/dist/internal/harness/xo.d.ts +13 -0
  23. package/dist/internal/harness/xo.js +8 -0
  24. package/dist/internal/protocol/events.d.ts +72 -0
  25. package/dist/internal/protocol/events.js +14 -1
  26. package/dist/internal/protocol/provider.d.ts +84 -2
  27. package/dist/internal/protocol/provider.js +51 -3
  28. package/dist/internal/provider/grok-voice.d.ts +1 -0
  29. package/dist/internal/provider/grok-voice.js +4 -0
  30. package/dist/internal/provider/openai-realtime.d.ts +10 -1
  31. package/dist/internal/provider/openai-realtime.js +26 -1
  32. package/dist/internal/provider/realtime-session.d.ts +6 -0
  33. package/dist/internal/provider/realtime-session.js +267 -32
  34. package/dist/internal/provider-fake/replay-voice-provider.d.ts +6 -2
  35. package/dist/internal/provider-fake/replay-voice-provider.js +13 -3
  36. package/dist/internal/skills/index.d.ts +2 -0
  37. package/dist/internal/skills/index.js +2 -0
  38. package/dist/internal/skills/node.d.ts +7 -0
  39. package/dist/internal/skills/node.js +99 -0
  40. package/dist/internal/skills/skill.d.ts +17 -0
  41. package/dist/internal/skills/skill.js +42 -0
  42. package/dist/internal/skills/tools.d.ts +7 -0
  43. package/dist/internal/skills/tools.js +82 -0
  44. package/dist/internal/storage/memory.d.ts +2 -0
  45. package/dist/internal/storage/memory.js +1 -0
  46. package/dist/internal/tools-openai/delegate-conversation.d.ts +12 -0
  47. package/dist/internal/tools-openai/delegate-conversation.js +66 -0
  48. package/dist/internal/tools-openai/index.d.ts +24 -0
  49. package/dist/internal/tools-openai/index.js +119 -0
  50. package/dist/internal/tools-openai/responses.d.ts +31 -0
  51. package/dist/internal/tools-openai/responses.js +146 -0
  52. package/dist/skills-node.d.ts +1 -0
  53. package/dist/skills-node.js +1 -0
  54. package/dist/skills.d.ts +1 -0
  55. package/dist/skills.js +1 -0
  56. package/dist/storage-memory.d.ts +1 -0
  57. package/dist/storage-memory.js +1 -0
  58. package/dist/tools-openai.d.ts +1 -0
  59. package/dist/tools-openai.js +1 -0
  60. package/package.json +18 -1
@@ -1,4 +1,11 @@
1
1
  import { ToolOutcomeSchema, } from "../protocol/index.js";
2
+ import { z } from "zod";
3
+ import { boundToolOutcomeForDelivery } from "./tool-delivery.js";
4
+ import { toolDenialError } from "./tool-policy.js";
5
+ const ToolAdmissionSchema = z.discriminatedUnion("decision", [
6
+ z.object({ decision: z.literal("allow") }),
7
+ z.object({ decision: z.literal("deny"), reason: z.string().trim().min(1) }),
8
+ ]);
2
9
  export class ToolRuntime {
3
10
  #tools;
4
11
  #tasks;
@@ -6,6 +13,9 @@ export class ToolRuntime {
6
13
  #resultSink;
7
14
  #signal;
8
15
  #flush;
16
+ #policy;
17
+ #history;
18
+ #maxResultBytes;
9
19
  #inFlight = new Set();
10
20
  #terminal = new Set();
11
21
  constructor(options) {
@@ -15,14 +25,27 @@ export class ToolRuntime {
15
25
  this.#resultSink = options.resultSink;
16
26
  this.#signal = options.signal;
17
27
  this.#flush = options.flush;
28
+ this.#policy = options.policy;
29
+ this.#history = options.history;
30
+ this.#maxResultBytes = options.maxResultBytes;
18
31
  }
19
32
  async execute(call) {
20
33
  if (this.#inFlight.has(call.callId) || this.#terminal.has(call.callId))
21
34
  return;
22
35
  this.#inFlight.add(call.callId);
23
36
  try {
37
+ const admission = await this.#admit(call);
38
+ // Cancellation can follow a resolved admission before this continuation runs.
39
+ if (admission === undefined || this.#signal.aborted)
40
+ return;
41
+ if (admission.decision === "deny") {
42
+ await this.#settleDenied(call, admission.reason);
43
+ return;
44
+ }
24
45
  await this.#record({ type: "tool.started", callId: call.callId, name: call.name });
25
46
  await this.#flush();
47
+ if (this.#signal.aborted)
48
+ return;
26
49
  const tool = this.#tools.get(call.name);
27
50
  if (!tool) {
28
51
  await this.#settleFailure(call.callId, `Tool not found: ${call.name}`);
@@ -31,6 +54,8 @@ export class ToolRuntime {
31
54
  let outcome;
32
55
  try {
33
56
  const input = await tool.input.parseAsync(call.arguments);
57
+ if (this.#signal.aborted)
58
+ return;
34
59
  outcome = ToolOutcomeSchema.parse(await this.#run(tool, input, call.callId));
35
60
  }
36
61
  catch (error) {
@@ -43,9 +68,46 @@ export class ToolRuntime {
43
68
  this.#inFlight.delete(call.callId);
44
69
  }
45
70
  }
71
+ async #admit(call) {
72
+ if (this.#signal.aborted)
73
+ return undefined;
74
+ const annotations = this.#tools.get(call.name)?.annotations;
75
+ const decision = (async () => {
76
+ try {
77
+ return ToolAdmissionSchema.parse(await this.#policy.admit({
78
+ call,
79
+ ...(annotations === undefined ? {} : { annotations }),
80
+ signal: this.#signal,
81
+ history: this.#history,
82
+ }));
83
+ }
84
+ catch (error) {
85
+ // A broken policy must not grant access; the gate fails closed with the reason.
86
+ return { decision: "deny", reason: `Tool policy error: ${errorMessage(error)}` };
87
+ }
88
+ })();
89
+ // Session close must never wait on a stalled policy (e.g. an unanswered approval
90
+ // round-trip). Unlike a running tool body, nothing durable has begun yet, so the
91
+ // aborted call is simply left unterminated — the same shape a lost process leaves.
92
+ return raceWithAbort(decision, this.#signal);
93
+ }
94
+ async #settleDenied(call, reason) {
95
+ this.#assertNotTerminal(call.callId);
96
+ this.#terminal.add(call.callId);
97
+ try {
98
+ await this.#record({ type: "tool.denied", callId: call.callId, name: call.name, reason });
99
+ }
100
+ catch (error) {
101
+ this.#terminal.delete(call.callId);
102
+ throw error;
103
+ }
104
+ await this.#flush();
105
+ await this.#deliver(call.callId, { type: "failed", error: toolDenialError(reason) });
106
+ }
46
107
  async #run(tool, input, callId) {
47
108
  return tool.execute(input, {
48
109
  signal: this.#signal,
110
+ maxResultBytes: this.#maxResultBytes,
49
111
  tasks: this.#tasks.forCall(callId),
50
112
  report: async (update) => {
51
113
  await this.#record({ type: "tool.progress", callId, update });
@@ -80,7 +142,11 @@ export class ToolRuntime {
80
142
  }
81
143
  async #deliver(callId, outcome) {
82
144
  try {
83
- await this.#resultSink.deliver({ callId, outcome });
145
+ // The log keeps the full outcome; only the provider-bound copy is bounded.
146
+ await this.#resultSink.deliver({
147
+ callId,
148
+ outcome: boundToolOutcomeForDelivery(outcome, this.#maxResultBytes),
149
+ });
84
150
  }
85
151
  catch (error) {
86
152
  await this.#record({ type: "tool.delivery_failed", callId, error: errorMessage(error) });
@@ -100,6 +166,21 @@ export class ToolRuntime {
100
166
  }
101
167
  }
102
168
  }
169
+ function raceWithAbort(work, signal) {
170
+ if (signal.aborted)
171
+ return Promise.resolve(undefined);
172
+ return new Promise((resolve, reject) => {
173
+ const onAbort = () => resolve(undefined);
174
+ signal.addEventListener("abort", onAbort, { once: true });
175
+ work.then((value) => {
176
+ signal.removeEventListener("abort", onAbort);
177
+ resolve(value);
178
+ }, (error) => {
179
+ signal.removeEventListener("abort", onAbort);
180
+ reject(error instanceof Error ? error : new Error(String(error)));
181
+ });
182
+ });
183
+ }
103
184
  function errorMessage(error) {
104
185
  return error instanceof Error ? error.message : String(error);
105
186
  }
@@ -1,22 +1,43 @@
1
1
  import { type JsonValue, type ProviderToolDefinition, type ToolOutcome } from "../protocol/index.js";
2
2
  import { z } from "zod";
3
3
  export interface BackgroundTaskContext {
4
+ /** Runners must release their resources and settle promptly when aborted; close waits for settlement. */
4
5
  readonly signal: AbortSignal;
5
6
  report(update: JsonValue): Promise<void>;
6
7
  }
7
8
  export type BackgroundTaskRunner = (context: BackgroundTaskContext) => Promise<JsonValue>;
9
+ export interface BackgroundTaskOptions {
10
+ /** Release resources reserved before start if the task is cancelled before its runner activates. */
11
+ onPendingCancel?: () => void;
12
+ }
8
13
  export interface BackgroundTaskLauncher {
9
- start(run: BackgroundTaskRunner): Promise<string>;
14
+ start(run: BackgroundTaskRunner, options?: BackgroundTaskOptions): Promise<string>;
10
15
  }
11
16
  export interface ToolExecutionContext {
17
+ /** Session lifetime signal. Executions must settle on abort; arbitrary JavaScript cannot be forcibly stopped. */
12
18
  readonly signal: AbortSignal;
19
+ /** JSON payload budget for provider delivery; tools can use it to size recoverable pages. */
20
+ readonly maxResultBytes?: number;
13
21
  readonly tasks: BackgroundTaskLauncher;
14
22
  report(update: JsonValue): Promise<void>;
15
23
  }
24
+ /**
25
+ * Behavior hints consumed by tool admission policies; never sent to providers. The two
26
+ * hints a permission decision actually turns on, named as in MCP `ToolAnnotations` so
27
+ * MCP-discovered tools map onto them verbatim. Hints, not guarantees — a policy should
28
+ * treat a missing hint conservatively (absent `readOnlyHint` means "may mutate").
29
+ */
30
+ export interface ToolAnnotations {
31
+ /** The tool observes without mutating its environment. */
32
+ readonly readOnlyHint?: boolean;
33
+ /** The tool may perform destructive updates (sends, deletes, spends). */
34
+ readonly destructiveHint?: boolean;
35
+ }
16
36
  export interface VoiceTool<Schema extends z.ZodType = z.ZodType> {
17
37
  readonly name: string;
18
38
  readonly description: string;
19
39
  readonly input: Schema;
40
+ readonly annotations?: ToolAnnotations;
20
41
  execute(input: z.output<Schema>, context: ToolExecutionContext): ToolOutcome | Promise<ToolOutcome>;
21
42
  }
22
43
  export declare function defineTool<const Schema extends z.ZodType>(tool: VoiceTool<Schema>): VoiceTool<Schema>;
@@ -1,7 +1,8 @@
1
1
  import { type AgentInput, type AssistantTurnRequest, type AudioChunk, type HarnessEvent, type OutputModality, type PlayoutProgress } from "../protocol/index.js";
2
2
  import type { VoiceProvider } from "../provider/index.js";
3
3
  import type { EventStore } from "../storage/index.js";
4
- import type { ToolRegistry } from "./tools.js";
4
+ import { type ToolPolicy } from "./tool-policy.js";
5
+ import { ToolRegistry } from "./tools.js";
5
6
  export interface CreateVoiceSessionOptions {
6
7
  sessionId: string;
7
8
  provider: VoiceProvider;
@@ -9,6 +10,13 @@ export interface CreateVoiceSessionOptions {
9
10
  signal?: AbortSignal;
10
11
  store: EventStore;
11
12
  tools: ToolRegistry;
13
+ /** Admits or refuses model-requested tool calls before execution. Default: allow all. */
14
+ toolPolicy?: ToolPolicy;
15
+ /**
16
+ * UTF-8 JSON byte budget for a completed value or error string, excluding provider envelopes.
17
+ * An integer of at least 128, or Infinity (the default). The log keeps the full outcome.
18
+ */
19
+ maxToolResultBytes?: number;
12
20
  instructions?: string;
13
21
  /** What the model may produce this session; ["text"] disables audio output. Default: audio. */
14
22
  outputModalities?: readonly OutputModality[];
@@ -1,7 +1,10 @@
1
1
  import { AgentInputSchema, AssistantTurnRequestSchema, AudioChunkSchema, agentInputToText, MessageInputTextSchema, PlayoutProgressSchema, ProviderEventSchema, } from "../protocol/index.js";
2
2
  import { ReplayEventStream } from "./event-stream.js";
3
3
  import { TaskSupervisor } from "./task-supervisor.js";
4
+ import { DEFAULT_MAX_TOOL_RESULT_BYTES, validateMaxToolResultBytes } from "./tool-delivery.js";
5
+ import { allowAllToolPolicy } from "./tool-policy.js";
4
6
  import { ToolRuntime } from "./tool-runtime.js";
7
+ import { ToolRegistry } from "./tools.js";
5
8
  export class VoiceSession {
6
9
  id;
7
10
  #providerSession;
@@ -47,9 +50,17 @@ export class VoiceSession {
47
50
  },
48
51
  signal: this.#controller.signal,
49
52
  flush: () => this.#store.flush(this.id),
53
+ policy: options.toolPolicy ?? allowAllToolPolicy,
54
+ history: () => this.#store.list(this.id),
55
+ maxResultBytes: options.maxToolResultBytes ?? DEFAULT_MAX_TOOL_RESULT_BYTES,
50
56
  });
51
57
  }
52
58
  static async create(options) {
59
+ const maxToolResultBytes = options.maxToolResultBytes ?? DEFAULT_MAX_TOOL_RESULT_BYTES;
60
+ validateMaxToolResultBytes(maxToolResultBytes);
61
+ // Registrations made after startup begins belong to future sessions. Execution
62
+ // must use the same catalog that this session advertises to its provider.
63
+ const tools = new ToolRegistry(options.tools.list());
53
64
  const existingEvents = await options.store.list(options.sessionId);
54
65
  if (existingEvents.length > 0) {
55
66
  throw new Error(`Session id already exists: ${options.sessionId}`);
@@ -80,7 +91,7 @@ export class VoiceSession {
80
91
  providerSession = await options.provider.createSession({
81
92
  sessionId: options.sessionId,
82
93
  signal: providerController.signal,
83
- tools: options.tools.definitions(),
94
+ tools: tools.definitions(),
84
95
  ...(options.instructions === undefined ? {} : { instructions: options.instructions }),
85
96
  ...(options.outputModalities === undefined ? {} : { outputModalities: options.outputModalities }),
86
97
  });
@@ -97,7 +108,7 @@ export class VoiceSession {
97
108
  await recordFailedCreation(options.store, options.sessionId, startedAt, "owner_revoked");
98
109
  throw options.signal.reason ?? new Error("Session owner revoked during startup");
99
110
  }
100
- const session = new VoiceSession(options, providerSession, startedAt, startedEvent);
111
+ const session = new VoiceSession({ ...options, tools, maxToolResultBytes }, providerSession, startedAt, startedEvent);
101
112
  session.#controller.signal.addEventListener("abort", () => providerController.abort(session.#controller.signal.reason), {
102
113
  once: true,
103
114
  });
@@ -228,6 +239,9 @@ export class VoiceSession {
228
239
  // error behind per-chunk persistence.
229
240
  this.#trackRecord(this.#record({ type: "audio.output", chunk: event.chunk }));
230
241
  break;
242
+ case "audio.interrupted":
243
+ this.#trackRecord(this.#record({ type: "audio.interrupted", streamId: event.streamId }));
244
+ break;
231
245
  case "transcript":
232
246
  await this.#record({
233
247
  type: "transcript",
@@ -248,6 +262,12 @@ export class VoiceSession {
248
262
  await this.#record({ type: "tool.requested", call: event.call });
249
263
  this.#trackToolExecution(this.#toolRuntime.execute(event.call));
250
264
  break;
265
+ case "response.state":
266
+ await this.#record(event);
267
+ break;
268
+ case "diagnostic":
269
+ await this.#record({ type: "provider.diagnostic", diagnostic: event.diagnostic });
270
+ break;
251
271
  case "usage": {
252
272
  const { type: _type, ...usage } = event;
253
273
  await this.#record({ type: "usage", ...usage });
@@ -1,17 +1,29 @@
1
1
  import type { OutputModality } from "../protocol/index.js";
2
2
  import type { VoiceProvider } from "../provider/index.js";
3
3
  import type { EventStore } from "../storage/index.js";
4
+ import type { ToolPolicy } from "./tool-policy.js";
4
5
  import { ToolRegistry, type VoiceTool } from "./tools.js";
5
6
  import { VoiceSession } from "./voice-session.js";
6
7
  export interface XOOptions {
7
8
  store: EventStore;
8
9
  tools?: readonly VoiceTool[];
10
+ /** Admits or refuses model-requested tool calls before execution. Default: allow all. */
11
+ toolPolicy?: ToolPolicy;
12
+ /**
13
+ * UTF-8 JSON byte budget for a completed value or error string, excluding provider envelopes.
14
+ * An integer of at least 128, or Infinity (the default). The log keeps the full outcome.
15
+ */
16
+ maxToolResultBytes?: number;
9
17
  }
10
18
  export interface StartSessionOptions {
11
19
  provider: VoiceProvider;
12
20
  /** Cancels provider startup and closes the live session when its owner is revoked. */
13
21
  signal?: AbortSignal;
14
22
  sessionId?: string;
23
+ /** Overrides the harness-level tool policy for this session. */
24
+ toolPolicy?: ToolPolicy;
25
+ /** Overrides the harness-level delivery budget for this session. */
26
+ maxToolResultBytes?: number;
15
27
  instructions?: string;
16
28
  /** What the model may produce this session; ["text"] disables audio output. Default: audio. */
17
29
  outputModalities?: readonly OutputModality[];
@@ -19,6 +31,7 @@ export interface StartSessionOptions {
19
31
  maxDurationMs?: number;
20
32
  }
21
33
  export declare class XO {
34
+ #private;
22
35
  readonly store: EventStore;
23
36
  readonly tools: ToolRegistry;
24
37
  constructor(options: XOOptions);
@@ -3,20 +3,28 @@ import { VoiceSession } from "./voice-session.js";
3
3
  export class XO {
4
4
  store;
5
5
  tools;
6
+ #toolPolicy;
7
+ #maxToolResultBytes;
6
8
  constructor(options) {
7
9
  this.store = options.store;
8
10
  this.tools = new ToolRegistry(options.tools);
11
+ this.#toolPolicy = options.toolPolicy;
12
+ this.#maxToolResultBytes = options.maxToolResultBytes;
9
13
  }
10
14
  registerTool(tool) {
11
15
  this.tools.register(tool);
12
16
  }
13
17
  startSession(options) {
18
+ const toolPolicy = options.toolPolicy ?? this.#toolPolicy;
19
+ const maxToolResultBytes = options.maxToolResultBytes ?? this.#maxToolResultBytes;
14
20
  return VoiceSession.create({
15
21
  sessionId: options.sessionId ?? crypto.randomUUID(),
16
22
  provider: options.provider,
17
23
  ...(options.signal === undefined ? {} : { signal: options.signal }),
18
24
  store: this.store,
19
25
  tools: this.tools,
26
+ ...(toolPolicy === undefined ? {} : { toolPolicy }),
27
+ ...(maxToolResultBytes === undefined ? {} : { maxToolResultBytes }),
20
28
  ...(options.instructions === undefined ? {} : { instructions: options.instructions }),
21
29
  ...(options.outputModalities === undefined ? {} : { outputModalities: options.outputModalities }),
22
30
  ...(options.maxDurationMs === undefined ? {} : { maxDurationMs: options.maxDurationMs }),
@@ -24,6 +24,7 @@ export declare const HarnessEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
24
24
  supportsToolCalls: z.ZodBoolean;
25
25
  supportsAsyncContext: z.ZodBoolean;
26
26
  supportsPlayoutAcknowledgements: z.ZodBoolean;
27
+ reportsResponseState: z.ZodOptional<z.ZodBoolean>;
27
28
  }, z.core.$strip>;
28
29
  instructions: z.ZodOptional<z.ZodString>;
29
30
  outputModalities: z.ZodOptional<z.ZodArray<z.ZodEnum<{
@@ -49,6 +50,55 @@ export declare const HarnessEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
49
50
  correlationId: z.ZodOptional<z.ZodString>;
50
51
  causationId: z.ZodOptional<z.ZodString>;
51
52
  type: z.ZodLiteral<"provider.ready">;
53
+ }, z.core.$strip>, z.ZodObject<{
54
+ id: z.ZodString;
55
+ sessionId: z.ZodString;
56
+ sequence: z.ZodInt;
57
+ recordedAtMs: z.ZodNumber;
58
+ sessionTimeMs: z.ZodNumber;
59
+ correlationId: z.ZodOptional<z.ZodString>;
60
+ causationId: z.ZodOptional<z.ZodString>;
61
+ type: z.ZodLiteral<"response.state">;
62
+ state: z.ZodEnum<{
63
+ idle: "idle";
64
+ pending: "pending";
65
+ }>;
66
+ }, z.core.$strip>, z.ZodObject<{
67
+ id: z.ZodString;
68
+ sessionId: z.ZodString;
69
+ sequence: z.ZodInt;
70
+ recordedAtMs: z.ZodNumber;
71
+ sessionTimeMs: z.ZodNumber;
72
+ correlationId: z.ZodOptional<z.ZodString>;
73
+ causationId: z.ZodOptional<z.ZodString>;
74
+ type: z.ZodLiteral<"provider.diagnostic">;
75
+ diagnostic: z.ZodDiscriminatedUnion<[z.ZodObject<{
76
+ kind: z.ZodLiteral<"session">;
77
+ model: z.ZodOptional<z.ZodString>;
78
+ }, z.core.$strip>, z.ZodObject<{
79
+ kind: z.ZodLiteral<"speech">;
80
+ phase: z.ZodEnum<{
81
+ started: "started";
82
+ stopped: "stopped";
83
+ }>;
84
+ streamId: z.ZodOptional<z.ZodString>;
85
+ audioOffsetMs: z.ZodOptional<z.ZodNumber>;
86
+ }, z.core.$strip>, z.ZodObject<{
87
+ kind: z.ZodLiteral<"response">;
88
+ phase: z.ZodEnum<{
89
+ completed: "completed";
90
+ started: "started";
91
+ }>;
92
+ responseId: z.ZodOptional<z.ZodString>;
93
+ status: z.ZodOptional<z.ZodString>;
94
+ reason: z.ZodOptional<z.ZodString>;
95
+ code: z.ZodOptional<z.ZodString>;
96
+ outputs: z.ZodOptional<z.ZodArray<z.ZodObject<{
97
+ streamId: z.ZodOptional<z.ZodString>;
98
+ type: z.ZodString;
99
+ contentTypes: z.ZodArray<z.ZodString>;
100
+ }, z.core.$strip>>>;
101
+ }, z.core.$strip>], "kind">;
52
102
  }, z.core.$strip>, z.ZodObject<{
53
103
  id: z.ZodString;
54
104
  sessionId: z.ZodString;
@@ -138,6 +188,16 @@ export declare const HarnessEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
138
188
  startSample: z.ZodInt;
139
189
  data: z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
140
190
  }, z.core.$strip>;
191
+ }, z.core.$strip>, z.ZodObject<{
192
+ id: z.ZodString;
193
+ sessionId: z.ZodString;
194
+ sequence: z.ZodInt;
195
+ recordedAtMs: z.ZodNumber;
196
+ sessionTimeMs: z.ZodNumber;
197
+ correlationId: z.ZodOptional<z.ZodString>;
198
+ causationId: z.ZodOptional<z.ZodString>;
199
+ type: z.ZodLiteral<"audio.interrupted">;
200
+ streamId: z.ZodString;
141
201
  }, z.core.$strip>, z.ZodObject<{
142
202
  role: z.ZodEnum<{
143
203
  assistant: "assistant";
@@ -209,6 +269,18 @@ export declare const HarnessEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
209
269
  name: z.ZodString;
210
270
  arguments: z.ZodRecord<z.ZodString, z.ZodUnknown>;
211
271
  }, z.core.$strip>;
272
+ }, z.core.$strip>, z.ZodObject<{
273
+ id: z.ZodString;
274
+ sessionId: z.ZodString;
275
+ sequence: z.ZodInt;
276
+ recordedAtMs: z.ZodNumber;
277
+ sessionTimeMs: z.ZodNumber;
278
+ correlationId: z.ZodOptional<z.ZodString>;
279
+ causationId: z.ZodOptional<z.ZodString>;
280
+ type: z.ZodLiteral<"tool.denied">;
281
+ callId: z.ZodString;
282
+ name: z.ZodString;
283
+ reason: z.ZodString;
212
284
  }, z.core.$strip>, z.ZodObject<{
213
285
  id: z.ZodString;
214
286
  sessionId: z.ZodString;
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { AudioChunkSchema } from "./audio.js";
3
3
  import { InputPartSchema } from "./parts.js";
4
- import { OutputModalitySchema, ProviderCapabilitiesSchema, TranscriptDeltaPayloadSchema, TranscriptPayloadSchema, UsagePayloadSchema, } from "./provider.js";
4
+ import { OutputModalitySchema, ProviderCapabilitiesSchema, ProviderDiagnosticSchema, ResponseStateSchema, TranscriptDeltaPayloadSchema, TranscriptPayloadSchema, UsagePayloadSchema, } from "./provider.js";
5
5
  import { JsonValueSchema, ToolCallSchema, ToolOutcomeSchema } from "./tools.js";
6
6
  /** Bounds for a typed user message, shared by the event, the session API, and wire schemas. */
7
7
  export const MessageInputTextSchema = z.string().min(1).max(8_000);
@@ -34,6 +34,10 @@ export const HarnessEventSchema = z.discriminatedUnion("type", [
34
34
  EventBaseSchema.extend({ type: z.literal("session.ended"), reason: z.string().min(1) }),
35
35
  /** The provider accepted the session configuration; media may now flow. */
36
36
  EventBaseSchema.extend({ type: z.literal("provider.ready") }),
37
+ /** Optional provider generation state; never a gate on audio or background work. */
38
+ EventBaseSchema.extend({ type: z.literal("response.state"), state: ResponseStateSchema }),
39
+ /** Allow-listed provider observation for explaining response and speech behavior. */
40
+ EventBaseSchema.extend({ type: z.literal("provider.diagnostic"), diagnostic: ProviderDiagnosticSchema }),
37
41
  /** A host asked the ready provider to begin an assistant turn without fabricating user input. */
38
42
  EventBaseSchema.extend({ type: z.literal("assistant.turn.requested") }),
39
43
  /** The provider connection settled; the harness closes the session in response. */
@@ -58,6 +62,8 @@ export const HarnessEventSchema = z.discriminatedUnion("type", [
58
62
  EventBaseSchema.extend({ type: z.literal("audio.input"), chunk: AudioChunkSchema }),
59
63
  /** Model PCM as received; records are pipelined so bursts never stall the event pump. */
60
64
  EventBaseSchema.extend({ type: z.literal("audio.output"), chunk: AudioChunkSchema }),
65
+ /** User barge-in: clients must clear this output stream, including buffered audio. */
66
+ EventBaseSchema.extend({ type: z.literal("audio.interrupted"), streamId: z.string().min(1) }),
61
67
  /** Final text for either speaker — speech-to-text, or the model's text output in text mode. */
62
68
  EventBaseSchema.extend({ type: z.literal("transcript"), ...TranscriptPayloadSchema.shape }),
63
69
  /** Incremental assistant text streamed before the final transcript; live-render only. */
@@ -72,6 +78,13 @@ export const HarnessEventSchema = z.discriminatedUnion("type", [
72
78
  }),
73
79
  /** The model asked for a tool; recorded before execution begins. */
74
80
  EventBaseSchema.extend({ type: z.literal("tool.requested"), call: ToolCallSchema }),
81
+ /** The admission policy refused a requested tool; execution never began. Terminal for the call. */
82
+ EventBaseSchema.extend({
83
+ type: z.literal("tool.denied"),
84
+ callId: z.string().min(1),
85
+ name: z.string().min(1),
86
+ reason: z.string().min(1),
87
+ }),
75
88
  /** Local execution of a requested tool has begun. */
76
89
  EventBaseSchema.extend({
77
90
  type: z.literal("tool.started"),
@@ -9,6 +9,7 @@ export declare const ProviderCapabilitiesSchema: z.ZodObject<{
9
9
  supportsToolCalls: z.ZodBoolean;
10
10
  supportsAsyncContext: z.ZodBoolean;
11
11
  supportsPlayoutAcknowledgements: z.ZodBoolean;
12
+ reportsResponseState: z.ZodOptional<z.ZodBoolean>;
12
13
  }, z.core.$strip>;
13
14
  export type ProviderCapabilities = z.infer<typeof ProviderCapabilitiesSchema>;
14
15
  export declare const PlayoutProgressSchema: z.ZodObject<{
@@ -55,7 +56,7 @@ export declare const TranscriptPayloadSchema: z.ZodObject<{
55
56
  streamId: z.ZodOptional<z.ZodString>;
56
57
  }, z.core.$strip>;
57
58
  export type TranscriptPayload = z.infer<typeof TranscriptPayloadSchema>;
58
- /** Incremental assistant text as the model streams it; finalized by a `transcript`. */
59
+ /** Incremental text from either speaker; finalized by a matching `transcript`. */
59
60
  export declare const TranscriptDeltaPayloadSchema: z.ZodObject<{
60
61
  role: z.ZodEnum<{
61
62
  assistant: "assistant";
@@ -65,13 +66,91 @@ export declare const TranscriptDeltaPayloadSchema: z.ZodObject<{
65
66
  streamId: z.ZodOptional<z.ZodString>;
66
67
  }, z.core.$strip>;
67
68
  export type TranscriptDeltaPayload = z.infer<typeof TranscriptDeltaPayloadSchema>;
69
+ /** Generation state only: idle says nothing about buffered audio playback or background tasks. */
70
+ export declare const ResponseStateSchema: z.ZodEnum<{
71
+ idle: "idle";
72
+ pending: "pending";
73
+ }>;
74
+ export type ResponseState = z.infer<typeof ResponseStateSchema>;
75
+ /** Limits optional provider diagnostics so inspection artifacts stay bounded. */
76
+ export declare const PROVIDER_DIAGNOSTIC_STRING_MAX_LENGTH = 256;
77
+ export declare const PROVIDER_DIAGNOSTIC_MAX_OUTPUTS = 32;
78
+ export declare const PROVIDER_DIAGNOSTIC_MAX_CONTENT_TYPES = 16;
79
+ /**
80
+ * Bounded, allow-listed provider observations for diagnosing a silent turn. These
81
+ * facts are never used to schedule media or model work, and intentionally exclude
82
+ * vendor payloads, reasoning, credentials, and content text.
83
+ */
84
+ export declare const ProviderDiagnosticSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
85
+ kind: z.ZodLiteral<"session">;
86
+ model: z.ZodOptional<z.ZodString>;
87
+ }, z.core.$strip>, z.ZodObject<{
88
+ kind: z.ZodLiteral<"speech">;
89
+ phase: z.ZodEnum<{
90
+ started: "started";
91
+ stopped: "stopped";
92
+ }>;
93
+ streamId: z.ZodOptional<z.ZodString>;
94
+ audioOffsetMs: z.ZodOptional<z.ZodNumber>;
95
+ }, z.core.$strip>, z.ZodObject<{
96
+ kind: z.ZodLiteral<"response">;
97
+ phase: z.ZodEnum<{
98
+ completed: "completed";
99
+ started: "started";
100
+ }>;
101
+ responseId: z.ZodOptional<z.ZodString>;
102
+ status: z.ZodOptional<z.ZodString>;
103
+ reason: z.ZodOptional<z.ZodString>;
104
+ code: z.ZodOptional<z.ZodString>;
105
+ outputs: z.ZodOptional<z.ZodArray<z.ZodObject<{
106
+ streamId: z.ZodOptional<z.ZodString>;
107
+ type: z.ZodString;
108
+ contentTypes: z.ZodArray<z.ZodString>;
109
+ }, z.core.$strip>>>;
110
+ }, z.core.$strip>], "kind">;
111
+ export type ProviderDiagnostic = z.infer<typeof ProviderDiagnosticSchema>;
68
112
  /**
69
113
  * What a provider adapter yields to the harness. Deliberately minimal: adapters
70
- * translate vendor wire dialects into these eight shapes, and the harness turns them
114
+ * translate vendor wire dialects into these shapes, and the harness turns them
71
115
  * into HarnessEvents. Anything not expressible here does not exist to the harness.
72
116
  */
73
117
  export declare const ProviderEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
74
118
  type: z.ZodLiteral<"ready">;
119
+ }, z.core.$strip>, z.ZodObject<{
120
+ type: z.ZodLiteral<"response.state">;
121
+ state: z.ZodEnum<{
122
+ idle: "idle";
123
+ pending: "pending";
124
+ }>;
125
+ }, z.core.$strip>, z.ZodObject<{
126
+ type: z.ZodLiteral<"diagnostic">;
127
+ diagnostic: z.ZodDiscriminatedUnion<[z.ZodObject<{
128
+ kind: z.ZodLiteral<"session">;
129
+ model: z.ZodOptional<z.ZodString>;
130
+ }, z.core.$strip>, z.ZodObject<{
131
+ kind: z.ZodLiteral<"speech">;
132
+ phase: z.ZodEnum<{
133
+ started: "started";
134
+ stopped: "stopped";
135
+ }>;
136
+ streamId: z.ZodOptional<z.ZodString>;
137
+ audioOffsetMs: z.ZodOptional<z.ZodNumber>;
138
+ }, z.core.$strip>, z.ZodObject<{
139
+ kind: z.ZodLiteral<"response">;
140
+ phase: z.ZodEnum<{
141
+ completed: "completed";
142
+ started: "started";
143
+ }>;
144
+ responseId: z.ZodOptional<z.ZodString>;
145
+ status: z.ZodOptional<z.ZodString>;
146
+ reason: z.ZodOptional<z.ZodString>;
147
+ code: z.ZodOptional<z.ZodString>;
148
+ outputs: z.ZodOptional<z.ZodArray<z.ZodObject<{
149
+ streamId: z.ZodOptional<z.ZodString>;
150
+ type: z.ZodString;
151
+ contentTypes: z.ZodArray<z.ZodString>;
152
+ }, z.core.$strip>>>;
153
+ }, z.core.$strip>], "kind">;
75
154
  }, z.core.$strip>, z.ZodObject<{
76
155
  type: z.ZodLiteral<"audio.output">;
77
156
  chunk: z.ZodObject<{
@@ -83,6 +162,9 @@ export declare const ProviderEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
83
162
  startSample: z.ZodInt;
84
163
  data: z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
85
164
  }, z.core.$strip>;
165
+ }, z.core.$strip>, z.ZodObject<{
166
+ type: z.ZodLiteral<"audio.interrupted">;
167
+ streamId: z.ZodString;
86
168
  }, z.core.$strip>, z.ZodObject<{
87
169
  role: z.ZodEnum<{
88
170
  assistant: "assistant";