{"id":44291594,"url":"https://github.com/agenticklabs/agentick","last_synced_at":"2026-06-07T16:01:08.642Z","repository":{"id":337163033,"uuid":"1152543836","full_name":"agenticklabs/agentick","owner":"agenticklabs","description":"The component framework for AI.","archived":false,"fork":false,"pushed_at":"2026-06-07T14:29:38.000Z","size":10383,"stargazers_count":5,"open_issues_count":6,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2026-06-07T15:17:59.725Z","etag":null,"topics":["agentic","framework","jsx","react","tool-use","typescript"],"latest_commit_sha":null,"homepage":"https://agenticklabs.github.io/agentick/","language":"TypeScript","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/agenticklabs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":".github/SECURITY.md","support":null,"governance":null,"roadmap":"ROADMAP.md","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-08T03:07:34.000Z","updated_at":"2026-06-07T14:15:42.000Z","dependencies_parsed_at":"2026-02-14T03:02:44.555Z","dependency_job_id":null,"html_url":"https://github.com/agenticklabs/agentick","commit_stats":null,"previous_names":["agenticklabs/agentick"],"tags_count":2856,"template":false,"template_full_name":null,"purl":"pkg:github/agenticklabs/agentick","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agenticklabs%2Fagentick","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agenticklabs%2Fagentick/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agenticklabs%2Fagentick/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agenticklabs%2Fagentick/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/agenticklabs","download_url":"https://codeload.github.com/agenticklabs/agentick/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agenticklabs%2Fagentick/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34027670,"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-07T02:00:07.652Z","response_time":124,"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":["agentic","framework","jsx","react","tool-use","typescript"],"created_at":"2026-02-11T00:00:37.012Z","updated_at":"2026-06-07T16:01:08.593Z","avatar_url":"https://github.com/agenticklabs.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# agentick\n\n**The component framework for AI.**\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=for-the-badge\u0026logo=typescript\u0026logoColor=white)](https://www.typescriptlang.org/)\n[![React](https://img.shields.io/badge/React_19-reconciler-blue?style=for-the-badge\u0026logo=react\u0026logoColor=white)](https://react.dev/)\n[![Node.js](https://img.shields.io/badge/Node.js-%E2%89%A520-339933?style=for-the-badge\u0026logo=node.js\u0026logoColor=white)](https://nodejs.org/)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge)](https://github.com/agenticklabs/agentick/pulls)\n\nA React reconciler where the render target is a language model. You build the context window with JSX — the same components, hooks, and composition you already know — and the framework compiles it into what the model sees.\n\n```tsx\nimport { createApp, System, Timeline, createTool, useContinuation } from \"@agentick/core\";\nimport { openai } from \"@agentick/openai\";\nimport { z } from \"zod\";\n\nconst Search = createTool({\n  name: \"search\",\n  description: \"Search the knowledge base\",\n  input: z.object({ query: z.string() }),\n  handler: async ({ query }) =\u003e {\n    const results = await knowledgeBase.search(query);\n    return [{ type: \"text\", text: JSON.stringify(results) }];\n  },\n});\n\nfunction ResearchAgent() {\n  useContinuation((result) =\u003e result.tick \u003c 10);\n\n  return (\n    \u003c\u003e\n      \u003cSystem\u003eSearch thoroughly, then write a summary.\u003c/System\u003e\n      \u003cTimeline /\u003e\n      \u003cSearch /\u003e\n    \u003c/\u003e\n  );\n}\n\nconst app = createApp(ResearchAgent, { model: openai({ model: \"gpt-4o\" }) });\nconst result = await app.run({\n  messages: [\n    { role: \"user\", content: [{ type: \"text\", text: \"What's new in quantum computing?\" }] },\n  ],\n});\nconsole.log(result.response);\n```\n\n## Quick Start\n\n```bash\nnpm install agentick @agentick/openai zod\n```\n\nAdd to `tsconfig.json`:\n\n```json\n{\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"react\"\n  }\n}\n```\n\n## Why Agentick\n\nEvery other AI framework gives you a pipeline. A chain. A graph. You slot your prompt into a template, bolt on some tools, and hope the model figures it out.\n\nAgentick gives you a **programming language for AI applications.** The context window is your canvas. Components compose into it. Tools render their state back into it. Hooks run arbitrary code between ticks — verify output, summarize history, gate continuation. The model's entire world is JSX that you control, down to how individual content blocks render.\n\nThere are no prompt templates because JSX _is_ the template language. There are no special abstractions between you and what the model sees — you build it, the framework compiles it, the model reads it. When the model calls a tool, your component re-renders. When you want older messages compressed, you write a component. When you need to verify output before continuing, you write a hook.\n\nThis is application development, not chatbot configuration.\n\n## Built-in Components\n\nEverything in the component tree compiles to what the model sees. Components are the building blocks — compose them to construct the context window.\n\n### Structure\n\n| Component             | Description                                                                                                                                                                                                             |\n| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `\u003cTimeline\u003e`          | Conversation history. Accepts a render function for full control, or renders with sensible defaults. Token budget compaction via `maxTokens`, `strategy`, `filter`, `limit`, `roles`.                                   |\n| `\u003cTimeline.Provider\u003e` | Context provider exposing timeline entries to descendants via `useTimelineContext()`.                                                                                                                                   |\n| `\u003cTimeline.Messages\u003e` | Renders messages from `Timeline.Provider` context. Optional `renderEntry` prop for custom rendering.                                                                                                                    |\n| `\u003cSection\u003e`           | Structured context block injected every tick. `audience` controls visibility: `\"model\"`, `\"user\"`, or `\"all\"`.                                                                                                          |\n| `\u003cTool\u003e`              | Tool the model can call. Use inline (`\u003cTool name=\"...\" handler={...} /\u003e`) or via `createTool()`. Supports `render()` for persistent state, `use()` for context injection, `audience: \"user\"` for model-hidden dispatch. |\n| `\u003cModel\u003e`             | Model configuration. Pass `engine` prop, or use adapter-specific components like `\u003cOpenAIModel\u003e` or `\u003cGoogleModel\u003e`.                                                                                                    |\n\n### Messages\n\n| Component      | Description                                                                                                                  |\n| -------------- | ---------------------------------------------------------------------------------------------------------------------------- |\n| `\u003cSystem\u003e`     | System/instruction message.                                                                                                  |\n| `\u003cUser\u003e`       | User message.                                                                                                                |\n| `\u003cAssistant\u003e`  | Assistant message.                                                                                                           |\n| `\u003cMessage\u003e`    | Generic message — takes `role` prop. The primitive underlying all role-specific components.                                  |\n| `\u003cToolResult\u003e` | Tool execution result. Requires `toolCallId`.                                                                                |\n| `\u003cEvent\u003e`      | Persisted application event. Use for structured logging that survives in the timeline.                                       |\n| `\u003cEphemeral\u003e`  | Non-persisted context. Visible during compilation but not saved to history. `position`: `\"start\"`, `\"before-user\"`, `\"end\"`. |\n| `\u003cGrounding\u003e`  | Semantic wrapper for grounding context (ephemeral). `audience`: `\"model\"`, `\"user\"`, `\"both\"`.                               |\n\n### Event Blocks\n\nUse inside `\u003cEvent\u003e` messages for structured event content:\n\n| Component       | Description                                                              |\n| --------------- | ------------------------------------------------------------------------ |\n| `\u003cUserAction\u003e`  | User action block. Props: `action`, `actor?`, `target?`, `details?`.     |\n| `\u003cSystemEvent\u003e` | System event block. Props: `event`, `source?`, `data?`.                  |\n| `\u003cStateChange\u003e` | State change block. Props: `entity`, `field?`, `from`, `to`, `trigger?`. |\n\n### Semantic Formatting\n\nCompile to renderer-appropriate output (markdown, XML, etc.):\n\n| Component              | Description                                                                                 |\n| ---------------------- | ------------------------------------------------------------------------------------------- |\n| `\u003cH1\u003e`, `\u003cH2\u003e`, `\u003cH3\u003e` | Heading levels 1–3.                                                                         |\n| `\u003cHeader level={n}\u003e`   | Generic heading (levels 1–6).                                                               |\n| `\u003cParagraph\u003e`          | Text paragraph block.                                                                       |\n| `\u003cList\u003e`               | List container. `ordered` for numbered, `task` for checkboxes. `title` for a heading.       |\n| `\u003cListItem\u003e`           | List item. `checked` prop for task lists.                                                   |\n| `\u003cTable\u003e`              | Table. `headers`/`rows` props for data, or `\u003cRow\u003e`/`\u003cColumn\u003e` children for JSX composition. |\n| `\u003cRow\u003e`                | Table row. `header` prop for header rows.                                                   |\n| `\u003cColumn\u003e`             | Table column. `align`: `\"left\"`, `\"center\"`, `\"right\"`.                                     |\n\n### Content Blocks\n\nTyped content for composing rich message content:\n\n| Component    | Description                                                                                 |\n| ------------ | ------------------------------------------------------------------------------------------- |\n| `\u003cText\u003e`     | Text content. Children or `text` prop. Supports inline formatting: `\u003cb\u003e`, `\u003cem\u003e`, `\u003ccode\u003e`. |\n| `\u003cImage\u003e`    | Image. `source: MediaSource` (URL or base64).                                               |\n| `\u003cDocument\u003e` | Document attachment. `source: MediaSource`, `title?`.                                       |\n| `\u003cAudio\u003e`    | Audio content. `source: MediaSource`, `transcript?`.                                        |\n| `\u003cVideo\u003e`    | Video content. `source: MediaSource`, `transcript?`.                                        |\n| `\u003cCode\u003e`     | Code block. `language` prop (typescript, python, etc.).                                     |\n| `\u003cJson\u003e`     | JSON data block. `data` prop for objects, or `text`/children for raw JSON strings.          |\n\n## The Context Is Yours\n\nThe core insight: **only what you render gets sent to the model.** `\u003cTimeline\u003e` isn't a magic black box — it accepts a render function, and you decide exactly how every message appears in the context window. Skip a message? The model never sees it. Rewrite it? That's what the model reads.\n\n```tsx\n\u003cTimeline\u003e\n  {(history, pending) =\u003e (\n    \u003c\u003e\n      {history.map((entry, i) =\u003e {\n        const msg = entry.message;\n        const isOld = i \u003c history.length - 6;\n\n        if (isOld \u0026\u0026 msg.role === \"user\") {\n          const textOnly = msg.content\n            .filter((b) =\u003e b.type === \"text\")\n            .map((b) =\u003e b.text)\n            .join(\" \");\n          return (\n            \u003cMessage key={i} role=\"user\"\u003e\n              [Earlier: {textOnly.slice(0, 100)}...]\n            \u003c/Message\u003e\n          );\n        }\n\n        if (isOld \u0026\u0026 msg.role === \"assistant\") {\n          return (\n            \u003cMessage key={i} role=\"assistant\"\u003e\n              [Previous response]\n            \u003c/Message\u003e\n          );\n        }\n\n        return \u003cMessage key={i} {...msg} /\u003e;\n      })}\n      {pending.map((msg, i) =\u003e (\n        \u003cMessage key={`p-${i}`} {...msg.message} /\u003e\n      ))}\n    \u003c/\u003e\n  )}\n\u003c/Timeline\u003e\n```\n\nImages from 20 messages ago eating your context window? Render them as `[Image: beach sunset]`. Tool results from early in the conversation? Collapse them. Recent messages? Full detail. You write the function, you decide.\n\n### Default — Just Works\n\nWith no children, `\u003cTimeline /\u003e` renders conversation history with sensible defaults:\n\n```tsx\nfunction SimpleAgent() {\n  return (\n    \u003c\u003e\n      \u003cSystem\u003eYou are helpful.\u003c/System\u003e\n      \u003cTimeline /\u003e\n    \u003c/\u003e\n  );\n}\n```\n\n### Composability — It's React\n\nThat render logic getting complex? Extract it into a component:\n\n```tsx\nfunction CompactMessage({ entry }: { entry: COMTimelineEntry }) {\n  const msg = entry.message;\n\n  const summary = msg.content\n    .map((block) =\u003e {\n      switch (block.type) {\n        case \"text\":\n          return block.text.slice(0, 80);\n        case \"image\":\n          return `[Image: ${block.source?.description ?? \"image\"}]`;\n        case \"tool_use\":\n          return `[Called ${block.name}]`;\n        case \"tool_result\":\n          return `[Result from ${block.name}]`;\n        default:\n          return \"\";\n      }\n    })\n    .filter(Boolean)\n    .join(\" | \");\n\n  return \u003cMessage role={msg.role}\u003e{summary}\u003c/Message\u003e;\n}\n\nfunction Agent() {\n  return (\n    \u003c\u003e\n      \u003cSystem\u003eYou are helpful.\u003c/System\u003e\n      \u003cTimeline\u003e\n        {(history, pending) =\u003e (\n          \u003c\u003e\n            {history.map((entry, i) =\u003e\n              i \u003c history.length - 4 ? (\n                \u003cCompactMessage key={i} entry={entry} /\u003e\n              ) : (\n                \u003cMessage key={i} {...entry.message} /\u003e\n              ),\n            )}\n            {pending.map((msg, i) =\u003e (\n              \u003cMessage key={`p-${i}`} {...msg.message} /\u003e\n            ))}\n          \u003c/\u003e\n        )}\n      \u003c/Timeline\u003e\n    \u003c/\u003e\n  );\n}\n```\n\nOr go further — you don't even need `\u003cTimeline\u003e`. Render the entire conversation as a single user message:\n\n```tsx\nfunction NarrativeAgent() {\n  return (\n    \u003c\u003e\n      \u003cSystem\u003eContinue the conversation.\u003c/System\u003e\n      \u003cTimeline\u003e\n        {(history) =\u003e (\n          \u003cUser\u003e\n            Here's what happened so far:{\"\\n\"}\n            {history.map((e) =\u003e `${e.message.role}: ${extractText(e)}`).join(\"\\n\")}\n          \u003c/User\u003e\n        )}\n      \u003c/Timeline\u003e\n    \u003c/\u003e\n  );\n}\n```\n\nThe framework doesn't care how you structure the context. Multiple messages, one message, XML, prose — anything that compiles to content blocks gets sent.\n\n### Sections — Structured Context\n\n```tsx\nfunction AgentWithContext({ userId }: { userId: string }) {\n  const profile = useData(\"profile\", () =\u003e fetchProfile(userId), [userId]);\n\n  return (\n    \u003c\u003e\n      \u003cSystem\u003eYou are a support agent.\u003c/System\u003e\n      \u003cSection id=\"user-context\" audience=\"model\"\u003e\n        Customer: {profile?.name}, Plan: {profile?.plan}, Since: {profile?.joinDate}\n      \u003c/Section\u003e\n      \u003cTimeline /\u003e\n      \u003cTicketTool /\u003e\n    \u003c/\u003e\n  );\n}\n```\n\n`\u003cSection\u003e` injects structured context that the model sees every tick — live data, computed state, whatever you need. The `audience` prop controls visibility (`\"model\"`, `\"user\"`, or `\"all\"`).\n\n### Knobs — The Model Controls Its Context\n\nLike accordions in a UI. The model sees section headers and expands what it needs:\n\n```tsx\nfunction SupportAgent() {\n  const [active] = useKnob(\"section\", \"none\", {\n    options: [\"none\", \"api\", \"billing\", \"troubleshooting\"],\n    description: \"Expand a documentation section\",\n    momentary: true, // auto-collapses after each execution\n  });\n\n  return (\n    \u003c\u003e\n      \u003cSystem\u003eYou help users with our product. Expand a section when you need it.\u003c/System\u003e\n\n      \u003cSection id=\"api\" audience=\"model\"\u003e\n        {active === \"api\" ? apiDocs : \"API Reference (expand to read)\"}\n      \u003c/Section\u003e\n      \u003cSection id=\"billing\" audience=\"model\"\u003e\n        {active === \"billing\" ? billingDocs : \"Billing Guide (expand to read)\"}\n      \u003c/Section\u003e\n      \u003cSection id=\"troubleshooting\" audience=\"model\"\u003e\n        {active === \"troubleshooting\" ? troubleshootingDocs : \"Troubleshooting (expand to read)\"}\n      \u003c/Section\u003e\n\n      \u003cKnobs /\u003e\n      \u003cTimeline /\u003e\n    \u003c/\u003e\n  );\n}\n```\n\nThe model sees collapsed headers → sets the knob → reads the content → answers. The `momentary` flag resets the knob after each execution, so sections collapse automatically. Only what the model needs consumes tokens.\n\n## Hooks\n\nHooks are real React hooks — `useState`, `useEffect`, `useMemo` — plus lifecycle hooks that fire at each phase of the agent execution loop.\n\n### All Hooks\n\n#### Lifecycle\n\n| Hook                  | Description                                                                                                                         |\n| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |\n| `useOnMount(cb)`      | Run once when component first mounts.                                                                                               |\n| `useOnUnmount(cb)`    | Run once when component unmounts.                                                                                                   |\n| `useOnTickStart(cb)`  | Run at start of each tick (tick 2+). Receives `(tickState, ctx)`.                                                                   |\n| `useOnTickEnd(cb)`    | Run at end of each tick. Receives `(result, ctx)`. Return `false` or call `result.stop()` to halt.                                  |\n| `useAfterCompile(cb)` | Run after compilation completes. Receives `(compiled, ctx)`.                                                                        |\n| `useContinuation(cb)` | Control whether execution continues. `result.shouldContinue` shows framework default. Return `boolean`, object, or `void` to defer. |\n\n#### State \u0026 Signals\n\n| Hook                         | Description                                                                                                     |\n| ---------------------------- | --------------------------------------------------------------------------------------------------------------- |\n| `useSignal(initial)`         | Reactive signal. `.set()`, `.update()`, `.subscribe()`. Reads outside render, triggers reconciliation on write. |\n| `useComputed(fn, deps)`      | Computed signal. Auto-updates when dependencies change.                                                         |\n| `useComState(key, default?)` | Reactive COM state. Bidirectional sync with the context object model.                                           |\n\nStandalone signal factories (no hook rules — use anywhere):\n\n| Function          | Description                                                                                |\n| ----------------- | ------------------------------------------------------------------------------------------ |\n| `signal(initial)` | Create a signal.                                                                           |\n| `computed(fn)`    | Create a computed signal.                                                                  |\n| `effect(fn)`      | Run side effect with automatic dependency tracking. Returns `EffectRef` with `.dispose()`. |\n| `batch(fn)`       | Batch signal updates — effects fire once after all updates.                                |\n| `untracked(fn)`   | Read signals without tracking as dependencies.                                             |\n\n#### Data\n\n| Hook                           | Description                                                                                                   |\n| ------------------------------ | ------------------------------------------------------------------------------------------------------------- |\n| `useData(key, fetcher, deps?)` | Async data fetch with resolve-then-render. Throws promise on first render, returns cached value on re-render. |\n| `useInvalidateData()`          | Returns `(pattern: string \\| RegExp) =\u003e void` to invalidate cached data.                                      |\n\n#### Knobs (Model-Visible State)\n\nKnobs are reactive values the model can see _and set_ via tool calls:\n\n| API                         | Description                                                                    |\n| --------------------------- | ------------------------------------------------------------------------------ |\n| `knob(default, opts?)`      | Create a knob descriptor at config level.                                      |\n| `useKnob(name, descriptor)` | Hook returning `[resolvedValue, setValue]`.                                    |\n| `\u003cKnobs /\u003e`                 | Component that renders all knobs as a `\u003cSection\u003e` + registers `set_knob` tool. |\n| `\u003cKnobs.Provider\u003e`          | Context provider for custom knob rendering.                                    |\n| `\u003cKnobs.Controls\u003e`          | Renders knob controls from `Knobs.Provider` context.                           |\n| `isKnob(value)`             | Type guard for knob descriptors.                                               |\n\n#### Gates (Continuation Conditions)\n\nGates are named checkpoints that block the model from completing until cleared. Built on knobs + continuation — a three-state knob (`inactive`/`active`/`deferred`) with an auto-activation trigger and an exit blocker.\n\n| API                         | Description                                                                       |\n| --------------------------- | --------------------------------------------------------------------------------- |\n| `gate(descriptor)`          | Create a gate descriptor with `description`, `instructions`, and `activateWhen`.  |\n| `useGate(name, descriptor)` | Hook returning `GateState`: `{active, deferred, engaged, clear, defer, element}`. |\n\nThe model clears gates via the existing `set_knob` tool. See [hooks README](packages/core/src/hooks/README.md#gates-continuation-conditions) for full documentation.\n\n#### Context \u0026 Environment\n\n| Hook                       | Description                                                        |\n| -------------------------- | ------------------------------------------------------------------ |\n| `useCom()`                 | Access the COM (context object model) — state, timeline, channels. |\n| `useTickState()`           | Current tick state: `{tick, previous, queuedMessages}`.            |\n| `useRuntimeStore()`        | Runtime data store (hooks, knobs, lifecycle callbacks).            |\n| `useFormatter()`           | Access message formatter context.                                  |\n| `useContextInfo()`         | Real-time context utilization: token counts, utilization %.        |\n| `useTimelineContext()`     | Timeline context (requires `Timeline.Provider` ancestor).          |\n| `useConversationHistory()` | Full conversation history from COM (no provider needed).           |\n\n#### React (re-exported)\n\nAll standard React hooks work in agent components: `useState`, `useEffect`, `useReducer`, `useMemo`, `useCallback`, `useRef`.\n\n### Stop Conditions\n\nThe agent loop auto-continues when the model makes tool calls or messages are queued. `useContinuation` adds your own stop conditions.\n\n`result.shouldContinue` shows the framework's current decision (including overrides from prior callbacks in the chain). Return nothing to defer, or override with a boolean, object, or `result.stop()`/`result.continue()`:\n\n```tsx\n// Veto: stop even if framework would continue\nuseContinuation((result) =\u003e {\n  if (result.tick \u003e= 10) return { stop: true, reason: \"max-ticks\" };\n  if (result.usage \u0026\u0026 result.usage.totalTokens \u003e 100_000) return false;\n});\n\n// Defer: no return = accept framework decision\nuseContinuation((result) =\u003e {\n  logger.info(`tick ${result.tick}, continuing: ${result.shouldContinue}`);\n});\n```\n\n### Between-Tick Logic\n\n`useContinuation` is sugar for `useOnTickEnd`. Use the full version when you need to do real work:\n\n```tsx\nfunction VerifiedAgent() {\n  useOnTickEnd(async (result) =\u003e {\n    if (result.text \u0026\u0026 !result.toolCalls.length) {\n      const quality = await verifyWithModel(result.text);\n      if (!quality.acceptable) result.continue(\"failed-verification\");\n    }\n  });\n\n  return (\n    \u003c\u003e\n      \u003cSystem\u003eBe accurate. Your responses will be verified.\u003c/System\u003e\n      \u003cTimeline /\u003e\n    \u003c/\u003e\n  );\n}\n```\n\n### Gates — Named Exit Conditions\n\nWhen `useContinuation` is too low-level and you want a named, stateful checkpoint, use `useGate`. The gate activates when a condition is met, blocks the model from completing until it explicitly clears the gate, and auto-renders instructions via `\u003cEphemeral\u003e`.\n\n```tsx\nimport { gate, useGate, Knobs } from \"@agentick/core\";\n\nconst verificationGate = gate({\n  description: \"Verify your changes before completing\",\n  instructions: `VERIFICATION PENDING: You've modified files.\nRun typecheck, tests, or lint. Clear the gate when satisfied.`,\n  activateWhen: (result) =\u003e\n    result.toolCalls.some((tc) =\u003e [\"write_file\", \"edit_file\"].includes(tc.name)),\n});\n\nfunction CodingAgent() {\n  const verification = useGate(\"verification\", verificationGate);\n\n  return (\n    \u003c\u003e\n      \u003cSystem\u003eYou are a coding agent.\u003c/System\u003e\n      \u003cTimeline /\u003e\n      \u003cKnobs /\u003e\n      {verification.element}\n    \u003c/\u003e\n  );\n}\n```\n\nThe model edits a file → gate activates → model gets another turn with verification instructions → runs checks → calls `set_knob(name=\"verification\", value=\"inactive\")` → execution completes normally.\n\nThe model can also `set_knob(name=\"verification\", value=\"deferred\")` to acknowledge the gate without addressing it immediately. Deferred gates still block at exit — they un-defer to `active` first, forcing one more turn.\n\n### Custom Hooks\n\nCustom hooks work exactly like React — they're just functions that call other hooks:\n\n```tsx\n// Reusable hook: stop after a token budget\nfunction useTokenBudget(maxTokens: number) {\n  const [spent, setSpent] = useState(0);\n\n  useOnTickEnd((result) =\u003e {\n    const total = spent + (result.usage?.totalTokens ?? 0);\n    setSpent(total);\n    if (total \u003e maxTokens) result.stop(\"budget-exceeded\");\n  });\n\n  return spent;\n}\n\n// Reusable hook: verify output before finishing\nfunction useVerifiedOutput(verifier: (text: string) =\u003e Promise\u003cboolean\u003e) {\n  useOnTickEnd(async (result) =\u003e {\n    if (!result.text || result.toolCalls.length \u003e 0) return;\n    const ok = await verifier(result.text);\n    if (!ok) result.continue(\"failed-verification\");\n  });\n}\n\n// Compose them — it's just functions\nfunction CarefulAgent() {\n  const spent = useTokenBudget(50_000);\n  useVerifiedOutput(myVerifier);\n\n  return (\n    \u003c\u003e\n      \u003cSystem\u003eYou have a token budget. Be concise.\u003c/System\u003e\n      \u003cSection id=\"budget\" audience=\"model\"\u003e\n        Tokens used: {spent}\n      \u003c/Section\u003e\n      \u003cTimeline /\u003e\n    \u003c/\u003e\n  );\n}\n```\n\n## Tools Render State\n\nTools aren't just functions the model calls — they render their state back into the context window. The model sees the current state _every time it thinks_, not just in the tool response.\n\n```tsx\nconst TodoTool = createTool({\n  name: \"manage_todos\",\n  description: \"Add, complete, or list todos\",\n  input: z.object({\n    action: z.enum([\"add\", \"complete\", \"list\"]),\n    text: z.string().optional(),\n    id: z.number().optional(),\n  }),\n  handler: async ({ action, text, id }, ctx) =\u003e {\n    if (action === \"add\") todos.push({ id: todos.length, text, done: false });\n    if (action === \"complete\") todos[id!].done = true;\n    return [{ type: \"text\", text: \"Done.\" }];\n  },\n  render: () =\u003e (\n    \u003cSection id=\"todos\" audience=\"model\"\u003e\n      Current todos: {JSON.stringify(todos)}\n    \u003c/Section\u003e\n  ),\n});\n```\n\nEverything is dual-use — tools and models work as JSX components in the tree _and_ as direct function calls:\n\n```tsx\n// JSX — in the component tree\n\u003cSearch /\u003e\n\u003cmodel temperature={0.2} /\u003e\n\n// Direct calls — use programmatically\nconst output = await Search.run({ query: \"test\" });\nconst handle = await model.generate(input);\n```\n\n### Tool Types\n\nTools have execution types and intents that control routing and behavior:\n\n| Execution Type | Description                              |\n| -------------- | ---------------------------------------- |\n| `SERVER`       | Executes on server (default).            |\n| `CLIENT`       | Executes in browser.                     |\n| `MCP`          | Routed to Model Context Protocol server. |\n| `PROVIDER`     | Handled by model provider natively.      |\n\n| Intent    | Description                |\n| --------- | -------------------------- |\n| `COMPUTE` | Returns data (default).    |\n| `ACTION`  | Performs side effects.     |\n| `RENDER`  | Produces UI/visualization. |\n\n## Sessions\n\n```tsx\nconst app = createApp(Agent, { model: openai({ model: \"gpt-4o\" }) });\nconst session = await app.session(\"conv-1\");\n\nconst msg = (text: string) =\u003e ({\n  role: \"user\" as const,\n  content: [{ type: \"text\" as const, text }],\n});\n\nawait session.send({ messages: [msg(\"Hi there!\")] });\nawait session.send({ messages: [msg(\"Tell me a joke\")] });\n\n// Stream responses\nfor await (const event of session.send({ messages: [msg(\"Another one\")] })) {\n  if (event.type === \"content_delta\") process.stdout.write(event.delta);\n}\n\nawait session.close();\n```\n\nSessions are long-lived conversation contexts. Each `send()` creates an **execution** (one user message → model response cycle). Each model API call within an execution is a **tick**. Multi-tick executions happen automatically with tool use.\n\n```\nSession\n├── Execution 1 (user: \"Hello\")\n│   └── Tick 1 → model response\n├── Execution 2 (user: \"Use calculator\")\n│   ├── Tick 1 → tool_use (calculator)\n│   └── Tick 2 → final response\n└── Execution 3 ...\n```\n\nSession states: `idle` → `running` → `idle` (or `closed`).\n\n### Dynamic Model Selection\n\nModels are JSX components — conditionally render them:\n\n```tsx\nconst gpt = openai({ model: \"gpt-4o\" });\nconst gemini = google({ model: \"gemini-2.5-pro\" });\n\nfunction AdaptiveAgent({ task }: { task: string }) {\n  return (\n    \u003c\u003e\n      {task.includes(\"creative\") ? \u003cgemini temperature={0.9} /\u003e : \u003cgpt temperature={0.2} /\u003e}\n      \u003cSystem\u003eHandle this task: {task}\u003c/System\u003e\n      \u003cTimeline /\u003e\n    \u003c/\u003e\n  );\n}\n```\n\n## Execution Runners\n\nThe context window is JSX. But what _consumes_ that context — and how tool calls _execute_ — is pluggable.\n\nAn `ExecutionRunner` is a swappable backend that sits between the compiled context and execution. It transforms what the model sees, intercepts how tools run, and manages its own lifecycle state. Your agent code doesn't change — the runner changes the execution model underneath it.\n\n```tsx\nimport { type ExecutionRunner } from \"@agentick/core\";\n\nconst repl: ExecutionRunner = {\n  name: \"repl\",\n\n  // The model sees command descriptions instead of tool schemas\n  transformCompiled(compiled, tools) {\n    return { ...compiled, tools: [executeTool] };\n  },\n\n  // \"execute\" calls go to a sandbox; everything else runs normally\n  async executeToolCall(call, tool, next) {\n    if (call.name === \"execute\") return sandbox.run(call.input.code);\n    return next();\n  },\n\n  onSessionInit(session) {\n    sandbox.create(session.id);\n  },\n  onDestroy(session) {\n    sandbox.destroy(session.id);\n  },\n};\n\nconst app = createApp(Agent, { model, runner: repl });\n```\n\nSame agent, same JSX, different execution model. Build once — run against standard tool_use in production, a sandboxed REPL for code execution, a human-approval gateway for sensitive operations.\n\nAll hooks are optional. Without a runner, standard model → tool_use behavior applies. Runners are inherited by spawned child sessions — override per-child via `SpawnOptions`:\n\n```tsx\nawait session.spawn(CodeAgent, { messages }, { runner: replEnv });\n```\n\n## Testing\n\nAgentick includes a full testing toolkit. Render agents, compile context, mock models, and assert on behavior — all without making real API calls.\n\n### `renderAgent` — Full Execution\n\nRender an agent in a test environment with a mock model:\n\n```tsx\nimport { renderAgent, cleanup } from \"@agentick/core/testing\";\nimport { afterEach, test, expect } from \"vitest\";\n\nafterEach(cleanup);\n\ntest(\"research agent searches then summarizes\", async () =\u003e {\n  const { send, model } = renderAgent(\u003cResearchAgent /\u003e);\n\n  // Queue model responses\n  model.addResponse({ text: \"\", toolCalls: [{ name: \"search\", input: { query: \"quantum\" } }] });\n  model.addResponse({ text: \"Here's a summary of quantum computing...\" });\n\n  const result = await send(\"What's new in quantum computing?\");\n\n  expect(result.response).toContain(\"summary\");\n  expect(model.calls).toHaveLength(2); // two ticks\n});\n```\n\n### `compileAgent` — Inspect Context\n\nCompile an agent without executing to inspect what the model would see:\n\n```tsx\ntest(\"agent includes user context in system prompt\", async () =\u003e {\n  const { sections, tools } = compileAgent(\u003cAgentWithContext userId=\"user-123\" /\u003e);\n\n  expect(sections).toContainEqual(expect.objectContaining({ id: \"user-context\" }));\n  expect(tools.map((t) =\u003e t.name)).toContain(\"create_ticket\");\n});\n```\n\n### Test Adapter\n\nCreate a mock model adapter for fine-grained control over streaming:\n\n```tsx\nimport { createTestAdapter } from \"@agentick/core/testing\";\n\nconst adapter = createTestAdapter();\n\n// Simulate streaming chunks\nadapter.stream([\n  { type: \"text\", text: \"Hello \" },\n  { type: \"text\", text: \"world!\" },\n  { type: \"finish\", stopReason: \"end_turn\" },\n]);\n```\n\n### Mocks \u0026 Helpers\n\n| Utility                       | Description                                           |\n| ----------------------------- | ----------------------------------------------------- |\n| `createMockApp()`             | Mock app for client/transport tests.                  |\n| `createMockSession()`         | Mock session with send/close/abort.                   |\n| `createMockExecutionHandle()` | Mock execution handle (async iterable + result).      |\n| `createTestRunner()`          | Mock execution runner with call tracking.             |\n| `createMockCom()`             | Mock COM for hook tests.                              |\n| `createMockTickState()`       | Mock tick state.                                      |\n| `createMockTickResult()`      | Mock tick result for `useOnTickEnd` tests.            |\n| `makeTimelineEntry()`         | Create timeline entries for assertions.               |\n| `act(fn)` / `actSync(fn)`     | Execute in act context.                               |\n| `waitFor(fn)`                 | Poll until condition is met.                          |\n| `flushMicrotasks()`           | Flush pending microtasks.                             |\n| `createDeferred()`            | Create deferred promise with external resolve/reject. |\n\n## Terminal UI\n\n`@agentick/tui` provides an Ink-based terminal interface for chatting with agents — locally or over the network.\n\n```bash\nnpm install @agentick/tui\n```\n\n### Local — In-Process\n\nConnect directly to an app. No server needed:\n\n```tsx\nimport { createTUI } from \"@agentick/tui\";\nimport { createApp, System, Timeline } from \"@agentick/core\";\nimport { openai } from \"@agentick/openai\";\n\nfunction Agent() {\n  return (\n    \u003c\u003e\n      \u003cSystem\u003eYou are helpful.\u003c/System\u003e\n      \u003cTimeline /\u003e\n    \u003c/\u003e\n  );\n}\n\nconst app = createApp(Agent, { model: openai({ model: \"gpt-4o\" }) });\nconst tui = createTUI({ app });\nawait tui.start();\n```\n\n### Remote — Over SSE\n\nConnect to a running gateway or express server:\n\n```tsx\nconst tui = createTUI({ url: \"http://localhost:3000/api\" });\nawait tui.start();\n```\n\n### CLI\n\n```bash\n# Run a local agent file\nagentick-tui --app ./my-agent.tsx\n\n# Connect to a remote server\nagentick-tui --url http://localhost:3000/api\n\n# Custom export name\nagentick-tui --app ./agents.tsx --export SalesAgent\n\n# Custom UI component\nagentick-tui --app ./my-agent.tsx --ui ./dashboard.tsx\n```\n\n### Pluggable UI\n\nThe TUI ships with a default `Chat` component, but you can provide your own:\n\n```tsx\nimport { createTUI, type TUIComponent } from \"@agentick/tui\";\nimport { useSession, useStreamingText } from \"@agentick/react\";\n\nconst Dashboard: TUIComponent = ({ sessionId }) =\u003e {\n  const { send } = useSession({ sessionId });\n  const { text, isStreaming } = useStreamingText({ sessionId });\n  // ... your Ink components\n};\n\nconst tui = createTUI({ app, ui: Dashboard });\n```\n\nAll building-block components are exported for custom UIs: `MessageList`, `StreamingMessage`, `ToolCallIndicator`, `ToolConfirmationPrompt`, `InputBar`, `ErrorDisplay`.\n\n## React Hooks for UIs\n\n`@agentick/react` provides hooks for building browser or terminal UIs over agent sessions. These are pure React — no browser APIs — so they work in both React DOM and Ink.\n\n```bash\nnpm install @agentick/react\n```\n\n| Hook                      | Description                                                    |\n| ------------------------- | -------------------------------------------------------------- |\n| `useClient()`             | Access the Agentick client from context.                       |\n| `useConnection()`         | SSE connection state: `{state, isConnected, isConnecting}`.    |\n| `useSession(opts?)`       | Session accessor: `{send, abort, close, subscribe, accessor}`. |\n| `useEvents(opts?)`        | Subscribe to stream events. Returns `{event, clear()}`.        |\n| `useStreamingText(opts?)` | Accumulated streaming text: `{text, isStreaming, clear()}`.    |\n| `useContextInfo(opts?)`   | Context utilization info (token counts, %).                    |\n\nWrap your app in `\u003cAgentickProvider client={client}\u003e` to provide the client context.\n\n## Packages\n\n| Package               | Description                                                       |\n| --------------------- | ----------------------------------------------------------------- |\n| `@agentick/core`      | Reconciler, components, hooks, tools, sessions, testing utilities |\n| `@agentick/kernel`    | Execution kernel — procedures, context, middleware, channels      |\n| `@agentick/shared`    | Platform-independent types and utilities                          |\n| `@agentick/openai`    | OpenAI adapter (GPT-4o, o1, etc.)                                 |\n| `@agentick/google`    | Google AI adapter (Gemini)                                        |\n| `@agentick/ai-sdk`    | Vercel AI SDK adapter (any provider)                              |\n| `@agentick/gateway`   | Multi-app server with auth, routing, and channels                 |\n| `@agentick/express`   | Express.js integration                                            |\n| `@agentick/nestjs`    | NestJS integration                                                |\n| `@agentick/client`    | TypeScript client for gateway connections                         |\n| `@agentick/react`     | React hooks for building UIs over sessions                        |\n| `@agentick/tui`       | Terminal UI — Ink-based chat interface for local or remote agents |\n| `@agentick/devtools`  | Fiber tree inspector, tick scrubber, token tracker                |\n| `@agentick/cli`       | CLI for running agents                                            |\n| `@agentick/server`    | Server utilities                                                  |\n| `@agentick/socket.io` | Socket.IO transport                                               |\n\n## Adapters\n\nThree built-in, same interface. Or build your own — implement `prepareInput`, `mapChunk`, `execute`, and `executeStream`. See [`packages/adapters/README.md`](packages/adapters/README.md).\n\n```tsx\nimport { openai } from \"@agentick/openai\";\nimport { google } from \"@agentick/google\";\nimport { aiSdk } from \"@agentick/ai-sdk\";\n\nconst gpt = openai({ model: \"gpt-4o\" });\nconst gemini = google({ model: \"gemini-2.5-pro\" });\nconst sdk = aiSdk({ model: yourAiSdkModel });\n```\n\nAdapters return a `ModelClass` — callable _and_ a JSX component:\n\n```tsx\n// As JSX — configure model in the component tree\n\u003cgpt temperature={0.2} maxTokens={1000} /\u003e;\n\n// As function — call programmatically\nconst handle = await gpt.generate(input);\n```\n\n## DevTools\n\n### Agentick DevTools\n\n```tsx\nconst app = createApp(Agent, { model, devTools: true });\n```\n\nFiber tree inspector, tick-by-tick scrubber, token usage tracking, real-time execution timeline. Record full sessions for replay with `session({ recording: 'full' })`.\n\n### React DevTools\n\nAgentick is built on `react-reconciler` — the same foundation as React DOM and React Native. This means [React DevTools](https://github.com/facebook/react/tree/main/packages/react-devtools) works out of the box. You can inspect the component tree that compiles into the model's context window, live.\n\n```sh\nnpm install --save-dev react-devtools-core\n```\n\n```tsx\nimport { enableReactDevTools } from \"@agentick/core\";\n\nenableReactDevTools(); // connects to standalone DevTools on port 8097\n```\n\n```sh\n# Terminal 1: start React DevTools\nnpx react-devtools\n\n# Terminal 2: run your agent\nnode my-agent.js\n```\n\nYou'll see the full component tree — `\u003cSystem\u003e`, `\u003cTimeline\u003e`, `\u003cSection\u003e`, your custom components, tools — in the same inspector you use for web and mobile apps. Inspect props, watch state changes between ticks, and see exactly what compiles into the context window.\n\n## Gateway\n\nDeploy multiple apps behind a single server with auth, routing, and channel adapters:\n\n```tsx\nimport { createGateway } from \"@agentick/gateway\";\n\nconst gateway = createGateway({\n  apps: { support: supportApp, sales: salesApp },\n  defaultApp: \"support\",\n  auth: { type: \"token\", token: process.env.API_TOKEN! },\n});\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagenticklabs%2Fagentick","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fagenticklabs%2Fagentick","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagenticklabs%2Fagentick/lists"}