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.
- package/README.md +732 -0
- package/dist/app-C_umwZXh.d.ts +790 -0
- package/dist/extensions/langgraph.d.ts +144 -0
- package/dist/extensions/langgraph.js +326 -0
- package/dist/extensions/langgraph.js.map +1 -0
- package/dist/extensions/vercel-ai.d.ts +124 -0
- package/dist/extensions/vercel-ai.js +177 -0
- package/dist/extensions/vercel-ai.js.map +1 -0
- package/dist/index.d.ts +260 -0
- package/dist/index.js +1695 -0
- package/dist/index.js.map +1 -0
- package/dist/proto/gateway.proto +99 -0
- package/package.json +83 -0
- package/src/agent-state-client.ts +115 -0
- package/src/agent.ts +293 -0
- package/src/api-paths.ts +60 -0
- package/src/app.ts +235 -0
- package/src/artifact.ts +59 -0
- package/src/attachment-client.ts +78 -0
- package/src/checkpoint-client.ts +175 -0
- package/src/conversation-client.ts +109 -0
- package/src/conversation-message.ts +61 -0
- package/src/conversation.ts +123 -0
- package/src/exceptions.ts +78 -0
- package/src/extensions/index.ts +6 -0
- package/src/extensions/langgraph.ts +351 -0
- package/src/extensions/vercel-ai.ts +177 -0
- package/src/gateway-client.ts +420 -0
- package/src/heartbeat.ts +89 -0
- package/src/index.ts +70 -0
- package/src/memory-client.ts +125 -0
- package/src/memory.ts +68 -0
- package/src/message.ts +178 -0
- package/src/proto/gateway.proto +99 -0
- package/src/streamer.ts +242 -0
- package/src/types.ts +196 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* March Agent SDK - Gateway Client
|
|
3
|
+
* Port of Python march_agent/gateway_client.py
|
|
4
|
+
*
|
|
5
|
+
* gRPC client for communicating with the Agent Gateway.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as grpc from '@grpc/grpc-js'
|
|
9
|
+
import * as protoLoader from '@grpc/proto-loader'
|
|
10
|
+
import { fileURLToPath } from 'url'
|
|
11
|
+
import { dirname, join } from 'path'
|
|
12
|
+
import { GatewayError } from './exceptions.js'
|
|
13
|
+
import type { KafkaMessage, ProduceAck } from './types.js'
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
16
|
+
const __dirname = dirname(__filename)
|
|
17
|
+
|
|
18
|
+
// Load proto file
|
|
19
|
+
const PROTO_PATH = join(__dirname, 'proto', 'gateway.proto')
|
|
20
|
+
|
|
21
|
+
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
|
|
22
|
+
keepCase: true,
|
|
23
|
+
longs: String,
|
|
24
|
+
enums: String,
|
|
25
|
+
defaults: true,
|
|
26
|
+
oneofs: true,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const gatewayProto = grpc.loadPackageDefinition(packageDefinition) as unknown as {
|
|
30
|
+
gateway: {
|
|
31
|
+
AgentGateway: new (
|
|
32
|
+
address: string,
|
|
33
|
+
credentials: grpc.ChannelCredentials
|
|
34
|
+
) => AgentGatewayClient
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface AgentGatewayClient {
|
|
39
|
+
AgentStream(): grpc.ClientDuplexStream<ClientMessage, ServerMessage>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ClientMessage {
|
|
43
|
+
auth?: { api_key: string; agent_names: string[] }
|
|
44
|
+
subscribe?: { agent_name: string }
|
|
45
|
+
unsubscribe?: { agent_name: string }
|
|
46
|
+
produce?: {
|
|
47
|
+
topic: string
|
|
48
|
+
key: string
|
|
49
|
+
headers: Record<string, string>
|
|
50
|
+
body: Buffer
|
|
51
|
+
correlation_id: string
|
|
52
|
+
}
|
|
53
|
+
ping?: { timestamp: string }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ServerMessage {
|
|
57
|
+
auth_response?: { connection_id: string; subscribed_topics: string[] }
|
|
58
|
+
message?: {
|
|
59
|
+
topic: string
|
|
60
|
+
partition: number
|
|
61
|
+
offset: string
|
|
62
|
+
key: string
|
|
63
|
+
headers: Record<string, string>
|
|
64
|
+
body: Buffer
|
|
65
|
+
timestamp: string
|
|
66
|
+
}
|
|
67
|
+
produce_ack?: {
|
|
68
|
+
topic: string
|
|
69
|
+
partition: number
|
|
70
|
+
offset: string
|
|
71
|
+
correlation_id: string
|
|
72
|
+
}
|
|
73
|
+
subscribe_ack?: { topic: string }
|
|
74
|
+
unsubscribe_ack?: { agent_name: string }
|
|
75
|
+
pong?: { client_timestamp: string; server_timestamp: string }
|
|
76
|
+
error?: { code: string; message: string; correlation_id: string }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Client for communicating with the Agent Gateway.
|
|
81
|
+
* Provides gRPC bidirectional streaming for Kafka consume/produce.
|
|
82
|
+
*/
|
|
83
|
+
export class GatewayClient {
|
|
84
|
+
private readonly gatewayUrl: string
|
|
85
|
+
private readonly apiKey: string
|
|
86
|
+
private readonly secure: boolean
|
|
87
|
+
|
|
88
|
+
private client?: AgentGatewayClient
|
|
89
|
+
private stream?: grpc.ClientDuplexStream<ClientMessage, ServerMessage>
|
|
90
|
+
private _connectionId?: string
|
|
91
|
+
private messageQueue: KafkaMessage[] = []
|
|
92
|
+
private pendingProduceAcks: Map<string, (ack: ProduceAck) => void> = new Map()
|
|
93
|
+
private pendingSubscribeAcks: Map<string, (topic: string) => void> = new Map()
|
|
94
|
+
private messageHandlers: Map<string, (msg: KafkaMessage) => void> = new Map()
|
|
95
|
+
private correlationCounter: number = 0
|
|
96
|
+
private connected: boolean = false
|
|
97
|
+
|
|
98
|
+
constructor(gatewayUrl: string, apiKey: string, secure: boolean = false) {
|
|
99
|
+
this.gatewayUrl = gatewayUrl
|
|
100
|
+
this.apiKey = apiKey
|
|
101
|
+
this.secure = secure
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* HTTP URL for AI Inventory service via proxy.
|
|
106
|
+
*/
|
|
107
|
+
get aiInventoryUrl(): string {
|
|
108
|
+
const protocol = this.secure ? 'https' : 'http'
|
|
109
|
+
return `${protocol}://${this.gatewayUrl}/s/ai-inventory`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* HTTP URL for Conversation Store service via proxy.
|
|
114
|
+
*/
|
|
115
|
+
get conversationStoreUrl(): string {
|
|
116
|
+
const protocol = this.secure ? 'https' : 'http'
|
|
117
|
+
return `${protocol}://${this.gatewayUrl}/s/conversation-store`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* HTTP URL for AI Memory service via proxy.
|
|
122
|
+
*/
|
|
123
|
+
get aiMemoryUrl(): string {
|
|
124
|
+
const protocol = this.secure ? 'https' : 'http'
|
|
125
|
+
return `${protocol}://${this.gatewayUrl}/s/ai-memory`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* HTTP URL for Attachment service via proxy.
|
|
130
|
+
*/
|
|
131
|
+
get attachmentUrl(): string {
|
|
132
|
+
const protocol = this.secure ? 'https' : 'http'
|
|
133
|
+
return `${protocol}://${this.gatewayUrl}/s/attachment`
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Register a handler for a topic.
|
|
138
|
+
*/
|
|
139
|
+
registerHandler(topic: string, handler: (msg: KafkaMessage) => void): void {
|
|
140
|
+
this.messageHandlers.set(topic, handler)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Connect to the gateway and authenticate.
|
|
145
|
+
*/
|
|
146
|
+
async connect(agentNames: string[]): Promise<string[]> {
|
|
147
|
+
const credentials = this.secure
|
|
148
|
+
? grpc.credentials.createSsl()
|
|
149
|
+
: grpc.credentials.createInsecure()
|
|
150
|
+
|
|
151
|
+
this.client = new gatewayProto.gateway.AgentGateway(
|
|
152
|
+
this.gatewayUrl,
|
|
153
|
+
credentials
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
this.stream = this.client.AgentStream()
|
|
157
|
+
|
|
158
|
+
// Set up message handling
|
|
159
|
+
this.stream.on('data', (msg: ServerMessage) => {
|
|
160
|
+
this.handleServerMessage(msg)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
this.stream.on('error', (err: Error) => {
|
|
164
|
+
console.error('Gateway stream error:', err)
|
|
165
|
+
this.connected = false
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
this.stream.on('end', () => {
|
|
169
|
+
console.log('Gateway stream ended')
|
|
170
|
+
this.connected = false
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// Send auth request
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
const timeout = setTimeout(() => {
|
|
176
|
+
reject(new GatewayError('Authentication timeout'))
|
|
177
|
+
}, 10000)
|
|
178
|
+
|
|
179
|
+
const authHandler = (msg: ServerMessage) => {
|
|
180
|
+
if (msg.auth_response) {
|
|
181
|
+
clearTimeout(timeout)
|
|
182
|
+
this._connectionId = msg.auth_response.connection_id
|
|
183
|
+
this.connected = true
|
|
184
|
+
resolve(msg.auth_response.subscribed_topics)
|
|
185
|
+
} else if (msg.error) {
|
|
186
|
+
clearTimeout(timeout)
|
|
187
|
+
reject(new GatewayError(`Authentication failed: ${msg.error.message}`))
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Temporarily override handler for auth response
|
|
192
|
+
const originalHandler = this.handleServerMessage.bind(this)
|
|
193
|
+
this.handleServerMessage = (msg: ServerMessage) => {
|
|
194
|
+
if (msg.auth_response || msg.error) {
|
|
195
|
+
authHandler(msg)
|
|
196
|
+
this.handleServerMessage = originalHandler
|
|
197
|
+
} else {
|
|
198
|
+
originalHandler(msg)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.stream!.write({
|
|
203
|
+
auth: {
|
|
204
|
+
api_key: this.apiKey,
|
|
205
|
+
agent_names: agentNames,
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Handle incoming server messages.
|
|
213
|
+
*/
|
|
214
|
+
private handleServerMessage(msg: ServerMessage): void {
|
|
215
|
+
if (msg.message) {
|
|
216
|
+
const kafkaMsg: KafkaMessage = {
|
|
217
|
+
topic: msg.message.topic,
|
|
218
|
+
partition: msg.message.partition,
|
|
219
|
+
offset: parseInt(msg.message.offset, 10),
|
|
220
|
+
key: msg.message.key,
|
|
221
|
+
headers: msg.message.headers,
|
|
222
|
+
body: JSON.parse(msg.message.body.toString()),
|
|
223
|
+
timestamp: parseInt(msg.message.timestamp, 10),
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check for registered handler
|
|
227
|
+
const handler = this.messageHandlers.get(kafkaMsg.topic)
|
|
228
|
+
if (handler) {
|
|
229
|
+
handler(kafkaMsg)
|
|
230
|
+
} else {
|
|
231
|
+
// Queue message if no handler
|
|
232
|
+
this.messageQueue.push(kafkaMsg)
|
|
233
|
+
}
|
|
234
|
+
} else if (msg.produce_ack) {
|
|
235
|
+
const callback = this.pendingProduceAcks.get(msg.produce_ack.correlation_id)
|
|
236
|
+
if (callback) {
|
|
237
|
+
callback({
|
|
238
|
+
topic: msg.produce_ack.topic,
|
|
239
|
+
partition: msg.produce_ack.partition,
|
|
240
|
+
offset: parseInt(msg.produce_ack.offset, 10),
|
|
241
|
+
correlationId: msg.produce_ack.correlation_id,
|
|
242
|
+
})
|
|
243
|
+
this.pendingProduceAcks.delete(msg.produce_ack.correlation_id)
|
|
244
|
+
}
|
|
245
|
+
} else if (msg.subscribe_ack) {
|
|
246
|
+
const callback = this.pendingSubscribeAcks.get(msg.subscribe_ack.topic)
|
|
247
|
+
if (callback) {
|
|
248
|
+
callback(msg.subscribe_ack.topic)
|
|
249
|
+
this.pendingSubscribeAcks.delete(msg.subscribe_ack.topic)
|
|
250
|
+
}
|
|
251
|
+
} else if (msg.error) {
|
|
252
|
+
console.error('Gateway error:', msg.error.message)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Subscribe to an additional agent's topic.
|
|
258
|
+
*/
|
|
259
|
+
async subscribe(agentName: string): Promise<string> {
|
|
260
|
+
if (!this.stream || !this.connected) {
|
|
261
|
+
throw new GatewayError('Not connected')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
const timeout = setTimeout(() => {
|
|
266
|
+
reject(new GatewayError('Subscribe timeout'))
|
|
267
|
+
}, 5000)
|
|
268
|
+
|
|
269
|
+
const expectedTopic = `${agentName}.inbox`
|
|
270
|
+
this.pendingSubscribeAcks.set(expectedTopic, (topic) => {
|
|
271
|
+
clearTimeout(timeout)
|
|
272
|
+
resolve(topic)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
this.stream!.write({
|
|
276
|
+
subscribe: { agent_name: agentName },
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Unsubscribe from an agent's topic.
|
|
283
|
+
*/
|
|
284
|
+
unsubscribe(agentName: string): void {
|
|
285
|
+
if (!this.stream || !this.connected) {
|
|
286
|
+
throw new GatewayError('Not connected')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this.stream.write({
|
|
290
|
+
unsubscribe: { agent_name: agentName },
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Produce a message to Kafka via the gateway.
|
|
296
|
+
*/
|
|
297
|
+
produce(
|
|
298
|
+
topic: string,
|
|
299
|
+
key: string,
|
|
300
|
+
headers: Record<string, string>,
|
|
301
|
+
body: Record<string, unknown>,
|
|
302
|
+
correlationId?: string
|
|
303
|
+
): void {
|
|
304
|
+
if (!this.stream || !this.connected) {
|
|
305
|
+
throw new GatewayError('Not connected')
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const corrId = correlationId ?? `${++this.correlationCounter}`
|
|
309
|
+
|
|
310
|
+
this.stream.write({
|
|
311
|
+
produce: {
|
|
312
|
+
topic,
|
|
313
|
+
key,
|
|
314
|
+
headers,
|
|
315
|
+
body: Buffer.from(JSON.stringify(body)),
|
|
316
|
+
correlation_id: corrId,
|
|
317
|
+
},
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Produce a message and wait for acknowledgment.
|
|
323
|
+
*/
|
|
324
|
+
async produceAndWait(
|
|
325
|
+
topic: string,
|
|
326
|
+
key: string,
|
|
327
|
+
headers: Record<string, string>,
|
|
328
|
+
body: Record<string, unknown>
|
|
329
|
+
): Promise<ProduceAck> {
|
|
330
|
+
if (!this.stream || !this.connected) {
|
|
331
|
+
throw new GatewayError('Not connected')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const correlationId = `${++this.correlationCounter}`
|
|
335
|
+
|
|
336
|
+
return new Promise((resolve, reject) => {
|
|
337
|
+
const timeout = setTimeout(() => {
|
|
338
|
+
this.pendingProduceAcks.delete(correlationId)
|
|
339
|
+
reject(new GatewayError('Produce timeout'))
|
|
340
|
+
}, 10000)
|
|
341
|
+
|
|
342
|
+
this.pendingProduceAcks.set(correlationId, (ack) => {
|
|
343
|
+
clearTimeout(timeout)
|
|
344
|
+
resolve(ack)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
this.stream!.write({
|
|
348
|
+
produce: {
|
|
349
|
+
topic,
|
|
350
|
+
key,
|
|
351
|
+
headers,
|
|
352
|
+
body: Buffer.from(JSON.stringify(body)),
|
|
353
|
+
correlation_id: correlationId,
|
|
354
|
+
},
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Consume a single message (polling from queue).
|
|
361
|
+
*/
|
|
362
|
+
consumeOne(_timeout: number = 1000): KafkaMessage | null {
|
|
363
|
+
if (this.messageQueue.length > 0) {
|
|
364
|
+
return this.messageQueue.shift()!
|
|
365
|
+
}
|
|
366
|
+
return null
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Send a ping to the gateway.
|
|
371
|
+
*/
|
|
372
|
+
ping(): void {
|
|
373
|
+
if (!this.stream || !this.connected) {
|
|
374
|
+
throw new GatewayError('Not connected')
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
this.stream.write({
|
|
378
|
+
ping: { timestamp: String(Date.now()) },
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Make a sync POST request (used for registration).
|
|
384
|
+
*/
|
|
385
|
+
async httpPost(
|
|
386
|
+
service: string,
|
|
387
|
+
path: string,
|
|
388
|
+
body: unknown
|
|
389
|
+
): Promise<Response> {
|
|
390
|
+
const protocol = this.secure ? 'https' : 'http'
|
|
391
|
+
const url = `${protocol}://${this.gatewayUrl}/s/${service}${path}`
|
|
392
|
+
|
|
393
|
+
return fetch(url, {
|
|
394
|
+
method: 'POST',
|
|
395
|
+
headers: {
|
|
396
|
+
'Content-Type': 'application/json',
|
|
397
|
+
'X-API-Key': this.apiKey,
|
|
398
|
+
},
|
|
399
|
+
body: JSON.stringify(body),
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Check if connected.
|
|
405
|
+
*/
|
|
406
|
+
isConnected(): boolean {
|
|
407
|
+
return this.connected
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Close the gateway connection.
|
|
412
|
+
*/
|
|
413
|
+
close(): void {
|
|
414
|
+
if (this.stream) {
|
|
415
|
+
this.stream.end()
|
|
416
|
+
this.stream = undefined
|
|
417
|
+
}
|
|
418
|
+
this.connected = false
|
|
419
|
+
}
|
|
420
|
+
}
|
package/src/heartbeat.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* March Agent SDK - Heartbeat Manager
|
|
3
|
+
* Port of Python march_agent/heartbeat.py
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GatewayClient } from './gateway-client.js'
|
|
7
|
+
import { AI_INVENTORY_PATHS, SERVICES } from './api-paths.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Manages periodic heartbeats to keep the agent status active.
|
|
11
|
+
* Sends heartbeats via HTTP to the AI Inventory service.
|
|
12
|
+
*/
|
|
13
|
+
export class HeartbeatManager {
|
|
14
|
+
private readonly gatewayClient: GatewayClient
|
|
15
|
+
private readonly agentName: string
|
|
16
|
+
private readonly intervalMs: number
|
|
17
|
+
private timer?: ReturnType<typeof setInterval>
|
|
18
|
+
private running: boolean = false
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
gatewayClient: GatewayClient,
|
|
22
|
+
agentName: string,
|
|
23
|
+
intervalSeconds: number = 60
|
|
24
|
+
) {
|
|
25
|
+
this.gatewayClient = gatewayClient
|
|
26
|
+
this.agentName = agentName
|
|
27
|
+
this.intervalMs = intervalSeconds * 1000
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Start sending heartbeats.
|
|
32
|
+
*/
|
|
33
|
+
start(): void {
|
|
34
|
+
if (this.running) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.running = true
|
|
39
|
+
this.timer = setInterval(() => {
|
|
40
|
+
this.sendHeartbeat()
|
|
41
|
+
}, this.intervalMs)
|
|
42
|
+
|
|
43
|
+
// Send first heartbeat immediately
|
|
44
|
+
this.sendHeartbeat()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Stop sending heartbeats.
|
|
49
|
+
*/
|
|
50
|
+
stop(): void {
|
|
51
|
+
this.running = false
|
|
52
|
+
if (this.timer) {
|
|
53
|
+
clearInterval(this.timer)
|
|
54
|
+
this.timer = undefined
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Send a single heartbeat via HTTP to AI Inventory.
|
|
60
|
+
*/
|
|
61
|
+
private async sendHeartbeat(): Promise<void> {
|
|
62
|
+
if (!this.running) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const response = await this.gatewayClient.httpPost(
|
|
68
|
+
SERVICES.AI_INVENTORY,
|
|
69
|
+
AI_INVENTORY_PATHS.HEALTH_HEARTBEAT,
|
|
70
|
+
{ name: this.agentName }
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if (response.status === 404) {
|
|
74
|
+
console.warn(`Agent '${this.agentName}' not found. Re-registration may be needed.`)
|
|
75
|
+
} else if (!response.ok) {
|
|
76
|
+
console.warn(`Heartbeat returned status ${response.status}`)
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('Heartbeat failed:', error)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if heartbeat is running.
|
|
85
|
+
*/
|
|
86
|
+
isRunning(): boolean {
|
|
87
|
+
return this.running
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* March Agent SDK - TypeScript framework for building AI agents
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Main classes
|
|
8
|
+
export { MarchAgentApp } from './app.js'
|
|
9
|
+
export { Agent, SenderFilter } from './agent.js'
|
|
10
|
+
export { Message } from './message.js'
|
|
11
|
+
export { Streamer } from './streamer.js'
|
|
12
|
+
export { Conversation } from './conversation.js'
|
|
13
|
+
export { Memory } from './memory.js'
|
|
14
|
+
|
|
15
|
+
// Message types
|
|
16
|
+
export { ConversationMessage } from './conversation-message.js'
|
|
17
|
+
export { type Artifact, type ArtifactType, type ArtifactInput, toArtifact } from './artifact.js'
|
|
18
|
+
|
|
19
|
+
// Clients
|
|
20
|
+
export { GatewayClient } from './gateway-client.js'
|
|
21
|
+
export { ConversationClient } from './conversation-client.js'
|
|
22
|
+
export { CheckpointClient, type CheckpointConfig, type CheckpointData, type CheckpointMetadata, type CheckpointTuple } from './checkpoint-client.js'
|
|
23
|
+
export { AgentStateClient, type AgentStateResponse } from './agent-state-client.js'
|
|
24
|
+
export { MemoryClient } from './memory-client.js'
|
|
25
|
+
export { AttachmentClient, createAttachmentInfo, type AttachmentInfo } from './attachment-client.js'
|
|
26
|
+
|
|
27
|
+
// Helpers
|
|
28
|
+
export { HeartbeatManager } from './heartbeat.js'
|
|
29
|
+
|
|
30
|
+
// API Paths Configuration
|
|
31
|
+
export {
|
|
32
|
+
AI_INVENTORY_PATHS,
|
|
33
|
+
CONVERSATION_STORE_PATHS,
|
|
34
|
+
MEMORY_PATHS,
|
|
35
|
+
SERVICES,
|
|
36
|
+
type ServiceName,
|
|
37
|
+
} from './api-paths.js'
|
|
38
|
+
|
|
39
|
+
// Types
|
|
40
|
+
export type {
|
|
41
|
+
KafkaMessage,
|
|
42
|
+
KafkaHeaders,
|
|
43
|
+
AgentRegistrationData,
|
|
44
|
+
RegisterOptions,
|
|
45
|
+
MessageHandler,
|
|
46
|
+
SenderFilterOptions,
|
|
47
|
+
StreamOptions,
|
|
48
|
+
StreamerOptions,
|
|
49
|
+
AppOptions,
|
|
50
|
+
ConversationData,
|
|
51
|
+
GetMessagesOptions,
|
|
52
|
+
MemoryMessage,
|
|
53
|
+
MemorySearchResult,
|
|
54
|
+
UserSummary,
|
|
55
|
+
ProduceAck,
|
|
56
|
+
} from './types.js'
|
|
57
|
+
|
|
58
|
+
// Exceptions
|
|
59
|
+
export {
|
|
60
|
+
MarchAgentError,
|
|
61
|
+
RegistrationError,
|
|
62
|
+
KafkaError,
|
|
63
|
+
ConfigurationError,
|
|
64
|
+
APIException,
|
|
65
|
+
HeartbeatError,
|
|
66
|
+
GatewayError,
|
|
67
|
+
} from './exceptions.js'
|
|
68
|
+
|
|
69
|
+
// Version
|
|
70
|
+
export const VERSION = '0.3.0'
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* March Agent SDK - Memory Client
|
|
3
|
+
* Port of Python march_agent/memory_client.py
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { APIException } from './exceptions.js'
|
|
7
|
+
import type { MemoryMessage, MemorySearchResult, UserSummary } from './types.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Async HTTP client for AI memory API.
|
|
11
|
+
* Provides long-term memory storage and semantic search.
|
|
12
|
+
*/
|
|
13
|
+
export class MemoryClient {
|
|
14
|
+
private readonly baseUrl: string
|
|
15
|
+
|
|
16
|
+
constructor(baseUrl: string) {
|
|
17
|
+
this.baseUrl = baseUrl.replace(/\/$/, '')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Add messages to memory for a user/conversation.
|
|
22
|
+
*/
|
|
23
|
+
async addMessages(
|
|
24
|
+
userId: string,
|
|
25
|
+
conversationId: string,
|
|
26
|
+
messages: MemoryMessage[]
|
|
27
|
+
): Promise<{ added: number }> {
|
|
28
|
+
const url = `${this.baseUrl}/memory/${userId}/messages`
|
|
29
|
+
const payload = {
|
|
30
|
+
conversation_id: conversationId,
|
|
31
|
+
messages,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(url, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
body: JSON.stringify(payload),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
const errorText = await response.text()
|
|
43
|
+
throw new APIException(`Failed to add messages to memory: ${response.status} - ${errorText}`, response.status)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return await response.json() as { added: number }
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error instanceof APIException) throw error
|
|
49
|
+
throw new APIException(`Failed to add messages to memory: ${error}`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Search memory for relevant content.
|
|
55
|
+
*/
|
|
56
|
+
async search(
|
|
57
|
+
userId: string,
|
|
58
|
+
query: string,
|
|
59
|
+
limit: number = 10
|
|
60
|
+
): Promise<MemorySearchResult[]> {
|
|
61
|
+
const url = new URL(`${this.baseUrl}/memory/${userId}/search`)
|
|
62
|
+
url.searchParams.set('query', query)
|
|
63
|
+
url.searchParams.set('limit', String(limit))
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch(url.toString())
|
|
67
|
+
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
const errorText = await response.text()
|
|
70
|
+
throw new APIException(`Failed to search memory: ${response.status} - ${errorText}`, response.status)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return await response.json() as MemorySearchResult[]
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof APIException) throw error
|
|
76
|
+
throw new APIException(`Failed to search memory: ${error}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get user summary.
|
|
82
|
+
*/
|
|
83
|
+
async getUserSummary(userId: string): Promise<UserSummary | null> {
|
|
84
|
+
const url = `${this.baseUrl}/memory/${userId}/summary`
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const response = await fetch(url)
|
|
88
|
+
|
|
89
|
+
if (response.status === 404) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const errorText = await response.text()
|
|
95
|
+
throw new APIException(`Failed to get user summary: ${response.status} - ${errorText}`, response.status)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return await response.json() as UserSummary
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error instanceof APIException) throw error
|
|
101
|
+
throw new APIException(`Failed to get user summary: ${error}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Clear user's memory.
|
|
107
|
+
*/
|
|
108
|
+
async clearMemory(userId: string): Promise<{ deleted: number }> {
|
|
109
|
+
const url = `${this.baseUrl}/memory/${userId}`
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch(url, { method: 'DELETE' })
|
|
113
|
+
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const errorText = await response.text()
|
|
116
|
+
throw new APIException(`Failed to clear memory: ${response.status} - ${errorText}`, response.status)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return await response.json() as { deleted: number }
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error instanceof APIException) throw error
|
|
122
|
+
throw new APIException(`Failed to clear memory: ${error}`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|