feat: messages API returns parts with accurate running status for active subagents
This commit is contained in:
parent
a7a00a1a26
commit
bd968b8266
|
|
@ -747,6 +747,21 @@ export async function* runAgent({
|
||||||
agentType: agentDefinition.agentType,
|
agentType: agentDefinition.agentType,
|
||||||
...(worktreePath && { worktreePath }),
|
...(worktreePath && { worktreePath }),
|
||||||
...(description && { description }),
|
...(description && { description }),
|
||||||
|
parent_session_id: getSessionId(),
|
||||||
|
prompt: promptMessages
|
||||||
|
.map((m: Message) => {
|
||||||
|
const content = (m as Record<string, unknown>).content
|
||||||
|
if (typeof content === 'string') return content
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
return content
|
||||||
|
.filter((b: Record<string, unknown>) => b.type === 'text')
|
||||||
|
.map((b: Record<string, unknown>) => b.text)
|
||||||
|
.join('')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(-1)[0] ?? undefined,
|
||||||
}).catch(_err => logForDebugging(`Failed to write agent metadata: ${_err}`))
|
}).catch(_err => logForDebugging(`Failed to write agent metadata: ${_err}`))
|
||||||
|
|
||||||
// Track the last recorded message UUID for parent chain continuity
|
// Track the last recorded message UUID for parent chain continuity
|
||||||
|
|
@ -812,6 +827,7 @@ export async function* runAgent({
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
message.agent_id = agentId
|
||||||
yield message as Message
|
yield message as Message
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -828,6 +844,7 @@ export async function* runAgent({
|
||||||
if (message.type !== 'progress') {
|
if (message.type !== 'progress') {
|
||||||
lastRecordedUuid = message.uuid
|
lastRecordedUuid = message.uuid
|
||||||
}
|
}
|
||||||
|
message.agent_id = agentId
|
||||||
yield message
|
yield message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -789,9 +789,10 @@ export class QueryEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'tombstone':
|
case 'tombstone': {
|
||||||
// Tombstone messages are control signals for removing messages, skip them
|
yield message as SDKMessage
|
||||||
break
|
break
|
||||||
|
}
|
||||||
case 'assistant': {
|
case 'assistant': {
|
||||||
// Capture stop_reason if already set (synthetic messages). For
|
// Capture stop_reason if already set (synthetic messages). For
|
||||||
// streamed responses, this is null at content_block_stop time;
|
// streamed responses, this is null at content_block_stop time;
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,60 @@ import { notFound } from '../errors.js'
|
||||||
import {
|
import {
|
||||||
readSessionMessages,
|
readSessionMessages,
|
||||||
readSessionTodos,
|
readSessionTodos,
|
||||||
|
readSessionTasks,
|
||||||
readSessionDiff,
|
readSessionDiff,
|
||||||
type SessionMessage,
|
type SessionMessage,
|
||||||
|
type TaskInfo,
|
||||||
|
type MessagePart,
|
||||||
|
decomposeMessageToParts,
|
||||||
} from '../transcriptReader.js'
|
} from '../transcriptReader.js'
|
||||||
|
import { getSubagentProgress } from '../sessionMessageRouter.js'
|
||||||
|
|
||||||
|
function findParentCwd(sessionManager: SessionManager, id: string): string | undefined {
|
||||||
|
for (const handle of sessionManager.getAllSessions()) {
|
||||||
|
if (handle.activeSubagents.has(id)) {
|
||||||
|
return handle.spawnCwd ?? handle.cwd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchActiveSubagentsInMessages(
|
||||||
|
messages: SessionMessage[],
|
||||||
|
activeSubagents: ReadonlyMap<string, { agentId: string; toolUseId?: string }>,
|
||||||
|
): void {
|
||||||
|
if (activeSubagents.size === 0) return
|
||||||
|
|
||||||
|
const runningCallIDs = new Set<string>()
|
||||||
|
const progressByCallID = new Map<string, string[]>()
|
||||||
|
|
||||||
|
for (const [, info] of activeSubagents) {
|
||||||
|
if (info.toolUseId) {
|
||||||
|
runningCallIDs.add(info.toolUseId)
|
||||||
|
const progress = getSubagentProgress(info.agentId)
|
||||||
|
if (progress?.progressLines?.length) {
|
||||||
|
progressByCallID.set(info.toolUseId, progress.progressLines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runningCallIDs.size === 0) return
|
||||||
|
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (!Array.isArray(msg.parts)) continue
|
||||||
|
for (const part of msg.parts) {
|
||||||
|
if (part.type !== 'tool') continue
|
||||||
|
const toolPart = part as MessagePart & { type: 'tool'; callID: string; state: Record<string, unknown> }
|
||||||
|
if (!runningCallIDs.has(toolPart.callID)) continue
|
||||||
|
toolPart.state = {
|
||||||
|
...toolPart.state,
|
||||||
|
status: 'running',
|
||||||
|
progress: progressByCallID.get(toolPart.callID) ?? [],
|
||||||
|
time: { start: (toolPart.state as any).time?.start ?? Date.now() },
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function dedupeMessages(disk: SessionMessage[], memory: SessionMessage[]): SessionMessage[] {
|
function dedupeMessages(disk: SessionMessage[], memory: SessionMessage[]): SessionMessage[] {
|
||||||
if (disk.length === 0) return memory
|
if (disk.length === 0) return memory
|
||||||
|
|
@ -20,6 +71,8 @@ export function createMessageRoutes(sessionManager: SessionManager): Hono {
|
||||||
.get('/session/:sessionID/message', async c => {
|
.get('/session/:sessionID/message', async c => {
|
||||||
const id = c.req.param('sessionID')
|
const id = c.req.param('sessionID')
|
||||||
const handle = sessionManager.getSession(id)
|
const handle = sessionManager.getSession(id)
|
||||||
|
const parentCwd = handle ? undefined : findParentCwd(sessionManager, id)
|
||||||
|
const effectiveCwd = handle?.spawnCwd ?? handle?.cwd ?? parentCwd
|
||||||
|
|
||||||
const url = new URL(c.req.url)
|
const url = new URL(c.req.url)
|
||||||
const limit = parseInt(url.searchParams.get('limit') ?? '50', 10)
|
const limit = parseInt(url.searchParams.get('limit') ?? '50', 10)
|
||||||
|
|
@ -28,7 +81,7 @@ export function createMessageRoutes(sessionManager: SessionManager): Hono {
|
||||||
|
|
||||||
const { messages: diskMessages, nextCursor } = await readSessionMessages({
|
const { messages: diskMessages, nextCursor } = await readSessionMessages({
|
||||||
sessionId: id,
|
sessionId: id,
|
||||||
cwd: handle?.spawnCwd ?? handle?.cwd,
|
cwd: effectiveCwd,
|
||||||
limit,
|
limit,
|
||||||
before,
|
before,
|
||||||
includeSystem,
|
includeSystem,
|
||||||
|
|
@ -38,12 +91,25 @@ export function createMessageRoutes(sessionManager: SessionManager): Hono {
|
||||||
? dedupeMessages(diskMessages, [...handle.messageBuffer])
|
? dedupeMessages(diskMessages, [...handle.messageBuffer])
|
||||||
: diskMessages
|
: diskMessages
|
||||||
|
|
||||||
|
const tombstoned = handle?.tombstonedUuids
|
||||||
|
const filtered = tombstoned && tombstoned.size > 0
|
||||||
|
? messages.filter(m => !tombstoned.has(m.uuid))
|
||||||
|
: messages
|
||||||
|
|
||||||
|
const decomposed = filtered.map(decomposeMessageToParts)
|
||||||
|
|
||||||
|
if (handle) {
|
||||||
|
patchActiveSubagentsInMessages(decomposed, handle.activeSubagents)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = decomposed
|
||||||
|
|
||||||
if (nextCursor) {
|
if (nextCursor) {
|
||||||
c.header('Link', `<${url.pathname}?limit=${limit}&before=${nextCursor}>; rel="prev"`)
|
c.header('Link', `<${url.pathname}?limit=${limit}&before=${nextCursor}>; rel="prev"`)
|
||||||
c.header('X-Next-Cursor', nextCursor)
|
c.header('X-Next-Cursor', nextCursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({ messages })
|
return c.json({ messages: result })
|
||||||
})
|
})
|
||||||
.get('/session/:sessionID/todo', async c => {
|
.get('/session/:sessionID/todo', async c => {
|
||||||
const id = c.req.param('sessionID')
|
const id = c.req.param('sessionID')
|
||||||
|
|
@ -56,6 +122,17 @@ export function createMessageRoutes(sessionManager: SessionManager): Hono {
|
||||||
|
|
||||||
return c.json({ todos })
|
return c.json({ todos })
|
||||||
})
|
})
|
||||||
|
.get('/session/:sessionID/tasks', async c => {
|
||||||
|
const id = c.req.param('sessionID')
|
||||||
|
const handle = sessionManager.getSession(id)
|
||||||
|
|
||||||
|
const tasks = await readSessionTasks({
|
||||||
|
sessionId: id,
|
||||||
|
cwd: handle?.spawnCwd ?? handle?.cwd,
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.json({ tasks })
|
||||||
|
})
|
||||||
.get('/session/:sessionID/diff', async c => {
|
.get('/session/:sessionID/diff', async c => {
|
||||||
const id = c.req.param('sessionID')
|
const id = c.req.param('sessionID')
|
||||||
const handle = sessionManager.getSession(id)
|
const handle = sessionManager.getSession(id)
|
||||||
|
|
|
||||||
|
|
@ -528,7 +528,16 @@ export function createSessionRoutes(
|
||||||
.post('/session/:sessionID/abort', async c => {
|
.post('/session/:sessionID/abort', async c => {
|
||||||
const id = c.req.param('sessionID')
|
const id = c.req.param('sessionID')
|
||||||
const handle = sessionManager.getSession(id)
|
const handle = sessionManager.getSession(id)
|
||||||
if (!handle) return c.json({ aborted: true })
|
if (!handle) {
|
||||||
|
// Even when no in-memory handle exists, emit idle status so that
|
||||||
|
// a prior prompt_async busy event (which fires before the handle
|
||||||
|
// is created) is not left dangling forever.
|
||||||
|
eventBus.publish('session.status', {
|
||||||
|
sessionID: id,
|
||||||
|
status: { type: 'idle' },
|
||||||
|
})
|
||||||
|
return c.json({ aborted: true })
|
||||||
|
}
|
||||||
await handle.abort()
|
await handle.abort()
|
||||||
return c.json({ aborted: true })
|
return c.json({ aborted: true })
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,12 @@ export type MessageRouterCtx = {
|
||||||
addTombstonedUuid(uuid: string): void
|
addTombstonedUuid(uuid: string): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSubagentProgress(agentId: string): { progressLines: string[]; mainToolUseID: string } | undefined {
|
||||||
|
const state = subagentToolState.get(agentId)
|
||||||
|
if (!state) return undefined
|
||||||
|
return { progressLines: state.progressLines, mainToolUseID: state.mainToolUseID }
|
||||||
|
}
|
||||||
|
|
||||||
export function routeMessage(msg: StdoutMessage, ctx: MessageRouterCtx): void {
|
export function routeMessage(msg: StdoutMessage, ctx: MessageRouterCtx): void {
|
||||||
if (
|
if (
|
||||||
ctx.getStatus() === 'starting' &&
|
ctx.getStatus() === 'starting' &&
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,10 @@ type TaskStartedEvent = {
|
||||||
task_type?: string
|
task_type?: string
|
||||||
workflow_name?: string
|
workflow_name?: string
|
||||||
prompt?: string
|
prompt?: string
|
||||||
|
/** Explicit agent_id for virtual session mapping in serve mode.
|
||||||
|
* Same value as task_id but named for clarity. Enables the serve layer
|
||||||
|
* to construct virtual child sessions without guessing the relationship. */
|
||||||
|
agent_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type TaskProgressEvent = {
|
type TaskProgressEvent = {
|
||||||
|
|
@ -27,10 +31,9 @@ type TaskProgressEvent = {
|
||||||
}
|
}
|
||||||
last_tool_name?: string
|
last_tool_name?: string
|
||||||
summary?: string
|
summary?: string
|
||||||
// Delta batch of workflow state changes. Clients upsert by
|
|
||||||
// `${type}:${index}` then group by phaseIndex to rebuild the phase tree,
|
|
||||||
// same fold as collectFromEvents + groupByPhase in PhaseProgress.tsx.
|
|
||||||
workflow_progress?: SdkWorkflowProgress[]
|
workflow_progress?: SdkWorkflowProgress[]
|
||||||
|
/** Explicit agent_id for virtual session mapping in serve mode. */
|
||||||
|
agent_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emitted when a foreground agent completes without being backgrounded.
|
// Emitted when a foreground agent completes without being backgrounded.
|
||||||
|
|
@ -51,6 +54,8 @@ type TaskNotificationSdkEvent = {
|
||||||
tool_uses: number
|
tool_uses: number
|
||||||
duration_ms: number
|
duration_ms: number
|
||||||
}
|
}
|
||||||
|
/** Explicit agent_id for virtual session mapping in serve mode. */
|
||||||
|
agent_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionStateChangedEvent is now emitted directly by sessionState.ts's
|
// SessionStateChangedEvent is now emitted directly by sessionState.ts's
|
||||||
|
|
@ -122,5 +127,6 @@ export function emitTaskTerminatedSdk(
|
||||||
output_file: opts?.outputFile ?? '',
|
output_file: opts?.outputFile ?? '',
|
||||||
summary: opts?.summary ?? '',
|
summary: opts?.summary ?? '',
|
||||||
usage: opts?.usage,
|
usage: opts?.usage,
|
||||||
|
agent_id: taskId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,14 @@ export type AgentMetadata = {
|
||||||
* resumed agent's notification can show the original description instead
|
* resumed agent's notification can show the original description instead
|
||||||
* of a placeholder. Optional — older metadata files lack this field. */
|
* of a placeholder. Optional — older metadata files lack this field. */
|
||||||
description?: string
|
description?: string
|
||||||
|
/** Parent session ID for virtual session hierarchy in serve mode.
|
||||||
|
* Enables the serve layer to reconstruct subagent → parent relationships
|
||||||
|
* on restart. Optional — older metadata files lack this field. */
|
||||||
|
parent_session_id?: string
|
||||||
|
/** Agent-specific prompt for the subagent. Persisted so the serve layer
|
||||||
|
* can construct a virtual session with the original prompt context.
|
||||||
|
* Optional — older metadata files lack this field. */
|
||||||
|
prompt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ export function registerTask(task: TaskState, setAppState: SetAppState): void {
|
||||||
? (task.workflowName as string | undefined)
|
? (task.workflowName as string | undefined)
|
||||||
: undefined,
|
: undefined,
|
||||||
prompt: 'prompt' in task ? (task.prompt as string) : undefined,
|
prompt: 'prompt' in task ? (task.prompt as string) : undefined,
|
||||||
|
agent_id: task.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,5 +32,6 @@ export function emitTaskProgress(params: {
|
||||||
last_tool_name: params.lastToolName,
|
last_tool_name: params.lastToolName,
|
||||||
summary: params.summary,
|
summary: params.summary,
|
||||||
workflow_progress: params.workflowProgress,
|
workflow_progress: params.workflowProgress,
|
||||||
|
agent_id: params.taskId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user