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:
@@ -295,3 +295,111 @@ def test_a_queued_turn_is_not_lost_when_it_was_forced(db, user_id, vision_chat):
|
||||
|
||||
assert chats_api._reply_in_flight(db, vision_chat) is True
|
||||
assert db.scalar(select(Attachment)) is None
|
||||
|
||||
|
||||
# --- Which model reviews what was drawn ---------------------------------------
|
||||
#
|
||||
# The reviewer is named in the instance settings, and it used to be named by the
|
||||
# `Model` row's primary key. "Test & refresh" on the connection screen deletes
|
||||
# any model the endpoint has stopped listing and recreates it when it comes back
|
||||
# with a new primary key -- so one refresh taken while an endpoint happened to be
|
||||
# loading something else silently unset the administrator's choice. It did not
|
||||
# fail: `_reviewer` falls back to the chat's own model, so the picture was
|
||||
# reviewed by a different model than the one chosen, with nothing saying so.
|
||||
def _reviewer_of(db, chat, settings: dict):
|
||||
from lembas.db.models import User
|
||||
from lembas.services import tools as tools_service
|
||||
from lembas.services.images import tool as image_tool
|
||||
|
||||
user = db.get(User, chat.user_id)
|
||||
context = tools_service.context_for(db, user, chat, tools=tools_service.ToolSet())
|
||||
context.image_config = settings
|
||||
return image_tool._reviewer(context)
|
||||
|
||||
|
||||
def test_the_reviewer_is_named_by_the_models_own_id(db, vision_chat):
|
||||
db.add(
|
||||
Model(
|
||||
connection_id=vision_chat.connection_id,
|
||||
model_id="reviewer",
|
||||
capabilities_json={"vision": True},
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
resolved = _reviewer_of(
|
||||
db, vision_chat, {"review_enabled": True, "review_model_id": "reviewer"}
|
||||
)
|
||||
|
||||
assert resolved is not None
|
||||
assert resolved[1] == "reviewer"
|
||||
|
||||
|
||||
def test_the_reviewer_survives_its_row_being_deleted_and_remade(db, vision_chat):
|
||||
"""The refresh case, end to end: the row goes, an identical one arrives with
|
||||
a different primary key, and the choice still resolves."""
|
||||
db.add(
|
||||
Model(
|
||||
connection_id=vision_chat.connection_id,
|
||||
model_id="reviewer",
|
||||
capabilities_json={"vision": True},
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
settings = {"review_enabled": True, "review_model_id": "reviewer"}
|
||||
assert _reviewer_of(db, vision_chat, settings)[1] == "reviewer"
|
||||
|
||||
row = db.scalar(select(Model).where(Model.model_id == "reviewer"))
|
||||
connection_id = row.connection_id
|
||||
db.delete(row)
|
||||
db.commit()
|
||||
db.add(
|
||||
Model(
|
||||
connection_id=connection_id,
|
||||
model_id="reviewer",
|
||||
capabilities_json={"vision": True},
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
assert _reviewer_of(db, vision_chat, settings)[1] == "reviewer"
|
||||
|
||||
|
||||
def test_a_primary_key_stored_by_an_older_release_still_resolves(db, vision_chat):
|
||||
"""The value written before the id was the rule is a primary key, and an
|
||||
instance that never touches the setting again must keep working."""
|
||||
db.add(
|
||||
Model(
|
||||
connection_id=vision_chat.connection_id,
|
||||
model_id="reviewer",
|
||||
capabilities_json={"vision": True},
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
row = db.scalar(select(Model).where(Model.model_id == "reviewer"))
|
||||
|
||||
resolved = _reviewer_of(
|
||||
db, vision_chat, {"review_enabled": True, "review_model_id": row.id}
|
||||
)
|
||||
|
||||
assert resolved is not None
|
||||
assert resolved[1] == "reviewer"
|
||||
|
||||
|
||||
def test_the_admin_page_offers_the_models_own_id_as_the_value(client, db, vision_chat):
|
||||
"""The other half. Storing the primary key is what created the problem, so
|
||||
the form must not put one back."""
|
||||
db.add(
|
||||
Model(
|
||||
connection_id=vision_chat.connection_id,
|
||||
model_id="reviewer",
|
||||
display_name="Reviewer",
|
||||
capabilities_json={"vision": True},
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
page = client.get("/admin/images").text
|
||||
row = db.scalar(select(Model).where(Model.model_id == "reviewer"))
|
||||
assert 'value="reviewer"' in page
|
||||
assert f'value="{row.id}"' not in page
|
||||
|
||||
Reference in New Issue
Block a user