From 3285ff7290a045667f2756774e01a01693a35436 Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Fri, 14 Aug 2026 15:39:26 -0700 Subject: [PATCH] fix(core): distinguish skill context from user turns HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a00177-5f25-7139-86e4-11b3c04c7a5a --- .../src/tools/subagent-fork.ts | 16 +++++++++-- .../test/subagent-tool.test.ts | 27 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/agentlayer-core/src/tools/subagent-fork.ts b/packages/agentlayer-core/src/tools/subagent-fork.ts index 0c0f5b3..c56cb22 100644 --- a/packages/agentlayer-core/src/tools/subagent-fork.ts +++ b/packages/agentlayer-core/src/tools/subagent-fork.ts @@ -16,13 +16,24 @@ function invokesToolCall(message: ModelMessage, toolCallId: string): boolean { ) } +function isInjectedSkillMessage(message: ModelMessage): boolean { + if (message.role !== 'user' || typeof message.content !== 'string') return false + const content = message.content.trim() + return /^)/.test(content) && content.endsWith('') +} + +function isUserTurnBoundary(message: ModelMessage): boolean { + return message.role === 'user' && !isInjectedSkillMessage(message) +} + function removeInvokingTurn(messages: ReadonlyArray, toolCallId: string): ModelMessage[] { const invokingAssistantIndex = messages.findIndex((message) => invokesToolCall(message, toolCallId)) if (invokingAssistantIndex < 0) return [...messages] let triggeringUserIndex = -1 for (let index = invokingAssistantIndex - 1; index >= 0; index--) { - if (messages[index]?.role === 'user') { + const message = messages[index] + if (message && isUserTurnBoundary(message)) { triggeringUserIndex = index break } @@ -46,7 +57,8 @@ function isEligibleForkMessage(message: ModelMessage): boolean { function selectRecentTurns(messages: ModelMessage[], turns: number): ModelMessage[] { let userTurns = 0 for (let index = messages.length - 1; index >= 0; index--) { - if (messages[index]?.role !== 'user') continue + const message = messages[index] + if (!message || !isUserTurnBoundary(message)) continue userTurns += 1 if (userTurns === turns) return messages.slice(index) } diff --git a/packages/agentlayer-core/test/subagent-tool.test.ts b/packages/agentlayer-core/test/subagent-tool.test.ts index 0627c13..105ab8a 100644 --- a/packages/agentlayer-core/test/subagent-tool.test.ts +++ b/packages/agentlayer-core/test/subagent-tool.test.ts @@ -823,6 +823,33 @@ describe('forking subagent tool', () => { expect(projectForkMessages(messages, 'none', 'spawn-call')).toEqual([]) }) + test('does not treat an injected skill message as the invoking user turn boundary', () => { + const messages: ModelMessage[] = [ + { role: 'user', content: 'old request' }, + { role: 'assistant', content: [{ type: 'text', text: 'old answer' }] }, + { role: 'user', content: 'actual triggering request' }, + { + role: 'user', + content: '\nInvestigate carefully.\n', + }, + { + role: 'assistant', + content: [ + { type: 'tool-call', toolCallId: 'spawn-call', toolName: 'subagent', input: { prompt: 'work' } }, + ], + }, + ] + + expect(projectForkMessages(messages, 'all', 'spawn-call')).toEqual([ + { role: 'user', content: 'old request' }, + { role: 'assistant', content: [{ type: 'text', text: 'old answer' }] }, + ]) + expect(projectForkMessages(messages, 1, 'spawn-call')).toEqual([ + { role: 'user', content: 'old request' }, + { role: 'assistant', content: [{ type: 'text', text: 'old answer' }] }, + ]) + }) + test('constructs isolated fork state without caller tool, pending, child, or context state', () => { const caller: AgentState = { messages: [{ role: 'user', content: { nested: ['original'] } } as any],