@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.
- package/README.md +84 -44
- package/dist/bootstrap.d.ts +20 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +21 -11
- package/dist/bootstrap.js.map +1 -1
- package/dist/core/dataflow.d.ts.map +1 -1
- package/dist/core/dataflow.js +45 -9
- package/dist/core/dataflow.js.map +1 -1
- package/dist/core/run-context.d.ts +3 -0
- package/dist/core/run-context.d.ts.map +1 -1
- package/dist/core/run-context.js +2 -0
- package/dist/core/run-context.js.map +1 -1
- package/dist/core/task-executor.d.ts.map +1 -1
- package/dist/core/task-executor.js +46 -84
- package/dist/core/task-executor.js.map +1 -1
- package/dist/engine.d.ts +6 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +3 -0
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins.d.ts +2 -2
- package/dist/plugins.d.ts.map +1 -1
- package/dist/ports.d.ts +4 -0
- package/dist/ports.d.ts.map +1 -1
- package/dist/ports.js +27 -4
- package/dist/ports.js.map +1 -1
- package/dist/registry.d.ts +10 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +64 -25
- package/dist/registry.js.map +1 -1
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +8 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -7
- package/dist/schema.js.map +1 -1
- package/dist/tagma.d.ts +11 -1
- package/dist/tagma.d.ts.map +1 -1
- package/dist/tagma.js +6 -0
- package/dist/tagma.js.map +1 -1
- package/dist/validate-raw.d.ts +4 -4
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +89 -230
- package/dist/validate-raw.js.map +1 -1
- package/package.json +2 -2
- package/src/bootstrap.ts +23 -14
- package/src/core/dataflow.test.ts +8 -9
- package/src/core/dataflow.ts +57 -14
- package/src/core/run-context.test.ts +12 -0
- package/src/core/run-context.ts +4 -0
- package/src/core/task-executor.ts +75 -135
- package/src/engine-ports-mixed.test.ts +68 -411
- package/src/engine-ports.test.ts +37 -341
- package/src/engine.ts +8 -0
- package/src/index.ts +5 -0
- package/src/pipeline-runner.test.ts +5 -9
- package/src/plugin-registry.test.ts +138 -1
- package/src/plugins.ts +5 -2
- package/src/ports.test.ts +80 -0
- package/src/ports.ts +36 -4
- package/src/registry.ts +81 -26
- package/src/runtime.ts +20 -0
- package/src/schema-ports.test.ts +47 -197
- package/src/schema.ts +1 -7
- package/src/tagma.test.ts +72 -1
- package/src/tagma.ts +16 -1
- package/src/validate-raw-ports.test.ts +80 -393
- 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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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(
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
346
|
-
return;
|
|
351
|
+
inferredPromptInputs = inputResolution.inputs;
|
|
347
352
|
}
|
|
348
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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}"
|
|
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
|
-
//
|
|
579
|
-
// already coerced
|
|
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(
|