@itsaplan/runner 0.3.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.
- package/dist/cli.js +152 -20
- 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
|
|
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
|
}
|
|
@@ -620,7 +730,7 @@ function stdinText(preset, task) {
|
|
|
620
730
|
return "";
|
|
621
731
|
return presetPrompt(preset, task.systemPrompt, task.prompt);
|
|
622
732
|
}
|
|
623
|
-
async function execute(config, task,
|
|
733
|
+
async function execute(config, task, opts = {}) {
|
|
624
734
|
const preset = presetOf(config);
|
|
625
735
|
const [bin, args] = spawnArgs(config, preset, task);
|
|
626
736
|
const child = spawn(bin, args, {
|
|
@@ -628,19 +738,23 @@ async function execute(config, task, onData) {
|
|
|
628
738
|
env: childEnv(config, task),
|
|
629
739
|
detached: true
|
|
630
740
|
});
|
|
741
|
+
const kill = () => {
|
|
742
|
+
if (child.pid !== undefined)
|
|
743
|
+
killGroup(child.pid);
|
|
744
|
+
};
|
|
631
745
|
let timedOut = false;
|
|
632
746
|
const timer = setTimeout(() => {
|
|
633
747
|
timedOut = true;
|
|
634
|
-
|
|
635
|
-
killGroup(child.pid);
|
|
748
|
+
kill();
|
|
636
749
|
}, config.timeoutMs);
|
|
750
|
+
opts.signal?.addEventListener("abort", kill, { once: true });
|
|
637
751
|
let stdout = "";
|
|
638
752
|
let stderr = "";
|
|
639
753
|
child.stdout.setEncoding("utf8");
|
|
640
754
|
child.stderr.setEncoding("utf8");
|
|
641
755
|
child.stdout.on("data", (chunk) => {
|
|
642
756
|
stdout = tail2(stdout + chunk, OUTPUT_LIMIT);
|
|
643
|
-
onData?.(chunk);
|
|
757
|
+
opts.onData?.(chunk);
|
|
644
758
|
});
|
|
645
759
|
child.stderr.on("data", (chunk) => {
|
|
646
760
|
stderr = tail2(stderr + chunk, ERROR_LIMIT);
|
|
@@ -657,6 +771,7 @@ async function execute(config, task, onData) {
|
|
|
657
771
|
({ code, signal } = await exited);
|
|
658
772
|
} finally {
|
|
659
773
|
clearTimeout(timer);
|
|
774
|
+
opts.signal?.removeEventListener("abort", kill);
|
|
660
775
|
}
|
|
661
776
|
const output = stdout.trim();
|
|
662
777
|
if (code === 0)
|
|
@@ -672,13 +787,14 @@ async function execute(config, task, onData) {
|
|
|
672
787
|
|
|
673
788
|
// src/chat.ts
|
|
674
789
|
var FLUSH_MS = 500;
|
|
675
|
-
async function answer(config, client, message) {
|
|
790
|
+
async function answer(config, client, message, stop) {
|
|
676
791
|
let reported = message.sessionId !== null;
|
|
677
|
-
const stream = new AnswerStream(config.outputFormat, message.threadId, String(message.id), (events) => {
|
|
792
|
+
const stream = new AnswerStream(config.outputFormat, message.threadId, String(message.id), async (events) => {
|
|
678
793
|
const started = reported ? undefined : stream.startedSession() ?? undefined;
|
|
679
794
|
if (started)
|
|
680
795
|
reported = true;
|
|
681
|
-
|
|
796
|
+
if (await client.chatEvents(message.id, events, started))
|
|
797
|
+
stop.abort();
|
|
682
798
|
});
|
|
683
799
|
const flushing = setInterval(() => {
|
|
684
800
|
stream.flush().catch(() => {});
|
|
@@ -694,15 +810,17 @@ async function answer(config, client, message) {
|
|
|
694
810
|
ITSAPLAN_MESSAGE_ID: String(message.id),
|
|
695
811
|
ITSAPLAN_SESSION_ID: message.sessionId ?? ""
|
|
696
812
|
}
|
|
697
|
-
}, (chunk) => stream.write(chunk)).finally(() => clearInterval(flushing));
|
|
813
|
+
}, { onData: (chunk) => stream.write(chunk), signal: stop.signal }).finally(() => clearInterval(flushing));
|
|
814
|
+
if (stop.signal.aborted)
|
|
815
|
+
return;
|
|
698
816
|
if (outcome.status === "success") {
|
|
699
817
|
await stream.finish(outcome.output);
|
|
700
|
-
await client.chatResult(message.id, { status: "success" });
|
|
818
|
+
await client.chatResult(message.id, { status: "success", usage: stream.contextUsage() });
|
|
701
819
|
return;
|
|
702
820
|
}
|
|
703
821
|
const error = outcome.error ?? "The command failed";
|
|
704
822
|
await stream.fail(error, outcome.output);
|
|
705
|
-
await client.chatResult(message.id, { status: "failed", error });
|
|
823
|
+
await client.chatResult(message.id, { status: "failed", error, usage: stream.contextUsage() });
|
|
706
824
|
}
|
|
707
825
|
|
|
708
826
|
// src/client.ts
|
|
@@ -754,18 +872,23 @@ class Client {
|
|
|
754
872
|
return body.message;
|
|
755
873
|
}
|
|
756
874
|
async chatEvents(messageId, events, sessionId) {
|
|
757
|
-
await this.post(`/agent-chats/${messageId}/events`, {
|
|
875
|
+
const res = await this.post(`/agent-chats/${messageId}/events`, {
|
|
758
876
|
events,
|
|
759
877
|
...sessionId && { sessionId }
|
|
760
878
|
});
|
|
879
|
+
return canceled(res);
|
|
761
880
|
}
|
|
762
881
|
async chatHeartbeat(messageId) {
|
|
763
|
-
await this.post(`/agent-chats/${messageId}/heartbeat`);
|
|
882
|
+
return canceled(await this.post(`/agent-chats/${messageId}/heartbeat`));
|
|
764
883
|
}
|
|
765
884
|
async chatResult(messageId, result) {
|
|
766
885
|
await this.post(`/agent-chats/${messageId}/result`, result);
|
|
767
886
|
}
|
|
768
887
|
}
|
|
888
|
+
async function canceled(res) {
|
|
889
|
+
const body = await res.json().catch(() => ({}));
|
|
890
|
+
return body.canceled === true;
|
|
891
|
+
}
|
|
769
892
|
|
|
770
893
|
// src/cli.ts
|
|
771
894
|
var HEARTBEAT_MS = 60000;
|
|
@@ -803,8 +926,10 @@ async function handle(config, client, log2, run) {
|
|
|
803
926
|
const label = run.issueIdentifier ?? `run ${run.id}`;
|
|
804
927
|
log2(`${label}: started (${run.trigger})`);
|
|
805
928
|
try {
|
|
806
|
-
const
|
|
807
|
-
await client.
|
|
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() });
|
|
808
933
|
log2(`${label}: ${outcome.status}${outcome.error ? ` — ${outcome.error}` : ""}`);
|
|
809
934
|
} catch (err) {
|
|
810
935
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -814,14 +939,21 @@ async function handle(config, client, log2, run) {
|
|
|
814
939
|
}
|
|
815
940
|
async function handleChat(config, client, log2, message) {
|
|
816
941
|
log2(`chat ${message.id}: answering`);
|
|
942
|
+
const stop = new AbortController;
|
|
817
943
|
try {
|
|
818
|
-
await withHeartbeat(log2, () =>
|
|
819
|
-
|
|
944
|
+
await withHeartbeat(log2, async () => {
|
|
945
|
+
if (await client.chatHeartbeat(message.id))
|
|
946
|
+
stop.abort();
|
|
947
|
+
}, answer(config, client, message, stop));
|
|
820
948
|
} catch (err) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
949
|
+
if (!stop.signal.aborted) {
|
|
950
|
+
const text = err instanceof Error ? err.message : String(err);
|
|
951
|
+
log2(`chat ${message.id}: runner error — ${text}`);
|
|
952
|
+
await client.chatResult(message.id, { status: "failed", error: text }).catch(() => {});
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
824
955
|
}
|
|
956
|
+
log2(`chat ${message.id}: ${stop.signal.aborted ? "stopped" : "answered"}`);
|
|
825
957
|
}
|
|
826
958
|
async function drain(state, log2, concurrency, take, run, onEmpty) {
|
|
827
959
|
const active = new Set;
|