saeeol 1.2.0 → 1.2.2

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 (193) hide show
  1. package/package.json +14 -14
  2. package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
  3. package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
  4. package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
  5. package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
  6. package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
  7. package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
  8. package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
  9. package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
  10. package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
  11. package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
  12. package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
  13. package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
  14. package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
  15. package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
  16. package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
  17. package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
  18. package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
  19. package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
  20. package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
  21. package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
  22. package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
  23. package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
  24. package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
  25. package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
  26. package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
  27. package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
  28. package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
  29. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
  30. package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
  31. package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
  32. package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
  33. package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
  34. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
  35. package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
  36. package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
  37. package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
  38. package/src/session/compaction-helpers.ts +1 -169
  39. package/src/session/compaction.ts +1 -712
  40. package/src/session/core/compaction/compaction-helpers.ts +169 -0
  41. package/src/session/core/compaction/compaction.ts +712 -0
  42. package/src/session/core/compaction/overflow.ts +28 -0
  43. package/src/session/core/instruction.ts +234 -0
  44. package/src/session/core/llm.ts +504 -0
  45. package/src/session/core/network.ts +392 -0
  46. package/src/session/core/processor.ts +731 -0
  47. package/src/session/core/projectors.ts +139 -0
  48. package/src/session/core/resolve-tools.ts +241 -0
  49. package/src/session/core/retry.ts +149 -0
  50. package/src/session/core/revert.ts +173 -0
  51. package/src/session/core/run-state.ts +110 -0
  52. package/src/session/core/schema.ts +35 -0
  53. package/src/session/core/session-types.ts +160 -0
  54. package/src/session/core/session.sql.ts +124 -0
  55. package/src/session/core/session.ts +948 -0
  56. package/src/session/core/shell-exec.ts +205 -0
  57. package/src/session/core/status.ts +100 -0
  58. package/src/session/core/subtask.ts +268 -0
  59. package/src/session/core/summary.ts +173 -0
  60. package/src/session/core/system.ts +114 -0
  61. package/src/session/core/todo.ts +86 -0
  62. package/src/session/core/user-part.ts +293 -0
  63. package/src/session/instruction.ts +1 -234
  64. package/src/session/llm.ts +1 -504
  65. package/src/session/message/message-errors.ts +83 -0
  66. package/src/session/message/message-parts.ts +89 -0
  67. package/src/session/message/message-query.ts +107 -0
  68. package/src/session/message/message-transform.ts +156 -0
  69. package/src/session/message/message-types.ts +68 -0
  70. package/src/session/message/message-v2.ts +73 -0
  71. package/src/session/message/message.ts +192 -0
  72. package/src/session/message-errors.ts +1 -83
  73. package/src/session/message-parts.ts +1 -89
  74. package/src/session/message-query.ts +1 -107
  75. package/src/session/message-transform.ts +1 -156
  76. package/src/session/message-types.ts +1 -68
  77. package/src/session/message-v2.ts +1 -73
  78. package/src/session/message.ts +1 -192
  79. package/src/session/network.ts +1 -392
  80. package/src/session/overflow.ts +1 -28
  81. package/src/session/processor.ts +1 -731
  82. package/src/session/projectors.ts +2 -139
  83. package/src/session/prompt/prompt-command.ts +93 -0
  84. package/src/session/prompt/prompt-loop.ts +299 -0
  85. package/src/session/prompt/prompt-model.ts +44 -0
  86. package/src/session/prompt/prompt-reminders.ts +120 -0
  87. package/src/session/prompt/prompt-resolve.ts +42 -0
  88. package/src/session/prompt/prompt-schemas.ts +128 -0
  89. package/src/session/prompt/prompt-title.ts +55 -0
  90. package/src/session/prompt/prompt-types.ts +47 -0
  91. package/src/session/prompt/prompt-user-msg.ts +80 -0
  92. package/src/session/prompt/prompt.ts +211 -0
  93. package/src/session/prompt-command.ts +1 -93
  94. package/src/session/prompt-loop.ts +1 -299
  95. package/src/session/prompt-model.ts +1 -44
  96. package/src/session/prompt-reminders.ts +1 -120
  97. package/src/session/prompt-resolve.ts +1 -42
  98. package/src/session/prompt-schemas.ts +1 -128
  99. package/src/session/prompt-title.ts +1 -55
  100. package/src/session/prompt-types.ts +1 -47
  101. package/src/session/prompt-user-msg.ts +1 -80
  102. package/src/session/prompt.ts +1 -211
  103. package/src/session/resolve-tools.ts +1 -241
  104. package/src/session/retry.ts +1 -149
  105. package/src/session/revert.ts +1 -173
  106. package/src/session/run-state.ts +1 -110
  107. package/src/session/schema.ts +1 -35
  108. package/src/session/session-types.ts +1 -160
  109. package/src/session/session.sql.ts +1 -124
  110. package/src/session/session.ts +1 -948
  111. package/src/session/shell-exec.ts +1 -205
  112. package/src/session/status.ts +1 -100
  113. package/src/session/subtask.ts +1 -268
  114. package/src/session/summary.ts +1 -173
  115. package/src/session/system.ts +1 -114
  116. package/src/session/todo.ts +1 -86
  117. package/src/session/user-part.ts +1 -293
  118. package/src/tool/apply_patch.ts +1 -334
  119. package/src/tool/bash.ts +1 -656
  120. package/src/tool/core/external-directory.ts +55 -0
  121. package/src/tool/core/invalid.ts +21 -0
  122. package/src/tool/core/recall.ts +164 -0
  123. package/src/tool/core/recall.txt +12 -0
  124. package/src/tool/core/schema.ts +16 -0
  125. package/src/tool/core/tool.ts +162 -0
  126. package/src/tool/core/truncate.ts +160 -0
  127. package/src/tool/core/truncation-dir.ts +4 -0
  128. package/src/tool/diagnostics.ts +1 -20
  129. package/src/tool/edit-replacers.ts +1 -288
  130. package/src/tool/edit-utils.ts +1 -86
  131. package/src/tool/edit.ts +1 -262
  132. package/src/tool/external-directory.ts +1 -55
  133. package/src/tool/file/apply_patch.ts +334 -0
  134. package/src/tool/file/apply_patch.txt +33 -0
  135. package/src/tool/file/bash.ts +656 -0
  136. package/src/tool/file/bash.txt +119 -0
  137. package/src/tool/file/edit-replacers.ts +288 -0
  138. package/src/tool/file/edit-utils.ts +86 -0
  139. package/src/tool/file/edit.ts +262 -0
  140. package/src/tool/file/edit.txt +10 -0
  141. package/src/tool/file/read.ts +389 -0
  142. package/src/tool/file/read.txt +14 -0
  143. package/src/tool/file/write.ts +114 -0
  144. package/src/tool/file/write.txt +8 -0
  145. package/src/tool/glob.ts +1 -115
  146. package/src/tool/grep.ts +1 -151
  147. package/src/tool/integration/diagnostics.ts +20 -0
  148. package/src/tool/integration/lsp.ts +113 -0
  149. package/src/tool/integration/lsp.txt +24 -0
  150. package/src/tool/integration/mcp-exa.ts +73 -0
  151. package/src/tool/integration/package.ts +168 -0
  152. package/src/tool/integration/registry.ts +375 -0
  153. package/src/tool/invalid.ts +1 -21
  154. package/src/tool/lsp.ts +1 -113
  155. package/src/tool/mcp-exa.ts +1 -73
  156. package/src/tool/package.ts +1 -168
  157. package/src/tool/plan.ts +1 -30
  158. package/src/tool/question.ts +1 -52
  159. package/src/tool/read.ts +1 -389
  160. package/src/tool/recall.ts +1 -164
  161. package/src/tool/registry.ts +1 -375
  162. package/src/tool/schema.ts +1 -16
  163. package/src/tool/search/glob.ts +115 -0
  164. package/src/tool/search/glob.txt +6 -0
  165. package/src/tool/search/grep.ts +151 -0
  166. package/src/tool/search/grep.txt +8 -0
  167. package/src/tool/search/warpgrep.ts +107 -0
  168. package/src/tool/search/warpgrep.txt +10 -0
  169. package/src/tool/search/webfetch.ts +202 -0
  170. package/src/tool/search/webfetch.txt +13 -0
  171. package/src/tool/search/websearch.ts +71 -0
  172. package/src/tool/search/websearch.txt +14 -0
  173. package/src/tool/skill.ts +1 -91
  174. package/src/tool/task.ts +1 -197
  175. package/src/tool/todo.ts +1 -62
  176. package/src/tool/tool.ts +1 -162
  177. package/src/tool/truncate.ts +1 -160
  178. package/src/tool/truncation-dir.ts +1 -4
  179. package/src/tool/warpgrep.ts +1 -107
  180. package/src/tool/webfetch.ts +1 -202
  181. package/src/tool/websearch.ts +1 -71
  182. package/src/tool/workflow/plan-enter.txt +14 -0
  183. package/src/tool/workflow/plan-exit.txt +13 -0
  184. package/src/tool/workflow/plan.ts +30 -0
  185. package/src/tool/workflow/question.ts +52 -0
  186. package/src/tool/workflow/question.txt +11 -0
  187. package/src/tool/workflow/skill.ts +91 -0
  188. package/src/tool/workflow/skill.txt +5 -0
  189. package/src/tool/workflow/task.ts +197 -0
  190. package/src/tool/workflow/task.txt +57 -0
  191. package/src/tool/workflow/todo.ts +62 -0
  192. package/src/tool/workflow/todowrite.txt +167 -0
  193. package/src/tool/write.ts +1 -114
@@ -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"