@aikirun/task 0.17.0 → 0.19.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/README.md CHANGED
@@ -43,7 +43,6 @@ export const notificationWorkflowV1 = notificationWorkflow.v("1.0.0", {
43
43
 
44
44
  - **Automatic Retries** - Configurable retry strategies (fixed, exponential, jittered)
45
45
  - **Idempotent Execution** - Same input returns cached result
46
- - **Reference IDs** - Custom identifiers for deduplication
47
46
  - **Schema Validation** - Validate input and output at runtime
48
47
  - **Type Safety** - Full TypeScript support
49
48
 
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
@@ -156,6 +156,7 @@ function getRetryParams(attempts, strategy) {
156
156
  import { INTERNAL } from "@aikirun/types/symbols";
157
157
  import { TaskFailedError } from "@aikirun/types/task";
158
158
  import {
159
+ NonDeterminismError,
159
160
  WorkflowRunFailedError,
160
161
  WorkflowRunRevisionConflictError,
161
162
  WorkflowRunSuspendedError
@@ -183,40 +184,18 @@ var TaskImpl = class {
183
184
  const inputRaw = args[0];
184
185
  const input = await this.parse(handle, this.params.schema?.input, inputRaw, run.logger);
185
186
  const inputHash = await hashInput(input);
186
- const reference = startOpts.reference;
187
- const address = getTaskAddress(this.name, reference?.id ?? inputHash);
188
- const existingTaskInfo = handle.run.tasks[address];
189
- if (existingTaskInfo) {
190
- await this.assertUniqueTaskReferenceId(handle, existingTaskInfo, inputHash, reference, run.logger);
191
- }
192
- if (existingTaskInfo?.state.status === "completed") {
193
- return this.parse(handle, this.params.schema?.output, existingTaskInfo.state.output, run.logger);
194
- }
195
- if (existingTaskInfo?.state.status === "failed") {
196
- const { state } = existingTaskInfo;
197
- 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);
198
195
  }
199
- let attempts = 0;
196
+ const attempts = 1;
200
197
  const retryStrategy = startOpts.retry ?? { type: "never" };
201
- if (existingTaskInfo?.state) {
202
- const taskId2 = existingTaskInfo.id;
203
- const state = existingTaskInfo?.state;
204
- attempts = state.attempts;
205
- this.assertRetryAllowed(taskId2, attempts, retryStrategy, run.logger);
206
- run.logger.debug("Retrying task", {
207
- "aiki.taskName": this.name,
208
- "aiki.taskId": taskId2,
209
- "aiki.attempts": attempts,
210
- "aiki.taskStatus": state.status
211
- });
212
- }
213
- attempts++;
214
- const { taskId } = existingTaskInfo ? await handle[INTERNAL].transitionTaskState({
215
- type: "retry",
216
- taskId: existingTaskInfo.id,
217
- options: startOpts,
218
- taskState: { status: "running", attempts, input }
219
- }) : await handle[INTERNAL].transitionTaskState({
198
+ const taskInfo = await handle[INTERNAL].transitionTaskState({
220
199
  type: "create",
221
200
  taskName: this.name,
222
201
  options: startOpts,
@@ -225,20 +204,89 @@ var TaskImpl = class {
225
204
  const logger = run.logger.child({
226
205
  "aiki.component": "task-execution",
227
206
  "aiki.taskName": this.name,
228
- "aiki.taskId": taskId
207
+ "aiki.taskId": taskInfo.id
229
208
  });
230
209
  logger.info("Task started", { "aiki.attempts": attempts });
231
210
  const { output, lastAttempt } = await this.tryExecuteTask(
232
211
  handle,
233
212
  input,
234
- taskId,
213
+ taskInfo.id,
235
214
  retryStrategy,
236
215
  attempts,
237
216
  run[INTERNAL].options.spinThresholdMs,
238
217
  logger
239
218
  );
240
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",
241
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,
242
290
  taskState: { status: "completed", attempts: lastAttempt, output }
243
291
  });
244
292
  logger.info("Task complete", { "aiki.attempts": lastAttempt });
@@ -252,7 +300,7 @@ var TaskImpl = class {
252
300
  const output = await this.parse(handle, this.params.schema?.output, outputRaw, logger);
253
301
  return { output, lastAttempt: attempts };
254
302
  } catch (error) {
255
- if (error instanceof WorkflowRunFailedError || error instanceof WorkflowRunSuspendedError || error instanceof WorkflowRunRevisionConflictError) {
303
+ if (error instanceof WorkflowRunSuspendedError || error instanceof WorkflowRunFailedError || error instanceof WorkflowRunRevisionConflictError) {
256
304
  throw error;
257
305
  }
258
306
  const serializableError = createSerializableError(error);
@@ -291,30 +339,6 @@ var TaskImpl = class {
291
339
  }
292
340
  }
293
341
  }
294
- async assertUniqueTaskReferenceId(handle, existingTaskInfo, inputHash, reference, logger) {
295
- if (existingTaskInfo.inputHash !== inputHash && reference) {
296
- const conflictPolicy = reference.conflictPolicy ?? "error";
297
- if (conflictPolicy !== "error") {
298
- return;
299
- }
300
- logger.error("Reference ID already used by another task", {
301
- "aiki.taskName": this.name,
302
- "aiki.referenceId": reference.id,
303
- "aiki.existingTaskId": existingTaskInfo.id
304
- });
305
- const error = new WorkflowRunFailedError(
306
- handle.run.id,
307
- handle.run.attempts,
308
- `Reference ID "${reference.id}" already used by another task ${existingTaskInfo.id}`
309
- );
310
- await handle[INTERNAL].transitionState({
311
- status: "failed",
312
- cause: "self",
313
- error: createSerializableError(error)
314
- });
315
- throw error;
316
- }
317
- }
318
342
  assertRetryAllowed(taskId, attempts, retryStrategy, logger) {
319
343
  const retryParams = getRetryParams(attempts, retryStrategy);
320
344
  if (!retryParams.retriesLeft) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikirun/task",
3
- "version": "0.17.0",
3
+ "version": "0.19.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.17.0",
22
- "@aikirun/workflow": "0.17.0",
21
+ "@aikirun/types": "0.19.0",
22
+ "@aikirun/workflow": "0.19.0",
23
23
  "@standard-schema/spec": "^1.1.0"
24
24
  },
25
25
  "publishConfig": {