pipeai 0.1.0 → 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/dist/index.cjs CHANGED
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  Workflow: () => Workflow,
25
25
  WorkflowBranchError: () => WorkflowBranchError,
26
26
  WorkflowLoopError: () => WorkflowLoopError,
27
+ WorkflowSuspended: () => WorkflowSuspended,
27
28
  defineTool: () => defineTool
28
29
  });
29
30
  module.exports = __toCommonJS(index_exports);
@@ -33,6 +34,31 @@ var import_ai2 = require("ai");
33
34
 
34
35
  // src/tool-provider.ts
35
36
  var import_ai = require("ai");
37
+
38
+ // src/utils.ts
39
+ var import_node_async_hooks = require("async_hooks");
40
+ var writerStorage = new import_node_async_hooks.AsyncLocalStorage();
41
+ function runWithWriter(writer, fn) {
42
+ return writerStorage.run(writer, fn);
43
+ }
44
+ function getActiveWriter() {
45
+ return writerStorage.getStore();
46
+ }
47
+ function resolveValue(value, ctx, input) {
48
+ if (typeof value === "function") {
49
+ return value(ctx, input);
50
+ }
51
+ return value;
52
+ }
53
+ async function extractOutput(result, hasStructuredOutput) {
54
+ if (hasStructuredOutput) {
55
+ const output = await result.output;
56
+ if (output !== void 0) return output;
57
+ }
58
+ return await result.text;
59
+ }
60
+
61
+ // src/tool-provider.ts
36
62
  var TOOL_PROVIDER_BRAND = /* @__PURE__ */ Symbol.for("agent-workflow.ToolProvider");
37
63
  var ToolProvider = class {
38
64
  [TOOL_PROVIDER_BRAND] = true;
@@ -45,7 +71,7 @@ var ToolProvider = class {
45
71
  return (0, import_ai.tool)({
46
72
  ...toolDef,
47
73
  parameters: inputSchema,
48
- execute: (input, options) => execute(input, context, options)
74
+ execute: (input, options) => execute(input, context, { ...options, writer: getActiveWriter() })
49
75
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
76
  });
51
77
  }
@@ -57,21 +83,6 @@ function isToolProvider(obj) {
57
83
  return typeof obj === "object" && obj !== null && TOOL_PROVIDER_BRAND in obj;
58
84
  }
59
85
 
60
- // src/utils.ts
61
- function resolveValue(value, ctx, input) {
62
- if (typeof value === "function") {
63
- return value(ctx, input);
64
- }
65
- return value;
66
- }
67
- async function extractOutput(result, hasStructuredOutput) {
68
- if (hasStructuredOutput) {
69
- const output = await result.output;
70
- if (output !== void 0) return output;
71
- }
72
- return await result.text;
73
- }
74
-
75
86
  // src/agent.ts
76
87
  var Agent = class {
77
88
  id;
@@ -136,7 +147,7 @@ var Agent = class {
136
147
  return await (0, import_ai2.generateText)(options);
137
148
  } catch (error) {
138
149
  if (this.config.onError) {
139
- await this.config.onError({ error, ctx, input });
150
+ await this.config.onError({ error, ctx, input, writer: getActiveWriter() });
140
151
  }
141
152
  throw error;
142
153
  }
@@ -147,7 +158,7 @@ var Agent = class {
147
158
  const options = this.buildCallOptions(resolved, ctx, input);
148
159
  return (0, import_ai2.streamText)({
149
160
  ...options,
150
- onError: this.config.onError ? ({ error }) => this.config.onError({ error, ctx, input }) : void 0
161
+ onError: this.config.onError ? ({ error }) => this.config.onError({ error, ctx, input, writer: getActiveWriter() }) : void 0
151
162
  });
152
163
  }
153
164
  asTool(ctx, options) {
@@ -170,6 +181,13 @@ var Agent = class {
170
181
  description: this.description,
171
182
  parameters: this.config.input,
172
183
  execute: async (toolInput) => {
184
+ const writer = getActiveWriter();
185
+ if (writer) {
186
+ const result2 = await this.stream(ctx, toolInput);
187
+ writer.merge(result2.toUIMessageStream());
188
+ if (options?.mapOutput) return options.mapOutput(result2);
189
+ return extractOutput(result2, this.hasOutput);
190
+ }
173
191
  const result = await this.generate(ctx, toolInput);
174
192
  if (options?.mapOutput) return options.mapOutput(result);
175
193
  return extractOutput(result, this.hasOutput);
@@ -189,8 +207,8 @@ var Agent = class {
189
207
  ...resolved.messages ? { messages: resolved.messages } : { prompt: resolved.prompt ?? "" },
190
208
  ...resolved.system ? { system: resolved.system } : {},
191
209
  ...this.config.output ? { output: this.config.output } : {},
192
- onStepFinish: this._onStepFinish ? (event) => this._onStepFinish({ result: event, ctx, input }) : void 0,
193
- onFinish: this._onFinish ? (event) => this._onFinish({ result: event, ctx, input }) : void 0
210
+ onStepFinish: this._onStepFinish ? (event) => this._onStepFinish({ result: event, ctx, input, writer: getActiveWriter() }) : void 0,
211
+ onFinish: this._onFinish ? (event) => this._onFinish({ result: event, ctx, input, writer: getActiveWriter() }) : void 0
194
212
  };
195
213
  }
196
214
  resolveConfig(ctx, input) {
@@ -259,6 +277,14 @@ var WorkflowLoopError = class extends Error {
259
277
  this.name = "WorkflowLoopError";
260
278
  }
261
279
  };
280
+ var WorkflowSuspended = class extends Error {
281
+ snapshot;
282
+ constructor(snapshot) {
283
+ super(`Workflow suspended at gate "${snapshot.gateId}"`);
284
+ this.name = "WorkflowSuspended";
285
+ this.snapshot = snapshot;
286
+ }
287
+ };
262
288
  var SealedWorkflow = class {
263
289
  id;
264
290
  steps;
@@ -315,12 +341,13 @@ var SealedWorkflow = class {
315
341
  };
316
342
  }
317
343
  // ── Internal: execute pipeline ────────────────────────────────
318
- async execute(state) {
344
+ async execute(state, startIndex = 0) {
319
345
  if (this.steps.length === 0) {
320
346
  throw new Error("Workflow has no steps. Add at least one step before calling generate() or stream().");
321
347
  }
322
348
  let pendingError = null;
323
- for (const node of this.steps) {
349
+ for (let i = startIndex; i < this.steps.length; i++) {
350
+ const node = this.steps[i];
324
351
  if (node.type === "finally") {
325
352
  await node.execute(state);
326
353
  continue;
@@ -340,10 +367,26 @@ var SealedWorkflow = class {
340
367
  }
341
368
  continue;
342
369
  }
370
+ if (node.type === "gate") {
371
+ if (pendingError) continue;
372
+ if (node.condition) {
373
+ const shouldSuspend = await node.condition(state);
374
+ if (!shouldSuspend) continue;
375
+ }
376
+ const gatePayload = await node.payload(state);
377
+ throw new WorkflowSuspended({
378
+ version: 1,
379
+ resumeFromIndex: i,
380
+ output: state.output,
381
+ gateId: node.id,
382
+ gatePayload
383
+ });
384
+ }
343
385
  if (pendingError) continue;
344
386
  try {
345
387
  await node.execute(state);
346
388
  } catch (error) {
389
+ if (error instanceof WorkflowSuspended) throw error;
347
390
  pendingError = { error, stepId: node.id };
348
391
  }
349
392
  }
@@ -353,7 +396,16 @@ var SealedWorkflow = class {
353
396
  // Defined on SealedWorkflow (not Workflow) because TypeScript's protected
354
397
  // access rules only allow calling workflow.execute() from the same class.
355
398
  async executeNestedWorkflow(state, workflow) {
356
- await workflow.execute(state);
399
+ try {
400
+ await workflow.execute(state);
401
+ } catch (error) {
402
+ if (error instanceof WorkflowSuspended) {
403
+ throw new Error(
404
+ `Gates inside nested workflows are not yet supported. Gate "${error.snapshot.gateId}" was hit inside nested workflow "${workflow.id ?? "(anonymous)"}". Consider using a conditional gate with \`condition\` to skip when criteria are met, or restructure the workflow to use gates at the top level only.`
405
+ );
406
+ }
407
+ throw error;
408
+ }
357
409
  }
358
410
  // ── Internal: execute an agent within a step/branch ───────────
359
411
  // In stream mode, output extraction awaits the full stream before returning.
@@ -363,20 +415,23 @@ var SealedWorkflow = class {
363
415
  const input = state.output;
364
416
  const hasStructuredOutput = agent.hasOutput;
365
417
  if (state.mode === "stream" && state.writer) {
366
- const result = await agent.stream(ctx, state.output);
367
- if (options?.handleStream) {
368
- await options.handleStream({ result, writer: state.writer, ctx });
369
- } else {
370
- state.writer.merge(result.toUIMessageStream());
371
- }
372
- if (options?.onStreamResult) {
373
- await options.onStreamResult({ result, ctx, input });
374
- }
375
- if (options?.mapStreamResult) {
376
- state.output = await options.mapStreamResult({ result, ctx, input });
377
- } else {
378
- state.output = await extractOutput(result, hasStructuredOutput);
379
- }
418
+ const writer = state.writer;
419
+ await runWithWriter(writer, async () => {
420
+ const result = await agent.stream(ctx, state.output);
421
+ if (options?.handleStream) {
422
+ await options.handleStream({ result, writer, ctx });
423
+ } else {
424
+ writer.merge(result.toUIMessageStream());
425
+ }
426
+ if (options?.onStreamResult) {
427
+ await options.onStreamResult({ result, ctx, input });
428
+ }
429
+ if (options?.mapStreamResult) {
430
+ state.output = await options.mapStreamResult({ result, ctx, input });
431
+ } else {
432
+ state.output = await extractOutput(result, hasStructuredOutput);
433
+ }
434
+ });
380
435
  } else {
381
436
  const result = await agent.generate(ctx, state.output);
382
437
  if (options?.onGenerateResult) {
@@ -389,6 +444,106 @@ var SealedWorkflow = class {
389
444
  }
390
445
  }
391
446
  }
447
+ // ── Gate: load persisted state for resumption ──────────────────
448
+ loadState(gateId, snapshot) {
449
+ if (snapshot.gateId !== gateId) {
450
+ throw new Error(
451
+ `loadState: gate ID mismatch \u2014 expected "${gateId}" but snapshot has "${snapshot.gateId}".`
452
+ );
453
+ }
454
+ const gateIndex = this.findGateIndex(snapshot);
455
+ const gateNode = this.steps[gateIndex];
456
+ return new ResumedWorkflow(
457
+ this.steps,
458
+ gateIndex + 1,
459
+ gateNode.schema,
460
+ gateNode.merge,
461
+ snapshot.output
462
+ );
463
+ }
464
+ findGateIndex(snapshot) {
465
+ if (snapshot.version !== 1) {
466
+ throw new Error(`Unsupported snapshot version: ${snapshot.version}`);
467
+ }
468
+ const hint = snapshot.resumeFromIndex;
469
+ if (hint >= 0 && hint < this.steps.length) {
470
+ const node = this.steps[hint];
471
+ if (node.type === "gate" && node.id === snapshot.gateId) {
472
+ return hint;
473
+ }
474
+ }
475
+ for (let i = 0; i < this.steps.length; i++) {
476
+ const node = this.steps[i];
477
+ if (node.type === "gate" && node.id === snapshot.gateId) {
478
+ return i;
479
+ }
480
+ }
481
+ throw new Error(
482
+ `Gate "${snapshot.gateId}" not found in workflow. The workflow definition may have changed since the snapshot was created.`
483
+ );
484
+ }
485
+ };
486
+ var ResumedWorkflow = class extends SealedWorkflow {
487
+ startIndex;
488
+ schema;
489
+ mergeFn;
490
+ priorOutput;
491
+ /** @internal */
492
+ constructor(steps, startIndex, schema, mergeFn, priorOutput) {
493
+ super(steps);
494
+ this.startIndex = startIndex;
495
+ this.schema = schema;
496
+ this.mergeFn = mergeFn;
497
+ this.priorOutput = priorOutput;
498
+ }
499
+ validateResponse(response) {
500
+ if (this.schema) {
501
+ return this.schema.parse(response);
502
+ }
503
+ return response;
504
+ }
505
+ async generate(ctx, ...args) {
506
+ const response = this.validateResponse(args[0]);
507
+ const output = this.mergeFn ? await this.mergeFn({ priorOutput: this.priorOutput, response }) : response;
508
+ const state = { ctx, output, mode: "generate" };
509
+ await this.execute(state, this.startIndex);
510
+ return { output: state.output };
511
+ }
512
+ stream(ctx, ...args) {
513
+ const response = this.validateResponse(args[0]);
514
+ const options = args[1];
515
+ let resolveOutput;
516
+ let rejectOutput;
517
+ const outputPromise = new Promise((res, rej) => {
518
+ resolveOutput = res;
519
+ rejectOutput = rej;
520
+ });
521
+ outputPromise.catch(() => {
522
+ });
523
+ const mergeFn = this.mergeFn;
524
+ const priorOutput = this.priorOutput;
525
+ const stream = (0, import_ai3.createUIMessageStream)({
526
+ execute: async ({ writer }) => {
527
+ const output = mergeFn ? await mergeFn({ priorOutput, response }) : response;
528
+ const state = {
529
+ ctx,
530
+ output,
531
+ mode: "stream",
532
+ writer
533
+ };
534
+ try {
535
+ await this.execute(state, this.startIndex);
536
+ resolveOutput(state.output);
537
+ } catch (error) {
538
+ rejectOutput(error);
539
+ throw error;
540
+ }
541
+ },
542
+ ...options?.onError ? { onError: options.onError } : {},
543
+ ...options?.onFinish ? { onFinish: options.onFinish } : {}
544
+ });
545
+ return { stream, output: outputPromise };
546
+ }
392
547
  };
393
548
  var Workflow = class _Workflow extends SealedWorkflow {
394
549
  constructor(steps = [], id) {
@@ -442,6 +597,32 @@ var Workflow = class _Workflow extends SealedWorkflow {
442
597
  };
443
598
  return new _Workflow([...this.steps, node], this.id);
444
599
  }
600
+ // ── gate: human-in-the-loop suspension point ────────────────
601
+ gate(id, options) {
602
+ if (this.steps.some((s) => s.type === "gate" && s.id === id)) {
603
+ throw new Error(`Workflow: duplicate gate ID "${id}". Each gate must have a unique identifier.`);
604
+ }
605
+ const node = {
606
+ type: "gate",
607
+ id,
608
+ schema: options?.schema,
609
+ condition: options?.condition ? async (state) => options.condition({
610
+ ctx: state.ctx,
611
+ input: state.output
612
+ }) : void 0,
613
+ merge: options?.merge ? (params) => options.merge(params) : void 0,
614
+ payload: async (state) => {
615
+ if (options?.payload) {
616
+ return options.payload({
617
+ ctx: state.ctx,
618
+ input: state.output
619
+ });
620
+ }
621
+ return state.output;
622
+ }
623
+ };
624
+ return new _Workflow([...this.steps, node], this.id);
625
+ }
445
626
  // ── branch: implementation ────────────────────────────────────
446
627
  branch(casesOrConfig) {
447
628
  if (Array.isArray(casesOrConfig)) {
@@ -588,6 +769,7 @@ var Workflow = class _Workflow extends SealedWorkflow {
588
769
  Workflow,
589
770
  WorkflowBranchError,
590
771
  WorkflowLoopError,
772
+ WorkflowSuspended,
591
773
  defineTool
592
774
  });
593
775
  //# sourceMappingURL=index.cjs.map