@juicesharp/rpiv-advisor 2.1.0 → 2.2.0

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/advisor/execute.ts +76 -42
  2. package/package.json +2 -2
@@ -6,7 +6,7 @@
6
6
  * buildAdvisorResult so the envelope is built in exactly one place.
7
7
  */
8
8
 
9
- import type { Message, StopReason, ThinkingLevel, Usage } from "@earendil-works/pi-ai";
9
+ import type { AssistantMessage, Message, StopReason, TextContent, ThinkingLevel, Usage } from "@earendil-works/pi-ai";
10
10
  import {
11
11
  type AgentToolResult,
12
12
  type AgentToolUpdateCallback,
@@ -43,6 +43,18 @@ interface AdvisorDetails {
43
43
  errorMessage?: string;
44
44
  }
45
45
 
46
+ // Extract the advisor's text content from a completeSimple response: concatenate
47
+ // every text part, trim. Thinking/toolCall parts are ignored. Returns "" when the
48
+ // model returned no text content — the empty-response class R6.4 retries once
49
+ // before surfacing. Pure so both attempts share one extraction path.
50
+ function advisorTextFromResponse(response: AssistantMessage): string {
51
+ return response.content
52
+ .filter((c): c is TextContent => c.type === "text")
53
+ .map((c) => c.text)
54
+ .join("\n")
55
+ .trim();
56
+ }
57
+
46
58
  // Single result-envelope builder — every executeAdvisor branch and the pre-call
47
59
  // error paths funnel through here. `effort` is snapshotted once at executeAdvisor
48
60
  // entry and threaded through every call so the returned details.effort always
@@ -127,51 +139,73 @@ export async function executeAdvisor(
127
139
  const requestOptions = runtimeCompleteSimple
128
140
  ? { signal, reasoning: effort }
129
141
  : { apiKey: auth.apiKey, headers: auth.headers, signal, reasoning: effort };
130
- const response = await completeSimple(
131
- advisor,
132
- // `tools: []` reaffirms the "never calls tools" contract even when
133
- // `messages` contains prior toolCall/toolResult blocks (btw.ts:235).
134
- { systemPrompt: ADVISOR_SYSTEM_PROMPT, messages, tools: [] },
135
- requestOptions,
136
- );
137
-
138
- if (response.stopReason === "aborted") {
139
- return buildAdvisorResult({
140
- text: ERR_CALL_ABORTED,
141
- effort,
142
- advisorLabel,
143
- usage: response.usage,
144
- stopReason: response.stopReason,
145
- errorMessage: response.errorMessage ?? ERR_ABORTED_DETAIL,
146
- });
147
- }
148
142
 
149
- if (response.stopReason === "error") {
150
- return buildAdvisorResult({
151
- text: errCallFailed(response.errorMessage),
152
- effort,
153
- advisorLabel,
154
- usage: response.usage,
155
- stopReason: response.stopReason,
156
- errorMessage: response.errorMessage,
157
- });
158
- }
143
+ // Single dispatch point — both attempts reuse the SAME `messages` and
144
+ // `requestOptions`, so the retry cannot diverge from attempt 1. `tools: []`
145
+ // reaffirms the "never calls tools" contract even when `messages` contains
146
+ // prior toolCall/toolResult blocks (btw.ts:235).
147
+ const callAdvisor = (): Promise<AssistantMessage> =>
148
+ completeSimple(advisor, { systemPrompt: ADVISOR_SYSTEM_PROMPT, messages, tools: [] }, requestOptions);
149
+
150
+ // Build the terminal envelope for an aborted/error stopReason, or return
151
+ // undefined when the attempt produced a normal stop whose text (or lack of
152
+ // text) the caller must still resolve. Aborted/error short-circuit and are
153
+ // NEVER retried — they are not the empty-response class R6.4 targets.
154
+ const stopReasonEnvelope = (r: AssistantMessage): AgentToolResult<AdvisorDetails> | undefined => {
155
+ if (r.stopReason === "aborted") {
156
+ return buildAdvisorResult({
157
+ text: ERR_CALL_ABORTED,
158
+ effort,
159
+ advisorLabel,
160
+ usage: r.usage,
161
+ stopReason: r.stopReason,
162
+ errorMessage: r.errorMessage ?? ERR_ABORTED_DETAIL,
163
+ });
164
+ }
165
+ if (r.stopReason === "error") {
166
+ return buildAdvisorResult({
167
+ text: errCallFailed(r.errorMessage),
168
+ effort,
169
+ advisorLabel,
170
+ usage: r.usage,
171
+ stopReason: r.stopReason,
172
+ errorMessage: r.errorMessage,
173
+ });
174
+ }
175
+ return undefined;
176
+ };
159
177
 
160
- const advisorText = response.content
161
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
162
- .map((c) => c.text)
163
- .join("\n")
164
- .trim();
178
+ let response = await callAdvisor();
165
179
 
180
+ // Aborted/error short-circuit on the first attempt — no retry.
181
+ const firstTerminal = stopReasonEnvelope(response);
182
+ if (firstTerminal) return firstTerminal;
183
+
184
+ let advisorText = advisorTextFromResponse(response);
185
+
186
+ // R6.4: a transient empty advisor response (normal stop, no text) gets
187
+ // exactly ONE retry with identical inputs before surfacing as a terminal
188
+ // error. Bounded to a single second call — never a `while`/loop — so a
189
+ // persistent-empty provider cannot hot-loop. The retry reuses the SAME
190
+ // pre-computed `messages`/`requestOptions` (no re-derivation that could
191
+ // diverge from attempt 1), then applies the same three-way route.
166
192
  if (!advisorText) {
167
- return buildAdvisorResult({
168
- text: ERR_EMPTY_RESPONSE,
169
- effort,
170
- advisorLabel,
171
- usage: response.usage,
172
- stopReason: response.stopReason,
173
- errorMessage: ERR_EMPTY_RESPONSE_DETAIL,
174
- });
193
+ response = await callAdvisor();
194
+
195
+ const retryTerminal = stopReasonEnvelope(response);
196
+ if (retryTerminal) return retryTerminal;
197
+
198
+ advisorText = advisorTextFromResponse(response);
199
+ if (!advisorText) {
200
+ return buildAdvisorResult({
201
+ text: ERR_EMPTY_RESPONSE,
202
+ effort,
203
+ advisorLabel,
204
+ usage: response.usage,
205
+ stopReason: response.stopReason,
206
+ errorMessage: ERR_EMPTY_RESPONSE_DETAIL,
207
+ });
208
+ }
175
209
  }
176
210
 
177
211
  return buildAdvisorResult({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-advisor",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Pi extension. A second opinion the model can request from a stronger reviewer model before it acts.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -45,7 +45,7 @@
45
45
  ]
46
46
  },
47
47
  "dependencies": {
48
- "@juicesharp/rpiv-config": "^2.1.0",
48
+ "@juicesharp/rpiv-config": "^2.2.0",
49
49
  "typebox": "^1.1.24"
50
50
  },
51
51
  "peerDependencies": {