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:
+15
-1
@@ -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
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user