АРЧ АП11 - Архитектура: протянуть continuity snapshot в living-chat organization authority и добавить оценку риска правки в AGENTS
This commit is contained in:
parent
21738f8662
commit
d8ce2b7d88
|
|
@ -4,6 +4,13 @@
|
||||||
## commit_message_rule
|
## commit_message_rule
|
||||||
- After applying fixes, always provide the user with a ready commit title in Russian.
|
- After applying fixes, always provide the user with a ready commit title in Russian.
|
||||||
|
|
||||||
|
## change_risk_rule
|
||||||
|
- After applying fixes, always provide `Степень опасности правки: X/10` immediately above the ready commit title.
|
||||||
|
- The score must use an integer scale from `1` to `10`, where:
|
||||||
|
- `1` = low-risk local change with narrow blast radius;
|
||||||
|
- `10` = high-risk architecture/runtime change with broad blast radius and mandatory close validation.
|
||||||
|
- The score must reflect real project risk, not optimism, and should help the user decide how much manual attention and replay validation the change deserves.
|
||||||
|
|
||||||
## graphify
|
## graphify
|
||||||
|
|
||||||
This project has a graphify knowledge graph at graphify-out/.
|
This project has a graphify knowledge graph at graphify-out/.
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,10 @@ Latest continuity-authority convergence evidence after the current route pass:
|
||||||
- active organization continuity is now allowed to participate in organization-selection arbitration, instead of forcing route policy to reconstruct that context only from immediate clarification payloads;
|
- active organization continuity is now allowed to participate in organization-selection arbitration, instead of forcing route policy to reconstruct that context only from immediate clarification payloads;
|
||||||
- a bare organization-selection turn after grounded bookkeeping continuity is no longer automatically classified as `non_domain_query_indexed` noise when the session still carries valid grounded business context;
|
- a bare organization-selection turn after grounded bookkeeping continuity is no longer automatically classified as `non_domain_query_indexed` noise when the session still carries valid grounded business context;
|
||||||
- session organization recovery inside the data-scope layer now has a final fallback to the same continuity snapshot, reducing one more duplicate path that used to rescan assistant history independently;
|
- session organization recovery inside the data-scope layer now has a final fallback to the same continuity snapshot, reducing one more duplicate path that used to rescan assistant history independently;
|
||||||
|
- the living-chat runtime now also consumes continuity-backed organization authority:
|
||||||
|
- deterministic organization-fact boundary replies can now trigger from grounded continuity even when `sessionScope.selectedOrganization` and `sessionScope.activeOrganization` are both empty at runtime entry;
|
||||||
|
- the chat layer now records whether it entered with grounded continuity and which organization came from that continuity snapshot, making future saved-session review less blind;
|
||||||
|
- proactive organization offer logic is now explicitly blocked when grounded address continuity already exists, so the chat layer does not re-offer company selection on top of an already grounded business session;
|
||||||
- this pass does not yet finish full single-owner continuity, but it narrows one of the remaining seams where route arbitration and scope memory could disagree about whether the session was still grounded.
|
- this pass does not yet finish full single-owner continuity, but it narrows one of the remaining seams where route arbitration and scope memory could disagree about whether the session was still grounded.
|
||||||
|
|
||||||
## Next Execution Slice (2026-04-18)
|
## Next Execution Slice (2026-04-18)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.runAssistantLivingChatRuntime = runAssistantLivingChatRuntime;
|
exports.runAssistantLivingChatRuntime = runAssistantLivingChatRuntime;
|
||||||
const assistantMemoryRecapPolicy_1 = require("./assistantMemoryRecapPolicy");
|
const assistantMemoryRecapPolicy_1 = require("./assistantMemoryRecapPolicy");
|
||||||
|
const assistantContinuityPolicy_1 = require("./assistantContinuityPolicy");
|
||||||
function formatIsoDateForReply(value) {
|
function formatIsoDateForReply(value) {
|
||||||
const source = String(value ?? "").trim();
|
const source = String(value ?? "").trim();
|
||||||
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
const match = source.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
|
@ -151,6 +152,10 @@ function buildAddressMemoryRecapReply(input) {
|
||||||
}
|
}
|
||||||
async function runAssistantLivingChatRuntime(input) {
|
async function runAssistantLivingChatRuntime(input) {
|
||||||
const userMessage = String(input.userMessage ?? "");
|
const userMessage = String(input.userMessage ?? "");
|
||||||
|
const continuitySnapshot = (0, assistantContinuityPolicy_1.resolveAssistantContinuitySnapshot)({
|
||||||
|
sessionItems: input.sessionItems,
|
||||||
|
toNonEmptyString: input.toNonEmptyString
|
||||||
|
});
|
||||||
const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage);
|
const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage);
|
||||||
const capabilityMetaQuery = input.shouldHandleAsAssistantCapabilityMetaQuery(userMessage);
|
const capabilityMetaQuery = input.shouldHandleAsAssistantCapabilityMetaQuery(userMessage);
|
||||||
const destructiveSignal = input.hasDestructiveDataActionSignal(userMessage);
|
const destructiveSignal = input.hasDestructiveDataActionSignal(userMessage);
|
||||||
|
|
@ -164,9 +169,13 @@ async function runAssistantLivingChatRuntime(input) {
|
||||||
let livingChatGroundingGuardApplied = false;
|
let livingChatGroundingGuardApplied = false;
|
||||||
let livingChatGroundingGuardReason = null;
|
let livingChatGroundingGuardReason = null;
|
||||||
let livingChatProactiveScopeOfferApplied = false;
|
let livingChatProactiveScopeOfferApplied = false;
|
||||||
let knownOrganizations = input.mergeKnownOrganizations(input.sessionScope.knownOrganizations ?? []);
|
const continuityActiveOrganization = input.toNonEmptyString(continuitySnapshot.activeOrganization);
|
||||||
|
let knownOrganizations = input.mergeKnownOrganizations([
|
||||||
|
...(Array.isArray(input.sessionScope.knownOrganizations) ? input.sessionScope.knownOrganizations : []),
|
||||||
|
continuityActiveOrganization
|
||||||
|
]);
|
||||||
let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization);
|
let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization);
|
||||||
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization);
|
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization) ?? continuityActiveOrganization;
|
||||||
const memoryRecapContext = (0, assistantMemoryRecapPolicy_1.resolveAssistantLivingChatMemoryContext)({
|
const memoryRecapContext = (0, assistantMemoryRecapPolicy_1.resolveAssistantLivingChatMemoryContext)({
|
||||||
modeDecisionReason: input.modeDecision?.reason ?? null,
|
modeDecisionReason: input.modeDecision?.reason ?? null,
|
||||||
sessionItems: input.sessionItems
|
sessionItems: input.sessionItems
|
||||||
|
|
@ -265,6 +274,7 @@ async function runAssistantLivingChatRuntime(input) {
|
||||||
}
|
}
|
||||||
const shouldOfferProactiveOrganizationScope = !selectedOrganization &&
|
const shouldOfferProactiveOrganizationScope = !selectedOrganization &&
|
||||||
!activeOrganization &&
|
!activeOrganization &&
|
||||||
|
!continuitySnapshot.hasGroundedAddressContext &&
|
||||||
!hasPriorAssistantTurn(input.sessionItems) &&
|
!hasPriorAssistantTurn(input.sessionItems) &&
|
||||||
input.modeDecision?.mode === "chat" &&
|
input.modeDecision?.mode === "chat" &&
|
||||||
input.hasLivingChatSignal(userMessage);
|
input.hasLivingChatSignal(userMessage);
|
||||||
|
|
@ -330,6 +340,8 @@ async function runAssistantLivingChatRuntime(input) {
|
||||||
? input.mergeKnownOrganizations(dataScopeProbe.organizations)
|
? input.mergeKnownOrganizations(dataScopeProbe.organizations)
|
||||||
: [],
|
: [],
|
||||||
living_chat_data_scope_probe_error: dataScopeProbe?.error ?? null,
|
living_chat_data_scope_probe_error: dataScopeProbe?.error ?? null,
|
||||||
|
living_chat_continuity_grounded_context_detected: continuitySnapshot.hasGroundedAddressContext,
|
||||||
|
living_chat_continuity_active_organization: continuityActiveOrganization,
|
||||||
living_chat_selected_organization: selectedOrganization ?? null,
|
living_chat_selected_organization: selectedOrganization ?? null,
|
||||||
assistant_known_organizations: knownOrganizations,
|
assistant_known_organizations: knownOrganizations,
|
||||||
assistant_active_organization: activeOrganization ?? null,
|
assistant_active_organization: activeOrganization ?? null,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
buildInventoryHistoryCapabilityFollowupReply as buildInventoryHistoryCapabilityFollowupReplyFromPolicy,
|
buildInventoryHistoryCapabilityFollowupReply as buildInventoryHistoryCapabilityFollowupReplyFromPolicy,
|
||||||
resolveAssistantLivingChatMemoryContext
|
resolveAssistantLivingChatMemoryContext
|
||||||
} from "./assistantMemoryRecapPolicy";
|
} from "./assistantMemoryRecapPolicy";
|
||||||
|
import { resolveAssistantContinuitySnapshot } from "./assistantContinuityPolicy";
|
||||||
|
|
||||||
export interface AssistantLivingChatSessionScopeInput {
|
export interface AssistantLivingChatSessionScopeInput {
|
||||||
knownOrganizations?: unknown[];
|
knownOrganizations?: unknown[];
|
||||||
|
|
@ -248,6 +249,10 @@ export async function runAssistantLivingChatRuntime(
|
||||||
input: AssistantLivingChatRuntimeInput
|
input: AssistantLivingChatRuntimeInput
|
||||||
): Promise<AssistantLivingChatRuntimeOutput> {
|
): Promise<AssistantLivingChatRuntimeOutput> {
|
||||||
const userMessage = String(input.userMessage ?? "");
|
const userMessage = String(input.userMessage ?? "");
|
||||||
|
const continuitySnapshot = resolveAssistantContinuitySnapshot({
|
||||||
|
sessionItems: input.sessionItems,
|
||||||
|
toNonEmptyString: input.toNonEmptyString
|
||||||
|
});
|
||||||
const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage);
|
const dataScopeMetaQuery = input.hasAssistantDataScopeMetaQuestionSignal(userMessage);
|
||||||
const capabilityMetaQuery = input.shouldHandleAsAssistantCapabilityMetaQuery(userMessage);
|
const capabilityMetaQuery = input.shouldHandleAsAssistantCapabilityMetaQuery(userMessage);
|
||||||
const destructiveSignal = input.hasDestructiveDataActionSignal(userMessage);
|
const destructiveSignal = input.hasDestructiveDataActionSignal(userMessage);
|
||||||
|
|
@ -262,9 +267,13 @@ export async function runAssistantLivingChatRuntime(
|
||||||
let livingChatGroundingGuardApplied = false;
|
let livingChatGroundingGuardApplied = false;
|
||||||
let livingChatGroundingGuardReason: string | null = null;
|
let livingChatGroundingGuardReason: string | null = null;
|
||||||
let livingChatProactiveScopeOfferApplied = false;
|
let livingChatProactiveScopeOfferApplied = false;
|
||||||
let knownOrganizations = input.mergeKnownOrganizations(input.sessionScope.knownOrganizations ?? []);
|
const continuityActiveOrganization = input.toNonEmptyString(continuitySnapshot.activeOrganization);
|
||||||
|
let knownOrganizations = input.mergeKnownOrganizations([
|
||||||
|
...(Array.isArray(input.sessionScope.knownOrganizations) ? input.sessionScope.knownOrganizations : []),
|
||||||
|
continuityActiveOrganization
|
||||||
|
]);
|
||||||
let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization);
|
let selectedOrganization = input.toNonEmptyString(input.sessionScope.selectedOrganization);
|
||||||
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization);
|
let activeOrganization = input.toNonEmptyString(input.sessionScope.activeOrganization) ?? continuityActiveOrganization;
|
||||||
const memoryRecapContext = resolveAssistantLivingChatMemoryContext({
|
const memoryRecapContext = resolveAssistantLivingChatMemoryContext({
|
||||||
modeDecisionReason: input.modeDecision?.reason ?? null,
|
modeDecisionReason: input.modeDecision?.reason ?? null,
|
||||||
sessionItems: input.sessionItems
|
sessionItems: input.sessionItems
|
||||||
|
|
@ -362,6 +371,7 @@ export async function runAssistantLivingChatRuntime(
|
||||||
const shouldOfferProactiveOrganizationScope =
|
const shouldOfferProactiveOrganizationScope =
|
||||||
!selectedOrganization &&
|
!selectedOrganization &&
|
||||||
!activeOrganization &&
|
!activeOrganization &&
|
||||||
|
!continuitySnapshot.hasGroundedAddressContext &&
|
||||||
!hasPriorAssistantTurn(input.sessionItems) &&
|
!hasPriorAssistantTurn(input.sessionItems) &&
|
||||||
input.modeDecision?.mode === "chat" &&
|
input.modeDecision?.mode === "chat" &&
|
||||||
input.hasLivingChatSignal(userMessage);
|
input.hasLivingChatSignal(userMessage);
|
||||||
|
|
@ -432,6 +442,8 @@ export async function runAssistantLivingChatRuntime(
|
||||||
? input.mergeKnownOrganizations(dataScopeProbe.organizations as unknown[])
|
? input.mergeKnownOrganizations(dataScopeProbe.organizations as unknown[])
|
||||||
: [],
|
: [],
|
||||||
living_chat_data_scope_probe_error: dataScopeProbe?.error ?? null,
|
living_chat_data_scope_probe_error: dataScopeProbe?.error ?? null,
|
||||||
|
living_chat_continuity_grounded_context_detected: continuitySnapshot.hasGroundedAddressContext,
|
||||||
|
living_chat_continuity_active_organization: continuityActiveOrganization,
|
||||||
living_chat_selected_organization: selectedOrganization ?? null,
|
living_chat_selected_organization: selectedOrganization ?? null,
|
||||||
assistant_known_organizations: knownOrganizations,
|
assistant_known_organizations: knownOrganizations,
|
||||||
assistant_active_organization: activeOrganization ?? null,
|
assistant_active_organization: activeOrganization ?? null,
|
||||||
|
|
|
||||||
|
|
@ -219,4 +219,39 @@ describe("assistant living chat runtime adapter", () => {
|
||||||
expect(output.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract");
|
expect(output.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract");
|
||||||
expect(executeLlmChat).not.toHaveBeenCalled();
|
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
it("uses continuity-backed active organization for organization-fact boundary even when session scope is empty", async () => {
|
||||||
|
const executeLlmChat = vi.fn(async () => "raw-llm");
|
||||||
|
const input = buildRuntimeInput({
|
||||||
|
userMessage: "сколько лет альтернативе плюс",
|
||||||
|
sessionItems: [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
debug: {
|
||||||
|
execution_lane: "address_query",
|
||||||
|
answer_grounding_check: {
|
||||||
|
status: "grounded"
|
||||||
|
},
|
||||||
|
detected_intent: "receivables_confirmed_as_of_date",
|
||||||
|
extracted_filters: {
|
||||||
|
organization: "ООО Альтернатива Плюс",
|
||||||
|
as_of_date: "2020-03-31"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
hasOrganizationFactLookupSignal: () => true,
|
||||||
|
buildAssistantOrganizationFactBoundaryReply: (organization: string | null) => `org-boundary:${organization ?? "none"}`,
|
||||||
|
executeLlmChat
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = await runAssistantLivingChatRuntime(input);
|
||||||
|
|
||||||
|
expect(output.handled).toBe(true);
|
||||||
|
expect(output.chatText).toBe("org-boundary:ООО Альтернатива Плюс");
|
||||||
|
expect(output.debug?.living_chat_response_source).toBe("deterministic_organization_fact_boundary");
|
||||||
|
expect(output.debug?.assistant_active_organization).toBe("ООО Альтернатива Плюс");
|
||||||
|
expect(output.debug?.living_chat_continuity_grounded_context_detected).toBe(true);
|
||||||
|
expect(output.debug?.living_chat_continuity_active_organization).toBe("ООО Альтернатива Плюс");
|
||||||
|
expect(executeLlmChat).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
{
|
||||||
|
"suite_id": "assistant_saved_session_runtime_job-knjH7nTx3E",
|
||||||
|
"suite_version": "0.1.0",
|
||||||
|
"schema_version": "assistant_saved_session_runtime_v0_1",
|
||||||
|
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
|
||||||
|
"scenario_count": 1,
|
||||||
|
"case_ids": [
|
||||||
|
"SAVED-001"
|
||||||
|
],
|
||||||
|
"cases": [
|
||||||
|
{
|
||||||
|
"case_id": "SAVED-001",
|
||||||
|
"scenario_tag": "saved_user_sessions_runtime",
|
||||||
|
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
|
||||||
|
"question_type": "followup",
|
||||||
|
"broadness_level": "medium",
|
||||||
|
"turns": [
|
||||||
|
{
|
||||||
|
"user_message": "приветик - че как там дела"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "расскажи что можешь интересного"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "кайф - что там на складе по остаткам?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а исторические остатки на другие даты умеешь?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "давай на июль 2017"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "март 2016"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а кому продали?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "кто у нас самый доходный клиент за все время"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "кто нам должен денег на май 2017"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "мы должны комуто денег на сегодня?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а нам?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "какой у нас самый доходный год"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а за 2017 мы скок заработали?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "сколько вообще денег мы заработали за все время?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "ты умеешь считать дельту по договорам?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "по чепурнову покажи все доки"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а по свк"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а сейчас у нас есть что на складе?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "какие остатки на складе на сегодня"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "остатки на март 2016"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "хвосты покажи по счету 60 на август 2022"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "Есть ли остатки товара, которые закупались очень давно"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue