saeeol 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/session/compaction-helpers.ts +1 -169
- package/src/session/compaction.ts +1 -712
- package/src/session/core/compaction/compaction-helpers.ts +169 -0
- package/src/session/core/compaction/compaction.ts +712 -0
- package/src/session/core/compaction/overflow.ts +28 -0
- package/src/session/core/instruction.ts +234 -0
- package/src/session/core/llm.ts +504 -0
- package/src/session/core/network.ts +392 -0
- package/src/session/core/processor.ts +731 -0
- package/src/session/core/projectors.ts +139 -0
- package/src/session/core/resolve-tools.ts +241 -0
- package/src/session/core/retry.ts +149 -0
- package/src/session/core/revert.ts +173 -0
- package/src/session/core/run-state.ts +110 -0
- package/src/session/core/schema.ts +35 -0
- package/src/session/core/session-types.ts +160 -0
- package/src/session/core/session.sql.ts +124 -0
- package/src/session/core/session.ts +948 -0
- package/src/session/core/shell-exec.ts +205 -0
- package/src/session/core/status.ts +100 -0
- package/src/session/core/subtask.ts +268 -0
- package/src/session/core/summary.ts +173 -0
- package/src/session/core/system.ts +114 -0
- package/src/session/core/todo.ts +86 -0
- package/src/session/core/user-part.ts +293 -0
- package/src/session/instruction.ts +1 -234
- package/src/session/llm.ts +1 -504
- package/src/session/message/message-errors.ts +83 -0
- package/src/session/message/message-parts.ts +89 -0
- package/src/session/message/message-query.ts +107 -0
- package/src/session/message/message-transform.ts +156 -0
- package/src/session/message/message-types.ts +68 -0
- package/src/session/message/message-v2.ts +73 -0
- package/src/session/message/message.ts +192 -0
- package/src/session/message-errors.ts +1 -83
- package/src/session/message-parts.ts +1 -89
- package/src/session/message-query.ts +1 -107
- package/src/session/message-transform.ts +1 -156
- package/src/session/message-types.ts +1 -68
- package/src/session/message-v2.ts +1 -73
- package/src/session/message.ts +1 -192
- package/src/session/network.ts +1 -392
- package/src/session/overflow.ts +1 -28
- package/src/session/processor.ts +1 -731
- package/src/session/projectors.ts +2 -139
- package/src/session/prompt/prompt-command.ts +93 -0
- package/src/session/prompt/prompt-loop.ts +299 -0
- package/src/session/prompt/prompt-model.ts +44 -0
- package/src/session/prompt/prompt-reminders.ts +120 -0
- package/src/session/prompt/prompt-resolve.ts +42 -0
- package/src/session/prompt/prompt-schemas.ts +128 -0
- package/src/session/prompt/prompt-title.ts +55 -0
- package/src/session/prompt/prompt-types.ts +47 -0
- package/src/session/prompt/prompt-user-msg.ts +80 -0
- package/src/session/prompt/prompt.ts +211 -0
- package/src/session/prompt-command.ts +1 -93
- package/src/session/prompt-loop.ts +1 -299
- package/src/session/prompt-model.ts +1 -44
- package/src/session/prompt-reminders.ts +1 -120
- package/src/session/prompt-resolve.ts +1 -42
- package/src/session/prompt-schemas.ts +1 -128
- package/src/session/prompt-title.ts +1 -55
- package/src/session/prompt-types.ts +1 -47
- package/src/session/prompt-user-msg.ts +1 -80
- package/src/session/prompt.ts +1 -211
- package/src/session/resolve-tools.ts +1 -241
- package/src/session/retry.ts +1 -149
- package/src/session/revert.ts +1 -173
- package/src/session/run-state.ts +1 -110
- package/src/session/schema.ts +1 -35
- package/src/session/session-types.ts +1 -160
- package/src/session/session.sql.ts +1 -124
- package/src/session/session.ts +1 -948
- package/src/session/shell-exec.ts +1 -205
- package/src/session/status.ts +1 -100
- package/src/session/subtask.ts +1 -268
- package/src/session/summary.ts +1 -173
- package/src/session/system.ts +1 -114
- package/src/session/todo.ts +1 -86
- package/src/session/user-part.ts +1 -293
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Effect, Layer, Context, Schema } from "effect"
|
|
2
|
+
import { Bus } from "@/bus"
|
|
3
|
+
import { Snapshot } from "@/snapshot"
|
|
4
|
+
import { Storage } from "@/storage/storage"
|
|
5
|
+
import { zod } from "@/util/effect-zod"
|
|
6
|
+
import { withStatics } from "@/util/schema"
|
|
7
|
+
import * as Session from "./session"
|
|
8
|
+
import { MessageV2 } from "../message/message-v2"
|
|
9
|
+
import { SessionID, MessageID } from "./schema"
|
|
10
|
+
import { makeRuntime } from "@/effect/run-service"
|
|
11
|
+
|
|
12
|
+
function unquoteGitPath(input: string) {
|
|
13
|
+
if (!input.startsWith('"')) return input
|
|
14
|
+
if (!input.endsWith('"')) return input
|
|
15
|
+
const body = input.slice(1, -1)
|
|
16
|
+
const bytes: number[] = []
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < body.length; i++) {
|
|
19
|
+
const char = body[i]!
|
|
20
|
+
if (char !== "\\") {
|
|
21
|
+
bytes.push(char.charCodeAt(0))
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const next = body[i + 1]
|
|
26
|
+
if (!next) {
|
|
27
|
+
bytes.push("\\".charCodeAt(0))
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (next >= "0" && next <= "7") {
|
|
32
|
+
const chunk = body.slice(i + 1, i + 4)
|
|
33
|
+
const match = chunk.match(/^[0-7]{1,3}/)
|
|
34
|
+
if (!match) {
|
|
35
|
+
bytes.push(next.charCodeAt(0))
|
|
36
|
+
i++
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
bytes.push(parseInt(match[0], 8))
|
|
40
|
+
i += match[0].length
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const escaped =
|
|
45
|
+
next === "n"
|
|
46
|
+
? "\n"
|
|
47
|
+
: next === "r"
|
|
48
|
+
? "\r"
|
|
49
|
+
: next === "t"
|
|
50
|
+
? "\t"
|
|
51
|
+
: next === "b"
|
|
52
|
+
? "\b"
|
|
53
|
+
: next === "f"
|
|
54
|
+
? "\f"
|
|
55
|
+
: next === "v"
|
|
56
|
+
? "\v"
|
|
57
|
+
: next === "\\" || next === '"'
|
|
58
|
+
? next
|
|
59
|
+
: undefined
|
|
60
|
+
|
|
61
|
+
bytes.push((escaped ?? next).charCodeAt(0))
|
|
62
|
+
i++
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return Buffer.from(bytes).toString()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface Interface {
|
|
69
|
+
readonly summarize: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect<void>
|
|
70
|
+
readonly diff: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Snapshot.FileDiff[]>
|
|
71
|
+
readonly computeDiff: (input: { messages: MessageV2.WithParts[] }) => Effect.Effect<Snapshot.FileDiff[]>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class Service extends Context.Service<Service, Interface>()("@saeeol/SessionSummary") {}
|
|
75
|
+
|
|
76
|
+
export const layer = Layer.effect(
|
|
77
|
+
Service,
|
|
78
|
+
Effect.gen(function* () {
|
|
79
|
+
const sessions = yield* Session.Service
|
|
80
|
+
const snapshot = yield* Snapshot.Service
|
|
81
|
+
const storage = yield* Storage.Service
|
|
82
|
+
const bus = yield* Bus.Service
|
|
83
|
+
|
|
84
|
+
const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: { messages: MessageV2.WithParts[] }) {
|
|
85
|
+
let from: string | undefined
|
|
86
|
+
let to: string | undefined
|
|
87
|
+
for (const item of input.messages) {
|
|
88
|
+
if (!from) {
|
|
89
|
+
for (const part of item.parts) {
|
|
90
|
+
if (part.type === "step-start" && part.snapshot) {
|
|
91
|
+
from = part.snapshot
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const part of item.parts) {
|
|
97
|
+
if (part.type === "step-finish" && part.snapshot) to = part.snapshot
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (from && to) return yield* snapshot.diffFull(from, to)
|
|
101
|
+
return []
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const summarize = Effect.fn("SessionSummary.summarize")(function* (input: {
|
|
105
|
+
sessionID: SessionID
|
|
106
|
+
messageID: MessageID
|
|
107
|
+
}) {
|
|
108
|
+
const all = yield* sessions.messages({ sessionID: input.sessionID })
|
|
109
|
+
if (!all.length) return
|
|
110
|
+
|
|
111
|
+
const diffs = yield* computeDiff({ messages: all })
|
|
112
|
+
yield* sessions.setSummary({
|
|
113
|
+
sessionID: input.sessionID,
|
|
114
|
+
summary: {
|
|
115
|
+
additions: diffs.reduce((sum, x) => sum + x.additions, 0),
|
|
116
|
+
deletions: diffs.reduce((sum, x) => sum + x.deletions, 0),
|
|
117
|
+
files: diffs.length,
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
yield* storage.write(["session_diff", input.sessionID], diffs).pipe(Effect.ignore)
|
|
121
|
+
yield* bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs })
|
|
122
|
+
|
|
123
|
+
const messages = all.filter(
|
|
124
|
+
(m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
|
|
125
|
+
)
|
|
126
|
+
const target = messages.find((m) => m.info.id === input.messageID)
|
|
127
|
+
if (!target || target.info.role !== "user") return
|
|
128
|
+
const msgDiffs = yield* computeDiff({ messages })
|
|
129
|
+
target.info.summary = { ...target.info.summary, diffs: msgDiffs }
|
|
130
|
+
yield* sessions.updateMessage(target.info)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const diff = Effect.fn("SessionSummary.diff")(function* (input: { sessionID: SessionID; messageID?: MessageID }) {
|
|
134
|
+
const diffs = yield* storage
|
|
135
|
+
.read<Snapshot.FileDiff[]>(["session_diff", input.sessionID])
|
|
136
|
+
.pipe(Effect.catch(() => Effect.succeed([] as Snapshot.FileDiff[])))
|
|
137
|
+
const next = diffs.map((item) => {
|
|
138
|
+
const file = unquoteGitPath(item.file)
|
|
139
|
+
const oversized = Buffer.byteLength(item.patch) > Snapshot.MAX_DIFF_SIZE
|
|
140
|
+
if (file === item.file && !oversized) return item
|
|
141
|
+
return {
|
|
142
|
+
...item,
|
|
143
|
+
file,
|
|
144
|
+
patch: oversized ? "" : item.patch,
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
const changed = next.some((item, i) => item.file !== diffs[i]?.file)
|
|
148
|
+
if (changed) yield* storage.write(["session_diff", input.sessionID], next).pipe(Effect.ignore)
|
|
149
|
+
return next
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
return Service.of({ summarize, diff, computeDiff })
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
export const defaultLayer = Layer.suspend(() =>
|
|
157
|
+
layer.pipe(
|
|
158
|
+
Layer.provide(Session.defaultLayer),
|
|
159
|
+
Layer.provide(Snapshot.defaultLayer),
|
|
160
|
+
Layer.provide(Storage.defaultLayer),
|
|
161
|
+
Layer.provide(Bus.layer),
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
export const DiffInput = Schema.Struct({
|
|
166
|
+
sessionID: SessionID,
|
|
167
|
+
messageID: Schema.optional(MessageID),
|
|
168
|
+
}).pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
169
|
+
export type DiffInput = Schema.Schema.Type<typeof DiffInput>
|
|
170
|
+
const { runPromise } = makeRuntime(Service, defaultLayer)
|
|
171
|
+
export const diff = (input: { sessionID: SessionID; messageID?: MessageID }) => runPromise((svc) => svc.diff(input))
|
|
172
|
+
|
|
173
|
+
export * as SessionSummary from "./summary"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect"
|
|
2
|
+
|
|
3
|
+
import { InstanceState } from "@/effect/instance-state"
|
|
4
|
+
|
|
5
|
+
import PROMPT_ANTHROPIC from "../prompt/anthropic.txt"
|
|
6
|
+
import PROMPT_DEFAULT from "../prompt/default.txt"
|
|
7
|
+
import PROMPT_BEAST from "../prompt/beast.txt"
|
|
8
|
+
import PROMPT_GEMINI from "../prompt/gemini.txt"
|
|
9
|
+
import PROMPT_GPT from "../prompt/gpt.txt"
|
|
10
|
+
import PROMPT_GPT55 from "../prompt/saeeol-gpt-5.5.txt"
|
|
11
|
+
import PROMPT_KIMI from "../prompt/kimi.txt"
|
|
12
|
+
import PROMPT_LING from "../prompt/ling.txt"
|
|
13
|
+
|
|
14
|
+
import PROMPT_CODEX from "../prompt/codex.txt"
|
|
15
|
+
import PROMPT_TRINITY from "../prompt/trinity.txt"
|
|
16
|
+
import type { Provider } from "@/provider/provider"
|
|
17
|
+
import type { Agent } from "@/agent/agent"
|
|
18
|
+
import { Permission } from "@/permission"
|
|
19
|
+
import { Skill } from "@/skill"
|
|
20
|
+
import SOUL from "../../overlay/soul.txt"
|
|
21
|
+
import type { EditorContext } from "../../overlay/editor-context"
|
|
22
|
+
import { SaeeolSystemPrompt } from "../../overlay/system-prompt"
|
|
23
|
+
import { isLing } from "../../overlay/model-match"
|
|
24
|
+
export function instructions() {
|
|
25
|
+
return PROMPT_CODEX.trim()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function soul() {
|
|
29
|
+
return SOUL.trim()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function provider(model: Provider.Model) {
|
|
33
|
+
function prompt() {
|
|
34
|
+
switch ((model as any).prompt) {
|
|
35
|
+
case "anthropic":
|
|
36
|
+
return [PROMPT_ANTHROPIC]
|
|
37
|
+
case "anthropic_without_todo":
|
|
38
|
+
return [PROMPT_DEFAULT]
|
|
39
|
+
case "beast":
|
|
40
|
+
return [PROMPT_BEAST]
|
|
41
|
+
case "codex":
|
|
42
|
+
return [PROMPT_CODEX]
|
|
43
|
+
case "gemini":
|
|
44
|
+
return [PROMPT_GEMINI]
|
|
45
|
+
case "gpt55":
|
|
46
|
+
return [PROMPT_GPT55]
|
|
47
|
+
case "ling":
|
|
48
|
+
return [PROMPT_LING]
|
|
49
|
+
case "trinity":
|
|
50
|
+
return [PROMPT_TRINITY]
|
|
51
|
+
}
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const saeeol = prompt()
|
|
56
|
+
if (saeeol) return saeeol
|
|
57
|
+
|
|
58
|
+
if (model.api.id.includes("gpt-4") || model.api.id.includes("o1") || model.api.id.includes("o3"))
|
|
59
|
+
return [PROMPT_BEAST]
|
|
60
|
+
if (model.api.id.includes("gpt")) {
|
|
61
|
+
if (model.api.id.includes("codex")) {
|
|
62
|
+
return [PROMPT_CODEX]
|
|
63
|
+
}
|
|
64
|
+
return [PROMPT_GPT]
|
|
65
|
+
}
|
|
66
|
+
if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]
|
|
67
|
+
if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC]
|
|
68
|
+
if (model.api.id.toLowerCase().includes("trinity")) return [PROMPT_TRINITY]
|
|
69
|
+
if (model.api.id.toLowerCase().includes("kimi")) return [PROMPT_KIMI]
|
|
70
|
+
if (isLing(model.api.id)) return [PROMPT_LING]
|
|
71
|
+
return [PROMPT_DEFAULT]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface Interface {
|
|
75
|
+
readonly environment: (model: Provider.Model, editorContext?: EditorContext) => Effect.Effect<string[]>
|
|
76
|
+
readonly skills: (agent: Agent.Info) => Effect.Effect<string | undefined>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class Service extends Context.Service<Service, Interface>()("@saeeol/SystemPrompt") {}
|
|
80
|
+
|
|
81
|
+
export const layer = Layer.effect(
|
|
82
|
+
Service,
|
|
83
|
+
Effect.gen(function* () {
|
|
84
|
+
const skill = yield* Skill.Service
|
|
85
|
+
|
|
86
|
+
return Service.of({
|
|
87
|
+
environment: Effect.fn("SystemPrompt.environment")(function* (
|
|
88
|
+
model: Provider.Model,
|
|
89
|
+
editorContext?: EditorContext,
|
|
90
|
+
) {
|
|
91
|
+
const ctx = yield* InstanceState.context
|
|
92
|
+
return SaeeolSystemPrompt.environment({ ctx, model, editor: editorContext })
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
skills: Effect.fn("SystemPrompt.skills")(function* (agent: Agent.Info) {
|
|
96
|
+
if (Permission.disabled(["skill"], agent.permission).has("skill")) return
|
|
97
|
+
|
|
98
|
+
const list = yield* skill.available(agent)
|
|
99
|
+
|
|
100
|
+
return [
|
|
101
|
+
"Skills provide specialized instructions and workflows for specific tasks.",
|
|
102
|
+
"Use the skill tool to load a skill when a task matches its description.",
|
|
103
|
+
// the agents seem to ingest the information about skills a bit better if we present a more verbose
|
|
104
|
+
// version of them here and a less verbose version in tool description, rather than vice versa.
|
|
105
|
+
Skill.fmt(list, { verbose: true }),
|
|
106
|
+
].join("\n")
|
|
107
|
+
}),
|
|
108
|
+
})
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer))
|
|
113
|
+
|
|
114
|
+
export * as SystemPrompt from "./system"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { BusEvent } from "@/bus/bus-event"
|
|
2
|
+
import { Bus } from "@/bus"
|
|
3
|
+
import { SessionID } from "./schema"
|
|
4
|
+
import { zod } from "@/util/effect-zod"
|
|
5
|
+
import { withStatics } from "@/util/schema"
|
|
6
|
+
import { Effect, Layer, Context, Schema } from "effect"
|
|
7
|
+
import z from "zod"
|
|
8
|
+
import { Database } from "@/storage/db"
|
|
9
|
+
import { eq } from "drizzle-orm"
|
|
10
|
+
import { asc } from "drizzle-orm"
|
|
11
|
+
import { TodoTable } from "./session.sql"
|
|
12
|
+
|
|
13
|
+
export const Info = Schema.Struct({
|
|
14
|
+
content: Schema.String.annotate({ description: "Brief description of the task" }),
|
|
15
|
+
status: Schema.String.annotate({
|
|
16
|
+
description: "Current status of the task: pending, in_progress, completed, cancelled",
|
|
17
|
+
}),
|
|
18
|
+
priority: Schema.String.annotate({ description: "Priority level of the task: high, medium, low" }),
|
|
19
|
+
})
|
|
20
|
+
.annotate({ identifier: "Todo" })
|
|
21
|
+
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
|
22
|
+
export type Info = Schema.Schema.Type<typeof Info>
|
|
23
|
+
|
|
24
|
+
export const Event = {
|
|
25
|
+
Updated: BusEvent.define(
|
|
26
|
+
"todo.updated",
|
|
27
|
+
Schema.Struct({
|
|
28
|
+
sessionID: SessionID,
|
|
29
|
+
todos: Schema.Array(Info),
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Interface {
|
|
35
|
+
readonly update: (input: { sessionID: SessionID; todos: Info[] }) => Effect.Effect<void>
|
|
36
|
+
readonly get: (sessionID: SessionID) => Effect.Effect<Info[]>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class Service extends Context.Service<Service, Interface>()("@saeeol/SessionTodo") {}
|
|
40
|
+
|
|
41
|
+
export const layer = Layer.effect(
|
|
42
|
+
Service,
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const bus = yield* Bus.Service
|
|
45
|
+
|
|
46
|
+
const update = Effect.fn("Todo.update")(function* (input: { sessionID: SessionID; todos: Info[] }) {
|
|
47
|
+
yield* Effect.sync(() =>
|
|
48
|
+
Database.transaction((db) => {
|
|
49
|
+
db.delete(TodoTable).where(eq(TodoTable.session_id, input.sessionID)).run()
|
|
50
|
+
if (input.todos.length === 0) return
|
|
51
|
+
db.insert(TodoTable)
|
|
52
|
+
.values(
|
|
53
|
+
input.todos.map((todo, position) => ({
|
|
54
|
+
session_id: input.sessionID,
|
|
55
|
+
content: todo.content,
|
|
56
|
+
status: todo.status,
|
|
57
|
+
priority: todo.priority,
|
|
58
|
+
position,
|
|
59
|
+
})),
|
|
60
|
+
)
|
|
61
|
+
.run()
|
|
62
|
+
}),
|
|
63
|
+
)
|
|
64
|
+
yield* bus.publish(Event.Updated, input)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const get = Effect.fn("Todo.get")(function* (sessionID: SessionID) {
|
|
68
|
+
const rows = yield* Effect.sync(() =>
|
|
69
|
+
Database.use((db) =>
|
|
70
|
+
db.select().from(TodoTable).where(eq(TodoTable.session_id, sessionID)).orderBy(asc(TodoTable.position)).all(),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
return rows.map((row) => ({
|
|
74
|
+
content: row.content,
|
|
75
|
+
status: row.status,
|
|
76
|
+
priority: row.priority,
|
|
77
|
+
}))
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return Service.of({ update, get })
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
|
|
85
|
+
|
|
86
|
+
export * as Todo from "./todo"
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { fileURLToPath } from "url"
|
|
2
|
+
import { Effect, Context, Layer, Exit, Cause, Scope, Types } from "effect"
|
|
3
|
+
import { SessionID, PartID } from "./schema"
|
|
4
|
+
import { MessageV2 } from "../message/message-v2"
|
|
5
|
+
import * as Session from "./session"
|
|
6
|
+
import { Provider } from "@/provider/provider"
|
|
7
|
+
import { Bus } from "../../bus"
|
|
8
|
+
import { ToolRegistry } from "@/tool/registry"
|
|
9
|
+
import { MCP } from "../../mcp"
|
|
10
|
+
import { LSP } from "@/lsp/lsp"
|
|
11
|
+
import { Plugin } from "../../plugin"
|
|
12
|
+
import { AppFileSystem } from "@saeeol/core/filesystem"
|
|
13
|
+
import { decodeDataUrl } from "@/util/data-url"
|
|
14
|
+
import { Permission } from "@/permission"
|
|
15
|
+
import { NamedError } from "@saeeol/core/util/error"
|
|
16
|
+
import * as Log from "@saeeol/core/util/log"
|
|
17
|
+
import type { PromptInput } from "../prompt/prompt-schemas"
|
|
18
|
+
|
|
19
|
+
const log = Log.create({ service: "session.user-part" })
|
|
20
|
+
|
|
21
|
+
export interface ResolveUserPartDeps {
|
|
22
|
+
sessionID: SessionID
|
|
23
|
+
messageID: ReturnType<typeof MessageV2.Info.make>["id"]
|
|
24
|
+
agent: string
|
|
25
|
+
agentPermission: any
|
|
26
|
+
model: { providerID: any; modelID: any }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Interface {
|
|
30
|
+
readonly resolve: (
|
|
31
|
+
part: PromptInput["parts"][number],
|
|
32
|
+
deps: ResolveUserPartDeps,
|
|
33
|
+
) => Effect.Effect<any[], never, Scope.Scope>
|
|
34
|
+
}
|
|
35
|
+
export class Service extends Context.Service<Service, Interface>()("@saeeol/SessionUserPart") {}
|
|
36
|
+
|
|
37
|
+
export const layer = Layer.effect(
|
|
38
|
+
Service,
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const fsys = yield* AppFileSystem.Service
|
|
41
|
+
const mcp = yield* MCP.Service
|
|
42
|
+
const lsp = yield* LSP.Service
|
|
43
|
+
const registry = yield* ToolRegistry.Service
|
|
44
|
+
const plugin = yield* Plugin.Service
|
|
45
|
+
const provider = yield* Provider.Service
|
|
46
|
+
const bus = yield* Bus.Service
|
|
47
|
+
|
|
48
|
+
const resolve = Effect.fn("SessionUserPart.resolve")(function* (
|
|
49
|
+
part: PromptInput["parts"][number],
|
|
50
|
+
deps: ResolveUserPartDeps,
|
|
51
|
+
) {
|
|
52
|
+
const { sessionID, messageID, agent, agentPermission, model } = deps
|
|
53
|
+
|
|
54
|
+
if (part.type === "file") {
|
|
55
|
+
if (part.source?.type === "resource") {
|
|
56
|
+
const { clientName, uri } = part.source
|
|
57
|
+
log.info("mcp resource", { clientName, uri, mime: part.mime })
|
|
58
|
+
const pieces: any[] = [
|
|
59
|
+
{
|
|
60
|
+
messageID, sessionID,
|
|
61
|
+
type: "text", synthetic: true,
|
|
62
|
+
text: `Reading MCP resource: ${part.filename} (${uri})`,
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
const exit = yield* mcp.readResource(clientName, uri).pipe(Effect.exit)
|
|
66
|
+
if (Exit.isSuccess(exit)) {
|
|
67
|
+
const content = exit.value
|
|
68
|
+
if (!content) throw new Error(`Resource not found: ${clientName}/${uri}`)
|
|
69
|
+
const items = Array.isArray(content.contents) ? content.contents : [content.contents]
|
|
70
|
+
for (const c of items) {
|
|
71
|
+
if ("text" in c && c.text) {
|
|
72
|
+
pieces.push({
|
|
73
|
+
messageID, sessionID,
|
|
74
|
+
type: "text", synthetic: true, text: c.text,
|
|
75
|
+
})
|
|
76
|
+
} else if ("blob" in c && c.blob) {
|
|
77
|
+
const mime = "mimeType" in c ? c.mimeType : part.mime
|
|
78
|
+
pieces.push({
|
|
79
|
+
messageID, sessionID,
|
|
80
|
+
type: "text", synthetic: true,
|
|
81
|
+
text: `[Binary content: ${mime}]`,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
pieces.push({ ...part, messageID, sessionID })
|
|
86
|
+
} else {
|
|
87
|
+
const error = Cause.squash(exit.cause)
|
|
88
|
+
log.error("failed to read MCP resource", { error, clientName, uri })
|
|
89
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
90
|
+
pieces.push({
|
|
91
|
+
messageID, sessionID,
|
|
92
|
+
type: "text", synthetic: true,
|
|
93
|
+
text: `Failed to read MCP resource ${part.filename}: ${message}`,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
return pieces
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const url = new URL(part.url)
|
|
100
|
+
switch (url.protocol) {
|
|
101
|
+
case "data:":
|
|
102
|
+
if (part.mime === "text/plain") {
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
messageID, sessionID,
|
|
106
|
+
type: "text", synthetic: true,
|
|
107
|
+
text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: part.filename })}`,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
messageID, sessionID,
|
|
111
|
+
type: "text", synthetic: true,
|
|
112
|
+
text: decodeDataUrl(part.url),
|
|
113
|
+
},
|
|
114
|
+
{ ...part, messageID, sessionID },
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
break
|
|
118
|
+
case "file:": {
|
|
119
|
+
log.info("file", { mime: part.mime })
|
|
120
|
+
const filepath = fileURLToPath(part.url)
|
|
121
|
+
const mime = (yield* fsys.isDir(filepath)) ? "application/x-directory" : part.mime
|
|
122
|
+
|
|
123
|
+
const { read } = yield* registry.named()
|
|
124
|
+
const execRead = (args: Parameters<typeof read.execute>[0], extra?: any) => {
|
|
125
|
+
const controller = new AbortController()
|
|
126
|
+
return read
|
|
127
|
+
.execute(args, {
|
|
128
|
+
sessionID, abort: controller.signal,
|
|
129
|
+
agent, messageID,
|
|
130
|
+
extra: { bypassCwdCheck: true, ...extra },
|
|
131
|
+
messages: [], metadata: () => Effect.void, ask: () => Effect.void,
|
|
132
|
+
})
|
|
133
|
+
.pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort())))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (mime === "text/plain") {
|
|
137
|
+
let offset: number | undefined
|
|
138
|
+
let limit: number | undefined
|
|
139
|
+
const range = { start: url.searchParams.get("start"), end: url.searchParams.get("end") }
|
|
140
|
+
if (range.start != null) {
|
|
141
|
+
const filePathURI = part.url.split("?")[0]
|
|
142
|
+
let start = parseInt(range.start)
|
|
143
|
+
let end = range.end ? parseInt(range.end) : undefined
|
|
144
|
+
if (start === end) {
|
|
145
|
+
const symbols = yield* lsp.documentSymbol(filePathURI).pipe(Effect.catch(() => Effect.succeed([])))
|
|
146
|
+
for (const symbol of symbols) {
|
|
147
|
+
let r: LSP.Range | undefined
|
|
148
|
+
if ("range" in symbol) r = symbol.range
|
|
149
|
+
else if ("location" in symbol) r = symbol.location.range
|
|
150
|
+
if (r?.start?.line && r?.start?.line === start) {
|
|
151
|
+
start = r.start.line
|
|
152
|
+
end = r?.end?.line ?? start
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
offset = Math.max(start, 1)
|
|
158
|
+
if (end) limit = end - (offset - 1)
|
|
159
|
+
}
|
|
160
|
+
const args = { filePath: filepath, offset, limit }
|
|
161
|
+
const pieces: any[] = [
|
|
162
|
+
{
|
|
163
|
+
messageID, sessionID,
|
|
164
|
+
type: "text", synthetic: true,
|
|
165
|
+
text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
|
|
166
|
+
},
|
|
167
|
+
]
|
|
168
|
+
const exit = yield* provider.getModel(model.providerID, model.modelID).pipe(
|
|
169
|
+
Effect.flatMap((mdl) => execRead(args, { model: mdl })),
|
|
170
|
+
Effect.exit,
|
|
171
|
+
)
|
|
172
|
+
if (Exit.isSuccess(exit)) {
|
|
173
|
+
const result = exit.value
|
|
174
|
+
pieces.push({
|
|
175
|
+
messageID, sessionID,
|
|
176
|
+
type: "text", synthetic: true, text: result.output,
|
|
177
|
+
})
|
|
178
|
+
if (result.attachments?.length) {
|
|
179
|
+
pieces.push(
|
|
180
|
+
...result.attachments.map((a: any) => ({
|
|
181
|
+
...a, synthetic: true,
|
|
182
|
+
filename: a.filename ?? part.filename,
|
|
183
|
+
messageID, sessionID,
|
|
184
|
+
})),
|
|
185
|
+
)
|
|
186
|
+
} else {
|
|
187
|
+
pieces.push({ ...part, mime, messageID, sessionID })
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
const error = Cause.squash(exit.cause)
|
|
191
|
+
log.error("failed to read file", { error })
|
|
192
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
193
|
+
yield* bus.publish(Session.Event.Error, {
|
|
194
|
+
sessionID,
|
|
195
|
+
error: new NamedError.Unknown({ message }).toObject(),
|
|
196
|
+
})
|
|
197
|
+
pieces.push({
|
|
198
|
+
messageID, sessionID,
|
|
199
|
+
type: "text", synthetic: true,
|
|
200
|
+
text: `Read tool failed to read ${filepath} with the following error: ${message}`,
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
return pieces
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (mime === "application/x-directory") {
|
|
207
|
+
const args = { filePath: filepath }
|
|
208
|
+
const exit = yield* execRead(args, { includeDirectoryFiles: true }).pipe(Effect.exit)
|
|
209
|
+
if (Exit.isFailure(exit)) {
|
|
210
|
+
const error = Cause.squash(exit.cause)
|
|
211
|
+
log.error("failed to read directory", { error })
|
|
212
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
213
|
+
yield* bus.publish(Session.Event.Error, {
|
|
214
|
+
sessionID,
|
|
215
|
+
error: new NamedError.Unknown({ message }).toObject(),
|
|
216
|
+
})
|
|
217
|
+
return [
|
|
218
|
+
{
|
|
219
|
+
messageID, sessionID,
|
|
220
|
+
type: "text", synthetic: true,
|
|
221
|
+
text: `Read tool failed to read ${filepath} with the following error: ${message}`,
|
|
222
|
+
},
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
return [
|
|
226
|
+
{
|
|
227
|
+
messageID, sessionID,
|
|
228
|
+
type: "text", synthetic: true,
|
|
229
|
+
text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
messageID, sessionID,
|
|
233
|
+
type: "text", synthetic: true,
|
|
234
|
+
text: exit.value.output,
|
|
235
|
+
},
|
|
236
|
+
{ ...part, mime, messageID, sessionID },
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return [
|
|
241
|
+
{
|
|
242
|
+
messageID, sessionID,
|
|
243
|
+
type: "text", synthetic: true,
|
|
244
|
+
text: `Called the Read tool with the following input: {"filePath":"${filepath}"}`,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: part.id, messageID, sessionID,
|
|
248
|
+
type: "file",
|
|
249
|
+
url:
|
|
250
|
+
`data:${mime};base64,` +
|
|
251
|
+
Buffer.from(yield* fsys.readFile(filepath).pipe(Effect.catch(Effect.die))).toString("base64"),
|
|
252
|
+
mime,
|
|
253
|
+
filename: part.filename!,
|
|
254
|
+
source: part.source,
|
|
255
|
+
},
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (part.type === "agent") {
|
|
262
|
+
const perm = Permission.evaluate("task", part.name, agentPermission)
|
|
263
|
+
const hint = perm.action === "deny" ? " . Invoked by user; guaranteed to exist." : ""
|
|
264
|
+
return [
|
|
265
|
+
{ ...part, messageID, sessionID } as any,
|
|
266
|
+
{
|
|
267
|
+
messageID, sessionID,
|
|
268
|
+
type: "text", synthetic: true,
|
|
269
|
+
text:
|
|
270
|
+
" Use the above message and context to generate a prompt and call the task tool with subagent: " +
|
|
271
|
+
part.name + hint,
|
|
272
|
+
},
|
|
273
|
+
]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return [{ ...part, messageID, sessionID } as any]
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
return { resolve }
|
|
280
|
+
}),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
export const defaultLayer = Layer.suspend(() =>
|
|
284
|
+
layer.pipe(
|
|
285
|
+
Layer.provide(AppFileSystem.defaultLayer),
|
|
286
|
+
Layer.provide(MCP.defaultLayer),
|
|
287
|
+
Layer.provide(LSP.defaultLayer),
|
|
288
|
+
Layer.provide(ToolRegistry.defaultLayer),
|
|
289
|
+
Layer.provide(Plugin.defaultLayer),
|
|
290
|
+
Layer.provide(Provider.defaultLayer),
|
|
291
|
+
Layer.provide(Bus.layer),
|
|
292
|
+
),
|
|
293
|
+
)
|