@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/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
- WorkQueue,
23
- WorkStats,
24
- CompleteWorkItemScope,
25
- RenewWorkItemLeaseInput,
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
- createPlatformDatabaseProxyResource,
39
- createPlatformResourceProxy,
40
- } from './resources/platform-proxy.js';
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
- appId: string;
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 的续租周期,必须小于 loop 的 reclaimOlderThanMs。 */
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
- appId: string;
130
- environmentId: string;
131
- contextResolveQueue: 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
- export const DEFAULT_WORK_LEASE_RENEW_INTERVAL_MS = 60_000;
150
-
151
- const PLATFORM_PROXY_RESOURCE_SLOTS = new Set(['database', 'skills', 'memory']);
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 只通过 WorkQueue 认领当前 app/environment 的业务 tool 工作项,并在本地
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
- if (!leaseId) {
296
- return;
297
- }
298
- const intervalMs = normalizePositiveInteger(
299
- this.input.leaseRenewIntervalMs,
300
- DEFAULT_WORK_LEASE_RENEW_INTERVAL_MS,
301
- );
302
- while (!signal.aborted) {
303
- await delay(intervalMs, signal);
304
- if (signal.aborted) {
305
- return;
306
- }
307
- try {
308
- const renewed = await this.input.workQueue.renew({
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
- appId: this.input.appId,
312
- environmentId: this.input.environmentId,
313
- executionMode: this.input.executionMode ?? item.executionMode ?? 'self_hosted',
314
- });
315
- if (!renewed) {
316
- return;
317
- }
318
- } catch (error) {
317
+ }),
318
+ onError: this.input.onError,
319
+ },
320
+ execute: async () => {
321
+ const disposers: Array<() => Promise<void>> = [];
319
322
  try {
320
- await this.input.onError?.(error);
321
- } catch {
322
- // 观测回调失败不能终止仍持有 lease 的业务 handler;下一周期继续 renew。
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, harness.version),
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: this.input.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
- appId: this.input.appId,
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
- let result: ContextResolveResult;
503
- try {
504
- result = {
505
- workId: item.workId,
506
- ...(item.leaseId ? { leaseId: item.leaseId } : {}),
507
- status: 'completed',
508
- snapshot: await this.input.resolvePromptContext(item),
509
- };
510
- } catch (error) {
511
- result = {
512
- workId: item.workId,
513
- ...(item.leaseId ? { leaseId: item.leaseId } : {}),
514
- status: 'failed',
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
- const accepted = await this.input.contextResolveQueue.complete(result, this.buildCompleteScope(item));
523
- if (!accepted) {
524
- throw new Error(`context work complete 未被接受:${item.workId}`);
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
- appId: this.input.appId,
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
- let tracked: Promise<void>;
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 waitForActiveWorkOrDeadline(active, nextReclaimAtMs, input.abortSignal);
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
- appId?: string;
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 WorkQueue {
702
- private readonly baseUrl: string;
703
- private readonly fetchImpl: typeof fetch;
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.baseUrl = trimTrailingSlash(input.baseUrl);
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: { appId: string; environmentId: string; blockMs?: number; max?: number; executionMode?: EnvironmentExecutionMode }) {
715
- this.assertScope(input.appId, input.environmentId);
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: RenewWorkItemLeaseInput): Promise<boolean> {
735
- this.assertScope(input.appId, input.environmentId);
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({ workId: input.workId, leaseId: input.leaseId }),
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?: CompleteWorkItemScope): Promise<boolean> {
746
- if (!scope?.leaseId) {
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.assertScope(scope.appId, scope.environmentId);
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({ ...result, leaseId: scope.leaseId }),
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 reclaim(input: { appId: string; environmentId: string; olderThanMs: number; executionMode?: EnvironmentExecutionMode }): Promise<number> {
762
- this.assertScope(input.appId, input.environmentId);
763
- const data = await this.requestJson('/work/reclaim', {
764
- method: 'POST',
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({ olderThanMs: input.olderThanMs, executionMode: input.executionMode }),
767
- });
768
-
769
- const record = expectRecord(data, 'reclaim 响应缺少 data');
770
- return typeof record.reclaimed === 'number' ? record.reclaimed : 0;
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
- private assertScope(appId: string, environmentId: string): void {
782
- if (this.input.appId && appId !== this.input.appId) {
783
- throw new Error(`worker appId 不匹配:${appId}`);
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
- private async requestJson(path: string, init: RequestInit): Promise<unknown> {
791
- const headers = new Headers(init.headers);
792
- headers.set('x-api-key', this.input.environmentKey);
793
- if (init.body && !headers.has('content-type')) {
794
- headers.set('content-type', 'application/json');
795
- }
796
-
797
- const response = await this.fetchImpl(this.buildUrl(path), {
798
- ...init,
799
- headers,
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 parseApiEnvelope(response, `平台工作队列响应不是 JSON 对象:${response.status}`);
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 ContextResolveQueue {
810
- private readonly baseUrl: string;
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: HttpWorkQueueClientInput) {
814
- this.baseUrl = trimTrailingSlash(input.baseUrl);
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: { appId: string; environmentId: string; max?: number }) {
823
- this.assertScope(input.appId, input.environmentId);
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 reclaim(input: { appId: string; environmentId: string; olderThanMs: number }): Promise<number> {
853
- this.assertScope(input.appId, input.environmentId);
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({ olderThanMs: input.olderThanMs }),
858
- });
859
-
860
- const record = expectRecord(data, 'context reclaim 响应缺少 data');
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: handler.resultSchema.parse(execution.modelResult),
875
+ modelResult: assertToolResultWireValue(execution.modelResult),
938
876
  effects: execution.effects,
939
877
  };
940
878
  }
941
879
 
942
- function buildSyntheticSession(item: ToolWorkItem, harnessId: string, harnessVersion: number): SessionRecord {
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
- harnessVersion,
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 appId = expectString(record.appId, 'appId');
998
- const environmentId = expectString(record.environmentId, 'environmentId');
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
- appId,
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 = parseResumeEvent(record.actionEvent, 'agent.action.requested', 'resume.actionEvent');
1058
- const resolvedEvent = parseResumeEvent(record.resolvedEvent, 'user.action.resolved', 'resume.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 parseResumeEvent<TType extends NonNullable<ToolWorkItem['resume']>['actionEvent']['type'] | NonNullable<ToolWorkItem['resume']>['resolvedEvent']['type']>(
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
- ): Extract<NonNullable<ToolWorkItem['resume']>['actionEvent'] | NonNullable<ToolWorkItem['resume']>['resolvedEvent'], { type: TType }> {
1065
+ ): NonNullable<ToolWorkItem['resume']>['resolvedEvent'] {
1070
1066
  const event = expectRecord(value, `${field} 必须是对象`);
1071
- if (event.type !== type) {
1072
- throw new Error(`${field} 类型必须是 ${type}`);
1067
+ if (event.type !== 'user.action.resolved') {
1068
+ throw new Error(`${field} 类型必须是 user.action.resolved`);
1073
1069
  }
1074
- expectString(event.id, `${field}.id`);
1075
- if (typeof event.seq !== 'number' || !Number.isFinite(event.seq)) {
1076
- throw new Error(`${field}.seq 必须是有限数字`);
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
- expectRecord(event.actor, `${field}.actor 必须是对象`);
1079
- expectString(event.createdAt, `${field}.createdAt`);
1080
- expectRecord(event.payload, `${field}.payload 必须是对象`);
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
- if (type === 'agent.action.requested') {
1083
- return event as unknown as Extract<NonNullable<ToolWorkItem['resume']>['actionEvent'], { type: TType }>;
1142
+ function parseOptionalArray(value: unknown, field: string): unknown[] | undefined {
1143
+ if (value === undefined) {
1144
+ return undefined;
1084
1145
  }
1085
- return event as unknown as Extract<NonNullable<ToolWorkItem['resume']>['resolvedEvent'], { type: TType }>;
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,