clanka 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +3 -0
  2. package/dist/Agent.d.ts +119 -0
  3. package/dist/Agent.d.ts.map +1 -0
  4. package/dist/Agent.js +240 -0
  5. package/dist/Agent.js.map +1 -0
  6. package/dist/AgentTools.d.ts +246 -0
  7. package/dist/AgentTools.d.ts.map +1 -0
  8. package/dist/AgentTools.js +374 -0
  9. package/dist/AgentTools.js.map +1 -0
  10. package/dist/AgentTools.test.d.ts +2 -0
  11. package/dist/AgentTools.test.d.ts.map +1 -0
  12. package/dist/AgentTools.test.js +147 -0
  13. package/dist/AgentTools.test.js.map +1 -0
  14. package/dist/ApplyPatch.d.ts +27 -0
  15. package/dist/ApplyPatch.d.ts.map +1 -0
  16. package/dist/ApplyPatch.js +343 -0
  17. package/dist/ApplyPatch.js.map +1 -0
  18. package/dist/ApplyPatch.test.d.ts +2 -0
  19. package/dist/ApplyPatch.test.d.ts.map +1 -0
  20. package/dist/ApplyPatch.test.js +99 -0
  21. package/dist/ApplyPatch.test.js.map +1 -0
  22. package/dist/Codex.d.ts +11 -0
  23. package/dist/Codex.d.ts.map +1 -0
  24. package/dist/Codex.js +14 -0
  25. package/dist/Codex.js.map +1 -0
  26. package/dist/CodexAuth.d.ts +68 -0
  27. package/dist/CodexAuth.d.ts.map +1 -0
  28. package/dist/CodexAuth.js +270 -0
  29. package/dist/CodexAuth.js.map +1 -0
  30. package/dist/CodexAuth.test.d.ts +2 -0
  31. package/dist/CodexAuth.test.d.ts.map +1 -0
  32. package/dist/CodexAuth.test.js +425 -0
  33. package/dist/CodexAuth.test.js.map +1 -0
  34. package/dist/Executor.d.ts +20 -0
  35. package/dist/Executor.d.ts.map +1 -0
  36. package/dist/Executor.js +76 -0
  37. package/dist/Executor.js.map +1 -0
  38. package/dist/OutputFormatter.d.ts +11 -0
  39. package/dist/OutputFormatter.d.ts.map +1 -0
  40. package/dist/OutputFormatter.js +5 -0
  41. package/dist/OutputFormatter.js.map +1 -0
  42. package/dist/ToolkitRenderer.d.ts +17 -0
  43. package/dist/ToolkitRenderer.d.ts.map +1 -0
  44. package/dist/ToolkitRenderer.js +25 -0
  45. package/dist/ToolkitRenderer.js.map +1 -0
  46. package/dist/TypeBuilder.d.ts +11 -0
  47. package/dist/TypeBuilder.d.ts.map +1 -0
  48. package/dist/TypeBuilder.js +383 -0
  49. package/dist/TypeBuilder.js.map +1 -0
  50. package/dist/TypeBuilder.test.d.ts +2 -0
  51. package/dist/TypeBuilder.test.d.ts.map +1 -0
  52. package/dist/TypeBuilder.test.js +243 -0
  53. package/dist/TypeBuilder.test.js.map +1 -0
  54. package/dist/index.d.ts +25 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +25 -0
  57. package/dist/index.js.map +1 -0
  58. package/package.json +72 -0
  59. package/src/Agent.ts +398 -0
  60. package/src/AgentTools.test.ts +215 -0
  61. package/src/AgentTools.ts +507 -0
  62. package/src/ApplyPatch.test.ts +154 -0
  63. package/src/ApplyPatch.ts +473 -0
  64. package/src/Codex.ts +14 -0
  65. package/src/CodexAuth.test.ts +729 -0
  66. package/src/CodexAuth.ts +571 -0
  67. package/src/Executor.ts +129 -0
  68. package/src/OutputFormatter.ts +17 -0
  69. package/src/ToolkitRenderer.ts +39 -0
  70. package/src/TypeBuilder.test.ts +508 -0
  71. package/src/TypeBuilder.ts +670 -0
  72. package/src/index.ts +29 -0
package/src/Agent.ts ADDED
@@ -0,0 +1,398 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import {
5
+ Array,
6
+ Data,
7
+ Deferred,
8
+ Effect,
9
+ FileSystem,
10
+ identity,
11
+ Layer,
12
+ Option,
13
+ Path,
14
+ pipe,
15
+ Queue,
16
+ Schema,
17
+ Scope,
18
+ ServiceMap,
19
+ Stream,
20
+ } from "effect"
21
+ import { LanguageModel, Prompt, Tool, Toolkit } from "effect/unstable/ai"
22
+ import {
23
+ AgentToolHandlers,
24
+ AgentTools,
25
+ CurrentDirectory,
26
+ SubagentContext,
27
+ TaskCompleteDeferred,
28
+ } from "./AgentTools.ts"
29
+ import { Executor } from "./Executor.ts"
30
+ import { ToolkitRenderer } from "./ToolkitRenderer.ts"
31
+ import { ProviderName } from "effect/unstable/ai/Model"
32
+ import { OpenAiLanguageModel } from "@effect/ai-openai"
33
+ import { type StreamPart } from "effect/unstable/ai/Response"
34
+ import type { ChildProcessSpawner } from "effect/unstable/process"
35
+
36
+ /**
37
+ * @since 1.0.0
38
+ * @category Models
39
+ */
40
+ export interface Agent {
41
+ readonly output: Stream.Stream<Output, AgentFinished>
42
+ }
43
+
44
+ /**
45
+ * A layer that provides most of the common services needed to run an agent.
46
+ *
47
+ * @since 1.0.0
48
+ * @category Services
49
+ */
50
+ export const layerServices: Layer.Layer<
51
+ Tool.HandlersFor<typeof AgentTools.tools> | Executor | ToolkitRenderer,
52
+ never,
53
+ FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner
54
+ > = Layer.mergeAll(AgentToolHandlers, Executor.layer, ToolkitRenderer.layer)
55
+
56
+ /**
57
+ * Start an agent in the given directory with the given prompt and tools.
58
+ *
59
+ * @since 1.0.0
60
+ * @category Constructors
61
+ */
62
+ export const make: <Tools extends Record<string, Tool.Any> = {}>(options: {
63
+ /** The working directory to run the agent in */
64
+ readonly directory: string
65
+ /** The prompt to use for the agent */
66
+ readonly prompt: Prompt.RawInput
67
+ /** Additional system instructions to provide to the agent */
68
+ readonly system?: string | undefined
69
+ /** Additional tools to provide to the agent */
70
+ readonly tools?: Toolkit.Toolkit<Tools> | undefined
71
+ }) => Effect.Effect<
72
+ Agent,
73
+ never,
74
+ | Scope.Scope
75
+ | FileSystem.FileSystem
76
+ | Path.Path
77
+ | Executor
78
+ | LanguageModel.LanguageModel
79
+ | ProviderName
80
+ | ToolkitRenderer
81
+ | Tool.HandlersFor<Tools>
82
+ | Tool.HandlersFor<typeof AgentTools.tools>
83
+ | Tool.HandlerServices<Tools[keyof Tools]>
84
+ > = Effect.fnUntraced(function* (options: {
85
+ /** The working directory to run the agent in */
86
+ readonly directory: string
87
+ /** The prompt to use for the agent */
88
+ readonly prompt: Prompt.RawInput
89
+ /** Additional system instructions to provide to the agent */
90
+ readonly system?: string | undefined
91
+ /** Additional tools to provide to the agent */
92
+ readonly tools?: Toolkit.Toolkit<{}> | undefined
93
+ }) {
94
+ const ai = yield* LanguageModel.LanguageModel
95
+ const provider = yield* ProviderName
96
+ const fs = yield* FileSystem.FileSystem
97
+ const pathService = yield* Path.Path
98
+ const executor = yield* Executor
99
+ const allTools = Toolkit.merge(AgentTools, options.tools ?? Toolkit.empty)
100
+ const tools = yield* allTools
101
+ const services = yield* Effect.services<Tool.HandlerServices<{}>>()
102
+
103
+ let system = yield* generateSystem(allTools)
104
+ if (options.system) {
105
+ system += `\n${options.system}`
106
+ }
107
+ const withSystemPrompt = OpenAiLanguageModel.withConfigOverride({
108
+ instructions: system,
109
+ })
110
+
111
+ const agentsMd = yield* pipe(
112
+ fs.readFileString(pathService.resolve(options.directory, "AGENTS.md")),
113
+ Effect.option,
114
+ )
115
+
116
+ let subagentId = 0
117
+
118
+ const spawn: (prompt: Prompt.Prompt) => Stream.Stream<Output, AgentFinished> =
119
+ Effect.fnUntraced(function* (prompt) {
120
+ const deferred = yield* Deferred.make<string>()
121
+ const output = yield* Queue.make<Output, AgentFinished>()
122
+
123
+ const taskServices = SubagentContext.serviceMap({
124
+ spawn: ({ prompt }) => {
125
+ let id = ++subagentId
126
+ return spawn(Prompt.make(prompt)).pipe(
127
+ Stream.broadcast({
128
+ capacity: "unbounded",
129
+ }),
130
+ Effect.flatMap((stream) => {
131
+ Queue.offerUnsafe(
132
+ output,
133
+ new SubagentPart({ id, output: stream }),
134
+ )
135
+ return Stream.runDrain(stream)
136
+ }),
137
+ Effect.scoped,
138
+ Effect.as(""),
139
+ Effect.catch((e) => Effect.succeed(e.summary)),
140
+ )
141
+ },
142
+ }).pipe(
143
+ ServiceMap.add(CurrentDirectory, options.directory),
144
+ ServiceMap.add(TaskCompleteDeferred, deferred),
145
+ )
146
+
147
+ prompt = Prompt.concat(
148
+ prompt,
149
+ agentsMd.pipe(
150
+ Option.map((md) =>
151
+ Prompt.make(`Here is a copy of ./AGENTS.md. ALWAYS follow these instructions when completing the above task:
152
+
153
+ ${md}`),
154
+ ),
155
+ Option.getOrElse(() => Prompt.empty),
156
+ ),
157
+ )
158
+
159
+ if (provider !== "openai") {
160
+ prompt = Prompt.setSystem(prompt, system)
161
+ }
162
+
163
+ let currentScript = ""
164
+ yield* Effect.gen(function* () {
165
+ while (true) {
166
+ if (currentScript.length > 0) {
167
+ Queue.offerUnsafe(
168
+ output,
169
+ new ScriptStart({ script: currentScript }),
170
+ )
171
+ const result = yield* pipe(
172
+ executor.execute({
173
+ tools,
174
+ script: currentScript,
175
+ }),
176
+ Stream.mkString,
177
+ )
178
+ Queue.offerUnsafe(output, new ScriptEnd({ output: result }))
179
+ prompt = Prompt.concat(prompt, `Javascript output:\n\n${result}`)
180
+ currentScript = ""
181
+ }
182
+
183
+ if (Deferred.isDoneUnsafe(deferred)) {
184
+ yield* Queue.fail(
185
+ output,
186
+ new AgentFinished({ summary: yield* Deferred.await(deferred) }),
187
+ )
188
+ return
189
+ }
190
+
191
+ let response = Array.empty<StreamPart<{}>>()
192
+ yield* pipe(
193
+ ai.streamText({ prompt }),
194
+ Stream.takeUntil((part) => part.type === "text-end"),
195
+ Stream.runForEachArray((parts) => {
196
+ response.push(...parts)
197
+ for (const part of parts) {
198
+ switch (part.type) {
199
+ case "text-start":
200
+ currentScript = ""
201
+ break
202
+ case "text-delta":
203
+ currentScript += part.delta
204
+ break
205
+ case "reasoning-start":
206
+ break
207
+ case "reasoning-delta":
208
+ // process.stdout.write(part.delta)
209
+ break
210
+ case "reasoning-end":
211
+ // console.log("\n")
212
+ break
213
+ case "finish":
214
+ // console.log("Tokens used:", part.usage, "\n")
215
+ break
216
+ }
217
+ }
218
+ return Effect.void
219
+ }),
220
+ Effect.retry({
221
+ while: (err) => {
222
+ response = []
223
+ return err.isRetryable
224
+ },
225
+ }),
226
+ provider === "openai" ? withSystemPrompt : identity,
227
+ )
228
+ prompt = Prompt.concat(prompt, Prompt.fromResponseParts(response))
229
+ currentScript = currentScript.trim()
230
+ }
231
+ }).pipe(
232
+ Effect.provideServices(taskServices),
233
+ Effect.provideServices(services),
234
+ Effect.forkScoped,
235
+ )
236
+
237
+ return Stream.fromQueue(output)
238
+ }, Stream.unwrap)
239
+
240
+ const output = yield* spawn(Prompt.make(options.prompt)).pipe(
241
+ Stream.broadcast({
242
+ capacity: "unbounded",
243
+ }),
244
+ )
245
+
246
+ return identity<Agent>({
247
+ output,
248
+ })
249
+ // oxlint-disable-next-line typescript/no-explicit-any
250
+ }) as any
251
+
252
+ // oxlint-disable-next-line typescript/no-explicit-any
253
+ const generateSystem = Effect.fn(function* (tools: Toolkit.Toolkit<any>) {
254
+ const renderer = yield* ToolkitRenderer
255
+
256
+ return `# Who you are
257
+
258
+ You are a professional software engineer. You are precise, thoughtful and concise. You make changes with care and always do the due diligence to ensure the best possible outcome. You make no mistakes.
259
+
260
+ # Completing the task
261
+
262
+ To complete the task respond with javascript code that will be executed for you.
263
+
264
+ - Do not add any markdown formatting, just code.
265
+ - Use \`console.log\` to print any output you need.
266
+ - Top level await is supported.
267
+ - **Prefer using the functions provided** over the bash tool
268
+
269
+ You have the following functions available to you:
270
+
271
+ \`\`\`ts
272
+ ${renderer.render(tools)}
273
+
274
+ declare const fetch: typeof globalThis.fetch
275
+ \`\`\`
276
+
277
+ Here is how you would read a file:
278
+
279
+ \`\`\`
280
+ const content = await readFile({
281
+ path: "package.json",
282
+ startLine: 1,
283
+ endLine: 10,
284
+ })
285
+ console.log(JSON.parse(content))
286
+ \`\`\`
287
+
288
+ And the output would look like this:
289
+
290
+ \`\`\`
291
+ Javascript output:
292
+
293
+ [22:44:53.054] INFO (#47): Calling "readFile" { path: 'package.json' }
294
+ {
295
+ "name": "my-project",
296
+ "version": "1.0.0"
297
+ }
298
+ \`\`\`
299
+
300
+ # Guidelines
301
+
302
+ - Use the current state of the codebase to inform your decisions. Don't look at git history unless explicity asked to.
303
+ - Only add comments when necessary.
304
+ - Repect the users AGENTS.md file and ALWAYS follow the instructions in it.
305
+ `
306
+ })
307
+
308
+ /**
309
+ * @since 1.0.0
310
+ * @category Output
311
+ */
312
+ export class ReasoningStart extends Schema.TaggedClass<ReasoningStart>()(
313
+ "ReasoningStart",
314
+ {},
315
+ ) {}
316
+
317
+ /**
318
+ * @since 1.0.0
319
+ * @category Output
320
+ */
321
+ export class ReasoningDelta extends Schema.TaggedClass<ReasoningDelta>()(
322
+ "ReasoningDelta",
323
+ {
324
+ delta: Schema.String,
325
+ },
326
+ ) {}
327
+
328
+ /**
329
+ * @since 1.0.0
330
+ * @category Output
331
+ */
332
+ export class ReasoningEnd extends Schema.TaggedClass<ReasoningEnd>()(
333
+ "ReasoningEnd",
334
+ {},
335
+ ) {}
336
+
337
+ /**
338
+ * @since 1.0.0
339
+ * @category Output
340
+ */
341
+ export class ScriptStart extends Schema.TaggedClass<ScriptStart>()(
342
+ "ScriptStart",
343
+ {
344
+ script: Schema.String,
345
+ },
346
+ ) {}
347
+
348
+ /**
349
+ * @since 1.0.0
350
+ * @category Output
351
+ */
352
+ export class ScriptEnd extends Schema.TaggedClass<ScriptEnd>()("ScriptEnd", {
353
+ output: Schema.String,
354
+ }) {}
355
+
356
+ /**
357
+ * @since 1.0.0
358
+ * @category Output
359
+ */
360
+ export class AgentFinished extends Schema.TaggedErrorClass<AgentFinished>()(
361
+ "AgentFinished",
362
+ {
363
+ summary: Schema.String,
364
+ },
365
+ ) {}
366
+
367
+ /**
368
+ * @since 1.0.0
369
+ * @category Output
370
+ */
371
+ export const OutputPart = Schema.Union([
372
+ ReasoningStart,
373
+ ReasoningDelta,
374
+ ReasoningEnd,
375
+ ScriptStart,
376
+ ScriptEnd,
377
+ ])
378
+
379
+ /**
380
+ * @since 1.0.0
381
+ * @category Output
382
+ */
383
+ export type OutputPart = typeof OutputPart.Type
384
+
385
+ /**
386
+ * @since 1.0.0
387
+ * @category Output
388
+ */
389
+ export class SubagentPart extends Data.TaggedError("SubagentPart")<{
390
+ id: number
391
+ output: Stream.Stream<Output, AgentFinished>
392
+ }> {}
393
+
394
+ /**
395
+ * @since 1.0.0
396
+ * @category Output
397
+ */
398
+ export type Output = OutputPart | SubagentPart
@@ -0,0 +1,215 @@
1
+ import { tmpdir } from "node:os"
2
+ import { join } from "node:path"
3
+ import { NodeFileSystem, NodeServices } from "@effect/platform-node"
4
+ import { Deferred, Effect, FileSystem, Stream } from "effect"
5
+ import { describe, it } from "@effect/vitest"
6
+ import { expect } from "vitest"
7
+ import {
8
+ AgentToolHandlers,
9
+ AgentTools,
10
+ CurrentDirectory,
11
+ makeContextNoop,
12
+ TaskCompleteDeferred,
13
+ } from "./AgentTools.ts"
14
+ import { Executor } from "./Executor.ts"
15
+ import { ToolkitRenderer } from "./ToolkitRenderer.ts"
16
+
17
+ const makeTempRoot = (prefix: string) =>
18
+ Effect.gen(function* () {
19
+ const fs = yield* FileSystem.FileSystem
20
+ return yield* fs.makeTempDirectoryScoped({
21
+ directory: tmpdir(),
22
+ prefix,
23
+ })
24
+ })
25
+
26
+ describe("AgentTools", () => {
27
+ it.effect("renders the tool signatures", () =>
28
+ Effect.gen(function* () {
29
+ const renderer = yield* ToolkitRenderer
30
+ const output = renderer.render(AgentTools)
31
+
32
+ expect(output).toContain(
33
+ "/** Read a file and optionally filter the lines to return. Returns null if the file doesn't exist. */",
34
+ )
35
+ expect(output).toContain("declare function readFile(options: {")
36
+ expect(output).toContain("readonly path: string;")
37
+ expect(output).toContain("readonly startLine?: number | undefined;")
38
+ expect(output).toContain("readonly endLine?: number | undefined;")
39
+ expect(output).toContain(
40
+ "/** Apply a wrapped patch with Add/Delete/Update sections. */",
41
+ )
42
+ expect(output).toContain(
43
+ "declare function applyPatch(patch: string): Promise<string>",
44
+ )
45
+ expect(output).not.toContain("declare function python(")
46
+ }).pipe(
47
+ Effect.provide([
48
+ AgentToolHandlers,
49
+ Executor.layer,
50
+ ToolkitRenderer.layer,
51
+ ]),
52
+ Effect.provide(NodeServices.layer),
53
+ Effect.provideService(CurrentDirectory, process.cwd()),
54
+ Effect.provideServiceEffect(
55
+ TaskCompleteDeferred,
56
+ Deferred.make<string>(),
57
+ ),
58
+ ),
59
+ )
60
+
61
+ it.effect("applies multi-file patches with add, move, and delete", () =>
62
+ Effect.gen(function* () {
63
+ const fs = yield* FileSystem.FileSystem
64
+ const tempRoot = yield* makeTempRoot("clanka-apply-patch-")
65
+ yield* fs.makeDirectory(join(tempRoot, "src"), { recursive: true })
66
+ yield* fs.writeFileString(join(tempRoot, "src", "app.txt"), "old\n")
67
+ yield* fs.writeFileString(join(tempRoot, "obsolete.txt"), "remove me\n")
68
+
69
+ const executor = yield* Executor
70
+ const tools = yield* AgentTools
71
+ const output = yield* executor
72
+ .execute({
73
+ tools,
74
+ script: [
75
+ "const output = await applyPatch(`",
76
+ "*** Begin Patch",
77
+ "*** Add File: notes/hello.txt",
78
+ "+hello",
79
+ "*** Update File: src/app.txt",
80
+ "*** Move to: src/main.txt",
81
+ "@@",
82
+ "-old",
83
+ "+new",
84
+ "*** Delete File: obsolete.txt",
85
+ "*** End Patch",
86
+ "`)",
87
+ "console.log(output)",
88
+ ].join("\n"),
89
+ })
90
+ .pipe(
91
+ Stream.mkString,
92
+ Effect.provideServices(makeContextNoop(tempRoot)),
93
+ )
94
+
95
+ expect(output).toContain("A notes/hello.txt")
96
+ expect(output).toContain("M src/main.txt")
97
+ expect(output).toContain("D obsolete.txt")
98
+ expect(
99
+ yield* fs.readFileString(join(tempRoot, "notes", "hello.txt")),
100
+ ).toBe("hello\n")
101
+ expect(yield* fs.readFileString(join(tempRoot, "src", "main.txt"))).toBe(
102
+ "new\n",
103
+ )
104
+ yield* Effect.flip(fs.readFileString(join(tempRoot, "obsolete.txt")))
105
+ yield* Effect.flip(fs.readFileString(join(tempRoot, "src", "app.txt")))
106
+ }).pipe(
107
+ Effect.provide([
108
+ AgentToolHandlers,
109
+ Executor.layer,
110
+ ToolkitRenderer.layer,
111
+ ]),
112
+ Effect.provide(NodeServices.layer),
113
+ ),
114
+ )
115
+
116
+ it.effect("plans later hunks against in-memory file state", () =>
117
+ Effect.gen(function* () {
118
+ const fs = yield* FileSystem.FileSystem
119
+ const tempRoot = yield* makeTempRoot("clanka-apply-patch-state-")
120
+ yield* fs.makeDirectory(join(tempRoot, "src"), { recursive: true })
121
+ yield* fs.writeFileString(join(tempRoot, "src", "app.txt"), "old\n")
122
+
123
+ const executor = yield* Executor
124
+ const tools = yield* AgentTools
125
+ const output = yield* executor
126
+ .execute({
127
+ tools,
128
+ script: [
129
+ "const output = await applyPatch(`",
130
+ "*** Begin Patch",
131
+ "*** Add File: notes/hello.txt",
132
+ "+hello",
133
+ "*** Update File: notes/hello.txt",
134
+ "@@",
135
+ "-hello",
136
+ "+hello again",
137
+ "*** Update File: src/app.txt",
138
+ "*** Move to: src/main.txt",
139
+ "@@",
140
+ "-old",
141
+ "+new",
142
+ "*** Update File: src/main.txt",
143
+ "@@",
144
+ "-new",
145
+ "+newer",
146
+ "*** End Patch",
147
+ "`)",
148
+ "console.log(output)",
149
+ ].join("\n"),
150
+ })
151
+ .pipe(
152
+ Stream.mkString,
153
+ Effect.provideServices(makeContextNoop(tempRoot)),
154
+ )
155
+
156
+ expect(output).toContain("A notes/hello.txt")
157
+ expect(output).toContain("M notes/hello.txt")
158
+ expect(output).toContain("M src/main.txt")
159
+ expect(
160
+ yield* fs.readFileString(join(tempRoot, "notes", "hello.txt")),
161
+ ).toBe("hello again\n")
162
+ expect(yield* fs.readFileString(join(tempRoot, "src", "main.txt"))).toBe(
163
+ "newer\n",
164
+ )
165
+ yield* Effect.flip(fs.readFileString(join(tempRoot, "src", "app.txt")))
166
+ }).pipe(
167
+ Effect.provide([
168
+ AgentToolHandlers,
169
+ Executor.layer,
170
+ ToolkitRenderer.layer,
171
+ ]),
172
+ Effect.provide(NodeServices.layer),
173
+ ),
174
+ )
175
+
176
+ it.effect("renames a file", () =>
177
+ Effect.gen(function* () {
178
+ const fs = yield* FileSystem.FileSystem
179
+ const tempRoot = yield* makeTempRoot("clanka-rename-file-")
180
+ yield* fs.makeDirectory(join(tempRoot, "src"), { recursive: true })
181
+ yield* fs.writeFileString(join(tempRoot, "src", "app.txt"), "hello\n")
182
+
183
+ const executor = yield* Executor
184
+ const tools = yield* AgentTools
185
+ yield* executor
186
+ .execute({
187
+ tools,
188
+ script: [
189
+ "await renameFile({",
190
+ ' from: "src/app.txt",',
191
+ ' to: "src/main.txt",',
192
+ "})",
193
+ 'console.log("renamed")',
194
+ ].join("\n"),
195
+ })
196
+ .pipe(
197
+ Stream.mkString,
198
+ Effect.provideServices(makeContextNoop(tempRoot)),
199
+ )
200
+
201
+ expect(yield* fs.readFileString(join(tempRoot, "src", "main.txt"))).toBe(
202
+ "hello\n",
203
+ )
204
+ yield* Effect.flip(fs.readFileString(join(tempRoot, "src", "app.txt")))
205
+ }).pipe(
206
+ Effect.provide([
207
+ AgentToolHandlers,
208
+ Executor.layer,
209
+ ToolkitRenderer.layer,
210
+ NodeFileSystem.layer,
211
+ ]),
212
+ Effect.provide(NodeServices.layer),
213
+ ),
214
+ )
215
+ })