@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,305 @@
|
|
|
1
|
+
import { TokenTracker } from '../tokenTracker.js';
|
|
2
|
+
|
|
3
|
+
// Subagent delegation.
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS USED TO DO (finding D-7)
|
|
6
|
+
//
|
|
7
|
+
// The previous implementation imported `fork` and `Worker` and used neither.
|
|
8
|
+
// It waited on a `setTimeout`, then returned:
|
|
9
|
+
//
|
|
10
|
+
// "Subagent [X] successfully executed task: "..." within scope [...].
|
|
11
|
+
// Zero errors detected."
|
|
12
|
+
//
|
|
13
|
+
// for work that had never happened. It pushed a "✅ Subagent Completed" push
|
|
14
|
+
|
|
15
|
+
// the tokens it pretended to spend. Worst of all, that sentence went straight
|
|
16
|
+
// into the parent model's context, so the model reasoned onward from a lie —
|
|
17
|
+
// and the schema described the whole thing as "surpasses standard CLI
|
|
18
|
+
// single-loop agents".
|
|
19
|
+
//
|
|
20
|
+
// A tool that reports success it did not earn is worse than a tool that does
|
|
21
|
+
// not exist: a missing tool fails loudly and the agent routes around it, while
|
|
22
|
+
// a lying tool corrupts every conclusion drawn after it.
|
|
23
|
+
//
|
|
24
|
+
// WHAT IT DOES NOW
|
|
25
|
+
//
|
|
26
|
+
// It runs a real AgentLoop. Each subagent gets its own message history, its own
|
|
27
|
+
// step ceiling, its own token accounting, and a tool scope that is actually
|
|
28
|
+
// enforced by the loop (agentLoop.js filters the catalogue against
|
|
29
|
+
|
|
30
|
+
// transport, tagged with its id. `delegate_and_wait` returns the child's real
|
|
31
|
+
// final message. When a subagent fails, this tool says so.
|
|
32
|
+
//
|
|
33
|
+
// The loops run in-process on purpose. Delegation and real concurrency come
|
|
34
|
+
// from several loops awaiting network I/O at once; forking a child process of
|
|
35
|
+
// our own binary would add a process-spawner pattern to the artefact for no
|
|
36
|
+
// capability we do not already have.
|
|
37
|
+
|
|
38
|
+
const activeSubagents = new Map();
|
|
39
|
+
const MAX_CONCURRENT_SUBAGENTS = 4;
|
|
40
|
+
|
|
41
|
+
// A subagent may never delegate again. The depth argument documents the rule;
|
|
42
|
+
// withholding the tool from the child's scope is what enforces it.
|
|
43
|
+
const SUBAGENT_TOOL_NAME = 'agent.subagent_orchestrate';
|
|
44
|
+
const DEFAULT_TOOL_SCOPE = ['fs_read', 'fs_find', 'fs_search', 'fs_list', 'code_search_ast'];
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
function childTransport(parentLoop, subagentId, name) {
|
|
48
|
+
return {
|
|
49
|
+
get readyState() {
|
|
50
|
+
return parentLoop && parentLoop.eventSink ? parentLoop.eventSink.readyState : 3;
|
|
51
|
+
},
|
|
52
|
+
send(raw) {
|
|
53
|
+
if (!parentLoop || typeof parentLoop.emitEvent !== 'function') return;
|
|
54
|
+
let frame;
|
|
55
|
+
try { frame = JSON.parse(raw); } catch (e) { return; }
|
|
56
|
+
parentLoop.emitEvent({ ...frame, subagentId, subagentName: name });
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function executeSubagent(record, parentLoop) {
|
|
62
|
+
// Imported lazily: agentLoop.js reaches this module through the tool
|
|
63
|
+
// registry, so a static import would close a cycle.
|
|
64
|
+
const { AgentLoop } = await import('../agentLoop.js');
|
|
65
|
+
|
|
66
|
+
const transport = childTransport(parentLoop, record.subagentId, record.name);
|
|
67
|
+
const child = new AgentLoop(transport, record.subagentId, record.prompt);
|
|
68
|
+
|
|
69
|
+
// A delegated task is a bounded errand, not a second full session.
|
|
70
|
+
child.maxSteps = Math.min(child.maxSteps || 25, record.maxSteps);
|
|
71
|
+
child.maxCircuitBreaker = child.maxSteps;
|
|
72
|
+
child.messages = [];
|
|
73
|
+
child.toolScope = new Set(record.toolScope.filter(n => n !== SUBAGENT_TOOL_NAME));
|
|
74
|
+
child.currentTaskPrompt = record.prompt;
|
|
75
|
+
// Depth is carried so the child's own guard can refuse, even if a future
|
|
76
|
+
// change ever puts the delegation tool back in scope.
|
|
77
|
+
child.subagentDepth = record.depth;
|
|
78
|
+
|
|
79
|
+
record.loop = child;
|
|
80
|
+
record.startedAtMs = Date.now();
|
|
81
|
+
|
|
82
|
+
await child.run();
|
|
83
|
+
|
|
84
|
+
const finalAssistant = [...child.messages].reverse()
|
|
85
|
+
.find(m => m.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
|
|
86
|
+
const toolCalls = child.messages.filter(m => m.role === 'tool');
|
|
87
|
+
const failures = toolCalls.filter(m => {
|
|
88
|
+
try { return JSON.parse(m.content).success === false; } catch (e) { return false; }
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
subagentId: record.subagentId,
|
|
93
|
+
name: record.name,
|
|
94
|
+
stepsCompleted: child.stepsCompleted,
|
|
95
|
+
toolCallsMade: toolCalls.length,
|
|
96
|
+
toolFailures: failures.length,
|
|
97
|
+
elapsedMs: Date.now() - record.startedAtMs,
|
|
98
|
+
// The child's own words, not a template. If it produced nothing, say that
|
|
99
|
+
// rather than inventing a summary.
|
|
100
|
+
summary: finalAssistant ? finalAssistant.content : null,
|
|
101
|
+
stoppedEarly: child.stepsCompleted >= child.maxSteps
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const subagentOrchestrator = {
|
|
106
|
+
schema: {
|
|
107
|
+
name: SUBAGENT_TOOL_NAME,
|
|
108
|
+
description: 'Delegate a bounded sub-task to an independent agent loop with its own context, step budget and restricted tool scope. Use it to parallelise genuinely independent work (research several files, audit separate modules). The subagent runs a real agent loop and returns its actual result; it cannot delegate further.',
|
|
109
|
+
parameters: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
action: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
enum: ['spawn', 'list', 'kill', 'delegate_and_wait'],
|
|
115
|
+
description: 'spawn (start in the background and return immediately), list (show active subagents), kill (stop one), delegate_and_wait (run it and return the result).'
|
|
116
|
+
},
|
|
117
|
+
name: { type: 'string', description: 'A short name for the subagent, e.g. Audit_Worker.' },
|
|
118
|
+
prompt: { type: 'string', description: 'The complete instruction for the subagent. It has no memory of this conversation, so it must be self-contained.' },
|
|
119
|
+
subagentId: { type: 'string', description: 'Target subagent for the kill action.' },
|
|
120
|
+
toolScope: { type: 'array', items: { type: 'string' }, description: 'Catalogue tool names the subagent may use. Defaults to read-only inspection tools. It can never receive the delegation tool itself.' },
|
|
121
|
+
maxSteps: { type: 'integer', description: 'Step ceiling for the subagent. Defaults to 25.' },
|
|
122
|
+
maxTokens: { type: 'integer', description: 'Advisory token budget for this subagent. Defaults to 50000.' },
|
|
123
|
+
depth: { type: 'integer', description: 'Hierarchy depth. 0 = master, 1 = worker. Subagents cannot spawn subagents.' }
|
|
124
|
+
},
|
|
125
|
+
required: ['action']
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async execute(args, context = {}) {
|
|
130
|
+
try {
|
|
131
|
+
const action = args.action;
|
|
132
|
+
const parentLoop = context.parentLoop || null;
|
|
133
|
+
|
|
134
|
+
if (action === 'list') {
|
|
135
|
+
const list = [...activeSubagents.values()].map(worker => ({
|
|
136
|
+
subagentId: worker.subagentId,
|
|
137
|
+
name: worker.name,
|
|
138
|
+
status: worker.status,
|
|
139
|
+
depth: worker.depth,
|
|
140
|
+
toolScope: worker.toolScope,
|
|
141
|
+
startTime: worker.startTime,
|
|
142
|
+
stepsCompleted: worker.loop ? worker.loop.stepsCompleted : 0,
|
|
143
|
+
summary: worker.summary,
|
|
144
|
+
error: worker.error
|
|
145
|
+
}));
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
output: JSON.stringify({
|
|
149
|
+
active_workers: list.filter(w => w.status === 'running').length,
|
|
150
|
+
max_concurrent_allowed: MAX_CONCURRENT_SUBAGENTS,
|
|
151
|
+
workers: list
|
|
152
|
+
}, null, 2)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (action === 'kill') {
|
|
157
|
+
const id = args.subagentId || args.name;
|
|
158
|
+
if (!id) return { success: false, error: 'subagentId or name is required to stop a subagent.' };
|
|
159
|
+
for (const [key, worker] of activeSubagents.entries()) {
|
|
160
|
+
if (key === id || worker.name === id) {
|
|
161
|
+
// Safe now that run() rearms the controller (Phase 2): aborting one
|
|
162
|
+
// loop no longer poisons any other.
|
|
163
|
+
try { if (worker.loop) worker.loop.abort(); } catch (e) {}
|
|
164
|
+
worker.status = 'killed';
|
|
165
|
+
activeSubagents.delete(key);
|
|
166
|
+
return { success: true, output: `Subagent ${key} (${worker.name}) stopped.` };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { success: false, error: `Subagent ${id} not found.` };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (action !== 'spawn' && action !== 'delegate_and_wait') {
|
|
173
|
+
return { success: false, error: `Unknown action: ${action}` };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const prompt = args.prompt;
|
|
177
|
+
if (!prompt) return { success: false, error: 'A prompt is required to delegate a task.' };
|
|
178
|
+
|
|
179
|
+
const depth = args.depth !== undefined ? args.depth : 1;
|
|
180
|
+
if (depth > 1) {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: `Subagent hierarchy depth violation: depth ${depth} exceeds the 2-tier boundary (master -> worker). Subagents cannot delegate further.`
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (parentLoop && parentLoop.subagentDepth !== undefined) {
|
|
187
|
+
return { success: false, error: 'A subagent cannot delegate to another subagent.' };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const running = [...activeSubagents.values()].filter(w => w.status === 'running').length;
|
|
191
|
+
if (running >= MAX_CONCURRENT_SUBAGENTS) {
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
error: `Subagent concurrency limit reached (${MAX_CONCURRENT_SUBAGENTS} running). Stop one, or wait for it to finish.`
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const name = args.name || `Worker_${Math.random().toString(36).slice(2, 7)}`;
|
|
199
|
+
const subagentId = `sub_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
200
|
+
const requestedScope = Array.isArray(args.toolScope) && args.toolScope.length > 0
|
|
201
|
+
? args.toolScope
|
|
202
|
+
: DEFAULT_TOOL_SCOPE;
|
|
203
|
+
|
|
204
|
+
// Reject an unknown tool name instead of silently accepting it. The old
|
|
205
|
+
// implementation accepted anything, which is easy to do when the scope is
|
|
206
|
+
// never used for anything.
|
|
207
|
+
const { TOOL_DEFINITIONS } = await import('./registry.js');
|
|
208
|
+
const catalogue = new Set(TOOL_DEFINITIONS.map(t => t.name));
|
|
209
|
+
const unknown = requestedScope.filter(n => !catalogue.has(n) && n !== SUBAGENT_TOOL_NAME);
|
|
210
|
+
if (unknown.length > 0) {
|
|
211
|
+
return { success: false, error: `Unknown tools in toolScope: ${unknown.join(', ')}.` };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const record = {
|
|
215
|
+
subagentId,
|
|
216
|
+
name,
|
|
217
|
+
prompt,
|
|
218
|
+
toolScope: requestedScope,
|
|
219
|
+
depth,
|
|
220
|
+
maxSteps: args.maxSteps || 25,
|
|
221
|
+
maxTokens: args.maxTokens || 50000,
|
|
222
|
+
status: 'running',
|
|
223
|
+
startTime: new Date().toISOString(),
|
|
224
|
+
summary: null,
|
|
225
|
+
error: null,
|
|
226
|
+
loop: null
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Budget pre-check against the real tracker, before any work starts.
|
|
230
|
+
const tracker = new TokenTracker();
|
|
231
|
+
const estimated = Math.ceil(prompt.length / 4) + 1500;
|
|
232
|
+
const check = await tracker.trackUsage(estimated, 0);
|
|
233
|
+
if (!check.allowed) {
|
|
234
|
+
return {
|
|
235
|
+
success: false,
|
|
236
|
+
error: `Token budget block: ${check.reason || 'the budget threshold would be exceeded by this subagent.'}`
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
activeSubagents.set(subagentId, record);
|
|
241
|
+
|
|
242
|
+
const work = executeSubagent(record, parentLoop)
|
|
243
|
+
.then(result => {
|
|
244
|
+
record.status = 'completed';
|
|
245
|
+
record.summary = result.summary;
|
|
246
|
+
return result;
|
|
247
|
+
})
|
|
248
|
+
.catch(error => {
|
|
249
|
+
record.status = 'failed';
|
|
250
|
+
record.error = error.message;
|
|
251
|
+
throw error;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (action === 'delegate_and_wait') {
|
|
255
|
+
try {
|
|
256
|
+
const result = await work;
|
|
257
|
+
activeSubagents.delete(subagentId);
|
|
258
|
+
// No summary means the subagent produced nothing. Report that
|
|
259
|
+
// plainly — it is a real outcome, and pretending otherwise is the
|
|
260
|
+
// exact defect this rewrite exists to remove.
|
|
261
|
+
if (!result.summary) {
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
error: `Subagent [${name}] finished without producing a result after ${result.stepsCompleted} steps.`,
|
|
265
|
+
output: JSON.stringify(result, null, 2)
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return { success: true, output: JSON.stringify(result, null, 2) };
|
|
269
|
+
} catch (error) {
|
|
270
|
+
activeSubagents.delete(subagentId);
|
|
271
|
+
return { success: false, error: `Subagent [${name}] failed: ${error.message}` };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Completion stays on the parent's local event stream.
|
|
276
|
+
work
|
|
277
|
+
.then(result => {
|
|
278
|
+
parentLoop.emitEvent({
|
|
279
|
+
type: 'notice', subagentId, subagentName: name,
|
|
280
|
+
message: `Subagent [${name}] finished in ${result.stepsCompleted} steps.`
|
|
281
|
+
});
|
|
282
|
+
})
|
|
283
|
+
.catch(error => {
|
|
284
|
+
parentLoop.emitEvent({
|
|
285
|
+
type: 'notice', tone: 'error', subagentId, subagentName: name,
|
|
286
|
+
message: `Subagent [${name}] failed: ${error.message}`
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
success: true,
|
|
292
|
+
output: JSON.stringify({
|
|
293
|
+
subagentId,
|
|
294
|
+
name,
|
|
295
|
+
status: 'running',
|
|
296
|
+
toolScope: requestedScope,
|
|
297
|
+
maxSteps: record.maxSteps,
|
|
298
|
+
message: `Subagent [${name}] is running. Poll it with action "list"; the result appears there when it finishes.`
|
|
299
|
+
}, null, 2)
|
|
300
|
+
};
|
|
301
|
+
} catch (e) {
|
|
302
|
+
return { success: false, error: e.message };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
|
|
4
|
+
// Phase 1 AGI Upgrade: ANSI Stripper
|
|
5
|
+
function stripAnsi(string) {
|
|
6
|
+
const pattern = [
|
|
7
|
+
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
|
|
8
|
+
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))'
|
|
9
|
+
].join('|');
|
|
10
|
+
return string.replace(new RegExp(pattern, 'g'), '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const sysRun = {
|
|
14
|
+
schema: {
|
|
15
|
+
name: 'sysRun',
|
|
16
|
+
description: 'Execute a shell command. CRITICAL CONSTRAINTS: 1. NEVER use interactive flags (like git rebase -i). 2. Always use non-interactive flags (--yes, -y). 3. If running git commit, always run git status, git diff, and git log first.',
|
|
17
|
+
parameters: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
command: { type: 'string', description: 'The shell command to run' },
|
|
21
|
+
cwd: { type: 'string', description: 'Working directory' },
|
|
22
|
+
timeout: { type: 'number', description: 'Timeout in ms (default 60000)' }
|
|
23
|
+
},
|
|
24
|
+
required: ['command'],
|
|
25
|
+
additionalProperties: false
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
async execute(args, context = {}) {
|
|
29
|
+
const cwd = args.cwd ? args.cwd.replace(/^~\//, os.homedir() + '/') : process.cwd();
|
|
30
|
+
const timeoutMs = args.timeout || 60000;
|
|
31
|
+
const signal = context.signal;
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
// Phase 1 AGI Upgrade: Spawn instead of Exec
|
|
35
|
+
const shell = os.platform() === 'win32' ? 'cmd.exe' : '/bin/bash';
|
|
36
|
+
const shellArgs = os.platform() === 'win32' ? ['/c', args.command] : ['-c', args.command];
|
|
37
|
+
|
|
38
|
+
const child = spawn(shell, shellArgs, { cwd, signal });
|
|
39
|
+
|
|
40
|
+
// Phase 1 AGI Upgrade: 50KB Ring Buffer (FIFO)
|
|
41
|
+
const MAX_LENGTH = 50000;
|
|
42
|
+
let outputBuffer = '';
|
|
43
|
+
|
|
44
|
+
const appendToBuffer = (data) => {
|
|
45
|
+
outputBuffer += data.toString();
|
|
46
|
+
if (outputBuffer.length > MAX_LENGTH) {
|
|
47
|
+
// Truncate from the start (keep the most recent 50KB)
|
|
48
|
+
outputBuffer = '...[OUTPUT TRUNCATED]...\n' + outputBuffer.slice(outputBuffer.length - MAX_LENGTH);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
child.stdout.on('data', appendToBuffer);
|
|
53
|
+
child.stderr.on('data', appendToBuffer);
|
|
54
|
+
|
|
55
|
+
let timeoutId;
|
|
56
|
+
if (timeoutMs > 0) {
|
|
57
|
+
timeoutId = setTimeout(() => {
|
|
58
|
+
child.kill('SIGKILL');
|
|
59
|
+
resolve({ success: false, output: stripAnsi(outputBuffer), error: `Process timed out after ${timeoutMs}ms`, exitCode: 124 });
|
|
60
|
+
}, timeoutMs);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
child.on('error', (err) => {
|
|
64
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
65
|
+
resolve({ success: false, output: stripAnsi(outputBuffer), error: err.message, exitCode: 1 });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
child.on('close', (code) => {
|
|
69
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
70
|
+
const cleanOutput = stripAnsi(outputBuffer);
|
|
71
|
+
|
|
72
|
+
if (code === 0) {
|
|
73
|
+
resolve({ success: true, output: cleanOutput, exitCode: 0 });
|
|
74
|
+
} else {
|
|
75
|
+
if (signal && signal.aborted) {
|
|
76
|
+
resolve({ success: false, output: cleanOutput, error: 'Process killed by user.', exitCode: 1 });
|
|
77
|
+
} else {
|
|
78
|
+
resolve({ success: false, output: cleanOutput, error: `Command exited with code ${code}`, exitCode: code });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const sysRunBg = {
|
|
87
|
+
schema: {
|
|
88
|
+
name: 'sysRunBg',
|
|
89
|
+
description: 'Start a long-running process in background (dev servers, watchers)',
|
|
90
|
+
parameters: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
command: { type: 'string' },
|
|
94
|
+
cwd: { type: 'string' },
|
|
95
|
+
label: { type: 'string' },
|
|
96
|
+
maxDuration: { type: 'number' }
|
|
97
|
+
},
|
|
98
|
+
required: ['command', 'label']
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
async execute(args) {
|
|
102
|
+
// Phase 1 AGI Upgrade: Real Detached Process Implementation
|
|
103
|
+
try {
|
|
104
|
+
const cwd = args.cwd ? args.cwd.replace(/^~\//, os.homedir() + '/') : process.cwd();
|
|
105
|
+
const shell = os.platform() === 'win32' ? 'cmd.exe' : '/bin/bash';
|
|
106
|
+
const shellArgs = os.platform() === 'win32' ? ['/c', args.command] : ['-c', args.command];
|
|
107
|
+
|
|
108
|
+
const child = spawn(shell, shellArgs, {
|
|
109
|
+
cwd,
|
|
110
|
+
detached: true,
|
|
111
|
+
stdio: 'ignore' // We don't track output for background tasks currently
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
child.unref(); // Allow Node to exit even if this child is running
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
success: true,
|
|
118
|
+
output: `Successfully started background task [${args.label}]: ${args.command}`,
|
|
119
|
+
pid: child.pid
|
|
120
|
+
};
|
|
121
|
+
} catch (e) {
|
|
122
|
+
return { success: false, error: e.message };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export const todoWrite = {
|
|
5
|
+
schema: {
|
|
6
|
+
name: 'TodoWrite',
|
|
7
|
+
description: 'Dynamic Context Injector: Create and manage structured tasks. Forces atomic, granular steps instead of vague goals.',
|
|
8
|
+
parameters: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
todos: {
|
|
12
|
+
type: 'array',
|
|
13
|
+
description: 'The updated todo list (JSON state)',
|
|
14
|
+
items: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
id: { type: 'string' },
|
|
18
|
+
content: { type: 'string', minLength: 5, description: 'Must be an atomic, actionable step.' },
|
|
19
|
+
status: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
enum: ['pending', 'in_progress', 'completed']
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
required: ['id', 'content', 'status'],
|
|
25
|
+
additionalProperties: false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
required: ['todos'],
|
|
30
|
+
additionalProperties: false
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
async execute(args) {
|
|
34
|
+
try {
|
|
35
|
+
const todoPath = path.join(process.cwd(), '.causyne_todos.json');
|
|
36
|
+
const mdPath = path.join(process.cwd(), 'AGI_PLAN.md');
|
|
37
|
+
|
|
38
|
+
// Phase 2 AGI Upgrade: Atomic Validation
|
|
39
|
+
for (const task of args.todos) {
|
|
40
|
+
const words = task.content.split(' ').length;
|
|
41
|
+
if (words < 2) {
|
|
42
|
+
return { success: false, error: `Task [${task.id}] is too vague ("${task.content}"). Rewrite it into an atomic, actionable step (e.g., "Scaffold React Navbar").` };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Save JSON state
|
|
47
|
+
await fs.writeFile(todoPath, JSON.stringify(args.todos, null, 2), 'utf8');
|
|
48
|
+
|
|
49
|
+
// Render to MD for user visibility
|
|
50
|
+
let mdContent = '# AGI Execution Plan\n\n';
|
|
51
|
+
for (const t of args.todos) {
|
|
52
|
+
let box = '[ ]';
|
|
53
|
+
if (t.status === 'in_progress') box = '[/]';
|
|
54
|
+
if (t.status === 'completed') box = '[x]';
|
|
55
|
+
mdContent += `- ${box} ${t.id}: ${t.content}\n`;
|
|
56
|
+
}
|
|
57
|
+
await fs.writeFile(mdPath, mdContent, 'utf8');
|
|
58
|
+
|
|
59
|
+
// Phase 2 AGI Upgrade: Memory Hook (Signaling back to AgentLoop context window)
|
|
60
|
+
const inProgress = args.todos.filter(t => t.status === 'in_progress');
|
|
61
|
+
const completed = args.todos.filter(t => t.status === 'completed');
|
|
62
|
+
|
|
63
|
+
if (inProgress.length > 1) {
|
|
64
|
+
return { success: true, output: `[Memory Hook] Saved. WARNING: You have ${inProgress.length} tasks in_progress. Only ONE task may be in progress at a time.` };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
success: true,
|
|
69
|
+
output: `[Memory Hook] Tasks synchronized. ${completed.length} completed, ${inProgress.length} active. Focus locked on: ${inProgress[0]?.content || 'None'}`
|
|
70
|
+
};
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return { success: false, error: e.message };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// The tool-dispatch discipline: permission → pre-hook → execute → post-hook → result.
|
|
2
|
+
//
|
|
3
|
+
// Ported from the reference agent loop (claw-code rust/crates/runtime/src/
|
|
4
|
+
// conversation.rs:201-254). The single rule that matters: a denial or a failure
|
|
5
|
+
// is NEVER thrown out of the loop — it comes back as a tool result with an error
|
|
6
|
+
// flag, so the model sees it and self-corrects. Only a fatal/auth/transport error
|
|
7
|
+
// (handled by the caller) ends the turn.
|
|
8
|
+
//
|
|
9
|
+
// This is pure logic with no I/O of its own: the caller injects `authorize`,
|
|
10
|
+
// `preHook`, `postHook` and `execute`. That is what makes the agent loop's tool
|
|
11
|
+
// handling testable for the first time — a scripted set of these four functions
|
|
12
|
+
// exercises deny → result, execute → result, throw → recovered-not-fatal without
|
|
13
|
+
// a live model or a real filesystem.
|
|
14
|
+
//
|
|
15
|
+
// Pure Node. No dependency, no shell.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A tool-result message in the shape the providers expect, matching what
|
|
19
|
+
* agentLoop.js already pushes: { role:'tool', tool_call_id, name, content }
|
|
20
|
+
* where content is a JSON string carrying at least { success }.
|
|
21
|
+
*/
|
|
22
|
+
export function toolResultMessage(toolCallId, name, payload) {
|
|
23
|
+
return {
|
|
24
|
+
role: 'tool',
|
|
25
|
+
tool_call_id: toolCallId,
|
|
26
|
+
name,
|
|
27
|
+
content: JSON.stringify(payload)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Fold hook feedback lines into a result payload.
|
|
33
|
+
*
|
|
34
|
+
* Hook stdout is model-visible feedback, not a log line (the reference appends it
|
|
35
|
+
* to the tool output). `denied` marks it as the reason a call was refused.
|
|
36
|
+
*/
|
|
37
|
+
export function mergeHookFeedback(payload, messages, denied = false) {
|
|
38
|
+
if (!messages || messages.length === 0) return payload;
|
|
39
|
+
const label = denied ? 'Hook feedback (denied)' : 'Hook feedback';
|
|
40
|
+
const note = `${label}:\n${messages.join('\n')}`;
|
|
41
|
+
const existing = typeof payload.hookFeedback === 'string' ? payload.hookFeedback + '\n' : '';
|
|
42
|
+
return { ...payload, hookFeedback: existing + note };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ALLOW = { denied: false, messages: [] };
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run one tool call through the discipline and return a result message.
|
|
49
|
+
*
|
|
50
|
+
* Injected functions (all optional except execute):
|
|
51
|
+
* authorize(name, args) -> { allow:true } | { allow:false, reason }
|
|
52
|
+
* preHook(name, args) -> { denied, messages } (Stage 3 fills this in)
|
|
53
|
+
* execute(name, args) -> any payload, or throws
|
|
54
|
+
* postHook(name, args, payload, isError) -> { denied, messages }
|
|
55
|
+
*
|
|
56
|
+
* Returns { message, denied, isError }. Never throws for a tool-level failure —
|
|
57
|
+
* a thrown execute() becomes an is_error result. The caller decides, from the
|
|
58
|
+
* result, whether to break (it never should for a tool error) or continue.
|
|
59
|
+
*/
|
|
60
|
+
export async function dispatchToolCall({
|
|
61
|
+
toolCallId,
|
|
62
|
+
name,
|
|
63
|
+
args = {},
|
|
64
|
+
authorize = null,
|
|
65
|
+
preHook = null,
|
|
66
|
+
postHook = null,
|
|
67
|
+
execute
|
|
68
|
+
}) {
|
|
69
|
+
// 1. Permission. A denial is a result, not an exception.
|
|
70
|
+
if (authorize) {
|
|
71
|
+
const decision = authorize(name, args) || {};
|
|
72
|
+
if (decision.allow === false) {
|
|
73
|
+
return {
|
|
74
|
+
denied: true,
|
|
75
|
+
isError: true,
|
|
76
|
+
message: toolResultMessage(toolCallId, name, {
|
|
77
|
+
success: false,
|
|
78
|
+
error: decision.reason || `Tool "${name}" was not permitted.`
|
|
79
|
+
})
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 2. PreToolUse hook. Denial short-circuits before execute.
|
|
85
|
+
const pre = (preHook ? await preHook(name, args) : ALLOW) || ALLOW;
|
|
86
|
+
if (pre.denied) {
|
|
87
|
+
return {
|
|
88
|
+
denied: true,
|
|
89
|
+
isError: true,
|
|
90
|
+
message: toolResultMessage(toolCallId, name, mergeHookFeedback(
|
|
91
|
+
{ success: false, error: `PreToolUse hook denied tool "${name}".` },
|
|
92
|
+
pre.messages,
|
|
93
|
+
true
|
|
94
|
+
))
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 3. Execute. A throw becomes an is_error result — never propagated.
|
|
99
|
+
let payload;
|
|
100
|
+
let isError = false;
|
|
101
|
+
try {
|
|
102
|
+
payload = await execute(name, args);
|
|
103
|
+
if (payload == null) payload = { success: true };
|
|
104
|
+
if (typeof payload !== 'object') payload = { success: true, output: payload };
|
|
105
|
+
if (payload.success === undefined) payload.success = true;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
isError = true;
|
|
108
|
+
payload = { success: false, error: error && error.message ? error.message : String(error) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Pre-hook feedback rides along even on a successful call.
|
|
112
|
+
payload = mergeHookFeedback(payload, pre.messages, false);
|
|
113
|
+
|
|
114
|
+
// 4. PostToolUse hook. Can flip a success to an error and add feedback.
|
|
115
|
+
const post = (postHook ? await postHook(name, args, payload, isError) : ALLOW) || ALLOW;
|
|
116
|
+
if (post.denied) {
|
|
117
|
+
isError = true;
|
|
118
|
+
payload = { ...payload, success: false };
|
|
119
|
+
}
|
|
120
|
+
payload = mergeHookFeedback(payload, post.messages, post.denied);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
denied: false,
|
|
124
|
+
isError,
|
|
125
|
+
message: toolResultMessage(toolCallId, name, payload)
|
|
126
|
+
};
|
|
127
|
+
}
|