## ADDED Requirements ### Requirement: Task submission enqueues a scheduled task The system SHALL allow a caller to submit a task (a goal string, or a reference to a `WorkflowDefinition`, plus optional device constraints: `driver_type`, required capability tags) and SHALL enqueue it as a `ScheduledTask` with status `queued`, returning a stable task id the caller can poll. #### Scenario: Successful submission - **WHEN** a caller submits a task with a goal and no constraints - **THEN** the scheduler creates a `ScheduledTask` with status `queued`, assigns it a unique id, and returns that id to the caller without blocking for a device to become available #### Scenario: Queue depth limit reached - **WHEN** a caller submits a task while the queue already holds `config.max_queue_depth` queued tasks - **THEN** the scheduler rejects the submission with a clear error rather than accepting an unbounded backlog ### Requirement: Assignment matches a queued task to an idle, constraint-matching device The system SHALL assign a queued `ScheduledTask` to an idle `PooledDevice` (as reported by the `device-pool` capability) whose `driver_type` and capability tags satisfy the task's constraints, using a named, registrable `AssignmentStrategy`. #### Scenario: Matching idle device available - **WHEN** `assign()` runs and at least one idle `PooledDevice` matches the head-of-queue task's constraints - **THEN** the scheduler selects one such device via the configured `AssignmentStrategy`, transitions the task to status `assigned`, and records the chosen `device_id`/`host_id` #### Scenario: No matching device available - **WHEN** `assign()` runs and no idle `PooledDevice` matches the head-of-queue task's constraints - **THEN** the task remains `queued` (not failed), and `assign()` returns without error, ready to be retried on a later call #### Scenario: Unknown assignment strategy configured - **WHEN** `TaskScheduler` is configured with an `AssignmentStrategy` name that is not registered - **THEN** the scheduler raises a clear configuration error at startup/first-assign rather than silently falling back to a default strategy ### Requirement: Assignment strategies are pluggable by name The system SHALL provide an `AssignmentStrategy` registry mapping a strategy name to an implementation, with a default `fifo_match` strategy (oldest-queued matching task first, first matching idle device), and SHALL allow a new strategy to be added by registering a name without modifying `TaskScheduler`'s control flow. #### Scenario: Default FIFO strategy orders by submission time - **WHEN** two tasks with satisfiable, overlapping constraints are queued in order A then B, and one matching idle device exists - **THEN** the default `fifo_match` strategy assigns the device to task A, leaving task B queued #### Scenario: Adding a new strategy requires no scheduler edit - **WHEN** a new `AssignmentStrategy` implementation is registered under a new name - **THEN** `TaskScheduler` can be configured to use it by name alone, with no change to `scheduler.py`'s assignment control flow ### Requirement: Local dispatch executes an assignment via existing runners The system SHALL provide a `TaskDispatcher` that, for an assignment whose device is owned by the local process's own host, executes the assigned task by composing the existing `agent-runtime` task-execution entry point (for a goal-based submission) or the `workflow-orchestration` workflow-execution entry point (for a workflow-based submission), without reimplementing planning/execution/retry logic. #### Scenario: Dispatching a goal-based assignment - **WHEN** `TaskDispatcher.dispatch()` is called with an assignment for a goal-based `ScheduledTask` whose device is local - **THEN** the dispatcher constructs and runs a `Task` through the existing task-execution entry point, and updates the `ScheduledTask`'s status to `done` or `failed` based on the resulting task's outcome #### Scenario: Dispatching a workflow-based assignment - **WHEN** `TaskDispatcher.dispatch()` is called with an assignment referencing a `WorkflowDefinition` whose device is local - **THEN** the dispatcher runs the definition through the existing workflow-execution entry point and updates the `ScheduledTask`'s status based on the resulting workflow run's outcome ### Requirement: Remote assignments are rejected explicitly, not silently ignored The system SHALL raise a distinct, typed error when `TaskDispatcher.dispatch()` is called for an assignment whose device is owned by a host other than the dispatching process's own host, rather than attempting execution or silently no-op'ing. #### Scenario: Assignment targets a remote host's device - **WHEN** `TaskDispatcher.dispatch()` is called with an assignment whose `host_id` does not match the local process's own host id - **THEN** the dispatcher raises a `RemoteDispatchNotSupportedError` and leaves the `ScheduledTask`'s status unchanged from `assigned`