@niscorp/signal 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @niscorp/signal
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - b67a125: Documentation only: each package's README, reference and design docs checked against its source and corrected — install lines and peers, signatures, defaults, status codes, licenses (loom, signal: Apache-2.0), and API that existed but was not documented.
package/DESIGN.md CHANGED
@@ -62,8 +62,7 @@ sides is two changes.
62
62
  - **The router owns classification.** A response is `tool_calls`
63
63
  (declared tool), `output` (anything whose repaired value passes the
64
64
  acceptance gate — exit-tool args and pseudo-tool args included), or
65
- `failed` with evidence (+ `attempted` when a parsed candidate existed
66
- but failed the gate; + `truncated` when bytes end mid-structure).
65
+ `failed` with evidence (+ `truncated` when bytes end mid-structure).
67
66
  Callers switch on the outcome; they never parse strings.
68
67
  - **Rejections are arrivals.** A provider 400 that carries the model's
69
68
  attempt is recovered by an error-hook strategy and routed
package/DOCS.md CHANGED
@@ -8,7 +8,7 @@ Complete reference for `@niscorp/signal`.
8
8
 
9
9
  ### Stateless Immutable Builder
10
10
 
11
- Signal uses an immutable builder pattern. Every configuration method returns a **new instance** — the original is never modified. `complete()` is the only method that executes.
11
+ Signal uses an immutable builder pattern. Every configuration method returns a **new instance** — the original is never modified. Nothing is sent until an execution method (`complete()`, `stream()`, `step()`, `stepStream()`, `embed()`, `decide()`) is called.
12
12
 
13
13
  ```typescript
14
14
  const base = createSignal('groq').systemPrompt('You are helpful.');
@@ -35,6 +35,9 @@ Each provider has a capability profile that Signal uses to pick the right strate
35
35
  - **`nativeTools`** — Can the provider do tool calling natively?
36
36
  - **`nativeJsonSchema`** — Can it enforce a JSON Schema on the response?
37
37
  - **`nativeJsonMode`** — Can it guarantee JSON output?
38
+ - **`toolsWithStructuredOutput`** — Is `response_format` accepted together with `tools` in one request?
39
+ - **`validatesToolArgs`** — Does the endpoint validate tool-call arguments server-side and reject the request on a mismatch?
40
+ - **`manglesNestedToolArgs`** — Does the model corrupt nested tool-call arguments?
38
41
  - **`multimodal`** — Does it accept image content?
39
42
  - **`supportsEmbedding`** — Can it convert text to vectors?
40
43
 
@@ -50,7 +53,18 @@ You don't need to think about this unless you're overriding defaults.
50
53
  const signal = createSignal('groq');
51
54
  ```
52
55
 
53
- Reads `GROQ_API_KEY` from environment automatically. Known providers: `'groq'`, `'openai'`, `'openrouter'`, `'anthropic'`, `'google'`.
56
+ Reads `GROQ_API_KEY` from environment automatically. Known providers: `'groq'`, `'openai'`, `'openrouter'`, `'anthropic'`, `'google'`, `'typesafe'`.
57
+
58
+ | Provider | Env var | Default model |
59
+ |----------|---------|---------------|
60
+ | `'groq'` | `GROQ_API_KEY` | `qwen/qwen3.8-27b` |
61
+ | `'openai'` | `OPENAI_API_KEY` | `gpt-4o` |
62
+ | `'openrouter'` | `OPENROUTER_API_KEY` | `openai/gpt-4o` |
63
+ | `'anthropic'` | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514` |
64
+ | `'google'` | `GOOGLE_API_KEY` | `gemini-2.0-flash` |
65
+ | `'typesafe'` | `TYPESAFE_API_KEY` | `jev-latest` |
66
+
67
+ `'anthropic'` and `'google'` are registry rows whose adapters are not implemented yet: the client builds and `describe()` answers, but every execution method throws `E_PROVIDER_ERROR` (`E_MISSING_API_KEY` first when no key is set; `count()`, which never reaches the adapter, still works). Reach those models through `'openrouter'`.
54
68
 
55
69
  ### Known Provider with Options
56
70
 
@@ -73,7 +87,7 @@ const signal = createSignal({
73
87
  });
74
88
  ```
75
89
 
76
- Any OpenAI-compatible API works.
90
+ Any OpenAI-compatible API works. A custom provider is outside the registry, so it states what it does itself — `capabilities` (a `Partial<Capabilities>`; unstated fields are `false`) and, optionally, `wire` strategy ids. `adapter` defaults to `'openai-compatible'`.
77
91
 
78
92
  ---
79
93
 
@@ -115,7 +129,7 @@ signal.history(previousMessages)
115
129
  ```
116
130
 
117
131
  ### `.retries(count)`
118
- Set max validation retries (default: 2). When a response fails Zod validation, Signal feeds the error back to the model and tries again.
132
+ Set max validation retries (default: 2). When a response fails Zod validation, Signal feeds the error back to the model and tries again; once the retries are spent the call fails with `E_VALIDATION_FAILED`.
119
133
  ```typescript
120
134
  signal.retries(3)
121
135
  ```
@@ -127,7 +141,7 @@ signal.apiKey('gsk_...')
127
141
  ```
128
142
 
129
143
  ### `.options(opts)`
130
- Set LLM options (temperature, maxTokens, etc.). These are rarely needed.
144
+ Set LLM options: `temperature`, `maxTokens`, `topP`, `stopSequences`, `seed`, `signal` (an `AbortSignal`), `reasoningEffort`. Merged over any options already set. These are rarely needed.
131
145
  ```typescript
132
146
  signal.options({ temperature: 0 })
133
147
  ```
@@ -144,10 +158,11 @@ createSignal('groq').describe()
144
158
  // { provider: 'groq', model: 'qwen/qwen3.8-27b', kind: 'chat', modelKnown: true, capabilities: {…} }
145
159
  ```
146
160
  A `reasoningEffort` the model's row does not list is refused when the
147
- client is built (`SignalError`), not by a provider 400 mid-run.
161
+ client is built (`SignalError`, `E_VALIDATION_FAILED`), not by a provider 400
162
+ mid-run. An unmeasured model is not checked.
148
163
 
149
164
  ### `.onRetry(handler)`
150
- Hook called on each validation retry.
165
+ Hook called on each validation failure, including the last one that exhausts the retries.
151
166
  ```typescript
152
167
  signal.onRetry((error, attempt) => console.log(`Retry ${attempt}:`, error.message))
153
168
  ```
@@ -179,9 +194,39 @@ await signal.complete([
179
194
  ]);
180
195
  ```
181
196
 
182
- ### `.stream(input)`
197
+ ### `.stream(input, options?)`
198
+
199
+ The same run as `complete()`, delivered as it happens. Returns `AsyncIterable<StreamEvent<T>>`; `options.signal` (an `AbortSignal`) aborts it.
200
+
201
+ ```typescript
202
+ for await (const event of signal.stream('Tell me a story')) {
203
+ if (event.type === 'text') process.stdout.write(event.text);
204
+ if (event.type === 'done') console.log(event.meta.usage);
205
+ }
206
+ ```
207
+
208
+ ```typescript
209
+ type StreamEvent<T> =
210
+ | { type: 'text'; text: string }
211
+ | { type: 'tool_start'; name: string; args: unknown }
212
+ | { type: 'tool_end'; name: string; result: unknown }
213
+ | { type: 'retry'; reason: string; attempt: number }
214
+ | { type: 'error'; error: Error; recovered: boolean }
215
+ | { type: 'done'; response: T; history: Message[]; meta: SignalMeta };
216
+ ```
217
+
218
+ Schema validation happens at the end of the stream; when the retries are spent the stream ends with an `error` event (which `complete()` throws).
219
+
220
+ ### `.step(request)` / `.stepStream(request, options?)`
183
221
 
184
- Not yet implemented. Will return `AsyncIterable<StreamEvent<T>>`.
222
+ The low-level primitives `complete()` and `stream()` are built on: one model call, no tool execution, no schema retries. The caller owns the loop. `step()` takes a `StepRequest` (`messages`, `tools`, `toolChoice`, `output`, `responseFormat`, `options`) and returns a `StepResult` (`content`, `toolCalls`, `usage`, `finishReason`, `raw`, and — when the request carried `output.accept` — the routed `outcome` and `wire` report). `stepStream()` yields `text`, `reasoning` and `tool_call_delta` events and a final `done` carrying the `StepResult`.
223
+
224
+ ### `.count(input)`
225
+
226
+ Rough token count for a string or a message list. A heuristic (about 4 characters per token), not a tokenizer.
227
+ ```typescript
228
+ const tokens = await signal.count('How long is this?');
229
+ ```
185
230
 
186
231
  ---
187
232
 
@@ -209,8 +254,8 @@ type SignalMeta = {
209
254
  retries: number; // validation retries that occurred
210
255
  toolCalls: ToolCallRecord[]; // tools called, args, results, timing
211
256
  provider: {
212
- raw: unknown; // raw API response for debugging
213
- errors: ProviderError[]; // all errors including recovered ones
257
+ raw: unknown; // raw API response for debugging (an array when the run took several calls)
258
+ errors: ProviderError[]; // currently always empty
214
259
  };
215
260
  };
216
261
  ```
@@ -237,15 +282,15 @@ const { response } = await createSignal('groq')
237
282
 
238
283
  ### How It Works
239
284
 
240
- 1. Zod schema is converted to JSON Schema via `z.toJSONSchema()`
241
- 2. Based on provider capabilities:
242
- - `nativeJsonSchema` → sends JSON Schema as `response_format`
243
- - `nativeJsonMode` → sends `response_format: json_object` + schema in system prompt
244
- - Neither → schema in system prompt only
245
- 3. Response is parsed as JSON
285
+ 1. Zod schema is converted to JSON Schema (draft-07)
286
+ 2. Based on capabilities:
287
+ - `nativeJsonSchema` (and, when tools are set, `toolsWithStructuredOutput`) → sends JSON Schema as `response_format`
288
+ - otherwise `nativeJsonMode` and no tools → sends `response_format: json_object` + schema in a system message
289
+ - otherwise → schema in a system message only
290
+ 3. The response is normalized by the wire layer (JSON extraction and repairs, each counted only if the result passes the schema)
246
291
  4. Validated with `schema.safeParse()`
247
- 5. On failure: Zod errors are sent back to the model as a correction message
248
- 6. Retries up to `retries` times (default: 2)
292
+ 5. On failure: the evidence is sent back to the model as a correction message
293
+ 6. Retries up to `retries` times (default: 2), then fails with `E_VALIDATION_FAILED`
249
294
  7. Returns typed, validated result
250
295
 
251
296
  ---
@@ -271,7 +316,7 @@ const searchTool = defineTool({
271
316
  });
272
317
  ```
273
318
 
274
- The Zod `input` schema validates the model's arguments before `execute` runs. The return value is stringified and sent back to the model.
319
+ The Zod `input` schema validates the model's arguments before `execute` runs. The return value is stringified and sent back to the model. A call to an unknown tool, arguments that fail the schema, and an `execute` that throws do not fail the run: each goes back to the model as an `error: …` tool result (and is recorded in `meta.toolCalls`).
275
320
 
276
321
  ### Using Tools
277
322
 
@@ -287,17 +332,15 @@ const { response, meta } = await createSignal('groq')
287
332
 
288
333
  ### How Tool Calling Works
289
334
 
290
- Signal picks a strategy based on provider capabilities:
291
-
292
- **Native tools** (OpenAI) — Uses the provider's native `tools` parameter and `tool_calls` response. The model decides when to call tools.
293
-
294
- **Unified schema** (Groq, others) — Tool descriptions are injected into the system prompt. The response format includes a discriminator field (`_action: "call" | "respond"`). Signal handles the loop:
335
+ Tools use the provider's native `tools` parameter and `tool_calls` response; the model decides when to call them. A provider without `nativeTools` cannot take tools — the call throws `E_PROVIDER_ERROR`. Signal handles the loop:
295
336
 
296
- 1. Model responds with `_action: "call"` + tool name + args
337
+ 1. Model responds with tool calls
297
338
  2. Signal validates args with Zod, executes the tool
298
339
  3. Tool result is sent back to the model
299
- 4. Model responds with `_action: "respond"` + final answer
300
- 5. Final answer is validated against the output schema
340
+ 4. Model responds with the final answer
341
+ 5. With a schema set, the final answer is validated against it
342
+
343
+ Where the model cannot combine `response_format` with tools (`toolsWithStructuredOutput: false`, e.g. Groq), the schema rides a system message instead and is validated client-side. A run that is still calling tools after 10 tool turns (plus the retry budget) fails with `E_MAX_ITERATIONS`.
301
344
 
302
345
  ---
303
346
 
@@ -348,6 +391,14 @@ const small = await embedder.embed('text', { dimensions: 256 });
348
391
  // small: number[256]
349
392
  ```
350
393
 
394
+ ### Usage
395
+
396
+ `embed()` returns vectors only. To read what the call cost, pass `onUsage` — called after a successful call with the provider's usage and the model that resolved:
397
+
398
+ ```typescript
399
+ await embedder.embed('text', { onUsage: (usage, model) => meter(model, usage.totalTokens) });
400
+ ```
401
+
351
402
  ### Similarity
352
403
 
353
404
  Cosine similarity between embedding vectors measures semantic relatedness:
@@ -468,12 +519,10 @@ All errors are `SignalError` instances with a `.code` and optional `.context`:
468
519
  | `E_MISSING_API_KEY` | No API key found (env or explicit) |
469
520
  | `E_MISSING_MODEL` | No model specified for custom provider |
470
521
  | `E_MISSING_SDK` | Provider SDK not installed |
471
- | `E_VALIDATION_FAILED` | Response failed Zod validation after all retries |
472
- | `E_MAX_RETRIES` | Retry loop exhausted |
473
- | `E_PROVIDER_ERROR` | Provider API error (rate limit, server error, etc.) |
474
- | `E_TOOL_NOT_FOUND` | Model called an unknown tool |
475
- | `E_TOOL_EXECUTION` | Tool execute() threw |
476
- | `E_TOOL_VALIDATION` | Tool args failed Zod validation |
522
+ | `E_VALIDATION_FAILED` | Response failed Zod validation after all retries; a `decide()` answer failed its gate; a `reasoningEffort` the model does not take |
523
+ | `E_MAX_ITERATIONS` | The tool loop did not finish within its turn limit |
524
+ | `E_PROVIDER_ERROR` | Provider API error (rate limit, server error, etc.); tools or `embed()` on a provider without support; an adapter that is not implemented |
525
+ | `E_MAX_RETRIES`, `E_TOOL_NOT_FOUND`, `E_TOOL_EXECUTION`, `E_TOOL_VALIDATION` | Declared in `ErrorCode`, not currently thrown — tool failures go back to the model as tool results, and exhausted retries are `E_VALIDATION_FAILED` |
477
526
  | `E_VERB_NOT_SUPPORTED` | A chat verb was called on a decision provider |
478
527
 
479
528
  ```typescript
@@ -494,7 +543,7 @@ try {
494
543
 
495
544
  Groq is fast and cheap but has quirks:
496
545
 
497
- - **No native tools + structured output together.** Signal uses the unified schema strategy automatically.
546
+ - **No native tools + structured output together.** With tools set, Signal sends no `response_format` there; the schema rides a system message and is validated client-side.
498
547
  - **`json_validate_failed` errors.** Groq sometimes returns the model's failed output in the error. Signal extracts and recovers from this.
499
548
  - **Model-dependent capabilities.** What each Groq model does (json_schema, nested tool args, reasoning efforts) is its own row in the model registry, measured by the probe.
500
549
  - **Tool args are validated server-side.** So `respond` params would enforce nothing on Groq, and `auto` resolves every agent there to `emit`.
package/README.md CHANGED
@@ -53,6 +53,8 @@ signal.schema(zodSchema) // typed structured output
53
53
  signal.tools([myTool]) // tool calling
54
54
  signal.history(messages) // multi-turn
55
55
  signal.retries(3) // validation retries
56
+ signal.options({ temperature: 0 }) // sampling / generation options
57
+ signal.apiKey(key) // override the env key
56
58
  signal.describe() // provider, model, capabilities — from the registry
57
59
  signal.onRetry(handler) // retry hook
58
60
  signal.onToolCall(handler) // tool call hook
@@ -114,4 +116,4 @@ API keys are read from environment variables (`GROQ_API_KEY`, `OPENAI_API_KEY`,
114
116
 
115
117
  ## License
116
118
 
117
- MIT
119
+ Apache-2.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@niscorp/signal",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Universal LLM abstraction — stateless, immutable, provider-agnostic",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",