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
5 changes: 4 additions & 1 deletion docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The plugin lifecycle supports project Python extensions such as the sample `plug

## Dynamic tools

Runtime-created dynamic tools are a distinct feature from skills. They remain disabled unless the operator enables them with `KYROZEN_ALLOW_DYNAMIC_TOOLS` and all normal tool capability and approval checks still apply. Review generated source and permissions before activation. A learning feature that observes successful tools does not silently switch this policy on.
Runtime-created dynamic tools are a distinct feature from skills. They remain disabled unless the operator enables them with `KYROZEN_ALLOW_DYNAMIC_TOOLS` and all normal tool capability and approval checks still apply. Dynamic executors must return `ToolResult`; the restricted source namespace provides `ToolResult` and `ToolError` constructors. Review generated source and permissions before activation. A learning feature that observes successful tools does not silently switch this policy on.

## Learned policies and skills

Expand Down Expand Up @@ -63,3 +63,6 @@ Registration validates the supported object-schema vocabulary, including nested
properties/items/combinators and scalar constraints. Unsupported keywords are
rejected rather than forwarded unchecked; this registry is not a general JSON
Schema evaluation engine. Existing legacy argument conversion remains unchanged.


Built-in tools use stable result codes such as `invalid_arguments`, `not_found`, `permission_denied`, `approval_denied`, `dependency_unavailable`, `timeout`, `cancelled`, `command_failed`, `stale_file`, `conflict`, `execution_error`, and `contract_error`. Mutating tools keep failures non-retriable unless retry safety is explicitly established.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ This index covers the current product guides, developer references, and dated en
- [V3 provider contract plan](superpowers/plans/2026-10-08-v3-issue-228.md) records the structured response and compatibility migration for issue #228.
- [V3 provider error plan](superpowers/plans/2026-10-09-v3-issue-231.md) records typed failures, cancellation and bounded retry for issue #231.
- [V3 authoritative tool catalog plan](superpowers/plans/2026-10-09-v3-issue-232.md) records registry ownership and schema/policy migration for issue #232.
- [V3 typed tool results plan](superpowers/plans/2026-10-11-v3-issue-235.md) records typed execution outcomes, error codes, observer propagation, and compatibility for issue #235.
- Machine-readable audit receipts and benchmark inputs remain beside their corresponding reports under `docs/` and `docs/benchmarks/`.

The repository's [English overview](../README.md) links here. Technical guides are maintained in English; README translations provide localized entry points.
77 changes: 77 additions & 0 deletions docs/superpowers/plans/2026-10-11-v3-issue-235.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# V3 Issue #235 Typed Tool Results Implementation Plan

> **For agentic workers:** Execute this plan task by task with test-first changes and a whole-branch independent review before delivery.

**Goal:** Complete R024, R025 and R031: tool outcomes and stable failures are machine-readable, while legacy text output is a presentation-only view.

**Architecture:** Add immutable `ToolError`/`ToolResult` contracts and a single renderer. Bind typed executors in each workspace registry while preserving text-facing built-in methods and mappings. Make the runtime consume `ToolResult` for receipts, observers, learning and MCP status; preserve existing authorization, scope and evidence gates.

**Tech Stack:** Python 3.12–3.13; stdlib dataclasses/JSON; current unittest suite; no new dependencies or paid provider calls.

**Spec:** GitHub issue #235 and the supplied OpenKyrozen V3 architecture proposal (R024, R025, R031); only #235 is in scope.

## Global Constraints

- Preserve `CommandResult`, built-in text APIs, MCP text content, approval requirements, workspace and subagent boundaries, usage accounting, receipt/evidence verification, and canonical registry metadata.
- Native typed argument migration, provider serialization, receipt identity expansion, parallel scheduling and retries stay in their assigned issues.
- A custom/dynamic executor must return `ToolResult`; invalid return values fail terminally with `contract_error` after one invocation.
- Never infer success from result text. Child work status is distinct from orchestration-call status.

## Review Focus

- Successful content beginning with `Error:` remains successful; failed Git/command results remain failed even when their text omits error markers.
- Exceptions and returned `ToolError` retain stable codes without exposing credentials or request bodies.
- A tool that mutates state and returns an invalid result is not run a second time.
- Child failure reports do not make an orchestration query itself fail; malformed coordinator requests do.
- Changing display rendering cannot change authorization, receipts, observer events, learning statistics, MCP `isError`, or plan acceptance.

---

### Task 1: Result and error contracts

**Files:** `openkyrozen/tools/models.py`, `openkyrozen/tools/__init__.py`, `tests/test_tool_results.py`.

- [x] Write failing tests for frozen `ToolError`/`ToolResult`, `success`, required code/message, JSON-safe data/metadata, defensive copies, and render behavior.
- [x] Verify RED, then implement immutable records, validation, and `render_tool_result(result) -> str` while retaining `CommandResult` unchanged.
- [x] Define and document stable codes for invalid arguments, not found, permission/approval denial, unavailable dependency, timeout, cancellation, command failure, stale/conflict, execution error and contract error. Default retries to false; no retry behavior is added.
- [x] Run the focused tool-result contract tests; all pass on Python 3.12.

### Task 2: Typed registry bindings and tool producers

**Files:** `openkyrozen/tools/manifest.py`, tool adapter modules, `openkyrozen/agent/runtime.py`, `openkyrozen/agent/delegation_runtime.py`, `openkyrozen/memory/retrieval.py`, `tests/test_tool_registry.py`, `tests/test_tools.py`.

- [x] Add regressions proving all 48 canonical specs bind typed executors; built-in text projections, registry metadata, and workspace isolation remain intact.
- [x] Add failure regressions for error-prefixed valid content, command status, browser snapshot failure, and invalid custom returns.
- [x] Migrate producers at the point status is known, preserving text and structured data and converting `CommandResult` fields explicitly.
- [x] Represent delegation invocation status separately from child statuses.
- [x] Bind typed executors in `ToolSpec`; preserve text projections and require typed custom/dynamic results in the existing restricted namespace.
- [x] Add terminal contract errors for wrong return types and assert single execution.
- [x] Run focused tool, registry, dynamic-tool, browser, and task suites on Python 3.12; 127 tests pass (one browser integration skip).

### Task 3: Runtime and surface status propagation

**Files:** `openkyrozen/agent/executor.py`, `openkyrozen/agent/planner.py`, `openkyrozen/agent/subagent_runtime.py`, `openkyrozen/learning/failures.py`, `openkyrozen/interfaces/mcp/server.py`, CLI/Web render boundaries, relevant tests.

- [x] Add regressions showing result wording cannot change receipt, plugin, or learning status.
- [x] Derive runtime receipt success and error code, observer and learning status, and MCP `isError` from typed outcomes while retaining text projections.
- [x] Preserve independent effect and acceptance verification.
- [x] Remove tool status classification from shared execution; leave unused legacy text helper for compatibility.
- [x] Run focused executor, receipt, plugin, learning, MCP, subagent and surface suites on Python 3.12.

### Task 4: Integration, docs and delivery

**Files:** `docs/extensions.md`, generated/API docs as needed, focused and integration tests.

- [x] Document the typed result contract, stable codes, custom/dynamic migration, text compatibility and non-retriable mutation failures; index the implementation plan.
- [x] Python 3.12 `make test-core`: 625 passed, 14 optional skips. `make check`, `make lint`, `make docs-check`, and offline agent/subagent acceptance pass.
- [x] Python 3.13 focused tool/runtime suites: 123 pass; the three mocked browser manager tests also pass. Browser-enabled `make test` reached 625 tests but its three browser flows require the unavailable Chromium executable.
- [x] Independent review found a browser click/snapshot status-loss defect; fixed it and added a regression. Await automated PR reviews for any further findings.
- [ ] Create one signed PR, verify signatures locally and on GitHub, attach it, and wait for final-head CI/review. Leave the PR open and unmerged.

## Assumptions and defaults

Built-in string contracts remain available to current direct callers; internal registry execution is typed. Unknown custom/dynamic return types fail closed after one call. `retriable` defaults false, with no automatic retry in this issue. Tool success, child-task success and verified acceptance remain three separate states.

## Interface and compatibility

`ToolResult` exposes `data`, `text`, optional `ToolError`, metadata and derived `success`; `ToolError` exposes stable `code`, `message`, `retriable` and metadata. Existing built-in string methods and `AVAILABLE_TOOLS` calls continue through rendering wrappers. Custom/dynamic string-returning callables must migrate to `ToolResult`. Existing receipt/client text fields remain compatible; machine status comes from the typed result.
48 changes: 39 additions & 9 deletions openkyrozen/agent/delegation_runtime.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Runtime integration for chat-owned, independently scoped delegation."""
from __future__ import annotations
from openkyrozen.tools.models import ToolError, ToolResult, command_tool_result, legacy_text_api, tool_failure, tool_json_result, tool_success

import copy
import json
Expand Down Expand Up @@ -110,37 +111,66 @@ def _json_args(args):
return value


def _typed_json_args(args):
try:
return _json_args(args), None
except (TypeError, ValueError) as exc:
return None, tool_failure("invalid_arguments", f"Error: invalid delegation arguments: {exc}")


@legacy_text_api
def spawn_agents(self, args):
"""Automatically start parallel specialist assignments; args is a JSON assignment batch."""
if self.execution_context.child_run_id:
raise ValueError("Only the main agent can delegate")
return json.dumps({"agents": self.delegation().spawn(_json_args(args).get("assignments"))}, ensure_ascii=False)
value, error = _typed_json_args(args)
if error:
return error
data = {"agents": self.delegation().spawn(value.get("assignments"))}
return tool_json_result(data, text=json.dumps(data, ensure_ascii=False))


@legacy_text_api
def send_subagent(self, args):
"""Reuse a finished sub-agent; args JSON contains run_id and a complete assignment."""
value = _json_args(args)
return json.dumps(self.delegation().send(value.get("run_id"), value.get("assignment")), ensure_ascii=False)
value, error = _typed_json_args(args)
if error:
return error
data = self.delegation().send(value.get("run_id"), value.get("assignment"))
return tool_json_result(data, text=json.dumps(data, ensure_ascii=False))


@legacy_text_api
def list_subagents(self, args):
"""List agents or inspect one run_id from this chat/project; args is JSON."""
value = _json_args(args)
value, error = _typed_json_args(args)
if error:
return error
coordinator = self.delegation()
return json.dumps(coordinator.detail(value["run_id"]) if value.get("run_id") else
{"agents": [coordinator.summary(run, results=True) for run in coordinator.list()]}, ensure_ascii=False)
data = coordinator.detail(value["run_id"]) if value.get("run_id") else {
"agents": [coordinator.summary(run, results=True) for run in coordinator.list()]}
return tool_json_result(data, text=json.dumps(data, ensure_ascii=False))


@legacy_text_api
def wait_subagents(self, args):
"""Wait up to 60 seconds for delegated work and its mandatory reviews; args is JSON."""
value = _json_args(args)
value, error = _typed_json_args(args)
if error:
return error
coordinator = self.delegation()
return json.dumps({"agents": [coordinator.summary(run, results=True) for run in coordinator.wait(value.get("run_ids"), value.get("timeout", 30))]}, ensure_ascii=False)
data = {"agents": [coordinator.summary(run, results=True) for run in coordinator.wait(value.get("run_ids"), value.get("timeout", 30))]}
return tool_json_result(data, text=json.dumps(data, ensure_ascii=False))


@legacy_text_api
def cancel_subagent(self, args):
"""Cancel a scoped sub-agent and prevent subsequent tool execution; args JSON contains run_id."""
return json.dumps(self.delegation().cancel(_json_args(args).get("run_id")), ensure_ascii=False)
value, error = _typed_json_args(args)
if error:
return error
data = self.delegation().cancel(value.get("run_id"))
return tool_json_result(data, text=json.dumps(data, ensure_ascii=False))


def _invoke_delegated(self, run, *, review, feedback, coordinator):
Expand Down
Loading
Loading