pi-opencode-cli-bridge 0.2.6 → 0.2.7

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.
package/README.md CHANGED
@@ -123,6 +123,8 @@ Without an override, discovery selects active `opencode/` models whose reported
123
123
  6. Text markers such as `<pi_tool_call>{"name":"read","arguments":{"path":"example.txt"}}</pi_tool_call>` become Pi tool calls. Tool names must be in the current Pi tool list; execution then follows Pi's normal validation and permission pipeline. DeepSeek models sometimes write their native DSML tool-call blocks (`<|DSML|invoke name="...">`) as text instead; complete blocks are converted the same way, and an incomplete one fails the turn with a diagnostic instead of ending it silently.
124
124
  7. Successful continuation turns reuse an OpenCode session and send only the new transcript. Changes to the model, prompt, tools or transcript prefix start a fresh session.
125
125
 
126
+ A turn containing only reasoning, with no answer or usable Pi tool call, also gets one fresh recovery attempt instead of silently ending the conversation. The model is asked to answer or request an available Pi tool; normal text answers and valid calls found in reasoning are unchanged. Reasoning-only and blocked-native failures share the same single retry and remaining turn timeout. If recovery fails, Pi receives an explicit error, not another thinking-only message.
127
+
126
128
  Pi's provider instrumentation hooks follow the CLI transport, not OpenCode's internal HTTP traffic:
127
129
 
128
130
  - `onPayload` receives `{ prompt }`, the exact text written to the CLI's standard input. Returning `{ prompt: "..." }` replaces it; any other non-`undefined` value fails the turn. A turn with a payload hook always starts a fresh OpenCode session.
@@ -130,7 +132,7 @@ Pi's provider instrumentation hooks follow the CLI transport, not OpenCode's int
130
132
  - `onProviderStreamEvent` receives each validated `--format json` event in order, and is awaited before the event is processed.
131
133
  - Provider-scoped `env` values are passed to the subprocess. The bridge's own configuration directory and update-check settings always take precedence.
132
134
 
133
- Reasoning variants, image support and token limits follow CLI discovery. Token usage is taken from CLI events when available, otherwise estimated, and includes both attempts when native-tool recovery is needed. Session records and temporary project directories are cleaned up on a best-effort basis.
135
+ Reasoning variants, image support and token limits follow CLI discovery. Token usage is taken from CLI events when available, otherwise estimated, and includes both attempts when recovery is needed. Session records and temporary project directories are cleaned up on a best-effort basis.
134
136
 
135
137
  ## Security, privacy and limits
136
138
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-opencode-cli-bridge",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Bridge the OpenCode CLI into Pi Coding Agent",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
package/src/index.ts CHANGED
@@ -1518,7 +1518,7 @@ function streamOpenCodeAttempt(
1518
1518
  model: Model<Api>,
1519
1519
  context: Context,
1520
1520
  options: SimpleStreamOptions | undefined,
1521
- allowNativeRecovery: boolean,
1521
+ allowRecovery: boolean,
1522
1522
  ): AssistantMessageEventStream {
1523
1523
  const stream = createAssistantMessageEventStream();
1524
1524
  const deadline = Date.now() + resolveTurnTimeoutMs(options?.timeoutMs);
@@ -1543,7 +1543,7 @@ function streamOpenCodeAttempt(
1543
1543
  let stdoutRemainder = "";
1544
1544
  let capturedSessionId: string | undefined;
1545
1545
  let opencodeToolUse: string | undefined;
1546
- let nativeToolFailure: Error | undefined;
1546
+ let recoverableFailure: Error | undefined;
1547
1547
  // Text after the last blocked native call shows whether the model recovered in-run.
1548
1548
  let nativeTextMark = 0;
1549
1549
  let stepNativeCalls: (ParsedToolCall | undefined)[] = [];
@@ -1767,10 +1767,10 @@ function streamOpenCodeAttempt(
1767
1767
  }
1768
1768
  if (opencodeError) throw new Error(opencodeError);
1769
1769
  if (opencodeToolUse && !nativeCalls && !accumulatedText.slice(nativeTextMark).trim()) {
1770
- nativeToolFailure = new Error(
1771
- `OpenCode could not use the available Pi tools${allowNativeRecovery ? "" : " after one recovery attempt"} (blocked native tool: ${JSON.stringify(opencodeToolUse)}). Retry the request or select another OpenCode model.`,
1770
+ recoverableFailure = new Error(
1771
+ `OpenCode could not use the available Pi tools${allowRecovery ? "" : " after one recovery attempt"} (blocked native tool: ${JSON.stringify(opencodeToolUse)}). Retry the request or select another OpenCode model.`,
1772
1772
  );
1773
- throw nativeToolFailure;
1773
+ throw recoverableFailure;
1774
1774
  }
1775
1775
 
1776
1776
  const toolCallResult = parseToolCallResponse(
@@ -1815,6 +1815,12 @@ function streamOpenCodeAttempt(
1815
1815
  );
1816
1816
  }
1817
1817
  }
1818
+ if (toolCalls.length === 0 && !displayText.trim() && accumulatedThinking.trim()) {
1819
+ recoverableFailure = new Error(
1820
+ `OpenCode returned only reasoning without an answer or Pi tool call${allowRecovery ? "" : " after one recovery attempt"}. Retry the request or select another OpenCode model.`,
1821
+ );
1822
+ throw recoverableFailure;
1823
+ }
1818
1824
  if (
1819
1825
  toolCalls.length === 0 &&
1820
1826
  !displayText.trim() &&
@@ -1943,12 +1949,14 @@ function streamOpenCodeAttempt(
1943
1949
  deleteOpenCodeSession(capturedSessionId);
1944
1950
  if (piState) dropOpencodeSession(piState);
1945
1951
  const remainingMs = deadline - Date.now();
1946
- if (nativeToolFailure && error === nativeToolFailure && allowNativeRecovery &&
1947
- !options?.signal?.aborted && remainingMs > 0 && context.tools?.length) {
1952
+ if (recoverableFailure && error === recoverableFailure && allowRecovery &&
1953
+ !options?.signal?.aborted && remainingMs > 0 && (!opencodeToolUse || context.tools?.length)) {
1948
1954
  setEstimatedUsage(model, output, prompt, accumulatedText + accumulatedThinking);
1949
1955
  const correction = [
1950
- `Your previous attempt used the disabled OpenCode native tool ${JSON.stringify(opencodeToolUse)}.`,
1951
- `Available Pi tools: ${context.tools.map(tool => tool.name).join(", ")}.`,
1956
+ opencodeToolUse
1957
+ ? `Your previous attempt used the disabled OpenCode native tool ${JSON.stringify(opencodeToolUse)}.`
1958
+ : "Your previous attempt ended with reasoning only, without an answer or Pi tool call. Provide a final answer or request a Pi tool, not just a plan to act.",
1959
+ `Available Pi tools: ${context.tools?.map(tool => tool.name).join(", ") || "none"}.`,
1952
1960
  "Complete the original request using only those Pi tools. Do not call any OpenCode native tool.",
1953
1961
  'Request Pi tools as <pi_tool_call>{"name":"tool_name","arguments":{}}</pi_tool_call> markers, or answer normally if no tool is needed.',
1954
1962
  "If Pi has no grep tool, request a search through Pi bash when it is available instead.",