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
ww/hooks/transcripts.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""Recover a conversation with the operator from an agent's own transcript.
|
|
3
|
+
|
|
4
|
+
When a session ends while an interactive step is still talking, the
|
|
5
|
+
``interrupt`` hook reads the session's transcript file and keeps only what
|
|
6
|
+
the two sides said to each other: the operator's typed messages and the
|
|
7
|
+
agent's text replies. Tool calls and their results, reasoning, injected
|
|
8
|
+
context and subagent lines are left out.
|
|
9
|
+
|
|
10
|
+
The transcript formats are the agents' internal ones, not a published
|
|
11
|
+
contract, so every reader is best effort: a line it does not understand is
|
|
12
|
+
skipped, and an unreadable file recovers nothing. Only the end of the file
|
|
13
|
+
is read, since the conversation of the current step is at its end.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from collections.abc import Callable, Iterator
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
# The conversation of the step in progress is recent; reading only the end
|
|
25
|
+
# of a long session's file keeps the hook within its few seconds.
|
|
26
|
+
READ_LIMIT = 2 * 1024 * 1024
|
|
27
|
+
# Text that starts like this was put into the conversation by the agent's
|
|
28
|
+
# tooling, not typed by the operator: slash-command echoes, injected
|
|
29
|
+
# context, and the agent's own note that a turn was interrupted.
|
|
30
|
+
_INJECTED = (
|
|
31
|
+
"<command-",
|
|
32
|
+
"<local-command-",
|
|
33
|
+
"<system-reminder>",
|
|
34
|
+
"<environment_context>",
|
|
35
|
+
"<user_instructions>",
|
|
36
|
+
"[Request interrupted",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
Entry = tuple[str, str]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def recover_conversation(agent: str, path: Path, since: datetime) -> tuple[Entry, ...]:
|
|
43
|
+
"""The ``(speaker, text)`` entries newer than ``since``, in order.
|
|
44
|
+
|
|
45
|
+
Speakers are ``operator`` and ``agent``; consecutive messages of one
|
|
46
|
+
side form one entry. An agent whose format ww does not read, or a file
|
|
47
|
+
it cannot read, recovers nothing.
|
|
48
|
+
"""
|
|
49
|
+
reader = _READERS.get(agent)
|
|
50
|
+
if reader is None:
|
|
51
|
+
return ()
|
|
52
|
+
try:
|
|
53
|
+
records = list(_records(path))
|
|
54
|
+
except OSError:
|
|
55
|
+
return ()
|
|
56
|
+
return _merged(reader(records, since))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _records(path: Path) -> Iterator[dict[str, Any]]:
|
|
60
|
+
"""The JSON objects of the file's last ``READ_LIMIT`` bytes, one per line."""
|
|
61
|
+
with path.open("rb") as handle:
|
|
62
|
+
size = handle.seek(0, 2)
|
|
63
|
+
handle.seek(max(0, size - READ_LIMIT))
|
|
64
|
+
if size > READ_LIMIT:
|
|
65
|
+
handle.readline() # the first line read is cut short
|
|
66
|
+
for raw in handle:
|
|
67
|
+
try:
|
|
68
|
+
value = json.loads(raw.decode("utf-8", errors="replace"))
|
|
69
|
+
except json.JSONDecodeError:
|
|
70
|
+
continue
|
|
71
|
+
if isinstance(value, dict):
|
|
72
|
+
yield value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _claude_code(records: list[dict[str, Any]], since: datetime) -> Iterator[Entry]:
|
|
76
|
+
"""Claude Code's session JSONL.
|
|
77
|
+
|
|
78
|
+
A kept line looks like ``{"type": "user", "timestamp":
|
|
79
|
+
"2026-10-01T09:00:00.000Z", "message": {"role": "user", "content":
|
|
80
|
+
"Use a queue."}}``; an assistant line holds a list of blocks, of which
|
|
81
|
+
only ``{"type": "text", "text": ...}`` is kept, never ``tool_use`` or
|
|
82
|
+
``thinking``. Subagent (``isSidechain``) and injected (``isMeta``)
|
|
83
|
+
lines are skipped, and so is a user line carrying a ``tool_result``.
|
|
84
|
+
"""
|
|
85
|
+
for record in records:
|
|
86
|
+
kind = record.get("type")
|
|
87
|
+
if kind not in ("user", "assistant"):
|
|
88
|
+
continue
|
|
89
|
+
if record.get("isSidechain") is True or record.get("isMeta") is True:
|
|
90
|
+
continue
|
|
91
|
+
if not _newer(record.get("timestamp"), since):
|
|
92
|
+
continue
|
|
93
|
+
message = record.get("message")
|
|
94
|
+
if not isinstance(message, dict):
|
|
95
|
+
continue
|
|
96
|
+
text = _text_of(message.get("content"), ("text",))
|
|
97
|
+
if text:
|
|
98
|
+
yield ("operator" if kind == "user" else "agent", text)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _codex(records: list[dict[str, Any]], since: datetime) -> Iterator[Entry]:
|
|
102
|
+
"""A Codex rollout JSONL.
|
|
103
|
+
|
|
104
|
+
The conversation is read from event lines such as ``{"timestamp":
|
|
105
|
+
"2026-10-01T09:00:00.000Z", "type": "event_msg", "payload": {"type":
|
|
106
|
+
"user_message", "message": "Use a queue."}}`` and ``agent_message``.
|
|
107
|
+
A rollout without them falls back to ``response_item`` lines of payload
|
|
108
|
+
type ``message``, whose ``role`` is ``user`` or ``assistant`` and whose
|
|
109
|
+
content blocks are ``input_text`` or ``output_text``.
|
|
110
|
+
"""
|
|
111
|
+
recent = [record for record in records if _newer(record.get("timestamp"), since)]
|
|
112
|
+
payloads: list[tuple[object, dict[str, Any]]] = [
|
|
113
|
+
(record.get("type"), payload)
|
|
114
|
+
for record in recent
|
|
115
|
+
if isinstance(payload := record.get("payload"), dict)
|
|
116
|
+
]
|
|
117
|
+
speakers = {"user_message": "operator", "agent_message": "agent"}
|
|
118
|
+
if any(kind == "event_msg" for kind, _ in payloads):
|
|
119
|
+
for kind, payload in payloads:
|
|
120
|
+
speaker = speakers.get(str(payload.get("type")), "")
|
|
121
|
+
message = payload.get("message")
|
|
122
|
+
if kind == "event_msg" and speaker and isinstance(message, str):
|
|
123
|
+
text = _kept(message)
|
|
124
|
+
if text:
|
|
125
|
+
yield (speaker, text)
|
|
126
|
+
return
|
|
127
|
+
roles = {"user": "operator", "assistant": "agent"}
|
|
128
|
+
for kind, payload in payloads:
|
|
129
|
+
speaker = roles.get(str(payload.get("role")), "")
|
|
130
|
+
if kind != "response_item" or payload.get("type") != "message" or not speaker:
|
|
131
|
+
continue
|
|
132
|
+
text = _text_of(payload.get("content"), ("input_text", "output_text", "text"))
|
|
133
|
+
if text:
|
|
134
|
+
yield (speaker, text)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _text_of(content: object, kinds: tuple[str, ...]) -> str:
|
|
138
|
+
"""The kept text of a message: a plain string, or its text blocks joined."""
|
|
139
|
+
if isinstance(content, str):
|
|
140
|
+
return _kept(content)
|
|
141
|
+
if not isinstance(content, list):
|
|
142
|
+
return ""
|
|
143
|
+
parts = (
|
|
144
|
+
_kept(block["text"])
|
|
145
|
+
for block in content
|
|
146
|
+
if isinstance(block, dict)
|
|
147
|
+
and block.get("type") in kinds
|
|
148
|
+
and isinstance(block.get("text"), str)
|
|
149
|
+
)
|
|
150
|
+
return "\n\n".join(part for part in parts if part)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _kept(text: str) -> str:
|
|
154
|
+
"""``text`` stripped, or empty when the agent's tooling put it there."""
|
|
155
|
+
text = text.strip()
|
|
156
|
+
return "" if text.startswith(_INJECTED) else text
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _newer(value: object, since: datetime) -> bool:
|
|
160
|
+
if not isinstance(value, str):
|
|
161
|
+
return False
|
|
162
|
+
try:
|
|
163
|
+
moment = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
164
|
+
except ValueError:
|
|
165
|
+
return False
|
|
166
|
+
return moment.tzinfo is not None and moment > since
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _merged(entries: Iterator[Entry]) -> tuple[Entry, ...]:
|
|
170
|
+
"""Consecutive messages of one side as one entry, as a reader sees a turn."""
|
|
171
|
+
result: list[Entry] = []
|
|
172
|
+
for speaker, text in entries:
|
|
173
|
+
if result and result[-1][0] == speaker:
|
|
174
|
+
result[-1] = (speaker, result[-1][1] + "\n\n" + text)
|
|
175
|
+
else:
|
|
176
|
+
result.append((speaker, text))
|
|
177
|
+
return tuple(result)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
_READERS: dict[str, Callable[[list[dict[str, Any]], datetime], Iterator[Entry]]] = {
|
|
181
|
+
"claudecode": _claude_code,
|
|
182
|
+
"codex": _codex,
|
|
183
|
+
}
|