@cargo-ai/cli 1.0.92 → 1.0.95

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 (130) hide show
  1. package/README.md +17 -17
  2. package/build/commands/ai/chat.d.ts.map +1 -1
  3. package/build/commands/ai/chat.js +2 -1
  4. package/build/commands/ai/message.d.ts.map +1 -1
  5. package/build/commands/ai/message.js +2 -1
  6. package/build/commands/ai/release.d.ts.map +1 -1
  7. package/build/commands/ai/release.js +7 -6
  8. package/build/commands/ask/harness/approvals.d.ts +7 -0
  9. package/build/commands/ask/harness/approvals.d.ts.map +1 -0
  10. package/build/commands/ask/harness/approvals.js +82 -0
  11. package/build/commands/ask/harness/artifacts.d.ts.map +1 -0
  12. package/build/commands/ask/harness/claudeConfig.d.ts.map +1 -0
  13. package/build/commands/ask/harness/create.d.ts +59 -0
  14. package/build/commands/ask/harness/create.d.ts.map +1 -0
  15. package/build/commands/ask/harness/create.js +292 -0
  16. package/build/commands/ask/harness/index.d.ts +8 -0
  17. package/build/commands/ask/harness/index.d.ts.map +1 -0
  18. package/build/commands/ask/harness/index.js +7 -0
  19. package/build/commands/ask/harness/ingest.d.ts +25 -0
  20. package/build/commands/ask/harness/ingest.d.ts.map +1 -0
  21. package/build/commands/ask/harness/ingest.js +35 -0
  22. package/build/commands/ask/harness/instructions.d.ts +13 -0
  23. package/build/commands/ask/harness/instructions.d.ts.map +1 -0
  24. package/build/commands/ask/harness/instructions.js +41 -0
  25. package/build/commands/ask/harness/layout.d.ts +23 -0
  26. package/build/commands/ask/harness/layout.d.ts.map +1 -0
  27. package/build/commands/ask/harness/layout.js +32 -0
  28. package/build/commands/ask/harness/mcp.d.ts +25 -0
  29. package/build/commands/ask/harness/mcp.d.ts.map +1 -0
  30. package/build/commands/ask/harness/mcp.js +72 -0
  31. package/build/commands/ask/harness/pnpmShim.d.ts.map +1 -0
  32. package/build/commands/ask/harness/release.d.ts +18 -0
  33. package/build/commands/ask/harness/release.d.ts.map +1 -0
  34. package/build/commands/ask/harness/release.js +117 -0
  35. package/build/commands/ask/harness/sandbox.d.ts +9 -0
  36. package/build/commands/ask/harness/sandbox.d.ts.map +1 -0
  37. package/build/commands/ask/harness/sandbox.js +27 -0
  38. package/build/commands/ask/harness/timing.d.ts +16 -0
  39. package/build/commands/ask/harness/timing.d.ts.map +1 -0
  40. package/build/commands/ask/harness/timing.js +72 -0
  41. package/build/commands/ask/harness/turn.d.ts +145 -0
  42. package/build/commands/ask/harness/turn.d.ts.map +1 -0
  43. package/build/commands/ask/harness/turn.js +626 -0
  44. package/build/commands/ask/harness/usage.d.ts +50 -0
  45. package/build/commands/ask/harness/usage.d.ts.map +1 -0
  46. package/build/commands/ask/harness/usage.js +87 -0
  47. package/build/commands/ask/index.d.ts +9 -0
  48. package/build/commands/ask/index.d.ts.map +1 -1
  49. package/build/commands/ask/index.js +7 -7
  50. package/build/commands/ask/input/completer.d.ts.map +1 -0
  51. package/build/commands/ask/input/index.d.ts +5 -0
  52. package/build/commands/ask/input/index.d.ts.map +1 -0
  53. package/build/commands/ask/input/index.js +4 -0
  54. package/build/commands/ask/input/prompt.d.ts.map +1 -0
  55. package/build/commands/ask/{prompt.js → input/prompt.js} +1 -1
  56. package/build/commands/ask/input/slash.d.ts.map +1 -0
  57. package/build/commands/ask/{slash.js → input/slash.js} +2 -2
  58. package/build/commands/ask/input/typeAhead.d.ts.map +1 -0
  59. package/build/commands/ask/{history.d.ts → persist/history.d.ts} +1 -1
  60. package/build/commands/ask/persist/history.d.ts.map +1 -0
  61. package/build/commands/ask/persist/index.d.ts +4 -0
  62. package/build/commands/ask/persist/index.d.ts.map +1 -0
  63. package/build/commands/ask/persist/index.js +3 -0
  64. package/build/commands/ask/persist/persist.d.ts.map +1 -0
  65. package/build/commands/ask/persist/state.d.ts.map +1 -0
  66. package/build/commands/ask/session/claim.d.ts +56 -0
  67. package/build/commands/ask/session/claim.d.ts.map +1 -0
  68. package/build/commands/ask/session/claim.js +76 -0
  69. package/build/commands/ask/session/index.d.ts +5 -0
  70. package/build/commands/ask/session/index.d.ts.map +1 -0
  71. package/build/commands/ask/session/index.js +4 -0
  72. package/build/commands/ask/session/run.d.ts +36 -0
  73. package/build/commands/ask/session/run.d.ts.map +1 -0
  74. package/build/commands/ask/session/run.js +832 -0
  75. package/build/commands/ask/session/thinking.d.ts.map +1 -0
  76. package/build/commands/ask/session/tree.d.ts +90 -0
  77. package/build/commands/ask/session/tree.d.ts.map +1 -0
  78. package/build/commands/ask/session/tree.js +318 -0
  79. package/build/commands/start/brief.js +1 -1
  80. package/build/commands/start/index.d.ts +1 -1
  81. package/build/commands/start/index.d.ts.map +1 -1
  82. package/build/commands/start/index.js +7 -7
  83. package/build/index.js +6 -1
  84. package/build/paletteLayout.d.ts.map +1 -1
  85. package/build/paletteLayout.js +9 -8
  86. package/build/utils/cdkState.js +2 -2
  87. package/build/utils/loadCompiledAgent.js +1 -1
  88. package/package.json +6 -5
  89. package/build/commands/ask/artifacts.d.ts.map +0 -1
  90. package/build/commands/ask/binding.d.ts +0 -13
  91. package/build/commands/ask/binding.d.ts.map +0 -1
  92. package/build/commands/ask/binding.js +0 -19
  93. package/build/commands/ask/claudeConfig.d.ts.map +0 -1
  94. package/build/commands/ask/completer.d.ts.map +0 -1
  95. package/build/commands/ask/history.d.ts.map +0 -1
  96. package/build/commands/ask/localHarness.d.ts +0 -278
  97. package/build/commands/ask/localHarness.d.ts.map +0 -1
  98. package/build/commands/ask/localHarness.js +0 -1300
  99. package/build/commands/ask/messageStream.d.ts +0 -33
  100. package/build/commands/ask/messageStream.d.ts.map +0 -1
  101. package/build/commands/ask/messageStream.js +0 -114
  102. package/build/commands/ask/persist.d.ts.map +0 -1
  103. package/build/commands/ask/pnpmShim.d.ts.map +0 -1
  104. package/build/commands/ask/prompt.d.ts.map +0 -1
  105. package/build/commands/ask/session.d.ts +0 -101
  106. package/build/commands/ask/session.d.ts.map +0 -1
  107. package/build/commands/ask/session.js +0 -1328
  108. package/build/commands/ask/slash.d.ts.map +0 -1
  109. package/build/commands/ask/state.d.ts.map +0 -1
  110. package/build/commands/ask/thinking.d.ts.map +0 -1
  111. package/build/commands/ask/typeAhead.d.ts.map +0 -1
  112. /package/build/commands/ask/{artifacts.d.ts → harness/artifacts.d.ts} +0 -0
  113. /package/build/commands/ask/{artifacts.js → harness/artifacts.js} +0 -0
  114. /package/build/commands/ask/{claudeConfig.d.ts → harness/claudeConfig.d.ts} +0 -0
  115. /package/build/commands/ask/{claudeConfig.js → harness/claudeConfig.js} +0 -0
  116. /package/build/commands/ask/{pnpmShim.d.ts → harness/pnpmShim.d.ts} +0 -0
  117. /package/build/commands/ask/{pnpmShim.js → harness/pnpmShim.js} +0 -0
  118. /package/build/commands/ask/{completer.d.ts → input/completer.d.ts} +0 -0
  119. /package/build/commands/ask/{completer.js → input/completer.js} +0 -0
  120. /package/build/commands/ask/{prompt.d.ts → input/prompt.d.ts} +0 -0
  121. /package/build/commands/ask/{slash.d.ts → input/slash.d.ts} +0 -0
  122. /package/build/commands/ask/{typeAhead.d.ts → input/typeAhead.d.ts} +0 -0
  123. /package/build/commands/ask/{typeAhead.js → input/typeAhead.js} +0 -0
  124. /package/build/commands/ask/{history.js → persist/history.js} +0 -0
  125. /package/build/commands/ask/{persist.d.ts → persist/persist.d.ts} +0 -0
  126. /package/build/commands/ask/{persist.js → persist/persist.js} +0 -0
  127. /package/build/commands/ask/{state.d.ts → persist/state.d.ts} +0 -0
  128. /package/build/commands/ask/{state.js → persist/state.js} +0 -0
  129. /package/build/commands/ask/{thinking.d.ts → session/thinking.d.ts} +0 -0
  130. /package/build/commands/ask/{thinking.js → session/thinking.js} +0 -0
@@ -0,0 +1,626 @@
1
+ import { DENIED_TOOL_REASON, isAlwaysDenied, isAutoApproved, } from "./approvals.js";
2
+ import { addTurnUsage, createTurnAccumulator, } from "./usage.js";
3
+ /**
4
+ * How many times one turn may suspend for approvals. Each round trip is a
5
+ * decision the user made, so this is only a stop for a harness that keeps
6
+ * re-asking for a tool it was already refused.
7
+ */
8
+ const MAX_APPROVAL_ROUNDS = 64;
9
+ /**
10
+ * How long a turn may produce nothing at all while no tool is running.
11
+ *
12
+ * Generous on purpose: this is a deadlock detector, not a latency budget. The
13
+ * clock only runs when nothing is in flight, so the wait is over a turn that
14
+ * is neither working nor talking.
15
+ *
16
+ * `CARGO_ASK_STALL_TIMEOUT_MS` shortens it for the end-to-end tests, which
17
+ * would otherwise have to sit out the real deadline to prove a hang is caught.
18
+ */
19
+ const TURN_STALL_TIMEOUT_MS = readTimeoutEnv("CARGO_ASK_STALL_TIMEOUT_MS", 120_000);
20
+ /**
21
+ * The same deadline, for a turn that has a tool executing.
22
+ *
23
+ * A tool at work is silent by design — a test run says nothing for as long as
24
+ * it takes — so this cannot be the deadline above. But it cannot be *no*
25
+ * deadline either, which is what it used to be: a tool the user approved and
26
+ * that never reported back left the turn waiting forever, spinner and all,
27
+ * with nothing to do but kill the terminal. That is the shape of every "it got
28
+ * stuck on the tool" report.
29
+ *
30
+ * Twelve minutes, from Claude Code's own ten-minute ceiling on a Bash tool.
31
+ * That is the bound for the common case, not for every case: the ceiling is
32
+ * configurable, and it does not apply to an MCP tool at all. A `mcp__cargo__*`
33
+ * call that genuinely runs longer than this is cut off with its work lost, so
34
+ * `CARGO_ASK_TOOL_STALL_TIMEOUT_MS` raises it for a workspace that does that
35
+ * routinely. The trade is deliberate: a deadline that covers every conceivable
36
+ * tool is indistinguishable from the hang it replaced.
37
+ *
38
+ * Recoverable either way — the session is replaced and the chat, its history
39
+ * and the working copy all survive.
40
+ */
41
+ const TURN_TOOL_STALL_TIMEOUT_MS = readTimeoutEnv("CARGO_ASK_TOOL_STALL_TIMEOUT_MS", 12 * 60_000);
42
+ function readTimeoutEnv(name, fallbackMs) {
43
+ const raw = process.env[name];
44
+ if (raw === undefined) {
45
+ return fallbackMs;
46
+ }
47
+ const parsed = Number(raw);
48
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackMs;
49
+ }
50
+ /**
51
+ * Run one turn to completion, answering approval requests along the way.
52
+ *
53
+ * A turn suspends whenever Claude Code wants a tool `permissionMode` does not
54
+ * cover: the stream ends with the approvals still pending, and the turn only
55
+ * finishes once decisions are fed back in. Exported for tests.
56
+ */
57
+ export async function runPromptTurn(input) {
58
+ const handlers = input.handlers === undefined ? {} : input.handlers;
59
+ const onApproval = handlers.onApproval;
60
+ const accumulator = input.accumulator === undefined
61
+ ? createTurnAccumulator()
62
+ : input.accumulator;
63
+ const stallTimeoutMs = input.stallTimeoutMs === undefined
64
+ ? TURN_STALL_TIMEOUT_MS
65
+ : input.stallTimeoutMs;
66
+ const toolStallTimeoutMs = input.toolStallTimeoutMs === undefined
67
+ ? TURN_TOOL_STALL_TIMEOUT_MS
68
+ : input.toolStallTimeoutMs;
69
+ const assistantChunks = accumulator.chunks;
70
+ const toolCalls = new Map();
71
+ const turnState = {
72
+ toolRanSinceText: false,
73
+ announced: [],
74
+ };
75
+ let stream = await input.driver.start();
76
+ let round = 0;
77
+ for (;;) {
78
+ const approvals = await consumeTurn({
79
+ stream,
80
+ handlers,
81
+ assistantChunks,
82
+ toolCalls,
83
+ usage: accumulator.usage,
84
+ turnState,
85
+ stallTimeoutMs,
86
+ toolStallTimeoutMs,
87
+ onStall: input.onStall,
88
+ });
89
+ if (approvals.length === 0) {
90
+ break;
91
+ }
92
+ if (input.unanswered !== undefined) {
93
+ input.unanswered.push(...approvals);
94
+ }
95
+ round += 1;
96
+ if (round > MAX_APPROVAL_ROUNDS) {
97
+ throw new Error(`Local Claude Code asked for tool approval ${String(MAX_APPROVAL_ROUNDS)} times in one turn without finishing. Stopping the turn.`);
98
+ }
99
+ const continuations = [];
100
+ for (const approval of approvals) {
101
+ // Refused before the user is asked: not their call to make, and a
102
+ // prompt for it could not say what it was granting.
103
+ const denied = isAlwaysDenied(approval.toolName);
104
+ const approved = denied === true
105
+ ? false
106
+ : isAutoApproved(approval.toolName, {
107
+ unattended: onApproval === undefined,
108
+ }) === true
109
+ ? true
110
+ : onApproval === undefined
111
+ ? false
112
+ : await onApproval(approval);
113
+ // Recorded against the call so the retraction pass below knows it is
114
+ // settled: an approved one is running and must be left alone, a refused
115
+ // one is certainly not and needs no reprieve to find out. Absent when
116
+ // its result already arrived, which takes it off the list.
117
+ const announced = turnState.announced.find((call) => call.toolCallId === approval.toolCallId);
118
+ if (announced !== undefined) {
119
+ announced.status = approved === true ? "approved" : "refused";
120
+ }
121
+ // Answered, so the drain on a later turn must not deny it again.
122
+ if (input.unanswered !== undefined) {
123
+ const at = input.unanswered.indexOf(approval);
124
+ if (at !== -1) {
125
+ input.unanswered.splice(at, 1);
126
+ }
127
+ }
128
+ continuations.push({
129
+ approvalResponse: {
130
+ type: "tool-approval-response",
131
+ approvalId: approval.approvalId,
132
+ approved,
133
+ // Told why, so it does something else rather than asking again
134
+ // until the approval-round cap ends the turn.
135
+ reason: denied === true ? DENIED_TOOL_REASON : undefined,
136
+ },
137
+ toolCall: {
138
+ type: "tool-call",
139
+ toolCallId: approval.toolCallId,
140
+ toolName: approval.toolName,
141
+ input: approval.input,
142
+ },
143
+ });
144
+ }
145
+ retractAbandonedCalls(turnState.announced, handlers.onActivity);
146
+ stream = await input.driver.continueWithApprovals(continuations);
147
+ }
148
+ const streamed = assistantChunks.join("");
149
+ if (streamed.length > 0) {
150
+ return streamed;
151
+ }
152
+ const fallback = await stream.text;
153
+ if (typeof fallback !== "string" || fallback.length === 0) {
154
+ return "";
155
+ }
156
+ if (handlers.onTextDelta !== undefined) {
157
+ handlers.onTextDelta(fallback);
158
+ }
159
+ return fallback;
160
+ }
161
+ /**
162
+ * Retract the calls the caller was told about and will never hear the end of,
163
+ * at the boundary where the answers to this round's approvals are known.
164
+ *
165
+ * Left on screen instead, an abandoned call outlives the work: no result is
166
+ * coming, so no `end` event is either, and the caller keeps showing a tool
167
+ * that stopped existing.
168
+ *
169
+ * `"approved"` is kept, and `"running"` is the one case that cannot be judged
170
+ * yet — the reason this is not a plain sweep. It is either the sibling whose
171
+ * approval request the harness lost (it surfaces one per stream) or a tool
172
+ * Claude Code ran without asking, whose result has simply not arrived. Both
173
+ * look identical here, so it gets one more stream to prove itself: a live one
174
+ * reports its result and leaves this list on its own.
175
+ *
176
+ * Everything else is certain and retracted — `"streaming"` never finished
177
+ * arriving, `"refused"` was told no, and `"abandoning"` already had its
178
+ * stream and stayed silent. Refusals matter especially: a tool
179
+ * `isAlwaysDenied` refuses never reaches `onApproval`, so this is the caller's
180
+ * only notice.
181
+ *
182
+ * The reprieve is for the caller's benefit only — `"abandoning"` is not an
183
+ * executing status, so the stall watchdog keeps its short deadline throughout
184
+ * it. Extending the reprieve to the watchdog would deadlock the very hang it
185
+ * exists to prevent: a dropped sibling would hold the timer off, the stream
186
+ * would stay silent, and the next boundary, the thing that resolves the
187
+ * ambiguity, would never arrive.
188
+ */
189
+ function retractAbandonedCalls(announced, onActivity) {
190
+ for (const call of [...announced]) {
191
+ if (call.status === "approved") {
192
+ continue;
193
+ }
194
+ if (call.status === "running") {
195
+ call.status = "abandoning";
196
+ continue;
197
+ }
198
+ announced.splice(announced.indexOf(call), 1);
199
+ if (onActivity !== undefined) {
200
+ onActivity({
201
+ kind: "dropped",
202
+ toolCallId: call.toolCallId,
203
+ toolName: call.toolName,
204
+ });
205
+ }
206
+ }
207
+ }
208
+ /**
209
+ * The previous turn could not be wound down, so this session cannot take
210
+ * another prompt. Raised rather than pressed on with: the harness would either
211
+ * refuse the prompt itself or run it alongside the turn we failed to finish.
212
+ *
213
+ * The caller's recovery is to replace the session — nothing about a wedged
214
+ * runtime gets better by asking it again.
215
+ */
216
+ export class HarnessTurnStuck extends Error {
217
+ constructor() {
218
+ super("The previous turn could not be wound down, so this coding session cannot take another question.");
219
+ this.name = "HarnessTurnStuck";
220
+ }
221
+ }
222
+ /**
223
+ * The turn produced nothing for `TURN_STALL_TIMEOUT_MS` while no tool was in
224
+ * flight. Common causes: a step that asked to run several confirmable tools at
225
+ * once (the harness only surfaces one approval), or the Claude Code bridge
226
+ * going silent. Recoverable — replace the session and ask again.
227
+ */
228
+ export class HarnessTurnStalled extends Error {
229
+ constructor(timeoutMs, whileToolRunning = false) {
230
+ const seconds = String(Math.round(timeoutMs / 1000));
231
+ super(whileToolRunning === true
232
+ ? `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.`
233
+ : `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.`);
234
+ this.name = "HarnessTurnStalled";
235
+ }
236
+ }
237
+ /**
238
+ * The Claude Code bridge dropped mid-turn. Same recovery as a stall: the
239
+ * process behind the session is gone, so the next question needs a new one.
240
+ */
241
+ export function isBridgeClosedError(error) {
242
+ return (error instanceof Error &&
243
+ error.message.includes("bridge closed before the turn finished") === true);
244
+ }
245
+ /**
246
+ * Fail a turn that has gone silent with no tool running, instead of hanging.
247
+ *
248
+ * `@ai-sdk/harness` surfaces one approval per stream and ends the stream
249
+ * there, so a step that asks to run two tools at once loses the second
250
+ * request: Claude Code keeps waiting for a decision that is never offered,
251
+ * while we keep waiting for output that never comes. Both sides wait forever.
252
+ * `LOCAL_WORKING_COPY_INSTRUCTIONS` asks for one confirmable tool per step to
253
+ * avoid it; this is the backstop for when the model does it anyway.
254
+ *
255
+ * Two deadlines rather than one, because a tool at work is silent by design
256
+ * and a ten-minute test run must not be cut off: `timeoutMs` for a stream with
257
+ * nothing executing, and the far longer `toolTimeoutMs` while a tool is. What
258
+ * there is never again is *no* deadline — a tool that never reports back used
259
+ * to leave the turn on the spinner forever.
260
+ */
261
+ async function* withStallTimeout(stream, timeoutMs, toolTimeoutMs, isToolInFlight, onStall) {
262
+ if (Number.isFinite(timeoutMs) === false &&
263
+ Number.isFinite(toolTimeoutMs) === false) {
264
+ for await (const part of stream) {
265
+ yield part;
266
+ }
267
+ return;
268
+ }
269
+ const iterator = stream[Symbol.asyncIterator]();
270
+ for (;;) {
271
+ const next = iterator.next();
272
+ const running = isToolInFlight();
273
+ const budgetMs = running === true ? toolTimeoutMs : timeoutMs;
274
+ const result = await (Number.isFinite(budgetMs) === false
275
+ ? next
276
+ : rejectIfIdle(next, budgetMs, onStall, running));
277
+ if (result.done === true) {
278
+ return;
279
+ }
280
+ yield result.value;
281
+ }
282
+ }
283
+ /**
284
+ * Wait for `work`, but give up after `timeoutMs` rather than blocking a
285
+ * teardown forever. Resolves either way; the caller has already decided that
286
+ * an unfinished result is better than waiting.
287
+ *
288
+ * Exported for tests.
289
+ */
290
+ export async function settleWithin(work, timeoutMs) {
291
+ let timer;
292
+ try {
293
+ await Promise.race([
294
+ work,
295
+ new Promise((resolve) => {
296
+ timer = setTimeout(resolve, timeoutMs);
297
+ }),
298
+ ]);
299
+ }
300
+ finally {
301
+ if (timer !== undefined) {
302
+ clearTimeout(timer);
303
+ }
304
+ }
305
+ }
306
+ async function rejectIfIdle(next, timeoutMs, onStall, whileToolRunning) {
307
+ let timer;
308
+ try {
309
+ return await Promise.race([
310
+ next,
311
+ new Promise((_resolve, reject) => {
312
+ timer = setTimeout(() => {
313
+ // Reject first so Promise.race settles on HarnessTurnStalled, not
314
+ // on an AbortError the interrupt below may race onto `next`.
315
+ reject(new HarnessTurnStalled(timeoutMs, whileToolRunning));
316
+ // Then stop the bridge so teardown is an intentional abort, not a
317
+ // mysterious "bridge closed before the turn finished" after we
318
+ // have already walked away from the stream.
319
+ if (onStall !== undefined) {
320
+ onStall();
321
+ }
322
+ }, timeoutMs);
323
+ }),
324
+ ]);
325
+ }
326
+ finally {
327
+ if (timer !== undefined) {
328
+ clearTimeout(timer);
329
+ }
330
+ // On a stall we walk away from `next`; a later failure has no other
331
+ // handler and would come back as an unhandled rejection.
332
+ next.catch(() => undefined);
333
+ }
334
+ }
335
+ /** Tool inputs that name a file the turn is about to change. */
336
+ const FILE_WRITING_TOOLS = new Set([
337
+ "write",
338
+ "edit",
339
+ "Write",
340
+ "Edit",
341
+ "NotebookEdit",
342
+ ]);
343
+ /**
344
+ * Drain one turn: text deltas go to the caller, tool calls are remembered so
345
+ * an approval request can be matched back to its call, and pending approvals
346
+ * are returned so the caller can decide and continue the turn.
347
+ */
348
+ async function consumeTurn(input) {
349
+ const approvals = [];
350
+ const { handlers } = input;
351
+ const pendingFiles = new Map();
352
+ const parts = withStallTimeout(input.stream.fullStream, input.stallTimeoutMs, input.toolStallTimeoutMs, () => input.turnState.announced.some((call) => call.status === "running" || call.status === "approved"), input.onStall);
353
+ for await (const rawPart of parts) {
354
+ if (typeof rawPart !== "object" || rawPart === null) {
355
+ continue;
356
+ }
357
+ const part = rawPart;
358
+ if (part["type"] === "error") {
359
+ const error = part["error"];
360
+ throw new Error(error instanceof Error
361
+ ? error.message
362
+ : typeof error === "string"
363
+ ? error
364
+ : "Local Claude Code turn failed.");
365
+ }
366
+ // Claude Code streams the tool name before the full call is assembled.
367
+ // Surface that early so the spinner is not stuck on "Asking…" while the
368
+ // input is still arriving — and so a later `tool-call` can fill in the
369
+ // summary without looking like a second tool.
370
+ if (part["type"] === "tool-input-start") {
371
+ const toolCallId = part["id"];
372
+ const toolName = part["toolName"];
373
+ if (typeof toolCallId === "string" && typeof toolName === "string") {
374
+ if (input.toolCalls.has(toolCallId) === false) {
375
+ input.toolCalls.set(toolCallId, {
376
+ toolName,
377
+ input: undefined,
378
+ });
379
+ input.turnState.toolRanSinceText = true;
380
+ // Announced, because the spinner is about to show this name and is
381
+ // owed a retraction if the call never happens — but `"streaming"`,
382
+ // not executing. The tool is not running yet, its input is still
383
+ // being generated, and the deltas that carry it are output the
384
+ // watchdog is entitled to expect; standing it down here would turn a
385
+ // bridge that dies mid-input into a hang.
386
+ input.turnState.announced.push({
387
+ toolCallId,
388
+ toolName,
389
+ status: "streaming",
390
+ });
391
+ if (handlers.onActivity !== undefined) {
392
+ handlers.onActivity({
393
+ kind: "start",
394
+ toolCallId,
395
+ toolName,
396
+ summary: "",
397
+ });
398
+ }
399
+ }
400
+ }
401
+ continue;
402
+ }
403
+ if (part["type"] === "tool-call") {
404
+ const toolCallId = part["toolCallId"];
405
+ const toolName = part["toolName"];
406
+ if (typeof toolCallId === "string" && typeof toolName === "string") {
407
+ input.toolCalls.set(toolCallId, {
408
+ toolName,
409
+ input: part["input"],
410
+ });
411
+ input.turnState.toolRanSinceText = true;
412
+ // The full call is assembled, so something is executing it — that is
413
+ // what stands the stall watchdog down, and it deliberately waits until
414
+ // here rather than firing at `tool-input-start`.
415
+ const announced = input.turnState.announced.find((call) => call.toolCallId === toolCallId);
416
+ if (announced === undefined) {
417
+ input.turnState.announced.push({
418
+ toolCallId,
419
+ toolName,
420
+ status: "running",
421
+ });
422
+ }
423
+ else {
424
+ announced.status = "running";
425
+ }
426
+ // Re-emitted when `tool-input-start` already announced the name, so
427
+ // the spinner can pick up the input summary once it is known.
428
+ if (handlers.onActivity !== undefined) {
429
+ handlers.onActivity({
430
+ kind: "start",
431
+ toolCallId,
432
+ toolName,
433
+ summary: summarizeToolInput(part["input"]),
434
+ });
435
+ }
436
+ const touched = FILE_WRITING_TOOLS.has(toolName) === true
437
+ ? readFilePath(part["input"])
438
+ : undefined;
439
+ // Held until the result says the write happened: a denied or failed
440
+ // edit must not show up under "files changed".
441
+ if (touched !== undefined) {
442
+ pendingFiles.set(toolCallId, touched);
443
+ }
444
+ }
445
+ continue;
446
+ }
447
+ if (part["type"] === "tool-result" || part["type"] === "tool-error") {
448
+ const toolCallId = part["toolCallId"];
449
+ const recorded = typeof toolCallId === "string"
450
+ ? input.toolCalls.get(toolCallId)
451
+ : undefined;
452
+ const isError = part["type"] === "tool-error";
453
+ // A preliminary result is progress from a tool that is still running,
454
+ // so it must not clear the spinner or tell the stall watchdog the tool
455
+ // is done.
456
+ const preliminary = part["preliminary"] === true;
457
+ if (preliminary === false &&
458
+ typeof toolCallId === "string" &&
459
+ handlers.onActivity !== undefined) {
460
+ handlers.onActivity({
461
+ kind: "end",
462
+ toolCallId,
463
+ toolName: recorded === undefined ? "tool" : recorded.toolName,
464
+ isError,
465
+ });
466
+ }
467
+ if (typeof toolCallId === "string" && preliminary === false) {
468
+ // Off the list on the way out. The `end` above is the caller's
469
+ // retraction, so a later approval round must not announce a second
470
+ // one — and leaving under its own steam is how a tool that ran unasked
471
+ // proves it was alive rather than abandoned.
472
+ const at = input.turnState.announced.findIndex((call) => call.toolCallId === toolCallId);
473
+ if (at !== -1) {
474
+ input.turnState.announced.splice(at, 1);
475
+ }
476
+ const touched = pendingFiles.get(toolCallId);
477
+ pendingFiles.delete(toolCallId);
478
+ if (touched !== undefined &&
479
+ isError === false &&
480
+ handlers.onFileTouched !== undefined) {
481
+ handlers.onFileTouched(touched);
482
+ }
483
+ }
484
+ continue;
485
+ }
486
+ if (part["type"] === "finish") {
487
+ // One suspended turn is several streams, each ending with its own
488
+ // `finish` and its own totals — sum them. Per-step parts are ignored
489
+ // so the same tokens are not counted twice.
490
+ addTurnUsage(input.usage, part);
491
+ continue;
492
+ }
493
+ if (part["type"] === "tool-approval-request") {
494
+ const approval = readApprovalRequest(part, input.toolCalls);
495
+ if (approval !== undefined) {
496
+ approvals.push(approval);
497
+ }
498
+ continue;
499
+ }
500
+ if (part["type"] === "text-delta") {
501
+ const text = typeof part["text"] === "string"
502
+ ? part["text"]
503
+ : typeof part["delta"] === "string"
504
+ ? part["delta"]
505
+ : undefined;
506
+ if (text === undefined || text.length === 0) {
507
+ continue;
508
+ }
509
+ // Claude Code narrates, runs tools, then narrates again. Without a
510
+ // break the two sides of a tool call run into one another mid-sentence.
511
+ const previous = input.assistantChunks.join("");
512
+ if (input.turnState.toolRanSinceText === true &&
513
+ previous.length > 0 &&
514
+ previous.endsWith("\n") === false) {
515
+ input.assistantChunks.push("\n\n");
516
+ if (handlers.onTextDelta !== undefined) {
517
+ handlers.onTextDelta("\n\n");
518
+ }
519
+ }
520
+ input.turnState.toolRanSinceText = false;
521
+ input.assistantChunks.push(text);
522
+ if (handlers.onTextDelta !== undefined) {
523
+ handlers.onTextDelta(text);
524
+ }
525
+ }
526
+ }
527
+ return approvals;
528
+ }
529
+ /**
530
+ * The AI SDK part nests the whole `toolCall`; the harness bridge part carries
531
+ * only `toolCallId`. Accept both and fall back to the recorded call.
532
+ */
533
+ function readApprovalRequest(part, toolCalls) {
534
+ const approvalId = part["approvalId"];
535
+ if (typeof approvalId !== "string") {
536
+ return undefined;
537
+ }
538
+ const nested = part["toolCall"];
539
+ const toolCall = typeof nested === "object" && nested !== null
540
+ ? nested
541
+ : undefined;
542
+ const toolCallId = toolCall !== undefined && typeof toolCall["toolCallId"] === "string"
543
+ ? toolCall["toolCallId"]
544
+ : typeof part["toolCallId"] === "string"
545
+ ? part["toolCallId"]
546
+ : undefined;
547
+ if (toolCallId === undefined) {
548
+ return undefined;
549
+ }
550
+ const recorded = toolCalls.get(toolCallId);
551
+ const toolName = toolCall !== undefined && typeof toolCall["toolName"] === "string"
552
+ ? toolCall["toolName"]
553
+ : recorded !== undefined
554
+ ? recorded.toolName
555
+ : "tool";
556
+ const toolInput = toolCall !== undefined && "input" in toolCall
557
+ ? toolCall["input"]
558
+ : recorded !== undefined
559
+ ? recorded.input
560
+ : undefined;
561
+ return {
562
+ approvalId,
563
+ toolCallId,
564
+ toolName,
565
+ summary: summarizeToolInput(toolInput),
566
+ input: toolInput,
567
+ };
568
+ }
569
+ /** Fields worth showing first when asking to run a tool. */
570
+ const TOOL_INPUT_PREVIEW_KEYS = [
571
+ "command",
572
+ "file_path",
573
+ "path",
574
+ "pattern",
575
+ "url",
576
+ ];
577
+ const TOOL_SUMMARY_MAX_LENGTH = 160;
578
+ /**
579
+ * The bridge sends tool input as a JSON string, while the AI SDK part carries
580
+ * the parsed object. Normalize to a record when the shape allows it.
581
+ */
582
+ function readToolInputRecord(toolInput) {
583
+ if (typeof toolInput === "string") {
584
+ try {
585
+ const parsed = JSON.parse(toolInput);
586
+ return typeof parsed === "object" && parsed !== null
587
+ ? parsed
588
+ : undefined;
589
+ }
590
+ catch {
591
+ return undefined;
592
+ }
593
+ }
594
+ return typeof toolInput === "object" && toolInput !== null
595
+ ? toolInput
596
+ : undefined;
597
+ }
598
+ function readFilePath(toolInput) {
599
+ const record = readToolInputRecord(toolInput);
600
+ if (record === undefined) {
601
+ return undefined;
602
+ }
603
+ const filePath = record["file_path"];
604
+ return typeof filePath === "string" && filePath.length > 0
605
+ ? filePath
606
+ : undefined;
607
+ }
608
+ function summarizeToolInput(toolInput) {
609
+ const record = readToolInputRecord(toolInput);
610
+ if (record === undefined) {
611
+ return typeof toolInput === "string" ? truncate(toolInput) : "";
612
+ }
613
+ for (const key of TOOL_INPUT_PREVIEW_KEYS) {
614
+ const value = record[key];
615
+ if (typeof value === "string" && value.length > 0) {
616
+ return truncate(value);
617
+ }
618
+ }
619
+ return truncate(JSON.stringify(record));
620
+ }
621
+ function truncate(value) {
622
+ const flat = value.replace(/\s+/g, " ").trim();
623
+ return flat.length > TOOL_SUMMARY_MAX_LENGTH
624
+ ? `${flat.slice(0, TOOL_SUMMARY_MAX_LENGTH - 1)}…`
625
+ : flat;
626
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * What one turn spent upstream, summed over the streams a suspended turn is
3
+ * made of. `costUsd` is Claude Code's own accounting; the API stores it on
4
+ * the message. Credits for the spend are billed on the sandbox proxy from
5
+ * the forwarded Anthropic response.
6
+ */
7
+ export type LocalTurnUsage = {
8
+ costUsd: number;
9
+ totalTokens: number;
10
+ inputTokens: number;
11
+ outputTokens: number;
12
+ cacheReadTokens: number;
13
+ cacheWriteTokens: number;
14
+ reasoningTokens: number;
15
+ };
16
+ /**
17
+ * Text and usage of a turn in progress, owned by the caller rather than by
18
+ * `runPromptTurn`: a cancelled or failed turn still spent those tokens, and
19
+ * they have to reach the API anyway.
20
+ */
21
+ export type LocalTurnAccumulator = {
22
+ chunks: string[];
23
+ usage: LocalTurnUsage;
24
+ };
25
+ /** Exported for tests. */
26
+ export declare function createTurnAccumulator(): LocalTurnAccumulator;
27
+ /**
28
+ * Whether the turn got as far as Anthropic. `costUsd` and `totalTokens` are
29
+ * reported independently by the harness, so either alone means it did.
30
+ */
31
+ export declare function didReachAnthropic(usage: LocalTurnUsage): boolean;
32
+ /**
33
+ * Add one stream's `finish` totals to the turn. `totalUsage` is the AI SDK
34
+ * shape; `costUsd` is what Claude Code reports for the same stream under
35
+ * `harnessMetadata`, and is stored on the ingested message.
36
+ */
37
+ export declare function addTurnUsage(usage: LocalTurnUsage, part: Record<string, unknown>): void;
38
+ /**
39
+ * What Claude Code says the turn cost, off the `finish` part.
40
+ *
41
+ * Two keys, because the value is renamed on its way here: the bridge emits
42
+ * `harnessMetadata`, and `HarnessAgent` republishes the same object as
43
+ * `providerMetadata` on the parts this code actually reads. Looking only for
44
+ * the bridge's name found nothing, so every local turn was ingested at a cost
45
+ * of zero and priced into no credits at all.
46
+ *
47
+ * Exported for tests.
48
+ */
49
+ export declare function readClaudeCodeCostUsd(part: Record<string, unknown>): number | undefined;
50
+ //# sourceMappingURL=usage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../../../src/commands/ask/harness/usage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,0BAA0B;AAC1B,wBAAgB,qBAAqB,IAAI,oBAAoB,CAa5D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAEhE;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,IAAI,CAmCN;AAQD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,GAAG,SAAS,CA0BpB"}