saeeol 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/package.json +14 -14
  2. package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
  3. package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
  4. package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
  5. package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
  6. package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
  7. package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
  8. package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
  9. package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
  10. package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
  11. package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
  12. package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
  13. package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
  14. package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
  15. package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
  16. package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
  17. package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
  18. package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
  19. package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
  20. package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
  21. package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
  22. package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
  23. package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
  24. package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
  25. package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
  26. package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
  27. package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
  28. package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
  29. package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
  30. package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
  31. package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
  32. package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
  33. package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
  34. package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
  35. package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
  36. package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
  37. package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
  38. package/src/session/compaction-helpers.ts +1 -169
  39. package/src/session/compaction.ts +1 -712
  40. package/src/session/core/compaction/compaction-helpers.ts +169 -0
  41. package/src/session/core/compaction/compaction.ts +712 -0
  42. package/src/session/core/compaction/overflow.ts +28 -0
  43. package/src/session/core/instruction.ts +234 -0
  44. package/src/session/core/llm.ts +504 -0
  45. package/src/session/core/network.ts +392 -0
  46. package/src/session/core/processor.ts +731 -0
  47. package/src/session/core/projectors.ts +139 -0
  48. package/src/session/core/resolve-tools.ts +241 -0
  49. package/src/session/core/retry.ts +149 -0
  50. package/src/session/core/revert.ts +173 -0
  51. package/src/session/core/run-state.ts +110 -0
  52. package/src/session/core/schema.ts +35 -0
  53. package/src/session/core/session-types.ts +160 -0
  54. package/src/session/core/session.sql.ts +124 -0
  55. package/src/session/core/session.ts +948 -0
  56. package/src/session/core/shell-exec.ts +205 -0
  57. package/src/session/core/status.ts +100 -0
  58. package/src/session/core/subtask.ts +268 -0
  59. package/src/session/core/summary.ts +173 -0
  60. package/src/session/core/system.ts +114 -0
  61. package/src/session/core/todo.ts +86 -0
  62. package/src/session/core/user-part.ts +293 -0
  63. package/src/session/instruction.ts +1 -234
  64. package/src/session/llm.ts +1 -504
  65. package/src/session/message/message-errors.ts +83 -0
  66. package/src/session/message/message-parts.ts +89 -0
  67. package/src/session/message/message-query.ts +107 -0
  68. package/src/session/message/message-transform.ts +156 -0
  69. package/src/session/message/message-types.ts +68 -0
  70. package/src/session/message/message-v2.ts +73 -0
  71. package/src/session/message/message.ts +192 -0
  72. package/src/session/message-errors.ts +1 -83
  73. package/src/session/message-parts.ts +1 -89
  74. package/src/session/message-query.ts +1 -107
  75. package/src/session/message-transform.ts +1 -156
  76. package/src/session/message-types.ts +1 -68
  77. package/src/session/message-v2.ts +1 -73
  78. package/src/session/message.ts +1 -192
  79. package/src/session/network.ts +1 -392
  80. package/src/session/overflow.ts +1 -28
  81. package/src/session/processor.ts +1 -731
  82. package/src/session/projectors.ts +2 -139
  83. package/src/session/prompt/prompt-command.ts +93 -0
  84. package/src/session/prompt/prompt-loop.ts +299 -0
  85. package/src/session/prompt/prompt-model.ts +44 -0
  86. package/src/session/prompt/prompt-reminders.ts +120 -0
  87. package/src/session/prompt/prompt-resolve.ts +42 -0
  88. package/src/session/prompt/prompt-schemas.ts +128 -0
  89. package/src/session/prompt/prompt-title.ts +55 -0
  90. package/src/session/prompt/prompt-types.ts +47 -0
  91. package/src/session/prompt/prompt-user-msg.ts +80 -0
  92. package/src/session/prompt/prompt.ts +211 -0
  93. package/src/session/prompt-command.ts +1 -93
  94. package/src/session/prompt-loop.ts +1 -299
  95. package/src/session/prompt-model.ts +1 -44
  96. package/src/session/prompt-reminders.ts +1 -120
  97. package/src/session/prompt-resolve.ts +1 -42
  98. package/src/session/prompt-schemas.ts +1 -128
  99. package/src/session/prompt-title.ts +1 -55
  100. package/src/session/prompt-types.ts +1 -47
  101. package/src/session/prompt-user-msg.ts +1 -80
  102. package/src/session/prompt.ts +1 -211
  103. package/src/session/resolve-tools.ts +1 -241
  104. package/src/session/retry.ts +1 -149
  105. package/src/session/revert.ts +1 -173
  106. package/src/session/run-state.ts +1 -110
  107. package/src/session/schema.ts +1 -35
  108. package/src/session/session-types.ts +1 -160
  109. package/src/session/session.sql.ts +1 -124
  110. package/src/session/session.ts +1 -948
  111. package/src/session/shell-exec.ts +1 -205
  112. package/src/session/status.ts +1 -100
  113. package/src/session/subtask.ts +1 -268
  114. package/src/session/summary.ts +1 -173
  115. package/src/session/system.ts +1 -114
  116. package/src/session/todo.ts +1 -86
  117. package/src/session/user-part.ts +1 -293
  118. package/src/tool/apply_patch.ts +1 -334
  119. package/src/tool/bash.ts +1 -656
  120. package/src/tool/core/external-directory.ts +55 -0
  121. package/src/tool/core/invalid.ts +21 -0
  122. package/src/tool/core/recall.ts +164 -0
  123. package/src/tool/core/recall.txt +12 -0
  124. package/src/tool/core/schema.ts +16 -0
  125. package/src/tool/core/tool.ts +162 -0
  126. package/src/tool/core/truncate.ts +160 -0
  127. package/src/tool/core/truncation-dir.ts +4 -0
  128. package/src/tool/diagnostics.ts +1 -20
  129. package/src/tool/edit-replacers.ts +1 -288
  130. package/src/tool/edit-utils.ts +1 -86
  131. package/src/tool/edit.ts +1 -262
  132. package/src/tool/external-directory.ts +1 -55
  133. package/src/tool/file/apply_patch.ts +334 -0
  134. package/src/tool/file/apply_patch.txt +33 -0
  135. package/src/tool/file/bash.ts +656 -0
  136. package/src/tool/file/bash.txt +119 -0
  137. package/src/tool/file/edit-replacers.ts +288 -0
  138. package/src/tool/file/edit-utils.ts +86 -0
  139. package/src/tool/file/edit.ts +262 -0
  140. package/src/tool/file/edit.txt +10 -0
  141. package/src/tool/file/read.ts +389 -0
  142. package/src/tool/file/read.txt +14 -0
  143. package/src/tool/file/write.ts +114 -0
  144. package/src/tool/file/write.txt +8 -0
  145. package/src/tool/glob.ts +1 -115
  146. package/src/tool/grep.ts +1 -151
  147. package/src/tool/integration/diagnostics.ts +20 -0
  148. package/src/tool/integration/lsp.ts +113 -0
  149. package/src/tool/integration/lsp.txt +24 -0
  150. package/src/tool/integration/mcp-exa.ts +73 -0
  151. package/src/tool/integration/package.ts +168 -0
  152. package/src/tool/integration/registry.ts +375 -0
  153. package/src/tool/invalid.ts +1 -21
  154. package/src/tool/lsp.ts +1 -113
  155. package/src/tool/mcp-exa.ts +1 -73
  156. package/src/tool/package.ts +1 -168
  157. package/src/tool/plan.ts +1 -30
  158. package/src/tool/question.ts +1 -52
  159. package/src/tool/read.ts +1 -389
  160. package/src/tool/recall.ts +1 -164
  161. package/src/tool/registry.ts +1 -375
  162. package/src/tool/schema.ts +1 -16
  163. package/src/tool/search/glob.ts +115 -0
  164. package/src/tool/search/glob.txt +6 -0
  165. package/src/tool/search/grep.ts +151 -0
  166. package/src/tool/search/grep.txt +8 -0
  167. package/src/tool/search/warpgrep.ts +107 -0
  168. package/src/tool/search/warpgrep.txt +10 -0
  169. package/src/tool/search/webfetch.ts +202 -0
  170. package/src/tool/search/webfetch.txt +13 -0
  171. package/src/tool/search/websearch.ts +71 -0
  172. package/src/tool/search/websearch.txt +14 -0
  173. package/src/tool/skill.ts +1 -91
  174. package/src/tool/task.ts +1 -197
  175. package/src/tool/todo.ts +1 -62
  176. package/src/tool/tool.ts +1 -162
  177. package/src/tool/truncate.ts +1 -160
  178. package/src/tool/truncation-dir.ts +1 -4
  179. package/src/tool/warpgrep.ts +1 -107
  180. package/src/tool/webfetch.ts +1 -202
  181. package/src/tool/websearch.ts +1 -71
  182. package/src/tool/workflow/plan-enter.txt +14 -0
  183. package/src/tool/workflow/plan-exit.txt +13 -0
  184. package/src/tool/workflow/plan.ts +30 -0
  185. package/src/tool/workflow/question.ts +52 -0
  186. package/src/tool/workflow/question.txt +11 -0
  187. package/src/tool/workflow/skill.ts +91 -0
  188. package/src/tool/workflow/skill.txt +5 -0
  189. package/src/tool/workflow/task.ts +197 -0
  190. package/src/tool/workflow/task.txt +57 -0
  191. package/src/tool/workflow/todo.ts +62 -0
  192. package/src/tool/workflow/todowrite.txt +167 -0
  193. package/src/tool/write.ts +1 -114
@@ -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"