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/src/memory.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * March Agent SDK - Memory Helper
3
+ * Port of Python march_agent/memory.py
4
+ */
5
+
6
+ import { MemoryClient } from './memory-client.js'
7
+ import type { MemoryMessage, MemorySearchResult, UserSummary } from './types.js'
8
+
9
+ /**
10
+ * Helper class for accessing long-term memory.
11
+ * Provides convenient methods for storing and searching memories.
12
+ */
13
+ export class Memory {
14
+ readonly userId: string
15
+ readonly conversationId: string
16
+ private readonly client: MemoryClient
17
+
18
+ constructor(
19
+ userId: string,
20
+ conversationId: string,
21
+ client: MemoryClient
22
+ ) {
23
+ this.userId = userId
24
+ this.conversationId = conversationId
25
+ this.client = client
26
+ }
27
+
28
+ /**
29
+ * Add messages to memory.
30
+ */
31
+ async addMessages(messages: MemoryMessage[]): Promise<number> {
32
+ const result = await this.client.addMessages(
33
+ this.userId,
34
+ this.conversationId,
35
+ messages
36
+ )
37
+ return result.added
38
+ }
39
+
40
+ /**
41
+ * Add a single message to memory.
42
+ */
43
+ async addMessage(role: 'user' | 'assistant', content: string): Promise<void> {
44
+ await this.addMessages([{ role, content }])
45
+ }
46
+
47
+ /**
48
+ * Search memory for relevant content.
49
+ */
50
+ async search(query: string, limit: number = 10): Promise<MemorySearchResult[]> {
51
+ return this.client.search(this.userId, query, limit)
52
+ }
53
+
54
+ /**
55
+ * Get user summary.
56
+ */
57
+ async getSummary(): Promise<UserSummary | null> {
58
+ return this.client.getUserSummary(this.userId)
59
+ }
60
+
61
+ /**
62
+ * Clear all memories for this user.
63
+ */
64
+ async clear(): Promise<number> {
65
+ const result = await this.client.clearMemory(this.userId)
66
+ return result.deleted
67
+ }
68
+ }
package/src/message.ts ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * March Agent SDK - Message
3
+ * Port of Python march_agent/message.py
4
+ */
5
+
6
+ import { Conversation } from './conversation.js'
7
+ import { Memory } from './memory.js'
8
+ import { ConversationClient } from './conversation-client.js'
9
+ import { MemoryClient } from './memory-client.js'
10
+ import { AttachmentClient, createAttachmentInfo } from './attachment-client.js'
11
+ import type { AttachmentInfo, KafkaHeaders } from './types.js'
12
+
13
+ /**
14
+ * Represents an incoming message to the agent.
15
+ */
16
+ export class Message {
17
+ readonly content: string
18
+ readonly conversationId: string
19
+ readonly userId: string
20
+ readonly headers: Record<string, string>
21
+ readonly rawBody: Record<string, unknown>
22
+ readonly conversation?: Conversation
23
+ readonly memory?: Memory
24
+ readonly metadata?: Record<string, unknown>
25
+ readonly schema?: Record<string, unknown>
26
+ readonly attachment?: AttachmentInfo
27
+
28
+ private readonly attachmentClient?: AttachmentClient
29
+
30
+ constructor(options: {
31
+ content: string
32
+ conversationId: string
33
+ userId: string
34
+ headers: Record<string, string>
35
+ rawBody: Record<string, unknown>
36
+ conversation?: Conversation
37
+ memory?: Memory
38
+ metadata?: Record<string, unknown>
39
+ schema?: Record<string, unknown>
40
+ attachment?: AttachmentInfo
41
+ attachmentClient?: AttachmentClient
42
+ }) {
43
+ this.content = options.content
44
+ this.conversationId = options.conversationId
45
+ this.userId = options.userId
46
+ this.headers = options.headers
47
+ this.rawBody = options.rawBody
48
+ this.conversation = options.conversation
49
+ this.memory = options.memory
50
+ this.metadata = options.metadata
51
+ this.schema = options.schema
52
+ this.attachment = options.attachment
53
+ this.attachmentClient = options.attachmentClient
54
+ }
55
+
56
+ /**
57
+ * Create Message from Kafka message data.
58
+ */
59
+ static fromKafkaMessage(
60
+ body: Record<string, unknown>,
61
+ headers: KafkaHeaders,
62
+ options: {
63
+ conversationClient?: ConversationClient
64
+ memoryClient?: MemoryClient
65
+ attachmentClient?: AttachmentClient
66
+ agentName?: string
67
+ } = {}
68
+ ): Message {
69
+ const conversationId = headers.conversationId ?? ''
70
+ const userId = headers.userId ?? 'anonymous'
71
+
72
+ // Parse metadata from header
73
+ let metadata: Record<string, unknown> | undefined
74
+ if (headers.messageMetadata) {
75
+ try {
76
+ metadata = JSON.parse(headers.messageMetadata)
77
+ } catch {
78
+ // Ignore parse errors
79
+ }
80
+ }
81
+
82
+ // Parse schema from header
83
+ let schema: Record<string, unknown> | undefined
84
+ if (headers.messageSchema) {
85
+ try {
86
+ schema = JSON.parse(headers.messageSchema)
87
+ } catch {
88
+ // Ignore parse errors
89
+ }
90
+ }
91
+
92
+ // Parse attachment from header or body
93
+ let attachment: AttachmentInfo | undefined
94
+ if (headers.attachment) {
95
+ try {
96
+ const attachmentData = JSON.parse(headers.attachment)
97
+ attachment = createAttachmentInfo(attachmentData)
98
+ } catch {
99
+ // Ignore parse errors
100
+ }
101
+ }
102
+ // Fallback to body attachment
103
+ if (!attachment && body.attachment) {
104
+ try {
105
+ attachment = createAttachmentInfo(body.attachment as Record<string, unknown>)
106
+ } catch {
107
+ // Ignore parse errors
108
+ }
109
+ }
110
+
111
+ // Create conversation helper
112
+ let conversation: Conversation | undefined
113
+ if (conversationId && options.conversationClient) {
114
+ conversation = new Conversation(
115
+ conversationId,
116
+ options.conversationClient,
117
+ options.agentName
118
+ )
119
+ }
120
+
121
+ // Create memory helper
122
+ let memory: Memory | undefined
123
+ if (options.memoryClient && userId && conversationId) {
124
+ memory = new Memory(userId, conversationId, options.memoryClient)
125
+ }
126
+
127
+ return new Message({
128
+ content: (body.content as string) ?? '',
129
+ conversationId,
130
+ userId,
131
+ headers: headers as Record<string, string>,
132
+ rawBody: body,
133
+ conversation,
134
+ memory,
135
+ metadata,
136
+ schema,
137
+ attachment,
138
+ attachmentClient: options.attachmentClient,
139
+ })
140
+ }
141
+
142
+ /**
143
+ * Check if message has an attachment.
144
+ */
145
+ hasAttachment(): boolean {
146
+ return this.attachment !== undefined
147
+ }
148
+
149
+ /**
150
+ * Download attachment as bytes (Buffer).
151
+ *
152
+ * @throws Error if no attachment is available
153
+ */
154
+ async getAttachmentBytes(): Promise<Buffer> {
155
+ if (!this.attachment) {
156
+ throw new Error('No attachment available')
157
+ }
158
+ if (!this.attachmentClient) {
159
+ throw new Error('AttachmentClient not available')
160
+ }
161
+ return this.attachmentClient.download(this.attachment.url)
162
+ }
163
+
164
+ /**
165
+ * Get attachment as base64 string (for LLM vision APIs).
166
+ *
167
+ * @throws Error if no attachment is available
168
+ */
169
+ async getAttachmentBase64(): Promise<string> {
170
+ if (!this.attachment) {
171
+ throw new Error('No attachment available')
172
+ }
173
+ if (!this.attachmentClient) {
174
+ throw new Error('AttachmentClient not available')
175
+ }
176
+ return this.attachmentClient.downloadAsBase64(this.attachment.url)
177
+ }
178
+ }
@@ -0,0 +1,99 @@
1
+ syntax = "proto3";
2
+ package gateway;
3
+ option go_package = "agent-gateway/internal/grpc/pb";
4
+
5
+ // Bidirectional streaming service for agent communication
6
+ service AgentGateway {
7
+ // Main bidirectional stream for all agent communication
8
+ rpc AgentStream(stream ClientMessage) returns (stream ServerMessage);
9
+ }
10
+
11
+ // Client -> Server messages
12
+ message ClientMessage {
13
+ oneof payload {
14
+ AuthRequest auth = 1;
15
+ SubscribeRequest subscribe = 2;
16
+ UnsubscribeRequest unsubscribe = 3;
17
+ ProduceRequest produce = 4;
18
+ PingRequest ping = 5;
19
+ }
20
+ }
21
+
22
+ // Server -> Client messages
23
+ message ServerMessage {
24
+ oneof payload {
25
+ AuthResponse auth_response = 1;
26
+ KafkaMessage message = 2;
27
+ ProduceAck produce_ack = 3;
28
+ SubscribeAck subscribe_ack = 4;
29
+ UnsubscribeAck unsubscribe_ack = 5;
30
+ PongResponse pong = 6;
31
+ ErrorResponse error = 7;
32
+ }
33
+ }
34
+
35
+ message AuthRequest {
36
+ string api_key = 1;
37
+ repeated string agent_names = 2;
38
+ }
39
+
40
+ message AuthResponse {
41
+ string connection_id = 1;
42
+ repeated string subscribed_topics = 2;
43
+ }
44
+
45
+ message SubscribeRequest {
46
+ string agent_name = 1;
47
+ }
48
+
49
+ message SubscribeAck {
50
+ string topic = 1;
51
+ }
52
+
53
+ message UnsubscribeRequest {
54
+ string agent_name = 1;
55
+ }
56
+
57
+ message UnsubscribeAck {
58
+ string agent_name = 1;
59
+ }
60
+
61
+ message ProduceRequest {
62
+ string topic = 1;
63
+ string key = 2;
64
+ map<string, string> headers = 3;
65
+ bytes body = 4; // JSON as bytes
66
+ string correlation_id = 5;
67
+ }
68
+
69
+ message ProduceAck {
70
+ string topic = 1;
71
+ int32 partition = 2;
72
+ int64 offset = 3;
73
+ string correlation_id = 4;
74
+ }
75
+
76
+ message KafkaMessage {
77
+ string topic = 1;
78
+ int32 partition = 2;
79
+ int64 offset = 3;
80
+ string key = 4;
81
+ map<string, string> headers = 5;
82
+ bytes body = 6; // JSON as bytes
83
+ int64 timestamp = 7;
84
+ }
85
+
86
+ message PingRequest {
87
+ int64 timestamp = 1;
88
+ }
89
+
90
+ message PongResponse {
91
+ int64 client_timestamp = 1;
92
+ int64 server_timestamp = 2;
93
+ }
94
+
95
+ message ErrorResponse {
96
+ string code = 1;
97
+ string message = 2;
98
+ string correlation_id = 3;
99
+ }
@@ -0,0 +1,242 @@
1
+ /**
2
+ * March Agent SDK - Streamer
3
+ * Port of Python march_agent/streamer.py
4
+ */
5
+
6
+ import type { Message } from './message.js'
7
+ import type { GatewayClient } from './gateway-client.js'
8
+ import type { ConversationClient } from './conversation-client.js'
9
+ import type { Artifact, ArtifactInput } from './artifact.js'
10
+ import { toArtifact } from './artifact.js'
11
+ import type { StreamOptions } from './types.js'
12
+
13
+ /**
14
+ * Handles streaming responses back to the conversation via the gateway.
15
+ */
16
+ export class Streamer {
17
+ private readonly agentName: string
18
+ private readonly originalMessage: Message
19
+ private readonly gatewayClient: GatewayClient
20
+ private readonly conversationClient?: ConversationClient
21
+ private readonly sendTo: string
22
+ private awaiting: boolean
23
+
24
+ private responseSchema?: Record<string, unknown>
25
+ private messageMetadata?: Record<string, unknown>
26
+ private artifacts: Artifact[] = []
27
+ private streamedContent: string = ''
28
+ private firstChunkSent: boolean = false
29
+ private finished: boolean = false
30
+
31
+ constructor(options: {
32
+ agentName: string
33
+ originalMessage: Message
34
+ gatewayClient: GatewayClient
35
+ conversationClient?: ConversationClient
36
+ awaiting?: boolean
37
+ sendTo?: string
38
+ }) {
39
+ this.agentName = options.agentName
40
+ this.originalMessage = options.originalMessage
41
+ this.gatewayClient = options.gatewayClient
42
+ this.conversationClient = options.conversationClient
43
+ this.awaiting = options.awaiting ?? false
44
+ this.sendTo = options.sendTo ?? 'user'
45
+ }
46
+
47
+ /**
48
+ * Set response schema for form rendering (fluent API).
49
+ */
50
+ setResponseSchema(schema: Record<string, unknown>): this {
51
+ this.responseSchema = schema
52
+ return this
53
+ }
54
+
55
+ /**
56
+ * Set message metadata (fluent API).
57
+ */
58
+ setMessageMetadata(metadata: Record<string, unknown>): this {
59
+ this.messageMetadata = metadata
60
+ return this
61
+ }
62
+
63
+ /**
64
+ * Add an artifact to the message (fluent API).
65
+ */
66
+ addArtifact(artifact: ArtifactInput): this {
67
+ this.artifacts.push(toArtifact(artifact))
68
+ return this
69
+ }
70
+
71
+ /**
72
+ * Set all artifacts at once (replaces any existing).
73
+ */
74
+ setArtifacts(artifacts: ArtifactInput[]): this {
75
+ this.artifacts = artifacts.map(toArtifact)
76
+ return this
77
+ }
78
+
79
+ /**
80
+ * Stream a content chunk.
81
+ */
82
+ stream(content: string, options: StreamOptions = {}): void {
83
+ const { persist = true, eventType } = options
84
+
85
+ if (this.finished) {
86
+ console.warn('Streamer.stream() called after finish()')
87
+ return
88
+ }
89
+
90
+ if (persist) {
91
+ this.streamedContent += content
92
+ }
93
+
94
+ this.send(content, false, persist, eventType)
95
+ }
96
+
97
+ /**
98
+ * Alias for stream() - write a content chunk.
99
+ */
100
+ write(content: string, persist: boolean = true): void {
101
+ this.stream(content, { persist })
102
+ }
103
+
104
+ /**
105
+ * Finish streaming with done=true signal.
106
+ */
107
+ async finish(awaitingOverride?: boolean): Promise<void> {
108
+ if (this.finished) {
109
+ return
110
+ }
111
+
112
+ this.finished = true
113
+
114
+ // Determine final awaiting value
115
+ let finalAwaiting = awaitingOverride ?? this.awaiting
116
+
117
+ // If response schema was set and awaiting not explicitly false, set awaiting
118
+ if (this.responseSchema && awaitingOverride !== false) {
119
+ finalAwaiting = true
120
+ }
121
+
122
+ // Send final done message
123
+ this.send('', true, false)
124
+
125
+ // Set pending response schema on conversation
126
+ if (this.responseSchema && this.conversationClient) {
127
+ await this.setPendingResponseSchema()
128
+ }
129
+
130
+ // Set awaiting route
131
+ if (finalAwaiting && this.conversationClient) {
132
+ await this.setAwaitingRoute()
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Send message to router via gateway.
138
+ */
139
+ private send(
140
+ content: string,
141
+ done: boolean,
142
+ persist: boolean = true,
143
+ eventType?: string
144
+ ): void {
145
+ // Build headers (matching Python implementation)
146
+ const headers: Record<string, string> = {
147
+ conversationId: this.originalMessage.conversationId,
148
+ userId: this.originalMessage.userId,
149
+ from_: this.agentName,
150
+ to_: this.sendTo,
151
+ nextRoute: this.sendTo,
152
+ }
153
+
154
+ if (eventType) {
155
+ headers.eventType = eventType
156
+ }
157
+
158
+ // Include metadata, artifacts, and schema on first chunk (only once)
159
+ if (!this.firstChunkSent) {
160
+ this.firstChunkSent = true
161
+
162
+ if (this.messageMetadata) {
163
+ headers.messageMetadata = JSON.stringify(this.messageMetadata)
164
+ }
165
+
166
+ if (this.artifacts.length > 0) {
167
+ headers.artifacts = JSON.stringify(this.artifacts)
168
+ }
169
+
170
+ if (this.responseSchema) {
171
+ headers.responseSchema = JSON.stringify(this.responseSchema)
172
+ }
173
+ }
174
+
175
+ // Build body (matching Python implementation - includes persist)
176
+ const body: Record<string, unknown> = {
177
+ content,
178
+ done,
179
+ persist,
180
+ }
181
+
182
+ if (eventType) {
183
+ body.eventType = eventType
184
+ }
185
+
186
+ // Produce message via gateway
187
+ this.gatewayClient.produce(
188
+ 'router.inbox',
189
+ this.originalMessage.conversationId,
190
+ headers,
191
+ body
192
+ )
193
+ }
194
+
195
+ /**
196
+ * Store response schema on conversation for form validation.
197
+ */
198
+ private async setPendingResponseSchema(): Promise<void> {
199
+ if (!this.conversationClient || !this.responseSchema) return
200
+
201
+ try {
202
+ await this.conversationClient.updateConversation(
203
+ this.originalMessage.conversationId,
204
+ { pendingResponseSchema: this.responseSchema } as never
205
+ )
206
+ } catch (error) {
207
+ console.error('Failed to set pending response schema:', error)
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Set awaiting_route to this agent's name.
213
+ */
214
+ private async setAwaitingRoute(): Promise<void> {
215
+ if (!this.conversationClient) return
216
+
217
+ try {
218
+ await this.conversationClient.updateConversation(
219
+ this.originalMessage.conversationId,
220
+ { awaitingRoute: this.agentName } as never
221
+ )
222
+ } catch (error) {
223
+ console.error('Failed to set awaiting route:', error)
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Get the accumulated streamed content.
229
+ */
230
+ getStreamedContent(): string {
231
+ return this.streamedContent
232
+ }
233
+
234
+ /**
235
+ * Support for async disposal (TypeScript 5.2+ "using" syntax).
236
+ */
237
+ async [Symbol.asyncDispose](): Promise<void> {
238
+ if (!this.finished) {
239
+ await this.finish()
240
+ }
241
+ }
242
+ }