refactor(rhai): rename language workflow domain (#4571)

This commit is contained in:
Steven Enamakel
2026-07-05 11:21:20 -07:00
committed by GitHub
parent e3906dfeb3
commit f24220ee68
18 changed files with 275 additions and 257 deletions
+1 -1
View File
@@ -64,7 +64,7 @@ tinyjuice = { version = "0.1", default-features = false }
# Durable graph checkpoints still use `SqlRunLedgerCheckpointer` until the
# migration re-points those rows to the crate checkpointer.
# The `repl` feature adds the embedded Rhai `.ragsh` session runtime powering
# the `rlm` language-workflow tool (`src/openhuman/rlm/`).
# the `rhai_workflows` language-workflow tool (`src/openhuman/rhai_workflows/`).
tinyagents = { version = "1.7", features = ["sqlite", "repl"] }
# TinyCortex — Rust core for the memory engine (store/chunks/tree/retrieval/
# queue/ingest/score + long tail), vendored as a git submodule and patched
+5 -5
View File
@@ -69,7 +69,7 @@ The repo has **three** "workflow" systems. This plan touches only the first:
1. `flows::` / `openhuman.flows_*`**tinyflows typed graphs** (this plan).
2. `workflows::` / `openhuman.workflows_*` — WORKFLOW.md/SKILL.md bundle discovery/install (separate product surface under `/skills`). **Slated for decommission** — it is essentially the skills feature wearing the "workflows" name; see Phase 8. Retiring it frees the "Workflows" branding for tinyflows (the `/flows` nav tab already reads "Workflows").
3. `rlm::` — Rhai `.ragsh` language workflows (`docs/plans/rlm-workflows/`), positioned in `gitbooks/features/orchestration.md` as the _next_ layer on the same substrate. tinyflows remains the shipping visual/typed product; rlm does not replace it.
3. `rhai::` — Rhai `.ragsh` language workflows (`docs/plans/rlm-workflows/`), positioned in `gitbooks/features/orchestration.md` as the _next_ layer on the same substrate. tinyflows remains the shipping visual/typed product; Rhai workflows do not replace it.
---
@@ -113,16 +113,16 @@ Every workflow is, at run time, a **unique tinyagents state graph**: `compile()`
Audit (2026-07-04):
-**Single unified copy** — both Cargo worlds (`Cargo.toml:355`, `app/src-tauri/Cargo.toml:214`) patch `tinyagents` to `vendor/tinyagents`; exactly one `tinyagents 1.5.0` in both lockfiles. tinyflows' `tinyagents = "1.2"` requirement unifies onto the same copy (semver-compatible). No duplication, no type-identity risk.
- ⚠️ **Not on a tag** — the submodule is pinned at `df391c4` = `v1.5.0-13-gdf391c4`: 13 untagged commits past v1.5.0 (REPL host-embedding / cancel work taken early for the rlm feature). The crate still self-reports `1.5.0`, so the version string understates the vendored code.
- ⚠️ **Not on a tag** — the submodule is pinned at `df391c4` = `v1.5.0-13-gdf391c4`: 13 untagged commits past v1.5.0 (REPL host-embedding / cancel work taken early for the Rhai workflow feature). The crate still self-reports `1.5.0`, so the version string understates the vendored code.
- ⚠️ **Two minor versions behind** — upstream is at **v1.7.1** (60 commits past v1.5.0). The 13 early-adopted commits all landed upstream (via PR #19 etc.; `ReplSession::set_cancel_flag` verified present in v1.7.1), so retagging loses nothing.
- ⚠️ Same pattern on tinyflows itself: submodule at `v0.3.0-1-g438f8fc` (one commit past tag).
Work items:
1. Bump `vendor/tinyagents` submodule to the **v1.7.1 tag**; bump root requirement `tinyagents = { version = "1.7", features = ["sqlite", "repl"] }` (verify both features still exist in 1.7); `cargo update -p tinyagents` in both lockfiles.
2. Review the v1.5→v1.7 changelog for API breaks in the seams we touch: `Checkpointer`/`DurabilityMode`, `GraphEventJournal`/`GraphObservation`, interrupt/resume semantics (used by `flows::ops` + `src/openhuman/tinyagents/` + rlm).
2. Review the v1.5→v1.7 changelog for API breaks in the seams we touch: `Checkpointer`/`DurabilityMode`, `GraphEventJournal`/`GraphObservation`, interrupt/resume semantics (used by `flows::ops` + `src/openhuman/tinyagents/` + Rhai workflows).
3. Retag `vendor/tinyflows` on a proper release (v0.3.1) whose `Cargo.toml` requires `tinyagents = "1.7"` so the version story is coherent end-to-end.
4. Gate with `cargo check` both worlds, `pnpm test:rust`, and the flows/rlm unit suites; adopt a standing rule: **vendored tiny\* submodules pin release tags, never floating commits** (early-adopting an upstream PR requires a pre-release tag).
4. Gate with `cargo check` both worlds, `pnpm test:rust`, and the flows/Rhai unit suites; adopt a standing rule: **vendored tiny\* submodules pin release tags, never floating commits** (early-adopting an upstream PR requires a pre-release tag).
### Phase 1 — Backend completion (triggers, lifecycle, observability)
@@ -255,7 +255,7 @@ Tracked separately since it's a submodule with its own release cadence (host pin
The `workflows::` domain (WORKFLOW.md/SKILL.md bundle discovery/install, RPC `openhuman.workflows_*`) predates tinyflows and is functionally the **skills** feature under a different name. Keeping two things called "workflows" confuses users, agents, and contributors alike. Plan: fold what's unique into `skills`, delete the rest, and hand the name to tinyflows.
- **Audit consumers first**: `agent/tools/run_workflow.rs` (agent tool that runs WORKFLOW.md bundles — decide: retire, or repoint to `flows_run`/skills), the `/skills` UI surfaces (`WorkflowsTab`, `CreateWorkflowForm`, `WorkflowRunnerBody`, `WorkflowNew.tsx`, `WorkflowsRun.tsx`, `DevWorkflowPanel`, `workflowsApi.ts`), `about_app`, gitbooks, and the rlm plan's references to `run_workflow` as a composition surface.
- **Audit consumers first**: `agent/tools/run_workflow.rs` (agent tool that runs WORKFLOW.md bundles — decide: retire, or repoint to `flows_run`/skills), the `/skills` UI surfaces (`WorkflowsTab`, `CreateWorkflowForm`, `WorkflowRunnerBody`, `WorkflowNew.tsx`, `WorkflowsRun.tsx`, `DevWorkflowPanel`, `workflowsApi.ts`), `about_app`, gitbooks, and the Rhai workflow plan's references to `run_workflow` as a composition surface.
- **Migrate**: bundle discovery/install semantics that skills doesn't already cover move into the `skills` domain (it is metadata-only post-QuickJS-removal, so this is mostly file-format and registry work).
- **Deprecate then delete**: mark `openhuman.workflows_*` deprecated for one release (RPC responses carry a deprecation notice), then remove `src/openhuman/workflows/`, its controllers from `src/core/all.rs`, the frontend clients/pages, and the `/workflows/new`//`workflows/run` routes (bare `/workflows` already redirects to `/settings/automations`).
- **Not in scope**: `openhuman.workflow_run_*` (`agent_orchestration`'s declarative run ledger) is a different system and untouched here — though its name should also be revisited once "Workflows" ≡ tinyflows.
@@ -54,7 +54,7 @@ OpenHuman pins `tinyagents = { version = "1.5.0", features = ["sqlite", "repl"]
- **OpenHuman-owned providers only.** We do **not** enable any bundled provider feature. OpenHuman owns provider transport, credentials, OAuth, and billing classification, so the live model is always OpenHuman's `Provider` wrapped as [`ProviderModel`](../../../src/openhuman/tinyagents/model.rs), never an SDK-owned provider client. The `ChatModel` adapter is the seam that replaces feature-gated SDK providers.
- **`sqlite` feature enabled with one native sqlite chain.** OpenHuman's root and Tauri Cargo worlds pin `rusqlite = "=0.40.0"` and patch `rusqlite` / `libsqlite3-sys` locally to avoid the upstream `cfg_select!` build break on the current toolchain. Both worlds resolve to a single `libsqlite3-sys v0.38.0` chain. Durable graph checkpoints still run through [`SqlRunLedgerCheckpointer`](../../../src/openhuman/tinyagents/checkpoint.rs) until the migration re-points those rows to the crate checkpointer.
- **WhatsApp Web storage bridge.** `whatsapp-rust`'s Diesel-backed `sqlite-storage` feature links sqlite separately from rusqlite 0.40, so the optional `whatsapp-web` feature currently builds against `wacore::store::InMemoryBackend` and logs that sessions are not durable. A rusqlite-backed durable WhatsApp store is required before treating Web sessions as persistent again.
- **`repl` feature enabled for language workflows; `.rag` expressive language unused.** OpenHuman still drives *graphs* from Rust (`GraphBuilder`), not the declarative `.rag` language. But the `repl` feature (the imperative Rhai `.ragsh` session runtime) is enabled to power the `rlm` language-workflow tool ([`openhuman::rlm`](../../../src/openhuman/rlm/README.md), see "Language workflows (`rlm`)" below).
- **`repl` feature enabled for language workflows; `.rag` expressive language unused.** OpenHuman still drives *graphs* from Rust (`GraphBuilder`), not the declarative `.rag` language. But the `repl` feature (the imperative Rhai `.ragsh` session runtime) is enabled to power the `rhai_workflows` language-workflow tool ([`openhuman::rhai_workflows`](../../../src/openhuman/rhai_workflows/README.md), see "Language workflows (Rhai)" below).
- **Adapter map (feature-gated SDK piece → OpenHuman replacement):** provider clients → `ProviderModel`; crate SQLite checkpointer rows not yet adopted → `SqlRunLedgerCheckpointer`; task/status stores not yet controller-canonical → OpenHuman SQL/JSON run ledgers (`running_subagents`, `workflow_runs`, `agent_teams`, `command_center`). The generic harness/graph/middleware/event primitives are used as-is.
The agent harness is the runtime that turns a user message (or a webhook fire, or a cron tick) into a complete, tool-using LLM interaction. It owns the tool-call loop, sub-agent dispatch, the trigger-triage pipeline, and the hook surface around them. It does **not** own provider HTTP transport, tool implementations, prompt-section assembly, or memory storage - those are separate domains the harness composes.
@@ -282,20 +282,20 @@ Each `AgentDefinition` carries an `agent_tier` field (`chat` / `reasoning` / `wo
For Composio toolkits with hundreds of actions (GitHub alone has 500+), loading every action into the sub-agent's tool set balloons prompt size. The harness ranks the toolkit's actions against the parent-refined task prompt with a cheap CPU-only filter (verb detection, token overlap, verb-alignment boost) and only loads the top-ranked subset into the sub-agent. No model call, pure heuristic - fast and explainable.
## Language workflows (`rlm`)
## Language workflows (Rhai)
The fixed delegation primitives (`spawn_subagent`, `spawn_parallel_agents`, `run_workflow`) can't express *ad-hoc control flow* — "spawn N readers, dedupe their findings, verify each survivor with 3 refuters, loop until dry". The **`rlm` tool** closes that gap: it exposes TinyAgents' Rhai-backed `.ragsh` REPL (the `repl` cargo feature) so the orchestrator can author and run its own workflow scripts.
The fixed delegation primitives (`spawn_subagent`, `spawn_parallel_agents`, `run_workflow`) can't express *ad-hoc control flow* — "spawn N readers, dedupe their findings, verify each survivor with 3 refuters, loop until dry". The **`rhai_workflows` tool** closes that gap: it exposes TinyAgents' Rhai-backed `.ragsh` REPL (the `repl` cargo feature) so the orchestrator can author and run its own workflow scripts.
**One tool call = one `eval_cell`.** The orchestrator's normal tool-call loop *is* the CodeAct driver loop: the model writes a Rhai cell, the cell runs against a persistent per-session namespace (top-level `let` bindings survive into the next cell via an optional `session_id`), and the structured result flows back as the tool result. Scripts reach the host only through capability functions — `tool_call`, `agent_query`, `model_query`, their `*_batched` fan-out variants, `emit`, and `answer`.
The domain lives in [`src/openhuman/rlm/`](../../../src/openhuman/rlm/README.md):
The domain lives in [`src/openhuman/rhai_workflows/`](../../../src/openhuman/rhai_workflows/README.md):
- **`policy.rs`** maps the autonomy tier + `tool_timeout` clamps onto a `tinyagents::ReplPolicy` (always bounded, never unbounded; `readonly` refused; `full` may raise call-count limits to a hard 2× ceiling).
- **`bridge.rs`** builds the `CapabilityRegistry`: the parent's visible tools (each re-wrapped so the **approval gate runs in the bridge** — it is *not* on the repl path, which bypasses the harness `wrap_tool` middleware), the turn's provider model, and a sub-agent capability per `allowed_subagent_ids`. Recursion/duplication hazards (`rlm`, `spawn_*`, workflow tools, `CliRpcOnly`-scoped tools) are excluded. Because `eval_cell` runs on `spawn_blocking` + `block_on`, the `agent_query` adapter re-installs the `PARENT_CONTEXT` task-local that `run_subagent` resolves.
- **`bridge.rs`** builds the `CapabilityRegistry`: the parent's visible tools (each re-wrapped so the **approval gate runs in the bridge** — it is *not* on the repl path, which bypasses the harness `wrap_tool` middleware), the turn's provider model, and a sub-agent capability per `allowed_subagent_ids`. Recursion/duplication hazards (`rhai`, legacy `rlm`, `spawn_*`, workflow tools, `CliRpcOnly`-scoped tools) are excluded. Because `eval_cell` runs on `spawn_blocking` + `block_on`, the `agent_query` adapter re-installs the `PARENT_CONTEXT` task-local that `run_subagent` resolves.
- **`sessions.rs`** is a bounded (LRU + idle-TTL) manager of persistent sessions, one cell at a time (a concurrent call on a busy session returns a typed "busy" error).
- **`ops.rs`** runs the cell on `spawn_blocking` under a layered time bound (rhai `on_progress` deadline → `bridge_block_on` timer race → outer `tokio::timeout` backstop → harness `ToolTimeout`), wires the run-cancellation token to a fresh per-cell `ReplCancelFlag`, and maps every failure mode to a model-consumable result.
The tool is registered for the orchestrator on `supervised`/`full` tiers only, behind the `OPENHUMAN_RLM=0` kill switch. The TinyAgents-side host-embedding support (external cancellation, live capability events) landed in that crate's `repl` feature.
The tool is registered for the orchestrator on `supervised`/`full` tiers only, behind the `OPENHUMAN_RHAI_WORKFLOWS=0` kill switch; `OPENHUMAN_RHAI=0` and `OPENHUMAN_RLM=0` remain legacy aliases. The TinyAgents-side host-embedding support (external cancellation, live capability events) landed in that crate's `repl` feature.
## Triage - handling external triggers
+1 -1
View File
@@ -38,7 +38,7 @@ Because the transport is plain Signal-encrypted messaging, the other side doesn'
## What's next: RLMs
The direction we're building toward: **RLM-style language-based workflows**. These are agents that express orchestration as small programs in a sandboxed REPL, rather than a fixed graph, so control flow itself becomes something the model writes, inspects, and repairs. The graph engine, checkpointing, and trust model above are the substrate for it.
The direction we're building toward: **Rhai-backed language workflows**. These are agents that express orchestration as small programs in a sandboxed REPL, rather than a fixed graph, so control flow itself becomes something the model writes, inspects, and repairs. The graph engine, checkpointing, and trust model above are the substrate for it.
***
+2 -2
View File
@@ -622,11 +622,11 @@ pub(super) const CAPABILITIES: &[Capability] = &[
},
Capability {
id: "intelligence.language_workflows",
name: "Language Workflows (RLM)",
name: "Language Workflows (Rhai)",
domain: "intelligence",
category: CapabilityCategory::Intelligence,
description: "The orchestrator can author and run small Rhai workflow scripts to express ad-hoc control flow over delegated work — parallel fan-out, loops, and dedup-then-verify pipelines that fixed spawn/parallel primitives cannot. Each script runs bounded and fail-closed (per-cell timeout, per-session caps on tool/model/agent calls and recursion depth), and every effectful step still passes the same approval and permission gates as a direct tool call. Progress rides the existing tool-call timeline.",
how_to: "Runs automatically when the orchestrator chooses the `rlm` tool; disable with OPENHUMAN_RLM=0 or the read-only autonomy tier",
how_to: "Runs automatically when the orchestrator chooses the `rhai_workflows` tool; disable with OPENHUMAN_RHAI_WORKFLOWS=0 or the read-only autonomy tier",
status: CapabilityStatus::Beta,
privacy: DERIVED_TO_BACKEND,
},
@@ -103,11 +103,11 @@ their refs separate by `subagent_session_id` or `task_id` (`agentId` is only the
worker type), tick or wait on each independently, and synthesise only completed
outputs. Never fabricate a result for a worker that is still running or failed.
## Language workflows (rlm)
## Language workflows (Rhai)
When a task needs **ad-hoc control flow** over delegated work — loops, conditionals, a
dedup-then-verify pipeline, "spawn N, filter, then verify each survivor with M checks" — that
the fixed `spawn_parallel_agents` / `delegate_*` primitives can't express, use the `rlm` tool.
the fixed `spawn_parallel_agents` / `delegate_*` primitives can't express, use the `rhai_workflows` tool.
It evaluates a small **Rhai workflow cell** whose only side effects are capability calls:
`tool_call`, `agent_query`, `model_query`, and their `*_batched` fan-out variants (plus
`emit`/`answer`/`print`).
@@ -115,11 +115,11 @@ It evaluates a small **Rhai workflow cell** whose only side effects are capabili
- **One call = one cell.** Top-level `let` bindings persist within a `session_id`, so pass the
same `session_id` back to continue a namespace across calls (`let findings = …` in cell 1,
reference it in cell 2). Omit `session_id` for a fresh session; set `close_session: true` when done.
- **Prefer `rlm`** over `spawn_parallel_agents` when you need iteration, branching, or a
- **Prefer `rhai_workflows`** over `spawn_parallel_agents` when you need iteration, branching, or a
reduce/verify step over results — not for a single delegation (use the matching `delegate_*`
or `spawn_subagent` for that).
- **It stays inside the gates.** Every effectful inner `tool_call` still hits the approval gate;
`agent_query` only reaches sub-agents already in your allowlist. `rlm` itself, `spawn_*`, and
`agent_query` only reaches sub-agents already in your allowlist. `rhai` itself, `spawn_*`, and
workflow tools are not callable from a cell.
- **It is bounded and fail-closed.** Cells have a wall-clock timeout and per-session caps on
model/tool/agent calls and recursion depth. Exceeding one returns an error you can fix and
+1 -1
View File
@@ -98,7 +98,7 @@ pub mod provider_surfaces;
pub mod recall_calendar;
pub mod redirect_links;
pub mod referral;
pub mod rlm;
pub mod rhai_workflows;
pub mod routing;
pub mod runtime_node;
pub mod runtime_python;
@@ -1,7 +1,7 @@
# `rlm` — language-based workflows (Rhai `.ragsh` REPL)
# `rhai` — language-based workflows (Rhai `.ragsh` REPL)
Exposes TinyAgents' Rhai-backed `.ragsh` session runtime (the `repl` cargo
feature, `tinyagents::ReplSession`) as a first-class **`rlm` tool** so the
feature, `tinyagents::ReplSession`) as a first-class **`rhai_workflows` tool** so the
orchestrator model can author and execute its own workflow scripts — fan-out
over subagents, batched tool/model calls, loops, dedup/verify pipelines — and
run them deterministically, in the spirit of Claude Code Workflows and
@@ -17,12 +17,12 @@ the tool result. The orchestrator's own turn loop *is* the CodeAct driver loop.
| File | Role |
| ---- | ---- |
| `mod.rs` | Exports only (no controller schemas in v1). |
| `types.rs` | `RlmSessionId`, `RlmEvalRequest`/`RlmEvalResponse`, `RlmLimitsOverride`, `RlmCallSummary`, serde types. |
| `types.rs` | `RhaiSessionId`, `RhaiEvalRequest`/`RhaiEvalResponse`, `RhaiLimitsOverride`, `RhaiCallSummary`, serde types. |
| `policy.rs` | Maps openhuman autonomy tier + `tool_timeout` clamps → `tinyagents::ReplPolicy` (fail-closed, bounded). |
| `bridge.rs` | Builds the `CapabilityRegistry<()>`: openhuman tools (approval-gated, scope-filtered) + provider models + subagents. |
| `sessions.rs` | `RlmSessionManager`: LRU + idle-TTL bounded map of persistent `ReplSession`s, keyed `<thread>:<session_id>`, one cell at a time. |
| `ops.rs` | `eval_rlm_cell`: spawn_blocking + outer timeout, cancellation wiring, event forwarding, error → model-consumable result. |
| `tools.rs` | `RlmTool` (the `rlm` tool: schema, permission, scope, timeout, display). |
| `sessions.rs` | `RhaiSessionManager`: LRU + idle-TTL bounded map of persistent `ReplSession`s, keyed `<thread>:<session_id>`, one cell at a time. |
| `ops.rs` | `eval_rhai_cell`: spawn_blocking + outer timeout, cancellation wiring, event forwarding, error → model-consumable result. |
| `tools.rs` | `RhaiTool` (the `rhai_workflows` tool: schema, permission, scope, timeout, display). |
## Fail-closed guarantees
@@ -47,8 +47,8 @@ never a hung turn:
## Approval & security (bridged tools keep their own gates)
The RLM bridge restricts callable tools to the parent turn's
`visible_tool_names`, and **excludes** recursion/duplication hazards: `rlm`
The Rhai bridge restricts callable tools to the parent turn's
`visible_tool_names`, and **excludes** recursion/duplication hazards: `rhai`
itself, `spawn_subagent`/`spawn_parallel_agents` (use `agent_query` instead),
and `run_workflow`/`await_workflow`. `ToolScope::CliRpcOnly` tools are denied.
@@ -66,5 +66,6 @@ and `answer`. Graph authoring/execution (`graph_*`) is **out of scope for v1**
## Kill switch & rollout
The tool is **not registered** when the autonomy tier is `readonly` or when
`OPENHUMAN_RLM=0`; default-on for `supervised`/`full`. Reverting the
registration line disables the surface without touching the domain.
`OPENHUMAN_RHAI_WORKFLOWS=0`; default-on for `supervised`/`full`.
`OPENHUMAN_RHAI=0` and `OPENHUMAN_RLM=0` are still honoured as legacy aliases.
Reverting the registration line disables the surface without touching the domain.
@@ -4,7 +4,7 @@
//!
//! Three capability kinds are wired, each keeping openhuman's own gates:
//!
//! - **Tools** — one [`RlmToolAdapter`] per visible, non-excluded, agent-scoped
//! - **Tools** — one [`RhaiToolAdapter`] per visible, non-excluded, agent-scoped
//! tool. Approval is **not** on the tinyagents repl path (it lives in the
//! harness `wrap_tool` middleware the REPL bypasses), so the adapter itself
//! invokes the [`ApprovalGate`] for any tool whose `external_effect_with_args`
@@ -15,7 +15,7 @@
//! `allowed_subagent_ids`, so `agent_query("<id>", ...)` spawns a real
//! openhuman sub-agent through `run_subagent`.
//!
//! Recursion/duplication hazards are **excluded** from the tool surface: `rlm`
//! Recursion/duplication hazards are **excluded** from the tool surface: `rhai`
//! itself (no REPL-in-REPL), `spawn_*` (use `agent_query`), and
//! `run_workflow`/`await_workflow`. `ToolScope::CliRpcOnly` tools are excluded
//! too.
@@ -49,7 +49,9 @@ use crate::openhuman::tools::Tool as OhTool;
/// re-entering the REPL) and capability duplication (spawn/workflow primitives
/// the script models with `agent_query` instead).
fn is_excluded_tool(name: &str) -> bool {
name == "rlm"
name == "rhai_workflows"
|| name == "rhai"
|| name == "rlm"
|| name == "run_workflow"
|| name == "await_workflow"
|| name.starts_with("spawn_")
@@ -62,6 +64,8 @@ mod tests {
#[test]
fn recursion_and_duplication_hazards_are_excluded() {
for name in [
"rhai",
"rhai_workflows",
"rlm",
"run_workflow",
"await_workflow",
@@ -81,7 +85,7 @@ mod tests {
/// turn's execution context.
///
/// Reads the parent's visible tool set, provider/model, and sub-agent
/// allowlist. The returned registry carries no `rlm`, `spawn_*`, or workflow
/// allowlist. The returned registry carries no `rhai`, `spawn_*`, or workflow
/// tools, and no `CliRpcOnly`-scoped tools.
pub(super) fn build_capability_registry(parent: &ParentExecutionContext) -> CapabilityRegistry<()> {
let mut registry = CapabilityRegistry::<()>::new();
@@ -107,7 +111,7 @@ pub(super) fn build_capability_registry(parent: &ParentExecutionContext) -> Capa
if matches!(tool.scope(), ToolScope::CliRpcOnly) {
continue;
}
registry.replace_tool(Arc::new(RlmToolAdapter::new(
registry.replace_tool(Arc::new(RhaiToolAdapter::new(
parent.all_tools.clone(),
tool.as_ref(),
)));
@@ -128,7 +132,7 @@ pub(super) fn build_capability_registry(parent: &ParentExecutionContext) -> Capa
tools = tool_count,
agents = agent_count,
model = %parent.model_name,
"[rlm] built capability registry"
"[rhai_workflows] built capability registry"
);
registry
}
@@ -137,7 +141,7 @@ pub(super) fn build_capability_registry(parent: &ParentExecutionContext) -> Capa
/// in the parent's shared tool set on each call (the set is `Arc`-shared, not
/// cloned). Adds the approval gate the harness `wrap_tool` middleware would
/// otherwise apply — absent on the repl bridge path.
pub(super) struct RlmToolAdapter {
pub(super) struct RhaiToolAdapter {
tools: Arc<Vec<Box<dyn OhTool>>>,
name: String,
description: String,
@@ -145,7 +149,7 @@ pub(super) struct RlmToolAdapter {
policy: TaToolPolicy,
}
impl RlmToolAdapter {
impl RhaiToolAdapter {
fn new(tools: Arc<Vec<Box<dyn OhTool>>>, tool: &dyn OhTool) -> Self {
let schema = TaToolSchema {
name: tool.name().to_string(),
@@ -171,7 +175,7 @@ impl RlmToolAdapter {
match found {
Some(tool) => gated_execute(tool.as_ref(), call, context).await,
None => {
tracing::warn!(tool = %self.name, "[rlm] bridged tool not found at call time");
tracing::warn!(tool = %self.name, "[rhai_workflows] bridged tool not found at call time");
TaToolResult {
call_id: call.id,
name: call.name,
@@ -186,7 +190,7 @@ impl RlmToolAdapter {
}
#[async_trait]
impl TaTool<()> for RlmToolAdapter {
impl TaTool<()> for RhaiToolAdapter {
fn name(&self) -> &str {
&self.name
}
@@ -229,12 +233,12 @@ async fn gated_execute(
if let Some(gate) = ApprovalGate::try_global() {
let summary = summarize_action(&call.name, &call.arguments);
let redacted = redact_args(&call.arguments);
tracing::debug!(tool = %call.name, "[rlm] external-effect tool — routing through approval gate");
tracing::debug!(tool = %call.name, "[rhai_workflows] external-effect tool — routing through approval gate");
let (outcome, request_id) =
gate.intercept_audited(&call.name, &summary, redacted).await;
match outcome {
GateOutcome::Deny { reason } => {
tracing::info!(tool = %call.name, %reason, "[rlm] tool denied by approval gate");
tracing::info!(tool = %call.name, %reason, "[rhai_workflows] tool denied by approval gate");
return TaToolResult {
content: format!("Denied by approval gate: {reason}"),
error: Some(format!("approval denied: {reason}")),
@@ -309,7 +313,7 @@ impl HarnessAgent for SubagentCapability {
..Default::default()
};
tracing::debug!(agent = %self.agent_id, "[rlm] agent_query — spawning sub-agent");
tracing::debug!(agent = %self.agent_id, "[rhai_workflows] agent_query — spawning sub-agent");
let outcome = with_parent_context(
self.parent.clone(),
run_subagent(definition, &input.prompt, options),
@@ -1,7 +1,7 @@
//! `rlm` — language-based workflows over the TinyAgents Rhai `.ragsh` REPL.
//! `rhai` — language-based workflows over the TinyAgents Rhai `.ragsh` REPL.
//!
//! Exposes `tinyagents::ReplSession` (the `repl` feature) as a first-class
//! [`RlmTool`], so the orchestrator can author and run its own workflow scripts
//! [`RhaiTool`], so the orchestrator can author and run its own workflow scripts
//! — fan-out, batched calls, loops, dedup/verify pipelines — bounded and
//! fail-closed. See [`README.md`](https://github.com/tinyhumansai/openhuman)
//! (module `README.md`) for the design.
@@ -17,4 +17,4 @@ mod types;
pub mod tools;
pub use tools::RlmTool;
pub use tools::RhaiTool;
@@ -17,9 +17,9 @@ use crate::openhuman::security::policy::AutonomyLevel;
use crate::openhuman::tinyagents::run_cancellation_context::current_run_cancellation;
use super::bridge::build_capability_registry;
use super::policy::{resolve_policy, DEFAULT_RLM_TIMEOUT_SECS};
use super::sessions::RlmSessionManager;
use super::types::{RlmCallSummary, RlmEvalRequest, RlmEvalResponse, RlmLimitsRemaining};
use super::policy::{resolve_policy, DEFAULT_RHAI_TIMEOUT_SECS};
use super::sessions::RhaiSessionManager;
use super::types::{RhaiCallSummary, RhaiEvalRequest, RhaiEvalResponse, RhaiLimitsRemaining};
/// Grace added to the inner policy timeout for the outer `spawn_blocking`
/// backstop — the inner deadline should always fire first; this defends against
@@ -28,12 +28,12 @@ const OUTER_TIMEOUT_GRACE: Duration = Duration::from_secs(5);
/// Hard cap on the characters returned to the model in `stdout`, so a runaway
/// print cannot flood the context window even within the byte policy.
const MAX_RLM_RESULT_CHARS: usize = 24_000;
const MAX_RHAI_RESULT_CHARS: usize = 24_000;
/// A typed RLM failure, each mapped to a model-consumable tool result by
/// [`RlmError::message`] (with a fix hint) and [`RlmError::kind`].
/// A typed Rhai failure, each mapped to a model-consumable tool result by
/// [`RhaiError::message`] (with a fix hint) and [`RhaiError::kind`].
#[derive(Debug, Clone)]
pub(crate) enum RlmError {
pub(crate) enum RhaiError {
/// The autonomy tier or policy refused the session.
Denied(String),
/// Script failed to compile or raised a runtime error.
@@ -56,60 +56,62 @@ pub(crate) enum RlmError {
Internal(String),
}
impl RlmError {
impl RhaiError {
/// A stable, snake_case kind tag for logging and the tool result.
pub(crate) fn kind(&self) -> &'static str {
match self {
RlmError::Denied(_) => "denied",
RlmError::Script(_) => "script_error",
RlmError::Timeout(_) => "timeout",
RlmError::LimitExceeded(_) => "limit_exceeded",
RlmError::UnknownCapability(_) => "unknown_capability",
RlmError::Depth(_) => "recursion_depth",
RlmError::CapabilityError(_) => "capability_error",
RlmError::Cancelled => "cancelled",
RlmError::SessionBusy(_) => "session_busy",
RlmError::Internal(_) => "internal",
RhaiError::Denied(_) => "denied",
RhaiError::Script(_) => "script_error",
RhaiError::Timeout(_) => "timeout",
RhaiError::LimitExceeded(_) => "limit_exceeded",
RhaiError::UnknownCapability(_) => "unknown_capability",
RhaiError::Depth(_) => "recursion_depth",
RhaiError::CapabilityError(_) => "capability_error",
RhaiError::Cancelled => "cancelled",
RhaiError::SessionBusy(_) => "session_busy",
RhaiError::Internal(_) => "internal",
}
}
/// The model-facing message, including a concrete fix hint.
pub(crate) fn message(&self) -> String {
match self {
RlmError::Denied(m) => m.clone(),
RlmError::Script(m) => {
format!("rlm script error: {m}\nFix the script and retry — reuse the same session_id to keep your bindings.")
RhaiError::Denied(m) => m.clone(),
RhaiError::Script(m) => {
format!("rhai_workflows script error: {m}\nFix the script and retry — reuse the same session_id to keep your bindings.")
}
RlmError::Timeout(m) => {
format!("rlm cell timed out: {m}\nSplit the work across cells, lower fan-out, or raise timeout_secs.")
RhaiError::Timeout(m) => {
format!("rhai_workflows cell timed out: {m}\nSplit the work across cells, lower fan-out, or raise timeout_secs.")
}
RlmError::LimitExceeded(m) => {
format!("rlm limit exceeded: {m}\nSplit the work across cells, or (full tier) raise the relevant limit in `limits`.")
RhaiError::LimitExceeded(m) => {
format!("rhai_workflows limit exceeded: {m}\nSplit the work across cells, or (full tier) raise the relevant limit in `limits`.")
}
RlmError::UnknownCapability(m) => format!("rlm unknown capability: {m}"),
RlmError::Depth(m) => format!("rlm recursion depth exceeded: {m}"),
RlmError::CapabilityError(m) => {
format!("rlm capability call failed: {m}\nInspect the failing call's arguments and retry.")
RhaiError::UnknownCapability(m) => {
format!("rhai_workflows unknown capability: {m}")
}
RlmError::Cancelled => {
"rlm cell was cancelled by the user. The session is intact and resumable."
RhaiError::Depth(m) => format!("rhai_workflows recursion depth exceeded: {m}"),
RhaiError::CapabilityError(m) => {
format!("rhai_workflows capability call failed: {m}\nInspect the failing call's arguments and retry.")
}
RhaiError::Cancelled => {
"rhai_workflows cell was cancelled by the user. The session is intact and resumable."
.to_string()
}
RlmError::SessionBusy(m) => m.clone(),
RlmError::Internal(m) => format!("rlm internal error: {m}"),
RhaiError::SessionBusy(m) => m.clone(),
RhaiError::Internal(m) => format!("rhai_workflows internal error: {m}"),
}
}
}
/// The registered capability names, snapshotted for unknown-capability error
/// messages so the model sees the live surface it can actually call.
struct RlmAvailable {
struct RhaiAvailable {
tools: Vec<String>,
agents: Vec<String>,
models: Vec<String>,
}
impl RlmAvailable {
impl RhaiAvailable {
fn from_registry(registry: &CapabilityRegistry<()>) -> Self {
Self {
tools: registry.names(ComponentKind::Tool),
@@ -130,17 +132,18 @@ enum CellRun {
Poisoned,
}
/// Evaluates one RLM cell against a (possibly new) session, fail-closed.
/// Evaluates one Rhai cell against a (possibly new) session, fail-closed.
///
/// Resolves the parent turn context, maps autonomy/timeout to a `ReplPolicy`,
/// resolves or creates the session, wires user cancellation to a fresh per-cell
/// flag, runs `eval_cell` on `spawn_blocking` under an outer `tokio::timeout`
/// backstop, and maps every outcome to [`RlmEvalResponse`] or a typed
/// [`RlmError`].
pub(crate) async fn eval_rlm_cell(req: RlmEvalRequest) -> Result<RlmEvalResponse, RlmError> {
/// backstop, and maps every outcome to [`RhaiEvalResponse`] or a typed
/// [`RhaiError`].
pub(crate) async fn eval_rhai_cell(req: RhaiEvalRequest) -> Result<RhaiEvalResponse, RhaiError> {
let parent = current_parent().ok_or_else(|| {
RlmError::Internal(
"no parent execution context — the rlm tool must run inside an agent turn".to_string(),
RhaiError::Internal(
"no parent execution context — the rhai_workflows tool must run inside an agent turn"
.to_string(),
)
})?;
@@ -148,7 +151,7 @@ pub(crate) async fn eval_rlm_cell(req: RlmEvalRequest) -> Result<RlmEvalResponse
.map(|p| p.autonomy)
.unwrap_or(AutonomyLevel::Supervised);
let policy =
resolve_policy(tier, req.timeout_secs, req.limits.as_ref()).map_err(RlmError::Denied)?;
resolve_policy(tier, req.timeout_secs, req.limits.as_ref()).map_err(RhaiError::Denied)?;
let registry = build_capability_registry(&parent);
run_cell(
@@ -156,35 +159,35 @@ pub(crate) async fn eval_rlm_cell(req: RlmEvalRequest) -> Result<RlmEvalResponse
policy,
registry,
&parent.session_id,
RlmSessionManager::global(),
RhaiSessionManager::global(),
)
.await
}
/// The parent-independent core of [`eval_rlm_cell`]: given a resolved policy and
/// The parent-independent core of [`eval_rhai_cell`]: given a resolved policy and
/// a prebuilt capability registry, resolves/creates the session, runs the cell
/// under the layered time bounds and cancellation wiring, and maps the outcome.
///
/// Split out so the full evaluation path is unit-testable with a hand-built
/// registry, without constructing a whole `ParentExecutionContext`.
async fn run_cell(
req: RlmEvalRequest,
req: RhaiEvalRequest,
policy: tinyagents::ReplPolicy,
registry: CapabilityRegistry<()>,
thread_scope: &str,
manager: &RlmSessionManager,
) -> Result<RlmEvalResponse, RlmError> {
manager: &RhaiSessionManager,
) -> Result<RhaiEvalResponse, RhaiError> {
let session_id = req
.session_id
.clone()
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| format!("rlm-{}", uuid::Uuid::new_v4()));
let key = RlmSessionManager::session_key(thread_scope, &session_id);
.unwrap_or_else(|| format!("rhai_workflows-{}", uuid::Uuid::new_v4()));
let key = RhaiSessionManager::session_key(thread_scope, &session_id);
// Snapshot the registry's names for error messages, then hand it to the
// session builder (used only on a fresh session; a reused session keeps its
// own registry).
let available = RlmAvailable::from_registry(&registry);
let available = RhaiAvailable::from_registry(&registry);
let policy_for_build = policy.clone();
tracing::info!(
@@ -192,7 +195,7 @@ async fn run_cell(
thread_id = %thread_scope,
session_id = %session_id,
script_bytes = req.script.len(),
"[rlm] eval_rlm_cell: start"
"[rhai_workflows] eval_rhai_cell: start"
);
let handle = manager.get_or_create(&key, move || {
@@ -209,7 +212,9 @@ async fn run_cell(
let flag = cell_flag.clone();
tokio::spawn(async move {
token.cancelled().await;
tracing::debug!("[rlm] run cancellation observed — tripping cell cancel flag");
tracing::debug!(
"[rhai_workflows] run cancellation observed — tripping cell cancel flag"
);
flag.cancel();
});
}
@@ -219,7 +224,7 @@ async fn run_cell(
let flag_for_cell = cell_flag.clone();
let outer_bound = policy
.timeout
.unwrap_or(Duration::from_secs(DEFAULT_RLM_TIMEOUT_SECS))
.unwrap_or(Duration::from_secs(DEFAULT_RHAI_TIMEOUT_SECS))
+ OUTER_TIMEOUT_GRACE;
let join = tokio::task::spawn_blocking(move || {
@@ -238,9 +243,9 @@ async fn run_cell(
Ok(Err(join_err)) => {
// The blocking task panicked — the session may be poisoned; drop it.
manager.close(&key);
tracing::error!(session_key = %key, %join_err, "[rlm] cell task panicked — session dropped");
return Err(RlmError::Internal(format!(
"rlm cell task failed: {join_err}"
tracing::error!(session_key = %key, %join_err, "[rhai_workflows] cell task panicked — session dropped");
return Err(RhaiError::Internal(format!(
"rhai_workflows cell task failed: {join_err}"
)));
}
Err(_elapsed) => {
@@ -250,10 +255,10 @@ async fn run_cell(
tracing::error!(
session_key = %key,
outer_secs = outer_bound.as_secs(),
"[rlm] outer wall-clock backstop fired — session dropped (inner deadline should have fired first)"
"[rhai_workflows] outer wall-clock backstop fired — session dropped (inner deadline should have fired first)"
);
return Err(RlmError::Timeout(
"the rlm cell exceeded its outer wall-clock backstop".to_string(),
return Err(RhaiError::Timeout(
"the rhai_workflows cell exceeded its outer wall-clock backstop".to_string(),
));
}
};
@@ -261,15 +266,15 @@ async fn run_cell(
let eval = match run {
CellRun::Completed(eval) => eval,
CellRun::Busy => {
tracing::info!(session_key = %key, "[rlm] session busy — concurrent cell rejected");
return Err(RlmError::SessionBusy(format!(
"rlm session '{session_id}' is already evaluating a cell; wait for it to finish or use a different session_id"
tracing::info!(session_key = %key, "[rhai_workflows] session busy — concurrent cell rejected");
return Err(RhaiError::SessionBusy(format!(
"rhai_workflows session '{session_id}' is already evaluating a cell; wait for it to finish or use a different session_id"
)));
}
CellRun::Poisoned => {
manager.close(&key);
return Err(RlmError::Internal(
"the rlm session was poisoned by a prior panic and has been dropped; start a fresh session".to_string(),
return Err(RhaiError::Internal(
"the rhai_workflows session was poisoned by a prior panic and has been dropped; start a fresh session".to_string(),
));
}
};
@@ -278,7 +283,7 @@ async fn run_cell(
Ok(result) => result,
Err(err) => {
let mapped = map_eval_error(err, &available);
tracing::info!(session_key = %key, kind = mapped.kind(), "[rlm] cell failed");
tracing::info!(session_key = %key, kind = mapped.kind(), "[rhai_workflows] cell failed");
return Err(mapped);
}
};
@@ -288,16 +293,16 @@ async fn run_cell(
manager.close(&key);
}
let response = RlmEvalResponse {
let response = RhaiEvalResponse {
session_id,
stdout: truncate_chars(&result.stdout, MAX_RLM_RESULT_CHARS),
stdout: truncate_chars(&result.stdout, MAX_RHAI_RESULT_CHARS),
value: result.value.as_ref().map(|v| v.to_json()),
variables_changed: result.variables_changed,
calls: summarize_calls(&result.calls),
final_answer: result.final_answer,
elapsed_ms: result.elapsed.as_millis() as u64,
cells_used: stats.cells,
limits_remaining: RlmLimitsRemaining {
limits_remaining: RhaiLimitsRemaining {
cells: policy.max_iterations.saturating_sub(stats.cells),
model_calls: policy.max_model_calls.saturating_sub(stats.model_calls),
tool_calls: policy.max_tool_calls.saturating_sub(stats.tool_calls),
@@ -310,45 +315,45 @@ async fn run_cell(
elapsed_ms = response.elapsed_ms,
calls = response.calls.len(),
cells_used = response.cells_used,
"[rlm] eval_rlm_cell: done"
"[rhai_workflows] eval_rhai_cell: done"
);
Ok(response)
}
/// Maps a raw `TinyAgentsError` from `eval_cell` onto a typed [`RlmError`],
/// Maps a raw `TinyAgentsError` from `eval_cell` onto a typed [`RhaiError`],
/// enriching unknown-capability errors with the live registered names.
fn map_eval_error(err: TinyAgentsError, available: &RlmAvailable) -> RlmError {
fn map_eval_error(err: TinyAgentsError, available: &RhaiAvailable) -> RhaiError {
match err {
TinyAgentsError::Validation(m) => RlmError::Script(m),
TinyAgentsError::Timeout(m) => RlmError::Timeout(m),
TinyAgentsError::LimitExceeded(m) => RlmError::LimitExceeded(m),
TinyAgentsError::Cancelled => RlmError::Cancelled,
TinyAgentsError::SubAgentDepth(n) => RlmError::Depth(format!(
TinyAgentsError::Validation(m) => RhaiError::Script(m),
TinyAgentsError::Timeout(m) => RhaiError::Timeout(m),
TinyAgentsError::LimitExceeded(m) => RhaiError::LimitExceeded(m),
TinyAgentsError::Cancelled => RhaiError::Cancelled,
TinyAgentsError::SubAgentDepth(n) => RhaiError::Depth(format!(
"sub-agent recursion exceeded the maximum depth of {n}"
)),
TinyAgentsError::ModelNotFound(name) => RlmError::UnknownCapability(format!(
TinyAgentsError::ModelNotFound(name) => RhaiError::UnknownCapability(format!(
"model `{name}` is not registered. Available models: {}",
join_or_none(&available.models)
)),
TinyAgentsError::ToolNotFound(name) => RlmError::UnknownCapability(format!(
TinyAgentsError::ToolNotFound(name) => RhaiError::UnknownCapability(format!(
"tool `{name}` is not registered. Available tools: {}",
join_or_none(&available.tools)
)),
TinyAgentsError::Capability(m) => RlmError::UnknownCapability(format!(
TinyAgentsError::Capability(m) => RhaiError::UnknownCapability(format!(
"{m}. Available agents: {}",
join_or_none(&available.agents)
)),
TinyAgentsError::Tool(m) => RlmError::CapabilityError(m),
other => RlmError::Script(other.to_string()),
TinyAgentsError::Tool(m) => RhaiError::CapabilityError(m),
other => RhaiError::Script(other.to_string()),
}
}
/// Summarizes recorded calls into the model-visible shape — kind, name, timing,
/// success only, never raw arguments/payloads.
fn summarize_calls(calls: &[ReplCallRecord]) -> Vec<RlmCallSummary> {
fn summarize_calls(calls: &[ReplCallRecord]) -> Vec<RhaiCallSummary> {
calls
.iter()
.map(|c| RlmCallSummary {
.map(|c| RhaiCallSummary {
kind: call_kind_str(c.kind).to_string(),
name: c.name.clone(),
elapsed_ms: c.elapsed.as_millis() as u64,
@@ -383,7 +388,7 @@ fn truncate_chars(s: &str, max: usize) -> String {
return s.to_string();
}
let mut out: String = s.chars().take(max).collect();
out.push_str("\n… (rlm output truncated)");
out.push_str("\n… (rhai_workflows output truncated)");
out
}
@@ -410,16 +415,16 @@ mod tests {
#[test]
fn error_kinds_are_stable() {
assert_eq!(RlmError::Cancelled.kind(), "cancelled");
assert_eq!(RlmError::Timeout("x".into()).kind(), "timeout");
assert!(RlmError::Script("boom".into())
assert_eq!(RhaiError::Cancelled.kind(), "cancelled");
assert_eq!(RhaiError::Timeout("x".into()).kind(), "timeout");
assert!(RhaiError::Script("boom".into())
.message()
.contains("Fix the script"));
}
#[test]
fn every_taxonomy_variant_maps_to_the_right_kind() {
let avail = RlmAvailable {
let avail = RhaiAvailable {
tools: vec!["read_file".into(), "grep".into()],
agents: vec!["researcher".into()],
models: vec!["chat".into()],
@@ -494,8 +499,8 @@ mod tests {
registry
}
fn req(script: &str) -> RlmEvalRequest {
RlmEvalRequest {
fn req(script: &str) -> RhaiEvalRequest {
RhaiEvalRequest {
script: script.to_string(),
..Default::default()
}
@@ -503,7 +508,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn happy_path_runs_a_tool_call_and_reports_usage() {
let manager = RlmSessionManager::new_for_test();
let manager = RhaiSessionManager::new_for_test();
let resp = run_cell(
req(r#"tool_call(#{ tool: "echo", arguments: #{ msg: "hi" } })"#),
ReplPolicy::default(),
@@ -526,7 +531,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn parse_error_is_a_script_error() {
let manager = RlmSessionManager::new_for_test();
let manager = RhaiSessionManager::new_for_test();
let err = run_cell(
req("let x = ;"),
ReplPolicy::default(),
@@ -541,7 +546,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn unknown_tool_lists_registered_names() {
let manager = RlmSessionManager::new_for_test();
let manager = RhaiSessionManager::new_for_test();
let err = run_cell(
req(r#"tool_call(#{ tool: "does_not_exist" })"#),
ReplPolicy::default(),
@@ -557,7 +562,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn call_count_limit_fails_closed() {
let manager = RlmSessionManager::new_for_test();
let manager = RhaiSessionManager::new_for_test();
let policy = ReplPolicy {
max_tool_calls: 1,
..ReplPolicy::default()
@@ -576,7 +581,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn script_loop_times_out_within_the_bound() {
let manager = RlmSessionManager::new_for_test();
let manager = RhaiSessionManager::new_for_test();
let policy = ReplPolicy {
timeout: Some(Duration::from_millis(200)),
max_operations: 0,
@@ -596,7 +601,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn oversized_output_fails_closed() {
let manager = RlmSessionManager::new_for_test();
let manager = RhaiSessionManager::new_for_test();
let policy = ReplPolicy {
max_output_bytes: 64,
..ReplPolicy::default()
@@ -615,9 +620,9 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn namespace_persists_and_close_session_drops_it() {
let manager = RlmSessionManager::new_for_test();
let manager = RhaiSessionManager::new_for_test();
let first = run_cell(
RlmEvalRequest {
RhaiEvalRequest {
script: "let acc = 10; acc".into(),
session_id: Some("keep".into()),
..Default::default()
@@ -632,7 +637,7 @@ mod tests {
assert_eq!(first.session_id, "keep");
let second = run_cell(
RlmEvalRequest {
RhaiEvalRequest {
script: "acc + 5".into(),
session_id: Some("keep".into()),
close_session: true,
@@ -652,14 +657,14 @@ mod tests {
#[tokio::test(flavor = "current_thread")]
async fn concurrent_cell_on_a_busy_session_is_rejected() {
let manager = RlmSessionManager::new_for_test();
let key = RlmSessionManager::session_key("t", "busy");
let manager = RhaiSessionManager::new_for_test();
let key = RhaiSessionManager::session_key("t", "busy");
let handle = manager.get_or_create(&key, || tinyagents::ReplSession::<()>::new());
// Hold the session lock to simulate an in-flight cell.
let _guard = handle.session.lock().unwrap();
let res = run_cell(
RlmEvalRequest {
RhaiEvalRequest {
script: "1".into(),
session_id: Some("busy".into()),
..Default::default()
@@ -670,6 +675,6 @@ mod tests {
&manager,
)
.await;
assert!(matches!(res, Err(RlmError::SessionBusy(_))), "got {res:?}");
assert!(matches!(res, Err(RhaiError::SessionBusy(_))), "got {res:?}");
}
}
@@ -1,7 +1,7 @@
//! Maps openhuman's autonomy tier and tool-timeout clamps onto a
//! [`tinyagents::ReplPolicy`], fail-closed.
//!
//! Every RLM session is bounded: the resolved policy is always based on the
//! Every Rhai session is bounded: the resolved policy is always based on the
//! conservative crate default, the wall-clock timeout is always set (never
//! unbounded), the `readonly` tier is refused outright, and caller-supplied
//! limit overrides are clamped — a `full`-tier caller may raise call-count
@@ -14,38 +14,38 @@ use tinyagents::ReplPolicy;
use crate::openhuman::security::policy::AutonomyLevel;
use crate::openhuman::tool_timeout;
use super::types::RlmLimitsOverride;
use super::types::RhaiLimitsOverride;
/// Default per-cell wall-clock timeout when the caller does not specify one.
/// Matches the `rlm` tool's default `ToolTimeout::Secs(300)` so the inner
/// Matches the `rhai_workflows` tool's default `ToolTimeout::Secs(300)` so the inner
/// deadline and the harness backstop agree.
pub const DEFAULT_RLM_TIMEOUT_SECS: u64 = 300;
pub const DEFAULT_RHAI_TIMEOUT_SECS: u64 = 300;
/// Hard upper bound on batched concurrency, regardless of tier or override.
pub const MAX_RLM_CONCURRENCY: usize = 8;
pub const MAX_RHAI_CONCURRENCY: usize = 8;
/// Ceiling multiplier applied to the crate default call-count limits when a
/// `full`-tier caller raises them via the tool's `limits` argument.
pub const LIMIT_CEILING_MULTIPLIER: usize = 2;
/// Maximum sub-agent recursion depth an RLM session may drive. Mirrors the
/// Maximum sub-agent recursion depth an Rhai session may drive. Mirrors the
/// harness `MAX_SPAWN_DEPTH` (kept in lock-step by intent; a divergence only
/// tightens one side, never opens an unbounded path).
pub const RLM_MAX_DEPTH: usize = 3;
pub const RHAI_MAX_DEPTH: usize = 3;
/// Resolves a [`ReplPolicy`] for a session opened at autonomy `tier`, with an
/// optional caller `timeout_secs` and `overrides`.
///
/// Returns `Err(reason)` — a model-consumable string — when the tier forbids
/// RLM entirely (`readonly`). The `reason` is surfaced verbatim to the model.
/// Rhai entirely (`readonly`). The `reason` is surfaced verbatim to the model.
pub fn resolve_policy(
tier: AutonomyLevel,
timeout_secs: Option<u64>,
overrides: Option<&RlmLimitsOverride>,
overrides: Option<&RhaiLimitsOverride>,
) -> Result<ReplPolicy, String> {
if tier == AutonomyLevel::ReadOnly {
return Err(
"the `rlm` tool is disabled at the read-only autonomy tier (it can drive tools, \
"the `rhai_workflows` tool is disabled at the read-only autonomy tier (it can drive tools, \
models, and sub-agents); raise autonomy to `supervised` or `full` to use it"
.to_string(),
);
@@ -55,15 +55,15 @@ pub fn resolve_policy(
let base = ReplPolicy::default();
// Wall-clock timeout: clamp the caller's request to [1, 3600] and cap it,
// defaulting to DEFAULT_RLM_TIMEOUT_SECS. Never unbounded.
// defaulting to DEFAULT_RHAI_TIMEOUT_SECS. Never unbounded.
let secs =
tool_timeout::explicit_call_timeout_secs(timeout_secs, tool_timeout::MAX_TIMEOUT_SECS)
.unwrap_or(DEFAULT_RLM_TIMEOUT_SECS);
.unwrap_or(DEFAULT_RHAI_TIMEOUT_SECS);
let ov = overrides.cloned().unwrap_or_default();
let policy = ReplPolicy {
timeout: Some(Duration::from_secs(secs)),
max_depth: base.max_depth.min(RLM_MAX_DEPTH),
max_depth: base.max_depth.min(RHAI_MAX_DEPTH),
max_model_calls: clamp_limit(base.max_model_calls, ov.max_model_calls, allow_raise),
max_agent_calls: clamp_limit(base.max_agent_calls, ov.max_agent_calls, allow_raise),
max_tool_calls: clamp_limit(base.max_tool_calls, ov.max_tool_calls, allow_raise),
@@ -79,7 +79,7 @@ pub fn resolve_policy(
max_tool_calls = policy.max_tool_calls,
max_concurrency = policy.max_concurrency,
max_depth = policy.max_depth,
"[rlm] resolved ReplPolicy"
"[rhai_workflows] resolved ReplPolicy"
);
Ok(policy)
}
@@ -103,16 +103,16 @@ fn clamp_limit(default: usize, requested: Option<usize>, allow_raise: bool) -> u
}
}
/// Clamps batched concurrency to `[1, MAX_RLM_CONCURRENCY]`, and to the default
/// Clamps batched concurrency to `[1, MAX_RHAI_CONCURRENCY]`, and to the default
/// unless the tier allows raising it.
fn clamp_concurrency(default: usize, requested: Option<usize>, allow_raise: bool) -> usize {
let ceiling = if allow_raise {
MAX_RLM_CONCURRENCY
MAX_RHAI_CONCURRENCY
} else {
default.min(MAX_RLM_CONCURRENCY)
default.min(MAX_RHAI_CONCURRENCY)
};
match requested {
None => default.min(MAX_RLM_CONCURRENCY),
None => default.min(MAX_RHAI_CONCURRENCY),
Some(n) => n.clamp(1, ceiling.max(1)),
}
}
@@ -136,7 +136,7 @@ mod tests {
let p = resolve_policy(AutonomyLevel::Supervised, None, None).expect("policy");
assert_eq!(
p.timeout,
Some(Duration::from_secs(DEFAULT_RLM_TIMEOUT_SECS))
Some(Duration::from_secs(DEFAULT_RHAI_TIMEOUT_SECS))
);
// A caller request is clamped to [1, 3600].
@@ -145,14 +145,14 @@ mod tests {
let p = resolve_policy(AutonomyLevel::Supervised, Some(0), None).expect("policy");
assert_eq!(
p.timeout,
Some(Duration::from_secs(DEFAULT_RLM_TIMEOUT_SECS))
Some(Duration::from_secs(DEFAULT_RHAI_TIMEOUT_SECS))
);
}
#[test]
fn supervised_may_only_tighten_limits() {
let base = ReplPolicy::default();
let ov = RlmLimitsOverride {
let ov = RhaiLimitsOverride {
max_tool_calls: Some(base.max_tool_calls * 10), // request a raise
..Default::default()
};
@@ -162,7 +162,7 @@ mod tests {
"supervised cannot raise above the default"
);
let ov = RlmLimitsOverride {
let ov = RhaiLimitsOverride {
max_tool_calls: Some(1),
..Default::default()
};
@@ -173,7 +173,7 @@ mod tests {
#[test]
fn full_may_raise_up_to_the_ceiling() {
let base = ReplPolicy::default();
let ov = RlmLimitsOverride {
let ov = RhaiLimitsOverride {
max_model_calls: Some(base.max_model_calls * 100), // way over ceiling
..Default::default()
};
@@ -187,17 +187,17 @@ mod tests {
#[test]
fn concurrency_is_hard_capped() {
let ov = RlmLimitsOverride {
let ov = RhaiLimitsOverride {
max_concurrency: Some(1000),
..Default::default()
};
let p = resolve_policy(AutonomyLevel::Full, None, Some(&ov)).expect("policy");
assert_eq!(p.max_concurrency, MAX_RLM_CONCURRENCY);
assert_eq!(p.max_concurrency, MAX_RHAI_CONCURRENCY);
}
#[test]
fn depth_respects_the_spawn_ceiling() {
let p = resolve_policy(AutonomyLevel::Full, None, None).expect("policy");
assert!(p.max_depth <= RLM_MAX_DEPTH);
assert!(p.max_depth <= RHAI_MAX_DEPTH);
}
}
@@ -60,16 +60,16 @@ pub(super) struct CellStats {
}
/// The process-global session manager.
pub(super) struct RlmSessionManager {
pub(super) struct RhaiSessionManager {
inner: Mutex<HashMap<String, SessionSlot>>,
}
static MANAGER: OnceLock<RlmSessionManager> = OnceLock::new();
static MANAGER: OnceLock<RhaiSessionManager> = OnceLock::new();
impl RlmSessionManager {
impl RhaiSessionManager {
/// Returns the process-global manager, initialising it on first use.
pub(super) fn global() -> &'static RlmSessionManager {
MANAGER.get_or_init(|| RlmSessionManager {
pub(super) fn global() -> &'static RhaiSessionManager {
MANAGER.get_or_init(|| RhaiSessionManager {
inner: Mutex::new(HashMap::new()),
})
}
@@ -97,7 +97,10 @@ impl RlmSessionManager {
let now = Instant::now();
if let Some(slot) = map.get_mut(key) {
slot.last_access = now;
tracing::debug!(session_key = key, "[rlm] reusing existing session");
tracing::debug!(
session_key = key,
"[rhai_workflows] reusing existing session"
);
return SlotHandle {
session: slot.session.clone(),
cancel: slot.cancel.clone(),
@@ -122,7 +125,7 @@ impl RlmSessionManager {
tracing::debug!(
session_key = key,
live_sessions = map.len(),
"[rlm] created new session"
"[rhai_workflows] created new session"
);
SlotHandle {
session,
@@ -159,7 +162,7 @@ impl RlmSessionManager {
/// session that must never be reused).
pub(super) fn close(&self, key: &str) {
if self.lock().remove(key).is_some() {
tracing::debug!(session_key = key, "[rlm] closed session");
tracing::debug!(session_key = key, "[rhai_workflows] closed session");
}
}
@@ -171,7 +174,7 @@ impl RlmSessionManager {
map.retain(|k, slot| {
let keep = now.duration_since(slot.last_access) < IDLE_TTL;
if !keep {
tracing::debug!(session_key = %k, "[rlm] evicting idle session (TTL)");
tracing::debug!(session_key = %k, "[rhai_workflows] evicting idle session (TTL)");
}
keep
});
@@ -185,7 +188,7 @@ impl RlmSessionManager {
.map(|(k, _)| k.clone())
{
map.remove(&lru_key);
tracing::debug!(session_key = %lru_key, "[rlm] evicting LRU session (cap)");
tracing::debug!(session_key = %lru_key, "[rhai_workflows] evicting LRU session (cap)");
}
}
@@ -199,7 +202,7 @@ impl RlmSessionManager {
/// the process-global singleton and from each other.
#[cfg(test)]
pub(super) fn new_for_test() -> Self {
RlmSessionManager {
RhaiSessionManager {
inner: Mutex::new(HashMap::new()),
}
}
@@ -216,8 +219,8 @@ mod tests {
#[test]
fn namespace_persists_across_cells_in_one_session() {
let manager = RlmSessionManager::new_for_test();
let key = RlmSessionManager::session_key("t", "s1");
let manager = RhaiSessionManager::new_for_test();
let key = RhaiSessionManager::session_key("t", "s1");
let handle = manager.get_or_create(&key, build_session);
assert!(handle.fresh);
@@ -243,20 +246,20 @@ mod tests {
#[test]
fn distinct_session_keys_are_isolated() {
let manager = RlmSessionManager::new_for_test();
let a = manager.get_or_create(&RlmSessionManager::session_key("t", "a"), build_session);
let manager = RhaiSessionManager::new_for_test();
let a = manager.get_or_create(&RhaiSessionManager::session_key("t", "a"), build_session);
a.session.lock().unwrap().eval_cell("let x = 1;").unwrap();
// A different key has its own namespace, so `x` is undefined there.
let b = manager.get_or_create(&RlmSessionManager::session_key("t", "b"), build_session);
let b = manager.get_or_create(&RhaiSessionManager::session_key("t", "b"), build_session);
assert!(b.session.lock().unwrap().eval_cell("x").is_err());
}
#[test]
fn thread_scope_isolates_the_same_session_id() {
let manager = RlmSessionManager::new_for_test();
let k1 = RlmSessionManager::session_key("thread-1", "shared");
let k2 = RlmSessionManager::session_key("thread-2", "shared");
let manager = RhaiSessionManager::new_for_test();
let k1 = RhaiSessionManager::session_key("thread-1", "shared");
let k2 = RhaiSessionManager::session_key("thread-2", "shared");
assert_ne!(k1, k2);
manager.get_or_create(&k1, build_session);
let h2 = manager.get_or_create(&k2, build_session);
@@ -268,10 +271,10 @@ mod tests {
#[test]
fn lru_cap_bounds_the_number_of_live_sessions() {
let manager = RlmSessionManager::new_for_test();
let manager = RhaiSessionManager::new_for_test();
for i in 0..(MAX_SESSIONS + 5) {
manager.get_or_create(
&RlmSessionManager::session_key("t", &format!("s{i}")),
&RhaiSessionManager::session_key("t", &format!("s{i}")),
build_session,
);
}
@@ -284,8 +287,8 @@ mod tests {
#[test]
fn close_drops_the_session() {
let manager = RlmSessionManager::new_for_test();
let key = RlmSessionManager::session_key("t", "closeme");
let manager = RhaiSessionManager::new_for_test();
let key = RhaiSessionManager::session_key("t", "closeme");
manager.get_or_create(&key, build_session);
assert_eq!(manager.len(), 1);
manager.close(&key);
@@ -296,8 +299,8 @@ mod tests {
#[test]
fn finish_cell_accumulates_and_bounds_are_reported() {
let manager = RlmSessionManager::new_for_test();
let key = RlmSessionManager::session_key("t", "stats");
let manager = RhaiSessionManager::new_for_test();
let key = RhaiSessionManager::session_key("t", "stats");
let policy = ReplPolicy::default();
let handle = manager.get_or_create(&key, || {
ReplSession::<()>::new().with_policy(policy.clone())
@@ -1,39 +1,39 @@
//! The first-class `rlm` tool — the orchestrator's language-workflow surface.
//! The first-class `rhai_workflows` tool — the orchestrator's language-workflow surface.
//!
//! One tool call evaluates one Rhai cell against a persistent session
//! namespace. All effectful work a cell performs goes through the bridged inner
//! tools/models/sub-agents (each carrying its own approval/permission gates in
//! [`super::bridge`]), so the `rlm` tool itself declares no external effect.
//! [`super::bridge`]), so the `rhai_workflows` tool itself declares no external effect.
use async_trait::async_trait;
use serde_json::{json, Value};
use crate::openhuman::tools::traits::{PermissionLevel, Tool, ToolResult, ToolScope, ToolTimeout};
use super::policy::DEFAULT_RLM_TIMEOUT_SECS;
use super::types::RlmEvalRequest;
use super::policy::DEFAULT_RHAI_TIMEOUT_SECS;
use super::types::RhaiEvalRequest;
/// The `rlm` language-workflow tool. Stateless: it resolves the parent turn
/// The `rhai_workflows` language-workflow tool. Stateless: it resolves the parent turn
/// context, autonomy tier, and cancellation per call.
pub struct RlmTool;
pub struct RhaiTool;
impl RlmTool {
impl RhaiTool {
/// Builds the tool.
pub fn new() -> Self {
Self
}
}
impl Default for RlmTool {
impl Default for RhaiTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for RlmTool {
impl Tool for RhaiTool {
fn name(&self) -> &str {
"rlm"
"rhai_workflows"
}
fn description(&self) -> &str {
@@ -52,7 +52,7 @@ In-cell built-ins:\n\
\n\
Bounds (fail-closed): per-cell wall-clock timeout, and per-session caps on model/tool/agent \
calls, iterations, output bytes, and recursion depth. Exceeding one returns an error you can \
fix and retry in the same session. Excluded from cells: `rlm` itself, `spawn_*`, and workflow tools.\n\
fix and retry in the same session. Excluded from cells: `rhai_workflows` itself, `spawn_*`, and workflow tools.\n\
\n\
Example parallel fan-out over sub-agents, then reduce:\n\
```\n\
@@ -71,7 +71,7 @@ for r in reads { if r.ok && r.content.contains(\"TODO\") { hits.push(r.content);
hits\n\
```\n\
\n\
Prefer `rlm` over spawn_parallel_agents when you need loops, conditionals, or a dedup/verify \
Prefer `rhai_workflows` over spawn_parallel_agents when you need loops, conditionals, or a dedup/verify \
pipeline over results. Pass a `session_id` to continue a prior cell's bindings; set \
`close_session: true` when done."
}
@@ -86,7 +86,7 @@ pipeline over results. Pass a `session_id` to continue a prior cell's bindings;
},
"session_id": {
"type": "string",
"description": "Continue a prior RLM session's namespace; omit for a fresh session (its generated id is returned)."
"description": "Continue a prior Rhai session's namespace; omit for a fresh session (its generated id is returned)."
},
"timeout_secs": {
"type": "integer",
@@ -132,12 +132,12 @@ pipeline over results. Pass a `session_id` to continue a prior cell's bindings;
requested,
crate::openhuman::tool_timeout::MAX_TIMEOUT_SECS,
)
.unwrap_or(DEFAULT_RLM_TIMEOUT_SECS);
.unwrap_or(DEFAULT_RHAI_TIMEOUT_SECS);
ToolTimeout::Secs(secs)
}
fn display_label(&self, _args: &Value) -> Option<String> {
Some("running RLM workflow".to_string())
Some("running Rhai workflow".to_string())
}
fn display_detail(&self, args: &Value) -> Option<String> {
@@ -155,30 +155,33 @@ pipeline over results. Pass a `session_id` to continue a prior cell's bindings;
}
async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
let req: RlmEvalRequest = match serde_json::from_value(args) {
let req: RhaiEvalRequest = match serde_json::from_value(args) {
Ok(req) => req,
Err(err) => {
return Ok(ToolResult::error(format!(
"rlm: invalid arguments: {err}. Required: `script` (string). \
"rhai_workflows: invalid arguments: {err}. Required: `script` (string). \
Optional: session_id, timeout_secs (13600), limits, close_session."
)));
}
};
if req.script.trim().is_empty() {
return Ok(ToolResult::error(
"rlm: `script` is required and must be a non-empty Rhai cell.",
"rhai_workflows: `script` is required and must be a non-empty Rhai cell.",
));
}
match super::ops::eval_rlm_cell(req).await {
match super::ops::eval_rhai_cell(req).await {
Ok(response) => match serde_json::to_value(&response) {
Ok(value) => Ok(ToolResult::json(value)),
Err(err) => Ok(ToolResult::error(format!(
"rlm: internal error rendering result: {err}"
"rhai_workflows: internal error rendering result: {err}"
))),
},
Err(err) => {
log::info!("[rlm] tool returning error result (kind={})", err.kind());
log::info!(
"[rhai_workflows] tool returning error result (kind={})",
err.kind()
);
Ok(ToolResult::error(err.message()))
}
}
@@ -191,8 +194,8 @@ mod tests {
#[test]
fn metadata_is_stable() {
let tool = RlmTool::new();
assert_eq!(tool.name(), "rlm");
let tool = RhaiTool::new();
assert_eq!(tool.name(), "rhai_workflows");
assert_eq!(tool.permission_level(), PermissionLevel::Execute);
assert!(matches!(tool.scope(), ToolScope::AgentOnly));
assert!(!tool.external_effect());
@@ -200,10 +203,10 @@ mod tests {
#[test]
fn timeout_is_always_bounded() {
let tool = RlmTool::new();
let tool = RhaiTool::new();
assert_eq!(
tool.timeout_policy(&json!({})),
ToolTimeout::Secs(DEFAULT_RLM_TIMEOUT_SECS)
ToolTimeout::Secs(DEFAULT_RHAI_TIMEOUT_SECS)
);
assert_eq!(
tool.timeout_policy(&json!({ "timeout_secs": 42 })),
@@ -218,7 +221,7 @@ mod tests {
#[test]
fn display_detail_elides_first_script_line() {
let tool = RlmTool::new();
let tool = RhaiTool::new();
let detail = tool
.display_detail(&json!({ "script": "\n\nlet x = 1;\nlet y = 2;" }))
.expect("detail");
@@ -227,7 +230,7 @@ mod tests {
#[tokio::test]
async fn empty_script_is_a_model_consumable_error() {
let tool = RlmTool::new();
let tool = RhaiTool::new();
let result = tool
.execute(json!({ "script": " " }))
.await
@@ -237,7 +240,7 @@ mod tests {
#[tokio::test]
async fn missing_script_is_a_model_consumable_error() {
let tool = RlmTool::new();
let tool = RhaiTool::new();
// No `script` field at all → invalid arguments, not a panic.
let result = tool
.execute(json!({ "session_id": "x" }))
@@ -249,7 +252,7 @@ mod tests {
#[test]
fn schema_requires_script() {
let schema = RlmTool::new().parameters_schema();
let schema = RhaiTool::new().parameters_schema();
assert_eq!(schema["required"], json!(["script"]));
}
}
@@ -1,6 +1,6 @@
//! Serde domain types for the `rlm` language-workflow tool.
//! Serde domain types for the `rhai` language-workflow tool.
//!
//! These cross the tool boundary (parsed from the `rlm` tool call, and rendered
//! These cross the tool boundary (parsed from the `rhai_workflows` tool call, and rendered
//! back into its [`ToolResult`](crate::openhuman::tools::ToolResult)). The
//! runtime types that wire a session to openhuman's tools/models/subagents live
//! in [`super::bridge`] and [`super::sessions`]; the tinyagents-side limit and
@@ -8,32 +8,32 @@
use serde::{Deserialize, Serialize};
/// A caller-supplied RLM session id. Continuing a prior `session_id` reuses that
/// A caller-supplied Rhai session id. Continuing a prior `session_id` reuses that
/// session's persistent namespace (`let` bindings survive across cells); an
/// absent id starts a fresh session. Namespaces are additionally scoped by the
/// parent thread inside [`super::sessions`], so two chats never collide.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RlmSessionId(pub String);
pub struct RhaiSessionId(pub String);
impl RlmSessionId {
impl RhaiSessionId {
/// The session id as a string slice.
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for RlmSessionId {
impl std::fmt::Display for RhaiSessionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
/// Per-call limit overrides a caller may pass in the `rlm` tool's `limits`
/// Per-call limit overrides a caller may pass in the `rhai_workflows` tool's `limits`
/// argument. Each is clamped in [`super::policy`] to a hard ceiling (never
/// unbounded); a `full`-tier caller may raise them, others are capped at the
/// conservative defaults.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RlmLimitsOverride {
pub struct RhaiLimitsOverride {
/// Requested `max_tool_calls` for the session.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tool_calls: Option<usize>,
@@ -48,9 +48,9 @@ pub struct RlmLimitsOverride {
pub max_concurrency: Option<usize>,
}
/// A parsed `rlm` tool call — one cell to evaluate against a session.
/// A parsed `rhai_workflows` tool call — one cell to evaluate against a session.
#[derive(Debug, Clone, Default, Deserialize)]
pub struct RlmEvalRequest {
pub struct RhaiEvalRequest {
/// The Rhai workflow cell to evaluate.
pub script: String,
/// Continue a prior session's namespace; `None` starts a fresh session.
@@ -61,7 +61,7 @@ pub struct RlmEvalRequest {
pub timeout_secs: Option<u64>,
/// Optional per-session limit overrides.
#[serde(default)]
pub limits: Option<RlmLimitsOverride>,
pub limits: Option<RhaiLimitsOverride>,
/// Close (drop) the session after this cell.
#[serde(default)]
pub close_session: bool,
@@ -72,7 +72,7 @@ pub struct RlmEvalRequest {
/// level and on the live event stream), so a model-visible result cannot leak a
/// large or sensitive payload back into the context window.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RlmCallSummary {
pub struct RhaiCallSummary {
/// `model` | `tool` | `agent` | `graph` | `emit`.
pub kind: String,
/// The capability or event name.
@@ -87,7 +87,7 @@ pub struct RlmCallSummary {
/// The remaining per-session budget after a cell, surfaced so the model can plan
/// how much more fan-out it can do before splitting work across sessions.
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct RlmLimitsRemaining {
pub struct RhaiLimitsRemaining {
/// Cells left before `max_iterations`.
pub cells: usize,
/// `model_query` calls left.
@@ -98,10 +98,10 @@ pub struct RlmLimitsRemaining {
pub agent_calls: usize,
}
/// The structured result of evaluating one RLM cell, rendered into the JSON
/// The structured result of evaluating one Rhai cell, rendered into the JSON
/// content of the tool result.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RlmEvalResponse {
pub struct RhaiEvalResponse {
/// The session the cell ran in (echoed so a fresh session's generated id is
/// discoverable, and a later cell can continue it).
pub session_id: String,
@@ -113,7 +113,7 @@ pub struct RlmEvalResponse {
/// Names of persistent variables the cell created or changed.
pub variables_changed: Vec<String>,
/// Summarized capability calls the cell performed.
pub calls: Vec<RlmCallSummary>,
pub calls: Vec<RhaiCallSummary>,
/// The final answer, if the cell called `answer(...)`.
#[serde(skip_serializing_if = "Option::is_none")]
pub final_answer: Option<String>,
@@ -122,7 +122,7 @@ pub struct RlmEvalResponse {
/// Number of cells this session has evaluated so far.
pub cells_used: usize,
/// Remaining per-session budget.
pub limits_remaining: RlmLimitsRemaining,
pub limits_remaining: RhaiLimitsRemaining,
/// Whether the session was closed after this cell.
#[serde(default)]
pub closed: bool,
+1 -1
View File
@@ -366,7 +366,7 @@ pub(super) struct ProviderModel {
/// Builds a `ChatModel<()>` capability over an openhuman [`Provider`], pinned to
/// `model`/`temperature`, ready to register into a `tinyagents`
/// [`CapabilityRegistry`](tinyagents::registry::CapabilityRegistry) — e.g. the
/// `.ragsh` REPL bridge (`crate::openhuman::rlm`). [`ProviderModel::new`] is
/// `.ragsh` REPL bridge (`crate::openhuman::rhai_workflows`). [`ProviderModel::new`] is
/// `pub(super)`; this is the crate-visible factory for that one bridge use.
pub(crate) fn provider_chat_model(
provider: Arc<dyn Provider>,
+1 -1
View File
@@ -38,7 +38,7 @@ pub use crate::openhuman::monitor::tools::*;
pub use crate::openhuman::orchestration::tools::*;
pub use crate::openhuman::people::tools::*;
pub use crate::openhuman::referral::tools::*;
pub use crate::openhuman::rlm::tools::*;
pub use crate::openhuman::rhai_workflows::tools::*;
pub use crate::openhuman::screen_intelligence::tools::*;
pub use crate::openhuman::search::tools::*;
pub use crate::openhuman::security::tools::*;
+11 -9
View File
@@ -966,26 +966,28 @@ pub fn all_tools_with_runtime(
tracing::debug!("[lsp] capability gate off (set OPENHUMAN_LSP_ENABLED=1 to register)");
}
// Language-workflow `rlm` tool (RLM / `.ragsh` REPL, `openhuman::rlm`): lets
// Language-workflow `rhai_workflows` tool (`.ragsh` REPL, `openhuman::rhai_workflows`): lets
// the orchestrator author and run its own Rhai workflow cells (fan-out,
// loops, dedup/verify pipelines). Registered on the `supervised`/`full`
// tiers only — dark on `readonly` (it can drive effectful tools/sub-agents)
// and behind the `OPENHUMAN_RLM=0` kill switch. Every effectful inner call
// still re-gates itself in the RLM bridge, so this surface adds no new
// and behind the `OPENHUMAN_RHAI_WORKFLOWS=0` kill switch. Every effectful inner call
// still re-gates itself in the Rhai bridge, so this surface adds no new
// ungated capability.
let rlm_enabled = std::env::var("OPENHUMAN_RLM")
let rhai_workflows_enabled = std::env::var("OPENHUMAN_RHAI_WORKFLOWS")
.or_else(|_| std::env::var("OPENHUMAN_RHAI"))
.or_else(|_| std::env::var("OPENHUMAN_RLM"))
.map(|v| v != "0")
.unwrap_or(true);
if rlm_enabled
if rhai_workflows_enabled
&& security.autonomy != crate::openhuman::security::policy::AutonomyLevel::ReadOnly
{
tools.push(Box::new(crate::openhuman::rlm::RlmTool::new()));
tracing::debug!("[rlm] registered rlm language-workflow tool");
tools.push(Box::new(crate::openhuman::rhai_workflows::RhaiTool::new()));
tracing::debug!("[rhai_workflows] registered rhai_workflows language-workflow tool");
} else {
tracing::debug!(
rlm_enabled,
rhai_workflows_enabled,
tier = ?security.autonomy,
"[rlm] rlm tool not registered (readonly tier or OPENHUMAN_RLM=0)"
"[rhai_workflows] rhai_workflows tool not registered (readonly tier or OPENHUMAN_RHAI_WORKFLOWS=0)"
);
}