Преглед изворни кода

fix request-messages boundary immutability

Yichen Jiang пре 2 месеци
родитељ
комит
f3d26ed049

+ 2 - 1
packages/core/agent-loop/src/loop.ts

@@ -727,9 +727,10 @@ async function runStep(
   // so a listener's session append lands past the boundary and joins the NEXT
   // request — the same window rule as the `agent/request` waterfall.
   const emptyRequestMessages: RequestMessages = deepFreeze({ before: [], after: [] })
+  const requestMessagesBoundary = deepFreeze([...boundaryMessages])
   const requestMessages = await ctx.waterfall(
     'agent/request-messages', agent, turn, step, emptyRequestMessages,
-    { system, assembly, boundaryMessages, signal },
+    { system, assembly, boundaryMessages: requestMessagesBoundary, signal },
     () => Promise.resolve(emptyRequestMessages),
   )
 

+ 23 - 0
packages/core/agent-loop/tests/interception.spec.ts

@@ -418,6 +418,29 @@ describe('agent/request-messages (RequestMessages)', () => {
     expect(adapter.requests[0]!.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'hi' }] }])
   })
 
+  it('the read-only boundary context rejects in-place mutation before the request is built', async () => {
+    const adapter = new MockAdapter([textResponse('ok')])
+    const ctx = await harness(adapter)
+    const agent = ctx.agentLoop.create(AgentId('a1'), { model: 'mock' })
+
+    let mutationError: unknown
+    ctx.on('agent/request-messages', async (_agent, _turn, _step, _messages, context, next): Promise<RequestMessages> => {
+      try {
+        const mutableBoundary = context.boundaryMessages as Message[]
+        mutableBoundary.push({ role: 'user', content: [{ type: 'text', text: 'smuggled' }] })
+      } catch (error: unknown) {
+        mutationError = error
+      }
+      return next()
+    })
+
+    send(agent, 'hi')
+    await waitForIdle(ctx, agent)
+
+    expect(mutationError).toBeInstanceOf(TypeError)
+    expect(adapter.requests[0]!.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'hi' }] }])
+  })
+
   it('a per-step contribution change is logged as a header delta, so every request stays reconstructable', async () => {
     const adapter = new MockAdapter([
       toolCallResponse('c1', 'echo', { text: 'ping' }),