ideation-loop is a local, stateful ideation runner built on the OpenAI API.
It keeps one exploration in a JSON file, proposes follow-up questions, drafts
structured answers, scores them, deduplicates weak repeats, and optionally
renders a lineage graph.
Use it when you want:
- an exploration that improves over multiple runs instead of one-shot prompting
- reusable structured output in JSON instead of a pile of chat transcripts
- something easy to inspect with
jqor feed into another AI agent
It is not built for:
- fact-verified research archives
- polished narrative reports
- retrieval over large document corpora
This is a script-first local uv app, not a published PyPI package. Git tags
mark repository snapshots, not PyPI releases.
Needs Python 3.9+, uv, and an OpenAI API key.
Fastest path:
download the current main branch zip,
then extract main.py.
curl -L -o ideation-loop-main.zip \
https://github.com/bnomei/ideation-loop/archive/refs/heads/main.zip
unzip -j ideation-loop-main.zip '*/main.py'
export OPENAI_API_KEY="your_openai_api_key_here"
uv run \
--with "openai>=2.30,<3" \
--with "pydantic>=2.12,<3" \
--with "matplotlib>=3.9,<4" \
--with "networkx>=3.2,<4" \
python -u main.pyRepo workflow:
uv sync --frozen
cp .env.example .env
uv run --env-file .env python -u main.pyIf you do not already have uv:
curl -LsSf https://astral.sh/uv/install.sh | shuv sync --frozen is the supported install path for this repository. The
checked-in uv.lock is part of the supported local and CI environment.
After a normal run you get:
world_state.json: the canonical output for the explorationlineage_graph.png: an optional visualization of artifact relationships- stdout progress lines showing accepted or rejected artifacts
For non-default runs:
- use
--state-file runs/foo.jsonto keep live work in the git-ignoredruns/directory - if
--graph-fileis omitted, the graph defaults toruns/foo_lineage_graph.png - treat
states/*.jsonas checked-in examples, not your main scratch space
The state file is the source of truth. Stdout is just progress logging, and the PNG is only a visualization layer.
Typical shape of the saved JSON:
{
"iteration": 10,
"artifacts": [
{
"question": {
"text": "What mechanism would make this idea robust outside lab conditions?",
"mode": "exploit"
},
"answer": "A stronger version would rely on ...",
"fate": 0.81,
"claims": [
{
"text": "Robustness depends on explicit verification steps."
}
]
}
],
"registry": [
{
"name": "verification scaffold",
"meaning": "A lightweight structure that helps a user check an answer."
}
]
}uv run --env-file .env python -u main.py \
--state-file runs/cross-lingual-cot-trust.json \
--iters 3Use the same state file again to continue the same exploration. If that state already has a stored seed, it keeps using it.
uv run --env-file .env python -u main.py \
--state-file runs/medical-calibration.json \
--seed-topic "Trust calibration for medical advice without visible CoT" \
--seed-goal "Prefer answer-level summaries and verification prompts over raw reasoning traces" \
--seed-include "non-expert users" \
--seed-include "verification scaffolds" \
--seed-avoid "broad AGI safety"uv run --env-file .env python -u main.py \
--state-file runs/cross-lingual-cot-trust.json \
--seed-file seed.example.jsonWeb search is optional and off by default. When enabled, only question
generation and artifact drafting may use OpenAI's built-in web_search tool.
The judging and scoring passes still operate against the current state.
uv run --env-file .env python -u main.py \
--state-file runs/current-topic.json \
--web-search \
--iters 3Restrict web search to specific domains:
uv run --env-file .env python -u main.py \
--state-file runs/current-topic.json \
--web-search \
--web-search-domain www.anthropic.com \
--web-search-domain openai.com \
--iters 3Rescore an older state with the current scoring logic without generating new artifacts:
uv run --env-file .env python -u main.py \
--state-file runs/cross-lingual-cot-trust.json \
--rejudge-existing \
--iters 0Rescore first, then continue:
uv run --env-file .env python -u main.py \
--state-file runs/cross-lingual-cot-trust.json \
--rejudge-existing \
--iters 3uv run --env-file .env python -u main.py \
--state-file runs/cross-lingual-cot-trust.json \
--seed-file seed.example.json \
--iters 0By default, the runner refuses to retarget a populated unseeded state. Use
--replace-seed only when you want to intentionally repurpose an existing
state file.
uv run --env-file .env python -u main.py \
--state-file runs/cross-lingual-cot-trust.json \
--seed-file seed.example.json \
--replace-seedBecause the canonical output is JSON, jq is the easiest way to inspect runs
from the shell. It is also the best handoff format for AI agents: instead of
feeding an agent the full state file, slice out the parts it actually needs.
Basic summary:
jq '{
iteration,
artifact_count: (.artifacts | length),
registry_count: (.registry | length),
seed_topic: (.seed.topic // null)
}' world_state.jsonTop artifacts by fate:
jq '[
.artifacts
| sort_by(.fate)
| reverse
| .[:5]
| .[]
| {
id,
mode: .question.mode,
question: .question.text,
fate,
novelty,
grounding_score,
overclaim_penalty
}
]' world_state.jsonCompact JSON slice for an AI agent:
jq -c '{
iteration,
seed,
top_artifacts: (
.artifacts
| sort_by(.fate)
| reverse
| .[:8]
| map({
id,
question: .question.text,
answer,
fate,
evidence_type,
evidence_strength,
claims: [.claims[].text],
open_questions
})
),
registry: (
.registry
| map({
id,
name,
meaning,
status
})
)
}' world_state.jsonUse jq -c when you want one compact JSON object that another tool or agent
can consume directly.
High-level flow:
state.json
-> summarize strongest artifacts and registry concepts
-> generate a small batch of exploit and explore questions
-> draft one structured artifact per question
-> score usefulness, reuse, grounding, overclaim, and dedup
-> keep survivors and drop near-clones
-> write updated state.json
-> optionally render lineage_graph.png
The loop is optimized for disciplined ideation, not truth-verification. It is best used to generate better frames, mechanisms, scenario ideas, and next-check questions rather than to build a definitive research archive.
By default:
- the script runs
10iterations - each iteration generates
3new questions - the default state file is
world_state.json - the default graph file is
lineage_graph.png
A world state is the JSON file that stores one running exploration.
Core fields:
iteration: current iteration countartifacts: surviving question and answer records plus scores and metadataregistry: reusable definitions discovered during the runseed: optional topic guidance stored with the state
Important behavior:
- a world state is a working set, not a permanent archive of everything ever generated
- the loop prunes artifacts over time, so weaker artifacts may disappear
- if a state has a stored seed, future runs of that same state continue using it
explorequestions open new frames, whileexploitquestions deepen or stress-test the strongest current line
Newer runs may also persist extra epistemic fields such as evidence type, evidence strength, assumptions, a competing hypothesis, and a main failure case. Older state files without these fields still load normally.
Show the full help:
uv run --env-file .env python -u main.py --helpThe most important flags are:
--iters ITERSNumber of iterations to run. Default:10.--state-file PATHLoad from and save to a different state file instead ofworld_state.json.--seed-file PATHLoad a JSON seed document and persist it into the selected state.--seed-topic TEXTProvide a seed topic inline from the CLI.--rejudge-existingRe-score existing artifacts in the selected state before running iterations.--web-searchAllow question and artifact generation to use OpenAI's built-in web search tool.--web-search-domain TEXTRestrict built-in web search to specific domains. Requires--web-search.--no-graphSkip graph rendering.--quietSuppress routine progress logs and keep only errors plus final save paths.--redact-outputReplace question text in progress logs and graph labels with stable redacted ids.--gitCommit state-file updates after each save.
A seed file is JSON with this shape:
{
"topic": "Trust calibration for medical advice without visible chain-of-thought",
"goal": "Explore brief answer-level summaries and verification prompts as safer alternatives to visible reasoning traces.",
"include": ["non-expert users", "verification scaffolds", "uncertainty cues"],
"avoid": ["broad AGI safety", "general philosophy of mind"],
"seed_questions": [
"Do short contrastive summaries calibrate trust better than full chain-of-thought?",
"Which verification prompts actually increase external checking in safety-relevant tasks?"
],
"seed_definitions": [
{
"name": "verification scaffold",
"meaning": "A lightweight interface element that helps a user check an answer with an external calculation, source, or checklist."
}
]
}Only topic is strictly required. The rest are optional guidance fields.
Runtime model configuration comes from environment variables:
OPENAI_API_KEYRequired. Used for all OpenAI API calls.OPENAI_MODELOptional. Defaults togpt-5.1.OPENAI_EMBED_MODELOptional. Defaults totext-embedding-3-small.
The script does not load .env by itself. Use uv run --env-file .env ... if
you want .env values injected into the process. Startup validates this
configuration early and fails before the first model call if required values
are missing or empty.
Each question currently triggers multiple model calls:
- one structured draft generation
- two positive scoring passes
- one adversarial scoring pass
- embedding calls for deduplication when needed
That means longer runs can consume a meaningful number of tokens and API requests.
Also note:
- the loop keeps an in-memory embedding cache for the current process
- the script rewrites the full state JSON on each save
- graph rendering imports
matplotlibandnetworkx, so first runs may spend a moment building local caches - use
--quietin shared shells or CI when you do not want routine prompt text echoed to stdout - use
--redact-outputwhen you want logs and graph labels to stay traceable without exposing question text
- use
python -uif you want live progress lines while the loop is running - a seed is stored inside the selected state file, so you do not need to pass it again when continuing that exploration
- the default behavior is intentionally conservative: an existing
world_state.jsonis not reseeded unless you explicitly opt in with--replace-seed - treat
pyproject.tomlas the source of truth for the app version - treat Git tags such as
v0.1.1as repository snapshots, not PyPI releases