@awcp/sdk 0.0.17 → 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/daemon.d.ts +1 -1
- package/dist/delegator/bin/daemon.d.ts.map +1 -1
- package/dist/delegator/bin/daemon.js +5 -3
- 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 +27 -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 +253 -300
- 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 +320 -234
- 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 +7 -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,160 +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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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;
|
|
59
86
|
}
|
|
60
|
-
console.log(`[AWCP:Executor] SSE subscriber attached for ${delegationId}`);
|
|
61
|
-
const handler = (event) => callback(event);
|
|
62
|
-
delegation.eventEmitter.on('event', handler);
|
|
63
|
-
return () => {
|
|
64
|
-
console.log(`[AWCP:Executor] SSE subscriber detached for ${delegationId}`);
|
|
65
|
-
delegation.eventEmitter.off('event', handler);
|
|
66
|
-
};
|
|
67
87
|
}
|
|
68
88
|
async handleInvite(invite) {
|
|
69
89
|
const { delegationId } = invite;
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
};
|
|
114
|
+
}
|
|
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
|
+
};
|
|
72
138
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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;
|
|
76
146
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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(',')}])`);
|
|
80
154
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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(',')}])`);
|
|
84
169
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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 () => { };
|
|
90
187
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 () => { };
|
|
97
205
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
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 () => { };
|
|
103
210
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
maxTtlSeconds: Math.min(invite.lease.ttlSeconds, maxTtl),
|
|
111
|
-
sandboxProfile: this.config.sandbox,
|
|
112
|
-
};
|
|
113
|
-
const acceptMessage = {
|
|
114
|
-
version: PROTOCOL_VERSION,
|
|
115
|
-
type: 'ACCEPT',
|
|
116
|
-
delegationId,
|
|
117
|
-
executorWorkDir: { path: workPath },
|
|
118
|
-
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);
|
|
119
217
|
};
|
|
120
|
-
return acceptMessage;
|
|
121
|
-
}
|
|
122
|
-
async handleStart(start) {
|
|
123
|
-
const { delegationId } = start;
|
|
124
|
-
const pending = this.pendingInvitations.get(delegationId);
|
|
125
|
-
if (!pending) {
|
|
126
|
-
console.warn(`[AWCP:Executor] START rejected for unknown delegation ${delegationId}` +
|
|
127
|
-
` (pending=[${Array.from(this.pendingInvitations.keys()).join(',')}])`);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const workPath = this.workspace.allocate(delegationId);
|
|
131
|
-
this.pendingInvitations.delete(delegationId);
|
|
132
|
-
const eventEmitter = new EventEmitter();
|
|
133
|
-
this.activeDelegations.set(delegationId, {
|
|
134
|
-
id: delegationId,
|
|
135
|
-
workPath,
|
|
136
|
-
task: pending.invite.task,
|
|
137
|
-
lease: start.lease,
|
|
138
|
-
environment: pending.invite.environment,
|
|
139
|
-
startedAt: new Date(),
|
|
140
|
-
eventEmitter,
|
|
141
|
-
});
|
|
142
|
-
console.log(`[AWCP:Executor] Delegation ${delegationId} registered` +
|
|
143
|
-
` (active=${this.activeDelegations.size}, workPath=${workPath})`);
|
|
144
|
-
// Task execution runs async - don't await
|
|
145
|
-
this.executeTask(delegationId, start, workPath, pending.invite.task, start.lease, pending.invite.environment, eventEmitter);
|
|
146
218
|
}
|
|
147
|
-
async executeTask(delegationId, start
|
|
219
|
+
async executeTask(delegationId, start) {
|
|
220
|
+
const assignment = this.assignments.get(delegationId);
|
|
221
|
+
const emitter = this.eventEmitters.get(delegationId);
|
|
148
222
|
try {
|
|
149
223
|
console.log(`[AWCP:Executor] Task ${delegationId} preparing workspace...`);
|
|
150
|
-
await this.
|
|
224
|
+
await this.workspaceManager.prepare(assignment.workPath);
|
|
151
225
|
console.log(`[AWCP:Executor] Task ${delegationId} setting up transport (${this.transport.type})...`);
|
|
152
226
|
const actualPath = await this.transport.setup({
|
|
153
227
|
delegationId,
|
|
154
|
-
|
|
155
|
-
|
|
228
|
+
handle: start.transportHandle,
|
|
229
|
+
localPath: assignment.workPath,
|
|
156
230
|
});
|
|
157
|
-
this.config.hooks.onTaskStart?.({
|
|
158
|
-
|
|
231
|
+
this.config.hooks.onTaskStart?.({
|
|
232
|
+
delegationId,
|
|
233
|
+
workPath: actualPath,
|
|
234
|
+
task: assignment.invite.task,
|
|
235
|
+
lease: assignment.lease,
|
|
236
|
+
environment: assignment.invite.environment,
|
|
237
|
+
});
|
|
238
|
+
console.log(`[AWCP:Executor] Task ${delegationId} executing (listeners=${emitter.listenerCount('event')})...`);
|
|
159
239
|
const statusEvent = {
|
|
160
240
|
delegationId,
|
|
161
241
|
type: 'status',
|
|
@@ -163,17 +243,17 @@ export class ExecutorService {
|
|
|
163
243
|
status: 'running',
|
|
164
244
|
message: 'Task execution started',
|
|
165
245
|
};
|
|
166
|
-
|
|
246
|
+
emitter.emit('event', statusEvent);
|
|
167
247
|
const result = await this.executor.execute({
|
|
168
248
|
delegationId,
|
|
169
249
|
workPath: actualPath,
|
|
170
|
-
task,
|
|
171
|
-
environment,
|
|
250
|
+
task: assignment.invite.task,
|
|
251
|
+
environment: assignment.invite.environment,
|
|
172
252
|
});
|
|
173
|
-
console.log(`[AWCP:Executor] Task ${delegationId} completed,
|
|
174
|
-
const
|
|
253
|
+
console.log(`[AWCP:Executor] Task ${delegationId} completed, capturing snapshot...`);
|
|
254
|
+
const snapshotResult = await this.transport.captureSnapshot?.({ delegationId, localPath: actualPath });
|
|
175
255
|
const snapshotId = generateSnapshotId();
|
|
176
|
-
if (
|
|
256
|
+
if (snapshotResult?.snapshotBase64) {
|
|
177
257
|
const snapshotEvent = {
|
|
178
258
|
delegationId,
|
|
179
259
|
type: 'snapshot',
|
|
@@ -181,10 +261,10 @@ export class ExecutorService {
|
|
|
181
261
|
snapshotId,
|
|
182
262
|
summary: result.summary,
|
|
183
263
|
highlights: result.highlights,
|
|
184
|
-
snapshotBase64:
|
|
264
|
+
snapshotBase64: snapshotResult.snapshotBase64,
|
|
185
265
|
recommended: true,
|
|
186
266
|
};
|
|
187
|
-
|
|
267
|
+
emitter.emit('event', snapshotEvent);
|
|
188
268
|
}
|
|
189
269
|
const doneEvent = {
|
|
190
270
|
delegationId,
|
|
@@ -192,36 +272,25 @@ export class ExecutorService {
|
|
|
192
272
|
timestamp: new Date().toISOString(),
|
|
193
273
|
summary: result.summary,
|
|
194
274
|
highlights: result.highlights,
|
|
195
|
-
snapshotIds:
|
|
196
|
-
recommendedSnapshotId:
|
|
275
|
+
snapshotIds: snapshotResult?.snapshotBase64 ? [snapshotId] : undefined,
|
|
276
|
+
recommendedSnapshotId: snapshotResult?.snapshotBase64 ? snapshotId : undefined,
|
|
197
277
|
};
|
|
198
|
-
|
|
278
|
+
emitter.emit('event', doneEvent);
|
|
199
279
|
console.log(`[AWCP:Executor] Task ${delegationId} done event emitted` +
|
|
200
|
-
` (listeners=${
|
|
280
|
+
` (listeners=${emitter.listenerCount('event')})`);
|
|
201
281
|
this.config.hooks.onTaskComplete?.(delegationId, result.summary);
|
|
202
|
-
this.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
} : undefined,
|
|
212
|
-
});
|
|
213
|
-
this.scheduleResultCleanup(delegationId);
|
|
214
|
-
console.log(`[AWCP:Executor] Delegation ${delegationId} moved to completed, removing from active`);
|
|
215
|
-
this.activeDelegations.delete(delegationId);
|
|
216
|
-
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(() => { });
|
|
217
291
|
}
|
|
218
292
|
catch (error) {
|
|
219
293
|
console.error(`[AWCP:Executor] Task ${delegationId} failed:`, error instanceof Error ? error.message : error);
|
|
220
|
-
const delegation = this.activeDelegations.get(delegationId);
|
|
221
|
-
if (delegation) {
|
|
222
|
-
await this.transport.teardown({ delegationId, workDir: delegation.workPath }).catch(() => { });
|
|
223
|
-
await this.workspace.release(delegation.workPath);
|
|
224
|
-
}
|
|
225
294
|
const errorEvent = {
|
|
226
295
|
delegationId,
|
|
227
296
|
type: 'error',
|
|
@@ -230,43 +299,30 @@ export class ExecutorService {
|
|
|
230
299
|
message: error instanceof Error ? error.message : String(error),
|
|
231
300
|
hint: 'Check task requirements and try again',
|
|
232
301
|
};
|
|
233
|
-
|
|
302
|
+
emitter.emit('event', errorEvent);
|
|
234
303
|
console.log(`[AWCP:Executor] Task ${delegationId} error event emitted` +
|
|
235
|
-
` (listeners=${
|
|
304
|
+
` (listeners=${emitter.listenerCount('event')})`);
|
|
236
305
|
this.config.hooks.onError?.(delegationId, error instanceof Error ? error : new Error(String(error)));
|
|
237
|
-
this.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
error:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
});
|
|
247
|
-
this.scheduleResultCleanup(delegationId);
|
|
248
|
-
console.log(`[AWCP:Executor] Delegation ${delegationId} moved to error state, removing from active`);
|
|
249
|
-
this.activeDelegations.delete(delegationId);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
async handleError(error) {
|
|
253
|
-
const { delegationId } = error;
|
|
254
|
-
console.log(`[AWCP:Executor] Received ERROR message for ${delegationId}: ${error.code} - ${error.message}`);
|
|
255
|
-
const delegation = this.activeDelegations.get(delegationId);
|
|
256
|
-
if (delegation) {
|
|
257
|
-
await this.transport.teardown({ delegationId, workDir: delegation.workPath }).catch(() => { });
|
|
258
|
-
console.log(`[AWCP:Executor] Delegation ${delegationId} removed by delegator error`);
|
|
259
|
-
this.activeDelegations.delete(delegationId);
|
|
260
|
-
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(() => { });
|
|
261
315
|
}
|
|
262
|
-
this.pendingInvitations.delete(delegationId);
|
|
263
|
-
this.config.hooks.onError?.(delegationId, new AwcpError(error.code, error.message, error.hint, delegationId));
|
|
264
316
|
}
|
|
265
317
|
async cancelDelegation(delegationId) {
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
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) {
|
|
270
326
|
const errorEvent = {
|
|
271
327
|
delegationId,
|
|
272
328
|
type: 'error',
|
|
@@ -274,68 +330,98 @@ export class ExecutorService {
|
|
|
274
330
|
code: ErrorCodes.CANCELLED,
|
|
275
331
|
message: 'Delegation cancelled',
|
|
276
332
|
};
|
|
277
|
-
|
|
278
|
-
console.log(`[AWCP:Executor] Delegation ${delegationId} removed by cancellation`);
|
|
279
|
-
this.activeDelegations.delete(delegationId);
|
|
280
|
-
await this.workspace.release(delegation.workPath);
|
|
281
|
-
this.config.hooks.onError?.(delegationId, new CancelledError('Delegation cancelled by Delegator', undefined, delegationId));
|
|
282
|
-
return;
|
|
333
|
+
emitter.emit('event', errorEvent);
|
|
283
334
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
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));
|
|
289
340
|
}
|
|
290
341
|
getStatus() {
|
|
342
|
+
const active = Array.from(this.assignments.values()).filter(a => a.state === 'active');
|
|
291
343
|
return {
|
|
292
|
-
pendingInvitations: this.
|
|
293
|
-
activeDelegations:
|
|
294
|
-
completedDelegations: this.
|
|
295
|
-
delegations:
|
|
296
|
-
id:
|
|
297
|
-
workPath:
|
|
298
|
-
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,
|
|
299
351
|
})),
|
|
300
352
|
};
|
|
301
353
|
}
|
|
302
354
|
getTaskResult(delegationId) {
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const completed = this.completedDelegations.get(delegationId);
|
|
308
|
-
if (completed) {
|
|
309
|
-
if (completed.state === 'completed') {
|
|
310
|
-
return {
|
|
311
|
-
status: 'completed',
|
|
312
|
-
completedAt: completed.completedAt.toISOString(),
|
|
313
|
-
summary: completed.snapshot?.summary,
|
|
314
|
-
highlights: completed.snapshot?.highlights,
|
|
315
|
-
snapshotBase64: completed.snapshot?.snapshotBase64,
|
|
316
|
-
};
|
|
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' };
|
|
317
359
|
}
|
|
318
|
-
return {
|
|
319
|
-
status: 'error',
|
|
320
|
-
completedAt: completed.completedAt.toISOString(),
|
|
321
|
-
error: completed.error,
|
|
322
|
-
};
|
|
360
|
+
return { status: 'not_found' };
|
|
323
361
|
}
|
|
324
|
-
|
|
362
|
+
const sm = this.stateMachines.get(delegationId);
|
|
363
|
+
if (!sm.isTerminal()) {
|
|
364
|
+
return { status: 'running' };
|
|
365
|
+
}
|
|
366
|
+
if (assignment.state === 'completed') {
|
|
325
367
|
return {
|
|
326
|
-
status: '
|
|
327
|
-
|
|
368
|
+
status: 'completed',
|
|
369
|
+
completedAt: assignment.completedAt,
|
|
370
|
+
summary: assignment.result?.summary,
|
|
371
|
+
highlights: assignment.result?.highlights,
|
|
372
|
+
snapshotBase64: assignment.result?.snapshotBase64,
|
|
328
373
|
};
|
|
329
374
|
}
|
|
330
|
-
return {
|
|
375
|
+
return {
|
|
376
|
+
status: 'error',
|
|
377
|
+
completedAt: assignment.completedAt,
|
|
378
|
+
error: assignment.error,
|
|
379
|
+
};
|
|
331
380
|
}
|
|
332
381
|
acknowledgeResult(delegationId) {
|
|
333
|
-
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();
|
|
334
400
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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);
|
|
339
425
|
}
|
|
340
426
|
createErrorMessage(delegationId, code, message, hint) {
|
|
341
427
|
return {
|