АРЧ АП11 - Включить response guard для capability binding ассистента
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user