Scenarios¶
A scenario is a complete use case as data. The orchestrator never changes; the scenario decides what the memory holds, who answers, what a good answer contains and how much it may cost.
Anatomy of a scenario¶
| Field | Type | Role |
|---|---|---|
name |
string | identifier; also the default file name for inspect |
title, description |
string | shown by poincare-orchestra list |
knowledge |
list of KnowledgeItem |
the hierarchy and dated facts that seed the memory |
profiles |
list of CapabilityProfile |
what each model/provider is good at and what it costs |
roles |
list of AgentRole |
the specialists; each points at a profile |
tasks |
list of Task |
example questions with the facets a good answer must contain |
policy |
ControllerPolicy |
budget, retrieval depth, objective weights, constraints, temporal policy |
A Scenario validates itself on construction and when loaded from a file. Problems are reported together with their position: duplicate knowledge ids, a parent declared after its child or not at all, an item that is its own parent, duplicate profile or role names, a role that references an unknown profile, a task without facets or with a blank facet, unknown objective names in the policy, thresholds outside [0, 1], malformed decay specs, and empty knowledge, profiles or roles lists.
Knowledge: the hierarchy and its dates¶
KnowledgeItem field |
Meaning |
|---|---|
id |
unique id; cited by claims and shown in answers |
text |
the record. Begin entity records with the entity name and a colon ("Athlete A: ...", "Ticket 4411: ..."): the controller recognises a question that names them |
parent |
id of the parent record, which must be declared earlier in the list. This builds the hierarchy that becomes depth on the Poincaré ball |
days_ago |
age of a dated record; undated items are structural knowledge and count as current |
valid_from_days_ago, valid_until_days_ago |
a validity window, enforced by the validity decay kind: superseded facts stop surfacing after their end date |
meta |
free dict. Two keys matter: domain on a branch (a child of the root) labels the domain the controller reports; kind in event, ticket, issue, case, observation marks a dated, ephemeral record |
Shape the hierarchy as root → domain branches → facts → dated observations. The controller maps every retrieved record to its branch, so the branches are the domains it reasons about; the retrieval searches a sub-task's branch first, then the subtrees of the entities the question names, then the whole memory. Records whose time weight has decayed below 0.001 (expired validity windows, very old observations under a short half-life) are not retrieved.
All dates are relative, so a scenario never goes stale: Scenario.build_memory(now=...) anchors them (default: the time of the run).
Roles and capability profiles¶
CapabilityProfile field |
Default | Used for |
|---|---|---|
name |
— | referenced by roles |
provider |
— | the provider the router prefers for this profile; match it to your provider objects' names |
domains |
[] |
domain labels the model is good at |
reasoning, creativity, reliability |
0.5 each |
intent-dependent fit: planning and comparison weigh reasoning, explanation the mean of reasoning and creativity, factual reliability |
cost_per_call |
1.0 |
sub-task cost = cost_per_call × (1 + complexity) |
latency_s |
1.0 |
descriptive (the detector measures real latencies) |
An AgentRole has a name, a description, the system_prompt sent to the provider, the domains it covers and the profile it runs on. Capability matching scores roles by domain overlap plus profile fit (Controller). Give each domain branch at least one role that lists it.
Tasks and facets¶
A Task is a question, the expected_facets (substrings a good answer must contain, case-insensitive) and optional notes. Tasks drive poincare-orchestra eval and should be answerable from the records: a facet is usually a version number, an id or a technical term that appears in the record you expect to be cited (Evaluation).
Policy¶
ControllerPolicy field |
Default | Effect |
|---|---|---|
max_subtasks |
4 |
cap on the plan size |
total_budget |
6.0 |
the budget split by gain per cost; sub-tasks that do not fit are skipped |
min_budget |
0.5 |
floor per kept sub-task (reduced when the budget is small) |
retrieval_k |
5 |
base number of records retrieved per sub-task (scaled by its budget, up to 2×) |
objectives |
coverage 0.4, density 0.2, agreement 0.1, recency 0.15, cost 0.15 | the weights \(w_i\) of eq. 7 |
constraints |
coverage ≥ 0.5, density ≥ 0.2 | the thresholds \(\epsilon_j\) of eq. 7 |
refine_rounds |
1 |
refinement rounds allowed while a constraint fails |
decay |
half_life:720 |
temporal policy of the memory (below) |
The decay spec grammar: none · half_life:<hours> (default 720 h = 30 days) · ebbinghaus:<hours> (stability, default 168 h) · validity (validity windows) · any of these joined with + for a product, e.g. half_life:2160+validity. parse_decay() turns a spec into a poincare_memory policy.
from poincare_orchestra import ControllerPolicy, parse_decay
print(ControllerPolicy().model_dump())
print(parse_decay("half_life:2160+validity"))
Writing your own scenario in YAML¶
This is scenarios/greenhouse.yaml from the repository, a complete scenario for a small tomato greenhouse. Every feature appears once: a hierarchy with labelled branches, a superseded record hidden by a validity window, dated observations, an entity (Bay 3), two profiles on two providers, two roles, two measurable tasks and a policy.
# A complete hand-written scenario. Everything the orchestrator needs is in this file:
# the knowledge hierarchy (parents before children), dated and versioned records,
# capability profiles, roles, example tasks with facets, and the controller policy.
name: greenhouse
title: Greenhouse crop advisor
description: Climate, pest and crop knowledge for a small tomato greenhouse, with dated scouting notes.
knowledge:
- id: greenhouse
text: Greenhouse operations knowledge base for tomatoes and cucumbers
meta: {domain: operations}
- id: climate
text: "Climate: temperature, humidity and ventilation targets"
parent: greenhouse
meta: {domain: climate} # a branch label; analysis reports it as a domain
- id: temp
text: Tomatoes set fruit best between 18 and 26 °C; above 32 °C pollen viability drops sharply
parent: climate
- id: humidity
text: Keep relative humidity below 85% at night to limit botrytis and leaf mould
parent: climate
- id: venting
text: Open roof vents when the canopy exceeds 24 °C; a 2 °C day-night difference improves fruit set
parent: climate
- id: pests
text: "Pests and diseases: identification and thresholds"
parent: greenhouse
meta: {domain: pests}
- id: whitefly
text: Whitefly adults sit on the underside of young leaves; treat when counts exceed 5 per leaf
parent: pests
- id: encarsia
text: Encarsia formosa parasitises whitefly nymphs; release weekly at 3 per square metre while counts rise
parent: pests
- id: botrytis
text: Botrytis (grey mould) starts on pruning wounds in humid nights; prune in the morning so wounds dry
parent: pests
- id: crops
text: "Crops: varieties and feeding"
parent: greenhouse
meta: {domain: crops}
- id: feed_old
text: Standard feed is EC 2.2 with potassium raised at fruit set (2025 schedule)
parent: crops
days_ago: 400
valid_until_days_ago: 60 # superseded: hidden by the validity policy
meta: {version: "2025"}
- id: feed_new
text: Standard feed is EC 2.5 with calcium raised in hot weeks (2026 schedule)
parent: crops
days_ago: 60
valid_from_days_ago: 60
meta: {version: "2026"}
- id: bays
text: Scouting notes per bay
parent: greenhouse
meta: {domain: scouting}
- id: bay3
text: "Bay 3: tomato, variety Merlice, planted in week 4" # "Bay 3" is an entity
parent: bays
- id: bay3_whitefly
text: "Bay 3: whitefly at 8 adults per leaf on the south row"
parent: bay3
days_ago: 2
meta: {kind: observation} # dated kinds: event, ticket, issue, case, observation
- id: bay3_humidity
text: "Bay 3: night humidity logged at 90% for three nights"
parent: bay3
days_ago: 1
meta: {kind: observation}
profiles:
- name: agronomist-model
provider: provider-a # the provider name a router looks for
domains: [climate, pests, crops]
reasoning: 0.85
reliability: 0.8
cost_per_call: 1.2
latency_s: 1.5
- name: scout-model
provider: provider-b
domains: [scouting, operations]
reasoning: 0.6
reliability: 0.9
cost_per_call: 0.5
latency_s: 0.5
roles:
- name: agronomist
description: Crop and climate agronomist
system_prompt: You are a greenhouse agronomist. Answer from the records; cite record ids.
domains: [climate, pests, crops]
profile: agronomist-model
- name: scout
description: Pest scout
system_prompt: You summarise scouting notes with their dates; cite record ids.
domains: [scouting]
profile: scout-model
tasks:
- question: Bay 3 shows whitefly and humid nights; what should we do this week?
expected_facets: [Encarsia, humidity]
- question: What is the current standard feed?
expected_facets: [EC 2.5]
policy:
max_subtasks: 3
total_budget: 6.0 # 5.0 would skip the third sub-task of the first task (see the docs)
decay: half_life:720+validity # 30-day half-life for dated records, validity windows enforced
constraints: {coverage: 0.5}
Check it, evaluate it and ask it something:
poincare-orchestra validate scenarios/greenhouse.yaml
poincare-orchestra eval scenarios/greenhouse.yaml
poincare-orchestra run scenarios/greenhouse.yaml "What is the current standard feed?"
ok: greenhouse — 16 knowledge items, 2 profiles, 2 roles, 2 tasks, decay=half_life:720+validity
greenhouse: 2 tasks, facet recall 1.00, mean J 0.746, constraints met 100%, throttle events 0
recall=1.00 J=0.778 subtasks=3 missing=[] Bay 3 shows whitefly and humid nights; what should we do this week?
recall=1.00 J=0.713 subtasks=3 missing=[] What is the current standard feed?
The feed question is answered from feed_new only: feed_old carries valid_until_days_ago: 60, so under the validity policy its time weight is zero and it is never retrieved.
The budget decides what gets investigated¶
The first task touches three branches (climate, scouting, pests), and the two agronomist sub-tasks cost 2.39 each. With total_budget: 6.0 all three sub-tasks fit (5.77) and the answer includes the encarsia record; with 5.0 the pests sub-task is skipped and the Encarsia facet is missed. You can see the skip rule from Python without editing the file:
from poincare_orchestra import Orchestrator, Scenario
scenario = Scenario.from_file("scenarios/greenhouse.yaml")
question = "Bay 3 shows whitefly and humid nights; what should we do this week?"
for budget in (5.0, 6.0):
policy = scenario.policy.model_copy(update={"total_budget": budget})
result = Orchestrator(scenario, policy=policy).run(question)
print(
budget,
[(st.id, st.domains[0], st.skipped) for st in result.plan.subtasks],
"Encarsia" in result.synthesis.answer,
)
5.0 [('q1', 'climate', False), ('q2', 'scouting', False), ('q3', 'pests', True)] False
6.0 [('q1', 'climate', False), ('q2', 'scouting', False), ('q3', 'pests', False)] True
Starting from a bundled scenario¶
poincare-orchestra export support_kb -o my_scenario.yaml writes a bundled scenario as YAML (JSON with a .json suffix) with defaults omitted. Edit it, then validate, run and eval it by path. YAML needs pip install 'poincare-orchestra[yaml]'; JSON always works.
Loading and saving from Python¶
from poincare_orchestra import Scenario, load_scenario
scenario = load_scenario("scenarios/greenhouse.yaml") # bundled name or path
assert scenario == Scenario.from_file("scenarios/greenhouse.yaml")
path = scenario.to_file("my_copy.json") # format follows the suffix; format="yaml" overrides
assert Scenario.from_file(path) == scenario
data = scenario.to_dict() # JSON-compatible dict, defaults and None omitted
assert Scenario.model_validate(data) == scenario
A scenario built in Python (as the bundled ones are, see src/poincare_orchestra/scenarios/support_kb.py) is passed straight to Orchestrator(scenario).
The three bundled scenarios¶
| Scenario | Knowledge (dated) | Profiles / roles / tasks | Policy | Demonstrates |
|---|---|---|---|---|
sprint_coaching |
29 (7) | 2 / 4 / 5 | total_budget: 8.0, 30-day half-life |
the paper's domain: physiology, biomechanics, strength work and athlete records; planning questions that span several branches; skipped sub-tasks; entity recognition ("Athlete A") |
clinical_anatomy |
24 (3) | 2 / 2 / 4 | max_subtasks: 3, total_budget: 4.0, decay: none |
a deep hierarchy (anatomy → system → joint → muscle) driving capability matching between an anatomist and a clinician; a tight budget; the one evaluation miss (Evaluation) |
support_kb |
22 (10) | 2 / 3 / 4 | max_subtasks: 3, coverage ≥ 0.5, decay: half_life:2160+validity |
versioned facts with validity windows: the 2.3 rate limit and Kafka requirement stop surfacing after release 2.4; tickets and issues as ephemeral records; a cheap tier-1 provider beside a tier-2 one |
The counts come from the scenarios themselves:
from poincare_orchestra import list_scenarios, load_scenario
for name in list_scenarios():
sc = load_scenario(name)
dated = sum(item.days_ago is not None for item in sc.knowledge)
print(name, len(sc.knowledge), dated, len(sc.profiles), len(sc.roles), len(sc.tasks))
The same three scenarios are exported as scenarios/sprint_coaching.yaml, scenarios/clinical_anatomy.yaml and scenarios/support_kb.yaml; a test asserts that each file loads to exactly the bundled Scenario.