saeeol 1.2.8 → 1.3.0
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 +11 -11
- package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +2 -3
- package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +0 -2
- package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +0 -1
- package/src/cli/cmd/tui/context/app/helper.tsx +2 -2
- package/src/cli/cmd/tui/context/app/sdk.tsx +2 -4
- package/src/cli/cmd/tui/context/app/sync.tsx +0 -1
- package/src/cli/cmd/tui/context/app/theme.tsx +1 -1
- package/src/cli/cmd/tui/context/runtime/local.tsx +0 -3
- package/src/ltm/config.ts +2 -12
- package/src/ltm/memory/procedural.ts +2 -12
- package/src/ltm/pipeline.ts +3 -19
- package/src/ltm/scheduler.ts +2 -14
- package/src/ltm/store.ts +2 -11
- package/src/ltm/types.ts +2 -8
- package/src/provider/local/embedder.ts +2 -18
- package/src/session/core/llm.ts +2 -6
- package/src/session/core/retry.ts +2 -6
- package/src/session/core/session-events.ts +143 -0
- package/src/session/core/session-types.ts +5 -7
- package/src/session/core/session.ts +9 -9
- package/src/session/message/message-errors.ts +2 -6
- package/src/session/message/message-parts.ts +2 -4
- package/src/session/prompt/prompt.ts +2 -3
- package/src/tool/file/apply_patch.ts +0 -21
- package/src/tool/file/edit-replacers.ts +2 -3
- package/src/tool/integration/package.ts +2 -4
- package/src/tool/search/warpgrep.ts +0 -8
- package/src/tool/search/webfetch.ts +2 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"name": "saeeol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"@babel/core": "7.28.4",
|
|
51
51
|
"@effect/language-service": "0.84.2",
|
|
52
52
|
"@octokit/webhooks-types": "7.6.1",
|
|
53
|
-
"@saeeol/script": "7.3.
|
|
54
|
-
"@saeeol/core": "7.3.
|
|
53
|
+
"@saeeol/script": "7.3.6",
|
|
54
|
+
"@saeeol/core": "7.3.6",
|
|
55
55
|
"@parcel/watcher-darwin-arm64": "2.5.1",
|
|
56
56
|
"@parcel/watcher-darwin-x64": "2.5.1",
|
|
57
57
|
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
|
@@ -133,14 +133,14 @@
|
|
|
133
133
|
"@parcel/watcher": "2.5.1",
|
|
134
134
|
"@pierre/diffs": "1.1.0-beta.18",
|
|
135
135
|
"@saeeol/boxes": "0.2.0",
|
|
136
|
-
"@saeeol/core": "7.3.
|
|
137
|
-
"@saeeol/gateway": "7.3.
|
|
138
|
-
"@saeeol/i18n": "7.3.
|
|
139
|
-
"@saeeol/indexing": "7.3.
|
|
140
|
-
"@saeeol/plugin": "7.3.
|
|
141
|
-
"@saeeol/script": "7.3.
|
|
142
|
-
"@saeeol/sdk": "7.3.
|
|
143
|
-
"@saeeol/telemetry": "7.3.
|
|
136
|
+
"@saeeol/core": "7.3.6",
|
|
137
|
+
"@saeeol/gateway": "7.3.6",
|
|
138
|
+
"@saeeol/i18n": "7.3.6",
|
|
139
|
+
"@saeeol/indexing": "7.3.6",
|
|
140
|
+
"@saeeol/plugin": "7.3.6",
|
|
141
|
+
"@saeeol/script": "7.3.6",
|
|
142
|
+
"@saeeol/sdk": "7.3.6",
|
|
143
|
+
"@saeeol/telemetry": "7.3.6",
|
|
144
144
|
"@solid-primitives/event-bus": "1.1.2",
|
|
145
145
|
"@solid-primitives/scheduled": "1.5.2",
|
|
146
146
|
"@standard-schema/spec": "1.0.0",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useDialog } from "@tui/ui/dialog"
|
|
1
|
+
import { useDialog } from "@tui/ui/dialog"
|
|
2
2
|
import { DialogSelect, type DialogSelectOption, type DialogSelectRef } from "@tui/ui/dialog-select"
|
|
3
3
|
import {
|
|
4
4
|
createContext,
|
|
@@ -74,7 +74,6 @@ function init() {
|
|
|
74
74
|
for (const option of entries()) {
|
|
75
75
|
if (!isEnabled(option)) continue
|
|
76
76
|
if (option.keybind && keybind.match(option.keybind, evt)) {
|
|
77
|
-
// Require double-tap for agent cycle keybinds
|
|
78
77
|
if (DOUBLE_TAB_KEYS.has(option.keybind)) {
|
|
79
78
|
const now = Date.now()
|
|
80
79
|
const match = option.keybind === lastTabKey && now - lastTabTime < DOUBLE_TAB_WINDOW
|
|
@@ -187,4 +186,4 @@ function DialogCommand(props: { options: CommandOption[]; suggestedOptions: Comm
|
|
|
187
186
|
return [...props.suggestedOptions, ...props.options]
|
|
188
187
|
}
|
|
189
188
|
return <DialogSelect ref={(r) => (ref = r)} title={t("cmd.title")} options={list()} />
|
|
190
|
-
}
|
|
189
|
+
}
|
|
@@ -27,7 +27,6 @@ export function DialogMcp() {
|
|
|
27
27
|
const [loading, setLoading] = createSignal<string | null>(null)
|
|
28
28
|
|
|
29
29
|
const options = createMemo(() => {
|
|
30
|
-
// Track sync data and loading state to trigger re-render when they change
|
|
31
30
|
const mcpData = sync.data.mcp
|
|
32
31
|
const loadingMcp = loading()
|
|
33
32
|
|
|
@@ -56,7 +55,6 @@ export function DialogMcp() {
|
|
|
56
55
|
setLoading(option.value)
|
|
57
56
|
try {
|
|
58
57
|
await local.mcp.toggle(option.value)
|
|
59
|
-
// Refresh MCP status from server
|
|
60
58
|
const status = await sdk.client.mcp.status()
|
|
61
59
|
if (status.data) {
|
|
62
60
|
sync.set("mcp", status.data)
|
|
@@ -36,7 +36,6 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
|
|
|
36
36
|
|
|
37
37
|
const options = createMemo(() => {
|
|
38
38
|
const entries = stash.list()
|
|
39
|
-
// Show most recent first
|
|
40
39
|
return entries
|
|
41
40
|
.map((entry, index) => {
|
|
42
41
|
const isDeleting = toDelete() === index
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, Show, useContext, type ParentProps } from "solid-js"
|
|
1
|
+
import { createContext, Show, useContext, type ParentProps } from "solid-js"
|
|
2
2
|
|
|
3
3
|
export function createSimpleContext<T, Props extends Record<string, any>>(input: {
|
|
4
4
|
name: string
|
|
@@ -22,4 +22,4 @@ export function createSimpleContext<T, Props extends Record<string, any>>(input:
|
|
|
22
22
|
return value
|
|
23
23
|
},
|
|
24
24
|
}
|
|
25
|
-
}
|
|
25
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createSaeeolClient } from "@saeeol/sdk/v2"
|
|
1
|
+
import { createSaeeolClient } from "@saeeol/sdk/v2"
|
|
2
2
|
import type { GlobalEvent } from "@saeeol/sdk/v2"
|
|
3
3
|
import { createSimpleContext } from "./helper"
|
|
4
4
|
import { createGlobalEmitter } from "@solid-primitives/event-bus"
|
|
@@ -100,8 +100,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|
|
100
100
|
if (queue.length > 0) flush()
|
|
101
101
|
attempt += 1
|
|
102
102
|
if (abort.signal.aborted || ctrl.signal.aborted) break
|
|
103
|
-
|
|
104
|
-
// Exponential backoff
|
|
105
103
|
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), maxRetryDelay)
|
|
106
104
|
await new Promise((resolve) => setTimeout(resolve, backoff))
|
|
107
105
|
}
|
|
@@ -139,4 +137,4 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
|
|
|
139
137
|
url: props.url,
|
|
140
138
|
}
|
|
141
139
|
},
|
|
142
|
-
})
|
|
140
|
+
})
|
|
@@ -565,7 +565,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|
|
565
565
|
})
|
|
566
566
|
.then(() => {
|
|
567
567
|
if (store.status !== "complete") setStore("status", "partial")
|
|
568
|
-
// non-blocking
|
|
569
568
|
void Promise.all([
|
|
570
569
|
...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
|
|
571
570
|
consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))),
|
|
@@ -233,8 +233,8 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|
|
233
233
|
const subtleSyntax = createMemo(() => generateSubtleSyntax(values()))
|
|
234
234
|
return {
|
|
235
235
|
theme: new Proxy({} as Theme, {
|
|
236
|
+
// @ts-expect-error
|
|
236
237
|
get(_target, prop) {
|
|
237
|
-
// @ts-expect-error
|
|
238
238
|
return values()[prop]
|
|
239
239
|
},
|
|
240
240
|
}),
|
|
@@ -99,7 +99,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
99
99
|
if (agent?.color) {
|
|
100
100
|
const color = agent.color
|
|
101
101
|
if (color.startsWith("#")) return RGBA.fromHex(color)
|
|
102
|
-
// already validated by config, just satisfying TS here
|
|
103
102
|
return theme[color as keyof typeof theme] as RGBA
|
|
104
103
|
}
|
|
105
104
|
return colors()[index % colors().length]
|
|
@@ -440,10 +439,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
440
439
|
async toggle(name: string) {
|
|
441
440
|
const status = sync.data.mcp[name]
|
|
442
441
|
if (status?.status === "connected") {
|
|
443
|
-
// Disable: disconnect the MCP
|
|
444
442
|
await sdk.client.mcp.disconnect({ name })
|
|
445
443
|
} else {
|
|
446
|
-
// Enable/Retry: connect the MCP (handles disabled, failed, and other states)
|
|
447
444
|
await sdk.client.mcp.connect({ name })
|
|
448
445
|
}
|
|
449
446
|
},
|
package/src/ltm/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** LTM — hardware auto-detection + deterministic LLM parameter calculation */
|
|
1
|
+
/** LTM — hardware auto-detection + deterministic LLM parameter calculation */
|
|
2
2
|
|
|
3
3
|
import os from "os"
|
|
4
4
|
import { Effect } from "effect"
|
|
@@ -10,8 +10,6 @@ import type { HardwareProfile, LLMBakeParams, LTMConfig } from "./types"
|
|
|
10
10
|
|
|
11
11
|
const log = Log.create({ service: "ltm/config" })
|
|
12
12
|
|
|
13
|
-
// ── Default LTM config ──
|
|
14
|
-
|
|
15
13
|
export const DEFAULT_LTM_CONFIG: LTMConfig = {
|
|
16
14
|
enabled: false,
|
|
17
15
|
embeddingModel: "auto",
|
|
@@ -23,8 +21,6 @@ export const DEFAULT_LTM_CONFIG: LTMConfig = {
|
|
|
23
21
|
retrieval: { topK: 5, minScore: 0.7, maxTokens: 2000 },
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
// ── Hardware profiling ──
|
|
27
|
-
|
|
28
24
|
/** Collect system hardware information */
|
|
29
25
|
export async function profileHardware(): Promise<HardwareProfile> {
|
|
30
26
|
const gpu = await Effect.runPromise(GPU.profile)
|
|
@@ -51,8 +47,6 @@ export function hardwareHash(hw: HardwareProfile): string {
|
|
|
51
47
|
return h.toString(36)
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
// ── Embedding model selection ──
|
|
55
|
-
|
|
56
50
|
/** Auto-select embedding model based on available VRAM */
|
|
57
51
|
export function selectEmbeddingModel(hw: HardwareProfile) {
|
|
58
52
|
const vram = hw.availableVRAMMB
|
|
@@ -63,8 +57,6 @@ export function selectEmbeddingModel(hw: HardwareProfile) {
|
|
|
63
57
|
return RAG.EMBEDDING_MODELS[5] // all-minilm-l6, 384d, 80MB
|
|
64
58
|
}
|
|
65
59
|
|
|
66
|
-
// ── Bake (deterministic parameter calculation) ──
|
|
67
|
-
|
|
68
60
|
/**
|
|
69
61
|
* Compute hardware-based LLM parameters and persist to disk.
|
|
70
62
|
* Once computed, the same values are reused as long as hardware doesn't change.
|
|
@@ -96,8 +88,6 @@ export async function bake(hw: HardwareProfile): Promise<LLMBakeParams> {
|
|
|
96
88
|
return params
|
|
97
89
|
}
|
|
98
90
|
|
|
99
|
-
// ── Persistence ──
|
|
100
|
-
|
|
101
91
|
import path from "path"
|
|
102
92
|
import { mkdir, readFile, writeFile } from "fs/promises"
|
|
103
93
|
|
|
@@ -121,4 +111,4 @@ async function readBake(): Promise<LLMBakeParams | undefined> {
|
|
|
121
111
|
async function writeBake(params: LLMBakeParams): Promise<void> {
|
|
122
112
|
await mkdir(bakeDir(), { recursive: true })
|
|
123
113
|
await writeFile(bakePath(), JSON.stringify(params, null, 2))
|
|
124
|
-
}
|
|
114
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** LTM procedural memory — user coding preferences and patterns (English, LLM-to-LLM) */
|
|
1
|
+
/** LTM procedural memory — user coding preferences and patterns (English, LLM-to-LLM) */
|
|
2
2
|
|
|
3
3
|
import * as Log from "@saeeol/core/util/log"
|
|
4
4
|
import * as Embedder from "@/provider/local/embedder"
|
|
@@ -20,8 +20,6 @@ export function extractStyleSignals(
|
|
|
20
20
|
): StyleSignal[] {
|
|
21
21
|
const signals: StyleSignal[] = []
|
|
22
22
|
const ext = filePath.split(".").pop() ?? ""
|
|
23
|
-
|
|
24
|
-
// Comment language
|
|
25
23
|
if (content.includes("//") && (ext === "ts" || ext === "js")) {
|
|
26
24
|
const hasKoreanComment = /[ㄱ-ㅎㅏ-ㅣ가-힣]/.test(content)
|
|
27
25
|
signals.push({
|
|
@@ -30,15 +28,11 @@ export function extractStyleSignals(
|
|
|
30
28
|
evidence: hasKoreanComment ? "comments in Korean" : "comments in English",
|
|
31
29
|
})
|
|
32
30
|
}
|
|
33
|
-
|
|
34
|
-
// Indentation
|
|
35
31
|
if (content.includes(" ") && !content.includes("\t")) {
|
|
36
32
|
signals.push({ language: ext, pattern: "indent", evidence: "2-space indentation" })
|
|
37
33
|
} else if (content.includes("\t")) {
|
|
38
34
|
signals.push({ language: ext, pattern: "indent", evidence: "tab indentation" })
|
|
39
35
|
}
|
|
40
|
-
|
|
41
|
-
// Semicolons
|
|
42
36
|
if (ext === "ts" || ext === "js") {
|
|
43
37
|
const hasSemicolons = /;\s*\n/.test(content)
|
|
44
38
|
signals.push({
|
|
@@ -47,8 +41,6 @@ export function extractStyleSignals(
|
|
|
47
41
|
evidence: hasSemicolons ? "uses semicolons" : "no semicolons",
|
|
48
42
|
})
|
|
49
43
|
}
|
|
50
|
-
|
|
51
|
-
// Quote style
|
|
52
44
|
const singleQuotes = (content.match(/'/g) ?? []).length
|
|
53
45
|
const doubleQuotes = (content.match(/"/g) ?? []).length
|
|
54
46
|
if (singleQuotes > doubleQuotes * 2) {
|
|
@@ -56,8 +48,6 @@ export function extractStyleSignals(
|
|
|
56
48
|
} else if (doubleQuotes > singleQuotes * 2) {
|
|
57
49
|
signals.push({ language: ext, pattern: "quotes", evidence: "prefers double quotes" })
|
|
58
50
|
}
|
|
59
|
-
|
|
60
|
-
// Naming: camelCase vs snake_case
|
|
61
51
|
const camelCase = (content.match(/[a-z][A-Z]/g) ?? []).length
|
|
62
52
|
const snakeCase = (content.match(/_[a-z]/g) ?? []).length
|
|
63
53
|
if (camelCase > snakeCase * 3) {
|
|
@@ -99,4 +89,4 @@ export async function fromStyleSignals(
|
|
|
99
89
|
}
|
|
100
90
|
|
|
101
91
|
return memories
|
|
102
|
-
}
|
|
92
|
+
}
|
package/src/ltm/pipeline.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** LTM — background collection pipeline */
|
|
1
|
+
/** LTM — background collection pipeline */
|
|
2
2
|
|
|
3
3
|
import { Effect } from "effect"
|
|
4
4
|
import * as Log from "@saeeol/core/util/log"
|
|
@@ -19,8 +19,6 @@ let running = false
|
|
|
19
19
|
let config: LTMConfig | undefined
|
|
20
20
|
let unsubscribers: Array<() => void> = []
|
|
21
21
|
|
|
22
|
-
// ── Pipeline lifecycle ──
|
|
23
|
-
|
|
24
22
|
/** Start the pipeline */
|
|
25
23
|
export async function start(cfg: LTMConfig, bake: LLMBakeParams): Promise<void> {
|
|
26
24
|
if (running) return
|
|
@@ -31,24 +29,18 @@ export async function start(cfg: LTMConfig, bake: LLMBakeParams): Promise<void>
|
|
|
31
29
|
|
|
32
30
|
log.info("pipeline starting", { model: bake.embeddingModel })
|
|
33
31
|
|
|
34
|
-
// Start embedding server
|
|
35
32
|
const server = await Embedder.start(bake)
|
|
36
33
|
if (server.status !== "running") {
|
|
37
34
|
log.error("embedding server failed to start, pipeline disabled")
|
|
38
35
|
running = false
|
|
39
36
|
return
|
|
40
37
|
}
|
|
41
|
-
|
|
42
|
-
// Prune old memories
|
|
43
|
-
if (cfg.episodic.enabled) {
|
|
44
|
-
const pruned = await Store.prune(cfg.episodic.retainDays * 24 * 60 * 60 * 1000)
|
|
38
|
+
if (cfg.episodic.enabled) { const pruned = await Store.prune(cfg.episodic.retainDays * 24 * 60 * 60 * 1000)
|
|
45
39
|
if (pruned > 0) {
|
|
46
40
|
log.info("pruned old episodic memories", { count: pruned })
|
|
47
41
|
void Bus.publish(LTMEvent.MemoryPruned, { count: pruned, type: "episodic" })
|
|
48
42
|
}
|
|
49
43
|
}
|
|
50
|
-
|
|
51
|
-
// Enforce memory limit
|
|
52
44
|
const count = await Store.count()
|
|
53
45
|
if (count > cfg.maxMemories) {
|
|
54
46
|
const excess = count - cfg.maxMemories
|
|
@@ -58,8 +50,6 @@ export async function start(cfg: LTMConfig, bake: LLMBakeParams): Promise<void>
|
|
|
58
50
|
await Store.remove(toRemove)
|
|
59
51
|
log.info("trimmed memories to max", { removed: toRemove.length })
|
|
60
52
|
}
|
|
61
|
-
|
|
62
|
-
// Bind BusEvent subscriptions
|
|
63
53
|
bindSubscriptions()
|
|
64
54
|
|
|
65
55
|
log.info("pipeline started", { memoryCount: await Store.count() })
|
|
@@ -69,8 +59,6 @@ export async function start(cfg: LTMConfig, bake: LLMBakeParams): Promise<void>
|
|
|
69
59
|
export async function stop(): Promise<void> {
|
|
70
60
|
if (!running) return
|
|
71
61
|
running = false
|
|
72
|
-
|
|
73
|
-
// Unsubscribe
|
|
74
62
|
for (const unsub of unsubscribers) {
|
|
75
63
|
try { unsub() } catch { /* ignore */ }
|
|
76
64
|
}
|
|
@@ -86,8 +74,6 @@ export function isActive(): boolean {
|
|
|
86
74
|
return running
|
|
87
75
|
}
|
|
88
76
|
|
|
89
|
-
// ── BusEvent subscription binding ──
|
|
90
|
-
|
|
91
77
|
function bindSubscriptions() {
|
|
92
78
|
if (!config) return
|
|
93
79
|
|
|
@@ -169,8 +155,6 @@ function bindSubscriptions() {
|
|
|
169
155
|
log.info("bus subscriptions bound", { count: unsubscribers.length })
|
|
170
156
|
}
|
|
171
157
|
|
|
172
|
-
// ── Direct-call handlers (called explicitly from external modules) ──
|
|
173
|
-
|
|
174
158
|
/** Conversation message → episodic memory */
|
|
175
159
|
export async function onMessageCompleted(
|
|
176
160
|
sessionID: string,
|
|
@@ -254,4 +238,4 @@ export async function onCodeEdit(
|
|
|
254
238
|
for (const memory of memories) {
|
|
255
239
|
await Store.upsert(memory)
|
|
256
240
|
}
|
|
257
|
-
}
|
|
241
|
+
}
|
package/src/ltm/scheduler.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** LTM — VRAM/task scheduler */
|
|
1
|
+
/** LTM — VRAM/task scheduler */
|
|
2
2
|
|
|
3
3
|
import * as Log from "@saeeol/core/util/log"
|
|
4
4
|
import type { HardwareProfile } from "./types"
|
|
@@ -18,8 +18,6 @@ export interface Allocation {
|
|
|
18
18
|
/** Scheduling strategy */
|
|
19
19
|
export type Strategy = "concurrent" | "alternating" | "cpu-fallback" | "no-gpu"
|
|
20
20
|
|
|
21
|
-
// ── VRAM gauge ──
|
|
22
|
-
|
|
23
21
|
/** Query current VRAM allocation state */
|
|
24
22
|
export async function allocation(): Promise<Allocation> {
|
|
25
23
|
const gpu = await Effect.runPromise(GPU.profile)
|
|
@@ -40,14 +38,11 @@ export async function strategy(): Promise<Strategy> {
|
|
|
40
38
|
return "cpu-fallback"
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
// ── Execution decisions ──
|
|
44
|
-
|
|
45
41
|
/** Whether embedding background work can run */
|
|
46
42
|
export async function canRunEmbedding(): Promise<boolean> {
|
|
47
43
|
const strat = await strategy()
|
|
48
44
|
// no-gpu or cpu-fallback: always allowed (CPU processing)
|
|
49
45
|
if (strat === "no-gpu" || strat === "cpu-fallback") return true
|
|
50
|
-
// concurrent: always allowed
|
|
51
46
|
if (strat === "concurrent") return true
|
|
52
47
|
// alternating: need at least 2GB VRAM
|
|
53
48
|
const alloc = await allocation()
|
|
@@ -61,20 +56,13 @@ export async function canRunConcurrent(hw: HardwareProfile): Promise<boolean> {
|
|
|
61
56
|
return false
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
// ── LLM ↔ embedding time-sharing ──
|
|
65
|
-
|
|
66
59
|
/** Called when LLM requests VRAM — pauses embedding if needed */
|
|
67
60
|
export async function requestLLM(vramNeededMB: number): Promise<boolean> {
|
|
68
61
|
const strat = await strategy()
|
|
69
|
-
|
|
70
|
-
// concurrent strategy: both can run
|
|
71
62
|
if (strat === "concurrent") return true
|
|
72
|
-
// no-gpu: GPU not needed
|
|
73
63
|
if (strat === "no-gpu") return true
|
|
74
64
|
|
|
75
65
|
const alloc = await allocation()
|
|
76
|
-
|
|
77
|
-
// Sufficient VRAM — proceed as-is
|
|
78
66
|
if (alloc.available >= vramNeededMB) {
|
|
79
67
|
log.info("LLM request: enough VRAM", { available: alloc.available, needed: vramNeededMB })
|
|
80
68
|
return true
|
|
@@ -126,4 +114,4 @@ export async function resumeEmbedding(bake: {
|
|
|
126
114
|
} catch (e) {
|
|
127
115
|
log.error("failed to resume embedding server", { error: e })
|
|
128
116
|
}
|
|
129
|
-
}
|
|
117
|
+
}
|
package/src/ltm/store.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** LTM — filesystem-based vector store */
|
|
1
|
+
/** LTM — filesystem-based vector store */
|
|
2
2
|
|
|
3
3
|
import path from "path"
|
|
4
4
|
import { mkdir, readFile, writeFile, readdir, rm, stat } from "fs/promises"
|
|
@@ -8,8 +8,6 @@ import type { Memory, MemoryType } from "./types"
|
|
|
8
8
|
|
|
9
9
|
const log = Log.create({ service: "ltm/store" })
|
|
10
10
|
|
|
11
|
-
// ── Cosine similarity ──
|
|
12
|
-
|
|
13
11
|
function cosine(a: number[], b: number[]): number {
|
|
14
12
|
let dot = 0
|
|
15
13
|
let na = 0
|
|
@@ -24,14 +22,11 @@ function cosine(a: number[], b: number[]): number {
|
|
|
24
22
|
return denom === 0 ? 0 : dot / denom
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
// ── File paths ──
|
|
28
|
-
|
|
29
25
|
function storeDir(): string {
|
|
30
26
|
return path.join(Global.Path.data, "ltm", "memories")
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
function memoryPath(id: string): string {
|
|
34
|
-
// Sanitize special characters for safe filenames
|
|
35
30
|
const safe = id.replace(/[:<>\"|?*]/g, "_")
|
|
36
31
|
return path.join(storeDir(), `${safe}.json`)
|
|
37
32
|
}
|
|
@@ -44,8 +39,6 @@ async function ensure(): Promise<void> {
|
|
|
44
39
|
await mkdir(storeDir(), { recursive: true })
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
// ── Index ──
|
|
48
|
-
|
|
49
42
|
interface Index {
|
|
50
43
|
memories: Array<{ id: string; type: MemoryType; timestamp: number; source: string }>
|
|
51
44
|
}
|
|
@@ -64,8 +57,6 @@ async function writeIndex(idx: Index): Promise<void> {
|
|
|
64
57
|
await writeFile(indexPath(), JSON.stringify(idx, null, 2))
|
|
65
58
|
}
|
|
66
59
|
|
|
67
|
-
// ── Public API ──
|
|
68
|
-
|
|
69
60
|
export async function upsert(memory: Memory): Promise<void> {
|
|
70
61
|
await ensure()
|
|
71
62
|
await writeFile(memoryPath(memory.id), JSON.stringify(memory, null, 2))
|
|
@@ -149,4 +140,4 @@ export async function prune(olderThanMs: number): Promise<number> {
|
|
|
149
140
|
export async function count(): Promise<number> {
|
|
150
141
|
const idx = await readIndex()
|
|
151
142
|
return idx.memories.length
|
|
152
|
-
}
|
|
143
|
+
}
|
package/src/ltm/types.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
/** LTM — long-term memory type definitions */
|
|
1
|
+
/** LTM — long-term memory type definitions */
|
|
2
2
|
|
|
3
3
|
import { Schema } from "effect"
|
|
4
4
|
import { optionalOmitUndefined } from "@/util/schema"
|
|
5
5
|
|
|
6
|
-
// ── Memory types ──
|
|
7
6
|
|
|
8
7
|
export const MemoryType = Schema.Literals(["episodic", "semantic", "procedural"])
|
|
9
8
|
export type MemoryType = Schema.Schema.Type<typeof MemoryType>
|
|
10
9
|
|
|
11
|
-
// ── Memory entry ──
|
|
12
10
|
|
|
13
11
|
export const MemoryMetadata = Schema.Struct({
|
|
14
12
|
source: Schema.String,
|
|
@@ -30,7 +28,6 @@ export const Memory = Schema.Struct({
|
|
|
30
28
|
})
|
|
31
29
|
export type Memory = Schema.Schema.Type<typeof Memory>
|
|
32
30
|
|
|
33
|
-
// ── Embedding server ──
|
|
34
31
|
|
|
35
32
|
export const EmbedderStatus = Schema.Literals(["stopped", "starting", "running", "error"])
|
|
36
33
|
export type EmbedderStatus = Schema.Schema.Type<typeof EmbedderStatus>
|
|
@@ -45,7 +42,6 @@ export const EmbeddingServer = Schema.Struct({
|
|
|
45
42
|
})
|
|
46
43
|
export type EmbeddingServer = Schema.Schema.Type<typeof EmbeddingServer>
|
|
47
44
|
|
|
48
|
-
// ── Hardware profile ──
|
|
49
45
|
|
|
50
46
|
export const HardwareProfile = Schema.Struct({
|
|
51
47
|
gpuCount: Schema.Number,
|
|
@@ -57,7 +53,6 @@ export const HardwareProfile = Schema.Struct({
|
|
|
57
53
|
})
|
|
58
54
|
export type HardwareProfile = Schema.Schema.Type<typeof HardwareProfile>
|
|
59
55
|
|
|
60
|
-
// ── LLM parameters (deterministic) ──
|
|
61
56
|
|
|
62
57
|
export const LLMBakeParams = Schema.Struct({
|
|
63
58
|
/** Embedding model ID */
|
|
@@ -79,7 +74,6 @@ export const LLMBakeParams = Schema.Struct({
|
|
|
79
74
|
})
|
|
80
75
|
export type LLMBakeParams = Schema.Schema.Type<typeof LLMBakeParams>
|
|
81
76
|
|
|
82
|
-
// ── LTM configuration ──
|
|
83
77
|
|
|
84
78
|
export const LTMConfig = Schema.Struct({
|
|
85
79
|
enabled: Schema.Boolean,
|
|
@@ -105,4 +99,4 @@ export const LTMConfig = Schema.Struct({
|
|
|
105
99
|
maxTokens: Schema.Number,
|
|
106
100
|
}),
|
|
107
101
|
})
|
|
108
|
-
export type LTMConfig = Schema.Schema.Type<typeof LTMConfig>
|
|
102
|
+
export type LTMConfig = Schema.Schema.Type<typeof LTMConfig>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Local embedding server — Ollama-based embedding model lifecycle management */
|
|
1
|
+
/** Local embedding server — Ollama-based embedding model lifecycle management */
|
|
2
2
|
|
|
3
3
|
import { Effect } from "effect"
|
|
4
4
|
import * as Log from "@saeeol/core/util/log"
|
|
@@ -17,8 +17,6 @@ const log = Log.create({ service: "local/embedder" })
|
|
|
17
17
|
|
|
18
18
|
let server: EmbeddingServer | undefined
|
|
19
19
|
|
|
20
|
-
// ── Ollama status checks ──
|
|
21
|
-
|
|
22
20
|
async function isOllamaRunning(endpoint: string): Promise<boolean> {
|
|
23
21
|
try {
|
|
24
22
|
const res = await fetch(`${endpoint}/api/tags`, { signal: AbortSignal.timeout(3000) })
|
|
@@ -53,8 +51,6 @@ async function isModelInstalled(endpoint: string, model: string): Promise<boolea
|
|
|
53
51
|
}
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
// ── Pull model from Ollama ──
|
|
57
|
-
|
|
58
54
|
async function pullModel(endpoint: string, model: string): Promise<void> {
|
|
59
55
|
log.info("pulling embedding model", { model })
|
|
60
56
|
const ollamaModel = getOllamaModelName(model)
|
|
@@ -91,8 +87,6 @@ function getOllamaModelName(modelId: string): string {
|
|
|
91
87
|
return map[modelId] ?? modelId
|
|
92
88
|
}
|
|
93
89
|
|
|
94
|
-
// ── Embedding API call ──
|
|
95
|
-
|
|
96
90
|
async function embedViaOllama(endpoint: string, model: string, texts: string[]): Promise<number[][]> {
|
|
97
91
|
const ollamaModel = getOllamaModelName(model)
|
|
98
92
|
const res = await fetch(`${endpoint}/api/embed`, {
|
|
@@ -114,16 +108,12 @@ async function embedViaOllama(endpoint: string, model: string, texts: string[]):
|
|
|
114
108
|
return data.embeddings
|
|
115
109
|
}
|
|
116
110
|
|
|
117
|
-
// ── VRAM estimation ──
|
|
118
|
-
|
|
119
111
|
function estimateEmbeddingVRAM(modelId: string): number {
|
|
120
112
|
const model = RAG.EMBEDDING_MODELS.find((m) => m.id === modelId)
|
|
121
113
|
if (!model) return 200
|
|
122
114
|
return Math.ceil(model.sizeBytes * 1.2 / (1024 * 1024))
|
|
123
115
|
}
|
|
124
116
|
|
|
125
|
-
// ── Public API ──
|
|
126
|
-
|
|
127
117
|
/** Start the embedding server */
|
|
128
118
|
export async function start(bake: LLMBakeParams): Promise<EmbeddingServer> {
|
|
129
119
|
if (server && server.status === "running") {
|
|
@@ -146,8 +136,6 @@ export async function start(bake: LLMBakeParams): Promise<EmbeddingServer> {
|
|
|
146
136
|
}
|
|
147
137
|
|
|
148
138
|
void Bus.publish(LTMEvent.EmbedderStatusChanged, { status: "starting", model: bake.embeddingModel })
|
|
149
|
-
|
|
150
|
-
// Check if Ollama is running
|
|
151
139
|
const running = await isOllamaRunning(endpoint)
|
|
152
140
|
if (!running) {
|
|
153
141
|
log.warn("Ollama not running, embedding server unavailable", { endpoint })
|
|
@@ -155,16 +143,12 @@ export async function start(bake: LLMBakeParams): Promise<EmbeddingServer> {
|
|
|
155
143
|
void Bus.publish(LTMEvent.EmbedderStatusChanged, { status: "error", model: bake.embeddingModel })
|
|
156
144
|
return server
|
|
157
145
|
}
|
|
158
|
-
|
|
159
|
-
// Check if model is installed
|
|
160
146
|
const ollamaModel = getOllamaModelName(bake.embeddingModel)
|
|
161
147
|
const installed = await isModelInstalled(endpoint, ollamaModel)
|
|
162
148
|
if (!installed) {
|
|
163
149
|
log.info("model not installed, pulling", { model: ollamaModel })
|
|
164
150
|
await pullModel(endpoint, ollamaModel)
|
|
165
151
|
}
|
|
166
|
-
|
|
167
|
-
// Check if model is loaded (auto-loaded on first call)
|
|
168
152
|
const loaded = await isModelLoaded(endpoint, ollamaModel)
|
|
169
153
|
if (!loaded) {
|
|
170
154
|
// Warmup call — load model into memory
|
|
@@ -217,4 +201,4 @@ export async function embedOne(text: string): Promise<number[]> {
|
|
|
217
201
|
/** VRAM usage in MB */
|
|
218
202
|
export function vramUsage(): number {
|
|
219
203
|
return server?.vramMB ?? 0
|
|
220
|
-
}
|
|
204
|
+
}
|
package/src/session/core/llm.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Provider } from "@/provider/provider"
|
|
1
|
+
import { Provider } from "@/provider/provider"
|
|
2
2
|
import * as Log from "@saeeol/core/util/log"
|
|
3
3
|
import { Context, Effect, Layer, Record } from "effect"
|
|
4
4
|
import * as Stream from "effect/Stream"
|
|
@@ -119,11 +119,8 @@ const live: Layer.Layer<
|
|
|
119
119
|
system.push(
|
|
120
120
|
[
|
|
121
121
|
...(isOpenaiOauth ? [] : [SystemPrompt.soul()]),
|
|
122
|
-
// use agent prompt otherwise provider prompt
|
|
123
122
|
...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),
|
|
124
|
-
// any custom prompt passed into this call
|
|
125
123
|
...input.system,
|
|
126
|
-
// any custom prompt from last user message
|
|
127
124
|
...(input.user.system ? [input.user.system] : []),
|
|
128
125
|
]
|
|
129
126
|
.filter((x) => x)
|
|
@@ -435,7 +432,6 @@ const live: Layer.Layer<
|
|
|
435
432
|
specificationVersion: "v3" as const,
|
|
436
433
|
async transformParams(args) {
|
|
437
434
|
if (args.type === "stream") {
|
|
438
|
-
// @ts-expect-error
|
|
439
435
|
args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options)
|
|
440
436
|
}
|
|
441
437
|
return args.params
|
|
@@ -501,4 +497,4 @@ export function hasToolCalls(messages: ModelMessage[]): boolean {
|
|
|
501
497
|
return false
|
|
502
498
|
}
|
|
503
499
|
|
|
504
|
-
export * as LLM from "./llm"
|
|
500
|
+
export * as LLM from "./llm"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NamedError } from "@saeeol/core/util/error"
|
|
1
|
+
import type { NamedError } from "@saeeol/core/util/error"
|
|
2
2
|
import { Cause, Clock, Duration, Effect, Schedule } from "effect"
|
|
3
3
|
import { MessageV2 } from "../message/message-v2"
|
|
4
4
|
import { isSaeeolError } from "@/saeeol/errors"
|
|
@@ -36,10 +36,8 @@ export function delay(attempt: number, error?: MessageV2.APIError) {
|
|
|
36
36
|
if (retryAfter) {
|
|
37
37
|
const parsedSeconds = Number.parseFloat(retryAfter)
|
|
38
38
|
if (!Number.isNaN(parsedSeconds)) {
|
|
39
|
-
// convert seconds to milliseconds
|
|
40
39
|
return cap(Math.ceil(parsedSeconds * 1000))
|
|
41
40
|
}
|
|
42
|
-
// Try parsing as HTTP date format
|
|
43
41
|
const parsed = Date.parse(retryAfter) - Date.now()
|
|
44
42
|
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
45
43
|
return cap(Math.ceil(parsed))
|
|
@@ -68,8 +66,6 @@ export function retryable(error: Err) {
|
|
|
68
66
|
if (error.data.responseBody?.includes("FreeUsageLimitError")) return undefined
|
|
69
67
|
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
|
|
70
68
|
}
|
|
71
|
-
|
|
72
|
-
// Check for rate limit patterns in plain text error messages
|
|
73
69
|
const msg = error.data?.message
|
|
74
70
|
if (typeof msg === "string") {
|
|
75
71
|
const lower = msg.toLowerCase()
|
|
@@ -146,4 +142,4 @@ export function policy(opts: {
|
|
|
146
142
|
)
|
|
147
143
|
}
|
|
148
144
|
|
|
149
|
-
export * as SessionRetry from "./retry"
|
|
145
|
+
export * as SessionRetry from "./retry"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-events.ts — single source for SyncEvent/BusEvent definitions
|
|
3
|
+
*
|
|
4
|
+
* Shared by core/session.ts and core/session-types.ts.
|
|
5
|
+
* Does not import other core/session-* files, so no circular deps.
|
|
6
|
+
* TurnOpen/TurnClose require overlay deps, so each consumer adds them directly.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { BusEvent } from "@/bus/bus-event"
|
|
10
|
+
import { SyncEvent } from "../../sync"
|
|
11
|
+
import { SessionID } from "./schema"
|
|
12
|
+
import { Snapshot } from "@/snapshot"
|
|
13
|
+
import { MessageV2 } from "../message/message-v2"
|
|
14
|
+
import { Schema } from "effect"
|
|
15
|
+
import { Permission } from "@/permission"
|
|
16
|
+
import { ProjectID } from "../../project/schema"
|
|
17
|
+
import { WorkspaceID } from "../../control-plane/schema"
|
|
18
|
+
import { NonNegativeInt, optionalOmitUndefined } from "@/util/schema"
|
|
19
|
+
|
|
20
|
+
// ── Info schema (self-contained, mirrors session-types for event registration) ──
|
|
21
|
+
|
|
22
|
+
const Summary = Schema.Struct({
|
|
23
|
+
additions: NonNegativeInt,
|
|
24
|
+
deletions: NonNegativeInt,
|
|
25
|
+
files: NonNegativeInt,
|
|
26
|
+
diffs: optionalOmitUndefined(Schema.Array(Snapshot.SummaryFileDiff)),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const Share = Schema.Struct({ url: Schema.String })
|
|
30
|
+
|
|
31
|
+
const ArchivedTimestamp = Schema.Finite
|
|
32
|
+
|
|
33
|
+
const Revert = Schema.Struct({
|
|
34
|
+
messageID: SessionID,
|
|
35
|
+
partID: optionalOmitUndefined(SessionID),
|
|
36
|
+
snapshot: optionalOmitUndefined(Schema.String),
|
|
37
|
+
diff: optionalOmitUndefined(Schema.String),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const Time = Schema.Struct({
|
|
41
|
+
created: NonNegativeInt,
|
|
42
|
+
updated: NonNegativeInt,
|
|
43
|
+
compacting: optionalOmitUndefined(NonNegativeInt),
|
|
44
|
+
archived: optionalOmitUndefined(ArchivedTimestamp),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const Info = Schema.Struct({
|
|
48
|
+
id: SessionID,
|
|
49
|
+
slug: Schema.String,
|
|
50
|
+
projectID: ProjectID,
|
|
51
|
+
workspaceID: optionalOmitUndefined(WorkspaceID),
|
|
52
|
+
directory: Schema.String,
|
|
53
|
+
path: optionalOmitUndefined(Schema.String),
|
|
54
|
+
parentID: optionalOmitUndefined(SessionID),
|
|
55
|
+
summary: optionalOmitUndefined(Summary),
|
|
56
|
+
share: optionalOmitUndefined(Share),
|
|
57
|
+
title: Schema.String,
|
|
58
|
+
version: Schema.String,
|
|
59
|
+
time: Time,
|
|
60
|
+
permission: optionalOmitUndefined(Permission.Ruleset),
|
|
61
|
+
revert: optionalOmitUndefined(Revert),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// ── Update event schemas ──
|
|
65
|
+
|
|
66
|
+
const UpdatedShare = Schema.Struct({
|
|
67
|
+
url: Schema.optional(Schema.NullOr(Schema.String)),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const UpdatedTime = Schema.Struct({
|
|
71
|
+
created: Schema.optional(Schema.NullOr(NonNegativeInt)),
|
|
72
|
+
updated: Schema.optional(Schema.NullOr(NonNegativeInt)),
|
|
73
|
+
compacting: Schema.optional(Schema.NullOr(NonNegativeInt)),
|
|
74
|
+
archived: Schema.optional(Schema.NullOr(ArchivedTimestamp)),
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const UpdatedInfo = Schema.Struct({
|
|
78
|
+
id: Schema.optional(Schema.NullOr(SessionID)),
|
|
79
|
+
slug: Schema.optional(Schema.NullOr(Schema.String)),
|
|
80
|
+
projectID: Schema.optional(Schema.NullOr(ProjectID)),
|
|
81
|
+
workspaceID: Schema.optional(Schema.NullOr(WorkspaceID)),
|
|
82
|
+
directory: Schema.optional(Schema.NullOr(Schema.String)),
|
|
83
|
+
path: Schema.optional(Schema.NullOr(Schema.String)),
|
|
84
|
+
parentID: Schema.optional(Schema.NullOr(SessionID)),
|
|
85
|
+
summary: Schema.optional(Schema.NullOr(Summary)),
|
|
86
|
+
share: Schema.optional(UpdatedShare),
|
|
87
|
+
title: Schema.optional(Schema.NullOr(Schema.String)),
|
|
88
|
+
version: Schema.optional(Schema.NullOr(Schema.String)),
|
|
89
|
+
time: Schema.optional(UpdatedTime),
|
|
90
|
+
permission: Schema.optional(Schema.NullOr(Permission.Ruleset)),
|
|
91
|
+
revert: Schema.optional(Schema.NullOr(Revert)),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
export const CreatedEventSchema = Schema.Struct({
|
|
95
|
+
sessionID: SessionID,
|
|
96
|
+
info: Info,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
export const UpdatedEventSchema = Schema.Struct({
|
|
100
|
+
sessionID: SessionID,
|
|
101
|
+
info: UpdatedInfo,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// ── Event definitions (singleton registration) ──
|
|
105
|
+
|
|
106
|
+
export const SyncEvents = {
|
|
107
|
+
Created: SyncEvent.define({
|
|
108
|
+
type: "session.created",
|
|
109
|
+
version: 1,
|
|
110
|
+
aggregate: "sessionID",
|
|
111
|
+
schema: CreatedEventSchema,
|
|
112
|
+
}),
|
|
113
|
+
Updated: SyncEvent.define({
|
|
114
|
+
type: "session.updated",
|
|
115
|
+
version: 1,
|
|
116
|
+
aggregate: "sessionID",
|
|
117
|
+
schema: UpdatedEventSchema,
|
|
118
|
+
busSchema: CreatedEventSchema,
|
|
119
|
+
}),
|
|
120
|
+
Deleted: SyncEvent.define({
|
|
121
|
+
type: "session.deleted",
|
|
122
|
+
version: 1,
|
|
123
|
+
aggregate: "sessionID",
|
|
124
|
+
schema: CreatedEventSchema,
|
|
125
|
+
}),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const BusEvents = {
|
|
129
|
+
Diff: BusEvent.define(
|
|
130
|
+
"session.diff",
|
|
131
|
+
Schema.Struct({
|
|
132
|
+
sessionID: SessionID,
|
|
133
|
+
diff: Schema.Array(Snapshot.FileDiff),
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
Error: BusEvent.define(
|
|
137
|
+
"session.error",
|
|
138
|
+
Schema.Struct({
|
|
139
|
+
sessionID: Schema.optional(SessionID),
|
|
140
|
+
error: MessageV2.Assistant.fields.error,
|
|
141
|
+
}),
|
|
142
|
+
),
|
|
143
|
+
}
|
|
@@ -19,6 +19,7 @@ import { SaeeolSession } from "@/saeeol/session"
|
|
|
19
19
|
import { Effect, Schema, Types } from "effect"
|
|
20
20
|
import { zod } from "@/util/effect-zod"
|
|
21
21
|
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@/util/schema"
|
|
22
|
+
import { SyncEvents, BusEvents } from "./session-events"
|
|
22
23
|
|
|
23
24
|
const log = Log.create({ service: "session" })
|
|
24
25
|
|
|
@@ -70,11 +71,11 @@ export function sessionPath(worktree: string, cwd: string) {
|
|
|
70
71
|
return path.relative(path.resolve(worktree), cwd).replaceAll("\\", "/")
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
const Summary = Schema.Struct({ additions: NonNegativeInt, deletions: NonNegativeInt, files: NonNegativeInt, diffs: optionalOmitUndefined(Schema.Array(Snapshot.SummaryFileDiff)) })
|
|
74
|
+
export const Summary = Schema.Struct({ additions: NonNegativeInt, deletions: NonNegativeInt, files: NonNegativeInt, diffs: optionalOmitUndefined(Schema.Array(Snapshot.SummaryFileDiff)) })
|
|
74
75
|
const Share = Schema.Struct({ url: Schema.String })
|
|
75
76
|
export const ArchivedTimestamp = Schema.Finite
|
|
76
77
|
const Time = Schema.Struct({ created: NonNegativeInt, updated: NonNegativeInt, compacting: optionalOmitUndefined(NonNegativeInt), archived: optionalOmitUndefined(ArchivedTimestamp) })
|
|
77
|
-
const Revert = Schema.Struct({ messageID: MessageID, partID: optionalOmitUndefined(PartID), snapshot: optionalOmitUndefined(Schema.String), diff: optionalOmitUndefined(Schema.String) })
|
|
78
|
+
export const Revert = Schema.Struct({ messageID: MessageID, partID: optionalOmitUndefined(PartID), snapshot: optionalOmitUndefined(Schema.String), diff: optionalOmitUndefined(Schema.String) })
|
|
78
79
|
|
|
79
80
|
export const Info = Schema.Struct({
|
|
80
81
|
id: SessionID, slug: Schema.String, projectID: ProjectID, workspaceID: optionalOmitUndefined(WorkspaceID),
|
|
@@ -125,11 +126,8 @@ const UpdatedInfo = Schema.Struct({
|
|
|
125
126
|
const UpdatedEventSchema = Schema.Struct({ sessionID: SessionID, info: UpdatedInfo })
|
|
126
127
|
|
|
127
128
|
export const Event = {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Deleted: SyncEvent.define({ type: "session.deleted", version: 1, aggregate: "sessionID", schema: CreatedEventSchema }),
|
|
131
|
-
Diff: BusEvent.define("session.diff", Schema.Struct({ sessionID: SessionID, diff: Schema.Array(Snapshot.FileDiff) })),
|
|
132
|
-
Error: BusEvent.define("session.error", Schema.Struct({ sessionID: Schema.optional(SessionID), error: MessageV2.Assistant.fields.error })),
|
|
129
|
+
...SyncEvents,
|
|
130
|
+
...BusEvents,
|
|
133
131
|
TurnOpen: SaeeolSession.Event.TurnOpen,
|
|
134
132
|
TurnClose: SaeeolSession.Event.TurnClose,
|
|
135
133
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Slug } from "@saeeol/core/util/slug"
|
|
1
|
+
import { Slug } from "@saeeol/core/util/slug"
|
|
2
2
|
import path from "path"
|
|
3
3
|
import { BusEvent } from "@/bus/bus-event"
|
|
4
4
|
import { Bus } from "@/bus"
|
|
@@ -33,6 +33,7 @@ import { fn } from "@/util/fn"
|
|
|
33
33
|
import { Effect, Layer, Option, Context, Schema, Types } from "effect"
|
|
34
34
|
import { zod } from "@/util/effect-zod"
|
|
35
35
|
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@/util/schema"
|
|
36
|
+
import { SyncEvents, BusEvents } from "./session-events"
|
|
36
37
|
|
|
37
38
|
const log = Log.create({ service: "session" })
|
|
38
39
|
|
|
@@ -279,7 +280,12 @@ const UpdatedEventSchema = Schema.Struct({
|
|
|
279
280
|
info: UpdatedInfo,
|
|
280
281
|
})
|
|
281
282
|
|
|
282
|
-
export
|
|
283
|
+
export const Event = {
|
|
284
|
+
...SyncEvents,
|
|
285
|
+
...BusEvents,
|
|
286
|
+
TurnOpen: SaeeolSession.Event.TurnOpen,
|
|
287
|
+
TurnClose: SaeeolSession.Event.TurnClose,
|
|
288
|
+
}
|
|
283
289
|
|
|
284
290
|
export function plan(input: { slug: string; time: { created: number } }, instance: InstanceContext) {
|
|
285
291
|
const base = instance.project.vcs
|
|
@@ -310,11 +316,8 @@ export const getUsage = (input: {
|
|
|
310
316
|
input.usage.inputTokenDetails?.cacheWriteTokens ??
|
|
311
317
|
input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
|
|
312
318
|
// google-vertex-anthropic returns metadata under "vertex" key
|
|
313
|
-
// (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages')
|
|
314
319
|
input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ??
|
|
315
|
-
// @ts-expect-error
|
|
316
320
|
input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
|
|
317
|
-
// @ts-expect-error
|
|
318
321
|
input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ??
|
|
319
322
|
0,
|
|
320
323
|
),
|
|
@@ -802,9 +805,6 @@ function* listByProject(
|
|
|
802
805
|
)
|
|
803
806
|
}
|
|
804
807
|
} else if (input.scope !== "project" && !Flag.SAEEOL_EXPERIMENTAL_WORKSPACES) {
|
|
805
|
-
// if (input.directory) {
|
|
806
|
-
// conditions.push(eq(SessionTable.directory, input.directory))
|
|
807
|
-
// }
|
|
808
808
|
}
|
|
809
809
|
if (input.roots) {
|
|
810
810
|
conditions.push(isNull(SessionTable.parent_id))
|
|
@@ -907,4 +907,4 @@ export const updatePartDelta = fn(
|
|
|
907
907
|
(input) => runPromise((svc) => svc.updatePartDelta(input)),
|
|
908
908
|
)
|
|
909
909
|
|
|
910
|
-
export * as Session from "./session"
|
|
910
|
+
export * as Session from "./session"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Error types + fromError + OutputFormat — extracted from message-v2.ts */
|
|
1
|
+
/** Error types + fromError + OutputFormat — extracted from message-v2.ts */
|
|
2
2
|
|
|
3
3
|
import { APICallError, LoadAPIKeyError } from "ai"
|
|
4
4
|
import { NamedError } from "@saeeol/core/util/error"
|
|
@@ -37,8 +37,6 @@ const _Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotat
|
|
|
37
37
|
export { _Format }
|
|
38
38
|
export const Format = Object.assign(_Format, { zod: zod(_Format) })
|
|
39
39
|
export type OutputFormat = Schema.Schema.Type<typeof _Format>
|
|
40
|
-
|
|
41
|
-
// Assistant error union (Zod)
|
|
42
40
|
import z from "zod"
|
|
43
41
|
const AssistantErrorZod = z.discriminatedUnion("name", [
|
|
44
42
|
AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema, AbortedError.Schema,
|
|
@@ -46,8 +44,6 @@ const AssistantErrorZod = z.discriminatedUnion("name", [
|
|
|
46
44
|
])
|
|
47
45
|
export type AssistantError = z.infer<typeof AssistantErrorZod>
|
|
48
46
|
export { AssistantErrorZod }
|
|
49
|
-
|
|
50
|
-
// Assistant error union (Effect Schema)
|
|
51
47
|
export const AssistantErrorSchema = Schema.Union([
|
|
52
48
|
AuthError.EffectSchema,
|
|
53
49
|
Schema.Struct({ name: Schema.Literal("UnknownError"), data: Schema.Struct({ message: Schema.String }) }).annotate({ identifier: "UnknownError" }),
|
|
@@ -80,4 +76,4 @@ export function fromError(e: unknown, ctx: { providerID: ProviderID; aborted?: b
|
|
|
80
76
|
} catch {}
|
|
81
77
|
return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject()
|
|
82
78
|
}
|
|
83
|
-
}
|
|
79
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Part schemas + ToolState + Input schemas — extracted from message-v2.ts */
|
|
1
|
+
/** Part schemas + ToolState + Input schemas — extracted from message-v2.ts */
|
|
2
2
|
|
|
3
3
|
import { SessionID, MessageID, PartID } from "../core/schema"
|
|
4
4
|
import { LSP } from "@/lsp/lsp"
|
|
@@ -81,9 +81,7 @@ export const AgentPartInput = Schema.Struct({ id: Schema.optional(PartID), type:
|
|
|
81
81
|
export type AgentPartInput = Types.DeepMutable<Schema.Schema.Type<typeof AgentPartInput>>
|
|
82
82
|
export const SubtaskPartInput = Schema.Struct({ id: Schema.optional(PartID), type: Schema.Literal("subtask"), prompt: Schema.String, description: Schema.String, agent: Schema.String, model: Schema.optional(Schema.Struct({ providerID: ProviderID, modelID: ModelID })), command: Schema.optional(Schema.String) }).annotate({ identifier: "SubtaskPartInput" }).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
83
83
|
export type SubtaskPartInput = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPartInput>>
|
|
84
|
-
|
|
85
|
-
// Part union
|
|
86
84
|
const _Part = Schema.Union([TextPart, SubtaskPart, ReasoningPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart, PatchPart, AgentPart, RetryPart, CompactionPart]).annotate({ discriminator: "type", identifier: "Part" })
|
|
87
85
|
export const Part = Object.assign(_Part, { zod: zod(_Part) as unknown as z.ZodType<TextPart | SubtaskPart | ReasoningPart | FilePart | ToolPart | StepStartPart | StepFinishPart | SnapshotPart | PatchPart | AgentPart | RetryPart | CompactionPart> })
|
|
88
86
|
export type Part = TextPart | SubtaskPart | ReasoningPart | FilePart | ToolPart | StepStartPart | StepFinishPart | SnapshotPart | PatchPart | AgentPart | RetryPart | CompactionPart
|
|
89
|
-
export { _Part }
|
|
87
|
+
export { _Part }
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
globalThis.AI_SDK_LOG_WARNINGS = false
|
|
1
|
+
globalThis.AI_SDK_LOG_WARNINGS = false
|
|
3
2
|
|
|
4
3
|
import { Effect, Layer, Scope, Latch, Context } from "effect"
|
|
5
4
|
import { ChildProcessSpawner } from "effect/unstable/process"
|
|
@@ -208,4 +207,4 @@ export const prompt = (input: PromptInput) => runPromise((svc) => svc.prompt(inp
|
|
|
208
207
|
export const loopExport = (input: LoopInput) => runPromise((svc) => svc.loop(input))
|
|
209
208
|
export const cancel = (sessionID: SessionID) => runPromise((svc) => svc.cancel(sessionID))
|
|
210
209
|
|
|
211
|
-
export * as SessionPrompt from "./prompt"
|
|
210
|
+
export * as SessionPrompt from "./prompt"
|
|
@@ -37,8 +37,6 @@ export const ApplyPatchTool = Tool.define(
|
|
|
37
37
|
if (!params.patchText) {
|
|
38
38
|
return yield* Effect.fail(new Error("patchText is required"))
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
// Parse the patch to get hunks
|
|
42
40
|
let hunks: Patch.Hunk[]
|
|
43
41
|
try {
|
|
44
42
|
const parseResult = Patch.parsePatch(params.patchText)
|
|
@@ -56,8 +54,6 @@ export const ApplyPatchTool = Tool.define(
|
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
const instance = yield* InstanceState.context
|
|
59
|
-
|
|
60
|
-
// Validate file paths and check permissions
|
|
61
57
|
const fileChanges: Array<{
|
|
62
58
|
filePath: string
|
|
63
59
|
oldContent: string
|
|
@@ -109,7 +105,6 @@ export const ApplyPatchTool = Tool.define(
|
|
|
109
105
|
}
|
|
110
106
|
|
|
111
107
|
case "update": {
|
|
112
|
-
// Check if file exists for update
|
|
113
108
|
const stats = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
114
109
|
if (!stats || stats.type === "Directory") {
|
|
115
110
|
return yield* Effect.fail(
|
|
@@ -132,8 +127,6 @@ export const ApplyPatchTool = Tool.define(
|
|
|
132
127
|
let newContent = oldContent
|
|
133
128
|
let bom = source.bom
|
|
134
129
|
let encoding = read.encoding
|
|
135
|
-
|
|
136
|
-
// Apply the update chunks to get new content
|
|
137
130
|
try {
|
|
138
131
|
const fileUpdate = Patch.deriveNewContentsFromChunks(filePath, hunk.chunks)
|
|
139
132
|
newContent = fileUpdate.content
|
|
@@ -205,8 +198,6 @@ export const ApplyPatchTool = Tool.define(
|
|
|
205
198
|
}
|
|
206
199
|
}
|
|
207
200
|
}
|
|
208
|
-
|
|
209
|
-
// Build per-file metadata for UI rendering (used for both permission and result)
|
|
210
201
|
const files = fileChanges.map((change) => ({
|
|
211
202
|
filePath: change.filePath,
|
|
212
203
|
relativePath: path.relative(instance.worktree, change.movePath ?? change.filePath).replaceAll("\\", "/"),
|
|
@@ -216,8 +207,6 @@ export const ApplyPatchTool = Tool.define(
|
|
|
216
207
|
deletions: change.deletions,
|
|
217
208
|
movePath: change.movePath,
|
|
218
209
|
}))
|
|
219
|
-
|
|
220
|
-
// Check permissions if needed
|
|
221
210
|
const relativePaths = fileChanges.map((c) => path.relative(instance.worktree, c.filePath).replaceAll("\\", "/"))
|
|
222
211
|
yield* ctx.ask({
|
|
223
212
|
permission: "edit",
|
|
@@ -229,15 +218,12 @@ export const ApplyPatchTool = Tool.define(
|
|
|
229
218
|
files,
|
|
230
219
|
},
|
|
231
220
|
})
|
|
232
|
-
|
|
233
|
-
// Apply the changes
|
|
234
221
|
const updates: Array<{ file: string; event: "add" | "change" | "unlink" }> = []
|
|
235
222
|
|
|
236
223
|
for (const change of fileChanges) {
|
|
237
224
|
const edited = change.type === "delete" ? undefined : (change.movePath ?? change.filePath)
|
|
238
225
|
switch (change.type) {
|
|
239
226
|
case "add":
|
|
240
|
-
// Create parent directories (recursive: true is safe on existing/root dirs)
|
|
241
227
|
yield* EncodedIO.write(change.filePath, Bom.join(change.newContent, change.bom), change.encoding)
|
|
242
228
|
updates.push({ file: change.filePath, event: "add" })
|
|
243
229
|
break
|
|
@@ -249,7 +235,6 @@ export const ApplyPatchTool = Tool.define(
|
|
|
249
235
|
|
|
250
236
|
case "move":
|
|
251
237
|
if (change.movePath) {
|
|
252
|
-
// Create parent directories (recursive: true is safe on existing/root dirs)
|
|
253
238
|
yield* EncodedIO.write(change.movePath!, Bom.join(change.newContent, change.bom), change.encoding)
|
|
254
239
|
yield* afs.remove(change.filePath)
|
|
255
240
|
updates.push({ file: change.filePath, event: "unlink" })
|
|
@@ -270,21 +255,15 @@ export const ApplyPatchTool = Tool.define(
|
|
|
270
255
|
yield* bus.publish(File.Event.Edited, { file: edited })
|
|
271
256
|
}
|
|
272
257
|
}
|
|
273
|
-
|
|
274
|
-
// Publish file change events
|
|
275
258
|
for (const update of updates) {
|
|
276
259
|
yield* bus.publish(FileWatcher.Event.Updated, update)
|
|
277
260
|
}
|
|
278
|
-
|
|
279
|
-
// Notify LSP of file changes and collect diagnostics
|
|
280
261
|
for (const change of fileChanges) {
|
|
281
262
|
if (change.type === "delete") continue
|
|
282
263
|
const target = change.movePath ?? change.filePath
|
|
283
264
|
yield* lsp.touchFile(target, "document")
|
|
284
265
|
}
|
|
285
266
|
const diagnostics = yield* lsp.diagnostics()
|
|
286
|
-
|
|
287
|
-
// Generate output summary
|
|
288
267
|
const summaryLines = fileChanges.map((change) => {
|
|
289
268
|
if (change.type === "add") {
|
|
290
269
|
return `A ${path.relative(instance.worktree, change.filePath).replaceAll("\\", "/")}`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { levenshtein, SINGLE_CANDIDATE_SIMILARITY_THRESHOLD, MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD } from "./edit-utils"
|
|
1
|
+
import { levenshtein, SINGLE_CANDIDATE_SIMILARITY_THRESHOLD, MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD } from "./edit-utils"
|
|
2
2
|
|
|
3
3
|
export type Replacer = (content: string, find: string) => Generator<string, void, unknown>
|
|
4
4
|
|
|
@@ -151,7 +151,6 @@ export const WhitespaceNormalizedReplacer: Replacer = function* (content, find)
|
|
|
151
151
|
const match = line.match(regex)
|
|
152
152
|
if (match) yield match[0]
|
|
153
153
|
} catch {
|
|
154
|
-
// Invalid regex pattern, skip
|
|
155
154
|
}
|
|
156
155
|
}
|
|
157
156
|
}
|
|
@@ -285,4 +284,4 @@ export const ContextAwareReplacer: Replacer = function* (content, find) {
|
|
|
285
284
|
}
|
|
286
285
|
}
|
|
287
286
|
}
|
|
288
|
-
}
|
|
287
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect"
|
|
1
|
+
import { Effect, Schema } from "effect"
|
|
2
2
|
import { Npm } from "@saeeol/core/npm"
|
|
3
3
|
import * as Bus from "@/bus"
|
|
4
4
|
import * as Tool from "../core/tool"
|
|
@@ -98,8 +98,6 @@ export const PackageTool = Tool.define(
|
|
|
98
98
|
]
|
|
99
99
|
return { title: "Available Providers", output: lines.join("\n"), metadata: {} }
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
// action === "install"
|
|
103
101
|
const packages = params.packages
|
|
104
102
|
if (!packages || packages.length === 0) {
|
|
105
103
|
return {
|
|
@@ -165,4 +163,4 @@ export const PackageTool = Tool.define(
|
|
|
165
163
|
}),
|
|
166
164
|
}
|
|
167
165
|
}),
|
|
168
|
-
)
|
|
166
|
+
)
|
|
@@ -7,9 +7,6 @@ import { Bus } from "../../bus"
|
|
|
7
7
|
import { TuiEvent } from "../../cli/cmd/tui/event"
|
|
8
8
|
import DESCRIPTION from "./warpgrep.txt"
|
|
9
9
|
|
|
10
|
-
// FREE_PERIOD_TODO: Remove SAEEOL_WARPGREP_PROXY_URL constant and the proxy
|
|
11
|
-
// fallback below. After the free period ends, require MORPH_API_KEY and
|
|
12
|
-
// return an error when it is missing.
|
|
13
10
|
const SAEEOL_WARPGREP_PROXY_URL = "https://api.saeeol.ai/api/gateway"
|
|
14
11
|
|
|
15
12
|
const Parameters = Schema.Struct({
|
|
@@ -36,8 +33,6 @@ export const CodebaseSearchTool = Tool.define(
|
|
|
36
33
|
|
|
37
34
|
const apiKey = process.env["MORPH_API_KEY"]
|
|
38
35
|
|
|
39
|
-
// FREE_PERIOD_TODO: Remove proxy fallback — require apiKey, error if missing:
|
|
40
|
-
// if (!apiKey) return { title: ..., output: "Set MORPH_API_KEY to use codebase search.", metadata: {} }
|
|
41
36
|
const client = new WarpGrepClient({
|
|
42
37
|
morphApiKey: apiKey ?? "saeeol-free",
|
|
43
38
|
...(apiKey ? {} : { morphApiUrl: SAEEOL_WARPGREP_PROXY_URL }),
|
|
@@ -52,9 +47,6 @@ export const CodebaseSearchTool = Tool.define(
|
|
|
52
47
|
)
|
|
53
48
|
|
|
54
49
|
if (!result.success || !result.contexts?.length) {
|
|
55
|
-
// FREE_PERIOD_TODO: When the proxy stops serving free requests, errors
|
|
56
|
-
// from the proxy (401/402/429) will surface here. The message below
|
|
57
|
-
// tells the user exactly what to do.
|
|
58
50
|
const isAuthOrRateLimit =
|
|
59
51
|
result.error && /401|402|429|rate.limit|free.period|unauthorized/i.test(result.error)
|
|
60
52
|
const apiKeyMsg =
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, Schema } from "effect"
|
|
1
|
+
import { Effect, Schema } from "effect"
|
|
2
2
|
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
|
3
3
|
import * as Tool from "../core/tool"
|
|
4
4
|
import TurndownService from "turndown"
|
|
@@ -49,8 +49,6 @@ export const WebFetchTool = Tool.define(
|
|
|
49
49
|
})
|
|
50
50
|
|
|
51
51
|
const timeout = Math.min((params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000, MAX_TIMEOUT)
|
|
52
|
-
|
|
53
|
-
// Build Accept header based on requested format with q parameters for fallbacks
|
|
54
52
|
let acceptHeader = "*/*"
|
|
55
53
|
switch (params.format) {
|
|
56
54
|
case "markdown":
|
|
@@ -92,8 +90,6 @@ export const WebFetchTool = Tool.define(
|
|
|
92
90
|
),
|
|
93
91
|
Effect.timeoutOrElse({ duration: timeout, orElse: () => Effect.die(new Error("Request timed out")) }),
|
|
94
92
|
)
|
|
95
|
-
|
|
96
|
-
// Check content length
|
|
97
93
|
const contentLength = response.headers["content-length"]
|
|
98
94
|
if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {
|
|
99
95
|
throw new Error("Response too large (exceeds 5MB limit)")
|
|
@@ -125,8 +121,6 @@ export const WebFetchTool = Tool.define(
|
|
|
125
121
|
}
|
|
126
122
|
|
|
127
123
|
const content = new TextDecoder().decode(arrayBuffer)
|
|
128
|
-
|
|
129
|
-
// Handle content based on requested format and actual content type
|
|
130
124
|
switch (params.format) {
|
|
131
125
|
case "markdown":
|
|
132
126
|
if (contentType.includes("text/html")) {
|
|
@@ -167,12 +161,10 @@ async function extractTextFromHTML(html: string) {
|
|
|
167
161
|
skipContent = true
|
|
168
162
|
},
|
|
169
163
|
text() {
|
|
170
|
-
// Skip text content inside these elements
|
|
171
164
|
},
|
|
172
165
|
})
|
|
173
166
|
.on("*", {
|
|
174
167
|
element(element) {
|
|
175
|
-
// Reset skip flag when entering other elements
|
|
176
168
|
if (!["script", "style", "noscript", "iframe", "object", "embed"].includes(element.tagName)) {
|
|
177
169
|
skipContent = false
|
|
178
170
|
}
|
|
@@ -199,4 +191,4 @@ function convertHTMLToMarkdown(html: string): string {
|
|
|
199
191
|
})
|
|
200
192
|
turndownService.remove(["script", "style", "meta", "link"])
|
|
201
193
|
return turndownService.turndown(html)
|
|
202
|
-
}
|
|
194
|
+
}
|