@microfox/ai-worker 1.0.3 → 1.0.5

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 (69) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +22 -0
  3. package/dist/chainMapDefaults.d.mts +21 -0
  4. package/dist/chainMapDefaults.d.ts +21 -0
  5. package/dist/chainMapDefaults.js +59 -0
  6. package/dist/chainMapDefaults.js.map +1 -0
  7. package/dist/chainMapDefaults.mjs +10 -0
  8. package/dist/chainMapDefaults.mjs.map +1 -0
  9. package/dist/chunk-BCRJIFKB.mjs +9 -0
  10. package/dist/chunk-BCRJIFKB.mjs.map +1 -0
  11. package/dist/{chunk-72XGFZCE.mjs → chunk-CILTGUUQ.mjs} +14 -3
  12. package/dist/chunk-CILTGUUQ.mjs.map +1 -0
  13. package/dist/{chunk-7LQNS2SG.mjs → chunk-QHX55IML.mjs} +442 -56
  14. package/dist/chunk-QHX55IML.mjs.map +1 -0
  15. package/dist/chunk-SQB5FQCZ.mjs +21 -0
  16. package/dist/chunk-SQB5FQCZ.mjs.map +1 -0
  17. package/dist/{chunk-AOXGONGI.mjs → chunk-T7DRPKR6.mjs} +7 -5
  18. package/dist/chunk-T7DRPKR6.mjs.map +1 -0
  19. package/dist/chunk-XCKWV2WZ.mjs +34 -0
  20. package/dist/chunk-XCKWV2WZ.mjs.map +1 -0
  21. package/dist/chunk-ZW4PNCDH.mjs +17 -0
  22. package/dist/chunk-ZW4PNCDH.mjs.map +1 -0
  23. package/dist/client.d.mts +148 -2
  24. package/dist/client.d.ts +148 -2
  25. package/dist/client.js +13 -2
  26. package/dist/client.js.map +1 -1
  27. package/dist/client.mjs +1 -1
  28. package/dist/handler.d.mts +121 -23
  29. package/dist/handler.d.ts +121 -23
  30. package/dist/handler.js +450 -58
  31. package/dist/handler.js.map +1 -1
  32. package/dist/handler.mjs +5 -2
  33. package/dist/hitlConfig.d.mts +46 -0
  34. package/dist/hitlConfig.d.ts +46 -0
  35. package/dist/hitlConfig.js +33 -0
  36. package/dist/hitlConfig.js.map +1 -0
  37. package/dist/hitlConfig.mjs +8 -0
  38. package/dist/hitlConfig.mjs.map +1 -0
  39. package/dist/index.d.mts +28 -4
  40. package/dist/index.d.ts +28 -4
  41. package/dist/index.js +575 -74
  42. package/dist/index.js.map +1 -1
  43. package/dist/index.mjs +78 -20
  44. package/dist/index.mjs.map +1 -1
  45. package/dist/queue-B5n6YVQV.d.ts +306 -0
  46. package/dist/queue-DaR2UuZi.d.mts +306 -0
  47. package/dist/queue.d.mts +3 -0
  48. package/dist/queue.d.ts +3 -0
  49. package/dist/queue.js +47 -0
  50. package/dist/queue.js.map +1 -0
  51. package/dist/queue.mjs +12 -0
  52. package/dist/queue.mjs.map +1 -0
  53. package/dist/queueInputEnvelope.d.mts +31 -0
  54. package/dist/queueInputEnvelope.d.ts +31 -0
  55. package/dist/queueInputEnvelope.js +42 -0
  56. package/dist/queueInputEnvelope.js.map +1 -0
  57. package/dist/queueInputEnvelope.mjs +10 -0
  58. package/dist/queueInputEnvelope.mjs.map +1 -0
  59. package/dist/queueJobStore.d.mts +3 -2
  60. package/dist/queueJobStore.d.ts +3 -2
  61. package/dist/queueJobStore.js +6 -4
  62. package/dist/queueJobStore.js.map +1 -1
  63. package/dist/queueJobStore.mjs +1 -1
  64. package/package.json +7 -2
  65. package/dist/chunk-72XGFZCE.mjs.map +0 -1
  66. package/dist/chunk-7LQNS2SG.mjs.map +0 -1
  67. package/dist/chunk-AOXGONGI.mjs.map +0 -1
  68. package/dist/client-BqSJQ9mZ.d.mts +0 -183
  69. package/dist/client-BqSJQ9mZ.d.ts +0 -183
package/dist/handler.js CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var handler_exports = {};
22
22
  __export(handler_exports, {
23
23
  SQS_MAX_DELAY_SECONDS: () => SQS_MAX_DELAY_SECONDS,
24
+ TokenBudgetExceededError: () => TokenBudgetExceededError,
24
25
  createLambdaHandler: () => createLambdaHandler,
25
26
  createWorkerLogger: () => createWorkerLogger,
26
27
  wrapHandlerForQueue: () => wrapHandlerForQueue
@@ -68,7 +69,7 @@ async function getJobById(jobId) {
68
69
  return null;
69
70
  }
70
71
  }
71
- function createMongoJobStore(workerId, jobId, input, metadata) {
72
+ function createMongoJobStore(workerId, jobId, input, metadata, userId) {
72
73
  return {
73
74
  update: async (update) => {
74
75
  try {
@@ -107,6 +108,7 @@ function createMongoJobStore(workerId, jobId, input, metadata) {
107
108
  output: update.output,
108
109
  error: update.error,
109
110
  metadata: metadataUpdate,
111
+ ...userId ? { userId } : {},
110
112
  createdAt: now,
111
113
  updatedAt: now,
112
114
  completedAt: set.completedAt
@@ -172,7 +174,7 @@ function createMongoJobStore(workerId, jobId, input, metadata) {
172
174
  }
173
175
  };
174
176
  }
175
- async function upsertJob(jobId, workerId, input, metadata) {
177
+ async function upsertJob(jobId, workerId, input, metadata, userId) {
176
178
  const coll = await getCollection();
177
179
  const now = (/* @__PURE__ */ new Date()).toISOString();
178
180
  await coll.updateOne(
@@ -185,6 +187,7 @@ async function upsertJob(jobId, workerId, input, metadata) {
185
187
  status: "queued",
186
188
  input: input ?? {},
187
189
  metadata: metadata ?? {},
190
+ ...userId ? { userId } : {},
188
191
  createdAt: now,
189
192
  updatedAt: now
190
193
  }
@@ -263,13 +266,14 @@ async function loadJob(jobId) {
263
266
  error: parseJson(data.error),
264
267
  metadata: parseJson(data.metadata) ?? {},
265
268
  internalJobs,
269
+ ...data.userId ? { userId: data.userId } : {},
266
270
  createdAt: data.createdAt,
267
271
  updatedAt: data.updatedAt,
268
272
  completedAt: data.completedAt
269
273
  };
270
274
  return record;
271
275
  }
272
- function createRedisJobStore(workerId, jobId, input, metadata) {
276
+ function createRedisJobStore(workerId, jobId, input, metadata, userId) {
273
277
  return {
274
278
  update: async (update) => {
275
279
  const redis = getRedis();
@@ -327,28 +331,20 @@ function createRedisJobStore(workerId, jobId, input, metadata) {
327
331
  }
328
332
  };
329
333
  }
330
- async function upsertRedisJob(jobId, workerId, input, metadata) {
334
+ async function upsertRedisJob(jobId, workerId, input, metadata, userId) {
331
335
  const redis = getRedis();
332
336
  const key = jobKey(jobId);
333
337
  const now = (/* @__PURE__ */ new Date()).toISOString();
334
- const doc = {
335
- jobId,
336
- workerId,
337
- status: "queued",
338
- input,
339
- metadata,
340
- createdAt: now,
341
- updatedAt: now
342
- };
343
338
  const toSet = {
344
339
  jobId,
345
340
  workerId,
346
- status: doc.status,
347
- input: JSON.stringify(doc.input ?? {}),
348
- metadata: JSON.stringify(doc.metadata ?? {}),
341
+ status: "queued",
342
+ input: JSON.stringify(input ?? {}),
343
+ metadata: JSON.stringify(metadata ?? {}),
349
344
  createdAt: now,
350
345
  updatedAt: now
351
346
  };
347
+ if (userId) toSet.userId = userId;
352
348
  await redis.hset(key, toSet);
353
349
  if (jobTtlSeconds > 0) {
354
350
  await redis.expire(key, jobTtlSeconds);
@@ -440,6 +436,7 @@ async function loadQueueJobRedis(queueJobId) {
440
436
  status: String(d.status ?? "running"),
441
437
  steps: stepsFromHash(d.steps),
442
438
  metadata: metadataFromHash(d.metadata),
439
+ ...d.userId ? { userId: String(d.userId) } : {},
443
440
  createdAt: String(d.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()),
444
441
  updatedAt: String(d.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()),
445
442
  completedAt: d.completedAt != null ? String(d.completedAt) : void 0
@@ -459,9 +456,8 @@ async function saveQueueJobRedis(record) {
459
456
  createdAt: record.createdAt || now,
460
457
  updatedAt: record.updatedAt || now
461
458
  };
462
- if (record.completedAt) {
463
- toSet.completedAt = record.completedAt;
464
- }
459
+ if (record.completedAt) toSet.completedAt = record.completedAt;
460
+ if (record.userId) toSet.userId = record.userId;
465
461
  await redis.hset(key, toSet);
466
462
  if (queueJobTtlSeconds > 0) {
467
463
  await redis.expire(key, queueJobTtlSeconds);
@@ -478,7 +474,7 @@ function preferRedis() {
478
474
  return getStoreType() !== "mongodb" && Boolean((redisUrl2 || "").trim() && (redisToken2 || "").trim());
479
475
  }
480
476
  async function upsertInitialQueueJob(options) {
481
- const { queueJobId, queueId, firstWorkerId, firstWorkerJobId, metadata } = options;
477
+ const { queueJobId, queueId, firstWorkerId, firstWorkerJobId, metadata, userId } = options;
482
478
  const now = (/* @__PURE__ */ new Date()).toISOString();
483
479
  if (preferMongo()) {
484
480
  const coll = await getMongoQueueCollection();
@@ -515,6 +511,7 @@ async function upsertInitialQueueJob(options) {
515
511
  }
516
512
  ],
517
513
  metadata: metadata ?? {},
514
+ ...userId ? { userId } : {},
518
515
  createdAt: now,
519
516
  updatedAt: now
520
517
  };
@@ -553,6 +550,7 @@ async function upsertInitialQueueJob(options) {
553
550
  }
554
551
  ],
555
552
  metadata: metadata ?? {},
553
+ ...userId ? { userId } : {},
556
554
  createdAt: now,
557
555
  updatedAt: now
558
556
  };
@@ -665,6 +663,150 @@ async function appendQueueJobStepInStore(options) {
665
663
  }
666
664
  }
667
665
 
666
+ // src/queue.ts
667
+ var QUEUE_ORCHESTRATION_KEYS = [
668
+ "__workerQueue",
669
+ "__hitlInput",
670
+ "__hitlDecision",
671
+ "__hitlPending",
672
+ "hitl"
673
+ ];
674
+
675
+ // src/retryConfig.ts
676
+ var BUILT_IN_PATTERNS = {
677
+ "rate-limit": {
678
+ match: (err) => /rate.?limit|too.?many.?requests/i.test(err.message) || err.status === 429 || err.code === 429 || err.name === "RateLimitError",
679
+ delayMs: (attempt) => attempt * 1e4,
680
+ // 10s, 20s, 30s…
681
+ injectContext: false
682
+ },
683
+ "json-parse": {
684
+ match: (err) => err.name === "SyntaxError" || err.name === "ZodError" || /json|parse|unexpected.?token|invalid.?format/i.test(err.message),
685
+ delayMs: (_attempt) => 0,
686
+ // Immediate — model self-corrects from ctx
687
+ injectContext: true
688
+ },
689
+ overloaded: {
690
+ match: (err) => /overloaded|model.?is.?busy/i.test(err.message) || err.status === 529 || err.code === 529,
691
+ delayMs: (attempt) => attempt * 15e3,
692
+ // 15s, 30s…
693
+ injectContext: false
694
+ },
695
+ "server-error": {
696
+ match: (err) => /internal.?server.?error|service.?unavailable|bad.?gateway/i.test(err.message) || typeof err.status === "number" && err.status >= 500 && err.status < 600,
697
+ delayMs: (attempt) => attempt * 5e3,
698
+ // 5s, 10s…
699
+ injectContext: false
700
+ }
701
+ };
702
+ function matchesRetryPattern(err, patterns, attempt) {
703
+ for (const pattern of patterns) {
704
+ if (typeof pattern === "string") {
705
+ const impl = BUILT_IN_PATTERNS[pattern];
706
+ if (impl.match(err)) {
707
+ return { matched: true, delayMs: impl.delayMs(attempt), injectContext: impl.injectContext };
708
+ }
709
+ } else {
710
+ let matched = false;
711
+ if (pattern.match instanceof RegExp) {
712
+ matched = pattern.match.test(err.message);
713
+ } else {
714
+ try {
715
+ matched = pattern.match(err);
716
+ } catch {
717
+ matched = false;
718
+ }
719
+ }
720
+ if (matched) {
721
+ const delayMs = typeof pattern.delayMs === "function" ? pattern.delayMs(attempt) : pattern.delayMs ?? 0;
722
+ return { matched: true, delayMs, injectContext: pattern.injectContext ?? false };
723
+ }
724
+ }
725
+ }
726
+ return { matched: false, delayMs: 0, injectContext: false };
727
+ }
728
+ async function executeWithRetry(fn, config, onRetry) {
729
+ const maxAttempts = config.maxAttempts ?? 3;
730
+ let lastError;
731
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
732
+ const retryCtx = attempt > 1 && lastError ? {
733
+ attempt,
734
+ maxAttempts,
735
+ lastError: {
736
+ message: lastError.message,
737
+ name: lastError.name,
738
+ stack: lastError.stack,
739
+ code: lastError.code ?? lastError.status
740
+ }
741
+ } : void 0;
742
+ try {
743
+ return await fn(retryCtx);
744
+ } catch (err) {
745
+ lastError = err instanceof Error ? err : new Error(String(err));
746
+ if (err?.name === "TokenBudgetExceededError") throw err;
747
+ if (attempt >= maxAttempts) throw err;
748
+ const retryAttemptNumber = attempt;
749
+ const { matched, delayMs } = matchesRetryPattern(lastError, config.on, retryAttemptNumber);
750
+ if (!matched) throw err;
751
+ const nextCtx = {
752
+ attempt: attempt + 1,
753
+ maxAttempts,
754
+ lastError: {
755
+ message: lastError.message,
756
+ name: lastError.name,
757
+ stack: lastError.stack,
758
+ code: lastError.code ?? lastError.status
759
+ }
760
+ };
761
+ onRetry?.(nextCtx, delayMs);
762
+ if (delayMs > 0) {
763
+ await new Promise((r) => setTimeout(r, delayMs));
764
+ }
765
+ }
766
+ }
767
+ throw lastError ?? new Error("executeWithRetry: unknown error");
768
+ }
769
+
770
+ // src/tokenBudget.ts
771
+ var TokenBudgetExceededError = class extends Error {
772
+ constructor(used, budget) {
773
+ super(`Token budget exceeded: used ${used} tokens (budget: ${budget})`);
774
+ this.name = "TokenBudgetExceededError";
775
+ this.used = used;
776
+ this.budget = budget;
777
+ }
778
+ };
779
+ function createTokenTracker(budget) {
780
+ let inputTokens = 0;
781
+ let outputTokens = 0;
782
+ function checkBudget() {
783
+ if (budget !== null) {
784
+ const total = inputTokens + outputTokens;
785
+ if (total > budget) {
786
+ throw new TokenBudgetExceededError(total, budget);
787
+ }
788
+ }
789
+ }
790
+ return {
791
+ report(usage) {
792
+ inputTokens += usage.inputTokens;
793
+ outputTokens += usage.outputTokens;
794
+ checkBudget();
795
+ },
796
+ getState() {
797
+ return { inputTokens, outputTokens, budget };
798
+ },
799
+ getBudgetInfo() {
800
+ const used = inputTokens + outputTokens;
801
+ return {
802
+ used,
803
+ budget,
804
+ remaining: budget !== null ? Math.max(0, budget - used) : null
805
+ };
806
+ }
807
+ };
808
+ }
809
+
668
810
  // src/handler.ts
669
811
  var SQS_MAX_DELAY_SECONDS = 900;
670
812
  function createWorkerLogger(jobId, workerId) {
@@ -687,6 +829,67 @@ function createWorkerLogger(jobId, workerId) {
687
829
  };
688
830
  }
689
831
  var WORKER_QUEUE_KEY = "__workerQueue";
832
+ async function loadPreviousOutputsBeforeStep(queueRuntime, queueJobId, beforeStepIndex) {
833
+ if (!queueJobId || typeof queueRuntime.getQueueJob !== "function") {
834
+ return [];
835
+ }
836
+ try {
837
+ const job = await queueRuntime.getQueueJob(queueJobId);
838
+ if (!job?.steps) return [];
839
+ return job.steps.slice(0, beforeStepIndex).map((s, i) => ({ stepIndex: i, workerId: s.workerId, output: s.output }));
840
+ } catch (e) {
841
+ if (process.env.AI_WORKER_QUEUES_DEBUG === "1") {
842
+ console.warn("[Worker] getQueueJob failed (resume mapping):", e?.message ?? e);
843
+ }
844
+ return [];
845
+ }
846
+ }
847
+ async function maybeApplyHitlResumeMapper(params, queueRuntime) {
848
+ const inputObj = params.input;
849
+ if (!inputObj || typeof inputObj !== "object") return;
850
+ if (!("__hitlInput" in inputObj)) return;
851
+ const wq = inputObj[WORKER_QUEUE_KEY];
852
+ if (!wq?.id || typeof wq.stepIndex !== "number") return;
853
+ const queueId = wq.id;
854
+ const stepIndex = wq.stepIndex;
855
+ const initialInput = wq.initialInput;
856
+ const queueJobId = wq.queueJobId;
857
+ const previousOutputs = await loadPreviousOutputsBeforeStep(queueRuntime, queueJobId, stepIndex);
858
+ const pendingInput = { ...inputObj };
859
+ for (const key of QUEUE_ORCHESTRATION_KEYS) {
860
+ delete pendingInput[key];
861
+ }
862
+ delete pendingInput[WORKER_QUEUE_KEY];
863
+ const reviewerInput = inputObj.__hitlInput;
864
+ const decision = inputObj.__hitlDecision;
865
+ let merged;
866
+ if (typeof queueRuntime.invokeResume === "function") {
867
+ merged = await queueRuntime.invokeResume(queueId, stepIndex, {
868
+ initialInput,
869
+ previousOutputs,
870
+ reviewerInput,
871
+ pendingInput
872
+ });
873
+ } else {
874
+ merged = {
875
+ ...pendingInput,
876
+ ...reviewerInput !== null && typeof reviewerInput === "object" ? reviewerInput : {}
877
+ };
878
+ }
879
+ const mergedObj = merged !== null && typeof merged === "object" ? merged : { value: merged };
880
+ params.input = {
881
+ ...mergedObj,
882
+ [WORKER_QUEUE_KEY]: wq,
883
+ ...decision !== void 0 ? { __hitlDecision: decision } : {}
884
+ };
885
+ }
886
+ function getWorkerQueueContext(input, metadata) {
887
+ const fromInput = input !== null && typeof input === "object" && WORKER_QUEUE_KEY in input ? input[WORKER_QUEUE_KEY] : void 0;
888
+ const fromMeta = metadata !== void 0 && typeof metadata === "object" && WORKER_QUEUE_KEY in metadata ? metadata[WORKER_QUEUE_KEY] : void 0;
889
+ const q = fromInput ?? fromMeta;
890
+ if (q === null || typeof q !== "object") return void 0;
891
+ return q;
892
+ }
690
893
  async function notifyQueueJobStep(queueJobId, action, params) {
691
894
  try {
692
895
  if (action === "append") {
@@ -706,7 +909,7 @@ async function notifyQueueJobStep(queueJobId, action, params) {
706
909
  return;
707
910
  }
708
911
  if (params.stepIndex === void 0) return;
709
- const status = action === "start" ? "running" : action === "complete" ? "completed" : action === "fail" ? "failed" : void 0;
912
+ const status = action === "start" ? "running" : action === "awaiting_approval" ? "awaiting_approval" : action === "complete" ? "completed" : action === "fail" ? "failed" : void 0;
710
913
  if (!status) return;
711
914
  await updateQueueJobStepInStore({
712
915
  queueJobId,
@@ -726,6 +929,13 @@ async function notifyQueueJobStep(queueJobId, action, params) {
726
929
  status
727
930
  });
728
931
  } catch (err) {
932
+ if (action === "append") {
933
+ console.error("[Worker] Queue append failed (rethrowing):", {
934
+ queueJobId,
935
+ error: err?.message ?? String(err)
936
+ });
937
+ throw err;
938
+ }
729
939
  console.warn("[Worker] Queue job update error:", {
730
940
  queueJobId,
731
941
  action,
@@ -735,26 +945,152 @@ async function notifyQueueJobStep(queueJobId, action, params) {
735
945
  }
736
946
  function wrapHandlerForQueue(handler, queueRuntime) {
737
947
  return async (params) => {
738
- const queueContext = params.input?.[WORKER_QUEUE_KEY];
739
- const output = await handler(params);
740
- if (!queueContext || typeof queueContext !== "object" || !queueContext.id) {
948
+ await maybeApplyHitlResumeMapper(params, queueRuntime);
949
+ const inputObj = params.input !== null && typeof params.input === "object" ? params.input : {};
950
+ const queueContextRaw = inputObj[WORKER_QUEUE_KEY];
951
+ const queueCtxForRetry = queueContextRaw && typeof queueContextRaw === "object" ? queueContextRaw : void 0;
952
+ const stepRetryConfig = queueCtxForRetry?.id && typeof queueCtxForRetry.stepIndex === "number" && typeof queueRuntime.getStepAt === "function" ? queueRuntime.getStepAt(queueCtxForRetry.id, queueCtxForRetry.stepIndex)?.retry : void 0;
953
+ const domainInput = { ...inputObj };
954
+ for (const key of QUEUE_ORCHESTRATION_KEYS) {
955
+ delete domainInput[key];
956
+ }
957
+ delete domainInput[WORKER_QUEUE_KEY];
958
+ params.input = domainInput;
959
+ let output;
960
+ if (stepRetryConfig && stepRetryConfig.on.length > 0) {
961
+ output = await executeWithRetry(
962
+ async (retryCtx) => {
963
+ params.ctx.retryContext = retryCtx;
964
+ return handler(params);
965
+ },
966
+ stepRetryConfig,
967
+ (retryCtx, delayMs) => {
968
+ const logger = params.ctx.logger;
969
+ if (logger?.warn) {
970
+ logger.warn(
971
+ `[queue-retry] Retrying step (attempt ${retryCtx.attempt}/${retryCtx.maxAttempts}): ${retryCtx.lastError.message}`,
972
+ { delayMs }
973
+ );
974
+ } else {
975
+ console.warn("[queue-retry] Step retry", { attempt: retryCtx.attempt, error: retryCtx.lastError.message, delayMs });
976
+ }
977
+ }
978
+ );
979
+ } else {
980
+ output = await handler(params);
981
+ }
982
+ if (!queueContextRaw || typeof queueContextRaw !== "object") {
983
+ return output;
984
+ }
985
+ const queueContext = queueContextRaw;
986
+ if (!queueContext.id) {
741
987
  return output;
742
988
  }
743
989
  const { id: queueId, stepIndex, initialInput, queueJobId } = queueContext;
744
- const jobId = params.ctx?.jobId;
745
- const workerId = params.ctx?.workerId ?? "";
990
+ const arrayStepIndex = queueContext.arrayStepIndex ?? stepIndex;
991
+ const jobId = params.ctx.jobId;
992
+ const workerId = params.ctx.workerId ?? "";
746
993
  const next = queueRuntime.getNextStep(queueId, stepIndex);
747
994
  const childJobId = next ? `job-${Date.now()}-${Math.random().toString(36).slice(2, 11)}` : void 0;
995
+ const iterationCount = queueContext.iterationCount ?? 0;
996
+ if (typeof queueRuntime.invokeLoop === "function") {
997
+ const currentStep = typeof queueRuntime.getStepAt === "function" ? queueRuntime.getStepAt(queueId, stepIndex) : void 0;
998
+ const maxIterations = currentStep?.loop?.maxIterations ?? 50;
999
+ if (iterationCount < maxIterations - 1) {
1000
+ let previousOutputsForLoop = [];
1001
+ let stepsLengthBeforeAppend = arrayStepIndex + 1;
1002
+ if (queueJobId && typeof queueRuntime.getQueueJob === "function") {
1003
+ try {
1004
+ const job = await queueRuntime.getQueueJob(queueJobId);
1005
+ if (job?.steps) {
1006
+ previousOutputsForLoop = job.steps.slice(0, stepIndex).map((s, i) => ({ stepIndex: i, workerId: s.workerId, output: s.output }));
1007
+ stepsLengthBeforeAppend = job.steps.length;
1008
+ }
1009
+ } catch {
1010
+ }
1011
+ }
1012
+ previousOutputsForLoop = previousOutputsForLoop.concat([{ stepIndex, workerId, output }]);
1013
+ const shouldLoop = await queueRuntime.invokeLoop(queueId, stepIndex, {
1014
+ output,
1015
+ stepIndex,
1016
+ iterationCount,
1017
+ initialInput,
1018
+ previousOutputs: previousOutputsForLoop
1019
+ });
1020
+ if (shouldLoop) {
1021
+ const loopJobId = `job-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
1022
+ let loopInput = output;
1023
+ if (typeof queueRuntime.invokeChain === "function") {
1024
+ loopInput = await queueRuntime.invokeChain(queueId, stepIndex, {
1025
+ initialInput,
1026
+ previousOutputs: previousOutputsForLoop
1027
+ });
1028
+ }
1029
+ const loopInputWithQueue = {
1030
+ ...loopInput !== null && typeof loopInput === "object" ? loopInput : { value: loopInput },
1031
+ [WORKER_QUEUE_KEY]: {
1032
+ id: queueId,
1033
+ stepIndex,
1034
+ // definition index stays fixed
1035
+ arrayStepIndex: stepsLengthBeforeAppend,
1036
+ // actual index for next iteration
1037
+ initialInput,
1038
+ queueJobId,
1039
+ iterationCount: iterationCount + 1
1040
+ }
1041
+ };
1042
+ if (queueJobId) {
1043
+ await notifyQueueJobStep(queueJobId, "append", { workerJobId: loopJobId, workerId });
1044
+ }
1045
+ if (queueJobId && typeof arrayStepIndex === "number") {
1046
+ await notifyQueueJobStep(queueJobId, "complete", {
1047
+ queueId,
1048
+ stepIndex: arrayStepIndex,
1049
+ workerJobId: jobId,
1050
+ workerId,
1051
+ output
1052
+ });
1053
+ }
1054
+ if (currentStep?.requiresApproval && queueJobId) {
1055
+ const hitlUiSpec = currentStep.hitl && typeof currentStep.hitl === "object" && "ui" in currentStep.hitl ? currentStep.hitl.ui : void 0;
1056
+ const pendingInput = {
1057
+ ...loopInputWithQueue,
1058
+ ...hitlUiSpec !== void 0 ? { hitl: { uiSpec: hitlUiSpec } } : {},
1059
+ __hitlPending: {
1060
+ queueId,
1061
+ queueJobId,
1062
+ stepIndex,
1063
+ workerId,
1064
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1065
+ }
1066
+ };
1067
+ await notifyQueueJobStep(queueJobId, "awaiting_approval", {
1068
+ queueId,
1069
+ stepIndex: stepsLengthBeforeAppend,
1070
+ workerJobId: loopJobId,
1071
+ workerId,
1072
+ input: pendingInput
1073
+ });
1074
+ return output;
1075
+ }
1076
+ await params.ctx.dispatchWorker(workerId, loopInputWithQueue, {
1077
+ await: false,
1078
+ jobId: loopJobId
1079
+ });
1080
+ return output;
1081
+ }
1082
+ }
1083
+ }
748
1084
  if (next && queueJobId) {
749
1085
  await notifyQueueJobStep(queueJobId, "append", {
750
1086
  workerJobId: childJobId,
751
1087
  workerId: next.workerId
752
1088
  });
753
1089
  }
754
- if (queueJobId && typeof stepIndex === "number") {
1090
+ if (queueJobId && typeof arrayStepIndex === "number") {
755
1091
  await notifyQueueJobStep(queueJobId, "complete", {
756
1092
  queueId,
757
- stepIndex,
1093
+ stepIndex: arrayStepIndex,
758
1094
  workerJobId: jobId,
759
1095
  workerId,
760
1096
  output
@@ -764,7 +1100,7 @@ function wrapHandlerForQueue(handler, queueRuntime) {
764
1100
  return output;
765
1101
  }
766
1102
  let nextInput = output;
767
- if (next.mapInputFromPrev && typeof queueRuntime.invokeMapInput === "function") {
1103
+ if (typeof queueRuntime.invokeChain === "function") {
768
1104
  let previousOutputs = [];
769
1105
  if (queueJobId && typeof queueRuntime.getQueueJob === "function") {
770
1106
  try {
@@ -772,7 +1108,7 @@ function wrapHandlerForQueue(handler, queueRuntime) {
772
1108
  if (job?.steps) {
773
1109
  const fromStore = job.steps.slice(0, stepIndex).map((s, i) => ({ stepIndex: i, workerId: s.workerId, output: s.output }));
774
1110
  previousOutputs = fromStore.concat([
775
- { stepIndex, workerId: params.ctx?.workerId ?? "", output }
1111
+ { stepIndex, workerId: params.ctx.workerId ?? "", output }
776
1112
  ]);
777
1113
  }
778
1114
  } catch (e) {
@@ -781,12 +1117,10 @@ function wrapHandlerForQueue(handler, queueRuntime) {
781
1117
  }
782
1118
  }
783
1119
  }
784
- nextInput = await queueRuntime.invokeMapInput(
785
- queueId,
786
- stepIndex + 1,
1120
+ nextInput = await queueRuntime.invokeChain(queueId, stepIndex + 1, {
787
1121
  initialInput,
788
1122
  previousOutputs
789
- );
1123
+ });
790
1124
  }
791
1125
  const nextInputWithQueue = {
792
1126
  ...nextInput !== null && typeof nextInput === "object" ? nextInput : { value: nextInput },
@@ -806,6 +1140,37 @@ function wrapHandlerForQueue(handler, queueRuntime) {
806
1140
  delaySeconds: next.delaySeconds
807
1141
  });
808
1142
  }
1143
+ if (next.requiresApproval && queueJobId && typeof stepIndex === "number") {
1144
+ const hitlUiSpec = next.hitl && typeof next.hitl === "object" && "ui" in next.hitl ? next.hitl.ui : void 0;
1145
+ const pendingInput = {
1146
+ ...nextInputWithQueue,
1147
+ ...hitlUiSpec !== void 0 ? { hitl: { uiSpec: hitlUiSpec } } : {},
1148
+ __hitlPending: {
1149
+ queueId,
1150
+ queueJobId,
1151
+ stepIndex: stepIndex + 1,
1152
+ workerId: next.workerId,
1153
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1154
+ }
1155
+ };
1156
+ await notifyQueueJobStep(queueJobId, "awaiting_approval", {
1157
+ queueId,
1158
+ stepIndex: stepIndex + 1,
1159
+ workerJobId: childJobId,
1160
+ workerId: next.workerId,
1161
+ input: pendingInput
1162
+ });
1163
+ if (debug) {
1164
+ console.log("[Worker] Queue chain paused for HITL approval:", {
1165
+ queueId,
1166
+ queueJobId,
1167
+ nextStep: stepIndex + 1,
1168
+ nextWorkerId: next.workerId,
1169
+ pendingWorkerJobId: childJobId
1170
+ });
1171
+ }
1172
+ return output;
1173
+ }
809
1174
  await params.ctx.dispatchWorker(next.workerId, nextInputWithQueue, {
810
1175
  await: false,
811
1176
  delaySeconds: next.delaySeconds,
@@ -829,6 +1194,7 @@ function createDispatchWorker(parentJobId, parentWorkerId, parentContext, jobSto
829
1194
  const metadata = options?.metadata ?? {};
830
1195
  const serializedContext = {};
831
1196
  if (parentContext.requestId) serializedContext.requestId = parentContext.requestId;
1197
+ if (parentContext.userId) serializedContext.userId = parentContext.userId;
832
1198
  const messageBody = {
833
1199
  workerId: calleeWorkerId,
834
1200
  jobId: childJobId,
@@ -918,13 +1284,14 @@ async function sendWebhook(webhookUrl, payload) {
918
1284
  });
919
1285
  }
920
1286
  }
921
- function createLambdaHandler(handler, outputSchema) {
1287
+ function createLambdaHandler(handler, outputSchema, options) {
922
1288
  return async (event, lambdaContext) => {
923
1289
  const promises = event.Records.map(async (record) => {
924
1290
  let messageBody = null;
925
1291
  try {
926
1292
  messageBody = JSON.parse(record.body);
927
- const { workerId, jobId, input, context, webhookUrl, metadata = {} } = messageBody;
1293
+ const { workerId, jobId, input, context, webhookUrl, metadata = {}, userId: messageUserId, maxTokens } = messageBody;
1294
+ const userId = context.userId ?? messageUserId;
928
1295
  const raw = (process.env.WORKER_DATABASE_TYPE || "upstash-redis").toLowerCase();
929
1296
  const jobStoreType = raw === "mongodb" ? "mongodb" : "upstash-redis";
930
1297
  if (jobStoreType === "upstash-redis" && isRedisJobStoreConfigured()) {
@@ -950,33 +1317,45 @@ function createLambdaHandler(handler, outputSchema) {
950
1317
  }
951
1318
  let jobStore;
952
1319
  if (jobStoreType === "upstash-redis" && isRedisJobStoreConfigured()) {
953
- await upsertRedisJob(jobId, workerId, input, metadata);
954
- jobStore = createRedisJobStore(workerId, jobId, input, metadata);
1320
+ await upsertRedisJob(jobId, workerId, input, metadata, userId);
1321
+ jobStore = createRedisJobStore(workerId, jobId, input, metadata, userId);
955
1322
  } else if (jobStoreType === "mongodb" || isMongoJobStoreConfigured()) {
956
- await upsertJob(jobId, workerId, input, metadata);
957
- jobStore = createMongoJobStore(workerId, jobId, input, metadata);
1323
+ await upsertJob(jobId, workerId, input, metadata, userId);
1324
+ jobStore = createMongoJobStore(workerId, jobId, input, metadata, userId);
1325
+ }
1326
+ if (userId) {
1327
+ console.log(`[WORKER_USER:${userId}]`, { jobId, workerId, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
958
1328
  }
959
1329
  const baseContext = {
960
1330
  jobId,
961
1331
  workerId,
962
1332
  requestId: context.requestId || lambdaContext.awsRequestId,
1333
+ ...userId ? { userId } : {},
963
1334
  ...context
964
1335
  };
1336
+ const tokenTracker = createTokenTracker(maxTokens ?? null);
1337
+ const logger = createWorkerLogger(jobId, workerId);
965
1338
  const handlerContext = {
966
1339
  ...baseContext,
967
1340
  ...jobStore ? { jobStore } : {},
968
- logger: createWorkerLogger(jobId, workerId),
969
- dispatchWorker: createDispatchWorker(
970
- jobId,
971
- workerId,
972
- baseContext,
973
- jobStore
974
- )
1341
+ logger,
1342
+ dispatchWorker: createDispatchWorker(jobId, workerId, baseContext, jobStore),
1343
+ reportTokenUsage: async (usage) => {
1344
+ tokenTracker.report(usage);
1345
+ const state = tokenTracker.getState();
1346
+ if (jobStore) {
1347
+ await jobStore.update({ metadata: { tokenUsage: state } }).catch((e) => {
1348
+ logger.warn("Failed to persist tokenUsage to job store", { error: e?.message });
1349
+ });
1350
+ }
1351
+ },
1352
+ getTokenBudget: () => tokenTracker.getBudgetInfo(),
1353
+ retryContext: void 0
975
1354
  };
976
1355
  if (jobStore) {
977
1356
  try {
978
1357
  await jobStore.update({ status: "running" });
979
- const queueCtxForLog = input?.__workerQueue ?? metadata?.__workerQueue;
1358
+ const queueCtxForLog = getWorkerQueueContext(input, metadata);
980
1359
  console.log("[Worker] Job status updated to running:", {
981
1360
  jobId,
982
1361
  workerId,
@@ -991,7 +1370,7 @@ function createLambdaHandler(handler, outputSchema) {
991
1370
  });
992
1371
  }
993
1372
  }
994
- const queueCtx = input?.__workerQueue ?? metadata?.__workerQueue;
1373
+ const queueCtx = getWorkerQueueContext(input, metadata);
995
1374
  if (queueCtx?.queueJobId && typeof queueCtx.stepIndex === "number") {
996
1375
  if (queueCtx.stepIndex === 0) {
997
1376
  try {
@@ -1000,7 +1379,8 @@ function createLambdaHandler(handler, outputSchema) {
1000
1379
  queueId: queueCtx.id,
1001
1380
  firstWorkerId: workerId,
1002
1381
  firstWorkerJobId: jobId,
1003
- metadata
1382
+ metadata,
1383
+ userId
1004
1384
  });
1005
1385
  } catch (e) {
1006
1386
  console.warn("[Worker] Failed to upsert initial queue job:", {
@@ -1012,7 +1392,9 @@ function createLambdaHandler(handler, outputSchema) {
1012
1392
  }
1013
1393
  await notifyQueueJobStep(queueCtx.queueJobId, "start", {
1014
1394
  queueId: queueCtx.id,
1015
- stepIndex: queueCtx.stepIndex,
1395
+ // Use arrayStepIndex when set — it tracks the actual steps[] position for
1396
+ // looping steps where the definition index stays fixed across iterations.
1397
+ stepIndex: queueCtx.arrayStepIndex ?? queueCtx.stepIndex,
1016
1398
  workerJobId: jobId,
1017
1399
  workerId,
1018
1400
  input
@@ -1020,12 +1402,21 @@ function createLambdaHandler(handler, outputSchema) {
1020
1402
  }
1021
1403
  let output;
1022
1404
  try {
1023
- output = await handler({
1024
- input,
1025
- ctx: handlerContext
1026
- });
1027
- if (outputSchema) {
1028
- output = outputSchema.parse(output);
1405
+ const workerRetryConfig = options?.retry;
1406
+ const executeHandler = async (retryCtx) => {
1407
+ handlerContext.retryContext = retryCtx;
1408
+ const result = await handler({ input, ctx: handlerContext });
1409
+ return outputSchema ? outputSchema.parse(result) : result;
1410
+ };
1411
+ if (workerRetryConfig && workerRetryConfig.on.length > 0) {
1412
+ output = await executeWithRetry(executeHandler, workerRetryConfig, (retryCtx, delayMs) => {
1413
+ logger.warn(
1414
+ `[worker-retry] Retrying handler (attempt ${retryCtx.attempt}/${retryCtx.maxAttempts}): ${retryCtx.lastError.message}`,
1415
+ { delayMs }
1416
+ );
1417
+ });
1418
+ } else {
1419
+ output = await executeHandler(void 0);
1029
1420
  }
1030
1421
  } catch (error) {
1031
1422
  const errorPayload = {
@@ -1057,7 +1448,7 @@ function createLambdaHandler(handler, outputSchema) {
1057
1448
  });
1058
1449
  }
1059
1450
  }
1060
- const queueCtxFail = input?.__workerQueue ?? metadata?.__workerQueue;
1451
+ const queueCtxFail = getWorkerQueueContext(input, metadata);
1061
1452
  if (queueCtxFail?.queueJobId && typeof queueCtxFail.stepIndex === "number") {
1062
1453
  await notifyQueueJobStep(queueCtxFail.queueJobId, "fail", {
1063
1454
  queueId: queueCtxFail.id,
@@ -1121,6 +1512,7 @@ function createLambdaHandler(handler, outputSchema) {
1121
1512
  // Annotate the CommonJS export names for ESM import in node:
1122
1513
  0 && (module.exports = {
1123
1514
  SQS_MAX_DELAY_SECONDS,
1515
+ TokenBudgetExceededError,
1124
1516
  createLambdaHandler,
1125
1517
  createWorkerLogger,
1126
1518
  wrapHandlerForQueue