Files
T
2026-07-06 23:52:53 +08:00

9.6 KiB

1. Package scaffolding

  • 1.1 Create the workflow/ package (__init__.py, models.py, conditions.py, skill_exec.py, runner.py, store.py, config.py)
  • 1.2 Add workflow* to [tool.setuptools.packages.find].include in pyproject.toml (no new third-party dependency; sqlite3 is stdlib, already used by storage/task_metadata.py)
  • 1.3 Add Workflow Runtime configuration in workflow/config.py: default poll interval, default wait-step timeout, default WorkflowStore db path (workflows/workflows.sqlite3)
  • 1.4 Extend the project's smoke test (that imports every package) to import workflow

2. Workflow and step data model (capability: workflow-orchestration)

  • 2.1 Implement workflow/models.py: ConditionSpec{kind: str, params: dict}, the four step dataclasses (PlannedGoalStep{step_id, goal, next_step_id}, SkillInvocationStep{step_id, skill_id, args, next_step_id}, WaitForConditionStep{step_id, condition, timeout_seconds, poll_interval_seconds, next_step_id}, BranchStep{step_id, condition, on_true, on_false}), and the WorkflowStep union type
  • 2.2 Implement WorkflowDefinition{id, name, steps: list[WorkflowStep], entry_step_id} with a constructor-time validation pass rejecting duplicate step_ids and any next_step_id/on_true/on_false/entry_step_id that does not reference an existing step in the same definition
  • 2.3 Implement WorkflowRun{id, definition_id, status, current_step_id, variables: dict, step_results: list[WorkflowStepResult], created_at, updated_at} and WorkflowStepResult{step_id, kind, success, detail, task_id: str | None, timestamp}, with to_dict()/from_dict() helpers mirroring core/models.py's style
  • 2.4 Write unit tests for WorkflowDefinition construction: valid heterogeneous-step definition accepted; duplicate step_id rejected; dangling next_step_id/on_true/on_false/entry_step_id reference rejected

3. WorkflowStore persistence (capability: workflow-orchestration)

  • 3.1 Implement workflow/store.py: WorkflowStore(db_path) with schema creation for workflow_definitions, workflow_runs, workflow_step_results tables in its own SQLite file, following storage/task_metadata.py's connect-per-call pattern
  • 3.2 Implement WorkflowStore.save_definition(definition) / get_definition(definition_id)
  • 3.3 Implement WorkflowStore.create_run(definition_id, initial_variables) -> WorkflowRun (status pending/running, current_step_id set to the definition's entry_step_id)
  • 3.4 Implement WorkflowStore.append_step_result(run_id, step_result) and WorkflowStore.update_run(run_id, *, status=None, current_step_id=None, variables=None), both persisting immediately (no in-memory-only buffering)
  • 3.5 Implement WorkflowStore.get_run(run_id) -> WorkflowRun reconstructing the full run (status, current step, variables, ordered step results) from persisted rows
  • 3.6 Write unit tests for WorkflowStore: create/get round-trip, step-result append ordering, run survives being re-opened via a fresh WorkflowStore(same db_path) instance (simulating a process restart)

4. ConditionEvaluator registry (capability: workflow-orchestration)

  • 4.1 Implement workflow/conditions.py: ConditionEvaluator protocol/ABC with evaluate(spec, *, scene, world_state) -> bool, and a registry dict[str, ConditionEvaluator]
  • 4.2 Implement the scene_contains_text evaluator (checks the most recent Scene's elements/text for a target substring from spec.params)
  • 4.3 Implement the world_variable_equals evaluator (compares world_state.variables.get(spec.params["name"]) to spec.params["value"]; returns False — never raises — when world_state is None or the key is absent)
  • 4.4 Implement the elapsed_seconds evaluator (returns True once at least spec.params["seconds"] have elapsed since the wait step started polling)
  • 4.5 Implement the step_result_success evaluator (looks up a prior step's recorded WorkflowStepResult.success by spec.params["step_id"] from the run's step-result log)
  • 4.6 Implement evaluate_condition(spec, *, scene, world_state) -> bool as the registry lookup entry point, raising a distinguishable UnknownConditionKindError for an unregistered kind (caught by the runner and turned into a failed step, not an uncaught exception)
  • 4.7 Write unit tests for each built-in evaluator (true case, false case) and for the unregistered-kind error path

5. Skill-invocation execution path (capability: workflow-orchestration)

  • 5.1 Implement workflow/skill_exec.py: validate_skill_args(skill, args) -> None raising a descriptive error listing missing required parameters or unrecognized argument keys, called before any tool dispatch
  • 5.2 Implement resolve_skill_steps(skill, args) -> list[dict] substituting {param} placeholders in the skill's stored tool-call template with validated args values
  • 5.3 Implement run_flow_template_skill(skill, args) -> list[StepResult] calling validate_skill_args, resolve_skill_steps, then dispatching each resolved tool call through the same tools/* functions Executor.execute() already uses, collecting one StepResult per resolved step
  • 5.4 Implement the Skill.kind != "flow_template" guard: reject with a step-definition error before attempting resolution
  • 5.5 Define (or import, if skill-learning-runtime/skill-catalog-subscription is already implemented) the minimal Skill/FlowTemplateSkill dataclass shape skill_exec.py depends on, with a code comment flagging reconciliation once one of those changes lands
  • 5.6 Write unit tests for skill_exec: valid-args resolution produces the expected resolved tool-call sequence; missing required parameter fails before any tool call; non-flow-template skill kind is rejected

6. WorkflowRunner orchestration (capability: workflow-orchestration)

  • 6.1 Implement workflow/runner.py: WorkflowRunner(store, task_runner_factory, ...) with run(definition, device_id, initial_variables=None) -> WorkflowRun that creates a run via WorkflowStore.create_run() and drives steps until a terminal status
  • 6.2 Implement the planned-goal step handler: construct a core.models.Task(goal=step.goal, device_id=...), call TaskRunner(...).run(task), map task.status/task.failure_reason to a WorkflowStepResult
  • 6.3 Implement the skill-invocation step handler: look up the referenced skill by skill_id, call skill_exec.run_flow_template_skill, map the returned StepResults to one aggregate WorkflowStepResult
  • 6.4 Implement the wait-for-condition step handler: poll conditions.evaluate_condition() at poll_interval_seconds until True or timeout_seconds elapses; record success or a timeout failure
  • 6.5 Implement the branch step handler: evaluate the condition once and set the run's next step to on_true/on_false accordingly, bypassing default sequential advancement
  • 6.6 Implement default sequential advancement (no explicit next_step_id/branch target) as "the next step in WorkflowDefinition.steps order" for non-branch step kinds
  • 6.7 After each step handler returns, call WorkflowStore.append_step_result() and WorkflowStore.update_run() (new current_step_id, and status if terminal) before advancing — no step is considered "done" until this checkpoint write completes
  • 6.8 Implement WorkflowRunner.resume(run_id) -> WorkflowRun: load the persisted run via WorkflowStore.get_run(), and continue driving from its persisted current_step_id, skipping any step already present in the loaded step_results log
  • 6.9 Implement resume() on an already-completed/failed/cancelled run as a no-op that returns the existing run state unchanged
  • 6.10 Write unit tests for WorkflowRunner.run() covering: a linear planned-goal-only workflow reaching completed; a workflow with a failing planned-goal step reaching failed with a recorded reason; a skill-invocation step executing resolved tool calls (mocked tools/*); a wait step succeeding before timeout and timing out after
  • 6.11 Write unit tests for branch step selection (true path and false path) and for default sequential advancement between non-branch steps

7. Resumability end-to-end validation (capability: workflow-orchestration)

  • 7.1 Write an end-to-end test that runs a multi-step workflow through WorkflowRunner, stops after two steps (simulating a crash by discarding the in-memory WorkflowRunner/TaskRunner instances), constructs a fresh WorkflowRunner/WorkflowStore pointed at the same db file, and asserts resume(run_id) continues from the third step without re-invoking the first two steps' tool calls or delegated tasks
  • 7.2 Write an end-to-end test asserting a WorkflowRun's persisted variables and step_results log are readable and correctly ordered after a full run via WorkflowStore.get_run()
  • 7.3 Write an end-to-end test combining a branch step with a wait step and a skill-invocation step in one workflow, asserting the run reaches completed via the expected branch path

8. Composition safety checks and full-suite validation

  • 8.1 Confirm no existing file under runtime/, storage/, tools/, or core/ is modified by this change (composition via import only, per design.md's D1/D8/D9)
  • 8.2 Write a test that runs a real (non-mocked) runtime.task.TaskRunner instance inside a WorkflowRunner-driven planned-goal step, guarding against silent drift in agent-runtime's public run(task) -> Task contract this change composes over
  • 8.3 Run the full test suite (pytest) and confirm every existing test in tests/ passes unmodified, with only new tests/test_workflow_*.py-style files added