135 lines
4.0 KiB
TypeScript
135 lines
4.0 KiB
TypeScript
import type { UUID } from 'crypto'
|
|
import { randomUUID } from 'crypto'
|
|
import { getIsNonInteractiveSession, getSessionId } from '../bootstrap/state.js'
|
|
import type { SdkWorkflowProgress } from '../types/tools.js'
|
|
|
|
type TaskStartedEvent = {
|
|
type: 'system'
|
|
subtype: 'task_started'
|
|
task_id: string
|
|
tool_use_id?: string
|
|
description: string
|
|
task_type?: string
|
|
workflow_name?: string
|
|
prompt?: string
|
|
}
|
|
|
|
type TaskProgressEvent = {
|
|
type: 'system'
|
|
subtype: 'task_progress'
|
|
task_id: string
|
|
tool_use_id?: string
|
|
description: string
|
|
usage: {
|
|
total_tokens: number
|
|
tool_uses: number
|
|
duration_ms: number
|
|
}
|
|
last_tool_name?: 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[]
|
|
}
|
|
|
|
// Emitted when a foreground agent completes without being backgrounded.
|
|
// Drained by drainSdkEvents() directly into the output stream — does NOT
|
|
// go through the print.ts XML task_notification parser and does NOT trigger
|
|
// the LLM loop. Consumers (e.g. VS Code session.ts) use this to remove the
|
|
// task from the subagent panel.
|
|
type TaskNotificationSdkEvent = {
|
|
type: 'system'
|
|
subtype: 'task_notification'
|
|
task_id: string
|
|
tool_use_id?: string
|
|
status: 'completed' | 'failed' | 'stopped'
|
|
output_file: string
|
|
summary: string
|
|
usage?: {
|
|
total_tokens: number
|
|
tool_uses: number
|
|
duration_ms: number
|
|
}
|
|
}
|
|
|
|
// Mirrors notifySessionStateChanged. The CCR bridge already receives this
|
|
// via its own listener; SDK consumers (scmuxd, VS Code) need the same signal
|
|
// to know when the main turn's generator is idle vs actively producing.
|
|
// The 'idle' transition fires AFTER heldBackResult flushes and the bg-agent
|
|
// do-while loop exits — so SDK consumers can trust it as the authoritative
|
|
// "turn is over" signal even when result was withheld for background agents.
|
|
type SessionStateChangedEvent = {
|
|
type: 'system'
|
|
subtype: 'session_state_changed'
|
|
state: 'idle' | 'running' | 'requires_action'
|
|
}
|
|
|
|
export type SdkEvent =
|
|
| TaskStartedEvent
|
|
| TaskProgressEvent
|
|
| TaskNotificationSdkEvent
|
|
| SessionStateChangedEvent
|
|
|
|
const MAX_QUEUE_SIZE = 1000
|
|
const queue: SdkEvent[] = []
|
|
|
|
export function enqueueSdkEvent(event: SdkEvent): void {
|
|
// SDK events are only consumed (drained) in headless/streaming mode.
|
|
// In TUI mode they would accumulate up to the cap and never be read.
|
|
if (!getIsNonInteractiveSession()) {
|
|
return
|
|
}
|
|
if (queue.length >= MAX_QUEUE_SIZE) {
|
|
queue.shift()
|
|
}
|
|
queue.push(event)
|
|
}
|
|
|
|
export function drainSdkEvents(): Array<
|
|
SdkEvent & { uuid: UUID; session_id: string }
|
|
> {
|
|
if (queue.length === 0) {
|
|
return []
|
|
}
|
|
const events = queue.splice(0)
|
|
return events.map(e => ({
|
|
...e,
|
|
uuid: randomUUID(),
|
|
session_id: getSessionId(),
|
|
}))
|
|
}
|
|
|
|
/**
|
|
* Emit a task_notification SDK event for a task reaching a terminal state.
|
|
*
|
|
* registerTask() always emits task_started; this is the closing bookend.
|
|
* Call this from any exit path that sets a task terminal WITHOUT going
|
|
* through enqueuePendingNotification-with-<task-id> (print.ts parses that
|
|
* XML into the same SDK event, so paths that do both would double-emit).
|
|
* Paths that suppress the XML notification (notified:true pre-set, kill
|
|
* paths, abort branches) must call this directly so SDK consumers
|
|
* (Scuttle's bg-task dot, VS Code subagent panel) see the task close.
|
|
*/
|
|
export function emitTaskTerminatedSdk(
|
|
taskId: string,
|
|
status: 'completed' | 'failed' | 'stopped',
|
|
opts?: {
|
|
toolUseId?: string
|
|
summary?: string
|
|
outputFile?: string
|
|
usage?: { total_tokens: number; tool_uses: number; duration_ms: number }
|
|
},
|
|
): void {
|
|
enqueueSdkEvent({
|
|
type: 'system',
|
|
subtype: 'task_notification',
|
|
task_id: taskId,
|
|
tool_use_id: opts?.toolUseId,
|
|
status,
|
|
output_file: opts?.outputFile ?? '',
|
|
summary: opts?.summary ?? '',
|
|
usage: opts?.usage,
|
|
})
|
|
}
|