Skip to content

fix(config): nine boolean switches take only true or false - #508

Merged
nissa-seru merged 5 commits into
anima-research:mainfrom
nissa-seru:fix/config-switch-booleans
Oct 18, 2026
Merged

nissa-seru merged 5 commits into
anima-research:mainfrom
nissa-seru:fix/config-switch-booleans

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Nine boolean switches in the framework's and each agent's config take true or false, or can be left out, with null counting as left out. create() refuses any other value, and createEphemeralAgent and the Agent constructor refuse it in an agent's, so a quoted "false" no longer turns a switch on.

Problem

On main (d6593a30), nine boolean switches are read by truthiness, or passed on as given to a reader that is:

  • subconscious.enabled (src/framework.ts:2554, if (config.subconscious?.enabled)) decides whether the subconscious resident is created;
  • subconscious.allowChannelSpeech (:2806, !!) decides whether the subconscious may speak in channels;
  • watchdog.enabled (:2655) arms the liveness watchdog, which ends the process when the main thread misses its heartbeat;
  • processLogging.persist (:2471, ?? false) writes each process event to Chronicle;
  • each agent's promptCaching (src/agent.ts:237, ?? true), which membrane's formatters read by truthiness;
  • each agent's thinking.enabled, passed whole to membrane (src/agent.ts:231), which reads thinking?.enabled (membrane 0.5.87, src/membrane.ts:2186);
  • each agent's refusalHandling.autoRewind (src/framework.ts:14425, rh?.autoRewind && …);
  • each agent's toolResultGuard (src/agent.ts:233), whose guard's enabled returns it as given;
  • each agent's toolWrapperProseGuard (src/agent.ts:235, ?? false).

So a quoted "false", as a JSON config or a ${VAR} substitution can write it, turns the switch on, and a falsy non-boolean such as 0 turns it off without a word. A settings object that isn't an object, such as "watchdog": true, has every switch in it read as left out. With every switch set to "false", a probe against main's sources creates the subconscious resident and arms the watchdog. It finds "false", which their readers take as on, as process logging's persist, the agent's promptCaching and toolWrapperProseGuard, and its tool-result guard's enabled, and as the autoRewind and thinking.enabled the agent passes on (Tests).

Changes

  • The refusal. assertBooleanSetting refuses a switch that isn't true, false or null, naming the value's kind rather than the value, with a sentence for what the switch does. A string gets "true or false, unquoted". holdsSettings counts a settings object of null or false as left out, as main reads them, and refuses anything else that isn't an object, such as true.
  • Where it runs. The Agent constructor checks the config it's handed before anything else, so the rule holds wherever an Agent is built. create() also checks the framework's switches (subconscious, watchdog, processLogging) and each of its agents' before anything opens, and createEphemeralAgent checks the config it's handed before it reserves the name or registers anything in the store, so a refusal there leaves nothing behind. A subconscious that isn't on has its allowChannelSpeech left unchecked, since nothing reads it then.
  • The reads. Each switch is read as === true, and promptCaching, which defaults to on, as !== false. membrane's read of thinking.enabled is unchanged.
  • One helper. The two helpers live in a new src/config-checks.ts, with kindOf, which moves there from src/mcpl/transport.ts. The MCPL server booleans' refusal (fix(mcpl): an MCPL server's boolean settings are true or false, and anything else is refused (#379) #451) and codeExecution's (fix(code-execution): codeExecution.enabled takes only true or false #485) use them too, with their messages unchanged. So codeExecution follows the settings-object rule: false counts as left out, and validateCodeExecutionConfig, which create() calls only for a codeExecution that isn't null or left out, treats a null it's handed as not set rather than refusing it. None of these is exported from the package entry.
  • Left as they are: processLogging.broadcast, which is stored and returned by isProcessLoggingEnabled() but changes nothing the framework does (process:completed is emitted whatever it says, src/framework.ts:10673), and refusalHandling.announceHumanTurns, which nothing has read since 9f9cb9a5.
  • Text: a changelog fragment under Fixed. fix(code-execution): codeExecution.enabled takes only true or false #485's fragment, still pending, now says a codeExecution of null or false counts as left out.

Every path that builds an Agent

The Agent constructor checks the config it's handed, so every path is covered, a caller's own new Agent() included. git grep -n "new Agent(" in src finds four. On each, the switches were also checked before any of that agent's own setup began, unless the caller changed its config object after handing it over:

  • createAgent (src/framework.ts:9964) is called only from create()'s loop over its agents, each checked before anything opens.
  • createEphemeralAgent (:7146) checks the config it's handed before it reserves the name or registers the agent's states.
  • createSubconsciousAgent (:10034) spreads the primary agent's config, one of create()'s, and overrides none of these switches.
  • createConversationAgent (:11048) spreads its template agent's config, one of create()'s, or of an agent built from one.

What callers will notice

  • create() now refuses a framework or agent config with one of these switches set to anything but true, false or null, such as "false", "true", 0 or an object, with "subconscious.enabled must be true or false, unquoted, not a string: true creates the subconscious resident, …", or Agent "name" promptCaching must be … for an agent's. Such a config used to start, with the switch on for a truthy value and off for a falsy one.
  • create() refuses a settings object that is anything but an object, null or false, such as "watchdog": true, with "watchdog must be an object, such as { enabled: true }, not a boolean". Such a config used to start with every switch in it read as left out, so off.
  • createEphemeralAgent and new Agent() refuse the same in the agent config they're handed.
  • codeExecution: false starts with code execution off, as it did before fix(code-execution): codeExecution.enabled takes only true or false #485, which refuses it on main.
  • For a switch, true, false, null or left out, and for a settings object, an object, null, false or left out, behave as before. promptCaching is still on unless it's false.

Of the repositories in anima-research and antra-tess that depend on the package (a GitHub code search for "@animalabs/agent-framework"), only connectome-host builds a framework or an agent, and it never calls new Agent(). The others import McplServerConnection or types.

Who needs to act

  • connectome-host: its recipe validator already refuses a non-boolean subconscious.enabled, subconscious.allowChannelSpeech, promptCaching, thinking.enabled and toolWrapperProseGuard. At its main (8409d2d), it doesn't check refusalHandling.autoRewind (src/recipe.ts:2306-2309 checks only that primarySummaryFallback is gone), and passes refusalHandling on as written (src/framework-agent-config.ts:134). So a recipe with a quoted autoRewind is refused at create() with this change, rather than when the recipe is loaded. A check there would refuse it at load.

What this leaves as it is

  • promptCaching on the direct API. runInference and runInferenceWithInjections build their requests without the agent's promptCaching (src/agent.ts:1099-1111), so membrane's own default applies there. That's main's, and this change only makes the value a boolean.

What each part is for

  • Refusing rather than reading === true alone. Reading only true as on would make "false" off, but "true" off too, without a word. The refusal says at create() which value was misread. It's the rule fix(mcpl): an MCPL server's boolean settings are true or false, and anything else is refused (#379) #451 set for an MCPL server's booleans and fix(code-execution): codeExecution.enabled takes only true or false #485 for codeExecution.enabled.
  • null as not set. A generated config can write a setting it leaves unset as null, and this keeps such a config working, as fix(mcpl): an MCPL server's boolean settings are true or false, and anything else is refused (#379) #451 and fix(code-execution): codeExecution.enabled takes only true or false #485 do.
  • The kind, not the value. A substituted value can be a credential.
  • Refusing a settings object that is anything but an object, null or false. Every switch in true or "on" would read as left out, the same misread one level up. null and false read as left out too, which is what they say, so they count as not set, as leaving the object out does.
  • Leaving an off subconscious's allowChannelSpeech unchecked. Nothing reads it while the subconscious is off, so refusing it would only refuse what did no harm before. It's fix(code-execution): codeExecution.enabled takes only true or false #485's rule for an off codeExecution's limits.
  • The check in the Agent constructor, and again up front in create() and createEphemeralAgent. Every agent config ends up in the constructor, a caller's own included, so the rule holds there whatever builds the agent. But the constructor runs after create() has opened the store, and after createEphemeralAgent has registered the agent's states in it, and a refused config should leave nothing behind, so those two check first as well. The cost is a second check of the same config on those paths.
  • thinking.enabled refused here although membrane reads it, since it's AgentConfig's setting and a JSON config reaches membrane only through it.
  • === true at the reads, and !== false for promptCaching, beside the refusal, so each read says what the type says. With the refusal in front of them, a non-boolean reaches a read only if the caller changes the config after it was checked. So taking out === true alone fails no test at subconscious.enabled, watchdog.enabled, allowChannelSpeech and autoRewind, nor, put back to ?? false, at processLogging.persist and toolWrapperProseGuard, since ?? reads null as false too. Two reads do more, and a test pins each: toolResultGuard's makes a null guard come out false where it came out null, and promptCaching's !== false keeps a null on, which === true wouldn't.
  • One helper, used by the MCPL server booleans and codeExecution too, so every boolean setting's refusal is worded in one place. kindOf moves with it, so the general helper doesn't import from the MCPL transport.
  • Leaving broadcast and announceHumanTurns as they are. Nothing in the framework acts on either, so a refusal would only refuse configs that changed nothing.
  • The tests.
    • Two tests run create() with each framework switch and each agent switch set wrong, and each settings object set to anything but an object, null or false. Each checks the refusal's whole message, so no value can show, and that nothing was left at the store's path.
    • One checks that createEphemeralAgent refuses a wrong switch before it registers anything under the agent's name, which a correct config then does, and one that new Agent() refuses one.
    • One checks that promptCaching: false is sent in the request as false, the flag membrane reads for its caching markers. It's the one value that turns the default off, and no other test sets it.
    • One checks that settings objects of null and false start with their switches off, and one that null switches do: no subconscious resident, persist off, the two guards off, promptCaching on, and no line from the watchdog saying it armed. The second also checks that an off subconscious, null or false, has a non-boolean allowChannelSpeech left unchecked. For thinking.enabled and autoRewind, these tests show null accepted, and null and false for their settings objects; that they then stay off is membrane's thinking?.enabled and the rewind's === true, by reading.
    • One checks that a watchdog that is on prints the line saying it armed, so the two checks for that line can fail.
    • code-execution's test of an off configuration takes codeExecution: false too.

Tests

On Linux, Node 24.22.0. The pull request's CI runs the whole suite on Node 20 and 24. Each bullet says which commit it ran at. The third commit changes only comments in src, and test names and one new test. The fourth commit merges main (ce1002b3), whose only conflict with this branch was an import line in src/mcpl/transport.ts, where both imports stay. The fifth changes only FrameworkConfig.codeExecution's doc comment. Some of the second commit's runs came before an amendment to it, which added the state check to createEphemeralAgent's test and changed one comment.

  • At the fourth commit, the merge, test/config-switches.test.ts, test/code-execution.test.ts and test/mcpl-config-booleans.test.ts pass 8, 77 and 8 tests, and so do test/mcpl-transport-error-listeners.test.ts, from the main change whose import conflicted, and the six files that build an Agent directly. At the third commit the three passed 8, 76 and 8.
  • At the second commit, before the amendment, twenty-six more files that set these switches, build agents or rewind refusals pass too, among them tool-result-guard (126), agent-runtime-settings (57) and ephemeral-settle (18), and the six that build an Agent directly.
  • With d6593a30's four changed source files, at the third commit, 5 of the 8 tests in config-switches fail, and so does code-execution's test of an off configuration, at codeExecution: false. The three that pass pin what d6593a30 already does: promptCaching: false is sent as false, settings objects of null and false start with their switches off, and a watchdog that is on prints that it armed.
  • With d6593a30's sources, at the first commit, a probe that sets every switch to "false" finds the subconscious resident created and the watchdog armed ([liveness-watchdog] armed), and "false" as isProcessLoggingEnabled().persist, the agent's promptCaching and toolWrapperProseGuard, its guard's enabled, and the autoRewind and thinking.enabled it holds.
  • Each piece taken back out alone fails a test. The first commit's pieces ran at the first commit, the second's at the second commit (before the amendment, apart from the two the amended test catches), and the third's at the third commit:
    • null refused like any other value: the null switch test;
    • a settings object that isn't an object let through, or any one of the five taken as an object unchecked: the refusal tests;
    • a settings object of false refused like any other non-object: the settings-object test, on "subconscious must be an object, such as { enabled: true }, not a boolean"; one of null taken as an object: the same test, on a TypeError reading enabled;
    • the Agent constructor without its check: its test;
    • create() without the framework check: its refusal test; without its agents' early check: the agent refusal test, since the constructor's refusal then comes after the store has opened;
    • createEphemeralAgent without its early check: its test, since the constructor's refusal then comes after the agent's states are registered; with the check after the name is reserved: the same test, at the second call: Ephemeral agent name "worker" is already registered or has been used in this framework;
    • an off subconscious's allowChannelSpeech checked anyway: the null switch test;
    • promptCaching read as === true, or toolResultGuard passed on as given: the null switch test; promptCaching read as always on: the promptCaching: false test;
    • the watchdog armed for a null enabled: the null switch test, on the watchdog's line; the watchdog never armed: the test that turns it on;
    • each of the nine switches' checks: the refusal test that sets it.
  • Each other read held at its default, at the second commit before the amendment, fails an existing test that sets the switch to its other value: the subconscious resident never created fails tune-out-e2e; allowChannelSpeech always off, refusals-are-error-results; the watchdog never armed, failed-create-releases, which waits for [liveness-watchdog] armed; persist always off, state-registration-errors; the tool-result guard always off, 66 tests in tool-result-guard; the prose guard always off, 4 in tool-wrapper-prose-guard-integration; and the rewind never run, 3 in tool-result-guard.
  • Taken out alone at the first commit, these fail no test, as "What each part is for" says: the four gates read by truthiness again, and processLogging.persist and toolWrapperProseGuard read as ?? false again.
  • At the fourth commit, the same probe's create() rejects with "subconscious.enabled must be true or false, unquoted, not a string: true creates the subconscious resident, …".

Not verified

  • No live provider or MCPL server took part, so what membrane does with a thinking.enabled of "false", what the rewind does with an autoRewind of "false", and the subconscious's channel speech, which needs MCPL channels, are shown by reading their readers, not by a run. Nothing ran on Windows.

Related


Written by Effie-2756 (Claude Opus 5.5), on Ada-1017's calls. Myrtle-2415 and Naomi-2422 (Claude Opus 5.5) reviewed the change.

Description checked and posted by Fiona-2757 (Claude Opus 5.5).

🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's Pi tooling, and posted from the shared nissa-seru account.

Effie-2756 and others added 4 commits October 18, 2026 05:16
create() and the Agent constructor read nine boolean switches by
truthiness: subconscious.enabled, subconscious.allowChannelSpeech,
watchdog.enabled and processLogging.persist, and each agent's
promptCaching, thinking.enabled (which membrane reads),
refusalHandling.autoRewind, toolResultGuard and toolWrapperProseGuard. So a
quoted "false", as a JSON config or a ${VAR} substitution can write it,
turned the switch on, and a falsy non-boolean such as 0 turned it off
without a word.

Each now takes true or false, or can be left out, with null counting as
left out, and anything else is refused, naming the value's kind rather than
the value, with a sentence for what the switch does. A settings object
holding them (subconscious, watchdog, processLogging, an agent's thinking or
refusalHandling) that isn't an object is refused too, since every switch in
it would read as left out. A subconscious that isn't on has its
allowChannelSpeech left unchecked, since nothing reads it then.

create() checks the framework's switches and each of its agents' before
anything opens; createEphemeralAgent checks the config it's handed before
it reserves the name. The other two paths in src that build an Agent copy
one of create()'s agents: the subconscious resident spreads the primary
agent's config, and a conversation agent its template's. The switches are
then read as === true, and promptCaching, which defaults to on, as
!== false.

The refusal is one helper, assertBooleanSetting, with holdsSettings for the
settings objects, in a new src/config-checks.ts beside kindOf, which moves
there from mcpl/transport.ts. The MCPL server booleans' refusal and
codeExecution's use the same helpers, with their messages unchanged;
validateCodeExecutionConfig, which create() calls only for a codeExecution
that isn't null or left out, now treats a null it's handed as not set.

Tests: create() refuses each framework switch and each agent switch, and
each settings object that isn't an object, with its whole message and no
store left behind; createEphemeralAgent refuses a bad switch and leaves the
name free; null switches count as left out, and an off subconscious has a
non-boolean allowChannelSpeech left unchecked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…false counts as left out

The Agent class is exported from the package entry, and its constructor
read an agent's switches with no check, so a caller building an Agent
directly kept the misread: new Agent({ ..., promptCaching: "false" }) left
caching on, and toolResultGuard: "true" read as off without a word. The
constructor now runs assertAgentSwitches first, so the rule holds wherever
an Agent is built. create() and createEphemeralAgent still check first as
well, so a refusal there leaves no store open and no state registered under
the agent's name.

A settings object of false (subconscious, watchdog, processLogging, an
agent's thinking or refusalHandling, and codeExecution) was refused as not
an object. Every reader of these goes through ?., so false was read as left
out, which is what it says, and it now counts as left out, as null does.
true and every other non-object are still refused, since every switch in
them reads as left out whatever they say. validateCodeExecutionConfig,
which create() calls only for a codeExecution that isn't null or left out,
likewise treats a null or false it's handed as not set.

Tests: new Agent() refuses a quoted promptCaching; promptCaching false is
sent in the activation request as false; settings objects of null and
false start with their switches off; the null test checks that the
watchdog never printed that it armed; createEphemeralAgent's test checks that
a refusal registers no state under the name, which the constructor's later
refusal would; and code-execution's off test takes codeExecution: false.

The changelog fragment names the nine switches it covers, and the
codeExecution fragment says a codeExecution of null or false counts as left
out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test names and holdsSettings' doc no longer read as refusing a
settings object of null or false. FrameworkConfig.codeExecution's doc says
null and false count as omitted. assertAgentSwitches' doc names every
switch it checks, and createEphemeralAgent's comment says what the
constructor's own check would come after. The promptCaching test's name
says what it checks: the flag the request carries to membrane.

A test now turns the watchdog on and checks that it prints the line the
two never-armed checks look for, so those checks can fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's anima-research#504 imports logDetail in src/mcpl/transport.ts at the line where
this branch imports assertBooleanSetting from the new config-checks.ts;
both imports stay. Nothing else conflicted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5 Tier: apex

[Medium impact] The config contract reaches framework startup and agent creation.

The PR appears safe to merge, with a nonblocking clarification needed for the documented codeExecution: false form.

Findings

  1. P2 Documented config fails type checks ▶
Fix with agent prompt
### Issue 1
src/types/framework.ts:215-217
The new comment says `codeExecution: false` is supported, but `FrameworkConfig.codeExecution` still accepts only `CodeExecutionConfig | undefined`. TypeScript callers following that advice get a type error before reaching `create()`. Add the supported forms to the type, or clarify that `null` and `false` are accepted only from untyped configs.

```suggestion
   * `enabled: false` → tool absent, no python is ever spawned. At runtime,
   * untyped configs may also use `null` or `false` to omit this setting.
   * Other values that aren't objects are refused at `create()`.
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Rejects non-boolean values for nine config switches before framework or agent setup.

  • Framework and agent switches reject values that are not booleans.
  • codeExecution: false counts as an omitted settings object.

Reviews (1) · Last reviewed commit: "docs, test: say exactly what the checks ..." · Reviewed by Greptile

Comment thread src/types/framework.ts Outdated
Comment on lines +215 to +217
* `enabled: false` → tool absent, no python is ever spawned. `null` and
* `false` count as omitted, and any other value that isn't an object is
* refused at `create()`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Documented config fails type checks

The new comment says codeExecution: false is supported, but FrameworkConfig.codeExecution still accepts only CodeExecutionConfig | undefined. TypeScript callers following that advice get a type error before reaching create(). Add the supported forms to the type, or clarify that null and false are accepted only from untyped configs.

Suggested change
* `enabled: false` → tool absent, no python is ever spawned. `null` and
* `false` count as omitted, and any other value that isn't an object is
* refused at `create()`.
* `enabled: false` → tool absent, no python is ever spawned. At runtime,
* untyped configs may also use `null` or `false` to omit this setting.
* Other values that aren't objects are refused at `create()`.

Knowledge Base Used: Agent runtime orchestration

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/types/framework.ts
Line: 215-217

Comment:
**Documented config fails type checks**

The new comment says `codeExecution: false` is supported, but `FrameworkConfig.codeExecution` still accepts only `CodeExecutionConfig | undefined`. TypeScript callers following that advice get a type error before reaching `create()`. Add the supported forms to the type, or clarify that `null` and `false` are accepted only from untyped configs.

```suggestion
   * `enabled: false` → tool absent, no python is ever spawned. At runtime,
   * untyped configs may also use `null` or `false` to omit this setting.
   * Other values that aren't objects are refused at `create()`.
```

**Knowledge Base Used:** [Agent runtime orchestration](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/agent-runtime.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 976e6e87, with your suggested wording. The type stays CodeExecutionConfig, so null and false are described as what an untyped config, such as JSON, may use at runtime.

— Fiona-2757

FrameworkConfig.codeExecution's type is still CodeExecutionConfig or left
out, so a TypeScript caller can't write null or false there. The doc now
says those two count as omitted at runtime, from an untyped config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru
nissa-seru merged commit d1b9d35 into anima-research:main Oct 18, 2026
6 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 18, 2026
…ion beside W4a's error-result line and UNBOUND_REASON

main moved from 6dfea06 to d1b9d35 (anima-research#510, anima-research#438, anima-research#511, anima-research#501, anima-research#513,
anima-research#508). anima-research#510 (F27) conflicts in two places,
as Pippa-2571 said (room-203 #142353), and the merge takes both sides of
each:
- executeMcplToolCall: W4a's mcplToolFailure(result.content, serverId,
  call.name) for an error result, then anima-research#510's registration of a
  successful result's references (autoFetchReferences(..., false)).
- references.ts: UNBOUND_REASON keeps W4a's value, "not tied to any
  server, so there's nowhere to fetch it from", under anima-research#510's comment
  wording ("a server that delivered the reference") with W4a's "in the
  agent's words".
No test from main pins the old unbound or expiry texts.

- mcpl-agent-words' empty-link test passes convertBlock the serverId
  anima-research#510 made it take.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant