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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ cython_debug/

# Cursor
.cursor/
.qoder/
.claude

# Super Powers
.superpowers/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ Reading from stdin is also supported:

```bash
echo "Create an OSS Bucket" | iac-code --prompt -
```
```
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies = [
"opentelemetry-distro>=0.48b0",
"opentelemetry-exporter-otlp>=1.27.0",
"agent-client-protocol>=0.9.0",
"pillow==11.0.0",
"cryptography>=42.0",
"keyring>=25.0",
"tree-sitter>=0.23",
Expand Down Expand Up @@ -105,6 +106,10 @@ iac-code = "iac_code.cli.main:app"
[tool.uv]
index-url = "https://mirrors.aliyun.com/pypi/simple/"

[[tool.uv.index]]
url = "https://mirrors.aliyun.com/pypi/simple/"
default = true

[tool.coverage.run]
source_pkgs = ["iac_code"]
branch = true
Expand Down
24 changes: 17 additions & 7 deletions src/iac_code/agent/agent_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from loguru import logger

from iac_code.agent.message import TextBlock, ThinkingBlock, ToolResultBlock, ToolUseBlock
from iac_code.agent.message import ContentBlock, TextBlock, ThinkingBlock, ToolResultBlock, ToolUseBlock
from iac_code.i18n import _
from iac_code.services.context_manager import ContextManager
from iac_code.tools.base import ToolContext, ToolRegistry, ToolResult
Expand Down Expand Up @@ -151,20 +151,22 @@ def _get_provider_messages(self):
input=block.get("input"),
content=block.get("content"),
is_error=block.get("is_error", False),
media_type=block.get("media_type"),
data=block.get("data"),
)
)
provider_messages.append(ProviderMessage(role=role, content=blocks))
return provider_messages

async def run(self, user_input: str) -> str:
async def run(self, user_input: str | list[ContentBlock]) -> str:
"""Non-streaming execution. Returns final text."""
final_text = ""
async for event in self.run_streaming(user_input):
if isinstance(event, TextDeltaEvent):
final_text += event.text
return final_text

async def run_streaming(self, user_input: str) -> AsyncGenerator[StreamEvent, None]:
async def run_streaming(self, user_input: str | list[ContentBlock]) -> AsyncGenerator[StreamEvent, None]:
"""Streaming execution yielding fine-grained StreamEvents.

Flow:
Expand Down Expand Up @@ -201,7 +203,15 @@ async def run_streaming(self, user_input: str) -> AsyncGenerator[StreamEvent, No
serialize_user_input,
)

entry_attrs[GenAiAttr.INPUT_MESSAGES] = serialize_user_input(user_input)
# serialize_user_input expects str; for structured input (list[ContentBlock]),
# extract text-only segments so telemetry stays readable without leaking image bytes.
if isinstance(user_input, str):
input_text_for_telemetry = user_input
else:
input_text_for_telemetry = " ".join(
getattr(b, "text", "") for b in user_input if getattr(b, "type", None) == "text"
)
entry_attrs[GenAiAttr.INPUT_MESSAGES] = serialize_user_input(input_text_for_telemetry)
entry_attrs[GenAiAttr.SYSTEM_INSTRUCTIONS] = serialize_system_instructions(self.system_prompt)

with start_span(Spans.ENTRY, entry_attrs) as entry_span:
Expand Down Expand Up @@ -248,7 +258,7 @@ async def run_streaming(self, user_input: str) -> AsyncGenerator[StreamEvent, No
serialize_output_messages("".join(final_text_chunks), final_stop_reason),
)

async def _run_streaming_inner(self, user_input: str) -> AsyncGenerator[StreamEvent, None]:
async def _run_streaming_inner(self, user_input: str | list[ContentBlock]) -> AsyncGenerator[StreamEvent, None]:
"""Inner streaming loop (called from run_streaming inside the ENTRY span)."""
from iac_code.services.telemetry import start_span
from iac_code.services.telemetry.names import GenAiAttr, GenAiOperationName, GenAiSpanKind, Spans
Expand Down Expand Up @@ -423,7 +433,7 @@ async def _run_streaming_inner(self, user_input: str) -> AsyncGenerator[StreamEv
]
self.context_manager.add_tool_results(denied_blocks)
if self._session_storage:
from iac_code.agent.message import ContentBlock, Message
from iac_code.agent.message import Message

denied_content: list[ContentBlock] = list(denied_blocks)
self._session_storage.append(
Expand Down Expand Up @@ -509,7 +519,7 @@ async def poll_event_queues():

self.context_manager.add_tool_results(tool_result_blocks)
if self._session_storage:
from iac_code.agent.message import ContentBlock, Message
from iac_code.agent.message import Message

result_content: list[ContentBlock] = list(tool_result_blocks)
self._session_storage.append(
Expand Down
12 changes: 10 additions & 2 deletions src/iac_code/agent/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,14 @@ class ThinkingBlock(BaseModel):
thinking: str


class ImageBlock(BaseModel):
type: Literal["image"] = "image"
media_type: str # 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp'
data: str # base64


# Union type for all content blocks
ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | ThinkingBlock
ContentBlock = TextBlock | ToolUseBlock | ToolResultBlock | ThinkingBlock | ImageBlock


class Message(BaseModel):
Expand Down Expand Up @@ -110,6 +116,8 @@ def to_api_format(self) -> dict:
)
elif isinstance(block, ThinkingBlock):
content_list.append({"type": "thinking", "thinking": block.thinking})
elif isinstance(block, ImageBlock):
content_list.append({"type": "image", "media_type": block.media_type, "data": block.data})
return {"role": self.role, "content": content_list}


Expand All @@ -118,7 +126,7 @@ class Conversation(BaseModel):

messages: list[Message] = Field(default_factory=list)

def add_user_message(self, content: str) -> Message:
def add_user_message(self, content: str | list[ContentBlock]) -> Message:
"""Add a user message to the conversation."""
msg = Message(role="user", content=content)
self.messages.append(msg)
Expand Down
Loading
Loading