@animalabs/agent-framework 0.1.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.
Files changed (212) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/src/agent.d.ts +130 -0
  4. package/dist/src/agent.d.ts.map +1 -0
  5. package/dist/src/agent.js +406 -0
  6. package/dist/src/agent.js.map +1 -0
  7. package/dist/src/api/mcp-server.d.ts +37 -0
  8. package/dist/src/api/mcp-server.d.ts.map +1 -0
  9. package/dist/src/api/mcp-server.js +598 -0
  10. package/dist/src/api/mcp-server.js.map +1 -0
  11. package/dist/src/api/server.d.ts +83 -0
  12. package/dist/src/api/server.d.ts.map +1 -0
  13. package/dist/src/api/server.js +785 -0
  14. package/dist/src/api/server.js.map +1 -0
  15. package/dist/src/api/types.d.ts +395 -0
  16. package/dist/src/api/types.d.ts.map +1 -0
  17. package/dist/src/api/types.js +5 -0
  18. package/dist/src/api/types.js.map +1 -0
  19. package/dist/src/framework.d.ts +309 -0
  20. package/dist/src/framework.d.ts.map +1 -0
  21. package/dist/src/framework.js +2309 -0
  22. package/dist/src/framework.js.map +1 -0
  23. package/dist/src/gate/event-gate.d.ts +75 -0
  24. package/dist/src/gate/event-gate.d.ts.map +1 -0
  25. package/dist/src/gate/event-gate.js +503 -0
  26. package/dist/src/gate/event-gate.js.map +1 -0
  27. package/dist/src/gate/index.d.ts +3 -0
  28. package/dist/src/gate/index.d.ts.map +1 -0
  29. package/dist/src/gate/index.js +2 -0
  30. package/dist/src/gate/index.js.map +1 -0
  31. package/dist/src/gate/types.d.ts +82 -0
  32. package/dist/src/gate/types.d.ts.map +1 -0
  33. package/dist/src/gate/types.js +9 -0
  34. package/dist/src/gate/types.js.map +1 -0
  35. package/dist/src/index.d.ts +17 -0
  36. package/dist/src/index.d.ts.map +1 -0
  37. package/dist/src/index.js +18 -0
  38. package/dist/src/index.js.map +1 -0
  39. package/dist/src/mcpl/channel-registry.d.ts +141 -0
  40. package/dist/src/mcpl/channel-registry.d.ts.map +1 -0
  41. package/dist/src/mcpl/channel-registry.js +647 -0
  42. package/dist/src/mcpl/channel-registry.js.map +1 -0
  43. package/dist/src/mcpl/checkpoint-manager.d.ts +79 -0
  44. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -0
  45. package/dist/src/mcpl/checkpoint-manager.js +430 -0
  46. package/dist/src/mcpl/checkpoint-manager.js.map +1 -0
  47. package/dist/src/mcpl/errors.d.ts +52 -0
  48. package/dist/src/mcpl/errors.d.ts.map +1 -0
  49. package/dist/src/mcpl/errors.js +93 -0
  50. package/dist/src/mcpl/errors.js.map +1 -0
  51. package/dist/src/mcpl/feature-set-manager.d.ts +91 -0
  52. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -0
  53. package/dist/src/mcpl/feature-set-manager.js +250 -0
  54. package/dist/src/mcpl/feature-set-manager.js.map +1 -0
  55. package/dist/src/mcpl/hook-orchestrator.d.ts +51 -0
  56. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -0
  57. package/dist/src/mcpl/hook-orchestrator.js +217 -0
  58. package/dist/src/mcpl/hook-orchestrator.js.map +1 -0
  59. package/dist/src/mcpl/index.d.ts +14 -0
  60. package/dist/src/mcpl/index.d.ts.map +1 -0
  61. package/dist/src/mcpl/index.js +21 -0
  62. package/dist/src/mcpl/index.js.map +1 -0
  63. package/dist/src/mcpl/inference-router.d.ts +67 -0
  64. package/dist/src/mcpl/inference-router.d.ts.map +1 -0
  65. package/dist/src/mcpl/inference-router.js +249 -0
  66. package/dist/src/mcpl/inference-router.js.map +1 -0
  67. package/dist/src/mcpl/push-handler.d.ts +60 -0
  68. package/dist/src/mcpl/push-handler.d.ts.map +1 -0
  69. package/dist/src/mcpl/push-handler.js +162 -0
  70. package/dist/src/mcpl/push-handler.js.map +1 -0
  71. package/dist/src/mcpl/scope-manager.d.ts +76 -0
  72. package/dist/src/mcpl/scope-manager.d.ts.map +1 -0
  73. package/dist/src/mcpl/scope-manager.js +213 -0
  74. package/dist/src/mcpl/scope-manager.js.map +1 -0
  75. package/dist/src/mcpl/server-connection.d.ts +174 -0
  76. package/dist/src/mcpl/server-connection.d.ts.map +1 -0
  77. package/dist/src/mcpl/server-connection.js +559 -0
  78. package/dist/src/mcpl/server-connection.js.map +1 -0
  79. package/dist/src/mcpl/server-registry.d.ts +63 -0
  80. package/dist/src/mcpl/server-registry.d.ts.map +1 -0
  81. package/dist/src/mcpl/server-registry.js +119 -0
  82. package/dist/src/mcpl/server-registry.js.map +1 -0
  83. package/dist/src/mcpl/types.d.ts +755 -0
  84. package/dist/src/mcpl/types.d.ts.map +1 -0
  85. package/dist/src/mcpl/types.js +48 -0
  86. package/dist/src/mcpl/types.js.map +1 -0
  87. package/dist/src/module-registry.d.ts +93 -0
  88. package/dist/src/module-registry.d.ts.map +1 -0
  89. package/dist/src/module-registry.js +386 -0
  90. package/dist/src/module-registry.js.map +1 -0
  91. package/dist/src/modules/api/index.d.ts +22 -0
  92. package/dist/src/modules/api/index.d.ts.map +1 -0
  93. package/dist/src/modules/api/index.js +63 -0
  94. package/dist/src/modules/api/index.js.map +1 -0
  95. package/dist/src/modules/discord/discord-js-client.d.ts +58 -0
  96. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -0
  97. package/dist/src/modules/discord/discord-js-client.js +273 -0
  98. package/dist/src/modules/discord/discord-js-client.js.map +1 -0
  99. package/dist/src/modules/discord/index.d.ts +82 -0
  100. package/dist/src/modules/discord/index.d.ts.map +1 -0
  101. package/dist/src/modules/discord/index.js +959 -0
  102. package/dist/src/modules/discord/index.js.map +1 -0
  103. package/dist/src/modules/discord/types.d.ts +327 -0
  104. package/dist/src/modules/discord/types.d.ts.map +1 -0
  105. package/dist/src/modules/discord/types.js +5 -0
  106. package/dist/src/modules/discord/types.js.map +1 -0
  107. package/dist/src/modules/index.d.ts +11 -0
  108. package/dist/src/modules/index.d.ts.map +1 -0
  109. package/dist/src/modules/index.js +7 -0
  110. package/dist/src/modules/index.js.map +1 -0
  111. package/dist/src/modules/workspace/index.d.ts +59 -0
  112. package/dist/src/modules/workspace/index.d.ts.map +1 -0
  113. package/dist/src/modules/workspace/index.js +831 -0
  114. package/dist/src/modules/workspace/index.js.map +1 -0
  115. package/dist/src/modules/workspace/sync.d.ts +48 -0
  116. package/dist/src/modules/workspace/sync.d.ts.map +1 -0
  117. package/dist/src/modules/workspace/sync.js +253 -0
  118. package/dist/src/modules/workspace/sync.js.map +1 -0
  119. package/dist/src/modules/workspace/types.d.ts +166 -0
  120. package/dist/src/modules/workspace/types.d.ts.map +1 -0
  121. package/dist/src/modules/workspace/types.js +5 -0
  122. package/dist/src/modules/workspace/types.js.map +1 -0
  123. package/dist/src/modules/workspace/watcher.d.ts +43 -0
  124. package/dist/src/modules/workspace/watcher.d.ts.map +1 -0
  125. package/dist/src/modules/workspace/watcher.js +120 -0
  126. package/dist/src/modules/workspace/watcher.js.map +1 -0
  127. package/dist/src/queue.d.ts +50 -0
  128. package/dist/src/queue.d.ts.map +1 -0
  129. package/dist/src/queue.js +95 -0
  130. package/dist/src/queue.js.map +1 -0
  131. package/dist/src/safe-slice.d.ts +8 -0
  132. package/dist/src/safe-slice.d.ts.map +1 -0
  133. package/dist/src/safe-slice.js +18 -0
  134. package/dist/src/safe-slice.js.map +1 -0
  135. package/dist/src/types/agent.d.ts +118 -0
  136. package/dist/src/types/agent.d.ts.map +1 -0
  137. package/dist/src/types/agent.js +2 -0
  138. package/dist/src/types/agent.js.map +1 -0
  139. package/dist/src/types/events.d.ts +220 -0
  140. package/dist/src/types/events.d.ts.map +1 -0
  141. package/dist/src/types/events.js +2 -0
  142. package/dist/src/types/events.js.map +1 -0
  143. package/dist/src/types/framework.d.ts +184 -0
  144. package/dist/src/types/framework.d.ts.map +1 -0
  145. package/dist/src/types/framework.js +2 -0
  146. package/dist/src/types/framework.js.map +1 -0
  147. package/dist/src/types/index.d.ts +7 -0
  148. package/dist/src/types/index.d.ts.map +1 -0
  149. package/dist/src/types/index.js +7 -0
  150. package/dist/src/types/index.js.map +1 -0
  151. package/dist/src/types/inference-log.d.ts +64 -0
  152. package/dist/src/types/inference-log.d.ts.map +1 -0
  153. package/dist/src/types/inference-log.js +5 -0
  154. package/dist/src/types/inference-log.js.map +1 -0
  155. package/dist/src/types/module.d.ts +302 -0
  156. package/dist/src/types/module.d.ts.map +1 -0
  157. package/dist/src/types/module.js +2 -0
  158. package/dist/src/types/module.js.map +1 -0
  159. package/dist/src/types/trace.d.ts +173 -0
  160. package/dist/src/types/trace.d.ts.map +1 -0
  161. package/dist/src/types/trace.js +2 -0
  162. package/dist/src/types/trace.js.map +1 -0
  163. package/dist/test/event-gate.test.d.ts +2 -0
  164. package/dist/test/event-gate.test.d.ts.map +1 -0
  165. package/dist/test/event-gate.test.js +535 -0
  166. package/dist/test/event-gate.test.js.map +1 -0
  167. package/dist/test/framework.test.d.ts +5 -0
  168. package/dist/test/framework.test.d.ts.map +1 -0
  169. package/dist/test/framework.test.js +738 -0
  170. package/dist/test/framework.test.js.map +1 -0
  171. package/dist/tsconfig.tsbuildinfo +1 -0
  172. package/package.json +58 -0
  173. package/src/agent.ts +512 -0
  174. package/src/api/mcp-server.ts +687 -0
  175. package/src/api/server.ts +1023 -0
  176. package/src/api/types.ts +542 -0
  177. package/src/framework.ts +2784 -0
  178. package/src/gate/event-gate.ts +623 -0
  179. package/src/gate/index.ts +12 -0
  180. package/src/gate/types.ts +112 -0
  181. package/src/index.ts +63 -0
  182. package/src/mcpl/channel-registry.ts +824 -0
  183. package/src/mcpl/checkpoint-manager.ts +545 -0
  184. package/src/mcpl/errors.ts +114 -0
  185. package/src/mcpl/feature-set-manager.ts +333 -0
  186. package/src/mcpl/hook-orchestrator.ts +281 -0
  187. package/src/mcpl/index.ts +134 -0
  188. package/src/mcpl/inference-router.ts +335 -0
  189. package/src/mcpl/push-handler.ts +232 -0
  190. package/src/mcpl/scope-manager.ts +262 -0
  191. package/src/mcpl/server-connection.ts +694 -0
  192. package/src/mcpl/server-registry.ts +144 -0
  193. package/src/mcpl/types.ts +991 -0
  194. package/src/module-registry.ts +547 -0
  195. package/src/modules/api/index.ts +94 -0
  196. package/src/modules/discord/discord-js-client.ts +353 -0
  197. package/src/modules/discord/index.ts +1146 -0
  198. package/src/modules/discord/types.ts +465 -0
  199. package/src/modules/index.ts +43 -0
  200. package/src/modules/workspace/index.ts +969 -0
  201. package/src/modules/workspace/sync.ts +314 -0
  202. package/src/modules/workspace/types.ts +202 -0
  203. package/src/modules/workspace/watcher.ts +142 -0
  204. package/src/queue.ts +106 -0
  205. package/src/safe-slice.ts +18 -0
  206. package/src/types/agent.ts +119 -0
  207. package/src/types/events.ts +252 -0
  208. package/src/types/framework.ts +211 -0
  209. package/src/types/index.ts +6 -0
  210. package/src/types/inference-log.ts +64 -0
  211. package/src/types/module.ts +367 -0
  212. package/src/types/trace.ts +196 -0
package/src/agent.ts ADDED
@@ -0,0 +1,512 @@
1
+ import type { Membrane, NormalizedMessage, NormalizedRequest, ContentBlock, YieldingStream } from '@animalabs/membrane';
2
+ import { isAbortedResponse } from '@animalabs/membrane';
3
+
4
+ export interface StartStreamResult {
5
+ stream: YieldingStream;
6
+ request: NormalizedRequest;
7
+ }
8
+ import type { ContextManager, TokenBudget, ContextInjection, CompileResult } from '@animalabs/context-manager';
9
+ import type {
10
+ AgentConfig,
11
+ AgentState,
12
+ PendingToolCall,
13
+ CompletedToolCall,
14
+ ToolCallId,
15
+ ToolCall,
16
+ ToolResult,
17
+ ToolDefinition,
18
+ AgentInfo,
19
+ InferenceResult,
20
+ InferenceOptions,
21
+ } from './types/index.js';
22
+
23
+ /**
24
+ * An agent wraps a context manager and manages inference state.
25
+ */
26
+ export class Agent {
27
+ readonly name: string;
28
+ readonly model: string;
29
+ readonly systemPrompt: string;
30
+ readonly allowedTools: 'all' | string[];
31
+ readonly triggerSources: 'all' | string[];
32
+ readonly maxTokens: number;
33
+ readonly temperature: number;
34
+
35
+ private _state: AgentState = { status: 'idle' };
36
+ private _inferenceStartedAt = 0;
37
+ private _streamId = 0;
38
+ lastStreamInputTokens = 0;
39
+ maxStreamTokens: number;
40
+ private contextManager: ContextManager;
41
+ private membrane: Membrane;
42
+
43
+ constructor(
44
+ config: AgentConfig,
45
+ contextManager: ContextManager,
46
+ membrane: Membrane
47
+ ) {
48
+ this.name = config.name;
49
+ this.model = config.model;
50
+ this.systemPrompt = config.systemPrompt;
51
+ this.allowedTools = config.allowedTools ?? 'all';
52
+ this.triggerSources = config.triggerSources ?? 'all';
53
+ this.maxTokens = config.maxTokens ?? 4096;
54
+ this.temperature = config.temperature ?? 1;
55
+ this.maxStreamTokens = config.maxStreamTokens ?? 150_000;
56
+ this.contextManager = contextManager;
57
+ this.membrane = membrane;
58
+ }
59
+
60
+ /**
61
+ * Get current agent state.
62
+ */
63
+ get state(): AgentState {
64
+ return this._state;
65
+ }
66
+
67
+ /**
68
+ * Monotonically increasing stream generation counter.
69
+ * Used to guard stale driveStream handlers after a budget restart.
70
+ */
71
+ get streamId(): number {
72
+ return this._streamId;
73
+ }
74
+
75
+ /**
76
+ * Get agent info.
77
+ */
78
+ get info(): AgentInfo {
79
+ return {
80
+ name: this.name,
81
+ model: this.model,
82
+ status: this._state.status,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Check if agent can use a specific tool.
88
+ */
89
+ canUseTool(toolName: string): boolean {
90
+ if (this.allowedTools === 'all') {
91
+ return true;
92
+ }
93
+ return this.allowedTools.includes(toolName);
94
+ }
95
+
96
+ /**
97
+ * Check if a source can trigger inference for this agent.
98
+ */
99
+ canBeTriggeredBy(source: string): boolean {
100
+ if (this.triggerSources === 'all') {
101
+ return true;
102
+ }
103
+ return this.triggerSources.includes(source);
104
+ }
105
+
106
+ // ==========================================================================
107
+ // Composable Steps
108
+ // ==========================================================================
109
+
110
+ /**
111
+ * Compile the context without injections.
112
+ * Step 1 of the inference pipeline — callers can inspect/modify the result
113
+ * before building a request.
114
+ */
115
+ async compileContext(budget?: TokenBudget): Promise<CompileResult> {
116
+ return this.contextManager.compile(budget);
117
+ }
118
+
119
+ /**
120
+ * Compile the context with injections.
121
+ * Same as compileContext but forwards injections (e.g. from MCPL servers)
122
+ * to the context manager so they are merged into the compiled messages.
123
+ */
124
+ async compileWithInjections(
125
+ budget?: TokenBudget,
126
+ injections?: ContextInjection[]
127
+ ): Promise<CompileResult> {
128
+ return this.contextManager.compile(budget, injections);
129
+ }
130
+
131
+ // ==========================================================================
132
+ // Inference (backward-compatible)
133
+ // ==========================================================================
134
+
135
+ /**
136
+ * Run inference and return result with tool calls and speech content.
137
+ * Updates agent state during execution.
138
+ */
139
+ async runInference(
140
+ availableTools: ToolDefinition[],
141
+ budget?: TokenBudget,
142
+ options: InferenceOptions = {}
143
+ ): Promise<InferenceResult> {
144
+ return this.runInferenceWithInjections(availableTools, undefined, budget, options);
145
+ }
146
+
147
+ /**
148
+ * Run inference with context injections.
149
+ * Same as runInference but passes injections through to compile.
150
+ */
151
+ async runInferenceWithInjections(
152
+ availableTools: ToolDefinition[],
153
+ injections?: ContextInjection[],
154
+ budget?: TokenBudget,
155
+ options?: InferenceOptions
156
+ ): Promise<InferenceResult> {
157
+ if (this._state.status === 'inferring') {
158
+ throw new Error(`Agent ${this.name} is already inferring`);
159
+ }
160
+
161
+ if (this._state.status === 'waiting_for_tools') {
162
+ throw new Error(`Agent ${this.name} is waiting for tool results`);
163
+ }
164
+
165
+ // Filter tools to only allowed ones
166
+ const tools = availableTools.filter((t) => this.canUseTool(t.name));
167
+
168
+ // Compile context (with optional injections)
169
+ const { messages, systemInjections } = await this.compileWithInjections(budget, injections);
170
+
171
+ // If we have pending tool results, add them
172
+ if (this._state.status === 'ready') {
173
+ const toolResultMessages = this.buildToolResultMessages(this._state.toolResults);
174
+ messages.push(...toolResultMessages);
175
+ }
176
+
177
+ const request: NormalizedRequest = {
178
+ messages,
179
+ system: this.buildSystemPrompt(systemInjections),
180
+ config: {
181
+ model: this.model,
182
+ maxTokens: this.maxTokens,
183
+ temperature: this.temperature,
184
+ },
185
+ tools: tools.length > 0 ? tools : undefined,
186
+ assistantParticipant: this.name,
187
+ };
188
+
189
+ const abortController = new AbortController();
190
+ if (options?.signal) {
191
+ if (options.signal.aborted) {
192
+ abortController.abort();
193
+ } else {
194
+ options.signal.addEventListener('abort', () => abortController.abort(), { once: true });
195
+ }
196
+ }
197
+
198
+ // Set state to inferring
199
+ this._inferenceStartedAt = Date.now();
200
+ const inferencePromise = this.doInference(request, abortController.signal);
201
+ this._state = { status: 'inferring', promise: inferencePromise, abortController };
202
+
203
+ try {
204
+ const result = await inferencePromise;
205
+
206
+ if (result.aborted) {
207
+ this._state = { status: 'idle' };
208
+ return result;
209
+ }
210
+
211
+ if (result.toolCalls.length > 0) {
212
+ // Waiting for tool results
213
+ const pending = new Map<ToolCallId, PendingToolCall>();
214
+ for (const call of result.toolCalls) {
215
+ pending.set(call.id, {
216
+ id: call.id,
217
+ name: call.name,
218
+ input: call.input,
219
+ startedAt: Date.now(),
220
+ });
221
+ }
222
+ this._state = { status: 'waiting_for_tools', pending, completed: [] };
223
+ } else {
224
+ // Done, back to idle
225
+ this._state = { status: 'idle' };
226
+ }
227
+
228
+ return result;
229
+ } catch (error) {
230
+ // On error, go back to idle
231
+ this._state = { status: 'idle' };
232
+ throw error;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Provide a tool result.
238
+ */
239
+ provideToolResult(callId: ToolCallId, result: ToolResult): void {
240
+ if (this._state.status !== 'waiting_for_tools') {
241
+ throw new Error(`Agent ${this.name} is not waiting for tools`);
242
+ }
243
+
244
+ const pending = this._state.pending.get(callId);
245
+ if (!pending) {
246
+ throw new Error(`Unknown tool call: ${callId}`);
247
+ }
248
+
249
+ // Move from pending to completed
250
+ const completed: CompletedToolCall = {
251
+ id: pending.id,
252
+ name: pending.name,
253
+ input: pending.input,
254
+ result,
255
+ durationMs: Date.now() - pending.startedAt,
256
+ };
257
+
258
+ this._state.pending.delete(callId);
259
+ this._state.completed.push(completed);
260
+
261
+ // If all tools done, transition to ready
262
+ if (this._state.pending.size === 0) {
263
+ this._state = { status: 'ready', toolResults: this._state.completed, stream: this._state.stream };
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Start a yielding stream for inference.
269
+ * Returns the stream — the caller (framework) iterates it.
270
+ */
271
+ async startStream(
272
+ availableTools: ToolDefinition[],
273
+ budget?: TokenBudget
274
+ ): Promise<StartStreamResult> {
275
+ return this.startStreamWithInjections(availableTools, undefined, budget);
276
+ }
277
+
278
+ /**
279
+ * Start a yielding stream with context injections.
280
+ * Same as startStream but passes injections through to compile.
281
+ */
282
+ async startStreamWithInjections(
283
+ availableTools: ToolDefinition[],
284
+ injections?: ContextInjection[],
285
+ budget?: TokenBudget
286
+ ): Promise<StartStreamResult> {
287
+ if (this._state.status !== 'idle') {
288
+ throw new Error(`Agent ${this.name} cannot start stream in state ${this._state.status}`);
289
+ }
290
+
291
+ this._streamId++;
292
+ this._inferenceStartedAt = Date.now();
293
+ this.lastStreamInputTokens = 0;
294
+
295
+ let { messages, systemInjections } = await this.compileWithInjections(budget, injections);
296
+
297
+ // Safety: ensure messages don't end with an assistant message.
298
+ // Some models reject trailing assistant messages ("prefill not supported"),
299
+ // and after context compression a stale assistant turn can end up last.
300
+ if (messages.length > 0 && messages[messages.length - 1]!.participant === this.name) {
301
+ messages = [...messages, {
302
+ participant: 'user',
303
+ content: [{ type: 'text', text: '[Continue]' }],
304
+ }];
305
+ }
306
+
307
+ const request: NormalizedRequest = {
308
+ messages,
309
+ system: this.buildSystemPrompt(systemInjections),
310
+ config: {
311
+ model: this.model,
312
+ maxTokens: this.maxTokens,
313
+ temperature: this.temperature,
314
+ },
315
+ tools: availableTools.length > 0 ? availableTools : undefined,
316
+ promptCaching: true,
317
+ assistantParticipant: this.name,
318
+ };
319
+
320
+ const stream = this.membrane.streamYielding(request, {
321
+ emitTokens: true,
322
+ emitBlocks: false,
323
+ emitUsage: true,
324
+ });
325
+
326
+ this._state = { status: 'streaming', stream };
327
+ return { stream, request };
328
+ }
329
+
330
+ /**
331
+ * Transition to waiting_for_tools when stream yields tool calls.
332
+ * Called by framework's driveStream.
333
+ */
334
+ enterWaitingForTools(calls: ToolCall[], stream: YieldingStream): void {
335
+ const pending = new Map<ToolCallId, PendingToolCall>();
336
+ for (const call of calls) {
337
+ pending.set(call.id, {
338
+ id: call.id,
339
+ name: call.name,
340
+ input: call.input,
341
+ startedAt: Date.now(),
342
+ });
343
+ }
344
+ this._state = { status: 'waiting_for_tools', pending, completed: [], stream };
345
+ }
346
+
347
+ /**
348
+ * Add an assistant response to context.
349
+ * Called by framework when stream completes.
350
+ */
351
+ addAssistantResponse(content: ContentBlock[]): void {
352
+ this.contextManager.addMessage(this.name, content);
353
+ }
354
+
355
+ /**
356
+ * Transition back to streaming state after tool results are provided.
357
+ */
358
+ setStreaming(stream: YieldingStream): void {
359
+ this._state = { status: 'streaming', stream };
360
+ }
361
+
362
+ /**
363
+ * Cancel any active stream and reset to idle.
364
+ */
365
+ cancelStream(): void {
366
+ if (this._state.status === 'streaming') {
367
+ this._state.stream.cancel();
368
+ } else if (this._state.status === 'waiting_for_tools' && this._state.stream) {
369
+ this._state.stream.cancel();
370
+ }
371
+ this._state = { status: 'idle' };
372
+ }
373
+
374
+ /**
375
+ * Check if agent has pending tool calls.
376
+ */
377
+ hasPendingTools(): boolean {
378
+ return this._state.status === 'waiting_for_tools' && this._state.pending.size > 0;
379
+ }
380
+
381
+ /**
382
+ * Get pending tool call IDs.
383
+ */
384
+ getPendingToolIds(): ToolCallId[] {
385
+ if (this._state.status !== 'waiting_for_tools') {
386
+ return [];
387
+ }
388
+ return Array.from(this._state.pending.keys());
389
+ }
390
+
391
+ /**
392
+ * Reset agent to idle state.
393
+ */
394
+ reset(): void {
395
+ this._state = { status: 'idle' };
396
+ }
397
+
398
+ /**
399
+ * Get the context manager.
400
+ */
401
+ getContextManager(): ContextManager {
402
+ return this.contextManager;
403
+ }
404
+
405
+ /**
406
+ * Build the effective system prompt, appending any system-position injections.
407
+ */
408
+ private buildSystemPrompt(systemInjections: ContentBlock[]): string {
409
+ if (systemInjections.length === 0) {
410
+ return this.systemPrompt;
411
+ }
412
+
413
+ const injectedText = systemInjections
414
+ .filter((block): block is ContentBlock & { type: 'text' } => block.type === 'text')
415
+ .map((block) => block.text);
416
+
417
+ if (injectedText.length === 0) {
418
+ return this.systemPrompt;
419
+ }
420
+
421
+ return this.systemPrompt + '\n' + injectedText.join('\n');
422
+ }
423
+
424
+ abortInference(reason?: string): { aborted: true; durationMs: number } | false {
425
+ if (this._state.status === 'inferring') {
426
+ const durationMs = Date.now() - this._inferenceStartedAt;
427
+ this._state.abortController.abort(reason);
428
+ return { aborted: true, durationMs };
429
+ }
430
+
431
+ if (this._state.status === 'streaming' ||
432
+ (this._state.status === 'waiting_for_tools' && this._state.stream)) {
433
+ const durationMs = Date.now() - this._inferenceStartedAt;
434
+ this.cancelStream();
435
+ return { aborted: true, durationMs };
436
+ }
437
+
438
+ return false;
439
+ }
440
+
441
+ private async doInference(
442
+ request: NormalizedRequest,
443
+ signal?: AbortSignal
444
+ ): Promise<InferenceResult> {
445
+ const response = await this.membrane.stream(request, { signal });
446
+
447
+ if (isAbortedResponse(response)) {
448
+ const partialContent = response.partialContent ?? [];
449
+ const { toolCalls, speechContent } = this.extractToolCallsAndSpeech(partialContent);
450
+ return {
451
+ toolCalls,
452
+ speechContent,
453
+ usage: response.partialUsage,
454
+ stopReason: 'abort',
455
+ aborted: true,
456
+ abortReason: response.reason,
457
+ };
458
+ }
459
+
460
+ const { toolCalls, speechContent } = this.extractToolCallsAndSpeech(response.content);
461
+
462
+ // Add assistant response to context
463
+ this.contextManager.addMessage(this.name, response.content);
464
+
465
+ return {
466
+ toolCalls,
467
+ speechContent,
468
+ raw: response.raw,
469
+ usage: response.usage,
470
+ stopReason: response.stopReason,
471
+ };
472
+ }
473
+
474
+ private extractToolCallsAndSpeech(content: ContentBlock[]): {
475
+ toolCalls: ToolCall[];
476
+ speechContent: ContentBlock[];
477
+ } {
478
+ const toolCalls: ToolCall[] = [];
479
+ const speechContent: ContentBlock[] = [];
480
+
481
+ for (const block of content) {
482
+ if (block.type === 'tool_use') {
483
+ toolCalls.push({
484
+ id: block.id,
485
+ name: block.name,
486
+ input: block.input,
487
+ });
488
+ } else if (block.type === 'text') {
489
+ speechContent.push(block);
490
+ }
491
+ }
492
+
493
+ return { toolCalls, speechContent };
494
+ }
495
+
496
+ private buildToolResultMessages(results: CompletedToolCall[]): NormalizedMessage[] {
497
+ // Tool results go as a user message with tool_result blocks
498
+ const content = results.map((r) => ({
499
+ type: 'tool_result' as const,
500
+ toolUseId: r.id,
501
+ content: r.result.isError
502
+ ? r.result.error ?? 'Unknown error'
503
+ : JSON.stringify(r.result.data),
504
+ isError: r.result.isError,
505
+ }));
506
+
507
+ return [{
508
+ participant: 'user',
509
+ content,
510
+ }];
511
+ }
512
+ }