feat(orchestration): providers, waiting, models, profiles and final answers for subagents - #107
BIackFIame wants to merge 9 commits into
Conversation
An orchestrator with canvastty_agents did not know which providers it
could spawn, so it searched ~/.local/bin and ~/.config instead of
delegating, and had to poll for results.
- list_providers: every agent provider CanvasTTY can launch as a
subagent, with the exact spawn_agent.provider id, name, installed and
available from the provider CLI registry, sign-in state from the last
usage-limits read (ok, signed_out, expired, unknown; LimitsService.peek
never starts a read and nothing reads credentials), subagent and
orchestrator support, and plugin launch options with the plugin tool
that picks them (such as list_routes). The control CLI answers the
same list as `providers`.
- spawn_agent lists the known ids (enum, mirrored from providerCatalog
and kept equal by a test) and refuses an unknown provider with
INVALID_REQUEST naming list_providers. The stdio helper answers
malformed core calls itself, since the bridge drops the connection
over them.
- wait_for_agent({ sessionId, timeoutSeconds <= 600 }): returns when the
subagent is idle (after its screen settled), needs_approval, exited
(done/failed), quiet, closed, or on timeout, with status, exit code,
waited time and the tail masked before it is cut. Own subagents only,
never the orchestrator itself; it stops at once on cancel or
disconnect. It reads only metadata and the output offset while it
waits.
- The MCP instructions, tool descriptions, orchestrator skill, docs and
both refusal messages give the workflow list_providers -> spawn_agent
-> wait_for_agent -> get_agent_result and say not to search the
filesystem for agent CLIs or their configuration.
OpenCode subagents asked "Access external directory ~/Downloads/<name>" for their own project when its name had a letter with two Unicode spellings (Cyrillic "й"). Finder stores names decomposed (NFD); the path an orchestrator types is composed (NFC). macOS opens either, so the CLI started in the NFC path, read its working folder back from getcwd in NFD, and OpenCode's string comparison between its project root and the paths in its prompt made the project external. Codex trust entries had the same mismatch. - TerminalManager.create spells the requested cwd as stored on disk (onDiskPath: each non-ASCII part replaced with the parent's entry of that name, exact match first, else the one entry equal after normalization; symlinks are kept, ambiguous or unreadable parts are left as given). This covers spawn_agent, the control CLI, plugin sessions.create and the launcher. - Every launch gets PWD set to its folder (the wrapped one for plugin environments) instead of inheriting the app's. - OpenCode gets permission.external_directory "allow" for exactly its project folder in its other Unicode spelling (the folder and its contents) in the per-run inline config; a person's own external_directory word is kept as "*", YOLO and ASCII paths are unchanged.
…effort The person asked an orchestrator for subagents on "GLM-5.3 Flash"; they ran on OpenCode's default GLM-5.3 because spawn_agent had no model. - CreateSessionRequest/SessionMetadata carry model and effort; the launch passes them to the CLI's own flags for that run only (--model: OpenCode provider/model, Codex, Claude, Qwen, Kimi alias, Grok, OMP, Pi, Cursor; effort: Codex -c model_reasoning_effort, Claude --effort, Grok --reasoning-effort, as their --help lists them). Hermes, MiniMax, Devin and Antigravity take none. Nothing is written to CLI config. - Checked per provider (shared/launchModel.ts) with the reason in the refusal and a pointer to list_providers / the providers command; kept on restart and restore (persisted, and a stored value the CLI would not take is dropped instead of failing the restore). - spawn_agent gets model and effort (effort enum mirrored and tested); the control CLI gets --model and --effort. - list_providers shows each provider's model format and effort levels, and for OpenCode the models `opencode models` lists: run in the background with a 5 s timeout, cached for 10 minutes, never awaited. - The skill, docs and tool texts say: if the person names a model, pass it as model. - OpenCode stops with only "Unexpected server error" on a model it does not know. A model missing from the cached `opencode models` list is refused before launch (spawn_agent, control create, TerminalManager for the launcher and plugins) with up to five closest ids; when the list cannot be read, the model is passed as given. - A subagent that exits anyway reports the last lines of its screen as plain masked text (exitLines) in wait_for_agent, observe_agent and get_agent_result.
OpenCode subagents asked the person for every action. OpenCode 1.18 has
no auto flag, so its auto profile is an inline OPENCODE_CONFIG_CONTENT,
merged with the core's own inline config and never written to
~/.config/opencode.
Checked against the installed opencode 1.18.33: Permission.evaluate is
rules.findLast(permission and pattern match), fromConfig turns
{ tool: action } into a "*" rule and { tool: { pattern: action } } into
one rule per pattern in order, and an agent's permission is appended
after the top-level one. So the rules go under agent.build (OpenCode's
default agent): after the person's own rules, and every tool not named
keeps what the person's configuration says.
- read, glob, grep, list: allow (".env" files still ask, as OpenCode's
default does); edit (edit, write, apply_patch): allow.
- bash: allow only when base protection is on and CanvasTTY's guard is
installed in that OpenCode (tool.execute.before, hard denies still
deny before OpenCode's own check); otherwise it asks, so auto never
grants more than the person chose. A launch contributor's other model
keeps bash asking, like accept-edits elsewhere.
- external_directory is untouched: outside the project asks as before.
- hasAutoMode includes OpenCode (launcher, card label, control CLI);
TerminalManager learns base protection through configureBaseProtection.
…ofile Subagents were always launched in the normal profile, so a person who ran the orchestrator in auto was still asked about every step of every subagent. - spawn_agent takes an optional profile, "normal" or "auto" (auto only where the subagent's CLI has one). Without it the subagent gets its orchestrator's profile; an auto its CLI lacks becomes normal. - YOLO is never handed to a subagent: core allows it only in an isolated environment the person chose, which spawn_agent cannot pick. An explicit "yolo" is refused with the reason, and a YOLO orchestrator's subagents run in auto (or normal). - The answer reports profile and profileInherited, list_agents shows each subagent's profile (and model), and the card shows the profile it runs in. The control CLI's create --profile stays its equivalent. - The skill and docs describe it.
…screen tail How core decided an OpenCode subagent finished: CanvasTTY's OpenCode plugin reports working on session.status busy and idle on session.idle through the runtime gateway, so wait_for_agent returned at the right time. But get_agent_result only returned the raw PTY tail (the last 8,192 characters of a full-screen TUI's redraw sequences), and state stayed "running" while the CLI was open, so the orchestrator never got the answer text. - The OpenCode plugin, when result capture is on for its session, reads the session's last assistant message (text parts, not tool, reasoning or synthetic parts; v2 and v1 SDK shapes; 2 s timeout) at session.idle and reports it with the turn's end, at most 4,096 characters with the end kept. The runtime gateway accepts a result on OpenCode's session.idle as it does on a Stop hook, still only for a lease that captures results. - spawn_agent turns result capture on for Codex and OpenCode subagents only (TerminalManager accepts it for both); ordinary cards never keep an answer. - TerminalManager keeps the last answer in memory, masks it when read, and clears it when the next turn starts. get_agent_result returns it as answer, plus status; wait_for_agent includes it when the turn ended. - Tests drive the plugin in a separate process with a fake SDK client against a real runtime gateway, and cover masking, truncation, the v1/v2 client shapes and the no-capture path.
|
Привет! Собрал всё, что накопилось после 1.7.0, в шесть PR-ов стеком. Коммиты не переставлялись: каждая ветка — префикс одной линейной истории поверх текущего Порядок ревью и мержа:
Цифры (A/B против пересобранного 1.7.0 в одной сессии, 3 прогона, стабы вместо CLI): память приложения с 10 агентами и оркестратором −37 %, хелпер на агента 60 → 6 МБ, permission gate Claude 165 → 23 мс на вызов, компонентов на событие пан/зума 73 → 2, первый интерактивный кадр 511 → 434 мс, приложение 376 → 259 МБ (zip 195 → 121 МБ). Проверка: на финальной ветке полный набор тестов (1507: 1506 pass, 1 skip), typecheck, build, audit:secrets; на промежуточных — typecheck, build и тесты своего диапазона. Скрытый смоук собранного пакета прошёл. Честно о границах: на Windows нет слоя изоляции и нативный хелпер там по умолчанию выключен (транспорт через named pipe проверен только в CI); на Linux нужен По твоим Windows-фиксам в Плагины: в пяти официальных плагинах есть ветки |
25fe834 to
28de035
Compare
|
Rebased the whole stack (#107–#112) onto current CI on the fork for this tip ( Tips and runs for the rest of the stack are in the comments on #108–#112. |
|
Follow-up to the Windows fatal-host issue recorded in the stack summary: the orchestration gateway now replaces a failed pipe host with at most three retries (500 ms, 1 s, 2 s). An initial startup failure still rejects; explicit stop cancels recovery; disable/re-enable pauses and resumes it. Stale events cannot stop the replacement, and an explicit start safely takes ownership of a pending recovery. All old connections and capability leases are revoked on failure. Recovery allows newly launched orchestrators to register; an existing orchestrator with an expired lease still needs its terminal restarted. Tests cover those boundaries, including the independently reproduced disable/re-enable/start race. The shared environment test fixtures now stop all persistence owners before removing storage. The history-HUD fixture uses one fixed clock origin so Windows scheduling cannot reverse its timestamps. No assertions were removed. Updated head: Independent GPT-6 Luna reviews found no remaining concrete defect in the reported cases. Final cumulative local validation: 1605 passed, 3 skipped, 0 failed; production build/typecheck, secret audit and real-Electron hidden-terminal proof passed. Lower branches were additionally checked with their own gateway/environment/history tests and typecheck. |
Branch:
pr/1-orchestration→ basemain(a0f23b4).Depends on: nothing. This is the first PR of the stack.
Why
~/.local/bin, CLI config folders) instead of delegating.spawn_agentcould not pass a model or an effort, and subagents always started in the normal profile, so they asked the person about every step.get_agent_resultreturned the raw tail of a full-screen TUI, not the agent's final reply, and there was no way to wait for a subagent.What changes for users after merging
An orchestrator agent can now ask CanvasTTY which agents exist, start them with the model the person asked for, wait until they finish and read their final answer. Subagents start in the right folder even when the folder name uses non-Latin letters, and they inherit the orchestrator's launch profile (never YOLO), so they stop asking about every step.
list_providers: id, installed, sign-in state, models, profilesPWDmatches (2/2 subagents)--model(2/2 subagents)wait_for_agentreturns on idle / approval / exit / timeoutanswer, max 4,096 chars, masked)Numbers come from the hidden smoke of the packaged app (see "How to verify"); the stub agents do not measure real CLIs.
What's inside
list_providers(MCP) andproviders(control CLI): exact ids, availability, sign-in state from the last usage read (never starts a read, never reads credentials), model format and effort levels, OpenCode's cachedopencode models, plugin launch options.spawn_agentrefuses an unknown provider with a reason that nameslist_providers.wait_for_agent({ sessionId, timeoutSeconds ≤ 600 })for the orchestrator's own subagents; answers with status, exit code and a masked tail; stops on cancel or disconnect.PWDis set per launch; OpenCode allows exactly that folder in its other spelling.spawn_agentandcreate --model --effort, mapped to each CLI's own flags for that run only; validated per provider; kept on restart and restore; nothing written to CLI config.OPENCODE_CONFIG_CONTENT,.envstill asks,bashauto only while base protection guards it). Subagents takeprofileor inherit the orchestrator's; YOLO is never handed down.session.idle, last assistant message) subagents report their final reply; memory only, cleared on the next turn.list_providers→spawn_agent→wait_for_agent→get_agent_result.How to verify
Manual: start an OpenCode orchestrator in a folder named with Cyrillic letters, ask it to "split the task between two OpenCode agents on model X and wait for them". It should call
list_providers, spawn two cards on model X without folder prompts, wait, and quote both final answers.Risks / follow-ups
unknownotherwise).