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.
- package/package.json +14 -14
- package/src/session/compaction-helpers.ts +1 -169
- package/src/session/compaction.ts +1 -712
- package/src/session/core/compaction/compaction-helpers.ts +169 -0
- package/src/session/core/compaction/compaction.ts +712 -0
- package/src/session/core/compaction/overflow.ts +28 -0
- package/src/session/core/instruction.ts +234 -0
- package/src/session/core/llm.ts +504 -0
- package/src/session/core/network.ts +392 -0
- package/src/session/core/processor.ts +731 -0
- package/src/session/core/projectors.ts +139 -0
- package/src/session/core/resolve-tools.ts +241 -0
- package/src/session/core/retry.ts +149 -0
- package/src/session/core/revert.ts +173 -0
- package/src/session/core/run-state.ts +110 -0
- package/src/session/core/schema.ts +35 -0
- package/src/session/core/session-types.ts +160 -0
- package/src/session/core/session.sql.ts +124 -0
- package/src/session/core/session.ts +948 -0
- package/src/session/core/shell-exec.ts +205 -0
- package/src/session/core/status.ts +100 -0
- package/src/session/core/subtask.ts +268 -0
- package/src/session/core/summary.ts +173 -0
- package/src/session/core/system.ts +114 -0
- package/src/session/core/todo.ts +86 -0
- package/src/session/core/user-part.ts +293 -0
- package/src/session/instruction.ts +1 -234
- package/src/session/llm.ts +1 -504
- package/src/session/message/message-errors.ts +83 -0
- package/src/session/message/message-parts.ts +89 -0
- package/src/session/message/message-query.ts +107 -0
- package/src/session/message/message-transform.ts +156 -0
- package/src/session/message/message-types.ts +68 -0
- package/src/session/message/message-v2.ts +73 -0
- package/src/session/message/message.ts +192 -0
- package/src/session/message-errors.ts +1 -83
- package/src/session/message-parts.ts +1 -89
- package/src/session/message-query.ts +1 -107
- package/src/session/message-transform.ts +1 -156
- package/src/session/message-types.ts +1 -68
- package/src/session/message-v2.ts +1 -73
- package/src/session/message.ts +1 -192
- package/src/session/network.ts +1 -392
- package/src/session/overflow.ts +1 -28
- package/src/session/processor.ts +1 -731
- package/src/session/projectors.ts +2 -139
- package/src/session/prompt/prompt-command.ts +93 -0
- package/src/session/prompt/prompt-loop.ts +299 -0
- package/src/session/prompt/prompt-model.ts +44 -0
- package/src/session/prompt/prompt-reminders.ts +120 -0
- package/src/session/prompt/prompt-resolve.ts +42 -0
- package/src/session/prompt/prompt-schemas.ts +128 -0
- package/src/session/prompt/prompt-title.ts +55 -0
- package/src/session/prompt/prompt-types.ts +47 -0
- package/src/session/prompt/prompt-user-msg.ts +80 -0
- package/src/session/prompt/prompt.ts +211 -0
- package/src/session/prompt-command.ts +1 -93
- package/src/session/prompt-loop.ts +1 -299
- package/src/session/prompt-model.ts +1 -44
- package/src/session/prompt-reminders.ts +1 -120
- package/src/session/prompt-resolve.ts +1 -42
- package/src/session/prompt-schemas.ts +1 -128
- package/src/session/prompt-title.ts +1 -55
- package/src/session/prompt-types.ts +1 -47
- package/src/session/prompt-user-msg.ts +1 -80
- package/src/session/prompt.ts +1 -211
- package/src/session/resolve-tools.ts +1 -241
- package/src/session/retry.ts +1 -149
- package/src/session/revert.ts +1 -173
- package/src/session/run-state.ts +1 -110
- package/src/session/schema.ts +1 -35
- package/src/session/session-types.ts +1 -160
- package/src/session/session.sql.ts +1 -124
- package/src/session/session.ts +1 -948
- package/src/session/shell-exec.ts +1 -205
- package/src/session/status.ts +1 -100
- package/src/session/subtask.ts +1 -268
- package/src/session/summary.ts +1 -173
- package/src/session/system.ts +1 -114
- package/src/session/todo.ts +1 -86
- package/src/session/user-part.ts +1 -293
package/src/session/prompt.ts
CHANGED
|
@@ -1,211 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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"
|
package/src/session/retry.ts
CHANGED
|
@@ -1,149 +1 @@
|
|
|
1
|
-
|
|
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"
|