@anuma/agent-runtime 1.0.0-next.20260602215959

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ZetaChain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs ADDED
@@ -0,0 +1,381 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AuthError: () => AuthError,
24
+ createConnectorTokenGetter: () => createConnectorTokenGetter,
25
+ createPortalClient: () => createPortalClient,
26
+ denyInteractive: () => denyInteractive,
27
+ extractConnectorToolErrors: () => extractConnectorToolErrors,
28
+ extractGrantContext: () => extractGrantContext,
29
+ runAgentRequest: () => runAgentRequest
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/errors.ts
34
+ var AuthError = class _AuthError extends Error {
35
+ constructor(subtype, message) {
36
+ super(message ?? subtype);
37
+ this.name = "AuthError";
38
+ this.subtype = subtype;
39
+ Object.setPrototypeOf(this, _AuthError.prototype);
40
+ }
41
+ };
42
+
43
+ // src/extractGrantContext.ts
44
+ var DEFAULT_PORTAL_URL = "https://portal.anuma.ai";
45
+ var DEFAULT_TIMEOUT_MS = 5e3;
46
+ function readBearer(req) {
47
+ const headerValue = req.headers.authorization ?? req.headers.Authorization;
48
+ if (!headerValue) {
49
+ throw new AuthError("missing_bearer");
50
+ }
51
+ const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;
52
+ if (!value) throw new AuthError("missing_bearer");
53
+ const [scheme, token] = value.split(" ");
54
+ if (!scheme || scheme.toLowerCase() !== "bearer" || !token) {
55
+ throw new AuthError("invalid_bearer", "Authorization header must use the Bearer scheme");
56
+ }
57
+ return token;
58
+ }
59
+ async function extractGrantContext(req, opts = {}) {
60
+ const bearer = readBearer(req);
61
+ const baseUrl = opts.baseUrl ?? process.env.ANUMA_PORTAL_URL ?? DEFAULT_PORTAL_URL;
62
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
63
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
64
+ const controller = new AbortController();
65
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
66
+ let response;
67
+ try {
68
+ response = await fetchImpl(`${baseUrl}/api/v1/me`, {
69
+ method: "GET",
70
+ headers: {
71
+ Authorization: `Bearer ${bearer}`
72
+ },
73
+ signal: controller.signal
74
+ });
75
+ } catch (err) {
76
+ if (err.name === "AbortError") {
77
+ throw new Error(`portal /api/v1/me timed out after ${timeoutMs}ms`);
78
+ }
79
+ throw err;
80
+ } finally {
81
+ clearTimeout(timer);
82
+ }
83
+ if (response.status === 401) {
84
+ throw new AuthError("expired", "portal /api/v1/me returned 401");
85
+ }
86
+ if (response.status === 403) {
87
+ throw new AuthError("revoked", "portal /api/v1/me returned 403");
88
+ }
89
+ if (!response.ok) {
90
+ throw new Error(`portal /api/v1/me returned ${response.status}`);
91
+ }
92
+ const me = await response.json();
93
+ if (!me.user_address || !me.client_id) {
94
+ throw new AuthError("invalid_bearer", "portal /api/v1/me missing user_address or client_id");
95
+ }
96
+ return {
97
+ userAddress: me.user_address,
98
+ clientId: me.client_id,
99
+ scopes: me.scopes ?? [],
100
+ bearer
101
+ };
102
+ }
103
+
104
+ // src/createPortalClient.ts
105
+ var DEFAULT_PORTAL_URL2 = "https://portal.anuma.ai";
106
+ var DEFAULT_TIMEOUT_MS2 = 5e3;
107
+ var DEFAULT_MAX_RETRIES = 3;
108
+ var DEFAULT_RETRY_BASE_MS = 100;
109
+ function jitter(baseMs, attempt) {
110
+ const exp = baseMs * Math.pow(2, attempt);
111
+ return Math.floor(Math.random() * exp);
112
+ }
113
+ function sleep(ms) {
114
+ return new Promise((resolve) => setTimeout(resolve, ms));
115
+ }
116
+ function parseMintError(status, body) {
117
+ const code = body.code ?? body.error;
118
+ if (status === 412 || status === 403) {
119
+ if (code === "connector_not_connected") {
120
+ return {
121
+ code: "connector_not_connected",
122
+ provider: body.provider ?? "unknown",
123
+ connectUrl: body.connect_url ?? ""
124
+ };
125
+ }
126
+ if (code === "scope_not_covered") {
127
+ return {
128
+ code: "scope_not_covered",
129
+ provider: body.provider ?? "unknown",
130
+ missingScopes: body.missing_scopes ?? [],
131
+ connectUrl: body.connect_url ?? ""
132
+ };
133
+ }
134
+ if (code === "insufficient_scope") {
135
+ return {
136
+ code: "insufficient_scope",
137
+ required: body.required ?? ""
138
+ };
139
+ }
140
+ }
141
+ return {
142
+ code: "unknown",
143
+ message: body.message ?? `mint failed with status ${status}`
144
+ };
145
+ }
146
+ function createPortalClient(bearer, opts = {}) {
147
+ const baseUrl = opts.baseUrl ?? process.env.ANUMA_PORTAL_URL ?? DEFAULT_PORTAL_URL2;
148
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
149
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
150
+ const maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
151
+ const retryBaseMs = opts.retryBaseMs ?? DEFAULT_RETRY_BASE_MS;
152
+ async function requestWithRetry(path, init) {
153
+ let lastErr;
154
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
155
+ const controller = new AbortController();
156
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
157
+ try {
158
+ const response = await fetchImpl(`${baseUrl}${path}`, {
159
+ ...init,
160
+ signal: controller.signal,
161
+ headers: {
162
+ Authorization: `Bearer ${bearer}`,
163
+ ...init.headers
164
+ }
165
+ });
166
+ clearTimeout(timer);
167
+ if (response.status >= 500) {
168
+ lastErr = new Error(`portal ${path} returned ${response.status}`);
169
+ if (attempt < maxRetries - 1) {
170
+ await sleep(jitter(retryBaseMs, attempt));
171
+ continue;
172
+ }
173
+ return response;
174
+ }
175
+ return response;
176
+ } catch (err) {
177
+ clearTimeout(timer);
178
+ lastErr = err;
179
+ if (attempt < maxRetries - 1) {
180
+ await sleep(jitter(retryBaseMs, attempt));
181
+ continue;
182
+ }
183
+ throw lastErr;
184
+ }
185
+ }
186
+ throw lastErr ?? new Error(`portal ${path} failed`);
187
+ }
188
+ return {
189
+ async mintConnectorToken(provider) {
190
+ const response = await requestWithRetry(`/api/v1/connector-tokens/${provider}`, {
191
+ method: "POST",
192
+ headers: { "Content-Type": "application/json" }
193
+ });
194
+ if (response.ok) {
195
+ const body2 = await response.json();
196
+ return {
197
+ ok: true,
198
+ accessToken: body2.access_token,
199
+ expiresAt: Date.now() + body2.expires_in * 1e3
200
+ };
201
+ }
202
+ if (response.status >= 500) {
203
+ return {
204
+ ok: false,
205
+ error: { code: "upstream_unavailable" }
206
+ };
207
+ }
208
+ let body = {};
209
+ try {
210
+ body = await response.json();
211
+ } catch {
212
+ body = {};
213
+ }
214
+ return { ok: false, error: parseMintError(response.status, body) };
215
+ },
216
+ async listConnectors() {
217
+ const response = await requestWithRetry("/api/v1/connectors", { method: "GET" });
218
+ if (!response.ok) {
219
+ throw new Error(`portal /api/v1/connectors returned ${response.status}`);
220
+ }
221
+ const body = await response.json();
222
+ return body.connectors.map((c) => ({
223
+ oauthApp: c.oauth_app,
224
+ externalAccount: c.external_account,
225
+ grantedScopes: c.granted_scopes ?? [],
226
+ connectedAt: c.connected_at ?? 0
227
+ }));
228
+ },
229
+ async createConnectTicket(opts2) {
230
+ const response = await requestWithRetry("/api/v1/connect-tickets", {
231
+ method: "POST",
232
+ headers: { "Content-Type": "application/json" },
233
+ body: JSON.stringify({
234
+ oauth_app: opts2.oauthApp,
235
+ requested_scopes: opts2.requestedScopes,
236
+ return_to: opts2.returnTo
237
+ })
238
+ });
239
+ if (!response.ok) {
240
+ throw new Error(`portal /api/v1/connect-tickets returned ${response.status}`);
241
+ }
242
+ const body = await response.json();
243
+ return {
244
+ ticketId: body.ticket_id,
245
+ expiresAt: body.expires_at,
246
+ connectUrl: body.connect_url
247
+ };
248
+ }
249
+ };
250
+ }
251
+
252
+ // src/createConnectorTokenGetter.ts
253
+ var import_tools = require("@anuma/sdk/tools");
254
+ function createConnectorTokenGetter(client, provider, opts) {
255
+ return (0, import_tools.createConnectorTokenGetter)(client, provider, opts);
256
+ }
257
+
258
+ // src/runAgentRequest.ts
259
+ var import_server = require("@anuma/sdk/server");
260
+ var import_tools2 = require("@anuma/sdk/tools");
261
+ function isConnectorErrorPayload(value) {
262
+ if (!value || typeof value !== "object") return false;
263
+ return value[import_tools2.CONNECTOR_ERROR_MARKER] === true;
264
+ }
265
+ function extractConnectorToolErrors(toolResults) {
266
+ if (!toolResults) return [];
267
+ const errors = [];
268
+ for (let idx = 0; idx < toolResults.length; idx++) {
269
+ const entry = toolResults[idx];
270
+ if (typeof entry.result !== "string") continue;
271
+ let parsed;
272
+ try {
273
+ parsed = JSON.parse(entry.result);
274
+ } catch {
275
+ continue;
276
+ }
277
+ if (!isConnectorErrorPayload(parsed)) continue;
278
+ errors.push({
279
+ toolName: entry.name,
280
+ callId: `call_${idx}`,
281
+ error: {
282
+ code: parsed.code,
283
+ provider: parsed.provider,
284
+ connectUrl: parsed.connect_url,
285
+ missingScopes: parsed.missing_scopes,
286
+ required: parsed.required
287
+ }
288
+ });
289
+ }
290
+ return errors;
291
+ }
292
+ function extractUsage(data) {
293
+ if (!data || typeof data !== "object") return void 0;
294
+ const usage = data.usage;
295
+ if (!usage) return void 0;
296
+ const input = typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : typeof usage.input_tokens === "number" ? usage.input_tokens : void 0;
297
+ const output = typeof usage.completion_tokens === "number" ? usage.completion_tokens : typeof usage.output_tokens === "number" ? usage.output_tokens : void 0;
298
+ const total = typeof usage.total_tokens === "number" ? usage.total_tokens : void 0;
299
+ if (input === void 0 && output === void 0 && total === void 0) return void 0;
300
+ return { inputTokens: input, outputTokens: output, totalTokens: total };
301
+ }
302
+ function finalAssistantMessage(data) {
303
+ if (!data || typeof data !== "object") return void 0;
304
+ const chatLike = data;
305
+ const chatMsg = chatLike.choices?.[0]?.message;
306
+ if (chatMsg) return chatMsg;
307
+ const resp = data;
308
+ const output = resp.output;
309
+ if (Array.isArray(output)) {
310
+ for (const item of output) {
311
+ const message = item.message;
312
+ if (message) return message;
313
+ }
314
+ }
315
+ return void 0;
316
+ }
317
+ function buildResponseMessages(inputMessages, toolResults, finalMessage) {
318
+ const out = [...inputMessages];
319
+ if (toolResults && toolResults.length > 0) {
320
+ out.push({
321
+ role: "assistant",
322
+ tool_calls: toolResults.map((tr, idx) => ({
323
+ id: `call_${idx}`,
324
+ type: "function",
325
+ function: { name: tr.name, arguments: "{}" }
326
+ }))
327
+ });
328
+ for (let idx = 0; idx < toolResults.length; idx++) {
329
+ const tr = toolResults[idx];
330
+ const content = typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result) ?? "";
331
+ out.push({
332
+ role: "tool",
333
+ tool_call_id: `call_${idx}`,
334
+ content: [{ type: "text", text: content }]
335
+ });
336
+ }
337
+ }
338
+ if (finalMessage) out.push(finalMessage);
339
+ return out;
340
+ }
341
+ async function denyInteractive() {
342
+ throw new Error("server agent cannot initiate OAuth; user must connect via portal");
343
+ }
344
+ async function runAgentRequest(opts) {
345
+ const grant = await extractGrantContext(opts.request, opts.portalClientOpts);
346
+ const portal = createPortalClient(grant.bearer, opts.portalClientOpts);
347
+ const tools = (opts.toolFactories ?? []).flatMap((factory) => factory(portal));
348
+ const loopMessages = [
349
+ { role: "system", content: [{ type: "text", text: opts.agent.prompt }] },
350
+ ...opts.messages
351
+ ];
352
+ const loopResult = await (0, import_server.runToolLoop)({
353
+ messages: loopMessages,
354
+ model: opts.agent.model.default,
355
+ token: grant.bearer,
356
+ tools,
357
+ headers: { "X-Anuma-Surface": "agent" },
358
+ ...opts.portalBaseUrl ? { baseUrl: opts.portalBaseUrl } : void 0,
359
+ ...opts.transport ? { transport: opts.transport } : void 0,
360
+ ...opts.apiType ? { apiType: opts.apiType } : void 0
361
+ });
362
+ if (loopResult.error !== null) {
363
+ throw new Error(loopResult.error);
364
+ }
365
+ const finalMessage = finalAssistantMessage(loopResult.data);
366
+ const autoResults = loopResult.autoExecutedToolResults;
367
+ const messages = buildResponseMessages(opts.messages, autoResults, finalMessage);
368
+ const toolErrors = extractConnectorToolErrors(autoResults);
369
+ const usage = extractUsage(loopResult.data);
370
+ return { messages, toolErrors, usage, grant };
371
+ }
372
+ // Annotate the CommonJS export names for ESM import in node:
373
+ 0 && (module.exports = {
374
+ AuthError,
375
+ createConnectorTokenGetter,
376
+ createPortalClient,
377
+ denyInteractive,
378
+ extractConnectorToolErrors,
379
+ extractGrantContext,
380
+ runAgentRequest
381
+ });
@@ -0,0 +1,267 @@
1
+ import { ConnectorMintError, ConnectorTokenGetterOpts } from '@anuma/sdk/tools';
2
+ export { ConnectorTokenGetterOpts } from '@anuma/sdk/tools';
3
+ import { LlmapiMessage } from '@anuma/sdk';
4
+ import { ToolConfig, StreamingTransport, ApiType, AutoExecutedToolResult } from '@anuma/sdk/server';
5
+
6
+ /**
7
+ * Public types for `@anuma/agent-runtime`.
8
+ *
9
+ * Mirrors the contract in `.claude-docs/connecters/agent-runtime-spec.md`.
10
+ * These types are the contract between the SDK / portal vault and any
11
+ * server-side consumer (Haven, Sentinel, future SMS handler).
12
+ */
13
+
14
+ /**
15
+ * Parsed result of an incoming bearer token. Returned by
16
+ * {@link extractGrantContext}.
17
+ */
18
+ interface GrantContext {
19
+ /** Wallet address — primary user identifier on the portal. */
20
+ userAddress: string;
21
+ /** OAuth `client_id` that owns the bearer (e.g. `"haven_v1"`). */
22
+ clientId: string;
23
+ /** Scopes the user granted to this client. */
24
+ scopes: string[];
25
+ /** Bearer token verbatim — for relaying to the portal. */
26
+ bearer: string;
27
+ }
28
+ /**
29
+ * Discriminated result of a mint call. Failures are returned, never
30
+ * thrown — transport errors (5xx / network) raise from {@link PortalClient}
31
+ * directly.
32
+ */
33
+ type MintResult = {
34
+ ok: true;
35
+ accessToken: string;
36
+ expiresAt: number;
37
+ } | {
38
+ ok: false;
39
+ error: MintError;
40
+ };
41
+ /** Variants of `MintResult.error`. Re-exported from the SDK so consumers
42
+ * importing only `@anuma/agent-runtime` get the union without a second
43
+ * import. */
44
+ type MintError = ConnectorMintError;
45
+ /** Returned by {@link PortalClient.listConnectors}. */
46
+ interface ConnectorInfo {
47
+ oauthApp: string;
48
+ externalAccount?: string;
49
+ grantedScopes: string[];
50
+ connectedAt: number;
51
+ }
52
+ /** Argument shape for {@link PortalClient.createConnectTicket}. */
53
+ interface ConnectTicketOpts {
54
+ oauthApp: string;
55
+ requestedScopes: string[];
56
+ returnTo: string;
57
+ }
58
+ /** Result of a ticket-mint call. */
59
+ interface ConnectTicket {
60
+ ticketId: string;
61
+ expiresAt: number;
62
+ connectUrl: string;
63
+ }
64
+ /** Typed wrapper over the portal HTTP API. */
65
+ interface PortalClient {
66
+ /** Mint a fresh upstream access token for a logical provider. */
67
+ mintConnectorToken(provider: string): Promise<MintResult>;
68
+ /** List the user's currently-connected connectors. */
69
+ listConnectors(): Promise<ConnectorInfo[]>;
70
+ /** Mint a connect ticket so the user can be redirected to a connect flow. */
71
+ createConnectTicket(opts: ConnectTicketOpts): Promise<ConnectTicket>;
72
+ }
73
+ interface PortalClientOpts {
74
+ /** Defaults to `process.env.ANUMA_PORTAL_URL` then the production portal. */
75
+ baseUrl?: string;
76
+ /** Defaults to `globalThis.fetch`. */
77
+ fetchImpl?: typeof fetch;
78
+ /** Per-request timeout. @default 5000 */
79
+ timeoutMs?: number;
80
+ /** Max retry attempts on 5xx / network errors. @default 3 */
81
+ maxRetries?: number;
82
+ /** Initial backoff before the second attempt. @default 100 */
83
+ retryBaseMs?: number;
84
+ }
85
+ /** Structurally-typed incoming request. Any HTTP framework satisfies this. */
86
+ interface IncomingRequest {
87
+ headers: {
88
+ authorization?: string;
89
+ Authorization?: string;
90
+ [key: string]: string | string[] | undefined;
91
+ };
92
+ }
93
+ /**
94
+ * Structured tool error lifted from a tool-result message after the loop
95
+ * completes. Connector errors carry the canonical
96
+ * `__anuma_connector_error_v1` payload; other tool failures may surface
97
+ * here in the future.
98
+ */
99
+ /**
100
+ * Open shape so future tool-execution errors can lift into the same union
101
+ * without breaking consumers. Connector errors populate `provider` +
102
+ * `connectUrl`; other errors carry a `message`.
103
+ */
104
+ interface ToolErrorInfo {
105
+ code: string;
106
+ provider?: string;
107
+ connectUrl?: string;
108
+ missingScopes?: string[];
109
+ required?: string;
110
+ message?: string;
111
+ }
112
+ interface ToolError {
113
+ toolName: string;
114
+ callId: string;
115
+ error: ToolErrorInfo;
116
+ }
117
+
118
+ /**
119
+ * Error subclasses thrown by `@anuma/agent-runtime`.
120
+ *
121
+ * - {@link AuthError} comes out of `extractGrantContext` when the bearer is
122
+ * missing, malformed, expired, or already revoked.
123
+ */
124
+ /** Discriminant for {@link AuthError}. */
125
+ type AuthErrorSubtype = "missing_bearer" | "invalid_bearer" | "expired" | "revoked";
126
+ declare class AuthError extends Error {
127
+ readonly subtype: AuthErrorSubtype;
128
+ constructor(subtype: AuthErrorSubtype, message?: string);
129
+ }
130
+
131
+ /**
132
+ * Pull the bearer off an incoming request, validate it against the portal,
133
+ * and return the parsed grant.
134
+ *
135
+ * In v1 the portal validates by exposing `GET /api/v1/me`, which returns
136
+ * `{ user_address, client_id, scopes }`. A later portal version may switch
137
+ * to issuing signed JWTs and let `extractGrantContext` do local decode —
138
+ * the consumer signature stays the same.
139
+ */
140
+
141
+ type ExtractGrantContextOpts = Pick<PortalClientOpts, "baseUrl" | "fetchImpl" | "timeoutMs">;
142
+ declare function extractGrantContext(req: IncomingRequest, opts?: ExtractGrantContextOpts): Promise<GrantContext>;
143
+
144
+ /**
145
+ * Typed HTTP client over the portal API.
146
+ *
147
+ * Retries on 5xx / network errors with exponential backoff + jitter
148
+ * (default 3 attempts, base 100ms). Never retries 4xx — those become
149
+ * `MintResult.ok=false` (for mint) or thrown errors (for the bookkeeping
150
+ * endpoints `listConnectors` / `createConnectTicket`).
151
+ *
152
+ * Surface is duck-typed against `PortalClient` so test stubs can satisfy
153
+ * the same contract.
154
+ */
155
+
156
+ declare function createPortalClient(bearer: string, opts?: PortalClientOpts): PortalClient;
157
+
158
+ /**
159
+ * Convenience wrapper that re-exports the SDK's
160
+ * `createConnectorTokenGetter` with the same per-instance cache semantics.
161
+ *
162
+ * The wrapper exists so consumers importing only `@anuma/agent-runtime`
163
+ * get the helper as part of a single import surface, without also
164
+ * importing `@anuma/sdk/tools` directly. The behavior is identical.
165
+ */
166
+
167
+ declare function createConnectorTokenGetter(client: PortalClient, provider: string, opts?: ConnectorTokenGetterOpts): () => Promise<string | null>;
168
+
169
+ /**
170
+ * `runAgentRequest` — the single function a server-side agent host calls
171
+ * per inbound request.
172
+ *
173
+ * Wires the four pieces together:
174
+ * extractGrantContext → createPortalClient → toolFactories → runToolLoop
175
+ *
176
+ * After the loop returns, walks the auto-executed tool results, lifts any
177
+ * payload carrying the canonical `__anuma_connector_error_v1` marker into
178
+ * `AgentResponse.toolErrors`. The marker shape is the load-bearing contract
179
+ * — every connector tool factory uses `buildConnectorErrorResult` to emit
180
+ * it, and the parser keys solely on that marker so it can't false-positive
181
+ * on tools that legitimately return JSON.
182
+ */
183
+
184
+ /** Minimal slice of `AgentConfig` this runtime depends on. */
185
+ interface AgentConfigLike {
186
+ /** Model selection — at least `default` must be present. */
187
+ model: {
188
+ default: string;
189
+ };
190
+ /** System prompt threaded into the loop. */
191
+ prompt: string;
192
+ }
193
+ interface AgentRequestOpts {
194
+ /** Inbound request — only `headers.authorization` is read. */
195
+ request: IncomingRequest;
196
+ /** Agent configuration (haven, sentinel, …). */
197
+ agent: AgentConfigLike;
198
+ /** Conversation history including the user turn. */
199
+ messages: LlmapiMessage[];
200
+ /** Tool factories that receive the portal client and return `ToolConfig[]`. */
201
+ toolFactories?: Array<(portalClient: PortalClient) => ToolConfig[]>;
202
+ /** Override portal client opts (test injection). */
203
+ portalClientOpts?: PortalClientOpts;
204
+ /**
205
+ * Override the streaming transport runToolLoop uses to talk to the
206
+ * portal's chat completion API. Production code never sets this — it's
207
+ * the injection point e2e tests use to stub LLM behavior without a real
208
+ * portal in the loop. Forwarded verbatim to runToolLoop's
209
+ * `transport` option.
210
+ */
211
+ transport?: StreamingTransport;
212
+ /**
213
+ * Optional portal base URL forwarded to runToolLoop for chat completions.
214
+ * Defaults to runToolLoop's own default. Setting this without a stub
215
+ * `transport` will hit the real portal.
216
+ */
217
+ portalBaseUrl?: string;
218
+ /**
219
+ * Optional override for the LLM API strategy ("responses" | "completions" | "auto").
220
+ * Production callers don't set this — the default ("auto") picks the right
221
+ * endpoint for the model. Tests use "completions" with the stub transport
222
+ * to feed OpenAI-style streaming chunks.
223
+ */
224
+ apiType?: ApiType;
225
+ }
226
+ /** Minimal usage summary lifted off the LLM response. */
227
+ interface UsageSummary {
228
+ inputTokens?: number;
229
+ outputTokens?: number;
230
+ totalTokens?: number;
231
+ }
232
+ interface AgentResponse {
233
+ /**
234
+ * Conversation messages after the loop completes: the input messages,
235
+ * followed by tool-result messages for each auto-executed tool, followed
236
+ * by the final assistant message. Synthesized — `runToolLoop` doesn't
237
+ * expose the full message history directly.
238
+ */
239
+ messages: LlmapiMessage[];
240
+ /** Structured tool errors lifted from the post-loop parser. */
241
+ toolErrors: ToolError[];
242
+ /** Token usage summary if the response carried one. */
243
+ usage?: UsageSummary;
244
+ /** Grant context, surfaced for logging / multi-tenant context propagation. */
245
+ grant: GrantContext;
246
+ }
247
+ /**
248
+ * Walk the loop's tool results, lift entries that carry the
249
+ * `__anuma_connector_error_v1` marker into structured `ToolError`s.
250
+ *
251
+ * Each `AutoExecutedToolResult.result` is what the executor returned.
252
+ * Connector tool factories return the JSON string produced by
253
+ * `buildConnectorErrorResult`, so we JSON.parse strings and inspect for
254
+ * the marker. Non-string results (objects, arrays, errors) skip the
255
+ * parser entirely — they can't be connector errors.
256
+ */
257
+ declare function extractConnectorToolErrors(toolResults: AutoExecutedToolResult[] | undefined): ToolError[];
258
+ /**
259
+ * Default `requestAccess` passed to connector tool factories from inside
260
+ * `runAgentRequest`. Server agents cannot drive interactive OAuth — they
261
+ * have no surface to bounce the user through. The factory falls back to
262
+ * emitting the canonical connector error JSON when this throws.
263
+ */
264
+ declare function denyInteractive(): Promise<string | null>;
265
+ declare function runAgentRequest(opts: AgentRequestOpts): Promise<AgentResponse>;
266
+
267
+ export { type AgentConfigLike, type AgentRequestOpts, type AgentResponse, AuthError, type AuthErrorSubtype, type ConnectTicket, type ConnectTicketOpts, type ConnectorInfo, type ExtractGrantContextOpts, type GrantContext, type IncomingRequest, type MintError, type MintResult, type PortalClient, type PortalClientOpts, type ToolError, type ToolErrorInfo, type UsageSummary, createConnectorTokenGetter, createPortalClient, denyInteractive, extractConnectorToolErrors, extractGrantContext, runAgentRequest };
@@ -0,0 +1,267 @@
1
+ import { ConnectorMintError, ConnectorTokenGetterOpts } from '@anuma/sdk/tools';
2
+ export { ConnectorTokenGetterOpts } from '@anuma/sdk/tools';
3
+ import { LlmapiMessage } from '@anuma/sdk';
4
+ import { ToolConfig, StreamingTransport, ApiType, AutoExecutedToolResult } from '@anuma/sdk/server';
5
+
6
+ /**
7
+ * Public types for `@anuma/agent-runtime`.
8
+ *
9
+ * Mirrors the contract in `.claude-docs/connecters/agent-runtime-spec.md`.
10
+ * These types are the contract between the SDK / portal vault and any
11
+ * server-side consumer (Haven, Sentinel, future SMS handler).
12
+ */
13
+
14
+ /**
15
+ * Parsed result of an incoming bearer token. Returned by
16
+ * {@link extractGrantContext}.
17
+ */
18
+ interface GrantContext {
19
+ /** Wallet address — primary user identifier on the portal. */
20
+ userAddress: string;
21
+ /** OAuth `client_id` that owns the bearer (e.g. `"haven_v1"`). */
22
+ clientId: string;
23
+ /** Scopes the user granted to this client. */
24
+ scopes: string[];
25
+ /** Bearer token verbatim — for relaying to the portal. */
26
+ bearer: string;
27
+ }
28
+ /**
29
+ * Discriminated result of a mint call. Failures are returned, never
30
+ * thrown — transport errors (5xx / network) raise from {@link PortalClient}
31
+ * directly.
32
+ */
33
+ type MintResult = {
34
+ ok: true;
35
+ accessToken: string;
36
+ expiresAt: number;
37
+ } | {
38
+ ok: false;
39
+ error: MintError;
40
+ };
41
+ /** Variants of `MintResult.error`. Re-exported from the SDK so consumers
42
+ * importing only `@anuma/agent-runtime` get the union without a second
43
+ * import. */
44
+ type MintError = ConnectorMintError;
45
+ /** Returned by {@link PortalClient.listConnectors}. */
46
+ interface ConnectorInfo {
47
+ oauthApp: string;
48
+ externalAccount?: string;
49
+ grantedScopes: string[];
50
+ connectedAt: number;
51
+ }
52
+ /** Argument shape for {@link PortalClient.createConnectTicket}. */
53
+ interface ConnectTicketOpts {
54
+ oauthApp: string;
55
+ requestedScopes: string[];
56
+ returnTo: string;
57
+ }
58
+ /** Result of a ticket-mint call. */
59
+ interface ConnectTicket {
60
+ ticketId: string;
61
+ expiresAt: number;
62
+ connectUrl: string;
63
+ }
64
+ /** Typed wrapper over the portal HTTP API. */
65
+ interface PortalClient {
66
+ /** Mint a fresh upstream access token for a logical provider. */
67
+ mintConnectorToken(provider: string): Promise<MintResult>;
68
+ /** List the user's currently-connected connectors. */
69
+ listConnectors(): Promise<ConnectorInfo[]>;
70
+ /** Mint a connect ticket so the user can be redirected to a connect flow. */
71
+ createConnectTicket(opts: ConnectTicketOpts): Promise<ConnectTicket>;
72
+ }
73
+ interface PortalClientOpts {
74
+ /** Defaults to `process.env.ANUMA_PORTAL_URL` then the production portal. */
75
+ baseUrl?: string;
76
+ /** Defaults to `globalThis.fetch`. */
77
+ fetchImpl?: typeof fetch;
78
+ /** Per-request timeout. @default 5000 */
79
+ timeoutMs?: number;
80
+ /** Max retry attempts on 5xx / network errors. @default 3 */
81
+ maxRetries?: number;
82
+ /** Initial backoff before the second attempt. @default 100 */
83
+ retryBaseMs?: number;
84
+ }
85
+ /** Structurally-typed incoming request. Any HTTP framework satisfies this. */
86
+ interface IncomingRequest {
87
+ headers: {
88
+ authorization?: string;
89
+ Authorization?: string;
90
+ [key: string]: string | string[] | undefined;
91
+ };
92
+ }
93
+ /**
94
+ * Structured tool error lifted from a tool-result message after the loop
95
+ * completes. Connector errors carry the canonical
96
+ * `__anuma_connector_error_v1` payload; other tool failures may surface
97
+ * here in the future.
98
+ */
99
+ /**
100
+ * Open shape so future tool-execution errors can lift into the same union
101
+ * without breaking consumers. Connector errors populate `provider` +
102
+ * `connectUrl`; other errors carry a `message`.
103
+ */
104
+ interface ToolErrorInfo {
105
+ code: string;
106
+ provider?: string;
107
+ connectUrl?: string;
108
+ missingScopes?: string[];
109
+ required?: string;
110
+ message?: string;
111
+ }
112
+ interface ToolError {
113
+ toolName: string;
114
+ callId: string;
115
+ error: ToolErrorInfo;
116
+ }
117
+
118
+ /**
119
+ * Error subclasses thrown by `@anuma/agent-runtime`.
120
+ *
121
+ * - {@link AuthError} comes out of `extractGrantContext` when the bearer is
122
+ * missing, malformed, expired, or already revoked.
123
+ */
124
+ /** Discriminant for {@link AuthError}. */
125
+ type AuthErrorSubtype = "missing_bearer" | "invalid_bearer" | "expired" | "revoked";
126
+ declare class AuthError extends Error {
127
+ readonly subtype: AuthErrorSubtype;
128
+ constructor(subtype: AuthErrorSubtype, message?: string);
129
+ }
130
+
131
+ /**
132
+ * Pull the bearer off an incoming request, validate it against the portal,
133
+ * and return the parsed grant.
134
+ *
135
+ * In v1 the portal validates by exposing `GET /api/v1/me`, which returns
136
+ * `{ user_address, client_id, scopes }`. A later portal version may switch
137
+ * to issuing signed JWTs and let `extractGrantContext` do local decode —
138
+ * the consumer signature stays the same.
139
+ */
140
+
141
+ type ExtractGrantContextOpts = Pick<PortalClientOpts, "baseUrl" | "fetchImpl" | "timeoutMs">;
142
+ declare function extractGrantContext(req: IncomingRequest, opts?: ExtractGrantContextOpts): Promise<GrantContext>;
143
+
144
+ /**
145
+ * Typed HTTP client over the portal API.
146
+ *
147
+ * Retries on 5xx / network errors with exponential backoff + jitter
148
+ * (default 3 attempts, base 100ms). Never retries 4xx — those become
149
+ * `MintResult.ok=false` (for mint) or thrown errors (for the bookkeeping
150
+ * endpoints `listConnectors` / `createConnectTicket`).
151
+ *
152
+ * Surface is duck-typed against `PortalClient` so test stubs can satisfy
153
+ * the same contract.
154
+ */
155
+
156
+ declare function createPortalClient(bearer: string, opts?: PortalClientOpts): PortalClient;
157
+
158
+ /**
159
+ * Convenience wrapper that re-exports the SDK's
160
+ * `createConnectorTokenGetter` with the same per-instance cache semantics.
161
+ *
162
+ * The wrapper exists so consumers importing only `@anuma/agent-runtime`
163
+ * get the helper as part of a single import surface, without also
164
+ * importing `@anuma/sdk/tools` directly. The behavior is identical.
165
+ */
166
+
167
+ declare function createConnectorTokenGetter(client: PortalClient, provider: string, opts?: ConnectorTokenGetterOpts): () => Promise<string | null>;
168
+
169
+ /**
170
+ * `runAgentRequest` — the single function a server-side agent host calls
171
+ * per inbound request.
172
+ *
173
+ * Wires the four pieces together:
174
+ * extractGrantContext → createPortalClient → toolFactories → runToolLoop
175
+ *
176
+ * After the loop returns, walks the auto-executed tool results, lifts any
177
+ * payload carrying the canonical `__anuma_connector_error_v1` marker into
178
+ * `AgentResponse.toolErrors`. The marker shape is the load-bearing contract
179
+ * — every connector tool factory uses `buildConnectorErrorResult` to emit
180
+ * it, and the parser keys solely on that marker so it can't false-positive
181
+ * on tools that legitimately return JSON.
182
+ */
183
+
184
+ /** Minimal slice of `AgentConfig` this runtime depends on. */
185
+ interface AgentConfigLike {
186
+ /** Model selection — at least `default` must be present. */
187
+ model: {
188
+ default: string;
189
+ };
190
+ /** System prompt threaded into the loop. */
191
+ prompt: string;
192
+ }
193
+ interface AgentRequestOpts {
194
+ /** Inbound request — only `headers.authorization` is read. */
195
+ request: IncomingRequest;
196
+ /** Agent configuration (haven, sentinel, …). */
197
+ agent: AgentConfigLike;
198
+ /** Conversation history including the user turn. */
199
+ messages: LlmapiMessage[];
200
+ /** Tool factories that receive the portal client and return `ToolConfig[]`. */
201
+ toolFactories?: Array<(portalClient: PortalClient) => ToolConfig[]>;
202
+ /** Override portal client opts (test injection). */
203
+ portalClientOpts?: PortalClientOpts;
204
+ /**
205
+ * Override the streaming transport runToolLoop uses to talk to the
206
+ * portal's chat completion API. Production code never sets this — it's
207
+ * the injection point e2e tests use to stub LLM behavior without a real
208
+ * portal in the loop. Forwarded verbatim to runToolLoop's
209
+ * `transport` option.
210
+ */
211
+ transport?: StreamingTransport;
212
+ /**
213
+ * Optional portal base URL forwarded to runToolLoop for chat completions.
214
+ * Defaults to runToolLoop's own default. Setting this without a stub
215
+ * `transport` will hit the real portal.
216
+ */
217
+ portalBaseUrl?: string;
218
+ /**
219
+ * Optional override for the LLM API strategy ("responses" | "completions" | "auto").
220
+ * Production callers don't set this — the default ("auto") picks the right
221
+ * endpoint for the model. Tests use "completions" with the stub transport
222
+ * to feed OpenAI-style streaming chunks.
223
+ */
224
+ apiType?: ApiType;
225
+ }
226
+ /** Minimal usage summary lifted off the LLM response. */
227
+ interface UsageSummary {
228
+ inputTokens?: number;
229
+ outputTokens?: number;
230
+ totalTokens?: number;
231
+ }
232
+ interface AgentResponse {
233
+ /**
234
+ * Conversation messages after the loop completes: the input messages,
235
+ * followed by tool-result messages for each auto-executed tool, followed
236
+ * by the final assistant message. Synthesized — `runToolLoop` doesn't
237
+ * expose the full message history directly.
238
+ */
239
+ messages: LlmapiMessage[];
240
+ /** Structured tool errors lifted from the post-loop parser. */
241
+ toolErrors: ToolError[];
242
+ /** Token usage summary if the response carried one. */
243
+ usage?: UsageSummary;
244
+ /** Grant context, surfaced for logging / multi-tenant context propagation. */
245
+ grant: GrantContext;
246
+ }
247
+ /**
248
+ * Walk the loop's tool results, lift entries that carry the
249
+ * `__anuma_connector_error_v1` marker into structured `ToolError`s.
250
+ *
251
+ * Each `AutoExecutedToolResult.result` is what the executor returned.
252
+ * Connector tool factories return the JSON string produced by
253
+ * `buildConnectorErrorResult`, so we JSON.parse strings and inspect for
254
+ * the marker. Non-string results (objects, arrays, errors) skip the
255
+ * parser entirely — they can't be connector errors.
256
+ */
257
+ declare function extractConnectorToolErrors(toolResults: AutoExecutedToolResult[] | undefined): ToolError[];
258
+ /**
259
+ * Default `requestAccess` passed to connector tool factories from inside
260
+ * `runAgentRequest`. Server agents cannot drive interactive OAuth — they
261
+ * have no surface to bounce the user through. The factory falls back to
262
+ * emitting the canonical connector error JSON when this throws.
263
+ */
264
+ declare function denyInteractive(): Promise<string | null>;
265
+ declare function runAgentRequest(opts: AgentRequestOpts): Promise<AgentResponse>;
266
+
267
+ export { type AgentConfigLike, type AgentRequestOpts, type AgentResponse, AuthError, type AuthErrorSubtype, type ConnectTicket, type ConnectTicketOpts, type ConnectorInfo, type ExtractGrantContextOpts, type GrantContext, type IncomingRequest, type MintError, type MintResult, type PortalClient, type PortalClientOpts, type ToolError, type ToolErrorInfo, type UsageSummary, createConnectorTokenGetter, createPortalClient, denyInteractive, extractConnectorToolErrors, extractGrantContext, runAgentRequest };
package/dist/index.mjs ADDED
@@ -0,0 +1,352 @@
1
+ // src/errors.ts
2
+ var AuthError = class _AuthError extends Error {
3
+ constructor(subtype, message) {
4
+ super(message ?? subtype);
5
+ this.name = "AuthError";
6
+ this.subtype = subtype;
7
+ Object.setPrototypeOf(this, _AuthError.prototype);
8
+ }
9
+ };
10
+
11
+ // src/extractGrantContext.ts
12
+ var DEFAULT_PORTAL_URL = "https://portal.anuma.ai";
13
+ var DEFAULT_TIMEOUT_MS = 5e3;
14
+ function readBearer(req) {
15
+ const headerValue = req.headers.authorization ?? req.headers.Authorization;
16
+ if (!headerValue) {
17
+ throw new AuthError("missing_bearer");
18
+ }
19
+ const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;
20
+ if (!value) throw new AuthError("missing_bearer");
21
+ const [scheme, token] = value.split(" ");
22
+ if (!scheme || scheme.toLowerCase() !== "bearer" || !token) {
23
+ throw new AuthError("invalid_bearer", "Authorization header must use the Bearer scheme");
24
+ }
25
+ return token;
26
+ }
27
+ async function extractGrantContext(req, opts = {}) {
28
+ const bearer = readBearer(req);
29
+ const baseUrl = opts.baseUrl ?? process.env.ANUMA_PORTAL_URL ?? DEFAULT_PORTAL_URL;
30
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
31
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
32
+ const controller = new AbortController();
33
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
34
+ let response;
35
+ try {
36
+ response = await fetchImpl(`${baseUrl}/api/v1/me`, {
37
+ method: "GET",
38
+ headers: {
39
+ Authorization: `Bearer ${bearer}`
40
+ },
41
+ signal: controller.signal
42
+ });
43
+ } catch (err) {
44
+ if (err.name === "AbortError") {
45
+ throw new Error(`portal /api/v1/me timed out after ${timeoutMs}ms`);
46
+ }
47
+ throw err;
48
+ } finally {
49
+ clearTimeout(timer);
50
+ }
51
+ if (response.status === 401) {
52
+ throw new AuthError("expired", "portal /api/v1/me returned 401");
53
+ }
54
+ if (response.status === 403) {
55
+ throw new AuthError("revoked", "portal /api/v1/me returned 403");
56
+ }
57
+ if (!response.ok) {
58
+ throw new Error(`portal /api/v1/me returned ${response.status}`);
59
+ }
60
+ const me = await response.json();
61
+ if (!me.user_address || !me.client_id) {
62
+ throw new AuthError("invalid_bearer", "portal /api/v1/me missing user_address or client_id");
63
+ }
64
+ return {
65
+ userAddress: me.user_address,
66
+ clientId: me.client_id,
67
+ scopes: me.scopes ?? [],
68
+ bearer
69
+ };
70
+ }
71
+
72
+ // src/createPortalClient.ts
73
+ var DEFAULT_PORTAL_URL2 = "https://portal.anuma.ai";
74
+ var DEFAULT_TIMEOUT_MS2 = 5e3;
75
+ var DEFAULT_MAX_RETRIES = 3;
76
+ var DEFAULT_RETRY_BASE_MS = 100;
77
+ function jitter(baseMs, attempt) {
78
+ const exp = baseMs * Math.pow(2, attempt);
79
+ return Math.floor(Math.random() * exp);
80
+ }
81
+ function sleep(ms) {
82
+ return new Promise((resolve) => setTimeout(resolve, ms));
83
+ }
84
+ function parseMintError(status, body) {
85
+ const code = body.code ?? body.error;
86
+ if (status === 412 || status === 403) {
87
+ if (code === "connector_not_connected") {
88
+ return {
89
+ code: "connector_not_connected",
90
+ provider: body.provider ?? "unknown",
91
+ connectUrl: body.connect_url ?? ""
92
+ };
93
+ }
94
+ if (code === "scope_not_covered") {
95
+ return {
96
+ code: "scope_not_covered",
97
+ provider: body.provider ?? "unknown",
98
+ missingScopes: body.missing_scopes ?? [],
99
+ connectUrl: body.connect_url ?? ""
100
+ };
101
+ }
102
+ if (code === "insufficient_scope") {
103
+ return {
104
+ code: "insufficient_scope",
105
+ required: body.required ?? ""
106
+ };
107
+ }
108
+ }
109
+ return {
110
+ code: "unknown",
111
+ message: body.message ?? `mint failed with status ${status}`
112
+ };
113
+ }
114
+ function createPortalClient(bearer, opts = {}) {
115
+ const baseUrl = opts.baseUrl ?? process.env.ANUMA_PORTAL_URL ?? DEFAULT_PORTAL_URL2;
116
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
117
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
118
+ const maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
119
+ const retryBaseMs = opts.retryBaseMs ?? DEFAULT_RETRY_BASE_MS;
120
+ async function requestWithRetry(path, init) {
121
+ let lastErr;
122
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
123
+ const controller = new AbortController();
124
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
125
+ try {
126
+ const response = await fetchImpl(`${baseUrl}${path}`, {
127
+ ...init,
128
+ signal: controller.signal,
129
+ headers: {
130
+ Authorization: `Bearer ${bearer}`,
131
+ ...init.headers
132
+ }
133
+ });
134
+ clearTimeout(timer);
135
+ if (response.status >= 500) {
136
+ lastErr = new Error(`portal ${path} returned ${response.status}`);
137
+ if (attempt < maxRetries - 1) {
138
+ await sleep(jitter(retryBaseMs, attempt));
139
+ continue;
140
+ }
141
+ return response;
142
+ }
143
+ return response;
144
+ } catch (err) {
145
+ clearTimeout(timer);
146
+ lastErr = err;
147
+ if (attempt < maxRetries - 1) {
148
+ await sleep(jitter(retryBaseMs, attempt));
149
+ continue;
150
+ }
151
+ throw lastErr;
152
+ }
153
+ }
154
+ throw lastErr ?? new Error(`portal ${path} failed`);
155
+ }
156
+ return {
157
+ async mintConnectorToken(provider) {
158
+ const response = await requestWithRetry(`/api/v1/connector-tokens/${provider}`, {
159
+ method: "POST",
160
+ headers: { "Content-Type": "application/json" }
161
+ });
162
+ if (response.ok) {
163
+ const body2 = await response.json();
164
+ return {
165
+ ok: true,
166
+ accessToken: body2.access_token,
167
+ expiresAt: Date.now() + body2.expires_in * 1e3
168
+ };
169
+ }
170
+ if (response.status >= 500) {
171
+ return {
172
+ ok: false,
173
+ error: { code: "upstream_unavailable" }
174
+ };
175
+ }
176
+ let body = {};
177
+ try {
178
+ body = await response.json();
179
+ } catch {
180
+ body = {};
181
+ }
182
+ return { ok: false, error: parseMintError(response.status, body) };
183
+ },
184
+ async listConnectors() {
185
+ const response = await requestWithRetry("/api/v1/connectors", { method: "GET" });
186
+ if (!response.ok) {
187
+ throw new Error(`portal /api/v1/connectors returned ${response.status}`);
188
+ }
189
+ const body = await response.json();
190
+ return body.connectors.map((c) => ({
191
+ oauthApp: c.oauth_app,
192
+ externalAccount: c.external_account,
193
+ grantedScopes: c.granted_scopes ?? [],
194
+ connectedAt: c.connected_at ?? 0
195
+ }));
196
+ },
197
+ async createConnectTicket(opts2) {
198
+ const response = await requestWithRetry("/api/v1/connect-tickets", {
199
+ method: "POST",
200
+ headers: { "Content-Type": "application/json" },
201
+ body: JSON.stringify({
202
+ oauth_app: opts2.oauthApp,
203
+ requested_scopes: opts2.requestedScopes,
204
+ return_to: opts2.returnTo
205
+ })
206
+ });
207
+ if (!response.ok) {
208
+ throw new Error(`portal /api/v1/connect-tickets returned ${response.status}`);
209
+ }
210
+ const body = await response.json();
211
+ return {
212
+ ticketId: body.ticket_id,
213
+ expiresAt: body.expires_at,
214
+ connectUrl: body.connect_url
215
+ };
216
+ }
217
+ };
218
+ }
219
+
220
+ // src/createConnectorTokenGetter.ts
221
+ import {
222
+ createConnectorTokenGetter as sdkCreateConnectorTokenGetter
223
+ } from "@anuma/sdk/tools";
224
+ function createConnectorTokenGetter(client, provider, opts) {
225
+ return sdkCreateConnectorTokenGetter(client, provider, opts);
226
+ }
227
+
228
+ // src/runAgentRequest.ts
229
+ import {
230
+ runToolLoop
231
+ } from "@anuma/sdk/server";
232
+ import { CONNECTOR_ERROR_MARKER } from "@anuma/sdk/tools";
233
+ function isConnectorErrorPayload(value) {
234
+ if (!value || typeof value !== "object") return false;
235
+ return value[CONNECTOR_ERROR_MARKER] === true;
236
+ }
237
+ function extractConnectorToolErrors(toolResults) {
238
+ if (!toolResults) return [];
239
+ const errors = [];
240
+ for (let idx = 0; idx < toolResults.length; idx++) {
241
+ const entry = toolResults[idx];
242
+ if (typeof entry.result !== "string") continue;
243
+ let parsed;
244
+ try {
245
+ parsed = JSON.parse(entry.result);
246
+ } catch {
247
+ continue;
248
+ }
249
+ if (!isConnectorErrorPayload(parsed)) continue;
250
+ errors.push({
251
+ toolName: entry.name,
252
+ callId: `call_${idx}`,
253
+ error: {
254
+ code: parsed.code,
255
+ provider: parsed.provider,
256
+ connectUrl: parsed.connect_url,
257
+ missingScopes: parsed.missing_scopes,
258
+ required: parsed.required
259
+ }
260
+ });
261
+ }
262
+ return errors;
263
+ }
264
+ function extractUsage(data) {
265
+ if (!data || typeof data !== "object") return void 0;
266
+ const usage = data.usage;
267
+ if (!usage) return void 0;
268
+ const input = typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : typeof usage.input_tokens === "number" ? usage.input_tokens : void 0;
269
+ const output = typeof usage.completion_tokens === "number" ? usage.completion_tokens : typeof usage.output_tokens === "number" ? usage.output_tokens : void 0;
270
+ const total = typeof usage.total_tokens === "number" ? usage.total_tokens : void 0;
271
+ if (input === void 0 && output === void 0 && total === void 0) return void 0;
272
+ return { inputTokens: input, outputTokens: output, totalTokens: total };
273
+ }
274
+ function finalAssistantMessage(data) {
275
+ if (!data || typeof data !== "object") return void 0;
276
+ const chatLike = data;
277
+ const chatMsg = chatLike.choices?.[0]?.message;
278
+ if (chatMsg) return chatMsg;
279
+ const resp = data;
280
+ const output = resp.output;
281
+ if (Array.isArray(output)) {
282
+ for (const item of output) {
283
+ const message = item.message;
284
+ if (message) return message;
285
+ }
286
+ }
287
+ return void 0;
288
+ }
289
+ function buildResponseMessages(inputMessages, toolResults, finalMessage) {
290
+ const out = [...inputMessages];
291
+ if (toolResults && toolResults.length > 0) {
292
+ out.push({
293
+ role: "assistant",
294
+ tool_calls: toolResults.map((tr, idx) => ({
295
+ id: `call_${idx}`,
296
+ type: "function",
297
+ function: { name: tr.name, arguments: "{}" }
298
+ }))
299
+ });
300
+ for (let idx = 0; idx < toolResults.length; idx++) {
301
+ const tr = toolResults[idx];
302
+ const content = typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result) ?? "";
303
+ out.push({
304
+ role: "tool",
305
+ tool_call_id: `call_${idx}`,
306
+ content: [{ type: "text", text: content }]
307
+ });
308
+ }
309
+ }
310
+ if (finalMessage) out.push(finalMessage);
311
+ return out;
312
+ }
313
+ async function denyInteractive() {
314
+ throw new Error("server agent cannot initiate OAuth; user must connect via portal");
315
+ }
316
+ async function runAgentRequest(opts) {
317
+ const grant = await extractGrantContext(opts.request, opts.portalClientOpts);
318
+ const portal = createPortalClient(grant.bearer, opts.portalClientOpts);
319
+ const tools = (opts.toolFactories ?? []).flatMap((factory) => factory(portal));
320
+ const loopMessages = [
321
+ { role: "system", content: [{ type: "text", text: opts.agent.prompt }] },
322
+ ...opts.messages
323
+ ];
324
+ const loopResult = await runToolLoop({
325
+ messages: loopMessages,
326
+ model: opts.agent.model.default,
327
+ token: grant.bearer,
328
+ tools,
329
+ headers: { "X-Anuma-Surface": "agent" },
330
+ ...opts.portalBaseUrl ? { baseUrl: opts.portalBaseUrl } : void 0,
331
+ ...opts.transport ? { transport: opts.transport } : void 0,
332
+ ...opts.apiType ? { apiType: opts.apiType } : void 0
333
+ });
334
+ if (loopResult.error !== null) {
335
+ throw new Error(loopResult.error);
336
+ }
337
+ const finalMessage = finalAssistantMessage(loopResult.data);
338
+ const autoResults = loopResult.autoExecutedToolResults;
339
+ const messages = buildResponseMessages(opts.messages, autoResults, finalMessage);
340
+ const toolErrors = extractConnectorToolErrors(autoResults);
341
+ const usage = extractUsage(loopResult.data);
342
+ return { messages, toolErrors, usage, grant };
343
+ }
344
+ export {
345
+ AuthError,
346
+ createConnectorTokenGetter,
347
+ createPortalClient,
348
+ denyInteractive,
349
+ extractConnectorToolErrors,
350
+ extractGrantContext,
351
+ runAgentRequest
352
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@anuma/agent-runtime",
3
+ "version": "1.0.0-next.20260602215959",
4
+ "description": "Server-side runtime contract for Anuma agents — extractGrantContext, PortalClient, runAgentRequest.",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs",
13
+ "default": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/anuma-ai/sdk.git",
22
+ "directory": "packages/agents/runtime"
23
+ },
24
+ "license": "ISC",
25
+ "bugs": {
26
+ "url": "https://github.com/anuma-ai/sdk/issues"
27
+ },
28
+ "homepage": "https://github.com/anuma-ai/sdk#readme",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "peerDependencies": {
33
+ "@anuma/sdk": "1.0.0-next.20260602215959"
34
+ },
35
+ "devDependencies": {
36
+ "tsup": "^8.5.1",
37
+ "typescript": "^5.9.3",
38
+ "vitest": "^4.0.14",
39
+ "@anuma/sdk": "1.0.0-next.20260602215959"
40
+ },
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "test": "vitest run"
44
+ }
45
+ }