@indexnetwork/protocol 5.4.0-rc.374.1 → 6.1.0-rc.376.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/CHANGELOG.md +1 -0
- package/dist/index.d.ts +50 -121
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -55
- package/dist/index.js.map +1 -1
- package/dist/intent/intent.graph.d.ts +19 -19
- package/dist/intent/intent.indexer.d.ts +3 -3
- package/dist/intent/intent.inferrer.d.ts +8 -8
- package/dist/intent/intent.reconciler.d.ts +11 -11
- package/dist/intent/intent.state.d.ts +4 -4
- package/dist/intent/intent.verifier.d.ts +6 -6
- package/dist/negotiation/negotiation.agent.d.ts +10 -0
- package/dist/negotiation/negotiation.agent.d.ts.map +1 -1
- package/dist/negotiation/negotiation.agent.js +11 -1
- package/dist/negotiation/negotiation.agent.js.map +1 -1
- package/dist/negotiation/negotiation.deadlock.d.ts +96 -0
- package/dist/negotiation/negotiation.deadlock.d.ts.map +1 -0
- package/dist/negotiation/negotiation.deadlock.js +103 -0
- package/dist/negotiation/negotiation.deadlock.js.map +1 -0
- package/dist/negotiation/negotiation.graph.d.ts +7 -1
- package/dist/negotiation/negotiation.graph.d.ts.map +1 -1
- package/dist/negotiation/negotiation.graph.js +67 -0
- package/dist/negotiation/negotiation.graph.js.map +1 -1
- package/dist/negotiation/negotiation.state.d.ts +9 -0
- package/dist/negotiation/negotiation.state.d.ts.map +1 -1
- package/dist/negotiation/negotiation.state.js +11 -0
- package/dist/negotiation/negotiation.state.js.map +1 -1
- package/dist/network/indexer/indexer.graph.d.ts +15 -15
- package/dist/network/indexer/indexer.state.d.ts +4 -4
- package/dist/network/membership/membership.graph.d.ts +4 -4
- package/dist/network/membership/membership.state.d.ts +1 -1
- package/dist/network/network.graph.d.ts +4 -4
- package/dist/network/network.state.d.ts +1 -1
- package/dist/opportunity/discriminator/discriminator.assigner.d.ts +4 -4
- package/dist/opportunity/opportunity.evaluator.d.ts +9 -9
- package/dist/opportunity/opportunity.graph.d.ts +6 -6
- package/dist/opportunity/opportunity.state.d.ts +1 -1
- package/dist/premise/premise.analyzer.d.ts +2 -2
- package/dist/premise/premise.decomposer.d.ts +2 -2
- package/dist/premise/premise.indexer.d.ts +2 -2
- package/dist/shared/hyde/hyde.frame.d.ts +4 -4
- package/dist/shared/hyde/hyde.validator.d.ts +4 -4
- package/dist/shared/hyde/lens.inferrer.d.ts +6 -6
- package/dist/shared/interfaces/database.interface.d.ts +10 -0
- package/dist/shared/interfaces/database.interface.d.ts.map +1 -1
- package/dist/shared/interfaces/database.interface.js.map +1 -1
- package/dist/shared/schemas/negotiation-digest.schema.d.ts +2 -2
- package/dist/shared/schemas/question.schema.d.ts +36 -36
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { invokeWithAbortSignal } from "../shared/agent/model-signal.js";
|
|
|
3
3
|
import { SystemNegotiationTurnSchema, FinalNegotiationTurnSchema } from "./negotiation.state.js";
|
|
4
4
|
import { turnSchemaFor, fallbackActionFor } from "./negotiation.protocol.js";
|
|
5
5
|
import { renderNegotiatorMemorySection } from "./negotiation.memory.js";
|
|
6
|
+
import { renderBargainingShiftSection } from "./negotiation.deadlock.js";
|
|
6
7
|
import { protocolLogger } from "../shared/observability/protocol.logger.js";
|
|
7
8
|
const agentLog = protocolLogger("IndexNegotiator");
|
|
8
9
|
const SYSTEM_PROMPT = `You are the Index Negotiator, an AI agent acting on behalf of {userName}. You represent their interests in a bilateral negotiation about a potential connection on a discovery network.
|
|
@@ -19,7 +20,7 @@ Rules:
|
|
|
19
20
|
- Focus on concrete intent alignment, not vague overlap.
|
|
20
21
|
- Do NOT reference internal system details like scores, pre-screens, or evaluator outputs.
|
|
21
22
|
- suggestedRoles: "agent" = can help, "patient" = seeks help, "peer" = mutual benefit.
|
|
22
|
-
{finalTurnInstruction}{negotiatorMemory}`;
|
|
23
|
+
{finalTurnInstruction}{bargainingShift}{negotiatorMemory}`;
|
|
23
24
|
/** v1 action rules — byte-identical to the pre-seat-rules prompt. */
|
|
24
25
|
const V1_ACTION_RULES = `- On the FIRST turn: Propose the connection case. Explain why it would benefit both parties. Set action to "propose".
|
|
25
26
|
- On SUBSEQUENT turns: Evaluate the other agent's arguments. Either:
|
|
@@ -91,6 +92,9 @@ export class IndexNegotiator {
|
|
|
91
92
|
const seat = input.seat ?? (input.isDiscoverer ? "initiator" : "counterparty");
|
|
92
93
|
const isFinalTurn = input.isFinalTurn ?? false;
|
|
93
94
|
const canAskUser = input.canAskUser === true && version === "v2" && !isFinalTurn;
|
|
95
|
+
// Deadlock→bargaining stance (IND-428): v2 only — defense in depth on top
|
|
96
|
+
// of the graph-side gating, mirroring the canAskUser guard above.
|
|
97
|
+
const bargainingActive = input.bargaining != null && version === "v2";
|
|
94
98
|
const schema = turnSchemaFor(version, seat, isFinalTurn, {
|
|
95
99
|
system: SystemNegotiationTurnSchema,
|
|
96
100
|
final: FinalNegotiationTurnSchema,
|
|
@@ -128,6 +132,12 @@ QUERY PRIORITY RULE: This search query is the PRIMARY criterion for this negotia
|
|
|
128
132
|
.replace("{role}", role)
|
|
129
133
|
.replace("{networkContext}", networkContext)
|
|
130
134
|
.replace("{finalTurnInstruction}", finalTurnInstruction)
|
|
135
|
+
.replace("{bargainingShift}", renderBargainingShiftSection({
|
|
136
|
+
active: bargainingActive,
|
|
137
|
+
userName,
|
|
138
|
+
canAskUser,
|
|
139
|
+
consecutiveNonConvergent: input.bargaining?.consecutiveNonConvergent ?? 0,
|
|
140
|
+
}))
|
|
131
141
|
.replace("{negotiatorMemory}", renderNegotiatorMemorySection(input.memory ?? []));
|
|
132
142
|
const historyText = input.history.length > 0
|
|
133
143
|
? `\n\nNegotiation history:\n${input.history.map((t, i) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"negotiation.agent.js","sourceRoot":"/","sources":["negotiation/negotiation.agent.ts"],"names":[],"mappings":"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","sourcesContent":["import { createStructuredModel } from \"../shared/agent/model.config.js\";\nimport { invokeWithAbortSignal } from \"../shared/agent/model-signal.js\";\nimport { SystemNegotiationTurnSchema, FinalNegotiationTurnSchema, type NegotiationTurn, type UserNegotiationContext, type SeedAssessment } from \"./negotiation.state.js\";\nimport { turnSchemaFor, fallbackActionFor } from \"./negotiation.protocol.js\";\nimport type { NegotiationSeat, NegotiationProtocolVersion } from \"../shared/schemas/negotiation-state.schema.js\";\nimport type { NegotiationUserAnswer } from \"../shared/interfaces/database.interface.js\";\nimport { renderNegotiatorMemorySection, type NegotiatorMemoryEntry } from \"./negotiation.memory.js\";\nimport { protocolLogger } from \"../shared/observability/protocol.logger.js\";\n\nconst agentLog = protocolLogger(\"IndexNegotiator\");\n\nconst SYSTEM_PROMPT = `You are the Index Negotiator, an AI agent acting on behalf of {userName}. You represent their interests in a bilateral negotiation about a potential connection on a discovery network.\n\n{discoveryContext}\n{discoveryQueryContext}\nYour user's role in this connection: {role}\nNetwork context: {networkContext}\n\nYour job: Evaluate whether this connection genuinely serves {userName}'s interests given their role. Argue their case honestly — acknowledge weaknesses, but advocate for genuine fit.\n\nRules:\n{actionRules}\n- Focus on concrete intent alignment, not vague overlap.\n- Do NOT reference internal system details like scores, pre-screens, or evaluator outputs.\n- suggestedRoles: \"agent\" = can help, \"patient\" = seeks help, \"peer\" = mutual benefit.\n{finalTurnInstruction}{negotiatorMemory}`;\n\n/** v1 action rules — byte-identical to the pre-seat-rules prompt. */\nconst V1_ACTION_RULES = `- On the FIRST turn: Propose the connection case. Explain why it would benefit both parties. Set action to \"propose\".\n- On SUBSEQUENT turns: Evaluate the other agent's arguments. Either:\n - \"counter\" if you have specific objections but see potential\n - \"accept\" if the match genuinely benefits {userName}\n - \"reject\" if the match does not serve {userName}'s needs`;\n\n/** v2 initiator seat: reaching stance — accept is structurally unavailable. */\nconst V2_INITIATOR_RULES = `- You hold the INITIATING seat: your user's side surfaced this match and you are reaching out. Only the counterparty may accept — \"accept\" is NOT available to you.\n- On the FIRST turn: Make the outreach case. Explain why the connection would benefit both parties. Set action to \"outreach\".\n- On SUBSEQUENT turns: Evaluate the counterparty's arguments. Either:\n - \"counter\" if you have specific objections but see potential\n - \"question\" if you need a specific clarification from the counterparty\n - \"withdraw\" if the match does not serve {userName}'s needs`;\n\n/**\n * v2 client-consult pause rule (P3.2). Appended to either seat's rules only\n * when the caller granted `canAskUser` — the action never appears in the\n * prompt (or the schema) otherwise.\n */\nconst ASK_USER_RULE = `\n- \"ask_user\" if you need {userName}'s OWN input before you can proceed — typically permission to disclose something sensitive (budget, availability, private details) or a fact only they know. This PAUSES the negotiation until they answer (up to 24h), so use it only when proceeding without their input would risk over-disclosure or a wrong call. You get AT MOST ONE client consultation per negotiation — spend it well. Set askUser: { disclosureSubject: what you need permission for or need to know, draftQuestion: the question in your words }. Use \"question\" (not \"ask_user\") when the clarification should come from the other side.`;\n\n/** v2 counterparty seat: receiving stance — acceptance is this seat's decision alone. */\nconst V2_COUNTERPARTY_RULES = `- You hold the RECEIVING seat: the other side reached out to {userName}. Whether to accept is YOUR seat's decision alone.\n- Evaluate the initiator's arguments. Either:\n - \"accept\" if the match genuinely benefits {userName}\n - \"decline\" if the match does not serve {userName}'s needs\n - \"counter\" if you have specific objections but see potential\n - \"question\" if you need a specific clarification from the initiator\n- Never use \"outreach\" — you are responding, not reaching out.`;\n\nexport interface NegotiationAgentInput {\n ownUser: UserNegotiationContext;\n otherUser: UserNegotiationContext;\n indexContext: { networkId: string; prompt?: string };\n seedAssessment: SeedAssessment;\n history: NegotiationTurn[];\n isFinalTurn?: boolean;\n /** Whether ownUser is the party that initiated the discovery (searched/signalled). */\n isDiscoverer?: boolean;\n /** The explicit search query that triggered discovery (if any). Takes priority over background intents. */\n discoveryQuery?: string;\n /** Whether this negotiation is continuing a prior conversation with the same counterparty. */\n isContinuation?: boolean;\n /** User answers collected by the questioner between negotiation sessions. */\n userAnswers?: NegotiationUserAnswer[];\n /**\n * The acting user's seat under the v2 client-advocate protocol. Selects the\n * seat-scoped turn schema and prompt stance when `protocolVersion` is `v2`.\n * Ignored under v1. Defaults from `isDiscoverer` when omitted.\n */\n seat?: NegotiationSeat;\n /**\n * Negotiation protocol version for this task (inherited, never re-stamped).\n * `v1` (default) keeps the legacy symmetric vocabulary and prompt.\n */\n protocolVersion?: NegotiationProtocolVersion;\n /**\n * Whether the `ask_user` client-consult pause (P3.2) is available on this\n * turn. The caller (negotiation graph) grants it only when the feature flag\n * is on, the pause loop is fully wired (questioner + answer-window timer +\n * opportunity to resume against), the turn is v2 non-final and non-opening,\n * and this side has not already consumed its one client question for the\n * negotiation. When true, the seat schema and prompt gain the action.\n */\n canAskUser?: boolean;\n /**\n * Retrieved negotiator memories for the acting user (P5.3 read path).\n * Rendered as a private prompt section — hard disclosure constraints plus\n * advisory hints. Absent/empty → the prompt is byte-identical to before.\n */\n memory?: NegotiatorMemoryEntry[];\n}\n\nexport interface IndexNegotiatorConfig {\n /**\n * Hard ceiling on a single LLM turn round-trip, in ms. When the underlying\n * model.invoke call exceeds this, an AbortSignal cancels the request and the\n * promise rejects — the calling turn node catches the rejection and treats it\n * as a failed turn, so one slow upstream call cannot consume the whole\n * negotiate-phase budget.\n *\n * Defaults to `NEGOTIATOR_TURN_TIMEOUT_MS` env var when set, otherwise\n * `DEFAULT_TURN_TIMEOUT_MS`. Sized to clip the p99 tail on Gemini-2.5-Flash\n * (~20 s today on OpenRouter) without trimming p90 (~12 s).\n */\n turnTimeoutMs?: number;\n}\n\nconst DEFAULT_TURN_TIMEOUT_MS = 15_000;\n\n// Resolver-valid range is `(0, Number.MAX_SAFE_INTEGER]`. The upper bound is\n// the runtime ceiling: `AbortSignal.timeout(N)` throws when N is outside\n// `[0, Number.MAX_SAFE_INTEGER]`, so `Number.isFinite` alone isn't enough —\n// values like `1e30` pass finiteness but blow up at the AbortSignal call.\n// The lower bound (`n > 0`) is a design choice rather than a runtime\n// constraint: `AbortSignal.timeout(0)` is technically legal but would abort\n// every turn before the LLM produces a response, so we reject it and fall\n// back to the default just like any other invalid override.\nfunction isValidTimeoutMs(n: number): boolean {\n return Number.isFinite(n) && n > 0 && n <= Number.MAX_SAFE_INTEGER;\n}\n\nfunction resolveTurnTimeoutMs(override?: number): number {\n if (typeof override === \"number\" && isValidTimeoutMs(override)) return override;\n const envValue = process.env.NEGOTIATOR_TURN_TIMEOUT_MS;\n if (envValue) {\n const parsed = Number(envValue);\n if (isValidTimeoutMs(parsed)) return parsed;\n }\n return DEFAULT_TURN_TIMEOUT_MS;\n}\n\n/**\n * Unified system negotiation agent that advocates for its user.\n * Adapts behavior based on turn position (first turn = propose, subsequent = respond).\n * @remarks Uses structured output constrained to NegotiationTurnSchema (without question action).\n */\nexport class IndexNegotiator {\n private readonly turnTimeoutMs: number;\n\n constructor(config?: IndexNegotiatorConfig) {\n this.turnTimeoutMs = resolveTurnTimeoutMs(config?.turnTimeoutMs);\n }\n\n /**\n * Generate a negotiation turn.\n * @param input - User contexts, seed assessment, history, and final turn flag\n * @returns A structured NegotiationTurn\n * @throws If the per-turn timeout fires before the LLM responds.\n */\n async invoke(input: NegotiationAgentInput): Promise<NegotiationTurn> {\n const version: NegotiationProtocolVersion = input.protocolVersion ?? \"v1\";\n const seat: NegotiationSeat = input.seat ?? (input.isDiscoverer ? \"initiator\" : \"counterparty\");\n const isFinalTurn = input.isFinalTurn ?? false;\n const canAskUser = input.canAskUser === true && version === \"v2\" && !isFinalTurn;\n const schema = turnSchemaFor(version, seat, isFinalTurn, {\n system: SystemNegotiationTurnSchema,\n final: FinalNegotiationTurnSchema,\n }, { askUser: canAskUser });\n const model = createStructuredModel(\"negotiator\", schema, { name: \"index_negotiator\" });\n\n const userName = input.ownUser.profile.name ?? \"your user\";\n const role = input.seedAssessment.valencyRole || \"peer\";\n const networkContext = input.indexContext.prompt || \"General discovery\";\n const actionRules = (version === \"v2\"\n ? (seat === \"initiator\" ? V2_INITIATOR_RULES : V2_COUNTERPARTY_RULES)\n : V1_ACTION_RULES) + (canAskUser ? ASK_USER_RULE : \"\");\n const finalTurnInstruction = input.isFinalTurn\n ? (version === \"v2\"\n ? (seat === \"initiator\"\n ? \"\\n\\nIMPORTANT: This is your FINAL turn. You MUST choose either 'withdraw' or 'counter'. Accept is not available to your seat.\"\n : \"\\n\\nIMPORTANT: This is your FINAL turn. You MUST choose either 'accept' or 'decline'. No counter is allowed.\")\n : \"\\n\\nIMPORTANT: This is your FINAL turn. You MUST choose either 'accept' or 'reject'. No counter is allowed.\")\n : \"\";\n\n const otherName = input.otherUser.profile.name ?? \"the other user\";\n const discoveryContext = input.isDiscoverer\n ? `${userName} initiated this discovery — they are actively looking for connections. ${otherName} was identified as a potential match.`\n : `${otherName} initiated this discovery and found ${userName} as a potential match. You are representing the discovered party.`;\n\n const discoveryQueryContext = input.discoveryQuery\n ? `\\nDISCOVERY QUERY: ${userName} explicitly searched for \"${input.discoveryQuery}\".\nQUERY PRIORITY RULE: This search query is the PRIMARY criterion for this negotiation. Before evaluating intents or profile overlap, first answer: does ${otherName} satisfy the search query \"${input.discoveryQuery}\"?\n- If the query is a role or identity term (e.g. \"samurai\", \"investors\", \"designers\"): check whether ${otherName} IS that thing based on their profile. Subject-matter adjacency does not count (drawing samurai ≠ being a samurai, raising funding ≠ being an investor).\n- If ${otherName} does NOT satisfy the query: REJECT the match. Background intents cannot rescue a query mismatch.\n- If ${otherName} DOES satisfy the query: PROPOSE or ACCEPT the connection and evaluate fit normally using intents and profile data.`\n : '';\n\n const systemPrompt = SYSTEM_PROMPT\n .replace(\"{actionRules}\", actionRules)\n .replace(/{userName}/g, userName)\n .replace(\"{discoveryContext}\", discoveryContext)\n .replace(\"{discoveryQueryContext}\", discoveryQueryContext)\n .replace(\"{role}\", role)\n .replace(\"{networkContext}\", networkContext)\n .replace(\"{finalTurnInstruction}\", finalTurnInstruction)\n .replace(\"{negotiatorMemory}\", renderNegotiatorMemorySection(input.memory ?? []));\n\n const historyText = input.history.length > 0\n ? `\\n\\nNegotiation history:\\n${input.history.map((t, i) => {\n const msgPart = t.message ? ` — message: ${t.message}` : '';\n return `Turn ${i + 1}: ${t.action} — reasoning: ${t.assessment.reasoning}${msgPart}`;\n }).join(\"\\n\")}`\n : \"\";\n\n const continuationContext = input.isContinuation && input.history.length > 0\n ? `\\n\\n--- Prior dialogue with this counterparty ---\n${historyText}\n\n--- New signal under evaluation ---\n${input.discoveryQuery\n ? `Discovery query: \"${input.discoveryQuery}\"`\n : `Seed assessment: ${input.seedAssessment.reasoning}`\n}\n\nPolicy: You are continuing a prior dialogue. If this signal is materially the same as one you previously evaluated, you may resolve quickly. If materially different, evaluate on its own merits.`\n : '';\n\n const userAnswersContext = input.userAnswers && input.userAnswers.length > 0\n ? `\\n\\n--- ${userName}'s additional context (provided between sessions) ---\\n${input.userAnswers.map((a) => {\n const opts = Array.isArray(a.selectedOptions) ? a.selectedOptions : [];\n const parts = opts.length > 0 ? opts.join(', ') : '';\n const free = a.freeText ? (parts ? ` — ${a.freeText}` : a.freeText) : '';\n if (!parts && !free) return '';\n return `- ${parts}${free}`;\n }).filter(Boolean).join(\"\\n\")}\\n`\n : '';\n\n const discoveryQueryReminder = input.discoveryQuery\n ? `\\nREMINDER: ${userName} searched for \"${input.discoveryQuery}\". Evaluate ${otherName} against this query FIRST. If ${otherName} is not a \"${input.discoveryQuery}\", reject.\\n`\n : '';\n\n const intentsLabel = input.discoveryQuery ? 'Background intents (secondary to discovery query)' : 'Intents';\n\n const userMessage = `YOUR USER (${userName}):\nBio: ${input.ownUser.profile.bio ?? \"N/A\"}\nSkills: ${input.ownUser.profile.skills?.join(\", \") ?? \"N/A\"}\n${intentsLabel}:\n${input.ownUser.intents.map((i) => `- ${i.title}: ${i.description}`).join(\"\\n\")}\n\nOTHER USER (${otherName}):\nBio: ${input.otherUser.profile.bio ?? \"N/A\"}\nSkills: ${input.otherUser.profile.skills?.join(\", \") ?? \"N/A\"}\nIntents:\n${input.otherUser.intents.map((i) => `- ${i.title}: ${i.description}`).join(\"\\n\")}\n\nWhy this match was suggested: ${input.seedAssessment.reasoning}${input.isContinuation ? continuationContext : historyText}${userAnswersContext}\n${discoveryQueryReminder}\n${input.history.length === 0 && !input.isContinuation ? (version === \"v2\" && seat === \"initiator\" ? \"This is the opening turn. Make the outreach case.\" : \"This is the opening turn. Propose the connection case.\") : \"Evaluate the latest arguments and respond.\"}`;\n\n const chatMessages = [\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: userMessage },\n ];\n\n // Structured output is schema-constrained, but providers can still emit\n // out-of-vocabulary actions. Validate; retry once; then fall back to the\n // conservative seat-valid action instead of poisoning the turn history.\n for (let attempt = 0; attempt < 2; attempt++) {\n const result = await this.callModel(model, chatMessages);\n const parsed = schema.safeParse(result);\n if (parsed.success) return parsed.data as NegotiationTurn;\n agentLog.warn(\"Negotiator output failed seat-schema validation\", {\n attempt: attempt + 1,\n seat,\n version,\n isFinalTurn,\n issues: parsed.error.issues.map((i) => i.message).slice(0, 3),\n });\n }\n\n const fallbackAction = fallbackActionFor(version, seat, isFinalTurn);\n agentLog.warn(\"Negotiator output invalid after retry; using conservative fallback\", {\n seat, version, isFinalTurn, fallbackAction,\n });\n return {\n action: fallbackAction,\n assessment: {\n reasoning: \"Agent produced an invalid response; conservative fallback applied.\",\n suggestedRoles: { ownUser: \"peer\", otherUser: \"peer\" },\n },\n message: null,\n };\n }\n\n /**\n * Raw structured-model round trip. Split out as a seam so tests can drive\n * the validate→retry→fallback loop without a live provider.\n */\n protected async callModel(\n model: ReturnType<typeof createStructuredModel>,\n chatMessages: Array<{ role: string; content: string }>,\n ): Promise<unknown> {\n return invokeWithAbortSignal(model, chatMessages, AbortSignal.timeout(this.turnTimeoutMs));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"negotiation.agent.js","sourceRoot":"/","sources":["negotiation/negotiation.agent.ts"],"names":[],"mappings":"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","sourcesContent":["import { createStructuredModel } from \"../shared/agent/model.config.js\";\nimport { invokeWithAbortSignal } from \"../shared/agent/model-signal.js\";\nimport { SystemNegotiationTurnSchema, FinalNegotiationTurnSchema, type NegotiationTurn, type UserNegotiationContext, type SeedAssessment } from \"./negotiation.state.js\";\nimport { turnSchemaFor, fallbackActionFor } from \"./negotiation.protocol.js\";\nimport type { NegotiationSeat, NegotiationProtocolVersion } from \"../shared/schemas/negotiation-state.schema.js\";\nimport type { NegotiationUserAnswer } from \"../shared/interfaces/database.interface.js\";\nimport { renderNegotiatorMemorySection, type NegotiatorMemoryEntry } from \"./negotiation.memory.js\";\nimport { renderBargainingShiftSection } from \"./negotiation.deadlock.js\";\nimport { protocolLogger } from \"../shared/observability/protocol.logger.js\";\n\nconst agentLog = protocolLogger(\"IndexNegotiator\");\n\nconst SYSTEM_PROMPT = `You are the Index Negotiator, an AI agent acting on behalf of {userName}. You represent their interests in a bilateral negotiation about a potential connection on a discovery network.\n\n{discoveryContext}\n{discoveryQueryContext}\nYour user's role in this connection: {role}\nNetwork context: {networkContext}\n\nYour job: Evaluate whether this connection genuinely serves {userName}'s interests given their role. Argue their case honestly — acknowledge weaknesses, but advocate for genuine fit.\n\nRules:\n{actionRules}\n- Focus on concrete intent alignment, not vague overlap.\n- Do NOT reference internal system details like scores, pre-screens, or evaluator outputs.\n- suggestedRoles: \"agent\" = can help, \"patient\" = seeks help, \"peer\" = mutual benefit.\n{finalTurnInstruction}{bargainingShift}{negotiatorMemory}`;\n\n/** v1 action rules — byte-identical to the pre-seat-rules prompt. */\nconst V1_ACTION_RULES = `- On the FIRST turn: Propose the connection case. Explain why it would benefit both parties. Set action to \"propose\".\n- On SUBSEQUENT turns: Evaluate the other agent's arguments. Either:\n - \"counter\" if you have specific objections but see potential\n - \"accept\" if the match genuinely benefits {userName}\n - \"reject\" if the match does not serve {userName}'s needs`;\n\n/** v2 initiator seat: reaching stance — accept is structurally unavailable. */\nconst V2_INITIATOR_RULES = `- You hold the INITIATING seat: your user's side surfaced this match and you are reaching out. Only the counterparty may accept — \"accept\" is NOT available to you.\n- On the FIRST turn: Make the outreach case. Explain why the connection would benefit both parties. Set action to \"outreach\".\n- On SUBSEQUENT turns: Evaluate the counterparty's arguments. Either:\n - \"counter\" if you have specific objections but see potential\n - \"question\" if you need a specific clarification from the counterparty\n - \"withdraw\" if the match does not serve {userName}'s needs`;\n\n/**\n * v2 client-consult pause rule (P3.2). Appended to either seat's rules only\n * when the caller granted `canAskUser` — the action never appears in the\n * prompt (or the schema) otherwise.\n */\nconst ASK_USER_RULE = `\n- \"ask_user\" if you need {userName}'s OWN input before you can proceed — typically permission to disclose something sensitive (budget, availability, private details) or a fact only they know. This PAUSES the negotiation until they answer (up to 24h), so use it only when proceeding without their input would risk over-disclosure or a wrong call. You get AT MOST ONE client consultation per negotiation — spend it well. Set askUser: { disclosureSubject: what you need permission for or need to know, draftQuestion: the question in your words }. Use \"question\" (not \"ask_user\") when the clarification should come from the other side.`;\n\n/** v2 counterparty seat: receiving stance — acceptance is this seat's decision alone. */\nconst V2_COUNTERPARTY_RULES = `- You hold the RECEIVING seat: the other side reached out to {userName}. Whether to accept is YOUR seat's decision alone.\n- Evaluate the initiator's arguments. Either:\n - \"accept\" if the match genuinely benefits {userName}\n - \"decline\" if the match does not serve {userName}'s needs\n - \"counter\" if you have specific objections but see potential\n - \"question\" if you need a specific clarification from the initiator\n- Never use \"outreach\" — you are responding, not reaching out.`;\n\nexport interface NegotiationAgentInput {\n ownUser: UserNegotiationContext;\n otherUser: UserNegotiationContext;\n indexContext: { networkId: string; prompt?: string };\n seedAssessment: SeedAssessment;\n history: NegotiationTurn[];\n isFinalTurn?: boolean;\n /** Whether ownUser is the party that initiated the discovery (searched/signalled). */\n isDiscoverer?: boolean;\n /** The explicit search query that triggered discovery (if any). Takes priority over background intents. */\n discoveryQuery?: string;\n /** Whether this negotiation is continuing a prior conversation with the same counterparty. */\n isContinuation?: boolean;\n /** User answers collected by the questioner between negotiation sessions. */\n userAnswers?: NegotiationUserAnswer[];\n /**\n * The acting user's seat under the v2 client-advocate protocol. Selects the\n * seat-scoped turn schema and prompt stance when `protocolVersion` is `v2`.\n * Ignored under v1. Defaults from `isDiscoverer` when omitted.\n */\n seat?: NegotiationSeat;\n /**\n * Negotiation protocol version for this task (inherited, never re-stamped).\n * `v1` (default) keeps the legacy symmetric vocabulary and prompt.\n */\n protocolVersion?: NegotiationProtocolVersion;\n /**\n * Whether the `ask_user` client-consult pause (P3.2) is available on this\n * turn. The caller (negotiation graph) grants it only when the feature flag\n * is on, the pause loop is fully wired (questioner + answer-window timer +\n * opportunity to resume against), the turn is v2 non-final and non-opening,\n * and this side has not already consumed its one client question for the\n * negotiation. When true, the seat schema and prompt gain the action.\n */\n canAskUser?: boolean;\n /**\n * Deadlock→bargaining drafting stance (IND-428, flag-gated by the caller).\n * Present = the graph detected a stalemate (N consecutive counter/question\n * turns) and this turn should be drafted in the bargaining stance —\n * concessions/scope reductions instead of re-arguing merits. v2 only;\n * ignored under v1. Absent → the prompt is byte-identical to before.\n */\n bargaining?: { consecutiveNonConvergent: number };\n /**\n * Retrieved negotiator memories for the acting user (P5.3 read path).\n * Rendered as a private prompt section — hard disclosure constraints plus\n * advisory hints. Absent/empty → the prompt is byte-identical to before.\n */\n memory?: NegotiatorMemoryEntry[];\n}\n\nexport interface IndexNegotiatorConfig {\n /**\n * Hard ceiling on a single LLM turn round-trip, in ms. When the underlying\n * model.invoke call exceeds this, an AbortSignal cancels the request and the\n * promise rejects — the calling turn node catches the rejection and treats it\n * as a failed turn, so one slow upstream call cannot consume the whole\n * negotiate-phase budget.\n *\n * Defaults to `NEGOTIATOR_TURN_TIMEOUT_MS` env var when set, otherwise\n * `DEFAULT_TURN_TIMEOUT_MS`. Sized to clip the p99 tail on Gemini-2.5-Flash\n * (~20 s today on OpenRouter) without trimming p90 (~12 s).\n */\n turnTimeoutMs?: number;\n}\n\nconst DEFAULT_TURN_TIMEOUT_MS = 15_000;\n\n// Resolver-valid range is `(0, Number.MAX_SAFE_INTEGER]`. The upper bound is\n// the runtime ceiling: `AbortSignal.timeout(N)` throws when N is outside\n// `[0, Number.MAX_SAFE_INTEGER]`, so `Number.isFinite` alone isn't enough —\n// values like `1e30` pass finiteness but blow up at the AbortSignal call.\n// The lower bound (`n > 0`) is a design choice rather than a runtime\n// constraint: `AbortSignal.timeout(0)` is technically legal but would abort\n// every turn before the LLM produces a response, so we reject it and fall\n// back to the default just like any other invalid override.\nfunction isValidTimeoutMs(n: number): boolean {\n return Number.isFinite(n) && n > 0 && n <= Number.MAX_SAFE_INTEGER;\n}\n\nfunction resolveTurnTimeoutMs(override?: number): number {\n if (typeof override === \"number\" && isValidTimeoutMs(override)) return override;\n const envValue = process.env.NEGOTIATOR_TURN_TIMEOUT_MS;\n if (envValue) {\n const parsed = Number(envValue);\n if (isValidTimeoutMs(parsed)) return parsed;\n }\n return DEFAULT_TURN_TIMEOUT_MS;\n}\n\n/**\n * Unified system negotiation agent that advocates for its user.\n * Adapts behavior based on turn position (first turn = propose, subsequent = respond).\n * @remarks Uses structured output constrained to NegotiationTurnSchema (without question action).\n */\nexport class IndexNegotiator {\n private readonly turnTimeoutMs: number;\n\n constructor(config?: IndexNegotiatorConfig) {\n this.turnTimeoutMs = resolveTurnTimeoutMs(config?.turnTimeoutMs);\n }\n\n /**\n * Generate a negotiation turn.\n * @param input - User contexts, seed assessment, history, and final turn flag\n * @returns A structured NegotiationTurn\n * @throws If the per-turn timeout fires before the LLM responds.\n */\n async invoke(input: NegotiationAgentInput): Promise<NegotiationTurn> {\n const version: NegotiationProtocolVersion = input.protocolVersion ?? \"v1\";\n const seat: NegotiationSeat = input.seat ?? (input.isDiscoverer ? \"initiator\" : \"counterparty\");\n const isFinalTurn = input.isFinalTurn ?? false;\n const canAskUser = input.canAskUser === true && version === \"v2\" && !isFinalTurn;\n // Deadlock→bargaining stance (IND-428): v2 only — defense in depth on top\n // of the graph-side gating, mirroring the canAskUser guard above.\n const bargainingActive = input.bargaining != null && version === \"v2\";\n const schema = turnSchemaFor(version, seat, isFinalTurn, {\n system: SystemNegotiationTurnSchema,\n final: FinalNegotiationTurnSchema,\n }, { askUser: canAskUser });\n const model = createStructuredModel(\"negotiator\", schema, { name: \"index_negotiator\" });\n\n const userName = input.ownUser.profile.name ?? \"your user\";\n const role = input.seedAssessment.valencyRole || \"peer\";\n const networkContext = input.indexContext.prompt || \"General discovery\";\n const actionRules = (version === \"v2\"\n ? (seat === \"initiator\" ? V2_INITIATOR_RULES : V2_COUNTERPARTY_RULES)\n : V1_ACTION_RULES) + (canAskUser ? ASK_USER_RULE : \"\");\n const finalTurnInstruction = input.isFinalTurn\n ? (version === \"v2\"\n ? (seat === \"initiator\"\n ? \"\\n\\nIMPORTANT: This is your FINAL turn. You MUST choose either 'withdraw' or 'counter'. Accept is not available to your seat.\"\n : \"\\n\\nIMPORTANT: This is your FINAL turn. You MUST choose either 'accept' or 'decline'. No counter is allowed.\")\n : \"\\n\\nIMPORTANT: This is your FINAL turn. You MUST choose either 'accept' or 'reject'. No counter is allowed.\")\n : \"\";\n\n const otherName = input.otherUser.profile.name ?? \"the other user\";\n const discoveryContext = input.isDiscoverer\n ? `${userName} initiated this discovery — they are actively looking for connections. ${otherName} was identified as a potential match.`\n : `${otherName} initiated this discovery and found ${userName} as a potential match. You are representing the discovered party.`;\n\n const discoveryQueryContext = input.discoveryQuery\n ? `\\nDISCOVERY QUERY: ${userName} explicitly searched for \"${input.discoveryQuery}\".\nQUERY PRIORITY RULE: This search query is the PRIMARY criterion for this negotiation. Before evaluating intents or profile overlap, first answer: does ${otherName} satisfy the search query \"${input.discoveryQuery}\"?\n- If the query is a role or identity term (e.g. \"samurai\", \"investors\", \"designers\"): check whether ${otherName} IS that thing based on their profile. Subject-matter adjacency does not count (drawing samurai ≠ being a samurai, raising funding ≠ being an investor).\n- If ${otherName} does NOT satisfy the query: REJECT the match. Background intents cannot rescue a query mismatch.\n- If ${otherName} DOES satisfy the query: PROPOSE or ACCEPT the connection and evaluate fit normally using intents and profile data.`\n : '';\n\n const systemPrompt = SYSTEM_PROMPT\n .replace(\"{actionRules}\", actionRules)\n .replace(/{userName}/g, userName)\n .replace(\"{discoveryContext}\", discoveryContext)\n .replace(\"{discoveryQueryContext}\", discoveryQueryContext)\n .replace(\"{role}\", role)\n .replace(\"{networkContext}\", networkContext)\n .replace(\"{finalTurnInstruction}\", finalTurnInstruction)\n .replace(\"{bargainingShift}\", renderBargainingShiftSection({\n active: bargainingActive,\n userName,\n canAskUser,\n consecutiveNonConvergent: input.bargaining?.consecutiveNonConvergent ?? 0,\n }))\n .replace(\"{negotiatorMemory}\", renderNegotiatorMemorySection(input.memory ?? []));\n\n const historyText = input.history.length > 0\n ? `\\n\\nNegotiation history:\\n${input.history.map((t, i) => {\n const msgPart = t.message ? ` — message: ${t.message}` : '';\n return `Turn ${i + 1}: ${t.action} — reasoning: ${t.assessment.reasoning}${msgPart}`;\n }).join(\"\\n\")}`\n : \"\";\n\n const continuationContext = input.isContinuation && input.history.length > 0\n ? `\\n\\n--- Prior dialogue with this counterparty ---\n${historyText}\n\n--- New signal under evaluation ---\n${input.discoveryQuery\n ? `Discovery query: \"${input.discoveryQuery}\"`\n : `Seed assessment: ${input.seedAssessment.reasoning}`\n}\n\nPolicy: You are continuing a prior dialogue. If this signal is materially the same as one you previously evaluated, you may resolve quickly. If materially different, evaluate on its own merits.`\n : '';\n\n const userAnswersContext = input.userAnswers && input.userAnswers.length > 0\n ? `\\n\\n--- ${userName}'s additional context (provided between sessions) ---\\n${input.userAnswers.map((a) => {\n const opts = Array.isArray(a.selectedOptions) ? a.selectedOptions : [];\n const parts = opts.length > 0 ? opts.join(', ') : '';\n const free = a.freeText ? (parts ? ` — ${a.freeText}` : a.freeText) : '';\n if (!parts && !free) return '';\n return `- ${parts}${free}`;\n }).filter(Boolean).join(\"\\n\")}\\n`\n : '';\n\n const discoveryQueryReminder = input.discoveryQuery\n ? `\\nREMINDER: ${userName} searched for \"${input.discoveryQuery}\". Evaluate ${otherName} against this query FIRST. If ${otherName} is not a \"${input.discoveryQuery}\", reject.\\n`\n : '';\n\n const intentsLabel = input.discoveryQuery ? 'Background intents (secondary to discovery query)' : 'Intents';\n\n const userMessage = `YOUR USER (${userName}):\nBio: ${input.ownUser.profile.bio ?? \"N/A\"}\nSkills: ${input.ownUser.profile.skills?.join(\", \") ?? \"N/A\"}\n${intentsLabel}:\n${input.ownUser.intents.map((i) => `- ${i.title}: ${i.description}`).join(\"\\n\")}\n\nOTHER USER (${otherName}):\nBio: ${input.otherUser.profile.bio ?? \"N/A\"}\nSkills: ${input.otherUser.profile.skills?.join(\", \") ?? \"N/A\"}\nIntents:\n${input.otherUser.intents.map((i) => `- ${i.title}: ${i.description}`).join(\"\\n\")}\n\nWhy this match was suggested: ${input.seedAssessment.reasoning}${input.isContinuation ? continuationContext : historyText}${userAnswersContext}\n${discoveryQueryReminder}\n${input.history.length === 0 && !input.isContinuation ? (version === \"v2\" && seat === \"initiator\" ? \"This is the opening turn. Make the outreach case.\" : \"This is the opening turn. Propose the connection case.\") : \"Evaluate the latest arguments and respond.\"}`;\n\n const chatMessages = [\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: userMessage },\n ];\n\n // Structured output is schema-constrained, but providers can still emit\n // out-of-vocabulary actions. Validate; retry once; then fall back to the\n // conservative seat-valid action instead of poisoning the turn history.\n for (let attempt = 0; attempt < 2; attempt++) {\n const result = await this.callModel(model, chatMessages);\n const parsed = schema.safeParse(result);\n if (parsed.success) return parsed.data as NegotiationTurn;\n agentLog.warn(\"Negotiator output failed seat-schema validation\", {\n attempt: attempt + 1,\n seat,\n version,\n isFinalTurn,\n issues: parsed.error.issues.map((i) => i.message).slice(0, 3),\n });\n }\n\n const fallbackAction = fallbackActionFor(version, seat, isFinalTurn);\n agentLog.warn(\"Negotiator output invalid after retry; using conservative fallback\", {\n seat, version, isFinalTurn, fallbackAction,\n });\n return {\n action: fallbackAction,\n assessment: {\n reasoning: \"Agent produced an invalid response; conservative fallback applied.\",\n suggestedRoles: { ownUser: \"peer\", otherUser: \"peer\" },\n },\n message: null,\n };\n }\n\n /**\n * Raw structured-model round trip. Split out as a seam so tests can drive\n * the validate→retry→fallback loop without a live provider.\n */\n protected async callModel(\n model: ReturnType<typeof createStructuredModel>,\n chatMessages: Array<{ role: string; content: string }>,\n ): Promise<unknown> {\n return invokeWithAbortSignal(model, chatMessages, AbortSignal.timeout(this.turnTimeoutMs));\n }\n}\n"]}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deadlock detection + persuasion→bargaining mode shift (IND-428, backlog item 6).
|
|
3
|
+
*
|
|
4
|
+
* Grounding: Wells & Reed (2006), *Knowing When to Bargain* — a persuasion
|
|
5
|
+
* dialogue (arguing the merits) that reaches a stalemate may execute a *legal
|
|
6
|
+
* shift* into a negotiation dialogue (offering concessions). See
|
|
7
|
+
* `docs/design/negotiation-dialogue-game.md` for the formal framing of the
|
|
8
|
+
* turn protocol as a dialogue game.
|
|
9
|
+
*
|
|
10
|
+
* Design constraints (hard):
|
|
11
|
+
* - **Deterministic**: deadlock is decided by pure inspection of the persisted
|
|
12
|
+
* turn history — never by an LLM.
|
|
13
|
+
* - **Stance, not rules**: a detected deadlock changes the system agent's
|
|
14
|
+
* *drafting stance* only. Locutions, seat vocabularies (`allowedActionsFor`),
|
|
15
|
+
* termination, and turn-cap semantics are untouched.
|
|
16
|
+
* - **Default-off**: gated on `NEGOTIATION_DEADLOCK_SHIFT_ENABLED === "true"`
|
|
17
|
+
* (strict literal) and applied only to v2 negotiations, checked alongside the
|
|
18
|
+
* protocol-version plumbing. When off, the legacy path is byte-identical.
|
|
19
|
+
* - **Fail-open**: any detection error means "no deadlock" — advisory
|
|
20
|
+
* infrastructure never blocks a negotiation.
|
|
21
|
+
*/
|
|
22
|
+
import type { NegotiationTurn } from "./negotiation.state.js";
|
|
23
|
+
/**
|
|
24
|
+
* Whether the deadlock→bargaining mode shift is enabled, from the
|
|
25
|
+
* `NEGOTIATION_DEADLOCK_SHIFT_ENABLED` env switch. Strict literal `"true"`
|
|
26
|
+
* only — the deployment is byte-for-byte unchanged until the flag is flipped,
|
|
27
|
+
* and rolling back is the same single switch.
|
|
28
|
+
*/
|
|
29
|
+
export declare function configuredDeadlockShiftEnabled(): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Default deadlock threshold: 4 consecutive non-convergent turns. Sized
|
|
32
|
+
* against the ambient turn cap (6): an outreach plus 4 unbroken counters
|
|
33
|
+
* leaves exactly the closing turns to draft in the bargaining stance.
|
|
34
|
+
*/
|
|
35
|
+
export declare const DEFAULT_DEADLOCK_THRESHOLD = 4;
|
|
36
|
+
/**
|
|
37
|
+
* Lower bound on the configurable threshold. Below 2 the "stalemate" signal is
|
|
38
|
+
* meaningless — a single counter is ordinary dialogue, not a deadlock.
|
|
39
|
+
*/
|
|
40
|
+
export declare const MIN_DEADLOCK_THRESHOLD = 2;
|
|
41
|
+
/**
|
|
42
|
+
* Consecutive non-convergent turns that constitute a deadlock, from
|
|
43
|
+
* `NEGOTIATION_DEADLOCK_THRESHOLD`. Must be an integer >= 2; invalid,
|
|
44
|
+
* non-integer, or out-of-range values fall back to the default (fail-open
|
|
45
|
+
* toward the documented behavior, mirroring `askUserAnswerWindowMs`).
|
|
46
|
+
*/
|
|
47
|
+
export declare function configuredDeadlockThreshold(): number;
|
|
48
|
+
export interface DeadlockAssessment {
|
|
49
|
+
/** True when the trailing non-convergent run has reached the threshold. */
|
|
50
|
+
deadlocked: boolean;
|
|
51
|
+
/** Length of the maximal trailing run of counter/question turns. */
|
|
52
|
+
consecutiveNonConvergent: number;
|
|
53
|
+
/** The threshold the run was compared against. */
|
|
54
|
+
threshold: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Deterministic stalemate detector: measures the maximal *trailing* run of
|
|
58
|
+
* non-convergent turns (`counter`/`question`) in the persisted history and
|
|
59
|
+
* compares it against the threshold. Continuation histories are included by
|
|
60
|
+
* construction — the caller passes the full turn list, so a deadlock spanning
|
|
61
|
+
* sessions still counts.
|
|
62
|
+
*
|
|
63
|
+
* Pure state inspection; no LLM, no I/O, no clock.
|
|
64
|
+
*/
|
|
65
|
+
export declare function assessDeadlock(history: ReadonlyArray<Pick<NegotiationTurn, "action">>, threshold?: number): DeadlockAssessment;
|
|
66
|
+
/**
|
|
67
|
+
* Analytical record of an applied shift, persisted to
|
|
68
|
+
* `tasks.metadata.deadlockShift` via the optional `setTaskDeadlockShift`
|
|
69
|
+
* database hook. Internal-only: negotiation API surfaces project specific
|
|
70
|
+
* fields and never return task metadata verbatim (same privacy posture as
|
|
71
|
+
* `metadata.screenDecision` and the QUD/uptake detection metadata).
|
|
72
|
+
*/
|
|
73
|
+
export interface DeadlockShiftRecord {
|
|
74
|
+
reason: "consecutive_non_convergent";
|
|
75
|
+
consecutiveNonConvergent: number;
|
|
76
|
+
threshold: number;
|
|
77
|
+
/** Zero-based session turn index at which the shifted draft happened. */
|
|
78
|
+
shiftedAtTurn: number;
|
|
79
|
+
seat: "initiator" | "counterparty";
|
|
80
|
+
detectedAt: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Renders the bargaining-stance prompt section. Returns the empty string when
|
|
84
|
+
* the shift is not active, so the rendered system prompt is byte-identical to
|
|
85
|
+
* the legacy build on every non-shifted turn (mirrors
|
|
86
|
+
* `renderNegotiatorMemorySection`). The `ask_user` escalation line renders
|
|
87
|
+
* only when the caller already legally holds the action (`canAskUser`) — the
|
|
88
|
+
* shift never invents a locution.
|
|
89
|
+
*/
|
|
90
|
+
export declare function renderBargainingShiftSection(input: {
|
|
91
|
+
active: boolean;
|
|
92
|
+
userName: string;
|
|
93
|
+
canAskUser: boolean;
|
|
94
|
+
consecutiveNonConvergent: number;
|
|
95
|
+
}): string;
|
|
96
|
+
//# sourceMappingURL=negotiation.deadlock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"negotiation.deadlock.d.ts","sourceRoot":"/","sources":["negotiation/negotiation.deadlock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAI9D;;;;;GAKG;AACH,wBAAgB,8BAA8B,IAAI,OAAO,CAExD;AAED;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC;;;;;GAKG;AACH,wBAAgB,2BAA2B,IAAI,MAAM,CAOpD;AAcD,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,UAAU,EAAE,OAAO,CAAC;IACpB,oEAAoE;IACpE,wBAAwB,EAAE,MAAM,CAAC;IACjC,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAC,EACvD,SAAS,GAAE,MAAmC,GAC7C,kBAAkB,CAoBpB;AAID;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,4BAA4B,CAAC;IACrC,wBAAwB,EAAE,MAAM,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,WAAW,GAAG,cAAc,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;CACpB;AAgBD;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,wBAAwB,EAAE,MAAM,CAAC;CAClC,GAAG,MAAM,CAMT"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// ─── Configuration ───────────────────────────────────────────────────────────
|
|
2
|
+
/**
|
|
3
|
+
* Whether the deadlock→bargaining mode shift is enabled, from the
|
|
4
|
+
* `NEGOTIATION_DEADLOCK_SHIFT_ENABLED` env switch. Strict literal `"true"`
|
|
5
|
+
* only — the deployment is byte-for-byte unchanged until the flag is flipped,
|
|
6
|
+
* and rolling back is the same single switch.
|
|
7
|
+
*/
|
|
8
|
+
export function configuredDeadlockShiftEnabled() {
|
|
9
|
+
return process.env.NEGOTIATION_DEADLOCK_SHIFT_ENABLED === "true";
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Default deadlock threshold: 4 consecutive non-convergent turns. Sized
|
|
13
|
+
* against the ambient turn cap (6): an outreach plus 4 unbroken counters
|
|
14
|
+
* leaves exactly the closing turns to draft in the bargaining stance.
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_DEADLOCK_THRESHOLD = 4;
|
|
17
|
+
/**
|
|
18
|
+
* Lower bound on the configurable threshold. Below 2 the "stalemate" signal is
|
|
19
|
+
* meaningless — a single counter is ordinary dialogue, not a deadlock.
|
|
20
|
+
*/
|
|
21
|
+
export const MIN_DEADLOCK_THRESHOLD = 2;
|
|
22
|
+
/**
|
|
23
|
+
* Consecutive non-convergent turns that constitute a deadlock, from
|
|
24
|
+
* `NEGOTIATION_DEADLOCK_THRESHOLD`. Must be an integer >= 2; invalid,
|
|
25
|
+
* non-integer, or out-of-range values fall back to the default (fail-open
|
|
26
|
+
* toward the documented behavior, mirroring `askUserAnswerWindowMs`).
|
|
27
|
+
*/
|
|
28
|
+
export function configuredDeadlockThreshold() {
|
|
29
|
+
const raw = process.env.NEGOTIATION_DEADLOCK_THRESHOLD;
|
|
30
|
+
if (raw) {
|
|
31
|
+
const parsed = Number(raw);
|
|
32
|
+
if (Number.isInteger(parsed) && parsed >= MIN_DEADLOCK_THRESHOLD)
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
return DEFAULT_DEADLOCK_THRESHOLD;
|
|
36
|
+
}
|
|
37
|
+
// ─── Detection ───────────────────────────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* The locutions that count toward a stalemate: challenges and information
|
|
40
|
+
* requests that keep the dialogue open without converging. Everything else —
|
|
41
|
+
* openings (`propose`/`outreach`: a fresh case is on the table), terminal
|
|
42
|
+
* actions (the game is deciding, not stalling), and `ask_user` (new principal
|
|
43
|
+
* input is about to arrive) — RESETS the run to zero. Unknown/missing actions
|
|
44
|
+
* also reset (conservative: never manufacture a deadlock from unreadable data).
|
|
45
|
+
*/
|
|
46
|
+
const NON_CONVERGENT_ACTIONS = new Set(["counter", "question"]);
|
|
47
|
+
/**
|
|
48
|
+
* Deterministic stalemate detector: measures the maximal *trailing* run of
|
|
49
|
+
* non-convergent turns (`counter`/`question`) in the persisted history and
|
|
50
|
+
* compares it against the threshold. Continuation histories are included by
|
|
51
|
+
* construction — the caller passes the full turn list, so a deadlock spanning
|
|
52
|
+
* sessions still counts.
|
|
53
|
+
*
|
|
54
|
+
* Pure state inspection; no LLM, no I/O, no clock.
|
|
55
|
+
*/
|
|
56
|
+
export function assessDeadlock(history, threshold = DEFAULT_DEADLOCK_THRESHOLD) {
|
|
57
|
+
const effectiveThreshold = Number.isInteger(threshold) && threshold >= MIN_DEADLOCK_THRESHOLD
|
|
58
|
+
? threshold
|
|
59
|
+
: DEFAULT_DEADLOCK_THRESHOLD;
|
|
60
|
+
let run = 0;
|
|
61
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
62
|
+
const action = history[i]?.action;
|
|
63
|
+
if (typeof action === "string" && NON_CONVERGENT_ACTIONS.has(action)) {
|
|
64
|
+
run += 1;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
deadlocked: run >= effectiveThreshold,
|
|
72
|
+
consecutiveNonConvergent: run,
|
|
73
|
+
threshold: effectiveThreshold,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// ─── Prompt section (system agent drafting stance) ──────────────────────────
|
|
77
|
+
const BARGAINING_SHIFT_SECTION = `
|
|
78
|
+
|
|
79
|
+
DEADLOCK — SHIFT FROM PERSUASION TO BARGAINING. The last {consecutive} turns were counters/questions without convergence: the merits have been argued and restating them will not move the other side. For this turn, change stance:
|
|
80
|
+
- Do NOT re-argue fit or repeat points already made.
|
|
81
|
+
- Offer a concrete concession or scope reduction instead: a smaller first step (a single intro call, a scoped trial, a narrower version of the collaboration), dropping a contested requirement, or a trade on a dimension not yet contested.
|
|
82
|
+
- Make the remaining objection priceable: name the specific smaller commitment that would resolve it.{askUserEscalation}
|
|
83
|
+
- If no reduced scope would genuinely serve {userName}'s interests, conclude decisively with a terminal action from your allowed set rather than another repetitive counter.
|
|
84
|
+
This shift changes your stance only — your available actions are unchanged.`;
|
|
85
|
+
const BARGAINING_ASK_USER_ESCALATION = `
|
|
86
|
+
- If a concession would require {userName}'s own input or permission (budget, availability, private details), escalate with "ask_user" instead of guessing.`;
|
|
87
|
+
/**
|
|
88
|
+
* Renders the bargaining-stance prompt section. Returns the empty string when
|
|
89
|
+
* the shift is not active, so the rendered system prompt is byte-identical to
|
|
90
|
+
* the legacy build on every non-shifted turn (mirrors
|
|
91
|
+
* `renderNegotiatorMemorySection`). The `ask_user` escalation line renders
|
|
92
|
+
* only when the caller already legally holds the action (`canAskUser`) — the
|
|
93
|
+
* shift never invents a locution.
|
|
94
|
+
*/
|
|
95
|
+
export function renderBargainingShiftSection(input) {
|
|
96
|
+
if (!input.active)
|
|
97
|
+
return "";
|
|
98
|
+
return BARGAINING_SHIFT_SECTION
|
|
99
|
+
.replace("{consecutive}", String(input.consecutiveNonConvergent))
|
|
100
|
+
.replace("{askUserEscalation}", input.canAskUser ? BARGAINING_ASK_USER_ESCALATION : "")
|
|
101
|
+
.replace(/{userName}/g, input.userName);
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=negotiation.deadlock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"negotiation.deadlock.js","sourceRoot":"/","sources":["negotiation/negotiation.deadlock.ts"],"names":[],"mappings":"AAuBA,gFAAgF;AAEhF;;;;;GAKG;AACH,MAAM,UAAU,8BAA8B;IAC5C,OAAO,OAAO,CAAC,GAAG,CAAC,kCAAkC,KAAK,MAAM,CAAC;AACnE,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC;AAE5C;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AAExC;;;;;GAKG;AACH,MAAM,UAAU,2BAA2B;IACzC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC;IACvD,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,sBAAsB;YAAE,OAAO,MAAM,CAAC;IAClF,CAAC;IACD,OAAO,0BAA0B,CAAC;AACpC,CAAC;AAED,gFAAgF;AAEhF;;;;;;;GAOG;AACH,MAAM,sBAAsB,GAAwB,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;AAWrF;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAuD,EACvD,YAAoB,0BAA0B;IAE9C,MAAM,kBAAkB,GAAG,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,SAAS,IAAI,sBAAsB;QAC3F,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,0BAA0B,CAAC;IAE/B,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;QAClC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,sBAAsB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACrE,GAAG,IAAI,CAAC,CAAC;QACX,CAAC;aAAM,CAAC;YACN,MAAM;QACR,CAAC;IACH,CAAC;IAED,OAAO;QACL,UAAU,EAAE,GAAG,IAAI,kBAAkB;QACrC,wBAAwB,EAAE,GAAG;QAC7B,SAAS,EAAE,kBAAkB;KAC9B,CAAC;AACJ,CAAC;AAqBD,+EAA+E;AAE/E,MAAM,wBAAwB,GAAG;;;;;;;4EAO2C,CAAC;AAE7E,MAAM,8BAA8B,GAAG;4JACqH,CAAC;AAE7J;;;;;;;GAOG;AACH,MAAM,UAAU,4BAA4B,CAAC,KAK5C;IACC,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IAC7B,OAAO,wBAAwB;SAC5B,OAAO,CAAC,eAAe,EAAE,MAAM,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;SAChE,OAAO,CAAC,qBAAqB,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC,CAAC,EAAE,CAAC;SACtF,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;AAC5C,CAAC","sourcesContent":["/**\n * Deadlock detection + persuasion→bargaining mode shift (IND-428, backlog item 6).\n *\n * Grounding: Wells & Reed (2006), *Knowing When to Bargain* — a persuasion\n * dialogue (arguing the merits) that reaches a stalemate may execute a *legal\n * shift* into a negotiation dialogue (offering concessions). See\n * `docs/design/negotiation-dialogue-game.md` for the formal framing of the\n * turn protocol as a dialogue game.\n *\n * Design constraints (hard):\n * - **Deterministic**: deadlock is decided by pure inspection of the persisted\n * turn history — never by an LLM.\n * - **Stance, not rules**: a detected deadlock changes the system agent's\n * *drafting stance* only. Locutions, seat vocabularies (`allowedActionsFor`),\n * termination, and turn-cap semantics are untouched.\n * - **Default-off**: gated on `NEGOTIATION_DEADLOCK_SHIFT_ENABLED === \"true\"`\n * (strict literal) and applied only to v2 negotiations, checked alongside the\n * protocol-version plumbing. When off, the legacy path is byte-identical.\n * - **Fail-open**: any detection error means \"no deadlock\" — advisory\n * infrastructure never blocks a negotiation.\n */\nimport type { NegotiationTurn } from \"./negotiation.state.js\";\n\n// ─── Configuration ───────────────────────────────────────────────────────────\n\n/**\n * Whether the deadlock→bargaining mode shift is enabled, from the\n * `NEGOTIATION_DEADLOCK_SHIFT_ENABLED` env switch. Strict literal `\"true\"`\n * only — the deployment is byte-for-byte unchanged until the flag is flipped,\n * and rolling back is the same single switch.\n */\nexport function configuredDeadlockShiftEnabled(): boolean {\n return process.env.NEGOTIATION_DEADLOCK_SHIFT_ENABLED === \"true\";\n}\n\n/**\n * Default deadlock threshold: 4 consecutive non-convergent turns. Sized\n * against the ambient turn cap (6): an outreach plus 4 unbroken counters\n * leaves exactly the closing turns to draft in the bargaining stance.\n */\nexport const DEFAULT_DEADLOCK_THRESHOLD = 4;\n\n/**\n * Lower bound on the configurable threshold. Below 2 the \"stalemate\" signal is\n * meaningless — a single counter is ordinary dialogue, not a deadlock.\n */\nexport const MIN_DEADLOCK_THRESHOLD = 2;\n\n/**\n * Consecutive non-convergent turns that constitute a deadlock, from\n * `NEGOTIATION_DEADLOCK_THRESHOLD`. Must be an integer >= 2; invalid,\n * non-integer, or out-of-range values fall back to the default (fail-open\n * toward the documented behavior, mirroring `askUserAnswerWindowMs`).\n */\nexport function configuredDeadlockThreshold(): number {\n const raw = process.env.NEGOTIATION_DEADLOCK_THRESHOLD;\n if (raw) {\n const parsed = Number(raw);\n if (Number.isInteger(parsed) && parsed >= MIN_DEADLOCK_THRESHOLD) return parsed;\n }\n return DEFAULT_DEADLOCK_THRESHOLD;\n}\n\n// ─── Detection ───────────────────────────────────────────────────────────────\n\n/**\n * The locutions that count toward a stalemate: challenges and information\n * requests that keep the dialogue open without converging. Everything else —\n * openings (`propose`/`outreach`: a fresh case is on the table), terminal\n * actions (the game is deciding, not stalling), and `ask_user` (new principal\n * input is about to arrive) — RESETS the run to zero. Unknown/missing actions\n * also reset (conservative: never manufacture a deadlock from unreadable data).\n */\nconst NON_CONVERGENT_ACTIONS: ReadonlySet<string> = new Set([\"counter\", \"question\"]);\n\nexport interface DeadlockAssessment {\n /** True when the trailing non-convergent run has reached the threshold. */\n deadlocked: boolean;\n /** Length of the maximal trailing run of counter/question turns. */\n consecutiveNonConvergent: number;\n /** The threshold the run was compared against. */\n threshold: number;\n}\n\n/**\n * Deterministic stalemate detector: measures the maximal *trailing* run of\n * non-convergent turns (`counter`/`question`) in the persisted history and\n * compares it against the threshold. Continuation histories are included by\n * construction — the caller passes the full turn list, so a deadlock spanning\n * sessions still counts.\n *\n * Pure state inspection; no LLM, no I/O, no clock.\n */\nexport function assessDeadlock(\n history: ReadonlyArray<Pick<NegotiationTurn, \"action\">>,\n threshold: number = DEFAULT_DEADLOCK_THRESHOLD,\n): DeadlockAssessment {\n const effectiveThreshold = Number.isInteger(threshold) && threshold >= MIN_DEADLOCK_THRESHOLD\n ? threshold\n : DEFAULT_DEADLOCK_THRESHOLD;\n\n let run = 0;\n for (let i = history.length - 1; i >= 0; i--) {\n const action = history[i]?.action;\n if (typeof action === \"string\" && NON_CONVERGENT_ACTIONS.has(action)) {\n run += 1;\n } else {\n break;\n }\n }\n\n return {\n deadlocked: run >= effectiveThreshold,\n consecutiveNonConvergent: run,\n threshold: effectiveThreshold,\n };\n}\n\n// ─── Internal shift record (task metadata JSONB, never public) ──────────────\n\n/**\n * Analytical record of an applied shift, persisted to\n * `tasks.metadata.deadlockShift` via the optional `setTaskDeadlockShift`\n * database hook. Internal-only: negotiation API surfaces project specific\n * fields and never return task metadata verbatim (same privacy posture as\n * `metadata.screenDecision` and the QUD/uptake detection metadata).\n */\nexport interface DeadlockShiftRecord {\n reason: \"consecutive_non_convergent\";\n consecutiveNonConvergent: number;\n threshold: number;\n /** Zero-based session turn index at which the shifted draft happened. */\n shiftedAtTurn: number;\n seat: \"initiator\" | \"counterparty\";\n detectedAt: string;\n}\n\n// ─── Prompt section (system agent drafting stance) ──────────────────────────\n\nconst BARGAINING_SHIFT_SECTION = `\n\nDEADLOCK — SHIFT FROM PERSUASION TO BARGAINING. The last {consecutive} turns were counters/questions without convergence: the merits have been argued and restating them will not move the other side. For this turn, change stance:\n- Do NOT re-argue fit or repeat points already made.\n- Offer a concrete concession or scope reduction instead: a smaller first step (a single intro call, a scoped trial, a narrower version of the collaboration), dropping a contested requirement, or a trade on a dimension not yet contested.\n- Make the remaining objection priceable: name the specific smaller commitment that would resolve it.{askUserEscalation}\n- If no reduced scope would genuinely serve {userName}'s interests, conclude decisively with a terminal action from your allowed set rather than another repetitive counter.\nThis shift changes your stance only — your available actions are unchanged.`;\n\nconst BARGAINING_ASK_USER_ESCALATION = `\n- If a concession would require {userName}'s own input or permission (budget, availability, private details), escalate with \"ask_user\" instead of guessing.`;\n\n/**\n * Renders the bargaining-stance prompt section. Returns the empty string when\n * the shift is not active, so the rendered system prompt is byte-identical to\n * the legacy build on every non-shifted turn (mirrors\n * `renderNegotiatorMemorySection`). The `ask_user` escalation line renders\n * only when the caller already legally holds the action (`canAskUser`) — the\n * shift never invents a locution.\n */\nexport function renderBargainingShiftSection(input: {\n active: boolean;\n userName: string;\n canAskUser: boolean;\n consecutiveNonConvergent: number;\n}): string {\n if (!input.active) return \"\";\n return BARGAINING_SHIFT_SECTION\n .replace(\"{consecutive}\", String(input.consecutiveNonConvergent))\n .replace(\"{askUserEscalation}\", input.canAskUser ? BARGAINING_ASK_USER_ESCALATION : \"\")\n .replace(/{userName}/g, input.userName);\n}\n"]}
|
|
@@ -4,6 +4,7 @@ import type { NegotiationTimeoutQueue } from "../shared/interfaces/negotiation-e
|
|
|
4
4
|
import type { AgentDispatcher } from "../shared/interfaces/agent-dispatcher.interface.js";
|
|
5
5
|
import { type NegotiationTurn, type NegotiationOutcome, type UserNegotiationContext, type SeedAssessment, type NegotiationGraphLike } from "./negotiation.state.js";
|
|
6
6
|
import { type ScreenDecisionRecord } from "./negotiation.screen.js";
|
|
7
|
+
import { type DeadlockShiftRecord } from "./negotiation.deadlock.js";
|
|
7
8
|
import type { NegotiationProtocolVersion } from "../shared/schemas/negotiation-state.schema.js";
|
|
8
9
|
import type { QuestionerEnqueueFn } from "../questioner/questioner.types.js";
|
|
9
10
|
import type { ReflectEnqueueFn } from "./negotiation.reflect.js";
|
|
@@ -32,6 +33,7 @@ export declare class NegotiationGraphFactory {
|
|
|
32
33
|
discoveryQuery: string | undefined;
|
|
33
34
|
protocolVersion: NegotiationProtocolVersion;
|
|
34
35
|
screenDecision: ScreenDecisionRecord | null;
|
|
36
|
+
deadlockShift: DeadlockShiftRecord | null;
|
|
35
37
|
memoryBySide: Partial<Record<"source" | "candidate", NegotiatorMemoryEntry[]>>;
|
|
36
38
|
isContinuation: boolean;
|
|
37
39
|
opportunityId: string;
|
|
@@ -86,6 +88,7 @@ export declare class NegotiationGraphFactory {
|
|
|
86
88
|
discoveryQuery?: string | import("@langchain/langgraph").OverwriteValue<string | undefined> | undefined;
|
|
87
89
|
protocolVersion?: NegotiationProtocolVersion | import("@langchain/langgraph").OverwriteValue<NegotiationProtocolVersion> | undefined;
|
|
88
90
|
screenDecision?: ScreenDecisionRecord | import("@langchain/langgraph").OverwriteValue<ScreenDecisionRecord | null> | null | undefined;
|
|
91
|
+
deadlockShift?: DeadlockShiftRecord | import("@langchain/langgraph").OverwriteValue<DeadlockShiftRecord | null> | null | undefined;
|
|
89
92
|
memoryBySide?: Partial<Record<"source" | "candidate", NegotiatorMemoryEntry[]>> | import("@langchain/langgraph").OverwriteValue<Partial<Record<"source" | "candidate", NegotiatorMemoryEntry[]>>> | undefined;
|
|
90
93
|
isContinuation?: boolean | import("@langchain/langgraph").OverwriteValue<boolean> | undefined;
|
|
91
94
|
opportunityId?: string | import("@langchain/langgraph").OverwriteValue<string> | undefined;
|
|
@@ -148,7 +151,7 @@ export declare class NegotiationGraphFactory {
|
|
|
148
151
|
reason?: "timeout" | "turn_cap" | "screened_out" | undefined;
|
|
149
152
|
} | null> | null | undefined;
|
|
150
153
|
error?: string | import("@langchain/langgraph").OverwriteValue<string | null> | null | undefined;
|
|
151
|
-
}, "
|
|
154
|
+
}, "screen" | "turn" | "__start__" | "init" | "finalize", {
|
|
152
155
|
sourceUser: import("@langchain/langgraph").BaseChannel<UserNegotiationContext, UserNegotiationContext | import("@langchain/langgraph").OverwriteValue<UserNegotiationContext>, unknown>;
|
|
153
156
|
candidateUser: import("@langchain/langgraph").BaseChannel<UserNegotiationContext, UserNegotiationContext | import("@langchain/langgraph").OverwriteValue<UserNegotiationContext>, unknown>;
|
|
154
157
|
indexContext: import("@langchain/langgraph").BaseChannel<{
|
|
@@ -166,6 +169,7 @@ export declare class NegotiationGraphFactory {
|
|
|
166
169
|
discoveryQuery: import("@langchain/langgraph").BaseChannel<string | undefined, string | import("@langchain/langgraph").OverwriteValue<string | undefined> | undefined, unknown>;
|
|
167
170
|
protocolVersion: import("@langchain/langgraph").BaseChannel<NegotiationProtocolVersion, NegotiationProtocolVersion | import("@langchain/langgraph").OverwriteValue<NegotiationProtocolVersion>, unknown>;
|
|
168
171
|
screenDecision: import("@langchain/langgraph").BaseChannel<ScreenDecisionRecord | null, ScreenDecisionRecord | import("@langchain/langgraph").OverwriteValue<ScreenDecisionRecord | null> | null, unknown>;
|
|
172
|
+
deadlockShift: import("@langchain/langgraph").BaseChannel<DeadlockShiftRecord | null, DeadlockShiftRecord | import("@langchain/langgraph").OverwriteValue<DeadlockShiftRecord | null> | null, unknown>;
|
|
169
173
|
memoryBySide: import("@langchain/langgraph").BaseChannel<Partial<Record<"source" | "candidate", NegotiatorMemoryEntry[]>>, Partial<Record<"source" | "candidate", NegotiatorMemoryEntry[]>> | import("@langchain/langgraph").OverwriteValue<Partial<Record<"source" | "candidate", NegotiatorMemoryEntry[]>>>, unknown>;
|
|
170
174
|
isContinuation: import("@langchain/langgraph").BaseChannel<boolean, boolean | import("@langchain/langgraph").OverwriteValue<boolean>, unknown>;
|
|
171
175
|
opportunityId: import("@langchain/langgraph").BaseChannel<string, string | import("@langchain/langgraph").OverwriteValue<string>, unknown>;
|
|
@@ -269,6 +273,7 @@ export declare class NegotiationGraphFactory {
|
|
|
269
273
|
discoveryQuery: import("@langchain/langgraph").BaseChannel<string | undefined, string | import("@langchain/langgraph").OverwriteValue<string | undefined> | undefined, unknown>;
|
|
270
274
|
protocolVersion: import("@langchain/langgraph").BaseChannel<NegotiationProtocolVersion, NegotiationProtocolVersion | import("@langchain/langgraph").OverwriteValue<NegotiationProtocolVersion>, unknown>;
|
|
271
275
|
screenDecision: import("@langchain/langgraph").BaseChannel<ScreenDecisionRecord | null, ScreenDecisionRecord | import("@langchain/langgraph").OverwriteValue<ScreenDecisionRecord | null> | null, unknown>;
|
|
276
|
+
deadlockShift: import("@langchain/langgraph").BaseChannel<DeadlockShiftRecord | null, DeadlockShiftRecord | import("@langchain/langgraph").OverwriteValue<DeadlockShiftRecord | null> | null, unknown>;
|
|
272
277
|
memoryBySide: import("@langchain/langgraph").BaseChannel<Partial<Record<"source" | "candidate", NegotiatorMemoryEntry[]>>, Partial<Record<"source" | "candidate", NegotiatorMemoryEntry[]>> | import("@langchain/langgraph").OverwriteValue<Partial<Record<"source" | "candidate", NegotiatorMemoryEntry[]>>>, unknown>;
|
|
273
278
|
isContinuation: import("@langchain/langgraph").BaseChannel<boolean, boolean | import("@langchain/langgraph").OverwriteValue<boolean>, unknown>;
|
|
274
279
|
opportunityId: import("@langchain/langgraph").BaseChannel<string, string | import("@langchain/langgraph").OverwriteValue<string>, unknown>;
|
|
@@ -401,6 +406,7 @@ export declare class NegotiationGraphFactory {
|
|
|
401
406
|
discoveryQuery: import("@langchain/langgraph").BaseChannel<string | undefined, string | import("@langchain/langgraph").OverwriteValue<string | undefined> | undefined, unknown>;
|
|
402
407
|
protocolVersion: import("@langchain/langgraph").BaseChannel<NegotiationProtocolVersion, NegotiationProtocolVersion | import("@langchain/langgraph").OverwriteValue<NegotiationProtocolVersion>, unknown>;
|
|
403
408
|
screenDecision: import("@langchain/langgraph").BaseChannel<ScreenDecisionRecord | null, ScreenDecisionRecord | import("@langchain/langgraph").OverwriteValue<ScreenDecisionRecord | null> | null, unknown>;
|
|
409
|
+
deadlockShift: import("@langchain/langgraph").BaseChannel<DeadlockShiftRecord | null, DeadlockShiftRecord | import("@langchain/langgraph").OverwriteValue<DeadlockShiftRecord | null> | null, unknown>;
|
|
404
410
|
memoryBySide: import("@langchain/langgraph").BaseChannel<Partial<Record<"source" | "candidate", NegotiatorMemoryEntry[]>>, Partial<Record<"source" | "candidate", NegotiatorMemoryEntry[]>> | import("@langchain/langgraph").OverwriteValue<Partial<Record<"source" | "candidate", NegotiatorMemoryEntry[]>>>, unknown>;
|
|
405
411
|
isContinuation: import("@langchain/langgraph").BaseChannel<boolean, boolean | import("@langchain/langgraph").OverwriteValue<boolean>, unknown>;
|
|
406
412
|
opportunityId: import("@langchain/langgraph").BaseChannel<string, string | import("@langchain/langgraph").OverwriteValue<string>, unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"negotiation.graph.d.ts","sourceRoot":"/","sources":["negotiation/negotiation.graph.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,4CAA4C,CAAC;AAC/F,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AAC3F,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sDAAsD,CAAC;AACpG,OAAO,KAAK,EAAE,eAAe,EAA0B,MAAM,oDAAoD,CAAC;AAClH,OAAO,EAAyB,KAAK,eAAe,EAAE,KAAK,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,KAAK,cAAc,EAAE,KAAK,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAG3L,OAAO,EAAkE,KAAK,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpI,OAAO,KAAK,EAAmB,0BAA0B,EAAE,MAAM,+CAA+C,CAAC;AAEjH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,qBAAqB,EAAE,0BAA0B,EAAyB,MAAM,yBAAyB,CAAC;AAuExH;;;GAGG;AACH,qBAAa,uBAAuB;IAEhC,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,YAAY,CAAC;IACrB,OAAO,CAAC,iBAAiB,CAAC;IAC1B,OAAO,CAAC,cAAc,CAAC;IACvB,OAAO,CAAC,cAAc,CAAC;gBALf,QAAQ,EAAE,wBAAwB,EAClC,UAAU,EAAE,eAAe,EAC3B,YAAY,CAAC,EAAE,uBAAuB,YAAA,EACtC,iBAAiB,CAAC,EAAE,mBAAmB,YAAA,EACvC,cAAc,CAAC,EAAE,gBAAgB,YAAA,EACjC,cAAc,CAAC,EAAE,0BAA0B,YAAA;IAGrD,WAAW
|
|
1
|
+
{"version":3,"file":"negotiation.graph.d.ts","sourceRoot":"/","sources":["negotiation/negotiation.graph.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,4CAA4C,CAAC;AAC/F,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AAC3F,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sDAAsD,CAAC;AACpG,OAAO,KAAK,EAAE,eAAe,EAA0B,MAAM,oDAAoD,CAAC;AAClH,OAAO,EAAyB,KAAK,eAAe,EAAE,KAAK,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,KAAK,cAAc,EAAE,KAAK,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAG3L,OAAO,EAAkE,KAAK,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpI,OAAO,EAAwG,KAAK,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAC3K,OAAO,KAAK,EAAmB,0BAA0B,EAAE,MAAM,+CAA+C,CAAC;AAEjH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,qBAAqB,EAAE,0BAA0B,EAAyB,MAAM,yBAAyB,CAAC;AAuExH;;;GAGG;AACH,qBAAa,uBAAuB;IAEhC,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,YAAY,CAAC;IACrB,OAAO,CAAC,iBAAiB,CAAC;IAC1B,OAAO,CAAC,cAAc,CAAC;IACvB,OAAO,CAAC,cAAc,CAAC;gBALf,QAAQ,EAAE,wBAAwB,EAClC,UAAU,EAAE,eAAe,EAC3B,YAAY,CAAC,EAAE,uBAAuB,YAAA,EACtC,iBAAiB,CAAC,EAAE,mBAAmB,YAAA,EACvC,cAAc,CAAC,EAAE,gBAAgB,YAAA,EACjC,cAAc,CAAC,EAAE,0BAA0B,YAAA;IAGrD,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA46BZ;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,sBAAsB,CAAC;IACtC,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,kBAAkB,CAAC,aAAa,CAAC,CAAC;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,KAAK,EAAE;IAC1C,SAAS,EAAE,oBAAoB,CAAC;IAChC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACnC,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpB;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,gBAAgB,EAAE,oBAAoB,EACtC,UAAU,EAAE,sBAAsB,EAClC,UAAU,EAAE,oBAAoB,EAAE,EAClC,YAAY,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACnD,IAAI,CAAC,EAAE;IACL,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,qBAAqB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,qBAAqB,CAAC;IAC5C,OAAO,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACrC;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GACA,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAsJ9B"}
|
|
@@ -5,6 +5,7 @@ import { NegotiationGraphState } from "./negotiation.state.js";
|
|
|
5
5
|
import { IndexNegotiator } from "./negotiation.agent.js";
|
|
6
6
|
import { ASK_USER_LOCK_SLACK_MS, allowedActionsFor, askUserAnswerWindowMs, configuredAskUserEnabled, configuredProtocolVersion, fallbackActionFor, isRejectLikeAction, isTerminalAction, readProtocolVersion, rejectActionFor } from "./negotiation.protocol.js";
|
|
7
7
|
import { NegotiationScreener, configuredScreenMode } from "./negotiation.screen.js";
|
|
8
|
+
import { assessDeadlock, configuredDeadlockShiftEnabled, configuredDeadlockThreshold } from "./negotiation.deadlock.js";
|
|
8
9
|
import { protocolLogger } from "../shared/observability/protocol.logger.js";
|
|
9
10
|
const logger = protocolLogger("NegotiationGraph");
|
|
10
11
|
const initLog = protocolLogger("NegotiationGraph:Init");
|
|
@@ -416,6 +417,28 @@ export class NegotiationGraphFactory {
|
|
|
416
417
|
&& !!state.opportunityId
|
|
417
418
|
&& !(state.turnCount === 0 && !state.isContinuation)
|
|
418
419
|
&& !hasPriorAskUser(state.messages, ownUser.id);
|
|
420
|
+
// ─── Deadlock detection → persuasion→bargaining stance (IND-428) ──────
|
|
421
|
+
// Deterministic trailing-run inspection of the persisted history — no
|
|
422
|
+
// LLM in the decision. Gated on the strict default-off flag AND v2,
|
|
423
|
+
// checked alongside the protocol-version plumbing so v1 semantics stay
|
|
424
|
+
// untouched. Fail-open: any detection error means "no deadlock" and
|
|
425
|
+
// the legacy path proceeds byte-identically. The shift changes the
|
|
426
|
+
// system agent's drafting stance only — allowedActions, the dispatch
|
|
427
|
+
// payload, and all termination rules are untouched.
|
|
428
|
+
let deadlock = null;
|
|
429
|
+
if (version === 'v2' && configuredDeadlockShiftEnabled()) {
|
|
430
|
+
try {
|
|
431
|
+
deadlock = assessDeadlock(history, configuredDeadlockThreshold());
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
turnLog.warn('Deadlock detection failed; proceeding without mode shift', {
|
|
435
|
+
taskId: state.taskId,
|
|
436
|
+
error: err instanceof Error ? err.message : String(err),
|
|
437
|
+
});
|
|
438
|
+
deadlock = null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const bargainingMode = deadlock?.deadlocked === true;
|
|
419
442
|
// P5.3: the speaker's own negotiator memory (cached per side across
|
|
420
443
|
// turns). Injected into both the dispatch payload (the user's own
|
|
421
444
|
// agent — scope-correct) and the system-agent prompt.
|
|
@@ -491,6 +514,7 @@ export class NegotiationGraphFactory {
|
|
|
491
514
|
isContinuation: state.isContinuation,
|
|
492
515
|
...(state.userAnswers.length > 0 && { userAnswers: state.userAnswers }),
|
|
493
516
|
...(askUserAvailable && { canAskUser: true }),
|
|
517
|
+
...(bargainingMode && { bargaining: { consecutiveNonConvergent: deadlock.consecutiveNonConvergent } }),
|
|
494
518
|
...(ownMemory.length > 0 && { memory: ownMemory }),
|
|
495
519
|
});
|
|
496
520
|
}
|
|
@@ -516,6 +540,47 @@ export class NegotiationGraphFactory {
|
|
|
516
540
|
});
|
|
517
541
|
turn = { ...turn, action: fallbackActionFor(version, seat, isFinalTurn) };
|
|
518
542
|
}
|
|
543
|
+
// ─── Deadlock shift record (IND-428) ───────────────────────────────
|
|
544
|
+
// Applied-stance analytics: recorded once per session, on the first
|
|
545
|
+
// turn actually drafted in the bargaining stance (the system agent —
|
|
546
|
+
// externally dispatched turns never receive the stance). Internal
|
|
547
|
+
// metadata only: persisted to tasks.metadata.deadlockShift via the
|
|
548
|
+
// optional hook; negotiation API surfaces project specific fields and
|
|
549
|
+
// never return task metadata verbatim. Every step fails open.
|
|
550
|
+
const bargainingApplied = bargainingMode && !dispatchResult.handled;
|
|
551
|
+
let deadlockShiftRecord = null;
|
|
552
|
+
if (bargainingApplied && !state.deadlockShift) {
|
|
553
|
+
deadlockShiftRecord = {
|
|
554
|
+
reason: 'consecutive_non_convergent',
|
|
555
|
+
consecutiveNonConvergent: deadlock.consecutiveNonConvergent,
|
|
556
|
+
threshold: deadlock.threshold,
|
|
557
|
+
shiftedAtTurn: state.turnCount,
|
|
558
|
+
seat,
|
|
559
|
+
detectedAt: new Date().toISOString(),
|
|
560
|
+
};
|
|
561
|
+
await database.setTaskDeadlockShift?.(state.taskId, deadlockShiftRecord).catch((err) => {
|
|
562
|
+
turnLog.error('Failed to persist deadlock shift record', { taskId: state.taskId, error: err });
|
|
563
|
+
});
|
|
564
|
+
turnLog.info('negotiation_deadlock_shift', {
|
|
565
|
+
taskId: state.taskId,
|
|
566
|
+
opportunityId: state.opportunityId || undefined,
|
|
567
|
+
seat,
|
|
568
|
+
consecutiveNonConvergent: deadlockShiftRecord.consecutiveNonConvergent,
|
|
569
|
+
threshold: deadlockShiftRecord.threshold,
|
|
570
|
+
turnIndex: state.turnCount,
|
|
571
|
+
});
|
|
572
|
+
if (state.opportunityId) {
|
|
573
|
+
emitWide({
|
|
574
|
+
type: 'negotiation_deadlock_shift',
|
|
575
|
+
opportunityId: state.opportunityId,
|
|
576
|
+
negotiationConversationId: state.conversationId,
|
|
577
|
+
turnIndex: state.turnCount,
|
|
578
|
+
actor: isSource ? 'source' : 'candidate',
|
|
579
|
+
consecutiveNonConvergent: deadlockShiftRecord.consecutiveNonConvergent,
|
|
580
|
+
threshold: deadlockShiftRecord.threshold,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
519
584
|
const parts = [{ kind: "data", data: turn }];
|
|
520
585
|
const message = await database.createMessage({
|
|
521
586
|
conversationId: state.conversationId,
|
|
@@ -604,6 +669,7 @@ export class NegotiationGraphFactory {
|
|
|
604
669
|
turnCount: state.turnCount + 1,
|
|
605
670
|
lastTurn: turn,
|
|
606
671
|
status: 'input_required',
|
|
672
|
+
...(deadlockShiftRecord && { deadlockShift: deadlockShiftRecord }),
|
|
607
673
|
};
|
|
608
674
|
}
|
|
609
675
|
await database.updateTaskState(state.taskId, "working");
|
|
@@ -633,6 +699,7 @@ export class NegotiationGraphFactory {
|
|
|
633
699
|
currentSpeaker: (isSource ? "candidate" : "source"),
|
|
634
700
|
lastTurn: turn,
|
|
635
701
|
memoryBySide: { [ownSide]: ownMemory },
|
|
702
|
+
...(deadlockShiftRecord && { deadlockShift: deadlockShiftRecord }),
|
|
636
703
|
};
|
|
637
704
|
}
|
|
638
705
|
catch (err) {
|