@itsaplan/runner 0.4.0 → 0.5.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 (2) hide show
  1. package/dist/cli.js +117 -5
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -25,11 +25,13 @@ class AnswerStream {
25
25
  sawPartialText = false;
26
26
  sawAnyText = false;
27
27
  sending = Promise.resolve();
28
+ usage;
28
29
  constructor(format, threadId, runId, send) {
29
30
  this.format = format;
30
31
  this.threadId = threadId;
31
32
  this.runId = runId;
32
33
  this.send = send;
34
+ this.usage = new UsageReader(format);
33
35
  this.messageId = `msg-${runId}`;
34
36
  this.queued.push({ type: "RUN_STARTED", threadId, runId });
35
37
  }
@@ -48,6 +50,9 @@ class AnswerStream {
48
50
  startedSession() {
49
51
  return this.sessionId;
50
52
  }
53
+ contextUsage() {
54
+ return this.usage.value();
55
+ }
51
56
  async flush() {
52
57
  const sending = this.sending.catch(() => {}).then(() => this.sendQueued());
53
58
  this.sending = sending.catch(() => {});
@@ -127,6 +132,7 @@ class AnswerStream {
127
132
  }
128
133
  if (!parsed || typeof parsed !== "object")
129
134
  return;
135
+ this.usage.readLine(parsed);
130
136
  switch (this.format) {
131
137
  case "claude-stream-json":
132
138
  this.readClaudeLine(parsed);
@@ -275,7 +281,9 @@ class AnswerStream {
275
281
  }
276
282
  }
277
283
  readPartial(event) {
278
- if (event?.type !== "content_block_delta")
284
+ if (!event)
285
+ return;
286
+ if (event.type !== "content_block_delta")
279
287
  return;
280
288
  if (event.delta?.type === "text_delta" && event.delta.text) {
281
289
  this.sawPartialText = true;
@@ -313,6 +321,108 @@ class AnswerStream {
313
321
  });
314
322
  }
315
323
  }
324
+
325
+ class UsageReader {
326
+ format;
327
+ last = null;
328
+ buffered = "";
329
+ constructor(format) {
330
+ this.format = format;
331
+ }
332
+ write(chunk) {
333
+ if (this.format === "text")
334
+ return;
335
+ this.buffered += chunk;
336
+ const lines = this.buffered.split(`
337
+ `);
338
+ this.buffered = lines.pop() ?? "";
339
+ for (const line of lines)
340
+ this.readText(line);
341
+ }
342
+ end() {
343
+ if (!this.buffered)
344
+ return;
345
+ this.readText(this.buffered);
346
+ this.buffered = "";
347
+ }
348
+ value() {
349
+ if (this.format === "copilot-json")
350
+ return null;
351
+ return this.last ?? undefined;
352
+ }
353
+ readText(line) {
354
+ const trimmed = line.trim();
355
+ if (!trimmed)
356
+ return;
357
+ try {
358
+ const parsed = JSON.parse(trimmed);
359
+ if (parsed && typeof parsed === "object")
360
+ this.readLine(parsed);
361
+ } catch {}
362
+ }
363
+ readLine(parsed) {
364
+ switch (this.format) {
365
+ case "claude-stream-json":
366
+ this.readClaude(parsed);
367
+ return;
368
+ case "codex-jsonl":
369
+ this.readCodex(parsed);
370
+ return;
371
+ case "opencode-json":
372
+ this.readOpencode(parsed);
373
+ return;
374
+ case "antigravity-stream-json":
375
+ this.readAntigravity(parsed);
376
+ return;
377
+ }
378
+ }
379
+ readClaude(message) {
380
+ const event = message.type === "stream_event" ? message.event : undefined;
381
+ if (!event)
382
+ return;
383
+ const usage = event.type === "message_start" ? event.message?.usage : event.usage;
384
+ if (!usage)
385
+ return;
386
+ const read = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
387
+ const written = usage.output_tokens ?? 0;
388
+ if (event.type === "message_start") {
389
+ this.last = { inputTokens: read, outputTokens: written };
390
+ return;
391
+ }
392
+ if (event.type !== "message_delta")
393
+ return;
394
+ this.last = { inputTokens: read || (this.last?.inputTokens ?? 0), outputTokens: written };
395
+ }
396
+ readCodex(message) {
397
+ if (message.type !== "turn.completed" || !message.usage)
398
+ return;
399
+ this.last = {
400
+ inputTokens: message.usage.input_tokens ?? 0,
401
+ outputTokens: message.usage.output_tokens ?? 0
402
+ };
403
+ }
404
+ readOpencode(message) {
405
+ const tokens = message.part?.type === "step-finish" ? message.part.tokens : undefined;
406
+ if (!tokens)
407
+ return;
408
+ this.last = {
409
+ inputTokens: (tokens.input ?? 0) + (tokens.cache?.read ?? 0) + (tokens.cache?.write ?? 0),
410
+ outputTokens: (tokens.output ?? 0) + (tokens.reasoning ?? 0)
411
+ };
412
+ }
413
+ readAntigravity(message) {
414
+ const step = message.step_update;
415
+ if (!step)
416
+ return;
417
+ const usage = step.usage ?? message.usage;
418
+ if (!usage)
419
+ return;
420
+ this.last = {
421
+ inputTokens: (usage.input_tokens ?? 0) + (usage.cache_read_tokens ?? 0),
422
+ outputTokens: (usage.output_tokens ?? 0) + (usage.thinking_tokens ?? 0)
423
+ };
424
+ }
425
+ }
316
426
  function tail(text, limit) {
317
427
  return text.length <= limit ? text : `…${text.slice(-limit)}`;
318
428
  }
@@ -705,12 +815,12 @@ async function answer(config, client, message, stop) {
705
815
  return;
706
816
  if (outcome.status === "success") {
707
817
  await stream.finish(outcome.output);
708
- await client.chatResult(message.id, { status: "success" });
818
+ await client.chatResult(message.id, { status: "success", usage: stream.contextUsage() });
709
819
  return;
710
820
  }
711
821
  const error = outcome.error ?? "The command failed";
712
822
  await stream.fail(error, outcome.output);
713
- await client.chatResult(message.id, { status: "failed", error });
823
+ await client.chatResult(message.id, { status: "failed", error, usage: stream.contextUsage() });
714
824
  }
715
825
 
716
826
  // src/client.ts
@@ -816,8 +926,10 @@ async function handle(config, client, log2, run) {
816
926
  const label = run.issueIdentifier ?? `run ${run.id}`;
817
927
  log2(`${label}: started (${run.trigger})`);
818
928
  try {
819
- const outcome = await withHeartbeat(log2, () => client.heartbeat(run.id), execute(config, taskOf(run)));
820
- await client.report(run.id, outcome);
929
+ const usage = new UsageReader(config.outputFormat);
930
+ const outcome = await withHeartbeat(log2, () => client.heartbeat(run.id), execute(config, taskOf(run), { onData: (chunk) => usage.write(chunk) }));
931
+ usage.end();
932
+ await client.report(run.id, { ...outcome, usage: usage.value() });
821
933
  log2(`${label}: ${outcome.status}${outcome.error ? ` — ${outcome.error}` : ""}`);
822
934
  } catch (err) {
823
935
  const message = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itsaplan/runner",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Runs an Itsaplan external agent's queued tasks and answers its chat on your own machine",
5
5
  "keywords": [
6
6
  "itsaplan",