saeeol 1.0.6 → 1.0.8
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 +1 -1
- package/script/build.ts +107 -6
- package/specs/effect/http-api.md +17 -17
- package/src/cli/cmd/providers.ts +75 -1
- package/src/cli/cmd/tui/app-events.ts +32 -0
- package/src/cli/cmd/tui/component/prompt/PromptStatusBar.tsx +15 -6
- package/src/cli/cmd/tui/component/prompt/use-prompt-memos.ts +9 -3
- package/src/index.ts +14 -109
- package/src/provider/bundled-providers.ts +28 -27
- package/src/provider/provider-events.ts +29 -0
- package/src/provider/provider-resolve.ts +23 -3
- package/src/provider/tiers/code.ts +20 -0
- package/src/provider/tiers/light.ts +10 -0
- package/src/provider/tiers/master.ts +17 -0
- package/src/saeeol/plugins/sidebar-usage.tsx +25 -0
- package/src/saeeol/provider/provider.ts +2 -12
- package/src/saeeol/session/compaction-chunks-utils.ts +53 -2
- package/src/server/routes/instance/event.ts +2 -0
- package/src/server/routes/instance/httpapi/api.ts +2 -0
- package/src/server/routes/ui.ts +0 -1
- package/src/session/compaction.ts +30 -6
- package/src/session/prompt/anthropic.txt +4 -4
- package/src/session/prompt/default.txt +4 -4
- package/src/session/prompt/ling.txt +5 -5
- package/src/tool/package.ts +168 -0
- package/src/tool/registry.ts +4 -0
- package/test/fixture/skills/agents-sdk/SKILL.md +30 -30
- package/test/fixture/skills/cloudflare/SKILL.md +66 -66
- package/test/saeeol/compaction-smart-select.test.ts +100 -0
- package/src/cli/cmd/tui/context/theme/opencode.json +0 -245
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BusEvent } from "@/bus/bus-event"
|
|
2
|
+
import { Schema } from "effect"
|
|
3
|
+
|
|
4
|
+
// Provider SDK 동적 설치 이벤트 — TUI/웹뷰에서 구독하여 설치 진행 상태 표시
|
|
5
|
+
// BusEvent.define 호출로 전역 레지스트리에 자동 등록됨
|
|
6
|
+
export const ProviderInstallEvent = {
|
|
7
|
+
Started: BusEvent.define(
|
|
8
|
+
"provider.install.started",
|
|
9
|
+
Schema.Struct({
|
|
10
|
+
providerID: Schema.String,
|
|
11
|
+
pkg: Schema.String,
|
|
12
|
+
}),
|
|
13
|
+
),
|
|
14
|
+
Completed: BusEvent.define(
|
|
15
|
+
"provider.install.completed",
|
|
16
|
+
Schema.Struct({
|
|
17
|
+
providerID: Schema.String,
|
|
18
|
+
pkg: Schema.String,
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
Failed: BusEvent.define(
|
|
22
|
+
"provider.install.failed",
|
|
23
|
+
Schema.Struct({
|
|
24
|
+
providerID: Schema.String,
|
|
25
|
+
pkg: Schema.String,
|
|
26
|
+
error: Schema.String,
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
}
|
|
@@ -5,7 +5,9 @@ import { NoSuchModelError, type Provider as SDK } from "ai"
|
|
|
5
5
|
import * as Log from "@saeeol/core/util/log"
|
|
6
6
|
import { Npm } from "@saeeol/core/npm"
|
|
7
7
|
import { Hash } from "@saeeol/core/util/hash"
|
|
8
|
+
import * as Bus from "@/bus"
|
|
8
9
|
import { BUNDLED_PROVIDERS } from "./bundled-providers"
|
|
10
|
+
import { ProviderInstallEvent } from "./provider-events"
|
|
9
11
|
import { buildTimeoutSignal } from "@/saeeol/provider/provider"
|
|
10
12
|
import { InitError } from "./provider-types"
|
|
11
13
|
import type { State, BundledSDK } from "./provider-types"
|
|
@@ -182,9 +184,27 @@ export async function resolveSDK(model: Model, s: State, envs: Record<string, st
|
|
|
182
184
|
|
|
183
185
|
let installedPath: string
|
|
184
186
|
if (!model.api.npm.startsWith("file://")) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
187
|
+
log.info("installing provider package", { providerID: model.providerID, pkg: model.api.npm })
|
|
188
|
+
void Bus.publish(ProviderInstallEvent.Started, {
|
|
189
|
+
providerID: model.providerID,
|
|
190
|
+
pkg: model.api.npm,
|
|
191
|
+
})
|
|
192
|
+
try {
|
|
193
|
+
const item = await Npm.add(model.api.npm)
|
|
194
|
+
if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`)
|
|
195
|
+
installedPath = item.entrypoint
|
|
196
|
+
void Bus.publish(ProviderInstallEvent.Completed, {
|
|
197
|
+
providerID: model.providerID,
|
|
198
|
+
pkg: model.api.npm,
|
|
199
|
+
})
|
|
200
|
+
} catch (installErr) {
|
|
201
|
+
void Bus.publish(ProviderInstallEvent.Failed, {
|
|
202
|
+
providerID: model.providerID,
|
|
203
|
+
pkg: model.api.npm,
|
|
204
|
+
error: installErr instanceof Error ? installErr.message : String(installErr),
|
|
205
|
+
})
|
|
206
|
+
throw installErr
|
|
207
|
+
}
|
|
188
208
|
} else {
|
|
189
209
|
log.info("loading local provider", { pkg: model.api.npm })
|
|
190
210
|
installedPath = model.api.npm
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// CODE 티어 provider 맵 — LIGHT + 개발용 provider
|
|
2
|
+
import type { BundledSDK } from "../../provider/provider-types"
|
|
3
|
+
import { lightProviders } from "./light"
|
|
4
|
+
|
|
5
|
+
type L = () => Promise<(opts: any) => BundledSDK>
|
|
6
|
+
|
|
7
|
+
export const codeProviders: Record<string, L> = {
|
|
8
|
+
...lightProviders,
|
|
9
|
+
"@ai-sdk/amazon-bedrock": () => import("@ai-sdk/amazon-bedrock").then((m) => m.createAmazonBedrock),
|
|
10
|
+
"@ai-sdk/azure": () => import("@ai-sdk/azure").then((m) => m.createAzure),
|
|
11
|
+
"@ai-sdk/google-vertex": () => import("@ai-sdk/google-vertex").then((m) => m.createVertex),
|
|
12
|
+
"@ai-sdk/google-vertex/anthropic": () =>
|
|
13
|
+
import("@ai-sdk/google-vertex/anthropic").then((m) => m.createVertexAnthropic),
|
|
14
|
+
"@openrouter/ai-sdk-provider": () => import("@openrouter/ai-sdk-provider").then((m) => m.createOpenRouter),
|
|
15
|
+
"@ai-sdk/groq": () => import("@ai-sdk/groq").then((m) => m.createGroq),
|
|
16
|
+
"@ai-sdk/deepinfra": () => import("@ai-sdk/deepinfra").then((m) => m.createDeepInfra),
|
|
17
|
+
"@ai-sdk/gateway": () => import("@ai-sdk/gateway").then((m) => m.createGateway),
|
|
18
|
+
"@ai-sdk/alibaba": () => import("@ai-sdk/alibaba").then((m) => m.createAlibaba),
|
|
19
|
+
"@ai-sdk/cerebras": () => import("@ai-sdk/cerebras").then((m) => m.createCerebras),
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// LIGHT 티어 provider 맵 — @saeeol/gateway만 번들, 나머지는 런타임 동적 설치
|
|
2
|
+
// 번들 크기 최소화: 사용자가 provider를 선택하면 Npm.add()로 자동 설치됨
|
|
3
|
+
import type { BundledSDK } from "../../provider/provider-types"
|
|
4
|
+
import { createSaeeol } from "@saeeol/gateway"
|
|
5
|
+
|
|
6
|
+
type L = () => Promise<(opts: any) => BundledSDK>
|
|
7
|
+
|
|
8
|
+
export const lightProviders: Record<string, L> = {
|
|
9
|
+
"@saeeol/gateway": async () => createSaeeol as any,
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// MASTER 티어 provider 맵 — 전체 provider
|
|
2
|
+
import type { BundledSDK } from "../../provider/provider-types"
|
|
3
|
+
import { codeProviders } from "./code"
|
|
4
|
+
|
|
5
|
+
type L = () => Promise<(opts: any) => BundledSDK>
|
|
6
|
+
|
|
7
|
+
export const masterProviders: Record<string, L> = {
|
|
8
|
+
...codeProviders,
|
|
9
|
+
"@ai-sdk/xai": () => import("@ai-sdk/xai").then((m) => m.createXai),
|
|
10
|
+
"@ai-sdk/mistral": () => import("@ai-sdk/mistral").then((m) => m.createMistral),
|
|
11
|
+
"@ai-sdk/cohere": () => import("@ai-sdk/cohere").then((m) => m.createCohere),
|
|
12
|
+
"@ai-sdk/togetherai": () => import("@ai-sdk/togetherai").then((m) => m.createTogetherAI),
|
|
13
|
+
"@ai-sdk/perplexity": () => import("@ai-sdk/perplexity").then((m) => m.createPerplexity),
|
|
14
|
+
"@ai-sdk/vercel": () => import("@ai-sdk/vercel").then((m) => m.createVercel),
|
|
15
|
+
"gitlab-ai-provider": () => import("gitlab-ai-provider").then((m) => m.createGitLab),
|
|
16
|
+
"venice-ai-sdk-provider": () => import("venice-ai-sdk-provider").then((m) => m.createVenice),
|
|
17
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@saeeol/plugin/tui"
|
|
2
2
|
import { createMemo } from "solid-js"
|
|
3
3
|
import { formatCount, getUsage } from "@tui/routes/session/usage"
|
|
4
|
+
import type { AssistantMessage } from "@saeeol/sdk/v2"
|
|
4
5
|
|
|
5
6
|
const id = "internal:saeeol-sidebar-usage"
|
|
6
7
|
|
|
@@ -13,8 +14,24 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
|
|
|
13
14
|
input: formatCount(total.input),
|
|
14
15
|
output: formatCount(total.output),
|
|
15
16
|
cached: formatCount(total.cached),
|
|
17
|
+
total: total.input + total.output + total.cached,
|
|
16
18
|
}
|
|
17
19
|
})
|
|
20
|
+
const contextInfo = createMemo(() => {
|
|
21
|
+
const messages = msg()
|
|
22
|
+
const last = messages.findLast((item): item is AssistantMessage => item.role === "assistant" && item.tokens.output > 0)
|
|
23
|
+
if (!last) return null
|
|
24
|
+
const tokens = last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
|
25
|
+
const model = props.api.state.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
|
|
26
|
+
const limit = model?.limit.context ?? 0
|
|
27
|
+
if (!limit) return { pct: 0, label: "", color: theme().textMuted }
|
|
28
|
+
const pct = Math.round((tokens / limit) * 100)
|
|
29
|
+
const color = pct >= 90 ? theme().error : pct >= 75 ? theme().warning : theme().textMuted
|
|
30
|
+
const barWidth = 12
|
|
31
|
+
const filled = Math.round((pct / 100) * barWidth)
|
|
32
|
+
const bar = "█".repeat(filled) + "░".repeat(barWidth - filled)
|
|
33
|
+
return { pct, label: `${bar} ${pct}%`, color }
|
|
34
|
+
})
|
|
18
35
|
|
|
19
36
|
return (
|
|
20
37
|
<box>
|
|
@@ -33,6 +50,14 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
|
|
|
33
50
|
<text fg={theme().textMuted}>Cached</text>
|
|
34
51
|
<text fg={theme().textMuted}>{usage().cached}</text>
|
|
35
52
|
</box>
|
|
53
|
+
{contextInfo() && contextInfo()!.pct > 0 && (
|
|
54
|
+
<box flexDirection="column">
|
|
55
|
+
<text fg={theme().text}>
|
|
56
|
+
<b>Context</b>
|
|
57
|
+
</text>
|
|
58
|
+
<text fg={contextInfo()!.color}>{contextInfo()!.label}</text>
|
|
59
|
+
</box>
|
|
60
|
+
)}
|
|
36
61
|
</box>
|
|
37
62
|
)
|
|
38
63
|
}
|
|
@@ -4,26 +4,16 @@
|
|
|
4
4
|
//
|
|
5
5
|
// This module exports patch functions and data that the upstream provider.ts
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { AI_SDK_PROVIDERS, PROMPTS } from "@saeeol/gateway"
|
|
8
8
|
import { DEFAULT_HEADERS } from "@/saeeol/const"
|
|
9
9
|
import { ProviderID, ModelID } from "@/provider/schema"
|
|
10
10
|
import { Effect, Schema } from "effect"
|
|
11
|
-
import type {
|
|
11
|
+
import type { ProviderV2 } from "@ai-sdk/provider"
|
|
12
12
|
import { mapValues, omit, pickBy } from "remeda"
|
|
13
13
|
|
|
14
14
|
/** Default timeout (ms) for provider HTTP requests (connection phase). */
|
|
15
15
|
export const REQUEST_TIMEOUT_MS = 300_000 // 5 minutes
|
|
16
16
|
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Bundled providers
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
type BundledSDK = { languageModel(modelId: string): LanguageModelV3 }
|
|
22
|
-
|
|
23
|
-
export const SAEEOL_BUNDLED_PROVIDERS: Record<string, () => Promise<(options: any) => BundledSDK>> = {
|
|
24
|
-
"@saeeol/gateway": async () => createSaeeol as unknown as (options: any) => BundledSDK,
|
|
25
|
-
}
|
|
26
|
-
|
|
27
17
|
// ---------------------------------------------------------------------------
|
|
28
18
|
// Model schema extensions (spread into Provider.Model Schema.Struct)
|
|
29
19
|
// ---------------------------------------------------------------------------
|
|
@@ -21,6 +21,8 @@ export const DEFAULT_BATCH_SIZE = 10
|
|
|
21
21
|
export const DEFAULT_KEEP_HEAD = 2
|
|
22
22
|
export const DEFAULT_KEEP_TAIL = 2
|
|
23
23
|
export const DEFAULT_AUTO_BATCHES = 5
|
|
24
|
+
export const BATCH_TOKEN_TARGET = 4_000
|
|
25
|
+
export const BATCH_TOKEN_MAX = 8_000
|
|
24
26
|
|
|
25
27
|
export type Chunk = {
|
|
26
28
|
index: number
|
|
@@ -108,9 +110,57 @@ export function batchSplit(messages: MessageV2.WithParts[], cfg: Config.Info): B
|
|
|
108
110
|
const size = cfg.compaction?.batch_size ?? DEFAULT_BATCH_SIZE
|
|
109
111
|
const head = cfg.compaction?.keep_head ?? DEFAULT_KEEP_HEAD
|
|
110
112
|
const tail = cfg.compaction?.keep_tail ?? DEFAULT_KEEP_TAIL
|
|
113
|
+
const tokenTarget = BATCH_TOKEN_TARGET
|
|
114
|
+
const tokenMax = BATCH_TOKEN_MAX
|
|
111
115
|
const batches: Batch[] = []
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
let i = 0
|
|
117
|
+
while (i < messages.length) {
|
|
118
|
+
const remaining = messages.length - i
|
|
119
|
+
const fixedEnd = Math.min(i + size, messages.length)
|
|
120
|
+
if (remaining <= size + head + tail) {
|
|
121
|
+
const slice = messages.slice(i)
|
|
122
|
+
const midStart = Math.min(head, slice.length)
|
|
123
|
+
const midEnd = Math.max(midStart, slice.length - tail)
|
|
124
|
+
batches.push({
|
|
125
|
+
index: batches.length,
|
|
126
|
+
first: slice.slice(0, midStart),
|
|
127
|
+
middle: slice.slice(midStart, midEnd),
|
|
128
|
+
last: slice.slice(midEnd),
|
|
129
|
+
})
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
let end = fixedEnd
|
|
133
|
+
let est = 0
|
|
134
|
+
for (let j = i; j < fixedEnd; j++) {
|
|
135
|
+
const parts = messages[j]!.parts
|
|
136
|
+
for (const p of parts) {
|
|
137
|
+
if (p.type === "text") est += p.text.length
|
|
138
|
+
else if (p.type === "tool" && p.state.status === "completed") est += Math.min(p.state.output?.length ?? 0, TOOL_OUTPUT_MAX_CHARS)
|
|
139
|
+
else if (p.type === "reasoning") est += p.text?.length ?? 0
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
est = Math.round(est / 4)
|
|
143
|
+
if (est < tokenTarget && fixedEnd < messages.length) {
|
|
144
|
+
let expandEst = est
|
|
145
|
+
for (let j = fixedEnd; j < messages.length; j++) {
|
|
146
|
+
const parts = messages[j]!.parts
|
|
147
|
+
let add = 0
|
|
148
|
+
for (const p of parts) {
|
|
149
|
+
if (p.type === "text") add += p.text.length
|
|
150
|
+
else if (p.type === "tool" && p.state.status === "completed") add += Math.min(p.state.output?.length ?? 0, TOOL_OUTPUT_MAX_CHARS)
|
|
151
|
+
}
|
|
152
|
+
expandEst += Math.round(add / 4)
|
|
153
|
+
if (expandEst >= tokenTarget) { end = j + 1; break }
|
|
154
|
+
if (expandEst >= tokenMax) { end = j + 1; break }
|
|
155
|
+
end = j + 1
|
|
156
|
+
}
|
|
157
|
+
} else if (est > tokenMax) {
|
|
158
|
+
for (let j = fixedEnd - 1; j > i + head; j--) {
|
|
159
|
+
end = j
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const slice = messages.slice(i, end)
|
|
114
164
|
const midStart = Math.min(head, slice.length)
|
|
115
165
|
const midEnd = Math.max(midStart, slice.length - tail)
|
|
116
166
|
batches.push({
|
|
@@ -119,6 +169,7 @@ export function batchSplit(messages: MessageV2.WithParts[], cfg: Config.Info): B
|
|
|
119
169
|
middle: slice.slice(midStart, midEnd),
|
|
120
170
|
last: slice.slice(midEnd),
|
|
121
171
|
})
|
|
172
|
+
i = end
|
|
122
173
|
}
|
|
123
174
|
return batches
|
|
124
175
|
}
|
|
@@ -6,6 +6,8 @@ import * as Log from "@saeeol/core/util/log"
|
|
|
6
6
|
import { BusEvent } from "@/bus/bus-event"
|
|
7
7
|
import { Bus } from "@/bus"
|
|
8
8
|
import { AsyncQueue } from "@/util/queue"
|
|
9
|
+
// provider-events 등록: BusEvent.define 호출로 전역 레지스트리에 이벤트 타입 추가
|
|
10
|
+
import "@/provider/provider-events"
|
|
9
11
|
|
|
10
12
|
const log = Log.create({ service: "server" })
|
|
11
13
|
|
|
@@ -19,6 +19,8 @@ import { SessionApi } from "./groups/session"
|
|
|
19
19
|
import { SyncApi } from "./groups/sync"
|
|
20
20
|
import { TuiApi } from "./groups/tui"
|
|
21
21
|
import { WorkspaceApi } from "./groups/workspace"
|
|
22
|
+
// provider-events 등록: BusEvent.define 호출로 전역 레지스트리에 이벤트 타입 추가
|
|
23
|
+
import "@/provider/provider-events"
|
|
22
24
|
|
|
23
25
|
// SSE event schemas built from the same BusEvent/SyncEvent registries that
|
|
24
26
|
// the Hono spec uses, so both specs emit identical Event/SyncEvent components.
|
package/src/server/routes/ui.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { HttpClient, HttpServerRequest, HttpServerResponse } from "effect/unstab
|
|
|
5
5
|
import { Hono } from "hono"
|
|
6
6
|
import { getMimeType } from "hono/utils/mime"
|
|
7
7
|
import fs from "node:fs/promises"
|
|
8
|
-
// @ts-expect-error - generated file
|
|
9
8
|
import embeddedWebUIData from "../../saeeol-web-ui.gen.ts"
|
|
10
9
|
|
|
11
10
|
const embeddedWebUI: Record<string, string> | null = embeddedWebUIData ?? null
|
|
@@ -39,8 +39,11 @@ export const PRUNE_PROTECT = 40_000
|
|
|
39
39
|
const TOOL_OUTPUT_MAX_CHARS = 2_000
|
|
40
40
|
const PRUNE_PROTECTED_TOOLS = ["skill"]
|
|
41
41
|
const DEFAULT_TAIL_TURNS = 2
|
|
42
|
-
const MIN_PRESERVE_RECENT_TOKENS =
|
|
43
|
-
const MAX_PRESERVE_RECENT_TOKENS =
|
|
42
|
+
const MIN_PRESERVE_RECENT_TOKENS = 4_000
|
|
43
|
+
const MAX_PRESERVE_RECENT_TOKENS = 32_000
|
|
44
|
+
const TAIL_PRESERVE_RATIO = 0.20
|
|
45
|
+
const CONTEXT_HEALTH_WARNING = 0.75
|
|
46
|
+
const CONTEXT_HEALTH_CRITICAL = 0.90
|
|
44
47
|
const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <template> and keep the section order unchanged. Do not include the <template> tags in your response.
|
|
45
48
|
<template>
|
|
46
49
|
## Goal
|
|
@@ -70,12 +73,16 @@ const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <te
|
|
|
70
73
|
|
|
71
74
|
## Relevant Files
|
|
72
75
|
- [file or directory path: why it matters, or "(none)"]
|
|
76
|
+
|
|
77
|
+
## Code Artifacts
|
|
78
|
+
- [key code snippets, function signatures, type definitions that were created or modified]
|
|
73
79
|
</template>
|
|
74
80
|
|
|
75
81
|
Rules:
|
|
76
82
|
- Keep every section, even when empty.
|
|
77
83
|
- Use terse bullets, not prose paragraphs.
|
|
78
84
|
- Preserve exact file paths, commands, error strings, and identifiers when known.
|
|
85
|
+
- Preserve critical code: function signatures, type definitions, config values, and error messages.
|
|
79
86
|
- Do not mention the summary process or that context was compacted.`
|
|
80
87
|
type Turn = {
|
|
81
88
|
start: number
|
|
@@ -137,9 +144,11 @@ function buildPrompt(input: { previousSummary?: string; context: string[] }) {
|
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
function preserveRecentBudget(input: { cfg: Config.Info; model: Provider.Model }) {
|
|
147
|
+
const ctx = input.model.limit.context || 128_000
|
|
148
|
+
const autoBudget = Math.floor(ctx * TAIL_PRESERVE_RATIO)
|
|
140
149
|
return (
|
|
141
150
|
input.cfg.compaction?.preserve_recent_tokens ??
|
|
142
|
-
clamp(
|
|
151
|
+
clamp(autoBudget, MIN_PRESERVE_RECENT_TOKENS, MAX_PRESERVE_RECENT_TOKENS)
|
|
143
152
|
)
|
|
144
153
|
}
|
|
145
154
|
|
|
@@ -267,11 +276,26 @@ export const layer: Layer.Layer<
|
|
|
267
276
|
{ concurrency: 1 },
|
|
268
277
|
)
|
|
269
278
|
|
|
279
|
+
const totalRecent = sizes.reduce((s, n) => s + n, 0)
|
|
280
|
+
const expandedLimit = totalRecent > budget
|
|
281
|
+
? Math.min(limit + Math.ceil(totalRecent / budget), all.length)
|
|
282
|
+
: limit
|
|
283
|
+
const expanded = expandedLimit > limit ? all.slice(-expandedLimit) : recent
|
|
284
|
+
const expandedSizes = expandedLimit > limit
|
|
285
|
+
? yield* Effect.forEach(
|
|
286
|
+
expanded,
|
|
287
|
+
(turn) => estimate({ messages: input.messages.slice(turn.start, turn.end), model: input.model }),
|
|
288
|
+
{ concurrency: 1 },
|
|
289
|
+
)
|
|
290
|
+
: sizes
|
|
291
|
+
|
|
270
292
|
let total = 0
|
|
271
293
|
let keep: Tail | undefined
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
294
|
+
const iterLimit = expandedLimit > limit ? expanded : recent
|
|
295
|
+
const iterSizes = expandedLimit > limit ? expandedSizes : sizes
|
|
296
|
+
for (let i = iterLimit.length - 1; i >= 0; i--) {
|
|
297
|
+
const turn = iterLimit[i]!
|
|
298
|
+
const size = iterSizes[i]
|
|
275
299
|
if (total + size <= budget) {
|
|
276
300
|
total += size
|
|
277
301
|
keep = { start: turn.start, id: turn.id }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
You are
|
|
1
|
+
You are SAEEOL, the best coding agent on the planet.
|
|
2
2
|
|
|
3
3
|
You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
|
4
4
|
|
|
@@ -7,9 +7,9 @@ IMPORTANT: You must NEVER generate or guess URLs for the user unless you are con
|
|
|
7
7
|
If the user asks for help or wants to give feedback inform them of the following:
|
|
8
8
|
- ctrl+p to list available actions
|
|
9
9
|
- To give feedback, users should report the issue at
|
|
10
|
-
https://github.com/
|
|
10
|
+
https://github.com/byfabulist/saeeol
|
|
11
11
|
|
|
12
|
-
When the user directly asks about
|
|
12
|
+
When the user directly asks about SAEEOL (eg. "can SAEEOL do...", "does SAEEOL have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific SAEEOL feature (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from SAEEOL docs. The list of available docs is available at https://github.com/byfabulist/saeeol
|
|
13
13
|
|
|
14
14
|
# Tone and style
|
|
15
15
|
- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
|
|
@@ -18,7 +18,7 @@ When the user directly asks about Kilo (eg. "can Kilo do...", "does Kilo have...
|
|
|
18
18
|
- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files.
|
|
19
19
|
|
|
20
20
|
# Professional objectivity
|
|
21
|
-
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if
|
|
21
|
+
Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if SAEEOL honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.
|
|
22
22
|
|
|
23
23
|
# Task Management
|
|
24
24
|
You have access to the TodoWrite tools to help you manage and plan tasks. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
You are
|
|
1
|
+
You are SAEEOL, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
|
2
2
|
|
|
3
3
|
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
|
4
4
|
|
|
5
5
|
If the user asks for help or wants to give feedback inform them of the following:
|
|
6
|
-
- /help: Get help with using
|
|
7
|
-
- To give feedback, users should report the issue at https://github.com/
|
|
6
|
+
- /help: Get help with using SAEEOL
|
|
7
|
+
- To give feedback, users should report the issue at https://github.com/byfabulist/saeeol/issues
|
|
8
8
|
|
|
9
|
-
When the user directly asks about
|
|
9
|
+
When the user directly asks about SAEEOL (eg 'can SAEEOL do...', 'does SAEEOL have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from SAEEOL docs at https://github.com/byfabulist/saeeol
|
|
10
10
|
|
|
11
11
|
# Tone and style
|
|
12
12
|
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
You are
|
|
1
|
+
You are SAEEOL, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
|
2
2
|
|
|
3
3
|
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
|
|
4
4
|
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
|
|
5
5
|
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
|
6
6
|
IMPORTANT: Every Bash tool call MUST include a `description` field. Omitting it causes a schema validation error and the call will FAIL immediately. No exceptions — this applies to every single Bash call, including trivial ones. Example: {"command": "ls", "description": "List files in directory"}
|
|
7
7
|
|
|
8
|
-
When the user directly asks about
|
|
8
|
+
When the user directly asks about SAEEOL (eg 'can SAEEOL do...', 'does SAEEOL have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from SAEEOL docs at https://github.com/byfabulist/saeeol
|
|
9
9
|
|
|
10
10
|
# Professional objectivity
|
|
11
11
|
Prioritize technical accuracy over validating the user's beliefs. Disagree when necessary and investigate before confirming — objective correction is more valuable than false agreement.
|
|
@@ -92,7 +92,7 @@ When a user reports unexpected behavior:
|
|
|
92
92
|
Tool results and user messages may include `<system-reminder>` tags containing useful context — they are NOT part of the user's input.
|
|
93
93
|
|
|
94
94
|
# Project config files
|
|
95
|
-
`.
|
|
95
|
+
`.saeeol/command/*.md` defines slash commands; `.saeeol/agent/*.md` defines agent personas. These are not task specs — do not search them to understand what a task requires. Exception: when the user explicitly invokes a slash command or agent, you may read its definition file to understand how to execute it.
|
|
96
96
|
|
|
97
97
|
# Tool usage policy
|
|
98
98
|
- For file search, prefer Glob and Grep tools over Bash `find`/`ls` to reduce context usage.
|
|
@@ -125,5 +125,5 @@ assistant: Clients are marked as failed in the `connectToServer` function in src
|
|
|
125
125
|
</example>
|
|
126
126
|
|
|
127
127
|
If the user asks for help or wants to give feedback inform them of the following:
|
|
128
|
-
- /help: Get help with using
|
|
129
|
-
- To give feedback, users should report the issue at https://github.com/
|
|
128
|
+
- /help: Get help with using SAEEOL
|
|
129
|
+
- To give feedback, users should report the issue at https://github.com/byfabulist/saeeol/issues
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect"
|
|
2
|
+
import { Npm } from "@saeeol/core/npm"
|
|
3
|
+
import * as Bus from "@/bus"
|
|
4
|
+
import * as Tool from "./tool"
|
|
5
|
+
import { ProviderInstallEvent } from "@/provider/provider-events"
|
|
6
|
+
|
|
7
|
+
const Parameters = Schema.Struct({
|
|
8
|
+
action: Schema.Literals(["install", "list"]).annotate({
|
|
9
|
+
description: '"install" to install provider packages, "list" to show available providers',
|
|
10
|
+
}),
|
|
11
|
+
packages: Schema.optional(Schema.Array(Schema.String)).annotate({
|
|
12
|
+
description:
|
|
13
|
+
'Package names to install. npm names like "@ai-sdk/anthropic" or short names like "anthropic".',
|
|
14
|
+
}),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
type Params = Schema.Schema.Type<typeof Parameters>
|
|
18
|
+
|
|
19
|
+
const SHORT_NAME_MAP: Record<string, string> = {
|
|
20
|
+
anthropic: "@ai-sdk/anthropic",
|
|
21
|
+
openai: "@ai-sdk/openai",
|
|
22
|
+
"openai-compatible": "@ai-sdk/openai-compatible",
|
|
23
|
+
google: "@ai-sdk/google",
|
|
24
|
+
"google-vertex": "@ai-sdk/google-vertex",
|
|
25
|
+
bedrock: "@ai-sdk/amazon-bedrock",
|
|
26
|
+
"amazon-bedrock": "@ai-sdk/amazon-bedrock",
|
|
27
|
+
azure: "@ai-sdk/azure",
|
|
28
|
+
openrouter: "@openrouter/ai-sdk-provider",
|
|
29
|
+
groq: "@ai-sdk/groq",
|
|
30
|
+
deepinfra: "@ai-sdk/deepinfra",
|
|
31
|
+
gateway: "@ai-sdk/gateway",
|
|
32
|
+
alibaba: "@ai-sdk/alibaba",
|
|
33
|
+
cerebras: "@ai-sdk/cerebras",
|
|
34
|
+
xai: "@ai-sdk/xai",
|
|
35
|
+
mistral: "@ai-sdk/mistral",
|
|
36
|
+
cohere: "@ai-sdk/cohere",
|
|
37
|
+
togetherai: "@ai-sdk/togetherai",
|
|
38
|
+
perplexity: "@ai-sdk/perplexity",
|
|
39
|
+
vercel: "@ai-sdk/vercel",
|
|
40
|
+
gitlab: "gitlab-ai-provider",
|
|
41
|
+
venice: "venice-ai-sdk-provider",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const KNOWN_PROVIDERS = [
|
|
45
|
+
{ npm: "@saeeol/gateway", label: "Saeeol Gateway (bundled)" },
|
|
46
|
+
{ npm: "@ai-sdk/anthropic", label: "Anthropic (Claude)" },
|
|
47
|
+
{ npm: "@ai-sdk/openai", label: "OpenAI (GPT)" },
|
|
48
|
+
{ npm: "@ai-sdk/openai-compatible", label: "OpenAI Compatible" },
|
|
49
|
+
{ npm: "@ai-sdk/google", label: "Google (Gemini)" },
|
|
50
|
+
{ npm: "@ai-sdk/github-copilot", label: "GitHub Copilot" },
|
|
51
|
+
{ npm: "@ai-sdk/amazon-bedrock", label: "Amazon Bedrock" },
|
|
52
|
+
{ npm: "@ai-sdk/azure", label: "Azure OpenAI" },
|
|
53
|
+
{ npm: "@ai-sdk/google-vertex", label: "Google Vertex AI" },
|
|
54
|
+
{ npm: "@openrouter/ai-sdk-provider", label: "OpenRouter" },
|
|
55
|
+
{ npm: "@ai-sdk/groq", label: "Groq" },
|
|
56
|
+
{ npm: "@ai-sdk/deepinfra", label: "DeepInfra" },
|
|
57
|
+
{ npm: "@ai-sdk/gateway", label: "Vercel AI Gateway" },
|
|
58
|
+
{ npm: "@ai-sdk/alibaba", label: "Alibaba (Qwen)" },
|
|
59
|
+
{ npm: "@ai-sdk/cerebras", label: "Cerebras" },
|
|
60
|
+
{ npm: "@ai-sdk/xai", label: "xAI (Grok)" },
|
|
61
|
+
{ npm: "@ai-sdk/mistral", label: "Mistral" },
|
|
62
|
+
{ npm: "@ai-sdk/cohere", label: "Cohere" },
|
|
63
|
+
{ npm: "@ai-sdk/togetherai", label: "Together AI" },
|
|
64
|
+
{ npm: "@ai-sdk/perplexity", label: "Perplexity" },
|
|
65
|
+
{ npm: "@ai-sdk/vercel", label: "Vercel" },
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
function resolve(pkg: string) {
|
|
69
|
+
if (pkg.startsWith("@") || pkg.startsWith("file://")) return pkg
|
|
70
|
+
return SHORT_NAME_MAP[pkg] ?? pkg
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const PackageTool = Tool.define(
|
|
74
|
+
"package",
|
|
75
|
+
Effect.gen(function* () {
|
|
76
|
+
return {
|
|
77
|
+
description: [
|
|
78
|
+
"Manage saeeol provider SDK packages.",
|
|
79
|
+
"",
|
|
80
|
+
"Use this tool when the user asks to install additional AI providers,",
|
|
81
|
+
"or when you detect that a required provider is not available.",
|
|
82
|
+
"",
|
|
83
|
+
'Actions: "install" — install provider packages, "list" — show available providers',
|
|
84
|
+
"",
|
|
85
|
+
"Short names: anthropic, openai, google, bedrock, azure, openrouter, groq,",
|
|
86
|
+
"deepinfra, cerebras, xai, mistral, cohere, togetherai, perplexity, vercel",
|
|
87
|
+
].join("\n"),
|
|
88
|
+
parameters: Parameters,
|
|
89
|
+
execute: (params: Params, ctx: Tool.Context) =>
|
|
90
|
+
Effect.gen(function* () {
|
|
91
|
+
if (params.action === "list") {
|
|
92
|
+
const lines = [
|
|
93
|
+
"Available provider SDKs:",
|
|
94
|
+
"",
|
|
95
|
+
...KNOWN_PROVIDERS.map((p) => ` ${p.label}\n npm: ${p.npm}`),
|
|
96
|
+
"",
|
|
97
|
+
'Install: { action: "install", packages: ["anthropic"] }',
|
|
98
|
+
]
|
|
99
|
+
return { title: "Available Providers", output: lines.join("\n"), metadata: {} }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// action === "install"
|
|
103
|
+
const packages = params.packages
|
|
104
|
+
if (!packages || packages.length === 0) {
|
|
105
|
+
return {
|
|
106
|
+
title: "Package Install",
|
|
107
|
+
output: "No packages specified. Provide package names in the 'packages' array.",
|
|
108
|
+
metadata: {},
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const resolved = packages.map(resolve)
|
|
113
|
+
|
|
114
|
+
yield* ctx.ask({
|
|
115
|
+
permission: "package",
|
|
116
|
+
patterns: resolved,
|
|
117
|
+
always: resolved,
|
|
118
|
+
metadata: {},
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const results: string[] = []
|
|
122
|
+
for (const pkg of resolved) {
|
|
123
|
+
void Bus.publish(ProviderInstallEvent.Started, {
|
|
124
|
+
providerID: pkg,
|
|
125
|
+
pkg,
|
|
126
|
+
})
|
|
127
|
+
try {
|
|
128
|
+
const entry = yield* Effect.promise(() => Npm.add(pkg))
|
|
129
|
+
if (!entry.entrypoint) {
|
|
130
|
+
void Bus.publish(ProviderInstallEvent.Failed, {
|
|
131
|
+
providerID: pkg,
|
|
132
|
+
pkg,
|
|
133
|
+
error: "No import entrypoint found",
|
|
134
|
+
})
|
|
135
|
+
results.push(`X ${pkg}: no import entrypoint`)
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
void Bus.publish(ProviderInstallEvent.Completed, {
|
|
139
|
+
providerID: pkg,
|
|
140
|
+
pkg,
|
|
141
|
+
})
|
|
142
|
+
results.push(`OK ${pkg}`)
|
|
143
|
+
} catch (err) {
|
|
144
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
145
|
+
void Bus.publish(ProviderInstallEvent.Failed, {
|
|
146
|
+
providerID: pkg,
|
|
147
|
+
pkg,
|
|
148
|
+
error: msg,
|
|
149
|
+
})
|
|
150
|
+
results.push(`X ${pkg}: ${msg}`)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
title: "Package Install",
|
|
156
|
+
output: [
|
|
157
|
+
`${resolved.length} package(s) processed:`,
|
|
158
|
+
"",
|
|
159
|
+
...results,
|
|
160
|
+
"",
|
|
161
|
+
"Installed providers are now available.",
|
|
162
|
+
].join("\n"),
|
|
163
|
+
metadata: {},
|
|
164
|
+
}
|
|
165
|
+
}),
|
|
166
|
+
}
|
|
167
|
+
}),
|
|
168
|
+
)
|