feat(atlas): support plugin tools with metadata.sessionId in orchestration hook
The tool-execute-after hook only processed the built-in 'task' tool.
Plugin tools (e.g. custom-agent-bridge delegate) that set
context.metadata({ metadata: { sessionId } }) were ignored by atlas
orchestration (boulder tracking, verification reminders).
- Add extractSessionIdFromMetadata() to read sessionId from tool
metadata, preferred over text-based extraction
- Extend tool-execute-after guard to also process any tool that has
metadata.sessionId set (not just tool === 'task')
- Recognize 'Background delegate launched' as a background launch
- Add tests for metadata-based session ID extraction
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
|
||||||
import { extractSessionIdFromOutput } from "./subagent-session-id"
|
import { extractSessionIdFromMetadata, extractSessionIdFromOutput } from "./subagent-session-id"
|
||||||
|
|
||||||
describe("extractSessionIdFromOutput", () => {
|
describe("extractSessionIdFromOutput", () => {
|
||||||
test("extracts Session ID blocks from background output", () => {
|
test("extracts Session ID blocks from background output", () => {
|
||||||
@@ -77,3 +77,44 @@ debug log: session_id: ses_wrong_body_789`
|
|||||||
expect(result).toBe("ses_real_metadata_456")
|
expect(result).toBe("ses_real_metadata_456")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("extractSessionIdFromMetadata", () => {
|
||||||
|
test("extracts sessionId from tool metadata object", () => {
|
||||||
|
// given
|
||||||
|
const metadata = { sessionId: "ses_plugin_abc123" }
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = extractSessionIdFromMetadata(metadata)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toBe("ses_plugin_abc123")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns undefined for metadata without sessionId", () => {
|
||||||
|
// given
|
||||||
|
const metadata = { title: "some task" }
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = extractSessionIdFromMetadata(metadata)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns undefined for non-object metadata", () => {
|
||||||
|
expect(extractSessionIdFromMetadata(null)).toBeUndefined()
|
||||||
|
expect(extractSessionIdFromMetadata(undefined)).toBeUndefined()
|
||||||
|
expect(extractSessionIdFromMetadata("string")).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects sessionId values that don't start with ses_", () => {
|
||||||
|
// given
|
||||||
|
const metadata = { sessionId: "not-a-session-id" }
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = extractSessionIdFromMetadata(metadata)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ import { log } from "../../shared/logger"
|
|||||||
import { isSessionInBoulderLineage } from "./boulder-session-lineage"
|
import { isSessionInBoulderLineage } from "./boulder-session-lineage"
|
||||||
import { HOOK_NAME } from "./hook-name"
|
import { HOOK_NAME } from "./hook-name"
|
||||||
|
|
||||||
|
export function extractSessionIdFromMetadata(metadata: unknown): string | undefined {
|
||||||
|
if (metadata && typeof metadata === "object" && "sessionId" in metadata) {
|
||||||
|
const value = (metadata as Record<string, unknown>).sessionId
|
||||||
|
if (typeof value === "string" && value.startsWith("ses_")) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
export function extractSessionIdFromOutput(output: string): string | undefined {
|
export function extractSessionIdFromOutput(output: string): string | undefined {
|
||||||
const taskMetadataBlocks = [...output.matchAll(/<task_metadata>([\s\S]*?)<\/task_metadata>/gi)]
|
const taskMetadataBlocks = [...output.matchAll(/<task_metadata>([\s\S]*?)<\/task_metadata>/gi)]
|
||||||
const lastTaskMetadataBlock = taskMetadataBlocks.at(-1)?.[1]
|
const lastTaskMetadataBlock = taskMetadataBlocks.at(-1)?.[1]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { shouldPauseForFinalWaveApproval } from "./final-wave-approval-gate"
|
|||||||
import { HOOK_NAME } from "./hook-name"
|
import { HOOK_NAME } from "./hook-name"
|
||||||
import { DIRECT_WORK_REMINDER } from "./system-reminder-templates"
|
import { DIRECT_WORK_REMINDER } from "./system-reminder-templates"
|
||||||
import { isSisyphusPath } from "./sisyphus-path"
|
import { isSisyphusPath } from "./sisyphus-path"
|
||||||
import { extractSessionIdFromOutput, validateSubagentSessionId } from "./subagent-session-id"
|
import { extractSessionIdFromMetadata, extractSessionIdFromOutput, validateSubagentSessionId } from "./subagent-session-id"
|
||||||
import {
|
import {
|
||||||
buildCompletionGate,
|
buildCompletionGate,
|
||||||
buildFinalWaveApprovalReminder,
|
buildFinalWaveApprovalReminder,
|
||||||
@@ -105,7 +105,9 @@ export function createToolExecuteAfterHandler(input: {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolInput.tool !== "task") {
|
const metadataSessionId = extractSessionIdFromMetadata(toolOutput.metadata)
|
||||||
|
const isPluginToolWithSession = toolInput.tool !== "task" && !!metadataSessionId
|
||||||
|
if (toolInput.tool !== "task" && !isPluginToolWithSession) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +117,7 @@ export function createToolExecuteAfterHandler(input: {
|
|||||||
pendingTaskRefs.delete(toolInput.callID)
|
pendingTaskRefs.delete(toolInput.callID)
|
||||||
}
|
}
|
||||||
const isBackgroundLaunch = outputStr.includes("Background task launched") || outputStr.includes("Background task continued")
|
const isBackgroundLaunch = outputStr.includes("Background task launched") || outputStr.includes("Background task continued")
|
||||||
|
|| outputStr.includes("Background delegate launched")
|
||||||
if (isBackgroundLaunch) {
|
if (isBackgroundLaunch) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -125,7 +128,7 @@ export function createToolExecuteAfterHandler(input: {
|
|||||||
const verificationDirectory = worktreePath ? worktreePath : ctx.directory
|
const verificationDirectory = worktreePath ? worktreePath : ctx.directory
|
||||||
const gitStats = collectGitDiffStats(verificationDirectory)
|
const gitStats = collectGitDiffStats(verificationDirectory)
|
||||||
const fileChanges = formatFileChanges(gitStats)
|
const fileChanges = formatFileChanges(gitStats)
|
||||||
const extractedSessionId = extractSessionIdFromOutput(toolOutput.output)
|
const extractedSessionId = metadataSessionId ?? extractSessionIdFromOutput(toolOutput.output)
|
||||||
|
|
||||||
if (boulderState) {
|
if (boulderState) {
|
||||||
const progress = getPlanProgress(boulderState.active_plan)
|
const progress = getPlanProgress(boulderState.active_plan)
|
||||||
|
|||||||
Reference in New Issue
Block a user