@tangle-network/agent-gateway 0.8.0 → 0.8.1

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/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { createAgentGateway } from './middleware.js';
2
- import { G as GatewayConfig, P as PaymentOperationRecoveryResult, a as PaymentRecoveryRecord, M as MppAuthenticatedCredential, A as ApiKeyInfo, b as MppConfig, X as X402Config, c as PaymentRecoveryStore, S as SqlAdapter, C as ChatMessage } from './types-BHISsm7D.js';
3
- export { d as A2A_ERROR_CODES, e as AgentCapabilities, f as AgentCard, g as AgentCardAuthentication, h as AgentMeta, i as AgentProvider, j as AgentSkill, k as Artifact, l as ChatCompletionChunk, m as ChatCompletionRequest, D as D1DatabaseLike, n as D1StmtLike, o as DataPart, F as FilePart, I as InMemoryPushNotificationStore, p as InMemoryTaskStore, J as JSONRPCErrorResponse, q as JSONRPCRequest, r as JSONRPCResponse, s as JSONRPCSuccessResponse, t as MPP_CHARGE_PROTOCOL_VERSION, u as MemoryPaymentOperations, v as MemoryPaymentOperationsOptions, w as MemoryPaymentRecoveryStore, x as Message, y as MessageSendParams, z as MppChargeLifecycle, B as MppChargeOperation, E as MppChargeOperationState, H as MppChargeRecoveryResult, K as MppChargeRequest, L as PAYMENT_PROTOCOL_VERSION, N as PAYMENT_RECOVERY_VERSION, O as Part, Q as PaymentAuthorizationContext, R as PaymentOperation, T as PaymentOperationNotFound, U as PaymentOperationState, V as PaymentOperations, W as PaymentRecoveryAttribution, Y as PaymentRecoveryConfig, Z as PaymentRecoveryFenceError, _ as PaymentRecoveryState, $ as PaymentRecoveryTarget, a0 as PaymentResult, a1 as PaymentSettlementInput, a2 as PushDeliveryResult, a3 as PushNotificationAuthentication, a4 as PushNotificationConfig, a5 as PushNotificationDeliveryOptions, a6 as PushNotificationStore, a7 as SandboxBox, a8 as SandboxStreamEvent, a9 as SqlPushNotificationStore, aa as SqlTaskStore, ab as StreamingEvent, ac as Task, ad as TaskArtifactUpdateEvent, ae as TaskIdParams, af as TaskPushNotificationConfig, ag as TaskPushNotificationConfigGetParams, ah as TaskState, ai as TaskStatus, aj as TaskStatusUpdateEvent, ak as TaskStore, al as TextPart, am as d1ToSqlAdapter, an as deliverDemoPushNotifications, ao as deliverPushNotifications, ap as mppPaymentOperationId, aq as validatePushNotificationUrl } from './types-BHISsm7D.js';
2
+ import { G as GatewayConfig, P as PaymentOperationRecoveryResult, a as PaymentRecoveryRecord, M as MppAuthenticatedCredential, A as ApiKeyInfo, b as MppConfig, X as X402Config, c as PaymentRecoveryStore, S as SqlAdapter, C as ChatMessage } from './types-oQ58UakD.js';
3
+ export { d as A2A_ERROR_CODES, e as AgentCapabilities, f as AgentCard, g as AgentCardAuthentication, h as AgentMeta, i as AgentProvider, j as AgentSkill, k as ApiKeyGatewayConfig, l as Artifact, m as ChatCompletionChunk, n as ChatCompletionRequest, o as CreateAgentGatewayConfig, D as D1DatabaseLike, p as D1StmtLike, q as DataPart, F as FilePart, I as InMemoryPushNotificationStore, r as InMemoryTaskStore, J as JSONRPCErrorResponse, s as JSONRPCRequest, t as JSONRPCResponse, u as JSONRPCSuccessResponse, v as MPP_CHARGE_PROTOCOL_VERSION, w as MemoryPaymentOperations, x as MemoryPaymentOperationsOptions, y as MemoryPaymentRecoveryStore, z as Message, B as MessageSendParams, E as MppChargeLifecycle, H as MppChargeOperation, K as MppChargeOperationState, L as MppChargeRecoveryResult, N as MppChargeRequest, O as PAYMENT_PROTOCOL_VERSION, Q as PAYMENT_RECOVERY_VERSION, R as Part, T as PaymentAuthorizationContext, U as PaymentOperation, V as PaymentOperationNotFound, W as PaymentOperationState, Y as PaymentOperations, Z as PaymentRecoveryAttribution, _ as PaymentRecoveryConfig, $ as PaymentRecoveryFenceError, a0 as PaymentRecoveryState, a1 as PaymentRecoveryTarget, a2 as PaymentResult, a3 as PaymentSettlementInput, a4 as PushDeliveryResult, a5 as PushNotificationAuthentication, a6 as PushNotificationConfig, a7 as PushNotificationDeliveryOptions, a8 as PushNotificationStore, a9 as SandboxBox, aa as SandboxStreamEvent, ab as SqlPushNotificationStore, ac as SqlTaskStore, ad as StreamingEvent, ae as Task, af as TaskArtifactUpdateEvent, ag as TaskIdParams, ah as TaskPushNotificationConfig, ai as TaskPushNotificationConfigGetParams, aj as TaskState, ak as TaskStatus, al as TaskStatusUpdateEvent, am as TaskStore, an as TextPart, ao as d1ToSqlAdapter, ap as deliverDemoPushNotifications, aq as deliverPushNotifications, ar as mppPaymentOperationId, as as validatePushNotificationUrl } from './types-oQ58UakD.js';
4
4
  import { NonceStore } from './nonce-store.js';
5
5
  export { AtomicKvNonceClaim, AtomicNonceStore, KvNonceStore, KvNonceStoreOptions, MemoryNonceStore, isAtomicNonceStore } from './nonce-store.js';
6
6
  export { CompositeObserver, ConsoleObserver, generateRequestId } from './observer.js';
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  verifyMpp,
36
36
  verifyMppCredential,
37
37
  verifyX402
38
- } from "./chunk-MP6IIAIA.js";
38
+ } from "./chunk-C7Z2BRYV.js";
39
39
  import {
40
40
  KvNonceStore,
41
41
  MemoryNonceStore,
@@ -1,6 +1,6 @@
1
1
  import * as hono_types from 'hono/types';
2
2
  import { Hono } from 'hono';
3
- import { G as GatewayConfig } from './types-BHISsm7D.js';
3
+ import { o as CreateAgentGatewayConfig } from './types-oQ58UakD.js';
4
4
  import './nonce-store.js';
5
5
  import './rate-limit.js';
6
6
  import './observer-types-A0RtA8uL.js';
@@ -15,7 +15,7 @@ import './observer-types-A0RtA8uL.js';
15
15
  * GET /:slug/chat/completions — agent discovery metadata (Tangle-native shape)
16
16
  * POST /:slug/chat/completions — OpenAI-compatible chat endpoint (paid)
17
17
  */
18
- declare function createAgentGateway(inputConfig: GatewayConfig): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
18
+ declare function createAgentGateway(inputConfig: CreateAgentGatewayConfig): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
19
19
  /** Public package-boundary version marker for durable payment operations. */
20
20
  declare namespace createAgentGateway {
21
21
  const paymentProtocolVersion: 2;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createAgentGateway
3
- } from "./chunk-MP6IIAIA.js";
3
+ } from "./chunk-C7Z2BRYV.js";
4
4
  import "./chunk-J5SDVHOL.js";
5
5
  import "./chunk-GITV7CPT.js";
6
6
  import "./chunk-XCTXHZ76.js";
@@ -939,11 +939,24 @@ interface SandboxBox {
939
939
  signal?: AbortSignal;
940
940
  }): AsyncIterable<SandboxStreamEvent>;
941
941
  }
942
+ /** Authenticated request identity supplied when the host resolves a sandbox. */
943
+ interface GatewaySandboxContext {
944
+ consumerId: string;
945
+ paymentMethod: PaymentMethod;
946
+ keyInfo: ApiKeyInfo | null;
947
+ requestId: string;
948
+ messages: ChatMessage[];
949
+ /** Stable UI conversation id when `conversationMode` is `thread`. */
950
+ threadId?: string;
951
+ }
942
952
  interface GatewayConfig {
943
953
  /** Resolve agent metadata by slug. Return null if not found or not published. */
944
954
  resolveAgent: (slug: string) => Promise<AgentMeta | null>;
945
- /** Get a sandbox instance for the agent. Called after payment is verified. */
946
- getSandbox: (agent: AgentMeta) => Promise<SandboxBox>;
955
+ /**
956
+ * Get the agent execution adapter after payment is verified.
957
+ * Hosts that use agent-app can drive their normal persisted chat route here.
958
+ */
959
+ getSandbox: (agent: AgentMeta, context?: GatewaySandboxContext) => Promise<SandboxBox>;
947
960
  /**
948
961
  * Optional host authorization hook fired after payment verification
949
962
  * and before sandbox resolution. Use it for per-agent allowlists,
@@ -954,6 +967,8 @@ interface GatewayConfig {
954
967
  consumerId: string;
955
968
  keyId?: string;
956
969
  requestId: string;
970
+ /** Requested stable conversation id, after syntax validation. */
971
+ threadId?: string;
957
972
  }) => Promise<{
958
973
  allow: true;
959
974
  } | {
@@ -993,6 +1008,14 @@ interface GatewayConfig {
993
1008
  settlePayment?: (payment: PaymentResult, cost: number) => Promise<void>;
994
1009
  /** Base URL for API key purchase links (e.g. "https://film.tangle.tools") */
995
1010
  baseUrl?: string;
1011
+ /** Public API key prefix shown by discovery. Defaults to `sk_agent_`. */
1012
+ apiKeyPrefix?: string;
1013
+ /**
1014
+ * `consumer` keeps the historical session per API consumer.
1015
+ * `thread` accepts `X-Tangle-Thread-Id` or creates one per request and returns
1016
+ * it in the response, so a host can display the same conversation.
1017
+ */
1018
+ conversationMode?: 'consumer' | 'thread';
996
1019
  /** Max message length in chars (default: 8000) */
997
1020
  maxMessageLength?: number;
998
1021
  /** Maximum output token request the gateway accepts. Defaults to 4096. */
@@ -1090,6 +1113,14 @@ interface GatewayConfig {
1090
1113
  pushUrlValidator?: (url: URL) => boolean | Promise<boolean>;
1091
1114
  };
1092
1115
  }
1116
+ /** API-key-only gateway input. Payment transports stay disabled. */
1117
+ type ApiKeyGatewayConfig = Omit<GatewayConfig, 'mpp' | 'verifyApiKey' | 'x402'> & {
1118
+ mpp?: never;
1119
+ verifyApiKey: NonNullable<GatewayConfig['verifyApiKey']>;
1120
+ x402?: never;
1121
+ };
1122
+ /** Configuration accepted by `createAgentGateway`. */
1123
+ type CreateAgentGatewayConfig = GatewayConfig | ApiKeyGatewayConfig;
1093
1124
  interface ChatMessage {
1094
1125
  role: 'system' | 'user' | 'assistant' | 'tool';
1095
1126
  content: string;
@@ -1116,4 +1147,4 @@ interface ChatCompletionChunk {
1116
1147
  }>;
1117
1148
  }
1118
1149
 
1119
- export { type PaymentRecoveryTarget as $, type ApiKeyInfo as A, type MppChargeOperation as B, type ChatMessage as C, type D1DatabaseLike as D, type MppChargeOperationState as E, type FilePart as F, type GatewayConfig as G, type MppChargeRecoveryResult as H, InMemoryPushNotificationStore as I, type JSONRPCErrorResponse as J, type MppChargeRequest as K, PAYMENT_PROTOCOL_VERSION as L, type MppAuthenticatedCredential as M, PAYMENT_RECOVERY_VERSION as N, type Part as O, type PaymentOperationRecoveryResult as P, type PaymentAuthorizationContext as Q, type PaymentOperation as R, type SqlAdapter as S, type PaymentOperationNotFound as T, type PaymentOperationState as U, type PaymentOperations as V, type PaymentRecoveryAttribution as W, type X402Config as X, type PaymentRecoveryConfig as Y, PaymentRecoveryFenceError as Z, type PaymentRecoveryState as _, type PaymentRecoveryRecord as a, type PaymentResult as a0, type PaymentSettlementInput as a1, type PushDeliveryResult as a2, type PushNotificationAuthentication as a3, type PushNotificationConfig as a4, type PushNotificationDeliveryOptions as a5, type PushNotificationStore as a6, type SandboxBox as a7, type SandboxStreamEvent as a8, SqlPushNotificationStore as a9, SqlTaskStore as aa, type StreamingEvent as ab, type Task as ac, type TaskArtifactUpdateEvent as ad, type TaskIdParams as ae, type TaskPushNotificationConfig as af, type TaskPushNotificationConfigGetParams as ag, type TaskState as ah, type TaskStatus as ai, type TaskStatusUpdateEvent as aj, type TaskStore as ak, type TextPart as al, d1ToSqlAdapter as am, deliverDemoPushNotifications as an, deliverPushNotifications as ao, mppPaymentOperationId as ap, validatePushNotificationUrl as aq, type MppConfig as b, type PaymentRecoveryStore as c, A2A_ERROR_CODES as d, type AgentCapabilities as e, type AgentCard as f, type AgentCardAuthentication as g, type AgentMeta as h, type AgentProvider as i, type AgentSkill as j, type Artifact as k, type ChatCompletionChunk as l, type ChatCompletionRequest as m, type D1StmtLike as n, type DataPart as o, InMemoryTaskStore as p, type JSONRPCRequest as q, type JSONRPCResponse as r, type JSONRPCSuccessResponse as s, MPP_CHARGE_PROTOCOL_VERSION as t, MemoryPaymentOperations as u, type MemoryPaymentOperationsOptions as v, MemoryPaymentRecoveryStore as w, type Message as x, type MessageSendParams as y, type MppChargeLifecycle as z };
1150
+ export { PaymentRecoveryFenceError as $, type ApiKeyInfo as A, type MessageSendParams as B, type ChatMessage as C, type D1DatabaseLike as D, type MppChargeLifecycle as E, type FilePart as F, type GatewayConfig as G, type MppChargeOperation as H, InMemoryPushNotificationStore as I, type JSONRPCErrorResponse as J, type MppChargeOperationState as K, type MppChargeRecoveryResult as L, type MppAuthenticatedCredential as M, type MppChargeRequest as N, PAYMENT_PROTOCOL_VERSION as O, type PaymentOperationRecoveryResult as P, PAYMENT_RECOVERY_VERSION as Q, type Part as R, type SqlAdapter as S, type PaymentAuthorizationContext as T, type PaymentOperation as U, type PaymentOperationNotFound as V, type PaymentOperationState as W, type X402Config as X, type PaymentOperations as Y, type PaymentRecoveryAttribution as Z, type PaymentRecoveryConfig as _, type PaymentRecoveryRecord as a, type PaymentRecoveryState as a0, type PaymentRecoveryTarget as a1, type PaymentResult as a2, type PaymentSettlementInput as a3, type PushDeliveryResult as a4, type PushNotificationAuthentication as a5, type PushNotificationConfig as a6, type PushNotificationDeliveryOptions as a7, type PushNotificationStore as a8, type SandboxBox as a9, type SandboxStreamEvent as aa, SqlPushNotificationStore as ab, SqlTaskStore as ac, type StreamingEvent as ad, type Task as ae, type TaskArtifactUpdateEvent as af, type TaskIdParams as ag, type TaskPushNotificationConfig as ah, type TaskPushNotificationConfigGetParams as ai, type TaskState as aj, type TaskStatus as ak, type TaskStatusUpdateEvent as al, type TaskStore as am, type TextPart as an, d1ToSqlAdapter as ao, deliverDemoPushNotifications as ap, deliverPushNotifications as aq, mppPaymentOperationId as ar, validatePushNotificationUrl as as, type GatewaySandboxContext as at, type MppConfig as b, type PaymentRecoveryStore as c, A2A_ERROR_CODES as d, type AgentCapabilities as e, type AgentCard as f, type AgentCardAuthentication as g, type AgentMeta as h, type AgentProvider as i, type AgentSkill as j, type ApiKeyGatewayConfig as k, type Artifact as l, type ChatCompletionChunk as m, type ChatCompletionRequest as n, type CreateAgentGatewayConfig as o, type D1StmtLike as p, type DataPart as q, InMemoryTaskStore as r, type JSONRPCRequest as s, type JSONRPCResponse as t, type JSONRPCSuccessResponse as u, MPP_CHARGE_PROTOCOL_VERSION as v, MemoryPaymentOperations as w, type MemoryPaymentOperationsOptions as x, MemoryPaymentRecoveryStore as y, type Message as z };
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { h as AgentMeta, A as ApiKeyInfo, l as ChatCompletionChunk, m as ChatCompletionRequest, C as ChatMessage, G as GatewayConfig, b as MppConfig, a0 as PaymentResult, a7 as SandboxBox, a8 as SandboxStreamEvent, X as X402Config } from './types-BHISsm7D.js';
1
+ export { h as AgentMeta, k as ApiKeyGatewayConfig, A as ApiKeyInfo, m as ChatCompletionChunk, n as ChatCompletionRequest, C as ChatMessage, o as CreateAgentGatewayConfig, G as GatewayConfig, at as GatewaySandboxContext, b as MppConfig, a2 as PaymentResult, a9 as SandboxBox, aa as SandboxStreamEvent, X as X402Config } from './types-oQ58UakD.js';
2
2
  import './nonce-store.js';
3
3
  import './rate-limit.js';
4
4
  export { a as GatewayUsageEvent, P as PaymentMethod, c as SandboxExecutionBudget, S as SandboxUsageReceipt } from './observer-types-A0RtA8uL.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-gateway",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/tangle-network/agent-gateway.git"
@@ -19,6 +19,7 @@ import {
19
19
  defaultVerifyApiKey,
20
20
  isApiKeyAuthEnabled,
21
21
  isMppAuthEnabled,
22
+ isX402AuthEnabled,
22
23
  mppPaymentPayload,
23
24
  mppPaymentCredential,
24
25
  verifyMppCredential,
@@ -48,6 +49,18 @@ export async function authenticateAndGuard(
48
49
  const ctx: RequestContext = { requestId, agentSlug: slug, startMs }
49
50
  await state.obs?.onRequestStart?.(ctx)
50
51
 
52
+ let threadId: string | undefined
53
+ if (config.conversationMode === 'thread') {
54
+ const requestedThreadId = c.req.header('X-Tangle-Thread-Id')?.trim()
55
+ if (requestedThreadId && !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(requestedThreadId)) {
56
+ return c.json(
57
+ { error: { message: 'Invalid X-Tangle-Thread-Id', type: 'invalid_request' } },
58
+ { status: 400, headers: { 'X-Request-Id': requestId } },
59
+ )
60
+ }
61
+ threadId = requestedThreadId || requestId
62
+ }
63
+
51
64
  const agent = await config.resolveAgent(slug)
52
65
  if (!agent || !agent.enabled) {
53
66
  return c.json({ error: { message: 'Agent not found', type: 'not_found' } }, 404)
@@ -83,10 +96,14 @@ export async function authenticateAndGuard(
83
96
  messages,
84
97
  state.maxLen,
85
98
  )
86
- const userMessage = filtered
99
+ const userMessages = filtered
87
100
  .filter((m) => m.role === 'user')
88
- .map((m) => m.content)
89
- .join('\n\n')
101
+ // A thread-backed host already owns the persisted history. Send only the
102
+ // new turn to avoid storing the caller's full history as one new user row.
103
+ // Keep the historical concatenation for the default consumer session.
104
+ const userMessage = config.conversationMode === 'thread'
105
+ ? userMessages[userMessages.length - 1]?.content ?? ''
106
+ : userMessages.map((m) => m.content).join('\n\n')
90
107
  if (!userMessage) {
91
108
  return c.json(
92
109
  { error: { message: 'No user message provided', type: 'invalid_request' } },
@@ -176,6 +193,12 @@ export async function authenticateAndGuard(
176
193
  let mppPaymentIdentity: string | undefined
177
194
 
178
195
  if (spendAuthHeader) {
196
+ if (!isX402AuthEnabled(config)) {
197
+ return c.json(
198
+ { error: { message: 'x402 authentication is not configured', type: 'authentication_error' } },
199
+ { status: 401, headers: { 'X-Request-Id': requestId } },
200
+ )
201
+ }
179
202
  const signer = await verifyX402(
180
203
  spendAuthHeader,
181
204
  config.x402,
@@ -301,7 +324,8 @@ export async function authenticateAndGuard(
301
324
  code: 'payment_required',
302
325
  httpStatus: 402,
303
326
  })
304
- const methods: string[] = ['x402']
327
+ const methods: string[] = []
328
+ if (isX402AuthEnabled(config)) methods.push('x402')
305
329
  if (isMppAuthEnabled(config)) methods.push('mpp')
306
330
  if (isApiKeyAuthEnabled(config)) methods.push('api_key')
307
331
  const headers: Record<string, string> = {
@@ -318,14 +342,18 @@ export async function authenticateAndGuard(
318
342
  message: 'Payment required',
319
343
  type: 'payment_required',
320
344
  payment_methods: methods,
321
- x402: {
322
- operator: config.x402.operatorAddress,
323
- chain_id: config.x402.chainId,
324
- credits_address: config.x402.creditsAddress,
325
- required_amount: requiredPaymentAmount.toString(),
326
- currency_decimals: config.x402.currencyDecimals ?? 6,
327
- max_output_tokens: maxOutputTokens,
328
- },
345
+ ...(isX402AuthEnabled(config)
346
+ ? {
347
+ x402: {
348
+ operator: config.x402.operatorAddress,
349
+ chain_id: config.x402.chainId,
350
+ credits_address: config.x402.creditsAddress,
351
+ required_amount: requiredPaymentAmount.toString(),
352
+ currency_decimals: config.x402.currencyDecimals ?? 6,
353
+ max_output_tokens: maxOutputTokens,
354
+ },
355
+ }
356
+ : {}),
329
357
  ...(isMppAuthEnabled(config) && config.mpp
330
358
  ? { mpp: { realm: config.mpp.realm, method: config.mpp.method ?? 'blueprintevm' } }
331
359
  : {}),
@@ -399,6 +427,7 @@ export async function authenticateAndGuard(
399
427
  consumerId: consumerId,
400
428
  keyId: keyInfo?.keyId,
401
429
  requestId,
430
+ ...(threadId ? { threadId } : {}),
402
431
  })
403
432
  if (!authz.allow) {
404
433
  return c.json(
@@ -422,6 +451,8 @@ export async function authenticateAndGuard(
422
451
  userMessage,
423
452
  rateLimitRemaining: rl.remaining,
424
453
  requestId,
454
+ messages: filtered,
455
+ ...(threadId ? { threadId } : {}),
425
456
  startMs,
426
457
  maxOutputTokens,
427
458
  executionBudget,
@@ -7,6 +7,7 @@ import {
7
7
  import type {
8
8
  AgentMeta,
9
9
  GatewayConfig,
10
+ GatewaySandboxContext,
10
11
  SandboxExecutionBudget,
11
12
  SandboxStreamEvent,
12
13
  SandboxUsageReceipt,
@@ -58,9 +59,10 @@ export async function* dispatchSandboxStreamRich(
58
59
  onSandboxStart?: () => void | Promise<void>,
59
60
  maxInputTokens?: number,
60
61
  onExecutionHeartbeat?: () => Promise<void>,
62
+ sandboxContext?: GatewaySandboxContext,
61
63
  ): AsyncIterable<A2ADispatchEvent> {
62
64
  if (signal?.aborted) return
63
- const box = await config.getSandbox(agent)
65
+ const box = await config.getSandbox(agent, sandboxContext)
64
66
  if (signal?.aborted) return
65
67
  const outputLimit = maxOutputTokens ?? config.defaultOutputTokens ?? 1024
66
68
  if (!Number.isSafeInteger(outputLimit) || outputLimit <= 0) {
@@ -7,6 +7,7 @@ import type { PaymentSettlementBasis } from './payment-recovery'
7
7
  import type {
8
8
  AgentMeta,
9
9
  ApiKeyInfo,
10
+ ChatMessage,
10
11
  PaymentMethod,
11
12
  SandboxExecutionBudget,
12
13
  SandboxUsageReceipt,
@@ -37,6 +38,8 @@ export interface AuthorizedRequest {
37
38
  userMessage: string
38
39
  rateLimitRemaining: number | undefined
39
40
  requestId: string
41
+ messages?: ChatMessage[]
42
+ threadId?: string
40
43
  startMs: number
41
44
  maxOutputTokens: number
42
45
  executionBudget: SandboxExecutionBudget
package/src/index.ts CHANGED
@@ -111,6 +111,8 @@ export type {
111
111
  SandboxUsageReceipt,
112
112
  SandboxStreamEvent,
113
113
  SandboxBox,
114
+ ApiKeyGatewayConfig,
115
+ CreateAgentGatewayConfig,
114
116
  GatewayConfig,
115
117
  ChatMessage,
116
118
  ChatCompletionRequest,
package/src/middleware.ts CHANGED
@@ -23,8 +23,13 @@ import {
23
23
  assertPaymentRecoveryConfig,
24
24
  } from './payment-recovery'
25
25
  import { MemoryRateLimitStore, type RateLimitStore } from './rate-limit'
26
- import type { ChatCompletionChunk, ChatCompletionRequest, GatewayConfig } from './types'
27
- import { isApiKeyAuthEnabled, isMppAuthEnabled } from './verify'
26
+ import type {
27
+ ChatCompletionChunk,
28
+ ChatCompletionRequest,
29
+ CreateAgentGatewayConfig,
30
+ GatewayConfig,
31
+ } from './types'
32
+ import { isApiKeyAuthEnabled, isMppAuthEnabled, isX402AuthEnabled } from './verify'
28
33
 
29
34
  /**
30
35
  * Create a Hono router that serves the agent gateway.
@@ -36,13 +41,19 @@ import { isApiKeyAuthEnabled, isMppAuthEnabled } from './verify'
36
41
  * GET /:slug/chat/completions — agent discovery metadata (Tangle-native shape)
37
42
  * POST /:slug/chat/completions — OpenAI-compatible chat endpoint (paid)
38
43
  */
39
- export function createAgentGateway(inputConfig: GatewayConfig) {
40
- let config = inputConfig
44
+ export function createAgentGateway(inputConfig: CreateAgentGatewayConfig) {
45
+ let config: GatewayConfig = inputConfig.x402
46
+ ? inputConfig
47
+ : {
48
+ ...inputConfig,
49
+ x402: { operatorAddress: '', chainId: 0 },
50
+ }
41
51
  // Production gateways must verify x402 signatures. Tests and local
42
52
  // dev can opt into the explicit demo path.
43
- if (!config.x402.verifySigner && !config.x402.demoMode) {
53
+ if (!isX402AuthEnabled(config) && !config.verifyApiKey) {
44
54
  throw new Error(
45
- 'createAgentGateway: x402.verifySigner is required in production. ' +
55
+ 'createAgentGateway: verifySigner is required in production unless verifyApiKey is configured; ' +
56
+ 'configure x402.verifySigner or verifyApiKey. ' +
46
57
  'For tests, set x402.demoMode: true explicitly.',
47
58
  )
48
59
  }
@@ -185,14 +196,15 @@ export function createAgentGateway(inputConfig: GatewayConfig) {
185
196
  const agent = await config.resolveAgent(slug)
186
197
  if (!agent || !agent.enabled) return c.json({ error: 'Agent not found or not published' }, 404)
187
198
 
188
- const paymentMethods: Array<Record<string, unknown>> = [
189
- {
199
+ const paymentMethods: Array<Record<string, unknown>> = []
200
+ if (isX402AuthEnabled(config)) {
201
+ paymentMethods.push({
190
202
  type: 'x402',
191
203
  operator: config.x402.operatorAddress,
192
204
  chain_id: config.x402.chainId,
193
205
  credits_contract: config.x402.creditsAddress,
194
- },
195
- ]
206
+ })
207
+ }
196
208
  if (isMppAuthEnabled(config)) {
197
209
  paymentMethods.push({
198
210
  type: 'mpp',
@@ -200,7 +212,9 @@ export function createAgentGateway(inputConfig: GatewayConfig) {
200
212
  method: config.mpp!.method ?? 'blueprintevm',
201
213
  })
202
214
  }
203
- if (isApiKeyAuthEnabled(config)) paymentMethods.push({ type: 'api_key', prefix: 'sk_agent_' })
215
+ if (isApiKeyAuthEnabled(config)) {
216
+ paymentMethods.push({ type: 'api_key', prefix: config.apiKeyPrefix ?? 'sk_agent_' })
217
+ }
204
218
 
205
219
  return c.json({
206
220
  slug: agent.slug,
@@ -412,7 +426,7 @@ function streamChatCompletions(
412
426
  consumerId,
413
427
  config,
414
428
  abortController.signal,
415
- undefined,
429
+ authz.threadId,
416
430
  maxOutputTokens,
417
431
  () => beginPaymentExecution(authz, config),
418
432
  authz.paymentOperation !== undefined || authz.mppChargeOperation !== undefined,
@@ -422,6 +436,14 @@ function streamChatCompletions(
422
436
  },
423
437
  authz.executionBudget.maxInputTokens,
424
438
  () => renewPaymentExecution(authz, config),
439
+ {
440
+ consumerId,
441
+ paymentMethod,
442
+ keyInfo: authz.keyInfo,
443
+ requestId,
444
+ messages: authz.messages ?? [],
445
+ ...(authz.threadId ? { threadId: authz.threadId } : {}),
446
+ },
425
447
  )) {
426
448
  if (event.kind === 'text') {
427
449
  sendChunk(event.delta)
@@ -499,6 +521,7 @@ function streamChatCompletions(
499
521
  'X-Request-Id': requestId,
500
522
  'X-Agent-Slug': agent.slug,
501
523
  'X-Agent-Hosting': agent.sandboxEndpoint ? 'sovereign' : 'centralized',
524
+ ...(authz.threadId ? { 'X-Tangle-Thread-Id': authz.threadId } : {}),
502
525
  'X-Payment-Method': paymentMethod,
503
526
  'X-Payment-Settled': paymentMethod === 'x402' || authz.paymentOperation ? 'pending' : 'true',
504
527
  ...(authz.mppChargeOperation
package/src/types.ts CHANGED
@@ -225,14 +225,28 @@ export interface SandboxBox {
225
225
  ): AsyncIterable<SandboxStreamEvent>
226
226
  }
227
227
 
228
+ /** Authenticated request identity supplied when the host resolves a sandbox. */
229
+ export interface GatewaySandboxContext {
230
+ consumerId: string
231
+ paymentMethod: PaymentMethod
232
+ keyInfo: ApiKeyInfo | null
233
+ requestId: string
234
+ messages: ChatMessage[]
235
+ /** Stable UI conversation id when `conversationMode` is `thread`. */
236
+ threadId?: string
237
+ }
238
+
228
239
  // --- Gateway config ---
229
240
 
230
241
  export interface GatewayConfig {
231
242
  /** Resolve agent metadata by slug. Return null if not found or not published. */
232
243
  resolveAgent: (slug: string) => Promise<AgentMeta | null>
233
244
 
234
- /** Get a sandbox instance for the agent. Called after payment is verified. */
235
- getSandbox: (agent: AgentMeta) => Promise<SandboxBox>
245
+ /**
246
+ * Get the agent execution adapter after payment is verified.
247
+ * Hosts that use agent-app can drive their normal persisted chat route here.
248
+ */
249
+ getSandbox: (agent: AgentMeta, context?: GatewaySandboxContext) => Promise<SandboxBox>
236
250
 
237
251
  /**
238
252
  * Optional host authorization hook fired after payment verification
@@ -241,7 +255,14 @@ export interface GatewayConfig {
241
255
  */
242
256
  authorizeConsumer?: (
243
257
  agent: AgentMeta,
244
- consumer: { method: PaymentMethod; consumerId: string; keyId?: string; requestId: string },
258
+ consumer: {
259
+ method: PaymentMethod
260
+ consumerId: string
261
+ keyId?: string
262
+ requestId: string
263
+ /** Requested stable conversation id, after syntax validation. */
264
+ threadId?: string
265
+ },
245
266
  ) => Promise<{ allow: true } | { allow: false; reason: string; code: string }>
246
267
 
247
268
  /**
@@ -283,6 +304,16 @@ export interface GatewayConfig {
283
304
  /** Base URL for API key purchase links (e.g. "https://film.tangle.tools") */
284
305
  baseUrl?: string
285
306
 
307
+ /** Public API key prefix shown by discovery. Defaults to `sk_agent_`. */
308
+ apiKeyPrefix?: string
309
+
310
+ /**
311
+ * `consumer` keeps the historical session per API consumer.
312
+ * `thread` accepts `X-Tangle-Thread-Id` or creates one per request and returns
313
+ * it in the response, so a host can display the same conversation.
314
+ */
315
+ conversationMode?: 'consumer' | 'thread'
316
+
286
317
  /** Max message length in chars (default: 8000) */
287
318
  maxMessageLength?: number
288
319
 
@@ -392,6 +423,16 @@ export interface GatewayConfig {
392
423
  }
393
424
  }
394
425
 
426
+ /** API-key-only gateway input. Payment transports stay disabled. */
427
+ export type ApiKeyGatewayConfig = Omit<GatewayConfig, 'mpp' | 'verifyApiKey' | 'x402'> & {
428
+ mpp?: never
429
+ verifyApiKey: NonNullable<GatewayConfig['verifyApiKey']>
430
+ x402?: never
431
+ }
432
+
433
+ /** Configuration accepted by `createAgentGateway`. */
434
+ export type CreateAgentGatewayConfig = GatewayConfig | ApiKeyGatewayConfig
435
+
395
436
  // --- Chat completion types (OpenAI-compatible) ---
396
437
 
397
438
  export interface ChatMessage {
package/src/verify.ts CHANGED
@@ -108,6 +108,13 @@ export function isApiKeyAuthEnabled(
108
108
  return config.verifyApiKey !== undefined || config.x402.demoMode === true
109
109
  }
110
110
 
111
+ /** x402 is advertised only when the gateway can authenticate it. */
112
+ export function isX402AuthEnabled(
113
+ config: Pick<GatewayConfig, 'x402'>,
114
+ ): boolean {
115
+ return config.x402.verifySigner !== undefined || config.x402.demoMode === true
116
+ }
117
+
111
118
  /** MPP is enabled only when authentication and method settlement are complete. */
112
119
  export function isMppAuthEnabled(
113
120
  config: Pick<GatewayConfig, 'mpp' | 'x402'>,