Skip to content

feat(studio): studio.image writes a generated design image into the agent's workspace (ADR 0006 slice 2) - #513

Merged
ymote merged 9 commits into
mainfrom
studio/slice2-image-tool
Oct 15, 2026
Merged

ymote merged 9 commits into
mainfrom
studio/slice2-image-tool

Conversation

@ymote

@ymote ymote commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

ADR 0006, slice 2 of the accepted route: studio.image, an agent tool that generates one image through the host's model.image service and writes it into the system agent's workspace. Developer builds only, like the other studio.* tools, and offered to the system agent only in this slice.

  • New crates/shell/src/host_tools/studio_image.rs. The tool takes prompt (optionally size, seed, class) to start a generation, or job alone to poll one. The model.image call is queued for the UI thread and dispatched from host_tools::pump() the way a component's host calls are, under the system agent's own app id, with from_sheet/may_prompt false. The executor's thread only waits, for the call's share of the kernel deadline (timeout_ms minus 1.5 s), then returns {job, state: "running"}; a later call with that job collects the image. The UI thread owns only that queue, the waits and a cancel list, and never takes the job table or the developer-mode lock; each job has a watcher thread that parses the answer, notices developer mode moving, and ends the job when it expires. The decoded image is written into the caller's workspace through the same scoped::Workspace path as studio.inspect (.studio-<uuid>.png, or .jpg if the service says so) and the reply carries path, width, height, format.
  • Developer tag. Every job carries the developer-mode tag and app of the call that started it. Once the tag no longer stands: a queued call is not sent (the UI thread refuses a call queued under an older developer-mode generation, without taking any lock), a running one is failed by its watcher and cancelled at App Hub through the UI thread's cancel list, an answer is dropped, a finished image is forgotten, and nothing is written; collecting re-checks the tag, the grant and, for an app peer, its account. The relay's grant and the developer test spool exclude the tool for an app's agent and for fixtures (studio::granted_to).
  • Bounds and cleanup. Two generations in flight (the model service's own per-app bound), four jobs held in all and two per conversation, finished images counted; a finished image is collected once (a concurrent poll sees "being written") and expires ten minutes after it finished; a cancelled first call abandons its job through the same cancel list (the queued call is skipped, a sent one cancelled at App Hub, since the agent never learned the id); an expired wait is answered with a timeout and cancelled at App Hub; a queued call the UI thread picks up with under 130 s of the 240 s service wait left is refused rather than paid for and thrown away. The input schema has additionalProperties: false and no output path argument.
  • Admission (apps.rs). The model service's grant callback (model_grant) also admits the system agent while developer mode covers it (studio::system_caller_admitted), and the scope callback (model_scope) answers device for the system agent in a build that has the studio. Apps are admitted as before, as themselves. Release builds are unchanged: SUPPORTED is false there.
  • Wiring (studio.rs, mod.rs). studio.image joins APP_TOOLS but is declared to, and executed for, the system agent only (SYSTEM_ONLY); the executor branches to the new module with the call's developer tag; Workspace::output_image accepts png/jpg.
  • Docs. Walkthrough tool table row and a paragraph on cost, approval, bounds and revocation (EN + zh), ADR 0006 "Where things stand" row and route item 2, phone README clause (EN + zh). An independent language judge scored the English and Chinese additions; its replacements are applied.

Not in this PR: the first live call (needs a configured image provider and the owner's go-ahead for the paid call), reference images through files.pick_photo, and octos's own image_generation tool, which still has no backend.

Review

The second-review findings on aed37ef (one major: revocation stopped neither the write nor a generation; minors: orphaned cancel, lazy expiry, no cancel_heap on expiry, wait measured from queue time, reply parsed on the UI thread, cap above the service's bound, budgets adding up across agents, docs; nits) are addressed in 55671d9, as listed above. The second review on 7e3b69c (two minors: the fix had put the job lock and the developer-mode lock on the UI thread; cancel_heap ran off the UI thread and could miss a call taken but not yet dispatched; nits: watcher started after queueing, the relay still granting the tool to an app's agent, the test spool, an unused import, a release-build test, the docs' bounds) is addressed in 6785f4d as described above. The final check on 6785f4d found no critical, major or minor issues; its three nits (one developer-mode generation read shared by a job's watcher and its queued call, a revoked job dropped at once instead of held, the release-build guard on the child-process tests) are folded in as b1ceffd. Not taken from the nits: 512x512 and seed stay offered (the tool's description says some providers refuse them, and the service's refusal reaches the agent); no remaining-budget field is passed through because the image reply carries none.

Tests

  • Phone: cd phone && cargo test --features mobile-apps,dev-mode -p octosense-shell, the CI variant --features mobile-apps,toolbox-peers,craft-engines -p octosense-shell host_tools, and the CI set --locked --features mobile-apps -p octosense-shell.
  • Desktop: cargo test --locked -p octosense-shell (default features, which compile the module out, as they do studio_bundles), cargo test --locked -p octosense-shell --features app-hub studio, cargo test --locked -p octosense-app-peers.
  • New unit tests run the real executor against a fake model service registered under a family of its own, with the authorization and the developer-mode generation answered by the test (developer mode is process-wide): start, poll and collect once, another conversation refused; a cancel before the first reply abandons the job and cancels it at App Hub; developer mode ending drops a finished job on its poll and writes nothing; a watcher ends a running job when developer mode moves and no longer covers it, without a poll; a queued call is not sent once developer mode has moved; pump admission and expiry; the running, held and per-conversation bounds; reply decoding; the declaration's schema; which agents are offered and granted the tool and the model service's admission and scope answers. Two tests run in a child of the test binary with the real developer-mode controller on (OCTOSENSE_DEV_MODE=all): the real executor refuses an app's agent for the system-only reason before any service call and serves the system agent, and turning developer mode off ends a job in flight. The relay test checks an app's agent naming the tool is refused by the relay.

On 77a5719 (all three review rounds, merged with main 8b98596): phone mobile-apps,dev-mode 1287 passed; CI variant 138 passed; CI set --locked --features mobile-apps 1287 passed; desktop default 1054 passed; desktop --features app-hub studio 47 passed; app-peers 22 passed. Two-run local gate on 77a5719 (tools/ci-local.sh --only all): run A on the Linux build host 53 PASS, 8 SKIPPED (the rom.yml steps that need a JDK, the Android SDK and node, which that host lacks), 0 FAIL; run B on macOS with those tools on PATH 59 PASS, 0 FAIL; both target/ci-local/last.json records pass and bind to this head. No rom files are touched.

🤖 Generated with Claude Code

ymote and others added 9 commits October 13, 2026 10:53
…gent's workspace

ADR 0006, slice 2 of the accepted route. The new host tool `studio.image`
takes a prompt (or a job id to poll), runs `model.image` under the calling
agent's own identity on the UI thread with the other host calls, hands back
a job id when the provider outlasts the call's deadline, and writes the
decoded image through the scoped workspace, as `studio.inspect` writes its
PNG. Two running jobs per agent and conversation, eight on the device, a
ten-minute collection window, no output path argument.

The model service's grant callback admits the system agent while developer
mode covers it, with `device` as its scope; apps are admitted as before.
Release builds are unchanged: the studio is compiled out of them.

Docs: walkthrough tool row and a paragraph on cost and bounds (EN + zh),
ADR 0006 state table and route item 2, phone README clause (EN + zh); an
independent language judge's replacements are applied.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…led and expired jobs, system agent only

Review of #513:
- every job carries the developer-mode tag and app of the call that
  started it: once the tag no longer stands, a queued call is not sent, a
  running one is cancelled at App Hub, an answer is dropped and nothing is
  written; collecting re-checks the tag, the grant and, for an app peer,
  its account
- a cancelled first call abandons its job: the queued call is skipped or
  cancelled, the slot freed; the agent never learned the id
- expired waits are answered with a timeout and cancelled at App Hub; a
  queued call the UI thread picks up with under 130 s left is refused
- the UI thread passes the reply text on; the job's thread parses it
- caps follow the model service: two running per app, four held (finished
  images count), two per conversation; a finished image is collected once
  and a concurrent poll sees "being written"; a job is registered after
  its request is built; the watcher thread is spawned fallibly
- the tool is offered to the system agent only; the model service's grant
  and scope are named functions, the device scope gated on the studio build
- tests: the real executor against a fake service under a family of its
  own (start, poll, collect once, another conversation refused, a cancel
  abandons, developer mode ending drops a running job and a queued call),
  pump admission and expiry, bounds, decoding, declarations and admission
- docs follow (EN + zh); the language judge's replacements are applied

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mage tool is not offered to app agents

The model grant and scope functions moved the `app-hub`/`native_mobile`
cfg off `register_host_services`, so the default desktop build failed; all
three now carry it. The peer-offering test expects every studio app tool
but `studio.image` for an app's agent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… cancel through the UI thread

Second review of #513 (7e3b69c):
- the UI thread owns only the queue, the waits and a cancel list: a
  queued call is admitted by the developer-mode generation (lock-free)
  and the time left; cancels and revocations go through the list the
  pump drains, which skips a queued call or cancels a sent one at App
  Hub, so the isolate-closed listeners run on the UI thread and a cancel
  cannot miss a call taken but not yet dispatched
- each job's watcher thread notices developer mode moving, re-checks the
  tag, fails the job and has it cancelled; it then holds the finished
  image for its window and drops it on expiry or revocation; no sweep on
  the UI thread
- the watcher starts before the call is queued, so a request nobody waits
  for is never sent; `step` and `finish` check the tag outside the job lock
- the relay's grant and the developer test spool exclude `studio.image`
  for an app's agent and for fixtures (`studio::granted_to`)
- tests: the real executor in a child process with the real developer-mode
  controller (an app's agent refused for the system-only reason, turning
  developer mode off ends a job in flight), seam tests for the
  generation-moved paths, the relay's refusal, a `cargo test --release`
  guard; `SYSTEM` imported by the tests only
- docs: a conversation holds two jobs; a finished image is forgotten on
  revocation (EN + zh, judged)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…all; a revoked job is dropped at once

Final check of #513 (6785f4d), three nits:
- the developer-mode generation is read once before the watcher starts
  and carried by the queued call, so a move in between is refused by the
  UI thread's admission and the call is never sent
- a job whose developer mode ended is removed by its watcher instead of
  being held with a thread waking every second until a poll or the TTL;
  `finish` reports whether it recorded anything, and `hold` runs only then
- the two child-process tests return early in a build without the studio

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ymote

ymote commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Local CI passed on 77a5719 (tools/ci-local.sh --only all, 23m50s, Darwin arm64).

GitHub would run for this PR: desktop.yml, phone.yml, apps.yml. Merged on this local pass (macOS runner queue); GitHub CI runs on the merge commit on main.

Job Step Result Time
ci-local / drift The local mapping fits the workflows PASS -
desktop.yml / desktop Prepare the pinned framework sources PASS 1s
desktop.yml / desktop Compile the desktop with the shared runtime PASS 4m03s
desktop.yml / desktop One copy of every shell source PASS -
desktop.yml / desktop Desktop tools and scripts PASS 21s
desktop.yml / desktop In-app updater and installer handoff PASS 32s
desktop.yml / desktop One Makepad, App Hub, octos and Rinx in the locked graph PASS 3s
desktop.yml / desktop The native apps' generated places match native-apps.json PASS 0s
desktop.yml / native-host-api Prepare the pinned framework sources PASS 1s
desktop.yml / native-host-api Build the native acceptance host and pinned Hub CLI PASS 12s
desktop.yml / native-host-api Signed app tool calls a real OS permission API PASS 4s
phone.yml / home Prepare pinned sources and the reviewed runtime patch PASS 1s
phone.yml / home Validate launcher icons and Apple asset catalogs PASS 3s
phone.yml / home Compile Home and its bundled modules PASS 4m11s
phone.yml / home Verify the single runtime graph PASS 6s
phone.yml / home The native apps' generated places match native-apps.json PASS 0s
phone.yml / home The pinned octos revision PASS 0s
phone.yml / home Build octos (unless cached) PASS -
phone.yml / home Build Home, App Hub admission and runtime policy tests PASS 44s
phone.yml / home Test Home, App Hub admission and runtime policy PASS 46s
phone.yml / home Two-lane scenario (real kernel, scripted model) PASS 33s
phone.yml / home Build opt-in AppCard tests PASS 41s
phone.yml / home Test opt-in AppCard PASS 8s
phone.yml / home Build craft-engine relay and system-chat tests PASS 51s
phone.yml / home Test craft-engine relay and system chat PASS 27s
phone.yml / home Build linked Terminal tests PASS 39s
phone.yml / home Test linked Terminal PASS 6s
phone.yml / home Build Wasm and runtime policy tests PASS 10s
phone.yml / home Test Wasm and runtime policy PASS 16s
apps.yml / services python3 tools/setup.py PASS 1s
apps.yml / services Shared system app interface PASS 0s
apps.yml / services Kernel service PASS 28s
apps.yml / services OctoSense release updater PASS 20s
apps.yml / services App peers PASS 46s
apps.yml / services AI providers config PASS 24s
apps.yml / services Connected provider protocols and Linux host adapter PASS 57s
apps.yml / services L0 in-card chat (sys.chat, host side) PASS 19s
apps.yml / services System toolbox (workflow templates) PASS 2m27s
apps.yml / kernel-security python3 tools/setup.py PASS 1s
apps.yml / kernel-security The pinned octos revision PASS 0s
apps.yml / kernel-security Build octos (unless cached) PASS -
apps.yml / kernel-security Real-kernel tests PASS 39s
apps.yml / apps python3 tools/setup.py PASS 1s
apps.yml / apps Host services (Mail, AI providers, News) PASS 29s
apps.yml / apps Connected accounts and Rinx editor PASS 8s
apps.yml / apps Shell AI services (crates/ai-host) PASS 18s
apps.yml / apps AppCard runtime setup safeguards PASS 0s
apps.yml / apps AppCard clippy (own crates; shared runtime compiles) PASS 3s
apps.yml / apps AppCard transport and store PASS 7s
apps.yml / apps One Makepad, App Hub, octos and Rinx in the locked graph PASS 14s
rom.yml / product python3 -m unittest discover -s tests -v PASS 52s
rom.yml / product Check generated Agent Binder client PASS 0s
rom.yml / product bash -n scripts/stage-forks.sh scripts/publish-release.sh scripts/apply-to-tree.sh scripts/build-rom.sh PASS -
rom.yml / web-installer npm ci --ignore-scripts --no-audit --no-fund PASS 10s
rom.yml / web-installer npm test PASS 1s
rom.yml / web-installer npx playwright install --with-deps chromium PASS 1s
rom.yml / web-installer npm run test:browser PASS 4s
rom.yml / web-installer python3 -m unittest discover -s tests -p test_installer_manifest.py -v PASS 0s
rom.yml / web-installer bash -n scripts/release.sh PASS -

Notes:

  • GitHub runs Python 3.12; this run used Python 3.9.6
  • apps.yml:services runs on ubuntu-latest in GitHub; here on macOS, so #[cfg(target_os = "linux")] code in it is not exercised (--linux-host runs it on Linux)
  • apps.yml:kernel-security runs on ubuntu-latest in GitHub; here on macOS, so #[cfg(target_os = "linux")] code in it is not exercised (--linux-host runs it on Linux)

Native GitHub acceptance passed on the same head: embedded-browser.yml.

@ymote
ymote merged commit cc39de6 into main Oct 15, 2026
9 checks passed
@ymote
ymote deleted the studio/slice2-image-tool branch October 15, 2026 23:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant