From d2d08d2a37d02030270dc9efe390f6f3a185b0f6 Mon Sep 17 00:00:00 2001 From: Alfonso Sastre Date: Sat, 6 Jun 2026 09:23:23 +0200 Subject: [PATCH 1/2] feat: update agents, tools, permissions and web UI - lex.toml: switch to git deps - src/agents/*: build, explore, plan, refactor, review, spec, test agents updated - src/permissions/rules.lex, manifests.lex: permission rules and manifests - src/tools/index.lex, os_check.lex: tool index and OS check tool - src/server/web.lex: web server - src/web/index.html: updated UI Co-Authored-By: Claude Sonnet 4.6 --- lex.toml | 12 ++-- src/agents/build.lex | 4 +- src/agents/explore.lex | 2 +- src/agents/plan.lex | 4 +- src/agents/refactor.lex | 4 +- src/agents/review.lex | 4 +- src/agents/spec_agent.lex | 4 +- src/agents/test_agent.lex | 2 +- src/permissions/manifests.lex | 97 ++++++++++++++++++++++++++++ src/permissions/rules.lex | 2 +- src/tools/index.lex | 4 +- src/tools/os_check.lex | 117 ++++++++++++++++++++++++++++++++++ src/web/index.html | 2 +- 13 files changed, 238 insertions(+), 20 deletions(-) create mode 100644 src/permissions/manifests.lex create mode 100644 src/tools/os_check.lex diff --git a/lex.toml b/lex.toml index 4262705..dfdfaa2 100644 --- a/lex.toml +++ b/lex.toml @@ -4,11 +4,13 @@ version = "0.1.0" lex = "0.9.7" [dependencies] -"lex-llm" = { path = "../lex-llm" } -"lex-agent" = { path = "../lex-agent" } -"lex-trail" = { path = "../lex-trail" } -"lex-spec" = { path = "../lex-spec" } -"lex-schema" = { path = "../lex-schema" } +lex-llm = { git = "https://github.com/alpibrusl/lex-llm" } +lex-agent = { git = "https://github.com/alpibrusl/lex-agent" } +lex-trail = { git = "https://github.com/alpibrusl/lex-trail" } +lex-spec = { git = "https://github.com/alpibrusl/lex-spec" } +lex-schema = { git = "https://github.com/alpibrusl/lex-schema" } +lex-os-manifest = { git = "https://github.com/alpibrusl/lex-os-manifest" } +lex-web = { git = "https://github.com/alpibrusl/lex-web" } [bin] name = "lex-code" diff --git a/src/agents/build.lex b/src/agents/build.lex index e0f8419..917350c 100644 --- a/src/agents/build.lex +++ b/src/agents/build.lex @@ -20,7 +20,7 @@ fn agent() -> [env] ag.AgentDef { } fn openai_agent() -> [env] ag.AgentDef { - let base := { name: "build", goal: bpo.system(), model: prov.gpt55(), provider: providers.openai(), tools: tools.all_tools(), options: { temperature: None, top_p: None, max_steps: Some(50), max_tokens: None }, permission_spec: None } + let base := { name: "build", goal: bpo.system(), model: prov.gpt4o(), provider: providers.openai(), tools: tools.all_tools(), options: { temperature: None, top_p: None, max_steps: Some(50), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.build_permission()) } @@ -30,7 +30,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "build", goal: bpo.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: list.concat(tools.standard_tools(), tools.lex_cli_tools()), options: { temperature: None, top_p: None, max_steps: Some(10), max_tokens: None }, permission_spec: None } + let base := { name: "build", goal: bpo.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.build_permission()) } diff --git a/src/agents/explore.lex b/src/agents/explore.lex index be3994c..13fb5f2 100644 --- a/src/agents/explore.lex +++ b/src/agents/explore.lex @@ -21,7 +21,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "explore", goal: ep.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: tools.tools_for_spec(rules.explore_permission()), options: { temperature: None, top_p: None, max_steps: Some(20), max_tokens: None }, permission_spec: None } + let base := { name: "explore", goal: ep.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.explore_permission()) } diff --git a/src/agents/plan.lex b/src/agents/plan.lex index d516ceb..8a74724 100644 --- a/src/agents/plan.lex +++ b/src/agents/plan.lex @@ -21,7 +21,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "plan", goal: pp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: tools.tools_for_spec(rules.plan_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } + let base := { name: "plan", goal: pp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.plan_permission()) } @@ -31,7 +31,7 @@ fn vllm_agent() -> [env] ag.AgentDef { } fn openai_agent() -> [env] ag.AgentDef { - let base := { name: "plan", goal: pp.system(), model: prov.gpt55(), provider: providers.openai(), tools: tools.tools_for_spec(rules.plan_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } + let base := { name: "plan", goal: pp.system(), model: prov.gpt4o(), provider: providers.openai(), tools: tools.tools_for_spec(rules.plan_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.plan_permission()) } diff --git a/src/agents/refactor.lex b/src/agents/refactor.lex index 1bd6654..f41f3c7 100644 --- a/src/agents/refactor.lex +++ b/src/agents/refactor.lex @@ -21,7 +21,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "refactor", goal: rp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: tools.tools_for_spec(rules.refactor_permission()), options: { temperature: None, top_p: None, max_steps: Some(40), max_tokens: None }, permission_spec: None } + let base := { name: "refactor", goal: rp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.refactor_permission()) } @@ -31,7 +31,7 @@ fn vllm_agent() -> [env] ag.AgentDef { } fn openai_agent() -> [env] ag.AgentDef { - let base := { name: "refactor", goal: rp.system(), model: prov.gpt55(), provider: providers.openai(), tools: tools.tools_for_spec(rules.refactor_permission()), options: { temperature: None, top_p: None, max_steps: Some(40), max_tokens: None }, permission_spec: None } + let base := { name: "refactor", goal: rp.system(), model: prov.gpt4o(), provider: providers.openai(), tools: tools.tools_for_spec(rules.refactor_permission()), options: { temperature: None, top_p: None, max_steps: Some(40), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.refactor_permission()) } diff --git a/src/agents/review.lex b/src/agents/review.lex index 8df137c..bfb7ed0 100644 --- a/src/agents/review.lex +++ b/src/agents/review.lex @@ -21,7 +21,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "review", goal: rvp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: tools.tools_for_spec(rules.review_permission()), options: { temperature: None, top_p: None, max_steps: Some(25), max_tokens: None }, permission_spec: None } + let base := { name: "review", goal: rvp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.review_permission()) } @@ -31,7 +31,7 @@ fn vllm_agent() -> [env] ag.AgentDef { } fn openai_agent() -> [env] ag.AgentDef { - let base := { name: "review", goal: rvp.system(), model: prov.gpt55(), provider: providers.openai(), tools: tools.tools_for_spec(rules.review_permission()), options: { temperature: None, top_p: None, max_steps: Some(25), max_tokens: None }, permission_spec: None } + let base := { name: "review", goal: rvp.system(), model: prov.gpt4o(), provider: providers.openai(), tools: tools.tools_for_spec(rules.review_permission()), options: { temperature: None, top_p: None, max_steps: Some(25), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.review_permission()) } diff --git a/src/agents/spec_agent.lex b/src/agents/spec_agent.lex index ffdf6f3..6c3ce1c 100644 --- a/src/agents/spec_agent.lex +++ b/src/agents/spec_agent.lex @@ -21,7 +21,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "spec", goal: sp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: tools.tools_for_spec(rules.spec_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } + let base := { name: "spec", goal: sp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.spec_permission()) } @@ -31,7 +31,7 @@ fn vllm_agent() -> [env] ag.AgentDef { } fn openai_agent() -> [env] ag.AgentDef { - let base := { name: "spec", goal: sp.system(), model: prov.gpt55(), provider: providers.openai(), tools: tools.tools_for_spec(rules.spec_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } + let base := { name: "spec", goal: sp.system(), model: prov.gpt4o(), provider: providers.openai(), tools: tools.tools_for_spec(rules.spec_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.spec_permission()) } diff --git a/src/agents/test_agent.lex b/src/agents/test_agent.lex index 610dd1f..50d793b 100644 --- a/src/agents/test_agent.lex +++ b/src/agents/test_agent.lex @@ -21,7 +21,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "test", goal: tp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: tools.tools_for_spec(rules.test_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } + let base := { name: "test", goal: tp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.test_permission()) } diff --git a/src/permissions/manifests.lex b/src/permissions/manifests.lex new file mode 100644 index 0000000..94e847b --- /dev/null +++ b/src/permissions/manifests.lex @@ -0,0 +1,97 @@ +# Per-mode trust manifests for lex-os integration. +# +# Each AgentMode maps to a Grant that constrains what the mode can do +# on the three trust dimensions: filesystem, network, exec. +# +# Grant levels (from lex-types): +# filesystem: None | ReadOnly | ReadWrite | Full +# network: None | Loopback | Allowlist | Full +# exec: None | Sandboxed | Full +# +# Manifests are written to /tmp at call time and passed to `lex-os check`. + +import "std.str" as str + +import "std.int" as int + +import "std.io" as io + +fn manifest_json(goal :: Str, filesystem :: Str, network :: Str, exec_level :: Str, floor :: Str, wall :: Int, cmds :: Int, money :: Int, api_calls :: Int) -> Str { + str.join([ + "{\"goal\":{\"description\":\"", goal, "\"},", + "\"grant\":{\"filesystem\":\"", filesystem, "\",\"network\":\"", network, "\",\"exec\":\"", exec_level, "\"},", + "\"budget\":{\"wall_clock_secs\":", int.to_str(wall), ",", + "\"max_commands\":", int.to_str(cmds), ",", + "\"max_money_cents\":", int.to_str(money), ",", + "\"max_api_calls\":", int.to_str(api_calls), "},", + "\"isolation_floor\":\"", floor, "\",\"egress\":[]}" + ], "") +} + +fn explore_manifest_json() -> Str { + manifest_json("lex-code explore mode", "ReadOnly", "None", "None", "Namespace", 300, 200, 0, 50) +} + +fn plan_manifest_json() -> Str { + manifest_json("lex-code plan mode", "ReadOnly", "None", "None", "Namespace", 300, 200, 0, 50) +} + +fn review_manifest_json() -> Str { + manifest_json("lex-code review mode", "ReadOnly", "None", "None", "Namespace", 300, 200, 0, 50) +} + +fn spec_manifest_json() -> Str { + manifest_json("lex-code spec mode", "ReadWrite", "None", "None", "Namespace", 300, 200, 0, 50) +} + +fn test_manifest_json() -> Str { + manifest_json("lex-code test mode", "ReadWrite", "None", "Sandboxed", "Gvisor", 300, 200, 0, 50) +} + +fn refactor_manifest_json() -> Str { + manifest_json("lex-code refactor mode", "ReadWrite", "None", "Sandboxed", "Gvisor", 300, 200, 0, 50) +} + +fn build_manifest_json() -> Str { + manifest_json("lex-code build mode", "Full", "Allowlist", "Full", "MicroVm", 600, 500, 500, 100) +} + +fn json_for_mode(mode :: Str) -> Str { + if mode == "explore" { + explore_manifest_json() + } else { + if mode == "plan" { + plan_manifest_json() + } else { + if mode == "review" { + review_manifest_json() + } else { + if mode == "spec" { + spec_manifest_json() + } else { + if mode == "test" { + test_manifest_json() + } else { + if mode == "refactor" { + refactor_manifest_json() + } else { + build_manifest_json() + } + } + } + } + } + } +} + +fn temp_path_for_mode(mode :: Str) -> Str { + str.concat("/tmp/lex-code-manifest-", str.concat(mode, ".json")) +} + +fn write_manifest_for_mode(mode :: Str) -> [io] Result[Str, Str] { + let path := temp_path_for_mode(mode) + match io.write(path, json_for_mode(mode)) { + Ok(_) => Ok(path), + Err(e) => Err(e), + } +} diff --git a/src/permissions/rules.lex b/src/permissions/rules.lex index aa7c917..a420c36 100644 --- a/src/permissions/rules.lex +++ b/src/permissions/rules.lex @@ -53,7 +53,7 @@ fn spec_permission() -> sp.Spec { } fn refactor_permission() -> sp.Spec { - allow_tools("refactor_tools", list.concat(["read", "write", "edit", "grep", "glob", "bash", "lex_check", "lex_audit", "sigid_lookup", "effects_of", "lex_store_diff", "lex_store_apply", "lex_store_merge"], list.concat(vcs_read_names(), vcs_write_names()))) + allow_tools("refactor_tools", list.concat(["read", "write", "edit", "grep", "glob", "bash", "lex_check", "os_check", "lex_audit", "sigid_lookup", "effects_of", "lex_store_diff", "lex_store_apply", "lex_store_merge"], list.concat(vcs_read_names(), vcs_write_names()))) } fn test_permission() -> sp.Spec { diff --git a/src/tools/index.lex b/src/tools/index.lex index bf12599..43f44b7 100644 --- a/src/tools/index.lex +++ b/src/tools/index.lex @@ -14,6 +14,8 @@ import "./standard/todowrite" as todo_tool import "./lex_check" as check_tool +import "./os_check" as os_check_tool + import "./lex_audit" as audit_tool import "./lex_run" as run_tool @@ -99,7 +101,7 @@ fn vcs_tools() -> List[t.Tool] { } fn all_tools() -> List[t.Tool] { - list.concat([read_tool.tool(), write_tool.tool(), edit_tool.tool(), grep_tool.tool(), glob_tool.tool(), bash_tool.tool(), todo_tool.tool(), check_tool.tool(), audit_tool.tool(), run_tool.tool(), test_tool.tool(), spec_check_tool.tool(), spec_smt_tool.tool(), sigid_tool.tool(), attest_tool.tool(), effects_tool.tool(), store_diff_tool.tool(), store_apply_tool.tool(), store_merge_tool.tool(), guidelines_tool.tool()], vcs_tools()) + list.concat([read_tool.tool(), write_tool.tool(), edit_tool.tool(), grep_tool.tool(), glob_tool.tool(), bash_tool.tool(), todo_tool.tool(), check_tool.tool(), os_check_tool.tool(), audit_tool.tool(), run_tool.tool(), test_tool.tool(), spec_check_tool.tool(), spec_smt_tool.tool(), sigid_tool.tool(), attest_tool.tool(), effects_tool.tool(), store_diff_tool.tool(), store_apply_tool.tool(), store_merge_tool.tool(), guidelines_tool.tool()], vcs_tools()) } fn tools_for_spec(spec :: sp.Spec) -> List[t.Tool] { diff --git a/src/tools/os_check.lex b/src/tools/os_check.lex new file mode 100644 index 0000000..cc987a0 --- /dev/null +++ b/src/tools/os_check.lex @@ -0,0 +1,117 @@ +import "std.proc" as proc + +import "std.str" as str + +import "std.list" as list + +import "lex-llm/tool" as t + +import "lex-schema/json_value" as jv + +import "lex-schema/error" as e + +import "lex-schema/schema" as s + +import "./util" as util + +fn params() -> s.ModelSchema { + { title: "OsCheckArgs", description: "Check a Lex file's declared effects against the mode's trust grant", fields: [s.required_str("path", []), s.optional(s.required_str("mode", []))] } +} + +# Effects the mode forbids, derived from the lex-os grant for that mode. +# explore / plan / review — ReadOnly FS, No Net, No Exec +# spec — ReadWrite FS, No Net, No Exec +# test / refactor — ReadWrite FS, No Net, Sandboxed Exec +# build — Full FS, Allowlist Net, Full Exec (nothing forbidden) +fn forbidden_for_mode(mode :: Str) -> List[Str] { + if mode == "explore" or mode == "plan" or mode == "review" { + ["net", "proc", "fs_write"] + } else { + if mode == "spec" { + ["net", "proc"] + } else { + if mode == "test" or mode == "refactor" { + ["net"] + } else { + [] + } + } + } +} + +fn effect_in(effect :: Str, lst :: List[Str]) -> Bool { + list.fold(lst, false, fn (acc :: Bool, x :: Str) -> Bool { + acc or x == effect + }) +} + +fn violations(required :: List[Str], forbidden :: List[Str]) -> List[Str] { + list.filter(required, fn (eff :: Str) -> Bool { + effect_in(eff, forbidden) + }) +} + +fn extract_effects(check_json :: jv.Json) -> List[Str] { + match jv.get_field(check_json, "data") { + None => [], + Some(data) => match jv.get_field(data, "required_effects") { + Some(JList(items)) => list.fold(items, [], fn (acc :: List[Str], j :: jv.Json) -> List[Str] { + match j { + JStr(s) => list.concat(acc, [s]), + _ => acc, + } + }), + _ => [], + }, + } +} + +fn execute(args :: jv.Json) -> [net, io, proc] Result[jv.Json, e.Errors] { + let path := util.field_str_or(args, "path", ".") + let mode := util.field_str_or(args, "mode", "build") + match proc.spawn("lex", ["--output", "json", "check", path]) { + Err(msg) => Err(e.single("", "proc_error", msg)), + Ok(out) => if out.exit_code != 0 { + Err(e.single("", "lex_check_failed", str.concat(out.stdout, out.stderr))) + } else { + match jv.parse(out.stdout) { + Err(_) => Err(e.single("", "parse_error", "could not parse lex check output")), + Ok(parsed) => { + let required := extract_effects(parsed) + let forbidden := forbidden_for_mode(mode) + let violated := violations(required, forbidden) + if list.len(violated) == 0 { + Ok(JStr(str.concat("grant check passed [mode=", str.concat(mode, str.concat("] effects=", str.join(required, ",")))))) + } else { + Ok(JStr(str.join([ + "GRANT VIOLATION [mode=", mode, "]\n", + " forbidden effects used: ", str.join(violated, ", "), "\n", + " all required effects: ", str.join(required, ", "), "\n", + " grant allows: ", grant_summary_for_mode(mode) + ], ""))) + } + }, + } + }, + } +} + +fn grant_summary_for_mode(mode :: Str) -> Str { + if mode == "explore" or mode == "plan" or mode == "review" { + "fs=read-only net=none exec=none" + } else { + if mode == "spec" { + "fs=read-write net=none exec=none" + } else { + if mode == "test" or mode == "refactor" { + "fs=read-write net=none exec=sandboxed" + } else { + "fs=full net=allowlist exec=full" + } + } + } +} + +fn tool() -> t.Tool { + t.define("os_check", "Check a Lex file's declared effects against the mode's trust grant (lex-os integration). Run after lex_check to catch grant violations — e.g. a refactor agent must not use net effects. Returns GRANT VIOLATION with details if the file exceeds the mode's grant.", params(), execute) +} diff --git a/src/web/index.html b/src/web/index.html index 249b56d..c1fb0ca 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -24,11 +24,11 @@

lex-code

From a43b249d2b02820f50074e783aa91311b12d4650 Mon Sep 17 00:00:00 2001 From: Alfonso Sastre Date: Tue, 9 Jun 2026 17:03:24 +0200 Subject: [PATCH 2/2] feat: add Vertex AI provider + run_headless agentcmp entry point MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - build.lex: add vertex_agent() using vtx.gemini_35_flash() + providers.vertex(); fix ollama_agent() to use all_tools() and max_steps=20 (was empty tools + 3 steps) - session.lex: route "vertex" provider tag to vertex_agent() for Build mode - main.lex: add --vertex flag, run_headless() function for headless/CI use, and collect_final_text() that prefers StepDone over TextChunk accumulation run_headless emits [AGENTCMP_RESULT]\t{"ok":…,"final":…} sentinel so the agentcmp lex_code adapter can parse results without screen-scraping. Co-Authored-By: Claude Sonnet 4.6 --- src/agents/build.lex | 9 ++++- src/server/session.lex | 9 +++++ src/tui/main.lex | 79 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/agents/build.lex b/src/agents/build.lex index 917350c..c507c22 100644 --- a/src/agents/build.lex +++ b/src/agents/build.lex @@ -4,6 +4,8 @@ import "lex-llm/provider" as prov import "lex-llm/providers" as providers +import "lex-llm/providers/vertex" as vtx + import "../tools/index" as tools import "../permissions/rules" as rules @@ -30,7 +32,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "build", goal: bpo.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } + let base := { name: "build", goal: bpo.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: tools.all_tools(), options: { temperature: None, top_p: None, max_steps: Some(20), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.build_permission()) } @@ -44,3 +46,8 @@ fn google_agent() -> [env] ag.AgentDef { ag.with_permission_gate(base, rules.build_permission()) } +fn vertex_agent() -> [env] ag.AgentDef { + let base := { name: "build", goal: bpo.system(), model: vtx.gemini_35_flash(), provider: providers.vertex(), tools: tools.all_tools(), options: { temperature: None, top_p: None, max_steps: Some(50), max_tokens: None }, permission_spec: None } + ag.with_permission_gate(base, rules.build_permission()) +} + diff --git a/src/server/session.lex b/src/server/session.lex index 87c7508..4dd96a8 100644 --- a/src/server/session.lex +++ b/src/server/session.lex @@ -85,6 +85,15 @@ fn pick_agent(mode :: AgentMode, provider_tag :: Str) -> [env] ag.AgentDef { Test => test_a.vllm_agent(), Review => review_a.vllm_agent(), }, + "vertex" => match mode { + Build => build_agent.vertex_agent(), + Plan => plan_agent.google_agent(), + Explore => explore_agent.google_agent(), + Refactor => refactor_agent.google_agent(), + Spec => spec_a.google_agent(), + Test => test_a.google_agent(), + Review => review_a.google_agent(), + }, _ => match mode { Build => build_agent.agent(), Plan => plan_agent.agent(), diff --git a/src/tui/main.lex b/src/tui/main.lex index fa98287..1b493d8 100644 --- a/src/tui/main.lex +++ b/src/tui/main.lex @@ -6,6 +6,10 @@ import "std.list" as list import "lex-llm/delta" as d +import "lex-llm/message" as msg + +import "lex-schema/json_value" as jv + import "../server/session" as sess import "../server/multi_agent" as multi @@ -146,13 +150,17 @@ fn select_provider_tag(argv :: List[Str]) -> Str { if has_flag(argv, "--google") { "google" } else { - if has_flag(argv, "--ollama") { - "ollama" + if has_flag(argv, "--vertex") { + "vertex" } else { - if has_flag(argv, "--vllm") { - "vllm" + if has_flag(argv, "--ollama") { + "ollama" } else { - "anthropic" + if has_flag(argv, "--vllm") { + "vllm" + } else { + "anthropic" + } } } } @@ -160,6 +168,65 @@ fn select_provider_tag(argv :: List[Str]) -> Str { } } +# Headless entry point for agentcmp and CI. +# +# Usage (from the lex-code source directory): +# lex run src/tui/main.lex run_headless '""' '"ollama"' \ +# --allow-effects env,io,net,llm,proc,sql,fs_write,time,concurrent +# +# Emits streaming progress to stdout (tool names + text chunks), then on +# the last line emits a machine-readable sentinel the adapter can parse: +# +# [AGENTCMP_RESULT] {"ok":true,"final":""} +# +fn run_headless(task :: Str, provider_tag :: Str) -> [env, io, net, llm, proc, sql, fs_write, time] Nil { + match sess.new_session_with_provider("headless", Build, provider_tag) { + Err(e) => io.print(str.join(["[AGENTCMP_RESULT]\t{\"ok\":false,\"final\":\"", e, "\"}\n"], "")), + Ok(session) => { + let result := sess.run_turn_with_provider(session, task, provider_tag) + let nsteps := list.len(result.steps) + let _ := io.print(str.join(["[dbg:steps=", int.to_str(nsteps), "]\n"], "")) + let _ := list.map(result.steps, fn (s :: d.Step) -> [io] Nil { + let _ := io.print(match s { + StepDelta(delta) => match delta { + TextChunk(t) => str.join(["[dbg:text:", t, "]\n"], ""), + ToolCallBegin(_, n) => str.join(["[dbg:toolbegin:", n, "]\n"], ""), + ToolArgChunk(_, _) => "", + FinishDelta(r) => str.join(["[dbg:finish:", r, "]\n"], ""), + }, + StepToolExec(n, _) => str.join(["[dbg:exec:", n, "]\n"], ""), + StepToolResult(_, ok) => if ok { "[dbg:result:ok]\n" } else { "[dbg:result:err]\n" }, + StepDone(_) => "[dbg:done]\n", + }) + print_step(s) + }) + let final_text := collect_final_text(result.steps) + io.print(str.join(["\n[AGENTCMP_RESULT]\t{\"ok\":true,\"final\":", jv.stringify(JStr(final_text)), "}\n"], "")) + }, + } +} + +# Extract the final assistant message text from the steps. +# Prefers the StepDone message (full assembled text) over accumulating +# TextChunk deltas, which are absent for models that use XML tool calls. +fn collect_final_text(steps :: List[d.Step]) -> Str { + match sess.find_done_msg(steps) { + Some(m) => match m { + AssistantMsg(text, _) => text, + _ => "", + }, + None => list.fold(steps, "", fn (acc :: Str, s :: d.Step) -> Str { + match s { + StepDelta(delta) => match delta { + TextChunk(t) => str.concat(acc, t), + _ => acc, + }, + _ => acc, + } + }), + } +} + fn main() -> [env, io, net, llm, proc, sql, fs_write, time] Nil { let argv := [] let provider_tag := select_provider_tag(argv) @@ -169,7 +236,7 @@ fn main() -> [env, io, net, llm, proc, sql, fs_write, time] Nil { None => { io.print(str.concat("lex-code — Lex-specialized coding assistant", "\n")) io.print(str.concat("modes: --plan | --explore | --refactor | --spec | --test | --review | --multi", "\n")) - io.print(str.concat("providers: --mistral | --openai | --google | --ollama | --vllm (default: anthropic)", "\n")) + io.print(str.concat("providers: --mistral | --openai | --google | --vertex | --ollama | --vllm (default: anthropic)", "\n")) io.print(str.concat("one-shot: lex run src/tui/main.lex -- [flags] \"your task\"", "\n")) io.print(str.concat("Ctrl-D to exit", "\n")) if has_flag(argv, "--multi") {