A plan it can see is a plan it can keep
Plan mode produced a flat list of steps and then forgot it. Nothing told the
model to look before proposing, nothing let it ask when the scope was
ambiguous, and -- worst -- once execution started the plan was not in the prompt
at all, so it could not have kept it current if it had wanted to.
The shape is findings, objectives and phases of tasks now. Findings are the part
people skip and the part that makes a plan worth reading: what is actually
there, what surprised you, what the plan is working around. Plan mode is told to
research first and to ask with ask_user when the scope is genuinely ambiguous,
in one question rather than three.
steps is still always written, flattened from every phase in order. That is the
whole of the compatibility story: execute_plan reads it and needed no change,
and every row already on disk still works. services/plans.py:normalise is the
only place that knows version 1 existed -- a {title, steps} row comes back as
one phase, so the card, the harness and the Execute button have one shape to
deal with rather than two.
Chat.plan_message_id is what puts the plan in front of the model each turn, with
one primary-key lookup rather than a scan for "the newest message carrying a
plan" -- context_variables is synchronous and sits on the request path.
plan_update is offered only once there is a plan, because a tool for changing
something that does not exist costs a round to find out.
It is RISK_READ, and that sits in tension with notes_edit being RISK_WRITE, so:
risk is what a tool does to the world, and the world the four modes govern is
the machine. This cannot touch it. RISK_WRITE would put an approval card on
screen every time a task was ticked off -- four cards to carry out a four-task
plan, each approving a bookkeeping entry -- which is exactly the interruption
batching exists to prevent. A note is a durable artefact of the reader's that
outlives the chat; this is the chat's own record of what it is doing, nearer to
generation.status. An administrator who disagrees puts it in deny_default.
One thing that nearly went wrong quietly. A runner cannot write the message row,
since _persist is the single writer -- so plan_update returns the merged plan on
its event and the loop carries it. Both calls in a round would then have read
the same stale plan from the database and the second would have won. They merge
into AgentContext.plan instead, the snapshot seeded once when the context is
resolved. Both tools write event["plan"] so _persist stays one writer with one
rule; only plan_submit sets plan_final, which is what withdraws the tools.
The card does not re-render in place. The newest bubble carries the current plan
and older ones carry the plan as it was then -- that is what a transcript is
for, it needs no streaming machinery, and it makes "what did it think at step
three" answerable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,27 +1,77 @@
|
||||
{% from "_macros.html" import icon %}
|
||||
{#
|
||||
A plan the model proposed in Plan mode.
|
||||
A plan: what was found, what it is for, and the work as phases of tasks.
|
||||
|
||||
Rendered from `message.plan_json` rather than parsed back out of the prose, so
|
||||
the Execute button sends exactly what was proposed. Every line is model output
|
||||
and is escaped; a step is shown as text, never as Markdown, because a plan is
|
||||
the last thing that should be able to emit a link.
|
||||
Rendered from `message.plan` -- the property, which normalises -- rather than
|
||||
from `plan_json`, so a row written before version 2 comes through as one phase
|
||||
and this template never sees two shapes. The Execute button still posts the
|
||||
message id and the server still reads the flattened `steps`, so it sends
|
||||
exactly what was proposed.
|
||||
|
||||
Every line is model output and is escaped; a task is shown as text, never as
|
||||
Markdown, because a plan is the last thing that should be able to emit a link.
|
||||
|
||||
A status is a CLASS, never a character in the text: a tick written into the
|
||||
string would be indistinguishable from a tick the model wrote itself.
|
||||
|
||||
It does not re-render in place as work goes on. The newest bubble carries the
|
||||
current plan and older ones carry the plan as it was then -- that is what a
|
||||
transcript is, and it makes "what did it think at step three" answerable.
|
||||
|
||||
Execute switches the chat to Edit, never Auto -- the plan was written under a
|
||||
mode where every command stopped for approval, and a button that also removed
|
||||
the asking is not the button anybody pressed. The confirm dialog says so.
|
||||
#}
|
||||
{% set plan = message.plan %}
|
||||
<section class="plan">
|
||||
<h3 class="plan__title">
|
||||
{{ icon("check", "icon--sm") }}
|
||||
{{ message.plan_json.title or "A plan" }}
|
||||
{{ plan.title or "A plan" }}
|
||||
</h3>
|
||||
|
||||
<ol class="plan__steps">
|
||||
{% for step in message.plan_json.steps %}
|
||||
<li>{{ step }}</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% if plan.summary %}
|
||||
<p class="plan__summary">{{ plan.summary }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if plan.findings %}
|
||||
<div class="plan__section">
|
||||
<h4 class="plan__heading">What was found</h4>
|
||||
<ul class="plan__findings">
|
||||
{% for finding in plan.findings %}
|
||||
<li>{{ finding.text }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if plan.objectives %}
|
||||
<div class="plan__section">
|
||||
<h4 class="plan__heading">What it is for</h4>
|
||||
<ul class="plan__objectives">
|
||||
{% for objective in plan.objectives %}
|
||||
<li class="plan__item plan__item--{{ objective.status }}">{{ objective.text }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for phase in plan.phases %}
|
||||
<div class="plan__phase plan__phase--{{ phase.status }}">
|
||||
{# A single unnamed phase is what a version-1 row becomes, and heading it
|
||||
"Plan" above a plan headed "Plan" reads as a mistake. #}
|
||||
{% if plan.phases | length > 1 or phase.title != "Plan" %}
|
||||
<h4 class="plan__heading">{{ phase.title }}</h4>
|
||||
{% endif %}
|
||||
<ol class="plan__steps">
|
||||
{% for task in phase.tasks %}
|
||||
<li class="plan__item plan__item--{{ task.status }}" data-status="{{ task.status }}">
|
||||
{{ task.text }}
|
||||
{% if task.note %}<span class="plan__note-inline">{{ task.note }}</span>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if chat.kind == "agent" %}
|
||||
<div class="btn-row">
|
||||
|
||||
Reference in New Issue
Block a user