@evandu/super-pi 0.1.39 → 0.1.41

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,
@@ -110,7 +111,34 @@ const WorkflowParameters = Type.Object({
110
111
  type ExtensionApi = { registerTool(tool: ExtensionToolDefinition): void };
111
112
 
112
113
  const extension = (pi: ExtensionApi): void => {
113
- 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) return { error: "Interactive question is unavailable" }; 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 }) }); return await response.json(); } });
114
+ // THE QUESTION TOOL. It blocks on an HTTP request until the person answers, and the answer comes back
115
+ // as the tool result, which is what keeps the run alive across the exchange - the model does not decide
116
+ // to continue, it cannot: the tool has not returned.
117
+ //
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
+ } });
114
142
  // Read ONCE at load, not per call: the environment cannot change under a running Pi, and `prepareLoadout`
115
143
  // is synchronous - it cannot await a fetch for a roster - so the roster has to already be here.
116
144
  const config = readTaskConfig(process.env);