@awcp/sdk 0.0.16 → 0.0.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/dist/delegator/admission.d.ts +7 -28
- package/dist/delegator/admission.d.ts.map +1 -1
- package/dist/delegator/admission.js +35 -26
- package/dist/delegator/admission.js.map +1 -1
- package/dist/delegator/bin/client.d.ts +2 -2
- package/dist/delegator/bin/client.d.ts.map +1 -1
- package/dist/delegator/bin/client.js +6 -2
- package/dist/delegator/bin/client.js.map +1 -1
- package/dist/delegator/bin/daemon.d.ts +1 -1
- package/dist/delegator/bin/daemon.d.ts.map +1 -1
- package/dist/delegator/bin/daemon.js +13 -4
- package/dist/delegator/bin/daemon.js.map +1 -1
- package/dist/delegator/config.d.ts +57 -78
- package/dist/delegator/config.d.ts.map +1 -1
- package/dist/delegator/config.js +40 -24
- package/dist/delegator/config.js.map +1 -1
- package/dist/delegator/delegation-manager.d.ts +12 -0
- package/dist/delegator/delegation-manager.d.ts.map +1 -0
- package/dist/delegator/delegation-manager.js +36 -0
- package/dist/delegator/delegation-manager.js.map +1 -0
- package/dist/delegator/{environment-builder.d.ts → environment-manager.d.ts} +6 -12
- package/dist/delegator/{environment-builder.d.ts.map → environment-manager.d.ts.map} +1 -1
- package/dist/delegator/{environment-builder.js → environment-manager.js} +8 -26
- package/dist/delegator/environment-manager.js.map +1 -0
- package/dist/delegator/executor-client.d.ts +8 -25
- package/dist/delegator/executor-client.d.ts.map +1 -1
- package/dist/delegator/executor-client.js +33 -53
- package/dist/delegator/executor-client.js.map +1 -1
- package/dist/delegator/index.d.ts +5 -4
- package/dist/delegator/index.d.ts.map +1 -1
- package/dist/delegator/index.js +3 -2
- package/dist/delegator/index.js.map +1 -1
- package/dist/delegator/service.d.ts +28 -14
- package/dist/delegator/service.d.ts.map +1 -1
- package/dist/delegator/service.js +258 -297
- package/dist/delegator/service.js.map +1 -1
- package/dist/delegator/snapshot-manager.d.ts +23 -0
- package/dist/delegator/snapshot-manager.d.ts.map +1 -0
- package/dist/delegator/snapshot-manager.js +120 -0
- package/dist/delegator/snapshot-manager.js.map +1 -0
- package/dist/executor/a2a-adapter.d.ts +1 -1
- package/dist/executor/a2a-adapter.d.ts.map +1 -1
- package/dist/executor/admission.d.ts +13 -0
- package/dist/executor/admission.d.ts.map +1 -0
- package/dist/executor/admission.js +27 -0
- package/dist/executor/admission.js.map +1 -0
- package/dist/executor/assignment-manager.d.ts +12 -0
- package/dist/executor/assignment-manager.d.ts.map +1 -0
- package/dist/executor/assignment-manager.js +36 -0
- package/dist/executor/assignment-manager.js.map +1 -0
- package/dist/executor/config.d.ts +37 -24
- package/dist/executor/config.d.ts.map +1 -1
- package/dist/executor/config.js +21 -15
- package/dist/executor/config.js.map +1 -1
- package/dist/executor/index.d.ts +4 -2
- package/dist/executor/index.d.ts.map +1 -1
- package/dist/executor/index.js +3 -1
- package/dist/executor/index.js.map +1 -1
- package/dist/executor/service.d.ts +18 -12
- package/dist/executor/service.d.ts.map +1 -1
- package/dist/executor/service.js +325 -211
- package/dist/executor/service.js.map +1 -1
- package/dist/executor/workspace-manager.d.ts +1 -8
- package/dist/executor/workspace-manager.d.ts.map +1 -1
- package/dist/executor/workspace-manager.js +3 -25
- package/dist/executor/workspace-manager.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/listener/http-listener.d.ts +1 -1
- package/dist/listener/http-listener.d.ts.map +1 -1
- package/dist/listener/http-listener.js +8 -1
- package/dist/listener/http-listener.js.map +1 -1
- package/dist/listener/index.d.ts +1 -0
- package/dist/listener/index.d.ts.map +1 -1
- package/dist/listener/index.js.map +1 -1
- package/dist/listener/types.d.ts +52 -0
- package/dist/listener/types.d.ts.map +1 -0
- package/dist/listener/types.js +5 -0
- package/dist/listener/types.js.map +1 -0
- package/dist/listener/websocket-tunnel-listener.d.ts +1 -1
- package/dist/listener/websocket-tunnel-listener.d.ts.map +1 -1
- package/dist/server/express/awcp-executor-handler.d.ts +3 -3
- package/dist/server/express/awcp-executor-handler.d.ts.map +1 -1
- package/dist/server/express/awcp-executor-handler.js +3 -1
- package/dist/server/express/awcp-executor-handler.js.map +1 -1
- package/dist/utils/fs-helpers.d.ts +1 -1
- package/dist/utils/fs-helpers.js +1 -1
- package/package.json +2 -2
- package/dist/delegator/environment-builder.js.map +0 -1
- package/dist/delegator/snapshot-store.d.ts +0 -27
- package/dist/delegator/snapshot-store.d.ts.map +0 -1
- package/dist/delegator/snapshot-store.js +0 -40
- package/dist/delegator/snapshot-store.js.map +0 -1
package/dist/executor/service.js
CHANGED
|
@@ -2,144 +2,240 @@
|
|
|
2
2
|
* AWCP Executor Service
|
|
3
3
|
*/
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
5
|
-
import {
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { AssignmentStateMachine, isTerminalAssignmentState, generateSnapshotId, createAssignment, PROTOCOL_VERSION, ErrorCodes, AwcpError, CancelledError, } from '@awcp/core';
|
|
6
7
|
import { resolveExecutorConfig } from './config.js';
|
|
8
|
+
import { AdmissionController } from './admission.js';
|
|
9
|
+
import { AssignmentManager } from './assignment-manager.js';
|
|
7
10
|
import { WorkspaceManager } from './workspace-manager.js';
|
|
8
11
|
export class ExecutorService {
|
|
9
|
-
executor;
|
|
10
12
|
config;
|
|
11
13
|
transport;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
executor;
|
|
15
|
+
admissionController;
|
|
16
|
+
workspaceManager;
|
|
17
|
+
assignmentManager;
|
|
18
|
+
assignments = new Map();
|
|
19
|
+
stateMachines = new Map();
|
|
20
|
+
eventEmitters = new Map();
|
|
21
|
+
cleanupTimer;
|
|
16
22
|
constructor(options) {
|
|
17
|
-
this.executor = options.executor;
|
|
18
23
|
this.config = resolveExecutorConfig(options.config);
|
|
19
24
|
this.transport = this.config.transport;
|
|
20
|
-
this.
|
|
25
|
+
this.executor = options.executor;
|
|
26
|
+
this.admissionController = new AdmissionController(this.config.admission);
|
|
27
|
+
this.workspaceManager = new WorkspaceManager(this.config.workDir);
|
|
28
|
+
this.assignmentManager = new AssignmentManager({
|
|
29
|
+
baseDir: join(this.config.workDir, '.awcp', 'assignments'),
|
|
30
|
+
});
|
|
21
31
|
}
|
|
22
|
-
async
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
await this.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
async initialize() {
|
|
33
|
+
await this.transport.initialize?.(this.config.workDir);
|
|
34
|
+
if (this.config.cleanupOnInitialize) {
|
|
35
|
+
const persistedAssignments = await this.assignmentManager.loadAll();
|
|
36
|
+
for (const assignment of persistedAssignments) {
|
|
37
|
+
await this.transport.release({ delegationId: assignment.id, localPath: assignment.workPath }).catch(() => { });
|
|
38
|
+
await this.workspaceManager.release(assignment.workPath);
|
|
39
|
+
await this.assignmentManager.delete(assignment.id).catch(() => { });
|
|
40
|
+
}
|
|
41
|
+
this.startCleanupTimer();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const persistedAssignments = await this.assignmentManager.loadAll();
|
|
45
|
+
const knownIds = new Set(persistedAssignments.map(a => a.id));
|
|
46
|
+
for (const assignment of persistedAssignments) {
|
|
47
|
+
this.assignments.set(assignment.id, assignment);
|
|
48
|
+
this.stateMachines.set(assignment.id, new AssignmentStateMachine(assignment.state));
|
|
49
|
+
if (!isTerminalAssignmentState(assignment.state)) {
|
|
50
|
+
this.eventEmitters.set(assignment.id, new EventEmitter());
|
|
51
|
+
}
|
|
34
52
|
}
|
|
53
|
+
await this.workspaceManager.cleanupStale(knownIds);
|
|
54
|
+
this.startCleanupTimer();
|
|
35
55
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
async shutdown() {
|
|
57
|
+
if (this.cleanupTimer) {
|
|
58
|
+
clearInterval(this.cleanupTimer);
|
|
59
|
+
this.cleanupTimer = undefined;
|
|
60
|
+
}
|
|
61
|
+
await this.transport.shutdown?.();
|
|
62
|
+
this.assignments.clear();
|
|
63
|
+
this.stateMachines.clear();
|
|
64
|
+
this.eventEmitters.clear();
|
|
65
|
+
}
|
|
66
|
+
async handleMessage(message) {
|
|
67
|
+
try {
|
|
68
|
+
switch (message.type) {
|
|
69
|
+
case 'INVITE':
|
|
70
|
+
return await this.handleInvite(message);
|
|
71
|
+
case 'START':
|
|
72
|
+
await this.handleStart(message);
|
|
73
|
+
return null;
|
|
74
|
+
case 'ERROR':
|
|
75
|
+
await this.handleError(message);
|
|
76
|
+
return null;
|
|
77
|
+
default:
|
|
78
|
+
throw new Error(`Unexpected message type: ${message.type}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error instanceof AwcpError) {
|
|
83
|
+
return this.createErrorMessage(message.delegationId, error.code, error.message, error.hint);
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
51
86
|
}
|
|
52
|
-
const handler = (event) => callback(event);
|
|
53
|
-
delegation.eventEmitter.on('event', handler);
|
|
54
|
-
return () => {
|
|
55
|
-
delegation.eventEmitter.off('event', handler);
|
|
56
|
-
};
|
|
57
87
|
}
|
|
58
88
|
async handleInvite(invite) {
|
|
59
89
|
const { delegationId } = invite;
|
|
60
|
-
|
|
61
|
-
|
|
90
|
+
const existing = this.assignments.get(delegationId);
|
|
91
|
+
if (existing) {
|
|
92
|
+
await this.transport.detach({ delegationId, localPath: existing.workPath }).catch(() => { });
|
|
93
|
+
this.eventEmitters.delete(delegationId);
|
|
94
|
+
const retentionMs = Math.min(invite.retentionMs, this.config.assignment.maxRetentionMs);
|
|
95
|
+
existing.state = 'pending';
|
|
96
|
+
existing.invite = invite;
|
|
97
|
+
existing.retentionMs = retentionMs;
|
|
98
|
+
this.stateMachines.set(delegationId, new AssignmentStateMachine());
|
|
99
|
+
this.eventEmitters.set(delegationId, new EventEmitter());
|
|
100
|
+
await this.persistAssignment(delegationId);
|
|
101
|
+
console.log(`[AWCP:Executor] Re-accepting delegation ${delegationId} (was ${existing.state})`);
|
|
102
|
+
return {
|
|
103
|
+
version: PROTOCOL_VERSION,
|
|
104
|
+
type: 'ACCEPT',
|
|
105
|
+
delegationId,
|
|
106
|
+
retentionMs,
|
|
107
|
+
executorWorkDir: { path: existing.workPath },
|
|
108
|
+
executorConstraints: {
|
|
109
|
+
acceptedAccessMode: invite.lease.accessMode,
|
|
110
|
+
maxTtlSeconds: Math.min(invite.lease.ttlSeconds, this.config.admission.maxTtlSeconds),
|
|
111
|
+
sandboxProfile: this.config.assignment.sandbox,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
62
114
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
115
|
+
await this.admissionController.check({ invite, assignments: this.assignments, transport: this.transport });
|
|
116
|
+
await this.config.hooks.onAdmissionCheck?.(invite);
|
|
117
|
+
const retentionMs = Math.min(invite.retentionMs, this.config.assignment.maxRetentionMs);
|
|
118
|
+
const workPath = this.workspaceManager.allocate(delegationId);
|
|
119
|
+
try {
|
|
120
|
+
const assignment = createAssignment({ id: delegationId, invite, workPath, retentionMs });
|
|
121
|
+
this.assignments.set(delegationId, assignment);
|
|
122
|
+
this.stateMachines.set(delegationId, new AssignmentStateMachine());
|
|
123
|
+
this.eventEmitters.set(delegationId, new EventEmitter());
|
|
124
|
+
await this.persistAssignment(delegationId);
|
|
125
|
+
const executorConstraints = {
|
|
126
|
+
acceptedAccessMode: invite.lease.accessMode,
|
|
127
|
+
maxTtlSeconds: Math.min(invite.lease.ttlSeconds, this.config.admission.maxTtlSeconds),
|
|
128
|
+
sandboxProfile: this.config.assignment.sandbox,
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
version: PROTOCOL_VERSION,
|
|
132
|
+
type: 'ACCEPT',
|
|
133
|
+
delegationId,
|
|
134
|
+
retentionMs,
|
|
135
|
+
executorWorkDir: { path: workPath },
|
|
136
|
+
executorConstraints,
|
|
137
|
+
};
|
|
66
138
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
139
|
+
catch (error) {
|
|
140
|
+
this.assignments.delete(delegationId);
|
|
141
|
+
this.stateMachines.delete(delegationId);
|
|
142
|
+
this.eventEmitters.delete(delegationId);
|
|
143
|
+
await this.workspaceManager.release(workPath);
|
|
144
|
+
await this.assignmentManager.delete(delegationId).catch(() => { });
|
|
145
|
+
throw error;
|
|
70
146
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
147
|
+
}
|
|
148
|
+
async handleStart(start) {
|
|
149
|
+
const { delegationId } = start;
|
|
150
|
+
const assignment = this.assignments.get(delegationId);
|
|
151
|
+
if (!assignment) {
|
|
152
|
+
throw new Error(`Unknown delegation for START: ${delegationId}` +
|
|
153
|
+
` (known=[${Array.from(this.assignments.keys()).join(',')}])`);
|
|
74
154
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
155
|
+
this.transitionState(delegationId, { type: 'RECEIVE_START' });
|
|
156
|
+
assignment.lease = start.lease;
|
|
157
|
+
assignment.startedAt = new Date().toISOString();
|
|
158
|
+
await this.persistAssignment(delegationId);
|
|
159
|
+
console.log(`[AWCP:Executor] Delegation ${delegationId} started` +
|
|
160
|
+
` (active=${Array.from(this.assignments.values()).filter(a => a.state === 'active').length}, workPath=${assignment.workPath})`);
|
|
161
|
+
this.executeTask(delegationId, start);
|
|
162
|
+
}
|
|
163
|
+
async handleError(error) {
|
|
164
|
+
const { delegationId } = error;
|
|
165
|
+
const assignment = this.assignments.get(delegationId);
|
|
166
|
+
if (!assignment) {
|
|
167
|
+
throw new Error(`Unknown delegation for ERROR: ${delegationId}` +
|
|
168
|
+
` (known=[${Array.from(this.assignments.keys()).join(',')}])`);
|
|
80
169
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
170
|
+
this.transitionState(delegationId, { type: 'RECEIVE_ERROR' });
|
|
171
|
+
console.log(`[AWCP:Executor] Received ERROR for ${delegationId}: ${error.code} - ${error.message}`);
|
|
172
|
+
assignment.completedAt = new Date().toISOString();
|
|
173
|
+
assignment.error = { code: error.code, message: error.message, hint: error.hint };
|
|
174
|
+
await this.persistAssignment(delegationId);
|
|
175
|
+
this.config.hooks.onError?.(delegationId, new AwcpError(error.code, error.message, error.hint, delegationId));
|
|
176
|
+
}
|
|
177
|
+
subscribeTask(delegationId, callback) {
|
|
178
|
+
const assignment = this.assignments.get(delegationId);
|
|
179
|
+
if (!assignment) {
|
|
180
|
+
console.error(`[AWCP:Executor] SSE subscribe rejected for ${delegationId}: unknown delegation`);
|
|
181
|
+
const errorEvent = {
|
|
182
|
+
delegationId, type: 'error', timestamp: new Date().toISOString(),
|
|
183
|
+
code: 'NOT_FOUND', message: 'Delegation not found on executor',
|
|
184
|
+
};
|
|
185
|
+
callback(errorEvent);
|
|
186
|
+
return () => { };
|
|
187
|
+
}
|
|
188
|
+
const sm = this.stateMachines.get(delegationId);
|
|
189
|
+
if (sm.isTerminal()) {
|
|
190
|
+
console.log(`[AWCP:Executor] SSE reconnect for ${delegationId}, replaying ${assignment.state} event`);
|
|
191
|
+
const event = assignment.state === 'completed'
|
|
192
|
+
? {
|
|
193
|
+
delegationId, type: 'done', timestamp: assignment.completedAt,
|
|
194
|
+
summary: assignment.result?.summary ?? 'Task completed',
|
|
195
|
+
highlights: assignment.result?.highlights,
|
|
196
|
+
}
|
|
197
|
+
: {
|
|
198
|
+
delegationId, type: 'error', timestamp: assignment.completedAt,
|
|
199
|
+
code: assignment.error?.code ?? ErrorCodes.TASK_FAILED,
|
|
200
|
+
message: assignment.error?.message ?? 'Task failed',
|
|
201
|
+
hint: assignment.error?.hint,
|
|
202
|
+
};
|
|
203
|
+
setImmediate(() => callback(event));
|
|
204
|
+
return () => { };
|
|
87
205
|
}
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return this.createErrorMessage(delegationId, ErrorCodes.WORKDIR_DENIED, validation.reason ?? 'Workspace validation failed', 'Check workDir configuration');
|
|
206
|
+
const emitter = this.eventEmitters.get(delegationId);
|
|
207
|
+
if (!emitter) {
|
|
208
|
+
console.error(`[AWCP:Executor] SSE subscribe failed for ${delegationId}: no event emitter`);
|
|
209
|
+
return () => { };
|
|
93
210
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
maxTtlSeconds: Math.min(invite.lease.ttlSeconds, maxTtl),
|
|
101
|
-
sandboxProfile: this.config.sandbox,
|
|
102
|
-
};
|
|
103
|
-
const acceptMessage = {
|
|
104
|
-
version: PROTOCOL_VERSION,
|
|
105
|
-
type: 'ACCEPT',
|
|
106
|
-
delegationId,
|
|
107
|
-
executorWorkDir: { path: workPath },
|
|
108
|
-
executorConstraints,
|
|
211
|
+
console.log(`[AWCP:Executor] SSE subscriber attached for ${delegationId} (state=${assignment.state})`);
|
|
212
|
+
const handler = (event) => callback(event);
|
|
213
|
+
emitter.on('event', handler);
|
|
214
|
+
return () => {
|
|
215
|
+
console.log(`[AWCP:Executor] SSE subscriber detached for ${delegationId}`);
|
|
216
|
+
emitter.off('event', handler);
|
|
109
217
|
};
|
|
110
|
-
return acceptMessage;
|
|
111
|
-
}
|
|
112
|
-
async handleStart(start) {
|
|
113
|
-
const { delegationId } = start;
|
|
114
|
-
const pending = this.pendingInvitations.get(delegationId);
|
|
115
|
-
if (!pending) {
|
|
116
|
-
console.warn(`[AWCP:Executor] Unknown delegation for START: ${delegationId}`);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
const workPath = this.workspace.allocate(delegationId);
|
|
120
|
-
this.pendingInvitations.delete(delegationId);
|
|
121
|
-
const eventEmitter = new EventEmitter();
|
|
122
|
-
this.activeDelegations.set(delegationId, {
|
|
123
|
-
id: delegationId,
|
|
124
|
-
workPath,
|
|
125
|
-
task: pending.invite.task,
|
|
126
|
-
lease: start.lease,
|
|
127
|
-
environment: pending.invite.environment,
|
|
128
|
-
startedAt: new Date(),
|
|
129
|
-
eventEmitter,
|
|
130
|
-
});
|
|
131
|
-
// Task execution runs async - don't await
|
|
132
|
-
this.executeTask(delegationId, start, workPath, pending.invite.task, start.lease, pending.invite.environment, eventEmitter);
|
|
133
218
|
}
|
|
134
|
-
async executeTask(delegationId, start
|
|
219
|
+
async executeTask(delegationId, start) {
|
|
220
|
+
const assignment = this.assignments.get(delegationId);
|
|
221
|
+
const emitter = this.eventEmitters.get(delegationId);
|
|
135
222
|
try {
|
|
136
|
-
|
|
223
|
+
console.log(`[AWCP:Executor] Task ${delegationId} preparing workspace...`);
|
|
224
|
+
await this.workspaceManager.prepare(assignment.workPath);
|
|
225
|
+
console.log(`[AWCP:Executor] Task ${delegationId} setting up transport (${this.transport.type})...`);
|
|
137
226
|
const actualPath = await this.transport.setup({
|
|
138
227
|
delegationId,
|
|
139
|
-
|
|
140
|
-
|
|
228
|
+
handle: start.transportHandle,
|
|
229
|
+
localPath: assignment.workPath,
|
|
230
|
+
});
|
|
231
|
+
this.config.hooks.onTaskStart?.({
|
|
232
|
+
delegationId,
|
|
233
|
+
workPath: actualPath,
|
|
234
|
+
task: assignment.invite.task,
|
|
235
|
+
lease: assignment.lease,
|
|
236
|
+
environment: assignment.invite.environment,
|
|
141
237
|
});
|
|
142
|
-
|
|
238
|
+
console.log(`[AWCP:Executor] Task ${delegationId} executing (listeners=${emitter.listenerCount('event')})...`);
|
|
143
239
|
const statusEvent = {
|
|
144
240
|
delegationId,
|
|
145
241
|
type: 'status',
|
|
@@ -147,16 +243,17 @@ export class ExecutorService {
|
|
|
147
243
|
status: 'running',
|
|
148
244
|
message: 'Task execution started',
|
|
149
245
|
};
|
|
150
|
-
|
|
246
|
+
emitter.emit('event', statusEvent);
|
|
151
247
|
const result = await this.executor.execute({
|
|
152
248
|
delegationId,
|
|
153
249
|
workPath: actualPath,
|
|
154
|
-
task,
|
|
155
|
-
environment,
|
|
250
|
+
task: assignment.invite.task,
|
|
251
|
+
environment: assignment.invite.environment,
|
|
156
252
|
});
|
|
157
|
-
|
|
253
|
+
console.log(`[AWCP:Executor] Task ${delegationId} completed, capturing snapshot...`);
|
|
254
|
+
const snapshotResult = await this.transport.captureSnapshot?.({ delegationId, localPath: actualPath });
|
|
158
255
|
const snapshotId = generateSnapshotId();
|
|
159
|
-
if (
|
|
256
|
+
if (snapshotResult?.snapshotBase64) {
|
|
160
257
|
const snapshotEvent = {
|
|
161
258
|
delegationId,
|
|
162
259
|
type: 'snapshot',
|
|
@@ -164,10 +261,10 @@ export class ExecutorService {
|
|
|
164
261
|
snapshotId,
|
|
165
262
|
summary: result.summary,
|
|
166
263
|
highlights: result.highlights,
|
|
167
|
-
snapshotBase64:
|
|
264
|
+
snapshotBase64: snapshotResult.snapshotBase64,
|
|
168
265
|
recommended: true,
|
|
169
266
|
};
|
|
170
|
-
|
|
267
|
+
emitter.emit('event', snapshotEvent);
|
|
171
268
|
}
|
|
172
269
|
const doneEvent = {
|
|
173
270
|
delegationId,
|
|
@@ -175,32 +272,25 @@ export class ExecutorService {
|
|
|
175
272
|
timestamp: new Date().toISOString(),
|
|
176
273
|
summary: result.summary,
|
|
177
274
|
highlights: result.highlights,
|
|
178
|
-
snapshotIds:
|
|
179
|
-
recommendedSnapshotId:
|
|
275
|
+
snapshotIds: snapshotResult?.snapshotBase64 ? [snapshotId] : undefined,
|
|
276
|
+
recommendedSnapshotId: snapshotResult?.snapshotBase64 ? snapshotId : undefined,
|
|
180
277
|
};
|
|
181
|
-
|
|
278
|
+
emitter.emit('event', doneEvent);
|
|
279
|
+
console.log(`[AWCP:Executor] Task ${delegationId} done event emitted` +
|
|
280
|
+
` (listeners=${emitter.listenerCount('event')})`);
|
|
182
281
|
this.config.hooks.onTaskComplete?.(delegationId, result.summary);
|
|
183
|
-
this.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
} : undefined,
|
|
193
|
-
});
|
|
194
|
-
this.scheduleResultCleanup(delegationId);
|
|
195
|
-
this.activeDelegations.delete(delegationId);
|
|
196
|
-
await this.workspace.release(actualPath);
|
|
282
|
+
this.transitionState(delegationId, { type: 'TASK_COMPLETE' });
|
|
283
|
+
assignment.completedAt = new Date().toISOString();
|
|
284
|
+
assignment.result = {
|
|
285
|
+
summary: result.summary,
|
|
286
|
+
highlights: result.highlights,
|
|
287
|
+
snapshotBase64: snapshotResult?.snapshotBase64,
|
|
288
|
+
};
|
|
289
|
+
await this.persistAssignment(delegationId);
|
|
290
|
+
await this.transport.detach({ delegationId, localPath: assignment.workPath }).catch(() => { });
|
|
197
291
|
}
|
|
198
292
|
catch (error) {
|
|
199
|
-
|
|
200
|
-
if (delegation) {
|
|
201
|
-
await this.transport.teardown({ delegationId, workDir: delegation.workPath }).catch(() => { });
|
|
202
|
-
await this.workspace.release(delegation.workPath);
|
|
203
|
-
}
|
|
293
|
+
console.error(`[AWCP:Executor] Task ${delegationId} failed:`, error instanceof Error ? error.message : error);
|
|
204
294
|
const errorEvent = {
|
|
205
295
|
delegationId,
|
|
206
296
|
type: 'error',
|
|
@@ -209,37 +299,30 @@ export class ExecutorService {
|
|
|
209
299
|
message: error instanceof Error ? error.message : String(error),
|
|
210
300
|
hint: 'Check task requirements and try again',
|
|
211
301
|
};
|
|
212
|
-
|
|
302
|
+
emitter.emit('event', errorEvent);
|
|
303
|
+
console.log(`[AWCP:Executor] Task ${delegationId} error event emitted` +
|
|
304
|
+
` (listeners=${emitter.listenerCount('event')})`);
|
|
213
305
|
this.config.hooks.onError?.(delegationId, error instanceof Error ? error : new Error(String(error)));
|
|
214
|
-
this.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
error:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
});
|
|
224
|
-
this.scheduleResultCleanup(delegationId);
|
|
225
|
-
this.activeDelegations.delete(delegationId);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
async handleError(error) {
|
|
229
|
-
const { delegationId } = error;
|
|
230
|
-
const delegation = this.activeDelegations.get(delegationId);
|
|
231
|
-
if (delegation) {
|
|
232
|
-
await this.transport.teardown({ delegationId, workDir: delegation.workPath }).catch(() => { });
|
|
233
|
-
this.activeDelegations.delete(delegationId);
|
|
234
|
-
await this.workspace.release(delegation.workPath);
|
|
306
|
+
this.transitionState(delegationId, { type: 'TASK_FAIL' });
|
|
307
|
+
assignment.completedAt = new Date().toISOString();
|
|
308
|
+
assignment.error = {
|
|
309
|
+
code: ErrorCodes.TASK_FAILED,
|
|
310
|
+
message: error instanceof Error ? error.message : String(error),
|
|
311
|
+
hint: 'Check task requirements and try again',
|
|
312
|
+
};
|
|
313
|
+
await this.persistAssignment(delegationId);
|
|
314
|
+
await this.transport.detach({ delegationId, localPath: assignment.workPath }).catch(() => { });
|
|
235
315
|
}
|
|
236
|
-
this.pendingInvitations.delete(delegationId);
|
|
237
|
-
this.config.hooks.onError?.(delegationId, new AwcpError(error.code, error.message, error.hint, delegationId));
|
|
238
316
|
}
|
|
239
317
|
async cancelDelegation(delegationId) {
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
242
|
-
|
|
318
|
+
const assignment = this.assignments.get(delegationId);
|
|
319
|
+
if (!assignment) {
|
|
320
|
+
throw new Error(`Delegation not found: ${delegationId}`);
|
|
321
|
+
}
|
|
322
|
+
this.transitionState(delegationId, { type: 'CANCEL' });
|
|
323
|
+
console.log(`[AWCP:Executor] Cancelling delegation ${delegationId}`);
|
|
324
|
+
const emitter = this.eventEmitters.get(delegationId);
|
|
325
|
+
if (emitter) {
|
|
243
326
|
const errorEvent = {
|
|
244
327
|
delegationId,
|
|
245
328
|
type: 'error',
|
|
@@ -247,67 +330,98 @@ export class ExecutorService {
|
|
|
247
330
|
code: ErrorCodes.CANCELLED,
|
|
248
331
|
message: 'Delegation cancelled',
|
|
249
332
|
};
|
|
250
|
-
|
|
251
|
-
this.activeDelegations.delete(delegationId);
|
|
252
|
-
await this.workspace.release(delegation.workPath);
|
|
253
|
-
this.config.hooks.onError?.(delegationId, new CancelledError('Delegation cancelled by Delegator', undefined, delegationId));
|
|
254
|
-
return;
|
|
333
|
+
emitter.emit('event', errorEvent);
|
|
255
334
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
335
|
+
assignment.completedAt = new Date().toISOString();
|
|
336
|
+
assignment.error = { code: ErrorCodes.CANCELLED, message: 'Delegation cancelled' };
|
|
337
|
+
await this.persistAssignment(delegationId);
|
|
338
|
+
await this.transport.detach({ delegationId, localPath: assignment.workPath }).catch(() => { });
|
|
339
|
+
this.config.hooks.onError?.(delegationId, new CancelledError('Delegation cancelled by Delegator', undefined, delegationId));
|
|
261
340
|
}
|
|
262
341
|
getStatus() {
|
|
342
|
+
const active = Array.from(this.assignments.values()).filter(a => a.state === 'active');
|
|
263
343
|
return {
|
|
264
|
-
pendingInvitations: this.
|
|
265
|
-
activeDelegations:
|
|
266
|
-
completedDelegations: this.
|
|
267
|
-
delegations:
|
|
268
|
-
id:
|
|
269
|
-
workPath:
|
|
270
|
-
startedAt:
|
|
344
|
+
pendingInvitations: Array.from(this.assignments.values()).filter(a => a.state === 'pending').length,
|
|
345
|
+
activeDelegations: active.length,
|
|
346
|
+
completedDelegations: Array.from(this.assignments.values()).filter(a => a.state === 'completed' || a.state === 'error').length,
|
|
347
|
+
delegations: active.map((a) => ({
|
|
348
|
+
id: a.id,
|
|
349
|
+
workPath: a.workPath,
|
|
350
|
+
startedAt: a.startedAt,
|
|
271
351
|
})),
|
|
272
352
|
};
|
|
273
353
|
}
|
|
274
354
|
getTaskResult(delegationId) {
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const completed = this.completedDelegations.get(delegationId);
|
|
280
|
-
if (completed) {
|
|
281
|
-
if (completed.state === 'completed') {
|
|
282
|
-
return {
|
|
283
|
-
status: 'completed',
|
|
284
|
-
completedAt: completed.completedAt.toISOString(),
|
|
285
|
-
summary: completed.snapshot?.summary,
|
|
286
|
-
highlights: completed.snapshot?.highlights,
|
|
287
|
-
snapshotBase64: completed.snapshot?.snapshotBase64,
|
|
288
|
-
};
|
|
355
|
+
const assignment = this.assignments.get(delegationId);
|
|
356
|
+
if (!assignment) {
|
|
357
|
+
if (this.transport.type === 'sshfs') {
|
|
358
|
+
return { status: 'not_applicable', reason: 'SSHFS transport writes directly to source' };
|
|
289
359
|
}
|
|
290
|
-
return {
|
|
291
|
-
status: 'error',
|
|
292
|
-
completedAt: completed.completedAt.toISOString(),
|
|
293
|
-
error: completed.error,
|
|
294
|
-
};
|
|
360
|
+
return { status: 'not_found' };
|
|
295
361
|
}
|
|
296
|
-
|
|
362
|
+
const sm = this.stateMachines.get(delegationId);
|
|
363
|
+
if (!sm.isTerminal()) {
|
|
364
|
+
return { status: 'running' };
|
|
365
|
+
}
|
|
366
|
+
if (assignment.state === 'completed') {
|
|
297
367
|
return {
|
|
298
|
-
status: '
|
|
299
|
-
|
|
368
|
+
status: 'completed',
|
|
369
|
+
completedAt: assignment.completedAt,
|
|
370
|
+
summary: assignment.result?.summary,
|
|
371
|
+
highlights: assignment.result?.highlights,
|
|
372
|
+
snapshotBase64: assignment.result?.snapshotBase64,
|
|
300
373
|
};
|
|
301
374
|
}
|
|
302
|
-
return {
|
|
375
|
+
return {
|
|
376
|
+
status: 'error',
|
|
377
|
+
completedAt: assignment.completedAt,
|
|
378
|
+
error: assignment.error,
|
|
379
|
+
};
|
|
303
380
|
}
|
|
304
381
|
acknowledgeResult(delegationId) {
|
|
305
|
-
this.
|
|
382
|
+
const assignment = this.assignments.get(delegationId);
|
|
383
|
+
const sm = this.stateMachines.get(delegationId);
|
|
384
|
+
if (assignment && sm?.isTerminal()) {
|
|
385
|
+
this.assignments.delete(delegationId);
|
|
386
|
+
this.stateMachines.delete(delegationId);
|
|
387
|
+
this.eventEmitters.delete(delegationId);
|
|
388
|
+
this.assignmentManager.delete(delegationId).catch(() => { });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
transitionState(delegationId, event) {
|
|
392
|
+
const sm = this.stateMachines.get(delegationId);
|
|
393
|
+
const assignment = this.assignments.get(delegationId);
|
|
394
|
+
const result = sm.transition(event);
|
|
395
|
+
if (!result.success) {
|
|
396
|
+
throw new Error(`Cannot transition assignment ${delegationId} (${event.type}) in state '${assignment.state}': ${result.error}`);
|
|
397
|
+
}
|
|
398
|
+
assignment.state = sm.getState();
|
|
399
|
+
assignment.updatedAt = new Date().toISOString();
|
|
306
400
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
401
|
+
async persistAssignment(delegationId) {
|
|
402
|
+
const assignment = this.assignments.get(delegationId);
|
|
403
|
+
if (assignment) {
|
|
404
|
+
await this.assignmentManager.save(assignment);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
startCleanupTimer() {
|
|
408
|
+
this.cleanupTimer = setInterval(async () => {
|
|
409
|
+
const now = Date.now();
|
|
410
|
+
for (const [id, assignment] of this.assignments) {
|
|
411
|
+
const sm = this.stateMachines.get(id);
|
|
412
|
+
if (!sm?.isTerminal())
|
|
413
|
+
continue;
|
|
414
|
+
const updatedAt = new Date(assignment.updatedAt).getTime();
|
|
415
|
+
if (now - updatedAt > assignment.retentionMs) {
|
|
416
|
+
await this.transport.release({ delegationId: id, localPath: assignment.workPath }).catch(() => { });
|
|
417
|
+
await this.workspaceManager.release(assignment.workPath);
|
|
418
|
+
await this.assignmentManager.delete(id).catch(() => { });
|
|
419
|
+
this.assignments.delete(id);
|
|
420
|
+
this.stateMachines.delete(id);
|
|
421
|
+
this.eventEmitters.delete(id);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}, 60 * 1000);
|
|
311
425
|
}
|
|
312
426
|
createErrorMessage(delegationId, code, message, hint) {
|
|
313
427
|
return {
|