@ct-agents/worker 0.1.5 → 0.1.6
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/package.json +5 -5
- package/src/index.ts +179 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ct-agents/worker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src"
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"zod": "4.4.3",
|
|
15
|
-
"@ct-agents/memory": "0.1.
|
|
16
|
-
"@ct-agents/
|
|
17
|
-
"@ct-agents/
|
|
18
|
-
"@ct-agents/tools": "0.1.
|
|
15
|
+
"@ct-agents/memory": "0.1.6",
|
|
16
|
+
"@ct-agents/prompts": "0.1.6",
|
|
17
|
+
"@ct-agents/protocol": "0.1.6",
|
|
18
|
+
"@ct-agents/tools": "0.1.6"
|
|
19
19
|
},
|
|
20
20
|
"optionalDependencies": {
|
|
21
21
|
"dockerode": "4.0.9"
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,8 @@ import type {
|
|
|
21
21
|
ToolWorkItem,
|
|
22
22
|
WorkQueue,
|
|
23
23
|
WorkStats,
|
|
24
|
+
CompleteWorkItemScope,
|
|
25
|
+
RenewWorkItemLeaseInput,
|
|
24
26
|
} from '@ct-agents/protocol';
|
|
25
27
|
import type { MemoryResource, SkillResource } from '@ct-agents/protocol';
|
|
26
28
|
import { createLoadMemoryToolHandler, createUpdateMemoryToolHandler } from '@ct-agents/memory';
|
|
@@ -93,7 +95,11 @@ export type EnvironmentWorkerInput = {
|
|
|
93
95
|
resourceImpls?: EnvironmentWorkerResourceImplementations;
|
|
94
96
|
platformResourceProxy?: EnvironmentWorkerPlatformResourceProxy;
|
|
95
97
|
abortSignal?: AbortSignal;
|
|
96
|
-
|
|
98
|
+
/** 单进程持续允许的最大 active Tool work 数。 */
|
|
99
|
+
maxConcurrency?: number;
|
|
100
|
+
/** active Tool lease 的续租周期,必须小于 loop 的 reclaimOlderThanMs。 */
|
|
101
|
+
leaseRenewIntervalMs?: number;
|
|
102
|
+
onError?: (error: unknown) => void | Promise<void>;
|
|
97
103
|
harness?: {
|
|
98
104
|
id: string;
|
|
99
105
|
version: number;
|
|
@@ -140,6 +146,7 @@ export type RunContextResolverWorkerLoopInput = ContextResolverWorkerInput & {
|
|
|
140
146
|
export const DEFAULT_RECLAIM_OLDER_THAN_MS = 300_000;
|
|
141
147
|
export const DEFAULT_RECLAIM_INTERVAL_MS = 60_000;
|
|
142
148
|
export const DEFAULT_CLAIM_BLOCK_MS = 15_000;
|
|
149
|
+
export const DEFAULT_WORK_LEASE_RENEW_INTERVAL_MS = 60_000;
|
|
143
150
|
|
|
144
151
|
const PLATFORM_PROXY_RESOURCE_SLOTS = new Set(['database', 'skills', 'memory']);
|
|
145
152
|
|
|
@@ -166,6 +173,13 @@ function normalizeNonNegativeInteger(value: number | undefined, fallback: number
|
|
|
166
173
|
return Math.max(0, Math.trunc(value));
|
|
167
174
|
}
|
|
168
175
|
|
|
176
|
+
function normalizePositiveInteger(value: number | undefined, fallback: number): number {
|
|
177
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
178
|
+
return fallback;
|
|
179
|
+
}
|
|
180
|
+
return Math.max(1, Math.trunc(value));
|
|
181
|
+
}
|
|
182
|
+
|
|
169
183
|
class PlatformBuiltinToolRegistry implements ToolRegistry {
|
|
170
184
|
private readonly builtinHandlers = new Map<string, ToolHandler<unknown, unknown>>();
|
|
171
185
|
|
|
@@ -217,45 +231,97 @@ export class EnvironmentWorker {
|
|
|
217
231
|
}
|
|
218
232
|
|
|
219
233
|
public async drainOnce(options: DrainOnceOptions = {}): Promise<number> {
|
|
234
|
+
const items = await this.claim(
|
|
235
|
+
normalizePositiveInteger(this.input.maxConcurrency, 1),
|
|
236
|
+
options,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
await Promise.all(items.map((item) => this.handleItem(item)));
|
|
240
|
+
return items.length;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public async claim(max: number, options: DrainOnceOptions = {}): Promise<ToolWorkItem[]> {
|
|
220
244
|
const executionMode = this.input.executionMode ?? 'self_hosted';
|
|
221
|
-
|
|
245
|
+
return this.input.workQueue.claim({
|
|
222
246
|
appId: this.input.appId,
|
|
223
247
|
environmentId: this.input.environmentId,
|
|
224
248
|
executionMode,
|
|
225
|
-
max:
|
|
249
|
+
max: normalizePositiveInteger(max, 1),
|
|
226
250
|
blockMs: options.blockMs,
|
|
227
251
|
});
|
|
228
|
-
|
|
229
|
-
await Promise.all(items.map((item) => this.handleItem(item)));
|
|
230
|
-
return items.length;
|
|
231
252
|
}
|
|
232
253
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
254
|
+
public async handleItem(item: ToolWorkItem): Promise<void> {
|
|
255
|
+
if (!item.leaseId) {
|
|
256
|
+
throw new Error(`claimed work item 缺少 leaseId:${item.workId}`);
|
|
257
|
+
}
|
|
258
|
+
const renewController = new AbortController();
|
|
259
|
+
const renewal = this.renewLeaseUntilStopped(item, renewController.signal);
|
|
236
260
|
try {
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
261
|
+
let result: ToolExecutionResult;
|
|
262
|
+
const disposers: Array<() => Promise<void>> = [];
|
|
263
|
+
try {
|
|
264
|
+
const handler = this.input.toolRegistry.getTool(item.toolName);
|
|
265
|
+
const execution = await this.executeHandler(handler, item, disposers);
|
|
266
|
+
result = toToolExecutionResult(item.workId, handler, execution);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
const accepted = await this.input.workQueue.complete({
|
|
269
|
+
workId: item.workId,
|
|
270
|
+
status: 'failed',
|
|
271
|
+
error: toWorkerErrorPayload(error),
|
|
272
|
+
}, this.buildCompleteScope(item));
|
|
273
|
+
if (!accepted) {
|
|
274
|
+
throw new Error(`work complete 未被接受:${item.workId}`);
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
} finally {
|
|
278
|
+
await Promise.all(disposers.map(async (dispose) => {
|
|
279
|
+
await dispose().catch(() => undefined);
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const accepted = await this.input.workQueue.complete(result, this.buildCompleteScope(item));
|
|
246
284
|
if (!accepted) {
|
|
247
285
|
throw new Error(`work complete 未被接受:${item.workId}`);
|
|
248
286
|
}
|
|
249
|
-
return;
|
|
250
287
|
} finally {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}));
|
|
288
|
+
renewController.abort();
|
|
289
|
+
await renewal;
|
|
254
290
|
}
|
|
291
|
+
}
|
|
255
292
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
293
|
+
private async renewLeaseUntilStopped(item: ToolWorkItem, signal: AbortSignal): Promise<void> {
|
|
294
|
+
const leaseId = item.leaseId;
|
|
295
|
+
if (!leaseId) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const intervalMs = normalizePositiveInteger(
|
|
299
|
+
this.input.leaseRenewIntervalMs,
|
|
300
|
+
DEFAULT_WORK_LEASE_RENEW_INTERVAL_MS,
|
|
301
|
+
);
|
|
302
|
+
while (!signal.aborted) {
|
|
303
|
+
await delay(intervalMs, signal);
|
|
304
|
+
if (signal.aborted) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
const renewed = await this.input.workQueue.renew({
|
|
309
|
+
workId: item.workId,
|
|
310
|
+
leaseId,
|
|
311
|
+
appId: this.input.appId,
|
|
312
|
+
environmentId: this.input.environmentId,
|
|
313
|
+
executionMode: this.input.executionMode ?? item.executionMode ?? 'self_hosted',
|
|
314
|
+
});
|
|
315
|
+
if (!renewed) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
try {
|
|
320
|
+
await this.input.onError?.(error);
|
|
321
|
+
} catch {
|
|
322
|
+
// 观测回调失败不能终止仍持有 lease 的业务 handler;下一周期继续 renew。
|
|
323
|
+
}
|
|
324
|
+
}
|
|
259
325
|
}
|
|
260
326
|
}
|
|
261
327
|
|
|
@@ -398,11 +464,15 @@ export class EnvironmentWorker {
|
|
|
398
464
|
}
|
|
399
465
|
|
|
400
466
|
private buildCompleteScope(item: ToolWorkItem) {
|
|
467
|
+
if (!item.leaseId) {
|
|
468
|
+
throw new Error(`claimed work item 缺少 leaseId:${item.workId}`);
|
|
469
|
+
}
|
|
401
470
|
return {
|
|
402
471
|
appId: this.input.appId,
|
|
403
472
|
environmentId: this.input.environmentId,
|
|
404
473
|
executionMode: this.input.executionMode ?? item.executionMode ?? 'self_hosted',
|
|
405
|
-
|
|
474
|
+
leaseId: item.leaseId,
|
|
475
|
+
} satisfies CompleteWorkItemScope;
|
|
406
476
|
}
|
|
407
477
|
}
|
|
408
478
|
|
|
@@ -503,7 +573,28 @@ export async function runEnvironmentWorkerLoop(input: RunEnvironmentWorkerLoopIn
|
|
|
503
573
|
const reclaimOlderThanMs = input.reclaimOlderThanMs ?? DEFAULT_RECLAIM_OLDER_THAN_MS;
|
|
504
574
|
const reclaimIntervalMs = normalizeNonNegativeInteger(input.reclaimIntervalMs, DEFAULT_RECLAIM_INTERVAL_MS);
|
|
505
575
|
const blockMs = normalizeNonNegativeInteger(input.blockMs, DEFAULT_CLAIM_BLOCK_MS);
|
|
576
|
+
const maxConcurrency = normalizePositiveInteger(input.maxConcurrency, 1);
|
|
577
|
+
const leaseRenewIntervalMs = normalizePositiveInteger(
|
|
578
|
+
input.leaseRenewIntervalMs,
|
|
579
|
+
DEFAULT_WORK_LEASE_RENEW_INTERVAL_MS,
|
|
580
|
+
);
|
|
581
|
+
if (leaseRenewIntervalMs >= reclaimOlderThanMs) {
|
|
582
|
+
throw new Error('leaseRenewIntervalMs 必须小于 reclaimOlderThanMs');
|
|
583
|
+
}
|
|
506
584
|
let nextReclaimAtMs = 0;
|
|
585
|
+
const active = new Set<Promise<void>>();
|
|
586
|
+
|
|
587
|
+
const startWork = (item: ToolWorkItem) => {
|
|
588
|
+
let tracked: Promise<void>;
|
|
589
|
+
tracked = worker.handleItem(item)
|
|
590
|
+
.catch(async (error) => {
|
|
591
|
+
await input.onError?.(error);
|
|
592
|
+
})
|
|
593
|
+
.finally(() => {
|
|
594
|
+
active.delete(tracked);
|
|
595
|
+
});
|
|
596
|
+
active.add(tracked);
|
|
597
|
+
};
|
|
507
598
|
|
|
508
599
|
try {
|
|
509
600
|
while (!input.abortSignal?.aborted) {
|
|
@@ -522,9 +613,22 @@ export async function runEnvironmentWorkerLoop(input: RunEnvironmentWorkerLoopIn
|
|
|
522
613
|
await input.sandboxManager.sweepIdle(input.sandboxIdleTtlMs);
|
|
523
614
|
}
|
|
524
615
|
|
|
525
|
-
const
|
|
526
|
-
if (
|
|
527
|
-
await
|
|
616
|
+
const availableSlots = maxConcurrency - active.size;
|
|
617
|
+
if (availableSlots <= 0) {
|
|
618
|
+
await waitForActiveWorkOrDeadline(active, nextReclaimAtMs, input.abortSignal);
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const items = await worker.claim(availableSlots, { blockMs });
|
|
623
|
+
for (const item of items) {
|
|
624
|
+
startWork(item);
|
|
625
|
+
}
|
|
626
|
+
if (items.length === 0) {
|
|
627
|
+
if (active.size > 0) {
|
|
628
|
+
await waitForActiveWorkOrDelay(active, idleDelayMs, input.abortSignal);
|
|
629
|
+
} else {
|
|
630
|
+
await delay(idleDelayMs, input.abortSignal);
|
|
631
|
+
}
|
|
528
632
|
}
|
|
529
633
|
} catch (error) {
|
|
530
634
|
await input.onError?.(error);
|
|
@@ -532,6 +636,7 @@ export async function runEnvironmentWorkerLoop(input: RunEnvironmentWorkerLoopIn
|
|
|
532
636
|
}
|
|
533
637
|
}
|
|
534
638
|
} finally {
|
|
639
|
+
await Promise.allSettled(active);
|
|
535
640
|
await input.sandboxManager?.disposeAll();
|
|
536
641
|
}
|
|
537
642
|
}
|
|
@@ -626,10 +731,25 @@ export class HttpWorkQueueClient implements WorkQueue {
|
|
|
626
731
|
return items.map(parseToolWorkItem);
|
|
627
732
|
}
|
|
628
733
|
|
|
629
|
-
public async
|
|
734
|
+
public async renew(input: RenewWorkItemLeaseInput): Promise<boolean> {
|
|
735
|
+
this.assertScope(input.appId, input.environmentId);
|
|
736
|
+
const data = await this.requestJson('/work/renew', {
|
|
737
|
+
method: 'POST',
|
|
738
|
+
signal: this.input.signal,
|
|
739
|
+
body: JSON.stringify({ workId: input.workId, leaseId: input.leaseId }),
|
|
740
|
+
});
|
|
741
|
+
const record = expectRecord(data, 'renew 响应缺少 data');
|
|
742
|
+
return record.renewed === true;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
public async complete(result: ToolExecutionResult, scope?: CompleteWorkItemScope): Promise<boolean> {
|
|
746
|
+
if (!scope?.leaseId) {
|
|
747
|
+
throw new Error(`work complete 缺少 leaseId:${result.workId}`);
|
|
748
|
+
}
|
|
749
|
+
this.assertScope(scope.appId, scope.environmentId);
|
|
630
750
|
const data = await this.requestJson('/work/complete', {
|
|
631
751
|
method: 'POST',
|
|
632
|
-
body: JSON.stringify(result),
|
|
752
|
+
body: JSON.stringify({ ...result, leaseId: scope.leaseId }),
|
|
633
753
|
});
|
|
634
754
|
const record = expectRecord(data, 'complete 响应缺少 data');
|
|
635
755
|
if (record.accepted !== true) {
|
|
@@ -1101,10 +1221,35 @@ function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
1101
1221
|
}
|
|
1102
1222
|
|
|
1103
1223
|
return new Promise((resolve) => {
|
|
1104
|
-
const
|
|
1105
|
-
signal?.addEventListener('abort', () => {
|
|
1224
|
+
const finish = () => {
|
|
1106
1225
|
clearTimeout(timer);
|
|
1226
|
+
signal?.removeEventListener('abort', finish);
|
|
1107
1227
|
resolve();
|
|
1108
|
-
}
|
|
1228
|
+
};
|
|
1229
|
+
const timer = setTimeout(finish, ms);
|
|
1230
|
+
signal?.addEventListener('abort', finish, { once: true });
|
|
1109
1231
|
});
|
|
1110
1232
|
}
|
|
1233
|
+
|
|
1234
|
+
function waitForActiveWorkOrDeadline(
|
|
1235
|
+
active: Set<Promise<void>>,
|
|
1236
|
+
deadlineMs: number,
|
|
1237
|
+
signal?: AbortSignal,
|
|
1238
|
+
): Promise<void> {
|
|
1239
|
+
const waitMs = Math.max(1, deadlineMs - Date.now());
|
|
1240
|
+
return Promise.race([
|
|
1241
|
+
...active,
|
|
1242
|
+
delay(waitMs, signal),
|
|
1243
|
+
]).then(() => undefined);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function waitForActiveWorkOrDelay(
|
|
1247
|
+
active: Set<Promise<void>>,
|
|
1248
|
+
delayMs: number,
|
|
1249
|
+
signal?: AbortSignal,
|
|
1250
|
+
): Promise<void> {
|
|
1251
|
+
return Promise.race([
|
|
1252
|
+
...active,
|
|
1253
|
+
delay(delayMs, signal),
|
|
1254
|
+
]).then(() => undefined);
|
|
1255
|
+
}
|