fix: harden workflow demo and evaluation tooling
This commit is contained in:
+4
-2
@@ -10,8 +10,10 @@ implementation plans are kept for context, not as active instructions.
|
|||||||
and verification commands.
|
and verification commands.
|
||||||
- [`add/2026-06-workflow-platform-presentation.md`](add/2026-06-workflow-platform-presentation.md):
|
- [`add/2026-06-workflow-platform-presentation.md`](add/2026-06-workflow-platform-presentation.md):
|
||||||
concise presentation narrative for the current product shape and demo flow.
|
concise presentation narrative for the current product shape and demo flow.
|
||||||
- [`thesis/thesis-outline.md`](thesis/thesis-outline.md): thesis/report scaffold,
|
- [`thesis/system-design-implementation.md`](thesis/system-design-implementation.md):
|
||||||
argument structure, evidence checklist, limitations, and future work.
|
maintained thesis/report document.
|
||||||
|
- [`thesis/evidence-index.md`](thesis/evidence-index.md): claim-to-code and
|
||||||
|
claim-to-test evidence map for the thesis.
|
||||||
- [`current_roadmap.md`](current_roadmap.md): active next-work list after the
|
- [`current_roadmap.md`](current_roadmap.md): active next-work list after the
|
||||||
core type-shape cleanup.
|
core type-shape cleanup.
|
||||||
- [`wf_core_architecture.md`](wf_core_architecture.md): kernel package
|
- [`wf_core_architecture.md`](wf_core_architecture.md): kernel package
|
||||||
|
|||||||
@@ -134,7 +134,8 @@ clear operator feedback before adding more architecture.
|
|||||||
`ux_issues_found: []` so debug-profile reports do not fail by omission.
|
`ux_issues_found: []` so debug-profile reports do not fail by omission.
|
||||||
- Completed: `wf draft bind` now reuses existing workflow input/state schema
|
- Completed: `wf draft bind` now reuses existing workflow input/state schema
|
||||||
fields when binding to step-local inputs, avoiding redundant-schema failures
|
fields when binding to step-local inputs, avoiding redundant-schema failures
|
||||||
found by debug challenge runs.
|
found by debug challenge runs. Implementation:
|
||||||
|
[`idempotent draft bind inputs`](historical/superpowers/plans/2026-06-29-idempotent-draft-bind-inputs.md).
|
||||||
- Keep status read-only; do not mutate registry, auth, config, or stores.
|
- Keep status read-only; do not mutate registry, auth, config, or stores.
|
||||||
|
|
||||||
## Priority 2: Durable Run/Resume Hardening
|
## Priority 2: Durable Run/Resume Hardening
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
Date: 2026-07-01
|
Date: 2026-07-01
|
||||||
|
|
||||||
Status: Approved direction. Needs an executable implementation plan before code
|
Status: Implemented. This document is the live interrupt contract.
|
||||||
changes.
|
|
||||||
|
|
||||||
Related:
|
Related:
|
||||||
|
|
||||||
@@ -27,23 +26,18 @@ This is required for the Workflow Console and useful for CLI, JSON-RPC, MCP, and
|
|||||||
agent clients. It prevents every client from needing workflow-specific code just
|
agent clients. It prevents every client from needing workflow-specific code just
|
||||||
to render and answer an approval step.
|
to render and answer an approval step.
|
||||||
|
|
||||||
## Current Gap
|
## Implemented Contract
|
||||||
|
|
||||||
The current core interrupt model has useful mechanics but not a complete public
|
The core interrupt model now exposes a complete public pause/resume contract.
|
||||||
contract.
|
|
||||||
|
|
||||||
- `InterruptNode` stores `kind`, request bindings, resume bindings, and resume
|
- `InterruptNode` stores `kind`, request bindings, resume bindings, and resume
|
||||||
outcomes.
|
outcomes, plus `request_schema` and `resume_schema`.
|
||||||
- Runtime builds an `InterruptRequest` with id, frame id, node id, kind,
|
- Runtime builds an `InterruptRequest` with id, frame id, node id, kind,
|
||||||
payload, route, and resumability.
|
payload, route, resumability, outcomes, request schema, resume schema, and a
|
||||||
- `workflow.runs.inspect` serializes the current runtime interrupt request.
|
`typed` marker.
|
||||||
- Resume validates the selected outcome against `InterruptNode.outcomes` and
|
- `workflow.runs.inspect` serializes that persisted interrupt request.
|
||||||
applies resume bindings.
|
- Runtime validates request payloads before pausing and validates resume
|
||||||
|
payloads against the persisted pause-time schema before mutating state.
|
||||||
What is missing is a machine-readable schema for the request payload and resume
|
|
||||||
payload. A client can see data, but it cannot know whether the response should
|
|
||||||
be `{ "approved": true }`, `{ "selected_issue_ids": [...] }`, or something else
|
|
||||||
without reading workflow code or challenge-specific docs.
|
|
||||||
|
|
||||||
This is close to the LangGraph-style `interrupt(value)` and `Command(resume=...)`
|
This is close to the LangGraph-style `interrupt(value)` and `Command(resume=...)`
|
||||||
pattern: flexible and simple, but the response contract is mostly app
|
pattern: flexible and simple, but the response contract is mostly app
|
||||||
@@ -51,7 +45,7 @@ convention. `wf` should keep the flexibility while making the contract explicit.
|
|||||||
|
|
||||||
## Design Summary
|
## Design Summary
|
||||||
|
|
||||||
Add JSON Schema contracts to interrupt nodes and carry them through persisted run
|
JSON Schema contracts are carried from interrupt nodes through persisted run
|
||||||
inspection and resume validation.
|
inspection and resume validation.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
@@ -73,7 +67,7 @@ sequenceDiagram
|
|||||||
|
|
||||||
## Core Model
|
## Core Model
|
||||||
|
|
||||||
Extend `InterruptNode` with two optional schema fields:
|
`InterruptNode` carries two schema fields:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class InterruptNode(BaseModel):
|
class InterruptNode(BaseModel):
|
||||||
|
|||||||
@@ -347,12 +347,12 @@ The UI selects all issues by default and allows individual deselection.
|
|||||||
|
|
||||||
## Self-Describing Interrupt Dependency
|
## Self-Describing Interrupt Dependency
|
||||||
|
|
||||||
The current interrupt payload exposes `kind` and data but not a complete
|
Interrupted run inspection now exposes `kind`, payload, outcomes,
|
||||||
machine-readable response contract. The Workflow Console requires explicit
|
`request_schema`, `resume_schema`, and `typed`. Runtime validates request and
|
||||||
request and resume schemas so it can render and validate arbitrary interrupts
|
resume payloads against those contracts. The Workflow Console can therefore
|
||||||
without reading workflow code.
|
render and validate arbitrary interrupts without reading workflow code.
|
||||||
|
|
||||||
The prerequisite contract is defined in
|
The implemented contract is defined in
|
||||||
[Self-describing interrupt contracts](2026-07-01-self-describing-interrupt-contracts.md).
|
[Self-describing interrupt contracts](2026-07-01-self-describing-interrupt-contracts.md).
|
||||||
|
|
||||||
## Replay And Failure Handling
|
## Replay And Failure Handling
|
||||||
|
|||||||
@@ -35,6 +35,29 @@ if (-not (Test-Path $pandoc_diagram)) {
|
|||||||
Write-Error "pandoc diagram wrapper not found at $pandoc_diagram. Make sure it exists, then rerun this command."
|
Write-Error "pandoc diagram wrapper not found at $pandoc_diagram. Make sure it exists, then rerun this command."
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Test-RenderNeedsAgentResults([string[]] $arguments) {
|
||||||
|
for ($index = 0; $index -lt $arguments.Count; $index++) {
|
||||||
|
$candidate = $null
|
||||||
|
if ($arguments[$index] -in @("-i", "--input")) {
|
||||||
|
if ($index + 1 -lt $arguments.Count) {
|
||||||
|
$candidate = $arguments[$index + 1]
|
||||||
|
$index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($arguments[$index].EndsWith(".md")) {
|
||||||
|
$candidate = $arguments[$index]
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
$candidate -and
|
||||||
|
(Test-Path -LiteralPath $candidate) -and
|
||||||
|
(Select-String -LiteralPath $candidate -SimpleMatch "include-agent-challenge-results" -Quiet)
|
||||||
|
) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
. $pandoc_diagram
|
. $pandoc_diagram
|
||||||
|
|
||||||
$pandoc_crossref = Get-Command pandoc-crossref -ErrorAction SilentlyContinue
|
$pandoc_crossref = Get-Command pandoc-crossref -ErrorAction SilentlyContinue
|
||||||
@@ -62,7 +85,7 @@ if (-not (Test-Path $include_markdown_filter)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$agent_results = Join-Path $PSScriptRoot "agent-challenge-results.md"
|
$agent_results = Join-Path $PSScriptRoot "agent-challenge-results.md"
|
||||||
if (-not (Test-Path $agent_results)) {
|
if ((Test-RenderNeedsAgentResults $RemainingArgs) -and -not (Test-Path $agent_results)) {
|
||||||
Write-Error "agent-challenge-results.md is missing. Run generate_agent_challenge_evaluation.py first."
|
Write-Error "agent-challenge-results.md is missing. Run generate_agent_challenge_evaluation.py first."
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,10 +54,17 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
args = _parse_args(argv)
|
args = _parse_args(argv)
|
||||||
for path in generate(
|
output_dir = args.output_dir.resolve()
|
||||||
manifest_path=args.manifest.resolve(), output_dir=args.output_dir.resolve()
|
for path in generate(manifest_path=args.manifest.resolve(), output_dir=output_dir):
|
||||||
):
|
resolved = path.resolve()
|
||||||
print(path.relative_to(ROOT))
|
try:
|
||||||
|
display_path = resolved.relative_to(ROOT)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
display_path = resolved.relative_to(output_dir)
|
||||||
|
except ValueError:
|
||||||
|
display_path = resolved
|
||||||
|
print(display_path)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ if (-not (Test-Path $file)) {
|
|||||||
$file = (Resolve-Path $file).Path
|
$file = (Resolve-Path $file).Path
|
||||||
$resourcePath = [System.IO.Path]::GetDirectoryName($file)
|
$resourcePath = [System.IO.Path]::GetDirectoryName($file)
|
||||||
|
|
||||||
$evaluationGenerator = Join-Path $PSScriptRoot "generate_agent_challenge_evaluation.py"
|
$needsAgentResults = Select-String -LiteralPath $file -SimpleMatch "include-agent-challenge-results" -Quiet
|
||||||
& uv run python $evaluationGenerator
|
if ($needsAgentResults) {
|
||||||
if ($LASTEXITCODE -ne 0) {
|
$evaluationGenerator = Join-Path $PSScriptRoot "generate_agent_challenge_evaluation.py"
|
||||||
throw "agent challenge evaluation generation failed with exit code $LASTEXITCODE"
|
& uv run python $evaluationGenerator
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "agent challenge evaluation generation failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# name without extension
|
# name without extension
|
||||||
|
|||||||
@@ -1994,7 +1994,7 @@ product.
|
|||||||
|
|
||||||
Evidence:
|
Evidence:
|
||||||
|
|
||||||
- `docs/thesis/thesis-outline.md`
|
- `docs/historical/thesis/thesis-outline.md`
|
||||||
- `docs/current_roadmap.md`
|
- `docs/current_roadmap.md`
|
||||||
- Absence of scheduler, visual-editor, and secret-manager production packages
|
- Absence of scheduler, visual-editor, and secret-manager production packages
|
||||||
in the current source tree.
|
in the current source tree.
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ FIGURE_STEMS = (
|
|||||||
"agent-challenge-token-volume",
|
"agent-challenge-token-volume",
|
||||||
)
|
)
|
||||||
_MANUAL_OUTCOMES = frozenset({"pass", "invalid", "fail"})
|
_MANUAL_OUTCOMES = frozenset({"pass", "invalid", "fail"})
|
||||||
|
_TASK_OUTCOMES = frozenset({"success", "failed", "timeout", "runner_error"})
|
||||||
|
_EVALUATION_PROFILES = frozenset({"none", "skills", "all"})
|
||||||
_CHALLENGE_ORDER = {"browser": 0, "report": 1}
|
_CHALLENGE_ORDER = {"browser": 0, "report": 1}
|
||||||
_MODEL_ORDER = {"deepseek": 0, "mimo": 1}
|
_MODEL_ORDER = {"deepseek": 0, "mimo": 1}
|
||||||
_PROFILE_ORDER = {"none": 0, "skills": 1, "all": 2}
|
_PROFILE_ORDER = {"none": 0, "skills": 1, "all": 2}
|
||||||
@@ -105,6 +107,19 @@ def _load_trial(
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"unsupported manual outcome {manual_outcome!r}: {report_path}"
|
f"unsupported manual outcome {manual_outcome!r}: {report_path}"
|
||||||
)
|
)
|
||||||
|
profile = _string(run.get("profile"), field="runs[].profile")
|
||||||
|
if profile not in _EVALUATION_PROFILES:
|
||||||
|
raise ValueError(f"unsupported evaluation profile {profile!r}: {report_path}")
|
||||||
|
task_outcome = _string(run.get("task_outcome"), field="runs[].task_outcome")
|
||||||
|
if task_outcome not in _TASK_OUTCOMES:
|
||||||
|
raise ValueError(f"unsupported task outcome {task_outcome!r}: {report_path}")
|
||||||
|
raw_audit_notes = run.get("audit_notes")
|
||||||
|
if raw_audit_notes is None:
|
||||||
|
audit_notes = ""
|
||||||
|
elif isinstance(raw_audit_notes, str):
|
||||||
|
audit_notes = raw_audit_notes
|
||||||
|
else:
|
||||||
|
raise ValueError(f"runs[].audit_notes must be a string: {report_path}")
|
||||||
|
|
||||||
duration = run.get("duration_seconds")
|
duration = run.get("duration_seconds")
|
||||||
if not isinstance(duration, int | float):
|
if not isinstance(duration, int | float):
|
||||||
@@ -117,7 +132,7 @@ def _load_trial(
|
|||||||
_string(run.get("challenge"), field="runs[].challenge")
|
_string(run.get("challenge"), field="runs[].challenge")
|
||||||
),
|
),
|
||||||
model=short_model_name(_string(run.get("model"), field="runs[].model")),
|
model=short_model_name(_string(run.get("model"), field="runs[].model")),
|
||||||
profile=_string(run.get("profile"), field="runs[].profile"),
|
profile=profile,
|
||||||
trial_index=_integer(run.get("trial_index"), field="runs[].trial_index"),
|
trial_index=_integer(run.get("trial_index"), field="runs[].trial_index"),
|
||||||
repository_commit=_string(
|
repository_commit=_string(
|
||||||
run.get("repository_commit"), field="runs[].repository_commit"
|
run.get("repository_commit"), field="runs[].repository_commit"
|
||||||
@@ -126,10 +141,10 @@ def _load_trial(
|
|||||||
run.get("base_prompt_hash"), field="runs[].base_prompt_hash"
|
run.get("base_prompt_hash"), field="runs[].base_prompt_hash"
|
||||||
),
|
),
|
||||||
manual_outcome=manual_outcome,
|
manual_outcome=manual_outcome,
|
||||||
task_outcome=_string(run.get("task_outcome"), field="runs[].task_outcome"),
|
task_outcome=task_outcome,
|
||||||
duration_seconds=float(duration),
|
duration_seconds=float(duration),
|
||||||
tokens_total=_integer(run.get("tokens_total"), field="runs[].tokens_total"),
|
tokens_total=_integer(run.get("tokens_total"), field="runs[].tokens_total"),
|
||||||
audit_notes=str(run.get("audit_notes") or ""),
|
audit_notes=audit_notes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ _MODEL_STYLES = {
|
|||||||
_PROFILE_ORDER = {"none": 0, "skills": 1, "all": 2}
|
_PROFILE_ORDER = {"none": 0, "skills": 1, "all": 2}
|
||||||
_CHALLENGE_ORDER = {"browser": 0, "report": 1}
|
_CHALLENGE_ORDER = {"browser": 0, "report": 1}
|
||||||
_MODEL_ORDER = {"deepseek": 0, "mimo": 1}
|
_MODEL_ORDER = {"deepseek": 0, "mimo": 1}
|
||||||
|
_TASK_OUTCOME_ORDER = ("success", "failed", "timeout", "runner_error")
|
||||||
|
|
||||||
|
|
||||||
def _configure_matplotlib() -> Any:
|
def _configure_matplotlib() -> Any:
|
||||||
@@ -170,8 +171,17 @@ def _outcomes_by_cell(cohort: EvaluationCohort, plt: Any) -> Figure:
|
|||||||
def _automatic_vs_manual(cohort: EvaluationCohort, plt: Any) -> Figure:
|
def _automatic_vs_manual(cohort: EvaluationCohort, plt: Any) -> Figure:
|
||||||
from matplotlib.colors import LinearSegmentedColormap
|
from matplotlib.colors import LinearSegmentedColormap
|
||||||
|
|
||||||
task_labels = ("success", "failed")
|
task_labels = tuple(
|
||||||
|
outcome
|
||||||
|
for outcome in _TASK_OUTCOME_ORDER
|
||||||
|
if any(trial.task_outcome == outcome for trial in cohort.trials)
|
||||||
|
)
|
||||||
manual_labels = ("pass", "invalid", "fail")
|
manual_labels = ("pass", "invalid", "fail")
|
||||||
|
unknown_task_outcomes = sorted(
|
||||||
|
{trial.task_outcome for trial in cohort.trials} - set(_TASK_OUTCOME_ORDER)
|
||||||
|
)
|
||||||
|
if unknown_task_outcomes:
|
||||||
|
raise ValueError(f"unsupported task outcomes: {unknown_task_outcomes}")
|
||||||
matrix = [
|
matrix = [
|
||||||
[
|
[
|
||||||
sum(
|
sum(
|
||||||
@@ -211,7 +221,7 @@ def _automatic_vs_manual(cohort: EvaluationCohort, plt: Any) -> Figure:
|
|||||||
axis.set_ylabel("Automatic task outcome")
|
axis.set_ylabel("Automatic task outcome")
|
||||||
axis.set_title("Automatic completion does not imply clean evaluation evidence")
|
axis.set_title("Automatic completion does not imply clean evaluation evidence")
|
||||||
axis.set_xticks([value - 0.5 for value in range(1, len(manual_labels))], minor=True)
|
axis.set_xticks([value - 0.5 for value in range(1, len(manual_labels))], minor=True)
|
||||||
axis.set_yticks([0.5], minor=True)
|
axis.set_yticks([value - 0.5 for value in range(1, len(task_labels))], minor=True)
|
||||||
axis.grid(which="minor", color="white", linewidth=2)
|
axis.grid(which="minor", color="white", linewidth=2)
|
||||||
axis.tick_params(which="minor", bottom=False, left=False)
|
axis.tick_params(which="minor", bottom=False, left=False)
|
||||||
figure.colorbar(image, ax=axis, label="Trial count", shrink=0.82)
|
figure.colorbar(image, ax=axis, label="Trial count", shrink=0.82)
|
||||||
@@ -275,7 +285,11 @@ def _scatter_metric(
|
|||||||
profiles = ("none", "skills", "all")
|
profiles = ("none", "skills", "all")
|
||||||
wave_offsets = {1: -0.055, 2: 0.0, 3: 0.055}
|
wave_offsets = {1: -0.055, 2: 0.0, 3: 0.055}
|
||||||
for trial in sorted(trials, key=_trial_sort_key):
|
for trial in sorted(trials, key=_trial_sort_key):
|
||||||
style = _MODEL_STYLES[trial.model]
|
style = _MODEL_STYLES.get(trial.model)
|
||||||
|
if style is None:
|
||||||
|
raise ValueError(f"unsupported evaluation model {trial.model!r}")
|
||||||
|
if trial.profile not in profiles:
|
||||||
|
raise ValueError(f"unsupported evaluation profile {trial.profile!r}")
|
||||||
x = (
|
x = (
|
||||||
profiles.index(trial.profile)
|
profiles.index(trial.profile)
|
||||||
+ float(style["offset"])
|
+ float(style["offset"])
|
||||||
|
|||||||
@@ -97,20 +97,8 @@ def resume_command_from_result(
|
|||||||
variant: str | None = None,
|
variant: str | None = None,
|
||||||
prompt_mode: PromptMode = "auto",
|
prompt_mode: PromptMode = "auto",
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""Build a resume command from new metadata or recover it from old raw results."""
|
"""Rebuild a resume command from validated metadata or old raw results."""
|
||||||
opencode = result.get("opencode")
|
opencode = result.get("opencode")
|
||||||
if isinstance(opencode, dict):
|
|
||||||
command = opencode.get("resume_command")
|
|
||||||
has_override = (
|
|
||||||
any(value is not None for value in (session_id, attach_url, model, variant))
|
|
||||||
or prompt_mode != "auto"
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
not has_override
|
|
||||||
and isinstance(command, list)
|
|
||||||
and all(isinstance(part, str) for part in command)
|
|
||||||
):
|
|
||||||
return command
|
|
||||||
|
|
||||||
stdout = result.get("stdout")
|
stdout = result.get("stdout")
|
||||||
stdout_text = stdout if isinstance(stdout, str) else ""
|
stdout_text = stdout if isinstance(stdout, str) else ""
|
||||||
|
|||||||
@@ -249,13 +249,10 @@ def render_trial_report_markdown(report: TrialReport) -> str:
|
|||||||
)
|
)
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
if report.opencode is not None:
|
if report.opencode is not None and report.opencode.session_id:
|
||||||
lines.append("## OpenCode Resume")
|
lines.append("## OpenCode Resume")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
if report.opencode.session_id:
|
lines.append(f"- Session: `{report.opencode.session_id}`")
|
||||||
lines.append(f"- Session: `{report.opencode.session_id}`")
|
|
||||||
else:
|
|
||||||
lines.append("- Session: not captured")
|
|
||||||
if report.opencode.attach_url:
|
if report.opencode.attach_url:
|
||||||
lines.append(f"- Attach URL: `{report.opencode.attach_url}`")
|
lines.append(f"- Attach URL: `{report.opencode.attach_url}`")
|
||||||
if report.opencode.resume_command:
|
if report.opencode.resume_command:
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(_display_command(command))
|
print(_display_command(command))
|
||||||
except ValueError as exc:
|
except (OSError, ValueError) as exc:
|
||||||
parser.error(str(exc))
|
parser.error(str(exc))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ param(
|
|||||||
[object[]]$models = @( # object[] because ModelProfile is not known yet
|
[object[]]$models = @( # object[] because ModelProfile is not known yet
|
||||||
[ModelProfile]::new("opencode/deepseek-v4-flash-free", "max"),
|
[ModelProfile]::new("opencode/deepseek-v4-flash-free", "max"),
|
||||||
[ModelProfile]::new("opencode/mimo-v2.5-free", "high")
|
[ModelProfile]::new("opencode/mimo-v2.5-free", "high")
|
||||||
|
# Disabled after repeated provider timeouts and resource-exhausted failures.
|
||||||
# [ModelProfile]::new("opencode/nemotron-3-ultra-free", "high")
|
# [ModelProfile]::new("opencode/nemotron-3-ultra-free", "high")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class ModelProfile:
|
|||||||
DEFAULT_MODELS = (
|
DEFAULT_MODELS = (
|
||||||
ModelProfile("opencode/deepseek-v4-flash-free", "max"),
|
ModelProfile("opencode/deepseek-v4-flash-free", "max"),
|
||||||
ModelProfile("opencode/mimo-v2.5-free", "high"),
|
ModelProfile("opencode/mimo-v2.5-free", "high"),
|
||||||
|
# Disabled after repeated provider timeouts and resource-exhausted failures.
|
||||||
# ModelProfile("opencode/nemotron-3-ultra-free", "high"),
|
# ModelProfile("opencode/nemotron-3-ultra-free", "high"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -42,8 +42,11 @@ uv run wf --config examples/lda_report_workflow/wf.config.json --local run inspe
|
|||||||
|
|
||||||
Resume with selected issues:
|
Resume with selected issues:
|
||||||
|
|
||||||
|
Copy one or more actual issue ids from the interrupt payload and replace the
|
||||||
|
placeholder below.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
uv run wf --config examples/lda_report_workflow/wf.config.json --local run resume <run_id> --payload '{"approved":true,"selected_issue_ids":["risk-1"],"comment":"Create selected issues."}'
|
uv run wf --config examples/lda_report_workflow/wf.config.json --local run resume <run_id> --payload '{"approved":true,"selected_issue_ids":["<issue_id_from_interrupt>"],"comment":"Create selected issues."}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cleanup
|
## Cleanup
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ def build_workflow() -> Workflow:
|
|||||||
input=[{"path": "input.selected_documents", "target": "names"}],
|
input=[{"path": "input.selected_documents", "target": "names"}],
|
||||||
output=[{"source": "documents", "target": "state.documents"}],
|
output=[{"source": "documents", "target": "state.documents"}],
|
||||||
)
|
)
|
||||||
|
reset_board = builder.use_ref(
|
||||||
|
"local.issue_board.reset_issue_board",
|
||||||
|
id="reset_board",
|
||||||
|
input=[{"path": "input.board_path", "target": "board_path"}],
|
||||||
|
)
|
||||||
analyze = builder.use_ref(
|
analyze = builder.use_ref(
|
||||||
"local.lda_report.analyze_documents",
|
"local.lda_report.analyze_documents",
|
||||||
id="analyze",
|
id="analyze",
|
||||||
@@ -164,7 +169,8 @@ def build_workflow() -> Workflow:
|
|||||||
end_completed = builder.end("completed", id="end_completed")
|
end_completed = builder.end("completed", id="end_completed")
|
||||||
end_cancelled = builder.end("cancelled", id="end_cancelled")
|
end_cancelled = builder.end("cancelled", id="end_cancelled")
|
||||||
|
|
||||||
builder.set_entry_point(read_docs)
|
builder.set_entry_point(reset_board)
|
||||||
|
builder.connect(reset_board, "ok", read_docs)
|
||||||
builder.connect(read_docs, "ok", analyze)
|
builder.connect(read_docs, "ok", analyze)
|
||||||
builder.connect(analyze, "ok", build_report)
|
builder.connect(analyze, "ok", build_report)
|
||||||
builder.connect(build_report, "ok", draft_issues)
|
builder.connect(build_report, "ok", draft_issues)
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"edges": [
|
"edges": [
|
||||||
|
{
|
||||||
|
"from": "reset_board",
|
||||||
|
"outcome": "ok",
|
||||||
|
"to": "read_docs"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"from": "read_docs",
|
"from": "read_docs",
|
||||||
"outcome": "ok",
|
"outcome": "ok",
|
||||||
@@ -86,6 +91,21 @@
|
|||||||
"timeout_seconds": null,
|
"timeout_seconds": null,
|
||||||
"type": "node"
|
"type": "node"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"desc": null,
|
||||||
|
"id": "reset_board",
|
||||||
|
"input": [
|
||||||
|
{
|
||||||
|
"path": "input.board_path",
|
||||||
|
"target": "board_path"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"node": "local.issue_board.reset_issue_board",
|
||||||
|
"output": [],
|
||||||
|
"retry": null,
|
||||||
|
"timeout_seconds": null,
|
||||||
|
"type": "node"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"desc": null,
|
"desc": null,
|
||||||
"id": "analyze",
|
"id": "analyze",
|
||||||
@@ -368,7 +388,7 @@
|
|||||||
"required": [],
|
"required": [],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"start": "read_docs",
|
"start": "reset_board",
|
||||||
"state_schema": {
|
"state_schema": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"analysis": {
|
"analysis": {
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ def resume_interrupt(
|
|||||||
)
|
)
|
||||||
|
|
||||||
validate_payload_against_schema(
|
validate_payload_against_schema(
|
||||||
step.resume_schema,
|
run.interrupt.resume_schema,
|
||||||
resume_payload,
|
resume_payload,
|
||||||
f"interrupt resume for {step.id}",
|
f"interrupt resume for {step.id}",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ async def test_interrupt_request_payload_validates_against_schema() -> None:
|
|||||||
input_schema=_schema(),
|
input_schema=_schema(),
|
||||||
state_schema=StateSchema.from_field_map({}),
|
state_schema=StateSchema.from_field_map({}),
|
||||||
output_schema=_schema(),
|
output_schema=_schema(),
|
||||||
outcomes=["ok"],
|
outcomes=["submitted"],
|
||||||
start="ask",
|
start="ask",
|
||||||
nodes=[
|
nodes=[
|
||||||
InterruptNode(
|
InterruptNode(
|
||||||
@@ -147,8 +147,11 @@ async def test_interrupt_request_payload_validates_against_schema() -> None:
|
|||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
EndNode(id="end", type="end", outcome="submitted"),
|
||||||
|
],
|
||||||
|
edges=[
|
||||||
|
Edge.model_validate({"from": "ask", "outcome": "submitted", "to": "end"})
|
||||||
],
|
],
|
||||||
edges=[Edge.model_validate({"from": "ask", "outcome": "submitted", "to": END})],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
run = await execute_workflow_result_async(workflow, {}, {})
|
run = await execute_workflow_result_async(workflow, {}, {})
|
||||||
@@ -159,6 +162,55 @@ async def test_interrupt_request_payload_validates_against_schema() -> None:
|
|||||||
assert "interrupt request for ask" in run.error
|
assert "interrupt request for ask" in run.error
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_interrupt_resume_uses_persisted_pause_schema() -> None:
|
||||||
|
workflow = Workflow(
|
||||||
|
name="persisted_resume_contract",
|
||||||
|
input_schema=_schema(),
|
||||||
|
state_schema=StateSchema.from_field_map({}),
|
||||||
|
output_schema=_schema(),
|
||||||
|
outcomes=["submitted"],
|
||||||
|
start="ask",
|
||||||
|
nodes=[
|
||||||
|
InterruptNode(
|
||||||
|
id="ask",
|
||||||
|
type="interrupt",
|
||||||
|
kind="approval",
|
||||||
|
resume_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"approved": {"type": "boolean"}},
|
||||||
|
"required": ["approved"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
EndNode(id="end", type="end", outcome="submitted"),
|
||||||
|
],
|
||||||
|
edges=[
|
||||||
|
Edge.model_validate({"from": "ask", "outcome": "submitted", "to": "end"})
|
||||||
|
],
|
||||||
|
)
|
||||||
|
interrupted = await execute_workflow_async(workflow, {}, {})
|
||||||
|
changed_workflow = workflow.model_copy(deep=True)
|
||||||
|
changed_step = changed_workflow.nodes[0]
|
||||||
|
assert isinstance(changed_step, InterruptNode)
|
||||||
|
changed_step.resume_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"approved": {"type": "string"}},
|
||||||
|
"required": ["approved"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
resumed = await resume_workflow_result_async(
|
||||||
|
changed_workflow,
|
||||||
|
interrupted,
|
||||||
|
{},
|
||||||
|
resume_payload={"approved": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resumed.status == RunStatus.COMPLETED
|
||||||
|
assert resumed.outcome == "submitted"
|
||||||
|
|
||||||
|
|
||||||
async def test_interrupt_resume_payload_validates_before_state_mutation() -> None:
|
async def test_interrupt_resume_payload_validates_before_state_mutation() -> None:
|
||||||
workflow = Workflow(
|
workflow = Workflow(
|
||||||
name="resume_validation",
|
name="resume_validation",
|
||||||
|
|||||||
@@ -38,6 +38,18 @@ def test_project_map_links_big_doc() -> None:
|
|||||||
assert any(link.endswith("evidence-index.md") for link in links)
|
assert any(link.endswith("evidence-index.md") for link in links)
|
||||||
|
|
||||||
|
|
||||||
|
def test_docs_index_points_to_live_thesis_and_archives_scaffolds() -> None:
|
||||||
|
docs = ROOT / "docs"
|
||||||
|
index = (docs / "README.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "thesis/system-design-implementation.md" in index
|
||||||
|
assert "thesis/evidence-index.md" in index
|
||||||
|
assert not (docs / "thesis" / "thesis-outline.md").exists()
|
||||||
|
assert not (docs / "thesis" / "diagrams.md").exists()
|
||||||
|
assert (docs / "historical" / "thesis" / "thesis-outline.md").is_file()
|
||||||
|
assert (docs / "historical" / "thesis" / "diagrams.md").is_file()
|
||||||
|
|
||||||
|
|
||||||
def test_big_doc_keeps_mcp_as_source_family() -> None:
|
def test_big_doc_keeps_mcp_as_source_family() -> None:
|
||||||
doc = (ROOT / "docs" / "thesis" / "system-design-implementation.md").read_text(
|
doc = (ROOT / "docs" / "thesis" / "system-design-implementation.md").read_text(
|
||||||
encoding="utf-8"
|
encoding="utf-8"
|
||||||
@@ -92,6 +104,9 @@ def test_thesis_bundle_has_reproducible_agent_evaluation_assets() -> None:
|
|||||||
< generate_script.index("--filter=pandoc-crossref")
|
< generate_script.index("--filter=pandoc-crossref")
|
||||||
)
|
)
|
||||||
assert "generate_agent_challenge_evaluation.py" in combined_build_script
|
assert "generate_agent_challenge_evaluation.py" in combined_build_script
|
||||||
|
assert "Test-RenderNeedsAgentResults" in generate_script
|
||||||
|
assert "include-agent-challenge-results" in combined_build_script
|
||||||
|
assert "if ($needsAgentResults)" in combined_build_script
|
||||||
assert "--resource-path" in combined_build_script
|
assert "--resource-path" in combined_build_script
|
||||||
for stem in figure_stems:
|
for stem in figure_stems:
|
||||||
assert f"figures/{stem}.svg" in results
|
assert f"figures/{stem}.svg" in results
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
from dataclasses import replace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
COHORT_PATH = ROOT / "docs" / "thesis" / "agent-challenge-cohort.json"
|
COHORT_PATH = ROOT / "docs" / "thesis" / "agent-challenge-cohort.json"
|
||||||
|
|
||||||
@@ -60,6 +63,80 @@ def test_primary_cohort_snapshot_loads_without_local_report_files(
|
|||||||
assert len(cohort.trials) == 36
|
assert len(cohort.trials) == 36
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("field", "value", "message"),
|
||||||
|
[
|
||||||
|
("profile", "debug", "unsupported evaluation profile"),
|
||||||
|
("task_outcome", "unknown", "unsupported task outcome"),
|
||||||
|
("audit_notes", ["not", "text"], "audit_notes must be a string"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_evaluation_cohort_rejects_unknown_or_mistyped_run_values(
|
||||||
|
tmp_path: Path, field: str, value: object, message: str
|
||||||
|
) -> None:
|
||||||
|
from examples.agent_challenges.evaluation import load_evaluation_cohort
|
||||||
|
|
||||||
|
manifest = json.loads(COHORT_PATH.read_text(encoding="utf-8"))
|
||||||
|
manifest["runs"][0][field] = value
|
||||||
|
snapshot = tmp_path / "agent-challenge-cohort.json"
|
||||||
|
snapshot.write_text(json.dumps(manifest), encoding="utf-8")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match=message):
|
||||||
|
load_evaluation_cohort(snapshot, repository_root=tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_evaluation_figures_reject_unknown_direct_trial_values() -> None:
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
from examples.agent_challenges.evaluation import load_evaluation_cohort
|
||||||
|
from examples.agent_challenges.evaluation_figures import (
|
||||||
|
_automatic_vs_manual,
|
||||||
|
_scatter_metric,
|
||||||
|
)
|
||||||
|
|
||||||
|
cohort = load_evaluation_cohort(COHORT_PATH, repository_root=ROOT)
|
||||||
|
figure, axis = plt.subplots()
|
||||||
|
try:
|
||||||
|
with pytest.raises(ValueError, match="unsupported evaluation model"):
|
||||||
|
_scatter_metric(
|
||||||
|
axis,
|
||||||
|
[replace(cohort.trials[0], model="unknown-model")],
|
||||||
|
metric="duration",
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="unsupported evaluation profile"):
|
||||||
|
_scatter_metric(
|
||||||
|
axis,
|
||||||
|
[replace(cohort.trials[0], profile="unknown-profile")],
|
||||||
|
metric="duration",
|
||||||
|
)
|
||||||
|
invalid_cohort = replace(
|
||||||
|
cohort,
|
||||||
|
trials=(replace(cohort.trials[0], task_outcome="unknown"),),
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="unsupported task outcomes"):
|
||||||
|
_automatic_vs_manual(invalid_cohort, plt)
|
||||||
|
finally:
|
||||||
|
plt.close(figure)
|
||||||
|
|
||||||
|
|
||||||
|
def test_evaluation_generator_prints_paths_outside_repository(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
capsys: pytest.CaptureFixture[str],
|
||||||
|
) -> None:
|
||||||
|
from docs.thesis import generate_agent_challenge_evaluation as generator
|
||||||
|
|
||||||
|
generated = tmp_path / "agent-challenge-results.md"
|
||||||
|
monkeypatch.setattr(
|
||||||
|
generator,
|
||||||
|
"generate",
|
||||||
|
lambda **_kwargs: (generated,),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert generator.main(["--output-dir", str(tmp_path)]) == 0
|
||||||
|
assert capsys.readouterr().out.strip() == "agent-challenge-results.md"
|
||||||
|
|
||||||
|
|
||||||
def test_evaluation_renderer_writes_stable_svg_and_pdf_names(tmp_path: Path) -> None:
|
def test_evaluation_renderer_writes_stable_svg_and_pdf_names(tmp_path: Path) -> None:
|
||||||
from examples.agent_challenges.evaluation import (
|
from examples.agent_challenges.evaluation import (
|
||||||
FIGURE_STEMS,
|
FIGURE_STEMS,
|
||||||
|
|||||||
@@ -129,6 +129,13 @@ def test_challenge_prompt_is_identical_across_profiles(tmp_path: Path) -> None:
|
|||||||
assert "genuinely blocked" in rendered[InstructionProfile.DEBUG].text
|
assert "genuinely blocked" in rendered[InstructionProfile.DEBUG].text
|
||||||
assert "debug profile only" in rendered[InstructionProfile.DEBUG].text
|
assert "debug profile only" in rendered[InstructionProfile.DEBUG].text
|
||||||
assert "ux_issues_found" in rendered[InstructionProfile.DEBUG].text
|
assert "ux_issues_found" in rendered[InstructionProfile.DEBUG].text
|
||||||
|
for profile in (
|
||||||
|
InstructionProfile.NONE,
|
||||||
|
InstructionProfile.SKILLS,
|
||||||
|
InstructionProfile.ALL,
|
||||||
|
):
|
||||||
|
assert "debug profile only" not in rendered[profile].text
|
||||||
|
assert "ux_issues_found" not in rendered[profile].text
|
||||||
none_prompt = rendered[InstructionProfile.NONE].text.replace("\n", " ")
|
none_prompt = rendered[InstructionProfile.NONE].text.replace("\n", " ")
|
||||||
assert "inline in your" in none_prompt
|
assert "inline in your" in none_prompt
|
||||||
assert "without an inline self-report is invalid" in none_prompt
|
assert "without an inline self-report is invalid" in none_prompt
|
||||||
|
|||||||
@@ -616,3 +616,20 @@ def test_trial_report_renders_opencode_resume_metadata(tmp_path: Path) -> None:
|
|||||||
assert "## OpenCode Resume" in rendered
|
assert "## OpenCode Resume" in rendered
|
||||||
assert "ses_report" in rendered
|
assert "ses_report" in rendered
|
||||||
assert "opencode run --session ses_report" in rendered
|
assert "opencode run --session ses_report" in rendered
|
||||||
|
|
||||||
|
|
||||||
|
def test_trial_report_skips_resume_section_without_session_id(tmp_path: Path) -> None:
|
||||||
|
from examples.agent_challenges.report_models import build_trial_report
|
||||||
|
from examples.agent_challenges.reports import render_trial_report_markdown
|
||||||
|
|
||||||
|
result = _raw_result(tmp_path)
|
||||||
|
result["opencode"] = {
|
||||||
|
"model": "opencode/deepseek-v4-flash-free",
|
||||||
|
"variant": "max",
|
||||||
|
"session_id": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
report = build_trial_report(result, audit=None)
|
||||||
|
rendered = render_trial_report_markdown(report)
|
||||||
|
|
||||||
|
assert "## OpenCode Resume" not in rendered
|
||||||
|
|||||||
@@ -183,6 +183,25 @@ def test_resume_command_prompt_mode_overrides_stored_command() -> None:
|
|||||||
assert any("Continue this same trial" in part for part in command)
|
assert any("Continue this same trial" in part for part in command)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resume_command_never_executes_persisted_argv() -> None:
|
||||||
|
command = resume_command_from_result(
|
||||||
|
{
|
||||||
|
"task_outcome": "timeout",
|
||||||
|
"stdout": "",
|
||||||
|
"opencode": {
|
||||||
|
"model": "opencode/mimo-v2.5-free",
|
||||||
|
"variant": "high",
|
||||||
|
"session_id": "ses_metadata",
|
||||||
|
"resume_command": ["powershell", "-Command", "Write-Host tampered"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert command[0:4] == ["opencode", "run", "--session", "ses_metadata"]
|
||||||
|
assert "powershell" not in command
|
||||||
|
assert "Write-Host tampered" not in command
|
||||||
|
|
||||||
|
|
||||||
def test_resume_result_path_uses_next_resume_index(tmp_path: Path) -> None:
|
def test_resume_result_path_uses_next_resume_index(tmp_path: Path) -> None:
|
||||||
original = tmp_path / "trial.json"
|
original = tmp_path / "trial.json"
|
||||||
original.write_text("{}", encoding="utf-8")
|
original.write_text("{}", encoding="utf-8")
|
||||||
@@ -232,6 +251,20 @@ def test_resume_trial_prints_resume_command(
|
|||||||
assert "opencode run --session ses_cli" in output
|
assert "opencode run --session ses_cli" in output
|
||||||
|
|
||||||
|
|
||||||
|
def test_resume_trial_reports_missing_result_as_cli_error(
|
||||||
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
from examples.agent_challenges.resume_trial import main
|
||||||
|
|
||||||
|
missing = tmp_path / "missing.json"
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as exc_info:
|
||||||
|
main(["--from-result", str(missing), "--print-command"])
|
||||||
|
|
||||||
|
assert exc_info.value.code == 2
|
||||||
|
assert "missing.json" in capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
def test_resume_trial_prints_command_for_old_raw_result(
|
def test_resume_trial_prints_command_for_old_raw_result(
|
||||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ def test_lda_report_workflow_builder_generates_committed_raw_plan() -> None:
|
|||||||
|
|
||||||
assert workflow.name == "lda_report_case_study"
|
assert workflow.name == "lda_report_case_study"
|
||||||
assert validated.name == "lda_report_case_study"
|
assert validated.name == "lda_report_case_study"
|
||||||
|
assert validated.start == "reset_board"
|
||||||
assert any(node.id == "review_issues" for node in validated.nodes)
|
assert any(node.id == "review_issues" for node in validated.nodes)
|
||||||
assert payload == committed
|
assert payload == committed
|
||||||
|
|
||||||
@@ -219,7 +220,20 @@ async def test_lda_report_workflow_artifact_interrupt_resume_path(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
run_input = json.loads((EXAMPLE_DIR / "run-input.json").read_text(encoding="utf-8"))
|
run_input = json.loads((EXAMPLE_DIR / "run-input.json").read_text(encoding="utf-8"))
|
||||||
run_input["board_path"] = str(tmp_path / "issue-board.json")
|
board_path = tmp_path / "issue-board.json"
|
||||||
|
board_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "ISSUE-099",
|
||||||
|
"title": "Stale demo issue",
|
||||||
|
"url": "local://issue-board/ISSUE-099",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
run_input["board_path"] = str(board_path)
|
||||||
started = await server.api.run_deployment(
|
started = await server.api.run_deployment(
|
||||||
deployment_id="lda_report_case_study.default",
|
deployment_id="lda_report_case_study.default",
|
||||||
workflow_input=run_input,
|
workflow_input=run_input,
|
||||||
@@ -228,14 +242,14 @@ async def test_lda_report_workflow_artifact_interrupt_resume_path(
|
|||||||
assert started["status"] == "interrupted"
|
assert started["status"] == "interrupted"
|
||||||
assert started["interrupt"]["kind"] == "issue_review"
|
assert started["interrupt"]["kind"] == "issue_review"
|
||||||
assert started["interrupt"]["typed"] is True
|
assert started["interrupt"]["typed"] is True
|
||||||
assert started["interrupt"]["request_schema"]["required"] == [
|
assert set(started["interrupt"]["request_schema"]["required"]) == {
|
||||||
"report_markdown",
|
"report_markdown",
|
||||||
"proposed_issues",
|
"proposed_issues",
|
||||||
]
|
}
|
||||||
assert started["interrupt"]["resume_schema"]["required"] == [
|
assert set(started["interrupt"]["resume_schema"]["required"]) == {
|
||||||
"approved",
|
"approved",
|
||||||
"selected_issue_ids",
|
"selected_issue_ids",
|
||||||
]
|
}
|
||||||
proposed_ids = [
|
proposed_ids = [
|
||||||
issue["id"] for issue in started["interrupt"]["payload"]["proposed_issues"]
|
issue["id"] for issue in started["interrupt"]["payload"]["proposed_issues"]
|
||||||
]
|
]
|
||||||
@@ -255,6 +269,7 @@ async def test_lda_report_workflow_artifact_interrupt_resume_path(
|
|||||||
assert resumed["outcome"] == "completed"
|
assert resumed["outcome"] == "completed"
|
||||||
assert resumed["output"]["approved"] is True
|
assert resumed["output"]["approved"] is True
|
||||||
assert resumed["output"]["created_issues"]
|
assert resumed["output"]["created_issues"]
|
||||||
|
assert resumed["output"]["created_issues"][0]["id"] == "ISSUE-001"
|
||||||
assert resumed["output"]["markdown"].startswith(
|
assert resumed["output"]["markdown"].startswith(
|
||||||
"# lda.chat Thesis And Project Readiness Report"
|
"# lda.chat Thesis And Project Readiness Report"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ async def test_workflow_tools_have_human_metadata(tmp_path: Path) -> None:
|
|||||||
assert "Debug traces" in read_trace_schema.get("description", "")
|
assert "Debug traces" in read_trace_schema.get("description", "")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
async def test_create_artifact_from_plan_exposes_plan_as_plain_object(
|
async def test_create_artifact_from_plan_exposes_plan_as_plain_object(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -173,6 +174,7 @@ async def test_create_artifact_from_plan_exposes_plan_as_plain_object(
|
|||||||
assert plan_schema.get("additionalProperties") is True
|
assert plan_schema.get("additionalProperties") is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
async def test_draft_tools_expose_plain_object_and_patch_array_schemas(
|
async def test_draft_tools_expose_plain_object_and_patch_array_schemas(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -203,6 +205,7 @@ async def test_draft_tools_expose_plain_object_and_patch_array_schemas(
|
|||||||
assert "$defs" not in patch_patch_schema
|
assert "$defs" not in patch_patch_schema
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
async def test_admin_tools_have_human_metadata(tmp_path: Path) -> None:
|
async def test_admin_tools_have_human_metadata(tmp_path: Path) -> None:
|
||||||
config = BrokerConfig(
|
config = BrokerConfig(
|
||||||
store_root=tmp_path / "unified_admin_metadata_store",
|
store_root=tmp_path / "unified_admin_metadata_store",
|
||||||
|
|||||||
Reference in New Issue
Block a user