Repository navigation
Commit ca0dfb6
feat(spec): offer agent.structuredOutput on the agent form and drop its stale not-enforced-yet ledger row (#21398)
Fixes #21374
Clause-②: no
The form reconciliation ledger kept `agent.structuredOutput` unoffered
on a reason that stopped being true: the key is enforced and graded
`live`. This PR deletes that row and decides the offer on a measurement,
as triage `5948883894` directed. The measurement says the Studio
`composite` control carries the block, including its free-form JSON
Schema record, so the key is offered.
## What changes
| file | change |
|---|---|
| `packages/spec/src/system/metadata-form-zod-reconciliation.test.ts` |
The `agent` / `structuredOutput` `omit` row (base lines 408 to 414) is
deleted. The gate's own rule (lines 307 to 309) gives this decision to
the enforcement. No other line changes, and nothing new checks a row's
`why` text. |
| `packages/spec/src/ai/agent.form.ts` | One row in the AI Configuration
section: `{ field: 'structuredOutput', type: 'composite', helpText }`.
It is spelled like `memory` and `guardrails`, with no hand-written
`fields`, so Studio derives the sub-rows from the served JSON Schema. |
| four
`packages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts`
| Regenerated with `pnpm i18n:extract`. Two new leaves per locale: the
row's label and help text. The `zh-CN`, `ja-JP` and `es-ES` leaves are
authored, not left as copies of the English source. After the second
extract, no locale's `source-hashes.generated.ts` holds an entry for
them, so those three files are not in the diff. |
|
`packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts`
| The per-locale translated-label control moves from 659 to 660. The
test measured 660 (`expected 660 to be 659`) before the pin was edited.
|
| `.changeset/21374-agent-structured-output-form-offer.md` |
`@objectstack/spec` minor (a new form offer),
`@objectstack/platform-objects` patch (two catalog leaves). Neither is
breaking. |
## The measurement (triage item 2)
**Where it was read:** objectui at the `.objectui-sha` pin
`31971ff1e28f`, from a local clone with `git show`. `main` moved the pin
to `89cad75d5570` while this was in flight. `SchemaForm.tsx` and
`widgets.tsx` are byte-identical between the two pins (`git diff --stat`
is empty), and `merge-base --is-ancestor 31971ff1e28f 89cad75d5570`
exits 0. So the same reading holds at the pin this branch now carries.
**What the control is given:** the served node, measured with the
emitter's own code. `z.toJSONSchema` was run with
`markErasedAuthoringInput`, then `stripUnauthorableProperties`: the
steps `toJsonSchemaSafe` in `metadata-protocol` takes. `agent` takes the
output arm (24 top-level keys), and `structuredOutput` is served inline,
with no `$ref`:
| child | served node |
|---|---|
| `format`, `fallbackFormat` | `type: string`, `enum: [json_object,
json_schema]` |
| `schema` | `type: object`, `propertyNames: {type: string}`,
`additionalProperties: {}`, and no `properties` |
| `strict`, `retryOnValidationFailure` | `type: boolean` |
| `maxRetries` | `type: integer`, `minimum: 0` |
| `transformPipeline` | `type: array`, `items: {type: string, enum:
[trim, parse_json, validate]}` |
**How a `composite` row renders**
(`packages/app-shell/src/views/metadata-admin/`, at the pin):
1. `SchemaForm.tsx` `resolveFieldFace` (line 837): `fieldSpec.type ===
'composite'` gives `{ kind: 'composite' }`.
2. `FieldControl` (lines 2081 to 2099): the row has no `fields`, so
`derivePropertyNames(schema)` (line 3376) lists every property of the
served node. `CompositeField` (line 2518) renders one `FieldRow` per
property, with the child node from `pickSubSchema(schema, 'composite',
name)` (line 2487). A child edit writes `onChange({ ...obj, [field]: v
})`, and an untouched child is never written.
3. Each child's face is decided by `resolveFieldWidget`, then
`resolveFieldFace`:
- **`schema`, the open record:** `inferWidget` (line 519) answers
`object-fields` for `type: object`, and no name detector matches.
`object-fields` is not a key of `WIDGETS` (`widgets.tsx` line 2934).
`isObjectForm` is false (no `properties`), the node is not an object-row
array, and the name is not in `KNOWN_PASSTHROUGH_WIDGETS` (line 230). So
the face is `{ kind: 'raw-json', hint: 'object-fields' }`, which is
`RawJsonEditor` (line 3218). The editor shows `JSON.stringify(value,
null, 2)`. Each edit runs `JSON.parse` and passes the parsed value up.
Text that does not parse shows `Invalid JSON` and passes nothing, and
empty text passes `undefined`.
- **`transformPipeline`, the enum array:** `inferWidget` (line 510)
answers `multiselect`, a registered widget, so `MultiSelectWidget`
renders it (`widgets.tsx` line 1396). Its options are `items.enum`.
`toggle()` (lines 1428 to 1443) rebuilds the selection as `options.map(o
=> o.value).filter(v => set.has(v))`: declared enum order, duplicates
collapsed, and an empty selection passed up as `undefined`.
- `format` and `fallbackFormat` are selects over `enum`. `strict` and
`retryOnValidationFailure` are switches, and `maxRetries` is a number
input.
**Round-trip verdicts** ("carries" means view it, edit it and save it
back unchanged):
- **`schema`: carries.** The stored record is shown whole as JSON. An
untouched value is never re-emitted, so it saves back byte-equal. An
edited value saves exactly the JSON that was typed. The parse still
refuses an untyped subschema at its path, and that refusal is unchanged.
- **`transformPipeline`: carries the step set.** An untouched value
saves back unchanged. A toggle writes the steps in declared enum order
(`trim`, `parse_json`, `validate`) with duplicates collapsed, so this
control cannot author another order. Every pipeline written in this repo
is already in enum order (`git grep transformPipeline`: the agent tests
and the conversion fixtures). This repo cannot read whether the cloud
runtime honours another order, so this PR makes no claim about it.
- **Precedent, the same slot:** `action.ai.outputSchema` comes from the
same `aiJsonSchemaSlot` factory. It is served with an identical node and
is already offered through the action form's `ai` row, where it reaches
the same raw-JSON face. Offering `structuredOutput` adds no new face.
## Verification
All runs are on this branch. Gates and suites are at the merged head
`5870ff91d7` (`origin/main` `69a12a0952` merged in, with a full `turbo
run build --filter='./packages/**'` of 71 tasks first).
**Ablation of the pin.** Two legs, both written with
`scripts/ablation-replace.mjs` (anchor hit, then blob restored equal to
`HEAD` and `git diff HEAD` empty), run at `77dedb2c86`. The suite was
`pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2
src/system/metadata-form-zod-reconciliation.test.ts`, which imports the
form registry from source, so no build sits between the edit and the
test.
- **Leg A**, the offer removed with the row still deleted: red, 1 failed
and 75 passed, `agent.(root): accepted by the Zod but unauthorable in
the form … expected [ 'structuredOutput' ] to deeply equal []`.
- **Leg B**, the offer kept with the stale row re-inserted: red, 1
failed and 75 passed, `agent.(root).structuredOutput: the form offers it
now — drop the ledger entry`. The first attempt at leg B was a no-op:
the tool refused it because the replacement contained its own anchor, so
no test ran. The retry used an anchor the replacement does not contain.
- Restored, both files' blobs equal `HEAD`: `f37667ab25a0` (form) and
`2350eaed10be` (test).
**Suites:**
| suite | result |
|---|---|
| `pnpm --filter @objectstack/platform-objects test` | `Test Files 59
passed (59)` · `Tests 949 passed (949)` |
| `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2`
(whole package) | `Test Files 647 passed (647)` · `Tests 18407 passed \|
1 todo (18408)` |
| `pnpm --filter @objectstack/spec --filter
@objectstack/platform-objects run typecheck` | both `Done` |
| `pnpm --filter @objectstack/spec check:generated` | `✓ All 15
generated artifacts are up to date` |
**Gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 87 commands against merge
base `69a12a095`, with no stale-tree warning. Each was run, and its exit
code was recorded before any pipe. `--ran` reconciliation: `87 derived,
87 run, 0 NOT-MEASURED, 0 UNRUN`. This includes `pnpm check:i18n`
(`platform-objects in sync (11 bundle(s))`), `pnpm check:doc-authoring`,
`pnpm check:nul-bytes`, the changeset gates
(`check-adr-0087-registration`: no declared-breaking changeset;
`check-changeset-no-major`: no `major`) and `pnpm check:i18n-coverage`
(`621 baselined untranslated string(s), none new`, run in addition to
the derived list).
**Semver (A5):** `Clause-②: no`. `AgentSchema`'s accept set does not
move, and `check:authorable-surface` and `check:api-surface` are green
with no regeneration. The form definition ships in `@objectstack/spec`'s
`dist`, and the catalogs ship in `@objectstack/platform-objects`'s
`dist`. Both new leaves were found in the built `platform-objects`
`dist`. So both packages publish a change, and this is a changeset, not
`skip-changeset`.
## Acceptance notes
Observations only. None meets a filing class here.
- **The `schema` child's hint.** It shows objectui's announced fallback
line, `widget object-fields — falling back to JSON until a custom
renderer is registered.` The action form's `ai.outputSchema` shows the
same line today. Polish on the objectui side.
- **English sub-row labels.** The seven sub-row labels come from the
served schema (`prettify(name)`, help text from each `describe()`), so
they read in English in every locale. `memory`'s and `guardrails`'
sub-rows read the same way: a schema-derived composite has no catalog
key for its children.
- **Stale neighbouring help texts in `agent.form.ts`.** `planning` names
"strategy, max iterations, replan" (the schema declares only
`maxIterations`). `memory` names "short-term" (removed, with a refusal
and guidance on the key). Nobody owns them yet, so they are noted, not
filed.
- **Unenforced neighbours are offered.** `memory` and `lifecycle` are
offered as composites while their describes say `[EXPERIMENTAL — not
enforced]`, the reverse of this ledger's not-enforced-yet discipline.
That discipline is a reconciliation-ledger convention, not a published
contract, and this card does not touch them.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 8598614 commit ca0dfb6
8 files changed
Lines changed: 33 additions & 8 deletions
File tree
- .changeset
- packages
- platform-objects/src/apps/translations
- spec/src
- ai
- system
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
Lines changed: 4 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2404 | 2404 | | |
2405 | 2405 | | |
2406 | 2406 | | |
| 2407 | + | |
| 2408 | + | |
| 2409 | + | |
| 2410 | + | |
2407 | 2411 | | |
2408 | 2412 | | |
2409 | 2413 | | |
| |||
Lines changed: 4 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2404 | 2404 | | |
2405 | 2405 | | |
2406 | 2406 | | |
| 2407 | + | |
| 2408 | + | |
| 2409 | + | |
| 2410 | + | |
2407 | 2411 | | |
2408 | 2412 | | |
2409 | 2413 | | |
| |||
Lines changed: 4 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2404 | 2404 | | |
2405 | 2405 | | |
2406 | 2406 | | |
| 2407 | + | |
| 2408 | + | |
| 2409 | + | |
| 2410 | + | |
2407 | 2411 | | |
2408 | 2412 | | |
2409 | 2413 | | |
| |||
Lines changed: 3 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1150 | 1150 | | |
1151 | 1151 | | |
1152 | 1152 | | |
1153 | | - | |
| 1153 | + | |
| 1154 | + | |
| 1155 | + | |
1154 | 1156 | | |
1155 | 1157 | | |
1156 | 1158 | | |
| |||
Lines changed: 4 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2404 | 2404 | | |
2405 | 2405 | | |
2406 | 2406 | | |
| 2407 | + | |
| 2408 | + | |
| 2409 | + | |
| 2410 | + | |
2407 | 2411 | | |
2408 | 2412 | | |
2409 | 2413 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| 45 | + | |
45 | 46 | | |
46 | 47 | | |
47 | 48 | | |
| |||
Lines changed: 0 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
405 | 405 | | |
406 | 406 | | |
407 | 407 | | |
408 | | - | |
409 | | - | |
410 | | - | |
411 | | - | |
412 | | - | |
413 | | - | |
414 | | - | |
415 | 408 | | |
416 | 409 | | |
417 | 410 | | |
| |||
0 commit comments