Batching of thoughts, improved permissions
This commit is contained in:
@@ -16,3 +16,7 @@ working_directory: "/home/user/projects"
|
||||
|
||||
# Forward Goose's chain-of-thought reasoning to Signal (can be noisy)
|
||||
forward_thoughts: false
|
||||
|
||||
# Auto-approve all tool permission requests without asking the user.
|
||||
# When false (default), each tool call prompts the user for approval via Signal.
|
||||
auto_approve_tools: false
|
||||
|
||||
@@ -19,6 +19,7 @@ class Config:
|
||||
trusted_contacts: list[Contact] = field(default_factory=list)
|
||||
working_directory: str = "."
|
||||
forward_thoughts: bool = False
|
||||
auto_approve_tools: bool = False
|
||||
|
||||
def is_trusted(self, number: str) -> bool:
|
||||
"""Check whether a phone number belongs to a trusted contact."""
|
||||
@@ -56,10 +57,12 @@ def load_config(path: Path | None = None) -> Config:
|
||||
|
||||
working_directory = raw.get("working_directory", ".")
|
||||
forward_thoughts = raw.get("forward_thoughts", False)
|
||||
auto_approve_tools = raw.get("auto_approve_tools", False)
|
||||
|
||||
return Config(
|
||||
account=raw["account"],
|
||||
trusted_contacts=contacts,
|
||||
working_directory=working_directory,
|
||||
forward_thoughts=forward_thoughts,
|
||||
auto_approve_tools=auto_approve_tools,
|
||||
)
|
||||
|
||||
+90
-5
@@ -78,11 +78,20 @@ class GooseClient(Client):
|
||||
The client-side methods exist only as no-op stubs required by the protocol.
|
||||
"""
|
||||
|
||||
def __init__(self, forward_thoughts: bool = False) -> None:
|
||||
# Flush accumulated thoughts after this many seconds of quiet
|
||||
THOUGHT_FLUSH_DELAY = 3.0
|
||||
# Flush immediately if accumulated thought text exceeds this length
|
||||
THOUGHT_FLUSH_SIZE = 1000
|
||||
|
||||
def __init__(self, forward_thoughts: bool = False, auto_approve_tools: bool = False) -> None:
|
||||
self._response_chunks: dict[str, list[str]] = {} # session_id -> chunks
|
||||
self._forward_thoughts = forward_thoughts
|
||||
self._auto_approve_tools = auto_approve_tools
|
||||
self._ask_user: AskUserFunc | None = None
|
||||
self._notify_user: NotifyUserFunc | None = None
|
||||
# Thought batching state per session
|
||||
self._thought_buf: dict[str, list[str]] = {}
|
||||
self._thought_flush_tasks: dict[str, asyncio.Task] = {}
|
||||
|
||||
def set_ask_user(self, func: AskUserFunc) -> None:
|
||||
"""Set the callback used to ask the Signal user a question."""
|
||||
@@ -97,6 +106,46 @@ class GooseClient(Client):
|
||||
chunks = self._response_chunks.pop(session_id, [])
|
||||
return "".join(chunks)
|
||||
|
||||
def _accumulate_thought(self, session_id: str, text: str) -> None:
|
||||
"""Add thought text to the buffer and schedule a flush."""
|
||||
self._thought_buf.setdefault(session_id, []).append(text)
|
||||
|
||||
# Cancel any existing flush timer and restart it
|
||||
if session_id in self._thought_flush_tasks:
|
||||
self._thought_flush_tasks[session_id].cancel()
|
||||
|
||||
# Flush immediately if buffer is large enough
|
||||
buf_text = "".join(self._thought_buf.get(session_id, []))
|
||||
if len(buf_text) >= self.THOUGHT_FLUSH_SIZE:
|
||||
self._thought_flush_tasks[session_id] = asyncio.create_task(
|
||||
self._flush_thoughts(session_id)
|
||||
)
|
||||
else:
|
||||
# Otherwise schedule a delayed flush
|
||||
self._thought_flush_tasks[session_id] = asyncio.create_task(
|
||||
self._delayed_flush(session_id)
|
||||
)
|
||||
|
||||
async def _delayed_flush(self, session_id: str) -> None:
|
||||
"""Wait for the debounce delay, then flush thoughts."""
|
||||
await asyncio.sleep(self.THOUGHT_FLUSH_DELAY)
|
||||
await self._flush_thoughts(session_id)
|
||||
|
||||
async def _flush_thoughts(self, session_id: str) -> None:
|
||||
"""Send any accumulated thought text to the user."""
|
||||
# Cancel the timer if one is running
|
||||
task = self._thought_flush_tasks.pop(session_id, None)
|
||||
if task is not None and not task.done():
|
||||
task.cancel()
|
||||
|
||||
chunks = self._thought_buf.pop(session_id, [])
|
||||
if not chunks or self._notify_user is None:
|
||||
return
|
||||
|
||||
text = "".join(chunks).strip()
|
||||
if text:
|
||||
await self._notify_user(session_id, f"💭 {text}")
|
||||
|
||||
# -- Session update notifications --
|
||||
|
||||
async def session_update(self, session_id: str, update, **kwargs: Any) -> None:
|
||||
@@ -111,9 +160,11 @@ class GooseClient(Client):
|
||||
if isinstance(content, TextContentBlock):
|
||||
log.debug("Agent thought [%s]: %s", session_id[:8], content.text[:80])
|
||||
if self._forward_thoughts and self._notify_user:
|
||||
await self._notify_user(session_id, f"💭 {content.text}")
|
||||
self._accumulate_thought(session_id, content.text)
|
||||
|
||||
elif isinstance(update, ToolCallStart):
|
||||
# Flush any pending thoughts before reporting the tool call
|
||||
await self._flush_thoughts(session_id)
|
||||
title = update.title or "(unknown)"
|
||||
log.info("Tool call started [%s]: %s", session_id[:8], title)
|
||||
if self._notify_user:
|
||||
@@ -131,7 +182,31 @@ class GooseClient(Client):
|
||||
tool_call: ToolCallUpdate,
|
||||
**kwargs: Any,
|
||||
) -> RequestPermissionResponse:
|
||||
"""Ask the Signal user for permission via the bridge callback."""
|
||||
"""Ask the Signal user for permission, or auto-approve if configured."""
|
||||
if self._auto_approve_tools:
|
||||
# Find the most permissive allow option
|
||||
allow_option = None
|
||||
for opt in options:
|
||||
if opt.kind == PermissionOptionKind.ALWAYS_ALLOW:
|
||||
allow_option = opt
|
||||
break
|
||||
elif opt.kind == PermissionOptionKind.ALLOW:
|
||||
allow_option = opt
|
||||
if allow_option is None:
|
||||
allow_option = options[0]
|
||||
|
||||
log.info(
|
||||
"Auto-approving tool [%s]: %s (option: %s)",
|
||||
session_id[:8],
|
||||
tool_call.title or "(unknown)",
|
||||
allow_option.name,
|
||||
)
|
||||
return RequestPermissionResponse(
|
||||
outcome=AllowedOutcome(
|
||||
outcome="selected", option_id=allow_option.option_id
|
||||
)
|
||||
)
|
||||
|
||||
question, option_labels = _format_permission_message(tool_call, options)
|
||||
|
||||
if self._ask_user is not None:
|
||||
@@ -231,9 +306,12 @@ def _parse_permission_reply(
|
||||
class GooseConnection:
|
||||
"""Manages the ACP connection to a Goose agent process."""
|
||||
|
||||
def __init__(self, cwd: str = ".", forward_thoughts: bool = False):
|
||||
def __init__(self, cwd: str = ".", forward_thoughts: bool = False, auto_approve_tools: bool = False):
|
||||
self.cwd = cwd
|
||||
self.client = GooseClient(forward_thoughts=forward_thoughts)
|
||||
self.client = GooseClient(
|
||||
forward_thoughts=forward_thoughts,
|
||||
auto_approve_tools=auto_approve_tools,
|
||||
)
|
||||
self._conn = None
|
||||
self._process = None
|
||||
self._ctx = None
|
||||
@@ -313,6 +391,9 @@ class GooseConnection:
|
||||
prompt_content = [TextContentBlock(type="text", text=text)]
|
||||
response = await self._conn.prompt(prompt=prompt_content, session_id=session_id)
|
||||
|
||||
# Flush any remaining thoughts
|
||||
await self.client._flush_thoughts(session_id)
|
||||
|
||||
log.info("Prompt complete [%s]: stop_reason=%s", session_id[:8], response.stop_reason)
|
||||
|
||||
text_response = self.client.take_response(session_id)
|
||||
@@ -342,6 +423,10 @@ class GooseConnection:
|
||||
prompt_content.append(ImageContentBlock(type="image", data=image_data, mime_type=mime_type))
|
||||
|
||||
response = await self._conn.prompt(prompt=prompt_content, session_id=session_id)
|
||||
|
||||
# Flush any remaining thoughts
|
||||
await self.client._flush_thoughts(session_id)
|
||||
|
||||
log.info("Prompt (with image) complete [%s]: stop_reason=%s", session_id[:8], response.stop_reason)
|
||||
|
||||
return self.client.take_response(session_id)
|
||||
|
||||
@@ -54,6 +54,7 @@ async def run(args: argparse.Namespace) -> None:
|
||||
goose = GooseConnection(
|
||||
cwd=config.working_directory,
|
||||
forward_thoughts=config.forward_thoughts,
|
||||
auto_approve_tools=config.auto_approve_tools,
|
||||
)
|
||||
await goose.start()
|
||||
log.info("Goose ACP connection established (cwd=%s)", config.working_directory)
|
||||
|
||||
Reference in New Issue
Block a user