mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
a158434a9c
* feat(cli): compact workflow help card + version header Shrinks the per-task agent protocol tax of the help/skill surface. `agent-device help workflow` drops from 41025 to 8466 bytes (-79%) by moving depth into new `help scripting` (save-script, secret-safe fills, batch JSON, replay divergence/repair) and `help gestures` (multi-touch shapes/quirks) topics, and folding a few paragraphs into topics that already owned the subject (help debugging, help physical-device, help validate). Content is moved, not deleted. Every `help <topic>` first line is now `agent-device <version> — <topic>`, so the skill router reads the CLI version off the mandatory first help read instead of a separate `agent-device --version` call. SKILL.md is updated to do that and stays a thin router otherwise. The compact card also gains two terse behavioral rules: chain confident consecutive steps with `&&` (falling back to one command at a time when uncertain), and confirm the requested end state is actually visible on screen before declaring a task done. help-conformance-bench (22 cases x 2 runners) improves after the change: 29/44 -> 32/44 passing checks. * fix(cli): review follow-ups on the compact workflow card (#1663) Three fixes from PR review: - Extend the help-conformance plan validator to split a command line on unquoted && and validate each chained segment independently, so a plan that follows the workflow card's "chain confident consecutive steps with &&" guidance is accepted instead of rejected as one shell-projection violation. && inside a quoted selector value (e.g. label="A && B") is not a chain boundary and does not split. Adds unit tests for the splitter and a chains-confident-consecutive- settle-steps conformance case. batch stays out of this: it is deliberately stop-only. - Replace the literal @ref placeholder the compact card used in its own "snapshot -s @ref" example with a concrete ref (snapshot -s @e12 (the current concrete ref)), matching the same card's rule against placeholder targets. Reverts the test to demand the concrete shape. - Give help scripting and help gestures real conformance cases instead of waivers: a secret-safe recorded-fill + publish case, and an Android transform-then-verify case whose exact verification text only appears in the gestures topic. Removes both waivers. help-conformance-bench (25 cases x 2 runners, repeat=1) after these fixes: two full runs landed at 32/50 and 33/50. That is on par with the pre-change baseline (29/44) once the topic-untouched cases' run-to-run swings are accounted for (confirmed noise: one case with zero exposure to any change here flipped 10/10 -> 1/10 on a runner API error, and another swung across all three post-fix runs). The new scripting case now passes 8/8 for both runners; the new chaining case correctly reports the model's choice not to chain as a soft signal, not a validator failure. * fix(cli): update session.test.ts help pointer for moved script-authoring content * fix(cli): reject empty && chain operands in the plan validator (#1663) splitOnUnquotedAnd() previously trimmed and filtered out empty segments, so a plan with a leading (`&& press ...`), trailing (`press ... &&`), or doubled (`a && && b`) operator passed validPlanCommands even though a real shell rejects all three as a syntax error. The validator would bless a plan that fails at execution. Empty segments are now surfaced as an `empty-chain-operand` issue instead of being silently dropped. The quoted-&& non-split behavior (label="A && B") is unchanged, and a normal single command with no chain still parses identically to before. Adds regression tests for all three empty-operand shapes plus the quoted-&& case.
114 lines
3.9 KiB
JavaScript
114 lines
3.9 KiB
JavaScript
import { opensAndCloses, usesValidationPrep } from './help-conformance-expectations.mjs';
|
|
|
|
const EXPECTATION_SCORERS = {
|
|
validPlanCommands: ({ commands, commandValidation }) =>
|
|
commands.length > 0 && commandValidation.every(({ issues }) => issues.length === 0),
|
|
fullPrefix: ({ commands }) =>
|
|
commands.length > 0 &&
|
|
commands.every(
|
|
(command) => !/^(open|snapshot|press|fill|click|longpress|wait|close)\b/.test(command),
|
|
),
|
|
usesSnapshotI: ({ commands }) => commands.some((command) => /\bsnapshot\b.*\s-i\b/.test(command)),
|
|
usesSettleOnMutations: ({ commands }) => allMutationsUseSettle(commands),
|
|
// help workflow teaches chaining confident consecutive steps with an
|
|
// unquoted &&. This reads the raw (pre-split) command lines, not `joined`
|
|
// (canonicalPlan flattens a chain into separate lines once the plan
|
|
// validator splits and validates each segment), so it is the only place
|
|
// that can tell whether the model actually chained.
|
|
usesConfidentChaining: ({ commands }) => commands.some((command) => /&&/.test(command)),
|
|
noWaitStable: ({ joined }) => !joined.includes('wait stable'),
|
|
verifiesNamedExpectation: ({ joined }) => /\b(wait|is|get|find)\b/.test(joined),
|
|
usesDogfoodEvidence: ({ joined }) =>
|
|
/(?:\bscreenshot\b|\brecord\b|\blogs\b|\bnetwork\b|\bperf\b|\btrace\b|dogfood-output)/i.test(
|
|
joined,
|
|
),
|
|
usesValidationPrep,
|
|
opensAndCloses,
|
|
};
|
|
|
|
export function assertCaseDefinitions(cases) {
|
|
assertUniqueIds(
|
|
cases.map(({ id }) => id),
|
|
'benchmark case',
|
|
);
|
|
for (const testCase of cases) assertCaseDefinition(testCase);
|
|
}
|
|
|
|
export function countChecks(testCase) {
|
|
return resolveChecks(testCase).length;
|
|
}
|
|
|
|
export function scoreExpectations(testCase, commands, raw, commandValidation) {
|
|
const context = {
|
|
commands,
|
|
joined: canonicalPlan(commandValidation).toLowerCase(),
|
|
raw,
|
|
commandValidation,
|
|
};
|
|
return Object.fromEntries(resolveChecks(testCase).map(({ id, test }) => [id, test(context)]));
|
|
}
|
|
|
|
function resolveChecks(testCase) {
|
|
const named = (testCase.expectations ?? []).map((id) => ({
|
|
id,
|
|
test: (context) => scoreExpectation(id, context),
|
|
}));
|
|
const matched = (testCase.matchers ?? []).map(({ id, pattern }) => ({
|
|
id,
|
|
test: (context) => pattern.test(context.joined),
|
|
}));
|
|
const forbidden = (testCase.forbidden ?? []).map(({ id, pattern }) => ({
|
|
id,
|
|
test: (context) => !pattern.test(context.joined),
|
|
}));
|
|
return [...named, ...matched, ...forbidden];
|
|
}
|
|
|
|
function canonicalPlan(commandValidation) {
|
|
return commandValidation.map(({ tokens }) => tokens.map(canonicalToken).join(' ')).join('\n');
|
|
}
|
|
|
|
function canonicalToken(token) {
|
|
return /\s/.test(token) ? JSON.stringify(token) : token;
|
|
}
|
|
|
|
function assertCaseDefinition(testCase) {
|
|
const unknownExpectation = (testCase.expectations ?? []).find(
|
|
(expectation) => !(expectation in EXPECTATION_SCORERS),
|
|
);
|
|
if (unknownExpectation) {
|
|
throw new Error(`Unknown expectation "${unknownExpectation}" in case "${testCase.id}".`);
|
|
}
|
|
assertUniqueIds(
|
|
resolveChecks(testCase).map(({ id }) => id),
|
|
`check in case "${testCase.id}"`,
|
|
);
|
|
}
|
|
|
|
function assertUniqueIds(ids, label) {
|
|
const seen = new Set();
|
|
const duplicates = new Set();
|
|
for (const id of ids) {
|
|
if (seen.has(id)) duplicates.add(id);
|
|
seen.add(id);
|
|
}
|
|
if (duplicates.size > 0) {
|
|
throw new Error(`Duplicate ${label} id(s): ${[...duplicates].join(', ')}`);
|
|
}
|
|
}
|
|
|
|
function scoreExpectation(expectation, context) {
|
|
const scorer = EXPECTATION_SCORERS[expectation];
|
|
if (!scorer) throw new Error(`Unknown expectation: ${expectation}`);
|
|
return scorer(context);
|
|
}
|
|
|
|
function allMutationsUseSettle(commands) {
|
|
const mutating = commands.filter(isMutationCommand);
|
|
return mutating.length > 0 && mutating.every((command) => command.includes('--settle'));
|
|
}
|
|
|
|
function isMutationCommand(command) {
|
|
return /^agent-device\s+(?:press|click|fill|longpress)\b/.test(command);
|
|
}
|