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
@@ -0,0 +1,211 @@
1
+ // @ts-ignore
2
+ globalThis.AI_SDK_LOG_WARNINGS = false
3
+
4
+ import { Effect, Layer, Scope, Latch, Context } from "effect"
5
+ import { ChildProcessSpawner } from "effect/unstable/process"
6
+ import { CrossSpawnSpawner } from "@saeeol/core/cross-spawn-spawner"
7
+ import { SaeeolSessionPrompt } from "@/saeeol/session/prompt"
8
+ import { SaeeolSessionPromptQueue } from "@/saeeol/session/prompt-queue"
9
+ import { SaeeolSession } from "@/saeeol/session"
10
+ import { SaeeolSessionProcessor } from "@/saeeol/session/processor"
11
+ import { Suggestion } from "@/saeeol/suggestion"
12
+ import { Question } from "@/question"
13
+ import { SessionID, MessageID } from "../core/schema"
14
+ import { MessageV2 } from "../message/message-v2"
15
+ import { makeRuntime } from "@/effect/run-service"
16
+ import { InstanceState } from "@/effect/instance-state"
17
+ import { EffectBridge } from "@/effect/bridge"
18
+ import { SessionRunState } from "../core/run-state"
19
+ import { SessionStatus } from "../core/status"
20
+ import { SessionCompaction } from "../core/compaction/compaction"
21
+ import { SessionProcessor } from "../core/processor"
22
+ import { Command } from "../../command"
23
+ import { Permission } from "@/permission"
24
+ import { MCP } from "../../mcp"
25
+ import { LSP } from "@/lsp/lsp"
26
+ import { ToolRegistry } from "@/tool/registry"
27
+ import { Truncate } from "@/tool/truncate"
28
+ import { Provider } from "@/provider/provider"
29
+ import { Config } from "@/config/config"
30
+ import { Instruction } from "../core/instruction"
31
+ import { AppFileSystem } from "@saeeol/core/filesystem"
32
+ import { Plugin } from "../../plugin"
33
+ import { Session as SessionMod } from "../core/session"
34
+ import { SessionRevert } from "../core/revert"
35
+ import { SessionSummary } from "../core/summary"
36
+ import { Agent } from "../../agent/agent"
37
+ import { SystemPrompt } from "../core/system"
38
+ import { LLM } from "../core/llm"
39
+ import { Bus } from "../../bus"
40
+ import { Service as ResolveToolsService, defaultLayer as resolveToolsLayer } from "../core/resolve-tools"
41
+ import { Service as UserPartService, defaultLayer as userPartLayer } from "../core/user-part"
42
+ import { Service as SubtaskService, defaultLayer as subtaskLayer } from "../core/subtask"
43
+ import { Service as ShellService, defaultLayer as shellLayer } from "../core/shell-exec"
44
+ import { type PromptInput, type ShellInput, type CommandInput, LoopInput } from "./prompt-schemas"
45
+ import { type Interface, Service } from "./prompt-types"
46
+ import { resolvePromptParts } from "./prompt-resolve"
47
+ import { getModel, lastModel, lastAssistant } from "./prompt-model"
48
+ import { createUserMessage } from "./prompt-user-msg"
49
+ import { ensureTitle } from "./prompt-title"
50
+ import { insertReminders } from "./prompt-reminders"
51
+ import { commandHandler } from "./prompt-command"
52
+ import { createRunLoop } from "./prompt-loop"
53
+
54
+ export const shouldAskPlanFollowup = SaeeolSessionPrompt.shouldAskPlanFollowup
55
+
56
+ export const layer = Layer.effect(
57
+ Service,
58
+ Effect.gen(function* () {
59
+ const bus = yield* Bus.Service
60
+ const status = yield* SessionStatus.Service
61
+ const sessions = yield* SessionMod.Service
62
+ const agents = yield* Agent.Service
63
+ const provider = yield* Provider.Service
64
+ const processor = yield* SessionProcessor.Service
65
+ const compaction = yield* SessionCompaction.Service
66
+ const plugin = yield* Plugin.Service
67
+ const commands = yield* Command.Service
68
+ const config = yield* Config.Service
69
+ const permission = yield* Permission.Service
70
+ const fsys = yield* AppFileSystem.Service
71
+ const mcp = yield* MCP.Service
72
+ const lsp = yield* LSP.Service
73
+ const registry = yield* ToolRegistry.Service
74
+ const truncate = yield* Truncate.Service
75
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
76
+ const scope = yield* Scope.Scope
77
+ const instruction = yield* Instruction.Service
78
+ const state = yield* SessionRunState.Service
79
+ const revert = yield* SessionRevert.Service
80
+ const summary = yield* SessionSummary.Service
81
+ const sys = yield* SystemPrompt.Service
82
+ const llm = yield* LLM.Service
83
+ const resolveToolsSvc = yield* ResolveToolsService
84
+ const userPartSvc = yield* UserPartService
85
+ const subtaskSvc = yield* SubtaskService
86
+ const shellSvc = yield* ShellService
87
+
88
+ const deps = {
89
+ bus, status, sessions, agents, provider, processor, compaction, plugin,
90
+ commands, config, permission, fsys, mcp, lsp, registry, truncate,
91
+ spawner, scope, instruction, state, revert, summary, sys, llm,
92
+ resolveToolsSvc, userPartSvc, subtaskSvc, shellSvc,
93
+ }
94
+
95
+ const resolvePromptPartsFn = resolvePromptParts(deps as any)
96
+ const getModelFn = getModel(deps as any)
97
+ const lastModelFn = lastModel(deps as any)
98
+ const lastAssistantFn = lastAssistant(deps as any)
99
+ const createUserMessageFn = createUserMessage(deps as any)
100
+ const ensureTitleFn = ensureTitle(deps as any)
101
+ const insertRemindersFn = insertReminders(deps as any, SaeeolSessionPrompt)
102
+ const commandFn = commandHandler(deps as any, SaeeolSessionProcessor.markReviewTelemetry.bind(SaeeolSessionProcessor))
103
+
104
+ const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) {
105
+ yield* SaeeolSessionPromptQueue.cancel(sessionID)
106
+ SaeeolSessionPrompt.abortPlanFollowup(sessionID)
107
+ yield* state.cancel(sessionID)
108
+ })
109
+
110
+ const { runLoop, loop, shell } = createRunLoop(
111
+ deps as any,
112
+ {
113
+ getModel: getModelFn,
114
+ lastModel: lastModelFn,
115
+ lastAssistant: lastAssistantFn,
116
+ createUserMessage: createUserMessageFn,
117
+ ensureTitle: ensureTitleFn,
118
+ insertReminders: insertRemindersFn,
119
+ resolvePromptParts: resolvePromptPartsFn,
120
+ commandHandler: commandFn,
121
+ SaeeolSessionPrompt,
122
+ SaeeolSessionPromptQueue,
123
+ SaeeolSession,
124
+ SaeeolSessionProcessor,
125
+ } as any,
126
+ )
127
+
128
+ let promptFn: any
129
+
130
+ const promptImpl = Effect.fn("SessionPrompt.prompt")(function* (input: PromptInput) {
131
+ return yield* promptFn(input)
132
+ })
133
+
134
+ promptFn = Effect.fn("SessionPrompt.prompt")(function* (input: PromptInput) {
135
+ const session = yield* sessions.get(input.sessionID)
136
+ yield* revert.cleanup(session)
137
+ yield* SaeeolSessionPrompt.recoverDanglingAssistant({ sessionID: input.sessionID, status, sessions })
138
+ yield* SaeeolSessionPrompt.recoverProviderFinishError({ sessionID: input.sessionID, status, sessions })
139
+ const message = yield* createUserMessageFn(input)
140
+ yield* sessions.touch(input.sessionID)
141
+ const permissions: Permission.Ruleset = []
142
+ for (const [t, enabled] of Object.entries(input.tools ?? {})) {
143
+ permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" })
144
+ }
145
+ if (permissions.length > 0) {
146
+ session.permission = permissions
147
+ yield* sessions.setPermission({ sessionID: session.id, permission: permissions })
148
+ }
149
+ yield* Effect.promise(() => Suggestion.dismissAll(input.sessionID))
150
+ yield* Effect.promise(() => Question.dismissAll(input.sessionID))
151
+ if (input.noReply === true) return message
152
+ return yield* (SaeeolSessionPromptQueue.enqueue as any)(
153
+ input.sessionID, message.info.id,
154
+ loop({ sessionID: input.sessionID }),
155
+ lastAssistantFn(input.sessionID),
156
+ )
157
+ })
158
+
159
+ return Service.of({
160
+ cancel,
161
+ prompt: promptFn,
162
+ loop: loop as any,
163
+ shell: shell as any,
164
+ command: commandFn as any,
165
+ resolvePromptParts: resolvePromptPartsFn as any,
166
+ })
167
+ }),
168
+ )
169
+
170
+ export const defaultLayer = Layer.suspend(() =>
171
+ layer.pipe(
172
+ Layer.provide(
173
+ Layer.mergeAll(
174
+ resolveToolsLayer, userPartLayer, subtaskLayer, shellLayer,
175
+ SessionRunState.defaultLayer, SessionStatus.defaultLayer,
176
+ SessionCompaction.defaultLayer, SessionProcessor.defaultLayer,
177
+ Command.defaultLayer, Permission.defaultLayer, MCP.defaultLayer,
178
+ LSP.defaultLayer, ToolRegistry.defaultLayer, Truncate.defaultLayer,
179
+ Provider.defaultLayer, Config.defaultLayer, Instruction.defaultLayer,
180
+ AppFileSystem.defaultLayer, Plugin.defaultLayer, SessionMod.defaultLayer,
181
+ SessionRevert.defaultLayer, SessionSummary.defaultLayer, Agent.defaultLayer,
182
+ SystemPrompt.defaultLayer, LLM.defaultLayer, Bus.layer,
183
+ CrossSpawnSpawner.defaultLayer,
184
+ ),
185
+ ),
186
+ ),
187
+ )
188
+
189
+ export { Service } from "./prompt-types"
190
+ export type { Interface } from "./prompt-types"
191
+
192
+ export {
193
+ PromptInput,
194
+ type PromptInput as PromptInputType,
195
+ ShellInput,
196
+ type ShellInput as ShellInputType,
197
+ CommandInput,
198
+ type CommandInput as CommandInputType,
199
+ LoopInput,
200
+ createStructuredOutputTool,
201
+ STRUCTURED_OUTPUT_DESCRIPTION, STRUCTURED_OUTPUT_SYSTEM_PROMPT,
202
+ REQUEST_PRUNE_BYTES,
203
+ bashRegex, argsRegex, placeholderRegex, quoteTrimRegex,
204
+ } from "./prompt-schemas"
205
+
206
+ const { runPromise } = makeRuntime(Service, defaultLayer)
207
+ export const prompt = (input: PromptInput) => runPromise((svc) => svc.prompt(input))
208
+ export const loopExport = (input: LoopInput) => runPromise((svc) => svc.loop(input))
209
+ export const cancel = (sessionID: SessionID) => runPromise((svc) => svc.cancel(sessionID))
210
+
211
+ export * as SessionPrompt from "./prompt"
@@ -1,93 +1 @@
1
- import { Effect } from "effect"
2
- import { NamedError } from "@saeeol/core/util/error"
3
- import * as EffectLogger from "@saeeol/core/effect/logger"
4
- import { ConfigMarkdown } from "@/config/markdown"
5
- import { Shell } from "@/shell/shell"
6
- import { Provider } from "@/provider/provider"
7
- import { Process } from "@/util/process"
8
- import { SessionID } from "./schema"
9
- import * as Session from "./session"
10
- import { resolvePromptParts } from "./prompt-resolve"
11
- import { getModel, lastModel } from "./prompt-model"
12
- import type { CommandInput } from "./prompt-schemas"
13
- import type { PromptDeps } from "./prompt-types"
14
-
15
- const elog = EffectLogger.create({ service: "session.prompt" })
16
-
17
- export const commandHandler = (deps: PromptDeps, markReviewTelemetry?: (parts: any, cmd: string) => void) => {
18
- const resolveParts = resolvePromptParts(deps)
19
-
20
- return Effect.fn("SessionPrompt.command")(function* (input: CommandInput) {
21
- yield* elog.info("command", { sessionID: input.sessionID, command: input.command, agent: input.agent })
22
- const cmd = yield* deps.commands.get(input.command)
23
- if (!cmd) {
24
- const available = (yield* deps.commands.list()).map((c: any) => c.name)
25
- const hint = available.length ? ` Available commands: ${available.join(", ")}` : ""
26
- const error = new NamedError.Unknown({ message: `Command not found: "${input.command}".${hint}` })
27
- yield* deps.bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
28
- throw error
29
- }
30
- const argsRegex = /\S+|"[^"]*"|'[^']*'/g
31
- const quoteTrimRegex = /^["']|["']$/g
32
- const placeholderRegex = /\$(\d+)/g
33
- const agentName = cmd.agent ?? input.agent ?? (yield* deps.agents.defaultAgent())
34
- const raw = input.arguments.match(argsRegex) ?? []
35
- const args = raw.map((arg: string) => arg.replace(quoteTrimRegex, ""))
36
- const templateCommand = yield* Effect.promise(async () => cmd.template)
37
- const placeholders = templateCommand.match(placeholderRegex) ?? []
38
- let last = 0
39
- for (const item of placeholders) { const value = Number(item.slice(1)); if (value > last) last = value }
40
- const withArgs = templateCommand.replaceAll(placeholderRegex, (_: string, index: string) => {
41
- const position = Number(index)
42
- const argIndex = position - 1
43
- if (argIndex >= args.length) return ""
44
- if (position === last) return args.slice(argIndex).join(" ")
45
- return args[argIndex]
46
- })
47
- const usesArgumentsPlaceholder = templateCommand.includes("$ARGUMENTS")
48
- let template = withArgs.replaceAll("$ARGUMENTS", input.arguments)
49
- if (placeholders.length === 0 && !usesArgumentsPlaceholder && input.arguments.trim()) {
50
- template = template + "\n\n" + input.arguments
51
- }
52
- const bashRegex = /```bash\n([\s\S]*?)```/g
53
- const shellMatches = ConfigMarkdown.shell(template)
54
- if (shellMatches.length > 0) {
55
- const cfg = yield* deps.config.get()
56
- const sh = Shell.preferred(cfg.shell)
57
- const results = yield* Effect.promise(() =>
58
- Promise.all(shellMatches.map(async (match: RegExpExecArray) => (await Process.text([match[1]], { shell: sh, nothrow: true })).text)),
59
- )
60
- let index = 0
61
- template = template.replace(bashRegex, () => results[index++])
62
- }
63
- template = template.trim()
64
- const taskModel = yield* Effect.gen(function* () {
65
- if (cmd.model) return Provider.parseModel(cmd.model)
66
- if (cmd.agent) { const cmdAgent = yield* deps.agents.get(cmd.agent); if (cmdAgent?.model) return cmdAgent.model }
67
- if (input.model) return Provider.parseModel(input.model)
68
- return yield* lastModel(deps)(input.sessionID)
69
- })
70
- yield* getModel(deps)(taskModel.providerID, taskModel.modelID, input.sessionID)
71
- const agent = yield* deps.agents.get(agentName)
72
- if (!agent) {
73
- const available = (yield* deps.agents.list()).filter((a: any) => !a.hidden).map((a: any) => a.name)
74
- const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
75
- const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` })
76
- yield* deps.bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
77
- throw error
78
- }
79
- const templateParts = yield* resolveParts(template)
80
- markReviewTelemetry?.(templateParts, input.command)
81
- const isSubtask = (agent.mode === "subagent" && cmd.subtask !== false) || cmd.subtask === true
82
- const parts = isSubtask
83
- ? [{ type: "subtask" as const, agent: agent.name, description: cmd.description ?? "",
84
- command: input.command, model: { providerID: taskModel.providerID, modelID: taskModel.modelID },
85
- prompt: (templateParts.find((y: any) => y.type === "text") as any)?.text ?? "" }]
86
- : [...templateParts, ...(input.parts ?? [])]
87
- const userAgent = isSubtask ? (input.agent ?? (yield* deps.agents.defaultAgent())) : agentName
88
- const userModel = isSubtask ? input.model ? Provider.parseModel(input.model) : yield* lastModel(deps)(input.sessionID) : taskModel
89
- yield* deps.plugin.trigger("command.execute.before",
90
- { command: input.command, sessionID: input.sessionID, arguments: input.arguments }, { parts })
91
- return { sessionID: input.sessionID, messageID: input.messageID, model: userModel, agent: userAgent, parts, variant: input.variant, command: input.command, arguments: input.arguments }
92
- })
93
- }
1
+ export * from "./prompt/prompt-command"
@@ -1,299 +1 @@
1
- import { Effect, Latch, Option } from "effect"
2
- import * as EffectLogger from "@saeeol/core/effect/logger"
3
- import * as Log from "@saeeol/core/util/log"
4
- import { SessionID, MessageID } from "./schema"
5
- import { MessageV2 } from "./message-v2"
6
- import * as Session from "./session"
7
- import { SaeeolSession } from "@/saeeol/session"
8
- import { SaeeolSessionPrompt } from "@/saeeol/session/prompt"
9
- import { SaeeolSessionPromptQueue } from "@/saeeol/session/prompt-queue"
10
- import { SaeeolSessionProcessor } from "@/saeeol/session/processor"
11
- import { InstanceState } from "@/effect/instance-state"
12
- import { EffectBridge } from "@/effect/bridge"
13
- import { type TaskPromptOps } from "@/tool/task"
14
- import { NamedError } from "@saeeol/core/util/error"
15
- import MAX_STEPS from "./prompt/max-steps.txt"
16
- import { type PromptInput, type ShellInput, LoopInput, STRUCTURED_OUTPUT_SYSTEM_PROMPT, REQUEST_PRUNE_BYTES, createStructuredOutputTool } from "./prompt-schemas"
17
- import type { PromptDeps } from "./prompt-types"
18
- const elog = EffectLogger.create({ service: "session.prompt" })
19
- const log = Log.create({ service: "session.prompt" })
20
-
21
- export interface LoopHelpers {
22
- getModel: (...args: any[]) => any
23
- lastModel: (...args: any[]) => any
24
- lastAssistant: (...args: any[]) => any
25
- createUserMessage: (...args: any[]) => any
26
- ensureTitle: (...args: any[]) => any
27
- insertReminders: (...args: any[]) => any
28
- resolvePromptParts: (...args: any[]) => any
29
- commandHandler: (...args: any[]) => any
30
- prompt: (...args: any[]) => any
31
- cancel: (...args: any[]) => any
32
- }
33
-
34
- export function createRunLoop(deps: PromptDeps, helpers: LoopHelpers) {
35
- const closeReasons = new Map<string, any>()
36
- const resolveTools = (input: any) => deps.resolveToolsSvc.resolve(input)
37
- const runner = Effect.fn("SessionPrompt.runner")(function* () { return yield* EffectBridge.make() })
38
- const ops = Effect.fn("SessionPrompt.ops")(function* () {
39
- const run = yield* runner()
40
- return {
41
- cancel: (id: SessionID) => run.fork(helpers.cancel(id)),
42
- resolvePromptParts: (t: string) => helpers.resolvePromptParts(t),
43
- prompt: (input: PromptInput) => helpers.prompt(input),
44
- } satisfies TaskPromptOps
45
- })
46
- const handleSubtask = (input: any) => Effect.gen(function* () {
47
- const ops_ = yield* ops()
48
- return yield* deps.subtaskSvc.handle({ ...input, getModel: helpers.getModel, promptOps: ops_ })
49
- })
50
- const shellImpl = (input: ShellInput, ready?: Latch.Latch) =>
51
- deps.shellSvc.exec(input, helpers.getModel, helpers.lastModel as any, ready)
52
- const runLoop = Effect.fn("SessionPrompt.run")(function* (sessionID: SessionID) {
53
- const envCache: SaeeolSessionPrompt.EnvCache = {}
54
- closeReasons.delete(sessionID)
55
- let compactionAttempts = 0
56
- const ctx = yield* InstanceState.context
57
- const slog = elog.with({ sessionID })
58
- let structured: unknown | undefined
59
- let step = 0
60
- const session = yield* deps.sessions.get(sessionID)
61
- while (true) {
62
- yield* deps.status.set(sessionID, { type: "busy" })
63
- yield* slog.info("loop", { step })
64
- let msgs: MessageV2.WithParts[] = yield* MessageV2.filterCompactedEffect(sessionID)
65
- msgs = SaeeolSessionPromptQueue.scope(sessionID, msgs)
66
- msgs = SaeeolSessionPrompt.trimBeforeLastSummary(msgs)
67
- let lastUser: MessageV2.User | undefined, lastAssistant: MessageV2.Assistant | undefined, lastFinished: MessageV2.Assistant | undefined
68
- let tasks: (MessageV2.CompactionPart | MessageV2.SubtaskPart)[] = []
69
- for (let i = msgs.length - 1; i >= 0; i--) {
70
- const msg = msgs[i]
71
- if (!lastUser && msg.info.role === "user") lastUser = msg.info
72
- if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info
73
- if (!lastFinished && msg.info.role === "assistant" && msg.info.finish) lastFinished = msg.info
74
- if (lastUser && lastFinished) break
75
- const task = msg.parts.filter((part) => part.type === "compaction" || part.type === "subtask")
76
- if (task && !lastFinished) tasks.push(...task)
77
- }
78
- if (!lastUser) throw new Error("No user message found in stream.")
79
- const telemetry = SaeeolSessionProcessor.extractReviewTelemetry(
80
- msgs.findLast((m) => m.info.role === "user" && m.info.id === lastUser.id)?.parts ?? [],
81
- )
82
- const lastAssistantMsg = msgs.findLast(
83
- (msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id,
84
- )
85
- const hasToolCalls = lastAssistantMsg?.parts.some((part) => part.type === "tool" && !part.metadata?.providerExecuted) ?? false
86
- if (lastAssistant?.finish && hasToolCalls && lastAssistant.parentID === lastUser.id &&
87
- lastUser.id < lastAssistant.id &&
88
- SaeeolSessionPrompt.shouldAskPlanFollowup({ messages: msgs, abort: AbortSignal.any([]) })
89
- ) {
90
- const action = yield* Effect.promise((signal) =>
91
- SaeeolSessionPrompt.askPlanFollowup({ sessionID, messages: msgs, abort: signal }),
92
- )
93
- if (action === "continue") continue
94
- yield* slog.info("exiting loop")
95
- break
96
- }
97
- if (lastAssistant?.finish && !["tool-calls"].includes(lastAssistant.finish) &&
98
- !hasToolCalls && lastAssistant.parentID === lastUser.id && lastUser.id < lastAssistant.id
99
- ) {
100
- const action = yield* Effect.promise((signal) =>
101
- SaeeolSessionPrompt.askPlanFollowup({ sessionID, messages: msgs, abort: signal }),
102
- )
103
- if (action === "continue") continue
104
- yield* slog.info("exiting loop")
105
- break
106
- }
107
- step++
108
- if (step === 1)
109
- yield* helpers.ensureTitle({
110
- session, modelID: lastUser.model.modelID, providerID: lastUser.model.providerID, history: msgs,
111
- }).pipe(Effect.ignore, Effect.forkIn(deps.scope))
112
- const model = yield* helpers.getModel(lastUser.model.providerID, lastUser.model.modelID, sessionID)
113
- const task = tasks.pop()
114
- if (task?.type === "subtask") {
115
- yield* handleSubtask({ task, model, lastUser, sessionID, session, msgs })
116
- continue
117
- }
118
- if (task?.type === "compaction") {
119
- const result = yield* deps.compaction.process({
120
- messages: msgs, parentID: lastUser.id, sessionID, auto: task.auto, overflow: task.overflow,
121
- })
122
- if (result === "stop") { closeReasons.set(sessionID, "error"); break }
123
- continue
124
- }
125
- if (lastFinished && lastFinished.summary !== true &&
126
- (yield* deps.compaction.isOverflow({ tokens: lastFinished.tokens, model }))
127
- ) {
128
- const guard = SaeeolSessionPrompt.guardCompactionAttempt({
129
- sessionID, attempts: compactionAttempts, closeReasons, message: lastFinished,
130
- })
131
- if (guard.exhausted) {
132
- yield* deps.sessions.updateMessage(lastFinished)
133
- yield* deps.bus.publish(Session.Event.Error, { sessionID, error: guard.error })
134
- break
135
- }
136
- compactionAttempts++
137
- yield* deps.compaction.create({ sessionID, agent: lastUser.agent, model: lastUser.model, auto: true })
138
- continue
139
- }
140
- const agent = yield* deps.agents.get(lastUser.agent)
141
- if (!agent) {
142
- const available = (yield* deps.agents.list()).filter((a: any) => !a.hidden).map((a: any) => a.name)
143
- const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
144
- const error = new NamedError.Unknown({ message: `Agent not found: "${lastUser.agent}".${hint}` })
145
- yield* deps.bus.publish(Session.Event.Error, { sessionID, error: error.toObject() })
146
- throw error
147
- }
148
- const maxSteps = agent.steps ?? Infinity
149
- const isLastStep = step >= maxSteps
150
- msgs = yield* helpers.insertReminders({ messages: msgs, agent, session })
151
- const msg: MessageV2.Assistant = {
152
- id: MessageID.ascending(), parentID: lastUser.id, role: "assistant", mode: agent.name,
153
- agent: agent.name, variant: lastUser.model.variant, path: { cwd: ctx.directory, root: ctx.worktree },
154
- cost: 0, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
155
- modelID: model.id, providerID: model.providerID, time: { created: Date.now() }, sessionID,
156
- }
157
- yield* deps.sessions.updateMessage(msg)
158
- const handle = yield* deps.processor.create({ assistantMessage: msg, sessionID, model, telemetry })
159
- const outcome: "break" | "continue" = yield* Effect.gen(function* () {
160
- const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
161
- const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
162
- const tools = yield* resolveTools({
163
- agent, session, model, tools: lastUser.tools, processor: handle,
164
- bypassAgentCheck, messages: msgs,
165
- })
166
- if (lastUser.format?.type === "json_schema") {
167
- tools["StructuredOutput"] = createStructuredOutputTool({
168
- schema: lastUser.format.schema,
169
- onSuccess(output: unknown) { structured = output },
170
- })
171
- }
172
- if (step === 1)
173
- yield* deps.summary.summarize({ sessionID, messageID: lastUser.id }).pipe(Effect.ignore, Effect.forkIn(deps.scope))
174
- if (step > 1 && lastFinished) {
175
- for (const m of msgs) {
176
- if (m.info.role !== "user" || m.info.id <= lastFinished.id) continue
177
- for (const p of m.parts) {
178
- if (p.type !== "text" || p.ignored || p.synthetic || !p.text.trim()) continue
179
- p.text = ["<system-reminder>", "The user sent the following message:", p.text, "",
180
- "Please address this message and continue with your tasks.", "</system-reminder>"].join("\n")
181
- }
182
- }
183
- }
184
- yield* deps.plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
185
- SaeeolSessionPrompt.injectEditorContext({ msgs, lastUser, sessionID, cache: envCache })
186
- msgs = SaeeolSessionPrompt.maybeStripHistoricalMedia(msgs)
187
- const allResult: any = yield* Effect.all([
188
- deps.sys.skills(agent), deps.sys.environment(model, lastUser.editorContext),
189
- deps.instruction.system().pipe(Effect.orDie),
190
- ])
191
- const skills: any = allResult[0], env: any[] = allResult[1], instructions: any[] = allResult[2]
192
- let modelMsgs = yield* MessageV2.toModelMessagesEffect(msgs, model)
193
- const size = Buffer.byteLength(JSON.stringify(modelMsgs))
194
- if (size > REQUEST_PRUNE_BYTES) {
195
- yield* deps.compaction.prune({ sessionID, reason: "payload-limit" })
196
- msgs = yield* MessageV2.filterCompactedEffect(sessionID)
197
- msgs = SaeeolSessionPromptQueue.scope(sessionID, msgs)
198
- msgs = SaeeolSessionPrompt.trimBeforeLastSummary(msgs)
199
- yield* deps.plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
200
- SaeeolSessionPrompt.injectEditorContext({ msgs, lastUser, sessionID, cache: envCache })
201
- msgs = SaeeolSessionPrompt.maybeStripHistoricalMedia(msgs)
202
- modelMsgs = yield* MessageV2.toModelMessagesEffect(msgs, model)
203
- const nextSize = Buffer.byteLength(JSON.stringify(modelMsgs))
204
- if (nextSize > REQUEST_PRUNE_BYTES) log.warn("payload still large after pruning", { size: nextSize })
205
- }
206
- const system = [...env, ...instructions, ...(skills ? [skills] : [])]
207
- const format = lastUser.format ?? { type: "text" as const }
208
- if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT)
209
- const result = yield* handle.process({
210
- user: lastUser, agent, permission: SaeeolSessionPrompt.guardPermissions({ agent, session }),
211
- sessionID, parentSessionID: session.parentID, system,
212
- messages: [...modelMsgs, ...(isLastStep ? [{ role: "assistant" as const, content: MAX_STEPS }] : [])],
213
- tools, model, toolChoice: format.type === "json_schema" ? "required" : undefined,
214
- })
215
- if (structured !== undefined) {
216
- handle.message.structured = structured
217
- handle.message.finish = handle.message.finish ?? "stop"
218
- yield* deps.sessions.updateMessage(handle.message)
219
- return "break" as const
220
- }
221
- const finished = handle.message.finish && !["tool-calls", "unknown"].includes(handle.message.finish)
222
- if (finished && !handle.message.error) {
223
- if (format.type === "json_schema") {
224
- handle.message.error = new MessageV2.StructuredOutputError({
225
- message: "Model did not produce structured output", retries: 0,
226
- }).toObject()
227
- yield* deps.sessions.updateMessage(handle.message)
228
- return "break" as const
229
- }
230
- if (handle.message.finish === "error") {
231
- SaeeolSessionProcessor.providerFinishError(handle.message)
232
- yield* deps.sessions.updateMessage(handle.message)
233
- closeReasons.set(sessionID, "error")
234
- return "break" as const
235
- }
236
- }
237
- if (result === "stop") {
238
- if (handle.message.error) closeReasons.set(sessionID, "error")
239
- return "break" as const
240
- }
241
- if (result === "compact") {
242
- const guard = SaeeolSessionPrompt.guardCompactionAttempt({
243
- sessionID, attempts: compactionAttempts, closeReasons, message: handle.message,
244
- })
245
- if (guard.exhausted) {
246
- yield* deps.sessions.updateMessage(handle.message)
247
- yield* deps.bus.publish(Session.Event.Error, { sessionID, error: guard.error })
248
- return "break" as const
249
- }
250
- compactionAttempts++
251
- yield* deps.compaction.create({
252
- sessionID, agent: lastUser.agent, model: lastUser.model, auto: true,
253
- overflow: !handle.message.finish,
254
- })
255
- }
256
- if (SaeeolSessionPromptQueue.hasFollowup(sessionID)) {
257
- closeReasons.set(sessionID, "interrupted")
258
- return "break" as const
259
- }
260
- if (result !== "compact" && !handle.message.finish) {
261
- handle.message.finish = "unknown"
262
- yield* deps.sessions.updateMessage(handle.message)
263
- }
264
- return "continue" as const
265
- }).pipe(Effect.ensuring(deps.instruction.clear(handle.message.id)))
266
- if (outcome === "break") break
267
- continue
268
- }
269
- yield* deps.compaction.prune({ sessionID, reason: "normal" }).pipe(Effect.ignore, Effect.forkIn(deps.scope))
270
- return yield* helpers.lastAssistant(sessionID)
271
- })
272
- const loop = Effect.fn("SessionPrompt.loop")(function* (input: LoopInput) {
273
- yield* SaeeolSessionPrompt.recoverDanglingAssistant({
274
- sessionID: input.sessionID, status: deps.status, sessions: deps.sessions,
275
- })
276
- yield* SaeeolSessionPrompt.recoverProviderFinishError({
277
- sessionID: input.sessionID, status: deps.status, sessions: deps.sessions,
278
- })
279
- yield* deps.bus.publish(SaeeolSession.Event.TurnOpen, { sessionID: input.sessionID })
280
- return yield* Effect.onExit(
281
- deps.state.ensureRunning(input.sessionID, helpers.lastAssistant(input.sessionID), runLoop(input.sessionID)),
282
- Effect.fnUntraced(function* (exit) {
283
- yield* deps.bus.publish(SaeeolSession.Event.TurnClose, {
284
- sessionID: input.sessionID,
285
- reason: SaeeolSessionPrompt.resolveCloseReason({
286
- sessionID: input.sessionID, closeReasons, exit,
287
- }),
288
- })
289
- }),
290
- )
291
- })
292
- const shell = Effect.fn("SessionPrompt.shell")(function* (input: ShellInput) {
293
- const ready = yield* Latch.make()
294
- return yield* deps.state.startShell(
295
- input.sessionID, helpers.lastAssistant(input.sessionID), shellImpl(input, ready), ready,
296
- )
297
- })
298
- return { runLoop, loop, shell, closeReasons }
299
- }
1
+ export * from "./prompt/prompt-loop"
@@ -1,44 +1 @@
1
- import { Effect, Exit, Cause, Option, Types } from "effect"
2
- import { SessionID, MessageID, PartID } from "./schema"
3
- import { MessageV2 } from "./message-v2"
4
- import { NamedError } from "@saeeol/core/util/error"
5
- import * as Log from "@saeeol/core/util/log"
6
- import type { PromptInput } from "./prompt-schemas"
7
- import type { ModelID, ProviderID } from "../provider/schema"
8
- import type { PromptDeps } from "./prompt-types"
9
-
10
- const log = Log.create({ service: "session.prompt" })
11
-
12
- export const getModel = (deps: PromptDeps) =>
13
- Effect.fn("SessionPrompt.getModel")(function* (providerID: ProviderID, modelID: ModelID, sessionID: SessionID) {
14
- const exit = yield* deps.provider.getModel(providerID, modelID).pipe(Effect.exit)
15
- if (Exit.isSuccess(exit)) return exit.value
16
- const err = Cause.squash(exit.cause)
17
- if (deps.provider.ModelNotFoundError?.isInstance?.(err) ?? (err as any).constructor?.name === "ModelNotFoundError") {
18
- const hint = (err as any).data?.suggestions?.length ? ` Did you mean: ${(err as any).data.suggestions.join(", ")}?` : ""
19
- yield* deps.bus.publish(deps.sessions.constructor?.Event?.Error ?? { type: "session.error" }, {
20
- sessionID,
21
- error: new NamedError.Unknown({ message: `Model not found: ${providerID}/${modelID}.${hint}` }).toObject(),
22
- })
23
- }
24
- return yield* Effect.failCause(exit.cause)
25
- })
26
-
27
- export const lastModel = (deps: PromptDeps) =>
28
- Effect.fnUntraced(function* (sessionID: SessionID) {
29
- const match = yield* deps.sessions.findMessage(sessionID, (m: any) => m.info.role === "user" && !!m.info.model) as any
30
- if (Option.isSome(match) && (match.value as any).info.role === "user") return (match.value as any).info.model
31
- return yield* deps.provider.defaultModel()
32
- })
33
-
34
- export const lastAssistant = (deps: PromptDeps) =>
35
- Effect.fnUntraced(function* (sessionID: SessionID) {
36
- for (let attempt = 0; attempt < 10; attempt++) {
37
- const match = yield* deps.sessions.findMessage(sessionID, (m: any) => m.info.role !== "user")
38
- if (Option.isSome(match)) return match.value
39
- const msgs = yield* deps.sessions.messages({ sessionID, limit: 1 })
40
- if (msgs.length > 0) return msgs[0]
41
- yield* Effect.sleep("50 millis")
42
- }
43
- throw new Error("Impossible")
44
- })
1
+ export * from "./prompt/prompt-model"