ww-agentic-workflows 1.0.0.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ww/__init__.py +18 -0
- ww/_bundled_extensions/ww/git/extension.py +1728 -0
- ww/action_execution.py +887 -0
- ww/actions/__init__.py +94 -0
- ww/actions/command.py +444 -0
- ww/actions/contracts.py +699 -0
- ww/actions/extension.py +197 -0
- ww/actions/mcp.py +84 -0
- ww/actions/prompt.py +74 -0
- ww/actions/skill.py +62 -0
- ww/actions/slash_command.py +63 -0
- ww/agents.py +151 -0
- ww/amendments.py +54 -0
- ww/artifacts.py +93 -0
- ww/assessments.py +181 -0
- ww/assets/__init__.py +2 -0
- ww/assets/agent_instructions.md +49 -0
- ww/assets/docs/examples.md +879 -0
- ww/assets/docs/features.md +4639 -0
- ww/assets/docs/specification.md +1876 -0
- ww/assets/noww_skill.md +11 -0
- ww/assets/workflows/catchall.yaml +26 -0
- ww/assets/workflows/onboarding.yaml +586 -0
- ww/assets/workflows/scriptize.yaml +130 -0
- ww/assets/ww-automate_skill.md +23 -0
- ww/assets/ww-deduce-feedback_skill.md +38 -0
- ww/assets/ww-feedback-rules_skill.md +48 -0
- ww/assets/ww-learn-project_skill.md +22 -0
- ww/assets/ww-refresh_skill.md +26 -0
- ww/assets/ww-rule_skill.md +83 -0
- ww/assets/ww-rules-from-artifacts_skill.md +22 -0
- ww/assets/ww-scriptize_skill.md +33 -0
- ww/assets/ww-setup_skill.md +94 -0
- ww/assets/ww-solve_skill.md +23 -0
- ww/assets/ww-suggest_skill.md +32 -0
- ww/assets/ww-wizard_skill.md +105 -0
- ww/assets/ww_skill.md +59 -0
- ww/assignments.py +283 -0
- ww/bootstrap.py +405 -0
- ww/builtin_workflows.py +215 -0
- ww/changes.py +225 -0
- ww/child_coordination.py +482 -0
- ww/children.py +106 -0
- ww/claude_permissions.py +115 -0
- ww/cli/__init__.py +7 -0
- ww/cli/__main__.py +6 -0
- ww/cli/audit.py +129 -0
- ww/cli/catalogs.py +131 -0
- ww/cli/discover.py +607 -0
- ww/cli/initialization.py +898 -0
- ww/cli/lookup.py +287 -0
- ww/cli/main.py +1768 -0
- ww/cli/parser.py +1200 -0
- ww/cli/prompts.py +217 -0
- ww/cli/updates.py +117 -0
- ww/completion_artifacts.py +156 -0
- ww/completion_inputs.py +39 -0
- ww/config/__init__.py +582 -0
- ww/config/actions.py +591 -0
- ww/config/composition.py +571 -0
- ww/config/rules.py +511 -0
- ww/config/steps.py +1220 -0
- ww/config/values.py +223 -0
- ww/config_files.py +191 -0
- ww/config_writes.py +264 -0
- ww/contracts.py +155 -0
- ww/control.py +41 -0
- ww/defaults.py +130 -0
- ww/design_docs.py +32 -0
- ww/discovery.py +104 -0
- ww/documents.py +217 -0
- ww/errors.py +18 -0
- ww/executable.py +43 -0
- ww/execution_models/__init__.py +64 -0
- ww/execution_models/construction.py +148 -0
- ww/execution_models/decoding.py +38 -0
- ww/execution_models/plan_codec.py +565 -0
- ww/execution_models/records.py +1206 -0
- ww/execution_models/runs.py +266 -0
- ww/extensions/__init__.py +40 -0
- ww/extensions/api.py +559 -0
- ww/extensions/registry.py +864 -0
- ww/extensions/store.py +78 -0
- ww/feedback.py +342 -0
- ww/handler_repairs.py +57 -0
- ww/hooks/__init__.py +40 -0
- ww/hooks/agents.py +380 -0
- ww/hooks/install.py +168 -0
- ww/hooks/notices.py +206 -0
- ww/hooks/records.py +209 -0
- ww/hooks/runtime.py +266 -0
- ww/hooks/transcripts.py +183 -0
- ww/inspect.py +896 -0
- ww/instructions/__init__.py +17 -0
- ww/instructions/builder.py +1682 -0
- ww/instructions/commands.py +335 -0
- ww/instructions/handoff.py +149 -0
- ww/instructions/models.py +686 -0
- ww/instructions/policy.py +219 -0
- ww/instructions/text.py +168 -0
- ww/interactions.py +187 -0
- ww/interpolation.py +37 -0
- ww/item_passes.py +167 -0
- ww/items.py +99 -0
- ww/locking.py +207 -0
- ww/metadata_publication.py +230 -0
- ww/onboarding.py +229 -0
- ww/open_work.py +236 -0
- ww/operations.py +193 -0
- ww/operator_ui/__init__.py +16 -0
- ww/operator_ui/page.html +351 -0
- ww/operator_ui/server.py +215 -0
- ww/operator_ui/session.py +389 -0
- ww/operator_ui/sheet.py +104 -0
- ww/operator_ui/view.py +109 -0
- ww/output.py +339 -0
- ww/output_adapters/__init__.py +12 -0
- ww/output_adapters/base.py +25 -0
- ww/output_adapters/json_adapter.py +37 -0
- ww/output_adapters/markdown.py +2293 -0
- ww/output_adapters/rule_pages.py +337 -0
- ww/output_adapters/terminal.py +21 -0
- ww/package_updates.py +167 -0
- ww/plan/__init__.py +38 -0
- ww/plan/actions.py +207 -0
- ww/plan/compiler.py +1492 -0
- ww/plan/constructs.py +456 -0
- ww/plan/models.py +665 -0
- ww/project_config.py +752 -0
- ww/recovery.py +401 -0
- ww/replanning.py +367 -0
- ww/results.py +77 -0
- ww/rule_checks.py +230 -0
- ww/rule_conversion.py +331 -0
- ww/rule_disputes.py +148 -0
- ww/rule_store.py +456 -0
- ww/rule_verification.py +714 -0
- ww/rule_views.py +447 -0
- ww/rule_writes.py +920 -0
- ww/run_coordination.py +158 -0
- ww/runtimes.py +105 -0
- ww/service.py +4405 -0
- ww/setup_apply.py +428 -0
- ww/step_values.py +20 -0
- ww/storage.py +447 -0
- ww/storage_adapters/__init__.py +36 -0
- ww/storage_adapters/base.py +540 -0
- ww/storage_adapters/filesystem.py +370 -0
- ww/storage_adapters/memory.py +195 -0
- ww/storage_adapters/project_metadata.py +69 -0
- ww/storage_adapters/task_document.py +484 -0
- ww/task_ids.py +114 -0
- ww/task_references.py +124 -0
- ww/transitions.py +1619 -0
- ww/updates.py +399 -0
- ww/upgrade.py +95 -0
- ww/validation.py +168 -0
- ww/variables.py +275 -0
- ww/workflow_config.py +854 -0
- ww/workflow_update.py +239 -0
- ww/workflow_validation.py +1260 -0
- ww/workspace.py +50 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/METADATA +690 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/RECORD +167 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/WHEEL +4 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/entry_points.txt +2 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ww-wizard
|
|
3
|
+
description: Help the operator create a ww workflow, change an existing one, create or improve rules, or choose an approach, adapting the questions to the request and placing every change through ww's own commands. Use when the operator invokes /ww-wizard, or asks to design, change or tune how ww works in this project and has not said exactly what to edit.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Shape ww's setup with the operator
|
|
7
|
+
|
|
8
|
+
Talk in plain words and ask only what changes the outcome. This skill
|
|
9
|
+
orchestrates: ww's design documents decide what a setup can be made of, ww's
|
|
10
|
+
commands validate and place every change, and the rules skills write rules.
|
|
11
|
+
Never edit ww's configuration files yourself.
|
|
12
|
+
|
|
13
|
+
## Start
|
|
14
|
+
|
|
15
|
+
1. Run `./ww discover`. If ww is disabled, say so and stop. Read the
|
|
16
|
+
operator's request.
|
|
17
|
+
2. If it already says what to do, go straight to that branch. Otherwise ask
|
|
18
|
+
which one, as a single choice through the host's choice tool where
|
|
19
|
+
available (a numbered list in the chat otherwise), and end your turn until
|
|
20
|
+
the operator answers:
|
|
21
|
+
- Create a workflow.
|
|
22
|
+
- Change an existing workflow.
|
|
23
|
+
- Create or improve rules.
|
|
24
|
+
- Help me choose an approach.
|
|
25
|
+
3. Match the depth of your questions to the request: a precise request gets
|
|
26
|
+
one confirming question at most; a vague one gets the few questions it
|
|
27
|
+
needs, one message at a time. A timeout or a preselected value is not an
|
|
28
|
+
answer.
|
|
29
|
+
|
|
30
|
+
Before drafting anything, read the design authorities the branch needs, which
|
|
31
|
+
every installation prints: `./ww docs features` (start at "Designing a
|
|
32
|
+
workflow"), `./ww docs specification` for exact syntax and `./ww docs
|
|
33
|
+
examples` for the shape of a setup. Inspect the project: `.ww/project.md` when
|
|
34
|
+
it exists, the project's own commands and CI, and the workflows already
|
|
35
|
+
configured. Take a test, lint or type command only from that evidence and say
|
|
36
|
+
where it comes from; where it is missing or contradictory, ask instead of
|
|
37
|
+
guessing.
|
|
38
|
+
|
|
39
|
+
## Create a workflow
|
|
40
|
+
|
|
41
|
+
Ask for the trigger, the result wanted and where the operator wants to be
|
|
42
|
+
involved. Choose the smallest structure that expresses it, adding items,
|
|
43
|
+
loops, assessments, modes or reusable groups only for a concrete requirement
|
|
44
|
+
the features guide gives a reason for. Challenge anything the request does not
|
|
45
|
+
need: name a simpler alternative and what it costs the operator, and let them
|
|
46
|
+
decide; do not walk through every property. Then show concise YAML and a short
|
|
47
|
+
walkthrough of a representative task, with a failure and retry path when an
|
|
48
|
+
external effect or an automatic command is involved. Write the draft as a
|
|
49
|
+
setup fragment (root keys `workflows`, `modes`, `documents`, `handlers`,
|
|
50
|
+
`hooks`, `rules`, plus `settings`) to a file outside the repository, such as in
|
|
51
|
+
your scratch directory.
|
|
52
|
+
|
|
53
|
+
## Change an existing workflow
|
|
54
|
+
|
|
55
|
+
1. Find where it is defined: `./ww discover --json` gives each workflow's
|
|
56
|
+
`source` file and `source_level` (`local`, `project` or `global`). State
|
|
57
|
+
which definition is in force and which level it is at.
|
|
58
|
+
2. Draft the complete changed workflow as a fragment whose `workflows` list
|
|
59
|
+
holds that one entry. Keep what the operator did not ask to change.
|
|
60
|
+
3. Edit it where it is written: `./ww setup update <name> <fragment>`, adding
|
|
61
|
+
`--level <source_level>` so that ww refuses rather than edit a definition
|
|
62
|
+
another level hides. If the operator wants a change for themselves only to a
|
|
63
|
+
shared workflow, say that this is an override that hides the shared
|
|
64
|
+
definition for them alone, and place it with `./ww setup apply <fragment>
|
|
65
|
+
--for me` only when they confirm. Never create such an override silently,
|
|
66
|
+
and never hide a shared edit under a local one.
|
|
67
|
+
4. A workflow ww ships (`catchall`, the `ww-*` ones) is not edited: a same-named
|
|
68
|
+
definition placed with `setup apply` replaces it.
|
|
69
|
+
|
|
70
|
+
## Create or improve rules
|
|
71
|
+
|
|
72
|
+
Hand the work to the rules skills instead of writing rules yourself: `ww-rule`
|
|
73
|
+
for rules in the operator's own words, `ww-rules-from-artifacts` for lessons
|
|
74
|
+
that recur in past results, `ww-feedback-rules` for learned operator feedback,
|
|
75
|
+
and `ww-scriptize` to give rules that have none a check. Say which one fits and
|
|
76
|
+
why, then follow it.
|
|
77
|
+
|
|
78
|
+
## Help me choose an approach
|
|
79
|
+
|
|
80
|
+
Ask what goes wrong or what the operator wants to be easier, then lay out two
|
|
81
|
+
or three options in plain words, each with what it asks of the operator and
|
|
82
|
+
what it costs: a rule, a check, a mode, a small workflow, or doing nothing.
|
|
83
|
+
Recommend the simplest that meets the need. When they pick one, continue in its
|
|
84
|
+
branch.
|
|
85
|
+
|
|
86
|
+
## Validate, show, apply
|
|
87
|
+
|
|
88
|
+
1. Validate and inspect the draft before asking anything:
|
|
89
|
+
`./ww setup apply <fragment> --for <me or team> --dry-run --inspect <workflow>
|
|
90
|
+
--agent <your agent>` for new definitions, `./ww setup update <name>
|
|
91
|
+
<fragment> --dry-run --inspect <workflow> --agent <your agent>` for a
|
|
92
|
+
change. In the compiled plan, check that automation is ww-owned (commands
|
|
93
|
+
run as handlers or hooks, never as a step asking the agent to run them),
|
|
94
|
+
that item scopes are valid, and that the operator meets only the
|
|
95
|
+
conversations they intended. Fix the draft and validate again until it is
|
|
96
|
+
clean.
|
|
97
|
+
2. Show the YAML or the diff, the walkthrough, and the dry run's list of
|
|
98
|
+
changes. For a new definition, ask whether it is for the operator alone
|
|
99
|
+
(`--for me`, local files kept out of version control) or the team
|
|
100
|
+
(`--for team`, shared files).
|
|
101
|
+
3. When the operator approves the shown edit, apply exactly it with `--yes`;
|
|
102
|
+
do not ask again. If ww refuses, show its message and stop; never place the
|
|
103
|
+
configuration another way.
|
|
104
|
+
4. Run `./ww lint`, tell the operator which files changed, and that shared
|
|
105
|
+
ones are left uncommitted for them to review and commit.
|
ww/assets/ww_skill.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ww
|
|
3
|
+
description: Carry out a development request through ww, this project's workflow tool. Use only when the request changes something (implement, fix, refactor, merge, edit documentation) or the user explicitly asks to work via ww, to start or continue a ww task, or to run a ww workflow; where `./ww discover` says ww is used by default, use it before changing files for any request, since every change then goes through ww. Never use it for reviews, questions, explanations, investigations, status checks or other read-only work, even when they concern ww or a ww task.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Work through ww
|
|
7
|
+
|
|
8
|
+
ww is for development: requests that change files. A review, question,
|
|
9
|
+
explanation, investigation, status check or other read-only request never
|
|
10
|
+
goes through ww; answer it directly, without these steps and without a task,
|
|
11
|
+
even where ww is used by default. When a read-only conversation turns into a
|
|
12
|
+
change, start from step 1 then.
|
|
13
|
+
|
|
14
|
+
1. Run `./ww discover` and read all of it. If it says ww is disabled, stop:
|
|
15
|
+
do not use ww, and tell the user. If it says ww is used only on request
|
|
16
|
+
and the user did not explicitly ask for ww (invoking this skill counts),
|
|
17
|
+
carry out the request without ww and do not ask.
|
|
18
|
+
2. To continue an existing task, run
|
|
19
|
+
`./ww instruction <task-id> --role manager` instead of starting a new one.
|
|
20
|
+
3. Otherwise choose the workflow that matches the request, keep its default
|
|
21
|
+
modes unless the user's wording matches another mode, and start the task
|
|
22
|
+
with the start command `discover` shows. Pass the user's requirements,
|
|
23
|
+
normalized, as `--requirements`. When the request names an external
|
|
24
|
+
ticket, such as a Jira key, use that key as the task ID; omit the ID only
|
|
25
|
+
when there is none or the workflow obtains its own.
|
|
26
|
+
When no workflow fits and you are about to change files, do not start
|
|
27
|
+
`catchall` directly: run `./ww lookup [<task>] --agent <agent>` with the
|
|
28
|
+
task the conversation works on or the request names, as written, and
|
|
29
|
+
follow its answer, then do the work as you would without ww. Read-only work
|
|
30
|
+
needs no task.
|
|
31
|
+
When the first page of `start` says rules have no check
|
|
32
|
+
yet, tell the user once and carry on: it never blocks the task, and the
|
|
33
|
+
`ww-scriptize` skill builds those checks when they want it.
|
|
34
|
+
4. Follow every ww response exactly: run each displayed command with all
|
|
35
|
+
placeholders replaced, and keep going until ww reports that the workflow
|
|
36
|
+
is complete or reports an error. On `interactive: true` steps, converse
|
|
37
|
+
naturally until intent to finish is clear, ask if ambiguous, and treat
|
|
38
|
+
"done for today" as a pause. Record and end the interaction before
|
|
39
|
+
completing it. Use the host choice tool when available; keep asynchronous
|
|
40
|
+
choices pending until an answer arrives, and do not treat timeouts or
|
|
41
|
+
preselection as answers. After completion, relay any optional
|
|
42
|
+
`ww-deduce-feedback` suggestion; do not insert learning into the plan.
|
|
43
|
+
Deduction uses ww commands and explicit existing point IDs; rule review
|
|
44
|
+
and pruning are separate, and rules require operator approval.
|
|
45
|
+
5. Never run a ww-owned handler yourself, edit ww state, or read
|
|
46
|
+
`ww.yaml` or ww's source to work out what to do next. For a handler repair
|
|
47
|
+
assignment, fix the cause and complete with an artifact; ww retries the
|
|
48
|
+
command. In `auto`, the manager dispatches the repair with `next`. When
|
|
49
|
+
ww reports `awaiting_operator` (a failed handler or work, an interrupted
|
|
50
|
+
command, a loop at its iteration limit, a changed workflow), stop and report the task, its
|
|
51
|
+
`operator_reason` and the exact error to the user (unless they gave a
|
|
52
|
+
standing authorization for routine repairs such as dependency installation,
|
|
53
|
+
formatting or retries: apply it to a stop of that kind without asking again,
|
|
54
|
+
and ask only for a material decision or an action outside it); when they
|
|
55
|
+
decide, run the recovery command ww showed, `./ww next <task-id> --retry` to run the
|
|
56
|
+
handler again, `--force --reason` to skip it or leave the loop, or
|
|
57
|
+
`--replan` / `--keep-plan` for a changed workflow. A `pass_incomplete`
|
|
58
|
+
stop is cleared by recording what it names with `./ww update-item`, then
|
|
59
|
+
`./ww next <task-id> --retry`; `--force` is refused there.
|
ww/assignments.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""Structural worker-assignment boundaries for compiled workflow plans."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from ww.control import is_coordinator, workflow_transition
|
|
9
|
+
from ww.errors import StateError
|
|
10
|
+
from ww.plan import PlanItem, WorkflowPlan
|
|
11
|
+
from ww.workflow_config import ProvidedVariable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Assignment:
|
|
16
|
+
"""One manager-dispatched unit of agent and automatic lifecycle work."""
|
|
17
|
+
|
|
18
|
+
first_item_id: str
|
|
19
|
+
start: int
|
|
20
|
+
stop: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def selection_item(plan: WorkflowPlan, assignment: Assignment) -> PlanItem | None:
|
|
24
|
+
"""Return the item whose request drives a worker selection.
|
|
25
|
+
|
|
26
|
+
The main step wins; in a hook-only assignment an agent-owned hook wins
|
|
27
|
+
over a handler that merely waits for values, because the worker is
|
|
28
|
+
shaped for the work it performs, not for the value it types in.
|
|
29
|
+
"""
|
|
30
|
+
items = plan.items[assignment.start : assignment.stop]
|
|
31
|
+
for choose in (
|
|
32
|
+
lambda item: item.phase == "step" and item.owner == "agent",
|
|
33
|
+
lambda item: item.owner == "agent",
|
|
34
|
+
lambda item: item.requires_agent_input,
|
|
35
|
+
):
|
|
36
|
+
found = next((item for item in items if choose(item)), None)
|
|
37
|
+
if found is not None:
|
|
38
|
+
return found
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def assignment_at(
|
|
43
|
+
plan: WorkflowPlan, cursor: int, *, runtime: str
|
|
44
|
+
) -> Assignment | None:
|
|
45
|
+
"""Return the assignment beginning at ``cursor``, if it is worker-owned.
|
|
46
|
+
|
|
47
|
+
Preparation hooks share an assignment with the main action at the same
|
|
48
|
+
structural step. Completion hooks on ancestors trail the final descendant.
|
|
49
|
+
A new preparation/main action, item expansion, child coordinator, workflow
|
|
50
|
+
transition, or unrelated completion lifecycle ends the assignment. The
|
|
51
|
+
built-in workflow summary is deliberately included in the final assignment.
|
|
52
|
+
|
|
53
|
+
In the ``auto`` runtime, an ``items`` step with ``item_assignment`` set to
|
|
54
|
+
``per_item`` or ``together`` keeps later stages of the same item, or of
|
|
55
|
+
every item, in the assignment, and a ``loop`` with ``loop_assignment``
|
|
56
|
+
``per_round`` keeps the following body steps of the same round while
|
|
57
|
+
they resolve to the same worker settings. The ``single`` runtime keeps
|
|
58
|
+
per-step boundaries because one session already performs every assignment.
|
|
59
|
+
|
|
60
|
+
A verification item is an assignment of its own, so the worker who did a
|
|
61
|
+
step never verifies it.
|
|
62
|
+
"""
|
|
63
|
+
if cursor >= len(plan.items):
|
|
64
|
+
return None
|
|
65
|
+
first = plan.items[cursor]
|
|
66
|
+
if _coordinator(first):
|
|
67
|
+
return None
|
|
68
|
+
if first.verifies is not None:
|
|
69
|
+
return Assignment(first.id, cursor, cursor + 1)
|
|
70
|
+
spans_steps = runtime != "single"
|
|
71
|
+
lineage = {first.step, *first.ancestors}
|
|
72
|
+
worker = first if first.owner == "agent" else None
|
|
73
|
+
stop = cursor + 1
|
|
74
|
+
while stop < len(plan.items):
|
|
75
|
+
item = plan.items[stop]
|
|
76
|
+
if _coordinator(item) or item.verifies is not None:
|
|
77
|
+
break
|
|
78
|
+
if item.summary:
|
|
79
|
+
stop += 1
|
|
80
|
+
continue
|
|
81
|
+
if item.phase in {"before_start_workflow", "before_start", "step"}:
|
|
82
|
+
if item.step != first.step:
|
|
83
|
+
if not spans_steps or not (
|
|
84
|
+
shares_item_span(first, item, worker=worker)
|
|
85
|
+
or shares_loop_span(first, item, worker=worker)
|
|
86
|
+
):
|
|
87
|
+
break
|
|
88
|
+
lineage.update((item.step, *item.ancestors))
|
|
89
|
+
if worker is None and item.owner == "agent":
|
|
90
|
+
worker = item
|
|
91
|
+
stop += 1
|
|
92
|
+
continue
|
|
93
|
+
if item.step not in lineage:
|
|
94
|
+
break
|
|
95
|
+
stop += 1
|
|
96
|
+
return Assignment(first.id, cursor, stop)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def shares_item_span(
|
|
100
|
+
first: PlanItem, item: PlanItem, *, worker: PlanItem | None
|
|
101
|
+
) -> bool:
|
|
102
|
+
"""Whether ``item`` continues the per-item assignment begun at ``first``."""
|
|
103
|
+
if first.item_id is None or item.item_id is None:
|
|
104
|
+
return False
|
|
105
|
+
if first.item_pass != item.item_pass:
|
|
106
|
+
return False
|
|
107
|
+
if first.item_assignment != item.item_assignment:
|
|
108
|
+
return False
|
|
109
|
+
if item.item_assignment == "per_item" and first.item_id != item.item_id:
|
|
110
|
+
return False
|
|
111
|
+
if item.item_assignment == "per_step":
|
|
112
|
+
return False
|
|
113
|
+
return _same_worker(item, worker)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def shares_loop_span(
|
|
117
|
+
first: PlanItem, item: PlanItem, *, worker: PlanItem | None
|
|
118
|
+
) -> bool:
|
|
119
|
+
"""Whether ``item`` continues the loop round begun at ``first``."""
|
|
120
|
+
if first.loop_id is None or first.loop_id != item.loop_id:
|
|
121
|
+
return False
|
|
122
|
+
if item.loop_assignment != "per_round":
|
|
123
|
+
return False
|
|
124
|
+
return _same_worker(item, worker)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _same_worker(item: PlanItem, worker: PlanItem | None) -> bool:
|
|
128
|
+
"""Whether ``item`` can be performed by the assignment's current worker.
|
|
129
|
+
|
|
130
|
+
``worker`` is the first agent-owned item of the assignment so far. A step
|
|
131
|
+
that needs a different agent, model, reasoning, or profile, or that is
|
|
132
|
+
reserved for the manager with ``role: manager``, starts a new
|
|
133
|
+
assignment, because one running worker cannot change any of them.
|
|
134
|
+
"""
|
|
135
|
+
if item.owner != "agent":
|
|
136
|
+
# Automatic work inside the span drains within the assignment.
|
|
137
|
+
return True
|
|
138
|
+
if item.role == "manager" or (worker is not None and worker.role == "manager"):
|
|
139
|
+
return False
|
|
140
|
+
return worker is None or _worker_settings(worker) == _worker_settings(item)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _worker_settings(item: PlanItem) -> tuple[str | None, ...]:
|
|
144
|
+
return (
|
|
145
|
+
item.requested_agent,
|
|
146
|
+
item.requested_model,
|
|
147
|
+
item.requested_reasoning,
|
|
148
|
+
item.profile,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True)
|
|
153
|
+
class LoopSpan:
|
|
154
|
+
"""Several body steps of one loop round that one worker performs."""
|
|
155
|
+
|
|
156
|
+
loop_id: str
|
|
157
|
+
stages: tuple[PlanItem, ...]
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def stage_names(self) -> tuple[str, ...]:
|
|
161
|
+
return tuple(dict.fromkeys(stage.name for stage in self.stages))
|
|
162
|
+
|
|
163
|
+
def to_dict(self) -> dict[str, object]:
|
|
164
|
+
return {
|
|
165
|
+
"loop_assignment": "per_round",
|
|
166
|
+
"loop": self.loop_id,
|
|
167
|
+
"stages": list(self.stage_names),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def loop_span(plan: WorkflowPlan, assignment: Assignment) -> LoopSpan | None:
|
|
172
|
+
"""Return the loop body steps an assignment spans, when it spans several."""
|
|
173
|
+
stages = tuple(
|
|
174
|
+
item
|
|
175
|
+
for item in plan.items[assignment.start : assignment.stop]
|
|
176
|
+
if item.phase == "step" and item.owner == "agent" and item.loop_id is not None
|
|
177
|
+
)
|
|
178
|
+
if len(stages) <= 1:
|
|
179
|
+
return None
|
|
180
|
+
return LoopSpan(str(stages[0].loop_id), stages)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True)
|
|
184
|
+
class ItemSpan:
|
|
185
|
+
"""Several per-item stages that one worker performs in one assignment."""
|
|
186
|
+
|
|
187
|
+
item_assignment: str
|
|
188
|
+
stages: tuple[PlanItem, ...]
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def item_ids(self) -> tuple[str, ...]:
|
|
192
|
+
return tuple(dict.fromkeys(str(stage.item_id) for stage in self.stages))
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def stage_names(self) -> tuple[str, ...]:
|
|
196
|
+
"""The stage names of one item, in order."""
|
|
197
|
+
first = self.stages[0].item_id
|
|
198
|
+
return tuple(
|
|
199
|
+
dict.fromkeys(stage.name for stage in self.stages if stage.item_id == first)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def to_dict(self) -> dict[str, object]:
|
|
203
|
+
return {
|
|
204
|
+
"item_assignment": self.item_assignment,
|
|
205
|
+
"item_ids": list(self.item_ids),
|
|
206
|
+
"stages": list(self.stage_names),
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def item_span(plan: WorkflowPlan, assignment: Assignment) -> ItemSpan | None:
|
|
211
|
+
"""Return the per-item stages an assignment spans, when it spans several."""
|
|
212
|
+
stages = tuple(
|
|
213
|
+
item
|
|
214
|
+
for item in plan.items[assignment.start : assignment.stop]
|
|
215
|
+
if item.phase == "step" and item.owner == "agent" and item.item_id is not None
|
|
216
|
+
)
|
|
217
|
+
if len(stages) <= 1:
|
|
218
|
+
return None
|
|
219
|
+
return ItemSpan(stages[0].item_assignment, stages)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def input_only(plan: WorkflowPlan, assignment: Assignment) -> bool:
|
|
223
|
+
"""Whether an assignment holds no agent work, only values for handlers.
|
|
224
|
+
|
|
225
|
+
Such a span, for example a commit hook that needs its message after a
|
|
226
|
+
loop boundary, is not worth a worker: the manager has just read the
|
|
227
|
+
outcome it would summarize and supplies the values itself.
|
|
228
|
+
"""
|
|
229
|
+
span = plan.items[assignment.start : assignment.stop]
|
|
230
|
+
# The built-in workflow summary trails the final assignment; it does not
|
|
231
|
+
# make a value-only span worth a worker.
|
|
232
|
+
return not any(item.owner == "agent" and not item.summary for item in span) and any(
|
|
233
|
+
item.requires_agent_input for item in span
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def active_assignment(
|
|
238
|
+
plan: WorkflowPlan, first_item_id: str | None, *, runtime: str
|
|
239
|
+
) -> Assignment | None:
|
|
240
|
+
"""Rebuild a persisted assignment after reload or plan materialization."""
|
|
241
|
+
if first_item_id is None:
|
|
242
|
+
return None
|
|
243
|
+
for index, item in enumerate(plan.items):
|
|
244
|
+
if item.id == first_item_id:
|
|
245
|
+
return assignment_at(plan, index, runtime=runtime)
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def completion_window_items(
|
|
250
|
+
plan: WorkflowPlan, cursor: int, stop: int | None = None
|
|
251
|
+
) -> tuple[PlanItem, ...]:
|
|
252
|
+
"""Return the current item and the automatic items its completion feeds."""
|
|
253
|
+
items = [plan.items[cursor]]
|
|
254
|
+
for item in plan.items[cursor + 1 : stop]:
|
|
255
|
+
if item.owner == "agent" or workflow_transition(item) is not None:
|
|
256
|
+
break
|
|
257
|
+
items.append(item)
|
|
258
|
+
return tuple(items)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def completion_window(
|
|
262
|
+
plan: WorkflowPlan, cursor: int, stop: int | None = None
|
|
263
|
+
) -> tuple[tuple[ProvidedVariable, ...], tuple[str, ...]]:
|
|
264
|
+
"""Return inputs required to complete the current assignment boundary."""
|
|
265
|
+
items = completion_window_items(plan, cursor, stop)
|
|
266
|
+
required: dict[str, ProvidedVariable] = {}
|
|
267
|
+
sources: dict[str, str] = {}
|
|
268
|
+
for item in items:
|
|
269
|
+
for value in item.provide:
|
|
270
|
+
source = f"{item.name} ({item.source}, {item.id})"
|
|
271
|
+
if value.name in required and required[value.name] != value:
|
|
272
|
+
raise StateError(
|
|
273
|
+
f"completion window has conflicting provided variable "
|
|
274
|
+
f"{value.name!r}: {sources[value.name]} and {source}"
|
|
275
|
+
)
|
|
276
|
+
required.setdefault(value.name, value)
|
|
277
|
+
sources.setdefault(value.name, source)
|
|
278
|
+
context = [item.name for item in items[1:]]
|
|
279
|
+
return tuple(required.values()), tuple(context)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _coordinator(item: PlanItem) -> bool:
|
|
283
|
+
return is_coordinator(item)
|