clanka 0.7.6 → 0.8.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.
- package/dist/Acp.d.ts +21 -2
- package/dist/Acp.d.ts.map +1 -1
- package/dist/Acp.js +68 -13
- package/dist/Acp.js.map +1 -1
- package/dist/Acp.test.js +118 -10
- package/dist/Acp.test.js.map +1 -1
- package/dist/AcpImage.test.js +1 -0
- package/dist/AcpImage.test.js.map +1 -1
- package/dist/Agent.d.ts.map +1 -1
- package/dist/Agent.js +10 -0
- package/dist/Agent.js.map +1 -1
- package/dist/Agent.test.js +47 -2
- package/dist/Agent.test.js.map +1 -1
- package/dist/AgentOutput.d.ts +8 -0
- package/dist/AgentOutput.d.ts.map +1 -1
- package/dist/AgentOutput.js +8 -0
- package/dist/AgentOutput.js.map +1 -1
- package/dist/AgentTools.d.ts.map +1 -1
- package/dist/AgentTools.js +22 -7
- package/dist/AgentTools.js.map +1 -1
- package/dist/AgentTools.test.d.ts +2 -0
- package/dist/AgentTools.test.d.ts.map +1 -0
- package/dist/AgentTools.test.js +63 -0
- package/dist/AgentTools.test.js.map +1 -0
- package/dist/bin/{Effect-DdkIoNCD.mjs → Effect-CeAMoJlg.mjs} +7 -2
- package/dist/bin/Effect-CeAMoJlg.mjs.map +1 -0
- package/dist/bin/{McpClient-0f-vTQC-.mjs → McpClient-zQibhsnA.mjs} +3 -3
- package/dist/bin/{McpClient-0f-vTQC-.mjs.map → McpClient-zQibhsnA.mjs.map} +1 -1
- package/dist/bin/{OutputFormatter-DN8OGwuK.mjs → OutputFormatter-C7Awn0DS.mjs} +3 -3
- package/dist/bin/{OutputFormatter-DN8OGwuK.mjs.map → OutputFormatter-C7Awn0DS.mjs.map} +1 -1
- package/dist/bin/{Schema-gTtf66A-.mjs → Schema-Bc06DxcV.mjs} +2 -2
- package/dist/bin/{Schema-gTtf66A-.mjs.map → Schema-Bc06DxcV.mjs.map} +1 -1
- package/dist/bin/{SemanticSearch-CBs6yELV.mjs → SemanticSearch-BPxQoRSC.mjs} +4 -4
- package/dist/bin/{SemanticSearch-CBs6yELV.mjs.map → SemanticSearch-BPxQoRSC.mjs.map} +1 -1
- package/dist/bin/{Stream-B5ebASRi.mjs → Stream-DlGXvw0L.mjs} +2 -2
- package/dist/bin/{Stream-B5ebASRi.mjs.map → Stream-DlGXvw0L.mjs.map} +1 -1
- package/dist/bin/cli.mjs +107 -40
- package/dist/bin/cli.mjs.map +1 -1
- package/dist/cli.js +17 -8
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/src/Acp.test.ts +188 -9
- package/src/Acp.ts +92 -17
- package/src/AcpImage.test.ts +1 -0
- package/src/Agent.test.ts +54 -2
- package/src/Agent.ts +10 -0
- package/src/AgentOutput.ts +8 -0
- package/src/AgentTools.test.ts +116 -0
- package/src/AgentTools.ts +32 -12
- package/src/cli.ts +22 -9
- package/dist/bin/Effect-DdkIoNCD.mjs.map +0 -1
package/src/Agent.test.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type * as Response from "effect/unstable/ai/Response"
|
|
|
18
18
|
import * as ResponseIdTracker from "effect/unstable/ai/ResponseIdTracker"
|
|
19
19
|
import * as Agent from "./Agent.ts"
|
|
20
20
|
import * as AgentExecutor from "./AgentExecutor.ts"
|
|
21
|
-
import
|
|
21
|
+
import * as AgentOutput from "./AgentOutput.ts"
|
|
22
22
|
import * as Compaction from "./Compaction.ts"
|
|
23
23
|
|
|
24
24
|
const capabilities = new AgentExecutor.Capabilities({
|
|
@@ -310,11 +310,19 @@ const toolCall = (id: string, script: string): Response.StreamPartEncoded => ({
|
|
|
310
310
|
const finish = (
|
|
311
311
|
contextTokens: number,
|
|
312
312
|
reason: "stop" | "tool-calls" = "tool-calls",
|
|
313
|
+
cache: {
|
|
314
|
+
readonly read?: number | undefined
|
|
315
|
+
readonly write?: number | undefined
|
|
316
|
+
} = {},
|
|
313
317
|
): Response.StreamPartEncoded => ({
|
|
314
318
|
type: "finish",
|
|
315
319
|
reason,
|
|
316
320
|
usage: {
|
|
317
|
-
inputTokens: {
|
|
321
|
+
inputTokens: {
|
|
322
|
+
total: contextTokens,
|
|
323
|
+
cacheRead: cache.read,
|
|
324
|
+
cacheWrite: cache.write,
|
|
325
|
+
},
|
|
318
326
|
outputTokens: { total: 10 },
|
|
319
327
|
},
|
|
320
328
|
})
|
|
@@ -524,6 +532,50 @@ it.layer(localExecutorLayer, { excludeTestServices: true })(
|
|
|
524
532
|
|
|
525
533
|
// --- tests -------------------------------------------------------------------
|
|
526
534
|
|
|
535
|
+
describe("Agent usage", () => {
|
|
536
|
+
it.effect("accumulates provider cache reads and writes", () =>
|
|
537
|
+
Effect.gen(function* () {
|
|
538
|
+
const { exit, outputs } = yield* runAgentCollect({
|
|
539
|
+
executor: makeExecutor(() => Stream.succeed("done")),
|
|
540
|
+
respond: (_call, index) => {
|
|
541
|
+
switch (index) {
|
|
542
|
+
case 0:
|
|
543
|
+
return Stream.fromIterable([
|
|
544
|
+
toolCall("call-1", "console.log('done')"),
|
|
545
|
+
finish(100, "tool-calls", { read: 25, write: 5 }),
|
|
546
|
+
])
|
|
547
|
+
case 1:
|
|
548
|
+
return Stream.fromIterable([
|
|
549
|
+
...text("complete"),
|
|
550
|
+
finish(60, "stop", { read: 15, write: 3 }),
|
|
551
|
+
])
|
|
552
|
+
default:
|
|
553
|
+
return Stream.die(`unexpected model call #${index}`)
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
assert.deepStrictEqual(exit, Exit.succeed("complete"))
|
|
559
|
+
assert.deepStrictEqual(outputsOfTag(outputs, "Usage"), [
|
|
560
|
+
new AgentOutput.Usage({
|
|
561
|
+
contextTokens: 100,
|
|
562
|
+
inputTokens: 100,
|
|
563
|
+
outputTokens: 10,
|
|
564
|
+
cacheRead: 25,
|
|
565
|
+
cacheWrite: 5,
|
|
566
|
+
}),
|
|
567
|
+
new AgentOutput.Usage({
|
|
568
|
+
contextTokens: 60,
|
|
569
|
+
inputTokens: 160,
|
|
570
|
+
outputTokens: 20,
|
|
571
|
+
cacheRead: 40,
|
|
572
|
+
cacheWrite: 8,
|
|
573
|
+
}),
|
|
574
|
+
])
|
|
575
|
+
}),
|
|
576
|
+
)
|
|
577
|
+
})
|
|
578
|
+
|
|
527
579
|
describe("Agent execute output cap", () => {
|
|
528
580
|
const dump = "DUMP-HEAD " + filler("log", 40_000) + " DUMP-TAIL"
|
|
529
581
|
|
package/src/Agent.ts
CHANGED
|
@@ -211,6 +211,8 @@ ${content}
|
|
|
211
211
|
const output = yield* Queue.make<Output, AgentFinished | AiError.AiError>()
|
|
212
212
|
let inputTokens = 0
|
|
213
213
|
let outputTokens = 0
|
|
214
|
+
let cacheRead = 0
|
|
215
|
+
let cacheWrite = 0
|
|
214
216
|
const prompt = opts.disableHistory ? MutableRef.make(Prompt.empty) : history
|
|
215
217
|
|
|
216
218
|
MutableRef.update(prompt, Prompt.concat(opts.prompt))
|
|
@@ -536,6 +538,12 @@ ${content}
|
|
|
536
538
|
if (usage.outputTokens.total !== undefined) {
|
|
537
539
|
outputTokens += usage.outputTokens.total
|
|
538
540
|
}
|
|
541
|
+
if (usage.inputTokens.cacheRead !== undefined) {
|
|
542
|
+
cacheRead += usage.inputTokens.cacheRead
|
|
543
|
+
}
|
|
544
|
+
if (usage.inputTokens.cacheWrite !== undefined) {
|
|
545
|
+
cacheWrite += usage.inputTokens.cacheWrite
|
|
546
|
+
}
|
|
539
547
|
if (usage.inputTokens.total !== undefined) {
|
|
540
548
|
lastContextTokens = usage.inputTokens.total
|
|
541
549
|
inputTokens += usage.inputTokens.total
|
|
@@ -545,6 +553,8 @@ ${content}
|
|
|
545
553
|
contextTokens: usage.inputTokens.total,
|
|
546
554
|
inputTokens,
|
|
547
555
|
outputTokens,
|
|
556
|
+
cacheRead,
|
|
557
|
+
cacheWrite,
|
|
548
558
|
}),
|
|
549
559
|
})
|
|
550
560
|
}
|
package/src/AgentOutput.ts
CHANGED
|
@@ -87,6 +87,14 @@ export class Usage extends Schema.TaggedClass<Usage>()("Usage", {
|
|
|
87
87
|
contextTokens: Schema.Number,
|
|
88
88
|
inputTokens: Schema.Number,
|
|
89
89
|
outputTokens: Schema.Number,
|
|
90
|
+
/**
|
|
91
|
+
* Cumulative cache reads included in `inputTokens`. Zero is not proof of no cache activity.
|
|
92
|
+
*/
|
|
93
|
+
cacheRead: Schema.Number,
|
|
94
|
+
/**
|
|
95
|
+
* Cumulative cache writes included in `inputTokens`. Zero is not proof of no cache activity.
|
|
96
|
+
*/
|
|
97
|
+
cacheWrite: Schema.Number,
|
|
90
98
|
}) {}
|
|
91
99
|
|
|
92
100
|
/**
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { assert, describe, it } from "@effect/vitest"
|
|
2
|
+
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
|
|
3
|
+
import * as NodeServices from "@effect/platform-node/NodeServices"
|
|
4
|
+
import * as Effect from "effect/Effect"
|
|
5
|
+
import * as FileSystem from "effect/FileSystem"
|
|
6
|
+
import * as Layer from "effect/Layer"
|
|
7
|
+
import * as Path from "effect/Path"
|
|
8
|
+
import * as AgentExecutor from "./AgentExecutor.ts"
|
|
9
|
+
|
|
10
|
+
const match = "ignored-directory-regression-match"
|
|
11
|
+
|
|
12
|
+
const withProject = <A, E, R>(f: Effect.Effect<A, E, R>) =>
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const fs = yield* FileSystem.FileSystem
|
|
15
|
+
const path = yield* Path.Path
|
|
16
|
+
const project = yield* fs.makeTempDirectoryScoped()
|
|
17
|
+
yield* fs.makeDirectory(path.join(project, ".git"))
|
|
18
|
+
yield* fs.writeFileString(
|
|
19
|
+
path.join(project, ".gitignore"),
|
|
20
|
+
"node_modules/\n",
|
|
21
|
+
)
|
|
22
|
+
yield* fs.makeDirectory(path.join(project, "src"))
|
|
23
|
+
yield* fs.makeDirectory(path.join(project, ".git", "src"))
|
|
24
|
+
yield* fs.makeDirectory(path.join(project, "node_modules", "dependency"), {
|
|
25
|
+
recursive: true,
|
|
26
|
+
})
|
|
27
|
+
yield* fs.makeDirectory(
|
|
28
|
+
path.join(project, "node_modules", "dependency", "src"),
|
|
29
|
+
)
|
|
30
|
+
yield* fs.writeFileString(
|
|
31
|
+
path.join(project, "src", "visible.ts"),
|
|
32
|
+
`${match} visible`,
|
|
33
|
+
)
|
|
34
|
+
yield* fs.writeFileString(
|
|
35
|
+
path.join(project, "node_modules", "dependency", "ignored.ts"),
|
|
36
|
+
`${match} ignored`,
|
|
37
|
+
)
|
|
38
|
+
yield* fs.writeFileString(
|
|
39
|
+
path.join(project, "node_modules", "dependency", "src", "inner.ts"),
|
|
40
|
+
`${match} ignored dependency source`,
|
|
41
|
+
)
|
|
42
|
+
yield* fs.writeFileString(
|
|
43
|
+
path.join(project, ".git", "src", "hidden.ts"),
|
|
44
|
+
`${match} hidden git source`,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return yield* f.pipe(
|
|
48
|
+
Effect.provide(
|
|
49
|
+
AgentExecutor.layerLocal({ directory: project }).pipe(
|
|
50
|
+
Layer.provide(NodeHttpClient.layerUndici),
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped)
|
|
55
|
+
|
|
56
|
+
const rg = (glob?: string) =>
|
|
57
|
+
Effect.gen(function* () {
|
|
58
|
+
const executor = yield* AgentExecutor.AgentExecutor
|
|
59
|
+
const output = yield* executor.executeUnsafe({
|
|
60
|
+
tool: "rg",
|
|
61
|
+
params: { pattern: match, glob },
|
|
62
|
+
})
|
|
63
|
+
assert.isString(output)
|
|
64
|
+
return output
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe("rg", () => {
|
|
68
|
+
it.effect("does not search ignored directories by default", () =>
|
|
69
|
+
withProject(
|
|
70
|
+
Effect.gen(function* () {
|
|
71
|
+
const output = yield* rg()
|
|
72
|
+
assert.include(output, "src/visible.ts")
|
|
73
|
+
assert.notInclude(output, "node_modules/dependency/ignored.ts")
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
it.effect("does not search ignored directories for an ordinary glob", () =>
|
|
79
|
+
withProject(
|
|
80
|
+
Effect.gen(function* () {
|
|
81
|
+
const output = yield* rg("**/*.ts")
|
|
82
|
+
assert.include(output, "src/visible.ts")
|
|
83
|
+
assert.notInclude(output, "node_modules/dependency/ignored.ts")
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
it.effect("does not search ignored paths for an ordinary scoped glob", () =>
|
|
89
|
+
withProject(
|
|
90
|
+
Effect.gen(function* () {
|
|
91
|
+
const output = yield* rg("**/src/*.ts")
|
|
92
|
+
assert.include(output, "src/visible.ts")
|
|
93
|
+
assert.notInclude(output, "node_modules/dependency/src/inner.ts")
|
|
94
|
+
assert.notInclude(output, ".git/src/hidden.ts")
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
it.effect("searches an ignored directory explicitly named in the glob", () =>
|
|
100
|
+
withProject(
|
|
101
|
+
Effect.gen(function* () {
|
|
102
|
+
const output = yield* rg("**/node_modules/**")
|
|
103
|
+
assert.include(output, "node_modules/dependency/ignored.ts")
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
it.effect("searches a root-relative ignored directory glob", () =>
|
|
109
|
+
withProject(
|
|
110
|
+
Effect.gen(function* () {
|
|
111
|
+
const output = yield* rg("node_modules/**")
|
|
112
|
+
assert.include(output, "node_modules/dependency/ignored.ts")
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
})
|
package/src/AgentTools.ts
CHANGED
|
@@ -82,6 +82,12 @@ export class ImageAttacher extends Context.Service<
|
|
|
82
82
|
(image: ImageAttachment) => Effect.Effect<void>
|
|
83
83
|
>()("clanka/AgentTools/ImageAttacher") {}
|
|
84
84
|
|
|
85
|
+
const globNamesDirectory = (glob: string) =>
|
|
86
|
+
glob
|
|
87
|
+
.split("/")
|
|
88
|
+
.slice(0, -1)
|
|
89
|
+
.some((segment) => segment.length > 0 && !/[*?[\]{}]/.test(segment))
|
|
90
|
+
|
|
85
91
|
/**
|
|
86
92
|
* @since 1.0.0
|
|
87
93
|
* @category Context
|
|
@@ -440,24 +446,38 @@ export const AgentToolHandlersNoDeps = AgentToolsWithSearch.toLayer(
|
|
|
440
446
|
if (options.filesOnly) {
|
|
441
447
|
args.push("--files-with-matches")
|
|
442
448
|
}
|
|
449
|
+
const searchesIgnoredFiles =
|
|
450
|
+
options.glob !== undefined && !options.glob.startsWith("*")
|
|
443
451
|
if (options.glob) {
|
|
444
452
|
args.push("--glob", options.glob)
|
|
445
|
-
if (
|
|
453
|
+
if (searchesIgnoredFiles) {
|
|
446
454
|
args.push("-uu")
|
|
447
455
|
}
|
|
448
456
|
}
|
|
449
457
|
args.push(options.pattern)
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
458
|
+
const run = Effect.fnUntraced(function* (args: Array<string>) {
|
|
459
|
+
let stream = spawner.streamLines(
|
|
460
|
+
ChildProcess.make("rg", args, {
|
|
461
|
+
cwd,
|
|
462
|
+
stdin: "ignore",
|
|
463
|
+
}),
|
|
464
|
+
)
|
|
465
|
+
stream = Stream.take(stream, options.maxLines ?? 500)
|
|
466
|
+
return yield* Stream.runCollect(stream).pipe(
|
|
467
|
+
Effect.map(Array.join("\n")),
|
|
468
|
+
Effect.orDie,
|
|
469
|
+
)
|
|
470
|
+
})
|
|
471
|
+
const output = yield* run(args)
|
|
472
|
+
if (
|
|
473
|
+
output.length > 0 ||
|
|
474
|
+
searchesIgnoredFiles ||
|
|
475
|
+
options.glob === undefined ||
|
|
476
|
+
!globNamesDirectory(options.glob)
|
|
477
|
+
) {
|
|
478
|
+
return output
|
|
479
|
+
}
|
|
480
|
+
return yield* run(args.slice(0, -1).concat("-uu", options.pattern))
|
|
461
481
|
}),
|
|
462
482
|
glob: Effect.fn("AgentTools.glob")(function* (pattern) {
|
|
463
483
|
yield* Effect.logInfo(`Calling "glob"`).pipe(
|
package/src/cli.ts
CHANGED
|
@@ -64,14 +64,25 @@ const modelLayer = (provider: Provider, model: string, effort: string) =>
|
|
|
64
64
|
Layer.provide(Copilot.layerClient),
|
|
65
65
|
)
|
|
66
66
|
|
|
67
|
+
const isProvider = (provider: string | undefined): provider is Provider =>
|
|
68
|
+
providers.includes(provider as Provider)
|
|
69
|
+
|
|
70
|
+
// `<provider>/<model>/<effort>` as accepted by the `acp` --model flag.
|
|
67
71
|
const parseModelId = (modelId: string) => {
|
|
68
72
|
const [provider, model, effort, ...rest] = modelId.split("/")
|
|
69
|
-
return provider
|
|
70
|
-
providers.includes(provider as Provider) &&
|
|
73
|
+
return isProvider(provider) &&
|
|
71
74
|
model !== undefined &&
|
|
72
|
-
effort
|
|
75
|
+
Schema.is(Acp.ThoughtLevel)(effort) &&
|
|
73
76
|
rest.length === 0
|
|
74
|
-
? Option.some({
|
|
77
|
+
? Option.some({ model: `${provider}:${model}`, thoughtLevel: effort })
|
|
78
|
+
: Option.none()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// `<provider>:<model>` as advertised over ACP.
|
|
82
|
+
const parseAcpModelId = (modelId: string) => {
|
|
83
|
+
const [provider, model, ...rest] = modelId.split(":")
|
|
84
|
+
return isProvider(provider) && model !== undefined && rest.length === 0
|
|
85
|
+
? Option.some({ provider, model })
|
|
75
86
|
: Option.none()
|
|
76
87
|
}
|
|
77
88
|
|
|
@@ -80,7 +91,7 @@ const ModelId = Schema.String.pipe(
|
|
|
80
91
|
Schema.makeFilter(
|
|
81
92
|
(modelId: string) =>
|
|
82
93
|
Option.isSome(parseModelId(modelId)) ||
|
|
83
|
-
`Invalid model "${modelId}", expected <provider>/<model>/<effort
|
|
94
|
+
`Invalid model "${modelId}", expected <provider>/<model>/<effort> with effort one of ${Acp.ThoughtLevel.literals.join(", ")}`,
|
|
84
95
|
),
|
|
85
96
|
),
|
|
86
97
|
)
|
|
@@ -192,6 +203,7 @@ const acpModel = Flag.String("model").pipe(
|
|
|
192
203
|
),
|
|
193
204
|
Flag.withSchema(ModelId),
|
|
194
205
|
Flag.withDefault("openai/gpt-6-astra/medium"),
|
|
206
|
+
Flag.map((modelId) => Option.getOrThrow(parseModelId(modelId))),
|
|
195
207
|
)
|
|
196
208
|
|
|
197
209
|
const acp = Command.make("acp", { model: acpModel, compaction }).pipe(
|
|
@@ -201,14 +213,15 @@ const acp = Command.make("acp", { model: acpModel, compaction }).pipe(
|
|
|
201
213
|
Command.withHandler(({ model }) =>
|
|
202
214
|
Acp.runStdio({
|
|
203
215
|
version,
|
|
204
|
-
defaultModel: model,
|
|
216
|
+
defaultModel: model.model,
|
|
217
|
+
defaultThoughtLevel: model.thoughtLevel,
|
|
205
218
|
makeAgent: (cwd) =>
|
|
206
219
|
Agent.make.pipe(
|
|
207
220
|
Effect.provide(AgentExecutor.layerLocal({ directory: cwd })),
|
|
208
221
|
),
|
|
209
|
-
makeModel: (modelId) =>
|
|
210
|
-
Option.map(
|
|
211
|
-
modelLayer(provider, model,
|
|
222
|
+
makeModel: (modelId, thoughtLevel) =>
|
|
223
|
+
Option.map(parseAcpModelId(modelId), ({ provider, model }) =>
|
|
224
|
+
modelLayer(provider, model, thoughtLevel),
|
|
212
225
|
),
|
|
213
226
|
}),
|
|
214
227
|
),
|