Models that know about each other, and have a self
Three features sharing one idea: a model here started from nothing every
conversation and had no notion that anything else existed.
THE ROSTER. `chat.roster_block` builds one line per model this *person* can
reach -- through `permissions.models_visible_to`, never the table -- and
`{{model_roster}}` carries it, gated on the `friend` family for the reason the
memories block is gated on `memory`: a list of peers a model cannot talk to is
context spent on nothing, and one checkbox is then the whole switch. New
`Model.notes` column, a column and not a `capabilities_json` key for the reason
`context_length` and `reasoning_efforts` both carry.
ASKING A FRIEND. A second entry point in `services/subagent.py` rather than a
second module, so one place still owns the bounds and the lifecycle. `_create_
child` takes the friend's (model_id, connection_id) *pair*, because Model is
unique on both and an id alone does not say which endpoint. Three things differ
from a helper: the effort is the friend's own default and never the parent's (the
1.3.0 bug by another door -- the vocabularies differ and a level a model does not
take raises inside its chat template), the chat is ordinary even when the asker's
is an agent chat, and `scope_json["role"]` marks it so `core.friend` speaks
instead of `core.subagent`. `friend` joins the unattended withdrawal set: a
friend that could ask a friend is the same unbounded fan-out in politer clothes.
Budget, concurrency and quota are shared with helpers, so one reply cannot spend
the allowance twice.
PERSONALITY. One table, two roles, `owner_id IS NULL` the discriminator: the
model's own persona, and its read of one person. Keyed on the model's *text* id
with no foreign key, because "Test & refresh" deletes a model the endpoint has
stopped listing and a personality must not be collateral. `PersonaRevision`
copies SkillRevision, and so does the argument: the safety story for a model
rewriting itself is a record and a way back, not a gate. The reflection is shown
to the person it is about, in their own settings, which is the whole of why
keeping one is acceptable. `persona` is withdrawn from any unattended chat --
a helper's task, a friend's question and a schedule's instruction are all words
nobody watched being written.
Two bugs found while reading for this, both silent:
`review_model_id` stored a `Model` primary key, so a refresh taken while an
endpoint was not listing that model unset the administrator's choice -- and
`_reviewer` then fell back to the chat's own model, so pictures were judged by
a model nobody chose. Now the text id, with the primary key still accepted.
`_messages_after` used a bare `>` on `created_at`, so a row sharing the edited
turn's microsecond survived a rewind -- and `_send` writes a user turn and its
placeholder back to back, which is exactly that tie. Deliberately NOT
`thread_tail`'s `(created_at, id)` tiebreak: ids are random UUIDs, so that
settles a tie by coin toss. A tie now reads as "later", which is the safe
direction for an operation whose purpose is to discard what follows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -454,6 +454,13 @@
|
||||
research fan out instead of queueing. This applies to ordinary chats as
|
||||
much as agent ones.
|
||||
</p>
|
||||
<p class="field__hint">
|
||||
<strong>Asking another model a question uses the same switch and the same
|
||||
allowance below</strong>, because it costs the same thing: one reply
|
||||
setting another reply going. Which people may do it is a separate
|
||||
permission — <strong>Ask another model</strong> — and which models may is a
|
||||
switch on each model's own page.
|
||||
</p>
|
||||
|
||||
<div class="alert">
|
||||
{{ icon("shield", "icon--sm") }}
|
||||
@@ -482,13 +489,17 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="sub_max_per_reply">Most helpers one reply may send</label>
|
||||
<label class="field__label" for="sub_max_per_reply">
|
||||
Most helpers one reply may send
|
||||
</label>
|
||||
<input class="input" id="sub_max_per_reply" name="max_per_reply"
|
||||
type="number" min="1" max="20" step="1"
|
||||
value="{{ subagents.max_per_reply }}">
|
||||
<p class="field__hint">
|
||||
Fanning out across a handful of independent questions is what this is
|
||||
for. A reply that wants twenty has misread the tool.
|
||||
for. A reply that wants twenty has misread the tool. Questions put to
|
||||
other models count against this same number, so one reply cannot spend
|
||||
the allowance twice.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -259,8 +259,12 @@
|
||||
<option value="">The chat's own model, when it has vision</option>
|
||||
{% for model in vision_models %}
|
||||
{% if model.capabilities_json.get("vision") %}
|
||||
<option value="{{ model.id }}"
|
||||
{{ 'selected' if values.review_model_id == model.id }}>
|
||||
{# The model's own id, not the row's primary key: "Test & refresh"
|
||||
deletes a model the endpoint has stopped listing and gives it a new
|
||||
primary key when it comes back, which silently unset this. The same
|
||||
reasoning Chat.model_id carries. #}
|
||||
<option value="{{ model.model_id }}"
|
||||
{{ 'selected' if values.review_model_id == model.model_id }}>
|
||||
{{ model.label }}
|
||||
</option>
|
||||
{% endif %}
|
||||
|
||||
@@ -213,7 +213,22 @@
|
||||
<label class="field__label" for="description">Description</label>
|
||||
<textarea class="textarea" id="description" name="description" rows="2"
|
||||
placeholder="What is this model good at?">{{ model.description }}</textarea>
|
||||
<p class="field__hint">Shown in the chat settings panel and your users' settings.</p>
|
||||
<p class="field__hint">
|
||||
Shown in the chat settings panel and your users' settings — and, if any
|
||||
model here may ask another one a question, read by those models too.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="notes">Facts for other models</label>
|
||||
<textarea class="textarea" id="notes" name="notes" rows="3"
|
||||
placeholder="Parameters, quantisation, a benchmark figure, what it is bad at">{{ model.notes }}</textarea>
|
||||
<p class="field__hint">
|
||||
Never shown to a person. It goes into the list of the other models that a
|
||||
model sees when it is allowed to ask one of them a question, so write what
|
||||
would help it choose: size, what this one is good and bad at, a score you
|
||||
trust. Leave it empty and the description above carries that on its own.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -343,4 +358,76 @@
|
||||
<a class="btn btn--ghost" href="/admin/models">Back to all models</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{# Outside the form above, and it has to be: two forms cannot nest, and this one
|
||||
posts somewhere else. See the note beside the Detect button. #}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Personality</h2>
|
||||
<p class="card__lede">
|
||||
Who this model is, carried into every conversation rather than given to it for
|
||||
one. Different from the system prompt above: that is an instruction you write,
|
||||
this is a character it can be — and, with
|
||||
<strong>Edit its own personality</strong> ticked, one it can rewrite itself.
|
||||
Every version is kept below.
|
||||
</p>
|
||||
<form method="post" action="/admin/models/{{ model.id }}/persona">
|
||||
<div class="field">
|
||||
<label class="field__label visually-hidden" for="persona">Personality</label>
|
||||
<textarea class="textarea" id="persona" name="content" rows="6"
|
||||
placeholder="Nothing yet. Write one, or let the model write its own."
|
||||
>{{ persona.content if persona else "" }}</textarea>
|
||||
<p class="field__hint">
|
||||
Up to {{ persona_limit }} characters, in the first person. Empty removes it
|
||||
and its history. It is sent on every request, so length here costs the same
|
||||
as length in the system prompt.
|
||||
</p>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn" type="submit">Save personality</button>
|
||||
{% if persona and persona.author == "model" %}
|
||||
<span class="badge badge--leaf">last written by the model</span>
|
||||
{% elif persona %}
|
||||
<span class="badge">last written here</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% if persona and persona.revisions %}
|
||||
<section class="card">
|
||||
<h2 class="card__title">
|
||||
Earlier personalities <span class="badge">{{ persona.revisions|length }}</span>
|
||||
</h2>
|
||||
<p class="card__lede">
|
||||
What it said before each change. This is the whole safety story for a model
|
||||
that may rewrite itself: not a gate, but a record and a way back.
|
||||
</p>
|
||||
<ul class="model-list">
|
||||
{% for revision in persona.revisions %}
|
||||
<li class="model-list__item">
|
||||
<div style="min-width: 0">
|
||||
<strong>{{ revision.created_at.strftime("%Y-%m-%d %H:%M") }}</strong>
|
||||
{% if revision.author == "model" %}
|
||||
<span class="badge badge--leaf">model</span>
|
||||
{% else %}
|
||||
<span class="badge">you</span>
|
||||
{% endif %}
|
||||
{% if revision.note %}<div class="text-xs faint">{{ revision.note }}</div>{% endif %}
|
||||
<div class="text-xs faint">
|
||||
{{ revision.content[:200] }}{{ "…" if revision.content|length > 200 }}
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/admin/models/{{ model.id }}/persona/revert"
|
||||
data-confirm="Put this version back? The current one is kept in the history."
|
||||
data-confirm-label="Restore" data-confirm-danger="false">
|
||||
<input type="hidden" name="revision_id" value="{{ revision.id }}">
|
||||
<button class="btn btn--sm" type="submit">
|
||||
{{ icon("refresh", "icon--sm") }} Restore
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -420,6 +420,45 @@
|
||||
message.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{# What each model has made of you, in its own words. Shown whether or
|
||||
not any model is still allowed to write one: a model whose
|
||||
permission was taken away has not forgotten, and this is the only
|
||||
place the text can be read or removed. #}
|
||||
{% if reflections %}
|
||||
<div class="card">
|
||||
<h2 class="card__title">
|
||||
What models make of you
|
||||
<span class="badge">{{ reflections|length }}</span>
|
||||
</h2>
|
||||
<p class="card__lede">
|
||||
Each model's own impression of how you work, kept by that model and
|
||||
read back to it in every conversation. Opinions rather than facts,
|
||||
and each one is that model's alone — the others cannot see it, and
|
||||
neither can anybody else. Delete any of them; it will form another
|
||||
if it has reason to.
|
||||
</p>
|
||||
<ul class="model-list">
|
||||
{% for reflection in reflections %}
|
||||
<li class="model-list__item">
|
||||
<div style="min-width: 0">
|
||||
<strong>{{ reflection.model_key }}</strong>
|
||||
<div class="text-sm">{{ reflection.content }}</div>
|
||||
</div>
|
||||
<form method="post"
|
||||
action="/api/library/reflections/{{ reflection.id }}/delete">
|
||||
<button class="btn btn--sm btn--danger" type="submit"
|
||||
data-confirm-button="Delete what this model makes of you?"
|
||||
data-confirm-title="Delete"
|
||||
aria-label="Delete this" title="Delete this">
|
||||
{{ icon("trash", "icon--sm") }}
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user