@lucid-agents/pi-xgate 0.1.5 → 0.1.7
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 +103 -7
- package/dist/codec.d.ts +40 -0
- package/dist/codec.js +553 -0
- package/dist/extension.d.ts +16 -0
- package/dist/extension.js +132 -0
- package/dist/generated/openai-model-capabilities.json +847 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/provider.d.ts +24 -0
- package/dist/provider.js +243 -0
- package/dist/taskmarket.d.ts +131 -0
- package/dist/taskmarket.js +275 -0
- package/dist/transport.d.ts +72 -0
- package/dist/transport.js +21 -0
- package/package.json +21 -9
- package/src/codec.ts +117 -26
- package/src/extension.ts +60 -12
- package/src/index.ts +45 -0
- package/src/provider.ts +68 -26
- package/src/taskmarket.ts +484 -0
- package/src/transport.ts +94 -0
package/src/extension.ts
CHANGED
|
@@ -8,6 +8,15 @@ import { readFileSync, lstatSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
8
8
|
import { dirname, isAbsolute, join } from "node:path";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { createXgateProvider } from "./provider.ts";
|
|
11
|
+
import type { XgatePayment, XgateTransport } from "./transport.ts";
|
|
12
|
+
export interface XgateExtensionOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Host-managed transport, or a factory called on load and `/xgate refresh`. When set, the
|
|
15
|
+
* extension reads no broker configuration and `/xgate setup` is unavailable. Without it, the
|
|
16
|
+
* standalone `BrokerClient` from `XGATE_CONFIG` or `~/.pi/agent/xgate.json` is used.
|
|
17
|
+
*/
|
|
18
|
+
transport?: XgateTransport | (() => XgateTransport | Promise<XgateTransport>);
|
|
19
|
+
}
|
|
11
20
|
function privateText(path: string, max = 8192): string {
|
|
12
21
|
if (!isAbsolute(path)) throw new XgateError("absolute_config_path_required");
|
|
13
22
|
const s = lstatSync(path);
|
|
@@ -20,25 +29,57 @@ function privateText(path: string, max = 8192): string {
|
|
|
20
29
|
throw new XgateError("private_config_required");
|
|
21
30
|
return readFileSync(path, "utf8").trim();
|
|
22
31
|
}
|
|
23
|
-
|
|
32
|
+
function standalone(configPath: string): BrokerClient {
|
|
33
|
+
const cfg = JSON.parse(privateText(configPath));
|
|
34
|
+
if (typeof cfg.url !== "string" || typeof cfg.tokenFile !== "string")
|
|
35
|
+
throw new XgateError("invalid_config");
|
|
36
|
+
return new BrokerClient(cfg.url, privateText(cfg.tokenFile, 128));
|
|
37
|
+
}
|
|
38
|
+
/** Rail-neutral: atomic units of the named asset; decimals are not assumed. */
|
|
39
|
+
export function paymentStatus(p: XgatePayment): string {
|
|
40
|
+
const left = p.channel
|
|
41
|
+
? `; channel ${p.channel.refundableAtomic} refundable`
|
|
42
|
+
: "";
|
|
43
|
+
return `xgate paid ${p.chargedAtomic ?? "?"}/${p.authorizedAtomic} ${p.asset} on ${p.network} (${p.scheme})${left}`;
|
|
44
|
+
}
|
|
45
|
+
/** Pi extension factory. The default export is the standalone wallet-broker extension. */
|
|
46
|
+
export function createXgateExtension(
|
|
47
|
+
options: XgateExtensionOptions = {},
|
|
48
|
+
): (pi: ExtensionAPI) => Promise<void> {
|
|
49
|
+
return (pi) => register(pi, options);
|
|
50
|
+
}
|
|
51
|
+
export default createXgateExtension();
|
|
52
|
+
async function register(
|
|
53
|
+
pi: ExtensionAPI,
|
|
54
|
+
options: XgateExtensionOptions,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
const injected = options.transport;
|
|
24
57
|
const configPath =
|
|
25
58
|
process.env.XGATE_CONFIG ?? join(homedir(), ".pi", "agent", "xgate.json");
|
|
26
|
-
let broker:
|
|
59
|
+
let broker: XgateTransport | undefined;
|
|
27
60
|
let context: ExtensionContext | undefined;
|
|
28
61
|
const load = async () => {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
(
|
|
62
|
+
const next =
|
|
63
|
+
injected === undefined
|
|
64
|
+
? standalone(configPath)
|
|
65
|
+
: typeof injected === "function"
|
|
66
|
+
? await injected()
|
|
67
|
+
: injected;
|
|
68
|
+
const provider = await createXgateProvider(next, {
|
|
69
|
+
sessionId: () => context?.sessionManager.getSessionId() ?? "standalone",
|
|
70
|
+
onPaid: (r) =>
|
|
37
71
|
context?.ui.setStatus(
|
|
38
72
|
"xgate",
|
|
39
|
-
|
|
73
|
+
r.payment
|
|
74
|
+
? paymentStatus(r.payment)
|
|
75
|
+
: `xgate paid ${(Number(r.settlement.chargedAtomic) / 1e6).toFixed(6)}`,
|
|
40
76
|
),
|
|
41
|
-
|
|
77
|
+
onPurchaseFailed: (f) =>
|
|
78
|
+
context?.ui.setStatus(
|
|
79
|
+
"xgate",
|
|
80
|
+
`xgate ${f.pending ? "unresolved" : "failed"}: ${f.invocationId}`,
|
|
81
|
+
),
|
|
82
|
+
});
|
|
42
83
|
pi.registerProvider(provider);
|
|
43
84
|
broker = next;
|
|
44
85
|
};
|
|
@@ -63,6 +104,8 @@ export default async function extension(pi: ExtensionAPI): Promise<void> {
|
|
|
63
104
|
const [command, id] = args.trim().split(/\s+/);
|
|
64
105
|
try {
|
|
65
106
|
if (command === "setup") {
|
|
107
|
+
if (injected !== undefined)
|
|
108
|
+
throw new XgateError("transport_managed_by_host");
|
|
66
109
|
if (!ctx.hasUI)
|
|
67
110
|
throw new XgateError("configure_XGATE_CONFIG_for_headless_mode");
|
|
68
111
|
const url = await ctx.ui.input(
|
|
@@ -97,11 +140,15 @@ export default async function extension(pi: ExtensionAPI): Promise<void> {
|
|
|
97
140
|
if (!broker) throw new XgateError("wallet_not_configured");
|
|
98
141
|
if (command === "reconcile") {
|
|
99
142
|
if (!id) throw new XgateError("purchase_id_required");
|
|
143
|
+
if (!broker.reconcile)
|
|
144
|
+
throw new XgateError("unsupported_by_transport");
|
|
100
145
|
const result = await broker.reconcile(id);
|
|
101
146
|
ctx.ui.notify(JSON.stringify(result), "info");
|
|
102
147
|
return;
|
|
103
148
|
}
|
|
104
149
|
if (command === "receipts") {
|
|
150
|
+
if (!broker.receipts)
|
|
151
|
+
throw new XgateError("unsupported_by_transport");
|
|
105
152
|
ctx.ui.notify(
|
|
106
153
|
JSON.stringify(await broker.receipts(), null, 2),
|
|
107
154
|
"info",
|
|
@@ -109,6 +156,7 @@ export default async function extension(pi: ExtensionAPI): Promise<void> {
|
|
|
109
156
|
return;
|
|
110
157
|
}
|
|
111
158
|
if (command === "status" || command === "budget" || !command) {
|
|
159
|
+
if (!broker.status) throw new XgateError("unsupported_by_transport");
|
|
112
160
|
const status = await broker.status();
|
|
113
161
|
ctx.ui.notify(JSON.stringify(status, null, 2), "info");
|
|
114
162
|
return;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Supported entry point. Hosts build the provider around their own transport; the default
|
|
2
|
+
// Pi extension (`./extension`) keeps the standalone wallet broker.
|
|
3
|
+
export {
|
|
4
|
+
createXgateProvider,
|
|
5
|
+
invocationId,
|
|
6
|
+
type Broker,
|
|
7
|
+
type XgateProviderOptions,
|
|
8
|
+
type XgatePurchaseFailure,
|
|
9
|
+
} from "./provider.ts";
|
|
10
|
+
export {
|
|
11
|
+
XgatePurchaseError,
|
|
12
|
+
type XgateChannelBalance,
|
|
13
|
+
type XgatePayment,
|
|
14
|
+
type XgatePurchaseResult,
|
|
15
|
+
type XgateResponseHead,
|
|
16
|
+
type XgateTransport,
|
|
17
|
+
} from "./transport.ts";
|
|
18
|
+
export {
|
|
19
|
+
createXgateExtension,
|
|
20
|
+
type XgateExtensionOptions,
|
|
21
|
+
} from "./extension.ts";
|
|
22
|
+
export { XgateError } from "@lucid-agents/xgate-client/types";
|
|
23
|
+
export type {
|
|
24
|
+
Invocation,
|
|
25
|
+
PurchaseResult,
|
|
26
|
+
Route,
|
|
27
|
+
Settlement,
|
|
28
|
+
} from "@lucid-agents/xgate-client/types";
|
|
29
|
+
export {
|
|
30
|
+
createTaskMarketTransport,
|
|
31
|
+
type TaskMarketChargeStatus,
|
|
32
|
+
type TaskMarketFundingBalance,
|
|
33
|
+
type TaskMarketInvocationState,
|
|
34
|
+
type TaskMarketInvocationStatus,
|
|
35
|
+
type TaskMarketPayCallOptions,
|
|
36
|
+
type TaskMarketPayErrorCode,
|
|
37
|
+
type TaskMarketPayer,
|
|
38
|
+
type TaskMarketPayerLimits,
|
|
39
|
+
type TaskMarketPayRequest,
|
|
40
|
+
type TaskMarketPayResponse,
|
|
41
|
+
type TaskMarketPayResult,
|
|
42
|
+
type TaskMarketPaymentSummary,
|
|
43
|
+
type TaskMarketPaymentTerms,
|
|
44
|
+
type TaskMarketTransportOptions,
|
|
45
|
+
} from "./taskmarket.ts";
|
package/src/provider.ts
CHANGED
|
@@ -5,26 +5,34 @@ import {
|
|
|
5
5
|
type Model,
|
|
6
6
|
type Provider,
|
|
7
7
|
type SimpleStreamOptions,
|
|
8
|
-
type TranscriptContext,
|
|
9
8
|
} from "@earendil-works/pi-ai";
|
|
9
|
+
import { XgateError } from "@lucid-agents/xgate-client/types";
|
|
10
|
+
import { catalogEntries, encode, decode, type PiContext } from "./codec.ts";
|
|
10
11
|
import {
|
|
11
|
-
|
|
12
|
-
type
|
|
13
|
-
type
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
XgatePurchaseError,
|
|
13
|
+
type XgatePayment,
|
|
14
|
+
type XgatePurchaseResult,
|
|
15
|
+
type XgateTransport,
|
|
16
|
+
} from "./transport.ts";
|
|
16
17
|
const hash = (value: string): string =>
|
|
17
18
|
createHash("sha256").update(value).digest("hex");
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
/** @deprecated Use `XgateTransport`. */
|
|
20
|
+
export type Broker = XgateTransport;
|
|
21
|
+
export interface XgateProviderOptions {
|
|
22
|
+
/** Invocation identity scope when Pi supplies no `sessionId`. Defaults to `"standalone"`. */
|
|
23
|
+
sessionId?: () => string;
|
|
24
|
+
/** Display-only notification of a verified paid result; it cannot change the outcome. */
|
|
25
|
+
onPaid?: (result: XgatePurchaseResult) => void;
|
|
26
|
+
/** Terminal notification of a purchase that ended without a verified result. */
|
|
27
|
+
onPurchaseFailed?: (failure: XgatePurchaseFailure) => void;
|
|
28
|
+
}
|
|
29
|
+
export interface XgatePurchaseFailure {
|
|
30
|
+
/** The original invocation ID; reconcile it, never retry under a new one. */
|
|
31
|
+
invocationId: string;
|
|
32
|
+
code: string;
|
|
33
|
+
/** False only when the transport proved no unresolved payment. */
|
|
34
|
+
pending: boolean;
|
|
35
|
+
payment?: XgatePayment;
|
|
28
36
|
}
|
|
29
37
|
const zero = () => ({
|
|
30
38
|
input: 0,
|
|
@@ -37,9 +45,14 @@ const zero = () => ({
|
|
|
37
45
|
/** Stable across automatic retries and payload hooks; a changed paid payload cannot claim a new intent. */
|
|
38
46
|
export function invocationId(
|
|
39
47
|
model: string,
|
|
40
|
-
context:
|
|
48
|
+
context: PiContext,
|
|
41
49
|
session: string,
|
|
42
50
|
): string {
|
|
51
|
+
// Pi 0.85 carries the prompt and tools beside the messages; Pi 1.1 carries them inside.
|
|
52
|
+
const legacy =
|
|
53
|
+
"systemPrompt" in context || "tools" in context
|
|
54
|
+
? { systemPrompt: context.systemPrompt, tools: context.tools }
|
|
55
|
+
: {};
|
|
43
56
|
return hash(
|
|
44
57
|
JSON.stringify({
|
|
45
58
|
session,
|
|
@@ -49,18 +62,22 @@ export function invocationId(
|
|
|
49
62
|
m.role !== "assistant" ||
|
|
50
63
|
!["error", "aborted"].includes(m.stopReason),
|
|
51
64
|
),
|
|
65
|
+
...legacy,
|
|
52
66
|
}),
|
|
53
67
|
);
|
|
54
68
|
}
|
|
55
69
|
export async function createXgateProvider(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
transport: XgateTransport,
|
|
71
|
+
{
|
|
72
|
+
sessionId: identity = () => "standalone",
|
|
73
|
+
onPaid,
|
|
74
|
+
onPurchaseFailed,
|
|
75
|
+
}: XgateProviderOptions = {},
|
|
59
76
|
): Promise<Provider> {
|
|
60
|
-
let entries = catalogEntries(await
|
|
77
|
+
let entries = catalogEntries(await transport.models());
|
|
61
78
|
const stream = (
|
|
62
79
|
model: Model<any>,
|
|
63
|
-
context:
|
|
80
|
+
context: PiContext,
|
|
64
81
|
options: SimpleStreamOptions = {},
|
|
65
82
|
) => {
|
|
66
83
|
const events = createAssistantMessageEventStream();
|
|
@@ -86,10 +103,11 @@ export async function createXgateProvider(
|
|
|
86
103
|
stopReason: "pending",
|
|
87
104
|
timestamp: Date.now(),
|
|
88
105
|
};
|
|
106
|
+
let attempted: string | undefined, paid: XgatePurchaseResult | undefined;
|
|
89
107
|
void (async () => {
|
|
90
108
|
try {
|
|
91
109
|
options.signal?.throwIfAborted();
|
|
92
|
-
const current = catalogEntries(await
|
|
110
|
+
const current = catalogEntries(await transport.models(options.signal)),
|
|
93
111
|
entry = current.find(
|
|
94
112
|
(e) => e.model.id === model.id && e.model.api === model.api,
|
|
95
113
|
);
|
|
@@ -127,7 +145,8 @@ export async function createXgateProvider(
|
|
|
127
145
|
options.sessionId ?? identity(),
|
|
128
146
|
);
|
|
129
147
|
let observed = false;
|
|
130
|
-
|
|
148
|
+
attempted = id;
|
|
149
|
+
const result = await transport.purchase(
|
|
131
150
|
{ id, route: entry.route, body },
|
|
132
151
|
options.signal,
|
|
133
152
|
async (response) => {
|
|
@@ -135,7 +154,8 @@ export async function createXgateProvider(
|
|
|
135
154
|
await options.onResponse?.(response, model);
|
|
136
155
|
},
|
|
137
156
|
);
|
|
138
|
-
|
|
157
|
+
paid = result;
|
|
158
|
+
// In-process injected transports have no HTTP body; retain the observation hook there too.
|
|
139
159
|
if (!observed)
|
|
140
160
|
await options.onResponse?.(
|
|
141
161
|
{
|
|
@@ -234,6 +254,28 @@ export async function createXgateProvider(
|
|
|
234
254
|
: error instanceof XgateError
|
|
235
255
|
? error.message
|
|
236
256
|
: "Xgate operation stopped. Inspect /xgate status before requesting another purchase.";
|
|
257
|
+
if (attempted !== undefined) {
|
|
258
|
+
// After a verified result the charge is known even if Pi discards the content.
|
|
259
|
+
const payment =
|
|
260
|
+
paid?.payment ??
|
|
261
|
+
(error instanceof XgatePurchaseError ? error.payment : undefined);
|
|
262
|
+
const failure: XgatePurchaseFailure = {
|
|
263
|
+
invocationId: attempted,
|
|
264
|
+
code: error instanceof XgateError ? error.code : "purchase_failed",
|
|
265
|
+
pending:
|
|
266
|
+
paid === undefined &&
|
|
267
|
+
!(error instanceof XgatePurchaseError && !error.pending),
|
|
268
|
+
...(payment ? { payment } : {}),
|
|
269
|
+
};
|
|
270
|
+
// The original ID is the only reconciliation key. "Billing" also keeps Pi's transient
|
|
271
|
+
// error classifier from retrying a message whose hex ID happens to contain "500".
|
|
272
|
+
message.errorMessage += ` Billing invocation: ${attempted}.`;
|
|
273
|
+
try {
|
|
274
|
+
onPurchaseFailed?.(failure);
|
|
275
|
+
} catch {
|
|
276
|
+
/* Diagnostics cannot change the terminal outcome. */
|
|
277
|
+
}
|
|
278
|
+
}
|
|
237
279
|
events.push({
|
|
238
280
|
type: "error",
|
|
239
281
|
reason: message.stopReason,
|
|
@@ -257,7 +299,7 @@ export async function createXgateProvider(
|
|
|
257
299
|
getModels: () => entries.map((e) => e.model),
|
|
258
300
|
refreshModels: async (context) => {
|
|
259
301
|
if (!context.allowNetwork) return;
|
|
260
|
-
const updated = catalogEntries(await
|
|
302
|
+
const updated = catalogEntries(await transport.models(context.signal));
|
|
261
303
|
await context.publish({
|
|
262
304
|
update: () => {
|
|
263
305
|
entries = updated;
|