Signal · README.md

This is README.md as @niscorp/signal 0.1.5 ships it. A coding agent reads it from node_modules. The file itself:

nisc.dev/signal/README.md

@niscorp/signal

Universal LLM abstraction. Stateless, immutable, provider-agnostic. Structured output with Zod, tool calling, zero hard dependencies.

Install

pnpm add @niscorp/signal zod
# Plus the provider SDK you want to use:
pnpm add openai  # for OpenAI, Groq, OpenRouter, or any OpenAI-compatible API

Quick Example

import { createSignal } from '@niscorp/signal';
import { z } from 'zod';

const signal = createSignal('groq');

// Simple text completion
const { response } = await signal.complete('What is 2+2?');

// Structured output — response is typed
const { response: user } = await signal
  .schema(z.object({ name: z.string(), age: z.number() }))
  .complete('Extract: Alice is 30 years old');
// user.name === 'Alice', user.age === 30

Documentation

  • DOCS.md — Full API reference with examples
  • DESIGN.md — Architecture, design decisions, and trade-offs

API

Every builder method returns a new immutable instance. complete() and stream() run the full Signal pipeline (schema, retries, tool loop). step() and stepStream() are the low-level primitives — one adapter call, no auto tool execution — used by orchestrators like @niscorp/cortex that own their own tool loop.

// Create
const signal = createSignal('groq');               // known provider
const signal = createSignal('groq', { apiKey, model, systemPrompt, retries });

// Configure (each returns a new instance)
signal.model('qwen/qwen3.8-27b')
signal.systemPrompt('You are helpful.')
signal.schema(zodSchema)           // typed structured output
signal.tools([myTool])             // tool calling
signal.history(messages)           // multi-turn
signal.retries(3)                  // validation retries
signal.options({ temperature: 0 }) // sampling / generation options
signal.apiKey(key)                 // override the env key
signal.describe()                  // provider, model, capabilities — from the registry
signal.onRetry(handler)            // retry hook
signal.onToolCall(handler)         // tool call hook

// Execute — high level
const { response, history, meta } = await signal.complete('user message');

// Execute — streaming
for await (const event of signal.stream('user message')) {
  if (event.type === 'text') process.stdout.write(event.text);
  if (event.type === 'done') console.log(event.meta.usage);
}

// Execute — low level (single adapter call, no tool execution)
const { content, toolCalls, usage, finishReason } = await signal.step({
  messages: [...], tools: [{ name, description, parameters }],
});

// Execute — streaming low level (symmetric with step())
for await (const event of signal.stepStream({ messages, tools })) {
  if (event.type === 'text') process.stdout.write(event.text);
  if (event.type === 'done') {
    // event.result is the aggregated StepResult — same shape as step()
  }
}

// Execute — embedding (separate client, embedding model)
const embedder = createSignal('openai').model('text-embedding-3-small');
const vector = await embedder.embed('wireless headphones');             // number[]
const vectors = await embedder.embed(['shoes', 'boots', 'hat']);       // number[][]
const small = await embedder.embed('text', { dimensions: 256 });       // truncated

// Execute — decisions (typed questions about a state; no text is generated)
const jev = createSignal('typesafe');
const { decisions } = await jev.decide({
  state: { message: 'I was charged twice' },
  questions: {
    intent: { type: 'choice', instructions: 'What do they want?', criteria: { refund: 'Money back', other: 'Anything else' } },
    needsHuman: { type: 'noul', instructions: 'Does this need a person?' },
  },
});
decisions.intent.choice;                                               // 'refund' | 'other'

Providers

Provider String Kind SDK Embedding
Groq 'groq' chat openai No
OpenAI 'openai' chat openai Yes
OpenRouter 'openrouter' chat openai No
Anthropic 'anthropic' chat stub (use OpenRouter) No
Google 'google' chat stub (use OpenRouter) No
TypeSafe (Jev) 'typesafe' decisions none — one fetch —

A chat provider has every verb; decide() runs there by emulation, uncalibrated. A decision provider has decide() only.

API keys are read from environment variables (GROQ_API_KEY, OPENAI_API_KEY, TYPESAFE_API_KEY, etc.) or passed via .apiKey() / options.

License

Apache-2.0