SignalX AI
AI for SignalX — streaming models, tools and agents on signals.
🚧 Early release. The API surface is small and stabilising — feedback is very welcome.
📚 Documentation
Full guides, API reference and live examples → https://sigx.dev/ai/
Packages
Examples:
examples/chat— the@sigx/aiplayground: an SSR sigx app streaming a chat overserverStream, with the provider and model picked in the page per conversation (and validated server-side against the same table the picker renders from). Assistant text renders as markdown byRichTextViewfrom@sigx/richtext/dom; a scripted mock runs when no key is set.examples/agent— the@sigx/ai-agentplayground: open a session against any agent (sigx,mock, Claude Code, Codex, GitHub Copilot CLI, any ACP preset), on any model, in any mode — from the page, not from.env. Several at once, side by side on the same prompt. Switch a running session into plan mode or onto another model from its Settings panel, which is one<select>perConfigOptionand knows no adapter's vocabulary. Tool cards, permission prompts, sub-agent cards, cancel and usage; a second tab joins every live session and replays it. Runs with no key and no installed harness.
Install
npm install @sigx/ai @sigx/ai-anthropic @anthropic-ai/sdk
# or
npm install @sigx/ai @sigx/ai-openai openai
Quick start
// src/ai.server.ts — server only, never ships to the browser
import { serverStream } from '@sigx/server';
import { defineTool } from '@sigx/ai';
import { chatStream } from '@sigx/ai/server';
import { anthropic } from '@sigx/ai-anthropic';
import { z } from 'zod';
const model = anthropic().model('claude-opus-5');
const weather = defineTool({
name: 'get_weather',
description: 'Current weather for a city',
input: z.object({ city: z.string() }),
execute: async ({ city }) => ({ city, tempC: 21 })
});
export const chat = serverStream({
input: ChatInput,
handler: async function* ({ rq, input }) {
yield* chatStream({ model, tools: [weather], messages: input.messages, signal: rq.abortSignal });
}
});
// Chat.tsx
import { component } from 'sigx';
import { useChat } from '@sigx/ai/app';
import { chat } from './ai.server';
export const Chat = component(() => {
const thread = useChat({ stream: (input) => chat(input) });
return () => (
<>
{thread.messages.map((m) => <Message message={m} />)}
<Composer onSubmit={(text) => thread.send(text)} busy={thread.status === 'streaming'} />
</>
);
});
Every assistant message is made of signals: a streaming text delta updates one text node, never the transcript.
Why this exists
- Fine-grained streaming state — a token touches one signal, not a component tree.
- SSR-first AI pages —
useCompletionstreams a model answer inside the first HTML response and hydrates it without a second model call (built on core'suseStream). - One seam, many providers — the core knows nothing about vendors; a provider package only translates.
- Testable without a model —
mockModelscripts deterministic chunks for tests, docs and CI.
Part of SignalX
core—reactivity,runtime-core,runtime-dom,server-renderer,server,vite,sigxactors— virtual actors (durable agents are built on these)router·store·use·lynx·terminal- Docs site — main SignalX documentation
Contributing
See CONTRIBUTING.md. PRs welcome.
License
MIT © Andreas Ekdahl