Skip to content

template-first agent APIs and high-level authoring surfaces#6

Merged
dpsoft merged 16 commits intomainfrom
feat/template-first-agent-api
Apr 28, 2026
Merged

template-first agent APIs and high-level authoring surfaces#6
dpsoft merged 16 commits intomainfrom
feat/template-first-agent-api

Conversation

@dpsoft
Copy link
Copy Markdown
Contributor

@dpsoft dpsoft commented Apr 28, 2026

Problem

The current execution spec surface is too low-level for normal operators. It exposes orchestration internals directly and makes common workflows like template execution, remote offload, and simple team authoring harder than they need to be.

Summary of changes

  • add template-first bridge APIs with checked-in file-backed templates
  • add voidctl template ... commands and interactive /template ... support
  • add batch as the canonical remote-offload API with yolo aliasing
  • add phase-1 team authoring that compiles into the existing swarm/supervision engine
  • add Python, Node, and Go SDK surfaces for templates, executions, and batch/yolo
  • add starter examples, runtime templates, and operator docs
  • add regression coverage for bridge, CLI, batch, template, and team flows

Specs and docs

  • README.md
  • AGENTS.md

Verification

  • cargo fmt --all -- --check
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo test
  • cargo test --features serde
  • RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
  • python3 -m unittest sdks.python.tests.test_client
  • node --test sdks/node/test/client.test.mjs
  • cd sdks/go && GOCACHE=/tmp/go-build go test ./...

Follow-up

  • compute sandbox APIs live on a stacked branch: feat/compute-sandbox-api
  • live runtime integration for the compute contract depends on void-box daemon support

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR raises the authoring/ops surface of void-control by introducing template-first bridge endpoints and corresponding voidctl commands, plus new batch/yolo and team phase-1 flows, and SDKs (Python/Node/Go) to consume these higher-level APIs.

Changes:

  • Add file-backed control templates and bridge routes for listing/getting/dry-run/execute of templates.
  • Add canonical batch remote-offload API (with yolo alias) and phase-1 team authoring that compiles to ExecutionSpec.
  • Add voidctl CLI + interactive console commands, plus SDK scaffolds/tests/examples for Python, Node, and Go.

Reviewed changes

Copilot reviewed 56 out of 57 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/voidctl_execution_cli.rs CLI regression tests for template/batch/yolo/team commands (incl. interactive).
tests/template_api.rs Unit tests for template parsing/loading/compilation.
tests/team_api.rs Unit tests for team parsing/validation and bridge round-trips.
tests/execution_bridge.rs Bridge tests covering new template routes and benchmark template behavior.
tests/batch_api.rs Unit + bridge tests for batch/yolo parsing and compilation.
templates/warm-agent-basic.yaml Checked-in warm-agent control template definition.
templates/single-agent-basic.yaml Checked-in single-agent control template definition.
templates/benchmark-runner-python.yaml Checked-in benchmark control template definition with multi-proposal bindings.
src/templates/schema.rs Template schema structs + validation.
src/templates/mod.rs Template module public API: parse/list/load.
src/templates/compile.rs Template input normalization + binding application into ExecutionSpec.
src/team/schema.rs Team schema structs + validation rules (phase-1 constraints).
src/team/mod.rs Team module exports.
src/team/compile.rs Team spec compilation into ExecutionSpec.
src/lib.rs Expose batch, team, and templates modules under serde feature.
src/bridge.rs Add bridge routes/handlers for templates, batch/yolo, and team.
src/bin/voidctl.rs Add `voidctl template
src/batch/schema.rs Batch/yolo schema structs + validation/normalization.
src/batch/mod.rs Batch module exports.
src/batch/compile.rs Batch spec compilation into ExecutionSpec.
sdks/python/tests/test_client.py Python SDK client tests for templates/executions and batch/yolo.
sdks/python/src/void_control/templates.py Python Templates subclient.
sdks/python/src/void_control/models.py Python SDK models for templates/executions/batch runs.
sdks/python/src/void_control/executions.py Python Executions subclient (get/wait).
sdks/python/src/void_control/client.py Python root client wiring + error decoding.
sdks/python/src/void_control/batch.py Python batch/yolo clients + run waiters.
sdks/python/src/void_control/init.py Python package export surface.
sdks/python/pyproject.toml Python SDK packaging metadata.
sdks/python/examples/template_execute.py Python example for template execute + wait.
sdks/python/examples/batch_run.py Python example for batch/yolo run + wait.
sdks/python/README.md Python SDK usage docs.
sdks/node/test/client.test.mjs Node SDK tests for templates/executions and batch/yolo.
sdks/node/src/templates.js Node Templates subclient.
sdks/node/src/models.js Node model mapping helpers + error type.
sdks/node/src/index.js Node SDK entry export.
sdks/node/src/executions.js Node Executions subclient (get/wait).
sdks/node/src/client.js Node root client wiring + error decoding.
sdks/node/src/batch.js Node batch/yolo clients + run waiters.
sdks/node/examples/templateExecute.mjs Node example for template execute + wait.
sdks/node/examples/batchRun.mjs Node example for batch/yolo run + wait.
sdks/node/README.md Node SDK usage docs.
sdks/go/templates.go Go Templates subclient.
sdks/go/models.go Go SDK models for templates/executions/batch runs.
sdks/go/go.mod Go SDK module definition.
sdks/go/executions.go Go Executions subclient (get/wait).
sdks/go/examples/template_execute/main.go Go example for template execute + wait.
sdks/go/examples/batch_run/main.go Go example for batch/yolo run + wait.
sdks/go/client_test.go Go SDK integration tests via mocked HTTP transport.
sdks/go/client.go Go root client wiring + error decoding.
sdks/go/batch.go Go batch/yolo clients + run waiters.
sdks/go/README.md Go SDK usage docs.
examples/team/rust_article_team.yaml Example team spec.
examples/runtime-templates/warm_agent_basic.yaml Runtime worker template referenced by templates/batch/team.
examples/batch/background_repo_work.yaml Example batch spec.
README.md Operator docs for template/batch/yolo/team surfaces and CLI usage.
AGENTS.md Repository/module map updates documenting new surfaces.
.gitignore Ignore Python bytecode artifacts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/templates/mod.rs
Comment on lines +51 to +54
pub fn load_template(id: &str) -> Result<ControlTemplate, TemplateValidationError> {
let path = template_dir().join(format!("{id}.yaml"));
load_template_from_path(&path)
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

load_template joins template_id directly into a filesystem path (templates/{id}.yaml). Because template_id ultimately comes from the HTTP path, this allows path traversal (e.g. ../..) and reading arbitrary files outside templates/. Please validate id (reject path separators / .. components, or enforce an allowlist regex) before constructing the path.

Copilot uses AI. Check for mistakes.
Comment thread src/bridge.rs
Comment on lines +876 to +883
Err(err) if err.to_string().contains("No such file or directory") => json_response(
404,
&ApiError {
code: "NOT_FOUND",
message: format!("template '{}' not found", template_id),
retryable: false,
},
),
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

handle_template_get detects missing templates by checking whether the error string contains "No such file or directory". This is brittle (OS-/locale-dependent) and may misclassify other I/O errors. Consider preserving the underlying std::io::ErrorKind from the loader (e.g., return a structured error enum) so you can reliably map NotFound to HTTP 404.

Copilot uses AI. Check for mistakes.
Comment thread src/bridge.rs
Comment on lines +985 to +1005
let template = templates::load_template(template_id).map_err(|err| {
if err.to_string().contains("No such file or directory") {
json_response(
404,
&ApiError {
code: "NOT_FOUND",
message: format!("template '{}' not found", template_id),
retryable: false,
},
)
} else {
json_response(
400,
&ApiError {
code: "INVALID_TEMPLATE",
message: err.to_string(),
retryable: false,
},
)
}
})?;
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

compile_template_request also relies on string matching ("No such file or directory") to return 404 for missing templates. This has the same brittleness as the GET handler and can produce incorrect status codes on different platforms. Please switch to a structured not-found signal from templates::load_template (or validate the id and check std::fs::metadata/ErrorKind::NotFound).

Copilot uses AI. Check for mistakes.
Comment thread src/bridge.rs
Comment on lines +794 to +821
#[cfg(feature = "serde")]
fn parse_submitted_batch_spec(body: &str) -> Result<batch::BatchSpec, JsonHttpResponse> {
let trimmed = body.trim_start();
let parsed = if trimmed.starts_with('{') || trimmed.starts_with('[') {
batch::parse_batch_json(body)
} else {
batch::parse_batch_yaml(body)
};
parsed.map_err(|err| {
json_response(
400,
&ApiError {
code: "INVALID_BATCH",
message: err.to_string(),
retryable: false,
},
)
})
}

#[cfg(feature = "serde")]
fn parse_submitted_team_spec(body: &str) -> Result<team::TeamSpec, JsonHttpResponse> {
let trimmed = body.trim_start();
let parsed = if trimmed.starts_with('{') || trimmed.starts_with('[') {
team::parse_team_json(body)
} else {
team::parse_team_yaml(body)
};
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

parse_submitted_batch_spec/parse_submitted_team_spec decide JSON vs YAML by looking at the first non-whitespace character. YAML supports flow-style documents that can start with { or [; those would be misrouted to the JSON parser and fail without a YAML fallback. The existing execution parsing (parse_execution_spec_request) tries JSON first and then falls back to YAML; mirroring that approach here would make the behavior more robust and consistent.

Copilot uses AI. Check for mistakes.
Comment thread src/templates/compile.rs
Comment on lines +132 to +140
"integer" => {
let Some(number) = value.as_i64().or_else(|| value.as_u64().map(|n| n as i64)) else {
return Err(TemplateValidationError::new(format!(
"input '{}' must be an integer",
name
)));
};
validate_numeric_range(name, field, number as f64)?;
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

In integer input validation, as_u64() values are cast to i64 with as, which will wrap for values > i64::MAX (silently turning a large positive into a negative). Please reject out-of-range u64 values (or validate using i128/u64 directly) before casting so range checks and bindings can't be bypassed/incorrect.

Copilot uses AI. Check for mistakes.
Comment thread sdks/go/models.go
Comment on lines +66 to +68
BestCandidateID string `json:"best_candidate_id"`
CompletedIterations int `json:"completed_iterations"`
TotalCandidateFailures int `json:"total_candidate_failures"`
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

ExecutionResult.BestCandidateID is typed as string, but the bridge API returns best_candidate_id: null while runs are pending. JSON unmarshalling will fail on null into a string, breaking Executions.Get/Wait (and also BatchRunDetail). Use *string (or sql.NullString-style wrapper) for BestCandidateID so null is handled correctly.

Suggested change
BestCandidateID string `json:"best_candidate_id"`
CompletedIterations int `json:"completed_iterations"`
TotalCandidateFailures int `json:"total_candidate_failures"`
BestCandidateID *string `json:"best_candidate_id"`
CompletedIterations int `json:"completed_iterations"`
TotalCandidateFailures int `json:"total_candidate_failures"`

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +31
fn temp_inputs_path(name: &str) -> PathBuf {
let mut path = std::env::temp_dir();
path.push(format!("voidctl-test-{}-{name}", std::process::id()));
path
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

The tests write input files to a deterministic path in the OS temp dir based only on PID (and a caller-provided name). This can collide under parallel test execution, and it can leave temp files behind on failure. Prefer a unique temp dir/file helper (e.g., tempfile::tempdir / NamedTempFile) and/or ensure cleanup at the end of each test.

Copilot uses AI. Check for mistakes.
@dpsoft dpsoft merged commit ad55748 into main Apr 28, 2026
10 checks passed
@dpsoft dpsoft deleted the feat/template-first-agent-api branch April 28, 2026 00:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants