@causyne/agent 0.0.0-stage → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { UTILITY_MODEL_BY_PROVIDER } from './modelCatalogue.js';
|
|
2
|
+
|
|
3
|
+
// Hybrid model routing.
|
|
4
|
+
//
|
|
5
|
+
// Not every call an agent makes is reasoning. Condensing a dropped transcript,
|
|
6
|
+
// classifying an intent, filtering a log, extracting keywords — these are
|
|
7
|
+
// utility work, and paying a flagship rate for them is pure waste. The research
|
|
8
|
+
// spec puts the saving at the difference between the two rate cards, which for
|
|
9
|
+
// OpenAI is $5/$30 against $0.75/$4.50: roughly 6x on work that never needed
|
|
10
|
+
// the better model.
|
|
11
|
+
//
|
|
12
|
+
// THREE RULES THIS MODULE EXISTS TO ENFORCE, all of them things a naive router
|
|
13
|
+
// gets wrong:
|
|
14
|
+
//
|
|
15
|
+
// 1. NEVER CHANGE PROVIDER. Causyne is BYOK. The user holds one key. A router
|
|
16
|
+
// that sends utility work to "whatever is cheapest on the market" calls an
|
|
17
|
+
// endpoint there is no credential for and the task fails — having spent
|
|
18
|
+
// the tokens that got it there.
|
|
19
|
+
//
|
|
20
|
+
// 2. NEVER CHANGE THE PRIVACY POSTURE. Muse's cheap tier is `-contributor`,
|
|
21
|
+
// discounted because the vendor trains on the content. Routing into it to
|
|
22
|
+
// save money would opt the user into training silently. Cost is not a
|
|
23
|
+
// reason to make that choice for someone.
|
|
24
|
+
//
|
|
25
|
+
// 3. NEVER GUESS. A provider with no published second tier routes to nothing
|
|
26
|
+
// and pays full rate. An invented model id becomes a 404 mid-task, or
|
|
27
|
+
// worse, a real model at a price nobody predicted.
|
|
28
|
+
//
|
|
29
|
+
// The router returns a DESCRIPTION of where a call should go. It never
|
|
30
|
+
// constructs a provider and never performs a call, so it stays trivially
|
|
31
|
+
// testable and cannot become a second place where credentials are handled.
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What a call is for.
|
|
35
|
+
*
|
|
36
|
+
* `reasoning` — planning, code, architecture, tool selection, anything the
|
|
37
|
+
* user's result quality depends on. Always the configured model.
|
|
38
|
+
* `utility` — mechanical text work whose output is consumed by the machine
|
|
39
|
+
* rather than by the user's judgement.
|
|
40
|
+
*
|
|
41
|
+
* @typedef {'reasoning' | 'utility'} RouteClass
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Object} Route
|
|
46
|
+
* @property {string} provider The provider to call. Always the configured one.
|
|
47
|
+
* @property {string} model The model id to request.
|
|
48
|
+
* @property {boolean} routed True when this differs from the configured model.
|
|
49
|
+
* @property {string} reason Why, in one line. Written to the audit trail.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Utility work that is cheap to get wrong and expensive to run on a flagship.
|
|
54
|
+
* Anything not listed is treated as reasoning — the safe default is to spend
|
|
55
|
+
* more, never to silently downgrade work the user is judging.
|
|
56
|
+
*
|
|
57
|
+
* @type {ReadonlySet<string>}
|
|
58
|
+
*/
|
|
59
|
+
export const UTILITY_KINDS = new Set([
|
|
60
|
+
'condense', // cortex/autoCondenser semantic pass
|
|
61
|
+
'keywords', // plan-cache intent extraction
|
|
62
|
+
'classify', // intent / archetype disambiguation
|
|
63
|
+
'filter', // log and output filtering
|
|
64
|
+
'summarise' // non-decision summaries
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Decide where a call of a given kind should go.
|
|
69
|
+
*
|
|
70
|
+
* @param {{provider?: string, model?: string, modelRouting?: Record<string, string>, disableModelRouting?: boolean}} config
|
|
71
|
+
* The live agent config, as loaded from `~/.causyne/config.json`.
|
|
72
|
+
* @param {RouteClass|string} kind
|
|
73
|
+
* Either a RouteClass or one of UTILITY_KINDS.
|
|
74
|
+
* @returns {Route}
|
|
75
|
+
*/
|
|
76
|
+
export function resolveRoute(config, kind) {
|
|
77
|
+
const provider = String(config?.provider || '').toLowerCase();
|
|
78
|
+
const configured = String(config?.model || '');
|
|
79
|
+
|
|
80
|
+
/** @type {Route} */
|
|
81
|
+
const stay = {
|
|
82
|
+
provider,
|
|
83
|
+
model: configured,
|
|
84
|
+
routed: false,
|
|
85
|
+
reason: 'reasoning work runs on the configured model'
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (!provider || !configured) {
|
|
89
|
+
return { ...stay, reason: 'no provider or model configured' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const isUtility = kind === 'utility' || UTILITY_KINDS.has(String(kind));
|
|
93
|
+
if (!isUtility) return stay;
|
|
94
|
+
|
|
95
|
+
if (config?.disableModelRouting === true) {
|
|
96
|
+
return { ...stay, reason: 'model routing disabled by config' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// An explicit choice by the user outranks the table, including a deliberate
|
|
100
|
+
// choice of a training tier — they made it, we did not make it for them.
|
|
101
|
+
const override = config?.modelRouting?.utility;
|
|
102
|
+
if (override) {
|
|
103
|
+
return {
|
|
104
|
+
provider,
|
|
105
|
+
model: String(override),
|
|
106
|
+
routed: String(override) !== configured,
|
|
107
|
+
reason: 'utility route set explicitly in config'
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const cheap = UTILITY_MODEL_BY_PROVIDER[provider];
|
|
112
|
+
if (!cheap) {
|
|
113
|
+
return { ...stay, reason: `no published utility tier for ${provider}; paying full rate` };
|
|
114
|
+
}
|
|
115
|
+
if (cheap === configured) {
|
|
116
|
+
return { ...stay, reason: 'already on this provider\'s utility tier' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Rule 2. The configured model already carries a privacy posture; a route may
|
|
120
|
+
// not alter it in either direction.
|
|
121
|
+
if (/-contributor$/.test(configured) !== /-contributor$/.test(cheap)) {
|
|
122
|
+
return { ...stay, reason: 'a route may not change the training-data posture' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
provider,
|
|
127
|
+
model: cheap,
|
|
128
|
+
routed: true,
|
|
129
|
+
reason: `utility work routed to ${provider}'s cheaper tier`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A config object for the routed call, leaving everything except the model
|
|
135
|
+
* untouched — same provider, same key, same base URL.
|
|
136
|
+
*
|
|
137
|
+
* @param {Record<string, any>} config
|
|
138
|
+
* @param {RouteClass|string} kind
|
|
139
|
+
* @returns {{config: Record<string, any>, route: Route}}
|
|
140
|
+
*/
|
|
141
|
+
export function routedConfig(config, kind) {
|
|
142
|
+
const route = resolveRoute(config, kind);
|
|
143
|
+
if (!route.routed) return { config, route };
|
|
144
|
+
return { config: { ...config, model: route.model }, route };
|
|
145
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Sovereign Tool-Output Store
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS
|
|
4
|
+
// An oversized tool result used to be truncated to 32k chars and the remainder
|
|
5
|
+
// DESTROYED. The only route back to the missing part was to run the tool again:
|
|
6
|
+
// a second full execution, plus a second full-size result in the window. The
|
|
7
|
+
// decay stub in contextManager.js said so out loud — "Re-run the tool".
|
|
8
|
+
//
|
|
9
|
+
// Now the complete output is spilled to a file under ~/.causyne/outputs and the
|
|
10
|
+
// model is handed the path, so recovery is a bounded fs_read of a line range or
|
|
11
|
+
// a code_search_ripgrep over one file. A re-run costs a whole tool execution;
|
|
12
|
+
// a targeted read costs a few hundred tokens.
|
|
13
|
+
//
|
|
14
|
+
// ZERO-MALWARE: pure fs/path/os. No shell, no child process, no dependency, no
|
|
15
|
+
// dynamic eval — rules ZM-04/ZM-05 in .agents/rules.json hold by construction.
|
|
16
|
+
// Nothing here is obfuscated; it is plain readable code (Vision section 10).
|
|
17
|
+
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import os from 'os';
|
|
21
|
+
import { causyneHome } from './utils/paths.js';
|
|
22
|
+
|
|
23
|
+
const OUTPUT_DIR = path.join(causyneHome(), 'outputs');
|
|
24
|
+
|
|
25
|
+
// Matches the audit log's retention thinking: long enough that a spill is still
|
|
26
|
+
// there when the model comes back for it later in a long task, short enough
|
|
27
|
+
// that the directory cannot grow without bound.
|
|
28
|
+
const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
29
|
+
|
|
30
|
+
const FILE_PREFIX = 'tool_';
|
|
31
|
+
|
|
32
|
+
// How much of the spilled output travels back in the context window. The rest
|
|
33
|
+
// stays on disk behind the path.
|
|
34
|
+
const PREVIEW_LINES = 40;
|
|
35
|
+
const PREVIEW_CHARS = 4000;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The spill can be switched off entirely. Set CAUSYNE_OUTPUT_SPILL=0 to fall
|
|
39
|
+
* back to plain truncation.
|
|
40
|
+
*/
|
|
41
|
+
export function isSpillEnabled() {
|
|
42
|
+
return process.env.CAUSYNE_OUTPUT_SPILL !== '0';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ensureOutputDir() {
|
|
46
|
+
if (!fs.existsSync(OUTPUT_DIR)) {
|
|
47
|
+
// Owner-only: tool output routinely contains file contents and command
|
|
48
|
+
// output from the user's machine.
|
|
49
|
+
fs.mkdirSync(OUTPUT_DIR, { recursive: true, mode: 0o700 });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
function safeFragment(value, fallback) {
|
|
55
|
+
const cleaned = String(value == null ? '' : value).replace(/[^a-zA-Z0-9_-]/g, '');
|
|
56
|
+
return cleaned.length > 0 ? cleaned.slice(0, 64) : fallback;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function countLines(text) {
|
|
60
|
+
let lines = 1;
|
|
61
|
+
for (let i = 0; i < text.length; i++) {
|
|
62
|
+
if (text.charCodeAt(i) === 10) lines++;
|
|
63
|
+
}
|
|
64
|
+
return lines;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Take the head of `text` without splitting the whole string into an array —
|
|
69
|
+
* these payloads reach tens of megabytes and `split('\n')` on one of those is
|
|
70
|
+
* itself a memory problem.
|
|
71
|
+
*/
|
|
72
|
+
function headPreview(text) {
|
|
73
|
+
const charLimit = Math.min(text.length, PREVIEW_CHARS);
|
|
74
|
+
let cut = charLimit;
|
|
75
|
+
let seen = 0;
|
|
76
|
+
for (let i = 0; i < charLimit; i++) {
|
|
77
|
+
if (text.charCodeAt(i) === 10) {
|
|
78
|
+
seen++;
|
|
79
|
+
if (seen >= PREVIEW_LINES) { cut = i; break; }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return text.slice(0, cut);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Write the full tool output to disk.
|
|
87
|
+
*
|
|
88
|
+
* FAIL-OPEN BY CONTRACT: never throws and never blocks the tool call. A
|
|
89
|
+
* read-only disk or a full volume degrades this feature back to plain
|
|
90
|
+
* truncation, it does not fail the task.
|
|
91
|
+
*
|
|
92
|
+
* @returns {{path: string, bytes: number, lines: number, preview: string}|null}
|
|
93
|
+
*/
|
|
94
|
+
export function spillToolOutput(text, meta = {}) {
|
|
95
|
+
if (!isSpillEnabled()) return null;
|
|
96
|
+
if (typeof text !== 'string' || text.length === 0) return null;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
ensureOutputDir();
|
|
100
|
+
|
|
101
|
+
const taskId = safeFragment(meta.taskId, 'task');
|
|
102
|
+
const step = safeFragment(meta.step, '0');
|
|
103
|
+
const tool = safeFragment(meta.tool, 'tool');
|
|
104
|
+
const stamp = Date.now().toString(36);
|
|
105
|
+
const file = path.join(OUTPUT_DIR, `${FILE_PREFIX}${taskId}_${step}_${tool}_${stamp}.txt`);
|
|
106
|
+
|
|
107
|
+
fs.writeFileSync(file, text, { encoding: 'utf8', mode: 0o600 });
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
path: file,
|
|
111
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
112
|
+
lines: countLines(text),
|
|
113
|
+
preview: headPreview(text)
|
|
114
|
+
};
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.warn(`[output-store] spill skipped: ${e.message}`);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Render a tool result into a document that is actually READABLE by the tools
|
|
123
|
+
* we tell the model to use on it.
|
|
124
|
+
*
|
|
125
|
+
* Spilling `JSON.stringify(result)` was the obvious move and the wrong one: JSON
|
|
126
|
+
* escapes every newline, so a 900 KB result lands as ONE line. `fs_read` with a
|
|
127
|
+
* line range and `code_search_ripgrep` are both line-oriented, so the notice
|
|
128
|
+
* would have been pointing at a file neither of them could work with. Writing
|
|
129
|
+
* the raw field values keeps the line structure the payload actually had.
|
|
130
|
+
*/
|
|
131
|
+
export function renderSpillDocument(result, meta = {}) {
|
|
132
|
+
if (typeof result === 'string') return result;
|
|
133
|
+
if (result === null || typeof result !== 'object') return String(result);
|
|
134
|
+
|
|
135
|
+
const header = [
|
|
136
|
+
'=== CAUSYNE TOOL OUTPUT (full, untruncated) ===',
|
|
137
|
+
`tool : ${meta.tool || 'unknown'}`,
|
|
138
|
+
`task : ${meta.taskId || 'unknown'} step: ${meta.step == null ? 'unknown' : meta.step}`,
|
|
139
|
+
`saved: ${new Date().toISOString()}`
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
const sections = [];
|
|
143
|
+
const scalars = [];
|
|
144
|
+
|
|
145
|
+
for (const [key, value] of Object.entries(result)) {
|
|
146
|
+
if (typeof value === 'string' && value.length > 500) {
|
|
147
|
+
// Raw, unescaped — this is the whole point.
|
|
148
|
+
sections.push(`\n--- field: ${key} (${value.length} chars) ---\n${value}`);
|
|
149
|
+
} else if (value !== null && typeof value === 'object') {
|
|
150
|
+
sections.push(`\n--- field: ${key} (JSON) ---\n${JSON.stringify(value, null, 2)}`);
|
|
151
|
+
} else {
|
|
152
|
+
scalars.push(`${key}: ${JSON.stringify(value)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (scalars.length > 0) header.push('', ...scalars);
|
|
157
|
+
return header.join('\n') + '\n' + sections.join('\n') + '\n';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Spill a full tool RESULT (object or string) as a readable document.
|
|
162
|
+
* This is what the agent loop calls; spillToolOutput stays the raw primitive.
|
|
163
|
+
*/
|
|
164
|
+
export function spillToolResult(result, meta = {}) {
|
|
165
|
+
return spillToolOutput(renderSpillDocument(result, meta), meta);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The line the model reads in place of the bytes it did not get. It names the
|
|
170
|
+
* exact recovery route, because a notice that only says "truncated" is what
|
|
171
|
+
* drives the model back into a re-run.
|
|
172
|
+
*/
|
|
173
|
+
export function buildSpillNotice(spill) {
|
|
174
|
+
if (!spill) {
|
|
175
|
+
return '\n\n[... OUTPUT TRUNCATED TO PROTECT THE CONTEXT WINDOW. Narrow your arguments and retry if you need more ...]';
|
|
176
|
+
}
|
|
177
|
+
const kb = (spill.bytes / 1024).toFixed(1);
|
|
178
|
+
return `\n\n[... OUTPUT TRUNCATED HERE (${kb} KB, ${spill.lines} lines total). `
|
|
179
|
+
+ `THE COMPLETE OUTPUT IS SAVED AT: ${spill.path} — `
|
|
180
|
+
+ `read a line range from that file with fs_read, or search it with code_search_ripgrep. `
|
|
181
|
+
+ `DO NOT re-run the tool just to see the rest ...]`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The one-line form the decay stub uses once the payload leaves the window. */
|
|
185
|
+
export function buildDecayPointer(spill) {
|
|
186
|
+
if (!spill || !spill.path) return '';
|
|
187
|
+
return ` The full output is still on disk at ${spill.path} — read or search that file instead of re-running the tool.`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Delete spills past the retention window. Called fire-and-forget when a task
|
|
192
|
+
* starts, so the store self-maintains with no background timer and no installer
|
|
193
|
+
* behaviour (rule ZM-01).
|
|
194
|
+
*
|
|
195
|
+
* @returns {number} files removed
|
|
196
|
+
*/
|
|
197
|
+
export function pruneOutputStore() {
|
|
198
|
+
try {
|
|
199
|
+
if (!fs.existsSync(OUTPUT_DIR)) return 0;
|
|
200
|
+
const cutoff = Date.now() - RETENTION_MS;
|
|
201
|
+
let removed = 0;
|
|
202
|
+
|
|
203
|
+
for (const name of fs.readdirSync(OUTPUT_DIR)) {
|
|
204
|
+
if (!name.startsWith(FILE_PREFIX)) continue;
|
|
205
|
+
const file = path.join(OUTPUT_DIR, name);
|
|
206
|
+
try {
|
|
207
|
+
if (fs.statSync(file).mtimeMs < cutoff) {
|
|
208
|
+
fs.unlinkSync(file);
|
|
209
|
+
removed++;
|
|
210
|
+
}
|
|
211
|
+
} catch (e) { /* a file that vanished under us needs no pruning */ }
|
|
212
|
+
}
|
|
213
|
+
return removed;
|
|
214
|
+
} catch (e) {
|
|
215
|
+
console.warn(`[output-store] prune skipped: ${e.message}`);
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Absolute path of the store, for the `causyne-agent context` surface. */
|
|
221
|
+
export function getOutputDir() {
|
|
222
|
+
return OUTPUT_DIR;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Current store contents, newest first — used by the CLI reporting surface. */
|
|
226
|
+
export function listSpills(limit = 20) {
|
|
227
|
+
try {
|
|
228
|
+
if (!fs.existsSync(OUTPUT_DIR)) return [];
|
|
229
|
+
return fs.readdirSync(OUTPUT_DIR)
|
|
230
|
+
.filter(name => name.startsWith(FILE_PREFIX))
|
|
231
|
+
.map(name => {
|
|
232
|
+
const file = path.join(OUTPUT_DIR, name);
|
|
233
|
+
try {
|
|
234
|
+
const stats = fs.statSync(file);
|
|
235
|
+
return { path: file, bytes: stats.size, modified: stats.mtimeMs };
|
|
236
|
+
} catch (e) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
.filter(Boolean)
|
|
241
|
+
.sort((a, b) => b.modified - a.modified)
|
|
242
|
+
.slice(0, limit);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
export class WorkspacePerception {
|
|
6
|
+
async buildContext(workspacePath) {
|
|
7
|
+
const context = {};
|
|
8
|
+
const resolvedPath = workspacePath.replace(/^~\//, os.homedir() + '/');
|
|
9
|
+
|
|
10
|
+
// 1. Directory structure (tree, max depth 2 to keep context size manageable)
|
|
11
|
+
context.structure = await this.getDirectoryTree(resolvedPath, 2);
|
|
12
|
+
|
|
13
|
+
// 2. Framework detection
|
|
14
|
+
context.framework = await this.detectFramework(resolvedPath);
|
|
15
|
+
|
|
16
|
+
// 2b. Design system detection (Anti-Slop: tells the brain what's already installed)
|
|
17
|
+
context.designSystem = await this.detectDesignContext(resolvedPath);
|
|
18
|
+
|
|
19
|
+
// 3. Package/dependency context
|
|
20
|
+
try {
|
|
21
|
+
const pkgPath = path.join(resolvedPath, 'package.json');
|
|
22
|
+
const pkgStats = await fs.stat(pkgPath);
|
|
23
|
+
if (pkgStats.isFile()) {
|
|
24
|
+
const pkgData = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
25
|
+
context.packageJson = {
|
|
26
|
+
name: pkgData.name,
|
|
27
|
+
dependencies: pkgData.dependencies || {},
|
|
28
|
+
devDependencies: pkgData.devDependencies || {},
|
|
29
|
+
scripts: pkgData.scripts || {}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
} catch(e) {}
|
|
33
|
+
|
|
34
|
+
// 4. README (first 1000 chars)
|
|
35
|
+
try {
|
|
36
|
+
const readmePath = path.join(resolvedPath, 'README.md');
|
|
37
|
+
const readmeContent = await fs.readFile(readmePath, 'utf8');
|
|
38
|
+
context.readme = readmeContent.slice(0, 1000) + (readmeContent.length > 1000 ? '...' : '');
|
|
39
|
+
} catch(e) {}
|
|
40
|
+
|
|
41
|
+
// 5. OS & environment
|
|
42
|
+
context.os = { platform: os.platform(), arch: os.arch() };
|
|
43
|
+
|
|
44
|
+
return context;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getDirectoryTree(dir, maxDepth, currentDepth = 0) {
|
|
48
|
+
if (currentDepth > maxDepth) return '... (max depth reached)';
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
52
|
+
const tree = {};
|
|
53
|
+
|
|
54
|
+
let count = 0;
|
|
55
|
+
for (const item of items) {
|
|
56
|
+
if (item.name.startsWith('.') || item.name === 'node_modules') continue;
|
|
57
|
+
|
|
58
|
+
if (count >= 30) {
|
|
59
|
+
tree['...'] = `(${items.length - count} more items hidden)`;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
count++;
|
|
64
|
+
if (item.isDirectory()) {
|
|
65
|
+
tree[item.name] = await this.getDirectoryTree(path.join(dir, item.name), maxDepth, currentDepth + 1);
|
|
66
|
+
} else {
|
|
67
|
+
tree[item.name] = 'file';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return tree;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return `Error: ${e.message}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async detectFramework(dir) {
|
|
77
|
+
const detectors = [
|
|
78
|
+
{ file: 'next.config.js', type: 'nextjs', lang: 'javascript' },
|
|
79
|
+
{ file: 'next.config.ts', type: 'nextjs', lang: 'typescript' },
|
|
80
|
+
{ file: 'vite.config.ts', type: 'vite', lang: 'typescript' },
|
|
81
|
+
{ file: 'tsconfig.json', type: 'node', lang: 'typescript' },
|
|
82
|
+
{ file: 'package.json', type: 'node', lang: 'javascript' },
|
|
83
|
+
{ file: 'manage.py', type: 'django', lang: 'python' },
|
|
84
|
+
{ file: 'requirements.txt', type: 'python', lang: 'python' },
|
|
85
|
+
{ file: 'build.gradle.kts', type: 'android', lang: 'kotlin' },
|
|
86
|
+
{ file: 'docker-compose.yml', type: 'docker', lang: 'various' },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
for (const d of detectors) {
|
|
90
|
+
try {
|
|
91
|
+
await fs.access(path.join(dir, d.file));
|
|
92
|
+
return { type: d.type, language: d.lang };
|
|
93
|
+
} catch (e) {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { type: 'unknown', language: 'unknown' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async detectDesignContext(dir) {
|
|
100
|
+
const designDetectors = [
|
|
101
|
+
{ dep: 'tailwindcss', system: 'Tailwind CSS' },
|
|
102
|
+
{ dep: '@astryxdesign/core', system: 'Astryx Design System' },
|
|
103
|
+
{ dep: '@mui/material', system: 'Material-UI (MUI)' },
|
|
104
|
+
{ dep: '@chakra-ui/react', system: 'Chakra UI' },
|
|
105
|
+
{ dep: 'bootstrap', system: 'Bootstrap' },
|
|
106
|
+
{ dep: 'styled-components', system: 'Styled Components' },
|
|
107
|
+
{ dep: '@emotion/react', system: 'Emotion CSS-in-JS' },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
112
|
+
const pkgData = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
113
|
+
const allDeps = { ...pkgData.dependencies, ...pkgData.devDependencies };
|
|
114
|
+
|
|
115
|
+
const detected = designDetectors
|
|
116
|
+
.filter(d => allDeps[d.dep])
|
|
117
|
+
.map(d => d.system);
|
|
118
|
+
|
|
119
|
+
return detected.length > 0 ? detected : null;
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|