@causyne/agent 1.10.17 → 1.10.18
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/docs/ARCHITECTURE.md +25 -8
- package/docs/SECURITY_REVIEW.md +4 -4
- package/docs/THIRD_PARTY_NOTICES.md +3 -0
- package/package.json +1 -1
- package/src/agentLoop.js +99 -19
- package/src/browser/{camofoxRuntime.js → browserRuntime.js} +10 -15
- package/src/browser/{camofoxRuntime.ts → browserRuntime.ts} +11 -16
- package/src/browser/causyneBrowser.js +6 -6
- package/src/browser/causyneBrowser.ts +9 -9
- package/src/cliMode.js +13 -1
- package/src/decisionPolicy.js +68 -0
- package/src/decisionPolicy.ts +106 -0
- package/src/implementationPlan.js +50 -0
- package/src/implementationPlan.ts +59 -0
- package/src/provisioning/capabilityProvisioner.js +1 -1
- package/src/taskShape.js +1 -1
- package/src/taskShape.ts +1 -1
- package/src/toolPacks.js +7 -5
- package/src/tools/browserAutomator.js +1 -1
- package/src/tools/browserAutomator.ts +1 -1
- package/src/tools/manageLocalAiDependencies.js +1 -1
- package/src/tools/registry.js +4 -3
- package/src/tools/requestDecision.js +87 -0
- package/src/tools/requestDecision.ts +107 -0
- package/src/tools/updatePlan.js +37 -84
- package/src/tools/updatePlan.ts +48 -0
- package/src/tools/webFetch.js +1 -1
- package/src/tools/webFetch.ts +1 -1
- package/src/tools/webSearch.js +1 -1
- package/src/tools/webSearch.ts +1 -1
- package/src/tui/ask.js +41 -1
- package/src/tui/chatRenderer.js +28 -0
- package/src/tui/dialogs.js +91 -0
- package/src/tui/prompt.js +32 -3
- package/src/tui/toolDisplay.js +0 -1
- package/src/tools/todoWrite.js +0 -75
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export type DecisionKind = 'architecture' | 'product' | 'data' | 'security' | 'workflow' | 'other';
|
|
2
|
+
|
|
3
|
+
export interface DecisionOption {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DecisionRequest {
|
|
10
|
+
id: string;
|
|
11
|
+
kind: DecisionKind;
|
|
12
|
+
header: string;
|
|
13
|
+
question: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
options: DecisionOption[];
|
|
16
|
+
allowCustom: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DecisionAnswer {
|
|
20
|
+
requestId: string;
|
|
21
|
+
optionId: string | null;
|
|
22
|
+
label: string;
|
|
23
|
+
custom: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DecisionRequirement {
|
|
27
|
+
required: boolean;
|
|
28
|
+
kind: DecisionKind;
|
|
29
|
+
reason: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ARCHITECTURE_TERMS = [
|
|
33
|
+
'architecture', 'arsitektur',
|
|
34
|
+
'framework', 'runtime', 'database', 'storage', 'authentication', 'authorization',
|
|
35
|
+
'deployment', 'hosting', 'protocol', 'plugin system', 'sandbox', 'license',
|
|
36
|
+
'typescript', 'javascript', 'python', 'rust', 'kotlin', 'swift'
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const CHANGE_TERMS = [
|
|
40
|
+
'change', 'replace', 'switch', 'choose', 'decide', 'redesign', 'restructure',
|
|
41
|
+
'rewrite', 'rebuild', 'migration', 'migrate',
|
|
42
|
+
'ubah', 'ganti', 'pilih', 'tentukan', 'rombak', 'beralih', 'migrasi'
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const EXPLICIT_CHOICE_PATTERNS = [
|
|
46
|
+
/\b(?:i|we)\s+(?:choose|chose|selected?|decided? (?:on|to use))\s+[a-z0-9@._+-]+/i,
|
|
47
|
+
/\b(?:must use|will use|switch to|rewrite (?:to|in)|migrate to)\s+[a-z0-9@._+-]+/i,
|
|
48
|
+
/\b(?:saya pilih|aku pilih|kita pilih|harus memakai|beralih ke|ubah ke|ganti ke)\s+[a-z0-9@._+-]+/i,
|
|
49
|
+
/^(?:please\s+)?(?:use|choose|gunakan|pilih)\s+[a-z0-9@._+-]+/i,
|
|
50
|
+
/\b(?:do not|don't|jangan)\s+(?:use|gunakan|pakai)\s+[a-z0-9@._+-]+/i
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const MUTATING_TOOLS = new Set([
|
|
54
|
+
'fs_write', 'fs_edit', 'MultiEdit', 'fs_delete', 'fs_move', 'sysRun', 'sysRunBg',
|
|
55
|
+
'gitOp', 'manage_local_dependencies', 'plugin_manage', 'tool_forge', 'create_skill'
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
function containsAny(text: string, terms: readonly string[]): boolean {
|
|
59
|
+
return terms.some(term => text.includes(term));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function assessDecisionRequirement(prompt: string): DecisionRequirement {
|
|
63
|
+
const raw = String(prompt ?? '').trim();
|
|
64
|
+
const text = raw.toLowerCase();
|
|
65
|
+
const architectural = containsAny(text, ARCHITECTURE_TERMS);
|
|
66
|
+
const change = containsAny(text, CHANGE_TERMS);
|
|
67
|
+
const alternatives = /\b(?:or|versus|vs\.?|atau|dibanding(?:kan)? dengan)\b/i.test(raw);
|
|
68
|
+
const asksForChoice = /\b(?:which|what should|should we|mana|apakah sebaiknya|bagaimana sebaiknya)\b/i.test(raw);
|
|
69
|
+
const delegatesBroadRedesign = /\b(?:redesign|restructure|rombak)\s+(?:the\s+)?(?:architecture|system|runtime|database|arsitektur|sistem)\b/i.test(raw);
|
|
70
|
+
const explicit = EXPLICIT_CHOICE_PATTERNS.some(pattern => pattern.test(raw));
|
|
71
|
+
|
|
72
|
+
if (!architectural || explicit || (!alternatives && !asksForChoice && !(change && delegatesBroadRedesign))) {
|
|
73
|
+
return { required: false, kind: 'architecture', reason: '' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
required: true,
|
|
78
|
+
kind: 'architecture',
|
|
79
|
+
reason: 'The request reaches a durable architecture boundary with more than one plausible implementation.'
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function requiresImplementationPlan(prompt: string, archetype = '', confidence = ''): boolean {
|
|
84
|
+
const text = String(prompt ?? '').toLowerCase();
|
|
85
|
+
if (archetype === 'build_app' || archetype === 'integrate') return true;
|
|
86
|
+
if (confidence === 'high' && /\b(?:implement|build|create|migrate|rewrite|refactor|integrate|buat|bangun|ubah)\b/i.test(text)) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return containsAny(text, ARCHITECTURE_TERMS) && containsAny(text, CHANGE_TERMS);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isMutatingToolCall(toolName: string, args: Record<string, unknown> = {}): boolean {
|
|
93
|
+
if (MUTATING_TOOLS.has(toolName)) {
|
|
94
|
+
if (toolName === 'sysRun' || toolName === 'sysRunBg') {
|
|
95
|
+
const command = String(args.command ?? '');
|
|
96
|
+
return !/^\s*(?:pwd|ls|find|rg|grep|cat|sed|head|tail|git\s+(?:status|diff|log|show)|npm\s+(?:view|audit|test)|node\s+--check)\b/i.test(command);
|
|
97
|
+
}
|
|
98
|
+
if (toolName === 'gitOp') {
|
|
99
|
+
const action = String(args.action ?? args.command ?? args.operation ?? '');
|
|
100
|
+
return !/^(?:status|diff|log|show|branch|remote)$/i.test(action.trim());
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (toolName.startsWith('plugin__') || toolName.startsWith('mcp__')) return false;
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const MAX_STEPS = 24;
|
|
2
|
+
function cleanText(value, name, limit) {
|
|
3
|
+
if (typeof value !== 'string')
|
|
4
|
+
throw new Error(`${name} must be a string`);
|
|
5
|
+
const cleaned = value.replace(/\s+/g, ' ').trim();
|
|
6
|
+
if (!cleaned)
|
|
7
|
+
throw new Error(`${name} must not be empty`);
|
|
8
|
+
if (cleaned.length > limit)
|
|
9
|
+
throw new Error(`${name} exceeds ${limit} characters`);
|
|
10
|
+
return cleaned;
|
|
11
|
+
}
|
|
12
|
+
export function normalizePlanUpdate(value) {
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
14
|
+
throw new Error('Plan update must be an object');
|
|
15
|
+
const input = value;
|
|
16
|
+
if (!Array.isArray(input.plan) || input.plan.length === 0)
|
|
17
|
+
throw new Error('plan must contain at least one step');
|
|
18
|
+
if (input.plan.length > MAX_STEPS)
|
|
19
|
+
throw new Error(`plan may contain at most ${MAX_STEPS} steps`);
|
|
20
|
+
const ids = new Set();
|
|
21
|
+
let active = 0;
|
|
22
|
+
const plan = input.plan.map((raw, index) => {
|
|
23
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
24
|
+
throw new Error(`plan[${index}] must be an object`);
|
|
25
|
+
const item = raw;
|
|
26
|
+
const id = cleanText(item.id ?? `step-${index + 1}`, `plan[${index}].id`, 64);
|
|
27
|
+
const step = cleanText(item.step ?? item.content, `plan[${index}].step`, 240);
|
|
28
|
+
const status = item.status;
|
|
29
|
+
if (status !== 'pending' && status !== 'in_progress' && status !== 'completed') {
|
|
30
|
+
throw new Error(`plan[${index}].status must be pending, in_progress, or completed`);
|
|
31
|
+
}
|
|
32
|
+
if (ids.has(id))
|
|
33
|
+
throw new Error(`Duplicate plan step id: ${id}`);
|
|
34
|
+
ids.add(id);
|
|
35
|
+
if (status === 'in_progress')
|
|
36
|
+
active += 1;
|
|
37
|
+
return { id, step, status };
|
|
38
|
+
});
|
|
39
|
+
if (active > 1)
|
|
40
|
+
throw new Error('Only one plan step may be in_progress');
|
|
41
|
+
const explanation = typeof input.explanation === 'string'
|
|
42
|
+
? input.explanation.replace(/\s+/g, ' ').trim().slice(0, 500)
|
|
43
|
+
: '';
|
|
44
|
+
return { explanation, plan };
|
|
45
|
+
}
|
|
46
|
+
export function summarizePlan(plan) {
|
|
47
|
+
const completed = plan.filter(item => item.status === 'completed').length;
|
|
48
|
+
const active = plan.find(item => item.status === 'in_progress');
|
|
49
|
+
return `${completed}/${plan.length} completed${active ? `; in progress: ${active.step}` : ''}`;
|
|
50
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type PlanStatus = 'pending' | 'in_progress' | 'completed';
|
|
2
|
+
|
|
3
|
+
export interface PlanStep {
|
|
4
|
+
id: string;
|
|
5
|
+
step: string;
|
|
6
|
+
status: PlanStatus;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PlanUpdate {
|
|
10
|
+
explanation: string;
|
|
11
|
+
plan: PlanStep[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const MAX_STEPS = 24;
|
|
15
|
+
|
|
16
|
+
function cleanText(value: unknown, name: string, limit: number): string {
|
|
17
|
+
if (typeof value !== 'string') throw new Error(`${name} must be a string`);
|
|
18
|
+
const cleaned = value.replace(/\s+/g, ' ').trim();
|
|
19
|
+
if (!cleaned) throw new Error(`${name} must not be empty`);
|
|
20
|
+
if (cleaned.length > limit) throw new Error(`${name} exceeds ${limit} characters`);
|
|
21
|
+
return cleaned;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function normalizePlanUpdate(value: unknown): PlanUpdate {
|
|
25
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Plan update must be an object');
|
|
26
|
+
const input = value as Record<string, unknown>;
|
|
27
|
+
if (!Array.isArray(input.plan) || input.plan.length === 0) throw new Error('plan must contain at least one step');
|
|
28
|
+
if (input.plan.length > MAX_STEPS) throw new Error(`plan may contain at most ${MAX_STEPS} steps`);
|
|
29
|
+
|
|
30
|
+
const ids = new Set<string>();
|
|
31
|
+
let active = 0;
|
|
32
|
+
const plan = input.plan.map((raw, index): PlanStep => {
|
|
33
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(`plan[${index}] must be an object`);
|
|
34
|
+
const item = raw as Record<string, unknown>;
|
|
35
|
+
const id = cleanText(item.id ?? `step-${index + 1}`, `plan[${index}].id`, 64);
|
|
36
|
+
const step = cleanText(item.step ?? item.content, `plan[${index}].step`, 240);
|
|
37
|
+
const status = item.status;
|
|
38
|
+
if (status !== 'pending' && status !== 'in_progress' && status !== 'completed') {
|
|
39
|
+
throw new Error(`plan[${index}].status must be pending, in_progress, or completed`);
|
|
40
|
+
}
|
|
41
|
+
if (ids.has(id)) throw new Error(`Duplicate plan step id: ${id}`);
|
|
42
|
+
ids.add(id);
|
|
43
|
+
if (status === 'in_progress') active += 1;
|
|
44
|
+
return { id, step, status };
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (active > 1) throw new Error('Only one plan step may be in_progress');
|
|
48
|
+
const explanation = typeof input.explanation === 'string'
|
|
49
|
+
? input.explanation.replace(/\s+/g, ' ').trim().slice(0, 500)
|
|
50
|
+
: '';
|
|
51
|
+
return { explanation, plan };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function summarizePlan(plan: readonly PlanStep[]): string {
|
|
55
|
+
const completed = plan.filter(item => item.status === 'completed').length;
|
|
56
|
+
const active = plan.find(item => item.status === 'in_progress');
|
|
57
|
+
return `${completed}/${plan.length} completed${active ? `; in progress: ${active.step}` : ''}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
@@ -44,7 +44,7 @@ const GROUP_PACKAGES = {
|
|
|
44
44
|
|
|
45
45
|
// than listing package names.
|
|
46
46
|
export const GROUP_DESCRIPTIONS = {
|
|
47
|
-
browser: 'use
|
|
47
|
+
browser: 'use the Causyne Browser runtime for rendered navigation, extraction, and workspace-scoped downloads; Causyne never downloads the browser binary automatically',
|
|
48
48
|
vision: 'drive a browser and capture the screen, so the agent can look at what it built',
|
|
49
49
|
ocr: 'read text out of images and screenshots',
|
|
50
50
|
system: 'list running processes and read system load',
|
package/src/taskShape.js
CHANGED
|
@@ -49,7 +49,7 @@ const ARCHETYPES = {
|
|
|
49
49
|
weak: ['app', 'aplikasi', 'project', 'proyek', 'tool', 'system', 'sistem']
|
|
50
50
|
},
|
|
51
51
|
phases: [
|
|
52
|
-
{ name: 'Plan', tools: ['
|
|
52
|
+
{ name: 'Plan', tools: ['update_plan'], note: 'Create a visible atomic implementation plan before touching the filesystem.' },
|
|
53
53
|
{ name: 'Scaffold', tools: ['fs_write', 'sysRun'], note: 'Create structure and install dependencies at verified current versions.' },
|
|
54
54
|
{ name: 'Implement', tools: ['fs_write', 'fs_edit', 'MultiEdit'], note: 'Write runnable code with every import and dependency present.' },
|
|
55
55
|
{ name: 'Verify', tools: ['code_verify_syntax', 'sysRun'], note: 'Build, lint and run it. Never assume code works.' }
|
package/src/taskShape.ts
CHANGED
|
@@ -70,7 +70,7 @@ const ARCHETYPES: Record<string, ArchetypeDefinition> = {
|
|
|
70
70
|
weak: ['app', 'aplikasi', 'project', 'proyek', 'tool', 'system', 'sistem']
|
|
71
71
|
},
|
|
72
72
|
phases: [
|
|
73
|
-
{ name: 'Plan', tools: ['
|
|
73
|
+
{ name: 'Plan', tools: ['update_plan'], note: 'Create a visible atomic implementation plan before touching the filesystem.' },
|
|
74
74
|
{ name: 'Scaffold', tools: ['fs_write', 'sysRun'], note: 'Create structure and install dependencies at verified current versions.' },
|
|
75
75
|
{ name: 'Implement', tools: ['fs_write', 'fs_edit', 'MultiEdit'], note: 'Write runnable code with every import and dependency present.' },
|
|
76
76
|
{ name: 'Verify', tools: ['code_verify_syntax', 'sysRun'], note: 'Build, lint and run it. Never assume code works.' }
|
package/src/toolPacks.js
CHANGED
|
@@ -36,8 +36,8 @@ export const CORE_TOOLS = [
|
|
|
36
36
|
'code_verify_syntax',
|
|
37
37
|
'web_search',
|
|
38
38
|
'web_fetch',
|
|
39
|
-
'TodoWrite',
|
|
40
39
|
'update_plan',
|
|
40
|
+
'request_decision',
|
|
41
41
|
'memory_write',
|
|
42
42
|
'fs_read_skill',
|
|
43
43
|
'skill_create',
|
|
@@ -130,9 +130,11 @@ export const PACK_NAMES = Object.keys(PACKS);
|
|
|
130
130
|
*
|
|
131
131
|
* WHAT IS IN, AND WHY EXACTLY THESE: read, write, edit, list and search cover
|
|
132
132
|
* every file operation a first turn can need; `sysRun` is the escape hatch that
|
|
133
|
-
* can reach anything the other
|
|
134
|
-
* task announces its plan on step one; `tool_search` is the door to
|
|
135
|
-
*
|
|
133
|
+
* can reach anything the other resident tools cannot; `update_plan` is how a
|
|
134
|
+
* multi-step task announces its plan on step one; `tool_search` is the door to
|
|
135
|
+
* deferred tools. `request_decision` is injected on the first turn only when
|
|
136
|
+
* task policy detects an unresolved durable choice. With this set the agent is
|
|
137
|
+
* never stuck — worst case it spends one call
|
|
136
138
|
* discovering the tool it wants, and the tool it discovers stays loaded for the
|
|
137
139
|
* rest of the task.
|
|
138
140
|
*
|
|
@@ -147,7 +149,7 @@ export const RESIDENT_TOOLS = [
|
|
|
147
149
|
'fs_list',
|
|
148
150
|
'fs_search',
|
|
149
151
|
'sysRun',
|
|
150
|
-
'
|
|
152
|
+
'update_plan',
|
|
151
153
|
'tool_search'
|
|
152
154
|
];
|
|
153
155
|
|
|
@@ -2,7 +2,7 @@ import { BrowserCapabilityError, causyneBrowser } from '../browser/causyneBrowse
|
|
|
2
2
|
export const browserAct = {
|
|
3
3
|
schema: {
|
|
4
4
|
name: 'browser.act',
|
|
5
|
-
description: 'Use the dedicated Causyne
|
|
5
|
+
description: 'Use the dedicated Causyne Browser for resilient search, navigation, accessible snapshots, interaction, extraction, and workspace-scoped downloads. Take a snapshot before interacting by ref.',
|
|
6
6
|
parameters: {
|
|
7
7
|
type: 'object',
|
|
8
8
|
properties: {
|
|
@@ -3,7 +3,7 @@ import { BrowserCapabilityError, causyneBrowser, type BrowserAction } from '../b
|
|
|
3
3
|
export const browserAct = {
|
|
4
4
|
schema: {
|
|
5
5
|
name: 'browser.act',
|
|
6
|
-
description: 'Use the dedicated Causyne
|
|
6
|
+
description: 'Use the dedicated Causyne Browser for resilient search, navigation, accessible snapshots, interaction, extraction, and workspace-scoped downloads. Take a snapshot before interacting by ref.',
|
|
7
7
|
parameters: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
@@ -55,7 +55,7 @@ export const manageLocalDependencies = {
|
|
|
55
55
|
type: 'object',
|
|
56
56
|
properties: {
|
|
57
57
|
action: { type: 'string', enum: ['setup_venv', 'pipInst', 'check_and_setupEng', 'grant_capability', 'list_capabilities', 'grant_mcp_server', 'list_mcp_servers'] },
|
|
58
|
-
group: { type: 'string', description: 'Capability group for grant_capability: browser (dedicated
|
|
58
|
+
group: { type: 'string', description: 'Capability group for grant_capability: browser (dedicated Causyne Browser runtime), vision (screen capture), ocr (read text from images), system (process and load inspection), desktop (clipboard, open, recycle bin), archive (zip and unzip).' },
|
|
59
59
|
engine: { type: 'string', enum: ['godot', 'blender'], description: 'Engine to locate when action is check_and_setupEng' },
|
|
60
60
|
simulateMissing: { type: 'boolean', description: 'Simulate missing binary to verify sovereign fallback cascade' },
|
|
61
61
|
packages: { type: 'array', items: { type: 'string' }, description: 'Exact PyPI pins only, for example pillow==11.3.0. URLs, paths, VCS references and pip options are rejected.' }
|
package/src/tools/registry.js
CHANGED
|
@@ -12,7 +12,6 @@ import { processList } from './processList.js';
|
|
|
12
12
|
import { procTerm } from './procTerm.js';
|
|
13
13
|
import { webFetch } from './webFetch.js';
|
|
14
14
|
import { desktopOpen } from './desktopOpen.js';
|
|
15
|
-
import { todoWrite } from './todoWrite.js';
|
|
16
15
|
import { multiEdit } from './multiEdit.js';
|
|
17
16
|
import { codebaseSearch } from './codebaseSearch.js';
|
|
18
17
|
import { browserAct } from './browserAutomator.js';
|
|
@@ -22,6 +21,7 @@ import { palmierAutomator } from './palmierAutomator.js';
|
|
|
22
21
|
import { viewImage } from './viewImage.js';
|
|
23
22
|
import { webSearch } from './webSearch.js';
|
|
24
23
|
import { updatePlan } from './updatePlan.js';
|
|
24
|
+
import { requestDecision } from './requestDecision.js';
|
|
25
25
|
import { clipboard } from './clipboard.js';
|
|
26
26
|
import { screenshot } from './screenshot.js';
|
|
27
27
|
import { axSnapshot } from './axSnapshot.js';
|
|
@@ -106,9 +106,9 @@ const TOOL_MODULES = [
|
|
|
106
106
|
sysRun, sysRunBg, fsRead, fsWrite,
|
|
107
107
|
fsEdit, fsFind, fsSearch, fsList, fsMove,
|
|
108
108
|
fsDelete, gitExec, processList, procTerm,
|
|
109
|
-
webFetch, desktopOpen,
|
|
109
|
+
webFetch, desktopOpen, multiEdit,
|
|
110
110
|
codebaseSearch, browserAct, viewImage, webSearch,
|
|
111
|
-
updatePlan, clipboard, screenshot, axSnapshot, desktopAct, archive,
|
|
111
|
+
updatePlan, requestDecision, clipboard, screenshot, axSnapshot, desktopAct, archive,
|
|
112
112
|
checkSystemSpecs, executeProceduralMedia, manageLocalAiDependencies,
|
|
113
113
|
generateMediaViaApi, memoryWrite, printerPrint, printerList, printerStatus, toolForgeTool, pluginManagerTool,
|
|
114
114
|
astryxOracle, pdfRead, codeVerifySyntax, codeListSymbols, blenderAct, resolveAutomator, palmierAutomator,
|
|
@@ -145,6 +145,7 @@ for (const module of TOOL_MODULES) {
|
|
|
145
145
|
// a tool whose canonical schema name is different. Resolved only at execution
|
|
146
146
|
// time — never advertised, so they cost no context.
|
|
147
147
|
const TOOL_ALIASES = {
|
|
148
|
+
TodoWrite: 'update_plan',
|
|
148
149
|
manage_local_ai_dependencies: 'manage_local_dependencies',
|
|
149
150
|
procTerm: 'proc_term',
|
|
150
151
|
gitExec: 'gitOp',
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
const KINDS = new Set(['architecture', 'product', 'data', 'security', 'workflow', 'other']);
|
|
3
|
+
function text(value, name, limit) {
|
|
4
|
+
if (typeof value !== 'string')
|
|
5
|
+
throw new Error(`${name} must be a string`);
|
|
6
|
+
const cleaned = value.replace(/\s+/g, ' ').trim();
|
|
7
|
+
if (!cleaned)
|
|
8
|
+
throw new Error(`${name} must not be empty`);
|
|
9
|
+
if (cleaned.length > limit)
|
|
10
|
+
throw new Error(`${name} exceeds ${limit} characters`);
|
|
11
|
+
return cleaned;
|
|
12
|
+
}
|
|
13
|
+
function normalizeRequest(args) {
|
|
14
|
+
const kind = String(args.kind ?? 'other');
|
|
15
|
+
if (!KINDS.has(kind))
|
|
16
|
+
throw new Error('kind is invalid');
|
|
17
|
+
if (!Array.isArray(args.options) || args.options.length < 2 || args.options.length > 4) {
|
|
18
|
+
throw new Error('options must contain 2-4 choices');
|
|
19
|
+
}
|
|
20
|
+
const ids = new Set();
|
|
21
|
+
const options = args.options.map((raw, index) => {
|
|
22
|
+
const option = (raw && typeof raw === 'object' ? raw : {});
|
|
23
|
+
const id = text(option.id ?? `option-${index + 1}`, `options[${index}].id`, 48);
|
|
24
|
+
if (ids.has(id))
|
|
25
|
+
throw new Error(`Duplicate option id: ${id}`);
|
|
26
|
+
ids.add(id);
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
label: text(option.label, `options[${index}].label`, 80),
|
|
30
|
+
description: text(option.description, `options[${index}].description`, 240)
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
id: crypto.randomUUID(),
|
|
35
|
+
kind,
|
|
36
|
+
header: text(args.header, 'header', 48),
|
|
37
|
+
question: text(args.question, 'question', 500),
|
|
38
|
+
reason: text(args.reason, 'reason', 500),
|
|
39
|
+
options,
|
|
40
|
+
allowCustom: true
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export const requestDecision = {
|
|
44
|
+
schema: {
|
|
45
|
+
name: 'request_decision',
|
|
46
|
+
description: 'Pause for a durable user decision when several materially different implementation directions are valid. Provide 2-4 concrete options; a custom answer is always available. This is a design decision, not a tool permission request.',
|
|
47
|
+
parameters: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
kind: { type: 'string', enum: ['architecture', 'product', 'data', 'security', 'workflow', 'other'] },
|
|
51
|
+
header: { type: 'string', description: 'Short decision label, at most 48 characters.' },
|
|
52
|
+
question: { type: 'string', description: 'One specific decision the user must make.' },
|
|
53
|
+
reason: { type: 'string', description: 'Why this choice has durable consequences.' },
|
|
54
|
+
options: {
|
|
55
|
+
type: 'array', minItems: 2, maxItems: 4,
|
|
56
|
+
items: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
id: { type: 'string' },
|
|
60
|
+
label: { type: 'string' },
|
|
61
|
+
description: { type: 'string' }
|
|
62
|
+
},
|
|
63
|
+
required: ['id', 'label', 'description'],
|
|
64
|
+
additionalProperties: false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
required: ['kind', 'header', 'question', 'reason', 'options'],
|
|
69
|
+
additionalProperties: false
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
async execute(args, context = {}) {
|
|
73
|
+
try {
|
|
74
|
+
if (!context.parentLoop || typeof context.parentLoop.requestDecision !== 'function') {
|
|
75
|
+
return { success: false, error: 'Structured decisions require an active Causyne session' };
|
|
76
|
+
}
|
|
77
|
+
const request = normalizeRequest(args);
|
|
78
|
+
const answer = await context.parentLoop.requestDecision(request);
|
|
79
|
+
if (!answer)
|
|
80
|
+
return { success: false, error: 'The user dismissed the decision without choosing an option' };
|
|
81
|
+
return { success: true, output: JSON.stringify(answer), decision: answer };
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import type { DecisionKind, DecisionRequest } from '../decisionPolicy.js';
|
|
3
|
+
|
|
4
|
+
interface ToolContext {
|
|
5
|
+
parentLoop?: {
|
|
6
|
+
requestDecision(request: DecisionRequest): Promise<unknown>;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface RawOption {
|
|
11
|
+
id?: unknown;
|
|
12
|
+
label?: unknown;
|
|
13
|
+
description?: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface RawInput {
|
|
17
|
+
kind?: unknown;
|
|
18
|
+
header?: unknown;
|
|
19
|
+
question?: unknown;
|
|
20
|
+
reason?: unknown;
|
|
21
|
+
options?: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const KINDS = new Set<DecisionKind>(['architecture', 'product', 'data', 'security', 'workflow', 'other']);
|
|
25
|
+
|
|
26
|
+
function text(value: unknown, name: string, limit: number): string {
|
|
27
|
+
if (typeof value !== 'string') throw new Error(`${name} must be a string`);
|
|
28
|
+
const cleaned = value.replace(/\s+/g, ' ').trim();
|
|
29
|
+
if (!cleaned) throw new Error(`${name} must not be empty`);
|
|
30
|
+
if (cleaned.length > limit) throw new Error(`${name} exceeds ${limit} characters`);
|
|
31
|
+
return cleaned;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeRequest(args: RawInput): DecisionRequest {
|
|
35
|
+
const kind = String(args.kind ?? 'other') as DecisionKind;
|
|
36
|
+
if (!KINDS.has(kind)) throw new Error('kind is invalid');
|
|
37
|
+
if (!Array.isArray(args.options) || args.options.length < 2 || args.options.length > 4) {
|
|
38
|
+
throw new Error('options must contain 2-4 choices');
|
|
39
|
+
}
|
|
40
|
+
const ids = new Set<string>();
|
|
41
|
+
const options = args.options.map((raw, index) => {
|
|
42
|
+
const option = (raw && typeof raw === 'object' ? raw : {}) as RawOption;
|
|
43
|
+
const id = text(option.id ?? `option-${index + 1}`, `options[${index}].id`, 48);
|
|
44
|
+
if (ids.has(id)) throw new Error(`Duplicate option id: ${id}`);
|
|
45
|
+
ids.add(id);
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
label: text(option.label, `options[${index}].label`, 80),
|
|
49
|
+
description: text(option.description, `options[${index}].description`, 240)
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
id: crypto.randomUUID(),
|
|
54
|
+
kind,
|
|
55
|
+
header: text(args.header, 'header', 48),
|
|
56
|
+
question: text(args.question, 'question', 500),
|
|
57
|
+
reason: text(args.reason, 'reason', 500),
|
|
58
|
+
options,
|
|
59
|
+
allowCustom: true
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const requestDecision = {
|
|
64
|
+
schema: {
|
|
65
|
+
name: 'request_decision',
|
|
66
|
+
description: 'Pause for a durable user decision when several materially different implementation directions are valid. Provide 2-4 concrete options; a custom answer is always available. This is a design decision, not a tool permission request.',
|
|
67
|
+
parameters: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
kind: { type: 'string', enum: ['architecture', 'product', 'data', 'security', 'workflow', 'other'] },
|
|
71
|
+
header: { type: 'string', description: 'Short decision label, at most 48 characters.' },
|
|
72
|
+
question: { type: 'string', description: 'One specific decision the user must make.' },
|
|
73
|
+
reason: { type: 'string', description: 'Why this choice has durable consequences.' },
|
|
74
|
+
options: {
|
|
75
|
+
type: 'array', minItems: 2, maxItems: 4,
|
|
76
|
+
items: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
id: { type: 'string' },
|
|
80
|
+
label: { type: 'string' },
|
|
81
|
+
description: { type: 'string' }
|
|
82
|
+
},
|
|
83
|
+
required: ['id', 'label', 'description'],
|
|
84
|
+
additionalProperties: false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
required: ['kind', 'header', 'question', 'reason', 'options'],
|
|
89
|
+
additionalProperties: false
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
async execute(args: RawInput, context: ToolContext = {}) {
|
|
94
|
+
try {
|
|
95
|
+
if (!context.parentLoop || typeof context.parentLoop.requestDecision !== 'function') {
|
|
96
|
+
return { success: false, error: 'Structured decisions require an active Causyne session' };
|
|
97
|
+
}
|
|
98
|
+
const request = normalizeRequest(args);
|
|
99
|
+
const answer = await context.parentLoop.requestDecision(request);
|
|
100
|
+
if (!answer) return { success: false, error: 'The user dismissed the decision without choosing an option' };
|
|
101
|
+
return { success: true, output: JSON.stringify(answer), decision: answer };
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|