@ct-agents/worker 0.1.4 → 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 +185 -35
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/prompts": "0.1.
|
|
17
|
-
"@ct-agents/protocol": "0.1.
|
|
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,12 +21,18 @@ 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';
|
|
27
29
|
import { createLoadSkillToolHandler } from '@ct-agents/prompts';
|
|
28
30
|
import { createSandboxTools } from '@ct-agents/tools';
|
|
29
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
createAskUserToolHandler,
|
|
33
|
+
createTodoWriteToolHandler,
|
|
34
|
+
createWebSearchToolHandler,
|
|
35
|
+
} from '@ct-agents/tools/builtin-tools';
|
|
30
36
|
import { z } from 'zod';
|
|
31
37
|
import {
|
|
32
38
|
createPlatformDatabaseProxyResource,
|
|
@@ -89,7 +95,11 @@ export type EnvironmentWorkerInput = {
|
|
|
89
95
|
resourceImpls?: EnvironmentWorkerResourceImplementations;
|
|
90
96
|
platformResourceProxy?: EnvironmentWorkerPlatformResourceProxy;
|
|
91
97
|
abortSignal?: AbortSignal;
|
|
92
|
-
|
|
98
|
+
/** 单进程持续允许的最大 active Tool work 数。 */
|
|
99
|
+
maxConcurrency?: number;
|
|
100
|
+
/** active Tool lease 的续租周期,必须小于 loop 的 reclaimOlderThanMs。 */
|
|
101
|
+
leaseRenewIntervalMs?: number;
|
|
102
|
+
onError?: (error: unknown) => void | Promise<void>;
|
|
93
103
|
harness?: {
|
|
94
104
|
id: string;
|
|
95
105
|
version: number;
|
|
@@ -136,6 +146,7 @@ export type RunContextResolverWorkerLoopInput = ContextResolverWorkerInput & {
|
|
|
136
146
|
export const DEFAULT_RECLAIM_OLDER_THAN_MS = 300_000;
|
|
137
147
|
export const DEFAULT_RECLAIM_INTERVAL_MS = 60_000;
|
|
138
148
|
export const DEFAULT_CLAIM_BLOCK_MS = 15_000;
|
|
149
|
+
export const DEFAULT_WORK_LEASE_RENEW_INTERVAL_MS = 60_000;
|
|
139
150
|
|
|
140
151
|
const PLATFORM_PROXY_RESOURCE_SLOTS = new Set(['database', 'skills', 'memory']);
|
|
141
152
|
|
|
@@ -146,6 +157,7 @@ export function createPlatformBuiltinToolHandlers(): ToolHandler[] {
|
|
|
146
157
|
createLoadMemoryToolHandler(),
|
|
147
158
|
createUpdateMemoryToolHandler(),
|
|
148
159
|
createWebSearchToolHandler(),
|
|
160
|
+
createTodoWriteToolHandler(),
|
|
149
161
|
...createSandboxTools(),
|
|
150
162
|
];
|
|
151
163
|
}
|
|
@@ -161,6 +173,13 @@ function normalizeNonNegativeInteger(value: number | undefined, fallback: number
|
|
|
161
173
|
return Math.max(0, Math.trunc(value));
|
|
162
174
|
}
|
|
163
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
|
+
|
|
164
183
|
class PlatformBuiltinToolRegistry implements ToolRegistry {
|
|
165
184
|
private readonly builtinHandlers = new Map<string, ToolHandler<unknown, unknown>>();
|
|
166
185
|
|
|
@@ -212,45 +231,97 @@ export class EnvironmentWorker {
|
|
|
212
231
|
}
|
|
213
232
|
|
|
214
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[]> {
|
|
215
244
|
const executionMode = this.input.executionMode ?? 'self_hosted';
|
|
216
|
-
|
|
245
|
+
return this.input.workQueue.claim({
|
|
217
246
|
appId: this.input.appId,
|
|
218
247
|
environmentId: this.input.environmentId,
|
|
219
248
|
executionMode,
|
|
220
|
-
max:
|
|
249
|
+
max: normalizePositiveInteger(max, 1),
|
|
221
250
|
blockMs: options.blockMs,
|
|
222
251
|
});
|
|
223
|
-
|
|
224
|
-
await Promise.all(items.map((item) => this.handleItem(item)));
|
|
225
|
-
return items.length;
|
|
226
252
|
}
|
|
227
253
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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);
|
|
231
260
|
try {
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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));
|
|
241
284
|
if (!accepted) {
|
|
242
285
|
throw new Error(`work complete 未被接受:${item.workId}`);
|
|
243
286
|
}
|
|
244
|
-
return;
|
|
245
287
|
} finally {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}));
|
|
288
|
+
renewController.abort();
|
|
289
|
+
await renewal;
|
|
249
290
|
}
|
|
291
|
+
}
|
|
250
292
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
+
}
|
|
254
325
|
}
|
|
255
326
|
}
|
|
256
327
|
|
|
@@ -393,11 +464,15 @@ export class EnvironmentWorker {
|
|
|
393
464
|
}
|
|
394
465
|
|
|
395
466
|
private buildCompleteScope(item: ToolWorkItem) {
|
|
467
|
+
if (!item.leaseId) {
|
|
468
|
+
throw new Error(`claimed work item 缺少 leaseId:${item.workId}`);
|
|
469
|
+
}
|
|
396
470
|
return {
|
|
397
471
|
appId: this.input.appId,
|
|
398
472
|
environmentId: this.input.environmentId,
|
|
399
473
|
executionMode: this.input.executionMode ?? item.executionMode ?? 'self_hosted',
|
|
400
|
-
|
|
474
|
+
leaseId: item.leaseId,
|
|
475
|
+
} satisfies CompleteWorkItemScope;
|
|
401
476
|
}
|
|
402
477
|
}
|
|
403
478
|
|
|
@@ -498,7 +573,28 @@ export async function runEnvironmentWorkerLoop(input: RunEnvironmentWorkerLoopIn
|
|
|
498
573
|
const reclaimOlderThanMs = input.reclaimOlderThanMs ?? DEFAULT_RECLAIM_OLDER_THAN_MS;
|
|
499
574
|
const reclaimIntervalMs = normalizeNonNegativeInteger(input.reclaimIntervalMs, DEFAULT_RECLAIM_INTERVAL_MS);
|
|
500
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
|
+
}
|
|
501
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
|
+
};
|
|
502
598
|
|
|
503
599
|
try {
|
|
504
600
|
while (!input.abortSignal?.aborted) {
|
|
@@ -517,9 +613,22 @@ export async function runEnvironmentWorkerLoop(input: RunEnvironmentWorkerLoopIn
|
|
|
517
613
|
await input.sandboxManager.sweepIdle(input.sandboxIdleTtlMs);
|
|
518
614
|
}
|
|
519
615
|
|
|
520
|
-
const
|
|
521
|
-
if (
|
|
522
|
-
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
|
+
}
|
|
523
632
|
}
|
|
524
633
|
} catch (error) {
|
|
525
634
|
await input.onError?.(error);
|
|
@@ -527,6 +636,7 @@ export async function runEnvironmentWorkerLoop(input: RunEnvironmentWorkerLoopIn
|
|
|
527
636
|
}
|
|
528
637
|
}
|
|
529
638
|
} finally {
|
|
639
|
+
await Promise.allSettled(active);
|
|
530
640
|
await input.sandboxManager?.disposeAll();
|
|
531
641
|
}
|
|
532
642
|
}
|
|
@@ -621,10 +731,25 @@ export class HttpWorkQueueClient implements WorkQueue {
|
|
|
621
731
|
return items.map(parseToolWorkItem);
|
|
622
732
|
}
|
|
623
733
|
|
|
624
|
-
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);
|
|
625
750
|
const data = await this.requestJson('/work/complete', {
|
|
626
751
|
method: 'POST',
|
|
627
|
-
body: JSON.stringify(result),
|
|
752
|
+
body: JSON.stringify({ ...result, leaseId: scope.leaseId }),
|
|
628
753
|
});
|
|
629
754
|
const record = expectRecord(data, 'complete 响应缺少 data');
|
|
630
755
|
if (record.accepted !== true) {
|
|
@@ -1096,10 +1221,35 @@ function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
1096
1221
|
}
|
|
1097
1222
|
|
|
1098
1223
|
return new Promise((resolve) => {
|
|
1099
|
-
const
|
|
1100
|
-
signal?.addEventListener('abort', () => {
|
|
1224
|
+
const finish = () => {
|
|
1101
1225
|
clearTimeout(timer);
|
|
1226
|
+
signal?.removeEventListener('abort', finish);
|
|
1102
1227
|
resolve();
|
|
1103
|
-
}
|
|
1228
|
+
};
|
|
1229
|
+
const timer = setTimeout(finish, ms);
|
|
1230
|
+
signal?.addEventListener('abort', finish, { once: true });
|
|
1104
1231
|
});
|
|
1105
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
|
+
}
|