fluxflow-cli 1.0.7 → 1.0.8
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/ARCHITECTURE.md +3 -1
- package/README.md +2 -0
- package/dist/fluxflow.js +37 -18
- package/package.json +1 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -25,7 +25,9 @@ The execution flow of a single user prompt follows this loop:
|
|
|
25
25
|
5. **Turn Management & Continuation**: The model is instructed to append `[turn: finish]` if its goal is complete, or `[turn: continue]` if it expects tool results.
|
|
26
26
|
- If tools were called or `[turn: continue]` is present, the loop increments and re-prompts the model with the newly gathered `[TOOL_RESULT]` data.
|
|
27
27
|
- If `[turn: finish]` is detected and no further tools were called, the main loop terminates, passing the final synthesized context to the background Janitor process.
|
|
28
|
-
6. **Loop Limits & Resilience**: To prevent infinite loops or excessive API usage, **Flux mode** is capped at 50 iterations per user prompt, while **Flow mode** is capped at 5.
|
|
28
|
+
6. **Loop Limits & Resilience**: To prevent infinite loops or excessive API usage, **Flux mode** is capped at 50 iterations per user prompt, while **Flow mode** is capped at 5.
|
|
29
|
+
- **Multi-Stage Failover**: The loop features a sophisticated 8-attempt retry engine with random backoff (800ms - 2s).
|
|
30
|
+
- **Critical Fallback Pivot**: If the primary model fails 5 consecutive times, the agent surgically pivots to a lighter, high-concurrency fallback model (`gemini-3.1-flash-lite-preview`) for the final 3 attempts to ensure session navigation through API congestion.
|
|
29
31
|
|
|
30
32
|
## The Dual-Model System
|
|
31
33
|
|
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ Security isn't an afterthought; it's a boundary.
|
|
|
40
40
|
- **External Path Hardlock**: Restricts the agent to your Current Working Directory (CWD) unless you explicitly unlock it.
|
|
41
41
|
- **Human-in-the-Loop (HITL)**: Every file write and terminal command requires your high-fidelity approval.
|
|
42
42
|
- **XOR Vaulting**: All local session histories, memories, and API keys are obfuscated and encrypted at rest.
|
|
43
|
+
- **Adaptive Failover**: Automatic multi-stage retry logic with high-concurrency fallback model switching (Gemini 3.1 Flash Lite) during peak API congestion.
|
|
43
44
|
|
|
44
45
|
### 🧹 **The Background Janitor**
|
|
45
46
|
While you move at high speed, the Janitor follows behind—refining session titles, compressing data, and ensuring your context window remains at absolute peak performance.
|
|
@@ -50,6 +51,7 @@ While you move at high speed, the Janitor follows behind—refining session titl
|
|
|
50
51
|
- **Deep File-System Interaction**: Edit, move, and refactor code across multiple files with atomic precision.
|
|
51
52
|
- **Real-Time Web Intelligence**: Autonomous web-searching via DuckDuckGo for live news and technical research.
|
|
52
53
|
- **Autonomous Project Alignment**: Automatically detects and adheres to project-specific instructions in `Agent.md`, `Skills.md`, and `Fluxflow.md` for high-fidelity alignment with your coding standards and custom workflows.
|
|
54
|
+
- **High-Reliability Fallback**: Automatic failover to a lighter, high-concurrency model during peak traffic to ensure zero session loss.
|
|
53
55
|
- **Persistent Memory**: The agent learns from your preferences and project requirements across sessions.
|
|
54
56
|
|
|
55
57
|
---
|
package/dist/fluxflow.js
CHANGED
|
@@ -1284,8 +1284,8 @@ var getAIStream = async function* (modelName, history, settings, steeringCallbac
|
|
|
1284
1284
|
USER_PROMPT: ${agentText}`.trim();
|
|
1285
1285
|
modifiedHistory.push({ role: "user", text: firstUserMsg });
|
|
1286
1286
|
let lastUsage = null;
|
|
1287
|
-
const MAX_LOOPS = mode === "Flux" ? 50 :
|
|
1288
|
-
const MAX_RETRIES =
|
|
1287
|
+
const MAX_LOOPS = mode === "Flux" ? 50 : 7;
|
|
1288
|
+
const MAX_RETRIES = 7;
|
|
1289
1289
|
yield { type: "status", content: "Working..." };
|
|
1290
1290
|
TERMINATION_SIGNAL = false;
|
|
1291
1291
|
let fullAgentResponse = "";
|
|
@@ -1314,8 +1314,15 @@ USER_PROMPT: ${agentText}`.trim();
|
|
|
1314
1314
|
if (!await checkQuota("agent", settings)) {
|
|
1315
1315
|
throw new Error("Error: Daily Quota Exausted for Agent");
|
|
1316
1316
|
}
|
|
1317
|
+
let targetModel = modelName;
|
|
1318
|
+
if (retryCount >= 5) {
|
|
1319
|
+
targetModel = "gemini-3.1-flash-lite-preview";
|
|
1320
|
+
yield { type: "model_update", content: "Trying with fallback model" };
|
|
1321
|
+
} else if (retryCount > 0) {
|
|
1322
|
+
yield { type: "model_update", content: null };
|
|
1323
|
+
}
|
|
1317
1324
|
stream = await client.models.generateContentStream({
|
|
1318
|
-
model:
|
|
1325
|
+
model: targetModel,
|
|
1319
1326
|
contents,
|
|
1320
1327
|
config: {
|
|
1321
1328
|
temperature: mode === "Flux" ? 0.9 : 1.3,
|
|
@@ -1326,31 +1333,39 @@ USER_PROMPT: ${agentText}`.trim();
|
|
|
1326
1333
|
}
|
|
1327
1334
|
});
|
|
1328
1335
|
success = true;
|
|
1336
|
+
yield { type: "model_update", content: null };
|
|
1329
1337
|
} catch (err) {
|
|
1330
|
-
const errMsg = err.message || String(err);
|
|
1338
|
+
const errMsg = err.status || err.error && err.error.message || String(err);
|
|
1331
1339
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
|
|
1332
1340
|
const agentErrDir = path13.join(LOGS_DIR, "agent");
|
|
1333
|
-
if (!fs12.existsSync(agentErrDir)) {
|
|
1334
|
-
fs12.mkdirSync(agentErrDir, { recursive: true });
|
|
1335
|
-
}
|
|
1341
|
+
if (!fs12.existsSync(agentErrDir)) fs12.mkdirSync(agentErrDir, { recursive: true });
|
|
1336
1342
|
fs12.appendFileSync(path13.join(agentErrDir, "error.log"), `ERROR [${date}]: ${errMsg}
|
|
1337
1343
|
`);
|
|
1338
|
-
|
|
1339
|
-
if (isRetryable && retryCount < MAX_RETRIES) {
|
|
1344
|
+
if (retryCount < MAX_RETRIES) {
|
|
1340
1345
|
retryCount++;
|
|
1346
|
+
const waitTime = Math.floor(Math.random() * (2e3 - 800 + 1)) + 800;
|
|
1341
1347
|
yield { type: "status", content: `Retrying (${retryCount}/${MAX_RETRIES})...` };
|
|
1342
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1348
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
1343
1349
|
} else {
|
|
1344
1350
|
throw new Error(`Model cannot be reached: ${errMsg}`);
|
|
1345
1351
|
}
|
|
1346
1352
|
}
|
|
1347
1353
|
}
|
|
1348
1354
|
let turnText = "";
|
|
1355
|
+
let lastToolSniffed = null;
|
|
1349
1356
|
for await (const chunk of stream) {
|
|
1350
1357
|
if (TERMINATION_SIGNAL) break;
|
|
1351
1358
|
if (chunk.text) {
|
|
1352
1359
|
turnText += chunk.text;
|
|
1353
1360
|
yield { type: "text", content: chunk.text };
|
|
1361
|
+
if (turnText.includes("tool:functions.")) {
|
|
1362
|
+
const parts = turnText.split("tool:functions.");
|
|
1363
|
+
const potentialTool = parts[parts.length - 1].split("(")[0].trim();
|
|
1364
|
+
if (potentialTool && /^[a-z_]+$/.test(potentialTool) && potentialTool !== lastToolSniffed) {
|
|
1365
|
+
lastToolSniffed = potentialTool;
|
|
1366
|
+
yield { type: "status", content: `Working (${potentialTool})...` };
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1354
1369
|
}
|
|
1355
1370
|
if (chunk.usageMetadata) {
|
|
1356
1371
|
lastUsage = chunk.usageMetadata;
|
|
@@ -1510,6 +1525,7 @@ ${boxBottom}
|
|
|
1510
1525
|
yield { type: "memory_updated" };
|
|
1511
1526
|
}
|
|
1512
1527
|
}
|
|
1528
|
+
yield { type: "status", content: "Working..." };
|
|
1513
1529
|
}
|
|
1514
1530
|
const cleanedTurnText = turnText.replace(/<think>[\s\S]*?<\/think>/g, "").replace(/\[?\s*(turn\s*:)?\s*(continue|finish)\s*\]?/gi, "").trim();
|
|
1515
1531
|
if (hasFinish || !shouldContinue && toolResults.length === 0) {
|
|
@@ -1831,6 +1847,7 @@ function App() {
|
|
|
1831
1847
|
const [escTimer, setEscTimer] = useState4(null);
|
|
1832
1848
|
const [queuedPrompt, setQueuedPrompt] = useState4(null);
|
|
1833
1849
|
const [resolutionData, setResolutionData] = useState4(null);
|
|
1850
|
+
const [tempModelOverride, setTempModelOverride] = useState4(null);
|
|
1834
1851
|
const [messages, setMessages] = useState4([
|
|
1835
1852
|
{ id: "welcome", role: "system", text: FLUX_LOGO + "\n\n\u{1F30A}\u26A1 Welcome to Flux Flow! Type /help for commands.\n" }
|
|
1836
1853
|
]);
|
|
@@ -2201,6 +2218,10 @@ OUTPUT: ${execOutputRef.current}`;
|
|
|
2201
2218
|
setStatusText(packet.content);
|
|
2202
2219
|
continue;
|
|
2203
2220
|
}
|
|
2221
|
+
if (packet.type === "model_update") {
|
|
2222
|
+
setTempModelOverride(packet.content);
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2204
2225
|
if (packet.type === "turn_reset") {
|
|
2205
2226
|
currentThinkId = null;
|
|
2206
2227
|
currentAgentId = null;
|
|
@@ -2371,7 +2392,7 @@ OUTPUT: ${execOutputRef.current}`;
|
|
|
2371
2392
|
CommandMenu,
|
|
2372
2393
|
{
|
|
2373
2394
|
title: "\u{1F916} Select AI Model",
|
|
2374
|
-
items: [{ label: "Gemma 4 31B (Default)", value: "gemma-4-31b-it" }, { label: "Gemini 3.1 Pro (Req. paid API Key)", value: "gemini-3.1-pro-preview" }, { label: "Gemini 3 Flash", value: "gemini-3-flash-preview" }, { label: "Gemini 3.1 Flash Lite", value: "gemini-3.1-flash-lite" }, { label: "Cancel", value: "Cancel" }],
|
|
2395
|
+
items: [{ label: "Gemma 4 31B (Default)", value: "gemma-4-31b-it" }, { label: "Gemini 3.1 Pro (Req. paid API Key)", value: "gemini-3.1-pro-preview" }, { label: "Gemini 3 Flash", value: "gemini-3-flash-preview" }, { label: "Gemini 3.1 Flash Lite", value: "gemini-3.1-flash-lite-preview" }, { label: "Cancel", value: "Cancel" }],
|
|
2375
2396
|
onSelect: (item) => {
|
|
2376
2397
|
if (item.value !== "Cancel") setActiveModel(item.value);
|
|
2377
2398
|
setActiveView("chat");
|
|
@@ -2737,7 +2758,7 @@ OUTPUT: ${execOutputRef.current}`;
|
|
|
2737
2758
|
return acc + Math.max(1, Math.ceil(line.length / wrapWidth));
|
|
2738
2759
|
}, 0);
|
|
2739
2760
|
const maxLines = Math.max(1, wrappedLines);
|
|
2740
|
-
return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", marginTop: 1, flexShrink: 0, width: "100%" },
|
|
2761
|
+
return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", marginTop: 1, flexShrink: 0, width: "100%" }, /* @__PURE__ */ React8.createElement(Box8, { paddingX: 1, marginBottom: 0, justifyContent: "space-between", width: "100%" }, /* @__PURE__ */ React8.createElement(Box8, null, statusText && /* @__PURE__ */ React8.createElement(Text8, { color: "magenta", italic: true }, "\u23F3 ", statusText)), /* @__PURE__ */ React8.createElement(Text8, { color: "gray", dimColor: true }, "(", tempModelOverride || activeModel, ")")), suggestions.length > 0 && /* @__PURE__ */ React8.createElement(Box8, { paddingX: 1, marginBottom: 0 }, /* @__PURE__ */ React8.createElement(Text8, { color: "gray" }, "\u{1F4A1} Suggestions: "), suggestions.map((s, i) => /* @__PURE__ */ React8.createElement(Text8, { key: s, color: "yellow", bold: i === 0 }, " ", s, " "))), /* @__PURE__ */ React8.createElement(Box8, { backgroundColor: "#333333", paddingX: 1, paddingY: 1, width: "100%" }, /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", width: "100%" }, maxLines > 3 ? /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", width: "100%", paddingY: 0 }, /* @__PURE__ */ React8.createElement(Text8, { color: "gray", dimColor: true }, "[\u{1F4E6} ", maxLines, " lines of text in buffer - Full content will be sent]"), /* @__PURE__ */ React8.createElement(
|
|
2741
2762
|
Box8,
|
|
2742
2763
|
{
|
|
2743
2764
|
flexDirection: "row",
|
|
@@ -2747,7 +2768,7 @@ OUTPUT: ${execOutputRef.current}`;
|
|
|
2747
2768
|
alignItems: "flex-end"
|
|
2748
2769
|
},
|
|
2749
2770
|
/* @__PURE__ */ React8.createElement(Box8, { flexShrink: 0, width: 3 }, /* @__PURE__ */ React8.createElement(Text8, { color: "yellow" }, "\u276F ")),
|
|
2750
|
-
/* @__PURE__ */ React8.createElement(Box8, { flexGrow: 1 }, /* @__PURE__ */ React8.createElement(
|
|
2771
|
+
/* @__PURE__ */ React8.createElement(Box8, { flexGrow: 1 }, /* @__PURE__ */ React8.createElement(Box8, { flexGrow: 1, position: "relative" }, input.split("\n").pop() === "" && !isProcessing && /* @__PURE__ */ React8.createElement(Box8, { position: "absolute", paddingLeft: 0 }, /* @__PURE__ */ React8.createElement(Text8, { color: "gray", dimColor: true }, "Type your message...")), /* @__PURE__ */ React8.createElement(
|
|
2751
2772
|
MultilineInput,
|
|
2752
2773
|
{
|
|
2753
2774
|
value: input.split("\n").pop() || "",
|
|
@@ -2758,14 +2779,13 @@ OUTPUT: ${execOutputRef.current}`;
|
|
|
2758
2779
|
setInput(lines.join("\n"));
|
|
2759
2780
|
},
|
|
2760
2781
|
onSubmit: handleSubmit,
|
|
2761
|
-
placeholder: escPressed ? "Press ESC again to cancel the request." : isProcessing ? "Flux Flow is thinking..." : "Type your message or /command...",
|
|
2762
2782
|
keyBindings: {
|
|
2763
2783
|
submit: (key) => key.return && !key.shift && !key.ctrl,
|
|
2764
2784
|
newline: (key) => key.return && key.shift || key.return && key.ctrl
|
|
2765
2785
|
}
|
|
2766
2786
|
}
|
|
2767
|
-
))
|
|
2768
|
-
)) : /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "row", width: "100%", paddingY: 0 }, /* @__PURE__ */ React8.createElement(Box8, { flexShrink: 0, width: 3 }, /* @__PURE__ */ React8.createElement(Text8, { color: "yellow" }, "\u276F ")), /* @__PURE__ */ React8.createElement(Box8, { flexGrow: 1 }, /* @__PURE__ */ React8.createElement(
|
|
2787
|
+
)))
|
|
2788
|
+
)) : /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "row", width: "100%", paddingY: 0 }, /* @__PURE__ */ React8.createElement(Box8, { flexShrink: 0, width: 3 }, /* @__PURE__ */ React8.createElement(Text8, { color: "yellow" }, "\u276F ")), /* @__PURE__ */ React8.createElement(Box8, { flexGrow: 1 }, /* @__PURE__ */ React8.createElement(Box8, { flexGrow: 1, position: "relative" }, input === "" && !isProcessing && /* @__PURE__ */ React8.createElement(Box8, { position: "absolute", paddingLeft: 0 }, /* @__PURE__ */ React8.createElement(Text8, { color: "gray", dimColor: true }, escPressed ? " Press ESC again to cancel the request." : " Type your message or /command...")), /* @__PURE__ */ React8.createElement(
|
|
2769
2789
|
MultilineInput,
|
|
2770
2790
|
{
|
|
2771
2791
|
value: input,
|
|
@@ -2774,14 +2794,13 @@ OUTPUT: ${execOutputRef.current}`;
|
|
|
2774
2794
|
setInput(cleanVal);
|
|
2775
2795
|
},
|
|
2776
2796
|
onSubmit: handleSubmit,
|
|
2777
|
-
placeholder: escPressed ? "Press ESC again to cancel the request." : isProcessing ? "Flux Flow is thinking..." : "Type your message or /command...",
|
|
2778
2797
|
maxRows: 3,
|
|
2779
2798
|
keyBindings: {
|
|
2780
2799
|
submit: (key) => key.return && !key.shift && !key.ctrl,
|
|
2781
2800
|
newline: (key) => key.return && key.shift || key.return && key.ctrl
|
|
2782
2801
|
}
|
|
2783
2802
|
}
|
|
2784
|
-
))))));
|
|
2803
|
+
)))))));
|
|
2785
2804
|
}
|
|
2786
2805
|
};
|
|
2787
2806
|
return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", width: "100%" }, /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column" }, messages.slice(0, completedIndex).map((msg, idx) => /* @__PURE__ */ React8.createElement(ChatLayout, { key: msg.id || idx, messages: [msg], showFullThinking }))), /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", padding: 1, width: "100%" }, activeView === "chat" && /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", width: "100%" }, /* @__PURE__ */ React8.createElement(ChatLayout, { messages: messages.slice(completedIndex), showFullThinking }), activeCommand && /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(TerminalBox, { command: activeCommand, output: execOutput }))), isInitializing ? /* @__PURE__ */ React8.createElement(Box8, { borderStyle: "double", borderColor: "magenta", padding: 1, flexShrink: 0 }, /* @__PURE__ */ React8.createElement(Text8, { color: "magenta" }, "\u{1F30A} Starting Flux Flow...")) : !apiKey ? /* @__PURE__ */ React8.createElement(Box8, { borderStyle: "bold", borderColor: "yellow", padding: 1, flexDirection: "column", flexShrink: 0 }, /* @__PURE__ */ React8.createElement(Text8, { color: "yellow", bold: true }, "\u{1F511} API KEY REQUIRED"), /* @__PURE__ */ React8.createElement(Text8, null, "Please enter your Gemini API Key to initialize the agent's brain."), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, { color: "cyan" }, "\u276F "), /* @__PURE__ */ React8.createElement(
|