@stigmer/runner 3.12.5 → 3.12.6

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 (47) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/call-agent.js +19 -6
  3. package/dist/activities/call-agent.js.map +1 -1
  4. package/dist/activities/execute-cursor/error-classifier.d.ts +9 -0
  5. package/dist/activities/execute-cursor/error-classifier.js +30 -1
  6. package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
  7. package/dist/activities/execute-cursor/index.js +15 -7
  8. package/dist/activities/execute-cursor/index.js.map +1 -1
  9. package/dist/activities/execute-cursor/service-tier.d.ts +38 -29
  10. package/dist/activities/execute-cursor/service-tier.js +92 -63
  11. package/dist/activities/execute-cursor/service-tier.js.map +1 -1
  12. package/dist/activities/execute-cursor/usage-accumulator.d.ts +16 -2
  13. package/dist/activities/execute-cursor/usage-accumulator.js +12 -2
  14. package/dist/activities/execute-cursor/usage-accumulator.js.map +1 -1
  15. package/dist/shared/caller-identity.d.ts +10 -7
  16. package/dist/shared/caller-identity.js +10 -7
  17. package/dist/shared/caller-identity.js.map +1 -1
  18. package/dist/shared/thinking-mode.d.ts +35 -0
  19. package/dist/shared/thinking-mode.js +43 -0
  20. package/dist/shared/thinking-mode.js.map +1 -0
  21. package/dist/workflow-engine/loader.js +52 -13
  22. package/dist/workflow-engine/loader.js.map +1 -1
  23. package/dist/workflow-engine/tasks/human-input.d.ts +2 -1
  24. package/dist/workflow-engine/tasks/human-input.js +8 -1
  25. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  26. package/dist/workflow-engine/types.d.ts +17 -3
  27. package/dist/workflow-engine/types.js.map +1 -1
  28. package/dist/workflows/human-input-orchestrator.d.ts +4 -0
  29. package/dist/workflows/human-input-orchestrator.js +13 -0
  30. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/activities/call-agent.ts +20 -6
  33. package/src/activities/execute-cursor/__tests__/error-classifier-billing.test.ts +67 -0
  34. package/src/activities/execute-cursor/__tests__/service-tier.test.ts +67 -7
  35. package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +33 -1
  36. package/src/activities/execute-cursor/error-classifier.ts +42 -1
  37. package/src/activities/execute-cursor/index.ts +15 -6
  38. package/src/activities/execute-cursor/service-tier.ts +94 -63
  39. package/src/activities/execute-cursor/usage-accumulator.ts +11 -1
  40. package/src/shared/caller-identity.ts +10 -7
  41. package/src/shared/thinking-mode.ts +52 -0
  42. package/src/workflow-engine/__tests__/loader.test.ts +47 -3
  43. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +39 -0
  44. package/src/workflow-engine/loader.ts +63 -17
  45. package/src/workflow-engine/tasks/human-input.ts +8 -1
  46. package/src/workflow-engine/types.ts +18 -3
  47. package/src/workflows/human-input-orchestrator.ts +20 -2
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Harness-neutral thinking-mode semantics (stigmer/stigmer#772) — the
3
+ * second variant dimension alongside `shared/service-tier.ts`.
4
+ *
5
+ * The platform contract mirrors the service tier's: the thinking pin is
6
+ * ALWAYS explicit by the time a provider request leaves the runner.
7
+ * UNSPECIFIED resolves to DISABLED here and ONLY here — every upstream
8
+ * layer preserves the caller's raw enum so "user chose disabled" stays
9
+ * distinguishable from "platform default" all the way to the ledger.
10
+ *
11
+ * Unlike the fast tier, thinking is NOT separately priced: Cursor bills
12
+ * thinking variants at base per-token rates (ledger-verified 2026-08-15 —
13
+ * 277 events at exactly base; thinking+fast at exactly the fast rate).
14
+ * The cost of ENABLED is the extra reasoning tokens, billed as output.
15
+ * Selection is therefore capability-gated (registry capabilities.thinking)
16
+ * rather than pricing-gated, and the estimate/billing paths need no
17
+ * thinking-specific rates.
18
+ *
19
+ * v1 translates thinking on the Cursor harness only (the explicit
20
+ * `thinking` variant parameter, `execute-cursor/service-tier.ts`). No
21
+ * native wire mapping exists yet — create-time validation refuses ENABLED
22
+ * for native-harness models, mirroring the tier's #361 posture.
23
+ */
24
+
25
+ import { ThinkingMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
26
+
27
+ /**
28
+ * The effective mode after platform-default resolution: never UNSPECIFIED.
29
+ */
30
+ export type EffectiveThinkingMode = ThinkingMode.DISABLED | ThinkingMode.ENABLED;
31
+
32
+ /**
33
+ * Resolve the configured mode to its effective value. The single place in
34
+ * the platform where UNSPECIFIED becomes DISABLED.
35
+ */
36
+ export function resolveEffectiveThinkingMode(
37
+ configured: ThinkingMode | undefined,
38
+ ): EffectiveThinkingMode {
39
+ return configured === ThinkingMode.ENABLED ? ThinkingMode.ENABLED : ThinkingMode.DISABLED;
40
+ }
41
+
42
+ /** Human-readable mode label for logs and error messages. */
43
+ export function thinkingModeLabel(mode: ThinkingMode): string {
44
+ switch (mode) {
45
+ case ThinkingMode.ENABLED:
46
+ return "enabled";
47
+ case ThinkingMode.DISABLED:
48
+ return "disabled";
49
+ default:
50
+ return "unspecified";
51
+ }
52
+ }
@@ -1562,13 +1562,57 @@ do:
1562
1562
  }
1563
1563
  });
1564
1564
 
1565
- it("call: human_input rejects the not-implemented escalate policy at load time", () => {
1565
+ // The escalate outcome-by-name contract (stigmer/stigmer#781): the policy
1566
+ // loads only when the gate declares an outcome named "escalate" with
1567
+ // `then` set — the timeout resolves to that outcome and follows its branch.
1568
+ const escalateYamlWithOutcomes = (onTimeout: string, outcomesYaml: string) => `
1569
+ document:
1570
+ dsl: '1.0.0'
1571
+ name: test
1572
+ do:
1573
+ - timedApproval:
1574
+ call: human_input
1575
+ with:
1576
+ prompt: "Approve within time limit"
1577
+ timeout: 3600
1578
+ on_timeout: ${onTimeout}
1579
+ outcomes:
1580
+ ${outcomesYaml}
1581
+ `;
1582
+
1583
+ it("call: human_input accepts the escalate policy when an escalate outcome with then exists", () => {
1566
1584
  for (const form of ["HUMAN_INPUT_TIMEOUT_ESCALATE", "escalate"]) {
1567
- expect(() => loadWorkflowFromYaml(humanInputYamlWithOnTimeout(form)))
1568
- .toThrow(/timedApproval.*escalate.*not implemented/);
1585
+ const model = loadWorkflowFromYaml(escalateYamlWithOutcomes(form, `
1586
+ - name: proceed
1587
+ - name: escalate
1588
+ then: escalationPath
1589
+ `));
1590
+ const task = model.do[0].task;
1591
+ expect(task.kind).toBe("human_input");
1592
+ if (task.kind === "human_input") {
1593
+ expect(task.humanInput.onTimeout).toBe("escalate");
1594
+ }
1569
1595
  }
1570
1596
  });
1571
1597
 
1598
+ it("call: human_input rejects the escalate policy without an escalate outcome", () => {
1599
+ expect(() => loadWorkflowFromYaml(humanInputYamlWithOnTimeout("escalate")))
1600
+ .toThrow(/timedApproval.*escalate.*requires an outcome named 'escalate'/);
1601
+ expect(() => loadWorkflowFromYaml(escalateYamlWithOutcomes("HUMAN_INPUT_TIMEOUT_ESCALATE", `
1602
+ - name: proceed
1603
+ - name: reject
1604
+ `)))
1605
+ .toThrow(/timedApproval.*requires an outcome named 'escalate'/);
1606
+ });
1607
+
1608
+ it("call: human_input rejects the escalate policy when the escalate outcome has no then", () => {
1609
+ expect(() => loadWorkflowFromYaml(escalateYamlWithOutcomes("escalate", `
1610
+ - name: proceed
1611
+ - name: escalate
1612
+ `)))
1613
+ .toThrow(/timedApproval.*requires an outcome named 'escalate' with 'then' set/);
1614
+ });
1615
+
1572
1616
  it("call: human_input rejects unknown on_timeout values instead of silently failing at timeout", () => {
1573
1617
  expect(() => loadWorkflowFromYaml(humanInputYamlWithOnTimeout("sometimes")))
1574
1618
  .toThrow(/timedApproval.*unknown on_timeout value 'sometimes'.*fail, approve, deny/);
@@ -305,6 +305,45 @@ describe("executeHumanInputTask", () => {
305
305
  expect(result).toEqual({ outcome: "reject", auto_resolved: true, reason: "timeout" });
306
306
  });
307
307
 
308
+ // The escalate outcome-by-name contract (stigmer/stigmer#781): the policy
309
+ // word IS the declared outcome's name, so no positional remapping happens
310
+ // — the ordinary name lookup finds the "escalate" outcome and routes its
311
+ // `then`, regardless of the outcome's position in the list.
312
+ it("resolves timeout escalate to the escalate-named outcome and routes its then", async () => {
313
+ const awaitFn: AwaitHumanInputFn = async () => ({
314
+ outcome: "escalate",
315
+ auto_resolved: true,
316
+ reason: "timeout",
317
+ });
318
+
319
+ const escalateOutcomes = [
320
+ { name: "proceed", label: "Proceed", then: "deployStep" },
321
+ { name: "escalate", label: "Escalate", then: "escalationPath" },
322
+ { name: "reject", label: "Reject" },
323
+ ];
324
+ const taskDef: HumanInputTaskDef = {
325
+ kind: "human_input",
326
+ humanInput: {
327
+ prompt: "Review", timeout: 5, onTimeout: "escalate", outcomes: escalateOutcomes,
328
+ },
329
+ };
330
+
331
+ const state = createState();
332
+ const result = await executeHumanInputTask(taskDef, "gate", state, makeCtx(awaitFn));
333
+
334
+ expect(result).toEqual({
335
+ outcome: "escalate",
336
+ auto_resolved: true,
337
+ reason: "timeout",
338
+ __flow_directive__: "escalationPath",
339
+ });
340
+ expect(state.data.gate).toEqual({
341
+ outcome: "escalate",
342
+ auto_resolved: true,
343
+ reason: "timeout",
344
+ });
345
+ });
346
+
308
347
  it("reports the mapped outcome on the approval_resolved event", async () => {
309
348
  const emitted: WorkflowEventDescriptor[][] = [];
310
349
  const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
@@ -34,6 +34,7 @@ import type {
34
34
  BackoffConfig,
35
35
  JitterConfig,
36
36
  DurationDef,
37
+ HumanInputTimeoutPolicy,
37
38
  } from "./types.js";
38
39
 
39
40
  const SUPPORTED_DSL_RANGE = ">=1.0.0 <2.0.0";
@@ -548,12 +549,14 @@ function parseAgentCallRunConfig(raw: unknown): AgentCallConfig["run_config"] {
548
549
  }
549
550
  const obj = raw as Record<string, unknown>;
550
551
 
551
- const known = new Set(["model_name", "max_cost_usd", "max_tool_rounds", "service_tier"]);
552
+ const known = new Set([
553
+ "model_name", "max_cost_usd", "max_tool_rounds", "service_tier", "thinking_mode",
554
+ ]);
552
555
  for (const key of Object.keys(obj)) {
553
556
  if (!known.has(key)) {
554
557
  throw new Error(
555
558
  `call:agent 'run_config' has unknown field '${key}' ` +
556
- `(expected: model_name, max_cost_usd, max_tool_rounds, service_tier)`,
559
+ `(expected: model_name, max_cost_usd, max_tool_rounds, service_tier, thinking_mode)`,
557
560
  );
558
561
  }
559
562
  }
@@ -571,12 +574,14 @@ function parseAgentCallRunConfig(raw: unknown): AgentCallConfig["run_config"] {
571
574
  throw new Error("call:agent 'run_config.max_tool_rounds' must be a number >= 0");
572
575
  }
573
576
  const serviceTier = parseServiceTier(obj.service_tier);
577
+ const thinkingMode = parseThinkingMode(obj.thinking_mode);
574
578
 
575
579
  return {
576
580
  model_name: modelName,
577
581
  max_cost_usd: maxCostUsd,
578
582
  max_tool_rounds: maxToolRounds,
579
583
  service_tier: serviceTier,
584
+ thinking_mode: thinkingMode,
580
585
  };
581
586
  }
582
587
 
@@ -608,6 +613,34 @@ function parseServiceTier(raw: unknown): string | undefined {
608
613
  return canonical;
609
614
  }
610
615
 
616
+ /**
617
+ * Maps YAML thinking-mode shorthands to canonical enum names, mirroring
618
+ * SERVICE_TIER_SHORTHANDS (stigmer/stigmer#772). Unknown values are
619
+ * authoring errors: the mode exists to make the served variant
620
+ * deterministic, so a typo must never silently fall back to the default.
621
+ */
622
+ const THINKING_MODE_SHORTHANDS: Record<string, string> = {
623
+ disabled: "THINKING_MODE_DISABLED",
624
+ enabled: "THINKING_MODE_ENABLED",
625
+ };
626
+
627
+ function parseThinkingMode(raw: unknown): string | undefined {
628
+ if (raw === undefined || raw === null) return undefined;
629
+ if (typeof raw !== "string") {
630
+ throw new Error("call:agent 'run_config.thinking_mode' must be a string");
631
+ }
632
+ const canonical =
633
+ THINKING_MODE_SHORTHANDS[raw.toLowerCase()] ??
634
+ (Object.values(THINKING_MODE_SHORTHANDS).includes(raw) ? raw : undefined);
635
+ if (!canonical) {
636
+ throw new Error(
637
+ `call:agent 'run_config.thinking_mode' has unknown value '${raw}' ` +
638
+ `(expected: disabled, enabled)`,
639
+ );
640
+ }
641
+ return canonical;
642
+ }
643
+
611
644
  /**
612
645
  * Maps on_timeout values to the runner's internal policy words, mirroring
613
646
  * SERVICE_TIER_SHORTHANDS. The persisted CNCF YAML carries the proto enum
@@ -618,36 +651,30 @@ function parseServiceTier(raw: unknown): string | undefined {
618
651
  * enum-name policy reach the orchestrator unrecognized, so gates configured
619
652
  * to auto-approve/deny failed at their first real timeout instead).
620
653
  */
621
- const ON_TIMEOUT_VOCABULARY: Record<string, "fail" | "approve" | "deny"> = {
654
+ const ON_TIMEOUT_VOCABULARY: Record<string, HumanInputTimeoutPolicy> = {
622
655
  fail: "fail",
623
656
  approve: "approve",
624
657
  deny: "deny",
658
+ escalate: "escalate",
625
659
  HUMAN_INPUT_TIMEOUT_FAIL: "fail",
626
660
  HUMAN_INPUT_TIMEOUT_APPROVE: "approve",
627
661
  HUMAN_INPUT_TIMEOUT_DENY: "deny",
662
+ HUMAN_INPUT_TIMEOUT_ESCALATE: "escalate",
628
663
  };
629
664
 
630
665
  function parseOnTimeout(
631
666
  taskName: string,
632
667
  raw: unknown,
633
- ): "fail" | "approve" | "deny" | undefined {
668
+ ): HumanInputTimeoutPolicy | undefined {
634
669
  if (raw === undefined || raw === null) return undefined;
635
670
  if (typeof raw !== "string") {
636
671
  throw new Error(`human_input task '${taskName}': 'on_timeout' must be a string`);
637
672
  }
638
- // The proto declares HUMAN_INPUT_TIMEOUT_ESCALATE but no runtime exists
639
- // for it yet; refuse at load rather than misbehave at the gate's timeout.
640
- if (raw === "HUMAN_INPUT_TIMEOUT_ESCALATE" || raw === "escalate") {
641
- throw new Error(
642
- `human_input task '${taskName}': on_timeout policy 'escalate' is not implemented — ` +
643
- `use fail, approve, or deny (custom outcomes with 'then' cover reviewer-driven branching)`,
644
- );
645
- }
646
673
  const policy = ON_TIMEOUT_VOCABULARY[raw];
647
674
  if (!policy) {
648
675
  throw new Error(
649
676
  `human_input task '${taskName}': unknown on_timeout value '${raw}' ` +
650
- `(expected: fail, approve, deny, or a HUMAN_INPUT_TIMEOUT_* enum name)`,
677
+ `(expected: fail, approve, deny, escalate, or a HUMAN_INPUT_TIMEOUT_* enum name)`,
651
678
  );
652
679
  }
653
680
  return policy;
@@ -663,11 +690,30 @@ function parseHumanInputConfig(taskName: string, raw: unknown): import("./types.
663
690
  throw new Error(`human_input task '${taskName}' requires 'prompt' in 'with'`);
664
691
  }
665
692
 
693
+ const outcomes = Array.isArray(obj.outcomes) && obj.outcomes.length > 0
694
+ ? obj.outcomes.map((o: any) => ({ name: o.name, label: o.label, then: o.then }))
695
+ : undefined;
696
+ const onTimeout = parseOnTimeout(taskName, obj.on_timeout);
697
+
698
+ // The escalate policy's outcome-by-name contract (stigmer/stigmer#781):
699
+ // a timeout resolves to the outcome NAMED "escalate", so that outcome must
700
+ // exist and declare where the escalation branch goes. Checked at load so a
701
+ // misconfigured gate fails before it ever waits on a reviewer — the server
702
+ // validator enforces the same shape at apply; this covers hand-written YAML.
703
+ if (onTimeout === "escalate") {
704
+ const escalation = outcomes?.find((o) => o.name === "escalate");
705
+ if (!escalation || !escalation.then) {
706
+ throw new Error(
707
+ `human_input task '${taskName}': on_timeout policy 'escalate' requires ` +
708
+ `an outcome named 'escalate' with 'then' set (the timeout resolves to ` +
709
+ `that outcome and follows its 'then' branch)`,
710
+ );
711
+ }
712
+ }
713
+
666
714
  return {
667
715
  prompt: obj.prompt,
668
- outcomes: Array.isArray(obj.outcomes) && obj.outcomes.length > 0
669
- ? obj.outcomes.map((o: any) => ({ name: o.name, label: o.label, then: o.then }))
670
- : undefined,
716
+ outcomes,
671
717
  formSchema: obj.form_schema && typeof obj.form_schema === "object"
672
718
  ? obj.form_schema as Record<string, unknown>
673
719
  : undefined,
@@ -675,7 +721,7 @@ function parseHumanInputConfig(taskName: string, raw: unknown): import("./types.
675
721
  ? obj.approvers.filter((a: unknown) => typeof a === "string") as string[]
676
722
  : undefined,
677
723
  timeout: typeof obj.timeout === "number" ? obj.timeout : undefined,
678
- onTimeout: parseOnTimeout(taskName, obj.on_timeout),
724
+ onTimeout,
679
725
  // Any JSON shape is a valid payload (expression string, object, array),
680
726
  // so only null/undefined mean "no payload" here.
681
727
  payload: obj.payload ?? undefined,
@@ -2,7 +2,8 @@
2
2
  * Human input task executor — HITL approval gate.
3
3
  *
4
4
  * Pauses workflow execution until a human reviewer responds via signal.
5
- * Supports configurable timeout with policies (fail, auto-approve, auto-deny).
5
+ * Supports configurable timeout with policies (fail, auto-approve,
6
+ * auto-deny, escalate).
6
7
  *
7
8
  * The kernel validates the config and delegates to `ctx.awaitHumanInput()`
8
9
  * which is wired to the Temporal workflow layer's signal/timer selector.
@@ -143,6 +144,12 @@ function validateConfig(config: HumanInputConfig, taskName: string): void {
143
144
  * orchestrator's internal approve/deny words, which a reviewer of a
144
145
  * custom-outcome gate was never offered. Binary gates (no custom outcomes)
145
146
  * keep the plain approve/deny result.
147
+ *
148
+ * Escalate needs no remapping by construction (stigmer/stigmer#781): its
149
+ * policy word IS the declared outcome's name — the loader (and the server
150
+ * validator) only accept the escalate policy when an outcome named
151
+ * "escalate" with `then` exists, so the caller's ordinary name lookup
152
+ * routes the escalation branch.
146
153
  */
147
154
  function applyTimeoutOutcomeContract(
148
155
  result: HumanInputResult,
@@ -557,14 +557,23 @@ export interface RunWorkflowExecutionConfig {
557
557
 
558
558
  /**
559
559
  * Blocks workflow execution until a human provides input via signal.
560
- * Supports configurable timeout with policies (fail, approve, deny).
560
+ * Supports configurable timeout with policies (fail, approve, deny, escalate).
561
561
  */
562
562
  export type AwaitHumanInputFn = (config: HumanInputExecutionConfig) => Promise<HumanInputResult>;
563
563
 
564
+ /**
565
+ * Internal timeout-policy vocabulary — the loader-normalized form of the
566
+ * proto's HumanInputTimeoutPolicy enum. "escalate" carries the outcome-by-name
567
+ * contract (stigmer/stigmer#781): it is only loadable when the gate declares
568
+ * an outcome named "escalate" with `then` set, and a timeout resolves to that
569
+ * outcome so the existing `then`/flow-directive routing takes the branch.
570
+ */
571
+ export type HumanInputTimeoutPolicy = "fail" | "approve" | "deny" | "escalate";
572
+
564
573
  export interface HumanInputExecutionConfig {
565
574
  readonly signalName: string;
566
575
  readonly timeoutSeconds: number;
567
- readonly onTimeout: "fail" | "approve" | "deny";
576
+ readonly onTimeout: HumanInputTimeoutPolicy;
568
577
  }
569
578
 
570
579
  export interface HumanInputResult {
@@ -602,7 +611,7 @@ export interface HumanInputConfig {
602
611
  readonly formSchema?: Record<string, unknown>;
603
612
  readonly approvers?: readonly string[];
604
613
  readonly timeout?: number;
605
- readonly onTimeout?: "fail" | "approve" | "deny";
614
+ readonly onTimeout?: HumanInputTimeoutPolicy;
606
615
  /**
607
616
  * Material for the reviewer to examine — a whole-value expression
608
617
  * string, or an object/array with embedded expressions. Resolved at
@@ -749,6 +758,12 @@ export interface AgentCallRunConfig {
749
758
  * ("standard"/"fast") to these, mirroring harness.
750
759
  */
751
760
  readonly service_tier?: string;
761
+ /**
762
+ * Canonical ThinkingMode enum name ("THINKING_MODE_DISABLED" /
763
+ * "THINKING_MODE_ENABLED"); the loader maps the YAML shorthands
764
+ * ("disabled"/"enabled") to these, mirroring service_tier (#772).
765
+ */
766
+ readonly thinking_mode?: string;
752
767
  }
753
768
 
754
769
  export interface AgentCallOutputContract {
@@ -7,6 +7,10 @@
7
7
  * - "fail": throws an error
8
8
  * - "approve": returns auto-approved output
9
9
  * - "deny": returns auto-denied output
10
+ * - "escalate": returns the "escalate" outcome — by the outcome-by-name
11
+ * contract (stigmer/stigmer#781) the loader guarantees the gate declares
12
+ * an outcome with that exact name and a `then` branch, so the executor's
13
+ * ordinary outcome routing takes the escalation path.
10
14
  *
11
15
  * Signal payload shape:
12
16
  * { outcome, form_data?, reviewer, reviewer_actor?, responded_at }
@@ -16,7 +20,11 @@
16
20
 
17
21
  import { defineSignal, setHandler, condition } from "@temporalio/workflow";
18
22
 
19
- import type { HumanInputExecutionConfig, HumanInputResult } from "../workflow-engine/types.js";
23
+ import type {
24
+ HumanInputExecutionConfig,
25
+ HumanInputResult,
26
+ HumanInputTimeoutPolicy,
27
+ } from "../workflow-engine/types.js";
20
28
 
21
29
  /**
22
30
  * Orchestrates a human_input task — blocks until signal or timeout.
@@ -51,7 +59,7 @@ export async function orchestrateHumanInput(
51
59
 
52
60
  function handleTimeout(
53
61
  signalName: string,
54
- policy: "fail" | "approve" | "deny",
62
+ policy: HumanInputTimeoutPolicy,
55
63
  ): HumanInputResult {
56
64
  switch (policy) {
57
65
  case "approve":
@@ -68,6 +76,16 @@ function handleTimeout(
68
76
  reason: "timeout",
69
77
  };
70
78
 
79
+ case "escalate":
80
+ // The outcome word IS the declared outcome's name (loader-enforced),
81
+ // so no positional remapping happens downstream — the executor's name
82
+ // lookup routes the escalation branch directly.
83
+ return {
84
+ outcome: "escalate",
85
+ auto_resolved: true,
86
+ reason: "timeout",
87
+ };
88
+
71
89
  case "fail":
72
90
  default:
73
91
  throw new Error(