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) {
case "intent": {
const rawExtras = parsed.extras;
const validExtras = (rawExtras && typeof rawExtras === "object" && !Array.isArray(rawExtras))
? rawExtras as Record<string, string>
: undefined;
const intent: IntentCommand = {
intentAction: (parsed.intentAction as string) ?? "",
uri: parsed.uri as string | undefined,
intentType: parsed.intentType as string | undefined,
extras: parsed.extras as Record<string, string> | undefined,
packageName: parsed.packageName as string | undefined,
intentAction: typeof parsed.intentAction === "string" ? parsed.intentAction : "",
uri: typeof parsed.uri === "string" ? parsed.uri : undefined,
intentType: typeof parsed.intentType === "string" ? parsed.intentType : undefined,
extras: validExtras,
packageName: typeof parsed.packageName === "string" ? parsed.packageName : undefined,
};
if (!intent.intentAction) {
console.warn("[Classifier] Intent missing intentAction, falling through");

View File

@@ -293,7 +293,8 @@ Scrolling:
App Control:
{"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)
actions += `

View File

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

View File

@@ -8,6 +8,7 @@
import type {
InstalledApp,
PipelineResult,
ActionDecision,
} from "@droidclaw/shared";
import { sessions } from "../ws/sessions.js";
import { db } from "../db.js";
@@ -185,7 +186,7 @@ export async function runPipeline(
onStep?.({
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`,
screenHash: "",
});
@@ -228,7 +229,7 @@ export async function runPipeline(
onStep?.({
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}`,
screenHash: "",
});