Prompt T0.1 — Deterministic risk-appetite classifier, rendered as one prompt line
Part of prompt-building Tier 0 (tracking issue links the set). Principle for the whole tier: the model echoes; deterministic code computes. The composer model is qwen/qwen2.5-omni-7b (7B, see packages/arbitration-sdk/src/config.ts) — it has already been caught fabricating chainIds and deadlines (see the comment block in compose.ts around buildComposeMessages), and it gets MAX_COMPOSE_ATTEMPTS = 2 before template fallback, with a user watching a spinner. Anything that can be computed outside the model must be.
Why
Users express risk inclination in their prompt ("keep it safe", "I want max yield, I can take the risk") and today the prompt carries no signal for it — the model either ignores tone or, worse, free-associates on it. Classifying appetite is a task a 7B model should NOT be given (it mixes a classification task into a JSON-emission task, which raises malformed-output rates). It should be classified deterministically and handed to the model as a fact to condition on.
This classifier is also the input for T0.3 (risk-tiered band suggestions) and, later, the Tier 2 candidate-pair windowing — build it as a standalone pure module.
What
- A pure module
packages/arbitration-sdk/src/appetite.ts:
export type RiskAppetite = "conservative" | "neutral" | "aggressive";
export function classifyRiskAppetite(prompt: string): RiskAppetite;
- Keyword/phrase lexicon, case-insensitive, English-only for v0.
- conservative signals (starting set):
safe, safely, stable, conservative, cautious, careful, preserve, protect, low risk, don't want to lose
- aggressive signals (starting set):
aggressive, risky, high risk, degen, max yield, maximize, chase, yolo, can stomach, gamble
- Decision rule v0: count matches per side; more conservative matches →
conservative, more aggressive → aggressive; tie or zero matches → neutral. Keep the rule this dumb on purpose — it must be explainable in one sentence on stage.
- Negation is out of scope for v0 ("not too risky" will misfire toward aggressive). Acceptable: the output only shifts suggestions, it never overrides the budget or any validator invariant, and the user reviews before signing. Note the limitation in the module comment.
- One line rendered into the user message of the compose prompt (integration point today:
buildComposeMessages in packages/arbitration-sdk/src/compose.ts; see Coordination below), directly under USER PROMPT::
RISK APPETITE (derived deterministically from the user's words — do not re-infer it): NEUTRAL.
conservative = prefer wider bands and lower fees; neutral = balanced; aggressive = tighter bands acceptable when better fee capture justifies them.
The legend stays fixed; only the level varies. The model's job is to condition on the level, never to compute it.
Acceptance criteria
Coordination
A PR refactoring prompt building is in flight. The module itself is safe to build now (pure, standalone). Do the one-line prompt integration against whichever prompt builder that PR leaves standing; if packages/app/src/lib/compose/prompt.ts (the six-section F2 §9 contract) becomes canonical, the line belongs in its REQUEST section and PROMPT_VERSION must be bumped.
References
Prompt T0.1 — Deterministic risk-appetite classifier, rendered as one prompt line
Part of prompt-building Tier 0 (tracking issue links the set). Principle for the whole tier: the model echoes; deterministic code computes. The composer model is
qwen/qwen2.5-omni-7b(7B, seepackages/arbitration-sdk/src/config.ts) — it has already been caught fabricating chainIds and deadlines (see the comment block incompose.tsaroundbuildComposeMessages), and it getsMAX_COMPOSE_ATTEMPTS = 2before template fallback, with a user watching a spinner. Anything that can be computed outside the model must be.Why
Users express risk inclination in their prompt ("keep it safe", "I want max yield, I can take the risk") and today the prompt carries no signal for it — the model either ignores tone or, worse, free-associates on it. Classifying appetite is a task a 7B model should NOT be given (it mixes a classification task into a JSON-emission task, which raises malformed-output rates). It should be classified deterministically and handed to the model as a fact to condition on.
This classifier is also the input for T0.3 (risk-tiered band suggestions) and, later, the Tier 2 candidate-pair windowing — build it as a standalone pure module.
What
packages/arbitration-sdk/src/appetite.ts:safe,safely,stable,conservative,cautious,careful,preserve,protect,low risk,don't want to loseaggressive,risky,high risk,degen,max yield,maximize,chase,yolo,can stomach,gambleconservative, more aggressive →aggressive; tie or zero matches →neutral. Keep the rule this dumb on purpose — it must be explainable in one sentence on stage.buildComposeMessagesinpackages/arbitration-sdk/src/compose.ts; see Coordination below), directly underUSER PROMPT::The legend stays fixed; only the level varies. The model's job is to condition on the level, never to compute it.
Acceptance criteria
classifyRiskAppetiteis pure, side-effect free, and table-driven-tested: at least 5 fixture prompts per class, plus ties →neutral, empty string →neutral.Coordination
A PR refactoring prompt building is in flight. The module itself is safe to build now (pure, standalone). Do the one-line prompt integration against whichever prompt builder that PR leaves standing; if
packages/app/src/lib/compose/prompt.ts(the six-section F2 §9 contract) becomes canonical, the line belongs in its REQUEST section andPROMPT_VERSIONmust be bumped.References
packages/arbitration-sdk/src/compose.ts— current prompt builder + the fabrication history that motivates echo-don't-compute