@caupulican/pi-adaptative 0.81.12 → 0.81.15

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 (47) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/dist/bundled-resources/skills/tool-call-repair/SKILL.md +24 -18
  3. package/dist/bundled-resources/skills/tool-call-repair/references/failure-grammar.md +16 -10
  4. package/dist/cli/list-models.d.ts.map +1 -1
  5. package/dist/cli/list-models.js +5 -0
  6. package/dist/cli/list-models.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +30 -1
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +250 -21
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/model-registry.d.ts +1 -0
  12. package/dist/core/model-registry.d.ts.map +1 -1
  13. package/dist/core/model-registry.js +6 -0
  14. package/dist/core/model-registry.js.map +1 -1
  15. package/dist/core/models/adaptation-store.d.ts +18 -1
  16. package/dist/core/models/adaptation-store.d.ts.map +1 -1
  17. package/dist/core/models/adaptation-store.js +31 -3
  18. package/dist/core/models/adaptation-store.js.map +1 -1
  19. package/dist/core/slash-commands.d.ts.map +1 -1
  20. package/dist/core/slash-commands.js +5 -0
  21. package/dist/core/slash-commands.js.map +1 -1
  22. package/dist/core/tool-repair-health.d.ts.map +1 -1
  23. package/dist/core/tool-repair-health.js +21 -1
  24. package/dist/core/tool-repair-health.js.map +1 -1
  25. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  26. package/dist/modes/interactive/interactive-mode.js +24 -0
  27. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  28. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  29. package/dist/modes/rpc/rpc-mode.js +8 -0
  30. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  31. package/dist/modes/rpc/rpc-types.d.ts +23 -1
  32. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  33. package/dist/modes/rpc/rpc-types.js.map +1 -1
  34. package/docs/models.md +5 -1
  35. package/docs/rpc.md +39 -0
  36. package/docs/settings.md +4 -2
  37. package/docs/tool-repair.md +15 -5
  38. package/docs/usage.md +1 -0
  39. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  40. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  41. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  42. package/examples/extensions/sandbox/package-lock.json +2 -2
  43. package/examples/extensions/sandbox/package.json +1 -1
  44. package/examples/extensions/with-deps/package-lock.json +2 -2
  45. package/examples/extensions/with-deps/package.json +1 -1
  46. package/npm-shrinkwrap.json +12 -12
  47. 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,9 @@ export class AgentSession {
148
149
  _localRuntimeController;
149
150
  _modelAdaptationStore;
150
151
  _repairModeSessionCounts = new Map();
152
+ _textProtocolParseFailures = new Map();
153
+ _textProtocolParseObservedThisTurn = false;
154
+ _textProtocolValidationOutcomeThisTurn;
151
155
  /** Assembles the session's base system prompt from live session state (see
152
156
  * system-prompt-builder.ts); owns the paired _baseSystemPromptOptions. */
153
157
  _systemPromptBuilder;
@@ -258,6 +262,7 @@ export class AgentSession {
258
262
  this._cwd = config.cwd;
259
263
  this._agentDir = config.agentDir ?? getAgentDir();
260
264
  this._modelAdaptationStore = ModelAdaptationStore.forAgentDir(this._agentDir);
265
+ this.agent.onTextToolProtocolParse = (event) => this._handleTextToolProtocolParse(event);
261
266
  this._applyToolRepairLayerSettings();
262
267
  this._collectWorkspaceSources = config.collectWorkspaceSources ?? collectWorkspaceSources;
263
268
  this._localRuntimeController = new LocalRuntimeController({
@@ -503,6 +508,7 @@ export class AgentSession {
503
508
  previousToolArgumentValidation?.(taggedEvent);
504
509
  this._analytics.recordToolArgumentValidation(taggedEvent);
505
510
  this._recordToolValidationBounce(taggedEvent);
511
+ this._handleTextToolProtocolValidationOutcome(taggedEvent);
506
512
  this._handleModelAdaptationTelemetry(taggedEvent);
507
513
  };
508
514
  this._treeNavigator = new SessionTreeNavigator({
@@ -801,8 +807,31 @@ export class AgentSession {
801
807
  return modelKey ? this._modelAdaptationStore.get(modelKey).rules : [];
802
808
  }
803
809
  _textProtocolFlag(model) {
810
+ // Phase 7 gating hierarchy: PI_TEXT_TOOL_CALL_PROTOCOL_DISABLED is resolved
811
+ // in _toolRepairSettings() as the env kill switch, then settings.toolRepair.textProtocol
812
+ // force-enables/disables globally, then Model.textToolCallProtocol opts in per model,
813
+ // then a persisted /toolprobe text-protocol verdict opts in that exact model. The
814
+ // calibration store is consulted after this flag; native provider tool calls still
815
+ // win when emitted, and this only enables the text-protocol fallback lane.
804
816
  const override = this._toolRepairSettings().textProtocol;
805
- return override ?? model?.textToolCallProtocol === true;
817
+ if (override !== undefined)
818
+ return override;
819
+ if (model?.textToolCallProtocol === true)
820
+ return true;
821
+ const modelKey = this._modelAdaptationKeyFor(model);
822
+ return !!modelKey && this._modelAdaptationStore.get(modelKey).toolProbe?.status === "text-protocol";
823
+ }
824
+ async _streamForToolProbe(model, context, options) {
825
+ let requestOptions = options;
826
+ if (this._isRawStreamSimple(this.agent.streamFn)) {
827
+ const auth = await this._getRequiredRequestAuth(model);
828
+ requestOptions = {
829
+ ...options,
830
+ apiKey: auth.apiKey,
831
+ headers: auth.headers || options.headers ? { ...auth.headers, ...options.headers } : undefined,
832
+ };
833
+ }
834
+ return this.agent.streamFn(model, context, requestOptions);
806
835
  }
807
836
  _textProtocolCalibrationContext(variant, token) {
808
837
  const primer = generateTextToolProtocolPrimer([TEXT_TOOL_PROTOCOL_ECHO_TOOL], { variant });
@@ -810,18 +839,29 @@ export class AgentSession {
810
839
  return {
811
840
  systemPrompt: `${primer}\n\n${instruction}`,
812
841
  messages: [{ role: "user", content: [{ type: "text", text: instruction }], timestamp: Date.now() }],
813
- tools: [TEXT_TOOL_PROTOCOL_ECHO_TOOL],
814
842
  };
815
843
  }
844
+ _messageHasEchoProbe(message, token) {
845
+ return message.content.some((block) => block.type === "toolCall" && block.name === "echo" && block.arguments.data === token);
846
+ }
847
+ async _runNativeToolProbeTrial(model, token) {
848
+ const instruction = `Native tool-call capability probe. Use provider-native tool calling, not prose. ` +
849
+ `Call echo with data exactly "${token}".`;
850
+ const stream = await this._streamForToolProbe(model, {
851
+ systemPrompt: instruction,
852
+ messages: [{ role: "user", content: [{ type: "text", text: instruction }], timestamp: Date.now() }],
853
+ tools: [TEXT_TOOL_PROTOCOL_ECHO_TOOL],
854
+ }, { textToolCallProtocol: false, maxRetries: 0, temperature: 0, maxTokens: 256 });
855
+ return this._messageHasEchoProbe(await stream.result(), token);
856
+ }
816
857
  async _runTextProtocolTrial(model, variant, token) {
817
- const stream = await this.agent.streamFn(model, this._textProtocolCalibrationContext(variant, token), {
858
+ const stream = await this._streamForToolProbe(model, this._textProtocolCalibrationContext(variant, token), {
818
859
  textToolCallProtocol: false,
819
860
  maxRetries: 0,
861
+ temperature: 0,
862
+ maxTokens: 256,
820
863
  });
821
864
  const message = await stream.result();
822
- if (message.content.some((block) => block.type === "toolCall" && block.name === "echo" && block.arguments.data === token)) {
823
- return true;
824
- }
825
865
  const text = message.content
826
866
  .filter((block) => block.type === "text")
827
867
  .map((block) => block.text)
@@ -832,6 +872,32 @@ export class AgentSession {
832
872
  const parsed = parseTextToolCalls(text, [TEXT_TOOL_PROTOCOL_ECHO_TOOL]);
833
873
  return parsed.calls.some((call) => call.name === "echo" && call.arguments.data === token);
834
874
  }
875
+ async _calibrateTextToolProtocolForModel(model, modelKey, options) {
876
+ const variantsTried = [];
877
+ for (const variant of TEXT_TOOL_PROTOCOL_VARIANTS) {
878
+ variantsTried.push(variant);
879
+ let passed = true;
880
+ for (let trial = 0; trial < TEXT_TOOL_PROTOCOL_TRIALS_PER_VARIANT; trial++) {
881
+ const ok = await this._runTextProtocolTrial(model, variant, `pi-calibration-${trial + 1}`);
882
+ if (!ok) {
883
+ passed = false;
884
+ break;
885
+ }
886
+ }
887
+ if (passed) {
888
+ const calibratedAt = new Date().toISOString();
889
+ if (modelKey) {
890
+ this._modelAdaptationStore.setProtocol(modelKey, { version: TEXT_TOOL_PROTOCOL_VERSION, status: "calibrated", variant, calibratedAt }, calibratedAt);
891
+ }
892
+ return { status: "calibrated", variant, calibratedAt };
893
+ }
894
+ }
895
+ const attemptedAt = new Date().toISOString();
896
+ if (modelKey && options.persistFailure) {
897
+ this._modelAdaptationStore.setProtocol(modelKey, { version: TEXT_TOOL_PROTOCOL_VERSION, status: "failed", attemptedAt, variantsTried }, attemptedAt);
898
+ }
899
+ return { status: "failed", attemptedAt, variantsTried };
900
+ }
835
901
  async _ensureTextToolProtocolForActiveModel() {
836
902
  const model = this.agent.state.model;
837
903
  if (!this._textProtocolFlag(model)) {
@@ -845,27 +911,182 @@ export class AgentSession {
845
911
  }
846
912
  const profile = this._modelAdaptationStore.get(modelKey);
847
913
  if (profile.protocol?.version === TEXT_TOOL_PROTOCOL_VERSION) {
914
+ if (profile.protocol.status === "failed") {
915
+ this.agent.textToolCallProtocol = undefined;
916
+ throw new Error(`Previous text tool protocol calibration failed for ${modelKey} at ${profile.protocol.attemptedAt}. ` +
917
+ `Variants tried: ${profile.protocol.variantsTried.join(", ")}. ` +
918
+ `Run /toolhealth for details or /toolprotocol-reset ${modelKey} to retry calibration.`);
919
+ }
848
920
  this.agent.textToolCallProtocol = { variant: profile.protocol.variant };
849
921
  return;
850
922
  }
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
- }
923
+ const result = await this._calibrateTextToolProtocolForModel(model, modelKey, { persistFailure: true });
924
+ if (result.status === "calibrated") {
925
+ this.agent.textToolCallProtocol = { variant: result.variant };
926
+ return;
927
+ }
928
+ this.agent.textToolCallProtocol = undefined;
929
+ throw new Error(`Model ${modelKey} cannot follow the text tool protocol after calibration. ` +
930
+ `Run /toolhealth for details or /toolprotocol-reset ${modelKey} to retry calibration.`);
931
+ }
932
+ _modelRef(model) {
933
+ return `${model.provider}/${model.id}`;
934
+ }
935
+ _formatToolProbeReport(results) {
936
+ const lines = ["Tool probe results:", "Model | Verdict | Variant | Diagnostic", "--- | --- | --- | ---"];
937
+ for (const result of results) {
938
+ lines.push([
939
+ result.model,
940
+ result.verdict,
941
+ result.variant ?? "-",
942
+ result.diagnostic ? result.diagnostic.replace(/\s+/g, " ").slice(0, 160) : "-",
943
+ ].join(" | "));
944
+ }
945
+ return lines.join("\n");
946
+ }
947
+ _storeToolProbe(modelKey, probe) {
948
+ this._modelAdaptationStore.setToolProbe(modelKey, probe, probe.probedAt);
949
+ }
950
+ async _probeToolCallingForModel(model) {
951
+ const modelKey = this._modelRef(model);
952
+ const probedAt = new Date().toISOString();
953
+ let diagnostic;
954
+ try {
955
+ if (await this._runNativeToolProbeTrial(model, "pi-native-probe")) {
956
+ this._storeToolProbe(modelKey, { version: TEXT_TOOL_PROTOCOL_VERSION, status: "native", probedAt });
957
+ return { model: modelKey, verdict: "native" };
859
958
  }
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;
959
+ }
960
+ catch (error) {
961
+ diagnostic = error instanceof Error ? error.message : String(error);
962
+ }
963
+ try {
964
+ const calibrated = await this._calibrateTextToolProtocolForModel(model, modelKey, { persistFailure: false });
965
+ if (calibrated.status === "calibrated") {
966
+ this._storeToolProbe(modelKey, {
967
+ version: TEXT_TOOL_PROTOCOL_VERSION,
968
+ status: "text-protocol",
969
+ probedAt: calibrated.calibratedAt,
970
+ variant: calibrated.variant,
971
+ });
972
+ return { model: modelKey, verdict: "text-protocol", variant: calibrated.variant };
865
973
  }
974
+ diagnostic ??= `Text protocol variants failed: ${calibrated.variantsTried.join(", ")}`;
866
975
  }
867
- this.agent.textToolCallProtocol = undefined;
868
- throw new Error(`Model ${modelKey} cannot follow the text tool protocol after calibration.`);
976
+ catch (error) {
977
+ diagnostic = error instanceof Error ? error.message : String(error);
978
+ }
979
+ this._storeToolProbe(modelKey, { version: TEXT_TOOL_PROTOCOL_VERSION, status: "none", probedAt, diagnostic });
980
+ return { model: modelKey, verdict: "none", diagnostic };
981
+ }
982
+ async _resolveToolProbeModels(target) {
983
+ const trimmed = target?.trim();
984
+ if (!trimmed)
985
+ return this._modelRegistry.getAvailable();
986
+ const [provider, ...modelParts] = trimmed.split("/");
987
+ const modelId = modelParts.join("/");
988
+ if (!provider || !modelId)
989
+ throw new Error("Usage: /toolprobe [provider/model]");
990
+ const exact = this._modelRegistry.find(provider, modelId);
991
+ if (exact)
992
+ return [exact];
993
+ const current = this.agent.state.model;
994
+ if (current?.provider === provider && current.id === modelId)
995
+ return [current];
996
+ throw new Error(`Model not found: ${trimmed}`);
997
+ }
998
+ async probeToolCalling(target) {
999
+ const models = await this._resolveToolProbeModels(target);
1000
+ if (models.length === 0)
1001
+ throw new Error("No available models to probe.");
1002
+ const results = [];
1003
+ for (const model of models) {
1004
+ results.push(await this._probeToolCallingForModel(model));
1005
+ }
1006
+ return { results, table: this._formatToolProbeReport(results) };
1007
+ }
1008
+ _handleTextToolProtocolParse(event) {
1009
+ this._textProtocolParseObservedThisTurn = true;
1010
+ const modelKey = `${event.provider}/${event.model}`;
1011
+ if (event.status === "parsed")
1012
+ return;
1013
+ const signature = `${event.variant}:${event.reason ?? "failed"}`;
1014
+ const previous = this._textProtocolParseFailures.get(modelKey);
1015
+ const repeats = previous?.signature === signature ? previous.repeats + 1 : 1;
1016
+ this._textProtocolParseFailures.set(modelKey, { signature, repeats });
1017
+ if (repeats < TEXT_TOOL_PROTOCOL_PARSE_FAILURE_THRESHOLD)
1018
+ return;
1019
+ const profile = this._modelAdaptationStore.get(modelKey);
1020
+ if (profile.protocol?.version === TEXT_TOOL_PROTOCOL_VERSION && profile.protocol.status !== "failed") {
1021
+ this._modelAdaptationStore.removeProtocol(modelKey);
1022
+ this.agent.textToolCallProtocol = undefined;
1023
+ }
1024
+ this._textProtocolParseFailures.delete(modelKey);
1025
+ }
1026
+ _handleTextToolProtocolValidationOutcome(event) {
1027
+ if (event.source !== "text-protocol")
1028
+ return;
1029
+ const protocol = this.agent.textToolCallProtocol;
1030
+ const variant = protocol === true ? "tool-tag" : protocol ? protocol.variant : undefined;
1031
+ if (!variant)
1032
+ return;
1033
+ const status = event.outcome === "bounced" ? "failed" : "parsed";
1034
+ if (this._textProtocolValidationOutcomeThisTurn?.status === "parsed" && status === "failed")
1035
+ return;
1036
+ this._textProtocolValidationOutcomeThisTurn = {
1037
+ provider: event.provider ?? this.agent.state.model.provider,
1038
+ model: event.model ?? this.agent.state.model.id,
1039
+ variant,
1040
+ status,
1041
+ callCount: 1,
1042
+ textLength: 0,
1043
+ ...(status === "failed" && {
1044
+ reason: event.errorKeywords?.includes("unknown_tool") ? "unknown-tool" : "validation-failed",
1045
+ }),
1046
+ };
1047
+ }
1048
+ _recordTextToolProtocolParseOutcomeFromLastAssistant() {
1049
+ const validationOutcome = this._textProtocolValidationOutcomeThisTurn;
1050
+ this._textProtocolValidationOutcomeThisTurn = undefined;
1051
+ if (validationOutcome?.status === "parsed") {
1052
+ this._textProtocolParseObservedThisTurn = true;
1053
+ this._textProtocolParseFailures.delete(`${validationOutcome.provider}/${validationOutcome.model}`);
1054
+ return;
1055
+ }
1056
+ if (validationOutcome) {
1057
+ this._handleTextToolProtocolParse(validationOutcome);
1058
+ return;
1059
+ }
1060
+ if (this._textProtocolParseObservedThisTurn)
1061
+ return;
1062
+ const protocol = this.agent.textToolCallProtocol;
1063
+ if (protocol === false || protocol === true || !protocol?.variant)
1064
+ return;
1065
+ const response = this._findLastAssistantMessage();
1066
+ if (!response)
1067
+ return;
1068
+ const responseText = response.content
1069
+ .filter((content) => content.type === "text")
1070
+ .map((content) => content.text)
1071
+ .join("\n");
1072
+ if (!responseText)
1073
+ return;
1074
+ const parsed = parseTextToolCalls(responseText, this.agent.state.tools);
1075
+ const attempted = parsed.attempted || this._looksLikeTextToolProtocolAttempt(responseText);
1076
+ if (!attempted)
1077
+ return;
1078
+ this._handleTextToolProtocolParse({
1079
+ provider: this.agent.state.model.provider,
1080
+ model: this.agent.state.model.id,
1081
+ variant: protocol.variant,
1082
+ status: parsed.calls.length > 0 ? "parsed" : "failed",
1083
+ reason: parsed.failure,
1084
+ callCount: parsed.calls.length,
1085
+ textLength: responseText.length,
1086
+ });
1087
+ }
1088
+ _looksLikeTextToolProtocolAttempt(text) {
1089
+ return /<pi:call\b|<tool_call\b|```(?:tool|tool_call)[\s\S]*"name"\s*:/i.test(text);
869
1090
  }
870
1091
  _recordToolValidationBounce(event) {
871
1092
  if (event.outcome !== "bounced" || !event.failureShape || event.failureShape.length === 0)
@@ -1091,6 +1312,11 @@ export class AgentSession {
1091
1312
  removeToolRepairRule(model, mode) {
1092
1313
  return this._modelAdaptationStore.removeRule(model, mode);
1093
1314
  }
1315
+ resetToolProtocolCalibration(model) {
1316
+ const removed = this._modelAdaptationStore.removeProtocol(model);
1317
+ this._textProtocolParseFailures.delete(model);
1318
+ return removed;
1319
+ }
1094
1320
  /** Curation status for diagnostics/dashboard: settings, live telemetry, last refusal reason. */
1095
1321
  /** Curation status for diagnostics/dashboard (delegates to {@link ContextPipeline.getContextCurationStatus}). */
1096
1322
  getContextCurationStatus() {
@@ -1986,7 +2212,10 @@ export class AgentSession {
1986
2212
  return;
1987
2213
  }
1988
2214
  preflightResult?.(true);
2215
+ this._textProtocolParseObservedThisTurn = false;
2216
+ this._textProtocolValidationOutcomeThisTurn = undefined;
1989
2217
  await this._modelRouter.runRoutedTurn(messages, routedTurnModel, routedTurnRouteDecision);
2218
+ this._recordTextToolProtocolParseOutcomeFromLastAssistant();
1990
2219
  // R4: score whether the agent actually used the recalled context, so the recall gate can adapt.
1991
2220
  if (injectedRecall) {
1992
2221
  const response = this._findLastAssistantMessage();