basion-ai-sdk 0.18.0 → 0.20.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.
@@ -377,6 +377,46 @@ interface CreateConversationPayload {
377
377
  isNew?: boolean;
378
378
  metadata?: Record<string, unknown>;
379
379
  }
380
+ /**
381
+ * Payload for creating an issue on a conversation.
382
+ *
383
+ * Attribution is caller-supplied and unopinionated:
384
+ * - agent/judge callers pass `creatorType: 'agent'` + `createdByAgent`
385
+ * - human callers pass `createdBy` (an auth.users id)
386
+ * When omitted, conversation-store resolves attribution from request context
387
+ * (no X-User-ID header ⇒ agent path).
388
+ */
389
+ interface CreateIssuePayload {
390
+ /** Category KEY: 'factual_error' | 'tool_error' | 'communication' | 'safety' | 'other' */
391
+ category: string;
392
+ description: string;
393
+ severity?: 'low' | 'medium' | 'high' | 'critical';
394
+ messageId?: string;
395
+ creatorType?: 'evaluator' | 'admin' | 'agent';
396
+ createdByAgent?: string;
397
+ createdBy?: string;
398
+ }
399
+ /**
400
+ * Issue record returned by conversation-store (camelCase view of the API's
401
+ * snake_case IssueResponse).
402
+ */
403
+ interface IssueData {
404
+ id: string;
405
+ conversationId: string;
406
+ messageId: string | null;
407
+ categoryId: string;
408
+ severity: string;
409
+ status: string;
410
+ description: string;
411
+ resolutionNote: string | null;
412
+ createdBy: string | null;
413
+ createdByAgent: string | null;
414
+ creatorType: string;
415
+ resolvedBy: string | null;
416
+ resolvedAt: string | null;
417
+ createdAt: string;
418
+ updatedAt: string;
419
+ }
380
420
  /**
381
421
  * HTTP client for interacting with conversation-store API.
382
422
  */
@@ -399,6 +439,13 @@ declare class ConversationClient {
399
439
  * Update conversation fields (PATCH).
400
440
  */
401
441
  updateConversation(conversationId: string, data: Partial<ConversationData>): Promise<ConversationData>;
442
+ /**
443
+ * Create an issue on a conversation (or a specific message).
444
+ *
445
+ * Sends no X-User-ID header, so conversation-store treats the caller as a
446
+ * trusted service/agent and honors body-supplied attribution.
447
+ */
448
+ createIssue(conversationId: string, payload: CreateIssuePayload): Promise<IssueData>;
402
449
  }
403
450
 
404
451
  /**
@@ -2158,6 +2205,7 @@ declare class Agent {
2158
2205
  private readonly heartbeatInterval;
2159
2206
  private readonly heartbeatFailureThreshold;
2160
2207
  private readonly onHeartbeatFailure?;
2208
+ private readonly enableHeartbeat;
2161
2209
  /** Set by BasionAgentApp.registerMe() to enable agent.call() */
2162
2210
  _app?: BasionAgentApp;
2163
2211
  private messageHandlers;
@@ -2193,6 +2241,7 @@ declare class Agent {
2193
2241
  attachmentClient?: AttachmentClient;
2194
2242
  renderClient?: RenderClient;
2195
2243
  errorMessageTemplate?: string;
2244
+ enableHeartbeat?: boolean;
2196
2245
  });
2197
2246
  /**
2198
2247
  * Create an AbortSignal for a conversation's current operation.
@@ -2607,6 +2656,67 @@ declare class PatientProfileClient {
2607
2656
  private json;
2608
2657
  }
2609
2658
 
2659
+ /**
2660
+ * Judge types for the Basion Agent SDK
2661
+ */
2662
+ interface JudgeMetricDef {
2663
+ key: string;
2664
+ label: string;
2665
+ cluster: string;
2666
+ description?: string;
2667
+ scoreMin?: number;
2668
+ scoreMax?: number;
2669
+ }
2670
+ interface RegisterJudgeOptions {
2671
+ name: string;
2672
+ representationName?: string;
2673
+ about: string;
2674
+ document: string;
2675
+ metrics: JudgeMetricDef[];
2676
+ defaultScope?: {
2677
+ autoAgentName?: string;
2678
+ autoAgentVersion?: string;
2679
+ autoWindowN?: number;
2680
+ autoSamplePct?: number;
2681
+ autoUserIds?: string[];
2682
+ };
2683
+ }
2684
+ interface JudgeRequestPayload {
2685
+ targetConversationId: string;
2686
+ baseVfsCommit: string | null;
2687
+ vfsFilesystemId: string | null;
2688
+ runId?: string;
2689
+ }
2690
+ interface JudgeVerdictMetric {
2691
+ key: string;
2692
+ score: number | null;
2693
+ reason: string;
2694
+ }
2695
+ interface JudgeVerdict {
2696
+ status: 'completed' | 'skipped_ineligible' | 'failed';
2697
+ metrics: JudgeVerdictMetric[];
2698
+ summary?: string;
2699
+ usage?: {
2700
+ promptTokens?: number;
2701
+ completionTokens?: number;
2702
+ costUsd?: number;
2703
+ };
2704
+ }
2705
+ interface JudgeContext {
2706
+ app: BasionAgentApp;
2707
+ targetConversationId: string;
2708
+ userId: string;
2709
+ runId?: string;
2710
+ baseVfsCommit: string | null;
2711
+ vfsFilesystemId: string | null;
2712
+ conversation: {
2713
+ userView: unknown[];
2714
+ agentTrace: unknown[];
2715
+ };
2716
+ metrics: JudgeMetricDef[];
2717
+ }
2718
+ type JudgeHandler = (ctx: JudgeContext) => Promise<JudgeVerdict>;
2719
+
2610
2720
  /**
2611
2721
  * Loki log handler for remote log collection.
2612
2722
  *
@@ -2792,8 +2902,31 @@ declare class BasionAgentApp {
2792
2902
  private reinitializeAgents;
2793
2903
  /**
2794
2904
  * Register an agent with the backend.
2905
+ *
2906
+ * @param options - Agent registration options
2907
+ * @param internal - Internal options for special agents (judges, orchestrators).
2908
+ * When `skipInventory` is true, the agent is NOT registered in the AI Inventory
2909
+ * catalog; an AgentRegistrationData is synthesized from the options instead.
2910
+ * When `enableHeartbeat` is false, the heartbeat manager will not be started.
2911
+ */
2912
+ registerMe(options: RegisterOptions, internal?: {
2913
+ skipInventory?: boolean;
2914
+ enableHeartbeat?: boolean;
2915
+ }): Promise<Agent>;
2916
+ /**
2917
+ * Register a judge agent.
2918
+ *
2919
+ * Judges are NOT registered in the AI Inventory agent catalog — the gateway
2920
+ * routes on the API key alone. This method:
2921
+ * 1. Connects to the gateway WITHOUT an inventory row (`skipInventory:true`).
2922
+ * 2. Does NOT start a heartbeat (`enableHeartbeat:false`) — judges are
2923
+ * invoked on-demand, not long-running listeners.
2924
+ * 3. Self-registers into the JUDGES table via POST /s/judge/judges/sync.
2925
+ *
2926
+ * @param opts - Judge registration options (name, about, document, metrics, defaultScope)
2927
+ * @returns The connected Agent instance
2795
2928
  */
2796
- registerMe(options: RegisterOptions): Promise<Agent>;
2929
+ registerJudge(opts: RegisterJudgeOptions): Promise<Agent>;
2797
2930
  /**
2798
2931
  * Get the Loki log handler for remote logging.
2799
2932
  * Returns undefined if remote logging is not enabled.
@@ -2861,4 +2994,4 @@ declare class BasionAgentApp {
2861
2994
  private doShutdown;
2862
2995
  }
2863
2996
 
2864
- export { type DraftDecision as $, type AttachmentInfo as A, BasionAgentApp as B, type CheckboxConfig as C, type DatePickerConfig as D, type BinaryResult as E, type Field as F, GatewayClient as G, type HiddenConfig as H, type InferFields as I, type BraintrustConfig as J, type BundleRenderOptions as K, type Capability as L, type MultiSelectConfig as M, type NumberConfig as N, type OptionDef as O, type CapabilityItem as P, type ChunkResult as Q, type ConnectionState as R, StructuralStreamer as S, type TextConfig as T, type ConnectionStateListener as U, type ValidationResult as V, Conversation as W, ConversationClient as X, type ConversationData as Y, ConversationMessage as Z, DEFAULT_RECONNECTION_OPTIONS as _, type CheckboxGroupConfig as a, type ValidateCodeResult as a$, type DraftResult as a0, type Edge as a1, type Entity as a2, type ExpandValueSetResult as a3, type GenuiComponent as a4, type GenuiComponentDict as a5, type GetMessagesOptions as a6, type GroupedMatch as a7, type GroupedMatchEntry as a8, type HeartbeatFailureCallback as a9, type NeedWithMatches as aA, type PathStep as aB, PatientProfileClient as aC, type PdfOptions as aD, type ProduceAck as aE, type Prompt as aF, type QueryOptions as aG, type ReconnectionOptions as aH, type RegisterOptions as aI, type RelatedPage as aJ, type RenderBufferResult as aK, RenderClient as aL, type RenderOptions as aM, type RenderResult as aN, type SearchMatch as aO, type SearchOptions as aP, type SearchResult as aQ, SenderFilter as aR, type SenderFilterOptions as aS, type SentryConfig as aT, type SentryLoggerProxy as aU, type SimilarDisease as aV, type StreamOptions as aW, Streamer as aX, type StreamerOptions as aY, Tools as aZ, type UserSummary as a_, HeartbeatManager as aa, type HeartbeatOptions as ab, type IngestResponse as ac, type KafkaHeaders as ad, type KafkaMessage as ae, KnowledgeGraphTool as af, type LogEntry as ag, type LogLevel as ah, type LokiHandlerOptions as ai, LokiLogHandler as aj, type LookupCodeResult as ak, MatchingClient as al, MatchingTool as am, Memory as an, MemoryClient as ao, type MemoryIngestOptions as ap, type MemoryMessage as aq, type MemorySearchOptions as ar, type MemorySearchResult as as, MemoryV2 as at, MemoryV2Client as au, type MemoryV2IngestPayload as av, type MemoryV2SearchPayload as aw, type MessageHandler as ax, type Need as ay, type NeedItem as az, type FileFieldConfig as b, createAttachmentInfo as b0, getSentryLogger as b1, isBraintrustEnabled as b2, isSentryEnabled as b3, reportPath as b4, 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 AgentSqlResult as w, type AppOptions as x, type ApproveDraftResult as y, AttachmentClient as z };
2997
+ export { ConversationMessage as $, type AttachmentInfo as A, BasionAgentApp as B, type CheckboxConfig as C, type DatePickerConfig as D, AttachmentClient as E, type Field as F, GatewayClient as G, type HiddenConfig as H, type InferFields as I, type JudgeHandler as J, type BinaryResult as K, type BraintrustConfig as L, type MultiSelectConfig as M, type NumberConfig as N, type OptionDef as O, type BundleRenderOptions as P, type Capability as Q, type CapabilityItem as R, StructuralStreamer as S, type TextConfig as T, type ChunkResult as U, type ValidationResult as V, type ConnectionState as W, type ConnectionStateListener as X, Conversation as Y, ConversationClient as Z, type ConversationData as _, type CheckboxGroupConfig as a, type SenderFilterOptions as a$, type CreateIssuePayload as a0, DEFAULT_RECONNECTION_OPTIONS as a1, type DraftDecision as a2, type DraftResult as a3, type Edge as a4, type Entity as a5, type ExpandValueSetResult as a6, type GenuiComponent as a7, type GenuiComponentDict as a8, type GetMessagesOptions as a9, type MemorySearchOptions as aA, type MemorySearchResult as aB, MemoryV2 as aC, MemoryV2Client as aD, type MemoryV2IngestPayload as aE, type MemoryV2SearchPayload as aF, type Need as aG, type NeedItem as aH, type NeedWithMatches as aI, type PathStep as aJ, PatientProfileClient as aK, type PdfOptions as aL, type ProduceAck as aM, type Prompt as aN, type QueryOptions as aO, type ReconnectionOptions as aP, type RegisterJudgeOptions as aQ, type RegisterOptions as aR, type RelatedPage as aS, type RenderBufferResult as aT, RenderClient as aU, type RenderOptions as aV, type RenderResult as aW, type SearchMatch as aX, type SearchOptions as aY, type SearchResult as aZ, SenderFilter as a_, type GroupedMatch as aa, type GroupedMatchEntry as ab, type HeartbeatFailureCallback as ac, HeartbeatManager as ad, type HeartbeatOptions as ae, type IngestResponse as af, type IssueData as ag, type JudgeContext as ah, type JudgeMetricDef as ai, type JudgeRequestPayload as aj, type JudgeVerdict as ak, type JudgeVerdictMetric as al, type KafkaHeaders as am, type KafkaMessage as an, KnowledgeGraphTool as ao, type LogEntry as ap, type LogLevel as aq, type LokiHandlerOptions as ar, LokiLogHandler as as, type LookupCodeResult as at, MatchingClient as au, MatchingTool as av, Memory as aw, MemoryClient as ax, type MemoryIngestOptions as ay, type MemoryMessage as az, type FileFieldConfig as b, type SentryConfig as b0, type SentryLoggerProxy as b1, type SimilarDisease as b2, type StreamOptions as b3, Streamer as b4, type StreamerOptions as b5, Tools as b6, type UserSummary as b7, type ValidateCodeResult as b8, createAttachmentInfo as b9, getSentryLogger as ba, isBraintrustEnabled as bb, isSentryEnabled as bc, reportPath as bd, 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 MessageHandler as t, type AgentInfo as u, AgentInventoryTool as v, type AgentRegistrationData as w, type AgentSqlResult as x, type AppOptions as y, type ApproveDraftResult as z };
@@ -1,4 +1,4 @@
1
- import { B as BasionAgentApp } from '../app-mRFPU6oe.js';
1
+ import { B as BasionAgentApp } from '../app-CfmWOkE7.js';
2
2
  import 'zod';
3
3
  import '../vfs-client-CsHAFj1R.js';
4
4
 
@@ -1,4 +1,4 @@
1
- import { B as BasionAgentApp } from '../app-mRFPU6oe.js';
1
+ import { B as BasionAgentApp } from '../app-CfmWOkE7.js';
2
2
  import 'zod';
3
3
  import '../vfs-client-CsHAFj1R.js';
4
4
 
@@ -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 { DedupLRU } from './dedup-lru.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 private readonly dedup: DedupLRU = new DedupLRU(\n Number(process.env.BASION_SDK_DEDUP_LRU_SIZE) || 100_000,\n )\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 * Consults the dedup LRU before dispatching to suppress duplicate deliveries.\n * The dedup key is `userMessageId` when present; otherwise falls back to\n * `${topic}:${partition}:${offset}` so at-most-once semantics hold even for\n * messages that pre-date the userMessageId header.\n *\n * Note: ConsumeAck is sent by GatewayClient BEFORE this method is called,\n * so dedup suppression here only prevents double handler dispatch — it does\n * not affect ack delivery.\n */\n private handleKafkaMessage(kafkaMsg: KafkaMessage): void {\n const dedupKey = kafkaMsg.headers?.userMessageId\n ?? `${kafkaMsg.topic}:${kafkaMsg.partition}:${kafkaMsg.offset}`\n if (this.dedup.checkAndAdd(dedupKey)) {\n console.debug(`[basion-sdk] dedup hit: ${dedupKey} — suppressing handler dispatch`)\n return\n }\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 await 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"]}
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 { DedupLRU } from './dedup-lru.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 private readonly enableHeartbeat: boolean\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 private readonly dedup: DedupLRU = new DedupLRU(\n Number(process.env.BASION_SDK_DEDUP_LRU_SIZE) || 100_000,\n )\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 enableHeartbeat?: boolean\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 this.enableHeartbeat = options.enableHeartbeat ?? true\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.enableHeartbeat) {\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\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 * Consults the dedup LRU before dispatching to suppress duplicate deliveries.\n * The dedup key is `userMessageId` when present; otherwise falls back to\n * `${topic}:${partition}:${offset}` so at-most-once semantics hold even for\n * messages that pre-date the userMessageId header.\n *\n * Note: ConsumeAck is sent by GatewayClient BEFORE this method is called,\n * so dedup suppression here only prevents double handler dispatch — it does\n * not affect ack delivery.\n */\n private handleKafkaMessage(kafkaMsg: KafkaMessage): void {\n const dedupKey = kafkaMsg.headers?.userMessageId\n ?? `${kafkaMsg.topic}:${kafkaMsg.partition}:${kafkaMsg.offset}`\n if (this.dedup.checkAndAdd(dedupKey)) {\n console.debug(`[basion-sdk] dedup hit: ${dedupKey} — suppressing handler dispatch`)\n return\n }\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 await 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"]}