feat(agent): add background task loop with macOS automation and web search#245
feat(agent): add background task loop with macOS automation and web search#245caihongwei2006 wants to merge 7 commits into
Conversation
…ommunication Squash of the agent-loop-thread feature branch: - Agent loop thread and interact thread with macOS tools - Channel-based communication between loop and interact threads, with pydantic channel types, per-role prompts, and split adapters - Main chat delegates to the agent instead of claiming inability (verified with live E2E test) - Web search capability and always-written live trajectory - Persona-level capability note (real card testing caught deflection) - Round budget: 3 rounds retrieval / 5 rounds action - fix(tts): accept adapter-returned audio path so cloud TTS isn't silent Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cript queries - agent_task / run_shell / applescript / run_shortcut → risk="low": no confirm dialog anywhere in the chain (the 30s unclicked-confirm window was killing dispatches; user accepts running unattended-approval-free). - The Jul 16 osascript timeouts were query-all scripts iterating every Note, not a too-short timeout. Keep the 30s cap and instead instruct the loop (prompt + applescript tool description): filter with whose by name/date, fetch light fields first, read body only for the few matches, never repeat over all items. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New 'web' tool group with web_search: GET /res/v1/llm/context returns source-attributed grounding text ready for the loop model — replaces the brittle curl-DuckDuckGo-and-grep guidance in the loop prompt (curl stays for reading specific pages). Loop thread now pulls groups ["macos","web"]. Key resolution: BRAVE_SEARCH_API_KEY env var, else new ApiConfig field brave_search_api_key (stored in gitignored data/config/api.yaml — the key itself is never committed). Unhelpful states (missing key, non-200) return explanatory strings instead of raising. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live testing (twice: web-search test, Nanchang weather) showed Siri speaking alongside character TTS. Chain: agent_task's description said the executor "用语音向用户汇报", the main model copied that phrase into every task string, and the loop obeyed the injected order with its only mouth — run_shell/applescript `say`. Prompt-level scope enforcement at both ends: - agent_task description + capability note: reporting is automatic system plumbing; task text must contain only the job itself, never 「汇报」「告诉用户」「语音」. - LOOP_PROMPT hard rule: the loop has neither obligation nor ability to address the user — no say/afplay/AppleScript speech, notifications, or dialogs; ignore any report/voice wording that appears in task text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test_local/test_agent_live_api.py hits real LLM/TTS endpoints and spends money — useful on this machine, not in CI. Moved out of test/e2e and ignored; the mocked E2E/unit suites remain in the repo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reviewer's Guide by Ameath小爱整理了接下来的核对方向,按这个脉络看会更清楚。 入口链为 文件级变更
Tips and commands
Original review guide in EnglishThe entry chain is |
|
精简summary: |
There was a problem hiding this comment.
Pull request overview
This PR introduces a background “execution agent” for the desktop app: the main chat character can dispatch real-world tasks to an asynchronous agent loop (tools: macOS automation + web search), while a separate interact thread narrates progress/results via the existing TTS pipeline.
Changes:
- Added a two-thread agent session (
loopexecutes tools and streams typed events;interactnarrates via TTS based on channel reports). - Added new tool groups for macOS automation (
macos) and Brave-based web search (web), plus a dispatch-onlyagent_tasktrigger tool. - Added config schema support for
brave_search_api_keyand comprehensive unit tests for agent loop + tools.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
core/agent/agent_loop.py |
Implements the agent session, loop/interact thread split, typed report channel, and prompt construction. |
core/agent/__init__.py |
Exposes AgentSession / get_session API for the agent subsystem. |
llm/tools/agent_tools.py |
Adds agent_task dispatch tool and role-card capability note injection helper. |
llm/tools/macos_tools.py |
Adds macOS automation tools (shell, AppleScript, Shortcuts, open app/url). |
llm/tools/web_tools.py |
Adds Brave Search LLM-context web_search tool and API key lookup. |
main.py |
Imports new tool modules for registration; appends agent capability note to loaded character templates. |
config/schema.py |
Adds brave_search_api_key to ApiConfig. |
test/unit/core/test_agent_loop.py |
Adds unit tests covering agent loop/interact behavior, prompts, and channel semantics. |
test/unit/llm/test_agent_tools.py |
Adds unit tests for capability note content and idempotent template injection. |
test/unit/llm/test_web_tools.py |
Adds unit tests for web_search key handling, request shape, and error reporting. |
.gitignore |
Ignores test_local/ (local E2E script directory). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from sdk.messages import LLMDialogMessage | ||
| from llm.tools.tool_manager import ToolManager | ||
| from core.runtime.app_runtime import try_get_app_runtime | ||
|
|
||
| tool_manager = ToolManager() | ||
|
|
| self.loop_tools = ( | ||
| loop_tools if loop_tools is not None else tool_manager.get_definitions(groups=["macos", "web"]) | ||
| ) | ||
| self.interact_tools = interact_tools if interact_tools is not None else [] | ||
| self.run_tool = run_tool or tool_manager.execute |
| apps = "、".join(sorted(p.stem for p in Path("/Applications").glob("*.app"))) | ||
| self.loop_prompt = LOOP_PROMPT.format( | ||
| local_settings=( | ||
| f"- 系统:macOS {platform.mac_ver()[0]}({platform.machine()})\n" | ||
| f"- 工作目录:{os.getcwd()}\n" | ||
| f"- 已装应用(连同系统自带 App,都可用 AppleScript 驱动):{apps}\n" | ||
| f"- 联网:搜索一律用 web_search 工具(返回带来源的检索上下文);" |
| def web_search(q: str) -> str: | ||
| key = _api_key() | ||
| if not key: | ||
| return ("(web_search 未配置:请在 data/config/api.yaml 的 brave_search_api_key " | ||
| "或环境变量 BRAVE_SEARCH_API_KEY 里填入 Brave Search API Key)") | ||
| r = requests.get( | ||
| _ENDPOINT, | ||
| params={"q": q}, | ||
| headers={"X-Subscription-Token": key}, | ||
| timeout=30, | ||
| ) | ||
| if r.status_code != 200: | ||
| return f"(web_search HTTP {r.status_code}: {r.text[:300]})" | ||
| return r.text.strip()[:_LIMIT] or "(no results)" |
| def test_web_search_without_key_returns_config_hint(monkeypatch): | ||
| monkeypatch.delenv("BRAVE_SEARCH_API_KEY", raising=False) | ||
| monkeypatch.setattr(web_tools, "_api_key", lambda: "") | ||
| out = web_tools.web_search("anything") | ||
| assert "未配置" in out | ||
| assert "brave_search_api_key" in out |
| def test_web_search_sends_key_header_and_returns_body(monkeypatch): | ||
| captured = {} | ||
|
|
||
| def fake_get(url, params=None, headers=None, timeout=None): | ||
| captured.update(url=url, params=params, headers=headers, timeout=timeout) | ||
| return SimpleNamespace(status_code=200, text='{"grounding": "ok"}') | ||
|
|
||
| monkeypatch.setattr(web_tools, "_api_key", lambda: "test-key") | ||
| monkeypatch.setattr(web_tools.requests, "get", fake_get) | ||
|
|
||
| out = web_tools.web_search("tallest mountains") | ||
|
|
||
| assert out == '{"grounding": "ok"}' | ||
| assert captured["url"] == web_tools._ENDPOINT | ||
| assert captured["params"] == {"q": "tallest mountains"} | ||
| assert captured["headers"] == {"X-Subscription-Token": "test-key"} | ||
|
|
| def test_web_search_http_error_is_reported_not_raised(monkeypatch): | ||
| monkeypatch.setattr(web_tools, "_api_key", lambda: "test-key") | ||
| monkeypatch.setattr( | ||
| web_tools.requests, "get", | ||
| lambda *a, **k: SimpleNamespace(status_code=429, text="rate limited"), | ||
| ) | ||
| out = web_tools.web_search("q") | ||
| assert "HTTP 429" in out | ||
|
|
| @tool(name="run_shell", group="macos", risk="low", | ||
| description="在 macOS 上执行一条 bash 命令并返回输出。用于文件操作、包管理、git 等系统任务。") | ||
| def run_shell(command: str) -> str: | ||
| return _run(["bash", "-c", command], 120) | ||
|
|
||
|
|
||
| @tool(name="applescript", group="macos", risk="low", | ||
| description="执行 AppleScript。用于应用自动化、UI 控制、系统对话框、剪贴板、系统通知、日历与提醒事项。" | ||
| "30 秒硬超时:查询必须窄——用 whose 按名称/日期过滤、先只取 name 等轻字段、" | ||
| "确定少量匹配后再读 body;绝不要遍历全部备忘录、笔记或日历事件。") | ||
| def applescript(script: str) -> str: | ||
| return _run(["osascript", "-e", script], 30) | ||
|
|
| @tool(name="list_shortcuts", group="macos", risk="low", | ||
| description="列出本机可用的「快捷指令」名称。") | ||
| def list_shortcuts() -> str: | ||
| return _run(["shortcuts", "list"], 30) | ||
|
|
||
|
|
||
| @tool(name="run_shortcut", group="macos", risk="low", | ||
| description="按名称运行一个「快捷指令」。name: 指令名;input: 可选的输入文本。") | ||
| def run_shortcut(name: str, input: str = "") -> str: | ||
| if input: | ||
| return _run(["bash", "-c", "printf '%s' \"$1\" | shortcuts run \"$2\" -i -", "_", input, name], 60) | ||
| return _run(["shortcuts", "run", name], 60) | ||
|
|
| @tool(name="open_app", group="macos", risk="low", | ||
| description="用默认方式打开一个 macOS 应用。name: 应用名,如 'Calendar'。") | ||
| def open_app(name: str) -> str: | ||
| return _run(["open", "-a", name], 15) | ||
|
|
||
|
|
||
| @tool(name="open_url", group="macos", risk="low", | ||
| description="用默认浏览器打开一个网址。") | ||
| def open_url(url: str) -> str: | ||
| return _run(["open", url], 15) |
There was a problem hiding this comment.
小爱在细节里留了需要再确认的地方,咱们逐项看。
小爱已完成全部变更审查,共发布 6 条行级建议(高风险 1,中风险 5)。
合并前关注点(风险 1 · 建议测试 0)
风险
- PR 元数据没有关联 issue;基线
CONTRIBUTING.md要求 PR 描述链接已有 issue。
Prompt for AI Agents
请逐项处理以下审查问题;修改前先核实上下文,修改后运行相关测试。
1. 代理工具调用绕过白名单与确认门 / Agent tool calls bypass the allowlist and approval gate
core/agent/agent_loop.py:308 (RIGHT)
<issue_to_address>loop_tools 只决定发送给模型的 schema,并不限制这里的 ToolManager.execute。该循环把每个模型返回的名称直接执行,绕过普通聊天路径的 ToolExecutor(..., risk_confirm=...)。同一全局注册表还包含 file_delete 和 file_write 等 high-risk 工具;一次 completion 可含多个调用,因此 MAX_STEPS 也不限制实际动作数。经 agent_task 到达此处的模型响应因而可在无确认、无会话 allowlist 的情况下执行这些名称或多个 run_shell 调用。请在执行前校验会话 allowlist、限制每轮调用数,并复用风险确认路径。
loop_tools only controls the schema sent to the model; it does not constrain ToolManager.execute here. This loop directly executes every returned name and bypasses the normal chat path's ToolExecutor(..., risk_confirm=...). The same global registry includes high-risk tools such as file_delete and file_write, and one completion can contain multiple calls, so MAX_STEPS does not cap actions. A model response reaching this path through agent_task can therefore execute those names or multiple run_shell calls without confirmation or a session allowlist. Validate the session allowlist before execution, limit per-round fan-out, and reuse the risk-confirmation path.</issue_to_address>
2. Claude 配置下代理总是降级 / The agent always degrades with Claude
core/agent/agent_loop.py:168 (RIGHT)
<issue_to_address>ClaudeAdapter 的非流式调用返回 Anthropic Message 内容块,现有 LLMManager 已对此单独解析;这里却固定读取 OpenAI 格式的 .choices[0].message。该属性错误被第 181 行的宽泛捕获转换成 None,随后 _execute 只发送 _BREAK_TEXT。因此选择受支持 Claude provider 的用户会在任何工具调用前让代理降级。请先归一化 Claude 的文本和 tool_use 块。
ClaudeAdapter returns Anthropic Message content blocks for non-streaming calls, and the existing LLMManager handles that shape separately. This code unconditionally reads the OpenAI-style .choices[0].message; the broad catch at line 181 converts the resulting attribute error to None, and _execute emits only _BREAK_TEXT. Users of the supported Claude provider therefore lose the agent before any tool call. Normalize Claude text and tool_use blocks first.</issue_to_address>
3. 在非 macOS 主机上仍宣称 macOS 能力 / macOS tooling is advertised on unsupported hosts
core/agent/agent_loop.py:223 (RIGHT)
<issue_to_address>项目支持 Windows 和 Linux,但 main.py 无条件注册本功能,且这里无条件将宿主宣称为 macOS。由此暴露的 run_shell、applescript、shortcuts 和 open 在这些系统上不存在或含义不同;能力声明还要求主聊天派发而不是说明不可用。Windows/Linux 用户一旦请求真实任务就会进入错误的系统工具。应仅在 Darwin 注册该能力,或按平台提供实现和明确的不可用处理。
The project supports Windows and Linux, but main.py registers this feature unconditionally and this prompt always claims the host is macOS. The exposed run_shell, applescript, shortcuts, and open tools are absent or mean something different on those platforms; the capability note also tells main chat to dispatch rather than report unavailability. A Windows/Linux user requesting a real task is sent to the wrong system tools. Register this capability only on Darwin, or provide platform implementations and an explicit unavailable path.</issue_to_address>
4. 后台报告被归属到任意后续聊天轮次 / Background reports attach to whichever turn is current
core/agent/agent_loop.py:383 (RIGHT)
<issue_to_address>普通 LLM 对话在 core/runtime/workers.py 入队前会附加 turn_id;此处没有。TTS worker 对 None 不做与当前 turn 的不匹配检查,所以任务在轮次 n 发起、用户随后开始 n+1 时,其延迟报告会被接受并在 n+1 中播放。应将任务和报告绑定到来源 turn/session,并在其失效时停止或丢弃。
Normal LLM dialogue adds a turn_id before enqueueing in core/runtime/workers.py; this path does not. The TTS worker does not perform a current-turn mismatch check for None, so a task started in turn n and completing after the user starts n+1 is accepted and played in n+1. Bind tasks and reports to their originating turn/session, and stop or discard them when it becomes invalid.</issue_to_address>
5. 允许的长命令会使交互超时 / Allowed long commands can time out the interaction
core/agent/agent_loop.py:48 (RIGHT)
<issue_to_address>交互线程在应答后立即开始 get(timeout=120),而 tool_output 只有 run_tool 返回后才发送。run_shell 允许运行 120 秒;当首个 loop LLM 响应晚于交互应答时,这是可达的正常时序,结果会在超时之后才到达。交互线程已播放 _TIMEOUT_TEXT 并返回,外层循环会丢弃后续 tool_output 和 done。副作用可能已完成,但用户只收到超时且拿不到结果。请在执行前或执行中发送心跳,或让等待上限覆盖模型和工具的最大时长。
The interaction thread starts get(timeout=120) immediately after its acknowledgement, while tool_output is emitted only after run_tool returns. run_shell is allowed to run for 120 seconds; when the first loop LLM response arrives after the interaction acknowledgement, a normal reachable timing, the result arrives after the deadline. The interaction has already spoken _TIMEOUT_TEXT and returned, and the outer loop drops the later tool_output and done. The side effect may have completed, but the user receives only a timeout and no result. Emit heartbeats before or during execution, or make the wait bound cover the maximum model and tool duration.</issue_to_address>
6. 进度会用配置首个角色的声音 / Reports use the first configured character’s voice
core/agent/agent_loop.py:187 (RIGHT)
<issue_to_address>聊天启动路径可以传入所选 --characters,但这里固定取配置列表中的第一个角色。_speak 随后把该名称写入 LLMDialogMessage,TTS 据名称选择角色。因此配置顺序为 A、B 而当前聊天启动为 B 时,后台报告仍会用 A 的声音和角色身份。应将当前聊天选择传入会话并随任务保存。
The chat launch path can pass selected --characters, but this always takes the first configured character. _speak then stores that name in LLMDialogMessage, which TTS resolves to a character. If configuration order is A then B while the current chat launches B, background reports still use A's voice and persona. Pass the active chat selection into the session and retain it with the task.</issue_to_address>
| } | ||
| ) | ||
| for c in out.calls: | ||
| result = self.run_tool(c.name, c.arguments) |
There was a problem hiding this comment.
[HIGH · security] 代理工具调用绕过白名单与确认门 / Agent tool calls bypass the allowlist and approval gate
loop_tools 只决定发送给模型的 schema,并不限制这里的 ToolManager.execute。该循环把每个模型返回的名称直接执行,绕过普通聊天路径的 ToolExecutor(..., risk_confirm=...)。同一全局注册表还包含 file_delete 和 file_write 等 high-risk 工具;一次 completion 可含多个调用,因此 MAX_STEPS 也不限制实际动作数。经 agent_task 到达此处的模型响应因而可在无确认、无会话 allowlist 的情况下执行这些名称或多个 run_shell 调用。请在执行前校验会话 allowlist、限制每轮调用数,并复用风险确认路径。
loop_tools only controls the schema sent to the model; it does not constrain ToolManager.execute here. This loop directly executes every returned name and bypasses the normal chat path's ToolExecutor(..., risk_confirm=...). The same global registry includes high-risk tools such as file_delete and file_write, and one completion can contain multiple calls, so MAX_STEPS does not cap actions. A model response reaching this path through agent_task can therefore execute those names or multiple run_shell calls without confirmation or a session allowlist. Validate the session allowlist before execution, limit per-round fan-out, and reuse the risk-confirmation path.
置信度 99% · 可直接回复讨论 · 👍/👎 会影响后续审查
| ) | ||
| if resp is None: | ||
| return None | ||
| msg = resp.choices[0].message |
There was a problem hiding this comment.
[MEDIUM · bug] Claude 配置下代理总是降级 / The agent always degrades with Claude
ClaudeAdapter 的非流式调用返回 Anthropic Message 内容块,现有 LLMManager 已对此单独解析;这里却固定读取 OpenAI 格式的 .choices[0].message。该属性错误被第 181 行的宽泛捕获转换成 None,随后 _execute 只发送 _BREAK_TEXT。因此选择受支持 Claude provider 的用户会在任何工具调用前让代理降级。请先归一化 Claude 的文本和 tool_use 块。
ClaudeAdapter returns Anthropic Message content blocks for non-streaming calls, and the existing LLMManager handles that shape separately. This code unconditionally reads the OpenAI-style .choices[0].message; the broad catch at line 181 converts the resulting attribute error to None, and _execute emits only _BREAK_TEXT. Users of the supported Claude provider therefore lose the agent before any tool call. Normalize Claude text and tool_use blocks first.
置信度 99% · 可直接回复讨论 · 👍/👎 会影响后续审查
| apps = "、".join(sorted(p.stem for p in Path("/Applications").glob("*.app"))) | ||
| self.loop_prompt = LOOP_PROMPT.format( | ||
| local_settings=( | ||
| f"- 系统:macOS {platform.mac_ver()[0]}({platform.machine()})\n" |
There was a problem hiding this comment.
[MEDIUM · bug] 在非 macOS 主机上仍宣称 macOS 能力 / macOS tooling is advertised on unsupported hosts
项目支持 Windows 和 Linux,但 main.py 无条件注册本功能,且这里无条件将宿主宣称为 macOS。由此暴露的 run_shell、applescript、shortcuts 和 open 在这些系统上不存在或含义不同;能力声明还要求主聊天派发而不是说明不可用。Windows/Linux 用户一旦请求真实任务就会进入错误的系统工具。应仅在 Darwin 注册该能力,或按平台提供实现和明确的不可用处理。
Original in English
The project supports Windows and Linux, but main.py registers this feature unconditionally and this prompt always claims the host is macOS. The exposed run_shell, applescript, shortcuts, and open tools are absent or mean something different on those platforms; the capability note also tells main chat to dispatch rather than report unavailability. A Windows/Linux user requesting a real task is sent to the wrong system tools. Register this capability only on Darwin, or provide platform implementations and an explicit unavailable path.
置信度 99% · 可直接回复讨论 · 👍/👎 会影响后续审查
| def _speak(self, text: str) -> None: | ||
| # 唯一允许的另一处错误处理:UI bridge——喂给现有 TTS 管线,播不出也别拖垮线程。 | ||
| try: | ||
| self.rt.tts_queue.put(LLMDialogMessage(name=self._char, text=text, asset_id="-1")) |
There was a problem hiding this comment.
[MEDIUM · bug] 后台报告被归属到任意后续聊天轮次 / Background reports attach to whichever turn is current
普通 LLM 对话在 core/runtime/workers.py 入队前会附加 turn_id;此处没有。TTS worker 对 None 不做与当前 turn 的不匹配检查,所以任务在轮次 n 发起、用户随后开始 n+1 时,其延迟报告会被接受并在 n+1 中播放。应将任务和报告绑定到来源 turn/session,并在其失效时停止或丢弃。
Original in English
Normal LLM dialogue adds a turn_id before enqueueing in core/runtime/workers.py; this path does not. The TTS worker does not perform a current-turn mismatch check for None, so a task started in turn n and completing after the user starts n+1 is accepted and played in n+1. Bind tasks and reports to their originating turn/session, and stop or discard them when it becomes invalid.
置信度 99% · 可直接回复讨论 · 👍/👎 会影响后续审查
|
|
||
| # 交互线程读 channel 的兜底超时(秒),防意外死锁——对齐 MVP 的 get(timeout=...)。 | ||
| # 相邻两条事件的最大间隔 ≈ 一次 LLM 调用 + 一次工具执行,按真实耗时放大。 | ||
| CHANNEL_TIMEOUT = 120 |
There was a problem hiding this comment.
[MEDIUM · bug] 允许的长命令会使交互超时 / Allowed long commands can time out the interaction
交互线程在应答后立即开始 get(timeout=120),而 tool_output 只有 run_tool 返回后才发送。run_shell 允许运行 120 秒;当首个 loop LLM 响应晚于交互应答时,这是可达的正常时序,结果会在超时之后才到达。交互线程已播放 _TIMEOUT_TEXT 并返回,外层循环会丢弃后续 tool_output 和 done。副作用可能已完成,但用户只收到超时且拿不到结果。请在执行前或执行中发送心跳,或让等待上限覆盖模型和工具的最大时长。
Original in English
The interaction thread starts get(timeout=120) immediately after its acknowledgement, while tool_output is emitted only after run_tool returns. run_shell is allowed to run for 120 seconds; when the first loop LLM response arrives after the interaction acknowledgement, a normal reachable timing, the result arrives after the deadline. The interaction has already spoken _TIMEOUT_TEXT and returned, and the outer loop drops the later tool_output and done. The side effect may have completed, but the user receives only a timeout and no result. Emit heartbeats before or during execution, or make the wait bound cover the maximum model and tool duration.
置信度 98% · 可直接回复讨论 · 👍/👎 会影响后续审查
|
|
||
| def _first_character_name(rt: Any) -> str: | ||
| chars = getattr(rt.config.config, "characters", None) or [] | ||
| return chars[0].name if chars else "" |
There was a problem hiding this comment.
[MEDIUM · bug] 进度会用配置首个角色的声音 / Reports use the first configured character’s voice
聊天启动路径可以传入所选 --characters,但这里固定取配置列表中的第一个角色。_speak 随后把该名称写入 LLMDialogMessage,TTS 据名称选择角色。因此配置顺序为 A、B 而当前聊天启动为 B 时,后台报告仍会用 A 的声音和角色身份。应将当前聊天选择传入会话并随任务保存。
Original in English
The chat launch path can pass selected --characters, but this always takes the first configured character. _speak then stores that name in LLMDialogMessage, which TTS resolves to a character. If configuration order is A then B while the current chat launches B, background reports still use A's voice and persona. Pass the active chat selection into the session and retain it with the task.
置信度 98% · 可直接回复讨论 · 👍/👎 会影响后续审查
|
非常感谢你的贡献,在merge前能否根据修改意见修改一下如下内容:
还有其他bot提出的comment也请address一下,感谢!! |
Summary
Adds a background execution agent to the desktop app: the main chat character can delegate real-world tasks (web search, macOS automation) to an agent loop, which executes them asynchronously while a separate interact thread narrates progress through the character's own TTS voice.
Rebased onto current
main(737a3aa); the diff is purely additive — 11 files, +1197 lines, no modifications to existing behavior beyond 3 imports and the character-template capability note inmain.py.Architecture
agent_tasktool (main chat, groupdefault): dispatch-only — submits the task and returns immediately; the character stays in role and never claims inability.core/agent/agent_loop.py): ReAct loop over tool groupsmacos+web; round budget 3 (retrieval) / 5 (action); typed pydanticReportevents over a channel.rt.tts_queue, noturn_id, so interrupt filtering is unaffected). The loop is prompt-forbidden from producing any audio/notification (scope rule added after live testing caught it usingsay).llm/tools/macos_tools.py):run_shell,applescript(30s cap + narrow-query guidance),run_shortcut,open_app,open_url,list_shortcuts— all risk-low by owner decision (attended use).web_search(llm/tools/web_tools.py): Brave Search LLM-context endpoint returning source-attributed grounding; key viaBRAVE_SEARCH_API_KEYenv or gitignoreddata/config/api.yaml(brave_search_api_key) — no secrets in the repo.Test plan
main(PySide6-stub collection errors, mem0/qtbot).test_local/.🤖 Generated with Claude Code
Summary by Ameath
小爱先把这次核对的重点收拢一下,方便继续判断。
本 PR 为主聊天增加后台执行代理:通过
agent_task派发 macOS 自动化和网页检索任务,并借既有 TTS 队列转述进度;同时加入 Brave Search 配置和相应单元测试。New Features
core/agent的执行与交互线程,以及agent_task、macOS 和网页搜索工具。Enhancements
ApiConfig新增brave_search_api_key,角色模板加入执行能力说明。Tests
Chores
test_local/脚本。Original summary in English
This PR adds a background execution agent to the main chat: it dispatches macOS automation and web-search tasks through
agent_taskand narrates progress through the existing TTS queue; it also adds Brave Search configuration and corresponding unit tests.New Features
core/agentexecution and interaction threads, plusagent_task, macOS, and web-search tools.Enhancements
brave_search_api_keytoApiConfigand an execution-capability note to character templates.Tests
Chores
test_local/.