From a21644af59f6dc561ca1800271b6d00910a44b17 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Sat, 8 Aug 2026 15:53:39 +0800 Subject: [PATCH] fix(web-app,agent-presets): keep the goal service on the host plane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../agent-presets/standard/agent.cordis.yml | 25 +++++--------- packages/bundle/web-app/cordis.patch.yml | 16 ++++----- packages/host/apiproxy/src/api-proxy.ts | 30 +++------------- .../tests/api-proxy-agent-preset.spec.ts | 34 ------------------- 4 files changed, 21 insertions(+), 84 deletions(-) diff --git a/apps/cli/config/agent-presets/standard/agent.cordis.yml b/apps/cli/config/agent-presets/standard/agent.cordis.yml index 41d1d81a90..8eaa78d940 100644 --- a/apps/cli/config/agent-presets/standard/agent.cordis.yml +++ b/apps/cli/config/agent-presets/standard/agent.cordis.yml @@ -91,23 +91,14 @@ # ── goals ─────────────────────────────────────────────────────────────────── -- id: goals - name: cordis:group - group: true - isolate: - goals: true - config: - - id: goal - name: '@deepseek-ai/dsh-goal' - - - id: goal-session - name: '@deepseek-ai/dsh-goal-session' - - - id: command-goal - name: '@deepseek-ai/dsh-command-goal' - - - id: tool-goal - name: '@deepseek-ai/dsh-tool-goal' +# Only the model-facing tool. The goal SERVICE, its session driver, and the +# `/goal` command stay on the host plane: the Gateway serves the goal domain as +# Remote endpoints whose receiver comes from a generated descriptor, so it +# resolves `goals` on the host and an entry-local realm here would hide it. The +# registry is keyed by session anyway, so one host instance serves every +# session. What a preset chooses is whether its agent can call the goal tool. +- id: tool-goal + name: '@deepseek-ai/dsh-tool-goal' # ── plan mode ─────────────────────────────────────────────────────────────── diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index 064a5718af..29090b02dd 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -237,14 +237,14 @@ - id: tool-skill disabled: true -- id: goal - disabled: true - -- id: goal-session - disabled: true - -- id: command-goal - disabled: true +# The goal SERVICE, its session driver, and the `/goal` command STAY on the +# host plane; only the model-facing tool moves. The Gateway serves the goal +# domain as Remote endpoints, and a Remote method picks its receiver Service +# from a generated descriptor — it resolves `goals` on the host, so a +# per-session realm would answer `service-unavailable` for every browser call. +# That is the `bash-env` criterion read from the other side: injection is not +# the only host relationship a Service can have. The registry is keyed by +# session, so one host instance serves every session exactly as before presets. - id: tool-goal disabled: true diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index 3397657bed..45ff48f790 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -1127,30 +1127,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro return undefined } - /** - * The agent whose layer holds this session's tool presenters. - * - * A preset registers its tools into the agent's OWN layer, so a transcript - * read while no agent exists finds no presenter at all and every card - * degrades to the generic renderer — silently, because a viewless entry is - * also what a tool with no presenter produces. Reading therefore resolves - * the agent, through the same deduplicated resume every other session method - * takes, whenever a roster is composed. - * - * A deployment composing no roster keeps the storage-only read it always - * had: its tools are in the host layer, which needs no agent to address. A - * resolution failure is not a read failure either — the transcript still - * serves, with the generic cards it would have rendered anyway. - * @param sessionId - the transcript being read. - * @returns the agent to resolve presenters against, or undefined for none. - */ - async function presenterAgentFor(sessionId: SessionId): Promise { - const live = ctx.get('agents')?.get(sessionId) - if (live !== undefined || ctx.get('agentPresets') === undefined) return live - const found = await agentFor(sessionId) - return 'error' in found ? undefined : found.agent - } - /** Read one transcript cut and optional projection baseline without acquiring an Agent owner. */ async function historyStateFor( sessionId: SessionId, @@ -1780,7 +1756,11 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro details: {}, }) } - const page = historyPage(ctx, state.events, beforeSeq, maxMessages, await presenterAgentFor(sessionId)) + // `ctx.get`, not `ctx.agents`: this is the COLD path, and a caller may + // serve history from storage with no agent registry composed at all. + // An absent registry means no live agent, which is the same answer a + // present one gives here — presenters fall back to the global layer. + const page = historyPage(ctx, state.events, beforeSeq, maxMessages, ctx.get('agents')?.get(sessionId)) return ok(request, { events: page.events, hasMore: page.hasMore, diff --git a/packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts b/packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts index 38dd8a4ad3..37244f14b3 100644 --- a/packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts +++ b/packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts @@ -239,37 +239,3 @@ describe('a capability the session\'s preset mounts', () => { expect(failure.error.message).toContain('neither this session') }) }) - -describe('session.history presenters', () => { - it('resolves the live agent so a preset-composed presenter is reachable', async () => { - const { api, ctx } = await harness(['standard']) - await api.sessions.create(request({ sessionId: SessionId('h1'), agentPreset: 'standard' })) - - const response = await api.sessions.history(request({ sessionId: SessionId('h1') })) - - expect(response.result.ok).toBe(true) - expect(ctx.agents.get(SessionId('h1'))).toBeDefined() - }) - - it('serves the transcript when no agent can be resolved for it', async () => { - // The roster is composed, so the read tries; this harness resumes nothing. - // A resolution failure is not a read failure — the transcript still - // serves, with the generic cards a viewless entry renders. - const { api } = await harness(['standard']) - - const response = await api.sessions.history(request({ sessionId: SessionId('h2') })) - - expect(response.result.ok).toBe(false) - const failure = response.result as { ok: false; error: { code: string } } - expect(failure.error.code).toBe('session-not-found') - }) - - it('keeps the storage-only read when the deployment composes no roster', async () => { - const { api } = await harness() - await api.sessions.create(request({ sessionId: SessionId('h3') })) - - const response = await api.sessions.history(request({ sessionId: SessionId('h3') })) - - expect(response.result.ok).toBe(true) - }) -})