@ct-agents/worker 0.1.9 → 0.2.1
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 +399 -376
- 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 +33 -2
- package/src/resources/web-search/tavily.ts +2 -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 +2 -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,36 @@ 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
|
-
|
|
153
|
+
const PLATFORM_PROXY_RESOURCE_CAPABILITIES = [
|
|
154
|
+
{ slot: 'skills', implementationId: 'platform-skills-store' },
|
|
155
|
+
{ slot: 'memory', implementationId: 'platform-memory-store' },
|
|
156
|
+
] as const;
|
|
157
|
+
const PLATFORM_PROXY_RESOURCE_SLOTS = new Set<string>(
|
|
158
|
+
PLATFORM_PROXY_RESOURCE_CAPABILITIES.map((capability) => capability.slot),
|
|
159
|
+
);
|
|
152
160
|
|
|
153
161
|
export function createPlatformBuiltinToolHandlers(): ToolHandler[] {
|
|
154
162
|
return [
|
|
@@ -166,6 +174,50 @@ function withPlatformBuiltinTools(registry: ToolRegistry): ToolRegistry {
|
|
|
166
174
|
return new PlatformBuiltinToolRegistry(registry, createPlatformBuiltinToolHandlers());
|
|
167
175
|
}
|
|
168
176
|
|
|
177
|
+
export function createWorkerCapabilityRegistration(input: {
|
|
178
|
+
workerInstanceId: string;
|
|
179
|
+
implementationReleaseId: string;
|
|
180
|
+
toolRegistry: ToolRegistry;
|
|
181
|
+
resourceImpls: EnvironmentWorkerResourceImplementations;
|
|
182
|
+
platformResourceProxy?: boolean;
|
|
183
|
+
}): WorkerCapabilityRegistration {
|
|
184
|
+
const toolRegistry = withPlatformBuiltinTools(input.toolRegistry);
|
|
185
|
+
const toolCapabilities = toolRegistry.listTools().map((descriptor) => {
|
|
186
|
+
const contract = normalizeToolExecutionContract({
|
|
187
|
+
contractFormatVersion: 1,
|
|
188
|
+
toolName: descriptor.name,
|
|
189
|
+
inputSchema: descriptor.inputSchema,
|
|
190
|
+
requiredResources: descriptor.requiredResources ?? [],
|
|
191
|
+
annotations: descriptor.annotations,
|
|
192
|
+
compatibilityGeneration: descriptor.compatibilityGeneration ?? 1,
|
|
193
|
+
} satisfies ToolExecutionContract);
|
|
194
|
+
return {
|
|
195
|
+
contractDigest: digestToolExecutionContract(contract),
|
|
196
|
+
contract,
|
|
197
|
+
toolName: descriptor.name,
|
|
198
|
+
implementationId: `${input.implementationReleaseId}:${descriptor.name}`,
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
const resourceCapabilities = Object.entries(input.resourceImpls).flatMap(([slot, implementations]) => (
|
|
202
|
+
Object.keys(implementations).map((implementationId) => ({ slot, implementationId }))
|
|
203
|
+
));
|
|
204
|
+
if (input.platformResourceProxy) {
|
|
205
|
+
for (const capability of PLATFORM_PROXY_RESOURCE_CAPABILITIES) {
|
|
206
|
+
if (!resourceCapabilities.some((candidate) => (
|
|
207
|
+
candidate.slot === capability.slot && candidate.implementationId === capability.implementationId
|
|
208
|
+
))) {
|
|
209
|
+
resourceCapabilities.push(capability);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
workerInstanceId: input.workerInstanceId,
|
|
215
|
+
implementationReleaseId: input.implementationReleaseId,
|
|
216
|
+
toolCapabilities,
|
|
217
|
+
resourceCapabilities,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
169
221
|
function normalizeNonNegativeInteger(value: number | undefined, fallback: number): number {
|
|
170
222
|
if (value === undefined || !Number.isFinite(value)) {
|
|
171
223
|
return fallback;
|
|
@@ -214,10 +266,14 @@ class PlatformBuiltinToolRegistry implements ToolRegistry {
|
|
|
214
266
|
}
|
|
215
267
|
}
|
|
216
268
|
|
|
269
|
+
type EnvironmentWorkExecution =
|
|
270
|
+
| { kind: 'completed'; result: ToolExecutionResult }
|
|
271
|
+
| { kind: 'failed'; result: ToolExecutionResult; cause: unknown };
|
|
272
|
+
|
|
217
273
|
/**
|
|
218
274
|
* 独立执行面 worker。
|
|
219
275
|
*
|
|
220
|
-
* worker 只通过
|
|
276
|
+
* worker 只通过 Environment Key 绑定的队列认领业务 Tool Work,并在本地
|
|
221
277
|
* ToolRegistry/ResourceSlots 中执行 handler。它不持消费端 key,也不直接创建或读取 session。
|
|
222
278
|
*/
|
|
223
279
|
export class EnvironmentWorker {
|
|
@@ -241,11 +297,7 @@ export class EnvironmentWorker {
|
|
|
241
297
|
}
|
|
242
298
|
|
|
243
299
|
public async claim(max: number, options: DrainOnceOptions = {}): Promise<ToolWorkItem[]> {
|
|
244
|
-
const executionMode = this.input.executionMode ?? 'self_hosted';
|
|
245
300
|
return this.input.workQueue.claim({
|
|
246
|
-
appId: this.input.appId,
|
|
247
|
-
environmentId: this.input.environmentId,
|
|
248
|
-
executionMode,
|
|
249
301
|
max: normalizePositiveInteger(max, 1),
|
|
250
302
|
blockMs: options.blockMs,
|
|
251
303
|
});
|
|
@@ -255,74 +307,46 @@ export class EnvironmentWorker {
|
|
|
255
307
|
if (!item.leaseId) {
|
|
256
308
|
throw new Error(`claimed work item 缺少 leaseId:${item.workId}`);
|
|
257
309
|
}
|
|
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
310
|
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({
|
|
311
|
+
await runLeasedItem<EnvironmentWorkExecution>({
|
|
312
|
+
renewal: {
|
|
313
|
+
intervalMs: this.input.leaseRenewIntervalMs,
|
|
314
|
+
renew: () => this.input.workQueue.renew({
|
|
309
315
|
workId: item.workId,
|
|
310
316
|
leaseId,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
} catch (error) {
|
|
317
|
+
}),
|
|
318
|
+
onError: this.input.onError,
|
|
319
|
+
},
|
|
320
|
+
execute: async () => {
|
|
321
|
+
const disposers: Array<() => Promise<void>> = [];
|
|
319
322
|
try {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
+
const handler = this.input.toolRegistry.getTool(item.toolName);
|
|
324
|
+
const execution = await this.executeHandler(handler, item, disposers);
|
|
325
|
+
return {
|
|
326
|
+
kind: 'completed',
|
|
327
|
+
result: toToolExecutionResult(item.workId, execution),
|
|
328
|
+
};
|
|
329
|
+
} catch (error) {
|
|
330
|
+
return {
|
|
331
|
+
kind: 'failed',
|
|
332
|
+
result: {
|
|
333
|
+
workId: item.workId,
|
|
334
|
+
status: 'failed',
|
|
335
|
+
error: toWorkerErrorPayload(error),
|
|
336
|
+
},
|
|
337
|
+
cause: error,
|
|
338
|
+
};
|
|
339
|
+
} finally {
|
|
340
|
+
await Promise.all(disposers.map(async (dispose) => {
|
|
341
|
+
await dispose().catch(() => undefined);
|
|
342
|
+
}));
|
|
323
343
|
}
|
|
324
|
-
}
|
|
325
|
-
|
|
344
|
+
},
|
|
345
|
+
complete: ({ result }) => this.input.workQueue.complete(result, { leaseId }),
|
|
346
|
+
completeRejectedError: (execution) => execution.kind === 'failed'
|
|
347
|
+
? new Error(`work complete 未被接受:${item.workId}`, { cause: execution.cause })
|
|
348
|
+
: new Error(`work complete 未被接受:${item.workId}`),
|
|
349
|
+
});
|
|
326
350
|
}
|
|
327
351
|
|
|
328
352
|
private async executeHandler(
|
|
@@ -367,7 +391,7 @@ export class EnvironmentWorker {
|
|
|
367
391
|
};
|
|
368
392
|
|
|
369
393
|
return {
|
|
370
|
-
session: buildSyntheticSession(item, harness.id,
|
|
394
|
+
session: buildSyntheticSession(item, harness.id, item.environmentId),
|
|
371
395
|
harness: {
|
|
372
396
|
id: harness.id,
|
|
373
397
|
version: harness.version,
|
|
@@ -437,7 +461,7 @@ export class EnvironmentWorker {
|
|
|
437
461
|
};
|
|
438
462
|
const common = {
|
|
439
463
|
baseUrl: proxy.baseUrl,
|
|
440
|
-
environmentId:
|
|
464
|
+
environmentId: item.environmentId,
|
|
441
465
|
environmentKey: proxy.environmentKey,
|
|
442
466
|
implementationId,
|
|
443
467
|
audit,
|
|
@@ -445,9 +469,6 @@ export class EnvironmentWorker {
|
|
|
445
469
|
signal: proxy.signal,
|
|
446
470
|
requestTimeoutMs: proxy.requestTimeoutMs,
|
|
447
471
|
};
|
|
448
|
-
if (slotName === 'database') {
|
|
449
|
-
return createPlatformDatabaseProxyResource(common) as Database;
|
|
450
|
-
}
|
|
451
472
|
if (slotName === 'skills') {
|
|
452
473
|
return createPlatformResourceProxy<SkillResource>({
|
|
453
474
|
...common,
|
|
@@ -463,17 +484,6 @@ export class EnvironmentWorker {
|
|
|
463
484
|
throw new Error(`platform resource proxy 不支持 slot:${slotName}`);
|
|
464
485
|
}
|
|
465
486
|
|
|
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
487
|
}
|
|
478
488
|
|
|
479
489
|
/**
|
|
@@ -488,8 +498,7 @@ export class ContextResolverWorker {
|
|
|
488
498
|
public async drainOnce(): Promise<number> {
|
|
489
499
|
const executionMode = this.input.executionMode ?? 'self_hosted';
|
|
490
500
|
const items = await this.input.contextResolveQueue.claim({
|
|
491
|
-
|
|
492
|
-
environmentId: this.input.environmentId,
|
|
501
|
+
scope: this.input.scope ?? {},
|
|
493
502
|
executionMode,
|
|
494
503
|
max: this.input.maxBatchSize ?? 1,
|
|
495
504
|
});
|
|
@@ -499,36 +508,52 @@ export class ContextResolverWorker {
|
|
|
499
508
|
}
|
|
500
509
|
|
|
501
510
|
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,
|
|
511
|
+
const renew = this.input.contextResolveQueue.renew;
|
|
512
|
+
const leaseId = item.leaseId;
|
|
513
|
+
await runLeasedItem<ContextResolveResult>({
|
|
514
|
+
...(leaseId && renew ? {
|
|
515
|
+
renewal: {
|
|
516
|
+
intervalMs: this.input.leaseRenewIntervalMs,
|
|
517
|
+
renew: () => renew({
|
|
518
|
+
workId: item.workId,
|
|
519
|
+
leaseId,
|
|
520
|
+
scope: item.scope,
|
|
521
|
+
executionMode: this.input.executionMode ?? item.executionMode ?? 'self_hosted',
|
|
522
|
+
}),
|
|
523
|
+
onError: this.input.onError,
|
|
518
524
|
},
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
525
|
+
} : {}),
|
|
526
|
+
execute: async () => {
|
|
527
|
+
try {
|
|
528
|
+
return {
|
|
529
|
+
workId: item.workId,
|
|
530
|
+
...(item.leaseId ? { leaseId: item.leaseId } : {}),
|
|
531
|
+
status: 'completed',
|
|
532
|
+
snapshot: await this.input.resolvePromptContext(item),
|
|
533
|
+
};
|
|
534
|
+
} catch (error) {
|
|
535
|
+
return {
|
|
536
|
+
workId: item.workId,
|
|
537
|
+
...(item.leaseId ? { leaseId: item.leaseId } : {}),
|
|
538
|
+
status: 'failed',
|
|
539
|
+
error: {
|
|
540
|
+
message: normalizeContextResolverError(error),
|
|
541
|
+
retryable: false,
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
complete: (result) => this.input.contextResolveQueue.complete(
|
|
547
|
+
result,
|
|
548
|
+
this.buildCompleteScope(item),
|
|
549
|
+
),
|
|
550
|
+
completeRejectedError: () => new Error(`context work complete 未被接受:${item.workId}`),
|
|
551
|
+
});
|
|
526
552
|
}
|
|
527
553
|
|
|
528
554
|
private buildCompleteScope(item: ContextResolveWorkItem) {
|
|
529
555
|
return {
|
|
530
|
-
|
|
531
|
-
environmentId: this.input.environmentId,
|
|
556
|
+
scope: item.scope,
|
|
532
557
|
executionMode: this.input.executionMode ?? item.executionMode ?? 'self_hosted',
|
|
533
558
|
leaseId: item.leaseId,
|
|
534
559
|
};
|
|
@@ -539,22 +564,9 @@ export async function runContextResolverWorkerLoop(input: RunContextResolverWork
|
|
|
539
564
|
const worker = new ContextResolverWorker(input);
|
|
540
565
|
const idleDelayMs = input.idleDelayMs ?? 250;
|
|
541
566
|
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
567
|
|
|
546
568
|
while (!input.abortSignal?.aborted) {
|
|
547
569
|
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
570
|
const drained = await worker.drainOnce();
|
|
559
571
|
if (drained === 0) {
|
|
560
572
|
await delay(idleDelayMs, input.abortSignal);
|
|
@@ -570,23 +582,12 @@ export async function runEnvironmentWorkerLoop(input: RunEnvironmentWorkerLoopIn
|
|
|
570
582
|
const worker = new EnvironmentWorker(input);
|
|
571
583
|
const idleDelayMs = input.idleDelayMs ?? 250;
|
|
572
584
|
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
585
|
const blockMs = normalizeNonNegativeInteger(input.blockMs, DEFAULT_CLAIM_BLOCK_MS);
|
|
576
586
|
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
587
|
const active = new Set<Promise<void>>();
|
|
586
588
|
|
|
587
589
|
const startWork = (item: ToolWorkItem) => {
|
|
588
|
-
|
|
589
|
-
tracked = worker.handleItem(item)
|
|
590
|
+
const tracked = worker.handleItem(item)
|
|
590
591
|
.catch(async (error) => {
|
|
591
592
|
await input.onError?.(error);
|
|
592
593
|
})
|
|
@@ -599,23 +600,13 @@ export async function runEnvironmentWorkerLoop(input: RunEnvironmentWorkerLoopIn
|
|
|
599
600
|
try {
|
|
600
601
|
while (!input.abortSignal?.aborted) {
|
|
601
602
|
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
603
|
if (input.sandboxManager && input.sandboxIdleTtlMs !== undefined) {
|
|
613
604
|
await input.sandboxManager.sweepIdle(input.sandboxIdleTtlMs);
|
|
614
605
|
}
|
|
615
606
|
|
|
616
607
|
const availableSlots = maxConcurrency - active.size;
|
|
617
608
|
if (availableSlots <= 0) {
|
|
618
|
-
await
|
|
609
|
+
await waitForActiveWorkOrDelay(active, idleDelayMs, input.abortSignal);
|
|
619
610
|
continue;
|
|
620
611
|
}
|
|
621
612
|
|
|
@@ -643,13 +634,14 @@ export async function runEnvironmentWorkerLoop(input: RunEnvironmentWorkerLoopIn
|
|
|
643
634
|
|
|
644
635
|
export type HttpWorkQueueClientInput = {
|
|
645
636
|
baseUrl: string;
|
|
646
|
-
environmentId: string;
|
|
647
637
|
environmentKey: string;
|
|
648
|
-
|
|
638
|
+
workerInstanceId: string;
|
|
649
639
|
fetchImpl?: typeof fetch;
|
|
650
640
|
signal?: AbortSignal;
|
|
651
641
|
};
|
|
652
642
|
|
|
643
|
+
export type HttpContextResolveQueueClientInput = Omit<HttpWorkQueueClientInput, 'workerInstanceId'>;
|
|
644
|
+
|
|
653
645
|
export function zodObjectToJsonSchema(schema: z.ZodType<unknown>): Record<string, unknown> {
|
|
654
646
|
const jsonSchema = z.toJSONSchema(schema) as Record<string, unknown>;
|
|
655
647
|
const normalized = normalizeCatalogJsonSchema(jsonSchema, true);
|
|
@@ -698,59 +690,60 @@ function preserveCatalogUiMetadata(
|
|
|
698
690
|
return target;
|
|
699
691
|
}
|
|
700
692
|
|
|
701
|
-
export class HttpWorkQueueClient implements
|
|
702
|
-
private readonly
|
|
703
|
-
private
|
|
693
|
+
export class HttpWorkQueueClient implements WorkerWorkQueue {
|
|
694
|
+
private readonly transport: WorkerHttpTransport;
|
|
695
|
+
private identityPromise?: Promise<{ branchId: string; environmentId: string }>;
|
|
704
696
|
|
|
705
697
|
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 执行');
|
|
698
|
+
this.transport = new WorkerHttpTransport(input);
|
|
712
699
|
}
|
|
713
700
|
|
|
714
|
-
public async claim(input: {
|
|
715
|
-
this.
|
|
716
|
-
const data = await this.requestJson('/work/claim', {
|
|
701
|
+
public async claim(input: { blockMs?: number; max?: number }) {
|
|
702
|
+
const identity = await this.identity();
|
|
703
|
+
const data = await this.transport.requestJson('/work/claim', {
|
|
717
704
|
method: 'POST',
|
|
718
705
|
signal: this.input.signal,
|
|
719
706
|
body: JSON.stringify({
|
|
707
|
+
workerInstanceId: this.input.workerInstanceId,
|
|
720
708
|
blockMs: input.blockMs,
|
|
721
709
|
max: input.max,
|
|
722
|
-
executionMode: input.executionMode,
|
|
723
710
|
}),
|
|
724
|
-
});
|
|
711
|
+
}, '平台工作队列响应不是 JSON 对象');
|
|
725
712
|
|
|
726
713
|
const record = expectRecord(data, 'claim 响应缺少 data');
|
|
727
714
|
const items = record.items;
|
|
728
715
|
if (!Array.isArray(items)) {
|
|
729
716
|
throw new Error('claim 响应缺少 items 数组');
|
|
730
717
|
}
|
|
731
|
-
return items.map(parseToolWorkItem);
|
|
718
|
+
return items.map((item) => parseToolWorkItem(item, identity.environmentId));
|
|
732
719
|
}
|
|
733
720
|
|
|
734
|
-
public async renew(input:
|
|
735
|
-
this.
|
|
736
|
-
const data = await this.requestJson('/work/renew', {
|
|
721
|
+
public async renew(input: WorkerRenewWorkItemLeaseInput): Promise<boolean> {
|
|
722
|
+
const data = await this.transport.requestJson('/work/renew', {
|
|
737
723
|
method: 'POST',
|
|
738
724
|
signal: this.input.signal,
|
|
739
|
-
body: JSON.stringify({
|
|
740
|
-
|
|
725
|
+
body: JSON.stringify({
|
|
726
|
+
workId: input.workId,
|
|
727
|
+
leaseId: input.leaseId,
|
|
728
|
+
workerInstanceId: this.input.workerInstanceId,
|
|
729
|
+
}),
|
|
730
|
+
}, '平台工作队列响应不是 JSON 对象');
|
|
741
731
|
const record = expectRecord(data, 'renew 响应缺少 data');
|
|
742
732
|
return record.renewed === true;
|
|
743
733
|
}
|
|
744
734
|
|
|
745
|
-
public async complete(result: ToolExecutionResult, scope
|
|
746
|
-
if (!scope
|
|
735
|
+
public async complete(result: ToolExecutionResult, scope: WorkerCompleteWorkItemScope): Promise<boolean> {
|
|
736
|
+
if (!scope.leaseId) {
|
|
747
737
|
throw new Error(`work complete 缺少 leaseId:${result.workId}`);
|
|
748
738
|
}
|
|
749
|
-
this.
|
|
750
|
-
const data = await this.requestJson('/work/complete', {
|
|
739
|
+
const data = await this.transport.requestJson('/work/complete', {
|
|
751
740
|
method: 'POST',
|
|
752
|
-
body: JSON.stringify({
|
|
753
|
-
|
|
741
|
+
body: JSON.stringify({
|
|
742
|
+
...result,
|
|
743
|
+
leaseId: scope.leaseId,
|
|
744
|
+
workerInstanceId: this.input.workerInstanceId,
|
|
745
|
+
}),
|
|
746
|
+
}, '平台工作队列响应不是 JSON 对象');
|
|
754
747
|
const record = expectRecord(data, 'complete 响应缺少 data');
|
|
755
748
|
if (record.accepted !== true) {
|
|
756
749
|
throw new Error(`work complete 未被接受:${result.workId}`);
|
|
@@ -758,76 +751,70 @@ export class HttpWorkQueueClient implements WorkQueue {
|
|
|
758
751
|
return true;
|
|
759
752
|
}
|
|
760
753
|
|
|
761
|
-
public async
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
754
|
+
public async register(registration: WorkerCapabilityRegistration) {
|
|
755
|
+
if (registration.workerInstanceId !== this.input.workerInstanceId) {
|
|
756
|
+
throw new Error('Worker 注册身份与 client 实例不一致');
|
|
757
|
+
}
|
|
758
|
+
const { workerInstanceId: _workerInstanceId, ...body } = registration;
|
|
759
|
+
const data = await this.transport.requestJson(`/instances/${encodeURIComponent(this.input.workerInstanceId)}`, {
|
|
760
|
+
method: 'PUT',
|
|
765
761
|
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);
|
|
762
|
+
body: JSON.stringify(body),
|
|
763
|
+
}, '平台工作队列响应不是 JSON 对象');
|
|
764
|
+
const record = expectRecord(data, 'Worker 注册响应缺少 data');
|
|
765
|
+
return {
|
|
766
|
+
workerInstanceId: expectString(record.workerInstanceId, 'workerInstanceId'),
|
|
767
|
+
expiresAt: expectString(record.expiresAt, 'expiresAt'),
|
|
768
|
+
};
|
|
779
769
|
}
|
|
780
770
|
|
|
781
|
-
|
|
782
|
-
if (
|
|
783
|
-
throw new Error(
|
|
784
|
-
}
|
|
785
|
-
if (environmentId !== this.input.environmentId) {
|
|
786
|
-
throw new Error(`worker environmentId 不匹配:${environmentId}`);
|
|
771
|
+
public async heartbeat(workerInstanceId: string) {
|
|
772
|
+
if (workerInstanceId !== this.input.workerInstanceId) {
|
|
773
|
+
throw new Error('Worker 心跳身份与 client 实例不一致');
|
|
787
774
|
}
|
|
775
|
+
const data = await this.transport.requestJson(`/instances/${encodeURIComponent(workerInstanceId)}/heartbeat`, {
|
|
776
|
+
method: 'POST',
|
|
777
|
+
signal: this.input.signal,
|
|
778
|
+
}, '平台工作队列响应不是 JSON 对象');
|
|
779
|
+
const record = expectRecord(data, 'Worker 心跳响应缺少 data');
|
|
780
|
+
return {
|
|
781
|
+
workerInstanceId: expectString(record.workerInstanceId, 'workerInstanceId'),
|
|
782
|
+
expiresAt: expectString(record.expiresAt, 'expiresAt'),
|
|
783
|
+
};
|
|
788
784
|
}
|
|
789
785
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
786
|
+
public identity() {
|
|
787
|
+
this.identityPromise ??= this.transport.requestJson('/identity', {
|
|
788
|
+
method: 'GET',
|
|
789
|
+
signal: this.input.signal,
|
|
790
|
+
}, '平台工作队列响应不是 JSON 对象').then((data) => {
|
|
791
|
+
const record = expectRecord(data, 'Worker identity 响应缺少 data');
|
|
792
|
+
const identity = expectRecord(record.identity, 'Worker identity 响应缺少 identity');
|
|
793
|
+
return {
|
|
794
|
+
branchId: expectString(identity.branchId, 'identity.branchId'),
|
|
795
|
+
environmentId: expectString(identity.environmentId, 'identity.environmentId'),
|
|
796
|
+
};
|
|
800
797
|
});
|
|
801
|
-
return
|
|
798
|
+
return this.identityPromise;
|
|
802
799
|
}
|
|
803
800
|
|
|
804
|
-
private buildUrl(path: string): string {
|
|
805
|
-
return `${this.baseUrl}/v1/environments/${encodeURIComponent(this.input.environmentId)}${path}`;
|
|
806
|
-
}
|
|
807
801
|
}
|
|
808
802
|
|
|
809
|
-
export class HttpContextResolveQueueClient implements
|
|
810
|
-
private readonly
|
|
811
|
-
private readonly fetchImpl: typeof fetch;
|
|
803
|
+
export class HttpContextResolveQueueClient implements WorkerContextResolveQueue {
|
|
804
|
+
private readonly transport: WorkerHttpTransport;
|
|
812
805
|
|
|
813
|
-
public constructor(private readonly input:
|
|
814
|
-
this.
|
|
815
|
-
this.fetchImpl = input.fetchImpl ?? fetch;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
public async enqueue(): Promise<void> {
|
|
819
|
-
throw new Error('HTTP context resolver client 不能 enqueue;enqueue 只能由平台 Harness 执行');
|
|
806
|
+
public constructor(private readonly input: HttpContextResolveQueueClientInput) {
|
|
807
|
+
this.transport = new WorkerHttpTransport(input);
|
|
820
808
|
}
|
|
821
809
|
|
|
822
|
-
public async claim(input:
|
|
823
|
-
this.
|
|
824
|
-
const data = await this.requestJson('/context-work/claim', {
|
|
810
|
+
public async claim(input: ContextResolveQueueClaimInput) {
|
|
811
|
+
const data = await this.transport.requestJson('/context-work/claim', {
|
|
825
812
|
method: 'POST',
|
|
826
813
|
signal: this.input.signal,
|
|
827
814
|
body: JSON.stringify({
|
|
828
815
|
max: input.max,
|
|
829
816
|
}),
|
|
830
|
-
});
|
|
817
|
+
}, '平台 context 工作队列响应不是 JSON 对象');
|
|
831
818
|
|
|
832
819
|
const record = expectRecord(data, 'context claim 响应缺少 data');
|
|
833
820
|
const items = record.items;
|
|
@@ -838,10 +825,10 @@ export class HttpContextResolveQueueClient implements ContextResolveQueue {
|
|
|
838
825
|
}
|
|
839
826
|
|
|
840
827
|
public async complete(result: ContextResolveResult): Promise<boolean> {
|
|
841
|
-
const data = await this.requestJson('/context-work/complete', {
|
|
828
|
+
const data = await this.transport.requestJson('/context-work/complete', {
|
|
842
829
|
method: 'POST',
|
|
843
830
|
body: JSON.stringify(result),
|
|
844
|
-
});
|
|
831
|
+
}, '平台 context 工作队列响应不是 JSON 对象');
|
|
845
832
|
const record = expectRecord(data, 'context complete 响应缺少 data');
|
|
846
833
|
if (record.accepted !== true) {
|
|
847
834
|
throw new Error(`context complete 未被接受:${result.workId}`);
|
|
@@ -849,69 +836,20 @@ export class HttpContextResolveQueueClient implements ContextResolveQueue {
|
|
|
849
836
|
return true;
|
|
850
837
|
}
|
|
851
838
|
|
|
852
|
-
public async
|
|
853
|
-
this.
|
|
854
|
-
const data = await this.requestJson('/context-work/reclaim', {
|
|
839
|
+
public async renew(input: { workId: string; leaseId: string }): Promise<boolean> {
|
|
840
|
+
const data = await this.transport.requestJson('/context-work/renew', {
|
|
855
841
|
method: 'POST',
|
|
856
842
|
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}`;
|
|
843
|
+
body: JSON.stringify({ workId: input.workId, leaseId: input.leaseId }),
|
|
844
|
+
}, '平台 context 工作队列响应不是 JSON 对象');
|
|
845
|
+
const record = expectRecord(data, 'context renew 响应缺少 data');
|
|
846
|
+
return record.renewed === true;
|
|
896
847
|
}
|
|
897
|
-
}
|
|
898
848
|
|
|
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
849
|
}
|
|
911
850
|
|
|
912
851
|
function toToolExecutionResult(
|
|
913
852
|
workId: string,
|
|
914
|
-
handler: ToolHandler<unknown, unknown>,
|
|
915
853
|
execution: ToolExecution<unknown>,
|
|
916
854
|
): ToolExecutionResult {
|
|
917
855
|
if (execution.status === 'failed') {
|
|
@@ -934,22 +872,20 @@ function toToolExecutionResult(
|
|
|
934
872
|
return {
|
|
935
873
|
workId,
|
|
936
874
|
status: 'completed',
|
|
937
|
-
modelResult:
|
|
875
|
+
modelResult: assertToolResultWireValue(execution.modelResult),
|
|
938
876
|
effects: execution.effects,
|
|
939
877
|
};
|
|
940
878
|
}
|
|
941
879
|
|
|
942
|
-
function buildSyntheticSession(item: ToolWorkItem, harnessId: string,
|
|
880
|
+
function buildSyntheticSession(item: ToolWorkItem, harnessId: string, environmentId: string): SessionRecord {
|
|
943
881
|
const now = new Date().toISOString();
|
|
944
882
|
return {
|
|
945
883
|
id: item.sessionId as SessionId,
|
|
946
|
-
appId: item.appId,
|
|
947
884
|
scope: item.scope,
|
|
885
|
+
configuration: { status: 'available', configVersionId: item.configVersionId },
|
|
948
886
|
rootSessionId: item.rootSessionId as SessionId,
|
|
949
887
|
harnessId: harnessId as HarnessId,
|
|
950
|
-
|
|
951
|
-
environmentId: item.environmentId as EnvironmentId,
|
|
952
|
-
environmentUpdatedAt: now,
|
|
888
|
+
environmentId: environmentId as EnvironmentId,
|
|
953
889
|
title: item.sessionId,
|
|
954
890
|
metadata: item.sessionMetadata ?? {},
|
|
955
891
|
createdAt: now,
|
|
@@ -988,14 +924,35 @@ function hasDispose(value: unknown): value is { dispose(): Promise<void> | void
|
|
|
988
924
|
return isRecord(value) && typeof value.dispose === 'function';
|
|
989
925
|
}
|
|
990
926
|
|
|
991
|
-
function parseToolWorkItem(value: unknown): ToolWorkItem {
|
|
927
|
+
function parseToolWorkItem(value: unknown, environmentId: string): ToolWorkItem {
|
|
992
928
|
const record = expectRecord(value, 'work item 不是对象');
|
|
993
929
|
const workId = expectString(record.workId, 'workId');
|
|
994
930
|
const leaseId = typeof record.leaseId === 'string' && record.leaseId.trim()
|
|
995
931
|
? record.leaseId.trim()
|
|
996
932
|
: undefined;
|
|
997
|
-
const
|
|
998
|
-
const
|
|
933
|
+
const configVersionId = expectString(record.configVersionId, 'configVersionId');
|
|
934
|
+
const requiredContractDigest = expectString(record.requiredContractDigest, 'requiredContractDigest');
|
|
935
|
+
if (!Array.isArray(record.requiredResourceCapabilities)) {
|
|
936
|
+
throw new Error('requiredResourceCapabilities 必须是数组');
|
|
937
|
+
}
|
|
938
|
+
const requiredResourceCapabilities = record.requiredResourceCapabilities.map((value, index) => {
|
|
939
|
+
const capability = expectRecord(value, `requiredResourceCapabilities[${index}]`);
|
|
940
|
+
return {
|
|
941
|
+
slot: expectString(capability.slot, `requiredResourceCapabilities[${index}].slot`),
|
|
942
|
+
implementationId: expectString(
|
|
943
|
+
capability.implementationId,
|
|
944
|
+
`requiredResourceCapabilities[${index}].implementationId`,
|
|
945
|
+
),
|
|
946
|
+
};
|
|
947
|
+
});
|
|
948
|
+
const workerKind = record.workerKind;
|
|
949
|
+
if (workerKind !== 'self_hosted' && workerKind !== 'platform_builtin') {
|
|
950
|
+
throw new Error('workerKind 无效');
|
|
951
|
+
}
|
|
952
|
+
if (Object.prototype.hasOwnProperty.call(record, 'environmentId')
|
|
953
|
+
|| Object.prototype.hasOwnProperty.call(record, 'appId')) {
|
|
954
|
+
throw new Error('work item wire 不得携带服务端隔离字段');
|
|
955
|
+
}
|
|
999
956
|
const sessionId = expectString(record.sessionId, 'sessionId');
|
|
1000
957
|
const rootSessionId = expectString(record.rootSessionId, 'rootSessionId');
|
|
1001
958
|
const scope = parseExecutionScope(record.scope);
|
|
@@ -1006,7 +963,10 @@ function parseToolWorkItem(value: unknown): ToolWorkItem {
|
|
|
1006
963
|
const item: ToolWorkItem = {
|
|
1007
964
|
workId,
|
|
1008
965
|
...(leaseId ? { leaseId } : {}),
|
|
1009
|
-
|
|
966
|
+
configVersionId: configVersionId as ToolWorkItem['configVersionId'],
|
|
967
|
+
requiredContractDigest,
|
|
968
|
+
requiredResourceCapabilities,
|
|
969
|
+
workerKind,
|
|
1010
970
|
environmentId,
|
|
1011
971
|
sessionId,
|
|
1012
972
|
rootSessionId,
|
|
@@ -1054,35 +1014,139 @@ function parseExecutionMode(value: unknown): ToolWorkItem['executionMode'] | und
|
|
|
1054
1014
|
|
|
1055
1015
|
function parseToolResumeWorkPayload(value: unknown): NonNullable<ToolWorkItem['resume']> {
|
|
1056
1016
|
const record = expectRecord(value, 'resume 工作项缺少 resume payload');
|
|
1057
|
-
const actionEvent =
|
|
1058
|
-
const resolvedEvent =
|
|
1017
|
+
const actionEvent = parseActionRequestedEvent(record.actionEvent, 'resume.actionEvent');
|
|
1018
|
+
const resolvedEvent = parseActionResolvedEvent(record.resolvedEvent, 'resume.resolvedEvent');
|
|
1019
|
+
if (actionEvent.payload.kind !== resolvedEvent.payload.kind) {
|
|
1020
|
+
throw new Error('resume action kind 不一致');
|
|
1021
|
+
}
|
|
1059
1022
|
return {
|
|
1060
1023
|
actionEvent,
|
|
1061
1024
|
resolvedEvent,
|
|
1062
1025
|
};
|
|
1063
1026
|
}
|
|
1064
1027
|
|
|
1065
|
-
function
|
|
1028
|
+
function parseActionRequestedEvent(
|
|
1029
|
+
value: unknown,
|
|
1030
|
+
field: string,
|
|
1031
|
+
): NonNullable<ToolWorkItem['resume']>['actionEvent'] {
|
|
1032
|
+
const event = expectRecord(value, `${field} 必须是对象`);
|
|
1033
|
+
if (event.type !== 'agent.action.requested') {
|
|
1034
|
+
throw new Error(`${field} 类型必须是 agent.action.requested`);
|
|
1035
|
+
}
|
|
1036
|
+
const payload = expectRecord(event.payload, `${field}.payload 必须是对象`);
|
|
1037
|
+
const display = payload.display === undefined
|
|
1038
|
+
? undefined
|
|
1039
|
+
: parseActionDisplay(payload.display, `${field}.payload.display`);
|
|
1040
|
+
const allowedResults = parseOptionalArray(
|
|
1041
|
+
payload.allowedResults,
|
|
1042
|
+
`${field}.payload.allowedResults`,
|
|
1043
|
+
);
|
|
1044
|
+
return {
|
|
1045
|
+
...parseResumeEventCore(event, field),
|
|
1046
|
+
type: 'agent.action.requested',
|
|
1047
|
+
payload: {
|
|
1048
|
+
kind: parseActionKind(payload.kind, `${field}.payload.kind`),
|
|
1049
|
+
...(payload.prompt === undefined
|
|
1050
|
+
? {}
|
|
1051
|
+
: { prompt: expectOptionalString(payload.prompt, `${field}.payload.prompt`) }),
|
|
1052
|
+
...(Object.prototype.hasOwnProperty.call(payload, 'input') ? { input: payload.input } : {}),
|
|
1053
|
+
...(allowedResults ? { allowedResults } : {}),
|
|
1054
|
+
...(display ? { display } : {}),
|
|
1055
|
+
...(payload.expiresAt === undefined
|
|
1056
|
+
? {}
|
|
1057
|
+
: { expiresAt: expectOptionalString(payload.expiresAt, `${field}.payload.expiresAt`) }),
|
|
1058
|
+
},
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function parseActionResolvedEvent(
|
|
1066
1063
|
value: unknown,
|
|
1067
|
-
type: TType,
|
|
1068
1064
|
field: string,
|
|
1069
|
-
):
|
|
1065
|
+
): NonNullable<ToolWorkItem['resume']>['resolvedEvent'] {
|
|
1070
1066
|
const event = expectRecord(value, `${field} 必须是对象`);
|
|
1071
|
-
if (event.type !==
|
|
1072
|
-
throw new Error(`${field} 类型必须是
|
|
1067
|
+
if (event.type !== 'user.action.resolved') {
|
|
1068
|
+
throw new Error(`${field} 类型必须是 user.action.resolved`);
|
|
1073
1069
|
}
|
|
1074
|
-
|
|
1075
|
-
if (
|
|
1076
|
-
throw new Error(`${field}.
|
|
1070
|
+
const payload = expectRecord(event.payload, `${field}.payload 必须是对象`);
|
|
1071
|
+
if (!Object.prototype.hasOwnProperty.call(payload, 'result')) {
|
|
1072
|
+
throw new Error(`${field}.payload.result 缺失`);
|
|
1077
1073
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1074
|
+
return {
|
|
1075
|
+
...parseResumeEventCore(event, field),
|
|
1076
|
+
type: 'user.action.resolved',
|
|
1077
|
+
payload: {
|
|
1078
|
+
kind: parseActionKind(payload.kind, `${field}.payload.kind`),
|
|
1079
|
+
result: payload.result,
|
|
1080
|
+
},
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function parseResumeEventCore(event: Record<string, unknown>, field: string) {
|
|
1085
|
+
return {
|
|
1086
|
+
id: expectString(event.id, `${field}.id`),
|
|
1087
|
+
seq: expectNumber(event.seq, `${field}.seq`),
|
|
1088
|
+
...(event.globalSeq === undefined
|
|
1089
|
+
? {}
|
|
1090
|
+
: { globalSeq: expectNumber(event.globalSeq, `${field}.globalSeq`) }),
|
|
1091
|
+
...(event.turnId === undefined
|
|
1092
|
+
? {}
|
|
1093
|
+
: { turnId: expectString(event.turnId, `${field}.turnId`) }),
|
|
1094
|
+
...(event.parentEventId === undefined
|
|
1095
|
+
? {}
|
|
1096
|
+
: { parentEventId: expectString(event.parentEventId, `${field}.parentEventId`) }),
|
|
1097
|
+
actor: parseSessionEventActor(event.actor, `${field}.actor`),
|
|
1098
|
+
createdAt: expectString(event.createdAt, `${field}.createdAt`),
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function parseActionDisplay(value: unknown, field: string) {
|
|
1103
|
+
const display = expectRecord(value, `${field} 必须是对象`);
|
|
1104
|
+
return {
|
|
1105
|
+
...(display.title === undefined
|
|
1106
|
+
? {}
|
|
1107
|
+
: { title: expectOptionalString(display.title, `${field}.title`) }),
|
|
1108
|
+
...(display.summary === undefined
|
|
1109
|
+
? {}
|
|
1110
|
+
: { summary: expectOptionalString(display.summary, `${field}.summary`) }),
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function parseSessionEventActor(value: unknown, field: string): SessionEventActor {
|
|
1115
|
+
const actor = expectRecord(value, `${field} 必须是对象`);
|
|
1116
|
+
if (actor.type !== 'user'
|
|
1117
|
+
&& actor.type !== 'assistant'
|
|
1118
|
+
&& actor.type !== 'tool'
|
|
1119
|
+
&& actor.type !== 'system') {
|
|
1120
|
+
throw new Error(`${field}.type 无效`);
|
|
1121
|
+
}
|
|
1122
|
+
return {
|
|
1123
|
+
type: actor.type,
|
|
1124
|
+
...(actor.id === undefined ? {} : { id: expectString(actor.id, `${field}.id`) }),
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function parseActionKind(value: unknown, field: string) {
|
|
1129
|
+
if (value !== 'ask_user' && value !== 'tool_confirmation' && value !== 'custom_tool_result') {
|
|
1130
|
+
throw new Error(`${field} 无效`);
|
|
1131
|
+
}
|
|
1132
|
+
return value;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function expectOptionalString(value: unknown, field: string) {
|
|
1136
|
+
if (typeof value !== 'string') {
|
|
1137
|
+
throw new Error(`${field} 必须是字符串`);
|
|
1138
|
+
}
|
|
1139
|
+
return value;
|
|
1140
|
+
}
|
|
1081
1141
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1142
|
+
function parseOptionalArray(value: unknown, field: string): unknown[] | undefined {
|
|
1143
|
+
if (value === undefined) {
|
|
1144
|
+
return undefined;
|
|
1084
1145
|
}
|
|
1085
|
-
|
|
1146
|
+
if (!Array.isArray(value)) {
|
|
1147
|
+
throw new Error(`${field} 必须是数组`);
|
|
1148
|
+
}
|
|
1149
|
+
return value;
|
|
1086
1150
|
}
|
|
1087
1151
|
|
|
1088
1152
|
function parseExecutionScope(value: unknown): ExecutionScope {
|
|
@@ -1102,8 +1166,6 @@ function parseContextResolveWorkItem(value: unknown): ContextResolveWorkItem {
|
|
|
1102
1166
|
const leaseId = typeof record.leaseId === 'string' && record.leaseId.trim()
|
|
1103
1167
|
? record.leaseId.trim()
|
|
1104
1168
|
: undefined;
|
|
1105
|
-
const appId = expectString(record.appId, 'appId');
|
|
1106
|
-
const environmentId = expectString(record.environmentId, 'environmentId');
|
|
1107
1169
|
const sessionId = expectString(record.sessionId, 'sessionId');
|
|
1108
1170
|
const rootSessionId = expectString(record.rootSessionId, 'rootSessionId');
|
|
1109
1171
|
const request = expectRecord(record.request, 'request 必须是对象');
|
|
@@ -1125,8 +1187,6 @@ function parseContextResolveWorkItem(value: unknown): ContextResolveWorkItem {
|
|
|
1125
1187
|
return {
|
|
1126
1188
|
workId,
|
|
1127
1189
|
...(leaseId ? { leaseId } : {}),
|
|
1128
|
-
appId,
|
|
1129
|
-
environmentId,
|
|
1130
1190
|
sessionId,
|
|
1131
1191
|
rootSessionId,
|
|
1132
1192
|
scope: parseExecutionScope(record.scope),
|
|
@@ -1138,27 +1198,6 @@ function parseContextResolveWorkItem(value: unknown): ContextResolveWorkItem {
|
|
|
1138
1198
|
};
|
|
1139
1199
|
}
|
|
1140
1200
|
|
|
1141
|
-
function parseWorkStats(value: unknown): WorkStats {
|
|
1142
|
-
const record = expectRecord(value, 'stats 不是对象');
|
|
1143
|
-
const oldestQueuedAt = record.oldestQueuedAt;
|
|
1144
|
-
return {
|
|
1145
|
-
depth: expectNumber(record.depth, 'depth'),
|
|
1146
|
-
pending: expectNumber(record.pending, 'pending'),
|
|
1147
|
-
oldestQueuedAt: typeof oldestQueuedAt === 'string' ? oldestQueuedAt : null,
|
|
1148
|
-
workersPolling: expectNumber(record.workersPolling, 'workersPolling'),
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
function parseContextResolveStats(value: unknown) {
|
|
1153
|
-
const record = expectRecord(value, 'context stats 不是对象');
|
|
1154
|
-
const oldestQueuedAt = record.oldestQueuedAt;
|
|
1155
|
-
return {
|
|
1156
|
-
depth: expectNumber(record.depth, 'depth'),
|
|
1157
|
-
pending: expectNumber(record.pending, 'pending'),
|
|
1158
|
-
oldestQueuedAt: typeof oldestQueuedAt === 'string' ? oldestQueuedAt : null,
|
|
1159
|
-
};
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
1201
|
function normalizeWorkerError(error: unknown): string {
|
|
1163
1202
|
if (error instanceof Error) {
|
|
1164
1203
|
return `工具执行失败:${error.message}`;
|
|
@@ -1190,10 +1229,6 @@ function normalizeContextResolverError(error: unknown): string {
|
|
|
1190
1229
|
return `context resolve 失败:${String(error)}`;
|
|
1191
1230
|
}
|
|
1192
1231
|
|
|
1193
|
-
function trimTrailingSlash(value: string): string {
|
|
1194
|
-
return value.endsWith('/') ? value.slice(0, -1) : value;
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
1232
|
function expectString(value: unknown, field: string): string {
|
|
1198
1233
|
if (typeof value !== 'string' || !value.trim()) {
|
|
1199
1234
|
throw new Error(`字段 ${field} 必须是非空字符串`);
|
|
@@ -1235,18 +1270,6 @@ function delay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
|
1235
1270
|
});
|
|
1236
1271
|
}
|
|
1237
1272
|
|
|
1238
|
-
function waitForActiveWorkOrDeadline(
|
|
1239
|
-
active: Set<Promise<void>>,
|
|
1240
|
-
deadlineMs: number,
|
|
1241
|
-
signal?: AbortSignal,
|
|
1242
|
-
): Promise<void> {
|
|
1243
|
-
const waitMs = Math.max(1, deadlineMs - Date.now());
|
|
1244
|
-
return Promise.race([
|
|
1245
|
-
...active,
|
|
1246
|
-
delay(waitMs, signal),
|
|
1247
|
-
]).then(() => undefined);
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
1273
|
function waitForActiveWorkOrDelay(
|
|
1251
1274
|
active: Set<Promise<void>>,
|
|
1252
1275
|
delayMs: number,
|