mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
fix(kiro): 修复工具schema兼容性、消息交替和重复内容问题
- 递归清理工具schema中Kiro不支持的additionalProperties和空required字段 - 将thinking prefix注入从history移到currentMessage,仅作用于当前轮次 - 修复连续assistant消息缺少user消息导致角色不交替的问题 - 增加流式content事件去重,跳过Kiro发送的重复内容
This commit is contained in:
@@ -189,6 +189,29 @@ def _process_message_content(
|
||||
return "".join(text_parts), images, tool_results
|
||||
|
||||
|
||||
def _clean_tool_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Recursively remove fields that Kiro API rejects.
|
||||
|
||||
Kiro returns 400 "Improperly formed request" when tool schemas contain
|
||||
``additionalProperties`` (any value) or empty ``required: []`` arrays.
|
||||
"""
|
||||
if not isinstance(schema, dict):
|
||||
return schema # type: ignore[return-value]
|
||||
result: dict[str, Any] = {}
|
||||
for key, value in schema.items():
|
||||
if key == "additionalProperties":
|
||||
continue
|
||||
if key == "required" and isinstance(value, list) and not value:
|
||||
continue
|
||||
if isinstance(value, dict):
|
||||
result[key] = _clean_tool_schema(value)
|
||||
elif isinstance(value, list):
|
||||
result[key] = [_clean_tool_schema(v) if isinstance(v, dict) else v for v in value]
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def _convert_tools(tools: Any) -> list[dict[str, Any]]:
|
||||
if not isinstance(tools, list):
|
||||
return []
|
||||
@@ -216,6 +239,8 @@ def _convert_tools(tools: Any) -> list[dict[str, Any]]:
|
||||
if not isinstance(input_schema, dict):
|
||||
input_schema = {}
|
||||
|
||||
input_schema = _clean_tool_schema(input_schema)
|
||||
|
||||
out.append(
|
||||
{
|
||||
"toolSpecification": {
|
||||
@@ -236,11 +261,8 @@ def _create_placeholder_tool(name: str) -> dict[str, Any]:
|
||||
"description": "Tool used in conversation history",
|
||||
"inputSchema": {
|
||||
"json": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -348,8 +370,6 @@ def convert_claude_messages_to_conversation_state(
|
||||
if system_text:
|
||||
# Append chunked-write policy so the model silently obeys tool limits.
|
||||
final_system = f"{system_text}\n{_SYSTEM_CHUNKED_POLICY}"
|
||||
if thinking_prefix and not _has_thinking_tags(system_text):
|
||||
final_system = f"{thinking_prefix}\n{final_system}"
|
||||
history.append(
|
||||
{
|
||||
"userInputMessage": {
|
||||
@@ -362,19 +382,6 @@ def convert_claude_messages_to_conversation_state(
|
||||
history.append(
|
||||
{"assistantResponseMessage": {"content": "I will follow these instructions."}}
|
||||
)
|
||||
elif thinking_prefix:
|
||||
history.append(
|
||||
{
|
||||
"userInputMessage": {
|
||||
"content": thinking_prefix,
|
||||
"modelId": model_id,
|
||||
"origin": "AI_EDITOR",
|
||||
}
|
||||
}
|
||||
)
|
||||
history.append(
|
||||
{"assistantResponseMessage": {"content": "I will follow these instructions."}}
|
||||
)
|
||||
|
||||
# Build history from messages.
|
||||
# If the last message is assistant, include it in history (Kiro currentMessage
|
||||
@@ -437,9 +444,22 @@ def convert_claude_messages_to_conversation_state(
|
||||
user_item = _flush_user_buffer()
|
||||
if user_item is not None:
|
||||
history.append(user_item)
|
||||
assistant_item = _convert_assistant_message(msg)
|
||||
if assistant_item is not None:
|
||||
history.append({"assistantResponseMessage": assistant_item})
|
||||
elif not history or "assistantResponseMessage" in history[-1]:
|
||||
# No preceding user message: insert synthetic user message
|
||||
# to maintain alternating roles required by Kiro API.
|
||||
history.append(
|
||||
{
|
||||
"userInputMessage": {
|
||||
"content": "Continue.",
|
||||
"modelId": model_id,
|
||||
"origin": "AI_EDITOR",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
assistant_item = _convert_assistant_message(msg)
|
||||
if assistant_item is not None:
|
||||
history.append({"assistantResponseMessage": assistant_item})
|
||||
continue
|
||||
|
||||
# trailing unpaired user messages in history
|
||||
@@ -561,6 +581,11 @@ def convert_claude_messages_to_conversation_state(
|
||||
if validated_tool_results:
|
||||
user_ctx["toolResults"] = validated_tool_results
|
||||
|
||||
# Inject thinking tags into currentMessage (not history) so the
|
||||
# instruction applies to the current turn only.
|
||||
if thinking_prefix and not _has_thinking_tags(text_content):
|
||||
text_content = f"{thinking_prefix}\n{text_content}"
|
||||
|
||||
user_input: dict[str, Any] = {
|
||||
"userInputMessageContext": user_ctx,
|
||||
"content": text_content,
|
||||
|
||||
@@ -142,6 +142,7 @@ class _KiroStreamState:
|
||||
has_tool_use: bool = False
|
||||
stop_reason_override: str | None = None
|
||||
had_error: bool = False
|
||||
_last_content: str = ""
|
||||
|
||||
def generate_initial_events(self) -> list[dict[str, Any]]:
|
||||
events: list[dict[str, Any]] = []
|
||||
@@ -287,6 +288,11 @@ class _KiroStreamState:
|
||||
if not content:
|
||||
return []
|
||||
|
||||
# Kiro may send duplicate content events; skip exact repeats.
|
||||
if content == self._last_content:
|
||||
return []
|
||||
self._last_content = content
|
||||
|
||||
self.output_tokens += _estimate_tokens(content)
|
||||
|
||||
if not self.thinking_enabled:
|
||||
|
||||
Reference in New Issue
Block a user