@tagma/sdk 0.7.0 → 0.7.3

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 (72) hide show
  1. package/README.md +84 -44
  2. package/dist/bootstrap.d.ts +20 -0
  3. package/dist/bootstrap.d.ts.map +1 -1
  4. package/dist/bootstrap.js +21 -11
  5. package/dist/bootstrap.js.map +1 -1
  6. package/dist/core/dataflow.d.ts.map +1 -1
  7. package/dist/core/dataflow.js +45 -9
  8. package/dist/core/dataflow.js.map +1 -1
  9. package/dist/core/run-context.d.ts +3 -0
  10. package/dist/core/run-context.d.ts.map +1 -1
  11. package/dist/core/run-context.js +2 -0
  12. package/dist/core/run-context.js.map +1 -1
  13. package/dist/core/task-executor.d.ts.map +1 -1
  14. package/dist/core/task-executor.js +46 -84
  15. package/dist/core/task-executor.js.map +1 -1
  16. package/dist/engine.d.ts +6 -0
  17. package/dist/engine.d.ts.map +1 -1
  18. package/dist/engine.js +3 -0
  19. package/dist/engine.js.map +1 -1
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/plugins.d.ts +2 -2
  25. package/dist/plugins.d.ts.map +1 -1
  26. package/dist/ports.d.ts +4 -0
  27. package/dist/ports.d.ts.map +1 -1
  28. package/dist/ports.js +27 -4
  29. package/dist/ports.js.map +1 -1
  30. package/dist/registry.d.ts +10 -4
  31. package/dist/registry.d.ts.map +1 -1
  32. package/dist/registry.js +64 -25
  33. package/dist/registry.js.map +1 -1
  34. package/dist/runtime.d.ts +9 -0
  35. package/dist/runtime.d.ts.map +1 -0
  36. package/dist/runtime.js +8 -0
  37. package/dist/runtime.js.map +1 -0
  38. package/dist/schema.d.ts.map +1 -1
  39. package/dist/schema.js +1 -7
  40. package/dist/schema.js.map +1 -1
  41. package/dist/tagma.d.ts +11 -1
  42. package/dist/tagma.d.ts.map +1 -1
  43. package/dist/tagma.js +6 -0
  44. package/dist/tagma.js.map +1 -1
  45. package/dist/validate-raw.d.ts +4 -4
  46. package/dist/validate-raw.d.ts.map +1 -1
  47. package/dist/validate-raw.js +89 -230
  48. package/dist/validate-raw.js.map +1 -1
  49. package/package.json +2 -2
  50. package/src/bootstrap.ts +23 -14
  51. package/src/core/dataflow.test.ts +8 -9
  52. package/src/core/dataflow.ts +57 -14
  53. package/src/core/run-context.test.ts +12 -0
  54. package/src/core/run-context.ts +4 -0
  55. package/src/core/task-executor.ts +75 -135
  56. package/src/engine-ports-mixed.test.ts +68 -411
  57. package/src/engine-ports.test.ts +37 -341
  58. package/src/engine.ts +8 -0
  59. package/src/index.ts +5 -0
  60. package/src/pipeline-runner.test.ts +5 -9
  61. package/src/plugin-registry.test.ts +138 -1
  62. package/src/plugins.ts +5 -2
  63. package/src/ports.test.ts +80 -0
  64. package/src/ports.ts +36 -4
  65. package/src/registry.ts +81 -26
  66. package/src/runtime.ts +20 -0
  67. package/src/schema-ports.test.ts +47 -197
  68. package/src/schema.ts +1 -7
  69. package/src/tagma.test.ts +72 -1
  70. package/src/tagma.ts +16 -1
  71. package/src/validate-raw-ports.test.ts +80 -393
  72. package/src/validate-raw.ts +90 -250
@@ -12,7 +12,6 @@ import type {
12
12
  TriggerPlugin,
13
13
  } from '../types';
14
14
  import type { PluginRegistry } from '../registry';
15
- import { runCommand, runSpawn } from '../runner';
16
15
  import { parseDuration, nowISO } from '../utils';
17
16
  import {
18
17
  promptDocumentFromString,
@@ -21,11 +20,7 @@ import {
21
20
  renderInputsBlock,
22
21
  renderOutputSchemaBlock,
23
22
  } from '../prompt-doc';
24
- import {
25
- resolveTaskBindingInputs,
26
- resolveTaskInputs,
27
- substituteInputs,
28
- } from '../ports';
23
+ import { resolveTaskBindingInputs, resolveTaskInputs, substituteInputs } from '../ports';
29
24
  import { executeHook, buildTaskContext } from '../hooks';
30
25
  import { clip, tailLines, type Logger } from '../logger';
31
26
  import type { ApprovalGateway } from '../approval';
@@ -35,15 +30,17 @@ import { TriggerBlockedError, TriggerTimeoutError } from './trigger-errors';
35
30
 
36
31
  const MAX_NORMALIZED_BYTES = 1_000_000;
37
32
 
38
- function isPromptTaskConfig(
39
- task: { readonly prompt?: string; readonly command?: string },
40
- ): task is { readonly prompt: string; readonly command?: undefined } {
33
+ function isPromptTaskConfig(task: {
34
+ readonly prompt?: string;
35
+ readonly command?: string;
36
+ }): task is { readonly prompt: string; readonly command?: undefined } {
41
37
  return task.prompt !== undefined && task.command === undefined;
42
38
  }
43
39
 
44
- function isCommandTaskConfig(
45
- task: { readonly command?: string; readonly prompt?: string },
46
- ): task is { readonly command: string; readonly prompt?: undefined } {
40
+ function isCommandTaskConfig(task: {
41
+ readonly command?: string;
42
+ readonly prompt?: string;
43
+ }): task is { readonly command: string; readonly prompt?: undefined } {
47
44
  return task.command !== undefined && task.prompt === undefined;
48
45
  }
49
46
 
@@ -196,7 +193,12 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<void> {
196
193
  const hookResult = await executeHook(
197
194
  config.hooks,
198
195
  'task_start',
199
- buildTaskContext('task_start', pipelineInfo, ctx.trackInfoOf(taskId), ctx.buildTaskInfoObj(taskId)),
196
+ buildTaskContext(
197
+ 'task_start',
198
+ pipelineInfo,
199
+ ctx.trackInfoOf(taskId),
200
+ ctx.buildTaskInfoObj(taskId),
201
+ ),
200
202
  workDir,
201
203
  ctx.abortController.signal,
202
204
  );
@@ -309,50 +311,48 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<void> {
309
311
  );
310
312
  }
311
313
 
312
- // Feed effective ports into `resolveTaskInputs` by shallow-cloning
313
- // the task. Prompt tasks get the inferred ports; Command tasks are
314
- // unchanged (effectivePorts === task.ports).
315
- const taskForResolve: TaskConfig =
316
- effectivePorts === task.ports ? task : { ...task, ports: effectivePorts };
317
- const inputResolution = resolveTaskInputs(taskForResolve, ctx.outputValuesMap, node.dependsOn);
318
- if (inputResolution.kind === 'blocked') {
319
- log.error(
320
- `[task:${taskId}]`,
321
- `blocked — cannot resolve port inputs:\n${inputResolution.reason}`,
314
+ let inferredPromptInputs: Readonly<Record<string, unknown>> = {};
315
+ if (isPromptTask && effectivePorts?.inputs && effectivePorts.inputs.length > 0) {
316
+ const inputResolution = resolveTaskInputs(
317
+ { ...task, ports: effectivePorts },
318
+ ctx.outputValuesMap,
319
+ node.dependsOn,
322
320
  );
323
- state.result = {
324
- exitCode: -1,
325
- stdout: '',
326
- stderr: `[engine] port input resolution failed:\n${inputResolution.reason}`,
327
- stdoutPath: null,
328
- stderrPath: null,
329
- durationMs: 0,
330
- sessionId: null,
331
- normalizedOutput: null,
332
- failureKind: 'spawn_error',
333
- outputs: null,
334
- };
335
- state.finishedAt = nowISO();
336
- ctx.setTaskStatus(taskId, 'blocked');
337
- try {
338
- await ctx.fireHook(taskId, 'task_failure');
339
- } catch (hookErr) {
321
+ if (inputResolution.kind === 'blocked') {
340
322
  log.error(
341
323
  `[task:${taskId}]`,
342
- `hook execution failed: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`,
324
+ `blocked cannot resolve inferred prompt inputs:\n${inputResolution.reason}`,
343
325
  );
326
+ state.result = {
327
+ exitCode: -1,
328
+ stdout: '',
329
+ stderr: `[engine] inferred prompt input resolution failed:\n${inputResolution.reason}`,
330
+ stdoutPath: null,
331
+ stderrPath: null,
332
+ durationMs: 0,
333
+ sessionId: null,
334
+ normalizedOutput: null,
335
+ failureKind: 'spawn_error',
336
+ outputs: null,
337
+ };
338
+ state.finishedAt = nowISO();
339
+ ctx.setTaskStatus(taskId, 'blocked');
340
+ try {
341
+ await ctx.fireHook(taskId, 'task_failure');
342
+ } catch (hookErr) {
343
+ log.error(
344
+ `[task:${taskId}]`,
345
+ `hook execution failed: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`,
346
+ );
347
+ }
348
+ if (ctx.getOnFailure(taskId) === 'stop_all') ctx.applyStopAll();
349
+ return;
344
350
  }
345
- if (ctx.getOnFailure(taskId) === 'stop_all') ctx.applyStopAll();
346
- return;
351
+ inferredPromptInputs = inputResolution.inputs;
347
352
  }
348
- const resolvedInputs = { ...bindingResolution.inputs, ...inputResolution.inputs };
353
+
354
+ const resolvedInputs = { ...inferredPromptInputs, ...bindingResolution.inputs };
349
355
  ctx.resolvedInputsMap.set(taskId, resolvedInputs);
350
- if (inputResolution.missingOptional.length > 0) {
351
- log.debug(
352
- `[task:${taskId}]`,
353
- `optional inputs unresolved (empty in placeholders): ${inputResolution.missingOptional.join(', ')}`,
354
- );
355
- }
356
356
  if (effectivePorts?.inputs && effectivePorts.inputs.length > 0) {
357
357
  log.debug(
358
358
  `[task:${taskId}]`,
@@ -413,10 +413,7 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<void> {
413
413
  // errors, so the only way to land here with an unresolved is an
414
414
  // optional input that had no upstream producer and no default,
415
415
  // which we surface in the log.
416
- const { text: expandedCommand, unresolved } = substituteInputs(
417
- task.command,
418
- resolvedInputs,
419
- );
416
+ const { text: expandedCommand, unresolved } = substituteInputs(task.command, resolvedInputs);
420
417
  if (unresolved.length > 0) {
421
418
  log.debug(
422
419
  `[task:${taskId}]`,
@@ -424,7 +421,7 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<void> {
424
421
  );
425
422
  }
426
423
  log.debug(`[task:${taskId}]`, `command: ${expandedCommand}`);
427
- result = await runCommand(expandedCommand, task.cwd ?? workDir, runOpts);
424
+ result = await ctx.runtime.runCommand(expandedCommand, task.cwd ?? workDir, runOpts);
428
425
  } else {
429
426
  // AI task: apply middleware chain against a structured PromptDocument.
430
427
  const driverName = task.driver ?? track.driver ?? config.driver ?? 'opencode';
@@ -433,10 +430,7 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<void> {
433
430
  // Substitute placeholders in the user-authored prompt before
434
431
  // wrapping into a PromptDocument so middlewares see the
435
432
  // already-resolved task text.
436
- const { text: expandedPrompt, unresolved } = substituteInputs(
437
- task.prompt!,
438
- resolvedInputs,
439
- );
433
+ const { text: expandedPrompt, unresolved } = substituteInputs(task.prompt!, resolvedInputs);
440
434
  if (unresolved.length > 0) {
441
435
  log.debug(
442
436
  `[task:${taskId}]`,
@@ -460,10 +454,7 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<void> {
460
454
  }
461
455
  const mws = task.middlewares !== undefined ? task.middlewares : track.middlewares;
462
456
  if (mws && mws.length > 0) {
463
- log.debug(
464
- `[task:${taskId}]`,
465
- `middleware chain: ${mws.map((m) => m.type).join(' → ')}`,
466
- );
457
+ log.debug(`[task:${taskId}]`, `middleware chain: ${mws.map((m) => m.type).join(' → ')}`);
467
458
  const mwCtx: MiddlewareContext = {
468
459
  task,
469
460
  track,
@@ -474,52 +465,23 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<void> {
474
465
  const beforeBlocks = doc.contexts.length;
475
466
  const beforeLen = serializePromptDocument(doc).length;
476
467
 
477
- // Prefer the structured API. Fall back to the legacy
478
- // `enhance(string) → string` path so v0.x plugins keep
479
- // working that fallback loses context structure (the
480
- // middleware's output becomes the new task body) but never
481
- // silently drops content.
482
- if (typeof mwPlugin.enhanceDoc === 'function') {
483
- const next = await mwPlugin.enhanceDoc(
484
- doc,
485
- mwConfig as Record<string, unknown>,
486
- mwCtx,
487
- );
488
- if (
489
- !next ||
490
- typeof next !== 'object' ||
491
- !Array.isArray((next as PromptDocument).contexts) ||
492
- typeof (next as PromptDocument).task !== 'string'
493
- ) {
494
- throw new Error(
495
- `middleware "${mwConfig.type}".enhanceDoc() returned a malformed PromptDocument`,
496
- );
497
- }
498
- doc = next as PromptDocument;
499
- } else if (typeof mwPlugin.enhance === 'function') {
500
- const asString = serializePromptDocument(doc);
501
- const next = await mwPlugin.enhance(
502
- asString,
503
- mwConfig as Record<string, unknown>,
504
- mwCtx,
468
+ if (typeof mwPlugin.enhanceDoc !== 'function') {
469
+ throw new Error(
470
+ `middleware "${mwConfig.type}" must provide enhanceDoc`,
505
471
  );
506
- // R3: a middleware that returns undefined / null / a non-string
507
- // would silently corrupt the prompt. Fail loud.
508
- if (typeof next !== 'string') {
509
- throw new Error(
510
- `middleware "${mwConfig.type}".enhance() returned ${next === null ? 'null' : typeof next}, expected string`,
511
- );
512
- }
513
- // Legacy fallback: collapse the returned string into a
514
- // fresh doc. Earlier structure is folded into the string
515
- // (serializePromptDocument just ran), so bytes the driver
516
- // sees match the old string pipeline.
517
- doc = { contexts: [], task: next };
518
- } else {
472
+ }
473
+ const next = await mwPlugin.enhanceDoc(doc, mwConfig as Record<string, unknown>, mwCtx);
474
+ if (
475
+ !next ||
476
+ typeof next !== 'object' ||
477
+ !Array.isArray((next as PromptDocument).contexts) ||
478
+ typeof (next as PromptDocument).task !== 'string'
479
+ ) {
519
480
  throw new Error(
520
- `middleware "${mwConfig.type}" provides neither enhanceDoc nor enhance`,
481
+ `middleware "${mwConfig.type}".enhanceDoc() returned a malformed PromptDocument`,
521
482
  );
522
483
  }
484
+ doc = next as PromptDocument;
523
485
  const afterLen = serializePromptDocument(doc).length;
524
486
  const addedBlocks = doc.contexts.length - beforeBlocks;
525
487
  log.debug(
@@ -558,13 +520,6 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<void> {
558
520
  ...task,
559
521
  prompt,
560
522
  continue_from: node.resolvedContinueFrom,
561
- // Hand the driver the EFFECTIVE port schema rather than the
562
- // raw task.ports. For Prompt tasks this is the one inferred
563
- // from neighbor Commands; Command tasks are unchanged.
564
- // Drivers that introspect ports (e.g. to annotate a system
565
- // prompt with the I/O contract) otherwise saw `undefined`
566
- // for every prompt and had no way to know the contract.
567
- ports: effectivePorts,
568
523
  };
569
524
  const driverCtx: DriverContext = {
570
525
  sessionMap: ctx.sessionMap,
@@ -575,12 +530,8 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<void> {
575
530
  // contexts and task). Drivers that read task.prompt see the
576
531
  // default serialization and need no changes.
577
532
  promptDoc: doc,
578
- // Ports feature: resolved input values keyed by port name,
579
- // already coerced to the declared port type. Drivers that
580
- // need to re-substitute placeholders inside a custom envelope
581
- // can read this and call `substituteInputs`; most drivers can
582
- // ignore it because the engine has already expanded
583
- // `{{inputs.X}}` into `task.prompt` upstream.
533
+ // Resolved input values keyed by input name. Typed bindings have
534
+ // already been coerced when a binding declares `type`.
584
535
  inputs: resolvedInputs,
585
536
  };
586
537
  const spec = await driver.buildCommand(enrichedTask, track, driverCtx);
@@ -588,12 +539,9 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<void> {
588
539
  log.debug(`[task:${taskId}]`, `spawn args: ${JSON.stringify(spec.args)}`);
589
540
  if (spec.cwd) log.debug(`[task:${taskId}]`, `spawn cwd: ${spec.cwd}`);
590
541
  if (spec.env)
591
- log.debug(
592
- `[task:${taskId}]`,
593
- `spawn env overrides: ${Object.keys(spec.env).join(', ')}`,
594
- );
542
+ log.debug(`[task:${taskId}]`, `spawn env overrides: ${Object.keys(spec.env).join(', ')}`);
595
543
  if (spec.stdin) log.debug(`[task:${taskId}]`, `spawn stdin: ${spec.stdin.length} chars`);
596
- result = await runSpawn(spec, driver, runOpts);
544
+ result = await ctx.runtime.runSpawn(spec, driver, runOpts);
597
545
  }
598
546
 
599
547
  // 6. Determine terminal status (without emitting yet — result must be complete first)
@@ -652,13 +600,11 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<void> {
652
600
  );
653
601
  if (outputExtraction.bindingDiagnostic) {
654
602
  log.debug(`[task:${taskId}]`, outputExtraction.bindingDiagnostic);
603
+ const note = `\n[engine] ${outputExtraction.bindingDiagnostic}`;
604
+ result = { ...result, stderr: result.stderr + note };
655
605
  }
656
606
  }
657
607
 
658
- // Prompt tasks use inferred ports (from direct-downstream Command
659
- // inputs); Command tasks use their declared ports. Either way,
660
- // `extractTaskOutputs` is a no-op when there are no declared
661
- // outputs to pull, so pre-ports tasks pay nothing for this call.
662
608
  if (effectivePorts?.outputs && effectivePorts.outputs.length > 0) {
663
609
  log.debug(
664
610
  `[task:${taskId}]`,
@@ -745,16 +691,10 @@ export async function executeTask(options: ExecuteTaskOptions): Promise<void> {
745
691
  log.debug(`[task:${taskId}]`, `wrote stderr: ${result.stderrPath}`);
746
692
  }
747
693
  if (result.stdout) {
748
- log.quiet(
749
- `--- stdout (${taskId}) ---\n${clip(result.stdout)}\n--- end stdout ---`,
750
- taskId,
751
- );
694
+ log.quiet(`--- stdout (${taskId}) ---\n${clip(result.stdout)}\n--- end stdout ---`, taskId);
752
695
  }
753
696
  if (result.stderr) {
754
- log.quiet(
755
- `--- stderr (${taskId}) ---\n${clip(result.stderr)}\n--- end stderr ---`,
756
- taskId,
757
- );
697
+ log.quiet(`--- stderr (${taskId}) ---\n${clip(result.stderr)}\n--- end stderr ---`, taskId);
758
698
  }
759
699
  if (task.completion) {
760
700
  log.debug(