Skip to content

Commit 82ac1af

Browse files
initcronclaude
andcommitted
feat: Wire output schema into CLI and runtime execution
Complete CLI to runtime integration for structured I/O: CLI Layer: - Parse --output-schema flag (pre-built names or inline JSON) - Parse --output-schema-file flag (JSON schema from file) - Pass parsed schema to run_agent() function Runtime Integration: - Import AgentContext for schema attachment - Create context with schema using .with_output_schema() - Execute using runtime.execute_with_context() when schema present - Fallback to standard execute() when no schema Supported Schema Names: - container-list: Docker/K8s container listings - resource-stats: CPU/memory statistics - simple-list: Generic item lists - key-value: Key-value data Example Usage: aofctl run agent docker.yaml --output-schema container-list aofctl run agent k8s.yaml --output-schema-file custom.json aofctl run agent stats.yaml --output-schema resource-stats The schema enhances the system prompt with JSON schema instructions, ensuring the LLM produces consistently structured output. Related: #74 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a5a305b commit 82ac1af

3 files changed

Lines changed: 99 additions & 11 deletions

File tree

crates/aof-runtime/src/executor/agent_executor.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,7 @@ impl AgentExecutor {
663663
warn!("[BUILD_REQUEST] Converted {} messages", messages.len());
664664

665665
// Get tool definitions if available
666-
let tools: Vec<ModelToolDefinition> = if let Some(executor) = &self.tool_executor {
666+
let mut tools: Vec<ModelToolDefinition> = if let Some(executor) = &self.tool_executor {
667667
warn!("[BUILD_REQUEST] Tool executor available, listing tools...");
668668
let tool_defs = executor.list_tools();
669669
warn!("[BUILD_REQUEST] Got {} tool definitions from executor", tool_defs.len());
@@ -683,15 +683,28 @@ impl AgentExecutor {
683683
Vec::new()
684684
};
685685

686-
warn!("[BUILD_REQUEST] Final: messages={}, tools={}, system_prompt={:?}",
686+
// Enhance system prompt with output schema instructions if schema is present
687+
let system_prompt = if let Some(schema) = &context.output_schema {
688+
warn!("[BUILD_REQUEST] Output schema present, adding structured output instructions");
689+
690+
let base_prompt = self.config.system_prompt.as_deref().unwrap_or("");
691+
let schema_instructions = schema.to_system_instructions();
692+
693+
Some(format!("{}\n\n{}", base_prompt, schema_instructions))
694+
} else {
695+
self.config.system_prompt.clone()
696+
};
697+
698+
warn!("[BUILD_REQUEST] Final: messages={}, tools={}, system_prompt={:?}, has_schema={}",
687699
messages.len(),
688700
tools.len(),
689-
self.config.system_prompt.as_ref().map(|s| s.len())
701+
system_prompt.as_ref().map(|s| s.len()),
702+
context.output_schema.is_some()
690703
);
691704

692705
Ok(ModelRequest {
693706
messages,
694-
system: self.config.system_prompt.clone(),
707+
system: system_prompt,
695708
tools,
696709
temperature: Some(self.config.temperature),
697710
max_tokens: self.config.max_tokens,

crates/aofctl/src/cli.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ pub enum Commands {
4343
/// Output format (json, yaml, text)
4444
#[arg(short, long, default_value = "text")]
4545
output: String,
46+
47+
/// Output schema for structured responses
48+
/// Use pre-built schema names (container-list, resource-stats, simple-list, key-value)
49+
/// or provide inline JSON schema
50+
#[arg(long)]
51+
output_schema: Option<String>,
52+
53+
/// Path to JSON schema file for output validation
54+
#[arg(long, conflicts_with = "output_schema")]
55+
output_schema_file: Option<String>,
4656
},
4757

4858
/// Get resources (verb-first: get agents, get agent <name>)
@@ -219,12 +229,16 @@ impl Cli {
219229
name_or_config,
220230
input,
221231
output,
232+
output_schema,
233+
output_schema_file,
222234
} => {
223235
commands::run::execute(
224236
&resource_type,
225237
&name_or_config,
226238
input.as_deref(),
227239
&output,
240+
output_schema.as_deref(),
241+
output_schema_file.as_deref(),
228242
context.as_ref(),
229243
)
230244
.await

crates/aofctl/src/commands/run.rs

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use anyhow::{Context as AnyhowContext, Result, anyhow};
2-
use aof_core::{AgentConfig, Context as AofContext};
2+
use aof_core::{AgentConfig, AgentContext, Context as AofContext, OutputSchema};
33
use aof_runtime::Runtime;
44
use std::fs;
55
use std::io::{self, IsTerminal, Write};
@@ -155,12 +155,53 @@ impl Write for LogWriter {
155155
}
156156
}
157157

158+
/// Parse output schema from CLI arguments
159+
fn parse_output_schema(
160+
schema_name: Option<&str>,
161+
schema_file: Option<&str>,
162+
) -> Result<Option<OutputSchema>> {
163+
use aof_core::schema::schemas;
164+
165+
if let Some(file_path) = schema_file {
166+
// Read schema from file
167+
let content = fs::read_to_string(file_path)
168+
.with_context(|| format!("Failed to read schema file: {}", file_path))?;
169+
let schema_json: serde_json::Value = serde_json::from_str(&content)
170+
.with_context(|| format!("Failed to parse schema file as JSON: {}", file_path))?;
171+
return Ok(Some(OutputSchema::from_json_schema(schema_json)));
172+
}
173+
174+
if let Some(name) = schema_name {
175+
// Check if it's a pre-built schema name
176+
let schema = match name {
177+
"container-list" => schemas::container_list(),
178+
"resource-stats" => schemas::resource_stats(),
179+
"simple-list" => schemas::simple_list(),
180+
"key-value" => schemas::key_value(),
181+
_ => {
182+
// Try to parse as inline JSON schema
183+
let schema_json: serde_json::Value = serde_json::from_str(name)
184+
.with_context(|| format!(
185+
"Invalid schema name '{}'. Expected one of: container-list, resource-stats, simple-list, key-value, or inline JSON schema",
186+
name
187+
))?;
188+
OutputSchema::from_json_schema(schema_json)
189+
}
190+
};
191+
return Ok(Some(schema));
192+
}
193+
194+
Ok(None)
195+
}
196+
158197
/// Execute a resource (agent, workflow, job) with configuration and input
159198
pub async fn execute(
160199
resource_type: &str,
161200
name_or_config: &str,
162201
input: Option<&str>,
163202
output: &str,
203+
output_schema: Option<&str>,
204+
output_schema_file: Option<&str>,
164205
context: Option<&AofContext>,
165206
) -> Result<()> {
166207
// Log context if provided
@@ -178,8 +219,11 @@ pub async fn execute(
178219
let rt = ResourceType::from_str(resource_type)
179220
.ok_or_else(|| anyhow::anyhow!("Unknown resource type: {}", resource_type))?;
180221

222+
// Parse output schema if provided
223+
let schema = parse_output_schema(output_schema, output_schema_file)?;
224+
181225
match rt {
182-
ResourceType::Agent => run_agent(name_or_config, input, output, context).await,
226+
ResourceType::Agent => run_agent(name_or_config, input, output, schema, context).await,
183227
ResourceType::Workflow | ResourceType::Flow => run_workflow(name_or_config, input, output).await,
184228
ResourceType::Fleet => run_fleet(name_or_config, input, output).await,
185229
ResourceType::Job => run_job(name_or_config, input, output).await,
@@ -190,7 +234,13 @@ pub async fn execute(
190234
}
191235

192236
/// Run an agent with configuration
193-
async fn run_agent(config: &str, input: Option<&str>, output: &str, context: Option<&AofContext>) -> Result<()> {
237+
async fn run_agent(
238+
config: &str,
239+
input: Option<&str>,
240+
output: &str,
241+
schema: Option<OutputSchema>,
242+
context: Option<&AofContext>,
243+
) -> Result<()> {
194244
// Check if interactive mode should be enabled (when no input provided and stdin is a TTY)
195245
let interactive = input.is_none() && io::stdin().is_terminal();
196246

@@ -251,10 +301,21 @@ async fn run_agent(config: &str, input: Option<&str>, output: &str, context: Opt
251301
}
252302
});
253303

254-
let result = runtime
255-
.execute(&agent_name, input_str)
256-
.await
257-
.with_context(|| "Failed to execute agent")?;
304+
// Execute with or without schema
305+
let result = if let Some(output_schema) = schema {
306+
// Create context with schema attached
307+
let mut ctx = AgentContext::new(input_str).with_output_schema(output_schema);
308+
runtime
309+
.execute_with_context(&agent_name, &mut ctx)
310+
.await
311+
.with_context(|| "Failed to execute agent with schema")?
312+
} else {
313+
// Standard execution without schema
314+
runtime
315+
.execute(&agent_name, input_str)
316+
.await
317+
.with_context(|| "Failed to execute agent")?
318+
};
258319

259320
// Stop spinner
260321
spinner_handle.abort();

0 commit comments

Comments
 (0)