@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.
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/client/index.d.mts +3 -0
- package/dist/client/index.mjs +3 -0
- package/dist/client/state.d.mts +99 -0
- package/dist/client/state.d.mts.map +1 -0
- package/dist/client/state.mjs +245 -0
- package/dist/client/state.mjs.map +1 -0
- package/dist/client/transport.d.mts +67 -0
- package/dist/client/transport.d.mts.map +1 -0
- package/dist/client/transport.mjs +219 -0
- package/dist/client/transport.mjs.map +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/loop/accumulator.d.mts +11 -0
- package/dist/loop/accumulator.d.mts.map +1 -0
- package/dist/loop/accumulator.mjs +40 -0
- package/dist/loop/accumulator.mjs.map +1 -0
- package/dist/loop/error.d.mts +36 -0
- package/dist/loop/error.d.mts.map +1 -0
- package/dist/loop/error.mjs +84 -0
- package/dist/loop/error.mjs.map +1 -0
- package/dist/loop/index.d.mts +9 -0
- package/dist/loop/index.mjs +9 -0
- package/dist/loop/llm-event.d.mts +44 -0
- package/dist/loop/llm-event.d.mts.map +1 -0
- package/dist/loop/llm-event.mjs +34 -0
- package/dist/loop/llm-event.mjs.map +1 -0
- package/dist/loop/run.d.mts +37 -0
- package/dist/loop/run.d.mts.map +1 -0
- package/dist/loop/run.mjs +624 -0
- package/dist/loop/run.mjs.map +1 -0
- package/dist/loop/services/context-transformer.d.mts +18 -0
- package/dist/loop/services/context-transformer.d.mts.map +1 -0
- package/dist/loop/services/context-transformer.mjs +12 -0
- package/dist/loop/services/context-transformer.mjs.map +1 -0
- package/dist/loop/services/llm-provider.d.mts +20 -0
- package/dist/loop/services/llm-provider.d.mts.map +1 -0
- package/dist/loop/services/llm-provider.mjs +7 -0
- package/dist/loop/services/llm-provider.mjs.map +1 -0
- package/dist/loop/services/loop-config.d.mts +17 -0
- package/dist/loop/services/loop-config.d.mts.map +1 -0
- package/dist/loop/services/loop-config.mjs +15 -0
- package/dist/loop/services/loop-config.mjs.map +1 -0
- package/dist/loop/services/tool-executor.d.mts +12 -0
- package/dist/loop/services/tool-executor.d.mts.map +1 -0
- package/dist/loop/services/tool-executor.mjs +7 -0
- package/dist/loop/services/tool-executor.mjs.map +1 -0
- package/dist/loop/testing/faux-provider.d.mts +31 -0
- package/dist/loop/testing/faux-provider.d.mts.map +1 -0
- package/dist/loop/testing/faux-provider.mjs +47 -0
- package/dist/loop/testing/faux-provider.mjs.map +1 -0
- package/dist/loop/testing/index.d.mts +3 -0
- package/dist/loop/testing/index.mjs +3 -0
- package/dist/loop/testing/test-tool-executor.d.mts +10 -0
- package/dist/loop/testing/test-tool-executor.d.mts.map +1 -0
- package/dist/loop/testing/test-tool-executor.mjs +21 -0
- package/dist/loop/testing/test-tool-executor.mjs.map +1 -0
- package/dist/protocol/capability.d.mts +20 -0
- package/dist/protocol/capability.d.mts.map +1 -0
- package/dist/protocol/capability.mjs +34 -0
- package/dist/protocol/capability.mjs.map +1 -0
- package/dist/protocol/content.d.mts +31 -0
- package/dist/protocol/content.d.mts.map +1 -0
- package/dist/protocol/content.mjs +52 -0
- package/dist/protocol/content.mjs.map +1 -0
- package/dist/protocol/event.d.mts +228 -0
- package/dist/protocol/event.d.mts.map +1 -0
- package/dist/protocol/event.mjs +217 -0
- package/dist/protocol/event.mjs.map +1 -0
- package/dist/protocol/index.d.mts +14 -0
- package/dist/protocol/index.d.mts.map +1 -0
- package/dist/protocol/index.mjs +9 -0
- package/dist/protocol/message.d.mts +53 -0
- package/dist/protocol/message.d.mts.map +1 -0
- package/dist/protocol/message.mjs +49 -0
- package/dist/protocol/message.mjs.map +1 -0
- package/dist/protocol/reasoning.d.mts +8 -0
- package/dist/protocol/reasoning.d.mts.map +1 -0
- package/dist/protocol/reasoning.mjs +13 -0
- package/dist/protocol/reasoning.mjs.map +1 -0
- package/dist/protocol/session.d.mts +39 -0
- package/dist/protocol/session.d.mts.map +1 -0
- package/dist/protocol/session.mjs +38 -0
- package/dist/protocol/session.mjs.map +1 -0
- package/dist/protocol/tool.d.mts +101 -0
- package/dist/protocol/tool.d.mts.map +1 -0
- package/dist/protocol/tool.mjs +102 -0
- package/dist/protocol/tool.mjs.map +1 -0
- package/dist/protocol/usage.d.mts +26 -0
- package/dist/protocol/usage.d.mts.map +1 -0
- package/dist/protocol/usage.mjs +40 -0
- package/dist/protocol/usage.mjs.map +1 -0
- package/dist/runtime/error.d.mts +29 -0
- package/dist/runtime/error.d.mts.map +1 -0
- package/dist/runtime/error.mjs +46 -0
- package/dist/runtime/error.mjs.map +1 -0
- package/dist/runtime/index.d.mts +9 -0
- package/dist/runtime/index.d.mts.map +1 -0
- package/dist/runtime/index.mjs +4 -0
- package/dist/runtime/run-runtime.d.mts +47 -0
- package/dist/runtime/run-runtime.d.mts.map +1 -0
- package/dist/runtime/run-runtime.mjs +112 -0
- package/dist/runtime/run-runtime.mjs.map +1 -0
- package/dist/runtime/session-event-store.d.mts +75 -0
- package/dist/runtime/session-event-store.d.mts.map +1 -0
- package/dist/runtime/session-event-store.mjs +124 -0
- package/dist/runtime/session-event-store.mjs.map +1 -0
- package/dist/tools/index.d.mts +4 -0
- package/dist/tools/index.mjs +4 -0
- package/dist/tools/question.d.mts +21 -0
- package/dist/tools/question.d.mts.map +1 -0
- package/dist/tools/question.mjs +41 -0
- package/dist/tools/question.mjs.map +1 -0
- package/dist/tools/registry.d.mts +61 -0
- package/dist/tools/registry.d.mts.map +1 -0
- package/dist/tools/registry.mjs +113 -0
- package/dist/tools/registry.mjs.map +1 -0
- package/dist/tools/task.d.mts +34 -0
- package/dist/tools/task.d.mts.map +1 -0
- package/dist/tools/task.mjs +81 -0
- package/dist/tools/task.mjs.map +1 -0
- package/package.json +86 -0
- package/src/client/README.md +23 -0
- package/src/client/index.ts +43 -0
- package/src/client/state.ts +380 -0
- package/src/client/transport.ts +517 -0
- package/src/index.ts +2 -0
- package/src/loop/README.md +23 -0
- package/src/loop/accumulator.ts +71 -0
- package/src/loop/error.ts +105 -0
- package/src/loop/index.ts +35 -0
- package/src/loop/llm-event.ts +52 -0
- package/src/loop/run.ts +1237 -0
- package/src/loop/services/context-transformer.ts +24 -0
- package/src/loop/services/llm-provider.ts +20 -0
- package/src/loop/services/loop-config.ts +20 -0
- package/src/loop/services/tool-executor.ts +11 -0
- package/src/loop/testing/faux-provider.ts +94 -0
- package/src/loop/testing/index.ts +3 -0
- package/src/loop/testing/test-tool-executor.ts +28 -0
- package/src/protocol/README.md +24 -0
- package/src/protocol/capability.ts +29 -0
- package/src/protocol/content.ts +76 -0
- package/src/protocol/event.ts +286 -0
- package/src/protocol/index.ts +109 -0
- package/src/protocol/message.ts +86 -0
- package/src/protocol/reasoning.ts +4 -0
- package/src/protocol/session.ts +47 -0
- package/src/protocol/tool.ts +154 -0
- package/src/protocol/usage.ts +48 -0
- package/src/runtime/README.md +44 -0
- package/src/runtime/error.ts +70 -0
- package/src/runtime/index.ts +43 -0
- package/src/runtime/run-runtime.ts +307 -0
- package/src/runtime/session-event-store.ts +254 -0
- package/src/tools/README.md +22 -0
- package/src/tools/index.ts +29 -0
- package/src/tools/question.ts +58 -0
- package/src/tools/registry.ts +228 -0
- 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,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
|
+
}
|