diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 9a3628b..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true, - "node": true, - "jest": true - }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "prettier" - ], - "overrides": [], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint", - "simple-import-sort", - "unused-imports" - ], - "rules": { - "indent": [ - "error", - 2 - ], - "quotes": [ - "error", - "single", - { - "avoidEscape": true, - "allowTemplateLiterals": true - } - ], - "quote-props": [ - "error", - "as-needed" - ], - "semi": [ - "error", - "always" - ], - "simple-import-sort/imports": 1, - "simple-import-sort/exports": 1, - "unused-imports/no-unused-imports": 1, - "@typescript-eslint/no-unused-vars": [ - 1, - { - "argsIgnorePattern": "React|res|next|^_" - } - ], - "@typescript-eslint/no-explicit-any": 0, - "@typescript-eslint/no-var-requires": 0, - "no-console": 0, - "@typescript-eslint/ban-ts-comment": 0, - "prefer-const": 0, - "no-case-declarations": 0, - "no-implicit-globals": 0, - "@typescript-eslint/no-unsafe-declaration-merging": 0 - } -} \ No newline at end of file diff --git a/README.md b/README.md index a1e0f09..5081b92 100644 --- a/README.md +++ b/README.md @@ -11,38 +11,66 @@

-A unified, streaming-capable interface for multiple LLM providers. +A provider-portable LLM toolkit with structured streaming, model registries, +cross-provider message normalization, and an optional stateful agent runtime. ## Packages -- **agentic-kit** — core library with provider abstraction and `AgentKit` manager +- **agentic-kit** — low-level portability layer with model descriptors, registries, structured event streams, and compatibility helpers +- **@agentic-kit/agent** — minimal stateful runtime with sequential tool execution and lifecycle events - **@agentic-kit/ollama** — adapter for local Ollama inference - **@agentic-kit/anthropic** — adapter for Anthropic Claude models -- **@agentic-kit/openai** — adapter for OpenAI and OpenAI-compatible APIs +- **@agentic-kit/openai** — generalized adapter for OpenAI-compatible chat completion APIs ## Getting Started ```bash git clone git@github.com:constructive-io/agentic-kit.git cd agentic-kit -yarn install -yarn build -yarn test +pnpm install +pnpm build +pnpm test ``` ## Usage ```typescript -import { createOllamaKit, createMultiProviderKit, OllamaAdapter } from 'agentic-kit'; +import { complete, getModel } from "agentic-kit"; -const kit = createOllamaKit('http://localhost:11434'); -const text = await kit.generate({ model: 'mistral', prompt: 'Hello' }); +const model = getModel("openai", "gpt-4o-mini"); +const message = await complete(model!, { + messages: [{ role: "user", content: "Hello", timestamp: Date.now() }], +}); -// Multi-provider -const multi = createMultiProviderKit(); -multi.addProvider(new OllamaAdapter('http://localhost:11434')); +console.log(message.content); ``` ## Contributing See individual package READMEs for docs and local dev instructions. + +## Testing + +Default tests stay deterministic and local: + +```bash +pnpm test +``` + +There is also a local-only Ollama live lane that does not hit hosted +providers. The default root command runs the fast smoke tier: + +```bash +OLLAMA_LIVE_MODEL=qwen3.5:4b pnpm test:live:ollama +``` + +Run the broader lane explicitly when you want slower behavioral coverage: + +```bash +OLLAMA_LIVE_MODEL=qwen3.5:4b pnpm test:live:ollama:extended +``` + +The Ollama live script performs a preflight against `OLLAMA_BASE_URL` and exits +cleanly if the local server or requested model is unavailable. If +`nomic-embed-text:latest` is installed, the lane also exercises local embedding +generation. diff --git a/REDESIGN_DECISIONS.md b/REDESIGN_DECISIONS.md new file mode 100644 index 0000000..6f31c07 --- /dev/null +++ b/REDESIGN_DECISIONS.md @@ -0,0 +1,76 @@ +# Agentic Kit Redesign Decisions + +Date: 2026-04-18 + +This document records the redesign decisions made while evaluating `agentic-kit` +against the comparable `pi-mono` architecture, especially `packages/ai` and +`packages/agent`. + +## Scope and Package Boundaries + +1. `agentic-kit` remains the low-level provider portability layer. +2. Stateful orchestration moves into a separate `@agentic-kit/agent` package. +3. Tool execution stays out of `agentic-kit` core; the core only models tools, + tool calls, and tool results. +4. `@agentic-kit/agent` v1 should be intentionally minimal, shipping only the + sequential tool loop, lifecycle events, abort/continue, and pluggable context + transforms. Steering/follow-up queues and richer interruption policies are + deferred to phase 2. + +## Core Type System + +5. Core tool definitions use plain JSON Schema. +6. TypeBox/Zod support stays as helper adapters, not the core contract. +7. Core models are represented by a provider-independent `ModelDescriptor` + registry with capability metadata. +8. The model registry must support both built-in descriptors and runtime + registration of custom models/providers from day one. +9. The core message model treats `image` input and `thinking` output as + first-class content blocks in v1. +10. `usage`, `cost`, `stopReason`, and abort-driven partial-result semantics are + mandatory parts of the core contract in v1. + +## Streaming and Conversation Semantics + +11. Structured event streams become the primary streaming primitive; text-only + chunk callbacks remain as convenience wrappers. +12. Cross-provider replay and handoff is a hard requirement for v1, including + normalization for reasoning blocks, tool-call IDs, aborted turns, and + orphaned tool results. + +## Provider Strategy + +13. OpenAI-compatible backends should be handled by one generalized adapter path + with compatibility flags, not many first-class provider packages. +14. Embeddings stay out of the primary conversational core and live behind a + separate optional capability interface or companion package. + +## Migration Strategy + +15. `agentic-kit` should ship a backward-compatibility layer for the current + `generate({ model, prompt }, { onChunk })` API for one transition release. + +## Architectural Implications + +These decisions imply the following target architecture: + +- `agentic-kit` + Low-level portability layer. Owns message/content types, model descriptors, + provider registry, streaming event protocol, compatibility transforms, usage, + and provider adapters. +- `@agentic-kit/agent` + Optional stateful runtime. Owns tool execution, sequential loop semantics, + lifecycle events, context transforms, and abort/continue behavior. +- Separate optional capabilities or companion packages + For non-conversational workloads such as embeddings, and optional schema + helpers such as TypeBox/Zod integration. + +## Design Principles Confirmed + +- Keep the protocol portable and runtime-agnostic. +- Normalize provider differences in the core instead of leaking them upward. +- Treat OpenAI-compatible APIs as a compatibility class, not a brand-specific + architecture. +- Avoid coupling the low-level layer to any single schema library or vendor SDK. +- Preserve a migration path from the existing text-only API while moving the + real architecture to structured messages and events. diff --git a/apps/tanstack-chat-demo/.cta.json b/apps/tanstack-chat-demo/.cta.json new file mode 100644 index 0000000..a50eba2 --- /dev/null +++ b/apps/tanstack-chat-demo/.cta.json @@ -0,0 +1,16 @@ +{ + "projectName": "tanstack-chat-demo", + "mode": "file-router", + "typescript": true, + "tailwind": true, + "packageManager": "pnpm", + "git": false, + "install": false, + "addOnOptions": {}, + "includeExamples": false, + "envVarValues": {}, + "routerOnly": false, + "version": 1, + "framework": "react", + "chosenAddOns": [] +} \ No newline at end of file diff --git a/apps/tanstack-chat-demo/.gitignore b/apps/tanstack-chat-demo/.gitignore new file mode 100644 index 0000000..8b25bb5 --- /dev/null +++ b/apps/tanstack-chat-demo/.gitignore @@ -0,0 +1,13 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +.env +.nitro +.tanstack +.wrangler +.output +.vinxi +__unconfig* +todos.json diff --git a/apps/tanstack-chat-demo/.vscode/settings.json b/apps/tanstack-chat-demo/.vscode/settings.json new file mode 100644 index 0000000..00b5278 --- /dev/null +++ b/apps/tanstack-chat-demo/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + } +} diff --git a/apps/tanstack-chat-demo/README.md b/apps/tanstack-chat-demo/README.md new file mode 100644 index 0000000..4aaad03 --- /dev/null +++ b/apps/tanstack-chat-demo/README.md @@ -0,0 +1,193 @@ +Welcome to your new TanStack Start app! + +# Getting Started + +To run this application: + +```bash +pnpm install +pnpm dev +``` + +# Building For Production + +To build this application for production: + +```bash +pnpm build +``` + +## Testing + +This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with: + +```bash +pnpm test +``` + +## Styling + +This project uses [Tailwind CSS](https://tailwindcss.com/) for styling. + +### Removing Tailwind CSS + +If you prefer not to use Tailwind CSS: + +1. Remove the demo pages in `src/routes/demo/` +2. Replace the Tailwind import in `src/styles.css` with your own styles +3. Remove `tailwindcss()` from the plugins array in `vite.config.ts` +4. Uninstall the packages: `pnpm add @tailwindcss/vite tailwindcss --dev` + + + +## Routing + +This project uses [TanStack Router](https://tanstack.com/router) with file-based routing. Routes are managed as files in `src/routes`. + +### Adding A Route + +To add a new route to your application just add a new file in the `./src/routes` directory. + +TanStack will automatically generate the content of the route file for you. + +Now that you have two routes you can use a `Link` component to navigate between them. + +### Adding Links + +To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`. + +```tsx +import { Link } from "@tanstack/react-router"; +``` + +Then anywhere in your JSX you can use it like so: + +```tsx +About +``` + +This will create a link that will navigate to the `/about` route. + +More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent). + +### Using A Layout + +In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you render `{children}` in the `shellComponent`. + +Here is an example layout that includes a header: + +```tsx +import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: 'My App' }, + ], + }), + shellComponent: ({ children }) => ( + + + + + +
+ +
+ {children} + + + + ), +}) +``` + +More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts). + +## Server Functions + +TanStack Start provides server functions that allow you to write server-side code that seamlessly integrates with your client components. + +```tsx +import { createServerFn } from '@tanstack/react-start' + +const getServerTime = createServerFn({ + method: 'GET', +}).handler(async () => { + return new Date().toISOString() +}) + +// Use in a component +function MyComponent() { + const [time, setTime] = useState('') + + useEffect(() => { + getServerTime().then(setTime) + }, []) + + return
Server time: {time}
+} +``` + +## API Routes + +You can create API routes by using the `server` property in your route definitions: + +```tsx +import { createFileRoute } from '@tanstack/react-router' +import { json } from '@tanstack/react-start' + +export const Route = createFileRoute('/api/hello')({ + server: { + handlers: { + GET: () => json({ message: 'Hello, World!' }), + }, + }, +}) +``` + +## Data Fetching + +There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered. + +For example: + +```tsx +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/people')({ + loader: async () => { + const response = await fetch('https://swapi.dev/api/people') + return response.json() + }, + component: PeopleComponent, +}) + +function PeopleComponent() { + const data = Route.useLoaderData() + return ( + + ) +} +``` + +Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters). + +# Demo files + +Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed. + +# Learn More + +You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com). + +For TanStack Start specific documentation, visit [TanStack Start](https://tanstack.com/start). diff --git a/apps/tanstack-chat-demo/components.json b/apps/tanstack-chat-demo/components.json new file mode 100644 index 0000000..f5b98b3 --- /dev/null +++ b/apps/tanstack-chat-demo/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-nova", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "#/components", + "utils": "#/lib/utils", + "ui": "#/components/ui", + "lib": "#/lib", + "hooks": "#/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/apps/tanstack-chat-demo/package.json b/apps/tanstack-chat-demo/package.json new file mode 100644 index 0000000..c5ac7b3 --- /dev/null +++ b/apps/tanstack-chat-demo/package.json @@ -0,0 +1,58 @@ +{ + "name": "tanstack-chat-demo", + "private": true, + "type": "module", + "imports": { + "#/*": "./src/*" + }, + "scripts": { + "dev": "vite dev --port 3000", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint . --fix", + "test": "vitest run --passWithNoTests" + }, + "dependencies": { + "@agentic-kit/ollama": "workspace:*", + "@agentic-kit/openai": "workspace:*", + "@fontsource-variable/geist": "^5.2.8", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/react-devtools": "latest", + "@tanstack/react-router": "latest", + "@tanstack/react-router-devtools": "latest", + "@tanstack/react-router-ssr-query": "latest", + "@tanstack/react-start": "latest", + "@tanstack/router-plugin": "^1.132.0", + "agentic-kit": "workspace:*", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.545.0", + "radix-ui": "^1.4.3", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "shadcn": "^4.3.0", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.1.18", + "tw-animate-css": "^1.4.0" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.16", + "@tanstack/devtools-vite": "latest", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.0", + "@types/node": "^22.10.2", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^6.0.1", + "jsdom": "^28.1.0", + "typescript": "^5.7.2", + "vite": "^8.0.0", + "vitest": "^3.0.5" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild", + "lightningcss" + ] + } +} diff --git a/apps/tanstack-chat-demo/public/favicon.ico b/apps/tanstack-chat-demo/public/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/apps/tanstack-chat-demo/public/favicon.ico differ diff --git a/apps/tanstack-chat-demo/public/logo192.png b/apps/tanstack-chat-demo/public/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/apps/tanstack-chat-demo/public/logo192.png differ diff --git a/apps/tanstack-chat-demo/public/logo512.png b/apps/tanstack-chat-demo/public/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/apps/tanstack-chat-demo/public/logo512.png differ diff --git a/apps/tanstack-chat-demo/public/manifest.json b/apps/tanstack-chat-demo/public/manifest.json new file mode 100644 index 0000000..078ef50 --- /dev/null +++ b/apps/tanstack-chat-demo/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "TanStack App", + "name": "Create TanStack App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/apps/tanstack-chat-demo/public/robots.txt b/apps/tanstack-chat-demo/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/apps/tanstack-chat-demo/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/apps/tanstack-chat-demo/src/components/chat/chat-input.tsx b/apps/tanstack-chat-demo/src/components/chat/chat-input.tsx new file mode 100644 index 0000000..9973df7 --- /dev/null +++ b/apps/tanstack-chat-demo/src/components/chat/chat-input.tsx @@ -0,0 +1,86 @@ +import { ArrowUp, Square } from 'lucide-react'; +import { type KeyboardEvent, useEffect, useRef, useState } from 'react'; + +import { cn } from '#/lib/utils'; + +type Props = { + onSend: (text: string) => void; + onStop: () => void; + disabled?: boolean; + isStreaming: boolean; +}; + +export function ChatInput({ onSend, onStop, disabled, isStreaming }: Props) { + const [value, setValue] = useState(''); + const textareaRef = useRef(null); + + useEffect(() => { + const el = textareaRef.current; + if (!el) return; + el.style.height = 'auto'; + el.style.height = `${Math.min(el.scrollHeight, 140)}px`; + }, [value]); + + const hasText = value.trim().length > 0; + const canSend = hasText && !disabled && !isStreaming; + const showAction = isStreaming || hasText; + + function submit() { + if (!canSend) return; + onSend(value); + setValue(''); + } + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + submit(); + } + } + + return ( +
+
+