Problem
Runtime configuration is spread across three mechanisms with different rules, and
which one applies to a given value is not visible from any file a user edits.
1. limits is flat, so scope is invisible. All 24 manifest-narrowable names
sit at one level. workflow_heap_words is adjacent to evaluation_heap_words
and to run_duration_ms with nothing indicating that the first bounds the
orchestrator, the second bounds one mission evaluation, and the third bounds the
whole run. The scope is encoded only in a name prefix the reader has to know.
To be clear about what already works, because it is easy to assume otherwise:
heap sizes and timeouts are not hardcoded. They are :manifest_narrowable
rows in LimitCatalog, settable in a manifest and ceilinged in a host document.
The problem is legibility, not absence.
2. Prelude configuration has no declared contract. max_turns exists
nowhere in lib/ — three occurrences, all in .clj:
agent.core.clj:75 (positive-int-or (get cfg "max_turns") 4 128)
agent.core.clj:80 "max_turns" max-turns
agent.prompt.clj:9 :turns-remaining (get cfg "max_turns")
So it binds only because agent.core chooses to respect it, and a wrong value
is silently swallowed:
(agent.core/run-value task {"max_turns" "three"}) ; runs with 4 turns, no signal
There is no way to discover the accepted keys except reading the prelude source,
and no way to learn that your own key was ignored because you typo'd it.
3. Some values are unreachable from any file.
| Value |
Where it lives |
Reachable? |
pmap_timeout |
Context.@default_pmap_timeout (5_000) |
no — absent from LimitCatalog, and Runner.execute_workflow/5 never passes it |
value-history depth (*1 *2 *3) |
RunState.@history_depth (3) |
no — a constant, not a limit |
The pmap_timeout gap has teeth. Two concurrent agents under pcalls measured
4.1s against a hard 5s ceiling nothing can raise.
4. There is no per-agent budget. subordinate_evaluations is run-wide, so
two agents share one pot. If the first burns 15 of 16, the second gets 1, and no
file can prevent that. max_turns looks like a per-agent budget but is a
suggestion; the thing that actually enforces is shared.
Proposed shape
Group limits by scope, and let the grouping carry the meaning the prefixes
currently imply.
"limits": {
"run": {
"duration_ms": 120000,
"provider_tasks": 8
},
"workflow": {
"timeout_ms": 90000,
"heap_words": 8000000,
"pmap_timeout_ms": 30000,
"capability_calls": 64
},
"mission": {
"timeout_ms": 10000,
"heap_words": 1250000,
"evaluations": 24,
"capability_calls": 200,
"memory_bytes": 2000000,
"history_depth": 3
},
"missions": {
"research": {"evaluations": 20, "capability_calls": 180, "timeout_ms": 15000},
"review": {"evaluations": 4, "capability_calls": 10}
}
}
Read top to bottom, that states the whole budget without knowing any prefixes:
run — the shared pot: one deadline, one provider-slot budget.
workflow — the orchestrator. pmap_timeout_ms belongs here because that
is where parallel agents actually run, not inside a mission.
mission — the default for every space.
missions — per-space overrides.
Three narrowing levels, one rule at each: you may only ask for less.
host ceiling ≥ limits.mission ≥ limits.missions.research
So declaring a space can never buy budget the run did not already have. This is
the same rule per-space provider grants already follow on
spike/mission-spaces, where a space may only name providers that
providers.mission already selected.
history_depth becomes an ordinary mission limit rather than a constant, which
is what makes *1 *2 *3 configurable.
Declare the prelude config contract
Separately and independently: give the shipped agent entries a signature, using
the contract mechanism the runtime already has.
(defn run-value
"..."
{:signature "(task :string, cfg {max_turns :int?, mission :string?,
max_program_chars :int?, max_observation_chars :int?,
max_transcript_chars :int?, result_envelope :bool?}) -> :any"}
[task cfg]
...)
Shaped maps are open — verified empirically, an undeclared key passes
through — so a user's own configuration keys keep working while the known ones
become validated and discoverable via (doc "agent.core/run-value") and the
rendered prompt inventory.
A contract violation performs no capability call, so it is retryable and
agent.core already converts it into model correction feedback. Measured:
declared ok -> "t/3"
user's own key -> "t/3"
wrong type -> :prelude_contract_error
"cfgdemo/run-agent input cfg.max_turns: expected int, got string"
Known tradeoff: because the map is open, a typo in a declared key still passes
silently. Extensibility and typo-catching cannot both come from one map. If
typo-catching is wanted, the escape hatch is declared keys at the top level plus
a nested open sub-map for user keys.
Suggested split
These are independent and can land separately:
- Signature on
agent.core entries — metadata on three functions, no Kernel
change. Smallest, and useful on its own.
pmap_timeout as a real limit — one LimitCatalog row plus two call
sites. Should follow the parallel-agent hang investigation, since raising a
deadline on a hang only lengthens it.
- Grouped
limits shape — a catalog presentation and manifest-parser
change; enforcement points do not move.
- Per-space limits — the actual work.
RunState.reserve_evaluation/2
already receives the space, so it is mostly making run-wide counters
per-space.
Context
Measurements and the per-space groundwork are on spike/mission-spaces, written
up in docs/plans/mission-spaces-spike.md. That branch is spike code, not a
merge candidate.
Problem
Runtime configuration is spread across three mechanisms with different rules, and
which one applies to a given value is not visible from any file a user edits.
1.
limitsis flat, so scope is invisible. All 24 manifest-narrowable namessit at one level.
workflow_heap_wordsis adjacent toevaluation_heap_wordsand to
run_duration_mswith nothing indicating that the first bounds theorchestrator, the second bounds one mission evaluation, and the third bounds the
whole run. The scope is encoded only in a name prefix the reader has to know.
To be clear about what already works, because it is easy to assume otherwise:
heap sizes and timeouts are not hardcoded. They are
:manifest_narrowablerows in
LimitCatalog, settable in a manifest and ceilinged in a host document.The problem is legibility, not absence.
2. Prelude configuration has no declared contract.
max_turnsexistsnowhere in
lib/— three occurrences, all in.clj:So it binds only because
agent.corechooses to respect it, and a wrong valueis silently swallowed:
There is no way to discover the accepted keys except reading the prelude source,
and no way to learn that your own key was ignored because you typo'd it.
3. Some values are unreachable from any file.
pmap_timeoutContext.@default_pmap_timeout(5_000)LimitCatalog, andRunner.execute_workflow/5never passes it*1 *2 *3)RunState.@history_depth(3)The
pmap_timeoutgap has teeth. Two concurrent agents underpcallsmeasured4.1s against a hard 5s ceiling nothing can raise.
4. There is no per-agent budget.
subordinate_evaluationsis run-wide, sotwo agents share one pot. If the first burns 15 of 16, the second gets 1, and no
file can prevent that.
max_turnslooks like a per-agent budget but is asuggestion; the thing that actually enforces is shared.
Proposed shape
Group
limitsby scope, and let the grouping carry the meaning the prefixescurrently imply.
Read top to bottom, that states the whole budget without knowing any prefixes:
run— the shared pot: one deadline, one provider-slot budget.workflow— the orchestrator.pmap_timeout_msbelongs here because thatis where parallel agents actually run, not inside a mission.
mission— the default for every space.missions— per-space overrides.Three narrowing levels, one rule at each: you may only ask for less.
So declaring a space can never buy budget the run did not already have. This is
the same rule per-space provider grants already follow on
spike/mission-spaces, where a space may only name providers thatproviders.missionalready selected.history_depthbecomes an ordinary mission limit rather than a constant, whichis what makes
*1 *2 *3configurable.Declare the prelude config contract
Separately and independently: give the shipped agent entries a signature, using
the contract mechanism the runtime already has.
Shaped maps are open — verified empirically, an undeclared key passes
through — so a user's own configuration keys keep working while the known ones
become validated and discoverable via
(doc "agent.core/run-value")and therendered prompt inventory.
A contract violation performs no capability call, so it is retryable and
agent.corealready converts it into model correction feedback. Measured:Known tradeoff: because the map is open, a typo in a declared key still passes
silently. Extensibility and typo-catching cannot both come from one map. If
typo-catching is wanted, the escape hatch is declared keys at the top level plus
a nested open sub-map for user keys.
Suggested split
These are independent and can land separately:
agent.coreentries — metadata on three functions, no Kernelchange. Smallest, and useful on its own.
pmap_timeoutas a real limit — oneLimitCatalogrow plus two callsites. Should follow the parallel-agent hang investigation, since raising a
deadline on a hang only lengthens it.
limitsshape — a catalog presentation and manifest-parserchange; enforcement points do not move.
RunState.reserve_evaluation/2already receives the space, so it is mostly making run-wide counters
per-space.
Context
Measurements and the per-space groundwork are on
spike/mission-spaces, writtenup in
docs/plans/mission-spaces-spike.md. That branch is spike code, not amerge candidate.