@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.
@@ -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
- console.log(`[executor] Retry attempt ${attempt}: ${err.message}`);
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
- console.log(`[executor] Failed to claim task ${taskId}: ${err.message}`);
48
+ log.warn(`Failed to claim task ${taskId}: ${err.message}`);
46
49
  return null;
47
50
  }
48
51
  }
49
52
  async executeTask(task) {
50
- console.log(`[executor] Executing: ${task.title}`);
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 processSteps = await this.resolveProcess(task.taskType);
56
- if (processSteps) {
57
- // Inject process steps into the task's processHint
58
- task.processHint = processSteps;
59
- console.log(`[executor] Process loaded for type "${task.taskType}"`);
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
- console.log(`[executor] Secret "${key}" loaded`);
71
+ log.info(`Secret "${key}" loaded`);
69
72
  }
70
73
  }
71
74
  }
72
75
  // Select model based on task's hint
73
- const { adapter, name: modelName } = this.router.select(task.modelHint);
74
- console.log(`[executor] Model: ${modelName} (hint: ${task.modelHint || "auto"})`);
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
- result = await adapter.execute(task);
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
- console.log(`[executor] Task requires human input: ${result.humanRequired.reason}`);
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
- console.log(`[executor] Task completed successfully`);
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
- console.log(`[executor] Task failed`);
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
- console.log(`[executor] Task ${taskId} submitted as ${result.status}`);
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
- console.log(`[executor] Warning: failed to update status to ${status}`);
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
- // Format steps as instructions for the AI
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 > 0) {
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
- console.log(`[executor] Failed to fetch secret "${key}": ${err.message}`);
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.2.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
  }