Skip to content

Commit b1263bd

Browse files
committed
feat: implement progressive reading with steps_read tracking (#28)
1 parent be325fa commit b1263bd

File tree

5 files changed

+106
-61
lines changed

5 files changed

+106
-61
lines changed

src/workflow_as_list/cli/exec.py

Lines changed: 76 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
"""workflow exec command - Execution instance management."""
1+
"""workflow exec command - Execution instance management for progressive reading.
2+
3+
Design:
4+
- read: Read current step content (mark as read)
5+
- next: Advance to next step (must read first)
6+
7+
Progressive reading ensures Agent cannot skip steps without reading.
8+
"""
29

310
from pathlib import Path
411

@@ -17,17 +24,22 @@ def print_output(type: OutputType, message: str):
1724
console.print(f"[{type.value}] {message}")
1825

1926

20-
app = typer.Typer(help="Execution instance management")
27+
app = typer.Typer(help="Execution instance management (progressive reading)")
2128

2229

23-
@app.command("show")
24-
def exec_show(
30+
@app.command("read")
31+
def exec_read(
2532
execution_id: str = typer.Argument(..., help="Execution instance ID"),
2633
):
27-
"""Show execution instance details.
34+
"""Read current step content and mark as read.
35+
36+
Agent reads the current step, understands it, then executes operations
37+
using its own tools (git, file operations, API calls, etc.).
38+
39+
After reading and executing, call 'workflow exec next' to advance.
2840
2941
Example:
30-
workflow exec show commit-abc123
42+
workflow exec read commit-abc123
3143
"""
3244
ensure_directories()
3345
executor = Executor()
@@ -40,28 +52,54 @@ def exec_show(
4052
)
4153
raise typer.Exit(4)
4254

43-
console.print(f"Execution: {execution.execution_id}")
44-
console.print(f" Workflow: {execution.workflow_name}")
45-
console.print(f" Status: {execution.status.value}")
46-
console.print(f" Step: {execution.current_step}/{execution.steps_total}")
47-
console.print(f" Outputs: {execution.outputs_path}")
55+
# Load workflow and parse
56+
workflow = executor.get_workflow(execution.workflow_name)
57+
if not workflow:
58+
print_output(OutputType.ERROR, f"Workflow not found: {execution.workflow_name}")
59+
raise typer.Exit(4)
60+
61+
content = Path(workflow.file_path).read_text()
62+
parser = WorkflowParser(content)
63+
parser.parse()
64+
65+
# Get current step
66+
current_step = executor.get_next_step(execution, parser)
67+
if current_step is None:
68+
print_output(OutputType.SUCCESS, f"Execution completed: {execution_id}")
69+
console.print(f" All {execution.steps_total} steps completed")
70+
return
71+
72+
# Display current step content
73+
console.print(f"[INFO] Step {execution.current_step + 1}/{execution.steps_total}:")
74+
console.print(f" {current_step['content']}")
75+
if current_step.get("metadata"):
76+
console.print(f" Comments ({len(current_step['metadata'])}):")
77+
for comment in current_step["metadata"]:
78+
console.print(f" {comment}")
79+
80+
# Mark as read (if not already)
81+
if execution.current_step not in execution.steps_read:
82+
execution.steps_read.append(execution.current_step)
83+
executor.update_execution(execution)
84+
console.print(f"\n[INFO] Step {execution.current_step + 1} marked as read")
4885

4986

5087
@app.command("next")
5188
def exec_next(
5289
execution_id: str = typer.Argument(..., help="Execution instance ID"),
53-
output: str = typer.Option(None, "-o", "--output", help="Step output to store"),
54-
quiet: bool = typer.Option(False, "-q", "--quiet", help="Quiet mode (no output)"),
5590
):
56-
"""Mark current step complete and advance to next step.
91+
"""Advance to next step (must read current step first).
5792
58-
Shows current step content, stores output (if provided), advances execution,
59-
and displays next step content.
93+
Checks if current step has been read (progressive reading enforcement).
94+
If read, advances to next step. If not read, shows error.
95+
96+
Agent should:
97+
1. workflow exec read <id> (read and understand)
98+
2. Execute operations (using own tools)
99+
3. workflow exec next <id> (advance when ready)
60100
61101
Example:
62102
workflow exec next commit-abc123
63-
workflow exec next commit-abc123 -o "result"
64-
workflow exec next commit-abc123 -q # Quiet mode for scripts
65103
"""
66104
ensure_directories()
67105
executor = Executor()
@@ -71,7 +109,19 @@ def exec_next(
71109
print_output(OutputType.ERROR, f"Execution not found: {execution_id}")
72110
raise typer.Exit(4)
73111

74-
# Load workflow and parse
112+
# Check if current step has been read
113+
if execution.current_step not in execution.steps_read:
114+
print_output(OutputType.ERROR, "Current step not read yet")
115+
print_output(
116+
OutputType.NEXT,
117+
f"Read first: workflow exec read {execution_id}",
118+
)
119+
raise typer.Exit(1)
120+
121+
# Advance execution
122+
executor.advance_execution(execution)
123+
124+
# Check if completed
75125
workflow = executor.get_workflow(execution.workflow_name)
76126
if not workflow:
77127
print_output(OutputType.ERROR, f"Workflow not found: {execution.workflow_name}")
@@ -81,40 +131,15 @@ def exec_next(
81131
parser = WorkflowParser(content)
82132
parser.parse()
83133

84-
# Get current step BEFORE advancing
85-
current_step = executor.get_next_step(execution, parser)
86-
87-
# Show current step (unless quiet mode)
88-
if not quiet and current_step:
89-
console.print(
90-
f"[INFO] Current step {execution.current_step + 1}/{execution.steps_total}:"
91-
)
92-
console.print(f" {current_step['content']}")
93-
if current_step.get("metadata"):
94-
for comment in current_step["metadata"][:3]:
95-
console.print(f" {comment}")
96-
97-
# Store output if provided
98-
if output:
99-
executor.store_output(execution_id, execution.current_step, output)
100-
101-
# Advance execution
102-
executor.advance_execution(execution)
103-
104-
# Get next step AFTER advancing
105134
next_step = executor.get_next_step(execution, parser)
106135
if next_step is None:
107136
print_output(OutputType.SUCCESS, f"Execution completed: {execution_id}")
108-
console.print(f" Final step: {execution.steps_total}/{execution.steps_total}")
137+
console.print(f" Total steps: {execution.steps_total}")
109138
return
110139

111-
# Show next step (unless quiet mode)
112-
if not quiet:
113-
print_output(
114-
OutputType.SUCCESS,
115-
f"Advanced to step {execution.current_step}/{execution.steps_total}",
116-
)
117-
console.print(f" Next step: {next_step['content']}")
118-
if next_step.get("metadata"):
119-
for comment in next_step["metadata"][:3]:
120-
console.print(f" {comment}")
140+
# Show progress (not content - Agent reads with 'read' command)
141+
print_output(
142+
OutputType.SUCCESS,
143+
f"Advanced to step {execution.current_step + 1}/{execution.steps_total}",
144+
)
145+
console.print(f" Read next: workflow exec read {execution_id}")

src/workflow_as_list/config.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@
44
import configparser
55
from pathlib import Path
66

7-
from .constants import CONFIG_FILE, PROJECT_ROOT, TOKEN_HUB_LOWER, TOKEN_HUB_UPPER
7+
from .constants import (
8+
CONFIG_FILE,
9+
DEFAULT_HOST,
10+
DEFAULT_PORT,
11+
PROJECT_ROOT,
12+
TOKEN_HUB_LOWER,
13+
TOKEN_HUB_UPPER,
14+
)
815
from .models import Config
916

1017
DEFAULT_CONFIG = Config(
1118
blacklist=[],
1219
whitelist=[],
1320
enable_whitelist=False,
14-
host="127.0.0.1",
15-
port=8080,
21+
host=DEFAULT_HOST,
22+
port=DEFAULT_PORT,
1623
config_dir=str(PROJECT_ROOT),
1724
token_hub_lower=TOKEN_HUB_LOWER,
1825
token_hub_upper=TOKEN_HUB_UPPER,

src/workflow_as_list/constants.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
# Project Root
1414
# =============================================================================
1515

16-
PROJECT_ROOT = Path.home() / ".workflow-as-list"
1716
"""
1817
All workflow-as-list state lives here.
1918
@@ -23,6 +22,8 @@
2322
- Easy backup: backup one directory
2423
- State visible: always know where to look
2524
"""
25+
PROJECT_ROOT = Path.home() / ".workflow-as-list"
26+
LOGS_ROOT = PROJECT_ROOT / "logs"
2627

2728
# =============================================================================
2829
# Configuration Files
@@ -36,7 +37,7 @@
3637
# =============================================================================
3738

3839
PID_FILE = PROJECT_ROOT / "server.pid"
39-
SERVER_LOG = PROJECT_ROOT / "logs" / "server.log"
40+
SERVER_LOG = LOGS_ROOT / "server.log"
4041

4142
# =============================================================================
4243
# State Directories
@@ -93,9 +94,9 @@ def ensure_directories() -> None:
9394
Creates PROJECT_ROOT and all subdirectories on first use.
9495
"""
9596
PROJECT_ROOT.mkdir(parents=True, exist_ok=True)
97+
LOGS_ROOT.mkdir(parents=True, exist_ok=True)
9698
STATE_DIR.mkdir(parents=True, exist_ok=True)
9799
EXECUTIONS_DIR.mkdir(parents=True, exist_ok=True)
98100
OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
99101
CACHE_DIR.mkdir(parents=True, exist_ok=True)
100102
IMPORTS_CACHE.mkdir(parents=True, exist_ok=True)
101-
(PROJECT_ROOT / "logs").mkdir(parents=True, exist_ok=True)

src/workflow_as_list/models.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
from pydantic import BaseModel, Field
99

10+
# Import token constants for Config defaults
11+
from .constants import TOKEN_HUB_LOWER, TOKEN_HUB_UPPER
12+
1013

1114
# Audit status for workflow lifecycle
1215
class AuditStatus(str, Enum):
@@ -62,6 +65,11 @@ class Execution(BaseModel):
6265
6366
Created when workflow.run() is called.
6467
Stored in PROJECT_ROOT/state/executions/<name>-<timestamp>.json
68+
69+
Progressive reading design:
70+
- Agent must read current step before advancing (steps_read tracking)
71+
- steps_read contains indices of steps that have been read
72+
- next command checks if current_step is in steps_read
6573
"""
6674

6775
execution_id: str = Field(..., description="Unique execution identifier")
@@ -78,6 +86,10 @@ class Execution(BaseModel):
7886
# Execution state
7987
steps_total: int = Field(default=0, description="Total steps in workflow")
8088
outputs_path: str = Field(default="", description="Path to outputs directory")
89+
steps_read: list[int] = Field(
90+
default_factory=list,
91+
description="Indices of steps that have been read (progressive reading)",
92+
)
8193

8294

8395
class Config(BaseModel):
@@ -107,10 +119,10 @@ class Config(BaseModel):
107119

108120
# Token length constraints (from design docs)
109121
token_hub_lower: int = Field(
110-
default=282,
122+
default=TOKEN_HUB_LOWER,
111123
description="Recommended minimum hub tokens (warning if below)",
112124
)
113125
token_hub_upper: int = Field(
114-
default=358,
126+
default=TOKEN_HUB_UPPER,
115127
description="Maximum hub tokens before decomposition required (error if above)",
116128
)

src/workflow_as_list/server_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,11 @@ def print_status() -> None:
188188
info = status()
189189

190190
if info["running"]:
191-
print(f" Server running (PID: {info['pid']})")
191+
print(f"[OK] Server running (PID: {info['pid']})")
192192
print(f" Host: {info['host']}")
193193
print(f" Port: {info['port']}")
194194
print(f" URL: http://{info['host']}:{info['port']}")
195195
print(f" API: http://{info['host']}:{info['port']}/docs")
196196
else:
197-
print(" Server not running")
197+
print("[NO] Server not running")
198198
print(" Start with: workflow server start")

0 commit comments

Comments
 (0)