@cspeach/cli 1.1.51 → 1.1.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/chat-prompt.js +4 -1
- package/dist/agent/conversation-prompt.js +30 -13
- package/dist/agent/inline-skill-block.js +341 -0
- package/dist/agent/loop.js +208 -41
- package/dist/agent/provider-shape.js +8 -4
- package/dist/agent/providers/ai-hub-provider.js +2 -2
- package/dist/agent/providers/azure-provider.js +2 -2
- package/dist/agent/providers/byok-provider.js +2 -2
- package/dist/agent/providers/local-provider.js +2 -2
- package/dist/agent/repair-partial.js +12 -0
- package/dist/agent/skill-block-key.js +134 -0
- package/dist/agent/steering-queue.js +50 -14
- package/dist/agent/system-prompt.js +22 -0
- package/dist/agent/tool-dispatch.js +19 -7
- package/dist/agent/turn-watchdog.js +1 -1
- package/dist/approvals/advisory-render.js +6 -3
- package/dist/approvals/approval-prompt.js +9 -4
- package/dist/approvals/op-labels.js +14 -0
- package/dist/approvals/plan-grant.js +8 -0
- package/dist/approvals/render.js +19 -8
- package/dist/approvals/risk-floor.js +32 -4
- package/dist/approvals/visible-changes.js +50 -0
- package/dist/cli.js +10 -2
- package/dist/commands/compact.js +6 -1
- package/dist/commands/config-set.js +13 -0
- package/dist/commands/config-show.js +16 -4
- package/dist/commands/export-conversation.js +7 -4
- package/dist/commands/finish-check-guard.js +124 -0
- package/dist/commands/plan-reground.js +27 -4
- package/dist/commands/plan-resume.js +111 -1
- package/dist/config/loader.js +5 -1
- package/dist/cost/cost-log.js +6 -0
- package/dist/cost/system-block.js +6 -0
- package/dist/models/server-config.js +1 -1
- package/dist/projects/save-command.js +5 -1
- package/dist/projects/yes-no.js +5 -1
- package/dist/renderer/change-card.js +3 -1
- package/dist/renderer/steering-echo.js +53 -8
- package/dist/renderer/tool-labels.js +3 -1
- package/dist/repl/chat-lane.js +5 -2
- package/dist/repl/inline-mode.js +51 -0
- package/dist/repl/leftover.js +90 -0
- package/dist/repl/reroute-turn.js +7 -1
- package/dist/repl/turn-stop.js +64 -0
- package/dist/repl.js +144 -22
- package/dist/router/classifier.js +1 -1
- package/dist/sap/reachability-state.js +7 -0
- package/dist/sap/unreachable.js +2 -2
- package/dist/session/queued-step.js +3 -1
- package/dist/session/user-prompt.js +10 -1
- package/dist/skills/manifest-client.js +3 -2
- package/dist/skills/preamble.js +6 -4
- package/dist/skills/source-authed.js +3 -2
- package/dist/tools/approval.js +37 -7
- package/dist/tools/dispatch-skill.js +1 -1
- package/dist/tools/hand-off.js +49 -7
- package/dist/tools/lane/app-lane-resolve.js +9 -0
- package/dist/tools/method-only-change.js +241 -0
- package/dist/tools/rap/skeleton-resolve.js +4 -0
- package/dist/tools/sap-write.js +78 -3
- package/dist/tools/snapshot.js +7 -2
- package/dist/tools/subagent/agent_run.js +14 -0
- package/dist/tools/tool-loading.js +16 -8
- package/dist/tools/transport-fit-check.js +70 -0
- package/dist/tools/transport.js +17 -8
- package/dist/tools/write-pipeline.js +46 -5
- package/dist/ui/app.js +112 -13
- package/dist/ui/approval-modal.js +11 -6
- package/dist/ui/ascii-chars.js +21 -3
- package/dist/ui/footer.js +128 -7
- package/dist/ui/interrupt.js +35 -1
- package/dist/ui/live-area.js +21 -4
- package/dist/ui/queue-box-emitter.js +14 -0
- package/dist/ui/rewind-panel.js +2 -1
- package/dist/ui/session-timeline.js +2 -1
- package/dist/ui/text-input.js +31 -2
- package/dist/ui/widgets/bar-chart.js +2 -1
- package/dist/ui/widgets/confirm-card.js +2 -1
- package/dist/ui/widgets/dep-graph.js +2 -1
- package/dist/ui/widgets/diff-viewer.js +2 -1
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/shortcuts-sheet.js +2 -2
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/util/connect-retry.js +65 -0
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -33,7 +33,9 @@ import { turnStatusEmitter } from '../ui/turn-status-emitter.js';
|
|
|
33
33
|
import { buildRetryCapPausePayload, buildSkippedSiblingResults } from './retry-cap.js';
|
|
34
34
|
import { reportCallUsage } from '../usage/usage-reporter.js';
|
|
35
35
|
import { appendCostLine, buildEntry as buildCostEntry, perCallTokens, summariseInputTransformations } from '../cost/cost-log.js';
|
|
36
|
-
import { noteSystemBlock, systemBlockKey } from '../cost/system-block.js';
|
|
36
|
+
import { INLINE_SYSTEM_BLOCK_SKILL, noteSystemBlock, systemBlockKey } from '../cost/system-block.js';
|
|
37
|
+
import { escapeInjectedTags, historyHoldsInline, neutraliseForgedSkillTags, planInlineTurn, playbookLineSystem, playbookLineSystemChat, } from './inline-skill-block.js';
|
|
38
|
+
import { SKILL_INLINE_HEADER } from './system-prompt.js';
|
|
37
39
|
import { sessionMaxTokens, clampMaxTokens, sendsBlockBinding } from './provider-shape.js';
|
|
38
40
|
import { profileOf, sendsEffortFor } from './connection-profile.js';
|
|
39
41
|
import { computeSessionCost } from '../cost/session-cost.js';
|
|
@@ -69,7 +71,8 @@ import { renderStandardsBlock } from '../standards/standards-file.js';
|
|
|
69
71
|
import { composeSessionContextBlock } from './session-context.js';
|
|
70
72
|
import { objectKeyFromInput } from './retry-key.js';
|
|
71
73
|
import { repairPartialBlocks, stripPreFallbackBlocks, healSessionMessagesInPlace, healOrphanToolUses, isPartialJsonApiError, stripUnsignedThinking, isThinkingOnly, dropTrailingThinkingOnly } from './repair-partial.js';
|
|
72
|
-
import {
|
|
74
|
+
import { drainSteeringEntries, enqueueSteering } from './steering-queue.js';
|
|
75
|
+
import { steeringReadLine } from '../renderer/steering-echo.js';
|
|
73
76
|
import { TurnStreamWriter } from './turn-stream.js';
|
|
74
77
|
import { applyToolResultCheckpoint } from './skill-checkpoint.js';
|
|
75
78
|
import { loadWatchdogConfig, evaluateWatchdog } from './turn-watchdog.js';
|
|
@@ -84,6 +87,7 @@ import { CacheKeepalive, snapshotParams, recordLastRequest, isCacheablePrompt, n
|
|
|
84
87
|
import { maybeColdPrune } from './cold-prune.js';
|
|
85
88
|
import { createAdtLock, serializeAdt } from '../tools/subagent/adt-serial.js';
|
|
86
89
|
import { hasReaderReadTools, INTERRUPTED_LINE } from '../tools/subagent/reader-prompt.js';
|
|
90
|
+
import { settleHandOffNote } from '../tools/hand-off.js';
|
|
87
91
|
import { openStatusPage } from '../util/open-in-browser.js';
|
|
88
92
|
import { isChatSkill } from './chat-prompt.js';
|
|
89
93
|
import { getAssistantStyle } from './style-state.js';
|
|
@@ -135,6 +139,12 @@ const SAVE_HOOK_SKILLS = new Set([
|
|
|
135
139
|
// revision path in commands/plan-resume.ts persists instead.
|
|
136
140
|
'abap-plan',
|
|
137
141
|
]);
|
|
142
|
+
/** Stage 1A — the inline branch's debug channel (CSPEACH_DEBUG=1 → stderr). Never user output. */
|
|
143
|
+
function inlineDebug(line) {
|
|
144
|
+
if (process.env['CSPEACH_DEBUG'] !== '1')
|
|
145
|
+
return;
|
|
146
|
+
process.stderr.write(`[inline] ${line}\n`);
|
|
147
|
+
}
|
|
138
148
|
/**
|
|
139
149
|
* True when any user message in the session already carries the rendered
|
|
140
150
|
* <project_context> block. Phase 2 uses this to inject the block exactly
|
|
@@ -614,6 +624,7 @@ const SILENT_STATUS = {
|
|
|
614
624
|
activity: (_label) => undefined,
|
|
615
625
|
pause: () => undefined,
|
|
616
626
|
resume: () => undefined,
|
|
627
|
+
pausedMs: () => 0,
|
|
617
628
|
};
|
|
618
629
|
/** Task 18 — at most this many read_agent calls run at the same time. */
|
|
619
630
|
export const MAX_PARALLEL_READERS = 4;
|
|
@@ -946,6 +957,15 @@ ${imageExpansion.error}`));
|
|
|
946
957
|
if (!readerChild) {
|
|
947
958
|
userMessageForLLM = await expandTextFileAttachments(userMessageForLLM, params.ctx.cwd, (line) => emit(chalk.dim(line)));
|
|
948
959
|
}
|
|
960
|
+
// Stage 1A, Task 1A.2 (S5) — an interactive chat-lane process neutralises
|
|
961
|
+
// <skill, <playbook and <session_context in what the person sends (typed
|
|
962
|
+
// text and @file expansions), so no text of theirs can open a block the
|
|
963
|
+
// CLI writes. A caller-enriched message keeps its leading <session_context>.
|
|
964
|
+
// Absent flag (skills lane, piped, one-shot, children) ⇒ the text as it is.
|
|
965
|
+
if (params.ctx.chatLaneProcess === true && !readerChild && !params.ctx.subagent) {
|
|
966
|
+
const lead = userMessageForLLM.startsWith('<session_context>') ? '<session_context>' : '';
|
|
967
|
+
userMessageForLLM = lead + escapeInjectedTags(userMessageForLLM.slice(lead.length));
|
|
968
|
+
}
|
|
949
969
|
// App lane routing (Task 6) — the /abap-plan creation turn inside an app
|
|
950
970
|
// flow gets the confirmed <lane_decision> block (read with the app-flow sha;
|
|
951
971
|
// a changed file is refused with a plain line). '' on every other turn, so
|
|
@@ -1012,7 +1032,43 @@ ${imageExpansion.error}`));
|
|
|
1012
1032
|
const switchNote = session.systemSwitchNote
|
|
1013
1033
|
? `<system_switch>${session.systemSwitchNote}</system_switch>`
|
|
1014
1034
|
: '';
|
|
1015
|
-
|
|
1035
|
+
// Stage 1A, Task 1A.2 — inline skills (only when the REPL decided so for
|
|
1036
|
+
// this turn; never a reader or agent_run child). The playbook rides in the
|
|
1037
|
+
// conversation as a <skill> block, once per body per history ("loaded" is
|
|
1038
|
+
// read from the history by content hash, inline-skill-block.ts), and a
|
|
1039
|
+
// code-driven <playbook> line says which block is in charge. The main
|
|
1040
|
+
// playbook failing to load ⇒ this turn runs in system mode (S2). A
|
|
1041
|
+
// system-mode turn whose history holds inline blocks gets the
|
|
1042
|
+
// source="system" line (S-B); in a process that never ran inline the scan
|
|
1043
|
+
// finds nothing and nothing changes.
|
|
1044
|
+
const mainTurn = !readerChild && !params.ctx.subagent;
|
|
1045
|
+
let inlineTurn = false;
|
|
1046
|
+
let playbookLine = '';
|
|
1047
|
+
let inlineBlocks = [];
|
|
1048
|
+
if (mainTurn && params.ctx.inlineSkills === true) {
|
|
1049
|
+
const neutralised = neutraliseForgedSkillTags(session.messages);
|
|
1050
|
+
if (neutralised > 0)
|
|
1051
|
+
inlineDebug(`neutralised a <skill> tag that is not a loaded playbook in ${neutralised} earlier message(s)`);
|
|
1052
|
+
const plan = await planInlineTurn({
|
|
1053
|
+
skill: params.skill,
|
|
1054
|
+
delegateSkill: params.delegateSkill,
|
|
1055
|
+
messages: session.messages,
|
|
1056
|
+
source: params.ctx.skillSource ?? params.provider.skillSource,
|
|
1057
|
+
debug: inlineDebug,
|
|
1058
|
+
});
|
|
1059
|
+
if (plan.inline) {
|
|
1060
|
+
inlineTurn = true;
|
|
1061
|
+
playbookLine = plan.line;
|
|
1062
|
+
inlineBlocks = plan.blocks;
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
inlineDebug(`playbook ${params.skill} could not be loaded, this turn runs in system mode: ${plan.reason}`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (mainTurn && !inlineTurn && historyHoldsInline(session.messages)) {
|
|
1069
|
+
playbookLine = isChatSkill(params.skill) ? playbookLineSystemChat() : playbookLineSystem(params.skill);
|
|
1070
|
+
}
|
|
1071
|
+
const sessionContextBlock = composeSessionContextBlock([sapSystemBlock, sapProfileBlock, standardsBlock, switchNote, playbookLine]);
|
|
1016
1072
|
// Deterministically extract structured facts from the user's prompt
|
|
1017
1073
|
// (package, transport, object names, environment hint, create intent)
|
|
1018
1074
|
// and prepend them as an XML `<session_context>` block. The model reads
|
|
@@ -1027,8 +1083,10 @@ ${imageExpansion.error}`));
|
|
|
1027
1083
|
// when extraction yields nothing (enrichUserMessage returns original).
|
|
1028
1084
|
// Final batch 1 — a caller-enriched message still gets the switch note,
|
|
1029
1085
|
// inside its own <session_context> block.
|
|
1086
|
+
// Stage 1A — the <playbook> line rides the same way ('' ⇒ unchanged).
|
|
1087
|
+
const callerNotes = composeSessionContextBlock([switchNote, playbookLine]);
|
|
1030
1088
|
const enriched = userMessageForLLM.startsWith('<session_context>')
|
|
1031
|
-
? (
|
|
1089
|
+
? (callerNotes ? userMessageForLLM.replace('<session_context>', `<session_context>\n ${callerNotes}`) : userMessageForLLM)
|
|
1032
1090
|
: enrichUserMessage(userMessageForLLM, sessionContextBlock);
|
|
1033
1091
|
const switchNoteUsed = switchNote !== '' && enriched.includes(switchNote);
|
|
1034
1092
|
// Phase 2 of Track B — opt-in project-context enrichment, gated by
|
|
@@ -1060,9 +1118,14 @@ ${imageExpansion.error}`));
|
|
|
1060
1118
|
sapConnection,
|
|
1061
1119
|
});
|
|
1062
1120
|
}
|
|
1063
|
-
const
|
|
1121
|
+
const contextualContent = projectContextBlock
|
|
1064
1122
|
? `${projectContextBlock}\n\n${enriched}`
|
|
1065
1123
|
: enriched;
|
|
1124
|
+
// Stage 1A — the bodies the history lacks end the user message, each as
|
|
1125
|
+
// `\n\n<skill …>\n<body>\n</skill>` (none on most turns ⇒ unchanged).
|
|
1126
|
+
const finalUserContent = inlineBlocks.length > 0
|
|
1127
|
+
? `${contextualContent}${inlineBlocks.map((b) => `\n\n${b.text}`).join('')}`
|
|
1128
|
+
: contextualContent;
|
|
1066
1129
|
// Chat lane (D11) — what a withdrawn `_chat` turn restores: the whole
|
|
1067
1130
|
// pre-turn message array (placeSystemMessagesInPlace may re-sort the tail
|
|
1068
1131
|
// after the last assistant message, so a length-based splice is not
|
|
@@ -1184,6 +1247,13 @@ ${imageExpansion.error}`));
|
|
|
1184
1247
|
* N1 (c) — built from the turn's list (the list every request of this turn
|
|
1185
1248
|
* carries) and never for a session pinned static.
|
|
1186
1249
|
*/
|
|
1250
|
+
// Stage 1A (rev 6 M-5) — on an inline turn, what each tool_addition
|
|
1251
|
+
// surfaced and the size of its definition in this turn's list, for the
|
|
1252
|
+
// cost line of the call that carries it. Never filled in system mode.
|
|
1253
|
+
let pendingSurfaced = [];
|
|
1254
|
+
// Stage 1A — skill_appended goes on the turn's first cost line (a call
|
|
1255
|
+
// retried after a prompt-too-long recovery logs no line of its own).
|
|
1256
|
+
let skillAppendedLogged = false;
|
|
1187
1257
|
const surfaceStarters = (skill) => {
|
|
1188
1258
|
const msg = buildSkillToolAddition({
|
|
1189
1259
|
skill,
|
|
@@ -1193,6 +1263,12 @@ ${imageExpansion.error}`));
|
|
|
1193
1263
|
});
|
|
1194
1264
|
if (msg)
|
|
1195
1265
|
session.messages.push(msg);
|
|
1266
|
+
if (msg && inlineTurn) {
|
|
1267
|
+
for (const b of msg.content) {
|
|
1268
|
+
const def = turnTools.find((t) => t?.name === b.tool.name);
|
|
1269
|
+
pendingSurfaced.push({ name: b.tool.name, chars: JSON.stringify(def ?? null).length });
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1196
1272
|
};
|
|
1197
1273
|
// A turn on a different skill than the one the array was built for: the
|
|
1198
1274
|
// user message was just pushed, so the system message follows it.
|
|
@@ -1254,7 +1330,34 @@ ${imageExpansion.error}`));
|
|
|
1254
1330
|
// rounds so it doesn't spam the model. See agent/turn-watchdog.ts.
|
|
1255
1331
|
const watchdogConfig = loadWatchdogConfig();
|
|
1256
1332
|
const turnStartedAt = Date.now();
|
|
1333
|
+
// Card-wait time already paused on the status emitter before this turn; the
|
|
1334
|
+
// watchdog counts only what this turn adds on top of it as "not working".
|
|
1335
|
+
const turnPausedBase = status.pausedMs();
|
|
1257
1336
|
let watchdogWarned = false;
|
|
1337
|
+
// LT-3 (live re-check 2026-10-26) — set after a successful hand_off in a
|
|
1338
|
+
// turn that already said its line. The next round's text is HELD: shown at
|
|
1339
|
+
// once if the round calls a tool, shown at the end unless the round is only
|
|
1340
|
+
// a short closing line that ends the turn (the second announcement). Kept
|
|
1341
|
+
// in the session either way.
|
|
1342
|
+
let muteHandOffEcho = false;
|
|
1343
|
+
let heldEcho = null;
|
|
1344
|
+
const showText = (out) => {
|
|
1345
|
+
if (heldEcho) {
|
|
1346
|
+
heldEcho.push(out);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (params.chunkEmitter)
|
|
1350
|
+
params.chunkEmitter.emit('chunk', out);
|
|
1351
|
+
else
|
|
1352
|
+
writeStdout(out);
|
|
1353
|
+
};
|
|
1354
|
+
const releaseHeldEcho = () => {
|
|
1355
|
+
const held = heldEcho;
|
|
1356
|
+
heldEcho = null;
|
|
1357
|
+
if (held)
|
|
1358
|
+
for (const out of held)
|
|
1359
|
+
showText(out);
|
|
1360
|
+
};
|
|
1258
1361
|
// 2026-06-08 — manifest-on-interrupt: track the last plan-manifest block we
|
|
1259
1362
|
// already handed to onPlanManifest, so we fire once per DISTINCT manifest
|
|
1260
1363
|
// (a continue-in-session run emits one per phase) and not once per round.
|
|
@@ -1288,15 +1391,28 @@ ${imageExpansion.error}`));
|
|
|
1288
1391
|
// during round N lands at the start of round N+1 (between rounds). The
|
|
1289
1392
|
// Anthropic API concatenates consecutive user messages, so injecting after a
|
|
1290
1393
|
// tool_result user message is valid.
|
|
1394
|
+
// LT-15 F1 — only the main turn reads the queue: a reader or an agent_run
|
|
1395
|
+
// child (ctx.subagent) never takes the parent's messages.
|
|
1396
|
+
// LT-15 F2 — no drain when the turn is already stopped: this round can
|
|
1397
|
+
// never run, so the message stays queued for a later turn.
|
|
1291
1398
|
{
|
|
1292
|
-
const
|
|
1293
|
-
if (
|
|
1294
|
-
const
|
|
1399
|
+
const entries = mainTurn && !params.signal?.aborted ? drainSteeringEntries() : [];
|
|
1400
|
+
if (entries.length > 0) {
|
|
1401
|
+
const steers = entries.map((e) => e.text);
|
|
1402
|
+
// Stage 1A fix round 1 (I-1) — a steer is typed text: in an
|
|
1403
|
+
// interactive chat-lane process it gets the same tag escape.
|
|
1404
|
+
const joined = steers.join('\n');
|
|
1405
|
+
const text = params.ctx.chatLaneProcess === true ? escapeInjectedTags(joined) : joined;
|
|
1295
1406
|
session.messages.push({ role: 'user', content: text });
|
|
1296
1407
|
markCliMessage(session, text, 'steer', '');
|
|
1297
1408
|
if (roundIndex === 0)
|
|
1298
1409
|
round0Steers = steers;
|
|
1299
|
-
|
|
1410
|
+
// LT-15 Feature 1 (D5, F7) — one dim, clipped, sanitised line per message.
|
|
1411
|
+
// LT-15 Task 4 — not again for a message the D11 fallback put back
|
|
1412
|
+
// after its line was already shown.
|
|
1413
|
+
for (const e of entries)
|
|
1414
|
+
if (!e.alreadyRead)
|
|
1415
|
+
emit(steeringReadLine(e.text));
|
|
1300
1416
|
}
|
|
1301
1417
|
}
|
|
1302
1418
|
let buffer = initialBufferState();
|
|
@@ -1332,10 +1448,14 @@ ${imageExpansion.error}`));
|
|
|
1332
1448
|
session.turn_seq = preTurnState.turn_seq;
|
|
1333
1449
|
// Steering drained in round 0 goes back to the queue, ahead of any
|
|
1334
1450
|
// typed since, so the re-run turn still sees it.
|
|
1451
|
+
// LT-15 Task 4 — the round-0 ones were shown as "↳ read": marked, so the
|
|
1452
|
+
// re-run prints no second line; the later ones keep their own mark.
|
|
1335
1453
|
if (round0Steers.length > 0) {
|
|
1336
|
-
const later =
|
|
1337
|
-
for (const t of
|
|
1338
|
-
enqueueSteering(t);
|
|
1454
|
+
const later = drainSteeringEntries();
|
|
1455
|
+
for (const t of round0Steers)
|
|
1456
|
+
enqueueSteering(t, { alreadyRead: true });
|
|
1457
|
+
for (const e of later)
|
|
1458
|
+
enqueueSteering(e.text, { alreadyRead: e.alreadyRead });
|
|
1339
1459
|
}
|
|
1340
1460
|
throw new ChatUnavailableError(st);
|
|
1341
1461
|
};
|
|
@@ -1371,6 +1491,9 @@ ${imageExpansion.error}`));
|
|
|
1371
1491
|
// Task 15 — the request actually sent by the attempt that succeeded.
|
|
1372
1492
|
let sentParams;
|
|
1373
1493
|
let sentHeaders;
|
|
1494
|
+
// Stage 1A — the tool_additions this call carries (inline turns only).
|
|
1495
|
+
const callSurfaced = pendingSurfaced;
|
|
1496
|
+
pendingSurfaced = [];
|
|
1374
1497
|
try {
|
|
1375
1498
|
stream = await retryWithBackoff(() => (async () => {
|
|
1376
1499
|
// W2.5 — apply optional tool filter (used by agent_run with
|
|
@@ -1494,6 +1617,9 @@ ${imageExpansion.error}`));
|
|
|
1494
1617
|
...styleHeader(getAssistantStyle()),
|
|
1495
1618
|
// §10 D14 — developer sends nothing (byte-identical requests).
|
|
1496
1619
|
...levelHeader(getAssistantLevel()),
|
|
1620
|
+
// Stage 1A — an inline turn asks for the one constant system
|
|
1621
|
+
// block; every other call sends nothing (byte-identical requests).
|
|
1622
|
+
...(inlineTurn ? { [SKILL_INLINE_HEADER]: '1' } : {}),
|
|
1497
1623
|
};
|
|
1498
1624
|
sentParams = streamParams;
|
|
1499
1625
|
sentHeaders = headers;
|
|
@@ -1547,8 +1673,11 @@ ${imageExpansion.error}`));
|
|
|
1547
1673
|
// largest tool results, save, and run the same request once more (or
|
|
1548
1674
|
// show the card). The main turn only.
|
|
1549
1675
|
if (!readerChild && !params.ctx.subagent && isPromptTooLongError(err)) {
|
|
1550
|
-
if (await recoverFromPromptTooLong(err))
|
|
1676
|
+
if (await recoverFromPromptTooLong(err)) {
|
|
1677
|
+
// Stage 1A — the retried call carries the same tool_additions.
|
|
1678
|
+
pendingSurfaced = [...callSurfaced, ...pendingSurfaced];
|
|
1551
1679
|
continue;
|
|
1680
|
+
}
|
|
1552
1681
|
return;
|
|
1553
1682
|
}
|
|
1554
1683
|
// Proxy returns 409 upgrade_required when CLI is older than skill's min_cli_version.
|
|
@@ -1584,6 +1713,10 @@ ${imageExpansion.error}`));
|
|
|
1584
1713
|
// not in here was cut mid-stream and must never be persisted.
|
|
1585
1714
|
const completedBlocks = new WeakSet();
|
|
1586
1715
|
let sawEndTurn = false;
|
|
1716
|
+
if (muteHandOffEcho) {
|
|
1717
|
+
heldEcho = [];
|
|
1718
|
+
muteHandOffEcho = false;
|
|
1719
|
+
} // LT-3 — hold this round's text
|
|
1587
1720
|
let sawToolUse = false;
|
|
1588
1721
|
let sawMaxTokens = false;
|
|
1589
1722
|
let sawPauseTurn = false;
|
|
@@ -1642,13 +1775,10 @@ ${imageExpansion.error}`));
|
|
|
1642
1775
|
writingCode = false;
|
|
1643
1776
|
const block = event.content_block;
|
|
1644
1777
|
currentAssistantContent.push(block);
|
|
1645
|
-
if (block.type === '
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
else
|
|
1650
|
-
writeStdout(sep);
|
|
1651
|
-
}
|
|
1778
|
+
if (block.type === 'tool_use')
|
|
1779
|
+
releaseHeldEcho(); // LT-3 — the round does more than restate
|
|
1780
|
+
if (block.type === 'text')
|
|
1781
|
+
showText(chalk.gray('\n'));
|
|
1652
1782
|
}
|
|
1653
1783
|
else if (type === 'content_block_delta') {
|
|
1654
1784
|
const delta = event.delta;
|
|
@@ -1664,12 +1794,8 @@ ${imageExpansion.error}`));
|
|
|
1664
1794
|
writingCode = wa.writingCode;
|
|
1665
1795
|
if (wa.label)
|
|
1666
1796
|
status.activity(wa.label);
|
|
1667
|
-
if (output)
|
|
1668
|
-
|
|
1669
|
-
params.chunkEmitter.emit('chunk', output);
|
|
1670
|
-
else
|
|
1671
|
-
writeStdout(output);
|
|
1672
|
-
}
|
|
1797
|
+
if (output)
|
|
1798
|
+
showText(output);
|
|
1673
1799
|
if (last?.type === 'text')
|
|
1674
1800
|
last.text = (last.text ?? '') + delta.text;
|
|
1675
1801
|
// v0.6 — also write the raw model text to disk so the user can
|
|
@@ -1807,8 +1933,11 @@ ${imageExpansion.error}`));
|
|
|
1807
1933
|
// retry: the catch only decides; the card and the final save (with
|
|
1808
1934
|
// turnInterrupted cleared) come from the recovery after the persist block.
|
|
1809
1935
|
if (!readerChild && !params.ctx.subagent && !params.signal?.aborted && currentAssistantContent.length === 0 && isPromptTooLongError(err)) {
|
|
1810
|
-
if (await recoverFromPromptTooLong(err, { card: false }))
|
|
1936
|
+
if (await recoverFromPromptTooLong(err, { card: false })) {
|
|
1937
|
+
// Stage 1A — the retried call carries the same tool_additions.
|
|
1938
|
+
pendingSurfaced = [...callSurfaced, ...pendingSurfaced];
|
|
1811
1939
|
continue;
|
|
1940
|
+
}
|
|
1812
1941
|
}
|
|
1813
1942
|
}
|
|
1814
1943
|
// Final flush: emit any pending partial line after the stream ends.
|
|
@@ -1820,22 +1949,23 @@ ${imageExpansion.error}`));
|
|
|
1820
1949
|
if (heldProse) {
|
|
1821
1950
|
const { output, state } = renderChunk(heldProse, buffer);
|
|
1822
1951
|
buffer = state;
|
|
1823
|
-
if (output)
|
|
1824
|
-
|
|
1825
|
-
params.chunkEmitter.emit('chunk', output);
|
|
1826
|
-
else
|
|
1827
|
-
writeStdout(output);
|
|
1828
|
-
}
|
|
1952
|
+
if (output)
|
|
1953
|
+
showText(output);
|
|
1829
1954
|
}
|
|
1830
1955
|
if (buffer.pending.length > 0) {
|
|
1831
|
-
|
|
1832
|
-
if (params.chunkEmitter)
|
|
1833
|
-
params.chunkEmitter.emit('chunk', flushed);
|
|
1834
|
-
else
|
|
1835
|
-
writeStdout(flushed);
|
|
1956
|
+
showText(render(buffer.pending));
|
|
1836
1957
|
buffer = initialBufferState();
|
|
1837
1958
|
writingCode = false;
|
|
1838
1959
|
}
|
|
1960
|
+
if (heldEcho) {
|
|
1961
|
+
// LT-3 — drop only a short text-only reply that ends the turn.
|
|
1962
|
+
const said = currentAssistantContent.filter((b) => b?.type === 'text').map((b) => String(b.text ?? '')).join('').trim();
|
|
1963
|
+
const restatement = sawEndTurn && !currentAssistantContent.some((b) => b?.type === 'tool_use') && said.length <= HAND_OFF_ECHO_MAX;
|
|
1964
|
+
if (restatement)
|
|
1965
|
+
heldEcho = null;
|
|
1966
|
+
else
|
|
1967
|
+
releaseHeldEcho();
|
|
1968
|
+
}
|
|
1839
1969
|
// Repair partial blocks before persistence. ALWAYS run, not just on
|
|
1840
1970
|
// interruption: on a clean tool-use round the model's tool_use blocks can
|
|
1841
1971
|
// still carry a leftover `partial_json` field, and persisting that poisons
|
|
@@ -1960,10 +2090,13 @@ ${imageExpansion.error}`));
|
|
|
1960
2090
|
// this call sent and whether its system block matched the session's
|
|
1961
2091
|
// previous main call (same four inputs as the headers above).
|
|
1962
2092
|
const mainLine = !readerChild;
|
|
2093
|
+
// Stage 1A (M2) — an inline call's system block is the one constant
|
|
2094
|
+
// block whatever the skill or delegate: the key says so, so a switch is
|
|
2095
|
+
// 'same' and a 'changed' line in an inline session is a real change.
|
|
1963
2096
|
const systemBlock = mainLine
|
|
1964
2097
|
? noteSystemBlock(session, systemBlockKey({
|
|
1965
|
-
skill: params.skill,
|
|
1966
|
-
delegateSkill: params.delegateSkill,
|
|
2098
|
+
skill: inlineTurn ? INLINE_SYSTEM_BLOCK_SKILL : params.skill,
|
|
2099
|
+
delegateSkill: inlineTurn ? undefined : params.delegateSkill,
|
|
1967
2100
|
style: getAssistantStyle(),
|
|
1968
2101
|
level: getAssistantLevel(),
|
|
1969
2102
|
}))
|
|
@@ -1996,7 +2129,15 @@ ${imageExpansion.error}`));
|
|
|
1996
2129
|
reader: params.ctx.readerSpend,
|
|
1997
2130
|
skill: mainLine ? params.skill : undefined,
|
|
1998
2131
|
system_block: systemBlock,
|
|
2132
|
+
// Stage 1A (S8, S-C, M-5) — inline lines only; absent otherwise.
|
|
2133
|
+
system_mode: mainLine && inlineTurn ? 'inline' : undefined,
|
|
2134
|
+
skill_appended: mainLine && inlineTurn && !skillAppendedLogged
|
|
2135
|
+
? inlineBlocks.map((b) => ({ name: b.name, sha256: b.sha256, chars: b.text.length }))
|
|
2136
|
+
: undefined,
|
|
2137
|
+
tools_surfaced: mainLine && inlineTurn ? callSurfaced : undefined,
|
|
1999
2138
|
});
|
|
2139
|
+
if (mainLine)
|
|
2140
|
+
skillAppendedLogged = true;
|
|
2000
2141
|
readerCallsLogged = params.ctx.readerSpend?.calls ?? 0;
|
|
2001
2142
|
void appendCostLine(session.id, entry);
|
|
2002
2143
|
// Security batch item 3 — usage counts for byok / ai-hub / local (the
|
|
@@ -2084,6 +2225,7 @@ ${imageExpansion.error}`));
|
|
|
2084
2225
|
const outputTokensThisTurn = Math.max(0, (session.usage?.output_tokens ?? 0) - turnStartOutputTokens);
|
|
2085
2226
|
const decision = evaluateWatchdog({
|
|
2086
2227
|
startedAt: turnStartedAt,
|
|
2228
|
+
pausedMs: status.pausedMs() - turnPausedBase,
|
|
2087
2229
|
outputTokensThisTurn,
|
|
2088
2230
|
config: watchdogConfig,
|
|
2089
2231
|
alreadyWarned: false,
|
|
@@ -2617,10 +2759,17 @@ ${imageExpansion.error}`));
|
|
|
2617
2759
|
// immediately below handles both success and the not-yet-capped failure case.
|
|
2618
2760
|
// 2026-10-22 — the general guard: a result over TOOL_RESULT_MAX_CHARS
|
|
2619
2761
|
// is cut with a note; a result under it is the same value (byte-identical).
|
|
2762
|
+
// LT-3 (live check 2026-10-26) — text already went out in this turn:
|
|
2763
|
+
// the hand_off result tells the model plainly to add nothing more.
|
|
2764
|
+
const saidBefore = block.name === 'hand_off' && !result.is_error
|
|
2765
|
+
&& textBeforeInTurn(session.messages.slice(turnStartMessageCount), currentAssistantContent, block);
|
|
2766
|
+
const resultContent = saidBefore ? settleHandOffNote(result.content, true) : result.content;
|
|
2767
|
+
if (saidBefore && resultContent !== result.content)
|
|
2768
|
+
muteHandOffEcho = true;
|
|
2620
2769
|
toolResults.push({
|
|
2621
2770
|
type: 'tool_result',
|
|
2622
2771
|
tool_use_id: block.id,
|
|
2623
|
-
content: capToolResultContent(
|
|
2772
|
+
content: capToolResultContent(resultContent),
|
|
2624
2773
|
is_error: result.is_error,
|
|
2625
2774
|
});
|
|
2626
2775
|
}
|
|
@@ -2640,6 +2789,7 @@ ${imageExpansion.error}`));
|
|
|
2640
2789
|
const outputTokensThisTurn = Math.max(0, (session.usage?.output_tokens ?? 0) - turnStartOutputTokens);
|
|
2641
2790
|
const decision = evaluateWatchdog({
|
|
2642
2791
|
startedAt: turnStartedAt,
|
|
2792
|
+
pausedMs: status.pausedMs() - turnPausedBase,
|
|
2643
2793
|
outputTokensThisTurn,
|
|
2644
2794
|
config: watchdogConfig,
|
|
2645
2795
|
alreadyWarned: watchdogWarned,
|
|
@@ -2674,3 +2824,20 @@ function dispatchedSkills(assistantContent, toolResults) {
|
|
|
2674
2824
|
}
|
|
2675
2825
|
return out;
|
|
2676
2826
|
}
|
|
2827
|
+
/**
|
|
2828
|
+
* LT-3 — did the model already write text in this turn before `block`? Earlier
|
|
2829
|
+
* rounds' assistant messages, then this round's blocks ahead of the call.
|
|
2830
|
+
*/
|
|
2831
|
+
function textBeforeInTurn(turnMessages, roundContent, block) {
|
|
2832
|
+
const said = (b) => b?.type === 'text' && typeof b.text === 'string' && b.text.trim() !== '';
|
|
2833
|
+
for (const m of turnMessages) {
|
|
2834
|
+
if (m?.role !== 'assistant' || m.content === roundContent || !Array.isArray(m.content))
|
|
2835
|
+
continue;
|
|
2836
|
+
if (m.content.some(said))
|
|
2837
|
+
return true;
|
|
2838
|
+
}
|
|
2839
|
+
const at = roundContent.indexOf(block);
|
|
2840
|
+
return roundContent.slice(0, at < 0 ? roundContent.length : at).some(said);
|
|
2841
|
+
}
|
|
2842
|
+
/** LT-3 — the longest text-only closing reply after a hand-off that is treated as a second announcement. */
|
|
2843
|
+
const HAND_OFF_ECHO_MAX = 320;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TOOL_CHANGES_BETA, isToolSearchEntry } from '../tools/tool-loading.js';
|
|
2
|
-
import { DELEGATE_SKILL_HEADER } from './system-prompt.js';
|
|
2
|
+
import { DELEGATE_SKILL_HEADER, SKILL_INLINE_HEADER } from './system-prompt.js';
|
|
3
3
|
import { STYLE_HEADER } from './style.js';
|
|
4
4
|
import { LEVEL_HEADER } from './level.js';
|
|
5
5
|
/** Betas the proxy sends on every managed turn (messages.ts `betas: [...]`). */
|
|
@@ -270,14 +270,14 @@ export function withoutInternalHeaders(headers) {
|
|
|
270
270
|
/**
|
|
271
271
|
* Task 9 — the role header is new proxy plumbing; AI-hub keeps today's header
|
|
272
272
|
* set, so X-CSPeach-Model-Role (and, 2026-10-04, the delegate header; and,
|
|
273
|
-
* 2026-10-17, the talking-style and level headers — applied locally
|
|
274
|
-
* removed.
|
|
273
|
+
* 2026-10-17, the talking-style and level headers — applied locally; and,
|
|
274
|
+
* Stage 1A, the inline header — applied locally) are removed.
|
|
275
275
|
*/
|
|
276
276
|
export function withoutRoleHeader(headers) {
|
|
277
277
|
const out = {};
|
|
278
278
|
for (const [k, v] of Object.entries(headers ?? {})) {
|
|
279
279
|
const lk = k.toLowerCase();
|
|
280
|
-
if (lk === 'x-cspeach-model-role' || lk === DELEGATE_SKILL_HEADER.toLowerCase() || lk === STYLE_HEADER.toLowerCase() || lk === LEVEL_HEADER.toLowerCase())
|
|
280
|
+
if (lk === 'x-cspeach-model-role' || lk === DELEGATE_SKILL_HEADER.toLowerCase() || lk === STYLE_HEADER.toLowerCase() || lk === LEVEL_HEADER.toLowerCase() || lk === SKILL_INLINE_HEADER.toLowerCase())
|
|
281
281
|
continue;
|
|
282
282
|
out[k] = v;
|
|
283
283
|
}
|
|
@@ -291,6 +291,10 @@ export function headersForProfile(profile, headers) {
|
|
|
291
291
|
case 'none': return { ...(headers ?? {}) };
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
|
+
/** Stage 1A — true when the loop sent X-CSPeach-Skill-Inline: '1' (a direct provider then builds the one block locally). */
|
|
295
|
+
export function skillInlineFromHeaders(options) {
|
|
296
|
+
return options.headers?.[SKILL_INLINE_HEADER] === '1';
|
|
297
|
+
}
|
|
294
298
|
/** 2026-10-04 — the plan phase's delegate skill, when the loop sent one. */
|
|
295
299
|
export function delegateSkillName(options) {
|
|
296
300
|
const v = options.headers?.[DELEGATE_SKILL_HEADER];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import keychain from '../../auth/keychain.js';
|
|
3
3
|
import { createAuthedSkillSource, defaultBearer } from '../../skills/source-authed.js';
|
|
4
|
-
import { delegateSkillName, requireSkillName, toLegacyShape, withoutRoleHeader } from '../provider-shape.js';
|
|
4
|
+
import { delegateSkillName, requireSkillName, skillInlineFromHeaders, toLegacyShape, withoutRoleHeader } from '../provider-shape.js';
|
|
5
5
|
import { systemPromptForSkill } from '../system-prompt.js';
|
|
6
6
|
import { styleFromHeaders } from '../style.js';
|
|
7
7
|
import { levelFromHeaders } from '../level.js';
|
|
@@ -28,7 +28,7 @@ export function createAiHubProvider(cfg, getBearer = defaultBearer) {
|
|
|
28
28
|
const skillName = requireSkillName(options, 'AI Hub');
|
|
29
29
|
const token = await getToken();
|
|
30
30
|
// Task 18 — the `_reader` header uses the pinned reader prompt, no lookup.
|
|
31
|
-
const system = await systemPromptForSkill(skillName, skillSource, { cacheMarkers: AI_HUB_PROFILE.cacheMarkers }, delegateSkillName(options), styleFromHeaders(options.headers), levelFromHeaders(options.headers));
|
|
31
|
+
const system = await systemPromptForSkill(skillName, skillSource, { cacheMarkers: AI_HUB_PROFILE.cacheMarkers, inline: skillInlineFromHeaders(options) }, delegateSkillName(options), styleFromHeaders(options.headers), levelFromHeaders(options.headers));
|
|
32
32
|
const request = {
|
|
33
33
|
...toLegacyShape(params),
|
|
34
34
|
system,
|
|
@@ -15,7 +15,7 @@ import Anthropic from '@anthropic-ai/sdk';
|
|
|
15
15
|
import { sendsEffortFor } from '../connection-profile.js';
|
|
16
16
|
import { AZURE_PROFILE } from '../azure-profile.js';
|
|
17
17
|
import { createAuthedSkillSource, defaultBearer } from '../../skills/source-authed.js';
|
|
18
|
-
import { delegateSkillName, headersForProfile, requireSkillName, withoutToolChanges, withSkillPrompt } from '../provider-shape.js';
|
|
18
|
+
import { delegateSkillName, headersForProfile, requireSkillName, skillInlineFromHeaders, withoutToolChanges, withSkillPrompt } from '../provider-shape.js';
|
|
19
19
|
import { systemPromptForSkill } from '../system-prompt.js';
|
|
20
20
|
import { styleFromHeaders } from '../style.js';
|
|
21
21
|
import { levelFromHeaders } from '../level.js';
|
|
@@ -66,7 +66,7 @@ export function createAzureProvider(cfg, getBearer = defaultBearer, deps = {}) {
|
|
|
66
66
|
throw new AzureConnectionRefusedError(v.message);
|
|
67
67
|
const conn = cached.connection;
|
|
68
68
|
const skillName = requireSkillName(options, 'Azure');
|
|
69
|
-
const system = await systemPromptForSkill(skillName, skillSource, { cacheMarkers: profile.cacheMarkers }, delegateSkillName(options), styleFromHeaders(options.headers), levelFromHeaders(options.headers));
|
|
69
|
+
const system = await systemPromptForSkill(skillName, skillSource, { cacheMarkers: profile.cacheMarkers, inline: skillInlineFromHeaders(options) }, delegateSkillName(options), styleFromHeaders(options.headers), levelFromHeaders(options.headers));
|
|
70
70
|
const base = profile.toolChanges ? params : withoutToolChanges(params);
|
|
71
71
|
const family = familyFromTable(params.model, conn.deployments);
|
|
72
72
|
const body = withAzureEffort(shapeForAzure(withSkillPrompt(base, system, { cacheMarkers: profile.cacheMarkers }), profile), profile, roleOf(options.headers), family);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import keychain from '../../auth/keychain.js';
|
|
3
3
|
import { createAuthedSkillSource, defaultBearer } from '../../skills/source-authed.js';
|
|
4
|
-
import { delegateSkillName, modelAcceptsFallbacks, requiredBetas, requireSkillName, withoutInternalHeaders, withSkillPrompt } from '../provider-shape.js';
|
|
4
|
+
import { delegateSkillName, skillInlineFromHeaders, modelAcceptsFallbacks, requiredBetas, requireSkillName, withoutInternalHeaders, withSkillPrompt } from '../provider-shape.js';
|
|
5
5
|
import { systemPromptForSkill } from '../system-prompt.js';
|
|
6
6
|
import { styleFromHeaders } from '../style.js';
|
|
7
7
|
import { levelFromHeaders } from '../level.js';
|
|
@@ -21,7 +21,7 @@ export function createByokProvider(cfg, getBearer = defaultBearer) {
|
|
|
21
21
|
const skillName = requireSkillName(options, 'BYOK');
|
|
22
22
|
const key = await getKey();
|
|
23
23
|
// Task 18 — the `_reader` header uses the pinned reader prompt, no lookup.
|
|
24
|
-
const system = await systemPromptForSkill(skillName, skillSource, { cacheMarkers: BYOK_PROFILE.cacheMarkers }, delegateSkillName(options), styleFromHeaders(options.headers), levelFromHeaders(options.headers));
|
|
24
|
+
const system = await systemPromptForSkill(skillName, skillSource, { cacheMarkers: BYOK_PROFILE.cacheMarkers, inline: skillInlineFromHeaders(options) }, delegateSkillName(options), styleFromHeaders(options.headers), levelFromHeaders(options.headers));
|
|
25
25
|
// Task 6 — opt into server-side fallback, only for the families probe
|
|
26
26
|
// P3 proved (allow-list); requiredBetas adds its beta.
|
|
27
27
|
const prompted = withSkillPrompt(params, system, { cacheMarkers: BYOK_PROFILE.cacheMarkers });
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const loadOpenAI = async () => (await import('openai')).default;
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { createAuthedSkillSource, defaultBearer } from '../../skills/source-authed.js';
|
|
5
|
-
import { delegateSkillName, requireSkillName } from '../provider-shape.js';
|
|
5
|
+
import { delegateSkillName, requireSkillName, skillInlineFromHeaders } from '../provider-shape.js';
|
|
6
6
|
import { systemPromptForSkill } from '../system-prompt.js';
|
|
7
7
|
import { styleFromHeaders } from '../style.js';
|
|
8
8
|
import { levelFromHeaders } from '../level.js';
|
|
@@ -29,7 +29,7 @@ export function createLocalProvider(cfg, getBearer = defaultBearer) {
|
|
|
29
29
|
// message (no cache markers). anthropicParamsToOpenAI copies only
|
|
30
30
|
// max_tokens / messages / system, so newer fields never reach the endpoint.
|
|
31
31
|
const skillName = requireSkillName(options, 'Local');
|
|
32
|
-
const system = await systemPromptForSkill(skillName, skillSource, { cacheMarkers: false }, delegateSkillName(options), styleFromHeaders(options.headers), levelFromHeaders(options.headers));
|
|
32
|
+
const system = await systemPromptForSkill(skillName, skillSource, { cacheMarkers: false, inline: skillInlineFromHeaders(options) }, delegateSkillName(options), styleFromHeaders(options.headers), levelFromHeaders(options.headers));
|
|
33
33
|
const OpenAI = await loadOpenAI();
|
|
34
34
|
const sdk = new OpenAI({ apiKey: 'local-ignored', baseURL });
|
|
35
35
|
const openAiParams = anthropicParamsToOpenAI({ ...params, system }, model);
|
|
@@ -204,6 +204,18 @@ export function isPartialJsonApiError(err) {
|
|
|
204
204
|
/** Result text for a tool call whose real result was lost to an interruption. */
|
|
205
205
|
export const INTERRUPTED_TOOL_RESULT = 'This tool call did not complete — the turn was interrupted before its result was recorded. ' +
|
|
206
206
|
'Its effect is unknown: check before relying on it, and re-run it if needed.';
|
|
207
|
+
/**
|
|
208
|
+
* LT-15 D6 — result text for a tool call stopped (Esc / Ctrl+C) at a point
|
|
209
|
+
* where the tool KNOWS nothing ran yet: before its card, its WAL entry and any
|
|
210
|
+
* SAP change. Same shape and flags as INTERRUPTED_TOOL_RESULT (is_error: true).
|
|
211
|
+
*/
|
|
212
|
+
export const STOPPED_BEFORE_START_RESULT = 'This tool call was stopped before anything ran — no change was made in SAP. Ask again if it is still needed.';
|
|
213
|
+
/**
|
|
214
|
+
* LT-15 D6 (review M5) — the same, for the write pipeline's second guard: by
|
|
215
|
+
* then the approval was spent and SAP was read (a local snapshot may exist),
|
|
216
|
+
* but the change itself was not made. Same shape and flags.
|
|
217
|
+
*/
|
|
218
|
+
export const STOPPED_BEFORE_WRITE_RESULT = 'This tool call was stopped before the change was made — nothing was written to SAP. Ask again if it is still needed.';
|
|
207
219
|
/**
|
|
208
220
|
* 2026-09-24 — heal tool_use blocks that have no tool_result after them.
|
|
209
221
|
*
|