@indexnetwork/protocol 21.1.0-rc.492.1 → 22.0.0-rc.494.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/chat/chat-streaming.types.d.ts +2 -23
  3. package/dist/chat/chat-streaming.types.js +0 -3
  4. package/dist/chat/chat.agent.d.ts +0 -5
  5. package/dist/chat/chat.streamer.js +3 -6
  6. package/dist/chat/negotiator.persona.d.ts +1 -1
  7. package/dist/chat/negotiator.persona.js +0 -7
  8. package/dist/chat/negotiator.prompt.js +10 -12
  9. package/dist/chat/onboarding.persona.d.ts +1 -1
  10. package/dist/chat/onboarding.persona.js +0 -1
  11. package/dist/chat/onboarding.prompt.js +2 -2
  12. package/dist/chat/signal.persona.d.ts +1 -1
  13. package/dist/chat/signal.persona.js +1 -2
  14. package/dist/chat/signal.prompt.d.ts +6 -5
  15. package/dist/chat/signal.prompt.js +17 -37
  16. package/dist/index.d.ts +4 -6
  17. package/dist/index.js +3 -3
  18. package/dist/intents/graph/intent.graph.d.ts +1 -2
  19. package/dist/intents/graph/intent.graph.execute.js +0 -15
  20. package/dist/intents/graph/intent.graph.js +1 -2
  21. package/dist/intents/graph/intent.graph.shared.d.ts +0 -2
  22. package/dist/intents/intent.module.d.ts +0 -3
  23. package/dist/intents/intent.module.js +2 -2
  24. package/dist/mcp/mcp.authorization-policy.d.ts +2 -2
  25. package/dist/mcp/mcp.authorization-policy.js +0 -24
  26. package/dist/negotiations/negotiation.answer-consumption.d.ts +223 -0
  27. package/dist/negotiations/negotiation.answer-consumption.js +275 -0
  28. package/dist/negotiations/negotiation.module.d.ts +2 -0
  29. package/dist/negotiations/negotiation.module.js +1 -0
  30. package/dist/negotiations/negotiation.stance.contracts.d.ts +5 -5
  31. package/dist/negotiations/negotiation.stance.contracts.js +39 -6
  32. package/dist/opportunities/opportunity.graph.d.ts +1 -1
  33. package/dist/opportunities/opportunity.tools.cards.d.ts +0 -13
  34. package/dist/opportunities/opportunity.tools.cards.js +0 -22
  35. package/dist/opportunities/opportunity.tools.js +4 -65
  36. package/dist/opportunities/opportunity.tools.port.d.ts +1 -1
  37. package/dist/questions/question.env.d.ts +5 -35
  38. package/dist/questions/question.env.js +5 -70
  39. package/dist/questions/question.input.d.ts +36 -145
  40. package/dist/questions/question.input.js +1 -56
  41. package/dist/questions/question.module.d.ts +10 -22
  42. package/dist/questions/question.module.js +9 -19
  43. package/dist/shared/agent/activity-projection.d.ts +2 -2
  44. package/dist/shared/agent/tool.factory.js +3 -15
  45. package/dist/shared/agent/tool.helpers.d.ts +0 -73
  46. package/dist/shared/agent/tool.registry.js +0 -2
  47. package/dist/shared/agent/tool.runtime.d.ts +1 -1
  48. package/dist/shared/agent/tool.runtime.js +3 -14
  49. package/dist/shared/observability/request-context.d.ts +0 -5
  50. package/package.json +1 -1
  51. package/dist/opportunities/opportunity.pending-questions.d.ts +0 -33
  52. package/dist/opportunities/opportunity.pending-questions.js +0 -42
  53. package/dist/questions/question.agent.d.ts +0 -28
  54. package/dist/questions/question.agent.js +0 -136
  55. package/dist/questions/question.ask.tool.d.ts +0 -12
  56. package/dist/questions/question.ask.tool.js +0 -297
  57. package/dist/questions/question.persistence.port.d.ts +0 -84
  58. package/dist/questions/question.persistence.port.js +0 -1
  59. package/dist/questions/question.presets.d.ts +0 -21
  60. package/dist/questions/question.presets.js +0 -388
  61. package/dist/questions/question.tools.d.ts +0 -15
  62. package/dist/questions/question.tools.js +0 -216
  63. package/dist/questions/question.tools.port.d.ts +0 -12
  64. package/dist/questions/question.tools.port.js +0 -1
  65. package/dist/shared/schemas/pending-question.schema.d.ts +0 -28
  66. package/dist/shared/schemas/pending-question.schema.js +0 -1
@@ -1,297 +0,0 @@
1
- /**
2
- * questions/question.ask.tool — foreground adapter: chat ask_user_question tool.
3
- *
4
- * Blocking mid-conversation questions for the chat orchestrator
5
- * (AskUserQuestion-style human-in-the-loop).
6
- *
7
- * Flow (hybrid authoring):
8
- * 1. The orchestrator states what it needs to learn (`purpose`) plus optional
9
- * draft questions.
10
- * 2. QuestionerAgent (mode `chat`) refines that into polished structured
11
- * questions, grounded in the recent conversation excerpt and the user's
12
- * global context.
13
- * 3. Questions are persisted (`questions` table, mode `chat`,
14
- * `conversationId = sessionId`) via the injected {@link ChatQuestionsHost}.
15
- * 4. A `user_question` trace event streams the persisted questions to the
16
- * frontend, which renders them inline while the turn stays open.
17
- * 5. The tool blocks on `awaitAnswers` until the user answers/dismisses
18
- * through the questions REST endpoints, the wait budget elapses, or the
19
- * run is aborted. Answers come back as the tool result so the model
20
- * continues the SAME turn.
21
- *
22
- * On timeout the questions remain `pending`: they survive reloads via the
23
- * conversation-linked question fetch, and a later answer re-enters the chat
24
- * as a new user turn (frontend responsibility).
25
- *
26
- * Foreground adapter: registered by `createChatTools` only when
27
- * `deps.chatQuestions` is provided — never part of the MCP tool registry
28
- * (MCP clients have their own elicitation surface).
29
- */
30
- import { z } from "zod";
31
- import { error, success } from "../shared/agent/tool.helpers.js";
32
- import { requestContext } from "../shared/observability/request-context.js";
33
- import { protocolLogger } from "../shared/observability/protocol.logger.js";
34
- import { QuestionerAgent } from "./question.agent.js";
35
- import { chatQuestionWaitTimeoutMs } from "./question.env.js";
36
- const logger = protocolLogger("AskUserQuestionTool");
37
- /** Heartbeat interval while blocked, so SSE transports do not idle out. */
38
- const WAIT_HEARTBEAT_MS = 15000;
39
- /** Messages included in the conversation excerpt fed to the QuestionerAgent. */
40
- const EXCERPT_MESSAGE_COUNT = 10;
41
- /**
42
- * Fetch window for the excerpt. Host adapters return the FIRST N messages
43
- * (ascending) when a limit is passed, so we fetch a wide window and keep the
44
- * tail to get the most recent exchange.
45
- */
46
- const EXCERPT_FETCH_LIMIT = 100;
47
- /** Max characters per message inside the excerpt. */
48
- const EXCERPT_MESSAGE_CHARS = 400;
49
- // Lazy singleton — construction binds the LLM once; invocations are stateless.
50
- let questionerAgent = null;
51
- function getQuestionerAgent() {
52
- if (!questionerAgent)
53
- questionerAgent = new QuestionerAgent();
54
- return questionerAgent;
55
- }
56
- /** Test seam: replace or reset the module-level QuestionerAgent singleton. */
57
- export function setQuestionerAgentForTesting(agent) {
58
- questionerAgent = agent;
59
- }
60
- const draftQuestionSchema = z.object({
61
- prompt: z
62
- .string()
63
- .min(5)
64
- .max(400)
65
- .describe("The question to ask, ending in a question mark. Self-contained plain language."),
66
- options: z
67
- .array(z.string().min(1).max(120))
68
- .min(2)
69
- .max(4)
70
- .optional()
71
- .describe("2-4 mutually distinct answer options. Omit to let the question generator derive them."),
72
- multiSelect: z
73
- .boolean()
74
- .optional()
75
- .describe("True when several options can be picked together (priorities, bundles)."),
76
- });
77
- /**
78
- * Build a fallback Question directly from an orchestrator draft when the
79
- * QuestionerAgent produced nothing. Requires the draft to carry options.
80
- */
81
- function questionFromDraft(draft, index) {
82
- if (!draft.options || draft.options.length < 2)
83
- return null;
84
- return {
85
- title: `Question ${index + 1}`,
86
- prompt: draft.prompt.slice(0, 400),
87
- options: draft.options.slice(0, 4).map((label) => ({
88
- label: label.slice(0, 120),
89
- description: label.slice(0, 280),
90
- })),
91
- multiSelect: draft.multiSelect ?? false,
92
- };
93
- }
94
- /**
95
- * Creates the chat-only `ask_user_question` tool.
96
- *
97
- * @param defineTool - Tool factory provided by the composition root.
98
- * @param deps - Shared tool dependencies; requires `chatQuestions`.
99
- */
100
- export function createAskUserQuestionTools(defineTool, deps) {
101
- const askUserQuestion = defineTool({
102
- name: "ask_user_question",
103
- description: "Ask the user 1-3 structured clarifying questions and WAIT for their answer before continuing. " +
104
- "The conversation pauses: the user sees interactive question cards inline and your turn resumes " +
105
- "with their selections as the tool result.\n\n" +
106
- "**Use when** a decision materially changes what you do next — before an expensive operation " +
107
- "(discovery, creating an intent from ambiguous input), when facing meaningfully different " +
108
- "directions, or when one concrete missing detail (timing, scope, budget, format) blocks progress.\n\n" +
109
- "**Do not use** for facts already visible in the conversation or profile, procedural " +
110
- "confirmations (\"Should I proceed?\"), or open-ended questions better asked in your response text.\n\n" +
111
- "**Input:** `purpose` states what you need to learn and why. Optionally propose `questions` " +
112
- "drafts (prompt + 2-4 options); a question generator refines wording and option quality.\n\n" +
113
- "**Returns:** One entry per question with `status` (`answered`/`dismissed`/`timeout`) and the " +
114
- "user's `selectedOptions`/`freeText`. On `timeout` the questions stay visible in the " +
115
- "conversation — acknowledge briefly and end your turn; do NOT repeat the questions in text.",
116
- querySchema: z.object({
117
- purpose: z
118
- .string()
119
- .min(10)
120
- .max(600)
121
- .describe("What you need to learn from the user and why it changes what you do next."),
122
- questions: z
123
- .array(draftQuestionSchema)
124
- .min(1)
125
- .max(3)
126
- .optional()
127
- .describe("Draft questions to ask. The question generator polishes them before display."),
128
- }),
129
- handler: async ({ context, query }) => {
130
- const host = deps.chatQuestions;
131
- if (!host) {
132
- return error("Interactive questions are not available in this environment. Ask the user directly in your response text instead.");
133
- }
134
- if (context.isMcp || !context.sessionId) {
135
- return error("Interactive questions require a live chat session. Ask the user directly in your response text instead.");
136
- }
137
- const store = requestContext.getStore();
138
- const emit = store?.traceEmitter;
139
- const signal = store?.abortSignal;
140
- if (!emit) {
141
- return error("Interactive questions require a streaming chat turn. Ask the user directly in your response text instead.");
142
- }
143
- const sessionId = context.sessionId;
144
- // ── 1. Gather grounding context ────────────────────────────────────
145
- const [conversationExcerpt, userContext] = await Promise.all([
146
- loadConversationExcerpt(deps, sessionId),
147
- deps.getUserContextText?.(context.userId).catch(() => "") ?? Promise.resolve(""),
148
- ]);
149
- // ── 2. Generate polished questions (hybrid: drafts + QuestionerAgent) ──
150
- const chatContext = {
151
- purpose: query.purpose,
152
- ...(query.questions?.length ? { draftQuestions: query.questions } : {}),
153
- ...(conversationExcerpt ? { conversationExcerpt } : {}),
154
- ...(userContext ? { userContext } : {}),
155
- };
156
- let generated = null;
157
- try {
158
- generated = await getQuestionerAgent().invoke({
159
- mode: "chat",
160
- userId: context.userId,
161
- sourceType: "conversation",
162
- sourceId: sessionId,
163
- context: chatContext,
164
- conversationId: sessionId,
165
- }, signal ? { signal } : undefined);
166
- }
167
- catch (err) {
168
- logger.warn("QuestionerAgent invocation failed", {
169
- error: err instanceof Error ? err.message : String(err),
170
- });
171
- }
172
- let finalQuestions;
173
- let strategies;
174
- let underspecificationTypes;
175
- if (generated && generated.questions.length > 0) {
176
- finalQuestions = generated.questions;
177
- strategies = generated.strategies;
178
- underspecificationTypes = generated.underspecificationTypes;
179
- }
180
- else {
181
- const fromDrafts = (query.questions ?? [])
182
- .map((d, i) => questionFromDraft(d, i))
183
- .filter((q) => q !== null);
184
- if (fromDrafts.length === 0) {
185
- return error("Could not prepare structured questions. Ask the user directly in your response text instead.");
186
- }
187
- finalQuestions = fromDrafts;
188
- strategies = fromDrafts.map(() => "surface_missing_detail");
189
- underspecificationTypes = fromDrafts.map(() => null);
190
- }
191
- if (signal?.aborted) {
192
- return error("The chat turn was cancelled before the questions could be shown.");
193
- }
194
- // ── 3. Persist (mode `chat`, linked to this conversation) ──────────
195
- const timestamp = new Date().toISOString();
196
- const batch = finalQuestions.map((payload, i) => ({
197
- detection: {
198
- mode: "chat",
199
- sourceType: "conversation",
200
- sourceId: sessionId,
201
- timestamp,
202
- },
203
- actors: [{ userId: context.userId, role: "subject" }],
204
- payload,
205
- strategy: strategies[i] ?? "surface_missing_detail",
206
- underspecificationType: underspecificationTypes[i] ?? null,
207
- conversationId: sessionId,
208
- }));
209
- let persisted;
210
- try {
211
- persisted = await host.persist(batch);
212
- }
213
- catch (err) {
214
- logger.error("Failed to persist chat questions", {
215
- error: err instanceof Error ? err.message : String(err),
216
- });
217
- return error("Could not deliver the questions to the user. Ask directly in your response text instead.");
218
- }
219
- // ── 4. Stream the cards to the frontend ────────────────────────────
220
- emit({
221
- type: "user_question",
222
- questions: persisted.map((q) => ({ id: q.id })),
223
- });
224
- // ── 5. Block until answered / dismissed / timeout / abort ──────────
225
- const heartbeat = setInterval(() => {
226
- try {
227
- emit({ type: "status", message: "Waiting for your answer…" });
228
- }
229
- catch {
230
- /* stream may be closing; the wait resolves via timeout/abort */
231
- }
232
- }, WAIT_HEARTBEAT_MS);
233
- let outcomes;
234
- try {
235
- outcomes = await host.awaitAnswers(persisted.map((q) => q.id), { timeoutMs: chatQuestionWaitTimeoutMs(), ...(signal ? { signal } : {}) });
236
- }
237
- finally {
238
- clearInterval(heartbeat);
239
- }
240
- const byId = new Map(persisted.map((q) => [q.id, q]));
241
- const results = outcomes.map((o) => {
242
- const q = byId.get(o.questionId);
243
- return {
244
- questionId: o.questionId,
245
- prompt: q?.payload.prompt ?? "",
246
- status: o.status,
247
- ...(o.answer
248
- ? {
249
- selectedOptions: o.answer.selectedOptions,
250
- ...(o.answer.freeText ? { freeText: o.answer.freeText } : {}),
251
- }
252
- : {}),
253
- };
254
- });
255
- const answeredCount = results.filter((r) => r.status === "answered").length;
256
- const timedOut = results.some((r) => r.status === "timeout");
257
- return success({
258
- answers: results,
259
- summary: `${answeredCount} of ${results.length} question(s) answered`,
260
- ...(timedOut
261
- ? {
262
- guidance: "The user has not answered the remaining question(s) yet. They stay visible in the conversation — acknowledge briefly, do NOT repeat the questions in text, and end your turn.",
263
- }
264
- : {}),
265
- });
266
- },
267
- });
268
- return [askUserQuestion];
269
- }
270
- /**
271
- * Load a compact excerpt of the most recent conversation messages for the
272
- * QuestionerAgent's grounding. Best-effort: returns "" on any failure or when
273
- * no chat session reader is available.
274
- */
275
- async function loadConversationExcerpt(deps, sessionId) {
276
- if (!deps.chatSession)
277
- return "";
278
- try {
279
- const messages = await deps.chatSession.getSessionMessages(sessionId, EXCERPT_FETCH_LIMIT);
280
- if (!messages || messages.length === 0)
281
- return "";
282
- return messages
283
- .slice(-EXCERPT_MESSAGE_COUNT)
284
- .map((m) => {
285
- const role = m.role === "assistant" ? "Assistant" : "User";
286
- const text = (m.content ?? "").replace(/\s+/g, " ").trim();
287
- return `${role}: ${text.slice(0, EXCERPT_MESSAGE_CHARS)}`;
288
- })
289
- .join("\n");
290
- }
291
- catch (err) {
292
- logger.warn("Failed to load conversation excerpt", {
293
- error: err instanceof Error ? err.message : String(err),
294
- });
295
- return "";
296
- }
297
- }
@@ -1,84 +0,0 @@
1
- /**
2
- * questions/question.persistence.port — question persistence contracts.
3
- *
4
- * Protocol-level persistence contract for structured questions generated by
5
- * QuestionerAgent. Implementations live in the backend and are injected into
6
- * ProtocolDeps.
7
- */
8
- import type { Question, QuestionMode, QuestionPurpose, QuestionStrategy, QuestionDetection, QuestionActor, QuestionAnswer, UnderspecificationType } from "./question.schema.js";
9
- /** Shape accepted by `persist()` — everything needed to insert a question row. */
10
- export interface PersistableQuestion {
11
- detection: QuestionDetection;
12
- actors: QuestionActor[];
13
- payload: Question;
14
- strategy: QuestionStrategy;
15
- /** Internal QUD repair category; null when the question repairs no underspecification. */
16
- underspecificationType?: UnderspecificationType | null;
17
- /** Conversation ID — set when the question originates from a chat session. */
18
- conversationId?: string;
19
- }
20
- /** Shape returned by `findPending()` — a persisted question with its DB id and status. */
21
- export interface PersistedQuestion {
22
- id: string;
23
- detection: QuestionDetection;
24
- actors: QuestionActor[];
25
- payload: Question;
26
- status: "pending" | "answered" | "dismissed";
27
- answer: QuestionAnswer | null;
28
- createdAt: string;
29
- }
30
- /** Optional filters for `findPending()`. */
31
- export interface QuestionFilters {
32
- mode?: QuestionMode;
33
- purpose?: QuestionPurpose;
34
- sourceType?: string;
35
- sourceId?: string;
36
- /** Optional selected-intent scope. When `scopeType === 'intent'`, `scopeId` is the selected intent id. */
37
- scopeType?: 'intent';
38
- scopeId?: string;
39
- }
40
- /**
41
- * Resolution outcome for a single awaited chat question.
42
- * `timeout` means the wait budget elapsed (or the run was aborted) before the
43
- * user responded — the question stays `pending` in the database.
44
- */
45
- export interface ChatQuestionAnswerOutcome {
46
- questionId: string;
47
- status: "answered" | "dismissed" | "timeout";
48
- answer?: QuestionAnswer;
49
- }
50
- /**
51
- * Host bridge for the orchestrator's blocking `ask_user_question` tool.
52
- * `persist` synchronously inserts chat-mode question rows; `awaitAnswers`
53
- * blocks until every question resolves (answer/dismiss via the questions
54
- * REST endpoints) or the wait times out / the run is aborted.
55
- *
56
- * Implementations live in the backend (in-memory per-question wait bus,
57
- * single-instance semantics like the chat steer/queue interrupt emitter).
58
- */
59
- export interface ChatQuestionsHost {
60
- persist(batch: PersistableQuestion[]): Promise<PersistedQuestion[]>;
61
- awaitAnswers(questionIds: string[], opts?: {
62
- timeoutMs?: number;
63
- signal?: AbortSignal;
64
- }): Promise<ChatQuestionAnswerOutcome[]>;
65
- }
66
- /**
67
- * Narrow persistence port for ambient question delivery adapters.
68
- * Inject this instead of QuestionerDatabase when only persistence is needed.
69
- */
70
- export interface QuestionerDatabase {
71
- /** Persist a batch of generated questions (up to 3 per generation).
72
- * @returns The IDs of the inserted rows. */
73
- persist(questions: PersistableQuestion[]): Promise<string[]>;
74
- /** Find pending questions for a user, optionally filtered by mode/source. */
75
- findPending(userId: string, filters?: QuestionFilters): Promise<PersistedQuestion[]>;
76
- /** Record an answer for a question. Sets status to "answered".
77
- * Only succeeds if the user is an actor on a pending question.
78
- * @returns `true` if updated, `false` if not found, not pending, or unauthorized. */
79
- answer(questionId: string, userId: string, answer: QuestionAnswer): Promise<boolean>;
80
- /** Dismiss a question. Sets status to "dismissed".
81
- * Only succeeds if the user is an actor on a pending question.
82
- * @returns `true` if updated, `false` if not found, not pending, or unauthorized. */
83
- dismiss(questionId: string, userId: string): Promise<boolean>;
84
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,21 +0,0 @@
1
- /**
2
- * questions/question.presets — mode presets for QuestionerAgent.
3
- *
4
- * Each preset provides a system prompt and a buildPrompt function that assembles
5
- * the user message from a typed context object.
6
- */
7
- import type { QuestionMode, QuestionPurpose } from "./question.schema.js";
8
- export interface QuestionerPreset {
9
- /** The LLM system prompt for this mode. */
10
- systemPrompt: string;
11
- /** Builds the user-message string from the mode-specific context. */
12
- buildPrompt: (context: unknown) => string;
13
- }
14
- /**
15
- * Retrieve the preset for the given mode.
16
- * @param mode - The question mode to look up.
17
- * @param purpose - Optional purpose discriminant (for intent-recovery variant).
18
- * @returns The matching preset with systemPrompt and buildPrompt.
19
- * @throws Error if the mode's preset is not yet implemented.
20
- */
21
- export declare function getPreset(mode: QuestionMode, purpose?: QuestionPurpose): QuestionerPreset;