@stigmer/runner 3.38.1 → 3.38.3

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 (27) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +12 -10
  3. package/dist/activities/execute-deep-agent/subagent-transformer.js +12 -52
  4. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  5. package/dist/activities/execute-deep-agent/turn-setup.d.ts +5 -4
  6. package/dist/activities/execute-deep-agent/turn-setup.js +68 -24
  7. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  8. package/dist/shared/model-client.d.ts +24 -2
  9. package/dist/shared/model-client.js +8 -0
  10. package/dist/shared/model-client.js.map +1 -1
  11. package/dist/shared/model-registry.d.ts +42 -1
  12. package/dist/shared/model-registry.js +38 -1
  13. package/dist/shared/model-registry.js.map +1 -1
  14. package/dist/shared/thinking-mode.d.ts +51 -4
  15. package/dist/shared/thinking-mode.js +66 -4
  16. package/dist/shared/thinking-mode.js.map +1 -1
  17. package/package.json +6 -5
  18. package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +63 -12
  19. package/src/activities/execute-deep-agent/subagent-transformer.ts +24 -63
  20. package/src/activities/execute-deep-agent/turn-setup.ts +70 -19
  21. package/src/shared/__tests__/model-client-sampling.test.ts +56 -2
  22. package/src/shared/__tests__/model-client.test.ts +72 -0
  23. package/src/shared/__tests__/model-registry.test.ts +96 -1
  24. package/src/shared/__tests__/thinking-mode.test.ts +69 -0
  25. package/src/shared/model-client.ts +35 -2
  26. package/src/shared/model-registry.ts +83 -1
  27. package/src/shared/thinking-mode.ts +77 -4
@@ -7,7 +7,8 @@
7
7
  * model-pricing-data.ts) and uses `costTier` + `harness` fields to
8
8
  * dynamically resolve economy-tier models for extraction/summarization,
9
9
  * plus `capabilities` catalog metadata for per-model capability lookups
10
- * (getModelVisionCapability).
10
+ * (getModelVisionCapability) and the request facts a native row states
11
+ * (getNativeRequestProfile).
11
12
  */
12
13
 
13
14
  import {
@@ -38,6 +39,46 @@ interface RegistryModel {
38
39
  * Consumers gate only on the explicit `false` (see attachment-vision.ts).
39
40
  */
40
41
  visionCapability?: boolean;
42
+ /**
43
+ * The model's output ceiling in tokens, as the row states it. `undefined`
44
+ * when the row carries none or a value that is not a positive integer;
45
+ * the caller then keeps its own default.
46
+ */
47
+ maxOutputTokens?: number;
48
+ /** The thinking form the row declares; see {@link NativeThinkingProfile}. */
49
+ thinking: NativeThinkingProfile;
50
+ }
51
+
52
+ /**
53
+ * How a model reasons, as its registry row declares it (the proto's
54
+ * `ModelCapabilities`):
55
+ *
56
+ * - `shape` is `"adaptive"` when the row declares `adaptiveThinking` (depth
57
+ * adapts; Anthropic `{type: "adaptive"}`), `"budget"` when it declares only
58
+ * `thinking` (a fixed token budget; `{type: "enabled", budget_tokens}`),
59
+ * and `"none"` otherwise. Adaptive wins where a row declares both: it is
60
+ * the current form, and the budget form is deprecated on models that take
61
+ * both.
62
+ * - `required` is the row's `thinkingRequired`, tri-state: `true` for a
63
+ * model that always thinks and refuses to be told not to, `false` for one
64
+ * that accepts an explicit `{type: "disabled"}`, `undefined` for a row that
65
+ * was never assessed. The absence matters: a runner that read it as
66
+ * `false` would send `disabled` to a model that refuses it.
67
+ */
68
+ export interface NativeThinkingProfile {
69
+ readonly shape: "budget" | "adaptive" | "none";
70
+ readonly required: boolean | undefined;
71
+ }
72
+
73
+ /**
74
+ * The request facts a native-harness model's registry row states, read by
75
+ * the native execution turn when it builds its model client. One home for
76
+ * what the provider needs to be told about a model (the runner never names
77
+ * models in code); a field is `undefined` when the row does not state it.
78
+ */
79
+ export interface NativeRequestProfile {
80
+ readonly maxOutputTokens: number | undefined;
81
+ readonly thinking: NativeThinkingProfile;
41
82
  }
42
83
 
43
84
  let cache: { models: readonly RegistryModel[]; expiresAt: number } | null = null;
@@ -58,9 +99,28 @@ function parseRegistry(json: unknown): RegistryModel[] {
58
99
  harness: (m.harness as string) ?? "native",
59
100
  featured: !!m.featured,
60
101
  visionCapability: parseVisionCapability(m.capabilities),
102
+ maxOutputTokens: parsePositiveInteger(m.maxOutputTokens),
103
+ thinking: parseThinkingProfile(m.capabilities),
61
104
  }));
62
105
  }
63
106
 
107
+ function parsePositiveInteger(value: unknown): number | undefined {
108
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
109
+ }
110
+
111
+ /**
112
+ * The row's thinking form. A missing or malformed `capabilities` block
113
+ * declares no shape and leaves `required` unknown, the same tri-state
114
+ * posture as {@link parseVisionCapability}.
115
+ */
116
+ function parseThinkingProfile(capabilities: unknown): NativeThinkingProfile {
117
+ if (!capabilities || typeof capabilities !== "object") return { shape: "none", required: undefined };
118
+ const flags = capabilities as Record<string, unknown>;
119
+ const shape = flags.adaptiveThinking === true ? "adaptive" : flags.thinking === true ? "budget" : "none";
120
+ const required = typeof flags.thinkingRequired === "boolean" ? flags.thinkingRequired : undefined;
121
+ return { shape, required };
122
+ }
123
+
64
124
  /**
65
125
  * Extract `capabilities.vision` preserving the tri-state: a missing or
66
126
  * malformed `capabilities` block stays `undefined` (never coerced to false).
@@ -264,6 +324,28 @@ export async function getModelVisionCapability(
264
324
  return entry?.visionCapability;
265
325
  }
266
326
 
327
+ /**
328
+ * The native-harness row's request facts for a model, matched by registry
329
+ * `id` or `apiModelId` (the execution config carries the former, the
330
+ * registry default the latter) and filtered to `harness: native`: ids such
331
+ * as `claude-sonnet-5` exist on both harnesses, and only the native row
332
+ * describes the native request. `undefined` when the registry is
333
+ * unreachable, the name is empty or Auto, or no native row matches — the
334
+ * caller then sends what it sent before.
335
+ */
336
+ export async function getNativeRequestProfile(
337
+ modelName: string,
338
+ ): Promise<NativeRequestProfile | undefined> {
339
+ if (!modelName || modelName === "default") return undefined;
340
+
341
+ const registry = await getRegistry();
342
+ const entry = registry.find(
343
+ (m) => m.harness === "native" && (m.id === modelName || m.apiModelId === modelName),
344
+ );
345
+ if (!entry) return undefined;
346
+ return { maxOutputTokens: entry.maxOutputTokens, thinking: entry.thinking };
347
+ }
348
+
267
349
  /** Exposed for testing — resets the in-memory cache. */
268
350
  export function _resetRegistryCache(): void {
269
351
  cache = null;
@@ -16,14 +16,26 @@
16
16
  * rather than pricing-gated, and the estimate/billing paths need no
17
17
  * thinking-specific rates.
18
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.
19
+ * Each harness translates the mode in its own dialect: the Cursor harness
20
+ * as the explicit `thinking` variant parameter
21
+ * (`execute-cursor/service-tier.ts`), the native harness as Anthropic's
22
+ * `thinking` request parameter in the form the model's native registry row
23
+ * declares ({@link toAnthropicThinking}). ENABLED means "the model reasons
24
+ * before it answers", whichever form the row takes.
25
+ *
26
+ * One model family breaks the "always explicit" rule on purpose: a row
27
+ * that states `thinkingRequired` names a model that always thinks and
28
+ * refuses `{type: "disabled"}` with a 400. Create-time validation refuses
29
+ * an explicit DISABLED there, and the runner sends the model's thinking
30
+ * form whatever the mode, so its reasoning streams as text rather than the
31
+ * empty blocks the model's own `display` default produces.
23
32
  */
24
33
 
34
+ import type { ThinkingConfigParam } from "@anthropic-ai/sdk/resources/messages";
25
35
  import { ThinkingMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
26
36
 
37
+ import type { NativeThinkingProfile } from "./model-registry.js";
38
+
27
39
  /**
28
40
  * The effective mode after platform-default resolution: never UNSPECIFIED.
29
41
  */
@@ -50,3 +62,64 @@ export function thinkingModeLabel(mode: ThinkingMode): string {
50
62
  return "unspecified";
51
63
  }
52
64
  }
65
+
66
+ /**
67
+ * The thinking budget sent to a model whose row declares the fixed-budget
68
+ * form. One platform constant, not a user choice (per-execution budgets are
69
+ * a later proto change): 16,000 tokens is room for a real plan while
70
+ * leaving every budget row's output ceiling (64,000 on the smallest) most
71
+ * of its length for the answer. The provider requires at least 1,024 and
72
+ * less than `max_tokens`. No interleaved-thinking beta rides with it: the
73
+ * hosted proxy forwards no `anthropic-beta` header, so these models think
74
+ * at the start of each model call, not between tool calls.
75
+ */
76
+ export const BUDGET_THINKING_TOKENS = 16_000;
77
+
78
+ /**
79
+ * The native request's `thinking` parameter for an execution's effective
80
+ * mode on a model's native row, or `undefined` to send none.
81
+ *
82
+ * - A row that requires thinking gets its thinking form whatever the mode;
83
+ * the model thinks either way, and `display: "summarized"` is what makes
84
+ * its reasoning readable (the newest models default to `"omitted"`).
85
+ * - ENABLED gets the row's form: adaptive with `display: "summarized"`, or
86
+ * the fixed budget. A row that declares no form gets nothing; create-time
87
+ * validation refuses ENABLED there, so this is the fail-safe.
88
+ * - DISABLED gets an explicit `{type: "disabled"}` only where the row
89
+ * declares a thinking form and states `thinkingRequired: false`. Where the
90
+ * flag was never assessed nothing is sent (the request as it was before),
91
+ * because sending `disabled` to a model that requires thinking fails every
92
+ * turn; a row with no thinking form has no parameter to pin.
93
+ * - No row (an unreachable registry, an unknown model) sends nothing.
94
+ */
95
+ export function toAnthropicThinking(
96
+ mode: EffectiveThinkingMode,
97
+ profile: NativeThinkingProfile | undefined,
98
+ ): ThinkingConfigParam | undefined {
99
+ if (profile === undefined) return undefined;
100
+ if (profile.required === true || mode === ThinkingMode.ENABLED) {
101
+ switch (profile.shape) {
102
+ case "adaptive":
103
+ return { type: "adaptive", display: "summarized" };
104
+ case "budget":
105
+ return { type: "enabled", budget_tokens: BUDGET_THINKING_TOKENS };
106
+ case "none":
107
+ return undefined;
108
+ }
109
+ }
110
+ return profile.required === false && profile.shape !== "none" ? { type: "disabled" } : undefined;
111
+ }
112
+
113
+ /**
114
+ * Whether a graph built on this row reasons natively: true exactly when
115
+ * {@link toAnthropicThinking} asks for thinking. Derived from the same
116
+ * mapping, never decided separately, so the `think` tool and the structured
117
+ * output strategy can never disagree with the wire.
118
+ */
119
+ export function graphThinks(
120
+ mode: EffectiveThinkingMode,
121
+ profile: NativeThinkingProfile | undefined,
122
+ ): boolean {
123
+ const thinking = toAnthropicThinking(mode, profile);
124
+ return thinking !== undefined && thinking.type !== "disabled";
125
+ }