9.6 KiB
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].includeinpyproject.toml(no new third-party dependency;sqlite3is stdlib, already used bystorage/task_metadata.py) - 1.3 Add Workflow Runtime configuration in
workflow/config.py: default poll interval, default wait-step timeout, defaultWorkflowStoredb 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 theWorkflowStepunion type - 2.2 Implement
WorkflowDefinition{id, name, steps: list[WorkflowStep], entry_step_id}with a constructor-time validation pass rejecting duplicatestep_ids and anynext_step_id/on_true/on_false/entry_step_idthat 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}andWorkflowStepResult{step_id, kind, success, detail, task_id: str | None, timestamp}, withto_dict()/from_dict()helpers mirroringcore/models.py's style - 2.4 Write unit tests for
WorkflowDefinitionconstruction: valid heterogeneous-step definition accepted; duplicatestep_idrejected; danglingnext_step_id/on_true/on_false/entry_step_idreference rejected
3. WorkflowStore persistence (capability: workflow-orchestration)
- 3.1 Implement
workflow/store.py:WorkflowStore(db_path)with schema creation forworkflow_definitions,workflow_runs,workflow_step_resultstables in its own SQLite file, followingstorage/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(statuspending/running,current_step_idset to the definition'sentry_step_id) - 3.4 Implement
WorkflowStore.append_step_result(run_id, step_result)andWorkflowStore.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) -> WorkflowRunreconstructing 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 freshWorkflowStore(same db_path)instance (simulating a process restart)
4. ConditionEvaluator registry (capability: workflow-orchestration)
- 4.1 Implement
workflow/conditions.py:ConditionEvaluatorprotocol/ABC withevaluate(spec, *, scene, world_state) -> bool, and a registrydict[str, ConditionEvaluator] - 4.2 Implement the
scene_contains_textevaluator (checks the most recentScene's elements/text for a target substring fromspec.params) - 4.3 Implement the
world_variable_equalsevaluator (comparesworld_state.variables.get(spec.params["name"])tospec.params["value"]; returnsFalse— never raises — whenworld_stateisNoneor the key is absent) - 4.4 Implement the
elapsed_secondsevaluator (returnsTrueonce at leastspec.params["seconds"]have elapsed since the wait step started polling) - 4.5 Implement the
step_result_successevaluator (looks up a prior step's recordedWorkflowStepResult.successbyspec.params["step_id"]from the run's step-result log) - 4.6 Implement
evaluate_condition(spec, *, scene, world_state) -> boolas the registry lookup entry point, raising a distinguishableUnknownConditionKindErrorfor an unregisteredkind(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) -> Noneraising 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 validatedargsvalues - 5.3 Implement
run_flow_template_skill(skill, args) -> list[StepResult]callingvalidate_skill_args,resolve_skill_steps, then dispatching each resolved tool call through the sametools/*functionsExecutor.execute()already uses, collecting oneStepResultper 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-subscriptionis already implemented) the minimalSkill/FlowTemplateSkilldataclass shapeskill_exec.pydepends 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, ...)withrun(definition, device_id, initial_variables=None) -> WorkflowRunthat creates a run viaWorkflowStore.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=...), callTaskRunner(...).run(task), maptask.status/task.failure_reasonto aWorkflowStepResult - 6.3 Implement the skill-invocation step handler: look up the referenced skill by
skill_id, callskill_exec.run_flow_template_skill, map the returnedStepResults to one aggregateWorkflowStepResult - 6.4 Implement the wait-for-condition step handler: poll
conditions.evaluate_condition()atpoll_interval_secondsuntilTrueortimeout_secondselapses; 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_falseaccordingly, bypassing default sequential advancement - 6.6 Implement default sequential advancement (no explicit
next_step_id/branch target) as "the next step inWorkflowDefinition.stepsorder" for non-branch step kinds - 6.7 After each step handler returns, call
WorkflowStore.append_step_result()andWorkflowStore.update_run()(newcurrent_step_id, andstatusif 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 viaWorkflowStore.get_run(), and continue driving from its persistedcurrent_step_id, skipping any step already present in the loadedstep_resultslog - 6.9 Implement
resume()on an already-completed/failed/cancelledrun 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 reachingcompleted; a workflow with a failing planned-goal step reachingfailedwith a recorded reason; a skill-invocation step executing resolved tool calls (mockedtools/*); 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-memoryWorkflowRunner/TaskRunnerinstances), constructs a freshWorkflowRunner/WorkflowStorepointed at the same db file, and assertsresume(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 persistedvariablesandstep_resultslog are readable and correctly ordered after a full run viaWorkflowStore.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
completedvia the expected branch path
8. Composition safety checks and full-suite validation
- 8.1 Confirm no existing file under
runtime/,storage/,tools/, orcore/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.TaskRunnerinstance inside aWorkflowRunner-driven planned-goal step, guarding against silent drift inagent-runtime's publicrun(task) -> Taskcontract this change composes over - 8.3 Run the full test suite (
pytest) and confirm every existing test intests/passes unmodified, with only newtests/test_workflow_*.py-style files added