chore: import upstream snapshot with attribution
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
"""Interactive-shell action tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Assistant handoff pseudo-tool for non-executable requests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from core.agent_harness.tools.tool_context import (
|
||||
ActionToolContext,
|
||||
execute_with_action_context,
|
||||
object_schema,
|
||||
string_property,
|
||||
)
|
||||
from core.tool_framework.registered_tool import RegisteredTool
|
||||
|
||||
|
||||
def execute_assistant_handoff_tool(args: dict[str, Any], ctx: ActionToolContext) -> bool:
|
||||
_ = args
|
||||
_ = ctx
|
||||
# Handoffs are informational planning outputs and intentionally
|
||||
# execute no terminal side effects.
|
||||
return True
|
||||
|
||||
|
||||
def run_assistant_handoff(*, content: str, context: Any) -> dict[str, Any]:
|
||||
return execute_with_action_context(
|
||||
{"content": content},
|
||||
context,
|
||||
execute_assistant_handoff_tool,
|
||||
)
|
||||
|
||||
|
||||
assistant_handoff_tool = RegisteredTool(
|
||||
name="assistant_handoff",
|
||||
description=(
|
||||
"Mark a request as non-executable and hand off to assistant response generation. "
|
||||
"Use for informational, conversational, ambiguous, or non-actionable requests, "
|
||||
"including a bare pasted alert JSON/YAML/key-value blob or bare incident statement "
|
||||
"when the user did not explicitly ask to investigate, analyze, diagnose, RCA, or "
|
||||
"root-cause it."
|
||||
),
|
||||
input_schema=object_schema(
|
||||
properties={
|
||||
"content": string_property(
|
||||
description=(
|
||||
"Concise assistant handoff text for informational, ambiguous, "
|
||||
"or non-executable requests. Prefer structured tags when the "
|
||||
"topic is known — e.g. docs:datadog_setup, chat:greeting, "
|
||||
"provider:local_llama_connect for vague local-model setup."
|
||||
),
|
||||
min_length=1,
|
||||
)
|
||||
},
|
||||
required=("content",),
|
||||
),
|
||||
source="interactive_shell",
|
||||
surfaces=("action",),
|
||||
parallel_safe=False,
|
||||
accepts_runtime_context=True,
|
||||
run=run_assistant_handoff,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["assistant_handoff_tool", "execute_assistant_handoff_tool"]
|
||||
@@ -0,0 +1,60 @@
|
||||
"""CLI command tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from core.agent_harness.tools.tool_context import (
|
||||
ActionToolContext,
|
||||
capability_available_from_sources,
|
||||
execute_with_action_context,
|
||||
object_schema,
|
||||
string_property,
|
||||
)
|
||||
from core.tool_framework.registered_tool import RegisteredTool
|
||||
from tools.interactive_shell.cli import run_opensre_cli_command
|
||||
from tools.interactive_shell.subprocess import require_subprocess_presenter
|
||||
|
||||
|
||||
def execute_cli_command_tool(args: dict[str, Any], ctx: ActionToolContext) -> bool:
|
||||
payload = str(args.get("payload", "")).strip()
|
||||
if not payload:
|
||||
return False
|
||||
run_opensre_cli_command(payload, require_subprocess_presenter(ctx))
|
||||
return True
|
||||
|
||||
|
||||
def run_cli_command(*, payload: str, context: Any) -> dict[str, Any]:
|
||||
return execute_with_action_context({"payload": payload}, context, execute_cli_command_tool)
|
||||
|
||||
|
||||
cli_exec_tool = RegisteredTool(
|
||||
name="cli_exec",
|
||||
description=(
|
||||
"Run an `opensre` CLI subcommand payload (without the leading `opensre ` prefix). "
|
||||
"Prefer allowed operational families such as health/status/list/show/integrations/"
|
||||
"synthetic checks; avoid unrelated or dangerous payloads."
|
||||
),
|
||||
input_schema=object_schema(
|
||||
properties={
|
||||
"payload": string_property(
|
||||
description=(
|
||||
"CLI payload passed to `opensre` without the leading command prefix "
|
||||
"(for example: `integrations list`, `health`, `synthetic run ...`). "
|
||||
"Must not start with `opensre `."
|
||||
),
|
||||
min_length=1,
|
||||
)
|
||||
},
|
||||
required=("payload",),
|
||||
),
|
||||
source="interactive_shell",
|
||||
surfaces=("action",),
|
||||
parallel_safe=False,
|
||||
accepts_runtime_context=True,
|
||||
run=run_cli_command,
|
||||
is_available=lambda sources: capability_available_from_sources(sources, "cli_commands"),
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["cli_exec_tool", "execute_cli_command_tool"]
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Implementation tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from core.agent_harness.tools.tool_context import (
|
||||
ActionToolContext,
|
||||
capability_available_from_sources,
|
||||
execute_with_action_context,
|
||||
object_schema,
|
||||
string_property,
|
||||
)
|
||||
from core.tool_framework.registered_tool import RegisteredTool
|
||||
from tools.interactive_shell.implementation.claude_code_executor import (
|
||||
run_claude_code_implementation,
|
||||
)
|
||||
from tools.interactive_shell.subprocess import require_subprocess_presenter
|
||||
|
||||
|
||||
def execute_implementation_tool(args: dict[str, Any], ctx: ActionToolContext) -> bool:
|
||||
task = str(args.get("task", "")).strip()
|
||||
if not task:
|
||||
return False
|
||||
run_claude_code_implementation(task, require_subprocess_presenter(ctx))
|
||||
return True
|
||||
|
||||
|
||||
def run_implementation(*, task: str, context: Any) -> dict[str, Any]:
|
||||
return execute_with_action_context({"task": task}, context, execute_implementation_tool)
|
||||
|
||||
|
||||
code_implement_tool = RegisteredTool(
|
||||
name="code_implement",
|
||||
description="Run code implementation workflow using Claude Code.",
|
||||
input_schema=object_schema(
|
||||
properties={
|
||||
"task": string_property(
|
||||
description="Implementation task to execute in the codebase.",
|
||||
min_length=1,
|
||||
)
|
||||
},
|
||||
required=("task",),
|
||||
),
|
||||
source="interactive_shell",
|
||||
surfaces=("action",),
|
||||
parallel_safe=False,
|
||||
accepts_runtime_context=True,
|
||||
run=run_implementation,
|
||||
is_available=lambda sources: capability_available_from_sources(sources, "implementation"),
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["code_implement_tool", "execute_implementation_tool"]
|
||||
@@ -0,0 +1,141 @@
|
||||
"""Investigation tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
from core.agent_harness.tools.tool_context import (
|
||||
ActionToolContext,
|
||||
execute_with_action_context,
|
||||
object_schema,
|
||||
string_property,
|
||||
)
|
||||
from core.tool_framework.registered_tool import RegisteredTool
|
||||
from platform.common.task_types import TaskRecord
|
||||
from surfaces.interactive_shell.session import Session
|
||||
from tools.interactive_shell.shared.investigation_launch import launch_investigation
|
||||
|
||||
|
||||
def normalize_investigation_alert_text(raw: str) -> str:
|
||||
"""Strip outer quotes models often echo from user-quoted investigation payloads."""
|
||||
value = raw.strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
|
||||
return value[1:-1].strip()
|
||||
return value
|
||||
|
||||
|
||||
def run_text_investigation(
|
||||
alert_text: str,
|
||||
session: Session,
|
||||
console: Console,
|
||||
*,
|
||||
confirm_fn: Callable[[str], str] | None = None,
|
||||
is_tty: bool | None = None,
|
||||
action_already_listed: bool = False,
|
||||
) -> None:
|
||||
from surfaces.interactive_shell.runtime.investigation_adapter import (
|
||||
repl_investigation_launch_ports,
|
||||
run_investigation_for_session,
|
||||
)
|
||||
|
||||
def _run(task: TaskRecord) -> dict[str, object]:
|
||||
return run_investigation_for_session(
|
||||
alert_text=alert_text,
|
||||
context_overrides=session.accumulated_context or None,
|
||||
cancel_requested=task.cancel_requested,
|
||||
)
|
||||
|
||||
def _start_background() -> None:
|
||||
from surfaces.interactive_shell.runtime.background.runner import (
|
||||
start_background_text_investigation,
|
||||
)
|
||||
|
||||
start_background_text_investigation(
|
||||
alert_text=alert_text,
|
||||
session=session,
|
||||
console=console,
|
||||
display_command="background free-text investigation",
|
||||
)
|
||||
|
||||
launch_investigation(
|
||||
session=session,
|
||||
console=console,
|
||||
ports=repl_investigation_launch_ports(),
|
||||
tool_type="investigation",
|
||||
action_summary=f'investigation from text "{alert_text}"',
|
||||
announce_label="investigation",
|
||||
announce_value=alert_text,
|
||||
record_value=alert_text,
|
||||
foreground_task_command=f"investigate:{alert_text}",
|
||||
exception_context="surfaces.interactive_shell.text_investigation",
|
||||
run=_run,
|
||||
start_background=_start_background,
|
||||
confirm_fn=confirm_fn,
|
||||
is_tty=is_tty,
|
||||
action_already_listed=action_already_listed,
|
||||
)
|
||||
|
||||
|
||||
def execute_investigation_tool(args: dict[str, Any], ctx: ActionToolContext) -> bool:
|
||||
alert_text = normalize_investigation_alert_text(str(args.get("alert_text", "")))
|
||||
if not alert_text:
|
||||
return False
|
||||
run_text_investigation(
|
||||
alert_text,
|
||||
ctx.session,
|
||||
ctx.console,
|
||||
confirm_fn=ctx.confirm_fn,
|
||||
is_tty=ctx.is_tty,
|
||||
action_already_listed=ctx.action_already_listed,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def run_investigation(*, alert_text: str, context: Any) -> dict[str, Any]:
|
||||
return execute_with_action_context(
|
||||
{"alert_text": alert_text},
|
||||
context,
|
||||
execute_investigation_tool,
|
||||
)
|
||||
|
||||
|
||||
investigation_start_tool = RegisteredTool(
|
||||
name="investigation_start",
|
||||
description=(
|
||||
"Start an investigation with the provided alert text or quoted payload. "
|
||||
"Use whenever the user explicitly instructs you to investigate, RCA, "
|
||||
"diagnose, analyze, root-cause, or send an investigation payload — including "
|
||||
"'investigate why X ...' and placeholder quoted text like 'hello world' — "
|
||||
"regardless of CONNECTED INTEGRATIONS. In compound turns like `run /remote "
|
||||
'and then investigate "hello world"`, emit this as a separate second tool '
|
||||
"call; never drop the quoted investigation after emitting the slash command. "
|
||||
"Do NOT use for bare incident statements with no investigate verb, generic "
|
||||
"'Run an investigation.' with no subject, sample/demo alerts, or plain data "
|
||||
"lookups."
|
||||
),
|
||||
input_schema=object_schema(
|
||||
properties={
|
||||
"alert_text": string_property(
|
||||
description="Alert text or incident details to investigate.",
|
||||
min_length=1,
|
||||
)
|
||||
},
|
||||
required=("alert_text",),
|
||||
),
|
||||
source="interactive_shell",
|
||||
surfaces=("action",),
|
||||
parallel_safe=False,
|
||||
accepts_runtime_context=True,
|
||||
run=run_investigation,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"execute_investigation_tool",
|
||||
"investigation_start_tool",
|
||||
"normalize_investigation_alert_text",
|
||||
"run_text_investigation",
|
||||
]
|
||||
@@ -0,0 +1,96 @@
|
||||
"""LLM provider switch tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from rich.markup import escape
|
||||
|
||||
from core.agent_harness.tools.tool_context import (
|
||||
ActionToolContext,
|
||||
capability_available_from_sources,
|
||||
execute_with_action_context,
|
||||
object_schema,
|
||||
)
|
||||
from core.tool_framework.registered_tool import RegisteredTool
|
||||
from surfaces.interactive_shell.command_registry import (
|
||||
switch_llm_provider,
|
||||
switch_reasoning_model,
|
||||
)
|
||||
from surfaces.interactive_shell.ui.execution_confirm import execution_allowed
|
||||
from tools.interactive_shell.shared import allow_tool
|
||||
|
||||
|
||||
def _provider_values() -> tuple[str, ...]:
|
||||
from surfaces.cli.wizard.config import PROVIDER_BY_VALUE
|
||||
|
||||
return tuple(sorted(PROVIDER_BY_VALUE.keys()))
|
||||
|
||||
|
||||
def _target_property_schema() -> dict[str, Any]:
|
||||
provider_values = _provider_values()
|
||||
provider_list = ", ".join(provider_values)
|
||||
return {
|
||||
"description": (
|
||||
"Target passed to `/model set <target>`. Use one of the provider names "
|
||||
f"({provider_list}) to switch providers, or pass a valid reasoning model "
|
||||
"name for the active provider."
|
||||
),
|
||||
"oneOf": [
|
||||
{"type": "string", "enum": list(provider_values)},
|
||||
{"type": "string", "minLength": 1},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _apply_model_set_target(target: str, ctx: ActionToolContext) -> bool:
|
||||
from surfaces.cli.wizard.config import PROVIDER_BY_VALUE
|
||||
|
||||
candidate = target.strip()
|
||||
if candidate.lower() in PROVIDER_BY_VALUE:
|
||||
return switch_llm_provider(candidate, ctx.console)
|
||||
return switch_reasoning_model(candidate, ctx.console)
|
||||
|
||||
|
||||
def execute_llm_provider_tool(args: dict[str, Any], ctx: ActionToolContext) -> bool:
|
||||
target = str(args.get("target", args.get("provider", ""))).strip()
|
||||
if not target:
|
||||
return False
|
||||
policy = allow_tool("switch_llm_provider")
|
||||
if not execution_allowed(
|
||||
policy,
|
||||
session=ctx.session,
|
||||
console=ctx.console,
|
||||
action_summary=f"/model set {target}",
|
||||
confirm_fn=ctx.confirm_fn,
|
||||
is_tty=ctx.is_tty,
|
||||
action_already_listed=ctx.action_already_listed,
|
||||
):
|
||||
return True
|
||||
ctx.console.print(f"[bold]$ /model set {escape(target)}[/bold]")
|
||||
ok = _apply_model_set_target(target, ctx)
|
||||
ctx.session.record("slash", f"/model set {target}", ok=ok)
|
||||
return True
|
||||
|
||||
|
||||
def run_llm_provider(*, target: str, context: Any) -> dict[str, Any]:
|
||||
return execute_with_action_context({"target": target}, context, execute_llm_provider_tool)
|
||||
|
||||
|
||||
llm_set_provider_tool = RegisteredTool(
|
||||
name="llm_set_provider",
|
||||
description="Switch the active LLM provider or reasoning model.",
|
||||
input_schema=object_schema(
|
||||
properties={"target": _target_property_schema()},
|
||||
required=("target",),
|
||||
),
|
||||
source="interactive_shell",
|
||||
surfaces=("action",),
|
||||
parallel_safe=False,
|
||||
accepts_runtime_context=True,
|
||||
run=run_llm_provider,
|
||||
is_available=lambda sources: capability_available_from_sources(sources, "llm_provider"),
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["execute_llm_provider_tool", "llm_set_provider_tool"]
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Sample alert tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
from core.agent_harness.tools.tool_context import (
|
||||
ActionToolContext,
|
||||
execute_with_action_context,
|
||||
object_schema,
|
||||
string_property,
|
||||
)
|
||||
from core.tool_framework.registered_tool import RegisteredTool
|
||||
from platform.common.task_types import TaskRecord
|
||||
from surfaces.interactive_shell.session import Session
|
||||
from tools.interactive_shell.shared.investigation_launch import launch_investigation
|
||||
|
||||
_SAMPLE_ALERT_TEMPLATES = ("generic",)
|
||||
|
||||
|
||||
def run_sample_alert(
|
||||
template_name: str,
|
||||
session: Session,
|
||||
console: Console,
|
||||
*,
|
||||
confirm_fn: Callable[[str], str] | None = None,
|
||||
is_tty: bool | None = None,
|
||||
action_already_listed: bool = False,
|
||||
) -> None:
|
||||
from surfaces.interactive_shell.runtime.investigation_adapter import (
|
||||
repl_investigation_launch_ports,
|
||||
run_sample_alert_for_session,
|
||||
)
|
||||
|
||||
def _run(task: TaskRecord) -> dict[str, object]:
|
||||
return run_sample_alert_for_session(
|
||||
template_name=template_name,
|
||||
context_overrides=session.accumulated_context or None,
|
||||
cancel_requested=task.cancel_requested,
|
||||
)
|
||||
|
||||
def _start_background() -> None:
|
||||
from surfaces.interactive_shell.runtime.background.runner import (
|
||||
start_background_template_investigation,
|
||||
)
|
||||
|
||||
start_background_template_investigation(
|
||||
template_name=template_name,
|
||||
session=session,
|
||||
console=console,
|
||||
display_command=f"sample alert:{template_name}",
|
||||
)
|
||||
|
||||
launch_investigation(
|
||||
session=session,
|
||||
console=console,
|
||||
ports=repl_investigation_launch_ports(),
|
||||
tool_type="sample_alert",
|
||||
action_summary=f"sample alert investigation ({template_name})",
|
||||
announce_label="sample alert",
|
||||
announce_value=template_name,
|
||||
record_value=f"sample:{template_name}",
|
||||
foreground_task_command=f"sample alert:{template_name}",
|
||||
exception_context="surfaces.interactive_shell.sample_alert",
|
||||
run=_run,
|
||||
start_background=_start_background,
|
||||
confirm_fn=confirm_fn,
|
||||
is_tty=is_tty,
|
||||
action_already_listed=action_already_listed,
|
||||
)
|
||||
|
||||
|
||||
def execute_sample_alert_tool(args: dict[str, Any], ctx: ActionToolContext) -> bool:
|
||||
template = str(args.get("template", "")).strip()
|
||||
if not template:
|
||||
return False
|
||||
run_sample_alert(
|
||||
template,
|
||||
ctx.session,
|
||||
ctx.console,
|
||||
confirm_fn=ctx.confirm_fn,
|
||||
is_tty=ctx.is_tty,
|
||||
action_already_listed=ctx.action_already_listed,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def run_sample_alert_action(*, template: str, context: Any) -> dict[str, Any]:
|
||||
return execute_with_action_context(
|
||||
{"template": template},
|
||||
context,
|
||||
execute_sample_alert_tool,
|
||||
)
|
||||
|
||||
|
||||
alert_sample_tool = RegisteredTool(
|
||||
name="alert_sample",
|
||||
description=(
|
||||
"Run the built-in synthetic sample alert end-to-end (read alert → "
|
||||
"investigate → diagnose). Use for any request to run/try/start/launch/"
|
||||
"fire/trigger/investigate/look at a 'sample alert', 'test alert', or "
|
||||
"'demo alert' (e.g. 'investigate a sample test alert?', 'kick off a "
|
||||
"sample alert'). These requests carry NO real pasted alert text — that "
|
||||
"is what separates them from investigation_start. Prefer this over "
|
||||
"investigation_start and assistant_handoff for sample/test/demo alerts, "
|
||||
"regardless of the verb or a trailing '?'."
|
||||
),
|
||||
input_schema=object_schema(
|
||||
properties={
|
||||
"template": string_property(
|
||||
description="Sample alert template name to run.",
|
||||
enum=_SAMPLE_ALERT_TEMPLATES,
|
||||
)
|
||||
},
|
||||
required=("template",),
|
||||
),
|
||||
source="interactive_shell",
|
||||
surfaces=("action",),
|
||||
parallel_safe=False,
|
||||
accepts_runtime_context=True,
|
||||
run=run_sample_alert_action,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["alert_sample_tool", "execute_sample_alert_tool", "run_sample_alert"]
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Action tool: dispatch a natural-language "fix this Sentry issue" request.
|
||||
|
||||
This is the intake-routing half of the Sentry issue-fix flow. It exposes the
|
||||
``fix_sentry_issue`` capability to the interactive-shell **action agent** as a
|
||||
proper AgentTool, so a request like *"fix this Sentry issue <url> and open a
|
||||
PR"* is dispatched by the action LLM (semantic intent) rather than by any
|
||||
regex/keyword shortcut. The tool is only offered when the fix capability is
|
||||
opted in (``PI_ISSUE_FIX_ENABLED``); the PR-shipping gate + token check stay
|
||||
inside ``fix_sentry_issue`` itself.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from core.agent_harness.tools.tool_context import (
|
||||
ActionToolContext,
|
||||
execute_with_action_context,
|
||||
object_schema,
|
||||
string_property,
|
||||
)
|
||||
from core.tool_framework.registered_tool import RegisteredTool
|
||||
from tools.cross_vendor.fix_sentry_issue import fix_sentry_issue
|
||||
from tools.cross_vendor.fix_sentry_issue.runner import is_issue_fix_enabled
|
||||
|
||||
|
||||
def _render_result(console: Any, out: dict[str, Any]) -> None:
|
||||
"""Print a concise, human-readable summary of the tool result."""
|
||||
if not out.get("success"):
|
||||
console.print(
|
||||
f"[red]Could not fix the Sentry issue[/] "
|
||||
f"({out.get('error_kind') or 'error'}): {out.get('error') or ''}"
|
||||
)
|
||||
# Point the user at their fix so they can recover manually, with guidance
|
||||
# matched to how far shipping got on the new branch:
|
||||
# commit_failed -> on the branch but NOT committed (edits staged only)
|
||||
# push_failed -> committed but not pushed
|
||||
# pr_failed -> pushed, only the PR call failed
|
||||
branch = out.get("branch_name")
|
||||
if branch:
|
||||
kind = out.get("error_kind")
|
||||
if kind == "commit_failed":
|
||||
console.print(
|
||||
f"[dim]Your changes are on branch [cyan]{branch}[/] but not committed — "
|
||||
"commit them, push, and open the PR manually.[/]"
|
||||
)
|
||||
elif kind == "pr_failed":
|
||||
console.print(
|
||||
f"[dim]Your fix is pushed to branch [cyan]{branch}[/] — "
|
||||
"open the PR manually.[/]"
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
f"[dim]Your fix is committed on branch [cyan]{branch}[/] — "
|
||||
"push it and open the PR manually.[/]"
|
||||
)
|
||||
elif out.get("changed_files"):
|
||||
console.print("[dim]The proposed fix is still in your working tree.[/]")
|
||||
return
|
||||
|
||||
console.print(f"[green]Fixed Sentry issue {out.get('issue_id')}[/].")
|
||||
if out.get("summary"):
|
||||
console.print(str(out["summary"]))
|
||||
for path in out.get("changed_files") or []:
|
||||
console.print(f" • {path}")
|
||||
if out.get("pr_url"):
|
||||
console.print(
|
||||
f"[bold]Opened pull request:[/] {out['pr_url']} "
|
||||
f"(branch [cyan]{out.get('branch_name')}[/])"
|
||||
)
|
||||
else:
|
||||
console.print("[dim]Diff left in your working tree (no PR requested).[/]")
|
||||
|
||||
|
||||
def execute_sentry_fix_tool(args: dict[str, Any], ctx: ActionToolContext) -> bool:
|
||||
sentry_url = str(args.get("sentry_url", "")).strip()
|
||||
if not sentry_url:
|
||||
return False
|
||||
open_pr = bool(args.get("open_pr", False))
|
||||
|
||||
ctx.console.print(
|
||||
f"[bold]Fixing Sentry issue[/] {sentry_url}"
|
||||
+ (" and opening a pull request…" if open_pr else "…")
|
||||
)
|
||||
out = fix_sentry_issue.run(sentry_url=sentry_url, open_pr=open_pr)
|
||||
_render_result(ctx.console, out)
|
||||
return True
|
||||
|
||||
|
||||
def run_sentry_fix(*, sentry_url: str, context: Any, open_pr: bool = False) -> dict[str, Any]:
|
||||
return execute_with_action_context(
|
||||
{"sentry_url": sentry_url, "open_pr": open_pr},
|
||||
context,
|
||||
execute_sentry_fix_tool,
|
||||
)
|
||||
|
||||
|
||||
fix_sentry_issue_start_tool = RegisteredTool(
|
||||
name="fix_sentry_issue_start",
|
||||
description=(
|
||||
"Fix a Sentry issue in code with a coding agent, given a Sentry issue URL. "
|
||||
"Use whenever the user asks to FIX, patch, resolve in code, or open/raise a pull "
|
||||
"request for a Sentry issue and provides a Sentry issue URL — e.g. 'fix this sentry "
|
||||
"issue <url>' or 'fix <url> and open a PR'. Set open_pr=true when they ask to "
|
||||
"open/create/raise a PR or to ship the fix; otherwise false to only produce a diff. "
|
||||
"Do NOT use for investigate/RCA/diagnose/analyze requests (use investigation_start), "
|
||||
"for non-Sentry URLs, or when no Sentry issue URL is provided."
|
||||
),
|
||||
input_schema=object_schema(
|
||||
properties={
|
||||
"sentry_url": string_property(
|
||||
description="The Sentry issue URL to fix (e.g. https://<org>.sentry.io/issues/<id>/).",
|
||||
min_length=1,
|
||||
),
|
||||
"open_pr": {
|
||||
"type": "boolean",
|
||||
"description": (
|
||||
"True when the user asks to open/create/raise a pull request or to ship "
|
||||
"the fix; false to only produce a reviewable diff."
|
||||
),
|
||||
},
|
||||
},
|
||||
required=("sentry_url",),
|
||||
),
|
||||
source="interactive_shell",
|
||||
surfaces=("action",),
|
||||
side_effect_level="mutating",
|
||||
parallel_safe=False,
|
||||
accepts_runtime_context=True,
|
||||
is_available=lambda _sources: is_issue_fix_enabled(),
|
||||
run=run_sentry_fix,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"execute_sentry_fix_tool",
|
||||
"fix_sentry_issue_start_tool",
|
||||
"run_sentry_fix",
|
||||
]
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Shell execution tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from core.agent_harness.tools.tool_context import (
|
||||
ActionToolContext,
|
||||
capability_available_from_sources,
|
||||
execute_with_action_context,
|
||||
object_schema,
|
||||
string_property,
|
||||
)
|
||||
from core.tool_framework.registered_tool import RegisteredTool
|
||||
from tools.interactive_shell.shell.runner import run_shell_command
|
||||
from tools.interactive_shell.subprocess import require_subprocess_presenter
|
||||
|
||||
|
||||
def execute_shell_tool(args: dict[str, Any], ctx: ActionToolContext) -> dict[str, Any]:
|
||||
command = str(args.get("command", "")).strip()
|
||||
if not command:
|
||||
return {"ok": False, "command": "", "response_text": "missing shell command"}
|
||||
return run_shell_command(command, require_subprocess_presenter(ctx))
|
||||
|
||||
|
||||
def run_shell(*, command: str, context: Any) -> dict[str, Any]:
|
||||
return execute_with_action_context({"command": command}, context, execute_shell_tool)
|
||||
|
||||
|
||||
shell_run_tool = RegisteredTool(
|
||||
name="shell_run",
|
||||
description=(
|
||||
"Run a narrowly scoped local diagnostic shell command. Use for read-only inspection "
|
||||
"or controlled operational steps already requested by the user; avoid destructive, "
|
||||
"credential-exfiltrating, or unrelated commands."
|
||||
),
|
||||
input_schema=object_schema(
|
||||
properties={
|
||||
"command": string_property(
|
||||
description=(
|
||||
"Exact shell command to execute. Prefer safe diagnostics (for example: "
|
||||
"`ls`, `pwd`, `git status`, `uv run python -m pytest ...`). Do not use "
|
||||
"commands that wipe data or alter unrelated system state."
|
||||
),
|
||||
min_length=1,
|
||||
)
|
||||
},
|
||||
required=("command",),
|
||||
),
|
||||
source="interactive_shell",
|
||||
surfaces=("action",),
|
||||
parallel_safe=False,
|
||||
accepts_runtime_context=True,
|
||||
run=run_shell,
|
||||
is_available=lambda sources: capability_available_from_sources(sources, "shell_commands"),
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["execute_shell_tool", "shell_run_tool"]
|
||||
@@ -0,0 +1,181 @@
|
||||
"""Slash command tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from rich.markup import escape
|
||||
|
||||
from core.agent_harness.session.terminal_access import (
|
||||
agent_turn_executed_slashes,
|
||||
exclusive_stdin_active,
|
||||
session_terminal,
|
||||
set_auto_command,
|
||||
set_turn_outcome_hint,
|
||||
)
|
||||
from core.agent_harness.tools.tool_context import (
|
||||
ActionToolContext,
|
||||
capability_available_from_sources,
|
||||
execute_with_action_context,
|
||||
)
|
||||
from core.tool_framework.registered_tool import RegisteredTool
|
||||
from surfaces.interactive_shell.command_registry import SLASH_COMMANDS, dispatch_slash
|
||||
from surfaces.interactive_shell.command_registry.slash_catalog import (
|
||||
slash_invoke_input_schema,
|
||||
slash_invoke_tool_description,
|
||||
)
|
||||
from surfaces.interactive_shell.ui import BOLD_BRAND, DIM, repl_tty_interactive
|
||||
from surfaces.interactive_shell.ui.execution_confirm import execution_allowed
|
||||
from surfaces.interactive_shell.utils.telemetry.turn_outcome import format_terminal_turn_outcome
|
||||
from tools.interactive_shell.shared import plan_foreground_tool
|
||||
|
||||
# Slash commands that drive a raw-stdin inline picker or wizard (questionary /
|
||||
# repl_choose_one). When the action agent resolves free text (e.g. "remove
|
||||
# github") into one of these, the REPL loop has NOT reserved exclusive stdin for
|
||||
# the turn — it only does so for deterministically-typed commands. Running the
|
||||
# picker inline then races the concurrently open prompt_async() for stdin and the
|
||||
# terminal's cursor-position replies (ESC[row;colR) leak into the input line as
|
||||
# literal keystrokes. Defer them through ``set_auto_command`` so the loop
|
||||
# re-dispatches the command as a deterministic turn it runs with exclusive stdin.
|
||||
_INTERACTIVE_PICKER_MENUS: frozenset[str] = frozenset({"/auth", "/login", "/integrations", "/mcp"})
|
||||
_INTERACTIVE_PICKER_SUBCOMMANDS: frozenset[tuple[str, str]] = frozenset(
|
||||
{
|
||||
("/auth", "login"),
|
||||
("/auth", "logout"),
|
||||
("/integrations", "setup"),
|
||||
("/integrations", "remove"),
|
||||
("/mcp", "connect"),
|
||||
("/mcp", "disconnect"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _slash_drives_interactive_picker(
|
||||
name: str,
|
||||
slash_args: list[str],
|
||||
*,
|
||||
session: Any,
|
||||
is_tty: bool | None,
|
||||
) -> bool:
|
||||
"""True when a planned slash command opens a raw-stdin inline picker/wizard.
|
||||
|
||||
Only relevant in an interactive REPL with a terminal facet: gateway/headless
|
||||
sessions always run inline, and non-TTY turns must not queue back to a REPL
|
||||
loop that does not exist (e.g. gateway running under tmux with a TTY stdin).
|
||||
"""
|
||||
if is_tty is False or session_terminal(session) is None:
|
||||
return False
|
||||
if not repl_tty_interactive():
|
||||
return False
|
||||
if name == "/login":
|
||||
return True
|
||||
if not slash_args:
|
||||
return name in _INTERACTIVE_PICKER_MENUS
|
||||
return (name, slash_args[0].lower()) in _INTERACTIVE_PICKER_SUBCOMMANDS
|
||||
|
||||
|
||||
def _dispatch_and_translate_exit(command: str, ctx: ActionToolContext, **kwargs: Any) -> bool:
|
||||
should_continue = dispatch_slash(
|
||||
command,
|
||||
ctx.session,
|
||||
ctx.console,
|
||||
confirm_fn=ctx.confirm_fn,
|
||||
is_tty=ctx.is_tty,
|
||||
**kwargs,
|
||||
)
|
||||
if not should_continue and ctx.request_exit is not None:
|
||||
ctx.request_exit()
|
||||
return True
|
||||
|
||||
|
||||
def execute_slash_tool(args: dict[str, Any], ctx: ActionToolContext) -> bool:
|
||||
command = str(args.get("command", "")).strip()
|
||||
raw_args = args.get("args")
|
||||
parsed_args = [str(item).strip() for item in raw_args] if isinstance(raw_args, list) else []
|
||||
full_command = " ".join([command, *parsed_args]) if parsed_args else command
|
||||
stripped = full_command.strip()
|
||||
if stripped == "/" or not stripped:
|
||||
return _dispatch_and_translate_exit(
|
||||
stripped or "/",
|
||||
ctx,
|
||||
)
|
||||
|
||||
parts = stripped.split()
|
||||
name = parts[0].lower()
|
||||
slash_args = parts[1:]
|
||||
cmd = SLASH_COMMANDS.get(name)
|
||||
if cmd is None:
|
||||
return _dispatch_and_translate_exit(
|
||||
stripped,
|
||||
ctx,
|
||||
)
|
||||
|
||||
if stripped in agent_turn_executed_slashes(ctx.session):
|
||||
return True
|
||||
|
||||
if _slash_drives_interactive_picker(
|
||||
name,
|
||||
slash_args,
|
||||
session=ctx.session,
|
||||
is_tty=ctx.is_tty,
|
||||
) and not exclusive_stdin_active(ctx.session):
|
||||
# Hand the picker back to the REPL loop instead of running it against the
|
||||
# live prompt: set_auto_command re-submits it as a deterministic turn
|
||||
# the loop dispatches with exclusive stdin, so no CPR replies leak in.
|
||||
# Do not record a slash history row here — dispatch_slash will record when
|
||||
# the queued command runs. Attach a turn hint for this turn's analytics.
|
||||
ctx.console.print(f"[{DIM}]Launching[/] [{BOLD_BRAND}]{escape(stripped)}[/]…")
|
||||
set_auto_command(ctx.session, stripped)
|
||||
set_turn_outcome_hint(ctx.session, f"queued {stripped} for exclusive stdin dispatch")
|
||||
return True
|
||||
|
||||
plan = plan_foreground_tool("slash", "slash")
|
||||
if not execution_allowed(
|
||||
plan.policy,
|
||||
session=ctx.session,
|
||||
console=ctx.console,
|
||||
action_summary=stripped,
|
||||
confirm_fn=ctx.confirm_fn,
|
||||
is_tty=ctx.is_tty,
|
||||
action_already_listed=ctx.action_already_listed,
|
||||
):
|
||||
ctx.session.record(
|
||||
"slash",
|
||||
stripped,
|
||||
ok=False,
|
||||
response_text=format_terminal_turn_outcome(stripped, kind="slash", ok=False),
|
||||
)
|
||||
return True
|
||||
|
||||
ctx.console.print(f"[bold]$ {escape(stripped)}[/bold]")
|
||||
_dispatch_and_translate_exit(
|
||||
stripped,
|
||||
ctx,
|
||||
policy_precleared=True,
|
||||
)
|
||||
agent_turn_executed_slashes(ctx.session).add(stripped)
|
||||
return True
|
||||
|
||||
|
||||
def run_slash(*, command: str, args: list[str] | None = None, context: Any) -> dict[str, Any]:
|
||||
return execute_with_action_context(
|
||||
{"command": command, "args": args or []},
|
||||
context,
|
||||
execute_slash_tool,
|
||||
)
|
||||
|
||||
|
||||
slash_invoke_tool = RegisteredTool(
|
||||
name="slash_invoke",
|
||||
description=slash_invoke_tool_description(),
|
||||
input_schema=slash_invoke_input_schema(),
|
||||
source="interactive_shell",
|
||||
surfaces=("action",),
|
||||
parallel_safe=False,
|
||||
accepts_runtime_context=True,
|
||||
run=run_slash,
|
||||
is_available=lambda sources: capability_available_from_sources(sources, "slash_commands"),
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["execute_slash_tool", "slash_invoke_tool"]
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Synthetic test tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from config.constants.paths import SYNTHETIC_SCENARIOS_DIR
|
||||
from core.agent_harness.tools.tool_context import (
|
||||
ActionToolContext,
|
||||
capability_available_from_sources,
|
||||
execute_with_action_context,
|
||||
object_schema,
|
||||
string_property,
|
||||
)
|
||||
from core.tool_framework.registered_tool import RegisteredTool
|
||||
from tools.interactive_shell.subprocess import require_subprocess_presenter
|
||||
from tools.interactive_shell.synthetic.runner import run_synthetic_test
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def list_rds_postgres_scenarios() -> tuple[str, ...]:
|
||||
"""Enumerate available RDS Postgres synthetic scenario directory names."""
|
||||
if not SYNTHETIC_SCENARIOS_DIR.is_dir():
|
||||
return ()
|
||||
return tuple(
|
||||
sorted(
|
||||
entry.name
|
||||
for entry in SYNTHETIC_SCENARIOS_DIR.iterdir()
|
||||
if entry.is_dir()
|
||||
and len(entry.name) >= 5
|
||||
and entry.name[:3].isdigit()
|
||||
and entry.name[3] == "-"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def execute_synthetic_tool(args: dict[str, Any], ctx: ActionToolContext) -> bool:
|
||||
suite = str(args.get("suite", "")).strip()
|
||||
scenario = str(args.get("scenario", "")).strip()
|
||||
if not suite or not scenario:
|
||||
return False
|
||||
run_synthetic_test(f"{suite}:{scenario}", require_subprocess_presenter(ctx))
|
||||
return True
|
||||
|
||||
|
||||
def run_synthetic(*, suite: str, scenario: str, context: Any) -> dict[str, Any]:
|
||||
return execute_with_action_context(
|
||||
{"suite": suite, "scenario": scenario},
|
||||
context,
|
||||
execute_synthetic_tool,
|
||||
)
|
||||
|
||||
|
||||
synthetic_run_tool = RegisteredTool(
|
||||
name="synthetic_run",
|
||||
description=(
|
||||
"Run a synthetic scenario in a suite. Match the scenario id exactly from "
|
||||
"the user request: a bare numeric prefix selects the enum value with that "
|
||||
'same prefix, e.g. "005" -> "005-failover" and "004" -> '
|
||||
'"004-cpu-saturation-bad-query". Never substitute a neighboring numbered '
|
||||
"scenario when the user supplied a numeric id."
|
||||
),
|
||||
input_schema=object_schema(
|
||||
properties={
|
||||
"suite": string_property(
|
||||
description="Synthetic suite name.",
|
||||
enum=("rds_postgres",),
|
||||
),
|
||||
"scenario": string_property(
|
||||
description=(
|
||||
"Synthetic scenario id within the selected suite or `all`. "
|
||||
"For bare numeric requests, use the enum value with the same "
|
||||
"three-digit prefix."
|
||||
),
|
||||
enum=("all", *list_rds_postgres_scenarios()),
|
||||
),
|
||||
},
|
||||
required=("suite", "scenario"),
|
||||
),
|
||||
source="interactive_shell",
|
||||
surfaces=("action",),
|
||||
parallel_safe=False,
|
||||
accepts_runtime_context=True,
|
||||
run=run_synthetic,
|
||||
is_available=lambda sources: capability_available_from_sources(sources, "synthetic_suites"),
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["execute_synthetic_tool", "list_rds_postgres_scenarios", "synthetic_run_tool"]
|
||||
@@ -0,0 +1,132 @@
|
||||
"""Task cancellation tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from rich.markup import escape
|
||||
|
||||
from core.agent_harness.tools.tool_context import (
|
||||
ActionToolContext,
|
||||
execute_with_action_context,
|
||||
object_schema,
|
||||
)
|
||||
from core.tool_framework.registered_tool import RegisteredTool
|
||||
from surfaces.interactive_shell.command_registry import dispatch_slash
|
||||
from surfaces.interactive_shell.runtime import TaskKind, TaskStatus
|
||||
from surfaces.interactive_shell.ui.execution_confirm import execution_allowed
|
||||
from tools.interactive_shell.shared import plan_foreground_tool
|
||||
|
||||
|
||||
def _running_task_matches(ctx: ActionToolContext, target: str) -> Sequence[object]:
|
||||
running = [
|
||||
task
|
||||
for task in ctx.session.task_registry.list_recent(n=50)
|
||||
if task.status == TaskStatus.RUNNING
|
||||
]
|
||||
if target == "synthetic_test":
|
||||
return [task for task in running if task.kind == TaskKind.SYNTHETIC_TEST]
|
||||
if target == "task":
|
||||
return running
|
||||
return []
|
||||
|
||||
|
||||
def _resolve_task_cancel_target(ctx: ActionToolContext, target: str) -> str | None:
|
||||
if target in {"synthetic_test", "task"}:
|
||||
matches = _running_task_matches(ctx, target)
|
||||
if not matches:
|
||||
ctx.console.print(
|
||||
f"[dim]no running {escape(target)} task found. use[/] [bold]/tasks[/bold]"
|
||||
)
|
||||
ctx.session.record("slash", f"/cancel {target}", ok=False)
|
||||
return None
|
||||
if len(matches) > 1:
|
||||
ids = ", ".join(str(getattr(task, "task_id", "")) for task in matches)
|
||||
ctx.console.print(
|
||||
f"[yellow]multiple running tasks match {escape(target)}:[/] "
|
||||
f"{escape(ids)} [dim](run /cancel <id>)[/]"
|
||||
)
|
||||
ctx.session.record("slash", f"/cancel {target}", ok=False)
|
||||
return None
|
||||
return str(getattr(matches[0], "task_id", ""))
|
||||
|
||||
candidates = ctx.session.task_registry.candidates(target)
|
||||
if not candidates:
|
||||
ctx.console.print(f"[red]no task matches id:[/] {escape(target)}")
|
||||
ctx.session.record("slash", f"/cancel {target}", ok=False)
|
||||
return None
|
||||
if len(candidates) > 1:
|
||||
ctx.console.print(
|
||||
f"[red]ambiguous id prefix:[/] {escape(target)} "
|
||||
f"[dim]({len(candidates)} matches — use a longer prefix)[/]"
|
||||
)
|
||||
ctx.session.record("slash", f"/cancel {target}", ok=False)
|
||||
return None
|
||||
return str(candidates[0].task_id)
|
||||
|
||||
|
||||
def execute_task_cancel_tool(args: dict[str, Any], ctx: ActionToolContext) -> bool:
|
||||
target = str(args.get("target", "")).strip()
|
||||
if not target:
|
||||
return False
|
||||
task_id = _resolve_task_cancel_target(ctx, target)
|
||||
if task_id is None:
|
||||
return True
|
||||
command = f"/cancel {task_id}"
|
||||
plan = plan_foreground_tool("slash", "slash")
|
||||
if not execution_allowed(
|
||||
plan.policy,
|
||||
session=ctx.session,
|
||||
console=ctx.console,
|
||||
action_summary=command,
|
||||
confirm_fn=ctx.confirm_fn,
|
||||
is_tty=ctx.is_tty,
|
||||
action_already_listed=ctx.action_already_listed,
|
||||
):
|
||||
ctx.session.record("slash", command, ok=False)
|
||||
return True
|
||||
ctx.console.print(f"[bold]$ {escape(command)}[/bold]")
|
||||
dispatch_slash(
|
||||
command,
|
||||
ctx.session,
|
||||
ctx.console,
|
||||
confirm_fn=ctx.confirm_fn,
|
||||
is_tty=ctx.is_tty,
|
||||
policy_precleared=True,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def run_task_cancel(*, target: str, context: Any) -> dict[str, Any]:
|
||||
return execute_with_action_context({"target": target}, context, execute_task_cancel_tool)
|
||||
|
||||
|
||||
task_cancel_tool = RegisteredTool(
|
||||
name="task_cancel",
|
||||
description="Cancel a running task by id or kind.",
|
||||
input_schema=object_schema(
|
||||
properties={
|
||||
"target": {
|
||||
"oneOf": [
|
||||
{"type": "string", "enum": ["synthetic_test", "task"]},
|
||||
{"type": "string", "pattern": "^[A-Za-z0-9_-]{3,}$"},
|
||||
],
|
||||
"description": (
|
||||
"Task selector: `synthetic_test` to cancel the one running synthetic task, "
|
||||
"`task` to cancel a single running task of any kind, or a task id/prefix "
|
||||
"for `/cancel <id>` resolution."
|
||||
),
|
||||
}
|
||||
},
|
||||
required=("target",),
|
||||
),
|
||||
source="interactive_shell",
|
||||
surfaces=("action",),
|
||||
parallel_safe=False,
|
||||
accepts_runtime_context=True,
|
||||
run=run_task_cancel,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["execute_task_cancel_tool", "task_cancel_tool"]
|
||||
Reference in New Issue
Block a user