pi-supernova 0.0.15 → 0.1.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/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- import { isString, isFunction, isObject } from "./decode.js";
2
+ import { isString, isFunction } from "./decode.js";
3
3
  import { buildCatalog, searchCatalog, describeTool, mergeNativeToolDefinitions } from "./catalog.js";
4
4
  import { loadConfig } from "./config.js";
5
5
  import { createHostBridge } from "./host-bridge.js";
@@ -82,7 +82,7 @@ function errorText(outcome, call) {
82
82
 
83
83
  function successText(outcome, call) {
84
84
  const truncated = outcome.returnTruncated ? " [return truncated]" : "";
85
- const hint = outcome.undefinedReturn ? " (no return statement; add \`return\` to get a value)" : "";
85
+ const hint = outcome.undefinedReturn ? " (no return statement; add `return` to get a value)" : "";
86
86
  return `ok #${call} ${outcome.wallMs}ms${truncated}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
87
87
  }
88
88
  const TOOL_DESCRIPTION = `Run one JavaScript program that composes host tools. Async body or arrow; \`return\` a small shaped value (compact literal, capped; strings raw; console.log is captured).
@@ -111,50 +111,38 @@ export default function piSupernova(pi) {
111
111
  getCwd: () => cwd,
112
112
  });
113
113
 
114
- function refreshCatalog() {
115
- let tools = [];
116
- try {
117
- if (isFunction(pi.getAllTools)) {
118
- tools = pi.getAllTools() || [];
119
- }
120
- } catch {
121
- tools = [];
122
- }
123
- const discoverable = mergeNativeToolDefinitions(tools, bridge.executors.keys());
114
+ function refreshCatalog(target = bridge) {
115
+ const tools = target.refreshTools();
116
+ const discoverable = mergeNativeToolDefinitions(tools, target.externalNames())
117
+ .filter(tool => target.isCallable(tool.name))
118
+ .map(tool => {
119
+ const schema = tool.parameters;
120
+ try {
121
+ if (isFunction(schema?.toJsonSchema)) return { ...tool, parameters: schema.toJsonSchema() };
122
+ if (schema && !schema.type && pi.zod?.toJSONSchema && (schema._zod || schema._def)) {
123
+ return { ...tool, parameters: pi.zod.toJSONSchema(schema, { io: "input" }) };
124
+ }
125
+ return tool;
126
+ } catch (error) {
127
+ return { ...tool, parameters: undefined, schemaError: error.message };
128
+ }
129
+ });
124
130
  catalog = buildCatalog(discoverable, config.excludeTools || []);
125
131
  return catalog;
126
132
  }
127
133
 
128
- function makeNovaApi() {
134
+ function makeNovaApi(runBridge, runCatalog, cancel) {
129
135
  return {
130
- async search(query, limit) {
131
- const cat = catalog.length ? catalog : refreshCatalog();
132
- const lim = Number.isInteger(limit) ? limit : config.maxSearchResults;
133
- return searchCatalog(cat, query, lim);
134
- },
135
- async describe(name) {
136
- const cat = catalog.length ? catalog : refreshCatalog();
137
- return describeTool(cat, name);
138
- },
139
- async call(name, args) {
140
- return bridge.call(name, args);
141
- },
142
- async callMany(calls) {
143
- return bridge.callMany(calls);
144
- },
145
- speculateBegin() {
146
- bridge.beginSpeculation();
147
- },
148
- async speculateCommit() {
149
- await bridge.commitSpeculation();
150
- },
151
- speculateRollback() {
152
- bridge.rollbackSpeculation();
153
- },
154
- names() {
155
- const cat = catalog.length ? catalog : refreshCatalog();
156
- return [...new Set([...cat.map((t) => t.name), ...bridge.executors.keys(), ...Object.keys(bridge.natives)])];
157
- },
136
+ search: async (query, limit) => searchCatalog(runCatalog, query, Number.isInteger(limit) ? limit : config.maxSearchResults),
137
+ describe: async (name) => describeTool(runCatalog, name),
138
+ call: (name, args) => runBridge.call(name, args),
139
+ callMany: (calls) => runBridge.callMany(calls),
140
+ speculateBegin: () => runBridge.beginSpeculation(),
141
+ speculateCommit: () => runBridge.commitSpeculation(),
142
+ speculateRollback: () => runBridge.rollbackSpeculation(),
143
+ names: () => runCatalog.map(tool => tool.name),
144
+ batchRead: runBridge.supportsBatchRead(),
145
+ cancel,
158
146
  };
159
147
  }
160
148
 
@@ -177,70 +165,62 @@ export default function piSupernova(pi) {
177
165
  renderCall: renderSupernovaCall,
178
166
  renderResult: renderSupernovaResult,
179
167
  async execute(_id, params, signal, onUpdate, ctx) {
180
- if (ctx && isString(ctx.cwd) && ctx.cwd) cwd = ctx.cwd;
168
+ const runCwd = ctx?.cwd || cwd;
181
169
  const runController = new AbortController();
182
170
  const abortRun = () => runController.abort(signal?.reason);
183
171
  if (signal?.aborted) abortRun();
184
172
  else signal?.addEventListener("abort", abortRun, { once: true });
173
+ const runBridge = bridge.fork({ getCwd: () => runCwd });
174
+ runBridge.bindCallContext(ctx, runController.signal);
175
+ runBridge.resetCallBudget();
185
176
 
186
- bridge.bindCallContext(ctx, runController.signal);
187
- bridge.resetCallBudget();
188
- refreshCatalog();
189
- bridge.beginSpeculation();
190
177
  const call = ++programSeq;
191
- bridge.ledger.beginProgram(call);
178
+ runBridge.ledger.beginProgram(call);
192
179
  const emitProgress = progressEmitter(onUpdate);
193
- bridge.setCallListener((_record, allTrace) => emitProgress(allTrace));
180
+ runBridge.setCallListener((_record, trace) => emitProgress(trace));
194
181
  emitProgress([]);
195
-
182
+ const started = performance.now();
196
183
  let outcome;
197
184
  try {
198
- outcome = await runProgram(params, runController.signal, abortRun);
185
+ const runCatalog = refreshCatalog(runBridge);
186
+ runBridge.beginSpeculation();
187
+ outcome = await runGuestProgram({
188
+ code: params?.code,
189
+ nova: makeNovaApi(runBridge, runCatalog, abortRun),
190
+ config: { ...config, timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs },
191
+ signal: runController.signal,
192
+ onTimeout: abortRun,
193
+ });
194
+ runBridge.close();
195
+ if (outcome.ok) {
196
+ if (runBridge.getOverlayDepth() !== 1) throw new Error("program ended with an unfinished nova.speculate branch; await it before returning");
197
+ await runBridge.commitSpeculation();
198
+ }
199
+ else runBridge.rollbackSpeculation();
200
+ } catch (error) {
201
+ abortRun();
202
+ runBridge.close();
203
+ runBridge.rollbackSpeculation();
204
+ outcome = { ok: false, error: error instanceof Error ? error.message : String(error), logs: outcome?.logs ?? [], wallMs: Math.round(performance.now() - started) };
199
205
  } finally {
200
- bridge.setCallListener(null);
206
+ runBridge.setCallListener(null);
201
207
  emitProgress.flush();
202
208
  signal?.removeEventListener("abort", abortRun);
203
209
  }
204
-
205
- const trace = bridge.getTrace();
206
- if (!outcome.ok) {
207
- bridge.rollbackSpeculation();
208
- return result(bridge.ledger.dedupe(errorText(outcome, call), call), { ok: false, error: outcome.error, wallMs: outcome.wallMs, logs: outcome.logs, trace });
209
- }
210
- await bridge.commitSpeculation();
211
- return result(bridge.ledger.dedupe(successText(outcome, call), call), {
212
- ok: true,
213
- wallMs: outcome.wallMs,
214
- returnTruncated: outcome.returnTruncated,
215
- logTruncated: outcome.logTruncated,
216
- logs: outcome.logs,
217
- result: outcome.result,
218
- trace,
210
+ const trace = runBridge.getTrace();
211
+ const text = outcome.ok ? successText(outcome, call) : errorText(outcome, call);
212
+ return result(runBridge.ledger.dedupe(text, call), {
213
+ ok: outcome.ok, error: outcome.error, wallMs: outcome.wallMs,
214
+ returnTruncated: outcome.returnTruncated, logTruncated: outcome.logTruncated,
215
+ logs: outcome.logs, result: outcome.result, trace,
219
216
  });
220
217
  },
221
218
  });
222
219
 
223
- async function runProgram(params, signal, onTimeout) {
224
- const runStartedAt = performance.now();
225
- const runConfig = {
226
- ...config,
227
- timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs,
228
- };
229
- try {
230
- return await runGuestProgram({ code: String(params?.code || ""), nova: makeNovaApi(), config: runConfig, signal, onTimeout });
231
- } catch (error) {
232
- return {
233
- ok: false,
234
- error: error instanceof Error ? error.message : String(error),
235
- logs: [],
236
- wallMs: Math.round(performance.now() - runStartedAt),
237
- };
238
- }
239
- }
240
-
241
220
  pi.on("session_start", (_event, ctx) => {
242
221
  if (ctx && isString(ctx.cwd) && ctx.cwd) cwd = ctx.cwd;
243
222
  // A new session is a new model context: nothing has been seen yet.
223
+ bridge.bindCallContext(ctx);
244
224
  bridge.ledger.reset();
245
225
  programSeq = 0;
246
226
  refreshCatalog();
@@ -248,14 +228,15 @@ export default function piSupernova(pi) {
248
228
  });
249
229
 
250
230
  pi.registerCommand("supernova", {
251
- description: "Show pi-supernova status (catalog size, captured executors)",
231
+ description: "Show pi-supernova status (callable tools and session statistics)",
252
232
  handler: async (_args, ctx) => {
233
+ bridge.bindCallContext(ctx);
253
234
  refreshCatalog();
254
- const captured = [...bridge.executors.keys()].sort();
255
- const natives = Object.keys(bridge.natives).sort();
235
+ const external = bridge.externalNames().filter(bridge.isCallable).sort();
236
+ const natives = Object.keys(bridge.natives).filter(name => bridge.isCallable(name) && !external.includes(name)).sort();
256
237
  const lines = [
257
238
  `pi-supernova catalog: ${catalog.length} tools`,
258
- `captured executors: ${captured.length ? captured.join(", ") : "(none yet; load this package early)"}`,
239
+ `external tools: ${external.length ? external.join(", ") : "(none)"}`,
259
240
  `native adapters: ${natives.join(", ")}`,
260
241
  `timeoutMs=${config.timeoutMs} maxCallResultChars=${config.maxCallResultChars} maxReturnChars=${config.maxReturnChars} maxBridgeCalls=${config.maxBridgeCalls} maxHeapMb=${config.maxHeapMb}`,
261
242
  sessionStats(bridge.ledger.stats),
package/ledger.js CHANGED
@@ -1,13 +1,3 @@
1
- // Seen-ledger: the model's context window is a memory. Nothing that already reached the
2
- // model in this session is sent again verbatim. A run of identical lines (≥ MIN_RUN, with
3
- // enough substantive lines) collapses to one marker that cites the earlier program and,
4
- // when the lines came from a file, path:a–b, so one read(path, a, n) recovers them.
5
- //
6
- // This is not compression: every collapsed line already exists, verbatim, in the model's
7
- // context. Changed lines are never collapsed, so a re-read after an edit shows exactly the
8
- // delta. Lines the current program read with an explicit offset/limit are pinned and always
9
- // shown: that is the model asking for a specific window on purpose.
10
-
11
1
  const MIN_RUN = 6;
12
2
  const MIN_SUBSTANTIVE = 4;
13
3
  const MAX_CANDIDATES = 8;
@@ -15,136 +5,118 @@ const DEFAULT_WINDOW = 40;
15
5
  const MAX_STORED_LINES = 200_000;
16
6
 
17
7
  function hashLine(line) {
18
- let h = 0x811c9dc5;
19
- for (let i = 0; i < line.length; i++) {
20
- h ^= line.charCodeAt(i);
21
- h = Math.imul(h, 0x01000193);
22
- }
23
- return h >>> 0;
8
+ let hash = 0x811c9dc5;
9
+ for (let i = 0; i < line.length; i++) hash = Math.imul(hash ^ line.charCodeAt(i), 0x01000193);
10
+ return hash >>> 0;
24
11
  }
25
-
26
- function substantive(line) {
27
- return line.trim().length >= 8;
12
+ function substantive(line) { return line.trim().length >= 8; }
13
+ function newHistory() {
14
+ return { results: new Map(), occurrences: new Map(), storedLines: 0, latestCall: 0,
15
+ stats: { programs: 0, returnedChars: 0, collapsedChars: 0, collapsedRuns: 0 } };
28
16
  }
29
17
 
30
18
  export class SeenLedger {
31
- constructor({ window = DEFAULT_WINDOW } = {}) {
19
+ constructor({ window = DEFAULT_WINDOW, history } = {}) {
32
20
  this.window = window;
33
- this.results = new Map(); // call → { hashes: Uint32Array, lines: string[] }
34
- this.occurrences = new Map(); // hash → [{ call, index }]
35
- this.origins = new Map(); // hash → { path, line } (provenance recorded by the bridge)
36
- this.pinned = new Set(); // "path:line" pinned by the current program
37
- this.storedLines = 0;
38
- this.stats = { programs: 0, returnedChars: 0, collapsedChars: 0, collapsedRuns: 0 };
21
+ this.history = history ?? newHistory();
22
+ this.origins = new Map();
23
+ this.pinned = new Set();
39
24
  }
25
+ get results() { return this.history.results; }
26
+ get occurrences() { return this.history.occurrences; }
27
+ get storedLines() { return this.history.storedLines; }
28
+ get stats() { return this.history.stats; }
29
+ fork() { return new SeenLedger({ window: this.window, history: this.history }); }
40
30
 
41
31
  reset() {
42
- this.results.clear();
43
- this.occurrences.clear();
32
+ Object.assign(this.history, newHistory());
44
33
  this.origins.clear();
45
34
  this.pinned.clear();
46
- this.storedLines = 0;
47
- this.stats = { programs: 0, returnedChars: 0, collapsedChars: 0, collapsedRuns: 0 };
48
35
  }
49
36
 
50
37
  beginProgram(call) {
38
+ this.origins.clear();
51
39
  this.pinned.clear();
52
40
  this.stats.programs++;
53
- for (const old of [...this.results.keys()]) {
54
- if (old <= call - this.window) this.forget(old);
55
- }
41
+ this.history.latestCall = Math.max(this.history.latestCall, call);
42
+ for (const old of this.results.keys()) if (old <= this.history.latestCall - this.window) this.forget(old);
56
43
  }
57
44
 
58
45
  forget(call) {
59
46
  const entry = this.results.get(call);
60
47
  if (!entry) return;
61
- for (let i = 0; i < entry.hashes.length; i++) {
62
- const list = this.occurrences.get(entry.hashes[i]);
63
- if (!list) continue;
64
- const kept = list.filter((o) => o.call !== call);
65
- if (kept.length) this.occurrences.set(entry.hashes[i], kept);
66
- else this.occurrences.delete(entry.hashes[i]);
48
+ for (const hash of new Set(entry.hashes)) {
49
+ const kept = (this.occurrences.get(hash) ?? []).filter(item => item.call !== call);
50
+ if (kept.length) this.occurrences.set(hash, kept);
51
+ else this.occurrences.delete(hash);
67
52
  }
68
- this.storedLines -= entry.hashes.length;
53
+ this.history.storedLines -= entry.lines.length;
69
54
  this.results.delete(call);
70
55
  }
71
56
 
72
- /** Provenance for lines the bridge is about to hand to the program: file text, outlines, evidence spans. */
73
57
  recordOrigin(path, firstLine, lines, pin = false) {
58
+ if (this.window === 0) return;
74
59
  for (let i = 0; i < lines.length; i++) {
75
- if (!substantive(lines[i])) continue;
76
- this.origins.set(hashLine(lines[i]), { path, line: firstLine + i });
77
- if (pin) this.pinned.add(path + ":" + (firstLine + i));
60
+ const text = lines[i];
61
+ if (!substantive(text)) continue;
62
+ if (this.origins.size >= MAX_STORED_LINES && !this.origins.has(text)) this.origins.delete(this.origins.keys().next().value);
63
+ this.origins.set(text, { path, line: firstLine + i });
64
+ if (pin) this.pinned.add(text);
78
65
  }
79
66
  }
80
67
 
81
- isPinned(hash) {
82
- const o = this.origins.get(hash);
83
- return o !== undefined && this.pinned.has(o.path + ":" + o.line);
84
- }
85
-
86
- /** Length of the identical run between lines[i…] and earlier result `cand`, stopping at pinned lines. */
87
- runLength(hashes, cand, i) {
88
- const earlier = this.results.get(cand.call);
68
+ runLength(lines, hashes, candidate, index) {
69
+ const earlier = this.results.get(candidate.call);
89
70
  if (!earlier) return 0;
90
- let k = 0;
91
- while (i + k < hashes.length && cand.index + k < earlier.hashes.length && earlier.hashes[cand.index + k] === hashes[i + k] && !this.isPinned(hashes[i + k])) k++;
92
- return k;
71
+ let count = 0;
72
+ while (index + count < lines.length && candidate.index + count < earlier.lines.length
73
+ && hashes[index + count] === earlier.hashes[candidate.index + count]
74
+ && lines[index + count] === earlier.lines[candidate.index + count]
75
+ && !this.pinned.has(lines[index + count])) count++;
76
+ return count;
93
77
  }
94
78
 
95
- /** Longest earlier run starting at lines[i]; null when shorter than MIN_RUN or not substantive enough. */
96
- longestRun(hashes, lines, i) {
97
- const candidates = this.occurrences.get(hashes[i]);
79
+ longestRun(lines, hashes, index, call) {
80
+ const candidates = this.occurrences.get(hashes[index]);
98
81
  if (!candidates) return null;
99
82
  let best = null;
100
- for (const cand of candidates.slice(-MAX_CANDIDATES)) {
101
- const length = this.runLength(hashes, cand, i);
102
- if (length >= MIN_RUN && (!best || length > best.length)) best = { call: cand.call, index: cand.index, length };
83
+ for (const candidate of candidates.filter(item => item.call < call).slice(-MAX_CANDIDATES)) {
84
+ const length = this.runLength(lines, hashes, candidate, index);
85
+ if (length >= MIN_RUN && (!best || length > best.length)) best = { ...candidate, length };
103
86
  }
104
87
  if (!best) return null;
105
- const substantiveCount = lines.slice(i, i + best.length).filter(substantive).length;
106
- return substantiveCount >= MIN_SUBSTANTIVE ? best : null;
88
+ let count = 0;
89
+ for (let i = index; i < index + best.length; i++) if (substantive(lines[i])) count++;
90
+ return count >= MIN_SUBSTANTIVE ? best : null;
107
91
  }
108
92
 
109
- /** "path:a–b" when every line of the run has consecutive provenance in one file, else "". */
110
- citation(hashes, i, length) {
111
- const first = this.origins.get(hashes[i]);
93
+ citation(lines, index, run) {
94
+ const earlier = this.results.get(run.call);
95
+ const origin = offset => this.origins.get(lines[index + offset]) ?? earlier?.origins[run.index + offset];
96
+ const first = origin(0);
112
97
  if (!first) return "";
113
- let expectLine = first.line;
114
- for (let k = 0; k < length; k++) {
115
- const o = this.origins.get(hashes[i + k]);
116
- if (o) {
117
- if (o.path !== first.path || o.line < expectLine) return "";
118
- expectLine = o.line + 1;
119
- } else {
120
- expectLine++;
121
- }
98
+ let expected = first.line;
99
+ for (let i = 0; i < run.length; i++) {
100
+ const item = origin(i);
101
+ if (item && (item.path !== first.path || item.line !== expected)) return "";
102
+ expected++;
122
103
  }
123
- return first.path + ":" + first.line + "–" + (expectLine - 1);
104
+ return first.path + ":" + first.line + "–" + (expected - 1);
124
105
  }
125
106
 
126
- /**
127
- * Collapse runs already shown; returns the text to send and remembers exactly that text as call N.
128
- */
129
107
  dedupe(text, call) {
130
- const lines = text.split("\n");
131
- if (lines.length < MIN_RUN) {
108
+ if (this.window === 0) {
132
109
  this.stats.returnedChars += text.length;
133
- this.remember(call, lines);
134
110
  return text;
135
111
  }
136
- const hashes = new Uint32Array(lines.length);
137
- for (let i = 0; i < lines.length; i++) hashes[i] = hashLine(lines[i]);
112
+ const lines = text.split("\n");
113
+ const hashes = Uint32Array.from(lines, hashLine);
138
114
  const out = [];
139
115
  let collapsedChars = 0;
140
- for (let i = 0; i < lines.length; ) {
141
- const run = substantive(lines[i]) ? this.longestRun(hashes, lines, i) : null;
142
- if (!run) {
143
- out.push(lines[i]);
144
- i++;
145
- continue;
146
- }
147
- const cite = this.citation(hashes, i, run.length);
116
+ for (let i = 0; i < lines.length;) {
117
+ const run = substantive(lines[i]) ? this.longestRun(lines, hashes, i, call) : null;
118
+ if (!run) { out.push(lines[i++]); continue; }
119
+ const cite = this.citation(lines, i, run);
148
120
  out.push("⋯ " + run.length + " lines same as #" + run.call + (cite ? " · " + cite : "") + " ⋯");
149
121
  for (let k = 0; k < run.length; k++) collapsedChars += lines[i + k].length + 1;
150
122
  this.stats.collapsedRuns++;
@@ -158,21 +130,21 @@ export class SeenLedger {
158
130
  }
159
131
 
160
132
  remember(call, lines) {
161
- if (this.storedLines + lines.length > MAX_STORED_LINES) {
162
- for (const old of [...this.results.keys()].sort((a, b) => a - b)) {
163
- this.forget(old);
164
- if (this.storedLines + lines.length <= MAX_STORED_LINES) break;
165
- }
133
+ if (this.window === 0 || call <= this.history.latestCall - this.window || lines.length > MAX_STORED_LINES) return;
134
+ if (this.results.has(call)) this.forget(call);
135
+ for (const old of [...this.results.keys()].sort((a, b) => a - b)) {
136
+ if (this.storedLines + lines.length <= MAX_STORED_LINES) break;
137
+ this.forget(old);
166
138
  }
167
- const hashes = new Uint32Array(lines.length);
139
+ const hashes = Uint32Array.from(lines, hashLine);
140
+ const origins = lines.map(line => this.origins.get(line));
168
141
  for (let i = 0; i < lines.length; i++) {
169
- hashes[i] = hashLine(lines[i]);
170
142
  if (!substantive(lines[i])) continue;
171
143
  let list = this.occurrences.get(hashes[i]);
172
144
  if (!list) this.occurrences.set(hashes[i], (list = []));
173
145
  list.push({ call, index: i });
174
146
  }
175
- this.results.set(call, { hashes });
176
- this.storedLines += lines.length;
147
+ this.results.set(call, { hashes, lines, origins });
148
+ this.history.storedLines += lines.length;
177
149
  }
178
150
  }
package/omp-frame.js CHANGED
@@ -127,7 +127,6 @@ function renderPortableFrame(theme, { header, sections = [], state = "pending",
127
127
  const border = borderPaint(theme, state, borderColor);
128
128
  const bgFn = bgFnForState(theme, state);
129
129
  const h = box.horizontal;
130
- const v = box.vertical;
131
130
  const cap = h.repeat(3);
132
131
 
133
132
  const paintBar = (leftChar, rightChar, label) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-supernova",
3
- "version": "0.0.15",
3
+ "version": "0.1.0",
4
4
  "description": "Dual-host CodeMode for Pi/OMP: progressive tool discovery, result bottleneck, and Amdahl Auto parallel.",
5
5
  "type": "module",
6
6
  "author": "AdityaVG13",
@@ -90,5 +90,9 @@
90
90
  "bugs": {
91
91
  "url": "https://github.com/AdityaVG13/pi-stack/issues"
92
92
  },
93
- "homepage": "https://github.com/AdityaVG13/pi-stack/tree/main/packages/pi-supernova#readme"
93
+ "homepage": "https://github.com/AdityaVG13/pi-stack/tree/main/packages/pi-supernova#readme",
94
+ "dependencies": {
95
+ "acorn": "^8.18.0",
96
+ "string-width": "^8.2.2"
97
+ }
94
98
  }
package/parallel.js CHANGED
@@ -1,47 +1,55 @@
1
+ import { isFunction } from "./decode.js";
1
2
 
2
- import { isString } from "./decode.js";
3
- export function isMutatingTool(name, config) {
4
- const exact = new Set(config.mutatingTools || []);
5
- if (exact.has(name)) return true;
6
- const prefixes = config.mutatingPrefixes || [];
7
- for (const prefix of prefixes) {
8
- if (isString(prefix) && prefix.length > 0 && name.startsWith(prefix)) return true;
3
+ const READ_ONLY_TOOLS = new Set(["read", "grep", "glob", "find", "ls", "snap", "evidence", "surface", "asgrep_search", "asgrep_status", "ast_grep", "web_search"]);
4
+ const READ_ONLY_LSP = new Set(["definition", "references", "hover", "symbols", "diagnostics", "implementation", "type_definition", "incoming_calls", "outgoing_calls"]);
5
+
6
+ export function isMutatingTool(name, config = {}, args = {}, definition) {
7
+ if ((config.mutatingTools ?? []).includes(name)) return true;
8
+ if ((config.mutatingPrefixes ?? []).some(prefix => prefix && name.startsWith(prefix))) return true;
9
+ if (definition?.annotations?.readOnlyHint === true) return false;
10
+ if (name === "lsp") {
11
+ const action = args.action ?? args.operation;
12
+ if (READ_ONLY_LSP.has(action)) return false;
13
+ if (["rename", "rename_file"].includes(action)) return args.apply !== false;
14
+ if (action === "code_actions") return args.apply === true;
15
+ return true;
9
16
  }
10
- return false;
11
- }
12
- async function runSerial(list) {
13
- const out = [];
14
- for (const thunk of list) out.push(await thunk());
15
- return out;
17
+ if (name === "todo") return args.op !== "view";
18
+ if (name === "hub") return !["list", "ps", "logs", "describe"].includes(args.op);
19
+ return !READ_ONLY_TOOLS.has(name);
16
20
  }
17
- function shouldParallelize(mode, anyMutating, count) {
18
- if (mode === "parallel") return true;
19
- if (mode !== "auto") return false;
20
- if (anyMutating) return false;
21
- return count > 1;
21
+
22
+ function requireArray(value, name) {
23
+ if (!Array.isArray(value)) throw new TypeError(name + " requires an array");
24
+ return value;
22
25
  }
26
+
23
27
  export async function runParallelWave(thunks, meta, options = {}) {
24
- const list = Array.isArray(thunks) ? thunks : [];
25
- if (list.length === 0) return { results: [], mode: "serial", reason: "empty" };
28
+ const list = requireArray(thunks, "parallel wave");
29
+ if (list.some(item => !isFunction(item))) throw new TypeError("parallel wave requires functions");
30
+ if (!list.length) return { results: [], mode: "serial", reason: "empty" };
26
31
  const { mode = "auto", config = {} } = options;
27
- const names = Array.isArray(meta?.names) ? meta.names : [];
28
- const anyMutating = names.some((n) => isString(n) && isMutatingTool(n, config));
29
- if (shouldParallelize(mode, anyMutating, list.length)) {
30
- const results = await Promise.all(list.map((thunk) => thunk()));
31
- return { results, mode: "parallel", reason: "independent-reads" };
32
+ const names = meta?.names ?? [];
33
+ const mutating = names.length !== list.length || names.some((name, i) => isMutatingTool(name, config, meta?.calls?.[i]?.args, meta?.definitions?.[i]));
34
+ if (!mutating && (mode === "parallel" || (mode === "auto" && list.length > 1))) {
35
+ // Do not finish a wave while its already-started host calls are still running.
36
+ const settled = await Promise.allSettled(list.map(thunk => Promise.resolve().then(thunk)));
37
+ const failure = settled.find(item => item.status === "rejected");
38
+ if (failure) throw failure.reason;
39
+ return { results: settled.map(item => item.value), mode: "parallel", reason: "independent-reads" };
32
40
  }
33
- const results = await runSerial(list);
34
- if (anyMutating) return { results, mode: "serial", reason: "mutating" };
35
- return { results, mode: "serial", reason: "single-or-forced" };
41
+ const results = [];
42
+ for (const thunk of list) results.push(await thunk());
43
+ return { results, mode: "serial", reason: mutating ? "mutating" : "single-or-forced" };
36
44
  }
45
+
37
46
  export async function parallel(items) {
38
- const list = Array.isArray(items) ? items : [];
39
- return Promise.all(list.map((item) => (item instanceof Function ? item() : item)));
47
+ return Promise.all(requireArray(items, "parallel").map(item => isFunction(item) ? item() : item));
40
48
  }
49
+
41
50
  export async function pipeline(items, ...stages) {
42
- let current = Array.isArray(items) ? items.slice() : [];
43
- for (const stage of stages) {
44
- current = await Promise.all(current.map((item) => stage(item)));
45
- }
51
+ let current = requireArray(items, "pipeline");
52
+ if (stages.some(stage => !isFunction(stage))) throw new TypeError("pipeline stages must be functions");
53
+ for (const stage of stages) current = await Promise.all(current.map(item => stage(item)));
46
54
  return current;
47
55
  }