mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
b44f882c83
* docs(batch): name the step shape and the accepted commands in help and refusals
`batch` accepts one step shape and `help batch` documented none of it: the
usage line, one sentence, and the flags. Every refusal named only what was
wrong. A caller reaching for `press` through `batch` therefore saw
"Invalid batch step 1." for `["press @e12"]` and "unknown field(s): args" for
`{"command":"press","args":[...]}`, and reasonably concluded the mutating verbs
were excluded (#2062).
They are not, and never were: `press`, `click`, `fill`, `longpress`, `scroll`
and `back` all carry `batchable: true` in the command-descriptor registry,
including at 0.20.10. The exclusions are `batch`/`replay` (which never nest) and
the session/daemon/connection/host-tooling commands. Nothing about the
allowlist changes here; what changes is that it is stated.
- `help batch` documents the step shape, serial semantics, and RENDERS the
accepted commands from the registry's `batchable` trait, so the listing cannot
drift from the runtime allowlist.
- The step-shape refusals (non-object step, unknown field, non-object input)
share one hint naming `{"command":"<name>","input":{...}}`, owned by
`batch-contract.ts` next to the checks that raise them.
- The non-batchable-command refusal points at that listing and says which
families are excluded and why.
- `assertAllowedKeys` takes an optional hint so the batch call sites can attach
theirs without a second unknown-key check.
Closes #2062
* docs(batch): ground the step-shape guidance in the structured schema and keep contracts surface-neutral
`help batch` now prints runnable snapshot/press/fill steps carrying the real
structured field names (`target: {kind, ref}`, `text`, `interactiveOnly`), which
no `help <command>` text states, and says so instead of pointing at command help
for them. `cli-help-examples.test.ts` reads those steps back out of the rendered
help and runs each `input` through its own command's `readInput`, so a renamed
field fails there rather than shipping guidance nobody can run.
Fixes the stale batch guidance the audit missed: `help workflow` named
`batch ./steps.json`, which positional input rejects, and `help scripting` still
weighed the removed positionals/flags shape against the accepted one.
`BATCH_STEP_SHAPE_HINT` in `@agent-device/contracts` describes the shape only;
`readBatchStepRecord`/`readBatchStepInputObject` take the hint as a parameter so
the CLI attaches its own `agent-device help batch` recovery step while the Node
client and MCP tools keep the surface-neutral one.
* test(batch): pin every advertised step key, and hint the removed-shape refusal
The example-validation test accepted a step whose optional key the
reader silently dropped — readInput ignores unknown keys, so a renamed
settle or interactiveOnly kept the test green while help advertised a
step that does less than it claims. Every printed key must now survive
into the parsed input.
The removed positionals/flags refusal carries the CLI shape hint like
its three sibling refusals.
* fix(batch): keep the availability refusal surface-neutral; CLI attaches its recovery
readStructuredBatchCommandName emitted 'Run agent-device help batch'
unconditionally, and the same reader backs the MCP/Node batch metadata
— an MCP caller got a terminal-only, unrunnable recovery step. The
shared default now states the exclusion boundary itself with no
terminal vocabulary (MCP/Node read the accepted commands off the step
schema's command enum), and the CLI admission appends the help pointer
via the same optional-hint parameter the shape hint uses.
Found while fixing it: hint strings are redaction-capped at 400
characters, so enumerating the derived roster inline truncates the
hint — recovery pointer and all. The regression pins neutrality AND
that the hint survives the cap whole.
Addresses the P2 review on #2067.
---------
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
71 lines
2.5 KiB
TypeScript
71 lines
2.5 KiB
TypeScript
import { daemonRuntimeSchema, type SessionRuntimeHints } from '@agent-device/kernel/contracts';
|
|
import { AppError } from '@agent-device/kernel/errors';
|
|
import { isRecord } from './json.ts';
|
|
|
|
export const DEFAULT_BATCH_MAX_STEPS = 100;
|
|
|
|
export function isValidBatchMaxSteps(maxSteps: number): boolean {
|
|
return Number.isInteger(maxSteps) && maxSteps >= 1 && maxSteps <= 1000;
|
|
}
|
|
|
|
export function assertBatchStepCount(stepCount: number, maxSteps: number): void {
|
|
if (stepCount > maxSteps) {
|
|
throw new AppError('INVALID_ARGS', `batch has ${stepCount} steps; max allowed is ${maxSteps}.`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The one sentence every batch-step shape refusal owes the caller. `batch` accepts a single step
|
|
* shape, and none of its refusals named it: a string step answered "Invalid batch step 1." and an
|
|
* `args`/`target`/`argv` step answered "unknown field(s)", neither of which says what a step
|
|
* looks like (#2062).
|
|
*
|
|
* It describes the shape only. This module validates for the Node client and the MCP tools as
|
|
* well as the CLI, so a terminal recovery step ("run agent-device help ...") belongs to the CLI
|
|
* call sites, which pass their own hint through the `hint` parameters below.
|
|
*/
|
|
export const BATCH_STEP_SHAPE_HINT =
|
|
'Each batch step is {"command":"<name>","input":{...}}, where input is that command\'s own ' +
|
|
'structured input object, keyed by field name. There is no positional step form: args, argv, ' +
|
|
'positionals, and flags are not step fields.';
|
|
|
|
export function readBatchStepRecord(
|
|
step: unknown,
|
|
stepNumber: number,
|
|
hint: string = BATCH_STEP_SHAPE_HINT,
|
|
): Record<string, unknown> {
|
|
if (!isRecord(step)) {
|
|
throw new AppError('INVALID_ARGS', `Invalid batch step ${stepNumber}.`, { hint });
|
|
}
|
|
return step;
|
|
}
|
|
|
|
export function readBatchStepInputObject(
|
|
record: Record<string, unknown>,
|
|
stepNumber: number,
|
|
hint: string = BATCH_STEP_SHAPE_HINT,
|
|
): Record<string, unknown> {
|
|
const input = record.input;
|
|
if (!isRecord(input)) {
|
|
throw new AppError('INVALID_ARGS', `Batch step ${stepNumber} input must be an object.`, {
|
|
hint,
|
|
});
|
|
}
|
|
return input;
|
|
}
|
|
|
|
export function parseBatchStepRuntime(
|
|
value: unknown,
|
|
stepNumber: number,
|
|
): SessionRuntimeHints | undefined {
|
|
if (value === undefined) return undefined;
|
|
try {
|
|
return daemonRuntimeSchema.parse(value);
|
|
} catch (error) {
|
|
throw new AppError(
|
|
'INVALID_ARGS',
|
|
`Batch step ${stepNumber} runtime is invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|