odai-dsh-agent 0.2.55 → 0.2.57

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.
Files changed (74) hide show
  1. package/README.md +11 -9
  2. package/client/client.js +14 -7
  3. package/package.json +1 -1
  4. package/preset/odai/package.json +2 -1
  5. package/preset/odai/runtime/context-activation.mjs +1 -1
  6. package/preset/odai/runtime/context-activation.mjs.map +1 -1
  7. package/preset/odai/runtime/index.d.mts.map +1 -1
  8. package/preset/odai/runtime/index.mjs +51 -2
  9. package/preset/odai/runtime/index.mjs.map +1 -1
  10. package/preset/odai/runtime/lead-mode.d.mts +23 -0
  11. package/preset/odai/runtime/lead-mode.d.mts.map +1 -0
  12. package/preset/odai/runtime/lead-mode.mjs +17 -0
  13. package/preset/odai/runtime/lead-mode.mjs.map +1 -0
  14. package/preset/odai/runtime/lifecycle-runtime.d.mts.map +1 -1
  15. package/preset/odai/runtime/lifecycle-runtime.mjs +197 -14
  16. package/preset/odai/runtime/lifecycle-runtime.mjs.map +1 -1
  17. package/preset/odai/runtime/responsibility-gap.d.mts +4 -1
  18. package/preset/odai/runtime/responsibility-gap.d.mts.map +1 -1
  19. package/preset/odai/runtime/responsibility-gap.mjs +1 -0
  20. package/preset/odai/runtime/responsibility-gap.mjs.map +1 -1
  21. package/preset/odai/runtime/review-evidence.d.mts +2 -0
  22. package/preset/odai/runtime/review-evidence.d.mts.map +1 -1
  23. package/preset/odai/runtime/review-evidence.mjs +45 -0
  24. package/preset/odai/runtime/review-evidence.mjs.map +1 -1
  25. package/preset/odai/runtime/role-overlays.d.mts +1 -1
  26. package/preset/odai/runtime/role-overlays.d.mts.map +1 -1
  27. package/preset/odai/runtime/role-overlays.mjs +4 -1
  28. package/preset/odai/runtime/role-overlays.mjs.map +1 -1
  29. package/preset/odai/runtime/route-session.d.mts +20 -0
  30. package/preset/odai/runtime/route-session.d.mts.map +1 -0
  31. package/preset/odai/runtime/route-session.mjs +44 -0
  32. package/preset/odai/runtime/route-session.mjs.map +1 -0
  33. package/preset/odai/runtime/router.d.mts +2 -1
  34. package/preset/odai/runtime/router.d.mts.map +1 -1
  35. package/preset/odai/runtime/router.mjs +52 -1
  36. package/preset/odai/runtime/router.mjs.map +1 -1
  37. package/preset/odai/runtime/routing-config.d.mts +5 -2
  38. package/preset/odai/runtime/routing-config.d.mts.map +1 -1
  39. package/preset/odai/runtime/routing-config.mjs +25 -20
  40. package/preset/odai/runtime/routing-config.mjs.map +1 -1
  41. package/preset/odai/runtime/runtime-config.d.mts +1 -1
  42. package/preset/odai/runtime/runtime-config.d.mts.map +1 -1
  43. package/preset/odai/runtime/runtime-support.d.mts +6 -1
  44. package/preset/odai/runtime/runtime-support.d.mts.map +1 -1
  45. package/preset/odai/runtime/runtime-support.mjs +21 -8
  46. package/preset/odai/runtime/runtime-support.mjs.map +1 -1
  47. package/preset/odai/runtime/runtime-types.d.mts +2 -0
  48. package/preset/odai/runtime/runtime-types.d.mts.map +1 -1
  49. package/preset/odai/runtime/runtime-types.mjs.map +1 -1
  50. package/preset/odai/runtime/session-evidence.d.mts.map +1 -1
  51. package/preset/odai/runtime/session-evidence.mjs +6 -0
  52. package/preset/odai/runtime/session-evidence.mjs.map +1 -1
  53. package/preset/odai/runtime/skill-bundle.d.mts +1 -1
  54. package/preset/odai/runtime/skill-bundle.d.mts.map +1 -1
  55. package/preset/odai/runtime/tool-runtime.d.mts.map +1 -1
  56. package/preset/odai/runtime/tool-runtime.mjs +28 -0
  57. package/preset/odai/runtime/tool-runtime.mjs.map +1 -1
  58. package/preset/odai/skills/odai-orchestration/SKILL.md +1 -1
  59. package/preset/odai/skills/odai-orchestration/assets/routing-roles/controller.md +1 -1
  60. package/preset/odai/skills/odai-orchestration/assets/routing-roles/writer.md +9 -0
  61. package/preset/odai/skills/odai-orchestration/manifest.json +10 -4
  62. package/preset/odai/skills/odai-orchestration/references/install.md +66 -4
  63. package/preset/odai/skills/odai-orchestration/references/orchestration.md +2 -1
  64. package/preset/odai/skills/odai-orchestration/scripts/build-routing.mjs +78 -24
  65. package/preset/odai/skills/odai-orchestration/scripts/compose-contracts.d.mts +1 -1
  66. package/preset/odai/skills/odai-orchestration/scripts/compose-contracts.mjs +6 -5
  67. package/preset/odai/skills/odai-orchestration/scripts/install-routing.mjs +66 -15
  68. package/preset/odai/skills/odai-orchestration/scripts/local-cli.d.mts +8 -0
  69. package/preset/odai/skills/odai-orchestration/scripts/local-cli.mjs +25 -9
  70. package/preset/odai/skills/odai-orchestration/scripts/recognize-routing.d.mts +6 -0
  71. package/preset/odai/skills/odai-orchestration/scripts/recognize-routing.mjs +93 -0
  72. package/preset/odai/skills/odai-orchestration/scripts/route-hook.mjs +243 -0
  73. package/preset/odai/skills/odai-orchestration/scripts/run-role.mjs +71 -16
  74. package/preset/odai/skills/odai-orchestration/scripts/verify-routing.mjs +2 -2
@@ -1,4 +1,8 @@
1
+ import { recognizeLeadSwitch, leadState, leadTaskId, leadGap, parseLeadVerdict } from "./lead-mode.mjs";
1
2
  import { classifyResponsibilityInterruptionText, decideResearchPrefetch, decideRoute, extractLatestUserText, extractRoutingText, renderMissingRouteConfigNotice, renderRouteFailureNotice, renderRouteNotice, } from "./router.mjs";
3
+ import { recognizeNamedRequests, resolveResponsibilityGap } from "./responsibility-gap.mjs";
4
+ import { CONFIGURABLE_ROLES } from "./routing-config.mjs";
5
+ import { classifyPendingReviewerText } from "./router.mjs";
2
6
  import { classifyModelRouteFailure, probeModelRoute } from "./model-route.mjs";
3
7
  import { selectSharedOutputPolicyForTurn } from "./output-policy-state.mjs";
4
8
  import { prepareSessionOutputControl, previewSessionOutputControl } from "./output-session.mjs";
@@ -12,7 +16,7 @@ import { captureAutomaticMemories, claimSemanticMemoryTurn, currentDirectInput,
12
16
  import { sessionEvents } from "./runtime-types.mjs";
13
17
  const DIRECT_USER_TASK_HEADING = "\n# Direct user task";
14
18
  function effectiveRoleDispatch(role, configured, routingMode) {
15
- if (role === "creative")
19
+ if (role === "creative" || role === "writer")
16
20
  return "child";
17
21
  if (configured)
18
22
  return configured;
@@ -40,6 +44,97 @@ export function installLifecycleRuntime(deps) {
40
44
  const outputUsageBySession = new WeakMap();
41
45
  const inputRequests = new WeakMap();
42
46
  const inputGuards = new WeakSet();
47
+ const namedGaps = (agent) => evidence.events(agent).filter(event => event.type === "odai/responsibility-gap" && event.data.userNamed === true);
48
+ const gapConsumed = (agent, digest) => evidence.events(agent).some(event => event.type === "odai/responsibility-gap-consumed" && event.data.stateDigest === digest);
49
+ const promptNamedRequests = ({ agent, turn, signal }) => {
50
+ if (signal.aborted || isSubagentSession(agent) || config.routing.mode === "off" || namedGaps(agent).length === 0)
51
+ return;
52
+ pendingResponsibilityGap(agent, turn, Number.MAX_SAFE_INTEGER);
53
+ const events = evidence.events(agent);
54
+ const prompts = [];
55
+ for (const { data } of namedGaps(agent).sort((left, right) => Number(left.data.timing === "completion") - Number(right.data.timing === "completion"))) {
56
+ if (gapConsumed(agent, data.stateDigest))
57
+ continue;
58
+ const message = latestDirectUserMessage(agent);
59
+ if (!message || (message.id !== data.taskMessageId && classifyPendingReviewerText(extractLatestUserText([message])) !== "continue")) {
60
+ appendEvent(agent, "odai/responsibility-gap-consumed", { turn, stateDigest: data.stateDigest, responsibility: data.responsibility, reason: "SUPERSEDED_BY_DIRECT_USER_TASK" });
61
+ continue;
62
+ }
63
+ const step = agent.phase?.step ?? sessionEvents(agent.session).findLast(event => event.type === "step/start")?.data.step ?? 0;
64
+ const prompted = events.find(event => event.type === "odai/named-request-prompted" && event.data.stateDigest === data.stateDigest);
65
+ if (prompted) {
66
+ // Coexisting Plugin/Agent listeners can observe the same stop. Only a
67
+ // later native step may conclude that the one reminder was ineffective.
68
+ if (prompted.data.turn === turn && prompted.data.step === step)
69
+ return;
70
+ appendEvent(agent, "odai/responsibility-gap-consumed", { turn, step, stateDigest: data.stateDigest, responsibility: data.responsibility, reason: "NAMED_REQUEST_INCOMPLETE_AFTER_PROMPT" });
71
+ continue;
72
+ }
73
+ appendEvent(agent, "odai/named-request-prompted", { turn, step, stateDigest: data.stateDigest, responsibility: data.responsibility });
74
+ prompts.push(String(data.responsibility));
75
+ break;
76
+ }
77
+ if (prompts.length && !signal.aborted)
78
+ agent.steer?.(pluginMessage(`用户点名尚待运行:${prompts.join("、")}。运行时将在下一步按顺序派发;不要重复提交同项。最终答复逐项说明实际模型与结果,未完成时说明原因,不替换模型或配置。`, "odai named responsibility request"));
79
+ };
80
+ const checkedNamedInputs = new WeakMap();
81
+ const recognizeDirectNames = (agent, turn, step, message) => {
82
+ if (!message?.id || isSubagentSession(agent) || config.routing.mode === "off" || checkedNamedInputs.get(agent)?.has(message.id) || hasSessionEvent(agent, "odai/named-input-checked", data => data.taskMessageId === message.id))
83
+ return;
84
+ let checked = checkedNamedInputs.get(agent);
85
+ if (!checked) {
86
+ checked = new Set();
87
+ checkedNamedInputs.set(agent, checked);
88
+ }
89
+ checked.add(message.id);
90
+ const text = extractLatestUserText([message]);
91
+ const routes = {};
92
+ for (const role of CONFIGURABLE_ROLES) {
93
+ const route = configuredRole(agent, role, turn).route;
94
+ if (route)
95
+ routes[role] = route;
96
+ }
97
+ const change = recognizeLeadSwitch(text, routes);
98
+ if (change && !hasSessionEvent(agent, "odai/lead-switch-checked", data => data.taskMessageId === message.id)) {
99
+ appendEvent(agent, "odai/lead-switch-checked", { taskMessageId: message.id });
100
+ if (!change.issue) {
101
+ appendEvent(agent, "odai/lead-mode", { taskMessageId: message.id, ...change });
102
+ if (!change.enabled)
103
+ for (const event of namedGaps(agent))
104
+ if (event.data.source === "lead-mode" && !gapConsumed(agent, event.data.stateDigest))
105
+ appendEvent(agent, "odai/responsibility-gap-consumed", { stateDigest: event.data.stateDigest, reason: "LEAD_MODE_DISABLED" });
106
+ }
107
+ agent.inject?.(pluginMessage(`主导模式:${change.issue ?? (change.enabled ? "已开启" : "已关闭")}。规划与审查插槽:${JSON.stringify(Object.fromEntries(change.roles.map(role => [role, routes[role]])))}。未启用的插槽不调用;关闭说“关闭主导模式”。请向用户说明。`, "odai lead mode"));
108
+ }
109
+ const taskMessageId = leadTaskId(evidence.events(agent), message.id, classifyPendingReviewerText(text) === "continue");
110
+ if (leadState(evidence.events(agent))?.enabled && !hasSessionEvent(agent, "odai/lead-task", data => data.taskMessageId === taskMessageId))
111
+ appendEvent(agent, "odai/lead-task", { taskMessageId });
112
+ const requests = recognizeNamedRequests(text, routes);
113
+ if (requests.length)
114
+ appendEvent(agent, "odai/named-input-checked", { turn, step, taskMessageId: message.id });
115
+ const notices = [];
116
+ for (const request of requests) {
117
+ if (request.canceled) {
118
+ for (const { data } of namedGaps(agent))
119
+ if (data.responsibility === request.responsibility && !gapConsumed(agent, data.stateDigest))
120
+ appendEvent(agent, "odai/responsibility-gap-consumed", { turn, step, responsibility: data.responsibility, stateDigest: data.stateDigest, reason: "CANCELED_BY_USER" });
121
+ continue;
122
+ }
123
+ if (request.issue) {
124
+ appendEvent(agent, "odai/named-request-unfulfilled", { turn, step, taskMessageId: message.id, ...request, configuredRoute: routes[request.responsibility] });
125
+ notices.push(`${request.responsibility}(${request.name}):${request.issue};当前映射 ${JSON.stringify(routes[request.responsibility] ?? null)}`);
126
+ if (request.issue === "unconfigured") {
127
+ const proposal = resolveResponsibilityGap({ responsibility: request.responsibility, gap: request.excerpt, evidenceRefs: [`user-message:${message.id}`], expectedChange: "完成用户点名职责" });
128
+ notices.push(renderMissingRouteConfigNotice(decideRoute({ text, proposal, writerConfigured: true, creativeConfigured: true }), config.routing.mode));
129
+ }
130
+ continue;
131
+ }
132
+ const proposal = resolveResponsibilityGap({ responsibility: request.responsibility, gap: `用户明确点名:${request.excerpt}`, evidenceRefs: [`user-message:${message.id}`], expectedChange: `按用户指定的${request.timing}时机完成该职责并回交` });
133
+ appendEvent(agent, "odai/responsibility-gap", { ...proposal, stateDigest: `user-named:${proposal.stateDigest}:${message.id}:${request.timing}`, taskMessageId: message.id, turn, step: step - 1, userNamed: true, timing: request.timing, sourceExcerpt: request.excerpt, requestedName: request.name, requestedRoute: routes[request.responsibility] });
134
+ notices.push(`${request.responsibility}:${request.timing === "start" ? "开始时" : "完成时"}`);
135
+ }
136
+ return notices.length ? pluginMessage(`用户点名:${notices.join(";")}。由运行时负责按顺序派发;总控不要自己再派同一项。最终答复必须逐项说明哪个实际模型做了什么,以及未完成的原因;不得替换职责、模型或修改配置。`, "odai user named responsibilities") : undefined;
137
+ };
43
138
  ctx.on("agent/inbox/claimed", ({ agent, turn, message }) => {
44
139
  if (isSubagentSession(agent) || typeof agent.ctx?.on !== "function" || !agent.session.id)
45
140
  return;
@@ -355,6 +450,9 @@ export function installLifecycleRuntime(deps) {
355
450
  const turn = position?.turn;
356
451
  const step = position?.step;
357
452
  if (message && message.id === event.data.id && typeof turn === "number" && typeof step === "number") {
453
+ const notice = recognizeDirectNames(owner, turn, step, message);
454
+ if (notice)
455
+ session.append("user/message", notice, { surfaceOp: "append" });
358
456
  prepareSessionOutputControl({
359
457
  events: evidence.events(owner), text: extractLatestUserText([message]), turn, step,
360
458
  userMessageId: typeof message.id === "string" ? message.id : undefined,
@@ -396,13 +494,8 @@ export function installLifecycleRuntime(deps) {
396
494
  step: event.data?.step ?? activeScope.startStep,
397
495
  responsibility: activeScope.role,
398
496
  target: "controller",
399
- status: typeof owner.inject === "function" ? "recovery-injected" : "recovery-unavailable",
497
+ status: "recovery-pending",
400
498
  });
401
- owner.inject?.(pluginMessage([
402
- `The same-turn ${activeScope.role} responsibility emitted a terminal response without odai_responsibility_return.`,
403
- "Its text is an unverified read-only draft, not final delivery or independent acceptance.",
404
- "The runtime restored the controller route. Verify the decisive claims, continue the authorized task, and keep controller ownership of final delivery.",
405
- ].join("\n"), `${activeScope.role} handback recovery`));
406
499
  }
407
500
  }
408
501
  if (event?.type === "assistant/chunk" && event.data?.chunk?.type === "usage") {
@@ -590,8 +683,57 @@ export function installLifecycleRuntime(deps) {
590
683
  }
591
684
  pendingRouteReceipts.delete(session);
592
685
  });
593
- ctx.on("agent/turn-stopping", ({ agent, turn }) => {
686
+ ctx.on("agent/turn-stopping", ({ agent, turn, signal }) => {
594
687
  stopResponsibilityScope(agent, "turn-stopping");
688
+ const missing = evidence.events(agent).findLast(event => event.type === "odai/responsibility-return-missing" && event.data.turn === turn);
689
+ const recovered = missing && evidence.events(agent).some(event => event.type === "odai/responsibility-recovery" && event.data.scopeId === missing.data.scopeId);
690
+ if (missing && !recovered && !signal?.aborted && typeof agent.steer === "function") {
691
+ agent.steer(pluginMessage([
692
+ `The same-turn ${missing.data.responsibility} responsibility emitted a terminal response without odai_responsibility_return.`,
693
+ "Its text is an unverified read-only draft, not final delivery or independent acceptance.",
694
+ "The runtime restored the controller route. Verify the decisive claims, continue the authorized task, and keep controller ownership of final delivery.",
695
+ ].join("\n"), "responsibility handback recovery"));
696
+ appendEvent(agent, "odai/responsibility-recovery", { turn, scopeId: missing.data.scopeId, status: "recovery-steered" });
697
+ // Recovery already wakes the controller; defer completion prompting to its next stop.
698
+ }
699
+ else if (signal) {
700
+ if (!signal.aborted && !isSubagentSession(agent) && config.routing.mode !== "off" && leadState(evidence.events(agent))?.enabled) {
701
+ const message = latestDirectUserMessage(agent);
702
+ const events = evidence.events(agent);
703
+ const taskId = message && leadTaskId(events, message.id, classifyPendingReviewerText(extractLatestUserText([message])) === "continue");
704
+ const state = leadState(events);
705
+ if (taskId && Array.isArray(state?.roles) && state.roles.includes("reviewer") && events.some(event => event.type === "odai/lead-write" && event.data.taskMessageId === taskId)) {
706
+ const gaps = namedGaps(agent).filter(event => event.data.taskMessageId === taskId && event.data.responsibility === "reviewer" && event.data.timing === "completion");
707
+ const last = gaps.at(-1);
708
+ const prior = events.filter(event => event.type === "odai/lead-review" && event.data.taskMessageId === taskId);
709
+ const step = agent.phase?.step ?? sessionEvents(agent.session).findLast(event => event.type === "step/start")?.data.step ?? 0;
710
+ if (!last && configuredRole(agent, "reviewer", turn).route)
711
+ appendEvent(agent, "odai/responsibility-gap", leadGap("reviewer", taskId, turn, step));
712
+ else if (last && gapConsumed(agent, last.data.stateDigest)) {
713
+ const result = events.findLast(event => event.type === "odai/route-result" && event.data.stateDigest === last.data.stateDigest);
714
+ const decision = events.findLast(event => event.type === "odai/route-decided" && event.data.stateDigest === last.data.stateDigest);
715
+ const scope = decision && events.find(event => event.type === "odai/responsibility-scope-started" && event.data.startStep === decision.data.step && event.data.turn === decision.data.turn);
716
+ const returned = scope && events.find(event => event.type === "odai/responsibility-returned" && event.data.scopeId === scope.data.scopeId);
717
+ if ((result || returned) && !prior.some(event => event.data.stateDigest === last.data.stateDigest)) {
718
+ const text = String(result?.data.outputText ?? returned?.data.summary ?? "");
719
+ const success = (returned && events.some(event => event.type === "odai/route-applied" && event.data.responsibilityScopeId === scope?.data.scopeId && event.data.status === "applied")) || (result?.data.status === "completed" && result.data.routeReceiptStatus === "applied");
720
+ const verdict = success ? parseLeadVerdict(text) : "failed";
721
+ appendEvent(agent, "odai/lead-review", { taskMessageId: taskId, stateDigest: last.data.stateDigest, turn, step, verdict, round: prior.length });
722
+ if (verdict === "changes-requested" && prior.length < 2) {
723
+ agent.steer?.(pluginMessage(`主导模式审查要求修正(最多再审2轮):\n${text}\n请完成修正,下一次结束时再审。`, "odai lead correction"));
724
+ return;
725
+ }
726
+ if (verdict !== "pass")
727
+ agent.steer?.(pluginMessage(`主导模式审查未通过或未完成(${verdict});不再重试。最终答复说明未解决问题及原因。\n${text}`, "odai lead review incomplete"));
728
+ }
729
+ else if (prior.at(-1)?.data.verdict === "changes-requested" && prior.length <= 2 && prior.at(-1)?.data.stateDigest === last.data.stateDigest && (prior.at(-1)?.data.turn !== turn || prior.at(-1)?.data.step !== step)) {
730
+ appendEvent(agent, "odai/responsibility-gap", leadGap("reviewer", taskId, turn, step, prior.length));
731
+ }
732
+ }
733
+ }
734
+ }
735
+ promptNamedRequests({ agent, turn, signal });
736
+ }
595
737
  const role = routedRoleOf(agent);
596
738
  if (!role)
597
739
  return;
@@ -637,6 +779,21 @@ export function installLifecycleRuntime(deps) {
637
779
  return { ...downstream, kind: "reject", reason: "ODAI_INPUT_CHANGED_BEFORE_ADMISSION" };
638
780
  }
639
781
  const directMessage = latestDirectUserMessage(agent, suppliedDirectMessages, { turn });
782
+ const namedNotice = recognizeDirectNames(agent, turn, step, directMessage);
783
+ if (namedNotice)
784
+ downstream = { ...downstream, messages: [...downstream.messages, namedNotice] };
785
+ // A candidate must first pass native admission. The committed user/message
786
+ // observer records it before the model can execute controller tools.
787
+ if (!directMessage && previewInput?.message && !subagentSession && config.routing.mode !== "off") {
788
+ const routes = {};
789
+ for (const role of CONFIGURABLE_ROLES) {
790
+ const route = configuredRole(agent, role, turn).route;
791
+ if (route)
792
+ routes[role] = route;
793
+ }
794
+ if (recognizeNamedRequests(extractLatestUserText([previewInput.message]), routes).length)
795
+ return downstream;
796
+ }
640
797
  const responsibilityGap = subagentSession ? undefined : pendingResponsibilityGap(agent, turn, step);
641
798
  // Inbox messages are committed after pre-step. Do not dispatch a task-bound
642
799
  // proposal while a new or changed human batch still lacks native admission.
@@ -748,6 +905,8 @@ export function installLifecycleRuntime(deps) {
748
905
  }
749
906
  if (config.routing.mode === "off")
750
907
  return downstream;
908
+ if (!responsibilityGap && hasSessionEvent(agent, "odai/named-input-checked", data => data.taskMessageId === (directMessage ?? previewInput?.message)?.id))
909
+ return downstream;
751
910
  if (hasSessionEvent(agent, "odai/route-decided", (data) => data?.turn === turn && data?.step === step)) {
752
911
  return downstream;
753
912
  }
@@ -769,6 +928,7 @@ export function installLifecycleRuntime(deps) {
769
928
  gap: responsibilityGap.gap,
770
929
  expectedChange: responsibilityGap.expectedChange,
771
930
  evidenceRefs: responsibilityGap.evidenceRefs,
931
+ ...(responsibilityGap.userNamed ? { priorHandbacks: evidence.events(agent).filter(event => ["odai/route-result", "odai/research-result"].includes(event.type) && event.data.taskMessageId === responsibilityGap.taskMessageId && event.data.status === "completed" && event.data.routeReceiptStatus === "applied").map(event => ({ responsibility: event.data.role, actualRoute: event.data.actualRoute, output: event.data.outputText })) } : {}),
772
932
  }, undefined, 2),
773
933
  "",
774
934
  DIRECT_USER_TASK_HEADING.trimStart(),
@@ -789,11 +949,12 @@ export function installLifecycleRuntime(deps) {
789
949
  step,
790
950
  role: "researcher",
791
951
  action: "delegate",
952
+ ...(responsibilityGap?.userNamed ? { stateDigest: responsibilityGap.stateDigest, taskMessageId: responsibilityGap.taskMessageId } : {}),
792
953
  mode: config.routing.mode,
793
954
  reasonCode: researchDecision.reasonCode,
794
955
  signals: researchDecision.signals,
795
956
  });
796
- if (config.routing.mode === "observe") {
957
+ if (config.routing.mode === "observe" && !responsibilityGap?.userNamed) {
797
958
  appendEvent(agent, "odai/research-result", {
798
959
  turn,
799
960
  step,
@@ -828,8 +989,9 @@ export function installLifecycleRuntime(deps) {
828
989
  const researchBundle = sharedSkillSelection(agent, turn)?.bundle ?? bundled;
829
990
  const researchContract = dshRoleContract("researcher", researchBundle.roleContracts.researcher);
830
991
  const subagents = isSubagentsService(ctx.subagents) ? ctx.subagents : undefined;
831
- const result = subagents
992
+ const result = subagents || researchRoute.backend === "local-cli"
832
993
  ? await runRoutedRole({
994
+ sessionEvidence: { events: evidence.events, append: (owner, type, data) => appendEvent(owner, type, data) },
833
995
  subagents,
834
996
  provider: config.routing.provider,
835
997
  decision: researchDecision,
@@ -861,6 +1023,7 @@ export function installLifecycleRuntime(deps) {
861
1023
  turn,
862
1024
  step,
863
1025
  role: "researcher",
1026
+ ...(responsibilityGap?.userNamed ? { stateDigest: responsibilityGap.stateDigest, taskMessageId: responsibilityGap.taskMessageId, userNamed: true, outputText: completed ? outputText(result.output) : undefined } : {}),
864
1027
  status: completed ? "completed" : "fallback",
865
1028
  stopReason: completed ? result.stopReason : (packetError ? "packet-invalid" : result.stopReason),
866
1029
  routeSource: researchState.source,
@@ -869,9 +1032,12 @@ export function installLifecycleRuntime(deps) {
869
1032
  requestedRoute: researchRoute,
870
1033
  ...(result.routeReceiptError ? { routeReceiptError: result.routeReceiptError } : {}),
871
1034
  ...(result.actualRoute ? { actualRoute: result.actualRoute } : {}),
1035
+ ...(result.boundaryStatus ? { boundaryStatus: result.boundaryStatus, boundaryTools: result.boundaryTools } : {}),
872
1036
  ...(packet ? { packetDigest: packet.digest, sourceCount: packet.sourceCount } : {}),
873
1037
  ...(packetError ? { error: packetError } : result.taskError ? { error: result.taskError } : {}),
874
1038
  });
1039
+ if (result.sessionNotice)
1040
+ routedDownstream = { ...routedDownstream, messages: [...routedDownstream.messages, pluginMessage(result.sessionNotice, "responsibility session recovery")] };
875
1041
  if (completed && packet) {
876
1042
  researchPacketText = renderResearchPacket(packet);
877
1043
  routedDownstream = {
@@ -885,8 +1051,15 @@ export function installLifecycleRuntime(deps) {
885
1051
  }
886
1052
  }
887
1053
  }
1054
+ if (responsibilityGap?.userNamed && responsibilityGap.responsibility === "researcher" && !sameTurnResearchDecision) {
1055
+ appendEvent(agent, "odai/responsibility-gap-consumed", { turn, step, responsibility: "researcher", stateDigest: responsibilityGap.stateDigest, reason: "NAMED_RESEARCH_ATTEMPTED" });
1056
+ if (!researchPacketText)
1057
+ routedDownstream = { ...routedDownstream, messages: [...routedDownstream.messages, pluginMessage("用户点名 researcher 未完成:未取得通过核实的研究证据包。最终答复说明实际回执及原因,不重试、不替换模型或修改配置。", "odai named research incomplete")] };
1058
+ return routedDownstream;
1059
+ }
888
1060
  let decision = sameTurnResearchDecision ?? decideRoute({
889
1061
  text: taskText,
1062
+ writerConfigured: Boolean(configuredRole(agent, "writer", turn).route),
890
1063
  creativeConfigured: Boolean(configuredRole(agent, "creative", turn).route),
891
1064
  creativeText: (extractLatestUserText(downstream.messages) || authenticatedDirectText).slice(0, config.routing.maxInputChars),
892
1065
  proposal: responsibilityGap,
@@ -1023,7 +1196,7 @@ export function installLifecycleRuntime(deps) {
1023
1196
  ],
1024
1197
  };
1025
1198
  }
1026
- if (config.routing.mode === "observe") {
1199
+ if (config.routing.mode === "observe" && !responsibilityGap?.userNamed) {
1027
1200
  return {
1028
1201
  kind: "enter",
1029
1202
  messages: [
@@ -1057,11 +1230,16 @@ export function installLifecycleRuntime(deps) {
1057
1230
  }
1058
1231
  const roleBundle = sharedSkillSelection(agent, turn)?.bundle ?? bundled;
1059
1232
  const canonicalRoleContract = roleBundle.roleContracts[routeRole];
1060
- const roleContract = dshRoleContract(routeRole, canonicalRoleContract);
1233
+ const leadReview = routeRole === "reviewer" && leadState(evidence.events(agent))?.enabled === true && evidence.events(agent).some(event => event.type === "odai/lead-write" && event.data.taskMessageId === responsibilityGap?.taskMessageId);
1234
+ const roleContract = dshRoleContract(routeRole, canonicalRoleContract) + (leadReview ? "\n主导模式验收:回交末尾必须独占一行 VERDICT: pass 或 VERDICT: changes-requested。same-turn 时写在 odai_responsibility_return 的 summary 末尾。" : "");
1061
1235
  let rolePreflightVerified = false;
1062
1236
  if (roleRoute.backend !== "local-cli" && routeRole === "frontend" && decision.action === "upgrade") {
1063
1237
  const health = await probeModelRoute((candidate, candidateSignal) => ctx.llm.resolveCallConfig(candidate, candidateSignal), roleRoute, signal);
1064
1238
  if (health.status === "rejected") {
1239
+ if (responsibilityGap?.userNamed) {
1240
+ appendEvent(agent, "odai/route-result", { turn, step, role: routeRole, stateDigest: responsibilityGap.stateDigest, taskMessageId: responsibilityGap.taskMessageId, status: "fallback", stopReason: "route-preflight-failed", error: health.failure.message });
1241
+ return { kind: "enter", messages: [...routedDownstream.messages, pluginMessage(`用户点名的 ${routeRole} 未完成:${health.failure.message}。不重试,不替换模型,不修改配置;最终答复必须说明该项未完成及原因。`, "odai named responsibility failed")] };
1242
+ }
1065
1243
  const invalidation = invalidateFailedRoleRoute(agent, routeRole, roleRoute, roleState.source, health.failure, { turn, step, position: "pre-step" });
1066
1244
  appendEvent(agent, "odai/route-result", {
1067
1245
  turn,
@@ -1221,6 +1399,7 @@ export function installLifecycleRuntime(deps) {
1221
1399
  const subagents = isSubagentsService(ctx.subagents) ? ctx.subagents : undefined;
1222
1400
  const result = subagents || roleRoute.backend === "local-cli"
1223
1401
  ? await runRoutedRole({
1402
+ sessionEvidence: { events: evidence.events, append: (owner, type, data) => appendEvent(owner, type, data) },
1224
1403
  subagents,
1225
1404
  provider: config.routing.provider,
1226
1405
  decision: delegationDecision,
@@ -1245,6 +1424,7 @@ export function installLifecycleRuntime(deps) {
1245
1424
  action: "delegate",
1246
1425
  status: result.status,
1247
1426
  stopReason: result.stopReason,
1427
+ ...(responsibilityGap?.userNamed ? { stateDigest: responsibilityGap.stateDigest, taskMessageId: responsibilityGap.taskMessageId, userNamed: true, outputText: result.status === "completed" ? outputText(result.output) : undefined } : {}),
1248
1428
  routeSource: roleState.source,
1249
1429
  fallbackUsed: result.status !== "completed",
1250
1430
  routeReceiptStatus: result.routeReceiptStatus,
@@ -1253,6 +1433,8 @@ export function installLifecycleRuntime(deps) {
1253
1433
  ...(result.routeReceiptError ? { routeReceiptError: result.routeReceiptError } : {}),
1254
1434
  ...(result.actualRoute ? { actualRoute: result.actualRoute } : {}),
1255
1435
  ...(result.taskError ? { error: result.taskError } : {}),
1436
+ ...(result.sessionAction ? { sessionAction: result.sessionAction, routeSessionId: result.routeSessionId, sessionNotice: result.sessionNotice } : {}),
1437
+ ...(result.boundaryStatus ? { boundaryStatus: result.boundaryStatus, boundaryTools: result.boundaryTools } : {}),
1256
1438
  ...(result.durationMs === undefined ? {} : { durationMs: result.durationMs }),
1257
1439
  ...(result.usage ? { usage: result.usage } : {}),
1258
1440
  ...(result.cliReportedCostUsd === undefined ? {} : { cliReportedCostUsd: result.cliReportedCostUsd }),
@@ -1262,6 +1444,7 @@ export function installLifecycleRuntime(deps) {
1262
1444
  if (result.status === "completed") {
1263
1445
  const childText = outputText(result.output);
1264
1446
  const heading = [
1447
+ ...(result.sessionNotice ? [result.sessionNotice] : []),
1265
1448
  renderRouteNotice(delegationDecision, config.routing.mode, result.actualRoute, "child"),
1266
1449
  ...(result.reasoningEffortVerification === "verified"
1267
1450
  ? [`Reasoning effort: ${result.actualRoute?.reasoningEffort ?? "unspecified"} (verified from CLI session evidence).`]
@@ -1276,12 +1459,12 @@ export function installLifecycleRuntime(deps) {
1276
1459
  ],
1277
1460
  };
1278
1461
  }
1279
- const failure = result.error ?? result.stopReason;
1462
+ const failure = [result.sessionNotice, result.error ?? result.stopReason].filter(Boolean).join("; ");
1280
1463
  return {
1281
1464
  kind: "enter",
1282
1465
  messages: [
1283
1466
  ...routedDownstream.messages,
1284
- pluginMessage(renderRouteFailureNotice(delegationDecision, failure), `odai ${routeRole} route unavailable; controller retains ownership`),
1467
+ pluginMessage(renderRouteFailureNotice(delegationDecision, failure) + (responsibilityGap?.stateDigest.startsWith("user-named:lead-mode:") ? "\n主导模式该职责未完成:继续授权任务,不重试、不替换模型;最终答复必须说明未完成的步骤及上述原因。" : ""), `odai ${routeRole} route unavailable; controller retains ownership`),
1285
1468
  ],
1286
1469
  };
1287
1470
  };