mirror of
https://github.com/andrewyng/openworker.git
synced 2026-11-04 13:27:11 +00:00
The per-turn `<system-context>` block opened with `Now: YYYY-MM-DD HH:MM` (2c9fb74, 2026-08-20) so that absolute scheduling had a clock. The block is glued onto the last user message — the task prompt in a tool loop — and a provider's prompt cache is reusable only up to the first byte that differs, so every minute crossing rewrote message one and re-processed the whole conversation. 9d6a716 worked around it with a trailing-message shape, an Anthropic-only relocation of that message, and a `live_clock` switch that headless runs set off. This commit removes the cause instead and deletes the workarounds; the shape is the same on every provider again. - The block carries no clock, on any provider. What is left (folders, skill menu, mode notices, memory-off notice, disable countermand) changes only when the user changes something, so the outbound history is byte-identical turn to turn. - `current_time` tool (coworker/clock.py), registered for every persona: local time with offset and zone, the same instant in UTC, weekday. The environment block's "Today's date" now says it is a session-start snapshot and points at the tool. - `sleep_for(seconds)` is back beside `sleep_until` (a relative wait needs no clock arithmetic; the four lead personas' manifests still told the model to call sleep_for, which2c9fb74had removed). `sleep_until`'s description points at current_time for the date/timezone. Timer wake messages carry their fire time. - Deleted: `live_clock` config + COWORKER_LIVE_CLOCK, the trailing-message shape in `_outbound_messages`, `_relocate_ephemeral_note` in the Anthropic provider. The cache-diagnostics instrument (off by default) stays. Validation runs (one long tool-heavy task, one attempt each on Fable 5.1 and Kimi K3, compared with the 9d6a716 runs of the same task) are recorded on OPE-192. Tests: tests/test_clock_tool.py (new), tests/test_context_block_placement.py and tests/test_cache_breakpoint_placement.py rewritten for the single shape, tests/test_live_clock.py removed; self-wake, attachments, message-source and multiroot tests updated. Full suite unchanged against the Windows baseline.
70 lines
2.9 KiB
Python
70 lines
2.9 KiB
Python
"""OPE-192, Anthropic side. The engine glues its per-turn `<system-context>` block onto the
|
|
last user message (the same shape every provider gets) and this provider puts its cache
|
|
breakpoint on the last block of the final message. In a tool loop the final message is
|
|
pure tool results, so the breakpoint sits on a block that is identical next turn and the
|
|
whole conversation prefix is reused. Nothing is relocated any more: the converter sees
|
|
exactly what the engine sends."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from coworker.providers.anthropic_provider import _add_cache_breakpoints, convert_messages
|
|
|
|
BLOCK = "\n\n<system-context>\n(automatic per-turn context, not part of the user's message)\nAvailable directories: /app\n</system-context>"
|
|
|
|
|
|
def _tool_loop():
|
|
return [
|
|
{"role": "user", "content": "task" + BLOCK},
|
|
{"role": "assistant", "content": "", "tool_calls": [{"id": "c1", "function": {"name": "run_shell", "arguments": "{}"}}]},
|
|
{"role": "tool", "tool_call_id": "c1", "content": "out"},
|
|
]
|
|
|
|
|
|
def test_tool_loop_breakpoint_sits_on_the_tool_result_not_the_block():
|
|
system, conv = convert_messages(_tool_loop())
|
|
assert [m["role"] for m in conv] == ["user", "assistant", "user"]
|
|
assert conv[0]["content"][0]["text"] == "task" + BLOCK # sent as the engine built it
|
|
assert [b["type"] for b in conv[-1]["content"]] == ["tool_result"]
|
|
kw = {"system": system or "s", "messages": conv}
|
|
_add_cache_breakpoints(kw)
|
|
assert "cache_control" in kw["messages"][-1]["content"][-1]
|
|
assert "cache_control" not in kw["messages"][0]["content"][0]
|
|
|
|
|
|
def test_same_history_converts_identically_twice():
|
|
"""Byte-stable input, byte-stable request: the converter adds nothing of its own."""
|
|
assert convert_messages(_tool_loop()) == convert_messages(_tool_loop())
|
|
|
|
|
|
def test_chat_block_stays_on_the_newest_user_message():
|
|
system, conv = convert_messages(
|
|
[
|
|
{"role": "user", "content": "draft"},
|
|
{"role": "assistant", "content": "ok"},
|
|
{"role": "user", "content": "shorter" + BLOCK},
|
|
]
|
|
)
|
|
assert [m["role"] for m in conv] == ["user", "assistant", "user"]
|
|
assert conv[-1]["content"][0]["text"] == "shorter" + BLOCK
|
|
assert conv[0]["content"][0]["text"] == "draft" # earlier turns untouched
|
|
kw = {"system": system or "s", "messages": conv}
|
|
_add_cache_breakpoints(kw)
|
|
assert "cache_control" in kw["messages"][-1]["content"][-1]
|
|
|
|
|
|
def test_content_parts_keep_the_block_as_the_trailing_text_block():
|
|
system, conv = convert_messages(
|
|
[
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": "look"},
|
|
{"type": "text", "text": "closely"},
|
|
{"type": "text", "text": BLOCK},
|
|
],
|
|
},
|
|
]
|
|
)
|
|
assert len(conv) == 1
|
|
assert [b["text"] for b in conv[0]["content"]] == ["look", "closely", BLOCK]
|