@copilotkitnext/runtime 0.0.1

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.
Files changed (47) hide show
  1. package/.cursor/rules/runtime.always.mdc +9 -0
  2. package/.turbo/turbo-build.log +22 -0
  3. package/.turbo/turbo-check-types.log +4 -0
  4. package/.turbo/turbo-lint.log +56 -0
  5. package/.turbo/turbo-test$colon$coverage.log +149 -0
  6. package/.turbo/turbo-test.log +107 -0
  7. package/LICENSE +11 -0
  8. package/README-RUNNERS.md +78 -0
  9. package/dist/index.d.mts +245 -0
  10. package/dist/index.d.ts +245 -0
  11. package/dist/index.js +1873 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/index.mjs +1841 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/eslint.config.mjs +3 -0
  16. package/package.json +62 -0
  17. package/src/__tests__/get-runtime-info.test.ts +117 -0
  18. package/src/__tests__/handle-run.test.ts +69 -0
  19. package/src/__tests__/handle-transcribe.test.ts +289 -0
  20. package/src/__tests__/in-process-agent-runner-messages.test.ts +599 -0
  21. package/src/__tests__/in-process-agent-runner.test.ts +726 -0
  22. package/src/__tests__/middleware.test.ts +432 -0
  23. package/src/__tests__/routing.test.ts +257 -0
  24. package/src/endpoint.ts +150 -0
  25. package/src/handler.ts +3 -0
  26. package/src/handlers/get-runtime-info.ts +50 -0
  27. package/src/handlers/handle-connect.ts +144 -0
  28. package/src/handlers/handle-run.ts +156 -0
  29. package/src/handlers/handle-transcribe.ts +126 -0
  30. package/src/index.ts +8 -0
  31. package/src/middleware.ts +232 -0
  32. package/src/runner/__tests__/enterprise-runner.test.ts +992 -0
  33. package/src/runner/__tests__/event-compaction.test.ts +253 -0
  34. package/src/runner/__tests__/in-memory-runner.test.ts +483 -0
  35. package/src/runner/__tests__/sqlite-runner.test.ts +975 -0
  36. package/src/runner/agent-runner.ts +27 -0
  37. package/src/runner/enterprise.ts +653 -0
  38. package/src/runner/event-compaction.ts +250 -0
  39. package/src/runner/in-memory.ts +322 -0
  40. package/src/runner/index.ts +0 -0
  41. package/src/runner/sqlite.ts +481 -0
  42. package/src/runtime.ts +53 -0
  43. package/src/transcription-service/transcription-service-openai.ts +29 -0
  44. package/src/transcription-service/transcription-service.ts +11 -0
  45. package/tsconfig.json +13 -0
  46. package/tsup.config.ts +11 -0
  47. package/vitest.config.mjs +15 -0
@@ -0,0 +1,250 @@
1
+ import {
2
+ BaseEvent,
3
+ EventType,
4
+ TextMessageStartEvent,
5
+ TextMessageContentEvent,
6
+ TextMessageEndEvent,
7
+ ToolCallStartEvent,
8
+ ToolCallArgsEvent,
9
+ ToolCallEndEvent,
10
+ } from "@ag-ui/client";
11
+
12
+ /**
13
+ * Compacts streaming events by consolidating multiple deltas into single events.
14
+ * For text messages: multiple content deltas become one concatenated delta.
15
+ * For tool calls: multiple args deltas become one concatenated delta.
16
+ * Events between related streaming events are reordered to keep streaming events together.
17
+ *
18
+ * @param events - Array of events to compact
19
+ * @returns Compacted array of events
20
+ */
21
+ export function compactEvents(events: BaseEvent[]): BaseEvent[] {
22
+ const compacted: BaseEvent[] = [];
23
+ const pendingTextMessages = new Map<string, {
24
+ start?: TextMessageStartEvent;
25
+ contents: TextMessageContentEvent[];
26
+ end?: TextMessageEndEvent;
27
+ otherEvents: BaseEvent[];
28
+ }>();
29
+ const pendingToolCalls = new Map<string, {
30
+ start?: ToolCallStartEvent;
31
+ args: ToolCallArgsEvent[];
32
+ end?: ToolCallEndEvent;
33
+ otherEvents: BaseEvent[];
34
+ }>();
35
+
36
+ for (const event of events) {
37
+ // Handle text message streaming events
38
+ if (event.type === EventType.TEXT_MESSAGE_START) {
39
+ const startEvent = event as TextMessageStartEvent;
40
+ const messageId = startEvent.messageId;
41
+
42
+ if (!pendingTextMessages.has(messageId)) {
43
+ pendingTextMessages.set(messageId, {
44
+ contents: [],
45
+ otherEvents: []
46
+ });
47
+ }
48
+
49
+ const pending = pendingTextMessages.get(messageId)!;
50
+ pending.start = startEvent;
51
+ } else if (event.type === EventType.TEXT_MESSAGE_CONTENT) {
52
+ const contentEvent = event as TextMessageContentEvent;
53
+ const messageId = contentEvent.messageId;
54
+
55
+ if (!pendingTextMessages.has(messageId)) {
56
+ pendingTextMessages.set(messageId, {
57
+ contents: [],
58
+ otherEvents: []
59
+ });
60
+ }
61
+
62
+ const pending = pendingTextMessages.get(messageId)!;
63
+ pending.contents.push(contentEvent);
64
+ } else if (event.type === EventType.TEXT_MESSAGE_END) {
65
+ const endEvent = event as TextMessageEndEvent;
66
+ const messageId = endEvent.messageId;
67
+
68
+ if (!pendingTextMessages.has(messageId)) {
69
+ pendingTextMessages.set(messageId, {
70
+ contents: [],
71
+ otherEvents: []
72
+ });
73
+ }
74
+
75
+ const pending = pendingTextMessages.get(messageId)!;
76
+ pending.end = endEvent;
77
+
78
+ // Flush this message's events
79
+ flushTextMessage(messageId, pending, compacted);
80
+ pendingTextMessages.delete(messageId);
81
+ } else if (event.type === EventType.TOOL_CALL_START) {
82
+ const startEvent = event as ToolCallStartEvent;
83
+ const toolCallId = startEvent.toolCallId;
84
+
85
+ if (!pendingToolCalls.has(toolCallId)) {
86
+ pendingToolCalls.set(toolCallId, {
87
+ args: [],
88
+ otherEvents: []
89
+ });
90
+ }
91
+
92
+ const pending = pendingToolCalls.get(toolCallId)!;
93
+ pending.start = startEvent;
94
+ } else if (event.type === EventType.TOOL_CALL_ARGS) {
95
+ const argsEvent = event as ToolCallArgsEvent;
96
+ const toolCallId = argsEvent.toolCallId;
97
+
98
+ if (!pendingToolCalls.has(toolCallId)) {
99
+ pendingToolCalls.set(toolCallId, {
100
+ args: [],
101
+ otherEvents: []
102
+ });
103
+ }
104
+
105
+ const pending = pendingToolCalls.get(toolCallId)!;
106
+ pending.args.push(argsEvent);
107
+ } else if (event.type === EventType.TOOL_CALL_END) {
108
+ const endEvent = event as ToolCallEndEvent;
109
+ const toolCallId = endEvent.toolCallId;
110
+
111
+ if (!pendingToolCalls.has(toolCallId)) {
112
+ pendingToolCalls.set(toolCallId, {
113
+ args: [],
114
+ otherEvents: []
115
+ });
116
+ }
117
+
118
+ const pending = pendingToolCalls.get(toolCallId)!;
119
+ pending.end = endEvent;
120
+
121
+ // Flush this tool call's events
122
+ flushToolCall(toolCallId, pending, compacted);
123
+ pendingToolCalls.delete(toolCallId);
124
+ } else {
125
+ // For non-streaming events, check if we're in the middle of any streaming sequences
126
+ let addedToBuffer = false;
127
+
128
+ // Check text messages
129
+ for (const [messageId, pending] of pendingTextMessages) {
130
+ // If we have a start but no end yet, this event is "in between"
131
+ if (pending.start && !pending.end) {
132
+ pending.otherEvents.push(event);
133
+ addedToBuffer = true;
134
+ break;
135
+ }
136
+ }
137
+
138
+ // Check tool calls if not already buffered
139
+ if (!addedToBuffer) {
140
+ for (const [toolCallId, pending] of pendingToolCalls) {
141
+ // If we have a start but no end yet, this event is "in between"
142
+ if (pending.start && !pending.end) {
143
+ pending.otherEvents.push(event);
144
+ addedToBuffer = true;
145
+ break;
146
+ }
147
+ }
148
+ }
149
+
150
+ // If not in the middle of any streaming sequence, add directly to compacted
151
+ if (!addedToBuffer) {
152
+ compacted.push(event);
153
+ }
154
+ }
155
+ }
156
+
157
+ // Flush any remaining incomplete messages
158
+ for (const [messageId, pending] of pendingTextMessages) {
159
+ flushTextMessage(messageId, pending, compacted);
160
+ }
161
+
162
+ // Flush any remaining incomplete tool calls
163
+ for (const [toolCallId, pending] of pendingToolCalls) {
164
+ flushToolCall(toolCallId, pending, compacted);
165
+ }
166
+
167
+ return compacted;
168
+ }
169
+
170
+ function flushTextMessage(
171
+ messageId: string,
172
+ pending: {
173
+ start?: TextMessageStartEvent;
174
+ contents: TextMessageContentEvent[];
175
+ end?: TextMessageEndEvent;
176
+ otherEvents: BaseEvent[];
177
+ },
178
+ compacted: BaseEvent[]
179
+ ): void {
180
+ // Add start event if present
181
+ if (pending.start) {
182
+ compacted.push(pending.start);
183
+ }
184
+
185
+ // Compact all content events into one
186
+ if (pending.contents.length > 0) {
187
+ const concatenatedDelta = pending.contents
188
+ .map(c => c.delta)
189
+ .join('');
190
+
191
+ const compactedContent: TextMessageContentEvent = {
192
+ type: EventType.TEXT_MESSAGE_CONTENT,
193
+ messageId: messageId,
194
+ delta: concatenatedDelta,
195
+ };
196
+
197
+ compacted.push(compactedContent);
198
+ }
199
+
200
+ // Add end event if present
201
+ if (pending.end) {
202
+ compacted.push(pending.end);
203
+ }
204
+
205
+ // Add any events that were in between
206
+ for (const otherEvent of pending.otherEvents) {
207
+ compacted.push(otherEvent);
208
+ }
209
+ }
210
+
211
+ function flushToolCall(
212
+ toolCallId: string,
213
+ pending: {
214
+ start?: ToolCallStartEvent;
215
+ args: ToolCallArgsEvent[];
216
+ end?: ToolCallEndEvent;
217
+ otherEvents: BaseEvent[];
218
+ },
219
+ compacted: BaseEvent[]
220
+ ): void {
221
+ // Add start event if present
222
+ if (pending.start) {
223
+ compacted.push(pending.start);
224
+ }
225
+
226
+ // Compact all args events into one
227
+ if (pending.args.length > 0) {
228
+ const concatenatedArgs = pending.args
229
+ .map(a => a.delta)
230
+ .join('');
231
+
232
+ const compactedArgs: ToolCallArgsEvent = {
233
+ type: EventType.TOOL_CALL_ARGS,
234
+ toolCallId: toolCallId,
235
+ delta: concatenatedArgs,
236
+ };
237
+
238
+ compacted.push(compactedArgs);
239
+ }
240
+
241
+ // Add end event if present
242
+ if (pending.end) {
243
+ compacted.push(pending.end);
244
+ }
245
+
246
+ // Add any events that were in between
247
+ for (const otherEvent of pending.otherEvents) {
248
+ compacted.push(otherEvent);
249
+ }
250
+ }
@@ -0,0 +1,322 @@
1
+ import {
2
+ AgentRunner,
3
+ AgentRunnerConnectRequest,
4
+ AgentRunnerIsRunningRequest,
5
+ AgentRunnerRunRequest,
6
+ type AgentRunnerStopRequest,
7
+ } from "./agent-runner";
8
+ import { Observable, ReplaySubject } from "rxjs";
9
+ import {
10
+ BaseEvent,
11
+ Message,
12
+ EventType,
13
+ TextMessageStartEvent,
14
+ TextMessageContentEvent,
15
+ TextMessageEndEvent,
16
+ ToolCallStartEvent,
17
+ ToolCallArgsEvent,
18
+ ToolCallEndEvent,
19
+ ToolCallResultEvent
20
+ } from "@ag-ui/client";
21
+ import { compactEvents } from "./event-compaction";
22
+
23
+ interface HistoricRun {
24
+ threadId: string;
25
+ runId: string;
26
+ parentRunId: string | null;
27
+ events: BaseEvent[];
28
+ createdAt: number;
29
+ }
30
+
31
+ class InMemoryEventStore {
32
+ constructor(public threadId: string) {}
33
+
34
+ /** The subject that current consumers subscribe to. */
35
+ subject: ReplaySubject<BaseEvent> | null = null;
36
+
37
+ /** True while a run is actively producing events. */
38
+ isRunning = false;
39
+
40
+ /** Lets stop() cancel the current producer. */
41
+ abortController = new AbortController();
42
+
43
+ /** Current run ID */
44
+ currentRunId: string | null = null;
45
+
46
+ /** Historic completed runs */
47
+ historicRuns: HistoricRun[] = [];
48
+ }
49
+
50
+ const GLOBAL_STORE = new Map<string, InMemoryEventStore>();
51
+
52
+ export class InMemoryAgentRunner extends AgentRunner {
53
+ private convertMessageToEvents(message: Message): BaseEvent[] {
54
+ const events: BaseEvent[] = [];
55
+
56
+ if (
57
+ (message.role === "assistant" ||
58
+ message.role === "user" ||
59
+ message.role === "developer" ||
60
+ message.role === "system") &&
61
+ message.content
62
+ ) {
63
+ const textStartEvent: TextMessageStartEvent = {
64
+ type: EventType.TEXT_MESSAGE_START,
65
+ messageId: message.id,
66
+ role: message.role,
67
+ };
68
+ events.push(textStartEvent);
69
+
70
+ const textContentEvent: TextMessageContentEvent = {
71
+ type: EventType.TEXT_MESSAGE_CONTENT,
72
+ messageId: message.id,
73
+ delta: message.content,
74
+ };
75
+ events.push(textContentEvent);
76
+
77
+ const textEndEvent: TextMessageEndEvent = {
78
+ type: EventType.TEXT_MESSAGE_END,
79
+ messageId: message.id,
80
+ };
81
+ events.push(textEndEvent);
82
+ }
83
+
84
+ if (message.role === "assistant" && message.toolCalls) {
85
+ for (const toolCall of message.toolCalls) {
86
+ const toolStartEvent: ToolCallStartEvent = {
87
+ type: EventType.TOOL_CALL_START,
88
+ toolCallId: toolCall.id,
89
+ toolCallName: toolCall.function.name,
90
+ parentMessageId: message.id,
91
+ };
92
+ events.push(toolStartEvent);
93
+
94
+ const toolArgsEvent: ToolCallArgsEvent = {
95
+ type: EventType.TOOL_CALL_ARGS,
96
+ toolCallId: toolCall.id,
97
+ delta: toolCall.function.arguments,
98
+ };
99
+ events.push(toolArgsEvent);
100
+
101
+ const toolEndEvent: ToolCallEndEvent = {
102
+ type: EventType.TOOL_CALL_END,
103
+ toolCallId: toolCall.id,
104
+ };
105
+ events.push(toolEndEvent);
106
+ }
107
+ }
108
+
109
+ if (message.role === "tool" && message.toolCallId) {
110
+ const toolResultEvent: ToolCallResultEvent = {
111
+ type: EventType.TOOL_CALL_RESULT,
112
+ messageId: message.id,
113
+ toolCallId: message.toolCallId,
114
+ content: message.content,
115
+ role: "tool",
116
+ };
117
+ events.push(toolResultEvent);
118
+ }
119
+
120
+ return events;
121
+ }
122
+
123
+ run(request: AgentRunnerRunRequest): Observable<BaseEvent> {
124
+ let existingStore = GLOBAL_STORE.get(request.threadId);
125
+ if (!existingStore) {
126
+ existingStore = new InMemoryEventStore(request.threadId);
127
+ GLOBAL_STORE.set(request.threadId, existingStore);
128
+ }
129
+ const store = existingStore; // Now store is const and non-null
130
+
131
+ if (store.isRunning) {
132
+ throw new Error("Thread already running");
133
+ }
134
+ store.isRunning = true;
135
+ store.currentRunId = request.input.runId;
136
+
137
+ // Track seen message IDs and current run events for this run
138
+ const seenMessageIds = new Set<string>();
139
+ const currentRunEvents: BaseEvent[] = [];
140
+
141
+ // Get all previously seen message IDs from historic runs
142
+ const historicMessageIds = new Set<string>();
143
+ for (const run of store.historicRuns) {
144
+ for (const event of run.events) {
145
+ if ('messageId' in event && typeof event.messageId === 'string') {
146
+ historicMessageIds.add(event.messageId);
147
+ }
148
+ }
149
+ }
150
+
151
+ const nextSubject = new ReplaySubject<BaseEvent>(Infinity);
152
+ const prevSubject = store.subject;
153
+
154
+ // Update the store's subject immediately
155
+ store.subject = nextSubject;
156
+ store.abortController = new AbortController();
157
+
158
+ // Create a subject for run() return value
159
+ const runSubject = new ReplaySubject<BaseEvent>(Infinity);
160
+
161
+ // Helper function to run the agent and handle errors
162
+ const runAgent = async () => {
163
+ // Get parent run ID for chaining
164
+ const lastRun = store.historicRuns[store.historicRuns.length - 1];
165
+ const parentRunId = lastRun?.runId ?? null;
166
+
167
+ try {
168
+ await request.agent.runAgent(request.input, {
169
+ onEvent: ({ event }) => {
170
+ runSubject.next(event); // For run() return - only agent events
171
+ nextSubject.next(event); // For connect() / store - all events
172
+ currentRunEvents.push(event); // Accumulate for storage
173
+ },
174
+ onNewMessage: ({ message }) => {
175
+ // Called for each new message
176
+ if (!seenMessageIds.has(message.id)) {
177
+ seenMessageIds.add(message.id);
178
+ }
179
+ },
180
+ onRunStartedEvent: () => {
181
+ // Process input messages (same logic as SQLite)
182
+ if (request.input.messages) {
183
+ for (const message of request.input.messages) {
184
+ if (!seenMessageIds.has(message.id)) {
185
+ seenMessageIds.add(message.id);
186
+ const events = this.convertMessageToEvents(message);
187
+
188
+ // Check if this message is NEW (not in historic runs)
189
+ const isNewMessage = !historicMessageIds.has(message.id);
190
+
191
+ for (const event of events) {
192
+ // Always emit to stream for context
193
+ nextSubject.next(event);
194
+
195
+ // Store if this is a NEW message for this run
196
+ if (isNewMessage) {
197
+ currentRunEvents.push(event);
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }
203
+ },
204
+ });
205
+
206
+ // Store the completed run in memory with ONLY its events
207
+ if (store.currentRunId) {
208
+ // Compact the events before storing (like SQLite does)
209
+ const compactedEvents = compactEvents(currentRunEvents);
210
+ store.historicRuns.push({
211
+ threadId: request.threadId,
212
+ runId: store.currentRunId,
213
+ parentRunId,
214
+ events: compactedEvents,
215
+ createdAt: Date.now(),
216
+ });
217
+ }
218
+
219
+ // Complete the run
220
+ store.isRunning = false;
221
+ store.currentRunId = null;
222
+ runSubject.complete();
223
+ nextSubject.complete();
224
+ } catch {
225
+ // Store the run even if it failed (partial events)
226
+ if (store.currentRunId && currentRunEvents.length > 0) {
227
+ // Compact the events before storing (like SQLite does)
228
+ const compactedEvents = compactEvents(currentRunEvents);
229
+ store.historicRuns.push({
230
+ threadId: request.threadId,
231
+ runId: store.currentRunId,
232
+ parentRunId,
233
+ events: compactedEvents,
234
+ createdAt: Date.now(),
235
+ });
236
+ }
237
+
238
+ // Complete the run
239
+ store.isRunning = false;
240
+ store.currentRunId = null;
241
+ runSubject.complete();
242
+ nextSubject.complete();
243
+ }
244
+ };
245
+
246
+ // Bridge previous events if they exist
247
+ if (prevSubject) {
248
+ prevSubject.subscribe({
249
+ next: (e) => nextSubject.next(e),
250
+ error: (err) => nextSubject.error(err),
251
+ complete: () => {
252
+ // Don't complete nextSubject here - it needs to stay open for new events
253
+ },
254
+ });
255
+ }
256
+
257
+ // Start the agent execution immediately (not lazily)
258
+ runAgent();
259
+
260
+ // Return the run subject (only agent events, no injected messages)
261
+ return runSubject.asObservable();
262
+ }
263
+
264
+ connect(request: AgentRunnerConnectRequest): Observable<BaseEvent> {
265
+ const store = GLOBAL_STORE.get(request.threadId);
266
+ const connectionSubject = new ReplaySubject<BaseEvent>(Infinity);
267
+
268
+ if (!store) {
269
+ // No store means no events
270
+ connectionSubject.complete();
271
+ return connectionSubject.asObservable();
272
+ }
273
+
274
+ // Collect all historic events from memory
275
+ const allHistoricEvents: BaseEvent[] = [];
276
+ for (const run of store.historicRuns) {
277
+ allHistoricEvents.push(...run.events);
278
+ }
279
+
280
+ // Apply compaction to all historic events together (like SQLite)
281
+ const compactedEvents = compactEvents(allHistoricEvents);
282
+
283
+ // Emit compacted events and track message IDs
284
+ const emittedMessageIds = new Set<string>();
285
+ for (const event of compactedEvents) {
286
+ connectionSubject.next(event);
287
+ if ('messageId' in event && typeof event.messageId === 'string') {
288
+ emittedMessageIds.add(event.messageId);
289
+ }
290
+ }
291
+
292
+ // Bridge active run to connection if exists
293
+ if (store.subject && store.isRunning) {
294
+ store.subject.subscribe({
295
+ next: (event) => {
296
+ // Skip message events that we've already emitted from historic
297
+ if ('messageId' in event && typeof event.messageId === 'string' && emittedMessageIds.has(event.messageId)) {
298
+ return;
299
+ }
300
+ connectionSubject.next(event);
301
+ },
302
+ complete: () => connectionSubject.complete(),
303
+ error: (err) => connectionSubject.error(err)
304
+ });
305
+ } else {
306
+ // No active run, complete after historic events
307
+ connectionSubject.complete();
308
+ }
309
+
310
+ return connectionSubject.asObservable();
311
+ }
312
+
313
+ isRunning(request: AgentRunnerIsRunningRequest): Promise<boolean> {
314
+ const store = GLOBAL_STORE.get(request.threadId);
315
+ return Promise.resolve(store?.isRunning ?? false);
316
+ }
317
+
318
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
319
+ stop(_request: AgentRunnerStopRequest): Promise<boolean | undefined> {
320
+ throw new Error("Method not implemented.");
321
+ }
322
+ }
File without changes