@tagma/sdk 0.7.3 → 0.7.5
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 +85 -57
- package/dist/approval.d.ts +2 -12
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +1 -90
- package/dist/approval.js.map +1 -1
- package/dist/bootstrap.d.ts +1 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/completions/file-exists.js +1 -1
- package/dist/completions/file-exists.js.map +1 -1
- package/dist/completions/output-check.d.ts.map +1 -1
- package/dist/completions/output-check.js +17 -4
- package/dist/completions/output-check.js.map +1 -1
- package/dist/config.d.ts +4 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/dataflow.d.ts +3 -0
- package/dist/dataflow.d.ts.map +1 -0
- package/dist/dataflow.js +2 -0
- package/dist/dataflow.js.map +1 -0
- package/dist/drivers/opencode.d.ts.map +1 -1
- package/dist/drivers/opencode.js +23 -71
- package/dist/drivers/opencode.js.map +1 -1
- package/dist/engine.d.ts +5 -56
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +7 -297
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +4 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +2 -60
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +1 -153
- package/dist/logger.js.map +1 -1
- package/dist/middlewares/static-context.d.ts.map +1 -1
- package/dist/middlewares/static-context.js +1 -2
- package/dist/middlewares/static-context.js.map +1 -1
- package/dist/pipeline-runner.d.ts.map +1 -1
- package/dist/pipeline-runner.js +2 -2
- package/dist/pipeline-runner.js.map +1 -1
- package/dist/plugins.d.ts +2 -2
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +1 -1
- package/dist/plugins.js.map +1 -1
- package/dist/runner.d.ts +1 -35
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +1 -610
- package/dist/runner.js.map +1 -1
- package/dist/runtime/adapters/stdin-approval.d.ts +2 -0
- package/dist/runtime/adapters/stdin-approval.d.ts.map +1 -0
- package/dist/runtime/adapters/stdin-approval.js +2 -0
- package/dist/runtime/adapters/stdin-approval.js.map +1 -0
- package/dist/runtime/adapters/websocket-approval.d.ts +2 -0
- package/dist/runtime/adapters/websocket-approval.d.ts.map +1 -0
- package/dist/runtime/adapters/websocket-approval.js +2 -0
- package/dist/runtime/adapters/websocket-approval.js.map +1 -0
- package/dist/runtime/bun-process-runner.d.ts +2 -0
- package/dist/runtime/bun-process-runner.d.ts.map +1 -0
- package/dist/runtime/bun-process-runner.js +2 -0
- package/dist/runtime/bun-process-runner.js.map +1 -0
- package/dist/runtime.d.ts +2 -8
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1 -7
- package/dist/runtime.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +3 -4
- package/dist/schema.js.map +1 -1
- package/dist/tagma.d.ts +3 -4
- package/dist/tagma.d.ts.map +1 -1
- package/dist/tagma.js +2 -3
- package/dist/tagma.js.map +1 -1
- package/dist/triggers/file.d.ts.map +1 -1
- package/dist/triggers/file.js +74 -108
- package/dist/triggers/file.js.map +1 -1
- package/dist/triggers/manual.d.ts.map +1 -1
- package/dist/triggers/manual.js +1 -2
- package/dist/triggers/manual.js.map +1 -1
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -12
- package/dist/types.js.map +1 -1
- package/dist/utils-api.d.ts +1 -1
- package/dist/utils-api.d.ts.map +1 -1
- package/dist/utils-api.js +1 -1
- package/dist/utils-api.js.map +1 -1
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +5 -12
- package/dist/validate-raw.js.map +1 -1
- package/package.json +20 -22
- package/dist/adapters/stdin-approval.d.ts +0 -6
- package/dist/adapters/stdin-approval.d.ts.map +0 -1
- package/dist/adapters/stdin-approval.js +0 -90
- package/dist/adapters/stdin-approval.js.map +0 -1
- package/dist/adapters/websocket-approval.d.ts +0 -28
- package/dist/adapters/websocket-approval.d.ts.map +0 -1
- package/dist/adapters/websocket-approval.js +0 -147
- package/dist/adapters/websocket-approval.js.map +0 -1
- package/dist/core/dataflow.d.ts +0 -23
- package/dist/core/dataflow.d.ts.map +0 -1
- package/dist/core/dataflow.js +0 -99
- package/dist/core/dataflow.js.map +0 -1
- package/dist/core/log-prune.d.ts +0 -16
- package/dist/core/log-prune.d.ts.map +0 -1
- package/dist/core/log-prune.js +0 -34
- package/dist/core/log-prune.js.map +0 -1
- package/dist/core/preflight.d.ts +0 -13
- package/dist/core/preflight.d.ts.map +0 -1
- package/dist/core/preflight.js +0 -61
- package/dist/core/preflight.js.map +0 -1
- package/dist/core/run-context.d.ts +0 -55
- package/dist/core/run-context.d.ts.map +0 -1
- package/dist/core/run-context.js +0 -158
- package/dist/core/run-context.js.map +0 -1
- package/dist/core/run-state.d.ts +0 -25
- package/dist/core/run-state.d.ts.map +0 -1
- package/dist/core/run-state.js +0 -93
- package/dist/core/run-state.js.map +0 -1
- package/dist/core/scheduler.d.ts +0 -13
- package/dist/core/scheduler.d.ts.map +0 -1
- package/dist/core/scheduler.js +0 -35
- package/dist/core/scheduler.js.map +0 -1
- package/dist/core/task-executor.d.ts +0 -13
- package/dist/core/task-executor.d.ts.map +0 -1
- package/dist/core/task-executor.js +0 -601
- package/dist/core/task-executor.js.map +0 -1
- package/dist/core/trigger-errors.d.ts +0 -9
- package/dist/core/trigger-errors.d.ts.map +0 -1
- package/dist/core/trigger-errors.js +0 -15
- package/dist/core/trigger-errors.js.map +0 -1
- package/dist/dag.d.ts +0 -45
- package/dist/dag.d.ts.map +0 -1
- package/dist/dag.js +0 -177
- package/dist/dag.js.map +0 -1
- package/dist/hooks.d.ts +0 -73
- package/dist/hooks.d.ts.map +0 -1
- package/dist/hooks.js +0 -106
- package/dist/hooks.js.map +0 -1
- package/dist/pipeline-definition.d.ts +0 -3
- package/dist/pipeline-definition.d.ts.map +0 -1
- package/dist/pipeline-definition.js +0 -4
- package/dist/pipeline-definition.js.map +0 -1
- package/dist/ports.d.ts +0 -196
- package/dist/ports.d.ts.map +0 -1
- package/dist/ports.js +0 -688
- package/dist/ports.js.map +0 -1
- package/dist/prompt-doc.d.ts +0 -70
- package/dist/prompt-doc.d.ts.map +0 -1
- package/dist/prompt-doc.js +0 -154
- package/dist/prompt-doc.js.map +0 -1
- package/dist/registry.d.ts +0 -67
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js +0 -293
- package/dist/registry.js.map +0 -1
- package/dist/task-ref.d.ts +0 -55
- package/dist/task-ref.d.ts.map +0 -1
- package/dist/task-ref.js +0 -103
- package/dist/task-ref.js.map +0 -1
- package/dist/utils.d.ts +0 -13
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -177
- package/dist/utils.js.map +0 -1
- package/src/adapters/stdin-approval.ts +0 -106
- package/src/adapters/websocket-approval.ts +0 -224
- package/src/approval.ts +0 -131
- package/src/bootstrap.ts +0 -55
- package/src/completions/exit-code.ts +0 -34
- package/src/completions/file-exists.ts +0 -66
- package/src/completions/output-check.test.ts +0 -50
- package/src/completions/output-check.ts +0 -92
- package/src/config-ops.test.ts +0 -70
- package/src/config-ops.ts +0 -328
- package/src/config.ts +0 -26
- package/src/core/dataflow.test.ts +0 -166
- package/src/core/dataflow.ts +0 -161
- package/src/core/log-prune.test.ts +0 -58
- package/src/core/log-prune.ts +0 -43
- package/src/core/preflight.test.ts +0 -49
- package/src/core/preflight.ts +0 -89
- package/src/core/run-context.test.ts +0 -256
- package/src/core/run-context.ts +0 -211
- package/src/core/run-state.test.ts +0 -98
- package/src/core/run-state.ts +0 -122
- package/src/core/scheduler.test.ts +0 -83
- package/src/core/scheduler.ts +0 -42
- package/src/core/task-executor.ts +0 -743
- package/src/core/trigger-errors.ts +0 -15
- package/src/dag.test.ts +0 -56
- package/src/dag.ts +0 -245
- package/src/drivers/opencode.ts +0 -410
- package/src/engine-ports-mixed.test.ts +0 -156
- package/src/engine-ports.test.ts +0 -166
- package/src/engine-task-type.test.ts +0 -56
- package/src/engine.ts +0 -458
- package/src/hooks.ts +0 -193
- package/src/index.ts +0 -33
- package/src/logger.ts +0 -182
- package/src/middlewares/static-context.ts +0 -49
- package/src/pipeline-definition.ts +0 -5
- package/src/pipeline-runner.test.ts +0 -91
- package/src/pipeline-runner.ts +0 -194
- package/src/plugin-registry.test.ts +0 -382
- package/src/plugins.ts +0 -21
- package/src/ports.test.ts +0 -678
- package/src/ports.ts +0 -925
- package/src/prompt-doc.test.ts +0 -174
- package/src/prompt-doc.ts +0 -169
- package/src/registry.ts +0 -353
- package/src/runner.test.ts +0 -142
- package/src/runner.ts +0 -666
- package/src/runtime.ts +0 -20
- package/src/schema-ports.test.ts +0 -172
- package/src/schema.test.ts +0 -213
- package/src/schema.ts +0 -379
- package/src/tagma.test.ts +0 -155
- package/src/tagma.ts +0 -62
- package/src/task-ref.test.ts +0 -401
- package/src/task-ref.ts +0 -121
- package/src/triggers/file.ts +0 -164
- package/src/triggers/manual.ts +0 -86
- package/src/types.ts +0 -18
- package/src/utils-api.ts +0 -8
- package/src/utils.test.ts +0 -28
- package/src/utils.ts +0 -203
- package/src/validate-raw-plugin-types.test.ts +0 -60
- package/src/validate-raw-ports.test.ts +0 -136
- package/src/validate-raw.ts +0 -852
- package/src/yaml-compiler.test.ts +0 -108
- package/src/yaml-compiler.ts +0 -110
- package/src/yaml.ts +0 -11
package/dist/ports.js
DELETED
|
@@ -1,688 +0,0 @@
|
|
|
1
|
-
// ═══ Task ports: substitute / resolve / extract / infer ═══
|
|
2
|
-
//
|
|
3
|
-
// One module, four concerns, all keyed on `task.ports`:
|
|
4
|
-
//
|
|
5
|
-
// 1. `substituteInputs(text, inputs)` — expand `{{inputs.<name>}}` in
|
|
6
|
-
// user-authored strings (command lines, prompts). Strict syntax, no
|
|
7
|
-
// arbitrary expressions — the placeholder is a thin pasteboard, not
|
|
8
|
-
// a templating engine. Unknown / undefined references render as empty
|
|
9
|
-
// string with a diagnostic that the caller can surface.
|
|
10
|
-
//
|
|
11
|
-
// 2. `resolveTaskInputs(task, upstreamOutputs, dependsOn)` — gather the
|
|
12
|
-
// values a task will consume from its direct upstreams. Matches by
|
|
13
|
-
// port name (or by explicit `from:`), applies defaults, coerces to
|
|
14
|
-
// the declared type, and classifies the result as ready / missing
|
|
15
|
-
// required / ambiguous. The engine calls this before a task starts
|
|
16
|
-
// and uses the classification to decide whether to block.
|
|
17
|
-
//
|
|
18
|
-
// 3. `extractTaskOutputs(ports, stdout, normalizedOutput)` — after a
|
|
19
|
-
// task succeeds, pull the declared output values from the task's
|
|
20
|
-
// output stream. Default strategy: find the last non-empty line that
|
|
21
|
-
// parses as a JSON object, and read each declared output name from
|
|
22
|
-
// it. Prefer `normalizedOutput` for AI tasks, fall back to raw
|
|
23
|
-
// stdout — command tasks only ever have stdout.
|
|
24
|
-
//
|
|
25
|
-
// 4. `inferPromptPorts({upstreams, downstreams})` — Prompt Tasks do NOT
|
|
26
|
-
// declare ports; their I/O contract is inferred from direct-neighbor
|
|
27
|
-
// Command Tasks. This helper synthesizes a `TaskPorts` object the
|
|
28
|
-
// engine can feed into the three concerns above, and surfaces any
|
|
29
|
-
// collisions that block the task (same port name on two upstreams,
|
|
30
|
-
// incompatible types across downstreams, …). Prompt neighbors
|
|
31
|
-
// contribute zero structured I/O — they pass free text via
|
|
32
|
-
// `continue_from` / normalizedOutput instead.
|
|
33
|
-
//
|
|
34
|
-
// Everything here is pure / deterministic so it can be reused by the CLI,
|
|
35
|
-
// the editor (for preview/simulation), and the engine without side effects.
|
|
36
|
-
// ─── Template substitution ────────────────────────────────────────────
|
|
37
|
-
/**
|
|
38
|
-
* Matches `{{inputs.<identifier>}}` with optional whitespace inside the
|
|
39
|
-
* braces. The identifier is restricted to the same character set we use
|
|
40
|
-
* for task IDs (letter/underscore, then letters/digits/underscores) so
|
|
41
|
-
* accidental use of `{{inputs.foo.bar}}` fails loudly rather than
|
|
42
|
-
* silently producing garbage.
|
|
43
|
-
*/
|
|
44
|
-
const PLACEHOLDER_RE = /\{\{\s*inputs\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
|
45
|
-
/**
|
|
46
|
-
* Scan `text` for every `{{inputs.<name>}}` placeholder and return the
|
|
47
|
-
* set of referenced input names. Useful at validation time: the editor
|
|
48
|
-
* can cross-check that each placeholder has a corresponding declared
|
|
49
|
-
* port and flag typos before a run ever starts.
|
|
50
|
-
*/
|
|
51
|
-
export function extractInputReferences(text) {
|
|
52
|
-
const names = new Set();
|
|
53
|
-
for (const match of text.matchAll(PLACEHOLDER_RE)) {
|
|
54
|
-
names.add(match[1]);
|
|
55
|
-
}
|
|
56
|
-
return [...names];
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Replace `{{inputs.<name>}}` placeholders in `text` with values from
|
|
60
|
-
* `inputs`. Coercion:
|
|
61
|
-
* - string → as-is
|
|
62
|
-
* - number / boolean → `String(value)`
|
|
63
|
-
* - null / undefined → empty string (name is also reported as unresolved)
|
|
64
|
-
* - anything else (object, array, json port) → `JSON.stringify(value)`
|
|
65
|
-
*
|
|
66
|
-
* Values are substituted *verbatim* — quoting is the user's
|
|
67
|
-
* responsibility in the authored text. For command lines that interpolate
|
|
68
|
-
* user-provided strings, authors should wrap the placeholder in quotes:
|
|
69
|
-
*
|
|
70
|
-
* weather.sh --city "{{inputs.city}}"
|
|
71
|
-
*
|
|
72
|
-
* That's a documented contract rather than a silent shell-escape, because
|
|
73
|
-
* silent escaping would hide the difference between `--city Shanghai` and
|
|
74
|
-
* `--flag $(echo pwned)` — both valid command fragments, one a bug, one a
|
|
75
|
-
* feature. Users know which they want; the engine doesn't.
|
|
76
|
-
*/
|
|
77
|
-
export function substituteInputs(text, inputs) {
|
|
78
|
-
const unresolved = new Set();
|
|
79
|
-
const out = text.replace(PLACEHOLDER_RE, (_full, name) => {
|
|
80
|
-
if (!(name in inputs)) {
|
|
81
|
-
unresolved.add(name);
|
|
82
|
-
return '';
|
|
83
|
-
}
|
|
84
|
-
const value = inputs[name];
|
|
85
|
-
if (value === null || value === undefined) {
|
|
86
|
-
unresolved.add(name);
|
|
87
|
-
return '';
|
|
88
|
-
}
|
|
89
|
-
if (typeof value === 'string')
|
|
90
|
-
return value;
|
|
91
|
-
if (typeof value === 'number' || typeof value === 'boolean')
|
|
92
|
-
return String(value);
|
|
93
|
-
try {
|
|
94
|
-
return JSON.stringify(value);
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
// Circular / unserializable — render a placeholder rather than
|
|
98
|
-
// throwing, and mark it unresolved so the caller can warn.
|
|
99
|
-
unresolved.add(name);
|
|
100
|
-
return '';
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
return { text: out, unresolved: [...unresolved] };
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Resolve the input values for `task` from the outputs its direct
|
|
107
|
-
* upstreams produced.
|
|
108
|
-
*
|
|
109
|
-
* `upstreamOutputs` is keyed by fully-qualified task id and maps to the
|
|
110
|
-
* outputs that task published (its `TaskResult.outputs`). `dependsOn` is
|
|
111
|
-
* the already-qualified dependency list (from `DagNode.dependsOn`). When
|
|
112
|
-
* an upstream has no outputs entry (e.g. it didn't declare any or it
|
|
113
|
-
* failed), its entry may be missing — we just skip it during matching.
|
|
114
|
-
*
|
|
115
|
-
* Matching rules:
|
|
116
|
-
* - If the input port has `from: "taskId.portName"` → look up that
|
|
117
|
-
* specific upstream / port. Missing = unsatisfied.
|
|
118
|
-
* - If it has `from: "portName"` (bare) → treat as explicit port name
|
|
119
|
-
* but allow any upstream to provide it (useful when the user wants
|
|
120
|
-
* to match by name but still be explicit about the intent).
|
|
121
|
-
* - If no `from` → scan every upstream's outputs for a key matching
|
|
122
|
-
* the input name. Zero hits = unsatisfied; 2+ hits across different
|
|
123
|
-
* upstreams = ambiguous.
|
|
124
|
-
*
|
|
125
|
-
* The function never throws on config errors — every failure mode maps
|
|
126
|
-
* to a field of the `blocked` result so the engine can log a unified
|
|
127
|
-
* message and mark the task blocked.
|
|
128
|
-
*/
|
|
129
|
-
export function resolveTaskInputs(task, upstreamOutputs, dependsOn) {
|
|
130
|
-
const inputsDecl = task.ports?.inputs;
|
|
131
|
-
if (!inputsDecl || inputsDecl.length === 0) {
|
|
132
|
-
return { kind: 'ready', inputs: {}, missingOptional: [] };
|
|
133
|
-
}
|
|
134
|
-
const inputs = {};
|
|
135
|
-
const missingRequired = [];
|
|
136
|
-
const missingOptional = [];
|
|
137
|
-
const ambiguous = [];
|
|
138
|
-
const typeErrors = [];
|
|
139
|
-
for (const port of inputsDecl) {
|
|
140
|
-
const found = findUpstreamValue(port, upstreamOutputs, dependsOn);
|
|
141
|
-
if (found.kind === 'ambiguous') {
|
|
142
|
-
ambiguous.push({ port: port.name, producers: found.producers });
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
let value;
|
|
146
|
-
let present = false;
|
|
147
|
-
if (found.kind === 'hit') {
|
|
148
|
-
value = found.value;
|
|
149
|
-
present = true;
|
|
150
|
-
}
|
|
151
|
-
else if (port.default !== undefined) {
|
|
152
|
-
value = port.default;
|
|
153
|
-
present = true;
|
|
154
|
-
}
|
|
155
|
-
if (!present) {
|
|
156
|
-
if (port.required === true) {
|
|
157
|
-
missingRequired.push(port.name);
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
missingOptional.push(port.name);
|
|
161
|
-
}
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
const coerced = coerceValue(port, value);
|
|
165
|
-
if (coerced.kind === 'error') {
|
|
166
|
-
typeErrors.push({ port: port.name, reason: coerced.reason });
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
inputs[port.name] = coerced.value;
|
|
170
|
-
}
|
|
171
|
-
if (missingRequired.length > 0 || ambiguous.length > 0 || typeErrors.length > 0) {
|
|
172
|
-
const lines = [];
|
|
173
|
-
if (missingRequired.length > 0) {
|
|
174
|
-
lines.push(`missing required input(s): ${missingRequired.join(', ')}`);
|
|
175
|
-
}
|
|
176
|
-
if (ambiguous.length > 0) {
|
|
177
|
-
for (const amb of ambiguous) {
|
|
178
|
-
lines.push(`input "${amb.port}" is produced by multiple upstreams ` +
|
|
179
|
-
`(${amb.producers.join(', ')}) — disambiguate with "from: taskId.${amb.port}"`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (typeErrors.length > 0) {
|
|
183
|
-
for (const te of typeErrors) {
|
|
184
|
-
lines.push(`input "${te.port}": ${te.reason}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return {
|
|
188
|
-
kind: 'blocked',
|
|
189
|
-
missingRequired,
|
|
190
|
-
ambiguous,
|
|
191
|
-
typeErrors,
|
|
192
|
-
reason: lines.join('\n'),
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
return { kind: 'ready', inputs, missingOptional };
|
|
196
|
-
}
|
|
197
|
-
export function resolveTaskBindingInputs(task, upstreamData, dependsOn) {
|
|
198
|
-
const bindings = task.inputs;
|
|
199
|
-
if (!bindings || Object.keys(bindings).length === 0) {
|
|
200
|
-
return { kind: 'ready', inputs: {}, missingOptional: [] };
|
|
201
|
-
}
|
|
202
|
-
const inputs = {};
|
|
203
|
-
const missingRequired = [];
|
|
204
|
-
const missingOptional = [];
|
|
205
|
-
const ambiguous = [];
|
|
206
|
-
const typeErrors = [];
|
|
207
|
-
for (const [name, binding] of Object.entries(bindings)) {
|
|
208
|
-
let value;
|
|
209
|
-
let present = false;
|
|
210
|
-
if ('value' in binding) {
|
|
211
|
-
value = binding.value;
|
|
212
|
-
present = true;
|
|
213
|
-
}
|
|
214
|
-
else if (binding.from) {
|
|
215
|
-
const found = resolveBindingSource(binding.from, upstreamData, dependsOn);
|
|
216
|
-
if (found.kind === 'ambiguous') {
|
|
217
|
-
ambiguous.push({ input: name, producers: found.producers });
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
if (found.kind === 'hit') {
|
|
221
|
-
value = found.value;
|
|
222
|
-
present = true;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (!present && 'default' in binding) {
|
|
226
|
-
value = binding.default;
|
|
227
|
-
present = true;
|
|
228
|
-
}
|
|
229
|
-
if (!present || value === undefined || value === null) {
|
|
230
|
-
if (binding.required === true) {
|
|
231
|
-
missingRequired.push(name);
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
missingOptional.push(name);
|
|
235
|
-
}
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
const coerced = coerceBindingValue(binding, value);
|
|
239
|
-
if (coerced.kind === 'error') {
|
|
240
|
-
typeErrors.push({ input: name, reason: coerced.reason });
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
inputs[name] = coerced.value;
|
|
244
|
-
}
|
|
245
|
-
if (missingRequired.length > 0 || ambiguous.length > 0 || typeErrors.length > 0) {
|
|
246
|
-
const lines = [];
|
|
247
|
-
if (missingRequired.length > 0) {
|
|
248
|
-
lines.push(`missing required binding input(s): ${missingRequired.join(', ')}`);
|
|
249
|
-
}
|
|
250
|
-
for (const amb of ambiguous) {
|
|
251
|
-
lines.push(`binding input "${amb.input}" is produced by multiple upstreams ` +
|
|
252
|
-
`(${amb.producers.join(', ')}) — use "taskId.outputs.${amb.input}"`);
|
|
253
|
-
}
|
|
254
|
-
for (const te of typeErrors) {
|
|
255
|
-
lines.push(`binding input "${te.input}": ${te.reason}`);
|
|
256
|
-
}
|
|
257
|
-
return { kind: 'blocked', missingRequired, ambiguous, typeErrors, reason: lines.join('\n') };
|
|
258
|
-
}
|
|
259
|
-
return { kind: 'ready', inputs, missingOptional };
|
|
260
|
-
}
|
|
261
|
-
function resolveBindingSource(source, upstreamData, dependsOn) {
|
|
262
|
-
if (source.startsWith('outputs.')) {
|
|
263
|
-
return findOutputByName(source.slice('outputs.'.length), upstreamData, dependsOn);
|
|
264
|
-
}
|
|
265
|
-
const outputMarker = '.outputs.';
|
|
266
|
-
const outputIdx = source.lastIndexOf(outputMarker);
|
|
267
|
-
if (outputIdx > 0) {
|
|
268
|
-
const upstreamId = source.slice(0, outputIdx);
|
|
269
|
-
const outputName = source.slice(outputIdx + outputMarker.length);
|
|
270
|
-
if (!dependsOn.includes(upstreamId))
|
|
271
|
-
return { kind: 'miss' };
|
|
272
|
-
const upstream = upstreamData.get(upstreamId);
|
|
273
|
-
if (upstream?.outputs && outputName in upstream.outputs) {
|
|
274
|
-
return { kind: 'hit', producer: upstreamId, value: upstream.outputs[outputName] };
|
|
275
|
-
}
|
|
276
|
-
return { kind: 'miss' };
|
|
277
|
-
}
|
|
278
|
-
for (const field of ['stdout', 'stderr', 'normalizedOutput', 'exitCode']) {
|
|
279
|
-
const suffix = `.${field}`;
|
|
280
|
-
if (!source.endsWith(suffix))
|
|
281
|
-
continue;
|
|
282
|
-
const upstreamId = source.slice(0, -suffix.length);
|
|
283
|
-
if (!dependsOn.includes(upstreamId))
|
|
284
|
-
return { kind: 'miss' };
|
|
285
|
-
const upstream = upstreamData.get(upstreamId);
|
|
286
|
-
if (!upstream)
|
|
287
|
-
return { kind: 'miss' };
|
|
288
|
-
const value = upstream[field];
|
|
289
|
-
return value === undefined || value === null
|
|
290
|
-
? { kind: 'miss' }
|
|
291
|
-
: { kind: 'hit', producer: upstreamId, value };
|
|
292
|
-
}
|
|
293
|
-
return { kind: 'miss' };
|
|
294
|
-
}
|
|
295
|
-
function findOutputByName(name, upstreamData, dependsOn) {
|
|
296
|
-
const hits = [];
|
|
297
|
-
for (const upstreamId of dependsOn) {
|
|
298
|
-
const upstream = upstreamData.get(upstreamId);
|
|
299
|
-
if (upstream?.outputs && name in upstream.outputs) {
|
|
300
|
-
hits.push({ producer: upstreamId, value: upstream.outputs[name] });
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
if (hits.length === 0)
|
|
304
|
-
return { kind: 'miss' };
|
|
305
|
-
if (hits.length === 1)
|
|
306
|
-
return { kind: 'hit', producer: hits[0].producer, value: hits[0].value };
|
|
307
|
-
return { kind: 'ambiguous', producers: hits.map((h) => h.producer) };
|
|
308
|
-
}
|
|
309
|
-
function findUpstreamValue(port, upstreamOutputs, dependsOn) {
|
|
310
|
-
// Explicit fully-qualified binding: "taskId.portName"
|
|
311
|
-
if (port.from && port.from.includes('.')) {
|
|
312
|
-
const dot = port.from.lastIndexOf('.');
|
|
313
|
-
const upstreamId = port.from.slice(0, dot);
|
|
314
|
-
const portName = port.from.slice(dot + 1);
|
|
315
|
-
const upstream = upstreamOutputs.get(upstreamId);
|
|
316
|
-
if (upstream && portName in upstream) {
|
|
317
|
-
return { kind: 'hit', producer: upstreamId, value: upstream[portName] };
|
|
318
|
-
}
|
|
319
|
-
return { kind: 'miss' };
|
|
320
|
-
}
|
|
321
|
-
// Name match (either explicit `from: "portName"` or defaulted to port.name)
|
|
322
|
-
const key = port.from ?? port.name;
|
|
323
|
-
const hits = [];
|
|
324
|
-
for (const upstreamId of dependsOn) {
|
|
325
|
-
const upstream = upstreamOutputs.get(upstreamId);
|
|
326
|
-
if (upstream && key in upstream) {
|
|
327
|
-
hits.push({ producer: upstreamId, value: upstream[key] });
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
if (hits.length === 0)
|
|
331
|
-
return { kind: 'miss' };
|
|
332
|
-
if (hits.length === 1)
|
|
333
|
-
return { kind: 'hit', producer: hits[0].producer, value: hits[0].value };
|
|
334
|
-
return { kind: 'ambiguous', producers: hits.map((h) => h.producer) };
|
|
335
|
-
}
|
|
336
|
-
function coerceValue(port, raw) {
|
|
337
|
-
switch (port.type) {
|
|
338
|
-
case 'string': {
|
|
339
|
-
if (typeof raw === 'string')
|
|
340
|
-
return { kind: 'ok', value: raw };
|
|
341
|
-
if (typeof raw === 'number' || typeof raw === 'boolean') {
|
|
342
|
-
return { kind: 'ok', value: String(raw) };
|
|
343
|
-
}
|
|
344
|
-
return { kind: 'error', reason: `expected string, got ${describe(raw)}` };
|
|
345
|
-
}
|
|
346
|
-
case 'number': {
|
|
347
|
-
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
348
|
-
return { kind: 'ok', value: raw };
|
|
349
|
-
if (typeof raw === 'string' && raw.trim() !== '') {
|
|
350
|
-
const n = Number(raw);
|
|
351
|
-
if (Number.isFinite(n))
|
|
352
|
-
return { kind: 'ok', value: n };
|
|
353
|
-
}
|
|
354
|
-
return { kind: 'error', reason: `expected number, got ${describe(raw)}` };
|
|
355
|
-
}
|
|
356
|
-
case 'boolean': {
|
|
357
|
-
if (typeof raw === 'boolean')
|
|
358
|
-
return { kind: 'ok', value: raw };
|
|
359
|
-
if (raw === 'true' || raw === 'false')
|
|
360
|
-
return { kind: 'ok', value: raw === 'true' };
|
|
361
|
-
return { kind: 'error', reason: `expected boolean, got ${describe(raw)}` };
|
|
362
|
-
}
|
|
363
|
-
case 'enum': {
|
|
364
|
-
const allowed = port.enum ?? [];
|
|
365
|
-
if (allowed.length === 0) {
|
|
366
|
-
return { kind: 'error', reason: 'enum port declared without "enum" values' };
|
|
367
|
-
}
|
|
368
|
-
const asStr = typeof raw === 'string' ? raw : String(raw);
|
|
369
|
-
if (!allowed.includes(asStr)) {
|
|
370
|
-
return {
|
|
371
|
-
kind: 'error',
|
|
372
|
-
reason: `value ${JSON.stringify(raw)} not in enum [${allowed.map((v) => JSON.stringify(v)).join(', ')}]`,
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
return { kind: 'ok', value: asStr };
|
|
376
|
-
}
|
|
377
|
-
case 'json':
|
|
378
|
-
// 'json' accepts anything that survives JSON round-trip. We don't
|
|
379
|
-
// validate deeply — users opt into `json` precisely because they
|
|
380
|
-
// want a free-form payload.
|
|
381
|
-
return { kind: 'ok', value: raw };
|
|
382
|
-
default: {
|
|
383
|
-
// Exhaustiveness — TypeScript won't let us reach here unless a
|
|
384
|
-
// new PortType is added without updating this switch. The return
|
|
385
|
-
// satisfies the type checker; in practice the default branch is
|
|
386
|
-
// dead code.
|
|
387
|
-
const _exhaustive = port.type;
|
|
388
|
-
void _exhaustive;
|
|
389
|
-
return { kind: 'error', reason: `unknown port type "${String(port.type)}"` };
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
function coerceBindingValue(binding, raw) {
|
|
394
|
-
if (!binding.type)
|
|
395
|
-
return { kind: 'ok', value: raw };
|
|
396
|
-
return coerceValue({
|
|
397
|
-
name: 'binding',
|
|
398
|
-
type: binding.type,
|
|
399
|
-
...(binding.enum ? { enum: binding.enum } : {}),
|
|
400
|
-
}, raw);
|
|
401
|
-
}
|
|
402
|
-
function describe(v) {
|
|
403
|
-
if (v === null)
|
|
404
|
-
return 'null';
|
|
405
|
-
if (Array.isArray(v))
|
|
406
|
-
return 'array';
|
|
407
|
-
return typeof v;
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Extract declared outputs from a terminated task's output streams.
|
|
411
|
-
*
|
|
412
|
-
* Strategy (v1 — intentionally dumb but predictable):
|
|
413
|
-
* 1. Prefer `normalizedOutput` when provided (AI drivers populate this
|
|
414
|
-
* with the canonical assistant message; it's much cleaner than raw
|
|
415
|
-
* stdout, which often has JSONL event dumps). Fall back to stdout
|
|
416
|
-
* otherwise.
|
|
417
|
-
* 2. Scan from the end for the first non-empty line. If it parses as a
|
|
418
|
-
* JSON object, use that as the source record.
|
|
419
|
-
* 3. If (2) fails, try parsing the entire source as JSON (supports
|
|
420
|
-
* commands that pretty-print with line breaks).
|
|
421
|
-
* 4. For each declared output port, read the matching key and coerce
|
|
422
|
-
* to the declared type. Coercion failures produce a diagnostic and
|
|
423
|
-
* the port is absent from `outputs` (treated as missing downstream).
|
|
424
|
-
*
|
|
425
|
-
* When no declared outputs are present this returns an empty `outputs`
|
|
426
|
-
* map and null diagnostic — the engine interprets that as "task has no
|
|
427
|
-
* port contract".
|
|
428
|
-
*/
|
|
429
|
-
export function extractTaskOutputs(ports, stdout, normalizedOutput) {
|
|
430
|
-
const decl = ports?.outputs;
|
|
431
|
-
if (!decl || decl.length === 0) {
|
|
432
|
-
return { outputs: {}, diagnostic: null };
|
|
433
|
-
}
|
|
434
|
-
const source = (normalizedOutput ?? '').length > 0 ? normalizedOutput : stdout;
|
|
435
|
-
const record = parseJsonTail(source);
|
|
436
|
-
if (record === null) {
|
|
437
|
-
return {
|
|
438
|
-
outputs: {},
|
|
439
|
-
diagnostic: 'outputs: could not find a final-line JSON object in task output — declared outputs are unresolved',
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
const outputs = {};
|
|
443
|
-
const warnings = [];
|
|
444
|
-
for (const port of decl) {
|
|
445
|
-
if (!(port.name in record)) {
|
|
446
|
-
warnings.push(`missing key "${port.name}"`);
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
const coerced = coerceValue(port, record[port.name]);
|
|
450
|
-
if (coerced.kind === 'error') {
|
|
451
|
-
warnings.push(`"${port.name}": ${coerced.reason}`);
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
outputs[port.name] = coerced.value;
|
|
455
|
-
}
|
|
456
|
-
const diagnostic = warnings.length > 0 ? `outputs: ${warnings.join('; ')}` : null;
|
|
457
|
-
return { outputs, diagnostic };
|
|
458
|
-
}
|
|
459
|
-
export function extractTaskBindingOutputs(bindings, stdout, stderr, normalizedOutput) {
|
|
460
|
-
if (!bindings || Object.keys(bindings).length === 0) {
|
|
461
|
-
return { outputs: {}, diagnostic: null };
|
|
462
|
-
}
|
|
463
|
-
const outputs = {};
|
|
464
|
-
const missing = [];
|
|
465
|
-
let record;
|
|
466
|
-
for (const [name, binding] of Object.entries(bindings)) {
|
|
467
|
-
let value;
|
|
468
|
-
let present = false;
|
|
469
|
-
if ('value' in binding) {
|
|
470
|
-
value = binding.value;
|
|
471
|
-
present = true;
|
|
472
|
-
}
|
|
473
|
-
else {
|
|
474
|
-
const source = binding.from ?? `json.${name}`;
|
|
475
|
-
if (source === 'stdout') {
|
|
476
|
-
value = stdout;
|
|
477
|
-
present = true;
|
|
478
|
-
}
|
|
479
|
-
else if (source === 'stderr') {
|
|
480
|
-
value = stderr;
|
|
481
|
-
present = true;
|
|
482
|
-
}
|
|
483
|
-
else if (source === 'normalizedOutput') {
|
|
484
|
-
if (normalizedOutput !== null) {
|
|
485
|
-
value = normalizedOutput;
|
|
486
|
-
present = true;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
else if (source.startsWith('json.')) {
|
|
490
|
-
if (record === undefined) {
|
|
491
|
-
const jsonSource = (normalizedOutput ?? '').length > 0 ? normalizedOutput : stdout;
|
|
492
|
-
record = parseJsonTail(jsonSource);
|
|
493
|
-
}
|
|
494
|
-
const key = source.slice('json.'.length);
|
|
495
|
-
if (record && key in record) {
|
|
496
|
-
value = record[key];
|
|
497
|
-
present = true;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
if (!present && 'default' in binding) {
|
|
502
|
-
value = binding.default;
|
|
503
|
-
present = true;
|
|
504
|
-
}
|
|
505
|
-
if (!present || value === undefined || value === null) {
|
|
506
|
-
missing.push(name);
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
const coerced = coerceBindingValue(binding, value);
|
|
510
|
-
if (coerced.kind === 'error') {
|
|
511
|
-
missing.push(`"${name}": ${coerced.reason}`);
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
outputs[name] = coerced.value;
|
|
515
|
-
}
|
|
516
|
-
return {
|
|
517
|
-
outputs,
|
|
518
|
-
diagnostic: missing.length > 0 ? `outputs: unresolved binding output(s): ${missing.join(', ')}` : null,
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
/**
|
|
522
|
-
* Find the last non-empty line that parses as a JSON object. Returns
|
|
523
|
-
* null when no such line exists. Also tries the whole source as a
|
|
524
|
-
* fallback — covers pretty-printed JSON that spans multiple lines.
|
|
525
|
-
*/
|
|
526
|
-
function parseJsonTail(source) {
|
|
527
|
-
const lines = source.split(/\r?\n/);
|
|
528
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
529
|
-
const line = lines[i].trim();
|
|
530
|
-
if (!line)
|
|
531
|
-
continue;
|
|
532
|
-
const parsed = safeParseJson(line);
|
|
533
|
-
if (parsed !== null)
|
|
534
|
-
return parsed;
|
|
535
|
-
// First non-empty line from the tail — if it didn't parse, fall through
|
|
536
|
-
// to the whole-source attempt below rather than scanning further up
|
|
537
|
-
// (otherwise a prior human-readable line would be silently picked up
|
|
538
|
-
// if it happened to contain `{...}` fragments).
|
|
539
|
-
break;
|
|
540
|
-
}
|
|
541
|
-
return safeParseJson(source.trim());
|
|
542
|
-
}
|
|
543
|
-
function safeParseJson(candidate) {
|
|
544
|
-
if (!candidate.startsWith('{'))
|
|
545
|
-
return null;
|
|
546
|
-
try {
|
|
547
|
-
const parsed = JSON.parse(candidate);
|
|
548
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
549
|
-
return parsed;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
catch {
|
|
553
|
-
/* not JSON */
|
|
554
|
-
}
|
|
555
|
-
return null;
|
|
556
|
-
}
|
|
557
|
-
/**
|
|
558
|
-
* Derive the effective `TaskPorts` for a Prompt Task from its direct
|
|
559
|
-
* neighbors. See the module-level "Prompt-task port inference" comment
|
|
560
|
-
* for the full contract.
|
|
561
|
-
*
|
|
562
|
-
* Pure function — no side effects, safe to call from the CLI, editor
|
|
563
|
-
* preview, and engine hot path alike.
|
|
564
|
-
*/
|
|
565
|
-
export function inferPromptPorts(input) {
|
|
566
|
-
const { upstreams, downstreams } = input;
|
|
567
|
-
// ─── Inputs: union of upstream-Command outputs ─────────────────────
|
|
568
|
-
//
|
|
569
|
-
// Walk every upstream in DAG order. First occurrence of a name wins
|
|
570
|
-
// (for the synthesized port shape used to resolve values). Subsequent
|
|
571
|
-
// occurrences under the same name become an `inputConflicts` entry —
|
|
572
|
-
// the engine blocks the task because a Prompt can't disambiguate.
|
|
573
|
-
const inputsByName = new Map();
|
|
574
|
-
const inputCollisionSources = new Map();
|
|
575
|
-
for (const upstream of upstreams) {
|
|
576
|
-
if (!upstream.outputs || upstream.outputs.length === 0)
|
|
577
|
-
continue;
|
|
578
|
-
for (const out of upstream.outputs) {
|
|
579
|
-
const prior = inputsByName.get(out.name);
|
|
580
|
-
if (!prior) {
|
|
581
|
-
// Copy the shape verbatim but drop output-only fields and force
|
|
582
|
-
// `required: true`. Prompt-task inferred inputs are required by
|
|
583
|
-
// default: the LLM wouldn't be getting a real-world value
|
|
584
|
-
// otherwise, and substituting an empty string silently is the
|
|
585
|
-
// same kind of bug we already reject elsewhere.
|
|
586
|
-
inputsByName.set(out.name, {
|
|
587
|
-
port: {
|
|
588
|
-
name: out.name,
|
|
589
|
-
type: out.type,
|
|
590
|
-
...(out.description ? { description: out.description } : {}),
|
|
591
|
-
...(out.enum ? { enum: [...out.enum] } : {}),
|
|
592
|
-
required: true,
|
|
593
|
-
},
|
|
594
|
-
firstProducer: upstream.taskId,
|
|
595
|
-
});
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
// Collision — seed the source list with the first producer too so
|
|
599
|
-
// the emitted conflict lists *all* contributing producers.
|
|
600
|
-
const list = inputCollisionSources.get(out.name) ?? [
|
|
601
|
-
{ taskId: prior.firstProducer, type: prior.port.type },
|
|
602
|
-
];
|
|
603
|
-
list.push({ taskId: upstream.taskId, type: out.type });
|
|
604
|
-
inputCollisionSources.set(out.name, list);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
const inputConflicts = [];
|
|
608
|
-
for (const [portName, producers] of inputCollisionSources) {
|
|
609
|
-
const producerList = producers.map((p) => p.taskId).join(', ');
|
|
610
|
-
inputConflicts.push({
|
|
611
|
-
portName,
|
|
612
|
-
producers,
|
|
613
|
-
reason: `input "${portName}" is produced by multiple upstream Commands (${producerList}) — ` +
|
|
614
|
-
`Prompt tasks cannot disambiguate (no explicit "from:" binding). ` +
|
|
615
|
-
`Rename the output on one of the upstream Commands.`,
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
// ─── Outputs: union of downstream-Command inputs ───────────────────
|
|
619
|
-
//
|
|
620
|
-
// Compatible repeats merge (preserve first-encountered shape; prefer
|
|
621
|
-
// required when any downstream requires it). Incompatible repeats
|
|
622
|
-
// (different type, different enum set) go to `outputConflicts`.
|
|
623
|
-
const outputsByName = new Map();
|
|
624
|
-
const outputCollisionSources = new Map();
|
|
625
|
-
for (const downstream of downstreams) {
|
|
626
|
-
if (!downstream.inputs || downstream.inputs.length === 0)
|
|
627
|
-
continue;
|
|
628
|
-
for (const inp of downstream.inputs) {
|
|
629
|
-
const prior = outputsByName.get(inp.name);
|
|
630
|
-
if (!prior) {
|
|
631
|
-
// Outputs drop input-only fields (required, default, from).
|
|
632
|
-
outputsByName.set(inp.name, {
|
|
633
|
-
port: {
|
|
634
|
-
name: inp.name,
|
|
635
|
-
type: inp.type,
|
|
636
|
-
...(inp.description ? { description: inp.description } : {}),
|
|
637
|
-
...(inp.enum ? { enum: [...inp.enum] } : {}),
|
|
638
|
-
},
|
|
639
|
-
firstConsumer: downstream.taskId,
|
|
640
|
-
});
|
|
641
|
-
continue;
|
|
642
|
-
}
|
|
643
|
-
if (portsAreCompatible(prior.port, inp))
|
|
644
|
-
continue; // merge silently
|
|
645
|
-
const list = outputCollisionSources.get(inp.name) ?? [
|
|
646
|
-
{ taskId: prior.firstConsumer, type: prior.port.type },
|
|
647
|
-
];
|
|
648
|
-
list.push({ taskId: downstream.taskId, type: inp.type });
|
|
649
|
-
outputCollisionSources.set(inp.name, list);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
const outputConflicts = [];
|
|
653
|
-
for (const [portName, producers] of outputCollisionSources) {
|
|
654
|
-
const consumerList = producers.map((p) => `${p.taskId} (${p.type})`).join(', ');
|
|
655
|
-
outputConflicts.push({
|
|
656
|
-
portName,
|
|
657
|
-
producers,
|
|
658
|
-
reason: `output "${portName}" has conflicting type requirements across downstream Commands ` +
|
|
659
|
-
`(${consumerList}) — a single LLM emission cannot satisfy both. ` +
|
|
660
|
-
`Rename the input on one of the downstream Commands.`,
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
const inferredInputs = [...inputsByName.values()].map((e) => e.port);
|
|
664
|
-
const inferredOutputs = [...outputsByName.values()].map((e) => e.port);
|
|
665
|
-
const ports = {
|
|
666
|
-
...(inferredInputs.length > 0 ? { inputs: inferredInputs } : {}),
|
|
667
|
-
...(inferredOutputs.length > 0 ? { outputs: inferredOutputs } : {}),
|
|
668
|
-
};
|
|
669
|
-
return { ports, inputConflicts, outputConflicts };
|
|
670
|
-
}
|
|
671
|
-
/**
|
|
672
|
-
* Two ports with the same name are compatible if they agree on `type`
|
|
673
|
-
* and, for enum ports, on the enum value set. Descriptions and
|
|
674
|
-
* required/default flags are deliberately ignored — they don't affect
|
|
675
|
-
* whether a single value can satisfy both consumers.
|
|
676
|
-
*/
|
|
677
|
-
function portsAreCompatible(a, b) {
|
|
678
|
-
if (a.type !== b.type)
|
|
679
|
-
return false;
|
|
680
|
-
if (a.type === 'enum') {
|
|
681
|
-
const aEnum = [...(a.enum ?? [])].sort().join('');
|
|
682
|
-
const bEnum = [...(b.enum ?? [])].sort().join('');
|
|
683
|
-
if (aEnum !== bEnum)
|
|
684
|
-
return false;
|
|
685
|
-
}
|
|
686
|
-
return true;
|
|
687
|
-
}
|
|
688
|
-
//# sourceMappingURL=ports.js.map
|