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.
- package/README.md +17 -0
- package/dist/internal/harness/index.d.ts +3 -0
- package/dist/internal/harness/index.js +3 -0
- package/dist/internal/harness/message.d.ts +1 -2
- package/dist/internal/harness/message.js +45 -19
- package/dist/internal/harness/report-diff.d.ts +46 -0
- package/dist/internal/harness/report-diff.js +62 -0
- package/dist/internal/harness/report.d.ts +2 -0
- package/dist/internal/harness/report.js +10 -0
- package/dist/internal/harness/shadow.d.ts +9 -2
- package/dist/internal/harness/shadow.js +5 -1
- package/dist/internal/harness/task-supervisor.js +21 -6
- package/dist/internal/harness/tool-delivery.d.ts +17 -0
- package/dist/internal/harness/tool-delivery.js +53 -0
- package/dist/internal/harness/tool-policy.d.ts +52 -0
- package/dist/internal/harness/tool-policy.js +22 -0
- package/dist/internal/harness/tool-runtime.d.ts +4 -0
- package/dist/internal/harness/tool-runtime.js +82 -1
- package/dist/internal/harness/tools.d.ts +22 -1
- package/dist/internal/harness/voice-session.d.ts +9 -1
- package/dist/internal/harness/voice-session.js +22 -2
- package/dist/internal/harness/xo.d.ts +13 -0
- package/dist/internal/harness/xo.js +8 -0
- package/dist/internal/protocol/events.d.ts +72 -0
- package/dist/internal/protocol/events.js +14 -1
- package/dist/internal/protocol/provider.d.ts +84 -2
- package/dist/internal/protocol/provider.js +51 -3
- package/dist/internal/provider/grok-voice.d.ts +1 -0
- package/dist/internal/provider/grok-voice.js +4 -0
- package/dist/internal/provider/openai-realtime.d.ts +10 -1
- package/dist/internal/provider/openai-realtime.js +26 -1
- package/dist/internal/provider/realtime-session.d.ts +6 -0
- package/dist/internal/provider/realtime-session.js +267 -32
- package/dist/internal/provider-fake/replay-voice-provider.d.ts +6 -2
- package/dist/internal/provider-fake/replay-voice-provider.js +13 -3
- package/dist/internal/skills/index.d.ts +2 -0
- package/dist/internal/skills/index.js +2 -0
- package/dist/internal/skills/node.d.ts +7 -0
- package/dist/internal/skills/node.js +99 -0
- package/dist/internal/skills/skill.d.ts +17 -0
- package/dist/internal/skills/skill.js +42 -0
- package/dist/internal/skills/tools.d.ts +7 -0
- package/dist/internal/skills/tools.js +82 -0
- package/dist/internal/storage/memory.d.ts +2 -0
- package/dist/internal/storage/memory.js +1 -0
- package/dist/internal/tools-openai/delegate-conversation.d.ts +12 -0
- package/dist/internal/tools-openai/delegate-conversation.js +66 -0
- package/dist/internal/tools-openai/index.d.ts +24 -0
- package/dist/internal/tools-openai/index.js +119 -0
- package/dist/internal/tools-openai/responses.d.ts +31 -0
- package/dist/internal/tools-openai/responses.js +146 -0
- package/dist/skills-node.d.ts +1 -0
- package/dist/skills-node.js +1 -0
- package/dist/skills.d.ts +1 -0
- package/dist/skills.js +1 -0
- package/dist/storage-memory.d.ts +1 -0
- package/dist/storage-memory.js +1 -0
- package/dist/tools-openai.d.ts +1 -0
- package/dist/tools-openai.js +1 -0
- 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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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";
|