@nevermined-io/payments 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -93
- package/dist/x402/langchain/agent.d.ts +96 -0
- package/dist/x402/langchain/agent.d.ts.map +1 -0
- package/dist/x402/langchain/agent.js +121 -0
- package/dist/x402/langchain/agent.js.map +1 -0
- package/dist/x402/langchain/decorator.d.ts +43 -4
- package/dist/x402/langchain/decorator.d.ts.map +1 -1
- package/dist/x402/langchain/decorator.js +173 -6
- package/dist/x402/langchain/decorator.js.map +1 -1
- package/dist/x402/langchain/index.d.ts +2 -1
- package/dist/x402/langchain/index.d.ts.map +1 -1
- package/dist/x402/langchain/index.js +2 -1
- package/dist/x402/langchain/index.js.map +1 -1
- package/dist/x402/langsmith/index.d.ts +15 -0
- package/dist/x402/langsmith/index.d.ts.map +1 -0
- package/dist/x402/langsmith/index.js +15 -0
- package/dist/x402/langsmith/index.js.map +1 -0
- package/dist/x402/langsmith/spans.d.ts +163 -0
- package/dist/x402/langsmith/spans.d.ts.map +1 -0
- package/dist/x402/langsmith/spans.js +341 -0
- package/dist/x402/langsmith/spans.js.map +1 -0
- package/package.json +16 -2
package/README.md
CHANGED
|
@@ -19,16 +19,15 @@ The Nevermined Payments Library is a TypeScript SDK that allows AI Builders and
|
|
|
19
19
|
|
|
20
20
|
The Payments Library enables:
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
- Easy registration and discovery of AI agents and the payment plans required to access them. All agents registered in Nevermined expose their metadata in a generic way, making them searchable and discoverable for specific purposes.
|
|
23
|
+
- Flexible definition of pricing options and how AI agents can be queried. This is achieved through payment plans (based on time or credits) and consumption costs (fixed per request or dynamic). All of this can be defined by the AI builder or agent during the registration process.
|
|
24
|
+
- Subscribers (humans or other agents) to purchase credits that grant access to AI agent services. Payments can be made in crypto or fiat via Stripe integration. The protocol registers the payment and credits distribution settlement on-chain.
|
|
25
|
+
- Agents or users with access credits to query other AI agents. Nevermined authorizes only users with sufficient balance and keeps track of their credit usage.
|
|
27
26
|
|
|
28
27
|
The library is designed for use in browser environments or as part of AI Agents:
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
- In a browser, the library provides a simple way to connect to the Nevermined protocol, allowing users to query AI Agents or publish their own.
|
|
30
|
+
- As part of an AI Agent, the library allows the agent to query other agents programmatically. Additionally, agents can use the library to expose their own services and make them available to other agents or humans.
|
|
32
31
|
|
|
33
32
|
## Quickstart
|
|
34
33
|
|
|
@@ -41,6 +40,7 @@ npm install @nevermined-io/payments@^1.1
|
|
|
41
40
|
```
|
|
42
41
|
|
|
43
42
|
> Pin the major version (or rely on a committed lockfile) so SDK upgrades are explicit. Always commit `package-lock.json` / `pnpm-lock.yaml`.
|
|
43
|
+
|
|
44
44
|
## A2A Integration (Agents‑to‑Agents)
|
|
45
45
|
|
|
46
46
|
Nevermined Payments integrates with the A2A protocol to authorize and charge per request between agents:
|
|
@@ -78,13 +78,13 @@ Add a payment extension under `capabilities.extensions` carrying Nevermined meta
|
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
Important notes:
|
|
81
|
+
|
|
81
82
|
- The `url` must match exactly the URL registered in Nevermined for the agent/plan.
|
|
82
83
|
- The final streaming event must include `metadata.creditsUsed` with the consumed cost.
|
|
83
84
|
|
|
84
|
-
|
|
85
85
|
## Requirements
|
|
86
86
|
|
|
87
|
-
To use the Nevermined Payments Library, you need to get your Nevermined API key. You can get yours freely from the
|
|
87
|
+
To use the Nevermined Payments Library, you need to get your Nevermined API key. You can get yours freely from the [Nevermined App](https://nevermined.app).
|
|
88
88
|
|
|
89
89
|
### Environments
|
|
90
90
|
|
|
@@ -92,6 +92,15 @@ To use the Nevermined Payments Library, you need to get your Nevermined API key.
|
|
|
92
92
|
|
|
93
93
|
Pick the environment that matches where your agent and plans are registered. The agent card `url` must belong to that environment.
|
|
94
94
|
|
|
95
|
+
### Optional peer dependencies
|
|
96
|
+
|
|
97
|
+
The core SDK has no required runtime peers. Some integrations are gated behind optional peer dependencies you install only if you use them:
|
|
98
|
+
|
|
99
|
+
- **LangChain tool protection** (`@nevermined-io/payments/langchain`) — requires `@langchain/core` and `@langchain/langgraph >=1.0.0 <2`.
|
|
100
|
+
- **LangSmith observability** (`@nevermined-io/payments/langsmith`, and the spans the `requiresPayment` wrapper emits when `LANGSMITH_TRACING=true`) — requires **`langsmith >=0.7`**. The spans rely on `getCurrentRunTree` (exported from `langsmith/singletons/traceable`) and the merging `RunTree.metadata` setter, both of which are only reliably present from `0.7` onward. Install it yourself: `pnpm add langsmith`.
|
|
101
|
+
|
|
102
|
+
These are declared as optional peers, so npm/pnpm will not pull them in automatically and the SDK no-ops cleanly when they are absent.
|
|
103
|
+
|
|
95
104
|
### Initialize the Payments library in the Browser
|
|
96
105
|
|
|
97
106
|
This is a browser only method. Here we have an example using react.
|
|
@@ -124,7 +133,7 @@ export default function Home() {
|
|
|
124
133
|
### Initialize the Payments library in an AI Agent
|
|
125
134
|
|
|
126
135
|
```typescript
|
|
127
|
-
import { Payments } from
|
|
136
|
+
import { Payments } from '@nevermined-io/payments'
|
|
128
137
|
|
|
129
138
|
const payments = Payments.getInstance({
|
|
130
139
|
nvmApiKey,
|
|
@@ -138,19 +147,29 @@ Once the app is initialized we can create a payment plan:
|
|
|
138
147
|
|
|
139
148
|
```typescript
|
|
140
149
|
const planMetadata: PlanMetadata = {
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
name: 'E2E test Payments Plan',
|
|
151
|
+
}
|
|
143
152
|
const priceConfig = payments.plans.getERC20PriceConfig(20n, ERC20_ADDRESS, builderAddress)
|
|
144
153
|
const creditsConfig = payments.plans.getFixedCreditsConfig(100n)
|
|
145
|
-
const { planId } = await payments.plans.registerCreditsPlan(
|
|
154
|
+
const { planId } = await payments.plans.registerCreditsPlan(
|
|
155
|
+
planMetadata,
|
|
156
|
+
priceConfig,
|
|
157
|
+
creditsConfig,
|
|
158
|
+
)
|
|
146
159
|
```
|
|
147
160
|
|
|
148
161
|
Or register a plan limited by time:
|
|
149
162
|
|
|
150
163
|
```typescript
|
|
151
164
|
const priceConfig = payments.plans.getERC20PriceConfig(50n, ERC20_ADDRESS, builderAddress)
|
|
152
|
-
const expirablePlanConfig = payments.plans.getExpirableDurationConfig(
|
|
153
|
-
|
|
165
|
+
const expirablePlanConfig = payments.plans.getExpirableDurationConfig(
|
|
166
|
+
payments.plans.ONE_DAY_DURATION,
|
|
167
|
+
) // 1 day
|
|
168
|
+
const response = await payments.plans.registerTimePlan(
|
|
169
|
+
planMetadata,
|
|
170
|
+
priceConfig,
|
|
171
|
+
expirablePlanConfig,
|
|
172
|
+
)
|
|
154
173
|
```
|
|
155
174
|
|
|
156
175
|
You can also create trial plans (free plans that can only be purchased once):
|
|
@@ -166,7 +185,7 @@ const trialCreditsConfig = payments.plans.getFixedCreditsConfig(10n)
|
|
|
166
185
|
const trialPlan = await payments.plans.registerCreditsTrialPlan(
|
|
167
186
|
trialPlanMetadata,
|
|
168
187
|
freePriceConfig,
|
|
169
|
-
trialCreditsConfig
|
|
188
|
+
trialCreditsConfig,
|
|
170
189
|
)
|
|
171
190
|
|
|
172
191
|
// Time-based trial plan
|
|
@@ -174,7 +193,7 @@ const timeTrialConfig = payments.plans.getExpirableDurationConfig(ONE_DAY_DURATI
|
|
|
174
193
|
const timeTrialPlan = await payments.plans.registerTimeTrialPlan(
|
|
175
194
|
trialPlanMetadata,
|
|
176
195
|
freePriceConfig,
|
|
177
|
-
timeTrialConfig
|
|
196
|
+
timeTrialConfig,
|
|
178
197
|
)
|
|
179
198
|
```
|
|
180
199
|
|
|
@@ -192,12 +211,13 @@ const agentMetadata = {
|
|
|
192
211
|
// The API that the agent will expose
|
|
193
212
|
const agentApi = {
|
|
194
213
|
endpoints: [
|
|
195
|
-
{
|
|
196
|
-
{
|
|
197
|
-
]
|
|
214
|
+
{ POST: `https://example.com/api/v1/agents/:agentId/tasks` },
|
|
215
|
+
{ GET: `https://example.com/api/v1/agents/:agentId/tasks/invoke` },
|
|
216
|
+
],
|
|
217
|
+
}
|
|
198
218
|
|
|
199
219
|
// This is the list of payment plans that the agent will accept
|
|
200
|
-
const paymentPlans = [
|
|
220
|
+
const paymentPlans = [creditsPlanId, expirablePlanId]
|
|
201
221
|
const result = await payments.agents.registerAgent(agentMetadata, agentApi, paymentPlans)
|
|
202
222
|
```
|
|
203
223
|
|
|
@@ -212,9 +232,7 @@ const agentMetadata = {
|
|
|
212
232
|
}
|
|
213
233
|
|
|
214
234
|
const agentApi = {
|
|
215
|
-
endpoints: [
|
|
216
|
-
{ 'POST': 'https://example.com/api/v1/agents/:agentId/tasks' }
|
|
217
|
-
]
|
|
235
|
+
endpoints: [{ POST: 'https://example.com/api/v1/agents/:agentId/tasks' }],
|
|
218
236
|
}
|
|
219
237
|
|
|
220
238
|
const planMetadata = { name: 'Basic Plan' }
|
|
@@ -228,11 +246,12 @@ const { agentId, planId, txHash } = await payments.agents.registerAgentAndPlan(
|
|
|
228
246
|
planMetadata,
|
|
229
247
|
priceConfig,
|
|
230
248
|
creditsConfig,
|
|
231
|
-
'time' // Optionally set access limit to 'time' or 'credits'
|
|
249
|
+
'time', // Optionally set access limit to 'time' or 'credits'
|
|
232
250
|
)
|
|
233
251
|
```
|
|
234
252
|
|
|
235
253
|
The `accessLimit` parameter is optional. If not specified, it's automatically inferred:
|
|
254
|
+
|
|
236
255
|
- `'credits'` if `creditsConfig.durationSecs === 0n` (non-expirable)
|
|
237
256
|
- `'time'` if `creditsConfig.durationSecs > 0n` (expirable)
|
|
238
257
|
|
|
@@ -261,7 +280,7 @@ const agentHTTPOptions = {
|
|
|
261
280
|
headers: {
|
|
262
281
|
Accept: 'application/json',
|
|
263
282
|
'Content-Type': 'application/json',
|
|
264
|
-
'payment-signature': accessToken
|
|
283
|
+
'payment-signature': accessToken,
|
|
265
284
|
},
|
|
266
285
|
}
|
|
267
286
|
const response = await fetch(new URL(agentURL), agentHTTPOptions)
|
|
@@ -281,7 +300,7 @@ MCP servers expose handlers (tools/resources/prompts) with their logical URLs, s
|
|
|
281
300
|
|
|
282
301
|
### Why Nevermined Payments?
|
|
283
302
|
|
|
284
|
-
While MCP defines
|
|
303
|
+
While MCP defines _what_ an agent can do, it doesn't specify _who_ can access it or _how_ to charge for it. **Nevermined Payments** adds:
|
|
285
304
|
|
|
286
305
|
- **Authentication**: Validates user tokens via `Authorization` header
|
|
287
306
|
- **Credit System**: Checks and deducts credits per request
|
|
@@ -292,77 +311,77 @@ While MCP defines *what* an agent can do, it doesn't specify *who* can access it
|
|
|
292
311
|
#### 1. Initialize Nevermined Payments
|
|
293
312
|
|
|
294
313
|
```typescript
|
|
295
|
-
import { Payments, EnvironmentName } from
|
|
314
|
+
import { Payments, EnvironmentName } from '@nevermined-io/payments'
|
|
296
315
|
|
|
297
316
|
const payments = Payments.getInstance({
|
|
298
317
|
nvmApiKey: process.env.NVM_API_KEY!,
|
|
299
318
|
environment: process.env.NVM_ENVIRONMENT! as EnvironmentName,
|
|
300
|
-
})
|
|
319
|
+
})
|
|
301
320
|
```
|
|
302
321
|
|
|
303
322
|
#### 2. Register Protected Tools
|
|
304
323
|
|
|
305
324
|
```typescript
|
|
306
|
-
import { z } from
|
|
325
|
+
import { z } from 'zod'
|
|
307
326
|
|
|
308
327
|
const schema = z.object({
|
|
309
|
-
city: z.string().describe(
|
|
310
|
-
}) as any
|
|
328
|
+
city: z.string().describe('City name'),
|
|
329
|
+
}) as any
|
|
311
330
|
|
|
312
331
|
payments.mcp.registerTool(
|
|
313
|
-
|
|
332
|
+
'weather.today',
|
|
314
333
|
{
|
|
315
334
|
title: "Today's Weather",
|
|
316
|
-
description:
|
|
335
|
+
description: 'Get weather for a city',
|
|
317
336
|
inputSchema: schema,
|
|
318
337
|
},
|
|
319
338
|
async (args) => {
|
|
320
|
-
const { city } = args as { city: string }
|
|
339
|
+
const { city } = args as { city: string }
|
|
321
340
|
return {
|
|
322
|
-
content: [{ type:
|
|
323
|
-
}
|
|
341
|
+
content: [{ type: 'text', text: `Weather for ${city}: Sunny, 25C` }],
|
|
342
|
+
}
|
|
324
343
|
},
|
|
325
|
-
{ credits: 1n }
|
|
326
|
-
)
|
|
344
|
+
{ credits: 1n },
|
|
345
|
+
)
|
|
327
346
|
```
|
|
328
347
|
|
|
329
348
|
#### 3. Register Protected Resources
|
|
330
349
|
|
|
331
350
|
```typescript
|
|
332
351
|
payments.mcp.registerResource(
|
|
333
|
-
|
|
334
|
-
|
|
352
|
+
'Weather Data',
|
|
353
|
+
'weather://today',
|
|
335
354
|
{
|
|
336
355
|
title: "Today's Weather",
|
|
337
|
-
description:
|
|
338
|
-
mimeType:
|
|
356
|
+
description: 'JSON weather data',
|
|
357
|
+
mimeType: 'application/json',
|
|
339
358
|
},
|
|
340
359
|
async (uri) => {
|
|
341
360
|
return {
|
|
342
|
-
contents: [{ uri: uri.href, text:
|
|
343
|
-
}
|
|
361
|
+
contents: [{ uri: uri.href, text: '{...}', mimeType: 'application/json' }],
|
|
362
|
+
}
|
|
344
363
|
},
|
|
345
|
-
{ credits: 5n }
|
|
346
|
-
)
|
|
364
|
+
{ credits: 5n },
|
|
365
|
+
)
|
|
347
366
|
```
|
|
348
367
|
|
|
349
368
|
#### 4. Register Protected Prompts
|
|
350
369
|
|
|
351
370
|
```typescript
|
|
352
371
|
payments.mcp.registerPrompt(
|
|
353
|
-
|
|
372
|
+
'weather.ensureCity',
|
|
354
373
|
{
|
|
355
|
-
title:
|
|
356
|
-
description:
|
|
374
|
+
title: 'Ensure city',
|
|
375
|
+
description: 'Guide to call weather.today',
|
|
357
376
|
argsSchema: schema,
|
|
358
377
|
},
|
|
359
378
|
(args) => {
|
|
360
379
|
return {
|
|
361
|
-
messages: [{ role:
|
|
362
|
-
}
|
|
380
|
+
messages: [{ role: 'user', content: { type: 'text', text: '...' } }],
|
|
381
|
+
}
|
|
363
382
|
},
|
|
364
|
-
{ credits: (ctx) => ctx.result.length > 100 ? 2n : 1n }
|
|
365
|
-
)
|
|
383
|
+
{ credits: (ctx) => (ctx.result.length > 100 ? 2n : 1n) }, // Dynamic credits
|
|
384
|
+
)
|
|
366
385
|
```
|
|
367
386
|
|
|
368
387
|
#### 5. Start the Server
|
|
@@ -371,9 +390,9 @@ payments.mcp.registerPrompt(
|
|
|
371
390
|
const { info, stop } = await payments.mcp.start({
|
|
372
391
|
port: 3002,
|
|
373
392
|
agentId: process.env.NVM_AGENT_ID!,
|
|
374
|
-
serverName:
|
|
375
|
-
version:
|
|
376
|
-
})
|
|
393
|
+
serverName: 'weather-mcp',
|
|
394
|
+
version: '0.1.0',
|
|
395
|
+
})
|
|
377
396
|
```
|
|
378
397
|
|
|
379
398
|
### Credit Configuration
|
|
@@ -382,22 +401,32 @@ All registration functions support fixed or dynamic credits:
|
|
|
382
401
|
|
|
383
402
|
```typescript
|
|
384
403
|
// Fixed credits
|
|
385
|
-
{
|
|
386
|
-
|
|
404
|
+
{
|
|
405
|
+
credits: 1n
|
|
406
|
+
}
|
|
407
|
+
{
|
|
408
|
+
credits: 5n
|
|
409
|
+
}
|
|
387
410
|
|
|
388
411
|
// Dynamic credits based on input
|
|
389
|
-
{
|
|
412
|
+
{
|
|
413
|
+
credits: (ctx) => (ctx.args.premium ? 5n : 1n)
|
|
414
|
+
}
|
|
390
415
|
|
|
391
416
|
// Dynamic credits based on result
|
|
392
|
-
{
|
|
417
|
+
{
|
|
418
|
+
credits: (ctx) => (ctx.result.length > 1000 ? 3n : 1n)
|
|
419
|
+
}
|
|
393
420
|
|
|
394
421
|
// Tiered pricing
|
|
395
|
-
{
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
422
|
+
{
|
|
423
|
+
credits: (ctx) => {
|
|
424
|
+
const size = JSON.stringify(ctx.result).length
|
|
425
|
+
if (size > 1000) return 5n
|
|
426
|
+
if (size > 500) return 3n
|
|
427
|
+
return 1n
|
|
428
|
+
}
|
|
429
|
+
}
|
|
401
430
|
```
|
|
402
431
|
|
|
403
432
|
### Client Usage
|
|
@@ -405,55 +434,54 @@ All registration functions support fixed or dynamic credits:
|
|
|
405
434
|
#### Get Access Token
|
|
406
435
|
|
|
407
436
|
```typescript
|
|
408
|
-
import { Payments } from
|
|
437
|
+
import { Payments } from '@nevermined-io/payments'
|
|
409
438
|
|
|
410
439
|
const payments = Payments.getInstance({
|
|
411
440
|
nvmApiKey: process.env.NVM_API_KEY!,
|
|
412
|
-
environment:
|
|
413
|
-
})
|
|
441
|
+
environment: 'sandbox',
|
|
442
|
+
})
|
|
414
443
|
|
|
415
444
|
const { accessToken } = await payments.x402.getX402AccessToken(
|
|
416
445
|
process.env.NVM_PLAN_ID!,
|
|
417
|
-
process.env.NVM_AGENT_ID
|
|
418
|
-
)
|
|
446
|
+
process.env.NVM_AGENT_ID!,
|
|
447
|
+
)
|
|
419
448
|
```
|
|
420
449
|
|
|
421
450
|
#### Call Protected Tools
|
|
422
451
|
|
|
423
452
|
```typescript
|
|
424
|
-
import { Client } from
|
|
425
|
-
import { StreamableHTTPClientTransport } from
|
|
453
|
+
import { Client } from '@modelcontextprotocol/sdk/client'
|
|
454
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp'
|
|
426
455
|
|
|
427
|
-
const transport = new StreamableHTTPClientTransport(
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
);
|
|
456
|
+
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3002/mcp'), {
|
|
457
|
+
requestInit: { headers: { Authorization: `Bearer ${accessToken}` } },
|
|
458
|
+
})
|
|
431
459
|
|
|
432
|
-
const client = new Client({ name:
|
|
433
|
-
await client.connect(transport)
|
|
460
|
+
const client = new Client({ name: 'my-client' })
|
|
461
|
+
await client.connect(transport)
|
|
434
462
|
|
|
435
463
|
const result = await client.callTool({
|
|
436
|
-
name:
|
|
437
|
-
arguments: { city:
|
|
438
|
-
})
|
|
464
|
+
name: 'weather.today',
|
|
465
|
+
arguments: { city: 'London' },
|
|
466
|
+
})
|
|
439
467
|
```
|
|
440
468
|
|
|
441
469
|
### Endpoints
|
|
442
470
|
|
|
443
|
-
| Endpoint
|
|
444
|
-
|
|
445
|
-
| `POST /mcp`
|
|
446
|
-
| `GET /mcp`
|
|
447
|
-
| `DELETE /mcp` | Session termination
|
|
448
|
-
| `GET /health` | Health check
|
|
449
|
-
| `GET /`
|
|
471
|
+
| Endpoint | Description |
|
|
472
|
+
| ------------- | ---------------------------- |
|
|
473
|
+
| `POST /mcp` | MCP JSON-RPC requests |
|
|
474
|
+
| `GET /mcp` | SSE stream for notifications |
|
|
475
|
+
| `DELETE /mcp` | Session termination |
|
|
476
|
+
| `GET /health` | Health check |
|
|
477
|
+
| `GET /` | Server info |
|
|
450
478
|
|
|
451
479
|
### Error Codes
|
|
452
480
|
|
|
453
|
-
| Code
|
|
454
|
-
|
|
481
|
+
| Code | Description |
|
|
482
|
+
| -------- | ---------------------------------------------------------------- |
|
|
455
483
|
| `-32003` | Authorization required / Payment required / Insufficient credits |
|
|
456
|
-
| `-32002` | Server error
|
|
484
|
+
| `-32002` | Server error |
|
|
457
485
|
|
|
458
486
|
### Notes
|
|
459
487
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangGraph ReAct agent helper that surfaces `PaymentRequiredError` intact.
|
|
3
|
+
*
|
|
4
|
+
* By default `createReactAgent` from `@langchain/langgraph/prebuilt` constructs a
|
|
5
|
+
* `ToolNode` with `handleToolErrors: true` — tool exceptions are caught and
|
|
6
|
+
* rendered into a `ToolMessage` for the LLM. That is convenient for
|
|
7
|
+
* prompt-engineered recovery, but it stringifies the exception and loses the
|
|
8
|
+
* `X402PaymentRequired` payload attached to {@link PaymentRequiredError}.
|
|
9
|
+
* Without that payload the caller cannot run the x402 discovery flow (probe →
|
|
10
|
+
* read scheme/network/plan id → acquire token → retry).
|
|
11
|
+
*
|
|
12
|
+
* {@link createPaidReactAgent} builds the same agent but with a `ToolNode`
|
|
13
|
+
* configured to **re-raise** exceptions (`handleToolErrors: false`), so
|
|
14
|
+
* `PaymentRequiredError` propagates all the way back to `agent.invoke()`'s
|
|
15
|
+
* caller with `.paymentRequired` populated.
|
|
16
|
+
*
|
|
17
|
+
* `@langchain/langgraph` is imported **lazily** inside the function so the
|
|
18
|
+
* existing `peerDependencies` story is unchanged — users who only use the
|
|
19
|
+
* `requiresPayment` wrapper do not need LangGraph installed. Install it
|
|
20
|
+
* yourself (`pnpm add @langchain/langgraph`) to use this helper.
|
|
21
|
+
*
|
|
22
|
+
* Unlike Python's synchronous `create_paid_react_agent`, this helper is
|
|
23
|
+
* **`async`** — that is a deliberate consequence of the lazy `import()` that
|
|
24
|
+
* keeps `@langchain/langgraph` an optional peer, not a parity break. `await` it.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { tool } from '@langchain/core/tools'
|
|
29
|
+
* import { ChatOpenAI } from '@langchain/openai'
|
|
30
|
+
* import { z } from 'zod'
|
|
31
|
+
* import {
|
|
32
|
+
* PaymentRequiredError,
|
|
33
|
+
* createPaidReactAgent,
|
|
34
|
+
* requiresPayment,
|
|
35
|
+
* lastSettlement,
|
|
36
|
+
* } from '@nevermined-io/payments/langchain'
|
|
37
|
+
*
|
|
38
|
+
* const getMarketInsight = tool(
|
|
39
|
+
* requiresPayment(
|
|
40
|
+
* (args) => `Market insight for ${args.topic} ...`,
|
|
41
|
+
* { payments, planId: PLAN_ID, credits: 1 },
|
|
42
|
+
* ),
|
|
43
|
+
* { name: 'get_market_insight', description: 'Paid market insight', schema: z.object({ topic: z.string() }) },
|
|
44
|
+
* )
|
|
45
|
+
*
|
|
46
|
+
* const agent = await createPaidReactAgent(
|
|
47
|
+
* new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0 }),
|
|
48
|
+
* [getMarketInsight],
|
|
49
|
+
* { prompt: '...' },
|
|
50
|
+
* )
|
|
51
|
+
*
|
|
52
|
+
* // Discovery: invoke without a token to learn what to pay for.
|
|
53
|
+
* try {
|
|
54
|
+
* await agent.invoke({ messages: [...] }, { configurable: {} })
|
|
55
|
+
* } catch (err) {
|
|
56
|
+
* if (err instanceof PaymentRequiredError) {
|
|
57
|
+
* const accept = err.paymentRequired?.accepts[0]
|
|
58
|
+
* // ... acquire token using accept.planId / accept.scheme / accept.network ...
|
|
59
|
+
* }
|
|
60
|
+
* }
|
|
61
|
+
*
|
|
62
|
+
* const result = await agent.invoke(
|
|
63
|
+
* { messages: [...] },
|
|
64
|
+
* { configurable: { payment_token: token } },
|
|
65
|
+
* )
|
|
66
|
+
* const receipt = lastSettlement()
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
/**
|
|
70
|
+
* Options forwarded to the underlying `createReactAgent` call.
|
|
71
|
+
*
|
|
72
|
+
* Everything except `tools` (which this helper builds from the `tools`
|
|
73
|
+
* argument as a `ToolNode` with `handleToolErrors: false`) and `llm` (which
|
|
74
|
+
* this helper maps from the `model` argument) is passed through verbatim —
|
|
75
|
+
* `prompt`, `stateSchema`, `checkpointer`, `responseFormat`, etc.
|
|
76
|
+
*/
|
|
77
|
+
export type CreatePaidReactAgentOptions = Record<string, unknown>;
|
|
78
|
+
/**
|
|
79
|
+
* Build a LangGraph ReAct agent that lets `PaymentRequiredError` propagate.
|
|
80
|
+
*
|
|
81
|
+
* Wraps `createReactAgent` from `@langchain/langgraph/prebuilt` with a
|
|
82
|
+
* `ToolNode(tools, { handleToolErrors: false })`. The signature mirrors the
|
|
83
|
+
* Python `create_paid_react_agent(model, tools, **kwargs)` helper: `model` is
|
|
84
|
+
* mapped to the JS `llm` parameter and any extra `options` are forwarded.
|
|
85
|
+
*
|
|
86
|
+
* @param model - The chat model (mapped to `createReactAgent`'s `llm` argument).
|
|
87
|
+
* @param tools - The LangChain tools, typically functions wrapped with
|
|
88
|
+
* `requiresPayment` and registered via `tool(...)`.
|
|
89
|
+
* @param options - Forwarded verbatim to `createReactAgent` (`prompt`,
|
|
90
|
+
* `stateSchema`, `checkpointer`, …).
|
|
91
|
+
* @returns The compiled ReAct agent graph, ready to be invoked with
|
|
92
|
+
* `agent.invoke(...)`.
|
|
93
|
+
* @throws If `@langchain/langgraph` is not installed.
|
|
94
|
+
*/
|
|
95
|
+
export declare function createPaidReactAgent(model: unknown, tools: readonly unknown[], options?: CreatePaidReactAgentOptions): Promise<unknown>;
|
|
96
|
+
//# sourceMappingURL=agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../../src/x402/langchain/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AAEH;;;;;;;GAOG;AACH,MAAM,MAAM,2BAA2B,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEjE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,SAAS,OAAO,EAAE,EACzB,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,OAAO,CAAC,CAyClB"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangGraph ReAct agent helper that surfaces `PaymentRequiredError` intact.
|
|
3
|
+
*
|
|
4
|
+
* By default `createReactAgent` from `@langchain/langgraph/prebuilt` constructs a
|
|
5
|
+
* `ToolNode` with `handleToolErrors: true` — tool exceptions are caught and
|
|
6
|
+
* rendered into a `ToolMessage` for the LLM. That is convenient for
|
|
7
|
+
* prompt-engineered recovery, but it stringifies the exception and loses the
|
|
8
|
+
* `X402PaymentRequired` payload attached to {@link PaymentRequiredError}.
|
|
9
|
+
* Without that payload the caller cannot run the x402 discovery flow (probe →
|
|
10
|
+
* read scheme/network/plan id → acquire token → retry).
|
|
11
|
+
*
|
|
12
|
+
* {@link createPaidReactAgent} builds the same agent but with a `ToolNode`
|
|
13
|
+
* configured to **re-raise** exceptions (`handleToolErrors: false`), so
|
|
14
|
+
* `PaymentRequiredError` propagates all the way back to `agent.invoke()`'s
|
|
15
|
+
* caller with `.paymentRequired` populated.
|
|
16
|
+
*
|
|
17
|
+
* `@langchain/langgraph` is imported **lazily** inside the function so the
|
|
18
|
+
* existing `peerDependencies` story is unchanged — users who only use the
|
|
19
|
+
* `requiresPayment` wrapper do not need LangGraph installed. Install it
|
|
20
|
+
* yourself (`pnpm add @langchain/langgraph`) to use this helper.
|
|
21
|
+
*
|
|
22
|
+
* Unlike Python's synchronous `create_paid_react_agent`, this helper is
|
|
23
|
+
* **`async`** — that is a deliberate consequence of the lazy `import()` that
|
|
24
|
+
* keeps `@langchain/langgraph` an optional peer, not a parity break. `await` it.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { tool } from '@langchain/core/tools'
|
|
29
|
+
* import { ChatOpenAI } from '@langchain/openai'
|
|
30
|
+
* import { z } from 'zod'
|
|
31
|
+
* import {
|
|
32
|
+
* PaymentRequiredError,
|
|
33
|
+
* createPaidReactAgent,
|
|
34
|
+
* requiresPayment,
|
|
35
|
+
* lastSettlement,
|
|
36
|
+
* } from '@nevermined-io/payments/langchain'
|
|
37
|
+
*
|
|
38
|
+
* const getMarketInsight = tool(
|
|
39
|
+
* requiresPayment(
|
|
40
|
+
* (args) => `Market insight for ${args.topic} ...`,
|
|
41
|
+
* { payments, planId: PLAN_ID, credits: 1 },
|
|
42
|
+
* ),
|
|
43
|
+
* { name: 'get_market_insight', description: 'Paid market insight', schema: z.object({ topic: z.string() }) },
|
|
44
|
+
* )
|
|
45
|
+
*
|
|
46
|
+
* const agent = await createPaidReactAgent(
|
|
47
|
+
* new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0 }),
|
|
48
|
+
* [getMarketInsight],
|
|
49
|
+
* { prompt: '...' },
|
|
50
|
+
* )
|
|
51
|
+
*
|
|
52
|
+
* // Discovery: invoke without a token to learn what to pay for.
|
|
53
|
+
* try {
|
|
54
|
+
* await agent.invoke({ messages: [...] }, { configurable: {} })
|
|
55
|
+
* } catch (err) {
|
|
56
|
+
* if (err instanceof PaymentRequiredError) {
|
|
57
|
+
* const accept = err.paymentRequired?.accepts[0]
|
|
58
|
+
* // ... acquire token using accept.planId / accept.scheme / accept.network ...
|
|
59
|
+
* }
|
|
60
|
+
* }
|
|
61
|
+
*
|
|
62
|
+
* const result = await agent.invoke(
|
|
63
|
+
* { messages: [...] },
|
|
64
|
+
* { configurable: { payment_token: token } },
|
|
65
|
+
* )
|
|
66
|
+
* const receipt = lastSettlement()
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
/**
|
|
70
|
+
* Build a LangGraph ReAct agent that lets `PaymentRequiredError` propagate.
|
|
71
|
+
*
|
|
72
|
+
* Wraps `createReactAgent` from `@langchain/langgraph/prebuilt` with a
|
|
73
|
+
* `ToolNode(tools, { handleToolErrors: false })`. The signature mirrors the
|
|
74
|
+
* Python `create_paid_react_agent(model, tools, **kwargs)` helper: `model` is
|
|
75
|
+
* mapped to the JS `llm` parameter and any extra `options` are forwarded.
|
|
76
|
+
*
|
|
77
|
+
* @param model - The chat model (mapped to `createReactAgent`'s `llm` argument).
|
|
78
|
+
* @param tools - The LangChain tools, typically functions wrapped with
|
|
79
|
+
* `requiresPayment` and registered via `tool(...)`.
|
|
80
|
+
* @param options - Forwarded verbatim to `createReactAgent` (`prompt`,
|
|
81
|
+
* `stateSchema`, `checkpointer`, …).
|
|
82
|
+
* @returns The compiled ReAct agent graph, ready to be invoked with
|
|
83
|
+
* `agent.invoke(...)`.
|
|
84
|
+
* @throws If `@langchain/langgraph` is not installed.
|
|
85
|
+
*/
|
|
86
|
+
export async function createPaidReactAgent(model, tools, options = {}) {
|
|
87
|
+
// `llm` and `tools` are owned by this helper — `tools` carries the
|
|
88
|
+
// handleToolErrors:false ToolNode that makes PaymentRequiredError propagate,
|
|
89
|
+
// and overriding either would silently defeat the whole point of the helper
|
|
90
|
+
// (a caller-supplied `tools` re-enables the default handleToolErrors:true and
|
|
91
|
+
// the X402 payload is stringified away). Reject them up front, mirroring how
|
|
92
|
+
// Python's positional `create_paid_react_agent(model, tools, **kwargs)` raises
|
|
93
|
+
// a TypeError if `llm`/`tools` are passed again via kwargs.
|
|
94
|
+
if ('llm' in options || 'tools' in options) {
|
|
95
|
+
throw new Error('createPaidReactAgent: `llm` and `tools` are set from the `model` and ' +
|
|
96
|
+
'`tools` arguments and must not be passed in `options` (they would ' +
|
|
97
|
+
'override the handleToolErrors:false ToolNode and break x402 discovery).');
|
|
98
|
+
}
|
|
99
|
+
let prebuilt;
|
|
100
|
+
try {
|
|
101
|
+
prebuilt = await import('@langchain/langgraph/prebuilt');
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
throw new Error('createPaidReactAgent requires @langchain/langgraph. ' +
|
|
105
|
+
`Install it with \`pnpm add @langchain/langgraph\`. (${err instanceof Error ? err.message : String(err)})`);
|
|
106
|
+
}
|
|
107
|
+
const { ToolNode, createReactAgent } = prebuilt;
|
|
108
|
+
// handleToolErrors: false re-raises tool exceptions instead of stringifying
|
|
109
|
+
// them into a ToolMessage, so PaymentRequiredError reaches agent.invoke()'s
|
|
110
|
+
// caller with its X402PaymentRequired payload intact.
|
|
111
|
+
const toolNode = new ToolNode(tools, { handleToolErrors: false });
|
|
112
|
+
// `createReactAgent` is the current prebuilt entry point in
|
|
113
|
+
// @langchain/langgraph@1.2.0. It is marked @deprecated in favour of
|
|
114
|
+
// `createAgent`, but that replacement lives in the separate `langchain`
|
|
115
|
+
// package (out of scope for this SDK's optional langgraph peer), so the
|
|
116
|
+
// prebuilt `createReactAgent` is the deliberate, only in-package choice here.
|
|
117
|
+
// Spread `...options` FIRST so the protected `llm`/`tools` keys (set last)
|
|
118
|
+
// always win, even though the guard above already forbids them in `options`.
|
|
119
|
+
return createReactAgent({ ...options, llm: model, tools: toolNode });
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/x402/langchain/agent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AAYH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAc,EACd,KAAyB,EACzB,UAAuC,EAAE;IAEzC,mEAAmE;IACnE,6EAA6E;IAC7E,4EAA4E;IAC5E,8EAA8E;IAC9E,6EAA6E;IAC7E,+EAA+E;IAC/E,4DAA4D;IAC5D,IAAI,KAAK,IAAI,OAAO,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CACb,uEAAuE;YACrE,oEAAoE;YACpE,yEAAyE,CAC5E,CAAA;IACH,CAAC;IAED,IAAI,QAAwD,CAAA;IAC5D,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,MAAM,CAAC,+BAA+B,CAAC,CAAA;IAC1D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,sDAAsD;YACpD,uDACE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CACjD,GAAG,CACN,CAAA;IACH,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,gBAAgB,EAAE,GAAG,QAAQ,CAAA;IAE/C,4EAA4E;IAC5E,4EAA4E;IAC5E,sDAAsD;IACtD,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,KAAc,EAAE,EAAE,gBAAgB,EAAE,KAAK,EAAE,CAAC,CAAA;IAC1E,4DAA4D;IAC5D,oEAAoE;IACpE,wEAAwE;IACxE,wEAAwE;IACxE,8EAA8E;IAC9E,2EAA2E;IAC3E,6EAA6E;IAC7E,OAAO,gBAAgB,CAAC,EAAE,GAAG,OAAO,EAAE,GAAG,EAAE,KAAc,EAAE,KAAK,EAAE,QAAQ,EAAW,CAAC,CAAA;AACxF,CAAC","sourcesContent":["/**\n * LangGraph ReAct agent helper that surfaces `PaymentRequiredError` intact.\n *\n * By default `createReactAgent` from `@langchain/langgraph/prebuilt` constructs a\n * `ToolNode` with `handleToolErrors: true` — tool exceptions are caught and\n * rendered into a `ToolMessage` for the LLM. That is convenient for\n * prompt-engineered recovery, but it stringifies the exception and loses the\n * `X402PaymentRequired` payload attached to {@link PaymentRequiredError}.\n * Without that payload the caller cannot run the x402 discovery flow (probe →\n * read scheme/network/plan id → acquire token → retry).\n *\n * {@link createPaidReactAgent} builds the same agent but with a `ToolNode`\n * configured to **re-raise** exceptions (`handleToolErrors: false`), so\n * `PaymentRequiredError` propagates all the way back to `agent.invoke()`'s\n * caller with `.paymentRequired` populated.\n *\n * `@langchain/langgraph` is imported **lazily** inside the function so the\n * existing `peerDependencies` story is unchanged — users who only use the\n * `requiresPayment` wrapper do not need LangGraph installed. Install it\n * yourself (`pnpm add @langchain/langgraph`) to use this helper.\n *\n * Unlike Python's synchronous `create_paid_react_agent`, this helper is\n * **`async`** — that is a deliberate consequence of the lazy `import()` that\n * keeps `@langchain/langgraph` an optional peer, not a parity break. `await` it.\n *\n * @example\n * ```typescript\n * import { tool } from '@langchain/core/tools'\n * import { ChatOpenAI } from '@langchain/openai'\n * import { z } from 'zod'\n * import {\n * PaymentRequiredError,\n * createPaidReactAgent,\n * requiresPayment,\n * lastSettlement,\n * } from '@nevermined-io/payments/langchain'\n *\n * const getMarketInsight = tool(\n * requiresPayment(\n * (args) => `Market insight for ${args.topic} ...`,\n * { payments, planId: PLAN_ID, credits: 1 },\n * ),\n * { name: 'get_market_insight', description: 'Paid market insight', schema: z.object({ topic: z.string() }) },\n * )\n *\n * const agent = await createPaidReactAgent(\n * new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0 }),\n * [getMarketInsight],\n * { prompt: '...' },\n * )\n *\n * // Discovery: invoke without a token to learn what to pay for.\n * try {\n * await agent.invoke({ messages: [...] }, { configurable: {} })\n * } catch (err) {\n * if (err instanceof PaymentRequiredError) {\n * const accept = err.paymentRequired?.accepts[0]\n * // ... acquire token using accept.planId / accept.scheme / accept.network ...\n * }\n * }\n *\n * const result = await agent.invoke(\n * { messages: [...] },\n * { configurable: { payment_token: token } },\n * )\n * const receipt = lastSettlement()\n * ```\n */\n\n/**\n * Options forwarded to the underlying `createReactAgent` call.\n *\n * Everything except `tools` (which this helper builds from the `tools`\n * argument as a `ToolNode` with `handleToolErrors: false`) and `llm` (which\n * this helper maps from the `model` argument) is passed through verbatim —\n * `prompt`, `stateSchema`, `checkpointer`, `responseFormat`, etc.\n */\nexport type CreatePaidReactAgentOptions = Record<string, unknown>\n\n/**\n * Build a LangGraph ReAct agent that lets `PaymentRequiredError` propagate.\n *\n * Wraps `createReactAgent` from `@langchain/langgraph/prebuilt` with a\n * `ToolNode(tools, { handleToolErrors: false })`. The signature mirrors the\n * Python `create_paid_react_agent(model, tools, **kwargs)` helper: `model` is\n * mapped to the JS `llm` parameter and any extra `options` are forwarded.\n *\n * @param model - The chat model (mapped to `createReactAgent`'s `llm` argument).\n * @param tools - The LangChain tools, typically functions wrapped with\n * `requiresPayment` and registered via `tool(...)`.\n * @param options - Forwarded verbatim to `createReactAgent` (`prompt`,\n * `stateSchema`, `checkpointer`, …).\n * @returns The compiled ReAct agent graph, ready to be invoked with\n * `agent.invoke(...)`.\n * @throws If `@langchain/langgraph` is not installed.\n */\nexport async function createPaidReactAgent(\n model: unknown,\n tools: readonly unknown[],\n options: CreatePaidReactAgentOptions = {},\n): Promise<unknown> {\n // `llm` and `tools` are owned by this helper — `tools` carries the\n // handleToolErrors:false ToolNode that makes PaymentRequiredError propagate,\n // and overriding either would silently defeat the whole point of the helper\n // (a caller-supplied `tools` re-enables the default handleToolErrors:true and\n // the X402 payload is stringified away). Reject them up front, mirroring how\n // Python's positional `create_paid_react_agent(model, tools, **kwargs)` raises\n // a TypeError if `llm`/`tools` are passed again via kwargs.\n if ('llm' in options || 'tools' in options) {\n throw new Error(\n 'createPaidReactAgent: `llm` and `tools` are set from the `model` and ' +\n '`tools` arguments and must not be passed in `options` (they would ' +\n 'override the handleToolErrors:false ToolNode and break x402 discovery).',\n )\n }\n\n let prebuilt: typeof import('@langchain/langgraph/prebuilt')\n try {\n prebuilt = await import('@langchain/langgraph/prebuilt')\n } catch (err) {\n throw new Error(\n 'createPaidReactAgent requires @langchain/langgraph. ' +\n `Install it with \\`pnpm add @langchain/langgraph\\`. (${\n err instanceof Error ? err.message : String(err)\n })`,\n )\n }\n const { ToolNode, createReactAgent } = prebuilt\n\n // handleToolErrors: false re-raises tool exceptions instead of stringifying\n // them into a ToolMessage, so PaymentRequiredError reaches agent.invoke()'s\n // caller with its X402PaymentRequired payload intact.\n const toolNode = new ToolNode(tools as never, { handleToolErrors: false })\n // `createReactAgent` is the current prebuilt entry point in\n // @langchain/langgraph@1.2.0. It is marked @deprecated in favour of\n // `createAgent`, but that replacement lives in the separate `langchain`\n // package (out of scope for this SDK's optional langgraph peer), so the\n // prebuilt `createReactAgent` is the deliberate, only in-package choice here.\n // Spread `...options` FIRST so the protected `llm`/`tools` keys (set last)\n // always win, even though the guard above already forbids them in `options`.\n return createReactAgent({ ...options, llm: model as never, tools: toolNode } as never)\n}\n"]}
|