@@ -56,6 +56,10 @@ use a3s_code_core::hooks::{
5656 HookMatcher as RustHookMatcher , HookResponse as RustHookResponse ,
5757} ;
5858use a3s_code_core:: llm:: { ContentBlock as RustContentBlock , Message as RustMessage } ;
59+ use a3s_code_core:: orchestration:: {
60+ execute_steps_parallel, execute_steps_parallel_resumable, AgentStepSpec as RustAgentStepSpec ,
61+ StepOutcome as RustStepOutcome ,
62+ } ;
5963use a3s_code_core:: permissions:: {
6064 PermissionDecision as RustPermissionDecision , PermissionPolicy as RustPermissionPolicy ,
6165 PermissionRule as RustPermissionRule ,
@@ -3012,6 +3016,67 @@ pub struct McpServerStatusEntry {
30123016// Session
30133017// ============================================================================
30143018
3019+ /// One unit of orchestrated agent work — what to run, independent of where.
3020+ #[ napi( object) ]
3021+ #[ derive( Clone ) ]
3022+ pub struct AgentStepSpecObject {
3023+ /// Stable id for this step (assigned by the caller).
3024+ pub task_id : String ,
3025+ /// Registry key of the agent to run (e.g. "explore", "review").
3026+ pub agent : String ,
3027+ /// Short label for display/tracking.
3028+ pub description : String ,
3029+ /// Instruction handed to the child agent.
3030+ pub prompt : String ,
3031+ /// Optional per-step tool-round cap.
3032+ pub max_steps : Option < u32 > ,
3033+ /// Optional parent session id for event correlation.
3034+ pub parent_session_id : Option < String > ,
3035+ /// When set, the step must return JSON conforming to this schema; the
3036+ /// validated object lands in `StepOutcomeObject.structured`.
3037+ pub output_schema : Option < serde_json:: Value > ,
3038+ }
3039+
3040+ impl From < AgentStepSpecObject > for RustAgentStepSpec {
3041+ fn from ( o : AgentStepSpecObject ) -> Self {
3042+ RustAgentStepSpec {
3043+ task_id : o. task_id ,
3044+ agent : o. agent ,
3045+ description : o. description ,
3046+ prompt : o. prompt ,
3047+ max_steps : o. max_steps . map ( |n| n as usize ) ,
3048+ parent_session_id : o. parent_session_id ,
3049+ output_schema : o. output_schema ,
3050+ }
3051+ }
3052+ }
3053+
3054+ /// The result of running one orchestrated step.
3055+ #[ napi( object) ]
3056+ #[ derive( Clone ) ]
3057+ pub struct StepOutcomeObject {
3058+ pub task_id : String ,
3059+ pub session_id : String ,
3060+ pub agent : String ,
3061+ pub output : String ,
3062+ pub success : bool ,
3063+ /// Schema-validated structured output, when the step requested one.
3064+ pub structured : Option < serde_json:: Value > ,
3065+ }
3066+
3067+ impl From < RustStepOutcome > for StepOutcomeObject {
3068+ fn from ( o : RustStepOutcome ) -> Self {
3069+ StepOutcomeObject {
3070+ task_id : o. task_id ,
3071+ session_id : o. session_id ,
3072+ agent : o. agent ,
3073+ output : o. output ,
3074+ success : o. success ,
3075+ structured : o. structured ,
3076+ }
3077+ }
3078+ }
3079+
30153080/// Workspace-bound session. All LLM and tool operations happen here.
30163081#[ napi]
30173082pub struct Session {
@@ -3070,6 +3135,60 @@ impl Session {
30703135 Ok ( AgentResult :: from ( result) )
30713136 }
30723137
3138+ /// Run `specs` as a fan-out of agent steps, bounded by the session's
3139+ /// configured parallelism, and resolve with each step's outcome in input
3140+ /// order. A failed step surfaces as `success: false` without failing the
3141+ /// batch.
3142+ #[ napi]
3143+ pub async fn parallel (
3144+ & self ,
3145+ specs : Vec < AgentStepSpecObject > ,
3146+ ) -> napi:: Result < Vec < StepOutcomeObject > > {
3147+ let session = self . inner . clone ( ) ;
3148+ let rust_specs: Vec < RustAgentStepSpec > = specs. into_iter ( ) . map ( Into :: into) . collect ( ) ;
3149+ let outcomes = get_runtime ( )
3150+ . spawn ( async move {
3151+ let executor = session. agent_executor ( ) ;
3152+ execute_steps_parallel ( executor, rust_specs, None ) . await
3153+ } )
3154+ . await
3155+ . map_err ( |e| napi:: Error :: from_reason ( format ! ( "Task join error: {e}" ) ) ) ?;
3156+ Ok ( outcomes. into_iter ( ) . map ( StepOutcomeObject :: from) . collect ( ) )
3157+ }
3158+
3159+ /// Like `parallel`, but resumable: progress is journaled under
3160+ /// `workflowId` via the session's `sessionStore`, so an interrupted run
3161+ /// skips already-completed steps. Rejects when no `sessionStore` is
3162+ /// configured.
3163+ #[ napi]
3164+ pub async fn parallel_resumable (
3165+ & self ,
3166+ specs : Vec < AgentStepSpecObject > ,
3167+ workflow_id : String ,
3168+ ) -> napi:: Result < Vec < StepOutcomeObject > > {
3169+ let session = self . inner . clone ( ) ;
3170+ let rust_specs: Vec < RustAgentStepSpec > = specs. into_iter ( ) . map ( Into :: into) . collect ( ) ;
3171+ let outcomes = get_runtime ( )
3172+ . spawn ( async move {
3173+ let Some ( store) = session. session_store ( ) else {
3174+ return Err ( "parallelResumable requires a sessionStore on the session" ) ;
3175+ } ;
3176+ let executor = session. agent_executor ( ) ;
3177+ Ok ( execute_steps_parallel_resumable (
3178+ executor,
3179+ rust_specs,
3180+ & workflow_id,
3181+ store,
3182+ None ,
3183+ )
3184+ . await )
3185+ } )
3186+ . await
3187+ . map_err ( |e| napi:: Error :: from_reason ( format ! ( "Task join error: {e}" ) ) ) ?
3188+ . map_err ( napi:: Error :: from_reason) ?;
3189+ Ok ( outcomes. into_iter ( ) . map ( StepOutcomeObject :: from) . collect ( ) )
3190+ }
3191+
30733192 /// Send a prompt or request and get a streaming event iterator.
30743193 ///
30753194 /// Returns an `EventStream`. Use `for await (const event of stream)` or call `.next()` manually.
@@ -5374,6 +5493,39 @@ impl From<SearchConfig> for RustSearchConfig {
53745493mod tests {
53755494 use super :: * ;
53765495
5496+ #[ test]
5497+ fn orchestration_object_conversions_round_trip_fields ( ) {
5498+ let schema = serde_json:: json!( { "type" : "object" } ) ;
5499+ let spec = AgentStepSpecObject {
5500+ task_id : "t1" . into ( ) ,
5501+ agent : "explore" . into ( ) ,
5502+ description : "d" . into ( ) ,
5503+ prompt : "p" . into ( ) ,
5504+ max_steps : Some ( 5 ) ,
5505+ parent_session_id : Some ( "parent" . into ( ) ) ,
5506+ output_schema : Some ( schema. clone ( ) ) ,
5507+ } ;
5508+ let rust: RustAgentStepSpec = spec. into ( ) ;
5509+ assert_eq ! ( rust. task_id, "t1" ) ;
5510+ assert_eq ! ( rust. agent, "explore" ) ;
5511+ assert_eq ! ( rust. max_steps, Some ( 5 ) ) ;
5512+ assert_eq ! ( rust. parent_session_id. as_deref( ) , Some ( "parent" ) ) ;
5513+ assert_eq ! ( rust. output_schema, Some ( schema) ) ;
5514+
5515+ let outcome = RustStepOutcome {
5516+ task_id : "t1" . into ( ) ,
5517+ session_id : "task-run-t1" . into ( ) ,
5518+ agent : "explore" . into ( ) ,
5519+ output : "out" . into ( ) ,
5520+ success : true ,
5521+ structured : Some ( serde_json:: json!( { "k" : 1 } ) ) ,
5522+ } ;
5523+ let obj = StepOutcomeObject :: from ( outcome) ;
5524+ assert_eq ! ( obj. task_id, "t1" ) ;
5525+ assert ! ( obj. success) ;
5526+ assert_eq ! ( obj. structured, Some ( serde_json:: json!( { "k" : 1 } ) ) ) ;
5527+ }
5528+
53775529 fn sdk_test_config ( ) -> a3s_code_core:: CodeConfig {
53785530 a3s_code_core:: CodeConfig {
53795531 default_model : Some ( "openai/gpt-4o" . to_string ( ) ) ,
0 commit comments