@ct-agents/worker 0.1.8 → 0.2.0

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