Skip to content

Runtime configuration: scope is invisible, prelude config is undeclared, and some values are unreachable #1236

Description

@andreasronge

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:

  1. Signature on agent.core entries — metadata on three functions, no Kernel
    change. Smallest, and useful on its own.
  2. 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.
  3. Grouped limits shape — a catalog presentation and manifest-parser
    change; enforcement points do not move.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions