Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/readme-story-permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@smooai/smooth-operator-core": patch
---

docs: rewrite the root + per-language package READMEs as registry landing pages that tell a story

Every README (root and the Rust / TypeScript / Python / Go / .NET package pages)
now opens with a hook and a narrative arc — problem → one engine in five
languages → observe→think→act → the permission gate + deny-policy that makes an
agent safe to point at production → build → get started. Each package page leads
with a tight agent-plus-tool quickstart in its own idiom (the mock scripted to
call the tool, then answer) and a permissions/deny-policy example using that
language's real API (`with_deny_policy` in Rust, `denyPolicy`/`permissionMode`
options in TS/Py, `WithDenyPolicy` in Go/C#).

Adds the headline permission system + deny-policy (AutoMode ask / accept-edits /
deny-unmatched / bypass, circuit-breakers, declarative TOML rules + semantic
predicates) to the feature surface, refreshes the polyglot table
(language → package → registry), and fixes stale test-count claims. Docs only —
no code changes.
75 changes: 58 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<p align="center">
<img src="https://img.shields.io/badge/Rust-reference%20impl-FF6B6C?style=flat-square" alt="Rust reference implementation">
<img src="https://img.shields.io/badge/tests-337%20passing-00A6A6?style=flat-square" alt="337 tests passing">
<img src="https://img.shields.io/badge/tests-passing-00A6A6?style=flat-square" alt="tests passing">
</p>

<p align="center">
Expand All @@ -19,13 +19,15 @@

---

> The agent runtime behind the [smooth-operator](https://github.com/SmooAI/smooth-operator) service and [lom.smoo.ai](https://lom.smoo.ai). Agents, workflows, tools, checkpointing, memory, human-in-the-loop, and per-model cost budgets — as a single embeddable Rust crate. It's the engine, not a notebook demo.
> ### The agent brain you can point at production — because you decide what it must never do.
>
> One observe→think→act engine — typed tools, streaming, checkpointing, memory, cost budgets, and a permission gate with hard lines the model can't cross — native in **Rust, TypeScript, Python, Go, and C#**.

`smooai-smooth-operator-core` is the agent runtime that powers the [**smooth-operator**](https://github.com/SmooAI/smooth-operator) service and [**lom.smoo.ai**](https://lom.smoo.ai). It gives you the moving parts of a serious agent framework — an observe→think→act loop, a typed tool system, a graph workflow engine, pluggable checkpoint stores, memory, RAG, human-in-the-loop gates, and per-model cost budgets — as a single, embeddable Rust crate.
Most agent frameworks hand the model a pile of tools and hope for the best. `smooth-operator-core` gives you the whole loop **and the brakes**: a typed tool system with pre/post hooks, human-in-the-loop gates, per-model cost budgets — and a **deny-policy** that lets you draw lines the model can never cross, not even in bypass mode. *No prod AWS profile. No writes to the DB writer. No `rm -rf /`.* Declared once, enforced on every tool call.

Inspired by LangGraph, CrewAI, and Agno, with one hard difference: **it's the engine, not a notebook demo.** Every surface is covered by **337 fast, offline unit tests** built on a deterministic `MockLlmClient`, so the loop is verified — not vibe-coded.
It's the runtime that powers the [**smooth-operator**](https://github.com/SmooAI/smooth-operator) service and [**lom.smoo.ai**](https://lom.smoo.ai) — not a notebook demo. Inspired by LangGraph, CrewAI, and Agno, with one hard difference: every surface is covered by **hundreds of fast, offline unit tests** built on a deterministic `MockLlmClient`, so the loop is verified — not vibe-coded. And it's the **same engine in five languages** — write your agent where your stack already lives.

> The Rust implementation is the source of truth. TypeScript, Go, C#/.NET, and Python bindings mirror its surface (protocol-first; see [Repository layout](#repository-layout)).
> The Rust implementation is the source of truth. The TypeScript, Python, Go, and C#/.NET ports mirror its surface at parity (protocol-first; see [Repository layout](#repository-layout)).

---

Expand Down Expand Up @@ -166,6 +168,7 @@ let agent = Agent::new(config, registry).with_checkpoint_store(checkpoints);
| --- | --- |
| An agent loop you can **trust** | observe→think→act with iteration caps, parallel tool calls, and a typed `AgentEvent` stream |
| **Typed tools** with guardrails | `Tool` trait + `ToolRegistry`, with pre/post hooks for surveillance, secret detection, prompt-injection guards |
| **Deny what must never run** | `PermissionHook` gate (`AutoMode`: ask / accept-edits / deny-unmatched / bypass) + hard circuit-breakers + a consumer `DenyPolicy` (declarative TOML rules + semantic predicates) |
| **Stateful graphs** (a LangGraph analog) | `Workflow<S>` / `WorkflowBuilder<S>` with conditional edges and typed state |
| **Resume after a crash** | `CheckpointStore`: in-memory, file, SQLite, or Postgres |
| **RAG + memory** | `KnowledgeBase` / `Memory` traits (with in-memory impls) as clean seams |
Expand All @@ -178,6 +181,44 @@ It's the runtime the smooth-operator service actually ships on — not a referen

---

## Permissions & deny-policy — draw lines the agent can't cross

Here's the thing that makes an agent safe to point at real infrastructure: **you** decide what it can never do, and no prompt, jailbreak, or model mistake can talk it out of that.

Every tool call passes through a gate before it runs. `AutoMode` sets the baseline posture — read-only calls **allow**, mutating calls **ask**, dangerous calls **deny** — and hard circuit-breakers (`rm -rf /`, credential paths, pipe-to-shell, dangerous domains) fire in *every* mode, `Bypass` included. On top of that you attach a **`DenyPolicy`**: declarative TOML rules for the lines you can name, plus semantic predicates for the ones you can't.

```rust
use std::sync::Arc;
use smooth_operator_core::{Agent, AutoMode, DenyPolicy, DenyPredicate, DenyReason, ToolCall};

// Predicate: the checks strings can't express — is this AWS call the *prod account*?
// Is this DB connection the *writer* endpoint? Return Some(reason) to deny.
struct DenyDbWriter;
impl DenyPredicate for DenyDbWriter {
fn evaluate(&self, call: &ToolCall) -> Option<DenyReason> {
(call.name == "db_query" && call.arguments.to_string().contains("writer"))
.then(|| DenyReason::new("DB writer is off-limits — reads go to the replica"))
}
}

// Declarative rules: never the prod AWS profile, never a prod host.
let policy = DenyPolicy::from_toml(r#"
schema_version = 1
[bash]
deny_patterns = ["aws * --profile prod"]
[network]
deny_hosts = ["*.prod.internal"]
"#)?.with_predicate(Arc::new(DenyDbWriter));

let agent = Agent::new(config, registry)
.with_permission_mode(AutoMode::Ask)
.with_deny_policy(Arc::new(policy));
```

A deny-policy match is a **hard deny of circuit-breaker tier** — no stored grant waives it, no mode downgrades it. That's the difference between "we asked the model nicely" and "it structurally cannot." And it's identical across all five languages.

---

## Architecture

### The agent loop
Expand Down Expand Up @@ -237,7 +278,7 @@ The service is thin: it terminates the WebSocket protocol and hands turns to the

## Test-driven by default — verified, not vibe-coded

This is the part we care about most. The engine ships **408 unit tests** that run in **seconds, fully offline**, because every LLM call goes through an `LlmProvider` seam that tests satisfy with `MockLlmClient`:
This is the part we care about most. The engine ships **hundreds of unit tests** that run in **seconds, fully offline**, because every LLM call goes through an `LlmProvider` seam that tests satisfy with `MockLlmClient`:

```rust
use smooth_operator_core::llm_provider::{LlmProvider, MockLlmClient};
Expand Down Expand Up @@ -272,7 +313,7 @@ flowchart TD
J["LLM-as-judge evals — multi-turn quality, 0–5"]
E["Live E2E — real gateway + WS, streamed answer"]
C["Conformance — SQLite + Postgres stores, testcontainers"]
U["337 unit tests — MockLlmClient, offline, fast"]
U["hundreds of unit tests — MockLlmClient, offline, fast"]

J --> E --> C --> U

Expand All @@ -282,7 +323,7 @@ flowchart TD
class U teal
```

- **Unit (408):** the bulk. Loop control, tool dispatch, workflow edges, compaction, cost enforcement, HITL gating, checkpoint round-trips — all against `MockLlmClient`.
- **Unit (the bulk):** loop control, tool dispatch, workflow edges, compaction, cost enforcement, permission-gate + deny-policy verdicts, HITL gating, checkpoint round-trips — all against `MockLlmClient`.
- **Conformance:** the `sqlite` and `postgres` checkpoint stores run the same suite against real engines (testcontainers), so "resume" means the same thing everywhere.
- **Live E2E:** the smooth-operator service + [chat-widget](https://github.com/SmooAI/chat-widget) drive a real streamed, knowledge-grounded answer through a live gateway.
- **LLM-as-judge:** multi-turn conversation quality is scored 0–5 by a judge model. This caught a real multi-turn context defect: a regression scored **1/5**, the fix landed, and it went back to **5/5** — a class of bug no assertion-based test would have flagged.
Expand All @@ -309,17 +350,17 @@ cargo clippy --all-targets -- -D warnings

## Repository layout

This is a multi-language SmooAI package. The Rust crate is the reference; other languages mirror its surface. For install commands and a hello-agent example in every language, see [**docs/Polyglot-Engines.md**](./docs/Polyglot-Engines.md).
This is a multi-language SmooAI package. The Rust crate is the reference; the other four are **native ports at parity** — the same engine, idiomatic in each language, held to a shared eval suite. Each ships to its language's registry with its own README landing page. For install commands and a hello-agent example in every language, see [**docs/Polyglot-Engines.md**](./docs/Polyglot-Engines.md).

| Directory | Language | Status |
| --- | --- | --- |
| [`rust/`](./rust) | Rust (reference) | Active — crate `smooai-smooth-operator-core` (lib `smooth_operator_core`) |
| [`typescript/`](./typescript) | TypeScript | Planned |
| [`go/`](./go) | Go | Active — module `github.com/SmooAI/smooth-operator-core/go` |
| [`dotnet/`](./dotnet) | C# / .NET | Planned (first-class target) |
| [`python/`](./python) | Python | Planned |
| Language | Directory | Package | Registry |
| --- | --- | --- | --- |
| Rust (reference) | [`rust/`](./rust/smooth-operator-core) | `smooai-smooth-operator-core` (lib `smooth_operator_core`) | [crates.io](https://crates.io/crates/smooai-smooth-operator-core) |
| TypeScript | [`typescript/`](./typescript/core) | `@smooai/smooth-operator-core` | [npm](https://www.npmjs.com/package/@smooai/smooth-operator-core) |
| Python | [`python/`](./python/core) | `smooai-smooth-operator-core` | [PyPI](https://pypi.org/project/smooai-smooth-operator-core/) |
| Go | [`go/`](./go/core) | `github.com/SmooAI/smooth-operator-core/go/core` | [pkg.go.dev](https://pkg.go.dev/github.com/SmooAI/smooth-operator-core/go/core) |
| C# / .NET | [`dotnet/`](./dotnet/core) | `SmooAI.SmoothOperator.Core` | [nuget.org](https://www.nuget.org/packages/SmooAI.SmoothOperator.Core) |

Bindings follow a **protocol-first** strategy (a stable wire spec each language implements natively), with in-process FFI (napi-rs, PyO3/uniffi) layered on where embedding the engine pays off.
The ports follow a **protocol-first** strategy: a stable wire spec each language implements natively, so the loop, tool system, permission gate, checkpointing, and cost accounting behave the same everywhere.

---

Expand Down
63 changes: 56 additions & 7 deletions dotnet/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@

---

> The C#/.NET sibling of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core). Agents, tools, knowledge/RAG, memory, checkpointing, human-in-the-loop, cost budgets, and workflows — as one embeddable NuGet package. It's the engine, not a notebook demo.
> ### The agent brain you can point at production — right in your .NET process.
>
> Most agent frameworks hand the model a pile of tools and hope. This one gives you the loop **and the brakes**: draw hard lines the model can never cross, then let it run.

`SmooAI.SmoothOperator.Core` is the **native C# implementation** of the Smoo AI agent engine — the in-process observe→think→act loop that powers [**lom.smoo.ai**](https://lom.smoo.ai). It's a sibling of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core) and one of the [polyglot set](https://github.com/SmooAI/smooth-operator-core/blob/main/docs/Polyglot-Engines.md) (Rust, TypeScript, Python, Go, C#/.NET) whose behavior is held at parity by a shared eval suite. Its API follows Microsoft.Extensions.AI naming.
`SmooAI.SmoothOperator.Core` is the agent engine itself, in-process — an observe→think→act loop over any `IChatClient`, with typed tools (authored from ordinary C# methods via `AIFunctionFactory`), streaming, checkpointing, cost budgets, and a permission gate you control. Its API follows Microsoft.Extensions.AI naming, so it drops into an existing .NET AI stack. Not a client to a remote server: the agent *is* your process.

It's a library, not a client to a remote server: it *is* the agent, running in your .NET process. Every surface is covered by **fast, offline tests** built on a deterministic `MockLlmProvider`, so the loop is verified — not vibe-coded.
It's the native C# port of the [Rust reference engine](https://github.com/SmooAI/smooth-operator-core) — one of five siblings (Rust, TypeScript, Python, Go, C#/.NET) that share one wire spec and one eval suite. **The same agent brain, the same guarantees, wherever your stack already lives.** Every surface is covered by fast, offline tests on a deterministic `MockLlmProvider`, so the loop is verified — not vibe-coded.

## Install

Expand All @@ -31,17 +33,30 @@ dotnet add package SmooAI.SmoothOperator.Core

A complete agent — no credentials needed — using the deterministic mock provider the engine's own tests run on:

A complete agent with one tool — the mock is scripted to call the tool, then answer. Author tools from ordinary C# methods with `AIFunctionFactory.Create`:

```csharp
using Microsoft.Extensions.AI;
using SmooAI.SmoothOperator.Core;

var provider = new MockLlmProvider().PushText("the answer is 42");
var agent = new SmoothAgent(provider, new AgentOptions { Instructions = "You are a helpful assistant" });
var getWeather = AIFunctionFactory.Create(
(string city) => $"Weather in {city}: 72F, sunny",
"get_weather",
"Get the current weather for a city");

var provider = new MockLlmProvider()
.PushToolCall("call_1", "get_weather", new Dictionary<string, object?> { ["city"] = "Tokyo" })
.PushText("It's 72F and sunny in Tokyo.");

var options = new AgentOptions { Instructions = "You are a helpful assistant" };
options.Tools.Add(getWeather);
var agent = new SmoothAgent(provider, options);

var response = await agent.RunAsync("what is the answer?");
var response = await agent.RunAsync("what's the weather in Tokyo?");
Console.WriteLine(response.Text);
```

`new SmoothAgent(chatClient, options)` takes an `IChatClient` (the `MockLlmProvider` implements it — swap in any OpenAI-compatible client) and an `AgentOptions`. `await agent.RunAsync(...)` returns an `AgentRunResponse`; `response.Text` is the final assistant message.
`new SmoothAgent(chatClient, options)` takes an `IChatClient` (the `MockLlmProvider` implements it — swap in any OpenAI-compatible client) and an `AgentOptions`; tools are `AITool`s added to `options.Tools`. `await agent.RunAsync(...)` returns an `AgentRunResponse`; `response.Text` is the final assistant message.

## Features

Expand All @@ -57,6 +72,7 @@ The full parity surface — every engine in the [polyglot set](https://github.co
- **Rerank** — rerank retrieved hits before injection (lexical reranker built in).
- **Sub-agents / delegation** — spawn child agents for sub-tasks.
- **Cast + clearance** — roles with per-role tool-access policy.
- **Permissions + deny-policy** — a tool-call gate (`AutoMode`: ask / accept-edits / deny-unmatched / bypass) with hard circuit-breakers (`rm -rf /`, credential paths, pipe-to-shell, dangerous domains), a persisted allow-list, and a consumer `DenyPolicy` — declarative TOML rules plus semantic predicates for what strings can't express.
- **Human-in-the-loop gate** — require approval before designated tool calls run.
- **Conversation thread** — carry a conversation across multiple `RunAsync` calls.
- **`LlmProvider` seam + `MockLlmProvider`** — inject any OpenAI-compatible client; the record/replay mock drives the offline tests.
Expand All @@ -66,6 +82,39 @@ The full parity surface — every engine in the [polyglot set](https://github.co
- **Retry / backoff** — retry transient model-call failures with exponential backoff.
- **Streaming** — stream incremental text, tool calls, and tool results as the turn runs.

## Permissions & deny-policy — lines the agent can't cross

This is what makes an agent safe to point at real infrastructure: **you** decide what it can never do, and no prompt or model mistake talks it out of that. Every tool call passes through a gate. `AutoMode` sets the posture — read-only calls **allow**, mutating calls **ask**, dangerous calls **deny** — and hard circuit-breakers (`rm -rf /`, credential paths, pipe-to-shell, dangerous domains) fire in every mode, `Bypass` included. Attach a `DenyPolicy` on top: declarative TOML rules for the lines you can name, semantic predicates for the ones you can't. A match is a hard deny no stored grant and no mode can waive.

```csharp
using Microsoft.Extensions.AI;
using SmooAI.SmoothOperator.Core;

// Declarative rules (TOML): never the prod AWS profile, never a prod host.
var policy = DenyPolicy.FromToml(@"
schema_version = 1
[bash]
deny_patterns = [""aws * --profile prod""]
[network]
deny_hosts = [""*.prod.internal""]
").WithPredicate(new DenyDbWriter());

var options = new AgentOptions { Instructions = "You are a careful assistant" }
.WithPermissionMode(AutoMode.Ask) // read allow · mutate ask · dangerous deny
.WithDenyPolicy(policy);
options.Tools.Add(getWeather);
var agent = new SmoothAgent(provider, options);

// A predicate for what strings can't express — return a DenyReason to deny, null to allow.
sealed class DenyDbWriter : IDenyPredicate
{
public DenyReason? Evaluate(FunctionCallContent call) =>
call.Name == "db_query" && call.Arguments?.Values.Any(v => $"{v}".Contains("writer")) == true
? new DenyReason("DB writer endpoint is off-limits — reads go to the replica")
: null;
}
```

## Streaming

`RunStreamingAsync` is the streaming variant of `RunAsync`: it yields incremental updates — text deltas as the model produces them, each tool call before dispatch, each tool result after it finishes, and a terminal update carrying the same response `RunAsync` would have returned.
Expand Down
Loading
Loading