saeeol 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/package.json +14 -14
  2. package/src/session/compaction-helpers.ts +1 -169
  3. package/src/session/compaction.ts +1 -712
  4. package/src/session/core/compaction/compaction-helpers.ts +169 -0
  5. package/src/session/core/compaction/compaction.ts +712 -0
  6. package/src/session/core/compaction/overflow.ts +28 -0
  7. package/src/session/core/instruction.ts +234 -0
  8. package/src/session/core/llm.ts +504 -0
  9. package/src/session/core/network.ts +392 -0
  10. package/src/session/core/processor.ts +731 -0
  11. package/src/session/core/projectors.ts +139 -0
  12. package/src/session/core/resolve-tools.ts +241 -0
  13. package/src/session/core/retry.ts +149 -0
  14. package/src/session/core/revert.ts +173 -0
  15. package/src/session/core/run-state.ts +110 -0
  16. package/src/session/core/schema.ts +35 -0
  17. package/src/session/core/session-types.ts +160 -0
  18. package/src/session/core/session.sql.ts +124 -0
  19. package/src/session/core/session.ts +948 -0
  20. package/src/session/core/shell-exec.ts +205 -0
  21. package/src/session/core/status.ts +100 -0
  22. package/src/session/core/subtask.ts +268 -0
  23. package/src/session/core/summary.ts +173 -0
  24. package/src/session/core/system.ts +114 -0
  25. package/src/session/core/todo.ts +86 -0
  26. package/src/session/core/user-part.ts +293 -0
  27. package/src/session/instruction.ts +1 -234
  28. package/src/session/llm.ts +1 -504
  29. package/src/session/message/message-errors.ts +83 -0
  30. package/src/session/message/message-parts.ts +89 -0
  31. package/src/session/message/message-query.ts +107 -0
  32. package/src/session/message/message-transform.ts +156 -0
  33. package/src/session/message/message-types.ts +68 -0
  34. package/src/session/message/message-v2.ts +73 -0
  35. package/src/session/message/message.ts +192 -0
  36. package/src/session/message-errors.ts +1 -83
  37. package/src/session/message-parts.ts +1 -89
  38. package/src/session/message-query.ts +1 -107
  39. package/src/session/message-transform.ts +1 -156
  40. package/src/session/message-types.ts +1 -68
  41. package/src/session/message-v2.ts +1 -73
  42. package/src/session/message.ts +1 -192
  43. package/src/session/network.ts +1 -392
  44. package/src/session/overflow.ts +1 -28
  45. package/src/session/processor.ts +1 -731
  46. package/src/session/projectors.ts +2 -139
  47. package/src/session/prompt/prompt-command.ts +93 -0
  48. package/src/session/prompt/prompt-loop.ts +299 -0
  49. package/src/session/prompt/prompt-model.ts +44 -0
  50. package/src/session/prompt/prompt-reminders.ts +120 -0
  51. package/src/session/prompt/prompt-resolve.ts +42 -0
  52. package/src/session/prompt/prompt-schemas.ts +128 -0
  53. package/src/session/prompt/prompt-title.ts +55 -0
  54. package/src/session/prompt/prompt-types.ts +47 -0
  55. package/src/session/prompt/prompt-user-msg.ts +80 -0
  56. package/src/session/prompt/prompt.ts +211 -0
  57. package/src/session/prompt-command.ts +1 -93
  58. package/src/session/prompt-loop.ts +1 -299
  59. package/src/session/prompt-model.ts +1 -44
  60. package/src/session/prompt-reminders.ts +1 -120
  61. package/src/session/prompt-resolve.ts +1 -42
  62. package/src/session/prompt-schemas.ts +1 -128
  63. package/src/session/prompt-title.ts +1 -55
  64. package/src/session/prompt-types.ts +1 -47
  65. package/src/session/prompt-user-msg.ts +1 -80
  66. package/src/session/prompt.ts +1 -211
  67. package/src/session/resolve-tools.ts +1 -241
  68. package/src/session/retry.ts +1 -149
  69. package/src/session/revert.ts +1 -173
  70. package/src/session/run-state.ts +1 -110
  71. package/src/session/schema.ts +1 -35
  72. package/src/session/session-types.ts +1 -160
  73. package/src/session/session.sql.ts +1 -124
  74. package/src/session/session.ts +1 -948
  75. package/src/session/shell-exec.ts +1 -205
  76. package/src/session/status.ts +1 -100
  77. package/src/session/subtask.ts +1 -268
  78. package/src/session/summary.ts +1 -173
  79. package/src/session/system.ts +1 -114
  80. package/src/session/todo.ts +1 -86
  81. package/src/session/user-part.ts +1 -293
@@ -1,731 +1 @@
1
- import { Cause, Deferred, Effect, Layer, Context, Scope } from "effect"
2
- import * as Stream from "effect/Stream"
3
- import { Agent } from "@/agent/agent"
4
- import { Bus } from "@/bus"
5
- import { Config } from "@/config/config"
6
- import { Permission } from "@/permission"
7
- import { Plugin } from "@/plugin"
8
- import { Snapshot } from "@/snapshot"
9
- import * as Session from "./session"
10
- import { LLM } from "./llm"
11
- import { MessageV2 } from "./message-v2"
12
- import { isOverflow } from "./overflow"
13
- import { PartID } from "./schema"
14
- import type { SessionID } from "./schema"
15
- import { SessionRetry } from "./retry"
16
- import { SessionStatus } from "./status"
17
- import { SessionSummary } from "./summary"
18
- import type { Provider } from "@/provider/provider"
19
- import { Question } from "@/question"
20
- import { SaeeolSessionProcessor, type ReviewTelemetry } from "@/saeeol/session/processor"
21
- import { Suggestion } from "@/saeeol/suggestion"
22
- import { NotFoundError } from "@/storage/storage"
23
- import { errorMessage } from "@/util/error"
24
- import * as Log from "@saeeol/core/util/log"
25
- import { isRecord } from "@/util/record"
26
-
27
- const DOOM_LOOP_THRESHOLD = 3
28
- const log = Log.create({ service: "session.processor" })
29
-
30
- export type Result = "compact" | "stop" | "continue"
31
-
32
- export type Event = LLM.Event
33
-
34
- export interface Handle {
35
- readonly message: MessageV2.Assistant
36
- readonly updateToolCall: (
37
- toolCallID: string,
38
- update: (part: MessageV2.ToolPart) => MessageV2.ToolPart,
39
- ) => Effect.Effect<MessageV2.ToolPart | undefined>
40
- readonly completeToolCall: (
41
- toolCallID: string,
42
- output: {
43
- title: string
44
- metadata: Record<string, any>
45
- output: string
46
- attachments?: MessageV2.FilePart[]
47
- },
48
- ) => Effect.Effect<void>
49
- readonly process: (streamInput: LLM.StreamInput) => Effect.Effect<Result>
50
- readonly compactError?: () => ReturnType<typeof MessageV2.ContextOverflowError.prototype.toObject> | undefined
51
- }
52
-
53
- type Input = {
54
- assistantMessage: MessageV2.Assistant
55
- sessionID: SessionID
56
- model: Provider.Model
57
- telemetry?: ReviewTelemetry
58
- }
59
-
60
- export interface Interface {
61
- readonly create: (input: Input) => Effect.Effect<Handle>
62
- }
63
-
64
- type ToolCall = {
65
- partID: MessageV2.ToolPart["id"]
66
- messageID: MessageV2.ToolPart["messageID"]
67
- sessionID: MessageV2.ToolPart["sessionID"]
68
- done: Deferred.Deferred<void>
69
- }
70
-
71
- interface ProcessorContext extends Input {
72
- toolcalls: Record<string, ToolCall>
73
- shouldBreak: boolean
74
- snapshot: string | undefined
75
- blocked: boolean
76
- needsCompaction: boolean
77
- compactionError: ReturnType<typeof MessageV2.ContextOverflowError.prototype.toObject> | undefined
78
- currentText: MessageV2.TextPart | undefined
79
- reasoningMap: Record<string, MessageV2.ReasoningPart>
80
- stepStart: number
81
- step: { reasoning: boolean; text: boolean; tool: boolean }
82
- }
83
-
84
- type StreamEvent = Event
85
-
86
- export class Service extends Context.Service<Service, Interface>()("@saeeol/SessionProcessor") {}
87
-
88
- export const layer: Layer.Layer<
89
- Service,
90
- never,
91
- | Session.Service
92
- | Config.Service
93
- | Bus.Service
94
- | Snapshot.Service
95
- | Agent.Service
96
- | LLM.Service
97
- | Permission.Service
98
- | Plugin.Service
99
- | SessionSummary.Service
100
- | SessionStatus.Service
101
- > = Layer.effect(
102
- Service,
103
- Effect.gen(function* () {
104
- const session = yield* Session.Service
105
- const config = yield* Config.Service
106
- const bus = yield* Bus.Service
107
- const snapshot = yield* Snapshot.Service
108
- const agents = yield* Agent.Service
109
- const llm = yield* LLM.Service
110
- const permission = yield* Permission.Service
111
- const plugin = yield* Plugin.Service
112
- const summary = yield* SessionSummary.Service
113
- const scope = yield* Scope.Scope
114
- const status = yield* SessionStatus.Service
115
-
116
- const create = Effect.fn("SessionProcessor.create")(function* (input: Input) {
117
- // Pre-capture snapshot before the LLM stream starts. The AI SDK
118
- // may execute tools internally before emitting start-step events,
119
- // so capturing inside the event handler can be too late.
120
- const initialSnapshot = yield* snapshot.track({
121
- sessionID: input.sessionID,
122
- messageID: input.assistantMessage.id,
123
- })
124
- const ctx: ProcessorContext = {
125
- assistantMessage: input.assistantMessage,
126
- sessionID: input.sessionID,
127
- model: input.model,
128
- toolcalls: {},
129
- shouldBreak: false,
130
- snapshot: initialSnapshot,
131
- blocked: false,
132
- needsCompaction: false,
133
- compactionError: undefined,
134
- currentText: undefined,
135
- reasoningMap: {},
136
- telemetry: input.telemetry,
137
- stepStart: 0,
138
- step: { reasoning: false, text: false, tool: false },
139
- }
140
- let aborted = false
141
- const ac = new AbortController()
142
- const slog = log.clone().tag("session.id", input.sessionID).tag("messageID", input.assistantMessage.id)
143
-
144
- const parse = (e: unknown) =>
145
- MessageV2.fromError(e, {
146
- providerID: input.model.providerID,
147
- aborted,
148
- })
149
-
150
- const settleToolCall = Effect.fn("SessionProcessor.settleToolCall")(function* (toolCallID: string) {
151
- const done = ctx.toolcalls[toolCallID]?.done
152
- delete ctx.toolcalls[toolCallID]
153
- if (done) yield* Deferred.succeed(done, undefined).pipe(Effect.ignore)
154
- })
155
-
156
- const readToolCall = Effect.fn("SessionProcessor.readToolCall")(function* (toolCallID: string) {
157
- const call = ctx.toolcalls[toolCallID]
158
- if (!call) return
159
- const part = yield* session.getPart({
160
- partID: call.partID,
161
- messageID: call.messageID,
162
- sessionID: call.sessionID,
163
- })
164
- if (!part || part.type !== "tool") {
165
- delete ctx.toolcalls[toolCallID]
166
- return
167
- }
168
- return { call, part }
169
- })
170
- const reconcile = Effect.fn("SessionProcessor.reconcileCost")(function* () {
171
- const fresh = yield* Effect.sync(() => {
172
- try {
173
- return MessageV2.get({ sessionID: ctx.assistantMessage.sessionID, messageID: ctx.assistantMessage.id })
174
- } catch (err) {
175
- if (NotFoundError.isInstance(err)) return
176
- throw err
177
- }
178
- })
179
- if (fresh?.info.role !== "assistant") return
180
- if (fresh.info.cost <= ctx.assistantMessage.cost) return
181
- ctx.assistantMessage.cost = fresh.info.cost
182
- })
183
-
184
- const updateToolCall = Effect.fn("SessionProcessor.updateToolCall")(function* (
185
- toolCallID: string,
186
- update: (part: MessageV2.ToolPart) => MessageV2.ToolPart,
187
- ) {
188
- const match = yield* readToolCall(toolCallID)
189
- if (!match) return
190
- const part = yield* session.updatePart(update(match.part))
191
- ctx.toolcalls[toolCallID] = {
192
- ...match.call,
193
- partID: part.id,
194
- messageID: part.messageID,
195
- sessionID: part.sessionID,
196
- }
197
- return part
198
- })
199
-
200
- const completeToolCall = Effect.fn("SessionProcessor.completeToolCall")(function* (
201
- toolCallID: string,
202
- output: {
203
- title: string
204
- metadata: Record<string, any>
205
- output: string
206
- attachments?: MessageV2.FilePart[]
207
- },
208
- ) {
209
- const match = yield* readToolCall(toolCallID)
210
- if (!match || match.part.state.status !== "running") return
211
- yield* session.updatePart({
212
- ...match.part,
213
- state: {
214
- status: "completed",
215
- input: match.part.state.input,
216
- output: output.output,
217
- metadata: output.metadata,
218
- title: output.title,
219
- time: { start: match.part.state.time.start, end: Date.now() },
220
- attachments: output.attachments,
221
- },
222
- })
223
- yield* settleToolCall(toolCallID)
224
- })
225
-
226
- const failToolCall = Effect.fn("SessionProcessor.failToolCall")(function* (toolCallID: string, error: unknown) {
227
- const match = yield* readToolCall(toolCallID)
228
- if (!match || match.part.state.status !== "running") return false
229
- yield* session.updatePart({
230
- ...match.part,
231
- state: {
232
- status: "error",
233
- input: match.part.state.input,
234
- error: errorMessage(error),
235
- time: { start: match.part.state.time.start, end: Date.now() },
236
- },
237
- })
238
- if (
239
- error instanceof Permission.RejectedError ||
240
- error instanceof Question.RejectedError ||
241
- error instanceof Suggestion.DismissedError
242
- ) {
243
- ctx.blocked = ctx.shouldBreak
244
- }
245
- yield* settleToolCall(toolCallID)
246
- return true
247
- })
248
-
249
- const handleEvent = Effect.fnUntraced(function* (value: StreamEvent) {
250
- switch (value.type) {
251
- case "start":
252
- yield* status.set(ctx.sessionID, { type: "busy" })
253
- return
254
-
255
- case "reasoning-start":
256
- if (value.id in ctx.reasoningMap) return
257
- ctx.step.reasoning = true
258
- ctx.reasoningMap[value.id] = {
259
- id: PartID.ascending(),
260
- messageID: ctx.assistantMessage.id,
261
- sessionID: ctx.assistantMessage.sessionID,
262
- type: "reasoning",
263
- text: "",
264
- time: { start: Date.now() },
265
- metadata: value.providerMetadata,
266
- }
267
- yield* session.updatePart(ctx.reasoningMap[value.id])
268
- return
269
-
270
- case "reasoning-delta":
271
- if (!(value.id in ctx.reasoningMap)) return
272
- ctx.reasoningMap[value.id].text += value.text
273
- if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata
274
- yield* session.updatePartDelta({
275
- sessionID: ctx.reasoningMap[value.id].sessionID,
276
- messageID: ctx.reasoningMap[value.id].messageID,
277
- partID: ctx.reasoningMap[value.id].id,
278
- field: "text",
279
- delta: value.text,
280
- })
281
- return
282
-
283
- case "reasoning-end":
284
- if (!(value.id in ctx.reasoningMap)) return
285
- // oxlint-disable-next-line no-self-assign -- reactivity trigger
286
- ctx.reasoningMap[value.id].text = ctx.reasoningMap[value.id].text
287
- ctx.reasoningMap[value.id].time = { ...ctx.reasoningMap[value.id].time, end: Date.now() }
288
- if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata
289
- yield* session.updatePart(ctx.reasoningMap[value.id])
290
- delete ctx.reasoningMap[value.id]
291
- return
292
-
293
- case "tool-input-start":
294
- if (ctx.assistantMessage.summary) {
295
- throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`)
296
- }
297
- ctx.step.tool = true
298
- const part = yield* session.updatePart({
299
- id: ctx.toolcalls[value.id]?.partID ?? PartID.ascending(),
300
- messageID: ctx.assistantMessage.id,
301
- sessionID: ctx.assistantMessage.sessionID,
302
- type: "tool",
303
- tool: value.toolName,
304
- callID: value.id,
305
- state: { status: "pending", input: {}, raw: "" },
306
- metadata: value.providerExecuted ? { providerExecuted: true } : undefined,
307
- } satisfies MessageV2.ToolPart)
308
- ctx.toolcalls[value.id] = {
309
- done: yield* Deferred.make<void>(),
310
- partID: part.id,
311
- messageID: part.messageID,
312
- sessionID: part.sessionID,
313
- }
314
- return
315
-
316
- case "tool-input-delta":
317
- return
318
-
319
- case "tool-input-end":
320
- return
321
-
322
- case "tool-call": {
323
- if (ctx.assistantMessage.summary) {
324
- throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`)
325
- }
326
- ctx.step.tool = true
327
- if (!ctx.toolcalls[value.toolCallId]) {
328
- log.warn("tool-call without prior tool-input-start", {
329
- toolCallId: value.toolCallId,
330
- toolName: value.toolName,
331
- })
332
- const part = yield* session.updatePart({
333
- id: PartID.ascending(),
334
- messageID: ctx.assistantMessage.id,
335
- sessionID: ctx.assistantMessage.sessionID,
336
- type: "tool",
337
- tool: value.toolName,
338
- callID: value.toolCallId,
339
- state: { status: "pending", input: {}, raw: "" },
340
- } satisfies MessageV2.ToolPart)
341
- ctx.toolcalls[value.toolCallId] = {
342
- done: yield* Deferred.make<void>(),
343
- partID: part.id,
344
- messageID: part.messageID,
345
- sessionID: part.sessionID,
346
- }
347
- }
348
- yield* updateToolCall(value.toolCallId, (match) => ({
349
- ...match,
350
- tool: value.toolName,
351
- state: {
352
- ...match.state,
353
- status: "running",
354
- input: value.input,
355
- time: { start: Date.now() },
356
- },
357
- metadata: match.metadata?.providerExecuted
358
- ? { ...value.providerMetadata, providerExecuted: true }
359
- : value.providerMetadata,
360
- }))
361
-
362
- const parts = MessageV2.parts(ctx.assistantMessage.id)
363
- const recentParts = parts.slice(-DOOM_LOOP_THRESHOLD)
364
-
365
- if (
366
- recentParts.length !== DOOM_LOOP_THRESHOLD ||
367
- !recentParts.every(
368
- (part) =>
369
- part.type === "tool" &&
370
- part.tool === value.toolName &&
371
- part.state.status !== "pending" &&
372
- JSON.stringify(part.state.input) === JSON.stringify(value.input),
373
- )
374
- ) {
375
- return
376
- }
377
-
378
- const agent = yield* agents.get(ctx.assistantMessage.agent)
379
- yield* permission.ask({
380
- permission: "doom_loop",
381
- patterns: [value.toolName],
382
- sessionID: ctx.assistantMessage.sessionID,
383
- metadata: { tool: value.toolName, input: value.input },
384
- always: [value.toolName],
385
- ruleset: agent.permission,
386
- })
387
- return
388
- }
389
-
390
- case "tool-result": {
391
- yield* completeToolCall(value.toolCallId, value.output)
392
- if (value.output.metadata?.dismissed === true) {
393
- ctx.blocked = ctx.shouldBreak
394
- }
395
- return
396
- }
397
-
398
- case "tool-error": {
399
- yield* failToolCall(value.toolCallId, value.error)
400
- return
401
- }
402
-
403
- case "error":
404
- throw value.error
405
-
406
- case "start-step":
407
- ctx.stepStart = performance.now()
408
- ctx.step = { reasoning: false, text: false, tool: false }
409
- if (!ctx.snapshot)
410
- ctx.snapshot = yield* snapshot.track({ sessionID: ctx.sessionID, messageID: ctx.assistantMessage.id })
411
- yield* session.updatePart({
412
- id: PartID.ascending(),
413
- messageID: ctx.assistantMessage.id,
414
- sessionID: ctx.sessionID,
415
- snapshot: ctx.snapshot,
416
- type: "step-start",
417
- })
418
- return
419
-
420
- case "finish-step": {
421
- const usage = Session.getUsage({
422
- model: ctx.model,
423
- usage: value.usage,
424
- metadata: value.providerMetadata,
425
- })
426
- // ctx.stepStart is 0 until `start-step` fires, which would feed a
427
- // huge bogus `elapsed` into telemetry. Fall back to now().
428
- SaeeolSessionProcessor.trackStep({
429
- sessionID: ctx.sessionID,
430
- model: ctx.model,
431
- tokens: usage.tokens,
432
- cost: usage.cost,
433
- elapsed: Math.round(performance.now() - (ctx.stepStart || performance.now())),
434
- telemetry: ctx.telemetry,
435
- })
436
- ctx.assistantMessage.finish = value.finishReason
437
- yield* reconcile()
438
- ctx.assistantMessage.cost += usage.cost
439
- ctx.assistantMessage.tokens = usage.tokens
440
- yield* session.updatePart({
441
- id: PartID.ascending(),
442
- reason: value.finishReason,
443
- snapshot: yield* snapshot.track({
444
- sessionID: ctx.sessionID,
445
- messageID: ctx.assistantMessage.id,
446
- }),
447
- messageID: ctx.assistantMessage.id,
448
- sessionID: ctx.assistantMessage.sessionID,
449
- type: "step-finish",
450
- tokens: usage.tokens,
451
- cost: usage.cost,
452
- })
453
- const warn = SaeeolSessionProcessor.lengthWarning({ msg: ctx.assistantMessage, step: ctx.step })
454
- if (warn) {
455
- yield* session.updatePart({
456
- id: PartID.ascending(),
457
- messageID: ctx.assistantMessage.id,
458
- sessionID: ctx.assistantMessage.sessionID,
459
- type: "text",
460
- text: warn,
461
- ignored: true,
462
- })
463
- }
464
- const providerError = SaeeolSessionProcessor.providerFinishError(ctx.assistantMessage)
465
- if (providerError) {
466
- yield* bus.publish(Session.Event.Error, {
467
- sessionID: ctx.assistantMessage.sessionID,
468
- error: providerError,
469
- })
470
- yield* status.set(ctx.sessionID, { type: "idle" })
471
- }
472
- yield* session.updateMessage(ctx.assistantMessage)
473
- if (ctx.snapshot) {
474
- const patch = yield* snapshot.patch(ctx.snapshot)
475
- if (patch.files.length) {
476
- yield* session.updatePart({
477
- id: PartID.ascending(),
478
- messageID: ctx.assistantMessage.id,
479
- sessionID: ctx.sessionID,
480
- type: "patch",
481
- hash: patch.hash,
482
- files: patch.files,
483
- })
484
- }
485
- ctx.snapshot = undefined
486
- }
487
- yield* summary
488
- .summarize({
489
- sessionID: ctx.sessionID,
490
- messageID: ctx.assistantMessage.parentID,
491
- })
492
- .pipe(Effect.ignore, Effect.forkIn(scope))
493
- if (
494
- !ctx.assistantMessage.summary &&
495
- isOverflow({ cfg: yield* config.get(), tokens: usage.tokens, model: ctx.model })
496
- ) {
497
- ctx.needsCompaction = true
498
- ctx.compactionError = new MessageV2.ContextOverflowError({
499
- message: "Input exceeds context window of this model",
500
- }).toObject()
501
- }
502
- return
503
- }
504
-
505
- case "text-start":
506
- ctx.currentText = {
507
- id: PartID.ascending(),
508
- messageID: ctx.assistantMessage.id,
509
- sessionID: ctx.assistantMessage.sessionID,
510
- type: "text",
511
- text: "",
512
- time: { start: Date.now() },
513
- metadata: value.providerMetadata,
514
- }
515
- yield* session.updatePart(ctx.currentText)
516
- return
517
-
518
- case "text-delta":
519
- if (!ctx.currentText) return
520
- ctx.currentText.text += value.text
521
- if (value.text.trim()) ctx.step.text = true
522
- if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata
523
- yield* session.updatePartDelta({
524
- sessionID: ctx.currentText.sessionID,
525
- messageID: ctx.currentText.messageID,
526
- partID: ctx.currentText.id,
527
- field: "text",
528
- delta: value.text,
529
- })
530
- return
531
-
532
- case "text-end":
533
- if (!ctx.currentText) return
534
- // oxlint-disable-next-line no-self-assign -- reactivity trigger
535
- ctx.currentText.text = ctx.currentText.text
536
- ctx.currentText.text = (yield* plugin.trigger(
537
- "experimental.text.complete",
538
- {
539
- sessionID: ctx.sessionID,
540
- messageID: ctx.assistantMessage.id,
541
- partID: ctx.currentText.id,
542
- },
543
- { text: ctx.currentText.text },
544
- )).text
545
- if (ctx.currentText.text.trim()) ctx.step.text = true
546
- {
547
- const end = Date.now()
548
- ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end }
549
- }
550
- if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata
551
- yield* session.updatePart(ctx.currentText)
552
- ctx.currentText = undefined
553
- return
554
-
555
- case "finish":
556
- return
557
-
558
- default:
559
- slog.info("unhandled", { event: value.type, value })
560
- return
561
- }
562
- })
563
-
564
- const cleanup = Effect.fn("SessionProcessor.cleanup")(function* () {
565
- if (ctx.snapshot) {
566
- const patch = yield* snapshot.patch(ctx.snapshot)
567
- if (patch.files.length) {
568
- yield* session.updatePart({
569
- id: PartID.ascending(),
570
- messageID: ctx.assistantMessage.id,
571
- sessionID: ctx.sessionID,
572
- type: "patch",
573
- hash: patch.hash,
574
- files: patch.files,
575
- })
576
- }
577
- ctx.snapshot = undefined
578
- }
579
-
580
- if (ctx.currentText) {
581
- const end = Date.now()
582
- ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end }
583
- yield* session.updatePart(ctx.currentText)
584
- ctx.currentText = undefined
585
- }
586
-
587
- for (const part of Object.values(ctx.reasoningMap)) {
588
- const end = Date.now()
589
- yield* session.updatePart({
590
- ...part,
591
- time: { start: part.time.start ?? end, end },
592
- })
593
- }
594
- ctx.reasoningMap = {}
595
-
596
- yield* Effect.forEach(
597
- Object.values(ctx.toolcalls),
598
- (call) => Deferred.await(call.done).pipe(Effect.timeout("250 millis"), Effect.ignore),
599
- { concurrency: "unbounded" },
600
- )
601
-
602
- for (const toolCallID of Object.keys(ctx.toolcalls)) {
603
- const match = yield* readToolCall(toolCallID)
604
- if (!match) continue
605
- const part = match.part
606
- const end = Date.now()
607
- const metadata = "metadata" in part.state && isRecord(part.state.metadata) ? part.state.metadata : {}
608
- yield* session.updatePart({
609
- ...part,
610
- state: {
611
- ...part.state,
612
- status: "error",
613
- error: "Tool execution aborted",
614
- metadata: { ...metadata, interrupted: true },
615
- time: { start: "time" in part.state ? part.state.time.start : end, end },
616
- },
617
- })
618
- }
619
- ctx.toolcalls = {}
620
- SaeeolSessionProcessor.guardEmptyToolCalls(ctx.assistantMessage, MessageV2.parts(ctx.assistantMessage.id))
621
- ctx.assistantMessage.time.completed = Date.now()
622
- yield* reconcile()
623
- yield* session.updateMessage(ctx.assistantMessage)
624
- })
625
-
626
- const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) {
627
- slog.error("process", { error: errorMessage(e), stack: e instanceof Error ? e.stack : undefined })
628
- const error = parse(e)
629
- ctx.compactionError = MessageV2.ContextOverflowError.isInstance(error) ? error : ctx.compactionError
630
- if (MessageV2.ContextOverflowError.isInstance(error)) {
631
- ctx.needsCompaction = true
632
- yield* bus.publish(Session.Event.Error, { sessionID: ctx.sessionID, error })
633
- return
634
- }
635
- ctx.assistantMessage.error = error
636
- yield* bus.publish(Session.Event.Error, {
637
- sessionID: ctx.assistantMessage.sessionID,
638
- error: ctx.assistantMessage.error,
639
- })
640
- yield* status.set(ctx.sessionID, { type: "idle" })
641
- })
642
- const output = {
643
- compactError: () => ctx.compactionError,
644
- }
645
-
646
- const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) {
647
- slog.info("process")
648
- ctx.needsCompaction = false
649
- ctx.compactionError = undefined
650
- ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true
651
-
652
- return yield* Effect.gen(function* () {
653
- yield* Effect.gen(function* () {
654
- ctx.currentText = undefined
655
- ctx.reasoningMap = {}
656
- ctx.step = { reasoning: false, text: false, tool: false }
657
- const stream = llm.stream(streamInput)
658
-
659
- yield* stream.pipe(
660
- Stream.tap((event) => handleEvent(event)),
661
- Stream.takeUntil(() => ctx.needsCompaction),
662
- Stream.runDrain,
663
- )
664
- }).pipe(
665
- Effect.onInterrupt(() =>
666
- Effect.gen(function* () {
667
- aborted = true
668
- ac.abort()
669
- if (!ctx.assistantMessage.error) {
670
- yield* halt(new DOMException("Aborted", "AbortError"))
671
- }
672
- }),
673
- ),
674
- Effect.catchCauseIf(
675
- (cause) => !Cause.hasInterruptsOnly(cause),
676
- (cause) => Effect.fail(Cause.squash(cause)),
677
- ),
678
- Effect.retry(
679
- SessionRetry.policy({
680
- parse,
681
- ...SaeeolSessionProcessor.retryOpts({ sessionID: ctx.sessionID, abort: ac.signal, set: status.set }),
682
- set: (info) =>
683
- status.set(ctx.sessionID, {
684
- type: "retry",
685
- attempt: info.attempt,
686
- message: info.message,
687
- next: info.next,
688
- }),
689
- }),
690
- ),
691
- Effect.catch(halt),
692
- Effect.ensuring(cleanup()),
693
- )
694
-
695
- if (ctx.needsCompaction) return "compact"
696
- if (ctx.blocked || ctx.assistantMessage.error) return "stop"
697
- return "continue"
698
- })
699
- })
700
-
701
- return {
702
- get message() {
703
- return ctx.assistantMessage
704
- },
705
- updateToolCall,
706
- completeToolCall,
707
- ...output,
708
- process,
709
- } satisfies Handle
710
- })
711
-
712
- return Service.of({ create })
713
- }),
714
- )
715
-
716
- export const defaultLayer = Layer.suspend(() =>
717
- layer.pipe(
718
- Layer.provide(Session.defaultLayer),
719
- Layer.provide(Snapshot.defaultLayer),
720
- Layer.provide(Agent.defaultLayer),
721
- Layer.provide(LLM.defaultLayer),
722
- Layer.provide(Permission.defaultLayer),
723
- Layer.provide(Plugin.defaultLayer),
724
- Layer.provide(SessionSummary.defaultLayer),
725
- Layer.provide(SessionStatus.defaultLayer),
726
- Layer.provide(Bus.layer),
727
- Layer.provide(Config.defaultLayer),
728
- ),
729
- )
730
-
731
- export * as SessionProcessor from "./processor"
1
+ export * from "./core/processor"