@polyweave/creativity (1.0.5)
Installation
@polyweave:registry=https://hub.kl1.tenere.ai/api/packages/Polyweave/npm/npm install @polyweave/creativity@1.0.5"@polyweave/creativity": "1.0.5"About this package
@polyweave/creativity
Diversity-gated creative ideation pipeline that decorrelates prompts with @polyweave/sampler, fans them out through an injected LLM adapter, filters results by cosine-distance diversity, and iteratively anneals toward genuinely different territory via LLM-synthesized "bans" on pruned patterns.
Published as @polyweave/creativity.
Synopsis
import { createCreativityTool, filterDiverseIdeas, textToPseudoEmbedding } from '@polyweave/creativity';
// Standalone diversity filtering
const ideas = [
{ text: 'Solar panel windows for buildings' },
{ text: 'Photovoltaic glass facades' },
{ text: 'Tidal energy kites' },
{ text: 'Underwater current turbines' }
];
const result = filterDiverseIdeas(ideas, 0.35);
console.log(result.kept.length); // ~2
// Tool-mode: full pipeline with LLM generation + embedding-based filtering
const tool = createCreativityTool({
llm: myLlmAdapter,
embeddings: myEmbedAdapter,
diversityThreshold: 0.35,
defaultCount: 8
});
tool.source.pipe(mySink);
tool.sink.write({ type: 'TOOL_CALL', content: { arguments: { topic: 'sustainable energy' } } });
// Stream-level diversity transforms (no tool wrapping)
import { createDiversityTransform, createDiversityReducer } from '@polyweave/creativity';
const fanout = createDiversityTransform(llmAdapter, { count: 6, temperature: 0.9 });
const reducer = createDiversityReducer({ threshold: 0.3 });
source.pipe(fanout).pipe(reducer).pipe(outputSink);
Install
npm install @polyweave/creativity
Why
LLM responses cluster around familiar patterns — low-temperature sampling converges and high-temperature sampling produces gibberish. This package solves the "diversity problem" without sacrificing coherence: it decorrelates prompts to produce structurally varied inputs, fans each through the LLM, then prunes near-duplicates by embedding distance. After pruning, it asks the LLM to analyze the rejected outputs to synthesize "bans" — themes and patterns to avoid — which get injected into the next round's prompt. The result is a self-annealing creativity loop that pushes each iteration into genuinely different territory.
API
createCreativityTool(options)
Creates a Polyweave frame duplex tool. Send TOOL_CALL frames; receive PROGRESS, TOOL_RESULT, ERROR, and END frames in response.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
options.llm |
IFrameDuplex |
No | null |
LLM adapter { source, sink } for text generation. Must implement the Polyweave frame protocol. |
options.embeddings |
IFrameDuplex |
No | null |
Embedding adapter { source, sink } for cosine-distance filtering. Falls back to trigram pseudo-embeddings when omitted. |
options.diversityThreshold |
number |
No | 0.35 |
Cosine distance threshold (0–1). Items closer than this value are pruned. |
options.defaultCount |
number |
No | 8 |
Number of decorrelated variants generated per round. Clamped to 2–50. |
options.maxIterations |
number |
No | 1 |
Number of diversity annealing rounds. Clamped to 1–5. |
options.llmOptions |
object |
No | {} |
Generation overrides: { temperature, maxTokens }. |
options.embedOptions |
object |
No | {} |
Embedding overrides: { model }. |
options.tickDriven |
boolean |
No | false |
If true, each round advances only on receiving a TICK frame. |
options.generate |
boolean |
No | true |
If false, skips LLM generation and labels variants with their sampler seed strings. |
Returns: { source: IFrameSource, sink: IFrameSink, _toolName: 'creativity' }
Input frames (sink receives):
| Frame type | Purpose |
|---|---|
TOOL_CALL |
Initiates creative generation. content.arguments must contain topic (string, required). Optional: context, count, diversityThreshold, maxIterations. |
DESCRIBE |
Returns the tool's schema and capabilities via a DESCRIPTION frame. |
CANCEL / ABORT |
Resets internal state. |
TICK |
Advances one round (only when tickDriven is true). |
END / QUIT |
Ignored. |
Output frames (source emits):
| Frame type | Purpose |
|---|---|
PROGRESS |
Emitted at each phase: starting, iteration_start, generation_complete, filter_complete, bans_synthesized, done. |
TOOL_RESULT |
Emitted on completion. Contains success, topic, ideas (kept), pruned, pairs, keptCount, prunedCount, totalGenerated, iterations, diversityThreshold, embeddingMode, generateMode, banNotes, summary. |
ERROR |
Emitted when LLM generation fails, embedding fails, or topic is missing. Content contains message. |
END |
Emitted after TOOL_RESULT. |
DESCRIPTION |
Response to DESCRIBE. |
Error conditions:
| Condition | Error message |
|---|---|
topic is missing, empty, or whitespace-only in TOOL_CALL args |
"topic is required" (emitted as ERROR + END, state remains null) |
| LLM generation fails (any variant in a round) | err.message || "Generation failed" (emitted as ERROR, state reset to null) |
| Embedding adapter call fails | err.message || "Embedding failed" (emitted as ERROR, state reset to null) |
filterDiverseIdeas(ideas, threshold)
Synchronous diversity filter. Computes trigram pseudo-embeddings for each idea (unless idea.embedding is already an array), then iteratively keeps ideas whose cosine distance from all previously kept ideas exceeds threshold.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
ideas |
Array |
Yes | — | Array of idea objects. Each may have text, idea, proposal, or be a raw string. May include an optional embedding (number array). |
threshold |
number |
No | 0.35 |
Cosine distance threshold (0–1). |
Returns: { kept: Idea[], pruned: Idea[], pairs: Array<{a: number, b: number, distance: number}>, keptCount: number, prunedCount: number }
If ideas is not an array or has ≤ 1 element, returns { kept: ideas \|\| [], pruned: [], pairs: [] } without keptCount/prunedCount (only the non-array or ≤1 guard path omits count fields — the main code path always includes them).
filterDiverseIdeasAsync(ideas, threshold, getEmbedding, callback)
Async version of filterDiverseIdeas. Embeds each idea via the injected getEmbedding function, then applies the same greedy cosine-distance filter.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
ideas |
Array |
Yes | — | Same as filterDiverseIdeas. |
threshold |
number |
No | 0.35 |
Cosine distance threshold. |
getEmbedding |
(text: string, callback: (err, vector) => void) => void |
Yes | — | Async embedding function. Must call callback(err, vector) with a numeric array or null. |
callback |
(err, result) => void |
Yes | — | Node-style callback. On success: callback(null, { kept, pruned, pairs, keptCount, prunedCount }). |
Error conditions: If getEmbedding calls back with an error, that error is forwarded immediately via callback(err, null). Pending embeddings are cancelled.
textToPseudoEmbedding(text)
Generates a 128-dimensional unit-normalized trigram frequency vector. Used as a fallback when no embedding adapter is available.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
text |
any |
No | — | Input text. Coerced to string, trimmed, and lowercased. |
Returns: number[] — length 128 unit vector. Returns [] if text is empty after normalization.
Does not throw.
createDiversityTransform(adapter, options)
Fan-out stream transform implementing the IFrameTransform { source, sink } interface. Each input TEXT frame is decorrelated into N sampler variants, each is sent through the injected LLM adapter, and each result is emitted as a TEXT frame tagged with its sampler nonce (content.tag).
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
adapter |
IFrameDuplex |
Yes | — | LLM adapter { source, sink } used for each variant. |
options.count |
number |
No | 4 |
Variants per input TEXT frame. Clamped to 1–32. |
options.temperature |
number |
No | undefined |
Temperature override for each adapter call. |
options.maxTokens |
number |
No | undefined |
max_tokens override for each adapter call. |
options.model |
string |
No | undefined |
Model override for each adapter call. |
Returns: IFrameTransform { source, sink }
Frame protocol:
| Input → | Output → |
|---|---|
TEXT |
N × TEXT (one per variant, with content.tag containing id, uuids, words, index, temperature) |
non-TEXT frames |
Passthrough |
END / QUIT |
Deferred until all in-flight variant calls complete |
ERROR variant |
ERROR frame (with content.tag) |
Error conditions: None thrown. Per-variant errors produce ERROR frames rather than crashing the pipeline.
createDiversityReducer(options)
Fan-in stream reducer implementing IFrameTransform { source, sink }. Buffers incoming tagged TEXT frames, applies embedding-distance diversity filtering, and emits only the diverse subset when END arrives.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
options.threshold |
number |
No | 0.35 |
Cosine distance threshold (0–1). |
options.strategy |
string |
No | "first" |
Selection strategy: "first" keeps first-seen (insertion order), "longest" processes longest texts first (sort before greedy filter). |
options.embeddingFn |
(text: string) => vector |
No | internal textToPseudoEmbedding |
Embedding function. Falls back to the same trigram pseudo-embedding algorithm. |
options.maxOutput |
number |
No | Infinity |
Maximum diverse items to emit. Clamped to ≥ 1. |
Returns: IFrameTransform { source, sink }
Frame protocol:
| Input → | Output → |
|---|---|
TEXT |
Buffered, not passed through |
END / QUIT |
Flushes buffer: emits diverse subset as TEXT frames then forwards END |
non-TEXT |
Passthrough |
| empty TEXT frames | Filtered (dropped) |
Tests
npm test
Runs node --test test/creativity.test.js test/rigor.test.js. Covering: tool creation, DESCRIBE frame emission, tick-driven mode, auto-run with generate-mode=label (pseudo-embedding only), diversity filtering of idea arrays, and empty/single-element edge cases in filterDiverseIdeas.
Requirements
- Node.js >= 22.0.0
- ESM only
- Peer dependencies:
@polyweave/core,@polyweave/sampler,@polyweave/self-improving— all must be installed alongside this package.
Caveats
- When no embedding adapter is injected, the trigram pseudo-embedding fallback is used. This is fast but semantically weak — it only detects near-literal similarity based on 3-character shingles. For meaningful semantic diversity filtering, inject a proper embedding adapter.
- The
llmadapter is used for both generation and ban synthesis. Ifoptions.generateisfalse, no LLM calls are made and variants are labeled with their sampler seed strings. Ban synthesis is also skipped whenoptions.generateisfalse. - All adapters must implement the Polyweave frame duplex protocol (
{ source, sink }). - The tool is stateful: a second
TOOL_CALLarriving while a pipeline is running will overwrite the in-flight state.
Dependencies
Dependencies
| ID | Version |
|---|---|
| @polyweave/core | * |
| @polyweave/errors | * |
| @polyweave/sampler | * |
| @polyweave/self-improving | * |
Development Dependencies
| ID | Version |
|---|---|
| @rigor/core | * |