dominds 1.26.6 → 1.27.0

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.
@@ -159,34 +159,37 @@ This ensures crash recovery and enables the backend to resume from any persisted
159
159
 
160
160
  When a dialog still carries an inter-dialog reply obligation, but the user temporarily interjects and asks it to handle a local question first, the system must distinguish between the **UI projection** and the **true driving source state**.
161
161
 
162
- Plainly: the system should answer the user's interjection first. Once the user receives a visible answer, the backend records that answer as A2H (Answer to Human) in Human Attention so the user can find and acknowledge it even if the dialog immediately continues automatically.
162
+ Plainly: the system should answer the user's interjection first. Once the user receives a visible answer, the backend records that answer as A2H (Answer to Human) in Human Attention so the user can find and acknowledge it even if the dialog immediately continues automatically. In addition to recovering A2H from visible `saying`, the LLM may also produce structured `answering` output (or call the equivalent `answerHuman` tool entry) to create A2H directly.
163
163
 
164
164
  **Normative semantics**:
165
165
 
166
166
  1. Every user interjection message is driven as a complete normal round.
167
167
  2. If that round needs tools, the system MUST finish the full tool round and any post-tool follow-up before treating the interjection as answered.
168
168
  3. A visible assistant `saying` settles the pending user-interjection reply only when no same-round function/tellask call remains after that `saying`.
169
- 4. Settling the interjection appends an A2H item to the dialog's `a2h.yaml`. A2H is an acknowledgement queue, not a problem report and not durable drive work.
170
- 5. If an inter-dialog reply obligation still exists after the interjection is answered, the backend automatically reasserts that obligation and continues. The user should not need to click `Continue` merely because the interjection answer completed.
171
- 6. A2H disappears when the user acknowledges it. This is intentionally "read then burn"; the canonical answer remains in the dialog transcript at `answerRef`.
169
+ 4. The model may also produce structured `answering` output (or call `answerHuman({ answerContent })`) to express "this is an answer for the human." That output is always appended to the dialog's `a2h.yaml`, but it is not the same thing as a Side Dialog's formal reply to its requester.
170
+ 5. If an inter-dialog reply obligation still exists after the interjection is answered, it remains ordinary durable reply-obligation state. Subsequent continuation is driven by the normal business paths: queued prompts, reply reminders, diligence push, or explicit resume when the dialog is genuinely blocked.
171
+ 6. A2H disappears when the user acknowledges it. This is intentionally "read then burn"; `answerRef` only links back to the course/genseq that produced the answer. When A2H comes from visible `saying`, the canonical text remains in the transcript; when it comes from structured `answering`, the A2H item itself carries that one-way output text.
172
172
  7. The Human Attention panel shows Q4H and A2H together. Q4H waits for a human answer; A2H waits only for human acknowledgement.
173
173
 
174
174
  **Strict boundary**: a formal `askHuman` answer is not part of this "user interjection" category. As soon as a prompt carries a real `q4hAnswerCallId`, it belongs to the askHuman reply channel and semantically continues an already-materialized question/answer chain; it must never be downgraded into temporary local side-chat.
175
175
 
176
+ **Modeling boundary**: Dominds does not structurally model "current human question context", does not maintain coordinates for "which human question this A2H answers", and does not store `userInterjection` coordinates in A2H. `a2h` / `answering` is only one-way structured modeling at the LLM output layer: the model produced text meant for the human, and the runtime put it into the human-acknowledgement queue. Questions, interjections, task obligations, and continuation duties remain represented by existing business facts such as prompts, Q4H, reply obligations, and queued prompts.
177
+
176
178
  **Key point**: pending user-interjection reply and inter-dialog reply obligation are independent business facts. Reminder/footer copy can use those two facts directly: if the interjection is still pending, prioritize answering the user; if it is settled and the reply obligation remains active, continue toward the required inter-dialog closure.
177
179
 
178
180
  **Mental-model warning**:
179
181
 
180
182
  - Do not flatten every `origin === 'user'` prompt into "interjection"; a non-empty `q4hAnswerCallId` means askHuman answer continuation and follows a different semantic path.
181
183
  - Do not treat A2H as Q4H. A2H does not block drive and does not route input to an agent.
184
+ - Do not treat A2H as a "human question context" database. A2H only carries answer text, acknowledgement state, and answer provenance.
182
185
  - Do not store A2H in the Problems panel. It belongs to Human Attention and is removed by Ack.
183
186
 
184
187
  You need all of the following together to understand the behavior correctly:
185
188
 
186
- - reply-guidance suppression / deferred reassertion for interjection turns
189
+ - reply-guidance suppression for interjection turns
187
190
  - pending user-interjection reply settlement after visible final `saying`
188
191
  - A2H persistence and Ack flow
189
- - automatic reply-obligation reassertion after the user-visible answer
192
+ - ordinary continuation through active reply obligations, diligence push, and queued prompts after the user-visible answer
190
193
 
191
194
  This is an intentionally cross-module semantic contract. Do not locally "simplify" one piece based only on its surface meaning.
192
195
 
@@ -158,34 +158,37 @@ askerDialog 可以在执行期间接收来自当前需向它回复的支线对
158
158
 
159
159
  当某个对话仍带有跨对话回复义务,但用户临时插话要求它先处理本地问题时,系统必须区分**UI 投影**与**真实驱动源状态**。
160
160
 
161
- 直白地说:先把用户这次插话接住并答完。用户看到可见答复后,后端把这条答复记录成 A2H(Answer to Human)放进“待人处理”,这样即使对话马上自动续推,用户也能找到并确认已阅。
161
+ 直白地说:先把用户这次插话接住并答完。用户看到可见答复后,后端把这条答复记录成 A2H(Answer to Human)放进“待人处理”,这样即使对话马上自动续推,用户也能找到并确认已阅。除了从可见 `saying` 恢复 A2H 之外,LLM 还可以产生结构化 `answering` 输出(或调用等价的 `answerHuman` 工具入口)直接形成 A2H。
162
162
 
163
163
  **规范语义**:
164
164
 
165
165
  1. 每条用户插话消息都按正常驱动轮完整执行。
166
166
  2. 若该轮需要工具,则必须先完整跑完该工具轮及其 post-tool follow-up,之后才可认为插话已答完。
167
- 3. 只有当模型产生可见 `saying`,且该 `saying` 后同轮没有普通 function/tellask call 继续挂起时,才清除“待回复用户插话”。
168
- 4. 清除该状态时,把答复追加到该对话的 `a2h.yaml`。A2H 是待人确认队列,不是 Problems,也不是持久驱动工作。
169
- 5. 如果插话答完后仍有跨对话回复义务,后端自动重申并继续推进;用户不应仅因为插话答完而需要手工点 `Continue`。
170
- 6. 用户 Ack 以后 A2H 立即消失,语义是“阅后即焚”;答复正文的真源仍在对话 transcript 中,由 `answerRef` 回链定位。
167
+ 3. 只有当模型产生可见 `saying`,且该 `saying` 后同轮没有普通 function/tellask call 继续挂起时,才用这条 `saying` 结算“待回复用户插话”。
168
+ 4. 模型也可以产生结构化 `answering` 输出(或调用 `answerHuman({ answerContent })`)来表达“这是给人类看的答复”。该输出无条件追加到该对话的 `a2h.yaml`,但它不等价于支线对话对诉请者的正式回贴。
169
+ 5. 如果插话答完后仍有跨对话回复义务,它保持为普通的 durable reply-obligation 状态;后续由正常业务路径推进:queued prompt、回贴提醒、鞭策续推,或在对话确实阻塞时由显式 resume 触发。
170
+ 6. 用户 Ack 以后 A2H 立即消失,语义是“阅后即焚”;`answerRef` 只回链到产生答复的 course/genseq,用于定位来源生成轮。若 A2H 来自可见 `saying`,正文真源仍在 transcript 中;若来自结构化 `answering`,A2H 条目本身承载该单向输出正文。
171
171
  7. “待人处理”面板同时显示 Q4H 与 A2H:Q4H 等人回答,A2H 只等人确认已阅。
172
172
 
173
173
  **严格边界**:`askHuman` 的正式回答不属于这里的“用户插话”。只要一条 prompt 带着真实的 `q4hAnswerCallId`,它就属于 askHuman 回复通道,语义上是在继续已 materialize 的提问/应答链路,绝不能被压入“本地临时插话聊天”。
174
174
 
175
+ **建模边界**:Dominds 不对“当前人类问题上下文”做结构化业务建模,不维护“这个 A2H 回答的是哪一个人类问题”的坐标,也不把 `userInterjection` 坐标写进 A2H。`a2h` / `answering` 只是 LLM 输出层面的单向结构化建模:模型产生一段给人类看的答复,运行时把它放入待人确认队列。问题/插话/任务义务本身仍由既有 prompt、Q4H、reply obligation、queued prompt 等业务事实表达。
176
+
175
177
  **关键点**:“是否还有用户插话待可见回复”和“是否还有跨对话回复义务”是两个独立业务事实。提醒项 footer 可以直接用这两个事实定制:插话尚未答复时优先答人;插话已答复且回复义务仍 active 时,继续按跨对话回贴要求收口。
176
178
 
177
179
  **心智模型提醒**:
178
180
 
179
181
  - 更不能把所有 `origin === 'user'` 的输入都笼统视作“用户插话”;`q4hAnswerCallId` 非空的 prompt 是 askHuman answer continuation,必须按另一条语义链处理。
180
182
  - 不能把 A2H 当成 Q4H;A2H 不阻塞 drive,也不把输入路由给智能体。
183
+ - 不能把 A2H 当成“人类问题上下文”数据库;A2H 只有答复正文、确认状态与答复来源定位。
181
184
  - 不能把 A2H 放进 Problems 面板;它属于“待人处理”,由 Ack 删除。
182
185
 
183
186
  必须把以下几块一起看,才能形成完整且精确的理解:
184
187
 
185
- - reply-guidance 中对插话轮的回复义务 suppression / deferred reassertion
188
+ - reply-guidance 中对插话轮的回复义务 suppression
186
189
  - 可见最终 `saying` 后对 pending user-interjection reply 的结算
187
190
  - A2H 持久化与 Ack 流程
188
- - 用户可见答复后的跨对话回复义务自动重申
191
+ - 用户可见答复后的 active reply obligation、鞭策续推、queued prompt 等普通续推路径
189
192
 
190
193
  这是一条跨模块协同语义,不允许在单点上做“表面看起来更简单”的局部简化。
191
194
 
@@ -71,10 +71,10 @@ const RUNTIME_PROMPT_WRAPPER_PREFIXES = [
71
71
  '【系统提示】 上下文状态:🔴 告急;收到用户插话',
72
72
  reply_prompt_copy_1.ACTIVE_REPLY_TOOL_PREFIX_EN,
73
73
  reply_prompt_copy_1.ACTIVE_REPLY_TOOL_PREFIX_ZH,
74
+ reply_prompt_copy_1.ANSWERING_REPLY_REMINDER_PREFIX_EN,
75
+ reply_prompt_copy_1.ANSWERING_REPLY_REMINDER_PREFIX_ZH,
74
76
  reply_prompt_copy_1.NO_ACTIVE_REPLY_PREFIX_EN,
75
77
  reply_prompt_copy_1.NO_ACTIVE_REPLY_PREFIX_ZH,
76
- reply_prompt_copy_1.REPLY_REASSERTION_PREFIX_EN,
77
- reply_prompt_copy_1.REPLY_REASSERTION_PREFIX_ZH,
78
78
  reply_prompt_copy_1.REPLY_SUPPRESSION_PREFIX_EN,
79
79
  reply_prompt_copy_1.REPLY_SUPPRESSION_PREFIX_ZH,
80
80
  ];
@@ -485,6 +485,9 @@ responses:
485
485
  }
486
486
  await receiver.sayingFinish();
487
487
  }
488
+ if (matched?.answeringResponse !== undefined && matched.answeringResponse.trim() !== '') {
489
+ await receiver.answering?.(matched.answeringResponse);
490
+ }
488
491
  const funcCalls = matched?.funcCalls ?? [];
489
492
  for (let i = 0; i < funcCalls.length; i++) {
490
493
  const call = funcCalls[i];
@@ -606,6 +609,9 @@ responses:
606
609
  kind: 'invalid_func_call',
607
610
  call,
608
611
  }));
612
+ const answeringOutputs = matched?.answeringResponse !== undefined && matched.answeringResponse.trim() !== ''
613
+ ? [{ kind: 'answering', content: matched.answeringResponse }]
614
+ : [];
609
615
  const messages = thinking !== undefined
610
616
  ? saying
611
617
  ? [thinking, saying, ...funcMsgs]
@@ -615,10 +621,11 @@ responses:
615
621
  : [...funcMsgs];
616
622
  return {
617
623
  messages,
618
- ...(invalidFuncCallOutputs.length > 0
624
+ ...(invalidFuncCallOutputs.length > 0 || answeringOutputs.length > 0
619
625
  ? {
620
626
  outputs: [
621
627
  ...messages.map((message) => ({ kind: 'message', message })),
628
+ ...answeringOutputs,
622
629
  ...invalidFuncCallOutputs,
623
630
  ],
624
631
  }
package/dist/llm/gen.d.ts CHANGED
@@ -20,6 +20,9 @@ export declare class LlmStreamErrorEmittedError extends Error {
20
20
  export type LlmBatchOutput = {
21
21
  kind: 'message';
22
22
  message: ChatMessage;
23
+ } | {
24
+ kind: 'answering';
25
+ content: string;
23
26
  } | {
24
27
  kind: 'invalid_func_call';
25
28
  call: LlmInvalidFuncCall;
@@ -156,6 +159,7 @@ export interface LlmStreamReceiver {
156
159
  sayingStart: () => Promise<void>;
157
160
  sayingChunk: (chunk: string) => Promise<void>;
158
161
  sayingFinish: () => Promise<void>;
162
+ answering?: (content: string) => Promise<void>;
159
163
  funcCall: (callId: string, name: string, args: string, ids?: {
160
164
  rawCallId?: string;
161
165
  effectiveCallId?: string;
@@ -257,18 +257,14 @@ function isUserOriginPrompt(prompt) {
257
257
  }
258
258
  async function resolveReminderContextFooterState(args) {
259
259
  const latest = await persistence_1.DialogPersistence.loadDialogLatest(args.dlg.id, args.dlg.status);
260
- const deferredReplyReassertion = latest?.deferredReplyReassertion;
261
260
  const activeReplyObligation = await persistence_1.DialogPersistence.loadActiveTellaskReplyObligation(args.dlg.id, args.dlg.status);
262
261
  const pendingUserInterjectionReply = latest?.pendingUserInterjectionReply !== undefined;
263
- const hasDeferredReplyReassertion = deferredReplyReassertion?.reason === 'user_interjection_with_parked_original_task';
264
262
  const hasActiveReplyObligation = activeReplyObligation !== undefined;
265
263
  // Business scenario: a user can reopen a completed Side Dialog to ask a follow-up. A recorded
266
- // final response with no active/parked reply task means the old handoff has already been
264
+ // final response with no active reply task means the old handoff has already been
267
265
  // reported back; if a real user message is now present, the footer should say "talk with the
268
266
  // user now" instead of making the model infer that from old transcript/reminder context.
269
- const hasCompletedHandoffWithoutPendingReply = latest?.sideDialogFinalResponse !== undefined &&
270
- !hasDeferredReplyReassertion &&
271
- !hasActiveReplyObligation;
267
+ const hasCompletedHandoffWithoutPendingReply = latest?.sideDialogFinalResponse !== undefined && !hasActiveReplyObligation;
272
268
  const dialogScope = args.dlg instanceof dialog_1.SideDialog ? { kind: 'side_dialog' } : { kind: 'main_dialog' };
273
269
  return (0, reminder_context_1.resolveReminderContextFooterStateFromSignals)({
274
270
  dialogScope,
@@ -277,7 +273,6 @@ async function resolveReminderContextFooterState(args) {
277
273
  contextHealth: args.dlg.getLastContextHealth(),
278
274
  pendingUserInterjectionReply,
279
275
  hasCompletedHandoffWithoutPendingReply,
280
- hasDeferredReplyReassertion,
281
276
  hasActiveReplyObligation,
282
277
  });
283
278
  }
@@ -343,11 +338,6 @@ async function maybeResolveAnsweredUserInterjection(args) {
343
338
  id: `a2h-${Buffer.from(answerIdSource).toString('base64url')}`,
344
339
  content: args.assistantSayingContent,
345
340
  answeredAt: (0, time_1.formatUnifiedTimestamp)(new Date()),
346
- userInterjection: {
347
- msgId: pending.msgId,
348
- course: pending.course,
349
- genseq: pending.genseq,
350
- },
351
341
  answerRef: {
352
342
  course,
353
343
  genseq: args.assistantSayingGenseq,
@@ -706,6 +696,7 @@ const TELLASK_SPECIAL_VIRTUAL_TOOLS = [
706
696
  {
707
697
  type: 'func',
708
698
  name: 'askHuman',
699
+ followupMode: 'deferred',
709
700
  description: 'Ask for required clarification/decision from human.',
710
701
  parameters: {
711
702
  type: 'object',
@@ -719,6 +710,23 @@ const TELLASK_SPECIAL_VIRTUAL_TOOLS = [
719
710
  throw new Error('askHuman is handled by kernel-driver tellask-special channel');
720
711
  },
721
712
  },
713
+ {
714
+ type: 'func',
715
+ name: 'answerHuman',
716
+ followupMode: 'deferred',
717
+ description: 'Record the current human-facing answer for human attention.',
718
+ parameters: {
719
+ type: 'object',
720
+ properties: {
721
+ answerContent: { type: 'string' },
722
+ },
723
+ required: ['answerContent'],
724
+ additionalProperties: false,
725
+ },
726
+ call: async () => {
727
+ throw new Error('answerHuman is handled by kernel-driver tellask-special channel');
728
+ },
729
+ },
722
730
  ];
723
731
  const CONTEXT_HEALTH_TOOL_RESULT_VISIBLE_BYTE_LIMIT = 2000;
724
732
  const CONTEXT_HEALTH_LARGE_TOOL_RETURN_UNAVAILABLE_ZH = '这次函数返回内容太大,清理头脑之前不会显示给你。';
@@ -991,20 +999,6 @@ async function renderRemindersForContext(dlg) {
991
999
  ...renderedItems,
992
1000
  ];
993
1001
  }
994
- function hasSameReplyDirective(left, right) {
995
- if (!left || !right) {
996
- return left === right;
997
- }
998
- if (left.expectedReplyCallName !== right.expectedReplyCallName) {
999
- return false;
1000
- }
1001
- if (left.targetDialogId !== right.targetDialogId ||
1002
- left.targetCallId !== right.targetCallId ||
1003
- left.tellaskContent !== right.tellaskContent) {
1004
- return false;
1005
- }
1006
- return true;
1007
- }
1008
1002
  function buildPendingTellaskFuncResult(args) {
1009
1003
  return {
1010
1004
  type: 'func_result_msg',
@@ -1321,6 +1315,25 @@ async function emitAssistantSaying(dlg, content) {
1321
1315
  await dlg.sayingChunk(content);
1322
1316
  await dlg.sayingFinish();
1323
1317
  }
1318
+ async function recordStructuredAnswering(args) {
1319
+ if (args.content.trim() === '')
1320
+ return undefined;
1321
+ const course = args.dlg.activeGenCourseOrUndefined ?? args.dlg.currentCourse;
1322
+ const genseq = args.dlg.activeGenSeqOrUndefined ?? 1;
1323
+ return await (0, tellask_special_1.recordAnswerToHuman)({
1324
+ dlg: args.dlg,
1325
+ answerContent: args.content,
1326
+ course,
1327
+ genseq,
1328
+ answerIdSource: [
1329
+ args.dlg.id.rootId,
1330
+ args.dlg.id.selfId,
1331
+ `c${String(course)}`,
1332
+ `g${String(genseq)}`,
1333
+ args.source,
1334
+ ].join('|'),
1335
+ });
1336
+ }
1324
1337
  function formatInvalidFuncCallRuntimeGuide(language, call) {
1325
1338
  const rawName = call.rawFunctionName !== undefined && call.rawFunctionName.trim() !== ''
1326
1339
  ? call.rawFunctionName.trim()
@@ -2051,6 +2064,7 @@ async function executeFunctionRound(args) {
2051
2064
  shouldStopAfterPendingTellaskWait: false,
2052
2065
  pairedMessages: [],
2053
2066
  tellaskToolOutputs: [],
2067
+ answerHumanOutputs: [],
2054
2068
  };
2055
2069
  }
2056
2070
  throwIfAborted(args.abortSignal, args.dlg);
@@ -2064,6 +2078,7 @@ async function executeFunctionRound(args) {
2064
2078
  'replyTellaskSessionless',
2065
2079
  'replyTellaskBack',
2066
2080
  'askHuman',
2081
+ 'answerHuman',
2067
2082
  'freshBootsReasoning',
2068
2083
  ...(allowTellaskBack ? ['tellaskBack'] : []),
2069
2084
  ])
@@ -2167,6 +2182,7 @@ async function executeFunctionRound(args) {
2167
2182
  shouldStopAfterPendingTellaskWait: tellaskRound.shouldStopAfterPendingTellaskWait,
2168
2183
  pairedMessages,
2169
2184
  tellaskToolOutputs: [...tellaskRound.toolOutputs],
2185
+ answerHumanOutputs: tellaskRound.answerHumanOutputs,
2170
2186
  };
2171
2187
  }
2172
2188
  async function preserveDiligenceBudgetAcrossQ4H(dlg) {
@@ -2362,10 +2378,11 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
2362
2378
  let lastAssistantSayingGenseq = null;
2363
2379
  let lastAssistantThinkingContent = null;
2364
2380
  let lastAssistantThinkingGenseq = null;
2381
+ let lastAssistantAnsweringContent = null;
2382
+ let lastAssistantAnsweringGenseq = null;
2365
2383
  let lastFunctionCallGenseq = null;
2366
2384
  let lastAssistantReplyTarget;
2367
2385
  let lastBusinessContinuation = { kind: 'none' };
2368
- let answeredUserInterjection;
2369
2386
  let currentPromptIsUserInterjection = false;
2370
2387
  let currentUserInterjectionReply;
2371
2388
  let fbrConclusion;
@@ -2639,62 +2656,23 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
2639
2656
  prompt: currentPrompt,
2640
2657
  language: promptLanguage,
2641
2658
  });
2642
- const deferredReplyReassertionDirective = replyGuidance.deferredReplyReassertionDirective;
2643
2659
  currentPromptIsUserInterjection =
2644
2660
  currentPrompt.origin === 'user' &&
2645
2661
  replyGuidance.suppressInterDialogReplyGuidance &&
2646
- deferredReplyReassertionDirective !== undefined;
2662
+ !replyGuidance.isQ4HAnswerPrompt;
2647
2663
  if (currentPromptIsUserInterjection) {
2648
- // WARNING:
2649
- // User interjection suppression is a reversible state transition, not a one-shot
2650
- // latch. The normal cycle is:
2651
- // - user interjects -> suppress reply obligation
2652
- // - the visible local answer auto-reasserts the reply obligation
2653
- // - user interjects again -> suppress it again
2654
- //
2655
- // Legacy blocked-Continue paths may also re-enter here. A repeated interjection MUST
2656
- // re-arm the deferred state and re-materialize the suppression guide, even when the
2657
- // underlying reply directive itself did not change.
2658
- const deferredDirective = deferredReplyReassertionDirective;
2659
- if (deferredDirective === undefined) {
2660
- throw new Error(`kernel-driver user interjection invariant violation: missing deferred reply directive for dialog=${dlg.id.valueOf()} msgId=${currentPrompt.msgId}`);
2661
- }
2662
- const existingDeferredReplyReassertion = await persistence_1.DialogPersistence.getDeferredReplyReassertion(dlg.id, dlg.status);
2663
2664
  currentUserInterjectionReply = {
2664
2665
  msgId: currentPrompt.msgId,
2665
2666
  course: (0, storage_1.toDialogCourseNumber)(dlg.activeGenCourseOrUndefined ?? dlg.currentCourse),
2666
2667
  genseq: (0, storage_1.toCallSiteGenseqNo)(dlg.activeGenSeq),
2667
2668
  };
2668
- const nextDeferredReplyReassertion = {
2669
- reason: 'user_interjection_with_parked_original_task',
2670
- directive: deferredDirective,
2671
- userInterjection: currentUserInterjectionReply,
2672
- };
2673
- const mustRearmDeferredReplyReassertion = existingDeferredReplyReassertion === undefined ||
2674
- existingDeferredReplyReassertion.resumeGuideSurfaced === true ||
2675
- existingDeferredReplyReassertion.userInterjection.msgId !==
2676
- nextDeferredReplyReassertion.userInterjection.msgId ||
2677
- existingDeferredReplyReassertion.userInterjection.course !==
2678
- nextDeferredReplyReassertion.userInterjection.course ||
2679
- existingDeferredReplyReassertion.userInterjection.genseq !==
2680
- nextDeferredReplyReassertion.userInterjection.genseq ||
2681
- !hasSameReplyDirective(existingDeferredReplyReassertion.directive, nextDeferredReplyReassertion.directive);
2682
- if (mustRearmDeferredReplyReassertion) {
2683
- await persistence_1.DialogPersistence.setDeferredReplyReassertion(dlg.id, nextDeferredReplyReassertion, dlg.status);
2684
- }
2685
- if (mustRearmDeferredReplyReassertion) {
2686
- currentRuntimeGuideMsg = replyGuidance.transientGuideContent
2687
- ? {
2688
- type: 'transient_guide_msg',
2689
- role: 'assistant',
2690
- content: replyGuidance.transientGuideContent,
2691
- }
2692
- : undefined;
2693
- }
2694
- }
2695
- else if (currentPrompt.origin === 'user' &&
2696
- !replyGuidance.suppressInterDialogReplyGuidance) {
2697
- await persistence_1.DialogPersistence.setDeferredReplyReassertion(dlg.id, undefined, dlg.status);
2669
+ currentRuntimeGuideMsg = replyGuidance.transientGuideContent
2670
+ ? {
2671
+ type: 'transient_guide_msg',
2672
+ role: 'assistant',
2673
+ content: replyGuidance.transientGuideContent,
2674
+ }
2675
+ : undefined;
2698
2676
  }
2699
2677
  if (!replyGuidance.suppressInterDialogReplyGuidance &&
2700
2678
  !currentRuntimeGuideMsg &&
@@ -2984,6 +2962,8 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
2984
2962
  let streamAttemptSayingGenseq;
2985
2963
  let streamAttemptThinkingContent;
2986
2964
  let streamAttemptThinkingGenseq;
2965
+ let streamAttemptAnsweringContent;
2966
+ let streamAttemptAnsweringGenseq;
2987
2967
  let streamActive = { kind: 'idle' };
2988
2968
  const rollbackStreamAttempt = async () => {
2989
2969
  if (streamAttemptCourse === undefined ||
@@ -3005,6 +2985,8 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
3005
2985
  streamAttemptSayingGenseq = undefined;
3006
2986
  streamAttemptThinkingContent = undefined;
3007
2987
  streamAttemptThinkingGenseq = undefined;
2988
+ streamAttemptAnsweringContent = undefined;
2989
+ streamAttemptAnsweringGenseq = undefined;
3008
2990
  sawWebSearchSideChannelOutput = false;
3009
2991
  sawNativeToolSideChannelOutput = false;
3010
2992
  streamedFuncCalls.length = 0;
@@ -3117,6 +3099,35 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
3117
3099
  streamAttemptSayingContent = currentSayingContent;
3118
3100
  streamAttemptSayingGenseq = sayingMessage.genseq;
3119
3101
  },
3102
+ answering: async (content) => {
3103
+ throwIfAborted(abortSignal, dlg);
3104
+ if (streamActive.kind !== 'idle') {
3105
+ const detail = `Protocol violation: answering while ${streamActive.kind} is active`;
3106
+ await dlg.streamError(detail);
3107
+ throw new gen_1.LlmStreamErrorEmittedError({
3108
+ detail,
3109
+ i18nStopReason: (0, stop_reason_i18n_1.buildHumanSystemStopReasonTextI18n)({
3110
+ detail,
3111
+ kind: 'conflicting_stream',
3112
+ }),
3113
+ });
3114
+ }
3115
+ if (content.trim() !== '') {
3116
+ if (streamAttemptAnsweringContent !== undefined) {
3117
+ const detail = 'Protocol violation: multiple answering outputs in one generation';
3118
+ await dlg.streamError(detail);
3119
+ throw new gen_1.LlmStreamErrorEmittedError({
3120
+ detail,
3121
+ i18nStopReason: (0, stop_reason_i18n_1.buildHumanSystemStopReasonTextI18n)({
3122
+ detail,
3123
+ kind: 'conflicting_stream',
3124
+ }),
3125
+ });
3126
+ }
3127
+ streamAttemptAnsweringContent = content;
3128
+ streamAttemptAnsweringGenseq = dlg.activeGenSeq;
3129
+ }
3130
+ },
3120
3131
  funcCall: async (callId, name, argsStr, ids) => {
3121
3132
  throwIfAborted(abortSignal, dlg);
3122
3133
  const rawCallId = trimOptionalCallId(ids?.rawCallId) ?? callId;
@@ -3190,6 +3201,8 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
3190
3201
  streamAttemptSayingGenseq = undefined;
3191
3202
  streamAttemptThinkingContent = undefined;
3192
3203
  streamAttemptThinkingGenseq = undefined;
3204
+ streamAttemptAnsweringContent = undefined;
3205
+ streamAttemptAnsweringGenseq = undefined;
3193
3206
  sawWebSearchSideChannelOutput = false;
3194
3207
  sawNativeToolSideChannelOutput = false;
3195
3208
  streamedFuncCalls.length = 0;
@@ -3208,6 +3221,7 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
3208
3221
  msg.content.trim() !== '');
3209
3222
  const hasFunctionCall = streamedFuncCalls.length > 0;
3210
3223
  if (!hasFinishedMessageContent &&
3224
+ streamAttemptAnsweringContent === undefined &&
3211
3225
  !hasFunctionCall &&
3212
3226
  invalidFuncCallCount === 0 &&
3213
3227
  !sawWebSearchSideChannelOutput &&
@@ -3235,6 +3249,20 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
3235
3249
  lastAssistantReplyTarget = currentReplyTarget;
3236
3250
  }
3237
3251
  }
3252
+ if (streamAttemptAnsweringContent !== undefined) {
3253
+ const answer = await recordStructuredAnswering({
3254
+ dlg,
3255
+ content: streamAttemptAnsweringContent,
3256
+ source: 'structured-answering',
3257
+ });
3258
+ if (answer !== undefined) {
3259
+ lastAssistantAnsweringContent = answer.content;
3260
+ lastAssistantAnsweringGenseq =
3261
+ streamAttemptAnsweringGenseq === undefined
3262
+ ? answer.answerRef.genseq
3263
+ : streamAttemptAnsweringGenseq;
3264
+ }
3265
+ }
3238
3266
  return { usage: res.usage, llmGenModel: res.llmGenModel };
3239
3267
  };
3240
3268
  const previousAssistantSayingGenseq = lastAssistantSayingGenseq;
@@ -3256,6 +3284,7 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
3256
3284
  : Array.isArray(llmOutput.batchMessages)
3257
3285
  ? llmOutput.batchMessages.map((message) => ({ kind: 'message', message }))
3258
3286
  : [];
3287
+ let batchAnsweringSeen = false;
3259
3288
  for (const output of batchOutputs) {
3260
3289
  switch (output.kind) {
3261
3290
  case 'message': {
@@ -3283,6 +3312,33 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
3283
3312
  }
3284
3313
  break;
3285
3314
  }
3315
+ case 'answering': {
3316
+ if (output.content.trim() === '') {
3317
+ break;
3318
+ }
3319
+ if (batchAnsweringSeen) {
3320
+ const detail = 'Protocol violation: multiple answering outputs in one generation';
3321
+ await dlg.streamError(detail);
3322
+ throw new gen_1.LlmStreamErrorEmittedError({
3323
+ detail,
3324
+ i18nStopReason: (0, stop_reason_i18n_1.buildHumanSystemStopReasonTextI18n)({
3325
+ detail,
3326
+ kind: 'conflicting_stream',
3327
+ }),
3328
+ });
3329
+ }
3330
+ batchAnsweringSeen = true;
3331
+ const answer = await recordStructuredAnswering({
3332
+ dlg,
3333
+ content: output.content,
3334
+ source: 'structured-answering',
3335
+ });
3336
+ if (answer !== undefined) {
3337
+ lastAssistantAnsweringContent = answer.content;
3338
+ lastAssistantAnsweringGenseq = answer.answerRef.genseq;
3339
+ }
3340
+ break;
3341
+ }
3286
3342
  case 'invalid_func_call': {
3287
3343
  invalidFuncCallCount += 1;
3288
3344
  await persistInvalidFuncCallRuntimeGuide({
@@ -3323,6 +3379,7 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
3323
3379
  c.name === 'tellaskSessionless' ||
3324
3380
  c.name === 'tellaskBack' ||
3325
3381
  c.name === 'askHuman' ||
3382
+ c.name === 'answerHuman' ||
3326
3383
  c.name === 'freshBootsReasoning').length
3327
3384
  : 0;
3328
3385
  const policyViolationKind = (0, guardrails_1.resolveKernelDriverPolicyViolationKind)({
@@ -3354,6 +3411,8 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
3354
3411
  lastAssistantSayingGenseq,
3355
3412
  lastAssistantThinkingContent,
3356
3413
  lastAssistantThinkingGenseq,
3414
+ lastAssistantAnsweringContent,
3415
+ lastAssistantAnsweringGenseq,
3357
3416
  lastFunctionCallGenseq,
3358
3417
  lastAssistantReplyTarget,
3359
3418
  lastBusinessContinuation,
@@ -3396,9 +3455,11 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
3396
3455
  if (!Number.isFinite(rawCallGenseq) || rawCallGenseq <= 0)
3397
3456
  continue;
3398
3457
  const callGenseq = Math.floor(rawCallGenseq);
3399
- currentRoundFunctionCallGenseqs.push(callGenseq);
3400
- if (lastFunctionCallGenseq === null || callGenseq > lastFunctionCallGenseq) {
3401
- lastFunctionCallGenseq = callGenseq;
3458
+ if (call.name !== 'answerHuman') {
3459
+ currentRoundFunctionCallGenseqs.push(callGenseq);
3460
+ if (lastFunctionCallGenseq === null || callGenseq > lastFunctionCallGenseq) {
3461
+ lastFunctionCallGenseq = callGenseq;
3462
+ }
3402
3463
  }
3403
3464
  }
3404
3465
  const userInterjectionMsgIdForVisibleAnswer = currentPrompt?.origin === 'user' && !isQ4HAnswerPrompt
@@ -3406,7 +3467,29 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
3406
3467
  : currentGenerationBelongsToUserToolChain
3407
3468
  ? currentUserPromptMsgId
3408
3469
  : undefined;
3409
- if (userInterjectionMsgIdForVisibleAnswer !== undefined) {
3470
+ const routed = await executeFunctionRound({
3471
+ dlg,
3472
+ agent,
3473
+ agentTools,
3474
+ funcCalls: streamedFuncCalls,
3475
+ callbacks,
3476
+ abortSignal,
3477
+ allowTellaskFunctions: policy.allowTellaskFunctions,
3478
+ activePromptReplyDirective: currentPrompt?.tellaskReplyDirective,
3479
+ contextHealthForToolResultVisibility: pickContextHealthForLargeToolResultVisibility({
3480
+ previous: contextHealthBeforeGen,
3481
+ current: contextHealthForGen,
3482
+ }),
3483
+ });
3484
+ for (const answering of routed.answerHumanOutputs) {
3485
+ lastAssistantAnsweringContent = answering.answerContent;
3486
+ lastAssistantAnsweringGenseq = answering.genseq;
3487
+ }
3488
+ const currentRoundAnsweringGenseq = dlg.activeGenSeqOrUndefined;
3489
+ const hasCurrentRoundAnsweringOutput = currentRoundAnsweringGenseq !== undefined &&
3490
+ lastAssistantAnsweringGenseq === currentRoundAnsweringGenseq;
3491
+ if (userInterjectionMsgIdForVisibleAnswer !== undefined &&
3492
+ !hasCurrentRoundAnsweringOutput) {
3410
3493
  const streamedCurrentRoundSayingContent = batchOutputs.length === 0 &&
3411
3494
  lastAssistantSayingGenseq !== previousAssistantSayingGenseq
3412
3495
  ? lastAssistantSayingContent
@@ -3415,31 +3498,14 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
3415
3498
  lastAssistantSayingGenseq !== previousAssistantSayingGenseq
3416
3499
  ? lastAssistantSayingGenseq
3417
3500
  : null;
3418
- const answer = await maybeResolveAnsweredUserInterjection({
3501
+ await maybeResolveAnsweredUserInterjection({
3419
3502
  dlg,
3420
3503
  userPromptMsgId: userInterjectionMsgIdForVisibleAnswer,
3421
3504
  assistantSayingContent: currentRoundAssistantSayingContent ?? streamedCurrentRoundSayingContent,
3422
3505
  assistantSayingGenseq: currentRoundAssistantSayingGenseq ?? streamedCurrentRoundSayingGenseq,
3423
3506
  functionCallGenseqs: currentRoundFunctionCallGenseqs,
3424
3507
  });
3425
- if (answer !== undefined) {
3426
- answeredUserInterjection = answer;
3427
- }
3428
3508
  }
3429
- const routed = await executeFunctionRound({
3430
- dlg,
3431
- agent,
3432
- agentTools,
3433
- funcCalls: streamedFuncCalls,
3434
- callbacks,
3435
- abortSignal,
3436
- allowTellaskFunctions: policy.allowTellaskFunctions,
3437
- activePromptReplyDirective: currentPrompt?.tellaskReplyDirective,
3438
- contextHealthForToolResultVisibility: pickContextHealthForLargeToolResultVisibility({
3439
- previous: contextHealthBeforeGen,
3440
- current: contextHealthForGen,
3441
- }),
3442
- });
3443
3509
  if (routed.tellaskToolOutputs.length > 0) {
3444
3510
  newMsgs.push(...routed.tellaskToolOutputs);
3445
3511
  }
@@ -3869,10 +3935,11 @@ async function driveDialogStreamCore(dlg, callbacks, humanPrompt, driveOptions)
3869
3935
  lastAssistantSayingGenseq,
3870
3936
  lastAssistantThinkingContent,
3871
3937
  lastAssistantThinkingGenseq,
3938
+ lastAssistantAnsweringContent,
3939
+ lastAssistantAnsweringGenseq,
3872
3940
  lastFunctionCallGenseq,
3873
3941
  lastAssistantReplyTarget,
3874
3942
  lastBusinessContinuation,
3875
- answeredUserInterjection,
3876
3943
  fbrConclusion,
3877
3944
  };
3878
3945
  }