@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.
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/call-agent.js +19 -6
- package/dist/activities/call-agent.js.map +1 -1
- package/dist/activities/execute-cursor/error-classifier.d.ts +9 -0
- package/dist/activities/execute-cursor/error-classifier.js +30 -1
- package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +15 -7
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/service-tier.d.ts +38 -29
- package/dist/activities/execute-cursor/service-tier.js +92 -63
- package/dist/activities/execute-cursor/service-tier.js.map +1 -1
- package/dist/activities/execute-cursor/usage-accumulator.d.ts +16 -2
- package/dist/activities/execute-cursor/usage-accumulator.js +12 -2
- package/dist/activities/execute-cursor/usage-accumulator.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +10 -7
- package/dist/shared/caller-identity.js +10 -7
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/thinking-mode.d.ts +35 -0
- package/dist/shared/thinking-mode.js +43 -0
- package/dist/shared/thinking-mode.js.map +1 -0
- package/dist/workflow-engine/loader.js +52 -13
- package/dist/workflow-engine/loader.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.d.ts +2 -1
- package/dist/workflow-engine/tasks/human-input.js +8 -1
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +17 -3
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/human-input-orchestrator.d.ts +4 -0
- package/dist/workflows/human-input-orchestrator.js +13 -0
- package/dist/workflows/human-input-orchestrator.js.map +1 -1
- package/package.json +2 -2
- package/src/activities/call-agent.ts +20 -6
- package/src/activities/execute-cursor/__tests__/error-classifier-billing.test.ts +67 -0
- package/src/activities/execute-cursor/__tests__/service-tier.test.ts +67 -7
- package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +33 -1
- package/src/activities/execute-cursor/error-classifier.ts +42 -1
- package/src/activities/execute-cursor/index.ts +15 -6
- package/src/activities/execute-cursor/service-tier.ts +94 -63
- package/src/activities/execute-cursor/usage-accumulator.ts +11 -1
- package/src/shared/caller-identity.ts +10 -7
- package/src/shared/thinking-mode.ts +52 -0
- package/src/workflow-engine/__tests__/loader.test.ts +47 -3
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +39 -0
- package/src/workflow-engine/loader.ts +63 -17
- package/src/workflow-engine/tasks/human-input.ts +8 -1
- package/src/workflow-engine/types.ts +18 -3
- 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
|
-
|
|
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
|
-
|
|
1568
|
-
|
|
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([
|
|
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,
|
|
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
|
-
):
|
|
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
|
|
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
|
|
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,
|
|
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:
|
|
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?:
|
|
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 {
|
|
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:
|
|
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(
|