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:
@@ -40,6 +40,7 @@ from sqlalchemy.orm import Session as DBSession
|
||||
|
||||
from lembas.db.models import KIND_TASK, User
|
||||
from lembas.services import branding, prompts, settings_store
|
||||
from lembas.services import personas as personas_service
|
||||
from lembas.services.library import memories as memories_service
|
||||
from lembas.services.library import skills as skills_service
|
||||
from lembas.services.schedule import clock
|
||||
@@ -267,10 +268,31 @@ def context_variables(
|
||||
# though both mean "nobody is reading": the two say different things to
|
||||
# a model, and one fragment covering both would have to say neither.
|
||||
"subagent": "",
|
||||
# Set only in the chat of a model that has been asked a question by
|
||||
# another one, and the gate on `core.friend`. A third way of being
|
||||
# somebody's child, and a third thing to say: a helper is doing a job, a
|
||||
# scheduled task is running unwatched, and this one is being asked for an
|
||||
# opinion. One fragment covering all three would say nothing useful to
|
||||
# any of them.
|
||||
"friend": "",
|
||||
# Who else is here. Filled below, where the chat's own model is known --
|
||||
# a model does not need telling that it exists.
|
||||
"model_roster": "",
|
||||
# Who this model is, and what it makes of the person in front of it.
|
||||
# Family-gated like the memories block, and for the same two reasons: a
|
||||
# model that may not keep either has no business being handed them, and
|
||||
# the query should not happen at all on an instance that does not use
|
||||
# this.
|
||||
"persona": "",
|
||||
"person_view": "",
|
||||
}
|
||||
|
||||
if chat is not None:
|
||||
# `ROLE_FRIEND` is imported here rather than at the top for the reason
|
||||
# `chat_service` is: `services/tools.py` imports the subagent module and
|
||||
# this one, and a top-level import back is a cycle.
|
||||
from lembas.services import chat as chat_service
|
||||
from lembas.services.subagent import ROLE_FRIEND
|
||||
|
||||
model = chat_service.model_for(db, chat)
|
||||
values["model_name"] = model.label if model is not None else chat.model_id
|
||||
@@ -297,7 +319,27 @@ def context_variables(
|
||||
# Not gated on a family either, and for the same reason: what has to
|
||||
# reach a helper is that it is one. A column read, no query.
|
||||
if chat.parent_chat_id:
|
||||
values["subagent"] = "yes"
|
||||
# Which *kind* of child, because the two read differently. A friend
|
||||
# is marked on its scope by `subagent._create_child`; anything else
|
||||
# with a parent is a helper.
|
||||
if (chat.scope_json or {}).get("role") == ROLE_FRIEND:
|
||||
values["friend"] = "yes"
|
||||
else:
|
||||
values["subagent"] = "yes"
|
||||
|
||||
# Only for a model that can actually ask one of them something. A list
|
||||
# of peers it cannot reach is context spent on nothing -- the same
|
||||
# argument that gates the memories block on the memory family, and the
|
||||
# reason the roster and the tool are one checkbox rather than two.
|
||||
if "friend" in families:
|
||||
values["model_roster"] = chat_service.roster_block(
|
||||
db, user, exclude=chat.model_id
|
||||
)
|
||||
|
||||
if "persona" in families:
|
||||
key = chat.model_id
|
||||
values["persona"] = personas_service.block(db, key, None)
|
||||
values["person_view"] = personas_service.block(db, key, user)
|
||||
|
||||
return values
|
||||
|
||||
|
||||
Reference in New Issue
Block a user