Skip to content

feat(agent): add background task loop with macOS automation and web search#245

Open
caihongwei2006 wants to merge 7 commits into
RachelForster:mainfrom
caihongwei2006:feat/agent-loop-thread
Open

feat(agent): add background task loop with macOS automation and web search#245
caihongwei2006 wants to merge 7 commits into
RachelForster:mainfrom
caihongwei2006:feat/agent-loop-thread

Conversation

@caihongwei2006

@caihongwei2006 caihongwei2006 commented Jul 20, 2026

Copy link
Copy Markdown

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 in main.py.

Architecture

  • agent_task tool (main chat, group default): dispatch-only — submits the task and returns immediately; the character stays in role and never claims inability.
  • Loop thread (core/agent/agent_loop.py): ReAct loop over tool groups macos + web; round budget 3 (retrieval) / 5 (action); typed pydantic Report events over a channel.
  • Interact thread: the only voice — consumes channel events and speaks ≤40-char colloquial updates via the existing TTS pipeline (rt.tts_queue, no turn_id, so interrupt filtering is unaffected). The loop is prompt-forbidden from producing any audio/notification (scope rule added after live testing caught it using say).
  • macOS tools (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 via BRAVE_SEARCH_API_KEY env or gitignored data/config/api.yaml (brave_search_api_key) — no secrets in the repo.

Test plan

  • Unit: +40 new tests (agent loop 33, agent tools, web_search) — suite green on this branch with the same pre-existing environment failures as pristine main (PySide6-stub collection errors, mem0/qtbot).
  • Live: real sessions verified dispatch-without-confirmation, Brave search chosen unprompted by the loop, and character-voice-only reporting.
  • The live-API E2E script (real LLM/TTS spend) lives in gitignored 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

  • 新增代理循环、代理派发和网页搜索的 Python 单元测试。

Chores

  • 忽略本地真实 API 的 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_task and narrates progress through the existing TTS queue; it also adds Brave Search configuration and corresponding unit tests.

New Features

  • Adds core/agent execution and interaction threads, plus agent_task, macOS, and web-search tools.

Enhancements

  • Adds brave_search_api_key to ApiConfig and an execution-capability note to character templates.

Tests

  • Adds Python unit tests for the agent loop, agent dispatch, and web search.

Chores

  • Ignores local real-API scripts under test_local/.

蔡泓炜 and others added 5 commits July 20, 2026 15:24
…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>
Copilot AI review requested due to automatic review settings July 20, 2026 07:29
@ameath-review

ameath-review Bot commented Jul 20, 2026

Copy link
Copy Markdown

Reviewer's Guide by Ameath

小爱整理了接下来的核对方向,按这个脉络看会更清楚。

入口链为 main.pyagent_taskAgentSession:会话从全局 ToolManager 获取工具,由两个 LLM 后端执行和转述,并把消息写入既有 tts_queue。复核重点是本链路的会话工具授权与风险确认、Claude 响应归一化,以及后台任务与平台、当前角色、聊天轮次和工具等待时限的绑定;这些位置对应下列可达问题。

文件级变更

Change Details Files
新增文件 8 个文件,+1188 / -0 core/agent/__init__.py
core/agent/agent_loop.py
llm/tools/agent_tools.py
llm/tools/macos_tools.py
llm/tools/web_tools.py
test/unit/core/test_agent_loop.py
test/unit/llm/test_agent_tools.py
test/unit/llm/test_web_tools.py
修改文件 3 个文件,+9 / -0 .gitignore
config/schema.py
main.py

Tips and commands
  • 重新审查: @Ame4th review
  • 重新生成摘要: @Ame4th summary
  • 重新生成指南: @Ame4th guide
  • 生成标题: @Ame4th title
  • 解决本机器人线程: @Ame4th resolve
  • 清理本机器人审查: @Ame4th dismiss
  • 从行级评论创建 Issue: 回复 @Ame4th issue
  • 继续讨论: 直接回复任一行级评论。
  • 反馈偏好: 对行级评论点 👍 或 👎。
Original review guide in English

The entry chain is main.pyagent_taskAgentSession: the session obtains tools from the global ToolManager, uses two LLM backends for execution and narration, and writes messages to the existing tts_queue. Review should focus on session tool authorization and risk confirmation, Claude response normalization, and binding background tasks to the platform, active character, chat turn, and tool-wait duration; the reachable issues below occur in those areas.

@caihongwei2006

Copy link
Copy Markdown
Author

精简summary:
1 Loop agent与interact agent分离, 最小实现 见/mvp 下面的commune.py
2 切出tts的时机, 解决race问题
3 tool 的危险程度暂时都降低为low,感觉没什么能伤到计算机的东西,有些理想。
4 整体偏向MacOS, 提升agent简单任务的performance,后续在优化工具调度对各个人物的合理性。

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (loop executes tools and streams typed events; interact narrates via TTS based on channel reports).
  • Added new tool groups for macOS automation (macos) and Brave-based web search (web), plus a dispatch-only agent_task trigger tool.
  • Added config schema support for brave_search_api_key and 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.

Comment thread core/agent/agent_loop.py
Comment on lines +37 to +42
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()

Comment thread core/agent/agent_loop.py
Comment on lines +213 to +217
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
Comment thread core/agent/agent_loop.py
Comment on lines +220 to +226
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 工具(返回带来源的检索上下文);"
Comment thread llm/tools/web_tools.py
Comment on lines +34 to +47
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)"
Comment on lines +12 to +17
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
Comment on lines +20 to +36
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"}

Comment on lines +38 to +46
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

Comment thread llm/tools/macos_tools.py
Comment on lines +23 to +35
@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)

Comment thread llm/tools/macos_tools.py
Comment on lines +37 to +49
@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)

Comment thread llm/tools/macos_tools.py
Comment on lines +51 to +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)
@ameath-review ameath-review Bot changed the title feat(agent): background agent loop with interact-thread voice + web search feat(agent): add background task loop with macOS automation and web search Jul 20, 2026

@ameath-review ameath-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

小爱在细节里留了需要再确认的地方,咱们逐项看。

小爱已完成全部变更审查,共发布 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_deletefile_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_shellapplescriptshortcutsopen 在这些系统上不存在或含义不同;能力声明还要求主聊天派发而不是说明不可用。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_outputdone。副作用可能已完成,但用户只收到超时且拿不到结果。请在执行前或执行中发送心跳,或让等待上限覆盖模型和工具的最大时长。

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>

Comment thread core/agent/agent_loop.py
}
)
for c in out.calls:
result = self.run_tool(c.name, c.arguments)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH · security] 代理工具调用绕过白名单与确认门 / Agent tool calls bypass the allowlist and approval gate

loop_tools 只决定发送给模型的 schema,并不限制这里的 ToolManager.execute。该循环把每个模型返回的名称直接执行,绕过普通聊天路径的 ToolExecutor(..., risk_confirm=...)。同一全局注册表还包含 file_deletefile_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% · 可直接回复讨论 · 👍/👎 会影响后续审查

Comment thread core/agent/agent_loop.py
)
if resp is None:
return None
msg = resp.choices[0].message

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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% · 可直接回复讨论 · 👍/👎 会影响后续审查

Comment thread core/agent/agent_loop.py
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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM · bug] 在非 macOS 主机上仍宣称 macOS 能力 / macOS tooling is advertised on unsupported hosts

项目支持 Windows 和 Linux,但 main.py 无条件注册本功能,且这里无条件将宿主宣称为 macOS。由此暴露的 run_shellapplescriptshortcutsopen 在这些系统上不存在或含义不同;能力声明还要求主聊天派发而不是说明不可用。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% · 可直接回复讨论 · 👍/👎 会影响后续审查

Comment thread core/agent/agent_loop.py
def _speak(self, text: str) -> None:
# 唯一允许的另一处错误处理:UI bridge——喂给现有 TTS 管线,播不出也别拖垮线程。
try:
self.rt.tts_queue.put(LLMDialogMessage(name=self._char, text=text, asset_id="-1"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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% · 可直接回复讨论 · 👍/👎 会影响后续审查

Comment thread core/agent/agent_loop.py

# 交互线程读 channel 的兜底超时(秒),防意外死锁——对齐 MVP 的 get(timeout=...)。
# 相邻两条事件的最大间隔 ≈ 一次 LLM 调用 + 一次工具执行,按真实耗时放大。
CHANNEL_TIMEOUT = 120

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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_outputdone。副作用可能已完成,但用户只收到超时且拿不到结果。请在执行前或执行中发送心跳,或让等待上限覆盖模型和工具的最大时长。

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% · 可直接回复讨论 · 👍/👎 会影响后续审查

Comment thread core/agent/agent_loop.py

def _first_character_name(rt: Any) -> str:
chars = getattr(rt.config.config, "characters", None) or []
return chars[0].name if chars else ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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% · 可直接回复讨论 · 👍/👎 会影响后续审查

Copilot AI review requested due to automatic review settings July 20, 2026 11:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

Copilot AI review requested due to automatic review settings July 20, 2026 15:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review is ineligible. To be eligible to request a review, you need a paid Copilot license, or your organization must enable Copilot code review.

@RachelForster

Copy link
Copy Markdown
Owner

非常感谢你的贡献,在merge前能否根据修改意见修改一下如下内容:

  1. 请勿把所有的工具都标记为low risk,file_delete这样的工具如果不经过用户的同意调用,就有可能是我们的锅了,请还是让用户同意,甩锅给用户
  2. feat(agent): add background task loop with macOS automation and web search #245 (comment) 这个bug,要考虑下平台兼容性,不能在非MacOS平台上加载MacOS工具。

还有其他bot提出的comment也请address一下,感谢!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants