feat(web): mount time-context from the presets that use it

`packages/bundle/web-app/cordis.patch.yml` inserted `time-context` as a Host
row, so every session in the shipped Web profile — `minimal` included —
received a per-step clock reading its composition never declared. The row
moves into the `standard`, `cordis`, and `ptc` presets; `minimal` mounts
neither the reading nor the reminder tools. The Schedule service row and the
`ui-schedule` client row stay Host-wide. 139a7eb379 carried the bundle,
preset, and runtime-closure half of this move; this commit carries the
fixtures, documentation, and Agent Notes that follow it.

The `minimal` session fixture is refreshed keylessly, the assembled-Web spec
builds its every-step overlay from the shipped preset because a preset plugin
cannot be addressed by id from a top-level patch, and the composition Agent
Note points at the new note that owns the preset-scoped reading.
This commit is contained in:
Chinesezjc committed 2026-09-24 20:50:12 +08:00
1 parent 9706f3e5f3
commit 656adfc986
22 files changed
+188 -51

No files matched your search

@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-09-24-preset-scoped-time-context.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-24-preset-scoped-time-context.md
/agent-note-preset-scoped-time-context:
en: dabc6e224d2af71c
zh: 81dafffcf9dcb0e2
/agent-note-preset-scoped-time-context/problem:
en: 5922893267c8a30f
zh: b72aa565b5fd1792
/agent-note-preset-scoped-time-context/decision:
en: 5ee50b776e057b32
zh: a5b0e538f276dd54
/agent-note-preset-scoped-time-context/alternatives-considered:
en: 3db88ad95898e9ee
zh: aa211bd55052331c
/agent-note-preset-scoped-time-context/consequences:
en: 08db62d823472333
zh: 370e5f8b496551a2
/agent-note-preset-scoped-time-context/testing:
en: 0182b06b2993ffdc
zh: 234bb67c23c58c49
@@ -0,0 +1,37 @@
# Agent Note: Preset-scoped time context
Status: implemented
English | [中文](2026-09-24-preset-scoped-time-context.zh.md)
## Problem
`packages/bundle/web-app/cordis.patch.yml` inserted `time-context` in its Host row list, so every Agent in the shipped `web` profile appended one durable clock reading per eligible step. The `minimal` preset composes only a persona and a persistent shell: it declares no reminder tool, and no row in it turns the reading into a scheduled target, yet each eligible step still appended a user message that the model received.
## Decision
`packages/bundle/web-app/presets/standard.patch.yml`, `ptc.patch.yml`, and `cordis.patch.yml` each declare `time-context` among their Agent-context rows; `minimal.patch.yml` does not, and `packages/bundle/web-app/cordis.patch.yml` no longer carries the row. `packages/bundle/web-app/package.json` keeps declaring `@deepseek-ai/dsh-time-context`, which `verify-cordis-config` requires for the bare row name in a bundle patch.
The boundary this states is that an injection carrying time belongs to the preset that consumes it. A preset decides whether its Agents receive a clock reading, so the reading travels with the reminder tools that consume it; the `schedule` Host service row and the `ui-schedule` client row are preset-independent surfaces and stay in the Host roster. [The shipped Web composition](2026-09-24-web-default-schedule-composition.md) owns which Host rows and client surfaces the `web` profile mounts; this record owns the reading's preset placement.
The plugin is unchanged: it still appends the sampled instant, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message, at the configured minimum interval.
## Alternatives considered
**Keep the row in the Host row list.** `minimal` would keep receiving a model-visible message that nothing in its composition consumes, contradicting the rule that moved the reminder tools to preset declarations ([preset-scoped Schedule tools](2026-09-24-preset-scoped-schedule-tools.md)).
**Declare the row in `minimal` too.** That would make the placement uniform across presets, but it keeps the same unused durable message in the capability-poor composition, so it pays the cost the change removes.
**Give the Schedule Host service the clock instead.** The service validates an offset-bearing `at` value or an explicit `time_zone`; it reads no browser, Session, process, or model context, and the reading reaches the model only as prompt content. The Host service row therefore does not need the injection, and moving clock sampling into storage would couple two independently owned decisions.
## Consequences
- A `minimal` session appends no time-context message, so its model receives no clock reading and must ask for any date or time the user leaves unqualified.
- `standard`, `cordis`, and `ptc` sessions keep the reading at the shipped 10-minute minimum interval, and the four reminder tools stay in the same three presets.
- The reading is model-visible and durable, so it replays, compacts, and appears in exported Session logs; `snapshots/web/minimal-preset/session.v4.jsonl` no longer records one.
- A profile patch layer cannot disable or reconfigure the row by id: `applyEntryPatches` reaches a loaded entry or a group's children, and a preset's declared plugins sit inside the preset row's `config.plugins`. Changing the row means restating that row, which is how the Web editor saves preset edits.
- A preset mount reaches that preset's subagents, so a `standard`, `cordis`, or `ptc` child receives the reading its parent receives.
## Testing
`apps/web/tests/schedule-after.e2e.ts` pins the shipped roster: `schedule` as a Host row, `time-context` in the `standard`, `ptc`, and `cordis` preset declarations and absent from `minimal`. Its every-step overlay restates `preset-standard`'s declared plugins instead of patching the row by id. `apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts` pins the Host rows by name and settled state, where `time-context` no longer appears.
@@ -0,0 +1,37 @@
# Agent Note: 按 preset 归属的时间上下文
Status: implemented
[English](2026-09-24-preset-scoped-time-context.md) | 中文
## Problem
`packages/bundle/web-app/cordis.patch.yml` 曾把 `time-context` 插入其宿主行列表,因此发布版 `web` profile 中每个 Agent 都会在每个符合条件的步骤追加一条持久时钟读数。`minimal` preset 只组合 persona 与 persistent shell:它不声明提醒工具,其中也没有任何行会把该读数变成定时目标,但每个符合条件的步骤仍会追加一条模型能收到的 user 消息。
## Decision
`packages/bundle/web-app/presets/standard.patch.yml`、`ptc.patch.yml` 与 `cordis.patch.yml` 各自在其 Agent 上下文行中声明 `time-context`;`minimal.patch.yml` 不声明,`packages/bundle/web-app/cordis.patch.yml` 也不再携带该行。`packages/bundle/web-app/package.json` 继续声明 `@deepseek-ai/dsh-time-context`,这是 `verify-cordis-config` 对 bundle patch 中裸包名的要求。
这里确立的边界是:携带时间的注入归属于消费它的 preset。由 preset 决定其 Agent 是否收到时钟读数,因此该读数与消费它的提醒工具同行;`schedule` 宿主服务行与 `ui-schedule` 客户端行是与 preset 无关的界面,保留在宿主行清单中。[发布版 Web 组合](2026-09-24-web-default-schedule-composition.zh.md)负责 `web` profile 挂载哪些宿主行与客户端界面;本记录负责该读数的 preset 归属。
插件本身没有改动:它仍按配置的最小间隔追加采样瞬时、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长。
## Alternatives considered
**把该行留在宿主行列表。** 这样 `minimal` 会继续收到其组合中无任何消费者使用的模型可见消息,与把提醒工具改为 preset 声明所依据的规则相矛盾([按 preset 提供的 Schedule 工具](2026-09-24-preset-scoped-schedule-tools.zh.md))。
**在 `minimal` 中也声明该行。** 这会让各 preset 的归属看起来一致,但会在能力贫乏的组合中保留同一条无人使用的持久消息,因此它仍要付出本改动所消除的成本。
**把时钟交给 Schedule 宿主服务。** 该服务校验带偏移量的 `at` 值或显式 `time_zone`;它不读取浏览器、Session、进程或模型上下文,读数只作为提示词内容到达模型。因此宿主服务行不需要该注入,把时钟采样搬进存储也会把两个各自独立归属的决策耦合起来。
## Consequences
- `minimal` 会话不再追加 time-context 消息,其模型收不到时钟读数,因此用户未明确限定的任何日期或时间都必须询问。
- `standard`、`cordis` 与 `ptc` 会话保持发布版的 10 分钟最小间隔读数,且四个提醒工具仍留在这三个 preset 中。
- 读数对模型可见且持久,因此它参与回放、压缩并出现在导出的 Session 日志中;`snapshots/web/minimal-preset/session.v4.jsonl` 不再记录它。
- profile patch 层无法按 id 禁用或改配该行:`applyEntryPatches` 只能触达已加载的条目或某个 group 的子项,而 preset 声明的插件位于 preset 行的 `config.plugins` 内。改动该行意味着重述该行,这也是 Web 编辑器保存 preset 编辑的机制。
- preset 挂载会覆盖该 preset 的子 agent,因此 `standard`、`cordis` 或 `ptc` 的子 agent 会收到与其父级相同的读数。
## Testing
`apps/web/tests/schedule-after.e2e.ts` 固化发布版清单:`schedule` 为宿主行,`time-context` 出现在 `standard`、`ptc` 与 `cordis` 的 preset 声明中,且不在 `minimal` 中。其每步读数 overlay 通过重述 `preset-standard` 声明的插件实现,而不是按 id patch 该行。`apps/cli/tests/profiles/web/tests/web-default-isolation.expected.e2e.ts` 按名称与稳定状态固化宿主行,`time-context` 已不再出现在其中。
@@ -9,14 +9,14 @@
en: 2871fd5ed881c007
zh: d03c78d402d186da
/agent-note-schedule-in-the-shipped-web-composition/decision:
en: 933861ac13a3d203
zh: 5638c88357da524e
en: 1c6b2e91174b0dda
zh: 208f752074d5baf3
/agent-note-schedule-in-the-shipped-web-composition/recorded-sessions:
en: 5539a42052f4ddb7
zh: 98cc190ae63b4549
en: 8046c8613d7943d7
zh: 670284a39525f134
/agent-note-schedule-in-the-shipped-web-composition/alternatives-considered:
en: ca032a407c2ee8cb
zh: 748a4fcf2485a3da
/agent-note-schedule-in-the-shipped-web-composition/consequences:
en: dfefede418b7a769
zh: b37706c550e4d790
en: e2fd90c689553540
zh: 3ccb9b7f1a3b8cc0
@@ -10,17 +10,17 @@ The Automation tasks page, the Session reminder catalog, and the `schedule_*` to
## Decision
`packages/bundle/web-app/cordis.patch.yml` inserts `time-context` and `schedule` in its Host row list and leaves `ui-schedule` enabled; `packages/bundle/web-app/package.json` declares both packages, which `verify-cordis-config` requires for a bare row name in a bundle patch. The `web` profile consequently ships the Automation tasks page, the Session-header reminder clock, the idle Session row's clock mark and hover list, the right-Sidebar task tab, and, in its `standard`, `cordis`, and `ptc` presets, `schedule_create`, `schedule_list`, `schedule_update`, and `schedule_delete` from [the preset-scoped tool package](2026-09-24-preset-scoped-schedule-tools.md); the `minimal` preset ships no reminder tool.
`packages/bundle/web-app/cordis.patch.yml` inserts `schedule` in its Host row list and leaves `ui-schedule` enabled; `packages/bundle/web-app/package.json` declares `schedule`, `ui-schedule`, and `time-context`, which `verify-cordis-config` requires for a bare row name in a bundle patch. `time-context` is a preset row in `standard`, `cordis`, and `ptc`, not a Host row ([preset-scoped time context](2026-09-24-preset-scoped-time-context.md)). The `web` profile consequently ships the Automation tasks page, the Session-header reminder clock, the idle Session row's clock mark and hover list, the right-Sidebar task tab, and, in its `standard`, `cordis`, and `ptc` presets, `schedule_create`, `schedule_list`, `schedule_update`, and `schedule_delete` from [the preset-scoped tool package](2026-09-24-preset-scoped-schedule-tools.md); the `minimal` preset ships neither a reminder tool nor a clock reading.
`apps/cli/config/examples/schedule/cordis.yml` is deleted. `applyEntryPatches` appends an `insert` list without de-duplicating ids, so keeping the overlay would mount `time-context` and `schedule` a second time; the two Web suites and the preview packer that named the overlay compose the shipped profile alone.
`apps/cli/config/examples/schedule/cordis.yml` is deleted. `applyEntryPatches` appends an `insert` list without de-duplicating ids, so keeping the overlay would mount `schedule` a second time; the two Web suites and the preview packer that named the overlay compose the shipped profile alone.
`time-context` ships with Schedule because a reminder request states a wall-clock target. The plugin appends one durable user message per eligible step carrying the sampled instant, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message. The sampling instant is what lets the model turn a request such as "tomorrow at nine" into an offset-bearing `at` value; the [Schedule subsystem](../../../../docs/subsystems/schedule.md) owns that interpretation boundary and the explicit-zone requirement it feeds.
`time-context` ships with Schedule because a reminder request states a wall-clock target. The plugin appends one durable user message per eligible step carrying the sampled instant, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message. The sampling instant is what lets the model turn a request such as "tomorrow at nine" into an offset-bearing `at` value; the [Schedule subsystem](../../../../docs/subsystems/schedule.md) owns that interpretation boundary and the explicit-zone requirement it feeds. Declaring the row in the same presets that declare the reminder tools keeps the reading with its consumer.
The Host service, its storage domain, and the client half are unchanged. This decision moves which composition mounts them, and the [Host-owned scheduled messages decision](2026-09-16-host-schedule-storage.md) continues to own task storage, activation, dispatch, and delivery records.
## Recorded sessions
A Web scenario that drives a live step now logs one time-context reading per step, so its committed Session fixture and header sidecars were refreshed. A reading's sampled instant, browser zone, and elapsed duration are volatile, and `normalizeWebSessionVolatiles` in `apps/web/tests/scaffold.ts` replaces them with `{{timeContextTimestamp}}`, `{{clientTimeZone}}`, and `{{elapsed}}`. The turn and step numbers and the preceding-event baseline stay readable, so a fixture still shows what the model received. Refresh writes the current writer generation (`session.v4.jsonl`) beside the retained predecessors, which remain committed replay baselines.
A Web scenario in a preset that declares `time-context` logs one reading per step, so its committed Session fixture and header sidecars were refreshed. A reading's sampled instant, browser zone, and elapsed duration are volatile, and `normalizeWebSessionVolatiles` in `apps/web/tests/scaffold.ts` replaces them with `{{timeContextTimestamp}}`, `{{clientTimeZone}}`, and `{{elapsed}}`. The turn and step numbers and the preceding-event baseline stay readable, so a fixture still shows what the model received. Refresh writes the current writer generation (`session.v4.jsonl`) beside the retained predecessors, which remain committed replay baselines. Moving the row out of `minimal` also removes its reading from `snapshots/web/minimal-preset/session.v4.jsonl`.
## Alternatives considered
@@ -32,8 +32,8 @@ A Web scenario that drives a live step now logs one time-context reading per ste
## Consequences
- Every Web session in the `standard`, `cordis`, and `ptc` presets carries four additional tool schemas in its request header, and every eligible step appends one durable user message. A conversation that never creates a reminder pays that token cost.
- Every Web session in the `standard`, `cordis`, and `ptc` presets carries four additional tool schemas in its request header, and every eligible step appends one durable user message. A conversation that never creates a reminder pays that token cost; a `minimal` session pays neither, because it declares neither the tools nor the reading.
- The clock reading is model-visible and durable, so it replays, compacts, and appears in exported Session logs like any other user message.
- A deployment that wants the previous behavior disables the `time-context`, `schedule`, and `ui-schedule` rows in its own profile patch layer; the capability itself is untouched.
- A deployment that wants the previous behavior disables the `schedule` and `ui-schedule` rows in its own profile patch layer. `time-context` is a preset row, so `applyEntryPatches` cannot reach it by id: removing or reconfiguring it means restating the preset row's `config.plugins`, the mechanism the Web editor's saved edits use. The capability itself is untouched.
- The `cordis_inspect_query` `listTools` answer for a full preset table now passes the base composition's 12,500-token inline budget, so the spill policy retains that answer's head and tail with a spill path instead of the complete JSON.
- The repository preview image and both Web suites drop their overlay arguments, so one composition change reaches every surface at once.
@@ -10,17 +10,17 @@ Status: implemented
## Decision
`packages/bundle/web-app/cordis.patch.yml` 在其宿主行列表中插入 `time-context` 与 `schedule`,并让 `ui-schedule` 保持启用;`packages/bundle/web-app/package.json` 声明这两个包,这是 `verify-cordis-config` 对 bundle patch 中裸包名的要求。因此 `web` profile 会交付自动化任务页面、Session 页头的提醒时钟、空闲 Session 行的时钟标记与悬停列表、右侧栏任务页签,并在其 `standard`、`cordis` 和 `ptc` preset 中通过[按 preset 提供的工具包](2026-09-24-preset-scoped-schedule-tools.zh.md)提供 `schedule_create`、`schedule_list`、`schedule_update` 与 `schedule_delete`;`minimal` preset 不交付提醒工具。
`packages/bundle/web-app/cordis.patch.yml` 在其宿主行列表中插入 `schedule`,并让 `ui-schedule` 保持启用;`time-context` 是 `standard`、`cordis` 与 `ptc` 的 preset 行,不是宿主行([按 preset 归属的时间上下文](2026-09-24-preset-scoped-time-context.zh.md))。`packages/bundle/web-app/package.json` 声明 `schedule`、`ui-schedule` 与 `time-context`,这是 `verify-cordis-config` 对 bundle patch 中裸包名的要求。因此 `web` profile 会交付自动化任务页面、Session 页头的提醒时钟、空闲 Session 行的时钟标记与悬停列表、右侧栏任务页签,并在其 `standard`、`cordis` 和 `ptc` preset 中通过[按 preset 提供的工具包](2026-09-24-preset-scoped-schedule-tools.zh.md)提供 `schedule_create`、`schedule_list`、`schedule_update` 与 `schedule_delete`;`minimal` preset 既不交付提醒工具,也不交付时钟读数。
`apps/cli/config/examples/schedule/cordis.yml` 已删除。`applyEntryPatches` 追加 `insert` 列表时不对 id 去重,保留该 overlay 会把 `time-context` 与 `schedule` 挂载两次;原本引用它的两个 Web 测试套件与预览打包器现在只组合发布版 profile。
`apps/cli/config/examples/schedule/cordis.yml` 已删除。`applyEntryPatches` 追加 `insert` 列表时不对 id 去重,保留该 overlay 会把 `schedule` 挂载两次;原本引用它的两个 Web 测试套件与预览打包器现在只组合发布版 profile。
`time-context` 随 Schedule 一起发布,因为提醒请求本身给出的是墙上时钟目标。该插件在每个符合条件的步骤追加一条持久 user 消息,包含采样瞬时、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长。正是采样瞬时让模型能把「明天九点」这类请求落成带偏移量的 `at` 值;该解释边界及其要求的显式时区由 [Schedule 子系统](../../../../docs/subsystems/schedule.zh.md)负责。
`time-context` 随 Schedule 一起发布,因为提醒请求本身给出的是墙上时钟目标。该插件在每个符合条件的步骤追加一条持久 user 消息,包含采样瞬时、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长。正是采样瞬时让模型能把「明天九点」这类请求落成带偏移量的 `at` 值;该解释边界及其要求的显式时区由 [Schedule 子系统](../../../../docs/subsystems/schedule.zh.md)负责。把该行声明在声明提醒工具的同一批 preset 中,使读数与其消费者归属一致。
宿主服务、其存储 domain 与客户端半边都没有改动。本决策改变的是由哪个组合挂载它们,任务存储、激活、投递与投递记录仍由[宿主定时消息决策](2026-09-16-host-schedule-storage.zh.md)负责。
## Recorded sessions
驱动 live 步骤的 Web 场景现在每步记录一条 time-context 读数,因此其已提交的 Session 夹具与页头 sidecar 都已重刷。一条读数中的采样瞬时、浏览器时区与经过时长是易变值,`apps/web/tests/scaffold.ts` 的 `normalizeWebSessionVolatiles` 会把它们替换为 `{{timeContextTimestamp}}`、`{{clientTimeZone}}` 和 `{{elapsed}}`。turn 与 step 序号以及"前一条事件"的基线保持可读,因此夹具仍能显示模型收到的内容。重刷会在已保留的上一代旁写入当前 writer 代次(`session.v4.jsonl`),旧代次继续作为已提交的回放基线保留。
声明 `time-context` 的 preset 所驱动的 Web 场景每步记录一条读数,因此其已提交的 Session 夹具与页头 sidecar 都已重刷。一条读数中的采样瞬时、浏览器时区与经过时长是易变值,`apps/web/tests/scaffold.ts` 的 `normalizeWebSessionVolatiles` 会把它们替换为 `{{timeContextTimestamp}}`、`{{clientTimeZone}}` 和 `{{elapsed}}`。turn 与 step 序号以及"前一条事件"的基线保持可读,因此夹具仍能显示模型收到的内容。重刷会在已保留的上一代旁写入当前 writer 代次(`session.v4.jsonl`),旧代次继续作为已提交的回放基线保留。把该行移出 `minimal` 后,`snapshots/web/minimal-preset/session.v4.jsonl` 中也不再记录这条读数。
## Alternatives considered
@@ -32,8 +32,8 @@ Status: implemented
## Consequences
- `standard`、`cordis` 和 `ptc` preset 中每个 Web 会话的请求头多出四个工具 schema,且每个符合条件的步骤追加一条持久 user 消息。从不创建提醒的对话也要承担这份 token 成本。
- `standard`、`cordis` 和 `ptc` preset 中每个 Web 会话的请求头多出四个工具 schema,且每个符合条件的步骤追加一条持久 user 消息。从不创建提醒的对话也要承担这份 token 成本;`minimal` 会话两者都不承担,因为它既不声明这些工具,也不声明这条读数。
- 时钟读数对模型可见且持久,因此它与其他 user 消息一样参与回放、压缩,并出现在导出的 Session 日志中。
- 想要恢复旧行为的部署,可在自己的 profile patch 层禁用 `time-context`、`schedule` 与 `ui-schedule` 三行;能力本身没有改动。
- 想要恢复旧行为的部署,可在自己的 profile patch 层禁用 `schedule` 与 `ui-schedule` 两行。`time-context` 是 preset 行,`applyEntryPatches` 无法按 id 触达它:移除或改配它意味着重述该 preset 行的 `config.plugins`,也就是 Web 编辑器保存改动的机制。能力本身没有改动。
- 完整预设表的 `cordis_inspect_query` `listTools` 答案现在超过基础组合 12,500 token 的内联预算,因此 spill 策略改为保留该答案的首尾并给出 spill 路径,而不是完整 JSON。
- 仓库预览镜像与两个 Web 测试套件都不再传 overlay 参数,一处组合改动即同时作用于所有界面。
@@ -25,8 +25,9 @@ it('boots default Web without experimental modules or an active built-in Browser
expect.objectContaining({ name: '@deepseek-ai/dsh-host-webserver', state: FiberState.ACTIVE }),
expect.objectContaining({ name: '@deepseek-ai/dsh-client-modules', state: FiberState.ACTIVE }),
// The Loader rewrites an inserted row's id (`include:ui-schedule`), so
// the shipped rows are pinned by name and settled state.
expect.objectContaining({ name: '@deepseek-ai/dsh-time-context', state: FiberState.ACTIVE }),
// the shipped Host rows are pinned by name and settled state. A preset's
// declared plugins mount in the registry-owned preset tree, so
// `time-context` is not among these rows.
expect.objectContaining({ name: '@deepseek-ai/dsh-schedule', state: FiberState.ACTIVE }),
expect.objectContaining({ name: '@deepseek-ai/dsh-client-ui-schedule', state: FiberState.ACTIVE }),
]))
@@ -1,4 +0,0 @@
# Browser-zone assertions need a fresh reading after the non-browser preparation turn.
- id: time-context
config:
refreshIntervalMs: 0
+52 -7
View File
@@ -1,13 +1,16 @@
/** Keyless assembled-Web evidence for conversational Schedule delivery. */
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { readFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { dump } from 'js-yaml'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import type { Agent, AgentHandle } from '@deepseek-ai/dsh-agent'
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
import { bundlePatchPaths, composeEntries, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
import { MessageId, ToolCallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
import { appendDelivery } from '../../../packages/schedule/schedule/src/delivery-history.ts'
@@ -70,6 +73,37 @@ const CATALOG_EXPECTED = join(CATALOG_SNAPSHOT_DIR, 'catalog.expected.md')
const BASE_PATCH = fileURLToPath(new URL('../../../packages/bundle/base/cordis.patch.yml', import.meta.url))
const WEB_BUNDLE = fileURLToPath(new URL('../../../packages/bundle/web-app/', import.meta.url))
const WEB_PATCHES = bundlePatchPaths(WEB_BUNDLE, (JSON.parse(readFileSync(join(WEB_BUNDLE, 'package.json'), 'utf8')) as { dsh: { bundle: { patch: string[] } } }).dsh.bundle)
/**
* Write the overlay that makes the shipped `time-context` row read every
* eligible step.
*
* `time-context` is a preset row, and a top-level patch reaches only ids in
* the loaded entry list or a group's children, so no patch can target one
* declared plugin. The overlay restates the shipped `preset-standard` row
* with that single config change, keeping the scenario on the shipped
* composition.
* @param dir - Directory that receives the overlay file.
* @returns Absolute path of the written overlay.
*/
async function writeEveryStepOverlay(dir: string): Promise<string> {
const layers = [
loadOverlayPatches('Schedule Web every-step overlay', BASE_PATCH),
...WEB_PATCHES.map(file => loadOverlayPatches('Schedule Web every-step overlay', file)),
]
const row = composeEntries(layers).find(entry => entry.id === 'preset-standard')
if (row === undefined) throw new Error('the shipped Web surface declares no preset-standard row')
const config = row.config as { plugins: Array<{ id?: string; config?: unknown }> }
const plugins = config.plugins.map(plugin => plugin.id === 'time-context'
? { ...plugin, config: { refreshIntervalMs: 0 } }
: plugin)
const path = join(dir, 'time-context-every-step.patch.yml')
await writeFile(path, dump(
[{ id: 'preset-standard', config: { ...config, plugins } }],
{ schema: entryListSchema, noRefs: true, lineWidth: -1 },
))
return path
}
const CATALOG_NOW = Date.parse('2099-08-25T12:00:00.000Z')
const CATALOG_SESSION_ID = SessionId('schedule-catalog-web-e2e')
const CATALOG_TITLE = 'Active schedule catalog'
@@ -266,13 +300,15 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
let everyAssistantReply: SessionEvent<'assistant/message'> | undefined
let everyRecords: readonly [EveryScheduleRecord, EveryScheduleRecord]
let tripwire: ReturnType<typeof watchConsole>
let overlayRoot: string
const afterAdapter = new ReminderAdapter()
const atAdapter = new BrowserZoneAtAdapter()
const everyAdapter = new EveryReminderAdapter()
beforeAll(async () => {
overlayRoot = await mkdtemp(join(tmpdir(), 'dsh-schedule-every-step-'))
scaffold = await launchWebScaffold({
extraOverlayPath: fileURLToPath(new URL('./fixtures/time-context-every-step.patch.yml', import.meta.url)),
extraOverlayPath: await writeEveryStepOverlay(overlayRoot),
})
scaffold.ctx.effect(
() => scaffold.ctx.llm.registerAdapter([AFTER_PROVIDER], afterAdapter),
@@ -447,6 +483,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => {
await everyHandle?.dispose().catch((error: unknown) => failures.push(error))
await afterHandle?.dispose().catch((error: unknown) => failures.push(error))
await scaffold?.close().catch((error: unknown) => failures.push(error))
await rm(overlayRoot, { recursive: true, force: true }).catch((error: unknown) => failures.push(error))
if (failures.length === 1) throw failures[0]
if (failures.length > 1) throw new AggregateError(failures, 'Schedule Web evidence teardown failed')
})
@@ -847,12 +884,20 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
name: '@deepseek-ai/dsh-client-ui-schedule',
})
expect(shipped.find(entry => entry.id === 'ui-schedule')?.disabled).toBeUndefined()
for (const row of [
{ id: 'time-context', name: '@deepseek-ai/dsh-time-context' },
{ id: 'schedule', name: '@deepseek-ai/dsh-schedule' },
]) {
expect(shipped.filter(entry => entry.id === row.id && entry.name === row.name)).toHaveLength(1)
expect(shipped.filter(entry => entry.id === 'schedule' && entry.name === '@deepseek-ai/dsh-schedule'))
.toHaveLength(1)
// `time-context` belongs to a preset, not the Host roster: `standard`,
// `cordis`, and `ptc` declare it and `minimal` does not.
const presetPlugins = (id: string): Array<{ id?: string; name?: string }> => {
const row = shipped.find(entry => entry.id === id)
if (row === undefined) throw new Error(`missing shipped preset row ${id}`)
return (row.config as { plugins: Array<{ id?: string; name?: string }> }).plugins
}
for (const id of ['preset-standard', 'preset-ptc', 'preset-cordis']) {
expect(presetPlugins(id).filter(row => row.id === 'time-context'
&& row.name === '@deepseek-ai/dsh-time-context')).toHaveLength(1)
}
expect(presetPlugins('preset-minimal').some(row => row.name === '@deepseek-ai/dsh-time-context')).toBe(false)
await page.getByRole('button', { name: 'Automation tasks', exact: true }).click()
const manager = page.getByTestId('task-manager-page')
+2 -2
View File
@@ -9,8 +9,8 @@
en: 41cd197b5c74bfa3
zh: 18ed11259036d726
/host-wide-schedule/absolute-time-input:
en: e730fda52ba2854f
zh: 01a6c682e327a133
en: 7ef8f165bf37d3e1
zh: 4d004cbcd84c47f6
/host-wide-schedule/daily-wall-clock-input:
en: 2d249edc3dfccb8a
zh: 33fecda132b03b84
+1 -1
View File
@@ -116,7 +116,7 @@ interface LocalAtInput {
type AtInput = string | LocalAtInput
```
The shipped Web bundle mounts time-context, which samples the browser's IANA zone for every prompt. Time-context tells the model to interpret otherwise-unqualified natural-language dates and times in that request-local zone when the open turn has one unambiguous browser zone; mixed or missing browser-zone records tell the model to ask. That guidance is not a durable Session default: the model must still pass an offset in the string form or `time_zone` in the local form, and Schedule never reads browser, Session, process, or model context.
The shipped Web profile mounts time-context in its `standard`, `cordis`, and `ptc` presets, which sample the browser's IANA zone for every prompt. Time-context tells the model to interpret otherwise-unqualified natural-language dates and times in that request-local zone when the open turn has one unambiguous browser zone; mixed or missing browser-zone records tell the model to ask. That guidance is not a durable Session default: the model must still pass an offset in the string form or `time_zone` in the local form, and Schedule never reads browser, Session, process, or model context.
Schedule rejects invalid offsets and zones, offset-free strings, non-future targets, and local times inside daylight-saving gaps. A daylight-saving overlap chooses its first, earlier instant. Successful creation stores only canonical UTC `scheduledAt`, so replay never depends on ambient time-zone state.
+1 -1
View File
@@ -116,7 +116,7 @@ interface LocalAtInput {
type AtInput = string | LocalAtInput
```
发布的 Web bundle 会挂载 time-context,它为每条提示词采样浏览器的 IANA 时区。当 open turn 只有一个无歧义的浏览器时区时,Time-context 会告诉模型按该请求本地时区解释未明确限定时区的自然语言日期和时间;浏览器时区记录混合或缺失时,则告诉模型询问用户。该指引不是持久 Session 默认值:模型仍必须在字符串形式中传入偏移量,或在本地形式中传入 `time_zone`;Schedule 绝不会读取浏览器、Session、进程或模型上下文。
发布的 Web profile 在 `standard`、`cordis` 与 `ptc` preset 中挂载 time-context,它为每条提示词采样浏览器的 IANA 时区。当 open turn 只有一个无歧义的浏览器时区时,Time-context 会告诉模型按该请求本地时区解释未明确限定时区的自然语言日期和时间;浏览器时区记录混合或缺失时,则告诉模型询问用户。该指引不是持久 Session 默认值:模型仍必须在字符串形式中传入偏移量,或在本地形式中传入 `time_zone`;Schedule 绝不会读取浏览器、Session、进程或模型上下文。
Schedule 会拒绝无效偏移量与时区、不带偏移量的字符串、非未来目标,以及落在夏令时缺口内的本地时间。遇到夏令时重叠时,会选择第一次出现的较早时点。创建成功后只存储规范化后的 UTC `scheduledAt`,因此回放绝不依赖环境时区状态。
+2 -2
View File
@@ -12,8 +12,8 @@
en: b3b2a6e0ce02bdee
zh: 2ddb168de79fd19c
/schedule-reminders/create-reminders:
en: 9e2d609ac6281604
zh: ce916b2d3ef96ab2
en: 3e87d4422c7f274b
zh: f1736607eda4464b
/schedule-reminders/inspect-and-delete-tasks:
en: a44fc1aabc2ab957
zh: 31f444d4c7fd5c7a
+1 -1
View File
@@ -17,7 +17,7 @@ Create reminders in a conversation, then inspect active and inactive tasks and e
<a id="create-reminders"></a>
## Create reminders
The shipped Web profile mounts Schedule with the clock context that gives the model the current time and the browser's zone. Configure a model provider before asking it to create reminders.
The shipped Web profile mounts Schedule, and its `standard`, `cordis`, and `ptc` presets mount the clock context that gives the model the current time and the browser's zone. Configure a model provider before asking it to create reminders.
The shipped Web profile's `standard`, `cordis`, and `ptc` presets provide `schedule_create`, `schedule_list`, `schedule_update`, and `schedule_delete`; the `minimal` preset provides none.
+1 -1
View File
@@ -17,7 +17,7 @@
<a id="create-reminders"></a>
## 创建提醒
发布的 Web profile 会挂载 Schedule 及其时钟上下文,后者把当前时间和浏览器时区交给模型。在请求模型创建提醒前,先配置模型 provider。
发布的 Web profile 会挂载 Schedule,其 `standard`、`cordis` 与 `ptc` preset 会挂载时钟上下文,后者把当前时间和浏览器时区交给模型。在请求模型创建提醒前,先配置模型 provider。
发布版 Web profile 的 `standard`、`cordis` 和 `ptc` preset 提供 `schedule_create`、`schedule_list`、`schedule_update` 和 `schedule_delete`;`minimal` preset 不提供。
+2 -2
View File
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 405b30feaf48c087
/deepseek-ai-dsh-client-ui-schedule/use-this-package:
en: 3594ab6704cbd7b9
zh: 99a965f43187501b
en: c12841a44feafee4
zh: 132512b83cc90a2a
/deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source:
en: 6bd6882dbbbefcb0
zh: 03aa15f1a65d13dc
+1 -1
View File
@@ -25,7 +25,7 @@ Manage active and inactive tasks from the Automation tasks page and the right Si
<a id="use-this-package"></a>
## Use this package
The shipped Web bundle mounts `ui-schedule` with the Host Schedule service and the clock context that supplies the model's current time. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state keeps the heading's New action beside the notice. Selecting an active or inactive task opens a second detail level with Rules and Delivery records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation.
The shipped Web profile mounts `ui-schedule` with the Host Schedule service; its `standard`, `cordis`, and `ptc` presets mount the clock context that supplies the model's current time. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state keeps the heading's New action beside the notice. Selecting an active or inactive task opens a second detail level with Rules and Delivery records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation.
Selecting a task from the list opens Rules; refreshing that task preserves the selected tab. Rules edits an active task's name, instruction, and run time; the edits stay in a local draft until saved, and inactive tasks remain read-only. Unmodified Left/Right arrows cycle the tabs, Home/End select the first/last tab, and keyboard focus follows the selection. The tabs do not consume these keys with Alt, Ctrl, Meta, or Shift. The linked-Session control in the detail tab strip stays available on the Rules tab, and confirmed deletion is available from either view.
+1 -1
View File
@@ -25,7 +25,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用本包
发布的 Web bundle 将 `ui-schedule` 与 Host Schedule 服务、以及为模型提供当前时间的时钟上下文一起挂载。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示旁保留页头的“新建”操作。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。
发布的 Web profile 将 `ui-schedule` 与 Host Schedule 服务一起挂载;其 `standard`、`cordis` 与 `ptc` preset 挂载为模型提供当前时间的时钟上下文。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示旁保留页头的“新建”操作。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。
从列表选择任务时默认打开“规则”;刷新同一任务会保留当前页签。活动任务的“规则”修改名称、指令和运行时间;这些修改保留在本地草稿中直到保存,未运行任务保持只读。不带修饰键的左右方向键循环切换页签,Home/End 选择第一个/最后一个页签,键盘焦点随选择移动。页签不会拦截这些键与 Alt、Ctrl、Meta 或 Shift 的组合。共用的会话入口位于详情页签条内,在“规则”页签可见;确认删除操作在两种页签下都可用。
@@ -9,8 +9,8 @@
en: ec051915b3d81cda
zh: 3a0ef832e3a5e419
/deepseek-ai-dsh-time-context/summary:
en: e4e308b9fd3fcd56
zh: 9fb4d40ebe621d6a
en: c7c30dd707db3d37
zh: fcad976c4ff7ad2c
/deepseek-ai-dsh-time-context/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
+1 -1
View File
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
`dsh-time-context` gives the model a clock: on eligible steps it appends a durable, source-attributed reading with the current time, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message. It helps the model interpret otherwise-unqualified dates and times in the user's browser zone, and tells it to ask when current-turn browser zones are mixed or missing. The shipped Web bundle mounts it with Schedule. Readings default to a 10-minute minimum interval; `refreshIntervalMs: 0` injects at every eligible step.
`dsh-time-context` gives the model a clock: on eligible steps it appends a durable, source-attributed reading with the current time, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message. It helps the model interpret otherwise-unqualified dates and times in the user's browser zone, and tells it to ask when current-turn browser zones are mixed or missing. The shipped Web profile mounts it in the `standard`, `cordis`, and `ptc` presets, beside the reminder tools that use it; `minimal` mounts neither. Readings default to a 10-minute minimum interval; `refreshIntervalMs: 0` injects at every eligible step.
## Table of Contents
+1 -1
View File
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
`dsh-time-context` 给模型一只时钟:在符合条件的步骤上,它追加一条持久、带来源的读数,包含当前时间、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长。它帮助模型按用户的浏览器时区解释未明确限定时区的日期与时间;时区来源混杂或缺失时,它告诉模型去询问。发布的 Web bundle 会随 Schedule 一起挂载它。读数默认采用 10 分钟的最小间隔;`refreshIntervalMs: 0` 会在每个符合条件的步骤注入。
`dsh-time-context` 给模型一只时钟:在符合条件的步骤上,它追加一条持久、带来源的读数,包含当前时间、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长。它帮助模型按用户的浏览器时区解释未明确限定时区的日期与时间;时区来源混杂或缺失时,它告诉模型去询问。发布的 Web profile 在 `standard`、`cordis` 与 `ptc` preset 中把它挂载在消费它的提醒工具旁;`minimal` preset 两者都不挂载。读数默认采用 10 分钟的最小间隔;`refreshIntervalMs: 0` 会在每个符合条件的步骤注入。
## 目录
@@ -8,15 +8,14 @@
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: printf 'MINIMAL_BASH_CARD_OK\\n'. Then reply exactly MINIMAL_PRESET_REQUEST_OK and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"Time sampled while preparing turn 1, step 1: {{timeContextTimestamp}}\nBrowser time zone for this request: {{clientTimeZone}}. Ask the user to clarify otherwise-unqualified dates and times.\nElapsed since the preceding model-visible message: {{elapsed}}"}],"source":{"kind":"time-context","form":"snapshot","sections":[{"name":"time-context","text":"Time sampled while preparing turn 1, step 1: {{timeContextTimestamp}}\nBrowser time zone for this request: {{clientTimeZone}}. Ask the user to clarify otherwise-unqualified dates and times.\nElapsed since the preceding model-visible message: {{elapsed}}"}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"}
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":128000}}
{"type":"session/title","data":{"title":"Use the bash tool to","messageSeqs":[8],"source":{"kind":"fallback"}}}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"minimal-bash-card","name":"bash","arguments":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":4},"stream":[{"type":"chunk","time":1790242321072,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790242321084,"index":0,"dt":[],"id":"minimal-bash-card","name":"bash","args":["{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"]},{"type":"chunk","time":1790242321095,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"minimal-bash-card","name":"bash","arguments":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}}},{"type":"chunk","time":1790242321105,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}},{"type":"chunk","time":1790242321117,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"minimal-bash-card","name":"bash","arguments":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:3}}"},"usage":{"inputTokens":10,"outputTokens":4},"stream":[{"type":"chunk","time":1790253890050,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790253890062,"index":0,"dt":[],"id":"minimal-bash-card","name":"bash","args":["{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"]},{"type":"chunk","time":1790253890073,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"minimal-bash-card","name":"bash","arguments":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}}},{"type":"chunk","time":1790253890084,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}},{"type":"chunk","time":1790253890094,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"minimal-bash-card","name":"bash","arguments":"{\"command\":\"printf 'MINIMAL_BASH_CARD_OK\\\\n'\"}"}}
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"minimal-bash-card"},"toolCallId":"minimal-bash-card","content":[{"type":"text","text":"MINIMAL_BASH_CARD_OK\n[Command finished with exit code 0]"}],"isError":false,"id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"minimal-bash-card"},"toolCallId":"minimal-bash-card","content":[{"type":"text","text":"MINIMAL_BASH_CARD_OK\n[Command finished with exit code 0]"}],"isError":false,"id":"{{message:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"step/start","data":{"turn":1,"step":2}}
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":4},"stream":[{"type":"chunk","time":1790242321485,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1790242321496,"index":0,"dt":[],"texts":["MINIMAL_PRESET_REQUEST_OK"]},{"type":"chunk","time":1790242321507,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}}},{"type":"chunk","time":1790242321519,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}},{"type":"chunk","time":1790242321529,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":4},"stream":[{"type":"chunk","time":1790253891222,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1790253891233,"index":0,"dt":[],"texts":["MINIMAL_PRESET_REQUEST_OK"]},{"type":"chunk","time":1790253891244,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}}},{"type":"chunk","time":1790253891256,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}},{"type":"chunk","time":1790253891266,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":2}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}