use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value as JsonValue};
use std::collections::{HashMap, HashSet};
use std::sync::{Mutex, OnceLock};
use crate::ast::{Expr, Stmt};
use crate::builtins::BUILTIN_LOOKUP;
use crate::eval::{eval_expr, eval_program};
use crate::marketplace::{RegistryClient, SearchQuery, SortBy};
use crate::modules::all_modules;
use crate::parser::parse_program;
use crate::value::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum AgentRequest {
Call {
builtin: String,
#[serde(default)]
args: JsonValue,
},
Pipeline {
steps: Vec<PipelineStep>,
#[serde(default)]
input: Option<JsonValue>,
},
Eval { code: String },
Describe { builtin: String },
ListBuiltins {
#[serde(default)]
category: Option<String>,
},
Schema {
#[serde(default)]
format: SchemaFormat,
#[serde(default)]
module: Option<String>,
#[serde(default)]
q: Option<String>,
},
TypeInfo {
#[serde(default)]
type_name: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineStep {
pub builtin: String,
#[serde(default)]
pub args: JsonValue,
#[serde(default)]
pub select: Option<String>,
#[serde(default)]
pub predicate: Option<String>,
}
#[derive(Debug, Clone, Serialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SchemaFormat {
#[default]
JsonSchema,
OpenAI,
Claude,
Gemini,
Llama,
Mistral,
Cohere,
Grok,
DeepSeek,
Bedrock,
AzureOpenAI,
Qwen,
Ollama,
VLLM,
HuggingFace,
OpenRouter,
Kimi,
Yi,
GLM,
Reka,
AI21,
Perplexity,
Together,
Groq,
Fireworks,
Ontology,
JsonLD,
OwlTurtle,
SHACL,
}
impl<'de> Deserialize<'de> for SchemaFormat {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Self, D::Error> {
let name = String::deserialize(deserializer)?;
parse_schema_format(&name).ok_or_else(|| {
serde::de::Error::custom(format!(
"unknown format '{name}'; expected one of: {SCHEMA_FORMATS_HINT}"
))
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(into = "AgentResponseWire", from = "AgentResponseWire")]
pub struct AgentResponse {
pub success: bool,
pub result: Option<JsonValue>,
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct AgentResponseWire {
success: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
result: Option<JsonValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
error: Option<JsonValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
hint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
result_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
metadata: Option<JsonValue>,
}
impl From<AgentResponse> for AgentResponseWire {
fn from(r: AgentResponse) -> Self {
let error = if r.success && r.error.is_none() {
None
} else {
let text = r.error.as_deref().unwrap_or("the request failed");
Some(error_object(text, r.code.as_deref(), r.hint.as_deref()))
};
AgentResponseWire {
success: r.success,
result: r.result,
error,
code: r.code,
hint: r.hint,
result_type: r.result_type,
metadata: r.metadata,
}
}
}
impl From<AgentResponseWire> for AgentResponse {
fn from(w: AgentResponseWire) -> Self {
let (error, code, hint) = match &w.error {
None => (None, w.code.clone(), w.hint.clone()),
Some(JsonValue::String(text)) => (Some(text.clone()), w.code.clone(), w.hint.clone()),
Some(object) => {
let text = object
.get("message")
.and_then(JsonValue::as_str)
.map(str::to_string);
let code = object
.get("code")
.and_then(JsonValue::as_str)
.map(str::to_string)
.or_else(|| w.code.clone());
let hint = object
.get("hint")
.and_then(JsonValue::as_str)
.map(str::to_string)
.or_else(|| w.hint.clone());
(text, code, hint)
}
};
AgentResponse {
success: w.success,
result: w.result,
error,
code,
hint,
result_type: w.result_type,
metadata: w.metadata,
}
}
}
const DEFAULT_ERROR_HINT: &str = "read error.message; error.code names the class of failure, and error.retryable says whether a corrected call can succeed";
fn code_retryable(code: &str) -> bool {
matches!(
code,
"E_UNKNOWN_FIELD" | "E_NOT_FOUND" | "E_BAD_STATE" | "E_NEEDS_APPROVAL" | "E_NETWORK"
)
}
fn same_request_fails_again(code: &str) -> bool {
matches!(
code,
"E_PARSE" | "E_BAD_ARG" | "E_UNKNOWN_BUILTIN" | "E_UNKNOWN_NAME"
)
}
fn error_object(text: &str, code: Option<&str>, hint: Option<&str>) -> JsonValue {
let mut fields = match structured_error_body(text) {
Some((prefix, mut body)) => {
if !prefix.is_empty() {
let inner = body
.get("message")
.and_then(JsonValue::as_str)
.unwrap_or_default()
.to_string();
body.insert("message".to_string(), json!(format!("{prefix}: {inner}")));
}
body
}
None => serde_json::Map::new(),
};
if !fields.contains_key("message") {
fields.insert("message".to_string(), json!(text));
}
let code = fields
.get("code")
.and_then(JsonValue::as_str)
.map(str::to_string)
.or_else(|| code.map(str::to_string))
.unwrap_or_else(|| "E_UNKNOWN".to_string());
let hint = fields
.get("hint")
.and_then(JsonValue::as_str)
.map(str::to_string)
.or_else(|| hint.map(str::to_string))
.unwrap_or_else(|| DEFAULT_ERROR_HINT.to_string());
let retryable = !same_request_fails_again(&code)
&& fields
.get("retryable")
.and_then(JsonValue::as_bool)
.unwrap_or_else(|| code_retryable(&code));
fields.insert("code".to_string(), json!(code));
fields.insert("hint".to_string(), json!(hint));
fields.insert("retryable".to_string(), json!(retryable));
JsonValue::Object(fields)
}
fn structured_error_body(text: &str) -> Option<(String, serde_json::Map<String, JsonValue>)> {
text.match_indices('{').find_map(|(start, _)| {
let value = serde_json::Deserializer::from_str(&text[start..])
.into_iter::<JsonValue>()
.next()?
.ok()?;
let body = value.get("error")?.as_object()?;
body.get("code").and_then(JsonValue::as_str)?;
let prefix = text[..start]
.trim()
.trim_end_matches(':')
.trim()
.to_string();
Some((prefix, body.clone()))
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LanguageOntology {
pub ontology_version: String,
pub language: LanguageInfo,
pub types: Vec<TypeDefinition>,
pub builtins: Vec<BuiltinDefinition>,
pub modules: Vec<ModuleDefinition>,
pub operators: Vec<OperatorDefinition>,
pub syntax: SyntaxPatterns,
pub categories: Vec<CategoryInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub os_ontology: Option<JsonValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cli_tools: Option<JsonValue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LanguageInfo {
pub name: String,
pub version: String,
pub description: String,
pub paradigms: Vec<String>,
pub typing: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TypeDefinition {
pub name: String,
pub description: String,
pub json_equivalent: String,
pub examples: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<Vec<FieldDefinition>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldDefinition {
pub name: String,
#[serde(rename = "type")]
pub field_type: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuiltinDefinition {
pub name: String,
pub description: String,
pub category: String,
pub signature: String,
pub parameters: Vec<ParameterDefinition>,
pub return_type: String,
pub examples: Vec<ExampleDefinition>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aliases: Option<Vec<String>>,
pub json_schema: JsonValue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParameterDefinition {
pub name: String,
#[serde(rename = "type")]
pub param_type: String,
pub description: String,
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExampleDefinition {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
pub code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperatorDefinition {
pub symbol: String,
pub name: String,
pub description: String,
pub precedence: u8,
pub associativity: String,
pub operand_types: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyntaxPatterns {
pub variable_declaration: String,
pub function_definition: String,
pub lambda: String,
pub pipeline: String,
pub record_literal: String,
pub array_literal: String,
pub conditional: String,
pub pattern_matching: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategoryInfo {
pub name: String,
pub description: String,
pub builtin_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleDefinition {
pub name: String,
pub description: String,
pub functions: Vec<ModuleFunctionDef>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleFunctionDef {
pub method: String,
pub builtin: String,
pub syntax: String,
}
pub fn deterministic_mode() -> bool {
std::env::var("IRONSHELL_DETERMINISTIC")
.map(|s| s == "1" || s.eq_ignore_ascii_case("true"))
.unwrap_or(false)
}
const NONDETERMINISTIC_METADATA_KEYS: &[&str] = &[
"execution_time_ms",
"duration_ms",
"elapsed_ms",
"timestamp",
"created_at",
"started_at",
"finished_at",
"request_id",
"id",
];
fn sort_json_keys(v: &mut JsonValue) {
match v {
JsonValue::Object(m) => {
let mut entries: Vec<(String, JsonValue)> = std::mem::take(m).into_iter().collect();
entries.sort_by(|a, b| a.0.cmp(&b.0));
for (_, child) in entries.iter_mut() {
sort_json_keys(child);
}
*m = entries.into_iter().collect();
}
JsonValue::Array(items) => items.iter_mut().for_each(sort_json_keys),
_ => {}
}
}
fn deterministic_scrub(resp: &mut AgentResponse) {
if let Some(result) = resp.result.as_mut() {
sort_json_keys(result);
}
if let Some(JsonValue::Object(meta)) = resp.metadata.as_mut() {
for key in NONDETERMINISTIC_METADATA_KEYS {
meta.remove(*key);
}
}
}
pub fn process_request(request: &AgentRequest) -> AgentResponse {
let mut resp = dispatch_request(request);
annotate_error_fields(&mut resp);
if deterministic_mode() {
deterministic_scrub(&mut resp);
}
annotate_token_accounting(request, &mut resp);
resp
}
fn dispatch_request(request: &AgentRequest) -> AgentResponse {
match request {
AgentRequest::Call { builtin, args } => execute_call(builtin, args),
AgentRequest::Pipeline { steps, input } => execute_pipeline(steps, input.as_ref()),
AgentRequest::Eval { code } => execute_eval(code),
AgentRequest::Describe { builtin } => describe_builtin(builtin),
AgentRequest::ListBuiltins { category } => list_builtins(category.as_deref()),
AgentRequest::Schema { format, module, q } => {
get_schema(format, module.as_deref(), q.as_deref())
}
AgentRequest::TypeInfo { type_name } => get_type_info(type_name.as_deref()),
}
}
fn annotate_token_accounting(request: &AgentRequest, resp: &mut AgentResponse) {
let tokens_in =
crate::builtins::est_token_count(&serde_json::to_string(request).unwrap_or_default());
let out_str = match (&resp.result, &resp.error) {
(Some(r), _) => serde_json::to_string(r).unwrap_or_default(),
(None, Some(e)) => e.clone(),
_ => String::new(),
};
let tokens_out = crate::builtins::est_token_count(&out_str);
let acct = json!({
"tokens_in": tokens_in,
"tokens_out": tokens_out,
"tokens_total": tokens_in + tokens_out,
"estimated": true,
});
resp.metadata = Some(match resp.metadata.take() {
Some(JsonValue::Object(mut m)) => {
m.insert("token_accounting".to_string(), acct);
JsonValue::Object(m)
}
Some(other) => json!({ "info": other, "token_accounting": acct }),
None => json!({ "token_accounting": acct }),
});
}
fn annotate_error_fields(resp: &mut AgentResponse) {
if let Some((code, hint)) = resp.error.as_deref().and_then(structured_error_fields) {
resp.code = Some(code);
resp.hint = hint;
}
}
fn structured_error_fields(error: &str) -> Option<(String, Option<String>)> {
error.match_indices('{').find_map(|(start, _)| {
let value = serde_json::Deserializer::from_str(&error[start..])
.into_iter::<JsonValue>()
.next()?
.ok()?;
let body = value.get("error")?;
let code = body.get("code")?.as_str()?.to_string();
let hint = body
.get("hint")
.and_then(JsonValue::as_str)
.map(str::to_string);
Some((code, hint))
})
}
fn execute_call(builtin: &str, args: &JsonValue) -> AgentResponse {
let started = std::time::Instant::now();
let resolved;
let builtin = match builtin.split_once('.') {
None => builtin,
Some((module, member)) => match crate::modules::builtin_for_member(module, member) {
Some(name) => {
resolved = name;
resolved.as_str()
}
None => {
crate::record::note_outcome(
crate::record::Surface::Api,
"",
|_| vec![builtin.to_string()],
Err(crate::safety::ErrorCode::BadArg),
started.elapsed(),
);
return AgentResponse {
code: Some("E_BAD_ARG".to_string()),
hint: Some(format!(
"drop the module prefix and name the bare builtin: {{\"action\":\"call\",\"builtin\":\"{member}\"}}, or describe \"{member}\" first to check it is one"
)),
success: false,
result: None,
error: Some(format!(
"'{builtin}' is not a builtin name or a module member"
)),
result_type: None,
metadata: Some(json!({"code": "E_BAD_ARG"})),
};
}
},
};
if let Some(refusal) = unknown_builtin_refusal(builtin) {
crate::record::note_outcome(
crate::record::Surface::Api,
"",
|_| vec![builtin.to_string()],
Err(crate::safety::ErrorCode::UnknownBuiltin),
started.elapsed(),
);
return refusal;
}
let unwrapped = unwrap_args_envelope(builtin, args);
let args = &unwrapped;
let code = match args_to_code(builtin, args) {
Ok(c) => c,
Err(e) => {
crate::record::note_outcome(
crate::record::Surface::Api,
"",
|_| vec![builtin.to_string()],
Err(crate::safety::ErrorCode::BadArg),
started.elapsed(),
);
return AgentResponse {
code: Some("E_BAD_ARG".to_string()),
hint: Some(
"pass the arguments as a JSON array in parameter order, or as an object keyed by the parameter names describe lists; a /call/:builtin body is that arguments object itself (e.g. {\"path\": \".\"}), not {\"args\": ...}"
.to_string(),
),
success: false,
result: None,
error: Some(format!("Failed to convert arguments: {}", e)),
result_type: None,
metadata: Some(json!({"code": "E_BAD_ARG"})),
};
}
};
execute_eval(&code)
}
fn execute_pipeline(steps: &[PipelineStep], input: Option<&JsonValue>) -> AgentResponse {
match run_pipeline_steps(steps, input, |_| {}) {
Ok((value, code)) => response_from_value(value, &code),
Err(failure) => AgentResponse {
code: None,
hint: None,
success: false,
result: None,
error: Some(failure.message),
result_type: None,
metadata: failure
.code_attempted
.map(|code| json!({ "code_attempted": echo_code(&code) })),
},
}
}
struct PipelineFailure {
step: Option<usize>,
message: String,
code_attempted: Option<String>,
code: crate::safety::ErrorCode,
}
fn pipeline_slot(j: usize) -> String {
format!("__pipeline_stage_{j}")
}
fn single_expression(source: &str) -> Result<Expr, String> {
let mut stmts = parse_program(source).map_err(|e| e.to_string())?;
match (stmts.len(), stmts.pop()) {
(1, Some(Stmt::Expr(expr))) => Ok(expr),
_ => Err(
crate::parser::ParseError::new("a pipeline stage must be a single expression")
.to_string(),
),
}
}
fn run_pipeline_steps(
steps: &[PipelineStep],
input: Option<&JsonValue>,
on_step_done: impl FnMut(usize),
) -> Result<(Value, String), PipelineFailure> {
let started = std::time::Instant::now();
let outcome = run_pipeline_steps_unrecorded(steps, input, on_step_done);
match &outcome {
Ok((_, code)) => crate::record::note_outcome(
crate::record::Surface::Api,
code,
crate::record::heads_of,
Ok(()),
started.elapsed(),
),
Err(failure) => crate::record::note_outcome(
crate::record::Surface::Api,
failure.code_attempted.as_deref().unwrap_or(""),
crate::record::heads_of,
Err(failure.code),
started.elapsed(),
),
}
outcome
}
fn run_pipeline_steps_unrecorded(
steps: &[PipelineStep],
input: Option<&JsonValue>,
mut on_step_done: impl FnMut(usize),
) -> Result<(Value, String), PipelineFailure> {
use crate::safety::ErrorCode;
if steps.is_empty() {
return Err(PipelineFailure {
step: None,
message: "Pipeline must have at least one step".to_string(),
code_attempted: None,
code: ErrorCode::BadArg,
});
}
let mut stage_codes: Vec<String> = Vec::with_capacity(steps.len() + 1);
if let Some(input_val) = input {
let literal = json_to_ae_literal(input_val).map_err(|e| PipelineFailure {
step: None,
message: format!("Failed to build pipeline input: {}", e),
code_attempted: None,
code: ErrorCode::BadArg,
})?;
stage_codes.push(literal);
}
for step in steps {
let code = build_step_code(step).map_err(|e| PipelineFailure {
step: None,
message: format!("Failed to build step '{}': {}", step.builtin, e),
code_attempted: None,
code: ErrorCode::BadArg,
})?;
stage_codes.push(code);
}
let joined = stage_codes.join(" | ");
if let Err(e) = parse_program(&joined) {
return Err(PipelineFailure {
step: None,
message: e.to_string(),
code_attempted: Some(joined),
code: ErrorCode::BadArg,
});
}
let mut stage_exprs = Vec::with_capacity(stage_codes.len());
for (j, code) in stage_codes.iter().enumerate() {
let source = if j == 0 {
code.clone()
} else {
format!("{} | {}", pipeline_slot(j - 1), code)
};
match single_expression(&source) {
Ok(expr) => stage_exprs.push(expr),
Err(message) => {
return Err(PipelineFailure {
step: None,
message,
code_attempted: Some(joined.clone()),
code: ErrorCode::BadArg,
})
}
}
}
let offset = usize::from(input.is_some());
let last_stage = stage_exprs.len() - 1;
let mut env = crate::modules::env_with_modules();
let mut output = Value::Null;
for (j, expr) in stage_exprs.into_iter().enumerate() {
let step = j.checked_sub(offset);
let fail = |message: String, code: ErrorCode| PipelineFailure {
step,
message,
code_attempted: Some(joined.clone()),
code,
};
env.set_input(None);
let value = eval_expr(&expr, &mut env)
.map_err(|e| fail(format!("{}", e), crate::safety::code_of(&e)))?;
if let Some(i) = step {
on_step_done(i);
}
if j < last_stage {
env.set_var(pipeline_slot(j), value)
.map_err(|message| fail(message, ErrorCode::Unknown))?;
} else {
output = value;
}
}
if let Value::Error(msg) = &output {
return Err(PipelineFailure {
step: steps.len().checked_sub(1),
message: format!("uncaught error: {msg}"),
code_attempted: Some(joined),
code: ErrorCode::Unknown,
});
}
Ok((output, joined))
}
pub fn redact_for_agent(value: Value) -> Value {
if crate::safety::current_mode() == crate::safety::Mode::Agent
&& crate::safety::redaction_enabled()
{
crate::builtins::redact_value(&value)
} else {
value
}
}
fn response_from_value(value: Value, code: &str) -> AgentResponse {
let value = redact_for_agent(value);
let result_type = value_type_name(&value);
let paged = !deterministic_mode()
&& crate::safety::current_mode() == crate::safety::Mode::Agent
&& matches!(value, Value::Array(_) | Value::Table(_));
let mut metadata = serde_json::Map::new();
metadata.insert("code_executed".to_string(), json!(echo_code(code)));
let result = if paged {
match tabular_result(&value, &mut metadata) {
Ok(result) => result,
Err(e) => {
return AgentResponse {
code: None,
hint: None,
success: false,
result: None,
error: Some(e.to_string()),
result_type: None,
metadata: Some(JsonValue::Object(metadata)),
}
}
}
} else {
value_to_json(&value)
};
AgentResponse {
code: None,
hint: None,
success: true,
result: Some(result),
error: None,
result_type: Some(result_type),
metadata: Some(JsonValue::Object(metadata)),
}
}
fn echo_code(code: &str) -> String {
let chars = code.chars().count();
if chars < 1000 {
code.to_string()
} else {
let head: String = code.chars().take(200).collect();
format!("{head}... ({chars} chars)")
}
}
const HANDLE_PREVIEW_ROWS: usize = 3;
fn tabular_result(
value: &Value,
metadata: &mut serde_json::Map<String, JsonValue>,
) -> Result<JsonValue> {
let full = value_to_json(value);
let total = table_rows(&full).len();
let budget = std::env::var("IRONSHELL_TOKEN_BUDGET")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.filter(|m| *m > 0);
if let Some(max) = budget {
let shown = rows_within_budget(&full, max);
if shown < total {
metadata.insert("total".to_string(), json!(total));
metadata.insert("shown".to_string(), json!(shown));
metadata.insert("omitted".to_string(), json!(total - shown));
}
return Ok(first_rows(&full, shown));
}
let full_bytes = full.to_string().len();
if crate::handles::worth_handling(value, full_bytes) {
let shown = total.min(HANDLE_PREVIEW_ROWS);
let handle = crate::handles::put(value.clone(), full_bytes)?;
metadata.insert("handle".to_string(), json!(handle.id));
metadata.insert("shape".to_string(), json!(handle.shape));
metadata.insert("items".to_string(), json!(handle.items));
metadata.insert("bytes".to_string(), json!(handle.bytes));
metadata.insert("total".to_string(), json!(total));
metadata.insert("shown".to_string(), json!(shown));
metadata.insert("omitted".to_string(), json!(total - shown));
return Ok(first_rows(&full, shown));
}
Ok(full)
}
fn table_rows(full: &JsonValue) -> &[JsonValue] {
match full {
JsonValue::Array(rows) => rows.as_slice(),
JsonValue::Object(obj) => match obj.get("rows") {
Some(JsonValue::Array(rows)) => rows.as_slice(),
_ => &[],
},
_ => &[],
}
}
fn rows_within_budget(full: &JsonValue, max: usize) -> usize {
let mut used = 0usize;
let mut shown = 0usize;
for row in table_rows(full) {
let tokens = crate::builtins::est_token_count(&row.to_string());
if shown > 0 && used.saturating_add(tokens) > max {
break;
}
used = used.saturating_add(tokens);
shown += 1;
}
shown
}
fn first_rows(full: &JsonValue, n: usize) -> JsonValue {
match full {
JsonValue::Array(rows) => JsonValue::Array(rows.iter().take(n).cloned().collect()),
JsonValue::Object(obj) => {
let mut obj = obj.clone();
let page: Vec<JsonValue> = table_rows(full).iter().take(n).cloned().collect();
obj.insert("rows".to_string(), JsonValue::Array(page));
JsonValue::Object(obj)
}
other => other.clone(),
}
}
fn execute_eval(code: &str) -> AgentResponse {
let mut env = crate::modules::env_with_modules();
let started = std::time::Instant::now();
let outcome = parse_program(code).and_then(|stmts| eval_program(&stmts, &mut env));
crate::record::note(
crate::record::Surface::Api,
code,
&outcome,
started.elapsed(),
);
match outcome {
Ok(value) => response_from_value(value, code),
Err(e) => AgentResponse {
code: None,
hint: None,
success: false,
result: None,
error: Some(e.to_string()),
result_type: None,
metadata: Some(json!({
"code_attempted": echo_code(code)
})),
},
}
}
fn plugin_builtin_response(name: &str) -> AgentResponse {
let definition = BuiltinDefinition {
name: name.to_string(),
description: "A builtin provided by a loaded plugin; its body is arbitrary IronShell"
.to_string(),
category: "Plugin".to_string(),
signature: format!("{name}(...)"),
parameters: Vec::new(),
return_type: "Any".to_string(),
examples: Vec::new(),
aliases: None,
json_schema: json!({"type": "object"}),
};
AgentResponse {
code: None,
hint: None,
success: true,
result: Some(serde_json::to_value(&definition).unwrap_or(JsonValue::Null)),
error: None,
result_type: Some("BuiltinDefinition".to_string()),
metadata: Some(json!({"effect": crate::safety::effect_of(name).as_str()})),
}
}
fn describe_builtin(name: &str) -> AgentResponse {
let resolved = name
.split_once('.')
.and_then(|(module, member)| crate::modules::builtin_for_member(module, member));
let name = resolved.as_deref().unwrap_or(name);
let builtins = get_all_builtin_definitions();
if let Some(builtin) = builtins.iter().find(|b| b.name == name) {
AgentResponse {
code: None,
hint: None,
success: true,
result: Some(serde_json::to_value(builtin).unwrap_or(JsonValue::Null)),
error: None,
result_type: Some("BuiltinDefinition".to_string()),
metadata: None,
}
} else if crate::plugins::is_manifest_builtin(name) {
plugin_builtin_response(name)
} else {
if let Some(builtin) = builtins.iter().find(|b| {
b.aliases
.as_ref()
.map(|a| a.contains(&name.to_string()))
.unwrap_or(false)
}) {
AgentResponse {
code: None,
hint: None,
success: true,
result: Some(serde_json::to_value(builtin).unwrap_or(JsonValue::Null)),
error: None,
result_type: Some("BuiltinDefinition".to_string()),
metadata: Some(
json!({"note": format!("'{}' is an alias for '{}'", name, builtin.name)}),
),
}
} else {
let did_you_mean =
crate::builtins::nearest_names(name, builtins.iter().map(|b| b.name.as_str()));
let hint = if did_you_mean.is_empty() {
"search by keyword with suggest(\"<words>\"), or list a category with ontology_manifest()".to_string()
} else {
format!("did you mean {}?", did_you_mean.join(", "))
};
AgentResponse {
code: Some("E_UNKNOWN_BUILTIN".to_string()),
hint: Some(hint.clone()),
success: false,
result: None,
error: Some(format!("Builtin '{}' not found; {}", name, hint)),
result_type: None,
metadata: Some(json!({"code": "E_UNKNOWN_BUILTIN", "did_you_mean": did_you_mean})),
}
}
}
}
fn list_builtins(category: Option<&str>) -> AgentResponse {
let builtins = get_all_builtin_definitions();
let filtered: Vec<_> = if let Some(cat) = category {
builtins
.into_iter()
.filter(|b| b.category.to_lowercase() == cat.to_lowercase())
.collect()
} else {
builtins
};
let summary: Vec<JsonValue> = filtered
.iter()
.map(|b| {
json!({
"name": b.name,
"description": b.description,
"category": b.category,
"signature": b.signature
})
})
.collect();
AgentResponse {
code: None,
hint: None,
success: true,
result: Some(json!({
"count": summary.len(),
"builtins": summary
})),
error: None,
result_type: Some("BuiltinList".to_string()),
metadata: None,
}
}
type SchemaCacheKey = (String, String, bool, String, String, String, String);
static SCHEMA_CACHE: OnceLock<Mutex<HashMap<SchemaCacheKey, JsonValue>>> = OnceLock::new();
const SCHEMA_CACHE_LIMIT: usize = 256;
fn normalise_filter(value: Option<&str>) -> Option<String> {
value
.map(|v| v.trim().to_lowercase())
.filter(|v| !v.is_empty())
}
fn get_schema(format: &SchemaFormat, module: Option<&str>, q: Option<&str>) -> AgentResponse {
let module = normalise_filter(module);
let q = normalise_filter(q);
let key: SchemaCacheKey = (
format!("{:?}", format),
format!("{:?}", crate::safety::current_mode()),
deterministic_mode(),
std::env::var("PATH").unwrap_or_default(),
std::env::var("PATHEXT").unwrap_or_default(),
std::env::current_dir()
.map(|p| p.display().to_string())
.unwrap_or_default(),
format!("module={module:?} q={q:?}"),
);
let cache = SCHEMA_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let cached = cache
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.get(&key)
.cloned();
let result = match cached {
Some(value) => value,
None => {
let mut ontology = build_language_ontology();
if let Err(refused) = narrow_ontology(&mut ontology, module.as_deref(), q.as_deref()) {
return refused;
}
let built = build_schema_value(format, &ontology);
let mut cache_map = cache
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if cache_map.len() >= SCHEMA_CACHE_LIMIT {
cache_map.clear();
}
cache_map.insert(key, built.clone());
built
}
};
let filter = (module.is_some() || q.is_some()).then(|| json!({"module": module, "q": q}));
AgentResponse {
code: None,
hint: None,
success: true,
result: Some(result),
error: None,
result_type: Some(format!("{:?}Schema", format)),
metadata: filter.map(|f| json!({"filter": f})),
}
}
fn narrow_ontology(
ontology: &mut LanguageOntology,
module: Option<&str>,
q: Option<&str>,
) -> Result<(), AgentResponse> {
if module.is_none() && q.is_none() {
return Ok(());
}
if let Some(m) = module {
if !ontology
.modules
.iter()
.any(|md| md.name.eq_ignore_ascii_case(m))
{
let did_you_mean = crate::builtins::nearest_names(
m,
ontology.modules.iter().map(|md| md.name.as_str()),
);
let hint = if did_you_mean.is_empty() {
"ontology_manifest() lists every module".to_string()
} else {
format!(
"did you mean {}? ontology_manifest() lists every module",
did_you_mean.join(", ")
)
};
return Err(AgentResponse {
code: Some("E_BAD_ARG".to_string()),
hint: Some(hint.clone()),
success: false,
result: None,
error: Some(format!("Unknown module '{m}'; {hint}")),
result_type: None,
metadata: Some(json!({"code": "E_BAD_ARG", "did_you_mean": did_you_mean})),
});
}
}
let in_module: Option<Vec<String>> = module.map(|m| {
ontology
.modules
.iter()
.filter(|md| md.name.eq_ignore_ascii_case(m))
.flat_map(|md| md.functions.iter().map(|f| f.builtin.clone()))
.collect()
});
let member_of = |b: &BuiltinDefinition, names: &[String]| {
names.contains(&b.name)
|| b.aliases
.as_ref()
.is_some_and(|a| a.iter().any(|x| names.contains(x)))
};
ontology.builtins.retain(|b| {
in_module
.as_ref()
.is_none_or(|names| member_of(b, names.as_slice()))
&& q.is_none_or(|q| {
b.name.to_lowercase().contains(q) || b.description.to_lowercase().contains(q)
})
});
let kept: HashSet<&str> = ontology
.builtins
.iter()
.flat_map(|b| {
std::iter::once(b.name.as_str()).chain(b.aliases.iter().flatten().map(String::as_str))
})
.collect();
for md in &mut ontology.modules {
md.functions.retain(|f| kept.contains(f.builtin.as_str()));
}
ontology.modules.retain(|md| !md.functions.is_empty());
Ok(())
}
fn build_schema_value(format: &SchemaFormat, ontology: &LanguageOntology) -> JsonValue {
match format {
SchemaFormat::JsonSchema => serde_json::to_value(&ontology).unwrap_or(JsonValue::Null),
SchemaFormat::OpenAI => build_openai_schema(&ontology),
SchemaFormat::Claude => build_claude_schema(&ontology),
SchemaFormat::Gemini => build_gemini_schema(&ontology),
SchemaFormat::Llama => build_llama_schema(&ontology),
SchemaFormat::Mistral => build_mistral_schema(&ontology),
SchemaFormat::Cohere => build_cohere_schema(&ontology),
SchemaFormat::Grok => build_grok_schema(&ontology),
SchemaFormat::DeepSeek => build_deepseek_schema(&ontology),
SchemaFormat::Bedrock => build_bedrock_schema(&ontology),
SchemaFormat::AzureOpenAI => build_azure_openai_schema(&ontology),
SchemaFormat::Qwen => build_qwen_schema(&ontology),
SchemaFormat::Ollama => build_ollama_schema(&ontology),
SchemaFormat::VLLM => build_vllm_schema(&ontology),
SchemaFormat::HuggingFace => build_huggingface_schema(&ontology),
SchemaFormat::OpenRouter => build_openrouter_schema(&ontology),
SchemaFormat::Kimi => build_kimi_schema(&ontology),
SchemaFormat::Yi => build_yi_schema(&ontology),
SchemaFormat::GLM => build_glm_schema(&ontology),
SchemaFormat::Reka => build_reka_schema(&ontology),
SchemaFormat::AI21 => build_ai21_schema(&ontology),
SchemaFormat::Perplexity => build_perplexity_schema(&ontology),
SchemaFormat::Together => build_together_schema(&ontology),
SchemaFormat::Groq => build_groq_schema(&ontology),
SchemaFormat::Fireworks => build_fireworks_schema(&ontology),
SchemaFormat::Ontology => build_compact_ontology(&ontology),
SchemaFormat::JsonLD => crate::providers::ontology::OS_ONTOLOGY.to_json_ld(),
SchemaFormat::OwlTurtle => {
json!({"format": "text/turtle", "content": crate::providers::ontology::OS_ONTOLOGY.to_owl_turtle()})
}
SchemaFormat::SHACL => crate::providers::ontology::OS_ONTOLOGY.to_shacl(),
}
}
fn get_type_info(type_name: Option<&str>) -> AgentResponse {
let types = get_type_definitions();
if let Some(name) = type_name {
if let Some(type_def) = types
.iter()
.find(|t| t.name.to_lowercase() == name.to_lowercase())
{
AgentResponse {
code: None,
hint: None,
success: true,
result: Some(serde_json::to_value(type_def).unwrap_or(JsonValue::Null)),
error: None,
result_type: Some("TypeDefinition".to_string()),
metadata: None,
}
} else {
AgentResponse {
code: Some("E_NOT_FOUND".to_string()),
hint: Some(
"send a type_info request without type_name to list the types".to_string(),
),
success: false,
result: None,
error: Some(format!("Type '{}' not found", name)),
result_type: None,
metadata: None,
}
}
} else {
AgentResponse {
code: None,
hint: None,
success: true,
result: Some(serde_json::to_value(&types).unwrap_or(JsonValue::Null)),
error: None,
result_type: Some("TypeList".to_string()),
metadata: None,
}
}
}
fn args_to_code(builtin: &str, args: &JsonValue) -> Result<String> {
if !is_identifier(builtin) {
return Err(anyhow!(
"'{}' is not a builtin name; a builtin name is letters, digits and underscores",
builtin
));
}
let positional: Vec<JsonValue> = match args {
JsonValue::Null => Vec::new(),
JsonValue::Array(arr) => arr.clone(),
JsonValue::Object(obj) => named_args_to_positional(builtin, obj)?,
other => vec![other.clone()],
};
let literals = positional
.iter()
.map(json_to_ae_literal)
.collect::<Result<Vec<_>>>()?;
Ok(format!("{}({})", builtin, literals.join(", ")))
}
pub(crate) fn named_args_to_positional(
builtin: &str,
obj: &serde_json::Map<String, JsonValue>,
) -> Result<Vec<JsonValue>> {
if obj.is_empty() {
return Ok(Vec::new());
}
let definition = get_all_builtin_definitions().into_iter().find(|d| {
d.name == builtin
|| d.aliases
.as_ref()
.is_some_and(|names| names.iter().any(|n| n == builtin))
});
let (canonical, params) = match definition {
Some(d) => (d.name, d.parameters),
None => (builtin.to_string(), Vec::new()),
};
let signature = crate::signature::signature_of(&canonical);
let has_subject = signature.is_some_and(|s| s.subject.is_some());
let mut positions: Vec<(String, bool)> = Vec::new();
if has_subject && params.first().is_none_or(|p| p.name != "subject") {
let required = signature.is_some_and(|s| s.subject_required);
positions.push(("subject".to_string(), required));
}
positions.extend(params.iter().map(|p| (p.name.clone(), p.required)));
let valid = if positions.is_empty() {
"none (pass its arguments as an array)".to_string()
} else {
positions
.iter()
.map(|(name, _)| name.as_str())
.collect::<Vec<_>>()
.join(", ")
};
let mut slots: Vec<Option<JsonValue>> = vec![None; positions.len()];
for (key, value) in obj {
let index = positions
.iter()
.position(|(name, _)| name == key)
.or_else(|| (has_subject && key == "input").then_some(0));
let Some(i) = index else {
return Err(anyhow!(
"unknown argument '{}' for '{}'; valid parameters: {}",
key,
builtin,
valid
));
};
if slots[i].is_some() {
return Err(anyhow!(
"argument '{}' of '{}' is given twice",
positions[i].0,
builtin
));
}
slots[i] = Some(value.clone());
}
if let Some((name, _)) = positions
.iter()
.zip(&slots)
.find(|((_, required), slot)| *required && slot.is_none())
.map(|(position, _)| position)
{
if name == "subject" {
return Err(anyhow!(
"missing required argument 'subject' for '{}': the value it works on, which a pipeline would pipe in (the key 'input' is accepted too); valid parameters: {}",
builtin,
valid
));
}
return Err(anyhow!(
"missing required argument '{}' for '{}'",
name,
builtin
));
}
let Some(last) = slots.iter().rposition(Option::is_some) else {
return Ok(Vec::new());
};
let variadic_last = signature.is_some_and(|s| s.params.last().is_some_and(|p| p.variadic));
let mut out = Vec::with_capacity(last + 1);
for (i, (slot, (name, _))) in slots.into_iter().zip(&positions).enumerate() {
if i > last {
break;
}
match slot {
Some(JsonValue::Array(items)) if variadic_last && i + 1 == positions.len() => {
out.extend(items)
}
Some(value) => out.push(value),
None if i == 0
&& name.as_str() == "subject"
&& matches!(canonical.as_str(), "sql" | "sql_value") => {}
None => {
return Err(anyhow!(
"argument '{}' of '{}' is optional, but a later argument is given; \
give '{}' a value too",
name,
builtin,
name
))
}
}
}
Ok(out)
}
pub fn unknown_builtin_refusal(name: &str) -> Option<AgentResponse> {
let response = describe_builtin(name);
(response.code.as_deref() == Some("E_UNKNOWN_BUILTIN")).then_some(response)
}
fn unwrap_args_envelope(builtin: &str, args: &JsonValue) -> JsonValue {
let inner = match args {
JsonValue::Object(obj) if obj.len() == 1 => match obj.get("args") {
Some(inner @ (JsonValue::Object(_) | JsonValue::Array(_))) => inner,
_ => return args.clone(),
},
_ => return args.clone(),
};
let takes_args = get_all_builtin_definitions()
.into_iter()
.find(|d| {
d.name == builtin
|| d.aliases
.as_ref()
.is_some_and(|names| names.iter().any(|n| n == builtin))
})
.is_some_and(|d| d.parameters.iter().any(|p| p.name == "args"));
if takes_args {
args.clone()
} else {
inner.clone()
}
}
pub fn eval_code_from_body(body: &JsonValue) -> Result<String, AgentResponse> {
match body.get("code") {
Some(JsonValue::String(code)) => Ok(code.clone()),
other => {
let problem = match other {
None => "is missing".to_string(),
Some(v) => format!("must be a string, got {}", json_kind(v)),
};
Err(AgentResponse {
code: Some("E_BAD_ARG".to_string()),
hint: Some(
"send the IronShell source as a string: {\"code\": \"1 + 1\"}".to_string(),
),
success: false,
result: None,
error: Some(format!("the request field 'code' {problem}")),
result_type: None,
metadata: Some(json!({"code": "E_BAD_ARG", "field": "code"})),
})
}
}
}
fn json_kind(v: &JsonValue) -> &'static str {
match v {
JsonValue::Null => "null",
JsonValue::Bool(_) => "a boolean",
JsonValue::Number(_) => "a number",
JsonValue::String(_) => "a string",
JsonValue::Array(_) => "an array",
JsonValue::Object(_) => "an object",
}
}
fn is_identifier(name: &str) -> bool {
let mut chars = name.chars();
chars
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn build_step_code(step: &PipelineStep) -> Result<String> {
let mut code = args_to_code(&step.builtin, &step.args)?;
if let Some(pred) = &step.predicate {
let lambda = predicate_to_lambda(pred);
if !matches!(single_expression(&lambda), Ok(Expr::Lambda { .. })) {
return Err(crate::safety::bad_arg(
&step.builtin,
"a predicate that is one expression over x",
&format!("'{pred}'"),
));
}
code = format!("{}({})", step.builtin, lambda);
}
if let Some(field) = &step.select {
if !is_identifier(field) {
return Err(crate::safety::bad_arg(
&step.builtin,
"a field name of letters, digits and underscores",
&format!("'{field}'"),
));
}
code = format!("{} | map(fn(x) => x.{})", code, field);
}
Ok(code)
}
fn predicate_to_lambda(predicate: &str) -> String {
if predicate.contains(">")
|| predicate.contains("<")
|| predicate.contains("==")
|| predicate.contains("!=")
{
format!("fn(x) => x.{}", predicate)
} else if predicate.starts_with('.') {
format!("fn(x) => x{}", predicate)
} else {
format!("fn(x) => {}", predicate)
}
}
fn json_to_ae_literal(val: &JsonValue) -> Result<String> {
Ok(match val {
JsonValue::Null => "null".to_string(),
JsonValue::Bool(b) => if *b { "true" } else { "false" }.to_string(),
JsonValue::Number(n) => n.to_string(),
JsonValue::String(s) => ae_string_literal(s),
JsonValue::Array(arr) => {
let items = arr
.iter()
.map(json_to_ae_literal)
.collect::<Result<Vec<_>>>()?;
format!("[{}]", items.join(", "))
}
JsonValue::Object(obj) => {
let mut fields = Vec::with_capacity(obj.len());
for (key, value) in obj {
if !is_identifier(key) {
return Err(anyhow!(
"record key '{}' is not an identifier, so it cannot be written as a literal",
key
));
}
fields.push(format!("{}: {}", key, json_to_ae_literal(value)?));
}
format!("{{{}}}", fields.join(", "))
}
})
}
fn ae_string_literal(s: &str) -> String {
let mut body = String::with_capacity(s.len() + 2);
for c in s.chars() {
match c {
'\\' => body.push_str("\\\\"),
'"' => body.push_str("\\\""),
'\n' => body.push_str("\\n"),
'\r' => body.push_str("\\r"),
'\t' => body.push_str("\\t"),
c => body.push(c),
}
}
if body.contains("${") {
format!("(\"{}\")", body.replace("${", "$\" + \"{"))
} else {
format!("\"{}\"", body)
}
}
fn value_to_json(value: &Value) -> JsonValue {
match value {
Value::Null => JsonValue::Null,
Value::Bool(b) => json!(*b),
Value::Int(i) => json!(*i),
Value::Float(f) => json!(*f),
Value::Str(s) => json!(s),
Value::Uri(u) => json!({"_type": "Uri", "value": u}),
Value::Array(arr) => {
let items: Vec<JsonValue> = arr.iter().map(value_to_json).collect();
JsonValue::Array(items)
}
Value::Record(fields) => {
let obj: serde_json::Map<String, JsonValue> = fields
.iter()
.map(|(k, v)| (k.clone(), value_to_json(v)))
.collect();
JsonValue::Object(obj)
}
Value::Table(table) => {
json!({
"_type": "Table",
"schema": table.schema,
"rows": table.rows.iter().map(|row| {
let obj: serde_json::Map<String, JsonValue> = row
.iter()
.map(|(k, v)| (k.clone(), value_to_json(v)))
.collect();
JsonValue::Object(obj)
}).collect::<Vec<_>>()
})
}
Value::Lambda(lambda) => {
json!({
"_type": "Lambda",
"params": lambda.params,
"body": format!("{:?}", lambda.body)
})
}
Value::AsyncLambda(lambda) => {
json!({
"_type": "AsyncLambda",
"params": lambda.params,
"body": format!("{:?}", lambda.body)
})
}
Value::Future(future) => {
json!({
"_type": "Future",
"args": future.args.iter().map(value_to_json).collect::<Vec<_>>()
})
}
Value::Error(e) => json!({
"_type": "Error",
"message": e
}),
Value::Builtin(b) => json!({
"_type": "Builtin",
"name": b.name
}),
}
}
fn value_type_name(value: &Value) -> String {
value.type_name().to_string()
}
pub fn build_language_ontology() -> LanguageOntology {
LanguageOntology {
ontology_version: "1.0.0".to_string(),
language: LanguageInfo {
name: "IronShell".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
description: "A typed functional shell with multi-modal AI capabilities".to_string(),
paradigms: vec![
"Functional".to_string(),
"Pipeline-oriented".to_string(),
"Expression-based".to_string(),
],
typing: "Hindley-Milner type inference".to_string(),
},
types: get_type_definitions(),
builtins: get_all_builtin_definitions(),
modules: get_module_definitions(),
operators: get_operator_definitions(),
syntax: SyntaxPatterns {
variable_declaration: "let <name> = <expr>".to_string(),
function_definition: "fn <name>(<params>) => <expr>".to_string(),
lambda: "fn(<params>) => <expr>".to_string(),
pipeline: "<expr> | <func> | <func>".to_string(),
record_literal: "{ field1: value1, field2: value2 }".to_string(),
array_literal: "[item1, item2, item3]".to_string(),
conditional: "if <cond> then <expr> else <expr>".to_string(),
pattern_matching: "match <expr> { pattern => result, ... }".to_string(),
},
categories: get_category_info(),
os_ontology: Some(crate::providers::ontology::OS_ONTOLOGY.to_json_schema()),
cli_tools: Some(crate::providers::ontology::CLI_TOOL_REGISTRY.to_json_schema()),
}
}
fn get_type_definitions() -> Vec<TypeDefinition> {
vec![
TypeDefinition {
name: "Int".to_string(),
description: "64-bit signed integer".to_string(),
json_equivalent: "number (integer)".to_string(),
examples: vec!["42".to_string(), "-17".to_string(), "0".to_string()],
fields: None,
},
TypeDefinition {
name: "Float".to_string(),
description: "64-bit floating point number".to_string(),
json_equivalent: "number".to_string(),
examples: vec!["3.14".to_string(), "-0.5".to_string(), "1e10".to_string()],
fields: None,
},
TypeDefinition {
name: "String".to_string(),
description: "UTF-8 text string".to_string(),
json_equivalent: "string".to_string(),
examples: vec!["\"hello\"".to_string(), "\"multi\\nline\"".to_string()],
fields: None,
},
TypeDefinition {
name: "Bool".to_string(),
description: "Boolean true/false".to_string(),
json_equivalent: "boolean".to_string(),
examples: vec!["true".to_string(), "false".to_string()],
fields: None,
},
TypeDefinition {
name: "Array".to_string(),
description: "Ordered collection of values".to_string(),
json_equivalent: "array".to_string(),
examples: vec!["[1, 2, 3]".to_string(), "[\"a\", \"b\"]".to_string()],
fields: None,
},
TypeDefinition {
name: "Record".to_string(),
description: "Key-value mapping (like JSON object)".to_string(),
json_equivalent: "object".to_string(),
examples: vec!["{name: \"file.txt\", size: 1024}".to_string()],
fields: None,
},
TypeDefinition {
name: "Table".to_string(),
description: "Tabular data with headers and rows".to_string(),
json_equivalent: "object with headers and rows arrays".to_string(),
examples: vec!["Result of ls(), csv parsing".to_string()],
fields: Some(vec![
FieldDefinition {
name: "headers".to_string(),
field_type: "Array<String>".to_string(),
description: "Column names".to_string(),
},
FieldDefinition {
name: "rows".to_string(),
field_type: "Array<Array<Value>>".to_string(),
description: "Row data".to_string(),
},
]),
},
TypeDefinition {
name: "Lambda".to_string(),
description: "Anonymous function".to_string(),
json_equivalent: "object with params and body".to_string(),
examples: vec![
"fn(x) => x * 2".to_string(),
"fn(a, b) => a + b".to_string(),
],
fields: None,
},
TypeDefinition {
name: "Option".to_string(),
description: "Optional value (Some or None)".to_string(),
json_equivalent: "value or null".to_string(),
examples: vec!["Some(42)".to_string(), "None".to_string()],
fields: None,
},
]
}
fn annotate_effects(mut defs: Vec<BuiltinDefinition>) -> Vec<BuiltinDefinition> {
for d in &mut defs {
let eff = crate::safety::effect_of(&d.name).as_str();
if let JsonValue::Object(ref mut m) = d.json_schema {
m.insert("x-effect".to_string(), json!(eff));
m.insert(
"x-effect-declared".to_string(),
json!(crate::safety::effect_is_declared(&d.name)),
);
let examples = crate::shapes::field_examples(&d.name);
if !examples.is_empty() {
m.insert(
"x-returns-examples".to_string(),
json!(examples
.into_iter()
.map(|(f, e)| (f.to_string(), e.to_string()))
.collect::<std::collections::BTreeMap<_, _>>()),
);
}
if let Some(shape) = crate::shapes::shape_of(&d.name) {
m.insert("x-returns".to_string(), json!(shape));
} else if let Some(shape) = crate::shapes::polymorphic_shape_of(&d.name) {
m.insert("x-returns".to_string(), json!(shape));
m.insert(
"x-returns-var".to_string(),
json!("T = element type of the first argument"),
);
}
}
}
defs
}
fn get_builtin_definitions() -> Vec<BuiltinDefinition> {
let builtins = vec![
BuiltinDefinition {
name: "ls".to_string(),
description: "List directory contents".to_string(),
category: "FileSystem".to_string(),
signature: "ls(path?: String) -> Array<Record>".to_string(),
parameters: vec![ParameterDefinition {
name: "path".to_string(),
param_type: "String".to_string(),
description: "Directory path to list".to_string(),
required: false,
default: Some("\".\"".to_string()),
}],
return_type: "Array<Record>".to_string(),
examples: vec![
ExampleDefinition {
description: "List current directory".to_string(),
code: "ls()".to_string(),
result: Some("[{name: \"file.txt\", size: 1024, ...}, ...]".to_string()),
},
ExampleDefinition {
description: "List specific directory".to_string(),
code: "ls(\"/home\")".to_string(),
result: None,
},
],
aliases: Some(vec!["dir".to_string()]),
json_schema: json!({
"type": "object",
"properties": {
"path": {"type": "string", "description": "Directory path"}
}
}),
},
BuiltinDefinition {
name: "cat".to_string(),
description: "Read file contents".to_string(),
category: "FileSystem".to_string(),
signature: "cat(path: String) -> String".to_string(),
parameters: vec![ParameterDefinition {
name: "path".to_string(),
param_type: "String".to_string(),
description: "File path to read".to_string(),
required: true,
default: None,
}],
return_type: "String".to_string(),
examples: vec![ExampleDefinition {
description: "Read a file".to_string(),
code: "cat(\"README.md\")".to_string(),
result: None,
}],
aliases: Some(vec!["read".to_string(), "read_text".to_string()]),
json_schema: json!({
"type": "object",
"properties": {
"path": {"type": "string", "description": "File path to read"}
},
"required": ["path"]
}),
},
BuiltinDefinition {
name: "pwd".to_string(),
description: "Get current working directory".to_string(),
category: "FileSystem".to_string(),
signature: "pwd() -> String".to_string(),
parameters: vec![],
return_type: "String".to_string(),
examples: vec![ExampleDefinition {
description: "Get current directory".to_string(),
code: "pwd()".to_string(),
result: Some("\"/home/user\"".to_string()),
}],
aliases: None,
json_schema: json!({"type": "object", "properties": {}}),
},
BuiltinDefinition {
name: "cd".to_string(),
description: "Change current directory".to_string(),
category: "FileSystem".to_string(),
signature: "cd(path: String) -> String".to_string(),
parameters: vec![ParameterDefinition {
name: "path".to_string(),
param_type: "String".to_string(),
description: "Directory to change to".to_string(),
required: true,
default: None,
}],
return_type: "String".to_string(),
examples: vec![ExampleDefinition {
description: "Change to home directory".to_string(),
code: "cd(\"~\")".to_string(),
result: None,
}],
aliases: None,
json_schema: json!({
"type": "object",
"properties": {
"path": {"type": "string", "description": "Directory path"}
},
"required": ["path"]
}),
},
BuiltinDefinition {
name: "map".to_string(),
description: "Transform each element of an array".to_string(),
category: "Functional".to_string(),
signature: "map(array: Array, fn: Lambda) -> Array".to_string(),
parameters: vec![
ParameterDefinition {
name: "array".to_string(),
param_type: "Array".to_string(),
description: "Input array (or piped input)".to_string(),
required: true,
default: None,
},
ParameterDefinition {
name: "fn".to_string(),
param_type: "Lambda".to_string(),
description: "Transformation function".to_string(),
required: true,
default: None,
},
],
return_type: "Array".to_string(),
examples: vec![
ExampleDefinition {
description: "Double each number".to_string(),
code: "[1, 2, 3] | map(fn(x) => x * 2)".to_string(),
result: Some("[2, 4, 6]".to_string()),
},
ExampleDefinition {
description: "Extract field from records".to_string(),
code: "ls() | map(fn(f) => f.name)".to_string(),
result: None,
},
],
aliases: None,
json_schema: json!({
"type": "object",
"properties": {
"input": {"type": "array", "description": "Array to transform"},
"field": {"type": "string", "description": "Field to extract (shorthand for map)"}
}
}),
},
BuiltinDefinition {
name: "where".to_string(),
description: "Filter array elements by predicate".to_string(),
category: "Functional".to_string(),
signature: "where(array: Array, predicate: Lambda) -> Array".to_string(),
parameters: vec![
ParameterDefinition {
name: "array".to_string(),
param_type: "Array".to_string(),
description: "Input array".to_string(),
required: true,
default: None,
},
ParameterDefinition {
name: "predicate".to_string(),
param_type: "Lambda".to_string(),
description: "Filter function returning Bool".to_string(),
required: true,
default: None,
},
],
return_type: "Array".to_string(),
examples: vec![ExampleDefinition {
description: "Filter large files".to_string(),
code: "ls() | where(fn(f) => f.size > 1000)".to_string(),
result: None,
}],
aliases: Some(vec!["filter".to_string()]),
json_schema: json!({
"type": "object",
"properties": {
"input": {"type": "array"},
"predicate": {"type": "string", "description": "Filter condition like 'size > 1000'"}
}
}),
},
BuiltinDefinition {
name: "reduce".to_string(),
description: "Reduce array to single value".to_string(),
category: "Functional".to_string(),
signature: "reduce(array: Array, fn: Lambda, init: Value) -> Value".to_string(),
parameters: vec![
ParameterDefinition {
name: "array".to_string(),
param_type: "Array".to_string(),
description: "Input array".to_string(),
required: true,
default: None,
},
ParameterDefinition {
name: "fn".to_string(),
param_type: "Lambda".to_string(),
description: "Reducer function (acc, item) => new_acc".to_string(),
required: true,
default: None,
},
ParameterDefinition {
name: "init".to_string(),
param_type: "Value".to_string(),
description: "Initial accumulator value".to_string(),
required: true,
default: None,
},
],
return_type: "Value".to_string(),
examples: vec![ExampleDefinition {
description: "Sum numbers".to_string(),
code: "[1, 2, 3] | reduce(fn(a, b) => a + b, 0)".to_string(),
result: Some("6".to_string()),
}],
aliases: Some(vec!["fold".to_string()]),
json_schema: json!({
"type": "object",
"properties": {
"input": {"type": "array"},
"operation": {"type": "string", "enum": ["sum", "product", "concat", "custom"]},
"initial": {"description": "Initial value"}
}
}),
},
BuiltinDefinition {
name: "ai".to_string(),
description: "Query AI model with a prompt".to_string(),
category: "AI".to_string(),
signature: "ai(prompt?: String, config?: Record) -> String".to_string(),
parameters: vec![
ParameterDefinition {
name: "prompt".to_string(),
param_type: "String".to_string(),
description: "The prompt to send to the AI; with piped input the piped string is the prompt".to_string(),
required: false,
default: None,
},
ParameterDefinition {
name: "config".to_string(),
param_type: "Record".to_string(),
description: "Optional images, audio or video arrays of file paths; a model field is accepted and not used".to_string(),
required: false,
default: None,
},
],
return_type: "String".to_string(),
examples: vec![ExampleDefinition {
description: "Ask AI a question".to_string(),
code: "ai(\"Explain recursion\")".to_string(),
result: None,
}],
aliases: None,
json_schema: json!({
"type": "object",
"properties": {
"prompt": {"type": "string", "description": "AI prompt"},
"model": {"type": "string", "description": "Model to use"},
"temperature": {"type": "number", "minimum": 0, "maximum": 2}
},
"required": ["prompt"]
}),
},
BuiltinDefinition {
name: "sum".to_string(),
description: "Sum numeric values in an array".to_string(),
category: "Aggregation".to_string(),
signature: "sum(array: Array<Number>) -> Number".to_string(),
parameters: vec![ParameterDefinition {
name: "array".to_string(),
param_type: "Array<Number>".to_string(),
description: "Array of numbers".to_string(),
required: true,
default: None,
}],
return_type: "Number".to_string(),
examples: vec![ExampleDefinition {
description: "Sum numbers".to_string(),
code: "[1, 2, 3, 4, 5] | sum()".to_string(),
result: Some("15".to_string()),
}],
aliases: None,
json_schema: json!({
"type": "object",
"properties": {
"input": {"type": "array", "items": {"type": "number"}}
}
}),
},
BuiltinDefinition {
name: "avg".to_string(),
description: "Calculate average of numeric values".to_string(),
category: "Aggregation".to_string(),
signature: "avg(array: Array<Number>) -> Float".to_string(),
parameters: vec![ParameterDefinition {
name: "array".to_string(),
param_type: "Array<Number>".to_string(),
description: "Array of numbers".to_string(),
required: true,
default: None,
}],
return_type: "Float".to_string(),
examples: vec![ExampleDefinition {
description: "Average of numbers".to_string(),
code: "[1, 2, 3, 4, 5] | avg()".to_string(),
result: Some("3.0".to_string()),
}],
aliases: Some(vec!["mean".to_string()]),
json_schema: json!({
"type": "object",
"properties": {
"input": {"type": "array", "items": {"type": "number"}}
}
}),
},
BuiltinDefinition {
name: "len".to_string(),
description: "Get length of array or string".to_string(),
category: "Core".to_string(),
signature: "len(value: Array | String) -> Int".to_string(),
parameters: vec![ParameterDefinition {
name: "value".to_string(),
param_type: "Array | String".to_string(),
description: "Array or string to measure".to_string(),
required: true,
default: None,
}],
return_type: "Int".to_string(),
examples: vec![
ExampleDefinition {
description: "Array length".to_string(),
code: "len([1, 2, 3])".to_string(),
result: Some("3".to_string()),
},
ExampleDefinition {
description: "String length".to_string(),
code: "len(\"hello\")".to_string(),
result: Some("5".to_string()),
},
],
aliases: Some(vec!["length".to_string(), "count".to_string()]),
json_schema: json!({
"type": "object",
"properties": {
"input": {"description": "Array or string"}
}
}),
},
BuiltinDefinition {
name: "split".to_string(),
description: "Split string into array".to_string(),
category: "String".to_string(),
signature: "split(str: String, delimiter: String) -> Array<String>".to_string(),
parameters: vec![
ParameterDefinition {
name: "str".to_string(),
param_type: "String".to_string(),
description: "String to split".to_string(),
required: true,
default: None,
},
ParameterDefinition {
name: "delimiter".to_string(),
param_type: "String".to_string(),
description: "Delimiter to split on".to_string(),
required: true,
default: None,
},
],
return_type: "Array<String>".to_string(),
examples: vec![ExampleDefinition {
description: "Split CSV line".to_string(),
code: "\"a,b,c\" | split(\",\")".to_string(),
result: Some("[\"a\", \"b\", \"c\"]".to_string()),
}],
aliases: None,
json_schema: json!({
"type": "object",
"properties": {
"input": {"type": "string"},
"delimiter": {"type": "string"}
},
"required": ["delimiter"]
}),
},
BuiltinDefinition {
name: "join".to_string(),
description: "Join array elements into string".to_string(),
category: "String".to_string(),
signature: "join(array: Array, delimiter: String) -> String".to_string(),
parameters: vec![
ParameterDefinition {
name: "array".to_string(),
param_type: "Array".to_string(),
description: "Array to join".to_string(),
required: true,
default: None,
},
ParameterDefinition {
name: "delimiter".to_string(),
param_type: "String".to_string(),
description: "Delimiter between elements".to_string(),
required: true,
default: None,
},
],
return_type: "String".to_string(),
examples: vec![ExampleDefinition {
description: "Join with comma".to_string(),
code: "[\"a\", \"b\", \"c\"] | join(\", \")".to_string(),
result: Some("\"a, b, c\"".to_string()),
}],
aliases: None,
json_schema: json!({
"type": "object",
"properties": {
"input": {"type": "array"},
"delimiter": {"type": "string"}
},
"required": ["delimiter"]
}),
},
BuiltinDefinition {
name: "http_get".to_string(),
description: "Make HTTP GET request".to_string(),
category: "Network".to_string(),
signature: "http_get(url: String, options?: Record) -> Value".to_string(),
parameters: vec![
ParameterDefinition {
name: "url".to_string(),
param_type: "String".to_string(),
description: "URL to request".to_string(),
required: true,
default: None,
},
ParameterDefinition {
name: "options".to_string(),
param_type: "Record".to_string(),
description: "Headers, timeout, etc.".to_string(),
required: false,
default: None,
},
],
return_type: "Value".to_string(),
examples: vec![ExampleDefinition {
description: "Fetch JSON API".to_string(),
code: "http_get(\"https://api.example.com/data\")".to_string(),
result: None,
}],
aliases: Some(vec!["fetch".to_string(), "get".to_string()]),
json_schema: json!({
"type": "object",
"properties": {
"url": {"type": "string", "format": "uri"},
"headers": {"type": "object"}
},
"required": ["url"]
}),
},
BuiltinDefinition {
name: "mcp_tools".to_string(),
description: "List available MCP tools".to_string(),
category: "MCP".to_string(),
signature: "mcp_tools(options?: Record) -> Array<Record>".to_string(),
parameters: vec![ParameterDefinition {
name: "options".to_string(),
param_type: "Record".to_string(),
description: "Filter options like category".to_string(),
required: false,
default: None,
}],
return_type: "Array<Record>".to_string(),
examples: vec![ExampleDefinition {
description: "List all tools".to_string(),
code: "mcp_tools()".to_string(),
result: None,
}],
aliases: None,
json_schema: json!({
"type": "object",
"properties": {
"category": {"type": "string"}
}
}),
},
BuiltinDefinition {
name: "mcp_call".to_string(),
description: "Execute an MCP tool".to_string(),
category: "MCP".to_string(),
signature: "mcp_call(tool: String, args: Record) -> Value".to_string(),
parameters: vec![
ParameterDefinition {
name: "tool".to_string(),
param_type: "String".to_string(),
description: "Tool name".to_string(),
required: true,
default: None,
},
ParameterDefinition {
name: "args".to_string(),
param_type: "Record".to_string(),
description: "Tool arguments".to_string(),
required: true,
default: None,
},
],
return_type: "Value".to_string(),
examples: vec![ExampleDefinition {
description: "Call git tool".to_string(),
code: "mcp_call(\"git\", {command: \"status\"})".to_string(),
result: None,
}],
aliases: None,
json_schema: json!({
"type": "object",
"properties": {
"tool": {"type": "string"},
"args": {"type": "object"}
},
"required": ["tool", "args"]
}),
},
];
annotate_effects(builtins)
}
fn categorize_builtin(name: &str) -> String {
match name {
n if n.starts_with("ai") || n.starts_with("agent") || n.starts_with("swarm") => {
"AI".to_string()
}
n if n.starts_with("mcp_") => "MCP".to_string(),
n if n.starts_with("a2a_") => "A2A".to_string(),
n if n.starts_with("a2ui_") => "A2UI".to_string(),
n if n.starts_with("nanda_") => "NANDA".to_string(),
n if n.starts_with("nn_") => "NeuralNet".to_string(),
n if n.starts_with("evo_") => "Evolution".to_string(),
n if n.starts_with("rl_") => "ReinforcementLearning".to_string(),
n if n.starts_with("kg_")
|| n.starts_with("rag_")
|| n.starts_with("semantic_cache")
|| n.starts_with("fine_tune") =>
{
"Knowledge".to_string()
}
n if n.starts_with("http_") || n.starts_with("net_") || n == "http_get" || n == "fetch" => {
"Network".to_string()
}
n if n.starts_with("sys_") || n == "whoami" || n == "hostname" || n == "uptime" => {
"System".to_string()
}
n if n.starts_with("proc_") || n.starts_with("ps") || n == "kill" || n == "killall" => {
"Process".to_string()
}
n if n.starts_with("file_")
|| n.starts_with("fs_")
|| [
"ls",
"cat",
"pwd",
"cd",
"mkdir",
"rm",
"mv",
"cp",
"find",
"head",
"tail",
"wc",
"sort",
"uniq",
"grep",
"touch",
"chmod",
"chown",
"stat",
"glob",
"tree",
"exists",
"read_text",
"write",
"list",
"dir",
]
.contains(&n) =>
{
"FileSystem".to_string()
}
n if n.starts_with("svc_") || n.starts_with("service_") || n.starts_with("systemctl") => {
"Service".to_string()
}
n if n.starts_with("cron_") || n.starts_with("at_") => "Scheduling".to_string(),
n if n.starts_with("archive_")
|| n.starts_with("zip")
|| n.starts_with("tar")
|| n.starts_with("gzip")
|| n.starts_with("bzip2")
|| n.starts_with("xz") =>
{
"Archive".to_string()
}
n if n.starts_with("user_") || n.starts_with("perm_") || n.starts_with("group_") => {
"UserManagement".to_string()
}
n if n.starts_with("pkg_") => "Package".to_string(),
n if n.starts_with("hw_") || n.starts_with("device_") => "Hardware".to_string(),
n if n.starts_with("gui_")
|| n.starts_with("screen")
|| n.starts_with("click")
|| n.starts_with("type_text") =>
{
"GUI".to_string()
}
n if n.starts_with("web_") || n.starts_with("browser_") => "Web".to_string(),
n if n.starts_with("clip_") || n.starts_with("input_") => "Input".to_string(),
n if n.starts_with("db_") || n.starts_with("sqlite_") || n.starts_with("sql_") => {
"Database".to_string()
}
n if n.starts_with("crypto_")
|| n.starts_with("hash")
|| n.starts_with("encrypt")
|| n.starts_with("decrypt")
|| n.starts_with("sign")
|| n.starts_with("verify")
|| n.starts_with("ssl_") =>
{
"Crypto".to_string()
}
n if n.starts_with("docker_")
|| n.starts_with("podman_")
|| n.starts_with("container_")
|| n.starts_with("lxc_") =>
{
"Container".to_string()
}
n if n.starts_with("k8s_") || n.starts_with("helm_") || n.starts_with("kubectl_") => {
"Kubernetes".to_string()
}
n if n.starts_with("vm_")
|| n.starts_with("hyperv_")
|| n.starts_with("virsh_")
|| n.starts_with("qemu_")
|| n.starts_with("wsl_") =>
{
"Virtualization".to_string()
}
n if n.starts_with("terraform_")
|| n.starts_with("ansible_")
|| n.starts_with("pulumi_")
|| n.starts_with("vagrant_")
|| n.starts_with("packer_")
|| n.starts_with("cloud_") =>
{
"Cloud".to_string()
}
n if n.starts_with("ssh_")
|| n.starts_with("scp_")
|| n.starts_with("rsync_")
|| n.starts_with("rdp_") =>
{
"RemoteAccess".to_string()
}
n if n.starts_with("firewall_")
|| n.starts_with("selinux_")
|| n.starts_with("apparmor_") =>
{
"Security".to_string()
}
n if n.starts_with("monitor_")
|| n.starts_with("perf_")
|| n.starts_with("netstat_")
|| n.starts_with("watch_") =>
{
"Monitoring".to_string()
}
n if n.starts_with("rbac_") => "RBAC".to_string(),
n if n.starts_with("audit_") => "Audit".to_string(),
n if n.starts_with("sso_") => "SSO".to_string(),
n if n.starts_with("cluster_") || n.starts_with("job_") => "Cluster".to_string(),
n if n.starts_with("repl_") => "REPL".to_string(),
n if n.starts_with("workspace_") => "Workspace".to_string(),
n if n.starts_with("marketplace_") => "Marketplace".to_string(),
n if n.starts_with("telemetry_") => "Telemetry".to_string(),
n if n.starts_with("git_") => "Git".to_string(),
n if n.starts_with("code_") => "Code".to_string(),
n if n.starts_with("project_") => "Project".to_string(),
n if n.starts_with("search_") => "Search".to_string(),
n if n.starts_with("test_") => "Testing".to_string(),
n if n.starts_with("diag_") => "Diagnostics".to_string(),
n if n.starts_with("refactor_") => "Refactoring".to_string(),
n if n.starts_with("session_") => "Session".to_string(),
n if n.starts_with("docs_") || n.starts_with("doc_") => "Documentation".to_string(),
n if n.starts_with("devenv_") => "DevEnvironment".to_string(),
n if n.starts_with("platform_") => "Platform".to_string(),
n if n.starts_with("math_")
|| [
"sqrt", "pow", "abs", "ceil", "floor", "round", "sin", "cos", "tan", "log", "exp",
"pi", "e",
]
.contains(&n) =>
{
"Math".to_string()
}
n if n.starts_with("str_")
|| [
"split",
"join",
"trim",
"upper",
"lower",
"replace",
"contains",
"starts_with",
"ends_with",
"pad_left",
"pad_right",
"repeat",
"reverse",
"slice",
"format",
"to_upper",
"to_lower",
"capitalize",
"char_at",
"substr",
"str_replace",
]
.contains(&n) =>
{
"String".to_string()
}
n if n.starts_with("arr_")
|| [
"range",
"flatten",
"zip",
"unique",
"sort_by",
"group_by",
"chunk",
"window",
"scan",
"zip_with",
"interleave",
"partition",
]
.contains(&n) =>
{
"Array".to_string()
}
n if n.starts_with("json_") || n == "to_json" || n == "from_json" => "JSON".to_string(),
n if n.starts_with("shell_") || n == "exec" || n == "source" || n == "eval_bash" => {
"Shell".to_string()
}
n if [
"map", "where", "reduce", "filter", "each", "any", "all", "take", "first", "last",
"keys", "values", "select", "reject", "flat_map", "fold",
]
.contains(&n) =>
{
"Functional".to_string()
}
n if [
"sum", "avg", "mean", "min", "max", "count", "product", "len",
]
.contains(&n) =>
{
"Aggregation".to_string()
}
_ => "Core".to_string(),
}
}
fn describe_builtin_name(name: &str) -> String {
let words: Vec<&str> = name.split('_').collect();
if words.len() <= 1 {
return match name {
"ls" => "List directory contents".to_string(),
"cat" => "Read file contents".to_string(),
"pwd" => "Print working directory".to_string(),
"cd" => "Change directory".to_string(),
"rm" => "Remove file or directory".to_string(),
"mv" => "Move or rename file".to_string(),
"cp" => "Copy file or directory".to_string(),
"grep" => "Search for pattern in text".to_string(),
"sort" => "Sort lines or array elements".to_string(),
"uniq" => {
"Remove adjacent duplicate elements (like Unix uniq); use unique for all duplicates"
.to_string()
}
"wc" => "Count lines, words, and bytes".to_string(),
"head" => "Show first N lines".to_string(),
"tail" => "Show last N lines".to_string(),
"find" => "Find files by pattern".to_string(),
"mkdir" => "Create directory".to_string(),
"touch" => "Create empty file or update timestamp".to_string(),
"echo" => "Print value to output".to_string(),
"print" => "Print formatted output".to_string(),
"help" => "Show available commands and help".to_string(),
"clear" => "Clear terminal screen".to_string(),
"map" => "Transform each element with a function".to_string(),
"where" | "filter" => "Filter elements by predicate".to_string(),
"reduce" => "Reduce array to single value".to_string(),
"sum" => "Sum numeric values".to_string(),
"avg" => "Calculate average of numeric values".to_string(),
"len" => "Get length of array or string".to_string(),
"keys" => "Get keys of a record".to_string(),
"values" => "Get values of a record".to_string(),
"first" => "Get first element".to_string(),
"last" => "Get last element".to_string(),
"any" => "Check if any element matches".to_string(),
"all" => "Check if all elements match".to_string(),
"take" => "Take first N elements".to_string(),
"each" => "Execute function for each element".to_string(),
"split" => "Split string by delimiter".to_string(),
"join" => "Join array elements with delimiter".to_string(),
"trim" => "Remove whitespace from string".to_string(),
"upper" => "Convert string to uppercase".to_string(),
"lower" => "Convert string to lowercase".to_string(),
"contains" => "Check if string contains substring".to_string(),
"replace" => "Replace occurrences in string".to_string(),
"min" => "Get minimum value".to_string(),
"max" => "Get maximum value".to_string(),
"count" => "Count elements".to_string(),
"flatten" => "Flatten nested arrays".to_string(),
"range" => "Generate a range of numbers".to_string(),
"unique" => "Remove duplicate elements".to_string(),
"reverse" => "Reverse array or string".to_string(),
"select" => "Select fields from records".to_string(),
"reject" => "Remove fields from records".to_string(),
"call" => "Call a named builtin dynamically".to_string(),
"type" => "Get the type of a value".to_string(),
"whoami" => "Get current username".to_string(),
"hostname" => "Get system hostname".to_string(),
"uptime" => "Get system uptime".to_string(),
"kill" => "Terminate a process".to_string(),
"exec" => "Execute external command".to_string(),
"fetch" => "Fetch URL contents".to_string(),
"glob" => "Match files by glob pattern".to_string(),
_ => capitalize_first(name).to_string(),
};
}
let prefix = words[0];
let rest: Vec<&str> = words[1..].to_vec();
let action = rest.join(" ");
match prefix {
"sys" => format!("System: {}", action),
"proc" => format!("Process: {}", action),
"net" => format!("Network: {}", action),
"http" => format!("HTTP: {}", action),
"file" => format!("File: {}", action),
"fs" => format!("Filesystem: {}", action),
"gui" => format!("GUI: {}", action),
"web" => format!("Web: {}", action),
"db" | "sqlite" => format!("Database: {}", action),
"crypto" => format!("Crypto: {}", action),
"svc" | "service" => format!("Service: {}", action),
"cron" => format!("Schedule: {}", action),
"archive" => format!("Archive: {}", action),
"user" => format!("User: {}", action),
"perm" => format!("Permission: {}", action),
"pkg" => format!("Package: {}", action),
"hw" => format!("Hardware: {}", action),
"clip" => format!("Clipboard: {}", action),
"input" => format!("Input: {}", action),
"docker" => format!("Docker: {}", action),
"podman" => format!("Podman: {}", action),
"container" => format!("Container: {}", action),
"k8s" | "kubectl" => format!("Kubernetes: {}", action),
"helm" => format!("Helm: {}", action),
"vm" => format!("VM: {}", action),
"hyperv" => format!("Hyper-V: {}", action),
"virsh" => format!("Libvirt: {}", action),
"qemu" => format!("QEMU: {}", action),
"wsl" => format!("WSL: {}", action),
"lxc" => format!("LXC: {}", action),
"terraform" => format!("Terraform: {}", action),
"ansible" => format!("Ansible: {}", action),
"pulumi" => format!("Pulumi: {}", action),
"vagrant" => format!("Vagrant: {}", action),
"packer" => format!("Packer: {}", action),
"ssh" => format!("SSH: {}", action),
"scp" => format!("SCP: {}", action),
"rsync" => format!("Rsync: {}", action),
"rdp" => format!("RDP: {}", action),
"firewall" => format!("Firewall: {}", action),
"selinux" => format!("SELinux: {}", action),
"apparmor" => format!("AppArmor: {}", action),
"ssl" => format!("SSL/TLS: {}", action),
"monitor" => format!("Monitor: {}", action),
"perf" => format!("Performance: {}", action),
"git" => format!("Git: {}", action),
"code" => format!("Code: {}", action),
"project" => format!("Project: {}", action),
"search" => format!("Search: {}", action),
"test" => format!("Test: {}", action),
"diag" => format!("Diagnostics: {}", action),
"refactor" => format!("Refactor: {}", action),
"session" => format!("Session: {}", action),
"docs" | "doc" => format!("Documentation: {}", action),
"devenv" => format!("Dev environment: {}", action),
"platform" => format!("Platform: {}", action),
"math" => format!("Math: {}", action),
"str" => format!("String: {}", action),
"arr" => format!("Array: {}", action),
"json" => format!("JSON: {}", action),
"shell" => format!("Shell: {}", action),
"mcp" => format!("MCP: {}", action),
"a2a" => format!("Agent-to-Agent: {}", action),
"a2ui" => format!("Agent-to-UI: {}", action),
"nanda" => format!("NANDA: {}", action),
"nn" => format!("Neural net: {}", action),
"evo" => format!("Evolution: {}", action),
"rl" => format!("Reinforcement learning: {}", action),
"rbac" => format!("RBAC: {}", action),
"audit" => format!("Audit: {}", action),
"sso" => format!("SSO: {}", action),
"cluster" => format!("Cluster: {}", action),
"cloud" => format!("Cloud: {}", action),
"repl" => format!("REPL: {}", action),
"workspace" => format!("Workspace: {}", action),
"marketplace" => format!("Marketplace: {}", action),
"telemetry" => format!("Telemetry: {}", action),
_ => {
let full = words.join(" ");
capitalize_first(&full)
}
}
}
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().to_string() + chars.as_str(),
}
}
fn infer_return_type(name: &str) -> String {
match name {
n if n == "ls" || n == "find" || n == "glob" || n == "tree" => "Array<Record>".to_string(),
n if n == "cat"
|| n == "pwd"
|| n == "whoami"
|| n == "hostname"
|| n == "echo"
|| n == "print" =>
{
"String".to_string()
}
n if n == "len" || n == "count" || n == "wc" => "Int".to_string(),
n if n == "sum" || n == "avg" || n == "min" || n == "max" || n == "product" => {
"Number".to_string()
}
n if n == "any" || n == "all" || n == "exists" || n == "contains" => "Bool".to_string(),
n if n == "first" || n == "last" || n == "reduce" => "Value".to_string(),
n if n == "map"
|| n == "where"
|| n == "filter"
|| n == "take"
|| n == "sort"
|| n == "uniq"
|| n == "unique"
|| n == "flatten"
|| n == "range"
|| n == "reverse"
|| n == "select"
|| n == "reject"
|| n == "keys"
|| n == "values"
|| n == "each" =>
{
"Array".to_string()
}
n if n == "split" => "Array<String>".to_string(),
n if n == "join"
|| n == "trim"
|| n == "upper"
|| n == "lower"
|| n == "replace"
|| n == "format" =>
{
"String".to_string()
}
n if n.ends_with("_list")
|| n.ends_with("_all")
|| n.ends_with("_search")
|| n.ends_with("_query") =>
{
"Array<Record>".to_string()
}
n if n.ends_with("_info")
|| n.ends_with("_status")
|| n.ends_with("_config")
|| n.ends_with("_stats")
|| n.ends_with("_details") =>
{
"Record".to_string()
}
n if n.ends_with("_count")
|| n.ends_with("_size")
|| n.ends_with("_pid")
|| n.ends_with("_port") =>
{
"Int".to_string()
}
n if n.ends_with("_exists")
|| n.ends_with("_check")
|| n.ends_with("_validate")
|| n.ends_with("_enabled")
|| n.ends_with("_available") =>
{
"Bool".to_string()
}
n if n.ends_with("_read")
|| n.ends_with("_get")
|| n.ends_with("_name")
|| n.ends_with("_version")
|| n.ends_with("_path")
|| n.ends_with("_hostname") =>
{
"String".to_string()
}
n if n.starts_with("sys_") || n.starts_with("hw_") || n.starts_with("platform_") => {
"Record".to_string()
}
n if n.starts_with("proc_") && n.ends_with("s") => "Array<Record>".to_string(),
n if n.starts_with("net_") => "Record".to_string(),
_ => "Value".to_string(),
}
}
const SUBJECT_PARAM_DOC: &str =
"the value the builtin acts on; in a pipeline it is the piped value";
const SUBJECT_OPTIONAL_WITHOUT_PARAMS: &[&str] = &[];
fn subject_required_in_tools(sig: &crate::signature::Signature) -> bool {
if matches!(sig.name, "sql" | "sql_value") {
return false;
}
sig.subject_required
|| sig.params.iter().any(|p| p.required)
|| (sig.params.is_empty() && !SUBJECT_OPTIONAL_WITHOUT_PARAMS.contains(&sig.name))
}
fn defaults_to_pipeline(p: &crate::signature::Param) -> bool {
p.doc.contains("omitted uses pipeline input")
}
fn definition_from_signature(
sig: &crate::signature::Signature,
primary_name: &str,
idx: &usize,
index_to_names: &HashMap<usize, Vec<String>>,
) -> BuiltinDefinition {
let aliases: Vec<String> = index_to_names
.get(idx)
.map(|names| {
names
.iter()
.filter(|n| n.as_str() != primary_name)
.cloned()
.collect()
})
.unwrap_or_default();
let subject_param = sig.subject.map(|ty| {
let required = subject_required_in_tools(sig);
(ty.as_str(), required, SUBJECT_PARAM_DOC)
});
let properties: serde_json::Map<String, JsonValue> = subject_param
.iter()
.map(|(ty, _, doc)| {
(
"subject".to_string(),
json!({ "type": ty, "description": doc }),
)
})
.chain(sig.params.iter().map(|p| {
(
p.name.to_string(),
json!({ "type": p.ty.as_str(), "description": p.doc }),
)
}))
.collect();
let required: Vec<&str> = subject_param
.iter()
.filter(|(_, required, _)| *required)
.map(|_| "subject")
.chain(
sig.params
.iter()
.filter(|p| p.required || defaults_to_pipeline(p))
.map(|p| p.name),
)
.collect();
BuiltinDefinition {
name: primary_name.to_string(),
description: sig.doc.to_string(),
category: sig
.category
.map(str::to_string)
.unwrap_or_else(|| categorize_builtin(primary_name)),
signature: sig.render(),
parameters: subject_param
.iter()
.map(|(ty, required, doc)| ParameterDefinition {
name: "subject".to_string(),
param_type: ty.to_string(),
description: doc.to_string(),
required: *required,
default: None,
})
.chain(sig.params.iter().map(|p| ParameterDefinition {
name: p.name.to_string(),
param_type: p.ty.as_str().to_string(),
description: p.doc.to_string(),
required: p.required,
default: None,
}))
.collect(),
return_type: sig.returns.to_string(),
examples: sig
.examples
.iter()
.map(|(code, result)| ExampleDefinition {
description: String::new(),
code: (*code).to_string(),
result: Some((*result).to_string()),
})
.collect(),
aliases: if aliases.is_empty() {
None
} else {
Some(aliases)
},
json_schema: json!({
"type": "object",
"properties": properties,
"required": required,
}),
}
}
fn get_all_builtin_definitions() -> Vec<BuiltinDefinition> {
let mut index_to_names: HashMap<usize, Vec<String>> = HashMap::new();
for (name, &idx) in BUILTIN_LOOKUP.iter() {
index_to_names
.entry(idx)
.or_default()
.push(name.to_string());
}
let enriched: Vec<BuiltinDefinition> = get_builtin_definitions()
.into_iter()
.map(|d| {
match (
crate::signature::signature_of(&d.name),
BUILTIN_LOOKUP.get(d.name.as_str()),
) {
(Some(sig), Some(idx)) => {
definition_from_signature(sig, &d.name, idx, &index_to_names)
}
_ => d,
}
})
.collect();
let enriched_names: std::collections::HashSet<String> =
enriched.iter().map(|b| b.name.clone()).collect();
let mut canonical: HashMap<usize, String> = HashMap::new();
for (idx, names) in &index_to_names {
let mut sorted = names.clone();
sorted.sort_by(|a, b| {
a.contains('-')
.cmp(&b.contains('-'))
.then(a.len().cmp(&b.len()))
.then(a.cmp(b))
});
canonical.insert(*idx, sorted[0].clone());
}
let mut all = enriched;
let mut seen_indices: std::collections::HashSet<usize> = std::collections::HashSet::new();
for name in &enriched_names {
if let Some(&idx) = BUILTIN_LOOKUP.get(name.as_str()) {
seen_indices.insert(idx);
}
}
for (idx, primary_name) in &canonical {
if seen_indices.contains(idx) {
continue;
}
seen_indices.insert(*idx);
let category = categorize_builtin(primary_name);
if let Some(sig) = crate::signature::signature_of(primary_name) {
all.push(definition_from_signature(
sig,
primary_name,
idx,
&index_to_names,
));
continue;
}
let description = describe_builtin_name(primary_name);
let return_type = infer_return_type(primary_name);
let aliases: Vec<String> = index_to_names
.get(idx)
.map(|names| {
names
.iter()
.filter(|n| *n != primary_name)
.cloned()
.collect()
})
.unwrap_or_default();
let signature = format!("{}() -> {}", primary_name, return_type);
all.push(BuiltinDefinition {
name: primary_name.clone(),
description,
category,
signature,
parameters: vec![],
return_type,
examples: vec![],
aliases: if aliases.is_empty() {
None
} else {
Some(aliases)
},
json_schema: json!({
"type": "object",
"properties": {}
}),
});
}
let seen_names: std::collections::HashSet<String> =
all.iter().map(|d| d.name.clone()).collect();
let mut by_callee: HashMap<&str, Vec<&str>> = HashMap::new();
for (name, callee) in crate::builtins::FALLBACK_BUILTINS {
if BUILTIN_LOOKUP.contains_key(*name) {
continue;
}
by_callee.entry(callee).or_default().push(name);
}
for (_, mut names) in by_callee {
names.sort_by(|a, b| {
a.contains('-')
.cmp(&b.contains('-'))
.then(a.len().cmp(&b.len()))
.then(a.cmp(b))
});
let primary = names[0];
if seen_names.contains(primary) {
continue;
}
let aliases: Vec<String> = names[1..].iter().map(|n| (*n).to_string()).collect();
if let Some(sig) = crate::signature::signature_of(primary) {
let mut def = definition_from_signature(sig, primary, &usize::MAX, &HashMap::new());
def.aliases = (!aliases.is_empty()).then_some(aliases);
all.push(def);
continue;
}
let return_type = infer_return_type(primary);
all.push(BuiltinDefinition {
name: primary.to_string(),
description: describe_builtin_name(primary),
category: categorize_builtin(primary),
signature: format!("{primary}() -> {return_type}"),
parameters: vec![],
return_type,
examples: vec![],
aliases: (!aliases.is_empty()).then_some(aliases),
json_schema: json!({ "type": "object", "properties": {} }),
});
}
all.retain(|d| {
let call_head = d.signature.split('(').next().unwrap_or("");
!d.name.contains('-') && !call_head.contains('-')
});
for d in &mut all {
if let Some(aliases) = d.aliases.as_mut() {
aliases.retain(|a| !a.contains('-'));
}
if d.aliases.as_ref().is_some_and(|a| a.is_empty()) {
d.aliases = None;
}
}
all.sort_by(|a, b| a.category.cmp(&b.category).then(a.name.cmp(&b.name)));
for d in &mut all {
normalize_schema_types(&mut d.json_schema);
}
annotate_effects(all)
}
fn def_effect(d: &BuiltinDefinition) -> &str {
d.json_schema
.get("x-effect")
.and_then(|v| v.as_str())
.unwrap_or("pure")
}
fn def_effect_declared(d: &BuiltinDefinition) -> bool {
d.json_schema
.get("x-effect-declared")
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
pub fn ontology_manifest_json() -> JsonValue {
use std::collections::{BTreeMap, BTreeSet};
let defs = get_all_builtin_definitions();
let mut by_cat: BTreeMap<String, (usize, BTreeSet<String>)> = BTreeMap::new();
for d in &defs {
let entry = by_cat
.entry(d.category.clone())
.or_insert((0, BTreeSet::new()));
entry.0 += 1;
entry.1.insert(def_effect(d).to_string());
}
let categories: Vec<JsonValue> = by_cat
.into_iter()
.map(|(cat, (count, effects))| {
json!({
"category": cat,
"builtins": count,
"effects": effects.into_iter().collect::<Vec<_>>(),
})
})
.collect();
let mut modules: Vec<&'static str> = crate::modules::all_modules()
.into_iter()
.map(|(name, _)| name)
.collect();
modules.sort_unstable();
modules.dedup();
json!({
"ontology": "manifest",
"total_builtins": defs.len(),
"categories": categories,
"modules": modules.join(" "),
"effect_legend": [
"pure", "read_local", "write_local", "destructive",
"process", "network", "exec", "privileged"
],
"hint": "ontology_describe(\"<category>\") lists a category's builtins; ontology_describe(\"<builtin>\") returns full detail; modules lists namespaces (call as <module>.<member>); `ish agent schema --module <name>` lists a module's members"
})
}
pub fn builtin_tool_specs() -> Vec<JsonValue> {
get_all_builtin_definitions()
.iter()
.map(|d| {
json!({
"name": d.name,
"description": d.description,
"signature": d.signature,
"effect": def_effect(d),
"returns": crate::shapes::shape_of(&d.name)
.or_else(|| crate::shapes::polymorphic_shape_of(&d.name)),
})
})
.collect()
}
const IMPORT_SYNTAX: &str =
"import \"lib/x.ish\" (the path is relative to the importing file; only pub names are visible)";
pub fn ontology_category_names() -> Vec<String> {
let mut names: Vec<String> = get_all_builtin_definitions()
.into_iter()
.map(|d| d.category)
.collect();
names.sort();
names.dedup();
names
}
pub fn ontology_describe_json(query: &str) -> JsonValue {
let defs = get_all_builtin_definitions();
let resolved = query
.trim()
.split_once('.')
.and_then(|(m, f)| crate::modules::builtin_for_member(m, f));
let underscored = query.trim().replace('-', "_");
let q = resolved.as_deref().unwrap_or(&underscored);
let exact_cat: Vec<JsonValue> = defs
.iter()
.filter(|d| d.category == q)
.map(|d| json!({ "name": d.name, "signature": d.signature, "effect": def_effect(d) }))
.collect();
if !exact_cat.is_empty() {
return json!({ "category": q, "builtins": exact_cat });
}
if q == "import" {
return json!({
"syntax": "import",
"form": IMPORT_SYNTAX,
"description": "Loads another IronShell file. The path is resolved relative to the importing file, not the working directory. Only names marked `pub` in that file are visible to the importer.",
});
}
let found = defs
.iter()
.find(|d| d.name.eq_ignore_ascii_case(q))
.or_else(|| {
defs.iter().find(|d| {
d.aliases
.as_ref()
.is_some_and(|a| a.iter().any(|x| x.eq_ignore_ascii_case(q)))
})
})
.or_else(|| {
let member = crate::modules::builtin_for_member("session", q)?;
defs.iter().find(|d| d.name == member)
});
if let Some(d) = found {
let has_subject = crate::signature::signature_of(&d.name)
.is_some_and(|s| s.subject.is_some() && !s.params.iter().any(|p| p.name == "subject"));
let params: Vec<JsonValue> = d
.parameters
.iter()
.enumerate()
.map(|(i, p)| {
let piped = has_subject && i == 0 && p.name == "subject";
let mut param = json!({
"name": p.name,
"type": p.param_type,
"required": p.required,
"description": p.description,
});
if piped {
param["subject"] = json!(true);
}
param
})
.collect();
let examples: Vec<JsonValue> = d
.examples
.iter()
.map(|e| {
let mut ex = json!({ "code": e.code });
if let Some(result) = &e.result {
ex["result"] = json!(result);
}
if !e.description.is_empty() {
ex["description"] = json!(e.description);
}
ex
})
.collect();
let mut detail = json!({
"builtin": d.name,
"category": d.category,
"signature": d.signature,
"effect": def_effect(d),
"return_type": d.return_type,
"description": d.description,
"parameters": params,
"examples": examples,
"aliases": d.aliases,
"json_schema": d.json_schema.clone(),
});
if let Some(implemented_by) = d.signature.split('(').next().map(str::trim) {
if !implemented_by.is_empty() && implemented_by != d.name {
detail["implemented_by"] = json!(implemented_by);
}
}
if !def_effect_declared(d) {
if let Some(obj) = detail.as_object_mut() {
obj.insert("effect_declared".to_string(), json!(false));
}
}
return detail;
}
let in_cat: Vec<JsonValue> = defs
.iter()
.filter(|d| d.category.eq_ignore_ascii_case(q))
.map(|d| json!({ "name": d.name, "signature": d.signature, "effect": def_effect(d) }))
.collect();
if !in_cat.is_empty() {
return json!({ "category": q, "builtins": in_cat });
}
let near = crate::builtins::did_you_mean(q);
let error = if near.is_empty() {
format!("'{q}' is not a known category or builtin")
} else {
format!(
"'{q}' is not a known category or builtin; did you mean {}?",
near.join(", ")
)
};
json!({
"error": error,
"hint": "call ontology_manifest() to see categories",
"did_you_mean": near,
})
}
fn module_description(name: &str) -> &str {
match name {
"sys" => "System information — hostname, OS, CPU, memory, uptime",
"proc" => "Process management — list, kill, monitor processes",
"fs" => "Filesystem operations — mount, disk, symlink",
"file" => "File I/O — read, write, append, copy, move, edit",
"net" => "Network — ping, DNS, interfaces, connections",
"http" => "HTTP client — get, post, put, delete, request",
"gui" => "GUI automation — screenshot, click, type, window control",
"web" => "Web automation — browser control, scraping",
"crypto" => "Cryptography — hash, encrypt, decrypt, sign, UUID",
"db" => "Database — SQLite queries, connections",
"svc" => "Service management — start, stop, status, enable",
"cron" => "Scheduling — cron jobs, scheduled tasks",
"archive" => "Compression — zip, tar, gzip, extract",
"user" => "User management — create, delete, modify users",
"perm" => "Permissions — chmod, chown, ACLs",
"pkg" => "Package management — install, remove, search, update",
"hw" => "Hardware info — CPU, GPU, disk, memory, battery",
"clip" => "Clipboard — copy, paste, history",
"input" => "User input — prompt, confirm, select",
"ai" => "AI queries — multi-provider LLM completion",
"agent" => "Agent framework — autonomous agents with tools",
"math" => "Mathematics — sqrt, pow, trig, constants",
"str" => "String manipulation — upper, lower, split, join, replace",
"arr" => "Array operations — range, flatten, unique, chunk",
"json" => "JSON — parse, stringify, query, patch",
"mcp" => "Model Context Protocol — tool discovery and invocation",
"shell" => "Shell operations — exec, source, environment",
"a2ui" => "Agent-to-UI — notifications, progress, confirmations",
"a2a" => "Agent-to-Agent — inter-agent messaging",
"nanda" => "NANDA consensus protocol",
"git" => "Git operations — status, commit, push, pull, branch",
"code" => "Code intelligence — analysis, completion, generation",
"project" => "Project management — scaffold, dependencies",
"search" => "Code search — find symbols, references, definitions",
"test" => "Testing — run, discover, analyze test results",
"diag" => "Diagnostics — linting, type checking, profiling",
"refactor" => "Refactoring — rename, extract, inline, organize",
"session" => "Session management — save, restore, history",
"docs" => "Documentation — generate, browse, search docs",
"devenv" => "Dev environment — setup, tools, configuration",
"platform" => "Platform detection — OS, arch, features, libraries",
"rbac" => "Role-based access control — roles, permissions, grants",
"audit" => "Audit logging — log, query, export events",
"sso" => "Single sign-on — OIDC/SAML authentication",
"cluster" => "Distributed computing — cluster, job scheduling",
"nn" => "Neural networks — create, train, predict",
"evo" => "Evolutionary algorithms — population, evolve, fitness",
"rl" => "Reinforcement learning — Q-learning, DQN, policy",
"monitor" => "System monitoring — metrics, alerts, dashboards",
"cloud" => "Cloud platform — deploy, instances, regions",
"repl" => "Remote REPL — serve, connect, broadcast via WebSocket",
"workspace" => "Team workspaces — create, share agents, collaborate",
"marketplace" => "Plugin marketplace — publish, search, install",
"telemetry" => "Usage telemetry — opt-in analytics and reporting",
"docker" => "Docker — containers, images, compose",
"podman" => "Podman — rootless containers",
"container" => "Container abstraction — runtime-agnostic container ops",
"k8s" => "Kubernetes — pods, services, deployments",
"helm" => "Helm — chart management and releases",
"vm" => "Virtual machines — create, start, stop, snapshot",
"hyperv" => "Hyper-V — Windows VM management",
"virsh" => "Libvirt — Linux VM management",
"wsl" => "WSL — Windows Subsystem for Linux",
_ => "Module functions",
}
}
fn get_module_definitions() -> Vec<ModuleDefinition> {
let all = all_modules();
let mut modules = Vec::new();
for (name, value) in &all {
if let Value::Record(map) = value {
let mut functions = Vec::new();
for (method, _builtin_val) in map {
let builtin_name = if let Value::Builtin(bref) = _builtin_val {
bref.name.clone()
} else {
method.clone()
};
functions.push(ModuleFunctionDef {
method: method.clone(),
builtin: builtin_name.clone(),
syntax: format!("{}.{}()", name, method),
});
}
functions.sort_by(|a, b| a.method.cmp(&b.method));
modules.push(ModuleDefinition {
name: name.to_string(),
description: module_description(name).to_string(),
functions,
});
}
}
modules.sort_by(|a, b| a.name.cmp(&b.name));
modules.dedup_by(|a, b| a.name == b.name);
modules
}
fn get_operator_definitions() -> Vec<OperatorDefinition> {
vec![
OperatorDefinition {
symbol: "|".to_string(),
name: "pipe".to_string(),
description: "Pipeline operator - pass left result to right function".to_string(),
precedence: 1,
associativity: "left".to_string(),
operand_types: vec!["Value".to_string(), "Function".to_string()],
},
OperatorDefinition {
symbol: "+".to_string(),
name: "add".to_string(),
description: "Addition or string concatenation".to_string(),
precedence: 5,
associativity: "left".to_string(),
operand_types: vec!["Number".to_string(), "String".to_string()],
},
OperatorDefinition {
symbol: "-".to_string(),
name: "subtract".to_string(),
description: "Subtraction".to_string(),
precedence: 5,
associativity: "left".to_string(),
operand_types: vec!["Number".to_string()],
},
OperatorDefinition {
symbol: "*".to_string(),
name: "multiply".to_string(),
description: "Multiplication".to_string(),
precedence: 6,
associativity: "left".to_string(),
operand_types: vec!["Number".to_string()],
},
OperatorDefinition {
symbol: "/".to_string(),
name: "divide".to_string(),
description: "Division".to_string(),
precedence: 6,
associativity: "left".to_string(),
operand_types: vec!["Number".to_string()],
},
OperatorDefinition {
symbol: "==".to_string(),
name: "equals".to_string(),
description: "Equality comparison".to_string(),
precedence: 3,
associativity: "left".to_string(),
operand_types: vec!["Value".to_string()],
},
OperatorDefinition {
symbol: "!=".to_string(),
name: "not_equals".to_string(),
description: "Inequality comparison".to_string(),
precedence: 3,
associativity: "left".to_string(),
operand_types: vec!["Value".to_string()],
},
OperatorDefinition {
symbol: ">".to_string(),
name: "greater_than".to_string(),
description: "Greater than comparison".to_string(),
precedence: 4,
associativity: "left".to_string(),
operand_types: vec!["Number".to_string()],
},
OperatorDefinition {
symbol: "<".to_string(),
name: "less_than".to_string(),
description: "Less than comparison".to_string(),
precedence: 4,
associativity: "left".to_string(),
operand_types: vec!["Number".to_string()],
},
OperatorDefinition {
symbol: "&&".to_string(),
name: "and".to_string(),
description: "Logical AND".to_string(),
precedence: 2,
associativity: "left".to_string(),
operand_types: vec!["Bool".to_string()],
},
OperatorDefinition {
symbol: "||".to_string(),
name: "or".to_string(),
description: "Logical OR".to_string(),
precedence: 2,
associativity: "left".to_string(),
operand_types: vec!["Bool".to_string()],
},
OperatorDefinition {
symbol: ".".to_string(),
name: "field_access".to_string(),
description: "Access record field".to_string(),
precedence: 10,
associativity: "left".to_string(),
operand_types: vec!["Record".to_string(), "String".to_string()],
},
]
}
fn get_category_info() -> Vec<CategoryInfo> {
let builtins = get_all_builtin_definitions();
let mut categories: HashMap<String, usize> = HashMap::new();
for b in &builtins {
*categories.entry(b.category.clone()).or_insert(0) += 1;
}
let descriptions: HashMap<&str, &str> = [
("FileSystem", "File and directory operations"),
(
"Functional",
"Higher-order functions for data transformation",
),
("Aggregation", "Statistical and aggregation functions"),
("String", "String manipulation functions"),
("Array", "Array and collection operations"),
("Network", "HTTP and network operations"),
("AI", "AI model queries and agent operations"),
("MCP", "Model Context Protocol tools"),
("NeuralNet", "Neural network creation and training"),
("Evolution", "Evolutionary algorithm operations"),
("ReinforcementLearning", "Reinforcement learning agents"),
("Knowledge", "Knowledge graphs and RAG operations"),
("System", "System info — hostname, OS, CPU, memory"),
("Process", "Process management — list, kill, monitor"),
("Service", "Service/daemon management"),
("Database", "Database queries and connections"),
("Crypto", "Cryptography, hashing, and encryption"),
("Container", "Docker/Podman container management"),
("Kubernetes", "Kubernetes cluster operations"),
("Virtualization", "VM and hypervisor management"),
("Cloud", "Cloud infrastructure and IaC"),
("RemoteAccess", "SSH, SCP, and remote connectivity"),
("Security", "Firewall, SELinux, AppArmor"),
("Monitoring", "System monitoring and performance"),
("GUI", "GUI automation — screenshots, clicks"),
("Web", "Web automation and browser control"),
("Archive", "Compression and archive operations"),
("Scheduling", "Cron jobs and scheduled tasks"),
("UserManagement", "User and group management"),
("Package", "Package management"),
("Hardware", "Hardware info and device access"),
("Input", "Clipboard and user input"),
("JSON", "JSON parsing and manipulation"),
("Math", "Mathematical operations and constants"),
("Shell", "Shell operations and command execution"),
("Git", "Git version control operations"),
("Code", "Code intelligence and analysis"),
("Project", "Project scaffolding and management"),
("Search", "Code search and symbol lookup"),
("Testing", "Test execution and analysis"),
("Diagnostics", "Linting, profiling, type checking"),
("Refactoring", "Code refactoring operations"),
("Session", "Session save/restore and history"),
("Documentation", "Documentation generation and browsing"),
("DevEnvironment", "Development environment setup"),
("Platform", "Platform detection and capabilities"),
("A2A", "Agent-to-Agent messaging protocol"),
("A2UI", "Agent-to-UI notification protocol"),
("NANDA", "NANDA consensus protocol"),
("RBAC", "Role-based access control"),
("Audit", "Audit logging and compliance"),
("SSO", "Single sign-on authentication"),
("Cluster", "Distributed computing and job scheduling"),
("REPL", "Remote REPL and WebSocket sessions"),
("Workspace", "Team workspace management"),
("Marketplace", "Plugin marketplace"),
("Telemetry", "Usage telemetry and analytics"),
("Core", "Core language utilities and builtins"),
]
.into_iter()
.collect();
let mut result: Vec<CategoryInfo> = categories
.into_iter()
.map(|(name, count)| CategoryInfo {
description: descriptions.get(name.as_str()).unwrap_or(&"").to_string(),
name,
builtin_count: count,
})
.collect();
result.sort_by(|a, b| a.name.cmp(&b.name));
result
}
use crate::providers::schema::{builtins_to_tools, ToolFormat};
enum SchemaPiece {
Type(&'static str, Option<JsonValue>),
Any,
Lambda,
}
fn schema_piece(part: &str) -> SchemaPiece {
let part = part.trim();
let lower = part.to_ascii_lowercase();
let element = lower
.strip_prefix("array<")
.and_then(|r| r.strip_suffix('>'))
.or_else(|| lower.strip_prefix('[').and_then(|r| r.strip_suffix(']')));
if let Some(element) = element {
let items = match schema_piece(element) {
SchemaPiece::Type(t, items) => {
let mut s = json!({ "type": t });
if let Some(items) = items {
s["items"] = items;
}
Some(s)
}
SchemaPiece::Lambda => Some(json!({ "type": "string" })),
SchemaPiece::Any => None,
};
return SchemaPiece::Type("array", items);
}
match lower.as_str() {
"string" | "str" | "path" | "text" | "uri" | "url" => SchemaPiece::Type("string", None),
"int" | "integer" => SchemaPiece::Type("integer", None),
"float" | "number" | "numeric" => SchemaPiece::Type("number", None),
"bool" | "boolean" => SchemaPiece::Type("boolean", None),
"array" | "list" => SchemaPiece::Type("array", None),
"record" | "object" | "table" | "map" => SchemaPiece::Type("object", None),
"null" | "none" => SchemaPiece::Type("null", None),
"lambda" | "fn" | "function" => SchemaPiece::Lambda,
_ => SchemaPiece::Any,
}
}
fn normalize_schema_types(schema: &mut JsonValue) {
match schema {
JsonValue::Object(map) => {
if let Some(JsonValue::String(ty)) = map.get("type").cloned() {
let mut types: Vec<&'static str> = Vec::new();
let mut items: Option<JsonValue> = None;
let (mut any, mut lambda) = (false, false);
for piece in ty.split('|').map(schema_piece) {
match piece {
SchemaPiece::Type(t, i) => {
if !types.contains(&t) {
types.push(t);
}
items = i;
}
SchemaPiece::Lambda => {
lambda = true;
if !types.contains(&"string") {
types.push("string");
}
}
SchemaPiece::Any => any = true,
}
}
if any || types.is_empty() {
map.remove("type");
} else if types.len() == 1 {
map.insert("type".to_string(), json!(types[0]));
if let (Some(items), false) = (items, map.contains_key("items")) {
map.insert("items".to_string(), items);
}
} else {
map.insert("type".to_string(), json!(types));
}
if lambda {
let note = "IronShell lambda source, e.g. fn(x) => x * 2";
let described = match map.get("description").and_then(JsonValue::as_str) {
Some(d) if !d.is_empty() => format!("{d} ({note})"),
_ => note.to_string(),
};
map.insert("description".to_string(), json!(described));
}
}
for (key, child) in map.iter_mut() {
if !key.starts_with("x-") {
normalize_schema_types(child);
}
}
}
JsonValue::Array(items) => items.iter_mut().for_each(normalize_schema_types),
_ => {}
}
}
fn gemini_schema(schema: &JsonValue) -> JsonValue {
match schema {
JsonValue::Object(map) => {
let mut out = serde_json::Map::new();
for (key, value) in map {
if key.starts_with("x-") {
continue;
}
match (key.as_str(), value) {
("type", JsonValue::String(t)) => {
out.insert(key.clone(), json!(t.to_ascii_uppercase()));
}
("type", JsonValue::Array(types)) => {
let variants: Vec<JsonValue> = types
.iter()
.filter_map(JsonValue::as_str)
.map(|t| json!({ "type": t.to_ascii_uppercase() }))
.collect();
out.insert("anyOf".to_string(), JsonValue::Array(variants));
}
("properties", JsonValue::Object(props)) => {
let props = props
.iter()
.map(|(k, v)| (k.clone(), gemini_schema(v)))
.collect();
out.insert(key.clone(), JsonValue::Object(props));
}
_ => {
out.insert(key.clone(), gemini_schema(value));
}
}
}
JsonValue::Object(out)
}
JsonValue::Array(items) => JsonValue::Array(items.iter().map(gemini_schema).collect()),
other => other.clone(),
}
}
fn truncate_description(text: &str, max: usize) -> String {
if text.chars().count() <= max {
return text.to_string();
}
let kept: String = text.chars().take(max.saturating_sub(1)).collect();
let cut = match kept.rfind(char::is_whitespace) {
Some(i) if i > 0 => &kept[..i],
_ => kept.as_str(),
};
format!("{}\u{2026}", cut.trim_end())
}
fn ontology_tools(ontology: &LanguageOntology, format: ToolFormat, prefix: &str) -> Vec<JsonValue> {
let descriptions: Vec<String> = ontology
.builtins
.iter()
.map(|b| {
if format == ToolFormat::OpenAI {
truncate_description(&b.description, 1024)
} else {
b.description.clone()
}
})
.collect();
builtins_to_tools(
ontology
.builtins
.iter()
.zip(descriptions.iter())
.map(|(b, d)| (b.name.as_str(), d.as_str(), &b.json_schema)),
format,
prefix,
)
}
fn build_openai_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "ironshell_");
json!({
"format": "openai_function_calling",
"version": "2025-01",
"compatible_models": [
"gpt-5", "gpt-5-mini", "gpt-5-turbo",
"gpt-4o", "gpt-4o-mini", "gpt-4-turbo",
"o1", "o1-mini", "o1-preview",
"o3", "o3-mini", "o4-mini"
],
"tools": tools,
"instructions": "Use these tools to execute IronShell operations. Each tool corresponds to an IronShell builtin function."
})
}
fn build_claude_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::Anthropic, "ironshell_");
json!({
"format": "anthropic_tool_use",
"version": "2025-01",
"compatible_models": [
"claude-4.5-opus", "claude-4.5-sonnet", "claude-4.5-haiku",
"claude-4-opus", "claude-4-sonnet", "claude-4-haiku",
"claude-3.5-sonnet-v2", "claude-3.5-haiku",
"claude-3-opus", "claude-3-sonnet", "claude-3-haiku"
],
"tools": tools,
"instructions": "Use these tools to execute IronShell operations. Results are returned as JSON."
})
}
fn build_gemini_schema(ontology: &LanguageOntology) -> JsonValue {
let function_declarations: Vec<JsonValue> =
ontology_tools(ontology, ToolFormat::Google, "ironshell_")
.into_iter()
.map(|mut declaration| {
if let Some(obj) = declaration.as_object_mut() {
let no_params = obj
.get("parameters")
.and_then(|p| p.get("properties"))
.and_then(JsonValue::as_object)
.is_none_or(|props| props.is_empty());
match obj.remove("parameters") {
Some(params) if !no_params => {
obj.insert("parameters".to_string(), gemini_schema(¶ms));
}
_ => {}
}
}
declaration
})
.collect();
json!({
"format": "gemini_function_calling",
"version": "v2",
"compatible_models": [
"gemini-2.5-pro", "gemini-2.5-flash",
"gemini-2.0-flash", "gemini-2.0-flash-thinking",
"gemini-1.5-pro", "gemini-1.5-flash"
],
"function_declarations": function_declarations,
"instructions": "Use these functions to execute IronShell operations."
})
}
fn build_compact_ontology(ontology: &LanguageOntology) -> JsonValue {
let builtins_compact: Vec<JsonValue> = ontology
.builtins
.iter()
.map(|b| {
let mut entry = json!({
"n": b.name,
"d": b.description,
"s": b.signature,
"c": b.category
});
if let Some(aliases) = &b.aliases {
if !aliases.is_empty() {
entry["a"] = json!(aliases);
}
}
entry
})
.collect();
let modules_compact: Vec<JsonValue> = ontology
.modules
.iter()
.map(|m| {
let fns: Vec<String> = m.functions.iter().map(|f| f.method.clone()).collect();
json!({
"n": m.name,
"d": m.description,
"fns": fns
})
})
.collect();
json!({
"lang": "IronShell",
"ver": ontology.language.version,
"ontology_ver": ontology.ontology_version,
"types": ["Int", "Float", "String", "Bool", "Array", "Record", "Lambda", "Table", "Option"],
"ops": ["|", "+", "-", "*", "/", "%", "^", "==", "!=", ">", "<", ">=", "<=", "&&", "||", "!", "."],
"syntax": {
"let": "let x = expr",
"fn": "fn(x) => expr",
"pipe": "a | b | c",
"rec": "{k: v}",
"arr": "[a, b]",
"if": "if cond { a } else { b }",
"match": "match x { p => r }",
"import": IMPORT_SYNTAX
},
"modules": modules_compact,
"builtins": builtins_compact,
"stats": {
"total_builtins": ontology.builtins.len(),
"total_modules": ontology.modules.len(),
"categories": ontology.categories.len()
},
"os_ontology": ontology.os_ontology,
"cli_tools": ontology.cli_tools,
})
}
fn build_llama_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "ironshell_");
json!({
"format": "llama_function_calling",
"version": "4.0",
"compatible_models": [
"llama-4-maverick-17b", "llama-4-maverick-70b", "llama-4-maverick-405b",
"llama-4-scout-17b", "llama-4-scout-109b",
"llama-3.3-70b", "llama-3.2-90b-vision", "llama-3.2-11b-vision",
"llama-3.1-405b", "llama-3.1-70b", "llama-3.1-8b"
],
"tools": tools,
"system_prompt": "You are an assistant with access to IronShell tools. Use them to help users with shell operations, data processing, and AI tasks.",
"instructions": "Call tools using JSON format. Tool names are prefixed with 'ironshell_'."
})
}
fn build_mistral_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "mistral_function_calling",
"version": "v2",
"compatible_models": [
"mistral-large-2501", "mistral-large-latest",
"mistral-medium-2501", "mistral-small-2501",
"codestral-2501", "codestral-latest",
"pixtral-large-2501", "pixtral-12b",
"ministral-8b", "ministral-3b"
],
"tools": tools,
"tool_choice": "auto",
"instructions": "Use these tools to execute IronShell operations. Results are returned as JSON."
})
}
fn build_cohere_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::Cohere, "");
json!({
"format": "cohere_tools",
"version": "v3",
"compatible_models": [
"command-a", "command-a-03-2025",
"command-r-plus", "command-r-plus-08-2024",
"command-r", "command-r7b",
"command-nightly"
],
"tools": tools,
"instructions": "Use these tools to execute IronShell operations. Call tools by name with appropriate parameters."
})
}
fn build_grok_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "grok_function_calling",
"version": "v2",
"compatible_models": [
"grok-3", "grok-3-mini", "grok-3-fast",
"grok-2", "grok-2-mini", "grok-2-vision",
"grok-2-1212"
],
"tools": tools,
"instructions": "Use these tools to execute IronShell operations. Results are returned as JSON."
})
}
fn build_deepseek_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "deepseek_function_calling",
"version": "v4",
"compatible_models": [
"deepseek-chat", "deepseek-reasoner",
"deepseek-v3", "deepseek-v3-0324",
"deepseek-r1", "deepseek-r1-0528",
"deepseek-r1-distill-llama-70b", "deepseek-r1-distill-qwen-32b",
"deepseek-coder-v2", "deepseek-coder-v2.5"
],
"tools": tools,
"reasoning_support": true,
"instructions": "Use these tools to execute IronShell operations. For complex tasks, use chain-of-thought reasoning."
})
}
fn build_bedrock_schema(ontology: &LanguageOntology) -> JsonValue {
let tool_config: Vec<JsonValue> = ontology
.builtins
.iter()
.map(|b| {
json!({
"toolSpec": {
"name": b.name,
"description": b.description,
"inputSchema": {
"json": b.json_schema
}
}
})
})
.collect();
json!({
"format": "bedrock_converse",
"version": "v2",
"compatible_models": [
"anthropic.claude-4-5-opus-20250115-v1:0",
"anthropic.claude-4-5-sonnet-20250115-v1:0",
"anthropic.claude-4-5-haiku-20250115-v1:0",
"anthropic.claude-3-5-sonnet-20241022-v2:0",
"anthropic.claude-3-5-haiku-20241022-v1:0",
"anthropic.claude-3-opus-20240229-v1:0",
"amazon.nova-pro-v1:0",
"amazon.nova-lite-v1:0",
"amazon.nova-micro-v1:0",
"meta.llama3-3-70b-instruct-v1:0",
"meta.llama3-1-405b-instruct-v1:0",
"mistral.mistral-large-2411-v1:0"
],
"toolConfig": {
"tools": tool_config
},
"instructions": "Use toolUse blocks in your response to invoke IronShell tools."
})
}
fn build_azure_openai_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "azure_openai_function_calling",
"version": "2025-01-01-preview",
"compatible_deployments": [
"gpt-5", "gpt-5-mini", "gpt-5-turbo",
"gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4", "gpt-35-turbo",
"o1", "o1-preview", "o1-mini", "o3-mini", "o4-mini"
],
"tools": tools,
"api_version": "2025-01-01-preview",
"instructions": "Use these tools via Azure OpenAI API. Same format as OpenAI but with Azure endpoint."
})
}
fn build_qwen_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "qwen_function_calling",
"version": "v3",
"compatible_models": [
"qwen3-235b-a22b", "qwen3-32b", "qwen3-14b", "qwen3-8b", "qwen3-4b",
"qwen2.5-max", "qwen2.5-plus", "qwen2.5-turbo",
"qwen2.5-72b-instruct", "qwen2.5-coder-32b-instruct",
"qwq-32b", "qvq-72b-preview"
],
"tools": tools,
"instructions": "Use these tools to execute IronShell operations. Compatible with DashScope API."
})
}
fn build_ollama_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "ollama_tools",
"version": "v0.5",
"compatible_models": [
"llama4:scout-17b", "llama4:maverick-17b",
"llama3.3:70b", "llama3.2:3b", "llama3.1:8b", "llama3.1:70b",
"qwen3:32b", "qwen3:8b", "qwen2.5:72b", "qwen2.5-coder:32b",
"mistral-large:123b", "codestral:22b",
"deepseek-r1:70b", "deepseek-r1:32b", "deepseek-r1:14b", "deepseek-r1:7b",
"gemma3:27b", "phi4:14b", "command-a:111b"
],
"tools": tools,
"endpoint": "http://localhost:11434/api/chat",
"instructions": "Use these tools with Ollama's chat API. Requires tool-capable model."
})
}
fn build_vllm_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "vllm_openai_compatible",
"version": "v1",
"compatible_models": ["Any tool-capable model served via vLLM"],
"tools": tools,
"endpoint": "/v1/chat/completions",
"tool_choice_modes": ["auto", "required", "none", {"type": "function", "function": {"name": "specific_tool"}}],
"instructions": "Use OpenAI-compatible format with vLLM server. Ensure model supports function calling."
})
}
fn build_huggingface_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "huggingface_tgi",
"version": "v3",
"compatible_endpoints": ["inference-endpoints", "text-generation-inference", "transformers"],
"recommended_models": [
"meta-llama/Llama-4-Maverick-17B-128E-Instruct",
"meta-llama/Llama-3.3-70B-Instruct",
"Qwen/Qwen3-235B-A22B",
"Qwen/Qwen2.5-72B-Instruct",
"mistralai/Mistral-Large-Instruct-2501",
"deepseek-ai/DeepSeek-V3",
"deepseek-ai/DeepSeek-R1",
"google/gemma-3-27b-it"
],
"tools": tools,
"instructions": "Use with HuggingFace Inference Endpoints or local TGI deployment."
})
}
fn build_openrouter_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "openrouter_unified",
"version": "v2",
"tool_capable_models": [
"openai/gpt-5", "openai/gpt-5-mini", "openai/gpt-4o", "openai/gpt-4o-mini", "openai/o3", "openai/o4-mini",
"anthropic/claude-4.5-opus", "anthropic/claude-4.5-sonnet", "anthropic/claude-4.5-haiku",
"anthropic/claude-3.5-sonnet", "anthropic/claude-3-opus",
"google/gemini-2.5-pro", "google/gemini-2.0-flash",
"meta-llama/llama-4-maverick-70b", "meta-llama/llama-3.3-70b-instruct",
"mistralai/mistral-large-2501", "mistralai/codestral-latest",
"deepseek/deepseek-v3", "deepseek/deepseek-r1",
"qwen/qwen-2.5-72b-instruct", "qwen/qwq-32b-preview",
"x-ai/grok-3", "cohere/command-a"
],
"tools": tools,
"api_base": "https://openrouter.ai/api/v1",
"instructions": "Use OpenAI-compatible format. OpenRouter will route to the specified model provider."
})
}
fn build_kimi_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "kimi_function_calling",
"version": "v2",
"compatible_models": [
"kimi-latest", "kimi-k2",
"moonshot-v1-128k", "moonshot-v1-32k", "moonshot-v1-8k",
"moonshot-v2-128k"
],
"tools": tools,
"api_base": "https://api.moonshot.cn/v1",
"instructions": "Use OpenAI-compatible format with Moonshot API. Kimi excels at long-context tasks."
})
}
fn build_yi_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "yi_function_calling",
"version": "v2",
"compatible_models": [
"yi-large-fc", "yi-large-turbo", "yi-large-preview",
"yi-lightning", "yi-lightning-lite",
"yi-medium-200k", "yi-vision-v2",
"yi-coder-9b", "yi-coder-1.5b"
],
"tools": tools,
"api_base": "https://api.01.ai/v1",
"instructions": "Use OpenAI-compatible format with 01.AI API. Yi models support function calling."
})
}
fn build_glm_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "glm_function_calling",
"version": "v5",
"compatible_models": [
"glm-5", "glm-5-plus", "glm-5-air",
"glm-4-alltools", "glm-4-plus", "glm-4-air", "glm-4-airx", "glm-4-flash",
"glm-4v-plus", "glm-4v-flash",
"codegeex-4", "cogvideo-x"
],
"tools": tools,
"api_base": "https://open.bigmodel.cn/api/paas/v4",
"instructions": "Use OpenAI-compatible format with Zhipu API. GLM-5 supports advanced tools and code generation."
})
}
fn build_reka_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "reka_function_calling",
"version": "v2",
"compatible_models": [
"reka-core-20250115", "reka-core",
"reka-flash-20250115", "reka-flash",
"reka-edge-20250115", "reka-edge",
"reka-vibe"
],
"tools": tools,
"api_base": "https://api.reka.ai/v1",
"multimodal": true,
"instructions": "Use OpenAI-compatible format with Reka API. Reka models are natively multimodal."
})
}
fn build_ai21_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "ai21_function_calling",
"version": "v2",
"compatible_models": [
"jamba-2-large", "jamba-2-mini",
"jamba-1.5-large", "jamba-1.5-mini",
"jamba-instruct"
],
"tools": tools,
"api_base": "https://api.ai21.com/studio/v1",
"instructions": "Use OpenAI-compatible format with AI21 API. Jamba 2 models have 256K context and hybrid architecture."
})
}
fn build_perplexity_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "perplexity_function_calling",
"version": "v2",
"compatible_models": [
"sonar-pro", "sonar",
"sonar-reasoning-pro", "sonar-reasoning",
"sonar-deep-research",
"r1-1776"
],
"tools": tools,
"api_base": "https://api.perplexity.ai",
"online_search": true,
"instructions": "Use OpenAI-compatible format with Perplexity API. Sonar models have built-in web search."
})
}
fn build_together_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "together_function_calling",
"version": "v2",
"tool_capable_models": [
"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
"meta-llama/Llama-3.3-70B-Instruct-Turbo",
"meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo",
"Qwen/Qwen3-235B-A22B-fp8-tput",
"Qwen/Qwen2.5-72B-Instruct-Turbo",
"deepseek-ai/DeepSeek-R1",
"deepseek-ai/DeepSeek-V3",
"mistralai/Mistral-Large-2501-Instruct-FP8",
"google/gemma-3-27b-it"
],
"tools": tools,
"api_base": "https://api.together.xyz/v1",
"instructions": "Use OpenAI-compatible format with Together API. Supports many open-source models."
})
}
fn build_groq_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "groq_function_calling",
"version": "v2",
"compatible_models": [
"llama-4-scout-17b-16e-instruct",
"llama-3.3-70b-versatile", "llama-3.3-70b-specdec",
"llama-3.1-70b-versatile", "llama-3.1-8b-instant",
"qwen-qwq-32b", "deepseek-r1-distill-llama-70b",
"mixtral-8x7b-32768", "gemma2-9b-it"
],
"tools": tools,
"api_base": "https://api.groq.com/openai/v1",
"ultra_low_latency": true,
"instructions": "Use OpenAI-compatible format with Groq API. Ultra-fast inference on LPU hardware."
})
}
fn build_fireworks_schema(ontology: &LanguageOntology) -> JsonValue {
let tools = ontology_tools(ontology, ToolFormat::OpenAI, "");
json!({
"format": "fireworks_function_calling",
"version": "v2",
"tool_capable_models": [
"accounts/fireworks/models/llama4-maverick-instruct-basic",
"accounts/fireworks/models/llama-v3p3-70b-instruct",
"accounts/fireworks/models/llama-v3p1-405b-instruct",
"accounts/fireworks/models/qwen3-235b-a22b",
"accounts/fireworks/models/qwen2p5-72b-instruct",
"accounts/fireworks/models/deepseek-r1",
"accounts/fireworks/models/deepseek-v3",
"accounts/fireworks/models/mistral-large-2501-instruct"
],
"tools": tools,
"api_base": "https://api.fireworks.ai/inference/v1",
"fast_inference": true,
"instructions": "Use OpenAI-compatible format with Fireworks API. Optimized for fast model inference."
})
}
#[cfg(feature = "native")]
pub mod server {
use super::*;
use axum::{
body::Body,
extract::ws::{Message, WebSocket, WebSocketUpgrade},
http::{header, StatusCode},
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
use std::collections::HashMap;
use std::convert::Infallible;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{broadcast, mpsc, RwLock};
use tokio::task::JoinHandle;
use tower_http::cors::{Any, CorsLayer};
use tower_http::timeout::TimeoutLayer;
const MAX_CONCURRENT_REQUESTS: usize = 64;
const MAX_QUEUE_WAIT: Duration = Duration::from_secs(30);
const MAX_CONCURRENT_STREAMS: usize = 16;
fn stream_slots() -> Arc<tokio::sync::Semaphore> {
static SLOTS: std::sync::OnceLock<Arc<tokio::sync::Semaphore>> = std::sync::OnceLock::new();
SLOTS
.get_or_init(|| Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_STREAMS)))
.clone()
}
fn try_take_stream_slot(
slots: &Arc<tokio::sync::Semaphore>,
) -> Option<tokio::sync::OwnedSemaphorePermit> {
slots.clone().try_acquire_owned().ok()
}
fn stream_busy_detail() -> String {
format!("all {MAX_CONCURRENT_STREAMS} streaming evaluation slots are in use; retry later")
}
fn stream_busy_response() -> Response {
(
StatusCode::SERVICE_UNAVAILABLE,
[(header::RETRY_AFTER, "5")],
Json(serde_json::json!({
"error": "busy",
"code": "E_BUDGET_EXCEEDED",
"detail": stream_busy_detail(),
})),
)
.into_response()
}
fn ws_busy_response() -> AgentResponse {
AgentResponse {
code: Some("E_BUDGET_EXCEEDED".to_string()),
hint: Some(
"retry after an in-flight streamed or WebSocket evaluation finishes".to_string(),
),
success: false,
result: None,
error: Some(stream_busy_detail()),
result_type: None,
metadata: None,
}
}
pub(crate) async fn admit(
permits: Arc<tokio::sync::Semaphore>,
max: usize,
wait: Duration,
req: axum::extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
match tokio::time::timeout(wait, permits.acquire_owned()).await {
Ok(Ok(permit)) => {
let res = next.run(req).await;
drop(permit);
res
}
_ => (
StatusCode::SERVICE_UNAVAILABLE,
[(header::RETRY_AFTER, "5")],
Json(serde_json::json!({
"error": "busy",
"code": "E_BUDGET_EXCEEDED",
"detail": format!(
"all {max} request slots stayed busy for {} ms; retry later",
wait.as_millis()
),
})),
)
.into_response(),
}
}
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(tag = "type")]
pub enum WsClientMessage {
#[serde(rename = "execute")]
Execute { id: String, request: AgentRequest },
#[serde(rename = "subscribe")]
Subscribe { channel: String },
#[serde(rename = "unsubscribe")]
Unsubscribe { channel: String },
#[serde(rename = "ping")]
Ping { id: Option<String> },
#[serde(rename = "register")]
Register {
agent_id: String,
capabilities: Vec<String>,
},
#[serde(rename = "agent_message")]
AgentMessage { to: String, payload: JsonValue },
#[serde(rename = "broadcast")]
Broadcast { channel: String, payload: JsonValue },
}
#[derive(Debug, Clone, serde::Serialize)]
#[serde(tag = "type")]
pub enum WsServerMessage {
#[serde(rename = "response")]
Response { id: String, response: AgentResponse },
#[serde(rename = "stream")]
Stream { id: String, event: StreamEvent },
#[serde(rename = "channel")]
Channel { channel: String, payload: JsonValue },
#[serde(rename = "pong")]
Pong { id: Option<String>, timestamp: u64 },
#[serde(rename = "error")]
Error { id: Option<String>, message: String },
#[serde(rename = "agent_message")]
AgentMessage { from: String, payload: JsonValue },
#[serde(rename = "registered")]
Registered { agent_id: String },
#[serde(rename = "agents")]
Agents { agents: Vec<AgentInfo> },
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct AgentInfo {
pub id: String,
pub capabilities: Vec<String>,
pub connected_at: u64,
}
#[derive(Debug)]
pub struct OrchestratorState {
agents: RwLock<HashMap<String, AgentConnection>>,
channels: RwLock<HashMap<String, broadcast::Sender<JsonValue>>>,
task_queue: RwLock<Vec<Task>>,
workflows: RwLock<HashMap<String, Workflow>>,
marketplace: RwLock<Option<RegistryClient>>,
}
#[derive(Debug)]
struct AgentConnection {
info: AgentInfo,
sender: mpsc::Sender<WsServerMessage>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Task {
pub id: String,
pub name: String,
pub payload: JsonValue,
pub status: TaskStatus,
pub assigned_to: Option<String>,
pub created_at: u64,
pub priority: i32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub enum TaskStatus {
Pending,
Assigned,
Running,
Completed,
Failed,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Workflow {
pub id: String,
pub name: String,
pub steps: Vec<WorkflowStep>,
pub current_step: usize,
pub status: WorkflowStatus,
pub context: JsonValue,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct WorkflowStep {
pub name: String,
pub agent_capability: Option<String>,
pub request: AgentRequest,
pub on_success: Option<String>,
pub on_failure: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub enum WorkflowStatus {
Pending,
Running,
Paused,
Completed,
Failed,
}
impl OrchestratorState {
pub fn new() -> Self {
let marketplace_client = RegistryClient::new(None).ok();
Self {
agents: RwLock::new(HashMap::new()),
channels: RwLock::new(HashMap::new()),
task_queue: RwLock::new(Vec::new()),
workflows: RwLock::new(HashMap::new()),
marketplace: RwLock::new(marketplace_client),
}
}
pub async fn register_agent(
&self,
agent_id: String,
capabilities: Vec<String>,
sender: mpsc::Sender<WsServerMessage>,
) {
let info = AgentInfo {
id: agent_id.clone(),
capabilities,
connected_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
};
let conn = AgentConnection { info, sender };
self.agents.write().await.insert(agent_id, conn);
}
pub async fn unregister_agent(&self, agent_id: &str) {
self.agents.write().await.remove(agent_id);
}
pub async fn get_agents(&self) -> Vec<AgentInfo> {
self.agents
.read()
.await
.values()
.map(|c| c.info.clone())
.collect()
}
pub async fn send_to_agent(&self, agent_id: &str, message: WsServerMessage) -> bool {
if let Some(conn) = self.agents.read().await.get(agent_id) {
conn.sender.send(message).await.is_ok()
} else {
false
}
}
pub async fn broadcast_to_channel(&self, channel: &str, payload: JsonValue) {
let channels = self.channels.read().await;
if let Some(tx) = channels.get(channel) {
if tx.send(payload).is_err() {
drop(channels);
self.prune_idle_channels().await;
}
}
}
pub async fn prune_idle_channels(&self) {
self.channels
.write()
.await
.retain(|_, tx| tx.receiver_count() > 0);
}
pub async fn subscribe_to_channel(&self, channel: &str) -> broadcast::Receiver<JsonValue> {
let mut channels = self.channels.write().await;
if let Some(tx) = channels.get(channel) {
tx.subscribe()
} else {
let (tx, rx) = broadcast::channel(100);
channels.insert(channel.to_string(), tx);
rx
}
}
pub async fn add_task(&self, task: Task) {
self.task_queue.write().await.push(task);
}
pub async fn get_pending_tasks(&self) -> Vec<Task> {
self.task_queue
.read()
.await
.iter()
.filter(|t| t.status == TaskStatus::Pending)
.cloned()
.collect()
}
pub async fn claim_task(&self, task_id: &str, agent_id: &str) -> Option<Task> {
let mut queue = self.task_queue.write().await;
if let Some(task) = queue
.iter_mut()
.find(|t| t.id == task_id && t.status == TaskStatus::Pending)
{
task.status = TaskStatus::Assigned;
task.assigned_to = Some(agent_id.to_string());
Some(task.clone())
} else {
None
}
}
pub async fn complete_task(&self, task_id: &str, success: bool) {
let mut queue = self.task_queue.write().await;
if let Some(task) = queue.iter_mut().find(|t| t.id == task_id) {
task.status = if success {
TaskStatus::Completed
} else {
TaskStatus::Failed
};
}
}
pub async fn add_workflow(&self, workflow: Workflow) {
self.workflows
.write()
.await
.insert(workflow.id.clone(), workflow);
}
pub async fn get_workflow(&self, workflow_id: &str) -> Option<Workflow> {
self.workflows.read().await.get(workflow_id).cloned()
}
}
impl Default for OrchestratorState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct StreamEvent {
pub event: String,
pub data: JsonValue,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
}
impl StreamEvent {
pub fn start(message: &str) -> Self {
Self {
event: "start".to_string(),
data: json!({ "message": message }),
id: None,
}
}
pub fn progress(current: usize, total: usize, message: &str) -> Self {
Self {
event: "progress".to_string(),
data: json!({
"current": current,
"total": total,
"percentage": if total > 0 { (current as f64 / total as f64) * 100.0 } else { 0.0 },
"message": message
}),
id: None,
}
}
pub fn data(result: JsonValue, result_type: Option<&str>) -> Self {
Self {
event: "data".to_string(),
data: json!({
"result": result,
"result_type": result_type
}),
id: None,
}
}
pub fn complete(result: JsonValue, result_type: Option<&str>) -> Self {
Self {
event: "complete".to_string(),
data: json!({
"success": true,
"result": result,
"result_type": result_type
}),
id: None,
}
}
pub fn error(message: &str) -> Self {
Self {
event: "error".to_string(),
data: json!({
"success": false,
"error": message
}),
id: None,
}
}
pub fn chunk(seq: usize, rows: &[JsonValue], total: usize) -> Self {
Self {
event: "chunk".to_string(),
data: json!({ "seq": seq, "rows": rows, "total": total }),
id: None,
}
}
pub fn to_sse(&self) -> String {
let mut output = String::new();
output.push_str(&format!("event: {}\n", self.event));
if let Some(ref id) = self.id {
output.push_str(&format!("id: {}\n", id));
}
output.push_str(&format!(
"data: {}\n\n",
serde_json::to_string(&self.data).unwrap_or_default()
));
output
}
}
#[derive(Debug, Clone)]
pub struct AgentApiConfig {
pub host: String,
pub port: u16,
pub enable_cors: bool,
pub auth_token: Option<String>,
pub request_timeout_secs: u64,
}
impl Default for AgentApiConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 3002,
enable_cors: true,
auth_token: None,
request_timeout_secs: 300,
}
}
}
fn generate_api_token() -> Result<String> {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
let mut bytes = [0u8; 32];
crate::safety::ic_fill_random("agent_api", &mut bytes)?;
Ok(URL_SAFE_NO_PAD.encode(bytes))
}
fn tokens_match(presented: &str, expected: &str) -> bool {
let (a, b) = (presented.as_bytes(), expected.as_bytes());
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
pub async fn start_agent_api_server(config: AgentApiConfig) -> Result<()> {
let state = Arc::new(OrchestratorState::new());
let long_lived = Router::new()
.route("/api/v1/stream/execute", post(handle_stream_execute))
.route("/api/v1/stream/pipeline", post(handle_stream_pipeline))
.route("/api/v1/stream/eval", post(handle_stream_eval))
.route(
"/api/v1/ws",
get({
let state = Arc::clone(&state);
move |ws| handle_websocket(ws, state)
}),
);
let mut app = Router::new()
.route("/api/v1/execute", post(handle_execute))
.route("/api/v1/call/:builtin", post(handle_call))
.route("/api/v1/pipeline", post(handle_pipeline))
.route("/api/v1/eval", post(handle_eval))
.route(
"/api/v1/orchestration/agents",
get({
let state = Arc::clone(&state);
move || handle_list_agents(state)
}),
)
.route(
"/api/v1/orchestration/tasks",
get({
let state = Arc::clone(&state);
move || handle_list_tasks(state)
}),
)
.route(
"/api/v1/orchestration/tasks",
post({
let state = Arc::clone(&state);
move |body| handle_create_task(body, state)
}),
)
.route(
"/api/v1/orchestration/workflows",
post({
let state = Arc::clone(&state);
move |body| handle_create_workflow(body, state)
}),
)
.route(
"/api/v1/orchestration/workflows/:id",
get({
let state = Arc::clone(&state);
move |path| handle_get_workflow(path, state)
}),
)
.route(
"/api/v1/orchestration/workflows/:id/cancel",
post({
let state = Arc::clone(&state);
move |path| handle_cancel_workflow(path, state)
}),
)
.route(
"/api/v1/orchestration/workflows",
get({
let state = Arc::clone(&state);
move || handle_list_workflows(state)
}),
)
.route(
"/api/v1/orchestration/metrics",
get({
let state = Arc::clone(&state);
move || handle_orchestration_metrics(state)
}),
)
.route(
"/api/v1/marketplace/publish",
post({
let state = Arc::clone(&state);
move |body| handle_publish_agent(body, state)
}),
)
.route(
"/api/v1/marketplace/search",
get({
let state = Arc::clone(&state);
move |params| handle_marketplace_search(params, state)
}),
)
.route(
"/api/v1/marketplace/agents",
get({
let state = Arc::clone(&state);
move || handle_marketplace_list(state)
}),
)
.route(
"/api/v1/marketplace/install",
post({
let state = Arc::clone(&state);
move |body| handle_marketplace_install(body, state)
}),
)
.route(
"/api/v1/marketplace/uninstall",
post({
let state = Arc::clone(&state);
move |body| handle_marketplace_uninstall(body, state)
}),
)
.route("/api/v1/schema", get(handle_schema))
.route("/api/v1/schema/:format", get(handle_schema_format))
.route("/api/v1/builtins", get(handle_list_builtins))
.route("/api/v1/builtins/:name", get(handle_describe_builtin))
.route("/api/v1/types", get(handle_types))
.route("/health", get(handle_health));
EVAL_DEADLINE_SECS.store(
match config.request_timeout_secs {
0 => 0,
secs => (secs * 9 / 10).max(1),
},
std::sync::atomic::Ordering::Relaxed,
);
if config.request_timeout_secs > 0 {
app = app.layer(TimeoutLayer::new(Duration::from_secs(
config.request_timeout_secs,
)));
}
let mut app = app.merge(long_lived);
let permits = Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_REQUESTS));
app = app.layer(axum::middleware::from_fn(move |req, next| {
admit(
permits.clone(),
MAX_CONCURRENT_REQUESTS,
MAX_QUEUE_WAIT,
req,
next,
)
}));
app = app.layer(axum::middleware::from_fn(
|req: axum::extract::Request, next: axum::middleware::Next| async move {
let mut res = next.run(req).await;
let h = res.headers_mut();
h.insert(
"x-content-type-options",
header::HeaderValue::from_static("nosniff"),
);
h.insert("x-frame-options", header::HeaderValue::from_static("DENY"));
h.insert(
"content-security-policy",
header::HeaderValue::from_static("default-src 'none'; frame-ancestors 'none'"),
);
h.insert(
"referrer-policy",
header::HeaderValue::from_static("no-referrer"),
);
h.insert(
"cache-control",
header::HeaderValue::from_static("no-store"),
);
res
},
));
let token = Arc::new(match config.auth_token.clone() {
Some(t) if !t.trim().is_empty() => t,
_ => generate_api_token()?,
});
let auth_token = Arc::clone(&token);
app = app.layer(axum::middleware::from_fn(
move |req: axum::extract::Request, next: axum::middleware::Next| {
let expected = Arc::clone(&auth_token);
async move {
if req.uri().path() == "/health" {
return next.run(req).await;
}
let presented = req
.headers()
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.map(str::trim)
.unwrap_or("");
if tokens_match(presented, &expected) {
next.run(req).await
} else {
(
StatusCode::UNAUTHORIZED,
[(header::WWW_AUTHENTICATE, "Bearer")],
Json(serde_json::json!({
"error": "unauthorized",
"detail": "Send `Authorization: Bearer <token>`. \
The token is printed when the server starts.",
})),
)
.into_response()
}
}
},
));
if config.enable_cors {
app = app.layer(
CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any),
);
}
let bind = format!("{}:{}", config.host, config.port);
let addr: std::net::SocketAddr = bind.parse().map_err(|e| {
anyhow::anyhow!(
"invalid bind address {bind:?}: {e}. \
Use a literal IP such as 127.0.0.1 or 0.0.0.0, not a hostname."
)
})?;
if !addr.ip().is_loopback() {
println!(
"⚠Binding {} — this API evaluates arbitrary code, so it is now\n\
âš reachable by anything that can route to this host. The bearer\n\
âš token below is the only thing standing in front of it.",
addr.ip()
);
}
println!("🤖 IronShell Agent API starting on http://{}", addr);
if config.auth_token.is_none() {
println!(" Auth token (generated): {}", token);
} else {
println!(" Auth token: (from configuration)");
}
println!(" Send it as: Authorization: Bearer <token>");
match crate::safety::current_mode() {
crate::safety::Mode::Agent => {
println!(" Safety: agent mode — dangerous effect classes default-denied");
println!(
" Writes confined to {}",
crate::safety::workspace_root().display()
);
}
other => {
println!(
"⚠Safety: {other:?} mode (IRONSHELL_MODE set explicitly) — the effect
âš gate and the workspace jail are OFF for a surface that evaluates
âš arbitrary code. Unset IRONSHELL_MODE to get the agent profile."
);
}
}
println!(" Supports: OpenAI, Claude, Gemini, Llama, Mistral, Cohere, Grok, DeepSeek,");
println!(" Bedrock, Azure, Qwen, Ollama, vLLM, HuggingFace, OpenRouter,");
println!(" Kimi, Yi, GLM, Reka, AI21, Perplexity, Together, Groq, Fireworks");
println!();
println!("Endpoints:");
println!(" POST /api/v1/execute - Execute any request");
println!(" POST /api/v1/call/:builtin - Call a single builtin");
println!(" POST /api/v1/pipeline - Execute a pipeline");
println!(" POST /api/v1/eval - Evaluate raw code");
println!(" POST /api/v1/stream/execute - Stream execution (SSE)");
println!(" POST /api/v1/stream/pipeline - Stream pipeline (SSE)");
println!(" POST /api/v1/stream/eval - Stream eval (SSE)");
println!(" GET /api/v1/ws - WebSocket (real-time bidirectional)");
println!(" GET /api/v1/orchestration/* - Agent orchestration APIs");
println!(" POST /api/v1/marketplace/* - Agent marketplace APIs");
println!(" GET /api/v1/schema - Get language ontology");
println!(" GET /api/v1/schema/:format - Get schema for AI provider");
println!(" GET /api/v1/builtins - List all builtins");
println!(" GET /api/v1/builtins/:name - Describe a builtin");
println!(" GET /api/v1/types - Get type information");
let listener = tokio::net::TcpListener::bind(addr).await?;
crate::serve::serve_until(
listener,
app,
crate::serve::shutdown_signal(),
crate::serve::DRAIN,
)
.await?;
Ok(())
}
static EVAL_DEADLINE_SECS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
fn eval_deadline() -> Option<std::time::Duration> {
match EVAL_DEADLINE_SECS.load(std::sync::atomic::Ordering::Relaxed) {
0 => None,
secs => Some(std::time::Duration::from_secs(secs)),
}
}
async fn run_blocking(
request: AgentRequest,
deadline: Option<std::time::Duration>,
) -> (StatusCode, Json<AgentResponse>) {
match evaluate_off_thread(request, deadline).await {
Ok(response) => {
if response.success {
(StatusCode::OK, Json(response))
} else {
(StatusCode::BAD_REQUEST, Json(response))
}
}
Err(error) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(handler_failure(error)),
),
}
}
async fn evaluate_off_thread(
request: AgentRequest,
deadline: Option<std::time::Duration>,
) -> Result<AgentResponse, String> {
tokio::task::spawn_blocking(move || {
let _guard = deadline.map(crate::safety::enter_deadline);
process_request(&request)
})
.await
.map_err(|e| e.to_string())
}
fn handler_failure(error: String) -> AgentResponse {
AgentResponse {
code: None,
hint: None,
success: false,
result: None,
error: Some(format!("request handler failed: {error}")),
result_type: None,
metadata: None,
}
}
async fn handle_execute(body: String) -> (StatusCode, Json<AgentResponse>) {
match crate::agent_api::parse_request(&body) {
Ok(request) => run_blocking(request, eval_deadline()).await,
Err(refusal) => (StatusCode::BAD_REQUEST, Json(refusal)),
}
}
async fn handle_call(
axum::extract::Path(builtin): axum::extract::Path<String>,
Json(args): Json<JsonValue>,
) -> impl IntoResponse {
if !builtin.contains('.') {
if let Some(refusal) = crate::agent_api::unknown_builtin_refusal(&builtin) {
return (StatusCode::NOT_FOUND, Json(refusal));
}
}
run_blocking(AgentRequest::Call { builtin, args }, eval_deadline()).await
}
async fn handle_pipeline(Json(body): Json<JsonValue>) -> impl IntoResponse {
let steps: Vec<PipelineStep> = serde_json::from_value(
body.get("steps")
.cloned()
.unwrap_or(JsonValue::Array(vec![])),
)
.unwrap_or_default();
let input = body.get("input").cloned();
run_blocking(AgentRequest::Pipeline { steps, input }, eval_deadline()).await
}
async fn handle_eval(Json(body): Json<JsonValue>) -> impl IntoResponse {
let code = match crate::agent_api::eval_code_from_body(&body) {
Ok(code) => code,
Err(refusal) => return (StatusCode::BAD_REQUEST, Json(refusal)),
};
run_blocking(AgentRequest::Eval { code }, eval_deadline()).await
}
async fn handle_stream_execute(body: String) -> Response {
let request = match crate::agent_api::parse_request(&body) {
Ok(request) => request,
Err(refusal) => return (StatusCode::BAD_REQUEST, Json(refusal)).into_response(),
};
let Some(slot) = try_take_stream_slot(&stream_slots()) else {
return stream_busy_response();
};
let deadline = eval_deadline();
create_sse_response(async move {
let _slot = slot;
match evaluate_off_thread(request, deadline).await {
Ok(response) => stream_events_from_response(response, 50),
Err(error) => vec![StreamEvent::error(&format!(
"request handler failed: {error}"
))],
}
})
.into_response()
}
pub fn stream_events_from_response(
response: AgentResponse,
chunk_rows: usize,
) -> Vec<StreamEvent> {
let mut events = vec![StreamEvent::start("Processing request...")];
if !response.success {
events.push(StreamEvent::error(
&response
.error
.unwrap_or_else(|| "Unknown error".to_string()),
));
return events;
}
let result = response.result.unwrap_or(JsonValue::Null);
match &result {
JsonValue::Array(rows) if rows.len() > chunk_rows.max(1) => {
let n = chunk_rows.max(1);
let total = rows.len();
for (seq, chunk) in rows.chunks(n).enumerate() {
events.push(StreamEvent::chunk(seq, chunk, total));
}
events.push(StreamEvent::complete(
json!({ "streamed_rows": total }),
response.result_type.as_deref(),
));
}
_ => events.push(StreamEvent::complete(
result,
response.result_type.as_deref(),
)),
}
events
}
pub fn stream_pipeline_events(
steps: &[PipelineStep],
input: Option<&JsonValue>,
deadline: Option<std::time::Duration>,
) -> Vec<StreamEvent> {
let total = steps.len();
let mut events = vec![StreamEvent::start(&format!(
"Starting pipeline with {} steps",
total
))];
let _guard = deadline.map(crate::safety::enter_deadline);
let outcome = run_pipeline_steps(steps, input, |i| {
events.push(StreamEvent::progress(
i + 1,
total,
&format!("Completed step {}/{}: {}", i + 1, total, steps[i].builtin),
));
});
match outcome {
Ok((value, code)) => {
let response = response_from_value(value, &code);
events.push(StreamEvent::complete(
response.result.unwrap_or(JsonValue::Null),
response.result_type.as_deref(),
));
}
Err(failure) => {
let message = match failure.step {
Some(i) => format!(
"Step {}/{} ({}) failed: {}",
i + 1,
total,
steps[i].builtin,
failure.message
),
None => failure.message,
};
events.push(StreamEvent::error(&message));
}
}
events
}
async fn handle_stream_pipeline(Json(body): Json<JsonValue>) -> Response {
let Some(slot) = try_take_stream_slot(&stream_slots()) else {
return stream_busy_response();
};
let steps: Vec<PipelineStep> = serde_json::from_value(
body.get("steps")
.cloned()
.unwrap_or(JsonValue::Array(vec![])),
)
.unwrap_or_default();
let input = body.get("input").cloned();
let deadline = eval_deadline();
create_sse_response(async move {
let _slot = slot;
match tokio::task::spawn_blocking(move || {
stream_pipeline_events(&steps, input.as_ref(), deadline)
})
.await
{
Ok(events) => events,
Err(e) => vec![StreamEvent::error(&format!("request handler failed: {e}"))],
}
})
.into_response()
}
pub fn stream_eval_events(
code: &str,
deadline: Option<std::time::Duration>,
) -> Vec<StreamEvent> {
let mut events = vec![StreamEvent::start("Evaluating code...")];
let _guard = deadline.map(crate::safety::enter_deadline);
let mut env = crate::modules::env_with_modules();
let mut items: Vec<JsonValue> = Vec::new();
let started = std::time::Instant::now();
let result = {
let mut emit = |v: crate::value::Value| items.push(v.to_json());
crate::eval::eval_stream(code, &mut env, &mut emit)
};
crate::record::note(
crate::record::Surface::Api,
code,
&result,
started.elapsed(),
);
match result {
Ok(n) => {
for (seq, chunk) in items.chunks(50).enumerate() {
events.push(StreamEvent::chunk(seq, chunk, n));
}
events.push(StreamEvent::complete(
json!({ "streamed_items": n }),
Some("stream"),
));
}
Err(e) => events.push(StreamEvent::error(&format!("{}", e))),
}
events
}
async fn handle_stream_eval(Json(body): Json<JsonValue>) -> Response {
let code = match crate::agent_api::eval_code_from_body(&body) {
Ok(code) => code,
Err(refusal) => return (StatusCode::BAD_REQUEST, Json(refusal)).into_response(),
};
let Some(slot) = try_take_stream_slot(&stream_slots()) else {
return stream_busy_response();
};
let deadline = eval_deadline();
create_sse_response(async move {
let _slot = slot;
match tokio::task::spawn_blocking(move || stream_eval_events(&code, deadline)).await {
Ok(events) => events,
Err(e) => vec![StreamEvent::error(&format!("request handler failed: {e}"))],
}
})
.into_response()
}
fn create_sse_response<F>(event_generator: F) -> impl IntoResponse
where
F: std::future::Future<Output = Vec<StreamEvent>> + Send + 'static,
{
let stream = async_stream::stream! {
let events = event_generator.await;
for event in events {
yield Ok::<_, Infallible>(event.to_sse());
}
};
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/event-stream")
.header(header::CACHE_CONTROL, "no-cache")
.header("X-Accel-Buffering", "no")
.body(Body::from_stream(stream))
.unwrap()
}
async fn handle_schema(
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
) -> impl IntoResponse {
let request = AgentRequest::Schema {
format: SchemaFormat::Ontology,
module: params.get("module").cloned(),
q: params.get("q").cloned(),
};
schema_response(&request)
}
async fn handle_schema_format(
axum::extract::Path(format): axum::extract::Path<String>,
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
) -> impl IntoResponse {
let Some(schema_format) = parse_schema_format(&format) else {
return (
StatusCode::BAD_REQUEST,
Json(json!({
"success": false,
"error": format!("Unknown schema format: '{format}'. Supported: {SCHEMA_FORMATS_HINT}"),
"metadata": { "code": "E_BAD_ARG" },
})),
);
};
let request = AgentRequest::Schema {
format: schema_format,
module: params.get("module").cloned(),
q: params.get("q").cloned(),
};
schema_response(&request)
}
fn schema_response(request: &AgentRequest) -> (StatusCode, Json<JsonValue>) {
let response = process_request(request);
let status = if response.success {
StatusCode::OK
} else {
StatusCode::BAD_REQUEST
};
(status, Json(json!(response)))
}
async fn handle_list_builtins(
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
) -> impl IntoResponse {
let category = params.get("category").cloned();
let request = AgentRequest::ListBuiltins { category };
Json(process_request(&request))
}
async fn handle_describe_builtin(
axum::extract::Path(name): axum::extract::Path<String>,
) -> impl IntoResponse {
let request = AgentRequest::Describe { builtin: name };
let response = process_request(&request);
if response.success {
(StatusCode::OK, Json(response))
} else {
(StatusCode::NOT_FOUND, Json(response))
}
}
async fn handle_types() -> impl IntoResponse {
let request = AgentRequest::TypeInfo { type_name: None };
Json(process_request(&request))
}
async fn handle_health() -> impl IntoResponse {
Json(json!({
"status": "healthy",
"service": "ironshell-agent-api",
"version": env!("CARGO_PKG_VERSION"),
"features": {
"websocket": true,
"sse_streaming": true,
"orchestration": true
},
"supported_agents": [
"openai", "azure_openai", "claude", "gemini",
"llama", "mistral", "cohere", "grok", "deepseek",
"bedrock", "qwen", "ollama", "vllm", "huggingface", "openrouter",
"kimi", "yi", "glm", "reka", "ai21", "perplexity", "together", "groq", "fireworks"
],
"schema_formats": {
"openai_family": ["openai", "gpt", "chatgpt", "azure", "azure_openai"],
"anthropic": ["claude", "anthropic"],
"google": ["gemini", "google"],
"meta": ["llama", "meta", "llama3"],
"mistral": ["mistral", "codestral", "pixtral"],
"cohere": ["cohere", "command", "command-r"],
"xai": ["grok", "xai"],
"deepseek": ["deepseek", "deepseek-r1"],
"cloud": ["bedrock", "aws", "amazon"],
"alibaba": ["qwen", "alibaba", "dashscope"],
"chinese_ai": ["kimi", "moonshot", "yi", "01ai", "glm", "chatglm", "zhipu"],
"inference_platforms": ["together", "together-ai", "groq", "fireworks", "fireworks-ai"],
"additional_sota": ["reka", "ai21", "jamba", "perplexity", "sonar"],
"local": ["ollama", "lmstudio", "vllm"],
"platforms": ["huggingface", "hf", "tgi", "openrouter"],
"standard": ["json", "jsonschema", "ontology", "compact"]
}
}))
}
async fn forward_channel(
mut rx: broadcast::Receiver<JsonValue>,
out: mpsc::Sender<WsServerMessage>,
channel: String,
) {
loop {
match rx.recv().await {
Ok(payload) => {
let msg = WsServerMessage::Channel {
channel: channel.clone(),
payload,
};
if out.send(msg).await.is_err() {
break;
}
}
Err(broadcast::error::RecvError::Lagged(_)) => continue,
Err(broadcast::error::RecvError::Closed) => break,
}
}
}
#[derive(Default)]
struct SubscriptionTasks {
tasks: HashMap<String, JoinHandle<()>>,
}
impl SubscriptionTasks {
fn insert(&mut self, channel: String, task: JoinHandle<()>) {
if let Some(previous) = self.tasks.insert(channel, task) {
previous.abort();
}
}
async fn remove(&mut self, channel: &str) -> bool {
match self.tasks.remove(channel) {
Some(task) => {
stop_task(task).await;
true
}
None => false,
}
}
async fn stop_all(&mut self) {
let tasks: Vec<JoinHandle<()>> = self.tasks.drain().map(|(_, t)| t).collect();
for task in tasks {
stop_task(task).await;
}
}
#[cfg(test)]
fn len(&self) -> usize {
self.tasks.len()
}
}
impl Drop for SubscriptionTasks {
fn drop(&mut self) {
for (_, task) in self.tasks.drain() {
task.abort();
}
}
}
async fn stop_task(task: JoinHandle<()>) {
task.abort();
let _ = task.await;
}
async fn handle_websocket(
ws: WebSocketUpgrade,
state: Arc<OrchestratorState>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| handle_websocket_connection(socket, state))
}
async fn handle_websocket_connection(socket: WebSocket, state: Arc<OrchestratorState>) {
use futures_util::{SinkExt, StreamExt};
let (mut sender, mut receiver) = socket.split();
let (tx, mut rx) = mpsc::channel::<WsServerMessage>(100);
let mut agent_id: Option<String> = None;
let mut subscriptions = SubscriptionTasks::default();
let send_task = tokio::spawn(async move {
while let Some(msg) = rx.recv().await {
if let Ok(json) = serde_json::to_string(&msg) {
if sender.send(Message::Text(json)).await.is_err() {
break;
}
}
}
});
while let Some(result) = receiver.next().await {
match result {
Ok(Message::Text(text)) => {
if let Ok(msg) = serde_json::from_str::<WsClientMessage>(&text) {
let response = match msg {
WsClientMessage::Execute { id, request } => {
let response = match try_take_stream_slot(&stream_slots()) {
Some(_slot) => {
match evaluate_off_thread(request, eval_deadline()).await {
Ok(response) => response,
Err(error) => handler_failure(error),
}
}
None => ws_busy_response(),
};
Some(WsServerMessage::Response { id, response })
}
WsClientMessage::Ping { id } => {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
Some(WsServerMessage::Pong { id, timestamp })
}
WsClientMessage::Register {
agent_id: aid,
capabilities,
} => {
agent_id = Some(aid.clone());
state
.register_agent(aid.clone(), capabilities.clone(), tx.clone())
.await;
state
.broadcast_to_channel(
"agents",
json!({
"type": "agent_connected",
"agent": {
"id": aid,
"capabilities": capabilities,
"status": "online",
"connectedAt": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}
}),
)
.await;
Some(WsServerMessage::Registered { agent_id: aid })
}
WsClientMessage::AgentMessage { to, payload } => {
if let Some(ref from) = agent_id {
let msg = WsServerMessage::AgentMessage {
from: from.clone(),
payload,
};
state.send_to_agent(&to, msg).await;
}
None
}
WsClientMessage::Broadcast { channel, payload } => {
state.broadcast_to_channel(&channel, payload).await;
None
}
WsClientMessage::Subscribe { channel } => {
subscriptions.remove(&channel).await;
let rx = state.subscribe_to_channel(&channel).await;
let task =
tokio::spawn(forward_channel(rx, tx.clone(), channel.clone()));
subscriptions.insert(channel, task);
None
}
WsClientMessage::Unsubscribe { channel } => {
subscriptions.remove(&channel).await;
state.prune_idle_channels().await;
None
}
};
if let Some(response) = response {
if tx.send(response).await.is_err() {
break;
}
}
}
}
Ok(Message::Close(_)) => break,
Err(_) => break,
_ => {}
}
}
if let Some(ref aid) = agent_id {
state
.broadcast_to_channel(
"agents",
json!({
"type": "agent_disconnected",
"agentId": aid
}),
)
.await;
state.unregister_agent(aid).await;
}
subscriptions.stop_all().await;
state.prune_idle_channels().await;
send_task.abort();
}
async fn handle_list_agents(state: Arc<OrchestratorState>) -> impl IntoResponse {
let agents = state.get_agents().await;
Json(json!({
"success": true,
"agents": agents
}))
}
async fn handle_list_tasks(state: Arc<OrchestratorState>) -> impl IntoResponse {
let tasks = state.get_pending_tasks().await;
Json(json!({
"success": true,
"tasks": tasks
}))
}
async fn handle_create_task(
Json(body): Json<JsonValue>,
state: Arc<OrchestratorState>,
) -> impl IntoResponse {
let task = Task {
id: uuid::Uuid::new_v4().to_string(),
name: body
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unnamed")
.to_string(),
payload: body.get("payload").cloned().unwrap_or(JsonValue::Null),
status: TaskStatus::Pending,
assigned_to: None,
created_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
priority: body.get("priority").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
};
let task_id = task.id.clone();
state.add_task(task).await;
(
StatusCode::CREATED,
Json(json!({
"success": true,
"task_id": task_id
})),
)
}
async fn handle_create_workflow(
Json(body): Json<JsonValue>,
state: Arc<OrchestratorState>,
) -> impl IntoResponse {
let steps: Vec<WorkflowStep> = match body.get("steps") {
None => Vec::new(),
Some(value) => match serde_json::from_value(value.clone()) {
Ok(steps) => steps,
Err(e) => {
return (
StatusCode::BAD_REQUEST,
Json(json!({
"success": false,
"error": format!("invalid steps: {}", e)
})),
);
}
},
};
let workflow = Workflow {
id: uuid::Uuid::new_v4().to_string(),
name: body
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unnamed")
.to_string(),
steps,
current_step: 0,
status: WorkflowStatus::Pending,
context: body
.get("context")
.cloned()
.unwrap_or(JsonValue::Object(Default::default())),
};
let workflow_id = workflow.id.clone();
let workflow_name = workflow.name.clone();
let total_steps = workflow.steps.len();
state.add_workflow(workflow).await;
state
.broadcast_to_channel(
"workflows",
json!({
"type": "workflow_created",
"workflow": {
"id": workflow_id,
"name": workflow_name,
"status": "pending",
"currentStep": 0,
"totalSteps": total_steps,
"startedAt": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64,
"steps": []
}
}),
)
.await;
(
StatusCode::CREATED,
Json(json!({
"success": true,
"workflow_id": workflow_id
})),
)
}
async fn handle_get_workflow(
axum::extract::Path(workflow_id): axum::extract::Path<String>,
state: Arc<OrchestratorState>,
) -> impl IntoResponse {
if let Some(workflow) = state.get_workflow(&workflow_id).await {
(
StatusCode::OK,
Json(json!({
"success": true,
"workflow": workflow
})),
)
} else {
(
StatusCode::NOT_FOUND,
Json(json!({
"success": false,
"error": "Workflow not found"
})),
)
}
}
async fn handle_cancel_workflow(
axum::extract::Path(workflow_id): axum::extract::Path<String>,
state: Arc<OrchestratorState>,
) -> impl IntoResponse {
if let Some(mut workflow) = state.get_workflow(&workflow_id).await {
workflow.status = WorkflowStatus::Failed;
state.add_workflow(workflow).await;
state
.broadcast_to_channel(
"workflows",
json!({
"type": "workflow_update",
"workflow": {
"id": workflow_id,
"status": "cancelled"
}
}),
)
.await;
(
StatusCode::OK,
Json(json!({
"success": true,
"message": "Workflow cancelled"
})),
)
} else {
(
StatusCode::NOT_FOUND,
Json(json!({
"success": false,
"error": "Workflow not found"
})),
)
}
}
async fn handle_list_workflows(state: Arc<OrchestratorState>) -> impl IntoResponse {
let workflows = state.workflows.read().await;
let list: Vec<_> = workflows.values().cloned().collect();
Json(json!({
"success": true,
"workflows": list
}))
}
async fn handle_orchestration_metrics(state: Arc<OrchestratorState>) -> impl IntoResponse {
let agents = state.get_agents().await;
let workflows = state.workflows.read().await;
let tasks = state.task_queue.read().await;
let running_workflows = workflows
.values()
.filter(|w| w.status == WorkflowStatus::Running)
.count();
let completed_workflows = workflows
.values()
.filter(|w| w.status == WorkflowStatus::Completed)
.count();
let failed_workflows = workflows
.values()
.filter(|w| w.status == WorkflowStatus::Failed)
.count();
let pending_tasks = tasks
.iter()
.filter(|t| t.status == TaskStatus::Pending)
.count();
Json(json!({
"success": true,
"metrics": {
"agents": {
"total": agents.len(),
"online": agents.len(),
},
"workflows": {
"total": workflows.len(),
"running": running_workflows,
"completed": completed_workflows,
"failed": failed_workflows,
},
"tasks": {
"total": tasks.len(),
"pending": pending_tasks,
},
"timestamp": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
}
}))
}
async fn handle_publish_agent(
Json(body): Json<JsonValue>,
state: Arc<OrchestratorState>,
) -> impl IntoResponse {
let agent_name = body
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unnamed")
.to_string();
state
.broadcast_to_channel(
"agents",
json!({
"type": "agent_published",
"agent": {
"name": agent_name,
"description": body.get("description"),
"systemPrompt": body.get("systemPrompt"),
"tools": body.get("tools"),
"model": body.get("model"),
}
}),
)
.await;
(
StatusCode::ACCEPTED,
Json(json!({
"success": true,
"stored": false,
"message": format!(
"Agent '{}' announced to subscribers of the agents channel; it was not stored or sent to a marketplace registry",
agent_name
)
})),
)
}
async fn handle_marketplace_search(
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
state: Arc<OrchestratorState>,
) -> impl IntoResponse {
let query_str = params.get("q").cloned().unwrap_or_default();
let category = params.get("category").cloned();
let sort = params.get("sort").map(|s| match s.as_str() {
"stars" => SortBy::Stars,
"recent" => SortBy::Recent,
"name" => SortBy::Name,
_ => SortBy::Downloads,
});
let mut registry_error: Option<String> = None;
let marketplace = state.marketplace.read().await;
if let Some(client) = marketplace.as_ref() {
let search_query = SearchQuery {
query: if query_str.is_empty() {
None
} else {
Some(query_str.clone())
},
category: category.clone(),
keyword: None,
author: None,
sort_by: sort,
page: params.get("page").and_then(|p| p.parse().ok()),
per_page: params.get("per_page").and_then(|p| p.parse().ok()),
};
match client.search(search_query).await {
Ok(results) => {
let agents: Vec<JsonValue> = results
.results
.iter()
.map(|a| {
json!({
"id": a.manifest.name,
"name": a.manifest.display_name,
"description": a.manifest.description,
"author": a.manifest.author.name,
"version": a.manifest.version,
"downloads": a.downloads,
"stars": a.stars,
"forks": a.forks,
"tags": a.manifest.keywords,
"updatedAt": a.updated_at * 1000,
"verified": a.verified,
})
})
.collect();
return Json(json!({
"success": true,
"query": query_str,
"category": category,
"agents": agents,
"total": results.total,
"page": results.page,
"per_page": results.per_page,
}));
}
Err(e) => {
registry_error = Some(e.to_string());
}
}
}
let installed_agents = get_installed_as_json(&state).await;
let filtered: Vec<JsonValue> = if query_str.is_empty() {
installed_agents.clone()
} else {
let q = query_str.to_lowercase();
installed_agents
.iter()
.filter(|a| {
a.get("name")
.and_then(|n| n.as_str())
.unwrap_or("")
.to_lowercase()
.contains(&q)
|| a.get("description")
.and_then(|d| d.as_str())
.unwrap_or("")
.to_lowercase()
.contains(&q)
})
.cloned()
.collect()
};
Json(json!({
"success": true,
"query": query_str,
"category": category,
"agents": filtered,
"total": filtered.len(),
"page": 1,
"per_page": 20,
"source": "local",
"registry_error": registry_error,
}))
}
async fn handle_marketplace_list(state: Arc<OrchestratorState>) -> impl IntoResponse {
let installed_agents = get_installed_as_json(&state).await;
Json(json!({
"success": true,
"agents": installed_agents,
"total": installed_agents.len(),
}))
}
fn marketplace_name_refusal(
name: &str,
version: Option<&str>,
) -> Option<(StatusCode, Json<JsonValue>)> {
let checked =
crate::marketplace::validate_package_name(name, "agent name").and_then(|()| {
match version {
Some(v) => crate::marketplace::validate_package_name(v, "agent version"),
None => Ok(()),
}
});
let err = checked.err()?;
Some((
StatusCode::BAD_REQUEST,
Json(json!({
"success": false,
"code": "E_BAD_ARG",
"error": err.to_string(),
})),
))
}
async fn handle_marketplace_install(
Json(body): Json<JsonValue>,
state: Arc<OrchestratorState>,
) -> impl IntoResponse {
let name = match body.get("name").and_then(|n| n.as_str()) {
Some(n) => n.to_string(),
None => {
return (
StatusCode::BAD_REQUEST,
Json(json!({
"success": false,
"error": "Missing required field: name"
})),
)
}
};
let version = body
.get("version")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let Some(refusal) = marketplace_name_refusal(&name, version.as_deref()) {
return refusal;
}
let mut marketplace = state.marketplace.write().await;
let client = match marketplace.as_mut() {
Some(c) => c,
None => {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
"success": false,
"error": "Marketplace client not available"
})),
)
}
};
match client.install(&name, version.as_deref()).await {
Ok(installed) => {
state
.broadcast_to_channel(
"agents",
json!({
"type": "agent_installed",
"agent": {
"name": installed.manifest.name,
"version": installed.manifest.version,
"description": installed.manifest.description,
}
}),
)
.await;
(
StatusCode::OK,
Json(json!({
"success": true,
"message": format!("Installed {} v{}", installed.manifest.display_name, installed.manifest.version),
"agent": {
"name": installed.manifest.name,
"version": installed.manifest.version,
"path": installed.path.to_string_lossy(),
}
})),
)
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"success": false,
"error": format!("Failed to install {}: {}", name, e)
})),
),
}
}
async fn handle_marketplace_uninstall(
Json(body): Json<JsonValue>,
state: Arc<OrchestratorState>,
) -> impl IntoResponse {
let name = match body.get("name").and_then(|n| n.as_str()) {
Some(n) => n.to_string(),
None => {
return (
StatusCode::BAD_REQUEST,
Json(json!({
"success": false,
"error": "Missing required field: name"
})),
)
}
};
if let Some(refusal) = marketplace_name_refusal(&name, None) {
return refusal;
}
let mut marketplace = state.marketplace.write().await;
let client = match marketplace.as_mut() {
Some(c) => c,
None => {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
"success": false,
"error": "Marketplace client not available"
})),
)
}
};
match client.uninstall(&name) {
Ok(()) => {
state
.broadcast_to_channel(
"agents",
json!({
"type": "agent_uninstalled",
"name": name,
}),
)
.await;
(
StatusCode::OK,
Json(json!({
"success": true,
"message": format!("Uninstalled {}", name)
})),
)
}
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"success": false,
"error": format!("Failed to uninstall {}: {}", name, e)
})),
),
}
}
async fn get_installed_as_json(state: &OrchestratorState) -> Vec<JsonValue> {
let marketplace = state.marketplace.read().await;
match marketplace.as_ref() {
Some(client) => client
.list_installed()
.iter()
.map(|a| {
json!({
"id": a.manifest.name,
"name": a.manifest.display_name,
"description": a.manifest.description,
"author": a.manifest.author.name,
"version": a.manifest.version,
"downloads": 0,
"stars": 0,
"forks": 0,
"tags": a.manifest.keywords,
"updatedAt": a.installed_at * 1000,
"verified": false,
"installed": true,
})
})
.collect(),
None => vec![],
}
}
#[cfg(test)]
mod lifecycle_tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn a_request_that_cannot_get_a_slot_in_time_is_answered_busy() {
crate::tls::install_provider();
let permits = Arc::new(tokio::sync::Semaphore::new(1));
let app = Router::new()
.route(
"/slow",
get(|| async {
tokio::time::sleep(Duration::from_millis(1500)).await;
"slow"
}),
)
.route("/fast", get(|| async { "fast" }))
.layer(axum::middleware::from_fn(move |req, next| {
admit(permits.clone(), 1, Duration::from_millis(200), req, next)
}));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let _ = axum::serve(listener, app).await;
});
let client = reqwest::Client::new();
let slow = tokio::spawn({
let client = client.clone();
async move { client.get(format!("http://{addr}/slow")).send().await }
});
tokio::time::sleep(Duration::from_millis(300)).await;
let busy = client
.get(format!("http://{addr}/fast"))
.send()
.await
.unwrap();
assert_eq!(busy.status(), reqwest::StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(busy.headers()["retry-after"], "5");
let body: serde_json::Value = busy.json().await.unwrap();
assert_eq!(body["code"], "E_BUDGET_EXCEEDED", "{body}");
assert_eq!(slow.await.unwrap().unwrap().text().await.unwrap(), "slow");
let fast = client
.get(format!("http://{addr}/fast"))
.send()
.await
.unwrap();
assert_eq!(fast.status(), reqwest::StatusCode::OK);
server.abort();
}
fn test_state() -> OrchestratorState {
OrchestratorState {
agents: RwLock::new(HashMap::new()),
channels: RwLock::new(HashMap::new()),
task_queue: RwLock::new(Vec::new()),
workflows: RwLock::new(HashMap::new()),
marketplace: RwLock::new(None),
}
}
fn endless_task() -> (JoinHandle<()>, tokio::sync::oneshot::Receiver<()>) {
let (dropped_tx, dropped_rx) = tokio::sync::oneshot::channel::<()>();
let task = tokio::spawn(async move {
let _sentinel = dropped_tx;
std::future::pending::<()>().await
});
(task, dropped_rx)
}
async fn assert_stopped(dropped: tokio::sync::oneshot::Receiver<()>) {
let outcome = tokio::time::timeout(Duration::from_secs(5), dropped)
.await
.expect("task future was never dropped");
assert!(outcome.is_err(), "sentinel should drop without a value");
}
#[tokio::test]
async fn unsubscribe_stops_the_forwarder() {
let mut subs = SubscriptionTasks::default();
let (task, dropped) = endless_task();
subs.insert("news".to_string(), task);
assert_eq!(subs.len(), 1);
assert!(subs.remove("news").await);
assert!(!subs.remove("news").await, "second remove finds nothing");
assert_eq!(subs.len(), 0);
assert_stopped(dropped).await;
}
#[tokio::test]
async fn resubscribing_replaces_the_previous_forwarder() {
let mut subs = SubscriptionTasks::default();
let (first, first_dropped) = endless_task();
let (second, _second_dropped) = endless_task();
subs.insert("news".to_string(), first);
subs.insert("news".to_string(), second);
assert_eq!(subs.len(), 1);
assert_stopped(first_dropped).await;
}
#[tokio::test]
async fn disconnect_stops_every_forwarder() {
let mut subs = SubscriptionTasks::default();
let (a, a_dropped) = endless_task();
let (b, b_dropped) = endless_task();
subs.insert("a".to_string(), a);
subs.insert("b".to_string(), b);
subs.stop_all().await;
assert_eq!(subs.len(), 0);
assert_stopped(a_dropped).await;
assert_stopped(b_dropped).await;
}
#[tokio::test]
async fn dropping_the_set_aborts_what_is_left() {
let mut subs = SubscriptionTasks::default();
let (task, dropped) = endless_task();
subs.insert("news".to_string(), task);
drop(subs);
assert_stopped(dropped).await;
}
#[tokio::test]
async fn forwarder_skips_lagged_messages_and_ends_when_channel_closes() {
let (tx, rx) = broadcast::channel(2);
for i in 0..5 {
let _ = tx.send(json!(i));
}
drop(tx);
let (out_tx, mut out_rx) = mpsc::channel(16);
let task = tokio::spawn(forward_channel(rx, out_tx, "news".to_string()));
tokio::time::timeout(Duration::from_secs(5), task)
.await
.expect("forwarder did not end when the channel closed")
.expect("forwarder panicked");
let mut payloads = Vec::new();
while let Some(msg) = out_rx.recv().await {
match msg {
WsServerMessage::Channel { channel, payload } => {
assert_eq!(channel, "news");
payloads.push(payload);
}
_ => panic!("unexpected message kind"),
}
}
assert_eq!(payloads, vec![json!(3), json!(4)]);
}
#[tokio::test]
async fn channel_with_no_subscribers_is_pruned_on_broadcast() {
let state = test_state();
let idle = state.subscribe_to_channel("idle").await;
let live = state.subscribe_to_channel("live").await;
drop(idle);
state.broadcast_to_channel("idle", json!(1)).await;
let channels = state.channels.read().await;
assert!(!channels.contains_key("idle"), "idle channel kept");
assert!(channels.contains_key("live"), "live channel removed");
drop(channels);
drop(live);
}
async fn into_status_and_json<R: axum::response::IntoResponse>(
response: R,
) -> (StatusCode, JsonValue) {
let response = axum::response::IntoResponse::into_response(response);
let status = response.status();
let bytes = axum::body::to_bytes(response.into_body(), 1 << 20)
.await
.expect("response body");
let body = serde_json::from_slice(&bytes).expect("JSON body");
(status, body)
}
#[tokio::test]
async fn create_workflow_refuses_malformed_steps_and_stores_nothing() {
let state = Arc::new(test_state());
let (status, body) = into_status_and_json(
handle_create_workflow(
Json(json!({"name": "w", "steps": "not a list"})),
state.clone(),
)
.await,
)
.await;
assert_eq!(status, StatusCode::BAD_REQUEST, "{body}");
assert_eq!(body["success"], false, "{body}");
assert!(state.workflows.read().await.is_empty());
}
#[tokio::test]
async fn create_workflow_without_steps_is_still_created() {
let state = Arc::new(test_state());
let (status, body) = into_status_and_json(
handle_create_workflow(Json(json!({"name": "w"})), state.clone()).await,
)
.await;
assert_eq!(status, StatusCode::CREATED, "{body}");
let id = body["workflow_id"].as_str().expect("workflow_id");
let workflows = state.workflows.read().await;
assert_eq!(workflows.len(), 1);
assert!(workflows.contains_key(id));
}
#[tokio::test]
async fn publish_says_the_agent_was_not_stored() {
let state = Arc::new(test_state());
let (status, body) = into_status_and_json(
handle_publish_agent(Json(json!({"name": "a"})), state.clone()).await,
)
.await;
assert_eq!(status, StatusCode::ACCEPTED, "{body}");
assert_eq!(body["success"], true, "{body}");
assert_eq!(body["stored"], false, "{body}");
}
#[tokio::test]
async fn search_reports_a_failed_registry_in_registry_error() {
let state = Arc::new(OrchestratorState {
marketplace: RwLock::new(Some(
RegistryClient::new(Some("http://127.0.0.1:1".to_string()))
.expect("registry client"),
)),
..test_state()
});
let (status, body) = into_status_and_json(
handle_marketplace_search(axum::extract::Query(HashMap::new()), state.clone())
.await,
)
.await;
assert_eq!(status, StatusCode::OK, "{body}");
assert_eq!(body["success"], true, "{body}");
assert_eq!(body["source"], "local", "{body}");
assert!(
body["registry_error"]
.as_str()
.is_some_and(|e| !e.is_empty()),
"{body}"
);
}
#[tokio::test]
async fn search_with_no_registry_has_no_registry_error() {
let state = Arc::new(test_state());
let (status, body) = into_status_and_json(
handle_marketplace_search(axum::extract::Query(HashMap::new()), state.clone())
.await,
)
.await;
assert_eq!(status, StatusCode::OK, "{body}");
assert!(body["registry_error"].is_null(), "{body}");
}
#[tokio::test]
async fn marketplace_refuses_a_traversal_name_as_bad_arg() {
let cache = std::env::temp_dir().join(format!(
"ish-api-mp-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let client = crate::marketplace::RegistryClient::with_cache_dir(
Some("http://127.0.0.1:1".to_string()),
cache.clone(),
)
.expect("registry client over a temp cache");
let state = Arc::new(OrchestratorState {
marketplace: RwLock::new(Some(client)),
..test_state()
});
for body in [
json!({"name": "../../escape"}),
json!({"name": "a/b"}),
json!({"name": "ok", "version": "..\\x"}),
] {
let (status, out) = into_status_and_json(
handle_marketplace_install(Json(body.clone()), state.clone()).await,
)
.await;
assert_eq!(status, StatusCode::BAD_REQUEST, "{body} -> {out}");
assert_eq!(out["code"], "E_BAD_ARG", "{out}");
}
let (status, out) = into_status_and_json(
handle_marketplace_uninstall(Json(json!({"name": "../x"})), state.clone()).await,
)
.await;
assert_eq!(status, StatusCode::BAD_REQUEST, "{out}");
assert_eq!(out["code"], "E_BAD_ARG", "{out}");
let bare = Arc::new(test_state());
let (status, out) = into_status_and_json(
handle_marketplace_install(Json(json!({"name": ".."})), bare).await,
)
.await;
assert_eq!(status, StatusCode::BAD_REQUEST, "{out}");
assert_eq!(std::fs::read_dir(&cache).unwrap().count(), 0);
let _ = std::fs::remove_dir_all(&cache);
}
#[tokio::test]
async fn evaluate_off_thread_returns_the_evaluation_result() {
let response = evaluate_off_thread(
AgentRequest::Eval {
code: "1 + 1".to_string(),
},
None,
)
.await
.expect("evaluation panicked");
assert!(response.success);
}
#[test]
fn stream_slots_refuse_when_full_and_recover_on_release() {
let slots = Arc::new(tokio::sync::Semaphore::new(2));
let a = try_take_stream_slot(&slots).expect("first slot");
let b = try_take_stream_slot(&slots).expect("second slot");
assert!(
try_take_stream_slot(&slots).is_none(),
"a third streamed evaluation must be refused, not queued"
);
drop(a);
assert!(
try_take_stream_slot(&slots).is_some(),
"a released slot must be reusable"
);
drop(b);
}
#[test]
fn a_refused_stream_is_a_coded_busy_response() {
let r = ws_busy_response();
assert!(!r.success);
assert_eq!(r.code.as_deref(), Some("E_BUDGET_EXCEEDED"));
}
}
}
pub fn process_json_request(json_str: &str) -> Result<String> {
let request = parse_request(json_str).map_err(|r| {
anyhow!(
"{}",
r.error
.unwrap_or_else(|| "Invalid JSON request".to_string())
)
})?;
let response = process_request(&request);
Ok(serde_json::to_string_pretty(&response)?)
}
const REQUEST_HINT: &str = "a request is {\"action\":\"call\",\"builtin\":NAME,\"args\":[...]}; actions: call, pipeline, eval, describe, list_builtins, schema, type_info";
struct FirstRepeatedKey(Option<String>);
struct RepeatSeed<'p> {
path: &'p str,
}
impl<'de> serde::de::DeserializeSeed<'de> for RepeatSeed<'_> {
type Value = Option<String>;
fn deserialize<D: serde::Deserializer<'de>>(self, d: D) -> Result<Option<String>, D::Error> {
d.deserialize_any(RepeatVisitor { path: self.path })
}
}
struct RepeatVisitor<'p> {
path: &'p str,
}
impl<'de> serde::de::Visitor<'de> for RepeatVisitor<'_> {
type Value = Option<String>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("JSON")
}
fn visit_bool<E>(self, _: bool) -> Result<Option<String>, E> {
Ok(None)
}
fn visit_i64<E>(self, _: i64) -> Result<Option<String>, E> {
Ok(None)
}
fn visit_u64<E>(self, _: u64) -> Result<Option<String>, E> {
Ok(None)
}
fn visit_f64<E>(self, _: f64) -> Result<Option<String>, E> {
Ok(None)
}
fn visit_str<E>(self, _: &str) -> Result<Option<String>, E> {
Ok(None)
}
fn visit_unit<E>(self) -> Result<Option<String>, E> {
Ok(None)
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(
self,
mut seq: A,
) -> Result<Option<String>, A::Error> {
let mut found = None;
let mut i = 0usize;
loop {
let path = format!("{}[{i}]", self.path);
match seq.next_element_seed(RepeatSeed { path: &path })? {
Some(inner) => {
if found.is_none() {
found = inner;
}
}
None => break,
}
i += 1;
}
Ok(found)
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<Option<String>, A::Error> {
let mut seen = std::collections::HashSet::new();
let mut found = None;
while let Some(key) = map.next_key::<String>()? {
let path = if self.path.is_empty() {
key.clone()
} else {
format!("{}.{key}", self.path)
};
let inner = map.next_value_seed(RepeatSeed { path: &path })?;
if found.is_none() {
if !seen.insert(key) {
found = Some(path);
} else {
found = inner;
}
}
}
Ok(found)
}
}
pub(crate) fn first_repeated_key(text: &str) -> Option<String> {
serde_json::from_str::<FirstRepeatedKey>(text)
.ok()
.and_then(|k| k.0)
}
impl<'de> serde::Deserialize<'de> for FirstRepeatedKey {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
serde::de::DeserializeSeed::deserialize(RepeatSeed { path: "" }, d).map(FirstRepeatedKey)
}
}
pub fn parse_request(text: &str) -> Result<AgentRequest, AgentResponse> {
let text = text.strip_prefix('\u{feff}').unwrap_or(text);
let value: JsonValue = serde_json::from_str(text).map_err(|e| {
let hint = if e.to_string().starts_with("trailing characters") {
several_requests_hint()
} else if names_a_file(text) {
format!(
"the argument is the name of a file, but execute takes the JSON text itself: pass its contents (`ish agent execute \"$(cat FILE)\"`) or pipe the file to stdin. {REQUEST_HINT}"
)
} else {
REQUEST_HINT.to_string()
};
bad_request("E_PARSE", format!("Invalid JSON request: {e}"), hint)
})?;
if value.is_object() {
if let Ok(FirstRepeatedKey(Some(repeated))) = serde_json::from_str::<FirstRepeatedKey>(text)
{
return Err(bad_request(
"E_PARSE",
format!("Invalid JSON request: duplicate key \"{repeated}\""),
REQUEST_HINT.to_string(),
));
}
}
let hint = if value.get("action").and_then(JsonValue::as_str) == Some("pipeline") {
PIPELINE_HINT.to_string()
} else {
REQUEST_HINT.to_string()
};
serde_json::from_value(value)
.map_err(|e| bad_request("E_BAD_ARG", format!("Invalid request: {e}"), hint))
}
fn names_a_file(text: &str) -> bool {
let t = text.trim();
if t.is_empty() || t.len() > 260 || t.contains(['\n', '{', '[', '"']) {
return false;
}
let p = std::path::Path::new(t);
if p.is_absolute() || t.starts_with(['\\', '/']) {
return false;
}
if p.components()
.any(|c| !matches!(c, std::path::Component::Normal(_)))
{
return false;
}
p.is_file()
}
const PIPELINE_HINT: &str = "a pipeline is {\"action\":\"pipeline\",\"steps\":[{\"builtin\":NAME,\"args\":[...]}]}, with an optional \"input\"; the list is named steps, not stages";
fn several_requests_hint() -> String {
format!(
"execute takes one request; `ish agent interactive` takes one request per line. {REQUEST_HINT}"
)
}
fn bad_request(code: &str, message: String, hint: String) -> AgentResponse {
AgentResponse {
success: false,
result: None,
error: Some(message),
code: Some(code.to_string()),
hint: Some(hint),
result_type: None,
metadata: None,
}
}
pub fn process_json_response(text: &str) -> AgentResponse {
match parse_request(text) {
Ok(request) => process_request(&request),
Err(response) => response,
}
}
pub fn parse_schema_format(format: &str) -> Option<SchemaFormat> {
match format.to_lowercase().as_str() {
"openai" | "gpt" | "chatgpt" | "open_a_i" => Some(SchemaFormat::OpenAI),
"azure" | "azure_openai" | "azureopenai" | "azure_open_a_i" => {
Some(SchemaFormat::AzureOpenAI)
}
"claude" | "anthropic" => Some(SchemaFormat::Claude),
"gemini" | "google" => Some(SchemaFormat::Gemini),
"llama" | "meta" | "llama3" => Some(SchemaFormat::Llama),
"mistral" | "codestral" | "pixtral" => Some(SchemaFormat::Mistral),
"cohere" | "command" | "command-r" => Some(SchemaFormat::Cohere),
"grok" | "xai" => Some(SchemaFormat::Grok),
"deepseek" | "deepseek-r1" | "deep_seek" => Some(SchemaFormat::DeepSeek),
"bedrock" | "aws" | "amazon" => Some(SchemaFormat::Bedrock),
"qwen" | "alibaba" | "dashscope" => Some(SchemaFormat::Qwen),
"ollama" => Some(SchemaFormat::Ollama),
"lmstudio" | "lm-studio" | "lm_studio" => Some(SchemaFormat::OpenAI),
"vllm" | "v_llm" => Some(SchemaFormat::VLLM),
"huggingface" | "hugging_face" | "hf" | "tgi" => Some(SchemaFormat::HuggingFace),
"openrouter" | "open_router" => Some(SchemaFormat::OpenRouter),
"kimi" | "moonshot" => Some(SchemaFormat::Kimi),
"yi" | "01ai" | "lingyiwanwu" => Some(SchemaFormat::Yi),
"glm" | "g_l_m" | "chatglm" | "zhipu" => Some(SchemaFormat::GLM),
"reka" => Some(SchemaFormat::Reka),
"ai21" | "a_i21" | "jamba" | "jurassic" => Some(SchemaFormat::AI21),
"perplexity" | "sonar" => Some(SchemaFormat::Perplexity),
"together" | "together-ai" => Some(SchemaFormat::Together),
"groq" => Some(SchemaFormat::Groq),
"fireworks" | "fireworks-ai" => Some(SchemaFormat::Fireworks),
"json" | "jsonschema" | "json_schema" | "full" => Some(SchemaFormat::JsonSchema),
"compact" | "ontology" => Some(SchemaFormat::Ontology),
"jsonld" | "json-ld" | "json_ld" | "linked_data" => Some(SchemaFormat::JsonLD),
"owl" | "rdf" | "turtle" | "owl_turtle" => Some(SchemaFormat::OwlTurtle),
"shacl" | "shapes" | "s_h_a_c_l" => Some(SchemaFormat::SHACL),
_ => None,
}
}
pub const SCHEMA_FORMATS_HINT: &str = "openai, claude, gemini, llama, mistral, cohere, grok, deepseek, bedrock, azure, azure_openai, qwen, ollama, lmstudio, vllm, huggingface, openrouter, kimi, yi, glm, reka, ai21, perplexity, together, groq, fireworks, json, compact, ontology, jsonld, owl, shacl";
pub fn generate_schema(format: &str) -> Result<String> {
generate_schema_filtered(format, None, None)
}
pub fn generate_schema_filtered(
format: &str,
module: Option<&str>,
q: Option<&str>,
) -> Result<String> {
let schema_format = parse_schema_format(format).ok_or_else(|| {
anyhow!(
"Unknown schema format: '{}'. Supported: {}",
format,
SCHEMA_FORMATS_HINT
)
})?;
let request = AgentRequest::Schema {
format: schema_format,
module: module.map(str::to_string),
q: q.map(str::to_string),
};
let response = process_request(&request);
if response.success {
Ok(serde_json::to_string_pretty(&response.result)?)
} else {
Err(anyhow!(
"Failed to generate schema: {}",
response.error.unwrap_or_default()
))
}
}