skillflow-py 1.4.2__tar.gz → 1.4.3__tar.gz
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.
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/PKG-INFO +1 -1
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/pyproject.toml +1 -1
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/core.py +42 -23
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow_py.egg-info/PKG-INFO +1 -1
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_core.py +51 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_recovery.py +67 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/LICENSE +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/README.md +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/setup.cfg +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/__init__.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/agent_registry.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/context.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/convert_cli.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/exceptions.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/graph.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/notifications.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/outbox.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/linter/__init__.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/linter/cli.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/linter/tools/skillflow_lint/impl.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/linter/tools/skillflow_lint/tool.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/skill_converter/AGENT.md +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/skill_converter/__init__.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/skill_converter/converter.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/skill_converter/prompts/analyze_skill.md +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/skill_converter/prompts/design_graph.md +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/skill_converter/prompts/explain_design.md +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/skill_converter/prompts/fix_issues.md +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/skill_converter/skill_converter.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/skill_runner/AGENT.md +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/skill_runner/__init__.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/skill_runner/runner.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/read_tools.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/recovery.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/run_cli.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/schema.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/step_validation.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tool_loader.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/__init__.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/dir_tree/impl.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/dir_tree/tool.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/draft_commit/impl.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/draft_commit/tool.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/file_exists/impl.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/file_exists/tool.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/git_sync_pre/impl.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/git_sync_pre/tool.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/json_schema/impl.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/json_schema/tool.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/lint/impl.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/lint/tool.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/list_tree/impl.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/list_tree/tool.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/notify/impl.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/notify/tool.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/pytest/impl.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/pytest/tool.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/read_file/impl.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/read_file/tool.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/repo_apply/impl.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/repo_apply/tool.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/repo_validate/impl.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/repo_validate/tool.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/write/impl.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/tools/write/tool.yaml +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/validation.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/workspace.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/write_tools.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow_py.egg-info/SOURCES.txt +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow_py.egg-info/dependency_links.txt +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow_py.egg-info/entry_points.txt +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow_py.egg-info/requires.txt +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow_py.egg-info/top_level.txt +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_context.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_exceptions.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_existing_repo_apply.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_git_sync_pre.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_graph.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_integration.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_integration_configs.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_migration_regression.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_noop_floor_fix.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_notifications.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_outbox.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_read_tools_exec.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_read_tools_label.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_regression.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_step_validation.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_tool_loader.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_tool_lock_contention.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_tools.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_validation.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_workspace.py +0 -0
- {skillflow_py-1.4.2 → skillflow_py-1.4.3}/tests/test_write_tools.py +0 -0
|
@@ -1286,29 +1286,44 @@ class SkillFlow:
|
|
|
1286
1286
|
hook_name: str, items: list) -> dict:
|
|
1287
1287
|
"""Run a list of tool hooks in order (e.g. repo_apply then repo_delete).
|
|
1288
1288
|
|
|
1289
|
-
Each item is a ``{'tool', 'params', 'on_failure'?}`` dict
|
|
1290
|
-
:meth:`_execute_tool_hook` (full variable resolution +
|
|
1291
|
-
injection). Per-item
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1289
|
+
Each item is a ``{'tool', 'params', 'on_failure'?, 'max_retries'?}`` dict
|
|
1290
|
+
executed via :meth:`_execute_tool_hook` (full variable resolution +
|
|
1291
|
+
project_root injection). Per-item policy is honored IN PLACE:
|
|
1292
|
+
``on_failure: retry`` re-runs THAT item up to its own ``max_retries``
|
|
1293
|
+
(without re-running earlier, already-succeeded items or the agent step);
|
|
1294
|
+
``warn``/``skip`` log and continue to the next item; ``fail`` (or a retry
|
|
1295
|
+
item that exhausts its retries) stops the sequence and fails the step.
|
|
1296
|
+
The sequence never bubbles ``retry`` to the caller — that would reset the
|
|
1297
|
+
whole agent step and re-execute earlier, already-committed items.
|
|
1295
1298
|
"""
|
|
1296
1299
|
detail_files = None
|
|
1297
1300
|
for item in items:
|
|
1298
1301
|
if not (isinstance(item, dict) and "tool" in item):
|
|
1299
1302
|
return {"passed": False,
|
|
1300
1303
|
"error": f"Invalid tool hook in '{hook_name}': {item!r}"}
|
|
1301
|
-
|
|
1304
|
+
on_failure = item.get("on_failure", "fail")
|
|
1305
|
+
max_retries = int(item.get("max_retries", 0) or 0)
|
|
1306
|
+
res = {"passed": False}
|
|
1307
|
+
for attempt in range(max_retries + 1):
|
|
1308
|
+
res = self._execute_tool_hook(token, node, hook_name, item)
|
|
1309
|
+
if res.get("passed", True):
|
|
1310
|
+
break
|
|
1311
|
+
if on_failure == "retry" and attempt < max_retries:
|
|
1312
|
+
# Retry THIS item in place — do not re-run earlier items.
|
|
1313
|
+
self._emit_lifecycle_event(
|
|
1314
|
+
token, hook_name, "retry", res.get("error", ""))
|
|
1315
|
+
continue
|
|
1316
|
+
break
|
|
1302
1317
|
if not res.get("passed", True):
|
|
1303
|
-
on_failure = item.get("on_failure", "fail")
|
|
1304
1318
|
if on_failure in ("warn", "skip"):
|
|
1305
1319
|
self._emit_lifecycle_event(
|
|
1306
1320
|
token, hook_name,
|
|
1307
1321
|
"warned" if on_failure == "warn" else "skipped",
|
|
1308
1322
|
res.get("error", ""))
|
|
1309
1323
|
continue
|
|
1310
|
-
#
|
|
1311
|
-
|
|
1324
|
+
# 'fail', or a 'retry' item that exhausted max_retries → fail the
|
|
1325
|
+
# step (never bubble 'retry', which re-runs the whole sequence).
|
|
1326
|
+
return {**res, "on_failure": "fail"}
|
|
1312
1327
|
files = res.get("files")
|
|
1313
1328
|
if isinstance(files, list):
|
|
1314
1329
|
detail_files = files
|
|
@@ -2326,10 +2341,12 @@ class SkillFlow:
|
|
|
2326
2341
|
so it retries promptly instead of stalling until its claim times out.
|
|
2327
2342
|
|
|
2328
2343
|
Capped at 3 crashes: a tool that crashes deterministically is broken,
|
|
2329
|
-
not transient
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2344
|
+
not transient. On the 3rd crash the RUN is failed (not just the step) —
|
|
2345
|
+
marking only the step failed + current_node=NULL would let advance_run
|
|
2346
|
+
re-resolve back to the tool and open a FRESH instance with a reset
|
|
2347
|
+
counter, so the crash loop never actually stopped (only the host's
|
|
2348
|
+
step-count valve caught it). The SF-20 stale-recovery cap does NOT cover
|
|
2349
|
+
this path (a crash reopens directly, never via recover_stale_claims).
|
|
2333
2350
|
"""
|
|
2334
2351
|
with self._tx() as conn:
|
|
2335
2352
|
row = conn.execute(
|
|
@@ -2353,11 +2370,9 @@ class SkillFlow:
|
|
|
2353
2370
|
"claimed_by = NULL, updated_at = datetime('now') WHERE id = ?",
|
|
2354
2371
|
(error_msg, row["id"]),
|
|
2355
2372
|
)
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
(run_id,),
|
|
2360
|
-
)
|
|
2373
|
+
# Fail the RUN so advance_run returns None instead of re-resolving
|
|
2374
|
+
# the predecessor's transition and opening a fresh tool instance.
|
|
2375
|
+
self._fail_run_in_tx(conn, run_id, error_msg)
|
|
2361
2376
|
self.notifications.publish_sync(
|
|
2362
2377
|
"step_failed",
|
|
2363
2378
|
{"run_id": run_id, "step_id": step_id,
|
|
@@ -2984,10 +2999,14 @@ class SkillFlow:
|
|
|
2984
2999
|
try:
|
|
2985
3000
|
node = self._get_resolver_for_run(
|
|
2986
3001
|
row["run_id"]).get_node(row["step_id"])
|
|
2987
|
-
if
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
3002
|
+
if node and node.step_type == "tool":
|
|
3003
|
+
if node.timeout_seconds == 0:
|
|
3004
|
+
# 0 = "no timeout": a live tool may run arbitrarily
|
|
3005
|
+
# long, so it is NEVER stale — reclaiming it would
|
|
3006
|
+
# relaunch it concurrently with itself (the rampage).
|
|
3007
|
+
continue
|
|
3008
|
+
if node.timeout_seconds > window:
|
|
3009
|
+
window = float(node.timeout_seconds)
|
|
2991
3010
|
except Exception:
|
|
2992
3011
|
pass
|
|
2993
3012
|
threshold = time.strftime(
|
|
@@ -1247,6 +1247,57 @@ def test_on_deliver_list_order_and_on_failure(sf: SkillFlow, tmp_path: Path,
|
|
|
1247
1247
|
assert calls == []
|
|
1248
1248
|
|
|
1249
1249
|
|
|
1250
|
+
def test_on_deliver_item_retries_in_place(sf: SkillFlow, tmp_path: Path,
|
|
1251
|
+
monkeypatch):
|
|
1252
|
+
"""A failing on_deliver item with on_failure=retry re-runs THAT item up to
|
|
1253
|
+
its own max_retries IN PLACE; earlier items are not re-run, and the sequence
|
|
1254
|
+
never bubbles 'retry' to the caller (which would reset the whole step)."""
|
|
1255
|
+
sf._tool_loader = ToolLoader(
|
|
1256
|
+
Path(__file__).parent.parent / "src" / "skillflow" / "tools")
|
|
1257
|
+
sf._workspace = WorkspaceManager(str(tmp_path / "ws"),
|
|
1258
|
+
projects_base=str(tmp_path / "proj"))
|
|
1259
|
+
node = StepNode(id="s1", step_type="agent", output_mode="content",
|
|
1260
|
+
output_fixed={"out": "r.md"}, transitions=[Transition(to=None)])
|
|
1261
|
+
sf.register_graph(PipelineGraph(name="seq", begin="s1", steps=[node]))
|
|
1262
|
+
rid = sf.create_run("seq", {"project_id": "pid"})
|
|
1263
|
+
sf.start_run(rid)
|
|
1264
|
+
sf.advance_run(rid)
|
|
1265
|
+
token = sf.claim_next_step(rid).token
|
|
1266
|
+
|
|
1267
|
+
calls: list = []
|
|
1268
|
+
|
|
1269
|
+
# 'flaky' fails its first 2 attempts, succeeds on the 3rd.
|
|
1270
|
+
def fake(tok, nd, hook_name, spec):
|
|
1271
|
+
calls.append(spec["tool"])
|
|
1272
|
+
if spec["tool"] == "flaky":
|
|
1273
|
+
return {"passed": calls.count("flaky") >= 3}
|
|
1274
|
+
return {"passed": True}
|
|
1275
|
+
monkeypatch.setattr(sf, "_execute_tool_hook", fake)
|
|
1276
|
+
|
|
1277
|
+
r = sf._execute_lifecycle_hook(token, node, "on_deliver", [
|
|
1278
|
+
{"tool": "repo_apply"},
|
|
1279
|
+
{"tool": "flaky", "on_failure": "retry", "max_retries": 3},
|
|
1280
|
+
])
|
|
1281
|
+
# repo_apply ran ONCE (not re-run while flaky retried); flaky retried in place.
|
|
1282
|
+
assert calls == ["repo_apply", "flaky", "flaky", "flaky"]
|
|
1283
|
+
assert r["passed"] is True
|
|
1284
|
+
|
|
1285
|
+
# Exhausted retries → fail the STEP, never bubble 'retry'.
|
|
1286
|
+
calls.clear()
|
|
1287
|
+
|
|
1288
|
+
def always_fail(tok, nd, hook_name, spec):
|
|
1289
|
+
calls.append(spec["tool"])
|
|
1290
|
+
return {"passed": spec["tool"] != "boom"}
|
|
1291
|
+
monkeypatch.setattr(sf, "_execute_tool_hook", always_fail)
|
|
1292
|
+
r = sf._execute_lifecycle_hook(token, node, "on_deliver", [
|
|
1293
|
+
{"tool": "repo_apply"},
|
|
1294
|
+
{"tool": "boom", "on_failure": "retry", "max_retries": 2},
|
|
1295
|
+
])
|
|
1296
|
+
assert calls == ["repo_apply", "boom", "boom", "boom"] # 1 initial + 2 retries
|
|
1297
|
+
assert r["passed"] is False
|
|
1298
|
+
assert r.get("on_failure") == "fail" # NOT "retry" — no whole-step re-run
|
|
1299
|
+
|
|
1300
|
+
|
|
1250
1301
|
# ── Durable run trace ─────────────────────────────────────────────────
|
|
1251
1302
|
|
|
1252
1303
|
def test_trace_append_and_get(sf: SkillFlow):
|
|
@@ -1,17 +1,84 @@
|
|
|
1
1
|
"""Unit tests for recovery.py."""
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
|
|
7
8
|
from skillflow.core import SkillFlow
|
|
8
9
|
from skillflow.graph import PipelineGraph, StepNode, Transition
|
|
10
|
+
from skillflow.tool_loader import ToolLoader
|
|
11
|
+
from skillflow.workspace import WorkspaceManager
|
|
12
|
+
|
|
13
|
+
_REAL_TOOLS = Path(__file__).parent.parent / "src" / "skillflow" / "tools"
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
def _agent(id: str, transitions=None):
|
|
12
17
|
return StepNode(id=id, step_type="agent", transitions=transitions or [])
|
|
13
18
|
|
|
14
19
|
|
|
20
|
+
def test_recover_never_reclaims_tool_with_zero_timeout(sf: SkillFlow):
|
|
21
|
+
"""A tool node with timeout_seconds=0 ('no timeout') is never reclaimed,
|
|
22
|
+
however old the claim — reclaiming a live no-timeout tool would relaunch it
|
|
23
|
+
concurrently with itself (the rampage)."""
|
|
24
|
+
graph = PipelineGraph(
|
|
25
|
+
name="test", begin="a",
|
|
26
|
+
steps=[StepNode(id="a", step_type="tool", tool_name="write",
|
|
27
|
+
timeout_seconds=0, transitions=[])],
|
|
28
|
+
)
|
|
29
|
+
sf.register_graph(graph)
|
|
30
|
+
run_id = sf.create_run("test")
|
|
31
|
+
sf.start_run(run_id)
|
|
32
|
+
long_ago = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() - 99999))
|
|
33
|
+
with sf._lock:
|
|
34
|
+
sf._conn.execute(
|
|
35
|
+
"INSERT INTO skillflow_steps (run_id, step_id, step_config_json, "
|
|
36
|
+
"inputs_json, max_retries, status, claimed_at, created_at, updated_at) "
|
|
37
|
+
"VALUES (?, 'a', '{}', '{}', 3, 'claimed', ?, "
|
|
38
|
+
"datetime('now'), datetime('now'))",
|
|
39
|
+
(run_id, long_ago))
|
|
40
|
+
sf._conn.commit()
|
|
41
|
+
# Claimed 99999s ago, but timeout_seconds=0 → never stale.
|
|
42
|
+
assert sf.recover_stale_claims(stale_threshold_seconds=60) == []
|
|
43
|
+
st = sf._conn.execute(
|
|
44
|
+
"SELECT status FROM skillflow_steps WHERE run_id=? AND step_id='a' "
|
|
45
|
+
"ORDER BY id DESC LIMIT 1", (run_id,)).fetchone()
|
|
46
|
+
assert st["status"] == "claimed"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_tool_crash_fails_run_after_three(sf: SkillFlow, tmp_path,
|
|
50
|
+
monkeypatch):
|
|
51
|
+
"""A tool that crashes on EVERY execution fails the RUN after 3 crashes,
|
|
52
|
+
instead of looping forever via advance_run re-resolving into fresh instances
|
|
53
|
+
(regression: the per-instance _tool_reopen_count was reset each time
|
|
54
|
+
_claim_tool_step_in_tx opened a new instance)."""
|
|
55
|
+
sf._tool_loader = ToolLoader(_REAL_TOOLS)
|
|
56
|
+
sf._workspace = WorkspaceManager(str(tmp_path / "ws"),
|
|
57
|
+
projects_base=str(tmp_path / "proj"))
|
|
58
|
+
node = StepNode(id="boom", step_type="tool", tool_name="write",
|
|
59
|
+
transitions=[Transition(to=None)])
|
|
60
|
+
sf.register_graph(PipelineGraph(name="crash", begin="boom", steps=[node]))
|
|
61
|
+
run_id = sf.create_run("crash", {"project_id": "p"})
|
|
62
|
+
sf.start_run(run_id)
|
|
63
|
+
|
|
64
|
+
def _boom(*a, **k):
|
|
65
|
+
raise RuntimeError("boom")
|
|
66
|
+
monkeypatch.setattr(sf, "_execute_tool_inline", _boom)
|
|
67
|
+
|
|
68
|
+
for _ in range(25):
|
|
69
|
+
try:
|
|
70
|
+
sf.advance_run(run_id) # tool fast-path reopens then re-raises
|
|
71
|
+
except RuntimeError:
|
|
72
|
+
pass
|
|
73
|
+
if sf.get_run(run_id)["status"] == "failed":
|
|
74
|
+
break
|
|
75
|
+
assert sf.get_run(run_id)["status"] == "failed"
|
|
76
|
+
n = sf._conn.execute(
|
|
77
|
+
"SELECT COUNT(*) c FROM skillflow_steps WHERE run_id=? AND step_id='boom'",
|
|
78
|
+
(run_id,)).fetchone()["c"]
|
|
79
|
+
assert n == 1, f"crash should fail the run after 1 instance x3, got {n}"
|
|
80
|
+
|
|
81
|
+
|
|
15
82
|
def test_recover_respects_tool_timeout_seconds(sf: SkillFlow):
|
|
16
83
|
"""A TOOL step claimed longer than the flat threshold but WITHIN its
|
|
17
84
|
timeout_seconds is NOT reclaimed — a slow-but-alive tool (run_tests,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/linter/tools/skillflow_lint/impl.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/skill_converter/converter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{skillflow_py-1.4.2 → skillflow_py-1.4.3}/src/skillflow/plugins/skill_converter/skill_converter.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|