@epic-cloudcontrol/daemon 0.2.0 → 0.3.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/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +26 -0
- package/dist/__tests__/model-router.test.d.ts +1 -0
- package/dist/__tests__/model-router.test.js +59 -0
- package/dist/__tests__/profile.test.d.ts +1 -0
- package/dist/__tests__/profile.test.js +53 -0
- package/dist/__tests__/sandbox.test.d.ts +1 -0
- package/dist/__tests__/sandbox.test.js +78 -0
- package/dist/__tests__/version.test.d.ts +1 -0
- package/dist/__tests__/version.test.js +11 -0
- package/dist/browser.d.ts +7 -0
- package/dist/browser.js +56 -0
- package/dist/cli.js +17 -13
- package/dist/logger.d.ts +13 -0
- package/dist/logger.js +64 -0
- package/dist/mcp-server.js +8 -6
- package/dist/model-router.js +43 -7
- package/dist/models/gemini-api.d.ts +24 -0
- package/dist/models/gemini-api.js +134 -0
- package/dist/models/openai.d.ts +24 -0
- package/dist/models/openai.js +135 -0
- package/dist/multi-profile.js +9 -7
- package/dist/sandbox.js +3 -6
- package/dist/step-runner.d.ts +34 -0
- package/dist/step-runner.js +94 -0
- package/dist/task-executor.js +64 -27
- package/package.json +13 -2
package/dist/task-executor.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { ModelRouter } from "./model-router.js";
|
|
2
2
|
import { fetchWithRetry } from "./retry.js";
|
|
3
3
|
import { cleanupTaskTmpDir } from "./sandbox.js";
|
|
4
|
+
import { createLogger } from "./logger.js";
|
|
5
|
+
import { runProcessSteps } from "./step-runner.js";
|
|
6
|
+
const log = createLogger("executor");
|
|
4
7
|
const retryOpts = {
|
|
5
8
|
maxRetries: 3,
|
|
6
9
|
baseDelayMs: 1000,
|
|
7
10
|
onRetry: (attempt, err) => {
|
|
8
|
-
|
|
11
|
+
log.info(`Retry attempt ${attempt}: ${err.message}`);
|
|
9
12
|
},
|
|
10
13
|
};
|
|
11
14
|
export class TaskExecutor {
|
|
@@ -42,21 +45,21 @@ export class TaskExecutor {
|
|
|
42
45
|
return task;
|
|
43
46
|
}
|
|
44
47
|
catch (err) {
|
|
45
|
-
|
|
48
|
+
log.warn(`Failed to claim task ${taskId}: ${err.message}`);
|
|
46
49
|
return null;
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
async executeTask(task) {
|
|
50
|
-
|
|
51
|
-
// Update status to running
|
|
53
|
+
log.info(`Executing: ${task.title}`);
|
|
52
54
|
await this.updateTaskStatus(task.id, "running");
|
|
53
55
|
// Resolve process for this task type (if any)
|
|
56
|
+
let processSteps = null;
|
|
54
57
|
if (task.taskType) {
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
task.processHint =
|
|
59
|
-
|
|
58
|
+
const processData = await this.resolveProcess(task.taskType);
|
|
59
|
+
if (processData) {
|
|
60
|
+
processSteps = processData.steps;
|
|
61
|
+
task.processHint = processData.hint;
|
|
62
|
+
log.info(`Process loaded for type "${task.taskType}" (${processData.steps.length} steps)`);
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
// Fetch any needed secrets
|
|
@@ -65,30 +68,65 @@ export class TaskExecutor {
|
|
|
65
68
|
const value = await this.fetchSecret(key, task.id);
|
|
66
69
|
if (value) {
|
|
67
70
|
this.secrets.set(key, value);
|
|
68
|
-
|
|
71
|
+
log.info(`Secret "${key}" loaded`);
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
// Select model based on task's hint
|
|
73
|
-
const
|
|
74
|
-
|
|
76
|
+
const model = this.router.select(task.modelHint);
|
|
77
|
+
log.info(`Model: ${model.name} (hint: ${task.modelHint || "auto"})`);
|
|
75
78
|
// Pass taskId to adapter for sandbox isolation
|
|
76
|
-
if ("setTaskId" in adapter && typeof adapter.setTaskId === "function") {
|
|
77
|
-
adapter.setTaskId(task.id);
|
|
79
|
+
if ("setTaskId" in model.adapter && typeof model.adapter.setTaskId === "function") {
|
|
80
|
+
model.adapter.setTaskId(task.id);
|
|
78
81
|
}
|
|
79
|
-
// Execute with selected model adapter
|
|
80
82
|
let result;
|
|
81
83
|
try {
|
|
82
|
-
|
|
84
|
+
if (processSteps && processSteps.length > 0) {
|
|
85
|
+
// Step-by-step process execution
|
|
86
|
+
log.info(`Running ${processSteps.length} process steps`);
|
|
87
|
+
const stepResult = await runProcessSteps(processSteps, task, model, async (content) => {
|
|
88
|
+
try {
|
|
89
|
+
await fetchWithRetry(`${this.config.apiUrl}/api/tasks/${task.id}/activity`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: this.headers,
|
|
92
|
+
body: JSON.stringify({ activityType: "progress", content, workerId: this.workerId }),
|
|
93
|
+
}, { maxRetries: 1, baseDelayMs: 500 });
|
|
94
|
+
}
|
|
95
|
+
catch { /* non-fatal */ }
|
|
96
|
+
});
|
|
97
|
+
if (stepResult.humanRequired) {
|
|
98
|
+
result = stepResult.finalResult || {
|
|
99
|
+
success: false,
|
|
100
|
+
dialogue: [],
|
|
101
|
+
result: { steps: stepResult.results },
|
|
102
|
+
metadata: { model: model.name, tokens_used: 0, duration_ms: 0 },
|
|
103
|
+
humanRequired: { reason: stepResult.humanRequired.reason, context: `Paused at step: ${stepResult.humanRequired.stepId}` },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
else if (stepResult.finalResult) {
|
|
107
|
+
result = stepResult.finalResult;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
result = {
|
|
111
|
+
success: stepResult.completed,
|
|
112
|
+
dialogue: [],
|
|
113
|
+
result: { steps: stepResult.results },
|
|
114
|
+
metadata: { model: model.name, tokens_used: 0, duration_ms: 0 },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// No process steps — single AI call (existing behavior)
|
|
120
|
+
result = await model.adapter.execute(task);
|
|
121
|
+
}
|
|
83
122
|
}
|
|
84
123
|
finally {
|
|
85
|
-
// Flush secrets and cleanup sandbox tmpdir
|
|
86
124
|
this.secrets.clear();
|
|
87
125
|
cleanupTaskTmpDir(task.id);
|
|
88
126
|
}
|
|
89
127
|
// Submit result
|
|
90
128
|
if (result.humanRequired) {
|
|
91
|
-
|
|
129
|
+
log.info(`Task requires human input: ${result.humanRequired.reason}`);
|
|
92
130
|
await this.submitResult(task.id, {
|
|
93
131
|
status: "human_required",
|
|
94
132
|
result: result.result,
|
|
@@ -98,7 +136,7 @@ export class TaskExecutor {
|
|
|
98
136
|
});
|
|
99
137
|
}
|
|
100
138
|
else if (result.success) {
|
|
101
|
-
|
|
139
|
+
log.info("Task completed successfully");
|
|
102
140
|
await this.submitResult(task.id, {
|
|
103
141
|
status: "completed",
|
|
104
142
|
result: result.result,
|
|
@@ -107,7 +145,7 @@ export class TaskExecutor {
|
|
|
107
145
|
});
|
|
108
146
|
}
|
|
109
147
|
else {
|
|
110
|
-
|
|
148
|
+
log.info("Task failed");
|
|
111
149
|
await this.submitResult(task.id, {
|
|
112
150
|
status: "failed",
|
|
113
151
|
result: result.result,
|
|
@@ -124,7 +162,7 @@ export class TaskExecutor {
|
|
|
124
162
|
headers: this.headers,
|
|
125
163
|
body: JSON.stringify({ workerId: this.workerId, ...result }),
|
|
126
164
|
}, retryOpts);
|
|
127
|
-
|
|
165
|
+
log.info(`Task ${taskId} submitted as ${result.status}`);
|
|
128
166
|
}
|
|
129
167
|
async updateTaskStatus(taskId, status) {
|
|
130
168
|
try {
|
|
@@ -136,7 +174,7 @@ export class TaskExecutor {
|
|
|
136
174
|
}
|
|
137
175
|
catch {
|
|
138
176
|
// Status update failure is non-fatal — task will still be processed
|
|
139
|
-
|
|
177
|
+
log.warn(`Failed to update status to ${status}`);
|
|
140
178
|
}
|
|
141
179
|
}
|
|
142
180
|
async resolveProcess(taskType) {
|
|
@@ -145,7 +183,7 @@ export class TaskExecutor {
|
|
|
145
183
|
const { parsed } = await res.json();
|
|
146
184
|
if (!parsed?.steps?.length)
|
|
147
185
|
return null;
|
|
148
|
-
//
|
|
186
|
+
// Build text hint for AI context
|
|
149
187
|
const lines = [`Follow this process (${parsed.name} v${parsed.version}):\n`];
|
|
150
188
|
for (let i = 0; i < parsed.steps.length; i++) {
|
|
151
189
|
const step = parsed.steps[i];
|
|
@@ -160,13 +198,12 @@ export class TaskExecutor {
|
|
|
160
198
|
desc += ` [REQUIRES HUMAN APPROVAL]`;
|
|
161
199
|
lines.push(desc);
|
|
162
200
|
}
|
|
163
|
-
if (parsed.credentialsNeeded?.length
|
|
201
|
+
if (parsed.credentialsNeeded?.length) {
|
|
164
202
|
lines.push(`\nCredentials available: ${parsed.credentialsNeeded.join(", ")}`);
|
|
165
203
|
}
|
|
166
|
-
return lines.join("\n");
|
|
204
|
+
return { steps: parsed.steps, hint: lines.join("\n") };
|
|
167
205
|
}
|
|
168
206
|
catch {
|
|
169
|
-
// Process resolution failure is non-fatal
|
|
170
207
|
return null;
|
|
171
208
|
}
|
|
172
209
|
}
|
|
@@ -177,7 +214,7 @@ export class TaskExecutor {
|
|
|
177
214
|
return value;
|
|
178
215
|
}
|
|
179
216
|
catch (err) {
|
|
180
|
-
|
|
217
|
+
log.warn(`Failed to fetch secret "${key}": ${err.message}`);
|
|
181
218
|
return null;
|
|
182
219
|
}
|
|
183
220
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epic-cloudcontrol/daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "CloudControl local daemon — executes AI agent tasks on worker machines",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"dev": "tsx src/cli.ts",
|
|
16
16
|
"build": "tsc",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
17
19
|
"prepublishOnly": "npm run build",
|
|
18
20
|
"start": "node dist/cli.js start"
|
|
19
21
|
},
|
|
@@ -31,6 +33,15 @@
|
|
|
31
33
|
"devDependencies": {
|
|
32
34
|
"@types/node": "^22.0.0",
|
|
33
35
|
"tsx": "^4.19.0",
|
|
34
|
-
"typescript": "^5.7.0"
|
|
36
|
+
"typescript": "^5.7.0",
|
|
37
|
+
"vitest": "^4.1.2"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"playwright": ">=1.40.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"playwright": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
35
46
|
}
|
|
36
47
|
}
|