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.
- package/package.json +14 -14
- package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
- package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
- package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
- package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
- package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
- package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
- package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
- package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
- package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
- package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
- package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
- package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
- package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
- package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
- package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
- package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
- package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
- package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
- package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
- package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
- package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
- package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
- package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
- 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/tool/apply_patch.ts +1 -334
- package/src/tool/bash.ts +1 -656
- package/src/tool/core/external-directory.ts +55 -0
- package/src/tool/core/invalid.ts +21 -0
- package/src/tool/core/recall.ts +164 -0
- package/src/tool/core/recall.txt +12 -0
- package/src/tool/core/schema.ts +16 -0
- package/src/tool/core/tool.ts +162 -0
- package/src/tool/core/truncate.ts +160 -0
- package/src/tool/core/truncation-dir.ts +4 -0
- package/src/tool/diagnostics.ts +1 -20
- package/src/tool/edit-replacers.ts +1 -288
- package/src/tool/edit-utils.ts +1 -86
- package/src/tool/edit.ts +1 -262
- package/src/tool/external-directory.ts +1 -55
- package/src/tool/file/apply_patch.ts +334 -0
- package/src/tool/file/apply_patch.txt +33 -0
- package/src/tool/file/bash.ts +656 -0
- package/src/tool/file/bash.txt +119 -0
- package/src/tool/file/edit-replacers.ts +288 -0
- package/src/tool/file/edit-utils.ts +86 -0
- package/src/tool/file/edit.ts +262 -0
- package/src/tool/file/edit.txt +10 -0
- package/src/tool/file/read.ts +389 -0
- package/src/tool/file/read.txt +14 -0
- package/src/tool/file/write.ts +114 -0
- package/src/tool/file/write.txt +8 -0
- package/src/tool/glob.ts +1 -115
- package/src/tool/grep.ts +1 -151
- package/src/tool/integration/diagnostics.ts +20 -0
- package/src/tool/integration/lsp.ts +113 -0
- package/src/tool/integration/lsp.txt +24 -0
- package/src/tool/integration/mcp-exa.ts +73 -0
- package/src/tool/integration/package.ts +168 -0
- package/src/tool/integration/registry.ts +375 -0
- package/src/tool/invalid.ts +1 -21
- package/src/tool/lsp.ts +1 -113
- package/src/tool/mcp-exa.ts +1 -73
- package/src/tool/package.ts +1 -168
- package/src/tool/plan.ts +1 -30
- package/src/tool/question.ts +1 -52
- package/src/tool/read.ts +1 -389
- package/src/tool/recall.ts +1 -164
- package/src/tool/registry.ts +1 -375
- package/src/tool/schema.ts +1 -16
- package/src/tool/search/glob.ts +115 -0
- package/src/tool/search/glob.txt +6 -0
- package/src/tool/search/grep.ts +151 -0
- package/src/tool/search/grep.txt +8 -0
- package/src/tool/search/warpgrep.ts +107 -0
- package/src/tool/search/warpgrep.txt +10 -0
- package/src/tool/search/webfetch.ts +202 -0
- package/src/tool/search/webfetch.txt +13 -0
- package/src/tool/search/websearch.ts +71 -0
- package/src/tool/search/websearch.txt +14 -0
- package/src/tool/skill.ts +1 -91
- package/src/tool/task.ts +1 -197
- package/src/tool/todo.ts +1 -62
- package/src/tool/tool.ts +1 -162
- package/src/tool/truncate.ts +1 -160
- package/src/tool/truncation-dir.ts +1 -4
- package/src/tool/warpgrep.ts +1 -107
- package/src/tool/webfetch.ts +1 -202
- package/src/tool/websearch.ts +1 -71
- package/src/tool/workflow/plan-enter.txt +14 -0
- package/src/tool/workflow/plan-exit.txt +13 -0
- package/src/tool/workflow/plan.ts +30 -0
- package/src/tool/workflow/question.ts +52 -0
- package/src/tool/workflow/question.txt +11 -0
- package/src/tool/workflow/skill.ts +91 -0
- package/src/tool/workflow/skill.txt +5 -0
- package/src/tool/workflow/task.ts +197 -0
- package/src/tool/workflow/task.txt +57 -0
- package/src/tool/workflow/todo.ts +62 -0
- package/src/tool/workflow/todowrite.txt +167 -0
- package/src/tool/write.ts +1 -114
package/src/session/llm.ts
CHANGED
|
@@ -1,504 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import * as Log from "@saeeol/core/util/log"
|
|
3
|
-
import { Context, Effect, Layer, Record } from "effect"
|
|
4
|
-
import * as Stream from "effect/Stream"
|
|
5
|
-
import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai"
|
|
6
|
-
import { mergeDeep } from "remeda"
|
|
7
|
-
import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
|
|
8
|
-
import { ProviderTransform } from "@/provider/transform"
|
|
9
|
-
import { Config } from "@/config/config"
|
|
10
|
-
import { InstanceState } from "@/effect/instance-state"
|
|
11
|
-
import type { Agent } from "@/agent/agent"
|
|
12
|
-
import type { MessageV2 } from "./message-v2"
|
|
13
|
-
import { Plugin } from "@/plugin"
|
|
14
|
-
import { SystemPrompt } from "./system"
|
|
15
|
-
import { Flag } from "@saeeol/core/flag/flag"
|
|
16
|
-
import { Permission } from "@/permission"
|
|
17
|
-
import { PermissionID } from "@/permission/schema"
|
|
18
|
-
import { Bus } from "@/bus"
|
|
19
|
-
import { Wildcard } from "@/util/wildcard"
|
|
20
|
-
import { SessionID } from "@/session/schema"
|
|
21
|
-
import { Auth } from "@/auth"
|
|
22
|
-
import { DEFAULT_HEADERS } from "@/saeeol/const"
|
|
23
|
-
import { getSaeeolProjectId } from "@/saeeol/project-id"
|
|
24
|
-
import {
|
|
25
|
-
HEADER_FEATURE,
|
|
26
|
-
HEADER_PARENT_TASKID,
|
|
27
|
-
HEADER_PROJECTID,
|
|
28
|
-
HEADER_MACHINEID,
|
|
29
|
-
HEADER_TASKID,
|
|
30
|
-
} from "@saeeol/gateway"
|
|
31
|
-
import { Identity } from "@saeeol/telemetry"
|
|
32
|
-
import { makeRuntime } from "@/effect/run-service"
|
|
33
|
-
import { SaeeolSession } from "@/saeeol/session"
|
|
34
|
-
import { SaeeolLLM } from "@/saeeol/session/llm"
|
|
35
|
-
import { Installation } from "@/installation"
|
|
36
|
-
import { InstallationVersion } from "@saeeol/core/installation/version"
|
|
37
|
-
import { EffectBridge } from "@/effect/bridge"
|
|
38
|
-
import * as Option from "effect/Option"
|
|
39
|
-
import * as OtelTracer from "@effect/opentelemetry/Tracer"
|
|
40
|
-
|
|
41
|
-
const log = Log.create({ service: "llm" })
|
|
42
|
-
export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX
|
|
43
|
-
type Result = Awaited<ReturnType<typeof streamText>>
|
|
44
|
-
|
|
45
|
-
// Avoid re-instantiating remeda's deep merge types in this hot LLM path; the runtime behavior is still mergeDeep.
|
|
46
|
-
const mergeOptions = (target: Record<string, any>, source: Record<string, any> | undefined): Record<string, any> =>
|
|
47
|
-
mergeDeep(target, source ?? {}) as Record<string, any>
|
|
48
|
-
|
|
49
|
-
export type StreamInput = {
|
|
50
|
-
user: MessageV2.User
|
|
51
|
-
sessionID: string
|
|
52
|
-
parentSessionID?: string
|
|
53
|
-
model: Provider.Model
|
|
54
|
-
agent: Agent.Info
|
|
55
|
-
permission?: Permission.Ruleset
|
|
56
|
-
system: string[]
|
|
57
|
-
messages: ModelMessage[]
|
|
58
|
-
small?: boolean
|
|
59
|
-
tools: Record<string, Tool>
|
|
60
|
-
retries?: number
|
|
61
|
-
toolChoice?: "auto" | "required" | "none"
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export type StreamRequest = StreamInput & {
|
|
65
|
-
abort: AbortSignal
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export type Event = Result["fullStream"] extends AsyncIterable<infer T> ? T : never
|
|
69
|
-
|
|
70
|
-
export interface Interface {
|
|
71
|
-
readonly stream: (input: StreamInput) => Stream.Stream<Event, unknown>
|
|
72
|
-
readonly raw: (input: StreamRequest) => Effect.Effect<Result>
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export class Service extends Context.Service<Service, Interface>()("@saeeol/LLM") {}
|
|
76
|
-
|
|
77
|
-
const live: Layer.Layer<
|
|
78
|
-
Service,
|
|
79
|
-
never,
|
|
80
|
-
Auth.Service | Config.Service | Provider.Service | Plugin.Service | Permission.Service
|
|
81
|
-
> = Layer.effect(
|
|
82
|
-
Service,
|
|
83
|
-
Effect.gen(function* () {
|
|
84
|
-
const auth = yield* Auth.Service
|
|
85
|
-
const config = yield* Config.Service
|
|
86
|
-
const provider = yield* Provider.Service
|
|
87
|
-
const plugin = yield* Plugin.Service
|
|
88
|
-
const perm = yield* Permission.Service
|
|
89
|
-
|
|
90
|
-
const run = Effect.fn("LLM.run")(function* (input: StreamRequest) {
|
|
91
|
-
const l = log
|
|
92
|
-
.clone()
|
|
93
|
-
.tag("providerID", input.model.providerID)
|
|
94
|
-
.tag("modelID", input.model.id)
|
|
95
|
-
.tag("session.id", input.sessionID)
|
|
96
|
-
.tag("small", (input.small ?? false).toString())
|
|
97
|
-
.tag("agent", input.agent.name)
|
|
98
|
-
.tag("mode", input.agent.mode)
|
|
99
|
-
l.info("stream", {
|
|
100
|
-
modelID: input.model.id,
|
|
101
|
-
providerID: input.model.providerID,
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
const [language, cfg, item, info] = yield* Effect.all(
|
|
105
|
-
[
|
|
106
|
-
provider.getLanguage(input.model),
|
|
107
|
-
config.get(),
|
|
108
|
-
provider.getProvider(input.model.providerID),
|
|
109
|
-
auth.get(input.model.providerID),
|
|
110
|
-
],
|
|
111
|
-
{ concurrency: "unbounded" },
|
|
112
|
-
)
|
|
113
|
-
const attr = SaeeolSession.attribution(input.sessionID)
|
|
114
|
-
|
|
115
|
-
// TODO: move this to a proper hook
|
|
116
|
-
const isOpenaiOauth = item.id === "openai" && info?.type === "oauth"
|
|
117
|
-
|
|
118
|
-
const system: string[] = []
|
|
119
|
-
system.push(
|
|
120
|
-
[
|
|
121
|
-
...(isOpenaiOauth ? [] : [SystemPrompt.soul()]),
|
|
122
|
-
// use agent prompt otherwise provider prompt
|
|
123
|
-
...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),
|
|
124
|
-
// any custom prompt passed into this call
|
|
125
|
-
...input.system,
|
|
126
|
-
// any custom prompt from last user message
|
|
127
|
-
...(input.user.system ? [input.user.system] : []),
|
|
128
|
-
]
|
|
129
|
-
.filter((x) => x)
|
|
130
|
-
.join("\n"),
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
const header = system[0]
|
|
134
|
-
yield* plugin.trigger(
|
|
135
|
-
"experimental.chat.system.transform",
|
|
136
|
-
{ sessionID: input.sessionID, model: input.model },
|
|
137
|
-
{ system },
|
|
138
|
-
)
|
|
139
|
-
// rejoin to maintain 2-part structure for caching if header unchanged
|
|
140
|
-
if (system.length > 2 && system[0] === header) {
|
|
141
|
-
const rest = system.slice(1)
|
|
142
|
-
system.length = 0
|
|
143
|
-
system.push(header, rest.join("\n"))
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const variant =
|
|
147
|
-
!input.small && input.model.variants && input.user.model.variant
|
|
148
|
-
? input.model.variants[input.user.model.variant]
|
|
149
|
-
: {}
|
|
150
|
-
const base = input.small
|
|
151
|
-
? ProviderTransform.smallOptions(input.model)
|
|
152
|
-
: ProviderTransform.options({
|
|
153
|
-
model: input.model,
|
|
154
|
-
sessionID: input.sessionID,
|
|
155
|
-
providerOptions: item.options,
|
|
156
|
-
})
|
|
157
|
-
const options = mergeOptions(mergeOptions(mergeOptions(base, input.model.options), input.agent.options), variant)
|
|
158
|
-
if (isOpenaiOauth) {
|
|
159
|
-
options.instructions = SystemPrompt.soul() + "\n" + system.join("\n")
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const isWorkflow = language instanceof GitLabWorkflowLanguageModel
|
|
163
|
-
const messages = isOpenaiOauth
|
|
164
|
-
? input.messages
|
|
165
|
-
: isWorkflow
|
|
166
|
-
? input.messages
|
|
167
|
-
: [
|
|
168
|
-
...system.map(
|
|
169
|
-
(x): ModelMessage => ({
|
|
170
|
-
role: "system",
|
|
171
|
-
content: x,
|
|
172
|
-
}),
|
|
173
|
-
),
|
|
174
|
-
...input.messages,
|
|
175
|
-
]
|
|
176
|
-
|
|
177
|
-
const params = yield* plugin.trigger(
|
|
178
|
-
"chat.params",
|
|
179
|
-
{
|
|
180
|
-
sessionID: input.sessionID,
|
|
181
|
-
agent: input.agent.name,
|
|
182
|
-
model: input.model,
|
|
183
|
-
provider: item,
|
|
184
|
-
message: input.user,
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
temperature: input.model.capabilities.temperature
|
|
188
|
-
? (input.agent.temperature ?? ProviderTransform.temperature(input.model))
|
|
189
|
-
: undefined,
|
|
190
|
-
topP: input.agent.topP ?? ProviderTransform.topP(input.model),
|
|
191
|
-
topK: ProviderTransform.topK(input.model),
|
|
192
|
-
// rejects `max_tokens`; OpenAI requires `max_completion_tokens` and the compatible
|
|
193
|
-
// SDK cannot rename the field, so drop the cap and let the upstream default apply.
|
|
194
|
-
maxOutputTokens:
|
|
195
|
-
input.model.api.npm === "@ai-sdk/openai-compatible" && input.model.api.id.toLowerCase().includes("gpt-5")
|
|
196
|
-
? undefined
|
|
197
|
-
: ProviderTransform.maxOutputTokens(input.model),
|
|
198
|
-
options,
|
|
199
|
-
},
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
const { headers } = yield* plugin.trigger(
|
|
203
|
-
"chat.headers",
|
|
204
|
-
{
|
|
205
|
-
sessionID: input.sessionID,
|
|
206
|
-
agent: input.agent.name,
|
|
207
|
-
model: input.model,
|
|
208
|
-
provider: item,
|
|
209
|
-
message: input.user,
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
headers: {},
|
|
213
|
-
},
|
|
214
|
-
)
|
|
215
|
-
const isSaeeol = input.model.api.npm === "@saeeol/gateway"
|
|
216
|
-
const saeeolProjectId = yield* isSaeeol
|
|
217
|
-
? Effect.promise(() => getSaeeolProjectId().catch(() => undefined))
|
|
218
|
-
: Effect.succeed(undefined)
|
|
219
|
-
const machineId = yield* isSaeeol
|
|
220
|
-
? Effect.promise(() => Identity.getMachineId().catch(() => undefined))
|
|
221
|
-
: Effect.succeed(undefined)
|
|
222
|
-
|
|
223
|
-
const tools = resolveTools(input)
|
|
224
|
-
params.maxOutputTokens = SaeeolLLM.capOutputTokens({
|
|
225
|
-
model: input.model,
|
|
226
|
-
messages,
|
|
227
|
-
tools,
|
|
228
|
-
configured: params.maxOutputTokens,
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
// LiteLLM and some Anthropic proxies require the tools parameter to be present
|
|
232
|
-
// when message history contains tool calls, even if no tools are being used.
|
|
233
|
-
// Add a dummy tool that is never called to satisfy this validation.
|
|
234
|
-
// This is enabled for:
|
|
235
|
-
// 1. Providers with "litellm" in their ID or API ID (auto-detected)
|
|
236
|
-
// 2. Providers with explicit "litellmProxy: true" option (opt-in for custom gateways)
|
|
237
|
-
const isLiteLLMProxy =
|
|
238
|
-
item.options?.["litellmProxy"] === true ||
|
|
239
|
-
input.model.providerID.toLowerCase().includes("litellm") ||
|
|
240
|
-
input.model.api.id.toLowerCase().includes("litellm")
|
|
241
|
-
|
|
242
|
-
// LiteLLM/Bedrock rejects requests where the message history contains tool
|
|
243
|
-
// calls but no tools param is present. When there are no active tools (e.g.
|
|
244
|
-
// during compaction), inject a stub tool to satisfy the validation requirement.
|
|
245
|
-
// The stub description explicitly tells the model not to call it.
|
|
246
|
-
if (
|
|
247
|
-
(isLiteLLMProxy || input.model.providerID.includes("github-copilot")) &&
|
|
248
|
-
Object.keys(tools).length === 0 &&
|
|
249
|
-
hasToolCalls(input.messages)
|
|
250
|
-
) {
|
|
251
|
-
tools["_noop"] = tool({
|
|
252
|
-
description: "Do not call this tool. It exists only for API compatibility and must never be invoked.",
|
|
253
|
-
inputSchema: jsonSchema({
|
|
254
|
-
type: "object",
|
|
255
|
-
properties: {
|
|
256
|
-
reason: { type: "string", description: "Unused" },
|
|
257
|
-
},
|
|
258
|
-
}),
|
|
259
|
-
execute: async () => ({ output: "", title: "", metadata: {} }),
|
|
260
|
-
})
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Wire up toolExecutor for DWS workflow models so that tool calls
|
|
264
|
-
// from the workflow service are executed via saeeol's tool system
|
|
265
|
-
// and results sent back over the WebSocket.
|
|
266
|
-
if (language instanceof GitLabWorkflowLanguageModel) {
|
|
267
|
-
const workflowModel = language as GitLabWorkflowLanguageModel & {
|
|
268
|
-
sessionID?: string
|
|
269
|
-
sessionPreapprovedTools?: string[]
|
|
270
|
-
approvalHandler?: (approvalTools: { name: string; args: string }[]) => Promise<{ approved: boolean }>
|
|
271
|
-
}
|
|
272
|
-
workflowModel.sessionID = input.sessionID
|
|
273
|
-
workflowModel.systemPrompt = system.join("\n")
|
|
274
|
-
workflowModel.toolExecutor = async (toolName, argsJson, _requestID) => {
|
|
275
|
-
const t = tools[toolName]
|
|
276
|
-
if (!t || !t.execute) {
|
|
277
|
-
return { result: "", error: `Unknown tool: ${toolName}` }
|
|
278
|
-
}
|
|
279
|
-
try {
|
|
280
|
-
const result = await t.execute!(JSON.parse(argsJson), {
|
|
281
|
-
toolCallId: _requestID,
|
|
282
|
-
messages: input.messages,
|
|
283
|
-
abortSignal: input.abort,
|
|
284
|
-
})
|
|
285
|
-
const output = typeof result === "string" ? result : (result?.output ?? JSON.stringify(result))
|
|
286
|
-
return {
|
|
287
|
-
result: output,
|
|
288
|
-
metadata: typeof result === "object" ? result?.metadata : undefined,
|
|
289
|
-
title: typeof result === "object" ? result?.title : undefined,
|
|
290
|
-
}
|
|
291
|
-
} catch (e: any) {
|
|
292
|
-
return { result: "", error: e.message ?? String(e) }
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const ruleset = Permission.merge(input.agent.permission ?? [], input.permission ?? [])
|
|
297
|
-
workflowModel.sessionPreapprovedTools = Object.keys(tools).filter((name) => {
|
|
298
|
-
const match = ruleset.findLast((rule) => Wildcard.match(name, rule.permission))
|
|
299
|
-
return !match || match.action !== "ask"
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
const bridge = yield* EffectBridge.make()
|
|
303
|
-
const approvedToolsForSession = new Set<string>()
|
|
304
|
-
workflowModel.approvalHandler = InstanceState.bind(async (approvalTools) => {
|
|
305
|
-
const uniqueNames = [...new Set(approvalTools.map((t: { name: string }) => t.name))] as string[]
|
|
306
|
-
// Auto-approve tools that were already approved in this session
|
|
307
|
-
// (prevents infinite approval loops for server-side MCP tools)
|
|
308
|
-
if (uniqueNames.every((name) => approvedToolsForSession.has(name))) {
|
|
309
|
-
return { approved: true }
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const id = PermissionID.ascending()
|
|
313
|
-
let unsub: (() => void) | undefined
|
|
314
|
-
try {
|
|
315
|
-
unsub = Bus.subscribe(Permission.Event.Replied, (evt) => {
|
|
316
|
-
if (evt.properties.requestID === id) void evt.properties.reply
|
|
317
|
-
})
|
|
318
|
-
const toolPatterns = approvalTools.map((t: { name: string; args: string }) => {
|
|
319
|
-
try {
|
|
320
|
-
const parsed = JSON.parse(t.args) as Record<string, unknown>
|
|
321
|
-
const title = (parsed?.title ?? parsed?.name ?? "") as string
|
|
322
|
-
return title ? `${t.name}: ${title}` : t.name
|
|
323
|
-
} catch {
|
|
324
|
-
return t.name
|
|
325
|
-
}
|
|
326
|
-
})
|
|
327
|
-
const uniquePatterns = [...new Set(toolPatterns)] as string[]
|
|
328
|
-
await bridge.promise(
|
|
329
|
-
perm.ask({
|
|
330
|
-
id,
|
|
331
|
-
sessionID: SessionID.make(input.sessionID),
|
|
332
|
-
permission: "workflow_tool_approval",
|
|
333
|
-
patterns: uniquePatterns,
|
|
334
|
-
metadata: { tools: approvalTools },
|
|
335
|
-
always: uniquePatterns,
|
|
336
|
-
ruleset: [],
|
|
337
|
-
}),
|
|
338
|
-
)
|
|
339
|
-
for (const name of uniqueNames) approvedToolsForSession.add(name)
|
|
340
|
-
workflowModel.sessionPreapprovedTools = [...(workflowModel.sessionPreapprovedTools ?? []), ...uniqueNames]
|
|
341
|
-
return { approved: true }
|
|
342
|
-
} catch {
|
|
343
|
-
return { approved: false }
|
|
344
|
-
} finally {
|
|
345
|
-
unsub?.()
|
|
346
|
-
}
|
|
347
|
-
})
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const tracer = cfg.experimental?.openTelemetry
|
|
351
|
-
? Option.getOrUndefined(yield* Effect.serviceOption(OtelTracer.OtelTracer))
|
|
352
|
-
: undefined
|
|
353
|
-
const telemetryTracer = tracer
|
|
354
|
-
? new Proxy(tracer, {
|
|
355
|
-
get(target, prop, receiver) {
|
|
356
|
-
if (prop !== "startSpan") return Reflect.get(target, prop, receiver)
|
|
357
|
-
return (...args: Parameters<typeof target.startSpan>) => {
|
|
358
|
-
const span = target.startSpan(...args)
|
|
359
|
-
span.setAttribute("session.id", input.sessionID)
|
|
360
|
-
return span
|
|
361
|
-
}
|
|
362
|
-
},
|
|
363
|
-
})
|
|
364
|
-
: undefined
|
|
365
|
-
|
|
366
|
-
const saeeolProjectID = input.model.providerID.startsWith("saeeol")
|
|
367
|
-
? (yield* InstanceState.context).project.id
|
|
368
|
-
: undefined
|
|
369
|
-
|
|
370
|
-
return streamText({
|
|
371
|
-
onError(error) {
|
|
372
|
-
l.error("stream error", {
|
|
373
|
-
error,
|
|
374
|
-
})
|
|
375
|
-
},
|
|
376
|
-
async experimental_repairToolCall(failed) {
|
|
377
|
-
const lower = failed.toolCall.toolName.toLowerCase()
|
|
378
|
-
if (lower !== failed.toolCall.toolName && tools[lower]) {
|
|
379
|
-
l.info("repairing tool call", {
|
|
380
|
-
tool: failed.toolCall.toolName,
|
|
381
|
-
repaired: lower,
|
|
382
|
-
})
|
|
383
|
-
return {
|
|
384
|
-
...failed.toolCall,
|
|
385
|
-
toolName: lower,
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
return {
|
|
389
|
-
...failed.toolCall,
|
|
390
|
-
input: JSON.stringify({
|
|
391
|
-
tool: failed.toolCall.toolName,
|
|
392
|
-
error: failed.error.message,
|
|
393
|
-
}),
|
|
394
|
-
toolName: "invalid",
|
|
395
|
-
}
|
|
396
|
-
},
|
|
397
|
-
temperature: params.temperature,
|
|
398
|
-
topP: params.topP,
|
|
399
|
-
topK: params.topK,
|
|
400
|
-
providerOptions: ProviderTransform.providerOptions(input.model, params.options),
|
|
401
|
-
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
|
|
402
|
-
tools,
|
|
403
|
-
toolChoice: input.toolChoice,
|
|
404
|
-
maxOutputTokens: params.maxOutputTokens,
|
|
405
|
-
abortSignal: input.abort,
|
|
406
|
-
headers: {
|
|
407
|
-
...(input.model.providerID.startsWith("saeeol")
|
|
408
|
-
? {
|
|
409
|
-
"x-saeeol-project": saeeolProjectID,
|
|
410
|
-
"x-saeeol-session": input.sessionID,
|
|
411
|
-
"x-saeeol-request": input.user.id,
|
|
412
|
-
"x-saeeol-client": Flag.SAEEOL_CLIENT,
|
|
413
|
-
}
|
|
414
|
-
: {
|
|
415
|
-
"x-session-affinity": input.sessionID,
|
|
416
|
-
...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}),
|
|
417
|
-
"User-Agent": `saeeol/${InstallationVersion}`,
|
|
418
|
-
...(input.model.providerID !== "anthropic" ? DEFAULT_HEADERS : undefined),
|
|
419
|
-
}),
|
|
420
|
-
...(isSaeeol && input.agent.name ? { "x-saeeol-mode": input.agent.name.toLowerCase() } : {}),
|
|
421
|
-
...(isSaeeol && saeeolProjectId ? { [HEADER_PROJECTID]: saeeolProjectId } : {}),
|
|
422
|
-
...(isSaeeol && machineId ? { [HEADER_MACHINEID]: machineId } : {}),
|
|
423
|
-
...(isSaeeol ? { [HEADER_TASKID]: input.sessionID } : {}),
|
|
424
|
-
...(isSaeeol && input.parentSessionID ? { [HEADER_PARENT_TASKID]: input.parentSessionID } : {}),
|
|
425
|
-
...(isSaeeol && attr.feature ? { [HEADER_FEATURE]: attr.feature } : {}),
|
|
426
|
-
...input.model.headers,
|
|
427
|
-
...headers,
|
|
428
|
-
},
|
|
429
|
-
maxRetries: input.retries ?? 0,
|
|
430
|
-
messages,
|
|
431
|
-
model: wrapLanguageModel({
|
|
432
|
-
model: language,
|
|
433
|
-
middleware: [
|
|
434
|
-
{
|
|
435
|
-
specificationVersion: "v3" as const,
|
|
436
|
-
async transformParams(args) {
|
|
437
|
-
if (args.type === "stream") {
|
|
438
|
-
// @ts-expect-error
|
|
439
|
-
args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options)
|
|
440
|
-
}
|
|
441
|
-
return args.params
|
|
442
|
-
},
|
|
443
|
-
},
|
|
444
|
-
],
|
|
445
|
-
}),
|
|
446
|
-
experimental_telemetry: { isEnabled: false },
|
|
447
|
-
})
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
const stream: Interface["stream"] = (input) =>
|
|
451
|
-
Stream.scoped(
|
|
452
|
-
Stream.unwrap(
|
|
453
|
-
Effect.gen(function* () {
|
|
454
|
-
const ctrl = yield* Effect.acquireRelease(
|
|
455
|
-
Effect.sync(() => new AbortController()),
|
|
456
|
-
(ctrl) => Effect.sync(() => ctrl.abort()),
|
|
457
|
-
)
|
|
458
|
-
|
|
459
|
-
const result = yield* run({ ...input, abort: ctrl.signal })
|
|
460
|
-
|
|
461
|
-
return Stream.fromAsyncIterable(result.fullStream, (e) => (e instanceof Error ? e : new Error(String(e))))
|
|
462
|
-
}),
|
|
463
|
-
),
|
|
464
|
-
)
|
|
465
|
-
return Service.of({ stream, raw: (input) => run(input).pipe(Effect.orDie) })
|
|
466
|
-
}),
|
|
467
|
-
)
|
|
468
|
-
|
|
469
|
-
export const layer = live.pipe(Layer.provide(Permission.defaultLayer))
|
|
470
|
-
|
|
471
|
-
export const defaultLayer = Layer.suspend(() =>
|
|
472
|
-
layer.pipe(
|
|
473
|
-
Layer.provide(Auth.defaultLayer),
|
|
474
|
-
Layer.provide(Config.defaultLayer),
|
|
475
|
-
Layer.provide(Provider.defaultLayer),
|
|
476
|
-
Layer.provide(Plugin.defaultLayer),
|
|
477
|
-
),
|
|
478
|
-
)
|
|
479
|
-
const runtime = makeRuntime(Service, defaultLayer)
|
|
480
|
-
export async function stream(input: StreamRequest) {
|
|
481
|
-
return runtime.runPromise((svc) => svc.raw(input), { signal: input.abort })
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "permission" | "user">) {
|
|
485
|
-
const disabled = Permission.disabled(
|
|
486
|
-
Object.keys(input.tools),
|
|
487
|
-
Permission.merge(input.agent.permission, input.permission ?? []),
|
|
488
|
-
)
|
|
489
|
-
return Record.filter(input.tools, (_, k) => input.user.tools?.[k] !== false && !disabled.has(k))
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Check if messages contain any tool-call content
|
|
493
|
-
// Used to determine if a dummy tool should be added for LiteLLM proxy compatibility
|
|
494
|
-
export function hasToolCalls(messages: ModelMessage[]): boolean {
|
|
495
|
-
for (const msg of messages) {
|
|
496
|
-
if (!Array.isArray(msg.content)) continue
|
|
497
|
-
for (const part of msg.content) {
|
|
498
|
-
if (part.type === "tool-call" || part.type === "tool-result") return true
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return false
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
export * as LLM from "./llm"
|
|
1
|
+
export * from "./core/llm"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** 에러 타입 + fromError + OutputFormat — message-v2.ts에서 분리 */
|
|
2
|
+
|
|
3
|
+
import { APICallError, LoadAPIKeyError } from "ai"
|
|
4
|
+
import { NamedError } from "@saeeol/core/util/error"
|
|
5
|
+
import * as ProviderError from "@/provider/error"
|
|
6
|
+
import { SessionNetwork } from "../core/network"
|
|
7
|
+
import { CodexAuthExpiredError } from "@/saeeol/provider/codex-refresh"
|
|
8
|
+
import { Effect, Schema } from "effect"
|
|
9
|
+
import { zod } from "@/util/effect-zod"
|
|
10
|
+
import { NonNegativeInt } from "@/util/schema"
|
|
11
|
+
import { namedSchemaError } from "@/util/named-schema-error"
|
|
12
|
+
import { errorMessage } from "@/util/error"
|
|
13
|
+
import { ProviderID } from "@/provider/schema"
|
|
14
|
+
import type { Assistant } from "./message-types"
|
|
15
|
+
|
|
16
|
+
interface FetchDecompressionError extends Error { code: "ZlibError"; errno: number; path: string }
|
|
17
|
+
|
|
18
|
+
export const OutputLengthError = namedSchemaError("MessageOutputLengthError", {})
|
|
19
|
+
export const AbortedError = namedSchemaError("MessageAbortedError", { message: Schema.String })
|
|
20
|
+
export const StructuredOutputError = namedSchemaError("StructuredOutputError", { message: Schema.String, retries: NonNegativeInt })
|
|
21
|
+
export const AuthError = namedSchemaError("ProviderAuthError", { providerID: Schema.String, message: Schema.String })
|
|
22
|
+
export const APIError = namedSchemaError("APIError", {
|
|
23
|
+
message: Schema.String, statusCode: Schema.optional(NonNegativeInt), isRetryable: Schema.Boolean,
|
|
24
|
+
responseHeaders: Schema.optional(Schema.Record(Schema.String, Schema.String)), responseBody: Schema.optional(Schema.String),
|
|
25
|
+
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
26
|
+
})
|
|
27
|
+
export type APIError = import("zod").infer<typeof APIError.Schema>
|
|
28
|
+
export const ContextOverflowError = namedSchemaError("ContextOverflowError", { message: Schema.String, responseBody: Schema.optional(Schema.String) })
|
|
29
|
+
|
|
30
|
+
export class OutputFormatText extends Schema.Class<OutputFormatText>("OutputFormatText")({ type: Schema.Literal("text") }) { static readonly zod = zod(this) }
|
|
31
|
+
export class OutputFormatJsonSchema extends Schema.Class<OutputFormatJsonSchema>("OutputFormatJsonSchema")({
|
|
32
|
+
type: Schema.Literal("json_schema"), schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }),
|
|
33
|
+
retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))),
|
|
34
|
+
}) { static readonly zod = zod(this) }
|
|
35
|
+
|
|
36
|
+
const _Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({ discriminator: "type", identifier: "OutputFormat" })
|
|
37
|
+
export { _Format }
|
|
38
|
+
export const Format = Object.assign(_Format, { zod: zod(_Format) })
|
|
39
|
+
export type OutputFormat = Schema.Schema.Type<typeof _Format>
|
|
40
|
+
|
|
41
|
+
// Assistant error union (Zod)
|
|
42
|
+
import z from "zod"
|
|
43
|
+
const AssistantErrorZod = z.discriminatedUnion("name", [
|
|
44
|
+
AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema, AbortedError.Schema,
|
|
45
|
+
StructuredOutputError.Schema, ContextOverflowError.Schema, APIError.Schema,
|
|
46
|
+
])
|
|
47
|
+
export type AssistantError = z.infer<typeof AssistantErrorZod>
|
|
48
|
+
export { AssistantErrorZod }
|
|
49
|
+
|
|
50
|
+
// Assistant error union (Effect Schema)
|
|
51
|
+
export const AssistantErrorSchema = Schema.Union([
|
|
52
|
+
AuthError.EffectSchema,
|
|
53
|
+
Schema.Struct({ name: Schema.Literal("UnknownError"), data: Schema.Struct({ message: Schema.String }) }).annotate({ identifier: "UnknownError" }),
|
|
54
|
+
OutputLengthError.EffectSchema, AbortedError.EffectSchema, StructuredOutputError.EffectSchema, ContextOverflowError.EffectSchema, APIError.EffectSchema,
|
|
55
|
+
]).annotate({ discriminator: "name" })
|
|
56
|
+
|
|
57
|
+
export function fromError(e: unknown, ctx: { providerID: ProviderID; aborted?: boolean }): NonNullable<Assistant["error"]> {
|
|
58
|
+
switch (true) {
|
|
59
|
+
case e instanceof DOMException && e.name === "AbortError": return new AbortedError({ message: e.message }, { cause: e }).toObject()
|
|
60
|
+
case OutputLengthError.isInstance(e): return e
|
|
61
|
+
case LoadAPIKeyError.isInstance(e): return new AuthError({ providerID: ctx.providerID, message: e.message }, { cause: e }).toObject()
|
|
62
|
+
case e instanceof CodexAuthExpiredError: return new AuthError({ providerID: "openai", message: e.message }, { cause: e }).toObject()
|
|
63
|
+
case SessionNetwork.disconnected(e):
|
|
64
|
+
return new APIError({ message: SessionNetwork.message(e), isRetryable: true, metadata: { code: (e as import("bun").SystemError).code ?? "", syscall: (e as import("bun").SystemError).syscall ?? "", message: (e as import("bun").SystemError).message ?? "" } }, { cause: e }).toObject()
|
|
65
|
+
case e instanceof Error && (e as FetchDecompressionError).code === "ZlibError":
|
|
66
|
+
if (ctx.aborted) return new AbortedError({ message: e.message }, { cause: e }).toObject()
|
|
67
|
+
return new APIError({ message: "Response decompression failed", isRetryable: true, metadata: { code: (e as FetchDecompressionError).code, message: e.message } }, { cause: e }).toObject()
|
|
68
|
+
case APICallError.isInstance(e):
|
|
69
|
+
const parsed = ProviderError.parseAPICallError({ providerID: ctx.providerID, error: e })
|
|
70
|
+
if (parsed.type === "context_overflow") return new ContextOverflowError({ message: parsed.message, responseBody: parsed.responseBody }, { cause: e }).toObject()
|
|
71
|
+
return new APIError({ message: parsed.message, statusCode: parsed.statusCode, isRetryable: parsed.isRetryable, responseHeaders: parsed.responseHeaders, responseBody: parsed.responseBody, metadata: parsed.metadata }, { cause: e }).toObject()
|
|
72
|
+
case e instanceof Error: return new NamedError.Unknown({ message: errorMessage(e) }, { cause: e }).toObject()
|
|
73
|
+
default:
|
|
74
|
+
try {
|
|
75
|
+
const parsed = ProviderError.parseStreamError(e)
|
|
76
|
+
if (parsed) {
|
|
77
|
+
if (parsed.type === "context_overflow") return new ContextOverflowError({ message: parsed.message, responseBody: parsed.responseBody }, { cause: e }).toObject()
|
|
78
|
+
return new APIError({ message: parsed.message, isRetryable: parsed.isRetryable, responseBody: parsed.responseBody }, { cause: e }).toObject()
|
|
79
|
+
}
|
|
80
|
+
} catch {}
|
|
81
|
+
return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject()
|
|
82
|
+
}
|
|
83
|
+
}
|