Commit Graph

393 Commits

Author SHA1 Message Date
Yichen Jiang
527eac6ca5 chore(docs): re-record the pairings after the wire merge 2026-08-08 22:52:08 +08:00
Yichen Jiang
64c992238a Merge branch 'stack/agent-profiles-3-wire' into stack/agent-profiles-5-web-ui 2026-08-08 22:51:17 +08:00
Yichen Jiang
bcfab57318 Merge branch 'stack/agent-profiles-1-seam' into stack/agent-profiles-3-wire 2026-08-08 22:47:15 +08:00
Yichen Jiang
c578ededa1 Merge branch 'stack/agent-profiles-3-wire' into stack/agent-profiles-5-web-ui 2026-08-08 20:16:58 +08:00
Yichen Jiang
132debb5fe fix(apiproxy): echo the resolved agent preset on create and session-added
session.create's reply named only the id, and the session-added frame's
agentPreset — which the server already derives via sessionListFields — was
stripped by the wire schema it never joined. A client therefore could not
label a session it just created (or learned live) until the next full list
refresh: the header's preset label rendered nothing for exactly the sessions
made in this tab. The create reply is the commit point that knows the
RESOLVED composition (a caller that named none gets the default the header
recorded), so both carriers now say it.
2026-08-08 20:16:50 +08:00
Hypatia May
336c84baf0 docs(subagent): clarify interrupt edge cases 2026-08-08 20:02:36 +08:00
Hypatia May
66a21a38b1 feat(subagent): add current-turn interrupt RPC
ctx.subagents.interrupt() stops one live continuable child's current turn
via Agent.cancel(cause, { keepInbox: true }) under either a human durable
parent address or an exact live ancestor Agent. Fire-and-return: admission
is synchronous, quiescence is not awaited. Pending inbox work, the
Activation, and published descendants are preserved; only a later waking
send resumes the parked FIFO queue. Absent, one-shot, and disposing
targets are accepted no-ops.

The new Host RPC subagent.interrupt calls only that primitive with user
authority — no catalog, history, persistence, or parent-registry lookup —
so a live child stays stoppable while its parent Agent is offline.

Refs #1535
2026-08-08 20:02:36 +08:00
Yichen Jiang
01bd8ecefe Merge branch 'stack/agent-profiles-3-wire' into stack/agent-profiles-5-web-ui
recompose becomes a parent re-link: the new preset's standing mount is
ensured BEFORE the link moves, so a failed switch leaves the agent exactly as
it was — the unmount-then-restore dance (and unmountPresetFor with it) is
gone, and the restore-failure test now asserts the agent KEEPS its tools with
the source directory deleted, because the standing mount is not the file.
2026-08-08 18:52:45 +08:00
Yichen Jiang
46649858f6 Merge branch 'stack/agent-profiles-1-seam' into stack/agent-profiles-3-wire
Rewires this layer onto the standing-mount model:

- serviceForAgent roots its search at the agent's standing mount (parent
  scope key → live mount fiber) — the composition no longer lives under the
  agent's own fiber, and two agents on one preset now address ONE instance,
  which the sharing test asserts instead of distinctness.
- viewFor/historyPage take a registry view SCOPE. A live agent is that scope;
  a cold read uses the recorded preset's standing key via standingKeyFor —
  composing plugins but starting no agent, session, or turn. A header without
  a preset (a pre-roster log) renders through the DEFAULT preset's standing
  layer; an unusable preset degrades the read to generic cards, never fails
  it. This turns produced-files and chat-scroll green structurally, with the
  token counts untouched (no resume, so the projections fold stays detached).
- The detached projections baseline now includes every standing unit's key at
  its empty fold (todos: null): the standing mount registers units
  deterministically, which is what makes the client's "omitted key =
  capability absence → clear" rule safe again. seeded-history's contract test
  asserts the new shape.
- The standard preset's realm preamble no longer claims a shared label pools
  instances — provide() throws on the second registration under one realm
  symbol; labels join REALMS.
2026-08-08 18:41:07 +08:00
Yichen Jiang
5e892aa6dc Merge branch 'stack/agent-profiles-3-wire' into stack/agent-profiles-5-web-ui
# Conflicts:
#	packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts
2026-08-08 15:54:16 +08:00
Yichen Jiang
a21644af59 fix(web-app,agent-presets): keep the goal service on the host plane
The Gateway serves the goal domain as Remote endpoints, and a Remote method
picks its receiver Service from a generated descriptor — `clear(agent, ref)`
takes its agent as a PARAMETER, so the invocation is direct and the receiver
resolves on the host. Behind the preset's entry-local realm there was nothing
to resolve, and every browser goal call answered `service-unavailable`: the
composer's Clear goal button left the bar on screen.

That is the `bash-env` criterion read from the other side. Injection is not the
only host relationship a Service can have; being READ from a host row is one
too. The registry is keyed by session, so one host instance serves every
session exactly as it did before presets. The preset keeps the model-facing
tool, which is the choice a preset is for.

Also reverts the cold-transcript presenter resolve: resuming an agent to reach
its presenters made the context meter drop its cache and token counts on every
cold-opened session, trading one silent degradation for another. The card
degradation it addressed is diagnosed and still open.
2026-08-08 15:53:39 +08:00
Tianyi Cui
b837e30a80 test(apiproxy): register question callers 2026-08-08 15:48:22 +08:00
Yichen Jiang
43b2673b69 Merge branch 'stack/agent-profiles-3-wire' into stack/agent-profiles-5-web-ui
# Conflicts:
#	packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts
2026-08-08 15:28:41 +08:00
Yichen Jiang
1c80023b12 fix(apiproxy): give a cold transcript read the agent its presenters live in
A preset registers its tools into the agent's OWN layer, so `session.history`
served while no agent exists found no presenter at all and every card degraded
to the generic renderer — silently, because a viewless entry is also what a
tool with no presenter produces. The web client opens a session by reading its
transcript, so this was the ordinary path, not an edge: the write row lost its
diff card, and with it the `locations` the produced-files row derives from.

The read now resolves the agent through the same deduplicated resume every
other session method takes, but only when a roster is composed: a deployment
without presets keeps its tools on the host layer, which needs no agent to
address, and keeps the storage-only read unchanged. A resolution failure stays
a successful read.
2026-08-08 15:27:57 +08:00
Yichen Jiang
beec364e04 Merge branch 'stack/agent-profiles-3-wire' into stack/agent-profiles-5-web-ui
The Client API carrier's `agentPresets` member was the one member of its class
without an `IApiClient[...]` annotation. Inferring it inlined `AgentPresetEntry`
into the emitted declaration by the specifier TS picks — the host `index.ts` —
dragging the whole gateway, and with it the host `Context` merges, into every
Client program importing the carrier. Annotated like its siblings.

`ApiRemoteAgentOptions.setup` now takes the inspected session rather than its
header alone: this layer resolves a resumed session's preset from the LOG,
because a session that switched while blank ran its turns under the newer
composition and the header is written once at creation.

Conflicts:
	apps/web/tests/snapshots/*/*.expected.md
	packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
	packages/host/apiproxy/src/api-proxy.ts
	scripts/doc-budgets.manifest.json
2026-08-08 15:00:31 +08:00
Yichen Jiang
c5ca12eabd Merge branch 'stack/agent-profiles-1-seam' into stack/agent-profiles-3-wire
master extracted this layer's inline cold-resume resolver into
@deepseek-ai/dsh-api-remotes, whose `setup` was a fixed AgentSetup. A resumed
session composes the preset ITS header recorded, so the option becomes a
function of that header; the resolver builds the setup before the published
re-checks so those stay adjacent to `resume`.

Conflicts:
	docs/cordis-catalog/services.md
	docs/module-graph.md
	packages/host/apiproxy/package.json
	packages/host/apiproxy/src/api-proxy.ts
	pnpm-lock.yaml
2026-08-08 14:26:38 +08:00
Tianyi Cui
f32aa54aeb feat(cli)!: make dsh run the headless entrypoint 2026-08-08 14:21:42 +08:00
Yichen Jiang
18dee248a6 Merge remote-tracking branch 'origin/master' into feature/issue-1470-skill-invoke
# Conflicts:
#	docs/module-graph.md
2026-08-08 14:03:29 +08:00
Yichen Jiang
0d53752c49 refactor(host)!: retire the skill.invoke RPC for the gesture boundary
Invocation is an ordinary session.prompt again: the pre-step gesture
boundary makes it deterministic host-side for every front end, so the
dedicated RPC (handler, wire schema, error codes, client face, fixtures)
and ui-skill's claim machinery are net deletions. The menu keeps decision
21 exactly — a pick lands literal /name text — plus the user-only marker
from skill.list's modelInvocable flag.
2026-08-08 13:15:52 +08:00
Yichen Jiang
c08fa27e5c feat(tool-skill): inject user-invoked skills at the pre-step gesture boundary
A whitespace-bounded /name token anywhere in a claimed user message,
naming a user-invocable skill in the workspace directory, now injects that
skill's renderSkillContent as instructions context appended after every
other injection of the step — the same agent/pre-step seam the catalog,
workspace instructions, and the runtime snapshot ride. Closed-set matching
mirrors the command registry (a miss stays plain prose), only user-source
messages are scanned, the policy check runs on the loaded definition, and
this is the sole entry point for disable-model-invocation skills. The
catalog's no-reload sentence now names the gesture boundary.
2026-08-08 13:14:49 +08:00
Yichen Jiang
c4c2355b50 fix(host): harden skill.invoke at the enforcement boundary
Review fixes: recheck isUserInvocable on the loaded definition (list and
get collect independently, so a provider change between them could swap in
a user-disabled body — the skill-tool execute template's second check);
thread the carrier signal through the lookup and refuse an abandoned
caller's turn as cancelled; fold lookup/loader failures into the
structured internal error the list face already uses; refuse cwd-less
sessions with the skill.list stance; and reject blank trailing text at the
wire schema instead of relying on client trimming.
2026-08-08 11:30:14 +08:00
imccyu
9c3d5725a5 build: order Host and Client compilation faces 2026-08-08 04:20:39 +08:00
Yichen Jiang
3584d8e088 docs(skill): document the user-explicit invocation path
Bilingual README updates for the four touched packages (ui-skill's claim
flow and deterministic-injection model experience, the apiproxy skills
domain, the shared renderSkillContent seam export, the catalog stitch
sentence), the implemented Agent Note triplet recording the decision and
its peer-product evidence, and the regenerated catalogs/graphs.
2026-08-08 01:52:43 +08:00
Yichen Jiang
db146f0eba refactor(host): share the turn-start route refusal between prompt and skill.invoke
turnAgentFor owns the addressed-agent resolution and the model-unavailable
refusal both turn-starting methods repeat; the duplication gate flagged the
copied block.
2026-08-08 01:52:42 +08:00
Yichen Jiang
85422f44dc feat(host): user-invocable skill listing and skill.invoke injection RPC
skill.list now serves every user-invocable skill and carries modelInvocable
so menus can mark user-only entries; the old model-and-user intersection
hid disable-model-invocation skills from their only legitimate entry point
(issue #1470). skill.invoke enforces user-invocation policy at the host
boundary, renders the canonical <skill_content> body, and injects it as a
user-role message carrying the skill-invocation source before starting a
turn. The connection fixture mirrors both faces for client tests.
2026-08-08 00:51:24 +08:00
imccyu
55ccfb5a48 fix(api): preserve dynamic defaults after rebase 2026-08-07 21:47:18 +08:00
imccyu
bb61dc13f2 refactor(api): colocate gateway and remote assembly 2026-08-07 21:47:17 +08:00
imccyu
d941350227 fix(typert): preserve remote lookup semantics 2026-08-07 21:47:17 +08:00
imccyu
64a963da0b feat: add TypeRT remote gateway infrastructure 2026-08-07 21:47:14 +08:00
Yichen Jiang
209fb4d355 Merge remote-tracking branch 'origin/master' into worktree/default-model-persistence
# Conflicts:
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/host/apiproxy/README.i18n.yaml
2026-08-07 18:18:40 +08:00
imccyu
a6ae15c0f6 Merge remote-tracking branch 'origin/master' into feat/web-workspace-file-links
# Conflicts:
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/src/client/chat/ToolRow.module.css
#	packages/host/apiproxy/src/native-path-opener.ts
2026-08-07 16:49:20 +08:00
Yichen Jiang
e265c6038f test: cover the agent-preset wire routes and both restore edges
master moved the RPC surface onto the fetch transport, so the two agentPreset
methods arrived there with nothing driving them. Adds the round trip, plus the
two `recompose` edges that had none: an agent that never composed a preset, so
there is nothing to restore, and a restore that fails because the composition
it reaches for is gone — the roster is a live directory.

Marks the deliberate non-Error rejections in the store specs, which is the
branch they exist to cover.
2026-08-07 16:39:44 +08:00
Yichen Jiang
d612103135 refactor(apiproxy): one wording for a preset failure on both paths
Session create and the preset switch can be handed the same two failures, and
a client branching on the code needs them worded identically from either.
2026-08-07 15:57:23 +08:00
Yichen Jiang
d03d3ab70b Merge remote-tracking branch 'origin/master' into worktree/default-model-persistence
Carries two edits beyond conflict resolution, both forced by what master
brought in:

- `CustomProviderCard`: master added front-end key validation and a
  component-level `keyValue` (already trimmed) while still writing
  `apiKeyEnv` unconditionally. Kept this branch's blank-key rule and its
  committed-profile retry gate, and adopted master's single `keyValue` so
  the component has one spelling of the key rather than two.
- `docs/user/guide/providers`: master merged #1810, whose default-model
  section still taught overriding the `api-gateway` row in
  `$DSH_HOME/config.yaml` — the behavior this branch replaced. Rewritten
  for the settings section the picker now writes, plus the review fix from
  #1810 replacing the colloquial 挂着 in the opener.
2026-08-07 15:44:57 +08:00
Yichen Jiang
3b35ad4b2b chore(docs): re-record the apiproxy README pairing after the master merge 2026-08-07 15:42:26 +08:00
Yichen Jiang
df2d2adbb8 Merge branch 'stack/agent-profiles-3-wire' into stack/agent-profiles-5-web-ui
# Conflicts:
#	packages/host/apiproxy/README.i18n.yaml
2026-08-07 15:41:14 +08:00
Yichen Jiang
6093c266c6 Merge branch 'stack/agent-profiles-1-seam' into stack/agent-profiles-3-wire 2026-08-07 15:41:09 +08:00
Yichen Jiang
bb43ff4f37 feat(ui): make a session that cannot send refuse to accept one
A default naming a route the Models page has since removed left the
composer saying 选择模型 while the input still accepted a message, which
then failed inside the adapter mid-turn.

`session.prompt` now refuses with `model-unavailable` before opening a
turn. That is the enforcement boundary: the method stays callable no
matter what a client disables. `session.models` reports the same fact as
`routable`, and ui-model pushes a block through the new
`ctx.conversation.blocks` registry so the bar renders the disabled
textarea it already renders without a workspace, carrying the blocker's
own reason. The push direction is forced — ui-model already depends on
ui-conversation, so ui-conversation cannot read it back.

The gate is `routable`, not "matches no advertised group": catalog
membership is advisory, so a route serving a model it stopped advertising
is missing from the groups yet perfectly usable, and `null` before the
first load never blocks so a slow Host cannot lock a working composer.

The scaffold gains a route-only adapter for fixture-less keyless
scenarios. Registering zero providers is a test artifact — every product
composition mounts one — and the goldens that froze the seat's fallback
label now show the model those scenarios actually route to.
2026-08-07 15:26:42 +08:00
Yichen Jiang
9679597204 feat(ui-models): tag the provider rows this deployment declared
A row's stored profile could not tell a hand-declared gateway from a
shipped provider whose models someone narrowed — both look identical from
outside the adapter — so the Models page had no way to mark the routes a
deployment added itself.

The directory entry now carries `declared`, answered by the owning adapter
against its own installed catalog, and the page renders a Custom tag from
it. Absence stays "this adapter draws no such distinction" rather than
"shipped", so a route no adapter claims is labelled neither way.

Also records the default-route work's Agent Note and the e2e evidence for
all three changes: the composer switch writing the section, and the Models
page declaring a route with its own reasoning effort.
2026-08-07 13:49:47 +08:00
Yichen Jiang
e95e413115 Merge branch 'stack/agent-profiles-3-wire' into stack/agent-profiles-4-settings 2026-08-07 13:39:40 +08:00
Yichen Jiang
a184d9efde Merge branch 'stack/agent-profiles-4-settings' into stack/agent-profiles-5-web-ui 2026-08-07 13:39:40 +08:00
Yichen Jiang
f3f2129154 Merge branch 'stack/agent-profiles-2-configs' into stack/agent-profiles-3-wire
# Conflicts:
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/host/apiproxy/tests/api-proxy-subagents.spec.ts
2026-08-07 13:39:29 +08:00
Yichen Jiang
0cb922199d fix(apiproxy): open config files through Windows on WSL 2026-08-07 13:30:05 +08:00
Yichen Jiang
f2d1a29636 feat(apiproxy): make the default model a user setting the picker writes
The route a new session starts from was frozen into the gateway's
composition entry, so switching models in a conversation reached only that
conversation and every later session went back to the shipped default.

The gateway now owns an `api-gateway` settings section: the entry is the
base layer and the user document layers over it, so `session.selectModel`
records an accepted switch as the default for the next session. The write
is wholesale rather than a merge — switching to a model with no reasoning
effort has to clear a stored one — and a storage failure is reported
without undoing the switch, which already applies to its own session.

`targetFor` now resolves its tiers on every read instead of seeding once:
an explicit selection, else the session's own logged request header, else
the live default. That is what keeps a session that has run a turn deriving
its route from its log forever after, while a session still blank — New
Session reuses one rather than minting another — starts from a default
saved after it was created.
2026-08-07 13:23:19 +08:00
Yichen Jiang
d099a24cb1 fix(web): resume the preset the log records, and serialize the switch
Six review findings on the select surface, all reachable from the wire:

**Resume read the header, not the log.** The switch was recorded as
`agent-preset/selected` and every projection resolved from it, but `agentFor`
still composed from `inspected.meta.agentPreset` — the value written once at
creation. A blank session that switched and then ran turns came back after a
restart under the ORIGINAL preset, restoring that history under the tool set it
was not produced with, which is the mismatch this feature exists to prevent.
`inspected` already carries the events.

**Cold summaries dropped the preset entirely.** `summarizeCold` hand-copied
three header fields and omitted the fourth, so a restored session reported no
preset and the picker showed the deployment default. It now uses the same
projection the attached path does.

**`select` had no gate.** Two concurrent selects both passed the blank check;
the second `unmountPresetFor` then found no record, because the first had
already removed it, and both mounts installed into one agent layer. Selects on
one session now queue, and the blank check is re-read inside the queue. This is
not turn admission — a `session.prompt` racing a switch is the agent loop's to
reserve — but it closes the select-versus-select tear-down.

**A same-id restore was skipped.** The roster is a live directory, so "the same
inputs that worked a moment ago" does not hold: a changed file is exactly how a
same-id reselect fails, and skipping the restore left the agent with no
composition at all.

**`writable` was dead state**, initialized true and never set, so the row could
never disable. It now carries `settings.describe`'s bit — a browser that may
not write settings sees the current default and no control, rather than one
whose write answers `settings-not-exposed`.

**`list` was documented as id-ordered.** It is root-precedence order with each
root's own presets sorted, first root to supply an id winning.
2026-08-07 11:45:10 +08:00
imccyu
3216150ae0 fix: address review round next
- subagents.history computes its projections best-effort on both arms
  (a hostile unit's fold rejection serves the page without the block,
  matching the session-list precedent) with dual-arm coverage
- the lifecycle-witness spec probes every field of the seven-key
  witness, protecting the key list itself
- list-children's own module docs catch up with the seq-gate contract,
  and the design note records the rung-two later-event window as an
  accepted, self-healing deviation of the corruption class
2026-08-07 10:17:35 +08:00
Yichen Jiang
98fbe0ee94 fix(web): record which preset a session actually runs
The creation header names the preset a session STARTED with and is frozen,
which is correct — it is a creation fact. Switching is legal only while a
session is blank, and that looked like enough: no history exists yet.

It is not, because the switch's effect outlives the blank window. The user
switches, then sends the first message; every turn from there runs under the
new composition while the header still names the old one. The session is
then locked around a misrecorded preset, and resume reads the header to
rebuild it — composing one preset's tools over a history another produced,
which is exactly the replay the blank-only lock exists to prevent, reached
by another route. A picker showed `standard` for a session running
`core-web`.

A switch is now an `agent-preset/selected` event appended after the swap
commits, and `resolveSessionPreset()` (last selection, else the header) is
what every reconstruction reads: the summary, resume, the conflict guard,
and the fork introduced one layer down.
2026-08-07 00:38:11 +08:00
Yichen Jiang
a2ab09003f feat(web): pick a session's agent preset from the composer
A seat in the composer tool row, left of the model select, showing the preset
THIS session runs — its own recorded one, not the deployment default, because a
resumed session runs what it was created with. `SessionSummary` carries
`agentPreset` for that, alongside `cwd` and `origin`.

The switch exists only while the conversation has not started. After the first
turn the seat becomes a plain label rather than a disabled menu: a greyed
control reads as "temporarily unavailable", when in fact the choice is gone for
good. The seat never asks in that state, and the host refuses independently
with `agent-preset-locked`, so a stale client cannot slip one past it.

A rejected switch restores the previous value and surfaces the host's message
rather than leaving the seat showing something the session is not running.
2026-08-07 00:38:11 +08:00
Yichen Jiang
bf4356cf35 feat(web): let a blank session switch its agent preset
`agentPreset.select` recomposes one session's agent from a different preset.
It is allowed only while the session is blank — once a turn has run, that
history was produced under the preset's tools and swapping them would strand
logged tool calls, so the attempt answers `agent-preset-locked`.

The agent and the session survive; only the preset subtree is swapped. That
was forced by what the host actually owns: api-proxy discards the `AgentHandle`
it creates, and there is no delete RPC, so neither disposing nor recreating the
session was available. Swapping the subtree is also the better answer — the
session id, its workspace attachment, and its projections all stay put.

`recompose` is unmount-then-mount because two compositions cannot coexist: both
would register the same tool names into one layer. So it resolves the new
preset BEFORE tearing anything down (an unknown id is a no-op) and restores
the previous composition when the new one fails to mount, rather than leaving
the agent with no tools at all. Both paths are pinned by test.

Also restores the English half of the `agentPreset.list` README paragraph,
which was lost before the previous commit — and `verify-translation-pairing
--write` recorded the pair as consistent anyway, because it records whatever
state it finds rather than checking the two sides say the same thing.
2026-08-07 00:38:11 +08:00
Yichen Jiang
e6fe32b3c3 feat(web): expose the agent-preset roster over the API
`agentPreset.list` gives a browser the deployment's roster so it can offer a
choice when starting a session. Each row carries the id, its `trust`, and
whether it is the current default.

`trust` is on the wire deliberately: a `user` preset is exactly as privileged
as the plugins it names, so a surface that offers one alongside a shipped
preset can say which is which rather than presenting both as vetted.

The domain is read-only. A preset is a composition on disk, so authoring one
is a filesystem act rather than an RPC; and a deployment composing no presets
answers with an empty roster rather than an error, because sharing the host
composition is a valid deployment.

The RPC map made every registration site a type error, so the route, the
response-schema table, the service delegate, and the browser fixture are all
wired rather than only the ones I remembered.
2026-08-07 00:38:11 +08:00