Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: install install-core run clean lint test test-core tui-test check docs-check shell-check benchmark wheel-smoke docker-smoke git-status git-diff git-log web
.PHONY: install install-core run clean lint test test-core tui-test check docs-check shell-check benchmark agent-acceptance wheel-smoke docker-smoke git-status git-diff git-log web

# Tests parse the benchmark target's stdout as JSON; do not inject GNU make's
# recursive directory banners into that machine-readable output.
Expand Down Expand Up @@ -64,7 +64,7 @@ clean:

# Syntax check
lint:
$(VENV_PYTHON) -m compileall -q main.py tui_launcher.py tui_backend.py main_debug.py server.py tools.py memory.py event_store.py interaction.py task_engine.py learning_engine.py learning_benchmark.py learning_worker.py migration.py scheduler.py skill_registry.py browser_manager.py instruction_loader.py agent_config.py subagents.py capability_tokens.py tool_registry.py dynamic_tools.py plugin_runtime.py workspace_context.py project_graph.py github_cli.py scripts/generate_tool_inventory.py scripts/check_docs.py scripts/check_zsh_extras.py scripts/wheel_smoke.py
$(VENV_PYTHON) -m compileall -q main.py tui_launcher.py tui_backend.py main_debug.py server.py tools.py memory.py event_store.py interaction.py task_engine.py learning_engine.py learning_benchmark.py learning_worker.py migration.py scheduler.py skill_registry.py browser_manager.py instruction_loader.py agent_config.py subagents.py capability_tokens.py tool_registry.py dynamic_tools.py plugin_runtime.py workspace_context.py project_graph.py github_cli.py scripts/generate_tool_inventory.py scripts/check_docs.py scripts/check_zsh_extras.py scripts/agent_workflow_acceptance.py scripts/wheel_smoke.py
@echo "Python syntax OK."
@echo "All files pass syntax check."

Expand All @@ -91,6 +91,9 @@ wheel-smoke:
$(VENV_PYTHON) -m pip install build -q
$(VENV_PYTHON) scripts/wheel_smoke.py

agent-acceptance:
$(VENV_PYTHON) scripts/agent_workflow_acceptance.py

docs-check:
$(VENV_PYTHON) scripts/generate_tool_inventory.py --check
$(VENV_PYTHON) scripts/check_docs.py
Expand All @@ -107,7 +110,7 @@ docker-smoke:
# Quick verification
check:
@echo "Checking Python syntax..."
@$(VENV_PYTHON) -m py_compile main.py tui_launcher.py tui_backend.py main_debug.py server.py tools.py memory.py event_store.py interaction.py task_engine.py learning_engine.py learning_benchmark.py learning_worker.py migration.py scheduler.py skill_registry.py browser_manager.py instruction_loader.py agent_config.py subagents.py capability_tokens.py tool_registry.py dynamic_tools.py plugin_runtime.py workspace_context.py project_graph.py github_cli.py scripts/generate_tool_inventory.py scripts/check_docs.py scripts/check_zsh_extras.py scripts/wheel_smoke.py
@$(VENV_PYTHON) -m py_compile main.py tui_launcher.py tui_backend.py main_debug.py server.py tools.py memory.py event_store.py interaction.py task_engine.py learning_engine.py learning_benchmark.py learning_worker.py migration.py scheduler.py skill_registry.py browser_manager.py instruction_loader.py agent_config.py subagents.py capability_tokens.py tool_registry.py dynamic_tools.py plugin_runtime.py workspace_context.py project_graph.py github_cli.py scripts/generate_tool_inventory.py scripts/check_docs.py scripts/check_zsh_extras.py scripts/agent_workflow_acceptance.py scripts/wheel_smoke.py
@echo " Python modules: OK"
@echo "Checking git tools..."
@$(VENV_PYTHON) -c "from tools import AVAILABLE_TOOLS; git = [k for k in AVAILABLE_TOOLS if k.startswith('git_')]; print(f' {len(git)} git tools, {len(AVAILABLE_TOOLS)} total tools')"
Expand Down
21 changes: 21 additions & 0 deletions docs/agentic-runtime-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,24 @@ is intentionally deferred rather than hidden behind speculative abstractions:

MCP remains intentionally non-interactive. Tool approvals remain distinct from
clarification questions on every future surface.

## Release acceptance

Run `make agent-acceptance` for the deterministic plan-to-deployment workflow.
`make wheel-smoke` repeats the same check against a newly installed wheel from
outside the checkout. It verifies plan approval, Agent-mode execution, durable
receipts, generated files, and a real loopback HTTP deployment without using a
provider key.

Live-provider validation is optional and must be reported as unverified unless
it is actually run. To exercise it manually, create a temporary project and
state database, then run the installed UI with the configured provider:

```bash
work_dir="$(mktemp -d)"
mkdir "$work_dir/project"
KYROZEN_DB_PATH="$work_dir/state.sqlite3" kyrozen --project "$work_dir/project"
```

Select Plan mode, request a small locally served page, accept the proposal, and
confirm the task receipts and deployment check before reporting a live pass.
4 changes: 2 additions & 2 deletions docs/self-evolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ diagnostic only and is not treated as a release claim.

## Verification snapshot commands

Current repository test count at this snapshot: **233 unittest cases**.
Current repository test count at this snapshot: **239 unittest cases**.

The post-#54 snapshot ran the repository's current checks and smoke coverage:

Expand Down Expand Up @@ -275,7 +275,7 @@ git diff --check
```

The historical post-#54 verification passed the 130 discovered tests; the current
repository contains 233 discovered tests. The historical run also covered the API
repository contains 239 discovered tests. The historical run also covered the API
health/scoping smoke, the CLI command-loop smoke, and the five-case
clean/evolved benchmark described above. `make check` reports the live 39-tool
runtime inventory, including 14 `git_` tools. A new artifact is not immediate:
Expand Down
47 changes: 47 additions & 0 deletions interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,53 @@ def parse_control_block(text: str, name: str) -> Any | None:
raise InteractionError(f"{name} contains invalid JSON: {exc.msg}") from exc


def normalize_provider_control(text: str) -> tuple[str, dict[str, Any]] | None:
"""Map a bounded provider JSON shape to one non-executable control."""
source = str(text or "").strip()
fenced = re.fullmatch(r"```(?:json)?\s*([\s\S]*?)\s*```", source, re.IGNORECASE)
if fenced:
source = fenced.group(1).strip()
try:
value = json.loads(source)
except json.JSONDecodeError:
return None
if not isinstance(value, dict):
return None

executable_keys = {"action", "args", "arguments", "command", "tool", "tool_calls"}
stack: list[Any] = [value]
while stack:
item = stack.pop()
if isinstance(item, dict):
if executable_keys & {str(key).lower() for key in item}:
return None
stack.extend(item.values())
elif isinstance(item, list):
stack.extend(item)

mode = str(value.get("mode") or "").strip().lower()
if mode == "plan" or value.get("do_not_execute_until_approved") is True:
steps = []
for index, step in enumerate(value.get("steps") or [], 1):
if not isinstance(step, dict):
return None
steps.append({
"id": str(step.get("id") or step.get("step_id") or f"step-{step.get('step', index)}"),
"title": step.get("title") or f"Step {index}",
"description": step.get("description") or step.get("details"),
"acceptance": step.get("acceptance") or step.get("acceptance_criteria"),
})
return "PlanProposal", {
"title": value.get("title") or value.get("plan_name"),
"summary": value.get("summary") or value.get("overview"),
"assumptions": value.get("assumptions") or [],
"steps": steps,
}
if mode == "ask" or value.get("ask_user") is True:
return "AskUser", {"questions": value.get("questions")}
return None


def route_mode(preference: str, user_input: str, *, pending_plan: bool = False,
executing_plan: bool = False) -> str:
if executing_plan:
Expand Down
52 changes: 45 additions & 7 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def _terminal_supports_unicode() -> bool:
from event_store import stable_hash, utc_now
from interaction import (
InteractionController, InteractionError, is_plan_acceptance, mode_capabilities,
parse_control_block, render_plan, render_question, validate_plan_proposal,
normalize_provider_control, parse_control_block, render_plan, render_question, validate_plan_proposal,
validate_question_request,
)
from learning_engine import LearningEngine
Expand Down Expand Up @@ -2748,16 +2748,25 @@ def interaction_envelope(user_input: str = "") -> dict[str, Any]:
return _interaction_controller.envelope(user_input)


def interaction_workspace_id(context: LaunchContext | None = None) -> str:
"""Keep project interaction state separate without splitting global memory."""
active = context if context is not None else _launch_context
if isinstance(active, LaunchContext) and not active.is_global:
return active.source_scope_id
return memory_bank.workspace_id


def bind_interaction_scope(session_id: str, *, user_id: str | None = None) -> None:
"""Bind the shared task and interaction facades to one local surface."""
global tasks, _interaction_controller
owner = user_id or memory_bank.user_id
workspace_id = interaction_workspace_id()
tasks = TaskManager(
memory_bank.store, workspace_id=memory_bank.workspace_id,
memory_bank.store, workspace_id=workspace_id,
session_id=session_id, user_id=owner,
)
_interaction_controller = InteractionController(
memory_bank.store, user_id=owner, workspace_id=memory_bank.workspace_id,
memory_bank.store, user_id=owner, workspace_id=workspace_id,
session_id=session_id,
)
_restore_ponytail_level()
Expand Down Expand Up @@ -3879,6 +3888,17 @@ def _execute_turn_action(action: str, args: Any, *, operation_scope: str,
)
args = str(args)
operation_id = _operation_id(operation_scope, canonical, args)
if (_is_state_changing_action(canonical, args)
and _interaction_controller.state().get("executing_plan")):
allowed, reason = tasks.mutation_matches_current_task(canonical, args)
if not allowed:
result = f"Error: action does not match the accepted plan; {reason}."
_notify_tool_execute(canonical, args, result)
return _make_execution_receipt(
action=canonical, args=args, authorized=False, started_at=started_at,
success=False, result=result, operation_scope=operation_scope,
failure="plan_action_mismatch",
)
if _is_state_changing_action(canonical, args) and operation_id in successful_operations:
result = "Error: duplicate successful state-changing action refused for this turn."
_notify_tool_execute(canonical, args, result)
Expand Down Expand Up @@ -4337,14 +4357,17 @@ class DeepSeekDSMLFilter:
r"(?i)(?<![\w])(?P<kind>Action|Thought|Plan|TaskList|TaskDone|DefineTool)\s*:"
)
_GENERIC_OPEN_RE = re.compile(
r"<\s*(?P<kind>invoke|parameter|calls|tool_calls|function_calls)\b[^>]*>",
r"<\s*(?P<kind>invoke|parameter|calls|tool_calls|function_calls|tool_use|notes|thought|reasoning)\b[^>]*>",
re.IGNORECASE,
)
_GENERIC_CLOSE_RE = re.compile(
r"</\s*(?P<kind>invoke|parameter|calls|tool_calls|function_calls)\s*>",
r"</\s*(?P<kind>invoke|parameter|calls|tool_calls|function_calls|tool_use|notes|thought|reasoning)\s*>",
re.IGNORECASE,
)
_GENERIC_KINDS = ("invoke", "parameter", "calls", "tool_calls", "function_calls")
_GENERIC_KINDS = (
"invoke", "parameter", "calls", "tool_calls", "function_calls",
"tool_use", "notes", "thought", "reasoning",
)
_ACTION_MARKER_RE = _ACTION_MARKER_RE
_CONTROL_PREFIXES = tuple(
item[:length].lower()
Expand Down Expand Up @@ -4474,7 +4497,10 @@ def _action_end(cls, value: str, start: int, *, final: bool) -> int | None:
@classmethod
def _generic_block_end(cls, value: str, match: re.Match[str]) -> int | None:
kind = match.group("kind").lower()
close_kinds = "parameter" if kind == "parameter" else "invoke|calls|tool_calls|function_calls"
close_kinds = (
"parameter" if kind == "parameter" else
"invoke|calls|tool_calls|function_calls|tool_use|notes|thought|reasoning"
)
close = re.compile(rf"</\s*(?:{close_kinds})\s*>", re.IGNORECASE).search(value, match.end())
return close.end() if close else None

Expand Down Expand Up @@ -4740,6 +4766,12 @@ def _parse_model_response(text: str) -> dict[str, Any]:
try:
question_value = parse_control_block(raw, "AskUser")
plan_value = parse_control_block(raw, "PlanProposal")
if question_value is None and plan_value is None:
normalized = normalize_provider_control(raw)
if normalized is not None:
name, value = normalized
question_value = value if name == "AskUser" else None
plan_value = value if name == "PlanProposal" else None
if question_value is not None:
question = validate_question_request(question_value)
if plan_value is not None:
Expand Down Expand Up @@ -4856,6 +4888,8 @@ def _safe_fstring(s: str) -> str:

def _tasks_from_plan(text: str) -> None:
"""Parse a Plan block and create pending tasks."""
if tasks.tasks:
return
plan_match = re.search(
r"Plan:\s*\n(.*?)(?=\n\s*(?:Action|TaskList|$))",
text,
Expand Down Expand Up @@ -5745,6 +5779,10 @@ def _chat_turn(user_input: str, clear_tasks: bool = False, profile: str | None =
"Execute the accepted plan below in Agent mode. Complete each durable task with evidence.\n\n"
+ json.dumps(accepted_plan, ensure_ascii=False)
)
_emit_stream_event({
"event": "interaction",
"interaction": _interaction_controller.envelope(user_input),
})
clear_tasks = False
elif mode_override is None and pending_plan:
user_input = (
Expand Down
Loading
Loading