@cspeach/cli 1.1.50 → 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 +314 -44
- 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 +11 -3
- package/dist/commands/compact.js +12 -1
- package/dist/commands/config-set.js +14 -1
- package/dist/commands/config-show.js +16 -4
- package/dist/commands/export-audit.js +26 -3
- package/dist/commands/export-conversation.js +346 -0
- package/dist/commands/finish-check-guard.js +124 -0
- package/dist/commands/help.js +3 -0
- package/dist/commands/plan-boundary.js +5 -0
- package/dist/commands/plan-continue.js +11 -0
- package/dist/commands/plan-reground.js +27 -4
- package/dist/commands/plan-resume.js +111 -1
- package/dist/commands/plan-startup.js +5 -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/docx/doc-meta.js +9 -1
- package/dist/docx/markdown-to-docx.js +14 -1
- package/dist/lane/app-chain.js +24 -1
- package/dist/models/server-config.js +1 -1
- package/dist/projects/document-save.js +65 -8
- package/dist/projects/document-skills.js +32 -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/builtin-commands.js +1 -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/slash-completer.js +4 -0
- package/dist/repl/turn-stop.js +64 -0
- package/dist/repl.js +178 -32
- 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/cli-record.js +165 -0
- package/dist/session/fresh-start.js +4 -0
- 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/card-slot.js +18 -1
- 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';
|
|
@@ -45,7 +47,9 @@ import { resetRule8State, shouldGateRule8 } from '../repl/rule8-detector.js';
|
|
|
45
47
|
import { consumeTodoSetThisTurn } from '../ui/todo-emitter.js';
|
|
46
48
|
import { runSaveCommand, runPromoteCommand, detectArtifactSkill, offerAnswerBlockers } from '../projects/index.js';
|
|
47
49
|
import { planLaneBlockForTurn } from '../projects/plan-lane.js';
|
|
48
|
-
import { DOCUMENT_SKILLS } from '../projects/document-skills.js';
|
|
50
|
+
import { DOCUMENT_SKILLS, WORD_AFTER_ARTIFACT_SAVE } from '../projects/document-skills.js';
|
|
51
|
+
import { metaFromMarkdown } from '../docx/doc-meta.js';
|
|
52
|
+
import { bindCliCardSession, documentOffersFor, markCliMessage, CONTINUE_LINE, WATCHDOG_LINE } from '../session/cli-record.js';
|
|
49
53
|
import { clearAppFlow, getAppFlow, setGapConfirmed, startAppFlow } from '../lane/app-flow.js';
|
|
50
54
|
import { getLatestSkeletonDecision } from '../rap/skeleton-session.js';
|
|
51
55
|
import { afterRebuildTurn } from '../journey/rebuild.js';
|
|
@@ -67,7 +71,8 @@ import { renderStandardsBlock } from '../standards/standards-file.js';
|
|
|
67
71
|
import { composeSessionContextBlock } from './session-context.js';
|
|
68
72
|
import { objectKeyFromInput } from './retry-key.js';
|
|
69
73
|
import { repairPartialBlocks, stripPreFallbackBlocks, healSessionMessagesInPlace, healOrphanToolUses, isPartialJsonApiError, stripUnsignedThinking, isThinkingOnly, dropTrailingThinkingOnly } from './repair-partial.js';
|
|
70
|
-
import {
|
|
74
|
+
import { drainSteeringEntries, enqueueSteering } from './steering-queue.js';
|
|
75
|
+
import { steeringReadLine } from '../renderer/steering-echo.js';
|
|
71
76
|
import { TurnStreamWriter } from './turn-stream.js';
|
|
72
77
|
import { applyToolResultCheckpoint } from './skill-checkpoint.js';
|
|
73
78
|
import { loadWatchdogConfig, evaluateWatchdog } from './turn-watchdog.js';
|
|
@@ -82,6 +87,7 @@ import { CacheKeepalive, snapshotParams, recordLastRequest, isCacheablePrompt, n
|
|
|
82
87
|
import { maybeColdPrune } from './cold-prune.js';
|
|
83
88
|
import { createAdtLock, serializeAdt } from '../tools/subagent/adt-serial.js';
|
|
84
89
|
import { hasReaderReadTools, INTERRUPTED_LINE } from '../tools/subagent/reader-prompt.js';
|
|
90
|
+
import { settleHandOffNote } from '../tools/hand-off.js';
|
|
85
91
|
import { openStatusPage } from '../util/open-in-browser.js';
|
|
86
92
|
import { isChatSkill } from './chat-prompt.js';
|
|
87
93
|
import { getAssistantStyle } from './style-state.js';
|
|
@@ -133,6 +139,12 @@ const SAVE_HOOK_SKILLS = new Set([
|
|
|
133
139
|
// revision path in commands/plan-resume.ts persists instead.
|
|
134
140
|
'abap-plan',
|
|
135
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
|
+
}
|
|
136
148
|
/**
|
|
137
149
|
* True when any user message in the session already carries the rendered
|
|
138
150
|
* <project_context> block. Phase 2 uses this to inject the block exactly
|
|
@@ -373,6 +385,69 @@ export async function inkAwareChoose(a) {
|
|
|
373
385
|
emitTrace(formatAnswerTrace(header, shown, uiWidth()));
|
|
374
386
|
return { kind: 'picked', answer };
|
|
375
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* Word copy everywhere (2026-10-23) — a newer document skill offers the save
|
|
390
|
+
* only when its answer is shaped like a document: at least one Markdown
|
|
391
|
+
* heading. A short answer to a quick question ("what is clean core?") is
|
|
392
|
+
* not offered. Review/handover do not use this check (unchanged).
|
|
393
|
+
*/
|
|
394
|
+
function looksLikeDocument(text) {
|
|
395
|
+
return /^#{1,6}\s+\S/m.test(text);
|
|
396
|
+
}
|
|
397
|
+
const OBJECT_TOKEN_RE = /(?<![\w/])((?:\/[A-Za-z0-9]+\/)?[ZYzy][A-Za-z0-9_]{2,39})\b/g;
|
|
398
|
+
/**
|
|
399
|
+
* The first Z/Y object name in the prompt (final-review m-2): a name with an
|
|
400
|
+
* underscore (zcl_order, ZFI_DUP_INVOICE_CHECK), or one typed in capitals with
|
|
401
|
+
* at least 4 characters (ZTEST). Ordinary words never count: you, yes, YES, YOU.
|
|
402
|
+
*/
|
|
403
|
+
export function promptObjectName(prompt) {
|
|
404
|
+
for (const m of prompt.matchAll(OBJECT_TOKEN_RE)) {
|
|
405
|
+
const t = m[1];
|
|
406
|
+
if (t.includes('_') || (t.length >= 4 && t === t.toUpperCase()))
|
|
407
|
+
return t.toUpperCase();
|
|
408
|
+
}
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Note this turn's (skill, object). `fresh` is false only when the key was
|
|
413
|
+
* already offered in this session (final-review I-3: a clarifying question
|
|
414
|
+
* before the document no longer uses up the offer). Without a session: always
|
|
415
|
+
* fresh (no rule). The offers are cleared by /new (session/cli-record.ts).
|
|
416
|
+
*/
|
|
417
|
+
function noteDocumentTurn(p) {
|
|
418
|
+
if (!p.session)
|
|
419
|
+
return { fresh: true, markOffered: () => { } };
|
|
420
|
+
const object = metaFromMarkdown(p.assistantText, { date: '' }).object ?? promptObjectName(p.userMessage) ?? '';
|
|
421
|
+
const key = `${p.skill}|${object}`;
|
|
422
|
+
const offered = documentOffersFor(p.session);
|
|
423
|
+
return { fresh: !offered.has(key), markOffered: () => { offered.add(key); } };
|
|
424
|
+
}
|
|
425
|
+
/** Today's local date, YYYY-MM-DD (the document file names use it). */
|
|
426
|
+
function localToday() {
|
|
427
|
+
const d = new Date();
|
|
428
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* The Word step after an artefact save. document-save.ts (and the docx
|
|
432
|
+
* package) loads only here, inside the catch: a broken docx install costs
|
|
433
|
+
* one line after the save and never the save itself.
|
|
434
|
+
*/
|
|
435
|
+
async function offerWordAfterArtifactSave(p, saveSkill, savedPath, confirm) {
|
|
436
|
+
try {
|
|
437
|
+
const { offerWordCopyAfterArtifactSave } = await import('../projects/document-save.js');
|
|
438
|
+
const { isHeadless } = await import('../renderer/tty.js');
|
|
439
|
+
const { loadConfig, resolveWordCopy } = await import('../config/loader.js');
|
|
440
|
+
await offerWordCopyAfterArtifactSave({
|
|
441
|
+
saveSkill, savedPath, markdown: p.assistantText, cwd: process.cwd(), sapSystem: p.sapSystem ?? null,
|
|
442
|
+
// Inside a guided flow the Word step never asks: same rule as headless.
|
|
443
|
+
today: localToday(), headless: isHeadless() || p.guided === true, wordCopy: resolveWordCopy(await loadConfig()),
|
|
444
|
+
confirm, emit: p.emit,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
catch (e) {
|
|
448
|
+
p.emit(formatInfo(`Word copy not written: ${e.message}`).trimEnd());
|
|
449
|
+
}
|
|
450
|
+
}
|
|
376
451
|
export async function maybeOfferSave(p) {
|
|
377
452
|
if (p.assistantText.trim().length === 0)
|
|
378
453
|
return;
|
|
@@ -383,7 +458,26 @@ export async function maybeOfferSave(p) {
|
|
|
383
458
|
// The skill check reads a tiny constant module; document-save.ts (and the
|
|
384
459
|
// docx package) loads only for these two skills, inside the catch, so a
|
|
385
460
|
// broken docx install costs one line on a review save and nothing else.
|
|
386
|
-
|
|
461
|
+
// Word copy everywhere (2026-10-23): the newer document skills (explain,
|
|
462
|
+
// impact, preflight, …) yield to the artefact save when their answer
|
|
463
|
+
// carries an artefact block, and offer nothing when the turn ended on a
|
|
464
|
+
// question. Review and handover take this branch exactly as before.
|
|
465
|
+
const confirm = p.confirm ?? inkAwareConfirm;
|
|
466
|
+
const doc = DOCUMENT_SKILLS[p.skill];
|
|
467
|
+
// Fix round 1 (I-3): which skill and object this turn was about (every turn,
|
|
468
|
+
// so a follow-up of the same skill on the same object is known).
|
|
469
|
+
const docTurn = noteDocumentTurn(p);
|
|
470
|
+
let offerDoc = doc !== undefined;
|
|
471
|
+
if (doc?.yieldsToArtifact) {
|
|
472
|
+
// Headless runs of the newer document skills write no file (scripts and CI
|
|
473
|
+
// keep today's behaviour). Review and handover keep theirs.
|
|
474
|
+
// A sub-agent turn never saves, so it never uses up the offer (final-review m-1).
|
|
475
|
+
offerDoc = !p.subagent && docTurn.fresh && !(await import('../renderer/tty.js')).isHeadless() && !p.awaitingAnswer && !p.guided
|
|
476
|
+
&& looksLikeDocument(p.assistantText) && !detectArtifactSkill(p.assistantText);
|
|
477
|
+
if (offerDoc)
|
|
478
|
+
docTurn.markOffered();
|
|
479
|
+
}
|
|
480
|
+
if (doc && offerDoc) {
|
|
387
481
|
if (p.subagent)
|
|
388
482
|
return;
|
|
389
483
|
const d = new Date();
|
|
@@ -395,7 +489,7 @@ export async function maybeOfferSave(p) {
|
|
|
395
489
|
await offerDocumentSave({
|
|
396
490
|
skill: p.skill, markdown: p.assistantText, cwd: process.cwd(), sapSystem: p.sapSystem ?? null,
|
|
397
491
|
today, headless: isHeadless(), wordCopy: resolveWordCopy(await loadConfig()),
|
|
398
|
-
confirm
|
|
492
|
+
confirm, emit: p.emit,
|
|
399
493
|
});
|
|
400
494
|
}
|
|
401
495
|
catch (e) {
|
|
@@ -471,6 +565,13 @@ export async function maybeOfferSave(p) {
|
|
|
471
565
|
// Never headless: a headless journey queues nothing.
|
|
472
566
|
...(p.journey && !isHeadless() && p.skill === saveSkill ? { journey: p.journey } : {}),
|
|
473
567
|
});
|
|
568
|
+
// Word copy everywhere (2026-10-23) — a saved project file whose answer
|
|
569
|
+
// is a document (spec gaps, design, estimate, plan, upgrade scan/report)
|
|
570
|
+
// gets the same Word step as a review: `word_copy` decides, headless
|
|
571
|
+
// never asks. The project file itself is untouched. Sub-agent turns: no.
|
|
572
|
+
if (savedPath && !p.subagent && WORD_AFTER_ARTIFACT_SAVE[saveSkill]) {
|
|
573
|
+
await offerWordAfterArtifactSave(p, saveSkill, savedPath, confirm);
|
|
574
|
+
}
|
|
474
575
|
// After a spec-gap file is saved, let a developer answer the open BLOCKER
|
|
475
576
|
// questions inline (recorded as `answered` items in the SAME file) — the
|
|
476
577
|
// business questions stay open for a consultant to answer in the viewer.
|
|
@@ -523,6 +624,7 @@ const SILENT_STATUS = {
|
|
|
523
624
|
activity: (_label) => undefined,
|
|
524
625
|
pause: () => undefined,
|
|
525
626
|
resume: () => undefined,
|
|
627
|
+
pausedMs: () => 0,
|
|
526
628
|
};
|
|
527
629
|
/** Task 18 — at most this many read_agent calls run at the same time. */
|
|
528
630
|
export const MAX_PARALLEL_READERS = 4;
|
|
@@ -855,6 +957,15 @@ ${imageExpansion.error}`));
|
|
|
855
957
|
if (!readerChild) {
|
|
856
958
|
userMessageForLLM = await expandTextFileAttachments(userMessageForLLM, params.ctx.cwd, (line) => emit(chalk.dim(line)));
|
|
857
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
|
+
}
|
|
858
969
|
// App lane routing (Task 6) — the /abap-plan creation turn inside an app
|
|
859
970
|
// flow gets the confirmed <lane_decision> block (read with the app-flow sha;
|
|
860
971
|
// a changed file is refused with a plain line). '' on every other turn, so
|
|
@@ -921,7 +1032,43 @@ ${imageExpansion.error}`));
|
|
|
921
1032
|
const switchNote = session.systemSwitchNote
|
|
922
1033
|
? `<system_switch>${session.systemSwitchNote}</system_switch>`
|
|
923
1034
|
: '';
|
|
924
|
-
|
|
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]);
|
|
925
1072
|
// Deterministically extract structured facts from the user's prompt
|
|
926
1073
|
// (package, transport, object names, environment hint, create intent)
|
|
927
1074
|
// and prepend them as an XML `<session_context>` block. The model reads
|
|
@@ -936,8 +1083,10 @@ ${imageExpansion.error}`));
|
|
|
936
1083
|
// when extraction yields nothing (enrichUserMessage returns original).
|
|
937
1084
|
// Final batch 1 — a caller-enriched message still gets the switch note,
|
|
938
1085
|
// inside its own <session_context> block.
|
|
1086
|
+
// Stage 1A — the <playbook> line rides the same way ('' ⇒ unchanged).
|
|
1087
|
+
const callerNotes = composeSessionContextBlock([switchNote, playbookLine]);
|
|
939
1088
|
const enriched = userMessageForLLM.startsWith('<session_context>')
|
|
940
|
-
? (
|
|
1089
|
+
? (callerNotes ? userMessageForLLM.replace('<session_context>', `<session_context>\n ${callerNotes}`) : userMessageForLLM)
|
|
941
1090
|
: enrichUserMessage(userMessageForLLM, sessionContextBlock);
|
|
942
1091
|
const switchNoteUsed = switchNote !== '' && enriched.includes(switchNote);
|
|
943
1092
|
// Phase 2 of Track B — opt-in project-context enrichment, gated by
|
|
@@ -969,9 +1118,14 @@ ${imageExpansion.error}`));
|
|
|
969
1118
|
sapConnection,
|
|
970
1119
|
});
|
|
971
1120
|
}
|
|
972
|
-
const
|
|
1121
|
+
const contextualContent = projectContextBlock
|
|
973
1122
|
? `${projectContextBlock}\n\n${enriched}`
|
|
974
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;
|
|
975
1129
|
// Chat lane (D11) — what a withdrawn `_chat` turn restores: the whole
|
|
976
1130
|
// pre-turn message array (placeSystemMessagesInPlace may re-sort the tail
|
|
977
1131
|
// after the last assistant message, so a length-based splice is not
|
|
@@ -988,6 +1142,12 @@ ${imageExpansion.error}`));
|
|
|
988
1142
|
// Images (if any) ride as blocks ahead of the text; with none this is the
|
|
989
1143
|
// same plain string as before.
|
|
990
1144
|
session.messages.push({ role: 'user', content: buildUserContent(finalUserContent, imageExpansion.images) });
|
|
1145
|
+
if (params.userMessageOrigin) {
|
|
1146
|
+
markCliMessage(session, session.messages[session.messages.length - 1].content, params.userMessageOrigin.kind, params.userMessageOrigin.line);
|
|
1147
|
+
}
|
|
1148
|
+
// The CLI cards answered during this turn (plan gate, lane, journey) belong to this session.
|
|
1149
|
+
if (!params.ctx.subagent && !readerChild)
|
|
1150
|
+
bindCliCardSession(session);
|
|
991
1151
|
// Round 6 review I4 / final batch 1 — the switch note is dropped only once
|
|
992
1152
|
// it is in a pushed user message.
|
|
993
1153
|
if (switchNoteUsed)
|
|
@@ -1087,6 +1247,13 @@ ${imageExpansion.error}`));
|
|
|
1087
1247
|
* N1 (c) — built from the turn's list (the list every request of this turn
|
|
1088
1248
|
* carries) and never for a session pinned static.
|
|
1089
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;
|
|
1090
1257
|
const surfaceStarters = (skill) => {
|
|
1091
1258
|
const msg = buildSkillToolAddition({
|
|
1092
1259
|
skill,
|
|
@@ -1096,6 +1263,12 @@ ${imageExpansion.error}`));
|
|
|
1096
1263
|
});
|
|
1097
1264
|
if (msg)
|
|
1098
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
|
+
}
|
|
1099
1272
|
};
|
|
1100
1273
|
// A turn on a different skill than the one the array was built for: the
|
|
1101
1274
|
// user message was just pushed, so the system message follows it.
|
|
@@ -1157,7 +1330,34 @@ ${imageExpansion.error}`));
|
|
|
1157
1330
|
// rounds so it doesn't spam the model. See agent/turn-watchdog.ts.
|
|
1158
1331
|
const watchdogConfig = loadWatchdogConfig();
|
|
1159
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();
|
|
1160
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
|
+
};
|
|
1161
1361
|
// 2026-06-08 — manifest-on-interrupt: track the last plan-manifest block we
|
|
1162
1362
|
// already handed to onPlanManifest, so we fire once per DISTINCT manifest
|
|
1163
1363
|
// (a continue-in-session run emits one per phase) and not once per round.
|
|
@@ -1191,14 +1391,28 @@ ${imageExpansion.error}`));
|
|
|
1191
1391
|
// during round N lands at the start of round N+1 (between rounds). The
|
|
1192
1392
|
// Anthropic API concatenates consecutive user messages, so injecting after a
|
|
1193
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.
|
|
1194
1398
|
{
|
|
1195
|
-
const
|
|
1196
|
-
if (
|
|
1197
|
-
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;
|
|
1198
1406
|
session.messages.push({ role: 'user', content: text });
|
|
1407
|
+
markCliMessage(session, text, 'steer', '');
|
|
1199
1408
|
if (roundIndex === 0)
|
|
1200
1409
|
round0Steers = steers;
|
|
1201
|
-
|
|
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));
|
|
1202
1416
|
}
|
|
1203
1417
|
}
|
|
1204
1418
|
let buffer = initialBufferState();
|
|
@@ -1234,10 +1448,14 @@ ${imageExpansion.error}`));
|
|
|
1234
1448
|
session.turn_seq = preTurnState.turn_seq;
|
|
1235
1449
|
// Steering drained in round 0 goes back to the queue, ahead of any
|
|
1236
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.
|
|
1237
1453
|
if (round0Steers.length > 0) {
|
|
1238
|
-
const later =
|
|
1239
|
-
for (const t of
|
|
1240
|
-
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 });
|
|
1241
1459
|
}
|
|
1242
1460
|
throw new ChatUnavailableError(st);
|
|
1243
1461
|
};
|
|
@@ -1273,6 +1491,9 @@ ${imageExpansion.error}`));
|
|
|
1273
1491
|
// Task 15 — the request actually sent by the attempt that succeeded.
|
|
1274
1492
|
let sentParams;
|
|
1275
1493
|
let sentHeaders;
|
|
1494
|
+
// Stage 1A — the tool_additions this call carries (inline turns only).
|
|
1495
|
+
const callSurfaced = pendingSurfaced;
|
|
1496
|
+
pendingSurfaced = [];
|
|
1276
1497
|
try {
|
|
1277
1498
|
stream = await retryWithBackoff(() => (async () => {
|
|
1278
1499
|
// W2.5 — apply optional tool filter (used by agent_run with
|
|
@@ -1396,6 +1617,9 @@ ${imageExpansion.error}`));
|
|
|
1396
1617
|
...styleHeader(getAssistantStyle()),
|
|
1397
1618
|
// §10 D14 — developer sends nothing (byte-identical requests).
|
|
1398
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' } : {}),
|
|
1399
1623
|
};
|
|
1400
1624
|
sentParams = streamParams;
|
|
1401
1625
|
sentHeaders = headers;
|
|
@@ -1449,8 +1673,11 @@ ${imageExpansion.error}`));
|
|
|
1449
1673
|
// largest tool results, save, and run the same request once more (or
|
|
1450
1674
|
// show the card). The main turn only.
|
|
1451
1675
|
if (!readerChild && !params.ctx.subagent && isPromptTooLongError(err)) {
|
|
1452
|
-
if (await recoverFromPromptTooLong(err))
|
|
1676
|
+
if (await recoverFromPromptTooLong(err)) {
|
|
1677
|
+
// Stage 1A — the retried call carries the same tool_additions.
|
|
1678
|
+
pendingSurfaced = [...callSurfaced, ...pendingSurfaced];
|
|
1453
1679
|
continue;
|
|
1680
|
+
}
|
|
1454
1681
|
return;
|
|
1455
1682
|
}
|
|
1456
1683
|
// Proxy returns 409 upgrade_required when CLI is older than skill's min_cli_version.
|
|
@@ -1486,6 +1713,10 @@ ${imageExpansion.error}`));
|
|
|
1486
1713
|
// not in here was cut mid-stream and must never be persisted.
|
|
1487
1714
|
const completedBlocks = new WeakSet();
|
|
1488
1715
|
let sawEndTurn = false;
|
|
1716
|
+
if (muteHandOffEcho) {
|
|
1717
|
+
heldEcho = [];
|
|
1718
|
+
muteHandOffEcho = false;
|
|
1719
|
+
} // LT-3 — hold this round's text
|
|
1489
1720
|
let sawToolUse = false;
|
|
1490
1721
|
let sawMaxTokens = false;
|
|
1491
1722
|
let sawPauseTurn = false;
|
|
@@ -1544,13 +1775,10 @@ ${imageExpansion.error}`));
|
|
|
1544
1775
|
writingCode = false;
|
|
1545
1776
|
const block = event.content_block;
|
|
1546
1777
|
currentAssistantContent.push(block);
|
|
1547
|
-
if (block.type === '
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
else
|
|
1552
|
-
writeStdout(sep);
|
|
1553
|
-
}
|
|
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'));
|
|
1554
1782
|
}
|
|
1555
1783
|
else if (type === 'content_block_delta') {
|
|
1556
1784
|
const delta = event.delta;
|
|
@@ -1566,12 +1794,8 @@ ${imageExpansion.error}`));
|
|
|
1566
1794
|
writingCode = wa.writingCode;
|
|
1567
1795
|
if (wa.label)
|
|
1568
1796
|
status.activity(wa.label);
|
|
1569
|
-
if (output)
|
|
1570
|
-
|
|
1571
|
-
params.chunkEmitter.emit('chunk', output);
|
|
1572
|
-
else
|
|
1573
|
-
writeStdout(output);
|
|
1574
|
-
}
|
|
1797
|
+
if (output)
|
|
1798
|
+
showText(output);
|
|
1575
1799
|
if (last?.type === 'text')
|
|
1576
1800
|
last.text = (last.text ?? '') + delta.text;
|
|
1577
1801
|
// v0.6 — also write the raw model text to disk so the user can
|
|
@@ -1709,8 +1933,11 @@ ${imageExpansion.error}`));
|
|
|
1709
1933
|
// retry: the catch only decides; the card and the final save (with
|
|
1710
1934
|
// turnInterrupted cleared) come from the recovery after the persist block.
|
|
1711
1935
|
if (!readerChild && !params.ctx.subagent && !params.signal?.aborted && currentAssistantContent.length === 0 && isPromptTooLongError(err)) {
|
|
1712
|
-
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];
|
|
1713
1939
|
continue;
|
|
1940
|
+
}
|
|
1714
1941
|
}
|
|
1715
1942
|
}
|
|
1716
1943
|
// Final flush: emit any pending partial line after the stream ends.
|
|
@@ -1722,22 +1949,23 @@ ${imageExpansion.error}`));
|
|
|
1722
1949
|
if (heldProse) {
|
|
1723
1950
|
const { output, state } = renderChunk(heldProse, buffer);
|
|
1724
1951
|
buffer = state;
|
|
1725
|
-
if (output)
|
|
1726
|
-
|
|
1727
|
-
params.chunkEmitter.emit('chunk', output);
|
|
1728
|
-
else
|
|
1729
|
-
writeStdout(output);
|
|
1730
|
-
}
|
|
1952
|
+
if (output)
|
|
1953
|
+
showText(output);
|
|
1731
1954
|
}
|
|
1732
1955
|
if (buffer.pending.length > 0) {
|
|
1733
|
-
|
|
1734
|
-
if (params.chunkEmitter)
|
|
1735
|
-
params.chunkEmitter.emit('chunk', flushed);
|
|
1736
|
-
else
|
|
1737
|
-
writeStdout(flushed);
|
|
1956
|
+
showText(render(buffer.pending));
|
|
1738
1957
|
buffer = initialBufferState();
|
|
1739
1958
|
writingCode = false;
|
|
1740
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
|
+
}
|
|
1741
1969
|
// Repair partial blocks before persistence. ALWAYS run, not just on
|
|
1742
1970
|
// interruption: on a clean tool-use round the model's tool_use blocks can
|
|
1743
1971
|
// still carry a leftover `partial_json` field, and persisting that poisons
|
|
@@ -1862,10 +2090,13 @@ ${imageExpansion.error}`));
|
|
|
1862
2090
|
// this call sent and whether its system block matched the session's
|
|
1863
2091
|
// previous main call (same four inputs as the headers above).
|
|
1864
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.
|
|
1865
2096
|
const systemBlock = mainLine
|
|
1866
2097
|
? noteSystemBlock(session, systemBlockKey({
|
|
1867
|
-
skill: params.skill,
|
|
1868
|
-
delegateSkill: params.delegateSkill,
|
|
2098
|
+
skill: inlineTurn ? INLINE_SYSTEM_BLOCK_SKILL : params.skill,
|
|
2099
|
+
delegateSkill: inlineTurn ? undefined : params.delegateSkill,
|
|
1869
2100
|
style: getAssistantStyle(),
|
|
1870
2101
|
level: getAssistantLevel(),
|
|
1871
2102
|
}))
|
|
@@ -1898,7 +2129,15 @@ ${imageExpansion.error}`));
|
|
|
1898
2129
|
reader: params.ctx.readerSpend,
|
|
1899
2130
|
skill: mainLine ? params.skill : undefined,
|
|
1900
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,
|
|
1901
2138
|
});
|
|
2139
|
+
if (mainLine)
|
|
2140
|
+
skillAppendedLogged = true;
|
|
1902
2141
|
readerCallsLogged = params.ctx.readerSpend?.calls ?? 0;
|
|
1903
2142
|
void appendCostLine(session.id, entry);
|
|
1904
2143
|
// Security batch item 3 — usage counts for byok / ai-hub / local (the
|
|
@@ -1986,6 +2225,7 @@ ${imageExpansion.error}`));
|
|
|
1986
2225
|
const outputTokensThisTurn = Math.max(0, (session.usage?.output_tokens ?? 0) - turnStartOutputTokens);
|
|
1987
2226
|
const decision = evaluateWatchdog({
|
|
1988
2227
|
startedAt: turnStartedAt,
|
|
2228
|
+
pausedMs: status.pausedMs() - turnPausedBase,
|
|
1989
2229
|
outputTokensThisTurn,
|
|
1990
2230
|
config: watchdogConfig,
|
|
1991
2231
|
alreadyWarned: false,
|
|
@@ -2042,6 +2282,9 @@ ${imageExpansion.error}`));
|
|
|
2042
2282
|
emit,
|
|
2043
2283
|
sapSystem: session.sap_system,
|
|
2044
2284
|
...(params.ctx.subagent ? { subagent: true } : {}),
|
|
2285
|
+
...(session.awaitingSkillAnswer === true ? { awaitingAnswer: true } : {}),
|
|
2286
|
+
...(getJourneyFlow(session) || getAppFlow(session) ? { guided: true } : {}),
|
|
2287
|
+
session,
|
|
2045
2288
|
promotedFrom: promotedFromForSave,
|
|
2046
2289
|
...(promotedFromPathForSave ? { promotedFromPath: promotedFromPathForSave } : {}),
|
|
2047
2290
|
...(() => {
|
|
@@ -2195,6 +2438,7 @@ ${imageExpansion.error}`));
|
|
|
2195
2438
|
+ 'If you were mid-way through a tool call, or (for /abap-plan) had not yet emitted the closing cspeach:plan-manifest block, complete it now. '
|
|
2196
2439
|
+ 'Keep narration brief to stay within the limit.',
|
|
2197
2440
|
});
|
|
2441
|
+
markCliMessage(session, session.messages[session.messages.length - 1].content, 'continue', CONTINUE_LINE);
|
|
2198
2442
|
continue;
|
|
2199
2443
|
}
|
|
2200
2444
|
if (sawMaxTokens) {
|
|
@@ -2515,10 +2759,17 @@ ${imageExpansion.error}`));
|
|
|
2515
2759
|
// immediately below handles both success and the not-yet-capped failure case.
|
|
2516
2760
|
// 2026-10-22 — the general guard: a result over TOOL_RESULT_MAX_CHARS
|
|
2517
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;
|
|
2518
2769
|
toolResults.push({
|
|
2519
2770
|
type: 'tool_result',
|
|
2520
2771
|
tool_use_id: block.id,
|
|
2521
|
-
content: capToolResultContent(
|
|
2772
|
+
content: capToolResultContent(resultContent),
|
|
2522
2773
|
is_error: result.is_error,
|
|
2523
2774
|
});
|
|
2524
2775
|
}
|
|
@@ -2538,6 +2789,7 @@ ${imageExpansion.error}`));
|
|
|
2538
2789
|
const outputTokensThisTurn = Math.max(0, (session.usage?.output_tokens ?? 0) - turnStartOutputTokens);
|
|
2539
2790
|
const decision = evaluateWatchdog({
|
|
2540
2791
|
startedAt: turnStartedAt,
|
|
2792
|
+
pausedMs: status.pausedMs() - turnPausedBase,
|
|
2541
2793
|
outputTokensThisTurn,
|
|
2542
2794
|
config: watchdogConfig,
|
|
2543
2795
|
alreadyWarned: watchdogWarned,
|
|
@@ -2546,6 +2798,7 @@ ${imageExpansion.error}`));
|
|
|
2546
2798
|
watchdogWarned = true;
|
|
2547
2799
|
emit(chalk.yellow(`\n[watchdog] ${decision.reason}. Asking the model to wrap up.`));
|
|
2548
2800
|
session.messages.push({ role: 'user', content: decision.message });
|
|
2801
|
+
markCliMessage(session, decision.message, 'watchdog', WATCHDOG_LINE);
|
|
2549
2802
|
await saveSession(session);
|
|
2550
2803
|
}
|
|
2551
2804
|
}
|
|
@@ -2571,3 +2824,20 @@ function dispatchedSkills(assistantContent, toolResults) {
|
|
|
2571
2824
|
}
|
|
2572
2825
|
return out;
|
|
2573
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];
|