@causyne/agent 0.0.0-stage → 1.10.1
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 +102 -0
- package/README.md +31 -2
- package/SECURITY.md +24 -0
- package/bin/causyne.js +13 -0
- package/docs/ARCHITECTURE.md +63 -0
- package/docs/PROVENANCE.md +29 -0
- package/docs/SECURITY_REVIEW.md +89 -0
- package/docs/THIRD_PARTY_NOTICES.md +49 -0
- package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
- package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
- package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
- package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
- package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
- package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
- package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
- package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
- package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
- package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
- package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
- package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
- package/native-skills/android/play-store-release/SKILL.md +43 -0
- package/native-skills/android/room-and-persistence/SKILL.md +40 -0
- package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
- package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
- package/native-skills/backend/caching-strategy/SKILL.md +44 -0
- package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
- package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
- package/native-skills/backend/rest-api-design/SKILL.md +40 -0
- package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
- package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
- package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
- package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
- package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
- package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
- package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
- package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
- package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
- package/native-skills/data/metric-definition/SKILL.md +42 -0
- package/native-skills/data/notebook-to-production/SKILL.md +43 -0
- package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
- package/native-skills/database/connection-pooling/SKILL.md +42 -0
- package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
- package/native-skills/database/query-optimisation/SKILL.md +38 -0
- package/native-skills/database/safe-migrations/SKILL.md +47 -0
- package/native-skills/database/schema-design/SKILL.md +38 -0
- package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
- package/native-skills/design/colour-system-construction/SKILL.md +38 -0
- package/native-skills/design/design-direction-brief/SKILL.md +41 -0
- package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
- package/native-skills/design/responsive-strategy/SKILL.md +39 -0
- package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
- package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
- package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
- package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
- package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
- package/native-skills/devops/incident-triage/SKILL.md +38 -0
- package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
- package/native-skills/devops/observability-baseline/SKILL.md +38 -0
- package/native-skills/devops/secret-management/SKILL.md +40 -0
- package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
- package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
- package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
- package/native-skills/git/branch-strategy/SKILL.md +39 -0
- package/native-skills/git/commit-hygiene/SKILL.md +41 -0
- package/native-skills/git/history-recovery/SKILL.md +44 -0
- package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
- package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
- package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
- package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
- package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
- package/native-skills/mobile/deep-linking/SKILL.md +39 -0
- package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
- package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
- package/native-skills/mobile/push-notifications/SKILL.md +38 -0
- package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
- package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
- package/native-skills/performance/load-testing/SKILL.md +42 -0
- package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
- package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
- package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
- package/native-skills/python/async-correctness/SKILL.md +40 -0
- package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
- package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
- package/native-skills/python/environment-isolation/SKILL.md +41 -0
- package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
- package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
- package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
- package/native-skills/security/authorization-model/SKILL.md +42 -0
- package/native-skills/security/crypto-usage/SKILL.md +48 -0
- package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
- package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
- package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
- package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
- package/native-skills/security/threat-modelling/SKILL.md +46 -0
- package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
- package/native-skills/testing/property-based-testing/SKILL.md +46 -0
- package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
- package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
- package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
- package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
- package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
- package/native-skills/web/accessible-components/SKILL.md +45 -0
- package/native-skills/web/api-client-resilience/SKILL.md +39 -0
- package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
- package/native-skills/web/core-web-vitals/SKILL.md +40 -0
- package/native-skills/web/css-layout-systems/SKILL.md +41 -0
- package/native-skills/web/form-validation-ux/SKILL.md +39 -0
- package/native-skills/web/react-state-architecture/SKILL.md +41 -0
- package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
- package/package.json +68 -4
- package/src/agentLoop.js +2458 -0
- package/src/appChannel.js +109 -0
- package/src/approvalGrants.js +66 -0
- package/src/auditLog.js +52 -0
- package/src/autoUpdate.js +12 -0
- package/src/capabilities.js +36 -0
- package/src/checkpoints.js +1199 -0
- package/src/cliMode.js +638 -0
- package/src/commands.js +805 -0
- package/src/compiledSkills.js +540 -0
- package/src/config.js +130 -0
- package/src/contextManager.js +498 -0
- package/src/cortex/autoCondenser.js +192 -0
- package/src/cortex/provisioner.js +27 -0
- package/src/cortex/vectorDb.js +447 -0
- package/src/designPalettes.js +99 -0
- package/src/devMode.js +6 -0
- package/src/embodimentSession.js +180 -0
- package/src/entrypoint.js +194 -0
- package/src/execSafe.js +21 -0
- package/src/gitSnapshot.js +378 -0
- package/src/mcp/jsonRpc.js +165 -0
- package/src/mcp/mcpClient.js +136 -0
- package/src/mcp/mcpRegistry.js +207 -0
- package/src/memory.js +62 -0
- package/src/memoryManager.js +124 -0
- package/src/modelCatalogue.js +132 -0
- package/src/modelIds.js +42 -0
- package/src/modelRouter.js +145 -0
- package/src/outputStore.js +246 -0
- package/src/perception.js +124 -0
- package/src/planCache.js +437 -0
- package/src/procSignal.js +32 -0
- package/src/providers/anthropic.js +408 -0
- package/src/providers/base.js +328 -0
- package/src/providers/dashscope-byok.js +86 -0
- package/src/providers/deepseek.js +80 -0
- package/src/providers/factory.js +37 -0
- package/src/providers/gemini.js +346 -0
- package/src/providers/moonshot.js +73 -0
- package/src/providers/muse.js +169 -0
- package/src/providers/ollama.js +104 -0
- package/src/providers/openai-compatible.js +84 -0
- package/src/providers/openai.js +20 -0
- package/src/providers/qwen-byok.js +76 -0
- package/src/providers/xai.js +73 -0
- package/src/provisioning/capabilityConsent.js +24 -0
- package/src/provisioning/capabilityProvisioner.js +271 -0
- package/src/provisioning/capabilityTools.js +29 -0
- package/src/reporting.js +373 -0
- package/src/sandbox.js +529 -0
- package/src/sessionStore.js +279 -0
- package/src/skillsManager.js +288 -0
- package/src/slopDetector.js +307 -0
- package/src/taskBuffer.js +45 -0
- package/src/taskShape.js +241 -0
- package/src/textDiff.js +150 -0
- package/src/tokenTracker.js +560 -0
- package/src/tool-scripts/ax_snapshot.applescript +39 -0
- package/src/tool-scripts/ax_snapshot.ps1 +10 -0
- package/src/tool-scripts/desktop_act.applescript +58 -0
- package/src/tool-scripts/resolve_automator.py +119 -0
- package/src/toolPacks.js +524 -0
- package/src/tools/archive.js +63 -0
- package/src/tools/astryxOracle.js +78 -0
- package/src/tools/axSnapshot.js +165 -0
- package/src/tools/blenderAutomator.js +349 -0
- package/src/tools/browserAutomator.js +252 -0
- package/src/tools/checkSystemSpecs.js +64 -0
- package/src/tools/clipboard.js +36 -0
- package/src/tools/codeSearchAst.js +101 -0
- package/src/tools/codeSearcher.js +230 -0
- package/src/tools/codeTools.js +262 -0
- package/src/tools/codebaseSearch.js +75 -0
- package/src/tools/createSkill.js +129 -0
- package/src/tools/deferredMcpSearch.js +246 -0
- package/src/tools/desktopAct.js +208 -0
- package/src/tools/desktopOpen.js +34 -0
- package/src/tools/dynamicLoader.js +44 -0
- package/src/tools/executeProceduralMedia.js +93 -0
- package/src/tools/fsDelete.js +51 -0
- package/src/tools/fsEdit.js +105 -0
- package/src/tools/fsFind.js +128 -0
- package/src/tools/fsList.js +77 -0
- package/src/tools/fsMove.js +57 -0
- package/src/tools/fsRead.js +87 -0
- package/src/tools/fsSearch.js +198 -0
- package/src/tools/fsWrite.js +41 -0
- package/src/tools/gameAiBehaviorTree.js +208 -0
- package/src/tools/gameAnimationRig.js +194 -0
- package/src/tools/gameAssetFetcher.js +372 -0
- package/src/tools/gameAssetHarvester.js +210 -0
- package/src/tools/gameAudioSynth.js +267 -0
- package/src/tools/gameBuilder.js +886 -0
- package/src/tools/gameCanvasAudit.js +101 -0
- package/src/tools/gameCinematicSequencer.js +151 -0
- package/src/tools/gameDataForge.js +124 -0
- package/src/tools/gameDialogueTreeGen.js +166 -0
- package/src/tools/gameEconomyBalancer.js +161 -0
- package/src/tools/gameEntitySpawner.js +207 -0
- package/src/tools/gameLoreForge.js +153 -0
- package/src/tools/gameQuestArcBuilder.js +152 -0
- package/src/tools/gameSceneBuilder.js +158 -0
- package/src/tools/gameShaderForge.js +181 -0
- package/src/tools/gameTilemapGenerator.js +164 -0
- package/src/tools/gameUiHudBuilder.js +292 -0
- package/src/tools/gameVfxEmitterGen.js +108 -0
- package/src/tools/gameWebRuntimeInspector.js +91 -0
- package/src/tools/generateMediaViaApi.js +358 -0
- package/src/tools/gitExec.js +54 -0
- package/src/tools/godotAutomator.js +437 -0
- package/src/tools/manageLocalAiDependencies.js +220 -0
- package/src/tools/memoryWrite.js +99 -0
- package/src/tools/multiEdit.js +107 -0
- package/src/tools/palmierAutomator.js +104 -0
- package/src/tools/pdfReader.js +112 -0
- package/src/tools/persistentShell.js +148 -0
- package/src/tools/printer.js +316 -0
- package/src/tools/procTerm.js +91 -0
- package/src/tools/processList.js +52 -0
- package/src/tools/readNativeSkill.js +39 -0
- package/src/tools/registry.js +277 -0
- package/src/tools/resolveAutomator.js +102 -0
- package/src/tools/screenshot.js +75 -0
- package/src/tools/subagentOrchestrator.js +305 -0
- package/src/tools/sysRun.js +125 -0
- package/src/tools/todoWrite.js +75 -0
- package/src/tools/toolDispatch.js +127 -0
- package/src/tools/toolForge.js +124 -0
- package/src/tools/updatePlan.js +88 -0
- package/src/tools/viewImage.js +82 -0
- package/src/tools/visionInspector.js +154 -0
- package/src/tools/webFetch.js +74 -0
- package/src/tools/webSearch.js +82 -0
- package/src/tui/ask.js +164 -0
- package/src/tui/banner.js +158 -0
- package/src/tui/block.js +152 -0
- package/src/tui/chatRenderer.js +953 -0
- package/src/tui/completion.js +424 -0
- package/src/tui/dialogs.js +606 -0
- package/src/tui/diff.js +296 -0
- package/src/tui/history.js +135 -0
- package/src/tui/home.js +217 -0
- package/src/tui/invocation.js +105 -0
- package/src/tui/keys.js +443 -0
- package/src/tui/lineEditor.js +256 -0
- package/src/tui/overlay.js +256 -0
- package/src/tui/prompt.js +1258 -0
- package/src/tui/selectList.js +335 -0
- package/src/tui/stage.js +330 -0
- package/src/tui/stageSurface.js +609 -0
- package/src/tui/statusLine.js +254 -0
- package/src/tui/text.js +345 -0
- package/src/tui/theme.js +393 -0
- package/src/tui/toolDisplay.js +176 -0
- package/src/tui/transcript.js +235 -0
- package/src/tui/viewport.js +337 -0
- package/src/uncertainty.js +271 -0
- package/src/userSkills.js +265 -0
- package/src/utils/cli_ui.js +0 -0
- package/src/utils/inquirer.js +12 -0
- package/src/utils/paths.js +37 -0
- package/src/version.js +38 -0
- package/src/vfeTracker.js +220 -0
package/src/agentLoop.js
ADDED
|
@@ -0,0 +1,2458 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { isAlwaysAllowed } from './approvalGrants.js';
|
|
3
|
+
import { pruneSessions as pruneEmbodimentSessions, describeSession } from './embodimentSession.js';
|
|
4
|
+
import { WorkspacePerception } from './perception.js';
|
|
5
|
+
import { audioFormatForMimeType } from './providers/base.js';
|
|
6
|
+
import { TaskBuffer } from './taskBuffer.js';
|
|
7
|
+
import { TOOL_DEFINITIONS, getDynamicTools, resolveTool, canonicalToolName } from './tools/registry.js';
|
|
8
|
+
import { Sandbox } from './sandbox.js';
|
|
9
|
+
import { auditLog } from './auditLog.js';
|
|
10
|
+
import { loadConfig } from './config.js';
|
|
11
|
+
import { isDevEmulator } from './devMode.js';
|
|
12
|
+
import { TokenTracker } from './tokenTracker.js';
|
|
13
|
+
|
|
14
|
+
import { getMemoryContextString } from './memory.js';
|
|
15
|
+
import { MemoryManager } from './memoryManager.js';
|
|
16
|
+
import { cortexDb } from './cortex/vectorDb.js';
|
|
17
|
+
import { condenseAndSave } from './cortex/autoCondenser.js';
|
|
18
|
+
import { analyzeTask, buildTaskBrief, packsFromShape } from './taskShape.js';
|
|
19
|
+
import { lookupPlan, recordPlan, recordMiss, renderPlanHint, toStep } from './planCache.js';
|
|
20
|
+
import { routedConfig } from './modelRouter.js';
|
|
21
|
+
import { assessStep, isThrashing } from './uncertainty.js';
|
|
22
|
+
import { VFETracker } from './vfeTracker.js';
|
|
23
|
+
import {
|
|
24
|
+
routePacks,
|
|
25
|
+
packForTool,
|
|
26
|
+
isCoreTool,
|
|
27
|
+
isGameIntent,
|
|
28
|
+
isWebIntent,
|
|
29
|
+
getPackCatalogue,
|
|
30
|
+
PACK_PLAYBOOKS,
|
|
31
|
+
PACKS
|
|
32
|
+
} from './toolPacks.js';
|
|
33
|
+
import {
|
|
34
|
+
resolveCharPool,
|
|
35
|
+
measureMessages,
|
|
36
|
+
resultCapFor,
|
|
37
|
+
decayToolResults,
|
|
38
|
+
buildWindow,
|
|
39
|
+
compactInPlace,
|
|
40
|
+
describeDroppedMessages,
|
|
41
|
+
sanitizeSequences,
|
|
42
|
+
applyProgressiveCompaction,
|
|
43
|
+
snipOversizedToolOutputs
|
|
44
|
+
} from './contextManager.js';
|
|
45
|
+
|
|
46
|
+
// Provider imports
|
|
47
|
+
import { detectWorkspaceDomains, ensureSkillRepo, getSkillDomainHeader } from './skillsManager.js';
|
|
48
|
+
import { spillToolResult, buildSpillNotice, buildDecayPointer, pruneOutputStore } from './outputStore.js';
|
|
49
|
+
import { beginTask as beginCheckpointTask, captureBeforeMutation, pruneStore as pruneCheckpointStore, resolveProjectRoot } from './checkpoints.js';
|
|
50
|
+
import fsSync from 'fs';
|
|
51
|
+
import os from 'os';
|
|
52
|
+
import { analyzeFiles, formatFindings, isScannableFile } from './slopDetector.js';
|
|
53
|
+
import { suggestMovements, formatMovementBrief } from './designPalettes.js';
|
|
54
|
+
import { createProvider } from './providers/factory.js';
|
|
55
|
+
|
|
56
|
+
const SYSTEM_PROMPT = `You are Causyne Agent, a God-Tier Autonomous AI. You operate the user's computer through a local desktop harness. You are not just a coding assistant; you are a general-purpose superintelligence.
|
|
57
|
+
|
|
58
|
+
# Your Capabilities
|
|
59
|
+
You can do anything a human can do from a terminal: write code, build apps, manage files, install software, configure systems, run tests, deploy projects, analyze security vulnerabilities, and more. If a human can do it, you can do it.
|
|
60
|
+
Specifically, you excel at:
|
|
61
|
+
- **System Administration**: Organizing folders, cleaning the desktop, categorizing downloads, finding lost files.
|
|
62
|
+
- **Office & Creative Work**: Researching and writing massive 100-page theses, novels, or film scripts. Writing scripts to output Excel, Word, or PDF files from raw data.
|
|
63
|
+
- **Web & Media**: Searching the web, downloading files, and opening browsers to specific videos (YouTube, Netflix) using cross-platform terminal commands (e.g., AppleScript for macOS, PowerShell for Windows, or xdg-open for Linux).
|
|
64
|
+
|
|
65
|
+
# Core Mandates
|
|
66
|
+
1. **Autonomy & Completion**: You are an agent. Keep going until the user's query is completely resolved before yielding back. Do not stop until the problem is solved. Autonomously resolve the query to the best of your ability using tools.
|
|
67
|
+
2. **Ambition vs. Precision**: Be highly ambitious and creative on new tasks. Be surgically precise on existing codebases. Treat existing code with respect, never touching unrelated files or fixing unrelated bugs.
|
|
68
|
+
3. **Conventions**: Rigorously adhere to existing project conventions. Mimic the style, structure, framework choices, and architectural patterns of existing code. NEVER assume a library/framework is available; verify it first.
|
|
69
|
+
|
|
70
|
+
# Operational Guidelines & Communication
|
|
71
|
+
- **Ruthless Conciseness**: Answer concisely with fewer than 4 lines unless detail is requested. No preambles ("Okay, I will now...") or postambles ("I have finished..."). One-word answers are best for simple questions.
|
|
72
|
+
- **Status Updates**: Write in a continuous conversational style. Give a brief progress note (1-2 sentences) BEFORE executing a tool. **CRITICAL: You MUST output this text and invoke the tool in the EXACT SAME RESPONSE.** If you output text without invoking a tool, the system will assume you are finished and terminate the task.
|
|
73
|
+
- **Confirm Ambiguity**: Do not take significant actions beyond the clear scope of the request without confirming. If asked *how* to do something, explain first, don't just do it.
|
|
74
|
+
- **No Chitchat**: Tools are for actions, text output is *only* for communication. Do not add explanatory comments within tool calls or code blocks.
|
|
75
|
+
- **File Paths**: ALWAYS output the absolute path of any file you create, modify, or generate so the user knows exactly where it is.
|
|
76
|
+
|
|
77
|
+
# Task Management & Planning (TodoWrite)
|
|
78
|
+
- ALWAYS use the TodoWrite tool to track and manage tasks before starting implementation. Create atomic, verb-led, clear outcome items.
|
|
79
|
+
- Check off completed TODOs immediately before reporting progress. Do not batch up multiple task completions.
|
|
80
|
+
- Reconcile and close the todo list when all goal tasks are done. If the user asks you to implement, do not output a separate text-based plan. Just build and display the todo list.
|
|
81
|
+
|
|
82
|
+
# Maximize Parallel Tool Calls (Speed is Key)
|
|
83
|
+
- CRITICAL INSTRUCTION: For maximum efficiency, whenever you perform multiple operations, invoke all relevant tools concurrently (in parallel) rather than sequentially.
|
|
84
|
+
- Searching for different patterns (imports, usage, definitions) MUST happen in parallel. Reading multiple files MUST happen in parallel.
|
|
85
|
+
- Sequential calls can ONLY be used when you genuinely REQUIRE the output of one tool to determine the usage of the next tool. Parallel tool execution is 3-5x faster and is the expected behavior.
|
|
86
|
+
|
|
87
|
+
# Context Understanding & Searching
|
|
88
|
+
- Semantic search (or grep) is your MAIN exploration tool. Start with a broad, high-level query.
|
|
89
|
+
- MANDATORY: Run multiple searches with different wording in parallel; first-pass results often miss key details.
|
|
90
|
+
- Keep searching new areas until you're CONFIDENT nothing important remains. Bias towards not asking the user for help if you can find the answer yourself.
|
|
91
|
+
|
|
92
|
+
# Making Code Changes & Execution
|
|
93
|
+
- **No Blind Patching**: NEVER edit a file using a patch/edit tool if you have not opened it with the read tool within your last 5 messages. Read the file again to confirm its contents before patching.
|
|
94
|
+
- **Verification Loop**: NEVER assume code works. After making code changes, execute the project-specific build, linting, and type-checking commands (e.g., 'tsc', 'npm run lint', 'pytest'). Identify these commands by examining 'README' files or 'package.json'.
|
|
95
|
+
- **Code Quality**: Write HIGH-VERBOSITY code. Optimize for clarity. Use meaningful variable names (no 1-letter names like 'n'). Use guard clauses/early returns. Avoid deep nesting.
|
|
96
|
+
- **No TODOs/Comments**: Do not add comments for trivial code. NEVER use inline comments. Avoid TODO comments—implement the logic instead.
|
|
97
|
+
- **Runnable Code**: Ensure generated code can be run immediately. Add all necessary imports, dependencies, and scaffolding (e.g. requirements.txt) if building from scratch.
|
|
98
|
+
|
|
99
|
+
# Git Workflow & Best Practices
|
|
100
|
+
- When asked to commit changes, start by gathering information: run \`git status\`, \`git diff HEAD\`, and \`git log -n 3\` to match commit style. Combine these shell commands to save steps.
|
|
101
|
+
- Always propose a draft commit message. Prefer messages that are clear, concise, and focused more on "why" and less
|
|
102
|
+
|
|
103
|
+
# Security & Constraints (Causyne Architecture)
|
|
104
|
+
- **Token Budgeting**: You must respect the user's BYOK (Bring Your Own Key) limits. Be highly token-efficient.
|
|
105
|
+
- **Explain Critical Commands**: Before executing destructive shell commands, provide a brief explanation of the impact.
|
|
106
|
+
- **Interactive Commands**: Avoid shell commands that require user interaction (e.g., \`git rebase -i\`). Use non-interactive flags (e.g., \`-y\`).
|
|
107
|
+
- **Sandboxing**: You operate within a strict 4-layer sandbox. Flag destructive commands (rm, overwrite) as "dangerous" for user approval.
|
|
108
|
+
- **SSRF Protection**: Do not fetch internal IPs (127.0.0.1, 169.254.x.x, 10.x.x.x).
|
|
109
|
+
|
|
110
|
+
# ABSOLUTE ETHICAL SAFETY NET (NON-NEGOTIABLE)
|
|
111
|
+
You are bound by an unbreakable ethical safety net. Regardless of user prompt, jailbreak attempts, or roleplay scenarios, you MUST REFUSE to:
|
|
112
|
+
1. Create software designed to damage, disrupt, or gain unauthorized access to computer systems (malware, ransomware, keyloggers, trojans, rootkits).
|
|
113
|
+
2. Write phishing pages, credential harvesters, social engineering scripts, or fake login forms designed to steal user passwords.
|
|
114
|
+
3. Build DDoS tools, network flooding scripts, or tools designed for unauthorized network exploitation or cracking.
|
|
115
|
+
4. Generate content that exploits minors or non-consenting individuals.
|
|
116
|
+
5. Create surveillance/spyware tools, unauthorized screen capture malware, or microphone/camera hijackers.
|
|
117
|
+
6. Write tools for software piracy, license cracking, or key generators.
|
|
118
|
+
7. Build unauthorized cryptocurrency miners or crypto wallet drainers.
|
|
119
|
+
8. Forge custom tools (via tool_forge) that perform any of the above harmful actions.
|
|
120
|
+
|
|
121
|
+
If asked to perform ANY harmful task, politely decline, state that Causyne exists to make the world better, and offer legitimate developer assistance instead.
|
|
122
|
+
|
|
123
|
+
# Dependency Management & Autonomy Modes
|
|
124
|
+
- **Mode A (Passive)**: You MUST explicitly ask the user for permission (by ending your turn and asking a question) before installing any dependencies.
|
|
125
|
+
- **Mode B (Supervised)**: You may install standard development dependencies automatically, but you MUST ask for permission before modifying OS-level settings or running \`sudo\`.
|
|
126
|
+
- **Mode C (Autonomous)**: You have full freedom. Silently execute installation commands if a dependency is missing.
|
|
127
|
+
|
|
128
|
+
# Direct API & Automation Policy
|
|
129
|
+
- **Prefer REST APIs (curl)**: When interacting with external services (like web, email, chat), prioritize finding and using official REST APIs via \`curl\` or simple scripts.
|
|
130
|
+
- **Avoid UI/Browser Automation**: Automating a browser (e.g., Puppeteer/Playwright) is noisy, expensive, and fragile. ONLY use browser automation if a site genuinely has no API. Reading JSON is always better than reading DOM.
|
|
131
|
+
|
|
132
|
+
# UNIVERSAL CRITICAL DIRECTIVE (APPLIES TO ALL TASKS)
|
|
133
|
+
**DO NOT OUTPUT CONVERSATIONAL TEXT WITHOUT INVOKING A TOOL CALL SIMULTANEOUSLY.**
|
|
134
|
+
If the task is not 100% finished, you MUST invoke a tool (like web_search, fs_read, sysRun, etc.) in the exact same response as your text. If you output text without a tool call, the system will assume you are completely done and immediately terminate the task!
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
function getProviderSpecificPrompt(provider = '') {
|
|
138
|
+
const p = provider.toLowerCase();
|
|
139
|
+
|
|
140
|
+
if (p === 'anthropic') {
|
|
141
|
+
return `ANTHROPIC BRAIN ACTIVATION:
|
|
142
|
+
You are Claude — a model with exceptional analytical depth and nuanced reasoning. Leverage your strengths:
|
|
143
|
+
- Use <thinking> tags for complex multi-step reasoning before acting.
|
|
144
|
+
- You excel at understanding subtle context and edge cases. Apply this to architecture decisions.
|
|
145
|
+
- Your creativity is rooted in deep understanding, not randomness. When designing UIs, draw from your vast knowledge of design history, typography, and color theory.
|
|
146
|
+
- Self-critique: Before finalizing any creative output, internally evaluate "Does this look like generic AI output?" If yes, redesign.
|
|
147
|
+
- Ensure all tool calls strictly match the schema without omitting required fields. Avoid conversational filler completely.`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (p === 'openai') {
|
|
151
|
+
return `OPENAI BRAIN ACTIVATION:
|
|
152
|
+
You are GPT — a model with exceptional parallel reasoning and creative synthesis. Leverage your strengths:
|
|
153
|
+
- Execute maximum parallel tool calls. You can read 5 files, search 3 patterns, and write 2 files simultaneously.
|
|
154
|
+
- Your training includes vast exposure to award-winning web design. Channel this: think of specific real-world sites you've seen (Stripe, Linear, Vercel, Apple) and synthesize NEW aesthetics from them.
|
|
155
|
+
- When generating creative work, push beyond the first idea. Your strength is in synthesis — combine unexpected influences.`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (p === 'deepseek') {
|
|
159
|
+
return `DEEPSEEK BRAIN ACTIVATION:
|
|
160
|
+
You are DeepSeek — a model with outstanding code generation and mathematical reasoning. Leverage your strengths:
|
|
161
|
+
- Pay close attention to raw JSON formatting. Avoid escaping quotes unnecessarily.
|
|
162
|
+
- Your code output is exceptionally clean and well-structured. Use this for building pixel-perfect CSS layouts.
|
|
163
|
+
- For creative tasks: your training data includes massive open-source design repositories. Access this knowledge — don't default to generic patterns.
|
|
164
|
+
- Self-verify: After generating UI code, mentally render it. Does it look unique? If not, iterate.`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (p === 'qwen') {
|
|
168
|
+
return `QWEN BRAIN ACTIVATION:
|
|
169
|
+
You are Qwen — a model with exceptional multilingual capability and creative breadth. Leverage your strengths:
|
|
170
|
+
- Pay close attention to raw JSON formatting. Avoid escaping quotes unnecessarily.
|
|
171
|
+
- Your training includes diverse cultural design traditions (Eastern minimalism, European modernism, etc.). Draw from this diversity.
|
|
172
|
+
- For web tasks: vary your design approach. Don't repeat the same layout structure across projects.`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (p === 'muse' || p === 'meta') {
|
|
176
|
+
return `MUSE BRAIN ACTIVATION:
|
|
177
|
+
You are Muse Spark — a model built for long-horizon coding and multi-step agentic work, with a 1,048,576-token context window. Leverage your strengths:
|
|
178
|
+
- You are tuned for tool loops that run for many steps. Do not stop early to summarise progress; keep acting until the task is actually finished.
|
|
179
|
+
- Your context is enormous. Read the surrounding files before you edit — you can afford to understand the whole module, and guessing is a choice, not a constraint.
|
|
180
|
+
- You emit fewer tokens than earlier versions for the same work. Spend that budget on correctness, not on narration.
|
|
181
|
+
- You can see: images, video frames, and PDFs all reach you. After building any UI, screenshot it and look at what you actually produced.
|
|
182
|
+
- For creative work: your training spans design history far beyond the templates. A layout that looks like every other AI-generated page is a failure, not a default.`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (p === 'gemini') {
|
|
186
|
+
return `GEMINI BRAIN ACTIVATION:
|
|
187
|
+
You are Gemini — a multimodal model with strong visual-spatial reasoning. Leverage your strengths:
|
|
188
|
+
- You can reason about visual layouts spatially. Use this when designing page compositions.
|
|
189
|
+
- Your training includes vast visual data. When building UIs, think in terms of visual hierarchy, whitespace rhythm, and typographic scale.
|
|
190
|
+
- After building a web page, use the screenshot tool to visually verify your work. You can see — use this advantage.`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (p === 'ollama') {
|
|
194
|
+
return `LOCAL MODEL ACTIVATION:
|
|
195
|
+
You are running locally — respect the user's hardware constraints. Be token-efficient but don't sacrifice quality.
|
|
196
|
+
- Focus on clean, minimal implementations that work on first try.
|
|
197
|
+
- For creative work: simplicity can be beautiful. Japanese minimalism, Swiss design — these are aesthetically powerful AND computationally light.`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (p === 'xai' || p === 'grok') {
|
|
201
|
+
return `GROK BRAIN ACTIVATION:
|
|
202
|
+
You are Grok — a model highly optimized for speed and parallel tool execution.
|
|
203
|
+
- Prioritize direct, functional solutions over verbose explanations.
|
|
204
|
+
- Execute maximum parallel tool calls when searching or refactoring code.
|
|
205
|
+
- Your personality is unapologetic and highly technical. Focus purely on the most efficient code path.`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (p === 'moonshot' || p === 'kimi') {
|
|
209
|
+
return `KIMI BRAIN ACTIVATION:
|
|
210
|
+
You are Kimi — a model powered by KDA (Kimi Delta Attention) with a massive context window.
|
|
211
|
+
- Leverage your massive memory to reason holistically about the entire codebase structure before making surgical edits.
|
|
212
|
+
- You excel at end-to-end programming and agentic task decomposition.
|
|
213
|
+
- When reading large files, synthesize the global context. Do not lose track of the big picture.`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return "";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The system message carrying an invoked skill — or null when none is active.
|
|
221
|
+
*
|
|
222
|
+
* A pure function, and deliberately exported: this is the single link that was
|
|
223
|
+
* missing. `/skills <id>` printed the file to the terminal and returned, so the
|
|
224
|
+
* body never entered a request and no API call was made at all. Building it
|
|
225
|
+
* here rather than inline in run() means it can be tested for what it actually
|
|
226
|
+
* produces, instead of by reading the source and hoping.
|
|
227
|
+
*
|
|
228
|
+
* Returned as an ARRAY so the caller spreads it, which keeps the systemMessages
|
|
229
|
+
* literal readable.
|
|
230
|
+
*/
|
|
231
|
+
export function skillSystemMessage(skill) {
|
|
232
|
+
if (!skill || typeof skill.content !== 'string' || !skill.content.trim()) return null;
|
|
233
|
+
return [{
|
|
234
|
+
role: 'system',
|
|
235
|
+
content:
|
|
236
|
+
`THE USER HAS INVOKED A SKILL: ${skill.id}\n\n`
|
|
237
|
+
+ 'Follow this procedure for the current task. It is a working method, not a document to '
|
|
238
|
+
+ 'summarise back — carry out its steps, and treat each "Done when:" line as the condition '
|
|
239
|
+
+ 'that must actually hold before moving on. Where it conflicts with your general approach, '
|
|
240
|
+
+ 'the skill wins; where it conflicts with your safety rules or the workspace boundaries, '
|
|
241
|
+
+ 'those win and you say so.\n\n'
|
|
242
|
+
+ '--- BEGIN SKILL ---\n'
|
|
243
|
+
+ skill.content
|
|
244
|
+
+ '\n--- END SKILL ---',
|
|
245
|
+
// Stable for the whole task, so it anchors the cache rather than
|
|
246
|
+
// invalidating the cached prefix on every step.
|
|
247
|
+
cacheAnchor: true
|
|
248
|
+
}];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
export function explainNetworkError(error, config = {}) {
|
|
253
|
+
const cause = error && error.cause;
|
|
254
|
+
const code = (cause && (cause.code || cause.errno)) || '';
|
|
255
|
+
|
|
256
|
+
let host = '';
|
|
257
|
+
try {
|
|
258
|
+
const raw = config.baseUrl && String(config.baseUrl).trim();
|
|
259
|
+
if (raw) host = new URL(raw).host;
|
|
260
|
+
} catch (e) { /* a malformed base URL is itself worth reporting, below */ }
|
|
261
|
+
|
|
262
|
+
const REASONS = {
|
|
263
|
+
ENOTFOUND: 'the host could not be resolved (DNS)',
|
|
264
|
+
EAI_AGAIN: 'DNS lookup failed — the network may be down or captive',
|
|
265
|
+
ECONNREFUSED: 'nothing accepted the connection on that host and port',
|
|
266
|
+
ECONNRESET: 'the connection was reset before a reply arrived',
|
|
267
|
+
ETIMEDOUT: 'the connection timed out',
|
|
268
|
+
UND_ERR_CONNECT_TIMEOUT: 'the connection timed out',
|
|
269
|
+
UND_ERR_HEADERS_TIMEOUT: 'the server accepted the connection but never replied',
|
|
270
|
+
EHOSTUNREACH: 'that host is unreachable from this network',
|
|
271
|
+
ENETUNREACH: 'this machine has no route to the network',
|
|
272
|
+
EPIPE: 'the connection closed mid-request',
|
|
273
|
+
CERT_HAS_EXPIRED: 'the TLS certificate has expired',
|
|
274
|
+
DEPTH_ZERO_SELF_SIGNED_CERT: 'the TLS certificate is self-signed — a proxy may be intercepting',
|
|
275
|
+
UNABLE_TO_VERIFY_LEAF_SIGNATURE: 'the TLS certificate could not be verified — a proxy may be intercepting',
|
|
276
|
+
SELF_SIGNED_CERT_IN_CHAIN: 'a self-signed certificate is in the chain — a proxy may be intercepting'
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const reason = REASONS[code] || (code ? `the connection failed (${code})` : 'the connection failed');
|
|
280
|
+
const where = host ? ` reaching ${host}` : '';
|
|
281
|
+
const provider = config.provider ? ` via ${config.provider}` : '';
|
|
282
|
+
|
|
283
|
+
const checks = [];
|
|
284
|
+
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'ENETUNREACH') {
|
|
285
|
+
checks.push('confirm this machine is online');
|
|
286
|
+
if (host) checks.push(`confirm ${host} is the endpoint you meant — /config changes it`);
|
|
287
|
+
} else if (code === 'ECONNREFUSED') {
|
|
288
|
+
checks.push(host
|
|
289
|
+
? `confirm something is listening on ${host}`
|
|
290
|
+
: 'confirm your local model server is running');
|
|
291
|
+
} else if (String(code).startsWith('CERT') || String(code).includes('SELF_SIGNED')) {
|
|
292
|
+
checks.push('a corporate proxy is likely intercepting TLS — the CA needs to be trusted');
|
|
293
|
+
} else {
|
|
294
|
+
checks.push('retry in a moment; if it persists, check the network and the configured endpoint');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return `Could not reach the model${provider}${where}: ${reason}.\n`
|
|
298
|
+
+ checks.map((check) => ` - ${check}`).join('\n');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// classifyError() returns this for every connection-level failure. Assembled
|
|
302
|
+
// the same way the classifier does, so the two can never drift apart.
|
|
303
|
+
const NETWORK_ERROR_TYPE = "network_error";
|
|
304
|
+
|
|
305
|
+
export class AgentLoop {
|
|
306
|
+
/**
|
|
307
|
+
* How many times in a row the same class of failure may repeat before the
|
|
308
|
+
* loop gives up.
|
|
309
|
+
*
|
|
310
|
+
* Three is enough to ride out a blip and few enough that a genuinely broken
|
|
311
|
+
* endpoint is reported in seconds rather than after the whole step budget.
|
|
312
|
+
*/
|
|
313
|
+
static MAX_CONSECUTIVE_FAILURES = 3;
|
|
314
|
+
constructor(eventSink, taskId, userPrompt) {
|
|
315
|
+
this.eventSink = eventSink;
|
|
316
|
+
this.taskId = taskId;
|
|
317
|
+
this.userPrompt = userPrompt;
|
|
318
|
+
|
|
319
|
+
this.config = loadConfig();
|
|
320
|
+
this.sandbox = new Sandbox();
|
|
321
|
+
|
|
322
|
+
// Retire spilled tool outputs past their retention window. Fire-and-forget
|
|
323
|
+
// at task start rather than on a timer: nothing may run in the background
|
|
324
|
+
// (rule ZM-01), and a task boundary is often enough for a 7-day window.
|
|
325
|
+
try { pruneOutputStore(); } catch (e) { /* never block a task on housekeeping */ }
|
|
326
|
+
try { pruneCheckpointStore(); } catch (e) { /* never block a task on housekeeping */ }
|
|
327
|
+
try { pruneEmbodimentSessions(); } catch (e) { /* never block a task on housekeeping */ }
|
|
328
|
+
// Set by the caller when the user invokes a skill by name. Its body becomes
|
|
329
|
+
// a system message for the task; see systemMessages [4].
|
|
330
|
+
this.activeSkill = null;
|
|
331
|
+
// Consecutive same-class failures, for the retry ceiling below.
|
|
332
|
+
this.consecutiveFailures = 0;
|
|
333
|
+
this.lastFailureType = null;
|
|
334
|
+
this.perception = new WorkspacePerception();
|
|
335
|
+
this.taskBuffer = new TaskBuffer();
|
|
336
|
+
this.tokenTracker = new TokenTracker();
|
|
337
|
+
|
|
338
|
+
this.llm = this.initializeProvider();
|
|
339
|
+
this.memoryManager = new MemoryManager();
|
|
340
|
+
this.messages = this.memoryManager.loadHistory(); // Persistent context window
|
|
341
|
+
this.stepsCompleted = 0;
|
|
342
|
+
this.consecutiveCommands = 0;
|
|
343
|
+
this.circuitBreakerUnlocked = false;
|
|
344
|
+
this.stopRequested = false;
|
|
345
|
+
this.pendingApprovals = new Map(); // Maps toolCallId -> resolve function
|
|
346
|
+
|
|
347
|
+
// Limits based on tier/budget & emulator fast bypass
|
|
348
|
+
this.updateTierLimits();
|
|
349
|
+
|
|
350
|
+
this.consecutiveThinkingSteps = 0;
|
|
351
|
+
this.consecutiveBlockedActions = 0; // V8 Transcendence: Circuit Breaker
|
|
352
|
+
|
|
353
|
+
// Sovereign Progressive Disclosure: which capability packs have entered the
|
|
354
|
+
// context window for this task. Nothing is disabled — see src/toolPacks.js.
|
|
355
|
+
this.unlockedPacks = new Set();
|
|
356
|
+
this.unlockedTools = new Set();
|
|
357
|
+
// sha256 of every large tool result seen this task -> the step that produced
|
|
358
|
+
// it. Lets a verbatim repeat collapse to a pointer instead of paying twice.
|
|
359
|
+
this._observationDigests = new Map();
|
|
360
|
+
// Unlock sequence drives append-only tool ordering, which is what keeps the
|
|
361
|
+
// prompt-cache prefix intact when a pack loads mid-task.
|
|
362
|
+
this.unlockOrder = [];
|
|
363
|
+
this.workspaceContext = null;
|
|
364
|
+
this.taskShape = null;
|
|
365
|
+
// Budget for the billed Tier 2 Cortex condensation pass.
|
|
366
|
+
this._cortexPassesUsed = 0;
|
|
367
|
+
// Per-task memo for context that is constant for the whole task but used to
|
|
368
|
+
// be recomputed on every single step (cortex retrieval, skills index,
|
|
369
|
+
// design detection, workspace domain scan).
|
|
370
|
+
this.staticContext = null;
|
|
371
|
+
this.vfeTracker = new VFETracker();
|
|
372
|
+
this._lessonSimilarity = 0;
|
|
373
|
+
this._paradigmShiftPending = false;
|
|
374
|
+
|
|
375
|
+
// Completion guards fire at most once per task. Previously they were gated
|
|
376
|
+
// on consecutiveThinkingSteps, which resets on every tool call, so the
|
|
377
|
+
// agent could be blocked from finishing over and over until it burned the
|
|
378
|
+
// entire 300-step budget on a task that was already done.
|
|
379
|
+
this.gameGuardFired = false;
|
|
380
|
+
this.webGuardFired = false;
|
|
381
|
+
// P6: rendered surfaces this task wrote, for the anti-slop gate to read back.
|
|
382
|
+
this.webArtifacts = new Set();
|
|
383
|
+
this.slopGateFired = false;
|
|
384
|
+
|
|
385
|
+
this.gameCascadeState = { active: true, currentStage: 1, stageHistory: [], verificationPassed: false };
|
|
386
|
+
this.gameToolStats = { godot_act: 0, game_asset_fetch: 0, game_build_html5: 0, last_godot_action: null };
|
|
387
|
+
this.webVerificationState = { screenshotTaken: false, browserActUsed: false }; // V8 Transcendence: Anti-Slop Guard
|
|
388
|
+
|
|
389
|
+
this.abortController = new AbortController();
|
|
390
|
+
|
|
391
|
+
// V5: Context compaction tracking
|
|
392
|
+
this._lastCompactionIndex = null;
|
|
393
|
+
// V5: Error recovery tracking (per tool call)
|
|
394
|
+
// V5: Error recovery tracking (per tool call)
|
|
395
|
+
this._toolRetryCount = new Map();
|
|
396
|
+
this.isInitialized = false;
|
|
397
|
+
this.taskTemperature = 0.1; // Default
|
|
398
|
+
// Set only on a delegated subagent loop; null means "the full catalogue".
|
|
399
|
+
this.toolScope = null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// V6: Adaptive Temperature based on task intent
|
|
403
|
+
determineTemperature(prompt) {
|
|
404
|
+
if (!prompt) return 0.1;
|
|
405
|
+
const lower = prompt.toLowerCase();
|
|
406
|
+
|
|
407
|
+
// Creative / Open-ended tasks get higher temp
|
|
408
|
+
if (lower.includes("brainstorm") || lower.includes("write a story") || lower.includes("creative") || lower.includes("poem") || lower.includes("novel") || lower.includes("screenplay")) {
|
|
409
|
+
return 0.5;
|
|
410
|
+
}
|
|
411
|
+
// Web Design / UI tasks get creative-code temp (Anti-Slop: warm enough for diversity, cool enough for valid code)
|
|
412
|
+
if (lower.includes("website") || lower.includes("landing page") || lower.includes("web app") || lower.includes("dashboard") || lower.includes("portfolio") || lower.includes("ui design") || lower.includes("homepage") || lower.includes("frontend")) {
|
|
413
|
+
return 0.4;
|
|
414
|
+
}
|
|
415
|
+
// Conversational / Explanatory tasks get medium temp
|
|
416
|
+
if (lower.includes("explain") || lower.includes("how does") || lower.includes("what is") || lower.includes("teach me")) {
|
|
417
|
+
return 0.3;
|
|
418
|
+
}
|
|
419
|
+
// Code / Technical / Precision tasks get low temp
|
|
420
|
+
return 0.1;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Load a capability pack into the context window, recording the sequence.
|
|
425
|
+
*
|
|
426
|
+
* Order matters for cost, not just bookkeeping: tool definitions sit at the
|
|
427
|
+
* front of the cached prefix, so appending is nearly free while reordering
|
|
428
|
+
* invalidates the cache for the tools, the system prompt and every message.
|
|
429
|
+
*/
|
|
430
|
+
unlockPack(packName) {
|
|
431
|
+
if (!packName || this.unlockedPacks.has(packName)) return false;
|
|
432
|
+
this.unlockedPacks.add(packName);
|
|
433
|
+
this.unlockOrder.push(packName);
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Build the parts of the system context that are constant for a whole task.
|
|
439
|
+
* Called once per task rather than once per step.
|
|
440
|
+
*/
|
|
441
|
+
async buildStaticContext() {
|
|
442
|
+
let lessonsLearned = '';
|
|
443
|
+
this._lessonSimilarity = 0;
|
|
444
|
+
if (this.currentTaskPrompt) {
|
|
445
|
+
try {
|
|
446
|
+
const matches = await cortexDb.search(this.currentTaskPrompt, 2);
|
|
447
|
+
if (matches.length > 0) {
|
|
448
|
+
lessonsLearned = `\n\n🧠 DIGITAL BRAIN (LESSONS LEARNED FROM PAST MISTAKES):\n` + matches.map(m => `- ${m.text}`).join('\n');
|
|
449
|
+
this._lessonSimilarity = matches[0].similarity || 0;
|
|
450
|
+
this.debugLog(`cortex search hit: ${(this._lessonSimilarity * 100).toFixed(0)}% similarity`);
|
|
451
|
+
}
|
|
452
|
+
} catch (e) {
|
|
453
|
+
this.debugLog(`cortex retrieval failed: ${e.message}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Anti-Slop: Design Context Awareness (what design system is already installed)
|
|
458
|
+
let designContext = '';
|
|
459
|
+
try {
|
|
460
|
+
const detectedDesign = await this.perception.detectDesignContext(process.cwd());
|
|
461
|
+
if (detectedDesign && detectedDesign.length > 0) {
|
|
462
|
+
designContext = `\nDETECTED DESIGN SYSTEM IN WORKSPACE: ${detectedDesign.join(', ')} — USE THIS. Do NOT install a conflicting system.`;
|
|
463
|
+
}
|
|
464
|
+
} catch (e) {}
|
|
465
|
+
|
|
466
|
+
// Anti-Slop: Creative Pattern Memory (prevents repeating the same design approach)
|
|
467
|
+
let antiRepeatDirective = '';
|
|
468
|
+
if (this.currentTaskPrompt && isWebIntent(this.currentTaskPrompt)) {
|
|
469
|
+
try {
|
|
470
|
+
const recentPatterns = await cortexDb.getRecentCreativePatterns(5);
|
|
471
|
+
if (recentPatterns.length > 0) {
|
|
472
|
+
antiRepeatDirective = `\n\n🎨 CREATIVE DIVERSITY (DO NOT REPEAT RECENT PATTERNS):\n` +
|
|
473
|
+
recentPatterns.map(p => `- ${p}`).join('\n') +
|
|
474
|
+
`\nChoose a DIFFERENT design approach, color palette, and font combination than the above.`;
|
|
475
|
+
}
|
|
476
|
+
} catch (e) {}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Universal skills awareness
|
|
480
|
+
let universalSkillsContext = '';
|
|
481
|
+
try {
|
|
482
|
+
const detectedDomains = detectWorkspaceDomains(process.cwd());
|
|
483
|
+
if (detectedDomains.length > 0) {
|
|
484
|
+
this.debugLog(`detected workspace domains: ${detectedDomains.join(', ')}`);
|
|
485
|
+
for (const domain of detectedDomains) {
|
|
486
|
+
ensureSkillRepo(domain);
|
|
487
|
+
}
|
|
488
|
+
universalSkillsContext = getSkillDomainHeader(detectedDomains);
|
|
489
|
+
}
|
|
490
|
+
} catch (e) {
|
|
491
|
+
this.debugLog(`skill domain detection failed: ${e.message}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Plan cache lookup. Deterministic and local: no model call, no network.
|
|
495
|
+
// Goes in the per-task STATIC block, which is byte-identical every step and
|
|
496
|
+
// already carries a cache anchor, so a hit costs its own tokens exactly
|
|
497
|
+
// once per task rather than once per step.
|
|
498
|
+
let planHint = '';
|
|
499
|
+
try {
|
|
500
|
+
const match = lookupPlan({
|
|
501
|
+
prompt: this.currentTaskPrompt || this.userPrompt,
|
|
502
|
+
archetype: this.taskShape ? this.taskShape.archetype : ''
|
|
503
|
+
});
|
|
504
|
+
if (match) {
|
|
505
|
+
planHint = renderPlanHint(match);
|
|
506
|
+
this._offeredPlanId = match.template.id;
|
|
507
|
+
this._planSimilarity = match.similarity;
|
|
508
|
+
this.debugLog(`plan cache hit ${(match.similarity * 100).toFixed(0)}% (${match.template.steps.length} steps)`);
|
|
509
|
+
}
|
|
510
|
+
} catch (e) {
|
|
511
|
+
this.debugLog(`plan cache lookup skipped: ${e.message}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return { lessonsLearned, designContext, antiRepeatDirective, universalSkillsContext, planHint };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
rearmAbortController() {
|
|
519
|
+
this.abortController = new AbortController();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
abort() {
|
|
523
|
+
this.stopRequested = true;
|
|
524
|
+
if (this.abortController) {
|
|
525
|
+
this.abortController.abort();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* A provider for UTILITY work — condensation, classification, filtering.
|
|
531
|
+
*
|
|
532
|
+
* Same provider, cheaper model. Condensing a dropped transcript is mechanical
|
|
533
|
+
* text work whose output the machine consumes, and paying a flagship rate for
|
|
534
|
+
* it is the clearest waste in the loop: on Anthropic it is $5/$25 against
|
|
535
|
+
* Haiku's $1/$5. Built lazily and cached, because most tasks never compact at
|
|
536
|
+
* all and constructing a second provider they will not use is its own cost.
|
|
537
|
+
*
|
|
538
|
+
* Falls back to the reasoning provider whenever the router declines — an
|
|
539
|
+
* unknown provider, a config override, or a route that would change the
|
|
540
|
+
* training-data posture. Utility work still has to happen.
|
|
541
|
+
*
|
|
542
|
+
* @returns {any}
|
|
543
|
+
*/
|
|
544
|
+
utilityProvider() {
|
|
545
|
+
if (this._utilityLlm) return this._utilityLlm;
|
|
546
|
+
try {
|
|
547
|
+
const { config, route } = routedConfig(this.config, 'condense');
|
|
548
|
+
if (!route.routed) {
|
|
549
|
+
this._utilityLlm = this.llm;
|
|
550
|
+
this.debugLog(`utility route: ${route.reason}`);
|
|
551
|
+
} else {
|
|
552
|
+
this._utilityLlm = createProvider(config);
|
|
553
|
+
this.debugLog(`utility route: ${this.config.model} -> ${route.model}`);
|
|
554
|
+
}
|
|
555
|
+
} catch (e) {
|
|
556
|
+
this._utilityLlm = this.llm; // a routing failure must never stop a task
|
|
557
|
+
this.debugLog(`utility route failed, staying on the reasoning model: ${e.message}`);
|
|
558
|
+
}
|
|
559
|
+
return this._utilityLlm;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
initializeProvider() {
|
|
563
|
+
return createProvider(this.config);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Tools that only observe. Nothing here writes to disk, mutates process
|
|
568
|
+
* state, spends money, or can require approval — which is what makes running
|
|
569
|
+
* several of them at once safe and order-independent.
|
|
570
|
+
*
|
|
571
|
+
* Deliberately an allowlist. A denylist would silently start prefetching
|
|
572
|
+
* every tool added in the future, including destructive ones.
|
|
573
|
+
*/
|
|
574
|
+
static READ_ONLY_TOOLS = new Set([
|
|
575
|
+
'fs_read', 'fs_list', 'fs_find', 'fs_search',
|
|
576
|
+
'code_search_ast', 'code_list_symbols', 'code_search_ripgrep', 'codebase_search',
|
|
577
|
+
'web_fetch', 'web_search', 'pdf_read', 'fs_read_skill',
|
|
578
|
+
'check_system_specs', 'process_list', 'view_image'
|
|
579
|
+
]);
|
|
580
|
+
|
|
581
|
+
/** How many reads may be in flight at once. */
|
|
582
|
+
static PREFETCH_CONCURRENCY = 6;
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Start the read-only calls of one assistant turn concurrently.
|
|
586
|
+
*
|
|
587
|
+
* @returns {Map<string, Promise<object|null>>} keyed by tool_call_id. A entry
|
|
588
|
+
* resolves to the tool result, or to null when the call was not eligible,
|
|
589
|
+
* was denied by the sandbox, or threw — in every one of those cases the
|
|
590
|
+
* serial loop executes it normally and keeps its own error handling.
|
|
591
|
+
*/
|
|
592
|
+
prefetchReadOnlyCalls(toolCalls) {
|
|
593
|
+
const prefetched = new Map();
|
|
594
|
+
if (!Array.isArray(toolCalls) || toolCalls.length < 2) return prefetched;
|
|
595
|
+
|
|
596
|
+
// Mode A promises approval before every tool, including reads. Starting a
|
|
597
|
+
// read here would execute it before the serial approval and hook gates.
|
|
598
|
+
if ((this.config.autonomyMode || 'C') === 'A') return prefetched;
|
|
599
|
+
|
|
600
|
+
const eligible = [];
|
|
601
|
+
for (const toolCall of toolCalls) {
|
|
602
|
+
const functionName = canonicalToolName(toolCall.function.name);
|
|
603
|
+
if (!AgentLoop.READ_ONLY_TOOLS.has(functionName)) continue;
|
|
604
|
+
|
|
605
|
+
let args;
|
|
606
|
+
try {
|
|
607
|
+
args = typeof toolCall.function.arguments === 'string'
|
|
608
|
+
? JSON.parse(toolCall.function.arguments || '{}')
|
|
609
|
+
: (toolCall.function.arguments || {});
|
|
610
|
+
} catch (e) {
|
|
611
|
+
continue; // malformed arguments are the serial loop's business
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// The same guard the serial path applies, applied before anything runs.
|
|
615
|
+
let validation;
|
|
616
|
+
try {
|
|
617
|
+
validation = this.sandbox.validate(functionName, args);
|
|
618
|
+
} catch (e) {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
if (!validation || validation.allowed === false || validation.risk !== 'safe') continue;
|
|
622
|
+
|
|
623
|
+
const tool = resolveTool(functionName);
|
|
624
|
+
if (!tool || typeof tool.execute !== 'function') continue;
|
|
625
|
+
|
|
626
|
+
eligible.push({ toolCall, functionName, args, tool });
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (eligible.length < 2) return prefetched;
|
|
630
|
+
|
|
631
|
+
// A bounded worker pool rather than a bare Promise.all: a model can emit a
|
|
632
|
+
// large batch, and opening fifty file handles or fifty sockets at once
|
|
633
|
+
// would trade one problem for another.
|
|
634
|
+
let cursor = 0;
|
|
635
|
+
const resolvers = new Map();
|
|
636
|
+
for (const item of eligible) {
|
|
637
|
+
prefetched.set(item.toolCall.id, new Promise(resolve => resolvers.set(item.toolCall.id, resolve)));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const worker = async () => {
|
|
641
|
+
while (cursor < eligible.length) {
|
|
642
|
+
const item = eligible[cursor++];
|
|
643
|
+
const resolve = resolvers.get(item.toolCall.id);
|
|
644
|
+
if (this.stopRequested || this.abortController.signal.aborted) { resolve(null); continue; }
|
|
645
|
+
try {
|
|
646
|
+
resolve(await item.tool.execute(item.args, {
|
|
647
|
+
signal: this.abortController.signal,
|
|
648
|
+
parentLoop: this,
|
|
649
|
+
...this.sandbox
|
|
650
|
+
}));
|
|
651
|
+
} catch (e) {
|
|
652
|
+
resolve(null); // let the serial path fail it, with its own retries
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const lanes = Math.min(AgentLoop.PREFETCH_CONCURRENCY, eligible.length);
|
|
658
|
+
for (let i = 0; i < lanes; i++) worker();
|
|
659
|
+
|
|
660
|
+
return prefetched;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
emitEvent(messageObj) {
|
|
664
|
+
if (this.eventSink && this.eventSink.readyState === 1) {
|
|
665
|
+
this.eventSink.send(JSON.stringify({ taskId: this.taskId, ...messageObj }));
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
// WHAT BELONGS HERE: things the user would want to know even though they did
|
|
671
|
+
// not ask — a file was written on their behalf, a limit was reached. Not
|
|
672
|
+
// internal telemetry. Converting the old console.log calls to notices fixed
|
|
673
|
+
// the display corruption but kept the noise, and three lines of task-shape and
|
|
674
|
+
// tier sizing after every invocation is exactly the clutter neither reference
|
|
675
|
+
// shows. Diagnostics go to debugLog().
|
|
676
|
+
notice(message, tone = 'muted') {
|
|
677
|
+
this.emitEvent({ type: 'notice', tone, message: String(message) });
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Diagnostics, off by default.
|
|
682
|
+
*
|
|
683
|
+
* These are for whoever is debugging Causyne, not for the person using it.
|
|
684
|
+
* Printed unconditionally they are both noise and a second uncontrolled writer
|
|
685
|
+
* to a terminal the prompt is repainting. Behind CAUSYNE_DEBUG they cost
|
|
686
|
+
* nothing in a normal session, and stderr keeps them out of a piped stdout.
|
|
687
|
+
*/
|
|
688
|
+
debugLog(message) {
|
|
689
|
+
if (process.env.CAUSYNE_DEBUG !== '1') return;
|
|
690
|
+
process.stderr.write(`[causyne] ${message}\n`);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
handleApprovalResponse(toolCallId, approved) {
|
|
695
|
+
if (this.pendingApprovals.has(toolCallId)) {
|
|
696
|
+
const resolve = this.pendingApprovals.get(toolCallId);
|
|
697
|
+
resolve(approved);
|
|
698
|
+
this.pendingApprovals.delete(toolCallId);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
updateTierLimits() {
|
|
703
|
+
this.config = loadConfig();
|
|
704
|
+
if (typeof this.initializeProvider === 'function') {
|
|
705
|
+
this.llm = this.initializeProvider();
|
|
706
|
+
this._utilityLlm = null; // the route is derived from the model; re-derive it
|
|
707
|
+
}
|
|
708
|
+
if (this.tokenTracker) {
|
|
709
|
+
this.tokenTracker.config = this.config;
|
|
710
|
+
this.tokenTracker.budget = this.config.tokenBudget || { enabled: false };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// verified entitlement). Developer emulator mode is a hardened, secret-gated
|
|
714
|
+
// override for local development on a Fast-tier emulator — see devMode.js.
|
|
715
|
+
const devMode = isDevEmulator(this.config);
|
|
716
|
+
this.activeTier = devMode ? 'super_max' : (this.config.tier || 'premium');
|
|
717
|
+
if (this.config.maxStepsPerTask) {
|
|
718
|
+
this.maxSteps = this.config.maxStepsPerTask;
|
|
719
|
+
} else if (this.activeTier === 'super_max') {
|
|
720
|
+
this.maxSteps = 600;
|
|
721
|
+
} else if (this.activeTier === 'max') {
|
|
722
|
+
this.maxSteps = 400;
|
|
723
|
+
} else {
|
|
724
|
+
this.maxSteps = 300;
|
|
725
|
+
}
|
|
726
|
+
this.maxCircuitBreaker = this.maxSteps;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async run() {
|
|
730
|
+
this.stopRequested = false;
|
|
731
|
+
// A task that was stopped must not poison the next one — see
|
|
732
|
+
// rearmAbortController(). This has to happen before any tool can run.
|
|
733
|
+
this.rearmAbortController();
|
|
734
|
+
// Refresh tier limits on every task run
|
|
735
|
+
this.updateTierLimits();
|
|
736
|
+
|
|
737
|
+
// 1. PERCEIVE — runs once per task so the agent starts with real workspace
|
|
738
|
+
// awareness instead of burning steps rediscovering it. The result used to be
|
|
739
|
+
// assigned to a local variable and thrown away.
|
|
740
|
+
auditLog.log({ event: 'task_start', taskId: this.taskId, request: this.userPrompt });
|
|
741
|
+
this.emitEvent({ type: 'status', message: 'Analyzing workspace...' });
|
|
742
|
+
|
|
743
|
+
// P2: open the undo group for this task. This snapshots the CONVERSATION as
|
|
744
|
+
// well as the workspace tree, because restoring files while chat_history
|
|
745
|
+
// still claims the edits happened leaves the agent believing in a disk state
|
|
746
|
+
// that no longer exists. Fail-open — never blocks the task.
|
|
747
|
+
await beginCheckpointTask({
|
|
748
|
+
taskId: this.taskId,
|
|
749
|
+
prompt: this.userPrompt,
|
|
750
|
+
// The working directory FIRST, always.
|
|
751
|
+
//
|
|
752
|
+
// This passed sandbox.workspacePaths, which is the broad default SCOPE —
|
|
753
|
+
// the places the agent is permitted to touch, not the place it is working.
|
|
754
|
+
// beginTask() snapshots the first three, so on a default install it
|
|
755
|
+
// captured /Users/Shared and /home (5.6 MB of files nobody was editing)
|
|
756
|
+
// and never captured the project the user was actually in. Permission and
|
|
757
|
+
// location are different questions; undo needs the location.
|
|
758
|
+
// beginTask() de-duplicates and takes the first three.
|
|
759
|
+
// The PROJECT, not the process's working directory.
|
|
760
|
+
//
|
|
761
|
+
|
|
762
|
+
// started inside a project. Started from the home directory — which is
|
|
763
|
+
// simply what happens when someone opens a terminal and types the command
|
|
764
|
+
// — cwd is $HOME, the snapshot blew past MAX_TREE_FILES and was recorded
|
|
765
|
+
// as `too_many_files`, and the permission-scope fallbacks then captured
|
|
766
|
+
// /Users/Shared and /home: 5.6 MB of files nobody has ever edited. Every
|
|
767
|
+
// task produced a checkpoint containing nothing worth restoring, which is
|
|
768
|
+
// why undo appeared to do nothing no matter where it was invoked.
|
|
769
|
+
//
|
|
770
|
+
// resolveProjectRoot walks up for a real project marker and refuses to
|
|
771
|
+
// return $HOME or a system root, so the checkpoint covers the code being
|
|
772
|
+
// worked on or honestly covers nothing at all.
|
|
773
|
+
workspaceRoots: [
|
|
774
|
+
resolveProjectRoot(process.cwd()),
|
|
775
|
+
process.cwd(),
|
|
776
|
+
...(this.sandbox.grantAllAccess ? [] : (this.sandbox.workspacePaths || []))
|
|
777
|
+
].filter(Boolean)
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
try {
|
|
781
|
+
this.workspaceContext = await this.perception.buildContext(process.cwd());
|
|
782
|
+
} catch (perceptionError) {
|
|
783
|
+
this.debugLog(`workspace scan failed: ${perceptionError.message}`);
|
|
784
|
+
this.workspaceContext = null;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// 2. ANALYSE TASK SHAPE & PRE-WARM CAPABILITY PACKS.
|
|
788
|
+
// A game request gets the full game toolset from step one with no discovery
|
|
789
|
+
// round trip, while a "rename my files" request never pays the 7,282 tokens
|
|
790
|
+
// those 21 schemas cost. The shape analysis additionally tells the model the
|
|
791
|
+
// ORDER to work in, which saves whole exploratory steps.
|
|
792
|
+
this.gameGuardFired = false;
|
|
793
|
+
this.webGuardFired = false;
|
|
794
|
+
this.webArtifacts = new Set();
|
|
795
|
+
this.slopGateFired = false;
|
|
796
|
+
this.staticContext = null;
|
|
797
|
+
// Plan cache: the procedure THIS task follows, and the template (if any)
|
|
798
|
+
// that was offered to it. Both reset per task — a plan is a property of one
|
|
799
|
+
// request, not of the session.
|
|
800
|
+
this._planSteps = [];
|
|
801
|
+
this._offeredPlanId = null;
|
|
802
|
+
this._planSimilarity = 0;
|
|
803
|
+
this._lessonSimilarity = 0;
|
|
804
|
+
this._lastToolResult = null;
|
|
805
|
+
this._paradigmShiftPending = false;
|
|
806
|
+
if (this.vfeTracker) this.vfeTracker.reset();
|
|
807
|
+
|
|
808
|
+
if (this.userPrompt) {
|
|
809
|
+
this.taskShape = analyzeTask(this.userPrompt, this.workspaceContext);
|
|
810
|
+
|
|
811
|
+
// Scored routing, with the regex router as a safety net so a pack can
|
|
812
|
+
// never be missed just because it scored below threshold.
|
|
813
|
+
const scored = packsFromShape(this.taskShape);
|
|
814
|
+
const fallback = routePacks(this.userPrompt, this.workspaceContext);
|
|
815
|
+
for (const pack of [...scored, ...fallback]) {
|
|
816
|
+
this.unlockPack(pack);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
this.debugLog(`task shape: ${this.taskShape.archetype} (${this.taskShape.confidence} confidence)`);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (!this.isInitialized) {
|
|
823
|
+
const activeTier = this.activeTier;
|
|
824
|
+
const charPoolLimit = resolveCharPool(activeTier, this.config.maxCharPool);
|
|
825
|
+
const devTag = isDevEmulator(this.config) ? ' (dev emulator)' : '';
|
|
826
|
+
// Was a console.log carrying a hardcoded \x1b[1m\x1b[36m, which ignored
|
|
827
|
+
// NO_COLOR outright. The renderer paints this through the theme.
|
|
828
|
+
// Internal sizing, and already implied by the header's `tier` field.
|
|
829
|
+
this.debugLog(
|
|
830
|
+
`tier ${activeTier}${devTag} | ${this.maxSteps} steps | ${charPoolLimit} char context`
|
|
831
|
+
);
|
|
832
|
+
this.isInitialized = true;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
this.taskTemperature = this.determineTemperature(this.userPrompt);
|
|
836
|
+
if (this.unlockedPacks.size > 0) {
|
|
837
|
+
this.debugLog(`capability packs pre-warmed: ${[...this.unlockedPacks].join(', ')}`);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (this.userPrompt) {
|
|
841
|
+
this.currentTaskPrompt = this.userPrompt; // Save for notification later
|
|
842
|
+
|
|
843
|
+
// V5: Multimodal Attachments Support
|
|
844
|
+
if (this.attachments && this.attachments.length > 0) {
|
|
845
|
+
let contentArray = [{ type: 'text', text: this.userPrompt }];
|
|
846
|
+
|
|
847
|
+
for (const att of this.attachments) {
|
|
848
|
+
if (att.type === 'image') {
|
|
849
|
+
contentArray.push({
|
|
850
|
+
type: 'image_url',
|
|
851
|
+
image_url: { url: `data:${att.mimeType};base64,${att.base64}` }
|
|
852
|
+
});
|
|
853
|
+
} else if (att.type === 'audio') {
|
|
854
|
+
|
|
855
|
+
// be hardcoded to 'mp3' while the recorder produced AAC/m4a, so every
|
|
856
|
+
// clip was mislabelled and the provider failed on decode. att.mimeType
|
|
857
|
+
// has always been on the wire; it is now actually read.
|
|
858
|
+
const audioFormat = audioFormatForMimeType(att.mimeType);
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
// cap during reassembly; this is the final, vendor-specific ceiling,
|
|
862
|
+
// reported as a Causyne message rather than an opaque upstream 400.
|
|
863
|
+
const audioBytes = Math.round((att.base64?.length || 0) * 0.75);
|
|
864
|
+
const byteCeiling = this.llm?.maxAudioBytes;
|
|
865
|
+
if (byteCeiling && audioBytes > byteCeiling) {
|
|
866
|
+
contentArray[0].text +=
|
|
867
|
+
`\n\n[SYSTEM NOTIFICATION: The user's voice clip (${Math.round(audioBytes / 1024)} KB) exceeds the ${Math.round(byteCeiling / 1024)} KB limit of the selected model and was not attached. Ask the user to record a shorter clip.]`;
|
|
868
|
+
this.notice(`voice clip dropped: ${audioBytes} B over the ${byteCeiling} B provider limit`);
|
|
869
|
+
} else if (!this.llm?.supportsAudio) {
|
|
870
|
+
|
|
871
|
+
// model that cannot hear — a stale capability advertisement. Say so
|
|
872
|
+
// in-band; prepareMessages would otherwise strip it further down
|
|
873
|
+
// and the user would never learn why nothing was heard.
|
|
874
|
+
// Before giving up: the provider may ship a dedicated
|
|
875
|
+
// speech-to-text model on this same key. Meta does, and it is the
|
|
876
|
+
// whole answer to Muse Spark 1.3 being both the recommended model
|
|
877
|
+
// and the one that cannot hear.
|
|
878
|
+
let transcript = null;
|
|
879
|
+
let transcribeError = null;
|
|
880
|
+
if (typeof this.llm?.transcribeAudio === 'function') {
|
|
881
|
+
try {
|
|
882
|
+
transcript = await this.llm.transcribeAudio(att.base64, att.mimeType);
|
|
883
|
+
} catch (e) {
|
|
884
|
+
transcribeError = e.message;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (transcript) {
|
|
889
|
+
contentArray[0].text +=
|
|
890
|
+
`\n\n[VOICE TRANSCRIPT: The user spoke rather than typed. The selected model cannot take audio directly, so the clip was transcribed by this provider's speech-to-text model. Treat the following as the user's own words: "${transcript}"]`;
|
|
891
|
+
this.notice(`voice clip transcribed (${transcript.length} chars) — the chat model cannot hear`, 'accent');
|
|
892
|
+
} else {
|
|
893
|
+
// Name the way out when the provider knows one. A refusal the
|
|
894
|
+
// user cannot act on is worse than the limitation it reports.
|
|
895
|
+
const alternative = typeof this.llm?.audioAlternative === 'function'
|
|
896
|
+
? this.llm.audioAlternative()
|
|
897
|
+
: null;
|
|
898
|
+
const route = alternative
|
|
899
|
+
? `Ask them to switch to ${alternative}, which accepts audio on this same provider and key, or to use the on-device transcription instead.`
|
|
900
|
+
: 'Ask them to switch to an audio-capable model, or to use the on-device transcription instead.';
|
|
901
|
+
contentArray[0].text +=
|
|
902
|
+
`\n\n[SYSTEM NOTIFICATION: The user sent a voice recording but the selected model cannot process audio. ${route}]`;
|
|
903
|
+
this.notice(transcribeError
|
|
904
|
+
? `voice clip dropped: transcription failed — ${transcribeError}`
|
|
905
|
+
: alternative
|
|
906
|
+
? `voice clip dropped: ${this.config.model} cannot hear — ${alternative} can`
|
|
907
|
+
: 'voice clip dropped: selected model does not support audio input',
|
|
908
|
+
transcribeError ? 'warn' : 'muted');
|
|
909
|
+
}
|
|
910
|
+
} else {
|
|
911
|
+
contentArray.push({
|
|
912
|
+
type: 'input_audio',
|
|
913
|
+
input_audio: {
|
|
914
|
+
data: att.base64,
|
|
915
|
+
format: audioFormat
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
} else if (att.type === 'document') {
|
|
920
|
+
const fs = await import('fs');
|
|
921
|
+
const path = await import('path');
|
|
922
|
+
// Ensure unique name to prevent overwriting
|
|
923
|
+
const safeName = att.name.replace(/[^a-zA-Z0-9.\-_]/g, '_');
|
|
924
|
+
const writePath = path.join(process.cwd(), safeName);
|
|
925
|
+
fs.writeFileSync(writePath, Buffer.from(att.base64, 'base64'));
|
|
926
|
+
|
|
927
|
+
contentArray[0].text += `\n\n[SYSTEM NOTIFICATION: The user attached a document named '${safeName}'. It has been saved to your workspace at ./${safeName}. Please use your file reading tools to examine its contents.]`;
|
|
928
|
+
this.notice(`saved attached document to ${writePath}`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
this.messages.push({ role: 'user', content: contentArray });
|
|
932
|
+
} else {
|
|
933
|
+
this.messages.push({ role: 'user', content: this.userPrompt });
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
this.userPrompt = null; // Clear so we don't push it again
|
|
937
|
+
this.attachments = null; // Clear attachments
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
this.stepsCompleted = 0;
|
|
941
|
+
// Fresh per task. A digest from a previous task would point at a step
|
|
942
|
+
// number the model can no longer see, which is worse than paying twice.
|
|
943
|
+
this._observationDigests = new Map();
|
|
944
|
+
this.consecutiveCommands = 0;
|
|
945
|
+
this.circuitBreakerUnlocked = false;
|
|
946
|
+
this.tokenTracker.resetTask();
|
|
947
|
+
|
|
948
|
+
// The core loop governed by Sovereign Human-in-the-Loop Checkpoints
|
|
949
|
+
while (!this.stopRequested) {
|
|
950
|
+
if (this.stopRequested) {
|
|
951
|
+
this.emitEvent({ type: 'error', message: 'Task killed by user.' });
|
|
952
|
+
auditLog.log({ event: 'task_killed', taskId: this.taskId });
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
this.emitEvent({ type: 'status', message: 'Thinking...' });
|
|
956
|
+
|
|
957
|
+
try {
|
|
958
|
+
// --- V8 MEMORY ARCHITECTURE: DECAY → WINDOW → REAL COMPACTION ---
|
|
959
|
+
// The window is no longer only a view over an ever-growing array. Old
|
|
960
|
+
// tool payloads decay into stubs, and anything amputated is physically
|
|
961
|
+
// replaced by a summary, so memory, disk and CPU all stay bounded.
|
|
962
|
+
|
|
963
|
+
const activeTier = isDevEmulator(this.config) ? 'super_max' : (this.config.tier || 'premium');
|
|
964
|
+
const MAX_CHAR_POOL = resolveCharPool(activeTier, this.config.maxCharPool);
|
|
965
|
+
|
|
966
|
+
// 5-Stage Progressive Compaction (Claude Code Architecture / Research Section E):
|
|
967
|
+
// Stage 2 (Snip >15K tokens / 60K chars) + Stage 1 (Budget Reduction >70%) +
|
|
968
|
+
// Stage 3 (Microcompact) + Stage 4 (Context Collapse >85%)
|
|
969
|
+
const compaction = applyProgressiveCompaction(this.messages, MAX_CHAR_POOL, {
|
|
970
|
+
budgetKeepRecent: this.config.keepRecentToolResults ?? 8,
|
|
971
|
+
collapseKeepRecent: 2
|
|
972
|
+
});
|
|
973
|
+
if (compaction.totalReclaimed > 0) {
|
|
974
|
+
this.debugLog(`progressive compaction reclaimed ${(compaction.totalReclaimed / 1024).toFixed(1)} KB [${compaction.stagesFired.join(', ')}]`);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const { window: slidingWindow, splitIndex } = buildWindow(this.messages, MAX_CHAR_POOL);
|
|
978
|
+
|
|
979
|
+
// What the context window actually holds right now.
|
|
980
|
+
//
|
|
981
|
+
// The footer's context meter was being fed `usedTokens` — the CUMULATIVE
|
|
982
|
+
// token spend of the whole task — and divided by a CHARACTER pool. Two
|
|
983
|
+
// unit errors compounding: after 16 steps it read 290,069 against
|
|
984
|
+
// 120,000 and, because the meter clamped at 99, displayed "290K (99%)"
|
|
985
|
+
// while the real window sat around a quarter full. A meter pinned at 99%
|
|
986
|
+
// for most of a run answers no question at all.
|
|
987
|
+
//
|
|
988
|
+
// Measured, not extrapolated: this is the same array the request is
|
|
989
|
+
// built from, one line after it is built.
|
|
990
|
+
this._contextChars = measureMessages(slidingWindow);
|
|
991
|
+
this._contextLimitChars = MAX_CHAR_POOL;
|
|
992
|
+
|
|
993
|
+
// Real compaction: physically collapse the amputated range so the array
|
|
994
|
+
// stops growing. Index 0 (the Root Anchor / original user intent) is
|
|
995
|
+
// never compacted away.
|
|
996
|
+
if (splitIndex >= 1) {
|
|
997
|
+
const dropped = this.messages.slice(1, splitIndex + 1);
|
|
998
|
+
if (dropped.length > 0) {
|
|
999
|
+
const removed = compactInPlace(this.messages, splitIndex, describeDroppedMessages(dropped));
|
|
1000
|
+
this._lastCompactionIndex = null;
|
|
1001
|
+
this.debugLog(`compacted ${removed} messages out of the live context`);
|
|
1002
|
+
|
|
1003
|
+
// V5 Cortex tiered condensation. Tier 1 (structural, free) always
|
|
1004
|
+
// runs so the Digital Brain keeps learning on every provider. Tier 2
|
|
1005
|
+
// (semantic, billed) is capped per task and fully tracked.
|
|
1006
|
+
condenseAndSave(dropped, this.utilityProvider(), this.tokenTracker, {
|
|
1007
|
+
enableLlmPass: this.config.enableCortexCondenser !== false,
|
|
1008
|
+
maxLlmPasses: this.config.maxCortexCondenserPasses ?? 3,
|
|
1009
|
+
passesUsed: this._cortexPassesUsed
|
|
1010
|
+
}).then(outcome => {
|
|
1011
|
+
if (outcome.semantic) this._cortexPassesUsed++;
|
|
1012
|
+
}).catch(() => {});
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Resolve the active toolset before assembling the prompt: the task
|
|
1017
|
+
// brief only cites tools the model can actually see right now.
|
|
1018
|
+
// Providers with native schema deferral (Anthropic: `defer_loading` +
|
|
1019
|
+
// `tool_search_tool_regex`) keep the whole core in context for free —
|
|
1020
|
+
// the API holds the schemas server-side and expands them on demand,
|
|
1021
|
+
// with no discovery round trip. Every OTHER provider was serialising
|
|
1022
|
+
// all 24 core schemas into every request: 3,341 tokens to advertise
|
|
1023
|
+
// tools a typical turn never calls. There, ship the resident 8 (1,059
|
|
1024
|
+
// tokens) and let `tool_search` — the same mechanism that already
|
|
1025
|
+
// unlocks packs — fetch the rest.
|
|
1026
|
+
const residentOnly = this.llm.supportsToolSearch?.() !== true;
|
|
1027
|
+
let dynamicTools = await getDynamicTools(this.config.autonomyMode, this.llm.supportsVision, {
|
|
1028
|
+
unlockedPacks: this.unlockedPacks,
|
|
1029
|
+
unlockedTools: this.unlockedTools,
|
|
1030
|
+
unlockOrder: this.unlockOrder,
|
|
1031
|
+
grantAllTools: this.config.grantAllTools === true,
|
|
1032
|
+
residentOnly
|
|
1033
|
+
});
|
|
1034
|
+
// A delegated subagent runs with a declared toolScope. Enforcing it
|
|
1035
|
+
// here — the one place the catalogue is assembled — is what makes the
|
|
1036
|
+
// scope real; the previous implementation accepted the argument and
|
|
1037
|
+
// ignored it entirely (finding D-7).
|
|
1038
|
+
if (this.toolScope instanceof Set && this.toolScope.size > 0) {
|
|
1039
|
+
dynamicTools = dynamicTools.filter(t => this.toolScope.has(t.name));
|
|
1040
|
+
}
|
|
1041
|
+
const dynamicToolNames = dynamicTools.map(t => t.name);
|
|
1042
|
+
|
|
1043
|
+
// Dynamically inject the absolute latest System context
|
|
1044
|
+
const autonomyMode = this.config.autonomyMode || 'C';
|
|
1045
|
+
const allowedPaths = this.sandbox.grantAllAccess
|
|
1046
|
+
? "/ (ALL ACCESS GRANTED - You can read/write anywhere EXCEPT system protected folders like /etc or /System)"
|
|
1047
|
+
: (this.sandbox.workspacePaths ? this.sandbox.workspacePaths.join(', ') : process.cwd());
|
|
1048
|
+
const providerPrompt = getProviderSpecificPrompt(this.config.provider);
|
|
1049
|
+
const memoryContext = getMemoryContextString();
|
|
1050
|
+
|
|
1051
|
+
// --- PER-TASK STATIC CONTEXT (computed once, not once per step) ---
|
|
1052
|
+
// Cortex retrieval, design detection, creative-pattern lookup and the
|
|
1053
|
+
// workspace domain scan are all constant for the duration of a task, but
|
|
1054
|
+
// used to run on every single iteration: a SQLite query, several
|
|
1055
|
+
// directory scans and a skills-index rebuild per step. They also churned
|
|
1056
|
+
// the prompt prefix whenever a result shifted, costing cache hits.
|
|
1057
|
+
if (!this.staticContext) {
|
|
1058
|
+
this.staticContext = await this.buildStaticContext();
|
|
1059
|
+
}
|
|
1060
|
+
const { lessonsLearned, designContext, antiRepeatDirective, universalSkillsContext, planHint } = this.staticContext;
|
|
1061
|
+
|
|
1062
|
+
// --- WORKSPACE AWARENESS ---
|
|
1063
|
+
// perception.buildContext() output used to be computed and discarded,
|
|
1064
|
+
// leaving the agent blind and forcing it to rediscover the project with
|
|
1065
|
+
// ls/cat on every task.
|
|
1066
|
+
let workspaceSummary = '';
|
|
1067
|
+
if (this.workspaceContext) {
|
|
1068
|
+
const ctx = this.workspaceContext;
|
|
1069
|
+
const facts = [
|
|
1070
|
+
ctx.cwd && `Working directory: ${ctx.cwd}`,
|
|
1071
|
+
ctx.os && `OS: ${ctx.os}`,
|
|
1072
|
+
ctx.framework && `Framework: ${ctx.framework}`,
|
|
1073
|
+
ctx.projectType && `Project type: ${ctx.projectType}`,
|
|
1074
|
+
ctx.packageManager && `Package manager: ${ctx.packageManager}`,
|
|
1075
|
+
ctx.gitBranch && `Git branch: ${ctx.gitBranch}`,
|
|
1076
|
+
ctx.gitStatus && `Git status: ${ctx.gitStatus}`,
|
|
1077
|
+
Array.isArray(ctx.keyFiles) && ctx.keyFiles.length > 0 && `Key files: ${ctx.keyFiles.slice(0, 25).join(', ')}`
|
|
1078
|
+
].filter(Boolean);
|
|
1079
|
+
if (facts.length > 0) {
|
|
1080
|
+
workspaceSummary = `\n\n📂 WORKSPACE CONTEXT (already scanned for you — do NOT waste steps rediscovering this):\n${facts.join('\n')}`;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// --- PROMPT ASSEMBLY, ORDERED STABLE → VOLATILE ---
|
|
1085
|
+
// Every provider's prompt cache binds on an identical prefix. Anthropic
|
|
1086
|
+
// marks it explicitly with cache_control (up to 4 breakpoints); OpenAI,
|
|
1087
|
+
// DeepSeek and Gemini match the longest stable prefix automatically. So
|
|
1088
|
+
// each block that is constant within a task carries its own cache anchor,
|
|
1089
|
+
// and the ONLY bytes that can change between steps live in a single
|
|
1090
|
+
// volatile block kept dead last. Previously just [1] was anchored, so the
|
|
1091
|
+
// pack catalogue and the whole workspace/memory/skills context were
|
|
1092
|
+
// re-billed on every step even though they never changed mid-task.
|
|
1093
|
+
const activePlaybooks = [...this.unlockedPacks]
|
|
1094
|
+
.map(pack => PACK_PLAYBOOKS[pack])
|
|
1095
|
+
.filter(Boolean)
|
|
1096
|
+
.join('\n\n');
|
|
1097
|
+
|
|
1098
|
+
// The task brief turns "here are 28 tools" into "here is the order to
|
|
1099
|
+
// use them in", which is where whole exploratory steps get saved.
|
|
1100
|
+
const taskBrief = this.taskShape
|
|
1101
|
+
? buildTaskBrief(this.taskShape, dynamicToolNames)
|
|
1102
|
+
: '';
|
|
1103
|
+
|
|
1104
|
+
// Uncertainty gate. Every input is already in hand — no sampling, no
|
|
1105
|
+
// second model, no network. Returns '' for a confident step, and that
|
|
1106
|
+
// empty string is the point: reflecting on every step is the waste the
|
|
1107
|
+
// gating literature exists to prevent.
|
|
1108
|
+
let uncertaintyDirective = '';
|
|
1109
|
+
let uncertaintyTemperatureDelta = 0;
|
|
1110
|
+
try {
|
|
1111
|
+
const lastAssistant = [...this.messages].reverse()
|
|
1112
|
+
.find((m) => m.role === 'assistant' && typeof m.content === 'string');
|
|
1113
|
+
const vfeStatus = this.vfeTracker ? this.vfeTracker.getStatus() : { isPlateau: false };
|
|
1114
|
+
const isVfePlateau = Boolean(this._paradigmShiftPending || vfeStatus.isPlateau);
|
|
1115
|
+
|
|
1116
|
+
const verdict = assessStep({
|
|
1117
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
1118
|
+
lastResult: this._lastToolResult,
|
|
1119
|
+
recentTools: (this._planSteps || []).map((step) => step.tool),
|
|
1120
|
+
planSimilarity: this._planSimilarity,
|
|
1121
|
+
lessonSimilarity: this._lessonSimilarity,
|
|
1122
|
+
vfePlateau: isVfePlateau,
|
|
1123
|
+
assistantText: lastAssistant ? lastAssistant.content : ''
|
|
1124
|
+
});
|
|
1125
|
+
uncertaintyDirective = verdict.directive;
|
|
1126
|
+
uncertaintyTemperatureDelta = verdict.temperatureDelta || 0;
|
|
1127
|
+
if (isVfePlateau) {
|
|
1128
|
+
this._paradigmShiftPending = false;
|
|
1129
|
+
}
|
|
1130
|
+
if (verdict.directive) {
|
|
1131
|
+
this.debugLog(`uncertainty ${verdict.uncertainty.toFixed(2)} (${verdict.level}): ${verdict.signals.join(', ')}`);
|
|
1132
|
+
auditLog.log({
|
|
1133
|
+
event: 'uncertainty_gate', taskId: this.taskId, step: this.stepsCompleted,
|
|
1134
|
+
level: verdict.level, score: Number(verdict.uncertainty.toFixed(3)), signals: verdict.signals
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
} catch (e) {
|
|
1138
|
+
this.debugLog(`uncertainty gate skipped: ${e.message}`);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const systemMessages = [
|
|
1142
|
+
// [1] stable for the entire task — the primary cache anchor.
|
|
1143
|
+
{ role: 'system', content: SYSTEM_PROMPT, cacheAnchor: true },
|
|
1144
|
+
// [2] changes only when a capability pack unlocks (append-only), so it
|
|
1145
|
+
// is stable within a task and gets its own breakpoint.
|
|
1146
|
+
{
|
|
1147
|
+
role: 'system',
|
|
1148
|
+
content: `${taskBrief ? taskBrief + '\n\n' : ''}${getPackCatalogue(this.unlockedPacks)}${activePlaybooks ? '\n\n' + activePlaybooks : ''}`,
|
|
1149
|
+
cacheAnchor: true
|
|
1150
|
+
},
|
|
1151
|
+
// [3] per-task STATIC context — provider activation, detected design,
|
|
1152
|
+
// the one-time workspace scan, Cortex lessons, creative-diversity and
|
|
1153
|
+
// the skills index. All from this.staticContext (computed once) so it
|
|
1154
|
+
// is byte-identical every step; cached.
|
|
1155
|
+
{
|
|
1156
|
+
role: 'system',
|
|
1157
|
+
content: `${providerPrompt}${designContext}${workspaceSummary}${lessonsLearned}${planHint ? '\n' + planHint + '\n' : ''}${antiRepeatDirective}\n${universalSkillsContext}`,
|
|
1158
|
+
cacheAnchor: true
|
|
1159
|
+
},
|
|
1160
|
+
// [4] THE INVOKED SKILL, when the user named one.
|
|
1161
|
+
//
|
|
1162
|
+
// This is what makes `/skills <id> <instruction>` mean anything. Before
|
|
1163
|
+
// it existed the command printed the skill to the terminal and called
|
|
1164
|
+
// `continue` — the body never entered a request, the trailing
|
|
1165
|
+
// instruction was dropped on the floor, and no API call was made at
|
|
1166
|
+
// all. "Activating" a skill was a document viewer.
|
|
1167
|
+
//
|
|
1168
|
+
// Its own message rather than appended to [3]: [3] is the per-task
|
|
1169
|
+
// static anchor and appending here would invalidate that cache every
|
|
1170
|
+
// time a different skill is invoked. This one is stable for the task,
|
|
1171
|
+
// so it anchors too.
|
|
1172
|
+
...(skillSystemMessage(this.activeSkill) || []),
|
|
1173
|
+
// [5] VOLATILE tail — the only bytes that may differ between steps:
|
|
1174
|
+
// autonomy/workspace paths (change via config) and persistent memory
|
|
1175
|
+
// (grows via memory_write mid-task). Kept LAST and UNcached so a change
|
|
1176
|
+
// here never invalidates the cached prefix in [1]-[3].
|
|
1177
|
+
{
|
|
1178
|
+
role: 'system',
|
|
1179
|
+
content: `${uncertaintyDirective ? uncertaintyDirective + '\n\n' : ''}${describeSession(this.taskId)}CURRENT AUTONOMY MODE: ${autonomyMode}\nALLOWED WORKSPACE PATHS: ${allowedPaths}\n(You MUST NOT write files outside these paths!)\n\n🧠 PERSISTENT MEMORY (FACTS LEARNED ABOUT THE USER):\n${memoryContext}`
|
|
1180
|
+
}
|
|
1181
|
+
];
|
|
1182
|
+
|
|
1183
|
+
// Dev-only visibility into what fills each step's prompt. Set
|
|
1184
|
+
// CAUSYNE_TOKEN_DEBUG=1 to see the cached prefix ([1]-[3]) vs the volatile
|
|
1185
|
+
// tail ([4]) and confirm the cache is doing its job (pair it with the
|
|
1186
|
+
// "Prompt cache hit" line logged after the call).
|
|
1187
|
+
if (process.env.CAUSYNE_TOKEN_DEBUG === '1') {
|
|
1188
|
+
const chars = (m) => (typeof m?.content === 'string' ? m.content.length : JSON.stringify(m?.content || '').length);
|
|
1189
|
+
const windowChars = slidingWindow.reduce((sum, m) => sum + chars(m), 0);
|
|
1190
|
+
this.debugLog(
|
|
1191
|
+
`[token] sys1_base=${chars(systemMessages[0])} sys2_packs=${chars(systemMessages[1])} `
|
|
1192
|
+
+ `sys3_static(cached)=${chars(systemMessages[2])} sys4_volatile=${chars(systemMessages[3])} `
|
|
1193
|
+
+ `tools=${JSON.stringify(dynamicTools).length} window=${windowChars} (chars)`
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// --- SOVEREIGN ROOT ANCHOR PRESERVATION ---
|
|
1198
|
+
// Ensure the original User Blueprint / Root Intent (`this.messages[0]`) is NEVER amputated
|
|
1199
|
+
// by the sliding window, regardless of how many steps or tool outputs accumulate.
|
|
1200
|
+
const rootAnchor = [];
|
|
1201
|
+
// The `slidingWindow.length > 0` guard is GONE on purpose. It meant an
|
|
1202
|
+
// empty window skipped the anchor too, and a request of nothing but
|
|
1203
|
+
// system blocks is a 400 on every provider. The window can no longer be
|
|
1204
|
+
// empty (contextManager.buildWindow), but the anchor must not depend on
|
|
1205
|
+
// that staying true.
|
|
1206
|
+
if (this.messages.length > 0 && slidingWindow[0] !== this.messages[0] && this.messages[0].role === 'user') {
|
|
1207
|
+
rootAnchor.push(this.messages[0]);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Final Assembly for the LLM. sanitizeSequences guarantees every
|
|
1211
|
+
// assistant tool_calls block is followed by matching tool results and
|
|
1212
|
+
// drops orphans that would otherwise 400 the provider.
|
|
1213
|
+
let prunedMessages = sanitizeSequences([...systemMessages, ...rootAnchor, ...slidingWindow]);
|
|
1214
|
+
|
|
1215
|
+
// THE INVARIANT, ENFORCED AT THE LAST POSSIBLE MOMENT.
|
|
1216
|
+
//
|
|
1217
|
+
// Every provider rejects a request whose messages carry no `user` or
|
|
1218
|
+
// `tool` turn. Two separate paths above can produce one — an empty
|
|
1219
|
+
// window, and sanitizeSequences dropping orphaned tool results — and
|
|
1220
|
+
// relying on either to be correct forever is how a 340,000-token run
|
|
1221
|
+
// died at step 13 with nothing to show for it. Checked here because
|
|
1222
|
+
// this is the last place the shape is still ours to fix.
|
|
1223
|
+
if (!prunedMessages.some(m => m.role === 'user' || m.role === 'tool')) {
|
|
1224
|
+
const root = this.messages.find(m => m.role === 'user');
|
|
1225
|
+
prunedMessages = [
|
|
1226
|
+
...prunedMessages,
|
|
1227
|
+
root || { role: 'user', content: 'Continue the task from where you left off.' }
|
|
1228
|
+
];
|
|
1229
|
+
this.debugLog('request had no user/tool message; re-anchored before sending');
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// BUDGET CEILING, CHECKED BEFORE SPENDING RATHER THAN AFTER.
|
|
1233
|
+
//
|
|
1234
|
+
// trackUsage() below stops the loop once the ceiling has ALREADY been
|
|
1235
|
+
// passed, so a run always overshot by one step — with ~9,000-token
|
|
1236
|
+
// steps a 299,000 ceiling landed near 308,000. Asking first is what
|
|
1237
|
+
// makes a ceiling mean the number the user typed.
|
|
1238
|
+
const affordability = this.tokenTracker.canAffordAnotherStep();
|
|
1239
|
+
if (!affordability.allowed) {
|
|
1240
|
+
this.emitEvent({ type: 'budget_warning', message: affordability.reason });
|
|
1241
|
+
this.emitEvent({ type: 'notice', message: 'Stopped before the token ceiling. Nothing was truncated mid-step.' });
|
|
1242
|
+
// A plan that was offered and did NOT carry the task through loses
|
|
1243
|
+
// rank, so a stale template stops being suggested forever. Only the
|
|
1244
|
+
// denominator moves here — recordPlan() is what awards a win.
|
|
1245
|
+
if (this._offeredPlanId) {
|
|
1246
|
+
try { recordMiss(this._offeredPlanId); } catch (e) { /* never block a stop */ }
|
|
1247
|
+
}
|
|
1248
|
+
auditLog.log({
|
|
1249
|
+
event: 'task_stopped_on_budget', taskId: this.taskId,
|
|
1250
|
+
used: affordability.used, limit: affordability.limit, projected: affordability.projected
|
|
1251
|
+
});
|
|
1252
|
+
break;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// 2. PLAN (Call LLM). dynamicTools was resolved above so the task brief
|
|
1256
|
+
// could reference the tools actually in context.
|
|
1257
|
+
const response = await this.llm.chat(prunedMessages, {
|
|
1258
|
+
tools: dynamicTools,
|
|
1259
|
+
// IARS couples escalation to a wider search. Bounded so a stuck task
|
|
1260
|
+
// cannot climb into randomness across repeated escalations.
|
|
1261
|
+
temperature: Math.min(1, this.taskTemperature + uncertaintyTemperatureDelta),
|
|
1262
|
+
coreToolCount: dynamicTools.filter(t => isCoreTool(t.name)).length
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
// The call came back. Whatever went wrong before is no longer
|
|
1266
|
+
// consecutive — without this reset, one failure at step 2 and another
|
|
1267
|
+
// at step 200 would count toward the same ceiling and stop a task that
|
|
1268
|
+
// was working perfectly well.
|
|
1269
|
+
this.consecutiveFailures = 0;
|
|
1270
|
+
this.lastFailureType = null;
|
|
1271
|
+
|
|
1272
|
+
// Track tokens. usage carries cachedPromptTokens / cacheWriteTokens so
|
|
1273
|
+
// the cost estimate reflects the real cache-discounted bill.
|
|
1274
|
+
const tracking = await this.tokenTracker.trackUsage(
|
|
1275
|
+
response.usage.promptTokens,
|
|
1276
|
+
response.usage.completionTokens,
|
|
1277
|
+
(msg) => this.emitEvent({ type: 'budget_warning', message: msg }),
|
|
1278
|
+
response.usage
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1281
|
+
const cachedTokens = response.usage.cachedPromptTokens || 0;
|
|
1282
|
+
if (cachedTokens > 0) {
|
|
1283
|
+
this.debugLog(`prompt cache hit: ${cachedTokens.toLocaleString()} of ${response.usage.promptTokens.toLocaleString()} input tokens served from cache`);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
//
|
|
1288
|
+
|
|
1289
|
+
// ALWAYS read (AgentViewModel: `optInt("dailyTokens", optInt("usedTokens",
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
// — which this frame set to the TASK total. The card therefore drew task
|
|
1293
|
+
// tokens against `maxTokensPerDay` and read 29% while the daily limiter
|
|
1294
|
+
// fired at 1,001,877 / 1,000,000. The contract existed on one side only.
|
|
1295
|
+
//
|
|
1296
|
+
// Context is reported in characters against the character pool, the unit
|
|
1297
|
+
// the window is actually budgeted in. The renderer converts once, using
|
|
1298
|
+
// the project's own CHAR_POOLS convention, instead of comparing a token
|
|
1299
|
+
// count to a character limit.
|
|
1300
|
+
this.emitEvent({
|
|
1301
|
+
type: 'usage_update',
|
|
1302
|
+
usedTokens: tracking.dailyTokens || 0,
|
|
1303
|
+
dailyTokens: tracking.dailyTokens || 0,
|
|
1304
|
+
dailyCostUsd: tracking.dailyCostUsd || 0,
|
|
1305
|
+
dailyLimit: tracking.dailyLimit || 0,
|
|
1306
|
+
taskTokens: tracking.taskTokens || 0,
|
|
1307
|
+
taskCostUsd: tracking.taskCostUsd || 0,
|
|
1308
|
+
taskLimit: tracking.taskLimit || 0,
|
|
1309
|
+
costUsd: tracking.costUsd || 0,
|
|
1310
|
+
lifetimeTokens: tracking.lifetimeTokens || 0,
|
|
1311
|
+
lifetimeCostUsd: tracking.lifetimeCostUsd || 0,
|
|
1312
|
+
contextChars: this._contextChars || 0,
|
|
1313
|
+
contextLimitChars: this._contextLimitChars || 0,
|
|
1314
|
+
cachedTokens
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
if (!tracking.allowed) {
|
|
1318
|
+
this.emitEvent({ type: 'error', message: tracking.reason });
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Add assistant response to history. This is the ONLY place the turn is
|
|
1323
|
+
// recorded — the completion and guard branches below used to push it a
|
|
1324
|
+
// second time, duplicating every finished turn in the history.
|
|
1325
|
+
const hasToolCalls = Boolean(response.toolCalls && response.toolCalls.length > 0);
|
|
1326
|
+
const assistantMsg = {
|
|
1327
|
+
role: 'assistant',
|
|
1328
|
+
// An assistant message with neither text nor tool calls is rejected by
|
|
1329
|
+
// some providers, so a terminal turn always carries something.
|
|
1330
|
+
content: response.content || (hasToolCalls ? '' : 'Task completed.')
|
|
1331
|
+
};
|
|
1332
|
+
if (hasToolCalls) {
|
|
1333
|
+
assistantMsg.tool_calls = response.toolCalls;
|
|
1334
|
+
}
|
|
1335
|
+
if (response.reasoningContent) {
|
|
1336
|
+
assistantMsg.reasoning_content = response.reasoningContent;
|
|
1337
|
+
// The user asked to see the model think; show it rather than letting
|
|
1338
|
+
|
|
1339
|
+
this.emitEvent({ type: 'agent_reasoning', content: response.reasoningContent });
|
|
1340
|
+
}
|
|
1341
|
+
// Kept verbatim on the turn. A thinking-enabled model that gets its own
|
|
1342
|
+
// thinking blocks back is continuing a thought; one that does not is
|
|
1343
|
+
// starting over, and with tool_use in the same turn the API can reject
|
|
1344
|
+
// the sequence outright.
|
|
1345
|
+
if (response.thinkingBlocks) {
|
|
1346
|
+
assistantMsg.thinking_blocks = response.thinkingBlocks;
|
|
1347
|
+
}
|
|
1348
|
+
this.messages.push(assistantMsg);
|
|
1349
|
+
|
|
1350
|
+
if (response.content) {
|
|
1351
|
+
// V5: Parse TodoWrite checklist for visual plan display
|
|
1352
|
+
const planRegex = /-\s*\[([ xX\/])\]\s*(.*)/g;
|
|
1353
|
+
const planMatch = [...response.content.matchAll(planRegex)];
|
|
1354
|
+
if (planMatch.length > 0) {
|
|
1355
|
+
const planTasks = planMatch.map(m => {
|
|
1356
|
+
const marker = m[1].toLowerCase();
|
|
1357
|
+
const status = marker === 'x' ? 'completed' : (marker === '/' ? 'in_progress' : 'pending');
|
|
1358
|
+
return { status, text: m[2].trim() };
|
|
1359
|
+
});
|
|
1360
|
+
this.emitEvent({ type: 'agent_plan', plan: planTasks });
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
this.emitEvent({ type: 'agent_message', content: response.content });
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// If no tool calls, check if the agent is just thinking/planning or actually done
|
|
1367
|
+
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
1368
|
+
const contentText = response.content || '';
|
|
1369
|
+
const lower = contentText.toLowerCase();
|
|
1370
|
+
const thinkingKeywords = ['let me ', "i'll ", 'i will ', 'now i ', 'next, i ', 'first, i ', 'checking ', 'trying to ', 'let us ', 'going to '];
|
|
1371
|
+
const isThinking = thinkingKeywords.some(kw => lower.includes(kw));
|
|
1372
|
+
|
|
1373
|
+
if (isThinking && this.consecutiveThinkingSteps < 3) {
|
|
1374
|
+
this.consecutiveThinkingSteps++;
|
|
1375
|
+
this.emitEvent({ type: 'status', message: 'Agent planning next steps...' });
|
|
1376
|
+
// assistantMsg was already pushed above — pushing it again here
|
|
1377
|
+
// duplicated the whole turn in history on every nudge.
|
|
1378
|
+
this.messages.push({
|
|
1379
|
+
role: 'system',
|
|
1380
|
+
content: '[SYSTEM NOTICE: You provided reasoning/planning without calling any tools. Please invoke the next appropriate tool now to continue progressing on the task.]'
|
|
1381
|
+
});
|
|
1382
|
+
this.memoryManager.saveHistory(this.messages);
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// --- COMPLETION GUARDS ---
|
|
1387
|
+
// Two hard rules learned the expensive way:
|
|
1388
|
+
// 1. Match on word boundaries. Substring matching made 'ui' fire on
|
|
1389
|
+
// "build"/"quick"/"requirements" and 'race' fire on "trace",
|
|
1390
|
+
// dragging ordinary scripting tasks into game/web enforcement.
|
|
1391
|
+
// 2. Fire at most once per task. These used to be gated on
|
|
1392
|
+
// consecutiveThinkingSteps, which resets on every tool call, so
|
|
1393
|
+
// a guard could block completion indefinitely and burn the whole
|
|
1394
|
+
// step budget on an already-finished task.
|
|
1395
|
+
const taskPrompt = this.currentTaskPrompt || this.userPrompt || '';
|
|
1396
|
+
const isGameTask = isGameIntent(taskPrompt);
|
|
1397
|
+
|
|
1398
|
+
this.gameToolStats = this.gameToolStats || { godot_act: 0, game_asset_fetch: 0, game_build_html5: 0, last_godot_action: null };
|
|
1399
|
+
|
|
1400
|
+
if (isGameTask && !this.gameGuardFired) {
|
|
1401
|
+
const buildingRealGame = this.gameCascadeState.currentStage > 1 || this.stepsCompleted > 0;
|
|
1402
|
+
const needsVerification = buildingRealGame && !this.gameCascadeState.verificationPassed && this.gameCascadeState.currentStage >= 7;
|
|
1403
|
+
// Production-depth enforcement is opt-in. It is right for
|
|
1404
|
+
// "build me a full RPG" and wrong for "fix the jump height in my game".
|
|
1405
|
+
const needsDepth = this.config.enforceGameDepth === true && this.stepsCompleted < 50;
|
|
1406
|
+
|
|
1407
|
+
if (needsVerification || needsDepth) {
|
|
1408
|
+
this.gameGuardFired = true;
|
|
1409
|
+
this.emitEvent({ type: 'status', message: '⚡ Sovereign Game Guard: Enforcing Production Depth & Quality Gate...' });
|
|
1410
|
+
|
|
1411
|
+
const guardMsg = needsDepth && !needsVerification
|
|
1412
|
+
? `[SOVEREIGN DEPTH BLOCK: You are building a Full Production Game but only took ${this.stepsCompleted} steps. A full production game requires 100+ steps. Expand the game with more levels, richer UI, complex enemy AI, or more assets before finishing. This check fires once — if the task genuinely is complete, say so plainly and finish.]`
|
|
1413
|
+
: `[SOVEREIGN QUALITY GATE: You built a game but declared completion without verifying it. Run \`game_web_runtime_inspector\` and \`game_canvas_audit\` to confirm runtime stability (FPS >= 30, no missing sprites) before exiting. This check fires once — if verification is genuinely not applicable, say so plainly and finish.]`;
|
|
1414
|
+
|
|
1415
|
+
this.messages.push({ role: 'system', content: guardMsg });
|
|
1416
|
+
this.memoryManager.saveHistory(this.messages);
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// P6 — ANTI-SLOP GATE (Vision §7, ENFORCED)
|
|
1422
|
+
//
|
|
1423
|
+
// The visual guard below makes the agent LOOK at its work, but the
|
|
1424
|
+
// agent was the only judge of what it saw — and a model that just
|
|
1425
|
+
// emitted a purple gradient hero does not reliably call it generic.
|
|
1426
|
+
// This reads the HTML/CSS actually written and scores it. Weighted and
|
|
1427
|
+
// threshold-based, never a single tripwire: a guard with false
|
|
1428
|
+
// positives gets switched off (the ZM-09 lesson).
|
|
1429
|
+
if (!this.slopGateFired && this.webArtifacts.size > 0) {
|
|
1430
|
+
this.slopGateFired = true; // fires at most once per task, like the others
|
|
1431
|
+
try {
|
|
1432
|
+
const files = [];
|
|
1433
|
+
for (const artifactPath of this.webArtifacts) {
|
|
1434
|
+
try {
|
|
1435
|
+
files.push({ path: artifactPath, content: fsSync.readFileSync(artifactPath, 'utf8') });
|
|
1436
|
+
} catch (e) { /* deleted or moved since: nothing to judge */ }
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const result = analyzeFiles(files);
|
|
1440
|
+
if (result.verdict === 'slop') {
|
|
1441
|
+
const recent = Array.isArray(this.staticContext?.recentMovements) ? this.staticContext.recentMovements : [];
|
|
1442
|
+
const brief = formatMovementBrief(suggestMovements(recent, 3));
|
|
1443
|
+
|
|
1444
|
+
auditLog.log({
|
|
1445
|
+
event: 'anti_slop_block', taskId: this.taskId,
|
|
1446
|
+
score: result.score, signals: result.findings.map(f => f.id)
|
|
1447
|
+
});
|
|
1448
|
+
this.emitEvent({ type: 'status', message: `⚡ Anti-Slop Gate: design scored ${result.score} — rebuilding.` });
|
|
1449
|
+
this.messages.push({ role: 'system', content: formatFindings(result, brief) });
|
|
1450
|
+
this.memoryManager.saveHistory(this.messages);
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (result.findings.length > 0) {
|
|
1455
|
+
auditLog.log({
|
|
1456
|
+
event: 'anti_slop_pass', taskId: this.taskId,
|
|
1457
|
+
score: result.score, verdict: result.verdict,
|
|
1458
|
+
signals: result.findings.map(f => f.id)
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
} catch (e) {
|
|
1462
|
+
// A quality gate must never be the reason a task fails.
|
|
1463
|
+
this.debugLog(`anti-slop gate skipped: ${e.message}`);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// Web Visual Validation Guard (Anti-Slop / Transcendence)
|
|
1468
|
+
const isWebTask = isWebIntent(taskPrompt);
|
|
1469
|
+
|
|
1470
|
+
if (isWebTask && !this.webGuardFired && this.stepsCompleted > 0) {
|
|
1471
|
+
const hasVision = this.llm.supportsVision !== false;
|
|
1472
|
+
// Finding D-6: this guard used to demand `screenshot` /
|
|
1473
|
+
// `vision_inspect_ui` unconditionally. Both need the browser and
|
|
1474
|
+
// capture packages, which are provisioned only after the user
|
|
1475
|
+
// consents — so on a fresh install the guard was ordering the agent
|
|
1476
|
+
// to use a tool that could not run. Ask for the capability once;
|
|
1477
|
+
// if it was declined, fall through to the path that needs nothing.
|
|
1478
|
+
let visionReady = false;
|
|
1479
|
+
let visionDeclined = false;
|
|
1480
|
+
try {
|
|
1481
|
+
const { groupStatus, isGroupDeclined } = await import('./provisioning/capabilityProvisioner.js');
|
|
1482
|
+
visionReady = groupStatus('vision').state === 'ready';
|
|
1483
|
+
visionDeclined = isGroupDeclined('vision');
|
|
1484
|
+
} catch (e) { /* treat as not ready */ }
|
|
1485
|
+
let webGuardMsg = '';
|
|
1486
|
+
|
|
1487
|
+
if (hasVision && !visionReady && !visionDeclined) {
|
|
1488
|
+
webGuardMsg = `[ANTI-SLOP CHECK: You are building a Web/UI project and must look at it before finishing, but the browser and screen-capture capability is not enabled yet. Call \`manage_local_dependencies\` with action "grant_capability" and group "vision" to request it from the user. If the user declines, read the final compiled HTML/CSS instead and verify it that way. This check fires once.]`;
|
|
1489
|
+
} else if (hasVision && visionReady && !this.webVerificationState.screenshotTaken) {
|
|
1490
|
+
webGuardMsg = `[ANTI-SLOP CHECK: You are building a Web/UI project but attempted to finish without VISUALLY verifying your work. Use \`screenshot\` or \`vision_inspect_ui\` to look at the UI in a browser. A human looks at their work to avoid slop. This check fires once — if there is genuinely nothing visual to inspect, say so plainly and finish.]`;
|
|
1491
|
+
} else if ((!hasVision || visionDeclined) && !this.webVerificationState.browserActUsed && !this.webVerificationState.screenshotTaken) {
|
|
1492
|
+
webGuardMsg = `[TRANSCENDENCE CHECK: You are building a Web/UI project. As a vision-disabled model you cannot screenshot, but you MUST use \`browser_act\` to inspect the DOM, or read the final compiled HTML/CSS, to verify semantic correctness before finishing. This check fires once.]`;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
if (webGuardMsg) {
|
|
1496
|
+
this.webGuardFired = true;
|
|
1497
|
+
// The verification tools live in the web pack; make sure they are
|
|
1498
|
+
// actually in context before demanding the agent use them.
|
|
1499
|
+
this.unlockPack('web');
|
|
1500
|
+
this.emitEvent({ type: 'status', message: '⚡ Anti-Slop Guard: Forcing visual/semantic verification...' });
|
|
1501
|
+
this.messages.push({ role: 'system', content: webGuardMsg });
|
|
1502
|
+
this.memoryManager.saveHistory(this.messages);
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
this.emitEvent({ type: 'status', message: 'Task completed.' });
|
|
1508
|
+
this.memoryManager.saveHistory(this.messages); // Save final state
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
// The task worked. Keep the route so the next one like it does not
|
|
1513
|
+
// have to rediscover it. Deterministic: we already watched every step
|
|
1514
|
+
// run, so asking a model to summarise it would be paying for a worse
|
|
1515
|
+
// copy of a fact we hold.
|
|
1516
|
+
try {
|
|
1517
|
+
const stored = recordPlan({
|
|
1518
|
+
prompt: this.currentTaskPrompt || this.userPrompt,
|
|
1519
|
+
archetype: this.taskShape ? this.taskShape.archetype : '',
|
|
1520
|
+
steps: this._planSteps || []
|
|
1521
|
+
});
|
|
1522
|
+
if (stored) this.debugLog(`plan cached: ${stored.steps.length} steps, ${stored.wins}/${stored.uses} wins`);
|
|
1523
|
+
} catch (e) {
|
|
1524
|
+
this.debugLog(`plan cache write skipped: ${e.message}`);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
auditLog.log({ event: 'task_complete', taskId: this.taskId, steps: this.stepsCompleted });
|
|
1528
|
+
this.emitEvent({ type: 'task_complete', taskId: this.taskId });
|
|
1529
|
+
break;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
this.consecutiveThinkingSteps = 0; // Reset thinking counter when tools are called
|
|
1533
|
+
|
|
1534
|
+
// 3a. PREFETCH — run the independent read-only calls of this batch
|
|
1535
|
+
// concurrently (finding D-8).
|
|
1536
|
+
//
|
|
1537
|
+
// Every competitive agent batches independent reads; this loop executed
|
|
1538
|
+
// them strictly one after another, so six `fs_read`s cost six full
|
|
1539
|
+
// round trips of tool latency.
|
|
1540
|
+
//
|
|
1541
|
+
// The serial loop below is deliberately left EXACTLY as it was. Nothing
|
|
1542
|
+
// about ordering, approval gates, step counting, streaming, retries or
|
|
1543
|
+
// error classification changes — the only difference is that a
|
|
1544
|
+
// read-only result may already be sitting in this map by the time the
|
|
1545
|
+
// loop reaches it. Restructuring the 400-line body into a batch would
|
|
1546
|
+
// have been the obvious approach and a far worse one: it is dense with
|
|
1547
|
+
// `break`s, approval waits and circuit breakers, and a subtle reordering
|
|
1548
|
+
// there is exactly the class of silent defect this whole remediation
|
|
1549
|
+
// exists to clean up.
|
|
1550
|
+
//
|
|
1551
|
+
// Safety rules that make this sound:
|
|
1552
|
+
// * Only tools on the read-only allowlist are eligible. Nothing that
|
|
1553
|
+
// writes, deletes, executes or asks for approval is ever prefetched.
|
|
1554
|
+
// * The sandbox is consulted FIRST, with the same call the serial path
|
|
1555
|
+
// makes, so a denied path is never read behind the guard's back.
|
|
1556
|
+
// * Only a SUCCESSFUL prefetch is consumed. A failure is discarded and
|
|
1557
|
+
// the serial path executes normally, so the retry-with-backoff and
|
|
1558
|
+
// error-classification logic keeps its exact behaviour.
|
|
1559
|
+
const prefetched = this.prefetchReadOnlyCalls(response.toolCalls);
|
|
1560
|
+
|
|
1561
|
+
// 3. ACT (Execute tool calls)
|
|
1562
|
+
for (const toolCall of response.toolCalls) {
|
|
1563
|
+
// Canonicalise once, here. Every downstream comparison — the sandbox's
|
|
1564
|
+
// risk classification, the approval gates, the game cascade lists —
|
|
1565
|
+
// matches on this string, so if it is not the catalogue name those
|
|
1566
|
+
// checks silently never fire.
|
|
1567
|
+
const functionName = canonicalToolName(toolCall.function.name);
|
|
1568
|
+
let args = {};
|
|
1569
|
+
try {
|
|
1570
|
+
args = typeof toolCall.function.arguments === 'string'
|
|
1571
|
+
? JSON.parse(toolCall.function.arguments || '{}')
|
|
1572
|
+
: (toolCall.function.arguments || {});
|
|
1573
|
+
} catch (parseErr) {
|
|
1574
|
+
this.messages.push({
|
|
1575
|
+
role: 'tool',
|
|
1576
|
+
tool_call_id: toolCall.id,
|
|
1577
|
+
name: functionName,
|
|
1578
|
+
content: JSON.stringify({ success: false, error: `Invalid JSON in arguments: ${parseErr.message}` })
|
|
1579
|
+
});
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
this.emitEvent({ type: 'step_start', tool: functionName, args: args });
|
|
1584
|
+
this.consecutiveCommands++;
|
|
1585
|
+
|
|
1586
|
+
if (functionName === 'screenshot' || functionName === 'vision_inspect_ui') {
|
|
1587
|
+
this.webVerificationState.screenshotTaken = true;
|
|
1588
|
+
}
|
|
1589
|
+
if (functionName === 'browser_act') {
|
|
1590
|
+
this.webVerificationState.browserActUsed = true;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// Sovereign Game Pre-Flight Interceptor (v1.3.13)
|
|
1594
|
+
const isGameTaskExec = isGameIntent(this.currentTaskPrompt || this.userPrompt || '');
|
|
1595
|
+
|
|
1596
|
+
const ALL_GAME_TOOLS_LIST = [
|
|
1597
|
+
'godot_act', 'game_asset_fetch', 'game_build_html5',
|
|
1598
|
+
'game_lore_forge', 'game_dialogue_tree_gen', 'game_quest_arc_builder',
|
|
1599
|
+
'game_animation_rig', 'game_ai_behavior_tree', 'game_cinematic_sequencer',
|
|
1600
|
+
'game_asset_harvest', 'game_audio_synth', 'game_vfx_emitter_gen',
|
|
1601
|
+
'game_shader_forge', 'game_scene_build', 'game_entity_spawner',
|
|
1602
|
+
'game_tilemap_gen', 'game_ui_hud_builder', 'game_economy_balancer',
|
|
1603
|
+
'game_data_forge', 'check_system_specs', 'game_web_runtime_inspector', 'game_canvas_audit'
|
|
1604
|
+
];
|
|
1605
|
+
|
|
1606
|
+
if (ALL_GAME_TOOLS_LIST.includes(functionName)) {
|
|
1607
|
+
// Stage progression
|
|
1608
|
+
if (['check_system_specs', 'godot_act'].includes(functionName)) this.gameCascadeState.currentStage = Math.max(this.gameCascadeState.currentStage, 2);
|
|
1609
|
+
if (['game_lore_forge', 'game_dialogue_tree_gen', 'game_quest_arc_builder'].includes(functionName)) this.gameCascadeState.currentStage = Math.max(this.gameCascadeState.currentStage, 3);
|
|
1610
|
+
if (['game_animation_rig', 'game_ai_behavior_tree', 'game_cinematic_sequencer'].includes(functionName)) this.gameCascadeState.currentStage = Math.max(this.gameCascadeState.currentStage, 4);
|
|
1611
|
+
if (['game_asset_harvest', 'game_audio_synth', 'game_vfx_emitter_gen'].includes(functionName)) this.gameCascadeState.currentStage = Math.max(this.gameCascadeState.currentStage, 5);
|
|
1612
|
+
if (['game_shader_forge', 'game_scene_build', 'game_entity_spawner'].includes(functionName)) this.gameCascadeState.currentStage = Math.max(this.gameCascadeState.currentStage, 6);
|
|
1613
|
+
if (['game_tilemap_gen', 'game_ui_hud_builder', 'game_economy_balancer', 'game_build_html5'].includes(functionName)) this.gameCascadeState.currentStage = Math.max(this.gameCascadeState.currentStage, 7);
|
|
1614
|
+
|
|
1615
|
+
if (functionName === 'game_web_runtime_inspector' || functionName === 'game_canvas_audit') {
|
|
1616
|
+
this.gameCascadeState.verificationPassed = true;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
if (isGameTaskExec && ['fs_write', 'sysRun', 'terminal_persistent_shell', 'game_build_html5'].includes(functionName) && this.gameCascadeState.currentStage < 7) {
|
|
1621
|
+
const isGameBuildHtml5 = functionName === 'game_build_html5';
|
|
1622
|
+
const isCustomScriptBlock = isGameBuildHtml5 && args.template === 'custom' && args.customScript && args.customScript.length > 2000;
|
|
1623
|
+
const targetPath = (args.path || args.filepath || args.targetFile || args.command || '').toLowerCase();
|
|
1624
|
+
|
|
1625
|
+
// BUGFIX: Only block actual game file extensions to prevent false positives when creating directories (e.g., mkdir /Desktop/RacerGame)
|
|
1626
|
+
const isGameFileOrDir = targetPath.includes('.html'); // Removed .gd and .tscn to allow Godot to function
|
|
1627
|
+
const isEchoingToFile = targetPath.includes('echo') && targetPath.includes('>') && isGameFileOrDir;
|
|
1628
|
+
|
|
1629
|
+
if ((['fs_write'].includes(functionName) && isGameFileOrDir) || isEchoingToFile || isCustomScriptBlock) {
|
|
1630
|
+
const interceptMsg = `[SOVEREIGN CASCADE VIOLATION: You attempted to skip from Stage ${this.gameCascadeState.currentStage} to Stage 7 without completing Stages 3, 4, 5, and 6 modularly. DO NOT write monolithic 20 KB custom scripts!]`;
|
|
1631
|
+
this.emitEvent({ type: 'step_error', tool: functionName, error: '⚡ Sovereign Game Intercept: Cascade violation detected.' });
|
|
1632
|
+
this.messages.push({
|
|
1633
|
+
role: 'tool',
|
|
1634
|
+
tool_call_id: toolCall.id,
|
|
1635
|
+
name: functionName,
|
|
1636
|
+
content: JSON.stringify({ success: false, error: "Sovereign Cascade Violation", CRITICAL_INSTRUCTION: interceptMsg })
|
|
1637
|
+
});
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
if (functionName === 'game_build_html5' && args.template === 'custom' && args.customScript) {
|
|
1643
|
+
const scr = args.customScript;
|
|
1644
|
+
if (scr.includes('c.drw ?') && !scr.includes('drw(')) {
|
|
1645
|
+
this.messages.push({
|
|
1646
|
+
role: 'tool', tool_call_id: toolCall.id, name: functionName,
|
|
1647
|
+
content: JSON.stringify({ success: false, error: 'AST SANITIZER: Detected ternary check for c.drw but no actual drw() method definition found! Fix and retry.' })
|
|
1648
|
+
});
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
if (scr.includes('ctx.translate(-cam.x, -cam.y)') && (scr.includes('c.x - cam.x') || scr.includes('c.y - cam.y'))) {
|
|
1652
|
+
this.messages.push({
|
|
1653
|
+
role: 'tool', tool_call_id: toolCall.id, name: functionName,
|
|
1654
|
+
content: JSON.stringify({ success: false, error: 'AST SANITIZER: Detected double translation! You are translating context by cam and then manually subtracting cam again inside drawing calls!' })
|
|
1655
|
+
});
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// 4-Layer Security Sandbox Validation
|
|
1661
|
+
// 2. Sandbox Security Validation (CRITICAL FIX: Prevent Infinite Loop on Block)
|
|
1662
|
+
const validation = this.sandbox.validate(functionName, args);
|
|
1663
|
+
if (!validation.allowed) {
|
|
1664
|
+
this.consecutiveBlockedActions++;
|
|
1665
|
+
|
|
1666
|
+
// Hard Circuit Breaker (Transcendence Context Consciousness)
|
|
1667
|
+
if (this.consecutiveBlockedActions >= 4) {
|
|
1668
|
+
const fatalMsg = `FATAL ERROR: Infinite Token Drain Detected. Agent repeatedly attempted blocked/forbidden actions 4 times in a row. Forcibly aborting task to save tokens.`;
|
|
1669
|
+
this.emitEvent({ type: 'step_error', tool: functionName, error: fatalMsg });
|
|
1670
|
+
auditLog.log({ event: 'task_killed_circuit_breaker', taskId: this.taskId, reason: fatalMsg });
|
|
1671
|
+
|
|
1672
|
+
// Save memory of failure for RLTF
|
|
1673
|
+
cortexDb.saveMemory(
|
|
1674
|
+
this.currentTaskPrompt || 'Unknown task',
|
|
1675
|
+
'Agent stubbornly repeated blocked actions causing an infinite loop.',
|
|
1676
|
+
`Do NOT repeat ${functionName} with these exact arguments, as it is permanently blocked by Sandbox. Ensure you respect ALLOWED WORKSPACE PATHS.`
|
|
1677
|
+
).catch(() => {});
|
|
1678
|
+
|
|
1679
|
+
this.abort();
|
|
1680
|
+
break;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
auditLog.log({ event: 'tool_blocked', taskId: this.taskId, tool: functionName, args, reason: validation.reason });
|
|
1684
|
+
this.emitEvent({ type: 'step_error', tool: functionName, error: validation.reason });
|
|
1685
|
+
|
|
1686
|
+
// Feed error back to LLM with absolute command to stop repeating
|
|
1687
|
+
this.messages.push({
|
|
1688
|
+
role: 'tool',
|
|
1689
|
+
tool_call_id: toolCall.id,
|
|
1690
|
+
name: functionName,
|
|
1691
|
+
content: JSON.stringify({
|
|
1692
|
+
success: false,
|
|
1693
|
+
error: validation.reason,
|
|
1694
|
+
CRITICAL_INSTRUCTION: "DO NOT RETRY THIS EXACT ACTION. The Sandbox blocked it. Ensure you only write to paths inside ALLOWED WORKSPACE PATHS, or use a different approach. Repeating this will drain tokens."
|
|
1695
|
+
})
|
|
1696
|
+
});
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
this.consecutiveBlockedActions = 0; // Reset counter on allowed action
|
|
1701
|
+
|
|
1702
|
+
// Approval flow for dangerous actions (Mode A/B) or ALWAYS for heavy AI downloads
|
|
1703
|
+
// The schema name is manage_local_dependencies. Comparing against the
|
|
1704
|
+
// old alias meant this consent gate never fired and multi-GB model
|
|
1705
|
+
// downloads ran unannounced.
|
|
1706
|
+
const isHeavyAiDownload = functionName === 'manage_local_dependencies' &&
|
|
1707
|
+
['pipInst', 'fetchBin', 'fetchMod'].includes(args.action);
|
|
1708
|
+
// A standing approval from "Allow always" lifts the gate for this
|
|
1709
|
+
// tool until the process restarts. It is checked HERE rather than
|
|
1710
|
+
|
|
1711
|
+
// asking and then auto-answering would still interrupt the user,
|
|
1712
|
+
// which is the entire thing they were trying to stop.
|
|
1713
|
+
//
|
|
1714
|
+
// Nothing else is bypassed: the sandbox has already run above, so a
|
|
1715
|
+
// FORBIDDEN command is still refused and a path outside the workspace
|
|
1716
|
+
// is still blocked. This waives the QUESTION, not the guard.
|
|
1717
|
+
const standingApproval = isAlwaysAllowed(functionName);
|
|
1718
|
+
if (standingApproval) {
|
|
1719
|
+
auditLog.log({
|
|
1720
|
+
event: 'approval_auto_granted', taskId: this.taskId,
|
|
1721
|
+
tool: functionName, risk: validation.risk, reason: 'allow_always'
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (!standingApproval && (
|
|
1726
|
+
validation.risk === 'extreme' ||
|
|
1727
|
+
isHeavyAiDownload ||
|
|
1728
|
+
(this.config.autonomyMode !== 'C' && validation.risk === 'dangerous') ||
|
|
1729
|
+
this.config.autonomyMode === 'A')) {
|
|
1730
|
+
|
|
1731
|
+
// Wait for local approval
|
|
1732
|
+
this.emitEvent({ type: 'approval_request', toolCallId: toolCall.id, tool: functionName, args, risk: validation.risk });
|
|
1733
|
+
|
|
1734
|
+
const approved = await this.waitForApproval(toolCall.id);
|
|
1735
|
+
|
|
1736
|
+
if (!approved) {
|
|
1737
|
+
this.messages.push({
|
|
1738
|
+
role: 'tool',
|
|
1739
|
+
tool_call_id: toolCall.id,
|
|
1740
|
+
name: functionName,
|
|
1741
|
+
content: JSON.stringify({ success: false, error: 'User denied this action' })
|
|
1742
|
+
});
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// Execute
|
|
1748
|
+
const tool = resolveTool(functionName);
|
|
1749
|
+
|
|
1750
|
+
// GUARD: Prevent crash if LLM hallucinates a non-existent tool name
|
|
1751
|
+
if (!tool || typeof tool.execute !== 'function') {
|
|
1752
|
+
// Only advertise what is currently in context. Listing all 69 names
|
|
1753
|
+
// here re-bloated the very context progressive disclosure protects.
|
|
1754
|
+
this.messages.push({
|
|
1755
|
+
role: 'tool',
|
|
1756
|
+
tool_call_id: toolCall.id,
|
|
1757
|
+
name: functionName,
|
|
1758
|
+
content: JSON.stringify({
|
|
1759
|
+
success: false,
|
|
1760
|
+
error: `Tool "${functionName}" does not exist. Call tool_search with a description of the capability you need to discover the correct tool.`
|
|
1761
|
+
})
|
|
1762
|
+
});
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// AUTO-UNLOCK RECOVERY: the model named a real tool whose pack is not
|
|
1767
|
+
// loaded yet (it remembered it from an earlier task, or guessed well).
|
|
1768
|
+
// Unlock the pack and run it rather than dead-ending — capability is
|
|
1769
|
+
// deferred, never removed.
|
|
1770
|
+
if (!isCoreTool(functionName) && !this.unlockedTools.has(functionName)) {
|
|
1771
|
+
const pack = packForTool(functionName);
|
|
1772
|
+
if (this.unlockPack(pack)) {
|
|
1773
|
+
this.debugLog(`unlocked capability pack "${pack}" for ${functionName}`);
|
|
1774
|
+
}
|
|
1775
|
+
this.unlockedTools.add(functionName);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// V5: Tool Call Argument Validation Layer
|
|
1779
|
+
const validationErrors = this.validateToolArgs(functionName, args);
|
|
1780
|
+
if (validationErrors) {
|
|
1781
|
+
this.messages.push({
|
|
1782
|
+
role: 'tool',
|
|
1783
|
+
tool_call_id: toolCall.id,
|
|
1784
|
+
name: functionName,
|
|
1785
|
+
content: JSON.stringify({
|
|
1786
|
+
success: false,
|
|
1787
|
+
error: `Invalid arguments: ${validationErrors}. Please fix and retry.`
|
|
1788
|
+
})
|
|
1789
|
+
});
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// Stage 3 seam: PreToolUse hook. A no-op that always allows today; when
|
|
1794
|
+
// hooks are configured it can deny a call before it runs, returned as an
|
|
1795
|
+
// is_error tool result so the model self-corrects (never thrown).
|
|
1796
|
+
const preHook = await this.runPreToolUse(functionName, args, toolCall.id);
|
|
1797
|
+
if (preHook && preHook.denied) {
|
|
1798
|
+
this.messages.push({
|
|
1799
|
+
role: 'tool',
|
|
1800
|
+
tool_call_id: toolCall.id,
|
|
1801
|
+
name: functionName,
|
|
1802
|
+
content: JSON.stringify({
|
|
1803
|
+
success: false,
|
|
1804
|
+
error: `PreToolUse hook denied "${functionName}".`,
|
|
1805
|
+
hookFeedback: (preHook.messages || []).join('\n') || undefined
|
|
1806
|
+
})
|
|
1807
|
+
});
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// P2: snapshot the files this call is about to change, before it runs.
|
|
1812
|
+
// Only the mutating fs tools match; everything else returns immediately.
|
|
1813
|
+
// Fail-open by contract — this can never block or fail a tool call.
|
|
1814
|
+
const restorePoints = captureBeforeMutation(functionName, args, {
|
|
1815
|
+
taskId: this.taskId,
|
|
1816
|
+
step: this.stepsCompleted
|
|
1817
|
+
});
|
|
1818
|
+
|
|
1819
|
+
// Say that a restore point exists.
|
|
1820
|
+
//
|
|
1821
|
+
// The ids came back and were dropped on the floor, so checkpointing
|
|
1822
|
+
// ran on every mutating call and the user was never told — /undo was
|
|
1823
|
+
// there the whole time with nothing pointing at it. On the Stage this
|
|
1824
|
+
// also marks the rail, which is what makes scrolling back double as
|
|
1825
|
+
// finding somewhere to roll back TO.
|
|
1826
|
+
if (restorePoints.length > 0) {
|
|
1827
|
+
this.emitEvent({
|
|
1828
|
+
type: 'checkpoint',
|
|
1829
|
+
taskId: this.taskId,
|
|
1830
|
+
count: restorePoints.length,
|
|
1831
|
+
tool: functionName,
|
|
1832
|
+
path: args.path || args.file_path || null,
|
|
1833
|
+
step: this.stepsCompleted
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// P6: remember which rendered surfaces this task touched, so the
|
|
1838
|
+
// anti-slop gate can read the finished result back off disk rather
|
|
1839
|
+
// than trusting the model's own account of what it built.
|
|
1840
|
+
if (['fs_write', 'fs_edit', 'MultiEdit'].includes(functionName)) {
|
|
1841
|
+
const written = args.path || args.file_path;
|
|
1842
|
+
if (typeof written === 'string' && isScannableFile(written)) {
|
|
1843
|
+
this.webArtifacts.add(written.replace(/^~\//, os.homedir() + '/'));
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// V5: Execute with Error Recovery Loop (3-attempt retry with exponential backoff)
|
|
1848
|
+
const retryKey = `${functionName}:${JSON.stringify(args).substring(0, 100)}`;
|
|
1849
|
+
let result;
|
|
1850
|
+
let duration;
|
|
1851
|
+
const MAX_RETRIES = 3;
|
|
1852
|
+
const currentRetries = this._toolRetryCount.get(retryKey) || 0;
|
|
1853
|
+
|
|
1854
|
+
try {
|
|
1855
|
+
const wasRetried = currentRetries > 0;
|
|
1856
|
+
const startTime = Date.now();
|
|
1857
|
+
// A concurrent read from the prefetch pass above, if it succeeded.
|
|
1858
|
+
const ready = prefetched.get(toolCall.id);
|
|
1859
|
+
const early = ready ? await ready : null;
|
|
1860
|
+
result = (early && early.success)
|
|
1861
|
+
? early
|
|
1862
|
+
: await tool.execute(args, { signal: this.abortController.signal, parentLoop: this, ...this.sandbox });
|
|
1863
|
+
duration = Date.now() - startTime;
|
|
1864
|
+
|
|
1865
|
+
// Success: reset retry count for this operation
|
|
1866
|
+
this._toolRetryCount.delete(retryKey);
|
|
1867
|
+
|
|
1868
|
+
// V5 Cortex RLTF: Save the successful recovery as a permanent neural memory
|
|
1869
|
+
if (wasRetried) {
|
|
1870
|
+
cortexDb.saveMemory(
|
|
1871
|
+
this.currentTaskPrompt || `Task using ${functionName}`,
|
|
1872
|
+
`Failed ${currentRetries} times previously.`,
|
|
1873
|
+
`Successfully recovered and executed ${functionName} with arguments: ${JSON.stringify(args).substring(0, 200)}`
|
|
1874
|
+
).catch(e => this.debugLog(`cortex save failed: ${e.message}`));
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
} catch (toolError) {
|
|
1878
|
+
duration = 0;
|
|
1879
|
+
const errorType = this.classifyError(toolError);
|
|
1880
|
+
|
|
1881
|
+
if (currentRetries < MAX_RETRIES && errorType !== 'fatal') {
|
|
1882
|
+
this._toolRetryCount.set(retryKey, currentRetries + 1);
|
|
1883
|
+
const backoffMs = Math.pow(2, currentRetries) * 1000; // 1s, 2s, 4s
|
|
1884
|
+
|
|
1885
|
+
this.emitEvent({
|
|
1886
|
+
type: 'step_error',
|
|
1887
|
+
tool: functionName,
|
|
1888
|
+
error: `${errorType}: ${toolError.message} (retry ${currentRetries + 1}/${MAX_RETRIES} in ${backoffMs/1000}s)`
|
|
1889
|
+
});
|
|
1890
|
+
|
|
1891
|
+
auditLog.log({
|
|
1892
|
+
event: 'tool_retry',
|
|
1893
|
+
taskId: this.taskId,
|
|
1894
|
+
tool: functionName,
|
|
1895
|
+
attempt: currentRetries + 1,
|
|
1896
|
+
errorType,
|
|
1897
|
+
error: toolError.message
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
// Wait before retry (the LLM will see the error and can adjust)
|
|
1901
|
+
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
1902
|
+
|
|
1903
|
+
// Feed the classified error back so LLM can adjust its approach
|
|
1904
|
+
this.messages.push({
|
|
1905
|
+
role: 'tool',
|
|
1906
|
+
tool_call_id: toolCall.id,
|
|
1907
|
+
name: functionName,
|
|
1908
|
+
content: JSON.stringify({
|
|
1909
|
+
success: false,
|
|
1910
|
+
error: toolError.message,
|
|
1911
|
+
errorType: errorType,
|
|
1912
|
+
retryHint: this.getRetryHint(errorType),
|
|
1913
|
+
retriesLeft: MAX_RETRIES - currentRetries - 1
|
|
1914
|
+
})
|
|
1915
|
+
});
|
|
1916
|
+
continue;
|
|
1917
|
+
} else {
|
|
1918
|
+
// Max retries exceeded or fatal error: give up on this tool call
|
|
1919
|
+
this._toolRetryCount.delete(retryKey);
|
|
1920
|
+
this.emitEvent({
|
|
1921
|
+
type: 'step_error',
|
|
1922
|
+
tool: functionName,
|
|
1923
|
+
error: `Failed after ${MAX_RETRIES} attempts: ${toolError.message}`
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
this.messages.push({
|
|
1927
|
+
role: 'tool',
|
|
1928
|
+
tool_call_id: toolCall.id,
|
|
1929
|
+
name: functionName,
|
|
1930
|
+
content: JSON.stringify({
|
|
1931
|
+
success: false,
|
|
1932
|
+
error: `PERMANENT FAILURE after ${MAX_RETRIES} retries: ${toolError.message}. Do NOT retry this exact operation. Try a completely different approach or ask the user for help.`,
|
|
1933
|
+
errorType: errorType
|
|
1934
|
+
})
|
|
1935
|
+
});
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
auditLog.log({
|
|
1941
|
+
event: 'tool_exec',
|
|
1942
|
+
taskId: this.taskId,
|
|
1943
|
+
tool: functionName,
|
|
1944
|
+
success: result.success,
|
|
1945
|
+
duration_ms: duration
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
// Stage 3 seam: PostToolUse hook. A no-op that always allows today; when
|
|
1949
|
+
// hooks are configured it can mark a completed call failed (e.g. a
|
|
1950
|
+
// formatter rejecting a write) so the model sees it. Neutral until then.
|
|
1951
|
+
const postHook = await this.runPostToolUse(functionName, args, result);
|
|
1952
|
+
if (postHook && postHook.denied) {
|
|
1953
|
+
result = {
|
|
1954
|
+
...result,
|
|
1955
|
+
success: false,
|
|
1956
|
+
error: postHook.reason || `PostToolUse hook rejected "${functionName}".`,
|
|
1957
|
+
hookFeedback: (postHook.messages || []).join('\n') || undefined
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
// PROGRESSIVE DISCLOSURE: honour unlock signals from tool_search.
|
|
1962
|
+
// The control fields are stripped before the result reaches the LLM.
|
|
1963
|
+
if (result && (result._unlock_tools || result._unlock_packs)) {
|
|
1964
|
+
for (const packName of result._unlock_packs || []) {
|
|
1965
|
+
this.unlockPack(packName);
|
|
1966
|
+
}
|
|
1967
|
+
for (const toolName of result._unlock_tools || []) {
|
|
1968
|
+
this.unlockedTools.add(toolName);
|
|
1969
|
+
}
|
|
1970
|
+
delete result._unlock_tools;
|
|
1971
|
+
delete result._unlock_packs;
|
|
1972
|
+
this.debugLog(`capability packs loaded: ${[...this.unlockedPacks].join(', ') || 'core only'}`);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// Buffer output for disconnect resilience
|
|
1976
|
+
await this.taskBuffer.appendOutput(this.taskId, { tool: functionName, result });
|
|
1977
|
+
|
|
1978
|
+
this.emitEvent({ type: 'step_complete', tool: functionName, result: result._is_image_marker ? '[IMAGE DATA]' : result });
|
|
1979
|
+
this.stepsCompleted++;
|
|
1980
|
+
|
|
1981
|
+
// The procedure, as it actually ran. Tool name plus a whitelisted
|
|
1982
|
+
// action token only — planCache.toStep() reads no paths, arguments or
|
|
1983
|
+
// command lines, so what is cached is a route and never a transcript.
|
|
1984
|
+
try {
|
|
1985
|
+
if (Array.isArray(this._planSteps)) {
|
|
1986
|
+
const stepSuccess = Boolean(result && result.success !== false && !result.error);
|
|
1987
|
+
this._planSteps.push(toStep(
|
|
1988
|
+
functionName,
|
|
1989
|
+
args,
|
|
1990
|
+
stepSuccess,
|
|
1991
|
+
result
|
|
1992
|
+
));
|
|
1993
|
+
this._lastToolResult = result;
|
|
1994
|
+
|
|
1995
|
+
// Active Inference: Record step prediction error into VFE rolling tracker
|
|
1996
|
+
if (this.vfeTracker) {
|
|
1997
|
+
const isThrash = isThrashing((this._planSteps || []).map((s) => s.tool), functionName);
|
|
1998
|
+
const vfeOutcome = this.vfeTracker.recordStep({
|
|
1999
|
+
tool: functionName,
|
|
2000
|
+
success: stepSuccess,
|
|
2001
|
+
result,
|
|
2002
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
2003
|
+
isThrashing: isThrash
|
|
2004
|
+
});
|
|
2005
|
+
if (vfeOutcome.paradigmShift) {
|
|
2006
|
+
this.debugLog(`[ACTIVE INFERENCE] VFE plateau detected: mean=${vfeOutcome.mean}, var=${vfeOutcome.variance}. Triggering paradigm shift.`);
|
|
2007
|
+
this._paradigmShiftPending = true;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
} catch (e) { /* a plan is never worth failing a step for */ }
|
|
2012
|
+
|
|
2013
|
+
const ALL_GAME_TOOLS = [
|
|
2014
|
+
'godot_act', 'game_asset_fetch', 'game_build_html5',
|
|
2015
|
+
'game_lore_forge', 'game_dialogue_tree_gen', 'game_quest_arc_builder',
|
|
2016
|
+
'game_animation_rig', 'game_ai_behavior_tree', 'game_cinematic_sequencer',
|
|
2017
|
+
'game_asset_harvest', 'game_audio_synth', 'game_vfx_emitter_gen',
|
|
2018
|
+
'game_shader_forge', 'game_scene_build', 'game_entity_spawner',
|
|
2019
|
+
'game_tilemap_gen', 'game_ui_hud_builder', 'game_economy_balancer',
|
|
2020
|
+
'game_data_forge', 'check_system_specs', 'game_web_runtime_inspector', 'game_canvas_audit'
|
|
2021
|
+
];
|
|
2022
|
+
if (ALL_GAME_TOOLS.includes(functionName)) {
|
|
2023
|
+
// this.hasUsedGameTools is deprecated by this.gameCascadeState
|
|
2024
|
+
this.gameToolStats = this.gameToolStats || { godot_act: 0, game_asset_fetch: 0, game_build_html5: 0, last_godot_action: null };
|
|
2025
|
+
this.gameToolStats[functionName] = (this.gameToolStats[functionName] || 0) + 1;
|
|
2026
|
+
if (functionName === 'godot_act' && args && args.action) {
|
|
2027
|
+
this.gameToolStats.last_godot_action = args.action;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// Sovereign 100-Step Circuit Breaker Checkpoint Gate
|
|
2032
|
+
if (this.consecutiveCommands === 100 && !this.circuitBreakerUnlocked) {
|
|
2033
|
+
this.emitEvent({
|
|
2034
|
+
type: 'approval_request',
|
|
2035
|
+
toolCallId: 'circuit_breaker_100_' + this.stepsCompleted,
|
|
2036
|
+
tool: 'circuit_breaker_checkpoint',
|
|
2037
|
+
args: {
|
|
2038
|
+
consecutiveCommands: this.consecutiveCommands,
|
|
2039
|
+
stepsCompleted: this.stepsCompleted,
|
|
2040
|
+
activeTier: this.activeTier,
|
|
2041
|
+
maxTierSteps: this.maxSteps,
|
|
2042
|
+
message: `⚡ Circuit Breaker Checkpoint (100 steps reached). Approve to unlock and continue using your remaining ${this.activeTier.toUpperCase()} tier budget (up to ${this.maxSteps} steps total)?`
|
|
2043
|
+
},
|
|
2044
|
+
risk: 'info'
|
|
2045
|
+
});
|
|
2046
|
+
|
|
2047
|
+
const approved = await this.waitForApproval('circuit_breaker_100_' + this.stepsCompleted);
|
|
2048
|
+
if (!approved) {
|
|
2049
|
+
this.stopRequested = true;
|
|
2050
|
+
this.emitEvent({ type: 'status', message: 'Task stopped at 100-step circuit breaker checkpoint by user.' });
|
|
2051
|
+
break;
|
|
2052
|
+
}
|
|
2053
|
+
this.circuitBreakerUnlocked = true;
|
|
2054
|
+
this.emitEvent({ type: 'status', message: `⚡ Circuit breaker unlocked! Resuming execution up to ${this.maxSteps} tier budget...` });
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// Sovereign Tier Budget Extension Checkpoint Gate
|
|
2058
|
+
if (this.stepsCompleted >= this.maxSteps) {
|
|
2059
|
+
const extensionBatch = (this.activeTier === 'super_max') ? 600 : ((this.activeTier === 'max') ? 400 : 300);
|
|
2060
|
+
this.emitEvent({
|
|
2061
|
+
type: 'approval_request',
|
|
2062
|
+
toolCallId: 'tier_extension_' + this.stepsCompleted,
|
|
2063
|
+
tool: 'tier_budget_extension',
|
|
2064
|
+
args: {
|
|
2065
|
+
stepsCompleted: this.stepsCompleted,
|
|
2066
|
+
activeTier: this.activeTier,
|
|
2067
|
+
currentMaxSteps: this.maxSteps,
|
|
2068
|
+
message: `⚡ Tier Limit Reached (${this.stepsCompleted} / ${this.maxSteps} steps). The agent needs more steps to fulfill your intent. Approve to unlock another +${extensionBatch} steps for your ${this.activeTier.toUpperCase()} tier?`
|
|
2069
|
+
},
|
|
2070
|
+
risk: 'info'
|
|
2071
|
+
});
|
|
2072
|
+
|
|
2073
|
+
const approved = await this.waitForApproval('tier_extension_' + this.stepsCompleted);
|
|
2074
|
+
if (!approved) {
|
|
2075
|
+
this.stopRequested = true;
|
|
2076
|
+
this.emitEvent({ type: 'status', message: `Task stopped at ${this.maxSteps}-step tier limit by user.` });
|
|
2077
|
+
break;
|
|
2078
|
+
}
|
|
2079
|
+
this.maxSteps += extensionBatch;
|
|
2080
|
+
this.maxCircuitBreaker = this.maxSteps;
|
|
2081
|
+
this.emitEvent({ type: 'status', message: `⚡ Tier budget extended (+${extensionBatch} steps unlocked, new limit: ${this.maxSteps}). Resuming task...` });
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// Feed result back to LLM (4. OBSERVE)
|
|
2085
|
+
if (result && result._is_image_marker) {
|
|
2086
|
+
if (!this.llm.supportsVision) {
|
|
2087
|
+
// Fallback for blind models
|
|
2088
|
+
this.messages.push({
|
|
2089
|
+
role: 'tool',
|
|
2090
|
+
tool_call_id: toolCall.id,
|
|
2091
|
+
name: functionName,
|
|
2092
|
+
content: "[ERROR: The current LLM model you are using does not support Vision/Image capabilities. You cannot view images. Please rely on text tools or metadata instead.]"
|
|
2093
|
+
});
|
|
2094
|
+
} else {
|
|
2095
|
+
// Provider-specific format.
|
|
2096
|
+
//
|
|
2097
|
+
// This used to read `this.llm.constructor.name.includes('OpenAI')`.
|
|
2098
|
+
// Constructor names are not a stable provider identifier and can
|
|
2099
|
+
// change during packaging. Each provider declares its wire shape
|
|
2100
|
+
// explicitly.
|
|
2101
|
+
const imageContent = this.llm.imagePayloadFormat === 'openai' ? {
|
|
2102
|
+
type: "image_url",
|
|
2103
|
+
image_url: { url: `data:${result.mimeType};base64,${result.base64}` }
|
|
2104
|
+
} : {
|
|
2105
|
+
type: "image",
|
|
2106
|
+
source: {
|
|
2107
|
+
type: "base64",
|
|
2108
|
+
media_type: result.mimeType,
|
|
2109
|
+
data: result.base64
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
|
|
2113
|
+
// We MUST push the tool return FIRST so tool_calls is immediately followed by tool messages
|
|
2114
|
+
this.messages.push({
|
|
2115
|
+
role: 'tool',
|
|
2116
|
+
tool_call_id: toolCall.id,
|
|
2117
|
+
name: functionName,
|
|
2118
|
+
content: `Image successfully captured: ${result.filename || 'image'}. Passing to vision observation.`
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
// Then push the user observation message containing the image data block
|
|
2122
|
+
this.messages.push({
|
|
2123
|
+
role: 'user',
|
|
2124
|
+
content: [
|
|
2125
|
+
imageContent,
|
|
2126
|
+
{
|
|
2127
|
+
type: "text",
|
|
2128
|
+
text: `Image successfully loaded into vision context: ${result.filename || ''}`
|
|
2129
|
+
}
|
|
2130
|
+
]
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
} else {
|
|
2134
|
+
// SOVEREIGN CONTEXT WINDOW PROTECTION.
|
|
2135
|
+
// The previous implementation spread `...result` and only shortened
|
|
2136
|
+
// `output`, so tools that return their payload under a different key
|
|
2137
|
+
// (browser_act and blender_act return `log`) leaked the full
|
|
2138
|
+
// untruncated blob into history — and gained a redundant `output`
|
|
2139
|
+
// field on top. Truncate whichever string field is actually large.
|
|
2140
|
+
let resultString = JSON.stringify(result);
|
|
2141
|
+
// Per tool, not one number for all of them. A build log and a
|
|
2142
|
+
// source file are not equally worth their space in the window.
|
|
2143
|
+
const MAX_RESULT_CHARS = resultCapFor(functionName);
|
|
2144
|
+
let spill = null;
|
|
2145
|
+
|
|
2146
|
+
if (resultString.length > MAX_RESULT_CHARS) {
|
|
2147
|
+
// A1: spill the WHOLE result to disk before shortening it. The
|
|
2148
|
+
// previous code threw the overflow away, so the only way back to
|
|
2149
|
+
// it was re-running the tool — a second full execution and a
|
|
2150
|
+
// second full-size result in the window. Now the bytes survive
|
|
2151
|
+
// behind a path and recovery is one bounded read.
|
|
2152
|
+
// The raw result, not JSON.stringify(result): JSON escapes every
|
|
2153
|
+
// newline, which would land a large payload on a single line and
|
|
2154
|
+
// make the fs_read/ripgrep recovery route we advertise useless.
|
|
2155
|
+
spill = spillToolResult(result, {
|
|
2156
|
+
taskId: this.taskId,
|
|
2157
|
+
step: this.stepsCompleted,
|
|
2158
|
+
tool: functionName
|
|
2159
|
+
});
|
|
2160
|
+
const TRUNCATION_NOTICE = buildSpillNotice(spill);
|
|
2161
|
+
|
|
2162
|
+
const truncatedResult = {};
|
|
2163
|
+
// Budget shared across every oversized string field on the result.
|
|
2164
|
+
let budget = MAX_RESULT_CHARS - 2000;
|
|
2165
|
+
|
|
2166
|
+
for (const [key, value] of Object.entries(result)) {
|
|
2167
|
+
if (typeof value === 'string' && value.length > 500) {
|
|
2168
|
+
const allowance = Math.max(0, Math.min(value.length, budget));
|
|
2169
|
+
budget -= allowance;
|
|
2170
|
+
truncatedResult[key] = allowance < value.length
|
|
2171
|
+
? value.substring(0, allowance) + TRUNCATION_NOTICE
|
|
2172
|
+
: value;
|
|
2173
|
+
} else if (value !== null && typeof value === 'object') {
|
|
2174
|
+
const serialized = JSON.stringify(value);
|
|
2175
|
+
if (serialized.length > budget) {
|
|
2176
|
+
truncatedResult[key] = spill
|
|
2177
|
+
? `[${key} omitted here: ${(serialized.length / 1024).toFixed(1)} KB. The complete result is saved at ${spill.path} — read or search that file rather than re-running.]`
|
|
2178
|
+
: `[${key} omitted: ${(serialized.length / 1024).toFixed(1)} KB. Re-run with narrower arguments if you need it.]`;
|
|
2179
|
+
} else {
|
|
2180
|
+
budget -= serialized.length;
|
|
2181
|
+
truncatedResult[key] = value;
|
|
2182
|
+
}
|
|
2183
|
+
} else {
|
|
2184
|
+
truncatedResult[key] = value;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
resultString = JSON.stringify(truncatedResult);
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// B5 — OBSERVATION COMPACTION.
|
|
2192
|
+
//
|
|
2193
|
+
// ARC Prize measured the same GPT-6 Astra weights at 62.7% under a
|
|
2194
|
+
// note-passing harness and 99.9% under one that compacts and reuses
|
|
2195
|
+
// prior work — with 49% FEWER tokens. Re-reading a file the agent
|
|
2196
|
+
// already read, or re-listing an unchanged directory, pays full
|
|
2197
|
+
// price for bytes that are verbatim identical to bytes already in
|
|
2198
|
+
// the window.
|
|
2199
|
+
//
|
|
2200
|
+
// Replacing the repeat with a pointer is not a loss of information:
|
|
2201
|
+
// "identical to step N" is strictly MORE informative than the bytes
|
|
2202
|
+
// alone, because it tells the model the thing did not change —
|
|
2203
|
+
// which after an edit is exactly the question it is asking.
|
|
2204
|
+
//
|
|
2205
|
+
// Only payloads worth the trade are folded. A small `{"success":true}`
|
|
2206
|
+
// ack costs less than the pointer that would replace it, and every
|
|
2207
|
+
// TodoWrite would otherwise collide with every other TodoWrite.
|
|
2208
|
+
const OBSERVATION_FOLD_FLOOR = 600;
|
|
2209
|
+
if (resultString.length >= OBSERVATION_FOLD_FLOOR) {
|
|
2210
|
+
const digest = createHash('sha256').update(resultString).digest('hex');
|
|
2211
|
+
const seenAt = this._observationDigests.get(digest);
|
|
2212
|
+
if (seenAt !== undefined) {
|
|
2213
|
+
resultString = JSON.stringify({
|
|
2214
|
+
success: true,
|
|
2215
|
+
unchanged: true,
|
|
2216
|
+
output: `Identical to the result of step ${seenAt} — nothing has changed since. `
|
|
2217
|
+
+ `That earlier result is still in your context; re-read it there rather than asking again.`
|
|
2218
|
+
});
|
|
2219
|
+
} else {
|
|
2220
|
+
this._observationDigests.set(digest, this.stepsCompleted + 1);
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
this.messages.push({
|
|
2225
|
+
role: 'tool',
|
|
2226
|
+
tool_call_id: toolCall.id,
|
|
2227
|
+
name: functionName,
|
|
2228
|
+
content: resultString,
|
|
2229
|
+
// Internal bookkeeping only: stripped before any provider call
|
|
2230
|
+
// (providers/base.js INTERNAL_MESSAGE_FIELDS). It lets the decay
|
|
2231
|
+
// stub keep pointing at the spill after the payload is gone.
|
|
2232
|
+
...(spill ? { spillPath: spill.path } : {})
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
// Save state continuously after each tool loop
|
|
2237
|
+
this.memoryManager.saveHistory(this.messages);
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
} catch (e) {
|
|
2241
|
+
// A real failure. It reaches the user as an `error` frame further down
|
|
2242
|
+
// this same catch; the stack is a diagnostic and stays behind the flag.
|
|
2243
|
+
this.debugLog(`agent loop error: ${e && e.stack ? e.stack : e}`);
|
|
2244
|
+
const errorType = this.classifyError(e);
|
|
2245
|
+
|
|
2246
|
+
if (e.message.includes('NO_BACKEND_URL:')) {
|
|
2247
|
+
this.emitEvent({ type: 'warning', message: e.message.replace('NO_BACKEND_URL: ', '') });
|
|
2248
|
+
break;
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
// V5: Only break on fatal errors. Transient errors get fed back to LLM.
|
|
2252
|
+
if (errorType === 'fatal' || errorType === 'auth_error' || errorType === 'invalid_request_error') {
|
|
2253
|
+
this.emitEvent({ type: 'error', message: e.message });
|
|
2254
|
+
auditLog.log({ event: 'task_error', taskId: this.taskId, error: e.message, errorType });
|
|
2255
|
+
|
|
2256
|
+
break;
|
|
2257
|
+
} else {
|
|
2258
|
+
// A transient error. "Transient" only means the loop MAY continue —
|
|
2259
|
+
// it does not mean it should continue forever.
|
|
2260
|
+
//
|
|
2261
|
+
// This used to retry immediately, with no backoff and no cap, until
|
|
2262
|
+
// the step budget ran out. A sleeping laptop or a wifi blip therefore
|
|
2263
|
+
// produced up to 300 attempts against a dead endpoint, each one
|
|
2264
|
+
// waiting out a full connect timeout, while the status line said
|
|
2265
|
+
// "Thinking" and the only signal was `network_error: fetch failed`
|
|
2266
|
+
// repeated in red. From the outside it is indistinguishable from
|
|
2267
|
+
// working, and it burns the user's step budget doing nothing.
|
|
2268
|
+
const repeated = this.lastFailureType === errorType;
|
|
2269
|
+
this.consecutiveFailures = repeated ? this.consecutiveFailures + 1 : 1;
|
|
2270
|
+
this.lastFailureType = errorType;
|
|
2271
|
+
|
|
2272
|
+
const isNetwork = errorType === NETWORK_ERROR_TYPE;
|
|
2273
|
+
const detail = isNetwork ? explainNetworkError(e, this.config) : e.message;
|
|
2274
|
+
auditLog.log({ event: 'loop_error_recovered', taskId: this.taskId, error: e.message, errorType });
|
|
2275
|
+
|
|
2276
|
+
if (this.consecutiveFailures >= AgentLoop.MAX_CONSECUTIVE_FAILURES) {
|
|
2277
|
+
// Nothing is changing between attempts. Stop and say why, rather
|
|
2278
|
+
// than spinning until the budget is gone.
|
|
2279
|
+
this.emitEvent({
|
|
2280
|
+
type: 'error',
|
|
2281
|
+
message: `${detail}\n\nStopped after ${this.consecutiveFailures} attempts. `
|
|
2282
|
+
+ 'Nothing changed between them, so retrying further would only spend your budget.'
|
|
2283
|
+
});
|
|
2284
|
+
|
|
2285
|
+
break;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
this.emitEvent({
|
|
2289
|
+
type: 'step_error',
|
|
2290
|
+
tool: 'agent_loop',
|
|
2291
|
+
error: `${detail}\n retrying (${this.consecutiveFailures} of `
|
|
2292
|
+
+ `${AgentLoop.MAX_CONSECUTIVE_FAILURES})`
|
|
2293
|
+
});
|
|
2294
|
+
|
|
2295
|
+
// Back off before trying again. Hammering a failing endpoint as fast
|
|
2296
|
+
// as the loop can spin turns a one-second blip into a burst of
|
|
2297
|
+
// failures and gives the endpoint no chance to recover.
|
|
2298
|
+
await this.backoff(this.consecutiveFailures);
|
|
2299
|
+
|
|
2300
|
+
// Only tell the MODEL about errors the model can act on. It cannot
|
|
2301
|
+
// fix DNS or a firewall, and a system message per attempt grows the
|
|
2302
|
+
// context — and the bill — for nothing.
|
|
2303
|
+
if (!isNetwork) {
|
|
2304
|
+
this.messages.push({
|
|
2305
|
+
role: 'system',
|
|
2306
|
+
content: `[SYSTEM ERROR RECOVERED: A ${errorType} error occurred: "${e.message}". `
|
|
2307
|
+
+ 'The loop has recovered. You may continue or try a different approach.]'
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
this.emitEvent({ type: 'task_complete', taskId: this.taskId });
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
async waitForApproval(toolCallId) {
|
|
2318
|
+
|
|
2319
|
+
return new Promise((resolve) => {
|
|
2320
|
+
this.pendingApprovals.set(toolCallId, resolve);
|
|
2321
|
+
|
|
2322
|
+
// Safety timeout: If user doesn't respond in 1 hour, auto-deny.
|
|
2323
|
+
setTimeout(() => {
|
|
2324
|
+
if (this.pendingApprovals.has(toolCallId)) {
|
|
2325
|
+
this.handleApprovalResponse(toolCallId, false);
|
|
2326
|
+
}
|
|
2327
|
+
}, 3600000);
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
/**
|
|
2332
|
+
* Wait before retrying, and stay interruptible while waiting.
|
|
2333
|
+
*
|
|
2334
|
+
* Exponential, capped. Without the abort check a user pressing esc during a
|
|
2335
|
+
* backoff would wait out the full delay before anything responded.
|
|
2336
|
+
*/
|
|
2337
|
+
async backoff(attempt) {
|
|
2338
|
+
const delay = Math.min(8000, 500 * Math.pow(2, Math.max(0, attempt - 1)));
|
|
2339
|
+
await new Promise((resolve) => {
|
|
2340
|
+
const timer = setTimeout(resolve, delay);
|
|
2341
|
+
if (timer.unref) timer.unref();
|
|
2342
|
+
const signal = this.abortController && this.abortController.signal;
|
|
2343
|
+
if (signal) {
|
|
2344
|
+
signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
// V5: Classify errors for intelligent retry decisions
|
|
2350
|
+
classifyError(error) {
|
|
2351
|
+
const msg = (error.message || '').toLowerCase();
|
|
2352
|
+
|
|
2353
|
+
if (msg.includes('syntax') || msg.includes('unexpected token') || msg.includes('invalid json')) {
|
|
2354
|
+
return 'syntax_error';
|
|
2355
|
+
}
|
|
2356
|
+
if (msg.includes('enoent') || msg.includes('no such file') || msg.includes('not found') || msg.includes('does not exist')) {
|
|
2357
|
+
return 'path_not_found';
|
|
2358
|
+
}
|
|
2359
|
+
if (msg.includes('econnrefused') || msg.includes('econnreset') || msg.includes('etimedout') || msg.includes('fetch failed') || msg.includes('network')) {
|
|
2360
|
+
return "network_error";
|
|
2361
|
+
}
|
|
2362
|
+
if (msg.includes('rate limit') || msg.includes('429') || msg.includes('too many requests') || msg.includes('quota')) {
|
|
2363
|
+
return 'rate_limit';
|
|
2364
|
+
}
|
|
2365
|
+
if (msg.includes('eacces') || msg.includes('permission denied') || msg.includes('eperm')) {
|
|
2366
|
+
return 'permission_denied';
|
|
2367
|
+
}
|
|
2368
|
+
if (msg.includes('401') || msg.includes('403') || msg.includes('unauthorized') || msg.includes('api key')) {
|
|
2369
|
+
return 'auth_error';
|
|
2370
|
+
}
|
|
2371
|
+
if (msg.includes('400') || msg.includes('invalid_parameter') || msg.includes('invalid_request') || msg.includes('required property') || msg.includes('bad request')) {
|
|
2372
|
+
return 'invalid_request_error';
|
|
2373
|
+
}
|
|
2374
|
+
if (msg.includes('kill') || msg.includes('abort') || msg.includes('signal') || msg.includes('fatal')) {
|
|
2375
|
+
return 'fatal';
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
return 'unknown_error';
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// V5: Provide actionable retry hints per error type
|
|
2382
|
+
getRetryHint(errorType) {
|
|
2383
|
+
const hints = {
|
|
2384
|
+
'syntax_error': 'Check your command syntax or JSON structure. Ensure quotes are balanced.',
|
|
2385
|
+
'path_not_found': 'The file or directory does not exist. Use fs.list to verify the path first, or create it with fs.write/mkdir.',
|
|
2386
|
+
["network_error"]: "Network connectivity issue. Wait a moment and retry. If persistent, check if the URL is correct.",
|
|
2387
|
+
'rate_limit': 'API rate limit hit. Wait 5-10 seconds before retrying.',
|
|
2388
|
+
'permission_denied': 'Permission denied. Check if the path is within allowed workspace paths. Try a different directory.',
|
|
2389
|
+
'auth_error': 'Authentication failed. Do NOT retry — the API key may be invalid.',
|
|
2390
|
+
'invalid_request_error': 'API request format error (400 Bad Request). Do NOT retry — check model parameter compliance.',
|
|
2391
|
+
'unknown_error': 'An unexpected error occurred. Try a different approach entirely.'
|
|
2392
|
+
};
|
|
2393
|
+
return hints[errorType] || hints['unknown_error'];
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
// V5: Validate tool call arguments before execution
|
|
2397
|
+
/**
|
|
2398
|
+
* PreToolUse / PostToolUse seams (Stage 3 fills these in).
|
|
2399
|
+
*
|
|
2400
|
+
* Today they are no-ops that always allow, so the dispatch behaviour is exactly
|
|
2401
|
+
* what it was. When `.causyne/settings.json` gains a `hooks` block (Stage 3),
|
|
2402
|
+
* these run the user's shell hooks with the exit-code protocol
|
|
2403
|
+
* (0 allow / 2 deny / else warn) and return { denied, messages }. The loop
|
|
2404
|
+
* already branches on `denied`, so Stage 3 is a fill-in here, not another edit
|
|
2405
|
+
* to the dispatch path. See src/tools/toolDispatch.js for the discipline these
|
|
2406
|
+
* plug into.
|
|
2407
|
+
*/
|
|
2408
|
+
async runPreToolUse(_toolName, _args, _toolCallId) {
|
|
2409
|
+
return { denied: false, messages: [] };
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
async runPostToolUse(_toolName, _args, _result) {
|
|
2413
|
+
return { denied: false, messages: [] };
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
validateToolArgs(toolName, args) {
|
|
2417
|
+
// Check for common LLM mistakes
|
|
2418
|
+
|
|
2419
|
+
// 1. File operations must have absolute paths
|
|
2420
|
+
const pathFields = ['path', 'source', 'destination'];
|
|
2421
|
+
for (const field of pathFields) {
|
|
2422
|
+
if (args[field] && typeof args[field] === 'string') {
|
|
2423
|
+
// Auto-fix relative paths by prepending cwd
|
|
2424
|
+
if (!args[field].startsWith('/') && !args[field].startsWith('~')) {
|
|
2425
|
+
args[field] = `${process.cwd()}/${args[field]}`;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
// 2. Terminal commands must have a command string
|
|
2431
|
+
if ((toolName === 'sysRun' || toolName === 'sysRunBg') && !args.command) {
|
|
2432
|
+
return 'Missing required "command" argument';
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// 3. File write must have a path AND content.
|
|
2436
|
+
//
|
|
2437
|
+
// These branches were dead: canonicalToolName() (called on every tool name
|
|
2438
|
+
// before this runs) normalises '.'→'_', so `toolName` is 'fs_write', never
|
|
2439
|
+
// 'fs.write'. The old 'fs.write' / 'codebase.search' / 'web.search' literals
|
|
2440
|
+
// could never match, so the model's missing-argument mistakes reached the
|
|
2441
|
+
// tool as runtime exceptions instead of a corrective message. Matched on the
|
|
2442
|
+
// canonical names now, and fs_write's `content` is actually checked (the old
|
|
2443
|
+
// comment said "must have content" but the code only looked at `path`).
|
|
2444
|
+
if (toolName === 'fs_write' && !args.path) {
|
|
2445
|
+
return 'Missing required "path" argument';
|
|
2446
|
+
}
|
|
2447
|
+
if (toolName === 'fs_write' && args.content === undefined) {
|
|
2448
|
+
return 'Missing required "content" argument';
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// 4. Search must have a query
|
|
2452
|
+
if ((toolName === 'codebase_search' || toolName === 'web_search') && !args.query) {
|
|
2453
|
+
return 'Missing required "query" argument';
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
return null; // No validation errors
|
|
2457
|
+
}
|
|
2458
|
+
}
|