-- SPDX-FileCopyrightText: © 2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. local cancel = require("agent_smith.cancel") local agent_error = require("agent_smith.error") local dispatch_job = require("agent_smith.dispatch.job") local dispatch_worker = require("agent_smith.dispatch.worker") local dispatch_result = require("agent_smith.dispatch.result") local role_matrix = require("agent_smith.role.matrix") local M = {} local function resolve_parent_role(runtime, opts) if type(opts.parent_role) == "string" and opts.parent_role ~= "" then return opts.parent_role end if runtime and type(runtime.execution_context) == "function" then local ctx = runtime:execution_context() if type(ctx) == "table" and type(ctx.active_role) == "string" and ctx.active_role ~= "" then return ctx.active_role end end return nil end local function resolve_parent_turn_id(runtime, opts) if type(opts.parent_turn_id) == "string" and opts.parent_turn_id ~= "" then return opts.parent_turn_id end if runtime and type(runtime.current_turn) == "function" then local turn = runtime:current_turn() if type(turn) == "table" and type(turn.id) == "string" and turn.id ~= "" then return turn.id end end return nil end function M.build_job(runtime, opts) opts = opts or {} local parent_role = resolve_parent_role(runtime, opts) if not parent_role then return nil, agent_error.new({ kind = agent_error.kind.schema, code = "missing_parent_role", message = "dispatch requires parent role", origin = "dispatch:service", }) end local parent_turn_id = resolve_parent_turn_id(runtime, opts) if not parent_turn_id then return nil, agent_error.new({ kind = agent_error.kind.schema, code = "missing_parent_turn_id", message = "dispatch requires parent turn id", origin = "dispatch:service", }) end local parent_token = opts.parent_cancel_token if parent_token == nil and runtime and type(runtime.current_cancel_token) == "function" then parent_token = runtime:current_cancel_token() end local child_token, child_err = cancel.derive_child(parent_token) if parent_token and not child_token then return nil, agent_error.normalize(child_err, { kind = agent_error.kind.internal, origin = "dispatch:service", }) end local target_role = opts.target_role if not role_matrix.check_dispatch(parent_role, target_role) then return nil, agent_error.new({ kind = agent_error.kind.policy, code = "invalid_dispatch_target", message = "role '" .. parent_role .. "' may not dispatch to '" .. tostring(target_role) .. "'", origin = "dispatch:service", }) end return dispatch_job.new({ job_id = opts.job_id, parent_turn_id = parent_turn_id, parent_role = parent_role, target_role = target_role, task = opts.task, context = opts.context, files = opts.files, cancel_token = child_token, }) end function M.launch(runtime, job, opts) return dispatch_worker.launch(runtime, job, opts) end function M.normalize(report_args, role, usage) return dispatch_result.normalize_from_report(report_args, role, usage) end function M.dispatch(runtime, opts) opts = opts or {} if not runtime then return nil, agent_error.new({ kind = agent_error.kind.internal, code = "no_runtime", message = "dispatch requires runtime", origin = "dispatch:service", }) end local job, job_err = M.build_job(runtime, opts) if not job then return nil, job_err end return M.launch(runtime, job, { turn_module = opts.turn_module, child_conversation_state = opts.child_conversation_state, on_event = opts.on_event, }) end local new = function(options) local service = { name = "dispatch", kind = "service", options = options or {}, build_job = function(_, runtime, opts) return M.build_job(runtime, opts) end, launch = function(_, runtime, job, opts) return M.launch(runtime, job, opts) end, normalize = function(_, report_args, role, usage) return M.normalize(report_args, role, usage) end, dispatch = function(_, runtime, opts) return M.dispatch(runtime, opts) end, } return service end M.new = new return M