@ebowwa/daemons 0.5.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/README.md +264 -0
- package/dist/bin/discord-cli.js +124118 -0
- package/dist/bin/manager.js +143 -0
- package/dist/bin/telegram-cli.js +124114 -0
- package/dist/index.js +125340 -0
- package/package.json +94 -0
- package/src/agent.ts +111 -0
- package/src/channels/base.ts +573 -0
- package/src/channels/discord.ts +306 -0
- package/src/channels/index.ts +169 -0
- package/src/channels/telegram.ts +315 -0
- package/src/daemon.ts +534 -0
- package/src/hooks.ts +97 -0
- package/src/index.ts +111 -0
- package/src/memory.ts +369 -0
- package/src/skills/coding/commit.ts +202 -0
- package/src/skills/coding/execute-subtask.ts +136 -0
- package/src/skills/coding/fix-issues.ts +126 -0
- package/src/skills/coding/index.ts +26 -0
- package/src/skills/coding/plan-task.ts +158 -0
- package/src/skills/coding/quality-check.ts +155 -0
- package/src/skills/index.ts +65 -0
- package/src/skills/registry.ts +380 -0
- package/src/skills/shared/index.ts +21 -0
- package/src/skills/shared/reflect.ts +156 -0
- package/src/skills/shared/review.ts +201 -0
- package/src/skills/shared/trajectory.ts +319 -0
- package/src/skills/trading/analyze-market.ts +144 -0
- package/src/skills/trading/check-risk.ts +176 -0
- package/src/skills/trading/execute-trade.ts +185 -0
- package/src/skills/trading/generate-signal.ts +160 -0
- package/src/skills/trading/index.ts +26 -0
- package/src/skills/trading/monitor-position.ts +179 -0
- package/src/skills/types.ts +235 -0
- package/src/skills/workflows.ts +340 -0
- package/src/state.ts +77 -0
- package/src/tools.ts +134 -0
- package/src/types.ts +314 -0
- package/src/workflow.ts +341 -0
- package/src/workflows/coding.ts +580 -0
- package/src/workflows/index.ts +61 -0
- package/src/workflows/trading.ts +608 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLM Daemon - Skill-Based Workflows
|
|
3
|
+
*
|
|
4
|
+
* Workflow definitions using skill sequences.
|
|
5
|
+
* Replaces the old class-based workflow system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SkillWorkflowConfig, SkillTransition } from "./types.js";
|
|
9
|
+
|
|
10
|
+
// ============================================
|
|
11
|
+
// CODING WORKFLOW
|
|
12
|
+
// ============================================
|
|
13
|
+
|
|
14
|
+
const codingTransitions: SkillTransition[] = [
|
|
15
|
+
// Planning -> Execution
|
|
16
|
+
{ from: "/plan-task", to: "/execute-subtask", priority: 100 },
|
|
17
|
+
|
|
18
|
+
// Execution flow
|
|
19
|
+
{ from: "/execute-subtask", to: "/quality-check",
|
|
20
|
+
condition: (ctx) => {
|
|
21
|
+
const completed = ctx.state.slam.completedSubtasks?.length || 0;
|
|
22
|
+
const total = ctx.state.slam.subtasks?.length || 0;
|
|
23
|
+
return total > 0 && completed >= total;
|
|
24
|
+
},
|
|
25
|
+
priority: 90 },
|
|
26
|
+
{ from: "/execute-subtask", to: "/execute-subtask", priority: 50 },
|
|
27
|
+
|
|
28
|
+
// Quality check flow
|
|
29
|
+
{ from: "/quality-check", to: "/fix-issues",
|
|
30
|
+
condition: (_, result) => (result.data as Record<string, unknown>)?.hasIssues === true,
|
|
31
|
+
priority: 90 },
|
|
32
|
+
{ from: "/quality-check", to: "/review", priority: 80 },
|
|
33
|
+
|
|
34
|
+
// Review flow
|
|
35
|
+
{ from: "/review", to: "/fix-issues",
|
|
36
|
+
condition: (_, result) => (result.data as Record<string, unknown>)?.approved === false,
|
|
37
|
+
priority: 90 },
|
|
38
|
+
{ from: "/review", to: "/commit", priority: 80 },
|
|
39
|
+
|
|
40
|
+
// Fix flow
|
|
41
|
+
{ from: "/fix-issues", to: "/quality-check", priority: 100 },
|
|
42
|
+
|
|
43
|
+
// Commit -> Complete
|
|
44
|
+
{ from: "/commit", to: "/complete", priority: 100 },
|
|
45
|
+
|
|
46
|
+
// Reflection (can happen from any skill)
|
|
47
|
+
{ from: "*", to: "/reflect",
|
|
48
|
+
condition: (ctx) => {
|
|
49
|
+
const enabled = ctx.state.reflection?.toolCount !== undefined;
|
|
50
|
+
const limit = 50; // Default tool limit
|
|
51
|
+
const count = ctx.state.reflection?.toolCount || 0;
|
|
52
|
+
return enabled && count >= limit;
|
|
53
|
+
},
|
|
54
|
+
priority: 200 },
|
|
55
|
+
{ from: "/reflect", to: "/execute-subtask", priority: 100 },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export const codingWorkflow: SkillWorkflowConfig = {
|
|
59
|
+
id: "coding",
|
|
60
|
+
name: "Coding Workflow",
|
|
61
|
+
description: "Software development workflow with planning, execution, and quality checks",
|
|
62
|
+
|
|
63
|
+
skills: [
|
|
64
|
+
"/plan-task",
|
|
65
|
+
"/execute-subtask",
|
|
66
|
+
"/reflect",
|
|
67
|
+
"/quality-check",
|
|
68
|
+
"/review",
|
|
69
|
+
"/fix-issues",
|
|
70
|
+
"/commit",
|
|
71
|
+
],
|
|
72
|
+
|
|
73
|
+
initialSkill: "/plan-task",
|
|
74
|
+
terminalSkills: ["/commit", "/complete"],
|
|
75
|
+
|
|
76
|
+
transitions: codingTransitions,
|
|
77
|
+
|
|
78
|
+
systemPromptExtensions: `You are a software development assistant.
|
|
79
|
+
Use tools when available. Be thorough but efficient.
|
|
80
|
+
Focus on code changes, testing, and verification.`,
|
|
81
|
+
|
|
82
|
+
reflectionSkill: "/reflect",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ============================================
|
|
86
|
+
// TRADING WORKFLOW
|
|
87
|
+
// ============================================
|
|
88
|
+
|
|
89
|
+
const tradingTransitions: SkillTransition[] = [
|
|
90
|
+
// Analysis flow
|
|
91
|
+
{ from: "/analyze-market", to: "/generate-signal", priority: 100 },
|
|
92
|
+
|
|
93
|
+
// Signal flow
|
|
94
|
+
{ from: "/generate-signal", to: "/check-risk",
|
|
95
|
+
condition: (_, result) => (result.data as Record<string, unknown>)?.direction !== "none",
|
|
96
|
+
priority: 100 },
|
|
97
|
+
{ from: "/generate-signal", to: "/analyze-market",
|
|
98
|
+
condition: (_, result) => (result.data as Record<string, unknown>)?.direction === "none",
|
|
99
|
+
priority: 90 },
|
|
100
|
+
|
|
101
|
+
// Risk check flow
|
|
102
|
+
{ from: "/check-risk", to: "/execute-trade",
|
|
103
|
+
condition: (_, result) => (result.data as Record<string, unknown>)?.approved === true,
|
|
104
|
+
priority: 100 },
|
|
105
|
+
{ from: "/check-risk", to: "/analyze-market",
|
|
106
|
+
condition: (_, result) => (result.data as Record<string, unknown>)?.approved === false,
|
|
107
|
+
priority: 90 },
|
|
108
|
+
|
|
109
|
+
// Execution -> Monitoring
|
|
110
|
+
{ from: "/execute-trade", to: "/monitor-position",
|
|
111
|
+
condition: (_, result) => result.success && (result.data as Record<string, unknown>)?.status !== "failed",
|
|
112
|
+
priority: 100 },
|
|
113
|
+
{ from: "/execute-trade", to: "/analyze-market",
|
|
114
|
+
condition: (_, result) => !result.success,
|
|
115
|
+
priority: 90 },
|
|
116
|
+
|
|
117
|
+
// Monitoring flow
|
|
118
|
+
{ from: "/monitor-position", to: "/complete",
|
|
119
|
+
condition: (ctx) => ctx.custom.targetReached === true || ctx.custom.stopLoss === true,
|
|
120
|
+
priority: 100 },
|
|
121
|
+
{ from: "/monitor-position", to: "/analyze-market",
|
|
122
|
+
condition: (ctx) => ctx.custom.positionClosed === true,
|
|
123
|
+
priority: 90 },
|
|
124
|
+
{ from: "/monitor-position", to: "/reflect",
|
|
125
|
+
condition: (ctx) => ctx.state.iteration > 0 && ctx.state.iteration % 10 === 0,
|
|
126
|
+
priority: 80 },
|
|
127
|
+
{ from: "/monitor-position", to: "/monitor-position", priority: 50 },
|
|
128
|
+
|
|
129
|
+
// Reflection
|
|
130
|
+
{ from: "/reflect", to: "/monitor-position", priority: 100 },
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
export const tradingWorkflow: SkillWorkflowConfig = {
|
|
134
|
+
id: "trading",
|
|
135
|
+
name: "Trading Workflow",
|
|
136
|
+
description: "Trading workflow with analysis, signal generation, risk management, and execution",
|
|
137
|
+
|
|
138
|
+
skills: [
|
|
139
|
+
"/analyze-market",
|
|
140
|
+
"/generate-signal",
|
|
141
|
+
"/check-risk",
|
|
142
|
+
"/execute-trade",
|
|
143
|
+
"/monitor-position",
|
|
144
|
+
"/reflect",
|
|
145
|
+
],
|
|
146
|
+
|
|
147
|
+
initialSkill: "/analyze-market",
|
|
148
|
+
terminalSkills: ["/complete"],
|
|
149
|
+
|
|
150
|
+
transitions: tradingTransitions,
|
|
151
|
+
|
|
152
|
+
skillParams: {
|
|
153
|
+
"/check-risk": {
|
|
154
|
+
maxRiskPerTrade: 2,
|
|
155
|
+
minRiskReward: 1.5,
|
|
156
|
+
},
|
|
157
|
+
"/execute-trade": {
|
|
158
|
+
paper: true, // Default to paper trading
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
systemPromptExtensions: `You are a trading assistant.
|
|
163
|
+
Follow risk management rules strictly.
|
|
164
|
+
Never exceed position size limits.
|
|
165
|
+
Always use stop-loss orders.
|
|
166
|
+
No git operations are needed - focus on market execution.`,
|
|
167
|
+
|
|
168
|
+
reflectionSkill: "/reflect",
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// ============================================
|
|
172
|
+
// WORKFLOW REGISTRY
|
|
173
|
+
// ============================================
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Simple workflow registry for skill-based workflows
|
|
177
|
+
*/
|
|
178
|
+
class WorkflowConfigRegistry {
|
|
179
|
+
private workflows: Map<string, SkillWorkflowConfig> = new Map();
|
|
180
|
+
private defaultWorkflow: string | null = null;
|
|
181
|
+
|
|
182
|
+
register(config: SkillWorkflowConfig, setDefault = false): void {
|
|
183
|
+
this.workflows.set(config.id, config);
|
|
184
|
+
if (setDefault || this.workflows.size === 1) {
|
|
185
|
+
this.defaultWorkflow = config.id;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
get(id: string): SkillWorkflowConfig | undefined {
|
|
190
|
+
return this.workflows.get(id);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
getDefault(): SkillWorkflowConfig | undefined {
|
|
194
|
+
if (!this.defaultWorkflow) return undefined;
|
|
195
|
+
return this.workflows.get(this.defaultWorkflow);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
list(): Array<{ id: string; name: string; description: string }> {
|
|
199
|
+
return Array.from(this.workflows.values()).map((w) => ({
|
|
200
|
+
id: w.id,
|
|
201
|
+
name: w.name,
|
|
202
|
+
description: w.description,
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const workflowConfigRegistry = new WorkflowConfigRegistry();
|
|
208
|
+
|
|
209
|
+
// Register built-in workflows
|
|
210
|
+
workflowConfigRegistry.register(codingWorkflow, true);
|
|
211
|
+
workflowConfigRegistry.register(tradingWorkflow);
|
|
212
|
+
|
|
213
|
+
// ============================================
|
|
214
|
+
// WORKFLOW EXECUTOR
|
|
215
|
+
// ============================================
|
|
216
|
+
|
|
217
|
+
import type { SkillContext, SkillResult } from "./types.js";
|
|
218
|
+
import { skillRegistry } from "./registry.js";
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Execute a skill-based workflow
|
|
222
|
+
*/
|
|
223
|
+
export async function executeWorkflowSkill(
|
|
224
|
+
workflow: SkillWorkflowConfig,
|
|
225
|
+
context: SkillContext
|
|
226
|
+
): Promise<SkillResult> {
|
|
227
|
+
const currentSkillId = context.state.slam.phase as string;
|
|
228
|
+
|
|
229
|
+
// Get skill params from workflow config
|
|
230
|
+
const skillParams = workflow.skillParams?.[currentSkillId];
|
|
231
|
+
|
|
232
|
+
// Execute the current skill
|
|
233
|
+
const result = await skillRegistry.execute(
|
|
234
|
+
currentSkillId,
|
|
235
|
+
context,
|
|
236
|
+
skillParams
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (!result.success) {
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check if skill specified next skill
|
|
244
|
+
if (result.nextSkill) {
|
|
245
|
+
return {
|
|
246
|
+
...result,
|
|
247
|
+
stateUpdates: {
|
|
248
|
+
...result.stateUpdates,
|
|
249
|
+
slam: {
|
|
250
|
+
...context.state.slam,
|
|
251
|
+
...result.stateUpdates?.slam,
|
|
252
|
+
phase: result.nextSkill,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Find next skill using transitions
|
|
259
|
+
const nextSkill = await findNextSkill(workflow, context, result);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
...result,
|
|
263
|
+
stateUpdates: {
|
|
264
|
+
...result.stateUpdates,
|
|
265
|
+
slam: {
|
|
266
|
+
...context.state.slam,
|
|
267
|
+
...result.stateUpdates?.slam,
|
|
268
|
+
phase: nextSkill,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Find the next skill based on transitions
|
|
276
|
+
*/
|
|
277
|
+
async function findNextSkill(
|
|
278
|
+
workflow: SkillWorkflowConfig,
|
|
279
|
+
context: SkillContext,
|
|
280
|
+
result: SkillResult
|
|
281
|
+
): Promise<string> {
|
|
282
|
+
const currentSkillId = context.state.slam.phase as string;
|
|
283
|
+
|
|
284
|
+
// Sort transitions by priority (descending)
|
|
285
|
+
const sortedTransitions = [...workflow.transitions].sort(
|
|
286
|
+
(a, b) => (b.priority || 0) - (a.priority || 0)
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
// Check each transition
|
|
290
|
+
for (const transition of sortedTransitions) {
|
|
291
|
+
// Check if transition matches current skill (or wildcard)
|
|
292
|
+
if (transition.from !== "*" && transition.from !== currentSkillId) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check condition if present
|
|
297
|
+
if (transition.condition) {
|
|
298
|
+
try {
|
|
299
|
+
const shouldTransition = await transition.condition(context, result);
|
|
300
|
+
if (!shouldTransition) continue;
|
|
301
|
+
} catch {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Return target skill
|
|
307
|
+
return transition.to;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Fall back to default order
|
|
311
|
+
const currentIndex = workflow.skills.indexOf(currentSkillId);
|
|
312
|
+
if (currentIndex >= 0 && currentIndex < workflow.skills.length - 1) {
|
|
313
|
+
return workflow.skills[currentIndex + 1];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Stay on current skill if no transition matches
|
|
317
|
+
return currentSkillId;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Check if workflow is complete
|
|
322
|
+
*/
|
|
323
|
+
export async function isWorkflowComplete(
|
|
324
|
+
workflow: SkillWorkflowConfig,
|
|
325
|
+
context: SkillContext
|
|
326
|
+
): Promise<boolean> {
|
|
327
|
+
const currentPhase = context.state.slam.phase as string;
|
|
328
|
+
|
|
329
|
+
// Check if in terminal skill
|
|
330
|
+
if (workflow.terminalSkills.includes(currentPhase)) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Check custom completion condition
|
|
335
|
+
if (workflow.isComplete) {
|
|
336
|
+
return await workflow.isComplete(context);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return false;
|
|
340
|
+
}
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLM Daemon - State Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages SLAM state persistence to disk.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { promises as fsp } from "fs";
|
|
8
|
+
import type { GLMDaemonState } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export class StateManager {
|
|
11
|
+
private statePath: string;
|
|
12
|
+
|
|
13
|
+
constructor(statePath: string) {
|
|
14
|
+
this.statePath = statePath;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load state from disk
|
|
19
|
+
*/
|
|
20
|
+
async load(): Promise<GLMDaemonState | null> {
|
|
21
|
+
try {
|
|
22
|
+
const content = await fsp.readFile(this.statePath, "utf-8");
|
|
23
|
+
return JSON.parse(content) as GLMDaemonState;
|
|
24
|
+
} catch {
|
|
25
|
+
// File doesn't exist or invalid JSON
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Save state to disk
|
|
32
|
+
*/
|
|
33
|
+
async save(state: GLMDaemonState): Promise<void> {
|
|
34
|
+
await fsp.writeFile(
|
|
35
|
+
this.statePath,
|
|
36
|
+
JSON.stringify(state, null, 2),
|
|
37
|
+
"utf-8"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Delete state file
|
|
43
|
+
*/
|
|
44
|
+
async delete(): Promise<void> {
|
|
45
|
+
try {
|
|
46
|
+
await fsp.unlink(this.statePath);
|
|
47
|
+
} catch {
|
|
48
|
+
// File doesn't exist, ignore
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if state file exists
|
|
54
|
+
*/
|
|
55
|
+
async exists(): Promise<boolean> {
|
|
56
|
+
try {
|
|
57
|
+
await fsp.access(this.statePath);
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Update a portion of state (merge)
|
|
66
|
+
*/
|
|
67
|
+
async update(updates: Partial<GLMDaemonState>): Promise<GLMDaemonState> {
|
|
68
|
+
const current = await this.load();
|
|
69
|
+
if (!current) {
|
|
70
|
+
throw new Error("No existing state to update");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const updated = { ...current, ...updates };
|
|
74
|
+
await this.save(updated);
|
|
75
|
+
return updated;
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLM Daemon - Tool Bridge
|
|
3
|
+
*
|
|
4
|
+
* Connects GLM agents to MCP servers for tool access.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { GLMToolsConfig } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export class ToolBridge {
|
|
10
|
+
private config: GLMToolsConfig;
|
|
11
|
+
private availableTools: string[] = [];
|
|
12
|
+
private toolTimeout: number;
|
|
13
|
+
private maxRetries: number;
|
|
14
|
+
|
|
15
|
+
constructor(config: GLMToolsConfig = {}) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.toolTimeout = config.toolTimeout || 30000;
|
|
18
|
+
this.maxRetries = config.maxRetries || 3;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get list of available tools
|
|
23
|
+
*/
|
|
24
|
+
getAvailableTools(): string[] {
|
|
25
|
+
return this.availableTools;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Set available tools (from MCP discovery)
|
|
30
|
+
*/
|
|
31
|
+
setAvailableTools(tools: string[]): void {
|
|
32
|
+
this.availableTools = tools;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if a tool is allowed
|
|
37
|
+
*/
|
|
38
|
+
isToolAllowed(toolName: string): boolean {
|
|
39
|
+
// Check blacklist first
|
|
40
|
+
if (this.config.blockedTools?.includes(toolName)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// If whitelist exists, check it
|
|
45
|
+
if (this.config.allowedTools && this.config.allowedTools.length > 0) {
|
|
46
|
+
return this.config.allowedTools.includes(toolName);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Default: allow if not blocked
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Execute a tool with timeout and retry
|
|
55
|
+
*/
|
|
56
|
+
async executeTool(
|
|
57
|
+
toolName: string,
|
|
58
|
+
args: Record<string, unknown>
|
|
59
|
+
): Promise<{ success: boolean; result?: unknown; error?: string }> {
|
|
60
|
+
if (!this.isToolAllowed(toolName)) {
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
error: `Tool not allowed: ${toolName}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let lastError: Error | null = null;
|
|
68
|
+
|
|
69
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
70
|
+
try {
|
|
71
|
+
const result = await this.executeWithTimeout(toolName, args);
|
|
72
|
+
return { success: true, result };
|
|
73
|
+
} catch (error) {
|
|
74
|
+
lastError = error as Error;
|
|
75
|
+
// Wait before retry (exponential backoff)
|
|
76
|
+
if (attempt < this.maxRetries - 1) {
|
|
77
|
+
await new Promise((resolve) =>
|
|
78
|
+
setTimeout(resolve, Math.pow(2, attempt) * 1000)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
error: lastError?.message || "Unknown error",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Execute tool with timeout
|
|
92
|
+
*/
|
|
93
|
+
private async executeWithTimeout(
|
|
94
|
+
toolName: string,
|
|
95
|
+
args: Record<string, unknown>
|
|
96
|
+
): Promise<unknown> {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
reject(new Error(`Tool timeout: ${toolName}`));
|
|
100
|
+
}, this.toolTimeout);
|
|
101
|
+
|
|
102
|
+
// TODO: Actually call MCP tool here
|
|
103
|
+
// For now, this is a placeholder
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
resolve(`Executed ${toolName} with ${JSON.stringify(args)}`);
|
|
107
|
+
}, 100);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Discover tools from MCP servers
|
|
113
|
+
*/
|
|
114
|
+
async discoverMCPTools(): Promise<void> {
|
|
115
|
+
if (!this.config.enableMCP) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// TODO: Query MCP servers for available tools
|
|
120
|
+
// For now, add common tools
|
|
121
|
+
const commonTools = [
|
|
122
|
+
"Read",
|
|
123
|
+
"Write",
|
|
124
|
+
"Edit",
|
|
125
|
+
"Grep",
|
|
126
|
+
"Glob",
|
|
127
|
+
"Bash",
|
|
128
|
+
"git_status",
|
|
129
|
+
"git_commit",
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
this.setAvailableTools(commonTools);
|
|
133
|
+
}
|
|
134
|
+
}
|