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.
Files changed (81) hide show
  1. package/package.json +14 -14
  2. package/src/session/compaction-helpers.ts +1 -169
  3. package/src/session/compaction.ts +1 -712
  4. package/src/session/core/compaction/compaction-helpers.ts +169 -0
  5. package/src/session/core/compaction/compaction.ts +712 -0
  6. package/src/session/core/compaction/overflow.ts +28 -0
  7. package/src/session/core/instruction.ts +234 -0
  8. package/src/session/core/llm.ts +504 -0
  9. package/src/session/core/network.ts +392 -0
  10. package/src/session/core/processor.ts +731 -0
  11. package/src/session/core/projectors.ts +139 -0
  12. package/src/session/core/resolve-tools.ts +241 -0
  13. package/src/session/core/retry.ts +149 -0
  14. package/src/session/core/revert.ts +173 -0
  15. package/src/session/core/run-state.ts +110 -0
  16. package/src/session/core/schema.ts +35 -0
  17. package/src/session/core/session-types.ts +160 -0
  18. package/src/session/core/session.sql.ts +124 -0
  19. package/src/session/core/session.ts +948 -0
  20. package/src/session/core/shell-exec.ts +205 -0
  21. package/src/session/core/status.ts +100 -0
  22. package/src/session/core/subtask.ts +268 -0
  23. package/src/session/core/summary.ts +173 -0
  24. package/src/session/core/system.ts +114 -0
  25. package/src/session/core/todo.ts +86 -0
  26. package/src/session/core/user-part.ts +293 -0
  27. package/src/session/instruction.ts +1 -234
  28. package/src/session/llm.ts +1 -504
  29. package/src/session/message/message-errors.ts +83 -0
  30. package/src/session/message/message-parts.ts +89 -0
  31. package/src/session/message/message-query.ts +107 -0
  32. package/src/session/message/message-transform.ts +156 -0
  33. package/src/session/message/message-types.ts +68 -0
  34. package/src/session/message/message-v2.ts +73 -0
  35. package/src/session/message/message.ts +192 -0
  36. package/src/session/message-errors.ts +1 -83
  37. package/src/session/message-parts.ts +1 -89
  38. package/src/session/message-query.ts +1 -107
  39. package/src/session/message-transform.ts +1 -156
  40. package/src/session/message-types.ts +1 -68
  41. package/src/session/message-v2.ts +1 -73
  42. package/src/session/message.ts +1 -192
  43. package/src/session/network.ts +1 -392
  44. package/src/session/overflow.ts +1 -28
  45. package/src/session/processor.ts +1 -731
  46. package/src/session/projectors.ts +2 -139
  47. package/src/session/prompt/prompt-command.ts +93 -0
  48. package/src/session/prompt/prompt-loop.ts +299 -0
  49. package/src/session/prompt/prompt-model.ts +44 -0
  50. package/src/session/prompt/prompt-reminders.ts +120 -0
  51. package/src/session/prompt/prompt-resolve.ts +42 -0
  52. package/src/session/prompt/prompt-schemas.ts +128 -0
  53. package/src/session/prompt/prompt-title.ts +55 -0
  54. package/src/session/prompt/prompt-types.ts +47 -0
  55. package/src/session/prompt/prompt-user-msg.ts +80 -0
  56. package/src/session/prompt/prompt.ts +211 -0
  57. package/src/session/prompt-command.ts +1 -93
  58. package/src/session/prompt-loop.ts +1 -299
  59. package/src/session/prompt-model.ts +1 -44
  60. package/src/session/prompt-reminders.ts +1 -120
  61. package/src/session/prompt-resolve.ts +1 -42
  62. package/src/session/prompt-schemas.ts +1 -128
  63. package/src/session/prompt-title.ts +1 -55
  64. package/src/session/prompt-types.ts +1 -47
  65. package/src/session/prompt-user-msg.ts +1 -80
  66. package/src/session/prompt.ts +1 -211
  67. package/src/session/resolve-tools.ts +1 -241
  68. package/src/session/retry.ts +1 -149
  69. package/src/session/revert.ts +1 -173
  70. package/src/session/run-state.ts +1 -110
  71. package/src/session/schema.ts +1 -35
  72. package/src/session/session-types.ts +1 -160
  73. package/src/session/session.sql.ts +1 -124
  74. package/src/session/session.ts +1 -948
  75. package/src/session/shell-exec.ts +1 -205
  76. package/src/session/status.ts +1 -100
  77. package/src/session/subtask.ts +1 -268
  78. package/src/session/summary.ts +1 -173
  79. package/src/session/system.ts +1 -114
  80. package/src/session/todo.ts +1 -86
  81. package/src/session/user-part.ts +1 -293
@@ -1,173 +1 @@
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-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"
1
+ export * from "./core/summary"
@@ -1,114 +1 @@
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"
1
+ export * from "./core/system"
@@ -1,86 +1 @@
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"
1
+ export * from "./core/todo"
@@ -1,293 +1 @@
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-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-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
- )
1
+ export * from "./core/user-part"