@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
@@ -1,11 +1,12 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
2
+ import { ServiceTier, ThinkingMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
3
3
 
4
4
  /**
5
- * Verifies the service-tier → Cursor variant-parameter translation (#357):
6
- * the runner must always send an explicit selection whose price-bearing
7
- * parameters are a deterministic function of the requested tier — never
8
- * the catalog's (account-influenced) default variant.
5
+ * Verifies the variant-attribute → Cursor variant-parameter translation
6
+ * (#357 service tier, #772 thinking mode): the runner must always send an
7
+ * explicit selection whose user-selectable parameters are a deterministic
8
+ * function of the requested attributes — never the catalog's
9
+ * (account-influenced) default variant.
9
10
  *
10
11
  * Catalog fixtures mirror the real shapes observed 2026-08-06: composer
11
12
  * has a `fast` bool (default fast=true), haiku has a `thinking` bool
@@ -23,6 +24,7 @@ import {
23
24
  resetCatalogCacheForTests,
24
25
  } from "../service-tier.js";
25
26
  import { resolveEffectiveServiceTier } from "../../../shared/service-tier.js";
27
+ import { resolveEffectiveThinkingMode } from "../../../shared/thinking-mode.js";
26
28
 
27
29
  const CATALOG = [
28
30
  {
@@ -66,8 +68,12 @@ const CATALOG = [
66
68
  },
67
69
  ];
68
70
 
69
- function opts(modelId: string, tier: ServiceTier.STANDARD | ServiceTier.FAST) {
70
- return { apiKey: "key-1", modelId, tier, executionId: "aex_test" };
71
+ function opts(
72
+ modelId: string,
73
+ tier: ServiceTier.STANDARD | ServiceTier.FAST,
74
+ thinking: ThinkingMode.DISABLED | ThinkingMode.ENABLED = ThinkingMode.DISABLED,
75
+ ) {
76
+ return { apiKey: "key-1", modelId, tier, thinking, executionId: "aex_test" };
71
77
  }
72
78
 
73
79
  beforeEach(() => {
@@ -88,6 +94,18 @@ describe("resolveEffectiveServiceTier", () => {
88
94
  });
89
95
  });
90
96
 
97
+ describe("resolveEffectiveThinkingMode", () => {
98
+ it("resolves UNSPECIFIED to DISABLED — never the account default", () => {
99
+ expect(resolveEffectiveThinkingMode(ThinkingMode.UNSPECIFIED)).toBe(ThinkingMode.DISABLED);
100
+ expect(resolveEffectiveThinkingMode(undefined)).toBe(ThinkingMode.DISABLED);
101
+ });
102
+
103
+ it("preserves explicit DISABLED and ENABLED", () => {
104
+ expect(resolveEffectiveThinkingMode(ThinkingMode.DISABLED)).toBe(ThinkingMode.DISABLED);
105
+ expect(resolveEffectiveThinkingMode(ThinkingMode.ENABLED)).toBe(ThinkingMode.ENABLED);
106
+ });
107
+ });
108
+
91
109
  describe("resolveServiceTierParams", () => {
92
110
  it("STANDARD pins fast=false on a fast-capable model", async () => {
93
111
  const params = await resolveServiceTierParams(opts("composer-2.5", ServiceTier.STANDARD));
@@ -123,6 +141,48 @@ describe("resolveServiceTierParams", () => {
123
141
  ]);
124
142
  });
125
143
 
144
+ it("ENABLED pins thinking=true on a thinking-capable model (#772)", async () => {
145
+ const params = await resolveServiceTierParams(
146
+ opts("claude-haiku-4-5", ServiceTier.STANDARD, ThinkingMode.ENABLED),
147
+ );
148
+ expect(params).toEqual([{ id: "thinking", value: "true" }]);
149
+ });
150
+
151
+ it("ENABLED on a model with no thinking parameter fails loudly, never a silent base variant", async () => {
152
+ await expect(
153
+ resolveServiceTierParams(opts("composer-2.5", ServiceTier.STANDARD, ThinkingMode.ENABLED)),
154
+ ).rejects.toThrow(/no "thinking" parameter/);
155
+ });
156
+
157
+ it("thinking combines freely with the fast tier — both pinned true, sorted", async () => {
158
+ const params = await resolveServiceTierParams(
159
+ opts("claude-opus-4-8", ServiceTier.FAST, ThinkingMode.ENABLED),
160
+ );
161
+ expect(params).toEqual([
162
+ { id: "fast", value: "true" },
163
+ { id: "thinking", value: "true" },
164
+ ]);
165
+ });
166
+
167
+ it("Auto + ENABLED is a loud failure (no variant dimensions to pin)", async () => {
168
+ await expect(
169
+ resolveServiceTierParams(opts("default", ServiceTier.STANDARD, ThinkingMode.ENABLED)),
170
+ ).rejects.toThrow(/requires a pinned model/);
171
+ });
172
+
173
+ it("unknown model + ENABLED fails loudly, never degrades", async () => {
174
+ await expect(
175
+ resolveServiceTierParams(opts("not-a-model", ServiceTier.STANDARD, ThinkingMode.ENABLED)),
176
+ ).rejects.toThrow(/does not list that model/);
177
+ });
178
+
179
+ it("catalog fetch failure + ENABLED fails loudly, never degrades", async () => {
180
+ listMock.mockRejectedValue(new Error("proxy down"));
181
+ await expect(
182
+ resolveServiceTierParams(opts("claude-haiku-4-5", ServiceTier.STANDARD, ThinkingMode.ENABLED)),
183
+ ).rejects.toThrow(/catalog fetch failed/);
184
+ });
185
+
126
186
  it("resolves models referenced by alias", async () => {
127
187
  const params = await resolveServiceTierParams(opts("composer", ServiceTier.STANDARD));
128
188
  expect(params).toEqual([{ id: "fast", value: "false" }]);
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeAll } from "vitest";
2
- import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
2
+ import { ServiceTier, ThinkingMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
3
3
 
4
4
  import { UsageAccumulator } from "../usage-accumulator.js";
5
5
 
@@ -129,4 +129,36 @@ describe("UsageAccumulator", () => {
129
129
  expect(unspecified.snapshot().estimatedCostUsd)
130
130
  .toBe(standard.snapshot().estimatedCostUsd);
131
131
  });
132
+
133
+ it("records the requested thinking mode into the snapshot (#772 audit trail)", () => {
134
+ const acc = new UsageAccumulator(
135
+ "claude-haiku-4-5",
136
+ ServiceTier.STANDARD,
137
+ [{ id: "thinking", value: "true" }],
138
+ ThinkingMode.ENABLED,
139
+ );
140
+ acc.addTurn({ inputTokens: 10, outputTokens: 5 });
141
+ const snap = acc.snapshot();
142
+ expect(snap.requestedThinkingMode).toBe(ThinkingMode.ENABLED);
143
+ expect(snap.requestedModelParams).toBe('[{"id":"thinking","value":"true"}]');
144
+ });
145
+
146
+ it("thinking never changes the estimate — per-token price-neutral (#772)", () => {
147
+ // Ledger-verified 2026-08-15: thinking wire ids bill exactly base
148
+ // per-token rates; the extra cost of thinking is more output tokens,
149
+ // which the accumulator already counts as they arrive.
150
+ const turn = { inputTokens: 1_000_000, outputTokens: 1_000_000 };
151
+
152
+ const disabled = new UsageAccumulator(
153
+ "composer-2.5", ServiceTier.STANDARD, [], ThinkingMode.DISABLED,
154
+ );
155
+ disabled.addTurn(turn);
156
+ const enabled = new UsageAccumulator(
157
+ "composer-2.5", ServiceTier.STANDARD, [], ThinkingMode.ENABLED,
158
+ );
159
+ enabled.addTurn(turn);
160
+
161
+ expect(enabled.snapshot().estimatedCostUsd)
162
+ .toBe(disabled.snapshot().estimatedCostUsd);
163
+ });
132
164
  });
@@ -21,6 +21,7 @@
21
21
  */
22
22
 
23
23
  import type { CapturedRejection } from "./rejection-capture.js";
24
+ import { PLATFORM_CAPACITY_SENTINEL } from "../../shared/model-error.js";
24
25
 
25
26
  export type ErrorCategory =
26
27
  | "auth"
@@ -212,6 +213,15 @@ interface SynthesizeErrorOpts {
212
213
  durationMs?: number;
213
214
  /** Number of messages received from the stream (0 = no response at all). */
214
215
  messageCount?: number;
216
+ /**
217
+ * True when the execution key is platform-managed (the run rides the
218
+ * Stigmer proxy). Enables the D4 attribution of the platform provider
219
+ * error contract (see shared/model-error.ts): billing errors on a
220
+ * platform key must never tell the customer to fix an account they do
221
+ * not own. BYO-key runs leave this false — there the raw Cursor message
222
+ * IS the actionable one (it is the user's own account).
223
+ */
224
+ proxyMode?: boolean;
215
225
  }
216
226
 
217
227
  /**
@@ -244,7 +254,38 @@ export function synthesizeError(opts: SynthesizeErrorOpts): ClassifiedError {
244
254
  return { ...classified, category: "agent-stale", retryable: true };
245
255
  }
246
256
 
247
- return classified;
257
+ return attributePlatformBilling(classified, opts.proxyMode === true);
258
+ }
259
+
260
+ /**
261
+ * D4 attribution (platform provider error contract, Cursor surface): a
262
+ * billing error on a platform-managed key is the PLATFORM's fault — the
263
+ * customer's org credits are fine, and Cursor's raw prose ("reach out to
264
+ * an admin to enable on-demand usage") points at a Cursor dashboard they
265
+ * do not own. Reword with platform attribution, quoting the original so
266
+ * Cursor's limit-reset date survives.
267
+ *
268
+ * <p>Two cases pass through untouched: messages already carrying the
269
+ * sentinel (the proxy's end-stream rewrite landed — this is the runner-side
270
+ * fallback for the message-bearing in-stream error path the proxy relays
271
+ * verbatim), and BYO-key runs (the raw message is about the user's own
272
+ * account and must never be hidden).
273
+ */
274
+ function attributePlatformBilling(
275
+ classified: ClassifiedError,
276
+ proxyMode: boolean,
277
+ ): ClassifiedError {
278
+ if (classified.category !== "billing" || !proxyMode) return classified;
279
+ if (classified.message.includes(PLATFORM_CAPACITY_SENTINEL)) return classified;
280
+ return {
281
+ ...classified,
282
+ message:
283
+ `The Stigmer platform's Cursor capacity is temporarily exhausted. ` +
284
+ `This is a platform-side issue - your organization's credits were not ` +
285
+ `charged for this call. Ask your platform operator to restock Cursor ` +
286
+ `execution keys. Provider message: "${classified.message}" ` +
287
+ `[code: ${PLATFORM_CAPACITY_SENTINEL}]`,
288
+ };
248
289
  }
249
290
 
250
291
  /**
@@ -138,6 +138,7 @@ import { setInterceptorExecutionId, runWithExecutionContext } from "./fetch-inte
138
138
  import { closeProxySessions } from "./http2-interceptor.js";
139
139
  import { resolveModelId, ensureLoaded as ensurePricingLoaded } from "./model-pricing.js";
140
140
  import { resolveEffectiveServiceTier } from "../../shared/service-tier.js";
141
+ import { resolveEffectiveThinkingMode } from "../../shared/thinking-mode.js";
141
142
  import { resolveServiceTierParams } from "./service-tier.js";
142
143
  import { UsageAccumulator } from "./usage-accumulator.js";
143
144
  import { StreamingUsageSummarySchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
@@ -960,9 +961,10 @@ async function executeCursorInner(
960
961
  await ensurePricingLoaded();
961
962
  setupTiming.mark("load_pricing");
962
963
 
963
- // Phase 6: Validate model selection and resolve the service tier.
964
- // UNSPECIFIED → STANDARD resolves here and nowhere else (#357): every
965
- // upstream layer preserves the caller's raw enum value.
964
+ // Phase 6: Validate model selection and resolve the variant attributes.
965
+ // UNSPECIFIED → STANDARD (#357) and UNSPECIFIED DISABLED (#772)
966
+ // resolve here and nowhere else: every upstream layer preserves the
967
+ // caller's raw enum values.
966
968
  const requestedModel = spec.executionConfig?.modelName || "default";
967
969
  const validatedModel = resolveModelId(requestedModel);
968
970
  if (validatedModel !== requestedModel) {
@@ -971,6 +973,7 @@ async function executeCursorInner(
971
973
  );
972
974
  }
973
975
  const requestedServiceTier = resolveEffectiveServiceTier(spec.executionConfig?.serviceTier);
976
+ const requestedThinkingMode = resolveEffectiveThinkingMode(spec.executionConfig?.thinkingMode);
974
977
 
975
978
  heartbeat();
976
979
 
@@ -1005,13 +1008,15 @@ async function executeCursorInner(
1005
1008
  );
1006
1009
  }
1007
1010
 
1008
- // Translate the tier into the explicit variant params sent with every
1009
- // create/resume. Never a bare { id }: the catalog's default variant is
1010
- // account-influenced and picks the price (#357).
1011
+ // Translate the tier + thinking mode into the explicit variant params
1012
+ // sent with every create/resume. Never a bare { id }: the catalog's
1013
+ // default variant is account-influenced and picks the served variant
1014
+ // (#357 fast pricing, #772 thinking).
1011
1015
  const modelParams = await resolveServiceTierParams({
1012
1016
  apiKey: effectiveApiKey,
1013
1017
  modelId: validatedModel,
1014
1018
  tier: requestedServiceTier,
1019
+ thinking: requestedThinkingMode,
1015
1020
  executionId,
1016
1021
  });
1017
1022
 
@@ -1228,6 +1233,7 @@ async function executeCursorInner(
1228
1233
  validatedModel,
1229
1234
  requestedServiceTier,
1230
1235
  modelParams,
1236
+ requestedThinkingMode,
1231
1237
  );
1232
1238
 
1233
1239
  // Phase 10c: Start OTel turn span. Coarse-grained — spans the whole turn
@@ -1819,6 +1825,7 @@ async function executeCursorInner(
1819
1825
  fallbackContext: { model: validatedModel, mode: agentMode, agentId: resolution.agentId },
1820
1826
  durationMs: (result as unknown as Record<string, unknown>).durationMs as number | undefined,
1821
1827
  messageCount: status.messages.length,
1828
+ proxyMode: !!config.proxyEndpoint,
1822
1829
  });
1823
1830
 
1824
1831
  console.error(
@@ -1958,6 +1965,7 @@ async function executeCursorInner(
1958
1965
  conversationErrorText: retryConversationErrorText,
1959
1966
  isResumedHandle: false,
1960
1967
  fallbackContext: { model: validatedModel, mode: agentMode, agentId: freshAgent.agentId },
1968
+ proxyMode: !!config.proxyEndpoint,
1961
1969
  });
1962
1970
 
1963
1971
  status.phase = ExecutionPhase.EXECUTION_FAILED;
@@ -2259,6 +2267,7 @@ async function executeCursorInner(
2259
2267
  capturedRejection: getCapturedRejection(executionId),
2260
2268
  isResumedHandle: false,
2261
2269
  fallbackContext: errorContext,
2270
+ proxyMode: !!config.proxyEndpoint,
2262
2271
  });
2263
2272
  clearCapturedRejection(executionId);
2264
2273
  status.phase = ExecutionPhase.EXECUTION_FAILED;
@@ -1,18 +1,23 @@
1
1
  /**
2
- * Service-tier → Cursor variant-parameter translation (stigmer/stigmer#357).
2
+ * Variant-attribute → Cursor variant-parameter translation
3
+ * (stigmer/stigmer#357 service tier, #772 thinking mode).
3
4
  *
4
5
  * The platform contract: an execution's model selection is ALWAYS explicit.
5
- * A bare `{ id }` lets the Cursor catalog's default variant decide the price
6
- * (observed 2026-08-06: composer-2.5 defaults to fast=true at ~4x base
7
- * rates, claude-haiku-4-5 to thinking=true), and that default follows an
8
- * out-of-band account setting. This module pins every price-bearing variant
9
- * parameter the model declares, so the billed variant is a deterministic
10
- * function of ExecutionConfig.service_tier:
6
+ * A bare `{ id }` lets the Cursor catalog's default variant decide the
7
+ * variant (observed 2026-08-06: composer-2.5 defaults to fast=true at ~4x
8
+ * base rates, claude-haiku-4-5 to thinking=true), and that default follows
9
+ * an out-of-band account setting. This module pins every user-selectable
10
+ * variant parameter the model declares, so the served variant is a
11
+ * deterministic function of the execution config:
11
12
  *
12
- * - STANDARD: every price-bearing boolean pinned to its base value
13
- * (fast=false, thinking=false where the parameter exists).
14
- * - FAST: fast=true, thinking still pinned false.
15
- * - Price-neutral parameters (e.g. effort) are deliberately NOT pinned —
13
+ * - fast: pinned from ExecutionConfig.service_tier (FAST true).
14
+ * Price-bearing — the fast variant bills at pricingVariants.fast rates.
15
+ * - thinking: pinned from ExecutionConfig.thinking_mode (ENABLED → true).
16
+ * Per-token price-neutral (ledger-verified 2026-08-15: thinking wire ids
17
+ * bill exactly base rates; thinking+fast bills exactly the fast rate) —
18
+ * pinned anyway because the served variant must never follow the account
19
+ * default, and ENABLED turns consume more output (reasoning) tokens.
20
+ * - Parameters that are neither (e.g. effort) are deliberately NOT pinned —
16
21
  * they follow the catalog default and do not change the bill.
17
22
  *
18
23
  * Parameter bundles come from Cursor.models.list() (worker-cached): the
@@ -20,17 +25,19 @@
20
25
  * rides the same proxy fetch-interceptor as every other SDK call, so it
21
26
  * works identically in proxy and direct modes.
22
27
  *
23
- * The harness-neutral halves — the tier enum semantics, and the single
24
- * UNSPECIFIED→STANDARD resolution point — live in
25
- * `shared/service-tier.ts` since #361 extended tiers to the native
26
- * harness; this module keeps only the Cursor-catalog translation.
28
+ * The harness-neutral halves — the enum semantics and the single
29
+ * UNSPECIFIED→default resolution points — live in `shared/service-tier.ts`
30
+ * (since #361 extended tiers to the native harness) and
31
+ * `shared/thinking-mode.ts`; this module keeps only the Cursor-catalog
32
+ * translation.
27
33
  */
28
34
 
29
35
  import { Cursor } from "@cursor/sdk";
30
36
  import type { ModelListItem, ModelParameterValue } from "@cursor/sdk";
31
- import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
37
+ import { ServiceTier, ThinkingMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
32
38
 
33
39
  import { serviceTierLabel, type EffectiveServiceTier } from "../../shared/service-tier.js";
40
+ import { thinkingModeLabel, type EffectiveThinkingMode } from "../../shared/thinking-mode.js";
34
41
 
35
42
  /**
36
43
  * Catalog ids that mean "Cursor picks the model" (Auto). Auto's single
@@ -42,9 +49,11 @@ import { serviceTierLabel, type EffectiveServiceTier } from "../../shared/servic
42
49
  const AUTO_MODEL_IDS = new Set(["default", "auto"]);
43
50
 
44
51
  /**
45
- * Variant parameter ids that change the per-token price. Pinning exactly
46
- * these keeps the bill deterministic while leaving latency/effort knobs on
47
- * their catalog defaults. Sourced from the Cursor catalog survey
52
+ * The user-selectable variant parameter ids. Pinning exactly these keeps
53
+ * the served variant deterministic while leaving effort knobs on their
54
+ * catalog defaults. `fast` changes the per-token price; `thinking` is
55
+ * price-neutral but changes token consumption and must never follow the
56
+ * account default. Sourced from the Cursor catalog survey
48
57
  * (stigmer-cloud _projects/2026-08/20260806.04.model-service-tier).
49
58
  */
50
59
  const FAST_PARAM_ID = "fast";
@@ -108,46 +117,55 @@ export interface ResolveServiceTierParamsOptions {
108
117
  /** Validated model id the execution runs on (may be "default" for Auto). */
109
118
  readonly modelId: string;
110
119
  readonly tier: EffectiveServiceTier;
120
+ readonly thinking: EffectiveThinkingMode;
111
121
  /** For log correlation only. */
112
122
  readonly executionId: string;
113
123
  }
114
124
 
115
125
  /**
116
- * Translate the effective tier into the explicit variant parameters to send
117
- * with every Agent.create/resume for this execution.
126
+ * Translate the effective tier + thinking mode into the explicit variant
127
+ * parameters to send with every Agent.create/resume for this execution.
118
128
  *
119
- * Fail-closed posture: FAST with no pinnable fast dimension is an error,
120
- * never a silent downgrade — create-time validation makes this unreachable
121
- * unless the registry and the provider catalog have drifted, and that drift
122
- * must be heard about, not absorbed.
129
+ * Fail-closed posture: an ACTIVE selection (FAST tier, ENABLED thinking)
130
+ * with no pinnable dimension is an error, never a silent downgrade —
131
+ * create-time validation makes this unreachable unless the registry and the
132
+ * provider catalog have drifted, and that drift must be heard about, not
133
+ * absorbed.
123
134
  *
124
- * STANDARD degrades to empty params on catalog failures rather than failing
125
- * the execution — but be clear about what that costs: an unpinned selection
126
- * falls to the catalog default variant, which for several models IS the
127
- * fast/thinking variant at multiples of base rates (the incident this module
128
- * exists to prevent). Failing every standard execution whenever the catalog
129
- * endpoint blips would be the worse trade; the WARN below plus billing's
130
- * requested-vs-billed mismatch alarm (which catches exactly this window)
131
- * are the compensating controls.
135
+ * The base selection (STANDARD + DISABLED) degrades to empty params on
136
+ * catalog failures rather than failing the execution — but be clear about
137
+ * what that costs: an unpinned selection falls to the catalog default
138
+ * variant, which for several models IS the fast/thinking variant (the
139
+ * incident this module exists to prevent). Failing every base execution
140
+ * whenever the catalog endpoint blips would be the worse trade; the WARN
141
+ * below plus billing's requested-vs-billed mismatch alarms (which catch
142
+ * exactly this window) are the compensating controls.
132
143
  */
133
144
  export async function resolveServiceTierParams(
134
145
  options: ResolveServiceTierParamsOptions,
135
146
  ): Promise<ModelParameterValue[]> {
136
- const { apiKey, modelId, tier, executionId } = options;
147
+ const { apiKey, modelId, tier, thinking, executionId } = options;
137
148
  const tierName = serviceTierLabel(tier);
149
+ const thinkingName = thinkingModeLabel(thinking);
150
+ // Selections that actively deviate from the base variant must fail loudly
151
+ // when they cannot be pinned; the base selection may degrade with a WARN.
152
+ const active: string[] = [];
153
+ if (tier === ServiceTier.FAST) active.push("service_tier=fast");
154
+ if (thinking === ThinkingMode.ENABLED) active.push("thinking=enabled");
138
155
 
139
156
  if (AUTO_MODEL_IDS.has(modelId)) {
140
- if (tier === ServiceTier.FAST) {
157
+ if (active.length > 0) {
141
158
  throw new Error(
142
- `service_tier=fast requires a pinned model — Auto ("${modelId}") has no ` +
143
- `tier dimension. Execution ${executionId} should have been refused at ` +
144
- `create time; the model registry and provider catalog may have drifted.`,
159
+ `${active.join(" + ")} requires a pinned model — Auto ("${modelId}") has ` +
160
+ `no variant dimensions. Execution ${executionId} should have been ` +
161
+ `refused at create time; the model registry and provider catalog may ` +
162
+ `have drifted.`,
145
163
  );
146
164
  }
147
165
  console.log(
148
- `ServiceTier: execution=${executionId} model=${modelId} tier=${tierName} ` +
149
- `Auto has no variant parameters; Cursor picks the model and variant ` +
150
- `(documented v1 limitation).`,
166
+ `VariantParams: execution=${executionId} model=${modelId} tier=${tierName} ` +
167
+ `thinking=${thinkingName} — Auto has no variant parameters; Cursor picks ` +
168
+ `the model and variant (documented v1 limitation).`,
151
169
  );
152
170
  return [];
153
171
  }
@@ -156,37 +174,38 @@ export async function resolveServiceTierParams(
156
174
  try {
157
175
  models = await listCatalogModels(apiKey);
158
176
  } catch (err) {
159
- if (tier === ServiceTier.FAST) {
177
+ if (active.length > 0) {
160
178
  throw new Error(
161
- `service_tier=fast for execution ${executionId} needs the Cursor model ` +
162
- `catalog to resolve variant params for "${modelId}", and the catalog ` +
163
- `fetch failed: ${err instanceof Error ? err.message : String(err)}`,
179
+ `${active.join(" + ")} for execution ${executionId} needs the Cursor ` +
180
+ `model catalog to resolve variant params for "${modelId}", and the ` +
181
+ `catalog fetch failed: ${err instanceof Error ? err.message : String(err)}`,
164
182
  );
165
183
  }
166
184
  console.warn(
167
- `ServiceTier UNPINNED: execution=${executionId} model=${modelId} tier=${tierName} ` +
168
- `catalog fetch failed (${err instanceof Error ? err.message : err}); ` +
169
- `sending no variant params, so the catalog DEFAULT variant decides the ` +
170
- `price for this execution (fast/thinking on several models the ` +
171
- `expensive direction). Billing's requested-vs-billed mismatch alarm ` +
172
- `covers this window.`,
185
+ `VariantParams UNPINNED: execution=${executionId} model=${modelId} tier=${tierName} ` +
186
+ `thinking=${thinkingName} — catalog fetch failed ` +
187
+ `(${err instanceof Error ? err.message : err}); sending no variant params, ` +
188
+ `so the catalog DEFAULT variant decides the served variant for this ` +
189
+ `execution (fast/thinking on several models — the expensive direction). ` +
190
+ `Billing's requested-vs-billed mismatch alarms cover this window.`,
173
191
  );
174
192
  return [];
175
193
  }
176
194
 
177
195
  const model = findCatalogModel(models, modelId);
178
196
  if (!model) {
179
- if (tier === ServiceTier.FAST) {
197
+ if (active.length > 0) {
180
198
  throw new Error(
181
- `service_tier=fast requested for "${modelId}" (execution ${executionId}) ` +
182
- `but the Cursor catalog does not list that model — cannot pin a fast ` +
183
- `variant. The model registry and provider catalog have drifted.`,
199
+ `${active.join(" + ")} requested for "${modelId}" (execution ${executionId}) ` +
200
+ `but the Cursor catalog does not list that model — cannot pin its ` +
201
+ `variant parameters. The model registry and provider catalog have drifted.`,
184
202
  );
185
203
  }
186
204
  console.warn(
187
- `ServiceTier UNPINNED: execution=${executionId} model=${modelId} tier=${tierName} ` +
188
- `model not in the Cursor catalog; sending no variant params, so the ` +
189
- `catalog DEFAULT variant decides the price for this execution.`,
205
+ `VariantParams UNPINNED: execution=${executionId} model=${modelId} tier=${tierName} ` +
206
+ `thinking=${thinkingName} — model not in the Cursor catalog; sending no ` +
207
+ `variant params, so the catalog DEFAULT variant decides the served ` +
208
+ `variant for this execution.`,
190
209
  );
191
210
  return [];
192
211
  }
@@ -196,10 +215,13 @@ export async function resolveServiceTierParams(
196
215
  if (def.id === FAST_PARAM_ID) {
197
216
  params.push({ id: FAST_PARAM_ID, value: tier === ServiceTier.FAST ? "true" : "false" });
198
217
  } else if (def.id === THINKING_PARAM_ID) {
199
- params.push({ id: THINKING_PARAM_ID, value: "false" });
218
+ params.push({
219
+ id: THINKING_PARAM_ID,
220
+ value: thinking === ThinkingMode.ENABLED ? "true" : "false",
221
+ });
200
222
  }
201
- // Any other parameter (e.g. effort) is price-neutral: left to the
202
- // catalog default variant on purpose.
223
+ // Any other parameter (e.g. effort) is price-neutral and not
224
+ // user-selectable: left to the catalog default variant on purpose.
203
225
  }
204
226
 
205
227
  if (tier === ServiceTier.FAST && !params.some((p) => p.id === FAST_PARAM_ID)) {
@@ -211,10 +233,19 @@ export async function resolveServiceTierParams(
211
233
  );
212
234
  }
213
235
 
236
+ if (thinking === ThinkingMode.ENABLED && !params.some((p) => p.id === THINKING_PARAM_ID)) {
237
+ throw new Error(
238
+ `thinking=enabled requested for "${modelId}" (execution ${executionId}) ` +
239
+ `but the Cursor catalog declares no "thinking" parameter for it. The ` +
240
+ `model registry claims a thinking capability the provider no longer ` +
241
+ `offers — refusing rather than silently serving the base variant.`,
242
+ );
243
+ }
244
+
214
245
  params.sort((a, b) => a.id.localeCompare(b.id));
215
246
  console.log(
216
- `ServiceTier: execution=${executionId} model=${modelId} tier=${tierName} ` +
217
- `params=${JSON.stringify(params)}`,
247
+ `VariantParams: execution=${executionId} model=${modelId} tier=${tierName} ` +
248
+ `thinking=${thinkingName} params=${JSON.stringify(params)}`,
218
249
  );
219
250
  return params;
220
251
  }
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import type { ModelParameterValue } from "@cursor/sdk";
17
- import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
17
+ import { ServiceTier, ThinkingMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
18
18
 
19
19
  import { getCursorModelPricingForVariant, computeTurnCost } from "./model-pricing.js";
20
20
 
@@ -39,6 +39,8 @@ export interface UsageSnapshot {
39
39
  readonly requestedServiceTier: ServiceTier;
40
40
  /** JSON-encoded ModelSelection.params the runner sent; "" when none. */
41
41
  readonly requestedModelParams: string;
42
+ /** Thinking mode the runner requested — always explicit post-translation (#772). */
43
+ readonly requestedThinkingMode: ThinkingMode;
42
44
  }
43
45
 
44
46
  const EMPTY_SNAPSHOT: UsageSnapshot = {
@@ -53,6 +55,7 @@ const EMPTY_SNAPSHOT: UsageSnapshot = {
53
55
  observedAt: "",
54
56
  requestedServiceTier: ServiceTier.UNSPECIFIED,
55
57
  requestedModelParams: "",
58
+ requestedThinkingMode: ThinkingMode.UNSPECIFIED,
56
59
  };
57
60
 
58
61
  export interface TurnRecord {
@@ -85,6 +88,12 @@ export class UsageAccumulator {
85
88
  */
86
89
  private readonly requestedServiceTier: ServiceTier = ServiceTier.UNSPECIFIED,
87
90
  requestedModelParams: readonly ModelParameterValue[] = [],
91
+ /**
92
+ * The explicit thinking mode the runner requested. Price-neutral
93
+ * (thinking bills at base rates, #772), so it never enters the
94
+ * estimate — recorded purely as the audit trail twin of the tier.
95
+ */
96
+ private readonly requestedThinkingMode: ThinkingMode = ThinkingMode.UNSPECIFIED,
88
97
  ) {
89
98
  this.requestedModelParams =
90
99
  requestedModelParams.length > 0 ? JSON.stringify(requestedModelParams) : "";
@@ -154,6 +163,7 @@ export class UsageAccumulator {
154
163
  observedAt: this.observedAt,
155
164
  requestedServiceTier: this.requestedServiceTier,
156
165
  requestedModelParams: this.requestedModelParams,
166
+ requestedThinkingMode: this.requestedThinkingMode,
157
167
  };
158
168
  }
159
169
  }
@@ -58,15 +58,18 @@ export const ANONYMOUS_KIND = "anonymous";
58
58
 
59
59
  /**
60
60
  * Audit-actor id that backends stamp when NO caller identity exists —
61
- * the OSS server writes it on every create (no local auth), and the
62
- * cloud's AuditActorBuilder falls back to it for caller-less internal
63
- * writes. It names "nobody in particular": unrelated sessions from
64
- * unrelated people all carry it, so presenting it as a caller identity
65
- * would make the one string a grantable value that silently covers ALL
66
- * such traffic in an MCP server's binding sheet. A creator matching this
61
+ * the OSS server writes it on every create unless the deployment
62
+ * configured an operator identity (STIGMER_OPERATOR_EMAIL,
63
+ * stigmer/stigmer#400; a configured install stamps a real actor whose
64
+ * email resolves below like any other), and the cloud's
65
+ * AuditActorBuilder falls back to it for caller-less internal writes.
66
+ * It names "nobody in particular": unrelated sessions from unrelated
67
+ * people all carry it, so presenting it as a caller identity would make
68
+ * the one string a grantable value that silently covers ALL such
69
+ * traffic in an MCP server's binding sheet. A creator matching this
67
70
  * sentinel (and carrying no email) is therefore unresolvable and falls
68
71
  * to anonymous — the deny-by-default the docs guide already promises
69
- * for self-hosted backends.
72
+ * for unconfigured self-hosted backends.
70
73
  */
71
74
  export const SYSTEM_CREATOR_SENTINEL = "system";
72
75