Skip to content

feat(server,sdk, ui): Control Templates#158

Merged
lan17 merged 45 commits intomainfrom
lev/controltemplates
Apr 7, 2026
Merged

feat(server,sdk, ui): Control Templates#158
lan17 merged 45 commits intomainfrom
lev/controltemplates

Conversation

@lan17
Copy link
Copy Markdown
Contributor

@lan17 lan17 commented Apr 1, 2026

What this does

Today, creating or updating a control requires hand-editing the full control JSON — condition trees, evaluator configs, selector paths, and all. Templates let callers define a reusable control shape with named, typed parameters, and then create or update controls by filling in just the parameter values.

For example, instead of constructing an entire regex-denial control from scratch, a caller can submit a template with a pattern parameter and a step_name parameter, provide values for those parameters, and get a fully rendered, validated control back.

Template controls can also be created without parameter values (unrendered templates). This supports a "set up now, configure later" workflow — the template is attached to an agent but excluded from evaluation until values are provided.

RFC and implementation plan: https://gist.github.com/lan17/ea9aaca990c9bcbfda6595469f3e76c5

How it works

Templates use a render-before-save design. The caller sends a TemplateDefinition (parameter schema + a definition_template with {"$param": "..."} placeholders) and template_values. The server substitutes the values, validates the result as an ordinary ControlDefinition, and stores both the rendered control and the template metadata in the same controls.data JSONB column. No schema migration needed.

                         ┌─────────────────────┐
  TemplateDefinition ──▶ │  Server-side render  │ ──▶ ControlDefinition
  + parameter values     │  (validate, coerce,  │     (concrete, stored
                         │   substitute $param) │      in controls.data)
                         └─────────────────────┘
                                                        │
                                                        ▼
                                                 ┌──────────────┐
                                                 │   Evaluation  │
                                                 │    engine     │
                                                 │ (reads only   │
                                                 │  concrete     │
                                                 │  fields)      │
                                                 └──────────────┘

A template control exists in one of two states:

  • Rendered — parameter values are complete, the template has been rendered into a concrete ControlDefinition, and the control is ready for evaluation (once enabled).
  • Unrendered — the template definition is stored, but parameter values are missing or incomplete. The control is visible and attachable to agents, but forced enabled: false and excluded from evaluation.

The evaluation engine never sees template metadata or unrendered templates. Rendered template controls use ControlDefinitionRuntime with extra="ignore" to skip template fields. Unrendered templates are filtered from runtime queries via data ? 'condition'.

Key design decisions:

  • No new CRUD endpoints for controls. Existing create (PUT /controls) and update (PUT /controls/{id}/data) endpoints detect template payloads via a ControlDefinition | TemplateControlInput union and render transparently. One new endpoint (POST /control-templates/render) provides stateless previews.
  • Unrendered templates are first-class. Creating a template control with empty template_values stores an unrendered template (enabled: false). The server validates template structure (parameter references, forbidden fields, agent-scoped evaluators) but skips rendering. Partial values are type-checked on create.
  • enabled and name stay outside the template. Templates cannot set or bind these fields. enabled is managed via PATCH and preserved across template updates. Enabling an unrendered template is rejected with 422.
  • Template-backed controls cannot be converted back to raw controls in v1. PUT /data with a raw ControlDefinition on a template-backed control returns 409.
  • Validation errors map back to template parameters. When a rendered control fails validation, the server traces the error back through a reverse path map to the originating $param binding.
  • GET responses use a union type. GET /controls/{id}/data returns ControlDefinition for rendered controls or UnrenderedTemplateControl for unrendered templates.
  • List filters exclude unrendered templates when filtering by rendered-only fields (execution, step_type, stage, tag). Unrendered templates appear in unfiltered listings and the template_backed filter.
  • SDK evaluation skips unrendered templates in check_evaluation_with_local to avoid triggering server-call fallbacks.

Reviewer guide

Start here — these tests show the full lifecycle:

  1. test_render_control_template_preview_returns_rendered_control — preview a template without persisting
  2. test_create_template_backed_control_persists_template_metadata — create rendered and verify stored state
  3. test_create_unrendered_template_control_without_values — create without values and verify unrendered state
  4. test_update_unrendered_template_with_complete_values_renders — provide values to render an unrendered template
  5. test_template_backed_control_evaluates_after_policy_attachment — attach to agent and verify evaluation
  6. test_unrendered_template_excluded_from_evaluation — verify unrendered templates don't affect evaluation

Then follow by layer:

Layer Key files What to look for
Shared models models/.../controls.py Template types, UnrenderedTemplateControl, _ConditionBackedControlMixin, ControlDefinition extension, ControlDefinitionRuntime
Payload discrimination models/.../server.py _parse_control_input — discriminates raw vs template payloads, rejects mixed payloads. Response unions for GetControlResponse, GetControlDataResponse
Rendering service server/.../services/control_templates.py can_render_template, validate_template_structure, validate_partial_template_values, render_template_control_input, reverse path map, error remapping
Endpoints server/.../endpoints/controls.py _materialize_control_input (rendered vs unrendered branching), PATCH handler (enable guard), list filters, _parse_stored_control_data union
Runtime split server/.../services/controls.py, engine/.../core.py ControlDefinitionRuntime wired into evaluation, unrendered templates skipped in runtime and agent-controls queries
Python SDK sdks/python/.../controls.py, .../evaluation.py to_template_control_input() handles both rendered and unrendered shapes. check_evaluation_with_local skips unrendered templates

V1 limitations

  • No agent-scoped evaluators in templates — rejected during both structural validation and rendering
  • No in-place template-to-raw conversion — delete and recreate to convert
  • No $param escaping — the $param key is reserved in all template JSON values
  • No string interpolation$param replaces the entire JSON value, not a substring
  • No template catalogs — callers supply the template definition on each request
  • Last-write-wins concurrency — no optimistic locking in v1
  • Read/write asymmetry — GET returns rendered fields + template metadata, but PUT expects TemplateControlInput only (no rendered fields). Use to_template_control_input() SDK helper to reshape.

Validation

  • make check (lint + typecheck + all tests)
  • make sdk-ts-generate + make sdk-ts-name-check + make sdk-ts-typecheck + make sdk-ts-build

@lan17 lan17 changed the title feat: add control template support feat(server): Control Temlates Apr 1, 2026
@lan17 lan17 changed the title feat(server): Control Temlates feat: add control template support Apr 1, 2026
@lan17 lan17 changed the title feat: add control template support feat(server,sdk): add control template support Apr 1, 2026
@lan17 lan17 marked this pull request as ready for review April 1, 2026 07:08
@lan17 lan17 changed the title feat(server,sdk): add control template support feat(server,sdk): Control Templates Apr 1, 2026
lan17 added 9 commits April 1, 2026 00:19
Template controls can now be created without providing parameter values.
The server validates the template structure but skips rendering, storing
the template metadata with enabled=false. Unrendered controls are visible
in listings, attachable to agents, but excluded from evaluation.

- Add UnrenderedTemplateControl model for GET response union
- Create endpoint branches: complete values render, incomplete store unrendered
- Update endpoint supports unrendered→rendered transition when values provided
- GET endpoints discriminate via condition key presence in stored JSONB
- PATCH rejects enabling unrendered templates with 422
- ControlSummary gains template_rendered field
- Runtime evaluation query skips unrendered templates
- Agent policy validation skips unrendered templates
- 10 new server tests covering the full unrendered lifecycle
- Prevent rendered→unrendered downgrade: updating a rendered template
  control with incomplete values now forces a full render attempt,
  returning a clear error about missing parameters instead of silently
  stripping rendered fields
- Deepen unrendered structural validation: validate_template_structure
  now walks definition_template to check $param bindings, reject
  undefined parameter references, detect unused parameters, and reject
  hardcoded agent-scoped evaluator names
- Fix PATCH enabled=false on unrendered templates: detect unrendered
  state before attempting ControlDefinition.model_validate, treating
  disable as a no-op instead of raising CORRUPTED_DATA
- Add 4 behavioral tests: rendered rejects incomplete update, PATCH
  disable no-op, unrendered rejects undefined $param / unused param /
  agent-scoped evaluator
- Reject optional params without defaults in unrendered structural
  validation (catches templates that can never render at creation time)
- Fix PATCH rename-only on unrendered templates: detect unrendered state
  before ControlDefinition.model_validate to avoid false CORRUPTED_DATA
- Export UnrenderedTemplateControl from Python SDK
- Strengthen rename test to verify enabled=false in response
- Add test for optional-param-without-default rejection on unrendered create
- Reject unknown keys and wrong-typed values in partial template_values
  on unrendered create (fail fast instead of persisting garbage)
- Deduplicate structural validation: render_template_control_input now
  calls validate_template_structure instead of inlining the same checks
- Fix description fallback in list endpoint: unrendered templates show
  template.description when top-level description is absent
- Fix _reject_hardcoded_agent_scoped_evaluators to report actual
  condition path instead of hardcoded "condition.evaluator.name"
- Fix PATCH error message indentation
- Move UnrenderedTemplateControl import to module level in controls service
- Add tests: unknown value key rejection, wrong-type value rejection,
  description fallback in list
- Wrap UnrenderedTemplateControl.model_validate in _parse_stored_control_data
  with proper error handling (returns 422 CORRUPTED_DATA instead of 500)
- Wrap unrendered parse in list_controls_for_agent with try/except to skip
  corrupted rows instead of crashing the entire listing
- Remove redundant unused-parameter check in render_template_control_input
  (already caught by validate_template_structure called at the top)
…elper

- Skip unrendered template controls in check_evaluation_with_local so
  they don't trigger the server-call fallback (prevents hot-path latency
  regression for agents with attached unrendered templates)
- Accept UnrenderedTemplateControl in to_template_control_input so
  callers can round-trip unrendered template data from GET endpoints
- Add test: unrendered template does not trigger server fallback
- Add test: to_template_control_input accepts unrendered template data
- Make /controls/validate mirror create: incomplete template values
  validate structure only (returns 200) instead of forcing a full render
  that rejects missing params. Use the render preview endpoint to check
  renderability.
- Exclude unrendered templates from list filters that reference
  rendered-only fields (execution, step_type, stage, tag). Unrendered
  templates still appear in unfiltered listings and template_backed
  filter.
- Update validate test to expect 200 for incomplete values
- Add test: validate rejects structurally invalid unrendered templates
- Add test: unrendered templates excluded from rendered-field filters
  but included in unfiltered listings
@lan17 lan17 enabled auto-merge (squash) April 3, 2026 22:29
Copy link
Copy Markdown
Contributor

@namrataghadi-galileo namrataghadi-galileo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let wait for a final Go from @yashsheth46

@lan17 lan17 disabled auto-merge April 3, 2026 22:35
lan17 added 2 commits April 4, 2026 12:40
# Conflicts:
#	sdks/python/src/agent_control/evaluation.py
#	server/src/agent_control_server/endpoints/evaluation.py
## What this does

Adds UI support for creating and editing template-backed controls. This
PR targets the `lev/controltemplates` branch which implements the
server-side template rendering and API changes.

video:  


https://github.com/user-attachments/assets/f4d70e1c-9961-4140-ac56-3da4448853ac



## How it works

**Creating template-backed controls:** Users paste a
`TemplateControlInput` JSON via the existing "From JSON" flow. The
server discriminates template payloads from raw payloads and handles
rendering transparently — no separate UI flow needed.

**Editing template-backed controls:** When a user opens a
template-backed control, the edit modal detects template metadata and
shows a dedicated editor with:
- **Parameters mode** (default): auto-generated form inputs driven by
the template's parameter definitions, with a collapsible rendered
preview panel
- **Full JSON mode**: editable textarea with the full
`TemplateControlInput` JSON for power users
- A read-only left panel showing the rendered control's description,
action, execution, and template info

**Controls list:** Template-backed controls show a "Template" badge. The
enable/disable toggle uses PATCH (not PUT /data) to avoid the 409 that
the server returns when sending a raw `ControlDefinition` to a
template-backed control.

## New components

| Component | What it does |
|---|---|
| `TemplateParamForm` | Auto-generates form inputs from template
parameter definitions (string→TextInput, string_list→TagsInput,
enum→Select, boolean→Switch, regex_re2→monospace TextInput) |
| `TemplatePreview` | Debounced render preview that calls `POST
/control-templates/render` and strips `template`/`template_values` from
the output so users see what the engine sees |
| `TemplateEditContent` | Template-backed edit modal with
Parameters/Full JSON toggle, read-only summary panel, and save via
`TemplateControlInput` payload |
| `useRenderTemplate` | Mutation hook wrapping the render preview API
call |

## Test plan

13 Playwright tests covering:
- Template badge display and absence for raw controls
- Parameter form rendering with fields from template definition
- Parameters ↔ Full JSON mode switching
- Preview panel strips template metadata from rendered output
- Read-only summary shows rendered control info
- Control name pre-fill and editing
- Raw control editing regression (standard form, no template UI)
- From JSON creation flow still works

All 123 tests pass (110 existing + 13 new), 0 regressions.

## Notes

- Template types are defined manually in `types.ts` until `npm run
fetch-api-types` is run against the updated server. They can be replaced
with generated types after.
- The `CreateFromTemplate` modal component is included but not wired up
(we decided to use "From JSON" instead of a separate creation flow). It
can be removed or repurposed later.
@lan17 lan17 changed the title feat(server,sdk): Control Templates feat(server,sdk, ui): Control Templates Apr 7, 2026
lan17 added 13 commits April 6, 2026 20:54
# Conflicts:
#	ui/src/core/page-components/agent-detail/agent-detail.tsx
#	ui/src/core/page-components/agent-detail/modals/edit-control/edit-control-content.tsx
Replace the plain <Textarea> in template-edit-content with the Monaco-based
JsonEditorMonaco component, adding a new 'template' editor mode that
understands the template JSON envelope structure.

Smart features now available when editing templates in Full JSON mode:
- Schema-driven property completions inside definition_template
- Evaluator name and selector path autocomplete
- Auto-edit: evaluator config fill on name change, steering_context toggle
- $param reference completions at value positions in definition_template
- Parameter name suggestions inside {"$param": ""} and template_values keys
- Inline validation via the template render endpoint
- Hover info on properties and $param references
- Code actions (wrap in AND/OR/NOT) on conditions in definition_template
- Empty value hints for evaluator names, selector paths, and $param values

The implementation uses a path-prefix approach: a definitionPrefix field
(['template', 'definition_template']) on the autocomplete context allows
existing location-check functions to work on relative paths without
per-function template awareness.
Fix Prettier formatting issues in the template editor language support
and editor view files. Regenerate the TypeScript SDK client to reflect
updated list-controls endpoint defaults (all states by default).
Use inline enum definitions in the operations file instead of extracted
model files, matching what CI's Speakeasy generation produces. Updates
default filter values from 'rendered'/'enabled' to 'all'/'all'.
The template editor now uses JsonEditorMonaco instead of a plain
Textarea, so the test needs to read the value via the Monaco test
bridge (getJsonEditorValue) instead of inputValue().
@lan17 lan17 merged commit 78bb538 into main Apr 7, 2026
6 checks passed
@lan17 lan17 deleted the lev/controltemplates branch April 7, 2026 17:49
galileo-automation pushed a commit that referenced this pull request Apr 7, 2026
## [2.2.0](ts-sdk-v2.1.0...ts-sdk-v2.2.0) (2026-04-07)

### Features

* **evaluators:** add starts_with/ends_with mode to list evaluator ([#154](#154)) ([bf1f7d7](bf1f7d7))
* **sdk:** [Enterprise Integration]: Add provider agnostic traceing ([#145](#145)) ([f1ca27c](f1ca27c))
* **sdk:** Add telemetry package to support sinks ([#164](#164)) ([2186ba1](2186ba1))
* **sdk:** default merge events in SDK ([#155](#155)) ([5984a60](5984a60))
* **server,sdk, ui:** Control Templates ([#158](#158)) ([78bb538](78bb538))
* **server:** Override PG password in dockerfile ([#148](#148)) ([5d70c7d](5d70c7d))
* **server:** Remove container name for dev postgres ([92b2d13](92b2d13))
* **server:** Start local dev pg under docker compose project endign with dev ([88bee63](88bee63))
* **ui, server:** Intuitive JSON editing for Controls ([#151](#151)) ([8c23cef](8c23cef))
* **ui:** add full control JSON editing and create-from-JSON ([#147](#147)) ([e685ed0](e685ed0))

### Bug Fixes

* **docs:** add explicit shutdown to quickstart example ([#149](#149)) ([b76014f](b76014f))
* **sdk:** use sync shutdown flush fallback ([#150](#150)) ([90265ba](90265ba))
* **server:**  remove unused evaluator config store ([#152](#152)) ([dea2873](dea2873))
* **server:** Omit null fields in control JSON editor ([#157](#157)) ([0aa2f3c](0aa2f3c))
* **server:** Update docker-compose.dev.yml to use different container name ([14d4c87](14d4c87))
* **ui:** improve edit control ux, no layout shift, consistent spacing ([#122](#122)) ([76d67b9](76d67b9))
@galileo-automation
Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 2.2.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants