effect-agent 0.1.0-beta.150 → 0.1.0-beta.152

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.
Files changed (51) hide show
  1. package/dist/{Admin-B8ISZyYf.d.mts → Admin-CKIdJoP8.d.mts} +6 -5
  2. package/dist/{DurableAgentRuntime-DLa4IwFi.mjs → DurableAgentRuntime-DQFV26SB.mjs} +68 -82
  3. package/dist/DurableAgentRuntime-DQFV26SB.mjs.map +1 -0
  4. package/dist/{EventSource-CkN05NT3.d.mts → EventSource-CMbXXmwe.d.mts} +2 -2
  5. package/dist/{PreparedInputAdmission-fctZOTnn.d.mts → PreparedInputAdmission-Bt6rgtVu.d.mts} +3 -3
  6. package/dist/{Subscription-B16A-48F.d.mts → Subscription-BJAzYNw1.d.mts} +2 -2
  7. package/dist/{SubscriptionInput-DIU7CiXE.d.mts → SubscriptionInput-CkBJt0AS.d.mts} +2 -2
  8. package/dist/WorkerAdmission-CJ_oWRuS.mjs +171 -0
  9. package/dist/WorkerAdmission-CJ_oWRuS.mjs.map +1 -0
  10. package/dist/durable/Admin.d.mts +1 -1
  11. package/dist/durable/AgentRegistration.d.mts +1 -1
  12. package/dist/durable/Certification.d.mts +1 -1
  13. package/dist/durable/CommittedActivity.d.mts +1 -1
  14. package/dist/durable/DurableAgentRuntime.d.mts +1 -1
  15. package/dist/durable/DurableAgentRuntime.mjs +2 -1
  16. package/dist/durable/EventSource.d.mts +1 -1
  17. package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
  18. package/dist/durable/LifecyclePublication.d.mts +1 -1
  19. package/dist/durable/MessageDelivery.d.mts +41 -41
  20. package/dist/durable/MessageDeliveryStoreConformance.d.mts +1 -1
  21. package/dist/durable/PersistentHistory.d.mts +1 -1
  22. package/dist/durable/PreparedInputAdmission.d.mts +1 -1
  23. package/dist/durable/Recovery.d.mts +1 -1
  24. package/dist/durable/RunJournal.d.mts +1 -1
  25. package/dist/durable/Scheduling.d.mts +1 -1
  26. package/dist/durable/SubmissionLedger.d.mts +1 -1
  27. package/dist/durable/SubmissionLedgerConformance.d.mts +1 -1
  28. package/dist/durable/SubmissionStatus.d.mts +1 -1
  29. package/dist/durable/Subscription.d.mts +1 -1
  30. package/dist/durable/SubscriptionInput.d.mts +1 -1
  31. package/dist/durable/SubscriptionStoreConformance.d.mts +1 -1
  32. package/dist/durable/SubscriptionTools.d.mts +2 -2
  33. package/dist/durable/SubscriptionTransition.d.mts +1 -1
  34. package/dist/durable/Subscriptions.d.mts +5 -5
  35. package/dist/durable/ThreadContextHistory.d.mts +1 -1
  36. package/dist/durable/ThreadInvariants.d.mts +1 -1
  37. package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
  38. package/dist/durable/ThreadStore.d.mts +1 -1
  39. package/dist/durable/ThreadStoreConformance.d.mts +1 -1
  40. package/dist/durable/WorkerAdmission.d.mts +172 -0
  41. package/dist/durable/WorkerAdmission.mjs +2 -0
  42. package/dist/durable/WorkerHost.d.mts +1 -1
  43. package/dist/index.d.mts +7 -6
  44. package/dist/index.mjs +3 -2
  45. package/package.json +1 -1
  46. package/src/durable/DurableAgentRuntime.ts +79 -109
  47. package/src/durable/WorkerAdmission.ts +104 -0
  48. package/src/durable/internal/thread-initialization.ts +195 -0
  49. package/src/durable/internal/worker-host.ts +121 -123
  50. package/src/index.ts +1 -0
  51. package/dist/DurableAgentRuntime-DLa4IwFi.mjs.map +0 -1
@@ -72,7 +72,6 @@ import {
72
72
  WorkerStopRequested,
73
73
  WorkerOrigin,
74
74
  WorkerContinuation,
75
- WorkerOriginRecorded,
76
75
  WorkerInputCompleted,
77
76
  WorkerReportPrepared,
78
77
  WorkerReportRefused,
@@ -137,6 +136,7 @@ import {
137
136
  } from "./agent-registration.ts";
138
137
  import { lastWorkerReportMessageId } from "./agent-updates.ts";
139
138
  import { messageStatus } from "./message-status.ts";
139
+ import { ensureWorkerOrigin } from "./thread-initialization.ts";
140
140
 
141
141
  const failure = (
142
142
  operation: WorkerError["operation"],
@@ -570,14 +570,13 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
570
570
  payload:
571
571
  | WorkerStopRequested
572
572
  | WorkerInputRequested
573
- | WorkerOriginRecorded
574
573
  | WorkerInputCompleted
575
574
  | WorkerReportPrepared
576
575
  | WorkerReportRefused
577
576
  | SubtreeBudgetReserved,
578
577
  current: Pick<ThreadExport, "tailSequence" | "tailDigest" | "records"> &
579
578
  Partial<Pick<ThreadTail, "producerEpoch">>,
580
- phase: "source" | "origin" | "completion" | "subtree" | "report" | "stop",
579
+ phase: "source" | "completion" | "subtree" | "report" | "stop",
581
580
  acknowledgements: ReadonlyArray<WorkerInputCompleted> = [],
582
581
  ) {
583
582
  const operation = "start";
@@ -612,11 +611,9 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
612
611
  createdAt: DateTime.makeUnsafe(
613
612
  payload._tag === "WorkerInputRequested"
614
613
  ? payload.admission.createdAtMillis
615
- : payload._tag === "WorkerOriginRecorded"
616
- ? payload.origin.createdAtMillis
617
- : payload._tag === "WorkerInputCompleted"
618
- ? payload.completedAtMillis
619
- : yield* Clock.currentTimeMillis,
614
+ : payload._tag === "WorkerInputCompleted"
615
+ ? payload.completedAtMillis
616
+ : yield* Clock.currentTimeMillis,
620
617
  ),
621
618
  deploymentId: deps.deploymentId,
622
619
  payload,
@@ -654,49 +651,55 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
654
651
  record.payload._tag === "WorkerInputRequested" ? [record.payload] : [],
655
652
  );
656
653
 
657
- // Completion is published in the child's own journal. Only the source, while
658
- // admitting work, reads those exact receipts to release its capacity reservations.
659
- const completedInputs = Effect.fn("WorkerHost.completedInputs")(function* (
660
- rows: ReadonlyArray<WorkerInputRequested>,
661
- sourceRecords: ThreadExport["records"],
662
- ) {
663
- const completed = new Set<string>(
664
- sourceRecords.flatMap(({ record }) =>
654
+ const completedMessageIds = (records: ThreadExport["records"]) =>
655
+ new Set<string>(
656
+ records.flatMap(({ record }) =>
665
657
  record.payload._tag === "WorkerInputCompleted" && record.payload.effectsResolved
666
658
  ? [record.payload.messageId]
667
659
  : [],
668
660
  ),
669
661
  );
670
662
 
671
- const acknowledgements: Array<WorkerInputCompleted> = [];
663
+ // Read exact child receipts only under capacity pressure. Collect in source order so
664
+ // parallel foreign reads cannot change the canonical acknowledgement batch order.
665
+ const completedInputs = Effect.fn("WorkerHost.completedInputs")(function* (
666
+ rows: ReadonlyArray<WorkerInputRequested>,
667
+ sourceRecords: ThreadExport["records"],
668
+ ) {
669
+ const completed = completedMessageIds(sourceRecords);
670
+
671
+ const receipts = yield* Effect.forEach(
672
+ rows.filter(({ admission }) => !completed.has(admission.messageId)),
673
+ Effect.fnUntraced(function* ({ admission }) {
674
+ const receipt = Option.getOrUndefined(
675
+ yield* getRecord({
676
+ threadId: admission.origin.worker.threadId,
677
+ recordId: Schema.decodeSync(RecordId)(`worker-effects-resolved:${admission.messageId}`),
678
+ }).pipe(
679
+ Effect.provideService(ThreadStore, deps.store),
680
+ // An unmaterialized reservation still consumes capacity.
681
+ Effect.catchTag("ThreadNotMaterialized", () => Effect.succeed(Option.none())),
682
+ Effect.mapError(storageFailure("start")),
683
+ ),
684
+ )?.record.payload;
672
685
 
673
- for (const { admission } of rows) {
674
- if (completed.has(admission.messageId)) continue;
686
+ if (receipt === undefined) return;
687
+ if (
688
+ receipt._tag !== "WorkerInputCompleted" ||
689
+ !receipt.effectsResolved ||
690
+ receipt.messageId !== admission.messageId ||
691
+ receipt.workerThreadId !== admission.origin.worker.threadId
692
+ )
693
+ return yield* failure("start", "corrupt");
675
694
 
676
- const receipt = Option.getOrUndefined(
677
- yield* getRecord({
678
- threadId: admission.origin.worker.threadId,
679
- recordId: Schema.decodeSync(RecordId)(`worker-effects-resolved:${admission.messageId}`),
680
- }).pipe(
681
- Effect.provideService(ThreadStore, deps.store),
682
- // A source reservation can precede child materialization. It still consumes
683
- // capacity; absence of storage is never a completion acknowledgement.
684
- Effect.catchTag("ThreadNotMaterialized", () => Effect.succeed(Option.none())),
685
- Effect.mapError(storageFailure("start")),
686
- ),
687
- )?.record.payload;
695
+ return receipt;
696
+ }),
697
+ { concurrency: 8 },
698
+ );
688
699
 
689
- if (receipt === undefined) continue;
690
- if (
691
- receipt._tag !== "WorkerInputCompleted" ||
692
- !receipt.effectsResolved ||
693
- receipt.messageId !== admission.messageId ||
694
- receipt.workerThreadId !== admission.origin.worker.threadId
695
- )
696
- return yield* failure("start", "corrupt");
697
- completed.add(admission.messageId);
698
- acknowledgements.push(receipt);
699
- }
700
+ const acknowledgements = receipts.filter((receipt) => receipt !== undefined);
701
+
702
+ for (const receipt of acknowledgements) completed.add(receipt.messageId);
700
703
 
701
704
  return { completed, acknowledgements };
702
705
  });
@@ -1047,33 +1050,40 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1047
1050
 
1048
1051
  // Include unreconciled reservations conservatively; only canonical completion frees an
1049
1052
  // active slot. A worker Thread consumes one slot even when multiple inputs safely join it.
1050
- const completedWorkers = yield* completedInputs(
1051
- requests(source.current.records),
1052
- source.current.records,
1053
- );
1054
-
1055
1053
  const completedAttached = new Set<string>(
1056
1054
  source.current.records.flatMap(({ record }) =>
1057
1055
  record.payload._tag === "SubagentJoined" ? [record.payload.reservationId] : [],
1058
1056
  ),
1059
1057
  );
1060
1058
 
1061
- const active = new Set(
1062
- rows
1063
- .filter((row) =>
1064
- row.lifetime === "background"
1065
- ? !completedWorkers.completed.has(row.reservationId)
1066
- : !completedAttached.has(row.reservationId),
1067
- )
1068
- .map((row) => row.childThreadId),
1059
+ const activeLimit = Math.min(
1060
+ caps.maxConcurrentChildren ?? Infinity,
1061
+ source.policy.toolConcurrency,
1069
1062
  );
1070
1063
 
1071
- if (
1072
- !active.has(payload.childThreadId) &&
1073
- active.size >=
1074
- Math.min(caps.maxConcurrentChildren ?? Infinity, source.policy.toolConcurrency)
1075
- )
1076
- return yield* failure("start", "capacity");
1064
+ const hasSlot = (completed: ReadonlySet<string>, conservative = false) => {
1065
+ const active = new Set(
1066
+ rows
1067
+ .filter((row) =>
1068
+ row.lifetime === "background"
1069
+ ? !completed.has(row.reservationId)
1070
+ : !completedAttached.has(row.reservationId),
1071
+ )
1072
+ .map((row) => row.childThreadId),
1073
+ );
1074
+
1075
+ return active.has(payload.childThreadId)
1076
+ ? !conservative || active.size <= activeLimit
1077
+ : active.size < activeLimit;
1078
+ };
1079
+
1080
+ const localCompletions = completedMessageIds(source.current.records);
1081
+
1082
+ const completedWorkers = hasSlot(localCompletions, true)
1083
+ ? { completed: localCompletions, acknowledgements: [] }
1084
+ : yield* completedInputs(requests(source.current.records), source.current.records);
1085
+
1086
+ if (!hasSlot(completedWorkers.completed)) return yield* failure("start", "capacity");
1077
1087
  if (
1078
1088
  yield* append(
1079
1089
  sourceThreadId,
@@ -1275,17 +1285,6 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1275
1285
  return yield* failure("start", "capacity");
1276
1286
  }
1277
1287
 
1278
- const completed = yield* completedInputs(rows, current.records);
1279
-
1280
- const pendingRows = rows.filter((row) => !completed.completed.has(row.admission.messageId));
1281
- const activeWorkers = new Set(pendingRows.map((row) => row.admission.origin.worker.threadId));
1282
-
1283
- const pendingOwn = pendingRows.filter(
1284
- (row) =>
1285
- row.admission.origin.worker.threadId === origin.worker.threadId &&
1286
- row.admission.reportKind === admission.reportKind,
1287
- ).length;
1288
-
1289
1288
  // Resolve inside the source CAS loop: independently delivered starts must compete
1290
1289
  // against one canonical prefix. A conflict repeats both authority and capacity reads.
1291
1290
  const selectedConcurrency = yield* deps.concurrencyResolver.resolve({
@@ -1301,21 +1300,48 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1301
1300
  .maxActiveWorkersPerSource
1302
1301
  : Infinity;
1303
1302
 
1304
- if (
1305
- pendingOwn >=
1306
- (admission.reportKind === "update"
1307
- ? (deps.limits.maxPendingUpdateInputsPerWorker ?? 32)
1308
- : deps.limits.maxPendingInputsPerWorker) ||
1309
- (!activeWorkers.has(origin.worker.threadId) &&
1310
- activeWorkers.size >=
1311
- Math.min(
1312
- sourceConcurrency,
1313
- deps.limits.maxActiveWorkersPerSource ?? source.policy.toolConcurrency,
1314
- independent
1315
- ? Infinity
1316
- : Math.min(caps.maxConcurrentChildren ?? Infinity, source.policy.toolConcurrency),
1317
- ))
1318
- )
1303
+ const activeLimit = Math.min(
1304
+ sourceConcurrency,
1305
+ deps.limits.maxActiveWorkersPerSource ?? source.policy.toolConcurrency,
1306
+ independent
1307
+ ? Infinity
1308
+ : Math.min(caps.maxConcurrentChildren ?? Infinity, source.policy.toolConcurrency),
1309
+ );
1310
+
1311
+ const hasCapacity = (completed: ReadonlySet<string>, conservative = false) => {
1312
+ const pendingRows = rows.filter((row) => !completed.has(row.admission.messageId));
1313
+
1314
+ const activeWorkers = new Set(
1315
+ pendingRows.map((row) => row.admission.origin.worker.threadId),
1316
+ );
1317
+
1318
+ const pendingOwn = pendingRows.filter(
1319
+ (row) =>
1320
+ row.admission.origin.worker.threadId === origin.worker.threadId &&
1321
+ row.admission.reportKind === admission.reportKind,
1322
+ ).length;
1323
+
1324
+ return (
1325
+ pendingOwn <
1326
+ (admission.reportKind === "update"
1327
+ ? (deps.limits.maxPendingUpdateInputsPerWorker ?? 32)
1328
+ : deps.limits.maxPendingInputsPerWorker) &&
1329
+ (activeWorkers.has(origin.worker.threadId)
1330
+ ? !conservative || activeWorkers.size <= activeLimit
1331
+ : activeWorkers.size < activeLimit)
1332
+ );
1333
+ };
1334
+
1335
+ // Unresolved inputs bound pending work from above. An apparently active destination
1336
+ // could already be idle: after a limit reduction, prove activity before exempting its
1337
+ // follow-up from acquiring a new slot.
1338
+ const localCompletions = completedMessageIds(current.records);
1339
+
1340
+ const completed = hasCapacity(localCompletions, true)
1341
+ ? { completed: localCompletions, acknowledgements: [] }
1342
+ : yield* completedInputs(rows, current.records);
1343
+
1344
+ if (!hasCapacity(completed.completed))
1319
1345
  return yield* WorkerError.make({ operation: "start", reason: "capacity", retryable: true });
1320
1346
  if (!independent)
1321
1347
  yield* reserveSubtree(
@@ -1351,42 +1377,14 @@ export const makeWorkerRuntime = Effect.fn("WorkerHost.make")(function* (
1351
1377
  return yield* failure("start", "storage");
1352
1378
  });
1353
1379
 
1354
- const ensureOrigin = Effect.fn("WorkerHost.ensureOrigin")(function* (origin: WorkerOrigin) {
1355
- for (let attempt = 0; attempt < 16; attempt++) {
1356
- const current = yield* readIdentity(origin.worker.threadId, "start");
1357
-
1358
- const existing = current.records.find(
1359
- ({ record }) => record.payload._tag === "WorkerOriginRecorded",
1360
- )?.record.payload;
1361
-
1362
- if (existing?._tag === "WorkerOriginRecorded") {
1363
- if (!sameOrigin(existing.origin, origin)) return yield* failure("start", "worker-mismatch");
1364
-
1365
- return;
1366
- }
1367
- const first = current.records[0]?.record.payload;
1368
-
1369
- if (
1370
- first?._tag !== "ThreadCreated" ||
1371
- first.agentId !== origin.worker.targetAgentId ||
1372
- !definitionDigestsEqual(first.definitions, origin.targetDigests) ||
1373
- current.records.some(({ record }) => record.payload._tag === "SubagentLineageRecorded")
1374
- )
1375
- return yield* failure("start", "worker-mismatch");
1376
- if (
1377
- yield* append(
1378
- origin.worker.threadId,
1379
- workerOriginRecordId(origin.worker.threadId),
1380
- WorkerOriginRecorded.make({ origin }),
1381
- current,
1382
- "origin",
1383
- )
1384
- )
1385
- return;
1386
- }
1387
-
1388
- return yield* failure("start", "storage");
1389
- });
1380
+ const ensureOrigin = (origin: WorkerOrigin) =>
1381
+ ensureWorkerOrigin(
1382
+ { producerId: deps.producerId, deploymentId: deps.deploymentId },
1383
+ origin,
1384
+ ).pipe(
1385
+ Effect.provideService(ThreadStore, deps.store),
1386
+ Effect.provideService(DurableRuntimeFailpoint, deps.failpoint),
1387
+ );
1390
1388
 
1391
1389
  const validateAdmission = Effect.fn("WorkerHost.validateAdmission")(function* (
1392
1390
  unvalidated: WorkerAdmission,
package/src/index.ts CHANGED
@@ -96,3 +96,4 @@ export * as WorkerHost from "./durable/WorkerHost.ts";
96
96
  export * as ThreadContextHistory from "./durable/ThreadContextHistory.ts";
97
97
  export * as ThreadContextHistoryProjection from "./durable/ThreadContextHistoryProjection.ts";
98
98
  export * as LifecyclePublication from "./durable/LifecyclePublication.ts";
99
+ export * as WorkerAdmission from "./durable/WorkerAdmission.ts";