clanka 0.3.1 → 0.4.0

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 (69) hide show
  1. package/dist/Acp.d.ts.map +1 -1
  2. package/dist/Acp.js +3 -0
  3. package/dist/Acp.js.map +1 -1
  4. package/dist/Acp.test.js +58 -0
  5. package/dist/Acp.test.js.map +1 -1
  6. package/dist/Agent.d.ts.map +1 -1
  7. package/dist/Agent.js +86 -8
  8. package/dist/Agent.js.map +1 -1
  9. package/dist/Agent.test.js +671 -0
  10. package/dist/Agent.test.js.map +1 -1
  11. package/dist/AgentExecutor.d.ts +5 -0
  12. package/dist/AgentExecutor.d.ts.map +1 -1
  13. package/dist/AgentExecutor.js +22 -0
  14. package/dist/AgentExecutor.js.map +1 -1
  15. package/dist/AgentOutput.d.ts +49 -4
  16. package/dist/AgentOutput.d.ts.map +1 -1
  17. package/dist/AgentOutput.js +45 -0
  18. package/dist/AgentOutput.js.map +1 -1
  19. package/dist/AgentSkills.d.ts +56 -0
  20. package/dist/AgentSkills.d.ts.map +1 -0
  21. package/dist/AgentSkills.js +126 -0
  22. package/dist/AgentSkills.js.map +1 -0
  23. package/dist/AgentSkills.test.d.ts +2 -0
  24. package/dist/AgentSkills.test.d.ts.map +1 -0
  25. package/dist/AgentSkills.test.js +356 -0
  26. package/dist/AgentSkills.test.js.map +1 -0
  27. package/dist/Codex.js +1 -1
  28. package/dist/Codex.js.map +1 -1
  29. package/dist/Compaction.d.ts +342 -0
  30. package/dist/Compaction.d.ts.map +1 -0
  31. package/dist/Compaction.js +566 -0
  32. package/dist/Compaction.js.map +1 -0
  33. package/dist/Compaction.test.d.ts +2 -0
  34. package/dist/Compaction.test.d.ts.map +1 -0
  35. package/dist/Compaction.test.js +576 -0
  36. package/dist/Compaction.test.js.map +1 -0
  37. package/dist/CompactionTransport.test.d.ts +2 -0
  38. package/dist/CompactionTransport.test.d.ts.map +1 -0
  39. package/dist/CompactionTransport.test.js +123 -0
  40. package/dist/CompactionTransport.test.js.map +1 -0
  41. package/dist/Copilot.d.ts.map +1 -1
  42. package/dist/Copilot.js +7 -2
  43. package/dist/Copilot.js.map +1 -1
  44. package/dist/OutputFormatter.d.ts.map +1 -1
  45. package/dist/OutputFormatter.js +9 -0
  46. package/dist/OutputFormatter.js.map +1 -1
  47. package/dist/cli.js +13 -4
  48. package/dist/cli.js.map +1 -1
  49. package/dist/index.d.ts +5 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +5 -0
  52. package/dist/index.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/Acp.test.ts +85 -0
  55. package/src/Acp.ts +2 -0
  56. package/src/Agent.test.ts +994 -0
  57. package/src/Agent.ts +127 -6
  58. package/src/AgentExecutor.ts +27 -0
  59. package/src/AgentOutput.ts +58 -0
  60. package/src/AgentSkills.test.ts +652 -0
  61. package/src/AgentSkills.ts +155 -0
  62. package/src/Codex.ts +3 -1
  63. package/src/Compaction.test.ts +824 -0
  64. package/src/Compaction.ts +788 -0
  65. package/src/CompactionTransport.test.ts +228 -0
  66. package/src/Copilot.ts +8 -1
  67. package/src/OutputFormatter.ts +9 -0
  68. package/src/cli.ts +26 -5
  69. package/src/index.ts +6 -0
package/src/Agent.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  import * as Model from "effect/unstable/ai/Model"
5
5
  import type * as Response from "effect/unstable/ai/Response"
6
6
  import * as AgentExecutor from "./AgentExecutor.ts"
7
+ import * as AgentSkills from "./AgentSkills.ts"
7
8
  import { stripWrappingCodeFence } from "./ScriptExtraction.ts"
8
9
  import type * as Path from "effect/Path"
9
10
  import type * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"
@@ -36,6 +37,8 @@ import * as Duration from "effect/Duration"
36
37
  import * as Cause from "effect/Cause"
37
38
  import * as Latch from "effect/Latch"
38
39
  import * as Clock from "effect/Clock"
40
+ import * as Exit from "effect/Exit"
41
+ import * as Compaction from "./Compaction.ts"
39
42
  import {
40
43
  ScriptEnd,
41
44
  ScriptOutput,
@@ -52,6 +55,9 @@ import {
52
55
  ScriptStart,
53
56
  ScriptDelta,
54
57
  AgentStart,
58
+ ExecuteOutputCapped,
59
+ CompactionStarted,
60
+ CompactionEnded,
55
61
  } from "./AgentOutput.ts"
56
62
 
57
63
  /**
@@ -187,6 +193,9 @@ ${content}
187
193
  const conversationMode = yield* ConversationMode
188
194
  const turnTimeout = yield* TurnTimeout
189
195
  let finalSummary = Option.none<string>()
196
+ // `inputTokens.total` from the most recent finish part; undefined until
197
+ // the first response and after every compaction.
198
+ let lastContextTokens: number | undefined = undefined
190
199
 
191
200
  const output = yield* Queue.make<Output, AgentFinished | AiError.AiError>()
192
201
  let inputTokens = 0
@@ -319,6 +328,50 @@ ${content}
319
328
  }
320
329
  })
321
330
 
331
+ // Compaction. `compactWithEvents` applies a successful rewrite and closes
332
+ // the progress event on every exit: success, failure, timeout, interrupt.
333
+ let compactionStarted = false
334
+ const onCompactionStart = (reason: Compaction.CompactionReason) =>
335
+ Effect.sync(() => {
336
+ compactionStarted = true
337
+ maybeSend({ agentId, part: new CompactionStarted({ reason }) })
338
+ })
339
+ const compactWithEvents = <E, R>(
340
+ reason: Compaction.CompactionReason,
341
+ compaction: Effect.Effect<
342
+ Option.Option<Compaction.CompactionResult>,
343
+ E,
344
+ R
345
+ >,
346
+ ) =>
347
+ Effect.suspend(() => {
348
+ compactionStarted = false
349
+ return compaction
350
+ }).pipe(
351
+ Effect.onExit((exit) =>
352
+ Effect.sync(() => {
353
+ if (!compactionStarted) return
354
+ const result = Exit.isSuccess(exit) ? exit.value : Option.none()
355
+ let tokensBefore: number
356
+ let tokensAfter: number
357
+ if (Option.isSome(result)) {
358
+ MutableRef.set(prompt, result.value.prompt)
359
+ lastContextTokens = undefined
360
+ tokensBefore = result.value.tokensBefore
361
+ tokensAfter = result.value.tokensAfter
362
+ } else {
363
+ tokensBefore = tokensAfter = Compaction.estimateTokens(
364
+ prompt.current,
365
+ )
366
+ }
367
+ maybeSend({
368
+ agentId,
369
+ part: new CompactionEnded({ reason, tokensBefore, tokensAfter }),
370
+ })
371
+ }),
372
+ ),
373
+ )
374
+
322
375
  // Script execution
323
376
  const executeScript = Effect.fnUntraced(function* (script: string) {
324
377
  yield* toolLatchAcquire
@@ -335,8 +388,20 @@ ${content}
335
388
  }),
336
389
  Stream.mkString,
337
390
  )
391
+ // Always-on cap on what enters the Prompt. The ScriptOutput event keeps
392
+ // the full output for display; the formatter applies its own truncation.
393
+ const capped = Compaction.capOutput(output)
394
+ if (capped.capped) {
395
+ maybeSend({
396
+ agentId,
397
+ part: new ExecuteOutputCapped({
398
+ charsBefore: capped.charsBefore,
399
+ charsAfter: capped.charsAfter,
400
+ }),
401
+ })
402
+ }
338
403
  maybeSend({ agentId, part: new ScriptOutput({ output }) })
339
- return output
404
+ return capped.output
340
405
  }, Effect.ensuring(toolLatchRelease))
341
406
 
342
407
  if (!modelConfig.systemPromptTransform) {
@@ -369,12 +434,29 @@ ${content}
369
434
  pendingMessages.clear()
370
435
  }
371
436
 
437
+ // Threshold compaction before the next model call
438
+ yield* compactWithEvents(
439
+ "threshold",
440
+ Compaction.compactIfNeeded({
441
+ prompt: prompt.current,
442
+ contextTokens: lastContextTokens,
443
+ onStart: onCompactionStart,
444
+ }).pipe(Effect.timeout(turnTimeout)),
445
+ ).pipe(
446
+ Effect.catchTag("TimeoutError", (error) =>
447
+ Effect.logWarning(
448
+ "Compaction timed out, continuing with the uncompacted prompt",
449
+ error,
450
+ ),
451
+ ),
452
+ )
453
+
372
454
  // oxlint-disable-next-line typescript/no-explicit-any
373
455
  let response = Array.empty<Response.StreamPart<any>>()
374
456
  let reasoningStarted = false
375
457
  let hadReasoningDelta = false
376
458
  let hadToolCall = false
377
- yield* pipe(
459
+ const runModel = pipe(
378
460
  Stream.suspend(() =>
379
461
  ai.streamText({ prompt: prompt.current, toolkit: singleTool }),
380
462
  ),
@@ -431,6 +513,7 @@ ${content}
431
513
  outputTokens += usage.outputTokens.total
432
514
  }
433
515
  if (usage.inputTokens.total !== undefined) {
516
+ lastContextTokens = usage.inputTokens.total
434
517
  inputTokens += usage.inputTokens.total
435
518
  maybeSend({
436
519
  agentId,
@@ -449,6 +532,12 @@ ${content}
449
532
  }
450
533
  return Effect.void
451
534
  }),
535
+ modelConfig.systemPromptTransform
536
+ ? (effect) => modelConfig.systemPromptTransform!(system, effect)
537
+ : identity,
538
+ )
539
+ const attempt = pipe(
540
+ runModel,
452
541
  Effect.raceFirst(turnTimeoutEffect),
453
542
  Effect.retry({
454
543
  while: (err) => {
@@ -468,6 +557,9 @@ ${content}
468
557
  response = []
469
558
  return true
470
559
  }
560
+ // Never retry a context-length error as is: the overflow
561
+ // handler below compacts first.
562
+ if (Compaction.isContextLengthError(err)) return false
471
563
  if (err.isRetryable) {
472
564
  maybeSend({ agentId, part: new ErrorRetry({ error: err }) })
473
565
  switch (err.reason._tag) {
@@ -488,9 +580,33 @@ ${content}
488
580
  schedule: retryPolicy,
489
581
  }),
490
582
  Effect.catchTag("TimeoutError", Effect.die),
491
- modelConfig.systemPromptTransform
492
- ? (effect) => modelConfig.systemPromptTransform!(system, effect)
493
- : identity,
583
+ )
584
+ // Overflow: compact once, retry the attempt once, then fail.
585
+ yield* attempt.pipe(
586
+ Effect.catchIf(
587
+ (err): err is AiError.AiError =>
588
+ Compaction.isContextLengthError(err),
589
+ (err) =>
590
+ Effect.gen(function* () {
591
+ response = []
592
+ const compacted = yield* compactWithEvents(
593
+ "overflow",
594
+ Compaction.compact({
595
+ prompt: prompt.current,
596
+ reason: "overflow",
597
+ onStart: onCompactionStart,
598
+ }).pipe(
599
+ Effect.timeout(turnTimeout),
600
+ Effect.retry({
601
+ while: (error) => error._tag === "TimeoutError",
602
+ times: 1,
603
+ }),
604
+ Effect.catchTag("TimeoutError", Effect.die),
605
+ ),
606
+ )
607
+ return Option.isSome(compacted) ? yield* attempt : yield* err
608
+ }),
609
+ ),
494
610
  )
495
611
  MutableRef.update(
496
612
  prompt,
@@ -620,7 +736,12 @@ ${capabilities.toolsDts}
620
736
 
621
737
  /** The global Fetch API available for making HTTP requests. */
622
738
  declare const fetch: typeof globalThis.fetch
623
- \`\`\``
739
+ \`\`\`${Option.match(AgentSkills.renderCatalog(capabilities.skills), {
740
+ onNone: () => "",
741
+ onSome: (catalog) => `
742
+
743
+ ${catalog}`,
744
+ })}`
624
745
 
625
746
  class ScriptExecutor extends Context.Service<
626
747
  ScriptExecutor,
@@ -2,6 +2,7 @@
2
2
  * @since 1.0.0
3
3
  */
4
4
  import * as NodeConsole from "node:console"
5
+ import * as NodeOs from "node:os"
5
6
  import * as NodeVm from "node:vm"
6
7
  import { Writable } from "node:stream"
7
8
  import {
@@ -13,6 +14,7 @@ import {
13
14
  TaskCompleter,
14
15
  } from "./AgentTools.ts"
15
16
  import { ToolkitRenderer } from "./ToolkitRenderer.ts"
17
+ import * as AgentSkills from "./AgentSkills.ts"
16
18
  import type * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"
17
19
  import type * as HttpClient from "effect/unstable/http/HttpClient"
18
20
  import * as Context from "effect/Context"
@@ -69,6 +71,13 @@ export class Capabilities extends Schema.Class<Capabilities>("Capabilities")({
69
71
  toolsDts: Schema.String,
70
72
  agentsMd: Schema.Option(Schema.String),
71
73
  supportsSearch: Schema.Boolean,
74
+ /**
75
+ * Skills available on the executor filesystem.
76
+ */
77
+ skills: Schema.Array(AgentSkills.Skill).pipe(
78
+ Schema.withDecodingDefaultKey(Effect.succeed([])),
79
+ Schema.withConstructorDefault(Effect.succeed([])),
80
+ ),
72
81
  }) {}
73
82
 
74
83
  /**
@@ -201,10 +210,18 @@ export const makeLocal = Effect.fnUntraced(function* <
201
210
  const agentsMd = yield* Effect.option(
202
211
  fs.readFileString(pathService.join(options.directory, "AGENTS.md")),
203
212
  )
213
+ const skills = yield* AgentSkills.discover({
214
+ directory: options.directory,
215
+ homeDirectory: homeDirectory(),
216
+ }).pipe(
217
+ Effect.provideService(FileSystem.FileSystem, fs),
218
+ Effect.provideService(Path.Path, pathService),
219
+ )
204
220
  return new Capabilities({
205
221
  toolsDts,
206
222
  agentsMd,
207
223
  supportsSearch: hasSearch,
224
+ skills,
208
225
  })
209
226
  }),
210
227
  execute,
@@ -500,6 +517,16 @@ const compileScript = (script: string): NodeVm.Script => {
500
517
 
501
518
  const defaultMain = () => Promise.resolve()
502
519
 
520
+ const homeDirectory = (): Option.Option<string> => {
521
+ const fromEnv = process.env.HOME
522
+ if (fromEnv) return Option.some(fromEnv)
523
+ try {
524
+ return Option.some(NodeOs.homedir())
525
+ } catch {
526
+ return Option.none()
527
+ }
528
+ }
529
+
503
530
  const makeConsole = Effect.fn(function* (
504
531
  queue: Queue.Queue<string, Cause.Done>,
505
532
  ) {
@@ -138,6 +138,58 @@ export class SubagentComplete extends Schema.TaggedClass<SubagentComplete>()(
138
138
  },
139
139
  ) {}
140
140
 
141
+ /**
142
+ * Emitted when an `execute` result exceeded the always-on output cap and was
143
+ * shortened before being appended to the Prompt.
144
+ *
145
+ * @since 1.0.0
146
+ * @category Output
147
+ */
148
+ export class ExecuteOutputCapped extends Schema.TaggedClass<ExecuteOutputCapped>()(
149
+ "ExecuteOutputCapped",
150
+ {
151
+ charsBefore: Schema.Number,
152
+ charsAfter: Schema.Number,
153
+ },
154
+ ) {}
155
+
156
+ /**
157
+ * @since 1.0.0
158
+ * @category Output
159
+ */
160
+ export const CompactionReason = Schema.Literals(["threshold", "overflow"])
161
+
162
+ /**
163
+ * Emitted when auto-compaction begins. `reason` is `threshold` when the
164
+ * context token count crossed the configured limit before a model call, and
165
+ * `overflow` when a context-length error from the provider triggered it.
166
+ *
167
+ * @since 1.0.0
168
+ * @category Output
169
+ */
170
+ export class CompactionStarted extends Schema.TaggedClass<CompactionStarted>()(
171
+ "CompactionStarted",
172
+ {
173
+ reason: CompactionReason,
174
+ },
175
+ ) {}
176
+
177
+ /**
178
+ * Emitted when auto-compaction finished and the Prompt was rewritten.
179
+ * `tokensBefore` / `tokensAfter` are estimates.
180
+ *
181
+ * @since 1.0.0
182
+ * @category Output
183
+ */
184
+ export class CompactionEnded extends Schema.TaggedClass<CompactionEnded>()(
185
+ "CompactionEnded",
186
+ {
187
+ reason: CompactionReason,
188
+ tokensBefore: Schema.Number,
189
+ tokensAfter: Schema.Number,
190
+ },
191
+ ) {}
192
+
141
193
  export type ContentPart =
142
194
  | ReasoningStart
143
195
  | ReasoningDelta
@@ -148,6 +200,9 @@ export type ContentPart =
148
200
  | ScriptOutput
149
201
  | Usage
150
202
  | ErrorRetry
203
+ | ExecuteOutputCapped
204
+ | CompactionStarted
205
+ | CompactionEnded
151
206
 
152
207
  export const ContentPart = Schema.Union([
153
208
  ReasoningStart,
@@ -159,6 +214,9 @@ export const ContentPart = Schema.Union([
159
214
  ScriptOutput,
160
215
  Usage,
161
216
  ErrorRetry,
217
+ ExecuteOutputCapped,
218
+ CompactionStarted,
219
+ CompactionEnded,
162
220
  ])
163
221
 
164
222
  /**