@caupulican/pi-adaptative 0.81.9 → 0.81.13

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 (95) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/bundled-resources/skills/tool-call-repair/SKILL.md +152 -0
  3. package/dist/bundled-resources/skills/tool-call-repair/references/failure-grammar.md +128 -0
  4. package/dist/bundled-resources/skills/tool-call-repair/references/repair-catalogue.md +201 -0
  5. package/dist/bundled-resources/skills/tool-call-repair/references/text-protocol-grammar.md +141 -0
  6. package/dist/cli/list-models.d.ts.map +1 -1
  7. package/dist/cli/list-models.js +5 -0
  8. package/dist/cli/list-models.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +46 -1
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +394 -1
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/delegation/worker-actions.d.ts +2 -1
  14. package/dist/core/delegation/worker-actions.d.ts.map +1 -1
  15. package/dist/core/delegation/worker-actions.js +34 -2
  16. package/dist/core/delegation/worker-actions.js.map +1 -1
  17. package/dist/core/extensions/types.d.ts +3 -1
  18. package/dist/core/extensions/types.d.ts.map +1 -1
  19. package/dist/core/extensions/types.js.map +1 -1
  20. package/dist/core/failure-corpus.d.ts +27 -1
  21. package/dist/core/failure-corpus.d.ts.map +1 -1
  22. package/dist/core/failure-corpus.js +33 -11
  23. package/dist/core/failure-corpus.js.map +1 -1
  24. package/dist/core/model-registry.d.ts +3 -2
  25. package/dist/core/model-registry.d.ts.map +1 -1
  26. package/dist/core/model-registry.js +12 -4
  27. package/dist/core/model-registry.js.map +1 -1
  28. package/dist/core/models/adaptation-store.d.ts +79 -0
  29. package/dist/core/models/adaptation-store.d.ts.map +1 -0
  30. package/dist/core/models/adaptation-store.js +196 -0
  31. package/dist/core/models/adaptation-store.js.map +1 -0
  32. package/dist/core/session-analytics.d.ts +24 -1
  33. package/dist/core/session-analytics.d.ts.map +1 -1
  34. package/dist/core/session-analytics.js +75 -0
  35. package/dist/core/session-analytics.js.map +1 -1
  36. package/dist/core/settings-manager.d.ts +6 -0
  37. package/dist/core/settings-manager.d.ts.map +1 -1
  38. package/dist/core/settings-manager.js.map +1 -1
  39. package/dist/core/slash-commands.d.ts.map +1 -1
  40. package/dist/core/slash-commands.js +10 -0
  41. package/dist/core/slash-commands.js.map +1 -1
  42. package/dist/core/system-prompt-builder.d.ts +4 -0
  43. package/dist/core/system-prompt-builder.d.ts.map +1 -1
  44. package/dist/core/system-prompt-builder.js +8 -0
  45. package/dist/core/system-prompt-builder.js.map +1 -1
  46. package/dist/core/tool-repair-health.d.ts +3 -0
  47. package/dist/core/tool-repair-health.d.ts.map +1 -0
  48. package/dist/core/tool-repair-health.js +63 -0
  49. package/dist/core/tool-repair-health.js.map +1 -0
  50. package/dist/core/tool-repair-settings.d.ts +11 -0
  51. package/dist/core/tool-repair-settings.d.ts.map +1 -0
  52. package/dist/core/tool-repair-settings.js +18 -0
  53. package/dist/core/tool-repair-settings.js.map +1 -0
  54. package/dist/core/tools/edit.d.ts.map +1 -1
  55. package/dist/core/tools/edit.js +0 -9
  56. package/dist/core/tools/edit.js.map +1 -1
  57. package/dist/core/tools/find.d.ts.map +1 -1
  58. package/dist/core/tools/find.js +10 -7
  59. package/dist/core/tools/find.js.map +1 -1
  60. package/dist/core/tools/grep.d.ts.map +1 -1
  61. package/dist/core/tools/grep.js +15 -9
  62. package/dist/core/tools/grep.js.map +1 -1
  63. package/dist/modes/interactive/components/tool-execution.d.ts +7 -3
  64. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  65. package/dist/modes/interactive/components/tool-execution.js +21 -4
  66. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  67. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  68. package/dist/modes/interactive/external-editor.js +5 -3
  69. package/dist/modes/interactive/external-editor.js.map +1 -1
  70. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  71. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  72. package/dist/modes/interactive/interactive-mode.js +61 -7
  73. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  74. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  75. package/dist/modes/rpc/rpc-mode.js +16 -0
  76. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  77. package/dist/modes/rpc/rpc-types.d.ts +47 -1
  78. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  79. package/dist/modes/rpc/rpc-types.js.map +1 -1
  80. package/docs/index.md +1 -0
  81. package/docs/models.md +5 -1
  82. package/docs/rpc.md +75 -0
  83. package/docs/settings.md +22 -0
  84. package/docs/tool-repair.md +83 -0
  85. package/docs/usage.md +3 -0
  86. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  87. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  88. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  89. package/examples/extensions/sandbox/package-lock.json +2 -2
  90. package/examples/extensions/sandbox/package.json +1 -1
  91. package/examples/extensions/with-deps/package-lock.json +2 -2
  92. package/examples/extensions/with-deps/package.json +1 -1
  93. package/npm-shrinkwrap.json +12 -12
  94. package/package.json +6 -4
  95. package/docs/bug-ledger.md +0 -134
@@ -2,7 +2,8 @@ import { readFileSync } from "node:fs";
2
2
  import { basename, dirname, join } from "node:path";
3
3
  import { classifyFailure, compactToolResultDetailsForRetention, computeRetryDelayMs, createCustomMessage, DEFAULT_RETRY_POLICY, RetryController, sleepAbortable, withStreamIdleWatchdog, } from "@caupulican/pi-agent-core";
4
4
  import { calculateContextTokens, compact, createDeterministicCompaction, estimateContextTokens, getLatestCompactionEntry, prepareCompaction, runCompactionLoop, shouldCompact, } from "@caupulican/pi-agent-core/node";
5
- import { cleanupSessionResources, isContextOverflow, streamSimple } from "@caupulican/pi-ai";
5
+ import { cleanupSessionResources, formatToolRepairStandingRule, generateTextToolProtocolPrimer, isContextOverflow, parseTextToolCalls, streamSimple, } from "@caupulican/pi-ai";
6
+ import { Type } from "typebox";
6
7
  import { getAgentDir } from "../config.js";
7
8
  import { stripFrontmatter } from "../utils/frontmatter.js";
8
9
  import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
@@ -28,6 +29,7 @@ import { MemoryController } from "./memory-controller.js";
28
29
  import { deriveModelCapabilityProfile, filterToolNamesForCapability, } from "./model-capability.js";
29
30
  import { formatModelRouterModel, ModelRouterController } from "./model-router-controller.js";
30
31
  import { ModelSelectionController } from "./model-selection-controller.js";
32
+ import { ModelAdaptationStore } from "./models/adaptation-store.js";
31
33
  import { ProfileFilterController } from "./profile-filter-controller.js";
32
34
  import { expandPromptTemplate } from "./prompt-templates.js";
33
35
  import { ReflectionController } from "./reflection-controller.js";
@@ -39,6 +41,8 @@ import { SessionAnalytics } from "./session-analytics.js";
39
41
  import { SessionTreeNavigator } from "./session-tree-navigator.js";
40
42
  import { SystemPromptBuilder } from "./system-prompt-builder.js";
41
43
  import { ToolGateController } from "./tool-gate-controller.js";
44
+ import { formatToolRepairHealthReport } from "./tool-repair-health.js";
45
+ import { resolveCurrentToolRepairSettings } from "./tool-repair-settings.js";
42
46
  // ============================================================================
43
47
  // Stream-idle watchdog wiring
44
48
  // ============================================================================
@@ -52,6 +56,16 @@ import { ToolGateController } from "./tool-gate-controller.js";
52
56
  * key stable regardless of how many times this module is evaluated.
53
57
  */
54
58
  const RAW_STREAM_MARKER = Symbol.for("pi.rawStreamSimple");
59
+ const MODEL_ADAPTATION_REPAIR_THRESHOLD = 3;
60
+ const TEXT_TOOL_PROTOCOL_VERSION = 1;
61
+ const TEXT_TOOL_PROTOCOL_TRIALS_PER_VARIANT = 2;
62
+ const TEXT_TOOL_PROTOCOL_PARSE_FAILURE_THRESHOLD = 3;
63
+ const TEXT_TOOL_PROTOCOL_VARIANTS = ["tool-tag", "tool-call", "fenced-json"];
64
+ const TEXT_TOOL_PROTOCOL_ECHO_TOOL = {
65
+ name: "echo",
66
+ description: "Echo calibration data",
67
+ parameters: Type.Object({ data: Type.String() }),
68
+ };
55
69
  /** Test-only override of the stream-idle bounds. Read per-request by the wiring's resolver. */
56
70
  let streamIdleOptionsOverride;
57
71
  /**
@@ -133,6 +147,10 @@ export class AgentSession {
133
147
  _agentDir;
134
148
  _collectWorkspaceSources;
135
149
  _localRuntimeController;
150
+ _modelAdaptationStore;
151
+ _repairModeSessionCounts = new Map();
152
+ _textProtocolParseFailures = new Map();
153
+ _textProtocolParseObservedThisTurn = false;
136
154
  /** Assembles the session's base system prompt from live session state (see
137
155
  * system-prompt-builder.ts); owns the paired _baseSystemPromptOptions. */
138
156
  _systemPromptBuilder;
@@ -242,6 +260,9 @@ export class AgentSession {
242
260
  this._customTools = config.customTools ?? [];
243
261
  this._cwd = config.cwd;
244
262
  this._agentDir = config.agentDir ?? getAgentDir();
263
+ this._modelAdaptationStore = ModelAdaptationStore.forAgentDir(this._agentDir);
264
+ this.agent.onTextToolProtocolParse = (event) => this._handleTextToolProtocolParse(event);
265
+ this._applyToolRepairLayerSettings();
245
266
  this._collectWorkspaceSources = config.collectWorkspaceSources ?? collectWorkspaceSources;
246
267
  this._localRuntimeController = new LocalRuntimeController({
247
268
  agentDir: this._agentDir,
@@ -260,6 +281,7 @@ export class AgentSession {
260
281
  hasTool: (name) => this._runtimeBuilder.hasTool(name),
261
282
  getToolPromptSnippet: (name) => this._runtimeBuilder.getToolPromptSnippet(name),
262
283
  getToolPromptGuidelines: (name) => this._runtimeBuilder.getToolPromptGuidelines(name),
284
+ getModelAdaptationRules: () => this._getModelAdaptationRulesForPrompt(),
263
285
  getActiveExtensions: () => this._extensionRunner.activeExtensions,
264
286
  });
265
287
  this._autonomyTelemetry = new AutonomyTelemetry({
@@ -479,6 +501,14 @@ export class AgentSession {
479
501
  getSettingsManager: () => this.settingsManager,
480
502
  getToolDefinition: (name) => this.getToolDefinition(name),
481
503
  });
504
+ const previousToolArgumentValidation = this.agent.onToolArgumentValidation;
505
+ this.agent.onToolArgumentValidation = (event) => {
506
+ const taggedEvent = this._tagModelAdaptationRuleTeaching(event);
507
+ previousToolArgumentValidation?.(taggedEvent);
508
+ this._analytics.recordToolArgumentValidation(taggedEvent);
509
+ this._recordToolValidationBounce(taggedEvent);
510
+ this._handleModelAdaptationTelemetry(taggedEvent);
511
+ };
482
512
  this._treeNavigator = new SessionTreeNavigator({
483
513
  getSessionManager: () => this.sessionManager,
484
514
  getModel: () => this.model,
@@ -757,6 +787,354 @@ export class AgentSession {
757
787
  tools: this.agent.state.tools.slice(),
758
788
  };
759
789
  }
790
+ _modelAdaptationKeyFor(model) {
791
+ return model ? formatModelRouterModel(model) : undefined;
792
+ }
793
+ _toolRepairSettings() {
794
+ return resolveCurrentToolRepairSettings(this.settingsManager.settings);
795
+ }
796
+ _applyToolRepairLayerSettings() {
797
+ const settings = this._toolRepairSettings();
798
+ this.agent.toolArgumentRepairEnabled = settings.repair;
799
+ this.agent.toolArgumentTeachEnabled = settings.teach;
800
+ }
801
+ _getModelAdaptationRulesForPrompt() {
802
+ if (!this._toolRepairSettings().teach)
803
+ return [];
804
+ const modelKey = this._modelAdaptationKeyFor(this.agent.state.model);
805
+ return modelKey ? this._modelAdaptationStore.get(modelKey).rules : [];
806
+ }
807
+ _textProtocolFlag(model) {
808
+ // Phase 7 gating hierarchy: PI_TEXT_TOOL_CALL_PROTOCOL_DISABLED is resolved
809
+ // in _toolRepairSettings() as the env kill switch, then settings.toolRepair.textProtocol
810
+ // force-enables/disables globally, then Model.textToolCallProtocol opts in per model,
811
+ // then a persisted /toolprobe text-protocol verdict opts in that exact model. The
812
+ // calibration store is consulted after this flag; native provider tool calls still
813
+ // win when emitted, and this only enables the text-protocol fallback lane.
814
+ const override = this._toolRepairSettings().textProtocol;
815
+ if (override !== undefined)
816
+ return override;
817
+ if (model?.textToolCallProtocol === true)
818
+ return true;
819
+ const modelKey = this._modelAdaptationKeyFor(model);
820
+ return !!modelKey && this._modelAdaptationStore.get(modelKey).toolProbe?.status === "text-protocol";
821
+ }
822
+ async _streamForToolProbe(model, context, options) {
823
+ let requestOptions = options;
824
+ if (this._isRawStreamSimple(this.agent.streamFn)) {
825
+ const auth = await this._getRequiredRequestAuth(model);
826
+ requestOptions = {
827
+ ...options,
828
+ apiKey: auth.apiKey,
829
+ headers: auth.headers || options.headers ? { ...auth.headers, ...options.headers } : undefined,
830
+ };
831
+ }
832
+ return this.agent.streamFn(model, context, requestOptions);
833
+ }
834
+ _textProtocolCalibrationContext(variant, token) {
835
+ const primer = generateTextToolProtocolPrimer([TEXT_TOOL_PROTOCOL_ECHO_TOOL], { variant });
836
+ const instruction = `Text tool protocol calibration trial. Using the protocol above, call echo with data exactly "${token}". Output only the tool-call envelope.`;
837
+ return {
838
+ systemPrompt: `${primer}\n\n${instruction}`,
839
+ messages: [{ role: "user", content: [{ type: "text", text: instruction }], timestamp: Date.now() }],
840
+ tools: [TEXT_TOOL_PROTOCOL_ECHO_TOOL],
841
+ };
842
+ }
843
+ _messageHasEchoProbe(message, token) {
844
+ return message.content.some((block) => block.type === "toolCall" && block.name === "echo" && block.arguments.data === token);
845
+ }
846
+ async _runNativeToolProbeTrial(model, token) {
847
+ const instruction = `Native tool-call capability probe. Use provider-native tool calling, not prose. ` +
848
+ `Call echo with data exactly "${token}".`;
849
+ const stream = await this._streamForToolProbe(model, {
850
+ systemPrompt: instruction,
851
+ messages: [{ role: "user", content: [{ type: "text", text: instruction }], timestamp: Date.now() }],
852
+ tools: [TEXT_TOOL_PROTOCOL_ECHO_TOOL],
853
+ }, { textToolCallProtocol: false, maxRetries: 0 });
854
+ return this._messageHasEchoProbe(await stream.result(), token);
855
+ }
856
+ async _runTextProtocolTrial(model, variant, token) {
857
+ const stream = await this._streamForToolProbe(model, this._textProtocolCalibrationContext(variant, token), {
858
+ textToolCallProtocol: false,
859
+ maxRetries: 0,
860
+ });
861
+ const message = await stream.result();
862
+ if (this._messageHasEchoProbe(message, token))
863
+ return true;
864
+ const text = message.content
865
+ .filter((block) => block.type === "text")
866
+ .map((block) => block.text)
867
+ .join("\n")
868
+ .trim();
869
+ if (!text)
870
+ return false;
871
+ const parsed = parseTextToolCalls(text, [TEXT_TOOL_PROTOCOL_ECHO_TOOL]);
872
+ return parsed.calls.some((call) => call.name === "echo" && call.arguments.data === token);
873
+ }
874
+ async _calibrateTextToolProtocolForModel(model, modelKey, options) {
875
+ const variantsTried = [];
876
+ for (const variant of TEXT_TOOL_PROTOCOL_VARIANTS) {
877
+ variantsTried.push(variant);
878
+ let passed = true;
879
+ for (let trial = 0; trial < TEXT_TOOL_PROTOCOL_TRIALS_PER_VARIANT; trial++) {
880
+ const ok = await this._runTextProtocolTrial(model, variant, `pi-calibration-${trial + 1}`);
881
+ if (!ok) {
882
+ passed = false;
883
+ break;
884
+ }
885
+ }
886
+ if (passed) {
887
+ const calibratedAt = new Date().toISOString();
888
+ if (modelKey) {
889
+ this._modelAdaptationStore.setProtocol(modelKey, { version: TEXT_TOOL_PROTOCOL_VERSION, status: "calibrated", variant, calibratedAt }, calibratedAt);
890
+ }
891
+ return { status: "calibrated", variant, calibratedAt };
892
+ }
893
+ }
894
+ const attemptedAt = new Date().toISOString();
895
+ if (modelKey && options.persistFailure) {
896
+ this._modelAdaptationStore.setProtocol(modelKey, { version: TEXT_TOOL_PROTOCOL_VERSION, status: "failed", attemptedAt, variantsTried }, attemptedAt);
897
+ }
898
+ return { status: "failed", attemptedAt, variantsTried };
899
+ }
900
+ async _ensureTextToolProtocolForActiveModel() {
901
+ const model = this.agent.state.model;
902
+ if (!this._textProtocolFlag(model)) {
903
+ this.agent.textToolCallProtocol = undefined;
904
+ return;
905
+ }
906
+ const modelKey = this._modelAdaptationKeyFor(model);
907
+ if (!modelKey) {
908
+ this.agent.textToolCallProtocol = true;
909
+ return;
910
+ }
911
+ const profile = this._modelAdaptationStore.get(modelKey);
912
+ if (profile.protocol?.version === TEXT_TOOL_PROTOCOL_VERSION) {
913
+ if (profile.protocol.status === "failed") {
914
+ this.agent.textToolCallProtocol = undefined;
915
+ throw new Error(`Previous text tool protocol calibration failed for ${modelKey} at ${profile.protocol.attemptedAt}. ` +
916
+ `Variants tried: ${profile.protocol.variantsTried.join(", ")}. ` +
917
+ `Run /toolhealth for details or /toolprotocol-reset ${modelKey} to retry calibration.`);
918
+ }
919
+ this.agent.textToolCallProtocol = { variant: profile.protocol.variant };
920
+ return;
921
+ }
922
+ const result = await this._calibrateTextToolProtocolForModel(model, modelKey, { persistFailure: true });
923
+ if (result.status === "calibrated") {
924
+ this.agent.textToolCallProtocol = { variant: result.variant };
925
+ return;
926
+ }
927
+ this.agent.textToolCallProtocol = undefined;
928
+ throw new Error(`Model ${modelKey} cannot follow the text tool protocol after calibration. ` +
929
+ `Run /toolhealth for details or /toolprotocol-reset ${modelKey} to retry calibration.`);
930
+ }
931
+ _modelRef(model) {
932
+ return `${model.provider}/${model.id}`;
933
+ }
934
+ _formatToolProbeReport(results) {
935
+ const lines = ["Tool probe results:", "Model | Verdict | Variant | Diagnostic", "--- | --- | --- | ---"];
936
+ for (const result of results) {
937
+ lines.push([
938
+ result.model,
939
+ result.verdict,
940
+ result.variant ?? "-",
941
+ result.diagnostic ? result.diagnostic.replace(/\s+/g, " ").slice(0, 160) : "-",
942
+ ].join(" | "));
943
+ }
944
+ return lines.join("\n");
945
+ }
946
+ _storeToolProbe(modelKey, probe) {
947
+ this._modelAdaptationStore.setToolProbe(modelKey, probe, probe.probedAt);
948
+ }
949
+ async _probeToolCallingForModel(model) {
950
+ const modelKey = this._modelRef(model);
951
+ const probedAt = new Date().toISOString();
952
+ let diagnostic;
953
+ try {
954
+ if (await this._runNativeToolProbeTrial(model, "pi-native-probe")) {
955
+ this._storeToolProbe(modelKey, { version: TEXT_TOOL_PROTOCOL_VERSION, status: "native", probedAt });
956
+ return { model: modelKey, verdict: "native" };
957
+ }
958
+ }
959
+ catch (error) {
960
+ diagnostic = error instanceof Error ? error.message : String(error);
961
+ }
962
+ try {
963
+ const calibrated = await this._calibrateTextToolProtocolForModel(model, modelKey, { persistFailure: false });
964
+ if (calibrated.status === "calibrated") {
965
+ this._storeToolProbe(modelKey, {
966
+ version: TEXT_TOOL_PROTOCOL_VERSION,
967
+ status: "text-protocol",
968
+ probedAt: calibrated.calibratedAt,
969
+ variant: calibrated.variant,
970
+ });
971
+ return { model: modelKey, verdict: "text-protocol", variant: calibrated.variant };
972
+ }
973
+ diagnostic ??= `Text protocol variants failed: ${calibrated.variantsTried.join(", ")}`;
974
+ }
975
+ catch (error) {
976
+ diagnostic = error instanceof Error ? error.message : String(error);
977
+ }
978
+ this._storeToolProbe(modelKey, { version: TEXT_TOOL_PROTOCOL_VERSION, status: "none", probedAt, diagnostic });
979
+ return { model: modelKey, verdict: "none", diagnostic };
980
+ }
981
+ async _resolveToolProbeModels(target) {
982
+ const trimmed = target?.trim();
983
+ if (!trimmed)
984
+ return this._modelRegistry.getAvailable();
985
+ const [provider, ...modelParts] = trimmed.split("/");
986
+ const modelId = modelParts.join("/");
987
+ if (!provider || !modelId)
988
+ throw new Error("Usage: /toolprobe [provider/model]");
989
+ const exact = this._modelRegistry.find(provider, modelId);
990
+ if (exact)
991
+ return [exact];
992
+ const current = this.agent.state.model;
993
+ if (current?.provider === provider && current.id === modelId)
994
+ return [current];
995
+ throw new Error(`Model not found: ${trimmed}`);
996
+ }
997
+ async probeToolCalling(target) {
998
+ const models = await this._resolveToolProbeModels(target);
999
+ if (models.length === 0)
1000
+ throw new Error("No available models to probe.");
1001
+ const results = [];
1002
+ for (const model of models) {
1003
+ results.push(await this._probeToolCallingForModel(model));
1004
+ }
1005
+ return { results, table: this._formatToolProbeReport(results) };
1006
+ }
1007
+ _handleTextToolProtocolParse(event) {
1008
+ this._textProtocolParseObservedThisTurn = true;
1009
+ const modelKey = `${event.provider}/${event.model}`;
1010
+ if (event.status === "parsed") {
1011
+ this._textProtocolParseFailures.delete(modelKey);
1012
+ return;
1013
+ }
1014
+ const signature = `${event.variant}:${event.reason ?? "failed"}`;
1015
+ const previous = this._textProtocolParseFailures.get(modelKey);
1016
+ const repeats = previous?.signature === signature ? previous.repeats + 1 : 1;
1017
+ this._textProtocolParseFailures.set(modelKey, { signature, repeats });
1018
+ if (repeats < TEXT_TOOL_PROTOCOL_PARSE_FAILURE_THRESHOLD)
1019
+ return;
1020
+ const profile = this._modelAdaptationStore.get(modelKey);
1021
+ if (profile.protocol?.version === TEXT_TOOL_PROTOCOL_VERSION && profile.protocol.status !== "failed") {
1022
+ this._modelAdaptationStore.removeProtocol(modelKey);
1023
+ this.agent.textToolCallProtocol = undefined;
1024
+ }
1025
+ this._textProtocolParseFailures.delete(modelKey);
1026
+ }
1027
+ _recordTextToolProtocolParseOutcomeFromLastAssistant() {
1028
+ if (this._textProtocolParseObservedThisTurn)
1029
+ return;
1030
+ const protocol = this.agent.textToolCallProtocol;
1031
+ if (protocol === false || protocol === true || !protocol?.variant)
1032
+ return;
1033
+ const response = this._findLastAssistantMessage();
1034
+ if (!response)
1035
+ return;
1036
+ const responseText = response.content
1037
+ .filter((content) => content.type === "text")
1038
+ .map((content) => content.text)
1039
+ .join("\n");
1040
+ if (!responseText)
1041
+ return;
1042
+ const parsed = parseTextToolCalls(responseText, this.agent.state.tools);
1043
+ const attempted = parsed.attempted || this._looksLikeTextToolProtocolAttempt(responseText);
1044
+ if (!attempted)
1045
+ return;
1046
+ this._handleTextToolProtocolParse({
1047
+ provider: this.agent.state.model.provider,
1048
+ model: this.agent.state.model.id,
1049
+ variant: protocol.variant,
1050
+ status: parsed.calls.length > 0 ? "parsed" : "failed",
1051
+ reason: parsed.failure,
1052
+ callCount: parsed.calls.length,
1053
+ textLength: responseText.length,
1054
+ });
1055
+ }
1056
+ _looksLikeTextToolProtocolAttempt(text) {
1057
+ return /<pi:call\b|<tool_call\b|```(?:tool|tool_call)[\s\S]*"name"\s*:/i.test(text);
1058
+ }
1059
+ _recordToolValidationBounce(event) {
1060
+ if (event.outcome !== "bounced" || !event.failureShape || event.failureShape.length === 0)
1061
+ return;
1062
+ this._failureCorpus.recordToolValidation({
1063
+ provider: event.provider ?? this.agent.state.model?.provider,
1064
+ modelId: event.model ?? this.agent.state.model?.id,
1065
+ tool: event.tool,
1066
+ failureModes: event.failureModes,
1067
+ shape: event.failureShape,
1068
+ errorKeywords: event.errorKeywords,
1069
+ });
1070
+ }
1071
+ _tagModelAdaptationRuleTeaching(event) {
1072
+ if (!this._toolRepairSettings().teach || event.taught !== "none")
1073
+ return event;
1074
+ const modelKey = event.provider && event.model
1075
+ ? `${event.provider}/${event.model}`
1076
+ : this._modelAdaptationKeyFor(this.agent.state.model);
1077
+ if (!modelKey)
1078
+ return event;
1079
+ try {
1080
+ const rules = this._modelAdaptationStore.get(modelKey).rules;
1081
+ const modes = new Set([...event.failureModes, ...event.repairsApplied]);
1082
+ if (rules.some((rule) => modes.has(rule.mode))) {
1083
+ return { ...event, taught: "rule" };
1084
+ }
1085
+ }
1086
+ catch {
1087
+ // Adaptation telemetry tagging is best-effort; leave the original event unchanged.
1088
+ }
1089
+ return event;
1090
+ }
1091
+ _repairSessionCount(modelKey, mode) {
1092
+ const key = `${modelKey}\0${mode}`;
1093
+ const count = (this._repairModeSessionCounts.get(key) ?? 0) + 1;
1094
+ this._repairModeSessionCounts.set(key, count);
1095
+ return count;
1096
+ }
1097
+ _handleModelAdaptationTelemetry(event) {
1098
+ if (!this._toolRepairSettings().teach || event.outcome !== "repaired" || event.repairsApplied.length === 0)
1099
+ return;
1100
+ const modelKey = event.provider && event.model
1101
+ ? `${event.provider}/${event.model}`
1102
+ : this._modelAdaptationKeyFor(this.agent.state.model);
1103
+ if (!modelKey)
1104
+ return;
1105
+ try {
1106
+ for (const mode of [...new Set(event.repairsApplied)]) {
1107
+ const profile = this._modelAdaptationStore.get(modelKey);
1108
+ const stats = profile.teachStats[mode] ?? { taught: 0, recurrenceBefore: 0, recurrenceAfter: 0 };
1109
+ if (profile.rules.some((rule) => rule.mode === mode)) {
1110
+ this._modelAdaptationStore.markRuleFired(modelKey, mode);
1111
+ this._modelAdaptationStore.setTeachStats(modelKey, mode, {
1112
+ ...stats,
1113
+ recurrenceAfter: stats.recurrenceAfter + 1,
1114
+ });
1115
+ continue;
1116
+ }
1117
+ const recurrenceBefore = stats.recurrenceBefore + 1;
1118
+ this._modelAdaptationStore.setTeachStats(modelKey, mode, { ...stats, recurrenceBefore });
1119
+ const sessionCount = this._repairSessionCount(modelKey, mode);
1120
+ if (sessionCount >= MODEL_ADAPTATION_REPAIR_THRESHOLD ||
1121
+ recurrenceBefore >= MODEL_ADAPTATION_REPAIR_THRESHOLD) {
1122
+ this._modelAdaptationStore.addRule(modelKey, {
1123
+ mode,
1124
+ text: formatToolRepairStandingRule(mode),
1125
+ });
1126
+ this._modelAdaptationStore.setTeachStats(modelKey, mode, {
1127
+ ...stats,
1128
+ taught: stats.taught + 1,
1129
+ recurrenceBefore,
1130
+ });
1131
+ }
1132
+ }
1133
+ }
1134
+ catch {
1135
+ // Model adaptation is best-effort; failed telemetry persistence must not affect the turn.
1136
+ }
1137
+ }
760
1138
  /** Tool-build call-site delegation to {@link ContextPipeline.getToolArtifactStore}. */
761
1139
  _getToolArtifactStore() {
762
1140
  return this._pipeline.getToolArtifactStore();
@@ -896,6 +1274,17 @@ export class AgentSession {
896
1274
  formatContextCompositionDashboard() {
897
1275
  return formatContextCompositionDashboard(this.getContextCompositionReport());
898
1276
  }
1277
+ formatToolRepairHealthReport() {
1278
+ return formatToolRepairHealthReport(this._modelAdaptationStore);
1279
+ }
1280
+ removeToolRepairRule(model, mode) {
1281
+ return this._modelAdaptationStore.removeRule(model, mode);
1282
+ }
1283
+ resetToolProtocolCalibration(model) {
1284
+ const removed = this._modelAdaptationStore.removeProtocol(model);
1285
+ this._textProtocolParseFailures.delete(model);
1286
+ return removed;
1287
+ }
899
1288
  /** Curation status for diagnostics/dashboard: settings, live telemetry, last refusal reason. */
900
1289
  /** Curation status for diagnostics/dashboard (delegates to {@link ContextPipeline.getContextCurationStatus}). */
901
1290
  getContextCurationStatus() {
@@ -1492,6 +1881,7 @@ export class AgentSession {
1492
1881
  try {
1493
1882
  const maxGoalLoopRounds = this.settingsManager.getAutonomySettings().maxStallTurns;
1494
1883
  this.agent.maxStallTurns = maxGoalLoopRounds;
1884
+ await this._ensureTextToolProtocolForActiveModel();
1495
1885
  let goalLoopRounds = 1;
1496
1886
  await this.agent.prompt(messages);
1497
1887
  while ((maxGoalLoopRounds === 0 || goalLoopRounds < maxGoalLoopRounds) && (await this._handlePostAgentRun())) {
@@ -1587,6 +1977,7 @@ export class AgentSession {
1587
1977
  return this._promptUnserialized(text, options);
1588
1978
  }
1589
1979
  async _promptUnserialized(text, options) {
1980
+ this._applyToolRepairLayerSettings();
1590
1981
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
1591
1982
  const processSlashCommands = options?.processSlashCommands ?? expandPromptTemplates;
1592
1983
  const preflightResult = options?.preflightResult;
@@ -1789,7 +2180,9 @@ export class AgentSession {
1789
2180
  return;
1790
2181
  }
1791
2182
  preflightResult?.(true);
2183
+ this._textProtocolParseObservedThisTurn = false;
1792
2184
  await this._modelRouter.runRoutedTurn(messages, routedTurnModel, routedTurnRouteDecision);
2185
+ this._recordTextToolProtocolParseOutcomeFromLastAssistant();
1793
2186
  // R4: score whether the agent actually used the recalled context, so the recall gate can adapt.
1794
2187
  if (injectedRecall) {
1795
2188
  const response = this._findLastAssistantMessage();