@yeaft/webchat-agent 1.0.453 → 1.0.456

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,12 +19,13 @@ export default defineTool({
19
19
  Use this to give the sub-agent more work, additional instructions, or relay
20
20
  information. The prompt is queued for the agent to process on its next turn.
21
21
 
22
- IMPORTANT — PromptAgent only QUEUES the message; it does NOT block. After this
23
- returns you almost always want to call WaitAgent next to collect the reply.
24
- Do NOT end your turn after PromptAgent without either (a) calling WaitAgent,
25
- (b) explaining to the user what you just asked the sub-agent, or (c) calling
26
- CloseAgent. The orchestration loop is
27
- SpawnAgent → (PromptAgent ↔ WaitAgent)+ → CloseAgent → final reply to user.
22
+ IMPORTANT — PromptAgent only QUEUES the message; it does NOT block. A follow-up
23
+ is unfinished until you collect the reply. After PromptAgent returns, call
24
+ WaitAgent in the same parent turn. If WaitAgent reports running/timedOut, call
25
+ WaitAgent again with a larger bounded timeout unless the agent is stale/stalled.
26
+ When the reply arrives, relay the result to the user or
27
+ continue the work that depends on it. Do not end the parent turn immediately
28
+ after PromptAgent.
28
29
 
29
30
  PromptAgent is rejected if the sub-agent is in a terminal state
30
31
  (completed/failed/closed/abandoned). Use SpawnAgent to start a fresh one.`,
@@ -32,10 +33,10 @@ PromptAgent is rejected if the sub-agent is in a terminal state
32
33
 
33
34
  用于给子 Agent 更多工作、额外指令或传递信息。提示会排队等待子 Agent 在其下一个 turn 处理。
34
35
 
35
- 重要——PromptAgent 仅将消息排队,不阻塞。返回后你通常需要立即调用 WaitAgent 来收集回复。
36
- 不要在 PromptAgent 后直接结束 turn,除非:(a) 调用 WaitAgent,(b) 向用户说明你刚让子 Agent
37
- 做了什么,或 (c) 调用 CloseAgent。编排循环为:
38
- SpawnAgent -> (PromptAgent <-> WaitAgent)+ -> CloseAgent -> 最终回复给用户。
36
+ 重要——PromptAgent 仅将消息排队,不阻塞。后续任务只有拿到回复才算完成。PromptAgent 返回后,
37
+ 父级必须在同一个 turn 调用 WaitAgent。如果 WaitAgent 返回 running/timedOut,除非 Agent 已经
38
+ stale/stalled,否则必须使用更大的有界 timeout 再次调用 WaitAgent。回复到达后,必须向用户转述结果或继续执行
39
+ 依赖该结果的工作。禁止在 PromptAgent 后立刻结束父级 turn。
39
40
 
40
41
  如果子 Agent 处于终止状态(completed/failed/closed/abandoned),PromptAgent 会被拒绝。
41
42
  用 SpawnAgent 启动新的。`
@@ -125,6 +126,11 @@ SpawnAgent -> (PromptAgent <-> WaitAgent)+ -> CloseAgent -> 最终回复给用
125
126
  content: message,
126
127
  timestamp: Date.now(),
127
128
  });
129
+ // WaitAgent uses this marker to distinguish an explicitly queued follow-up
130
+ // from an ordinary asynchronous SpawnAgent run. A bounded timeout must not
131
+ // silently downgrade the same-parent-turn collection contract.
132
+ agent.promptReplyPending = true;
133
+ agent.promptReplyPendingAt = Date.now();
128
134
  if (agent.status === STATUS.IDLE || agent.status === STATUS.CREATED) {
129
135
  agent.status = STATUS.RUNNING;
130
136
  }
@@ -132,15 +138,15 @@ SpawnAgent -> (PromptAgent <-> WaitAgent)+ -> CloseAgent -> 最终回复给用
132
138
  return JSON.stringify({
133
139
  next_steps:
134
140
  'Message is queued — the sub-agent has NOT replied yet. Call WaitAgent ' +
135
- 'next to collect the reply, then relay it to the user. Do NOT end your ' +
136
- 'turn here without either waiting for the reply or telling the user ' +
137
- 'what you just asked.',
141
+ 'in this parent turn and collect the reply. If it is still running, call ' +
142
+ 'WaitAgent again with a larger bounded timeout unless it is stale/stalled. ' +
143
+ 'Relay the reply or continue the dependent work; do NOT end now.',
138
144
  success: true,
139
145
  agentId: agent_id,
140
146
  name: agent.name,
141
147
  messageCount: agent.messages.length,
142
148
  pending: agent.pendingPrompts.length,
143
- message: `Message sent to agent "${agent.name}". Use WaitAgent to collect its reply.`,
149
+ message: `Message sent to agent "${agent.name}". Call WaitAgent now; the reply is still pending.`,
144
150
  });
145
151
  },
146
152
  });
@@ -86,6 +86,7 @@ Actions:
86
86
  },
87
87
  isConcurrencySafe: () => true,
88
88
  isReadOnly: () => true,
89
+ duplicateCallPolicy: () => 'suppress',
89
90
  async execute(input, ctx) {
90
91
  const skillManager = ctx?.skillManager;
91
92
 
@@ -46,7 +46,7 @@ export default defineTool({
46
46
  This tool returns a planning instruction. Use it to land a structured plan, then keep working in the same turn. The expected flow is:
47
47
  1. Produce a short prose plan (problem, approach, risks).
48
48
  2. Call \`TodoWrite\` with the ordered steps. Mark the first concrete step "in_progress", the rest "pending".
49
- 3. When the first-step tools and arguments are already known, emit \`TodoWrite\` and those independent tool calls in the same assistant response. Wait only when a result genuinely determines the next action.
49
+ 3. Emit \`TodoWrite\` with a first work-tool call only when that call is already necessary and its arguments and safety do not depend on another result. Start with the smallest such call; inspect its result before issuing calls it could change or make unnecessary.
50
50
 
51
51
  WHEN TO USE:
52
52
  - Multi-step implementation (3+ steps), refactor, or open-ended investigation.
@@ -65,7 +65,7 @@ The tool takes the topic plus optional guiding fields (stuckAt, userProblem, exp
65
65
  此工具返回规划指令。用它产出一份结构化计划,然后在同一个 turn 中继续工作。预期流程是:
66
66
  1. 产出简短文字计划(问题、方法、风险)。
67
67
  2. 调用 TodoWrite 写出有序步骤。将第一个具体步骤标记为 "in_progress",其余标记为 "pending"。
68
- 3. 如果第一步所需的工具和参数已经确定,应在同一个 assistant response 中发出 TodoWrite 和这些彼此独立的工具调用;只有某个结果确实决定下一动作时才等待。
68
+ 3. 只有第一个工作工具调用已经确定有必要,且其参数和安全性都不依赖其他结果时,才在同一个 assistant response 中把它与 TodoWrite 一起发出。先执行满足条件的最小调用;如果结果可能改变或使后续调用不再必要,应先检查结果。
69
69
 
70
70
  何时使用:
71
71
  - 多步骤实现(3+ 步)、重构或开放式调查。
@@ -173,8 +173,8 @@ The tool takes the topic plus optional guiding fields (stuckAt, userProblem, exp
173
173
  }
174
174
  lines.push('');
175
175
  const nextInstruction = String(language).toLowerCase().startsWith('zh')
176
- ? '下一步:产出计划并调用 `TodoWrite`。如果第一步的工具和参数已经确定,在同一个 assistant response 中一并发出这些彼此独立的工具调用。只有第一步必须询问用户时才在计划后停下。'
177
- : 'Next: produce the plan and call `TodoWrite`. If the first-step tools and arguments are already known, emit those independent tool calls in the same assistant response. Stop after the plan only when the first step must ask the user.';
176
+ ? '下一步:产出计划并调用 `TodoWrite`。只有第一个工作工具调用已经确定有必要,且其参数和安全性都不依赖其他结果时,才在同一个 assistant response 中发出这个最小调用;如果结果可能改变后续调用,先检查结果。只有第一步必须询问用户时才在计划后停下。'
177
+ : 'Next: produce the plan and call `TodoWrite`. Emit only the smallest first work-tool call whose necessity, arguments, and safety do not depend on another result; inspect its result before calls it could change. Stop after the plan only when the first step must ask the user.';
178
178
  lines.push(nextInstruction);
179
179
 
180
180
  return lines.join('\n');
@@ -36,7 +36,7 @@ WHEN TO USE:
36
36
  FIRST CALL — PLAN WITHOUT AN EXTRA MODEL ROUND:
37
37
  - Write a short visible prose plan in the same assistant response: problem, approach, and risks.
38
38
  - Call TodoWrite directly; do not call a separate planning-mode tool first.
39
- - If the first work tools and arguments are known, emit them beside TodoWrite in that same response.
39
+ - Emit TodoWrite beside the first work-tool call only when that call is already necessary and its arguments and safety do not depend on another result. Start with the smallest such call.
40
40
 
41
41
  HOW TO USE:
42
42
  - First call: enumerate all the todos with status "pending", set exactly one to "in_progress".
@@ -45,8 +45,8 @@ HOW TO USE:
45
45
  - \`content\` is the imperative form ("Run tests"); \`activeForm\` is the present-continuous shown during execution ("Running tests").
46
46
 
47
47
  BATCH WITH WORK:
48
- - Avoid an intermediate TodoWrite-only model round when the next work tool and its arguments are already known. Emit TodoWrite and those independent work tool calls in the same assistant response.
49
- - This is batching, not speculative progress: mark work completed only after evidence. Keep calls separate when a pending result can change the next action, its arguments, or its safety.
48
+ - Avoid an intermediate TodoWrite-only model round only when the next work-tool call passes the necessity, argument-independence, and safety-independence test. Emit that minimal call beside TodoWrite.
49
+ - Do not speculative-batch an investigation. Mark work completed only after evidence, and inspect a pending result before issuing any call it could change, invalidate, or make unnecessary.
50
50
  - A standalone TodoWrite remains valid when no work tool should follow, including final completion or a blocking user question.
51
51
 
52
52
  WHEN NOT TO USE:
@@ -61,7 +61,7 @@ WHEN NOT TO USE:
61
61
  首次调用——不要浪费额外模型回合进入规划模式:
62
62
  - 在同一个 assistant response 中先写简短可见计划:问题、方案和风险。
63
63
  - 直接调用 TodoWrite,不要先调用单独的规划模式工具。
64
- - 如果第一批工作工具及参数已经确定,把它们和 TodoWrite 在同一响应中发出。
64
+ - 只有第一个工作工具调用已经确定有必要,且其参数和安全性都不依赖其他结果时,才把它与 TodoWrite 在同一响应中发出;先执行满足条件的最小调用。
65
65
 
66
66
  如何使用:
67
67
  - 首次调用:枚举所有 todo,状态为 "pending",将其中恰好一个设为 "in_progress"。
@@ -70,8 +70,8 @@ WHEN NOT TO USE:
70
70
  - content 是祈使形式(如 "Run tests");activeForm 是执行时显示的进行时态(如 "Running tests")。
71
71
 
72
72
  和工作工具合批:
73
- - 如果下一项工作所用的工具和参数已经确定,不要让中间状态的 TodoWrite 单独占一个模型回合;应在同一个 assistant response 中发出 TodoWrite 和这些彼此独立的工作工具调用。
74
- - 这是合批,不是提前宣告进度:只有已有证据时才能把工作标记为完成。如果待返回结果可能改变下一动作、参数或安全性,就必须分开调用。
73
+ - 只有下一个工作工具调用通过必要性、参数独立性和安全独立性检查时,才避免让中间状态的 TodoWrite 单独占一个模型回合;把这个最小调用与 TodoWrite 一起发出。
74
+ - 不要推测性批量展开调查。只有已有证据时才能把工作标记为完成;如果待返回结果可能改变、否定或使后续调用不再必要,应先检查该结果。
75
75
  - 没有工作工具应继续执行时(包括记录最终完成态或询问阻塞问题),TodoWrite 仍可单独调用。
76
76
 
77
77
  何时不使用:
@@ -72,6 +72,10 @@
72
72
  * @property {boolean | ((input?: object) => boolean)} [cacheWithinQuery] — explicitly safe to reuse for identical calls in one query
73
73
  * @property {boolean | ((input?: object) => boolean)} [mayMutateWorkspaceAfterReturn] — may keep changing the workspace after execute() resolves; disables same-query read reuse
74
74
  * @property {(input?: object) => boolean} [isDestructive] — destructive operation?
75
+ * @property {(input?: object) => 'allow' | 'warn' | 'suppress'} [duplicateCallPolicy]
76
+ * — repeated exact-call policy for one query. Use `allow` for polling/time-varying
77
+ * tools and `suppress` only when the result is stable for the whole query.
78
+ * Errors are never counted as successful duplicates.
75
79
  * @property {'json-error-envelope' | null} [errorOutput] — explicit returned-output error contract; null means only thrown errors fail
76
80
  * @property {string} [mcpServer] — owning MCP server for flattened MCP tools
77
81
  * @property {'external' | 'run'} [sideEffectScope] — whether mutations escape the current Run collector
@@ -90,6 +94,7 @@
90
94
  * cacheWithinQuery?: boolean | ((input?: object) => boolean),
91
95
  * mayMutateWorkspaceAfterReturn?: boolean | ((input?: object) => boolean),
92
96
  * isDestructive?: (input?: object) => boolean,
97
+ * duplicateCallPolicy?: (input?: object) => 'allow' | 'warn' | 'suppress',
93
98
  * errorOutput?: 'json-error-envelope' | null,
94
99
  * mcpServer?: string,
95
100
  * sideEffectScope?: 'external' | 'run',
@@ -108,6 +113,7 @@ export function defineTool({
108
113
  cacheWithinQuery = false,
109
114
  mayMutateWorkspaceAfterReturn = false,
110
115
  isDestructive = () => false,
116
+ duplicateCallPolicy = () => 'warn',
111
117
  errorOutput = 'json-error-envelope',
112
118
  mcpServer,
113
119
  sideEffectScope = 'external',
@@ -126,6 +132,7 @@ export function defineTool({
126
132
  cacheWithinQuery,
127
133
  mayMutateWorkspaceAfterReturn,
128
134
  isDestructive,
135
+ duplicateCallPolicy,
129
136
  errorOutput,
130
137
  sideEffectScope,
131
138
  };
@@ -19,7 +19,7 @@
19
19
  * processing. The envelope flags `runningInBackground:
20
20
  * true` (the sub-agent IS continuing — it does NOT need
21
21
  * another PromptAgent to keep going) and recommends either
22
- * another WaitAgent or CloseAgent. `result` carries the
22
+ * another bounded WaitAgent or CloseAgent. `result` carries the
23
23
  * mid-stream preview (the driver keeps lastResult fresh
24
24
  * from every text_delta).
25
25
  *
@@ -43,7 +43,7 @@ import { consumeNotificationForAgent } from '../sub-agent/notifications.js';
43
43
  * otherwise eat tail-positioned nudges when `result` is long).
44
44
  *
45
45
  * @param {string} status
46
- * @param {{ timedOut?: boolean, runningInBackground?: boolean, budgetExceeded?: boolean, stale?: boolean }} [opts]
46
+ * @param {{ timedOut?: boolean, budgetExceeded?: boolean, stale?: boolean, mustCollectReply?: boolean }} [opts]
47
47
  */
48
48
  function nextStepsFor(status, opts = {}) {
49
49
  if (opts.budgetExceeded) {
@@ -63,6 +63,15 @@ function nextStepsFor(status, opts = {}) {
63
63
  'task and start a fresh agent if needed.'
64
64
  );
65
65
  }
66
+ if (opts.timedOut && opts.mustCollectReply) {
67
+ return (
68
+ 'The PromptAgent follow-up reply is still pending and must be collected ' +
69
+ 'in this parent turn. Call WaitAgent again with a larger bounded timeout. ' +
70
+ 'Do not end the turn or switch to ListAgents/notifications. Stop re-waiting ' +
71
+ 'only if the agent becomes stale/stalled, the wait is cancelled, or the ' +
72
+ 'agent returns idle/terminal.'
73
+ );
74
+ }
66
75
  if (opts.timedOut) {
67
76
  return (
68
77
  'Sub-agent is running in the background; it does not need another ' +
@@ -129,6 +138,11 @@ function errorNextSteps() {
129
138
  */
130
139
  function buildEnvelope(agent, { timedOut = false } = {}) {
131
140
  const status = agent.status;
141
+ const mustCollectReply = agent.promptReplyPending === true;
142
+ if (!timedOut && (status === STATUS.IDLE || isTerminalAgentStatus(status))) {
143
+ agent.promptReplyPending = false;
144
+ agent.promptReplyPendingAt = null;
145
+ }
132
146
  const liveness = diagnoseAgentLiveness(agent);
133
147
  const budgetResult = agent.result && typeof agent.result === 'object'
134
148
  && agent.result.status === 'budget_exceeded'
@@ -140,7 +154,12 @@ function buildEnvelope(agent, { timedOut = false } = {}) {
140
154
  ? agent.result
141
155
  : (agent.lastResult || ''));
142
156
  const env = {
143
- next_steps: nextStepsFor(status, { timedOut, budgetExceeded: !!budgetResult, stale: liveness.stale }),
157
+ next_steps: nextStepsFor(status, {
158
+ timedOut,
159
+ budgetExceeded: !!budgetResult,
160
+ stale: liveness.stale,
161
+ mustCollectReply: mustCollectReply && !liveness.stale,
162
+ }),
144
163
  agentId: agent.id,
145
164
  name: agent.name,
146
165
  status,
@@ -154,6 +173,7 @@ function buildEnvelope(agent, { timedOut = false } = {}) {
154
173
  diagnostic: liveness.diagnostic,
155
174
  messages: Array.isArray(agent.messages) ? agent.messages.length : 0,
156
175
  turns: agent.usage?.turns || 0,
176
+ mustCollectReply: timedOut ? mustCollectReply : false,
157
177
  };
158
178
  if (timedOut) {
159
179
  env.timedOut = true;
@@ -192,15 +212,15 @@ Status semantics:
192
212
  another PromptAgent. Either WaitAgent again with a larger timeout,
193
213
  CloseAgent to cut it short, or tell the user it's still working.
194
214
 
195
- CRITICAL — after WaitAgent returns you MUST take one of these actions:
196
- • status terminal: relay/retry/report.
197
- • status idle: reply to user OR PromptAgent OR CloseAgent.
198
- • timedOut: re-wait, cut short, or report progress.
199
- NEVER end your turn silently right after WaitAgent — the user has not seen
200
- the sub-agent's reply yet; only you have. The orchestration loop is
201
- SpawnAgent → (PromptAgent ↔ WaitAgent)+ → CloseAgent → final reply to user.
215
+ After WaitAgent returns, act on the status. A non-stale timeout after PromptAgent
216
+ has 'mustCollectReply=true': call WaitAgent again with a larger bounded timeout in
217
+ the same parent turn until idle/terminal. A stale/stalled agent breaks that loop:
218
+ inspect/report/close it instead. For ordinary SpawnAgent background work, use
219
+ ListAgents or later completion notifications instead of repeatedly re-waiting.
202
220
 
203
- Compatibility tool. The default wait is a short 5000ms poll. Callers may request up to 300000ms (5 minutes), but this is no longer the primary sub-agent workflow; prefer SpawnAgent + ListAgents + completion notifications for async background work.`,
221
+ The default wait is a bounded 5000ms poll; callers may request up to 300000ms
222
+ (5 minutes). Never use an unbounded blind loop: every wait is capped, liveness is
223
+ checked after each timeout, and stale/stalled is the explicit stop condition.`,
204
224
  zh: `等待子 Agent 的下一次状态变更(turn 结束、终止或等待超时)并获取状态信封。
205
225
 
206
226
  返回 JSON,含明确的 status、最新的 result 文本、liveness 计数器(toolUseCount、tokenCount、
@@ -215,11 +235,13 @@ msSinceLastEvent、recentTools)、可随时 Read 的持久化 outputFile 路
215
235
  仍在运行。不需要再 PromptAgent。要么用更大 timeout 再次 WaitAgent,要么 CloseAgent 中断,
216
236
  要么告知用户它仍在工作。
217
237
 
238
+ PromptAgent 后若非 stale/stalled 的有界等待超时,必须在同一父级 turn 使用更大的有界 timeout
239
+ 再次调用 WaitAgent,直到 idle/terminal;不要改用 ListAgents/notification 丢下未收集的回复。
218
240
  关键——如果信封显示 stale/stalled,子 Agent 可能卡死或空转。不要反复调用 WaitAgent——向用户
219
241
  报告情况,决定是 CloseAgent(带 close_reason)还是重试。
220
242
 
221
- 此工具保留用于向后兼容。现代异步流程请用 ListAgents 做非阻塞状态检查,依赖 turn 开始时的
222
- notification 获取完成事件。`
243
+ 普通 SpawnAgent 异步流程仍用 ListAgents 做非阻塞状态检查,并依赖后续 completion
244
+ notification;不要对普通后台任务盲目循环等待。每次等待都有上限,stale/stalled 是停止条件。`
223
245
  },
224
246
  parameters: {
225
247
  type: 'object',
@@ -247,6 +269,7 @@ notification 获取完成事件。`
247
269
  isConcurrencySafe: () => true,
248
270
  isReadOnly: () => true,
249
271
  cacheWithinQuery: false,
272
+ duplicateCallPolicy: () => 'allow',
250
273
  async execute(input, ctx) {
251
274
  const { agent_id, timeout_ms = 5000 } = input;
252
275
  if (!agent_id) {
@@ -297,6 +320,13 @@ notification 获取完成事件。`
297
320
  await new Promise(r => setTimeout(r, 200));
298
321
  }
299
322
 
323
+ // Re-check after the final sleep. The status may have changed just before
324
+ // the deadline without another loop iteration.
325
+ if (isTerminalAgentStatus(agent.status) || agent.status === STATUS.IDLE) {
326
+ consumeNotificationForAgent(agent.id);
327
+ return JSON.stringify(buildEnvelope(agent));
328
+ }
329
+
300
330
  // Wait elapsed; the sub-agent is still running. Surface mid-stream
301
331
  // preview + liveness so the parent has actionable signal.
302
332
  return JSON.stringify(buildEnvelope(agent, { timedOut: true }));
@@ -42,7 +42,8 @@ Use this to read documentation, articles, or any web page.
42
42
 
43
43
  Guidelines:
44
44
  - Provide the full URL including protocol (https://)
45
- - Large pages will be truncated — use the offset parameter for pagination
45
+ - Fetch one authoritative relevant page first; inspect it before fetching alternatives
46
+ - Set max_length to the smallest useful content budget. Large pages are truncated; use a more targeted source rather than repeatedly fetching the same URL
46
47
  - For APIs, the raw response body is returned as-is
47
48
  - Respects the abort signal for cancellation`,
48
49
  zh: `获取并读取网页内容。
@@ -51,7 +52,8 @@ Guidelines:
51
52
 
52
53
  使用指南:
53
54
  - 提供完整 URL 含协议(https://)
54
- - 大页面会截断——用 offset 参数做分页
55
+ - 先抓取一个最权威、最相关的页面;检查结果后再决定是否抓取备选页面
56
+ - 将 max_length 设为满足需要的最小内容预算。大页面会截断;应改用更定向的来源,而不是重复抓取同一 URL
55
57
  - 对 API 请求,原始响应体原样返回
56
58
  - 尊重取消信号`
57
59
  },
@@ -32,17 +32,17 @@ Use this when you need up-to-date information that may not be in your training d
32
32
  Returns search results with titles, URLs, and snippets.
33
33
 
34
34
  Guidelines:
35
- - Use specific, targeted search queries
35
+ - Use one specific, targeted query with the smallest useful result limit; inspect it before trying alternatives
36
36
  - Include the current year for time-sensitive queries
37
- - Combine with WebFetch to read full page content from results`,
37
+ - Fetch the most authoritative relevant result first. Add another source only when the claim is consequential, disputed, stale, or not established by the first source`,
38
38
  zh: `搜索网页获取最新信息。
39
39
 
40
40
  当你需要训练数据中可能没有的最新信息时使用。返回搜索结果,含标题、URL 和摘要。
41
41
 
42
42
  使用指南:
43
- - 使用具体、有针对性的搜索关键词
43
+ - 先执行一个具体、定向的查询,并使用满足需要的最小结果数;检查结果后再决定是否尝试其他查询
44
44
  - 时间敏感的查询要包含当前年份
45
- - 配合 WebFetch 读取搜索结果中的完整页面内容`
45
+ - 优先抓取最权威、最相关的结果。只有结论影响重大、存在争议、可能过时,或首个来源不能证明时才增加来源`
46
46
  },
47
47
  parameters: {
48
48
  type: 'object',
@@ -1349,6 +1349,7 @@ function projectPersistedToHistoryEntry(m, { includeReflections = false } = {})
1349
1349
  if (m.turnId) entry.turnId = m.turnId;
1350
1350
  if (m.imageAssetAnchor) entry.imageAssetAnchor = true;
1351
1351
  if (m.responseKind === 'progress' || m.responseKind === 'result') entry.responseKind = m.responseKind;
1352
+ if (Number.isInteger(m.llmCallCount) && m.llmCallCount > 0) entry.llmCallCount = m.llmCallCount;
1352
1353
  if (m.incomplete === true) entry.incomplete = true;
1353
1354
  if (typeof m.stopReason === 'string' && m.stopReason) entry.stopReason = m.stopReason;
1354
1355
  if (m.sessionId) entry.sessionId = m.sessionId;
@@ -1489,6 +1490,7 @@ function projectVisibleHistoryChunkMessages(messages = []) {
1489
1490
  ...(Array.isArray(m.images) && m.images.length > 0 ? { images: m.images } : {}),
1490
1491
  ...(m.speakerVpId ? { speakerVpId: m.speakerVpId } : {}),
1491
1492
  ...(m.responseKind === 'progress' || m.responseKind === 'result' ? { responseKind: m.responseKind } : {}),
1493
+ ...(Number.isInteger(m.llmCallCount) && m.llmCallCount > 0 ? { llmCallCount: m.llmCallCount } : {}),
1492
1494
  ...(m.incomplete === true ? { incomplete: true } : {}),
1493
1495
  ...(typeof m.stopReason === 'string' && m.stopReason ? { stopReason: m.stopReason } : {}),
1494
1496
  ...(Array.isArray(m.todos) ? { todos: m.todos } : {}),