@mupt-ai/dari-router 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +89 -60
  2. package/dist/anonymous_actions.js +32 -10
  3. package/dist/anonymous_actions.js.map +1 -1
  4. package/dist/auto_router.d.ts +4 -1
  5. package/dist/auto_router.js +3 -3
  6. package/dist/auto_router.js.map +1 -1
  7. package/dist/cache_behavior.d.ts +8 -6
  8. package/dist/cache_behavior.js +56 -30
  9. package/dist/cache_behavior.js.map +1 -1
  10. package/dist/cache_scope.d.ts +2 -2
  11. package/dist/cache_scope.js +23 -18
  12. package/dist/cache_scope.js.map +1 -1
  13. package/dist/cost.d.ts +3 -2
  14. package/dist/cost.js +32 -23
  15. package/dist/cost.js.map +1 -1
  16. package/dist/dari_policy.d.ts +8 -2
  17. package/dist/dari_policy.js +39 -1
  18. package/dist/dari_policy.js.map +1 -1
  19. package/dist/eval_score_imputation.d.ts +26 -0
  20. package/dist/eval_score_imputation.js +83 -0
  21. package/dist/eval_score_imputation.js.map +1 -0
  22. package/dist/framework_lifecycle.d.ts +1 -1
  23. package/dist/framework_lifecycle.js +4 -1
  24. package/dist/framework_lifecycle.js.map +1 -1
  25. package/dist/framework_models.js +6 -0
  26. package/dist/framework_models.js.map +1 -1
  27. package/dist/framework_router.js +22 -11
  28. package/dist/framework_router.js.map +1 -1
  29. package/dist/framework_types.d.ts +9 -5
  30. package/dist/index.d.ts +9 -35
  31. package/dist/index.js +6 -37
  32. package/dist/index.js.map +1 -1
  33. package/dist/model_ids.js +4 -0
  34. package/dist/model_ids.js.map +1 -1
  35. package/dist/pi_runtime.js +19 -11
  36. package/dist/pi_runtime.js.map +1 -1
  37. package/dist/pi_types.d.ts +2 -2
  38. package/{src/index.ts → dist/policy-engine.d.ts} +2 -14
  39. package/dist/policy-engine.js +25 -0
  40. package/dist/policy-engine.js.map +1 -0
  41. package/dist/prompts.d.ts +1 -1
  42. package/dist/prompts.js +3 -3
  43. package/dist/prompts.js.map +1 -1
  44. package/dist/protocols.d.ts +5 -0
  45. package/dist/protocols.js +6 -0
  46. package/dist/protocols.js.map +1 -0
  47. package/dist/route.d.ts +3 -4
  48. package/dist/route.js +17 -10
  49. package/dist/route.js.map +1 -1
  50. package/dist/selector_input.d.ts +1 -0
  51. package/dist/selector_input.js +66 -78
  52. package/dist/selector_input.js.map +1 -1
  53. package/dist/selector_parse.d.ts +1 -0
  54. package/dist/selector_parse.js +2 -1
  55. package/dist/selector_parse.js.map +1 -1
  56. package/dist/selector_request.d.ts +1 -0
  57. package/dist/selector_request.js +1 -0
  58. package/dist/selector_request.js.map +1 -1
  59. package/dist/types.d.ts +1 -1
  60. package/examples/auto_router.ts +40 -0
  61. package/examples/basic_route.ts +130 -0
  62. package/examples/create_router.ts +114 -0
  63. package/examples/dari_policy.ts +86 -0
  64. package/examples/lease_stores.ts +159 -0
  65. package/examples/managed/README.md +50 -0
  66. package/examples/managed/byok-router/README.md +13 -0
  67. package/examples/managed/byok-router/router.yml +13 -0
  68. package/examples/managed/custom-rules-router/README.md +12 -0
  69. package/examples/managed/custom-rules-router/router.yml +24 -0
  70. package/examples/managed/eval-router/README.md +18 -0
  71. package/examples/managed/eval-router/router.yml +11 -0
  72. package/examples/managed/slm-router/README.md +12 -0
  73. package/examples/managed/slm-router/router.yml +8 -0
  74. package/examples/pi_runtime.ts +55 -0
  75. package/package.json +26 -7
  76. package/src/anonymous_actions.ts +0 -434
  77. package/src/auto_router.ts +0 -95
  78. package/src/cache_behavior.ts +0 -130
  79. package/src/cache_scope.ts +0 -58
  80. package/src/cache_switch_decision.ts +0 -53
  81. package/src/candidate_pruning.ts +0 -257
  82. package/src/candidate_resolution.ts +0 -208
  83. package/src/compatibility.ts +0 -162
  84. package/src/continuation_state.ts +0 -368
  85. package/src/cost.ts +0 -455
  86. package/src/dari_policy.ts +0 -360
  87. package/src/errors.ts +0 -27
  88. package/src/fingerprint.ts +0 -148
  89. package/src/fixed_turn_cost_config.ts +0 -24
  90. package/src/framework_error.ts +0 -38
  91. package/src/framework_lifecycle.ts +0 -74
  92. package/src/framework_models.ts +0 -186
  93. package/src/framework_router.ts +0 -490
  94. package/src/framework_streaming.ts +0 -613
  95. package/src/framework_types.ts +0 -372
  96. package/src/framework_validation.ts +0 -235
  97. package/src/json.ts +0 -43
  98. package/src/model_ids.ts +0 -29
  99. package/src/pi_context.ts +0 -251
  100. package/src/pi_options.ts +0 -295
  101. package/src/pi_output.ts +0 -387
  102. package/src/pi_reasoning_signature.ts +0 -41
  103. package/src/pi_runtime.ts +0 -238
  104. package/src/pi_types.ts +0 -71
  105. package/src/portable_reasoning_state.ts +0 -129
  106. package/src/previous_decision.ts +0 -109
  107. package/src/prompts.ts +0 -69
  108. package/src/protocol_anthropic.ts +0 -571
  109. package/src/protocol_openai_chat.ts +0 -645
  110. package/src/protocol_validation.ts +0 -202
  111. package/src/route.ts +0 -490
  112. package/src/routing_history.ts +0 -157
  113. package/src/selector_input.ts +0 -246
  114. package/src/selector_parse.ts +0 -138
  115. package/src/selector_request.ts +0 -130
  116. package/src/selector_truncation.ts +0 -48
  117. package/src/selector_wire.ts +0 -167
  118. package/src/types.ts +0 -231
package/src/pi_types.ts DELETED
@@ -1,71 +0,0 @@
1
- import type {
2
- Api,
3
- AssistantMessage,
4
- AssistantMessageEvent,
5
- Context,
6
- Model,
7
- SimpleStreamOptions,
8
- } from "@mupt-ai/pi-ai";
9
-
10
- import type { ChatCompletionRequest, ReasoningEffort } from "./types.js";
11
- import type {
12
- RouterCandidate,
13
- RouterExecutorInput,
14
- RouterExecutorResult,
15
- RouterModel,
16
- RouterRequest,
17
- } from "./framework_types.js";
18
-
19
- export type PiModel = Model<Api>;
20
-
21
- export type PiCredentialInput = {
22
- provider: string;
23
- model: string;
24
- api: string;
25
- purpose: "execution" | "selector";
26
- };
27
-
28
- export type PiApiKey = string | ((input: PiCredentialInput) => string | Promise<string>);
29
-
30
- export type PiModelRegistry = {
31
- getModel(provider: string, id: string): PiModel | undefined;
32
- completeSimple(
33
- model: PiModel,
34
- context: Context,
35
- options?: SimpleStreamOptions,
36
- ): Promise<AssistantMessage>;
37
- streamSimple(
38
- model: PiModel,
39
- context: Context,
40
- options?: SimpleStreamOptions,
41
- ): AsyncIterable<AssistantMessageEvent>;
42
- };
43
-
44
- export type CreatePiRuntimeOptions = {
45
- apiKey: PiApiKey;
46
- registry?: PiModelRegistry;
47
- timeoutMs?: number;
48
- maxRetries?: number;
49
- };
50
-
51
- export type PiRouterModelOptions<Metadata = unknown> = Omit<
52
- RouterModel<Metadata>,
53
- "id" | "executor" | "provider" | "api"
54
- >;
55
-
56
- export type PiRuntime = {
57
- model<Metadata = unknown>(
58
- id: string,
59
- options?: Partial<PiRouterModelOptions<Metadata>>,
60
- ): RouterModel<Metadata>;
61
- execute<Metadata>(input: RouterExecutorInput<Metadata>): Promise<RouterExecutorResult>;
62
- select(request: ChatCompletionRequest, signal?: AbortSignal): Promise<string>;
63
- };
64
-
65
- export type PiExecution = {
66
- request: RouterRequest;
67
- candidate: Pick<RouterCandidate, "id" | "provider" | "api">;
68
- reasoningEffort: ReasoningEffort;
69
- signal: AbortSignal;
70
- purpose: PiCredentialInput["purpose"];
71
- };
@@ -1,129 +0,0 @@
1
- import { RouterFrameworkError } from "./framework_error.js";
2
- import type { ProviderIdentity } from "./continuation_state.js";
3
- import { isRecord } from "./types.js";
4
-
5
- const PREFIX = "dari-ir-v1.";
6
- const FAMILY_PREFIX = "dari-ir-";
7
- const MAX_JSON_BYTES = 4 * 1024;
8
- const MAX_ENCODED_CHARS = Math.ceil(MAX_JSON_BYTES / 3) * 4;
9
-
10
- export type PortableReasoningState = {
11
- source?: ProviderIdentity;
12
- itemId?: string;
13
- };
14
-
15
- export function encodePortableReasoningState(state: PortableReasoningState): string {
16
- const source = state.source ? providerIdentity(state.source, "configuration") : undefined;
17
- const itemId = state.itemId ? boundedString(state.itemId, 512, "configuration", "item ID") : undefined;
18
- const payload = {
19
- v: 1,
20
- ...(source ? { source } : {}),
21
- ...(itemId ? { id: itemId } : {}),
22
- };
23
- const json = JSON.stringify(payload);
24
- if (Buffer.byteLength(json, "utf8") > MAX_JSON_BYTES) {
25
- throw invalidPortableState(
26
- "configuration",
27
- "Portable reasoning state exceeds the encoded size limit.",
28
- );
29
- }
30
- return `${PREFIX}${Buffer.from(json, "utf8").toString("base64url")}`;
31
- }
32
-
33
- export function decodeUntrustedPortableReasoningState(value: string): PortableReasoningState | null {
34
- if (!value.startsWith(FAMILY_PREFIX)) return null;
35
- if (!value.startsWith(PREFIX)) {
36
- throw invalidPortableState(
37
- "invalid_request",
38
- "Portable reasoning state version is not supported.",
39
- );
40
- }
41
-
42
- const encoded = value.slice(PREFIX.length);
43
- if (!encoded || encoded.length > MAX_ENCODED_CHARS || !/^[A-Za-z0-9_-]+$/.test(encoded)) {
44
- throw invalidPortableState(
45
- "invalid_request",
46
- "Portable reasoning state encoding is invalid.",
47
- );
48
- }
49
- const bytes = Buffer.from(encoded, "base64url");
50
- if (bytes.length > MAX_JSON_BYTES || bytes.toString("base64url") !== encoded) {
51
- throw invalidPortableState(
52
- "invalid_request",
53
- "Portable reasoning state encoding is invalid.",
54
- );
55
- }
56
-
57
- let decoded: unknown;
58
- try {
59
- decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
60
- } catch {
61
- throw invalidPortableState(
62
- "invalid_request",
63
- "Portable reasoning state payload is invalid.",
64
- );
65
- }
66
- if (!isRecord(decoded) || decoded.v !== 1) {
67
- throw invalidPortableState(
68
- "invalid_request",
69
- "Portable reasoning state schema is invalid.",
70
- );
71
- }
72
- const keys = Object.keys(decoded);
73
- if (!keys.every((key) => key === "v" || key === "source" || key === "id")) {
74
- throw invalidPortableState(
75
- "invalid_request",
76
- "Portable reasoning state schema is invalid.",
77
- );
78
- }
79
- return {
80
- ...(decoded.source !== undefined
81
- ? { source: providerIdentity(decoded.source, "invalid_request") }
82
- : {}),
83
- ...(decoded.id !== undefined
84
- ? { itemId: boundedString(decoded.id, 512, "invalid_request", "item ID") }
85
- : {}),
86
- };
87
- }
88
-
89
- function providerIdentity(
90
- value: unknown,
91
- kind: "configuration" | "invalid_request",
92
- ): ProviderIdentity {
93
- if (!isRecord(value) || !hasExactKeys(value, ["provider", "api", "model"])) {
94
- throw invalidPortableState(kind, "Portable reasoning state source is invalid.");
95
- }
96
- return {
97
- provider: boundedString(value.provider, 128, kind, "provider"),
98
- api: boundedString(value.api, 128, kind, "API"),
99
- model: boundedString(value.model, 512, kind, "model"),
100
- };
101
- }
102
-
103
- function boundedString(
104
- value: unknown,
105
- maxLength: number,
106
- kind: "configuration" | "invalid_request",
107
- field: string,
108
- ): string {
109
- if (typeof value !== "string" || !value || value.length > maxLength) {
110
- throw invalidPortableState(kind, `Portable reasoning state ${field} is invalid.`);
111
- }
112
- return value;
113
- }
114
-
115
- function hasExactKeys(value: Record<string, unknown>, keys: string[]): boolean {
116
- const actual = Object.keys(value);
117
- return actual.length === keys.length && keys.every((key) => actual.includes(key));
118
- }
119
-
120
- function invalidPortableState(
121
- kind: "configuration" | "invalid_request",
122
- message: string,
123
- ): RouterFrameworkError {
124
- return new RouterFrameworkError(
125
- kind,
126
- message,
127
- "invalid_provider_continuation_state",
128
- );
129
- }
@@ -1,109 +0,0 @@
1
- import {
2
- isReasoningEffort,
3
- routingCandidateKey,
4
- type PreviousDecision,
5
- type RouterPrefixHit,
6
- type RoutingCandidate,
7
- } from "./types.js";
8
-
9
- // A previous decision only carries over when its exact model/thinking-level
10
- // pair is still a candidate for this request.
11
- export function compatiblePreviousDecision(
12
- previousDecision: PreviousDecision | undefined,
13
- candidates: RoutingCandidate[],
14
- ): PreviousDecision | undefined {
15
- if (previousDecision === undefined) return undefined;
16
- const previousKey = routingCandidateKey(previousDecision);
17
- return candidates.some((candidate) => routingCandidateKey(candidate) === previousKey)
18
- ? previousDecision
19
- : undefined;
20
- }
21
-
22
- export type ActiveLease = RoutingCandidate & { turnsRemaining: number };
23
-
24
- // An unexpired lease from the deepest identity hit. The lease names the stored
25
- // next-turn target exactly; if that pair is no longer a candidate for this
26
- // request, the lease dissolves and routing decides fresh.
27
- export function activeLeaseFromHit(
28
- entry: RouterPrefixHit,
29
- candidates: RoutingCandidate[],
30
- ): ActiveLease | undefined {
31
- const remaining = entry.lease_turns_remaining;
32
- if (typeof remaining !== "number" || !Number.isInteger(remaining) || remaining <= 0) {
33
- return undefined;
34
- }
35
- const nextEffort = entry.next_reasoning_effort;
36
- if (!entry.next_model || !isReasoningEffort(nextEffort)) return undefined;
37
- const target = { model: entry.next_model, reasoningEffort: nextEffort };
38
- const targetKey = routingCandidateKey(target);
39
- return candidates.some((candidate) => routingCandidateKey(candidate) === targetKey)
40
- ? { ...target, turnsRemaining: remaining }
41
- : undefined;
42
- }
43
-
44
- export type PendingLease = RoutingCandidate & {
45
- turns: number;
46
- reason: string | null;
47
- outputText: string | null;
48
- };
49
-
50
- // The next lease decided ahead of the active one's expiry. Like the active
51
- // lease, it only survives while its exact pair is still a candidate.
52
- export function pendingLeaseFromHit(
53
- entry: RouterPrefixHit,
54
- candidates: RoutingCandidate[],
55
- ): PendingLease | undefined {
56
- const turns = entry.pending_lease_turns;
57
- if (typeof turns !== "number" || !Number.isInteger(turns) || turns <= 0) {
58
- return undefined;
59
- }
60
- const effort = entry.pending_lease_reasoning_effort;
61
- if (!entry.pending_lease_model || !isReasoningEffort(effort)) return undefined;
62
- const target = { model: entry.pending_lease_model, reasoningEffort: effort };
63
- const targetKey = routingCandidateKey(target);
64
- return candidates.some((candidate) => routingCandidateKey(candidate) === targetKey)
65
- ? {
66
- ...target,
67
- turns,
68
- reason: entry.pending_lease_reason ?? null,
69
- outputText: entry.pending_lease_output_text ?? null,
70
- }
71
- : undefined;
72
- }
73
-
74
- // Reconstructs the previous decision from the deepest warm-prefix entry: the
75
- // stored next-turn recommendation when it is still a candidate, otherwise the
76
- // serving pair.
77
- export function previousDecisionFromHit(
78
- entry: RouterPrefixHit,
79
- candidates: RoutingCandidate[],
80
- ): PreviousDecision | undefined {
81
- const candidateKeys = new Set(candidates.map(routingCandidateKey));
82
- const uniqueCandidateForModel = (model: string): RoutingCandidate | null => {
83
- const matching = candidates.filter((candidate) => candidate.model === model);
84
- return matching.length === 1 ? matching[0]! : null;
85
- };
86
- const nextEffort = entry.next_reasoning_effort;
87
- const storedNext = entry.next_model
88
- ? isReasoningEffort(nextEffort)
89
- ? { model: entry.next_model, reasoningEffort: nextEffort }
90
- : uniqueCandidateForModel(entry.next_model)
91
- : null;
92
- const serving = isReasoningEffort(entry.reasoning_bucket)
93
- ? { model: entry.model, reasoningEffort: entry.reasoning_bucket }
94
- : uniqueCandidateForModel(entry.model);
95
- const storedNextIsCandidate = Boolean(
96
- storedNext && candidateKeys.has(routingCandidateKey(storedNext)),
97
- );
98
- const selected = storedNext && storedNextIsCandidate
99
- ? storedNext
100
- : serving && candidateKeys.has(routingCandidateKey(serving))
101
- ? serving
102
- : null;
103
- if (!selected) return undefined;
104
- const storedNextWasRejected = Boolean(entry.next_model) && !storedNextIsCandidate;
105
- return {
106
- ...selected,
107
- reason: storedNextWasRejected ? "" : (entry.reason ?? ""),
108
- };
109
- }
package/src/prompts.ts DELETED
@@ -1,69 +0,0 @@
1
- // Selector system prompts, standalone so prompt review never requires
2
- // reading request-assembly code. Per-request candidate data rides in the
3
- // user message, keeping these byte-stable for provider prompt caching.
4
-
5
- import { FIXED_TURN_COST_PROJECTED_TURNS } from "./fixed_turn_cost_config.js";
6
-
7
- // Interpolated where a prompt names the projection horizons so the wording
8
- // cannot drift from the config that produces the data.
9
- const PROJECTED_TURNS_PHRASE = FIXED_TURN_COST_PROJECTED_TURNS.join(", ");
10
-
11
- export const SELECTOR_SYSTEM_PROMPT = `You are Dari's model router. Pick the best candidate model/thinking-level pair for the user's request.
12
-
13
- # Selection procedure
14
-
15
- 1. Capability first: make sure the selected pair can reasonably handle the task. Consider both model capability and how much thinking the task needs; capability is never traded away for cost.
16
- 2. Use imported eval scorecards as benchmark evidence when relevant.
17
- 3. A missing benchmark score means that candidate pair was not evaluated on that benchmark; do not treat it as zero, failure, or negative evidence.
18
- 4. When several pairs are viable on capability, use cost_estimates as the deciding factor between them.
19
- 5. Prefer the previous_decision pair when the task hasn't changed.
20
-
21
- # Cost comparison rules
22
-
23
- - fixed_turn_cost_estimate.projections is the primary cost metric whenever it is present. Each entry estimates the whole agent loop from this decision point out to projected_turns requests, using current cache warmth, provider cache rules, cache hit probabilities, and output tokens folded into later requests. Compare candidates at the horizon the task looks likely to need.
24
- - Candidates with fixed_turn_cost_estimate are the preferred cost-comparison pool when capability is otherwise comparable.
25
- - Do not rank candidates by output_cost_per_mtok or est_input_cost_usd ahead of fixed_turn_cost_estimate.projections. Do not discard those estimates just because some candidates lack them.
26
- - For candidates without fixed_turn_cost_estimate, use est_input_cost_usd and output_cost_per_mtok as fallback cost evidence, and select such a candidate only when its capability and fallback pricing make it a better fit than candidates with fixed-turn estimates.
27
-
28
- # Output
29
-
30
- Return JSON matching the provided schema. selected_model and reasoning_effort must exactly match one candidate pair.
31
- The runtime chooses provider fallbacks separately; return only the primary decision.`;
32
-
33
- export const CUSTOM_SELECTOR_SYSTEM_PROMPT = `You are Dari's model router. Pick the best candidate model/thinking-level pair for the user's request.
34
- This router is configured with user-authored rules. Each custom_rules entry pairs a natural-language when condition with a model and an optional thinking_level. A null thinking_level means Auto: choose the most appropriate candidate thinking level for that rule's model. A non-null thinking_level pins the exact pair.
35
- Decide which rule's when condition best describes the current request — judge the latest phase or intent of the conversation (for example planning versus implementation), not just the first message — and select that rule's model and either its pinned thinking_level or an appropriate enabled level when it is Auto.
36
- custom_rules is ordered by model price, most expensive first; the order is presentation, not priority.
37
- When several rules match, pick the one whose when condition is most specific to the current request.
38
- When no rule matches, select default_target if it is set; its null thinking_level also means Auto. Otherwise prefer previous_decision when it is a candidate; otherwise pick the most capable candidate pair.
39
- Prefer previous_decision while the matched rule (or task phase) hasn't changed.
40
- Use imported eval scorecards as benchmark evidence when relevant. A missing benchmark score means that candidate pair was not evaluated on that benchmark; do not treat it as zero, failure, or negative evidence.
41
- Use cost_estimates only to break ties between rule-matched candidates or to choose among an Auto target's viable thinking levels; a rule match is never traded away for cost.
42
- When using cost_estimates, fixed_turn_cost_estimate.projections is the primary cost metric whenever present because it includes current cache warmth and projected loop cost at ${PROJECTED_TURNS_PHRASE} turns. For candidates without fixed_turn_cost_estimate, use est_input_cost_usd and output_cost_per_mtok as fallback cost evidence. Do not rank by output_cost_per_mtok ahead of fixed_turn_cost_estimate.projections.
43
- Return JSON matching the provided schema.
44
- selected_model and reasoning_effort must exactly match one candidate pair.
45
- The runtime chooses provider fallbacks separately; return only the primary decision.`;
46
-
47
- // System prompt for the anonymous candidate-action protocol (see
48
- // anonymous_actions.ts). It carries every invariant instruction, including how
49
- // to read the per-action blocks, so the user message stays pure data.
50
- export const ANONYMOUS_ACTION_SYSTEM_PROMPT = [
51
- "You are Dari's model router for a coding agent.",
52
- "Choose exactly one anonymous candidate action for the agent's next turns, and how many turns to commit to it.",
53
- "Your choice is a lease: the chosen action serves that many consecutive turns before you are consulted again. A longer lease amortizes the cold start paid on its first turn across warm cache hits on the turns that follow; the lease ends early only if the task finishes or the provider fails.",
54
- "Each action's block lists its benchmark standing and its projected cost at each lease length you may choose.",
55
- "Rank is the action's position among every model scored on that benchmark, 1 being best. Z is how many standard deviations that model sits above or below the mean of those models.",
56
- "Cost is the projected spend for the whole agent loop over exactly the turns of each lease option, from current cache warmth. Compare actions at the lease you intend to pick.",
57
- "A missing benchmark score means that action was not evaluated on that benchmark; do not treat it as zero, failure, or negative evidence.",
58
- "Prefer a cheaper action only when it is sufficiently likely to complete the task successfully.",
59
- // Scaffolding for the thinking budget: the decision decomposes into judging
60
- // how hard the task is and then finding the cheapest action-and-lease that
61
- // clears that bar. Stating the decomposition costs a few prompt tokens and
62
- // saves the policy from having to discover it from reward alone.
63
- "Think in exactly three short steps, then stop:",
64
- "1. Phase: one sentence — what stage the task is at based on the latest turn (exploring, editing, testing, wrapping up), roughly how many turns remain, and how hard the next step looks.",
65
- "2. Evidence: one sentence — the one or two benchmarks most like this work, and which actions stand out on them.",
66
- "3. Pick: name the cheapest action whose standing clears the difficulty, and the longest lease the remaining work justifies; go pricier or shorter only when the task demands it. Then end thinking immediately and answer.",
67
- "Do not walk through every action, restate the scorecards, or retell the conversation.",
68
- "Return only JSON matching the provided schema.",
69
- ].join("\n");