@@ -6,8 +6,11 @@ import { tool } from "@intx/agent";
66import type { AgentTool } from "@intx/agent" ;
77import { type } from "arktype" ;
88import type { ReactorEmittedEvent } from "@intx/inference" ;
9+ import { getLogger } from "@intx/log" ;
910import type { ToolDefinition , ToolResult } from "@intx/types/runtime" ;
1011
12+ import { LOG_NAMESPACE_ROOT } from "../branding.js" ;
13+
1114import { runtimeSettingsWithCatalog , type ProviderCatalogEntry } from "../config/index.js" ;
1215import { formatSubAgentTaskAuthFailureMessage } from "./inference-auth-failure.js" ;
1316import type { CapabilityFilter , AgentProfile } from "../agent/profiles.js" ;
@@ -30,6 +33,14 @@ import {
3033} from "../provider/reasoning-effort.js" ;
3134import { isCodexProviderName } from "../config/codex-providers.js" ;
3235import { DEFAULT_CANCEL_REASON , type SubAgentSessionStore } from "./session-store.js" ;
36+ import {
37+ createFleetRecords ,
38+ createSpawnAgentTool ,
39+ createWaitAgentsTool ,
40+ MAX_WAIT_TIMEOUT_MS ,
41+ type AgentFleetDeps ,
42+ type FleetRecordsHandle ,
43+ } from "./agent-fleet.js" ;
3344import { buildDispatchBrief , type TaskIntent } from "./report.js" ;
3445import { appendSubAgentParentHints , type ForcedStopReason } from "./stop-policy.js" ;
3546import {
@@ -55,6 +66,8 @@ import type {
5566 SubAgentSandboxDeps ,
5667} from "./types.js" ;
5768
69+ const log = getLogger ( [ LOG_NAMESPACE_ROOT , "subagent" , "task-tool" ] ) ;
70+
5871export const TaskToolArgs = type ( {
5972 description : "string" ,
6073 prompt : "string" ,
@@ -186,6 +199,8 @@ export type TaskToolDeps = SubAgentSandboxDeps & {
186199 // Records sub-agent starts and outcomes. Injected so the tool has no
187200 // process-wide dependency; omitting it makes dispatch silent.
188201 telemetry ?: Telemetry ;
202+ /** Shared with spawn_agent/wait_agents when this task tool is fleet-backed. */
203+ fleetRecords ?: FleetRecordsHandle ;
189204} ;
190205
191206function taskToolResult (
@@ -249,11 +264,159 @@ function requiredTaskFieldsError(
249264 return message ;
250265}
251266
267+ async function runTaskViaFleet ( input : {
268+ callId : string ;
269+ signal : AbortSignal ;
270+ description : string ;
271+ prompt : string ;
272+ context : string | undefined ;
273+ agentId : string | undefined ;
274+ goals : string [ ] ;
275+ intent : TaskIntent | undefined ;
276+ successCriteria : string [ ] ;
277+ doNot : string [ ] ;
278+ reportFocus : string | undefined ;
279+ deps : TaskToolDeps ;
280+ sessions : SubAgentSessionStore ;
281+ fleetRecords : FleetRecordsHandle ;
282+ } ) : Promise < ToolResult > {
283+ const fleetDeps : AgentFleetDeps = {
284+ permissionGate : input . deps . permissionGate ,
285+ ...( input . deps . inheritMcpTools !== undefined
286+ ? { inheritMcpTools : input . deps . inheritMcpTools }
287+ : { } ) ,
288+ ...( input . deps . shellTimeout !== undefined ? { shellTimeout : input . deps . shellTimeout } : { } ) ,
289+ ...( input . deps . shellEnv !== undefined ? { shellEnv : input . deps . shellEnv } : { } ) ,
290+ ...( input . deps . extraToolPlugins !== undefined
291+ ? { extraToolPlugins : input . deps . extraToolPlugins }
292+ : { } ) ,
293+ ...( input . deps . getBlobReader !== undefined ? { getBlobReader : input . deps . getBlobReader } : { } ) ,
294+ cwd : input . deps . cwd ,
295+ getWorkdirBase : input . deps . getWorkdirBase ,
296+ provider : input . deps . provider ,
297+ run : input . deps . run ,
298+ sessions : input . sessions ,
299+ fleetRecords : input . fleetRecords ,
300+ persist : false ,
301+ ...( input . deps . parentSessionId !== undefined
302+ ? { parentSessionId : input . deps . parentSessionId }
303+ : { } ) ,
304+ ...( input . deps . spawnAllowlist !== undefined
305+ ? { spawnAllowlist : input . deps . spawnAllowlist }
306+ : { } ) ,
307+ ...( input . deps . allowOrchestrator !== undefined
308+ ? { allowOrchestrator : input . deps . allowOrchestrator }
309+ : { } ) ,
310+ ...( input . deps . useWorktree !== undefined ? { useWorktree : input . deps . useWorktree } : { } ) ,
311+ ...( input . deps . deadlineMs !== undefined ? { deadlineMs : input . deps . deadlineMs } : { } ) ,
312+ ...( input . deps . settings !== undefined ? { settings : input . deps . settings } : { } ) ,
313+ ...( input . deps . catalog !== undefined ? { catalog : input . deps . catalog } : { } ) ,
314+ ...( input . deps . onEvent !== undefined ? { onEvent : input . deps . onEvent } : { } ) ,
315+ ...( input . deps . onProgress !== undefined ? { onProgress : input . deps . onProgress } : { } ) ,
316+ ...( input . deps . telemetry !== undefined ? { telemetry : input . deps . telemetry } : { } ) ,
317+ } ;
318+ const spawn = createSpawnAgentTool ( fleetDeps ) ;
319+ const wait = createWaitAgentsTool ( {
320+ sessions : input . sessions ,
321+ fleetRecords : input . fleetRecords ,
322+ } ) ;
323+ if ( spawn . kind !== "full" || wait . kind !== "full" ) {
324+ return taskToolResult ( input . callId , "Error: fleet tools are unavailable." ) ;
325+ }
326+ const started = await spawn . handler (
327+ {
328+ id : input . callId ,
329+ name : "spawn_agent" ,
330+ arguments : {
331+ description : input . description ,
332+ prompt : input . prompt ,
333+ ...( input . context !== undefined ? { context : input . context } : { } ) ,
334+ ...( input . agentId !== undefined ? { agent : input . agentId } : { } ) ,
335+ ...( input . goals . length > 0 ? { goals : input . goals } : { } ) ,
336+ ...( input . intent !== undefined ? { intent : input . intent } : { } ) ,
337+ ...( input . successCriteria . length > 0 ? { success_criteria : input . successCriteria } : { } ) ,
338+ ...( input . doNot . length > 0 ? { do_not : input . doNot } : { } ) ,
339+ ...( input . reportFocus !== undefined ? { report_focus : input . reportFocus } : { } ) ,
340+ } ,
341+ } ,
342+ input . signal ,
343+ ) ;
344+ const startedText =
345+ typeof started . content === "string" ? started . content : JSON . stringify ( started . content ) ;
346+ if ( started . isError === true || startedText . startsWith ( "Error:" ) ) {
347+ return taskToolResult ( input . callId , startedText ) ;
348+ }
349+ let agentId : string ;
350+ try {
351+ const parsed = JSON . parse ( startedText ) as { agent_id ?: unknown } ;
352+ if ( typeof parsed . agent_id !== "string" || parsed . agent_id . length === 0 ) {
353+ return taskToolResult ( input . callId , "Error: spawn_agent returned no agent_id." ) ;
354+ }
355+ agentId = parsed . agent_id ;
356+ } catch ( err ) {
357+ log . error ( "spawn_agent payload was not JSON: {error}" , {
358+ error : err instanceof Error ? err . message : String ( err ) ,
359+ } ) ;
360+ return taskToolResult (
361+ input . callId ,
362+ `Error: spawn_agent returned invalid payload: ${ startedText } ` ,
363+ ) ;
364+ }
365+
366+ while ( ! input . signal . aborted ) {
367+ const waited = await wait . handler (
368+ {
369+ id : `${ input . callId } -wait` ,
370+ name : "wait_agents" ,
371+ arguments : { targets : [ agentId ] , mode : "all" , timeout_ms : MAX_WAIT_TIMEOUT_MS } ,
372+ } ,
373+ input . signal ,
374+ ) ;
375+ const waitedText =
376+ typeof waited . content === "string" ? waited . content : JSON . stringify ( waited . content ) ;
377+ if ( waited . isError === true || waitedText . startsWith ( "Error:" ) ) {
378+ return taskToolResult ( input . callId , waitedText ) ;
379+ }
380+ let payload : {
381+ timed_out ?: boolean ;
382+ results ?: { status ?: string ; report ?: string ; error ?: string } [ ] ;
383+ } ;
384+ try {
385+ payload = JSON . parse ( waitedText ) as typeof payload ;
386+ } catch ( err ) {
387+ log . error ( "wait_agents payload was not JSON: {error}" , {
388+ error : err instanceof Error ? err . message : String ( err ) ,
389+ } ) ;
390+ return taskToolResult (
391+ input . callId ,
392+ `Error: wait_agents returned invalid payload: ${ waitedText } ` ,
393+ ) ;
394+ }
395+ if ( payload . timed_out === true ) continue ;
396+ const result = payload . results ?. [ 0 ] ;
397+ if ( result === undefined ) {
398+ return taskToolResult ( input . callId , `Error: wait_agents returned no result for ${ agentId } .` ) ;
399+ }
400+ if ( result . status === "failed" ) {
401+ return taskToolResult (
402+ input . callId ,
403+ `Error: sub-agent "${ input . description } " failed: ${ result . error ?? "unknown error" } ` ,
404+ ) ;
405+ }
406+ const report = result . report ?? "" ;
407+ return taskToolResult ( input . callId , `Sub-agent "${ input . description } " reported:\n\n${ report } ` ) ;
408+ }
409+ return taskToolResult ( input . callId , `Sub-agent "${ input . description } " cancelled by operator.` ) ;
410+ }
411+
252412export function createTaskTool ( deps : TaskToolDeps ) : AgentTool {
253413 const run = deps . run ;
254414 const telemetry = deps . telemetry ?? NOOP_TELEMETRY ;
255415 // Session-scoped re-dispatch ledger: one per parent task tool instance.
256416 const briefLedger = createBriefDispatchLedger ( ) ;
417+ const fleetSessions = deps . sessions ;
418+ const fleetRecords =
419+ deps . fleetRecords ?? ( fleetSessions !== undefined ? createFleetRecords ( ) : undefined ) ;
257420 // Every completed dispatch gets an outcome record — the log otherwise
258421 // carries shape and run state but never what the run actually produced.
259422 // Tagged with the dispatched child's provider/model/family so
@@ -334,6 +497,32 @@ export function createTaskTool(deps: TaskToolDeps): AgentTool {
334497 return taskToolResult ( call . id , requiredTaskFieldsError ( args , empty ) ) ;
335498 }
336499
500+ // Closed-director task() is spawn_agent + wait_agents. Custom profiles
501+ // still use the legacy await-run path until spawn grows profile lookup.
502+ const agentForFleet = typeof args . agent === "string" ? args . agent : undefined ;
503+ const canUseFleet =
504+ fleetSessions !== undefined &&
505+ fleetRecords !== undefined &&
506+ ( agentForFleet === undefined || agentForFleet . length === 0 || isDirectorId ( agentForFleet ) ) ;
507+ if ( canUseFleet ) {
508+ return await runTaskViaFleet ( {
509+ callId : call . id ,
510+ signal,
511+ description,
512+ prompt,
513+ context,
514+ agentId,
515+ goals,
516+ intent,
517+ successCriteria,
518+ doNot,
519+ reportFocus,
520+ deps,
521+ sessions : fleetSessions ,
522+ fleetRecords,
523+ } ) ;
524+ }
525+
337526 let provider : SubAgentProvider =
338527 typeof deps . provider === "function" ? deps . provider ( ) : deps . provider ;
339528 // Snapshot parent effort before profile-inference rebuilds so role-default
0 commit comments