@yolk-sdk/agent 0.0.1-canary.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 (161) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -0
  3. package/dist/client/index.d.mts +3 -0
  4. package/dist/client/index.mjs +3 -0
  5. package/dist/client/state.d.mts +99 -0
  6. package/dist/client/state.d.mts.map +1 -0
  7. package/dist/client/state.mjs +245 -0
  8. package/dist/client/state.mjs.map +1 -0
  9. package/dist/client/transport.d.mts +67 -0
  10. package/dist/client/transport.d.mts.map +1 -0
  11. package/dist/client/transport.mjs +219 -0
  12. package/dist/client/transport.mjs.map +1 -0
  13. package/dist/index.d.mts +1 -0
  14. package/dist/index.mjs +1 -0
  15. package/dist/loop/accumulator.d.mts +11 -0
  16. package/dist/loop/accumulator.d.mts.map +1 -0
  17. package/dist/loop/accumulator.mjs +40 -0
  18. package/dist/loop/accumulator.mjs.map +1 -0
  19. package/dist/loop/error.d.mts +36 -0
  20. package/dist/loop/error.d.mts.map +1 -0
  21. package/dist/loop/error.mjs +84 -0
  22. package/dist/loop/error.mjs.map +1 -0
  23. package/dist/loop/index.d.mts +9 -0
  24. package/dist/loop/index.mjs +9 -0
  25. package/dist/loop/llm-event.d.mts +44 -0
  26. package/dist/loop/llm-event.d.mts.map +1 -0
  27. package/dist/loop/llm-event.mjs +34 -0
  28. package/dist/loop/llm-event.mjs.map +1 -0
  29. package/dist/loop/run.d.mts +37 -0
  30. package/dist/loop/run.d.mts.map +1 -0
  31. package/dist/loop/run.mjs +624 -0
  32. package/dist/loop/run.mjs.map +1 -0
  33. package/dist/loop/services/context-transformer.d.mts +18 -0
  34. package/dist/loop/services/context-transformer.d.mts.map +1 -0
  35. package/dist/loop/services/context-transformer.mjs +12 -0
  36. package/dist/loop/services/context-transformer.mjs.map +1 -0
  37. package/dist/loop/services/llm-provider.d.mts +20 -0
  38. package/dist/loop/services/llm-provider.d.mts.map +1 -0
  39. package/dist/loop/services/llm-provider.mjs +7 -0
  40. package/dist/loop/services/llm-provider.mjs.map +1 -0
  41. package/dist/loop/services/loop-config.d.mts +17 -0
  42. package/dist/loop/services/loop-config.d.mts.map +1 -0
  43. package/dist/loop/services/loop-config.mjs +15 -0
  44. package/dist/loop/services/loop-config.mjs.map +1 -0
  45. package/dist/loop/services/tool-executor.d.mts +12 -0
  46. package/dist/loop/services/tool-executor.d.mts.map +1 -0
  47. package/dist/loop/services/tool-executor.mjs +7 -0
  48. package/dist/loop/services/tool-executor.mjs.map +1 -0
  49. package/dist/loop/testing/faux-provider.d.mts +31 -0
  50. package/dist/loop/testing/faux-provider.d.mts.map +1 -0
  51. package/dist/loop/testing/faux-provider.mjs +47 -0
  52. package/dist/loop/testing/faux-provider.mjs.map +1 -0
  53. package/dist/loop/testing/index.d.mts +3 -0
  54. package/dist/loop/testing/index.mjs +3 -0
  55. package/dist/loop/testing/test-tool-executor.d.mts +10 -0
  56. package/dist/loop/testing/test-tool-executor.d.mts.map +1 -0
  57. package/dist/loop/testing/test-tool-executor.mjs +21 -0
  58. package/dist/loop/testing/test-tool-executor.mjs.map +1 -0
  59. package/dist/protocol/capability.d.mts +20 -0
  60. package/dist/protocol/capability.d.mts.map +1 -0
  61. package/dist/protocol/capability.mjs +34 -0
  62. package/dist/protocol/capability.mjs.map +1 -0
  63. package/dist/protocol/content.d.mts +31 -0
  64. package/dist/protocol/content.d.mts.map +1 -0
  65. package/dist/protocol/content.mjs +52 -0
  66. package/dist/protocol/content.mjs.map +1 -0
  67. package/dist/protocol/event.d.mts +228 -0
  68. package/dist/protocol/event.d.mts.map +1 -0
  69. package/dist/protocol/event.mjs +217 -0
  70. package/dist/protocol/event.mjs.map +1 -0
  71. package/dist/protocol/index.d.mts +14 -0
  72. package/dist/protocol/index.d.mts.map +1 -0
  73. package/dist/protocol/index.mjs +9 -0
  74. package/dist/protocol/message.d.mts +53 -0
  75. package/dist/protocol/message.d.mts.map +1 -0
  76. package/dist/protocol/message.mjs +49 -0
  77. package/dist/protocol/message.mjs.map +1 -0
  78. package/dist/protocol/reasoning.d.mts +8 -0
  79. package/dist/protocol/reasoning.d.mts.map +1 -0
  80. package/dist/protocol/reasoning.mjs +13 -0
  81. package/dist/protocol/reasoning.mjs.map +1 -0
  82. package/dist/protocol/session.d.mts +39 -0
  83. package/dist/protocol/session.d.mts.map +1 -0
  84. package/dist/protocol/session.mjs +38 -0
  85. package/dist/protocol/session.mjs.map +1 -0
  86. package/dist/protocol/tool.d.mts +101 -0
  87. package/dist/protocol/tool.d.mts.map +1 -0
  88. package/dist/protocol/tool.mjs +102 -0
  89. package/dist/protocol/tool.mjs.map +1 -0
  90. package/dist/protocol/usage.d.mts +26 -0
  91. package/dist/protocol/usage.d.mts.map +1 -0
  92. package/dist/protocol/usage.mjs +40 -0
  93. package/dist/protocol/usage.mjs.map +1 -0
  94. package/dist/runtime/error.d.mts +29 -0
  95. package/dist/runtime/error.d.mts.map +1 -0
  96. package/dist/runtime/error.mjs +46 -0
  97. package/dist/runtime/error.mjs.map +1 -0
  98. package/dist/runtime/index.d.mts +9 -0
  99. package/dist/runtime/index.d.mts.map +1 -0
  100. package/dist/runtime/index.mjs +4 -0
  101. package/dist/runtime/run-runtime.d.mts +47 -0
  102. package/dist/runtime/run-runtime.d.mts.map +1 -0
  103. package/dist/runtime/run-runtime.mjs +112 -0
  104. package/dist/runtime/run-runtime.mjs.map +1 -0
  105. package/dist/runtime/session-event-store.d.mts +75 -0
  106. package/dist/runtime/session-event-store.d.mts.map +1 -0
  107. package/dist/runtime/session-event-store.mjs +124 -0
  108. package/dist/runtime/session-event-store.mjs.map +1 -0
  109. package/dist/tools/index.d.mts +4 -0
  110. package/dist/tools/index.mjs +4 -0
  111. package/dist/tools/question.d.mts +21 -0
  112. package/dist/tools/question.d.mts.map +1 -0
  113. package/dist/tools/question.mjs +41 -0
  114. package/dist/tools/question.mjs.map +1 -0
  115. package/dist/tools/registry.d.mts +61 -0
  116. package/dist/tools/registry.d.mts.map +1 -0
  117. package/dist/tools/registry.mjs +113 -0
  118. package/dist/tools/registry.mjs.map +1 -0
  119. package/dist/tools/task.d.mts +34 -0
  120. package/dist/tools/task.d.mts.map +1 -0
  121. package/dist/tools/task.mjs +81 -0
  122. package/dist/tools/task.mjs.map +1 -0
  123. package/package.json +86 -0
  124. package/src/client/README.md +23 -0
  125. package/src/client/index.ts +43 -0
  126. package/src/client/state.ts +380 -0
  127. package/src/client/transport.ts +517 -0
  128. package/src/index.ts +2 -0
  129. package/src/loop/README.md +23 -0
  130. package/src/loop/accumulator.ts +71 -0
  131. package/src/loop/error.ts +105 -0
  132. package/src/loop/index.ts +35 -0
  133. package/src/loop/llm-event.ts +52 -0
  134. package/src/loop/run.ts +1237 -0
  135. package/src/loop/services/context-transformer.ts +24 -0
  136. package/src/loop/services/llm-provider.ts +20 -0
  137. package/src/loop/services/loop-config.ts +20 -0
  138. package/src/loop/services/tool-executor.ts +11 -0
  139. package/src/loop/testing/faux-provider.ts +94 -0
  140. package/src/loop/testing/index.ts +3 -0
  141. package/src/loop/testing/test-tool-executor.ts +28 -0
  142. package/src/protocol/README.md +24 -0
  143. package/src/protocol/capability.ts +29 -0
  144. package/src/protocol/content.ts +76 -0
  145. package/src/protocol/event.ts +286 -0
  146. package/src/protocol/index.ts +109 -0
  147. package/src/protocol/message.ts +86 -0
  148. package/src/protocol/reasoning.ts +4 -0
  149. package/src/protocol/session.ts +47 -0
  150. package/src/protocol/tool.ts +154 -0
  151. package/src/protocol/usage.ts +48 -0
  152. package/src/runtime/README.md +44 -0
  153. package/src/runtime/error.ts +70 -0
  154. package/src/runtime/index.ts +43 -0
  155. package/src/runtime/run-runtime.ts +307 -0
  156. package/src/runtime/session-event-store.ts +254 -0
  157. package/src/tools/README.md +22 -0
  158. package/src/tools/index.ts +29 -0
  159. package/src/tools/question.ts +58 -0
  160. package/src/tools/registry.ts +228 -0
  161. package/src/tools/task.ts +132 -0
@@ -0,0 +1,1237 @@
1
+ import { Clock, Effect, Ref, Stream } from 'effect'
2
+ import * as Schema from 'effect/Schema'
3
+ import {
4
+ AgentAwaitingInput,
5
+ AgentEnd,
6
+ AgentRetry,
7
+ AgentStart,
8
+ AssistantMessageEvent,
9
+ UsageUpdate,
10
+ addAgentUsage,
11
+ contentParts,
12
+ contentPreview,
13
+ LLMReasoningDelta as AgentLLMReasoningDelta,
14
+ LLMStreamEnd,
15
+ LLMStreamStart,
16
+ LLMTextDelta as AgentLLMTextDelta,
17
+ ToolExecutionCompleted,
18
+ ToolExecutionError,
19
+ ToolExecutionStarted,
20
+ ToolApprovalDenied,
21
+ ToolApprovalGranted,
22
+ ToolApprovalRequested,
23
+ ToolInputEnd,
24
+ ToolInputDelta,
25
+ ToolInputStart,
26
+ QuestionAnswered,
27
+ QuestionCancelled,
28
+ QuestionRequested,
29
+ ProviderToolResult,
30
+ QuestionRequest,
31
+ QuestionToolParams,
32
+ formatQuestionResponseContent,
33
+ ToolApprovalRequest,
34
+ ToolResultMessage,
35
+ SubagentCompleted,
36
+ SubagentStarted,
37
+ assistantHostToolCalls,
38
+ type ToolCall,
39
+ type AgentReasoningEffort,
40
+ type HitlRequest,
41
+ type HitlResponse,
42
+ type QuestionPrompt,
43
+ type QuestionResponse,
44
+ type ToolApprovalResponse,
45
+ ToolResult,
46
+ TurnEnd,
47
+ TurnStart,
48
+ zeroAgentUsage,
49
+ type AgentEvent,
50
+ type AgentErrorCode,
51
+ type AgentMessage,
52
+ type AgentUsage,
53
+ type AgentModelCapabilities,
54
+ type ToolDef
55
+ } from '@yolk-sdk/agent/protocol'
56
+ import { accumulateAssistantMessage, collectToolCalls } from './accumulator.ts'
57
+ import {
58
+ AbortError,
59
+ LLMError,
60
+ ToolError,
61
+ type AgentLoopError,
62
+ type LLMProviderError,
63
+ } from './error.ts'
64
+ import type { LLMEvent } from './llm-event.ts'
65
+ import { ContextTransformer, type ContextTransformResult } from './services/context-transformer.ts'
66
+ import { LLMProvider, type LLMRequest } from './services/llm-provider.ts'
67
+ import { LoopConfig, type LoopConfigShape } from './services/loop-config.ts'
68
+ import { ToolExecutor } from './services/tool-executor.ts'
69
+
70
+ export type AgentLoopRunId = string
71
+
72
+ export type RunConfig = {
73
+ readonly messages: ReadonlyArray<AgentMessage>
74
+ readonly systemPrompt: string
75
+ readonly tools: ReadonlyArray<ToolDef>
76
+ readonly hitlResponses?: ReadonlyArray<HitlResponse>
77
+ readonly model: string
78
+ readonly reasoningEffort?: AgentReasoningEffort
79
+ readonly capabilities?: AgentModelCapabilities
80
+ }
81
+
82
+ export type ModelTurnConfig = RunConfig & {
83
+ readonly turn: number
84
+ }
85
+
86
+ export type ToolBatchConfig = {
87
+ readonly calls: ReadonlyArray<ToolCall>
88
+ readonly tools?: ReadonlyArray<ToolDef>
89
+ readonly hitlResponses?: ReadonlyArray<HitlResponse>
90
+ readonly model?: string
91
+ readonly createdMessages?: ReadonlyArray<AgentMessage>
92
+ readonly turn?: number
93
+ readonly usage?: AgentUsage
94
+ }
95
+
96
+ const questionToolName = 'question'
97
+
98
+ type TaskCallMetadata = {
99
+ readonly subagentRunId: string
100
+ readonly subagentType: string
101
+ readonly description: string
102
+ }
103
+
104
+ const objectField = (input: unknown, key: string) =>
105
+ input !== null && typeof input === 'object' ? Object.getOwnPropertyDescriptor(input, key)?.value : undefined
106
+
107
+ const nonEmptyStringField = (input: unknown, key: string) => {
108
+ const value = objectField(input, key)
109
+
110
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined
111
+ }
112
+
113
+ const taskCallMetadata = (call: ToolCall): TaskCallMetadata | undefined => {
114
+ if (call.name !== 'task') {
115
+ return undefined
116
+ }
117
+
118
+ const subagentType = nonEmptyStringField(call.params, 'subagent_type')
119
+ const description = nonEmptyStringField(call.params, 'description')
120
+
121
+ if (subagentType === undefined || description === undefined) {
122
+ return undefined
123
+ }
124
+
125
+ return {
126
+ subagentRunId: `subagent:${call.id}`,
127
+ subagentType,
128
+ description
129
+ }
130
+ }
131
+
132
+ const subagentStartedEvent = (input: {
133
+ readonly call: ToolCall
134
+ readonly model: string
135
+ readonly startedAtMs: number
136
+ }) => {
137
+ const metadata = taskCallMetadata(input.call)
138
+
139
+ return metadata === undefined
140
+ ? undefined
141
+ : SubagentStarted.make({
142
+ parentToolCallId: input.call.id,
143
+ subagentRunId: metadata.subagentRunId,
144
+ subagentType: metadata.subagentType,
145
+ description: metadata.description,
146
+ model: input.model,
147
+ createdAtMs: input.startedAtMs
148
+ })
149
+ }
150
+
151
+ const subagentCompletedEvent = (input: {
152
+ readonly call: ToolCall
153
+ readonly result: ToolResult
154
+ readonly model: string
155
+ readonly startedAtMs: number
156
+ readonly endedAtMs: number
157
+ }) => {
158
+ const metadata = taskCallMetadata(input.call)
159
+
160
+ return metadata === undefined
161
+ ? undefined
162
+ : SubagentCompleted.make({
163
+ parentToolCallId: input.call.id,
164
+ subagentRunId: metadata.subagentRunId,
165
+ subagentType: metadata.subagentType,
166
+ description: metadata.description,
167
+ model: input.model,
168
+ status: input.result.isError === true ? 'error' : 'completed',
169
+ durationMs: Math.max(0, input.endedAtMs - input.startedAtMs),
170
+ summary: contentPreview(input.result.content),
171
+ createdAtMs: input.endedAtMs
172
+ })
173
+ }
174
+
175
+ const unsupportedInputError = (message: string) =>
176
+ new LLMError({
177
+ cause: 'validation_error',
178
+ message,
179
+ retryable: false
180
+ })
181
+
182
+ const validateContent = (message: AgentMessage, capabilities: AgentModelCapabilities) =>
183
+ Effect.forEach(contentPartsFromMessage(message), part => {
184
+ switch (part._tag) {
185
+ case 'Text':
186
+ return capabilities.input.text
187
+ ? Effect.void
188
+ : Effect.fail(unsupportedInputError('Text input is not supported by this model'))
189
+ case 'Image':
190
+ return capabilities.input.image
191
+ ? Effect.void
192
+ : Effect.fail(unsupportedInputError('Image input is not supported by this model'))
193
+ case 'Audio':
194
+ return capabilities.input.audio
195
+ ? Effect.void
196
+ : Effect.fail(unsupportedInputError('Audio input is not supported by this model'))
197
+ }
198
+ })
199
+
200
+ const contentPartsFromMessage = (message: AgentMessage) => {
201
+ switch (message._tag) {
202
+ case 'User':
203
+ case 'ToolResult':
204
+ return contentParts(message.content)
205
+ case 'Assistant':
206
+ return message.parts.flatMap(part => (part._tag === 'Text' ? contentParts(part.content) : []))
207
+ }
208
+ }
209
+
210
+ const validateCapabilities = (
211
+ config: RunConfig,
212
+ messages: ReadonlyArray<AgentMessage>
213
+ ): Effect.Effect<void, LLMError> => {
214
+ const capabilities = config.capabilities
215
+
216
+ if (capabilities === undefined) {
217
+ return Effect.void
218
+ }
219
+
220
+ if (!capabilities.tools && config.tools.length > 0) {
221
+ return Effect.fail(unsupportedInputError('Tools are not supported by this model'))
222
+ }
223
+
224
+ if (!capabilities.reasoning && config.reasoningEffort !== undefined) {
225
+ return Effect.fail(unsupportedInputError('Reasoning effort is not supported by this model'))
226
+ }
227
+
228
+ return Effect.forEach(messages, message => validateContent(message, capabilities)).pipe(
229
+ Effect.asVoid
230
+ )
231
+ }
232
+
233
+ const toLlmEvent = (event: LLMEvent): ReadonlyArray<AgentEvent> => {
234
+ switch (event._tag) {
235
+ case 'TextDelta':
236
+ return [AgentLLMTextDelta.make({ text: event.text })]
237
+ case 'ReasoningDelta':
238
+ return [AgentLLMReasoningDelta.make({ text: event.text })]
239
+ case 'ToolCall':
240
+ return [ToolInputEnd.make({ call: event.call })]
241
+ case 'ToolInputStart':
242
+ return [ToolInputStart.make({ id: event.id, name: event.name })]
243
+ case 'ToolInputDelta':
244
+ return [ToolInputDelta.make({ id: event.id, delta: event.delta })]
245
+ case 'ProviderToolResult':
246
+ return [ProviderToolResult.make({ call: event.call, result: event.result })]
247
+ case 'Usage':
248
+ return [UsageUpdate.make({ usage: event.usage })]
249
+ case 'Done':
250
+ return []
251
+ }
252
+ }
253
+
254
+ const isLlmEvent = (event: LLMEvent | AgentEvent | AgentRetry): event is LLMEvent => {
255
+ switch (event._tag) {
256
+ case 'TextDelta':
257
+ case 'ReasoningDelta':
258
+ case 'Done':
259
+ case 'ToolCall':
260
+ case 'ToolInputStart':
261
+ case 'ToolInputDelta':
262
+ case 'ProviderToolResult':
263
+ case 'Usage':
264
+ return true
265
+ default:
266
+ return false
267
+ }
268
+ }
269
+
270
+ type TurnStreamInput = {
271
+ readonly config: RunConfig
272
+ readonly contextTransformer: {
273
+ readonly transform: (
274
+ messages: ReadonlyArray<AgentMessage>
275
+ ) => Effect.Effect<ContextTransformResult, AgentLoopError>
276
+ }
277
+ readonly loopConfig: LoopConfigShape
278
+ readonly provider: {
279
+ readonly stream: (request: LLMRequest) => Stream.Stream<LLMEvent, LLMProviderError>
280
+ }
281
+ readonly executor: {
282
+ readonly execute: (call: ToolCall) => Effect.Effect<ToolResult, ToolError>
283
+ }
284
+ readonly currentMessages: ReadonlyArray<AgentMessage>
285
+ readonly createdMessages: Ref.Ref<ReadonlyArray<AgentMessage>>
286
+ readonly usage: Ref.Ref<AgentUsage>
287
+ readonly turn: number
288
+ }
289
+
290
+ const retryDelayMs = (baseDelayMs: number, attempt: number) =>
291
+ Math.max(0, Math.floor(baseDelayMs * 2 ** Math.max(0, attempt - 1)))
292
+
293
+ const retryReason = (error: LLMError): AgentErrorCode => error.cause
294
+
295
+ const retrySleep = (delayMs: number) =>
296
+ delayMs === 0 ? Effect.void : Effect.sleep(`${delayMs} millis`)
297
+
298
+ const failAgentLoopError = (
299
+ error: AgentLoopError
300
+ ): Stream.Stream<LLMEvent | AgentRetry, AgentLoopError> => Stream.fail(error)
301
+
302
+ const sleepStream = (delayMs: number): Stream.Stream<LLMEvent | AgentRetry, AgentLoopError> =>
303
+ Stream.fromEffect(retrySleep(delayMs)).pipe(Stream.flatMap(() => Stream.empty))
304
+
305
+ const withProviderRetries = (
306
+ stream: Stream.Stream<LLMEvent, LLMProviderError>,
307
+ loopConfig: TurnStreamInput['loopConfig'],
308
+ makeStream: () => Stream.Stream<LLMEvent, LLMProviderError>,
309
+ attempt: number
310
+ ): Stream.Stream<LLMEvent | AgentRetry, AgentLoopError> =>
311
+ Stream.unwrap(
312
+ Ref.make(false).pipe(
313
+ Effect.map(emittedProviderEvent =>
314
+ stream.pipe(
315
+ Stream.tap(() => Ref.set(emittedProviderEvent, true)),
316
+ Stream.catchTags({
317
+ LLMError: error =>
318
+ Stream.unwrap(
319
+ Ref.get(emittedProviderEvent).pipe(
320
+ Effect.map(emitted => {
321
+ if (
322
+ emitted ||
323
+ !error.retryable ||
324
+ error.cause === 'context_overflow' ||
325
+ attempt > loopConfig.maxRetries
326
+ ) {
327
+ return failAgentLoopError(error)
328
+ }
329
+
330
+ const delayMs = retryDelayMs(loopConfig.retryBaseDelayMs, attempt)
331
+ return Stream.make(
332
+ AgentRetry.make({
333
+ attempt,
334
+ reason: retryReason(error),
335
+ delayMs,
336
+ message: error.message
337
+ })
338
+ ).pipe(
339
+ Stream.concat(sleepStream(delayMs)),
340
+ Stream.concat(
341
+ withProviderRetries(makeStream(), loopConfig, makeStream, attempt + 1)
342
+ )
343
+ )
344
+ })
345
+ )
346
+ ),
347
+ AbortError: failAgentLoopError,
348
+ FauxExhaustedError: failAgentLoopError
349
+ })
350
+ )
351
+ )
352
+ )
353
+ )
354
+
355
+ const makeToolExecutionStream = (
356
+ executor: TurnStreamInput['executor'],
357
+ call: ToolCall,
358
+ model: string
359
+ ): Stream.Stream<AgentEvent, AgentLoopError> =>
360
+ Stream.unwrap(
361
+ Effect.gen(function* () {
362
+ const startedAtMs = yield* Clock.currentTimeMillis
363
+ const started = subagentStartedEvent({ call, model, startedAtMs })
364
+ const startEvents: ReadonlyArray<AgentEvent> = started === undefined
365
+ ? [ToolExecutionStarted.make({ call, createdAtMs: startedAtMs })]
366
+ : [ToolExecutionStarted.make({ call, createdAtMs: startedAtMs }), started]
367
+
368
+ return Stream.fromIterable(startEvents).pipe(
369
+ Stream.concat(
370
+ Stream.fromEffect(
371
+ executor.execute(call).pipe(
372
+ Effect.flatMap(result =>
373
+ Clock.currentTimeMillis.pipe(
374
+ Effect.map(endedAtMs => {
375
+ const completed = subagentCompletedEvent({
376
+ call,
377
+ result,
378
+ model,
379
+ startedAtMs,
380
+ endedAtMs
381
+ })
382
+ const toolCompleted = ToolExecutionCompleted.make({
383
+ call,
384
+ result,
385
+ createdAtMs: endedAtMs
386
+ })
387
+
388
+ return completed === undefined
389
+ ? [toolCompleted]
390
+ : [toolCompleted, completed]
391
+ })
392
+ )
393
+ )
394
+ )
395
+ ).pipe(
396
+ Stream.flatMap(Stream.fromIterable),
397
+ Stream.catchTag('ToolError', error =>
398
+ Stream.fromEffect(Clock.currentTimeMillis).pipe(
399
+ Stream.flatMap(endedAtMs =>
400
+ Stream.make(
401
+ ToolExecutionError.make({
402
+ call,
403
+ message: error.message,
404
+ code: toolErrorCode(error),
405
+ createdAtMs: endedAtMs
406
+ })
407
+ )
408
+ ),
409
+ Stream.concat(Stream.fail(error))
410
+ )
411
+ )
412
+ )
413
+ )
414
+ )
415
+ })
416
+ )
417
+
418
+ type IndexedToolResultMessage = {
419
+ readonly index: number
420
+ readonly message: AgentMessage
421
+ }
422
+
423
+ type IndexedToolCall = {
424
+ readonly index: number
425
+ readonly call: ToolCall
426
+ }
427
+
428
+ type PreparedToolCall =
429
+ | {
430
+ readonly _tag: 'Execute'
431
+ readonly index: number
432
+ readonly call: ToolCall
433
+ readonly events: ReadonlyArray<AgentEvent>
434
+ }
435
+ | {
436
+ readonly _tag: 'Result'
437
+ readonly index: number
438
+ readonly call: ToolCall
439
+ readonly result: ToolResult
440
+ readonly events: ReadonlyArray<AgentEvent>
441
+ }
442
+ | {
443
+ readonly _tag: 'Pending'
444
+ readonly request: HitlRequest
445
+ readonly events: ReadonlyArray<AgentEvent>
446
+ }
447
+
448
+ type PreparedToolBatch = {
449
+ readonly callsToExecute: ReadonlyArray<IndexedToolCall>
450
+ readonly resultMessages: ReadonlyArray<IndexedToolResultMessage>
451
+ readonly resultEvents: ReadonlyArray<AgentEvent>
452
+ readonly events: ReadonlyArray<AgentEvent>
453
+ readonly pendingRequests: ReadonlyArray<HitlRequest>
454
+ readonly pendingEvents: ReadonlyArray<AgentEvent>
455
+ }
456
+
457
+ type NonEmptyHitlRequests = readonly [HitlRequest, ...Array<HitlRequest>]
458
+
459
+ const boundedToolConcurrency = (loopConfig: LoopConfigShape) =>
460
+ Math.max(1, loopConfig.toolConcurrency)
461
+
462
+ const toolResultMessageFromResult = (result: ToolResult) =>
463
+ ToolResultMessage.make({
464
+ toolCallId: result.toolCallId,
465
+ content: result.content,
466
+ isError: result.isError,
467
+ structuredContent: result.structuredContent
468
+ })
469
+
470
+ const toolDefFor = (tools: ReadonlyArray<ToolDef>, call: ToolCall) =>
471
+ tools.find(tool => tool.name === call.name)
472
+
473
+ const approvalRequired = (tools: ReadonlyArray<ToolDef>, call: ToolCall) =>
474
+ toolDefFor(tools, call)?.approval?.mode === 'manual'
475
+
476
+ const approvalRequestId = (call: ToolCall) => `approval:${call.id}`
477
+
478
+ const questionRequestId = (call: ToolCall) => `question:${call.id}`
479
+
480
+ const matchesApproval = (response: ToolApprovalResponse, call: ToolCall) =>
481
+ response.toolCallId === call.id || response.requestId === approvalRequestId(call)
482
+
483
+ const matchesQuestion = (response: QuestionResponse, call: ToolCall) =>
484
+ response.toolCallId === call.id || response.requestId === questionRequestId(call)
485
+
486
+ const approvalResponseFor = (responses: ReadonlyArray<HitlResponse>, call: ToolCall) =>
487
+ responses.flatMap(response =>
488
+ response._tag === 'ToolApprovalResponse' && matchesApproval(response, call) ? [response] : []
489
+ )[0]
490
+
491
+ const questionResponseFor = (responses: ReadonlyArray<HitlResponse>, call: ToolCall) =>
492
+ responses.flatMap(response =>
493
+ response._tag === 'QuestionResponse' && matchesQuestion(response, call) ? [response] : []
494
+ )[0]
495
+
496
+ const toolApprovalRequest = (
497
+ tools: ReadonlyArray<ToolDef>,
498
+ call: ToolCall
499
+ ): ToolApprovalRequest =>
500
+ ToolApprovalRequest.make({
501
+ requestId: approvalRequestId(call),
502
+ toolCallId: call.id,
503
+ call,
504
+ policy: toolDefFor(tools, call)?.approval
505
+ })
506
+
507
+ const deniedToolResult = (call: ToolCall, response: ToolApprovalResponse) => {
508
+ const reason = response.reason ?? 'Denied by user'
509
+
510
+ return ToolResult.make({
511
+ toolCallId: call.id,
512
+ content: `Tool call denied: ${reason}`,
513
+ isError: true,
514
+ structuredContent: {
515
+ type: 'tool_approval_denied',
516
+ reason,
517
+ source: response.source
518
+ }
519
+ })
520
+ }
521
+
522
+ const questionToolResult = (
523
+ response: QuestionResponse,
524
+ questions: ReadonlyArray<QuestionPrompt>
525
+ ) => {
526
+ return ToolResult.make({
527
+ toolCallId: response.toolCallId,
528
+ content: formatQuestionResponseContent(response, questions),
529
+ isError: response.outcome === 'cancelled' ? true : undefined,
530
+ structuredContent: {
531
+ type: 'question_response',
532
+ outcome: response.outcome,
533
+ answers: response.answers ?? [],
534
+ reason: response.reason,
535
+ source: response.source
536
+ }
537
+ })
538
+ }
539
+
540
+ const invalidQuestionToolResult = (call: ToolCall) =>
541
+ ToolResult.make({
542
+ toolCallId: call.id,
543
+ content: 'Invalid question arguments.',
544
+ isError: true,
545
+ structuredContent: { type: 'question_invalid' }
546
+ })
547
+
548
+ const prepareQuestionCall = (
549
+ call: ToolCall,
550
+ index: number,
551
+ responses: ReadonlyArray<HitlResponse>
552
+ ): Effect.Effect<PreparedToolCall> =>
553
+ Effect.gen(function* () {
554
+ const decoded = yield* Schema.decodeUnknownEffect(QuestionToolParams)(call.params).pipe(
555
+ Effect.result
556
+ )
557
+
558
+ if (decoded._tag === 'Failure') {
559
+ return {
560
+ _tag: 'Result',
561
+ index,
562
+ call,
563
+ result: invalidQuestionToolResult(call),
564
+ events: []
565
+ }
566
+ }
567
+
568
+ const response = questionResponseFor(responses, call)
569
+
570
+ if (response !== undefined) {
571
+ return {
572
+ _tag: 'Result',
573
+ index,
574
+ call,
575
+ result: questionToolResult(response, decoded.success.questions),
576
+ events: [
577
+ response.outcome === 'answered'
578
+ ? QuestionAnswered.make({ response })
579
+ : QuestionCancelled.make({ response })
580
+ ]
581
+ }
582
+ }
583
+
584
+ const request = QuestionRequest.make({
585
+ requestId: questionRequestId(call),
586
+ toolCallId: call.id,
587
+ call,
588
+ questions: decoded.success.questions
589
+ })
590
+
591
+ return {
592
+ _tag: 'Pending',
593
+ request,
594
+ events: [QuestionRequested.make({ request })]
595
+ }
596
+ })
597
+
598
+ const prepareApprovalCall = (
599
+ tools: ReadonlyArray<ToolDef>,
600
+ call: ToolCall,
601
+ index: number,
602
+ responses: ReadonlyArray<HitlResponse>
603
+ ): PreparedToolCall => {
604
+ if (!approvalRequired(tools, call)) {
605
+ return { _tag: 'Execute', index, call, events: [] }
606
+ }
607
+
608
+ const request = toolApprovalRequest(tools, call)
609
+ const response = approvalResponseFor(responses, call)
610
+
611
+ if (response === undefined) {
612
+ return {
613
+ _tag: 'Pending',
614
+ request,
615
+ events: [ToolApprovalRequested.make({ call, request })]
616
+ }
617
+ }
618
+
619
+ if (response.decision === 'denied') {
620
+ return {
621
+ _tag: 'Result',
622
+ index,
623
+ call,
624
+ result: deniedToolResult(call, response),
625
+ events: [ToolApprovalDenied.make({ toolCallId: call.id, reason: response.reason ?? 'Denied by user', response })]
626
+ }
627
+ }
628
+
629
+ return {
630
+ _tag: 'Execute',
631
+ index,
632
+ call,
633
+ events: [ToolApprovalGranted.make({ toolCallId: call.id, response })]
634
+ }
635
+ }
636
+
637
+ const prepareToolCall = (input: {
638
+ readonly tools: ReadonlyArray<ToolDef>
639
+ readonly responses: ReadonlyArray<HitlResponse>
640
+ readonly call: ToolCall
641
+ readonly index: number
642
+ }): Effect.Effect<PreparedToolCall> =>
643
+ input.call.name === questionToolName
644
+ ? prepareQuestionCall(input.call, input.index, input.responses)
645
+ : Effect.succeed(prepareApprovalCall(input.tools, input.call, input.index, input.responses))
646
+
647
+ const prepareToolBatch = (input: {
648
+ readonly tools: ReadonlyArray<ToolDef>
649
+ readonly responses: ReadonlyArray<HitlResponse>
650
+ readonly calls: ReadonlyArray<ToolCall>
651
+ }): Effect.Effect<PreparedToolBatch> =>
652
+ Effect.gen(function* () {
653
+ const prepared = yield* Effect.forEach(input.calls, (call, index) =>
654
+ prepareToolCall({ tools: input.tools, responses: input.responses, call, index })
655
+ )
656
+
657
+ return {
658
+ callsToExecute: prepared.flatMap(item =>
659
+ item._tag === 'Execute' ? [{ index: item.index, call: item.call }] : []
660
+ ),
661
+ resultMessages: prepared.flatMap(item =>
662
+ item._tag === 'Result'
663
+ ? [{ index: item.index, message: toolResultMessageFromResult(item.result) }]
664
+ : []
665
+ ),
666
+ resultEvents: syntheticToolCompletionEvents(prepared),
667
+ events: prepared.flatMap(item => (item._tag === 'Pending' ? [] : item.events)),
668
+ pendingRequests: prepared.flatMap(item => (item._tag === 'Pending' ? [item.request] : [])),
669
+ pendingEvents: prepared.flatMap(item => (item._tag === 'Pending' ? item.events : []))
670
+ }
671
+ })
672
+
673
+ const orderedToolResultMessages = (results: ReadonlyArray<IndexedToolResultMessage>) =>
674
+ [...results].sort((left, right) => left.index - right.index).map(result => result.message)
675
+
676
+ const syntheticToolCompletionEvents = (
677
+ prepared: ReadonlyArray<PreparedToolCall>
678
+ ): ReadonlyArray<AgentEvent> =>
679
+ prepared.flatMap(item =>
680
+ item._tag === 'Result'
681
+ ? [ToolExecutionCompleted.make({ call: item.call, result: item.result })]
682
+ : []
683
+ )
684
+
685
+ const toolResultIds = (messages: ReadonlyArray<AgentMessage>): ReadonlySet<string> =>
686
+ new Set(messages.flatMap(message => (message._tag === 'ToolResult' ? [message.toolCallId] : [])))
687
+
688
+ const pendingHostToolCalls = (messages: ReadonlyArray<AgentMessage>) => {
689
+ const completed = toolResultIds(messages)
690
+
691
+ return messages.flatMap(message =>
692
+ message._tag === 'Assistant'
693
+ ? assistantHostToolCalls(message).filter(call => !completed.has(call.id))
694
+ : []
695
+ )
696
+ }
697
+
698
+ const nonEmptyHitlRequests = (
699
+ requests: ReadonlyArray<HitlRequest>
700
+ ): NonEmptyHitlRequests | undefined => {
701
+ const first = requests[0]
702
+
703
+ return first === undefined ? undefined : [first, ...requests.slice(1)]
704
+ }
705
+
706
+ const parallelToolExecutionStream = (input: {
707
+ readonly calls: ReadonlyArray<IndexedToolCall>
708
+ readonly executor: TurnStreamInput['executor']
709
+ readonly loopConfig: LoopConfigShape
710
+ readonly model: string
711
+ readonly results: Ref.Ref<ReadonlyArray<IndexedToolResultMessage>>
712
+ }) =>
713
+ Stream.mergeAll(
714
+ input.calls.map(({ call, index }) =>
715
+ makeToolExecutionStream(input.executor, call, input.model).pipe(
716
+ Stream.tap(event => {
717
+ if (event._tag !== 'ToolExecutionCompleted') {
718
+ return Effect.void
719
+ }
720
+
721
+ return Ref.update(input.results, results => [
722
+ ...results,
723
+ { index, message: toolResultMessageFromResult(event.result) }
724
+ ])
725
+ })
726
+ )
727
+ ),
728
+ { concurrency: boundedToolConcurrency(input.loopConfig) }
729
+ )
730
+
731
+ const toolErrorCode = (error: ToolError): AgentErrorCode => {
732
+ switch (error.cause) {
733
+ case 'validation':
734
+ case 'invalid_input':
735
+ return 'validation_error'
736
+ case 'timeout':
737
+ return 'tool_timeout'
738
+ case 'permission':
739
+ case 'denied':
740
+ return 'tool_denied'
741
+ case 'execution':
742
+ case 'not_found':
743
+ case 'unavailable':
744
+ return 'tool_error'
745
+ }
746
+ }
747
+
748
+ type TurnCompletion = {
749
+ readonly toolCalls: ReadonlyArray<ToolCall>
750
+ readonly stopReason: 'stop' | 'tool_use'
751
+ }
752
+
753
+ const validateTurnCompletion = (
754
+ events: ReadonlyArray<LLMEvent>
755
+ ): Effect.Effect<TurnCompletion, LLMError> => {
756
+ const doneEvents = events.filter(event => event._tag === 'Done')
757
+ const toolCalls = collectToolCalls(events)
758
+ const stopReason: TurnCompletion['stopReason'] = toolCalls.length === 0 ? 'stop' : 'tool_use'
759
+
760
+ if (doneEvents.length !== 1) {
761
+ return Effect.fail(
762
+ new LLMError({
763
+ cause: 'invalid_response',
764
+ message: `Expected exactly one LLM done event, received ${doneEvents.length}`,
765
+ retryable: false
766
+ })
767
+ )
768
+ }
769
+
770
+ const doneEvent = doneEvents[0]
771
+
772
+ if (doneEvent === undefined || doneEvent.stopReason !== stopReason) {
773
+ return Effect.fail(
774
+ new LLMError({
775
+ cause: 'invalid_response',
776
+ message: `LLM done reason must be ${stopReason}`,
777
+ retryable: false
778
+ })
779
+ )
780
+ }
781
+
782
+ return Effect.succeed({ toolCalls, stopReason })
783
+ }
784
+
785
+ const makeAfterLlmStream = (
786
+ input: TurnStreamInput,
787
+ llmEventsRef: Ref.Ref<ReadonlyArray<LLMEvent>>
788
+ ): Stream.Stream<AgentEvent, AgentLoopError> =>
789
+ Stream.unwrap(
790
+ Effect.gen(function* () {
791
+ const llmEvents = yield* Ref.get(llmEventsRef)
792
+ const completion = yield* validateTurnCompletion(llmEvents)
793
+ const assistantMessage = accumulateAssistantMessage(llmEvents)
794
+ const turnEndEvents: ReadonlyArray<AgentEvent> = [
795
+ LLMStreamEnd.make({ turn: input.turn }),
796
+ AssistantMessageEvent.make({ message: assistantMessage })
797
+ ]
798
+
799
+ yield* Ref.update(input.createdMessages, messages => [...messages, assistantMessage])
800
+
801
+ if (completion.toolCalls.length === 0) {
802
+ const messages = yield* Ref.get(input.createdMessages)
803
+ const usage = yield* Ref.get(input.usage)
804
+
805
+ return Stream.fromIterable([
806
+ ...turnEndEvents,
807
+ TurnEnd.make({ turn: input.turn, reason: completion.stopReason }),
808
+ AgentEnd.make({
809
+ messages,
810
+ turns: input.turn,
811
+ usage
812
+ })
813
+ ])
814
+ }
815
+
816
+ const toolResultMessages = yield* Ref.make<ReadonlyArray<IndexedToolResultMessage>>([])
817
+ const prepared = yield* prepareToolBatch({
818
+ tools: input.config.tools,
819
+ responses: input.config.hitlResponses ?? [],
820
+ calls: completion.toolCalls
821
+ })
822
+
823
+ if (prepared.resultMessages.length > 0) {
824
+ yield* Ref.update(toolResultMessages, results => [...results, ...prepared.resultMessages])
825
+ }
826
+
827
+ if (prepared.pendingRequests.length > 0) {
828
+ const pendingRequests = nonEmptyHitlRequests(prepared.pendingRequests)
829
+
830
+ if (pendingRequests === undefined) {
831
+ return Stream.empty
832
+ }
833
+
834
+ const readyResults = orderedToolResultMessages(yield* Ref.get(toolResultMessages))
835
+ if (readyResults.length > 0) {
836
+ yield* Ref.update(input.createdMessages, messages => [...messages, ...readyResults])
837
+ }
838
+
839
+ const messages = yield* Ref.get(input.createdMessages)
840
+ const usage = yield* Ref.get(input.usage)
841
+
842
+ return Stream.fromIterable([
843
+ ...turnEndEvents,
844
+ ...prepared.events,
845
+ ...prepared.pendingEvents,
846
+ TurnEnd.make({ turn: input.turn, reason: completion.stopReason }),
847
+ AgentAwaitingInput.make({
848
+ requests: pendingRequests,
849
+ messages,
850
+ turns: input.turn,
851
+ usage
852
+ })
853
+ ])
854
+ }
855
+
856
+ const toolExecutionStream = parallelToolExecutionStream({
857
+ calls: prepared.callsToExecute,
858
+ executor: input.executor,
859
+ loopConfig: input.loopConfig,
860
+ model: input.config.model,
861
+ results: toolResultMessages
862
+ })
863
+ const nextTurnStream = Stream.unwrap(
864
+ Ref.get(toolResultMessages).pipe(
865
+ Effect.flatMap(results => {
866
+ const orderedResults = orderedToolResultMessages(results)
867
+
868
+ return Ref.update(input.createdMessages, messages => [...messages, ...orderedResults]).pipe(
869
+ Effect.as(
870
+ Stream.make(TurnEnd.make({ turn: input.turn, reason: completion.stopReason })).pipe(
871
+ Stream.concat(
872
+ makeTurnStream({
873
+ ...input,
874
+ currentMessages: [...input.currentMessages, assistantMessage, ...orderedResults],
875
+ turn: input.turn + 1
876
+ })
877
+ )
878
+ )
879
+ )
880
+ )
881
+ })
882
+ )
883
+ )
884
+
885
+ return Stream.fromIterable(turnEndEvents).pipe(
886
+ Stream.concat(Stream.fromIterable(prepared.events)),
887
+ Stream.concat(toolExecutionStream),
888
+ Stream.concat(nextTurnStream)
889
+ )
890
+ })
891
+ )
892
+
893
+ const makeModelOnlyAfterLlmStream = (
894
+ input: TurnStreamInput,
895
+ llmEventsRef: Ref.Ref<ReadonlyArray<LLMEvent>>
896
+ ): Stream.Stream<AgentEvent, AgentLoopError> =>
897
+ Stream.unwrap(
898
+ Effect.gen(function* () {
899
+ const llmEvents = yield* Ref.get(llmEventsRef)
900
+ const completion = yield* validateTurnCompletion(llmEvents)
901
+ const assistantMessage = accumulateAssistantMessage(llmEvents)
902
+
903
+ yield* Ref.update(input.createdMessages, messages => [...messages, assistantMessage])
904
+
905
+ return Stream.fromIterable([
906
+ LLMStreamEnd.make({ turn: input.turn }),
907
+ AssistantMessageEvent.make({ message: assistantMessage }),
908
+ TurnEnd.make({ turn: input.turn, reason: completion.stopReason })
909
+ ])
910
+ })
911
+ )
912
+
913
+ const makeLlmStream = (
914
+ input: TurnStreamInput,
915
+ llmEvents: Ref.Ref<ReadonlyArray<LLMEvent>>,
916
+ result: ContextTransformResult
917
+ ): Stream.Stream<AgentEvent, AgentLoopError> => {
918
+ const makeStream = () =>
919
+ input.provider.stream({
920
+ messages: result.messages,
921
+ tools: input.config.tools,
922
+ model: input.config.model,
923
+ reasoningEffort: input.config.reasoningEffort,
924
+ systemPrompt: input.config.systemPrompt
925
+ })
926
+
927
+ return Stream.fromIterable(result.events)
928
+ .pipe(Stream.concat(withProviderRetries(makeStream(), input.loopConfig, makeStream, 1)))
929
+ .pipe(
930
+ Stream.tap(event => {
931
+ if (!isLlmEvent(event)) {
932
+ return Effect.void
933
+ }
934
+
935
+ const appendEvent = Ref.update(llmEvents, events => [...events, event])
936
+
937
+ if (event._tag !== 'Usage') {
938
+ return appendEvent
939
+ }
940
+
941
+ return Ref.update(input.usage, usage => addAgentUsage(usage, event.usage)).pipe(
942
+ Effect.flatMap(() => appendEvent)
943
+ )
944
+ }),
945
+ Stream.flatMap(event =>
946
+ event._tag === 'AgentRetry'
947
+ ? Stream.make(event)
948
+ : isLlmEvent(event)
949
+ ? Stream.fromIterable(toLlmEvent(event))
950
+ : Stream.make(event)
951
+ ),
952
+ Stream.concat(makeAfterLlmStream(input, llmEvents))
953
+ )
954
+ }
955
+
956
+ const makeModelOnlyLlmStream = (
957
+ input: TurnStreamInput,
958
+ llmEvents: Ref.Ref<ReadonlyArray<LLMEvent>>,
959
+ result: ContextTransformResult
960
+ ): Stream.Stream<AgentEvent, AgentLoopError> => {
961
+ const makeStream = () =>
962
+ input.provider.stream({
963
+ messages: result.messages,
964
+ tools: input.config.tools,
965
+ model: input.config.model,
966
+ reasoningEffort: input.config.reasoningEffort,
967
+ systemPrompt: input.config.systemPrompt
968
+ })
969
+
970
+ return Stream.fromIterable(result.events)
971
+ .pipe(Stream.concat(withProviderRetries(makeStream(), input.loopConfig, makeStream, 1)))
972
+ .pipe(
973
+ Stream.tap(event => {
974
+ if (!isLlmEvent(event)) {
975
+ return Effect.void
976
+ }
977
+
978
+ const appendEvent = Ref.update(llmEvents, events => [...events, event])
979
+
980
+ if (event._tag !== 'Usage') {
981
+ return appendEvent
982
+ }
983
+
984
+ return Ref.update(input.usage, usage => addAgentUsage(usage, event.usage)).pipe(
985
+ Effect.flatMap(() => appendEvent)
986
+ )
987
+ }),
988
+ Stream.flatMap(event =>
989
+ event._tag === 'AgentRetry'
990
+ ? Stream.make(event)
991
+ : isLlmEvent(event)
992
+ ? Stream.fromIterable(toLlmEvent(event))
993
+ : Stream.make(event)
994
+ ),
995
+ Stream.concat(makeModelOnlyAfterLlmStream(input, llmEvents))
996
+ )
997
+ }
998
+
999
+ const makeTurnStream = (input: TurnStreamInput): Stream.Stream<AgentEvent, AgentLoopError> =>
1000
+ Stream.suspend(() => {
1001
+ if (input.turn > input.loopConfig.maxTurns) {
1002
+ return Stream.fail(new AbortError({ reason: 'max_turns' }))
1003
+ }
1004
+
1005
+ const llmStream = Stream.unwrap(
1006
+ Effect.gen(function* () {
1007
+ const llmEvents = yield* Ref.make<ReadonlyArray<LLMEvent>>([])
1008
+ const result = yield* input.contextTransformer.transform(input.currentMessages)
1009
+ yield* validateCapabilities(input.config, result.messages)
1010
+
1011
+ return makeLlmStream(input, llmEvents, result)
1012
+ })
1013
+ )
1014
+
1015
+ return Stream.fromIterable([
1016
+ TurnStart.make({ turn: input.turn }),
1017
+ LLMStreamStart.make({ turn: input.turn })
1018
+ ]).pipe(Stream.concat(llmStream))
1019
+ })
1020
+
1021
+ const makePendingToolResumeStream = (input: TurnStreamInput): Stream.Stream<AgentEvent, AgentLoopError> =>
1022
+ Stream.unwrap(
1023
+ Effect.gen(function* () {
1024
+ const pendingCalls = pendingHostToolCalls(input.currentMessages)
1025
+
1026
+ if (pendingCalls.length === 0 || (input.config.hitlResponses ?? []).length === 0) {
1027
+ return makeTurnStream(input)
1028
+ }
1029
+
1030
+ const toolResultMessages = yield* Ref.make<ReadonlyArray<IndexedToolResultMessage>>([])
1031
+ const prepared = yield* prepareToolBatch({
1032
+ tools: input.config.tools,
1033
+ responses: input.config.hitlResponses ?? [],
1034
+ calls: pendingCalls
1035
+ })
1036
+
1037
+ if (prepared.resultMessages.length > 0) {
1038
+ yield* Ref.update(toolResultMessages, results => [...results, ...prepared.resultMessages])
1039
+ }
1040
+
1041
+ if (prepared.pendingRequests.length > 0) {
1042
+ const pendingRequests = nonEmptyHitlRequests(prepared.pendingRequests)
1043
+
1044
+ if (pendingRequests === undefined) {
1045
+ return Stream.empty
1046
+ }
1047
+
1048
+ const readyResults = orderedToolResultMessages(yield* Ref.get(toolResultMessages))
1049
+ if (readyResults.length > 0) {
1050
+ yield* Ref.update(input.createdMessages, messages => [...messages, ...readyResults])
1051
+ }
1052
+
1053
+ const messages = yield* Ref.get(input.createdMessages)
1054
+ const usage = yield* Ref.get(input.usage)
1055
+
1056
+ return Stream.fromIterable([
1057
+ ...prepared.events,
1058
+ ...prepared.pendingEvents,
1059
+ AgentAwaitingInput.make({
1060
+ requests: pendingRequests,
1061
+ messages,
1062
+ turns: Math.max(0, input.turn - 1),
1063
+ usage
1064
+ })
1065
+ ])
1066
+ }
1067
+
1068
+ const toolExecutionStream = parallelToolExecutionStream({
1069
+ calls: prepared.callsToExecute,
1070
+ executor: input.executor,
1071
+ loopConfig: input.loopConfig,
1072
+ model: input.config.model,
1073
+ results: toolResultMessages
1074
+ })
1075
+ const nextTurnStream = Stream.unwrap(
1076
+ Ref.get(toolResultMessages).pipe(
1077
+ Effect.flatMap(results => {
1078
+ const orderedResults = orderedToolResultMessages(results)
1079
+
1080
+ return Ref.update(input.createdMessages, messages => [...messages, ...orderedResults]).pipe(
1081
+ Effect.as(
1082
+ makeTurnStream({
1083
+ ...input,
1084
+ currentMessages: [...input.currentMessages, ...orderedResults]
1085
+ })
1086
+ )
1087
+ )
1088
+ })
1089
+ )
1090
+ )
1091
+
1092
+ return Stream.fromIterable(prepared.events).pipe(
1093
+ Stream.concat(toolExecutionStream),
1094
+ Stream.concat(nextTurnStream)
1095
+ )
1096
+ })
1097
+ )
1098
+
1099
+ const unavailableToolExecutor: TurnStreamInput['executor'] = {
1100
+ execute: call =>
1101
+ Effect.fail(
1102
+ new ToolError({
1103
+ tool: call.name,
1104
+ message: 'Tool execution is not available in model turn step',
1105
+ cause: 'execution'
1106
+ })
1107
+ )
1108
+ }
1109
+
1110
+ const makeModelOnlyTurnStream = (
1111
+ input: TurnStreamInput
1112
+ ): Stream.Stream<AgentEvent, AgentLoopError> =>
1113
+ Stream.suspend(() => {
1114
+ if (input.turn > input.loopConfig.maxTurns) {
1115
+ return Stream.fail(new AbortError({ reason: 'max_turns' }))
1116
+ }
1117
+
1118
+ const llmStream = Stream.unwrap(
1119
+ Effect.gen(function* () {
1120
+ const llmEvents = yield* Ref.make<ReadonlyArray<LLMEvent>>([])
1121
+ const result = yield* input.contextTransformer.transform(input.currentMessages)
1122
+ yield* validateCapabilities(input.config, result.messages)
1123
+
1124
+ return makeModelOnlyLlmStream(input, llmEvents, result)
1125
+ })
1126
+ )
1127
+
1128
+ return Stream.fromIterable([
1129
+ TurnStart.make({ turn: input.turn }),
1130
+ LLMStreamStart.make({ turn: input.turn })
1131
+ ]).pipe(Stream.concat(llmStream))
1132
+ })
1133
+
1134
+ export const runModelTurn = (
1135
+ config: ModelTurnConfig
1136
+ ): Stream.Stream<AgentEvent, AgentLoopError, ContextTransformer | LLMProvider | LoopConfig> =>
1137
+ Stream.unwrap(
1138
+ Effect.gen(function* () {
1139
+ const contextTransformer = yield* ContextTransformer
1140
+ const loopConfig = yield* LoopConfig
1141
+ const provider = yield* LLMProvider
1142
+ const createdMessages = yield* Ref.make<ReadonlyArray<AgentMessage>>([])
1143
+ const usage = yield* Ref.make(zeroAgentUsage)
1144
+
1145
+ return makeModelOnlyTurnStream({
1146
+ config,
1147
+ contextTransformer,
1148
+ loopConfig,
1149
+ provider,
1150
+ executor: unavailableToolExecutor,
1151
+ currentMessages: config.messages,
1152
+ createdMessages,
1153
+ usage,
1154
+ turn: config.turn
1155
+ })
1156
+ })
1157
+ )
1158
+
1159
+ export const runToolBatch = (
1160
+ config: ToolBatchConfig
1161
+ ): Stream.Stream<AgentEvent, AgentLoopError, LoopConfig | ToolExecutor> =>
1162
+ Stream.unwrap(
1163
+ Effect.gen(function* () {
1164
+ const executor = yield* ToolExecutor
1165
+ const loopConfig = yield* LoopConfig
1166
+ const toolResultMessages = yield* Ref.make<ReadonlyArray<IndexedToolResultMessage>>([])
1167
+ const prepared = yield* prepareToolBatch({
1168
+ tools: config.tools ?? [],
1169
+ responses: config.hitlResponses ?? [],
1170
+ calls: config.calls
1171
+ })
1172
+ const hasPendingRequests = prepared.pendingRequests.length > 0
1173
+ const resultEvents = hasPendingRequests ? [] : prepared.resultEvents
1174
+ const pendingRequests = nonEmptyHitlRequests(prepared.pendingRequests)
1175
+ const awaitingEvents: ReadonlyArray<AgentEvent> = pendingRequests === undefined
1176
+ ? []
1177
+ : [
1178
+ AgentAwaitingInput.make({
1179
+ requests: pendingRequests,
1180
+ messages: config.createdMessages ?? [],
1181
+ turns: config.turn ?? 0,
1182
+ usage: config.usage ?? zeroAgentUsage
1183
+ })
1184
+ ]
1185
+
1186
+ return Stream.fromIterable([
1187
+ ...prepared.events,
1188
+ ...resultEvents,
1189
+ ...prepared.pendingEvents,
1190
+ ...awaitingEvents
1191
+ ]).pipe(
1192
+ Stream.concat(
1193
+ parallelToolExecutionStream({
1194
+ calls: hasPendingRequests ? [] : prepared.callsToExecute,
1195
+ executor,
1196
+ loopConfig,
1197
+ model: config.model ?? '',
1198
+ results: toolResultMessages
1199
+ })
1200
+ )
1201
+ )
1202
+ })
1203
+ )
1204
+
1205
+ export const run = (
1206
+ config: RunConfig
1207
+ ): Stream.Stream<
1208
+ AgentEvent,
1209
+ AgentLoopError,
1210
+ ContextTransformer | LLMProvider | LoopConfig | ToolExecutor
1211
+ > =>
1212
+ Stream.unwrap(
1213
+ Effect.gen(function* () {
1214
+ const contextTransformer = yield* ContextTransformer
1215
+ const loopConfig = yield* LoopConfig
1216
+ const provider = yield* LLMProvider
1217
+ const executor = yield* ToolExecutor
1218
+ const createdMessages = yield* Ref.make<ReadonlyArray<AgentMessage>>([])
1219
+ const usage = yield* Ref.make(zeroAgentUsage)
1220
+
1221
+ return Stream.make(AgentStart.make({})).pipe(
1222
+ Stream.concat(
1223
+ makePendingToolResumeStream({
1224
+ config,
1225
+ contextTransformer,
1226
+ loopConfig,
1227
+ provider,
1228
+ executor,
1229
+ currentMessages: config.messages,
1230
+ createdMessages,
1231
+ usage,
1232
+ turn: 1
1233
+ })
1234
+ )
1235
+ )
1236
+ })
1237
+ )