{"id":47714330,"url":"https://github.com/inngest/utah","last_synced_at":"2026-04-02T18:48:27.285Z","repository":{"id":340660059,"uuid":"1160923871","full_name":"inngest/utah","owner":"inngest","description":"Universally Triggered Agent Harness - An OpenClaw-like Inngest-powered personal agent","archived":false,"fork":false,"pushed_at":"2026-03-19T00:39:18.000Z","size":592,"stargazers_count":104,"open_issues_count":2,"forks_count":3,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-19T14:12:13.950Z","etag":null,"topics":["agent-harness","ai-agent","durable-execution","event-driven","inngest"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/inngest.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":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-02-18T14:31:43.000Z","updated_at":"2026-03-19T00:39:21.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/inngest/utah","commit_stats":null,"previous_names":["inngest/utah"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/inngest/utah","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inngest%2Futah","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inngest%2Futah/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inngest%2Futah/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inngest%2Futah/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/inngest","download_url":"https://codeload.github.com/inngest/utah/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inngest%2Futah/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31313394,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"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-harness","ai-agent","durable-execution","event-driven","inngest"],"created_at":"2026-04-02T18:48:25.784Z","updated_at":"2026-04-02T18:48:27.266Z","avatar_url":"https://github.com/inngest.png","language":"TypeScript","funding_links":[],"categories":["Lightweight Alternatives \u0026 Forks"],"sub_categories":[],"readme":"# Inngest Agent Example — Utah\n\n_**U**niversally **T**riggered **A**gent **H**arness_\n\nA durable AI agent built with [Inngest](https://inngest.com) and [pi-ai](https://github.com/badlogic/pi-mono). No framework. Just a think/act/observe loop — Inngest provides durability, retries, and observability, while pi-ai provides a unified LLM interface across providers.\n\nSimple TypeScript that gives you:\n\n- 🔄 **Durable agent loop** — every LLM call and tool execution is an Inngest step\n- 🔁 **Automatic retries** — LLM API timeouts are handled by Inngest, not your code\n- 🔒 **Singleton concurrency** — one conversation at a time per chat, no race conditions\n- ⚡ **Cancel on new message** — user sends again? Current run cancels, new one starts\n- 📡 **Multi-channel** — Slack, Telegram, and more via a simple channel interface\n- 🏠 **Local development** — runs on your machine via `connect()`, no server needed\n\n## Architecture\n\n```\nChannel (e.g. Telegram) → Inngest Cloud (webhook + transform) → WebSocket → Local Worker → LLM (Anthropic/OpenAI/Google) → Reply Event → Channel API\n```\n\nThe worker connects to Inngest Cloud via WebSocket. No public endpoint. No ngrok. No VPS. Messages flow through Inngest as events, and the agent processes them locally with full filesystem access.\n\n## Prerequisites\n\n- **Node.js 23+** (uses native TypeScript strip-types)\n- LLM API key (e.g. **Anthropic API key** ([console.anthropic.com](https://console.anthropic.com)))\n- **Inngest account** ([app.inngest.com](https://app.inngest.com))\n- **At least one channel** configured (see [Channels](#channels) below)\n\n## Setup\n\n### 1. Create an Inngest Account\n\n1. Sign up at [app.inngest.com](https://app.inngest.com/sign-up)\n2. Go to **Settings → Keys** and copy your:\n   - **Event Key** (for sending events)\n   - **Signing Key** (for authenticating your worker)\n\n### 2. Configure and Run\n\n```bash\ngit clone https://github.com/inngest/utah\ncd utah\nnpm install # or pnpm\ncp .env.example .env\n```\n\nEdit `.env` with your keys:\n\n```env\nANTHROPIC_API_KEY=sk-ant-...\nINNGEST_EVENT_KEY=...\nINNGEST_SIGNING_KEY=signkey-prod-...\n```\n\nThen add the environment variables for your channel(s) — see setup guides below.\n\nStart the worker:\n\n```bash\n# Production mode (connects to Inngest Cloud via WebSocket)\nnpm start\n\n# Development mode (uses local Inngest dev server)\nnpx inngest-cli@latest dev \u0026\nnpm run dev\n```\n\nOn startup, the worker automatically sets up webhooks and transforms for each configured channel.\n\n## Channels\n\nThe agent supports multiple messaging channels. Each channel has its own setup guide:\n\n- **[Telegram](src/channels/telegram/README.md)** — Fully automated setup. Just add your bot token and run.\n- **[Slack](src/channels/slack/README.md)** — Requires creating a Slack app and configuring Event Subscriptions.\n\n## Project Structure\n\n```\nsrc/\n├── worker.ts                  # Entry point — connect() or serve()\n├── client.ts                  # Inngest client\n├── config.ts                  # Configuration from env vars\n├── agent-loop.ts              # Core think → act → observe cycle\n├── setup.ts                   # Channel setup orchestration\n├── lib/\n│   ├── llm.ts                 # pi-ai wrapper (multi-provider: Anthropic, OpenAI, Google)\n│   ├── tools.ts               # Tool definitions (TypeBox schemas) + execution\n│   ├── context.ts             # System prompt builder with workspace file injection\n│   ├── session.ts             # JSONL session persistence\n│   ├── memory.ts              # File-based memory system (daily logs + distillation)\n│   └── compaction.ts          # LLM-powered conversation summarization\n├── functions/\n│   ├── message.ts             # Main agent function (singleton + cancelOn)\n│   ├── send-reply.ts          # Channel-agnostic reply dispatch\n│   ├── acknowledge-message.ts # Message acknowledgment (typing indicator, etc.)\n│   ├── heartbeat.ts           # Cron-based memory maintenance\n│   └── failure-handler.ts     # Global error handler with notifications\n└── channels/\n    ├── types.ts               # ChannelHandler interface\n    ├── index.ts               # Channel registry\n    ├── setup-helpers.ts       # Inngest REST API helpers for webhook setup\n    └── \u003cchannel-name\u003e/        # A channel implementation (see README for setup)\n        ├── handler.ts         # ChannelHandler implementation\n        ├── api.ts             # API client\n        ├── setup.ts           # Webhook setup automation\n        ├── transform.ts       # Webhook transform\n        └── format.ts          # Formatting for channel messages\nworkspace/                       # Agent workspace (persisted across runs)\n├── SOUL.md                    # Agent personality and behavioral guidelines\n├── USER.md                    # User information\n├── MEMORY.md                  # Long-term memory (agent-writable)\n├── memory/                    # Daily logs (YYYY-MM-DD.md, auto-managed)\n└── sessions/                  # JSONL conversation files (gitignored)\n```\n\n## How It Works\n\n### The Agent Loop\n\nThe core is a while loop where each iteration is an Inngest step:\n\n1. **Think** — `step.run(\"think\")` calls the LLM via [pi-ai](https://github.com/badlogic/pi-mono)'s `complete()`\n2. **Act** — if the LLM wants tools, each tool runs as `step.run(\"tool-read\")`\n3. **Observe** — tool results are fed back into the conversation\n4. **Repeat** — until the LLM responds with text (no tools) or max iterations\n\nInngest auto-indexes duplicate step IDs in loops (`think:0`, `think:1`, etc.), so you don't need to track iteration numbers in step names.\n\n### Event-Driven Composition\n\nOne incoming message triggers multiple independent functions:\n\n| Function                 | Purpose                                  | Config                                    |\n| ------------------------ | ---------------------------------------- | ----------------------------------------- |\n| `agent-handle-message`   | Run the agent loop                       | Singleton per chat, cancel on new message |\n| `acknowledge-message`    | Show \"typing...\" immediately             | No retries (best effort)                  |\n| `send-reply`             | Format and send the response             | 3 retries, channel dispatch               |\n| `agent-heartbeat`        | Distill daily logs into long-term memory | Cron (every 30 min)                       |\n| `global-failure-handler` | Catch errors, notify user                | Triggered by `inngest/function.failed`    |\n\n### Workspace Context Injection\n\nThe agent reads markdown files from the workspace directory and injects them into the system prompt:\n\n| File        | Purpose                                                    |\n| ----------- | ---------------------------------------------------------- |\n| `SOUL.md`   | Agent personality, behavioral guidelines, tone, boundaries |\n| `USER.md`   | Info about the user (name, timezone, preferences)          |\n| `MEMORY.md` | Curated long-term memory (agent-writable)                  |\n\nEdit these files to customize your agent's personality and knowledge. The agent can also update `MEMORY.md` using the `write` tool to remember things across conversations.\n\n### Memory System\n\nThe agent has a two-tier memory system:\n\n- **Daily logs** (`workspace/memory/YYYY-MM-DD.md`) — append-only notes written via the `remember` tool during conversations\n- **Long-term memory** (`workspace/MEMORY.md`) — curated summary distilled from daily logs by the heartbeat function\n\nThe `agent-heartbeat` function runs on a cron schedule (default: every 30 minutes). It checks if daily logs have accumulated enough content, then uses the LLM to distill them into `MEMORY.md`. Old daily logs are pruned after a configurable retention period (default: 30 days).\n\n### Conversation Compaction\n\nLong conversations get summarized automatically so the agent doesn't lose context or hit token limits:\n\n- **Token estimation**: Uses a chars/4 heuristic to estimate conversation size\n- **Threshold**: Compaction triggers when estimated tokens exceed 80% of the configured max (150K)\n- **LLM summarization**: Old messages are summarized into a structured checkpoint (goals, progress, decisions, next steps)\n- **Recent messages preserved**: The most recent ~20K tokens of conversation are kept verbatim\n- **Persisted**: The compacted session replaces the JSONL file, so it survives restarts\n\nCompaction runs as an Inngest step (`step.run(\"compact\")`), so it's durable and retryable.\n\n### Context Pruning\n\nLong tool results bloat the conversation context and cause the LLM to lose focus. The agent uses two-tier pruning:\n\n- **Soft trim**: Tool results over 4K chars get head+tail trimmed (first 1,500 + last 1,500 chars)\n- **Hard clear**: When total old tool content exceeds 50K chars, old results are replaced entirely\n- **Budget warnings**: System messages are injected when iterations are running low\n\n### Adding New Channels\n\nThe agent is channel-agnostic. Each channel implements a `ChannelHandler` interface (`src/channels/types.ts`) with methods for sending replies, acknowledging messages, and setup. Each channel directory follows the same structure:\n\n```\nsrc/channels/\u003cname\u003e/\n├── handler.ts      # ChannelHandler implementation (sendReply, acknowledge)\n├── api.ts          # API client for the channel's platform\n├── setup.ts        # Webhook setup automation\n├── transform.ts    # Plain JS transform for Inngest webhook\n└── format.ts       # Markdown → channel-specific format conversion\n```\n\nTo add Discord, WhatsApp, or any other channel:\n\n1. Create a new directory under `src/channels/` following the structure above\n2. Implement the `ChannelHandler` interface in `handler.ts`\n3. Write a webhook transform that converts the channel's payload to `agent.message.received`\n4. Register the channel in `src/channels/index.ts`\n\nThe agent loop, reply dispatch, and acknowledgment functions are all channel-agnostic — no changes needed outside `src/channels/`.\n\n## Key Inngest Features Used\n\n- **[`connect()`](https://www.inngest.com/docs/setup/connect)** — WebSocket-based worker\n- **[Singleton execution](https://www.inngest.com/docs/guides/singleton)** — one run per chat at a time\n- **[Step retries](https://www.inngest.com/docs/guides/error-handling)** — automatic retry on LLM API failures\n- **[Event-driven functions](https://www.inngest.com/docs/features/inngest-functions)** — compose behavior from small focused functions\n- **[Webhook transforms](https://www.inngest.com/docs/platform/webhooks)** — convert external payloads to typed events\n- **[Checkpointing](https://www.inngest.com/docs/setup/checkpointing)** — near-zero inter-step latency\n\n## Acknowledgments\n\nThis project uses [pi-ai](https://github.com/badlogic/pi-mono) (`@mariozechner/pi-ai`) by [Mario Zechner](https://github.com/badlogic) for its unified LLM interface and `@mariozechner/pi-coding-agent` for it's. standard tools. pi-ai provides a single `complete()` function that works across Anthropic, OpenAI, Google, and other providers — making it easy to swap models without changing any agent code. It's a great library.\n\n## License\n\nApache-2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finngest%2Futah","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Finngest%2Futah","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finngest%2Futah/lists"}