march-ai-sdk 0.3.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.
@@ -0,0 +1,351 @@
1
+ /**
2
+ * March Agent SDK - LangGraph Extension
3
+ *
4
+ * HTTPCheckpointSaver for LangGraph that stores state via HTTP API.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ import type { MarchAgentApp } from '../app.js'
10
+ import { CheckpointClient } from '../checkpoint-client.js'
11
+ import type {
12
+ CheckpointConfig,
13
+ CheckpointData,
14
+ CheckpointMetadata as APICheckpointMetadata,
15
+ CheckpointTuple as APICheckpointTuple,
16
+ } from '../checkpoint-client.js'
17
+
18
+ // Type definitions for LangGraph (optional peer dependency)
19
+ // These are compatible with @langchain/langgraph-checkpoint
20
+ interface RunnableConfig {
21
+ configurable?: {
22
+ thread_id?: string
23
+ checkpoint_ns?: string
24
+ checkpoint_id?: string
25
+ }
26
+ }
27
+
28
+ interface Checkpoint {
29
+ v?: number
30
+ id?: string
31
+ ts?: string
32
+ channel_values?: Record<string, unknown>
33
+ channel_versions?: Record<string, string>
34
+ versions_seen?: Record<string, Record<string, string>>
35
+ pending_sends?: unknown[]
36
+ }
37
+
38
+ interface CheckpointMetadata {
39
+ source?: string
40
+ step?: number
41
+ writes?: unknown
42
+ parents?: Record<string, string>
43
+ }
44
+
45
+ interface CheckpointTuple {
46
+ config: RunnableConfig
47
+ checkpoint: Checkpoint
48
+ metadata: CheckpointMetadata
49
+ parent_config?: RunnableConfig
50
+ pending_writes?: unknown[]
51
+ }
52
+
53
+ interface PendingWrite {
54
+ [key: string]: unknown
55
+ }
56
+
57
+ /**
58
+ * HTTP-based checkpoint saver for LangGraph.
59
+ *
60
+ * Stores graph state via HTTP calls to the conversation-store checkpoint API,
61
+ * enabling distributed checkpoint storage without direct database access.
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * import { MarchAgentApp } from 'march-ai-sdk'
66
+ * import { HTTPCheckpointSaver } from 'march-ai-sdk/extensions/langgraph'
67
+ * import { StateGraph } from '@langchain/langgraph'
68
+ *
69
+ * const app = new MarchAgentApp({
70
+ * gatewayUrl: 'agent-gateway:8080',
71
+ * apiKey: 'your-key',
72
+ * })
73
+ *
74
+ * const checkpointer = new HTTPCheckpointSaver(app)
75
+ *
76
+ * const graph = new StateGraph(MyState)
77
+ * // ... define graph ...
78
+ * const compiled = graph.compile({ checkpointer })
79
+ *
80
+ * const config = { configurable: { thread_id: 'my-thread' } }
81
+ * const result = await compiled.invoke({ messages: [...] }, config)
82
+ * ```
83
+ */
84
+ export class HTTPCheckpointSaver {
85
+ private readonly client: CheckpointClient
86
+
87
+ constructor(app: MarchAgentApp) {
88
+ this.client = new CheckpointClient(app.gatewayClient.conversationStoreUrl)
89
+ }
90
+
91
+ /**
92
+ * Get thread_id from config.
93
+ */
94
+ private getThreadId(config: RunnableConfig): string {
95
+ const threadId = config.configurable?.thread_id
96
+ if (!threadId) {
97
+ throw new Error('Config must contain configurable.thread_id')
98
+ }
99
+ return threadId
100
+ }
101
+
102
+ /**
103
+ * Get checkpoint_ns from config.
104
+ */
105
+ private getCheckpointNs(config: RunnableConfig): string {
106
+ return config.configurable?.checkpoint_ns ?? ''
107
+ }
108
+
109
+ /**
110
+ * Get checkpoint_id from config.
111
+ */
112
+ private getCheckpointId(config: RunnableConfig): string | undefined {
113
+ return config.configurable?.checkpoint_id
114
+ }
115
+
116
+ /**
117
+ * Generate a unique checkpoint ID.
118
+ */
119
+ private generateCheckpointId(): string {
120
+ return new Date().toISOString()
121
+ }
122
+
123
+ /**
124
+ * Fetch a checkpoint tuple asynchronously.
125
+ */
126
+ async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {
127
+ const threadId = this.getThreadId(config)
128
+ const checkpointNs = this.getCheckpointNs(config)
129
+ const checkpointId = this.getCheckpointId(config)
130
+
131
+ const result = await this.client.getTuple(threadId, checkpointNs, checkpointId)
132
+
133
+ if (!result) {
134
+ return undefined
135
+ }
136
+
137
+ return this.responseToTuple(result)
138
+ }
139
+
140
+ /**
141
+ * List checkpoints asynchronously.
142
+ */
143
+ async *list(
144
+ config: RunnableConfig | undefined,
145
+ options?: {
146
+ filter?: Record<string, unknown>
147
+ before?: RunnableConfig
148
+ limit?: number
149
+ }
150
+ ): AsyncGenerator<CheckpointTuple> {
151
+ const threadId = config?.configurable?.thread_id
152
+ const checkpointNs = config?.configurable?.checkpoint_ns
153
+ const beforeId = options?.before?.configurable?.checkpoint_id
154
+
155
+ const results = await this.client.list({
156
+ threadId,
157
+ checkpointNs,
158
+ before: beforeId,
159
+ limit: options?.limit,
160
+ })
161
+
162
+ for (const result of results) {
163
+ const tuple = this.responseToTuple(result)
164
+ if (tuple) {
165
+ yield tuple
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Store a checkpoint asynchronously.
172
+ */
173
+ async put(
174
+ config: RunnableConfig,
175
+ checkpoint: Checkpoint,
176
+ metadata: CheckpointMetadata,
177
+ newVersions?: Record<string, unknown>
178
+ ): Promise<RunnableConfig> {
179
+ const threadId = this.getThreadId(config)
180
+ const checkpointNs = this.getCheckpointNs(config)
181
+
182
+ let checkpointId = this.getCheckpointId(config)
183
+ if (!checkpointId) {
184
+ checkpointId = checkpoint.id ?? this.generateCheckpointId()
185
+ }
186
+
187
+ const apiConfig: CheckpointConfig = {
188
+ configurable: {
189
+ thread_id: threadId,
190
+ checkpoint_ns: checkpointNs,
191
+ checkpoint_id: checkpointId,
192
+ },
193
+ }
194
+
195
+ const checkpointData = this.checkpointToApi(checkpoint)
196
+ const metadataData = this.metadataToApi(metadata)
197
+
198
+ const result = await this.client.put(
199
+ apiConfig,
200
+ checkpointData,
201
+ metadataData,
202
+ newVersions ?? {}
203
+ )
204
+
205
+ return result.config as RunnableConfig
206
+ }
207
+
208
+ /**
209
+ * Store intermediate writes asynchronously.
210
+ */
211
+ async putWrites(
212
+ _config: RunnableConfig,
213
+ _writes: PendingWrite[],
214
+ _taskId: string
215
+ ): Promise<void> {
216
+ // Stub - writes are not persisted separately
217
+ // They are included in the checkpoint metadata
218
+ }
219
+
220
+ /**
221
+ * Delete all checkpoints for a thread.
222
+ */
223
+ async deleteThread(threadId: string): Promise<void> {
224
+ await this.client.deleteThread(threadId)
225
+ }
226
+
227
+ /**
228
+ * Convert checkpoint to API format.
229
+ */
230
+ private checkpointToApi(checkpoint: Checkpoint): CheckpointData {
231
+ return {
232
+ v: checkpoint.v ?? 1,
233
+ id: checkpoint.id ?? this.generateCheckpointId(),
234
+ ts: checkpoint.ts ?? new Date().toISOString(),
235
+ channel_values: this.serializeChannelValues(checkpoint.channel_values ?? {}),
236
+ channel_versions: checkpoint.channel_versions ?? {},
237
+ versions_seen: checkpoint.versions_seen ?? {},
238
+ pending_sends: checkpoint.pending_sends ?? [],
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Convert metadata to API format.
244
+ */
245
+ private metadataToApi(metadata: CheckpointMetadata): APICheckpointMetadata {
246
+ return {
247
+ source: metadata.source ?? 'input',
248
+ step: metadata.step ?? -1,
249
+ writes: metadata.writes,
250
+ parents: metadata.parents ?? {},
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Convert API response to CheckpointTuple.
256
+ */
257
+ private responseToTuple(response: APICheckpointTuple): CheckpointTuple {
258
+ return {
259
+ config: response.config as RunnableConfig,
260
+ checkpoint: this.deserializeCheckpoint(response.checkpoint),
261
+ metadata: response.metadata,
262
+ parent_config: response.parent_config as RunnableConfig | undefined,
263
+ pending_writes: response.pending_writes ?? [],
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Serialize channel values for transmission.
269
+ */
270
+ private serializeChannelValues(values: Record<string, unknown>): Record<string, unknown> {
271
+ return this.serializeValue(values) as Record<string, unknown>
272
+ }
273
+
274
+ /**
275
+ * Serialize a value for JSON transmission.
276
+ */
277
+ private serializeValue(value: unknown, depth: number = 0): unknown {
278
+ const MAX_DEPTH = 100
279
+ if (depth > MAX_DEPTH) {
280
+ return { __max_depth_exceeded__: true }
281
+ }
282
+
283
+ if (value === null || value === undefined) {
284
+ return value
285
+ }
286
+
287
+ // Handle Buffer/Uint8Array
288
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
289
+ return { __bytes__: Buffer.from(value).toString('base64') }
290
+ }
291
+
292
+ // Handle arrays
293
+ if (Array.isArray(value)) {
294
+ return value.map(item => this.serializeValue(item, depth + 1))
295
+ }
296
+
297
+ // Handle objects
298
+ if (typeof value === 'object') {
299
+ const result: Record<string, unknown> = {}
300
+ for (const [key, val] of Object.entries(value)) {
301
+ result[key] = this.serializeValue(val, depth + 1)
302
+ }
303
+ return result
304
+ }
305
+
306
+ return value
307
+ }
308
+
309
+ /**
310
+ * Deserialize checkpoint data.
311
+ */
312
+ private deserializeCheckpoint(data: CheckpointData): Checkpoint {
313
+ return {
314
+ ...data,
315
+ channel_values: this.deserializeValue(data.channel_values) as Record<string, unknown>,
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Deserialize a value.
321
+ */
322
+ private deserializeValue(value: unknown): unknown {
323
+ if (value === null || value === undefined) {
324
+ return value
325
+ }
326
+
327
+ if (typeof value === 'object' && !Array.isArray(value)) {
328
+ const obj = value as Record<string, unknown>
329
+
330
+ // Decode base64 bytes
331
+ if ('__bytes__' in obj && typeof obj.__bytes__ === 'string') {
332
+ return Buffer.from(obj.__bytes__, 'base64')
333
+ }
334
+
335
+ // Recurse into object
336
+ const result: Record<string, unknown> = {}
337
+ for (const [key, val] of Object.entries(obj)) {
338
+ result[key] = this.deserializeValue(val)
339
+ }
340
+ return result
341
+ }
342
+
343
+ if (Array.isArray(value)) {
344
+ return value.map(item => this.deserializeValue(item))
345
+ }
346
+
347
+ return value
348
+ }
349
+ }
350
+
351
+ export type { RunnableConfig, Checkpoint, CheckpointMetadata, CheckpointTuple, PendingWrite }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * March Agent SDK - Vercel AI SDK Extension
3
+ *
4
+ * VercelAIMessageStore for persistent message history with Vercel AI SDK.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ import type { MarchAgentApp } from '../app.js'
10
+ import { AgentStateClient } from '../agent-state-client.js'
11
+
12
+ // Type definitions compatible with Vercel AI SDK's CoreMessage
13
+ interface CoreMessage {
14
+ role: 'system' | 'user' | 'assistant' | 'tool'
15
+ content: string | Array<{ type: string;[key: string]: unknown }>
16
+ [key: string]: unknown
17
+ }
18
+
19
+ /**
20
+ * Persistent message store for Vercel AI SDK.
21
+ *
22
+ * Stores and retrieves AI SDK message history using the agent-state API.
23
+ * Messages are serialized as JSON for full fidelity.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * import { MarchAgentApp } from 'march-ai-sdk'
28
+ * import { VercelAIMessageStore } from 'march-ai-sdk/extensions/vercel-ai'
29
+ * import { streamText } from 'ai'
30
+ * import { openai } from '@ai-sdk/openai'
31
+ *
32
+ * const app = new MarchAgentApp({
33
+ * gatewayUrl: 'agent-gateway:8080',
34
+ * apiKey: 'your-key',
35
+ * })
36
+ *
37
+ * const store = new VercelAIMessageStore(app)
38
+ * const agent = app.registerMe({ ... })
39
+ *
40
+ * agent.onMessage(async (message, sender) => {
41
+ * // Load message history
42
+ * const history = await store.load(message.conversationId)
43
+ *
44
+ * // Add user message
45
+ * const messages: CoreMessage[] = [
46
+ * ...history,
47
+ * { role: 'user', content: message.content }
48
+ * ]
49
+ *
50
+ * // Stream response
51
+ * const streamer = agent.streamer(message)
52
+ *
53
+ * const result = await streamText({
54
+ * model: openai('gpt-4o'),
55
+ * messages,
56
+ * onChunk: ({ chunk }) => {
57
+ * if (chunk.type === 'text-delta') {
58
+ * streamer.stream(chunk.textDelta)
59
+ * }
60
+ * }
61
+ * })
62
+ *
63
+ * await streamer.finish()
64
+ *
65
+ * // Save updated history
66
+ * await store.save(message.conversationId, [
67
+ * ...messages,
68
+ * { role: 'assistant', content: result.text }
69
+ * ])
70
+ * })
71
+ *
72
+ * app.run()
73
+ * ```
74
+ */
75
+ export class VercelAIMessageStore {
76
+ private static readonly NAMESPACE = 'vercel_ai'
77
+ private readonly client: AgentStateClient
78
+
79
+ constructor(app: MarchAgentApp) {
80
+ this.client = new AgentStateClient(app.gatewayClient.conversationStoreUrl)
81
+ }
82
+
83
+ /**
84
+ * Load message history for a conversation.
85
+ *
86
+ * @param conversationId - The conversation ID to load history for
87
+ * @returns Array of CoreMessage objects (empty array if no history)
88
+ */
89
+ async load(conversationId: string): Promise<CoreMessage[]> {
90
+ const result = await this.client.get(conversationId, VercelAIMessageStore.NAMESPACE)
91
+
92
+ if (!result) {
93
+ return []
94
+ }
95
+
96
+ const state = result.state ?? {}
97
+ const messages = state.messages as unknown[]
98
+
99
+ if (!Array.isArray(messages)) {
100
+ return []
101
+ }
102
+
103
+ // Validate and return messages
104
+ return messages.filter(this.isValidMessage) as CoreMessage[]
105
+ }
106
+
107
+ /**
108
+ * Save message history for a conversation.
109
+ *
110
+ * @param conversationId - The conversation ID to save history for
111
+ * @param messages - Array of CoreMessage objects to save
112
+ */
113
+ async save(conversationId: string, messages: CoreMessage[]): Promise<void> {
114
+ await this.client.put(conversationId, VercelAIMessageStore.NAMESPACE, {
115
+ messages: messages.map(this.serializeMessage),
116
+ })
117
+ }
118
+
119
+ /**
120
+ * Clear message history for a conversation.
121
+ *
122
+ * @param conversationId - The conversation ID to clear history for
123
+ */
124
+ async clear(conversationId: string): Promise<void> {
125
+ await this.client.delete(conversationId, VercelAIMessageStore.NAMESPACE)
126
+ }
127
+
128
+ /**
129
+ * Append messages to existing history.
130
+ *
131
+ * @param conversationId - The conversation ID
132
+ * @param newMessages - Messages to append
133
+ */
134
+ async append(conversationId: string, newMessages: CoreMessage[]): Promise<void> {
135
+ const existing = await this.load(conversationId)
136
+ await this.save(conversationId, [...existing, ...newMessages])
137
+ }
138
+
139
+ /**
140
+ * Get the last N messages from history.
141
+ *
142
+ * @param conversationId - The conversation ID
143
+ * @param count - Number of messages to retrieve
144
+ */
145
+ async getLastMessages(conversationId: string, count: number): Promise<CoreMessage[]> {
146
+ const history = await this.load(conversationId)
147
+ return history.slice(-count)
148
+ }
149
+
150
+ /**
151
+ * Validate that an object is a valid message.
152
+ */
153
+ private isValidMessage(msg: unknown): msg is CoreMessage {
154
+ if (typeof msg !== 'object' || msg === null) {
155
+ return false
156
+ }
157
+
158
+ const m = msg as Record<string, unknown>
159
+ const validRoles = ['system', 'user', 'assistant', 'tool']
160
+
161
+ return (
162
+ typeof m.role === 'string' &&
163
+ validRoles.includes(m.role) &&
164
+ (typeof m.content === 'string' || Array.isArray(m.content))
165
+ )
166
+ }
167
+
168
+ /**
169
+ * Serialize a message for storage.
170
+ */
171
+ private serializeMessage(msg: CoreMessage): Record<string, unknown> {
172
+ // Return a plain object copy
173
+ return { ...msg }
174
+ }
175
+ }
176
+
177
+ export type { CoreMessage }