@evandu/super-pi 0.1.40 → 0.1.42

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.
@@ -5,6 +5,7 @@ import {
5
5
  executeTask,
6
6
  readTaskConfig,
7
7
  type TaskParams,
8
+ type TaskToolResult,
8
9
  } from "./core.ts";
9
10
  import {
10
11
  WORKFLOW_DESCRIPTION,
@@ -114,14 +115,30 @@ const extension = (pi: ExtensionApi): void => {
114
115
  // as the tool result, which is what keeps the run alive across the exchange - the model does not decide
115
116
  // to continue, it cannot: the tool has not returned.
116
117
  //
117
- // A REFUSED QUESTION THROWS, and that is the whole of the dismissal semantics. It used to `return`
118
- // whatever came back, which on a refusal is `{error: "用户没有回答这个问题"}` - a SUCCESSFUL tool call
119
- // carrying an error object, so the row settled as `ok` and the model carried on from a question nobody
120
- // answered. Throwing fails the call, and a failed call is what stops the run. OpenCode is the reference
121
- // and is explicit about it: rejecting a question fails the deferred and the tool dies
122
- // (`packages/core/src/question.ts:128-141`). Nothing is ever invented to fill the gap - not
123
- // `options[0]`, not an empty selection, which would look to the model exactly like a real choice.
124
- pi.registerTool({ name: "question", label: "Question", description: "Ask the user a blocking single or multiple choice question with an optional extra note.", parameters: QuestionParameters, execute: async (_id, raw) => { const endpoint = process.env.SUPER_PI_QUESTION_ENDPOINT; const conversationId = process.env.SUPER_PI_TASK_CONVERSATION; if (!endpoint || !conversationId) throw new Error("This run cannot ask the user anything: the question endpoint is not available."); const p = raw as { question: string; options: string[]; multiple?: boolean; extraNote?: boolean }; const response = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ conversationId, ...p }) }); const payload = await response.json() as { error?: string; answers?: unknown }; if (!response.ok || payload?.error) throw new Error(payload?.error ?? `The question could not be delivered (HTTP ${response.status}).`); return payload; } });
118
+ // IT RETURNS THE SHAPE EVERY OTHER TOOL HERE RETURNS, AND THAT IS NOT COSMETIC. `TaskToolResult` is
119
+ // `{content: [{type:"text", text}], details}` - `todo`, `task` and `workflow` all build it through the
120
+ // local `text()` helper. This tool used to `return` the parsed JSON object bare, which Pi does not read
121
+ // as a tool result at all: the model received an EMPTY result and said so - 「我无法复述你的答案,
122
+ // question 工具调用后返回了空结果」. The answer was in the store the whole time; it simply never arrived.
123
+ //
124
+ // A REFUSED QUESTION THROWS, and that is the whole of the dismissal semantics: a failed call is what
125
+ // stops the run. OpenCode is the reference and is explicit - rejecting a question fails the deferred
126
+ // and the tool dies (`packages/core/src/question.ts:128-141`). Nothing is invented to fill the gap, not
127
+ // `options[0]` and not an empty selection, which would look to the model exactly like a real choice.
128
+ pi.registerTool({ name: "question", label: "Question", description: "Ask the user a blocking single or multiple choice question with an optional extra note.", parameters: QuestionParameters, execute: async (_id, raw): Promise<TaskToolResult> => {
129
+ const endpoint = process.env.SUPER_PI_QUESTION_ENDPOINT;
130
+ const conversationId = process.env.SUPER_PI_TASK_CONVERSATION;
131
+ const text = (body: string, isError?: boolean): TaskToolResult => ({ content: [{ type: "text", text: body }], details: undefined, ...(isError ? { isError: true } : {}) });
132
+ if (!endpoint || !conversationId) return text("This run cannot ask the user anything: the question endpoint is not available.", true);
133
+ const p = raw as { question: string; options: string[]; multiple?: boolean; extraNote?: boolean };
134
+ const response = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ conversationId, ...p }) });
135
+ const payload = await response.json() as { error?: string; answers?: { id: string; selected: string[]; note?: string }[]; summary?: string };
136
+ if (!response.ok || payload?.error) return text(payload?.error ?? `The question could not be delivered (HTTP ${response.status}).`, true);
137
+ // READABLE, NOT JUST PRESENT. The bare `{answers:[{id, selected}]}` carries a UUID and an array, and
138
+ // a model handed that has to guess what it means - which it did, describing a real answer as a
139
+ // cancellation. The text says what was asked and what was chosen, in words.
140
+ return text(JSON.stringify({ answers: (payload.answers ?? []).map((answer) => ({ question: p.question, selected: answer.selected, ...(answer.note ? { note: answer.note } : {}) })), ...(payload.summary ? { summary: payload.summary } : {}) }));
141
+ } });
125
142
  // Read ONCE at load, not per call: the environment cannot change under a running Pi, and `prepareLoadout`
126
143
  // is synchronous - it cannot await a fetch for a roster - so the roster has to already be here.
127
144
  const config = readTaskConfig(process.env);