@knights-eye-chess/ai-analysis 0.0.0-stage → 0.1.0-dev.14

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 (258) hide show
  1. package/LICENSE +235 -0
  2. package/LICENSING.md +26 -0
  3. package/NOTICE +5 -0
  4. package/NPM-PUBLISHING.md +24 -0
  5. package/README.md +141 -2
  6. package/SOURCE-ORIGIN.json +16 -0
  7. package/THIRD_PARTY_NOTICES.md +18 -0
  8. package/build.mjs +22 -0
  9. package/dist/answer-pipeline.test.mjs +375 -0
  10. package/dist/chunk-PLKNJRPF.js +10662 -0
  11. package/dist/chunk-XQDZJOYD.js +2515 -0
  12. package/dist/index.js +146 -0
  13. package/dist/owner-tests/chess-reasoning-compact-search-preparation.test.mjs +963 -0
  14. package/dist/owner-tests/mechanism-encoding.test.mjs +529 -0
  15. package/dist/owner-tests/move-mechanism-facts.test.mjs +396 -0
  16. package/dist/owner-tests/pipeline-host-preparation.test.mjs +10556 -0
  17. package/dist/owner-tests/position-preparation.test.mjs +625 -0
  18. package/dist/owner-tests/preparation-consolidation.test.mjs +1052 -0
  19. package/dist/owner-tests/prose-reference-admission.test.mjs +1770 -0
  20. package/dist/owner-tests/score-comparison-notices.test.mjs +240 -0
  21. package/dist/owner-tests/simple-engine-presentation.test.mjs +233 -0
  22. package/dist/owner-tests/tool-input-loop-guard.test.mjs +78 -0
  23. package/dist/research.js +2274 -0
  24. package/dist/runtime-closure.json +103 -0
  25. package/dist/shared-runtime-closure.json +39 -0
  26. package/dist/shared.js +152 -0
  27. package/dist/types/ai-request-failure.d.ts +14 -0
  28. package/dist/types/ai-request-limits.d.ts +4 -0
  29. package/dist/types/ai-routing-diagnostics.d.ts +14 -0
  30. package/dist/types/chess-reasoning/ablation-adaptive-evidence-loop.d.ts +127 -0
  31. package/dist/types/chess-reasoning/ablation-adaptive-runtime.d.ts +178 -0
  32. package/dist/types/chess-reasoning/ablation-direct-synthesis.d.ts +1030 -0
  33. package/dist/types/chess-reasoning/ablation-evidence-bundle.d.ts +185 -0
  34. package/dist/types/chess-reasoning/ablation-evidence-capture.d.ts +37 -0
  35. package/dist/types/chess-reasoning/ablation-lean-direct-runtime.d.ts +193 -0
  36. package/dist/types/chess-reasoning/ablation-model-facing-evidence.d.ts +38 -0
  37. package/dist/types/chess-reasoning/ablation-transport-correction.d.ts +13 -0
  38. package/dist/types/chess-reasoning/ai-sdk-runtime-support.d.ts +93 -0
  39. package/dist/types/chess-reasoning/answer-host-contracts.d.ts +59 -0
  40. package/dist/types/chess-reasoning/answer-model-connector.d.ts +68 -0
  41. package/dist/types/chess-reasoning/answer-pipeline-exports.d.ts +98 -0
  42. package/dist/types/chess-reasoning/answer-pipeline.d.ts +41 -0
  43. package/dist/types/chess-reasoning/answer-reference-validation.d.ts +40 -0
  44. package/dist/types/chess-reasoning/answer-tool-host.d.ts +8 -0
  45. package/dist/types/chess-reasoning/branch-expansion-policy.d.ts +13 -0
  46. package/dist/types/chess-reasoning/branch-expansion.d.ts +39 -0
  47. package/dist/types/chess-reasoning/causal-claim-checks.d.ts +21 -0
  48. package/dist/types/chess-reasoning/chess-reference-dictionary.d.ts +13 -0
  49. package/dist/types/chess-reasoning/classification-question-target.d.ts +22 -0
  50. package/dist/types/chess-reasoning/classification-reassessment.d.ts +31 -0
  51. package/dist/types/chess-reasoning/compact-search-preparation.d.ts +7 -0
  52. package/dist/types/chess-reasoning/concept-registry.d.ts +99 -0
  53. package/dist/types/chess-reasoning/conversation-context.d.ts +12 -0
  54. package/dist/types/chess-reasoning/engine-search-failure.d.ts +7 -0
  55. package/dist/types/chess-reasoning/evidence-citation-normalization.d.ts +9 -0
  56. package/dist/types/chess-reasoning/evidence-licenses.d.ts +57 -0
  57. package/dist/types/chess-reasoning/evidence-selector-contract.d.ts +5 -0
  58. package/dist/types/chess-reasoning/existing-analysis-context.d.ts +104 -0
  59. package/dist/types/chess-reasoning/fact-observation-provider.d.ts +15 -0
  60. package/dist/types/chess-reasoning/fact-provider.d.ts +111 -0
  61. package/dist/types/chess-reasoning/fresh-engine-pv-witnesses.d.ts +67 -0
  62. package/dist/types/chess-reasoning/historical-prose.d.ts +23 -0
  63. package/dist/types/chess-reasoning/idea-graph.d.ts +34 -0
  64. package/dist/types/chess-reasoning/initial-search-plan.d.ts +37 -0
  65. package/dist/types/chess-reasoning/lossless-evidence-sharing.d.ts +18 -0
  66. package/dist/types/chess-reasoning/matched-comparison.d.ts +10 -0
  67. package/dist/types/chess-reasoning/material-accounting.d.ts +80 -0
  68. package/dist/types/chess-reasoning/material-format.d.ts +4 -0
  69. package/dist/types/chess-reasoning/mechanical-fact-provider.d.ts +33 -0
  70. package/dist/types/chess-reasoning/mechanical-ray-facts.d.ts +19 -0
  71. package/dist/types/chess-reasoning/mechanism-presentation.d.ts +9 -0
  72. package/dist/types/chess-reasoning/move-mechanism-facts.d.ts +45 -0
  73. package/dist/types/chess-reasoning/native-answer-connector.d.ts +4 -0
  74. package/dist/types/chess-reasoning/opening-source-contracts.d.ts +54 -0
  75. package/dist/types/chess-reasoning/opening-source-credits.d.ts +16 -0
  76. package/dist/types/chess-reasoning/opponent-threat-preparation.d.ts +7 -0
  77. package/dist/types/chess-reasoning/piece-identities.d.ts +11 -0
  78. package/dist/types/chess-reasoning/poc-contracts.d.ts +1026 -0
  79. package/dist/types/chess-reasoning/poc-hypotheses.d.ts +74 -0
  80. package/dist/types/chess-reasoning/poc-move-witnesses.d.ts +13 -0
  81. package/dist/types/chess-reasoning/poc-observations.d.ts +23 -0
  82. package/dist/types/chess-reasoning/poc-piece-refs.d.ts +12 -0
  83. package/dist/types/chess-reasoning/poc-plan-model.d.ts +45 -0
  84. package/dist/types/chess-reasoning/poc-semantic-text.d.ts +8 -0
  85. package/dist/types/chess-reasoning/poc-tools.d.ts +81 -0
  86. package/dist/types/chess-reasoning/portable-answer-runtime.d.ts +291 -0
  87. package/dist/types/chess-reasoning/position-analysis-contracts.d.ts +31 -0
  88. package/dist/types/chess-reasoning/position-analysis-scope.d.ts +3 -0
  89. package/dist/types/chess-reasoning/preparation-consolidation.d.ts +21 -0
  90. package/dist/types/chess-reasoning/production-observation-provider.d.ts +16 -0
  91. package/dist/types/chess-reasoning/question-routing.d.ts +326 -0
  92. package/dist/types/chess-reasoning/recorded-main-line.d.ts +37 -0
  93. package/dist/types/chess-reasoning/recorded-position-tools.d.ts +11 -0
  94. package/dist/types/chess-reasoning/recorded-reference-context.d.ts +13 -0
  95. package/dist/types/chess-reasoning/recorded-selected-classification.d.ts +36 -0
  96. package/dist/types/chess-reasoning/reference-limits.d.ts +2 -0
  97. package/dist/types/chess-reasoning/research-exports.d.ts +197 -0
  98. package/dist/types/chess-reasoning/saved-score-presentation.d.ts +14 -0
  99. package/dist/types/chess-reasoning/score-comparison-notices.d.ts +33 -0
  100. package/dist/types/chess-reasoning/server-simple-policy.d.ts +31 -0
  101. package/dist/types/chess-reasoning/simple-agent-config.d.ts +73 -0
  102. package/dist/types/chess-reasoning/simple-board-analysis.d.ts +85 -0
  103. package/dist/types/chess-reasoning/simple-compact-evidence.d.ts +7 -0
  104. package/dist/types/chess-reasoning/simple-completion.d.ts +23 -0
  105. package/dist/types/chess-reasoning/simple-engine-presentation.d.ts +3 -0
  106. package/dist/types/chess-reasoning/simple-evidence-preparation.d.ts +17 -0
  107. package/dist/types/chess-reasoning/simple-evidence-view.d.ts +17 -0
  108. package/dist/types/chess-reasoning/simple-explanation-coverage.d.ts +90 -0
  109. package/dist/types/chess-reasoning/simple-fact-delivery.d.ts +43 -0
  110. package/dist/types/chess-reasoning/simple-fallback-premise.d.ts +5 -0
  111. package/dist/types/chess-reasoning/simple-prose-references.d.ts +108 -0
  112. package/dist/types/chess-reasoning/simple-runtime-version.d.ts +1 -0
  113. package/dist/types/chess-reasoning/stable.d.ts +2 -0
  114. package/dist/types/chess-reasoning/tool-input-loop-guard.d.ts +5 -0
  115. package/dist/types/chess-reasoning/types.d.ts +180 -0
  116. package/dist/types/chess-reasoning/visible-support-lines.d.ts +12 -0
  117. package/dist/types/chess-reasoning/wikibooks-index.d.ts +30 -0
  118. package/dist/types/chess-reasoning/wikibooks-tool-context.d.ts +10 -0
  119. package/dist/types/player-perspective.d.ts +16 -0
  120. package/dist/types/shared-exports.d.ts +22 -0
  121. package/docs/API.md +82 -0
  122. package/docs/OWNERSHIP-ADOPTION.md +35 -0
  123. package/docs/OWNERSHIP-DRIFT.json +55 -0
  124. package/docs/RESEARCH-API.md +28 -0
  125. package/licenses/third-party/INVENTORY.json +202 -0
  126. package/licenses/third-party/ai-7.0.85/LICENSE +13 -0
  127. package/licenses/third-party/ai-sdk--gateway-4.0.69/LICENSE +13 -0
  128. package/licenses/third-party/ai-sdk--provider-4.0.9/LICENSE +13 -0
  129. package/licenses/third-party/ai-sdk--provider-utils-5.0.34/APACHE-2.0.txt +73 -0
  130. package/licenses/third-party/ai-sdk--provider-utils-5.0.34/src/to-json-schema/zod3-to-json-schema/LICENSE +16 -0
  131. package/licenses/third-party/chess.js-1.4.0/LICENSE +23 -0
  132. package/licenses/third-party/eventsource-parser-3.1.1/LICENSE +21 -0
  133. package/licenses/third-party/json-schema-0.4.0/LICENSE +195 -0
  134. package/licenses/third-party/standard-schema--spec-1.1.0/LICENSE +21 -0
  135. package/licenses/third-party/types--json-schema-7.0.15/LICENSE +21 -0
  136. package/licenses/third-party/types--node-22.19.19/LICENSE +21 -0
  137. package/licenses/third-party/undici-7.30.0/LICENSE +21 -0
  138. package/licenses/third-party/undici-7.30.0/lib/web/fetch/LICENSE +21 -0
  139. package/licenses/third-party/undici-types-6.21.0/LICENSE +21 -0
  140. package/licenses/third-party/vercel--oidc-3.2.0/LICENSE +202 -0
  141. package/licenses/third-party/workflow--serde-4.1.0/APACHE-2.0.txt +73 -0
  142. package/licenses/third-party/workflow--serde-4.1.0/LICENSE.md +3 -0
  143. package/licenses/third-party/zod-4.6.5/LICENSE +21 -0
  144. package/npm-release.json +5 -0
  145. package/package.json +72 -4
  146. package/scripts/check-release-tooling.mjs +22 -0
  147. package/scripts/pack.mjs +68 -0
  148. package/scripts/publish-reviewed.mjs +72 -0
  149. package/scripts/release-tooling.json +13 -0
  150. package/scripts/update-dependency.mjs +44 -0
  151. package/scripts/verify-release.mjs +52 -0
  152. package/src/ai-request-failure.ts +34 -0
  153. package/src/ai-request-limits.ts +4 -0
  154. package/src/ai-routing-diagnostics.ts +33 -0
  155. package/src/chess-reasoning/ablation-adaptive-evidence-loop.ts +515 -0
  156. package/src/chess-reasoning/ablation-adaptive-runtime.ts +924 -0
  157. package/src/chess-reasoning/ablation-direct-synthesis.ts +1128 -0
  158. package/src/chess-reasoning/ablation-evidence-bundle.ts +726 -0
  159. package/src/chess-reasoning/ablation-evidence-capture.ts +513 -0
  160. package/src/chess-reasoning/ablation-lean-direct-runtime.ts +670 -0
  161. package/src/chess-reasoning/ablation-model-facing-evidence.ts +212 -0
  162. package/src/chess-reasoning/ablation-transport-correction.ts +106 -0
  163. package/src/chess-reasoning/ai-sdk-runtime-support.ts +381 -0
  164. package/src/chess-reasoning/answer-host-contracts.ts +10 -0
  165. package/src/chess-reasoning/answer-model-connector.ts +83 -0
  166. package/src/chess-reasoning/answer-pipeline-exports.ts +98 -0
  167. package/src/chess-reasoning/answer-pipeline.ts +195 -0
  168. package/src/chess-reasoning/answer-reference-validation.ts +124 -0
  169. package/src/chess-reasoning/answer-tool-host.ts +10 -0
  170. package/src/chess-reasoning/branch-expansion-policy.ts +32 -0
  171. package/src/chess-reasoning/branch-expansion.ts +80 -0
  172. package/src/chess-reasoning/causal-claim-checks.ts +256 -0
  173. package/src/chess-reasoning/chess-reference-dictionary.ts +21 -0
  174. package/src/chess-reasoning/classification-question-target.ts +30 -0
  175. package/src/chess-reasoning/classification-reassessment.ts +34 -0
  176. package/src/chess-reasoning/compact-search-preparation.ts +125 -0
  177. package/src/chess-reasoning/concept-registry.ts +275 -0
  178. package/src/chess-reasoning/conversation-context.ts +29 -0
  179. package/src/chess-reasoning/engine-search-failure.ts +11 -0
  180. package/src/chess-reasoning/evidence-citation-normalization.ts +15 -0
  181. package/src/chess-reasoning/evidence-licenses.ts +166 -0
  182. package/src/chess-reasoning/evidence-selector-contract.ts +5 -0
  183. package/src/chess-reasoning/existing-analysis-context.ts +544 -0
  184. package/src/chess-reasoning/fact-observation-provider.ts +134 -0
  185. package/src/chess-reasoning/fact-provider.ts +250 -0
  186. package/src/chess-reasoning/fresh-engine-pv-witnesses.ts +128 -0
  187. package/src/chess-reasoning/historical-prose.ts +116 -0
  188. package/src/chess-reasoning/idea-graph.ts +224 -0
  189. package/src/chess-reasoning/initial-search-plan.ts +115 -0
  190. package/src/chess-reasoning/lossless-evidence-sharing.ts +135 -0
  191. package/src/chess-reasoning/matched-comparison.ts +40 -0
  192. package/src/chess-reasoning/material-accounting.ts +55 -0
  193. package/src/chess-reasoning/material-format.ts +47 -0
  194. package/src/chess-reasoning/mechanical-fact-provider.ts +1602 -0
  195. package/src/chess-reasoning/mechanical-ray-facts.ts +123 -0
  196. package/src/chess-reasoning/mechanism-presentation.ts +94 -0
  197. package/src/chess-reasoning/move-mechanism-facts.ts +185 -0
  198. package/src/chess-reasoning/native-answer-connector.ts +13 -0
  199. package/src/chess-reasoning/opening-source-contracts.ts +47 -0
  200. package/src/chess-reasoning/opening-source-credits.ts +53 -0
  201. package/src/chess-reasoning/opponent-threat-preparation.ts +98 -0
  202. package/src/chess-reasoning/piece-identities.ts +159 -0
  203. package/src/chess-reasoning/poc-contracts.ts +1097 -0
  204. package/src/chess-reasoning/poc-hypotheses.ts +759 -0
  205. package/src/chess-reasoning/poc-move-witnesses.ts +170 -0
  206. package/src/chess-reasoning/poc-observations.ts +611 -0
  207. package/src/chess-reasoning/poc-piece-refs.ts +62 -0
  208. package/src/chess-reasoning/poc-plan-model.ts +345 -0
  209. package/src/chess-reasoning/poc-semantic-text.ts +10 -0
  210. package/src/chess-reasoning/poc-tools.ts +978 -0
  211. package/src/chess-reasoning/portable-answer-runtime.ts +684 -0
  212. package/src/chess-reasoning/position-analysis-contracts.ts +88 -0
  213. package/src/chess-reasoning/position-analysis-scope.ts +20 -0
  214. package/src/chess-reasoning/preparation-consolidation.ts +91 -0
  215. package/src/chess-reasoning/production-observation-provider.ts +41 -0
  216. package/src/chess-reasoning/question-routing.ts +332 -0
  217. package/src/chess-reasoning/recorded-main-line.ts +55 -0
  218. package/src/chess-reasoning/recorded-position-tools.ts +78 -0
  219. package/src/chess-reasoning/recorded-reference-context.ts +44 -0
  220. package/src/chess-reasoning/recorded-selected-classification.ts +54 -0
  221. package/src/chess-reasoning/reference-limits.ts +2 -0
  222. package/src/chess-reasoning/research-exports.ts +197 -0
  223. package/src/chess-reasoning/saved-score-presentation.ts +6 -0
  224. package/src/chess-reasoning/score-comparison-notices.ts +115 -0
  225. package/src/chess-reasoning/server-simple-policy.ts +75 -0
  226. package/src/chess-reasoning/simple-agent-config.ts +58 -0
  227. package/src/chess-reasoning/simple-board-analysis.ts +57 -0
  228. package/src/chess-reasoning/simple-compact-evidence.ts +79 -0
  229. package/src/chess-reasoning/simple-completion.ts +37 -0
  230. package/src/chess-reasoning/simple-engine-presentation.ts +95 -0
  231. package/src/chess-reasoning/simple-evidence-preparation.ts +32 -0
  232. package/src/chess-reasoning/simple-evidence-view.ts +69 -0
  233. package/src/chess-reasoning/simple-explanation-coverage.ts +118 -0
  234. package/src/chess-reasoning/simple-fact-delivery.ts +52 -0
  235. package/src/chess-reasoning/simple-fallback-premise.ts +65 -0
  236. package/src/chess-reasoning/simple-prose-references.ts +528 -0
  237. package/src/chess-reasoning/simple-runtime-version.ts +2 -0
  238. package/src/chess-reasoning/stable.ts +18 -0
  239. package/src/chess-reasoning/tool-input-loop-guard.ts +38 -0
  240. package/src/chess-reasoning/types.ts +195 -0
  241. package/src/chess-reasoning/visible-support-lines.ts +44 -0
  242. package/src/chess-reasoning/wikibooks-index.ts +56 -0
  243. package/src/chess-reasoning/wikibooks-tool-context.ts +91 -0
  244. package/src/player-perspective.ts +41 -0
  245. package/src/shared-exports.ts +23 -0
  246. package/test/answer-pipeline.test.mjs +375 -0
  247. package/test/owner/chess-reasoning-compact-search-preparation.test.mjs +111 -0
  248. package/test/owner/mechanism-encoding.test.mjs +78 -0
  249. package/test/owner/move-mechanism-facts.test.mjs +105 -0
  250. package/test/owner/pipeline-host-preparation.test.mjs +115 -0
  251. package/test/owner/position-preparation.test.mjs +22 -0
  252. package/test/owner/preparation-consolidation.test.mjs +74 -0
  253. package/test/owner/prose-reference-admission.test.mjs +26 -0
  254. package/test/owner/score-comparison-notices.test.mjs +116 -0
  255. package/test/owner/simple-engine-presentation.test.mjs +50 -0
  256. package/test/owner/tool-input-loop-guard.test.mjs +25 -0
  257. package/test-types.mts +23 -0
  258. package/tsconfig.json +22 -0
@@ -0,0 +1,684 @@
1
+ import { classificationReassessment } from './classification-reassessment';
2
+ import { CHESS_REFERENCE_DICTIONARY } from './chess-reference-dictionary';
3
+ import type { RecordedSelectedClassification } from './recorded-selected-classification';
4
+ import { createToolInputLoopGuard } from './tool-input-loop-guard';
5
+ import { visibleSupportLines } from './visible-support-lines';
6
+ import { compactMechanism } from './mechanism-presentation';
7
+ import { projectEnginePresentation } from './simple-engine-presentation';
8
+ import { consolidatePreparedSearches, type PreparedPresentation } from './preparation-consolidation';
9
+ import type { WikibooksIndex } from './wikibooks-index';
10
+ import { compactPreparedSearchResult } from './compact-search-preparation';
11
+ import { validateAnswerReferences } from './answer-reference-validation';
12
+ import { scoreComparisonNotices } from './score-comparison-notices';
13
+ import { completionPacket, evidenceFallback } from "./simple-completion";
14
+ import { AI_MODEL_CONTEXT_MAX_BYTES } from "../ai-request-limits";
15
+ import { readAiRoutingDiagnostics } from '../ai-routing-diagnostics';
16
+ import { recordedEvidenceContext } from './recorded-reference-context';
17
+ import {openingSourcesInMessages,openingSourceKey,type OpeningSourceCredit} from './opening-source-credits';
18
+ import type { MaterialFormat } from './material-format';
19
+ import { shareEvidenceMessages, hasSharedEvidence } from "./lossless-evidence-sharing";
20
+ import { isRecordedPositionTool } from './recorded-position-tools';
21
+ import { preparedMoveComparison, type InitialSearchPlan } from './initial-search-plan';
22
+ import { wikibooksTargetsForToolIndexed } from "./wikibooks-tool-context";
23
+ import type { WikibooksContextResult, WikibooksTarget } from "./opening-source-contracts";
24
+ import {EXPLANATION_REVIEW_SCHEMA,legalInvestigationRoots,validateExplanationReview,explanationCoverage,explanationSummary,planningEvidence,type ExplanationReview} from './simple-explanation-coverage';
25
+ import { failureFromError, type AiFailureDetails } from "../ai-request-failure";
26
+ import type { SimpleBoardAnalysis } from "./simple-board-analysis";
27
+ import { priorConversationMessages, type PriorConversationMessage } from "./conversation-context";
28
+ import { recordedMainLineResult } from "./recorded-main-line";
29
+ import { moveSectionScope } from './question-routing';
30
+ import type { RecordedMainLine } from "./answer-host-contracts";
31
+ import type { SimpleEffort } from "./simple-agent-config";
32
+ import { jsonSchema, tool, type ModelMessage } from "ai";
33
+ import { invokeAnswerTurn, type AnswerModelConnector } from "./answer-model-connector";
34
+ import { simpleInitialEvidenceView, compactSimpleEngineResult } from "./simple-evidence-view";
35
+ import { SimpleFactDelivery } from "./simple-fact-delivery";
36
+ import { projectLeanDirectEvidence } from "./ablation-lean-direct-runtime";
37
+ import { validateAblationEvidenceClosureBundle, type AblationEvidenceClosureBundle } from "./ablation-evidence-bundle";
38
+ import { emitObservableTrace, observableTraceMessages, type ObservableLlmTraceSink } from "./ai-sdk-runtime-support";
39
+ import { effectiveLicensesForRecord } from "./evidence-licenses";
40
+ import { createContextEvidence, reasoningContextHash, type ReasoningEvidenceRecord, type ReasoningToolSet, type ReasoningToolName } from "./poc-contracts";
41
+ import type { PlayerPerspective } from "../player-perspective";
42
+
43
+ export { SIMPLE_TOOL_AGENT_VERSION } from "./simple-runtime-version";
44
+ export const SIMPLE_MODELS = { "Simple-Sol": "openai/gpt-5.6-sol", "Simple-Opus": "anthropic/claude-opus-5" } as const;
45
+ export type SimpleModelOption = keyof typeof SIMPLE_MODELS;
46
+ export const SIMPLE_BUDGETS = Object.freeze({ maxModelCalls: 6, maxOutputTokensPerCall: 6000,
47
+ maxSearchesPerBatch: 8, maxOutputTokens: 18000, maxWallTimeMs: 180000, maxRequestBytes: AI_MODEL_CONTEXT_MAX_BYTES, maxDepth: 14, maxRootMoves: 6 });
48
+ export const SIMPLE_EXPANDED_BUDGETS = Object.freeze({ ...SIMPLE_BUDGETS,
49
+ maxOutputTokensPerCall: 12000, maxOutputTokens: 36000, maxWallTimeMs: 300000 });
50
+ export const SIMPLE_RECOVERY_BUDGETS = Object.freeze({ ...SIMPLE_BUDGETS,
51
+ maxOutputTokensPerCall: 24000, maxOutputTokens: 72000, maxWallTimeMs: 600000 });
52
+ export const SIMPLE_DISCLOSURE = "Experimental answer. Tool inputs, legality, evidence scope and interactive references are checked at completion and reported in referenceValidation; prose correctness and claim entailment are not validated. Unresolved references remain plain text.";
53
+
54
+ /** Keep only billing and bounded routing metadata; provider reasoning stays private in continuation memory. */
55
+ export function simpleUsageForTrace(usage: unknown, metadata: unknown, headers?: Record<string, string>) {
56
+ const cost = (metadata as {openrouter?:{usage?:{cost?:unknown}}}|undefined)?.openrouter?.usage?.cost;
57
+ const serviceRouting = readAiRoutingDiagnostics(headers);
58
+ return {usage, ...(serviceRouting ? {serviceRouting} : {}), ...(typeof cost === 'number' && Number.isFinite(cost) && cost >= 0
59
+ ? {providerMetadata:{openrouter:{usage:{cost}}}} : {})};
60
+ }
61
+
62
+ // Only the finite scalar/array argument vocabulary of the existing six chess
63
+ // tools. The tool host remains responsible for exact legality and phase replay.
64
+ function inputError(code: string, field: string, expected: string, details?: string, choices?: unknown[]) {
65
+ return Object.assign(new Error(`${field}: ${expected}${details ? `; ${details}` : ""}`), {code, field, expected, ...(details?{details}:{}), ...(choices?{choices:choices.slice(0,32)}:{})});
66
+ }
67
+ function toolError(error: unknown) {
68
+ const e = error as {message?:unknown;code?:unknown;field?:unknown;expected?:unknown;details?:unknown;choices?:unknown};
69
+ const bounded = (value: unknown, n=300) => typeof value === 'string' ? value.slice(0,n) : undefined;
70
+ const message=bounded(e?.message) || 'tool-failed';
71
+ return {error:message,code:bounded(e?.code,80) || message,
72
+ ...(bounded(e?.field,100)?{field:bounded(e.field,100)}:{}),
73
+ ...(bounded(e?.expected)?{expected:bounded(e.expected)}:{}),
74
+ ...(bounded(e?.details)?{details:bounded(e.details)}:{}),
75
+ ...(Array.isArray(e?.choices)?{choices:e.choices.slice(0,32).filter((choice):choice is string=>typeof choice==='string').map(choice=>choice.slice(0,100))}:{})};
76
+ }
77
+ export function validateSimpleToolInput(name: string, input: Record<string, unknown>, schema: Record<string, unknown>) {
78
+ if (!input || typeof input !== "object" || Array.isArray(input)) throw inputError('invalid-tool-input','input','object');
79
+ const properties = schema.properties as Record<string, Record<string, unknown>>;
80
+ const unknown=Object.keys(input).find(key=>!Object.hasOwn(properties,key));
81
+ if (unknown) throw inputError('unknown-tool-field',unknown,'known schema field',undefined,Object.keys(properties));
82
+ const missing=((schema.required || []) as string[]).find(key=>input[key]===undefined);
83
+ if (missing) throw inputError('missing-tool-field',missing,'required field',undefined,Object.keys(properties));
84
+ for (const [key, value] of Object.entries(input)) {
85
+ // Retired metadata is discarded; never require a paid repair for it.
86
+ if (name === "explanation_review" && key === "shortLineReasons") continue;
87
+ const rule = properties[key];
88
+ if (rule.enum && !(rule.enum as unknown[]).includes(value)) throw inputError('invalid-tool-enum',key,'one of the listed values',undefined,rule.enum as unknown[]);
89
+ if (rule.type === "boolean" && typeof value !== "boolean") throw inputError('invalid-tool-boolean',key,'boolean');
90
+ if (rule.type === "integer" && (!Number.isInteger(value) || (rule.minimum!==undefined && Number(value)<Number(rule.minimum)) || (rule.maximum!==undefined && Number(value)>Number(rule.maximum)))) throw inputError('invalid-tool-integer',key,`integer${rule.minimum!==undefined?` >= ${rule.minimum}`:''}${rule.maximum!==undefined?` <= ${rule.maximum}`:''}`);
91
+ if (rule.type === 'string' && (typeof value !== 'string' || (rule.minLength!==undefined && value.length<Number(rule.minLength)) || (rule.maxLength!==undefined && value.length>Number(rule.maxLength)) || (rule.pattern!==undefined && !new RegExp(String(rule.pattern)).test(value)))) throw inputError('invalid-tool-string',key,`string${rule.maxLength!==undefined?` of at most ${rule.maxLength} characters`:''}${rule.pattern!==undefined?` matching ${rule.pattern}`:''}`);
92
+ if (rule.type === "array") {
93
+ if (!Array.isArray(value) || value.length < Number(rule.minItems || 0) || (rule.maxItems!==undefined && value.length > Number(rule.maxItems))) throw inputError('invalid-tool-array',key,`array of ${rule.minItems??0}..${rule.maxItems??'unbounded'} items`);
94
+ if (name === "search_batch" && key === "requests") {
95
+ continue;
96
+ }
97
+ const itemRule=rule.items as Record<string,unknown> | undefined;
98
+ value.forEach((item,index)=>{
99
+ const field=`${key}[${index}]`;
100
+ if (itemRule?.enum && !(itemRule.enum as unknown[]).includes(item)) throw inputError('invalid-tool-enum',field,'one of the listed values',undefined,itemRule.enum as unknown[]);
101
+ if (itemRule?.type === "integer" && (!Number.isInteger(item) || (itemRule.minimum!==undefined && Number(item)<Number(itemRule.minimum)) || (itemRule.maximum!==undefined && Number(item)>Number(itemRule.maximum)))) throw inputError('invalid-tool-integer',field,`integer${itemRule.minimum!==undefined?` >= ${itemRule.minimum}`:''}${itemRule.maximum!==undefined?` <= ${itemRule.maximum}`:''}`);
102
+ if (itemRule?.type === "string") {
103
+ const maximum=Number(itemRule.maxLength??300);
104
+ if(typeof item!=="string" || item.length>maximum || (itemRule.minLength!==undefined && item.length<Number(itemRule.minLength)) || (itemRule.pattern!==undefined && !new RegExp(String(itemRule.pattern)).test(item))) throw inputError('invalid-tool-string',field,`string of at most ${maximum} characters${itemRule.pattern?` matching ${itemRule.pattern}`:''}`);
105
+ }
106
+ });
107
+ if ((key === "squares" || key === "inspectSquares") && value.some(item => !/^[a-h][1-8]$/.test(item))) throw inputError('invalid-square',key,'chess squares a1 through h8');
108
+ if ((key === "movesUci" || key === "rootMoves" || key === "compareFinalMovesUci") && value.some(item => !/^[a-h][1-8][a-h][1-8][qrbn]?$/.test(item))) throw inputError('invalid-uci',key,'UCI coordinate moves such as e2e4 or a7a8q');
109
+ if ((name === "compare_moves" || name === "enumerate_defenses") && value.length > SIMPLE_BUDGETS.maxRootMoves) throw inputError('root-budget',key,`at most ${SIMPLE_BUDGETS.maxRootMoves} root moves`);
110
+ }
111
+ }
112
+ }
113
+
114
+ export type SimpleAgentDiagnostic = { stage: string; inputTokens: number; outputTokens: number; latencyMs: number; finishReason: string };
115
+ export async function runPreparedAnswer(input: {
116
+ policy?: { system: (flags: {encoded:boolean;reusedFacts:boolean;sharedEvidence?:boolean;final:boolean;purposeGuidance?:boolean;searchBreadthGuidance?:boolean;boardAnalysisGuidance?:boolean;explanationCoverage?:boolean;answerComposition?:'move-explanation'|'none'}) => string; description: (name:string) => string }; connector: AnswerModelConnector; openingIndex?: () => Promise<WikibooksIndex>; modelName: string; requestId: string;
117
+ /** Offline evaluation only; callers supply a condition-specific bundle without withheld evidence. */
118
+ evaluation?: { fixedEvidence: true; requiredEvidenceIds: string[] };
119
+ /** Node evaluation only: provider metering is unavailable; no browser caller sets this. */
120
+ evaluationTransport?: { kind: "subagent-files" | "openrouter-direct"; wallTimeMs: number };
121
+ /** Baseline switch for controlled serializer evaluation only. */
122
+ evidenceCompaction?: "baseline" | "lossless";
123
+ /** Node comparison override; browser uses the qualified default. */
124
+ materialFormat?: MaterialFormat;
125
+ question?: string;
126
+ /** Diagnostic ablation only; live Simple enables bounded explanation review. */
127
+ explanationCoverage?: boolean;
128
+ boardAnalysis?: SimpleBoardAnalysis;
129
+ recordedSelectedClassification?: RecordedSelectedClassification;
130
+ /** Research ablation; normal evidence always includes the standard reference. */
131
+ squareReference?: boolean;
132
+ openingSourceContext?: WikibooksContextResult;
133
+ openingSourceLookup?: (targets: WikibooksTarget[], signal: AbortSignal, root?: import("./poc-contracts").ReasoningPositionContext) => Promise<WikibooksContextResult>;
134
+ /** Total overlapping browser preparation time, including optional source wait. */
135
+ preModelElapsedMs?: number;
136
+ playerPerspective?: PlayerPerspective | null;
137
+ conversationHistory?: readonly PriorConversationMessage[];
138
+ getMainLine?: (signal: AbortSignal) => Promise<RecordedMainLine>;
139
+ initialMainLine?: RecordedMainLine;
140
+ initialSearchPlan?: InitialSearchPlan;
141
+ refreshPreparedBoardAnalysis?: (signal:AbortSignal)=>Promise<SimpleBoardAnalysis | undefined>;
142
+ omitSelectedContext?: boolean;
143
+ routingDecision?: import('./question-routing').QuestionRoutingDecision;
144
+ effort?: SimpleEffort;
145
+ providerRoute?: string;
146
+ /** Explicit browser analysis policy; frozen diagnostic defaults remain unchanged. */
147
+ analysisDepth?: 18;
148
+ preparation?: import("./simple-evidence-preparation").SimpleEvidencePreparation;
149
+ purposeGuidance?: boolean;
150
+ searchBreadthGuidance?: boolean;
151
+ boardAnalysisGuidance?: boolean;
152
+ simpleAllowance?: "default" | "expanded" | "glm-recovery";
153
+ bundle: Readonly<AblationEvidenceClosureBundle>; host: ReasoningToolSet; signal?: AbortSignal;
154
+ observableTraceSink?: ObservableLlmTraceSink; onProgress?: (message: string) => void }) {
155
+ validateAblationEvidenceClosureBundle(input.bundle);
156
+ if (input.simpleAllowance && !["default", "expanded", "glm-recovery"].includes(input.simpleAllowance)) throw new Error("invalid-simple-allowance");
157
+ if (input.simpleAllowance === "glm-recovery" && input.modelName !== "z-ai/glm-5.3") throw new Error("recovery-allowance-model");
158
+ const allowance = input.simpleAllowance === "glm-recovery" ? SIMPLE_RECOVERY_BUDGETS
159
+ : input.simpleAllowance === "expanded" ? SIMPLE_EXPANDED_BUDGETS : SIMPLE_BUDGETS;
160
+ const budgets = { ...allowance, maxWallTimeMs: Number(allowance.maxWallTimeMs), maxOutputTokens: Number(allowance.maxOutputTokens), ...(input.analysisDepth ? { defaultDepth: input.analysisDepth, minimumDepth: input.analysisDepth, maxDepth: input.analysisDepth } : {}) };
161
+ if (input.evaluationTransport && !["subagent-files", "openrouter-direct"].includes(input.evaluationTransport.kind)) throw Error("invalid-evaluation-transport");
162
+ const subagentEvaluation = input.evaluationTransport?.kind === "subagent-files" ? input.evaluationTransport : undefined;
163
+ if (input.evaluationTransport?.kind === "openrouter-direct") {
164
+ const wall = input.evaluationTransport.wallTimeMs;
165
+ if (input.providerRoute !== "openrouter-direct" || !Number.isInteger(wall) || wall < 1000 || wall > 1800000) throw Error("invalid-openrouter-evaluation");
166
+ budgets.maxWallTimeMs = wall;
167
+ }
168
+ if (subagentEvaluation) {
169
+ if (subagentEvaluation.kind !== "subagent-files" || input.providerRoute !== "codex-subagent-file"
170
+ || !Number.isInteger(subagentEvaluation.wallTimeMs) || subagentEvaluation.wallTimeMs < 1000
171
+ || subagentEvaluation.wallTimeMs > 1800000) throw Error("invalid-subagent-evaluation");
172
+ budgets.maxWallTimeMs = subagentEvaluation.wallTimeMs;
173
+ // Reserve each entire response allowance when actual usage is unavailable.
174
+ // This is a safety counter, not a token measurement.
175
+ budgets.maxOutputTokens = budgets.maxModelCalls * budgets.maxOutputTokensPerCall;
176
+ }
177
+ const contextHash = reasoningContextHash(input.bundle.context);
178
+ const delivery = new SimpleFactDelivery(contextHash);
179
+ if (input.evaluation && (input.evaluation.fixedEvidence !== true || !Array.isArray(input.evaluation.requiredEvidenceIds)
180
+ || input.evaluation.requiredEvidenceIds.some(id => typeof id !== "string" || !id.trim()))) throw Error("invalid-fixed-evidence-evaluation");
181
+ const fixedEvidence = input.evaluation?.fixedEvidence === true;
182
+ const initialGameRecord=input.initialMainLine?recordedMainLineResult(input.initialMainLine,input.bundle.context):undefined;
183
+ const omitSelectedContext=input.omitSelectedContext===true;
184
+ const answerComposition=input.routingDecision ? moveSectionScope(input.routingDecision) : 'none';
185
+ if(omitSelectedContext&&(!initialGameRecord||input.initialMainLine!.truncated||fixedEvidence))throw Error('context-omission-requires-complete-game-record');
186
+ let selectedContextAvailable=!omitSelectedContext;
187
+ const coverageEnabled = !omitSelectedContext && !fixedEvidence && input.explanationCoverage !== false && input.bundle.context.kind !== 'position';
188
+ let review:ExplanationReview|null=null, reviewCount=0;
189
+ const stopRepairs=0, qualificationRequested=false;
190
+ const requiredEvidenceIds = [...new Set([...(input.preparation?.result?.evidence.map(e => e.id) ?? []),
191
+ ...(input.evaluation?.requiredEvidenceIds ?? [])])];
192
+ const initial = projectLeanDirectEvidence(input.bundle, requiredEvidenceIds).bundle;
193
+ if (fixedEvidence && requiredEvidenceIds.some(id => !initial.evidence.some(record => record.id === id))) throw Error("fixed-evidence-projection-omitted-required-record");
194
+ const visibleIds = new Set(omitSelectedContext?[]:initial.evidence.map(e => e.id));
195
+ const known = new Map(input.bundle.evidence.map(e => [e.id, e]));
196
+ // Lean observation selection can omit the context record. Deliver the selected
197
+ // transition independently, retaining its exact record for normal retrieval.
198
+ const selectedContextRecord = input.bundle.context.kind === 'position' ? undefined : createContextEvidence(input.bundle.context,contextHash);
199
+ if (selectedContextRecord) {
200
+ known.set(selectedContextRecord.id,selectedContextRecord);
201
+ if (!omitSelectedContext) visibleIds.add(selectedContextRecord.id);
202
+ }
203
+ const selectedMechanism = selectedContextRecord?.data.selectedMoveMechanism;
204
+ const selectedMechanismView = selectedContextRecord && selectedMechanism && typeof selectedMechanism === 'object' && !Array.isArray(selectedMechanism)
205
+ ? {evidenceId:selectedContextRecord.id,fenBefore:input.bundle.context.fenBefore,
206
+ fenAfter:input.bundle.context.fenAfter,moveUci:input.bundle.context.moveUci,
207
+ mechanism:compactMechanism(selectedMechanism,
208
+ (selectedMechanism.controlDelta as Array<{square:string}>).map(delta=>delta.square))} : undefined;
209
+ if (input.boardAnalysis) {
210
+ if (input.boardAnalysis.record.contextHash !== contextHash) throw Error("stale-board-analysis");
211
+ known.set(input.boardAnalysis.record.id, input.boardAnalysis.record);
212
+ if(!omitSelectedContext)visibleIds.add(input.boardAnalysis.record.id);
213
+ }
214
+ const previousClassificationAnalysis = input.initialSearchPlan?.classificationReassessment && !omitSelectedContext ? structuredClone(input.boardAnalysis) : undefined;
215
+ const visibleRecords=()=>[...known.values()].filter(e=>visibleIds.has(e.id));
216
+ let lastRecurringSummary="";
217
+ const recurringForTurn=()=>{const summary=explanationSummary(input.bundle.context,visibleRecords());const encoded=JSON.stringify(summary);if(encoded===lastRecurringSummary)return {unchanged:true,scope:"Reuse the identical recurring summary already delivered in this current native loop."};lastRecurringSummary=encoded;return summary;};
218
+ const started = performance.now(), controller = new AbortController();
219
+ const abort = () => controller.abort();
220
+ if (input.signal?.aborted) abort(); else input.signal?.addEventListener("abort", abort, { once: true });
221
+ const preparationMs = input.preModelElapsedMs ?? input.preparation?.elapsedMs ?? 0;
222
+ if (!Number.isFinite(preparationMs) || preparationMs < 0 || (input.preparation && (!Number.isFinite(input.preparation.elapsedMs) || input.preparation.elapsedMs < 0 || preparationMs < input.preparation.elapsedMs || ![0, 1, 2, 3].includes(input.preparation.searchCount)))) throw Error("invalid-evidence-preparation");
223
+ const timeToFinish = () => performance.now() - started + preparationMs >= Math.max(0, budgets.maxWallTimeMs - 60000);
224
+ const diagnostics: SimpleAgentDiagnostic[] = [];
225
+ let unmeteredCalls=0, reportedOutputTokens=0, outputBudgetReservation=0, unmeteredInputCalls=0, reportedInputTokens=0;
226
+ const toolRuns: Record<string, unknown>[] = [];
227
+ const acquiredMainLines: Array<ReturnType<typeof recordedMainLineResult>["recordedMainLine"]> = initialGameRecord?[initialGameRecord.recordedMainLine]:[];
228
+ const actionable = new Set(['insufficient_credits','invalid_key','rate_limited','model_unavailable','free_unavailable','free_funds_exhausted','ai_request_active','ai_hourly_limit','ai_admission_busy','sign_in_required','forbidden','connection_changed','cancelled']);
229
+ let failureDetails: AiFailureDetails | undefined;
230
+ let completionReason: string | null = null;
231
+ let toolCount = 0, searchCount = input.preparation?.searchCount ?? 0, outputTokens = 0, answer = "", failure: string | null = null, contractFailure = false;
232
+ // Legacy preparation remains accepted; known retained records establish only
233
+ // their own completed outcomes, never an assumed success for missing records.
234
+ let searchesSucceeded = input.preparation?.searchesSucceeded ?? (input.preparation?.result ? 1 : input.preparation?.rawRecords?.length ?? 0);
235
+ let searchesFailed = input.preparation?.searchesFailed ?? (input.preparation?.failure ? input.preparation.searchCount : 0);
236
+ // Count engine dispatches for diagnostics; cancellation/time, not a count quota, gates them.
237
+ const execution = { reserveAdditionalSearch: () => {
238
+ controller.signal.throwIfAborted();
239
+ searchCount++;
240
+ return true;
241
+ }, reserveSearch: () => {
242
+ controller.signal.throwIfAborted();
243
+ searchCount++;
244
+ return true;
245
+ }, recordSearchOutcome: (outcome: 'succeeded' | 'failed') => {
246
+ if (outcome === 'succeeded') searchesSucceeded++; else searchesFailed++;
247
+ } };
248
+ const trace = (stage: string, direction: "input" | "output" | "error", payload: Record<string, unknown>) => emitObservableTrace(input.observableTraceSink,
249
+ { stage, direction, model: input.modelName, providerRoute: input.providerRoute ?? "model-turn", reasoningEffort: input.effort ?? "medium", payload });
250
+ if (input.preparation) trace("simple-preparation", "output", { ...input.preparation });
251
+ const source = input.openingSourceContext;
252
+ const openingSourceContext = (!omitSelectedContext || source?.scope === 'game-review') && source?.status === "available" && source.contextHash === contextHash && source.pages.length
253
+ ? { authority: "untrusted-opening-commentary", contextHash, pages: source.pages, limitations: source.limitations }
254
+ : undefined;
255
+ if (source) trace("simple-opening-source", "output", { ...source, injected: Boolean(openingSourceContext) });
256
+ const withOpeningSource = (chessResult: unknown, openingSourceContext: unknown) => openingSourceContext ? { chessResult, openingSourceContext } : chessResult;
257
+ const deliveredSources = new Set(openingSourceContext?.pages.map(page => `${page.title}@${page.revisionId}`) ?? []);
258
+ const knownOpeningPages=new Map((openingSourceContext?.pages??[]).map(page=>[openingSourceKey(page),page]));
259
+ const openingSources=new Map<string,OpeningSourceCredit>();
260
+ const theoryForTool = async (name: string, args: Record<string, unknown>, result: import("./poc-contracts").ReasoningToolResult) => {
261
+ if (!input.openingSourceLookup || controller.signal.aborted) return undefined;
262
+ const sourceStarted = performance.now();
263
+ try {
264
+ if (!input.openingIndex) return undefined;
265
+ const catalog = await input.openingIndex();
266
+ if (controller.signal.aborted) return undefined;
267
+ const candidates = wikibooksTargetsForToolIndexed(input.bundle.context, name, args, result, catalog);
268
+ if (!candidates.candidateCount) return undefined;
269
+ const targets = candidates.targets;
270
+ const root = isRecordedPositionTool(name) ? recordedEvidenceContext(input.bundle.context, result.data.recordedPosition) : input.bundle.context;
271
+ const source: WikibooksContextResult = targets.length ? await input.openingSourceLookup(targets, controller.signal, root) : {
272
+ status:'unavailable',contextHash:reasoningContextHash(root),pages:[],requestCount:0,networkRequestCount:0,
273
+ networkRequestedTitles:[],localIndexCandidateCount:0,localIndexPositionCheckCount:0,
274
+ localIndexSelectedTitleCount:0,cacheHitTitleCount:0,timingMs:0,
275
+ limitations:[`No indexed usable prose at candidate positions or their two actual-path ancestors; no titles built and no MediaWiki request. Catalog: ${catalog.at}.`],
276
+ };
277
+ const diagnosedSource = {...source,timingMs:Math.round(performance.now()-sourceStarted),localIndexPositionCheckCount:(source.localIndexPositionCheckCount ?? 0)+candidates.positionChecks};
278
+ if (controller.signal.aborted) return undefined;
279
+ if (source.contextHash !== reasoningContextHash(root)) throw Error("stale-opening-source");
280
+ const pages: WikibooksContextResult["pages"] = [], alreadyPresented: Array<Pick<WikibooksContextResult["pages"][number], "title" | "revisionId" | "positionFen" | "relation" | "matchedTargets" | "requestedTitle" | "ancestorPlies" | "transposition">> = [];
281
+ if (source.status === "available") for (const page of source.pages) {
282
+ const key = `${page.title}@${page.revisionId}`;
283
+ if (deliveredSources.has(key)) alreadyPresented.push({title:page.title,revisionId:page.revisionId,positionFen:page.positionFen,relation:page.relation,...(page.matchedTargets ? {matchedTargets:page.matchedTargets,requestedTitle:page.requestedTitle,ancestorPlies:page.ancestorPlies,transposition:page.transposition} : {})});
284
+ else { pages.push(page); deliveredSources.add(key); knownOpeningPages.set(key,page); }
285
+ }
286
+ const packet = { ...diagnosedSource, authority: "untrusted-opening-commentary", pages, alreadyPresented };
287
+ trace("simple-opening-source", "output", { tool: name, callId: result.callId, lookupMode: 'local-fen-index-then-network', candidatePositionCount:candidates.candidateCount, selectedTitleCount:targets.length, selectedTitleSample:targets.slice(0,3).map(t=>t.title.slice(0,180)), selectedTitleSampleTruncated:targets.length>3||targets.some(t=>t.title.length>180), ...packet, injected: pages.length > 0, reused: alreadyPresented.length });
288
+ return packet;
289
+ } catch (error) {
290
+ trace("simple-opening-source", "error", { tool: name, callId: result.callId, status: controller.signal.aborted ? "aborted" : "unavailable", error: error instanceof Error ? error.message : "source-unavailable" });
291
+ return undefined;
292
+ }
293
+ };
294
+ const toolInputGuard=createToolInputLoopGuard();
295
+ const initialSearches: Array<Record<string,unknown>> = [];
296
+ const initialSearchPresentations: PreparedPresentation[] = [];
297
+ const preparationDependencies=new Map<InitialSearchPlan["requests"][number],string>();
298
+ const initialPreparedEvidenceIds:string[]=[];
299
+ let deferredPreparationRequests=0;
300
+ if (!fixedEvidence && input.initialSearchPlan) {
301
+ const descriptors=new Map(input.host.descriptors().map(d=>[d.name,d]));
302
+ const preparationQueue=[...input.initialSearchPlan.requests];
303
+ for(const [index,request] of preparationQueue.entries()) {
304
+ const callId=`initial-search-${index+1}`, start=performance.now();
305
+ const run:Record<string,unknown>={origin:'automatic-preparation',callId,...request};
306
+ let preparedModelResult: unknown;
307
+ let preparedRecords: ReasoningEvidenceRecord[]=[],preparedLimitations:string[]=[];
308
+ const playedMove = request.playedMove ?? (Number.isInteger(request.input.ply) ? input.initialMainLine?.moves[Number(request.input.ply)]?.uci : request.input.phase === 'before' && input.bundle.context.kind !== 'position' ? input.bundle.context.moveUci : undefined);
309
+ try {
310
+ controller.signal.throwIfAborted();
311
+ if(timeToFinish()) {deferredPreparationRequests=preparationQueue.length-index;break;}
312
+ const descriptor=descriptors.get(request.tool);if(!descriptor)throw Error('tool-unavailable');
313
+ if(omitSelectedContext&&!isRecordedPositionTool(request.tool))throw Error('selected-context-not-loaded');
314
+ validateSimpleToolInput(request.tool,request.input,descriptor.inputSchema);
315
+ input.onProgress?.('Preparing relevant engine comparisons');
316
+ const result=await input.host.execute(request.tool,{...request.input,callId,contextHash},controller.signal,execution);
317
+ controller.signal.throwIfAborted();
318
+ if(result.contextHash!==contextHash||result.callId!==callId||result.tool!==request.tool)throw Error('cross-context-tool-result');
319
+ for(const e of result.evidence){if(e.contextHash!==contextHash||!e.id)throw Error('cross-context-evidence');effectiveLicensesForRecord(e);const old=known.get(e.id);if(old&&JSON.stringify(old)!==JSON.stringify(e))throw Error('evidence-identity-conflict');}
320
+ for(const e of result.evidence){known.set(e.id,e);visibleIds.add(e.id);initialPreparedEvidenceIds.push(e.id);}
321
+ if(request.playedMove && request.tool==='analyze_game_position') {
322
+ const comparison=preparedMoveComparison(request, result.evidence);
323
+ if(comparison){preparationDependencies.set(comparison,callId);preparationQueue.splice(index+1,0,comparison);}
324
+ }
325
+ const openingSource = await theoryForTool(request.tool, request.input, result);
326
+ controller.signal.throwIfAborted();
327
+ preparedRecords=result.evidence;preparedLimitations=result.limitations;
328
+ preparedModelResult = withOpeningSource(compactPreparedSearchResult(result, playedMove, input.materialFormat), openingSource);
329
+ Object.assign(run,{status:'completed',result:withOpeningSource(compactSimpleEngineResult({contextHash,evidence:result.evidence,limitations:result.limitations},input.materialFormat), openingSource)});
330
+ } catch(error) { if(controller.signal.aborted){input.signal?.removeEventListener('abort',abort);throw error;}Object.assign(run,{status:'failed',...toolError(error)}); }
331
+ run.elapsedMs=performance.now()-start;initialSearches.push(run);
332
+ initialSearchPresentations.push({callId,modelRun:preparedModelResult === undefined ? {...run} : {...run,result:preparedModelResult},records:preparedRecords,limitations:preparedLimitations,playedMove,discoveryCallId:preparationDependencies.get(request)});
333
+ trace('simple-initial-search','output',run);
334
+ }
335
+ }
336
+ const consolidatedPreparation=consolidatePreparedSearches(initialSearchPresentations);
337
+ if(input.initialSearchPlan)trace('simple-preparation-consolidation','output',consolidatedPreparation.diagnostics);
338
+ if(initialSearches.some(r=>!isRecordedPositionTool(String(r.tool))) && !omitSelectedContext) {
339
+ // Fresh root searches can change canonical classifications. Never send a stale
340
+ // pre-search snapshot alongside newer engine evidence.
341
+ if(input.boardAnalysis)visibleIds.delete(input.boardAnalysis.record.id);
342
+ input.boardAnalysis=undefined;
343
+ try { input.boardAnalysis=await input.refreshPreparedBoardAnalysis?.(controller.signal); }
344
+ catch(error) { if(controller.signal.aborted){input.signal?.removeEventListener('abort',abort);throw error;}trace('simple-initial-search','error',{stage:'refresh-board-analysis',...toolError(error)}); }
345
+ if(input.boardAnalysis){if(input.boardAnalysis.record.contextHash!==contextHash){input.signal?.removeEventListener('abort',abort);throw Error('stale-board-analysis');}known.set(input.boardAnalysis.record.id,input.boardAnalysis.record);visibleIds.add(input.boardAnalysis.record.id);}
346
+ }
347
+ // The historical label has independent provenance; never substitute it for a
348
+ // freshly refreshed canonical badge or expose it for an omitted selection.
349
+ const classificationReview = input.initialSearchPlan?.classificationReassessment && !omitSelectedContext
350
+ ? {...classificationReassessment(previousClassificationAnalysis, input.boardAnalysis), searchStatus: initialSearches[0]?.status ?? 'not-run'} : undefined;
351
+ if(classificationReview?.labelChanged && previousClassificationAnalysis) visibleIds.add(previousClassificationAnalysis.record.id);
352
+ const recordedClassification=!input.boardAnalysis?.snapshot?.classification ? input.recordedSelectedClassification : undefined;
353
+ if(recordedClassification?.record.contextHash && recordedClassification.record.contextHash!==contextHash)throw Error('stale-recorded-selected-classification');
354
+ if(recordedClassification && !omitSelectedContext) {
355
+ known.set(recordedClassification.record.id,recordedClassification.record);visibleIds.add(recordedClassification.record.id);
356
+ }
357
+ const playerPerspective = input.playerPerspective;
358
+ const planningInitial = coverageEnabled ? {...initial, evidence:planningEvidence({contextHash,evidence:[...initial.evidence],limitations:[]}).evidence} : initial;
359
+ const selectedEvidence = () => ({...simpleInitialEvidenceView(planningInitial,input.bundle,budgets,Boolean(input.boardAnalysis),input.materialFormat),
360
+ ...(selectedMechanismView?{selectedMoveMechanism:selectedMechanismView}:{}),
361
+ ...(input.boardAnalysis?{boardAnalysis:input.boardAnalysis.snapshot}:{}),
362
+ ...(classificationReview?{classificationReassessment:classificationReview}:{}),
363
+ ...(recordedClassification?{recordedSelectedClassification:recordedClassification.snapshot}:{}),
364
+ ...(coverageEnabled?{legalInvestigationRoots:legalInvestigationRoots(input.bundle.context)}:{}),
365
+ ...(input.preparation?.investigationLeads?{investigationLeads:input.preparation.investigationLeads}:{}),
366
+ ...(openingSourceContext?{openingSourceContext}:{}),
367
+ ...(input.preparation?{preparation:{searchesUsed:input.preparation.searchCount,searchesSucceeded,searchesFailed,limitation:'Prepared continuations are bounded evidence, not a purpose inference.'}}:{})});
368
+ let gameView:Record<string,unknown>|undefined;
369
+ if(initialGameRecord){
370
+ const record=Object.fromEntries(Object.entries(initialGameRecord.recordedMainLine).filter(([key])=>!["selectedIndex","selectedAtStart","selectedOnMainLine"].includes(key)));
371
+ gameView={recordedMainLine:omitSelectedContext?record:initialGameRecord.recordedMainLine,
372
+ ...('recordedAnalysis' in initialGameRecord?{recordedAnalysis:initialGameRecord.recordedAnalysis}:{}),
373
+ complete:!input.initialMainLine!.truncated,limitations:omitSelectedContext?["PGN is reconstructed from recorded moves; original headers, clocks and result metadata are not included.","Recorded primary-line play, not proof of best play or intent. This is not an arbitrary branch continuation."]:initialGameRecord.limitations};
374
+ if(!omitSelectedContext)for(const e of initialGameRecord.evidence){known.set(e.id,e);visibleIds.add(e.id);}
375
+ }
376
+ const standardReference=input.squareReference===false?undefined:CHESS_REFERENCE_DICTIONARY;
377
+ const currentQuestion={...(standardReference?{standardReference}:{}),scoreComparisonNotices:scoreComparisonNotices(visibleRecords()),scope:'CURRENT_QUESTION',...(playerPerspective?{playerPerspective}:{}),
378
+ ...(omitSelectedContext?{question:input.question??input.bundle.question,selectionContext:'omitted',budgets,
379
+ contextAvailability:'The selected move/position was not supplied. The complete recorded primary game is supplied independently of the board selection. Use get_selected_context before any selection-relative chess tool; do not guess a root or treat a historical move as the current selection.'}:selectedEvidence()),
380
+ ...(gameView?{gameRecord:gameView}:{}),
381
+ ...(openingSourceContext?{openingSourceContext}:{}),
382
+ ...(input.initialSearchPlan?{initialSearchPreparation:{...input.initialSearchPlan,results:consolidatedPreparation.results,searchesUsed:searchCount,deferredRequests:deferredPreparationRequests,completion:deferredPreparationRequests?'partial: soft deadline reached; final answer must disclose unfinished preparation':'finished; inspect individual failures',scope:'Optional bounded leads, not exhaustive review or proof of prose. Reuse supplied results; request dependent follow-ups only as useful.'}}:{}),
383
+ ...(input.routingDecision?{evidenceRouting:{version:input.routingDecision.version,selectedContext:omitSelectedContext?'omitted':'included',gameRecord:gameView?(input.initialMainLine!.truncated?'partial':'complete'):'not-initially-supplied',answerComposition}}:{evidenceRouting:{answerComposition}})};
384
+ const messages:ModelMessage[]=[...priorConversationMessages(input.conversationHistory),{role:'user',content:JSON.stringify(currentQuestion)}];
385
+ if(input.routingDecision)trace('question-evidence-plan','output',{version:input.routingDecision.version,status:input.routingDecision.status,appliedOmission:omitSelectedContext,gameRecordComplete:gameView?.complete??false,gameRecordTrigger:input.routingDecision.gameRecordTrigger,answerScope:input.routingDecision.answers?.answer_scope,answerComposition,reason:input.routingDecision.reason});
386
+
387
+ trace("simple-evidence", "output", { context: input.bundle.context, initialEvidence: omitSelectedContext?[]:initial.evidence, retainedEvidence: [...known.values()] });
388
+ const descriptors = input.host.descriptors().filter(d => (d.category === "fact" || d.category === "probe") && !(input.bundle.context.kind === "position" && ["analyze_move", "enumerate_defenses"].includes(d.name)));
389
+ const searchDescriptors = descriptors.filter(d => d.category === "probe");
390
+ const searchDescriptorByName = new Map(searchDescriptors.map(d => [d.name, d]));
391
+ const batchSearchSchema: Record<string, unknown> = { type: "object", additionalProperties: false, required: ["requests"], properties: {
392
+ requests: { type: "array", minItems: 1, maxItems: budgets.maxSearchesPerBatch, items: { type: "object", additionalProperties: false,
393
+ required: ["tool", "input"], properties: {
394
+ queryId: { type: "string", description: "Optional correlation ID. Invalid, missing, and repeated values are assigned a unique query ID with an explicit mapping." },
395
+ tool: { type: "string", enum: searchDescriptors.map(d => d.name) },
396
+ input: { type: "object" },
397
+ } },
398
+ },
399
+ } };
400
+ const retrievalSchema: Record<string, unknown> = { type: "object", additionalProperties: false, required: ["evidenceIds"], properties: {
401
+ evidenceIds: { type: "array", minItems: 1, maxItems: 8, items: { type: "string", maxLength: 300 } } } };
402
+ const definitions = [...(omitSelectedContext?[{name:"get_selected_context",description:input.policy?.description("get_selected_context"),inputSchema:{type:"object",additionalProperties:false,properties:{}}}]:[]),...(coverageEnabled ? [{name:'explanation_review',description:input.policy?.description('explanation_review'),inputSchema:EXPLANATION_REVIEW_SCHEMA}] : []), ...(input.getMainLine && (!input.initialMainLine || input.initialMainLine.truncated) ? [{ name: "get_main_line", description: input.policy?.description("get_main_line"), inputSchema: { type: "object", additionalProperties: false, properties: {} } }] : []), ...descriptors.map(d=>({...d,description:input.policy?.description(d.name),inputSchema:["compare_moves","enumerate_defenses"].includes(d.name)?{...d.inputSchema,properties:{...(d.inputSchema.properties as Record<string,unknown>),[d.name==="compare_moves"?"movesUci":"rootMoves"]:{...((d.inputSchema.properties as Record<string,Record<string,unknown>>)[d.name==="compare_moves"?"movesUci":"rootMoves"]),maxItems:budgets.maxRootMoves}}}:d.inputSchema})),
403
+ ...(searchDescriptors.length ? [{ name: "search_batch", description: input.policy?.description("search_batch"), inputSchema: batchSearchSchema }] : []),
404
+ { name: "retrieve_evidence", description: input.policy?.description("retrieve_evidence"), inputSchema: retrievalSchema }];
405
+ const tools = Object.fromEntries(definitions.map(d => [d.name, tool({ description: d.description,
406
+ inputSchema: jsonSchema<Record<string, unknown>>(d.inputSchema), execute: async (args, options) => {
407
+ if (controller.signal.aborted) throw new Error("cancelled");
408
+ if (timeToFinish()) return {error:"investigation-closed",evidence:[],limitation:"Time to summarize acquired evidence. This request was not executed."};
409
+ if(d.name==='explanation_review')reviewCount++;else toolCount += 1;
410
+ const run: Record<string, unknown> = { tool: d.name, input: args, callId: options.toolCallId, status: "started" };
411
+ toolRuns.push(run); trace("simple-tool", "input", run);
412
+ const start = performance.now();
413
+ let admittingEvidence = false;
414
+ try {
415
+ toolInputGuard.validate(d.name,args,()=>validateSimpleToolInput(d.name, args, d.inputSchema));
416
+ if(d.name==='get_selected_context') {
417
+ selectedContextAvailable=true;
418
+ if(selectedContextRecord)visibleIds.add(selectedContextRecord.id);
419
+ for(const e of initial.evidence)visibleIds.add(e.id);
420
+ if(input.boardAnalysis)visibleIds.add(input.boardAnalysis.record.id);
421
+ if(recordedClassification){known.set(recordedClassification.record.id,recordedClassification.record);visibleIds.add(recordedClassification.record.id);}
422
+ const result={selectionContext:'included',...selectedEvidence()};
423
+ Object.assign(run,{status:'completed',result,elapsedMs:performance.now()-start});trace('simple-tool','output',run);return result;
424
+ }
425
+ if(!selectedContextAvailable && !['get_main_line','search_batch','retrieve_evidence'].includes(d.name) && !isRecordedPositionTool(d.name))
426
+ throw inputError('selected-context-not-loaded','context','Call get_selected_context before selection-relative chess tools. Historical game positions are not automatically the selected root.');
427
+ if(d.name==='explanation_review') {
428
+ review=validateExplanationReview(args,input.bundle.context,known);
429
+ const newlyPresented=review.evidenceIds.filter(id=>!visibleIds.has(id)).map(id=>known.get(id)!);
430
+ for(const e of newlyPresented)visibleIds.add(e.id);
431
+ const result={supportingEvidence:planningEvidence({contextHash,evidence:newlyPresented,limitations:[]}),coverage:explanationCoverage(input.bundle.context,visibleRecords(),review),recurring:recurringForTurn()};
432
+ Object.assign(run,{status:'completed',result,elapsedMs:performance.now()-start});trace('simple-tool','output',run);return result;
433
+ }
434
+ if (d.name === "search_batch") {
435
+ const requests = args.requests as Array<{ queryId?: unknown; tool?: unknown; input?: unknown }>;
436
+ const queryIds = new Set<string>();
437
+ const reservedIds=new Set(requests.map(r=>r && typeof r==='object' && typeof r.queryId==='string' && /^[A-Za-z0-9_-]{1,48}$/.test(r.queryId)?r.queryId:null).filter((id):id is string=>Boolean(id)));
438
+ const queryIdRepairs: Array<{index:number;requestedQueryId:unknown;queryId:string;reason:string}>=[];
439
+ input.onProgress?.(`Checking ${requests.length} chess searches`);
440
+ const searches: Record<string, unknown>[] = [];
441
+ // The browser host owns one Stockfish worker. Keep engine execution
442
+ // ordered while eliminating extra model round trips around known probes.
443
+ for (const [index, request] of requests.entries()) {
444
+ const suppliedId=request && typeof request==='object' ? request.queryId : undefined;
445
+ const validId=typeof suppliedId==='string' && /^[A-Za-z0-9_-]{1,48}$/.test(suppliedId);
446
+ let queryId=validId && !queryIds.has(suppliedId) ? suppliedId : `batch_${index+1}`;
447
+ if(!validId || queryIds.has(suppliedId as string)){
448
+ while(reservedIds.has(queryId) || queryIds.has(queryId))queryId=`${queryId}_`;
449
+ queryIdRepairs.push({index,requestedQueryId:suppliedId??null,queryId,reason:validId?'duplicate':'missing-or-invalid'});
450
+ }
451
+ queryIds.add(queryId);
452
+ const toolName = request && typeof request==='object' ? request.tool as ReasoningToolName : undefined;
453
+ const callId = `${options.toolCallId}/${queryId}`;
454
+ const searchStart = performance.now();
455
+ let validatingHostResult = false;
456
+ try {
457
+ if(!request || typeof request!=='object' || Array.isArray(request))throw inputError('invalid-batch-search',`requests[${index}]`,'object with tool and input');
458
+ const unknown=Object.keys(request).find(key=>!["queryId","tool","input"].includes(key));
459
+ if(unknown)throw inputError('unknown-batch-search-field',`requests[${index}].${unknown}`,'queryId, tool or input');
460
+ if(typeof toolName!=='string'||!searchDescriptorByName.has(toolName))throw inputError('invalid-batch-search-tool',`requests[${index}].tool`,'available search tool',undefined,[...searchDescriptorByName.keys()]);
461
+ if(!request.input || typeof request.input!=='object' || Array.isArray(request.input))throw inputError('invalid-batch-search-input',`requests[${index}].input`,'object matching selected tool schema');
462
+ toolInputGuard.validate(toolName,request.input as Record<string,unknown>,()=>validateSimpleToolInput(toolName,request.input as Record<string,unknown>,searchDescriptorByName.get(toolName)!.inputSchema));
463
+ if(!selectedContextAvailable && !isRecordedPositionTool(toolName))throw inputError('selected-context-not-loaded','context','Call get_selected_context before selection-relative tools.');
464
+ if(timeToFinish()) throw Error("investigation-closed: summarize acquired evidence");
465
+ if(input.host.reservesSearchAtDispatch!==true && !(['test_line','test_game_line'].includes(toolName) && (request.input as {analyzeFinal?:unknown}).analyzeFinal!==true))execution.reserveSearch();
466
+ const result = await input.host.execute(toolName, { ...(request.input as Record<string, unknown>), callId, contextHash }, controller.signal, execution);
467
+ validatingHostResult = true;
468
+ if (result.contextHash !== contextHash || result.callId !== callId || result.tool !== toolName) throw new Error("cross-context-tool-result");
469
+ if (controller.signal.aborted) throw new Error("cancelled");
470
+ for (const e of result.evidence) {
471
+ if (e.contextHash !== contextHash || !e.id) throw new Error("cross-context-evidence");
472
+ effectiveLicensesForRecord(e);
473
+ const old = known.get(e.id);
474
+ if (old && JSON.stringify(old) !== JSON.stringify(e)) throw new Error("evidence-identity-conflict");
475
+ }
476
+ for (const e of result.evidence) { known.set(e.id, e); visibleIds.add(e.id); }
477
+ const openingSourceContext = await theoryForTool(toolName, request.input as Record<string, unknown>, result);
478
+ if (controller.signal.aborted) throw new Error("cancelled");
479
+ const compact = { contextHash, evidence: result.evidence, limitations: result.limitations, scoreComparisonNotices:scoreComparisonNotices(visibleRecords()).filter(n=>n.observations.some(o=>result.evidence.some(e=>e.id===o.evidenceId))) };
480
+ const full = compactSimpleEngineResult(coverageEnabled ? planningEvidence(compact) : compact,input.materialFormat);
481
+ const reused = coverageEnabled ? full : compactSimpleEngineResult(delivery.project(callId, projectEnginePresentation(compact) as typeof compact),input.materialFormat);
482
+ searches.push({ queryId, tool: toolName, input: request.input, callId, status: "completed",
483
+ result: withOpeningSource(JSON.stringify(reused).length < JSON.stringify(full).length ? reused : full, openingSourceContext), elapsedMs: performance.now() - searchStart });
484
+ } catch (error) {
485
+ if (validatingHostResult && !controller.signal.aborted) contractFailure = true;
486
+ const detail=toolError(error);
487
+ searches.push({ queryId, tool: toolName, input: request && typeof request==='object' ? request.input : undefined, callId, status: "failed", ...detail,
488
+ result: { ...detail, evidence: [], limitation: "No evidence acquired by this failed search." }, elapsedMs: performance.now() - searchStart });
489
+ }
490
+ }
491
+ const batched = { contextHash, searches, ...(queryIdRepairs.length?{queryIdRepairs}:{}), ...(coverageEnabled?{investigation:explanationCoverage(input.bundle.context,visibleRecords(),review),recurring:recurringForTurn()}:{}) };
492
+ Object.assign(run, { status: "completed", result: batched, elapsedMs: performance.now() - start });
493
+ trace("simple-tool", "output", run);
494
+ return batched;
495
+ }
496
+ input.onProgress?.(d.name === "retrieve_evidence" ? "Retrieving chess evidence" : `Checking ${d.name.replaceAll("_", " ")}`);
497
+ let result;
498
+ if (d.name === "get_main_line") {
499
+ result = recordedMainLineResult(await input.getMainLine!(controller.signal), input.bundle.context);
500
+ acquiredMainLines.push(result.recordedMainLine);
501
+ } else if (d.name === "retrieve_evidence") {
502
+ const ids = args.evidenceIds as string[];
503
+ if(!selectedContextAvailable&&ids.some(id=>!visibleIds.has(id)))throw inputError('selected-context-not-loaded','context','Only already-presented evidence is available before get_selected_context.');
504
+ const unknown=ids.find(id=>!known.has(id)||(!selectedContextAvailable&&!visibleIds.has(id)));
505
+ if (unknown) throw inputError('unknown-evidence-id','evidenceIds',`known evidence ID; ${unknown.slice(0,80)} is unavailable`,undefined,[...known.keys()].slice(0,32));
506
+ result = { contextHash, evidence: ids.map(id => known.get(id)!), limitations: ["Retrieved records retain their original scope and authority."] };
507
+ } else {
508
+ if (searchDescriptorByName.has(d.name as ReasoningToolName) && input.host.reservesSearchAtDispatch!==true && !(['test_line','test_game_line'].includes(d.name) && args.analyzeFinal!==true))execution.reserveSearch();
509
+ result = await input.host.execute(d.name as ReasoningToolName, { ...args, callId: options.toolCallId, contextHash }, controller.signal, execution);
510
+ admittingEvidence = true;
511
+ if (result.contextHash !== contextHash || result.callId !== options.toolCallId || result.tool !== d.name) throw new Error("cross-context-tool-result");
512
+ }
513
+ if (controller.signal.aborted) throw new Error("cancelled");
514
+ admittingEvidence = true;
515
+ for (const e of result.evidence) {
516
+ if (e.contextHash !== contextHash || !e.id) throw new Error("cross-context-evidence");
517
+ effectiveLicensesForRecord(e);
518
+ const old = known.get(e.id);
519
+ if (old && JSON.stringify(old) !== JSON.stringify(e)) throw new Error("evidence-identity-conflict");
520
+ }
521
+ for (const e of result.evidence) { known.set(e.id, e); visibleIds.add(e.id); }
522
+ // Avoid repeating the data already carried by complete evidence records.
523
+ const compact = { contextHash, evidence: result.evidence, limitations: result.limitations, scoreComparisonNotices:scoreComparisonNotices(visibleRecords()).filter(n=>n.observations.some(o=>result.evidence.some(e=>e.id===o.evidenceId))) };
524
+ if ("recordedMainLine" in result) {
525
+ const recorded = { ...compact, recordedMainLine: result.recordedMainLine, ...("recordedAnalysis" in result?{recordedAnalysis:result.recordedAnalysis}:{}) };
526
+ Object.assign(run, { status: "completed", result: recorded, elapsedMs: performance.now() - start });
527
+ trace("simple-tool", "output", run); return recorded;
528
+ }
529
+ const openingSourceContext = "tool" in result ? await theoryForTool(d.name, args, result) : undefined;
530
+ if (controller.signal.aborted) throw new Error("cancelled");
531
+ Object.assign(run, { status: "completed", result: { ...compact, ...(openingSourceContext ? {openingSourceContext} : {}) }, elapsedMs: performance.now() - start });
532
+ trace("simple-tool", "output", run);
533
+ const full = compactSimpleEngineResult(coverageEnabled ? planningEvidence(compact) : compact,input.materialFormat);
534
+ const reused = coverageEnabled ? full : compactSimpleEngineResult(delivery.project(options.toolCallId, projectEnginePresentation(compact) as typeof compact),input.materialFormat);
535
+ if(d.name==='retrieve_evidence')return compactSimpleEngineResult(compact,input.materialFormat,true);
536
+ const projected=withOpeningSource(JSON.stringify(reused).length < JSON.stringify(full).length ? reused : full, openingSourceContext);
537
+ return coverageEnabled ? {result:projected,investigation:explanationCoverage(input.bundle.context,visibleRecords(),review),recurring:recurringForTurn()}:projected;
538
+ } catch (error) {
539
+ if (admittingEvidence && !controller.signal.aborted) contractFailure = true;
540
+ const detail=toolError(error);
541
+ Object.assign(run, { status: "failed", ...detail, elapsedMs: performance.now() - start });
542
+ trace("simple-tool", "error", run); return { ...detail, evidence: [], limitation: "No evidence acquired by this failed request." };
543
+ } finally { input.onProgress?.("Reasoning about the chess evidence"); }
544
+ } })]));
545
+ try {
546
+ for (let step = 0; step < (fixedEvidence ? 1 : SIMPLE_BUDGETS.maxModelCalls); step += 1) {
547
+ if (controller.signal.aborted) throw new Error("cancelled-or-time-budget");
548
+ const remaining = budgets.maxOutputTokens - outputTokens;
549
+ let final = toolInputGuard.repeatedFailures>0 || fixedEvidence || step === budgets.maxModelCalls - 1 || timeToFinish() || remaining <= budgets.maxOutputTokensPerCall;
550
+ if(final && !fixedEvidence) completionReason = toolInputGuard.repeatedFailures>0 ? 'repeated-invalid-tool-input' : timeToFinish() ? 'time' : remaining <= budgets.maxOutputTokensPerCall ? 'output' : 'model-calls';
551
+ if(coverageEnabled && final)messages.push({role:'user',content:JSON.stringify({investigationLimit:explanationCoverage(input.bundle.context,visibleRecords(),review),instruction:'Answer now from acquired evidence. Explicitly narrow any materially unresolved mechanism; do not present incomplete coverage as established.'})});
552
+ let modelMessages = input.evidenceCompaction === 'baseline' ? messages : shareEvidenceMessages(messages);
553
+ let sharedEvidence = input.evidenceCompaction !== 'baseline' && hasSharedEvidence(modelMessages);
554
+ let encoded = JSON.stringify(modelMessages).includes('simple-engine-evidence/1');
555
+ let reusedFacts = JSON.stringify(messages).includes('"$priorFact"');
556
+ let system = input.policy?.system({encoded,reusedFacts,sharedEvidence,final,purposeGuidance:input.purposeGuidance,searchBreadthGuidance:input.searchBreadthGuidance,boardAnalysisGuidance:input.boardAnalysisGuidance,explanationCoverage:coverageEnabled,answerComposition});
557
+ const requestBytes = new TextEncoder().encode((system ?? "") + JSON.stringify(modelMessages) + JSON.stringify(final ? [] : definitions)).length;
558
+ if (requestBytes > budgets.maxRequestBytes * 0.75) {
559
+ final = true; completionReason = 'context';
560
+ modelMessages = [{role:'user',content:JSON.stringify(completionPacket(input.question ?? input.bundle.question, visibleRecords(), gameView, omitSelectedContext ? undefined : input.bundle.context, {...(standardReference?{standardReference}:{}),playerPerspective:input.playerPerspective, selectionContext:omitSelectedContext ? "omitted" : "included", evidenceRouting:{answerComposition}, ...(coverageEnabled ? {investigation:explanationCoverage(input.bundle.context,visibleRecords(),review)} : {})}))}];
561
+ sharedEvidence=false; encoded=false; reusedFacts=false;
562
+ system=input.policy?.system({encoded,reusedFacts,sharedEvidence,final:true,purposeGuidance:input.purposeGuidance,answerComposition});
563
+ }
564
+ trace('simple-request-budget','output',{step:step+1,preCompletionBytes:requestBytes,
565
+ sentEstimatedBytes:new TextEncoder().encode((system??'')+JSON.stringify(modelMessages)+JSON.stringify(final?[]:definitions)).length,
566
+ maxRequestBytes:budgets.maxRequestBytes,contextThresholdBytes:budgets.maxRequestBytes*0.75,
567
+ originalMessageCount:messages.length,sentMessageCount:modelMessages.length,
568
+ historyReplaced:completionReason==='context',completionReason:completionReason??null,final,
569
+ ...(completionReason==='context'?{completionOmissions:(()=>{const p=JSON.parse(String(modelMessages[0].content));return {omittedRecords:p.omittedRecords,omittedEvidenceIds:p.omittedEvidenceIds,omittedGameRecord:p.omittedGameRecord,omittedContext:p.omittedContext};})()}: {})});
570
+ if (new TextEncoder().encode((system ?? "") + JSON.stringify(modelMessages)).length > budgets.maxRequestBytes) {
571
+ completionReason='context';answer=evidenceFallback(visibleRecords(),searchCount,'',input.question ?? input.bundle.question,input.bundle.context,selectedContextAvailable);break;
572
+ }
573
+ // The final allowance is reserved before investigation can consume it.
574
+ const maxOutputTokens = final ? budgets.maxOutputTokensPerCall : Math.min(budgets.maxOutputTokensPerCall, Math.max(1,remaining-budgets.maxOutputTokensPerCall));
575
+ if(final) input.onProgress?.("Summarizing the evidence collected");
576
+ trace("simple-model", "input", { system: system ?? "Server-owned policy: simple-policy/1", messages: observableTraceMessages(modelMessages), tools: final ? [] : definitions,
577
+ maxOutputTokens, toolChoice: final ? "none" : "auto", step, reasoning: { effort: input.effort ?? "medium", exclude: false } });
578
+ const start = performance.now();
579
+ const suppliedOpeningSources=openingSourcesInMessages(modelMessages,knownOpeningPages);
580
+ const result = await invokeAnswerTurn(input.connector, { system, messages: modelMessages, tools: final ? undefined : tools,
581
+ providerOptions: { openai: { store: false, include: ["reasoning.encrypted_content"], reasoningEffort: input.effort ?? "medium" } },
582
+ toolChoice: final ? "none" : "auto", maxOutputTokens, maxRetries: 0, abortSignal: controller.signal,
583
+ ...(fixedEvidence ? {fixedEvidenceDiscardTools:tools} : {}) });
584
+ for(const credit of suppliedOpeningSources)openingSources.set(openingSourceKey(credit),credit);
585
+ const reportedOutput = subagentEvaluation ? undefined : result.usage.outputTokens;
586
+ const inputUsage=result.usage.inputTokens;
587
+ if(typeof inputUsage === "number" && Number.isFinite(inputUsage) && inputUsage>=0)reportedInputTokens+=inputUsage;else unmeteredInputCalls++;
588
+ const outputReported = typeof reportedOutput === "number" && Number.isFinite(reportedOutput) && reportedOutput >= 0;
589
+ if(outputReported)reportedOutputTokens+=reportedOutput;else {unmeteredCalls++;outputBudgetReservation+=maxOutputTokens;}
590
+ const diagnostic = { stage: "simple-model", inputTokens: result.usage.inputTokens || 0,
591
+ outputTokens: typeof reportedOutput === "number" && Number.isFinite(reportedOutput) && reportedOutput >= 0 ? reportedOutput : maxOutputTokens,
592
+ latencyMs: performance.now() - start, finishReason: result.finishReason, ...(!outputReported ? {metering:"unavailable" as const, outputBudgetReservation:maxOutputTokens} : {}) };
593
+ diagnostics.push(diagnostic); outputTokens += diagnostic.outputTokens;
594
+ if (contractFailure) { failure = "tool-evidence-contract-failed"; break; }
595
+ trace("simple-model", "output", { ...diagnostic, text: result.text, toolCalls: result.toolCalls,
596
+ messages: observableTraceMessages(result.response.messages), ...simpleUsageForTrace(result.usage,result.providerMetadata,result.response.headers) });
597
+ // Preserve provider-owned metadata unchanged in memory, never reconstruct it
598
+ // from the sanitized trace or persist its hidden reasoning contents.
599
+ messages.push(...result.response.messages);
600
+ delivery.commit();
601
+ if (!result.toolCalls.length) {
602
+ answer = result.text.trim();
603
+ if (result.finishReason === "length" || !answer) {
604
+ completionReason = result.finishReason === "length" ? 'output-truncated' : 'empty-answer';
605
+ answer=evidenceFallback(visibleRecords(),searchCount,answer,input.question ?? input.bundle.question,input.bundle.context,selectedContextAvailable);
606
+ }
607
+ break;
608
+ }
609
+ }
610
+ if (!answer && !failure) { completionReason="no-final-answer";answer=evidenceFallback(visibleRecords(),searchCount,'',input.question ?? input.bundle.question,input.bundle.context,selectedContextAvailable); }
611
+ } catch (error) {
612
+ failure = controller.signal.aborted ? "cancelled-or-time-budget" : error instanceof Error && /budget/.test(error.message) ? error.message : "provider-interface-failed";
613
+ failureDetails = failureFromError(error, controller.signal.aborted ? (input.signal?.aborted ? "cancelled" : "request_timeout") : failure.replaceAll("-", "_"));
614
+ trace("simple-model", "error", { failure, error: failureDetails, ...(error && typeof error === "object" && "serviceRouting" in error && error.serviceRouting ? {serviceRouting:error.serviceRouting} : {}), completedCalls: diagnostics.length });
615
+
616
+ if (!input.signal?.aborted && !contractFailure && diagnostics.length > 0 && !actionable.has(failureDetails.code)) {
617
+ completionReason=failure; answer=evidenceFallback(visibleRecords(),searchCount,answer,input.question ?? input.bundle.question,input.bundle.context,selectedContextAvailable);
618
+ failure=null; failureDetails=undefined;
619
+ }
620
+ } finally { input.signal?.removeEventListener("abort", abort); }
621
+ // One bounded, tool-free recovery for a failed/empty/truncated response. Never
622
+ // retry account errors or cancellation. A separate single recovery slot may
623
+ // exceed the soft normal-call/output targets to deliver a final summary.
624
+ if (!failure && !input.signal?.aborted && !fixedEvidence
625
+ && ['output-truncated','empty-answer','no-final-answer','provider-interface-failed'].includes(completionReason ?? '')) {
626
+ const recoveryMessages:ModelMessage[]=[{role:'user',content:JSON.stringify(completionPacket(input.question ?? input.bundle.question,visibleRecords(),gameView,omitSelectedContext?undefined:input.bundle.context,{...(standardReference?{standardReference}:{}),playerPerspective:input.playerPerspective,selectionContext:omitSelectedContext?'omitted':'included',evidenceRouting:{answerComposition}}))}];
627
+ const system=input.policy?.system({encoded:false,reusedFacts:false,final:true,answerComposition});
628
+ if(new TextEncoder().encode((system??'')+JSON.stringify(recoveryMessages)).length <= budgets.maxRequestBytes) {
629
+ input.onProgress?.('Completing a concise summary');
630
+ const start=performance.now();
631
+ trace('simple-model','input',{system:system??'Server-owned policy: simple-policy/1',messages:observableTraceMessages(recoveryMessages),tools:[],toolChoice:'none',maxOutputTokens:budgets.maxOutputTokensPerCall,recovery:true});
632
+ try {
633
+ const result=await invokeAnswerTurn(input.connector, {system,messages:recoveryMessages,toolChoice:'none',maxOutputTokens:budgets.maxOutputTokensPerCall,maxRetries:0,abortSignal:input.signal,
634
+ providerOptions:{openai:{store:false,include:['reasoning.encrypted_content'],reasoningEffort:input.effort??'medium'}}});
635
+ const recoveryInput=result.usage.inputTokens;
636
+ if(typeof recoveryInput === "number" && Number.isFinite(recoveryInput) && recoveryInput>=0)reportedInputTokens+=recoveryInput;else unmeteredInputCalls++;
637
+ const recoveryOutput=result.usage.outputTokens;
638
+ const recoveryReported=typeof recoveryOutput === "number" && Number.isFinite(recoveryOutput) && recoveryOutput>=0;
639
+ const used=recoveryReported?recoveryOutput:budgets.maxOutputTokensPerCall;
640
+ if(recoveryReported)reportedOutputTokens+=used;else{unmeteredCalls++;outputBudgetReservation+=used;}
641
+ const diagnostic={stage:'simple-model',inputTokens:result.usage.inputTokens??0,outputTokens:used,latencyMs:performance.now()-start,finishReason:result.finishReason};
642
+ diagnostics.push(diagnostic);outputTokens+=used;
643
+ trace('simple-model','output',{...diagnostic,recovery:true,text:result.text,...simpleUsageForTrace(result.usage,result.providerMetadata,result.response.headers)});
644
+ if(result.text.trim() && result.finishReason!=='length') answer=result.text.trim();
645
+ } catch(error) {
646
+ const detail=failureFromError(error);
647
+ trace('simple-model','error',{recovery:true,error:detail});
648
+ if(actionable.has(detail.code)){failure=detail.code;failureDetails=detail;answer='';}
649
+ if(input.signal?.aborted){failure='cancelled';failureDetails=failureFromError(error,'cancelled');answer='';}
650
+ }
651
+ }
652
+ }
653
+ const engineEvidenceStatus = searchesSucceeded > 0 ? 'available' as const
654
+ : searchesFailed > 0 && searchesSucceeded + searchesFailed === searchCount ? 'unavailable' as const
655
+ : searchCount > 0 ? 'unknown' as const : 'not-requested' as const;
656
+ const engineLimitation = 'Engine analysis was unavailable for this answer, so evaluations and tactical lines have not been verified with Stockfish.';
657
+ if (!failure && answer && engineEvidenceStatus === 'unavailable') answer = `${engineLimitation}\n\n${answer}`;
658
+ let referenceValidation: ReturnType<typeof validateAnswerReferences>['referenceValidation'] & {supportingContinuations?: ReturnType<typeof visibleSupportLines>['additions']} | {status:'unavailable';reason:string};
659
+ try {
660
+ const validated = validateAnswerReferences(answer, input.bundle.context, visibleRecords(), acquiredMainLines.at(-1));
661
+ const expanded = visibleSupportLines(validated.answer, input.bundle.context, visibleRecords(), acquiredMainLines.at(-1));
662
+ const finalValidation = expanded.additions.length
663
+ ? validateAnswerReferences(expanded.answer, input.bundle.context, visibleRecords(), acquiredMainLines.at(-1)) : validated;
664
+ answer = finalValidation.answer;
665
+ referenceValidation = {...finalValidation.referenceValidation,
666
+ repairs:validated.referenceValidation.repairs, citationRepairs:validated.referenceValidation.citationRepairs,
667
+ labelCorrections:validated.referenceValidation.labelCorrections,
668
+ notationCorrections:[...new Set([...validated.referenceValidation.notationCorrections,...finalValidation.referenceValidation.notationCorrections])],
669
+ supportingContinuations:expanded.additions};
670
+ } catch {
671
+ // Diagnostics must never discard an already completed paid answer. The
672
+ // renderer retains its own admission checks and readable fallback.
673
+ referenceValidation = {status:'unavailable',reason:'Reference validation could not complete; original answer retained.'};
674
+ }
675
+ trace('simple-reference-validation', 'output', referenceValidation);
676
+ const engineSearches = {searchesUsed:searchCount,searchesSucceeded,searchesFailed,engineEvidenceStatus};
677
+ trace('simple-engine-evidence', 'output', {...engineSearches,status:engineEvidenceStatus === 'unavailable' ? 'degraded' : engineEvidenceStatus});
678
+ if(completionReason) trace("simple-completion","output",{reason:completionReason,hasAnswer:Boolean(answer),...engineSearches,toolCallsUsed:toolCount});
679
+ return { preparationConsolidation:consolidatedPreparation.diagnostics, referenceValidation, completionReason, toolCallsUsed:toolCount, initialSearches, ...(coverageEnabled?{explanationCoverage:{...explanationCoverage(input.bundle.context,visibleRecords(),review),stopRepairs,reviewCount,qualificationRequested},recurringMotifs:explanationSummary(input.bundle.context,visibleRecords())}:{}),preparation: input.preparation, ...engineSearches, acquiredMainLines, status: failure ? "failure" as const : "diagnostic" as const, answer, failure, failureDetails, diagnostics, toolRuns,
680
+ elapsedMs: performance.now() - started + preparationMs, initialEvidenceIds: [...initialPreparedEvidenceIds,...(omitSelectedContext?[]:[...new Set([...initial.evidence.map(e => e.id),...(selectedContextRecord?[selectedContextRecord.id]:[])])]), ...(!omitSelectedContext && input.boardAnalysis ? [input.boardAnalysis.record.id] : [])], presentedEvidenceIds: [...visibleIds],
681
+ presentedEvidence: [...known.values()].filter(record => visibleIds.has(record.id)), openingSources:[...openingSources.values()],
682
+ ...(subagentEvaluation ? {evaluationTransport:{...subagentEvaluation,metering:"unavailable",outputBudgetReserved:outputTokens}} : {}),
683
+ usage: { inputTokens: diagnostics.reduce((n, d) => n + d.inputTokens, 0), outputTokens, metering:(unmeteredCalls||unmeteredInputCalls) ? "partly-unavailable" as const : "reported" as const, reportedOutputTokens:unmeteredCalls ? null : reportedOutputTokens, outputBudgetReservation, unmeteredCalls, reportedInputTokens:unmeteredInputCalls ? null : reportedInputTokens, unmeteredInputCalls }, limitations: [SIMPLE_DISCLOSURE, ...(engineEvidenceStatus === 'unavailable' ? [engineLimitation] : [])] };
684
+ }