@aikirun/task 0.16.0 → 0.18.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/dist/index.d.ts CHANGED
@@ -11,10 +11,7 @@ type IsSubtype<SubT, SuperT> = SubT extends SuperT ? true : false;
11
11
  type And<T extends NonEmptyArray<boolean>> = T extends [infer First, ...infer Rest] ? false extends First ? false : Rest extends NonEmptyArray<boolean> ? And<Rest> : true : never;
12
12
  type Or<T extends NonEmptyArray<boolean>> = T extends [infer First, ...infer Rest] ? true extends First ? true : Rest extends NonEmptyArray<boolean> ? Or<Rest> : false : never;
13
13
  type PathFromObject<T, IncludeArrayKeys extends boolean = false> = T extends T ? PathFromObjectInternal<T, IncludeArrayKeys> : never;
14
- type PathFromObjectInternal<T, IncludeArrayKeys extends boolean> = And<[
15
- IsSubtype<T, object>,
16
- Or<[IncludeArrayKeys, NonArrayObject<T> extends never ? false : true]>
17
- ]> extends true ? {
14
+ type PathFromObjectInternal<T, IncludeArrayKeys extends boolean> = And<[IsSubtype<T, object>, Or<[IncludeArrayKeys, NonArrayObject<T> extends never ? false : true]>]> extends true ? {
18
15
  [K in Exclude<keyof T, symbol>]-?: And<[
19
16
  IsSubtype<NonNullable<T[K]>, object>,
20
17
  Or<[IncludeArrayKeys, NonArrayObject<NonNullable<T[K]>> extends never ? false : true]>
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // ../../lib/address/index.ts
2
- function getTaskAddress(name, referenceId) {
3
- return `${name}/${referenceId}`;
2
+ function getTaskAddress(name, inputHash) {
3
+ return `${name}:${inputHash}`;
4
4
  }
5
5
 
6
6
  // ../../lib/async/delay.ts
@@ -22,6 +22,9 @@ function delay(ms, options) {
22
22
  });
23
23
  }
24
24
 
25
+ // ../../lib/crypto/hash.ts
26
+ import { createHash } from "crypto";
27
+
25
28
  // ../../lib/json/stable-stringify.ts
26
29
  function stableStringify(value) {
27
30
  return stringifyValue(value);
@@ -153,6 +156,7 @@ function getRetryParams(attempts, strategy) {
153
156
  import { INTERNAL } from "@aikirun/types/symbols";
154
157
  import { TaskFailedError } from "@aikirun/types/task";
155
158
  import {
159
+ NonDeterminismError,
156
160
  WorkflowRunFailedError,
157
161
  WorkflowRunRevisionConflictError,
158
162
  WorkflowRunSuspendedError
@@ -180,40 +184,18 @@ var TaskImpl = class {
180
184
  const inputRaw = args[0];
181
185
  const input = await this.parse(handle, this.params.schema?.input, inputRaw, run.logger);
182
186
  const inputHash = await hashInput(input);
183
- const reference = startOpts.reference;
184
- const address = getTaskAddress(this.name, reference?.id ?? inputHash);
185
- const existingTaskInfo = handle.run.tasks[address];
186
- if (existingTaskInfo) {
187
- await this.assertUniqueTaskReferenceId(handle, existingTaskInfo, inputHash, reference, run.logger);
188
- }
189
- if (existingTaskInfo?.state.status === "completed") {
190
- return this.parse(handle, this.params.schema?.output, existingTaskInfo.state.output, run.logger);
191
- }
192
- if (existingTaskInfo?.state.status === "failed") {
193
- const { state } = existingTaskInfo;
194
- throw new TaskFailedError(existingTaskInfo.id, state.attempts, state.error.message);
187
+ const address = getTaskAddress(this.name, inputHash);
188
+ const replayManifest = run[INTERNAL].replayManifest;
189
+ if (replayManifest.hasUnconsumedEntries()) {
190
+ const existingTaskInfo = replayManifest.consumeNextTask(address);
191
+ if (existingTaskInfo) {
192
+ return this.getExistingTaskResult(run, handle, startOpts, input, existingTaskInfo);
193
+ }
194
+ await this.throwNonDeterminismError(run, handle, inputHash, replayManifest);
195
195
  }
196
- let attempts = 0;
196
+ const attempts = 1;
197
197
  const retryStrategy = startOpts.retry ?? { type: "never" };
198
- if (existingTaskInfo?.state) {
199
- const taskId2 = existingTaskInfo.id;
200
- const state = existingTaskInfo?.state;
201
- attempts = state.attempts;
202
- this.assertRetryAllowed(taskId2, attempts, retryStrategy, run.logger);
203
- run.logger.debug("Retrying task", {
204
- "aiki.taskName": this.name,
205
- "aiki.taskId": taskId2,
206
- "aiki.attempts": attempts,
207
- "aiki.taskStatus": state.status
208
- });
209
- }
210
- attempts++;
211
- const { taskId } = existingTaskInfo ? await handle[INTERNAL].transitionTaskState({
212
- type: "retry",
213
- taskId: existingTaskInfo.id,
214
- options: startOpts,
215
- taskState: { status: "running", attempts, input }
216
- }) : await handle[INTERNAL].transitionTaskState({
198
+ const taskInfo = await handle[INTERNAL].transitionTaskState({
217
199
  type: "create",
218
200
  taskName: this.name,
219
201
  options: startOpts,
@@ -222,20 +204,89 @@ var TaskImpl = class {
222
204
  const logger = run.logger.child({
223
205
  "aiki.component": "task-execution",
224
206
  "aiki.taskName": this.name,
225
- "aiki.taskId": taskId
207
+ "aiki.taskId": taskInfo.id
226
208
  });
227
209
  logger.info("Task started", { "aiki.attempts": attempts });
228
210
  const { output, lastAttempt } = await this.tryExecuteTask(
229
211
  handle,
230
212
  input,
231
- taskId,
213
+ taskInfo.id,
232
214
  retryStrategy,
233
215
  attempts,
234
216
  run[INTERNAL].options.spinThresholdMs,
235
217
  logger
236
218
  );
237
219
  await handle[INTERNAL].transitionTaskState({
220
+ taskId: taskInfo.id,
221
+ taskState: { status: "completed", attempts: lastAttempt, output }
222
+ });
223
+ logger.info("Task complete", { "aiki.attempts": lastAttempt });
224
+ return output;
225
+ }
226
+ async getExistingTaskResult(run, handle, startOpts, input, existingTaskInfo) {
227
+ const existingTaskState = existingTaskInfo.state;
228
+ if (existingTaskState.status === "completed") {
229
+ return this.parse(handle, this.params.schema?.output, existingTaskState.output, run.logger);
230
+ }
231
+ if (existingTaskState.status === "failed") {
232
+ throw new TaskFailedError(
233
+ existingTaskInfo.id,
234
+ existingTaskState.attempts,
235
+ existingTaskState.error.message
236
+ );
237
+ }
238
+ existingTaskState.status;
239
+ const attempts = existingTaskState.attempts;
240
+ const retryStrategy = startOpts.retry ?? { type: "never" };
241
+ this.assertRetryAllowed(existingTaskInfo.id, attempts, retryStrategy, run.logger);
242
+ run.logger.debug("Retrying task", {
243
+ "aiki.taskName": this.name,
244
+ "aiki.taskId": existingTaskInfo.id,
245
+ "aiki.attempts": attempts,
246
+ "aiki.taskStatus": existingTaskState.status
247
+ });
248
+ return this.retryAndExecute(run, handle, input, existingTaskInfo.id, startOpts, retryStrategy, attempts);
249
+ }
250
+ async throwNonDeterminismError(run, handle, inputHash, manifest) {
251
+ const unconsumedManifestEntries = manifest.getUnconsumedEntries();
252
+ run.logger.error("Replay divergence", {
253
+ "aiki.taskName": this.name,
254
+ "aiki.inputHash": inputHash,
255
+ "aiki.unconsumedManifestEntries": unconsumedManifestEntries
256
+ });
257
+ const error = new NonDeterminismError(run.id, handle.run.attempts, unconsumedManifestEntries);
258
+ await handle[INTERNAL].transitionState({
259
+ status: "failed",
260
+ cause: "self",
261
+ error: createSerializableError(error)
262
+ });
263
+ throw error;
264
+ }
265
+ async retryAndExecute(run, handle, input, taskId, startOpts, retryStrategy, previousAttempts) {
266
+ const attempts = previousAttempts + 1;
267
+ const taskInfo = await handle[INTERNAL].transitionTaskState({
268
+ type: "retry",
238
269
  taskId,
270
+ options: startOpts,
271
+ taskState: { status: "running", attempts, input }
272
+ });
273
+ const logger = run.logger.child({
274
+ "aiki.component": "task-execution",
275
+ "aiki.taskName": this.name,
276
+ "aiki.taskId": taskInfo.id
277
+ });
278
+ logger.info("Task started", { "aiki.attempts": attempts });
279
+ const { output, lastAttempt } = await this.tryExecuteTask(
280
+ handle,
281
+ input,
282
+ taskInfo.id,
283
+ retryStrategy,
284
+ attempts,
285
+ run[INTERNAL].options.spinThresholdMs,
286
+ logger
287
+ );
288
+ await handle[INTERNAL].transitionTaskState({
289
+ taskId: taskInfo.id,
239
290
  taskState: { status: "completed", attempts: lastAttempt, output }
240
291
  });
241
292
  logger.info("Task complete", { "aiki.attempts": lastAttempt });
@@ -249,7 +300,7 @@ var TaskImpl = class {
249
300
  const output = await this.parse(handle, this.params.schema?.output, outputRaw, logger);
250
301
  return { output, lastAttempt: attempts };
251
302
  } catch (error) {
252
- if (error instanceof WorkflowRunFailedError || error instanceof WorkflowRunSuspendedError || error instanceof WorkflowRunRevisionConflictError) {
303
+ if (error instanceof WorkflowRunSuspendedError || error instanceof WorkflowRunFailedError || error instanceof WorkflowRunRevisionConflictError) {
253
304
  throw error;
254
305
  }
255
306
  const serializableError = createSerializableError(error);
@@ -288,30 +339,6 @@ var TaskImpl = class {
288
339
  }
289
340
  }
290
341
  }
291
- async assertUniqueTaskReferenceId(handle, existingTaskInfo, inputHash, reference, logger) {
292
- if (existingTaskInfo.inputHash !== inputHash && reference) {
293
- const conflictPolicy = reference.conflictPolicy ?? "error";
294
- if (conflictPolicy !== "error") {
295
- return;
296
- }
297
- logger.error("Reference ID already used by another task", {
298
- "aiki.taskName": this.name,
299
- "aiki.referenceId": reference.id,
300
- "aiki.existingTaskId": existingTaskInfo.id
301
- });
302
- const error = new WorkflowRunFailedError(
303
- handle.run.id,
304
- handle.run.attempts,
305
- `Reference ID "${reference.id}" already used by another task ${existingTaskInfo.id}`
306
- );
307
- await handle[INTERNAL].transitionState({
308
- status: "failed",
309
- cause: "self",
310
- error: createSerializableError(error)
311
- });
312
- throw error;
313
- }
314
- }
315
342
  assertRetryAllowed(taskId, attempts, retryStrategy, logger) {
316
343
  const retryParams = getRetryParams(attempts, retryStrategy);
317
344
  if (!retryParams.retriesLeft) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikirun/task",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Task SDK for Aiki - define reliable tasks with automatic retries, idempotency, and error handling",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,8 +18,8 @@
18
18
  "build": "tsup"
19
19
  },
20
20
  "dependencies": {
21
- "@aikirun/types": "0.16.0",
22
- "@aikirun/workflow": "0.16.0",
21
+ "@aikirun/types": "0.18.0",
22
+ "@aikirun/workflow": "0.18.0",
23
23
  "@standard-schema/spec": "^1.1.0"
24
24
  },
25
25
  "publishConfig": {