Skip to content

feat(realtime): schedule mutations, real database, and command.result fix - #210

Merged
Wintercom merged 7 commits into
1024XEngineer:mainfrom
LUPENGHAN:feature/realtime-tools
Aug 12, 2026
Merged

feat(realtime): schedule mutations, real database, and command.result fix#210
Wintercom merged 7 commits into
1024XEngineer:mainfrom
LUPENGHAN:feature/realtime-tools

Conversation

@LUPENGHAN

@LUPENGHAN LUPENGHAN commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

关联 Issue

#197 —— 完成其中的写操作部分,在 #201(真机测通只读查询)之上继续。

改动

#201 建好的边界后面,把「只能查」补成「能查能改」,并把数据层从占位实现换成真数据库。

文件 作用
intelligence/realtime/schedule_tools.py ToolBox 新增 schedule_create / schedule_update / schedule_delete 三个工具的 schema 与执行,和已有的 schedule_query 并列
intelligence/realtime/tool_mapping.py 模型给的参数 → CreateScheduleCommand / UpdateScheduleCommand / Delete{Once,Recurring}ScheduleCommand 的映射与校验
intelligence/realtime/instructions.py system prompt 加改动日程的规矩:改/删前必须先 schedule_query 拿 id 和 revision(不能凭印象编)、删除前必须确认、工具失败要如实说
gateway/websocket/handlers/agent_audio.py _StreamIdentity 带上 account_id,工具按账号隔离,不靠调用方自觉
main.py 接上 ScheduleApplicationService + SqlAlchemyScheduleUnitOfWork#192#200 已经在 upstream 的实现),换掉本地重复写的数据层(约 900 行)
intelligence/ports.py CommandResult 拆成可选的 schedule / schedules 两个字段,不再共用一个必填 dict
gateway/websocket/agent_ports.py CommandOutcome protocol 同步加 schedules
gateway/websocket/messages/agent.py VoiceCommandResultPayload 同步加 schedules,序列化时 exclude_none 掉不适用的那个
gateway/websocket/handlers/agent_result.py deliver_result 把两个字段分别透传,不再塞进同一个
intelligence/realtime/agent.py tool_requested 不再把整个 outcome 塞进 schedule,改成分别取 outcome["schedule"] / outcome["schedules"]
infrastructure/settings.py.env.example TIMEFLOW_VOICE_AGENT_MODE 开关:1 走实时模型(本 PR),2 走级联管线,管线还没接 Agent 端口前直接拒绝启动,不允许静默用错后端

顺手修的一个 bug:command.result 嵌套

RealtimeAgent.tool_requested 把整个工具 outcome({operation, status, schedule})塞进了 CommandResult.schedule,客户端拿到的是 payload.schedule.schedule.title,不是协议 §5.5 要求的 payload.schedule.title——日程字段全部读不到。查询那条路径(§5.6 要求的 payload.schedules 复数)原来更是完全没地方放。上面表格里那几处改动把这个修掉,两条路径都补了回归测试。

真机实测

docs/tools/serve.py + ws-console.html 起真实语音(非合成音频)跑通两条路径,数据库用的是真的 Postgres,不是假实现:

场景 结果
「明天下午三点开会」→ schedule_create asr.completed → command.result 1134.7 ms;payload.schedule.title = 「开会」、start_time = 2026-08-13T15:00:00+08:00,字段齐全
「明天有什么安排」→ schedule_query payload.schedules 返回 5 条真实记录(含一条周期日程),扁平结构,没有再套一层

验证

  • mypy src:64 文件全过
  • pytest:415 passed / 37 skipped(跳过的是需要 TIMEFLOW_TEST_DATABASE_URL 的 postgres 集成测试)
  • ruff check / ruff format --check:全过

本轮不含

  • 周期日程删除的 this_occurrence / this_and_future 范围依赖 occurrence override 序列化,这是继承自 feat(schedule): implement Agent schedule application service #200 的既有缺口,不是本 PR 引入,也没打算在这里顺手补
  • TIMEFLOW_VOICE_AGENT_MODE=2(级联管线)本 PR 只加了开关和 fail-closed 保护,管线本身没有实现

@vercel

vercel Bot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
timeflow Ready Ready Preview Aug 12, 2026 9:07am

@fennoai fennoai 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.

Found one correctness issue in datetime normalization. Static validation otherwise completed; the targeted pytest suite could not run because uv is unavailable in the environment.

View job run

Comment thread backend/src/timeflow/intelligence/realtime/tool_mapping.py Outdated
LUPENGHAN added a commit to LUPENGHAN/timeflow that referenced this pull request Aug 12, 2026
normalize_datetime_args decided whether a datetime string already carried
a timezone by checking for a literal "+" or "Z", which misses negative
offsets like "-05:00". fromisoformat parses that as already aware, but
the code then force-replaced its tzinfo with LOCAL anyway -- silently
shifting the instant by the difference between the two zones (e.g. a
schedule meant for 07:00-05:00 gets stored as 07:00+08:00, 13 hours off).

Parse first and only attach LOCAL when the result is genuinely naive,
matching how _optional_datetime already decides the same thing.

Found by fennoai's review on PR 1024XEngineer#210.
LUPENGHAN and others added 7 commits August 12, 2026 17:02
…apping layer

- Add account_id to StreamInfo/StreamIdentity/StreamContext protocols
- Change RealtimeAgent to use tools_factory for per-account tool binding
- Extract tool mapping functions from conversation layer to tool_mapping.py
- Implement ToolBox with five schedule tools and exception translation
- Add SeededScheduleService as temporary data source
- Validate candidates field for ambiguous_target questions

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- schedule_create/update/delete join the existing query tool, with
  instructions covering when to ask before touching a schedule versus
  acting on it.
- account_id now flows from the WS transport through to tool binding,
  so a session's tools are scoped to its authenticated account.
- A session stays open past a turn that asked a question, so a
  follow-up answer reaches a model that still remembers asking it.
- Hardens the Qwen-Audio transport pump: a binary frame is skipped
  rather than treated as fatal, and non-JSON text fails the turn with
  a reported reason instead of raising.
Switch main.py off the local duplicate data layer (DatabaseScheduleService
plus its own repository and transaction handling) onto the already-merged
ScheduleApplicationService + SqlAlchemyScheduleUnitOfWork (1024XEngineer#192, 1024XEngineer#200),
and update the two database-backed end-to-end tests to match. Drops
~900 lines of implementation and tests that duplicated what was already
upstream.
RealtimeAgent.tool_requested passed the whole ToolBox outcome dict
(itself {operation, status, schedule}) into CommandResult.schedule, so
the client received payload.schedule.schedule.title instead of
payload.schedule.title -- every mutation reached the client with no
usable schedule data. list_schedules had the same problem in reverse:
its matches never had anywhere correct to go at all.

CommandResult and the wire payload now carry separate optional
schedule/schedules fields, matching protocol §5.5/§5.6, and only the
one that applies is sent (schedules is exclude_none'd off the wire
for a mutation, and vice versa for a query).

Verified against a real voice round trip over the actual database, not
just the added regression tests.
…kend

Settings gains voice_agent_mode ("1" realtime model, "2" LLM+ASR+TTS
pipeline), validated against those two values. main.py's _build_agent
already dispatches on it (committed in 6b88acf); mode 2 fails closed
with a clear message since the conversation pipeline does not
implement the Agent port yet.
ruff format wanted a call collapsed onto one line; missed running the
formatter locally before the earlier commit.
normalize_datetime_args decided whether a datetime string already carried
a timezone by checking for a literal "+" or "Z", which misses negative
offsets like "-05:00". fromisoformat parses that as already aware, but
the code then force-replaced its tzinfo with LOCAL anyway -- silently
shifting the instant by the difference between the two zones (e.g. a
schedule meant for 07:00-05:00 gets stored as 07:00+08:00, 13 hours off).

Parse first and only attach LOCAL when the result is genuinely naive,
matching how _optional_datetime already decides the same thing.

Found by fennoai's review on PR 1024XEngineer#210.

@Wintercom Wintercom left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ok

@Wintercom
Wintercom merged commit 51f9342 into 1024XEngineer:main Aug 12, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants