iac-code 0.1.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.
- iac_code/__init__.py +2 -0
- iac_code/acp/__init__.py +97 -0
- iac_code/acp/convert.py +423 -0
- iac_code/acp/http_sse.py +448 -0
- iac_code/acp/mcp.py +54 -0
- iac_code/acp/metrics.py +71 -0
- iac_code/acp/server.py +662 -0
- iac_code/acp/session.py +446 -0
- iac_code/acp/slash_registry.py +125 -0
- iac_code/acp/state.py +99 -0
- iac_code/acp/tools.py +112 -0
- iac_code/acp/types.py +13 -0
- iac_code/acp/version.py +26 -0
- iac_code/agent/__init__.py +19 -0
- iac_code/agent/agent_loop.py +640 -0
- iac_code/agent/agent_tool.py +269 -0
- iac_code/agent/agent_types.py +87 -0
- iac_code/agent/message.py +153 -0
- iac_code/agent/system_prompt.py +313 -0
- iac_code/cli/__init__.py +3 -0
- iac_code/cli/headless.py +114 -0
- iac_code/cli/main.py +246 -0
- iac_code/cli/output_formats.py +125 -0
- iac_code/commands/__init__.py +93 -0
- iac_code/commands/auth.py +1055 -0
- iac_code/commands/clear.py +34 -0
- iac_code/commands/compact.py +43 -0
- iac_code/commands/debug.py +45 -0
- iac_code/commands/effort.py +116 -0
- iac_code/commands/exit.py +10 -0
- iac_code/commands/help.py +49 -0
- iac_code/commands/model.py +130 -0
- iac_code/commands/registry.py +245 -0
- iac_code/commands/resume.py +49 -0
- iac_code/commands/tasks.py +41 -0
- iac_code/config.py +304 -0
- iac_code/i18n/__init__.py +141 -0
- iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +1355 -0
- iac_code/memory/__init__.py +1 -0
- iac_code/memory/memory_manager.py +92 -0
- iac_code/memory/memory_tools.py +88 -0
- iac_code/providers/__init__.py +1 -0
- iac_code/providers/anthropic_provider.py +284 -0
- iac_code/providers/base.py +128 -0
- iac_code/providers/dashscope_provider.py +47 -0
- iac_code/providers/deepseek_provider.py +36 -0
- iac_code/providers/manager.py +399 -0
- iac_code/providers/openai_provider.py +344 -0
- iac_code/providers/retry.py +58 -0
- iac_code/providers/stream_watchdog.py +47 -0
- iac_code/providers/thinking.py +164 -0
- iac_code/services/__init__.py +1 -0
- iac_code/services/agent_factory.py +127 -0
- iac_code/services/cloud_credentials.py +22 -0
- iac_code/services/context_manager.py +221 -0
- iac_code/services/providers/__init__.py +1 -0
- iac_code/services/providers/aliyun.py +232 -0
- iac_code/services/session_index.py +281 -0
- iac_code/services/session_storage.py +245 -0
- iac_code/services/telemetry/__init__.py +66 -0
- iac_code/services/telemetry/attributes.py +84 -0
- iac_code/services/telemetry/client.py +330 -0
- iac_code/services/telemetry/config.py +76 -0
- iac_code/services/telemetry/constants.py +75 -0
- iac_code/services/telemetry/content_serializer.py +124 -0
- iac_code/services/telemetry/events.py +42 -0
- iac_code/services/telemetry/fallback.py +59 -0
- iac_code/services/telemetry/identity.py +73 -0
- iac_code/services/telemetry/metrics.py +62 -0
- iac_code/services/telemetry/names.py +199 -0
- iac_code/services/telemetry/sanitize.py +88 -0
- iac_code/services/telemetry/sink.py +67 -0
- iac_code/services/telemetry/tracing.py +38 -0
- iac_code/services/telemetry/types.py +13 -0
- iac_code/services/token_budget.py +54 -0
- iac_code/services/token_counter.py +76 -0
- iac_code/skills/__init__.py +1 -0
- iac_code/skills/bundled/__init__.py +94 -0
- iac_code/skills/bundled/iac_aliyun/SKILL.md +192 -0
- iac_code/skills/bundled/iac_aliyun/__init__.py +16 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/ecs.md +167 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/oss.md +69 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/rds.md +95 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/redis.md +100 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/slb.md +60 -0
- iac_code/skills/bundled/iac_aliyun/references/cloud-products/vpc.md +54 -0
- iac_code/skills/bundled/iac_aliyun/references/ros-template.md +155 -0
- iac_code/skills/bundled/iac_aliyun/references/template-parameters.md +206 -0
- iac_code/skills/bundled/iac_aliyun/references/terraform-template.md +101 -0
- iac_code/skills/bundled/iac_aliyun/scripts/tf2ros.py +77 -0
- iac_code/skills/bundled/simplify.py +28 -0
- iac_code/skills/discovery.py +136 -0
- iac_code/skills/frontmatter.py +119 -0
- iac_code/skills/listing.py +92 -0
- iac_code/skills/loader.py +42 -0
- iac_code/skills/processor.py +81 -0
- iac_code/skills/renderer.py +157 -0
- iac_code/skills/skill_definition.py +82 -0
- iac_code/skills/skill_tool.py +261 -0
- iac_code/state/__init__.py +5 -0
- iac_code/state/app_state.py +122 -0
- iac_code/tasks/__init__.py +1 -0
- iac_code/tasks/notification_queue.py +28 -0
- iac_code/tasks/task_state.py +66 -0
- iac_code/tasks/task_tools.py +114 -0
- iac_code/tools/__init__.py +8 -0
- iac_code/tools/base.py +226 -0
- iac_code/tools/bash.py +133 -0
- iac_code/tools/cloud/__init__.py +0 -0
- iac_code/tools/cloud/aliyun/__init__.py +0 -0
- iac_code/tools/cloud/aliyun/aliyun_api.py +510 -0
- iac_code/tools/cloud/aliyun/aliyun_doc_search.py +145 -0
- iac_code/tools/cloud/aliyun/endpoints.yml +343 -0
- iac_code/tools/cloud/aliyun/ros_client.py +56 -0
- iac_code/tools/cloud/aliyun/ros_stack.py +633 -0
- iac_code/tools/cloud/aliyun/ros_stack_instances.py +247 -0
- iac_code/tools/cloud/base_api.py +162 -0
- iac_code/tools/cloud/base_stack.py +242 -0
- iac_code/tools/cloud/registry.py +20 -0
- iac_code/tools/cloud/types.py +105 -0
- iac_code/tools/edit_file.py +121 -0
- iac_code/tools/glob.py +103 -0
- iac_code/tools/grep.py +254 -0
- iac_code/tools/list_files.py +104 -0
- iac_code/tools/read_file.py +127 -0
- iac_code/tools/result_storage.py +39 -0
- iac_code/tools/tool_executor.py +165 -0
- iac_code/tools/web_fetch.py +177 -0
- iac_code/tools/write_file.py +88 -0
- iac_code/types/__init__.py +40 -0
- iac_code/types/permissions.py +26 -0
- iac_code/types/skill_source.py +11 -0
- iac_code/types/stream_events.py +227 -0
- iac_code/ui/__init__.py +5 -0
- iac_code/ui/banner.py +110 -0
- iac_code/ui/components/__init__.py +0 -0
- iac_code/ui/components/dialog.py +142 -0
- iac_code/ui/components/divider.py +20 -0
- iac_code/ui/components/fuzzy_picker.py +308 -0
- iac_code/ui/components/progress_bar.py +54 -0
- iac_code/ui/components/search_box.py +165 -0
- iac_code/ui/components/select.py +319 -0
- iac_code/ui/components/status_icon.py +42 -0
- iac_code/ui/components/tabs.py +128 -0
- iac_code/ui/core/__init__.py +0 -0
- iac_code/ui/core/in_place_render.py +129 -0
- iac_code/ui/core/input_history.py +118 -0
- iac_code/ui/core/key_event.py +41 -0
- iac_code/ui/core/prompt_input.py +507 -0
- iac_code/ui/core/raw_input.py +302 -0
- iac_code/ui/core/screen.py +80 -0
- iac_code/ui/dialogs/__init__.py +0 -0
- iac_code/ui/dialogs/global_search.py +178 -0
- iac_code/ui/dialogs/history_search.py +100 -0
- iac_code/ui/dialogs/model_picker.py +280 -0
- iac_code/ui/dialogs/quick_open.py +108 -0
- iac_code/ui/dialogs/resume_picker.py +749 -0
- iac_code/ui/keybindings/__init__.py +0 -0
- iac_code/ui/keybindings/manager.py +124 -0
- iac_code/ui/renderer.py +1535 -0
- iac_code/ui/repl.py +772 -0
- iac_code/ui/spinner.py +112 -0
- iac_code/ui/suggestions/__init__.py +0 -0
- iac_code/ui/suggestions/aggregator.py +171 -0
- iac_code/ui/suggestions/command_provider.py +43 -0
- iac_code/ui/suggestions/directory_provider.py +95 -0
- iac_code/ui/suggestions/file_provider.py +121 -0
- iac_code/ui/suggestions/shell_history_provider.py +108 -0
- iac_code/ui/suggestions/token_extractor.py +77 -0
- iac_code/ui/suggestions/types.py +45 -0
- iac_code/ui/transcript_view.py +199 -0
- iac_code/utils/__init__.py +0 -0
- iac_code/utils/background_housekeeping.py +53 -0
- iac_code/utils/cleanup.py +68 -0
- iac_code/utils/json_utils.py +60 -0
- iac_code/utils/log.py +150 -0
- iac_code/utils/project_paths.py +74 -0
- iac_code/utils/tool_input_parser.py +62 -0
- iac_code-0.1.0.dist-info/LICENSE +201 -0
- iac_code-0.1.0.dist-info/METADATA +64 -0
- iac_code-0.1.0.dist-info/RECORD +184 -0
- iac_code-0.1.0.dist-info/WHEEL +5 -0
- iac_code-0.1.0.dist-info/entry_points.txt +2 -0
- iac_code-0.1.0.dist-info/top_level.txt +1 -0
iac_code/__init__.py
ADDED
iac_code/acp/__init__.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def acp_main(*, debug: bool = False) -> None:
|
|
10
|
+
"""Run iac-code as an ACP stdio server."""
|
|
11
|
+
import asyncio
|
|
12
|
+
import signal
|
|
13
|
+
|
|
14
|
+
import acp
|
|
15
|
+
|
|
16
|
+
from iac_code.acp.server import ACPServer
|
|
17
|
+
from iac_code.utils.log import setup_logging
|
|
18
|
+
|
|
19
|
+
# Configure logging *before* the event loop starts so startup-time
|
|
20
|
+
# messages obey the ``--debug`` flag too.
|
|
21
|
+
setup_logging(session_id="acp", debug=debug)
|
|
22
|
+
_apply_stdlib_log_level(debug)
|
|
23
|
+
|
|
24
|
+
async def _run() -> None:
|
|
25
|
+
server = ACPServer()
|
|
26
|
+
shutdown_event = asyncio.Event()
|
|
27
|
+
|
|
28
|
+
def _signal_handler() -> None:
|
|
29
|
+
logger.info("Received shutdown signal, initiating graceful shutdown...")
|
|
30
|
+
shutdown_event.set()
|
|
31
|
+
|
|
32
|
+
loop = asyncio.get_running_loop()
|
|
33
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
34
|
+
loop.add_signal_handler(sig, _signal_handler)
|
|
35
|
+
|
|
36
|
+
agent_task = asyncio.create_task(acp.run_agent(server, use_unstable_protocol=True))
|
|
37
|
+
shutdown_task = asyncio.create_task(shutdown_event.wait())
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
# Wait for either the agent to finish or a shutdown signal
|
|
41
|
+
done, pending = await asyncio.wait(
|
|
42
|
+
{agent_task, shutdown_task},
|
|
43
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if shutdown_event.is_set() and not agent_task.done():
|
|
47
|
+
agent_task.cancel()
|
|
48
|
+
try:
|
|
49
|
+
await agent_task
|
|
50
|
+
except asyncio.CancelledError:
|
|
51
|
+
pass
|
|
52
|
+
finally:
|
|
53
|
+
if not shutdown_task.done():
|
|
54
|
+
shutdown_task.cancel()
|
|
55
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
56
|
+
await shutdown_task
|
|
57
|
+
await server.shutdown()
|
|
58
|
+
logger.info("ACP stdio server shut down. Metrics: %s", server.metrics.snapshot())
|
|
59
|
+
|
|
60
|
+
asyncio.run(_run())
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def acp_main_http(*, host: str = "127.0.0.1", port: int = 8765, debug: bool = False) -> None:
|
|
64
|
+
"""Start ACP server with HTTP+SSE transport."""
|
|
65
|
+
try:
|
|
66
|
+
import uvicorn
|
|
67
|
+
except ImportError:
|
|
68
|
+
raise SystemExit(
|
|
69
|
+
"HTTP transport requires extra dependencies. Install with: pip install iac-code[http]"
|
|
70
|
+
) from None
|
|
71
|
+
|
|
72
|
+
from iac_code.utils.log import setup_logging
|
|
73
|
+
|
|
74
|
+
setup_logging(session_id="acp", debug=debug)
|
|
75
|
+
_apply_stdlib_log_level(debug)
|
|
76
|
+
|
|
77
|
+
from iac_code.acp.http_sse import create_app
|
|
78
|
+
|
|
79
|
+
app = create_app()
|
|
80
|
+
uvicorn.run(
|
|
81
|
+
app,
|
|
82
|
+
host=host,
|
|
83
|
+
port=port,
|
|
84
|
+
log_level="debug" if debug else "info",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _apply_stdlib_log_level(debug: bool) -> None:
|
|
89
|
+
"""Lower the stdlib root + iac_code.acp logger to DEBUG when requested.
|
|
90
|
+
|
|
91
|
+
``setup_logging`` configures loguru, but the ACP modules use the stdlib
|
|
92
|
+
``logging`` module. Without this the ``--debug`` flag would have no
|
|
93
|
+
visible effect on ACP-emitted logs.
|
|
94
|
+
"""
|
|
95
|
+
level = logging.DEBUG if debug else logging.INFO
|
|
96
|
+
logging.getLogger().setLevel(level)
|
|
97
|
+
logging.getLogger("iac_code.acp").setLevel(level)
|
iac_code/acp/convert.py
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
import acp
|
|
7
|
+
|
|
8
|
+
from iac_code.acp.state import TurnState
|
|
9
|
+
from iac_code.acp.types import ACPContentBlock
|
|
10
|
+
from iac_code.types.stream_events import (
|
|
11
|
+
CompactionEvent,
|
|
12
|
+
ErrorEvent,
|
|
13
|
+
MessageEndEvent,
|
|
14
|
+
PermissionRequestEvent,
|
|
15
|
+
PlanEvent,
|
|
16
|
+
StackInstancesProgressEvent,
|
|
17
|
+
StackProgressEvent,
|
|
18
|
+
StreamEvent,
|
|
19
|
+
SubAgentToolEvent,
|
|
20
|
+
TextDeltaEvent,
|
|
21
|
+
ThinkingDeltaEvent,
|
|
22
|
+
ToolInputDeltaEvent,
|
|
23
|
+
ToolResultEvent,
|
|
24
|
+
ToolUseEndEvent,
|
|
25
|
+
ToolUseStartEvent,
|
|
26
|
+
Usage,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# ``acp.schema`` exposes individual session-update message classes
|
|
30
|
+
# (``AgentMessageChunk``, ``ToolCallStart``, ...) but not a single
|
|
31
|
+
# ``SessionUpdate`` union alias. We use ``Any`` here to type the
|
|
32
|
+
# heterogeneous list returned by :meth:`ACPEventConverter.event_to_updates`.
|
|
33
|
+
SessionUpdate = Any
|
|
34
|
+
|
|
35
|
+
# Mapping from internal tool name to ACP ``ToolCallStart.kind`` value.
|
|
36
|
+
# Values come from the ACP 0.9.0 ``kind`` Literal enum:
|
|
37
|
+
# read | edit | delete | move | search | execute | think | fetch |
|
|
38
|
+
# switch_mode | other
|
|
39
|
+
# Clients (e.g. Zed) use this to pick icons and UI treatment.
|
|
40
|
+
ToolKind = Literal[
|
|
41
|
+
"read",
|
|
42
|
+
"edit",
|
|
43
|
+
"delete",
|
|
44
|
+
"move",
|
|
45
|
+
"search",
|
|
46
|
+
"execute",
|
|
47
|
+
"think",
|
|
48
|
+
"fetch",
|
|
49
|
+
"switch_mode",
|
|
50
|
+
"other",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
_TOOL_KIND_MAP: dict[str, ToolKind] = {
|
|
54
|
+
# Read operations
|
|
55
|
+
"read_file": "read",
|
|
56
|
+
"list_files": "read",
|
|
57
|
+
"read_memory": "read",
|
|
58
|
+
"task_list": "read",
|
|
59
|
+
"task_get": "read",
|
|
60
|
+
# Edit / write operations
|
|
61
|
+
"write_file": "edit",
|
|
62
|
+
"edit_file": "edit",
|
|
63
|
+
"write_memory": "edit",
|
|
64
|
+
# Search operations
|
|
65
|
+
"grep": "search",
|
|
66
|
+
"glob": "search",
|
|
67
|
+
# Execute operations
|
|
68
|
+
"bash": "execute",
|
|
69
|
+
"task_stop": "execute",
|
|
70
|
+
"ros_stack": "execute",
|
|
71
|
+
"ros_stack_instances": "execute",
|
|
72
|
+
# Fetch operations
|
|
73
|
+
"web_fetch": "fetch",
|
|
74
|
+
"aliyun_doc_search": "fetch",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _tool_kind(tool_name: str) -> ToolKind:
|
|
79
|
+
"""Return the ACP ``ToolCallStart.kind`` value for a tool name.
|
|
80
|
+
|
|
81
|
+
Falls back to suffix-based heuristics so dynamically-named cloud tools
|
|
82
|
+
(e.g. ``aliyun_api``, ``foo_doc_search``) still get a sensible kind,
|
|
83
|
+
and finally to ``"other"`` for unknown tools.
|
|
84
|
+
"""
|
|
85
|
+
mapped = _TOOL_KIND_MAP.get(tool_name)
|
|
86
|
+
if mapped is not None:
|
|
87
|
+
return mapped
|
|
88
|
+
# Cloud provider API tools follow the ``{provider}_api`` naming convention.
|
|
89
|
+
if tool_name.endswith("_api"):
|
|
90
|
+
return "execute"
|
|
91
|
+
if tool_name.endswith("_doc_search"):
|
|
92
|
+
return "fetch"
|
|
93
|
+
return "other"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Callable returning ``(used_tokens, context_window_size)`` for the current
|
|
97
|
+
# session. Used by :class:`ACPEventConverter` to emit ``UsageUpdate`` events.
|
|
98
|
+
ContextSnapshot = Callable[[], tuple[int, int]]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def acp_blocks_to_prompt_text(blocks: list[ACPContentBlock]) -> str:
|
|
102
|
+
parts: list[str] = []
|
|
103
|
+
for block in blocks:
|
|
104
|
+
match block:
|
|
105
|
+
case acp.schema.TextContentBlock():
|
|
106
|
+
parts.append(block.text)
|
|
107
|
+
case acp.schema.EmbeddedResourceContentBlock():
|
|
108
|
+
resource = block.resource
|
|
109
|
+
if isinstance(resource, acp.schema.TextResourceContents):
|
|
110
|
+
parts.append(f"<resource uri={resource.uri!r}>\n{resource.text}\n</resource>")
|
|
111
|
+
case acp.schema.ResourceContentBlock():
|
|
112
|
+
parts.append(f"<resource_link uri={block.uri!r} name={block.name!r} />")
|
|
113
|
+
case acp.schema.ImageContentBlock():
|
|
114
|
+
parts.append(f"[image: {block.mime_type}]")
|
|
115
|
+
case acp.schema.AudioContentBlock():
|
|
116
|
+
parts.append(f"[audio: {block.mime_type}]")
|
|
117
|
+
case _:
|
|
118
|
+
parts.append(f"[Unsupported ACP content block: {type(block).__name__}]")
|
|
119
|
+
return "\n\n".join(part for part in parts if part)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def acp_blocks_to_multimodal(
|
|
123
|
+
blocks: list[ACPContentBlock],
|
|
124
|
+
) -> list[dict]:
|
|
125
|
+
"""Convert ACP content blocks to a list of provider-compatible content parts.
|
|
126
|
+
|
|
127
|
+
Returns a list of dicts suitable for multi-modal LLM APIs:
|
|
128
|
+
- {"type": "text", "text": "..."}
|
|
129
|
+
- {"type": "image", "mime_type": "...", "data": "..."}
|
|
130
|
+
|
|
131
|
+
When all blocks are text, callers may flatten to a single string.
|
|
132
|
+
"""
|
|
133
|
+
parts: list[dict] = []
|
|
134
|
+
for block in blocks:
|
|
135
|
+
match block:
|
|
136
|
+
case acp.schema.TextContentBlock():
|
|
137
|
+
parts.append({"type": "text", "text": block.text})
|
|
138
|
+
case acp.schema.EmbeddedResourceContentBlock():
|
|
139
|
+
resource = block.resource
|
|
140
|
+
if isinstance(resource, acp.schema.TextResourceContents):
|
|
141
|
+
parts.append(
|
|
142
|
+
{"type": "text", "text": f"<resource uri={resource.uri!r}>\n{resource.text}\n</resource>"}
|
|
143
|
+
)
|
|
144
|
+
case acp.schema.ResourceContentBlock():
|
|
145
|
+
parts.append({"type": "text", "text": f"<resource_link uri={block.uri!r} name={block.name!r} />"})
|
|
146
|
+
case acp.schema.ImageContentBlock():
|
|
147
|
+
parts.append({"type": "image", "mime_type": block.mime_type, "data": block.data})
|
|
148
|
+
case acp.schema.AudioContentBlock():
|
|
149
|
+
parts.append({"type": "audio", "mime_type": block.mime_type, "data": block.data})
|
|
150
|
+
case _:
|
|
151
|
+
parts.append({"type": "text", "text": f"[Unsupported ACP content block: {type(block).__name__}]"})
|
|
152
|
+
return parts
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ACPEventConverter:
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
turn_id: str,
|
|
159
|
+
turn_state: TurnState | None = None,
|
|
160
|
+
terminal_tool_names: set[str] | None = None,
|
|
161
|
+
context_snapshot: ContextSnapshot | None = None,
|
|
162
|
+
):
|
|
163
|
+
self._turn_id = turn_id
|
|
164
|
+
self._turn_state = turn_state
|
|
165
|
+
self._tool_inputs: dict[str, str] = {}
|
|
166
|
+
self._terminal_tool_names: set[str] = terminal_tool_names or set()
|
|
167
|
+
self._last_usage: Usage | None = None
|
|
168
|
+
self._context_snapshot = context_snapshot
|
|
169
|
+
|
|
170
|
+
def acp_tool_call_id(self, tool_use_id: str) -> str:
|
|
171
|
+
return f"{self._turn_id}/{tool_use_id}"
|
|
172
|
+
|
|
173
|
+
def event_to_updates(self, event: StreamEvent) -> list[SessionUpdate]:
|
|
174
|
+
match event:
|
|
175
|
+
case TextDeltaEvent(text=text):
|
|
176
|
+
return [
|
|
177
|
+
acp.schema.AgentMessageChunk(
|
|
178
|
+
session_update="agent_message_chunk",
|
|
179
|
+
content=acp.schema.TextContentBlock(type="text", text=text),
|
|
180
|
+
)
|
|
181
|
+
]
|
|
182
|
+
case ThinkingDeltaEvent(text=text):
|
|
183
|
+
return [
|
|
184
|
+
acp.schema.AgentThoughtChunk(
|
|
185
|
+
session_update="agent_thought_chunk",
|
|
186
|
+
content=acp.schema.TextContentBlock(type="text", text=text),
|
|
187
|
+
)
|
|
188
|
+
]
|
|
189
|
+
case ToolUseStartEvent(tool_use_id=tool_use_id, name=name):
|
|
190
|
+
if self._turn_state is not None:
|
|
191
|
+
self._turn_state.start_tool_call(tool_use_id, name)
|
|
192
|
+
return [
|
|
193
|
+
acp.schema.ToolCallStart(
|
|
194
|
+
session_update="tool_call",
|
|
195
|
+
tool_call_id=self.acp_tool_call_id(tool_use_id),
|
|
196
|
+
title=name,
|
|
197
|
+
kind=_tool_kind(name),
|
|
198
|
+
status="pending",
|
|
199
|
+
)
|
|
200
|
+
]
|
|
201
|
+
case ToolInputDeltaEvent(tool_use_id=tool_use_id, partial_json=partial_json):
|
|
202
|
+
self._tool_inputs[tool_use_id] = self._tool_inputs.get(tool_use_id, "") + partial_json
|
|
203
|
+
tc_state = self._turn_state.get_tool_call(tool_use_id) if self._turn_state else None
|
|
204
|
+
if tc_state is not None:
|
|
205
|
+
tc_state.update_input(partial_json)
|
|
206
|
+
title = tc_state.title if tc_state else None
|
|
207
|
+
update = acp.schema.ToolCallProgress(
|
|
208
|
+
session_update="tool_call_update",
|
|
209
|
+
tool_call_id=self.acp_tool_call_id(tool_use_id),
|
|
210
|
+
status="pending",
|
|
211
|
+
content=[_text_tool_content(self._tool_inputs[tool_use_id])],
|
|
212
|
+
)
|
|
213
|
+
if title:
|
|
214
|
+
update.title = title
|
|
215
|
+
return [update]
|
|
216
|
+
case ToolUseEndEvent(tool_use_id=tool_use_id, input=input):
|
|
217
|
+
return [
|
|
218
|
+
acp.schema.ToolCallProgress(
|
|
219
|
+
session_update="tool_call_update",
|
|
220
|
+
tool_call_id=self.acp_tool_call_id(tool_use_id),
|
|
221
|
+
status="in_progress",
|
|
222
|
+
content=[_text_tool_content(str(input))],
|
|
223
|
+
)
|
|
224
|
+
]
|
|
225
|
+
case ToolResultEvent(tool_use_id=tool_use_id, tool_name=tool_name, result=result, is_error=is_error):
|
|
226
|
+
content: list[
|
|
227
|
+
acp.schema.ContentToolCallContent
|
|
228
|
+
| acp.schema.FileEditToolCallContent
|
|
229
|
+
| acp.schema.TerminalToolCallContent
|
|
230
|
+
] = [_text_tool_content(result)]
|
|
231
|
+
meta: dict[str, Any] | None = None
|
|
232
|
+
if tool_name in self._terminal_tool_names:
|
|
233
|
+
meta = {"already_displayed": True}
|
|
234
|
+
# Attach tool call elapsed time if available
|
|
235
|
+
tc_state = self._turn_state.get_tool_call(tool_use_id) if self._turn_state else None
|
|
236
|
+
if tc_state is not None:
|
|
237
|
+
if meta is None:
|
|
238
|
+
meta = {}
|
|
239
|
+
meta["timing"] = {"elapsed_ms": tc_state.elapsed_ms}
|
|
240
|
+
# Emit final progress update with tool output
|
|
241
|
+
progress = acp.schema.ToolCallProgress(
|
|
242
|
+
session_update="tool_call_update",
|
|
243
|
+
tool_call_id=self.acp_tool_call_id(tool_use_id),
|
|
244
|
+
status="in_progress",
|
|
245
|
+
content=content,
|
|
246
|
+
)
|
|
247
|
+
if meta is not None:
|
|
248
|
+
progress.field_meta = meta
|
|
249
|
+
# Emit terminal update marking tool call as completed/failed
|
|
250
|
+
end = acp.schema.ToolCallProgress(
|
|
251
|
+
session_update="tool_call_update",
|
|
252
|
+
tool_call_id=self.acp_tool_call_id(tool_use_id),
|
|
253
|
+
status="failed" if is_error else "completed",
|
|
254
|
+
)
|
|
255
|
+
return [progress, end]
|
|
256
|
+
case CompactionEvent(original_tokens=original, compacted_tokens=compacted):
|
|
257
|
+
return [
|
|
258
|
+
acp.schema.AgentMessageChunk(
|
|
259
|
+
session_update="agent_message_chunk",
|
|
260
|
+
content=acp.schema.TextContentBlock(
|
|
261
|
+
type="text",
|
|
262
|
+
text=f"[Context compacted: {original} -> {compacted} tokens]",
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
]
|
|
266
|
+
case ErrorEvent(error=error):
|
|
267
|
+
return [
|
|
268
|
+
acp.schema.AgentMessageChunk(
|
|
269
|
+
session_update="agent_message_chunk",
|
|
270
|
+
content=acp.schema.TextContentBlock(type="text", text=f"[Error] {error}"),
|
|
271
|
+
)
|
|
272
|
+
]
|
|
273
|
+
case PlanEvent(steps=steps):
|
|
274
|
+
entries = [
|
|
275
|
+
acp.schema.PlanEntry(
|
|
276
|
+
content=step.content,
|
|
277
|
+
status=step.status,
|
|
278
|
+
priority=step.priority,
|
|
279
|
+
)
|
|
280
|
+
for step in steps
|
|
281
|
+
]
|
|
282
|
+
return [
|
|
283
|
+
acp.schema.AgentPlanUpdate(
|
|
284
|
+
session_update="plan",
|
|
285
|
+
entries=entries,
|
|
286
|
+
)
|
|
287
|
+
]
|
|
288
|
+
case MessageEndEvent(usage=usage):
|
|
289
|
+
self._last_usage = usage
|
|
290
|
+
# Emit an ACP ``UsageUpdate`` carrying current context-window
|
|
291
|
+
# occupancy. This is semantically different from the per-turn
|
|
292
|
+
# input/output token counts returned via
|
|
293
|
+
# ``PromptResponse.field_meta["usage"]``: ``UsageUpdate`` is
|
|
294
|
+
# the ACP-standard channel for clients to render context
|
|
295
|
+
# pressure / auto-compact hints.
|
|
296
|
+
if self._context_snapshot is None:
|
|
297
|
+
return []
|
|
298
|
+
try:
|
|
299
|
+
used, size = self._context_snapshot()
|
|
300
|
+
except Exception:
|
|
301
|
+
return []
|
|
302
|
+
if size <= 0 or used < 0:
|
|
303
|
+
return []
|
|
304
|
+
return [
|
|
305
|
+
acp.schema.UsageUpdate(
|
|
306
|
+
session_update="usage_update",
|
|
307
|
+
used=used,
|
|
308
|
+
size=size,
|
|
309
|
+
)
|
|
310
|
+
]
|
|
311
|
+
case PermissionRequestEvent() | StackProgressEvent() | StackInstancesProgressEvent() | SubAgentToolEvent():
|
|
312
|
+
return []
|
|
313
|
+
case _:
|
|
314
|
+
return []
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _text_tool_content(text: str) -> acp.schema.ContentToolCallContent:
|
|
318
|
+
return acp.schema.ContentToolCallContent(
|
|
319
|
+
type="content",
|
|
320
|
+
content=acp.schema.TextContentBlock(type="text", text=text),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
# Multimodal output helpers
|
|
326
|
+
# ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def create_image_content_block(
|
|
330
|
+
data: str,
|
|
331
|
+
mime_type: str = "image/png",
|
|
332
|
+
*,
|
|
333
|
+
uri: str | None = None,
|
|
334
|
+
) -> Any:
|
|
335
|
+
"""Create an ACP ImageContentBlock for multimodal output.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
data: Base64-encoded image data.
|
|
339
|
+
mime_type: MIME type of the image (default ``image/png``).
|
|
340
|
+
uri: Optional URI reference for the image.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
An ``acp.schema.ImageContentBlock`` instance.
|
|
344
|
+
"""
|
|
345
|
+
return acp.schema.ImageContentBlock(type="image", data=data, mime_type=mime_type, uri=uri)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def create_audio_content_block(
|
|
349
|
+
data: str,
|
|
350
|
+
mime_type: str = "audio/wav",
|
|
351
|
+
) -> Any:
|
|
352
|
+
"""Create an ACP AudioContentBlock for multimodal output.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
data: Base64-encoded audio data.
|
|
356
|
+
mime_type: MIME type of the audio (default ``audio/wav``).
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
An ``acp.schema.AudioContentBlock`` instance.
|
|
360
|
+
"""
|
|
361
|
+
return acp.schema.AudioContentBlock(type="audio", data=data, mime_type=mime_type)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def create_file_content_block(
|
|
365
|
+
data: str,
|
|
366
|
+
filename: str,
|
|
367
|
+
mime_type: str,
|
|
368
|
+
) -> Any:
|
|
369
|
+
"""Create an ACP EmbeddedResourceContentBlock wrapping binary file data.
|
|
370
|
+
|
|
371
|
+
This embeds file content as a ``BlobResourceContents`` resource inside an
|
|
372
|
+
``EmbeddedResourceContentBlock``, which is the ACP-standard way to
|
|
373
|
+
transmit arbitrary file payloads.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
data: Base64-encoded file data.
|
|
377
|
+
filename: Display name / URI for the file.
|
|
378
|
+
mime_type: MIME type of the file.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
An ``acp.schema.EmbeddedResourceContentBlock`` instance.
|
|
382
|
+
"""
|
|
383
|
+
resource = acp.schema.BlobResourceContents(
|
|
384
|
+
uri=filename,
|
|
385
|
+
mime_type=mime_type,
|
|
386
|
+
blob=data,
|
|
387
|
+
)
|
|
388
|
+
return acp.schema.EmbeddedResourceContentBlock(
|
|
389
|
+
type="resource",
|
|
390
|
+
resource=resource,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def create_multimodal_message_chunk(
|
|
395
|
+
content_blocks: list[Any],
|
|
396
|
+
) -> acp.schema.AgentMessageChunk:
|
|
397
|
+
"""Wrap one or more content blocks into an ``AgentMessageChunk``.
|
|
398
|
+
|
|
399
|
+
This is a convenience helper for building session updates that carry
|
|
400
|
+
non-text (image / audio / file) payloads.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
content_blocks: A list of ACP content block instances
|
|
404
|
+
(``ImageContentBlock``, ``AudioContentBlock``, etc.).
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
An ``AgentMessageChunk`` ready to be yielded from
|
|
408
|
+
``event_to_updates``.
|
|
409
|
+
"""
|
|
410
|
+
# AgentMessageChunk.content accepts a single block; for multiple blocks
|
|
411
|
+
# we emit one chunk per block. When only one block is provided we
|
|
412
|
+
# return it directly for simplicity.
|
|
413
|
+
if len(content_blocks) == 1:
|
|
414
|
+
return acp.schema.AgentMessageChunk(
|
|
415
|
+
session_update="agent_message_chunk",
|
|
416
|
+
content=content_blocks[0],
|
|
417
|
+
)
|
|
418
|
+
# For multiple blocks, return the first one – callers that need to emit
|
|
419
|
+
# several blocks should call this helper per block or iterate themselves.
|
|
420
|
+
return acp.schema.AgentMessageChunk(
|
|
421
|
+
session_update="agent_message_chunk",
|
|
422
|
+
content=content_blocks[0],
|
|
423
|
+
)
|