Something a model could not do, and so wrote a note about instead
Asked to remind somebody every Monday, a model looked down its tool list, found notes_create described as "something worth having in a later conversation" and memory_add beginning with the word Remember, wrote a note, and reported that it had scheduled something. Every screen agreed with it. There was no scheduling tool at all -- the near-misses were the only thing there was to reach for, and nothing anywhere said the thing it was being asked for existed. The seam had been left open on purpose: Schedule.origin has defined ORIGIN_MODEL, with no writer, since scheduling shipped, and services/schedules.py says in its first line that it holds what the routes *and the tools* both need. This is the tool that was meant to go through it. Four of them, and a thin layer: rule.validate is still the one total normaliser the form and the compile share, schedules.create still writes the row and the task chat together, and rule.describe still says what came out. A second dialect for models would mean two definitions of "every other Tuesday" and one of them going quietly wrong. The result is that description, never "done". A schedule is invisible until it fires, which may be days away, so the sentence in the reply is the only moment anybody can check that Monday was read as Monday -- and the tool says so, in the text the model reads back. The list badges the ones nobody typed. Gated on schedule.use rather than a permission of its own: somebody who may set one up by hand may say so to a model instead, and a second checkbox beside the first would only ever be answered "the same as that one". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
09156230b3
commit
9761082fa1
@@ -130,6 +130,13 @@ FAMILY_IMAGE = "image"
|
||||
# must not hand out the notebook.
|
||||
FAMILY_REPORT = "report"
|
||||
|
||||
# Setting work up to happen later, or repeatedly. Its own family and emphatically
|
||||
# not part of `notes`: the line between them is the whole reason this exists. A
|
||||
# note is something to find again; a schedule is something that *happens*, and a
|
||||
# model with only the first reached for it when asked for the second -- wrote the
|
||||
# note, said it had scheduled something, and nothing anywhere disagreed.
|
||||
FAMILY_SCHEDULE = "schedule"
|
||||
|
||||
# The built-in families, in the order they are offered.
|
||||
FAMILIES = (
|
||||
FAMILY_SEARCH,
|
||||
@@ -142,6 +149,7 @@ FAMILIES = (
|
||||
FAMILY_ASK,
|
||||
FAMILY_IMAGE,
|
||||
FAMILY_REPORT,
|
||||
FAMILY_SCHEDULE,
|
||||
FAMILY_AGENT,
|
||||
)
|
||||
|
||||
@@ -1286,7 +1294,13 @@ REGISTRY: dict[str, ToolDef] = {
|
||||
|
||||
|
||||
def _family_allowed(
|
||||
family: str, *, config: dict, capabilities: dict, allowed: dict, images: bool = False
|
||||
family: str,
|
||||
*,
|
||||
config: dict,
|
||||
capabilities: dict,
|
||||
allowed: dict,
|
||||
images: bool = False,
|
||||
schedules: bool = False,
|
||||
) -> bool:
|
||||
"""Whether one family is on for this chat.
|
||||
|
||||
@@ -1319,6 +1333,15 @@ def _family_allowed(
|
||||
# the shape `resolve_tools` already refuses for `skill_get` with an
|
||||
# empty library. `settings_store.images_ready` answers all three.
|
||||
return bool(allowed.get("tools.image") and images)
|
||||
if gate == FAMILY_SCHEDULE:
|
||||
# `schedule.use` rather than a `tools.schedule` of its own: a reader who
|
||||
# may set a schedule up by hand may say so to a model instead, and a
|
||||
# second permission beside the first would only ever be answered "the
|
||||
# same as that one". `schedules` is the instance switch, passed in for
|
||||
# the reason `images` is -- an instance with scheduling off must not
|
||||
# offer this at all, or a model spends a round being told the tool it
|
||||
# was handed does not work.
|
||||
return bool(allowed.get("schedule.use") and schedules)
|
||||
if gate in (
|
||||
FAMILY_CUSTOM,
|
||||
FAMILY_MCP,
|
||||
@@ -1375,6 +1398,20 @@ def _agent_defs(db: DBSession, chat: Chat | None, user: User | None) -> list[Too
|
||||
return agent_tools.tool_defs(context)
|
||||
|
||||
|
||||
def _schedule_defs() -> list[ToolDef]:
|
||||
"""The scheduling tools.
|
||||
|
||||
Not in `REGISTRY` even though they need no rows and no settings to build,
|
||||
because the module they live in imports `services/tools.py` for `ToolDef`
|
||||
and the risk constants -- so importing it back at module scope is a cycle.
|
||||
A function keeps the import inside the call, which is the same shape
|
||||
`_agent_defs` and `_image_defs` already have.
|
||||
"""
|
||||
from lembas.services.schedule import tool as schedule_tool
|
||||
|
||||
return schedule_tool.tool_defs()
|
||||
|
||||
|
||||
def _image_defs(db: DBSession, values: dict | None = None) -> list[ToolDef]:
|
||||
"""The image tool, whose schema carries this instance's own choices.
|
||||
|
||||
@@ -1417,9 +1454,19 @@ def registry(db: DBSession) -> dict[str, ToolDef]:
|
||||
working on. The same omission cost custom tools their guidance once already.
|
||||
"""
|
||||
from lembas.services.agent import tools as agent_tools
|
||||
from lembas.services.schedule import tool as schedule_tool
|
||||
|
||||
return _book(
|
||||
[*_row_defs(db, None, everything=True), *agent_tools.tool_defs(), *_image_defs(db)]
|
||||
[
|
||||
*_row_defs(db, None, everything=True),
|
||||
*agent_tools.tool_defs(),
|
||||
*_image_defs(db),
|
||||
# Listed here, ungated, or `harness._families` cannot map
|
||||
# `schedule_create` back to a family and the guidance never
|
||||
# reaches the model. That omission has cost two features their
|
||||
# instructions already.
|
||||
*schedule_tool.tool_defs(),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -1446,6 +1493,7 @@ def resolve_tools(db: DBSession, chat: Chat, user: User | None) -> ToolSet:
|
||||
config = settings_store.search(db)
|
||||
image_values = settings_store.images(db)
|
||||
images_ready = settings_store.images_ready(db)
|
||||
schedules_on = bool(settings_store.schedules(db).get("enabled"))
|
||||
|
||||
# Resolved against what this reader may see, not against everything that
|
||||
# exists: a tool restricted to a group is not offered outside it. The image
|
||||
@@ -1457,6 +1505,7 @@ def resolve_tools(db: DBSession, chat: Chat, user: User | None) -> ToolSet:
|
||||
*_row_defs(db, user),
|
||||
*_agent_defs(db, chat, user),
|
||||
*(_image_defs(db, image_values) if images_ready else []),
|
||||
*(_schedule_defs() if schedules_on else []),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1490,6 +1539,7 @@ def resolve_tools(db: DBSession, chat: Chat, user: User | None) -> ToolSet:
|
||||
capabilities=capabilities,
|
||||
allowed=allowed,
|
||||
images=images_ready,
|
||||
schedules=schedules_on,
|
||||
)
|
||||
and gate_of(tool.family) not in off
|
||||
# Nothing to read and nothing to improve. Offering `skill_get` with
|
||||
|
||||
Reference in New Issue
Block a user