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],