feat: add SDK session implementation with approval flow and audit logging

- Implement SDK session with secretary model for tool approval flow
- Add audit logging for tool usage and permission decisions
- Support Feishu card interactions for approval requests
- Add new commands for task interruption and progress checking
- Remove old test files and update documentation
This commit is contained in:
Yuyao Huang
2026-04-01 12:51:00 +08:00
parent ba1b5b76c6
commit eac90941ef
34 changed files with 2375 additions and 965 deletions
+15 -1
View File
@@ -76,6 +76,18 @@ NEVER use `run_shell` for bot control. NEVER use `run_command` for shell command
5. WEB / SEARCH: use `web` at most twice, then synthesize and reply.
6. BACKGROUND TASKS: when a task starts, reply immediately — do NOT poll `task_status`.
## Progress queries
When the user asks about task progress (e.g. "怎么样了?" "做得如何?" "进度"),
use `session_progress` with the active conv_id:
- busy=true → summarize recent_tools to tell the user what CC is doing
- busy=false + last_result → summarize the result
- pending_approval is not empty → remind the user to approve/deny
- error is not empty → report the error details
## Passthrough mode
Direct mode sends messages straight to the Claude Code session (no LLM overhead).
The user gets an immediate "executing" confirmation; results are pushed to Feishu on completion.
Guidelines:
- Relay Claude Code's output verbatim.
- If no active session and the user sends a task without naming a directory, ask which project.
@@ -159,7 +171,9 @@ class OrchestrationAgent:
# Passthrough mode: if enabled and active session, bypass LLM
if self._passthrough[user_id] and active_conv:
try:
reply = await manager.send(active_conv, text, user_id=user_id, direct=True)
from orchestrator.tools import get_current_chat
chat_id = get_current_chat()
reply = await manager.send_message(active_conv, text, user_id=user_id, chat_id=chat_id)
logger.info("<<< [passthrough] reply: %r", reply[:120])
return reply
except KeyError:
+65 -5
View File
@@ -83,7 +83,6 @@ class CreateConversationInput(BaseModel):
)
initial_message: Optional[str] = Field(None, description="Optional first message to send after spawning")
idle_timeout: Optional[int] = Field(None, description="Idle timeout in seconds (default 1800)")
cc_timeout: Optional[float] = Field(None, description="Claude Code execution timeout in seconds (default 300)")
class SendToConversationInput(BaseModel):
@@ -108,23 +107,24 @@ class CreateConversationTool(BaseTool):
)
args_schema: Type[BaseModel] = CreateConversationInput
def _run(self, working_dir: str, initial_message: Optional[str] = None, idle_timeout: Optional[int] = None, cc_timeout: Optional[float] = None) -> str:
def _run(self, working_dir: str, initial_message: Optional[str] = None, idle_timeout: Optional[int] = None) -> str:
raise NotImplementedError("Use async version")
async def _arun(self, working_dir: str, initial_message: Optional[str] = None, idle_timeout: Optional[int] = None, cc_timeout: Optional[float] = None) -> str:
async def _arun(self, working_dir: str, initial_message: Optional[str] = None, idle_timeout: Optional[int] = None) -> str:
try:
resolved = _resolve_dir(working_dir)
except ValueError as exc:
return json.dumps({"error": str(exc)})
user_id = get_current_user()
chat_id = get_current_chat()
conv_id = str(uuid.uuid4())[:8]
await manager.create(
conv_id,
str(resolved),
owner_id=user_id or "",
idle_timeout=idle_timeout or 1800,
cc_timeout=cc_timeout or 300.0,
chat_id=chat_id,
)
result: dict = {
@@ -154,8 +154,9 @@ class SendToConversationTool(BaseTool):
async def _arun(self, conv_id: str, message: str) -> str:
user_id = get_current_user()
chat_id = get_current_chat()
try:
output = await manager.send(conv_id, message, user_id=user_id)
output = await manager.send_and_wait(conv_id, message, user_id=user_id, chat_id=chat_id)
return json.dumps({"conv_id": conv_id, "response": output}, ensure_ascii=False)
except KeyError:
return json.dumps({"error": f"No active session for conv_id={conv_id!r}"})
@@ -752,12 +753,71 @@ class RunCommandTool(BaseTool):
return result
class SessionProgressInput(BaseModel):
conv_id: str = Field(..., description="Conversation ID to check progress")
class SessionProgressTool(BaseTool):
name: str = "session_progress"
description: str = (
"Check the progress of a running Claude Code session. "
"Returns: busy status, elapsed time, recent tool calls, "
"recent text output, and any pending approval requests. "
"Use this when the user asks about task status or progress."
)
args_schema: Type[BaseModel] = SessionProgressInput
def _run(self, conv_id: str) -> str:
raise NotImplementedError("Use async version")
async def _arun(self, conv_id: str) -> str:
user_id = get_current_user()
progress = manager.get_progress(conv_id, user_id)
if progress is None:
return json.dumps({"error": f"Session {conv_id} not found"})
return json.dumps({
"busy": progress.busy,
"elapsed_seconds": int(progress.elapsed_seconds),
"current_prompt": progress.current_prompt[:100],
"recent_text": progress.text_messages[-3:],
"recent_tools": progress.tool_calls[-5:],
"last_result": progress.last_result[:500] if not progress.busy else "",
"error": progress.error,
"pending_approval": progress.pending_approval,
}, ensure_ascii=False)
class InterruptConversationInput(BaseModel):
conv_id: str = Field(..., description="Conversation ID to interrupt")
class InterruptConversationTool(BaseTool):
name: str = "interrupt_conversation"
description: str = "Interrupt a running Claude Code task in a session."
args_schema: Type[BaseModel] = InterruptConversationInput
def _run(self, conv_id: str) -> str:
raise NotImplementedError("Use async version")
async def _arun(self, conv_id: str) -> str:
user_id = get_current_user()
try:
success = await manager.interrupt(conv_id, user_id)
return "Interrupted" if success else "No active task to interrupt"
except KeyError:
return f"Session {conv_id} not found"
except PermissionError as e:
return str(e)
# Module-level tool list for easy import
TOOLS = [
CreateConversationTool(),
SendToConversationTool(),
ListConversationsTool(),
CloseConversationTool(),
SessionProgressTool(),
InterruptConversationTool(),
RunCommandTool(),
ShellTool(),
FileReadTool(),