@@ -10,6 +10,7 @@ import { PartialService } from "@/node/services/partialService";
1010import { TaskService } from "@/node/services/taskService" ;
1111import { createRuntime } from "@/node/runtime/runtimeFactory" ;
1212import { Ok , Err , type Result } from "@/common/types/result" ;
13+ import type { StreamEndEvent } from "@/common/types/stream" ;
1314import { createMuxMessage } from "@/common/types/message" ;
1415import type { WorkspaceMetadata } from "@/common/types/workspace" ;
1516import type { AIService } from "@/node/services/aiService" ;
@@ -896,10 +897,16 @@ describe("TaskService", () => {
896897 const { taskService } = createTaskServiceHarness ( config , { aiService, workspaceService } ) ;
897898
898899 const internal = taskService as unknown as {
899- handleStreamEnd : ( event : { type : "stream-end" ; workspaceId : string } ) => Promise < void > ;
900+ handleStreamEnd : ( event : StreamEndEvent ) => Promise < void > ;
900901 } ;
901902
902- await internal . handleStreamEnd ( { type : "stream-end" , workspaceId : rootWorkspaceId } ) ;
903+ await internal . handleStreamEnd ( {
904+ type : "stream-end" ,
905+ workspaceId : rootWorkspaceId ,
906+ messageId : "assistant-root" ,
907+ metadata : { model : "openai:gpt-5.2" } ,
908+ parts : [ ] ,
909+ } ) ;
903910
904911 expect ( resumeStream ) . toHaveBeenCalledTimes ( 1 ) ;
905912 expect ( resumeStream ) . toHaveBeenCalledWith (
@@ -1261,9 +1268,15 @@ describe("TaskService", () => {
12611268 const { taskService } = createTaskServiceHarness ( config , { workspaceService } ) ;
12621269
12631270 const internal = taskService as unknown as {
1264- handleStreamEnd : ( event : { type : "stream-end" ; workspaceId : string } ) => Promise < void > ;
1271+ handleStreamEnd : ( event : StreamEndEvent ) => Promise < void > ;
12651272 } ;
1266- await internal . handleStreamEnd ( { type : "stream-end" , workspaceId : parentTaskId } ) ;
1273+ await internal . handleStreamEnd ( {
1274+ type : "stream-end" ,
1275+ workspaceId : parentTaskId ,
1276+ messageId : "assistant-parent-task" ,
1277+ metadata : { model : "openai:gpt-4o-mini" } ,
1278+ parts : [ ] ,
1279+ } ) ;
12671280
12681281 expect ( sendMessage ) . not . toHaveBeenCalled ( ) ;
12691282
@@ -1316,9 +1329,15 @@ describe("TaskService", () => {
13161329 const { taskService } = createTaskServiceHarness ( config , { workspaceService } ) ;
13171330
13181331 const internal = taskService as unknown as {
1319- handleStreamEnd : ( event : { type : "stream-end" ; workspaceId : string } ) => Promise < void > ;
1332+ handleStreamEnd : ( event : StreamEndEvent ) => Promise < void > ;
13201333 } ;
1321- await internal . handleStreamEnd ( { type : "stream-end" , workspaceId : parentTaskId } ) ;
1334+ await internal . handleStreamEnd ( {
1335+ type : "stream-end" ,
1336+ workspaceId : parentTaskId ,
1337+ messageId : "assistant-parent-task" ,
1338+ metadata : { model : "openai:gpt-4o-mini" } ,
1339+ parts : [ ] ,
1340+ } ) ;
13221341
13231342 expect ( sendMessage ) . not . toHaveBeenCalled ( ) ;
13241343
@@ -1675,6 +1694,123 @@ describe("TaskService", () => {
16751694 expect ( resumeStream ) . toHaveBeenCalled ( ) ;
16761695 } ) ;
16771696
1697+ test ( "uses agent_report from stream-end parts instead of fallback" , async ( ) => {
1698+ const config = await createTestConfig ( rootDir ) ;
1699+
1700+ const projectPath = path . join ( rootDir , "repo" ) ;
1701+ const parentId = "parent-111" ;
1702+ const childId = "child-222" ;
1703+
1704+ await config . saveConfig ( {
1705+ projects : new Map ( [
1706+ [
1707+ projectPath ,
1708+ {
1709+ workspaces : [
1710+ { path : path . join ( projectPath , "parent" ) , id : parentId , name : "parent" } ,
1711+ {
1712+ path : path . join ( projectPath , "child" ) ,
1713+ id : childId ,
1714+ name : "agent_explore_child" ,
1715+ parentWorkspaceId : parentId ,
1716+ agentType : "explore" ,
1717+ taskStatus : "awaiting_report" ,
1718+ taskModelString : "openai:gpt-4o-mini" ,
1719+ } ,
1720+ ] ,
1721+ } ,
1722+ ] ,
1723+ ] ) ,
1724+ taskSettings : { maxParallelAgentTasks : 3 , maxTaskNestingDepth : 3 } ,
1725+ } ) ;
1726+
1727+ const { aiService } = createAIServiceMocks ( config ) ;
1728+ const { workspaceService, sendMessage, resumeStream, remove } = createWorkspaceServiceMocks ( ) ;
1729+ const { partialService, taskService } = createTaskServiceHarness ( config , {
1730+ aiService,
1731+ workspaceService,
1732+ } ) ;
1733+
1734+ // Simulate the "second attempt" state (the task was already reminded).
1735+ ( taskService as unknown as { remindedAwaitingReport : Set < string > } ) . remindedAwaitingReport . add (
1736+ childId
1737+ ) ;
1738+
1739+ const parentPartial = createMuxMessage (
1740+ "assistant-parent-partial" ,
1741+ "assistant" ,
1742+ "Waiting on subagent…" ,
1743+ { timestamp : Date . now ( ) } ,
1744+ [
1745+ {
1746+ type : "dynamic-tool" ,
1747+ toolCallId : "task-call-1" ,
1748+ toolName : "task" ,
1749+ input : { subagent_type : "explore" , prompt : "do the thing" , title : "Test task" } ,
1750+ state : "input-available" ,
1751+ } ,
1752+ ]
1753+ ) ;
1754+ const writeParentPartial = await partialService . writePartial ( parentId , parentPartial ) ;
1755+ expect ( writeParentPartial . success ) . toBe ( true ) ;
1756+
1757+ const internal = taskService as unknown as {
1758+ handleStreamEnd : ( event : unknown ) => Promise < void > ;
1759+ } ;
1760+
1761+ await internal . handleStreamEnd ( {
1762+ type : "stream-end" ,
1763+ workspaceId : childId ,
1764+ messageId : "assistant-child-output" ,
1765+ metadata : { model : "openai:gpt-4o-mini" } ,
1766+ parts : [
1767+ {
1768+ type : "dynamic-tool" ,
1769+ toolCallId : "agent-report-call-1" ,
1770+ toolName : "agent_report" ,
1771+ input : { reportMarkdown : "Hello from child" , title : "Result" } ,
1772+ state : "output-available" ,
1773+ output : { success : true } ,
1774+ } ,
1775+ ] ,
1776+ } ) ;
1777+
1778+ expect ( sendMessage ) . not . toHaveBeenCalled ( ) ;
1779+
1780+ const updatedParentPartial = await partialService . readPartial ( parentId ) ;
1781+ expect ( updatedParentPartial ) . not . toBeNull ( ) ;
1782+ if ( updatedParentPartial ) {
1783+ const toolPart = updatedParentPartial . parts . find (
1784+ ( p ) =>
1785+ p &&
1786+ typeof p === "object" &&
1787+ "type" in p &&
1788+ ( p as { type ?: unknown } ) . type === "dynamic-tool"
1789+ ) as unknown as
1790+ | {
1791+ toolName : string ;
1792+ state : string ;
1793+ output ?: unknown ;
1794+ }
1795+ | undefined ;
1796+ expect ( toolPart ?. toolName ) . toBe ( "task" ) ;
1797+ expect ( toolPart ?. state ) . toBe ( "output-available" ) ;
1798+ const outputJson = JSON . stringify ( toolPart ?. output ) ;
1799+ expect ( outputJson ) . toContain ( "Hello from child" ) ;
1800+ expect ( outputJson ) . toContain ( "Result" ) ;
1801+ expect ( outputJson ) . not . toContain ( "fallback" ) ;
1802+ }
1803+
1804+ const postCfg = config . loadConfigOrDefault ( ) ;
1805+ const ws = Array . from ( postCfg . projects . values ( ) )
1806+ . flatMap ( ( p ) => p . workspaces )
1807+ . find ( ( w ) => w . id === childId ) ;
1808+ expect ( ws ?. taskStatus ) . toBe ( "reported" ) ;
1809+
1810+ expect ( remove ) . toHaveBeenCalled ( ) ;
1811+ expect ( resumeStream ) . toHaveBeenCalled ( ) ;
1812+ } ) ;
1813+
16781814 test ( "missing agent_report triggers one reminder, then posts fallback output and cleans up" , async ( ) => {
16791815 const config = await createTestConfig ( rootDir ) ;
16801816
@@ -1741,10 +1877,16 @@ describe("TaskService", () => {
17411877 expect ( appendChildHistory . success ) . toBe ( true ) ;
17421878
17431879 const internal = taskService as unknown as {
1744- handleStreamEnd : ( event : { type : "stream-end" ; workspaceId : string } ) => Promise < void > ;
1880+ handleStreamEnd : ( event : StreamEndEvent ) => Promise < void > ;
17451881 } ;
17461882
1747- await internal . handleStreamEnd ( { type : "stream-end" , workspaceId : childId } ) ;
1883+ await internal . handleStreamEnd ( {
1884+ type : "stream-end" ,
1885+ workspaceId : childId ,
1886+ messageId : "assistant-child-output" ,
1887+ metadata : { model : "openai:gpt-4o-mini" } ,
1888+ parts : [ ] ,
1889+ } ) ;
17481890 expect ( sendMessage ) . toHaveBeenCalled ( ) ;
17491891
17501892 const midCfg = config . loadConfigOrDefault ( ) ;
@@ -1753,7 +1895,13 @@ describe("TaskService", () => {
17531895 . find ( ( w ) => w . id === childId ) ;
17541896 expect ( midWs ?. taskStatus ) . toBe ( "awaiting_report" ) ;
17551897
1756- await internal . handleStreamEnd ( { type : "stream-end" , workspaceId : childId } ) ;
1898+ await internal . handleStreamEnd ( {
1899+ type : "stream-end" ,
1900+ workspaceId : childId ,
1901+ messageId : "assistant-child-output" ,
1902+ metadata : { model : "openai:gpt-4o-mini" } ,
1903+ parts : [ ] ,
1904+ } ) ;
17571905
17581906 const emitCalls = ( emit as unknown as { mock : { calls : Array < [ string , unknown ] > } } ) . mock . calls ;
17591907 const metadataEmitsForChild = emitCalls . filter ( ( call ) => {
0 commit comments