skillscript-runtime 0.15.7 → 0.16.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 +23 -24
- package/dist/compile.js +25 -71
- package/dist/compile.js.map +1 -1
- package/dist/errors.d.ts +2 -7
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -14
- package/dist/errors.js.map +1 -1
- package/dist/help-content.d.ts.map +1 -1
- package/dist/help-content.js +74 -103
- package/dist/help-content.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +43 -214
- package/dist/lint.js.map +1 -1
- package/dist/mutation-gate.d.ts +7 -13
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +6 -11
- package/dist/mutation-gate.js.map +1 -1
- package/dist/parser.d.ts +25 -74
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +30 -264
- package/dist/parser.js.map +1 -1
- package/dist/runtime.d.ts +1 -10
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +76 -188
- package/dist/runtime.js.map +1 -1
- package/dist/skill-manager.js +1 -1
- package/dist/skill-manager.js.map +1 -1
- package/docs/language-reference.md +3 -24
- package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
- package/package.json +1 -1
- package/examples/skillscripts/cut-release-tag.skill.md +0 -40
package/dist/runtime.js
CHANGED
|
@@ -6,7 +6,7 @@ import { validateQualifiedDispatch } from "./dispatch-validate.js";
|
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { readFile as fsReadFile, writeFile as fsWriteFile, mkdir as fsMkdir } from "node:fs/promises";
|
|
8
8
|
import { dirname as pathDirname } from "node:path";
|
|
9
|
-
import { OpError, ConnectorNotFoundError, OpTimeoutError,
|
|
9
|
+
import { OpError, ConnectorNotFoundError, OpTimeoutError, UnsafeShellDisabledError, UnresolvedVariableError, TypeMismatchError, MissingSkillReferenceError, UnconfirmedMutationError, messageOf, } from "./errors.js";
|
|
10
10
|
import { classifyMutation, authorizationGranted, buildAuthorizationSuggestion, } from "./mutation-gate.js";
|
|
11
11
|
import { TraceBuilder, shouldTraceFire } from "./trace.js";
|
|
12
12
|
/**
|
|
@@ -112,12 +112,11 @@ export async function execute(parsed, initialVars, order, ctx) {
|
|
|
112
112
|
continue;
|
|
113
113
|
let targetLastBound = null;
|
|
114
114
|
let targetLastValue = undefined;
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
115
|
+
// Fresh mutation-auth state per target. `skillAutonomous` is set from
|
|
116
|
+
// the parsed header. v0.16.0 retired the `sawConfirm` path with the
|
|
117
|
+
// `ask` op removal.
|
|
118
118
|
const authState = {
|
|
119
119
|
skillAutonomous: parsed.autonomous === true,
|
|
120
|
-
sawConfirm: false,
|
|
121
120
|
};
|
|
122
121
|
try {
|
|
123
122
|
const r = await execOps(target.ops, vars, emissions, fallbacks, ctx, targetName, parsed.timeout, absoluteTimeoutMs, traceBuilder, authState);
|
|
@@ -276,15 +275,11 @@ async function execOps(ops, vars, emissions, fallbacks, ctx, targetName, skillTi
|
|
|
276
275
|
let lastBoundVar = null;
|
|
277
276
|
let lastValue = undefined;
|
|
278
277
|
for (const op of ops) {
|
|
279
|
-
// v0.
|
|
280
|
-
//
|
|
281
|
-
// gate check) so a `??` op's own classification path stays consistent
|
|
282
|
-
// with lint. Non-mutation `??` skips the throw branch naturally.
|
|
283
|
-
if (op.kind === "??")
|
|
284
|
-
authState.sawConfirm = true;
|
|
278
|
+
// v0.16.0 — `ask` removed; mutation gate now relies solely on `approved=`
|
|
279
|
+
// per-op kwarg + `# Autonomous: true` skill flag (sawConfirm path retired).
|
|
285
280
|
const mutKind = classifyMutation(op);
|
|
286
281
|
if (mutKind !== null && !authorizationGranted(op, authState)) {
|
|
287
|
-
throw new UnconfirmedMutationError(mutKind.kind, mutKind.detail, ["# Autonomous: true header", "
|
|
282
|
+
throw new UnconfirmedMutationError(mutKind.kind, mutKind.detail, ["# Autonomous: true header", "approved=\"reason\" per-op kwarg"], buildAuthorizationSuggestion(mutKind), op.kind, targetName);
|
|
288
283
|
}
|
|
289
284
|
const r = await execOp(op, vars, emissions, fallbacks, ctx, targetName, skillTimeoutSec, absoluteTimeoutMs, traceBuilder, authState);
|
|
290
285
|
if (r.lastBoundVar !== null) {
|
|
@@ -327,16 +322,13 @@ async function execOp(op, vars, emissions, fallbacks, ctx, targetName, skillTime
|
|
|
327
322
|
}
|
|
328
323
|
}
|
|
329
324
|
}
|
|
330
|
-
/** Extract the connector instance name for
|
|
325
|
+
/** Extract the connector instance name for `$` ops; undefined for others. */
|
|
331
326
|
function extractOpConnector(op) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
case ">": return op.retrievalParams?.connector ?? "primary";
|
|
336
|
-
default: return undefined;
|
|
337
|
-
}
|
|
327
|
+
if (op.kind === "$")
|
|
328
|
+
return op.mcpConnector ?? "primary";
|
|
329
|
+
return undefined;
|
|
338
330
|
}
|
|
339
|
-
async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skillTimeoutSec, absoluteTimeoutMs, traceBuilder, authState = { skillAutonomous: false
|
|
331
|
+
async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skillTimeoutSec, absoluteTimeoutMs, traceBuilder, authState = { skillAutonomous: false }) {
|
|
340
332
|
switch (op.kind) {
|
|
341
333
|
case "$set": {
|
|
342
334
|
// v0.5.0 item 3 — `$set X = "...$(REF)..."` now resolves $(REF) at
|
|
@@ -392,12 +384,12 @@ async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skil
|
|
|
392
384
|
emissions.push(`Reason: ${body}`);
|
|
393
385
|
return { lastBoundVar: null, lastValue: undefined };
|
|
394
386
|
}
|
|
395
|
-
case "
|
|
387
|
+
case "emit": {
|
|
396
388
|
const body = substituteRuntime(op.body, vars);
|
|
397
389
|
emissions.push(body);
|
|
398
390
|
return { lastBoundVar: null, lastValue: undefined };
|
|
399
391
|
}
|
|
400
|
-
case "
|
|
392
|
+
case "shell": {
|
|
401
393
|
const body = op.policy === "unsafe"
|
|
402
394
|
? substituteRuntimeUnsafe(op.body, vars)
|
|
403
395
|
: substituteRuntime(op.body, vars);
|
|
@@ -405,8 +397,6 @@ async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skil
|
|
|
405
397
|
if (ctx.mechanical === true) {
|
|
406
398
|
const label = op.policy === "unsafe" ? "Would run unsafe shell" : "Would run shell";
|
|
407
399
|
emissions.push(`${label}: ${body} (mechanical: true preview).`);
|
|
408
|
-
// Bind a placeholder so downstream `$(VAR)` substitutions resolve.
|
|
409
|
-
// Matches the convention used by `$`/`~`/`>` mechanical-mode binding.
|
|
410
400
|
const flatKey = `${targetName}.output`;
|
|
411
401
|
const placeholder = `[mechanical: would run ${body.slice(0, 40)}${body.length > 40 ? "..." : ""}]`;
|
|
412
402
|
vars.set(flatKey, placeholder);
|
|
@@ -427,7 +417,7 @@ async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skil
|
|
|
427
417
|
else {
|
|
428
418
|
const tokens = tokenizeShellArgs(body);
|
|
429
419
|
if (tokens.length === 0) {
|
|
430
|
-
throw new OpError(`Empty
|
|
420
|
+
throw new OpError(`Empty \`shell(...)\` op body in target '${targetName}'.`, "shell", "Provide a non-empty `command=\"...\"` kwarg.", targetName);
|
|
431
421
|
}
|
|
432
422
|
const [bin, ...args] = tokens;
|
|
433
423
|
stdout = await execShellCommand(bin, args, shellTimeoutMs);
|
|
@@ -441,41 +431,14 @@ async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skil
|
|
|
441
431
|
lastValue: stdout,
|
|
442
432
|
};
|
|
443
433
|
}
|
|
444
|
-
case "
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
throw new InteractiveOpInAutonomousModeError(promptStr, targetName);
|
|
451
|
-
}
|
|
452
|
-
const response = await ctx.askUser(promptStr);
|
|
453
|
-
const outName = op.outputVar;
|
|
454
|
-
if (outName !== undefined)
|
|
455
|
-
vars.set(outName, response);
|
|
456
|
-
// Decline semantics (per Section 2 Ops + §13 Open Q #2 resolution):
|
|
457
|
-
// bind the response AND short-circuit downstream via soft op-error
|
|
458
|
-
// routed through else: / # OnError:. Closes the silent-fall-through
|
|
459
|
-
// security bug pattern (subsequent `apply:` running on a "no").
|
|
460
|
-
if (isDeclineResponse(response)) {
|
|
461
|
-
throw new OpError(`User declined at \`??\` prompt: '${promptStr}' (response: '${response}'). Dependent targets short-circuited.`, "??", "If decline should not short-circuit, handle the response in an `else:` branch or `# OnError:` fallback.", targetName);
|
|
462
|
-
}
|
|
463
|
-
return {
|
|
464
|
-
lastBoundVar: outName ?? null,
|
|
465
|
-
lastValue: response,
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
case "&": {
|
|
469
|
-
// v0.3.1: deferred-resolution path. `&` ops that reached runtime
|
|
470
|
-
// are either (a) forward-references that compile couldn't inline
|
|
471
|
-
// because the target wasn't yet stored, or (b) the rare "raw AST
|
|
472
|
-
// bypassed compile()" case. Try to resolve through a SkillStore on
|
|
473
|
-
// the context if one is wired; otherwise throw MissingSkillReferenceError
|
|
474
|
-
// with the structured fields so `# OnError:` can catch.
|
|
434
|
+
case "inline": {
|
|
435
|
+
// Deferred-resolution path. `inline` ops that reached runtime are
|
|
436
|
+
// either (a) forward-references that compile couldn't inline because
|
|
437
|
+
// the target wasn't yet stored, or (b) the rare "raw AST bypassed
|
|
438
|
+
// compile()" case. Surface as MissingSkillReferenceError so `# OnError:`
|
|
439
|
+
// can catch.
|
|
475
440
|
const skillName = op.ampParams?.skillName ?? "(unknown)";
|
|
476
|
-
|
|
477
|
-
// for consistency (same shape as runtime resolve-and-miss path).
|
|
478
|
-
throw new MissingSkillReferenceError(skillName, "&", "&", targetName);
|
|
441
|
+
throw new MissingSkillReferenceError(skillName, "inline", "inline", targetName);
|
|
479
442
|
}
|
|
480
443
|
case "file_read": {
|
|
481
444
|
// v0.7.0 — runtime-intrinsic file read. Substitutes `${VAR}` /
|
|
@@ -647,6 +610,18 @@ async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skil
|
|
|
647
610
|
const toolName = m[1];
|
|
648
611
|
const argsStr = m[2] ?? "";
|
|
649
612
|
const args = parseToolArgs(argsStr);
|
|
613
|
+
// v0.16.0 — op-level `timeout=N` kwarg reserved for `$` dispatch (parity
|
|
614
|
+
// with legacy `~` `timeoutSeconds`). Pop from args before forwarding so
|
|
615
|
+
// connectors don't see a kwarg they didn't declare. Accepts int literal
|
|
616
|
+
// or `$(VAR)` / `${VAR}` ref string; resolveOpTimeoutMs coerces refs.
|
|
617
|
+
const rawTimeoutKwarg = args["timeout"];
|
|
618
|
+
let perOpTimeoutSec;
|
|
619
|
+
if (typeof rawTimeoutKwarg === "number")
|
|
620
|
+
perOpTimeoutSec = rawTimeoutKwarg;
|
|
621
|
+
else if (typeof rawTimeoutKwarg === "string" && rawTimeoutKwarg !== "")
|
|
622
|
+
perOpTimeoutSec = rawTimeoutKwarg;
|
|
623
|
+
if (rawTimeoutKwarg !== undefined)
|
|
624
|
+
delete args["timeout"];
|
|
650
625
|
const connectorLabel = op.mcpConnector !== undefined ? `${op.mcpConnector}.` : "";
|
|
651
626
|
const flatKey = `${targetName}.output`;
|
|
652
627
|
// Mechanical preview, registry-routed, test escape hatch, no-dispatcher.
|
|
@@ -703,19 +678,23 @@ async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skil
|
|
|
703
678
|
vars.set(op.outputVar, parsed);
|
|
704
679
|
return { lastBoundVar: op.outputVar ?? flatKey, lastValue: parsed };
|
|
705
680
|
}
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
//
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
681
|
+
// Bare-form name-match dispatch: when `$ <name> ...` is bare (no dotted
|
|
682
|
+
// prefix), `<name>` must match a registered connector name. This makes
|
|
683
|
+
// typed-contract patterns (`$ llm prompt=...`, `$ data_read mode=...`)
|
|
684
|
+
// dispatch cleanly to the auto-wired bridge connectors. v0.16.0 dropped
|
|
685
|
+
// the legacy "primary" fallback — substrate-specific MCP tools require
|
|
686
|
+
// named form `$ <connector>.<tool>` and fail clearly if bare-form is
|
|
687
|
+
// used with no matching connector.
|
|
688
|
+
let connectorName;
|
|
689
|
+
if (op.mcpConnector !== undefined) {
|
|
690
|
+
connectorName = op.mcpConnector;
|
|
691
|
+
}
|
|
692
|
+
else if (ctx.registry.hasMcpConnector(toolName)) {
|
|
717
693
|
connectorName = toolName;
|
|
718
694
|
}
|
|
695
|
+
else {
|
|
696
|
+
connectorName = toolName; // will fail through to ConnectorNotFoundError below
|
|
697
|
+
}
|
|
719
698
|
// v0.4.1 — defense-in-depth allowlist check. Lint catches this at
|
|
720
699
|
// compile time via `disallowed-tool`; this runtime check is the
|
|
721
700
|
// backstop for compiled artifacts run against a different runtime
|
|
@@ -740,7 +719,7 @@ async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skil
|
|
|
740
719
|
}
|
|
741
720
|
let rawResult;
|
|
742
721
|
let dispatched = false;
|
|
743
|
-
const timeoutMs = resolveOpTimeoutMs(
|
|
722
|
+
const timeoutMs = resolveOpTimeoutMs(perOpTimeoutSec, skillTimeoutSec, absoluteTimeoutMs, vars);
|
|
744
723
|
// Op-level fallback (per language reference §9, extended to `$` for
|
|
745
724
|
// cold-agent corpus consistency). On dispatch throw, bind the
|
|
746
725
|
// fallback value to the output var; on missing connector with
|
|
@@ -796,121 +775,34 @@ async function execOpInner(op, vars, emissions, fallbacks, ctx, targetName, skil
|
|
|
796
775
|
throw new OpError(`tool ${connectorLabel}${toolName} returned isError: ${innerText}`, "$", "The tool itself failed; inspect the inner error text. Add `(fallback: ...)` to the op for graceful failure.", targetName);
|
|
797
776
|
}
|
|
798
777
|
const bindValue = unwrapToolResult(rawResult);
|
|
799
|
-
|
|
778
|
+
// v0.16.0 — fallback-on-empty parity with legacy `~`/`>` semantics
|
|
779
|
+
// (LocalModel empty-trimmed-response + Retrieval empty-array both bind
|
|
780
|
+
// fallback). Without this, the doc-vs-code mismatch at parser.ts:84-91
|
|
781
|
+
// (which claims `$` fallback fires "on throw or empty result") would
|
|
782
|
+
// persist. Empty = "" after trim, OR [], OR null/undefined.
|
|
783
|
+
let finalValue = bindValue;
|
|
784
|
+
if (dollarFallback !== undefined) {
|
|
785
|
+
const isEmptyString = typeof bindValue === "string" && bindValue.trim() === "";
|
|
786
|
+
const isEmptyArray = Array.isArray(bindValue) && bindValue.length === 0;
|
|
787
|
+
const isNullish = bindValue === null || bindValue === undefined;
|
|
788
|
+
if (isEmptyString || isEmptyArray || isNullish) {
|
|
789
|
+
finalValue = dollarFallback;
|
|
790
|
+
fallbacks.push({
|
|
791
|
+
target: targetName,
|
|
792
|
+
opKind: "$",
|
|
793
|
+
value: dollarFallback,
|
|
794
|
+
reason: `$ ${connectorLabel}${toolName} returned empty result`,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
vars.set(flatKey, finalValue);
|
|
800
799
|
if (op.outputVar !== undefined)
|
|
801
|
-
vars.set(op.outputVar,
|
|
800
|
+
vars.set(op.outputVar, finalValue);
|
|
802
801
|
return {
|
|
803
802
|
lastBoundVar: op.outputVar ?? flatKey,
|
|
804
|
-
lastValue:
|
|
803
|
+
lastValue: finalValue,
|
|
805
804
|
};
|
|
806
805
|
}
|
|
807
|
-
case ">": {
|
|
808
|
-
const p = op.retrievalParams;
|
|
809
|
-
const querySub = substituteRuntime(p.query, vars);
|
|
810
|
-
const extraSub = {};
|
|
811
|
-
for (const [k, v] of Object.entries(p.extra)) {
|
|
812
|
-
extraSub[k] = substituteRuntime(v, vars);
|
|
813
|
-
}
|
|
814
|
-
if (ctx.mechanical === true) {
|
|
815
|
-
// Bind a 1-element array of placeholders so foreach M in $(RESULTS)
|
|
816
|
-
// iterates once with a dotted-accessible M (matches common author
|
|
817
|
-
// patterns like `$(M.id)`, `$(M.summary)`). Authors expecting empty
|
|
818
|
-
// result sets test the empty case in unit tests, not mechanical mode.
|
|
819
|
-
const mechanicalValue = p.fallback !== undefined
|
|
820
|
-
? p.fallback
|
|
821
|
-
: [makeMechanicalPlaceholder(`${op.outputVar}[0]`)];
|
|
822
|
-
emissions.push(`Would query DataStore \`${p.connector}\` with mode=${p.mode}, ` +
|
|
823
|
-
`query="${querySub}", limit=${p.limit} (mechanical: true preview). ` +
|
|
824
|
-
`Binding $(${op.outputVar}) = placeholder result set.`);
|
|
825
|
-
vars.set(op.outputVar, mechanicalValue);
|
|
826
|
-
return { lastBoundVar: op.outputVar, lastValue: mechanicalValue };
|
|
827
|
-
}
|
|
828
|
-
const store = ctx.registry.getDataStore(p.connector);
|
|
829
|
-
const limitResolved = resolveIntParam(p.limit, vars, "limit");
|
|
830
|
-
const filters = {
|
|
831
|
-
query: querySub,
|
|
832
|
-
mode: p.mode,
|
|
833
|
-
limit: limitResolved,
|
|
834
|
-
...extraSub,
|
|
835
|
-
};
|
|
836
|
-
const retrievalTimeoutMs = resolveOpTimeoutMs(undefined, skillTimeoutSec, absoluteTimeoutMs, vars);
|
|
837
|
-
// Op-level fallback (per language reference §9): on throw OR empty
|
|
838
|
-
// result, bind the fallback value and continue. Without a fallback,
|
|
839
|
-
// throws propagate to `else:` / `# OnError:` / target error.
|
|
840
|
-
// coerceLiteralValue parses array-shaped literals (`[]`, `[a, b]`)
|
|
841
|
-
// into actual arrays so downstream `foreach M in $(VAR)` iterates
|
|
842
|
-
// correctly on the empty/sentinel case.
|
|
843
|
-
const coercedFallback = p.fallback !== undefined ? coerceLiteralValue(p.fallback) : undefined;
|
|
844
|
-
let results;
|
|
845
|
-
try {
|
|
846
|
-
results = await dispatchWithTimeout(() => store.query(filters), retrievalTimeoutMs, ">");
|
|
847
|
-
if (coercedFallback !== undefined && Array.isArray(results) && results.length === 0) {
|
|
848
|
-
results = coercedFallback;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
catch (err) {
|
|
852
|
-
if (coercedFallback !== undefined) {
|
|
853
|
-
results = coercedFallback;
|
|
854
|
-
}
|
|
855
|
-
else {
|
|
856
|
-
throw err;
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
vars.set(op.outputVar, results);
|
|
860
|
-
return { lastBoundVar: op.outputVar, lastValue: results };
|
|
861
|
-
}
|
|
862
|
-
case "~": {
|
|
863
|
-
const p = op.localModelParams;
|
|
864
|
-
const promptSub = substituteRuntime(p.prompt, vars);
|
|
865
|
-
if (ctx.mechanical === true) {
|
|
866
|
-
const modelName = p.model ?? "default";
|
|
867
|
-
// v0.2.12 Bug 23: bind a Proxy placeholder (same shape as the `$`/`>`
|
|
868
|
-
// mechanical handlers) so dotted field access — `$(HI.outputs.text)`,
|
|
869
|
-
// `$(HI.choices.0.message.content)` — resolves to deeper placeholders
|
|
870
|
-
// instead of erroring with UnresolvedVariableError. Pre-fix the `~` op
|
|
871
|
-
// bound a flat string, which broke field access in mechanical mode.
|
|
872
|
-
const placeholder = makeMechanicalPlaceholder(op.outputVar ?? `${modelName}.output`);
|
|
873
|
-
emissions.push(`Would invoke LocalModel \`${modelName}\` with prompt='${promptSub}' ` +
|
|
874
|
-
`(mechanical: true preview). Binding $(${op.outputVar}) = ${stringifyValue(placeholder)}`);
|
|
875
|
-
vars.set(op.outputVar, placeholder);
|
|
876
|
-
return { lastBoundVar: op.outputVar, lastValue: placeholder };
|
|
877
|
-
}
|
|
878
|
-
let model;
|
|
879
|
-
try {
|
|
880
|
-
model = ctx.registry.getLocalModel(p.model);
|
|
881
|
-
}
|
|
882
|
-
catch (err) {
|
|
883
|
-
if (p.fallback !== undefined) {
|
|
884
|
-
vars.set(op.outputVar, p.fallback);
|
|
885
|
-
return { lastBoundVar: op.outputVar, lastValue: p.fallback };
|
|
886
|
-
}
|
|
887
|
-
throw err;
|
|
888
|
-
}
|
|
889
|
-
const runOpts = {};
|
|
890
|
-
if (p.maxTokens !== undefined) {
|
|
891
|
-
runOpts.maxTokens = resolveIntParam(p.maxTokens, vars, "maxTokens");
|
|
892
|
-
}
|
|
893
|
-
const tildeTimeoutMs = resolveOpTimeoutMs(p.timeoutSeconds, skillTimeoutSec, absoluteTimeoutMs, vars);
|
|
894
|
-
// Op-level fallback (per language reference §9): on throw OR empty
|
|
895
|
-
// (trimmed) response, bind the fallback value.
|
|
896
|
-
let response;
|
|
897
|
-
try {
|
|
898
|
-
response = await dispatchWithTimeout(() => model.run(promptSub, runOpts), tildeTimeoutMs, "~");
|
|
899
|
-
if (p.fallback !== undefined && response.trim() === "") {
|
|
900
|
-
response = p.fallback;
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
catch (err) {
|
|
904
|
-
if (p.fallback !== undefined) {
|
|
905
|
-
response = p.fallback;
|
|
906
|
-
}
|
|
907
|
-
else {
|
|
908
|
-
throw err;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
vars.set(op.outputVar, response);
|
|
912
|
-
return { lastBoundVar: op.outputVar, lastValue: response };
|
|
913
|
-
}
|
|
914
806
|
case "foreach": {
|
|
915
807
|
const listVal = resolveListExpr(op.foreachList, vars);
|
|
916
808
|
const iterName = op.foreachIter;
|
|
@@ -1086,17 +978,17 @@ async function execShellCommand(bin, args, timeoutMs) {
|
|
|
1086
978
|
child.stderr?.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
1087
979
|
child.on("error", (err) => {
|
|
1088
980
|
clearTimeout(timer);
|
|
1089
|
-
reject(new OpError(`Failed to spawn '${bin}': ${err.message}`, "
|
|
981
|
+
reject(new OpError(`Failed to spawn '${bin}': ${err.message}`, "shell", "Verify the binary is on PATH and executable. Check for typos in the `shell(command=...)` body."));
|
|
1090
982
|
});
|
|
1091
983
|
child.on("close", (code) => {
|
|
1092
984
|
clearTimeout(timer);
|
|
1093
985
|
if (killed) {
|
|
1094
|
-
reject(new OpTimeoutError(timeoutMs, "
|
|
986
|
+
reject(new OpTimeoutError(timeoutMs, "shell"));
|
|
1095
987
|
return;
|
|
1096
988
|
}
|
|
1097
989
|
if (code !== 0) {
|
|
1098
990
|
const trimmed = stderr.trim();
|
|
1099
|
-
reject(new OpError(`Shell command '${bin}' exited with code ${code}${trimmed ? `: ${trimmed.slice(0, 200)}` : ""}.`, "
|
|
991
|
+
reject(new OpError(`Shell command '${bin}' exited with code ${code}${trimmed ? `: ${trimmed.slice(0, 200)}` : ""}.`, "shell", "Inspect the stderr output. Add `(fallback: ...)` to the `shell(...)` op for graceful failure on non-zero exit."));
|
|
1100
992
|
return;
|
|
1101
993
|
}
|
|
1102
994
|
// Strip trailing newline — convention for shell command output.
|
|
@@ -1104,10 +996,6 @@ async function execShellCommand(bin, args, timeoutMs) {
|
|
|
1104
996
|
});
|
|
1105
997
|
});
|
|
1106
998
|
}
|
|
1107
|
-
function isDeclineResponse(raw) {
|
|
1108
|
-
const t = raw.trim().toLowerCase();
|
|
1109
|
-
return t === "" || t === "no" || t === "n" || t === "false" || t === "0";
|
|
1110
|
-
}
|
|
1111
999
|
/**
|
|
1112
1000
|
* Resolve an integer parameter that may be a literal number or a string
|
|
1113
1001
|
* containing a `$(VAR)` ref. Substitutes any refs then parseInts. Throws
|