fix(agent): address code review issues

- Add empty goal guard in parser (returns done instead of passthrough)
- Replace `as any` casts in pipeline.ts with proper ActionDecision types
- Add runtime type guards for untrusted LLM output in classifier
- Add intent action to dynamic prompt so UI agent can fire intents

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sanju Sivalingam
2026-02-18 00:32:14 +05:30
parent 3769b21ed1
commit 9193b02d36
4 changed files with 15 additions and 8 deletions

View File

@@ -62,12 +62,16 @@ export async function classifyGoal(
switch (parsed.type) { switch (parsed.type) {
case "intent": { case "intent": {
const rawExtras = parsed.extras;
const validExtras = (rawExtras && typeof rawExtras === "object" && !Array.isArray(rawExtras))
? rawExtras as Record<string, string>
: undefined;
const intent: IntentCommand = { const intent: IntentCommand = {
intentAction: (parsed.intentAction as string) ?? "", intentAction: typeof parsed.intentAction === "string" ? parsed.intentAction : "",
uri: parsed.uri as string | undefined, uri: typeof parsed.uri === "string" ? parsed.uri : undefined,
intentType: parsed.intentType as string | undefined, intentType: typeof parsed.intentType === "string" ? parsed.intentType : undefined,
extras: parsed.extras as Record<string, string> | undefined, extras: validExtras,
packageName: parsed.packageName as string | undefined, packageName: typeof parsed.packageName === "string" ? parsed.packageName : undefined,
}; };
if (!intent.intentAction) { if (!intent.intentAction) {
console.warn("[Classifier] Intent missing intentAction, falling through"); console.warn("[Classifier] Intent missing intentAction, falling through");

View File

@@ -293,7 +293,8 @@ Scrolling:
App Control: App Control:
{"action": "launch", "package": "com.app.name", "reason": "Open app"} {"action": "launch", "package": "com.app.name", "reason": "Open app"}
{"action": "switch_app", "package": "com.app.name", "reason": "Switch app"}`; {"action": "switch_app", "package": "com.app.name", "reason": "Switch app"}
{"action": "intent", "intentAction": "android.intent.action.VIEW", "uri": "tel:123", "reason": "Fire Android intent directly"}`;
// Multi-step actions (always useful) // Multi-step actions (always useful)
actions += ` actions += `

View File

@@ -339,6 +339,7 @@ export function parseGoal(
caps: DeviceCapabilities caps: DeviceCapabilities
): PipelineResult { ): PipelineResult {
const trimmed = goal.trim(); const trimmed = goal.trim();
if (!trimmed) return { stage: "parser", type: "done", reason: "Empty goal" };
for (const matcher of PATTERNS) { for (const matcher of PATTERNS) {
const result = matcher(trimmed, caps); const result = matcher(trimmed, caps);
if (result) return result; if (result) return result;

View File

@@ -8,6 +8,7 @@
import type { import type {
InstalledApp, InstalledApp,
PipelineResult, PipelineResult,
ActionDecision,
} from "@droidclaw/shared"; } from "@droidclaw/shared";
import { sessions } from "../ws/sessions.js"; import { sessions } from "../ws/sessions.js";
import { db } from "../db.js"; import { db } from "../db.js";
@@ -185,7 +186,7 @@ export async function runPipeline(
onStep?.({ onStep?.({
stepNumber: 1, stepNumber: 1,
action: { action: parseResult.type, reason: `Parser: ${parseResult.type}` } as any, action: { action: parseResult.type, reason: `Parser: ${parseResult.type}` } as unknown as ActionDecision,
reasoning: `Parser: direct ${parseResult.type} action`, reasoning: `Parser: direct ${parseResult.type} action`,
screenHash: "", screenHash: "",
}); });
@@ -228,7 +229,7 @@ export async function runPipeline(
onStep?.({ onStep?.({
stepNumber: 1, stepNumber: 1,
action: { action: "intent", reason: "Classifier: intent" } as any, action: { action: "intent", intentAction: classResult.intent.intentAction, uri: classResult.intent.uri, reason: "Classifier: intent" } as unknown as ActionDecision,
reasoning: `Classifier: ${classResult.intent.intentAction}`, reasoning: `Classifier: ${classResult.intent.intentAction}`,
screenHash: "", screenHash: "",
}); });