Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
8327954
test(structure): a host with its own runtime divides rooms without th…
AxiomeCG Oct 6, 2026
810efa9
feat(agent-tools): one contract per tool on the MCP and the chat
AxiomeCG Oct 5, 2026
ff91873
feat(agent-tools): add_door builds a passage with no leaf (openingKin…
AxiomeCG Oct 5, 2026
c7fcfbf
fix(core): a level copy fills a freed floor and moves only the floors…
AxiomeCG Oct 5, 2026
72c5016
feat(agent-tools): add_door and add_window say what goes in code
AxiomeCG Oct 5, 2026
5f3279f
feat(agent-tools): one answer per write, said where it went (L56, S1)
AxiomeCG Oct 6, 2026
c11a2cb
feat(agent-tools): patches that do what they say
AxiomeCG Oct 6, 2026
ebe723e
feat(agent-tools): place_items hosts items on walls, ceilings and items
AxiomeCG Oct 6, 2026
90b9e1f
feat(agent-tools): create_stair takes create_stair_between_levels' op…
AxiomeCG Oct 6, 2026
29caddf
fix(agent-tools): create_stair's opening centre is a pair both lanes …
AxiomeCG Oct 6, 2026
5cb407a
docs(skills): place_items hosts items on walls, ceilings and items
AxiomeCG Oct 6, 2026
8cd30d5
refactor(agent-tools): an unknown material points to a flat colour by…
AxiomeCG Oct 6, 2026
178abb4
fix(mcp): create_project in a session bound to another project starts…
AxiomeCG Oct 2, 2026
70acf66
fix(mcp): save_scene never writes a session's scene over a project it…
AxiomeCG Oct 2, 2026
e285d2c
fix(mcp): save_scene without a target saves the session's project
AxiomeCG Oct 6, 2026
d84424f
fix(agent): duplicate_level lists at most 40 new ids and counts the rest
AxiomeCG Oct 6, 2026
f40abc1
test(agent): verify_scene with a stair's owned floor opening
AxiomeCG Oct 6, 2026
8f8bb72
fix(mcp): add_object answers what it achieved, as every write does
AxiomeCG Oct 6, 2026
61d07b1
fix(core): a wall behind a porch or a terrace is exterior on that face
AxiomeCG Oct 6, 2026
f22b87f
fix(agent-tools): furnish_room's door skip reads its new wording ever…
AxiomeCG Oct 6, 2026
1c1ef59
feat(agent-tools): place_items refuses a floor item in a door's way o…
AxiomeCG Oct 6, 2026
fbacb00
fix(core): the editor's default level takes core's storey height cons…
AxiomeCG Oct 6, 2026
1a8f526
test(agent-tools): a roof's nearest are roofing by the library's tags
AxiomeCG Oct 6, 2026
a407ebf
refactor(mcp): apply_patch's guards register from the module that own…
AxiomeCG Oct 6, 2026
35a4a0e
fix(doors): a door on an outside wall faces out, for agents and by hand
AxiomeCG Oct 6, 2026
8d7a3ca
test(core): the porch case checks the door's facing on its own
AxiomeCG Oct 6, 2026
478bdeb
Merge pascalorg/editor main (#915 fences, #1000 stair winders) into t…
AxiomeCG Oct 6, 2026
19e2741
fix(agent-tools): create_stair sizes its flight as main does
AxiomeCG Oct 6, 2026
f0af938
style: the merge's contract imports in sorted order
AxiomeCG Oct 6, 2026
bb6dd80
Merge pascalorg/editor main (#996 hosted service contracts) into the …
AxiomeCG Oct 6, 2026
4711336
fix(agent-tools): a room boundary with no wall is named as the editor…
AxiomeCG Oct 6, 2026
714c4ea
docs: comments say why, without internal test-run names, codes or dates
AxiomeCG Oct 6, 2026
1a75d84
fix(agent-tools): add_object's hints name only tools the server offers
AxiomeCG Oct 6, 2026
40067a0
fix(agent-tools): create_stair names the slab and ceiling its opening…
AxiomeCG Oct 6, 2026
5e4980b
fix(agent-tools): create_stair reports a slab hole only when the slab…
AxiomeCG Oct 6, 2026
f40a5eb
feat(editor): ephemeral captures, for an agent looking at the scene
AxiomeCG Oct 6, 2026
dd970c4
feat(agent-tools): view_scene on both surfaces; the MCP asks an edito…
AxiomeCG Oct 6, 2026
038c13b
fix(viewer): a capture waits for roofs whose merged shell is still to…
AxiomeCG Oct 6, 2026
e9e7d38
fix(viewer): only roofs on screen hold a capture for their shell
AxiomeCG Oct 6, 2026
e9a04e8
feat(agent-tools): the note a view comes with is core's, the same on …
AxiomeCG Oct 6, 2026
a931255
feat(agent-tools): view_scene frames one door, window or item at deta…
AxiomeCG Oct 6, 2026
98947b1
feat(agent-tools): view_scene returns the photo's crop beside the view
AxiomeCG Oct 6, 2026
1038434
fix(agent-tools): one crop size on both hosts, and a photo source tru…
AxiomeCG Oct 6, 2026
c83f767
feat(agent-operations): a small photo crop is enlarged to 512 px
AxiomeCG Oct 6, 2026
29a25d7
feat(agent-operations): view_scene frames a stair, a column, a fence …
AxiomeCG Oct 6, 2026
112bbd6
fix(agent-operations): view_scene frames a level with no walls by wha…
AxiomeCG Oct 6, 2026
0e2fa49
feat(mcp): a host adds its own lines to what a client reads at connect
AxiomeCG Oct 6, 2026
161c679
docs(agent-tools): view_scene's camera is any photo's, named by its f…
AxiomeCG Oct 6, 2026
3839f99
docs(agent-tools): view_scene names the refusals of the host's editor…
AxiomeCG Oct 6, 2026
4e70b48
docs: vision's comments say why, without internal test-run names, cod…
AxiomeCG Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 251 additions & 0 deletions packages/core/src/agent-operations/__fixtures__/add-object-cases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import { artifactUrl } from '../../lib/artifact-store'
import { scriptSource } from '../../lib/geometry-script-node'
import {
BuildingNode,
type CompiledGeometryScript,
GeometryArtifactManifest,
type GeometryScriptMount,
ItemNode,
LevelNode,
WallNode,
} from '../../schema'
import type { AgentSurface, SceneGraph } from './cases'

/**
* `add_object`, the one way to author an object Pascal has no type for. Each new object says what
* it stands in for (its reason, listed by verify_scene), so every one names a gap in Pascal; one
* on a floor shaped like a wall or a floor plate is refused with the tool to use; one named
* after something Pascal builds gets a hint, not a refusal (a word gate once refused "entry door
* pull handle (brass)", and the agent relabelled it until the label named nothing).
*
* The surfaces compile `code` before the operation runs; a case carries what that compile
* produced, and each runner hands it over in place of its own compile.
*/
export type AddObjectCase = {
name: string
scene: () => SceneGraph
input: Record<string, unknown>
compiled: CompiledGeometryScript
context?: { activeLevelId?: string | null }
surfaces?: AgentSurface[]
expect:
| {
refusal: string
mentions?: string[]
/** Refused before the script runs: the compile is never called. */
beforeCompile?: true
}
| {
result: Record<string, unknown>
/** Fields the object built or edited (the result's nodeId) has afterwards. */
node?: Record<string, unknown>
/** Text the result must include. */
mentions?: string[]
}
}

/**
* A compile of a solid of this size, bottom-centre at the origin, as the compiler reports it: a
* plain box (12 triangles) unless it has more detail.
*/
export function compiledSolid(
[width, height, depth]: [number, number, number],
mount: GeometryScriptMount = 'floor',
triangles = 12,
): CompiledGeometryScript {
return {
sha256: 'a'.repeat(64),
script: 'b'.repeat(64),
mount,
params: {},
manifest: GeometryArtifactManifest.parse({
bounds: { min: [-width / 2, 0, -depth / 2], max: [width / 2, height, depth / 2] },
triangles,
}),
}
}

/** An object add_object built earlier, with the reason it was given. */
export function authoredItem(
id: string,
parentId: string,
fields: { name: string; category: string; reason?: string; size: [number, number, number] },
) {
const compiled = compiledSolid(fields.size)
return ItemNode.parse({
id,
parentId,
name: fields.name,
source: scriptSource(compiled),
asset: {
id: `script_${compiled.sha256.slice(0, 16)}`,
category: fields.category,
name: fields.name,
thumbnail: '',
source: 'mine',
src: artifactUrl(compiled.sha256),
dimensions: fields.size,
},
...(fields.reason ? { metadata: { reason: fields.reason } } : {}),
})
}

/** One floor with a 6 m wall, and a screen built earlier with add_object. */
function floorScene(): SceneGraph {
const wall = WallNode.parse({
id: 'wall_south',
parentId: 'level_0',
start: [0, 0],
end: [6, 0],
height: 3,
})
const screen = authoredItem('item_screen', 'level_0', {
name: 'Garden screen',
category: 'screen',
reason: 'No screen type.',
size: [1.2, 1.8, 0.2],
})
const level = LevelNode.parse({
id: 'level_0',
parentId: 'building_main',
level: 0,
height: 3,
children: [wall.id, screen.id],
})
const building = BuildingNode.parse({ id: 'building_main', children: [level.id] })
return {
nodes: Object.fromEntries([building, level, wall, screen].map((node) => [node.id, node])),
rootNodeIds: [building.id],
}
}

const CODE = 'export default function build({ THREE }) { return new THREE.Group() }'

const create = (
name: string,
input: Record<string, unknown>,
size: [number, number, number],
expect: AddObjectCase['expect'],
mount: GeometryScriptMount = 'floor',
triangles?: number,
): AddObjectCase => ({
name,
scene: floorScene,
input: { code: CODE, reason: 'Pascal has no type for it.', ...input },
compiled: compiledSolid(size, mount, triangles),
expect,
})

export const ADD_OBJECT_CASES: AddObjectCase[] = [
create(
'a new object keeps what it stands in for',
{
name: 'Cornice',
category: 'trim',
reason: 'Pascal has no cornice type.',
position: [3, 2.4, 1],
},
[6, 0.3, 0.4],
{
result: { mount: 'floor' },
node: {
name: 'Cornice',
parentId: 'level_0',
asset: { category: 'trim' },
metadata: { reason: 'Pascal has no cornice type.' },
},
},
),
// Live over the MCP (2026-10-05): the script ran and its artifacts were stored before the missing
// reason was refused.
create(
'a new object without a reason is refused before its script runs',
{ name: 'Planter', reason: undefined },
[1.2, 0.5, 0.4],
{ refusal: 'reason_required', mentions: ['reason'], beforeCompile: true },
),
// Production saw whole houses of plain custom solids, nothing editable as walls or rooms.
create(
'a plain box with a wall’s size is refused: walls exist',
{ name: 'Panel', category: 'panel' },
[4, 2.7, 0.2],
{ refusal: 'use_walls', mentions: ['add_wall'] },
),
// A bookcase and a breeze-block screen (about 1.4 m wide and the tall box's height)
// have a wall's box too, with shelves or holes in it: built, the wall tool named in case.
create(
'a detailed object with a wall’s size is built, with a hint naming the wall tool',
{ name: 'Breeze-block screen', category: 'screen' },
[1.4, 5.6, 0.19],
{ result: { mount: 'floor' }, mentions: ['add_wall'] },
'floor',
1680,
),
create(
'a bookcase is built: furniture may have a wall’s box',
{ name: 'Bookcase', category: 'furniture' },
[1.2, 2.1, 0.35],
{ result: { mount: 'floor' }, node: { name: 'Bookcase' } },
'floor',
96,
),
create(
'a floor plate is refused, measured as drawn in x and z: slabs exist',
{ name: 'Terrace', category: 'deck' },
[8.05, 0.15, 13.52],
{ refusal: 'use_slab', mentions: ['slab', '8.05 × 13.52'] },
),
create(
'a turned floor plate is measured as it stands',
{ name: 'Terrace', category: 'deck', rotation: 90 },
[8.05, 0.15, 13.52],
{ refusal: 'use_slab', mentions: ['13.52 × 8.05'] },
),
create(
'a plate held off the floor is built: a canopy is no slab',
{ name: 'Canopy', category: 'canopy', position: [3, 2.6, 1] },
[4, 0.2, 3],
{ result: { mount: 'floor' }, node: { name: 'Canopy' } },
),
create(
'a wall-side object shaped like a wall is built: it dresses the wall',
{ name: 'Wainscot', category: 'panel', parentId: 'wall_south', position: [3, 0, 0] },
[4, 2.6, 0.05],
{ result: { mount: 'wall-side' }, node: { parentId: 'wall_south' } },
'wall-side',
),
create(
'a name naming a Pascal type is built, with a hint naming the tool for it',
{ name: 'Spiral staircase', category: 'stair' },
[1.6, 2.8, 1.6],
{ result: { mount: 'floor' }, mentions: ['is something Pascal builds: create_stair'] },
),
create(
'a type word that only qualifies the name is built',
{ name: 'entry door pull handle (brass)', category: 'hardware', position: [1, 0.9, 1] },
[0.04, 0.6, 0.06],
{ result: { mount: 'floor' }, node: { name: 'entry door pull handle (brass)' } },
),
// The editor's inspector and resize handles rebuild an object through the same operation: a
// person may make it any shape.
{
name: 'an edit is not judged by its shape, and keeps the reason',
scene: floorScene,
input: { nodeId: 'item_screen', code: CODE },
compiled: compiledSolid([4, 2.6, 0.2]),
expect: {
result: { nodeId: 'item_screen', size: [4, 2.6, 0.2] },
node: { metadata: { reason: 'No screen type.' } },
},
},
{
name: 'an edit takes a new reason',
scene: floorScene,
input: { nodeId: 'item_screen', code: CODE, reason: 'No breeze-block type.' },
compiled: compiledSolid([1.2, 1.8, 0.2]),
expect: {
result: { nodeId: 'item_screen' },
node: { metadata: { reason: 'No breeze-block type.' } },
},
},
]
Loading
Loading