@vtstech/pi-react-fallback 1.0.7 → 1.0.9

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 (2) hide show
  1. package/package.json +2 -2
  2. package/react-fallback.js +214 -76
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtstech/pi-react-fallback",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "ReAct fallback extension for Pi Coding Agent",
5
5
  "main": "react-fallback.js",
6
6
  "keywords": ["pi-extensions"],
@@ -14,7 +14,7 @@
14
14
  "url": "https://github.com/VTSTech/pi-coding-agent"
15
15
  },
16
16
  "dependencies": {
17
- "@vtstech/pi-shared": "1.0.7"
17
+ "@vtstech/pi-shared": "1.0.9"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "@mariozechner/pi-coding-agent": ">=0.66"
package/react-fallback.js CHANGED
@@ -1,4 +1,7 @@
1
1
  // .build-npm/react-fallback/react-fallback.temp.ts
2
+ import os from "node:os";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
2
5
  import { section, ok, fail, warn, info } from "@vtstech/pi-shared/format";
3
6
  function sanitizeModelJson(text) {
4
7
  text = text.replace(/:\s*True\b/g, ": true");
@@ -12,12 +15,72 @@ function sanitizeModelJson(text) {
12
15
  text = text.replace(/\\\\\\\\/g, "\\\\");
13
16
  return text;
14
17
  }
15
- var THOUGHT_RE = /Thought:\s*(.*?)(?=Action:|Final Answer:|$)/is;
16
- var ACTION_RE = /Action:\s*[`"']?(\w+)[`"']?\s*\n?\s*Action Input:\s*(.*?)(?=\n\s*(?:Observation:|Thought:|Final Answer:|Action:)|$)/is;
17
- var ACTION_RE_SAMELINE = /Action:\s*[`"']?(\w+)[`"']?\s+Action Input:\s*(.*?)(?=\n\s*(?:Observation:|Thought:|Final Answer:)|$)/is;
18
- var ACTION_RE_LOOSE = /Action:\s*(.+?)\n\s*Action Input:\s*(.*?)(?=\n\s*(?:Observation:|Thought:|Final Answer:|Action:)|$)/is;
19
- var ACTION_RE_PAREN = /Action:\s*(\w+)\s*\(([^)]*)\)/i;
20
- var FINAL_ANSWER_RE = /Final Answer:\s*([\s\S]*?)$/i;
18
+ var REACT_DIALECTS = [
19
+ {
20
+ name: "react",
21
+ actionTag: "Action:",
22
+ inputTag: "Action Input:",
23
+ thoughtTag: "Thought:",
24
+ stopTags: ["Observation:", "Thought:", "Final Answer:", "Action:"],
25
+ finalTag: "Final Answer:"
26
+ },
27
+ {
28
+ name: "function",
29
+ actionTag: "Function:",
30
+ inputTag: "Function Input:",
31
+ thoughtTag: "Thought:",
32
+ stopTags: ["Observation:", "Thought:", "Final Answer:", "Function:", "Action:"],
33
+ finalTag: "Final Answer:"
34
+ },
35
+ {
36
+ name: "tool",
37
+ actionTag: "Tool:",
38
+ inputTag: "Tool Input:",
39
+ thoughtTag: "Thought:",
40
+ stopTags: ["Observation:", "Thought:", "Final Answer:", "Tool:", "Action:"],
41
+ finalTag: "Final Answer:"
42
+ },
43
+ {
44
+ name: "call",
45
+ actionTag: "Call:",
46
+ inputTag: "Input:",
47
+ thoughtTag: "Thought:",
48
+ stopTags: ["Observation:", "Thought:", "Final Answer:", "Call:", "Action:"],
49
+ finalTag: "Final Answer:"
50
+ }
51
+ ];
52
+ function buildDialectPatterns(d) {
53
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
54
+ const aT = esc(d.actionTag);
55
+ const iT = esc(d.inputTag);
56
+ const stopAlt = d.stopTags.map(esc).join("|");
57
+ const tT = d.thoughtTag ? esc(d.thoughtTag) : void 0;
58
+ const fT = d.finalTag ? esc(d.finalTag) : void 0;
59
+ const thoughtRe = tT ? new RegExp(`${tT}\\s*(.*?)(?=${aT}|${fT}|$)`, "is") : void 0;
60
+ const actionRe = new RegExp(
61
+ `${aT}\\s*[\\x60"']?(\\w+)[\\x60"']?\\s*\\n?\\s*${iT}\\s*(.*?)(?=\\n\\s*(?:${stopAlt})|$)`,
62
+ "is"
63
+ );
64
+ const actionReSameline = new RegExp(
65
+ `${aT}\\s*[\\x60"']?(\\w+)[\\x60"']?\\s+${iT}\\s*(.*?)(?=\\n\\s*(?:${stopAlt})|$)`,
66
+ "is"
67
+ );
68
+ const actionReLoose = new RegExp(
69
+ `${aT}\\s*(.+?)\\n\\s*${iT}\\s*(.*?)(?=\\n\\s*(?:${stopAlt})|$)`,
70
+ "is"
71
+ );
72
+ const actionReParen = new RegExp(`${aT}\\s*(\\w+)\\s*\\(([^)]*)\\)`, "i");
73
+ const finalAnswerRe = fT ? new RegExp(`${fT}\\s*([\\s\\S]*?)$`, "i") : void 0;
74
+ return { thoughtRe, actionRe, actionReSameline, actionReLoose, actionReParen, finalAnswerRe, dialect: d };
75
+ }
76
+ var ALL_DIALECT_PATTERNS = REACT_DIALECTS.map(buildDialectPatterns);
77
+ var CLASSIC_PATTERNS = ALL_DIALECT_PATTERNS[0];
78
+ var THOUGHT_RE = CLASSIC_PATTERNS.thoughtRe;
79
+ var ACTION_RE = CLASSIC_PATTERNS.actionRe;
80
+ var ACTION_RE_SAMELINE = CLASSIC_PATTERNS.actionReSameline;
81
+ var ACTION_RE_LOOSE = CLASSIC_PATTERNS.actionReLoose;
82
+ var ACTION_RE_PAREN = CLASSIC_PATTERNS.actionReParen;
83
+ var FINAL_ANSWER_RE = CLASSIC_PATTERNS.finalAnswerRe;
21
84
  function extractJsonArgs(rawArgs) {
22
85
  const start = rawArgs.indexOf("{");
23
86
  if (start === -1) return null;
@@ -53,18 +116,43 @@ function extractJsonArgs(rawArgs) {
53
116
  return { input: jsonStr };
54
117
  }
55
118
  function parseReact(text) {
119
+ for (const dp of ALL_DIALECT_PATTERNS) {
120
+ const result = parseReactWithPatterns(text, dp);
121
+ if (result) return result;
122
+ }
123
+ return null;
124
+ }
125
+ function parseReactWithPatterns(text, dp, tightLoose = false) {
56
126
  let thought;
57
- const thoughtMatch = THOUGHT_RE.exec(text);
58
- if (thoughtMatch) thought = thoughtMatch[1].trim();
59
- let match = ACTION_RE.exec(text);
60
- if (!match) match = ACTION_RE_SAMELINE.exec(text);
127
+ if (dp.thoughtRe) {
128
+ const thoughtMatch = dp.thoughtRe.exec(text);
129
+ if (thoughtMatch) thought = thoughtMatch[1].trim();
130
+ }
131
+ let match = dp.actionRe.exec(text);
132
+ if (!match) match = dp.actionReSameline.exec(text);
61
133
  let looseMatch = false;
62
- if (!match) match = ACTION_RE_LOOSE.exec(text), looseMatch = true;
134
+ if (!match) {
135
+ const looseResult = dp.actionReLoose.exec(text);
136
+ if (looseResult) {
137
+ if (tightLoose) {
138
+ const candidate = looseResult[1].trim().replace(/[`"']/g, "");
139
+ const isToolIdentifier = /^\w+$/.test(candidate) && (candidate.includes("_") || candidate.includes("-"));
140
+ const isKnownTool = /^(get_weather|calculate)$/i.test(candidate);
141
+ if (isToolIdentifier || isKnownTool) {
142
+ match = looseResult;
143
+ looseMatch = true;
144
+ }
145
+ } else {
146
+ match = looseResult;
147
+ looseMatch = true;
148
+ }
149
+ }
150
+ }
63
151
  let parenMatch = false;
64
- if (!match) match = ACTION_RE_PAREN.exec(text), parenMatch = true;
152
+ if (!match) match = dp.actionReParen.exec(text), parenMatch = true;
65
153
  if (match) {
66
154
  let toolName = match[1].trim().replace(/[`"']/g, "");
67
- if (looseMatch && pi.context?.session?.tools) {
155
+ if (looseMatch && !tightLoose && pi.context?.session?.tools) {
68
156
  const availableTools = pi.context.session.tools || [];
69
157
  for (const real of availableTools) {
70
158
  const rl = real.toLowerCase().replace(/_/g, "");
@@ -112,9 +200,18 @@ function parseReact(text) {
112
200
  args = extractJsonArgs(rawArgs) || { input: rawArgs };
113
201
  }
114
202
  let finalAnswer;
115
- const faMatch = FINAL_ANSWER_RE.exec(text);
116
- if (faMatch) finalAnswer = faMatch[1].trim();
117
- return { name: toolName, args, thought, finalAnswer, raw: match[0] };
203
+ if (dp.finalAnswerRe) {
204
+ const faMatch = dp.finalAnswerRe.exec(text);
205
+ if (faMatch) finalAnswer = faMatch[1].trim();
206
+ }
207
+ return { name: toolName, args, thought, finalAnswer, raw: match[0], dialect: dp.dialect.name };
208
+ }
209
+ return null;
210
+ }
211
+ function detectReactDialect(text) {
212
+ for (const dp of ALL_DIALECT_PATTERNS) {
213
+ const tagPattern = new RegExp(`^\\s*${dp.dialect.actionTag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*`, "im");
214
+ if (tagPattern.test(text)) return dp.dialect;
118
215
  }
119
216
  return null;
120
217
  }
@@ -273,82 +370,103 @@ function looksLikeSchemaDump(text) {
273
370
  const matches = indicators.filter((i) => lower.includes(i.toLowerCase())).length;
274
371
  return matches >= 2;
275
372
  }
373
+ var REACT_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "react-mode.json");
374
+ function readReactConfig() {
375
+ try {
376
+ if (fs.existsSync(REACT_CONFIG_PATH)) {
377
+ const raw = JSON.parse(fs.readFileSync(REACT_CONFIG_PATH, "utf-8"));
378
+ if (typeof raw.enabled === "boolean") return raw;
379
+ }
380
+ } catch {
381
+ }
382
+ return { enabled: false };
383
+ }
384
+ function writeReactConfig(config) {
385
+ const dir = path.dirname(REACT_CONFIG_PATH);
386
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
387
+ fs.writeFileSync(REACT_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
388
+ }
276
389
  function react_fallback_temp_default(pi2) {
277
- let reactModeEnabled = false;
390
+ let reactModeEnabled = readReactConfig().enabled;
278
391
  let stats = { bridgeCalls: 0, fuzzyMatches: 0, argNormalizations: 0, parseFailures: 0 };
279
392
  const branding = [
280
- ` \u26A1 Pi ReAct Fallback Extension v1.0.7`,
393
+ ` \u26A1 Pi ReAct Fallback Extension v1.0.9`,
281
394
  ` Written by VTSTech`,
282
395
  ` GitHub: https://github.com/VTSTech`,
283
396
  ` Website: www.vts-tech.org`
284
397
  ].join("\n");
285
- pi2.registerTool({
286
- name: "tool_call",
287
- label: "Universal Tool Call",
288
- description: `Universal tool call bridge. Use this to call any available tool by specifying its name and arguments as JSON.
398
+ function registerBridgeTool() {
399
+ pi2.registerTool({
400
+ name: "tool_call",
401
+ label: "Universal Tool Call",
402
+ description: `Universal tool call bridge. Use this to call any available tool by specifying its name and arguments as JSON.
289
403
 
290
404
  To use: call tool_call with:
291
405
  - name: the exact tool name (e.g. "bash", "read", "write", "edit")
292
406
  - arguments: a JSON string of the tool's arguments (e.g. '{"command": "ls -la"}')
293
407
 
294
408
  The bridge will match your tool name (fuzzy matching supported) and normalize argument names automatically.`,
295
- promptSnippet: "tool_call - universal bridge for calling any tool",
296
- promptGuidelines: [
297
- "When you need to use a tool but are unsure of the exact name, use tool_call with the tool name and arguments.",
298
- `Example: tool_call(name='bash', arguments='{"command": "ls -la"}')`
299
- ],
300
- parameters: {
301
- type: "object",
302
- properties: {
303
- name: { type: "string", description: "Name of the tool to call (fuzzy matching supported)" },
304
- arguments: { type: "string", description: "Tool arguments as a JSON object string" }
409
+ promptSnippet: "tool_call - universal bridge for calling any tool",
410
+ promptGuidelines: [
411
+ "When you need to use a tool but are unsure of the exact name, use tool_call with the tool name and arguments.",
412
+ `Example: tool_call(name='bash', arguments='{"command": "ls -la"}')`
413
+ ],
414
+ parameters: {
415
+ type: "object",
416
+ properties: {
417
+ name: { type: "string", description: "Name of the tool to call (fuzzy matching supported)" },
418
+ arguments: { type: "string", description: "Tool arguments as a JSON object string" }
419
+ },
420
+ required: ["name", "arguments"]
305
421
  },
306
- required: ["name", "arguments"]
307
- },
308
- execute: async (toolCallId, params, signal, onUpdate, ctx) => {
309
- const p = params;
310
- const requestedName = p.name || "";
311
- const argsStr = p.arguments || "{}";
312
- stats.bridgeCalls++;
313
- let args;
314
- try {
315
- args = JSON.parse(argsStr);
316
- if (typeof args !== "object" || args === null || Array.isArray(args)) {
422
+ execute: async (toolCallId, params, signal, onUpdate, ctx) => {
423
+ const p = params;
424
+ const requestedName = p.name || "";
425
+ const argsStr = p.arguments || "{}";
426
+ stats.bridgeCalls++;
427
+ let args;
428
+ try {
429
+ args = JSON.parse(argsStr);
430
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
431
+ args = { input: argsStr };
432
+ }
433
+ } catch {
317
434
  args = { input: argsStr };
318
435
  }
319
- } catch {
320
- args = { input: argsStr };
321
- }
322
- const allTools = pi2.getAllTools();
323
- let targetToolName = null;
324
- if (allTools.includes(requestedName)) {
325
- targetToolName = requestedName;
326
- } else {
327
- targetToolName = fuzzyMatchToolName(requestedName, allTools);
328
- if (targetToolName) stats.fuzzyMatches++;
329
- }
330
- if (!targetToolName) {
331
- stats.parseFailures++;
436
+ const allTools = pi2.getAllTools();
437
+ let targetToolName = null;
438
+ if (allTools.includes(requestedName)) {
439
+ targetToolName = requestedName;
440
+ } else {
441
+ targetToolName = fuzzyMatchToolName(requestedName, allTools);
442
+ if (targetToolName) stats.fuzzyMatches++;
443
+ }
444
+ if (!targetToolName) {
445
+ stats.parseFailures++;
446
+ return {
447
+ content: [{ type: "text", text: `Error: Unknown tool "${requestedName}". Available tools: ${allTools.join(", ")}` }],
448
+ isError: true
449
+ };
450
+ }
451
+ const normalizedArgs = Object.keys(args).length > 0 ? args : {};
452
+ stats.argNormalizations++;
453
+ const argsJson = JSON.stringify(normalizedArgs);
332
454
  return {
333
- content: [{ type: "text", text: `Error: Unknown tool "${requestedName}". Available tools: ${allTools.join(", ")}` }],
334
- isError: true
335
- };
336
- }
337
- const normalizedArgs = Object.keys(args).length > 0 ? args : {};
338
- stats.argNormalizations++;
339
- const argsJson = JSON.stringify(normalizedArgs);
340
- return {
341
- content: [{
342
- type: "text",
343
- text: `[ReAct Bridge] Tool resolved: ${requestedName} \u2192 ${targetToolName}${targetToolName !== requestedName ? " (fuzzy matched)" : ""}
455
+ content: [{
456
+ type: "text",
457
+ text: `[ReAct Bridge] Tool resolved: ${requestedName} \u2192 ${targetToolName}${targetToolName !== requestedName ? " (fuzzy matched)" : ""}
344
458
 
345
459
  Please call ${targetToolName} with these arguments:
346
460
  ${argsJson}`
347
- }],
348
- isError: false
349
- };
350
- }
351
- });
461
+ }],
462
+ isError: false
463
+ };
464
+ }
465
+ });
466
+ }
467
+ if (reactModeEnabled) {
468
+ registerBridgeTool();
469
+ }
352
470
  pi2.on("context", (event) => {
353
471
  if (!reactModeEnabled) return;
354
472
  const model = event.messages;
@@ -367,18 +485,25 @@ ${argsJson}`
367
485
  description: "Toggle ReAct fallback mode for models without native tool calling",
368
486
  handler: async (_args, ctx) => {
369
487
  reactModeEnabled = !reactModeEnabled;
488
+ writeReactConfig({ enabled: reactModeEnabled });
370
489
  const status = reactModeEnabled ? "ENABLED" : "DISABLED";
371
490
  ctx.ui.notify(`ReAct mode ${status}`, "success");
372
491
  const lines = [branding];
373
492
  lines.push(section("REACT FALLBACK MODE"));
374
493
  lines.push(info(`Status: ${status}`));
494
+ lines.push(info(`Config: ${REACT_CONFIG_PATH}`));
375
495
  lines.push(info(`Bridge calls: ${stats.bridgeCalls}`));
376
496
  lines.push(info(`Fuzzy matches: ${stats.fuzzyMatches}`));
377
497
  lines.push(info(`Argument normalizations: ${stats.argNormalizations}`));
378
498
  lines.push(info(`Parse failures: ${stats.parseFailures}`));
379
499
  if (reactModeEnabled) {
500
+ registerBridgeTool();
380
501
  lines.push(ok("The tool_call bridge tool is now available to the model"));
381
502
  lines.push(info("ReAct system prompt instructions have been added"));
503
+ lines.push(info("Run /reload to make the bridge tool available to the current model"));
504
+ } else {
505
+ lines.push(warn("The tool_call bridge tool has been unregistered"));
506
+ lines.push(info("Run /reload to remove the tool from the current model"));
382
507
  }
383
508
  const report = lines.join("\n");
384
509
  pi2.sendMessage({
@@ -399,15 +524,23 @@ ${argsJson}`
399
524
  const lines = [branding];
400
525
  lines.push(section("REACT PARSER TEST"));
401
526
  lines.push(info(`Input: ${text.slice(0, 100)}${text.length > 100 ? "..." : ""}`));
527
+ const detectedDialect = detectReactDialect(text);
402
528
  const reactResult = parseReact(text);
403
529
  if (reactResult) {
404
- lines.push(ok("ReAct format detected!"));
530
+ lines.push(ok(`ReAct format detected! (dialect: ${reactResult.dialect || "react"})`));
405
531
  lines.push(info(`Tool: ${reactResult.name}`));
406
532
  lines.push(info(`Args: ${JSON.stringify(reactResult.args)}`));
407
533
  if (reactResult.thought) lines.push(info(`Thought: ${reactResult.thought}`));
408
534
  if (reactResult.finalAnswer) lines.push(info(`Final Answer: ${reactResult.finalAnswer}`));
409
535
  } else {
410
- lines.push(fail("No ReAct format detected"));
536
+ if (detectedDialect) {
537
+ lines.push(warn(`Dialect tag "${detectedDialect.actionTag}" detected but no valid tool call parsed`));
538
+ } else {
539
+ lines.push(fail("No ReAct format detected"));
540
+ }
541
+ }
542
+ if (detectedDialect && detectedDialect.name !== "react") {
543
+ lines.push(info(`Detected dialect: ${detectedDialect.name} (${detectedDialect.actionTag} / ${detectedDialect.inputTag})`));
411
544
  }
412
545
  try {
413
546
  const firstBrace = text.indexOf("{");
@@ -442,13 +575,18 @@ ${argsJson}`
442
575
  });
443
576
  pi2._reactParser = {
444
577
  parseReact,
578
+ parseReactWithPatterns,
579
+ detectReactDialect,
445
580
  sanitizeModelJson,
446
581
  extractToolFromJson,
447
582
  fuzzyMatchToolName,
448
583
  normalizeArguments,
449
- looksLikeSchemaDump
584
+ looksLikeSchemaDump,
585
+ REACT_DIALECTS,
586
+ ALL_DIALECT_PATTERNS
450
587
  };
451
588
  }
452
589
  export {
453
- react_fallback_temp_default as default
590
+ react_fallback_temp_default as default,
591
+ detectReactDialect
454
592
  };