Repository navigation
fix(config): nine boolean switches take only true or false - #508
Conversation
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>
[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 Findings
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.SummaryRejects non-boolean values for nine config switches before framework or agent setup.
Reviews (1) · Last reviewed commit: "docs, test: say exactly what the checks ..." · Reviewed by Greptile |
| * `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()`. |
There was a problem hiding this 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.
| * `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.There was a problem hiding this comment.
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>
…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.
Nine boolean switches in the framework's and each agent's config take
trueorfalse, or can be left out, withnullcounting as left out.create()refuses any other value, andcreateEphemeralAgentand theAgentconstructor 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;promptCaching(src/agent.ts:237,?? true), which membrane's formatters read by truthiness;thinking.enabled, passed whole to membrane (src/agent.ts:231), which readsthinking?.enabled(membrane 0.5.87,src/membrane.ts:2186);refusalHandling.autoRewind(src/framework.ts:14425,rh?.autoRewind && …);toolResultGuard(src/agent.ts:233), whose guard'senabledreturns it as given;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 as0turns 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 againstmain's sources creates the subconscious resident and arms the watchdog. It finds"false", which their readers take as on, as process logging'spersist, the agent'spromptCachingandtoolWrapperProseGuard, and its tool-result guard'senabled, and as theautoRewindandthinking.enabledthe agent passes on (Tests).Changes
assertBooleanSettingrefuses a switch that isn'ttrue,falseornull, naming the value's kind rather than the value, with a sentence for what the switch does. A string gets "true or false, unquoted".holdsSettingscounts a settings object ofnullorfalseas left out, asmainreads them, and refuses anything else that isn't an object, such astrue.Agentconstructor 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, andcreateEphemeralAgentchecks 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 itsallowChannelSpeechleft unchecked, since nothing reads it then.=== true, andpromptCaching, which defaults to on, as!== false. membrane's read ofthinking.enabledis unchanged.src/config-checks.ts, withkindOf, which moves there fromsrc/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) andcodeExecution's (fix(code-execution): codeExecution.enabled takes only true or false #485) use them too, with their messages unchanged. SocodeExecutionfollows the settings-object rule:falsecounts as left out, andvalidateCodeExecutionConfig, whichcreate()calls only for acodeExecutionthat isn'tnullor left out, treats anullit's handed as not set rather than refusing it. None of these is exported from the package entry.processLogging.broadcast, which is stored and returned byisProcessLoggingEnabled()but changes nothing the framework does (process:completedis emitted whatever it says,src/framework.ts:10673), andrefusalHandling.announceHumanTurns, which nothing has read since9f9cb9a5.codeExecutionofnullorfalsecounts as left out.Every path that builds an Agent
The
Agentconstructor checks the config it's handed, so every path is covered, a caller's ownnew Agent()included.git grep -n "new Agent("insrcfinds 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 fromcreate()'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 ofcreate()'s, and overrides none of these switches.createConversationAgent(:11048) spreads its template agent's config, one ofcreate()'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 buttrue,falseornull, such as"false","true",0or an object, with "subconscious.enabled must be true or false, unquoted, not a string: true creates the subconscious resident, …", orAgent "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,nullorfalse, 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.createEphemeralAgentandnew Agent()refuse the same in the agent config they're handed.codeExecution: falsestarts with code execution off, as it did before fix(code-execution): codeExecution.enabled takes only true or false #485, which refuses it onmain.true,false,nullor left out, and for a settings object, an object,null,falseor left out, behave as before.promptCachingis still on unless it'sfalse.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 callsnew Agent(). The others importMcplServerConnectionor types.Who needs to act
subconscious.enabled,subconscious.allowChannelSpeech,promptCaching,thinking.enabledandtoolWrapperProseGuard. At itsmain(8409d2d), it doesn't checkrefusalHandling.autoRewind(src/recipe.ts:2306-2309checks only thatprimarySummaryFallbackis gone), and passesrefusalHandlingon as written (src/framework-agent-config.ts:134). So a recipe with a quotedautoRewindis refused atcreate()with this change, rather than when the recipe is loaded. A check there would refuse it at load.What this leaves as it is
promptCachingon the direct API.runInferenceandrunInferenceWithInjectionsbuild their requests without the agent'spromptCaching(src/agent.ts:1099-1111), so membrane's own default applies there. That'smain's, and this change only makes the value a boolean.What each part is for
=== truealone. Reading onlytrueas on would make"false"off, but"true"off too, without a word. The refusal says atcreate()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 forcodeExecution.enabled.nullas not set. A generated config can write a setting it leaves unset asnull, 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.nullorfalse. Every switch intrueor"on"would read as left out, the same misread one level up.nullandfalseread as left out too, which is what they say, so they count as not set, as leaving the object out does.allowChannelSpeechunchecked. 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 offcodeExecution's limits.Agentconstructor, and again up front increate()andcreateEphemeralAgent. 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 aftercreate()has opened the store, and aftercreateEphemeralAgenthas 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.enabledrefused here although membrane reads it, since it'sAgentConfig's setting and a JSON config reaches membrane only through it.=== trueat the reads, and!== falseforpromptCaching, 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=== truealone fails no test atsubconscious.enabled,watchdog.enabled,allowChannelSpeechandautoRewind, nor, put back to?? false, atprocessLogging.persistandtoolWrapperProseGuard, since??readsnullasfalsetoo. Two reads do more, and a test pins each:toolResultGuard's makes anullguard come outfalsewhere it came outnull, andpromptCaching's!== falsekeeps anullon, which=== truewouldn't.codeExecutiontoo, so every boolean setting's refusal is worded in one place.kindOfmoves with it, so the general helper doesn't import from the MCPL transport.broadcastandannounceHumanTurnsas they are. Nothing in the framework acts on either, so a refusal would only refuse configs that changed nothing.create()with each framework switch and each agent switch set wrong, and each settings object set to anything but an object,nullorfalse. Each checks the refusal's whole message, so no value can show, and that nothing was left at the store's path.createEphemeralAgentrefuses a wrong switch before it registers anything under the agent's name, which a correct config then does, and one thatnew Agent()refuses one.promptCaching: falseis sent in the request asfalse, the flag membrane reads for its caching markers. It's the one value that turns the default off, and no other test sets it.nullandfalsestart with their switches off, and one thatnullswitches do: no subconscious resident,persistoff, the two guards off,promptCachingon, and no line from the watchdog saying it armed. The second also checks that an off subconscious,nullorfalse, has a non-booleanallowChannelSpeechleft unchecked. Forthinking.enabledandautoRewind, these tests shownullaccepted, andnullandfalsefor their settings objects; that they then stay off is membrane'sthinking?.enabledand the rewind's=== true, by reading.code-execution's test of an off configuration takescodeExecution: falsetoo.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 mergesmain(ce1002b3), whose only conflict with this branch was an import line insrc/mcpl/transport.ts, where both imports stay. The fifth changes onlyFrameworkConfig.codeExecution's doc comment. Some of the second commit's runs came before an amendment to it, which added the state check tocreateEphemeralAgent's test and changed one comment.test/config-switches.test.ts,test/code-execution.test.tsandtest/mcpl-config-booleans.test.tspass 8, 77 and 8 tests, and so dotest/mcpl-transport-error-listeners.test.ts, from themainchange whose import conflicted, and the six files that build anAgentdirectly. At the third commit the three passed 8, 76 and 8.tool-result-guard(126),agent-runtime-settings(57) andephemeral-settle(18), and the six that build anAgentdirectly.d6593a30's four changed source files, at the third commit, 5 of the 8 tests inconfig-switchesfail, and so doescode-execution's test of an off configuration, atcodeExecution: false. The three that pass pin whatd6593a30already does:promptCaching: falseis sent asfalse, settings objects ofnullandfalsestart with their switches off, and a watchdog that is on prints that it armed.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"asisProcessLoggingEnabled().persist, the agent'spromptCachingandtoolWrapperProseGuard, its guard'senabled, and theautoRewindandthinking.enabledit holds.nullrefused like any other value: thenullswitch test;falserefused like any other non-object: the settings-object test, on "subconscious must be an object, such as { enabled: true }, not a boolean"; one ofnulltaken as an object: the same test, on aTypeErrorreadingenabled;Agentconstructor 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;createEphemeralAgentwithout 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;allowChannelSpeechchecked anyway: thenullswitch test;promptCachingread as=== true, ortoolResultGuardpassed on as given: thenullswitch test;promptCachingread as always on: thepromptCaching: falsetest;nullenabled: thenullswitch test, on the watchdog's line; the watchdog never armed: the test that turns it on;tune-out-e2e;allowChannelSpeechalways off,refusals-are-error-results; the watchdog never armed,failed-create-releases, which waits for[liveness-watchdog] armed;persistalways off,state-registration-errors; the tool-result guard always off, 66 tests intool-result-guard; the prose guard always off, 4 intool-wrapper-prose-guard-integration; and the rewind never run, 3 intool-result-guard.processLogging.persistandtoolWrapperProseGuardread as?? falseagain.create()rejects with "subconscious.enabled must be true or false, unquoted, not a string: true creates the subconscious resident, …".Not verified
thinking.enabledof"false", what the rewind does with anautoRewindof"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
failureNotices, read=== true. It isn't onmain, soassertAgentSwitchesdoesn't check it.assertServerConfigBooleans, its skip condition, and keepskindOfinsrc/mcpl/transport.ts, so the two conflict there and in the import lines.changelog.d/fix-config-switch-booleans.fixed.md, with fix(code-execution): codeExecution.enabled takes only true or false #485's pendingchangelog.d/473-code-execution-enabled.fixed.mdupdated to matchWritten 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-seruaccount.