Skip to content

Commit 8a0eced

Browse files
committed
2026-03-10-zerotoken-script-engine-session-design
1 parent b383c6a commit 8a0eced

8 files changed

Lines changed: 987 additions & 42 deletions

mcp_server.py

Lines changed: 110 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -312,14 +312,57 @@ async def list_tools() -> list[Tool]:
312312
),
313313
Tool(
314314
name="run_script",
315-
description="Run a script by task_id without LLM (deterministic replay). Writes session to DB.",
315+
description="Run a script (start) or resume a paused session (deterministic replay). Writes session to DB.",
316316
inputSchema={
317317
"type": "object",
318318
"properties": {
319-
"task_id": {"type": "string", "description": "Task ID of the script"},
320-
"vars": {"type": "object", "description": "Optional map of {{varname}} replacements"}
319+
"task_id": {"type": "string", "description": "Start mode: task_id of the script (mutually exclusive with session_id)"},
320+
"vars": {"type": "object", "description": "Start mode: optional map of {{varname}} replacements"},
321+
"session_id": {"type": "string", "description": "Resume mode: session_id to resume (mutually exclusive with task_id)"},
322+
"resolution": {"type": "object", "description": "Resume mode: resolution object {type, note?, patch?}"}
323+
}
324+
}
325+
),
326+
Tool(
327+
name="dfu_save",
328+
description="Save a DFU (Dynamic Fuzzy Unit) to the MCP database. Overwrites if dfu_id exists.",
329+
inputSchema={
330+
"type": "object",
331+
"properties": {
332+
"dfu_id": {"type": "string", "description": "DFU ID (key)"},
333+
"name": {"type": "string", "description": "DFU name"},
334+
"description": {"type": "string", "description": "Optional description", "default": ""},
335+
"triggers": {"type": "array", "description": "Trigger rules (declarative JSON), exact match only"},
336+
"prompt": {"type": "string", "description": "Prompt shown to orchestrator", "default": ""},
337+
"allowed_resolutions": {"type": "array", "description": "Allowed resolution types", "items": {"type": "string"}}
321338
},
322-
"required": ["task_id"]
339+
"required": ["dfu_id", "name", "triggers"]
340+
}
341+
),
342+
Tool(
343+
name="dfu_list",
344+
description="List DFUs from the MCP database.",
345+
inputSchema={
346+
"type": "object",
347+
"properties": {"limit": {"type": "integer", "default": 100}}
348+
}
349+
),
350+
Tool(
351+
name="dfu_load",
352+
description="Load a DFU by dfu_id from the MCP database.",
353+
inputSchema={
354+
"type": "object",
355+
"properties": {"dfu_id": {"type": "string"}},
356+
"required": ["dfu_id"]
357+
}
358+
),
359+
Tool(
360+
name="dfu_delete",
361+
description="Delete a DFU by dfu_id from the MCP database.",
362+
inputSchema={
363+
"type": "object",
364+
"properties": {"dfu_id": {"type": "string"}},
365+
"required": ["dfu_id"]
323366
}
324367
),
325368
Tool(
@@ -683,16 +726,71 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
683726

684727
elif name == "run_script":
685728
task_id = arguments.get("task_id")
686-
vars_map = arguments.get("vars") or {}
687-
if not task_id:
688-
return [TextContent(type="text", text=_error_response("task_id is required", code="INVALID_PARAMS", retryable=False))]
689-
script = get_storage().script_load(task_id)
690-
if script is None:
691-
return [TextContent(type="text", text=_error_response(f"No script for task_id: {task_id}", code="SCRIPT_NOT_FOUND", retryable=False))]
692-
engine = ScriptEngine(vars_map=vars_map)
693-
result = await engine.run_script(script, controller, get_storage())
729+
session_id = arguments.get("session_id")
730+
if bool(task_id) == bool(session_id):
731+
return [
732+
TextContent(
733+
type="text",
734+
text=_error_response(
735+
"Provide exactly one of task_id or session_id",
736+
code="INVALID_PARAMS",
737+
retryable=False,
738+
),
739+
)
740+
]
741+
storage = get_storage()
742+
if task_id:
743+
vars_map = arguments.get("vars") or {}
744+
script = storage.script_load(task_id)
745+
if script is None:
746+
return [TextContent(type="text", text=_error_response(f"No script for task_id: {task_id}", code="SCRIPT_NOT_FOUND", retryable=False))]
747+
engine = ScriptEngine(vars_map=vars_map)
748+
result = await engine.run_script_start(script, controller, storage)
749+
return [TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
750+
resolution = arguments.get("resolution")
751+
if resolution is None:
752+
return [TextContent(type="text", text=_error_response("resolution is required for resume", code="INVALID_PARAMS", retryable=False))]
753+
engine = ScriptEngine(vars_map={})
754+
result = await engine.run_script_resume(session_id, resolution, controller, storage)
694755
return [TextContent(type="text", text=json.dumps(result, indent=2, ensure_ascii=False))]
695756

757+
elif name == "dfu_save":
758+
dfu_id = arguments.get("dfu_id")
759+
name_ = arguments.get("name")
760+
triggers = arguments.get("triggers")
761+
if not dfu_id or not name_ or triggers is None:
762+
return [TextContent(type="text", text=_error_response("dfu_id, name, triggers are required", code="INVALID_PARAMS", retryable=False))]
763+
get_storage().dfu_save(
764+
dfu_id,
765+
name=name_,
766+
description=arguments.get("description", "") or "",
767+
triggers=triggers,
768+
prompt=arguments.get("prompt", "") or "",
769+
allowed_resolutions=arguments.get("allowed_resolutions") or [],
770+
)
771+
return [TextContent(type="text", text=json.dumps({"success": True, "dfu_id": dfu_id}, indent=2, ensure_ascii=False))]
772+
773+
elif name == "dfu_list":
774+
limit = arguments.get("limit", 100)
775+
items = get_storage().dfu_list(limit=limit)
776+
return [TextContent(type="text", text=json.dumps({"dfus": items}, indent=2, ensure_ascii=False))]
777+
778+
elif name == "dfu_load":
779+
dfu_id = arguments.get("dfu_id")
780+
if not dfu_id:
781+
return [TextContent(type="text", text=_error_response("dfu_id is required", code="INVALID_PARAMS", retryable=False))]
782+
dfu = get_storage().dfu_load(dfu_id)
783+
if dfu is None:
784+
return [TextContent(type="text", text=_error_response(f"No dfu for dfu_id: {dfu_id}", code="DFU_NOT_FOUND", retryable=False))]
785+
return [TextContent(type="text", text=json.dumps({"success": True, "dfu": dfu}, indent=2, ensure_ascii=False))]
786+
787+
elif name == "dfu_delete":
788+
dfu_id = arguments.get("dfu_id")
789+
if not dfu_id:
790+
return [TextContent(type="text", text=_error_response("dfu_id is required", code="INVALID_PARAMS", retryable=False))]
791+
ok = get_storage().dfu_delete(dfu_id)
792+
return [TextContent(type="text", text=json.dumps({"success": True, "deleted": ok}, indent=2, ensure_ascii=False))]
793+
696794
elif name == "session_list":
697795
limit = arguments.get("limit", 100)
698796
items = get_storage().session_list(limit=limit)

tests/test_dfu_storage.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Tests for DFUStore and SessionRuntimeStore in SQLiteStorage."""
2+
3+
import os
4+
import tempfile
5+
6+
import pytest
7+
8+
from zerotoken.storage_sqlite import SQLiteStorage
9+
10+
11+
@pytest.fixture
12+
def db_path():
13+
fd, path = tempfile.mkstemp(suffix=".db")
14+
os.close(fd)
15+
yield path
16+
try:
17+
os.unlink(path)
18+
except OSError:
19+
pass
20+
21+
22+
@pytest.fixture
23+
def storage(db_path):
24+
return SQLiteStorage(db_path)
25+
26+
27+
def test_dfu_save_load_list_delete(storage: SQLiteStorage):
28+
storage.dfu_save(
29+
"captcha_v1",
30+
name="Captcha handler",
31+
description="Pause on captcha step",
32+
triggers=[{"action_is": "browser_click", "selector_is": "#captcha"}],
33+
prompt="Need human to solve captcha",
34+
allowed_resolutions=["human_done", "abort"],
35+
)
36+
37+
items = storage.dfu_list()
38+
assert any(it["dfu_id"] == "captcha_v1" for it in items)
39+
40+
loaded = storage.dfu_load("captcha_v1")
41+
assert loaded is not None
42+
assert loaded["dfu_id"] == "captcha_v1"
43+
assert loaded["name"] == "Captcha handler"
44+
assert loaded["prompt"] == "Need human to solve captcha"
45+
assert loaded["triggers"][0]["selector_is"] == "#captcha"
46+
assert "updated_at" in loaded
47+
48+
assert storage.dfu_delete("captcha_v1") is True
49+
assert storage.dfu_load("captcha_v1") is None
50+
51+
52+
def test_runtime_init_get_update(storage: SQLiteStorage):
53+
session_id = "sess_rt_1"
54+
storage.runtime_init(session_id, task_id="task_x", cursor_step_index=0, status="running", pause_event=None)
55+
56+
rt = storage.runtime_get(session_id)
57+
assert rt is not None
58+
assert rt["session_id"] == session_id
59+
assert rt["cursor_step_index"] == 0
60+
assert rt["status"] == "running"
61+
assert rt["pause_event"] is None
62+
63+
pause_event = {"kind": "dfu_pause", "step_index": 1}
64+
storage.runtime_update(session_id, cursor_step_index=1, status="paused", pause_event=pause_event)
65+
66+
rt2 = storage.runtime_get(session_id)
67+
assert rt2 is not None
68+
assert rt2["cursor_step_index"] == 1
69+
assert rt2["status"] == "paused"
70+
assert rt2["pause_event"]["kind"] == "dfu_pause"
71+

0 commit comments

Comments
 (0)