basion-ai-sdk 0.14.2 → 0.16.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 +6 -6
- package/dist/{app-CK-AZFOB.d.ts → app-BGjdui8X.d.ts} +31 -10
- package/dist/extensions/langgraph.d.ts +1 -1
- package/dist/extensions/vercel-ai.d.ts +1 -1
- package/dist/extensions/vercel-ai.js +1 -0
- package/dist/extensions/vercel-ai.js.map +1 -1
- package/dist/extensions/vfs-sync.js +40 -8
- package/dist/extensions/vfs-sync.js.map +1 -1
- package/dist/index.d.ts +53 -3
- package/dist/index.js +221 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agent.ts +103 -22
- package/src/app.ts +4 -1
- package/src/dlq.ts +214 -0
- package/src/extensions/vfs-sync.ts +76 -9
- package/src/index.ts +7 -0
- package/src/message.ts +10 -1
- package/src/streamer.ts +21 -4
package/README.md
CHANGED
|
@@ -1574,10 +1574,10 @@ intakeAgent.onMessage(async (message, sender) => {
|
|
|
1574
1574
|
|
|
1575
1575
|
**How it works:**
|
|
1576
1576
|
1. Produces a single Kafka message to `router.inbox` with the original message headers
|
|
1577
|
-
2.
|
|
1577
|
+
2. Provider API (router module) forwards to the target agent's inbox and updates `currentRoute` to the target agent
|
|
1578
1578
|
3. Target agent's `onMessage` handler fires with the forwarded message
|
|
1579
1579
|
4. The calling agent does **not** send `done=true` — the target agent is responsible for responding to the user
|
|
1580
|
-
5. The conversation lock is **not stuck** — the target agent responds to the user via normal streamer flow, which sends `done=true` to `
|
|
1580
|
+
5. The conversation lock is **not stuck** — the target agent responds to the user via normal streamer flow, which sends `done=true` back to `router.inbox` and Provider API finalizes in-process (persists, unlocks, publishes to Centrifugo)
|
|
1581
1581
|
|
|
1582
1582
|
```typescript
|
|
1583
1583
|
handOff(agentName: string, content?: string): void
|
|
@@ -1799,9 +1799,9 @@ symptomAgent.onMessage(async (message, sender) => {
|
|
|
1799
1799
|
|
|
1800
1800
|
### Conversation Locking and Persistence
|
|
1801
1801
|
|
|
1802
|
-
**Hand-off does not cause stuck locks.** When Agent A hands off to Agent B, the conversation lock persists correctly through the transition. Agent B responds to the user via the normal streamer flow, which sends `done=true` to `
|
|
1802
|
+
**Hand-off does not cause stuck locks.** When Agent A hands off to Agent B, the conversation lock persists correctly through the transition. Agent B responds to the user via the normal streamer flow, which sends `done=true` back to `router.inbox`. Provider API consumes the message, finalizes in-process (persists, publishes to Centrifugo, unlocks), and the lock is never orphaned — it simply transfers naturally as Agent B takes over.
|
|
1803
1803
|
|
|
1804
|
-
**Call messages are not persisted.** Agent-to-agent call messages are transient by design — they never reach
|
|
1804
|
+
**Call messages are not persisted.** Agent-to-agent call messages are transient by design — they never reach the user-facing finalization path and the database never sees them. The `conversationId` passed to `agent.call()` serves only as a Kafka partition key for ordering guarantees. This is intentional: if call messages were persisted, they would pollute the conversation history with confusing consecutive assistant messages from different agents (the call question and the call response) that the user never saw. The calling agent already incorporates the call result into its own streamed response, keeping the LLM's conversation history clean and coherent.
|
|
1805
1805
|
|
|
1806
1806
|
## Remote Logging (Loki)
|
|
1807
1807
|
|
|
@@ -2176,8 +2176,8 @@ await app.run()
|
|
|
2176
2176
|
### Message Flow
|
|
2177
2177
|
|
|
2178
2178
|
```
|
|
2179
|
-
User → Provider → Kafka: router.inbox →
|
|
2180
|
-
Agent → Gateway → Kafka: router.inbox →
|
|
2179
|
+
User → Provider → Kafka: router.inbox → Provider (RouterConsumer) → Kafka: {agent}.inbox → Gateway → Agent
|
|
2180
|
+
Agent → Gateway → Kafka: router.inbox → Provider (RouterConsumer → MessageFinalizationService) → WebSocket → User
|
|
2181
2181
|
```
|
|
2182
2182
|
|
|
2183
2183
|
## Development
|
|
@@ -2154,10 +2154,20 @@ declare class Agent {
|
|
|
2154
2154
|
private initialized;
|
|
2155
2155
|
private running;
|
|
2156
2156
|
private _tools?;
|
|
2157
|
-
/**
|
|
2157
|
+
/**
|
|
2158
|
+
* Live AbortControllers for in-flight turns, keyed by conversationId.
|
|
2159
|
+
* `assistantMessageId` scopes the controller to a specific turn so a cancel
|
|
2160
|
+
* for a previous turn cannot abort the current one.
|
|
2161
|
+
*/
|
|
2158
2162
|
private _cancelControllers;
|
|
2159
|
-
/**
|
|
2160
|
-
|
|
2163
|
+
/**
|
|
2164
|
+
* Pre-cancel intents that arrived before the handler registered its controller.
|
|
2165
|
+
* Keyed by `assistantMessageId` (never by conversationId alone) so a stray
|
|
2166
|
+
* cancel from an earlier turn cannot poison the next turn. Entries self-expire
|
|
2167
|
+
* after PENDING_CANCEL_TTL_MS as defense-in-depth.
|
|
2168
|
+
*/
|
|
2169
|
+
private _pendingCancels;
|
|
2170
|
+
private static readonly PENDING_CANCEL_TTL_MS;
|
|
2161
2171
|
constructor(options: {
|
|
2162
2172
|
name: string;
|
|
2163
2173
|
gatewayClient: GatewayClient;
|
|
@@ -2177,19 +2187,30 @@ declare class Agent {
|
|
|
2177
2187
|
* Call this before starting work (e.g. streamText) and pass the
|
|
2178
2188
|
* returned signal so the operation can be cancelled externally.
|
|
2179
2189
|
*
|
|
2180
|
-
*
|
|
2181
|
-
*
|
|
2190
|
+
* Pass `assistantMessageId` (typically `message.responseMessageId`) so
|
|
2191
|
+
* a cancel message targeting THIS turn can be matched precisely; a
|
|
2192
|
+
* cancel for any other turn will be ignored.
|
|
2193
|
+
*
|
|
2194
|
+
* If a cancel request arrived before this call AND carried a matching
|
|
2195
|
+
* `assistantMessageId`, the returned signal will already be aborted.
|
|
2182
2196
|
*/
|
|
2183
|
-
createAbortSignal(conversationId: string): AbortSignal;
|
|
2197
|
+
createAbortSignal(conversationId: string, assistantMessageId?: string): AbortSignal;
|
|
2184
2198
|
/**
|
|
2185
2199
|
* Cancel the running operation for a conversation.
|
|
2186
2200
|
* Called by the message interceptor when a cancel message arrives.
|
|
2201
|
+
*
|
|
2202
|
+
* If `assistantMessageId` is provided and does not match the live
|
|
2203
|
+
* controller's, the cancel is dropped as stale (prevents a late cancel
|
|
2204
|
+
* from a previous turn from aborting the current one).
|
|
2187
2205
|
*/
|
|
2188
|
-
cancelConversation(conversationId: string): void;
|
|
2206
|
+
cancelConversation(conversationId: string, assistantMessageId?: string): void;
|
|
2189
2207
|
/**
|
|
2190
2208
|
* Clean up the abort controller after the handler completes.
|
|
2209
|
+
* Also purges any matching pre-cancel intent so a stale entry cannot
|
|
2210
|
+
* outlive the turn it was meant for.
|
|
2191
2211
|
*/
|
|
2192
|
-
clearAbortController(conversationId: string): void;
|
|
2212
|
+
clearAbortController(conversationId: string, assistantMessageId?: string): void;
|
|
2213
|
+
private purgeExpiredPendingCancels;
|
|
2193
2214
|
/**
|
|
2194
2215
|
* Access to tools (knowledge graph, etc.).
|
|
2195
2216
|
*
|
|
@@ -2298,7 +2319,7 @@ declare class Agent {
|
|
|
2298
2319
|
*
|
|
2299
2320
|
* Creates a conversation in the conversation store and returns a Streamer
|
|
2300
2321
|
* for sending the initial message. The message flows through the normal
|
|
2301
|
-
* Kafka pipeline (router
|
|
2322
|
+
* Kafka pipeline (router.inbox → provider → Centrifugo).
|
|
2302
2323
|
*
|
|
2303
2324
|
* The user's reply will enter the normal handler pipeline via onMessage().
|
|
2304
2325
|
*
|
|
@@ -2700,4 +2721,4 @@ declare class BasionAgentApp {
|
|
|
2700
2721
|
shutdown(): Promise<void>;
|
|
2701
2722
|
}
|
|
2702
2723
|
|
|
2703
|
-
export { type GetMessagesOptions as $, type AttachmentInfo as A, BasionAgentApp as B, type CheckboxConfig as C, type DatePickerConfig as D, type Capability as E, type Field as F,
|
|
2724
|
+
export { type GetMessagesOptions as $, type AttachmentInfo as A, BasionAgentApp as B, type CheckboxConfig as C, type DatePickerConfig as D, type Capability as E, type Field as F, GatewayClient as G, type HiddenConfig as H, type InferFields as I, type CapabilityItem as J, type ConnectionState as K, type ConnectionStateListener as L, type MultiSelectConfig as M, type NumberConfig as N, type OptionDef as O, Conversation as P, ConversationClient as Q, type ConversationData as R, StructuralStreamer as S, type TextConfig as T, ConversationMessage as U, type ValidationResult as V, DEFAULT_RECONNECTION_OPTIONS as W, type Edge as X, type Entity as Y, type GenuiComponent as Z, type GenuiComponentDict as _, type CheckboxGroupConfig as a, type GroupedMatch as a0, type GroupedMatchEntry as a1, type HeartbeatFailureCallback as a2, HeartbeatManager as a3, type HeartbeatOptions as a4, type IngestResponse as a5, type KafkaHeaders as a6, type KafkaMessage as a7, KnowledgeGraphTool as a8, type LogEntry as a9, type RelatedPage as aA, type RenderBufferResult as aB, RenderClient as aC, type RenderOptions as aD, type RenderResult as aE, type SearchMatch as aF, type SearchOptions as aG, type SearchResult as aH, SenderFilter as aI, type SenderFilterOptions as aJ, type SentryConfig as aK, type SentryLoggerProxy as aL, type SimilarDisease as aM, type StreamOptions as aN, Streamer as aO, type StreamerOptions as aP, Tools as aQ, type UserSummary as aR, createAttachmentInfo as aS, getSentryLogger as aT, isBraintrustEnabled as aU, isSentryEnabled as aV, reportPath as aW, type LogLevel as aa, type LokiHandlerOptions as ab, LokiLogHandler as ac, MatchingClient as ad, MatchingTool as ae, Memory as af, MemoryClient as ag, type MemoryIngestOptions as ah, type MemoryMessage as ai, type MemorySearchOptions as aj, type MemorySearchResult as ak, MemoryV2 as al, MemoryV2Client as am, type MemoryV2IngestPayload as an, type MemoryV2SearchPayload as ao, type MessageHandler as ap, type Need as aq, type NeedItem as ar, type NeedWithMatches as as, type PathStep as at, type PdfOptions as au, type ProduceAck as av, type Prompt as aw, type QueryOptions as ax, type ReconnectionOptions as ay, type RegisterOptions as az, type FileFieldConfig as b, type SelectConfig as c, type SliderConfig as d, type SwitchConfig as e, type ConfirmationConfig as f, Message as g, type ConfirmationResult as h, type StepDef as i, type ToDict as j, type AccordionItemDef as k, type AccordionConfig as l, type AccordionComponentDict as m, type CardVariant as n, type CardSectionDef as o, type CardButtonDef as p, type CardConfig as q, type CardComponentDict as r, Agent as s, type AgentInfo as t, AgentInventoryTool as u, type AgentRegistrationData as v, type AppOptions as w, AttachmentClient as x, type BraintrustConfig as y, type BundleRenderOptions as z };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/exceptions.ts","../../src/types.ts","../../src/agent.ts","../../src/agent-state-client.ts","../../src/extensions/vercel-ai.ts"],"names":[],"mappings":"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","file":"vercel-ai.js","sourcesContent":["/**\n * Basion Agent SDK - Custom Errors\n * Port of Python basion_agent/exceptions.py\n */\n\n/**\n * Base error class for Basion Agent SDK\n */\nexport class BasionAgentError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'BasionAgentError'\n Error.captureStackTrace?.(this, this.constructor)\n }\n}\n\n/**\n * Error during agent registration with AI Inventory\n */\nexport class RegistrationError extends BasionAgentError {\n constructor(message: string) {\n super(message)\n this.name = 'RegistrationError'\n }\n}\n\n/**\n * Error during Kafka operations (produce/consume)\n */\nexport class KafkaError extends BasionAgentError {\n constructor(message: string) {\n super(message)\n this.name = 'KafkaError'\n }\n}\n\n/**\n * Error in SDK configuration\n */\nexport class ConfigurationError extends BasionAgentError {\n constructor(message: string) {\n super(message)\n this.name = 'ConfigurationError'\n }\n}\n\n/**\n * Error from API calls to backend services\n */\nexport class APIException extends BasionAgentError {\n statusCode?: number\n\n constructor(message: string, statusCode?: number) {\n super(message)\n this.name = 'APIException'\n this.statusCode = statusCode\n }\n}\n\n/**\n * Error during heartbeat operations\n */\nexport class HeartbeatError extends BasionAgentError {\n constructor(message: string) {\n super(message)\n this.name = 'HeartbeatError'\n }\n}\n\n/**\n * Error during gRPC connection/communication\n */\nexport class GatewayError extends BasionAgentError {\n constructor(message: string) {\n super(message)\n this.name = 'GatewayError'\n }\n}\n\n/**\n * Error during reconnection attempts\n */\nexport class ReconnectionError extends BasionAgentError {\n /** Number of attempts made before giving up */\n attempts: number\n /** The last error that occurred during reconnection */\n lastError?: Error\n\n constructor(message: string, attempts: number, lastError?: Error) {\n super(message)\n this.name = 'ReconnectionError'\n this.attempts = attempts\n this.lastError = lastError\n }\n}\n","/**\n * Basion Agent SDK - Shared Types\n * Port of Python basion_agent types\n */\n\nimport { z } from 'zod'\n\n// ============================================================================\n// Kafka Message Types\n// ============================================================================\n\nexport interface KafkaMessage {\n topic: string\n partition: number\n offset: number\n key: string\n headers: Record<string, string>\n body: Record<string, unknown>\n timestamp: number\n}\n\nexport interface KafkaHeaders {\n conversationId?: string\n userId?: string\n from_?: string\n to_?: string\n messageMetadata?: string\n messageSchema?: string\n /** @deprecated Use attachments (plural) instead. Kept for backward compatibility. */\n attachment?: string\n /** JSON-stringified array of AttachmentInfo objects */\n attachments?: string\n /** UUID of the user message in conversation-store (for cross-system mapping) */\n userMessageId?: string\n /** Pre-generated UUID for the assistant response in conversation-store (for cross-system mapping) */\n assistantMessageId?: string\n [key: string]: string | undefined\n}\n\n// ============================================================================\n// Agent Registration Types\n// ============================================================================\n\nexport interface AgentRegistrationData {\n id: string\n name: string\n about: string\n document: string\n representationName?: string\n baseUrl?: string\n metadata?: Record<string, unknown>\n relatedPages?: RelatedPage[]\n}\n\nexport interface RelatedPage {\n name: string\n endpoint: string\n}\n\nexport interface Prompt {\n label: string\n prompt: string\n}\n\nexport interface RegisterOptions {\n name: string\n about: string\n document: string\n representationName?: string\n baseUrl?: string\n metadata?: Record<string, unknown>\n relatedPages?: RelatedPage[]\n categoryNames?: string[]\n prompts?: Prompt[]\n detailedDescription?: string\n version?: string\n lifecycleStage?: 'dev' | 'test' | 'private-preview' | 'public-preview' | 'public'\n welcomeMessage?: string\n waitlist?: boolean\n forceUpdate?: boolean\n}\n\n// ============================================================================\n// Message Types\n// ============================================================================\n\n// Forward reference - actual Message class is imported where needed\n// Using 'any' here to avoid circular dependency, but consumers should use Message type\nexport interface MessageHandler {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (message: any, sender: string): void | Promise<void>\n}\n\nexport interface SenderFilterOptions {\n senders?: string[]\n}\n\n// ============================================================================\n// Streamer Types\n// ============================================================================\n\nexport interface StreamOptions {\n persist?: boolean\n eventType?: string\n}\n\nexport interface StreamerOptions {\n awaiting?: boolean\n}\n\n// ============================================================================\n// Attachment Types\n// ============================================================================\n\nexport const AttachmentInfoSchema = z.object({\n url: z.string(),\n filename: z.string(),\n contentType: z.string(),\n size: z.number().optional(),\n fileType: z.string().optional(),\n objectKey: z.string().optional(),\n})\n\nexport type AttachmentInfo = z.infer<typeof AttachmentInfoSchema>\n\nexport function isImageAttachment(attachment: AttachmentInfo): boolean {\n return attachment.contentType?.startsWith('image/') || false\n}\n\nexport function isPdfAttachment(attachment: AttachmentInfo): boolean {\n return attachment.contentType === 'application/pdf'\n}\n\n// ============================================================================\n// Conversation Message Types\n// ============================================================================\n\nexport interface ConversationMessageData {\n id: string\n conversationId: string\n role: 'user' | 'assistant' | 'system'\n content: string\n from?: string\n to?: string\n createdAt: string\n metadata?: Record<string, unknown>\n}\n\n// ============================================================================\n// Gateway Client Types\n// ============================================================================\n\nexport interface ProduceAck {\n topic: string\n partition: number\n offset: number\n correlationId?: string\n}\n\n// ============================================================================\n// Reconnection Types\n// ============================================================================\n\n/**\n * Configuration options for automatic reconnection behavior.\n */\nexport interface ReconnectionOptions {\n /** Maximum number of reconnection attempts before giving up. Default: 10 */\n maxRetries?: number\n /** Initial delay in milliseconds before first retry. Default: 1000 */\n initialDelayMs?: number\n /** Maximum delay in milliseconds between retries. Default: 30000 */\n maxDelayMs?: number\n /** Multiplier for exponential backoff. Default: 2 */\n backoffMultiplier?: number\n /** Whether to automatically reconnect on disconnect. Default: true */\n autoReconnect?: boolean\n}\n\n/**\n * Connection state for the gateway client.\n */\nexport type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting'\n\n/**\n * Listener callback for connection state changes.\n */\nexport interface ConnectionStateListener {\n (state: ConnectionState, error?: Error): void\n}\n\n/**\n * Default reconnection options.\n */\nexport const DEFAULT_RECONNECTION_OPTIONS: Required<ReconnectionOptions> = {\n maxRetries: 10,\n initialDelayMs: 1000,\n maxDelayMs: 30000,\n backoffMultiplier: 2,\n autoReconnect: true,\n}\n\n// ============================================================================\n// App Configuration Types\n// ============================================================================\n\nexport interface AppOptions {\n gatewayUrl: string\n apiKey: string\n heartbeatInterval?: number\n maxConcurrentTasks?: number\n errorMessageTemplate?: string\n secure?: boolean\n /** Reconnection options for the gateway connection */\n reconnection?: ReconnectionOptions\n /** Enable remote logging to Loki via the gateway (default: false) */\n enableRemoteLogging?: boolean\n /** Minimum log level for remote logging (default: 'info') */\n remoteLogLevel?: 'debug' | 'info' | 'warn' | 'error'\n /** Number of logs to batch before sending (default: 100) */\n remoteLogBatchSize?: number\n /** Maximum seconds between log flushes (default: 5.0) */\n remoteLogFlushInterval?: number\n /** Sentry configuration (optional — omit or leave dsn empty to disable) */\n sentry?: import('./sentry.js').SentryConfig\n /** Braintrust configuration (optional — omit or leave apiKey empty to disable) */\n braintrust?: import('./braintrust.js').BraintrustConfig\n}\n\n// ============================================================================\n// HTTP Response Types\n// ============================================================================\n\nexport interface ConversationData {\n id: string\n userId: string\n agentId?: string\n awaitingRoute?: string\n pendingResponseSchema?: Record<string, unknown>\n createdAt: string\n updatedAt: string\n metadata?: Record<string, unknown>\n}\n\nexport interface GetMessagesOptions {\n role?: string\n from?: string\n to?: string\n limit?: number\n offset?: number\n}\n\n// ============================================================================\n// Memory Types\n// ============================================================================\n\n/**\n * A message stored in ai-memory.\n * Matches the ai-memory API MessageStored schema.\n */\nexport interface MemoryMessage {\n id?: string\n role: 'user' | 'assistant' | 'system'\n content: string\n tenantId?: string\n userId?: string\n conversationId?: string\n metadata?: Record<string, unknown>\n timestamp?: string\n sequenceNumber?: number\n}\n\n/**\n * Options for memory search operations.\n */\nexport interface MemorySearchOptions {\n /** Maximum number of results (1-100, default: 10) */\n limit?: number\n /** Minimum similarity threshold (0-100, default: 70) */\n minSimilarity?: number\n /** Number of surrounding context messages to include (0-20, default: 0) */\n contextMessages?: number\n /** Tenant ID for multi-tenant filtering */\n tenantId?: string\n /** Tenant matching mode: 'exact' or 'hierarchy' */\n tenantMatch?: 'exact' | 'hierarchy'\n}\n\n/**\n * A memory search result with similarity score and optional context.\n */\nexport interface MemorySearchResult {\n message: MemoryMessage\n score: number\n context: MemoryMessage[]\n}\n\n/**\n * User conversation summary from ai-memory.\n */\nexport interface UserSummary {\n text: string\n lastUpdated: string\n messageCount: number\n version: number\n}\n","/**\n * Basion Agent SDK - Agent\n * Port of Python basion_agent/agent.py\n */\n\nimport { randomUUID } from 'crypto'\nimport { AsyncLocalStorage } from 'node:async_hooks'\n\nimport type { BasionAgentApp } from './app.js'\nimport type { AttachmentClient } from './attachment-client.js'\nimport { logBraintrustError,withBraintrustSpan } from './braintrust.js'\nimport type { ConversationClient } from './conversation-client.js'\nimport { ConfigurationError } from './exceptions.js'\nimport type { GatewayClient } from './gateway-client.js'\nimport { type HeartbeatFailureCallback,HeartbeatManager } from './heartbeat.js'\nimport type { MemoryClient } from './memory-client.js'\nimport type { MemoryV2Client } from './memory-v2-client.js'\nimport { Message } from './message.js'\nimport type { RenderClient } from './render-client.js'\nimport { captureAgentError, setConversationId,withAgentSpan } from './sentry.js'\nimport { Streamer } from './streamer.js'\nimport { Tools } from './tools/container.js'\nimport type {\n AgentRegistrationData,\n KafkaMessage,\n MessageHandler,\n SenderFilterOptions,\n StreamerOptions,\n} from './types.js'\n\n/**\n * AsyncLocalStorage context set automatically by the agent message handler.\n * VercelAIMessageStore reads from this to embed cross-system IDs without\n * requiring agent developers to pass them explicitly.\n *\n * Stored on globalThis so that the same instance is shared across all bundle\n * chunks (tsup can include this module in multiple output files; globalThis\n * ensures they all reference the same AsyncLocalStorage object).\n *\n * @internal — consumed by VercelAIMessageStore; not part of the public API.\n */\nconst _msgCtxKey = Symbol.for('basion-ai-sdk.messageContext')\nif (!(globalThis as Record<symbol, unknown>)[_msgCtxKey]) {\n (globalThis as Record<symbol, unknown>)[_msgCtxKey] = new AsyncLocalStorage<{\n userMessageId?: string\n responseMessageId?: string\n }>()\n}\nexport const _messageContext = (globalThis as Record<symbol, unknown>)[_msgCtxKey] as AsyncLocalStorage<{\n userMessageId?: string\n responseMessageId?: string\n}>\n\n/**\n * Filter for matching message senders.\n */\nexport class SenderFilter {\n private readonly _include: Set<string> = new Set()\n private readonly _exclude: Set<string> = new Set()\n readonly matchAll: boolean\n\n constructor(senders?: string[]) {\n if (!senders || senders.length === 0) {\n this.matchAll = true\n } else {\n this.matchAll = false\n for (const sender of senders) {\n if (sender.startsWith('~')) {\n this._exclude.add(sender.slice(1))\n } else {\n this._include.add(sender)\n }\n }\n }\n }\n\n /**\n * Get included senders as an array (for compatibility with Python tests).\n */\n get include(): string[] {\n return Array.from(this._include)\n }\n\n /**\n * Get excluded senders as an array (for compatibility with Python tests).\n */\n get exclude(): string[] {\n return Array.from(this._exclude)\n }\n\n /**\n * Check if sender matches this filter.\n */\n matches(sender: string): boolean {\n // If excluded, reject\n if (this._exclude.has(sender)) {\n return false\n }\n // If match all or explicitly included\n if (this.matchAll || this._include.size === 0) {\n return true\n }\n return this._include.has(sender)\n }\n}\n\n// Message handlers stored as tuples: [SenderFilter, MessageHandler]\ntype RegisteredHandler = [SenderFilter, MessageHandler]\n\n/**\n * Core agent class that handles messaging via the Agent Gateway.\n */\nexport class Agent {\n readonly name: string\n readonly agentData: AgentRegistrationData\n sendErrorResponses: boolean = true\n errorMessageTemplate: string\n\n private readonly gatewayClient: GatewayClient\n private readonly conversationClient?: ConversationClient\n private readonly memoryClient?: MemoryClient\n private readonly memoryV2Client?: MemoryV2Client\n private readonly attachmentClient?: AttachmentClient\n private readonly renderClient?: RenderClient\n private readonly heartbeatInterval: number\n private readonly heartbeatFailureThreshold: number\n private readonly onHeartbeatFailure?: HeartbeatFailureCallback\n\n /** Set by BasionAgentApp.registerMe() to enable agent.call() */\n _app?: BasionAgentApp\n\n private messageHandlers: RegisteredHandler[] = []\n private heartbeatManager?: HeartbeatManager\n private initialized: boolean = false\n private running: boolean = false\n private _tools?: Tools\n\n /** AbortControllers for cancellable conversations, keyed by conversationId */\n private _cancelControllers = new Map<string, AbortController>()\n /** Tracks cancel requests that arrived before the handler created an AbortController */\n private _cancelledConversations = new Set<string>()\n\n constructor(options: {\n name: string\n gatewayClient: GatewayClient\n agentData: AgentRegistrationData\n heartbeatInterval?: number\n heartbeatFailureThreshold?: number\n onHeartbeatFailure?: HeartbeatFailureCallback\n conversationClient?: ConversationClient\n memoryClient?: MemoryClient\n memoryV2Client?: MemoryV2Client\n attachmentClient?: AttachmentClient\n renderClient?: RenderClient\n errorMessageTemplate?: string\n }) {\n this.name = options.name\n this.gatewayClient = options.gatewayClient\n this.agentData = options.agentData\n this.heartbeatInterval = options.heartbeatInterval ?? 60\n this.heartbeatFailureThreshold = options.heartbeatFailureThreshold ?? 3\n this.onHeartbeatFailure = options.onHeartbeatFailure\n this.conversationClient = options.conversationClient\n this.memoryClient = options.memoryClient\n this.memoryV2Client = options.memoryV2Client\n this.attachmentClient = options.attachmentClient\n this.renderClient = options.renderClient\n this.errorMessageTemplate = options.errorMessageTemplate ??\n 'I encountered an error while processing your message. Please try again or contact support if the issue persists.'\n }\n\n /**\n * Create an AbortSignal for a conversation's current operation.\n * Call this before starting work (e.g. streamText) and pass the\n * returned signal so the operation can be cancelled externally.\n *\n * If a cancel request arrived before this call, the returned signal\n * will already be in the aborted state.\n */\n createAbortSignal(conversationId: string): AbortSignal {\n // Replace any existing controller (defensive cleanup)\n const existing = this._cancelControllers.get(conversationId)\n if (existing) {\n existing.abort()\n }\n\n const controller = new AbortController()\n this._cancelControllers.set(conversationId, controller)\n\n // Handle race: cancel arrived before handler created the controller\n if (this._cancelledConversations.has(conversationId)) {\n this._cancelledConversations.delete(conversationId)\n controller.abort()\n }\n\n return controller.signal\n }\n\n /**\n * Cancel the running operation for a conversation.\n * Called by the message interceptor when a cancel message arrives.\n */\n cancelConversation(conversationId: string): void {\n const controller = this._cancelControllers.get(conversationId)\n if (controller) {\n controller.abort()\n this._cancelControllers.delete(conversationId)\n } else {\n // Cancel arrived before handler started — mark for pre-abort\n this._cancelledConversations.add(conversationId)\n }\n }\n\n /**\n * Clean up the abort controller after the handler completes.\n */\n clearAbortController(conversationId: string): void {\n this._cancelControllers.delete(conversationId)\n this._cancelledConversations.delete(conversationId)\n }\n\n /**\n * Access to tools (knowledge graph, etc.).\n *\n * Example:\n * const kg = agent.tools.knowledgeGraph\n * const diseases = await kg.searchDiseases({ name: \"Huntington\" })\n */\n get tools(): Tools {\n if (!this._tools) {\n this._tools = new Tools(this.gatewayClient)\n }\n return this._tools\n }\n\n /**\n * Register a message handler.\n * \n * Usage:\n * agent.onMessage(async (message, sender) => { ... })\n * agent.onMessage(handler, { senders: ['user'] })\n */\n onMessage(handler: MessageHandler): void\n onMessage(handler: MessageHandler, options: SenderFilterOptions): void\n onMessage(\n handler: MessageHandler,\n options?: SenderFilterOptions\n ): void {\n const filter = new SenderFilter(options?.senders)\n this.messageHandlers.push([filter, handler])\n }\n\n /**\n * Initialize agent after gateway connection is established.\n * Can be called multiple times (e.g., after reconnection) to re-register handlers.\n */\n initializeWithGateway(): void {\n // Register message handler with gateway\n // This is idempotent - calling it again just updates the handler\n const topic = `${this.name}.inbox`\n this.gatewayClient.registerHandler(topic, (msg) => {\n this.handleKafkaMessage(msg)\n })\n\n // Start heartbeat if not already created\n if (!this.heartbeatManager) {\n this.heartbeatManager = new HeartbeatManager(\n this.gatewayClient,\n this.name,\n this.heartbeatInterval,\n {\n failureThreshold: this.heartbeatFailureThreshold,\n onFailureThresholdExceeded: this.handleHeartbeatFailure.bind(this),\n }\n )\n }\n\n // Start or resume heartbeat (in case it was stopped during disconnect)\n if (!this.heartbeatManager.isRunning()) {\n this.heartbeatManager.start()\n } else if (this.heartbeatManager.isPaused()) {\n this.heartbeatManager.resume()\n }\n\n this.initialized = true\n }\n\n /**\n * Handle heartbeat failure threshold exceeded.\n * This can indicate a stale connection that needs reconnection.\n */\n private handleHeartbeatFailure(consecutiveFailures: number, lastError?: Error): void {\n console.warn(\n `Agent '${this.name}' heartbeat failure threshold exceeded (${consecutiveFailures} failures)`\n )\n\n // Call custom callback if provided\n if (this.onHeartbeatFailure) {\n this.onHeartbeatFailure(consecutiveFailures, lastError)\n }\n\n // If gateway is not connected, try to trigger reconnection\n if (!this.gatewayClient.isConnected() && !this.gatewayClient.isReconnectingNow()) {\n console.warn('Gateway not connected, attempting force reconnect...')\n this.gatewayClient.forceReconnect().catch((err) => {\n console.error('Force reconnect from heartbeat failure failed:', err)\n })\n }\n }\n\n /**\n * Pause heartbeat sending (e.g., during reconnection).\n */\n pauseHeartbeat(): void {\n this.heartbeatManager?.pause()\n }\n\n /**\n * Resume heartbeat sending.\n */\n resumeHeartbeat(): void {\n this.heartbeatManager?.resume()\n }\n\n /**\n * Get sender from message headers.\n */\n private getSender(headers: Record<string, string>): string {\n return headers.from_ ?? headers.from ?? 'user'\n }\n\n /**\n * Find first handler that matches the sender.\n */\n private findMatchingHandler(sender: string): MessageHandler | undefined {\n for (const [filter, handler] of this.messageHandlers) {\n if (filter.matches(sender)) {\n return handler\n }\n }\n return undefined\n }\n\n /**\n * Handle incoming Kafka message.\n */\n private handleKafkaMessage(kafkaMsg: KafkaMessage): void {\n // Run handler asynchronously\n this.handleMessageAsync(kafkaMsg).catch((error) => {\n console.error('Error in message handler:', error)\n })\n }\n\n /**\n * Async message handling with error recovery.\n */\n private async handleMessageAsync(kafkaMsg: KafkaMessage): Promise<void> {\n let message: Message | undefined\n\n try {\n // Create message from Kafka data\n message = Message.fromKafkaMessage(\n kafkaMsg.body,\n kafkaMsg.headers,\n {\n conversationClient: this.conversationClient,\n memoryClient: this.memoryClient,\n memoryV2Client: this.memoryV2Client,\n attachmentClient: this.attachmentClient,\n agentName: this.name,\n gatewayClient: this.gatewayClient,\n }\n )\n\n // Find matching handler\n const sender = this.getSender(kafkaMsg.headers)\n const handler = this.findMatchingHandler(sender)\n\n if (!handler) {\n console.warn(`No handler matched for sender: ${sender}`)\n return\n }\n\n // Extract trace ID from Kafka headers for cross-agent Braintrust correlation\n const traceId = kafkaMsg.headers.traceId\n\n // Wrap handler execution in Sentry invoke_agent span\n // setConversationId must be called INSIDE the span so the conversation ID\n // is applied within the span's scope (see Sentry AI Agents docs)\n await withAgentSpan(\n `invoke_agent ${this.name}`,\n this.name,\n 'gen_ai.invoke_agent',\n async () => {\n setConversationId(message!.conversationId)\n\n // Also wrap in Braintrust span (nested under conversation trace)\n return withBraintrustSpan(\n `invoke_agent ${this.name}`,\n () => _messageContext.run(\n {\n userMessageId: message!.userMessageId,\n responseMessageId: message!.responseMessageId,\n },\n () => handler(message!, sender),\n ),\n {\n conversationId: message!.conversationId,\n agentName: this.name,\n traceId,\n type: 'llm',\n input: [{ role: 'user', content: message!.content }],\n metadata: {\n conversationId: message!.conversationId,\n agentName: this.name,\n sender,\n },\n },\n )\n },\n {\n 'gen_ai.operation.name': 'invoke_agent',\n 'gen_ai.conversation.id': message.conversationId,\n }\n )\n\n } catch (error) {\n console.error('Error handling message:', error)\n\n // Capture error in Sentry with agent context\n captureAgentError(error, this.name, {\n conversationId: message?.conversationId ?? 'unknown',\n })\n\n // Log error in Braintrust\n logBraintrustError(error, {\n conversationId: message?.conversationId ?? 'unknown',\n agentName: this.name,\n })\n\n // Send error response if we have a message\n if (message && this.sendErrorResponses) {\n await this.sendErrorResponse(message, error as Error)\n }\n }\n }\n\n /**\n * Send error response to user when handler fails.\n */\n private async sendErrorResponse(message: Message, _error: Error): Promise<void> {\n try {\n const streamer = this.streamer(message)\n streamer.stream(this.errorMessageTemplate)\n await streamer.finish()\n } catch (err) {\n console.error('Failed to send error response:', err)\n }\n }\n\n /**\n * Report an error with agent context.\n *\n * Logs to console.error and captures in Sentry (if configured) in one call.\n * Use this for errors that don't involve streaming a response (e.g. background tasks).\n * For errors during streaming, use `streamer.streamError()` instead.\n *\n * @param error - The error to report\n * @param context - Optional additional context tags\n *\n * @example\n * ```typescript\n * try {\n * await riskyOperation()\n * } catch (e) {\n * agent.reportError(e)\n * }\n * ```\n */\n reportError(error: unknown, context?: Record<string, string>): void {\n console.error(`[${this.name}] Error:`, error)\n captureAgentError(error, this.name, context)\n }\n\n /**\n * Create a new Streamer for streaming responses.\n */\n streamer(message: Message, options: StreamerOptions = {}): Streamer {\n return new Streamer({\n agentName: this.name,\n originalMessage: message,\n gatewayClient: this.gatewayClient,\n conversationClient: this.conversationClient,\n attachmentClient: this.attachmentClient,\n renderClient: this.renderClient,\n awaiting: options.awaiting ?? false,\n })\n }\n\n /**\n * Call another agent and wait for its response.\n *\n * Sends a message to the target agent with isCall/callId headers.\n * The response is intercepted by the app's call interception logic\n * and returned as a string.\n *\n * @param agentName - Target agent name to call\n * @param conversationId - The conversation ID for context\n * @param content - Content to send to the target agent\n * @param timeout - Timeout in milliseconds (default: 30000)\n * @returns The target agent's response as a Message (with content, attachments, metadata, etc.)\n *\n * @example\n * ```typescript\n * const response = await agent.call('medical-agent', message.conversationId, 'Is ibuprofen safe?')\n * streamer.stream(`Medical agent says: ${response.content}`)\n * // Access attachments:\n * if (response.hasAttachments()) {\n * const bytes = await response.getAttachmentBytes()\n * }\n * ```\n */\n async call(\n agentName: string,\n conversationId: string,\n content: string,\n timeout: number = 30000,\n ): Promise<Message> {\n if (!this._app) {\n throw new Error(\n 'agent.call() requires a BasionAgentApp. ' +\n 'Ensure the agent was created via app.registerMe().'\n )\n }\n\n return withAgentSpan(\n `handoff from ${this.name} to ${agentName}`,\n this.name,\n 'gen_ai.handoff',\n async () => {\n const callId = randomUUID()\n\n // Create a promise that will be resolved when the response arrives\n let resolvePromise!: (value: Message) => void\n let rejectPromise!: (reason: Error) => void\n const resultPromise = new Promise<Message>((resolve, reject) => {\n resolvePromise = resolve\n rejectPromise = reject\n })\n\n // Register the pending call with the app\n this._app!._pendingCalls.set(callId, {\n resolve: resolvePromise,\n reject: rejectPromise,\n callerName: this.name,\n })\n\n try {\n // Produce the call message\n const headers: Record<string, string> = {\n conversationId,\n from_: this.name,\n to_: agentName,\n nextRoute: agentName,\n isCall: 'true',\n callId,\n }\n\n const body: Record<string, unknown> = {\n content,\n done: true,\n persist: false,\n }\n\n this.gatewayClient.produce('router.inbox', conversationId, headers, body)\n\n // Wait for response with timeout\n const timeoutPromise = new Promise<never>((_, reject) => {\n setTimeout(() => reject(new Error('TIMEOUT')), timeout)\n })\n\n const result = await Promise.race([resultPromise, timeoutPromise])\n return result\n } catch (error) {\n // Clean up on error/timeout\n this._app!._pendingCalls.delete(callId)\n this._app!._callContent.delete(callId)\n\n if (error instanceof Error && error.message === 'TIMEOUT') {\n throw new Error(`agent.call() to '${agentName}' timed out after ${timeout}ms`)\n }\n throw error\n }\n }\n ) as Promise<Message>\n }\n\n /**\n * Start a new conversation and send the first message as an agent.\n *\n * Creates a conversation in the conversation store and returns a Streamer\n * for sending the initial message. The message flows through the normal\n * Kafka pipeline (router → user.inbox → provider → Centrifugo).\n *\n * The user's reply will enter the normal handler pipeline via onMessage().\n *\n * @param options - Configuration for the new conversation\n * @returns Tuple of [conversationId, streamer]\n *\n * @example\n * ```typescript\n * const [convId, streamer] = await agent.startConversation({\n * userId: 'user-uuid',\n * title: 'Weekly Check-in',\n * })\n * streamer.stream('Hi! How are you feeling today?')\n * await streamer.finish()\n * ```\n */\n async startConversation(options: {\n userId: string\n title?: string\n awaiting?: boolean\n responseSchema?: Record<string, unknown>\n messageMetadata?: Record<string, unknown>\n metadata?: Record<string, unknown>\n }): Promise<[string, Streamer]> {\n if (!this.conversationClient) {\n throw new ConfigurationError(\n 'ConversationClient is required for startConversation. ' +\n 'Ensure the agent is created via BasionAgentApp.'\n )\n }\n\n // Create conversation with current_route and locked_by set atomically\n const conversation = await this.conversationClient.createConversation({\n userId: options.userId,\n title: options.title ?? 'Agent-initiated conversation',\n agentIdentifier: this.name,\n currentRoute: this.name,\n lockedBy: this.name,\n isNew: true,\n metadata: options.metadata,\n })\n\n const conversationId = String(conversation.id)\n\n // Create streamer with direct conversationId/userId (no originalMessage)\n const streamer = new Streamer({\n agentName: this.name,\n conversationId,\n userId: options.userId,\n gatewayClient: this.gatewayClient,\n conversationClient: this.conversationClient,\n attachmentClient: this.attachmentClient,\n renderClient: this.renderClient,\n awaiting: options.awaiting ?? false,\n })\n\n if (options.responseSchema) {\n streamer.setResponseSchema(options.responseSchema)\n }\n if (options.messageMetadata) {\n streamer.setMessageMetadata(options.messageMetadata)\n }\n\n return [conversationId, streamer]\n }\n\n /**\n * Mark agent as ready to consume messages.\n */\n startConsuming(): void {\n if (this.messageHandlers.length === 0) {\n throw new ConfigurationError('No message handlers registered')\n }\n\n if (!this.initialized) {\n throw new ConfigurationError('Agent not initialized with gateway')\n }\n\n this.running = true\n console.log(`Agent ${this.name} is now consuming messages`)\n }\n\n /**\n * Shutdown agent gracefully.\n */\n shutdown(): void {\n this.running = false\n\n if (this.heartbeatManager) {\n this.heartbeatManager.stop()\n }\n\n console.log(`Agent ${this.name} shutdown`)\n }\n\n /**\n * Check if agent is running.\n */\n isRunning(): boolean {\n return this.running\n }\n}\n","/**\n * Basion Agent SDK - Agent State Client\n * Port of Python basion_agent/agent_state_client.py\n */\n\nimport { APIException } from './exceptions.js'\n\nexport interface AgentStateResponse {\n conversationId: string\n namespace: string\n state: Record<string, unknown>\n createdAt?: string\n updatedAt?: string\n}\n\n/**\n * Async HTTP client for agent-state API.\n * Used for storing framework-specific state (e.g., Pydantic AI messages).\n */\nexport class AgentStateClient {\n private readonly baseUrl: string\n\n constructor(baseUrl: string) {\n this.baseUrl = baseUrl.replace(/\\/$/, '')\n }\n\n /**\n * Store or update agent state.\n */\n async put(\n conversationId: string,\n namespace: string,\n state: Record<string, unknown>\n ): Promise<{ conversationId: string; namespace: string; created: boolean }> {\n const url = `${this.baseUrl}/agent-state/${conversationId}`\n const payload = {\n namespace,\n state,\n }\n\n try {\n const response = await fetch(url, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to store agent state: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as { conversationId: string; namespace: string; created: boolean }\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to store agent state: ${error}`)\n }\n }\n\n /**\n * Get agent state.\n */\n async get(\n conversationId: string,\n namespace: string\n ): Promise<AgentStateResponse | null> {\n const url = new URL(`${this.baseUrl}/agent-state/${conversationId}`)\n url.searchParams.set('namespace', namespace)\n\n try {\n const response = await fetch(url.toString())\n\n if (response.status === 404) {\n return null\n }\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to get agent state: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as AgentStateResponse\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to get agent state: ${error}`)\n }\n }\n\n /**\n * Delete agent state.\n */\n async delete(\n conversationId: string,\n namespace?: string\n ): Promise<{ conversationId: string; namespace?: string; deleted: number }> {\n const url = new URL(`${this.baseUrl}/agent-state/${conversationId}`)\n if (namespace) {\n url.searchParams.set('namespace', namespace)\n }\n\n try {\n const response = await fetch(url.toString(), { method: 'DELETE' })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to delete agent state: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as { conversationId: string; namespace?: string; deleted: number }\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to delete agent state: ${error}`)\n }\n }\n\n // ── Vercel AI Messages (row-per-message store) ──────────────────────\n\n /**\n * Append Vercel AI messages to a conversation.\n */\n async appendVercelMessages(\n conversationId: string,\n messages: Record<string, unknown>[],\n ): Promise<{ conversation_id: string; appended: number; total: number }> {\n const url = `${this.baseUrl}/vercel-ai-messages/${conversationId}/append`\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ messages }),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to append vercel messages: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as { conversation_id: string; appended: number; total: number }\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to append vercel messages: ${error}`)\n }\n }\n\n /**\n * Get Vercel AI messages for a conversation.\n * Returns AgentStateResponse-compatible shape or null if not found.\n */\n async getVercelMessages(\n conversationId: string,\n last?: number,\n ): Promise<AgentStateResponse | null> {\n const url = new URL(`${this.baseUrl}/vercel-ai-messages/${conversationId}`)\n if (last !== undefined) {\n url.searchParams.set('last', String(last))\n }\n\n try {\n const response = await fetch(url.toString())\n\n if (response.status === 404) {\n return null\n }\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to get vercel messages: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as AgentStateResponse\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to get vercel messages: ${error}`)\n }\n }\n\n /**\n * Prune Vercel AI messages before a given sequence number.\n * Pruned messages are excluded from load but retained for audit.\n */\n async pruneVercelMessages(\n conversationId: string,\n beforeSequence: number,\n ): Promise<{ conversation_id: string; pruned: number }> {\n const url = `${this.baseUrl}/vercel-ai-messages/${conversationId}/prune`\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ before_sequence: beforeSequence }),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to prune vercel messages: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as { conversation_id: string; pruned: number }\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to prune vercel messages: ${error}`)\n }\n }\n\n /**\n * Delete all Vercel AI messages for a conversation.\n */\n async deleteVercelMessages(\n conversationId: string,\n ): Promise<{ conversation_id: string; deleted: number }> {\n const url = `${this.baseUrl}/vercel-ai-messages/${conversationId}`\n\n try {\n const response = await fetch(url, { method: 'DELETE' })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to delete vercel messages: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as { conversation_id: string; deleted: number }\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to delete vercel messages: ${error}`)\n }\n }\n\n // ── Filesystem Snapshots (row-per-snapshot store) ─────────────────────\n\n /**\n * Append filesystem snapshots to a conversation.\n */\n async appendFilesystemSnapshots(\n conversationId: string,\n snapshots: Record<string, unknown>[],\n ): Promise<{ conversation_id: string; appended: number; total: number }> {\n const url = `${this.baseUrl}/filesystem-snapshots/${conversationId}/append`\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ snapshots }),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to append filesystem snapshots: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as { conversation_id: string; appended: number; total: number }\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to append filesystem snapshots: ${error}`)\n }\n }\n\n /**\n * Get filesystem snapshots for a conversation.\n * Returns AgentStateResponse-compatible shape or null if not found.\n */\n async getFilesystemSnapshots(\n conversationId: string,\n ): Promise<AgentStateResponse | null> {\n const url = `${this.baseUrl}/filesystem-snapshots/${conversationId}`\n\n try {\n const response = await fetch(url.toString())\n\n if (response.status === 404) {\n return null\n }\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to get filesystem snapshots: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as AgentStateResponse\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to get filesystem snapshots: ${error}`)\n }\n }\n}\n","/**\n * Basion Agent SDK - Vercel AI SDK Extension\n *\n * VercelAIMessageStore for persistent message history with Vercel AI SDK.\n *\n * @packageDocumentation\n */\n\nimport type { BasionAgentApp } from '../app.js'\nimport { _messageContext } from '../agent.js'\nimport { AgentStateClient } from '../agent-state-client.js'\n\n// Type definitions compatible with Vercel AI SDK's CoreMessage\ninterface CoreMessage {\n role: 'system' | 'user' | 'assistant' | 'tool'\n content: string | Array<{ type: string;[key: string]: unknown }>\n [key: string]: unknown\n}\n\n/**\n * Persistent message store for Vercel AI SDK.\n *\n * Stores and retrieves AI SDK message history using the agent-state API.\n * Cross-system message IDs (`_messageId`) are embedded automatically — no\n * manual ID passing required.\n *\n * @example\n * ```typescript\n * import { BasionAgentApp } from 'basion-ai-sdk'\n * import { VercelAIMessageStore } from 'basion-ai-sdk/extensions/vercel-ai'\n * import { streamText } from 'ai'\n * import { anthropic } from '@ai-sdk/anthropic'\n *\n * const app = new BasionAgentApp({ gatewayUrl: 'agent-gateway:8080', apiKey: 'your-key' })\n * const store = new VercelAIMessageStore(app)\n * const agent = await app.registerMe({ ... })\n *\n * agent.onMessage(async (message) => {\n * const history = await store.load(message.conversationId)\n *\n * const messages = [...history, { role: 'user', content: message.content }]\n *\n * const result = await streamText({ model: anthropic('claude-sonnet-4-6'), messages })\n *\n * // _messageId is embedded automatically — no IDs to pass\n * await store.append(message.conversationId, [\n * { role: 'user', content: message.content },\n * ...result.response.messages,\n * ])\n * })\n *\n * app.run()\n * ```\n */\nexport class VercelAIMessageStore {\n private static readonly NAMESPACE = 'vercel_ai'\n private readonly client: AgentStateClient\n\n constructor(app: BasionAgentApp) {\n this.client = new AgentStateClient(app.gatewayClient.conversationStoreUrl)\n }\n\n /**\n * Load message history for a conversation.\n *\n * @param conversationId - The conversation ID to load history for\n * @returns Array of CoreMessage objects (empty array if no history)\n */\n async load(conversationId: string): Promise<CoreMessage[]> {\n const result = await this.client.getVercelMessages(conversationId)\n\n if (!result) {\n return []\n }\n\n const state = result.state ?? {}\n const messages = state.messages as unknown[]\n\n if (!Array.isArray(messages)) {\n return []\n }\n\n return messages.filter(this.isValidMessage) as CoreMessage[]\n }\n\n /**\n * Append new messages to existing history.\n *\n * Cross-system `_messageId` fields are embedded automatically from the\n * current message handler context — no IDs need to be passed manually.\n *\n * @param conversationId - The conversation ID\n * @param newMessages - New messages to append (user turn + response messages)\n */\n async append(conversationId: string, newMessages: CoreMessage[]): Promise<void> {\n const ctx = _messageContext.getStore()\n const tagged = ctx ? this.embedMessageIds(newMessages, ctx) : newMessages\n await this.client.appendVercelMessages(\n conversationId,\n tagged.map(this.serializeMessage),\n )\n }\n\n /**\n * Clear message history for a conversation.\n *\n * @param conversationId - The conversation ID to clear history for\n */\n async clear(conversationId: string): Promise<void> {\n await this.client.deleteVercelMessages(conversationId)\n }\n\n /**\n * Prune messages before a given sequence number.\n *\n * Called after server-side compaction fires. Pruned messages are excluded\n * from `load()` but retained in the database for audit.\n *\n * @param conversationId - The conversation ID\n * @param beforeSequence - Prune all messages with sequence < this value\n * @returns Number of messages pruned\n */\n async pruneBeforeSequence(conversationId: string, beforeSequence: number): Promise<number> {\n const result = await this.client.pruneVercelMessages(conversationId, beforeSequence)\n return result.pruned\n }\n\n /**\n * Get the last N messages from history.\n *\n * @param conversationId - The conversation ID\n * @param count - Number of messages to retrieve\n */\n async getLastMessages(conversationId: string, count: number): Promise<CoreMessage[]> {\n const result = await this.client.getVercelMessages(conversationId, count)\n\n if (!result) {\n return []\n }\n\n const state = result.state ?? {}\n const messages = state.messages as unknown[]\n\n if (!Array.isArray(messages)) {\n return []\n }\n\n return messages.filter(this.isValidMessage) as CoreMessage[]\n }\n\n private embedMessageIds(\n messages: CoreMessage[],\n ids: { userMessageId?: string; responseMessageId?: string },\n ): CoreMessage[] {\n const result = [...messages]\n\n if (ids.userMessageId) {\n const userIdx = result.findIndex((m) => m.role === 'user')\n if (userIdx !== -1) {\n result[userIdx] = { ...result[userIdx], _messageId: ids.userMessageId }\n }\n }\n\n if (ids.responseMessageId) {\n for (let i = result.length - 1; i >= 0; i--) {\n const m = result[i]\n if (\n m.role === 'assistant' &&\n (typeof m.content === 'string'\n ? m.content.length > 0\n : (m.content as Array<{ type: string }>).some((c) => c.type === 'text'))\n ) {\n result[i] = { ...result[i], _messageId: ids.responseMessageId }\n break\n }\n }\n }\n\n return result\n }\n\n private isValidMessage(msg: unknown): msg is CoreMessage {\n if (typeof msg !== 'object' || msg === null) return false\n const m = msg as Record<string, unknown>\n return (\n typeof m.role === 'string' &&\n ['system', 'user', 'assistant', 'tool'].includes(m.role) &&\n (typeof m.content === 'string' || Array.isArray(m.content))\n )\n }\n\n private serializeMessage(msg: CoreMessage): Record<string, unknown> {\n return { ...msg }\n }\n}\n\nexport type { CoreMessage }\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/exceptions.ts","../../src/types.ts","../../src/agent.ts","../../src/agent-state-client.ts","../../src/extensions/vercel-ai.ts"],"names":[],"mappings":"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","file":"vercel-ai.js","sourcesContent":["/**\n * Basion Agent SDK - Custom Errors\n * Port of Python basion_agent/exceptions.py\n */\n\n/**\n * Base error class for Basion Agent SDK\n */\nexport class BasionAgentError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'BasionAgentError'\n Error.captureStackTrace?.(this, this.constructor)\n }\n}\n\n/**\n * Error during agent registration with AI Inventory\n */\nexport class RegistrationError extends BasionAgentError {\n constructor(message: string) {\n super(message)\n this.name = 'RegistrationError'\n }\n}\n\n/**\n * Error during Kafka operations (produce/consume)\n */\nexport class KafkaError extends BasionAgentError {\n constructor(message: string) {\n super(message)\n this.name = 'KafkaError'\n }\n}\n\n/**\n * Error in SDK configuration\n */\nexport class ConfigurationError extends BasionAgentError {\n constructor(message: string) {\n super(message)\n this.name = 'ConfigurationError'\n }\n}\n\n/**\n * Error from API calls to backend services\n */\nexport class APIException extends BasionAgentError {\n statusCode?: number\n\n constructor(message: string, statusCode?: number) {\n super(message)\n this.name = 'APIException'\n this.statusCode = statusCode\n }\n}\n\n/**\n * Error during heartbeat operations\n */\nexport class HeartbeatError extends BasionAgentError {\n constructor(message: string) {\n super(message)\n this.name = 'HeartbeatError'\n }\n}\n\n/**\n * Error during gRPC connection/communication\n */\nexport class GatewayError extends BasionAgentError {\n constructor(message: string) {\n super(message)\n this.name = 'GatewayError'\n }\n}\n\n/**\n * Error during reconnection attempts\n */\nexport class ReconnectionError extends BasionAgentError {\n /** Number of attempts made before giving up */\n attempts: number\n /** The last error that occurred during reconnection */\n lastError?: Error\n\n constructor(message: string, attempts: number, lastError?: Error) {\n super(message)\n this.name = 'ReconnectionError'\n this.attempts = attempts\n this.lastError = lastError\n }\n}\n","/**\n * Basion Agent SDK - Shared Types\n * Port of Python basion_agent types\n */\n\nimport { z } from 'zod'\n\n// ============================================================================\n// Kafka Message Types\n// ============================================================================\n\nexport interface KafkaMessage {\n topic: string\n partition: number\n offset: number\n key: string\n headers: Record<string, string>\n body: Record<string, unknown>\n timestamp: number\n}\n\nexport interface KafkaHeaders {\n conversationId?: string\n userId?: string\n from_?: string\n to_?: string\n messageMetadata?: string\n messageSchema?: string\n /** @deprecated Use attachments (plural) instead. Kept for backward compatibility. */\n attachment?: string\n /** JSON-stringified array of AttachmentInfo objects */\n attachments?: string\n /** UUID of the user message in conversation-store (for cross-system mapping) */\n userMessageId?: string\n /** Pre-generated UUID for the assistant response in conversation-store (for cross-system mapping) */\n assistantMessageId?: string\n [key: string]: string | undefined\n}\n\n// ============================================================================\n// Agent Registration Types\n// ============================================================================\n\nexport interface AgentRegistrationData {\n id: string\n name: string\n about: string\n document: string\n representationName?: string\n baseUrl?: string\n metadata?: Record<string, unknown>\n relatedPages?: RelatedPage[]\n}\n\nexport interface RelatedPage {\n name: string\n endpoint: string\n}\n\nexport interface Prompt {\n label: string\n prompt: string\n}\n\nexport interface RegisterOptions {\n name: string\n about: string\n document: string\n representationName?: string\n baseUrl?: string\n metadata?: Record<string, unknown>\n relatedPages?: RelatedPage[]\n categoryNames?: string[]\n prompts?: Prompt[]\n detailedDescription?: string\n version?: string\n lifecycleStage?: 'dev' | 'test' | 'private-preview' | 'public-preview' | 'public'\n welcomeMessage?: string\n waitlist?: boolean\n forceUpdate?: boolean\n}\n\n// ============================================================================\n// Message Types\n// ============================================================================\n\n// Forward reference - actual Message class is imported where needed\n// Using 'any' here to avoid circular dependency, but consumers should use Message type\nexport interface MessageHandler {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (message: any, sender: string): void | Promise<void>\n}\n\nexport interface SenderFilterOptions {\n senders?: string[]\n}\n\n// ============================================================================\n// Streamer Types\n// ============================================================================\n\nexport interface StreamOptions {\n persist?: boolean\n eventType?: string\n}\n\nexport interface StreamerOptions {\n awaiting?: boolean\n}\n\n// ============================================================================\n// Attachment Types\n// ============================================================================\n\nexport const AttachmentInfoSchema = z.object({\n url: z.string(),\n filename: z.string(),\n contentType: z.string(),\n size: z.number().optional(),\n fileType: z.string().optional(),\n objectKey: z.string().optional(),\n})\n\nexport type AttachmentInfo = z.infer<typeof AttachmentInfoSchema>\n\nexport function isImageAttachment(attachment: AttachmentInfo): boolean {\n return attachment.contentType?.startsWith('image/') || false\n}\n\nexport function isPdfAttachment(attachment: AttachmentInfo): boolean {\n return attachment.contentType === 'application/pdf'\n}\n\n// ============================================================================\n// Conversation Message Types\n// ============================================================================\n\nexport interface ConversationMessageData {\n id: string\n conversationId: string\n role: 'user' | 'assistant' | 'system'\n content: string\n from?: string\n to?: string\n createdAt: string\n metadata?: Record<string, unknown>\n}\n\n// ============================================================================\n// Gateway Client Types\n// ============================================================================\n\nexport interface ProduceAck {\n topic: string\n partition: number\n offset: number\n correlationId?: string\n}\n\n// ============================================================================\n// Reconnection Types\n// ============================================================================\n\n/**\n * Configuration options for automatic reconnection behavior.\n */\nexport interface ReconnectionOptions {\n /** Maximum number of reconnection attempts before giving up. Default: 10 */\n maxRetries?: number\n /** Initial delay in milliseconds before first retry. Default: 1000 */\n initialDelayMs?: number\n /** Maximum delay in milliseconds between retries. Default: 30000 */\n maxDelayMs?: number\n /** Multiplier for exponential backoff. Default: 2 */\n backoffMultiplier?: number\n /** Whether to automatically reconnect on disconnect. Default: true */\n autoReconnect?: boolean\n}\n\n/**\n * Connection state for the gateway client.\n */\nexport type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting'\n\n/**\n * Listener callback for connection state changes.\n */\nexport interface ConnectionStateListener {\n (state: ConnectionState, error?: Error): void\n}\n\n/**\n * Default reconnection options.\n */\nexport const DEFAULT_RECONNECTION_OPTIONS: Required<ReconnectionOptions> = {\n maxRetries: 10,\n initialDelayMs: 1000,\n maxDelayMs: 30000,\n backoffMultiplier: 2,\n autoReconnect: true,\n}\n\n// ============================================================================\n// App Configuration Types\n// ============================================================================\n\nexport interface AppOptions {\n gatewayUrl: string\n apiKey: string\n heartbeatInterval?: number\n maxConcurrentTasks?: number\n errorMessageTemplate?: string\n secure?: boolean\n /** Reconnection options for the gateway connection */\n reconnection?: ReconnectionOptions\n /** Enable remote logging to Loki via the gateway (default: false) */\n enableRemoteLogging?: boolean\n /** Minimum log level for remote logging (default: 'info') */\n remoteLogLevel?: 'debug' | 'info' | 'warn' | 'error'\n /** Number of logs to batch before sending (default: 100) */\n remoteLogBatchSize?: number\n /** Maximum seconds between log flushes (default: 5.0) */\n remoteLogFlushInterval?: number\n /** Sentry configuration (optional — omit or leave dsn empty to disable) */\n sentry?: import('./sentry.js').SentryConfig\n /** Braintrust configuration (optional — omit or leave apiKey empty to disable) */\n braintrust?: import('./braintrust.js').BraintrustConfig\n}\n\n// ============================================================================\n// HTTP Response Types\n// ============================================================================\n\nexport interface ConversationData {\n id: string\n userId: string\n agentId?: string\n awaitingRoute?: string\n pendingResponseSchema?: Record<string, unknown>\n createdAt: string\n updatedAt: string\n metadata?: Record<string, unknown>\n}\n\nexport interface GetMessagesOptions {\n role?: string\n from?: string\n to?: string\n limit?: number\n offset?: number\n}\n\n// ============================================================================\n// Memory Types\n// ============================================================================\n\n/**\n * A message stored in ai-memory.\n * Matches the ai-memory API MessageStored schema.\n */\nexport interface MemoryMessage {\n id?: string\n role: 'user' | 'assistant' | 'system'\n content: string\n tenantId?: string\n userId?: string\n conversationId?: string\n metadata?: Record<string, unknown>\n timestamp?: string\n sequenceNumber?: number\n}\n\n/**\n * Options for memory search operations.\n */\nexport interface MemorySearchOptions {\n /** Maximum number of results (1-100, default: 10) */\n limit?: number\n /** Minimum similarity threshold (0-100, default: 70) */\n minSimilarity?: number\n /** Number of surrounding context messages to include (0-20, default: 0) */\n contextMessages?: number\n /** Tenant ID for multi-tenant filtering */\n tenantId?: string\n /** Tenant matching mode: 'exact' or 'hierarchy' */\n tenantMatch?: 'exact' | 'hierarchy'\n}\n\n/**\n * A memory search result with similarity score and optional context.\n */\nexport interface MemorySearchResult {\n message: MemoryMessage\n score: number\n context: MemoryMessage[]\n}\n\n/**\n * User conversation summary from ai-memory.\n */\nexport interface UserSummary {\n text: string\n lastUpdated: string\n messageCount: number\n version: number\n}\n","/**\n * Basion Agent SDK - Agent\n * Port of Python basion_agent/agent.py\n */\n\nimport { randomUUID } from 'crypto'\nimport { AsyncLocalStorage } from 'node:async_hooks'\n\nimport type { BasionAgentApp } from './app.js'\nimport type { AttachmentClient } from './attachment-client.js'\nimport { logBraintrustError,withBraintrustSpan } from './braintrust.js'\nimport type { ConversationClient } from './conversation-client.js'\nimport { sendDlqEnvelope } from './dlq.js'\nimport { ConfigurationError } from './exceptions.js'\nimport type { GatewayClient } from './gateway-client.js'\nimport { type HeartbeatFailureCallback,HeartbeatManager } from './heartbeat.js'\nimport type { MemoryClient } from './memory-client.js'\nimport type { MemoryV2Client } from './memory-v2-client.js'\nimport { Message } from './message.js'\nimport type { RenderClient } from './render-client.js'\nimport { captureAgentError, setConversationId,withAgentSpan } from './sentry.js'\nimport { Streamer } from './streamer.js'\nimport { Tools } from './tools/container.js'\nimport type {\n AgentRegistrationData,\n KafkaMessage,\n MessageHandler,\n SenderFilterOptions,\n StreamerOptions,\n} from './types.js'\n\n/**\n * AsyncLocalStorage context set automatically by the agent message handler.\n * VercelAIMessageStore reads from this to embed cross-system IDs without\n * requiring agent developers to pass them explicitly.\n *\n * Stored on globalThis so that the same instance is shared across all bundle\n * chunks (tsup can include this module in multiple output files; globalThis\n * ensures they all reference the same AsyncLocalStorage object).\n *\n * @internal — consumed by VercelAIMessageStore; not part of the public API.\n */\nconst _msgCtxKey = Symbol.for('basion-ai-sdk.messageContext')\nif (!(globalThis as Record<symbol, unknown>)[_msgCtxKey]) {\n (globalThis as Record<symbol, unknown>)[_msgCtxKey] = new AsyncLocalStorage<{\n userMessageId?: string\n responseMessageId?: string\n }>()\n}\nexport const _messageContext = (globalThis as Record<symbol, unknown>)[_msgCtxKey] as AsyncLocalStorage<{\n userMessageId?: string\n responseMessageId?: string\n}>\n\n/**\n * Filter for matching message senders.\n */\nexport class SenderFilter {\n private readonly _include: Set<string> = new Set()\n private readonly _exclude: Set<string> = new Set()\n readonly matchAll: boolean\n\n constructor(senders?: string[]) {\n if (!senders || senders.length === 0) {\n this.matchAll = true\n } else {\n this.matchAll = false\n for (const sender of senders) {\n if (sender.startsWith('~')) {\n this._exclude.add(sender.slice(1))\n } else {\n this._include.add(sender)\n }\n }\n }\n }\n\n /**\n * Get included senders as an array (for compatibility with Python tests).\n */\n get include(): string[] {\n return Array.from(this._include)\n }\n\n /**\n * Get excluded senders as an array (for compatibility with Python tests).\n */\n get exclude(): string[] {\n return Array.from(this._exclude)\n }\n\n /**\n * Check if sender matches this filter.\n */\n matches(sender: string): boolean {\n // If excluded, reject\n if (this._exclude.has(sender)) {\n return false\n }\n // If match all or explicitly included\n if (this.matchAll || this._include.size === 0) {\n return true\n }\n return this._include.has(sender)\n }\n}\n\n// Message handlers stored as tuples: [SenderFilter, MessageHandler]\ntype RegisteredHandler = [SenderFilter, MessageHandler]\n\n/**\n * Core agent class that handles messaging via the Agent Gateway.\n */\nexport class Agent {\n readonly name: string\n readonly agentData: AgentRegistrationData\n sendErrorResponses: boolean = true\n errorMessageTemplate: string\n\n private readonly gatewayClient: GatewayClient\n private readonly conversationClient?: ConversationClient\n private readonly memoryClient?: MemoryClient\n private readonly memoryV2Client?: MemoryV2Client\n private readonly attachmentClient?: AttachmentClient\n private readonly renderClient?: RenderClient\n private readonly heartbeatInterval: number\n private readonly heartbeatFailureThreshold: number\n private readonly onHeartbeatFailure?: HeartbeatFailureCallback\n\n /** Set by BasionAgentApp.registerMe() to enable agent.call() */\n _app?: BasionAgentApp\n\n private messageHandlers: RegisteredHandler[] = []\n private heartbeatManager?: HeartbeatManager\n private initialized: boolean = false\n private running: boolean = false\n private _tools?: Tools\n\n /**\n * Live AbortControllers for in-flight turns, keyed by conversationId.\n * `assistantMessageId` scopes the controller to a specific turn so a cancel\n * for a previous turn cannot abort the current one.\n */\n private _cancelControllers = new Map<\n string,\n { controller: AbortController; assistantMessageId?: string }\n >()\n /**\n * Pre-cancel intents that arrived before the handler registered its controller.\n * Keyed by `assistantMessageId` (never by conversationId alone) so a stray\n * cancel from an earlier turn cannot poison the next turn. Entries self-expire\n * after PENDING_CANCEL_TTL_MS as defense-in-depth.\n */\n private _pendingCancels = new Map<string, { expiresAt: number }>()\n private static readonly PENDING_CANCEL_TTL_MS = 5000\n\n constructor(options: {\n name: string\n gatewayClient: GatewayClient\n agentData: AgentRegistrationData\n heartbeatInterval?: number\n heartbeatFailureThreshold?: number\n onHeartbeatFailure?: HeartbeatFailureCallback\n conversationClient?: ConversationClient\n memoryClient?: MemoryClient\n memoryV2Client?: MemoryV2Client\n attachmentClient?: AttachmentClient\n renderClient?: RenderClient\n errorMessageTemplate?: string\n }) {\n this.name = options.name\n this.gatewayClient = options.gatewayClient\n this.agentData = options.agentData\n this.heartbeatInterval = options.heartbeatInterval ?? 60\n this.heartbeatFailureThreshold = options.heartbeatFailureThreshold ?? 3\n this.onHeartbeatFailure = options.onHeartbeatFailure\n this.conversationClient = options.conversationClient\n this.memoryClient = options.memoryClient\n this.memoryV2Client = options.memoryV2Client\n this.attachmentClient = options.attachmentClient\n this.renderClient = options.renderClient\n this.errorMessageTemplate = options.errorMessageTemplate ??\n 'I encountered an error while processing your message. Please try again or contact support if the issue persists.'\n }\n\n /**\n * Create an AbortSignal for a conversation's current operation.\n * Call this before starting work (e.g. streamText) and pass the\n * returned signal so the operation can be cancelled externally.\n *\n * Pass `assistantMessageId` (typically `message.responseMessageId`) so\n * a cancel message targeting THIS turn can be matched precisely; a\n * cancel for any other turn will be ignored.\n *\n * If a cancel request arrived before this call AND carried a matching\n * `assistantMessageId`, the returned signal will already be aborted.\n */\n createAbortSignal(conversationId: string, assistantMessageId?: string): AbortSignal {\n // Replace any existing controller (defensive cleanup)\n const existing = this._cancelControllers.get(conversationId)\n if (existing) {\n existing.controller.abort()\n }\n\n const controller = new AbortController()\n this._cancelControllers.set(conversationId, { controller, assistantMessageId })\n\n this.purgeExpiredPendingCancels()\n\n // A pre-cancel only pre-aborts the new signal if it names the same turn.\n // Without this scoping, a stale cancel from a prior turn leaks across turns.\n if (assistantMessageId && this._pendingCancels.has(assistantMessageId)) {\n this._pendingCancels.delete(assistantMessageId)\n controller.abort()\n }\n\n return controller.signal\n }\n\n /**\n * Cancel the running operation for a conversation.\n * Called by the message interceptor when a cancel message arrives.\n *\n * If `assistantMessageId` is provided and does not match the live\n * controller's, the cancel is dropped as stale (prevents a late cancel\n * from a previous turn from aborting the current one).\n */\n cancelConversation(conversationId: string, assistantMessageId?: string): void {\n const entry = this._cancelControllers.get(conversationId)\n\n if (entry) {\n // Both IDs present and mismatched → stale cancel for a prior turn. Drop it.\n if (\n assistantMessageId &&\n entry.assistantMessageId &&\n assistantMessageId !== entry.assistantMessageId\n ) {\n return\n }\n entry.controller.abort()\n this._cancelControllers.delete(conversationId)\n return\n }\n\n // No live controller. Only remember cancels that name a specific turn;\n // blind conversation-scoped intent is what caused bug #1 and is removed.\n if (assistantMessageId) {\n this._pendingCancels.set(assistantMessageId, {\n expiresAt: Date.now() + Agent.PENDING_CANCEL_TTL_MS,\n })\n this.purgeExpiredPendingCancels()\n }\n }\n\n /**\n * Clean up the abort controller after the handler completes.\n * Also purges any matching pre-cancel intent so a stale entry cannot\n * outlive the turn it was meant for.\n */\n clearAbortController(conversationId: string, assistantMessageId?: string): void {\n this._cancelControllers.delete(conversationId)\n if (assistantMessageId) {\n this._pendingCancels.delete(assistantMessageId)\n }\n }\n\n private purgeExpiredPendingCancels(): void {\n const now = Date.now()\n for (const [id, { expiresAt }] of this._pendingCancels) {\n if (expiresAt <= now) {\n this._pendingCancels.delete(id)\n }\n }\n }\n\n /**\n * Access to tools (knowledge graph, etc.).\n *\n * Example:\n * const kg = agent.tools.knowledgeGraph\n * const diseases = await kg.searchDiseases({ name: \"Huntington\" })\n */\n get tools(): Tools {\n if (!this._tools) {\n this._tools = new Tools(this.gatewayClient)\n }\n return this._tools\n }\n\n /**\n * Register a message handler.\n * \n * Usage:\n * agent.onMessage(async (message, sender) => { ... })\n * agent.onMessage(handler, { senders: ['user'] })\n */\n onMessage(handler: MessageHandler): void\n onMessage(handler: MessageHandler, options: SenderFilterOptions): void\n onMessage(\n handler: MessageHandler,\n options?: SenderFilterOptions\n ): void {\n const filter = new SenderFilter(options?.senders)\n this.messageHandlers.push([filter, handler])\n }\n\n /**\n * Initialize agent after gateway connection is established.\n * Can be called multiple times (e.g., after reconnection) to re-register handlers.\n */\n initializeWithGateway(): void {\n // Register message handler with gateway\n // This is idempotent - calling it again just updates the handler\n const topic = `${this.name}.inbox`\n this.gatewayClient.registerHandler(topic, (msg) => {\n this.handleKafkaMessage(msg)\n })\n\n // Start heartbeat if not already created\n if (!this.heartbeatManager) {\n this.heartbeatManager = new HeartbeatManager(\n this.gatewayClient,\n this.name,\n this.heartbeatInterval,\n {\n failureThreshold: this.heartbeatFailureThreshold,\n onFailureThresholdExceeded: this.handleHeartbeatFailure.bind(this),\n }\n )\n }\n\n // Start or resume heartbeat (in case it was stopped during disconnect)\n if (!this.heartbeatManager.isRunning()) {\n this.heartbeatManager.start()\n } else if (this.heartbeatManager.isPaused()) {\n this.heartbeatManager.resume()\n }\n\n this.initialized = true\n }\n\n /**\n * Handle heartbeat failure threshold exceeded.\n * This can indicate a stale connection that needs reconnection.\n */\n private handleHeartbeatFailure(consecutiveFailures: number, lastError?: Error): void {\n console.warn(\n `Agent '${this.name}' heartbeat failure threshold exceeded (${consecutiveFailures} failures)`\n )\n\n // Call custom callback if provided\n if (this.onHeartbeatFailure) {\n this.onHeartbeatFailure(consecutiveFailures, lastError)\n }\n\n // If gateway is not connected, try to trigger reconnection\n if (!this.gatewayClient.isConnected() && !this.gatewayClient.isReconnectingNow()) {\n console.warn('Gateway not connected, attempting force reconnect...')\n this.gatewayClient.forceReconnect().catch((err) => {\n console.error('Force reconnect from heartbeat failure failed:', err)\n })\n }\n }\n\n /**\n * Pause heartbeat sending (e.g., during reconnection).\n */\n pauseHeartbeat(): void {\n this.heartbeatManager?.pause()\n }\n\n /**\n * Resume heartbeat sending.\n */\n resumeHeartbeat(): void {\n this.heartbeatManager?.resume()\n }\n\n /**\n * Get sender from message headers.\n */\n private getSender(headers: Record<string, string>): string {\n return headers.from_ ?? headers.from ?? 'user'\n }\n\n /**\n * Find first handler that matches the sender.\n */\n private findMatchingHandler(sender: string): MessageHandler | undefined {\n for (const [filter, handler] of this.messageHandlers) {\n if (filter.matches(sender)) {\n return handler\n }\n }\n return undefined\n }\n\n /**\n * Handle incoming Kafka message.\n */\n private handleKafkaMessage(kafkaMsg: KafkaMessage): void {\n // Run handler asynchronously\n this.handleMessageAsync(kafkaMsg).catch((error) => {\n console.error('Error in message handler:', error)\n })\n }\n\n /**\n * Async message handling with error recovery.\n */\n private async handleMessageAsync(kafkaMsg: KafkaMessage): Promise<void> {\n let message: Message | undefined\n\n try {\n // Create message from Kafka data\n message = Message.fromKafkaMessage(\n kafkaMsg.body,\n kafkaMsg.headers,\n {\n conversationClient: this.conversationClient,\n memoryClient: this.memoryClient,\n memoryV2Client: this.memoryV2Client,\n attachmentClient: this.attachmentClient,\n agentName: this.name,\n gatewayClient: this.gatewayClient,\n }\n )\n\n // Find matching handler\n const sender = this.getSender(kafkaMsg.headers)\n const handler = this.findMatchingHandler(sender)\n\n if (!handler) {\n console.warn(`No handler matched for sender: ${sender}`)\n return\n }\n\n // Extract trace ID from Kafka headers for cross-agent Braintrust correlation\n const traceId = kafkaMsg.headers.traceId\n\n // Wrap handler execution in Sentry invoke_agent span\n // setConversationId must be called INSIDE the span so the conversation ID\n // is applied within the span's scope (see Sentry AI Agents docs)\n await withAgentSpan(\n `invoke_agent ${this.name}`,\n this.name,\n 'gen_ai.invoke_agent',\n async () => {\n setConversationId(message!.conversationId)\n\n // Also wrap in Braintrust span (nested under conversation trace)\n return withBraintrustSpan(\n `invoke_agent ${this.name}`,\n () => _messageContext.run(\n {\n userMessageId: message!.userMessageId,\n responseMessageId: message!.responseMessageId,\n },\n () => handler(message!, sender),\n ),\n {\n conversationId: message!.conversationId,\n agentName: this.name,\n traceId,\n type: 'llm',\n input: [{ role: 'user', content: message!.content }],\n metadata: {\n conversationId: message!.conversationId,\n agentName: this.name,\n sender,\n },\n },\n )\n },\n {\n 'gen_ai.operation.name': 'invoke_agent',\n 'gen_ai.conversation.id': message.conversationId,\n }\n )\n\n } catch (error) {\n console.error('Error handling message:', error)\n\n // Capture error in Sentry with agent context\n captureAgentError(error, this.name, {\n conversationId: message?.conversationId ?? 'unknown',\n })\n\n // Log error in Braintrust\n logBraintrustError(error, {\n conversationId: message?.conversationId ?? 'unknown',\n agentName: this.name,\n })\n\n // DLQ-capture: without this, a thrown handler is silent —\n // the gateway has already auto-committed the offset, so the\n // user message gets no reply and there's no record on the\n // platform side. The DLQ envelope flows through gatewayClient\n // to the dlq.events Kafka topic via the agent's existing\n // gRPC stream.\n try {\n sendDlqEnvelope(this.gatewayClient, {\n service: `agent-${this.name}`,\n source: { kind: 'kafka', topic: kafkaMsg.topic },\n stage: 'agent.handler_error',\n error,\n payloadHeaders: kafkaMsg.headers,\n payload: kafkaMsg.body,\n correlation: {\n conversationId: message?.conversationId,\n userMessageId: message?.userMessageId,\n assistantMessageId: kafkaMsg.headers?.assistantMessageId,\n },\n })\n } catch (dlqErr) {\n // sendDlqEnvelope is supposed to be fire-and-forget but\n // belt and suspenders — never let DLQ capture break the\n // already-failing error path.\n console.error('[basion-sdk] DLQ envelope send failed', dlqErr)\n }\n\n // Send error response if we have a message\n if (message && this.sendErrorResponses) {\n await this.sendErrorResponse(message, error as Error)\n }\n }\n }\n\n /**\n * Send error response to user when handler fails.\n */\n private async sendErrorResponse(message: Message, _error: Error): Promise<void> {\n try {\n const streamer = this.streamer(message)\n streamer.stream(this.errorMessageTemplate)\n await streamer.finish()\n } catch (err) {\n console.error('Failed to send error response:', err)\n }\n }\n\n /**\n * Report an error with agent context.\n *\n * Logs to console.error and captures in Sentry (if configured) in one call.\n * Use this for errors that don't involve streaming a response (e.g. background tasks).\n * For errors during streaming, use `streamer.streamError()` instead.\n *\n * @param error - The error to report\n * @param context - Optional additional context tags\n *\n * @example\n * ```typescript\n * try {\n * await riskyOperation()\n * } catch (e) {\n * agent.reportError(e)\n * }\n * ```\n */\n reportError(error: unknown, context?: Record<string, string>): void {\n console.error(`[${this.name}] Error:`, error)\n captureAgentError(error, this.name, context)\n }\n\n /**\n * Create a new Streamer for streaming responses.\n */\n streamer(message: Message, options: StreamerOptions = {}): Streamer {\n return new Streamer({\n agentName: this.name,\n originalMessage: message,\n gatewayClient: this.gatewayClient,\n conversationClient: this.conversationClient,\n attachmentClient: this.attachmentClient,\n renderClient: this.renderClient,\n awaiting: options.awaiting ?? false,\n })\n }\n\n /**\n * Call another agent and wait for its response.\n *\n * Sends a message to the target agent with isCall/callId headers.\n * The response is intercepted by the app's call interception logic\n * and returned as a string.\n *\n * @param agentName - Target agent name to call\n * @param conversationId - The conversation ID for context\n * @param content - Content to send to the target agent\n * @param timeout - Timeout in milliseconds (default: 30000)\n * @returns The target agent's response as a Message (with content, attachments, metadata, etc.)\n *\n * @example\n * ```typescript\n * const response = await agent.call('medical-agent', message.conversationId, 'Is ibuprofen safe?')\n * streamer.stream(`Medical agent says: ${response.content}`)\n * // Access attachments:\n * if (response.hasAttachments()) {\n * const bytes = await response.getAttachmentBytes()\n * }\n * ```\n */\n async call(\n agentName: string,\n conversationId: string,\n content: string,\n timeout: number = 30000,\n ): Promise<Message> {\n if (!this._app) {\n throw new Error(\n 'agent.call() requires a BasionAgentApp. ' +\n 'Ensure the agent was created via app.registerMe().'\n )\n }\n\n return withAgentSpan(\n `handoff from ${this.name} to ${agentName}`,\n this.name,\n 'gen_ai.handoff',\n async () => {\n const callId = randomUUID()\n\n // Create a promise that will be resolved when the response arrives\n let resolvePromise!: (value: Message) => void\n let rejectPromise!: (reason: Error) => void\n const resultPromise = new Promise<Message>((resolve, reject) => {\n resolvePromise = resolve\n rejectPromise = reject\n })\n\n // Register the pending call with the app\n this._app!._pendingCalls.set(callId, {\n resolve: resolvePromise,\n reject: rejectPromise,\n callerName: this.name,\n })\n\n try {\n // Produce the call message\n const headers: Record<string, string> = {\n conversationId,\n from_: this.name,\n to_: agentName,\n nextRoute: agentName,\n isCall: 'true',\n callId,\n }\n\n const body: Record<string, unknown> = {\n content,\n done: true,\n persist: false,\n }\n\n this.gatewayClient.produce('router.inbox', conversationId, headers, body)\n\n // Wait for response with timeout\n const timeoutPromise = new Promise<never>((_, reject) => {\n setTimeout(() => reject(new Error('TIMEOUT')), timeout)\n })\n\n const result = await Promise.race([resultPromise, timeoutPromise])\n return result\n } catch (error) {\n // Clean up on error/timeout\n this._app!._pendingCalls.delete(callId)\n this._app!._callContent.delete(callId)\n\n if (error instanceof Error && error.message === 'TIMEOUT') {\n throw new Error(`agent.call() to '${agentName}' timed out after ${timeout}ms`)\n }\n throw error\n }\n }\n ) as Promise<Message>\n }\n\n /**\n * Start a new conversation and send the first message as an agent.\n *\n * Creates a conversation in the conversation store and returns a Streamer\n * for sending the initial message. The message flows through the normal\n * Kafka pipeline (router.inbox → provider → Centrifugo).\n *\n * The user's reply will enter the normal handler pipeline via onMessage().\n *\n * @param options - Configuration for the new conversation\n * @returns Tuple of [conversationId, streamer]\n *\n * @example\n * ```typescript\n * const [convId, streamer] = await agent.startConversation({\n * userId: 'user-uuid',\n * title: 'Weekly Check-in',\n * })\n * streamer.stream('Hi! How are you feeling today?')\n * await streamer.finish()\n * ```\n */\n async startConversation(options: {\n userId: string\n title?: string\n awaiting?: boolean\n responseSchema?: Record<string, unknown>\n messageMetadata?: Record<string, unknown>\n metadata?: Record<string, unknown>\n }): Promise<[string, Streamer]> {\n if (!this.conversationClient) {\n throw new ConfigurationError(\n 'ConversationClient is required for startConversation. ' +\n 'Ensure the agent is created via BasionAgentApp.'\n )\n }\n\n // Create conversation with current_route and locked_by set atomically\n const conversation = await this.conversationClient.createConversation({\n userId: options.userId,\n title: options.title ?? 'Agent-initiated conversation',\n agentIdentifier: this.name,\n currentRoute: this.name,\n lockedBy: this.name,\n isNew: true,\n metadata: options.metadata,\n })\n\n const conversationId = String(conversation.id)\n\n // Create streamer with direct conversationId/userId (no originalMessage)\n const streamer = new Streamer({\n agentName: this.name,\n conversationId,\n userId: options.userId,\n gatewayClient: this.gatewayClient,\n conversationClient: this.conversationClient,\n attachmentClient: this.attachmentClient,\n renderClient: this.renderClient,\n awaiting: options.awaiting ?? false,\n })\n\n if (options.responseSchema) {\n streamer.setResponseSchema(options.responseSchema)\n }\n if (options.messageMetadata) {\n streamer.setMessageMetadata(options.messageMetadata)\n }\n\n return [conversationId, streamer]\n }\n\n /**\n * Mark agent as ready to consume messages.\n */\n startConsuming(): void {\n if (this.messageHandlers.length === 0) {\n throw new ConfigurationError('No message handlers registered')\n }\n\n if (!this.initialized) {\n throw new ConfigurationError('Agent not initialized with gateway')\n }\n\n this.running = true\n console.log(`Agent ${this.name} is now consuming messages`)\n }\n\n /**\n * Shutdown agent gracefully.\n */\n shutdown(): void {\n this.running = false\n\n if (this.heartbeatManager) {\n this.heartbeatManager.stop()\n }\n\n console.log(`Agent ${this.name} shutdown`)\n }\n\n /**\n * Check if agent is running.\n */\n isRunning(): boolean {\n return this.running\n }\n}\n","/**\n * Basion Agent SDK - Agent State Client\n * Port of Python basion_agent/agent_state_client.py\n */\n\nimport { APIException } from './exceptions.js'\n\nexport interface AgentStateResponse {\n conversationId: string\n namespace: string\n state: Record<string, unknown>\n createdAt?: string\n updatedAt?: string\n}\n\n/**\n * Async HTTP client for agent-state API.\n * Used for storing framework-specific state (e.g., Pydantic AI messages).\n */\nexport class AgentStateClient {\n private readonly baseUrl: string\n\n constructor(baseUrl: string) {\n this.baseUrl = baseUrl.replace(/\\/$/, '')\n }\n\n /**\n * Store or update agent state.\n */\n async put(\n conversationId: string,\n namespace: string,\n state: Record<string, unknown>\n ): Promise<{ conversationId: string; namespace: string; created: boolean }> {\n const url = `${this.baseUrl}/agent-state/${conversationId}`\n const payload = {\n namespace,\n state,\n }\n\n try {\n const response = await fetch(url, {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to store agent state: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as { conversationId: string; namespace: string; created: boolean }\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to store agent state: ${error}`)\n }\n }\n\n /**\n * Get agent state.\n */\n async get(\n conversationId: string,\n namespace: string\n ): Promise<AgentStateResponse | null> {\n const url = new URL(`${this.baseUrl}/agent-state/${conversationId}`)\n url.searchParams.set('namespace', namespace)\n\n try {\n const response = await fetch(url.toString())\n\n if (response.status === 404) {\n return null\n }\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to get agent state: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as AgentStateResponse\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to get agent state: ${error}`)\n }\n }\n\n /**\n * Delete agent state.\n */\n async delete(\n conversationId: string,\n namespace?: string\n ): Promise<{ conversationId: string; namespace?: string; deleted: number }> {\n const url = new URL(`${this.baseUrl}/agent-state/${conversationId}`)\n if (namespace) {\n url.searchParams.set('namespace', namespace)\n }\n\n try {\n const response = await fetch(url.toString(), { method: 'DELETE' })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to delete agent state: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as { conversationId: string; namespace?: string; deleted: number }\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to delete agent state: ${error}`)\n }\n }\n\n // ── Vercel AI Messages (row-per-message store) ──────────────────────\n\n /**\n * Append Vercel AI messages to a conversation.\n */\n async appendVercelMessages(\n conversationId: string,\n messages: Record<string, unknown>[],\n ): Promise<{ conversation_id: string; appended: number; total: number }> {\n const url = `${this.baseUrl}/vercel-ai-messages/${conversationId}/append`\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ messages }),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to append vercel messages: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as { conversation_id: string; appended: number; total: number }\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to append vercel messages: ${error}`)\n }\n }\n\n /**\n * Get Vercel AI messages for a conversation.\n * Returns AgentStateResponse-compatible shape or null if not found.\n */\n async getVercelMessages(\n conversationId: string,\n last?: number,\n ): Promise<AgentStateResponse | null> {\n const url = new URL(`${this.baseUrl}/vercel-ai-messages/${conversationId}`)\n if (last !== undefined) {\n url.searchParams.set('last', String(last))\n }\n\n try {\n const response = await fetch(url.toString())\n\n if (response.status === 404) {\n return null\n }\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to get vercel messages: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as AgentStateResponse\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to get vercel messages: ${error}`)\n }\n }\n\n /**\n * Prune Vercel AI messages before a given sequence number.\n * Pruned messages are excluded from load but retained for audit.\n */\n async pruneVercelMessages(\n conversationId: string,\n beforeSequence: number,\n ): Promise<{ conversation_id: string; pruned: number }> {\n const url = `${this.baseUrl}/vercel-ai-messages/${conversationId}/prune`\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ before_sequence: beforeSequence }),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to prune vercel messages: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as { conversation_id: string; pruned: number }\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to prune vercel messages: ${error}`)\n }\n }\n\n /**\n * Delete all Vercel AI messages for a conversation.\n */\n async deleteVercelMessages(\n conversationId: string,\n ): Promise<{ conversation_id: string; deleted: number }> {\n const url = `${this.baseUrl}/vercel-ai-messages/${conversationId}`\n\n try {\n const response = await fetch(url, { method: 'DELETE' })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to delete vercel messages: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as { conversation_id: string; deleted: number }\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to delete vercel messages: ${error}`)\n }\n }\n\n // ── Filesystem Snapshots (row-per-snapshot store) ─────────────────────\n\n /**\n * Append filesystem snapshots to a conversation.\n */\n async appendFilesystemSnapshots(\n conversationId: string,\n snapshots: Record<string, unknown>[],\n ): Promise<{ conversation_id: string; appended: number; total: number }> {\n const url = `${this.baseUrl}/filesystem-snapshots/${conversationId}/append`\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ snapshots }),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to append filesystem snapshots: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as { conversation_id: string; appended: number; total: number }\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to append filesystem snapshots: ${error}`)\n }\n }\n\n /**\n * Get filesystem snapshots for a conversation.\n * Returns AgentStateResponse-compatible shape or null if not found.\n */\n async getFilesystemSnapshots(\n conversationId: string,\n ): Promise<AgentStateResponse | null> {\n const url = `${this.baseUrl}/filesystem-snapshots/${conversationId}`\n\n try {\n const response = await fetch(url.toString())\n\n if (response.status === 404) {\n return null\n }\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new APIException(`Failed to get filesystem snapshots: ${response.status} - ${errorText}`, response.status)\n }\n\n return await response.json() as AgentStateResponse\n } catch (error) {\n if (error instanceof APIException) throw error\n throw new APIException(`Failed to get filesystem snapshots: ${error}`)\n }\n }\n}\n","/**\n * Basion Agent SDK - Vercel AI SDK Extension\n *\n * VercelAIMessageStore for persistent message history with Vercel AI SDK.\n *\n * @packageDocumentation\n */\n\nimport type { BasionAgentApp } from '../app.js'\nimport { _messageContext } from '../agent.js'\nimport { AgentStateClient } from '../agent-state-client.js'\n\n// Type definitions compatible with Vercel AI SDK's CoreMessage\ninterface CoreMessage {\n role: 'system' | 'user' | 'assistant' | 'tool'\n content: string | Array<{ type: string;[key: string]: unknown }>\n [key: string]: unknown\n}\n\n/**\n * Persistent message store for Vercel AI SDK.\n *\n * Stores and retrieves AI SDK message history using the agent-state API.\n * Cross-system message IDs (`_messageId`) are embedded automatically — no\n * manual ID passing required.\n *\n * @example\n * ```typescript\n * import { BasionAgentApp } from 'basion-ai-sdk'\n * import { VercelAIMessageStore } from 'basion-ai-sdk/extensions/vercel-ai'\n * import { streamText } from 'ai'\n * import { anthropic } from '@ai-sdk/anthropic'\n *\n * const app = new BasionAgentApp({ gatewayUrl: 'agent-gateway:8080', apiKey: 'your-key' })\n * const store = new VercelAIMessageStore(app)\n * const agent = await app.registerMe({ ... })\n *\n * agent.onMessage(async (message) => {\n * const history = await store.load(message.conversationId)\n *\n * const messages = [...history, { role: 'user', content: message.content }]\n *\n * const result = await streamText({ model: anthropic('claude-sonnet-4-6'), messages })\n *\n * // _messageId is embedded automatically — no IDs to pass\n * await store.append(message.conversationId, [\n * { role: 'user', content: message.content },\n * ...result.response.messages,\n * ])\n * })\n *\n * app.run()\n * ```\n */\nexport class VercelAIMessageStore {\n private static readonly NAMESPACE = 'vercel_ai'\n private readonly client: AgentStateClient\n\n constructor(app: BasionAgentApp) {\n this.client = new AgentStateClient(app.gatewayClient.conversationStoreUrl)\n }\n\n /**\n * Load message history for a conversation.\n *\n * @param conversationId - The conversation ID to load history for\n * @returns Array of CoreMessage objects (empty array if no history)\n */\n async load(conversationId: string): Promise<CoreMessage[]> {\n const result = await this.client.getVercelMessages(conversationId)\n\n if (!result) {\n return []\n }\n\n const state = result.state ?? {}\n const messages = state.messages as unknown[]\n\n if (!Array.isArray(messages)) {\n return []\n }\n\n return messages.filter(this.isValidMessage) as CoreMessage[]\n }\n\n /**\n * Append new messages to existing history.\n *\n * Cross-system `_messageId` fields are embedded automatically from the\n * current message handler context — no IDs need to be passed manually.\n *\n * @param conversationId - The conversation ID\n * @param newMessages - New messages to append (user turn + response messages)\n */\n async append(conversationId: string, newMessages: CoreMessage[]): Promise<void> {\n const ctx = _messageContext.getStore()\n const tagged = ctx ? this.embedMessageIds(newMessages, ctx) : newMessages\n await this.client.appendVercelMessages(\n conversationId,\n tagged.map(this.serializeMessage),\n )\n }\n\n /**\n * Clear message history for a conversation.\n *\n * @param conversationId - The conversation ID to clear history for\n */\n async clear(conversationId: string): Promise<void> {\n await this.client.deleteVercelMessages(conversationId)\n }\n\n /**\n * Prune messages before a given sequence number.\n *\n * Called after server-side compaction fires. Pruned messages are excluded\n * from `load()` but retained in the database for audit.\n *\n * @param conversationId - The conversation ID\n * @param beforeSequence - Prune all messages with sequence < this value\n * @returns Number of messages pruned\n */\n async pruneBeforeSequence(conversationId: string, beforeSequence: number): Promise<number> {\n const result = await this.client.pruneVercelMessages(conversationId, beforeSequence)\n return result.pruned\n }\n\n /**\n * Get the last N messages from history.\n *\n * @param conversationId - The conversation ID\n * @param count - Number of messages to retrieve\n */\n async getLastMessages(conversationId: string, count: number): Promise<CoreMessage[]> {\n const result = await this.client.getVercelMessages(conversationId, count)\n\n if (!result) {\n return []\n }\n\n const state = result.state ?? {}\n const messages = state.messages as unknown[]\n\n if (!Array.isArray(messages)) {\n return []\n }\n\n return messages.filter(this.isValidMessage) as CoreMessage[]\n }\n\n private embedMessageIds(\n messages: CoreMessage[],\n ids: { userMessageId?: string; responseMessageId?: string },\n ): CoreMessage[] {\n const result = [...messages]\n\n if (ids.userMessageId) {\n const userIdx = result.findIndex((m) => m.role === 'user')\n if (userIdx !== -1) {\n result[userIdx] = { ...result[userIdx], _messageId: ids.userMessageId }\n }\n }\n\n if (ids.responseMessageId) {\n for (let i = result.length - 1; i >= 0; i--) {\n const m = result[i]\n if (\n m.role === 'assistant' &&\n (typeof m.content === 'string'\n ? m.content.length > 0\n : (m.content as Array<{ type: string }>).some((c) => c.type === 'text'))\n ) {\n result[i] = { ...result[i], _messageId: ids.responseMessageId }\n break\n }\n }\n }\n\n return result\n }\n\n private isValidMessage(msg: unknown): msg is CoreMessage {\n if (typeof msg !== 'object' || msg === null) return false\n const m = msg as Record<string, unknown>\n return (\n typeof m.role === 'string' &&\n ['system', 'user', 'assistant', 'tool'].includes(m.role) &&\n (typeof m.content === 'string' || Array.isArray(m.content))\n )\n }\n\n private serializeMessage(msg: CoreMessage): Record<string, unknown> {\n return { ...msg }\n }\n}\n\nexport type { CoreMessage }\n"]}
|
|
@@ -153,15 +153,25 @@ var VfsBackedFs = class _VfsBackedFs {
|
|
|
153
153
|
}
|
|
154
154
|
async mergeWithRetry(maxRetries = 3) {
|
|
155
155
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
this.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
let result;
|
|
157
|
+
try {
|
|
158
|
+
result = await this.vfsClient.merge(
|
|
159
|
+
this.filesystemId,
|
|
160
|
+
this.branchName,
|
|
161
|
+
this.baseBranch,
|
|
162
|
+
{
|
|
163
|
+
metadata: this.commitMetadata,
|
|
164
|
+
messageId: this.messageId
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (attempt < maxRetries && isTransientNetworkError(err)) {
|
|
169
|
+
const delayMs = 100 * Math.pow(2, attempt);
|
|
170
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
171
|
+
continue;
|
|
163
172
|
}
|
|
164
|
-
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
165
175
|
if (result.status === "noop") return null;
|
|
166
176
|
if (result.status === "merged") {
|
|
167
177
|
if (result.commit_id === this.currentCommitId) {
|
|
@@ -439,6 +449,28 @@ function isNotFoundError(err) {
|
|
|
439
449
|
}
|
|
440
450
|
return false;
|
|
441
451
|
}
|
|
452
|
+
function isTransientNetworkError(err) {
|
|
453
|
+
if (!err || typeof err !== "object") return false;
|
|
454
|
+
const statusCode = err.statusCode;
|
|
455
|
+
if (statusCode === 502 || statusCode === 503 || statusCode === 504) {
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
const seen = /* @__PURE__ */ new Set();
|
|
459
|
+
let current = err;
|
|
460
|
+
while (current && !seen.has(current)) {
|
|
461
|
+
seen.add(current);
|
|
462
|
+
const e = current;
|
|
463
|
+
const code = e.code;
|
|
464
|
+
if (code === "UND_ERR_SOCKET" || code === "ECONNRESET" || code === "EPIPE" || code === "ETIMEDOUT" || code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "EAI_AGAIN" || code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT" || code === "UND_ERR_CONNECT_TIMEOUT") {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
for (const sym of Object.getOwnPropertySymbols(e)) {
|
|
468
|
+
if (sym.toString().includes("undici.error")) return true;
|
|
469
|
+
}
|
|
470
|
+
current = e.cause;
|
|
471
|
+
}
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
442
474
|
|
|
443
475
|
export { VfsBackedFs, VfsMergeConflictError };
|
|
444
476
|
//# sourceMappingURL=vfs-sync.js.map
|