@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,517 @@
1
+ import { Cause, Effect, Queue, Stream, type Layer } from 'effect'
2
+ import {
3
+ FetchHttpClient,
4
+ HttpClient,
5
+ HttpClientRequest,
6
+ type HttpClientError,
7
+ type HttpClientResponse
8
+ } from 'effect/unstable/http'
9
+ import * as Schema from 'effect/Schema'
10
+ import {
11
+ AgentEvent,
12
+ AgentWebSocketServerMessage,
13
+ QuestionResponseInput,
14
+ ToolApprovalResponseInput,
15
+ UserInput
16
+ } from '@yolk-sdk/agent/protocol'
17
+ import type {
18
+ AgentEvent as AgentEventType,
19
+ AgentMessage,
20
+ AgentReasoningEffort,
21
+ AgentWebSocketServerMessage as AgentWebSocketServerMessageType,
22
+ HitlResponse,
23
+ QuestionResponse,
24
+ ToolApprovalResponse,
25
+ UserMessage
26
+ } from '@yolk-sdk/agent/protocol'
27
+ import type { AgentTranscript } from './state.ts'
28
+
29
+ export class AgentTransportError extends Schema.TaggedErrorClass<AgentTransportError>()(
30
+ 'AgentTransportError',
31
+ {
32
+ message: Schema.String,
33
+ cause: Schema.Unknown
34
+ }
35
+ ) {}
36
+
37
+ export type StreamAgentEventsRequest = {
38
+ readonly endpoint?: string
39
+ readonly sessionId: string
40
+ readonly messages: AgentTranscript
41
+ readonly hitlResponses?: ReadonlyArray<HitlResponse>
42
+ readonly model?: string
43
+ readonly reasoningEffort?: AgentReasoningEffort
44
+ readonly signal?: AbortSignal
45
+ readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>
46
+ readonly onResponse?: (response: AgentHttpResponseInfo) => void
47
+ }
48
+
49
+ export type StreamAgentRunEventsRequest = {
50
+ readonly endpoint: string
51
+ readonly signal?: AbortSignal
52
+ readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>
53
+ readonly onResponse?: (response: AgentHttpResponseInfo) => void
54
+ }
55
+
56
+ export type CancelAgentRunRequest = {
57
+ readonly endpoint: string
58
+ readonly httpClientLayer?: Layer.Layer<HttpClient.HttpClient>
59
+ }
60
+
61
+ export type SubmitToolApprovalResponseRequest = StreamAgentEventsRequest & {
62
+ readonly response: ToolApprovalResponse
63
+ }
64
+
65
+ export type SubmitQuestionResponseRequest = StreamAgentEventsRequest & {
66
+ readonly response: QuestionResponse
67
+ }
68
+
69
+ export type AgentHttpResponseInfo = {
70
+ readonly status: number
71
+ readonly headers: Readonly<Record<string, string | undefined>>
72
+ }
73
+
74
+ export type StreamCloudflareAgentEventsRequest = {
75
+ readonly webSocketUrl: string
76
+ readonly messages: AgentTranscript
77
+ readonly hitlResponses?: ReadonlyArray<HitlResponse>
78
+ readonly model?: string
79
+ readonly reasoningEffort?: AgentReasoningEffort
80
+ readonly signal?: AbortSignal
81
+ }
82
+
83
+ const defaultEndpoint = '/api/agent'
84
+
85
+ const unknownToMessage = (error: unknown) =>
86
+ error instanceof Error ? error.message : String(error)
87
+
88
+ const toTransportError = (message: string, cause: unknown) =>
89
+ new AgentTransportError({
90
+ message,
91
+ cause
92
+ })
93
+
94
+ const toHttpClientTransportError = (message: string) => (error: HttpClientError.HttpClientError) =>
95
+ toTransportError(`${message}: ${error.message}`, error)
96
+
97
+ const decodeAgentEvent = (value: unknown) =>
98
+ Schema.decodeUnknownEffect(AgentEvent)(value).pipe(
99
+ Effect.mapError(
100
+ error =>
101
+ new AgentTransportError({
102
+ message: `Invalid agent event: ${unknownToMessage(error)}`,
103
+ cause: error
104
+ })
105
+ )
106
+ )
107
+
108
+ const decodeWebSocketServerMessage = (value: unknown) =>
109
+ Schema.decodeUnknownEffect(AgentWebSocketServerMessage)(value).pipe(
110
+ Effect.mapError(
111
+ error =>
112
+ new AgentTransportError({
113
+ message: `Invalid agent WebSocket message: ${unknownToMessage(error)}`,
114
+ cause: error
115
+ })
116
+ )
117
+ )
118
+
119
+ const encodeJsonString = (value: unknown, message: string) =>
120
+ Schema.encodeUnknownEffect(Schema.UnknownFromJsonString)(value).pipe(
121
+ Effect.mapError(
122
+ error =>
123
+ new AgentTransportError({
124
+ message: `${message}: ${unknownToMessage(error)}`,
125
+ cause: error
126
+ })
127
+ )
128
+ )
129
+
130
+ const decodeJsonString = (raw: string, message: string) =>
131
+ Schema.decodeUnknownEffect(Schema.UnknownFromJsonString)(raw).pipe(
132
+ Effect.mapError(
133
+ error =>
134
+ new AgentTransportError({
135
+ message: `${message}: ${unknownToMessage(error)}`,
136
+ cause: error
137
+ })
138
+ )
139
+ )
140
+
141
+ const parseAgentEventLine = (line: string) =>
142
+ Effect.gen(function* () {
143
+ const parsed = yield* decodeJsonString(line, 'Invalid NDJSON line')
144
+
145
+ return yield* decodeAgentEvent(parsed)
146
+ })
147
+
148
+ const parseWebSocketServerMessage = (
149
+ raw: string
150
+ ): Effect.Effect<AgentWebSocketServerMessageType, AgentTransportError> =>
151
+ Effect.gen(function* () {
152
+ const parsed = yield* decodeJsonString(raw, 'Invalid WebSocket message')
153
+
154
+ return yield* decodeWebSocketServerMessage(parsed)
155
+ })
156
+
157
+ const isUserMessage = (message: AgentMessage): message is UserMessage => message._tag === 'User'
158
+
159
+ const lastUserMessage = (
160
+ messages: AgentTranscript
161
+ ): Effect.Effect<UserMessage, AgentTransportError> => {
162
+ const reversed = messages.slice().reverse()
163
+ const message = reversed.find(isUserMessage)
164
+
165
+ if (message === undefined) {
166
+ return Effect.fail(
167
+ new AgentTransportError({
168
+ message: 'Cloudflare WebSocket transport requires a user message',
169
+ cause: messages
170
+ })
171
+ )
172
+ }
173
+
174
+ return Effect.succeed(message)
175
+ }
176
+
177
+ const makeClientInputJson = (
178
+ request: StreamCloudflareAgentEventsRequest,
179
+ expectedRevision: number
180
+ ): Effect.Effect<string, AgentTransportError> =>
181
+ Effect.gen(function* () {
182
+ const hitlResponse = request.hitlResponses?.[0]
183
+
184
+ if (request.hitlResponses !== undefined && request.hitlResponses.length > 1) {
185
+ return yield* Effect.fail(
186
+ new AgentTransportError({
187
+ message: 'Cloudflare WebSocket transport supports one HITL response at a time',
188
+ cause: request.hitlResponses
189
+ })
190
+ )
191
+ }
192
+
193
+ if (hitlResponse === undefined) {
194
+ const message = yield* lastUserMessage(request.messages)
195
+
196
+ return yield* encodeJsonString(
197
+ UserInput.make({
198
+ message,
199
+ expectedRevision,
200
+ model: request.model,
201
+ reasoningEffort: request.reasoningEffort
202
+ }),
203
+ 'Could not serialize WebSocket user input'
204
+ )
205
+ }
206
+
207
+ return yield* encodeJsonString(
208
+ hitlResponse._tag === 'ToolApprovalResponse'
209
+ ? ToolApprovalResponseInput.make({
210
+ response: hitlResponse,
211
+ expectedRevision,
212
+ model: request.model,
213
+ reasoningEffort: request.reasoningEffort
214
+ })
215
+ : QuestionResponseInput.make({
216
+ response: hitlResponse,
217
+ expectedRevision,
218
+ model: request.model,
219
+ reasoningEffort: request.reasoningEffort
220
+ }),
221
+ 'Could not serialize WebSocket HITL response'
222
+ )
223
+ })
224
+
225
+ const responseErrorMessage = (response: HttpClientResponse.HttpClientResponse) =>
226
+ response.text.pipe(
227
+ Effect.mapError(toHttpClientTransportError('Could not read agent error body')),
228
+ Effect.map(text => (text.length > 0 ? text : `Request failed with ${response.status}`))
229
+ )
230
+
231
+ const makeHttpRequest = (request: StreamAgentEventsRequest) =>
232
+ encodeJsonString(
233
+ {
234
+ sessionId: request.sessionId,
235
+ messages: request.messages,
236
+ hitlResponses: request.hitlResponses,
237
+ model: request.model,
238
+ reasoningEffort: request.reasoningEffort
239
+ },
240
+ 'Could not serialize agent request'
241
+ ).pipe(
242
+ Effect.map(body =>
243
+ HttpClientRequest.post(request.endpoint ?? defaultEndpoint).pipe(
244
+ HttpClientRequest.setHeaders({
245
+ accept: 'application/x-ndjson',
246
+ 'content-type': 'application/json'
247
+ }),
248
+ HttpClientRequest.bodyText(body, 'application/json')
249
+ )
250
+ )
251
+ )
252
+
253
+ const requestAgentResponse = (request: StreamAgentEventsRequest) =>
254
+ Effect.gen(function* () {
255
+ const client = yield* HttpClient.HttpClient
256
+ const httpRequest = yield* makeHttpRequest(request)
257
+ const response = yield* client
258
+ .execute(httpRequest)
259
+ .pipe(Effect.mapError(toHttpClientTransportError('Agent request failed')))
260
+
261
+ if (response.status >= 200 && response.status < 300) {
262
+ yield* Effect.sync(() => request.onResponse?.({ status: response.status, headers: response.headers }))
263
+ return response
264
+ }
265
+
266
+ const message = yield* responseErrorMessage(response)
267
+
268
+ return yield* Effect.fail(
269
+ new AgentTransportError({
270
+ message: `Agent request failed (${response.status}): ${message}`,
271
+ cause: response.status
272
+ })
273
+ )
274
+ })
275
+
276
+ const requestAgentRunResponse = (request: StreamAgentRunEventsRequest) =>
277
+ Effect.gen(function* () {
278
+ const client = yield* HttpClient.HttpClient
279
+ const response = yield* client
280
+ .execute(
281
+ HttpClientRequest.get(request.endpoint).pipe(
282
+ HttpClientRequest.setHeaders({ accept: 'application/x-ndjson' })
283
+ )
284
+ )
285
+ .pipe(Effect.mapError(toHttpClientTransportError('Agent run request failed')))
286
+
287
+ if (response.status >= 200 && response.status < 300) {
288
+ yield* Effect.sync(() => request.onResponse?.({ status: response.status, headers: response.headers }))
289
+ return response
290
+ }
291
+
292
+ const message = yield* responseErrorMessage(response)
293
+
294
+ return yield* Effect.fail(
295
+ new AgentTransportError({
296
+ message: `Agent run request failed (${response.status}): ${message}`,
297
+ cause: response.status
298
+ })
299
+ )
300
+ })
301
+
302
+ const cancelAgentRunEffect = (request: CancelAgentRunRequest) =>
303
+ Effect.gen(function* () {
304
+ const client = yield* HttpClient.HttpClient
305
+ const response = yield* client
306
+ .execute(HttpClientRequest.delete(request.endpoint))
307
+ .pipe(Effect.mapError(toHttpClientTransportError('Agent run cancel failed')))
308
+
309
+ if (response.status >= 200 && response.status < 300) {
310
+ return
311
+ }
312
+
313
+ const message = yield* responseErrorMessage(response)
314
+
315
+ return yield* Effect.fail(
316
+ new AgentTransportError({
317
+ message: `Agent run cancel failed (${response.status}): ${message}`,
318
+ cause: response.status
319
+ })
320
+ )
321
+ }).pipe(Effect.provide(request.httpClientLayer ?? FetchHttpClient.layer))
322
+
323
+ const responseToEventStream = (response: HttpClientResponse.HttpClientResponse) =>
324
+ response.stream.pipe(
325
+ Stream.mapError(toHttpClientTransportError('Could not read agent response body')),
326
+ Stream.decodeText,
327
+ Stream.splitLines,
328
+ Stream.map(line => line.trim()),
329
+ Stream.filter(line => line.length > 0),
330
+ Stream.mapEffect(parseAgentEventLine),
331
+ Stream.takeUntil(
332
+ event =>
333
+ event._tag === 'AgentEnd' ||
334
+ event._tag === 'AgentError' ||
335
+ event._tag === 'AgentAwaitingInput'
336
+ )
337
+ )
338
+
339
+ const abortSignalError = (signal: AbortSignal) =>
340
+ new AgentTransportError({
341
+ message: 'Agent request aborted',
342
+ cause: signal.reason
343
+ })
344
+
345
+ const abortSignalEffect = (signal: AbortSignal) =>
346
+ Effect.callback<never, AgentTransportError>(resume => {
347
+ if (signal.aborted) {
348
+ resume(Effect.fail(abortSignalError(signal)))
349
+ return Effect.void
350
+ }
351
+
352
+ const listener = () => resume(Effect.fail(abortSignalError(signal)))
353
+ signal.addEventListener('abort', listener, { once: true })
354
+
355
+ return Effect.sync(() => signal.removeEventListener('abort', listener))
356
+ })
357
+
358
+ const applyAbortSignal = <A, E, R>(
359
+ stream: Stream.Stream<A, E, R>,
360
+ signal: AbortSignal | undefined
361
+ ) => (signal === undefined ? stream : stream.pipe(Stream.interruptWhen(abortSignalEffect(signal))))
362
+
363
+ export const streamAgentEventStream = (request: StreamAgentEventsRequest) =>
364
+ applyAbortSignal(
365
+ Stream.fromEffect(requestAgentResponse(request)).pipe(Stream.flatMap(responseToEventStream)),
366
+ request.signal
367
+ ).pipe(Stream.provide(request.httpClientLayer ?? FetchHttpClient.layer))
368
+
369
+ export const streamAgentRunEventStream = (request: StreamAgentRunEventsRequest) =>
370
+ applyAbortSignal(
371
+ Stream.fromEffect(requestAgentRunResponse(request)).pipe(Stream.flatMap(responseToEventStream)),
372
+ request.signal
373
+ ).pipe(Stream.provide(request.httpClientLayer ?? FetchHttpClient.layer))
374
+
375
+ export const streamToolApprovalResponseEventStream = (
376
+ request: SubmitToolApprovalResponseRequest
377
+ ) => streamAgentEventStream({ ...request, hitlResponses: [request.response] })
378
+
379
+ export const streamQuestionResponseEventStream = (request: SubmitQuestionResponseRequest) =>
380
+ streamAgentEventStream({ ...request, hitlResponses: [request.response] })
381
+
382
+ const isAgentEvent = (message: AgentWebSocketServerMessageType): message is AgentEventType =>
383
+ message._tag !== 'SessionSnapshot'
384
+
385
+ export const streamCloudflareAgentEventStream = (request: StreamCloudflareAgentEventsRequest) =>
386
+ applyAbortSignal(
387
+ Stream.callback<AgentEventType, AgentTransportError>(queue =>
388
+ Effect.gen(function* () {
389
+ const socket = new WebSocket(request.webSocketUrl)
390
+ let sentInput = false
391
+ let settled = false
392
+ const closeSocket = Effect.sync(() => {
393
+ if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) {
394
+ socket.close(1000, 'done')
395
+ }
396
+ })
397
+ const failQueue = (error: AgentTransportError) =>
398
+ Queue.failCauseUnsafe(queue, Cause.fail(error))
399
+ const endQueue = () => Queue.endUnsafe(queue)
400
+ const handleMessage = (event: MessageEvent) => {
401
+ if (typeof event.data !== 'string') {
402
+ failQueue(toTransportError('Agent WebSocket returned binary data', event.data))
403
+ return
404
+ }
405
+
406
+ Effect.runFork(
407
+ parseWebSocketServerMessage(event.data).pipe(
408
+ Effect.flatMap(message => {
409
+ if (message._tag === 'SessionSnapshot') {
410
+ return sentInput
411
+ ? Effect.void
412
+ : makeClientInputJson(request, message.revision).pipe(
413
+ Effect.flatMap(body => Effect.sync(() => socket.send(body))),
414
+ Effect.tap(() =>
415
+ Effect.sync(() => {
416
+ sentInput = true
417
+ })
418
+ )
419
+ )
420
+ }
421
+
422
+ if (!isAgentEvent(message)) {
423
+ return Effect.void
424
+ }
425
+
426
+ return Effect.sync(() => {
427
+ Queue.offerUnsafe(queue, message)
428
+ if (
429
+ message._tag === 'AgentEnd' ||
430
+ message._tag === 'AgentError' ||
431
+ message._tag === 'AgentAwaitingInput'
432
+ ) {
433
+ settled = true
434
+ endQueue()
435
+ socket.close(1000, 'done')
436
+ }
437
+ })
438
+ }),
439
+ Effect.catch(error => Effect.sync(() => failQueue(error)))
440
+ )
441
+ )
442
+ }
443
+ const handleError = () => {
444
+ failQueue(toTransportError('Agent WebSocket failed', request.webSocketUrl))
445
+ }
446
+ const handleClose = () => {
447
+ if (!settled) {
448
+ failQueue(toTransportError('Agent WebSocket closed', request.webSocketUrl))
449
+ }
450
+ }
451
+
452
+ yield* Effect.acquireRelease(
453
+ Effect.sync(() => {
454
+ socket.addEventListener('message', handleMessage)
455
+ socket.addEventListener('error', handleError)
456
+ socket.addEventListener('close', handleClose)
457
+ }),
458
+ () =>
459
+ Effect.sync(() => {
460
+ socket.removeEventListener('message', handleMessage)
461
+ socket.removeEventListener('error', handleError)
462
+ socket.removeEventListener('close', handleClose)
463
+ }).pipe(Effect.andThen(closeSocket))
464
+ )
465
+ })
466
+ ),
467
+ request.signal
468
+ )
469
+
470
+ export async function* streamCloudflareAgentEvents(
471
+ request: StreamCloudflareAgentEventsRequest
472
+ ): AsyncGenerator<AgentEventType, void, void> {
473
+ for await (const event of Stream.toAsyncIterable(streamCloudflareAgentEventStream(request))) {
474
+ yield event
475
+ }
476
+ }
477
+
478
+ export async function* streamAgentEvents(
479
+ request: StreamAgentEventsRequest
480
+ ): AsyncGenerator<AgentEventType, void, void> {
481
+ for await (const event of Stream.toAsyncIterable(streamAgentEventStream(request))) {
482
+ yield event
483
+ }
484
+ }
485
+
486
+ export async function* streamAgentRunEvents(
487
+ request: StreamAgentRunEventsRequest
488
+ ): AsyncGenerator<AgentEventType, void, void> {
489
+ for await (const event of Stream.toAsyncIterable(streamAgentRunEventStream(request))) {
490
+ yield event
491
+ }
492
+ }
493
+
494
+ export async function* submitToolApprovalResponse(
495
+ request: SubmitToolApprovalResponseRequest
496
+ ): AsyncGenerator<AgentEventType, void, void> {
497
+ for await (const event of Stream.toAsyncIterable(streamToolApprovalResponseEventStream(request))) {
498
+ yield event
499
+ }
500
+ }
501
+
502
+ export async function* submitQuestionResponse(
503
+ request: SubmitQuestionResponseRequest
504
+ ): AsyncGenerator<AgentEventType, void, void> {
505
+ for await (const event of Stream.toAsyncIterable(streamQuestionResponseEventStream(request))) {
506
+ yield event
507
+ }
508
+ }
509
+
510
+ export const cancelAgentRun = (request: CancelAgentRunRequest) =>
511
+ Effect.runPromise(cancelAgentRunEffect(request))
512
+
513
+ export const collectAgentEventsEffect = (request: StreamAgentEventsRequest) =>
514
+ streamAgentEventStream(request).pipe(Stream.runCollect)
515
+
516
+ export const collectAgentEvents = async (request: StreamAgentEventsRequest) =>
517
+ Array.from(await Effect.runPromise(collectAgentEventsEffect(request)))
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Root intentionally empty. Import feature APIs from explicit subpaths.
2
+ export {}
@@ -0,0 +1,23 @@
1
+ # @yolk-sdk/agent/loop
2
+
3
+ Stateless provider-neutral LLM/tool loop.
4
+
5
+ ## What it provides
6
+
7
+ - `run` for executing model turns over a protocol transcript.
8
+ - `LLMProvider`, `ToolExecutor`, `LoopConfig`, and `ContextTransformer` Effect service contracts.
9
+ - Assistant text/reasoning/tool-call accumulation helpers.
10
+ - HITL pauses for manual tool approvals and structured questions.
11
+ - Typed loop errors.
12
+ - `@yolk-sdk/agent/loop/testing` test helpers.
13
+
14
+ ## Use it when
15
+
16
+ - You have a provider adapter and tool executor and need to run an agent turn.
17
+ - You want protocol events, not UI-specific state.
18
+
19
+ ## Boundaries
20
+
21
+ - No sessions or persistence.
22
+ - No provider SDK imports.
23
+ - No app tool catalogs or product permissions.
@@ -0,0 +1,71 @@
1
+ import {
2
+ AssistantAgentMessage,
3
+ AssistantReasoningPart,
4
+ AssistantTextPart,
5
+ appendTextToContent,
6
+ HostToolCallPart,
7
+ ProviderToolCallPart,
8
+ ProviderToolResultPart,
9
+ type AssistantPart
10
+ } from '@yolk-sdk/agent/protocol'
11
+ import type { LLMEvent } from './llm-event.ts'
12
+
13
+ export const collectText = (events: ReadonlyArray<LLMEvent>) =>
14
+ events.reduce((text, event) => (event._tag === 'TextDelta' ? `${text}${event.text}` : text), '')
15
+
16
+ export const collectReasoning = (events: ReadonlyArray<LLMEvent>) =>
17
+ events.reduce(
18
+ (text, event) => (event._tag === 'ReasoningDelta' ? `${text}${event.text}` : text),
19
+ ''
20
+ )
21
+
22
+ export const collectToolCalls = (events: ReadonlyArray<LLMEvent>) =>
23
+ events.flatMap(event => (event._tag === 'ToolCall' ? [event.call] : []))
24
+
25
+ const appendTextPart = (parts: ReadonlyArray<AssistantPart>, text: string) => {
26
+ const last = parts.at(-1)
27
+
28
+ return last?._tag === 'Text'
29
+ ? [
30
+ ...parts.slice(0, -1),
31
+ AssistantTextPart.make({ content: appendTextToContent(last.content, text) })
32
+ ]
33
+ : [...parts, AssistantTextPart.make({ content: text })]
34
+ }
35
+
36
+ const appendReasoningPart = (parts: ReadonlyArray<AssistantPart>, text: string) => {
37
+ const last = parts.at(-1)
38
+
39
+ return last?._tag === 'Reasoning'
40
+ ? [...parts.slice(0, -1), AssistantReasoningPart.make({ text: `${last.text}${text}` })]
41
+ : [...parts, AssistantReasoningPart.make({ text })]
42
+ }
43
+
44
+ const accumulateAssistantParts = (events: ReadonlyArray<LLMEvent>) =>
45
+ events.reduce<ReadonlyArray<AssistantPart>>((parts, event) => {
46
+ switch (event._tag) {
47
+ case 'TextDelta':
48
+ return appendTextPart(parts, event.text)
49
+ case 'ReasoningDelta':
50
+ return appendReasoningPart(parts, event.text)
51
+ case 'ToolCall':
52
+ return [...parts, HostToolCallPart.make({ call: event.call })]
53
+ case 'ProviderToolResult':
54
+ return [
55
+ ...parts,
56
+ ProviderToolCallPart.make({ call: event.call }),
57
+ ProviderToolResultPart.make({ toolCallId: event.call.id, result: event.result })
58
+ ]
59
+ case 'Done':
60
+ case 'ToolInputDelta':
61
+ case 'ToolInputStart':
62
+ case 'Usage':
63
+ return parts
64
+ }
65
+ }, [])
66
+
67
+ export const accumulateAssistantMessage = (events: ReadonlyArray<LLMEvent>) => {
68
+ const parts = accumulateAssistantParts(events)
69
+
70
+ return AssistantAgentMessage.make({ parts })
71
+ }