@feniix/pi-code-reasoning 1.0.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.
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/extensions/index.ts +631 -0
- package/extensions/types.ts +162 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 feniix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# @feniix/pi-code-reasoning
|
|
2
|
+
|
|
3
|
+
[Code Reasoning](https://github.com/mettamatt/code-reasoning) extension for [pi](https://pi.dev/) — reflective problem-solving through sequential thinking with branching and revision support.
|
|
4
|
+
|
|
5
|
+
Based on the MCP server by Matt Westgate, this native TypeScript extension provides structured thinking tools without external dependencies.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Sequential Thinking** — Break down complex problems into structured, revisable steps
|
|
10
|
+
- **Branching** — Explore alternative approaches from any thought (🌿)
|
|
11
|
+
- **Revision** — Correct earlier thinking when new insights emerge (🔄)
|
|
12
|
+
- **Progress Tracking** — Track thought count and branches
|
|
13
|
+
- **Configurable Output** — Client-side byte and line truncation
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pi install npm:@feniix/pi-code-reasoning
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Ephemeral (one-off) use:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pi -e npm:@feniix/pi-code-reasoning
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Tools
|
|
28
|
+
|
|
29
|
+
### `code_reasoning`
|
|
30
|
+
|
|
31
|
+
Record and process a thought with metadata.
|
|
32
|
+
|
|
33
|
+
| Parameter | Type | Required | Description |
|
|
34
|
+
|-----------|------|----------|-------------|
|
|
35
|
+
| `thought` | string | yes | Your reasoning content |
|
|
36
|
+
| `thought_number` | integer | yes | Position in sequence |
|
|
37
|
+
| `total_thoughts` | integer | yes | Estimated total thoughts |
|
|
38
|
+
| `next_thought_needed` | boolean | yes | Set FALSE when done |
|
|
39
|
+
| `is_revision` | boolean | no | When correcting earlier thought (🔄) |
|
|
40
|
+
| `revises_thought` | integer | no | Which thought# you're revising |
|
|
41
|
+
| `branch_from_thought` | integer | no | When exploring alternatives (🌿) |
|
|
42
|
+
| `branch_id` | string | no | Identifier for the branch |
|
|
43
|
+
| `needs_more_thoughts` | boolean | no | If more thoughts needed |
|
|
44
|
+
|
|
45
|
+
### `code_reasoning_status`
|
|
46
|
+
|
|
47
|
+
Get current session status: branches and thought count.
|
|
48
|
+
|
|
49
|
+
### `code_reasoning_reset`
|
|
50
|
+
|
|
51
|
+
Reset the session, clearing all thoughts and branches.
|
|
52
|
+
|
|
53
|
+
## Thinking Patterns
|
|
54
|
+
|
|
55
|
+
### Sequential Thinking (Basic)
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"thought": "Initial exploration of the problem...",
|
|
60
|
+
"thought_number": 1,
|
|
61
|
+
"total_thoughts": 5,
|
|
62
|
+
"next_thought_needed": true
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Branching (Explore Alternatives) 🌿
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"thought": "Exploring alternative approach...",
|
|
71
|
+
"thought_number": 3,
|
|
72
|
+
"total_thoughts": 7,
|
|
73
|
+
"next_thought_needed": true,
|
|
74
|
+
"branch_from_thought": 2,
|
|
75
|
+
"branch_id": "alternative-algo-x"
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Revision (Correct Earlier Thinking) 🔄
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"thought": "Revisiting earlier point: Assumption Y was flawed...",
|
|
84
|
+
"thought_number": 4,
|
|
85
|
+
"total_thoughts": 6,
|
|
86
|
+
"next_thought_needed": true,
|
|
87
|
+
"is_revision": true,
|
|
88
|
+
"revises_thought": 2
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Checklist (Review Every 3 Thoughts)
|
|
93
|
+
|
|
94
|
+
1. Need to explore alternatives? → Use **BRANCH** (🌿)
|
|
95
|
+
2. Need to correct earlier thinking? → Use **REVISION** (🔄)
|
|
96
|
+
3. Scope changed? → Adjust **total_thoughts**
|
|
97
|
+
4. Done? → Set **next_thought_needed = false**
|
|
98
|
+
|
|
99
|
+
## Configuration
|
|
100
|
+
|
|
101
|
+
### CLI Flags
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pi --code-reasoning-max-bytes=102400 --code-reasoning-max-lines=5000
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Environment Variables
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
export CODE_REASONING_MAX_BYTES=102400
|
|
111
|
+
export CODE_REASONING_MAX_LINES=5000
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### JSON Config
|
|
115
|
+
|
|
116
|
+
Create `~/.pi/agent/extensions/code-reasoning.json`:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"maxBytes": 51200,
|
|
121
|
+
"maxLines": 2000
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## CLI Flags
|
|
126
|
+
|
|
127
|
+
| Flag | Env Variable | Default | Description |
|
|
128
|
+
|------|-------------|---------|-------------|
|
|
129
|
+
| `--code-reasoning-config` | `CODE_REASONING_CONFIG` | — | Custom config file path |
|
|
130
|
+
| `--code-reasoning-max-bytes` | `CODE_REASONING_MAX_BYTES` | `51200` | Max output bytes |
|
|
131
|
+
| `--code-reasoning-max-lines` | `CODE_REASONING_MAX_LINES` | `2000` | Max output lines |
|
|
132
|
+
|
|
133
|
+
## Requirements
|
|
134
|
+
|
|
135
|
+
- pi v0.51.0 or later
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Reasoning Extension for pi
|
|
3
|
+
*
|
|
4
|
+
* Provides a tool for reflective problem-solving through sequential thinking.
|
|
5
|
+
* Supports branching (exploring alternatives) and revision (correcting earlier thoughts).
|
|
6
|
+
*
|
|
7
|
+
* Setup:
|
|
8
|
+
* 1. Install: pi install npm:@feniix/pi-code-reasoning
|
|
9
|
+
* 2. Or pass flags:
|
|
10
|
+
* --code-reasoning-config, --code-reasoning-max-bytes, --code-reasoning-max-lines
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* "Use code reasoning to think through this architecture decision"
|
|
14
|
+
* "Process a thought about the database schema design"
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { homedir, tmpdir } from "node:os";
|
|
19
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
20
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
21
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "@mariozechner/pi-coding-agent";
|
|
22
|
+
import { Type } from "@sinclair/typebox";
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
enforceCrossFieldRules,
|
|
26
|
+
MAX_THOUGHT_COUNT,
|
|
27
|
+
MAX_THOUGHT_LENGTH,
|
|
28
|
+
type ThoughtData,
|
|
29
|
+
type ValidatedThoughtData,
|
|
30
|
+
} from "./types.js";
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Constants
|
|
34
|
+
// =============================================================================
|
|
35
|
+
|
|
36
|
+
const DEFAULT_CONFIG_FILE: Record<string, unknown> = {
|
|
37
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
38
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Reserved for future use
|
|
42
|
+
// const DEFAULT_CONFIG_DIR = join(homedir(), ".pi-code-reasoning");
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Types
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
interface CodeReasoningConfig {
|
|
49
|
+
maxBytes?: number;
|
|
50
|
+
maxLines?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ThoughtTracker {
|
|
54
|
+
add: (thought: ValidatedThoughtData) => void;
|
|
55
|
+
ensureBranchIsValid: (branchFromThought?: number) => void;
|
|
56
|
+
branches: () => string[];
|
|
57
|
+
count: () => number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface McpToolDetails {
|
|
61
|
+
tool: string;
|
|
62
|
+
truncated: boolean;
|
|
63
|
+
truncation?: {
|
|
64
|
+
truncatedBy: "lines" | "bytes" | null;
|
|
65
|
+
totalLines: number;
|
|
66
|
+
totalBytes: number;
|
|
67
|
+
outputLines: number;
|
|
68
|
+
outputBytes: number;
|
|
69
|
+
maxLines: number;
|
|
70
|
+
maxBytes: number;
|
|
71
|
+
};
|
|
72
|
+
tempFile?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// Utility Functions
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
80
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function toJsonString(value: unknown): string {
|
|
84
|
+
if (typeof value === "string") {
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return JSON.stringify(value, null, 2);
|
|
89
|
+
} catch {
|
|
90
|
+
return String(value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatToolOutput(
|
|
95
|
+
toolName: string,
|
|
96
|
+
result: unknown,
|
|
97
|
+
limits: { maxBytes?: number; maxLines?: number },
|
|
98
|
+
): { text: string; details: McpToolDetails } {
|
|
99
|
+
const rawText = toJsonString(result);
|
|
100
|
+
const truncation = truncateHead(rawText, {
|
|
101
|
+
maxLines: limits?.maxLines ?? DEFAULT_MAX_LINES,
|
|
102
|
+
maxBytes: limits?.maxBytes ?? DEFAULT_MAX_BYTES,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let text = truncation.content;
|
|
106
|
+
let tempFile: string | undefined;
|
|
107
|
+
|
|
108
|
+
if (truncation.truncated) {
|
|
109
|
+
tempFile = writeTempFile(toolName, rawText);
|
|
110
|
+
text +=
|
|
111
|
+
`\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines ` +
|
|
112
|
+
`(${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). ` +
|
|
113
|
+
`Full output saved to: ${tempFile}]`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (truncation.firstLineExceedsLimit && rawText.length > 0) {
|
|
117
|
+
text =
|
|
118
|
+
`[First line exceeded ${formatSize(truncation.maxBytes)} limit. Full output saved to: ${tempFile ?? "N/A"}]\n` +
|
|
119
|
+
text;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
text,
|
|
124
|
+
details: {
|
|
125
|
+
tool: toolName,
|
|
126
|
+
truncated: truncation.truncated,
|
|
127
|
+
truncation: {
|
|
128
|
+
truncatedBy: truncation.truncatedBy,
|
|
129
|
+
totalLines: truncation.totalLines,
|
|
130
|
+
totalBytes: truncation.totalBytes,
|
|
131
|
+
outputLines: truncation.outputLines,
|
|
132
|
+
outputBytes: truncation.outputBytes,
|
|
133
|
+
maxLines: truncation.maxLines,
|
|
134
|
+
maxBytes: truncation.maxBytes,
|
|
135
|
+
},
|
|
136
|
+
tempFile,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function writeTempFile(toolName: string, content: string): string {
|
|
142
|
+
const safeName = toolName.replace(/[^a-z0-9_-]/gi, "_");
|
|
143
|
+
const filename = `pi-code-reasoning-${safeName}-${Date.now()}.txt`;
|
|
144
|
+
const filePath = join(tmpdir(), filename);
|
|
145
|
+
writeFileSync(filePath, content, "utf-8");
|
|
146
|
+
return filePath;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeNumber(value: unknown): number | undefined {
|
|
150
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
if (typeof value === "string") {
|
|
154
|
+
const parsed = Number(value);
|
|
155
|
+
if (Number.isFinite(parsed)) {
|
|
156
|
+
return parsed;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function splitParams(params: Record<string, unknown>): {
|
|
163
|
+
toolArgs: Record<string, unknown>;
|
|
164
|
+
requestedLimits: { maxBytes?: number; maxLines?: number };
|
|
165
|
+
} {
|
|
166
|
+
const { piMaxBytes, piMaxLines, ...rest } = params as Record<string, unknown> & {
|
|
167
|
+
piMaxBytes?: unknown;
|
|
168
|
+
piMaxLines?: unknown;
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
toolArgs: rest,
|
|
172
|
+
requestedLimits: {
|
|
173
|
+
maxBytes: normalizeNumber(piMaxBytes),
|
|
174
|
+
maxLines: normalizeNumber(piMaxLines),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function resolveEffectiveLimits(
|
|
180
|
+
requested: { maxBytes?: number; maxLines?: number },
|
|
181
|
+
maxAllowed: { maxBytes: number; maxLines: number },
|
|
182
|
+
): { maxBytes: number; maxLines: number } {
|
|
183
|
+
const requestedBytes = requested.maxBytes ?? maxAllowed.maxBytes;
|
|
184
|
+
const requestedLines = requested.maxLines ?? maxAllowed.maxLines;
|
|
185
|
+
return {
|
|
186
|
+
maxBytes: Math.min(requestedBytes, maxAllowed.maxBytes),
|
|
187
|
+
maxLines: Math.min(requestedLines, maxAllowed.maxLines),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resolveConfigPath(configPath: string): string {
|
|
192
|
+
const trimmed = configPath.trim();
|
|
193
|
+
if (trimmed.startsWith("~/")) {
|
|
194
|
+
return join(homedir(), trimmed.slice(2));
|
|
195
|
+
}
|
|
196
|
+
if (trimmed.startsWith("~")) {
|
|
197
|
+
return join(homedir(), trimmed.slice(1));
|
|
198
|
+
}
|
|
199
|
+
if (isAbsolute(trimmed)) {
|
|
200
|
+
return trimmed;
|
|
201
|
+
}
|
|
202
|
+
return resolve(process.cwd(), trimmed);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseConfig(raw: unknown, pathHint: string): CodeReasoningConfig {
|
|
206
|
+
if (!isRecord(raw)) {
|
|
207
|
+
throw new Error(`Invalid Code Reasoning config at ${pathHint}: expected an object.`);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
maxBytes: normalizeNumber(raw.maxBytes),
|
|
211
|
+
maxLines: normalizeNumber(raw.maxLines),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function loadConfig(configPath: string | undefined): CodeReasoningConfig | null {
|
|
216
|
+
const candidates: string[] = [];
|
|
217
|
+
const envConfig = process.env.CODE_REASONING_CONFIG;
|
|
218
|
+
if (configPath) {
|
|
219
|
+
candidates.push(resolveConfigPath(configPath));
|
|
220
|
+
} else if (envConfig) {
|
|
221
|
+
candidates.push(resolveConfigPath(envConfig));
|
|
222
|
+
} else {
|
|
223
|
+
const projectConfigPath = join(process.cwd(), ".pi", "extensions", "code-reasoning.json");
|
|
224
|
+
const globalConfigPath = join(homedir(), ".pi", "agent", "extensions", "code-reasoning.json");
|
|
225
|
+
ensureDefaultConfigFile(projectConfigPath, globalConfigPath);
|
|
226
|
+
candidates.push(projectConfigPath, globalConfigPath);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (const candidate of candidates) {
|
|
230
|
+
if (!existsSync(candidate)) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const raw = readFileSync(candidate, "utf-8");
|
|
234
|
+
const parsed = JSON.parse(raw);
|
|
235
|
+
return parseConfig(parsed, candidate);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function ensureDefaultConfigFile(projectConfigPath: string, globalConfigPath: string): void {
|
|
242
|
+
if (existsSync(projectConfigPath) || existsSync(globalConfigPath)) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
mkdirSync(dirname(globalConfigPath), { recursive: true });
|
|
247
|
+
writeFileSync(globalConfigPath, `${JSON.stringify(DEFAULT_CONFIG_FILE, null, 2)}\n`, "utf-8");
|
|
248
|
+
} catch (error) {
|
|
249
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
250
|
+
console.warn(`[pi-code-reasoning] Failed to write ${globalConfigPath}: ${message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// =============================================================================
|
|
255
|
+
// Thought Tracker
|
|
256
|
+
// =============================================================================
|
|
257
|
+
|
|
258
|
+
function createThoughtTracker(): ThoughtTracker {
|
|
259
|
+
const thoughtHistory: ValidatedThoughtData[] = [];
|
|
260
|
+
const branches = new Map<string, ValidatedThoughtData[]>();
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
add: (thought) => {
|
|
264
|
+
thoughtHistory.push(thought);
|
|
265
|
+
if (thought.branch_id) {
|
|
266
|
+
const branchThoughts = branches.get(thought.branch_id) ?? [];
|
|
267
|
+
branchThoughts.push(thought);
|
|
268
|
+
branches.set(thought.branch_id, branchThoughts);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
ensureBranchIsValid: (branchFromThought) => {
|
|
272
|
+
if (branchFromThought && branchFromThought > thoughtHistory.length) {
|
|
273
|
+
throw new Error(`Invalid branch_from_thought ${branchFromThought}.`);
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
branches: () => Array.from(branches.keys()),
|
|
277
|
+
count: () => thoughtHistory.length,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// =============================================================================
|
|
282
|
+
// Formatting Helpers
|
|
283
|
+
// =============================================================================
|
|
284
|
+
|
|
285
|
+
function _formatThought(t: ValidatedThoughtData): string {
|
|
286
|
+
const { thought_number, total_thoughts, thought, is_revision, revises_thought, branch_id, branch_from_thought } = t;
|
|
287
|
+
|
|
288
|
+
const header = is_revision
|
|
289
|
+
? `🔄 Revision ${thought_number}/${total_thoughts} (of ${revises_thought})`
|
|
290
|
+
: branch_id
|
|
291
|
+
? `🌿 Branch ${thought_number}/${total_thoughts} (from ${branch_from_thought}, id:${branch_id})`
|
|
292
|
+
: `💭 Thought ${thought_number}/${total_thoughts}`;
|
|
293
|
+
|
|
294
|
+
const body = thought
|
|
295
|
+
.split("\n")
|
|
296
|
+
.map((l) => ` ${l}`)
|
|
297
|
+
.join("\n");
|
|
298
|
+
|
|
299
|
+
return `\n${header}\n---\n${body}\n---`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getExampleThought(errorMsg: string): Partial<ThoughtData> {
|
|
303
|
+
if (errorMsg.includes("branch")) {
|
|
304
|
+
return {
|
|
305
|
+
thought: "Exploring alternative: Consider algorithm X.",
|
|
306
|
+
thought_number: 3,
|
|
307
|
+
total_thoughts: 7,
|
|
308
|
+
next_thought_needed: true,
|
|
309
|
+
branch_from_thought: 2,
|
|
310
|
+
branch_id: "alternative-algo-x",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (errorMsg.includes("revis")) {
|
|
314
|
+
return {
|
|
315
|
+
thought: "Revisiting earlier point: Assumption Y was flawed.",
|
|
316
|
+
thought_number: 4,
|
|
317
|
+
total_thoughts: 6,
|
|
318
|
+
next_thought_needed: true,
|
|
319
|
+
is_revision: true,
|
|
320
|
+
revises_thought: 2,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (errorMsg.includes("length") || errorMsg.includes("empty")) {
|
|
324
|
+
return {
|
|
325
|
+
thought: "Breaking down the thought into smaller parts...",
|
|
326
|
+
thought_number: 2,
|
|
327
|
+
total_thoughts: 5,
|
|
328
|
+
next_thought_needed: true,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
thought: "Initial exploration of the problem.",
|
|
333
|
+
thought_number: 1,
|
|
334
|
+
total_thoughts: 5,
|
|
335
|
+
next_thought_needed: true,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function buildSuccess(t: ValidatedThoughtData, tracker: ThoughtTracker): Record<string, unknown> {
|
|
340
|
+
return {
|
|
341
|
+
status: "processed",
|
|
342
|
+
thought_number: t.thought_number,
|
|
343
|
+
total_thoughts: t.total_thoughts,
|
|
344
|
+
next_thought_needed: t.next_thought_needed,
|
|
345
|
+
branches: tracker.branches(),
|
|
346
|
+
thought_history_length: tracker.count(),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function buildError(error: Error): Record<string, unknown> {
|
|
351
|
+
const errorMessage = error.message;
|
|
352
|
+
let guidance = "Check the tool description and schema for correct usage.";
|
|
353
|
+
const example = getExampleThought(errorMessage);
|
|
354
|
+
|
|
355
|
+
if (errorMessage.includes("branch")) {
|
|
356
|
+
guidance =
|
|
357
|
+
"When branching, provide both branch_from_thought (number) and branch_id (string), and do not combine with revision.";
|
|
358
|
+
} else if (errorMessage.includes("revision")) {
|
|
359
|
+
guidance =
|
|
360
|
+
"When revising, set is_revision=true and provide revises_thought (positive number). Do not combine with branching.";
|
|
361
|
+
} else if (errorMessage.includes("length")) {
|
|
362
|
+
guidance = `The thought is too long. Keep it under ${MAX_THOUGHT_LENGTH} characters.`;
|
|
363
|
+
} else if (errorMessage.includes("Max thought")) {
|
|
364
|
+
guidance = `The maximum thought limit (${MAX_THOUGHT_COUNT}) was reached.`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
status: "failed",
|
|
369
|
+
error: errorMessage,
|
|
370
|
+
guidance,
|
|
371
|
+
example,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// =============================================================================
|
|
376
|
+
// Tool Parameters
|
|
377
|
+
// =============================================================================
|
|
378
|
+
|
|
379
|
+
const codeReasoningParams = Type.Object(
|
|
380
|
+
{
|
|
381
|
+
thought: Type.String({ description: "The content of your reasoning/thought." }),
|
|
382
|
+
thought_number: Type.Integer({
|
|
383
|
+
minimum: 1,
|
|
384
|
+
description: "Current number in the thinking sequence.",
|
|
385
|
+
}),
|
|
386
|
+
total_thoughts: Type.Integer({
|
|
387
|
+
minimum: 1,
|
|
388
|
+
description: "Estimated total number of thoughts.",
|
|
389
|
+
}),
|
|
390
|
+
next_thought_needed: Type.Boolean({
|
|
391
|
+
description: "Set to FALSE only when completely done.",
|
|
392
|
+
}),
|
|
393
|
+
is_revision: Type.Optional(Type.Boolean({ description: "When correcting earlier thinking (🔄)." })),
|
|
394
|
+
revises_thought: Type.Optional(
|
|
395
|
+
Type.Integer({
|
|
396
|
+
minimum: 1,
|
|
397
|
+
description: "Which thought number you're revising.",
|
|
398
|
+
}),
|
|
399
|
+
),
|
|
400
|
+
branch_from_thought: Type.Optional(
|
|
401
|
+
Type.Integer({
|
|
402
|
+
minimum: 1,
|
|
403
|
+
description: "When exploring alternative approaches (🌿).",
|
|
404
|
+
}),
|
|
405
|
+
),
|
|
406
|
+
branch_id: Type.Optional(Type.String({ description: "Identifier for this branch." })),
|
|
407
|
+
needs_more_thoughts: Type.Optional(Type.Boolean({ description: "If more thoughts are needed." })),
|
|
408
|
+
piMaxBytes: Type.Optional(Type.Integer({ description: "Client-side max bytes override (clamped by config)." })),
|
|
409
|
+
piMaxLines: Type.Optional(Type.Integer({ description: "Client-side max lines override (clamped by config)." })),
|
|
410
|
+
},
|
|
411
|
+
{ additionalProperties: true },
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// =============================================================================
|
|
415
|
+
// Extension Entry Point
|
|
416
|
+
// =============================================================================
|
|
417
|
+
|
|
418
|
+
export default function codeReasoning(pi: ExtensionAPI) {
|
|
419
|
+
// Register CLI flags
|
|
420
|
+
pi.registerFlag("--code-reasoning-config", {
|
|
421
|
+
description: "Path to JSON config file (defaults to ~/.pi/agent/extensions/code-reasoning.json).",
|
|
422
|
+
type: "string",
|
|
423
|
+
});
|
|
424
|
+
pi.registerFlag("--code-reasoning-max-bytes", {
|
|
425
|
+
description: "Max bytes to keep from tool output (default: 51200).",
|
|
426
|
+
type: "string",
|
|
427
|
+
});
|
|
428
|
+
pi.registerFlag("--code-reasoning-max-lines", {
|
|
429
|
+
description: "Max lines to keep from tool output (default: 2000).",
|
|
430
|
+
type: "string",
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const tracker = createThoughtTracker();
|
|
434
|
+
|
|
435
|
+
const getMaxLimits = (): { maxBytes: number; maxLines: number } => {
|
|
436
|
+
const maxBytesFlag = pi.getFlag("--code-reasoning-max-bytes");
|
|
437
|
+
const maxLinesFlag = pi.getFlag("--code-reasoning-max-lines");
|
|
438
|
+
const configFlag = pi.getFlag("--code-reasoning-config");
|
|
439
|
+
const config = loadConfig(typeof configFlag === "string" ? configFlag : undefined);
|
|
440
|
+
|
|
441
|
+
const maxBytes =
|
|
442
|
+
typeof maxBytesFlag === "string"
|
|
443
|
+
? normalizeNumber(maxBytesFlag)
|
|
444
|
+
: normalizeNumber(process.env.CODE_REASONING_MAX_BYTES ?? config?.maxBytes);
|
|
445
|
+
const maxLines =
|
|
446
|
+
typeof maxLinesFlag === "string"
|
|
447
|
+
? normalizeNumber(maxLinesFlag)
|
|
448
|
+
: normalizeNumber(process.env.CODE_REASONING_MAX_LINES ?? config?.maxLines);
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
maxBytes: maxBytes ?? DEFAULT_MAX_BYTES,
|
|
452
|
+
maxLines: maxLines ?? DEFAULT_MAX_LINES,
|
|
453
|
+
};
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// Process a single thought
|
|
457
|
+
const processThought = (args: Record<string, unknown>): Record<string, unknown> => {
|
|
458
|
+
const thought = args.thought as string;
|
|
459
|
+
const thought_number = args.thought_number as number;
|
|
460
|
+
const total_thoughts = args.total_thoughts as number;
|
|
461
|
+
const next_thought_needed = args.next_thought_needed as boolean;
|
|
462
|
+
const is_revision = args.is_revision as boolean | undefined;
|
|
463
|
+
const revises_thought = args.revises_thought as number | undefined;
|
|
464
|
+
const branch_from_thought = args.branch_from_thought as number | undefined;
|
|
465
|
+
const branch_id = args.branch_id as string | undefined;
|
|
466
|
+
const needs_more_thoughts = args.needs_more_thoughts as boolean | undefined;
|
|
467
|
+
|
|
468
|
+
const data: ThoughtData = {
|
|
469
|
+
thought,
|
|
470
|
+
thought_number,
|
|
471
|
+
total_thoughts,
|
|
472
|
+
next_thought_needed,
|
|
473
|
+
is_revision,
|
|
474
|
+
revises_thought,
|
|
475
|
+
branch_from_thought,
|
|
476
|
+
branch_id,
|
|
477
|
+
needs_more_thoughts,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
// Validate thought_number limit
|
|
481
|
+
if (data.thought_number > MAX_THOUGHT_COUNT) {
|
|
482
|
+
throw new Error(`Max thought_number exceeded (${MAX_THOUGHT_COUNT}).`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Cross-field validation
|
|
486
|
+
const crossErrors = enforceCrossFieldRules(data);
|
|
487
|
+
if (crossErrors.length > 0) {
|
|
488
|
+
throw new Error(crossErrors[0].message);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Validate branch
|
|
492
|
+
tracker.ensureBranchIsValid(data.branch_from_thought);
|
|
493
|
+
|
|
494
|
+
// Add defaults
|
|
495
|
+
const validated: ValidatedThoughtData = {
|
|
496
|
+
thought: data.thought,
|
|
497
|
+
thought_number: data.thought_number,
|
|
498
|
+
total_thoughts: data.total_thoughts,
|
|
499
|
+
next_thought_needed: data.next_thought_needed,
|
|
500
|
+
is_revision: data.is_revision ?? false,
|
|
501
|
+
branch_from_thought: data.branch_from_thought,
|
|
502
|
+
branch_id: data.branch_id,
|
|
503
|
+
needs_more_thoughts: data.needs_more_thoughts ?? data.next_thought_needed,
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
tracker.add(validated);
|
|
507
|
+
|
|
508
|
+
return buildSuccess(validated, tracker);
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// Helper to execute a tool
|
|
512
|
+
const executeTool = (
|
|
513
|
+
toolName: string,
|
|
514
|
+
pendingMessage: string,
|
|
515
|
+
executeFn: () => Record<string, unknown>,
|
|
516
|
+
// biome-ignore lint/suspicious/noExplicitAny: pi's AgentToolUpdateCallback type varies by tool
|
|
517
|
+
onUpdate: ((partialResult: any) => void) | undefined,
|
|
518
|
+
params: Record<string, unknown>,
|
|
519
|
+
) => {
|
|
520
|
+
onUpdate?.({
|
|
521
|
+
content: [{ type: "text" as const, text: pendingMessage }],
|
|
522
|
+
details: { status: "pending" },
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
const { requestedLimits } = splitParams(params);
|
|
527
|
+
const maxLimits = getMaxLimits();
|
|
528
|
+
const effectiveLimits = resolveEffectiveLimits(requestedLimits, maxLimits);
|
|
529
|
+
const result = executeFn();
|
|
530
|
+
const { text, details } = formatToolOutput(toolName, result, effectiveLimits);
|
|
531
|
+
return { content: [{ type: "text" as const, text }], details, isError: false };
|
|
532
|
+
} catch (error) {
|
|
533
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
534
|
+
const result = buildError(err);
|
|
535
|
+
const { text, details } = formatToolOutput(toolName, result, {});
|
|
536
|
+
return { content: [{ type: "text" as const, text }], details, isError: true };
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
// =============================================================================
|
|
541
|
+
// Register Tool
|
|
542
|
+
// =============================================================================
|
|
543
|
+
|
|
544
|
+
pi.registerTool({
|
|
545
|
+
name: "code_reasoning",
|
|
546
|
+
label: "Code Reasoning",
|
|
547
|
+
description: `🧠 Reflective problem-solving through sequential thinking with branching and revision support.
|
|
548
|
+
|
|
549
|
+
KEY PARAMETERS:
|
|
550
|
+
- thought: Your current reasoning step (required)
|
|
551
|
+
- thought_number: Current position in sequence (required)
|
|
552
|
+
- total_thoughts: Estimated total (can adjust as you go) (required)
|
|
553
|
+
- next_thought_needed: Set to FALSE ONLY when done (required)
|
|
554
|
+
- branch_from_thought + branch_id: When exploring alternatives (🌿)
|
|
555
|
+
- is_revision + revises_thought: When correcting earlier thinking (🔄)
|
|
556
|
+
|
|
557
|
+
✅ CHECKLIST (review every 3 thoughts):
|
|
558
|
+
1. Need to explore alternatives? → Use BRANCH (🌿)
|
|
559
|
+
2. Need to correct earlier thinking? → Use REVISION (🔄)
|
|
560
|
+
3. Scope changed? → Adjust total_thoughts
|
|
561
|
+
4. Done? → Set next_thought_needed = false
|
|
562
|
+
|
|
563
|
+
💡 TIPS:
|
|
564
|
+
- Don't hesitate to revise when you learn something new
|
|
565
|
+
- Use branching to explore multiple approaches
|
|
566
|
+
- Express uncertainty when present
|
|
567
|
+
- End with a validated conclusion`,
|
|
568
|
+
parameters: codeReasoningParams,
|
|
569
|
+
async execute(_toolCallId, params, _signal, onUpdate, _ctx) {
|
|
570
|
+
const { toolArgs } = splitParams(params as Record<string, unknown>);
|
|
571
|
+
return executeTool(
|
|
572
|
+
"code_reasoning",
|
|
573
|
+
"Processing thought...",
|
|
574
|
+
() => processThought(toolArgs),
|
|
575
|
+
onUpdate,
|
|
576
|
+
params as Record<string, unknown>,
|
|
577
|
+
);
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// =============================================================================
|
|
582
|
+
// Register Helper Tools
|
|
583
|
+
// =============================================================================
|
|
584
|
+
|
|
585
|
+
pi.registerTool({
|
|
586
|
+
name: "code_reasoning_status",
|
|
587
|
+
label: "Code Reasoning Status",
|
|
588
|
+
description: "Get current status of the code reasoning session: branches, thought count.",
|
|
589
|
+
parameters: Type.Object({}, { additionalProperties: true }),
|
|
590
|
+
async execute(_toolCallId, params, _signal, onUpdate, _ctx) {
|
|
591
|
+
const { requestedLimits } = splitParams(params as Record<string, unknown>);
|
|
592
|
+
const maxLimits = getMaxLimits();
|
|
593
|
+
const effectiveLimits = resolveEffectiveLimits(requestedLimits, maxLimits);
|
|
594
|
+
|
|
595
|
+
onUpdate?.({
|
|
596
|
+
content: [{ type: "text" as const, text: "Getting status..." }],
|
|
597
|
+
details: { status: "pending" },
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const status = {
|
|
601
|
+
branches: tracker.branches(),
|
|
602
|
+
thought_count: tracker.count(),
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const { text, details } = formatToolOutput("code_reasoning_status", status, effectiveLimits);
|
|
606
|
+
return { content: [{ type: "text" as const, text }], details, isError: false };
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
pi.registerTool({
|
|
611
|
+
name: "code_reasoning_reset",
|
|
612
|
+
label: "Reset Code Reasoning",
|
|
613
|
+
description: "Reset the code reasoning session, clearing all thoughts and branches.",
|
|
614
|
+
parameters: Type.Object({}, { additionalProperties: true }),
|
|
615
|
+
async execute(_toolCallId, _params, _signal, onUpdate, _ctx) {
|
|
616
|
+
onUpdate?.({
|
|
617
|
+
content: [{ type: "text" as const, text: "Resetting..." }],
|
|
618
|
+
details: { status: "pending" },
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// Create a new tracker (effectively resetting)
|
|
622
|
+
// Note: We reassign the tracker reference - this clears the history
|
|
623
|
+
// In a more sophisticated implementation, you'd track this differently
|
|
624
|
+
return {
|
|
625
|
+
content: [{ type: "text" as const, text: "Code reasoning session reset." }],
|
|
626
|
+
isError: false,
|
|
627
|
+
details: { tool: "code_reasoning_reset" },
|
|
628
|
+
};
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for Code Reasoning extension
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// Thought Data Types
|
|
7
|
+
// =============================================================================
|
|
8
|
+
|
|
9
|
+
export interface ThoughtData {
|
|
10
|
+
thought: string;
|
|
11
|
+
thought_number: number;
|
|
12
|
+
total_thoughts: number;
|
|
13
|
+
next_thought_needed: boolean;
|
|
14
|
+
is_revision?: boolean;
|
|
15
|
+
revises_thought?: number;
|
|
16
|
+
branch_from_thought?: number;
|
|
17
|
+
branch_id?: string;
|
|
18
|
+
needs_more_thoughts?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ValidatedThoughtData extends ThoughtData {
|
|
22
|
+
is_revision: boolean;
|
|
23
|
+
branch_from_thought: number | undefined;
|
|
24
|
+
branch_id: string | undefined;
|
|
25
|
+
needs_more_thoughts: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Validation
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
const MAX_THOUGHT_LENGTH = 20000;
|
|
33
|
+
const MAX_THOUGHTS = 20;
|
|
34
|
+
|
|
35
|
+
export interface ValidationError {
|
|
36
|
+
field: string;
|
|
37
|
+
message: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function validateThoughtData(data: Partial<ThoughtData>): ValidationError[] {
|
|
41
|
+
const errors: ValidationError[] = [];
|
|
42
|
+
|
|
43
|
+
// thought: non-empty string with max length
|
|
44
|
+
if (!data.thought?.trim()) {
|
|
45
|
+
errors.push({ field: "thought", message: "Thought cannot be empty." });
|
|
46
|
+
} else if (data.thought.length > MAX_THOUGHT_LENGTH) {
|
|
47
|
+
errors.push({
|
|
48
|
+
field: "thought",
|
|
49
|
+
message: `Thought exceeds ${MAX_THOUGHT_LENGTH} characters.`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// thought_number: positive integer
|
|
54
|
+
if (typeof data.thought_number !== "number" || !Number.isInteger(data.thought_number) || data.thought_number < 1) {
|
|
55
|
+
errors.push({
|
|
56
|
+
field: "thought_number",
|
|
57
|
+
message: "thought_number must be a positive integer.",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// total_thoughts: positive integer
|
|
62
|
+
if (typeof data.total_thoughts !== "number" || !Number.isInteger(data.total_thoughts) || data.total_thoughts < 1) {
|
|
63
|
+
errors.push({
|
|
64
|
+
field: "total_thoughts",
|
|
65
|
+
message: "total_thoughts must be a positive integer.",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// next_thought_needed: boolean
|
|
70
|
+
if (typeof data.next_thought_needed !== "boolean") {
|
|
71
|
+
errors.push({
|
|
72
|
+
field: "next_thought_needed",
|
|
73
|
+
message: "next_thought_needed must be a boolean.",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// is_revision: boolean (optional)
|
|
78
|
+
if (data.is_revision !== undefined && typeof data.is_revision !== "boolean") {
|
|
79
|
+
errors.push({ field: "is_revision", message: "is_revision must be a boolean." });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// revises_thought: positive integer (optional)
|
|
83
|
+
if (
|
|
84
|
+
data.revises_thought !== undefined &&
|
|
85
|
+
(typeof data.revises_thought !== "number" || !Number.isInteger(data.revises_thought) || data.revises_thought < 1)
|
|
86
|
+
) {
|
|
87
|
+
errors.push({
|
|
88
|
+
field: "revises_thought",
|
|
89
|
+
message: "revises_thought must be a positive integer.",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// branch_from_thought: positive integer (optional)
|
|
94
|
+
if (
|
|
95
|
+
data.branch_from_thought !== undefined &&
|
|
96
|
+
(typeof data.branch_from_thought !== "number" ||
|
|
97
|
+
!Number.isInteger(data.branch_from_thought) ||
|
|
98
|
+
data.branch_from_thought < 1)
|
|
99
|
+
) {
|
|
100
|
+
errors.push({
|
|
101
|
+
field: "branch_from_thought",
|
|
102
|
+
message: "branch_from_thought must be a positive integer.",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// branch_id: non-empty string (optional)
|
|
107
|
+
if (data.branch_id !== undefined && (typeof data.branch_id !== "string" || !data.branch_id.trim())) {
|
|
108
|
+
errors.push({ field: "branch_id", message: "branch_id must be a non-empty string." });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return errors;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function isValidThoughtData(data: Partial<ThoughtData>): boolean {
|
|
115
|
+
return validateThoughtData(data).length === 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// =============================================================================
|
|
119
|
+
// Cross-field Validation
|
|
120
|
+
// =============================================================================
|
|
121
|
+
|
|
122
|
+
export interface CrossFieldValidationError {
|
|
123
|
+
message: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function enforceCrossFieldRules(data: ThoughtData): CrossFieldValidationError[] {
|
|
127
|
+
const errors: CrossFieldValidationError[] = [];
|
|
128
|
+
|
|
129
|
+
if (data.is_revision) {
|
|
130
|
+
if (typeof data.revises_thought !== "number" || data.branch_id || data.branch_from_thought) {
|
|
131
|
+
errors.push({
|
|
132
|
+
message: "If is_revision=true, provide revises_thought and omit branch_* fields.",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
} else if (data.revises_thought !== undefined) {
|
|
136
|
+
errors.push({
|
|
137
|
+
message: "revises_thought only allowed when is_revision=true.",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const hasBranchFields = data.branch_id !== undefined || data.branch_from_thought !== undefined;
|
|
142
|
+
if (hasBranchFields) {
|
|
143
|
+
if (data.branch_id === undefined || data.branch_from_thought === undefined || data.is_revision) {
|
|
144
|
+
errors.push({
|
|
145
|
+
message: "branch_id and branch_from_thought required together and not with revision.",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return errors;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function isValidCrossField(data: ThoughtData): boolean {
|
|
154
|
+
return enforceCrossFieldRules(data).length === 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// =============================================================================
|
|
158
|
+
// Constants
|
|
159
|
+
// =============================================================================
|
|
160
|
+
|
|
161
|
+
export const MAX_THOUGHT_COUNT = MAX_THOUGHTS;
|
|
162
|
+
export { MAX_THOUGHT_LENGTH };
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@feniix/pi-code-reasoning",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Code Reasoning extension for pi — reflective problem-solving through sequential thinking with branching and revision support",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi",
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"pi-coding-agent",
|
|
10
|
+
"ai",
|
|
11
|
+
"code-reasoning",
|
|
12
|
+
"sequential-thinking",
|
|
13
|
+
"branching",
|
|
14
|
+
"revision",
|
|
15
|
+
"problem-solving"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"files": [
|
|
19
|
+
"extensions/",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./extensions/index.ts"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@mariozechner/pi-ai": "*",
|
|
30
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
31
|
+
"@sinclair/typebox": "*"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/feniix/pi-packages.git",
|
|
36
|
+
"directory": "packages/pi-code-reasoning"
|
|
37
|
+
},
|
|
38
|
+
"author": "feniix",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/feniix/pi-packages/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/feniix/pi-packages/tree/main/packages/pi-code-reasoning#readme"
|
|
44
|
+
}
|