@indexnetwork/protocol 21.0.0-rc.490.1 → 21.1.0-rc.491.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 CHANGED
@@ -20,6 +20,27 @@ went 6.7.1 → 8.0.2 with no 7.x in between because the whole 7.x line shipped a
20
20
  prereleases between the two promotions. To track every change, read `rc`; to
21
21
  pin a supported release, use `latest`.
22
22
 
23
+ ## 21.1.0 - 2026-08-18
24
+
25
+ ### Added
26
+
27
+ - The **question block contract** for conversational questions
28
+ (`docs/plans/2026-08-18-conversational-questions.md`):
29
+ `QuestionBlockSchema`, `QuestionBlockQuestionSchema`,
30
+ `parseQuestionMessage`, `serializeQuestionMessage`,
31
+ `QUESTION_BLOCK_MARKER`, `QUESTION_BLOCK_VERSION` and the
32
+ `QuestionBlock`/`QuestionBlockQuestion`/`ParsedQuestionMessage` types.
33
+ The block is the rendering contract embedded as a terminal
34
+ ` ```index-questions ` fenced section in the negotiator's question-message;
35
+ the parser fails closed (malformed → `null` → render as plain text), and a
36
+ question's identity is its primary `opportunityId` ref (negotiations have
37
+ no table of their own) — there are no question ids and no block-level state.
38
+ - First package **subpath exports**, `@indexnetwork/protocol/question-block`
39
+ and `.../question-block/fixture`, so the browser client can import the
40
+ contract (and its canonical fixture) without pulling the node-only package
41
+ root into a bundle. STABILITY.md now documents browser-safe subpaths as part
42
+ of the public contract.
43
+
23
44
  ## 21.0.0 - 2026-08-17
24
45
 
25
46
  ### Removed
package/STABILITY.md CHANGED
@@ -7,8 +7,16 @@ change. It is the reference behind the tier annotations in `src/index.ts`.
7
7
 
8
8
  ## The public contract
9
9
 
10
- - The **only** supported entry point is the package root:
11
- `import { ... } from "@indexnetwork/protocol"`.
10
+ - The supported entry points are the package root:
11
+ `import { ... } from "@indexnetwork/protocol"` — and the **browser-safe
12
+ subpaths** listed in `package.json` `exports` (as of 21.1.0:
13
+ `@indexnetwork/protocol/question-block` and
14
+ `@indexnetwork/protocol/question-block/fixture`). A browser-safe subpath
15
+ exposes one shared-schema module whose only runtime dependency is `zod`, for
16
+ consumers (the web client) that cannot load the node-only package root. The
17
+ schema module's symbols are also re-exported from the root (the fixture is
18
+ subpath-only, to keep test data out of the runtime barrel), and the subpaths
19
+ carry the Stable tier.
12
20
  - Deep imports (`@indexnetwork/protocol/dist/...` or `/src/...`) are **not** part
13
21
  of the contract and may change or disappear in any release — do not rely on them.
14
22
  - The contract is exactly the set of symbols re-exported from `src/index.ts`.
package/dist/index.d.ts CHANGED
@@ -29,6 +29,7 @@ export type { PendingQuestionSummary } from "./shared/schemas/pending-question.s
29
29
  export { McpApiKeyMetadataSchema } from "./shared/schemas/mcp-auth.schema.js";
30
30
  export type { McpAuthInput, McpResolvedIdentity, } from "./shared/schemas/mcp-auth.schema.js";
31
31
  export type { DiscoveryNegotiation } from "./shared/schemas/discovery-question.schema.js";
32
+ export { QUESTION_BLOCK_MARKER, QUESTION_BLOCK_VERSION, QuestionBlockSchema, QuestionBlockQuestionSchema, parseQuestionMessage, serializeQuestionMessage, type QuestionBlock, type QuestionBlockQuestion, type ParsedQuestionMessage, } from "./shared/schemas/question-block.schema.js";
32
33
  export type { NetworkAssignmentMetadata } from "./shared/schemas/network-assignment.schema.js";
33
34
  export { DEFAULT_NETWORK_ASSIGNMENT_THRESHOLD, resolveAssignmentNetworkScope, buildNetworkAssignmentDecision } from "./shared/assignment/network-assignment.policy.js";
34
35
  export { ChatGraphFactory } from "./agents/agent.module.js";
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ export { SYSTEM_AGENT_IDS } from './agents/agent.module.js';
26
26
  export { ChatContextDigestSchema } from "./shared/schemas/chat-context.schema.js";
27
27
  export { NegotiationQuestionCandidateSchema, } from "./questions/question.module.js";
28
28
  export { McpApiKeyMetadataSchema } from "./shared/schemas/mcp-auth.schema.js";
29
+ export { QUESTION_BLOCK_MARKER, QUESTION_BLOCK_VERSION, QuestionBlockSchema, QuestionBlockQuestionSchema, parseQuestionMessage, serializeQuestionMessage, } from "./shared/schemas/question-block.schema.js";
29
30
  export { DEFAULT_NETWORK_ASSIGNMENT_THRESHOLD, resolveAssignmentNetworkScope, buildNetworkAssignmentDecision } from "./shared/assignment/network-assignment.policy.js";
30
31
  // ─── Graph factories ──────────────────────────────────────────────────────────
31
32
  export { ChatGraphFactory } from "./agents/agent.module.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Canonical question-message fixture for the question block contract.
3
+ *
4
+ * `questionMessageFixture` is a literal — not derived from the serializer — so
5
+ * a serializer change that would alter the wire format breaks the spec loudly.
6
+ * The web client's steps-UI tests should import this via
7
+ * `@indexnetwork/protocol/question-block/fixture` instead of minting their own.
8
+ */
9
+ import type { QuestionBlock } from "./question-block.schema.js";
10
+ export declare const questionProseFixture: string;
11
+ export declare const questionBlockFixture: QuestionBlock;
12
+ export declare const questionMessageFixture: string;
@@ -0,0 +1,39 @@
1
+ export const questionProseFixture = [
2
+ "I moved three conversations forward on your search for a technical co-founder.",
3
+ "Two of them are waiting on details only you can give — answer here and I will",
4
+ "pick the negotiations back up.",
5
+ ].join("\n");
6
+ export const questionBlockFixture = {
7
+ version: 1,
8
+ questions: [
9
+ {
10
+ prompt: "Both counterparts asked about equity: what range are you prepared to offer a founding engineer?",
11
+ opportunityId: "0b0e8a9c-6d3f-4d6a-9f2e-1c5b7a4d8e01",
12
+ alsoUnblocks: ["7f3d2c1b-8a90-4e5f-b6c7-d8e9f0a1b2c3"],
13
+ },
14
+ {
15
+ prompt: "The Berlin robotics lab wants to know whether you can be on-site one week per month.",
16
+ opportunityId: "4a5b6c7d-8e9f-4a1b-8c2d-3e4f5a6b7c8d",
17
+ },
18
+ ],
19
+ };
20
+ export const questionMessageFixture = `${questionProseFixture}
21
+
22
+ \`\`\`index-questions
23
+ {
24
+ "version": 1,
25
+ "questions": [
26
+ {
27
+ "prompt": "Both counterparts asked about equity: what range are you prepared to offer a founding engineer?",
28
+ "opportunityId": "0b0e8a9c-6d3f-4d6a-9f2e-1c5b7a4d8e01",
29
+ "alsoUnblocks": [
30
+ "7f3d2c1b-8a90-4e5f-b6c7-d8e9f0a1b2c3"
31
+ ]
32
+ },
33
+ {
34
+ "prompt": "The Berlin robotics lab wants to know whether you can be on-site one week per month.",
35
+ "opportunityId": "4a5b6c7d-8e9f-4a1b-8c2d-3e4f5a6b7c8d"
36
+ }
37
+ ]
38
+ }
39
+ \`\`\``;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The question block: the rendering contract between the negotiator's
3
+ * question-message and the web client's steps UI, and the routing contract
4
+ * for answers (docs/plans/2026-08-18-conversational-questions.md).
5
+ *
6
+ * The block is a *view* of the currently-parked negotiations for one intent
7
+ * scope, regenerated fresh on every change — it is not a persistence schema
8
+ * and must never grow state (answered flags, timestamps, question ids).
9
+ * A question's identity is its primary negotiation reference: the id of the
10
+ * opportunity row the negotiation runs on. Task re-resolution from that row
11
+ * is server-side and never encoded here — deliberately, because a snapshotted
12
+ * task id would go stale: answer routing branches on what it re-resolves
13
+ * (an `input_required` task → mid-flight consult, answered via the exact
14
+ * continuation's successor task; a completed task on a stalled opportunity
15
+ * with a trailing ask_user gap → post-stall park, answered via a retry).
16
+ *
17
+ * This module must stay browser-safe: it may import zod and nothing else.
18
+ * It is exposed to the web client via the `@indexnetwork/protocol/question-block`
19
+ * subpath export (see STABILITY.md).
20
+ */
21
+ import { z } from "zod";
22
+ /** Info string of the fenced section that carries the block in a message body. */
23
+ export declare const QUESTION_BLOCK_MARKER = "index-questions";
24
+ /** Bump when the block shape changes; parsers fail closed on unknown versions. */
25
+ export declare const QUESTION_BLOCK_VERSION = 1;
26
+ export declare const QuestionBlockQuestionSchema: z.ZodObject<{
27
+ /** Agent-authored question text, rendered as one step. */
28
+ prompt: z.ZodString;
29
+ /** Primary negotiation this question unparks; doubles as the question's identity. */
30
+ opportunityId: z.ZodString;
31
+ /** Further negotiations parked on the same gap that this answer also unparks. */
32
+ alsoUnblocks: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
33
+ }, "strict", z.ZodTypeAny, {
34
+ prompt: string;
35
+ opportunityId: string;
36
+ alsoUnblocks?: string[] | undefined;
37
+ }, {
38
+ prompt: string;
39
+ opportunityId: string;
40
+ alsoUnblocks?: string[] | undefined;
41
+ }>;
42
+ export type QuestionBlockQuestion = z.infer<typeof QuestionBlockQuestionSchema>;
43
+ export declare const QuestionBlockSchema: z.ZodEffects<z.ZodObject<{
44
+ version: z.ZodLiteral<1>;
45
+ questions: z.ZodArray<z.ZodObject<{
46
+ /** Agent-authored question text, rendered as one step. */
47
+ prompt: z.ZodString;
48
+ /** Primary negotiation this question unparks; doubles as the question's identity. */
49
+ opportunityId: z.ZodString;
50
+ /** Further negotiations parked on the same gap that this answer also unparks. */
51
+ alsoUnblocks: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
52
+ }, "strict", z.ZodTypeAny, {
53
+ prompt: string;
54
+ opportunityId: string;
55
+ alsoUnblocks?: string[] | undefined;
56
+ }, {
57
+ prompt: string;
58
+ opportunityId: string;
59
+ alsoUnblocks?: string[] | undefined;
60
+ }>, "many">;
61
+ }, "strict", z.ZodTypeAny, {
62
+ questions: {
63
+ prompt: string;
64
+ opportunityId: string;
65
+ alsoUnblocks?: string[] | undefined;
66
+ }[];
67
+ version: 1;
68
+ }, {
69
+ questions: {
70
+ prompt: string;
71
+ opportunityId: string;
72
+ alsoUnblocks?: string[] | undefined;
73
+ }[];
74
+ version: 1;
75
+ }>, {
76
+ questions: {
77
+ prompt: string;
78
+ opportunityId: string;
79
+ alsoUnblocks?: string[] | undefined;
80
+ }[];
81
+ version: 1;
82
+ }, {
83
+ questions: {
84
+ prompt: string;
85
+ opportunityId: string;
86
+ alsoUnblocks?: string[] | undefined;
87
+ }[];
88
+ version: 1;
89
+ }>;
90
+ export type QuestionBlock = z.infer<typeof QuestionBlockSchema>;
91
+ /** A parsed question-message: the agent's prose and the block that followed it. */
92
+ export interface ParsedQuestionMessage {
93
+ prose: string;
94
+ block: QuestionBlock;
95
+ }
96
+ /**
97
+ * Embed a block after the agent's prose. The block is always the terminal
98
+ * section of the body: prose, a blank line, then a fenced section whose info
99
+ * string is the marker. The fence is lengthened past any backtick run in the
100
+ * payload so the payload can never close it early.
101
+ *
102
+ * Throws on a block that fails the schema — serializing an invalid block is a
103
+ * producer bug, not a rendering condition.
104
+ */
105
+ export declare function serializeQuestionMessage(prose: string, block: QuestionBlock): string;
106
+ /**
107
+ * Extract the block from a message body. Fails closed: any body that does not
108
+ * end in exactly one well-formed, schema-valid block returns null, and the
109
+ * caller renders the whole body as plain text — never a broken steps UI.
110
+ */
111
+ export declare function parseQuestionMessage(body: string): ParsedQuestionMessage | null;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The question block: the rendering contract between the negotiator's
3
+ * question-message and the web client's steps UI, and the routing contract
4
+ * for answers (docs/plans/2026-08-18-conversational-questions.md).
5
+ *
6
+ * The block is a *view* of the currently-parked negotiations for one intent
7
+ * scope, regenerated fresh on every change — it is not a persistence schema
8
+ * and must never grow state (answered flags, timestamps, question ids).
9
+ * A question's identity is its primary negotiation reference: the id of the
10
+ * opportunity row the negotiation runs on. Task re-resolution from that row
11
+ * is server-side and never encoded here — deliberately, because a snapshotted
12
+ * task id would go stale: answer routing branches on what it re-resolves
13
+ * (an `input_required` task → mid-flight consult, answered via the exact
14
+ * continuation's successor task; a completed task on a stalled opportunity
15
+ * with a trailing ask_user gap → post-stall park, answered via a retry).
16
+ *
17
+ * This module must stay browser-safe: it may import zod and nothing else.
18
+ * It is exposed to the web client via the `@indexnetwork/protocol/question-block`
19
+ * subpath export (see STABILITY.md).
20
+ */
21
+ import { z } from "zod";
22
+ /** Info string of the fenced section that carries the block in a message body. */
23
+ export const QUESTION_BLOCK_MARKER = "index-questions";
24
+ /** Bump when the block shape changes; parsers fail closed on unknown versions. */
25
+ export const QUESTION_BLOCK_VERSION = 1;
26
+ /**
27
+ * A negotiation reference: the id of the opportunity row the negotiation runs
28
+ * on. There is no negotiations table — opportunity id is the durable identity
29
+ * used by every negotiation binding (see NegotiationQuestionProvenanceSchema).
30
+ */
31
+ const NegotiationRefSchema = z.string().uuid();
32
+ export const QuestionBlockQuestionSchema = z.object({
33
+ /** Agent-authored question text, rendered as one step. */
34
+ prompt: z.string().min(1).max(2000),
35
+ /** Primary negotiation this question unparks; doubles as the question's identity. */
36
+ opportunityId: NegotiationRefSchema,
37
+ /** Further negotiations parked on the same gap that this answer also unparks. */
38
+ alsoUnblocks: z.array(NegotiationRefSchema).max(8).optional(),
39
+ }).strict();
40
+ export const QuestionBlockSchema = z.object({
41
+ version: z.literal(QUESTION_BLOCK_VERSION),
42
+ questions: z.array(QuestionBlockQuestionSchema).min(1).max(20),
43
+ }).strict().superRefine((block, ctx) => {
44
+ // Identity is the negotiation ref, so a ref may appear exactly once in the
45
+ // whole block — a duplicate would make answer routing ambiguous.
46
+ const seen = new Set();
47
+ block.questions.forEach((question, questionIndex) => {
48
+ const refs = [question.opportunityId, ...(question.alsoUnblocks ?? [])];
49
+ refs.forEach((ref, refIndex) => {
50
+ if (seen.has(ref)) {
51
+ ctx.addIssue({
52
+ code: z.ZodIssueCode.custom,
53
+ path: refIndex === 0
54
+ ? ["questions", questionIndex, "opportunityId"]
55
+ : ["questions", questionIndex, "alsoUnblocks", refIndex - 1],
56
+ message: `negotiation ref ${ref} appears more than once in the block`,
57
+ });
58
+ }
59
+ seen.add(ref);
60
+ });
61
+ });
62
+ });
63
+ const openFencePattern = new RegExp(`^(\`{3,})${QUESTION_BLOCK_MARKER}[ \\t]*\\r?$`, "m");
64
+ function longestBacktickRun(text) {
65
+ let longest = 0;
66
+ for (const run of text.match(/`+/g) ?? [])
67
+ longest = Math.max(longest, run.length);
68
+ return longest;
69
+ }
70
+ /**
71
+ * Embed a block after the agent's prose. The block is always the terminal
72
+ * section of the body: prose, a blank line, then a fenced section whose info
73
+ * string is the marker. The fence is lengthened past any backtick run in the
74
+ * payload so the payload can never close it early.
75
+ *
76
+ * Throws on a block that fails the schema — serializing an invalid block is a
77
+ * producer bug, not a rendering condition.
78
+ */
79
+ export function serializeQuestionMessage(prose, block) {
80
+ const payload = JSON.stringify(QuestionBlockSchema.parse(block), null, 2);
81
+ const fence = "`".repeat(Math.max(3, longestBacktickRun(payload) + 1));
82
+ const trimmedProse = prose.trimEnd();
83
+ const fenced = `${fence}${QUESTION_BLOCK_MARKER}\n${payload}\n${fence}`;
84
+ return trimmedProse.length > 0 ? `${trimmedProse}\n\n${fenced}` : fenced;
85
+ }
86
+ /**
87
+ * Extract the block from a message body. Fails closed: any body that does not
88
+ * end in exactly one well-formed, schema-valid block returns null, and the
89
+ * caller renders the whole body as plain text — never a broken steps UI.
90
+ */
91
+ export function parseQuestionMessage(body) {
92
+ // Anchor on the last marker fence so prose that merely mentions the marker
93
+ // earlier in the body cannot shadow the real block.
94
+ let open = null;
95
+ const searchPattern = new RegExp(openFencePattern.source, "gm");
96
+ for (let match = searchPattern.exec(body); match; match = searchPattern.exec(body)) {
97
+ open = match;
98
+ }
99
+ if (!open)
100
+ return null;
101
+ const fenceLength = open[1].length;
102
+ const payloadStart = open.index + open[0].length + 1; // past the fence line's newline
103
+ if (payloadStart > body.length)
104
+ return null;
105
+ const closePattern = new RegExp(`^\`{${fenceLength}}[ \\t]*\\r?$`, "m");
106
+ const close = closePattern.exec(body.slice(payloadStart));
107
+ if (!close)
108
+ return null;
109
+ // The block must terminate the message: nothing but whitespace may follow.
110
+ const afterClose = body.slice(payloadStart + close.index + close[0].length);
111
+ if (afterClose.trim().length > 0)
112
+ return null;
113
+ let parsedPayload;
114
+ try {
115
+ parsedPayload = JSON.parse(body.slice(payloadStart, payloadStart + close.index));
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ const block = QuestionBlockSchema.safeParse(parsedPayload);
121
+ if (!block.success)
122
+ return null;
123
+ return { prose: body.slice(0, open.index).trimEnd(), block: block.data };
124
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indexnetwork/protocol",
3
- "version": "21.0.0-rc.490.1",
3
+ "version": "21.1.0-rc.491.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -8,6 +8,14 @@
8
8
  ".": {
9
9
  "import": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts"
11
+ },
12
+ "./question-block": {
13
+ "types": "./dist/shared/schemas/question-block.schema.d.ts",
14
+ "import": "./dist/shared/schemas/question-block.schema.js"
15
+ },
16
+ "./question-block/fixture": {
17
+ "types": "./dist/shared/schemas/question-block.fixture.d.ts",
18
+ "import": "./dist/shared/schemas/question-block.fixture.js"
11
19
  }
12
20
  },
13
21
  "files": [