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,211 +1 @@
1
- // @ts-ignore
2
- globalThis.AI_SDK_LOG_WARNINGS = false
3
-
4
- import { Effect, Layer, Scope, Latch, Context } from "effect"
5
- import { ChildProcessSpawner } from "effect/unstable/process"
6
- import { CrossSpawnSpawner } from "@saeeol/core/cross-spawn-spawner"
7
- import { SaeeolSessionPrompt } from "@/saeeol/session/prompt"
8
- import { SaeeolSessionPromptQueue } from "@/saeeol/session/prompt-queue"
9
- import { SaeeolSession } from "@/saeeol/session"
10
- import { SaeeolSessionProcessor } from "@/saeeol/session/processor"
11
- import { Suggestion } from "@/saeeol/suggestion"
12
- import { Question } from "@/question"
13
- import { SessionID, MessageID } from "./schema"
14
- import { MessageV2 } from "./message-v2"
15
- import { makeRuntime } from "@/effect/run-service"
16
- import { InstanceState } from "@/effect/instance-state"
17
- import { EffectBridge } from "@/effect/bridge"
18
- import { SessionRunState } from "./run-state"
19
- import { SessionStatus } from "./status"
20
- import { SessionCompaction } from "./compaction"
21
- import { SessionProcessor } from "./processor"
22
- import { Command } from "../command"
23
- import { Permission } from "@/permission"
24
- import { MCP } from "../mcp"
25
- import { LSP } from "@/lsp/lsp"
26
- import { ToolRegistry } from "@/tool/registry"
27
- import { Truncate } from "@/tool/truncate"
28
- import { Provider } from "@/provider/provider"
29
- import { Config } from "@/config/config"
30
- import { Instruction } from "./instruction"
31
- import { AppFileSystem } from "@saeeol/core/filesystem"
32
- import { Plugin } from "../plugin"
33
- import { Session as SessionMod } from "./session"
34
- import { SessionRevert } from "./revert"
35
- import { SessionSummary } from "./summary"
36
- import { Agent } from "../agent/agent"
37
- import { SystemPrompt } from "./system"
38
- import { LLM } from "./llm"
39
- import { Bus } from "../bus"
40
- import { Service as ResolveToolsService, defaultLayer as resolveToolsLayer } from "./resolve-tools"
41
- import { Service as UserPartService, defaultLayer as userPartLayer } from "./user-part"
42
- import { Service as SubtaskService, defaultLayer as subtaskLayer } from "./subtask"
43
- import { Service as ShellService, defaultLayer as shellLayer } from "./shell-exec"
44
- import { type PromptInput, type ShellInput, type CommandInput, LoopInput } from "./prompt-schemas"
45
- import { type Interface, Service } from "./prompt-types"
46
- import { resolvePromptParts } from "./prompt-resolve"
47
- import { getModel, lastModel, lastAssistant } from "./prompt-model"
48
- import { createUserMessage } from "./prompt-user-msg"
49
- import { ensureTitle } from "./prompt-title"
50
- import { insertReminders } from "./prompt-reminders"
51
- import { commandHandler } from "./prompt-command"
52
- import { createRunLoop } from "./prompt-loop"
53
-
54
- export const shouldAskPlanFollowup = SaeeolSessionPrompt.shouldAskPlanFollowup
55
-
56
- export const layer = Layer.effect(
57
- Service,
58
- Effect.gen(function* () {
59
- const bus = yield* Bus.Service
60
- const status = yield* SessionStatus.Service
61
- const sessions = yield* SessionMod.Service
62
- const agents = yield* Agent.Service
63
- const provider = yield* Provider.Service
64
- const processor = yield* SessionProcessor.Service
65
- const compaction = yield* SessionCompaction.Service
66
- const plugin = yield* Plugin.Service
67
- const commands = yield* Command.Service
68
- const config = yield* Config.Service
69
- const permission = yield* Permission.Service
70
- const fsys = yield* AppFileSystem.Service
71
- const mcp = yield* MCP.Service
72
- const lsp = yield* LSP.Service
73
- const registry = yield* ToolRegistry.Service
74
- const truncate = yield* Truncate.Service
75
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
76
- const scope = yield* Scope.Scope
77
- const instruction = yield* Instruction.Service
78
- const state = yield* SessionRunState.Service
79
- const revert = yield* SessionRevert.Service
80
- const summary = yield* SessionSummary.Service
81
- const sys = yield* SystemPrompt.Service
82
- const llm = yield* LLM.Service
83
- const resolveToolsSvc = yield* ResolveToolsService
84
- const userPartSvc = yield* UserPartService
85
- const subtaskSvc = yield* SubtaskService
86
- const shellSvc = yield* ShellService
87
-
88
- const deps = {
89
- bus, status, sessions, agents, provider, processor, compaction, plugin,
90
- commands, config, permission, fsys, mcp, lsp, registry, truncate,
91
- spawner, scope, instruction, state, revert, summary, sys, llm,
92
- resolveToolsSvc, userPartSvc, subtaskSvc, shellSvc,
93
- }
94
-
95
- const resolvePromptPartsFn = resolvePromptParts(deps as any)
96
- const getModelFn = getModel(deps as any)
97
- const lastModelFn = lastModel(deps as any)
98
- const lastAssistantFn = lastAssistant(deps as any)
99
- const createUserMessageFn = createUserMessage(deps as any)
100
- const ensureTitleFn = ensureTitle(deps as any)
101
- const insertRemindersFn = insertReminders(deps as any, SaeeolSessionPrompt)
102
- const commandFn = commandHandler(deps as any, SaeeolSessionProcessor.markReviewTelemetry.bind(SaeeolSessionProcessor))
103
-
104
- const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) {
105
- yield* SaeeolSessionPromptQueue.cancel(sessionID)
106
- SaeeolSessionPrompt.abortPlanFollowup(sessionID)
107
- yield* state.cancel(sessionID)
108
- })
109
-
110
- const { runLoop, loop, shell } = createRunLoop(
111
- deps as any,
112
- {
113
- getModel: getModelFn,
114
- lastModel: lastModelFn,
115
- lastAssistant: lastAssistantFn,
116
- createUserMessage: createUserMessageFn,
117
- ensureTitle: ensureTitleFn,
118
- insertReminders: insertRemindersFn,
119
- resolvePromptParts: resolvePromptPartsFn,
120
- commandHandler: commandFn,
121
- SaeeolSessionPrompt,
122
- SaeeolSessionPromptQueue,
123
- SaeeolSession,
124
- SaeeolSessionProcessor,
125
- } as any,
126
- )
127
-
128
- let promptFn: any
129
-
130
- const promptImpl = Effect.fn("SessionPrompt.prompt")(function* (input: PromptInput) {
131
- return yield* promptFn(input)
132
- })
133
-
134
- promptFn = Effect.fn("SessionPrompt.prompt")(function* (input: PromptInput) {
135
- const session = yield* sessions.get(input.sessionID)
136
- yield* revert.cleanup(session)
137
- yield* SaeeolSessionPrompt.recoverDanglingAssistant({ sessionID: input.sessionID, status, sessions })
138
- yield* SaeeolSessionPrompt.recoverProviderFinishError({ sessionID: input.sessionID, status, sessions })
139
- const message = yield* createUserMessageFn(input)
140
- yield* sessions.touch(input.sessionID)
141
- const permissions: Permission.Ruleset = []
142
- for (const [t, enabled] of Object.entries(input.tools ?? {})) {
143
- permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" })
144
- }
145
- if (permissions.length > 0) {
146
- session.permission = permissions
147
- yield* sessions.setPermission({ sessionID: session.id, permission: permissions })
148
- }
149
- yield* Effect.promise(() => Suggestion.dismissAll(input.sessionID))
150
- yield* Effect.promise(() => Question.dismissAll(input.sessionID))
151
- if (input.noReply === true) return message
152
- return yield* (SaeeolSessionPromptQueue.enqueue as any)(
153
- input.sessionID, message.info.id,
154
- loop({ sessionID: input.sessionID }),
155
- lastAssistantFn(input.sessionID),
156
- )
157
- })
158
-
159
- return Service.of({
160
- cancel,
161
- prompt: promptFn,
162
- loop: loop as any,
163
- shell: shell as any,
164
- command: commandFn as any,
165
- resolvePromptParts: resolvePromptPartsFn as any,
166
- })
167
- }),
168
- )
169
-
170
- export const defaultLayer = Layer.suspend(() =>
171
- layer.pipe(
172
- Layer.provide(
173
- Layer.mergeAll(
174
- resolveToolsLayer, userPartLayer, subtaskLayer, shellLayer,
175
- SessionRunState.defaultLayer, SessionStatus.defaultLayer,
176
- SessionCompaction.defaultLayer, SessionProcessor.defaultLayer,
177
- Command.defaultLayer, Permission.defaultLayer, MCP.defaultLayer,
178
- LSP.defaultLayer, ToolRegistry.defaultLayer, Truncate.defaultLayer,
179
- Provider.defaultLayer, Config.defaultLayer, Instruction.defaultLayer,
180
- AppFileSystem.defaultLayer, Plugin.defaultLayer, SessionMod.defaultLayer,
181
- SessionRevert.defaultLayer, SessionSummary.defaultLayer, Agent.defaultLayer,
182
- SystemPrompt.defaultLayer, LLM.defaultLayer, Bus.layer,
183
- CrossSpawnSpawner.defaultLayer,
184
- ),
185
- ),
186
- ),
187
- )
188
-
189
- export { Service } from "./prompt-types"
190
- export type { Interface } from "./prompt-types"
191
-
192
- export {
193
- PromptInput,
194
- type PromptInput as PromptInputType,
195
- ShellInput,
196
- type ShellInput as ShellInputType,
197
- CommandInput,
198
- type CommandInput as CommandInputType,
199
- LoopInput,
200
- createStructuredOutputTool,
201
- STRUCTURED_OUTPUT_DESCRIPTION, STRUCTURED_OUTPUT_SYSTEM_PROMPT,
202
- REQUEST_PRUNE_BYTES,
203
- bashRegex, argsRegex, placeholderRegex, quoteTrimRegex,
204
- } from "./prompt-schemas"
205
-
206
- const { runPromise } = makeRuntime(Service, defaultLayer)
207
- export const prompt = (input: PromptInput) => runPromise((svc) => svc.prompt(input))
208
- export const loopExport = (input: LoopInput) => runPromise((svc) => svc.loop(input))
209
- export const cancel = (sessionID: SessionID) => runPromise((svc) => svc.cancel(sessionID))
210
-
211
- export * as SessionPrompt from "./prompt"
1
+ export * from "./prompt/prompt"
@@ -1,241 +1 @@
1
- import { type Tool as AITool, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai"
2
- import { Effect, Context, Layer } from "effect"
3
- import * as EffectZod from "@/util/effect-zod"
4
- import { ModelID, ProviderID } from "../provider/schema"
5
- import { SessionID, PartID } from "./schema"
6
- import { MessageV2 } from "./message-v2"
7
- import * as Session from "./session"
8
- import { Agent } from "../agent/agent"
9
- import { Provider } from "@/provider/provider"
10
- import { ProviderTransform } from "@/provider/transform"
11
- import { ToolRegistry } from "@/tool/registry"
12
- import { MCP } from "../mcp"
13
- import { Plugin } from "../plugin"
14
- import { Permission } from "@/permission"
15
- import { Truncate } from "@/tool/truncate"
16
- import { SessionProcessor } from "./processor"
17
- import { SaeeolSessionPrompt } from "@/saeeol/session/prompt"
18
- import { EffectBridge } from "@/effect/bridge"
19
- import { Tool } from "@/tool/tool"
20
- import { type TaskPromptOps } from "@/tool/task"
21
- import * as Log from "@saeeol/core/util/log"
22
-
23
- const log = Log.create({ service: "session.resolve-tools" })
24
-
25
- export interface ResolveToolsInput {
26
- agent: Agent.Info
27
- model: Provider.Model
28
- session: Session.Info
29
- tools?: Record<string, boolean>
30
- processor: Pick<SessionProcessor.Handle, "message" | "updateToolCall" | "completeToolCall">
31
- bypassAgentCheck: boolean
32
- messages: MessageV2.WithParts[]
33
- }
34
-
35
- export interface Interface {
36
- readonly resolve: (input: ResolveToolsInput) => Effect.Effect<Record<string, AITool>>
37
- }
38
-
39
- export class Service extends Context.Service<Service, Interface>()("@saeeol/SessionResolveTools") {}
40
-
41
- export const layer = Layer.effect(
42
- Service,
43
- Effect.gen(function* () {
44
- const registry = yield* ToolRegistry.Service
45
- const plugin = yield* Plugin.Service
46
- const permission = yield* Permission.Service
47
- const mcp = yield* MCP.Service
48
- const truncate = yield* Truncate.Service
49
-
50
- const resolve = Effect.fn("SessionResolveTools.resolve")(function* (input: ResolveToolsInput) {
51
- using _ = log.time("resolveTools")
52
- const tools: Record<string, AITool> = {}
53
- const bridge = yield* EffectBridge.make()
54
- const promptOps: TaskPromptOps = {
55
- cancel: (sessionID: SessionID) => Effect.void,
56
- resolvePromptParts: (template: string) => Effect.succeed([{ type: "text" as const, text: template }]) as any,
57
- prompt: (_input: any) => Effect.fail(new Error("not available")) as any,
58
- }
59
-
60
- const context = (args: any, options: ToolExecutionOptions): Tool.Context => ({
61
- sessionID: input.session.id,
62
- abort: options.abortSignal!,
63
- messageID: input.processor.message.id,
64
- callID: options.toolCallId,
65
- extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck, promptOps },
66
- agent: input.agent.name,
67
- messages: input.messages,
68
- metadata: (val: any) =>
69
- input.processor.updateToolCall(options.toolCallId, (match) => {
70
- if (!["running", "pending"].includes(match.state.status)) return match
71
- return {
72
- ...match,
73
- state: {
74
- title: val.title,
75
- metadata: val.metadata,
76
- status: "running",
77
- input: args,
78
- time: { start: Date.now() },
79
- },
80
- }
81
- }),
82
- ask: (req: any) =>
83
- permission
84
- .ask({
85
- ...req,
86
- sessionID: input.session.id,
87
- tool: { messageID: input.processor.message.id, callID: options.toolCallId },
88
- ruleset: Permission.merge(
89
- input.agent.permission,
90
- SaeeolSessionPrompt.guardPermissions({ agent: input.agent, session: input.session }),
91
- ),
92
- hardRuleset: SaeeolSessionPrompt.hardPermissions({ agent: input.agent }),
93
- })
94
- .pipe(Effect.orDie),
95
- })
96
-
97
- for (const item of yield* registry.tools({
98
- modelID: ModelID.make(input.model.api.id),
99
- providerID: input.model.providerID,
100
- agent: input.agent,
101
- })) {
102
- const schema = ProviderTransform.schema(input.model, EffectZod.toJsonSchema(item.parameters))
103
- tools[item.id] = tool({
104
- description: item.description,
105
- inputSchema: jsonSchema(schema),
106
- execute(args, options) {
107
- return bridge.promise(
108
- Effect.gen(function* () {
109
- const ctx = context(args, options)
110
- yield* plugin.trigger(
111
- "tool.execute.before",
112
- { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID },
113
- { args },
114
- )
115
- const result = yield* item.execute(args, ctx)
116
- const output = {
117
- ...result,
118
- attachments: result.attachments?.map((attachment) => ({
119
- ...attachment,
120
- id: PartID.ascending(),
121
- sessionID: ctx.sessionID,
122
- messageID: input.processor.message.id,
123
- })),
124
- }
125
- yield* plugin.trigger(
126
- "tool.execute.after",
127
- { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args },
128
- output,
129
- )
130
- if (options.abortSignal?.aborted) {
131
- yield* input.processor.completeToolCall(options.toolCallId, output)
132
- }
133
- return output
134
- }),
135
- )
136
- },
137
- })
138
- }
139
-
140
- for (const [key, item] of Object.entries(yield* mcp.tools())) {
141
- const execute = item.execute
142
- if (!execute) continue
143
- const schema = yield* Effect.promise(() => Promise.resolve(asSchema(item.inputSchema).jsonSchema))
144
- const transformed = ProviderTransform.schema(input.model, schema)
145
- item.inputSchema = jsonSchema(transformed)
146
- item.execute = (args, opts) =>
147
- bridge.promise(
148
- Effect.gen(function* () {
149
- const ctx = context(args, opts)
150
- yield* plugin.trigger(
151
- "tool.execute.before",
152
- { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId },
153
- { args },
154
- )
155
- const result: Awaited<ReturnType<NonNullable<typeof execute>>> = yield* Effect.gen(function* () {
156
- yield* ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] })
157
- return yield* Effect.promise(() => execute(args, opts))
158
- }).pipe(
159
- Effect.withSpan("Tool.execute", {
160
- attributes: {
161
- "tool.name": key,
162
- "tool.call_id": opts.toolCallId,
163
- "session.id": ctx.sessionID,
164
- "message.id": input.processor.message.id,
165
- },
166
- }),
167
- )
168
- yield* plugin.trigger(
169
- "tool.execute.after",
170
- { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId, args },
171
- result,
172
- )
173
-
174
- const textParts: string[] = []
175
- const attachments: Omit<MessageV2.FilePart, "id" | "sessionID" | "messageID">[] = []
176
- for (const contentItem of result.content) {
177
- if (contentItem.type === "text") textParts.push(contentItem.text)
178
- else if (contentItem.type === "image") {
179
- attachments.push({
180
- type: "file",
181
- mime: contentItem.mimeType,
182
- url: `data:${contentItem.mimeType};base64,${contentItem.data}`,
183
- })
184
- } else if (contentItem.type === "resource") {
185
- const { resource } = contentItem
186
- if (resource.text) textParts.push(resource.text)
187
- if (resource.blob) {
188
- attachments.push({
189
- type: "file",
190
- mime: resource.mimeType ?? "application/octet-stream",
191
- url: `data:${resource.mimeType ?? "application/octet-stream"};base64,${resource.blob}`,
192
- filename: resource.uri,
193
- })
194
- }
195
- }
196
- }
197
-
198
- const truncated = yield* truncate.output(textParts.join("\n\n"), {}, input.agent)
199
- const metadata = {
200
- ...result.metadata,
201
- truncated: truncated.truncated,
202
- ...(truncated.truncated && { outputPath: truncated.outputPath }),
203
- }
204
- const output = {
205
- title: "",
206
- metadata,
207
- output: truncated.content,
208
- attachments: attachments.map((attachment) => ({
209
- ...attachment,
210
- id: PartID.ascending(),
211
- sessionID: ctx.sessionID,
212
- messageID: input.processor.message.id,
213
- })),
214
- content: result.content,
215
- }
216
- if (opts.abortSignal?.aborted) {
217
- yield* input.processor.completeToolCall(opts.toolCallId, output)
218
- }
219
- return output
220
- }),
221
- )
222
- tools[key] = item
223
- }
224
-
225
- return tools
226
- })
227
-
228
- return { resolve }
229
- }),
230
- )
231
-
232
- export const defaultLayer = Layer.suspend(() =>
233
- layer.pipe(
234
- Layer.provide(ToolRegistry.defaultLayer),
235
- Layer.provide(MCP.defaultLayer),
236
- Layer.provide(Plugin.defaultLayer),
237
- Layer.provide(Permission.defaultLayer),
238
- Layer.provide(Truncate.defaultLayer),
239
- Layer.provide(Provider.defaultLayer),
240
- ),
241
- )
1
+ export * from "./core/resolve-tools"
@@ -1,149 +1 @@
1
- import type { NamedError } from "@saeeol/core/util/error"
2
- import { Cause, Clock, Duration, Effect, Schedule } from "effect"
3
- import { MessageV2 } from "./message-v2"
4
- import { isSaeeolError } from "@/saeeol/errors"
5
- import { SessionNetwork } from "./network"
6
- import { iife } from "@/util/iife"
7
-
8
- export type Err = ReturnType<NamedError["toObject"]>
9
-
10
- // This exported message is shared with the TUI upsell detector. Matching on a
11
- // literal error string kind of sucks, but it is the simplest for now.
12
- export const GO_UPSELL_MESSAGE = "Free usage exceeded, subscribe to Go https://saeeol.ai/go"
13
-
14
- export const RETRY_INITIAL_DELAY = 2000
15
- export const RETRY_BACKOFF_FACTOR = 2
16
- export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds
17
- export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout
18
-
19
- function cap(ms: number) {
20
- return Math.min(ms, RETRY_MAX_DELAY)
21
- }
22
-
23
- export function delay(attempt: number, error?: MessageV2.APIError) {
24
- if (error) {
25
- const headers = error.data.responseHeaders
26
- if (headers) {
27
- const retryAfterMs = headers["retry-after-ms"]
28
- if (retryAfterMs) {
29
- const parsedMs = Number.parseFloat(retryAfterMs)
30
- if (!Number.isNaN(parsedMs)) {
31
- return cap(parsedMs)
32
- }
33
- }
34
-
35
- const retryAfter = headers["retry-after"]
36
- if (retryAfter) {
37
- const parsedSeconds = Number.parseFloat(retryAfter)
38
- if (!Number.isNaN(parsedSeconds)) {
39
- // convert seconds to milliseconds
40
- return cap(Math.ceil(parsedSeconds * 1000))
41
- }
42
- // Try parsing as HTTP date format
43
- const parsed = Date.parse(retryAfter) - Date.now()
44
- if (!Number.isNaN(parsed) && parsed > 0) {
45
- return cap(Math.ceil(parsed))
46
- }
47
- }
48
-
49
- return cap(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1))
50
- }
51
- }
52
-
53
- return cap(Math.min(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1), RETRY_MAX_DELAY_NO_HEADERS))
54
- }
55
-
56
- export function retryable(error: Err) {
57
- // context overflow errors should not be retried
58
- if (MessageV2.ContextOverflowError.isInstance(error)) return undefined
59
- if (MessageV2.APIError.isInstance(error)) {
60
- const status = error.data.statusCode
61
- if (isSaeeolError(error)) return undefined
62
-
63
- // 5xx errors are transient server failures and should always be retried,
64
- // even when the provider SDK doesn't explicitly mark them as retryable.
65
- if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined
66
- // capped model is futile and the backoff loop cannot be broken by switching
67
- // models in the chat selector (the retry loop holds a stale model ref).
68
- if (error.data.responseBody?.includes("FreeUsageLimitError")) return undefined
69
- return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
70
- }
71
-
72
- // Check for rate limit patterns in plain text error messages
73
- const msg = error.data?.message
74
- if (typeof msg === "string") {
75
- const lower = msg.toLowerCase()
76
- if (
77
- lower.includes("rate increased too quickly") ||
78
- lower.includes("rate limit") ||
79
- lower.includes("too many requests")
80
- ) {
81
- return msg
82
- }
83
- }
84
-
85
- const json = iife(() => {
86
- try {
87
- if (typeof error.data?.message === "string") {
88
- const parsed = JSON.parse(error.data.message)
89
- return parsed
90
- }
91
-
92
- return JSON.parse(error.data.message)
93
- } catch {
94
- return undefined
95
- }
96
- })
97
- if (!json || typeof json !== "object") return undefined
98
- const code = typeof json.code === "string" ? json.code : ""
99
-
100
- if (json.type === "error" && json.error?.type === "too_many_requests") {
101
- return "Too Many Requests"
102
- }
103
- if (code.includes("exhausted") || code.includes("unavailable")) {
104
- return "Provider is overloaded"
105
- }
106
- if (json.type === "error" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) {
107
- return "Rate Limited"
108
- }
109
- return undefined
110
- }
111
-
112
- export function policy(opts: {
113
- parse: (error: unknown) => Err
114
- set: (input: { attempt: number; message: string; next: number }) => Effect.Effect<void>
115
- limit?: number
116
- offline?: (input: { error: unknown; message: string }) => Effect.Effect<"retry" | "blocked" | "aborted">
117
- }) {
118
- return Schedule.fromStepWithMetadata(
119
- Effect.succeed((meta: Schedule.InputMetadata<unknown>) => {
120
- if (opts.limit !== undefined && meta.attempt > opts.limit) {
121
- return Cause.done(meta.attempt)
122
- }
123
-
124
- const error = opts.parse(meta.input)
125
- const message = retryable(error)
126
- if (!message) return Cause.done(meta.attempt)
127
- return Effect.gen(function* () {
128
- if (opts.offline && SessionNetwork.disconnected(meta.input)) {
129
- const result = yield* opts.offline({
130
- error: meta.input,
131
- message: SessionNetwork.message(meta.input),
132
- })
133
- if (result !== "retry") {
134
- return yield* Cause.done(meta.attempt)
135
- }
136
- yield* opts.set({ attempt: 0, message: "Reconnected", next: Date.now() })
137
- return [0, Duration.zero] as [number, Duration.Duration]
138
- }
139
-
140
- const wait = delay(meta.attempt, MessageV2.APIError.isInstance(error) ? error : undefined)
141
- const now = yield* Clock.currentTimeMillis
142
- yield* opts.set({ attempt: meta.attempt, message, next: now + wait })
143
- return [meta.attempt, Duration.millis(wait)] as [number, Duration.Duration]
144
- })
145
- }),
146
- )
147
- }
148
-
149
- export * as SessionRetry from "./retry"
1
+ export * from "./core/retry"