@runfusion/fusion 0.18.1 → 0.20.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.
Files changed (60) hide show
  1. package/dist/bin.js +6533 -2558
  2. package/dist/client/assets/AgentDetailView-C6BG7O7i.js +18 -0
  3. package/dist/client/assets/AgentDetailView-CUtWvXBn.css +1 -0
  4. package/dist/client/assets/ChatView-DeXUYwSY.js +1 -0
  5. package/dist/client/assets/{DevServerView-r6V3FqkY.js → DevServerView-Dariyxt_.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-CTZE95Fk.js → DirectoryPicker-SchiK-Aq.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-DSEf1Lmg.js → DocumentsView-C6v-tBhG.js} +1 -1
  8. package/dist/client/assets/InsightsView-AWo5o_81.css +1 -0
  9. package/dist/client/assets/InsightsView-Cqim12az.js +11 -0
  10. package/dist/client/assets/{MemoryView-DicXjec9.js → MemoryView-CakLoJtY.js} +2 -2
  11. package/dist/client/assets/NodesView-BxGm3poT.js +14 -0
  12. package/dist/client/assets/{NodesView-sJgPLTzz.css → NodesView-fXqDk9ur.css} +1 -1
  13. package/dist/client/assets/PiExtensionsManager-lJbmskyZ.js +6 -0
  14. package/dist/client/assets/PluginManager-BZjNNf9m.js +1 -0
  15. package/dist/client/assets/ResearchView-Bzsr9V0y.js +1 -0
  16. package/dist/client/assets/{RoadmapsView-DfEF3mql.js → RoadmapsView-CeKks_OI.js} +2 -2
  17. package/dist/client/assets/SettingsModal-BWe0KrGY.css +1 -0
  18. package/dist/client/assets/SettingsModal-D-9CLguN.js +31 -0
  19. package/dist/client/assets/{SettingsModal-YcScdFiG.js → SettingsModal-YdeVPhRJ.js} +1 -1
  20. package/dist/client/assets/{SetupWizardModal-DRF5fOoR.css → SetupWizardModal-CGYGKurR.css} +1 -1
  21. package/dist/client/assets/SetupWizardModal-DAC04LlA.js +1 -0
  22. package/dist/client/assets/{SkillsView-Dkq2CQla.js → SkillsView-CClC_5RN.js} +1 -1
  23. package/dist/client/assets/index-CrHLf3pB.js +1222 -0
  24. package/dist/client/assets/index-Df1bHDY4.css +1 -0
  25. package/dist/client/assets/star-DxVRh9VT.js +6 -0
  26. package/dist/client/assets/{users-Cp5TSxVm.js → users-3SD3oNMQ.js} +1 -1
  27. package/dist/client/index.html +2 -2
  28. package/dist/client/version.json +1 -1
  29. package/dist/droid-cli/index.ts +3 -5
  30. package/dist/droid-cli/package.json +1 -1
  31. package/dist/droid-cli/src/__tests__/event-bridge.test.ts +6 -1315
  32. package/dist/droid-cli/src/__tests__/provider.test.ts +6 -1927
  33. package/dist/droid-cli/src/control-handler.ts +1 -82
  34. package/dist/droid-cli/src/event-bridge.ts +1 -397
  35. package/dist/droid-cli/src/mcp-config.ts +1 -144
  36. package/dist/droid-cli/src/process-manager.ts +1 -358
  37. package/dist/droid-cli/src/prompt-builder.ts +1 -629
  38. package/dist/droid-cli/src/provider.ts +1 -447
  39. package/dist/droid-cli/src/stream-parser.ts +1 -37
  40. package/dist/droid-cli/src/thinking-config.ts +1 -83
  41. package/dist/droid-cli/src/tool-mapping.ts +1 -147
  42. package/dist/droid-cli/src/types.ts +1 -87
  43. package/dist/extension.js +4674 -1748
  44. package/dist/pi-claude-cli/package.json +1 -1
  45. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  46. package/package.json +5 -4
  47. package/skill/fusion/references/engine-tools.md +5 -1
  48. package/skill/fusion/references/extension-tools.md +3 -1
  49. package/dist/client/assets/ChatView-3Sqm6teN.js +0 -1
  50. package/dist/client/assets/InsightsView-4KiUKzbz.css +0 -1
  51. package/dist/client/assets/InsightsView-F5PZsX5u.js +0 -11
  52. package/dist/client/assets/NodesView-DddCS7zB.js +0 -14
  53. package/dist/client/assets/PiExtensionsManager-Ch7si-v8.js +0 -11
  54. package/dist/client/assets/PluginManager-LcTh_fHP.js +0 -1
  55. package/dist/client/assets/ResearchView-D0TY1VcX.js +0 -1
  56. package/dist/client/assets/SettingsModal-SOADcCNJ.js +0 -31
  57. package/dist/client/assets/SettingsModal-oOnIed5O.css +0 -1
  58. package/dist/client/assets/SetupWizardModal-EDYuf9Yc.js +0 -1
  59. package/dist/client/assets/index-4hC8zoTD.css +0 -1
  60. package/dist/client/assets/index-DNzA4aZ7.js +0 -1229
@@ -1,447 +1 @@
1
- /**
2
- * Provider orchestration for bridging pi requests to the Droid CLI subprocess.
3
- *
4
- * streamViaCli is the core function that:
5
- * 1. Builds the prompt from conversation context
6
- * 2. Spawns a Droid CLI subprocess with correct flags
7
- * 3. Writes the user message to stdin as NDJSON
8
- * 4. Reads stdout line-by-line, parsing NDJSON
9
- * 5. Routes stream events through the event bridge to pi's stream
10
- * 6. Handles result/error messages and cleans up the subprocess
11
- * 7. Implements break-early: kills subprocess at message_stop when
12
- * built-in or custom-tools MCP tool_use blocks are seen
13
- * 8. Hardened lifecycle: inactivity timeout, subprocess exit handler,
14
- * streamEnded guard, abort via SIGKILL, process registry
15
- */
16
-
17
- import { createInterface } from "node:readline";
18
- import {
19
- AssistantMessageEventStream,
20
- type Api,
21
- type Model,
22
- type SimpleStreamOptions,
23
- type TextContent,
24
- type ThinkingContent,
25
- type ToolCall,
26
- } from "@mariozechner/pi-ai";
27
- import {
28
- buildPrompt,
29
- buildSystemPrompt,
30
- buildResumePrompt,
31
- type PiContext,
32
- } from "./prompt-builder.js";
33
- import {
34
- spawnDroid,
35
- writeUserMessage,
36
- cleanupProcess,
37
- captureStderr,
38
- forceKillProcess,
39
- registerProcess,
40
- cleanupSystemPromptFile,
41
- buildDroidSpawnArgs,
42
- } from "./process-manager.js";
43
- import { parseLine } from "./stream-parser.js";
44
- import { createEventBridge } from "./event-bridge.js";
45
- import { mapThinkingEffort } from "./thinking-config.js";
46
- import { isPiKnownDroidTool } from "./tool-mapping.js";
47
- /**
48
- * Inactivity safety net for the Droid CLI subprocess.
49
- *
50
- * Set very high (30 minutes) because the caller is the authoritative source of
51
- * truth for "this session is stuck": Fusion's engine runs a `StuckTaskDetector`
52
- * with a configurable heartbeat (default 1 hour) and aborts the session via
53
- * `AbortSignal` when it decides the agent has gone quiet. droid-cli already
54
- * forwards that signal to the subprocess (`forceKillProcess` on `signal.abort`).
55
- *
56
- * A short timeout here was racing the engine: Sonnet 4.6 with extended thinking
57
- * on the triage prompt (~40k chars) routinely goes >3 minutes between thinking
58
- * deltas, and we were killing those subprocesses before they could write
59
- * PROMPT.md and call `fn_review_spec`. The half-hour ceiling is just a
60
- * last-resort guard for catastrophically hung processes when no abort signal
61
- * arrives (e.g. someone embeds droid-cli without a stuck detector).
62
- */
63
- const INACTIVITY_TIMEOUT_MS = 30 * 60_000;
64
-
65
- /**
66
- * Cold-start ceiling: kill the subprocess if it hasn't produced a single line
67
- * of stdout within this window. Distinct from INACTIVITY_TIMEOUT_MS so a hung
68
- * binary (no output ever) is reported with a clear cause instead of being
69
- * indistinguishable from a slow-thinking turn. Observed cold-start on a healthy
70
- * droid is ~20s; 60s gives 3x headroom for slow machines / cold caches.
71
- */
72
- const FIRST_LINE_TIMEOUT_MS = 60_000;
73
- function isDebugStreamEnabled(): boolean {
74
- return process.env.PI_DROID_CLI_DEBUG === "1";
75
- }
76
-
77
- function debugLog(message: string): void {
78
- if (!isDebugStreamEnabled()) return;
79
- console.error(`[droid-cli] ${message}`);
80
- }
81
-
82
- /** Extended stream options: pi's SimpleStreamOptions plus optional cwd and mcpConfigPath */
83
- type StreamViaCLiOptions = SimpleStreamOptions & {
84
- cwd?: string;
85
- mcpConfigPath?: string;
86
- };
87
-
88
- /**
89
- * Stream a response from Droid CLI as an AssistantMessageEventStream.
90
- *
91
- * Orchestrates the full subprocess lifecycle: spawn, write prompt, parse NDJSON,
92
- * bridge events, handle result, and clean up. Implements break-early pattern:
93
- * at message_stop, if any built-in or custom-tools MCP tool was seen, kills
94
- * the subprocess before Droid CLI can auto-execute the tools.
95
- *
96
- * Hardened with: inactivity timeout (180s), subprocess exit handler with stderr
97
- * surfacing, streamEnded guard against double errors, abort via SIGKILL, and
98
- * process registry integration for teardown cleanup.
99
- *
100
- * @param model - The model to use (from pi's model catalog)
101
- * @param context - The conversation context with messages and system prompt
102
- * @param options - Optional cwd, abort signal, reasoning level, thinking budgets, and mcpConfigPath
103
- * @returns An AssistantMessageEventStream that receives bridged events
104
- */
105
- export function streamViaCli(
106
- model: Model<Api>,
107
- context: PiContext,
108
- options?: StreamViaCLiOptions,
109
- ): AssistantMessageEventStream {
110
- // @ts-expect-error — tsc can't verify AssistantMessageEventStream is a value
111
- // through pi-ai's `export *` re-export chain. The class constructor exists at runtime.
112
- const stream = new AssistantMessageEventStream();
113
-
114
- (async () => {
115
- let proc: ReturnType<typeof spawnDroid> | undefined;
116
- let abortHandler: (() => void) | undefined;
117
-
118
- try {
119
- const cwd = options?.cwd ?? process.cwd();
120
-
121
- // Resume if pi provides a session ID AND this isn't the first turn.
122
- // Pi passes sessionId on every call (including first), but we can only
123
- // --resume a CLI session that already exists on disk from a prior turn.
124
- const resumeSessionId =
125
- options?.sessionId && context.messages.length > 1
126
- ? options.sessionId
127
- : undefined;
128
-
129
- // Build prompt: if resuming, only send the latest user turn;
130
- // otherwise build the full flattened conversation history
131
- const prompt = resumeSessionId
132
- ? buildResumePrompt(context)
133
- : buildPrompt(context);
134
- const systemPrompt = resumeSessionId
135
- ? undefined
136
- : buildSystemPrompt(context, cwd);
137
-
138
- // Compute effort level from reasoning options
139
- const effort = mapThinkingEffort(
140
- options?.reasoning,
141
- model.id,
142
- options?.thinkingBudgets,
143
- );
144
-
145
- const spawnOptions = {
146
- cwd,
147
- signal: options?.signal,
148
- effort,
149
- mcpConfigPath: options?.mcpConfigPath,
150
- resumeSessionId,
151
- newSessionId: !resumeSessionId ? options?.sessionId : undefined,
152
- };
153
-
154
- // Spawn subprocess
155
- proc = spawnDroid(model.id, systemPrompt || undefined, spawnOptions);
156
- const getStderr = captureStderr(proc);
157
-
158
- // Register in global process registry for teardown cleanup
159
- registerProcess(proc);
160
- const spawnArgs = buildDroidSpawnArgs(model.id, undefined, {
161
- effort,
162
- mcpConfigPath: options?.mcpConfigPath,
163
- resumeSessionId,
164
- newSessionId: !resumeSessionId ? options?.sessionId : undefined,
165
- });
166
- debugLog(
167
- `spawned droid subprocess pid=${proc.pid ?? "unknown"} args=${JSON.stringify(spawnArgs)}`,
168
- );
169
-
170
- // Write user message to subprocess stdin
171
- writeUserMessage(proc, prompt);
172
- debugLog("user message written to stdin, stdin.end() called");
173
-
174
- // Create event bridge (before endStreamWithError so bridge is in scope)
175
- const bridge = createEventBridge(stream, model);
176
-
177
- // Guard against double stream.end() and double error events.
178
- // First error path wins; subsequent ones are no-ops.
179
- let streamEnded = false;
180
-
181
- /**
182
- * End the stream with an error, using a "done" event instead of "error".
183
- *
184
- * Why "done" not "error": AssistantMessageEventStream.extractResult()
185
- * returns event.error (a string) for error events, but agent-loop.js
186
- * then calls message.content.filter() on the result, crashing because
187
- * a string has no .content property. By pushing "done" with a valid
188
- * AssistantMessage (content:[]), pi gets a well-formed object.
189
- */
190
- function endStreamWithError(errMsg: string) {
191
- if (streamEnded || broken) return;
192
- streamEnded = true;
193
- const output = bridge.getOutput();
194
- const errorMessage = {
195
- ...output,
196
- content: output.content?.length
197
- ? output.content
198
- : [{ type: "text" as const, text: `Error: ${errMsg}` }],
199
- stopReason: "stop" as const,
200
- };
201
- stream.push({
202
- type: "done",
203
- reason: "stop",
204
- message: errorMessage,
205
- });
206
- stream.end();
207
- }
208
-
209
- // Inactivity timeout: kill subprocess if no stdout for INACTIVITY_TIMEOUT_MS
210
- let inactivityTimer: ReturnType<typeof setTimeout> | undefined;
211
-
212
- function resetInactivityTimer() {
213
- if (inactivityTimer !== undefined) clearTimeout(inactivityTimer);
214
- inactivityTimer = setTimeout(() => {
215
- forceKillProcess(proc!);
216
- endStreamWithError(
217
- `Droid CLI subprocess timed out: no output for ${INACTIVITY_TIMEOUT_MS / 1000} seconds`,
218
- );
219
- }, INACTIVITY_TIMEOUT_MS);
220
- }
221
-
222
- // Set up abort signal handler -- uses SIGKILL for immediate force-kill
223
- if (options?.signal) {
224
- abortHandler = () => {
225
- if (proc) {
226
- forceKillProcess(proc);
227
- }
228
- };
229
-
230
- if (options.signal.aborted) {
231
- abortHandler();
232
- return;
233
- }
234
- options.signal.addEventListener("abort", abortHandler, { once: true });
235
- }
236
-
237
- // Track tool_use blocks for break-early decision at message_stop
238
- let sawBuiltInOrCustomTool = false;
239
- let firstLineReceived = false;
240
- // Guard against buffered readline lines firing after rl.close()
241
- let broken = false;
242
-
243
- // Set up readline for line-by-line NDJSON parsing
244
- const rl = createInterface({
245
- input: proc.stdout!,
246
- crlfDelay: Infinity,
247
- terminal: false,
248
- });
249
-
250
- // Handle process error -- use endStreamWithError for guard
251
- proc.on("error", (err: Error) => {
252
- if (broken) return; // Break-early killed the process intentionally
253
- const stderr = getStderr();
254
- endStreamWithError(stderr || err.message);
255
- });
256
-
257
- // Handle subprocess close -- surface crashes with stderr and exit code
258
- proc.on("close", (code: number | null, _signal: string | null) => {
259
- clearTimeout(inactivityTimer);
260
- debugLog(`subprocess closed: code=${code} signal=${_signal}`);
261
- if (broken) return; // Break-early kill, expected
262
- const stderr = getStderr().trim();
263
- if (stderr) {
264
- console.warn(`[droid-cli] Droid CLI stderr on close: ${stderr}`);
265
- }
266
- if (code !== 0 && code !== null) {
267
- const message = stderr
268
- ? `Droid CLI exited with code ${code}: ${stderr}`
269
- : `Droid CLI exited unexpectedly with code ${code}`;
270
- endStreamWithError(message);
271
- }
272
- });
273
-
274
- // Start inactivity timer after writing user message
275
- resetInactivityTimer();
276
-
277
- // Cold-start ceiling: only fires if firstLineReceived stays false. Cleared
278
- // when the first line arrives, when proc closes, or on break-early. This
279
- // distinguishes "droid never started" from "droid is taking a long time
280
- // between thinking deltas" so the inactivity kill carries actionable info.
281
- const firstLineTimer: ReturnType<typeof setTimeout> = setTimeout(() => {
282
- if (firstLineReceived) return;
283
- forceKillProcess(proc!);
284
- endStreamWithError(
285
- `Droid CLI produced no output within ${FIRST_LINE_TIMEOUT_MS / 1000}s — likely binary hang or auth failure (try \`droid --version\` and \`droid auth status\`)`,
286
- );
287
- }, FIRST_LINE_TIMEOUT_MS);
288
- proc.on("close", () => clearTimeout(firstLineTimer));
289
-
290
- // Process NDJSON lines from stdout using event-based callback
291
- // NOTE: Using 'line' event instead of `for await` because the async
292
- // iterator batches lines, breaking real-time streaming to pi.
293
- rl.on("line", (line: string) => {
294
- if (!firstLineReceived) {
295
- firstLineReceived = true;
296
- debugLog("first stdout line received from Droid CLI");
297
- }
298
- if (broken) return; // Guard: ignore buffered lines after break-early
299
-
300
- // Reset inactivity timer on each line of output
301
- resetInactivityTimer();
302
-
303
- const msg = parseLine(line);
304
- if (!msg) return;
305
-
306
- if (msg.type === "stream_event") {
307
- // Only forward top-level events to pi's event bridge.
308
- // Sub-agent events (parent_tool_use_id !== null) are internal to the CLI.
309
- const isTopLevel = !msg.parent_tool_use_id;
310
- if (isTopLevel) {
311
- bridge.handleEvent(msg.event);
312
- }
313
-
314
- // Track tool_use blocks for break-early decision (top-level only)
315
- if (
316
- isTopLevel &&
317
- msg.event.type === "content_block_start" &&
318
- msg.event.content_block?.type === "tool_use"
319
- ) {
320
- const toolName = msg.event.content_block.name;
321
- if (toolName) {
322
- const piKnownTool = isPiKnownDroidTool(toolName);
323
- debugLog(
324
- `top-level tool_use seen: ${toolName} (piKnown=${piKnownTool ? "yes" : "no"})`,
325
- );
326
- if (piKnownTool) {
327
- // Built-in tool (Read/Write/etc.) OR custom MCP tool (mcp__custom-tools__*)
328
- // Internal Claude Code tools (ToolSearch, Task, etc.) are excluded
329
- sawBuiltInOrCustomTool = true;
330
- }
331
- }
332
- }
333
-
334
- // Break-early at message_stop: kill subprocess before CLI auto-executes tools
335
- // Only on top-level message_stop — sub-agent message_stop is internal
336
- if (
337
- isTopLevel &&
338
- msg.event.type === "message_stop" &&
339
- sawBuiltInOrCustomTool
340
- ) {
341
- debugLog("break-early triggered at message_stop after pi-known tool_use");
342
- broken = true; // Set guard BEFORE rl.close() to prevent buffered lines
343
- clearTimeout(inactivityTimer);
344
- clearTimeout(firstLineTimer);
345
- // Pi will execute these tools. Kill subprocess to prevent CLI from executing them.
346
- forceKillProcess(proc!);
347
- rl.close();
348
- return; // Don't process further -- done event already pushed by event bridge
349
- }
350
- } else if (msg.type === "control_request") {
351
- debugLog(
352
- `unexpected control_request received (stdin already closed): ${msg.request_id}`,
353
- );
354
- } else if (msg.type === "result") {
355
- if (msg.subtype === "error") {
356
- endStreamWithError(msg.error ?? "Unknown error from Droid CLI");
357
- }
358
- // For both success and error: clean up the subprocess
359
- clearTimeout(inactivityTimer);
360
- clearTimeout(firstLineTimer);
361
- cleanupProcess(proc!);
362
- rl.close();
363
- }
364
- });
365
-
366
- // Wait for readline to close (result received or process ended).
367
- // Also resolve on subprocess close: if SIGKILL races readline (e.g. after
368
- // an external abort or watchdog kill), `rl` may never emit "close" because
369
- // its input stream was destroyed mid-buffer. Forcing rl.close() from the
370
- // proc close handler guarantees this await unblocks instead of hanging
371
- // and triggering the engine's "executor did not unwind within 60s" path.
372
- await new Promise<void>((resolve) => {
373
- rl.on("close", resolve);
374
- proc!.on("close", () => {
375
- try { rl.close(); } catch { /* already closed */ }
376
- resolve();
377
- });
378
- });
379
-
380
- // Push done event after readline closes (async). Pushing synchronously
381
- // inside handleMessageStop prevents pi from executing tools.
382
- // Guard with streamEnded to avoid pushing done after an error was already pushed.
383
- if (!streamEnded) {
384
- const output = bridge.getOutput();
385
- const contentEvents = output.content || [];
386
-
387
- if (contentEvents.length === 0) {
388
- console.warn(
389
- `[droid-cli] Droid CLI closed without content events (model=${model.id}, sessionId=${options?.sessionId ?? "none"})`,
390
- );
391
- }
392
-
393
- // If stopReason is toolUse but there are no pi-known tool calls in content,
394
- // it means only user MCP tools were called (filtered by event bridge).
395
- // Override to "stop" so pi doesn't try to execute non-existent tools.
396
- const piToolCalls = (output.content || []).filter(
397
- (c: TextContent | ThinkingContent | ToolCall) => c.type === "toolCall",
398
- );
399
- const effectiveReason =
400
- output.stopReason === "toolUse" && piToolCalls.length === 0
401
- ? "stop"
402
- : output.stopReason;
403
-
404
- streamEnded = true;
405
- stream.push({
406
- type: "done",
407
- reason:
408
- effectiveReason === "toolUse"
409
- ? "toolUse"
410
- : effectiveReason === "length"
411
- ? "length"
412
- : "stop",
413
- message: { ...output, stopReason: effectiveReason },
414
- });
415
- stream.end();
416
- }
417
- } catch (err) {
418
- const errMsg = err instanceof Error ? err.message : String(err);
419
- // Push a "done" event with a text error so pi gets a valid AssistantMessage.
420
- // Pushing type:"error" would require an AssistantMessage in the error field,
421
- // but we don't have a full AssistantMessage here.
422
- stream.push({
423
- type: "done",
424
- reason: "stop",
425
- message: {
426
- role: "assistant" as const,
427
- content: [{ type: "text" as const, text: `Error: ${errMsg}` }],
428
- api: "droid-cli",
429
- provider: model.provider,
430
- model: model.id,
431
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
432
- stopReason: "stop" as const,
433
- timestamp: Date.now(),
434
- },
435
- });
436
- stream.end();
437
- } finally {
438
- // Clean up abort listener
439
- if (options?.signal && abortHandler) {
440
- options.signal.removeEventListener("abort", abortHandler);
441
- }
442
- cleanupSystemPromptFile();
443
- }
444
- })();
445
-
446
- return stream;
447
- }
1
+ export { streamViaCli } from "../../../plugins/fusion-plugin-droid-runtime/src/provider.js";
@@ -1,37 +1 @@
1
- import type { NdjsonMessage } from "./types";
2
-
3
- /**
4
- * Parse a single NDJSON line from Droid CLI stdout into a typed message.
5
- *
6
- * This function is deliberately resilient -- it never throws. Debug noise,
7
- * empty lines, and malformed JSON all return null so the streaming pipeline
8
- * can safely skip them and continue processing.
9
- */
10
- export function parseLine(line: string): NdjsonMessage | null {
11
- const trimmed = line.trim();
12
-
13
- // Skip empty lines
14
- if (!trimmed) {
15
- return null;
16
- }
17
-
18
- // Skip non-JSON lines (debug output like "[SandboxDebug] ...")
19
- if (!trimmed.startsWith("{")) {
20
- return null;
21
- }
22
-
23
- let parsed: unknown;
24
- try {
25
- parsed = JSON.parse(trimmed);
26
- } catch {
27
- console.error("Failed to parse NDJSON line:", trimmed);
28
- return null;
29
- }
30
-
31
- // Validate that the parsed result is a non-null object (not array, not primitive)
32
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
33
- return null;
34
- }
35
-
36
- return parsed as NdjsonMessage;
37
- }
1
+ export * from "../../../plugins/fusion-plugin-droid-runtime/src/stream-parser.js";
@@ -1,83 +1 @@
1
- /**
2
- * Thinking effort configuration for mapping pi's ThinkingLevel to Droid CLI --effort flags.
3
- *
4
- * Maps pi's reasoning levels (minimal/low/medium/high/xhigh) to the CLI's effort
5
- * levels (low/medium/high/max). Opus models get an elevated mapping where medium
6
- * becomes high and high becomes max, leveraging their superior reasoning capability.
7
- *
8
- * IMPORTANT: The CLI does NOT support --thinking-budget. Only --effort is supported.
9
- */
10
-
11
- import type { ThinkingLevel, ThinkingBudgets } from "@mariozechner/pi-ai";
12
-
13
- /** CLI effort levels accepted by the --effort flag */
14
- export type CliEffortLevel = "low" | "medium" | "high" | "max";
15
-
16
- /**
17
- * Standard model mapping: pi ThinkingLevel -> CLI effort.
18
- * Non-Opus models never receive "max" (would cause CLI error).
19
- */
20
- const STANDARD_EFFORT_MAP: Record<ThinkingLevel, CliEffortLevel> = {
21
- minimal: "low",
22
- low: "low",
23
- medium: "medium",
24
- high: "high",
25
- xhigh: "high", // non-Opus: silently downgrade (max not supported)
26
- };
27
-
28
- /**
29
- * Opus model mapping: shifted up for elevated reasoning.
30
- * Opus models get max capability at high/xhigh levels.
31
- */
32
- const OPUS_EFFORT_MAP: Record<ThinkingLevel, CliEffortLevel> = {
33
- minimal: "low",
34
- low: "low",
35
- medium: "high", // shifted: standard high
36
- high: "max", // shifted: maximum capability
37
- xhigh: "max", // Opus gets max
38
- };
39
-
40
- /**
41
- * Detect whether a model ID refers to an Opus model.
42
- * Uses includes('opus') for forward-compatibility with future Opus versions.
43
- *
44
- * @param modelId - The model identifier string
45
- * @returns true if the model is an Opus variant
46
- */
47
- export function isOpusModel(modelId: string): boolean {
48
- return modelId.includes("opus");
49
- }
50
-
51
- /**
52
- * Map pi's ThinkingLevel to a CLI effort string.
53
- *
54
- * When reasoning is undefined, returns undefined so the --effort flag is omitted
55
- * entirely, letting the CLI use its default behavior. When thinkingBudgets are
56
- * provided, a console.warn is logged because the CLI only supports effort levels,
57
- * not token budgets.
58
- *
59
- * @param reasoning - Pi's thinking level (undefined = omit flag)
60
- * @param modelId - Model ID for Opus detection
61
- * @param thinkingBudgets - Custom budgets (logged as unsupported, not applied)
62
- * @returns CLI effort level string, or undefined if flag should be omitted
63
- */
64
- export function mapThinkingEffort(
65
- reasoning?: ThinkingLevel,
66
- modelId?: string,
67
- thinkingBudgets?: ThinkingBudgets,
68
- ): CliEffortLevel | undefined {
69
- if (reasoning === undefined) {
70
- return undefined; // omit --effort flag entirely
71
- }
72
-
73
- if (thinkingBudgets && Object.keys(thinkingBudgets).length > 0) {
74
- console.warn(
75
- "[droid-cli] Custom thinkingBudgets are not supported with CLI subprocess. " +
76
- "The CLI uses --effort levels instead of token budgets. Budgets will be ignored.",
77
- );
78
- }
79
-
80
- const isOpus = modelId ? isOpusModel(modelId) : false;
81
- const map = isOpus ? OPUS_EFFORT_MAP : STANDARD_EFFORT_MAP;
82
- return map[reasoning];
83
- }
1
+ export * from "../../../plugins/fusion-plugin-droid-runtime/src/thinking-config.js";