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.
- package/README.md +3 -0
- package/dist/Agent.d.ts +119 -0
- package/dist/Agent.d.ts.map +1 -0
- package/dist/Agent.js +240 -0
- package/dist/Agent.js.map +1 -0
- package/dist/AgentTools.d.ts +246 -0
- package/dist/AgentTools.d.ts.map +1 -0
- package/dist/AgentTools.js +374 -0
- package/dist/AgentTools.js.map +1 -0
- package/dist/AgentTools.test.d.ts +2 -0
- package/dist/AgentTools.test.d.ts.map +1 -0
- package/dist/AgentTools.test.js +147 -0
- package/dist/AgentTools.test.js.map +1 -0
- package/dist/ApplyPatch.d.ts +27 -0
- package/dist/ApplyPatch.d.ts.map +1 -0
- package/dist/ApplyPatch.js +343 -0
- package/dist/ApplyPatch.js.map +1 -0
- package/dist/ApplyPatch.test.d.ts +2 -0
- package/dist/ApplyPatch.test.d.ts.map +1 -0
- package/dist/ApplyPatch.test.js +99 -0
- package/dist/ApplyPatch.test.js.map +1 -0
- package/dist/Codex.d.ts +11 -0
- package/dist/Codex.d.ts.map +1 -0
- package/dist/Codex.js +14 -0
- package/dist/Codex.js.map +1 -0
- package/dist/CodexAuth.d.ts +68 -0
- package/dist/CodexAuth.d.ts.map +1 -0
- package/dist/CodexAuth.js +270 -0
- package/dist/CodexAuth.js.map +1 -0
- package/dist/CodexAuth.test.d.ts +2 -0
- package/dist/CodexAuth.test.d.ts.map +1 -0
- package/dist/CodexAuth.test.js +425 -0
- package/dist/CodexAuth.test.js.map +1 -0
- package/dist/Executor.d.ts +20 -0
- package/dist/Executor.d.ts.map +1 -0
- package/dist/Executor.js +76 -0
- package/dist/Executor.js.map +1 -0
- package/dist/OutputFormatter.d.ts +11 -0
- package/dist/OutputFormatter.d.ts.map +1 -0
- package/dist/OutputFormatter.js +5 -0
- package/dist/OutputFormatter.js.map +1 -0
- package/dist/ToolkitRenderer.d.ts +17 -0
- package/dist/ToolkitRenderer.d.ts.map +1 -0
- package/dist/ToolkitRenderer.js +25 -0
- package/dist/ToolkitRenderer.js.map +1 -0
- package/dist/TypeBuilder.d.ts +11 -0
- package/dist/TypeBuilder.d.ts.map +1 -0
- package/dist/TypeBuilder.js +383 -0
- package/dist/TypeBuilder.js.map +1 -0
- package/dist/TypeBuilder.test.d.ts +2 -0
- package/dist/TypeBuilder.test.d.ts.map +1 -0
- package/dist/TypeBuilder.test.js +243 -0
- package/dist/TypeBuilder.test.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
- package/src/Agent.ts +398 -0
- package/src/AgentTools.test.ts +215 -0
- package/src/AgentTools.ts +507 -0
- package/src/ApplyPatch.test.ts +154 -0
- package/src/ApplyPatch.ts +473 -0
- package/src/Codex.ts +14 -0
- package/src/CodexAuth.test.ts +729 -0
- package/src/CodexAuth.ts +571 -0
- package/src/Executor.ts +129 -0
- package/src/OutputFormatter.ts +17 -0
- package/src/ToolkitRenderer.ts +39 -0
- package/src/TypeBuilder.test.ts +508 -0
- package/src/TypeBuilder.ts +670 -0
- 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
|
+
})
|