@lucid-agents/pi-xgate 0.1.5 → 0.1.6

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
@@ -1,6 +1,7 @@
1
1
  # xgate provider for Pi
2
2
 
3
- A Pi **v1.1.0** provider extension for wallet-paid inference. It uses native
3
+ A Pi provider extension for wallet-paid inference, supported on Pi **1.1.0** and
4
+ **0.85.1**. It uses native
4
5
  Anthropic Messages and structured OpenAI Responses through xgate. Pi executes its
5
6
  own tools; xgate never executes local tools. Text, tool calls/results and supported
6
7
  reasoning signatures survive the full agent loop.
@@ -12,7 +13,7 @@ and the reviewed model-capability snapshot; the internal router contracts packag
12
13
  is not required. With Node 24.19+ (below 25) and Pi 1.1.0:
13
14
 
14
15
  ```sh
15
- pi install npm:@lucid-agents/pi-xgate@0.1.5
16
+ pi install npm:@lucid-agents/pi-xgate@0.1.6
16
17
  ```
17
18
 
18
19
  For repository development before a release is available:
@@ -23,7 +24,8 @@ pi install ./packages/pi-xgate
23
24
  ```
24
25
 
25
26
  Pi supplies `@earendil-works/pi-ai` and `@earendil-works/pi-coding-agent` as peers.
26
- This release pins compatibility to 1.1.0; other versions need their own check.
27
+ This release supports exactly 1.1.0 and 0.85.1 (the version TaskMarket's agent
28
+ runtime pins); other versions need their own check.
27
29
 
28
30
  ## Connect a wallet broker
29
31
 
@@ -73,6 +75,83 @@ Pi cannot raise the service budget. Authorized ceilings remain consumed after
73
75
  successful settlement even when most of a deposit is returned. A new budget is an
74
76
  explicit wallet-operator action, never automatic rollover or a fresh journal.
75
77
 
78
+ ## Host-injected transport
79
+
80
+ A host that owns payment, such as TaskMarket, builds the provider around its own
81
+ transport instead of the wallet broker. The supported entry point ships built
82
+ JavaScript and type declarations:
83
+
84
+ ```ts
85
+ import {
86
+ createXgateExtension,
87
+ createXgateProvider,
88
+ type XgateTransport,
89
+ } from "@lucid-agents/pi-xgate";
90
+
91
+ const transport: XgateTransport = {
92
+ models: (signal) => hostCatalog(signal), // router catalog rows
93
+ purchase: (invocation, signal) => hostPurchase(invocation, signal),
94
+ };
95
+ // As a Pi extension: the broker configuration and `/xgate setup` are not used.
96
+ export default createXgateExtension({ transport });
97
+ // Or directly, e.g. for an agent's stream function.
98
+ const provider = await createXgateProvider(transport, {
99
+ sessionId: () => sessionId,
100
+ });
101
+ ```
102
+
103
+ `createXgateExtension({ transport })` also accepts a factory, called on load and on
104
+ `/xgate refresh`. `/xgate status`, `budget`, `receipts` and `reconcile` use the
105
+ transport's optional methods of the same names. The default extension
106
+ (`@lucid-agents/pi-xgate/extension`, and Pi's `pi.extensions` entry) keeps the
107
+ standalone `BrokerClient`.
108
+
109
+ The provider keeps catalog qualification, native codecs and invocation identity;
110
+ the transport only moves the frozen request and payment. A transport must send at
111
+ most one paid submission per `Invocation.id`, never retry a paid request, reject a
112
+ repeated ID rather than pay again, and resolve only with a verified result.
113
+ `BrokerClient` satisfies this contract.
114
+
115
+ ### TaskMarket payer
116
+
117
+ `createTaskMarketTransport` adapts TaskMarket's x402 payer (`@taskmarket/x402-payer`
118
+ API v1). That package is private, so this one declares a structural
119
+ `TaskMarketPayer` (`request`, `status`, `reconcile`) and TaskMarket injects its
120
+ instance. The payer remains the only wallet, signer and payment journal; the
121
+ adapter keeps no payment state.
122
+
123
+ ```ts
124
+ const transport = createTaskMarketTransport({
125
+ payer, // TaskMarket's X402Payer
126
+ origin: "https://api.xgate.run",
127
+ allocationId, // the agent session's inference allocation
128
+ nonInteractive: true,
129
+ balances: () => allocations.balances(), // optional, for display
130
+ });
131
+ const provider = await createXgateProvider(transport, {
132
+ sessionId: () => sessionId,
133
+ onPaid: (r) => show(r.payment), // network, scheme, asset, atomic amounts, channel
134
+ onPurchaseFailed: (f) => record(f), // original invocation ID, code, pending
135
+ });
136
+ ```
137
+
138
+ - The Pi invocation ID is the payer `invocationId`. A retried turn reuses it, so
139
+ the payer refuses to send it again (`invocation_duplicate`).
140
+ - The provider refreshes the router catalog (an unpaid `GET /v1/models`) before
141
+ every paid request. The payer's rail echoes the router quote binding and verifies
142
+ settlement.
143
+ - A result is accepted only with a `settled` charge, the Anonymous privacy
144
+ headers and a native JSON body. Every other outcome rejects with
145
+ `XgatePurchaseError`. It is `pending` unless the payer proved no unresolved
146
+ payment; unknown future charge states count as unverified.
147
+ - Pi gets a terminal `error`/`aborted` message naming the original invocation ID,
148
+ worded so Pi's transient-error retry does not fire. Only payer error codes reach
149
+ Pi, never payer messages. Hooks see only `content-type` and `cache-control`.
150
+ - Payment details are rail-neutral (`XgatePayment`): CAIP-2 network, scheme, asset,
151
+ payee, rail kind, authorized and charged atomic amounts, status, reference and,
152
+ for a single matching session deposit, its channel balance. Pi displays them
153
+ and never selects or configures a rail.
154
+
76
155
  ## Behavior and limits
77
156
 
78
157
  Responses are **buffered**, not token-streamed over the network. After a complete
@@ -118,7 +197,11 @@ from the current tool set is rejected rather than silently losing that history.
118
197
 
119
198
  Run `pnpm pi:check` at the repository root. Tests instantiate Pi's real agent core,
120
199
  execute a fixture tool, and verify the next request contains its exact result and
121
- reasoning history. Separate wallet tests cover durable financial boundaries.
200
+ reasoning history. `pnpm packages:check` installs the packed tarballs with Pi 1.1.0
201
+ and 0.85.1 and runs each real Pi CLI through the mock broker and an injected
202
+ transport, type-checks a consumer of the supported exports, and runs the TaskMarket
203
+ payer loop suite (`test/installed-loop.test.ts`: continuation, qualification,
204
+ cancellation, duplicate and lost response) against each installed Pi. Separate wallet tests cover durable financial boundaries.
122
205
  These are fixture tests. The [9 October staging campaign](../../docs/validation/pi-client-staging-20261009.md)
123
206
  separately exercised the installed Pi CLI with live providers and test tokens.
124
207
  That evidence is staging-only; it does not qualify production payment execution.
@@ -0,0 +1,34 @@
1
+ import type { AssistantMessage, Message, Model, SimpleStreamOptions, Tool, Usage } from "@earendil-works/pi-ai";
2
+ import { type Route } from "@lucid-agents/xgate-client/types";
3
+ export type Entry = {
4
+ model: Model<any>;
5
+ catalog: any;
6
+ route: Route;
7
+ efforts: string[];
8
+ };
9
+ export declare function catalogEntries(rows: any[]): Entry[];
10
+ /**
11
+ * Pi 1.1 passes a normalized transcript; Pi 0.85 passes `systemPrompt` and `tools` beside the
12
+ * messages. Structural, so published types do not name a type only one Pi version has.
13
+ */
14
+ export type PiContext = {
15
+ messages: Message[];
16
+ systemPrompt?: string;
17
+ tools?: Tool[];
18
+ };
19
+ type Turn = Exclude<Message, {
20
+ role: "system";
21
+ }>;
22
+ export declare function transcript(context: PiContext): {
23
+ system: string;
24
+ tools: Tool[];
25
+ messages: Turn[];
26
+ };
27
+ export declare function encode(entry: Entry, context: PiContext, options?: SimpleStreamOptions): any;
28
+ export declare function decode(entry: Entry, body: any): {
29
+ content: AssistantMessage["content"];
30
+ usage: Usage;
31
+ stopReason: "stop" | "toolUse";
32
+ responseId: string;
33
+ };
34
+ export {};
package/dist/codec.js ADDED
@@ -0,0 +1,510 @@
1
+ import * as pi from "@earendil-works/pi-ai";
2
+ import { XgateError } from "@lucid-agents/xgate-client/types";
3
+ import capabilities from "./generated/openai-model-capabilities.json" with { type: "json" };
4
+ import { createHash } from "node:crypto";
5
+ const need = (x, code) => {
6
+ if (!x)
7
+ throw new XgateError(code);
8
+ };
9
+ const object = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
10
+ const rate = (x) => {
11
+ need(x &&
12
+ typeof x.numerator === "string" &&
13
+ /^[0-9]{1,13}$/.test(x.numerator) &&
14
+ Number(x.numerator) <= 1e12 &&
15
+ Number.isSafeInteger(x.denominator) &&
16
+ x.denominator > 0 &&
17
+ x.denominator <= 1e6, "invalid_catalog_rate");
18
+ return Number(x.numerator) / x.denominator;
19
+ };
20
+ export function catalogEntries(rows) {
21
+ need(Array.isArray(rows) && rows.length <= 128, "invalid_catalog");
22
+ const entries = [];
23
+ for (const row of rows) {
24
+ const p = row?.execution_profile;
25
+ if (!object(p) ||
26
+ p.model !== row.id ||
27
+ !Array.isArray(row.qualified_profiles))
28
+ continue;
29
+ const anthropic = row.owned_by === "anthropic", cap = anthropic ? p.capabilities : capabilities[row.id];
30
+ if (!cap || (!anthropic && !cap.tools))
31
+ continue;
32
+ const op = anthropic ? "messages" : "responses";
33
+ const q = row.qualified_profiles.find((q) => q.model === row.id &&
34
+ q.operation === op &&
35
+ q.provider === (anthropic ? "anthropic" : "open_ai") &&
36
+ q.normalizer_revision ===
37
+ (anthropic
38
+ ? "anthropic-anonymous-usage-v3"
39
+ : "openai-anonymous-usage-v1") &&
40
+ q.transformation_revision ===
41
+ (anthropic
42
+ ? "anthropic-anonymous-messages-v1"
43
+ : "openai-anonymous-tools-v2"));
44
+ if (!q ||
45
+ row.privacy?.revision !== "anonymous-volatile-v1" ||
46
+ row.privacy.result_replay_available !== false ||
47
+ (anthropic && q.cache_mode !== "anthropic_no_cache"))
48
+ continue;
49
+ if (q.valid_until !== undefined &&
50
+ Number(q.valid_until) * 1000 <= Date.now())
51
+ continue;
52
+ if (row.lifecycle?.stop_before != null &&
53
+ Number(row.lifecycle.stop_before) * 1000 <= Date.now())
54
+ continue;
55
+ const contextWindow = Math.min(p.max_input_tokens, p.provider_limits?.context_window ??
56
+ cap.context_window ??
57
+ p.max_input_tokens), maxTokens = Math.min(8192, p.max_output_tokens);
58
+ need(Number.isSafeInteger(contextWindow) &&
59
+ contextWindow > 0 &&
60
+ Number.isSafeInteger(maxTokens) &&
61
+ maxTokens > 0, "invalid_catalog_limits");
62
+ const efforts = anthropic ? cap.effort : cap.reasoning_efforts;
63
+ const levels = [
64
+ "minimal",
65
+ "low",
66
+ "medium",
67
+ "high",
68
+ "xhigh",
69
+ "max",
70
+ ];
71
+ const thinkingLevelMap = {
72
+ off: anthropic
73
+ ? cap.thinking.disabled
74
+ ? "disabled"
75
+ : null
76
+ : efforts.length === 0 || efforts.includes("none")
77
+ ? "none"
78
+ : null,
79
+ };
80
+ for (const level of levels) {
81
+ const allowed = efforts.includes(level) ||
82
+ (anthropic &&
83
+ cap.thinking.enabled &&
84
+ ["minimal", "low", "medium", "high"].includes(level));
85
+ thinkingLevelMap[level] = allowed ? level : null;
86
+ }
87
+ entries.push({
88
+ catalog: row,
89
+ route: anthropic
90
+ ? "/providers/anthropic/v1/messages"
91
+ : "/providers/openai/v1/responses",
92
+ efforts,
93
+ model: {
94
+ id: row.id,
95
+ name: `${row.id} (xgate)`,
96
+ api: anthropic ? "xgate-messages" : "xgate-responses",
97
+ provider: "xgate",
98
+ baseUrl: row.xgate_origin ?? "https://api.xgate.run",
99
+ input: ["text"],
100
+ contextWindow,
101
+ maxTokens,
102
+ reasoning: anthropic
103
+ ? Boolean(cap.thinking?.adaptive || cap.thinking?.enabled)
104
+ : efforts.some((effort) => effort !== "none"),
105
+ thinkingLevelMap,
106
+ cost: {
107
+ input: rate(p.rates.input_uncached),
108
+ output: rate(p.rates.output),
109
+ cacheRead: rate(p.rates.input_cache_read),
110
+ cacheWrite: p.rates.input_cache_write
111
+ ? rate(p.rates.input_cache_write)
112
+ : 0,
113
+ },
114
+ inputLimits: { maxRequestBytes: 8 * 1024 * 1024 },
115
+ },
116
+ });
117
+ }
118
+ return entries;
119
+ }
120
+ export function transcript(context) {
121
+ const legacy = "systemPrompt" in context || "tools" in context;
122
+ if (typeof pi.collapseSystemMessages !== "function") {
123
+ need(!context.messages.some((m) => m.role === "system"), "unsupported_context");
124
+ const raw = context;
125
+ return {
126
+ system: raw.systemPrompt ?? "",
127
+ tools: raw.tools ?? [],
128
+ messages: raw.messages,
129
+ };
130
+ }
131
+ const collapsed = pi.collapseSystemMessages(legacy
132
+ ? pi.normalizeContext(context)
133
+ : context);
134
+ return {
135
+ system: pi.getCurrentSystemPrompt(collapsed.messages),
136
+ tools: pi.getCurrentTools(collapsed.messages),
137
+ messages: collapsed.messages.filter((m) => m.role !== "system"),
138
+ };
139
+ }
140
+ function text(content) {
141
+ if (typeof content === "string")
142
+ return content;
143
+ need(Array.isArray(content) &&
144
+ content.every((b) => b.type === "text" && typeof b.text === "string"), "text_only_request");
145
+ return content.map((b) => b.text).join("\n");
146
+ }
147
+ function callId(id) {
148
+ return /^[A-Za-z0-9_:.\-]{1,128}$/.test(id)
149
+ ? id
150
+ : "call_" + createHash("sha256").update(id).digest("hex").slice(0, 40);
151
+ }
152
+ function validateTools(tools, anthropic) {
153
+ need(tools.length <= (anthropic ? 32 : 8), "too_many_tools");
154
+ need(tools.every((t) => /^[A-Za-z0-9_-]{1,64}$/.test(t.name) &&
155
+ object(t.parameters) &&
156
+ t.parameters.type === "object"), "unsupported_tool_schema");
157
+ need(new Set(tools.map((t) => t.name)).size === tools.length, "duplicate_tools");
158
+ }
159
+ export function encode(entry, context, options = {}) {
160
+ need(options.toolChoice === undefined || options.toolChoice === "auto", "unsupported_tool_choice");
161
+ need(options.reasoning === undefined || typeof options.reasoning === "string", "unsupported_reasoning");
162
+ need(options.temperature === undefined &&
163
+ (!options.samplingParams ||
164
+ Object.keys(options.samplingParams).length === 0), "unsupported_sampling");
165
+ const anthropic = entry.route.endsWith("/messages"), { system, tools, messages } = transcript(context);
166
+ validateTools(tools, anthropic);
167
+ const max = options.maxTokens ?? Math.min(4096, entry.model.maxTokens);
168
+ need(Number.isSafeInteger(max) && max > 0 && max <= entry.model.maxTokens, "output_limit");
169
+ const requested = options.reasoning, effort = requested && entry.efforts.includes(requested)
170
+ ? requested
171
+ : entry.efforts[0];
172
+ if (requested)
173
+ need(entry.model.thinkingLevelMap?.[requested] !== null &&
174
+ (entry.efforts.includes(requested) ||
175
+ (anthropic &&
176
+ entry.catalog.execution_profile.capabilities.thinking.enabled)), "unsupported_reasoning");
177
+ const history = [];
178
+ const append = (role, content) => {
179
+ const previous = history.at(-1);
180
+ if (previous?.role === role)
181
+ previous.content.push(...content);
182
+ else
183
+ history.push({ role, content });
184
+ };
185
+ if (!anthropic && system)
186
+ history.push({ role: "system", content: system });
187
+ for (const message of messages) {
188
+ if (message.role === "user") {
189
+ const value = text(message.content);
190
+ if (anthropic)
191
+ append("user", [{ type: "text", text: value }]);
192
+ else
193
+ history.push({ role: "user", content: value });
194
+ continue;
195
+ }
196
+ if (message.role === "toolResult") {
197
+ const value = text(message.content), id = callId(message.toolCallId);
198
+ if (anthropic)
199
+ append("user", [
200
+ {
201
+ type: "tool_result",
202
+ tool_use_id: id,
203
+ content: value,
204
+ ...(message.isError ? { is_error: true } : {}),
205
+ },
206
+ ]);
207
+ else
208
+ history.push({
209
+ type: "function_call_output",
210
+ call_id: id,
211
+ output: message.isError ? "Tool error: " + value : value,
212
+ });
213
+ continue;
214
+ }
215
+ need(message.role === "assistant", "unsupported_message");
216
+ if (message.stopReason === "error" || message.stopReason === "aborted")
217
+ continue;
218
+ const same = message.provider === "xgate" &&
219
+ message.model === entry.model.id &&
220
+ message.api === entry.model.api;
221
+ const blocks = [];
222
+ for (const block of message.content) {
223
+ if (block.type === "text") {
224
+ if (anthropic) {
225
+ if (block.text)
226
+ blocks.push({ type: "text", text: block.text });
227
+ }
228
+ else if (block.text) {
229
+ let signature;
230
+ try {
231
+ signature = block.textSignature
232
+ ? JSON.parse(block.textSignature)
233
+ : undefined;
234
+ }
235
+ catch { }
236
+ const previous = history.at(-1);
237
+ if (same &&
238
+ signature?.id &&
239
+ previous?.type === "message" &&
240
+ previous.id === signature.id &&
241
+ previous.phase === signature.phase) {
242
+ previous.content.push({ type: "output_text", text: block.text });
243
+ continue;
244
+ }
245
+ history.push({
246
+ type: "message",
247
+ role: "assistant",
248
+ content: [{ type: "output_text", text: block.text }],
249
+ ...(same && signature?.id
250
+ ? {
251
+ id: signature.id,
252
+ ...(signature.phase ? { phase: signature.phase } : {}),
253
+ }
254
+ : {}),
255
+ });
256
+ }
257
+ }
258
+ else if (block.type === "toolCall") {
259
+ need(!block.namespace && object(block.arguments), "unsupported_tool_call");
260
+ const id = callId(block.id);
261
+ if (anthropic) {
262
+ need(tools.some((t) => t.name === block.name), "history_tool_not_declared");
263
+ blocks.push({
264
+ type: "tool_use",
265
+ id,
266
+ name: block.name,
267
+ input: block.arguments,
268
+ });
269
+ }
270
+ else
271
+ history.push({
272
+ type: "function_call",
273
+ call_id: id,
274
+ name: block.name,
275
+ arguments: JSON.stringify(block.arguments),
276
+ });
277
+ }
278
+ else if (block.type === "thinking" && same) {
279
+ if (anthropic) {
280
+ need(typeof block.thinkingSignature === "string" &&
281
+ block.thinkingSignature.length > 0, "missing_thinking_signature");
282
+ blocks.push(block.redacted
283
+ ? { type: "redacted_thinking", data: block.thinkingSignature }
284
+ : {
285
+ type: "thinking",
286
+ thinking: block.thinking,
287
+ signature: block.thinkingSignature,
288
+ });
289
+ }
290
+ else if (block.thinkingSignature) {
291
+ const reasoning = JSON.parse(block.thinkingSignature);
292
+ need(reasoning.type === "reasoning" &&
293
+ typeof reasoning.encrypted_content === "string", "invalid_reasoning_history");
294
+ history.push(reasoning);
295
+ }
296
+ }
297
+ }
298
+ if (anthropic && blocks.length)
299
+ append("assistant", blocks);
300
+ }
301
+ need(history.length > 0 && history.length <= 256, "history_limit");
302
+ let request;
303
+ if (anthropic) {
304
+ const cap = entry.catalog.execution_profile.capabilities;
305
+ request = {
306
+ model: entry.model.id,
307
+ messages: history,
308
+ max_tokens: max,
309
+ stream: false,
310
+ ...(system ? { system } : {}),
311
+ ...(tools.length
312
+ ? {
313
+ tools: tools.map((t) => ({
314
+ name: t.name,
315
+ description: t.description,
316
+ input_schema: t.parameters,
317
+ })),
318
+ }
319
+ : {}),
320
+ };
321
+ if (effort)
322
+ request.output_config = { effort };
323
+ if (cap.thinking.adaptive)
324
+ request.thinking = { type: "adaptive" };
325
+ else if (requested && cap.thinking.enabled) {
326
+ need(max > 1024, "thinking_output_limit");
327
+ request.thinking = {
328
+ type: "enabled",
329
+ budget_tokens: Math.min(2048, max - 1),
330
+ };
331
+ }
332
+ }
333
+ else
334
+ request = {
335
+ model: entry.model.id,
336
+ input: history,
337
+ max_output_tokens: max,
338
+ stream: false,
339
+ store: false,
340
+ include: ["reasoning.encrypted_content"],
341
+ ...(effort ? { reasoning: { effort } } : {}),
342
+ ...(tools.length
343
+ ? {
344
+ tools: tools.map((t) => ({
345
+ type: "function",
346
+ name: t.name,
347
+ description: t.description,
348
+ parameters: t.parameters,
349
+ })),
350
+ parallel_tool_calls: false,
351
+ }
352
+ : {}),
353
+ };
354
+ if (request.tools)
355
+ need(Buffer.byteLength(JSON.stringify(request.tools)) <=
356
+ (anthropic ? 65536 : 8192), "tool_schema_bound");
357
+ need(Buffer.byteLength(JSON.stringify(request)) <= 8 * 1024 * 1024, "request_bound");
358
+ return request;
359
+ }
360
+ function tokens(value) {
361
+ need(Number.isSafeInteger(value) && value >= 0, "invalid_usage");
362
+ return value;
363
+ }
364
+ export function decode(entry, body) {
365
+ need(object(body) && typeof body.id === "string" && object(body.usage), "invalid_result");
366
+ const anthropic = entry.route.endsWith("/messages"), content = [];
367
+ let stopReason;
368
+ if (anthropic) {
369
+ need(body.type === "message" &&
370
+ body.role === "assistant" &&
371
+ body.model === entry.model.id &&
372
+ ["end_turn", "tool_use", "stop_sequence"].includes(body.stop_reason) &&
373
+ Array.isArray(body.content), "invalid_result");
374
+ stopReason = body.stop_reason === "tool_use" ? "toolUse" : "stop";
375
+ for (const b of body.content) {
376
+ if (b.type === "text") {
377
+ need(typeof b.text === "string", "invalid_text");
378
+ content.push({ type: "text", text: b.text });
379
+ }
380
+ else if (b.type === "tool_use") {
381
+ need(typeof b.id === "string" &&
382
+ typeof b.name === "string" &&
383
+ object(b.input), "invalid_tool_result");
384
+ content.push({
385
+ type: "toolCall",
386
+ id: b.id,
387
+ name: b.name,
388
+ arguments: b.input,
389
+ });
390
+ }
391
+ else if (b.type === "thinking") {
392
+ need(typeof b.thinking === "string" && typeof b.signature === "string", "invalid_thinking");
393
+ content.push({
394
+ type: "thinking",
395
+ thinking: b.thinking,
396
+ thinkingSignature: b.signature,
397
+ });
398
+ }
399
+ else if (b.type === "redacted_thinking") {
400
+ need(typeof b.data === "string", "invalid_thinking");
401
+ content.push({
402
+ type: "thinking",
403
+ thinking: "",
404
+ thinkingSignature: b.data,
405
+ redacted: true,
406
+ });
407
+ }
408
+ else
409
+ throw new XgateError("unsupported_result_block");
410
+ }
411
+ }
412
+ else {
413
+ const cap = capabilities[entry.model.id];
414
+ need(cap.response_models.includes(body.model) &&
415
+ body.status === "completed" &&
416
+ !body.error &&
417
+ !body.incomplete_details &&
418
+ Array.isArray(body.output), "invalid_result");
419
+ stopReason = "stop";
420
+ for (const b of body.output) {
421
+ if (b.type === "message") {
422
+ need(b.status === "completed" && Array.isArray(b.content), "invalid_result");
423
+ for (const t of b.content) {
424
+ need(t.type === "output_text" && typeof t.text === "string", "invalid_text");
425
+ content.push({
426
+ type: "text",
427
+ text: t.text,
428
+ ...(b.id
429
+ ? {
430
+ textSignature: JSON.stringify({
431
+ v: 1,
432
+ id: b.id,
433
+ ...(b.phase ? { phase: b.phase } : {}),
434
+ }),
435
+ }
436
+ : {}),
437
+ });
438
+ }
439
+ }
440
+ else if (b.type === "function_call") {
441
+ need(typeof b.call_id === "string" &&
442
+ typeof b.name === "string" &&
443
+ typeof b.arguments === "string", "invalid_tool_result");
444
+ const args = JSON.parse(b.arguments);
445
+ need(object(args), "invalid_tool_result");
446
+ content.push({
447
+ type: "toolCall",
448
+ id: b.call_id,
449
+ name: b.name,
450
+ arguments: args,
451
+ });
452
+ stopReason = "toolUse";
453
+ }
454
+ else if (b.type === "reasoning") {
455
+ need(Array.isArray(b.summary), "invalid_thinking");
456
+ const summary = b.summary
457
+ .map((s) => {
458
+ need(s.type === "summary_text" && typeof s.text === "string", "invalid_thinking");
459
+ return s.text;
460
+ })
461
+ .join("\n");
462
+ need(typeof b.encrypted_content === "string" &&
463
+ b.encrypted_content.length > 0, "missing_encrypted_reasoning");
464
+ content.push({
465
+ type: "thinking",
466
+ thinking: summary,
467
+ thinkingSignature: JSON.stringify({
468
+ type: "reasoning",
469
+ id: b.id,
470
+ summary: b.summary,
471
+ encrypted_content: b.encrypted_content,
472
+ ...(b.status === "completed" ? { status: b.status } : {}),
473
+ }),
474
+ });
475
+ }
476
+ else
477
+ throw new XgateError("unsupported_result_block");
478
+ }
479
+ }
480
+ need(content.some((c) => (c.type === "text" && c.text.length > 0) || c.type === "toolCall"), "empty_result");
481
+ need((stopReason === "toolUse") === content.some((c) => c.type === "toolCall"), "tool_stop_mismatch");
482
+ const u = body.usage, read = tokens(anthropic
483
+ ? (u.cache_read_input_tokens ?? 0)
484
+ : u.input_tokens_details?.cached_tokens), write = tokens(anthropic
485
+ ? (u.cache_creation_input_tokens ?? 0)
486
+ : (u.input_tokens_details?.cache_write_tokens ?? 0)), totalInput = tokens(u.input_tokens), output = tokens(u.output_tokens), input = anthropic ? totalInput : totalInput - read - write;
487
+ need(input >= 0 && (!anthropic || (read === 0 && write === 0)), "cache_usage_mismatch");
488
+ const rates = entry.model.cost;
489
+ const cost = {
490
+ input: (input * rates.input) / 1e6,
491
+ output: (output * rates.output) / 1e6,
492
+ cacheRead: (read * rates.cacheRead) / 1e6,
493
+ cacheWrite: (write * rates.cacheWrite) / 1e6,
494
+ total: 0,
495
+ };
496
+ cost.total = cost.input + cost.output + cost.cacheRead + cost.cacheWrite;
497
+ return {
498
+ content,
499
+ usage: {
500
+ input,
501
+ output,
502
+ cacheRead: read,
503
+ cacheWrite: write,
504
+ totalTokens: input + read + write + output,
505
+ cost,
506
+ },
507
+ stopReason,
508
+ responseId: body.id,
509
+ };
510
+ }
@@ -0,0 +1,16 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { XgatePayment, XgateTransport } from "./transport.ts";
3
+ export interface XgateExtensionOptions {
4
+ /**
5
+ * Host-managed transport, or a factory called on load and `/xgate refresh`. When set, the
6
+ * extension reads no broker configuration and `/xgate setup` is unavailable. Without it, the
7
+ * standalone `BrokerClient` from `XGATE_CONFIG` or `~/.pi/agent/xgate.json` is used.
8
+ */
9
+ transport?: XgateTransport | (() => XgateTransport | Promise<XgateTransport>);
10
+ }
11
+ /** Rail-neutral: atomic units of the named asset; decimals are not assumed. */
12
+ export declare function paymentStatus(p: XgatePayment): string;
13
+ /** Pi extension factory. The default export is the standalone wallet-broker extension. */
14
+ export declare function createXgateExtension(options?: XgateExtensionOptions): (pi: ExtensionAPI) => Promise<void>;
15
+ declare const _default: (pi: ExtensionAPI) => Promise<void>;
16
+ export default _default;