Browse Source

Merge branch 'stack/agent-profiles-5-web-ui' into stack/agent-profiles-8-authoring

# Conflicts:
#	packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts
Yichen Jiang 2 months ago
parent
commit
61e7a93a66

+ 25 - 5
packages/host/apiproxy/src/api-proxy.ts

@@ -1211,6 +1211,30 @@ 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<Agent | undefined> {
+    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,
@@ -1831,11 +1855,7 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
             details: {},
           })
         }
-        // `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))
+        const page = historyPage(ctx, state.events, beforeSeq, maxMessages, await presenterAgentFor(sessionId))
         return ok(request, {
           events: page.events,
           hasMore: page.hasMore,

+ 34 - 0
packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts

@@ -438,3 +438,37 @@ describe('authoring over the wire', () => {
     expect(response.result.error.code).toBe('agent-preset-not-found')
   })
 })
+
+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)
+  })
+})