{"id":48817254,"url":"https://github.com/metravod/zymi-core","last_synced_at":"2026-05-02T11:02:01.322Z","repository":{"id":349422540,"uuid":"1199479145","full_name":"metravod/zymi-core","owner":"metravod","description":"Event-sourced agent engine — CLI and Python bindings for auditable AI workflows","archived":false,"fork":false,"pushed_at":"2026-04-14T10:36:40.000Z","size":484,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-14T11:11:04.090Z","etag":null,"topics":["agent-framework","agents","ai","ai-agents","declarative","generative-ai","llm","multi-agent-systems","multiagent","python","rust"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/metravod.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-02T11:46:07.000Z","updated_at":"2026-04-14T10:32:35.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/metravod/zymi-core","commit_stats":null,"previous_names":["metravod/zymi-core"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/metravod/zymi-core","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metravod%2Fzymi-core","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metravod%2Fzymi-core/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metravod%2Fzymi-core/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metravod%2Fzymi-core/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/metravod","download_url":"https://codeload.github.com/metravod/zymi-core/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metravod%2Fzymi-core/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31795334,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-14T11:13:53.975Z","status":"ssl_error","status_checked_at":"2026-04-14T11:13:53.299Z","response_time":153,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-framework","agents","ai","ai-agents","declarative","generative-ai","llm","multi-agent-systems","multiagent","python","rust"],"created_at":"2026-04-14T12:01:46.201Z","updated_at":"2026-05-02T11:02:01.300Z","avatar_url":"https://github.com/metravod.png","language":"Rust","funding_links":[],"categories":["Frameworks"],"sub_categories":["Tools"],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/metravod/zymi-core/main/assets/zymi-badge.png\" alt=\"zymi\" width=\"220\" /\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003ezymi-core\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\u003cem\u003eDeclarative agent engine — dbt for AI workflows, with event sourcing built in.\u003c/em\u003e\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\u003csub\u003ePronounced \u003cem\u003ezoomi\u003c/em\u003e — like dog zoomies.\u003c/sub\u003e\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://pypi.org/project/zymi-core/\"\u003e\u003cimg src=\"https://img.shields.io/pypi/v/zymi-core.svg?logo=pypi\u0026logoColor=white\" alt=\"PyPI\" /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n---\n\n## Why zymi-core?\n\nMost agent frameworks are imperative Python: you write a script that makes LLM calls, maybe persists some messages, and leaves you guessing about the rest. Debugging a bad run means reading logs, hoping you logged enough.\n\n`zymi-core` inverts that:\n\n- **Declarative, like dbt.** Describe agents, tools, pipelines, and integrations in YAML. The engine loads them, validates them, runs them as a DAG. No orchestration code to write.\n- **Event-sourced.** Every state change — inbound message, LLM call, tool result, approval, outbound reply — is an immutable event persisted to SQLite with a per-stream hash chain. Runs are replayable, resumable, and auditable without extra logging.\n- **Boundary-safe.** Agents emit *intentions* (\"I want to write this file\", \"I want to run this shell command\") that pass through contracts and optional human approval before execution. The unsafe thing never happens until someone said yes.\n\nThe ergonomic target: you can bring a useful agent online in minutes without writing code, and a year later still answer *exactly what this agent did* on any past run.\n\n---\n\n## Run a Telegram agent in two minutes\n\nThis is the primary demo — a real chat bot you talk to on your phone, wired declaratively.\n\n```bash\npip install zymi-core\n\nmkdir telegram-agent \u0026\u0026 cd telegram-agent\nzymi init --example telegram\n\n# 1. Create a bot via @BotFather in Telegram; copy the token.\n# 2. Fill .env:\ncp .env.example .env       # edit TELEGRAM_BOT_TOKEN + OPENAI_API_KEY\n# 3. Open project.yml, replace \"your_username_here\" with your actual\n#    Telegram username (no @). This keeps strangers out of the bot.\n\nsource .env\nzymi serve chat\n```\n\nMessage the bot — it replies within a couple of seconds. Every inbound message, LLM call, and outbound reply is persisted to `.zymi/events.db`; watch it live with `zymi observe`.\n\nThe entire wiring lives in `project.yml`:\n\n```yaml\nllm:\n  provider: openai\n  model: gpt-4o-mini\n  api_key: ${env.OPENAI_API_KEY}\n\nconnectors:\n  - type: http_poll                   # long-polls getUpdates (no HTTPS needed)\n    name: telegram\n    url: \"https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/getUpdates\"\n    interval_secs: 2\n    extract:\n      items:     \"$.result[*]\"\n      stream_id: \"$.message.chat.id\"\n      content:   \"$.message.text\"\n    cursor: { param: offset, from_item: \"$.update_id\", plus_one: true, persist: true }\n    filter:\n      \"$.message.from.username\":\n        one_of: [\"your_username_here\"]\n    pipeline: chat                    # every accepted message → `zymi serve chat`\n\noutputs:\n  - type: http_post                   # reply on every pipeline completion\n    name: telegram_reply\n    on: [ResponseReady]\n    url: \"https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage\"\n    headers: { Content-Type: \"application/json\" }\n    body_template: '{\"chat_id\":\"{{ event.stream_id }}\",\"text\":{{ event.content | tojson }}}'\n    retry: { attempts: 3, backoff_secs: [1, 5, 30] }\n```\n\nThat's it. No Rust. No Python. The scaffold ships with a small two-step DAG — `respond` (assistant with `web_search` / `web_scrape` tools) → `polish` (a brutally lazy reviewer that keeps the draft verbatim unless it's actually broken). Both steps are visible live in `zymi observe`.\n\nAsk the bot to \"announce that we're closing at 5pm\" and the agent reaches for `tools/broadcast.yml` (`requires_approval: true`). The `approvals:` section in `project.yml` DMs you with ✅ / ❌ buttons; nothing goes out until you click. See [Approvals on the bus](#approvals-on-the-bus) below.\n\nWant a real search backend? Open `tools/web_search.yml`, uncomment a provider block (Brave, Tavily, SerpAPI, Google), set its key in `.env`. Out of the box the bot still works — the assistant just answers from its own knowledge.\n\n### Same primitives elsewhere\n\n`http_inbound` / `http_poll` / `http_post` cover webhooks and REST for most SaaS. Pick `http_inbound` when the service can POST to you over HTTPS, `http_poll` when you're behind NAT or the service has no webhooks. Filter recipes for the common cases:\n\n```yaml\n# GitHub — only react to PR opens, not pushes or issues\nfilter:\n  \"$.action\":              { equals: \"opened\" }\n  \"$.pull_request.draft\":  { equals: false }\n\n# Slack Events API — one channel, skip bot echoes\nfilter:\n  \"$.event.channel\":  { equals: \"C0123456789\" }\n  \"$.event.subtype\":  { equals: null }\n```\n\nOut of the box `filter:` supports `one_of` / `equals`. 429 rate-limiting is handled automatically (`Retry-After` header + Telegram's body-level hint are both honoured on retries).\n\n---\n\n## Highlights\n\n### Pipelines are DAGs\n\nA pipeline is a list of steps with `depends_on:` edges. Independent steps run in parallel, dependencies stay explicit.\n\n```yaml\n# pipelines/research.yml\nsteps:\n  - id: search_web\n    agent: researcher\n    task: \"Find articles on: ${inputs.topic}\"\n\n  - id: search_deep\n    agent: researcher\n    task: \"Find technical details on: ${inputs.topic}\"\n\n  - id: analyze\n    agent: researcher\n    task: \"Cross-reference findings, store a structured summary in memory.\"\n    depends_on: [search_web, search_deep]    # runs after both finish\n\n  - id: write_report\n    agent: writer\n    task: \"Read memory, write ./output/report.md.\"\n    depends_on: [analyze]\n```\n\nTry it: `zymi init --example research`. The scaffold pre-configures a two-agent pipeline with parallel search.\n\n### Replay, resume, observe\n\nBecause everything is an event, you don't just log runs — you *keep* them.\n\n```bash\nzymi runs                                   # all pipeline runs\nzymi events --stream pipeline-research-abc  # every event in one run\nzymi verify --stream pipeline-research-abc  # hash-chain integrity check\nzymi observe                                # 3-panel TUI: runs / DAG / events live\n\n# Fork-resume an earlier run from a chosen step. Upstream steps are frozen;\n# the fork step + DAG-descendants re-run against current configs on disk.\nzymi resume pipeline-research-abc --from-step write_report\nzymi resume pipeline-research-abc --from-step write_report --dry-run\n```\n\nFork-resume is useful when you're iterating on a prompt or tool config: you don't have to re-burn the expensive early steps every time you tweak the later ones. See [ADR-0018](adr/0018-idempotent-fork-resume.md).\n\n### MCP servers — N tools per YAML entry\n\nDeclarative tools cover HTTP and shell. For anything heavier — filesystem sandbox, git client, search index, proprietary protocol — drop a [Model Context Protocol][mcp] server into `project.yml` and `zymi-core` handles the subprocess, handshake, restarts, and tool-catalog wiring.\n\n```yaml\nmcp_servers:\n  - name: fs\n    command: [npx, -y, \"@modelcontextprotocol/server-filesystem\", ./sandbox]\n    allow: [read_text_file, write_file, list_directory]\n    restart: { max_restarts: 2, backoff_secs: [1, 5] }\n```\n\nThen in the agent:\n\n```yaml\ntools:\n  - mcp__fs__read_text_file\n  - mcp__fs__write_file\n  - mcp__fs__list_directory\n```\n\nNo per-tool schemas to author — they come from the server's `tools/list` at startup. Every call is audited as a normal tool event. Probe new servers before wiring:\n\n```bash\nzymi mcp probe fs -- npx -y @modelcontextprotocol/server-filesystem /tmp\nzymi mcp probe gh --env GITHUB_PERSONAL_ACCESS_TOKEN=ghp_... \\\n                  -- npx -y @modelcontextprotocol/server-github\n```\n\nEnd-to-end demo: `zymi init --example mcp`. Full posture (PATH forwarding, env-isolation, restart policy) in [ADR-0023](adr/0023-mcp-client-integration.md).\n\n### Declarative custom tools\n\nHTTP and shell tools live in `tools/*.yml`. No rebuild, no Rust.\n\n```yaml\n# tools/slack_post.yml\nname: slack_post\ndescription: \"Post a message to a Slack channel\"\nparameters:\n  type: object\n  properties:\n    channel: { type: string }\n    text:    { type: string }\n  required: [channel, text]\nimplementation:\n  kind: http\n  method: POST\n  url: \"https://slack.com/api/chat.postMessage\"\n  headers:\n    Authorization: \"Bearer ${env.SLACK_TOKEN}\"\n    Content-Type:  \"application/json\"\n  body_template: '{\"channel\": \"${args.channel}\", \"text\": \"${args.text}\"}'\n```\n\n`${env.*}` resolves at parse time; `${args.*}` resolves at call time from LLM arguments. Collisions with built-in tools are a hard error.\n\n### Automatic context management\n\nThe agent's working context is reconstructed from the event log each iteration, not accumulated in a growing buffer. Older tool observations are masked in-place (~2× cost reduction, no extra LLM calls). When the token budget still gets tight, hybrid compaction summarises the oldest masked batch with a fast LLM call. Both caps are tunable:\n\n```yaml\nruntime:\n  context:\n    observation_window: 10         # recent turns kept verbatim\n    soft_cap_chars: 400000         # triggers LLM summarisation\n    hard_cap_chars: 600000         # fatal if exceeded after compaction\n    min_tail_turns: 4\n```\n\nDesign and trade-offs in [ADR-0016](adr/0016-context-window-management.md).\n\n### Safer side effects\n\nAgents don't take actions directly — they emit intentions (`ExecuteShellCommand`, `WriteFile`, `ReadFile`, `WebSearch`, `SpawnSubAgent`, `CallCustomTool`, …). Intentions pass through:\n\n1. **Policy engine** — shell command allow/deny patterns.\n2. **Contracts** — file-write boundaries, rate limits, tool-specific rules.\n3. **Approval** — declarative `approvals:` channels (terminal / http / telegram), or fail-closed when no channel is configured.\n\nNothing with side effects runs until the intention is approved.\n\n### Approvals on the bus\n\nApprovals are declarative in `project.yml` ([ADR-0022](adr/0022-event-sourced-approvals.md)). Pick a channel — terminal prompt, HTTP endpoint, or Telegram DM with inline ✅/❌ keyboard — and any tool flagged `requires_approval: true` routes through it.\n\n```yaml\ndefault_approval_channel: ops_tg\n\napprovals:\n  - type: telegram\n    name: ops_tg\n    bot_token:    \"${env.TELEGRAM_BOT_TOKEN}\"\n    chat_id:      \"${env.TELEGRAM_ADMIN_CHAT_ID}\"\n    bind:         \"127.0.0.1:8088\"\n    callback_path: /telegram/approval\n    secret_token: \"${env.TELEGRAM_WEBHOOK_SECRET}\"\n```\n\n```yaml\n# tools/broadcast.yml — gated tool\nname: broadcast\ndescription: \"Send an announcement to the team channel.\"\nparameters: { type: object, properties: { message: { type: string } }, required: [message] }\nrequires_approval: true\nimplementation:\n  kind: shell\n  command_template: \"post-to-slack ${args.message}\"\n```\n\nWhen the agent calls `broadcast`, the bot DMs the admin chat with approve/deny buttons; the click flows back as `ApprovalGranted` / `ApprovalDenied` events. Resolution order is **pipeline override → project default → fail-closed**. Every step is on the event bus, so the audit trail is uniform with the rest of the run, and a hard crash mid-approval is repaired on next start: in-flight requests are redelivered to live channels, expired ones are sealed with `ApprovalDenied{reason: restart_timeout}`. End-to-end demo: `zymi init --example telegram`.\n\n### JSON Schemas for configs\n\nIDE autocomplete and LLM-assisted config generation come free:\n\n```bash\nzymi schema project       # draft-07 JSON Schema for project.yml\nzymi schema --all\n```\n\n---\n\n## Python bindings\n\nThe same `pip install zymi-core` that gives you the CLI also exposes `Runtime`, `Event`, `EventBus`, `EventStore`, `Subscription`, `ToolRegistry`.\n\n```python\nfrom zymi_core import Runtime\n\nrt = Runtime.for_project(\".\", approval=\"terminal\")\nresult = rt.run_pipeline(\"research\", {\"topic\": \"rust event sourcing\"})\nprint(result.success, result.final_output)\n```\n\n`rt.bus()` and `rt.store()` hand out wrappers over the same `Arc`s the Rust runtime uses — a Python subscriber sees exactly what the handler publishes, no second bus over the same SQLite file.\n\n### Cross-process events (Django, Celery, scripts)\n\nThe SQLite store is the source of truth: events written from one process are visible to every other process that opens the same file, and `zymi serve` picks them up via a polling tail watcher ([ADR-0012](adr/0012-cross-process-event-delivery.md)).\n\nCanonical pattern — a web app publishes `PipelineRequested`, the long-running `zymi serve` runs the pipeline, the result comes back as `PipelineCompleted` with the same `correlation_id`.\n\n```python\n# Terminal A: zymi serve research\n# Terminal B: any Python process — Django view, Celery task, script\nimport uuid\nfrom zymi_core import Event, EventBus, EventStore\n\nstore = EventStore(\".zymi/events.db\")\nbus   = EventBus(store)\n\ncorrelation_id = str(uuid.uuid4())\nsub = bus.subscribe_correlation(correlation_id)\n\nevent = Event(\n    stream_id=f\"web-{correlation_id}\",\n    kind={\"type\": \"PipelineRequested\", \"data\": {\n        \"pipeline\": \"research\",\n        \"inputs\":   {\"topic\": \"rust event sourcing\"},\n    }},\n    source=\"django\",\n)\nevent.with_correlation(correlation_id)\nbus.publish(event)\n\nresult = sub.recv(timeout_secs=300)\nprint(result.kind)\n```\n\nInside `zymi serve` the `PipelineRequested → RunPipeline` translation is done by `EventCommandRouter` — re-exported from `zymi_core::runtime`, so your own scheduler or bot adapter can drive a `Runtime` without copying `cli/serve.rs`.\n\n---\n\n## Rust crate\n\n```toml\n[dependencies]\nzymi-core = \"0.4\"\n```\n\n```rust\nuse std::sync::Arc;\nuse zymi_core::{open_store, Event, EventBus, EventKind, Message, StoreBackend};\n\nlet store = open_store(StoreBackend::Sqlite { path: \"events.db\".into() })?;\nlet bus   = EventBus::new(store.clone());\nlet mut rx = bus.subscribe().await;\n\nlet event = Event::new(\n    \"conversation-1\".into(),\n    EventKind::UserMessageReceived {\n        content:   Message::User(\"Hello\".into()),\n        connector: \"cli\".into(),\n    },\n    \"cli\".into(),\n);\nbus.publish(event).await?;\n\nlet received   = rx.recv().await.unwrap();\nlet verified   = store.verify_chain(\"conversation-1\").await?;\n```\n\nFeature flags:\n\n| Feature | Description |\n| --- | --- |\n| `python`     | PyO3 bindings for the `_zymi_core` extension module |\n| `cli`        | The `zymi` CLI binary (implies `connectors`) |\n| `runtime`    | Async runtime and HTTP dependencies |\n| `webhook`    | HTTP approval handler (axum) |\n| `services`   | Event-bus services (LangFuse) |\n| `connectors` | Declarative `connectors:` / `outputs:` (`http_inbound` / `http_poll` / `http_post`, [ADR-0021](adr/0021-generic-http-connectors-and-plugin-protocol.md)) |\n\n---\n\n## CLI reference\n\n```bash\nzymi init                              # minimal scaffold\nzymi init --example telegram           # declarative chat bot\nzymi init --example research           # parallel-search + writer pipeline\nzymi init --example mcp                # agent wired to an MCP server\n\nzymi run \u003cpipeline\u003e -i key=value ...   # one-shot run\nzymi serve \u003cpipeline\u003e                  # long-running: react to PipelineRequested\n\nzymi pipelines                         # list pipelines in the project\nzymi runs [--pipeline NAME] [--limit N]\nzymi events [--stream ID] [--kind TAG] [--raw]\nzymi verify [--stream ID]              # hash-chain integrity check\nzymi observe [--run ID]                # 3-panel TUI\nzymi resume \u003crun-id\u003e --from-step \u003cid\u003e [--dry-run]\n\nzymi mcp probe \u003cname\u003e -- \u003ccmd\u003e [args...]\nzymi schema project|agent|pipeline|tool\nzymi schema --all\n```\n\n---\n\n## How it works\n\n1. **Every state change becomes an event.** The SQLite event store with per-stream hash chain is the source of truth.\n2. **Agents emit intentions, not side effects.** Intentions are evaluated against policy + contracts + approvals before execution.\n3. **Pipelines are DAGs.** Independent steps run in parallel; dependencies stay explicit.\n4. **Context is event-sourced.** The working context is reassembled from the log each iteration — older observations masked, hybrid compaction when the budget gets tight.\n5. **Runs stay replayable.** Inspect with `zymi events`, verify with `zymi verify`, fork-resume from any step.\n\nMore detail lives in [`adr/`](adr/) — each architectural decision is a short markdown file.\n\n---\n\n## Contributing\n\nBug reports, examples, and PRs welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the dev-loop, test matrix, and ADR workflow.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\n[mcp]: https://modelcontextprotocol.io\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmetravod%2Fzymi-core","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmetravod%2Fzymi-core","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmetravod%2Fzymi-core/lists"}