Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
124 commits
Select commit Hold shift + click to select a range
05e468a
Sync main back into staging after release
mindsdb-release-train[bot] Sep 21, 2026
1d63962
[ENG-2807] Parse the datasource block and provide the typed cloud hel…
tino097 Sep 21, 2026
74735c9
ENG-2689: Classify an oversized-image refusal as permanent, not trans…
alecantu7 Sep 22, 2026
fae9d0b
[ENG-2175] Check decks and Word files before they are handed over (#492)
Dav1dF Sep 23, 2026
883d1c1
[ENG-2121] Tie each completed turn to the account that ran it (#493)
Dav1dF Sep 23, 2026
8f3e99a
ENG-2892: treat a gateway alias echo as "served model not disclosed",…
alecantu7 Sep 23, 2026
093409b
ENG-2419: Name the kill in single-line readers of a scratchpad cell e…
alecantu7 Sep 24, 2026
19e16f4
fix: pin click to >=8.3.3 to clear command-injection CVE (#494)
martyna-mindsdb Sep 24, 2026
aaa4d09
fix: pin anyio to >=4.14.2 to clear IDNA TLS cert-spoofing CVE (#495)
martyna-mindsdb Sep 24, 2026
4d3d865
fix: floor pydantic-settings to >=2.14.2 to clear secrets_dir symlink…
martyna-mindsdb Sep 24, 2026
59cb11c
ENG-1827: emit the turn's compaction result from the cloud entrypoint…
ea-rus Sep 24, 2026
4840c61
feat: generate_artifact — one-tool web artifact pipeline (ENG-968, EN…
StpMax Sep 24, 2026
9118ccd
ENG-1816: MCP client in anton (Stage 1) (#478)
martyna-mindsdb Sep 24, 2026
6c785da
[ENG-2807] Reach the datasource gateway through the turn's own infere…
tino097 Sep 24, 2026
aa2d15a
ENG-2863: run the verifier live matrix only when the verifier changed…
alecantu7 Sep 24, 2026
a61c5b0
fix(scratchpad): stop reporting explainability bookkeeping errors as …
alecantu7 Sep 24, 2026
fb442a4
Scrub Google AQ. authorization keys at chat input (ENG-2684) (#498)
hamishfagg Sep 25, 2026
8bb7f68
test(skills): drop the duplicated BROWSER STORAGE marker (ENG-2875) (…
StpMax Sep 25, 2026
ff5008f
fix(cloud_turn): never rate-limit tool progress lines (ENG-2981) (#507)
StpMax Sep 25, 2026
94b3ef0
fix(llm): raise one exception type per MindsHub billing stop (ENG-813…
lucas-koontz Sep 27, 2026
6da9089
Sync main into staging
mindsdb-release-train[bot] Sep 27, 2026
5cad2f8
fix(minds_client): keep only chat models from the catalog (ENG-1958) …
lucas-koontz Sep 28, 2026
e162a0d
Use jev
ZoranPandovski Sep 28, 2026
6f0b476
Add jev settings
ZoranPandovski Sep 28, 2026
b47ccfe
Add tests
ZoranPandovski Sep 28, 2026
f758266
Add jev
ZoranPandovski Sep 28, 2026
87d37f2
Rmv stupid comments
ZoranPandovski Sep 28, 2026
ea53610
ADddress comments
ZoranPandovski Sep 28, 2026
4e852ba
Merge pull request #510 from mindsdb/feature/jev-completion-verifier
ZoranPandovski Sep 28, 2026
ab59301
MAke seed optional
ZoranPandovski Sep 28, 2026
606ba5b
On cloud off
ZoranPandovski Sep 28, 2026
32dbaa0
Add tests
ZoranPandovski Sep 28, 2026
628211d
Why do we even need seed?
ZoranPandovski Sep 28, 2026
ad4e681
Merge pull request #512 from mindsdb/bugfix/pip-seed-opt
ZoranPandovski Sep 28, 2026
20343c5
[ENG-2193] Let a host share the completion-verifier latch across sess…
tino097 Sep 28, 2026
cd0c609
Match example.com's current body text in the live fetch test (#515)
martyna-mindsdb Sep 28, 2026
b2fd71d
Pass Google Ads' developer token through the cloud data vault (#514)
martyna-mindsdb Sep 28, 2026
446e614
Bytoced compile
ZoranPandovski Sep 29, 2026
45e229b
Merge pull request #516 from mindsdb/feature/image-bytecode
ZoranPandovski Sep 30, 2026
89526a2
FIlter in paralel
ZoranPandovski Sep 30, 2026
3def488
Fix memory
ZoranPandovski Sep 30, 2026
ab7f1f4
Fix test
ZoranPandovski Sep 30, 2026
6c23c8c
Memoryy test
ZoranPandovski Sep 30, 2026
15d567b
Render connector usage notes for connected engines (#519)
StpMax Oct 2, 2026
4ddab4d
ENG-1985: single source of truth for artifact non-content names (#521)
StpMax Oct 2, 2026
c8dff67
feat(cloud_turn): interactive ask_user in cloud turns (ENG-3049) (#513)
StpMax Oct 2, 2026
860e9c6
fix(scratchpad): report venv failure facts instead of a misleading fi…
StpMax Oct 2, 2026
770515f
Sync main into staging
mindsdb-release-train[bot] Oct 4, 2026
b6b5faa
Responses API: report truncation and failures, forward tool images, s…
ianu82 Oct 5, 2026
e875e13
fix(scratchpad): build a fresh model client for each helper call (ENG…
lucas-koontz Oct 5, 2026
6a32487
Sync main into staging
mindsdb-release-train[bot] Oct 5, 2026
34aee12
Merge pull request #518 from mindsdb/feature/eng-3093-first-token-lat…
ZoranPandovski Oct 5, 2026
d3c384f
Cache the shared system prompt across sessions on OpenAI Responses
ianu82 Oct 5, 2026
6f0fd40
Record Jev's token usage in the turn's cost
ianu82 Oct 5, 2026
073a711
Add report_tools: building blocks for self-contained HTML reports
ianu82 Oct 5, 2026
3a44221
Return a small primary file's text from open_artifact
ianu82 Oct 5, 2026
60cfb00
Build fully specified reports directly instead of through the generator
ianu82 Oct 5, 2026
23c7222
Direct reports: use report_tools as documented, without reading its s…
ianu82 Oct 5, 2026
b77c615
report_tools: keep section headings on update, add insert, flag tags …
ianu82 Oct 5, 2026
ced70d5
Edit existing reports in place; keep the dashboard skill for dashboar…
ianu82 Oct 5, 2026
a9bbe36
Min tools to 2
ZoranPandovski Oct 6, 2026
4ae2d6e
Fix tests
ZoranPandovski Oct 6, 2026
57637a5
report_tools: locate elements by "\n" line starts only
StpMax Oct 6, 2026
7b8b4bb
report_tools: keep line endings when editing a page in place
StpMax Oct 6, 2026
107cda5
report_tools: match filter options and row keys for floats and blanks
StpMax Oct 6, 2026
3ac5a17
report_tools: replace the element when new markup carries its id
StpMax Oct 6, 2026
0f7534e
report_tools: add the filter script when a filter joins an existing page
StpMax Oct 6, 2026
5356e1c
report_tools: refuse dict rows that miss a column
StpMax Oct 6, 2026
eaa9fa9
Merge pull request #535 from mindsdb/feature/eng-3095-skip-verify-sin…
ZoranPandovski Oct 6, 2026
344654b
Merge pull request #534 from mindsdb/claude/e207-artifact-performance…
ZoranPandovski Oct 6, 2026
1ab047e
[ENG-2422] Tell a web turn where credentials go instead of asking for…
tino097 Oct 6, 2026
d115eb7
Bump docosaurus (#541)
ZoranPandovski Oct 7, 2026
61f4dd2
fix(llm): report silent model calls to hosts and end one after 10 min…
lucas-koontz Oct 7, 2026
f1a61b9
fix(scratchpad): run a pad's venv work off the event loop, and end th…
lucas-koontz Oct 7, 2026
2fc473a
feat(publish): keep the password hash while the password is unchanged…
StpMax Oct 8, 2026
bf68a22
Improve verifier
ZoranPandovski Oct 9, 2026
9337907
Add new tests
ZoranPandovski Oct 9, 2026
07e5bf2
fix(logging): omit provider payloads from warnings and cloud diagnost…
lucas-koontz Oct 10, 2026
c0f4a98
Send the turn's trace on a cell's model call (#551)
ZoranPandovski Oct 10, 2026
dabc78a
Merge pull request #555 from mindsdb/bugfix/verifier-honest-data-gaps
ZoranPandovski Oct 10, 2026
2e3a22f
perf(datasources): parse each YAML text once and reuse the package pr…
lucas-koontz Oct 10, 2026
f482d04
Sync main into staging
mindsdb-release-train[bot] Oct 12, 2026
83df8ea
test: name incident fixtures and example hosts by what they show (#559)
lucas-koontz Oct 12, 2026
8b9e4e9
ENG-3067 Preload the cloud turn in a resident pod process (#520)
ea-rus Oct 12, 2026
0fa02f7
fix(generate_artifact): keep the brief in the language the user write…
StpMax Oct 12, 2026
1521bab
feat(artifacts): act first, ask later in generate_artifact discovery …
StpMax Oct 13, 2026
dbc17ed
Keep owner-side files and files outside the artifact folder out of pu…
StpMax Oct 13, 2026
3e6425a
Reject root-relative paths in generated pages (ENG-3226) (#552)
StpMax Oct 13, 2026
2727d04
Tell the agent what to pass to publish_or_preview, not where artifact…
StpMax Oct 13, 2026
2267e21
Put report_tools blocks and table rows on their own lines (ENG-3358) …
StpMax Oct 13, 2026
312a882
fix(product): answer local-model setup questions per surface (ENG-332…
pnewsam Oct 13, 2026
2da58bd
Fail the turn instead of showing raw provider errors as a reply (ENG-…
pnewsam Oct 13, 2026
2c1752e
fix(session): name the limit that stopped a turn and let the user con…
pnewsam Oct 13, 2026
058df82
Replace open_artifact with read_text_file (ENG-3307 1/3) (#545)
StpMax Oct 14, 2026
bf20a83
merged main into staging
MinuraPunchihewa Oct 14, 2026
7057db1
Merge pull request #575 from mindsdb/backmerge/main-to-staging-eng-3436
MinuraPunchihewa Oct 14, 2026
7054937
[ENG-3408] Redact OAuth tokens in scratchpad output before they reach…
tino097 Oct 15, 2026
dd2e4ef
Don't guess at the work limit (#566)
ZoranPandovski Oct 15, 2026
2b2cb93
[ENG-1706] Show an unnamed connection's engine as its label in the da…
tino097 Oct 15, 2026
eff72bb
ENG-3349: Fullstack artifact generator: correct STATE usage, no inve…
ea-rus Oct 15, 2026
d1fb80a
ENG-3452: Trace the Jev completion check like the LLM verifier it rep…
alecantu7 Oct 15, 2026
4d29626
docs: note each piece of text cowork-server's figure sources read (EN…
lucas-koontz Oct 15, 2026
090eac8
ENG-3456: Attribute end-of-turn memory calls to their turn (#583)
alecantu7 Oct 16, 2026
fd15c63
Wait out Azure rate limits instead of failing the answer (#565)
ZoranPandovski Oct 16, 2026
919dee8
fix(session): keep a turn's tool rows through compaction and hard tru…
lucas-koontz Oct 16, 2026
d79bbc3
Handled Content Filters on Hand-back Call (#584)
MinuraPunchihewa Oct 17, 2026
6857357
fix(session): name a content-filter stop in the turn's reason and ret…
lucas-koontz Oct 17, 2026
2bce5a8
feat(memory): add an optional read-only memory layer to Cortex (ENG-3…
lucas-koontz Oct 17, 2026
539f21b
fix(session): fire the scratchpad observers on streaming turns, so th…
lucas-koontz Oct 17, 2026
725cdb3
[ENG-3448] Redact a printed token value that contains a quote or a sp…
tino097 Oct 17, 2026
bee1826
feat(sandbox): lock a person's agent code to their own folders with L…
lucas-koontz Oct 18, 2026
91fe121
fix(session): give a progress report when the content filter stops th…
lucas-koontz Oct 18, 2026
d287ef2
fix(identity): honor self-hosted product and memory context (ENG-3501…
lucas-koontz Oct 19, 2026
b4094c8
fix(llm): treat a 429 with only a detail body as a rate limit (ENG-28…
lucas-koontz Oct 19, 2026
936a561
fix(tools): hold file tools to the asker's sandbox, and stop the key-…
lucas-koontz Oct 19, 2026
077e839
fix(sandbox): keep pip index credentials out of sandboxed agent code …
lucas-koontz Oct 19, 2026
3dec332
fix(scrub): hide the whole unquoted value after a token key (ENG-3448…
lucas-koontz Oct 19, 2026
64d84a6
fix(scrub): hide saved secrets in their encoded forms too (ENG-3428) …
lucas-koontz Oct 19, 2026
a7bc755
fix(scrub): hide placeholder-led unquoted values, and scrub each line…
lucas-koontz Oct 19, 2026
b602f2f
fix(scrub): refuse a credential holding a scrub marker, and leave mar…
lucas-koontz Oct 19, 2026
d07d3ff
fix(scratchpad): drop saved variables of a connection that's gone, an…
lucas-koontz Oct 19, 2026
62e5692
feat(scratchpad): hold a cell that changes connected data or existing…
lucas-koontz Oct 19, 2026
13949bf
test(identity): assert the whole browser line, not a bare URL (ENG-35…
lucas-koontz Oct 19, 2026
212963b
fix(write-check): no false cards on read-only work, a card that shows…
lucas-koontz Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# minds-anton-scratchpad: the sandbox pod image (anton + scratchpad boot + cloud_turn entrypoint).
# Consumed by scratchpad-controller (SCRATCHPAD_CONTROLLER__SCRATCHPAD_IMAGE) and Minds.
# The controller execs `python -m anton.cloud_turn` (whole turn) or
# The controller execs `python -m anton.cloud_turn` or `/usr/local/bin/cloud-turn` (whole turn) or
# `/usr/local/bin/scratchpad-boot.sh` (single cell) inside a gVisor pod running as UID 1000.
FROM python:3.12-slim

Expand Down Expand Up @@ -69,6 +69,18 @@ RUN printf '#!/bin/sh\nexec python -m anton.core.backends.scratchpad_boot\n' \
> /usr/local/bin/scratchpad-boot.sh \
&& chmod 0755 /usr/local/bin/scratchpad-boot.sh

# cloud-turn-resident: the pod's main process when the controller enables it; it
# preloads the turn and forks one process per turn. cloud-turn: what the
# controller then execs for a turn, handing it to the resident or running it
# directly when there is none. The launcher is standard library only, so it runs
# isolated and without site-packages, by the path of the installed module.
RUN printf '#!/bin/sh\nexec python -m anton.cloud_turn.resident\n' \
> /usr/local/bin/cloud-turn-resident \
&& printf '#!/bin/sh\nexec %s -I -S %s "$@"\n' "$VIRTUAL_ENV/bin/python" \
"$(python -c 'import anton.cloud_turn.launcher as m; print(m.__file__)')" \
> /usr/local/bin/cloud-turn \
&& chmod 0755 /usr/local/bin/cloud-turn-resident /usr/local/bin/cloud-turn

# Non-root, matching the pod securityContext (drop ALL caps, no service-account token, fs_group 1000, gVisor).
RUN useradd -u 1000 -m -s /bin/sh scratchpad
USER 1000
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,14 @@ ANTON_LANGFUSE_HEADERS=1

---

## Self-hosted product identity

A host can supply `ChatSessionConfig.deployment_identity` using `DeploymentIdentity(kind="self-hosted", product_name="Acme Cowork")` from `anton.core.llm.identity`. Anton uses that name, identifies MindsHub as the maker, and explains that users access an operator-managed deployment in the browser. The prompt opens with a neutral identity, leaving the configured name in its authoritative PRODUCT block. These facts take precedence over the default product skill. When such a session recalls the `cowork-product` skill, the deployment's precedence rules come ahead of the skill's default facts; without a deployment identity, the skill is recalled unchanged. The product name is 1 to 64 characters, with no leading or trailing whitespace, control or format characters, unpaired surrogates, or line separators. The host must read it from trusted deployment configuration, never from a chat message or a user's preferences.

The same host supplies `learned_memory_scope` as `personal`, `shared`, `off` or `unavailable`. With a deployment identity, Anton explains the saved-memory state and distinguishes product instructions and operator rules from learned user preferences. It checks saved content before retrieval filters and reports unreadable stores as unverified. Off and unavailable turns skip learned-store reads during prompt assembly and tool-context construction; the host must also supply the appropriate stores and learning policy when constructing Cortex. Without a deployment identity, both identity and memory prompts keep their existing defaults.

These are in-process session inputs. The cloud-turn wire does not transport deployment identity or learned-memory scope.

## Model-call deadline
A model call that sends nothing for 10 minutes is stopped, and the turn ends with "The model sent no output for 10 minutes, so the call was stopped." Every piece of text, thinking or tool-call arguments anton reads from the model restarts the 10 minutes, so a slow model that keeps writing is never cut. A non-streamed call gets the 10 minutes for the whole call, including the SDK's own retries. After one call in a turn runs out, every later model call in that turn fails at once. The one exception is the completion check after an answer: when it runs out, the turn ends on the answer that already streamed, marked unverified.

Expand Down
77 changes: 52 additions & 25 deletions anton/README.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion anton/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,11 @@
# error_type, grace_granted + grace_tokens ("", "round",
# "ceiling", or "ceiling,round" — the one-time round-cap /
# spend-ceiling extensions granted this turn, and the
# ceiling one's size), verifier_failure + verifier_error_type
# ceiling one's size), limit_continues (times the user
# answered "Keep going" at the spend ceiling or round
# cap), limit_message_continues (times the turn's own
# message carried it past one of those limits without
# asking), verifier_failure + verifier_error_type
# (WHY the completion verifier produced no verdict — the
# loop's truncated/transient/hard/denied class, or
# latched_{hard,truncated,denied,mixed} for a turn that made
Expand Down
8 changes: 5 additions & 3 deletions anton/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -1525,7 +1525,7 @@ async def _chat_loop(
cortex = Cortex(
global_hc=Hippocampus(global_memory_dir),
project_hc=Hippocampus(project_memory_dir),
mode=settings.memory_mode,
mode=settings.effective_memory_mode,
llm_client=state["llm_client"],
episodic=episodic if episodic.enabled else None,
)
Expand Down Expand Up @@ -1584,6 +1584,8 @@ async def _chat_loop(
surface=SURFACE_CLI,
# The CLI footer shows a streaming tool's live tail (`tool_peek`).
live_tool_peek=True,
# The CLI prints a tool's message to the user as markdown.
tool_messages=True,
proactive_dashboards=settings.proactive_dashboards,
act_first=settings.act_first,
output_dir=settings.artifacts_dir,
Expand Down Expand Up @@ -1635,7 +1637,7 @@ async def _chat_loop(
_query_count = 0
_total_questions = 0 # tracks first 10 questions for time estimates

from anton.chat_ui import StreamDisplay, EscapeWatcher, ClosingSpinner, QuestionRenderTracker
from anton.chat_ui import StreamDisplay, EscapeWatcher, ClosingSpinner, QuestionRenderTracker, is_displayed_tool_result

toolbar = {"stats": "", "status": ""}
display = StreamDisplay(console, toolbar=toolbar)
Expand Down Expand Up @@ -2114,7 +2116,7 @@ def _bottom_toolbar():
ttft = time.monotonic() - t0
display.append_text(event.text)
elif isinstance(event, StreamToolResult):
if event.name == "scratchpad" and event.action == "dump":
if is_displayed_tool_result(event):
display.show_tool_result(event.content)
elif isinstance(event, StreamToolUseStart):
display.on_tool_use_start(event.id, event.name)
Expand Down
4 changes: 3 additions & 1 deletion anton/chat_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ async def rebuild_session(
# Update cortex with new LLM client and memory mode
if cortex is not None:
cortex._llm = state["llm_client"]
cortex.mode = settings.memory_mode
cortex.mode = settings.effective_memory_mode

# Refresh mind knowledge from remote server
refresh_knowledge(settings, cortex)
Expand Down Expand Up @@ -116,6 +116,8 @@ async def rebuild_session(
harness=HARNESS_ANTON,
# WHERE it ran — the other axis, and now the only place "cli" appears.
surface=SURFACE_CLI,
# Same host as chat.py's fresh session: it prints a tool's message.
tool_messages=True,
proactive_dashboards=settings.proactive_dashboards,
act_first=settings.act_first,
output_dir=settings.artifacts_dir,
Expand Down
12 changes: 12 additions & 0 deletions anton/chat_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,18 @@ async def wait_for_render(self, question_id: str, *, timeout: float = 1.0) -> No
self._rendered.pop(question_id, None)


def is_displayed_tool_result(event) -> bool:
"""Whether the CLI prints this `StreamToolResult` permanently.

A scratchpad dump is the user's own request to see the cells; a tool's
`message` is written for the user. Every other result is the agent's
input and stays off the screen.
"""
if event.action == "message":
return True
return event.name == "scratchpad" and event.action == "dump"


class StreamDisplay:
"""Manages streaming LLM output with permanent prints and a tiny Live spinner.

Expand Down
27 changes: 10 additions & 17 deletions anton/cloud_turn/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
#: Matches cowork's SSE-side PROGRESS_THROTTLE.
PROGRESS_WIRE_INTERVAL = 0.25

#: cowork-server's figure sources split a cut cell result at this marker
#: (cowork/harnesses/anton_harness/figure_sources.py).
_TRUNCATION_MARKER = "\n[… truncated …]\n"


Expand Down Expand Up @@ -291,14 +293,6 @@ async def _heartbeat() -> None:
# the event loop thread so the heartbeat ticker above keeps firing
# instead of the controller seeing the turn go silent and stall it.
session = await asyncio.get_running_loop().run_in_executor(None, build)
# Length of the seeded history — everything anton appends past this
# index belongs to this turn. Captured BEFORE turn_stream, so the
# current turn's user input lands inside the slice and is trimmed off
# below. Guarded: a session double without `.history` skips capture and
# the turn degrades to text-only replay (mirrors cowork-server's
# in-process harness).
seed_len = len(session.history) if hasattr(session, "history") else None
compacted = False # set if anton summarizes history mid-turn
# Fold in any attachments cowork-server staged into the workspace (read
# off the mount, so image bytes never crossed the stdin wire). Plain
# string when there are none. Never fail the turn over this — degrade to
Expand Down Expand Up @@ -387,7 +381,6 @@ async def _heartbeat() -> None:
"content": _clip_result_content(event.content or "")})
elif isinstance(event, StreamContextCompacted):
logger.info("context compacted: %s", event.message)
compacted = True
wire({"kind": "compacted", "message": event.message})
elif isinstance(event, StreamAskUser):
logger.info("ask_user question: %s", event.id)
Expand Down Expand Up @@ -434,14 +427,14 @@ async def _heartbeat() -> None:
# Pre-terminal for the same reason as memory and skills: cowork stops
# reading the stream at the terminal event.
#
# Skipped after a mid-turn compaction: session.history was reassigned,
# so seed_len no longer marks this turn's start and slicing could drop
# rows or surface an orphan tool_result whose tool_use was summarized
# away — an invalid replay. Text-only replay stays valid, so degrade to
# it. The failure paths below never reach this line, so a failed or
# torn-down turn degrades the same way.
if seed_len is not None and not compacted:
turn_slice = session.history[seed_len:]
# `turn_messages` reads every list the turn wrote to, so a turn that
# compacted or hard-truncated mid-turn sends its rows too, beside the
# compaction frame above. The failure paths below never reach this
# line, so a failed or torn-down turn degrades to text-only replay.
# Guarded: a session double without `turn_messages` skips capture and
# the turn degrades the same way.
if hasattr(session, "turn_messages"):
turn_slice = session.turn_messages()
# Drop this turn's user input (and anything before the first
# assistant message) so the slice starts where the model replied.
while turn_slice and (
Expand Down
32 changes: 25 additions & 7 deletions anton/cloud_turn/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,25 @@
surfaces a card and the user saves it, so nothing reaches the skill store.
{"kind": "history", "rows": [...]} - pre-terminal; this turn's tool
block-rows as [{"role", "content"}], where content holds only tool_use
(assistant) or tool_result (user) blocks. cowork persists them as their
own hidden `messages` rows so the NEXT turn's history replays a valid
tool_use -> tool_result sequence. Omitted after a mid-turn compaction or
on failure, in which case that turn replays text-only.
(assistant) or tool_result (user) blocks. For a turn that did not
compact, cowork persists them as their own hidden `messages` rows so the
NEXT turn's history replays a valid tool_use -> tool_result sequence.
Sent after a mid-turn compaction or hard truncation too: the rows come
from `ChatSession.turn_messages()`, which still holds the messages the
swap folded out of history. For such a turn, cowork-server since
mindsdb/cowork-server#712 reads the rows for figure sources and stores
none, so the next turn replays that turn text-only; older servers store
them. Omitted when the turn used no tools, and on failure, in which case
that turn replays text-only.
{"kind": "compaction", "summary": "...", "covered_through": N} - pre-terminal;
this turn folded the first N messages of the REQUEST's `history` into
`summary`. cowork saves both and seeds `summary` + the uncovered tail next
turn, instead of resending (and re-summarizing) the whole conversation.
Mutually exclusive with `history` above, which the same compaction
suppresses. Omitted on failure too: the next turn re-seeds the same
history and compacts it again.
Sent before `history` above when both apply. `covered_through` counts
only seeded messages, never this turn's own. The summary text may still
describe rows this turn also sends in full under `history`. Omitted on
failure too: the next turn re-seeds the same history and compacts it
again.
{"kind": "ask_user", "id", "prompt", "options": [{"value", "label",
"detail"}], "select", "allow_custom", "timeout_s"} - interactive turns
only: a question the turn is now blocked on. Never rate limited.
Expand All @@ -81,6 +89,9 @@
`round_end`, and `turn_ended` ({"ended_by": "..."}, sent before the terminal
event when a turn handed back early). A `heartbeat` only resets the
controller's stall timer.
A `tool_result` with `action: "message"` is a tool's message to the user,
rendered as an agent message; the pod sends it only when the request set
`tool_messages`.

Answer lines on stdin (interactive turns only), one JSON object per line,
at most 64 KB:
Expand Down Expand Up @@ -214,6 +225,12 @@ class TurnRequestV1:
#: answer lines from stdin. Anything but a JSON `true` reads as False, so a
#: controller that does not know the field leaves every turn non-interactive.
interactive: bool = False
#: Whether the client watching this turn renders a tool's message to the
#: user (a `tool_result` with `action: "message"`) as an agent message —
#: the cowork UI does, channel bots do not. Without it the pod drops those
#: messages and the tool hands the content to the agent instead. Anything
#: but a JSON `true` reads as False.
tool_messages: bool = False
#: Per-engine connector data cowork-server resolved for this turn:
#: {engine: {"usage_notes": str}}. Rendered only for engines the turn's
#: vault connects. Absent when no connected engine has notes.
Expand Down Expand Up @@ -248,6 +265,7 @@ def from_json(raw: str) -> "TurnRequestV1":
d.get("started_at") if isinstance(d.get("started_at"), str) else None
),
interactive=d.get("interactive") is True,
tool_messages=d.get("tool_messages") is True,
# Same defensive isinstance check as oauth/trace.
connectors=(
d.get("connectors") if isinstance(d.get("connectors"), dict) else None
Expand Down
Loading
Loading