{"slug": "show-hn-dagic-a-typed-dag-language-so-llm-agents-can-compose-tool-calls", "title": "Show HN: Dagic – a typed DAG language so LLM agents can compose tool calls", "summary": "Rohit Edathil released Dagic, a typed DAG language and async execution engine that lets LLM agents compose registered tools into a typed DAG instead of calling them one at a time, avoiding the need to sandbox arbitrary generated code. In experiments, a Dagic-based math agent achieved 100% accuracy (5/5) with ~33.8k average tokens, ~25.8s average latency per problem, and ~$0.0043 average estimated cost, versus ~248.1k tokens, ~82.5s, and ~$0.0153 for a per-call LangGraph agent, using ~7x fewer tokens, running ~3x faster, and costing ~3.6x less. A real-world test scraping Avengers: Infinity War cast birth dates succeeded for all 19 top-billed actors in ~54.9k tokens, ~69s, and ~$0.10.", "body_md": "**Typed, concurrent tool composition for LLM agents.**\n\nDagic is a tiny workflow language and an async execution engine that lets an LLM\ncompose registered tools into a **typed DAG**, instead of calling them one at a time.\n\nIt sits between traditional tool calling and full code execution: the model writes a short program that pipes results directly between tools and runs independent branches concurrently — without the host ever having to sandbox and execute arbitrary model-generated code.\n\nLLMs are good at deciding *what* should happen next, but plain tool calling makes them\nresponsible for shuttling every intermediate value back through the model, one call at\na time:\n\n```\nsearch(A) → result\nsearch(B) → result\ncombine(A, B) → result\nsummarize(result)\n```\n\nEach arrow above is a full model round-trip, even though the model already knows the shape of the pipeline.\n\nThe usual fix is code execution — let the model write:\n\n```\na = search(\"A\")\nb = search(\"B\")\nresult = combine(a, b)\nsummarize(result)\n```\n\nThat solves composition, but now the host is running arbitrary generated code, which means sandboxing, a runtime, and a much bigger attack surface.\n\nDagic is the middle ground. The model writes:\n\n```\na = search(\"A\");\nb = search(\"B\");\n\nresult = combine(a, b);\n\nsummarize(result);\n```\n\nDagic parses it, type-checks it, builds the DAG, and executes it — independent branches run concurrently, and only functions the host explicitly registered can ever be called. No arbitrary code, no manual dependency wiring.\n\nLet the model describe the graph. Let the host control what can execute.\n\nTwo experiments, both in [ experiments/](/RohitEdathil/dagic/blob/main/experiments), comparing a Dagic-based agent\nagainst a normal one-tool-per-call LangGraph agent.\n\nA math agent solves 5 JEE-Mains-level problems, run 5 times each, comparing a single\n`run_dagic`\n\ntool against one LangChain tool per math operation (`add`\n\n, `multiply`\n\n,\n`sqrt`\n\n, ...). Model: `deepseek-v4-flash`\n\n.\n\n| Metric | Dagic | Per-call tools |\n|---|---|---|\n| Accuracy (5 runs) | 5/5 (100%) | 5/5 (100%) |\n| Avg total tokens | ~33.8k | ~248.1k |\n| Avg latency / problem | ~25.8s | ~82.5s |\n| Avg est. cost | ~$0.0043 | ~$0.0153 |\n\nSame accuracy, but Dagic used **~7x fewer tokens**, ran **~3x faster**, and cost\n**~3.6x less**, on average. It was also far more *stable* run-to-run — per-call tools\nranged from 31s–159s and 75k–578k tokens across the 5 runs, while Dagic stayed in a\ntight 17s–35s / 28k–40k band. Full per-run numbers are in the\n[experiment README](/RohitEdathil/dagic/blob/main/experiments/efficiency/README.md).\n\n*Caveat: the per-call baseline isn't maximally optimized — you could\nhand-write a small expression parser for this specific problem to close some of the\ngap. That's intentionally not done as the point here is that an agent whose tool calls naturally chain together gets faster\nand cheaper for free when you let it express that chaining, instead of forcing every\nintermediate value through another model turn.*\n\nA harder test: *\"Find the date of birth of all the actors from Avengers: Infinity\nWar\"* — a multi-page scrape, parse, and aggregation job, the kind of thing you'd\nnormally reach for a code execution sandbox to do. One `web_scraper`\n\nagent, model\n`kimi-k3`\n\n.\n\n**Succeeded**: correct DOB table for all 19 top-billed cast members (e.g. Robert Downey Jr. 1965-04-04, Chris Hemsworth 1983-08-11), and the agent explicitly noted it had left out the wider supporting cast instead of fabricating entries for them.- ~54.9k tokens (15 model calls, 14 tool calls), ~69s wall time, ~$0.10 at kimi-k3 rates.\n\nA major observation from that trace is that the agent kept assuming it was writing Python and\ntried invalid syntax more than once, wasting turns; Also a loop construct in Dagic would\nlikely cut that down further; and smaller/cheaper models struggled enough that a\nlarger model was needed to get a clean run. Full trace and notes in\n[ experiments/real_tasks](/RohitEdathil/dagic/blob/main/experiments/real_tasks).\n\n**Composition**— chain multiple tools into a single workflow.** Parallelism**— independent branches execute concurrently, automatically.** Static type checking**— incompatible tool arguments are rejected before execution, not mid-run.** Controlled execution**— only functions the host registers can be called. No arbitrary code.** Tiny language**— deliberately limited to what's needed to express a DAG.** Async execution**— built on Python's`asyncio`\n\n.\n\nRequires Python 3.10+.\n\n```\npip install dagic\n```\n\nA Dagic program is just assignments and function calls:\n\n```\nresult = add(create(\"1\"), create(\"2\"));\n\nstore(result);\n```\n\nSlightly more interesting:\n\n```\na = fetch(\"A\");\nb = fetch(\"B\");\n\ncombined = combine(a, b);\n\nstore(combined);\n```\n\nThe graph is implicit in the data flow:\n\n```\nfetch(\"A\") ──┐\n              ├── combine ── store\nfetch(\"B\") ──┘\n```\n\n- Functions are nodes.\n- Function arguments are edges.\n- Assignments name intermediate values.\n- Calls returning\n`None`\n\nare terminal nodes. - Independent branches execute concurrently — no explicit parallel syntax needed.\n\nDagic type-checks the program at compile time, before anything runs.\n\nTwo built-in types:\n\n**Strings**—`\"Hello, World!\"`\n\n**Arrays**—`[\"Hello\", \"World!\"]`\n\nEvery other type comes from the host. If a function expects a `float`\n\n:\n\n``` php\ndef add(a: float, b: float) -> float:\n    return a + b\n```\n\nthen passing anything else is rejected before execution — including array element\ntypes (`List[float]`\n\nand `List[int]`\n\nare distinct).\n\nThis makes a Dagic program a **verifiable execution plan**, not an unchecked sequence\nof tool calls.\n\nTools are plain Python functions, registered with a `Module`\n\n:\n\n``` python\nfrom dagic import Module\n\nmath = Module(name=\"math\", desc=\"Basic arithmetic.\")\n\n@math.register\ndef create(value: str) -> float:\n    \"\"\"Create a float from a string.\"\"\"\n    return float(value)\n\n@math.register\ndef add(a: float, b: float) -> float:\n    \"\"\"Add two numbers.\"\"\"\n    return a + b\n```\n\nRegistered functions must:\n\n- annotate every parameter and the return value\n- have no\n`*args`\n\n/`**kwargs`\n\n- have no default or keyword-only parameters\n\nFunctions returning `None`\n\nare terminals; everything else produces a value that\ndownstream calls can consume.\n\n```\nSource → Parse → Type-check → Build DAG → Execute concurrently\n```\n\nExecution starts at terminal nodes and resolves dependencies backwards. In the\n`fetch`\n\n/`combine`\n\n/`store`\n\nexample above, the two `fetch`\n\ncalls have no dependency on\neach other, so they run concurrently while `combine`\n\nwaits on both.\n\nA program needs at least one terminal node; unused named subgraphs are rejected at compile time.\n\nA small `float_math`\n\nmodule ships with the package: `create`\n\n, `add`\n\n, `subtract`\n\n,\n`multiply`\n\n, `divide`\n\n, `power`\n\n, `modulus`\n\n, `floor_divide`\n\n, `absolute`\n\n, `negate`\n\n. It\ncomposes with your own modules directly:\n\n``` python\nimport asyncio\nfrom dagic import Dagic, Module\nfrom dagic.builtins import float_math\n\nsink = []\nio = Module(name=\"io\", desc=\"I/O helpers.\")\n\n@io.register\ndef store(value: float) -> None:\n    sink.append(value)\n\nasync def main():\n    dagic = Dagic([float_math.float_math, io])\n    await dagic.run('result = add(create(\"1\"), create(\"2\")); store(result);')\n    print(sink)  # [3.0]\n\nasyncio.run(main())\n```\n\n`Dagic.run()`\n\ncompiles the source against the registered modules, builds the DAG, and\nexecutes it — all async.\n\n| Tool calling | Dagic | Code execution | |\n|---|---|---|---|\n| Multi-step composition | Limited | ✓ | ✓ |\n| Parallel execution | Agent-managed | ✓ | ✓ |\n| Static type checking | Usually limited | ✓ | Depends |\n| Arbitrary code execution | ✗ | ✗ | ✓ |\n| Host-controlled operations | ✓ | ✓ | Harder |\n\nDagic isn't trying to replace general-purpose workflow engines or full code execution. It's aimed at a narrower question:\n\nHow can an LLM compose multiple\n\ntrustedoperations into a single, verifiable, concurrent workflow — without the host needing to run arbitrary code?\n\nSee [ examples/](/RohitEdathil/dagic/blob/main/examples) for the math agent and web-scraper agent used in the\nexperiments above.\n\n```\nmake test    # run the test suite\nmake format  # format with ruff\n```\n\nMIT.", "url": "https://wpnews.pro/news/show-hn-dagic-a-typed-dag-language-so-llm-agents-can-compose-tool-calls", "canonical_source": "https://github.com/RohitEdathil/dagic", "published_at": "2026-09-03 16:08:42+00:00", "updated_at": "2026-09-03 16:23:15.148232+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "developer-tools", "ai-tools"], "entities": ["Rohit Edathil", "Dagic", "LangGraph", "deepseek-v4-flash", "kimi-k3", "Avengers: Infinity War"], "alternates": {"html": "https://wpnews.pro/news/show-hn-dagic-a-typed-dag-language-so-llm-agents-can-compose-tool-calls", "markdown": "https://wpnews.pro/news/show-hn-dagic-a-typed-dag-language-so-llm-agents-can-compose-tool-calls.md", "text": "https://wpnews.pro/news/show-hn-dagic-a-typed-dag-language-so-llm-agents-can-compose-tool-calls.txt", "jsonld": "https://wpnews.pro/news/show-hn-dagic-a-typed-dag-language-so-llm-agents-can-compose-tool-calls.jsonld"}}