Skip to content

Cosmonapse 0.1.12 - native tool calls, and the Neuron's prompt in Genesis

Latest

Choose a tag to compare

@AqibKhan26 AqibKhan26 released this 14 Aug 10:54
· 12 commits to main since this release

Released 2026-08-11 · Changelog · PyPI

0.1.11 shipped tool-call recognition as text scraping: an Axon's Neuron
answered, and a regex-and-fence parser went looking through the reply for
something that looked like a call. That worked for models that narrate JSON in
prose, and it silently failed for the one provider that doesn't - Anthropic
returns a tool call as a separate structured tool_use content block, never as
text, and the wrapper around it kept only the text. The claude dialect was
unreachable against the real API.
0.1.12 fixes that by reading structured
calls off the provider's raw response first, on every provider, and falling
back to text scraping only when there is nothing structured to read.

Alongside the fix: calls that used to vanish now don't. Multiple tool calls in
one reply, a call wrapped in chain-of-thought JSON, a truncated <tool_call>
tag, a zero-argument call sent as "arguments": "" - each of these previously
resolved to "no call found," and an agent that stops mid-task on a dropped
call reports success. This release also gives Genesis a way to edit the one
part of an LLM Neuron that never appeared in its config form - the system
prompt - and fixes a handful of real bugs: a Prism button that degraded the
Brain View on every click, a Genesis synapse that could hang with no crash and
no traceback, and a wheel that shipped a stale Genesis UI because nothing
built it at release time.

Native tool calls

extract_native_calls() reads a structured call straight off the raw
provider response instead of parsing the rendered text:

Provider shape Where it reads from
OpenAI, vLLM, TGI, Together, Groq, OpenRouter, Mistral, Azure choices[0].message.tool_calls
Anthropic content[] blocks with type == "tool_use"
Ollama message.tool_calls

The Anthropic path is the actual bug fix. _AnthropicNeuron returns text-only
in response; a tool_use block sat untouched in meta, which is exactly
where extract_native_calls now reads it from. Nothing about the Neuron
wrapper's output changed - Genesis and Prism see the same response field
they always did - only where the Axon looks for a call.

Recognition order flipped: structured calls are tried first on every Axon;
Neuron-specific text recognizers (Hermes ChatML, Claude-in-prose, Codex JSON)
now only run when nothing structured is there to read. tool_standard= still
picks the text dialect when one is needed, and is now inferred when not
given explicitly - from the wrapped provider (claude for an Anthropic
Neuron) or from the model name (hermes for qwen/nous/functionary-family
models, codex for gpt/llama/mistral/deepseek/gemma-family models). An
explicit tool_standard= always wins over inference.

Calls that were silently dropped now aren't

Every text parser gained an _all variant that returns every call in a reply,
in order, instead of the first one found:

  • A reply with more than one call used to return only the first. Now every
    call is recovered; by default only the first still runs (see
    parallel_tools below), but the rest show up in the observation instead of
    disappearing.
  • A call wrapped in chain-of-thought JSON - {"thoughts": "...", "name": "read", "arguments": {...}}, the shape several Llama/DeepSeek prompt
    templates produce - used to fail the exact-key match required for Codex
    detection. A widened guard now ignores a known set of narration keys
    (thoughts, reasoning, observation, id, call_id, type, index)
    around the two that matter.
  • A truncated <tool_call> tag - a reply cut off at max_tokens before
    the closing tag, or a served endpoint that corrupts one of the pair - used
    to degrade into "no call found," the worst available failure, since the
    agent then reports whatever half-formed text came back as a final answer.
    The Hermes parser now salvages a call from an unclosed or malformed tag.
  • A small model narrating before calling - I'll check the weather. {"name": "get_weather", "arguments": {...}} with no fence around the
    object - used to be dropped by a strict fence-or-nothing rule. It's now
    recovered, deliberately scoped to only the trailing object in a reply so a
    call mentioned mid-sentence (The call {"name": "bash", ...} would be dangerous.) is never mistaken for a real one.
  • A zero-argument call sent as "arguments": "" used to read as a parse
    failure instead of {}.
  • The same call recovered twice by overlapping scan passes (whole reply,
    fenced block, trailing object) is now de-duplicated before it's returned.

parallel_tools, and arguments checked before dispatch

Axon("assistant", neuron_fn, effectors=TOOLS, parallel_tools=True)

Off by default. With one call recognized, nothing changes. With more than one:
by default only the first runs and the rest are reported under
dropped_calls (name, args, call_id) rather than vanishing; with
parallel_tools=True all of them run, sequentially, in the order the model
returned them - sequential because a batch of tool calls is still an ordered
list of side effects, not an unordered set.

Every call - single or parallel - now has its arguments checked against the
Effector's declared schema before dispatch. A call that fails validation never
reaches the tool; the Axon returns an error the model can read and correct
from, instead of a stack trace from the far side of the wire or a tool that
half-runs on bad input.

cosmonapse.effector.schema - schemas and validation, as a module

from cosmonapse import tool_schema, ToolSchema, validate_args
 
schema = tool_schema(search_web)   # name, description, and params from the function
binding = EffectorBinding(tools=("search_web",), schemas=(schema,), ...)

ToolSchema renders to any provider's tool-declaration shape
(.to_openai(), .to_anthropic(), .to_ollama()); render_tools() picks the
right one from a dialect name. tool_schema(fn) builds one from an ordinary
function - name from __name__, description from the first paragraph of the
docstring, per-parameter type from the annotation, required = parameters
without a default. EffectorBinding.schemas is new: set it and, if tools=
wasn't given explicitly, the routing table is derived from the schema names,
so the two can't drift apart. The rendered payload lands in
input_data["tools"] after before_task hooks run, so a hook that rebuilds
the input from scratch can't accidentally drop it.

tool_result_messages() is the other half - turning an Axon's tool
observations back into the message pairs each provider expects on the next
turn. It matters most for Anthropic, which requires every tool_result for
one assistant turn to arrive in a single user message, in the same order; a
per-provider id scheme (call_auto_N) is synthesized where a dialect (Hermes)
sends none, so results still correlate to calls on the round trip.

New at package top level: ToolSchema, tool_schema, validate_args,
render_tools, extract_native_calls, extract_tool_calls,
tool_result_messages.

Genesis: the Neuron's prompt

An LLM Neuron's system prompt is the text that gets rewritten most often and
the one thing that never showed up in the Code tab's config form - only
ollama, openai, and anthropic factories take a system= keyword, so the
corpus writes it as a module constant that a @AXON.before_task hook folds
into the request. That left the most-edited part of the module sitting in a
read-only chunk at the bottom of the tab, below every keyword nobody touches.

The new prompt card puts it at the top, editable, prose-formatted rather than
syntax-highlighted (a prompt is English; running the Python tokenizer over it
would color every "if" and "return" in the instructions). A few things it
gets right on purpose:

  • Read-only when saving would lose something. A prompt built from an
    f-string, concatenation, or with a real comment between its pieces is shown
    but not editable - saving from the card would flatten it. Comment detection
    is tokenize-based, not a # regex scan, because prompts routinely contain a
    literal # in their own text (a "# Tools" heading) that would otherwise
    false-positive as a comment.
  • Warns when nothing reads it. If the module never references the prompt
    constant by name, the card says so - the SDK doesn't look for SYSTEM by
    convention, a hook has to put it in the request, so an unread constant never
    reaches the model no matter how carefully it's written.
  • Round-trips before it confirms. After a save, the file is re-parsed and
    compared against what was typed; a mismatch raises rather than silently
    writing something subtly different. The literal renderer also prefers single
    quotes when a prompt contains " but not ', since a prompt full of JSON
    examples otherwise turns into a wall of backslashes under the default double
    quote.
    New route: POST /api/prompt. New in cosmonapse/genesis-ui: a first
    package-level README.md documenting the full local API surface and the one
    thing that trips up a source checkout - genesis_dist/ is not rebuilt from
    src/ automatically; npm run build:into-wheel has to run and the output has
    to be committed, or the served UI is stale. (This is exactly the gap closed in
    the release workflow below.)

Fixes

  • Prism's Clear button no longer degrades the Brain View. clear() used
    to wipe the neuron registry along with the signal log. The registry is built
    from REGISTER signals, which arrive once per participant and never again -
    wiping it stripped every node of its kind, capabilities, and version with no
    way back short of a reconnect. Clear now zeroes each node's counters and
    leaves its identity alone.
  • Prism connects over wss:// on an https page. The socket URL was
    hardcoded to ws://; it's now derived from location.protocol, since a
    plain ws:// socket from an https page is blocked outright.
  • A Genesis-managed synapse could hang with no crash and no traceback.
    Its stderr went to a fixed-size OS pipe that was only drained on a failure
    path; once a long-running synapse filled that buffer, its next write to
    stderr blocked inside the syscall forever - the process stayed alive, the
    port stayed bound, and the namespace just stopped answering. Stderr now
    goes to a temp file that's read by name instead of by blocking on a pipe,
    so nothing the child writes can back it into a wall.
  • MemorySynapse no longer swallows a handler's exception. Fan-out
    dispatch passed return_exceptions=True to asyncio.gather and never
    looked at the results. This is the default bus for cosmo init projects
    and for Genesis's Run button, so a handler that raised used to be the first
    error a new user hit - and it arrived as silence. It's now logged.
  • Genesis's Test-tab terminal could miss a brain's early output. The
    WebSocket handler sent buffered scrollback before subscribing to new
    output, dropping whatever the child process emitted in between - on a
    chatty boot, the interesting part. Order is now: snapshot, subscribe, then
    send.
  • The Genesis UI build was missing from the release workflow. Prism's
    build step (npm ci, npm run build:into-wheel) existed in
    release.yml; Genesis's did not, so a shipped wheel carried whatever
    genesis_dist/ happened to be committed rather than a fresh build. Any
    Genesis UI change released without a manual build went out stale and
    silent. Genesis now gets the same build step, in the same shape.
  • Genesis's dev-server proxy is missing /api/brain/ws, added with ws: true - without it the Test tab's terminal socket was dead under npm run dev, which read as a Genesis bug rather than a config gap.
  • A multi-line value typed into any Genesis form field (a prompt, a docstring)
    could render into the file as a literal with a real line break in it - a
    SyntaxError on the next parse. String escaping now covers \r / \t /
    \n, not just \/".
  • cosmo init had ~15 lines of dead scaffolding logic duplicating the
    scaffold_project() helper it was meant to call; removed, no behavior
    change. Its "populated directory" error message now says --force
    (matching the CLI flag) instead of "force," since the same error is now
    also raised through Genesis's HTTP import path, where there is no flag.
  • CI's dependency install referenced a flask extra that hasn't existed
    since the receptor extra replaced it in 0.1.11 - ci.yml now installs
    receptor. pyproject.toml's own [project.optional-dependencies] was
    already correct; this only affected what CI exercised, not what pip install resolves.
    Test count is up to 469 across 25 files, from 401 across 22 at 0.1.11 -
    46 new tests cover the tool-call transport end to end (structured beats
    narrated text, dropped-vs-parallel calls, argument validation, id
    correlation), 15 more cover the prompt feature's AST surgery, and a new
    test_cli_init.py locks down cosmo init's behavior directly.

Compatibility

Additive. No new signal types, no envelope changes. An Axon that never passed
effectors= behaves exactly as before - dialect inference and the native
tool-call channel only activate when there's something to call. An Axon that
already passed an explicit tool_standard= is unaffected by inference, and
extract_tool_call (singular) still works as a thin wrapper over the new
extract_tool_calls.

If you're on Anthropic with effectors= set: this release changes
observable behavior, in the direction of correctness. A tool call that used
to be silently missed will now be recognized and dispatched. If anything
downstream depended on that call never firing, it will now fire.

Install

pip install cosmonapse
 
# HTTP and chat Receptors, and now what CI actually tests against
pip install 'cosmonapse[receptor]'
cosmo genesis     # -> 127.0.0.1:7072, edit a Neuron's prompt from the Code tab
cosmo prism       # -> 127.0.0.1:7071

Links