@cargo-ai/cli 1.0.86 → 1.0.88
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/build/commands/ask/claudeConfig.d.ts +2 -0
- package/build/commands/ask/claudeConfig.d.ts.map +1 -1
- package/build/commands/ask/claudeConfig.js +67 -1
- package/build/commands/ask/index.js +1 -1
- package/build/commands/ask/localHarness.d.ts +53 -0
- package/build/commands/ask/localHarness.d.ts.map +1 -1
- package/build/commands/ask/localHarness.js +281 -55
- package/build/commands/ask/prompt.d.ts +7 -1
- package/build/commands/ask/prompt.d.ts.map +1 -1
- package/build/commands/ask/prompt.js +9 -3
- package/build/commands/ask/session.d.ts +8 -1
- package/build/commands/ask/session.d.ts.map +1 -1
- package/build/commands/ask/session.js +426 -63
- package/build/commands/ask/thinking.d.ts +21 -0
- package/build/commands/ask/thinking.d.ts.map +1 -0
- package/build/commands/ask/thinking.js +57 -0
- package/build/commands/ask/typeAhead.d.ts +67 -0
- package/build/commands/ask/typeAhead.d.ts.map +1 -0
- package/build/commands/ask/typeAhead.js +169 -0
- package/build/commands/runHandler.d.ts +7 -0
- package/build/commands/runHandler.d.ts.map +1 -1
- package/build/commands/runHandler.js +39 -4
- package/package.json +6 -4
|
@@ -14,8 +14,16 @@ import { ensureHarnessPnpmOnPath } from "./pnpmShim.js";
|
|
|
14
14
|
* from Claude Code's own transcript. `CARGO_ASK_TIMING=1` writes the phase
|
|
15
15
|
* boundaries — and the harness's own diagnostics — to stderr. Unset, every
|
|
16
16
|
* call below is a no-op and nothing is added to the stream.
|
|
17
|
+
*
|
|
18
|
+
* `CARGO_ASK_TIMING=parts` adds the whole of each stream part next to its
|
|
19
|
+
* type. Verbose, and the only way to see what the bridge actually sends: the
|
|
20
|
+
* part names alone say a tool call arrived, not that its result came back
|
|
21
|
+
* empty or that the field a reader is looking for is spelled differently here
|
|
22
|
+
* than in the bridge that emitted it.
|
|
17
23
|
*/
|
|
18
|
-
const
|
|
24
|
+
const ASK_TIMING_LEVEL = process.env["CARGO_ASK_TIMING"];
|
|
25
|
+
const ASK_TIMING_ENABLED = ASK_TIMING_LEVEL === "1" || ASK_TIMING_LEVEL === "parts";
|
|
26
|
+
const ASK_TIMING_DUMPS_PARTS = ASK_TIMING_LEVEL === "parts";
|
|
19
27
|
function createPhaseLog(label) {
|
|
20
28
|
if (ASK_TIMING_ENABLED === false) {
|
|
21
29
|
return () => undefined;
|
|
@@ -54,12 +62,24 @@ async function* timeParts(parts, phase) {
|
|
|
54
62
|
: "unknown";
|
|
55
63
|
// Text arrives one delta per token, which would bury everything else.
|
|
56
64
|
if (type.startsWith("text-delta") === false) {
|
|
57
|
-
phase(
|
|
65
|
+
phase(ASK_TIMING_DUMPS_PARTS === true
|
|
66
|
+
? `part ${String(seen)}: ${type} ${dumpPart(part)}`
|
|
67
|
+
: `part ${String(seen)}: ${type}`);
|
|
58
68
|
}
|
|
59
69
|
yield part;
|
|
60
70
|
}
|
|
61
71
|
phase(`stream ended after ${String(seen)} parts`);
|
|
62
72
|
}
|
|
73
|
+
/** One stream part, flattened onto its log line. Truncated: some are huge. */
|
|
74
|
+
function dumpPart(part) {
|
|
75
|
+
try {
|
|
76
|
+
const json = JSON.stringify(part, (_key, value) => value instanceof Error ? `Error(${value.message})` : value);
|
|
77
|
+
return json === undefined ? "" : json.slice(0, 900);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return "<unserializable>";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
63
83
|
const LOCAL_WORKING_COPY_INSTRUCTIONS = `You are working in the user's local working copy, not a sandbox. Prefer plan over deploy.
|
|
64
84
|
|
|
65
85
|
The user confirms each Bash, Write, Edit and NotebookEdit call, and can only answer one at a time. Ask for a single one of those per step and wait for its result before asking for the next; two in one step leaves the second waiting on a confirmation it will never be offered. Reads (Read, Glob, Grep) and the Cargo \`mcp__cargo__*\` tools need no confirmation — batch those freely.
|
|
@@ -395,6 +415,11 @@ export async function createLocalHarness(input) {
|
|
|
395
415
|
handlers: options,
|
|
396
416
|
accumulator,
|
|
397
417
|
unanswered,
|
|
418
|
+
// Interrupt Claude Code through the turn's abort signal so the
|
|
419
|
+
// bridge stops cleanly. Without this the host walks away from the
|
|
420
|
+
// stream and teardown looks like "bridge closed before the turn
|
|
421
|
+
// finished" rather than an intentional stop.
|
|
422
|
+
onStall: options === undefined ? undefined : options.onStall,
|
|
398
423
|
});
|
|
399
424
|
}
|
|
400
425
|
catch (error) {
|
|
@@ -520,11 +545,14 @@ export async function runPromptTurn(input) {
|
|
|
520
545
|
const stallTimeoutMs = input.stallTimeoutMs === undefined
|
|
521
546
|
? TURN_STALL_TIMEOUT_MS
|
|
522
547
|
: input.stallTimeoutMs;
|
|
548
|
+
const toolStallTimeoutMs = input.toolStallTimeoutMs === undefined
|
|
549
|
+
? TURN_TOOL_STALL_TIMEOUT_MS
|
|
550
|
+
: input.toolStallTimeoutMs;
|
|
523
551
|
const assistantChunks = accumulator.chunks;
|
|
524
552
|
const toolCalls = new Map();
|
|
525
553
|
const turnState = {
|
|
526
554
|
toolRanSinceText: false,
|
|
527
|
-
|
|
555
|
+
announced: [],
|
|
528
556
|
};
|
|
529
557
|
let stream = await input.driver.start();
|
|
530
558
|
let round = 0;
|
|
@@ -537,6 +565,8 @@ export async function runPromptTurn(input) {
|
|
|
537
565
|
usage: accumulator.usage,
|
|
538
566
|
turnState,
|
|
539
567
|
stallTimeoutMs,
|
|
568
|
+
toolStallTimeoutMs,
|
|
569
|
+
onStall: input.onStall,
|
|
540
570
|
});
|
|
541
571
|
if (approvals.length === 0) {
|
|
542
572
|
break;
|
|
@@ -549,7 +579,6 @@ export async function runPromptTurn(input) {
|
|
|
549
579
|
throw new Error(`Local Claude Code asked for tool approval ${String(MAX_APPROVAL_ROUNDS)} times in one turn without finishing. Stopping the turn.`);
|
|
550
580
|
}
|
|
551
581
|
const continuations = [];
|
|
552
|
-
const approvedToolCallIds = [];
|
|
553
582
|
for (const approval of approvals) {
|
|
554
583
|
// Refused before the user is asked: not their call to make, and a
|
|
555
584
|
// prompt for it could not say what it was granting.
|
|
@@ -561,8 +590,13 @@ export async function runPromptTurn(input) {
|
|
|
561
590
|
: onApproval === undefined
|
|
562
591
|
? false
|
|
563
592
|
: await onApproval(approval);
|
|
564
|
-
|
|
565
|
-
|
|
593
|
+
// Recorded against the call so the retraction pass below knows it is
|
|
594
|
+
// settled: an approved one is running and must be left alone, a refused
|
|
595
|
+
// one is certainly not and needs no reprieve to find out. Absent when
|
|
596
|
+
// its result already arrived, which takes it off the list.
|
|
597
|
+
const announced = turnState.announced.find((call) => call.toolCallId === approval.toolCallId);
|
|
598
|
+
if (announced !== undefined) {
|
|
599
|
+
announced.status = approved === true ? "approved" : "refused";
|
|
566
600
|
}
|
|
567
601
|
// Answered, so the drain on a later turn must not deny it again.
|
|
568
602
|
if (input.unanswered !== undefined) {
|
|
@@ -588,15 +622,7 @@ export async function runPromptTurn(input) {
|
|
|
588
622
|
},
|
|
589
623
|
});
|
|
590
624
|
}
|
|
591
|
-
|
|
592
|
-
// surfaces one approval per stream, so a sibling the model announced in
|
|
593
|
-
// the same stream was never asked about and will never report a result —
|
|
594
|
-
// left in the set it would tell the stall watchdog a tool is in flight
|
|
595
|
-
// for the rest of the turn, turning a silent stream into a hang.
|
|
596
|
-
turnState.inFlightToolCallIds.clear();
|
|
597
|
-
for (const toolCallId of approvedToolCallIds) {
|
|
598
|
-
turnState.inFlightToolCallIds.add(toolCallId);
|
|
599
|
-
}
|
|
625
|
+
retractAbandonedCalls(turnState.announced, handlers.onActivity);
|
|
600
626
|
stream = await input.driver.continueWithApprovals(continuations);
|
|
601
627
|
}
|
|
602
628
|
const streamed = assistantChunks.join("");
|
|
@@ -612,14 +638,94 @@ export async function runPromptTurn(input) {
|
|
|
612
638
|
}
|
|
613
639
|
return fallback;
|
|
614
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* Retract the calls the caller was told about and will never hear the end of,
|
|
643
|
+
* at the boundary where the answers to this round's approvals are known.
|
|
644
|
+
*
|
|
645
|
+
* Left on screen instead, an abandoned call outlives the work: no result is
|
|
646
|
+
* coming, so no `end` event is either, and the caller keeps showing a tool
|
|
647
|
+
* that stopped existing.
|
|
648
|
+
*
|
|
649
|
+
* `"approved"` is kept, and `"running"` is the one case that cannot be judged
|
|
650
|
+
* yet — the reason this is not a plain sweep. It is either the sibling whose
|
|
651
|
+
* approval request the harness lost (it surfaces one per stream) or a tool
|
|
652
|
+
* Claude Code ran without asking, whose result has simply not arrived. Both
|
|
653
|
+
* look identical here, so it gets one more stream to prove itself: a live one
|
|
654
|
+
* reports its result and leaves this list on its own.
|
|
655
|
+
*
|
|
656
|
+
* Everything else is certain and retracted — `"streaming"` never finished
|
|
657
|
+
* arriving, `"refused"` was told no, and `"abandoning"` already had its
|
|
658
|
+
* stream and stayed silent. Refusals matter especially: a tool
|
|
659
|
+
* `isAlwaysDenied` refuses never reaches `onApproval`, so this is the caller's
|
|
660
|
+
* only notice.
|
|
661
|
+
*
|
|
662
|
+
* The reprieve is for the caller's benefit only — `"abandoning"` is not an
|
|
663
|
+
* executing status, so the stall watchdog keeps its short deadline throughout
|
|
664
|
+
* it. Extending the reprieve to the watchdog would deadlock the very hang it
|
|
665
|
+
* exists to prevent: a dropped sibling would hold the timer off, the stream
|
|
666
|
+
* would stay silent, and the next boundary, the thing that resolves the
|
|
667
|
+
* ambiguity, would never arrive.
|
|
668
|
+
*/
|
|
669
|
+
function retractAbandonedCalls(announced, onActivity) {
|
|
670
|
+
for (const call of [...announced]) {
|
|
671
|
+
if (call.status === "approved") {
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (call.status === "running") {
|
|
675
|
+
call.status = "abandoning";
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
announced.splice(announced.indexOf(call), 1);
|
|
679
|
+
if (onActivity !== undefined) {
|
|
680
|
+
onActivity({
|
|
681
|
+
kind: "dropped",
|
|
682
|
+
toolCallId: call.toolCallId,
|
|
683
|
+
toolName: call.toolName,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
615
688
|
/**
|
|
616
689
|
* How long a turn may produce nothing at all while no tool is running.
|
|
617
690
|
*
|
|
618
691
|
* Generous on purpose: this is a deadlock detector, not a latency budget. The
|
|
619
692
|
* clock only runs when nothing is in flight, so the wait is over a turn that
|
|
620
693
|
* is neither working nor talking.
|
|
694
|
+
*
|
|
695
|
+
* `CARGO_ASK_STALL_TIMEOUT_MS` shortens it for the end-to-end tests, which
|
|
696
|
+
* would otherwise have to sit out the real deadline to prove a hang is caught.
|
|
621
697
|
*/
|
|
622
|
-
const TURN_STALL_TIMEOUT_MS = 120_000;
|
|
698
|
+
const TURN_STALL_TIMEOUT_MS = readTimeoutEnv("CARGO_ASK_STALL_TIMEOUT_MS", 120_000);
|
|
699
|
+
/**
|
|
700
|
+
* The same deadline, for a turn that has a tool executing.
|
|
701
|
+
*
|
|
702
|
+
* A tool at work is silent by design — a test run says nothing for as long as
|
|
703
|
+
* it takes — so this cannot be the deadline above. But it cannot be *no*
|
|
704
|
+
* deadline either, which is what it used to be: a tool the user approved and
|
|
705
|
+
* that never reported back left the turn waiting forever, spinner and all,
|
|
706
|
+
* with nothing to do but kill the terminal. That is the shape of every "it got
|
|
707
|
+
* stuck on the tool" report.
|
|
708
|
+
*
|
|
709
|
+
* Twelve minutes, from Claude Code's own ten-minute ceiling on a Bash tool.
|
|
710
|
+
* That is the bound for the common case, not for every case: the ceiling is
|
|
711
|
+
* configurable, and it does not apply to an MCP tool at all. A `mcp__cargo__*`
|
|
712
|
+
* call that genuinely runs longer than this is cut off with its work lost, so
|
|
713
|
+
* `CARGO_ASK_TOOL_STALL_TIMEOUT_MS` raises it for a workspace that does that
|
|
714
|
+
* routinely. The trade is deliberate: a deadline that covers every conceivable
|
|
715
|
+
* tool is indistinguishable from the hang it replaced.
|
|
716
|
+
*
|
|
717
|
+
* Recoverable either way — the session is replaced and the chat, its history
|
|
718
|
+
* and the working copy all survive.
|
|
719
|
+
*/
|
|
720
|
+
const TURN_TOOL_STALL_TIMEOUT_MS = readTimeoutEnv("CARGO_ASK_TOOL_STALL_TIMEOUT_MS", 12 * 60_000);
|
|
721
|
+
function readTimeoutEnv(name, fallbackMs) {
|
|
722
|
+
const raw = process.env[name];
|
|
723
|
+
if (raw === undefined) {
|
|
724
|
+
return fallbackMs;
|
|
725
|
+
}
|
|
726
|
+
const parsed = Number(raw);
|
|
727
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackMs;
|
|
728
|
+
}
|
|
623
729
|
/**
|
|
624
730
|
* How long teardown may take before the session is abandoned. Short, because
|
|
625
731
|
* the caller is usually someone who just pressed Ctrl-C.
|
|
@@ -644,6 +750,29 @@ export class HarnessTurnStuck extends Error {
|
|
|
644
750
|
this.name = "HarnessTurnStuck";
|
|
645
751
|
}
|
|
646
752
|
}
|
|
753
|
+
/**
|
|
754
|
+
* The turn produced nothing for `TURN_STALL_TIMEOUT_MS` while no tool was in
|
|
755
|
+
* flight. Common causes: a step that asked to run several confirmable tools at
|
|
756
|
+
* once (the harness only surfaces one approval), or the Claude Code bridge
|
|
757
|
+
* going silent. Recoverable — replace the session and ask again.
|
|
758
|
+
*/
|
|
759
|
+
export class HarnessTurnStalled extends Error {
|
|
760
|
+
constructor(timeoutMs, whileToolRunning = false) {
|
|
761
|
+
const seconds = String(Math.round(timeoutMs / 1000));
|
|
762
|
+
super(whileToolRunning === true
|
|
763
|
+
? `A tool has been running for ${seconds}s without reporting anything back, which is longer than Claude Code lets any tool run. Treating it as never coming back rather than waiting on it.`
|
|
764
|
+
: `Local Claude Code went quiet for ${seconds}s with no tool running. Often this is a step that asked to run several confirmable tools at once (only the first can be answered), or the coding session itself stalled.`);
|
|
765
|
+
this.name = "HarnessTurnStalled";
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* The Claude Code bridge dropped mid-turn. Same recovery as a stall: the
|
|
770
|
+
* process behind the session is gone, so the next question needs a new one.
|
|
771
|
+
*/
|
|
772
|
+
export function isBridgeClosedError(error) {
|
|
773
|
+
return (error instanceof Error &&
|
|
774
|
+
error.message.includes("bridge closed before the turn finished") === true);
|
|
775
|
+
}
|
|
647
776
|
/**
|
|
648
777
|
* Fail a turn that has gone silent with no tool running, instead of hanging.
|
|
649
778
|
*
|
|
@@ -654,11 +783,15 @@ export class HarnessTurnStuck extends Error {
|
|
|
654
783
|
* `LOCAL_WORKING_COPY_INSTRUCTIONS` asks for one confirmable tool per step to
|
|
655
784
|
* avoid it; this is the backstop for when the model does it anyway.
|
|
656
785
|
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
786
|
+
* Two deadlines rather than one, because a tool at work is silent by design
|
|
787
|
+
* and a ten-minute test run must not be cut off: `timeoutMs` for a stream with
|
|
788
|
+
* nothing executing, and the far longer `toolTimeoutMs` while a tool is. What
|
|
789
|
+
* there is never again is *no* deadline — a tool that never reports back used
|
|
790
|
+
* to leave the turn on the spinner forever.
|
|
659
791
|
*/
|
|
660
|
-
async function* withStallTimeout(stream, timeoutMs, isToolInFlight) {
|
|
661
|
-
if (Number.isFinite(timeoutMs) === false
|
|
792
|
+
async function* withStallTimeout(stream, timeoutMs, toolTimeoutMs, isToolInFlight, onStall) {
|
|
793
|
+
if (Number.isFinite(timeoutMs) === false &&
|
|
794
|
+
Number.isFinite(toolTimeoutMs) === false) {
|
|
662
795
|
for await (const part of stream) {
|
|
663
796
|
yield part;
|
|
664
797
|
}
|
|
@@ -667,9 +800,11 @@ async function* withStallTimeout(stream, timeoutMs, isToolInFlight) {
|
|
|
667
800
|
const iterator = stream[Symbol.asyncIterator]();
|
|
668
801
|
for (;;) {
|
|
669
802
|
const next = iterator.next();
|
|
670
|
-
const
|
|
803
|
+
const running = isToolInFlight();
|
|
804
|
+
const budgetMs = running === true ? toolTimeoutMs : timeoutMs;
|
|
805
|
+
const result = await (Number.isFinite(budgetMs) === false
|
|
671
806
|
? next
|
|
672
|
-
: rejectIfIdle(next,
|
|
807
|
+
: rejectIfIdle(next, budgetMs, onStall, running));
|
|
673
808
|
if (result.done === true) {
|
|
674
809
|
return;
|
|
675
810
|
}
|
|
@@ -699,14 +834,22 @@ export async function settleWithin(work, timeoutMs) {
|
|
|
699
834
|
}
|
|
700
835
|
}
|
|
701
836
|
}
|
|
702
|
-
async function rejectIfIdle(next, timeoutMs) {
|
|
837
|
+
async function rejectIfIdle(next, timeoutMs, onStall, whileToolRunning) {
|
|
703
838
|
let timer;
|
|
704
839
|
try {
|
|
705
840
|
return await Promise.race([
|
|
706
841
|
next,
|
|
707
842
|
new Promise((_resolve, reject) => {
|
|
708
843
|
timer = setTimeout(() => {
|
|
709
|
-
|
|
844
|
+
// Reject first so Promise.race settles on HarnessTurnStalled, not
|
|
845
|
+
// on an AbortError the interrupt below may race onto `next`.
|
|
846
|
+
reject(new HarnessTurnStalled(timeoutMs, whileToolRunning));
|
|
847
|
+
// Then stop the bridge so teardown is an intentional abort, not a
|
|
848
|
+
// mysterious "bridge closed before the turn finished" after we
|
|
849
|
+
// have already walked away from the stream.
|
|
850
|
+
if (onStall !== undefined) {
|
|
851
|
+
onStall();
|
|
852
|
+
}
|
|
710
853
|
}, timeoutMs);
|
|
711
854
|
}),
|
|
712
855
|
]);
|
|
@@ -759,19 +902,23 @@ export function isAutoApproved(toolName) {
|
|
|
759
902
|
return AUTO_APPROVED_TOOL_NAMES.has(toolName);
|
|
760
903
|
}
|
|
761
904
|
/**
|
|
762
|
-
* `Agent` spawns a subagent to run a task of its own
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
* individual approval is not something we control or currently know.
|
|
905
|
+
* `Agent` spawns a subagent to run a task of its own, and approving one is a
|
|
906
|
+
* different kind of decision from approving a `Bash`: the subagent runs its
|
|
907
|
+
* own tools, can be asked for with `mode: "bypassPermissions"`, and whether
|
|
908
|
+
* its calls come back here for individual approval is not something we control
|
|
909
|
+
* or currently know.
|
|
768
910
|
*
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
911
|
+
* The refusal that actually stops it is in `claudeConfig.ts`, as a deny rule
|
|
912
|
+
* in Claude Code's own settings — this never fired, because the approval
|
|
913
|
+
* request it waits for is never sent. The bridge writes `ask` rules only for
|
|
914
|
+
* the tools in its `NATIVE_TOOL_KINDS` map, the subagent tools are not in it,
|
|
915
|
+
* and so `allow-reads` left them running unattended.
|
|
916
|
+
*
|
|
917
|
+
* Kept as the second line: a user who points `CLAUDE_CONFIG_DIR` at their own
|
|
918
|
+
* Claude Code config gets no deny rule written for them, and a future bridge
|
|
919
|
+
* that does surface the approval should find an answer waiting.
|
|
773
920
|
*/
|
|
774
|
-
const DENIED_TOOL_NAMES = new Set(["Agent"]);
|
|
921
|
+
const DENIED_TOOL_NAMES = new Set(["Agent", "Task"]);
|
|
775
922
|
const DENIED_TOOL_REASON = "Spawning a subagent is not available in this session. Do the work in this turn instead, asking for each tool you need.";
|
|
776
923
|
/** Exported for tests. */
|
|
777
924
|
export function isAlwaysDenied(toolName) {
|
|
@@ -794,7 +941,7 @@ async function consumeTurn(input) {
|
|
|
794
941
|
const approvals = [];
|
|
795
942
|
const { handlers } = input;
|
|
796
943
|
const pendingFiles = new Map();
|
|
797
|
-
const parts = withStallTimeout(input.stream.fullStream, input.stallTimeoutMs, () => input.turnState.
|
|
944
|
+
const parts = withStallTimeout(input.stream.fullStream, input.stallTimeoutMs, input.toolStallTimeoutMs, () => input.turnState.announced.some((call) => call.status === "running" || call.status === "approved"), input.onStall);
|
|
798
945
|
for await (const rawPart of parts) {
|
|
799
946
|
if (typeof rawPart !== "object" || rawPart === null) {
|
|
800
947
|
continue;
|
|
@@ -808,6 +955,43 @@ async function consumeTurn(input) {
|
|
|
808
955
|
? error
|
|
809
956
|
: "Local Claude Code turn failed.");
|
|
810
957
|
}
|
|
958
|
+
// Claude Code streams the tool name before the full call is assembled.
|
|
959
|
+
// Surface that early so the spinner is not stuck on "Asking…" while the
|
|
960
|
+
// input is still arriving — and so a later `tool-call` can fill in the
|
|
961
|
+
// summary without looking like a second tool.
|
|
962
|
+
if (part["type"] === "tool-input-start") {
|
|
963
|
+
const toolCallId = part["id"];
|
|
964
|
+
const toolName = part["toolName"];
|
|
965
|
+
if (typeof toolCallId === "string" && typeof toolName === "string") {
|
|
966
|
+
if (input.toolCalls.has(toolCallId) === false) {
|
|
967
|
+
input.toolCalls.set(toolCallId, {
|
|
968
|
+
toolName,
|
|
969
|
+
input: undefined,
|
|
970
|
+
});
|
|
971
|
+
input.turnState.toolRanSinceText = true;
|
|
972
|
+
// Announced, because the spinner is about to show this name and is
|
|
973
|
+
// owed a retraction if the call never happens — but `"streaming"`,
|
|
974
|
+
// not executing. The tool is not running yet, its input is still
|
|
975
|
+
// being generated, and the deltas that carry it are output the
|
|
976
|
+
// watchdog is entitled to expect; standing it down here would turn a
|
|
977
|
+
// bridge that dies mid-input into a hang.
|
|
978
|
+
input.turnState.announced.push({
|
|
979
|
+
toolCallId,
|
|
980
|
+
toolName,
|
|
981
|
+
status: "streaming",
|
|
982
|
+
});
|
|
983
|
+
if (handlers.onActivity !== undefined) {
|
|
984
|
+
handlers.onActivity({
|
|
985
|
+
kind: "start",
|
|
986
|
+
toolCallId,
|
|
987
|
+
toolName,
|
|
988
|
+
summary: "",
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
811
995
|
if (part["type"] === "tool-call") {
|
|
812
996
|
const toolCallId = part["toolCallId"];
|
|
813
997
|
const toolName = part["toolName"];
|
|
@@ -817,10 +1001,26 @@ async function consumeTurn(input) {
|
|
|
817
1001
|
input: part["input"],
|
|
818
1002
|
});
|
|
819
1003
|
input.turnState.toolRanSinceText = true;
|
|
820
|
-
|
|
1004
|
+
// The full call is assembled, so something is executing it — that is
|
|
1005
|
+
// what stands the stall watchdog down, and it deliberately waits until
|
|
1006
|
+
// here rather than firing at `tool-input-start`.
|
|
1007
|
+
const announced = input.turnState.announced.find((call) => call.toolCallId === toolCallId);
|
|
1008
|
+
if (announced === undefined) {
|
|
1009
|
+
input.turnState.announced.push({
|
|
1010
|
+
toolCallId,
|
|
1011
|
+
toolName,
|
|
1012
|
+
status: "running",
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
announced.status = "running";
|
|
1017
|
+
}
|
|
1018
|
+
// Re-emitted when `tool-input-start` already announced the name, so
|
|
1019
|
+
// the spinner can pick up the input summary once it is known.
|
|
821
1020
|
if (handlers.onActivity !== undefined) {
|
|
822
1021
|
handlers.onActivity({
|
|
823
1022
|
kind: "start",
|
|
1023
|
+
toolCallId,
|
|
824
1024
|
toolName,
|
|
825
1025
|
summary: summarizeToolInput(part["input"]),
|
|
826
1026
|
});
|
|
@@ -842,18 +1042,28 @@ async function consumeTurn(input) {
|
|
|
842
1042
|
? input.toolCalls.get(toolCallId)
|
|
843
1043
|
: undefined;
|
|
844
1044
|
const isError = part["type"] === "tool-error";
|
|
845
|
-
|
|
1045
|
+
// A preliminary result is progress from a tool that is still running,
|
|
1046
|
+
// so it must not clear the spinner or tell the stall watchdog the tool
|
|
1047
|
+
// is done.
|
|
1048
|
+
const preliminary = part["preliminary"] === true;
|
|
1049
|
+
if (preliminary === false &&
|
|
1050
|
+
typeof toolCallId === "string" &&
|
|
1051
|
+
handlers.onActivity !== undefined) {
|
|
846
1052
|
handlers.onActivity({
|
|
847
1053
|
kind: "end",
|
|
1054
|
+
toolCallId,
|
|
848
1055
|
toolName: recorded === undefined ? "tool" : recorded.toolName,
|
|
849
1056
|
isError,
|
|
850
1057
|
});
|
|
851
1058
|
}
|
|
852
|
-
if (typeof toolCallId === "string") {
|
|
853
|
-
//
|
|
854
|
-
// so
|
|
855
|
-
|
|
856
|
-
|
|
1059
|
+
if (typeof toolCallId === "string" && preliminary === false) {
|
|
1060
|
+
// Off the list on the way out. The `end` above is the caller's
|
|
1061
|
+
// retraction, so a later approval round must not announce a second
|
|
1062
|
+
// one — and leaving under its own steam is how a tool that ran unasked
|
|
1063
|
+
// proves it was alive rather than abandoned.
|
|
1064
|
+
const at = input.turnState.announced.findIndex((call) => call.toolCallId === toolCallId);
|
|
1065
|
+
if (at !== -1) {
|
|
1066
|
+
input.turnState.announced.splice(at, 1);
|
|
857
1067
|
}
|
|
858
1068
|
const touched = pendingFiles.get(toolCallId);
|
|
859
1069
|
pendingFiles.delete(toolCallId);
|
|
@@ -943,19 +1153,35 @@ function readCount(value) {
|
|
|
943
1153
|
? value
|
|
944
1154
|
: 0;
|
|
945
1155
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1156
|
+
/**
|
|
1157
|
+
* What Claude Code says the turn cost, off the `finish` part.
|
|
1158
|
+
*
|
|
1159
|
+
* Two keys, because the value is renamed on its way here: the bridge emits
|
|
1160
|
+
* `harnessMetadata`, and `HarnessAgent` republishes the same object as
|
|
1161
|
+
* `providerMetadata` on the parts this code actually reads. Looking only for
|
|
1162
|
+
* the bridge's name found nothing, so every local turn was ingested at a cost
|
|
1163
|
+
* of zero and priced into no credits at all.
|
|
1164
|
+
*
|
|
1165
|
+
* Exported for tests.
|
|
1166
|
+
*/
|
|
1167
|
+
export function readClaudeCodeCostUsd(part) {
|
|
1168
|
+
for (const key of ["providerMetadata", "harnessMetadata"]) {
|
|
1169
|
+
const metadata = part[key];
|
|
1170
|
+
if (typeof metadata !== "object" || metadata === null) {
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
const claudeCode = metadata["claude-code"];
|
|
1174
|
+
if (typeof claudeCode !== "object" || claudeCode === null) {
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
const costUsd = claudeCode["costUsd"];
|
|
1178
|
+
if (typeof costUsd === "number" &&
|
|
1179
|
+
Number.isFinite(costUsd) &&
|
|
1180
|
+
costUsd > 0) {
|
|
1181
|
+
return costUsd;
|
|
1182
|
+
}
|
|
954
1183
|
}
|
|
955
|
-
|
|
956
|
-
return typeof costUsd === "number" && Number.isFinite(costUsd) && costUsd > 0
|
|
957
|
-
? costUsd
|
|
958
|
-
: undefined;
|
|
1184
|
+
return undefined;
|
|
959
1185
|
}
|
|
960
1186
|
/**
|
|
961
1187
|
* The AI SDK part nests the whole `toolCall`; the harness bridge part carries
|
|
@@ -16,5 +16,11 @@ export declare class AskInterrupted extends Error {
|
|
|
16
16
|
* Enter submits (accepting the highlighted suggestion when the menu is open).
|
|
17
17
|
* Empty Enter leaves. Tab inserts the highlight without submitting.
|
|
18
18
|
*/
|
|
19
|
-
export declare function readAskLine(input: NodeJS.ReadableStream, cwd: string
|
|
19
|
+
export declare function readAskLine(input: NodeJS.ReadableStream, cwd: string,
|
|
20
|
+
/**
|
|
21
|
+
* What the user had already typed while the last turn was running. The line
|
|
22
|
+
* opens with it in place and the cursor after it, so a question started
|
|
23
|
+
* mid-reply is continued rather than retyped.
|
|
24
|
+
*/
|
|
25
|
+
initialLine?: string): Promise<string | undefined>;
|
|
20
26
|
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/prompt.ts"],"names":[],"mappings":";AAiBA;;;;;;GAMG;AACH,qBAAa,cAAe,SAAQ,KAAK;;CAKxC;AASD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,CAAC,cAAc,EAC5B,GAAG,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/prompt.ts"],"names":[],"mappings":";AAiBA;;;;;;GAMG;AACH,qBAAa,cAAe,SAAQ,KAAK;;CAKxC;AASD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,MAAM,CAAC,cAAc,EAC5B,GAAG,EAAE,MAAM;AACX;;;;GAIG;AACH,WAAW,SAAK,GACf,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAiQ7B"}
|
|
@@ -25,10 +25,16 @@ function isTTYStream(stream) {
|
|
|
25
25
|
* Enter submits (accepting the highlighted suggestion when the menu is open).
|
|
26
26
|
* Empty Enter leaves. Tab inserts the highlight without submitting.
|
|
27
27
|
*/
|
|
28
|
-
export async function readAskLine(input, cwd
|
|
28
|
+
export async function readAskLine(input, cwd,
|
|
29
|
+
/**
|
|
30
|
+
* What the user had already typed while the last turn was running. The line
|
|
31
|
+
* opens with it in place and the cursor after it, so a question started
|
|
32
|
+
* mid-reply is continued rather than retyped.
|
|
33
|
+
*/
|
|
34
|
+
initialLine = "") {
|
|
29
35
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
30
|
-
let line =
|
|
31
|
-
let cursor =
|
|
36
|
+
let line = initialLine;
|
|
37
|
+
let cursor = initialLine.length;
|
|
32
38
|
let selected = 0;
|
|
33
39
|
let painted = false;
|
|
34
40
|
let finished = false;
|
|
@@ -68,6 +68,13 @@ type ChatTurnClaim = {
|
|
|
68
68
|
*/
|
|
69
69
|
export declare function claimChatTurn(api: Api, chatUuid: string, ownerId: string): Promise<ChatTurnClaim>;
|
|
70
70
|
export declare function doesChatExist(api: Api, chatUuid: string): Promise<boolean>;
|
|
71
|
+
/**
|
|
72
|
+
* Greedy wrap on spaces. A word longer than the width keeps its own line
|
|
73
|
+
* rather than being cut mid-token.
|
|
74
|
+
*
|
|
75
|
+
* Exported for tests.
|
|
76
|
+
*/
|
|
77
|
+
export declare function wrapLine(text: string, width: number): string[];
|
|
71
78
|
/**
|
|
72
79
|
* A path inside the session directory reads better relative to it; anything
|
|
73
80
|
* outside stays absolute so it is obvious the turn left the project.
|
|
@@ -78,7 +85,7 @@ export declare function displayPath(path: string, dir: string): string;
|
|
|
78
85
|
/**
|
|
79
86
|
* Same tree grammar as the palette:
|
|
80
87
|
* ◇ <your message>
|
|
81
|
-
*
|
|
88
|
+
* ■ reply…
|
|
82
89
|
* ◆
|
|
83
90
|
* Empty Enter or /exit leaves to the parent palette.
|
|
84
91
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/session.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAC;AAIxC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/commands/ask/session.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,eAAe,CAAC;AAIxC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAmCxC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,KAAK,WAAW,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAkC1D;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAKT;AAiBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,SAAS,GAAG,WAAW,CAErE;AA8ED;;;;;;;;;GASG;AACH;;;;GAIG;AACH,KAAK,aAAa,GACd;IAAE,OAAO,EAAE,SAAS,CAAA;CAAE,GACtB;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GACnB;IAAE,OAAO,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/C;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,aAAa,CAAC,CAkBxB;AAyCD,wBAAsB,aAAa,CACjC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAqBlB;AAeD;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAe9D;AAicD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ7D;AA2ID;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,GAAG,MAAM,CAAC,CAoqB7B;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE;IACvC,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACb,GAAG,OAAO,CAAC,IAAI,CAAC,CA8HhB"}
|