@nexrall/code-core 1.4.13 → 1.4.15

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.
@@ -102,4 +102,17 @@ export declare function compactMessagesForResume(messages: Message[], opts: {
102
102
  onNotice?: (text: string) => void;
103
103
  }): Promise<boolean>;
104
104
  export declare function runAgentLoop(initialMessages: Message[], options: AgentLoopOptions): Promise<Message[]>;
105
+ /**
106
+ * Cut a history back to the last point the API will accept as a resumable
107
+ * prefix, WITHOUT mutating the input.
108
+ *
109
+ * Anthropic rejects a conversation whose final assistant turn contains
110
+ * `tool_use` blocks that never got a matching `tool_result`. The main loop
111
+ * always appends in complete pairs so it can't produce that shape, but a crash
112
+ * in the middle of executing a tool round can: the assistant turn was already
113
+ * pushed, the tool_result carrier never was. Dropping that dangling assistant
114
+ * turn costs one model response and makes the rest of the (often very long)
115
+ * history resumable, instead of throwing all of it away.
116
+ */
117
+ export declare function trimToResumableBoundary(messages: Message[]): Message[];
105
118
  //# sourceMappingURL=loop.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;AAwKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AA+TD,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAM7D;AAsBD,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,aAA+G,CAAC;AAC7I,gGAAgG;AAChG,eAAO,MAAM,aAAa,QAA2J,CAAC;AAEtL;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK5E;AAUD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAwBxD;AAoBD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,aAAa,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,qFAAqF;IACrF,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,IAAI,cAAc,CAE7C;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,EAAE,EAAE,OAAO,EACX,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CAoDN;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA6B5D;AAmBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,eAAe,SAAI,GAAG,MAAM,CAgCpF;AA8JD,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAE/D;AAED;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,OAAO,EAAE,EACnB,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,GACA,OAAO,CAAC,OAAO,CAAC,CAqElB;AAID,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CA2oBpB"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;AAyKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AA+UD,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAM7D;AAsBD,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,aAA+G,CAAC;AAC7I,gGAAgG;AAChG,eAAO,MAAM,aAAa,QAA2J,CAAC;AAEtL;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAK5E;AAUD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAwBxD;AAoBD,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,aAAa,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,qFAAqF;IACrF,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,IAAI,cAAc,CAE7C;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC1C,EAAE,EAAE,OAAO,EACX,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CAoDN;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA6B5D;AAmBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,eAAe,SAAI,GAAG,MAAM,CAgCpF;AAsKD,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAE/D;AAED;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,OAAO,EAAE,EACnB,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,GACA,OAAO,CAAC,OAAO,CAAC,CAqElB;AAID,wBAAsB,YAAY,CAChC,eAAe,EAAE,OAAO,EAAE,EAC1B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,OAAO,EAAE,CAAC,CA+sBpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAqCtE"}
@@ -45,6 +45,8 @@ exports.pruneOldToolResults = pruneOldToolResults;
45
45
  exports.estimateTokensRough = estimateTokensRough;
46
46
  exports.compactMessagesForResume = compactMessagesForResume;
47
47
  exports.runAgentLoop = runAgentLoop;
48
+ exports.trimToResumableBoundary = trimToResumableBoundary;
49
+ const types_1 = require("../types");
48
50
  const client_1 = require("../api/client");
49
51
  const executor_1 = require("../tools/executor");
50
52
  const agentTypes_1 = require("./agentTypes");
@@ -366,6 +368,22 @@ async function runSubTask(input, options, agentTypes) {
366
368
  nexrallMd: subNexrallMd,
367
369
  requestPermission: gatedPermission,
368
370
  onText: () => { }, // sub-agent text is returned as the tool result, not streamed live
371
+ // Forwarded DELIBERATELY, and it must be a real handler rather than a no-op.
372
+ //
373
+ // A sub-agent streams no text (onText above is a no-op) but it DOES stream thinking
374
+ // through the parent's UI (see onThinking/onThinkingDelta below), and thinking sets
375
+ // `emittedToCaller`. So a sub-agent stream that dies after reasoning genuinely has
376
+ // rendered output to discard — a no-op here would let the restart proceed and then
377
+ // re-stream that reasoning on top of the copy still on screen, the exact duplication
378
+ // the opt-in exists to prevent.
379
+ //
380
+ // Forwarding is safe because the parent's own output is already closed by this
381
+ // point: dispatching the `task` tool goes through options.onToolUse, which finalizes
382
+ // the parent's bubble (VS Code) / flushes the renderer (CLI) before the sub-agent
383
+ // starts. The only live, discardable element at restart time is the sub-agent's own
384
+ // thinking block. Completed tool rows are left alone — those are real side effects
385
+ // that actually happened.
386
+ onStreamRestart: (reason, chars) => options.onStreamRestart?.(reason, chars),
369
387
  // Forward tool events with isSubTask=true so the UI can render a badge
370
388
  // instead of prepending "[sub-task]" to the tool name (which caused double-prefix
371
389
  // when the name was already labelled, and mixed display concerns into the data layer).
@@ -802,6 +820,11 @@ async function autoCompactMessages(messages, options, ledger) {
802
820
  env: options.env,
803
821
  clientType: options.clientType,
804
822
  abortSignal: options.abortSignal,
823
+ // The summariser renders NOTHING (onEvent below is a no-op) and its result is
824
+ // read only from the returned message, so a restart has nothing to roll back —
825
+ // always safe. Worth enabling: a blip here used to abandon compaction entirely,
826
+ // which then let the very next turn hit the context wall it was meant to prevent.
827
+ allowRestartAfterRender: true,
805
828
  }, () => { });
806
829
  summary = reply.content
807
830
  .filter((b) => b.type === 'text')
@@ -858,6 +881,9 @@ async function maybeCompactMemory(scope, options) {
858
881
  env: options.env,
859
882
  clientType: options.clientType,
860
883
  abortSignal: options.abortSignal,
884
+ // Same as the transcript summariser: no rendered output, result read only from
885
+ // the returned message, so restarting on a blip is always safe.
886
+ allowRestartAfterRender: true,
861
887
  }, () => { });
862
888
  return reply.content
863
889
  .filter((b) => b.type === 'text')
@@ -1010,6 +1036,11 @@ async function runAgentLoop(initialMessages, options) {
1010
1036
  // iteration backstop, so we can tell the user what happened rather than
1011
1037
  // silently returning a half-done task.
1012
1038
  let completedCleanly = false;
1039
+ // Tool rounds actually completed in this call. Counted explicitly rather than derived
1040
+ // from `messages.length` because auto-compaction splices the history SHORTER mid-run:
1041
+ // a long session that compacted and then lost its connection would show a negative
1042
+ // length delta and be judged "no progress", discarding the work it most needs to keep.
1043
+ let completedRounds = 0;
1013
1044
  let stalledOut = false; // tripped the runaway guard (all-failed rounds)
1014
1045
  let consecutiveErrorRounds = 0; // rounds where every tool call errored
1015
1046
  let budget = maxIterations; // extended by auto-continue, capped at hardCap
@@ -1110,8 +1141,6 @@ async function runAgentLoop(initialMessages, options) {
1110
1141
  if (!autoContinue && iteration === budget - 5 && budget > 5) {
1111
1142
  options.onText(`\n⚠️ Approaching the ${budget}-step limit (step ${iteration + 1}). Please wrap up and summarise what has been done.\n`);
1112
1143
  }
1113
- // Collect tool_use blocks emitted during this stream
1114
- const pendingToolUse = [];
1115
1144
  // Build SSE event handler
1116
1145
  const onEvent = (event) => {
1117
1146
  switch (event.type) {
@@ -1128,21 +1157,33 @@ async function runAgentLoop(initialMessages, options) {
1128
1157
  options.onThinkingProgress?.(event.tokens);
1129
1158
  break;
1130
1159
  case 'tool_use':
1131
- // Collect tool use blocks they may arrive incrementally
1132
- // We use message_complete to finalize them, so just track here
1133
- pendingToolUse.push({
1134
- type: 'tool_use',
1135
- id: event.id,
1136
- name: event.name,
1137
- input: event.input,
1138
- });
1160
+ // Nothing to do here. Streamed tool_use events are a PREVIEW only (their
1161
+ // `input` can still be partial JSON mid-stream); the blocks actually
1162
+ // executed are read from the completed message's content at the end of
1163
+ // this iteration. An accumulator here used to exist but was never read —
1164
+ // keeping it invited the assumption that dispatch happens from the stream,
1165
+ // which would break the restart guarantee below.
1139
1166
  break;
1140
1167
  case 'usage':
1141
- lastPromptTokens =
1142
- (event.usage.input_tokens ?? 0) +
1143
- (event.usage.cache_creation_input_tokens ?? 0) +
1144
- (event.usage.cache_read_input_tokens ?? 0);
1145
- options.onUsage(event.usage);
1168
+ // A `partial` report belongs to an attempt that was cut short and restarted.
1169
+ // Its token counts are real spend (already billed upstream) and are forwarded
1170
+ // so a cost display stays honest — but they must NOT drive `lastPromptTokens`,
1171
+ // which feeds the auto-compaction trigger: a truncated attempt understates the
1172
+ // prompt size, and letting it overwrite a correct reading would suppress a
1173
+ // compaction the next turn actually needs.
1174
+ if (!event.partial) {
1175
+ // A `replayed` report DOES update this: it describes the real, complete turn
1176
+ // (served from the backend's idempotency cache after a dropped connection),
1177
+ // so its prompt size is accurate and needed for the compaction decision.
1178
+ // Only `partial` — a truncated attempt — must be excluded.
1179
+ lastPromptTokens =
1180
+ (event.usage.input_tokens ?? 0) +
1181
+ (event.usage.cache_creation_input_tokens ?? 0) +
1182
+ (event.usage.cache_read_input_tokens ?? 0);
1183
+ }
1184
+ // Both flags mean "do not add these tokens to a running cost total": `partial`
1185
+ // was a discarded attempt, `replayed` was already counted when first billed.
1186
+ options.onUsage(event.usage, event.partial === true || event.replayed === true);
1146
1187
  break;
1147
1188
  case 'message_complete':
1148
1189
  // The complete message is returned from streamChat, we handle it below
@@ -1153,11 +1194,29 @@ async function runAgentLoop(initialMessages, options) {
1153
1194
  case 'retry_resolved':
1154
1195
  options.onRetryResolved?.();
1155
1196
  break;
1197
+ case 'stream_restart':
1198
+ // The attempt that died had already rendered text/thinking; the replacement
1199
+ // attempt re-sends the turn in full, so the caller must drop that partial
1200
+ // render or it shows up twice.
1201
+ //
1202
+ // Nothing else needs undoing, and that is a property of the code below, not
1203
+ // an assumption: tools are dispatched from `assistantMessage.content` AFTER
1204
+ // streamChat() returns, and a restarted attempt threw instead of returning.
1205
+ // So no tool ran, no file changed, no checkpoint or hook fired.
1206
+ options.onStreamRestart?.(event.reason, event.discardedChars);
1207
+ break;
1156
1208
  case 'balance_status':
1157
1209
  options.onBalanceStatus?.(event.balance, event.zero);
1158
1210
  break;
1159
1211
  case 'done':
1212
+ break;
1160
1213
  case 'error':
1214
+ // Reaching the loop means the stream did NOT reject on this — i.e. the turn's
1215
+ // message had already been delivered and the error arrived late (typically the
1216
+ // backend's post-turn billing write failing). The answer stands, so this must
1217
+ // not fail the turn, but it must not vanish either: route it to the system-
1218
+ // notice channel so it is visible and logged rather than silently swallowed.
1219
+ options.onNotice?.(`⚠️ ${event.message}`);
1161
1220
  break;
1162
1221
  }
1163
1222
  };
@@ -1176,6 +1235,11 @@ async function runAgentLoop(initialMessages, options) {
1176
1235
  extraTools: options.mcpManager?.getAnthropicTools(),
1177
1236
  agents: agentsCatalogue || undefined,
1178
1237
  skills: skillsCatalogue || undefined,
1238
+ // Only allow a post-render restart when the caller actually implements the
1239
+ // rollback. Without a handler the partial output can't be un-rendered, so we
1240
+ // keep the old conservative behaviour (fail the turn) rather than duplicate
1241
+ // text on screen.
1242
+ allowRestartAfterRender: !!options.onStreamRestart,
1179
1243
  }, onEvent);
1180
1244
  }
1181
1245
  catch (err) {
@@ -1192,7 +1256,14 @@ async function runAgentLoop(initialMessages, options) {
1192
1256
  break;
1193
1257
  }
1194
1258
  runSimpleHooks(hooks.OnError, options.workDir);
1195
- throw new Error(`Stream failed: ${err.message}`);
1259
+ // Carry the work already done in this turn out with the error. The loop
1260
+ // owns a COPY of the caller's history, so a plain throw would strand every
1261
+ // completed tool round inside this function and the caller would fall back
1262
+ // to a history containing only the original user message — the user pays
1263
+ // for N minutes of agent work and then has to start over because
1264
+ // "continue" remembers none of it. `messages` always ends on a complete
1265
+ // assistant→tool_result pair here, so it is valid to resend as-is.
1266
+ throw new types_1.AgentTurnError(`Stream failed: ${err.message}`, trimToResumableBoundary(messages), completedRounds, err);
1196
1267
  }
1197
1268
  if (options.abortSignal?.aborted)
1198
1269
  break;
@@ -1524,6 +1595,9 @@ async function runAgentLoop(initialMessages, options) {
1524
1595
  content: toolResultContent,
1525
1596
  };
1526
1597
  messages.push(toolResultMessage);
1598
+ // A complete assistant→tool_result pair just landed: the history is valid to
1599
+ // resend from here, so this is exactly one unit of salvageable progress.
1600
+ completedRounds++;
1527
1601
  // Incremental persistence hook: history is valid here (ends on a
1528
1602
  // tool_result user turn). Let the caller checkpoint progress so a crash
1529
1603
  // mid-run loses only the in-flight step, not the whole session.
@@ -1564,9 +1638,69 @@ async function runAgentLoop(initialMessages, options) {
1564
1638
  }
1565
1639
  }
1566
1640
  }
1641
+ catch (err) {
1642
+ // Every other failure path (a tool executor blowing up, a hook throwing, an
1643
+ // out-of-memory in a large read) gets the same treatment as a dead stream:
1644
+ // the turn's completed work leaves with the error instead of dying inside
1645
+ // this function's private copy of the history. Already-tagged errors pass
1646
+ // straight through so we don't double-wrap and lose the original message.
1647
+ if (err instanceof types_1.AgentTurnError)
1648
+ throw err;
1649
+ if (options.abortSignal?.aborted || err.name === 'AbortError')
1650
+ throw err;
1651
+ throw new types_1.AgentTurnError(err?.message || 'The turn ended unexpectedly', trimToResumableBoundary(messages), completedRounds, err);
1652
+ }
1567
1653
  finally {
1568
1654
  runSimpleHooks(hooks.OnStop, options.workDir);
1569
1655
  }
1570
1656
  return messages;
1571
1657
  }
1658
+ /**
1659
+ * Cut a history back to the last point the API will accept as a resumable
1660
+ * prefix, WITHOUT mutating the input.
1661
+ *
1662
+ * Anthropic rejects a conversation whose final assistant turn contains
1663
+ * `tool_use` blocks that never got a matching `tool_result`. The main loop
1664
+ * always appends in complete pairs so it can't produce that shape, but a crash
1665
+ * in the middle of executing a tool round can: the assistant turn was already
1666
+ * pushed, the tool_result carrier never was. Dropping that dangling assistant
1667
+ * turn costs one model response and makes the rest of the (often very long)
1668
+ * history resumable, instead of throwing all of it away.
1669
+ */
1670
+ function trimToResumableBoundary(messages) {
1671
+ // A trailing assistant turn is unresumable if it invoked ANY tool whose result is
1672
+ // missing. That includes server-side invocations (`server_tool_use`, produced by
1673
+ // web search / tool search), which carry a DIFFERENT block type than a client tool
1674
+ // call: matching only 'tool_use' would let a dangling web-search turn through, and
1675
+ // it would 400 on resend exactly like a dangling client call. `chooseFinalContent`
1676
+ // in api/client.ts deliberately preserves these blocks verbatim, so they really do
1677
+ // reach this function.
1678
+ const isUnansweredInvocation = (m) => {
1679
+ if (m?.role !== 'assistant' || !Array.isArray(m.content))
1680
+ return false;
1681
+ const blocks = m.content;
1682
+ // A client tool_use is answered by a tool_result in the NEXT (user) message, which
1683
+ // by definition doesn't exist if this is the last turn — so any of them is dangling.
1684
+ if (blocks.some((b) => b?.type === 'tool_use'))
1685
+ return true;
1686
+ // Server-side invocations are different: Anthropic pairs each `server_tool_use` with
1687
+ // its `*_tool_result` inside this SAME message, so a completed search turn is
1688
+ // perfectly resumable and must not be discarded.
1689
+ //
1690
+ // Pair by id rather than asking "is there any result block at all". A turn cut off
1691
+ // after two searches where only the first returned would otherwise look complete and
1692
+ // 400 on resend — exactly the mid-turn-death case this helper exists for. Matching on
1693
+ // the `_tool_result` suffix also covers result types beyond web/tool search
1694
+ // (code_execution, mcp) without needing to enumerate them.
1695
+ const answeredIds = new Set(blocks.filter((b) => /_tool_result$/.test(b?.type ?? '')).map((b) => b?.tool_use_id));
1696
+ return blocks.some((b) => b?.type === 'server_tool_use' && !answeredIds.has(b?.id));
1697
+ };
1698
+ // Loop rather than a single check: this is an exported helper that accepts any
1699
+ // history, not only the strictly-paired ones the agent loop builds, so consecutive
1700
+ // dangling assistant turns must all be removed for the result to be resendable.
1701
+ let end = messages.length;
1702
+ while (end > 0 && isUnansweredInvocation(messages[end - 1]))
1703
+ end--;
1704
+ return end === messages.length ? messages : messages.slice(0, end);
1705
+ }
1572
1706
  //# sourceMappingURL=loop.js.map
@@ -1,5 +1,5 @@
1
1
  import type { Message, SSEEvent, AuthConfig, EnvContext, EditorContext } from '../types';
2
- export declare const API_BASE = "https://api.nexrall.com";
2
+ export declare const API_BASE: string;
3
3
  /**
4
4
  * Decide the final assistant `content` array to store in history for a completed
5
5
  * turn. Pure + exported so it can be unit-tested without a live SSE stream.
@@ -44,8 +44,38 @@ export interface StreamChatOptions {
44
44
  agents?: string;
45
45
  /** Catalogue of auto-invokable skills (description only) — injected into the system prompt so the model can call use_skill when relevant. */
46
46
  skills?: string;
47
+ /**
48
+ * Opt in to restarting a turn that died AFTER partial output was rendered.
49
+ *
50
+ * Historically any transport failure past the first token was fatal: retrying
51
+ * would have re-streamed text the user could already see, duplicating it. That
52
+ * rule is correct but it protects almost nothing in practice — a long turn emits
53
+ * a thinking delta within seconds, so the retry machinery was effectively dead
54
+ * code exactly when it mattered most (deep into an expensive agentic run).
55
+ *
56
+ * When the caller can UNDO its rendered output for the current turn, it sets
57
+ * this and handles the `stream_restart` event by discarding that output. Then a
58
+ * mid-render disconnect becomes recoverable instead of turn-fatal. This is safe
59
+ * for side effects: tool calls are dispatched by the agent loop only after
60
+ * `message_complete`, and a restart by definition means that never arrived — so
61
+ * no tool ran and nothing but rendered characters is rolled back.
62
+ */
63
+ allowRestartAfterRender?: boolean;
47
64
  }
48
65
  export declare function streamChat(messages: Message[], options: StreamChatOptions, onEvent: (e: SSEEvent) => void): Promise<Message>;
66
+ /**
67
+ * Ask the server to stop generating a turn.
68
+ *
69
+ * This is REQUIRED for Stop to work, not an optimisation. A dropped socket used to abort
70
+ * the model; now it detaches and keeps generating so a network blip doesn't discard the
71
+ * turn (see the resume machinery above). Since a deliberate Stop looks identical to a
72
+ * blip from the server's side, it has to be signalled explicitly — otherwise the model
73
+ * would run to completion and bill in full after the user pressed Stop.
74
+ *
75
+ * Best-effort and never throws: Stop must feel instant, and the server's detach grace
76
+ * bounds the cost if this request never lands.
77
+ */
78
+ export declare function cancelTurn(turnId: string): Promise<void>;
49
79
  export declare function getBalance(): Promise<number>;
50
80
  export declare function exchangeVscodeCode(code: string): Promise<AuthConfig>;
51
81
  export declare function login(email: string, password: string): Promise<AuthConfig>;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrH,eAAO,MAAM,QAAQ,4BAA4B,CAAC;AAIlD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACtD,CAAC,EAAE,CAML;AAeD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6IAA6I;IAC7I,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA0BD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CA4clB;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAalD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAgB1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBhF"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAiErH,eAAO,MAAM,QAAQ,QAAmB,CAAC;AAIzC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5D,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GACtD,CAAC,EAAE,CAeL;AAeD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6IAA6I;IAC7I,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;;OAeG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAiDD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CA05BlB;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU9D;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAalD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAgB1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBhF"}