@vtxmacro/cli 2026.9.14 → 2026.9.17

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 CHANGED
@@ -288,6 +288,43 @@ schema plus `provenance` with `source: "external_agent"`, a stable
288
288
  `agent-assignment-release` only when intentionally ending the foreground Agent
289
289
  session; normal cadence is recorded with `agent-assignment-heartbeat`.
290
290
 
291
+ ## Exo foreground trading inference
292
+
293
+ Exo means [exoharness/exo](https://github.com/exoharness/exo). VTX runs a dedicated
294
+ Exo trading harness automatically in Provider and Main Agent modes. It is a
295
+ foreground integration on Linux or WSL, not a durable service or native Exo MCP
296
+ client. Keep the run command open; no separate keeper or manual job shuttling is
297
+ needed. Review and Screener remain Provider-only.
298
+
299
+ Prepare the supported Exo source revision and its upstream dependencies:
300
+
301
+ ```bash
302
+ git clone https://github.com/exoharness/exo.git
303
+ cd exo
304
+ git checkout c5c1963a3445417e64938c139da88edd9154075d
305
+ pnpm install --frozen-lockfile
306
+ cargo build --release -p exo
307
+ ```
308
+
309
+ Use a separate VTX instance and import its vendor key privately:
310
+
311
+ ```bash
312
+ vtx inference-host login --instance exo-1
313
+ vtx inference-host exo-login --provider openai --instance exo-1 < /private/path/provider.key
314
+ vtx inference-host doctor --adapter exo --exo-root /path/to/exo --instance exo-1 --json
315
+ vtx inference-host run --adapter exo --exo-root /path/to/exo --instance exo-1
316
+ ```
317
+
318
+ Use `--provider venice` instead to import a Venice key into its own instance.
319
+ The host advertises only the imported provider's qualified models and efforts.
320
+ Venice initially supports Gemma 4 31B IT with thinking disabled (`none`);
321
+ OpenAI supports the release-qualified Responses routes. VTX OAuth
322
+ remains in the VTX process and credential store. Exo receives the complete
323
+ Provider prompt or only assignment-scoped Agent tools, without stock shell,
324
+ self-modification, installed tools, or ambient conversation context. Interrupted
325
+ work with an unknown outcome is fenced rather than repeated. After stopping and
326
+ resolving pending work, remove the vendor key with `exo-logout --instance exo-1`.
327
+
291
328
  Claude Code, Antigravity, Gemini CLI, Kiro, Grok Build, Cursor, Amp, Auggie,
292
329
  Junie, Warp/Oz, Qwen Code, and OpenCode remain on the explicit foreground host
293
330
  path. Their current subscription routing,
@@ -0,0 +1,377 @@
1
+ /** Loaded by the pinned exoharness/exo TypeScript executor, never by the web app.
2
+ * The VTX broker owns OAuth and assignment authority. This module receives only
3
+ * a turn-scoped broker capability and the selected vendor credential.
4
+ */
5
+ import { join } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+
8
+ type Json = Record<string, unknown>;
9
+ type Message = { role: string; content: unknown };
10
+ type ToolCall = { toolCallId: string; request: { functionName: string; arguments: Json } };
11
+ type Context = {
12
+ exoharness: { current: { conversation: { getEvents(query: Json): Promise<{ events: unknown[] }> };
13
+ turn: { addEvents(events: unknown[]): Promise<{ latestEventId: string }> } } };
14
+ };
15
+ type Input = {
16
+ provider: 'openai' | 'venice';
17
+ mode: 'provider' | 'agent'; systemPrompt: string; userPrompt: string;
18
+ outputSchema: Json; decisionSchema: Json; dataContract: { id: string; input_schema: Json }[];
19
+ requestedModel: string; providerModel: string; reasoningEffort: string;
20
+ nativeReasoningEffort: string | null; maxRounds: number;
21
+ deadlineAtMs: number;
22
+ };
23
+ type ExoHarnessModule = {
24
+ materializePromptMessages(conversation: unknown, instructions: Message[]): Promise<Message[]>;
25
+ materializeEventsToMessages(events: unknown[]): Message[];
26
+ toolResultEvent(id: string, result: unknown): unknown;
27
+ };
28
+ type Runtime = {
29
+ complete(request: Json): Promise<Json>;
30
+ };
31
+ type ExoModelModule = {
32
+ ResponsesRuntime: new (options: { apiKey: string; baseURL: string }) => Runtime;
33
+ ChatCompletionsRuntime: new (options: { apiKey: string; baseURL: string }) => Runtime;
34
+ responseToLinguaEvents(response: Json): unknown[];
35
+ responseToolCalls(response: Json): ToolCall[];
36
+ };
37
+
38
+ const record = (value: unknown): Json => value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Json : {};
39
+ const count = (value: unknown): number => {
40
+ if (!Number.isSafeInteger(value) || Number(value) < 0) throw new Error('exo_usage_invalid');
41
+ return Number(value);
42
+ };
43
+ const rateLimitCode = async (response: Response): Promise<string> => {
44
+ if (!response.body) return 'exo_rate_limited';
45
+ const reader = response.body.getReader();
46
+ const chunks: Uint8Array[] = [];
47
+ let bytes = 0;
48
+ let timer: ReturnType<typeof setTimeout> | undefined;
49
+ const timeout = new Promise<never>((_resolve, reject) => { timer = setTimeout(() => reject(new Error('exo_error_body_timeout')), 1000); });
50
+ try {
51
+ for (;;) {
52
+ const part = await Promise.race([reader.read(), timeout]);
53
+ if (part.done) break;
54
+ bytes += part.value.byteLength;
55
+ if (bytes > 16_384) return 'exo_rate_limited';
56
+ chunks.push(part.value);
57
+ }
58
+ const error = record(record(JSON.parse(Buffer.concat(chunks).toString('utf8'))).error);
59
+ return error.type === 'insufficient_quota' || error.code === 'insufficient_quota'
60
+ ? 'exo_quota_exhausted' : 'exo_rate_limited';
61
+ } catch { return 'exo_rate_limited'; }
62
+ finally { if (timer) clearTimeout(timer); void reader.cancel().catch(() => undefined); }
63
+ };
64
+ const retryAfterDeadline = (value: string | null): number | null => {
65
+ if (!value || value.length > 128) return null;
66
+ const now = Date.now();
67
+ const trimmed = value.trim();
68
+ const deadline = /^\d+(?:\.\d+)?$/u.test(trimmed) ? now + Number(trimmed) * 1000 : Date.parse(trimmed);
69
+ return Number.isFinite(deadline) && deadline >= now && deadline - now <= 86_400_000 ? Math.ceil(deadline) : null;
70
+ };
71
+ const textOf = (response: Json): string => (Array.isArray(response.output) ? response.output : [])
72
+ .flatMap((item) => record(item).type === 'message' && Array.isArray(record(item).content) ? record(item).content as unknown[] : [])
73
+ .filter((part) => record(part).type === 'output_text')
74
+ .map((part) => typeof record(part).text === 'string' ? record(part).text : '').join('');
75
+
76
+ // A whole-response JSON fence is a presentation wrapper, not a schema change.
77
+ // Keep raw provider events intact; the broker validates the unwrapped document.
78
+ const unwrapJsonFence = (text: string): string => {
79
+ const match = /^```(?:json)?[ \t]*\r?\n([\s\S]*?)\r?\n```$/iu.exec(text.trim());
80
+ return match ? match[1] : text;
81
+ };
82
+
83
+ /** Gemma's tool renderer enumerates properties but misses object union fields.
84
+ * Add only constraints implied by every object branch. Original union branches
85
+ * remain untouched, and the broker still validates the canonical input schema.
86
+ */
87
+ const veniceToolParameters = (value: unknown): unknown => {
88
+ if (typeof value === 'boolean') return value;
89
+ const schema = record(value);
90
+ const existingProperties = record(schema.properties);
91
+ const properties = Object.fromEntries(Object.entries(existingProperties).map(([name, child]) => [name, veniceToolParameters(child)]));
92
+ const result: Json = { ...schema, ...(schema.properties ? { properties } : {}) };
93
+ const union = Array.isArray(schema.anyOf) ? schema.anyOf : Array.isArray(schema.oneOf) ? schema.oneOf : null;
94
+ if (!union?.length || (schema.type !== undefined && schema.type !== 'object')) return result;
95
+ const branches = union.map(record);
96
+ if (!branches.every((branch) => branch.type === 'object')) return result;
97
+ const commonNames = Object.keys(record(branches[0].properties))
98
+ .filter((name) => branches.every((branch) => Object.hasOwn(record(branch.properties), name)));
99
+ const common: Json = {};
100
+ for (const name of commonNames) {
101
+ const fields = branches.map((branch) => record(record(branch.properties)[name]));
102
+ const derived: Json = {};
103
+ const type = fields[0].type;
104
+ if (typeof type === 'string' && fields.every((field) => field.type === type)) derived.type = type;
105
+ if (fields.every((field) => Array.isArray(field.enum))) {
106
+ derived.enum = [...new Map(fields.flatMap((field) => field.enum as unknown[]).map((entry) => [JSON.stringify(entry), entry])).values()];
107
+ }
108
+ common[name] = typeof properties[name] === 'boolean' ? properties[name] : { ...derived, ...record(properties[name]) };
109
+ }
110
+ const required = (Array.isArray(branches[0].required) ? branches[0].required : [])
111
+ .filter((name): name is string => typeof name === 'string' && branches.every((branch) => Array.isArray(branch.required) && branch.required.includes(name)));
112
+ return { ...result, type: 'object', properties: { ...properties, ...common },
113
+ ...((required.length || Array.isArray(schema.required)) ? { required: [...new Set([...(Array.isArray(schema.required) ? schema.required : []), ...required])] } : {}) };
114
+ };
115
+
116
+ const chatResponseEvents = (events: unknown[]): unknown[] => events.map((event) => {
117
+ const data = record(event);
118
+ const messages = Array.isArray(data.messages) ? data.messages.map(record) : [];
119
+ if (data.type !== 'messages' || messages.length < 2 || !messages.every((message) => message.role === 'assistant')) return event;
120
+ // Exo's Responses-to-Lingua conversion splits one Chat assistant response
121
+ // into one message per tool call. Keep its calls together so Chat history
122
+ // never inserts another assistant message before the first call's result.
123
+ const content = messages.flatMap((message) => {
124
+ if (Array.isArray(message.content)) return message.content;
125
+ if (typeof message.content === 'string') return message.content ? [{ type: 'text', text: message.content }] : [];
126
+ if (message.content === null || message.content === undefined) return [];
127
+ throw new Error('exo_chat_message_invalid');
128
+ });
129
+ return { ...data, messages: [{ role: 'assistant', content }] };
130
+ });
131
+
132
+ const chatRequestMessages = (harness: ExoHarnessModule, events: unknown[]): Message[] => {
133
+ // The pinned Chat serializer emits Lingua's tagged argument wrapper; Rust
134
+ // Lingua persistence also changes numeric argument values. ToolRequested
135
+ // preserves the original JSON independently, so use its exact ordered
136
+ // receipt instead of coercing either representation. Stored events stay raw.
137
+ const requests = events.map((event) => record(record(event).data)).filter((event) => event.type === 'tool_requested');
138
+ let index = 0;
139
+ const messages = harness.materializeEventsToMessages(events).map((message) => {
140
+ if (message.role !== 'assistant' || !Array.isArray(message.content)) return message;
141
+ return { ...message, content: message.content.map((part) => {
142
+ const content = record(part);
143
+ if (content.type !== 'tool_call') return part;
144
+ const event = requests[index++];
145
+ const request = record(event?.request);
146
+ const args = request.arguments;
147
+ if (!event || event.tool_call_id !== content.tool_call_id || request.function_name !== content.tool_name
148
+ || args === null || typeof args !== 'object' || Array.isArray(args)) throw new Error('exo_chat_arguments_invalid');
149
+ return { ...content, arguments: args };
150
+ }) };
151
+ });
152
+ if (index !== requests.length) throw new Error('exo_chat_arguments_invalid');
153
+ return messages;
154
+ };
155
+
156
+ const toolsFor = (input: Input): Json[] => input.mode === 'provider' ? [] : [
157
+ { name: 'vtx_get_data', description: 'Request assignment-scoped VTX data.', parameters: {
158
+ // Keep the complete branch contract, while exposing its common fields to
159
+ // Chat tool renderers that enumerate only top-level properties.
160
+ type: 'object', additionalProperties: false, required: ['capability', 'arguments'],
161
+ properties: { capability: { type: 'string', enum: input.dataContract.map((entry) => entry.id) }, arguments: { type: 'object' } },
162
+ oneOf: input.dataContract.map((entry) => ({ type: 'object', additionalProperties: false,
163
+ required: ['capability', 'arguments'], properties: { capability: { const: entry.id }, arguments: entry.input_schema } })) } },
164
+ { name: 'vtx_submit_decision', description: 'Submit a canonical VTX decision.', parameters: {
165
+ type: 'object', additionalProperties: false, required: ['candidate'], properties: { candidate: input.decisionSchema } } },
166
+ { name: 'vtx_decision_status', description: 'Resolve a VTX decision operation.', parameters: {
167
+ type: 'object', additionalProperties: false, required: ['operation_id'], properties: { operation_id: { type: 'string', minLength: 1 } } } },
168
+ ];
169
+
170
+ export default {
171
+ tools: [],
172
+ async runTurn(context: Context): Promise<void> {
173
+ const nativeFetch = globalThis.fetch;
174
+ const brokerUrl = new URL(process.env.VTX_EXO_BRIDGE_URL ?? '');
175
+ const token = process.env.VTX_EXO_BRIDGE_TOKEN;
176
+ const source = process.env.VTX_EXO_SOURCE_ROOT;
177
+ if (brokerUrl.protocol !== 'http:' || brokerUrl.hostname !== '127.0.0.1' || brokerUrl.username || brokerUrl.password
178
+ || brokerUrl.pathname !== '/' || brokerUrl.search || brokerUrl.hash || !token || !source) throw new Error('exo_bridge_invalid');
179
+ const broker = async (path: string, body?: Json, deadlineAtMs?: number): Promise<Json> => {
180
+ const timeoutMs = deadlineAtMs === undefined ? 30_000 : deadlineAtMs - Date.now();
181
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) throw new Error('exo_deadline_reached');
182
+ const response = await nativeFetch(new URL(path, brokerUrl), {
183
+ method: body === undefined ? 'GET' : 'POST',
184
+ headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
185
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }), redirect: 'error', signal: AbortSignal.timeout(timeoutMs),
186
+ });
187
+ if (!response.ok) throw new Error('exo_bridge_rejected');
188
+ return record(await response.json());
189
+ };
190
+ let dispatched = false;
191
+ let terminalKnown = true;
192
+ let activeDispatch = false;
193
+ let roundDispatch = false;
194
+ let failureCode = 'exo_turn_failed';
195
+ let retryAtMs: number | null = null;
196
+ const usage = { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, reasoningOutputTokens: 0,
197
+ totalTokens: 0, cacheWriteInputTokens: null, cacheWriteSupported: false };
198
+ let completedCalls = 0;
199
+ let lastCompletedResponseId: string | null = null;
200
+ let currentReceipt: { responseId: string; providerModel: string; reasoningEffort: string } | null = null;
201
+ const addUsage = (nativeUsage: Json): void => {
202
+ const inputTokens = count(nativeUsage.input_tokens);
203
+ const outputTokens = count(nativeUsage.output_tokens);
204
+ const totalTokens = count(nativeUsage.total_tokens);
205
+ const cachedTokens = count(record(nativeUsage.input_tokens_details).cached_tokens ?? 0);
206
+ const reasoningTokens = count(record(nativeUsage.output_tokens_details).reasoning_tokens ?? 0);
207
+ if (totalTokens !== inputTokens + outputTokens || cachedTokens > inputTokens || reasoningTokens > outputTokens) throw new Error('exo_usage_invalid');
208
+ usage.inputTokens = count(usage.inputTokens + inputTokens); usage.outputTokens = count(usage.outputTokens + outputTokens);
209
+ usage.totalTokens = count(usage.totalTokens + totalTokens); usage.cachedInputTokens = count(usage.cachedInputTokens + cachedTokens);
210
+ usage.reasoningOutputTokens = count(usage.reasoningOutputTokens + reasoningTokens);
211
+ completedCalls += 1;
212
+ };
213
+ try {
214
+ const input = await broker('/input') as unknown as Input;
215
+ if (!['openai', 'venice'].includes(input.provider) || !['provider', 'agent'].includes(input.mode) || typeof input.systemPrompt !== 'string' || typeof input.userPrompt !== 'string'
216
+ || !Number.isSafeInteger(input.maxRounds) || input.maxRounds < 1 || input.maxRounds > 64
217
+ || !Number.isSafeInteger(input.deadlineAtMs) || input.deadlineAtMs < 1
218
+ || typeof input.providerModel !== 'string' || !input.providerModel || typeof input.reasoningEffort !== 'string'
219
+ || !(input.nativeReasoningEffort === null || typeof input.nativeReasoningEffort === 'string')) throw new Error('exo_input_invalid');
220
+ const credential = await broker('/credential');
221
+ const expectedBase = input.provider === 'venice' ? 'https://api.venice.ai/api/v1' : 'https://api.openai.com/v1';
222
+ const providerBase = new URL(typeof credential.baseURL === 'string' ? credential.baseURL : expectedBase);
223
+ const localFixture = providerBase.protocol === 'http:' && providerBase.hostname === '127.0.0.1' && Boolean(providerBase.port) && providerBase.pathname === '/v1';
224
+ if ((!localFixture && providerBase.href !== expectedBase)
225
+ || providerBase.username || providerBase.password || providerBase.search || providerBase.hash
226
+ || typeof credential.apiKey !== 'string' || !credential.apiKey) {
227
+ throw new Error('exo_provider_not_qualified');
228
+ }
229
+ if (input.provider === 'venice' && (input.reasoningEffort !== 'none' || input.nativeReasoningEffort !== 'none')) throw new Error('exo_effort_not_qualified');
230
+ const providerEndpoint = `${providerBase.href}/${input.provider === 'venice' ? 'chat/completions' : 'responses'}`;
231
+ const harness = await import(pathToFileURL(join(source, 'exoharness/typescript/harness/index.ts')).href) as ExoHarnessModule;
232
+ const model = await import(pathToFileURL(join(source, 'exoharness/typescript/model-runtime/responses.ts')).href) as ExoModelModule;
233
+ const definitions = toolsFor(input);
234
+ const allowed = new Set(definitions.map((tool) => tool.name));
235
+ // Install before constructing the upstream SDK, which captures fetch.
236
+ // Each complete() may dispatch once. SDK retries after an uncertain call
237
+ // are rejected before network I/O; they cannot repeat paid inference.
238
+ globalThis.fetch = async (resource, options) => {
239
+ const url = new URL(resource instanceof Request ? resource.url : String(resource));
240
+ if (url.href !== providerEndpoint || options?.method !== 'POST'
241
+ || !activeDispatch || roundDispatch || typeof options.body !== 'string') throw new Error('exo_dispatch_rejected');
242
+ const payload = record(JSON.parse(options.body));
243
+ if (payload.model !== input.providerModel || payload.stream === true) throw new Error('exo_request_identity_mismatch');
244
+ const format = { name: 'vtx_result', strict: true, schema: input.outputSchema };
245
+ if (input.provider === 'venice') {
246
+ payload.reasoning_effort = 'none';
247
+ payload.venice_parameters = { disable_thinking: true, strip_thinking_response: false, include_venice_system_prompt: false,
248
+ enable_web_search: 'off', enable_web_scraping: false, enable_x_search: false, enable_web_citations: false };
249
+ // Venice's native JSON grammar suppresses tool selection. Agent
250
+ // supplies the same final schema as context and validates it in VTX.
251
+ if (input.mode === 'provider') payload.response_format = { type: 'json_schema', json_schema: format };
252
+ else delete payload.response_format;
253
+ } else {
254
+ if (input.nativeReasoningEffort === null) delete payload.reasoning;
255
+ else payload.reasoning = { effort: input.nativeReasoningEffort };
256
+ payload.text = { format: { type: 'json_schema', ...format } };
257
+ }
258
+ // Exo marks every tool strict, but VTX capability schemas deliberately
259
+ // preserve optional fields and oneOf. The broker validates their exact
260
+ // canonical contracts; do not change those semantics for the SDK.
261
+ if (Array.isArray(payload.tools)) payload.tools = payload.tools.map((tool) => input.provider === 'venice'
262
+ ? { ...record(tool), function: { ...record(record(tool).function), strict: false,
263
+ parameters: veniceToolParameters(record(record(tool).function).parameters) } }
264
+ : { ...record(tool), strict: false });
265
+ await broker('/dispatch', { model: input.providerModel, reasoningEffort: input.reasoningEffort });
266
+ roundDispatch = true;
267
+ dispatched = true;
268
+ terminalKnown = false;
269
+ currentReceipt = null;
270
+ retryAtMs = null;
271
+ const response = await nativeFetch(resource, { ...options, body: JSON.stringify(payload), redirect: 'error' });
272
+ const headers = new Headers(response.headers);
273
+ headers.set('x-should-retry', 'false');
274
+ // A definite client rejection did not yield a model result. Server and
275
+ // transport failures stay uncertain, even if an SDK reports an error.
276
+ if (!response.ok && response.status >= 400 && response.status < 500 && response.status !== 408) terminalKnown = true;
277
+ if (!response.ok) {
278
+ retryAtMs = response.status === 429 ? retryAfterDeadline(response.headers.get('retry-after')) : null;
279
+ failureCode = response.status === 401 ? 'exo_invalid_api_key' : response.status === 429 ? await rateLimitCode(response) : 'exo_provider_rejected';
280
+ if (response.status !== 429) await response.body?.cancel();
281
+ return new Response(JSON.stringify({ error: { message: failureCode, code: failureCode } }), { status: response.status, headers });
282
+ }
283
+ if (input.provider === 'venice') {
284
+ try {
285
+ const raw = record(await response.clone().json());
286
+ const choices = Array.isArray(raw.choices) ? raw.choices : [];
287
+ const choice = record(choices[0]);
288
+ terminalKnown = choices.length === 1 && ['stop', 'tool_calls', 'length', 'content_filter'].includes(String(choice.finish_reason));
289
+ if (raw.model !== input.providerModel || typeof raw.id !== 'string' || !raw.id) throw new Error('exo_response_identity_mismatch');
290
+ const controls = record(raw.venice_parameters);
291
+ if (controls.disable_thinking !== true || controls.strip_thinking_response !== false
292
+ || controls.include_venice_system_prompt !== false || controls.enable_web_search !== 'off'
293
+ || controls.enable_web_scraping !== false || controls.enable_x_search !== false || controls.enable_web_citations !== false
294
+ || record(choice.message).reasoning_content || record(choice.message).reasoning
295
+ || (record(record(raw.usage).completion_tokens_details).reasoning_tokens ?? 0) !== 0
296
+ || (raw.reasoning_effort !== undefined && raw.reasoning_effort !== 'none')) throw new Error('exo_response_effort_mismatch');
297
+ const nativeUsage = record(raw.usage);
298
+ addUsage({ input_tokens: nativeUsage.prompt_tokens, output_tokens: nativeUsage.completion_tokens, total_tokens: nativeUsage.total_tokens,
299
+ input_tokens_details: nativeUsage.prompt_tokens_details, output_tokens_details: nativeUsage.completion_tokens_details });
300
+ lastCompletedResponseId = raw.id;
301
+ currentReceipt = { responseId: raw.id, providerModel: input.providerModel, reasoningEffort: input.reasoningEffort };
302
+ // Exo synthesizes completed status for every Chat response. Inspect
303
+ // the real terminal reason before that normalization can hide loss.
304
+ if (choices.length !== 1 || !['stop', 'tool_calls'].includes(String(choice.finish_reason))) throw new Error('exo_response_incomplete');
305
+ const hasTools = Array.isArray(record(choice.message).tool_calls) && (record(choice.message).tool_calls as unknown[]).length > 0;
306
+ if ((choice.finish_reason === 'tool_calls') !== hasTools) throw new Error('exo_response_incomplete');
307
+ } catch (error) {
308
+ failureCode = error instanceof Error && /^exo_[a-z_]+$/u.test(error.message) ? error.message : 'exo_response_invalid';
309
+ await response.body?.cancel();
310
+ throw new Error(failureCode);
311
+ }
312
+ }
313
+ return new Response(response.body, { status: response.status, headers });
314
+ };
315
+ const RuntimeClass = input.provider === 'venice' ? model.ChatCompletionsRuntime : model.ResponsesRuntime;
316
+ const runtime = new RuntimeClass({ apiKey: credential.apiKey, baseURL: providerBase.href });
317
+ for (let round = 0; round < input.maxRounds; round += 1) {
318
+ const instructions: Message[] = [{ role: 'system', content: input.systemPrompt }, { role: 'user', content: input.userPrompt }];
319
+ if (input.provider === 'venice' && input.mode === 'agent') instructions.push({
320
+ role: 'user', content: JSON.stringify({ final_response_schema: input.outputSchema,
321
+ decision_schema: input.decisionSchema, data_contract: input.dataContract }),
322
+ });
323
+ const messages = input.mode === 'provider' ? instructions : input.provider === 'venice'
324
+ ? [...instructions, ...chatRequestMessages(harness, (await context.exoharness.current.conversation.getEvents({
325
+ direction: 'asc', types: ['messages', 'tool_requested', 'tool_result'],
326
+ })).events)]
327
+ : await harness.materializePromptMessages(context.exoharness.current.conversation, instructions);
328
+ roundDispatch = false;
329
+ activeDispatch = true;
330
+ const response = await runtime.complete({ model: input.providerModel, messages, tools: definitions });
331
+ activeDispatch = false;
332
+ // Validate the actual completed provider receipt before exposing any
333
+ // model tool call to the broker, especially decision submission.
334
+ if (response.status !== 'completed') throw new Error('exo_response_incomplete');
335
+ terminalKnown = true;
336
+ if (response.model !== input.providerModel || typeof response.id !== 'string' || !response.id) throw new Error('exo_response_identity_mismatch');
337
+ if (input.provider === 'openai') {
338
+ const effectiveEffort = record(response.reasoning).effort ?? null;
339
+ if (effectiveEffort !== input.nativeReasoningEffort) throw new Error('exo_response_effort_mismatch');
340
+ addUsage(record(response.usage));
341
+ lastCompletedResponseId = response.id;
342
+ currentReceipt = { responseId: response.id, providerModel: input.providerModel, reasoningEffort: input.reasoningEffort };
343
+ }
344
+ const events = model.responseToLinguaEvents(response);
345
+ await context.exoharness.current.turn.addEvents(input.provider === 'venice' ? chatResponseEvents(events) : events);
346
+ const calls = model.responseToolCalls(response);
347
+ if (calls.length === 0) {
348
+ const rawText = textOf(response);
349
+ const text = input.provider === 'venice' && input.mode === 'agent' ? unwrapJsonFence(rawText) : rawText;
350
+ if (!text.trim()) throw new Error('exo_response_empty');
351
+ await broker('/terminal', { text, providerModel: response.model, reasoningEffort: input.reasoningEffort, responseId: response.id, usage });
352
+ return;
353
+ }
354
+ if (input.mode !== 'agent' || calls.some((call) => !allowed.has(call.request.functionName))) throw new Error('exo_tool_not_allowed');
355
+ for (const call of calls) {
356
+ const result = await broker('/tool', { callId: call.toolCallId, tool: call.request.functionName, arguments: call.request.arguments,
357
+ effectiveModel: input.requestedModel, effectiveReasoningEffort: input.reasoningEffort }, input.deadlineAtMs);
358
+ // Append the entire result directly. The default registry replaces
359
+ // results over 8k with previews, which loses assignment data.
360
+ await context.exoharness.current.turn.addEvents([harness.toolResultEvent(call.toolCallId,
361
+ result.success === true ? { ok: true, value: result.value } : { ok: false, error: 'vtx_tool_failed', value: result.value })]);
362
+ }
363
+ }
364
+ throw new Error('exo_round_limit');
365
+ } catch (error) {
366
+ const code = error instanceof Error && /^exo_[a-z_]+$/u.test(error.message) ? error.message : failureCode;
367
+ await broker('/failure', { code, dispatched, terminalKnown, usage: completedCalls ? usage : null,
368
+ responseId: currentReceipt?.responseId ?? null, providerModel: currentReceipt?.providerModel ?? null,
369
+ reasoningEffort: currentReceipt?.reasoningEffort ?? null, lastCompletedResponseId, retryAtMs }).catch(() => undefined);
370
+ // Rust surfaces errors and stacks to its caller; do not propagate SDK
371
+ // diagnostics that can contain prompts, provider bodies, or secrets.
372
+ throw new Error(code);
373
+ } finally {
374
+ globalThis.fetch = nativeFetch;
375
+ }
376
+ },
377
+ };
@@ -16,7 +16,7 @@ import { fileURLToPath } from "node:url";
16
16
  // agent-cli-release.json
17
17
  var agent_cli_release_default = {
18
18
  package_name: "@vtxmacro/cli",
19
- package_version: "2026.9.14",
19
+ package_version: "2026.9.17",
20
20
  codex_package_name: "@openai/codex",
21
21
  codex_version: "0.153.3",
22
22
  copilot_sdk_package_name: "@github/copilot-sdk",
@@ -1536,3 +1536,42 @@ export {
1536
1536
  runInferenceHostBootstrapNpmCommand,
1537
1537
  runInferenceHostServiceBootstrap
1538
1538
  };
1539
+ /*! Bundled license information:
1540
+
1541
+ smol-toml/dist/date.js:
1542
+ smol-toml/dist/error.js:
1543
+ smol-toml/dist/util.js:
1544
+ smol-toml/dist/primitive.js:
1545
+ smol-toml/dist/extract.js:
1546
+ smol-toml/dist/struct.js:
1547
+ smol-toml/dist/parse.js:
1548
+ smol-toml/dist/stringify.js:
1549
+ smol-toml/dist/index.js:
1550
+ (*!
1551
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
1552
+ * SPDX-License-Identifier: BSD-3-Clause
1553
+ *
1554
+ * Redistribution and use in source and binary forms, with or without
1555
+ * modification, are permitted provided that the following conditions are met:
1556
+ *
1557
+ * 1. Redistributions of source code must retain the above copyright notice, this
1558
+ * list of conditions and the following disclaimer.
1559
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
1560
+ * this list of conditions and the following disclaimer in the
1561
+ * documentation and/or other materials provided with the distribution.
1562
+ * 3. Neither the name of the copyright holder nor the names of its contributors
1563
+ * may be used to endorse or promote products derived from this software without
1564
+ * specific prior written permission.
1565
+ *
1566
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
1567
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
1568
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
1569
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
1570
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
1571
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
1572
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
1573
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
1574
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
1575
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1576
+ *)
1577
+ */