bhu-cli 1.46.0__py3-none-any.whl
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.
- bhu_cli-1.46.0.dist-info/METADATA +210 -0
- bhu_cli-1.46.0.dist-info/RECORD +258 -0
- bhu_cli-1.46.0.dist-info/WHEEL +4 -0
- bhu_cli-1.46.0.dist-info/entry_points.txt +4 -0
- kimi_cli/CHANGELOG.md +1281 -0
- kimi_cli/__init__.py +29 -0
- kimi_cli/__main__.py +43 -0
- kimi_cli/_build_info.py +1 -0
- kimi_cli/acp/AGENTS.md +92 -0
- kimi_cli/acp/__init__.py +13 -0
- kimi_cli/acp/convert.py +128 -0
- kimi_cli/acp/kaos.py +291 -0
- kimi_cli/acp/mcp.py +46 -0
- kimi_cli/acp/server.py +468 -0
- kimi_cli/acp/session.py +499 -0
- kimi_cli/acp/tools.py +167 -0
- kimi_cli/acp/types.py +13 -0
- kimi_cli/acp/version.py +45 -0
- kimi_cli/agents/default/agent.yaml +36 -0
- kimi_cli/agents/default/coder.yaml +25 -0
- kimi_cli/agents/default/explore.yaml +46 -0
- kimi_cli/agents/default/plan.yaml +30 -0
- kimi_cli/agents/default/system.md +160 -0
- kimi_cli/agents/okabe/agent.yaml +22 -0
- kimi_cli/agentspec.py +160 -0
- kimi_cli/app.py +807 -0
- kimi_cli/approval_runtime/__init__.py +29 -0
- kimi_cli/approval_runtime/models.py +42 -0
- kimi_cli/approval_runtime/runtime.py +235 -0
- kimi_cli/auth/__init__.py +5 -0
- kimi_cli/auth/oauth.py +1092 -0
- kimi_cli/auth/platforms.py +374 -0
- kimi_cli/background/__init__.py +36 -0
- kimi_cli/background/agent_runner.py +231 -0
- kimi_cli/background/ids.py +19 -0
- kimi_cli/background/manager.py +725 -0
- kimi_cli/background/models.py +105 -0
- kimi_cli/background/store.py +237 -0
- kimi_cli/background/summary.py +66 -0
- kimi_cli/background/worker.py +209 -0
- kimi_cli/cli/__init__.py +1081 -0
- kimi_cli/cli/__main__.py +34 -0
- kimi_cli/cli/_lazy_group.py +238 -0
- kimi_cli/cli/export.py +322 -0
- kimi_cli/cli/info.py +62 -0
- kimi_cli/cli/mcp.py +353 -0
- kimi_cli/cli/plugin.py +347 -0
- kimi_cli/cli/toad.py +73 -0
- kimi_cli/cli/vis.py +38 -0
- kimi_cli/cli/web.py +80 -0
- kimi_cli/config.py +429 -0
- kimi_cli/constant.py +116 -0
- kimi_cli/exception.py +43 -0
- kimi_cli/hooks/__init__.py +24 -0
- kimi_cli/hooks/config.py +34 -0
- kimi_cli/hooks/defaults/rtk.py +48 -0
- kimi_cli/hooks/engine.py +394 -0
- kimi_cli/hooks/events.py +190 -0
- kimi_cli/hooks/runner.py +103 -0
- kimi_cli/llm.py +333 -0
- kimi_cli/mcp_oauth.py +72 -0
- kimi_cli/metadata.py +79 -0
- kimi_cli/notifications/__init__.py +33 -0
- kimi_cli/notifications/llm.py +77 -0
- kimi_cli/notifications/manager.py +145 -0
- kimi_cli/notifications/models.py +50 -0
- kimi_cli/notifications/notifier.py +41 -0
- kimi_cli/notifications/store.py +118 -0
- kimi_cli/notifications/wire.py +21 -0
- kimi_cli/plugin/__init__.py +124 -0
- kimi_cli/plugin/manager.py +153 -0
- kimi_cli/plugin/tool.py +173 -0
- kimi_cli/prompts/__init__.py +6 -0
- kimi_cli/prompts/compact.md +73 -0
- kimi_cli/prompts/init.md +21 -0
- kimi_cli/py.typed +0 -0
- kimi_cli/session.py +319 -0
- kimi_cli/session_fork.py +325 -0
- kimi_cli/session_state.py +132 -0
- kimi_cli/share.py +14 -0
- kimi_cli/skill/__init__.py +727 -0
- kimi_cli/skill/flow/__init__.py +99 -0
- kimi_cli/skill/flow/d2.py +482 -0
- kimi_cli/skill/flow/mermaid.py +266 -0
- kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
- kimi_cli/skills/skill-creator/SKILL.md +367 -0
- kimi_cli/soul/__init__.py +304 -0
- kimi_cli/soul/agent.py +519 -0
- kimi_cli/soul/approval.py +267 -0
- kimi_cli/soul/btw.py +237 -0
- kimi_cli/soul/compaction.py +189 -0
- kimi_cli/soul/context.py +339 -0
- kimi_cli/soul/denwarenji.py +39 -0
- kimi_cli/soul/dynamic_injection.py +84 -0
- kimi_cli/soul/dynamic_injections/__init__.py +0 -0
- kimi_cli/soul/dynamic_injections/afk_mode.py +74 -0
- kimi_cli/soul/dynamic_injections/plan_mode.py +245 -0
- kimi_cli/soul/kimisoul.py +1710 -0
- kimi_cli/soul/message.py +92 -0
- kimi_cli/soul/slash.py +341 -0
- kimi_cli/soul/toolset.py +891 -0
- kimi_cli/subagents/__init__.py +21 -0
- kimi_cli/subagents/builder.py +42 -0
- kimi_cli/subagents/core.py +86 -0
- kimi_cli/subagents/git_context.py +170 -0
- kimi_cli/subagents/models.py +54 -0
- kimi_cli/subagents/output.py +71 -0
- kimi_cli/subagents/registry.py +28 -0
- kimi_cli/subagents/runner.py +428 -0
- kimi_cli/subagents/store.py +196 -0
- kimi_cli/telemetry/__init__.py +197 -0
- kimi_cli/telemetry/crash.py +149 -0
- kimi_cli/telemetry/sink.py +143 -0
- kimi_cli/telemetry/transport.py +318 -0
- kimi_cli/tools/AGENTS.md +5 -0
- kimi_cli/tools/__init__.py +105 -0
- kimi_cli/tools/agent/__init__.py +277 -0
- kimi_cli/tools/agent/description.md +41 -0
- kimi_cli/tools/ask_user/__init__.py +154 -0
- kimi_cli/tools/ask_user/description.md +19 -0
- kimi_cli/tools/background/__init__.py +318 -0
- kimi_cli/tools/background/list.md +10 -0
- kimi_cli/tools/background/output.md +11 -0
- kimi_cli/tools/background/stop.md +8 -0
- kimi_cli/tools/display.py +46 -0
- kimi_cli/tools/dmail/__init__.py +38 -0
- kimi_cli/tools/dmail/dmail.md +17 -0
- kimi_cli/tools/file/__init__.py +30 -0
- kimi_cli/tools/file/glob.md +21 -0
- kimi_cli/tools/file/glob.py +178 -0
- kimi_cli/tools/file/grep.md +6 -0
- kimi_cli/tools/file/grep_local.py +590 -0
- kimi_cli/tools/file/plan_mode.py +45 -0
- kimi_cli/tools/file/read.md +16 -0
- kimi_cli/tools/file/read.py +300 -0
- kimi_cli/tools/file/read_media.md +24 -0
- kimi_cli/tools/file/read_media.py +217 -0
- kimi_cli/tools/file/replace.md +7 -0
- kimi_cli/tools/file/replace.py +195 -0
- kimi_cli/tools/file/utils.py +257 -0
- kimi_cli/tools/file/write.md +5 -0
- kimi_cli/tools/file/write.py +177 -0
- kimi_cli/tools/plan/__init__.py +339 -0
- kimi_cli/tools/plan/description.md +29 -0
- kimi_cli/tools/plan/enter.py +197 -0
- kimi_cli/tools/plan/enter_description.md +35 -0
- kimi_cli/tools/plan/heroes.py +277 -0
- kimi_cli/tools/shell/__init__.py +260 -0
- kimi_cli/tools/shell/bash.md +35 -0
- kimi_cli/tools/test.py +55 -0
- kimi_cli/tools/think/__init__.py +21 -0
- kimi_cli/tools/think/think.md +1 -0
- kimi_cli/tools/todo/__init__.py +168 -0
- kimi_cli/tools/todo/set_todo_list.md +23 -0
- kimi_cli/tools/utils.py +199 -0
- kimi_cli/tools/web/__init__.py +4 -0
- kimi_cli/tools/web/fetch.md +1 -0
- kimi_cli/tools/web/fetch.py +189 -0
- kimi_cli/tools/web/search.md +1 -0
- kimi_cli/tools/web/search.py +163 -0
- kimi_cli/ui/__init__.py +0 -0
- kimi_cli/ui/acp/__init__.py +99 -0
- kimi_cli/ui/print/__init__.py +474 -0
- kimi_cli/ui/print/visualize.py +194 -0
- kimi_cli/ui/shell/__init__.py +1540 -0
- kimi_cli/ui/shell/console.py +109 -0
- kimi_cli/ui/shell/debug.py +190 -0
- kimi_cli/ui/shell/echo.py +17 -0
- kimi_cli/ui/shell/export_import.py +117 -0
- kimi_cli/ui/shell/keyboard.py +300 -0
- kimi_cli/ui/shell/mcp_status.py +111 -0
- kimi_cli/ui/shell/oauth.py +149 -0
- kimi_cli/ui/shell/placeholders.py +531 -0
- kimi_cli/ui/shell/prompt.py +2259 -0
- kimi_cli/ui/shell/replay.py +215 -0
- kimi_cli/ui/shell/session_picker.py +227 -0
- kimi_cli/ui/shell/setup.py +212 -0
- kimi_cli/ui/shell/slash.py +893 -0
- kimi_cli/ui/shell/startup.py +32 -0
- kimi_cli/ui/shell/task_browser.py +486 -0
- kimi_cli/ui/shell/update.py +349 -0
- kimi_cli/ui/shell/usage.py +295 -0
- kimi_cli/ui/shell/visualize/__init__.py +165 -0
- kimi_cli/ui/shell/visualize/_approval_panel.py +505 -0
- kimi_cli/ui/shell/visualize/_blocks.py +640 -0
- kimi_cli/ui/shell/visualize/_btw_panel.py +224 -0
- kimi_cli/ui/shell/visualize/_input_router.py +48 -0
- kimi_cli/ui/shell/visualize/_interactive.py +524 -0
- kimi_cli/ui/shell/visualize/_live_view.py +902 -0
- kimi_cli/ui/shell/visualize/_question_panel.py +586 -0
- kimi_cli/ui/theme.py +241 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +24 -0
- kimi_cli/utils/aioqueue.py +72 -0
- kimi_cli/utils/broadcast.py +37 -0
- kimi_cli/utils/changelog.py +108 -0
- kimi_cli/utils/clipboard.py +246 -0
- kimi_cli/utils/datetime.py +64 -0
- kimi_cli/utils/diff.py +135 -0
- kimi_cli/utils/editor.py +91 -0
- kimi_cli/utils/environment.py +225 -0
- kimi_cli/utils/envvar.py +22 -0
- kimi_cli/utils/export.py +696 -0
- kimi_cli/utils/file_filter.py +367 -0
- kimi_cli/utils/frontmatter.py +70 -0
- kimi_cli/utils/io.py +27 -0
- kimi_cli/utils/logging.py +124 -0
- kimi_cli/utils/media_tags.py +29 -0
- kimi_cli/utils/message.py +24 -0
- kimi_cli/utils/path.py +245 -0
- kimi_cli/utils/proctitle.py +33 -0
- kimi_cli/utils/proxy.py +31 -0
- kimi_cli/utils/pyinstaller.py +43 -0
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/columns.py +99 -0
- kimi_cli/utils/rich/diff_render.py +481 -0
- kimi_cli/utils/rich/markdown.py +898 -0
- kimi_cli/utils/rich/markdown_sample.md +108 -0
- kimi_cli/utils/rich/markdown_sample_short.md +2 -0
- kimi_cli/utils/rich/syntax.py +114 -0
- kimi_cli/utils/sensitive.py +54 -0
- kimi_cli/utils/server.py +121 -0
- kimi_cli/utils/shell_quoting.py +38 -0
- kimi_cli/utils/signals.py +43 -0
- kimi_cli/utils/slashcmd.py +145 -0
- kimi_cli/utils/string.py +41 -0
- kimi_cli/utils/subprocess_env.py +73 -0
- kimi_cli/utils/term.py +168 -0
- kimi_cli/utils/typing.py +20 -0
- kimi_cli/utils/windows_paths.py +48 -0
- kimi_cli/vis/__init__.py +0 -0
- kimi_cli/vis/api/__init__.py +5 -0
- kimi_cli/vis/api/sessions.py +687 -0
- kimi_cli/vis/api/statistics.py +209 -0
- kimi_cli/vis/api/system.py +19 -0
- kimi_cli/vis/app.py +175 -0
- kimi_cli/web/__init__.py +5 -0
- kimi_cli/web/api/__init__.py +15 -0
- kimi_cli/web/api/config.py +208 -0
- kimi_cli/web/api/open_in.py +197 -0
- kimi_cli/web/api/sessions.py +1223 -0
- kimi_cli/web/app.py +451 -0
- kimi_cli/web/auth.py +191 -0
- kimi_cli/web/models.py +98 -0
- kimi_cli/web/runner/__init__.py +5 -0
- kimi_cli/web/runner/messages.py +57 -0
- kimi_cli/web/runner/process.py +754 -0
- kimi_cli/web/runner/worker.py +95 -0
- kimi_cli/web/store/__init__.py +1 -0
- kimi_cli/web/store/sessions.py +432 -0
- kimi_cli/wire/__init__.py +148 -0
- kimi_cli/wire/file.py +151 -0
- kimi_cli/wire/jsonrpc.py +263 -0
- kimi_cli/wire/protocol.py +2 -0
- kimi_cli/wire/root_hub.py +27 -0
- kimi_cli/wire/serde.py +26 -0
- kimi_cli/wire/server.py +1060 -0
- kimi_cli/wire/types.py +717 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from kimi_cli.approval_runtime.models import (
|
|
2
|
+
ApprovalRequestRecord,
|
|
3
|
+
ApprovalResponseKind,
|
|
4
|
+
ApprovalRuntimeEvent,
|
|
5
|
+
ApprovalSource,
|
|
6
|
+
ApprovalSourceKind,
|
|
7
|
+
ApprovalStatus,
|
|
8
|
+
)
|
|
9
|
+
from kimi_cli.approval_runtime.runtime import (
|
|
10
|
+
ApprovalCancelledError,
|
|
11
|
+
ApprovalRuntime,
|
|
12
|
+
get_current_approval_source_or_none,
|
|
13
|
+
reset_current_approval_source,
|
|
14
|
+
set_current_approval_source,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ApprovalCancelledError",
|
|
19
|
+
"ApprovalRequestRecord",
|
|
20
|
+
"ApprovalResponseKind",
|
|
21
|
+
"ApprovalRuntime",
|
|
22
|
+
"ApprovalRuntimeEvent",
|
|
23
|
+
"ApprovalSource",
|
|
24
|
+
"ApprovalSourceKind",
|
|
25
|
+
"ApprovalStatus",
|
|
26
|
+
"get_current_approval_source_or_none",
|
|
27
|
+
"reset_current_approval_source",
|
|
28
|
+
"set_current_approval_source",
|
|
29
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from kimi_cli.wire.types import DisplayBlock
|
|
8
|
+
|
|
9
|
+
type ApprovalResponseKind = Literal["approve", "approve_for_session", "reject"]
|
|
10
|
+
type ApprovalSourceKind = Literal["foreground_turn", "background_agent"]
|
|
11
|
+
type ApprovalStatus = Literal["pending", "resolved", "cancelled"]
|
|
12
|
+
type ApprovalRuntimeEventKind = Literal["request_created", "request_resolved"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
16
|
+
class ApprovalSource:
|
|
17
|
+
kind: ApprovalSourceKind
|
|
18
|
+
id: str
|
|
19
|
+
agent_id: str | None = None
|
|
20
|
+
subagent_type: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True, kw_only=True)
|
|
24
|
+
class ApprovalRequestRecord:
|
|
25
|
+
id: str
|
|
26
|
+
tool_call_id: str
|
|
27
|
+
sender: str
|
|
28
|
+
action: str
|
|
29
|
+
description: str
|
|
30
|
+
display: list[DisplayBlock]
|
|
31
|
+
source: ApprovalSource
|
|
32
|
+
created_at: float = field(default_factory=time.time)
|
|
33
|
+
status: ApprovalStatus = "pending"
|
|
34
|
+
resolved_at: float | None = None
|
|
35
|
+
response: ApprovalResponseKind | None = None
|
|
36
|
+
feedback: str = ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
40
|
+
class ApprovalRuntimeEvent:
|
|
41
|
+
kind: ApprovalRuntimeEventKind
|
|
42
|
+
request: ApprovalRequestRecord
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import uuid
|
|
5
|
+
from contextvars import ContextVar, Token
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from kimi_cli.utils.logging import logger
|
|
9
|
+
from kimi_cli.wire.types import ApprovalRequest, ApprovalResponse
|
|
10
|
+
|
|
11
|
+
from .models import (
|
|
12
|
+
ApprovalRequestRecord,
|
|
13
|
+
ApprovalResponseKind,
|
|
14
|
+
ApprovalRuntimeEvent,
|
|
15
|
+
ApprovalSource,
|
|
16
|
+
ApprovalSourceKind,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
|
|
22
|
+
from kimi_cli.wire.root_hub import RootWireHub
|
|
23
|
+
from kimi_cli.wire.types import DisplayBlock
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ApprovalCancelledError(Exception):
|
|
27
|
+
"""Raised when a pending approval is cancelled by its source lifecycle."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_current_approval_source = ContextVar[ApprovalSource | None](
|
|
31
|
+
"current_approval_source",
|
|
32
|
+
default=None,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_current_approval_source_or_none() -> ApprovalSource | None:
|
|
37
|
+
return _current_approval_source.get()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def set_current_approval_source(source: ApprovalSource) -> Token[ApprovalSource | None]:
|
|
41
|
+
return _current_approval_source.set(source)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def reset_current_approval_source(token: Token[ApprovalSource | None]) -> None:
|
|
45
|
+
_current_approval_source.reset(token)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ApprovalRuntime:
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self._requests: dict[str, ApprovalRequestRecord] = {}
|
|
51
|
+
self._waiters: dict[str, asyncio.Future[tuple[ApprovalResponseKind, str]]] = {}
|
|
52
|
+
self._waiter_counts: dict[str, int] = {}
|
|
53
|
+
self._subscribers: dict[str, Callable[[ApprovalRuntimeEvent], None]] = {}
|
|
54
|
+
self._root_wire_hub: RootWireHub | None = None
|
|
55
|
+
|
|
56
|
+
def bind_root_wire_hub(self, root_wire_hub: RootWireHub) -> None:
|
|
57
|
+
if self._root_wire_hub is root_wire_hub:
|
|
58
|
+
return
|
|
59
|
+
self._root_wire_hub = root_wire_hub
|
|
60
|
+
|
|
61
|
+
def create_request(
|
|
62
|
+
self,
|
|
63
|
+
*,
|
|
64
|
+
sender: str,
|
|
65
|
+
action: str,
|
|
66
|
+
description: str,
|
|
67
|
+
tool_call_id: str,
|
|
68
|
+
display: list[DisplayBlock],
|
|
69
|
+
source: ApprovalSource,
|
|
70
|
+
request_id: str | None = None,
|
|
71
|
+
) -> ApprovalRequestRecord:
|
|
72
|
+
request = ApprovalRequestRecord(
|
|
73
|
+
id=request_id or str(uuid.uuid4()),
|
|
74
|
+
tool_call_id=tool_call_id,
|
|
75
|
+
sender=sender,
|
|
76
|
+
action=action,
|
|
77
|
+
description=description,
|
|
78
|
+
display=display,
|
|
79
|
+
source=source,
|
|
80
|
+
)
|
|
81
|
+
self._requests[request.id] = request
|
|
82
|
+
self._publish_event(ApprovalRuntimeEvent(kind="request_created", request=request))
|
|
83
|
+
self._publish_wire_request(request)
|
|
84
|
+
return request
|
|
85
|
+
|
|
86
|
+
async def wait_for_response(
|
|
87
|
+
self, request_id: str, timeout: float | None = None
|
|
88
|
+
) -> tuple[ApprovalResponseKind, str]:
|
|
89
|
+
waiter = self._waiters.get(request_id)
|
|
90
|
+
request = self._requests.get(request_id)
|
|
91
|
+
if request is None:
|
|
92
|
+
raise KeyError(f"Approval request not found: {request_id}")
|
|
93
|
+
if waiter is None:
|
|
94
|
+
if request.status == "cancelled":
|
|
95
|
+
raise ApprovalCancelledError(request_id)
|
|
96
|
+
if request.status == "resolved":
|
|
97
|
+
assert request.response is not None
|
|
98
|
+
return request.response, request.feedback
|
|
99
|
+
waiter = asyncio.get_running_loop().create_future()
|
|
100
|
+
self._waiters[request_id] = waiter
|
|
101
|
+
self._waiter_counts[request_id] = self._waiter_counts.get(request_id, 0) + 1
|
|
102
|
+
try:
|
|
103
|
+
if timeout is None:
|
|
104
|
+
return await asyncio.shield(waiter)
|
|
105
|
+
return await asyncio.wait_for(asyncio.shield(waiter), timeout=timeout)
|
|
106
|
+
except TimeoutError:
|
|
107
|
+
logger.warning(
|
|
108
|
+
"Approval request {id} timed out after {t}s",
|
|
109
|
+
id=request_id,
|
|
110
|
+
t=timeout,
|
|
111
|
+
)
|
|
112
|
+
# If this timeout is the only remaining observer, drop the shared
|
|
113
|
+
# waiter before cancelling so we do not later set_exception on an
|
|
114
|
+
# unobserved future. If other observers still exist, keep the
|
|
115
|
+
# shared waiter registered so they receive the cancellation too.
|
|
116
|
+
if self._waiter_counts.get(request_id, 0) <= 1:
|
|
117
|
+
self._waiters.pop(request_id, None)
|
|
118
|
+
self._cancel_request(request_id, feedback="approval timed out")
|
|
119
|
+
raise ApprovalCancelledError(request_id) from None
|
|
120
|
+
finally:
|
|
121
|
+
remaining = self._waiter_counts.get(request_id, 0) - 1
|
|
122
|
+
if remaining > 0:
|
|
123
|
+
self._waiter_counts[request_id] = remaining
|
|
124
|
+
else:
|
|
125
|
+
self._waiter_counts.pop(request_id, None)
|
|
126
|
+
if request.status == "pending" and self._waiters.get(request_id) is waiter:
|
|
127
|
+
self._waiters.pop(request_id, None)
|
|
128
|
+
|
|
129
|
+
def resolve(self, request_id: str, response: ApprovalResponseKind, feedback: str = "") -> bool:
|
|
130
|
+
request = self._requests.get(request_id)
|
|
131
|
+
if request is None or request.status != "pending":
|
|
132
|
+
return False
|
|
133
|
+
request.status = "resolved"
|
|
134
|
+
request.response = response
|
|
135
|
+
request.feedback = feedback
|
|
136
|
+
import time
|
|
137
|
+
|
|
138
|
+
request.resolved_at = time.time()
|
|
139
|
+
waiter = self._waiters.pop(request_id, None)
|
|
140
|
+
if waiter is not None and not waiter.done():
|
|
141
|
+
waiter.set_result((response, feedback))
|
|
142
|
+
self._publish_event(ApprovalRuntimeEvent(kind="request_resolved", request=request))
|
|
143
|
+
self._publish_wire_response(request_id, response, feedback)
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
def _cancel_request(self, request_id: str, feedback: str = "") -> None:
|
|
147
|
+
"""Cancel a single pending request by ID."""
|
|
148
|
+
import time
|
|
149
|
+
|
|
150
|
+
request = self._requests.get(request_id)
|
|
151
|
+
if request is None or request.status != "pending":
|
|
152
|
+
return
|
|
153
|
+
request.status = "cancelled"
|
|
154
|
+
request.response = "reject"
|
|
155
|
+
request.feedback = feedback
|
|
156
|
+
request.resolved_at = time.time()
|
|
157
|
+
waiter = self._waiters.pop(request_id, None)
|
|
158
|
+
if waiter is not None and not waiter.done():
|
|
159
|
+
waiter.set_exception(ApprovalCancelledError(request_id))
|
|
160
|
+
self._publish_event(ApprovalRuntimeEvent(kind="request_resolved", request=request))
|
|
161
|
+
self._publish_wire_response(request_id, "reject", feedback)
|
|
162
|
+
|
|
163
|
+
def cancel_by_source(self, source_kind: ApprovalSourceKind, source_id: str) -> int:
|
|
164
|
+
cancelled = 0
|
|
165
|
+
import time
|
|
166
|
+
|
|
167
|
+
for request_id, request in self._requests.items():
|
|
168
|
+
if request.status != "pending":
|
|
169
|
+
continue
|
|
170
|
+
if request.source.kind != source_kind or request.source.id != source_id:
|
|
171
|
+
continue
|
|
172
|
+
request.status = "cancelled"
|
|
173
|
+
request.response = "reject"
|
|
174
|
+
request.resolved_at = time.time()
|
|
175
|
+
waiter = self._waiters.pop(request_id, None)
|
|
176
|
+
if waiter is not None and not waiter.done():
|
|
177
|
+
waiter.set_exception(ApprovalCancelledError(request_id))
|
|
178
|
+
self._publish_event(ApprovalRuntimeEvent(kind="request_resolved", request=request))
|
|
179
|
+
self._publish_wire_response(request_id, "reject")
|
|
180
|
+
cancelled += 1
|
|
181
|
+
return cancelled
|
|
182
|
+
|
|
183
|
+
def list_pending(self) -> list[ApprovalRequestRecord]:
|
|
184
|
+
pending = [request for request in self._requests.values() if request.status == "pending"]
|
|
185
|
+
pending.sort(key=lambda request: request.created_at)
|
|
186
|
+
return pending
|
|
187
|
+
|
|
188
|
+
def get_request(self, request_id: str) -> ApprovalRequestRecord | None:
|
|
189
|
+
return self._requests.get(request_id)
|
|
190
|
+
|
|
191
|
+
def subscribe(self, callback: Callable[[ApprovalRuntimeEvent], None]) -> str:
|
|
192
|
+
token = uuid.uuid4().hex
|
|
193
|
+
self._subscribers[token] = callback
|
|
194
|
+
return token
|
|
195
|
+
|
|
196
|
+
def unsubscribe(self, token: str) -> None:
|
|
197
|
+
self._subscribers.pop(token, None)
|
|
198
|
+
|
|
199
|
+
def _publish_event(self, event: ApprovalRuntimeEvent) -> None:
|
|
200
|
+
for callback in list(self._subscribers.values()):
|
|
201
|
+
try:
|
|
202
|
+
callback(event)
|
|
203
|
+
except Exception:
|
|
204
|
+
logger.exception("Approval runtime event subscriber failed")
|
|
205
|
+
|
|
206
|
+
def _publish_wire_request(self, request: ApprovalRequestRecord) -> None:
|
|
207
|
+
if self._root_wire_hub is None:
|
|
208
|
+
return
|
|
209
|
+
self._root_wire_hub.publish_nowait(
|
|
210
|
+
ApprovalRequest(
|
|
211
|
+
id=request.id,
|
|
212
|
+
tool_call_id=request.tool_call_id,
|
|
213
|
+
sender=request.sender,
|
|
214
|
+
action=request.action,
|
|
215
|
+
description=request.description,
|
|
216
|
+
display=request.display,
|
|
217
|
+
source_kind=request.source.kind,
|
|
218
|
+
source_id=request.source.id,
|
|
219
|
+
agent_id=request.source.agent_id,
|
|
220
|
+
subagent_type=request.source.subagent_type,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def _publish_wire_response(
|
|
225
|
+
self, request_id: str, response: ApprovalResponseKind, feedback: str = ""
|
|
226
|
+
) -> None:
|
|
227
|
+
if self._root_wire_hub is None:
|
|
228
|
+
return
|
|
229
|
+
self._root_wire_hub.publish_nowait(
|
|
230
|
+
ApprovalResponse(
|
|
231
|
+
request_id=request_id,
|
|
232
|
+
response=response,
|
|
233
|
+
feedback=feedback,
|
|
234
|
+
)
|
|
235
|
+
)
|