clanka 0.7.7 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clanka",
3
3
  "type": "module",
4
- "version": "0.7.7",
4
+ "version": "0.8.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
package/src/Acp.test.ts CHANGED
@@ -154,6 +154,108 @@ describe("Acp", () => {
154
154
  ),
155
155
  )
156
156
 
157
+ it.effect("reports cache usage in the ACP usage extension", () =>
158
+ withStore(
159
+ Effect.scoped(
160
+ Effect.gen(function* () {
161
+ const server = yield* makeServer(() =>
162
+ parts(
163
+ { type: "text-start", id: "1" },
164
+ { type: "text-delta", id: "1", delta: "done" },
165
+ { type: "text-end", id: "1" },
166
+ {
167
+ type: "finish",
168
+ reason: "stop",
169
+ usage: {
170
+ inputTokens: {
171
+ total: 120,
172
+ cacheRead: 30,
173
+ cacheWrite: 6,
174
+ },
175
+ outputTokens: { total: 12 },
176
+ },
177
+ },
178
+ ),
179
+ )
180
+ const created = yield* server.request(1, "session/new", {
181
+ cwd: "/tmp",
182
+ })
183
+ const sessionId: string = created.result.sessionId
184
+
185
+ yield* server.request(2, "session/prompt", {
186
+ sessionId,
187
+ prompt: [{ type: "text", text: "hello" }],
188
+ })
189
+
190
+ assert.deepStrictEqual(
191
+ server
192
+ .updates(sessionId)
193
+ .find((update) => update.sessionUpdate === "usage_update"),
194
+ {
195
+ sessionUpdate: "usage_update",
196
+ usage: {
197
+ inputTokens: 120,
198
+ outputTokens: 12,
199
+ cacheRead: 30,
200
+ cacheWrite: 6,
201
+ },
202
+ },
203
+ )
204
+ }),
205
+ ),
206
+ ),
207
+ )
208
+
209
+ it.effect(
210
+ "reports zero cache usage when the provider omits cache fields",
211
+ () =>
212
+ withStore(
213
+ Effect.scoped(
214
+ Effect.gen(function* () {
215
+ const server = yield* makeServer(() =>
216
+ parts(
217
+ { type: "text-start", id: "1" },
218
+ { type: "text-delta", id: "1", delta: "done" },
219
+ { type: "text-end", id: "1" },
220
+ {
221
+ type: "finish",
222
+ reason: "stop",
223
+ usage: {
224
+ inputTokens: { total: 120 },
225
+ outputTokens: { total: 12 },
226
+ },
227
+ },
228
+ ),
229
+ )
230
+ const created = yield* server.request(1, "session/new", {
231
+ cwd: "/tmp",
232
+ })
233
+ const sessionId: string = created.result.sessionId
234
+
235
+ yield* server.request(2, "session/prompt", {
236
+ sessionId,
237
+ prompt: [{ type: "text", text: "hello" }],
238
+ })
239
+
240
+ assert.deepStrictEqual(
241
+ server
242
+ .updates(sessionId)
243
+ .find((update) => update.sessionUpdate === "usage_update"),
244
+ {
245
+ sessionUpdate: "usage_update",
246
+ usage: {
247
+ inputTokens: 120,
248
+ outputTokens: 12,
249
+ cacheRead: 0,
250
+ cacheWrite: 0,
251
+ },
252
+ },
253
+ )
254
+ }),
255
+ ),
256
+ ),
257
+ )
258
+
157
259
  it.effect("applies model and thought-level changes to the next turn", () =>
158
260
  withStore(
159
261
  Effect.scoped(
package/src/Acp.ts CHANGED
@@ -655,6 +655,8 @@ export const make = Effect.fnUntraced(function* <RAgent, RModel>(
655
655
  usage: {
656
656
  inputTokens: part.inputTokens,
657
657
  outputTokens: part.outputTokens,
658
+ cacheRead: part.cacheRead,
659
+ cacheWrite: part.cacheWrite,
658
660
  },
659
661
  })
660
662
  case "ErrorRetry":
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 type * as AgentOutput from "./AgentOutput.ts"
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: { total: contextTokens },
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
  }
@@ -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
  /**