@vtxmacro/cli 2026.9.13 → 2026.9.15

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
@@ -10,8 +10,8 @@ vtx --version
10
10
  ```
11
11
 
12
12
  The host supports Windows x64 and Linux x64, including WSL. The package installs
13
- the exact supported OpenAI Codex runtime, GitHub Copilot SDK, and official
14
- DeepSeek Harness package wave. The VTX host
13
+ the exact supported OpenAI Codex runtime, GitHub Copilot SDK, official
14
+ DeepSeek Harness package wave, and Pi SDK package wave. The VTX host
15
15
  verifies the Codex runtime's version and digest before use; it does not borrow a
16
16
  Codex binary from PATH or an editor extension.
17
17
  For Windows-login auto-start, install and configure the CLI from native Windows;
@@ -70,10 +70,10 @@ and VTX grant. VTX resolves the package-owned pinned Copilot platform runtime
70
70
  explicitly and rejects an `auto`-only catalog because it cannot prove the
71
71
  effective model.
72
72
 
73
- An explicitly capable durable Codex, Copilot, or DeepSeek Harness model can also
73
+ An explicitly capable durable Codex, Copilot, DeepSeek Harness, or Pi model can also
74
74
  control Main in **Agent** mode using the same VTX OAuth grant and its own local
75
- adapter-specific vendor credential: ChatGPT or Copilot login, or a DeepSeek API
76
- key. VTX assigns the running bot but does not send a VTX-authored prompt or set
75
+ adapter-specific vendor credential: ChatGPT or Copilot login, or a DeepSeek or
76
+ Pi provider API key. VTX assigns the running bot but does not send a VTX-authored prompt or set
77
77
  the analysis cadence. The assigned runtime keeps a durable Agent thread,
78
78
  chooses when to wake, requests only the assignment-scoped VTX data it needs (or
79
79
  none), and submits the normal structured decision for VTX to validate and
@@ -134,6 +134,27 @@ instance after reconciling an ambiguous fence. An authentication fence clears
134
134
  only after a successful replacement key import. Quota and rate-limit failures
135
135
  honor their provider retry time or a bounded local cooldown.
136
136
 
137
+ For Pi, use a separate instance and import one provider API key privately:
138
+
139
+ ```bash
140
+ vtx inference-host login --instance pi-1
141
+ vtx inference-host pi-login --provider openai --instance pi-1 < /private/path/provider.key
142
+ vtx inference-host doctor --adapter pi --instance pi-1 --json
143
+ vtx inference-host service install --adapter pi --instance pi-1
144
+ ```
145
+
146
+ Pi uses the package-owned SDK and exact provider/model identities. The existing
147
+ Provider / Agent controls apply: Provider receives VTX's prompt and has no
148
+ tools; Main Agent keeps an isolated session and only the three VTX tools.
149
+ The initial qualified models use an OpenAI API key; the release embeds VTX's
150
+ configured exact model IDs and SDK capability mappings. API compatibility alone
151
+ does not qualify a provider. Ordinary Pi
152
+ credentials, extensions, project instructions, and sessions are not imported.
153
+ Subscription OAuth is not enabled by this adapter. Calls with uncertain
154
+ outcomes remain fenced after restart; conversation persistence does not imply
155
+ same-attempt recovery. Uninstall the exact service instance and settle pending
156
+ work before removing its key with `pi-logout --instance pi-1`.
157
+
137
158
  To connect another Codex subscription to the same VTX account, repeat the
138
159
  login and service-install commands with a stable local name such as
139
160
  `--instance codex-2`. Each named instance has an isolated VTX grant, Codex
@@ -267,6 +288,43 @@ schema plus `provenance` with `source: "external_agent"`, a stable
267
288
  `agent-assignment-release` only when intentionally ending the foreground Agent
268
289
  session; normal cadence is recorded with `agent-assignment-heartbeat`.
269
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
+
270
328
  Claude Code, Antigravity, Gemini CLI, Kiro, Grok Build, Cursor, Amp, Auggie,
271
329
  Junie, Warp/Oz, Qwen Code, and OpenCode remain on the explicit foreground host
272
330
  path. Their current subscription routing,
@@ -0,0 +1,373 @@
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
+ };
22
+ type ExoHarnessModule = {
23
+ materializePromptMessages(conversation: unknown, instructions: Message[]): Promise<Message[]>;
24
+ materializeEventsToMessages(events: unknown[]): Message[];
25
+ toolResultEvent(id: string, result: unknown): unknown;
26
+ };
27
+ type Runtime = {
28
+ complete(request: Json): Promise<Json>;
29
+ };
30
+ type ExoModelModule = {
31
+ ResponsesRuntime: new (options: { apiKey: string; baseURL: string }) => Runtime;
32
+ ChatCompletionsRuntime: new (options: { apiKey: string; baseURL: string }) => Runtime;
33
+ responseToLinguaEvents(response: Json): unknown[];
34
+ responseToolCalls(response: Json): ToolCall[];
35
+ };
36
+
37
+ const record = (value: unknown): Json => value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Json : {};
38
+ const count = (value: unknown): number => {
39
+ if (!Number.isSafeInteger(value) || Number(value) < 0) throw new Error('exo_usage_invalid');
40
+ return Number(value);
41
+ };
42
+ const rateLimitCode = async (response: Response): Promise<string> => {
43
+ if (!response.body) return 'exo_rate_limited';
44
+ const reader = response.body.getReader();
45
+ const chunks: Uint8Array[] = [];
46
+ let bytes = 0;
47
+ let timer: ReturnType<typeof setTimeout> | undefined;
48
+ const timeout = new Promise<never>((_resolve, reject) => { timer = setTimeout(() => reject(new Error('exo_error_body_timeout')), 1000); });
49
+ try {
50
+ for (;;) {
51
+ const part = await Promise.race([reader.read(), timeout]);
52
+ if (part.done) break;
53
+ bytes += part.value.byteLength;
54
+ if (bytes > 16_384) return 'exo_rate_limited';
55
+ chunks.push(part.value);
56
+ }
57
+ const error = record(record(JSON.parse(Buffer.concat(chunks).toString('utf8'))).error);
58
+ return error.type === 'insufficient_quota' || error.code === 'insufficient_quota'
59
+ ? 'exo_quota_exhausted' : 'exo_rate_limited';
60
+ } catch { return 'exo_rate_limited'; }
61
+ finally { if (timer) clearTimeout(timer); void reader.cancel().catch(() => undefined); }
62
+ };
63
+ const retryAfterDeadline = (value: string | null): number | null => {
64
+ if (!value || value.length > 128) return null;
65
+ const now = Date.now();
66
+ const trimmed = value.trim();
67
+ const deadline = /^\d+(?:\.\d+)?$/u.test(trimmed) ? now + Number(trimmed) * 1000 : Date.parse(trimmed);
68
+ return Number.isFinite(deadline) && deadline >= now && deadline - now <= 86_400_000 ? Math.ceil(deadline) : null;
69
+ };
70
+ const textOf = (response: Json): string => (Array.isArray(response.output) ? response.output : [])
71
+ .flatMap((item) => record(item).type === 'message' && Array.isArray(record(item).content) ? record(item).content as unknown[] : [])
72
+ .filter((part) => record(part).type === 'output_text')
73
+ .map((part) => typeof record(part).text === 'string' ? record(part).text : '').join('');
74
+
75
+ // A whole-response JSON fence is a presentation wrapper, not a schema change.
76
+ // Keep raw provider events intact; the broker validates the unwrapped document.
77
+ const unwrapJsonFence = (text: string): string => {
78
+ const match = /^```(?:json)?[ \t]*\r?\n([\s\S]*?)\r?\n```$/iu.exec(text.trim());
79
+ return match ? match[1] : text;
80
+ };
81
+
82
+ /** Gemma's tool renderer enumerates properties but misses object union fields.
83
+ * Add only constraints implied by every object branch. Original union branches
84
+ * remain untouched, and the broker still validates the canonical input schema.
85
+ */
86
+ const veniceToolParameters = (value: unknown): unknown => {
87
+ if (typeof value === 'boolean') return value;
88
+ const schema = record(value);
89
+ const existingProperties = record(schema.properties);
90
+ const properties = Object.fromEntries(Object.entries(existingProperties).map(([name, child]) => [name, veniceToolParameters(child)]));
91
+ const result: Json = { ...schema, ...(schema.properties ? { properties } : {}) };
92
+ const union = Array.isArray(schema.anyOf) ? schema.anyOf : Array.isArray(schema.oneOf) ? schema.oneOf : null;
93
+ if (!union?.length || (schema.type !== undefined && schema.type !== 'object')) return result;
94
+ const branches = union.map(record);
95
+ if (!branches.every((branch) => branch.type === 'object')) return result;
96
+ const commonNames = Object.keys(record(branches[0].properties))
97
+ .filter((name) => branches.every((branch) => Object.hasOwn(record(branch.properties), name)));
98
+ const common: Json = {};
99
+ for (const name of commonNames) {
100
+ const fields = branches.map((branch) => record(record(branch.properties)[name]));
101
+ const derived: Json = {};
102
+ const type = fields[0].type;
103
+ if (typeof type === 'string' && fields.every((field) => field.type === type)) derived.type = type;
104
+ if (fields.every((field) => Array.isArray(field.enum))) {
105
+ derived.enum = [...new Map(fields.flatMap((field) => field.enum as unknown[]).map((entry) => [JSON.stringify(entry), entry])).values()];
106
+ }
107
+ common[name] = typeof properties[name] === 'boolean' ? properties[name] : { ...derived, ...record(properties[name]) };
108
+ }
109
+ const required = (Array.isArray(branches[0].required) ? branches[0].required : [])
110
+ .filter((name): name is string => typeof name === 'string' && branches.every((branch) => Array.isArray(branch.required) && branch.required.includes(name)));
111
+ return { ...result, type: 'object', properties: { ...properties, ...common },
112
+ ...((required.length || Array.isArray(schema.required)) ? { required: [...new Set([...(Array.isArray(schema.required) ? schema.required : []), ...required])] } : {}) };
113
+ };
114
+
115
+ const chatResponseEvents = (events: unknown[]): unknown[] => events.map((event) => {
116
+ const data = record(event);
117
+ const messages = Array.isArray(data.messages) ? data.messages.map(record) : [];
118
+ if (data.type !== 'messages' || messages.length < 2 || !messages.every((message) => message.role === 'assistant')) return event;
119
+ // Exo's Responses-to-Lingua conversion splits one Chat assistant response
120
+ // into one message per tool call. Keep its calls together so Chat history
121
+ // never inserts another assistant message before the first call's result.
122
+ const content = messages.flatMap((message) => {
123
+ if (Array.isArray(message.content)) return message.content;
124
+ if (typeof message.content === 'string') return message.content ? [{ type: 'text', text: message.content }] : [];
125
+ if (message.content === null || message.content === undefined) return [];
126
+ throw new Error('exo_chat_message_invalid');
127
+ });
128
+ return { ...data, messages: [{ role: 'assistant', content }] };
129
+ });
130
+
131
+ const chatRequestMessages = (harness: ExoHarnessModule, events: unknown[]): Message[] => {
132
+ // The pinned Chat serializer emits Lingua's tagged argument wrapper; Rust
133
+ // Lingua persistence also changes numeric argument values. ToolRequested
134
+ // preserves the original JSON independently, so use its exact ordered
135
+ // receipt instead of coercing either representation. Stored events stay raw.
136
+ const requests = events.map((event) => record(record(event).data)).filter((event) => event.type === 'tool_requested');
137
+ let index = 0;
138
+ const messages = harness.materializeEventsToMessages(events).map((message) => {
139
+ if (message.role !== 'assistant' || !Array.isArray(message.content)) return message;
140
+ return { ...message, content: message.content.map((part) => {
141
+ const content = record(part);
142
+ if (content.type !== 'tool_call') return part;
143
+ const event = requests[index++];
144
+ const request = record(event?.request);
145
+ const args = request.arguments;
146
+ if (!event || event.tool_call_id !== content.tool_call_id || request.function_name !== content.tool_name
147
+ || args === null || typeof args !== 'object' || Array.isArray(args)) throw new Error('exo_chat_arguments_invalid');
148
+ return { ...content, arguments: args };
149
+ }) };
150
+ });
151
+ if (index !== requests.length) throw new Error('exo_chat_arguments_invalid');
152
+ return messages;
153
+ };
154
+
155
+ const toolsFor = (input: Input): Json[] => input.mode === 'provider' ? [] : [
156
+ { name: 'vtx_get_data', description: 'Request assignment-scoped VTX data.', parameters: {
157
+ // Keep the complete branch contract, while exposing its common fields to
158
+ // Chat tool renderers that enumerate only top-level properties.
159
+ type: 'object', additionalProperties: false, required: ['capability', 'arguments'],
160
+ properties: { capability: { type: 'string', enum: input.dataContract.map((entry) => entry.id) }, arguments: { type: 'object' } },
161
+ oneOf: input.dataContract.map((entry) => ({ type: 'object', additionalProperties: false,
162
+ required: ['capability', 'arguments'], properties: { capability: { const: entry.id }, arguments: entry.input_schema } })) } },
163
+ { name: 'vtx_submit_decision', description: 'Submit a canonical VTX decision.', parameters: {
164
+ type: 'object', additionalProperties: false, required: ['candidate'], properties: { candidate: input.decisionSchema } } },
165
+ { name: 'vtx_decision_status', description: 'Resolve a VTX decision operation.', parameters: {
166
+ type: 'object', additionalProperties: false, required: ['operation_id'], properties: { operation_id: { type: 'string', minLength: 1 } } } },
167
+ ];
168
+
169
+ export default {
170
+ tools: [],
171
+ async runTurn(context: Context): Promise<void> {
172
+ const nativeFetch = globalThis.fetch;
173
+ const brokerUrl = new URL(process.env.VTX_EXO_BRIDGE_URL ?? '');
174
+ const token = process.env.VTX_EXO_BRIDGE_TOKEN;
175
+ const source = process.env.VTX_EXO_SOURCE_ROOT;
176
+ if (brokerUrl.protocol !== 'http:' || brokerUrl.hostname !== '127.0.0.1' || brokerUrl.username || brokerUrl.password
177
+ || brokerUrl.pathname !== '/' || brokerUrl.search || brokerUrl.hash || !token || !source) throw new Error('exo_bridge_invalid');
178
+ const broker = async (path: string, body?: Json): Promise<Json> => {
179
+ const response = await nativeFetch(new URL(path, brokerUrl), {
180
+ method: body === undefined ? 'GET' : 'POST',
181
+ headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
182
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }), redirect: 'error', signal: AbortSignal.timeout(30_000),
183
+ });
184
+ if (!response.ok) throw new Error('exo_bridge_rejected');
185
+ return record(await response.json());
186
+ };
187
+ let dispatched = false;
188
+ let terminalKnown = true;
189
+ let activeDispatch = false;
190
+ let roundDispatch = false;
191
+ let failureCode = 'exo_turn_failed';
192
+ let retryAtMs: number | null = null;
193
+ const usage = { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, reasoningOutputTokens: 0,
194
+ totalTokens: 0, cacheWriteInputTokens: null, cacheWriteSupported: false };
195
+ let completedCalls = 0;
196
+ let lastCompletedResponseId: string | null = null;
197
+ let currentReceipt: { responseId: string; providerModel: string; reasoningEffort: string } | null = null;
198
+ const addUsage = (nativeUsage: Json): void => {
199
+ const inputTokens = count(nativeUsage.input_tokens);
200
+ const outputTokens = count(nativeUsage.output_tokens);
201
+ const totalTokens = count(nativeUsage.total_tokens);
202
+ const cachedTokens = count(record(nativeUsage.input_tokens_details).cached_tokens ?? 0);
203
+ const reasoningTokens = count(record(nativeUsage.output_tokens_details).reasoning_tokens ?? 0);
204
+ if (totalTokens !== inputTokens + outputTokens || cachedTokens > inputTokens || reasoningTokens > outputTokens) throw new Error('exo_usage_invalid');
205
+ usage.inputTokens = count(usage.inputTokens + inputTokens); usage.outputTokens = count(usage.outputTokens + outputTokens);
206
+ usage.totalTokens = count(usage.totalTokens + totalTokens); usage.cachedInputTokens = count(usage.cachedInputTokens + cachedTokens);
207
+ usage.reasoningOutputTokens = count(usage.reasoningOutputTokens + reasoningTokens);
208
+ completedCalls += 1;
209
+ };
210
+ try {
211
+ const input = await broker('/input') as unknown as Input;
212
+ if (!['openai', 'venice'].includes(input.provider) || !['provider', 'agent'].includes(input.mode) || typeof input.systemPrompt !== 'string' || typeof input.userPrompt !== 'string'
213
+ || !Number.isSafeInteger(input.maxRounds) || input.maxRounds < 1 || input.maxRounds > 64
214
+ || typeof input.providerModel !== 'string' || !input.providerModel || typeof input.reasoningEffort !== 'string'
215
+ || !(input.nativeReasoningEffort === null || typeof input.nativeReasoningEffort === 'string')) throw new Error('exo_input_invalid');
216
+ const credential = await broker('/credential');
217
+ const expectedBase = input.provider === 'venice' ? 'https://api.venice.ai/api/v1' : 'https://api.openai.com/v1';
218
+ const providerBase = new URL(typeof credential.baseURL === 'string' ? credential.baseURL : expectedBase);
219
+ const localFixture = providerBase.protocol === 'http:' && providerBase.hostname === '127.0.0.1' && Boolean(providerBase.port) && providerBase.pathname === '/v1';
220
+ if ((!localFixture && providerBase.href !== expectedBase)
221
+ || providerBase.username || providerBase.password || providerBase.search || providerBase.hash
222
+ || typeof credential.apiKey !== 'string' || !credential.apiKey) {
223
+ throw new Error('exo_provider_not_qualified');
224
+ }
225
+ if (input.provider === 'venice' && (input.reasoningEffort !== 'none' || input.nativeReasoningEffort !== 'none')) throw new Error('exo_effort_not_qualified');
226
+ const providerEndpoint = `${providerBase.href}/${input.provider === 'venice' ? 'chat/completions' : 'responses'}`;
227
+ const harness = await import(pathToFileURL(join(source, 'exoharness/typescript/harness/index.ts')).href) as ExoHarnessModule;
228
+ const model = await import(pathToFileURL(join(source, 'exoharness/typescript/model-runtime/responses.ts')).href) as ExoModelModule;
229
+ const definitions = toolsFor(input);
230
+ const allowed = new Set(definitions.map((tool) => tool.name));
231
+ // Install before constructing the upstream SDK, which captures fetch.
232
+ // Each complete() may dispatch once. SDK retries after an uncertain call
233
+ // are rejected before network I/O; they cannot repeat paid inference.
234
+ globalThis.fetch = async (resource, options) => {
235
+ const url = new URL(resource instanceof Request ? resource.url : String(resource));
236
+ if (url.href !== providerEndpoint || options?.method !== 'POST'
237
+ || !activeDispatch || roundDispatch || typeof options.body !== 'string') throw new Error('exo_dispatch_rejected');
238
+ const payload = record(JSON.parse(options.body));
239
+ if (payload.model !== input.providerModel || payload.stream === true) throw new Error('exo_request_identity_mismatch');
240
+ const format = { name: 'vtx_result', strict: true, schema: input.outputSchema };
241
+ if (input.provider === 'venice') {
242
+ payload.reasoning_effort = 'none';
243
+ payload.venice_parameters = { disable_thinking: true, strip_thinking_response: false, include_venice_system_prompt: false,
244
+ enable_web_search: 'off', enable_web_scraping: false, enable_x_search: false, enable_web_citations: false };
245
+ // Venice's native JSON grammar suppresses tool selection. Agent
246
+ // supplies the same final schema as context and validates it in VTX.
247
+ if (input.mode === 'provider') payload.response_format = { type: 'json_schema', json_schema: format };
248
+ else delete payload.response_format;
249
+ } else {
250
+ if (input.nativeReasoningEffort === null) delete payload.reasoning;
251
+ else payload.reasoning = { effort: input.nativeReasoningEffort };
252
+ payload.text = { format: { type: 'json_schema', ...format } };
253
+ }
254
+ // Exo marks every tool strict, but VTX capability schemas deliberately
255
+ // preserve optional fields and oneOf. The broker validates their exact
256
+ // canonical contracts; do not change those semantics for the SDK.
257
+ if (Array.isArray(payload.tools)) payload.tools = payload.tools.map((tool) => input.provider === 'venice'
258
+ ? { ...record(tool), function: { ...record(record(tool).function), strict: false,
259
+ parameters: veniceToolParameters(record(record(tool).function).parameters) } }
260
+ : { ...record(tool), strict: false });
261
+ await broker('/dispatch', { model: input.providerModel, reasoningEffort: input.reasoningEffort });
262
+ roundDispatch = true;
263
+ dispatched = true;
264
+ terminalKnown = false;
265
+ currentReceipt = null;
266
+ retryAtMs = null;
267
+ const response = await nativeFetch(resource, { ...options, body: JSON.stringify(payload), redirect: 'error' });
268
+ const headers = new Headers(response.headers);
269
+ headers.set('x-should-retry', 'false');
270
+ // A definite client rejection did not yield a model result. Server and
271
+ // transport failures stay uncertain, even if an SDK reports an error.
272
+ if (!response.ok && response.status >= 400 && response.status < 500 && response.status !== 408) terminalKnown = true;
273
+ if (!response.ok) {
274
+ retryAtMs = response.status === 429 ? retryAfterDeadline(response.headers.get('retry-after')) : null;
275
+ failureCode = response.status === 401 ? 'exo_invalid_api_key' : response.status === 429 ? await rateLimitCode(response) : 'exo_provider_rejected';
276
+ if (response.status !== 429) await response.body?.cancel();
277
+ return new Response(JSON.stringify({ error: { message: failureCode, code: failureCode } }), { status: response.status, headers });
278
+ }
279
+ if (input.provider === 'venice') {
280
+ try {
281
+ const raw = record(await response.clone().json());
282
+ const choices = Array.isArray(raw.choices) ? raw.choices : [];
283
+ const choice = record(choices[0]);
284
+ terminalKnown = choices.length === 1 && ['stop', 'tool_calls', 'length', 'content_filter'].includes(String(choice.finish_reason));
285
+ if (raw.model !== input.providerModel || typeof raw.id !== 'string' || !raw.id) throw new Error('exo_response_identity_mismatch');
286
+ const controls = record(raw.venice_parameters);
287
+ if (controls.disable_thinking !== true || controls.strip_thinking_response !== false
288
+ || controls.include_venice_system_prompt !== false || controls.enable_web_search !== 'off'
289
+ || controls.enable_web_scraping !== false || controls.enable_x_search !== false || controls.enable_web_citations !== false
290
+ || record(choice.message).reasoning_content || record(choice.message).reasoning
291
+ || (record(record(raw.usage).completion_tokens_details).reasoning_tokens ?? 0) !== 0
292
+ || (raw.reasoning_effort !== undefined && raw.reasoning_effort !== 'none')) throw new Error('exo_response_effort_mismatch');
293
+ const nativeUsage = record(raw.usage);
294
+ addUsage({ input_tokens: nativeUsage.prompt_tokens, output_tokens: nativeUsage.completion_tokens, total_tokens: nativeUsage.total_tokens,
295
+ input_tokens_details: nativeUsage.prompt_tokens_details, output_tokens_details: nativeUsage.completion_tokens_details });
296
+ lastCompletedResponseId = raw.id;
297
+ currentReceipt = { responseId: raw.id, providerModel: input.providerModel, reasoningEffort: input.reasoningEffort };
298
+ // Exo synthesizes completed status for every Chat response. Inspect
299
+ // the real terminal reason before that normalization can hide loss.
300
+ if (choices.length !== 1 || !['stop', 'tool_calls'].includes(String(choice.finish_reason))) throw new Error('exo_response_incomplete');
301
+ const hasTools = Array.isArray(record(choice.message).tool_calls) && (record(choice.message).tool_calls as unknown[]).length > 0;
302
+ if ((choice.finish_reason === 'tool_calls') !== hasTools) throw new Error('exo_response_incomplete');
303
+ } catch (error) {
304
+ failureCode = error instanceof Error && /^exo_[a-z_]+$/u.test(error.message) ? error.message : 'exo_response_invalid';
305
+ await response.body?.cancel();
306
+ throw new Error(failureCode);
307
+ }
308
+ }
309
+ return new Response(response.body, { status: response.status, headers });
310
+ };
311
+ const RuntimeClass = input.provider === 'venice' ? model.ChatCompletionsRuntime : model.ResponsesRuntime;
312
+ const runtime = new RuntimeClass({ apiKey: credential.apiKey, baseURL: providerBase.href });
313
+ for (let round = 0; round < input.maxRounds; round += 1) {
314
+ const instructions: Message[] = [{ role: 'system', content: input.systemPrompt }, { role: 'user', content: input.userPrompt }];
315
+ if (input.provider === 'venice' && input.mode === 'agent') instructions.push({
316
+ role: 'user', content: JSON.stringify({ final_response_schema: input.outputSchema,
317
+ decision_schema: input.decisionSchema, data_contract: input.dataContract }),
318
+ });
319
+ const messages = input.mode === 'provider' ? instructions : input.provider === 'venice'
320
+ ? [...instructions, ...chatRequestMessages(harness, (await context.exoharness.current.conversation.getEvents({
321
+ direction: 'asc', types: ['messages', 'tool_requested', 'tool_result'],
322
+ })).events)]
323
+ : await harness.materializePromptMessages(context.exoharness.current.conversation, instructions);
324
+ roundDispatch = false;
325
+ activeDispatch = true;
326
+ const response = await runtime.complete({ model: input.providerModel, messages, tools: definitions });
327
+ activeDispatch = false;
328
+ // Validate the actual completed provider receipt before exposing any
329
+ // model tool call to the broker, especially decision submission.
330
+ if (response.status !== 'completed') throw new Error('exo_response_incomplete');
331
+ terminalKnown = true;
332
+ if (response.model !== input.providerModel || typeof response.id !== 'string' || !response.id) throw new Error('exo_response_identity_mismatch');
333
+ if (input.provider === 'openai') {
334
+ const effectiveEffort = record(response.reasoning).effort ?? null;
335
+ if (effectiveEffort !== input.nativeReasoningEffort) throw new Error('exo_response_effort_mismatch');
336
+ addUsage(record(response.usage));
337
+ lastCompletedResponseId = response.id;
338
+ currentReceipt = { responseId: response.id, providerModel: input.providerModel, reasoningEffort: input.reasoningEffort };
339
+ }
340
+ const events = model.responseToLinguaEvents(response);
341
+ await context.exoharness.current.turn.addEvents(input.provider === 'venice' ? chatResponseEvents(events) : events);
342
+ const calls = model.responseToolCalls(response);
343
+ if (calls.length === 0) {
344
+ const rawText = textOf(response);
345
+ const text = input.provider === 'venice' && input.mode === 'agent' ? unwrapJsonFence(rawText) : rawText;
346
+ if (!text.trim()) throw new Error('exo_response_empty');
347
+ await broker('/terminal', { text, providerModel: response.model, reasoningEffort: input.reasoningEffort, responseId: response.id, usage });
348
+ return;
349
+ }
350
+ if (input.mode !== 'agent' || calls.some((call) => !allowed.has(call.request.functionName))) throw new Error('exo_tool_not_allowed');
351
+ for (const call of calls) {
352
+ const result = await broker('/tool', { callId: call.toolCallId, tool: call.request.functionName, arguments: call.request.arguments,
353
+ effectiveModel: input.requestedModel, effectiveReasoningEffort: input.reasoningEffort });
354
+ // Append the entire result directly. The default registry replaces
355
+ // results over 8k with previews, which loses assignment data.
356
+ await context.exoharness.current.turn.addEvents([harness.toolResultEvent(call.toolCallId,
357
+ result.success === true ? { ok: true, value: result.value } : { ok: false, error: 'vtx_tool_failed', value: result.value })]);
358
+ }
359
+ }
360
+ throw new Error('exo_round_limit');
361
+ } catch (error) {
362
+ const code = error instanceof Error && /^exo_[a-z_]+$/u.test(error.message) ? error.message : failureCode;
363
+ await broker('/failure', { code, dispatched, terminalKnown, usage: completedCalls ? usage : null,
364
+ responseId: currentReceipt?.responseId ?? null, providerModel: currentReceipt?.providerModel ?? null,
365
+ reasoningEffort: currentReceipt?.reasoningEffort ?? null, lastCompletedResponseId, retryAtMs }).catch(() => undefined);
366
+ // Rust surfaces errors and stacks to its caller; do not propagate SDK
367
+ // diagnostics that can contain prompts, provider bodies, or secrets.
368
+ throw new Error(code);
369
+ } finally {
370
+ globalThis.fetch = nativeFetch;
371
+ }
372
+ },
373
+ };
@@ -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.13",
19
+ package_version: "2026.9.15",
20
20
  codex_package_name: "@openai/codex",
21
21
  codex_version: "0.153.3",
22
22
  copilot_sdk_package_name: "@github/copilot-sdk",
@@ -90,7 +90,16 @@ var agent_cli_release_default = {
90
90
  binary_name: "codex.exe",
91
91
  sha256: "e5ef3c4b81d2fb861f3731c91a773d45a1973c6a0b480d6449f80bc8fd749e96"
92
92
  }
93
- }
93
+ },
94
+ pi_coding_agent_package_name: "@earendil-works/pi-coding-agent",
95
+ pi_ai_package_name: "@earendil-works/pi-ai",
96
+ pi_version: "0.85.1",
97
+ pi_support_packages: [
98
+ "@earendil-works/pi-agent-core",
99
+ "@earendil-works/pi-telemetry",
100
+ "@earendil-works/pi-tui",
101
+ "@earendil-works/chord"
102
+ ]
94
103
  };
95
104
 
96
105
  // lib/inference-host/config.ts