clanka 0.0.4 → 0.0.5

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 (46) hide show
  1. package/dist/Agent.d.ts +45 -19
  2. package/dist/Agent.d.ts.map +1 -1
  3. package/dist/Agent.js +172 -66
  4. package/dist/Agent.js.map +1 -1
  5. package/dist/Codex.d.ts +6 -1
  6. package/dist/Codex.d.ts.map +1 -1
  7. package/dist/Codex.js +16 -3
  8. package/dist/Codex.js.map +1 -1
  9. package/dist/GithubCopilot.d.ts +11 -0
  10. package/dist/GithubCopilot.d.ts.map +1 -0
  11. package/dist/GithubCopilot.js +14 -0
  12. package/dist/GithubCopilot.js.map +1 -0
  13. package/dist/GithubCopilotAuth.d.ts +57 -0
  14. package/dist/GithubCopilotAuth.d.ts.map +1 -0
  15. package/dist/GithubCopilotAuth.js +218 -0
  16. package/dist/GithubCopilotAuth.js.map +1 -0
  17. package/dist/GithubCopilotAuth.test.d.ts +2 -0
  18. package/dist/GithubCopilotAuth.test.d.ts.map +1 -0
  19. package/dist/GithubCopilotAuth.test.js +267 -0
  20. package/dist/GithubCopilotAuth.test.js.map +1 -0
  21. package/dist/OutputFormatter.d.ts +2 -1
  22. package/dist/OutputFormatter.d.ts.map +1 -1
  23. package/dist/OutputFormatter.js +8 -2
  24. package/dist/OutputFormatter.js.map +1 -1
  25. package/dist/ScriptExtraction.d.ts +34 -0
  26. package/dist/ScriptExtraction.d.ts.map +1 -0
  27. package/dist/ScriptExtraction.js +68 -0
  28. package/dist/ScriptExtraction.js.map +1 -0
  29. package/dist/ScriptExtraction.test.d.ts +2 -0
  30. package/dist/ScriptExtraction.test.d.ts.map +1 -0
  31. package/dist/ScriptExtraction.test.js +64 -0
  32. package/dist/ScriptExtraction.test.js.map +1 -0
  33. package/dist/index.d.ts +4 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +4 -0
  36. package/dist/index.js.map +1 -1
  37. package/package.json +3 -1
  38. package/src/Agent.ts +247 -87
  39. package/src/Codex.ts +18 -3
  40. package/src/GithubCopilot.ts +14 -0
  41. package/src/GithubCopilotAuth.test.ts +469 -0
  42. package/src/GithubCopilotAuth.ts +441 -0
  43. package/src/OutputFormatter.ts +11 -4
  44. package/src/ScriptExtraction.test.ts +96 -0
  45. package/src/ScriptExtraction.ts +75 -0
  46. package/src/index.ts +5 -0
package/src/Agent.ts CHANGED
@@ -17,7 +17,13 @@ import {
17
17
  ServiceMap,
18
18
  Stream,
19
19
  } from "effect"
20
- import { LanguageModel, Prompt, Tool, Toolkit } from "effect/unstable/ai"
20
+ import {
21
+ AiError,
22
+ LanguageModel,
23
+ Prompt,
24
+ Tool,
25
+ Toolkit,
26
+ } from "effect/unstable/ai"
21
27
  import {
22
28
  AgentToolHandlers,
23
29
  AgentTools,
@@ -28,7 +34,6 @@ import {
28
34
  import { Executor } from "./Executor.ts"
29
35
  import { ToolkitRenderer } from "./ToolkitRenderer.ts"
30
36
  import { ModelName, ProviderName } from "effect/unstable/ai/Model"
31
- import { OpenAiLanguageModel } from "@effect/ai-openai"
32
37
  import { type StreamPart } from "effect/unstable/ai/Response"
33
38
  import type { ChildProcessSpawner } from "effect/unstable/process"
34
39
 
@@ -37,7 +42,7 @@ import type { ChildProcessSpawner } from "effect/unstable/process"
37
42
  * @category Models
38
43
  */
39
44
  export interface Agent {
40
- readonly output: Stream.Stream<Output, AgentFinished>
45
+ readonly output: Stream.Stream<Output, AgentFinished | AiError.AiError>
41
46
 
42
47
  /**
43
48
  * Send a message to the agent to steer its behavior. This is useful for
@@ -50,18 +55,6 @@ export interface Agent {
50
55
  steer(message: string): Effect.Effect<void>
51
56
  }
52
57
 
53
- /**
54
- * A layer that provides most of the common services needed to run an agent.
55
- *
56
- * @since 1.0.0
57
- * @category Services
58
- */
59
- export const layerServices: Layer.Layer<
60
- Tool.HandlersFor<typeof AgentTools.tools> | Executor | ToolkitRenderer,
61
- never,
62
- FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner
63
- > = Layer.mergeAll(AgentToolHandlers, Executor.layer, ToolkitRenderer.layer)
64
-
65
58
  /**
66
59
  * Start an agent in the given directory with the given prompt and tools.
67
60
  *
@@ -112,50 +105,30 @@ export const make: <
112
105
  const fs = yield* FileSystem.FileSystem
113
106
  const pathService = yield* Path.Path
114
107
  const executor = yield* Executor
108
+ const renderer = yield* ToolkitRenderer
115
109
  const allTools = Toolkit.merge(AgentTools, options.tools ?? Toolkit.empty)
110
+ const allToolsDts = renderer.render(allTools)
116
111
  const tools = yield* allTools
112
+ const singleTool = yield* SingleTools.asEffect().pipe(
113
+ Effect.provide(SingleToolHandlers),
114
+ )
117
115
  const services = yield* Effect.services<
118
116
  | Tool.HandlerServices<{}>
119
117
  | LanguageModel.LanguageModel
120
118
  | ProviderName
121
119
  | ModelName
122
120
  >()
121
+
123
122
  const pendingMessages = new Set<{
124
123
  readonly message: string
125
124
  readonly resume: (effect: Effect.Effect<void>) => void
126
125
  }>()
127
126
 
128
- let system = yield* generateSystem(allTools)
129
-
130
127
  const agentsMd = yield* pipe(
131
128
  fs.readFileString(pathService.resolve(options.directory, "AGENTS.md")),
132
129
  Effect.option,
133
130
  )
134
131
 
135
- if (Option.isSome(agentsMd)) {
136
- system += `
137
- # AGENTS.md
138
-
139
- The following instructions are from ./AGENTS.md in the current directory.
140
- You do not need to read this file again.
141
-
142
- **ALWAYS follow these instructions when completing tasks**:
143
-
144
- <!-- AGENTS.md start -->
145
- ${agentsMd.value}
146
- <!-- AGENTS.md end -->
147
- `
148
- }
149
-
150
- if (options.system) {
151
- system += `\n${options.system}\n`
152
- }
153
-
154
- const withSystemPrompt = OpenAiLanguageModel.withConfigOverride({
155
- store: false,
156
- instructions: system,
157
- })
158
-
159
132
  let agentCounter = 0
160
133
 
161
134
  const outputBuffer = new Map<number, Array<Output>>()
@@ -166,13 +139,35 @@ ${agentsMd.value}
166
139
  prompt: Prompt.Prompt,
167
140
  ) => Stream.Stream<
168
141
  Output,
169
- AgentFinished,
142
+ AgentFinished | AiError.AiError,
170
143
  LanguageModel.LanguageModel | ProviderName
171
144
  > = Effect.fnUntraced(function* (agentId, prompt) {
172
145
  const ai = yield* LanguageModel.LanguageModel
173
- const provider = yield* ProviderName
146
+ const modelConfig = yield* AgentModelConfig
147
+ const singleToolMode = modelConfig.supportsNoTools !== false
174
148
  const deferred = yield* Deferred.make<string>()
175
- const output = yield* Queue.make<Output, AgentFinished>()
149
+ const output = yield* Queue.make<Output, AgentFinished | AiError.AiError>()
150
+
151
+ let system = generateSystem(allToolsDts, !singleToolMode)
152
+
153
+ if (Option.isSome(agentsMd)) {
154
+ system += `
155
+ # AGENTS.md
156
+
157
+ The following instructions are from ./AGENTS.md in the current directory.
158
+ You do not need to read this file again.
159
+
160
+ **ALWAYS follow these instructions when completing tasks**:
161
+
162
+ <!-- AGENTS.md start -->
163
+ ${agentsMd.value}
164
+ <!-- AGENTS.md end -->
165
+ `
166
+ }
167
+
168
+ if (options.system) {
169
+ system += `\n${options.system}\n`
170
+ }
176
171
 
177
172
  function maybeSend(agentId: number, part: Output, lock = false) {
178
173
  if (currentOutputAgent === null || currentOutputAgent === agentId) {
@@ -233,13 +228,14 @@ ${prompt}`),
233
228
  return Effect.void
234
229
  }),
235
230
  Effect.as(""),
236
- Effect.catch((finished) => {
231
+ Effect.catchTag("AgentFinished", (finished) => {
237
232
  Queue.offerUnsafe(
238
233
  output,
239
234
  new SubagentComplete({ id, summary: finished.summary }),
240
235
  )
241
236
  return Effect.succeed(finished.summary)
242
237
  }),
238
+ Effect.orDie,
243
239
  )
244
240
  }).pipe(
245
241
  options.subagentModel
@@ -252,25 +248,31 @@ ${prompt}`),
252
248
  ServiceMap.add(TaskCompleteDeferred, deferred),
253
249
  )
254
250
 
255
- if (provider !== "openai") {
251
+ const executeScript = Effect.fnUntraced(function* (script: string) {
252
+ maybeSend(agentId, new ScriptEnd())
253
+ const output = yield* pipe(
254
+ executor.execute({ tools, script }),
255
+ Stream.mkString,
256
+ Effect.provideServices(taskServices),
257
+ )
258
+ maybeSend(agentId, new ScriptOutput({ output }))
259
+ return output
260
+ })
261
+
262
+ if (!modelConfig.systemPromptTransform) {
256
263
  prompt = Prompt.setSystem(prompt, system)
257
264
  }
258
265
 
259
266
  let currentScript = ""
260
267
  yield* Effect.gen(function* () {
261
268
  while (true) {
262
- if (currentScript.length > 0) {
263
- maybeSend(agentId, new ScriptStart({ script: currentScript }))
264
- const result = yield* pipe(
265
- executor.execute({
266
- tools,
267
- script: currentScript,
268
- }),
269
- Stream.mkString,
270
- )
271
- maybeSend(agentId, new ScriptEnd({ output: result }))
269
+ if (!singleToolMode && currentScript.length > 0) {
270
+ const result = yield* executeScript(currentScript)
272
271
  prompt = Prompt.concat(prompt, [
273
- { role: "assistant", content: `Javascript output:\n\n${result}` },
272
+ {
273
+ role: modelConfig.supportsAssistantPrefill ? "assistant" : "user",
274
+ content: `Javascript output:\n\n${result}`,
275
+ },
274
276
  ])
275
277
  currentScript = ""
276
278
  }
@@ -297,16 +299,26 @@ ${prompt}`),
297
299
  pendingMessages.clear()
298
300
  }
299
301
 
300
- let response = Array.empty<StreamPart<{}>>()
302
+ // oxlint-disable-next-line typescript/no-explicit-any
303
+ let response = Array.empty<StreamPart<any>>()
301
304
  let reasoningStarted = false
302
305
  let hadReasoningDelta = false
303
306
  yield* pipe(
304
- ai.streamText({ prompt }),
307
+ ai.streamText(
308
+ singleToolMode ? { prompt, toolkit: singleTool } : { prompt },
309
+ ),
305
310
  Stream.takeUntil((part) => {
306
- if (part.type === "text-end" && currentScript.trim().length > 0) {
311
+ if (
312
+ !singleToolMode &&
313
+ part.type === "text-end" &&
314
+ currentScript.trim().length > 0
315
+ ) {
307
316
  return true
308
317
  }
309
- if (part.type === "reasoning-end" && pendingMessages.size > 0) {
318
+ if (
319
+ (part.type === "text-end" || part.type === "reasoning-end") &&
320
+ pendingMessages.size > 0
321
+ ) {
310
322
  return true
311
323
  }
312
324
  return false
@@ -317,11 +329,43 @@ ${prompt}`),
317
329
  for (const part of parts) {
318
330
  switch (part.type) {
319
331
  case "text-start":
332
+ if (singleToolMode) {
333
+ reasoningStarted = true
334
+ break
335
+ }
320
336
  currentScript = ""
321
337
  break
322
- case "text-delta":
338
+ case "text-delta": {
339
+ if (singleToolMode) {
340
+ hadReasoningDelta = true
341
+ if (reasoningStarted) {
342
+ reasoningStarted = false
343
+ maybeSend(agentId, new ReasoningStart(), true)
344
+ }
345
+ maybeSend(
346
+ agentId,
347
+ new ReasoningDelta({ delta: part.delta }),
348
+ )
349
+ break
350
+ }
351
+ if (currentScript === "" && part.delta.length > 0) {
352
+ maybeSend(agentId, new ScriptStart(), true)
353
+ }
354
+ maybeSend(agentId, new ScriptDelta({ delta: part.delta }))
323
355
  currentScript += part.delta
324
356
  break
357
+ }
358
+ case "text-end": {
359
+ if (singleToolMode) {
360
+ reasoningStarted = false
361
+ if (hadReasoningDelta) {
362
+ hadReasoningDelta = false
363
+ const sent = maybeSend(agentId, new ReasoningEnd())
364
+ if (sent) flushBuffer()
365
+ }
366
+ }
367
+ break
368
+ }
325
369
  case "reasoning-start":
326
370
  reasoningStarted = true
327
371
  break
@@ -354,14 +398,25 @@ ${prompt}`),
354
398
  return err.isRetryable
355
399
  },
356
400
  }),
357
- provider === "openai" ? withSystemPrompt : identity,
401
+ modelConfig.systemPromptTransform
402
+ ? (effect) => modelConfig.systemPromptTransform!(system, effect)
403
+ : identity,
358
404
  )
359
405
  prompt = Prompt.concat(prompt, Prompt.fromResponseParts(response))
360
406
  currentScript = currentScript.trim()
361
407
  }
362
408
  }).pipe(
363
- Effect.provideServices(taskServices),
409
+ Effect.provideServices(
410
+ taskServices.pipe(
411
+ ServiceMap.add(ScriptExecutor, (script) => {
412
+ maybeSend(agentId, new ScriptStart())
413
+ maybeSend(agentId, new ScriptDelta({ delta: script }))
414
+ return executeScript(script)
415
+ }),
416
+ ),
417
+ ),
364
418
  Effect.provideServices(services),
419
+ Effect.catchCause((cause) => Queue.failCause(output, cause)),
365
420
  Effect.forkScoped,
366
421
  )
367
422
 
@@ -386,28 +441,18 @@ ${prompt}`),
386
441
  // oxlint-disable-next-line typescript/no-explicit-any
387
442
  }) as any
388
443
 
389
- // oxlint-disable-next-line typescript/no-explicit-any
390
- const generateSystem = Effect.fn(function* (tools: Toolkit.Toolkit<any>) {
391
- const renderer = yield* ToolkitRenderer
444
+ const generateSystem = (
445
+ // oxlint-disable-next-line typescript/no-explicit-any
446
+ toolsDts: string,
447
+ multi: boolean,
448
+ ) => {
449
+ const toolMd = multi
450
+ ? generateSystemMulti(toolsDts)
451
+ : generateSystemSingle(toolsDts)
392
452
 
393
453
  return `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.
394
454
 
395
- To do your job, only respond with javascript code that will be executed for you.
396
-
397
- - Do not add any markdown formatting, just code.
398
- - Use \`console.log\` to print any output you need.
399
- - Top level await is supported.
400
- - **Prefer using the functions provided** over the bash tool
401
-
402
- **When you have completed your task**, call the "taskComplete" function with the final output.
403
-
404
- You have the following functions available to you:
405
-
406
- \`\`\`ts
407
- ${renderer.render(tools)}
408
-
409
- declare const fetch: typeof globalThis.fetch
410
- \`\`\`
455
+ ${toolMd}
411
456
 
412
457
  Here is how you would read a file:
413
458
 
@@ -438,8 +483,96 @@ Javascript output:
438
483
  - Only add comments when necessary.
439
484
  - Use the "subagent" tool to delegate large tasks / exploration. Run multiple subagents in parallel with Promise.all
440
485
  `
486
+ }
487
+
488
+ // oxlint-disable-next-line typescript/no-explicit-any
489
+ const generateSystemMulti = (toolsDts: string) => {
490
+ return `From now on only respond with javascript code that will be executed for you.
491
+
492
+ - Use \`console.log\` to print any output you need.
493
+ - Top level await is supported.
494
+ - **Prefer using the functions provided** over the bash tool
495
+
496
+ **When you have completed your task**, call the "taskComplete" function with the final output.
497
+
498
+ You have the following functions available to you:
499
+
500
+ \`\`\`ts
501
+ ${toolsDts}
502
+
503
+ declare const fetch: typeof globalThis.fetch
504
+ \`\`\``
505
+ }
506
+
507
+ // oxlint-disable-next-line typescript/no-explicit-any
508
+ const generateSystemSingle = (toolsDts: string) => {
509
+ return `Use the "execute" tool to run javascript code to do your work.
510
+
511
+ - Use \`console.log\` to print any output you need.
512
+ - Top level await is supported.
513
+ - **Prefer using the functions provided** over the bash tool
514
+
515
+ You have the following functions available to you:
516
+
517
+ \`\`\`ts
518
+ ${toolsDts}
519
+
520
+ declare const fetch: typeof globalThis.fetch
521
+ \`\`\``
522
+ }
523
+
524
+ class ScriptExecutor extends ServiceMap.Service<
525
+ ScriptExecutor,
526
+ (script: string) => Effect.Effect<string>
527
+ >()("clanka/Agent/ScriptExecutor") {}
528
+
529
+ const SingleTools = Toolkit.make(
530
+ Tool.make("execute", {
531
+ description: "Execute javascript code and return the output",
532
+ parameters: Schema.Struct({
533
+ script: Schema.String,
534
+ }),
535
+ success: Schema.String,
536
+ dependencies: [ScriptExecutor],
537
+ }),
538
+ )
539
+ const SingleToolHandlers = SingleTools.toLayer({
540
+ execute: Effect.fnUntraced(function* ({ script }) {
541
+ const execute = yield* ScriptExecutor
542
+ return yield* execute(script)
543
+ }),
441
544
  })
442
545
 
546
+ /**
547
+ * @since 1.0.0
548
+ * @category System prompts
549
+ */
550
+ export class AgentModelConfig extends ServiceMap.Reference<{
551
+ readonly systemPromptTransform?: <A, E, R>(
552
+ system: string,
553
+ effect: Effect.Effect<A, E, R>,
554
+ ) => Effect.Effect<A, E, R>
555
+ readonly supportsAssistantPrefill?: boolean | undefined
556
+ readonly supportsNoTools?: boolean | undefined
557
+ }>("clanka/Agent/SystemPromptTransform", {
558
+ defaultValue: () => ({}),
559
+ }) {
560
+ static readonly layer = (options: typeof AgentModelConfig.Service) =>
561
+ Layer.succeed(AgentModelConfig, options)
562
+ }
563
+
564
+ /**
565
+ * A layer that provides most of the common services needed to run an agent.
566
+ *
567
+ * @since 1.0.0
568
+ * @category Services
569
+ */
570
+ export const layerServices: Layer.Layer<
571
+ Tool.HandlersFor<typeof AgentTools.tools> | Executor | ToolkitRenderer,
572
+ never,
573
+ FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner
574
+ > = Layer.mergeAll(AgentToolHandlers, Executor.layer, ToolkitRenderer.layer)
575
+
443
576
  /**
444
577
  * @since 1.0.0
445
578
  * @category Output
@@ -475,8 +608,17 @@ export class ReasoningEnd extends Schema.TaggedClass<ReasoningEnd>()(
475
608
  */
476
609
  export class ScriptStart extends Schema.TaggedClass<ScriptStart>()(
477
610
  "ScriptStart",
611
+ {},
612
+ ) {}
613
+
614
+ /**
615
+ * @since 1.0.0
616
+ * @category Output
617
+ */
618
+ export class ScriptDelta extends Schema.TaggedClass<ScriptDelta>()(
619
+ "ScriptDelta",
478
620
  {
479
- script: Schema.String,
621
+ delta: Schema.String,
480
622
  },
481
623
  ) {}
482
624
 
@@ -484,9 +626,21 @@ export class ScriptStart extends Schema.TaggedClass<ScriptStart>()(
484
626
  * @since 1.0.0
485
627
  * @category Output
486
628
  */
487
- export class ScriptEnd extends Schema.TaggedClass<ScriptEnd>()("ScriptEnd", {
488
- output: Schema.String,
489
- }) {}
629
+ export class ScriptEnd extends Schema.TaggedClass<ScriptEnd>()(
630
+ "ScriptEnd",
631
+ {},
632
+ ) {}
633
+
634
+ /**
635
+ * @since 1.0.0
636
+ * @category Output
637
+ */
638
+ export class ScriptOutput extends Schema.TaggedClass<ScriptOutput>()(
639
+ "ScriptOutput",
640
+ {
641
+ output: Schema.String,
642
+ },
643
+ ) {}
490
644
 
491
645
  /**
492
646
  * @since 1.0.0
@@ -534,14 +688,18 @@ export type ContentPart =
534
688
  | ReasoningDelta
535
689
  | ReasoningEnd
536
690
  | ScriptStart
691
+ | ScriptDelta
537
692
  | ScriptEnd
693
+ | ScriptOutput
538
694
 
539
695
  export const ContentPart = Schema.Union([
540
696
  ReasoningStart,
541
697
  ReasoningDelta,
542
698
  ReasoningEnd,
543
699
  ScriptStart,
700
+ ScriptDelta,
544
701
  ScriptEnd,
702
+ ScriptOutput,
545
703
  ])
546
704
 
547
705
  /**
@@ -575,7 +733,9 @@ export const Output = Schema.Union([
575
733
  ReasoningDelta,
576
734
  ReasoningEnd,
577
735
  ScriptStart,
736
+ ScriptDelta,
578
737
  ScriptEnd,
738
+ ScriptOutput,
579
739
  SubagentStart,
580
740
  SubagentComplete,
581
741
  SubagentPart,
package/src/Codex.ts CHANGED
@@ -1,14 +1,29 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
- import { OpenAiClient } from "@effect/ai-openai"
4
+ import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai"
5
5
  import { Layer } from "effect"
6
6
  import { CodexAuth } from "./CodexAuth.ts"
7
+ import { AgentModelConfig } from "./Agent.ts"
7
8
 
8
9
  /**
9
10
  * @since 1.0.0
10
11
  * @category Layers
11
12
  */
12
- export const CodexAiClient = OpenAiClient.layer({
13
+ export const layerModelConfig = AgentModelConfig.layer({
14
+ systemPromptTransform: (system, effect) =>
15
+ OpenAiLanguageModel.withConfigOverride(effect, {
16
+ store: false,
17
+ instructions: system,
18
+ }),
19
+ supportsAssistantPrefill: true,
20
+ supportsNoTools: true,
21
+ })
22
+
23
+ /**
24
+ * @since 1.0.0
25
+ * @category Layers
26
+ */
27
+ export const layer = OpenAiClient.layer({
13
28
  apiUrl: "https://chatgpt.com/backend-api/codex",
14
- }).pipe(Layer.provide(CodexAuth.layerClient))
29
+ }).pipe(Layer.merge(layerModelConfig), Layer.provide(CodexAuth.layerClient))
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import { OpenAiClient } from "@effect/ai-openai-compat"
5
+ import { Layer } from "effect"
6
+ import { API_URL, GithubCopilotAuth } from "./GithubCopilotAuth.ts"
7
+
8
+ /**
9
+ * @since 1.0.0
10
+ * @category Layers
11
+ */
12
+ export const layer = OpenAiClient.layer({
13
+ apiUrl: API_URL,
14
+ }).pipe(Layer.provide(GithubCopilotAuth.layerClient))