АРЧ АП11 - Включить response guard для capability binding ассистента

This commit is contained in:
2026-04-15 23:47:26 +03:00
parent 1c928a5d68
commit 918feaf06e
7 changed files with 384 additions and 9 deletions
@@ -88,6 +88,10 @@ describe("assistant address lane response runtime adapter", () => {
}),
capability_binding_contract: expect.objectContaining({
schema_version: "assistant_capability_runtime_binding_v1"
}),
capability_binding_response_guard: expect.objectContaining({
schema_version: "assistant_capability_binding_response_guard_v1",
applied: false
})
})
);
@@ -155,9 +159,67 @@ describe("assistant address lane response runtime adapter", () => {
effective_carryover_depth: "none",
capability_binding_status: "not_applicable",
capability_binding_action: "observe_only",
capability_binding_violations: []
capability_binding_violations: [],
capability_binding_response_guard: expect.objectContaining({
applied: false,
action: "observe_only"
})
})
);
expect(runtime.response).toEqual({ ok: true });
});
it("guards blocked capability binding responses before finalizing the turn", () => {
const finalizeAddressTurn = vi.fn(() => ({
response: {
ok: true
}
}));
runAssistantAddressLaneResponseRuntime({
sessionId: "asst-3",
userMessage: "кто поставщик?",
effectiveAddressUserMessage: "кто поставщик?",
addressLane: {
handled: true,
reply_text: "unsafe factual answer",
reply_type: "factual",
debug: {
capability_id: "inventory_inventory_purchase_provenance_for_item",
limited_reason_category: "missing_anchor",
missing_required_filters: ["item"]
}
},
knownOrganizations: [],
activeOrganization: null,
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
buildAddressDebugPayload: (addressDebug) => ({ ...(addressDebug as Record<string, unknown>) }),
buildAddressFollowupOffer: () => null,
mergeKnownOrganizations: (items) => items,
toNonEmptyString: (value) => (typeof value === "string" && value.trim() ? value.trim() : null),
appendItem: () => {},
getSession: () => ({ session_id: "asst-3", updated_at: "", items: [], investigation_state: null } as any),
persistSession: () => {},
cloneConversation: (items) => items,
logEvent: () => {},
messageIdFactory: () => "msg-3",
finalizeAddressTurn
});
expect(finalizeAddressTurn).toHaveBeenCalledWith(
expect.objectContaining({
assistantReply: expect.stringContaining("Нужно уточнение"),
replyType: "partial_coverage",
debug: expect.objectContaining({
capability_binding_status: "blocked",
capability_binding_action: "clarify",
capability_binding_response_guard: expect.objectContaining({
applied: true,
action: "clarify",
guarded_reply_type: "partial_coverage"
})
})
})
);
});
});
@@ -0,0 +1,104 @@
import { describe, expect, it } from "vitest";
import { applyAssistantCapabilityBindingResponseGuard } from "../src/services/assistantCapabilityBindingResponseGuard";
import type { AssistantCapabilityRuntimeBindingContract } from "../src/types/assistantRuntimeContracts";
function binding(overrides: Partial<AssistantCapabilityRuntimeBindingContract>): AssistantCapabilityRuntimeBindingContract {
return {
schema_version: "assistant_capability_runtime_binding_v1",
binding_owner: "assistantCapabilityRuntimeBindingAdapter",
capability_id: "inventory_inventory_purchase_provenance_for_item",
capability_contract_id: "inventory_inventory_purchase_provenance_for_item",
binding_status: "bound",
binding_action: "allow",
runtime_lane_expected: "address_exact",
runtime_lane_observed: "address_exact",
execution_adapter: "AddressQueryService",
transition_id: "T4",
transition_allowed: true,
required_anchors: ["item"],
provided_anchors: ["item"],
missing_anchors: [],
requires_focus_object: true,
focus_object_binding_status: "inferred_from_anchor",
result_shape: "supplier_purchase_provenance_trace",
answer_object_shape: "inventory_provenance_bundle",
truth_gate_behavior: "partial_or_blocked_if_evidence_insufficient",
truth_fallback_allowed: true,
answer_shape_compatible: true,
violations: [],
reason_codes: ["binding_status_bound"],
...overrides
};
}
describe("assistant capability binding response guard", () => {
it("leaves allowed answers unchanged", () => {
const output = applyAssistantCapabilityBindingResponseGuard({
assistantReply: "answer",
replyType: "factual",
capabilityBinding: binding({})
});
expect(output.assistantReply).toBe("answer");
expect(output.replyType).toBe("factual");
expect(output.audit.applied).toBe(false);
expect(output.audit.guarded_reply_type).toBe("factual");
});
it("turns missing required anchors into clarification replies", () => {
const output = applyAssistantCapabilityBindingResponseGuard({
assistantReply: "unsafe answer",
replyType: "factual",
capabilityBinding: binding({
binding_status: "blocked",
binding_action: "clarify",
missing_anchors: ["item"],
focus_object_binding_status: "missing",
violations: ["required_anchor_missing", "focus_object_required_but_unbound"],
reason_codes: ["required_anchor_missing"]
})
});
expect(output.replyType).toBe("partial_coverage");
expect(output.assistantReply).toContain("Нужно уточнение");
expect(output.assistantReply).toContain("item");
expect(output.audit.applied).toBe(true);
expect(output.audit.reason_codes).toContain("capability_binding_guard_clarification_reply");
});
it("turns blocked incompatible transitions into bounded replies", () => {
const output = applyAssistantCapabilityBindingResponseGuard({
assistantReply: "unsafe answer",
replyType: "factual",
capabilityBinding: binding({
binding_status: "blocked",
binding_action: "block",
violations: ["transition_not_supported_by_capability"],
reason_codes: ["transition_not_supported_by_capability"]
})
});
expect(output.replyType).toBe("partial_coverage");
expect(output.assistantReply).toContain("Не могу надежно подтвердить");
expect(output.assistantReply).toContain("transition_not_supported_by_capability");
expect(output.audit.applied).toBe(true);
expect(output.audit.reason_codes).toContain("capability_binding_guard_blocked_reply");
});
it("downgrades factual reply type for limited bound answers without rewriting text", () => {
const output = applyAssistantCapabilityBindingResponseGuard({
assistantReply: "limited answer",
replyType: "factual",
capabilityBinding: binding({
binding_status: "bound_with_limits",
binding_action: "limit",
reason_codes: ["truth_mode_limited"]
})
});
expect(output.assistantReply).toBe("limited answer");
expect(output.replyType).toBe("partial_coverage");
expect(output.audit.applied).toBe(true);
expect(output.audit.reason_codes).toContain("capability_binding_guard_limited_reply_type");
});
});