@ct-agents/worker 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -6
- package/src/index.ts +388 -377
- package/src/leased-item-runner.ts +85 -0
- package/src/platform-api-path.ts +5 -0
- package/src/resources/database/postgres.ts +36 -37
- package/src/resources/platform-proxy.ts +6 -33
- package/src/resources/text-generation/index.ts +25 -2
- package/src/resources/web-search/tavily.ts +1 -1
- package/src/sandbox/docker.ts +7 -78
- package/src/sandbox/index.ts +1 -0
- package/src/sandbox/local-process.ts +1 -6
- package/src/sandbox/manager.ts +1 -7
- package/src/sandbox/resources-def.ts +1 -20
- package/src/sandbox/resources.ts +1 -13
- package/src/sandbox/types.ts +124 -0
- package/src/worker-http-transport.ts +79 -0
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ContextResolveQueue,
|
|
3
|
+
ContextResolveQueueClaimInput,
|
|
3
4
|
ContextResolveResult,
|
|
4
5
|
ContextResolveWorkItem,
|
|
5
|
-
Database,
|
|
6
6
|
EnvironmentId,
|
|
7
7
|
EnvironmentExecutionMode,
|
|
8
8
|
EnvironmentHostedPolicy,
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
HarnessId,
|
|
11
11
|
ResourceSlots,
|
|
12
12
|
SessionEvent,
|
|
13
|
+
SessionEventActor,
|
|
13
14
|
SessionId,
|
|
14
15
|
SessionRecord,
|
|
15
16
|
ToolExecution,
|
|
@@ -19,12 +20,15 @@ import type {
|
|
|
19
20
|
ToolHandlerContext,
|
|
20
21
|
ToolRegistry,
|
|
21
22
|
ToolWorkItem,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
WorkerCapabilityRegistration,
|
|
24
|
+
WorkerCompleteWorkItemScope,
|
|
25
|
+
WorkerRenewWorkItemLeaseInput,
|
|
26
|
+
WorkerWorkQueue,
|
|
26
27
|
} from '@ct-agents/protocol';
|
|
28
|
+
import type { ToolExecutionContract } from '@ct-agents/protocol';
|
|
29
|
+
import { digestToolExecutionContract, normalizeToolExecutionContract } from '@ct-agents/environment';
|
|
27
30
|
import type { MemoryResource, SkillResource } from '@ct-agents/protocol';
|
|
31
|
+
import { assertToolResultWireValue } from '@ct-agents/protocol';
|
|
28
32
|
import { createLoadMemoryToolHandler, createUpdateMemoryToolHandler } from '@ct-agents/memory';
|
|
29
33
|
import { createLoadSkillToolHandler } from '@ct-agents/prompts';
|
|
30
34
|
import { createSandboxTools } from '@ct-agents/tools';
|
|
@@ -34,15 +38,22 @@ import {
|
|
|
34
38
|
createWebSearchToolHandler,
|
|
35
39
|
} from '@ct-agents/tools/builtin-tools';
|
|
36
40
|
import { z } from 'zod';
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
import { createPlatformResourceProxy } from './resources/platform-proxy.js';
|
|
42
|
+
import { WorkerHttpTransport } from './worker-http-transport.js';
|
|
43
|
+
import { runLeasedItem } from './leased-item-runner.js';
|
|
44
|
+
|
|
45
|
+
export { WorkerHttpError } from './worker-http-transport.js';
|
|
46
|
+
export { DEFAULT_WORK_LEASE_RENEW_INTERVAL_MS } from './leased-item-runner.js';
|
|
41
47
|
|
|
42
48
|
export type ResolveResources = (item: ToolWorkItem) => Promise<ResourceSlots> | ResourceSlots;
|
|
43
49
|
export type ResolvePromptContext = (item: ContextResolveWorkItem) =>
|
|
44
50
|
Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
45
51
|
|
|
52
|
+
export type WorkerContextResolveQueue = Pick<
|
|
53
|
+
ContextResolveQueue,
|
|
54
|
+
'claim' | 'renew' | 'complete'
|
|
55
|
+
>;
|
|
56
|
+
|
|
46
57
|
export type ResourceOptionsParser = {
|
|
47
58
|
parse(value: unknown): Record<string, unknown>;
|
|
48
59
|
};
|
|
@@ -86,9 +97,7 @@ export type WorkerSandboxManager = {
|
|
|
86
97
|
};
|
|
87
98
|
|
|
88
99
|
export type EnvironmentWorkerInput = {
|
|
89
|
-
|
|
90
|
-
environmentId: string;
|
|
91
|
-
workQueue: WorkQueue;
|
|
100
|
+
workQueue: WorkerWorkQueue;
|
|
92
101
|
toolRegistry: ToolRegistry;
|
|
93
102
|
resources?: ResourceSlots;
|
|
94
103
|
resolveResources?: ResolveResources;
|
|
@@ -97,7 +106,7 @@ export type EnvironmentWorkerInput = {
|
|
|
97
106
|
abortSignal?: AbortSignal;
|
|
98
107
|
/** 单进程持续允许的最大 active Tool work 数。 */
|
|
99
108
|
maxConcurrency?: number;
|
|
100
|
-
/** active Tool lease
|
|
109
|
+
/** active Tool lease 的续租周期。 */
|
|
101
110
|
leaseRenewIntervalMs?: number;
|
|
102
111
|
onError?: (error: unknown) => void | Promise<void>;
|
|
103
112
|
harness?: {
|
|
@@ -118,37 +127,30 @@ export type RunEnvironmentWorkerLoopInput = EnvironmentWorkerInput & {
|
|
|
118
127
|
blockMs?: number;
|
|
119
128
|
idleDelayMs?: number;
|
|
120
129
|
errorDelayMs?: number;
|
|
121
|
-
reclaimOlderThanMs?: number;
|
|
122
|
-
reclaimIntervalMs?: number;
|
|
123
130
|
sandboxManager?: WorkerSandboxManager;
|
|
124
131
|
sandboxIdleTtlMs?: number;
|
|
125
132
|
onError?: (error: unknown) => void | Promise<void>;
|
|
126
133
|
};
|
|
127
134
|
|
|
128
135
|
export type ContextResolverWorkerInput = {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
contextResolveQueue:
|
|
136
|
+
/** 本地/内存队列可选的不透明路由句柄;HTTP client 已由 Environment Key 绑定。 */
|
|
137
|
+
scope?: ExecutionScope;
|
|
138
|
+
contextResolveQueue: WorkerContextResolveQueue;
|
|
132
139
|
resolvePromptContext: ResolvePromptContext;
|
|
133
140
|
maxBatchSize?: number;
|
|
134
141
|
executionMode?: EnvironmentExecutionMode;
|
|
142
|
+
leaseRenewIntervalMs?: number;
|
|
143
|
+
onError?: (error: unknown) => void | Promise<void>;
|
|
135
144
|
};
|
|
136
145
|
|
|
137
146
|
export type RunContextResolverWorkerLoopInput = ContextResolverWorkerInput & {
|
|
138
147
|
abortSignal?: AbortSignal;
|
|
139
148
|
idleDelayMs?: number;
|
|
140
149
|
errorDelayMs?: number;
|
|
141
|
-
reclaimOlderThanMs?: number;
|
|
142
|
-
reclaimIntervalMs?: number;
|
|
143
|
-
onError?: (error: unknown) => void | Promise<void>;
|
|
144
150
|
};
|
|
145
151
|
|
|
146
|
-
export const DEFAULT_RECLAIM_OLDER_THAN_MS = 300_000;
|
|
147
|
-
export const DEFAULT_RECLAIM_INTERVAL_MS = 60_000;
|
|
148
152
|
export const DEFAULT_CLAIM_BLOCK_MS = 15_000;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const PLATFORM_PROXY_RESOURCE_SLOTS = new Set(['database', 'skills', 'memory']);
|
|
153
|
+
const PLATFORM_PROXY_RESOURCE_SLOTS = new Set(['skills', 'memory']);
|
|
152
154
|
|
|
153
155
|
export function createPlatformBuiltinToolHandlers(): ToolHandler[] {
|
|
154
156
|
return [
|
|
@@ -166,6 +168,40 @@ function withPlatformBuiltinTools(registry: ToolRegistry): ToolRegistry {
|
|
|
166
168
|
return new PlatformBuiltinToolRegistry(registry, createPlatformBuiltinToolHandlers());
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
export function createWorkerCapabilityRegistration(input: {
|
|
172
|
+
workerInstanceId: string;
|
|
173
|
+
implementationReleaseId: string;
|
|
174
|
+
toolRegistry: ToolRegistry;
|
|
175
|
+
resourceImpls: EnvironmentWorkerResourceImplementations;
|
|
176
|
+
}): WorkerCapabilityRegistration {
|
|
177
|
+
const toolRegistry = withPlatformBuiltinTools(input.toolRegistry);
|
|
178
|
+
const toolCapabilities = toolRegistry.listTools().map((descriptor) => {
|
|
179
|
+
const contract = normalizeToolExecutionContract({
|
|
180
|
+
contractFormatVersion: 1,
|
|
181
|
+
toolName: descriptor.name,
|
|
182
|
+
inputSchema: descriptor.inputSchema,
|
|
183
|
+
requiredResources: descriptor.requiredResources ?? [],
|
|
184
|
+
annotations: descriptor.annotations,
|
|
185
|
+
compatibilityGeneration: descriptor.compatibilityGeneration ?? 1,
|
|
186
|
+
} satisfies ToolExecutionContract);
|
|
187
|
+
return {
|
|
188
|
+
contractDigest: digestToolExecutionContract(contract),
|
|
189
|
+
contract,
|
|
190
|
+
toolName: descriptor.name,
|
|
191
|
+
implementationId: `${input.implementationReleaseId}:${descriptor.name}`,
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
const resourceCapabilities = Object.entries(input.resourceImpls).flatMap(([slot, implementations]) => (
|
|
195
|
+
Object.keys(implementations).map((implementationId) => ({ slot, implementationId }))
|
|
196
|
+
));
|
|
197
|
+
return {
|
|
198
|
+
workerInstanceId: input.workerInstanceId,
|
|
199
|
+
implementationReleaseId: input.implementationReleaseId,
|
|
200
|
+
toolCapabilities,
|
|
201
|
+
resourceCapabilities,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
169
205
|
function normalizeNonNegativeInteger(value: number | undefined, fallback: number): number {
|
|
170
206
|
if (value === undefined || !Number.isFinite(value)) {
|
|
171
207
|
return fallback;
|
|
@@ -214,10 +250,14 @@ class PlatformBuiltinToolRegistry implements ToolRegistry {
|
|
|
214
250
|
}
|
|
215
251
|
}
|
|
216
252
|
|
|
253
|
+
type EnvironmentWorkExecution =
|
|
254
|
+
| { kind: 'completed'; result: ToolExecutionResult }
|
|
255
|
+
| { kind: 'failed'; result: ToolExecutionResult; cause: unknown };
|
|
256
|
+
|
|
217
257
|
/**
|
|
218
258
|
* 独立执行面 worker。
|
|
219
259
|
*
|
|
220
|
-
* worker 只通过
|
|
260
|
+
* worker 只通过 Environment Key 绑定的队列认领业务 Tool Work,并在本地
|
|
221
261
|
* ToolRegistry/ResourceSlots 中执行 handler。它不持消费端 key,也不直接创建或读取 session。
|
|
222
262
|
*/
|
|
223
263
|
export class EnvironmentWorker {
|
|
@@ -241,11 +281,7 @@ export class EnvironmentWorker {
|
|
|
241
281
|
}
|
|
242
282
|
|
|
243
283
|
public async claim(max: number, options: DrainOnceOptions = {}): Promise<ToolWorkItem[]> {
|
|
244
|
-
const executionMode = this.input.executionMode ?? 'self_hosted';
|
|
245
284
|
return this.input.workQueue.claim({
|
|
246
|
-
appId: this.input.appId,
|
|
247
|
-
environmentId: this.input.environmentId,
|
|
248
|
-
executionMode,
|
|
249
285
|
max: normalizePositiveInteger(max, 1),
|
|
250
286
|
blockMs: options.blockMs,
|
|
251
287
|
});
|
|
@@ -255,74 +291,46 @@ export class EnvironmentWorker {
|
|
|
255
291
|
if (!item.leaseId) {
|
|
256
292
|
throw new Error(`claimed work item 缺少 leaseId:${item.workId}`);
|
|
257
293
|
}
|
|
258
|
-
const renewController = new AbortController();
|
|
259
|
-
const renewal = this.renewLeaseUntilStopped(item, renewController.signal);
|
|
260
|
-
try {
|
|
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));
|
|
284
|
-
if (!accepted) {
|
|
285
|
-
throw new Error(`work complete 未被接受:${item.workId}`);
|
|
286
|
-
}
|
|
287
|
-
} finally {
|
|
288
|
-
renewController.abort();
|
|
289
|
-
await renewal;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
private async renewLeaseUntilStopped(item: ToolWorkItem, signal: AbortSignal): Promise<void> {
|
|
294
294
|
const leaseId = item.leaseId;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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({
|
|
295
|
+
await runLeasedItem<EnvironmentWorkExecution>({
|
|
296
|
+
renewal: {
|
|
297
|
+
intervalMs: this.input.leaseRenewIntervalMs,
|
|
298
|
+
renew: () => this.input.workQueue.renew({
|
|
309
299
|
workId: item.workId,
|
|
310
300
|
leaseId,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
} catch (error) {
|
|
301
|
+
}),
|
|
302
|
+
onError: this.input.onError,
|
|
303
|
+
},
|
|
304
|
+
execute: async () => {
|
|
305
|
+
const disposers: Array<() => Promise<void>> = [];
|
|
319
306
|
try {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
307
|
+
const handler = this.input.toolRegistry.getTool(item.toolName);
|
|
308
|
+
const execution = await this.executeHandler(handler, item, disposers);
|
|
309
|
+
return {
|
|
310
|
+
kind: 'completed',
|
|
311
|
+
result: toToolExecutionResult(item.workId, execution),
|
|
312
|
+
};
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return {
|
|
315
|
+
kind: 'failed',
|
|
316
|
+
result: {
|
|
317
|
+
workId: item.workId,
|
|
318
|
+
status: 'failed',
|
|
319
|
+
error: toWorkerErrorPayload(error),
|
|
320
|
+
},
|
|
321
|
+
cause: error,
|
|
322
|
+
};
|
|
323
|
+
} finally {
|
|
324
|
+
await Promise.all(disposers.map(async (dispose) => {
|
|
325
|
+
await dispose().catch(() => undefined);
|
|
326
|
+
}));
|
|
323
327
|
}
|
|
324
|
-
}
|
|
325
|
-
|
|
328
|
+
},
|
|
329
|
+
complete: ({ result }) => this.input.workQueue.complete(result, { leaseId }),
|
|
330
|
+
completeRejectedError: (execution) => execution.kind === 'failed'
|
|
331
|
+
? new Error(`work complete 未被接受:${item.workId}`, { cause: execution.cause })
|
|
332
|
+
: new Error(`work complete 未被接受:${item.workId}`),
|
|
333
|
+
});
|
|
326
334
|
}
|
|
327
335
|
|
|
328
336
|
private async executeHandler(
|
|
@@ -367,7 +375,7 @@ export class EnvironmentWorker {
|
|
|
367
375
|
};
|
|
368
376
|
|
|
369
377
|
return {
|
|
370
|
-
session: buildSyntheticSession(item, harness.id,
|
|
378
|
+
session: buildSyntheticSession(item, harness.id, item.environmentId),
|
|
371
379
|
harness: {
|
|
372
380
|
id: harness.id,
|
|
373
381
|
version: harness.version,
|
|
@@ -437,7 +445,7 @@ export class EnvironmentWorker {
|
|
|
437
445
|
};
|
|
438
446
|
const common = {
|
|
439
447
|
baseUrl: proxy.baseUrl,
|
|
440
|
-
environmentId:
|
|
448
|
+
environmentId: item.environmentId,
|
|
441
449
|
environmentKey: proxy.environmentKey,
|
|
442
450
|
implementationId,
|
|
443
451
|
audit,
|
|
@@ -445,9 +453,6 @@ export class EnvironmentWorker {
|
|
|
445
453
|
signal: proxy.signal,
|
|
446
454
|
requestTimeoutMs: proxy.requestTimeoutMs,
|
|
447
455
|
};
|
|
448
|
-
if (slotName === 'database') {
|
|
449
|
-
return createPlatformDatabaseProxyResource(common) as Database;
|
|
450
|
-
}
|
|
451
456
|
if (slotName === 'skills') {
|
|
452
457
|
return createPlatformResourceProxy<SkillResource>({
|
|
453
458
|
...common,
|
|
@@ -463,17 +468,6 @@ export class EnvironmentWorker {
|
|
|
463
468
|
throw new Error(`platform resource proxy 不支持 slot:${slotName}`);
|
|
464
469
|
}
|
|
465
470
|
|
|
466
|
-
private buildCompleteScope(item: ToolWorkItem) {
|
|
467
|
-
if (!item.leaseId) {
|
|
468
|
-
throw new Error(`claimed work item 缺少 leaseId:${item.workId}`);
|
|
469
|
-
}
|
|
470
|
-
return {
|
|
471
|
-
appId: this.input.appId,
|
|
472
|
-
environmentId: this.input.environmentId,
|
|
473
|
-
executionMode: this.input.executionMode ?? item.executionMode ?? 'self_hosted',
|
|
474
|
-
leaseId: item.leaseId,
|
|
475
|
-
} satisfies CompleteWorkItemScope;
|
|
476
|
-
}
|
|
477
471
|
}
|
|
478
472
|
|
|
479
473
|
/**
|
|
@@ -488,8 +482,7 @@ export class ContextResolverWorker {
|
|
|
488
482
|
public async drainOnce(): Promise<number> {
|
|
489
483
|
const executionMode = this.input.executionMode ?? 'self_hosted';
|
|
490
484
|
const items = await this.input.contextResolveQueue.claim({
|
|
491
|
-
|
|
492
|
-
environmentId: this.input.environmentId,
|
|
485
|
+
scope: this.input.scope ?? {},
|
|
493
486
|
executionMode,
|
|
494
487
|
max: this.input.maxBatchSize ?? 1,
|
|
495
488
|
});
|
|
@@ -499,36 +492,52 @@ export class ContextResolverWorker {
|
|
|
499
492
|
}
|
|
500
493
|
|
|
501
494
|
private async handleItem(item: ContextResolveWorkItem): Promise<void> {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
error: {
|
|
516
|
-
message: normalizeContextResolverError(error),
|
|
517
|
-
retryable: false,
|
|
495
|
+
const renew = this.input.contextResolveQueue.renew;
|
|
496
|
+
const leaseId = item.leaseId;
|
|
497
|
+
await runLeasedItem<ContextResolveResult>({
|
|
498
|
+
...(leaseId && renew ? {
|
|
499
|
+
renewal: {
|
|
500
|
+
intervalMs: this.input.leaseRenewIntervalMs,
|
|
501
|
+
renew: () => renew({
|
|
502
|
+
workId: item.workId,
|
|
503
|
+
leaseId,
|
|
504
|
+
scope: item.scope,
|
|
505
|
+
executionMode: this.input.executionMode ?? item.executionMode ?? 'self_hosted',
|
|
506
|
+
}),
|
|
507
|
+
onError: this.input.onError,
|
|
518
508
|
},
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
509
|
+
} : {}),
|
|
510
|
+
execute: async () => {
|
|
511
|
+
try {
|
|
512
|
+
return {
|
|
513
|
+
workId: item.workId,
|
|
514
|
+
...(item.leaseId ? { leaseId: item.leaseId } : {}),
|
|
515
|
+
status: 'completed',
|
|
516
|
+
snapshot: await this.input.resolvePromptContext(item),
|
|
517
|
+
};
|
|
518
|
+
} catch (error) {
|
|
519
|
+
return {
|
|
520
|
+
workId: item.workId,
|
|
521
|
+
...(item.leaseId ? { leaseId: item.leaseId } : {}),
|
|
522
|
+
status: 'failed',
|
|
523
|
+
error: {
|
|
524
|
+
message: normalizeContextResolverError(error),
|
|
525
|
+
retryable: false,
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
complete: (result) => this.input.contextResolveQueue.complete(
|
|
531
|
+
result,
|
|
532
|
+
this.buildCompleteScope(item),
|
|
533
|
+
),
|
|
534
|
+
completeRejectedError: () => new Error(`context work complete 未被接受:${item.workId}`),
|
|
535
|
+
});
|
|
526
536
|
}
|
|
527
537
|
|
|
528
538
|
private buildCompleteScope(item: ContextResolveWorkItem) {
|
|
529
539
|
return {
|
|
530
|
-
|
|
531
|
-
environmentId: this.input.environmentId,
|
|
540
|
+
scope: item.scope,
|
|
532
541
|
executionMode: this.input.executionMode ?? item.executionMode ?? 'self_hosted',
|
|
533
542
|
leaseId: item.leaseId,
|
|
534
543
|
};
|
|
@@ -539,22 +548,9 @@ export async function runContextResolverWorkerLoop(input: RunContextResolverWork
|
|
|
539
548
|
const worker = new ContextResolverWorker(input);
|
|
540
549
|
const idleDelayMs = input.idleDelayMs ?? 250;
|
|
541
550
|
const errorDelayMs = input.errorDelayMs ?? 1000;
|
|
542
|
-
const reclaimOlderThanMs = input.reclaimOlderThanMs ?? DEFAULT_RECLAIM_OLDER_THAN_MS;
|
|
543
|
-
const reclaimIntervalMs = normalizeNonNegativeInteger(input.reclaimIntervalMs, DEFAULT_RECLAIM_INTERVAL_MS);
|
|
544
|
-
let nextReclaimAtMs = 0;
|
|
545
551
|
|
|
546
552
|
while (!input.abortSignal?.aborted) {
|
|
547
553
|
try {
|
|
548
|
-
if (Date.now() >= nextReclaimAtMs) {
|
|
549
|
-
await input.contextResolveQueue.reclaim({
|
|
550
|
-
appId: input.appId,
|
|
551
|
-
environmentId: input.environmentId,
|
|
552
|
-
executionMode: input.executionMode ?? 'self_hosted',
|
|
553
|
-
olderThanMs: reclaimOlderThanMs,
|
|
554
|
-
});
|
|
555
|
-
nextReclaimAtMs = Date.now() + reclaimIntervalMs;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
554
|
const drained = await worker.drainOnce();
|
|
559
555
|
if (drained === 0) {
|
|
560
556
|
await delay(idleDelayMs, input.abortSignal);
|
|
@@ -570,23 +566,12 @@ export async function runEnvironmentWorkerLoop(input: RunEnvironmentWorkerLoopIn
|
|
|
570
566
|
const worker = new EnvironmentWorker(input);
|
|
571
567
|
const idleDelayMs = input.idleDelayMs ?? 250;
|
|
572
568
|
const errorDelayMs = input.errorDelayMs ?? 1000;
|
|
573
|
-
const reclaimOlderThanMs = input.reclaimOlderThanMs ?? DEFAULT_RECLAIM_OLDER_THAN_MS;
|
|
574
|
-
const reclaimIntervalMs = normalizeNonNegativeInteger(input.reclaimIntervalMs, DEFAULT_RECLAIM_INTERVAL_MS);
|
|
575
569
|
const blockMs = normalizeNonNegativeInteger(input.blockMs, DEFAULT_CLAIM_BLOCK_MS);
|
|
576
570
|
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
|
-
}
|
|
584
|
-
let nextReclaimAtMs = 0;
|
|
585
571
|
const active = new Set<Promise<void>>();
|
|
586
572
|
|
|
587
573
|
const startWork = (item: ToolWorkItem) => {
|
|
588
|
-
|
|
589
|
-
tracked = worker.handleItem(item)
|
|
574
|
+
const tracked = worker.handleItem(item)
|
|
590
575
|
.catch(async (error) => {
|
|
591
576
|
await input.onError?.(error);
|
|
592
577
|
})
|
|
@@ -599,23 +584,13 @@ export async function runEnvironmentWorkerLoop(input: RunEnvironmentWorkerLoopIn
|
|
|
599
584
|
try {
|
|
600
585
|
while (!input.abortSignal?.aborted) {
|
|
601
586
|
try {
|
|
602
|
-
if (Date.now() >= nextReclaimAtMs) {
|
|
603
|
-
await input.workQueue.reclaim({
|
|
604
|
-
appId: input.appId,
|
|
605
|
-
environmentId: input.environmentId,
|
|
606
|
-
executionMode: input.executionMode ?? 'self_hosted',
|
|
607
|
-
olderThanMs: reclaimOlderThanMs,
|
|
608
|
-
});
|
|
609
|
-
nextReclaimAtMs = Date.now() + reclaimIntervalMs;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
587
|
if (input.sandboxManager && input.sandboxIdleTtlMs !== undefined) {
|
|
613
588
|
await input.sandboxManager.sweepIdle(input.sandboxIdleTtlMs);
|
|
614
589
|
}
|
|
615
590
|
|
|
616
591
|
const availableSlots = maxConcurrency - active.size;
|
|
617
592
|
if (availableSlots <= 0) {
|
|
618
|
-
await
|
|
593
|
+
await waitForActiveWorkOrDelay(active, idleDelayMs, input.abortSignal);
|
|
619
594
|
continue;
|
|
620
595
|
}
|
|
621
596
|
|
|
@@ -643,13 +618,14 @@ export async function runEnvironmentWorkerLoop(input: RunEnvironmentWorkerLoopIn
|
|
|
643
618
|
|
|
644
619
|
export type HttpWorkQueueClientInput = {
|
|
645
620
|
baseUrl: string;
|
|
646
|
-
environmentId: string;
|
|
647
621
|
environmentKey: string;
|
|
648
|
-
|
|
622
|
+
workerInstanceId: string;
|
|
649
623
|
fetchImpl?: typeof fetch;
|
|
650
624
|
signal?: AbortSignal;
|
|
651
625
|
};
|
|
652
626
|
|
|
627
|
+
export type HttpContextResolveQueueClientInput = Omit<HttpWorkQueueClientInput, 'workerInstanceId'>;
|
|
628
|
+
|
|
653
629
|
export function zodObjectToJsonSchema(schema: z.ZodType<unknown>): Record<string, unknown> {
|
|
654
630
|
const jsonSchema = z.toJSONSchema(schema) as Record<string, unknown>;
|
|
655
631
|
const normalized = normalizeCatalogJsonSchema(jsonSchema, true);
|
|
@@ -698,59 +674,60 @@ function preserveCatalogUiMetadata(
|
|
|
698
674
|
return target;
|
|
699
675
|
}
|
|
700
676
|
|
|
701
|
-
export class HttpWorkQueueClient implements
|
|
702
|
-
private readonly
|
|
703
|
-
private
|
|
677
|
+
export class HttpWorkQueueClient implements WorkerWorkQueue {
|
|
678
|
+
private readonly transport: WorkerHttpTransport;
|
|
679
|
+
private identityPromise?: Promise<{ branchId: string; environmentId: string }>;
|
|
704
680
|
|
|
705
681
|
public constructor(private readonly input: HttpWorkQueueClientInput) {
|
|
706
|
-
this.
|
|
707
|
-
this.fetchImpl = input.fetchImpl ?? fetch;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
public async enqueue(): Promise<void> {
|
|
711
|
-
throw new Error('HTTP worker client 不能 enqueue;enqueue 只能由平台 Environment 执行');
|
|
682
|
+
this.transport = new WorkerHttpTransport(input);
|
|
712
683
|
}
|
|
713
684
|
|
|
714
|
-
public async claim(input: {
|
|
715
|
-
this.
|
|
716
|
-
const data = await this.requestJson('/work/claim', {
|
|
685
|
+
public async claim(input: { blockMs?: number; max?: number }) {
|
|
686
|
+
const identity = await this.identity();
|
|
687
|
+
const data = await this.transport.requestJson('/work/claim', {
|
|
717
688
|
method: 'POST',
|
|
718
689
|
signal: this.input.signal,
|
|
719
690
|
body: JSON.stringify({
|
|
691
|
+
workerInstanceId: this.input.workerInstanceId,
|
|
720
692
|
blockMs: input.blockMs,
|
|
721
693
|
max: input.max,
|
|
722
|
-
executionMode: input.executionMode,
|
|
723
694
|
}),
|
|
724
|
-
});
|
|
695
|
+
}, '平台工作队列响应不是 JSON 对象');
|
|
725
696
|
|
|
726
697
|
const record = expectRecord(data, 'claim 响应缺少 data');
|
|
727
698
|
const items = record.items;
|
|
728
699
|
if (!Array.isArray(items)) {
|
|
729
700
|
throw new Error('claim 响应缺少 items 数组');
|
|
730
701
|
}
|
|
731
|
-
return items.map(parseToolWorkItem);
|
|
702
|
+
return items.map((item) => parseToolWorkItem(item, identity.environmentId));
|
|
732
703
|
}
|
|
733
704
|
|
|
734
|
-
public async renew(input:
|
|
735
|
-
this.
|
|
736
|
-
const data = await this.requestJson('/work/renew', {
|
|
705
|
+
public async renew(input: WorkerRenewWorkItemLeaseInput): Promise<boolean> {
|
|
706
|
+
const data = await this.transport.requestJson('/work/renew', {
|
|
737
707
|
method: 'POST',
|
|
738
708
|
signal: this.input.signal,
|
|
739
|
-
body: JSON.stringify({
|
|
740
|
-
|
|
709
|
+
body: JSON.stringify({
|
|
710
|
+
workId: input.workId,
|
|
711
|
+
leaseId: input.leaseId,
|
|
712
|
+
workerInstanceId: this.input.workerInstanceId,
|
|
713
|
+
}),
|
|
714
|
+
}, '平台工作队列响应不是 JSON 对象');
|
|
741
715
|
const record = expectRecord(data, 'renew 响应缺少 data');
|
|
742
716
|
return record.renewed === true;
|
|
743
717
|
}
|
|
744
718
|
|
|
745
|
-
public async complete(result: ToolExecutionResult, scope
|
|
746
|
-
if (!scope
|
|
719
|
+
public async complete(result: ToolExecutionResult, scope: WorkerCompleteWorkItemScope): Promise<boolean> {
|
|
720
|
+
if (!scope.leaseId) {
|
|
747
721
|
throw new Error(`work complete 缺少 leaseId:${result.workId}`);
|
|
748
722
|
}
|
|
749
|
-
this.
|
|
750
|
-
const data = await this.requestJson('/work/complete', {
|
|
723
|
+
const data = await this.transport.requestJson('/work/complete', {
|
|
751
724
|
method: 'POST',
|
|
752
|
-
body: JSON.stringify({
|
|
753
|
-
|
|
725
|
+
body: JSON.stringify({
|
|
726
|
+
...result,
|
|
727
|
+
leaseId: scope.leaseId,
|
|
728
|
+
workerInstanceId: this.input.workerInstanceId,
|
|
729
|
+
}),
|
|
730
|
+
}, '平台工作队列响应不是 JSON 对象');
|
|
754
731
|
const record = expectRecord(data, 'complete 响应缺少 data');
|
|
755
732
|
if (record.accepted !== true) {
|
|
756
733
|
throw new Error(`work complete 未被接受:${result.workId}`);
|
|
@@ -758,76 +735,70 @@ export class HttpWorkQueueClient implements WorkQueue {
|
|
|
758
735
|
return true;
|
|
759
736
|
}
|
|
760
737
|
|
|
761
|
-
public async
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
738
|
+
public async register(registration: WorkerCapabilityRegistration) {
|
|
739
|
+
if (registration.workerInstanceId !== this.input.workerInstanceId) {
|
|
740
|
+
throw new Error('Worker 注册身份与 client 实例不一致');
|
|
741
|
+
}
|
|
742
|
+
const { workerInstanceId: _workerInstanceId, ...body } = registration;
|
|
743
|
+
const data = await this.transport.requestJson(`/instances/${encodeURIComponent(this.input.workerInstanceId)}`, {
|
|
744
|
+
method: 'PUT',
|
|
765
745
|
signal: this.input.signal,
|
|
766
|
-
body: JSON.stringify(
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
public async stats(input: { appId: string; environmentId: string; executionMode?: EnvironmentExecutionMode }): Promise<WorkStats> {
|
|
774
|
-
this.assertScope(input.appId, input.environmentId);
|
|
775
|
-
const query = input.executionMode ? `?executionMode=${encodeURIComponent(input.executionMode)}` : '';
|
|
776
|
-
const data = await this.requestJson(`/work/stats${query}`, { method: 'GET', signal: this.input.signal });
|
|
777
|
-
const record = expectRecord(data, 'stats 响应缺少 data');
|
|
778
|
-
return parseWorkStats(record.stats);
|
|
746
|
+
body: JSON.stringify(body),
|
|
747
|
+
}, '平台工作队列响应不是 JSON 对象');
|
|
748
|
+
const record = expectRecord(data, 'Worker 注册响应缺少 data');
|
|
749
|
+
return {
|
|
750
|
+
workerInstanceId: expectString(record.workerInstanceId, 'workerInstanceId'),
|
|
751
|
+
expiresAt: expectString(record.expiresAt, 'expiresAt'),
|
|
752
|
+
};
|
|
779
753
|
}
|
|
780
754
|
|
|
781
|
-
|
|
782
|
-
if (
|
|
783
|
-
throw new Error(
|
|
784
|
-
}
|
|
785
|
-
if (environmentId !== this.input.environmentId) {
|
|
786
|
-
throw new Error(`worker environmentId 不匹配:${environmentId}`);
|
|
755
|
+
public async heartbeat(workerInstanceId: string) {
|
|
756
|
+
if (workerInstanceId !== this.input.workerInstanceId) {
|
|
757
|
+
throw new Error('Worker 心跳身份与 client 实例不一致');
|
|
787
758
|
}
|
|
759
|
+
const data = await this.transport.requestJson(`/instances/${encodeURIComponent(workerInstanceId)}/heartbeat`, {
|
|
760
|
+
method: 'POST',
|
|
761
|
+
signal: this.input.signal,
|
|
762
|
+
}, '平台工作队列响应不是 JSON 对象');
|
|
763
|
+
const record = expectRecord(data, 'Worker 心跳响应缺少 data');
|
|
764
|
+
return {
|
|
765
|
+
workerInstanceId: expectString(record.workerInstanceId, 'workerInstanceId'),
|
|
766
|
+
expiresAt: expectString(record.expiresAt, 'expiresAt'),
|
|
767
|
+
};
|
|
788
768
|
}
|
|
789
769
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
770
|
+
public identity() {
|
|
771
|
+
this.identityPromise ??= this.transport.requestJson('/identity', {
|
|
772
|
+
method: 'GET',
|
|
773
|
+
signal: this.input.signal,
|
|
774
|
+
}, '平台工作队列响应不是 JSON 对象').then((data) => {
|
|
775
|
+
const record = expectRecord(data, 'Worker identity 响应缺少 data');
|
|
776
|
+
const identity = expectRecord(record.identity, 'Worker identity 响应缺少 identity');
|
|
777
|
+
return {
|
|
778
|
+
branchId: expectString(identity.branchId, 'identity.branchId'),
|
|
779
|
+
environmentId: expectString(identity.environmentId, 'identity.environmentId'),
|
|
780
|
+
};
|
|
800
781
|
});
|
|
801
|
-
return
|
|
782
|
+
return this.identityPromise;
|
|
802
783
|
}
|
|
803
784
|
|
|
804
|
-
private buildUrl(path: string): string {
|
|
805
|
-
return `${this.baseUrl}/v1/environments/${encodeURIComponent(this.input.environmentId)}${path}`;
|
|
806
|
-
}
|
|
807
785
|
}
|
|
808
786
|
|
|
809
|
-
export class HttpContextResolveQueueClient implements
|
|
810
|
-
private readonly
|
|
811
|
-
private readonly fetchImpl: typeof fetch;
|
|
812
|
-
|
|
813
|
-
public constructor(private readonly input: HttpWorkQueueClientInput) {
|
|
814
|
-
this.baseUrl = trimTrailingSlash(input.baseUrl);
|
|
815
|
-
this.fetchImpl = input.fetchImpl ?? fetch;
|
|
816
|
-
}
|
|
787
|
+
export class HttpContextResolveQueueClient implements WorkerContextResolveQueue {
|
|
788
|
+
private readonly transport: WorkerHttpTransport;
|
|
817
789
|
|
|
818
|
-
public
|
|
819
|
-
|
|
790
|
+
public constructor(private readonly input: HttpContextResolveQueueClientInput) {
|
|
791
|
+
this.transport = new WorkerHttpTransport(input);
|
|
820
792
|
}
|
|
821
793
|
|
|
822
|
-
public async claim(input:
|
|
823
|
-
this.
|
|
824
|
-
const data = await this.requestJson('/context-work/claim', {
|
|
794
|
+
public async claim(input: ContextResolveQueueClaimInput) {
|
|
795
|
+
const data = await this.transport.requestJson('/context-work/claim', {
|
|
825
796
|
method: 'POST',
|
|
826
797
|
signal: this.input.signal,
|
|
827
798
|
body: JSON.stringify({
|
|
828
799
|
max: input.max,
|
|
829
800
|
}),
|
|
830
|
-
});
|
|
801
|
+
}, '平台 context 工作队列响应不是 JSON 对象');
|
|
831
802
|
|
|
832
803
|
const record = expectRecord(data, 'context claim 响应缺少 data');
|
|
833
804
|
const items = record.items;
|
|
@@ -838,10 +809,10 @@ export class HttpContextResolveQueueClient implements ContextResolveQueue {
|
|
|
838
809
|
}
|
|
839
810
|
|
|
840
811
|
public async complete(result: ContextResolveResult): Promise<boolean> {
|
|
841
|
-
const data = await this.requestJson('/context-work/complete', {
|
|
812
|
+
const data = await this.transport.requestJson('/context-work/complete', {
|
|
842
813
|
method: 'POST',
|
|
843
814
|
body: JSON.stringify(result),
|
|
844
|
-
});
|
|
815
|
+
}, '平台 context 工作队列响应不是 JSON 对象');
|
|
845
816
|
const record = expectRecord(data, 'context complete 响应缺少 data');
|
|
846
817
|
if (record.accepted !== true) {
|
|
847
818
|
throw new Error(`context complete 未被接受:${result.workId}`);
|
|
@@ -849,69 +820,20 @@ export class HttpContextResolveQueueClient implements ContextResolveQueue {
|
|
|
849
820
|
return true;
|
|
850
821
|
}
|
|
851
822
|
|
|
852
|
-
public async
|
|
853
|
-
this.
|
|
854
|
-
const data = await this.requestJson('/context-work/reclaim', {
|
|
823
|
+
public async renew(input: { workId: string; leaseId: string }): Promise<boolean> {
|
|
824
|
+
const data = await this.transport.requestJson('/context-work/renew', {
|
|
855
825
|
method: 'POST',
|
|
856
826
|
signal: this.input.signal,
|
|
857
|
-
body: JSON.stringify({
|
|
858
|
-
});
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
return typeof record.reclaimed === 'number' ? record.reclaimed : 0;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
public async stats(input: { appId: string; environmentId: string }) {
|
|
865
|
-
this.assertScope(input.appId, input.environmentId);
|
|
866
|
-
const data = await this.requestJson('/context-work/stats', { method: 'GET', signal: this.input.signal });
|
|
867
|
-
const record = expectRecord(data, 'context stats 响应缺少 data');
|
|
868
|
-
return parseContextResolveStats(record.stats);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
private assertScope(appId: string, environmentId: string): void {
|
|
872
|
-
if (this.input.appId && appId !== this.input.appId) {
|
|
873
|
-
throw new Error(`context resolver appId 不匹配:${appId}`);
|
|
874
|
-
}
|
|
875
|
-
if (environmentId !== this.input.environmentId) {
|
|
876
|
-
throw new Error(`context resolver environmentId 不匹配:${environmentId}`);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
private async requestJson(path: string, init: RequestInit): Promise<unknown> {
|
|
881
|
-
const headers = new Headers(init.headers);
|
|
882
|
-
headers.set('x-api-key', this.input.environmentKey);
|
|
883
|
-
if (init.body && !headers.has('content-type')) {
|
|
884
|
-
headers.set('content-type', 'application/json');
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
const response = await this.fetchImpl(this.buildUrl(path), {
|
|
888
|
-
...init,
|
|
889
|
-
headers,
|
|
890
|
-
});
|
|
891
|
-
return parseApiEnvelope(response, `平台 context 工作队列响应不是 JSON 对象:${response.status}`);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
private buildUrl(path: string): string {
|
|
895
|
-
return `${this.baseUrl}/v1/environments/${encodeURIComponent(this.input.environmentId)}${path}`;
|
|
827
|
+
body: JSON.stringify({ workId: input.workId, leaseId: input.leaseId }),
|
|
828
|
+
}, '平台 context 工作队列响应不是 JSON 对象');
|
|
829
|
+
const record = expectRecord(data, 'context renew 响应缺少 data');
|
|
830
|
+
return record.renewed === true;
|
|
896
831
|
}
|
|
897
|
-
}
|
|
898
832
|
|
|
899
|
-
async function parseApiEnvelope(response: Response, fallbackMessage: string): Promise<unknown> {
|
|
900
|
-
const body = await response.json().catch(() => null) as unknown;
|
|
901
|
-
const envelope = expectRecord(body, fallbackMessage);
|
|
902
|
-
if (!response.ok || envelope.success !== true) {
|
|
903
|
-
const error = isRecord(envelope.error) ? envelope.error : {};
|
|
904
|
-
const message = typeof error.message === 'string'
|
|
905
|
-
? error.message
|
|
906
|
-
: `平台请求失败:${response.status}`;
|
|
907
|
-
throw new Error(message);
|
|
908
|
-
}
|
|
909
|
-
return envelope.data;
|
|
910
833
|
}
|
|
911
834
|
|
|
912
835
|
function toToolExecutionResult(
|
|
913
836
|
workId: string,
|
|
914
|
-
handler: ToolHandler<unknown, unknown>,
|
|
915
837
|
execution: ToolExecution<unknown>,
|
|
916
838
|
): ToolExecutionResult {
|
|
917
839
|
if (execution.status === 'failed') {
|
|
@@ -934,24 +856,22 @@ function toToolExecutionResult(
|
|
|
934
856
|
return {
|
|
935
857
|
workId,
|
|
936
858
|
status: 'completed',
|
|
937
|
-
modelResult:
|
|
859
|
+
modelResult: assertToolResultWireValue(execution.modelResult),
|
|
938
860
|
effects: execution.effects,
|
|
939
861
|
};
|
|
940
862
|
}
|
|
941
863
|
|
|
942
|
-
function buildSyntheticSession(item: ToolWorkItem, harnessId: string,
|
|
864
|
+
function buildSyntheticSession(item: ToolWorkItem, harnessId: string, environmentId: string): SessionRecord {
|
|
943
865
|
const now = new Date().toISOString();
|
|
944
866
|
return {
|
|
945
867
|
id: item.sessionId as SessionId,
|
|
946
|
-
appId: item.appId,
|
|
947
868
|
scope: item.scope,
|
|
869
|
+
configuration: { status: 'available', configVersionId: item.configVersionId },
|
|
948
870
|
rootSessionId: item.rootSessionId as SessionId,
|
|
949
871
|
harnessId: harnessId as HarnessId,
|
|
950
|
-
|
|
951
|
-
environmentId: item.environmentId as EnvironmentId,
|
|
952
|
-
environmentUpdatedAt: now,
|
|
872
|
+
environmentId: environmentId as EnvironmentId,
|
|
953
873
|
title: item.sessionId,
|
|
954
|
-
metadata: {},
|
|
874
|
+
metadata: item.sessionMetadata ?? {},
|
|
955
875
|
createdAt: now,
|
|
956
876
|
updatedAt: now,
|
|
957
877
|
};
|
|
@@ -988,14 +908,35 @@ function hasDispose(value: unknown): value is { dispose(): Promise<void> | void
|
|
|
988
908
|
return isRecord(value) && typeof value.dispose === 'function';
|
|
989
909
|
}
|
|
990
910
|
|
|
991
|
-
function parseToolWorkItem(value: unknown): ToolWorkItem {
|
|
911
|
+
function parseToolWorkItem(value: unknown, environmentId: string): ToolWorkItem {
|
|
992
912
|
const record = expectRecord(value, 'work item 不是对象');
|
|
993
913
|
const workId = expectString(record.workId, 'workId');
|
|
994
914
|
const leaseId = typeof record.leaseId === 'string' && record.leaseId.trim()
|
|
995
915
|
? record.leaseId.trim()
|
|
996
916
|
: undefined;
|
|
997
|
-
const
|
|
998
|
-
const
|
|
917
|
+
const configVersionId = expectString(record.configVersionId, 'configVersionId');
|
|
918
|
+
const requiredContractDigest = expectString(record.requiredContractDigest, 'requiredContractDigest');
|
|
919
|
+
if (!Array.isArray(record.requiredResourceCapabilities)) {
|
|
920
|
+
throw new Error('requiredResourceCapabilities 必须是数组');
|
|
921
|
+
}
|
|
922
|
+
const requiredResourceCapabilities = record.requiredResourceCapabilities.map((value, index) => {
|
|
923
|
+
const capability = expectRecord(value, `requiredResourceCapabilities[${index}]`);
|
|
924
|
+
return {
|
|
925
|
+
slot: expectString(capability.slot, `requiredResourceCapabilities[${index}].slot`),
|
|
926
|
+
implementationId: expectString(
|
|
927
|
+
capability.implementationId,
|
|
928
|
+
`requiredResourceCapabilities[${index}].implementationId`,
|
|
929
|
+
),
|
|
930
|
+
};
|
|
931
|
+
});
|
|
932
|
+
const workerKind = record.workerKind;
|
|
933
|
+
if (workerKind !== 'self_hosted' && workerKind !== 'platform_builtin') {
|
|
934
|
+
throw new Error('workerKind 无效');
|
|
935
|
+
}
|
|
936
|
+
if (Object.prototype.hasOwnProperty.call(record, 'environmentId')
|
|
937
|
+
|| Object.prototype.hasOwnProperty.call(record, 'appId')) {
|
|
938
|
+
throw new Error('work item wire 不得携带服务端隔离字段');
|
|
939
|
+
}
|
|
999
940
|
const sessionId = expectString(record.sessionId, 'sessionId');
|
|
1000
941
|
const rootSessionId = expectString(record.rootSessionId, 'rootSessionId');
|
|
1001
942
|
const scope = parseExecutionScope(record.scope);
|
|
@@ -1006,7 +947,10 @@ function parseToolWorkItem(value: unknown): ToolWorkItem {
|
|
|
1006
947
|
const item: ToolWorkItem = {
|
|
1007
948
|
workId,
|
|
1008
949
|
...(leaseId ? { leaseId } : {}),
|
|
1009
|
-
|
|
950
|
+
configVersionId: configVersionId as ToolWorkItem['configVersionId'],
|
|
951
|
+
requiredContractDigest,
|
|
952
|
+
requiredResourceCapabilities,
|
|
953
|
+
workerKind,
|
|
1010
954
|
environmentId,
|
|
1011
955
|
sessionId,
|
|
1012
956
|
rootSessionId,
|
|
@@ -1022,6 +966,10 @@ function parseToolWorkItem(value: unknown): ToolWorkItem {
|
|
|
1022
966
|
item.resume = parseToolResumeWorkPayload(record.resume);
|
|
1023
967
|
}
|
|
1024
968
|
|
|
969
|
+
if (record.sessionMetadata !== undefined) {
|
|
970
|
+
item.sessionMetadata = expectRecord(record.sessionMetadata, 'sessionMetadata 必须是对象');
|
|
971
|
+
}
|
|
972
|
+
|
|
1025
973
|
if (isRecord(record.resourceBindings)) {
|
|
1026
974
|
item.resourceBindings = record.resourceBindings;
|
|
1027
975
|
}
|
|
@@ -1050,35 +998,139 @@ function parseExecutionMode(value: unknown): ToolWorkItem['executionMode'] | und
|
|
|
1050
998
|
|
|
1051
999
|
function parseToolResumeWorkPayload(value: unknown): NonNullable<ToolWorkItem['resume']> {
|
|
1052
1000
|
const record = expectRecord(value, 'resume 工作项缺少 resume payload');
|
|
1053
|
-
const actionEvent =
|
|
1054
|
-
const resolvedEvent =
|
|
1001
|
+
const actionEvent = parseActionRequestedEvent(record.actionEvent, 'resume.actionEvent');
|
|
1002
|
+
const resolvedEvent = parseActionResolvedEvent(record.resolvedEvent, 'resume.resolvedEvent');
|
|
1003
|
+
if (actionEvent.payload.kind !== resolvedEvent.payload.kind) {
|
|
1004
|
+
throw new Error('resume action kind 不一致');
|
|
1005
|
+
}
|
|
1055
1006
|
return {
|
|
1056
1007
|
actionEvent,
|
|
1057
1008
|
resolvedEvent,
|
|
1058
1009
|
};
|
|
1059
1010
|
}
|
|
1060
1011
|
|
|
1061
|
-
function
|
|
1012
|
+
function parseActionRequestedEvent(
|
|
1013
|
+
value: unknown,
|
|
1014
|
+
field: string,
|
|
1015
|
+
): NonNullable<ToolWorkItem['resume']>['actionEvent'] {
|
|
1016
|
+
const event = expectRecord(value, `${field} 必须是对象`);
|
|
1017
|
+
if (event.type !== 'agent.action.requested') {
|
|
1018
|
+
throw new Error(`${field} 类型必须是 agent.action.requested`);
|
|
1019
|
+
}
|
|
1020
|
+
const payload = expectRecord(event.payload, `${field}.payload 必须是对象`);
|
|
1021
|
+
const display = payload.display === undefined
|
|
1022
|
+
? undefined
|
|
1023
|
+
: parseActionDisplay(payload.display, `${field}.payload.display`);
|
|
1024
|
+
const allowedResults = parseOptionalArray(
|
|
1025
|
+
payload.allowedResults,
|
|
1026
|
+
`${field}.payload.allowedResults`,
|
|
1027
|
+
);
|
|
1028
|
+
return {
|
|
1029
|
+
...parseResumeEventCore(event, field),
|
|
1030
|
+
type: 'agent.action.requested',
|
|
1031
|
+
payload: {
|
|
1032
|
+
kind: parseActionKind(payload.kind, `${field}.payload.kind`),
|
|
1033
|
+
...(payload.prompt === undefined
|
|
1034
|
+
? {}
|
|
1035
|
+
: { prompt: expectOptionalString(payload.prompt, `${field}.payload.prompt`) }),
|
|
1036
|
+
...(Object.prototype.hasOwnProperty.call(payload, 'input') ? { input: payload.input } : {}),
|
|
1037
|
+
...(allowedResults ? { allowedResults } : {}),
|
|
1038
|
+
...(display ? { display } : {}),
|
|
1039
|
+
...(payload.expiresAt === undefined
|
|
1040
|
+
? {}
|
|
1041
|
+
: { expiresAt: expectOptionalString(payload.expiresAt, `${field}.payload.expiresAt`) }),
|
|
1042
|
+
},
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function parseActionResolvedEvent(
|
|
1062
1047
|
value: unknown,
|
|
1063
|
-
type: TType,
|
|
1064
1048
|
field: string,
|
|
1065
|
-
):
|
|
1049
|
+
): NonNullable<ToolWorkItem['resume']>['resolvedEvent'] {
|
|
1066
1050
|
const event = expectRecord(value, `${field} 必须是对象`);
|
|
1067
|
-
if (event.type !==
|
|
1068
|
-
throw new Error(`${field} 类型必须是
|
|
1051
|
+
if (event.type !== 'user.action.resolved') {
|
|
1052
|
+
throw new Error(`${field} 类型必须是 user.action.resolved`);
|
|
1069
1053
|
}
|
|
1070
|
-
|
|
1071
|
-
if (
|
|
1072
|
-
throw new Error(`${field}.
|
|
1054
|
+
const payload = expectRecord(event.payload, `${field}.payload 必须是对象`);
|
|
1055
|
+
if (!Object.prototype.hasOwnProperty.call(payload, 'result')) {
|
|
1056
|
+
throw new Error(`${field}.payload.result 缺失`);
|
|
1073
1057
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1058
|
+
return {
|
|
1059
|
+
...parseResumeEventCore(event, field),
|
|
1060
|
+
type: 'user.action.resolved',
|
|
1061
|
+
payload: {
|
|
1062
|
+
kind: parseActionKind(payload.kind, `${field}.payload.kind`),
|
|
1063
|
+
result: payload.result,
|
|
1064
|
+
},
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function parseResumeEventCore(event: Record<string, unknown>, field: string) {
|
|
1069
|
+
return {
|
|
1070
|
+
id: expectString(event.id, `${field}.id`),
|
|
1071
|
+
seq: expectNumber(event.seq, `${field}.seq`),
|
|
1072
|
+
...(event.globalSeq === undefined
|
|
1073
|
+
? {}
|
|
1074
|
+
: { globalSeq: expectNumber(event.globalSeq, `${field}.globalSeq`) }),
|
|
1075
|
+
...(event.turnId === undefined
|
|
1076
|
+
? {}
|
|
1077
|
+
: { turnId: expectString(event.turnId, `${field}.turnId`) }),
|
|
1078
|
+
...(event.parentEventId === undefined
|
|
1079
|
+
? {}
|
|
1080
|
+
: { parentEventId: expectString(event.parentEventId, `${field}.parentEventId`) }),
|
|
1081
|
+
actor: parseSessionEventActor(event.actor, `${field}.actor`),
|
|
1082
|
+
createdAt: expectString(event.createdAt, `${field}.createdAt`),
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function parseActionDisplay(value: unknown, field: string) {
|
|
1087
|
+
const display = expectRecord(value, `${field} 必须是对象`);
|
|
1088
|
+
return {
|
|
1089
|
+
...(display.title === undefined
|
|
1090
|
+
? {}
|
|
1091
|
+
: { title: expectOptionalString(display.title, `${field}.title`) }),
|
|
1092
|
+
...(display.summary === undefined
|
|
1093
|
+
? {}
|
|
1094
|
+
: { summary: expectOptionalString(display.summary, `${field}.summary`) }),
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1077
1097
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1098
|
+
function parseSessionEventActor(value: unknown, field: string): SessionEventActor {
|
|
1099
|
+
const actor = expectRecord(value, `${field} 必须是对象`);
|
|
1100
|
+
if (actor.type !== 'user'
|
|
1101
|
+
&& actor.type !== 'assistant'
|
|
1102
|
+
&& actor.type !== 'tool'
|
|
1103
|
+
&& actor.type !== 'system') {
|
|
1104
|
+
throw new Error(`${field}.type 无效`);
|
|
1080
1105
|
}
|
|
1081
|
-
return
|
|
1106
|
+
return {
|
|
1107
|
+
type: actor.type,
|
|
1108
|
+
...(actor.id === undefined ? {} : { id: expectString(actor.id, `${field}.id`) }),
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function parseActionKind(value: unknown, field: string) {
|
|
1113
|
+
if (value !== 'ask_user' && value !== 'tool_confirmation' && value !== 'custom_tool_result') {
|
|
1114
|
+
throw new Error(`${field} 无效`);
|
|
1115
|
+
}
|
|
1116
|
+
return value;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
function expectOptionalString(value: unknown, field: string) {
|
|
1120
|
+
if (typeof value !== 'string') {
|
|
1121
|
+
throw new Error(`${field} 必须是字符串`);
|
|
1122
|
+
}
|
|
1123
|
+
return value;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function parseOptionalArray(value: unknown, field: string): unknown[] | undefined {
|
|
1127
|
+
if (value === undefined) {
|
|
1128
|
+
return undefined;
|
|
1129
|
+
}
|
|
1130
|
+
if (!Array.isArray(value)) {
|
|
1131
|
+
throw new Error(`${field} 必须是数组`);
|
|
1132
|
+
}
|
|
1133
|
+
return value;
|
|
1082
1134
|
}
|
|
1083
1135
|
|
|
1084
1136
|
function parseExecutionScope(value: unknown): ExecutionScope {
|
|
@@ -1098,8 +1150,6 @@ function parseContextResolveWorkItem(value: unknown): ContextResolveWorkItem {
|
|
|
1098
1150
|
const leaseId = typeof record.leaseId === 'string' && record.leaseId.trim()
|
|
1099
1151
|
? record.leaseId.trim()
|
|
1100
1152
|
: undefined;
|
|
1101
|
-
const appId = expectString(record.appId, 'appId');
|
|
1102
|
-
const environmentId = expectString(record.environmentId, 'environmentId');
|
|
1103
1153
|
const sessionId = expectString(record.sessionId, 'sessionId');
|
|
1104
1154
|
const rootSessionId = expectString(record.rootSessionId, 'rootSessionId');
|
|
1105
1155
|
const request = expectRecord(record.request, 'request 必须是对象');
|
|
@@ -1121,8 +1171,6 @@ function parseContextResolveWorkItem(value: unknown): ContextResolveWorkItem {
|
|
|
1121
1171
|
return {
|
|
1122
1172
|
workId,
|
|
1123
1173
|
...(leaseId ? { leaseId } : {}),
|
|
1124
|
-
appId,
|
|
1125
|
-
environmentId,
|
|
1126
1174
|
sessionId,
|
|
1127
1175
|
rootSessionId,
|
|
1128
1176
|
scope: parseExecutionScope(record.scope),
|
|
@@ -1134,27 +1182,6 @@ function parseContextResolveWorkItem(value: unknown): ContextResolveWorkItem {
|
|
|
1134
1182
|
};
|
|
1135
1183
|
}
|
|
1136
1184
|
|
|
1137
|
-
function parseWorkStats(value: unknown): WorkStats {
|
|
1138
|
-
const record = expectRecord(value, 'stats 不是对象');
|
|
1139
|
-
const oldestQueuedAt = record.oldestQueuedAt;
|
|
1140
|
-
return {
|
|
1141
|
-
depth: expectNumber(record.depth, 'depth'),
|
|
1142
|
-
pending: expectNumber(record.pending, 'pending'),
|
|
1143
|
-
oldestQueuedAt: typeof oldestQueuedAt === 'string' ? oldestQueuedAt : null,
|
|
1144
|
-
workersPolling: expectNumber(record.workersPolling, 'workersPolling'),
|
|
1145
|
-
};
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
function parseContextResolveStats(value: unknown) {
|
|
1149
|
-
const record = expectRecord(value, 'context stats 不是对象');
|
|
1150
|
-
const oldestQueuedAt = record.oldestQueuedAt;
|
|
1151
|
-
return {
|
|
1152
|
-
depth: expectNumber(record.depth, 'depth'),
|
|
1153
|
-
pending: expectNumber(record.pending, 'pending'),
|
|
1154
|
-
oldestQueuedAt: typeof oldestQueuedAt === 'string' ? oldestQueuedAt : null,
|
|
1155
|
-
};
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
1185
|
function normalizeWorkerError(error: unknown): string {
|
|
1159
1186
|
if (error instanceof Error) {
|
|
1160
1187
|
return `工具执行失败:${error.message}`;
|
|
@@ -1186,10 +1213,6 @@ function normalizeContextResolverError(error: unknown): string {
|
|
|
1186
1213
|
return `context resolve 失败:${String(error)}`;
|
|
1187
1214
|
}
|
|
1188
1215
|
|
|
1189
|
-
function trimTrailingSlash(value: string): string {
|
|
1190
|
-
return value.endsWith('/') ? value.slice(0, -1) : value;
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
1216
|
function expectString(value: unknown, field: string): string {
|
|
1194
1217
|
if (typeof value !== 'string' || !value.trim()) {
|
|
1195
1218
|
throw new Error(`字段 ${field} 必须是非空字符串`);
|
|
@@ -1231,18 +1254,6 @@ function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
1231
1254
|
});
|
|
1232
1255
|
}
|
|
1233
1256
|
|
|
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
1257
|
function waitForActiveWorkOrDelay(
|
|
1247
1258
|
active: Set<Promise<void>>,
|
|
1248
1259
|
delayMs: number,
|