odai-dsh-agent 0.2.0 → 0.2.1
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 +8 -4
- package/package.json +1 -1
- package/preset/odai/runtime/context-activation.mjs +71 -0
- package/preset/odai/runtime/context-capability.mjs +86 -0
- package/preset/odai/runtime/governance.mjs +8 -1
- package/preset/odai/runtime/human-care.mjs +45 -0
- package/preset/odai/runtime/human-safety-continuity.mjs +2 -2
- package/preset/odai/runtime/human-safety.mjs +3 -3
- package/preset/odai/runtime/index.mjs +336 -50
- package/preset/odai/runtime/responsibility-gap.mjs +8 -10
- package/preset/odai/runtime/responsibility-scope.mjs +166 -0
- package/preset/odai/runtime/role-overlays.mjs +4 -4
- package/preset/odai/runtime/routing-config.mjs +1 -1
- package/preset/odai/runtime/semantic-memory.mjs +2 -2
- package/preset/odai/skills/odai/SKILL.md +69 -68
- package/preset/odai/skills/odai/manifest.json +2 -1
- package/preset/odai/skills/odai/references/care.md +26 -0
- package/preset/odai/skills/odai/references/human-safety.md +33 -54
- package/src/dsh-version.mjs +6 -2
|
@@ -51,6 +51,29 @@ import {
|
|
|
51
51
|
import { createSessionEvidence, resolveSessionEvidenceRoot } from "./session-evidence.mjs";
|
|
52
52
|
import { classifyModelRouteFailure, probeModelRoute } from "./model-route.mjs";
|
|
53
53
|
import { RESPONSIBILITY_GAP_PROMPT, createResponsibilityGapTool } from "./responsibility-gap.mjs";
|
|
54
|
+
import {
|
|
55
|
+
ODAI_CORE_TOOL_NAMES,
|
|
56
|
+
activeOdaiToolNames,
|
|
57
|
+
classifyContextActivation,
|
|
58
|
+
inactiveOdaiToolNames,
|
|
59
|
+
} from "./context-activation.mjs";
|
|
60
|
+
import {
|
|
61
|
+
activateRequestedCapabilities,
|
|
62
|
+
createContextCapabilityTool,
|
|
63
|
+
requestedContextCapabilities,
|
|
64
|
+
} from "./context-capability.mjs";
|
|
65
|
+
import {
|
|
66
|
+
claimResponsibilityScope,
|
|
67
|
+
createResponsibilityScope,
|
|
68
|
+
latestDanglingResponsibilityScope,
|
|
69
|
+
pendingResponsibilityScopeRestoration,
|
|
70
|
+
responsibilityScopeClaimedEvent,
|
|
71
|
+
responsibilityScopeOwnsRequest,
|
|
72
|
+
responsibilityScopeStartedEvent,
|
|
73
|
+
responsibilityScopeStopReason,
|
|
74
|
+
responsibilityScopeStoppedEvent,
|
|
75
|
+
} from "./responsibility-scope.mjs";
|
|
76
|
+
import { HUMAN_CARE_REFERENCE_PATH, createHumanCareTool } from "./human-care.mjs";
|
|
54
77
|
import { HUMAN_SAFETY_REFERENCE_PATH, createHumanSafetyTool } from "./human-safety.mjs";
|
|
55
78
|
import {
|
|
56
79
|
HUMAN_SAFETY_CONTINUITY_PROMPT,
|
|
@@ -878,6 +901,10 @@ export function apply(ctx, rawConfig) {
|
|
|
878
901
|
memorySettingsSnapshots.set(agent, { turn, settings });
|
|
879
902
|
return settings;
|
|
880
903
|
};
|
|
904
|
+
const contextActivationFor = (agent, text, turn = currentAgentTurn(agent)) => activateRequestedCapabilities(
|
|
905
|
+
classifyContextActivation(text),
|
|
906
|
+
requestedContextCapabilities(evidence.events(agent), turn),
|
|
907
|
+
);
|
|
881
908
|
const routingSnapshots = new WeakMap();
|
|
882
909
|
const routingSnapshotFor = (agent, turn = currentAgentTurn(agent)) => {
|
|
883
910
|
const cached = routingSnapshots.get(agent);
|
|
@@ -933,11 +960,18 @@ export function apply(ctx, rawConfig) {
|
|
|
933
960
|
}
|
|
934
961
|
const outputPrompt = renderOutputPolicyPrompt(outputSelection.policy);
|
|
935
962
|
const childSession = isSubagentSession(agent);
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
963
|
+
const directMessage = latestDirectUserMessage(agent);
|
|
964
|
+
const activation = contextActivationFor(agent, directMessage ? extractLatestUserText([directMessage]) : "", turn);
|
|
965
|
+
const proposedStep = (currentAgentStep(agent) ?? 0) + 1;
|
|
966
|
+
const routeCardNeeded = !childSession && (Boolean(activeRouteCard(evidence.events(agent)))
|
|
967
|
+
|| ["planner", "executor"].includes(pendingResponsibilityGap(agent, turn, proposedStep)?.responsibility));
|
|
968
|
+
const routingPrompt = !activation.routingConfig
|
|
969
|
+
? ""
|
|
970
|
+
: childSession
|
|
971
|
+
? ROUTING_CONFIG_PROMPT
|
|
972
|
+
: `${ROUTING_CONFIG_PROMPT}\n\n${renderEffectiveRoutingContext(routingSnapshotFor(agent, turn))}`;
|
|
939
973
|
let continuityRecordPrompt;
|
|
940
|
-
if (!childSession) {
|
|
974
|
+
if (!childSession && (activation.care || activation.safety || activation.continuity)) {
|
|
941
975
|
try {
|
|
942
976
|
continuityRecordPrompt = renderHumanSafetyContinuitySection(
|
|
943
977
|
readHumanSafetyContinuityStore(humanSafetyContinuityStorePath),
|
|
@@ -946,7 +980,9 @@ export function apply(ctx, rawConfig) {
|
|
|
946
980
|
logger.warn(`Odai human-safety continuity is unavailable for this turn: ${error instanceof Error ? error.message : String(error)}`);
|
|
947
981
|
}
|
|
948
982
|
}
|
|
949
|
-
const continuityPrompt = [HUMAN_SAFETY_CONTINUITY_PROMPT, continuityRecordPrompt]
|
|
983
|
+
const continuityPrompt = [activation.continuity ? HUMAN_SAFETY_CONTINUITY_PROMPT : undefined, continuityRecordPrompt]
|
|
984
|
+
.filter(Boolean)
|
|
985
|
+
.join("\n\n");
|
|
950
986
|
if (outputPrompt && !hasSessionEvent(agent, "odai/output-policy-selected", (data) => data?.turn === turn)) {
|
|
951
987
|
appendEvent(agent, "odai/output-policy-selected", {
|
|
952
988
|
...(turn === undefined ? {} : { turn }),
|
|
@@ -960,15 +996,51 @@ export function apply(ctx, rawConfig) {
|
|
|
960
996
|
if (section.name === "odai:canonical-governance") return { ...section, text: canonicalPrompt(selection) };
|
|
961
997
|
if (section.name === "odai:routing-configuration") return { ...section, text: routingPrompt };
|
|
962
998
|
if (section.name === "odai:human-safety-continuity") return { ...section, text: continuityPrompt };
|
|
999
|
+
if (section.name === "odai:responsibility-gap") return { ...section, text: childSession ? "" : RESPONSIBILITY_GAP_PROMPT };
|
|
1000
|
+
if (section.name === "odai:route-card") return { ...section, text: routeCardNeeded ? ROUTE_CARD_PROMPT : "" };
|
|
1001
|
+
if (section.name === "odai:skill-source-configuration") return { ...section, text: activation.skillSource ? SKILL_SOURCE_CONFIG_PROMPT : "" };
|
|
963
1002
|
if (section.name === "odai:controller-output-policy") return { ...section, text: outputPrompt };
|
|
1003
|
+
if (section.name === "odai:compaction-model-configuration") return { ...section, text: activation.compactionConfig ? COMPACTION_CONFIG_PROMPT : "" };
|
|
1004
|
+
if (section.name === "odai:semantic-memory") return { ...section, text: activation.memory ? MEMORY_PROMPT : "" };
|
|
964
1005
|
return section;
|
|
965
1006
|
}),
|
|
966
1007
|
};
|
|
967
1008
|
});
|
|
968
1009
|
|
|
969
1010
|
const routeProtections = new WeakMap();
|
|
970
|
-
const
|
|
1011
|
+
const responsibilityScopes = new WeakMap();
|
|
1012
|
+
const responsibilityScopeOwners = new WeakMap();
|
|
971
1013
|
const pendingRouteReceipts = new WeakMap();
|
|
1014
|
+
const pendingScopeRestorations = new WeakMap();
|
|
1015
|
+
const stopResponsibilityScope = (agent, reason, position = {}) => {
|
|
1016
|
+
const scope = responsibilityScopes.get(agent);
|
|
1017
|
+
if (!scope || (position.scopeId && position.scopeId !== scope.id)) return undefined;
|
|
1018
|
+
responsibilityScopes.delete(agent);
|
|
1019
|
+
const protection = routeProtections.get(agent);
|
|
1020
|
+
if (protection?.scopeId === scope.id) routeProtections.delete(agent);
|
|
1021
|
+
appendEvent(agent, "odai/route-protection-released", {
|
|
1022
|
+
scopeId: scope.id,
|
|
1023
|
+
turn: scope.turn,
|
|
1024
|
+
reason,
|
|
1025
|
+
});
|
|
1026
|
+
appendEvent(agent, "odai/responsibility-scope-stopped", responsibilityScopeStoppedEvent(scope, reason, position));
|
|
1027
|
+
return scope;
|
|
1028
|
+
};
|
|
1029
|
+
const stopDanglingResponsibilityScope = (agent, reason) => {
|
|
1030
|
+
if (responsibilityScopes.has(agent)) return undefined;
|
|
1031
|
+
const dangling = latestDanglingResponsibilityScope(evidence.events(agent));
|
|
1032
|
+
if (!dangling) return undefined;
|
|
1033
|
+
appendEvent(agent, "odai/route-protection-released", {
|
|
1034
|
+
scopeId: dangling.scopeId,
|
|
1035
|
+
turn: dangling.turn,
|
|
1036
|
+
reason,
|
|
1037
|
+
});
|
|
1038
|
+
appendEvent(agent, "odai/responsibility-scope-stopped", {
|
|
1039
|
+
...dangling,
|
|
1040
|
+
reason,
|
|
1041
|
+
});
|
|
1042
|
+
return dangling;
|
|
1043
|
+
};
|
|
972
1044
|
const configuredRole = (agent, role, turn = currentAgentTurn(agent)) => {
|
|
973
1045
|
const state = routingSnapshotFor(agent, turn);
|
|
974
1046
|
if (state.error) return state;
|
|
@@ -1043,6 +1115,18 @@ export function apply(ctx, rawConfig) {
|
|
|
1043
1115
|
return routeProtections.get(agent) ?? activeRouteProtection(agent, evidence.events(agent));
|
|
1044
1116
|
},
|
|
1045
1117
|
});
|
|
1118
|
+
ctx.tools.register(createContextCapabilityTool({
|
|
1119
|
+
isChild: isSubagent,
|
|
1120
|
+
onRequested(agent, capability) {
|
|
1121
|
+
const turn = currentAgentTurn(agent);
|
|
1122
|
+
const step = currentAgentStep(agent);
|
|
1123
|
+
appendEvent(agent, "odai/context-capability-requested", {
|
|
1124
|
+
...(turn === undefined ? {} : { turn }),
|
|
1125
|
+
...(step === undefined ? {} : { step }),
|
|
1126
|
+
capability,
|
|
1127
|
+
});
|
|
1128
|
+
},
|
|
1129
|
+
}));
|
|
1046
1130
|
ctx.tools.register(createRoutingConfigTool(config.routing.configPath, {
|
|
1047
1131
|
configuredRoles: config.routing.roles,
|
|
1048
1132
|
resolveCallConfig(route, signal) {
|
|
@@ -1055,6 +1139,13 @@ export function apply(ctx, rawConfig) {
|
|
|
1055
1139
|
appendEvent(agent, "odai/routing-configured", data);
|
|
1056
1140
|
},
|
|
1057
1141
|
}));
|
|
1142
|
+
ctx.tools.register(createHumanCareTool({
|
|
1143
|
+
isChild: isSubagent,
|
|
1144
|
+
contractFor(agent) {
|
|
1145
|
+
const bundle = sharedSkillSelection(agent)?.bundle ?? bundled;
|
|
1146
|
+
return readSkillBundleFile(bundle, HUMAN_CARE_REFERENCE_PATH).toString("utf8");
|
|
1147
|
+
},
|
|
1148
|
+
}));
|
|
1058
1149
|
ctx.tools.register(createHumanSafetyTool({
|
|
1059
1150
|
isChild: isSubagent,
|
|
1060
1151
|
contractFor(agent) {
|
|
@@ -1149,6 +1240,41 @@ export function apply(ctx, rawConfig) {
|
|
|
1149
1240
|
}));
|
|
1150
1241
|
ctx.tools.guard((execution) => childGuard(execution) ?? routeProtectionGuard(execution));
|
|
1151
1242
|
|
|
1243
|
+
const toolExposureStates = new WeakMap();
|
|
1244
|
+
const syncToolExposure = (agent, activation, options = {}) => {
|
|
1245
|
+
const child = isSubagentSession(agent);
|
|
1246
|
+
const activeNames = activeOdaiToolNames(activation, { child, routeCard: options.routeCard === true });
|
|
1247
|
+
const deniedNames = [
|
|
1248
|
+
...inactiveOdaiToolNames(activeNames),
|
|
1249
|
+
...(child ? ODAI_CORE_TOOL_NAMES : []),
|
|
1250
|
+
];
|
|
1251
|
+
const key = deniedNames.join("\u0000");
|
|
1252
|
+
const previous = toolExposureStates.get(agent);
|
|
1253
|
+
if (previous?.key === key || ["unsupported", "fallback"].includes(previous?.key)) return activeNames;
|
|
1254
|
+
previous?.dispose?.();
|
|
1255
|
+
const restrict = agent?.ctx?.tools?.restrict;
|
|
1256
|
+
if (typeof restrict !== "function") {
|
|
1257
|
+
toolExposureStates.set(agent, Object.freeze({ key: "unsupported", dispose: undefined }));
|
|
1258
|
+
return activeNames;
|
|
1259
|
+
}
|
|
1260
|
+
try {
|
|
1261
|
+
const dispose = deniedNames.length > 0
|
|
1262
|
+
? restrict.call(agent.ctx.tools, { deny: deniedNames })
|
|
1263
|
+
: undefined;
|
|
1264
|
+
toolExposureStates.set(agent, Object.freeze({ key, dispose }));
|
|
1265
|
+
appendEvent(agent, "odai/tool-exposure-selected", {
|
|
1266
|
+
...(options.turn === undefined ? {} : { turn: options.turn }),
|
|
1267
|
+
...(options.step === undefined ? {} : { step: options.step }),
|
|
1268
|
+
mode: "adaptive",
|
|
1269
|
+
activeTools: activeNames,
|
|
1270
|
+
});
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
logger.warn(`Odai adaptive tool exposure is unavailable; retaining the complete tool catalog: ${error instanceof Error ? error.message : String(error)}`);
|
|
1273
|
+
toolExposureStates.set(agent, Object.freeze({ key: "fallback", dispose: undefined }));
|
|
1274
|
+
}
|
|
1275
|
+
return activeNames;
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1152
1278
|
ctx.on("tools/result", (execution, result) => {
|
|
1153
1279
|
if (!execution.agent) return;
|
|
1154
1280
|
const summary = summarizeToolResult(execution, result);
|
|
@@ -1157,7 +1283,7 @@ export function apply(ctx, rawConfig) {
|
|
|
1157
1283
|
});
|
|
1158
1284
|
|
|
1159
1285
|
ctx.on("agent/request", async ({ agent, turn, step, signal }, next) => {
|
|
1160
|
-
|
|
1286
|
+
let proposed = await next();
|
|
1161
1287
|
const childRole = routedRoleOf(agent);
|
|
1162
1288
|
const childRoleState = childRole ? configuredRole(agent, childRole, turn) : undefined;
|
|
1163
1289
|
if (childRole && !childRoleState.route) {
|
|
@@ -1165,14 +1291,42 @@ export function apply(ctx, rawConfig) {
|
|
|
1165
1291
|
? `Odai ${childRole} child route is unavailable: ${childRoleState.detail}`
|
|
1166
1292
|
: `Odai ${childRole} child route is not configured`);
|
|
1167
1293
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1294
|
+
let scope = responsibilityScopes.get(agent);
|
|
1295
|
+
if (scope && !responsibilityScopeOwnsRequest(scope, turn, step)) {
|
|
1296
|
+
stopResponsibilityScope(agent, "ownership-boundary", { step });
|
|
1297
|
+
scope = undefined;
|
|
1298
|
+
}
|
|
1299
|
+
if (!childRole && !scope) {
|
|
1300
|
+
const restoration = pendingResponsibilityScopeRestoration(evidence.events(agent));
|
|
1301
|
+
if (restoration && sameRequestModelRoute(proposed, restoration.temporaryRoute)) {
|
|
1302
|
+
const { reasoningEffort: _temporaryEffort, maxTokens: _temporaryMaxTokens, ...withoutTemporaryRoute } = proposed;
|
|
1303
|
+
proposed = Object.freeze({ ...withoutTemporaryRoute, ...restoration.baseRoute });
|
|
1304
|
+
appendEvent(agent, "odai/responsibility-scope-restoration-requested", {
|
|
1305
|
+
scopeId: restoration.scopeId,
|
|
1306
|
+
turn,
|
|
1307
|
+
step,
|
|
1308
|
+
role: restoration.role,
|
|
1309
|
+
requestedRoute: restoration.baseRoute,
|
|
1310
|
+
});
|
|
1311
|
+
if (agent?.session) {
|
|
1312
|
+
pendingScopeRestorations.set(agent.session, Object.freeze({
|
|
1313
|
+
agent,
|
|
1314
|
+
scopeId: restoration.scopeId,
|
|
1315
|
+
turn,
|
|
1316
|
+
step,
|
|
1317
|
+
role: restoration.role,
|
|
1318
|
+
expectedRoute: restoration.baseRoute,
|
|
1319
|
+
}));
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
const upgradeRole = scope?.role;
|
|
1170
1324
|
let roleRoute = childRole
|
|
1171
1325
|
? childRoleState.route
|
|
1172
|
-
:
|
|
1173
|
-
?
|
|
1326
|
+
: scope
|
|
1327
|
+
? scope.route
|
|
1174
1328
|
: undefined;
|
|
1175
|
-
const routeSource = childRole ? childRoleState?.source :
|
|
1329
|
+
const routeSource = childRole ? childRoleState?.source : scope?.source;
|
|
1176
1330
|
let routeMode = childRole ? "child" : sameRequestModelRoute(proposed, roleRoute) ? "inline" : "same-turn";
|
|
1177
1331
|
let scopedFrontendMaxTokens = upgradeRole === "frontend" ? roleRoute?.maxTokens : undefined;
|
|
1178
1332
|
const finalize = (finalRequest) => {
|
|
@@ -1191,7 +1345,8 @@ export function apply(ctx, rawConfig) {
|
|
|
1191
1345
|
responsibility: childRole ?? upgradeRole,
|
|
1192
1346
|
routeMode,
|
|
1193
1347
|
routeSource,
|
|
1194
|
-
...(
|
|
1348
|
+
...(scope?.id ? { responsibilityScopeId: scope.id } : {}),
|
|
1349
|
+
...(scope?.cardId ? { routeCardId: scope.cardId } : {}),
|
|
1195
1350
|
requestedRoute: expectedRoute,
|
|
1196
1351
|
expectedRoute,
|
|
1197
1352
|
}));
|
|
@@ -1210,7 +1365,17 @@ export function apply(ctx, rawConfig) {
|
|
|
1210
1365
|
? { maxTokens: roleRoute.maxTokens }
|
|
1211
1366
|
: {}),
|
|
1212
1367
|
});
|
|
1213
|
-
|
|
1368
|
+
if (!childRole && scope?.state === "pending") {
|
|
1369
|
+
scope = claimResponsibilityScope(scope, {
|
|
1370
|
+
step,
|
|
1371
|
+
baseRoute: proposed,
|
|
1372
|
+
temporaryRoute: request,
|
|
1373
|
+
routeMode,
|
|
1374
|
+
});
|
|
1375
|
+
responsibilityScopes.set(agent, scope);
|
|
1376
|
+
appendEvent(agent, "odai/responsibility-scope-claimed", responsibilityScopeClaimedEvent(scope));
|
|
1377
|
+
}
|
|
1378
|
+
const validation = scope?.routeValidated
|
|
1214
1379
|
? Object.freeze({ status: "verified" })
|
|
1215
1380
|
: await probeModelRoute(
|
|
1216
1381
|
(candidate, candidateSignal) => ctx.llm.resolveCallConfig(candidate, candidateSignal),
|
|
@@ -1241,9 +1406,9 @@ export function apply(ctx, rawConfig) {
|
|
|
1241
1406
|
error: validation.failure.message,
|
|
1242
1407
|
invalidated: invalidation.invalidated,
|
|
1243
1408
|
});
|
|
1244
|
-
if (
|
|
1409
|
+
if (scope?.cardId) {
|
|
1245
1410
|
appendEvent(agent, "odai/route-card-claim-released", {
|
|
1246
|
-
cardId:
|
|
1411
|
+
cardId: scope.cardId,
|
|
1247
1412
|
turn,
|
|
1248
1413
|
step,
|
|
1249
1414
|
reason: "route-validation-failed",
|
|
@@ -1255,9 +1420,9 @@ export function apply(ctx, rawConfig) {
|
|
|
1255
1420
|
error.routeFailureKind = validation.failure.kind;
|
|
1256
1421
|
throw error;
|
|
1257
1422
|
}
|
|
1258
|
-
|
|
1259
|
-
if (
|
|
1260
|
-
protectController(agent, turn, step,
|
|
1423
|
+
stopResponsibilityScope(agent, "route-validation-failed", { step });
|
|
1424
|
+
if (scope?.decision && requiresFailClosedProtection(scope.decision)) {
|
|
1425
|
+
protectController(agent, turn, step, scope.decision, "route-validation", validation.failure.message);
|
|
1261
1426
|
}
|
|
1262
1427
|
roleRoute = undefined;
|
|
1263
1428
|
routeMode = "same-turn";
|
|
@@ -1303,13 +1468,18 @@ export function apply(ctx, rawConfig) {
|
|
|
1303
1468
|
|
|
1304
1469
|
const routeFallbackAttempts = new WeakMap();
|
|
1305
1470
|
ctx.on("agent/request-error", async ({ agent, turn, step, provider, failure, signal }, next) => {
|
|
1306
|
-
if (signal.aborted || failure?.code === "CONTEXT_WINDOW_EXCEEDED") return next();
|
|
1307
1471
|
const childRole = routedRoleOf(agent);
|
|
1308
|
-
const
|
|
1472
|
+
const scope = responsibilityScopes.get(agent);
|
|
1473
|
+
if (!childRole && scope && (signal.aborted || failure?.code === "CONTEXT_WINDOW_EXCEEDED")) {
|
|
1474
|
+
if (agent?.session) pendingRouteReceipts.delete(agent.session);
|
|
1475
|
+
stopResponsibilityScope(agent, signal.aborted ? "request-aborted" : "context-window-exceeded", { step });
|
|
1476
|
+
return next();
|
|
1477
|
+
}
|
|
1478
|
+
if (signal.aborted || failure?.code === "CONTEXT_WINDOW_EXCEEDED") return next();
|
|
1309
1479
|
const active = childRole
|
|
1310
1480
|
? { role: childRole, ...configuredRole(agent, childRole, turn) }
|
|
1311
|
-
:
|
|
1312
|
-
?
|
|
1481
|
+
: responsibilityScopeOwnsRequest(scope, turn, step)
|
|
1482
|
+
? scope
|
|
1313
1483
|
: undefined;
|
|
1314
1484
|
if (!active?.route || provider !== active.route.provider) return next();
|
|
1315
1485
|
|
|
@@ -1330,7 +1500,7 @@ export function apply(ctx, rawConfig) {
|
|
|
1330
1500
|
routeSource: active.source,
|
|
1331
1501
|
requestedRoute: active.route,
|
|
1332
1502
|
fallbackUsed: !childRole,
|
|
1333
|
-
fallbackRoute: childRole ? undefined : routeFromConfig(agent?.options),
|
|
1503
|
+
fallbackRoute: childRole ? undefined : active.baseRoute ?? routeFromConfig(agent?.options),
|
|
1334
1504
|
failureKind: classified.kind,
|
|
1335
1505
|
errorCode: classified.code,
|
|
1336
1506
|
error: classified.message,
|
|
@@ -1344,7 +1514,13 @@ export function apply(ctx, rawConfig) {
|
|
|
1344
1514
|
reason: "route-request-failed",
|
|
1345
1515
|
});
|
|
1346
1516
|
}
|
|
1347
|
-
if (childRole
|
|
1517
|
+
if (childRole) return next();
|
|
1518
|
+
if (agent?.session) pendingRouteReceipts.delete(agent.session);
|
|
1519
|
+
stopResponsibilityScope(agent, classified.kind === "cancelled" ? "request-cancelled" : "route-request-failed", { step });
|
|
1520
|
+
if (scope?.decision && requiresFailClosedProtection(scope.decision)) {
|
|
1521
|
+
protectController(agent, turn, step, scope.decision, "route-request-failure", classified.message);
|
|
1522
|
+
}
|
|
1523
|
+
if (classified.kind === "cancelled") return next();
|
|
1348
1524
|
|
|
1349
1525
|
let attempts = routeFallbackAttempts.get(agent);
|
|
1350
1526
|
if (!attempts) {
|
|
@@ -1354,14 +1530,10 @@ export function apply(ctx, rawConfig) {
|
|
|
1354
1530
|
const key = `${turn}:${step}`;
|
|
1355
1531
|
if (attempts.has(key)) return next();
|
|
1356
1532
|
attempts.add(key);
|
|
1357
|
-
controllerUpgrades.delete(agent);
|
|
1358
|
-
if (upgrade.decision && requiresFailClosedProtection(upgrade.decision)) {
|
|
1359
|
-
protectController(agent, turn, step, upgrade.decision, "route-request-failure", classified.message);
|
|
1360
|
-
}
|
|
1361
1533
|
return { kind: "retry" };
|
|
1362
1534
|
});
|
|
1363
1535
|
|
|
1364
|
-
const protectController = (agent, turn, step, decision, source, failure) => {
|
|
1536
|
+
const protectController = (agent, turn, step, decision, source, failure, scopeId) => {
|
|
1365
1537
|
const protection = Object.freeze({
|
|
1366
1538
|
turn,
|
|
1367
1539
|
step,
|
|
@@ -1369,12 +1541,59 @@ export function apply(ctx, rawConfig) {
|
|
|
1369
1541
|
reasonCode: decision.reasonCode,
|
|
1370
1542
|
source,
|
|
1371
1543
|
...(failure ? { failure } : {}),
|
|
1544
|
+
...(scopeId ? { scopeId } : {}),
|
|
1372
1545
|
});
|
|
1373
1546
|
routeProtections.set(agent, protection);
|
|
1374
1547
|
appendEvent(agent, "odai/route-protection", protection, logger);
|
|
1375
1548
|
};
|
|
1376
1549
|
|
|
1377
1550
|
ctx.on("session/event", (session, event) => {
|
|
1551
|
+
const owner = responsibilityScopeOwners.get(session);
|
|
1552
|
+
const activeScope = owner ? responsibilityScopes.get(owner) : undefined;
|
|
1553
|
+
const scopeStopReason = responsibilityScopeStopReason(activeScope, event);
|
|
1554
|
+
if (scopeStopReason) {
|
|
1555
|
+
stopResponsibilityScope(owner, scopeStopReason, {
|
|
1556
|
+
scopeId: activeScope.id,
|
|
1557
|
+
...(Number.isSafeInteger(event.data?.step) ? { step: event.data.step } : {}),
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
const pendingRestoration = pendingScopeRestorations.get(session);
|
|
1562
|
+
if (pendingRestoration && event?.type === "turn/end") {
|
|
1563
|
+
appendEvent(pendingRestoration.agent, "odai/responsibility-scope-restored", {
|
|
1564
|
+
scopeId: pendingRestoration.scopeId,
|
|
1565
|
+
turn: pendingRestoration.turn,
|
|
1566
|
+
step: pendingRestoration.step,
|
|
1567
|
+
role: pendingRestoration.role,
|
|
1568
|
+
status: "unverified",
|
|
1569
|
+
requestedRoute: pendingRestoration.expectedRoute,
|
|
1570
|
+
stopReason: "no-effective-request",
|
|
1571
|
+
});
|
|
1572
|
+
pendingScopeRestorations.delete(session);
|
|
1573
|
+
}
|
|
1574
|
+
if (pendingRestoration && ["request/header", "assistant/chunk"].includes(event?.type)) {
|
|
1575
|
+
let actualRoute;
|
|
1576
|
+
try {
|
|
1577
|
+
actualRoute = event.type === "request/header"
|
|
1578
|
+
? routeFromConfig(event.data?.header?.config)
|
|
1579
|
+
: routeFromConfig(session.requestHeader?.()?.config);
|
|
1580
|
+
} catch {}
|
|
1581
|
+
if (actualRoute || event.type === "assistant/chunk") {
|
|
1582
|
+
const mismatch = routeMismatchFor(pendingRestoration.expectedRoute, actualRoute, "base-route restoration");
|
|
1583
|
+
appendEvent(pendingRestoration.agent, "odai/responsibility-scope-restored", {
|
|
1584
|
+
scopeId: pendingRestoration.scopeId,
|
|
1585
|
+
turn: pendingRestoration.turn,
|
|
1586
|
+
step: pendingRestoration.step,
|
|
1587
|
+
role: pendingRestoration.role,
|
|
1588
|
+
status: mismatch ? "mismatch" : "applied",
|
|
1589
|
+
requestedRoute: pendingRestoration.expectedRoute,
|
|
1590
|
+
...(actualRoute ? { actualRoute } : {}),
|
|
1591
|
+
...(mismatch ? { error: mismatch } : {}),
|
|
1592
|
+
});
|
|
1593
|
+
pendingScopeRestorations.delete(session);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1378
1597
|
const pending = pendingRouteReceipts.get(session);
|
|
1379
1598
|
if (!pending) return;
|
|
1380
1599
|
if (event?.type === "turn/end") {
|
|
@@ -1397,6 +1616,10 @@ export function apply(ctx, rawConfig) {
|
|
|
1397
1616
|
reason: "no-effective-request",
|
|
1398
1617
|
});
|
|
1399
1618
|
}
|
|
1619
|
+
stopResponsibilityScope(pending.agent, "no-effective-request", {
|
|
1620
|
+
scopeId: pending.responsibilityScopeId,
|
|
1621
|
+
step: pending.step,
|
|
1622
|
+
});
|
|
1400
1623
|
pendingRouteReceipts.delete(session);
|
|
1401
1624
|
return;
|
|
1402
1625
|
}
|
|
@@ -1421,6 +1644,12 @@ export function apply(ctx, rawConfig) {
|
|
|
1421
1644
|
...(actualRoute ? { actualRoute } : {}),
|
|
1422
1645
|
...(mismatch ? { stopReason: "route-mismatch", error: mismatch } : {}),
|
|
1423
1646
|
});
|
|
1647
|
+
if (mismatch && pending.routeMode === "same-turn") {
|
|
1648
|
+
stopResponsibilityScope(pending.agent, "route-mismatch", {
|
|
1649
|
+
scopeId: pending.responsibilityScopeId,
|
|
1650
|
+
step: pending.step,
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1424
1653
|
if (pending.routeCardId) {
|
|
1425
1654
|
appendEvent(pending.agent, mismatch ? "odai/route-card-claim-released" : "odai/route-card-consumed", {
|
|
1426
1655
|
cardId: pending.routeCardId,
|
|
@@ -1443,6 +1672,7 @@ export function apply(ctx, rawConfig) {
|
|
|
1443
1672
|
});
|
|
1444
1673
|
|
|
1445
1674
|
ctx.on("agent/turn-stopping", ({ agent, turn }) => {
|
|
1675
|
+
stopResponsibilityScope(agent, "turn-stopping");
|
|
1446
1676
|
const role = routedRoleOf(agent);
|
|
1447
1677
|
if (!role) return;
|
|
1448
1678
|
const receipts = evidence.events(agent)
|
|
@@ -1461,15 +1691,22 @@ export function apply(ctx, rawConfig) {
|
|
|
1461
1691
|
const routedSteps = new WeakMap();
|
|
1462
1692
|
ctx.on("agent/pre-step", async ({ agent, turn, step, signal }, next) => {
|
|
1463
1693
|
const subagentSession = isSubagentSession(agent);
|
|
1464
|
-
if (
|
|
1465
|
-
|
|
1466
|
-
|
|
1694
|
+
if (!subagentSession) {
|
|
1695
|
+
if (step === 1) {
|
|
1696
|
+
stopResponsibilityScope(agent, "new-turn");
|
|
1697
|
+
routeProtections.delete(agent);
|
|
1698
|
+
}
|
|
1699
|
+
stopDanglingResponsibilityScope(agent, "runtime-resume");
|
|
1467
1700
|
}
|
|
1468
1701
|
let downstream = await next();
|
|
1469
|
-
if (downstream.kind === "reject" || signal.aborted
|
|
1470
|
-
|
|
1471
|
-
}
|
|
1472
|
-
const
|
|
1702
|
+
if (downstream.kind === "reject" || signal.aborted) return downstream;
|
|
1703
|
+
const responsibilityGap = subagentSession ? undefined : pendingResponsibilityGap(agent, turn, step);
|
|
1704
|
+
const directMessage = latestDirectUserMessage(agent, downstream.messages, { turn });
|
|
1705
|
+
const activation = contextActivationFor(agent, directMessage ? extractLatestUserText([directMessage]) : "", turn);
|
|
1706
|
+
const routeCardNeeded = !subagentSession && (Boolean(activeRouteCard(evidence.events(agent)))
|
|
1707
|
+
|| ["planner", "executor"].includes(responsibilityGap?.responsibility));
|
|
1708
|
+
syncToolExposure(agent, activation, { turn, step, routeCard: routeCardNeeded });
|
|
1709
|
+
if (subagentSession) return downstream;
|
|
1473
1710
|
if (step !== 1 && !responsibilityGap) return downstream;
|
|
1474
1711
|
|
|
1475
1712
|
if (step === 1 && claimSemanticMemoryTurn(agent, turn, step)) {
|
|
@@ -1653,17 +1890,19 @@ export function apply(ctx, rawConfig) {
|
|
|
1653
1890
|
let roleContext = decision.action === "direct"
|
|
1654
1891
|
? undefined
|
|
1655
1892
|
: buildRoleContextPacket(agent, routeRole, roleTaskText);
|
|
1893
|
+
let localReviewerCoverage;
|
|
1656
1894
|
if (config.routing.mode === "auto"
|
|
1657
1895
|
&& routeRole === "reviewer"
|
|
1658
1896
|
&& decision.action === "delegate"
|
|
1659
1897
|
&& !roleContext.sufficient) {
|
|
1898
|
+
localReviewerCoverage = roleContext.coverage;
|
|
1660
1899
|
decision = Object.freeze({
|
|
1661
1900
|
...decision,
|
|
1662
1901
|
role: "controller",
|
|
1663
|
-
mode: "
|
|
1664
|
-
action: "
|
|
1902
|
+
mode: "direct",
|
|
1903
|
+
action: "direct",
|
|
1665
1904
|
targetRole: "reviewer",
|
|
1666
|
-
signals: Object.freeze([...decision.signals, "review-evidence-packet-missing"]),
|
|
1905
|
+
signals: Object.freeze([...decision.signals, "review-evidence-packet-missing", "controller-local-review"]),
|
|
1667
1906
|
});
|
|
1668
1907
|
routeRole = "reviewer";
|
|
1669
1908
|
}
|
|
@@ -1695,7 +1934,46 @@ export function apply(ctx, rawConfig) {
|
|
|
1695
1934
|
}, logger);
|
|
1696
1935
|
}
|
|
1697
1936
|
|
|
1698
|
-
if (decision.action === "direct")
|
|
1937
|
+
if (decision.action === "direct") {
|
|
1938
|
+
if (!localReviewerCoverage) return routedDownstream;
|
|
1939
|
+
appendEvent(agent, "odai/route-context", {
|
|
1940
|
+
turn,
|
|
1941
|
+
step,
|
|
1942
|
+
role: "reviewer",
|
|
1943
|
+
mode: "controller-local",
|
|
1944
|
+
digest: roleContext.digest,
|
|
1945
|
+
evidenceCount: roleContext.evidenceCount,
|
|
1946
|
+
toolEvidenceCount: roleContext.toolEvidenceCount,
|
|
1947
|
+
acceptanceCount: localReviewerCoverage.acceptanceCount,
|
|
1948
|
+
diffCount: localReviewerCoverage.diffCount,
|
|
1949
|
+
testCount: localReviewerCoverage.testCount,
|
|
1950
|
+
truncated: roleContext.truncated,
|
|
1951
|
+
sufficient: false,
|
|
1952
|
+
}, logger);
|
|
1953
|
+
appendEvent(agent, "odai/route-result", {
|
|
1954
|
+
turn,
|
|
1955
|
+
step,
|
|
1956
|
+
role: "reviewer",
|
|
1957
|
+
action: "direct",
|
|
1958
|
+
status: "fallback",
|
|
1959
|
+
stopReason: "evidence-packet-missing",
|
|
1960
|
+
independent: false,
|
|
1961
|
+
}, logger);
|
|
1962
|
+
return {
|
|
1963
|
+
kind: "enter",
|
|
1964
|
+
messages: [
|
|
1965
|
+
...routedDownstream.messages,
|
|
1966
|
+
pluginMessage(
|
|
1967
|
+
[
|
|
1968
|
+
`An independent reviewer was not started because the bounded packet is incomplete (${JSON.stringify(localReviewerCoverage)}).`,
|
|
1969
|
+
"Remain on the current controller route and continue the authorized task. Gather project-available requirements, acceptance conditions, diff, tests, and matching tool evidence before resubmitting a changed reviewer gap.",
|
|
1970
|
+
"A controller-local read-only check may guide the work, but it is not independent acceptance. Do not stop solely to ask the user for review artifacts the project can produce, and do not claim the reviewer approved release.",
|
|
1971
|
+
].join("\n"),
|
|
1972
|
+
"odai reviewer evidence is incomplete; controller continues locally",
|
|
1973
|
+
),
|
|
1974
|
+
],
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1699
1977
|
|
|
1700
1978
|
if (config.routing.mode === "observe") {
|
|
1701
1979
|
if (requiresFailClosedProtection(decision)) {
|
|
@@ -1842,27 +2120,35 @@ export function apply(ctx, rawConfig) {
|
|
|
1842
2120
|
}
|
|
1843
2121
|
|
|
1844
2122
|
if (inPlaceUpgrade) {
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
}
|
|
1848
|
-
if (routeRole === "executor" && frozenCard) {
|
|
1849
|
-
appendEvent(agent, "odai/route-card-claimed", { cardId: frozenCard.id, turn, step });
|
|
1850
|
-
}
|
|
1851
|
-
controllerUpgrades.set(agent, Object.freeze({
|
|
2123
|
+
stopResponsibilityScope(agent, "superseded", { step });
|
|
2124
|
+
const responsibilityScope = createResponsibilityScope({
|
|
1852
2125
|
turn,
|
|
2126
|
+
startStep: step,
|
|
1853
2127
|
role: routeRole,
|
|
1854
2128
|
route: roleRoute,
|
|
1855
2129
|
source: roleState.source,
|
|
1856
2130
|
decision,
|
|
1857
|
-
|
|
2131
|
+
routeValidated: rolePreflightVerified,
|
|
1858
2132
|
...(routeRole === "executor" && frozenCard ? { cardId: frozenCard.id } : {}),
|
|
1859
|
-
})
|
|
2133
|
+
});
|
|
2134
|
+
responsibilityScopes.set(agent, responsibilityScope);
|
|
2135
|
+
if (agent?.session) responsibilityScopeOwners.set(agent.session, agent);
|
|
2136
|
+
appendEvent(agent, "odai/responsibility-scope-started", responsibilityScopeStartedEvent(responsibilityScope));
|
|
2137
|
+
if (["planner", "reviewer"].includes(routeRole)) {
|
|
2138
|
+
protectController(agent, turn, step, decision, `responsibility-scope-${routeRole}`, undefined, responsibilityScope.id);
|
|
2139
|
+
}
|
|
2140
|
+
if (routeRole === "executor" && frozenCard) {
|
|
2141
|
+
appendEvent(agent, "odai/route-card-claimed", { cardId: frozenCard.id, turn, step });
|
|
2142
|
+
}
|
|
1860
2143
|
appendEvent(agent, "odai/route-upgrade", {
|
|
1861
2144
|
turn,
|
|
1862
2145
|
step,
|
|
1863
2146
|
role: decision.role,
|
|
1864
2147
|
targetRole: routeRole,
|
|
1865
2148
|
status: "requested",
|
|
2149
|
+
responsibilityScopeId: responsibilityScope.id,
|
|
2150
|
+
continuationPolicy: responsibilityScope.continuationPolicy,
|
|
2151
|
+
stopPolicy: responsibilityScope.stopPolicy,
|
|
1866
2152
|
routeSource: roleState.source,
|
|
1867
2153
|
requestedRoute: roleRoute,
|
|
1868
2154
|
contextDigest: roleContext.digest,
|
|
@@ -3,12 +3,10 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { CONFIGURABLE_ROLES } from "./routing-config.mjs";
|
|
4
4
|
|
|
5
5
|
export const RESPONSIBILITY_GAP_PROMPT = [
|
|
6
|
-
"## Odai
|
|
7
|
-
"Users
|
|
8
|
-
"Keep work direct when the
|
|
9
|
-
"
|
|
10
|
-
"Use responsibility=user only when a missing user-owned choice, priority, or unacceptable outcome changes the route. Ask one concise question after the tool accepts that gap. Do not ask users for repository facts, technical investigation, implementation details the project already determines, or internal routing choices.",
|
|
11
|
-
"Do not submit a gap merely because a task is complex, risky, multi-step, uses a role word, has a configured model, or could use a cheaper model. Do not resubmit an unchanged gap or state.",
|
|
6
|
+
"## Odai responsibility gaps",
|
|
7
|
+
"Users provide goals, constraints, materials, and acceptance; they never need to request internal roles or handoffs.",
|
|
8
|
+
"Keep work direct when the controller can close it reliably. Call odai_responsibility_gap only when current evidence shows an independent capability or user-decision gap that can change a concrete result; keywords, complexity, risk, configured models, and price are insufficient. The runtime decides direct, inline, same-turn, child, or user-question handling.",
|
|
9
|
+
"Use responsibility=user only for a missing user-owned choice, priority, or unacceptable outcome, then ask exactly the accepted concise question. Do not ask users for repository facts or implementation details the project can determine. Do not resubmit unchanged state.",
|
|
12
10
|
].join("\n");
|
|
13
11
|
|
|
14
12
|
const RESPONSIBILITIES = Object.freeze([...CONFIGURABLE_ROLES, "user"]);
|
|
@@ -58,10 +56,10 @@ export function createResponsibilityGapTool(options = {}) {
|
|
|
58
56
|
required: ["responsibility", "gap", "evidenceRefs", "expectedChange"],
|
|
59
57
|
properties: {
|
|
60
58
|
responsibility: { type: "string", enum: [...RESPONSIBILITIES] },
|
|
61
|
-
gap: { type: "string"
|
|
62
|
-
evidenceRefs: { type: "array", items: { type: "string" }
|
|
63
|
-
expectedChange: { type: "string"
|
|
64
|
-
question: { type: "string"
|
|
59
|
+
gap: { type: "string" },
|
|
60
|
+
evidenceRefs: { type: "array", items: { type: "string" } },
|
|
61
|
+
expectedChange: { type: "string" },
|
|
62
|
+
question: { type: "string" },
|
|
65
63
|
},
|
|
66
64
|
},
|
|
67
65
|
output: {
|