chcode 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.
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ from typing import Callable
6
+
7
+ from langchain.agents import create_agent
8
+ from langchain.agents.middleware import (
9
+ dynamic_prompt,
10
+ wrap_tool_call,
11
+ wrap_model_call,
12
+ ModelRequest,
13
+ ModelResponse,
14
+ )
15
+ from langchain_core.messages import HumanMessage, ToolMessage
16
+ from langchain.tools.tool_node import ToolCallRequest
17
+
18
+ from chcode.agents.definitions import AgentDefinition
19
+ from chcode.utils.enhanced_chat_openai import EnhancedChatOpenAI
20
+ from chcode.utils.skill_loader import SkillLoader, SkillAgentContext
21
+ from chcode.utils.tool_result_pipeline import (
22
+ clean_tool_output,
23
+ truncate_large_result,
24
+ enforce_per_turn_budget,
25
+ )
26
+
27
+
28
+ @wrap_tool_call
29
+ async def _handle_tool_errors(
30
+ request: ToolCallRequest, handler: Callable[[ToolCallRequest], object]
31
+ ) -> object:
32
+ try:
33
+ return await handler(request)
34
+ except Exception as e:
35
+ return ToolMessage(
36
+ f"Tool error: Please check your input and try again ({e})",
37
+ tool_call_id=request.tool_call["id"],
38
+ status="error",
39
+ )
40
+
41
+
42
+ @dynamic_prompt
43
+ async def _subagent_system_prompt(request: ModelRequest) -> str:
44
+ return request.runtime.context.extra.get("system_prompt", "")
45
+
46
+
47
+ @wrap_model_call
48
+ async def _tool_result_budget(
49
+ request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
50
+ ) -> ModelResponse:
51
+ workplace = request.runtime.context.working_directory
52
+ messages = list(request.messages)
53
+ for i, msg in enumerate(messages):
54
+ if isinstance(msg, ToolMessage) and msg.content:
55
+ cleaned = clean_tool_output(msg.content)
56
+ truncated = truncate_large_result(
57
+ cleaned,
58
+ msg.name or "",
59
+ msg.tool_call_id,
60
+ workplace=workplace,
61
+ )
62
+ messages[i] = msg.model_copy(update={"content": truncated})
63
+ messages = enforce_per_turn_budget(messages, budget=200_000, workplace=workplace)
64
+ return await handler(request.override(messages=messages))
65
+
66
+
67
+ def _resolve_tools(
68
+ agent_def: AgentDefinition,
69
+ all_tools: list,
70
+ ) -> list:
71
+ result = []
72
+ for t in all_tools:
73
+ name = getattr(t, "name", None) or getattr(t, "func", {}).get("__name__", "")
74
+ if name == "agent":
75
+ continue
76
+ if name in agent_def.disallowed_tools:
77
+ continue
78
+ if agent_def.tools is not None and name not in agent_def.tools:
79
+ continue
80
+ result.append(t)
81
+ return result
82
+
83
+
84
+ async def run_subagent(
85
+ prompt: str,
86
+ agent_def: AgentDefinition,
87
+ model_config: dict,
88
+ working_directory: Path,
89
+ skill_loader: SkillLoader,
90
+ timeout_seconds: int = 300,
91
+ description: str = "",
92
+ ) -> str:
93
+ # 守卫:超时最小 300s
94
+ timeout_seconds = max(timeout_seconds, 300)
95
+ from chcode.utils.tools import ALL_TOOLS
96
+
97
+ filtered_tools = _resolve_tools(agent_def, ALL_TOOLS)
98
+
99
+ cfg = dict(model_config)
100
+ if agent_def.model:
101
+ cfg = {**cfg, "model": agent_def.model}
102
+
103
+ model = EnhancedChatOpenAI(**cfg)
104
+
105
+ subagent_context = SkillAgentContext(
106
+ skill_loader=skill_loader,
107
+ working_directory=working_directory,
108
+ model_config=cfg,
109
+ extra={"system_prompt": agent_def.system_prompt},
110
+ )
111
+
112
+ middleware = [
113
+ _handle_tool_errors,
114
+ _tool_result_budget,
115
+ _subagent_system_prompt,
116
+ ]
117
+
118
+ from chcode.agent_setup import model_retry_with_backoff, ModelSwitchError
119
+
120
+ middleware.append(model_retry_with_backoff)
121
+ # 非 read-only 子代理继承主 agent 的 HITL 配置
122
+ if not agent_def.read_only:
123
+ from chcode.agent_setup import _hitl_middleware
124
+
125
+ if _hitl_middleware is not None:
126
+ middleware.append(_hitl_middleware)
127
+
128
+ subagent = create_agent(
129
+ model,
130
+ filtered_tools,
131
+ middleware=middleware,
132
+ context_schema=SkillAgentContext,
133
+ )
134
+
135
+ try:
136
+ result = await asyncio.wait_for(
137
+ subagent.ainvoke(
138
+ {"messages": [HumanMessage(content=prompt)]},
139
+ config={"configurable": {"thread_id": f"subagent_{id(subagent)}"}},
140
+ context=subagent_context,
141
+ ),
142
+ timeout=timeout_seconds,
143
+ )
144
+ except asyncio.TimeoutError:
145
+ return f"Agent {agent_def.agent_type} timed out after {timeout_seconds}s."
146
+ except ModelSwitchError:
147
+ return f"Agent {agent_def.agent_type} 主模型失败,已切换备用模型,请重试"
148
+ except Exception as e:
149
+ return f"Agent {agent_def.agent_type} error: {e}"
150
+
151
+ from chcode.utils import get_text_content
152
+ messages = result.get("messages", [])
153
+ for msg in reversed(messages):
154
+ if msg.type == "ai" and msg.content:
155
+ content = get_text_content(msg.content)
156
+ if content.strip():
157
+ return content.strip()
158
+
159
+ return "(Agent completed with no text output)"