Let the agent use the graph, scene and project commands (#429)
The agent knew 23 of the editor's commands. It could not point a Variables node at a variable, group nodes, edit ports, transitions, containers, colliders or the project settings. Stage 5a brings the catalog to 85 commands in five areas and keeps every answer short. - The catalog lists names and descriptions only. With a query (an area or command names) it adds the arguments. All schemas at once would pass the 60 000 character limit of one answer. - add_port points a Variables node at a variable. The port takes the variable's kind, and an unknown name is refused with the list of the ones that exist. - add_group and add_comment take nodes instead of coordinates. They follow the nodes after the arrange step, in the same undo step. - The rule for which list a port picks from moved into portChoices.ts. Inspector and agent now share it. - The frame of a group around nodes moved into annotationDrag.ts, so the button and the agent get the same frame. - list_assets returns the project settings. get_asset shows the content of placed containers with the ids overrides need. get_graph shows declared ports and frame colors. - A new node is placed at the height of its port, in the free spot whose links cross the fewest others. A source goes left of the node it feeds. Before, new nodes always went right of the rest. - Arrange breaks a tie in crossings by putting the flow on top, and exec links pull harder when a column is sorted. - A comment over a group keeps clear of the group's title bar. - The agent's instructions and lint_graph ask to look at crossings too and to call arrange_graph when there are more than needed. Left out on purpose: file assets (no upload), paste from the clipboard, internal wrapper commands and switching plugins on. Found on the way: - Variable commands on a Struct asset did not run through WithStructTypes. New fields did not reach variables of that type. - A query "variables" also matched set_variables_scope. A word that names an area now means only that area. Tests: - agent-catalog (new, 10), agent-edit, graph-arrange (3 new) and relay green. 11 bends of the code, all red. - e2e 71 on atx: 22/22, the new check sees port speed and a group around all 4 drawn nodes. - Live on a test project: a speed variable of 300 moved the player to x 150.03 in 0.5 s. A re-placed Variables node landed below Axis input with 4 crossings instead of 7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1943539a7a
commit
0d8fd6ff52
26 files changed
+2585
-672
No files matched your search
@@ -1,60 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { type Command, CommandError } from '../../src/core/commands/Command';
|
||||
import { findGraphAsset } from '../../src/core/model/Project';
|
||||
import { argProblem } from '../../src/editor/agent/argCheck';
|
||||
import { runTool, type ToolHost, type ToolResult } from '../../src/editor/agent/agentTools';
|
||||
import type { AgentView } from '../../src/editor/agent/agentView';
|
||||
import { COMMANDS, type StepTarget } from '../../src/editor/agent/commandCatalog';
|
||||
import { runTool } from '../../src/editor/agent/agentTools';
|
||||
import { COMMANDS } from '../../src/editor/agent/commandCatalog';
|
||||
import { NODE_STEP } from '../../src/editor/agent/editPlan';
|
||||
import { COLUMN_GAP, type GraphLint } from '../../src/editor/graph/arrange';
|
||||
import { NODE_WIDTH } from '../../src/editor/graph/nodeBox';
|
||||
import TOOLS from '../../src/editor/agent/tools.json';
|
||||
import { makeBehavior, makeSpriteScene, type SceneFixture } from '../helpers';
|
||||
import { hostFor, text } from './agentTestHost';
|
||||
|
||||
/** What an agent changes and reads through the tools (#429, stage 2). */
|
||||
|
||||
function hostFor(fixture: SceneFixture, deleteAnswer: string | null = null, follows = false) {
|
||||
const asked: string[][] = [];
|
||||
const shown: StepTarget[] = [];
|
||||
const host: ToolHost = {
|
||||
view: () => ({ play: 'editing' }) as AgentView,
|
||||
screenshot: () => Promise.resolve({ data: '', width: 0, height: 0 }),
|
||||
project: () => fixture.project,
|
||||
execute: (command: Command) => {
|
||||
try {
|
||||
fixture.stack.execute(command);
|
||||
return null;
|
||||
} catch (error) {
|
||||
if (error instanceof CommandError) return error.message;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
confirmDeletes: (ids) => {
|
||||
asked.push([...ids]);
|
||||
return Promise.resolve(deleteAnswer);
|
||||
},
|
||||
open: () => null,
|
||||
select: () => null,
|
||||
play: () => null,
|
||||
undo: () => {
|
||||
const label = fixture.stack.undoLabel;
|
||||
if (label !== null) fixture.stack.undo();
|
||||
return label;
|
||||
},
|
||||
redo: () => {
|
||||
const label = fixture.stack.redoLabel;
|
||||
if (label !== null) fixture.stack.redo();
|
||||
return label;
|
||||
},
|
||||
follows: () => follows,
|
||||
show: (focus) => shown.push(focus),
|
||||
};
|
||||
return { host, asked, shown };
|
||||
}
|
||||
|
||||
const text = (result: ToolResult) =>
|
||||
result.content.map((item) => (item.type === 'text' ? item.text : '')).join('\n');
|
||||
|
||||
/** An object that walks left while the left arrow is held: behavior, placement, three nodes, three edges. */
|
||||
function walkLeft(fixture: SceneFixture) {
|
||||
return [
|
||||
@@ -470,12 +427,32 @@ describe('the catalog', () => {
|
||||
expect(command.args).toMatchObject({ type: 'object', additionalProperties: false });
|
||||
expect(argProblem(command.args, {})).toMatch(/is missing|^null$/);
|
||||
}
|
||||
const fixture = makeSpriteScene();
|
||||
const listed = (await runTool('catalog', { topic: 'commands' }, hostFor(fixture).host))
|
||||
.structuredContent as {
|
||||
commands: { name: string }[];
|
||||
const { host } = hostFor(makeSpriteScene());
|
||||
const listed = (await runTool('catalog', { topic: 'commands' }, host)).structuredContent as {
|
||||
commands: { areas: { area: string; commands: { name: string; args?: unknown }[] }[] };
|
||||
};
|
||||
expect(listed.commands.map((command) => command.name)).toEqual(names);
|
||||
const areas = listed.commands.areas;
|
||||
expect(areas.flatMap((area) => area.commands.map((command) => command.name))).toEqual(names);
|
||||
expect(areas.flatMap((area) => area.commands).some((command) => 'args' in command)).toBe(false);
|
||||
// Each area in full fits one answer, which is cut beyond 60 000 characters.
|
||||
for (const { area, commands } of areas) {
|
||||
const full = await runTool('catalog', { topic: 'commands', query: area }, host);
|
||||
expect(full.isError).toBeUndefined();
|
||||
expect(text(full).length).toBeLessThan(60_000);
|
||||
const rows = (full.structuredContent as { commands: { name: string; args: unknown }[] }).commands;
|
||||
expect(rows.map((row) => row.name)).toEqual(commands.map((command) => command.name));
|
||||
expect(rows.every((row) => row.args !== undefined)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('a query of command names answers just those in full, and an unknown one says so', async () => {
|
||||
const { host } = hostFor(makeSpriteScene());
|
||||
const found = await runTool('catalog', { topic: 'commands', query: 'add_port, add_group' }, host);
|
||||
const rows = (found.structuredContent as { commands: { area: string; name: string }[] }).commands;
|
||||
expect(rows.map((row) => `${row.area}/${row.name}`)).toEqual(['graphs/add_group', 'graphs/add_port']);
|
||||
const none = await runTool('catalog', { topic: 'commands', query: 'teleport' }, host);
|
||||
expect(none.isError).toBe(true);
|
||||
expect(text(none)).toContain('No area or command matches "teleport"');
|
||||
});
|
||||
|
||||
it('the tools are the ones the relay lists, each with a schema', () => {
|
||||
|
||||
Reference in new issue
Block a user