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.
- package/README.md +11 -9
- package/client/client.js +14 -7
- package/package.json +1 -1
- package/preset/odai/package.json +2 -1
- package/preset/odai/runtime/context-activation.mjs +1 -1
- package/preset/odai/runtime/context-activation.mjs.map +1 -1
- package/preset/odai/runtime/index.d.mts.map +1 -1
- package/preset/odai/runtime/index.mjs +51 -2
- package/preset/odai/runtime/index.mjs.map +1 -1
- package/preset/odai/runtime/lead-mode.d.mts +23 -0
- package/preset/odai/runtime/lead-mode.d.mts.map +1 -0
- package/preset/odai/runtime/lead-mode.mjs +17 -0
- package/preset/odai/runtime/lead-mode.mjs.map +1 -0
- package/preset/odai/runtime/lifecycle-runtime.d.mts.map +1 -1
- package/preset/odai/runtime/lifecycle-runtime.mjs +197 -14
- package/preset/odai/runtime/lifecycle-runtime.mjs.map +1 -1
- package/preset/odai/runtime/responsibility-gap.d.mts +4 -1
- package/preset/odai/runtime/responsibility-gap.d.mts.map +1 -1
- package/preset/odai/runtime/responsibility-gap.mjs +1 -0
- package/preset/odai/runtime/responsibility-gap.mjs.map +1 -1
- package/preset/odai/runtime/review-evidence.d.mts +2 -0
- package/preset/odai/runtime/review-evidence.d.mts.map +1 -1
- package/preset/odai/runtime/review-evidence.mjs +45 -0
- package/preset/odai/runtime/review-evidence.mjs.map +1 -1
- package/preset/odai/runtime/role-overlays.d.mts +1 -1
- package/preset/odai/runtime/role-overlays.d.mts.map +1 -1
- package/preset/odai/runtime/role-overlays.mjs +4 -1
- package/preset/odai/runtime/role-overlays.mjs.map +1 -1
- package/preset/odai/runtime/route-session.d.mts +20 -0
- package/preset/odai/runtime/route-session.d.mts.map +1 -0
- package/preset/odai/runtime/route-session.mjs +44 -0
- package/preset/odai/runtime/route-session.mjs.map +1 -0
- package/preset/odai/runtime/router.d.mts +2 -1
- package/preset/odai/runtime/router.d.mts.map +1 -1
- package/preset/odai/runtime/router.mjs +52 -1
- package/preset/odai/runtime/router.mjs.map +1 -1
- package/preset/odai/runtime/routing-config.d.mts +5 -2
- package/preset/odai/runtime/routing-config.d.mts.map +1 -1
- package/preset/odai/runtime/routing-config.mjs +25 -20
- package/preset/odai/runtime/routing-config.mjs.map +1 -1
- package/preset/odai/runtime/runtime-config.d.mts +1 -1
- package/preset/odai/runtime/runtime-config.d.mts.map +1 -1
- package/preset/odai/runtime/runtime-support.d.mts +6 -1
- package/preset/odai/runtime/runtime-support.d.mts.map +1 -1
- package/preset/odai/runtime/runtime-support.mjs +21 -8
- package/preset/odai/runtime/runtime-support.mjs.map +1 -1
- package/preset/odai/runtime/runtime-types.d.mts +2 -0
- package/preset/odai/runtime/runtime-types.d.mts.map +1 -1
- package/preset/odai/runtime/runtime-types.mjs.map +1 -1
- package/preset/odai/runtime/session-evidence.d.mts.map +1 -1
- package/preset/odai/runtime/session-evidence.mjs +6 -0
- package/preset/odai/runtime/session-evidence.mjs.map +1 -1
- package/preset/odai/runtime/skill-bundle.d.mts +1 -1
- package/preset/odai/runtime/skill-bundle.d.mts.map +1 -1
- package/preset/odai/runtime/tool-runtime.d.mts.map +1 -1
- package/preset/odai/runtime/tool-runtime.mjs +28 -0
- package/preset/odai/runtime/tool-runtime.mjs.map +1 -1
- package/preset/odai/skills/odai-orchestration/SKILL.md +1 -1
- package/preset/odai/skills/odai-orchestration/assets/routing-roles/controller.md +1 -1
- package/preset/odai/skills/odai-orchestration/assets/routing-roles/writer.md +9 -0
- package/preset/odai/skills/odai-orchestration/manifest.json +10 -4
- package/preset/odai/skills/odai-orchestration/references/install.md +66 -4
- package/preset/odai/skills/odai-orchestration/references/orchestration.md +2 -1
- package/preset/odai/skills/odai-orchestration/scripts/build-routing.mjs +78 -24
- package/preset/odai/skills/odai-orchestration/scripts/compose-contracts.d.mts +1 -1
- package/preset/odai/skills/odai-orchestration/scripts/compose-contracts.mjs +6 -5
- package/preset/odai/skills/odai-orchestration/scripts/install-routing.mjs +66 -15
- package/preset/odai/skills/odai-orchestration/scripts/local-cli.d.mts +8 -0
- package/preset/odai/skills/odai-orchestration/scripts/local-cli.mjs +25 -9
- package/preset/odai/skills/odai-orchestration/scripts/recognize-routing.d.mts +6 -0
- package/preset/odai/skills/odai-orchestration/scripts/recognize-routing.mjs +93 -0
- package/preset/odai/skills/odai-orchestration/scripts/route-hook.mjs +243 -0
- package/preset/odai/skills/odai-orchestration/scripts/run-role.mjs +71 -16
- 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:
|
|
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
|
|
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
|
};
|