@evandu/super-pi 0.1.1 → 0.1.2

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.
@@ -1,141 +1,198 @@
1
- import { Type } from "@sinclair/typebox";
2
- import {
3
- TASK_TOOL_NAME,
4
- describeTask,
5
- executeTask,
6
- readTaskConfig,
7
- type TaskParams,
8
- } from "./core.ts";
9
- import {
10
- TODO_DESCRIPTION,
11
- TODO_TOOL_NAME,
12
- executeTodo,
13
- readTodoConfig,
14
- type TodoParams,
15
- } from "./core.ts";
16
-
17
- /**
18
- * The Pi adapter for the `task` and `todowrite` tools. Everything with a decision in it is in
19
- * `./core.ts`; this file is the mounting hardware and nothing else.
20
- *
21
- * `@sinclair/typebox` is Pi's VIRTUAL MODULE (packages/coding-agent/src/core/extensions/virtual-modules.ts),
22
- * which is why importing it works here and would not work anywhere else in this repository: it is not in
23
- * `package.json` and is not installed in `node_modules`. Nothing outside Pi may import this file for that
24
- * reason - `scripts/task-tool.test.mjs` drives `core.ts` instead, and reads this file as TEXT for the
25
- * things that cannot be executed outside Pi.
26
- *
27
- * The parameter schema is OpenCode's `task.ts:43-52` verbatim, including `task_id`. `task_id` is
28
- * accepted and ignored in this step: resuming a sub-agent's session would mean keeping its Pi process
29
- * and its transcript alive between calls, and `runtime.ts` kills the primary's Pi after every turn, so
30
- * there is nothing for a session id to point at yet. It is kept in the schema because removing it would
31
- * make this tool incompatible with a model that has been told the OpenCode shape, and because the next
32
- * step is precisely the thing that gives it meaning.
33
- */
34
- const Parameters = Type.Object({
35
- description: Type.String({ description: "A short (3-5 words) description of the task" }),
36
- prompt: Type.String({ description: "The task for the agent to perform" }),
37
- subagent_type: Type.String({ description: "The type of specialized agent to use for this task" }),
38
- task_id: Type.Optional(Type.String({ description: "Resume a previous task when supported" })),
39
- background: Type.Optional(Type.Boolean({ description: "Run asynchronously and deliver the result as a follow-up message" })),
40
- });
41
-
42
- /**
43
- * The `todowrite` parameter schema, OpenCode's `tool/todo.ts:6-8` in the TypeBox dialect this adapter
44
- * speaks. `todos` is the WHOLE list every time - it replaces the list rather than adding to it - which is
45
- * stated in the per-property description, because a model that assumes it appends will send one item and
46
- * watch the other forty disappear.
47
- *
48
- * `id` is absent from the schema on purpose and this is the ONE place Super Pi's copy differs from
49
- * OpenCode's: OpenCode's `Todo.Info` carries only content/status/priority. Super Pi keeps those three and
50
- * ADDS a stable id server-side (`normalizeTodos`), which the schema does not mention so the model is not
51
- * invited to invent ids it will not control.
52
- */
53
- const TodoParameters = Type.Object({
54
- todos: Type.Array(Type.Object({
55
- content: Type.String({ description: "The task text, specific and actionable" }),
56
- status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")], { description: "pending = not started, in_progress = being worked on now (exactly one at a time), completed = finished successfully" }),
57
- priority: Type.Optional(Type.Union([Type.Literal("high"), Type.Literal("medium"), Type.Literal("low")], { description: "How urgent this item is" })),
58
- }), { description: "The COMPLETE updated todo list - this replaces the previous list entirely, so include every item you still want on screen" }),
59
- });
60
-
61
- /**
62
- * The only structural type this file needs, declared locally.
63
- *
64
- * It was `import type { ExtensionFactory } from "F:/code/open_agents_repo/pi/..."` before, which is an
65
- * absolute path into one machine's checkout: fine there, unresolvable on any other, and it made this
66
- * file impossible to open without that checkout present. Pi is a runtime, not a build dependency of
67
- * Super Pi, so there is no package to import its types from - declaring the two shapes this file uses is
68
- * the honest alternative, and `npm run check` would not have caught the old one because `agents/` is
69
- * outside every tsconfig.
70
- */
71
- type ExtensionToolDefinition = {
72
- name: string;
73
- label: string;
74
- description: string;
75
- parameters: unknown;
76
- execute(toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: unknown, context: { cwd: string }): Promise<unknown>;
77
- prepareLoadout?(loadout: unknown): { descriptions?: Record<string, string> } | undefined;
78
- };
79
- type ExtensionApi = { registerTool(tool: ExtensionToolDefinition): void };
80
-
81
- const extension = (pi: ExtensionApi): void => {
82
- // Read ONCE at load, not per call: the environment cannot change under a running Pi, and `prepareLoadout`
83
- // is synchronous - it cannot await a fetch for a roster - so the roster has to already be here.
84
- const config = readTaskConfig(process.env);
85
- // Same rule for the todo tool, and for a stronger reason: the todo endpoint is derived from the same
86
- // listen address as the task endpoint, so reading it per call would mean re-reading an environment
87
- // that cannot have changed since the process started.
88
- const todoConfig = readTodoConfig(process.env);
89
-
90
- pi.registerTool({
91
- name: TASK_TOOL_NAME,
92
- label: "Task",
93
- // The fallback for the seconds between registration and the first `prepareLoadout`: the guidance with
94
- // no roster, which is still true text rather than a placeholder.
95
- description: describeTask(config.roster),
96
- promptSnippet: "delegate substantial work to a specialized subagent",
97
- parameters: Parameters,
98
- /**
99
- * Injects the roster into the description the model is shown, on every loadout.
100
- *
101
- * Pi's own mechanism for a tool that lists other tools, and the reason it is used rather than
102
- * editing the system prompt: `ToolDefinition.prepareLoadout` is documented for exactly this
103
- * (types.ts:620-622 - "Tools that orchestrate other tools use it"), it is called whenever the active
104
- * tools change, and a description that Pi receives is a description that cannot drift from the tool
105
- * set it belongs to. `describeTask()` in OpenCode does the same thing one layer up, inside
106
- * `registry.ts`.
107
- *
108
- * Read from the environment the server filled at spawn time, never from a list written here, so a
109
- * sub-agent added to `agents/*.md` while the server runs is dispatchable on the next message.
110
- */
111
- prepareLoadout: () => ({ descriptions: { [TASK_TOOL_NAME]: describeTask(config.roster) } }),
112
- execute: async (_toolCallId, params, signal, _onUpdate, context) =>
113
- await executeTask(params as TaskParams, context, config, { signal: signal ?? undefined }),
114
- });
115
-
116
- /**
117
- * `todowrite`, registered SECOND and for one reason: order in the tool list is the order a model reads
118
- * them in, and a plan is worth more in front of the eye than a dispatcher is. Nothing depends on the
119
- * order functionally - `runtime.ts` names both tools in its allowlist and Pi activates whichever of them
120
- * the agent's own `tools:` map allows.
121
- *
122
- * NO `prepareLoadout`, and that is a decision rather than an omission. The task tool needs one because
123
- * its description embeds the ROSTER, which changes on disk while the server runs; the todo description
124
- * embeds nothing that changes, and the thing that does change - the current list - is the tool's own
125
- * OUTPUT, which the model reads back from its own transcript on every subsequent turn. Re-injecting the
126
- * list into the description would be a second source of truth for the same fact, and it would put a
127
- * snapshot of the plan in front of the model that could disagree with the one its history already
128
- * contains.
129
- */
130
- pi.registerTool({
131
- name: TODO_TOOL_NAME,
132
- label: "Todo",
133
- description: TODO_DESCRIPTION,
134
- promptSnippet: "track multi-step work as a live task list the user watches",
135
- parameters: TodoParameters,
136
- execute: async (_toolCallId, params, signal) =>
137
- await executeTodo(params as TodoParams, todoConfig, { signal: signal ?? undefined }),
138
- });
139
- };
140
-
141
- export default extension;
1
+ import { Type } from "@sinclair/typebox";
2
+ import {
3
+ TASK_TOOL_NAME,
4
+ describeTask,
5
+ executeTask,
6
+ readTaskConfig,
7
+ type TaskParams,
8
+ } from "./core.ts";
9
+ import {
10
+ WORKFLOW_DESCRIPTION,
11
+ WORKFLOW_TOOL_NAME,
12
+ executeWorkflow,
13
+ readWorkflowConfig,
14
+ type WorkflowParams,
15
+ } from "./core.ts";
16
+ import {
17
+ TODO_DESCRIPTION,
18
+ TODO_TOOL_NAME,
19
+ executeTodo,
20
+ readTodoConfig,
21
+ type TodoParams,
22
+ } from "./core.ts";
23
+
24
+ /**
25
+ * The Pi adapter for the `task` and `todowrite` tools. Everything with a decision in it is in
26
+ * `./core.ts`; this file is the mounting hardware and nothing else.
27
+ *
28
+ * `@sinclair/typebox` is Pi's VIRTUAL MODULE (packages/coding-agent/src/core/extensions/virtual-modules.ts),
29
+ * which is why importing it works here and would not work anywhere else in this repository: it is not in
30
+ * `package.json` and is not installed in `node_modules`. Nothing outside Pi may import this file for that
31
+ * reason - `scripts/task-tool.test.mjs` drives `core.ts` instead, and reads this file as TEXT for the
32
+ * things that cannot be executed outside Pi.
33
+ *
34
+ * The parameter schema is OpenCode's `task.ts:43-52` verbatim, including `task_id`. `task_id` is
35
+ * accepted and ignored in this step: resuming a sub-agent's session would mean keeping its Pi process
36
+ * and its transcript alive between calls, and `runtime.ts` kills the primary's Pi after every turn, so
37
+ * there is nothing for a session id to point at yet. It is kept in the schema because removing it would
38
+ * make this tool incompatible with a model that has been told the OpenCode shape, and because the next
39
+ * step is precisely the thing that gives it meaning.
40
+ */
41
+ const Parameters = Type.Object({
42
+ description: Type.String({ description: "A short (3-5 words) description of the task" }),
43
+ prompt: Type.String({ description: "The task for the agent to perform" }),
44
+ subagent_type: Type.String({ description: "The type of specialized agent to use for this task" }),
45
+ task_id: Type.Optional(Type.String({ description: "Resume a previous task when supported" })),
46
+ background: Type.Optional(Type.Boolean({ description: "Run asynchronously and deliver the result as a follow-up message" })),
47
+ });
48
+
49
+ /**
50
+ * The `todowrite` parameter schema, OpenCode's `tool/todo.ts:6-8` in the TypeBox dialect this adapter
51
+ * speaks. `todos` is the WHOLE list every time - it replaces the list rather than adding to it - which is
52
+ * stated in the per-property description, because a model that assumes it appends will send one item and
53
+ * watch the other forty disappear.
54
+ *
55
+ * `id` is absent from the schema on purpose and this is the ONE place Super Pi's copy differs from
56
+ * OpenCode's: OpenCode's `Todo.Info` carries only content/status/priority. Super Pi keeps those three and
57
+ * ADDS a stable id server-side (`normalizeTodos`), which the schema does not mention so the model is not
58
+ * invited to invent ids it will not control.
59
+ */
60
+ const TodoParameters = Type.Object({
61
+ todos: Type.Array(Type.Object({
62
+ content: Type.String({ description: "The task text, specific and actionable" }),
63
+ status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")], { description: "pending = not started, in_progress = being worked on now (exactly one at a time), completed = finished successfully" }),
64
+ priority: Type.Optional(Type.Union([Type.Literal("high"), Type.Literal("medium"), Type.Literal("low")], { description: "How urgent this item is" })),
65
+ }), { description: "The COMPLETE updated todo list - this replaces the previous list entirely, so include every item you still want on screen" }),
66
+ });
67
+
68
+ /**
69
+ * The only structural type this file needs, declared locally.
70
+ *
71
+ * It was `import type { ExtensionFactory } from "F:/code/open_agents_repo/pi/..."` before, which is an
72
+ * absolute path into one machine's checkout: fine there, unresolvable on any other, and it made this
73
+ * file impossible to open without that checkout present. Pi is a runtime, not a build dependency of
74
+ * Super Pi, so there is no package to import its types from - declaring the two shapes this file uses is
75
+ * the honest alternative, and `npm run check` would not have caught the old one because `agents/` is
76
+ * outside every tsconfig.
77
+ */
78
+ type ExtensionToolDefinition = {
79
+ name: string;
80
+ label: string;
81
+ description: string;
82
+ parameters: unknown;
83
+ execute(toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: unknown, context: { cwd: string }): Promise<unknown>;
84
+ prepareLoadout?(loadout: unknown): { descriptions?: Record<string, string> } | undefined;
85
+ };
86
+ /**
87
+ * The `workflow` parameter schema.
88
+ *
89
+ * `agentId` rather than `subagent_type`, and the difference is that it is REQUIRED here. A single
90
+ * dispatch can infer itself from its prompt - a model asking for one delegation is obvious about which
91
+ * agent it wants - but a plan of up to eight steps cannot: a step whose agent is unspecified would have to
92
+ * be guessed by the server, and a server that guesses which specialist to run is exactly the
93
+ * role-name branch this whole feature refuses to have.
94
+ *
95
+ * `onFailure` is a plain string rather than a TypeBox union so the SERVER owns the whitelist. A closed
96
+ * enum here would be a second list that could disagree with `parseFailurePolicy`, and the message a model
97
+ * gets for a bad value is better written by the side that knows the real options.
98
+ */
99
+ const WorkflowParameters = Type.Object({
100
+ summary: Type.String({ description: "One line saying what this whole plan is for, shown to the user above the timeline" }),
101
+ onFailure: Type.Optional(Type.String({ description: "What to do when a step FAILS TO RUN: stop (default), skip, retry:1, retry:2 or retry:3" })),
102
+ steps: Type.Array(Type.Object({
103
+ agentId: Type.String({ description: "The dispatchable sub-agent type to run, e.g. scout" }),
104
+ label: Type.Optional(Type.String({ description: "3-4 words saying what this step is FOR, shown to the user only" })),
105
+ prompt: Type.String({ description: "The instruction for THIS step alone. Earlier steps' output is added for you." }),
106
+ }), { minItems: 1, maxItems: 8, description: "The steps, in the order they should run" }),
107
+ });
108
+
109
+ type ExtensionApi = { registerTool(tool: ExtensionToolDefinition): void };
110
+
111
+ const extension = (pi: ExtensionApi): void => {
112
+ // Read ONCE at load, not per call: the environment cannot change under a running Pi, and `prepareLoadout`
113
+ // is synchronous - it cannot await a fetch for a roster - so the roster has to already be here.
114
+ const config = readTaskConfig(process.env);
115
+ // Same rule for the todo tool, and for a stronger reason: the todo endpoint is derived from the same
116
+ // listen address as the task endpoint, so reading it per call would mean re-reading an environment
117
+ // that cannot have changed since the process started.
118
+ const todoConfig = readTodoConfig(process.env);
119
+ // Same rule for the workflow tool: one read at load, because the environment cannot change under a
120
+ // running Pi. The capability it reads is minted for THIS turn and dies with it, so it is valid for
121
+ // exactly as long as this process is - which is the whole reason a sub-agent cannot present one.
122
+ const workflowConfig = readWorkflowConfig(process.env);
123
+
124
+ pi.registerTool({
125
+ name: TASK_TOOL_NAME,
126
+ label: "Task",
127
+ // The fallback for the seconds between registration and the first `prepareLoadout`: the guidance with
128
+ // no roster, which is still true text rather than a placeholder.
129
+ description: describeTask(config.roster),
130
+ promptSnippet: "delegate substantial work to a specialized subagent",
131
+ parameters: Parameters,
132
+ /**
133
+ * Injects the roster into the description the model is shown, on every loadout.
134
+ *
135
+ * Pi's own mechanism for a tool that lists other tools, and the reason it is used rather than
136
+ * editing the system prompt: `ToolDefinition.prepareLoadout` is documented for exactly this
137
+ * (types.ts:620-622 - "Tools that orchestrate other tools use it"), it is called whenever the active
138
+ * tools change, and a description that Pi receives is a description that cannot drift from the tool
139
+ * set it belongs to. `describeTask()` in OpenCode does the same thing one layer up, inside
140
+ * `registry.ts`.
141
+ *
142
+ * Read from the environment the server filled at spawn time, never from a list written here, so a
143
+ * sub-agent added to `agents/*.md` while the server runs is dispatchable on the next message.
144
+ */
145
+ prepareLoadout: () => ({ descriptions: { [TASK_TOOL_NAME]: describeTask(config.roster) } }),
146
+ execute: async (_toolCallId, params, signal, _onUpdate, context) =>
147
+ await executeTask(params as TaskParams, context, config, { signal: signal ?? undefined }),
148
+ });
149
+
150
+ /**
151
+ * `todowrite`, registered SECOND and for one reason: order in the tool list is the order a model reads
152
+ * them in, and a plan is worth more in front of the eye than a dispatcher is. Nothing depends on the
153
+ * order functionally - `runtime.ts` names both tools in its allowlist and Pi activates whichever of them
154
+ * the agent's own `tools:` map allows.
155
+ *
156
+ * NO `prepareLoadout`, and that is a decision rather than an omission. The task tool needs one because
157
+ * its description embeds the ROSTER, which changes on disk while the server runs; the todo description
158
+ * embeds nothing that changes, and the thing that does change - the current list - is the tool's own
159
+ * OUTPUT, which the model reads back from its own transcript on every subsequent turn. Re-injecting the
160
+ * list into the description would be a second source of truth for the same fact, and it would put a
161
+ * snapshot of the plan in front of the model that could disagree with the one its history already
162
+ * contains.
163
+ */
164
+ pi.registerTool({
165
+ name: TODO_TOOL_NAME,
166
+ label: "Todo",
167
+ description: TODO_DESCRIPTION,
168
+ promptSnippet: "track multi-step work as a live task list the user watches",
169
+ parameters: TodoParameters,
170
+ execute: async (_toolCallId, params, signal) =>
171
+ await executeTodo(params as TodoParams, todoConfig, { signal: signal ?? undefined }),
172
+ });
173
+
174
+ /**
175
+ * `workflow`, registered THIRD and last.
176
+ *
177
+ * Last on purpose: a tool list is read in order, and this is the one a model should reach for LAST. It
178
+ * is also the most expensive call available - it blocks a whole pipeline of sub-agent runs - so the
179
+ * entry before it in the list being `todowrite` (a plan, free) rather than `workflow` is doing real work:
180
+ * the cheap way to express a plan should be what a model sees first.
181
+ *
182
+ * NO `prepareLoadout`, unlike `task`, because unlike `task` its description embeds nothing that changes
183
+ * while the server runs. It does not list the roster: the ids it accepts are the same ones `task` lists
184
+ * one entry above, in the same loadout, so injecting them a second time would be the same fact in two
185
+ * places free to disagree - and its description points the model at that list rather than repeating it.
186
+ */
187
+ pi.registerTool({
188
+ name: WORKFLOW_TOOL_NAME,
189
+ label: "Workflow",
190
+ description: WORKFLOW_DESCRIPTION,
191
+ promptSnippet: "run an ordered plan of sub-agents, each handed the previous step's output",
192
+ parameters: WorkflowParameters,
193
+ execute: async (_toolCallId, params, signal) =>
194
+ await executeWorkflow(params as WorkflowParams, workflowConfig, { signal: signal ?? undefined }),
195
+ });
196
+ };
197
+
198
+ export default extension;