Skip to content

Commit 134d320

Browse files
author
Splash Agent
committed
feat: expanded fast paths, planner retry + degraded fallback
1 parent 4088f33 commit 134d320

2 files changed

Lines changed: 203 additions & 26 deletions

File tree

examples/cli.ts

Lines changed: 148 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -734,37 +734,166 @@ async function launchTui(_focusId?: string): Promise<void> {
734734

735735
async function checkFastPath(prompt: string): Promise<boolean> {
736736
const lowerPrompt = prompt.toLowerCase().trim();
737-
738-
const createMatch = lowerPrompt.match(/^(?:(?:run\s+)?(?:create|make)\s+file)\s+(.+)$/);
739-
if (createMatch) {
737+
738+
// --- helpers ---
739+
const resolvePath = (raw: string): string => {
740+
const p = raw.trim().replace(/^["']|["']$/g, "");
741+
if (/^[a-z]:[/\\]/i.test(p)) return path.resolve(p);
742+
return path.resolve(process.cwd(), p);
743+
};
744+
745+
const isPathLike = (s: string) =>
746+
/^(?:[a-z]:)?[\\/]/.test(s) || s.includes("/") || s.includes("\\");
747+
748+
// 1) Create file — catch "create [a] [python] file [named|called] snake.py"
749+
// "make a python file snake.py"
750+
// "create there python code for neon snake"
751+
const createAlias = lowerPrompt.match(
752+
/^(?:(?:run\s+)?(?:create|make|new|write|generate)\s+(?:a\s+)?(?:python\s+)?(?:file|code|script)?\.?\s*(?:named|called)?\.?\s*)([\w\-./\\]+(?:\.[a-z0-9]+)?(?:\s+\S+)*)$/i
753+
);
754+
if (createAlias) {
755+
const tail = createAlias[1].trim();
756+
// If tail itself contains spaces, take the first non-space token as filename
757+
const candidate = tail.split(/\s+/).find((t) => /^\w|[\w\-./\\]/.test(t)) || tail;
758+
const fp = resolvePath(candidate);
740759
try {
741-
fs.writeFileSync(path.resolve(process.cwd(), createMatch[1]!), "");
742-
console.log(`[OK] file created: ${createMatch[1]}`);
760+
fs.mkdirSync(path.dirname(fp), { recursive: true });
761+
fs.writeFileSync(fp, "");
762+
console.log(`[OK] created ${fp}`);
763+
return true;
743764
} catch (e: any) {
744765
console.log(`[ERR] failed to create file: ${e.message}`);
766+
return true;
745767
}
746-
return true;
747768
}
748769

749-
const runMatch = lowerPrompt.match(/^(?:(?:run\s+)?command|execute)\s+(.+)$/);
770+
// 2) Create file with content (e.g. "create python code for neon snake")
771+
// → generate a minimal Python script for the topic
772+
if (/^(?:create|make|write|generate)\s+(?:a\s+)?(?:python|js|ts|html|css|json|md)\b/i.test(lowerPrompt)) {
773+
const langMatch = lowerPrompt.match(/^(?:create|make|write|generate)\s+(?:a\s+)?(python|js|ts|html|css|json|md)/i);
774+
const lang = (langMatch?.[1] || "py").trim();
775+
let ext = lang;
776+
if (lang === "js") ext = "js";
777+
if (lang === "ts") ext = "ts";
778+
const safeName = prompt
779+
.replace(/^(?:create|make|write|generate)\s+(?:a\s+)?(?:python|js|ts|html|css|json|md)\s*/i, "")
780+
.split(/\s+/)
781+
.slice(0, 3)
782+
.join("_")
783+
.replace(/[^a-z0-9_\-]/gi, "");
784+
const fileName = safeName || `script.${ext}`;
785+
const fp = resolvePath(fileName.endsWith(`.${ext}`) ? fileName : `${fileName}.${ext}`);
786+
let content = "";
787+
if (lang === "python") {
788+
content = `# ${path.basename(fp)}\nprint("Hello from Splash")\n`;
789+
} else if (lang === "html") {
790+
content = `<!DOCTYPE html>\n<html>\n<body>\n <h1>Hello from Splash</h1>\n</body>\n</html>\n`;
791+
} else {
792+
content = `// ${path.basename(fp)}\nconsole.log("Hello from Splash");\n`;
793+
}
794+
try {
795+
fs.mkdirSync(path.dirname(fp), { recursive: true });
796+
fs.writeFileSync(fp, content);
797+
console.log(`[OK] created ${fp}`);
798+
return true;
799+
} catch (e: any) {
800+
console.log(`[ERR] failed to create file: ${e.message}`);
801+
return true;
802+
}
803+
}
804+
805+
// 3) List folders/files in a directory
806+
const listAlias = lowerPrompt.match(
807+
/^(?:list|ls|dir|show)\s+(?:all\s+)?(?:folders|directories|files|subfolders)?\.?\s*(?:in|of|for)?\.?\s*([\w\-./\\]+)?$/
808+
);
809+
if (listAlias && /(?:folders|files|list|ls|dir|show)/.test(lowerPrompt)) {
810+
const target = listAlias[1]
811+
? resolvePath(listAlias[1].trim())
812+
: process.cwd();
813+
try {
814+
const entries = fs.readdirSync(target, { withFileTypes: true });
815+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
816+
const files = entries.filter((e) => e.isFile()).map((e) => e.name);
817+
console.log(`[OK] ${target}${dirs.length} folder(s), ${files.length} file(s)`);
818+
for (const d of dirs) console.log(` [DIR] ${d}`);
819+
for (const f of files) console.log(` [FILE] ${f}`);
820+
return true;
821+
} catch (e: any) {
822+
console.log(`[ERR] failed to list directory: ${e.message}`);
823+
return true;
824+
}
825+
}
826+
827+
// 4) "go to <path> and [do simple thing]"
828+
const goToMatch = lowerPrompt.match(/^(?:go\s+to|open|cd)\s+(.+?)(?:\s+and\s+(.+))?$/);
829+
if (goToMatch) {
830+
const dir = goToMatch[1].trim();
831+
const rest = (goToMatch[2] || "").trim();
832+
if (/^(list|ls|show)\b/i.test(rest) || rest === "") {
833+
const target = resolvePath(dir);
834+
try {
835+
const entries = fs.readdirSync(target, { withFileTypes: true });
836+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
837+
console.log(`[OK] ${target}${dirs.length} folder(s)`);
838+
for (const d of dirs) console.log(` ${d}`);
839+
return true;
840+
} catch (e: any) {
841+
console.log(`[ERR] failed to list directory: ${e.message}`);
842+
return true;
843+
}
844+
}
845+
// fall through for non-simple rest
846+
}
847+
848+
// 5) Write "write X to file Y"
849+
const writeToMatch = lowerPrompt.match(/^(?:write|put|save)\s+(.+?)\s+to\s+(?:file\s+)?(.+)$/);
850+
if (writeToMatch) {
851+
const content = writeToMatch[1];
852+
const filePath = writeToMatch[2];
853+
if (isPathLike(filePath) || /\.\w{1,4}$/.test(filePath.trim())) {
854+
try {
855+
const fp = resolvePath(filePath);
856+
fs.mkdirSync(path.dirname(fp), { recursive: true });
857+
fs.writeFileSync(fp, content);
858+
console.log(`[OK] wrote ${content.length} chars to ${fp}`);
859+
return true;
860+
} catch (e: any) {
861+
console.log(`[ERR] failed to write file: ${e.message}`);
862+
return true;
863+
}
864+
}
865+
}
866+
867+
// 6) Read file "read file X" / "open file X"
868+
const readAlias = lowerPrompt.match(/^(?:read|open|cat|show|get)\s+(?:file\s+)?(.+)$/);
869+
if (readAlias) {
870+
const candidate = readAlias[1].trim();
871+
if (isPathLike(candidate) || /^[\w\-./\\]+$/.test(candidate)) {
872+
const fp = resolvePath(candidate);
873+
if (fs.existsSync(fp)) {
874+
try {
875+
const txt = fs.readFileSync(fp, "utf-8");
876+
console.log(txt);
877+
return true;
878+
} catch (e: any) {
879+
console.log(`[ERR] failed to read file: ${e.message}`);
880+
return true;
881+
}
882+
}
883+
}
884+
}
885+
886+
// 7) Shell command
887+
const runMatch = lowerPrompt.match(/^(?:(?:run\s+)?(?:command|execute|run|shell))\s+(.+)$/);
750888
if (runMatch) {
751889
try {
752-
spawnSync(runMatch[1]!, { shell: true, stdio: "inherit" });
890+
spawnSync(runMatch[1]!, { shell: true, stdio: "inherit", timeout: 30000 });
753891
console.log(`[OK] command executed: ${runMatch[1]}`);
892+
return true;
754893
} catch (e: any) {
755894
console.log(`[ERR] command failed: ${e.message}`);
895+
return true;
756896
}
757-
return true;
758-
}
759-
760-
const searchMatch = lowerPrompt.match(/^(?:(?:run\s+)?search(?:\s+for)?)\s+(.+)$/);
761-
if (searchMatch) {
762-
console.log(`[INFO] Searching for: ${searchMatch[1]}...`);
763-
const { WebSearchSkill } = await import("@alpclaw/skills");
764-
const skill = new WebSearchSkill();
765-
const result = await skill.execute({ query: searchMatch[1] }, {} as any);
766-
console.log((result as any).output || "[INFO] No results found.");
767-
return true;
768897
}
769898

770899
return false;

packages/core/src/planner.ts

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,25 +60,67 @@ Keep the plan practical and minimal. Only include steps that are necessary.`;
6060

6161
log.info("Creating plan", { taskDescription: taskDescription.slice(0, 100) });
6262

63-
const result = await this.router.route(
63+
const strictSystemPrompt = `${systemPrompt}
64+
CRITICAL: You must respond with ONLY a JSON object matching the schema above. Do not use markdown. Do not add explanation. Do not wrap in code blocks.`;
65+
66+
let result = await this.router.route(
6467
{ messages, temperature: 0.2, maxTokens: 2000 },
6568
{ taskType: "reasoning" },
6669
);
6770

68-
if (!result.ok) return result as Result<never>;
71+
// Retry once with stricter prompt on failure or empty/invalid plan
72+
if (!result.ok) {
73+
log.warn("Planner first attempt failed, retrying with stricter prompt");
74+
result = await this.router.route(
75+
{ messages: [{ role: "system", content: strictSystemPrompt }, { role: "user", content: taskDescription }], temperature: 0, maxTokens: 1000 },
76+
{ taskType: "reasoning" },
77+
);
78+
}
79+
80+
// Degraded fallback: never return err() from planner
81+
if (!result.ok) {
82+
const is429 = result.error?.message?.includes("429") || result.error?.message?.includes("rate-limit");
83+
const fallback: Plan = {
84+
reasoning: is429
85+
? "Provider rate-limited. Use direct execution."
86+
: "Planner failed after retry. Use direct execution.",
87+
estimatedComplexity: "simple",
88+
steps: [
89+
{
90+
description: is429
91+
? "Execute with a different provider or wait 60 seconds"
92+
: `Execute directly: ${taskDescription}`,
93+
toolOrSkill: is429 ? "provider-fallback" : "unknown",
94+
},
95+
],
96+
};
97+
log.warn("Planner returning degraded fallback plan", { is429 });
98+
return ok(fallback);
99+
}
69100

70101
try {
71102
const content = result.value.content.trim();
72-
// Extract JSON from potential markdown code block
73103
const jsonMatch = content.match(/\{[\s\S]*\}/);
74104
if (!jsonMatch) {
75-
return err(createError("task", "Planner did not return valid JSON"));
105+
const fallback: Plan = {
106+
reasoning: "Planner returned non-JSON. Falling back to direct execution.",
107+
estimatedComplexity: "simple",
108+
steps: [{ description: `Execute directly: ${taskDescription}`, toolOrSkill: "unknown" }],
109+
};
110+
log.warn("Planner returning fallback plan (no JSON)");
111+
return ok(fallback);
76112
}
77113

78114
const plan = JSON.parse(jsonMatch[0]) as Plan;
79115

80116
if (!plan.steps || plan.steps.length === 0) {
81-
return err(createError("task", "Planner returned empty plan"));
117+
const fallback: Plan = {
118+
reasoning: "Planner returned empty steps. Falling back to direct execution.",
119+
estimatedComplexity: "simple",
120+
steps: [{ description: `Execute directly: ${taskDescription}`, toolOrSkill: "unknown" }],
121+
};
122+
log.warn("Planner returning fallback plan (empty steps)");
123+
return ok(fallback);
82124
}
83125

84126
log.info("Plan created", {
@@ -87,8 +129,14 @@ Keep the plan practical and minimal. Only include steps that are necessary.`;
87129
});
88130

89131
return ok(plan);
90-
} catch (cause) {
91-
return err(createError("task", "Failed to parse plan", { cause }));
132+
} catch {
133+
const fallback: Plan = {
134+
reasoning: "Planner JSON parse failed. Falling back to direct execution.",
135+
estimatedComplexity: "simple",
136+
steps: [{ description: `Execute directly: ${taskDescription}`, toolOrSkill: "unknown" }],
137+
};
138+
log.warn("Planner returning fallback plan (parse error)");
139+
return ok(fallback);
92140
}
93141
}
94142
}

0 commit comments

Comments
 (0)