@pentoshi/clai 3.9.7 → 3.11.0
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/LICENSE +21 -0
- package/README.md +81 -9
- package/dist/agent/context-manager.d.ts +2 -1
- package/dist/agent/context-manager.js +16 -2
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/loop-guard.d.ts +18 -0
- package/dist/agent/loop-guard.js +50 -2
- package/dist/agent/loop-guard.js.map +1 -1
- package/dist/agent/must-continue.js +4 -8
- package/dist/agent/must-continue.js.map +1 -1
- package/dist/agent/prompt-composer.js +8 -8
- package/dist/agent/prompt-composer.js.map +1 -1
- package/dist/agent/runner.d.ts +1 -0
- package/dist/agent/runner.js +238 -74
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/session-policy.d.ts +1 -2
- package/dist/agent/session-policy.js +3 -2
- package/dist/agent/session-policy.js.map +1 -1
- package/dist/agent/tool-call-parser.js +6 -6
- package/dist/agent/tool-call-parser.js.map +1 -1
- package/dist/app/adapters/current-agent-adapter.js +1 -0
- package/dist/app/adapters/current-agent-adapter.js.map +1 -1
- package/dist/app/controllers/session-controller.js +2 -0
- package/dist/app/controllers/session-controller.js.map +1 -1
- package/dist/app/controllers/session-turn-request.d.ts +1 -0
- package/dist/app/controllers/session-turn-request.js +4 -1
- package/dist/app/controllers/session-turn-request.js.map +1 -1
- package/dist/app/ports/agent-port.d.ts +1 -0
- package/dist/attachments/clipboard-image.js +5 -4
- package/dist/attachments/clipboard-image.js.map +1 -1
- package/dist/attachments/image-content.d.ts +20 -2
- package/dist/attachments/image-content.js +201 -1
- package/dist/attachments/image-content.js.map +1 -1
- package/dist/attachments/image-prepare.d.ts +25 -0
- package/dist/attachments/image-prepare.js +390 -0
- package/dist/attachments/image-prepare.js.map +1 -0
- package/dist/attachments/service.d.ts +4 -2
- package/dist/attachments/service.js +51 -21
- package/dist/attachments/service.js.map +1 -1
- package/dist/commands/providers.d.ts +5 -2
- package/dist/commands/providers.js +121 -7
- package/dist/commands/providers.js.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/llm/agentrouter.js +2 -2
- package/dist/llm/agentrouter.js.map +1 -1
- package/dist/llm/anthropic.d.ts +5 -4
- package/dist/llm/anthropic.js +28 -25
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/aws-mantle.js +19 -20
- package/dist/llm/aws-mantle.js.map +1 -1
- package/dist/llm/bynara.js +2 -2
- package/dist/llm/bynara.js.map +1 -1
- package/dist/llm/capabilities.d.ts +19 -0
- package/dist/llm/capabilities.js +273 -16
- package/dist/llm/capabilities.js.map +1 -1
- package/dist/llm/gemini.js +16 -8
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/groq.js +2 -2
- package/dist/llm/groq.js.map +1 -1
- package/dist/llm/http.d.ts +7 -1
- package/dist/llm/http.js +210 -8
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/kimchi.js +2 -2
- package/dist/llm/kimchi.js.map +1 -1
- package/dist/llm/lightning.d.ts +11 -0
- package/dist/llm/lightning.js +108 -0
- package/dist/llm/lightning.js.map +1 -0
- package/dist/llm/modal.d.ts +16 -0
- package/dist/llm/modal.js +182 -0
- package/dist/llm/modal.js.map +1 -0
- package/dist/llm/nvidia.js +2 -2
- package/dist/llm/nvidia.js.map +1 -1
- package/dist/llm/ollama.js +3 -3
- package/dist/llm/ollama.js.map +1 -1
- package/dist/llm/openai.js +2 -2
- package/dist/llm/openai.js.map +1 -1
- package/dist/llm/openrouter.js +2 -2
- package/dist/llm/openrouter.js.map +1 -1
- package/dist/llm/provider.d.ts +11 -0
- package/dist/llm/provider.js +316 -1
- package/dist/llm/provider.js.map +1 -1
- package/dist/llm/qwen-cloud.js +2 -5
- package/dist/llm/qwen-cloud.js.map +1 -1
- package/dist/llm/router.js +127 -7
- package/dist/llm/router.js.map +1 -1
- package/dist/llm/system-messages.d.ts +5 -0
- package/dist/llm/system-messages.js +14 -0
- package/dist/llm/system-messages.js.map +1 -1
- package/dist/llm/token-usage.d.ts +13 -0
- package/dist/llm/token-usage.js +31 -0
- package/dist/llm/token-usage.js.map +1 -1
- package/dist/llm/tokenrouter.d.ts +15 -0
- package/dist/llm/tokenrouter.js +106 -0
- package/dist/llm/tokenrouter.js.map +1 -0
- package/dist/modes/agent.d.ts +1 -0
- package/dist/modes/agent.js.map +1 -1
- package/dist/modes/ask.js +15 -4
- package/dist/modes/ask.js.map +1 -1
- package/dist/prompts/embedded.js +2 -2
- package/dist/prompts/embedded.js.map +1 -1
- package/dist/prompts/index.d.ts +6 -0
- package/dist/prompts/index.js +39 -21
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/system.agent.md +15 -27
- package/dist/prompts/system.ask.md +1 -1
- package/dist/repl/slash-commands.js +53 -0
- package/dist/repl/slash-commands.js.map +1 -1
- package/dist/repl.js +16 -9
- package/dist/repl.js.map +1 -1
- package/dist/store/config.d.ts +55 -0
- package/dist/store/config.js +127 -0
- package/dist/store/config.js.map +1 -1
- package/dist/store/history.d.ts +2 -1
- package/dist/store/history.js +15 -16
- package/dist/store/history.js.map +1 -1
- package/dist/store/keys.js +41 -2
- package/dist/store/keys.js.map +1 -1
- package/dist/tools/definitions.js +31 -6
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/image.d.ts +0 -7
- package/dist/tools/image.js +76 -22
- package/dist/tools/image.js.map +1 -1
- package/dist/tools/jobs.d.ts +11 -1
- package/dist/tools/jobs.js +67 -0
- package/dist/tools/jobs.js.map +1 -1
- package/dist/tools/ocr.d.ts +28 -0
- package/dist/tools/ocr.js +316 -0
- package/dist/tools/ocr.js.map +1 -0
- package/dist/tools/pdf.d.ts +0 -12
- package/dist/tools/pdf.js +436 -150
- package/dist/tools/pdf.js.map +1 -1
- package/dist/tools/registry.js +5 -0
- package/dist/tools/registry.js.map +1 -1
- package/dist/tui/format-keys.js +10 -0
- package/dist/tui/format-keys.js.map +1 -1
- package/dist/tui-v2/actions/action-id.d.ts +1 -1
- package/dist/tui-v2/actions/action-id.js +3 -0
- package/dist/tui-v2/actions/action-id.js.map +1 -1
- package/dist/tui-v2/actions/format-shortcuts.js +4 -1
- package/dist/tui-v2/actions/format-shortcuts.js.map +1 -1
- package/dist/tui-v2/actions/keymap.js +6 -1
- package/dist/tui-v2/actions/keymap.js.map +1 -1
- package/dist/tui-v2/app/App.js +3 -1
- package/dist/tui-v2/app/App.js.map +1 -1
- package/dist/tui-v2/app/commands/key-commands.js +117 -15
- package/dist/tui-v2/app/commands/key-commands.js.map +1 -1
- package/dist/tui-v2/app/commands/picker-commands.js +80 -11
- package/dist/tui-v2/app/commands/picker-commands.js.map +1 -1
- package/dist/tui-v2/bootstrap/console-guard.d.ts +20 -0
- package/dist/tui-v2/bootstrap/console-guard.js +73 -0
- package/dist/tui-v2/bootstrap/console-guard.js.map +1 -0
- package/dist/tui-v2/bootstrap/start-tui-v2.js +14 -0
- package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -1
- package/dist/tui-v2/components/composer/composer-input-box.js +5 -1
- package/dist/tui-v2/components/composer/composer-input-box.js.map +1 -1
- package/dist/tui-v2/components/jobs/jobs-panel.js +25 -1
- package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -1
- package/dist/tui-v2/components/modal/keys-modal.js +15 -8
- package/dist/tui-v2/components/modal/keys-modal.js.map +1 -1
- package/dist/tui-v2/components/modal/secret-modal.js +13 -4
- package/dist/tui-v2/components/modal/secret-modal.js.map +1 -1
- package/dist/tui-v2/components/pager/pager.js +103 -12
- package/dist/tui-v2/components/pager/pager.js.map +1 -1
- package/dist/tui-v2/components/status/status-line.d.ts +8 -2
- package/dist/tui-v2/components/status/status-line.js +12 -8
- package/dist/tui-v2/components/status/status-line.js.map +1 -1
- package/dist/tui-v2/components/transcript/thinking-block.d.ts +14 -4
- package/dist/tui-v2/components/transcript/thinking-block.js +53 -8
- package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -1
- package/dist/tui-v2/components/transcript/transcript-row.d.ts +2 -0
- package/dist/tui-v2/components/transcript/transcript-row.js +2 -2
- package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -1
- package/dist/tui-v2/components/transcript/transcript-view.js +8 -1
- package/dist/tui-v2/components/transcript/transcript-view.js.map +1 -1
- package/dist/tui-v2/composer/composer-action-port.d.ts +15 -3
- package/dist/tui-v2/composer/composer-action-port.js +49 -6
- package/dist/tui-v2/composer/composer-action-port.js.map +1 -1
- package/dist/tui-v2/composer/composer-editor.js +37 -44
- package/dist/tui-v2/composer/composer-editor.js.map +1 -1
- package/dist/tui-v2/composer/draft-actions.d.ts +31 -0
- package/dist/tui-v2/composer/draft-actions.js +45 -0
- package/dist/tui-v2/composer/draft-actions.js.map +1 -0
- package/dist/tui-v2/composer/textarea-keybindings.d.ts +1 -1
- package/dist/tui-v2/composer/textarea-keybindings.js +33 -0
- package/dist/tui-v2/composer/textarea-keybindings.js.map +1 -1
- package/dist/tui-v2/composer/use-draft-actions.d.ts +32 -0
- package/dist/tui-v2/composer/use-draft-actions.js +59 -0
- package/dist/tui-v2/composer/use-draft-actions.js.map +1 -0
- package/dist/tui-v2/controllers/overlay-controller.d.ts +13 -0
- package/dist/tui-v2/controllers/overlay-controller.js +5 -2
- package/dist/tui-v2/controllers/overlay-controller.js.map +1 -1
- package/dist/tui-v2/rendering/artifact-pager-source.d.ts +10 -0
- package/dist/tui-v2/rendering/artifact-pager-source.js +6 -0
- package/dist/tui-v2/rendering/artifact-pager-source.js.map +1 -1
- package/dist/tui-v2/rendering/job-tail-source.d.ts +21 -0
- package/dist/tui-v2/rendering/job-tail-source.js +87 -0
- package/dist/tui-v2/rendering/job-tail-source.js.map +1 -0
- package/dist/tui-v2/state/use-has-draft.d.ts +6 -0
- package/dist/tui-v2/state/use-has-draft.js +13 -0
- package/dist/tui-v2/state/use-has-draft.js.map +1 -0
- package/dist/types.d.ts +7 -1
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -1
- package/dist/ui/mentions.d.ts +7 -2
- package/dist/ui/mentions.js +52 -117
- package/dist/ui/mentions.js.map +1 -1
- package/dist/version.generated.d.ts +2 -2
- package/dist/version.generated.js +2 -2
- package/dist/version.generated.js.map +1 -1
- package/package.json +1 -1
package/dist/agent/runner.js
CHANGED
|
@@ -9,7 +9,7 @@ import { sanitizeAssistantText } from "../ui/ansi-box.js";
|
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
import { jobManager, } from "../tools/jobs.js";
|
|
11
11
|
import { isResponderResultLedgerMessage, responderContextMessage, upsertResponderContextMessage, upsertResponderResultLedger, } from "./responder-context.js";
|
|
12
|
-
import { agentModeDirective, planModeDirective, renderAgentSystemPrompt, renderCompactAgentSystemPrompt, scratchDirFor, toolNudge, } from "../prompts/index.js";
|
|
12
|
+
import { agentModeDirective, planModeDirective, renderAgentSystemPrompt, renderCompactAgentSystemPrompt, renderRequestEnvironmentContext, scratchDirFor, toolNudge, } from "../prompts/index.js";
|
|
13
13
|
import { getConfig } from "../store/config.js";
|
|
14
14
|
import { beginSessionWorkspace, getActiveSessionWorkspace, } from "../store/session-workspace.js";
|
|
15
15
|
import { groqInputTokenBudget } from "../llm/groq.js";
|
|
@@ -314,7 +314,20 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
314
314
|
let suppressOutcomeDiagnostics = false;
|
|
315
315
|
const unreadResponderNotificationIds = new Set();
|
|
316
316
|
const releaseUnreadResponderClaims = () => {
|
|
317
|
+
// No session filter: this helper runs in the outer turn scope, before the
|
|
318
|
+
// session policy exists, and every id here is already known to belong to
|
|
319
|
+
// this turn (notification ids are globally unique).
|
|
320
|
+
const pending = new Map(jobManager
|
|
321
|
+
.getPendingNotifications()
|
|
322
|
+
.map((notification) => [notification.id, notification]));
|
|
317
323
|
for (const notificationId of unreadResponderNotificationIds) {
|
|
324
|
+
const notification = pending.get(notificationId);
|
|
325
|
+
if (notification?.deliveryStartedAt &&
|
|
326
|
+
!notification.readAt &&
|
|
327
|
+
!notification.analyzedAt &&
|
|
328
|
+
!notification.acknowledgedAt) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
318
331
|
jobManager.releaseResponderNotificationClaim(notificationId);
|
|
319
332
|
}
|
|
320
333
|
};
|
|
@@ -356,7 +369,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
356
369
|
const confirmPort = options.confirm ?? inquirerConfirmPort;
|
|
357
370
|
const projectContext = await loadProjectContext();
|
|
358
371
|
const hasAttachedImages = Boolean(options.images?.length);
|
|
359
|
-
const imageOcrEnabled = shouldEnableImageOcr(prompt, hasAttachedImages);
|
|
372
|
+
const imageOcrEnabled = shouldEnableImageOcr(prompt, hasAttachedImages, options.visionProven !== false);
|
|
360
373
|
// A vision-capable request already carries the actual image bytes. Hiding
|
|
361
374
|
// image.ocr from the model prevents it from replacing visual inspection
|
|
362
375
|
// with a lossy Tesseract pass (which produced fabricated screenshot text).
|
|
@@ -470,67 +483,68 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
470
483
|
if (!pinnedProject && discoveredProjects.length === 1) {
|
|
471
484
|
setActiveProjectRootIfValid(discoveredProjects[0]);
|
|
472
485
|
}
|
|
473
|
-
|
|
486
|
+
// Only long-lived instructions belong in the provider-cached system prefix.
|
|
487
|
+
// Request, project, workspace, recovery, scope, and plan state are appended
|
|
488
|
+
// later as system-marked turns so a changing byte cannot invalidate the
|
|
489
|
+
// constitution (and, on Anthropic, the native tool schemas before it).
|
|
490
|
+
const buildStableSystemContent = (native) => {
|
|
474
491
|
const reliability = getReliabilityPolicy();
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
: {}),
|
|
484
|
-
}),
|
|
485
|
-
];
|
|
486
|
-
if (projectContext) {
|
|
487
|
-
sections.push(`Project context from .clai/context.md:\n${projectContext}`);
|
|
488
|
-
}
|
|
489
|
-
const projectRoot = getActiveProjectRoot();
|
|
490
|
-
if (projectRoot) {
|
|
491
|
-
sections.push(`ACTIVE PROJECT ROOT: ${projectRoot}\n` +
|
|
492
|
-
`All relative paths (./src/…, manifests, configs) resolve under this directory — NOT the agent process cwd. ` +
|
|
493
|
-
`Prefer absolute paths under this root. shell cwd for install / run / build must be this root ` +
|
|
494
|
-
`(or its parent when creating a NEW named subfolder with a scaffolder). ` +
|
|
495
|
-
`Never write user app source into the agent package tree.`);
|
|
496
|
-
}
|
|
497
|
-
else if (destinationHint) {
|
|
498
|
-
sections.push(`USER DESTINATION: create or continue work under "${destinationHint}" (parent folder). ` +
|
|
499
|
-
`Pick or detect a project subfolder; do not scaffold into the agent working tree unless the user asked for that.`);
|
|
500
|
-
}
|
|
501
|
-
// Stack-agnostic PWD / existing-project snapshot so weak models cannot
|
|
502
|
-
// skip explore and re-scaffold into non-empty dirs.
|
|
503
|
-
if (buildLikeTurn &&
|
|
504
|
-
!informationalQuery &&
|
|
505
|
-
!idleOrSocialPrompt) {
|
|
506
|
-
const guessedName = guessProjectFolderName([prompt, activePlan?.goal, activePlan?.detail, activePlan?.tasks.map((t) => t.title).join(" ")]
|
|
507
|
-
.filter(Boolean)
|
|
508
|
-
.join("\n"));
|
|
509
|
-
const extraPaths = [];
|
|
510
|
-
if (destinationHint && guessedName) {
|
|
511
|
-
extraPaths.push(join(destinationHint, guessedName));
|
|
512
|
-
}
|
|
513
|
-
const fromText = extractProjectRootFromPlan(activePlan) ??
|
|
514
|
-
extractProjectRootFromText(prompt);
|
|
515
|
-
if (fromText)
|
|
516
|
-
extraPaths.push(fromText);
|
|
517
|
-
const orientInput = {
|
|
518
|
-
cwd: safeCwd(),
|
|
519
|
-
extraPaths,
|
|
520
|
-
};
|
|
521
|
-
if (destinationHint)
|
|
522
|
-
orientInput.destinationHint = destinationHint;
|
|
523
|
-
const candidate = getActiveProjectRoot() ?? fromText;
|
|
524
|
-
if (candidate)
|
|
525
|
-
orientInput.candidateProject = candidate;
|
|
526
|
-
sections.push(buildWorkspaceOrientation(orientInput));
|
|
527
|
-
}
|
|
528
|
-
if (freshWebSearchRequired) {
|
|
529
|
-
sections.push(freshnessGuardMessage());
|
|
530
|
-
}
|
|
531
|
-
return sections.join("\n\n");
|
|
492
|
+
return (useCompactSystemPrompt
|
|
493
|
+
? renderCompactAgentSystemPrompt
|
|
494
|
+
: renderAgentSystemPrompt)(toolNames.join(", "), {
|
|
495
|
+
nativeTools: native,
|
|
496
|
+
stableEnvironment: true,
|
|
497
|
+
// E6: slim native constitution when API tool schemas are attached.
|
|
498
|
+
...(native ? { slimNative: reliability.slimNativePrompt } : {}),
|
|
499
|
+
});
|
|
532
500
|
};
|
|
533
|
-
const systemSections = [
|
|
501
|
+
const systemSections = [renderRequestEnvironmentContext()];
|
|
502
|
+
if (projectContext) {
|
|
503
|
+
systemSections.push(`Project context from .clai/context.md:\n${projectContext}`);
|
|
504
|
+
}
|
|
505
|
+
const projectRoot = getActiveProjectRoot();
|
|
506
|
+
if (projectRoot) {
|
|
507
|
+
systemSections.push(`ACTIVE PROJECT ROOT: ${projectRoot}\n` +
|
|
508
|
+
`All relative paths (./src/…, manifests, configs) resolve under this directory — NOT the agent process cwd. ` +
|
|
509
|
+
`Prefer absolute paths under this root. shell cwd for install / run / build must be this root ` +
|
|
510
|
+
`(or its parent when creating a NEW named subfolder with a scaffolder). ` +
|
|
511
|
+
`Never write user app source into the agent package tree.`);
|
|
512
|
+
}
|
|
513
|
+
else if (destinationHint) {
|
|
514
|
+
systemSections.push(`USER DESTINATION: create or continue work under "${destinationHint}" (parent folder). ` +
|
|
515
|
+
`Pick or detect a project subfolder; do not scaffold into the agent working tree unless the user asked for that.`);
|
|
516
|
+
}
|
|
517
|
+
// Stack-agnostic PWD / existing-project snapshot so weak models cannot
|
|
518
|
+
// skip explore and re-scaffold into non-empty dirs. This is live filesystem
|
|
519
|
+
// data and therefore must remain outside the cached constitution.
|
|
520
|
+
if (buildLikeTurn &&
|
|
521
|
+
!informationalQuery &&
|
|
522
|
+
!idleOrSocialPrompt) {
|
|
523
|
+
const guessedName = guessProjectFolderName([prompt, activePlan?.goal, activePlan?.detail, activePlan?.tasks.map((t) => t.title).join(" ")]
|
|
524
|
+
.filter(Boolean)
|
|
525
|
+
.join("\n"));
|
|
526
|
+
const extraPaths = [];
|
|
527
|
+
if (destinationHint && guessedName) {
|
|
528
|
+
extraPaths.push(join(destinationHint, guessedName));
|
|
529
|
+
}
|
|
530
|
+
const fromText = extractProjectRootFromPlan(activePlan) ??
|
|
531
|
+
extractProjectRootFromText(prompt);
|
|
532
|
+
if (fromText)
|
|
533
|
+
extraPaths.push(fromText);
|
|
534
|
+
const orientInput = {
|
|
535
|
+
cwd: safeCwd(),
|
|
536
|
+
extraPaths,
|
|
537
|
+
};
|
|
538
|
+
if (destinationHint)
|
|
539
|
+
orientInput.destinationHint = destinationHint;
|
|
540
|
+
const candidate = getActiveProjectRoot() ?? fromText;
|
|
541
|
+
if (candidate)
|
|
542
|
+
orientInput.candidateProject = candidate;
|
|
543
|
+
systemSections.push(buildWorkspaceOrientation(orientInput));
|
|
544
|
+
}
|
|
545
|
+
if (freshWebSearchRequired) {
|
|
546
|
+
systemSections.push(freshnessGuardMessage());
|
|
547
|
+
}
|
|
534
548
|
// The live plan is mutable state: it is injected once as a keyed request
|
|
535
549
|
// suffix (upsertPlanContextMessage) instead of being frozen into the stable
|
|
536
550
|
// system prefix, so the model never sees a stale and a fresh plan together.
|
|
@@ -619,7 +633,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
619
633
|
}
|
|
620
634
|
}
|
|
621
635
|
const promptSections = () => {
|
|
622
|
-
const sections = systemSections.
|
|
636
|
+
const sections = systemSections.map((content) => ({
|
|
623
637
|
kind: content.startsWith("ACTIVE PLAN")
|
|
624
638
|
? "plan"
|
|
625
639
|
: content.startsWith("ENGAGEMENT SCOPE")
|
|
@@ -634,6 +648,12 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
634
648
|
content,
|
|
635
649
|
mandatory: content.startsWith("ACTIVE PLAN") ||
|
|
636
650
|
content.startsWith("ENGAGEMENT SCOPE") ||
|
|
651
|
+
content.startsWith("REQUEST ENVIRONMENT") ||
|
|
652
|
+
content.startsWith("Project context from .clai/context.md:") ||
|
|
653
|
+
content.startsWith("ACTIVE PROJECT ROOT:") ||
|
|
654
|
+
content.startsWith("USER DESTINATION:") ||
|
|
655
|
+
content.startsWith("WORKSPACE STATUS") ||
|
|
656
|
+
content.startsWith("Freshness guard for this turn:") ||
|
|
637
657
|
content.includes("MODE") ||
|
|
638
658
|
content.includes("OUTCOME"),
|
|
639
659
|
}));
|
|
@@ -666,20 +686,14 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
666
686
|
});
|
|
667
687
|
return sections;
|
|
668
688
|
};
|
|
669
|
-
const composeCurrentSystemPrompt = (native) =>
|
|
689
|
+
const composeCurrentSystemPrompt = (native) => buildStableSystemContent(native);
|
|
690
|
+
const requestContext = composeAgentSystemPrompt({
|
|
670
691
|
mode: agentMode,
|
|
671
|
-
nativeToolsActive
|
|
692
|
+
nativeToolsActive,
|
|
672
693
|
maxTokens: inputTokenBudget
|
|
673
694
|
? Math.min(2_000, Math.floor(inputTokenBudget * 0.4))
|
|
674
695
|
: undefined,
|
|
675
|
-
sections:
|
|
676
|
-
{
|
|
677
|
-
kind: "constitution",
|
|
678
|
-
content: buildSystemContent(native),
|
|
679
|
-
mandatory: true,
|
|
680
|
-
},
|
|
681
|
-
...promptSections(),
|
|
682
|
-
],
|
|
696
|
+
sections: promptSections(),
|
|
683
697
|
}).content;
|
|
684
698
|
const fullSystemPrompt = composeCurrentSystemPrompt(nativeToolsActive);
|
|
685
699
|
// Backend-only directives (implement, displayPrompt=null) stay in model
|
|
@@ -696,6 +710,10 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
696
710
|
const messages = [
|
|
697
711
|
{ role: "system", content: fullSystemPrompt },
|
|
698
712
|
...(options.history ?? []),
|
|
713
|
+
// Keep all per-request authority after prior history. This preserves the
|
|
714
|
+
// longest shared prefix for APC providers while single-system dialects
|
|
715
|
+
// retain it in place as a marked user turn.
|
|
716
|
+
{ role: "system", content: `REQUEST CONTEXT\n${requestContext}` },
|
|
699
717
|
userMessage,
|
|
700
718
|
];
|
|
701
719
|
liveMessages = messages;
|
|
@@ -1062,6 +1080,13 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1062
1080
|
let batchRemindCalls = new Set();
|
|
1063
1081
|
let batchReminderNote = "";
|
|
1064
1082
|
const deferredPostToolMessages = [];
|
|
1083
|
+
/**
|
|
1084
|
+
* Successful job.read receipts update the durable ledger only after the
|
|
1085
|
+
* assistant→tool group closes. Inserting the ledger system row inside
|
|
1086
|
+
* executeSingleTool would split native tool protocol and cause repair to
|
|
1087
|
+
* replace the real acknowledgement body with a "No stored body" stub.
|
|
1088
|
+
*/
|
|
1089
|
+
const deferredResponderLedgerNotifications = [];
|
|
1065
1090
|
/**
|
|
1066
1091
|
* Latest plan seen during tool execution. SESSION STATE is refreshed once
|
|
1067
1092
|
* after the full tool batch is recorded — never mid-group (see
|
|
@@ -1193,6 +1218,20 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1193
1218
|
async function executeSingleTool(rawCall, toolEventId, parentSignal) {
|
|
1194
1219
|
const scratchDir = scratchDirFor(safeCwd());
|
|
1195
1220
|
let call = normalizeToolCall(rawCall);
|
|
1221
|
+
const emitVisibleSyntheticReceipt = (result, summary) => {
|
|
1222
|
+
if (!alreadyPrintedIds.has(toolEventId)) {
|
|
1223
|
+
const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
|
|
1224
|
+
chalk.gray(` ${formatToolArgs(call)}`);
|
|
1225
|
+
writeToolCall(toolEventId, call, styleToolChatter(call, toolCallLine) + "\n");
|
|
1226
|
+
alreadyPrintedIds.add(toolEventId);
|
|
1227
|
+
}
|
|
1228
|
+
emit({ type: "tool-start", id: toolEventId });
|
|
1229
|
+
const output = result.output.endsWith("\n")
|
|
1230
|
+
? result.output
|
|
1231
|
+
: `${result.output}\n`;
|
|
1232
|
+
writeToolOutput(toolEventId, output, chalk.dim(` ${output}`));
|
|
1233
|
+
emitToolResult(toolEventId, result, summary);
|
|
1234
|
+
};
|
|
1196
1235
|
let dispatchedTaskId;
|
|
1197
1236
|
let delegation;
|
|
1198
1237
|
let engagementLease;
|
|
@@ -1273,7 +1312,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1273
1312
|
`${call.name} previously failed with identical arguments. Change the command/args and retry.`;
|
|
1274
1313
|
if (loopCheck.kind === "unchanged-success") {
|
|
1275
1314
|
const result = { ok: true, output: reason, exitCode: 0 };
|
|
1276
|
-
|
|
1315
|
+
emitVisibleSyntheticReceipt(result, reason);
|
|
1277
1316
|
return {
|
|
1278
1317
|
ok: true,
|
|
1279
1318
|
call,
|
|
@@ -1347,7 +1386,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1347
1386
|
? `${call.name} failed: Responder result ${identifier} was not delivered to this model turn. Analyze a delivered result before marking it read.`
|
|
1348
1387
|
: `${call.name} failed: read state for Responder result ${identifier} could not be persisted.`;
|
|
1349
1388
|
if (persistedRead && notification) {
|
|
1350
|
-
|
|
1389
|
+
deferredResponderLedgerNotifications.push(notification);
|
|
1351
1390
|
unreadResponderNotificationIds.delete(notification.id);
|
|
1352
1391
|
}
|
|
1353
1392
|
else if (staleWakeSettled && responderWakeNotificationId) {
|
|
@@ -2164,6 +2203,34 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2164
2203
|
engagementLease?.release();
|
|
2165
2204
|
parentSignal.removeEventListener("abort", onParentAbort);
|
|
2166
2205
|
}
|
|
2206
|
+
if (result.suppressedRepeat) {
|
|
2207
|
+
const contextOutput = result.output;
|
|
2208
|
+
const output = result.output.endsWith("\n")
|
|
2209
|
+
? result.output
|
|
2210
|
+
: `${result.output}\n`;
|
|
2211
|
+
writeToolOutput(toolEventId, output, "", { replace: true });
|
|
2212
|
+
emitToolResult(toolEventId, result, contextOutput);
|
|
2213
|
+
options.onToolResult?.(call, result);
|
|
2214
|
+
// Record it as an observation-free success so the per-call guard can
|
|
2215
|
+
// stop an identical replay, while a real state change (new job status
|
|
2216
|
+
// or bytes) still produces a fresh stateKey and is allowed through.
|
|
2217
|
+
const suppressedProbeState = probeStateKey(call);
|
|
2218
|
+
loopGuard.recordAttempt(step, call.name, call.args, true, result.exitCode, "", suppressedProbeState ? { stateKey: suppressedProbeState } : undefined);
|
|
2219
|
+
await auditLog("tool.result", {
|
|
2220
|
+
call,
|
|
2221
|
+
ok: result.ok,
|
|
2222
|
+
exitCode: result.exitCode,
|
|
2223
|
+
output: result.output.slice(0, 4_000),
|
|
2224
|
+
suppressedRepeat: true,
|
|
2225
|
+
});
|
|
2226
|
+
return {
|
|
2227
|
+
ok: result.ok,
|
|
2228
|
+
call,
|
|
2229
|
+
result,
|
|
2230
|
+
contextOutput,
|
|
2231
|
+
suppressedRepeat: true,
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2167
2234
|
if ((call.name === "shell.exec" || call.name === "shell.start") &&
|
|
2168
2235
|
typeof call.args.command === "string" &&
|
|
2169
2236
|
isScaffoldCreateCommand(call.args.command)) {
|
|
@@ -4034,9 +4101,79 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4034
4101
|
// Re-index toRun positions for UI callIds[] (0..n-1 this turn).
|
|
4035
4102
|
toRun = toRun.map((b, index) => ({ ...b, index }));
|
|
4036
4103
|
const allCalls = toRun.map((b) => b.call);
|
|
4104
|
+
const actionSequenceCalls = bound.map((entry) => {
|
|
4105
|
+
const candidate = entry.call;
|
|
4106
|
+
const stateKey = probeStateKey(candidate);
|
|
4107
|
+
return {
|
|
4108
|
+
name: candidate.name,
|
|
4109
|
+
args: candidate.args,
|
|
4110
|
+
...(stateKey ? { stateKey } : {}),
|
|
4111
|
+
};
|
|
4112
|
+
});
|
|
4037
4113
|
/** Stable call→Bound map (object identity; no indexOf for result ids). */
|
|
4038
4114
|
const callToBound = new Map(toRun.map((b) => [b.call, b]));
|
|
4039
4115
|
const runIds = new Set(toRun.map((b) => b.id));
|
|
4116
|
+
const sequenceDecision = loopGuard.observeActionSequence(actionSequenceCalls);
|
|
4117
|
+
if (sequenceDecision.suppress) {
|
|
4118
|
+
const reason = sequenceDecision.terminal
|
|
4119
|
+
? "The model repeated the same action sequence after it was already suppressed. No commands were run again."
|
|
4120
|
+
: "The same action sequence already ran in the previous model round. No commands were run again; reuse the existing results and choose a materially different next action or finish.";
|
|
4121
|
+
writeNotice("warn", reason, chalk.yellow(` ⚠ ${reason}\n`));
|
|
4122
|
+
const suppressedResults = bound.map((b) => {
|
|
4123
|
+
const duplicate = runIds.has(b.id);
|
|
4124
|
+
const resultReason = duplicate ? reason : deferReason;
|
|
4125
|
+
const result = {
|
|
4126
|
+
ok: false,
|
|
4127
|
+
output: resultReason,
|
|
4128
|
+
exitCode: duplicate ? 409 : 130,
|
|
4129
|
+
};
|
|
4130
|
+
return { b, resultReason, result };
|
|
4131
|
+
});
|
|
4132
|
+
// Sequence suppression happens before the normal queued-card flush.
|
|
4133
|
+
// Complete every card explicitly so streamed queued calls never turn
|
|
4134
|
+
// into empty "done" rows when the repeated sequence is skipped.
|
|
4135
|
+
for (const { b, resultReason, result } of suppressedResults) {
|
|
4136
|
+
const queued = deferredToolCalls[b.index];
|
|
4137
|
+
const eventId = queued?.eventId ?? `tool-${++nextToolEventId}`;
|
|
4138
|
+
const toolCallLine = chalk.cyan(` ▶ ${b.call.name}`) +
|
|
4139
|
+
chalk.gray(` ${formatToolArgs(b.call)}`);
|
|
4140
|
+
writeToolCall(eventId, b.call, styleToolChatter(b.call, toolCallLine) + "\n");
|
|
4141
|
+
alreadyPrintedIds.add(eventId);
|
|
4142
|
+
emit({ type: "tool-start", id: eventId });
|
|
4143
|
+
const output = resultReason.endsWith("\n")
|
|
4144
|
+
? resultReason
|
|
4145
|
+
: `${resultReason}\n`;
|
|
4146
|
+
writeToolOutput(eventId, output, chalk.dim(` ${output}`));
|
|
4147
|
+
emitToolResult(eventId, result, resultReason);
|
|
4148
|
+
}
|
|
4149
|
+
if (historyNativeCalls.length) {
|
|
4150
|
+
appendAssistantWithTools(messages, beforeTool ?? "", historyNativeCalls, completion.reasoningBlock);
|
|
4151
|
+
for (const { b, resultReason, result } of suppressedResults) {
|
|
4152
|
+
appendToolResult(messages, b.id, `Tool ${b.call.name} result (exit=${result.exitCode}, ok=false):\n${resultReason}`, b.call.name, false);
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
else {
|
|
4156
|
+
const standardizedContent = (beforeTool ? beforeTool.trim() + "\n\n" : "") +
|
|
4157
|
+
allCalls
|
|
4158
|
+
.map((candidate) => `\`\`\`tool\n${JSON.stringify(candidate)}\n\`\`\``)
|
|
4159
|
+
.join("\n\n");
|
|
4160
|
+
pushAssistantHistory(standardizedContent);
|
|
4161
|
+
}
|
|
4162
|
+
if (sequenceDecision.terminal) {
|
|
4163
|
+
const remainingCriteria = unreadResponderNotificationIds.size > 0
|
|
4164
|
+
? ["Analyze and acknowledge the delivered Responder result without repeating completed foreground work."]
|
|
4165
|
+
: ["Continue with a materially different action that can produce new evidence."];
|
|
4166
|
+
outcomeState.outcome.status = "partial";
|
|
4167
|
+
await saveOutcomeState(outcomeState);
|
|
4168
|
+
moveTurn("partial", "repeated identical action sequence");
|
|
4169
|
+
return finishTurn("Stopped an identical action cycle before it could execute again.", productiveSteps, "partial", remainingCriteria, "The model repeated an identical action sequence without a new premise or state change.");
|
|
4170
|
+
}
|
|
4171
|
+
messages.push(recoveryUserMessage(reason +
|
|
4172
|
+
(unreadResponderNotificationIds.size > 0
|
|
4173
|
+
? " A delivered Responder result is still unread: analyze the available result, gather only genuinely necessary bounded evidence, then call job.read before returning to foreground work."
|
|
4174
|
+
: " Reassess the evidence and select the next action yourself; do not replay completed work.")));
|
|
4175
|
+
continue;
|
|
4176
|
+
}
|
|
4040
4177
|
// Notice BEFORE tool cards so the transcript reads:
|
|
4041
4178
|
// thinking → response → "N tool calls…" → tool cards (not tools then info).
|
|
4042
4179
|
if (allCalls.length > 1) {
|
|
@@ -4086,6 +4223,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4086
4223
|
const planRemindedAt = new Set();
|
|
4087
4224
|
/** True after a successful plan.create this turn (activePlan is turn-start snapshot). */
|
|
4088
4225
|
let planCreatedThisTurn = Boolean(activePlan && activePlan.tasks.length > 0);
|
|
4226
|
+
let actionSequenceExecuted = 0;
|
|
4227
|
+
let actionSequenceEligible = allCalls.length > 0;
|
|
4089
4228
|
/**
|
|
4090
4229
|
* Record a tool result into history. Failures / user declines are
|
|
4091
4230
|
* always returned to the model — we never cancel later siblings or
|
|
@@ -4094,6 +4233,15 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4094
4233
|
*/
|
|
4095
4234
|
const recordResult = (boundCall, res) => {
|
|
4096
4235
|
recordedNativeIds.add(boundCall.id);
|
|
4236
|
+
actionSequenceExecuted += 1;
|
|
4237
|
+
// A policy-suppressed call is deterministic: replaying it verbatim
|
|
4238
|
+
// returns the identical receipt. It must therefore keep the sequence
|
|
4239
|
+
// eligible, otherwise the tool-level suppression and the sequence
|
|
4240
|
+
// guard cancel each other out and the round can repeat forever.
|
|
4241
|
+
actionSequenceEligible &&=
|
|
4242
|
+
(res.ok || Boolean(res.suppressedRepeat)) &&
|
|
4243
|
+
!res.blockOrCancel &&
|
|
4244
|
+
!res.aborted;
|
|
4097
4245
|
if (res.ok && res.call.name === "plan.create") {
|
|
4098
4246
|
planCreatedThisTurn = true;
|
|
4099
4247
|
}
|
|
@@ -4408,9 +4556,25 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4408
4556
|
}
|
|
4409
4557
|
fillMissingToolResults(messages, historyNativeCalls, "Cancelled — not executed this turn.");
|
|
4410
4558
|
}
|
|
4559
|
+
loopGuard.completeActionSequence(actionSequenceCalls, actionSequenceEligible &&
|
|
4560
|
+
toRun.length === bound.length &&
|
|
4561
|
+
actionSequenceExecuted === allCalls.length &&
|
|
4562
|
+
!aborted &&
|
|
4563
|
+
!awaitingPlanApproval &&
|
|
4564
|
+
!governorPauseReason);
|
|
4565
|
+
// Keep ledger system rows outside the native assistant→tool group so
|
|
4566
|
+
// protocol repair preserves the real successful job.read body.
|
|
4567
|
+
for (const notification of deferredResponderLedgerNotifications.splice(0)) {
|
|
4568
|
+
upsertResponderResultLedger(messages, notification);
|
|
4569
|
+
}
|
|
4411
4570
|
// SESSION STATE only after the assistant→tool group is closed.
|
|
4412
4571
|
// Mid-group upserts were the root cause of "No stored body" thrash.
|
|
4413
4572
|
refreshSessionState(pendingSessionStatePlan);
|
|
4573
|
+
if (responderWakeTurn &&
|
|
4574
|
+
unreadResponderNotificationIds.size > 0 &&
|
|
4575
|
+
!allCalls.some((candidate) => candidate.name === "job.read" || candidate.name === "task.read")) {
|
|
4576
|
+
messages.push(recoveryUserMessage("The delivered Responder result is still unread. Decide from the evidence already available whether it is understood. If it is, call job.read now; if not, gather only the smallest bounded evidence needed. Do not resume or repeat unrelated foreground work before resolving this receipt."));
|
|
4577
|
+
}
|
|
4414
4578
|
if (deferredPostToolMessages.length > 0) {
|
|
4415
4579
|
messages.push(...deferredPostToolMessages.splice(0));
|
|
4416
4580
|
}
|