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
package/src/tool/task.ts CHANGED
@@ -1,197 +1 @@
1
- import * as Tool from "./tool"
2
- import DESCRIPTION from "./task.txt"
3
- import { Session } from "@/session/session"
4
- import { SessionID, MessageID } from "../session/schema"
5
- import { MessageV2 } from "../session/message-v2"
6
- import { Agent } from "../agent/agent"
7
- import type { SessionPrompt } from "../session/prompt"
8
- import { Config } from "@/config/config"
9
- import { SaeeolTask } from "../overlay/tool/task"
10
- import { SaeeolCostPropagation } from "../overlay/session/cost-propagation"
11
- import { SaeeolSessionProcessor } from "../overlay/session/processor"
12
- import { Effect, Schema } from "effect"
13
-
14
- export interface TaskPromptOps {
15
- cancel(sessionID: SessionID): void
16
- resolvePromptParts(template: string): Effect.Effect<SessionPrompt.PromptInput["parts"]>
17
- prompt(input: SessionPrompt.PromptInput): Effect.Effect<MessageV2.WithParts>
18
- }
19
-
20
- const id = "task"
21
-
22
- export const Parameters = Schema.Struct({
23
- description: Schema.String.annotate({ description: "A short (3-5 words) description of the task" }),
24
- prompt: Schema.String.annotate({ description: "The task for the agent to perform" }),
25
- subagent_type: Schema.String.annotate({ description: "The type of specialized agent to use for this task" }),
26
- task_id: Schema.optional(Schema.String).annotate({
27
- description:
28
- "This should only be set if you mean to resume a previous task (you can pass a prior task_id and the task will continue the same subagent session as before instead of creating a fresh one)",
29
- }),
30
- command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }),
31
- })
32
-
33
- export const TaskTool = Tool.define(
34
- id,
35
- Effect.gen(function* () {
36
- const agent = yield* Agent.Service
37
- const config = yield* Config.Service
38
- const sessions = yield* Session.Service
39
-
40
- const run = Effect.fn("TaskTool.execute")(function* (
41
- params: Schema.Schema.Type<typeof Parameters>,
42
- ctx: Tool.Context,
43
- ) {
44
- const cfg = yield* config.get()
45
-
46
- if (!ctx.extra?.bypassAgentCheck) {
47
- yield* ctx.ask({
48
- permission: id,
49
- patterns: [params.subagent_type],
50
- always: ["*"],
51
- metadata: {
52
- description: params.description,
53
- subagent_type: params.subagent_type,
54
- },
55
- })
56
- }
57
-
58
- const next = yield* agent.get(params.subagent_type)
59
- if (!next) {
60
- return yield* Effect.fail(new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`))
61
- }
62
- SaeeolTask.validate(next, params.subagent_type)
63
-
64
- const canTask = SaeeolTask.nestedTask()
65
- const canTodo = next.permission.some((rule) => rule.permission === "todowrite")
66
-
67
- const parent = yield* sessions.get(ctx.sessionID)
68
- const caller = yield* agent.get(ctx.agent)
69
- const rules = SaeeolTask.inherited({ caller, session: parent, mcp: cfg.mcp })
70
-
71
- const taskID = params.task_id
72
- const session = taskID
73
- ? yield* sessions.get(SessionID.make(taskID)).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
74
- : undefined
75
- const nextSession =
76
- session ??
77
- (yield* sessions.create({
78
- parentID: ctx.sessionID,
79
- title: params.description + ` (@${next.name} subagent)`,
80
- permission: [
81
- ...(parent.permission ?? []).filter(
82
- (rule) => rule.permission === "external_directory" || rule.action === "deny",
83
- ),
84
- ...(canTodo
85
- ? []
86
- : [
87
- {
88
- permission: "todowrite" as const,
89
- pattern: "*" as const,
90
- action: "deny" as const,
91
- },
92
- ]),
93
- ...(canTask
94
- ? []
95
- : [
96
- {
97
- permission: id,
98
- pattern: "*" as const,
99
- action: "deny" as const,
100
- },
101
- ]),
102
- ...(cfg.experimental?.primary_tools?.map((item) => ({
103
- pattern: "*",
104
- action: "allow" as const,
105
- permission: item,
106
- })) ?? []),
107
- ...SaeeolTask.permissions(rules),
108
- ],
109
- }))
110
-
111
- const msg = yield* Effect.sync(() => MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID }))
112
- if (msg.info.role !== "assistant") return yield* Effect.fail(new Error("Not an assistant message"))
113
- const saved = yield* SaeeolTask.resolveModel(next.name)
114
- const model = saved ??
115
- next.model ?? {
116
- modelID: msg.info.modelID,
117
- providerID: msg.info.providerID,
118
- }
119
- const variant = saved?.variant ?? (saved ? undefined : next.variant)
120
-
121
- yield* ctx.metadata({
122
- title: params.description,
123
- metadata: {
124
- sessionId: nextSession.id,
125
- model,
126
- variant,
127
- },
128
- })
129
-
130
- const ops = ctx.extra?.promptOps as TaskPromptOps
131
- if (!ops) return yield* Effect.fail(new Error("TaskTool requires promptOps in ctx.extra"))
132
-
133
- const messageID = MessageID.ascending()
134
-
135
- function cancel() {
136
- ops.cancel(nextSession.id)
137
- }
138
-
139
- return yield* Effect.acquireUseRelease(
140
- Effect.gen(function* () {
141
- ctx.abort.addEventListener("abort", cancel)
142
- return yield* SaeeolCostPropagation.childCost(sessions, nextSession.id)
143
- }),
144
- () =>
145
- Effect.gen(function* () {
146
- const parts = yield* ops.resolvePromptParts(params.prompt)
147
- SaeeolSessionProcessor.markReviewTelemetry(parts, params.command)
148
- const result = yield* ops.prompt({
149
- messageID,
150
- sessionID: nextSession.id,
151
- model: {
152
- modelID: model.modelID,
153
- providerID: model.providerID,
154
- },
155
- variant,
156
- agent: next.name,
157
- tools: {
158
- ...(canTodo ? {} : { todowrite: false }),
159
- ...(canTask ? {} : { task: false }),
160
- ...Object.fromEntries((cfg.experimental?.primary_tools ?? []).map((item) => [item, false])),
161
- },
162
- parts,
163
- })
164
-
165
- return {
166
- title: params.description,
167
- metadata: {
168
- sessionId: nextSession.id,
169
- model,
170
- variant,
171
- },
172
- output: [
173
- `task_id: ${nextSession.id} (for resuming to continue this task if needed)`,
174
- "",
175
- "<task_result>",
176
- result.parts.findLast((item) => item.type === "text")?.text ?? "",
177
- "</task_result>",
178
- ].join("\n"),
179
- }
180
- }),
181
- (costBefore) =>
182
- Effect.gen(function* () {
183
- ctx.abort.removeEventListener("abort", cancel)
184
- const costAfter = yield* SaeeolCostPropagation.childCost(sessions, nextSession.id)
185
- yield* SaeeolCostPropagation.propagate(sessions, ctx.sessionID, ctx.messageID, costAfter - costBefore)
186
- }),
187
- )
188
- })
189
-
190
- return {
191
- description: DESCRIPTION,
192
- parameters: Parameters,
193
- execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
194
- run(params, ctx).pipe(Effect.orDie),
195
- }
196
- }),
197
- )
1
+ export * from "./workflow/task"
package/src/tool/todo.ts CHANGED
@@ -1,62 +1 @@
1
- import { Effect, Schema } from "effect"
2
- import * as Tool from "./tool"
3
- import DESCRIPTION_WRITE from "./todowrite.txt"
4
- import { Todo } from "../session/todo"
5
- import { TodoView } from "../overlay/todo-view"
6
-
7
- // Todo.Info is still a zod schema (session/todo.ts). Inline the field shape
8
- // here rather than referencing its `.shape` — the LLM-visible JSON Schema is
9
- // identical, and it removes the last zod dependency from this tool.
10
- const TodoItem = Schema.Struct({
11
- content: Schema.String.annotate({ description: "Brief description of the task" }),
12
- status: Schema.String.annotate({
13
- description: "Current status of the task: pending, in_progress, completed, cancelled",
14
- }),
15
- priority: Schema.String.annotate({ description: "Priority level of the task: high, medium, low" }),
16
- })
17
-
18
- export const Parameters = Schema.Struct({
19
- todos: Schema.mutable(Schema.Array(TodoItem)).annotate({ description: "The updated todo list" }),
20
- })
21
-
22
- type Metadata = {
23
- todos: Todo.Info[]
24
- view?: TodoView.Info
25
- }
26
-
27
- export const TodoWriteTool = Tool.define<typeof Parameters, Metadata, Todo.Service>(
28
- "todowrite",
29
- Effect.gen(function* () {
30
- const todo = yield* Todo.Service
31
-
32
- return {
33
- description: DESCRIPTION_WRITE,
34
- parameters: Parameters,
35
- execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context<Metadata>) =>
36
- Effect.gen(function* () {
37
- yield* ctx.ask({
38
- permission: "todowrite",
39
- patterns: ["*"],
40
- always: ["*"],
41
- metadata: {},
42
- })
43
- const before = yield* todo.get(ctx.sessionID)
44
- const view = TodoView.calculate(before, params.todos)
45
-
46
- yield* todo.update({
47
- sessionID: ctx.sessionID,
48
- todos: params.todos,
49
- })
50
-
51
- return {
52
- title: `${params.todos.filter((x) => x.status !== "completed").length} todos`,
53
- output: JSON.stringify(params.todos, null, 2),
54
- metadata: {
55
- todos: params.todos,
56
- view,
57
- },
58
- }
59
- }),
60
- } satisfies Tool.DefWithoutID<typeof Parameters, Metadata>
61
- }),
62
- )
1
+ export * from "./workflow/todo"
package/src/tool/tool.ts CHANGED
@@ -1,162 +1 @@
1
- import { Effect, Schema } from "effect"
2
- import type { MessageV2 } from "../session/message-v2"
3
- import type { Permission } from "../permission"
4
- import type { SessionID, MessageID } from "../session/schema"
5
- import * as Truncate from "./truncate"
6
- import { Agent } from "@/agent/agent"
7
-
8
- interface Metadata {
9
- [key: string]: any
10
- }
11
-
12
- // TODO: remove this hack
13
- export type DynamicDescription = (agent: Agent.Info) => Effect.Effect<string>
14
-
15
- export type Context<M extends Metadata = Metadata> = {
16
- sessionID: SessionID
17
- messageID: MessageID
18
- agent: string
19
- abort: AbortSignal
20
- callID?: string
21
- extra?: { [key: string]: unknown }
22
- messages: MessageV2.WithParts[]
23
- metadata(input: { title?: string; metadata?: M }): Effect.Effect<void>
24
- ask(input: Omit<Permission.Request, "id" | "sessionID" | "tool">): Effect.Effect<void>
25
- }
26
-
27
- export interface ExecuteResult<M extends Metadata = Metadata> {
28
- title: string
29
- metadata: M
30
- output: string
31
- attachments?: Omit<MessageV2.FilePart, "id" | "sessionID" | "messageID">[]
32
- }
33
-
34
- export interface Def<
35
- Parameters extends Schema.Decoder<unknown> = Schema.Decoder<unknown>,
36
- M extends Metadata = Metadata,
37
- > {
38
- id: string
39
- description: string
40
- parameters: Parameters
41
- execute(args: Schema.Schema.Type<Parameters>, ctx: Context): Effect.Effect<ExecuteResult<M>>
42
- formatValidationError?(error: unknown): string
43
- }
44
- export type DefWithoutID<
45
- Parameters extends Schema.Decoder<unknown> = Schema.Decoder<unknown>,
46
- M extends Metadata = Metadata,
47
- > = Omit<Def<Parameters, M>, "id">
48
-
49
- export interface Info<
50
- Parameters extends Schema.Decoder<unknown> = Schema.Decoder<unknown>,
51
- M extends Metadata = Metadata,
52
- > {
53
- id: string
54
- init: () => Effect.Effect<DefWithoutID<Parameters, M>>
55
- }
56
-
57
- type Init<Parameters extends Schema.Decoder<unknown>, M extends Metadata> =
58
- | DefWithoutID<Parameters, M>
59
- | (() => Effect.Effect<DefWithoutID<Parameters, M>>)
60
-
61
- export type InferParameters<T> =
62
- T extends Info<infer P, any>
63
- ? Schema.Schema.Type<P>
64
- : T extends Effect.Effect<Info<infer P, any>, any, any>
65
- ? Schema.Schema.Type<P>
66
- : never
67
- export type InferMetadata<T> =
68
- T extends Info<any, infer M> ? M : T extends Effect.Effect<Info<any, infer M>, any, any> ? M : never
69
-
70
- export type InferDef<T> =
71
- T extends Info<infer P, infer M>
72
- ? Def<P, M>
73
- : T extends Effect.Effect<Info<infer P, infer M>, any, any>
74
- ? Def<P, M>
75
- : never
76
-
77
- function wrap<Parameters extends Schema.Decoder<unknown>, Result extends Metadata>(
78
- id: string,
79
- init: Init<Parameters, Result>,
80
- truncate: Truncate.Interface,
81
- agents: Agent.Interface,
82
- ) {
83
- return () =>
84
- Effect.gen(function* () {
85
- const toolInfo = typeof init === "function" ? { ...(yield* init()) } : { ...init }
86
- // Compile the parser closure once per tool init; `decodeUnknownEffect`
87
- // allocates a new closure per call, so hoisting avoids re-closing it for
88
- // every LLM tool invocation.
89
- const decode = Schema.decodeUnknownEffect(toolInfo.parameters)
90
- const execute = toolInfo.execute
91
- toolInfo.execute = (args, ctx) => {
92
- const attrs = {
93
- "tool.name": id,
94
- "session.id": ctx.sessionID,
95
- "message.id": ctx.messageID,
96
- ...(ctx.callID ? { "tool.call_id": ctx.callID } : {}),
97
- }
98
- return Effect.gen(function* () {
99
- const decoded = yield* decode(args).pipe(
100
- Effect.mapError((error) =>
101
- toolInfo.formatValidationError
102
- ? new Error(toolInfo.formatValidationError(error), { cause: error })
103
- : new Error(
104
- `The ${id} tool was called with invalid arguments: ${error}.\nPlease rewrite the input so it satisfies the expected schema.`,
105
- { cause: error },
106
- ),
107
- ),
108
- )
109
- const result = yield* execute(decoded as Schema.Schema.Type<Parameters>, ctx)
110
- if (result.metadata.truncated !== undefined) {
111
- return result
112
- }
113
- const agent = yield* agents.get(ctx.agent)
114
- const truncated = yield* truncate.output(result.output, {}, agent)
115
- return {
116
- ...result,
117
- output: truncated.content,
118
- metadata: {
119
- ...result.metadata,
120
- truncated: truncated.truncated,
121
- ...(truncated.truncated && { outputPath: truncated.outputPath }),
122
- },
123
- }
124
- }).pipe(Effect.orDie, Effect.withSpan("Tool.execute", { attributes: attrs }))
125
- }
126
- return toolInfo
127
- })
128
- }
129
-
130
- export function define<
131
- Parameters extends Schema.Decoder<unknown>,
132
- Result extends Metadata,
133
- R,
134
- ID extends string = string,
135
- >(
136
- id: ID,
137
- init: Effect.Effect<Init<Parameters, Result>, never, R>,
138
- ): Effect.Effect<Info<Parameters, Result>, never, R | Truncate.Service | Agent.Service> & { id: ID } {
139
- return Object.assign(
140
- Effect.gen(function* () {
141
- const resolved = yield* init
142
- const truncate = yield* Truncate.Service
143
- const agents = yield* Agent.Service
144
- return { id, init: wrap(id, resolved, truncate, agents) }
145
- }),
146
- { id },
147
- )
148
- }
149
-
150
- export function init<P extends Schema.Decoder<unknown>, M extends Metadata>(
151
- info: Info<P, M>,
152
- ): Effect.Effect<Def<P, M>> {
153
- return Effect.gen(function* () {
154
- const init = yield* info.init()
155
- return {
156
- ...init,
157
- id: info.id,
158
- }
159
- })
160
- }
161
-
162
- export * as Tool from "./tool"
1
+ export * from "./core/tool"
@@ -1,160 +1 @@
1
- import { NodePath } from "@effect/platform-node"
2
- import { Cause, Duration, Effect, Layer, Option, Schedule, Context } from "effect"
3
- import path from "path"
4
- import type { Agent } from "../agent/agent"
5
- import { AppFileSystem } from "@saeeol/core/filesystem"
6
- import { evaluate } from "@/permission/evaluate"
7
- import { Config } from "@/config/config"
8
- import { Identifier } from "../id/id"
9
- import * as Log from "@saeeol/core/util/log"
10
- import { ToolID } from "./schema"
11
- import { TRUNCATION_DIR } from "./truncation-dir"
12
-
13
- const log = Log.create({ service: "truncation" })
14
- const RETENTION = Duration.days(7)
15
-
16
- export const MAX_LINES = 2000
17
- export const MAX_BYTES = 50 * 1024
18
- export const DIR = TRUNCATION_DIR
19
- export const GLOB = path.join(TRUNCATION_DIR, "*")
20
-
21
- export type Result = { content: string; truncated: false } | { content: string; truncated: true; outputPath: string }
22
-
23
- export interface Options {
24
- maxLines?: number
25
- maxBytes?: number
26
- direction?: "head" | "tail"
27
- }
28
-
29
- function hasTaskTool(agent?: Agent.Info) {
30
- if (!agent?.permission) return false
31
- return evaluate("task", "*", agent.permission).action !== "deny"
32
- }
33
-
34
- export interface Interface {
35
- readonly cleanup: () => Effect.Effect<void>
36
- readonly write: (text: string) => Effect.Effect<string>
37
- /**
38
- * Returns output unchanged when it fits within the limits, otherwise writes the full text
39
- * to the truncation directory and returns a preview plus a hint to inspect the saved file.
40
- */
41
- readonly output: (text: string, options?: Options, agent?: Agent.Info) => Effect.Effect<Result>
42
- /**
43
- * Resolved truncation limits: values from `tool_output` in saeeol config, or MAX_LINES / MAX_BYTES if unset.
44
- */
45
- readonly limits: () => Effect.Effect<{ maxLines: number; maxBytes: number }>
46
- }
47
-
48
- export class Service extends Context.Service<Service, Interface>()("@saeeol/Truncate") {}
49
-
50
- export const layer = Layer.effect(
51
- Service,
52
- Effect.gen(function* () {
53
- const fs = yield* AppFileSystem.Service
54
-
55
- const cleanup = Effect.fn("Truncate.cleanup")(function* () {
56
- const cutoff = Identifier.timestamp(
57
- Identifier.create("tool", "ascending", Date.now() - Duration.toMillis(RETENTION)),
58
- )
59
- const entries = yield* fs.readDirectory(TRUNCATION_DIR).pipe(
60
- Effect.map((all) => all.filter((name) => name.startsWith("tool_"))),
61
- Effect.catch(() => Effect.succeed([])),
62
- )
63
- for (const entry of entries) {
64
- if (Identifier.timestamp(entry) >= cutoff) continue
65
- yield* fs.remove(path.join(TRUNCATION_DIR, entry)).pipe(Effect.catch(() => Effect.void))
66
- }
67
- })
68
-
69
- const write = Effect.fn("Truncate.write")(function* (text: string) {
70
- const file = path.join(TRUNCATION_DIR, ToolID.ascending())
71
- yield* fs.ensureDir(TRUNCATION_DIR).pipe(Effect.orDie)
72
- yield* fs.writeFileString(file, text).pipe(Effect.orDie)
73
- return file
74
- })
75
-
76
- const limits = Effect.fn("Truncate.limits")(function* () {
77
- const configSvc = yield* Effect.serviceOption(Config.Service)
78
- if (Option.isNone(configSvc)) return { maxLines: MAX_LINES, maxBytes: MAX_BYTES }
79
- const cfg = yield* configSvc.value.get().pipe(Effect.catch(() => Effect.succeed(undefined)))
80
- return {
81
- maxLines: cfg?.tool_output?.max_lines ?? MAX_LINES,
82
- maxBytes: cfg?.tool_output?.max_bytes ?? MAX_BYTES,
83
- }
84
- })
85
-
86
- const output = Effect.fn("Truncate.output")(function* (text: string, options: Options = {}, agent?: Agent.Info) {
87
- const resolved = yield* limits()
88
- const maxLines = options.maxLines ?? resolved.maxLines
89
- const maxBytes = options.maxBytes ?? resolved.maxBytes
90
- const direction = options.direction ?? "head"
91
- const lines = text.split("\n")
92
- const totalBytes = Buffer.byteLength(text, "utf-8")
93
-
94
- if (lines.length <= maxLines && totalBytes <= maxBytes) {
95
- return { content: text, truncated: false } as const
96
- }
97
-
98
- const out: string[] = []
99
- let i = 0
100
- let bytes = 0
101
- let hitBytes = false
102
-
103
- if (direction === "head") {
104
- for (i = 0; i < lines.length && i < maxLines; i++) {
105
- const size = Buffer.byteLength(lines[i], "utf-8") + (i > 0 ? 1 : 0)
106
- if (bytes + size > maxBytes) {
107
- hitBytes = true
108
- break
109
- }
110
- out.push(lines[i])
111
- bytes += size
112
- }
113
- } else {
114
- for (i = lines.length - 1; i >= 0 && out.length < maxLines; i--) {
115
- const size = Buffer.byteLength(lines[i], "utf-8") + (out.length > 0 ? 1 : 0)
116
- if (bytes + size > maxBytes) {
117
- hitBytes = true
118
- break
119
- }
120
- out.unshift(lines[i])
121
- bytes += size
122
- }
123
- }
124
-
125
- const removed = hitBytes ? totalBytes - bytes : lines.length - out.length
126
- const unit = hitBytes ? "bytes" : "lines"
127
- const preview = out.join("\n")
128
- const file = yield* write(text)
129
-
130
- const hint = hasTaskTool(agent)
131
- ? `The tool call succeeded but the output was truncated. Full output saved to: ${file}\nUse the Task tool to have explore agent process this file with Grep and Read (with offset/limit). Do NOT read the full file yourself - delegate to save context.`
132
- : `The tool call succeeded but the output was truncated. Full output saved to: ${file}\nUse Grep to search the full content or Read with offset/limit to view specific sections.`
133
-
134
- return {
135
- content:
136
- direction === "head"
137
- ? `${preview}\n\n...${removed} ${unit} truncated...\n\n${hint}`
138
- : `...${removed} ${unit} truncated...\n\n${hint}\n\n${preview}`,
139
- truncated: true,
140
- outputPath: file,
141
- } as const
142
- })
143
-
144
- yield* cleanup().pipe(
145
- Effect.catchCause((cause) => {
146
- log.error("truncation cleanup failed", { cause: Cause.pretty(cause) })
147
- return Effect.void
148
- }),
149
- Effect.repeat(Schedule.spaced(Duration.hours(1))),
150
- Effect.delay(Duration.minutes(1)),
151
- Effect.forkScoped,
152
- )
153
-
154
- return Service.of({ cleanup, write, output, limits })
155
- }),
156
- )
157
-
158
- export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(NodePath.layer))
159
-
160
- export * as Truncate from "./truncate"
1
+ export * from "./core/truncate"
@@ -1,4 +1 @@
1
- import path from "path"
2
- import { Global } from "@saeeol/core/global"
3
-
4
- export const TRUNCATION_DIR = path.join(Global.Path.data, "tool-output")
1
+ export * from "./core/truncation-dir"