@caupulican/pi-adaptative 0.81.12 → 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 (46) hide show
  1. package/CHANGELOG.md +6 -1
  2. package/dist/bundled-resources/skills/tool-call-repair/SKILL.md +22 -18
  3. package/dist/cli/list-models.d.ts.map +1 -1
  4. package/dist/cli/list-models.js +5 -0
  5. package/dist/cli/list-models.js.map +1 -1
  6. package/dist/core/agent-session.d.ts +28 -1
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +215 -19
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/model-registry.d.ts +1 -0
  11. package/dist/core/model-registry.d.ts.map +1 -1
  12. package/dist/core/model-registry.js +6 -0
  13. package/dist/core/model-registry.js.map +1 -1
  14. package/dist/core/models/adaptation-store.d.ts +18 -1
  15. package/dist/core/models/adaptation-store.d.ts.map +1 -1
  16. package/dist/core/models/adaptation-store.js +31 -3
  17. package/dist/core/models/adaptation-store.js.map +1 -1
  18. package/dist/core/slash-commands.d.ts.map +1 -1
  19. package/dist/core/slash-commands.js +5 -0
  20. package/dist/core/slash-commands.js.map +1 -1
  21. package/dist/core/tool-repair-health.d.ts.map +1 -1
  22. package/dist/core/tool-repair-health.js +21 -1
  23. package/dist/core/tool-repair-health.js.map +1 -1
  24. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  25. package/dist/modes/interactive/interactive-mode.js +24 -0
  26. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  27. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  28. package/dist/modes/rpc/rpc-mode.js +8 -0
  29. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  30. package/dist/modes/rpc/rpc-types.d.ts +23 -1
  31. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  32. package/dist/modes/rpc/rpc-types.js.map +1 -1
  33. package/docs/models.md +5 -1
  34. package/docs/rpc.md +39 -0
  35. package/docs/settings.md +4 -2
  36. package/docs/tool-repair.md +13 -5
  37. package/docs/usage.md +1 -0
  38. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  39. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  40. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  41. package/examples/extensions/sandbox/package-lock.json +2 -2
  42. package/examples/extensions/sandbox/package.json +1 -1
  43. package/examples/extensions/with-deps/package-lock.json +2 -2
  44. package/examples/extensions/with-deps/package.json +1 -1
  45. package/npm-shrinkwrap.json +12 -12
  46. package/package.json +4 -4
@@ -59,6 +59,7 @@ const RAW_STREAM_MARKER = Symbol.for("pi.rawStreamSimple");
59
59
  const MODEL_ADAPTATION_REPAIR_THRESHOLD = 3;
60
60
  const TEXT_TOOL_PROTOCOL_VERSION = 1;
61
61
  const TEXT_TOOL_PROTOCOL_TRIALS_PER_VARIANT = 2;
62
+ const TEXT_TOOL_PROTOCOL_PARSE_FAILURE_THRESHOLD = 3;
62
63
  const TEXT_TOOL_PROTOCOL_VARIANTS = ["tool-tag", "tool-call", "fenced-json"];
63
64
  const TEXT_TOOL_PROTOCOL_ECHO_TOOL = {
64
65
  name: "echo",
@@ -148,6 +149,8 @@ export class AgentSession {
148
149
  _localRuntimeController;
149
150
  _modelAdaptationStore;
150
151
  _repairModeSessionCounts = new Map();
152
+ _textProtocolParseFailures = new Map();
153
+ _textProtocolParseObservedThisTurn = false;
151
154
  /** Assembles the session's base system prompt from live session state (see
152
155
  * system-prompt-builder.ts); owns the paired _baseSystemPromptOptions. */
153
156
  _systemPromptBuilder;
@@ -258,6 +261,7 @@ export class AgentSession {
258
261
  this._cwd = config.cwd;
259
262
  this._agentDir = config.agentDir ?? getAgentDir();
260
263
  this._modelAdaptationStore = ModelAdaptationStore.forAgentDir(this._agentDir);
264
+ this.agent.onTextToolProtocolParse = (event) => this._handleTextToolProtocolParse(event);
261
265
  this._applyToolRepairLayerSettings();
262
266
  this._collectWorkspaceSources = config.collectWorkspaceSources ?? collectWorkspaceSources;
263
267
  this._localRuntimeController = new LocalRuntimeController({
@@ -801,8 +805,31 @@ export class AgentSession {
801
805
  return modelKey ? this._modelAdaptationStore.get(modelKey).rules : [];
802
806
  }
803
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.
804
814
  const override = this._toolRepairSettings().textProtocol;
805
- return override ?? model?.textToolCallProtocol === true;
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);
806
833
  }
807
834
  _textProtocolCalibrationContext(variant, token) {
808
835
  const primer = generateTextToolProtocolPrimer([TEXT_TOOL_PROTOCOL_ECHO_TOOL], { variant });
@@ -813,15 +840,27 @@ export class AgentSession {
813
840
  tools: [TEXT_TOOL_PROTOCOL_ECHO_TOOL],
814
841
  };
815
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
+ }
816
856
  async _runTextProtocolTrial(model, variant, token) {
817
- const stream = await this.agent.streamFn(model, this._textProtocolCalibrationContext(variant, token), {
857
+ const stream = await this._streamForToolProbe(model, this._textProtocolCalibrationContext(variant, token), {
818
858
  textToolCallProtocol: false,
819
859
  maxRetries: 0,
820
860
  });
821
861
  const message = await stream.result();
822
- if (message.content.some((block) => block.type === "toolCall" && block.name === "echo" && block.arguments.data === token)) {
862
+ if (this._messageHasEchoProbe(message, token))
823
863
  return true;
824
- }
825
864
  const text = message.content
826
865
  .filter((block) => block.type === "text")
827
866
  .map((block) => block.text)
@@ -832,6 +871,32 @@ export class AgentSession {
832
871
  const parsed = parseTextToolCalls(text, [TEXT_TOOL_PROTOCOL_ECHO_TOOL]);
833
872
  return parsed.calls.some((call) => call.name === "echo" && call.arguments.data === token);
834
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
+ }
835
900
  async _ensureTextToolProtocolForActiveModel() {
836
901
  const model = this.agent.state.model;
837
902
  if (!this._textProtocolFlag(model)) {
@@ -845,27 +910,151 @@ export class AgentSession {
845
910
  }
846
911
  const profile = this._modelAdaptationStore.get(modelKey);
847
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
+ }
848
919
  this.agent.textToolCallProtocol = { variant: profile.protocol.variant };
849
920
  return;
850
921
  }
851
- for (const variant of TEXT_TOOL_PROTOCOL_VARIANTS) {
852
- let passed = true;
853
- for (let trial = 0; trial < TEXT_TOOL_PROTOCOL_TRIALS_PER_VARIANT; trial++) {
854
- const ok = await this._runTextProtocolTrial(model, variant, `pi-calibration-${trial + 1}`);
855
- if (!ok) {
856
- passed = false;
857
- break;
858
- }
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" };
859
957
  }
860
- if (passed) {
861
- const calibratedAt = new Date().toISOString();
862
- this._modelAdaptationStore.setProtocol(modelKey, { version: TEXT_TOOL_PROTOCOL_VERSION, variant, calibratedAt }, calibratedAt);
863
- this.agent.textToolCallProtocol = { variant };
864
- return;
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 };
865
972
  }
973
+ diagnostic ??= `Text protocol variants failed: ${calibrated.variantsTried.join(", ")}`;
866
974
  }
867
- this.agent.textToolCallProtocol = undefined;
868
- throw new Error(`Model ${modelKey} cannot follow the text tool protocol after calibration.`);
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);
869
1058
  }
870
1059
  _recordToolValidationBounce(event) {
871
1060
  if (event.outcome !== "bounced" || !event.failureShape || event.failureShape.length === 0)
@@ -1091,6 +1280,11 @@ export class AgentSession {
1091
1280
  removeToolRepairRule(model, mode) {
1092
1281
  return this._modelAdaptationStore.removeRule(model, mode);
1093
1282
  }
1283
+ resetToolProtocolCalibration(model) {
1284
+ const removed = this._modelAdaptationStore.removeProtocol(model);
1285
+ this._textProtocolParseFailures.delete(model);
1286
+ return removed;
1287
+ }
1094
1288
  /** Curation status for diagnostics/dashboard: settings, live telemetry, last refusal reason. */
1095
1289
  /** Curation status for diagnostics/dashboard (delegates to {@link ContextPipeline.getContextCurationStatus}). */
1096
1290
  getContextCurationStatus() {
@@ -1986,7 +2180,9 @@ export class AgentSession {
1986
2180
  return;
1987
2181
  }
1988
2182
  preflightResult?.(true);
2183
+ this._textProtocolParseObservedThisTurn = false;
1989
2184
  await this._modelRouter.runRoutedTurn(messages, routedTurnModel, routedTurnRouteDecision);
2185
+ this._recordTextToolProtocolParseOutcomeFromLastAssistant();
1990
2186
  // R4: score whether the agent actually used the recalled context, so the recall gate can adapt.
1991
2187
  if (injectedRecall) {
1992
2188
  const response = this._findLastAssistantMessage();