{"id":50485137,"url":"https://github.com/vinodhalaharvi/sibyl","last_synced_at":"2026-06-01T21:30:27.863Z","repository":{"id":357835420,"uuid":"1237755831","full_name":"vinodhalaharvi/sibyl","owner":"vinodhalaharvi","description":"A multi-agent convergence framework built on Temporal and Go","archived":false,"fork":false,"pushed_at":"2026-05-14T13:56:36.000Z","size":172,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-14T14:32:38.648Z","etag":null,"topics":["ai-agents","anthropic","claude","durable-execution","golang","llm","multi-agent","temporal","workflow"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vinodhalaharvi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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-05-13T13:32:04.000Z","updated_at":"2026-05-14T13:56:40.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/vinodhalaharvi/sibyl","commit_stats":null,"previous_names":["vinodhalaharvi/sibyl"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/vinodhalaharvi/sibyl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vinodhalaharvi%2Fsibyl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vinodhalaharvi%2Fsibyl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vinodhalaharvi%2Fsibyl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vinodhalaharvi%2Fsibyl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vinodhalaharvi","download_url":"https://codeload.github.com/vinodhalaharvi/sibyl/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vinodhalaharvi%2Fsibyl/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33795112,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["ai-agents","anthropic","claude","durable-execution","golang","llm","multi-agent","temporal","workflow"],"created_at":"2026-06-01T21:30:26.649Z","updated_at":"2026-06-01T21:30:27.847Z","avatar_url":"https://github.com/vinodhalaharvi.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sibyl\n\nA small, principled multi-agent convergence framework built on\n[Temporal](https://temporal.io) and Go.\n\nTwo cooperating agents — a **Researcher** and a **Critic** — iterate over a\nquestion until the Critic approves the answer or `MaxRounds` is reached. The\nloop itself is a Temporal Workflow (durable, replay-safe), and every LLM call\nis a Temporal Activity (retried automatically on transient failures).\n\n## Why this exists\n\nThe agent loop — *\"call the model, run a tool, reason about the result, call\nthe model again\"* — has the same shape as a long-running orchestration: many\nsteps, each can fail, each is expensive, and the whole thing must survive\ncrashes, restarts, and timeouts. Temporal is purpose-built for that shape.\nSibyl is a small reference implementation of an agent convergence pattern on\ntop of it.\n\n## Project layout\n\n```\nsibyl/\n├── agent/                    package agent — workflows, activities, types\n│   ├── types.go              Question, Answer, Verdict, Round\n│   ├── llm.go                CompleteFunc seam + Middleware + ScriptedLLM\n│   ├── lift.go               bridge between weft Arrows and Temporal activities\n│   ├── activities.go         Researcher and Critic — composed weft pipelines\n│   ├── workflow.go           ConvergeWorkflow — the single-question convergence loop\n│   ├── decompose.go          deterministic decompose + synthesize pipelines\n│   ├── supervisor.go         SupervisorWorkflow — fan-out coordinator\n│   ├── anthropic.go          Anthropic API client (CompleteFunc)\n│   ├── claudecode.go         Local Claude Code CLI client (CompleteFunc)\n│   └── *_test.go             unit tests (60 tests, in-process Temporal)\n├── worker/\n│   └── worker.go             Register() helper to wire Sibyl onto a Temporal worker\n├── cmd/\n│   ├── worker/main.go        runnable worker (-llm scripted | anthropic | claude-code)\n│   ├── ask/main.go           submit a single ConvergeWorkflow\n│   └── ask-supervisor/main.go  submit a SupervisorWorkflow (multi-agent fan-out)\n├── go.mod / go.sum\n├── Makefile\n└── README.md\n```\n\n## Quick start\n\nYou need Go 1.24+ and the Temporal CLI (for the local dev server).\n\n```bash\n# 1. Resolve deps and run tests\ngo mod tidy\ngo test -race ./...\n\n# 2. In one terminal, start the Temporal dev server\ntemporal server start-dev --db-filename temporal.db --ui-port 8080\n\n# 3. In a second terminal, start the Sibyl worker\ngo run ./cmd/worker\n\n# 4. In a third terminal, ask a question\ngo run ./cmd/ask -q \"What is the capital of France?\" -rounds 3\n\n# Open http://localhost:8080 to watch the workflow execute live.\n```\n\nThe bundled `cmd/worker` uses a **ScriptedLLM** by default — a deterministic,\nin-memory \"model\" that returns canned responses. This lets you run the whole\nstack end-to-end without API keys. To use a real LLM, pass `-llm`:\n\n```bash\n# Use the Anthropic API (requires ANTHROPIC_API_KEY)\ngo run ./cmd/worker -llm anthropic\n\n# Use your local Claude Code CLI (uses your Pro/Max subscription auth)\ngo run ./cmd/worker -llm claude-code\n\n# Default: scripted, no network, no auth\ngo run ./cmd/worker -llm scripted\n```\n\n## The CompleteFunc seam\n\nThe LLM boundary is a **function type**, not an interface:\n\n```go\ntype CompleteFunc func(ctx context.Context, systemPrompt, userMessage string) (string, error)\n```\n\nA function type is the right tool for a single-method seam in Go: any\ncompatible method becomes a `CompleteFunc` via a method value, test doubles\ncan be plain closures, and middleware composes as ordinary function wrapping.\n\nThree backends ship in the box:\n\n| Type | Use it for | How |\n|---|---|---|\n| `ScriptedLLM` | unit tests / offline demos | canned responses, records calls |\n| `AnthropicClient` | production / billed API | direct HTTP to `api.anthropic.com` |\n| `ClaudeCodeClient` | running on your machine | shells out to `claude -p` |\n\nEach exposes a `Complete` method that satisfies `CompleteFunc`:\n\n```go\nc, _ := agent.NewAnthropicClient(agent.AnthropicConfig{})\nsibylworker.Register(w, c.Complete)   // method value -\u003e CompleteFunc\n```\n\n### Middleware\n\nBecause `CompleteFunc` is a function type, wrapping it is trivial:\n\n```go\nfunc WithLogging(log *slog.Logger) agent.Middleware {\n    return func(next agent.CompleteFunc) agent.CompleteFunc {\n        return func(ctx context.Context, sys, user string) (string, error) {\n            start := time.Now()\n            out, err := next(ctx, sys, user)\n            log.Info(\"llm call\", \"took\", time.Since(start), \"err\", err)\n            return out, err\n        }\n    }\n}\n\ncomplete := agent.Chain(rawClient.Complete, WithLogging(logger), WithRateLimit(...))\nsibylworker.Register(w, complete)\n```\n\n## Composing arrows with weft\n\nSibyl uses [weft](https://github.com/vinodhalaharvi/weft) as its compositional\nlayer. Every step inside an activity — prompt building, LLM call, response\nparsing — is a `weft.Arrow[A, B]`. The activity body is just `Pipe3` over\nthree of them:\n\n```go\n// agent/activities.go\nresearcher := weft.Pipe3(\n    buildResearchRequest,        // weft.Arrow[ResearchInput, CompletionRequest]\n    agent.CompleteAsArrow(c),    // weft.Arrow[CompletionRequest, string]\n    weft.Pure(trimResponse),     // weft.Arrow[string, string]\n)\n```\n\nWhy this matters: as we move toward multi-agent systems, the unit of\ncomposition is no longer the activity — it's the Arrow. You can add a\ncaching layer, swap parsers, or fan out to multiple LLMs in parallel\n(`weft.Par`) without rewriting the activity surface. The activity wrapper\njust dispatches to whichever arrow you've composed.\n\nThe `agent` package exposes two adapters (in `lift.go`):\n\n| Adapter              | Direction              | Use it when                                  |\n|----------------------|------------------------|----------------------------------------------|\n| `CompleteAsArrow`    | `CompleteFunc` → Arrow | Lifting an LLM client into a weft pipeline   |\n| `ArrowAsActivity`    | Arrow → activity func  | Registering a composed arrow with Temporal   |\n\nThe convergence loop itself remains a Temporal workflow (must be deterministic\nfor replay), but the work *inside* each round is now expressible in the\nbroader weft algebra. This is the seam you'd build a multi-agent supervisor\non top of.\n\n## Multi-agent supervision\n\n`SupervisorWorkflow` decomposes a question into subquestions, spawns a child\n`ConvergeWorkflow` per subquestion in parallel, waits for all of them, and\nsynthesizes a final answer.\n\n```\n┌──────────────────────────────────────────────────────────────┐\n│ SupervisorWorkflow                                           │\n│                                                              │\n│  1. Decompose activity        question -\u003e []SubQuestion      │\n│  2. for each SubQuestion:                                    │\n│       ExecuteChildWorkflow(ConvergeWorkflow, ...)            │\n│  3. Wait for all children (swallow individual failures)      │\n│  4. Synthesize activity       []SubAnswer -\u003e final string    │\n└──────────────────────────────────────────────────────────────┘\n         │                  │                  │\n         ▼                  ▼                  ▼\n   ConvergeWorkflow   ConvergeWorkflow   ConvergeWorkflow\n   (subquestion 1)    (subquestion 2)    (subquestion N)\n```\n\nRun it:\n\n```bash\nmake ask-supervisor Q=\"What is Go and how does it compare to Rust\"\n```\n\nOr directly:\n\n```bash\ngo run ./cmd/ask-supervisor -q \"Postgres vs SQLite vs MySQL for a side project\" -rounds 3\n```\n\nEach child workflow appears in the Web UI as a separate execution with a\ndeterministic ID (`\u003csupervisor-id\u003e-sub-\u003cindex\u003e`), so you can drill into any\nchild's event history independently.\n\n**Failure handling.** Individual child failures are recorded in the output\n(`SubAnswer.Error`) but don't fail the supervisor. The supervisor only fails\nif every child failed, or if decomposition or synthesis itself failed.\n\n**Decomposer.** Sibyl ships a deterministic heuristic decomposer that splits\non `?`, `and`, `vs`, `versus`, `compared to`, `;`. It's pure code, no LLM call,\nno flakiness. Swap it for an LLM-backed decomposer by replacing\n`decomposeArrow` in `agent/decompose.go` — it's a single `weft.Arrow`.\n\n**Synthesizer.** Same story: the default synthesizer concatenates child\nanswers with markdown headings. Replace `synthesizeArrow` for LLM-backed\nsummarization.\n\n## How the convergence loop works\n\n```\n┌──────────────────────────────────────────────────────────┐\n│ ConvergeWorkflow (deterministic Go code)                 │\n│                                                          │\n│   for round := 1; round \u003c= MaxRounds; round++ {          │\n│       candidate := ExecuteActivity(Research, ...)        │\n│       verdict   := ExecuteActivity(Critique, candidate)  │\n│       if verdict.Approved {                              │\n│           return candidate                               │\n│       }                                                  │\n│       // carry feedback forward to the next round        │\n│   }                                                      │\n└──────────────────────────────────────────────────────────┘\n```\n\nBoth `Research` and `Critique` are activities. Their results are recorded in\nthe workflow's event history, so on a worker crash the workflow resumes\nwithout re-running them.\n\nThe Critic returns structured JSON:\n\n```json\n{\"approved\": true, \"confidence\": 0.92, \"feedback\": \"\"}\n```\n\nIf the model returns malformed JSON, the activity returns a non-retryable\nerror (`InvalidLLMResponse`) — retrying won't help if it's a prompt/model\nproblem, and we want to fail fast.\n\n## Testing strategy\n\nTemporal ships an in-process test environment (`testsuite.WorkflowTestSuite`)\nthat runs workflows and activities without a real server. All Sibyl tests use\nit — no Docker, no network, no API keys:\n\n```bash\ngo test -race -count=1 ./...\n```\n\nThe `ScriptedLLM` test double lets each test specify the exact sequence of\nLLM responses the workflow will see. Tests cover:\n\n- happy path: converges on round 1\n- revision path: converges on round 2 after critic feedback\n- max-rounds path: terminates with `Converged: false`\n- input validation: empty question, zero MaxRounds\n- non-retryable errors: malformed critic JSON fails fast (one call, not five)\n- the LLM-call parsing logic in each activity\n\n## Production notes\n\n- **Real LLM client.** Implement `LLMClient` against your provider. Keep\n  retries inside the provider client minimal; let Temporal's activity retry\n  policy handle it.\n- **Cost control.** Set `MaxRounds` conservatively. Every round is two LLM\n  calls. The Temporal Web UI shows exactly how many calls have happened so far.\n- **Long human-in-the-loop.** Add a signal handler (`workflow.GetSignalChannel`)\n  to inject human guidance mid-loop. The workflow can block on a signal for\n  hours or days without consuming worker resources.\n- **Multi-agent fan-out.** For more than two agents, spawn child workflows\n  with `workflow.ExecuteChildWorkflow` — each child has its own event history\n  and can crash/recover independently.\n\n## Naming\n\nA Sibyl, in Greek myth, was an oracle who deliberated before speaking. That's\nwhat a convergence loop is: a structured deliberation before an answer is\nreturned. The library name is intentionally lowercase: `sibyl`.\n\n## License\n\nMIT (or your choice — edit before publishing).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvinodhalaharvi%2Fsibyl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvinodhalaharvi%2Fsibyl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvinodhalaharvi%2Fsibyl/lists"}