@axiom-lattice/protocols 4.1.3 → 4.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/protocols",
3
- "version": "4.1.3",
3
+ "version": "4.2.0",
4
4
  "description": "Unified protocol type definitions for Axiom Lattice framework",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  AgentWebAppAppearance,
3
3
  AgentWebAppFeatures,
4
+ AgentWebAppIdentityAssurance,
4
5
  } from "./AgentWebAppStoreProtocol";
5
6
 
6
7
  /** Server-owned metadata that isolates an external user's Web App thread. */
@@ -63,7 +64,7 @@ export interface AgentWebAppBootstrap {
63
64
  defaultModelKey?: string;
64
65
  features: AgentWebAppFeatures;
65
66
  appearance: AgentWebAppAppearance;
66
- identityAssurance: "unverified";
67
+ identityAssurance: AgentWebAppIdentityAssurance;
67
68
  };
68
69
  projects: Array<{
69
70
  id: string;
@@ -17,6 +17,8 @@ export interface AgentWebAppFeatures {
17
17
  attachments: boolean;
18
18
  hitl: boolean;
19
19
  genUI: boolean;
20
+ /** File panel surface. Tolerated missing (treated as false) for records created before Phase 2. */
21
+ files?: boolean;
20
22
  }
21
23
 
22
24
  /** Optional display customization for an Agent Web App. */
@@ -26,6 +28,31 @@ export interface AgentWebAppAppearance {
26
28
  primaryColor?: string;
27
29
  }
28
30
 
31
+ /** Identity assurance levels reported by the runtime. */
32
+ export type AgentWebAppIdentityAssurance = "unverified" | "verified";
33
+
34
+ /** How external callers may identify themselves for this web app. */
35
+ export type AgentWebAppIdentityPolicy = "unverified" | "verified" | "both";
36
+
37
+ /** One trusted issuer entry. `secret` pairs with HS256, `jwksUrl` with RS256. */
38
+ export interface AgentWebAppIssuerConfig {
39
+ /** Exact `iss` claim match. */
40
+ iss: string;
41
+ /** Exact `aud` claim match; conventionally the webAppId. */
42
+ aud: string;
43
+ alg: "HS256" | "RS256";
44
+ /** Shared secret for HS256. */
45
+ secret?: string;
46
+ /** JWKS endpoint URL for RS256. */
47
+ jwksUrl?: string;
48
+ }
49
+
50
+ /** Identity configuration block for a web app. Absent = V1 unverified behavior. */
51
+ export interface AgentWebAppIdentityConfig {
52
+ policy: AgentWebAppIdentityPolicy;
53
+ issuers: AgentWebAppIssuerConfig[];
54
+ }
55
+
29
56
  /** Persisted React SDK publication of an assistant. */
30
57
  export interface AgentWebApp {
31
58
  id: string;
@@ -40,6 +67,7 @@ export interface AgentWebApp {
40
67
  scope: AgentWebAppScope;
41
68
  features: AgentWebAppFeatures;
42
69
  appearance: AgentWebAppAppearance;
70
+ identity?: AgentWebAppIdentityConfig;
43
71
  createdAt: Date;
44
72
  updatedAt: Date;
45
73
  }
@@ -55,6 +83,7 @@ export interface CreateAgentWebAppInput {
55
83
  scope: AgentWebAppScope;
56
84
  features: AgentWebAppFeatures;
57
85
  appearance: AgentWebAppAppearance;
86
+ identity?: AgentWebAppIdentityConfig;
58
87
  }
59
88
 
60
89
  /** Editable fields accepted when updating an Agent Web App. Nested objects use merge semantics. */
@@ -64,6 +93,7 @@ export interface UpdateAgentWebAppInput {
64
93
  scope?: Partial<AgentWebAppScope>;
65
94
  features?: Partial<AgentWebAppFeatures>;
66
95
  appearance?: Partial<AgentWebAppAppearance>;
96
+ identity?: AgentWebAppIdentityConfig;
67
97
  }
68
98
 
69
99
  /** Internal persistence patch. Nested objects are complete replacements when provided. */
@@ -73,6 +103,7 @@ export interface AgentWebAppStorePatch {
73
103
  scope?: AgentWebAppScope;
74
104
  features?: AgentWebAppFeatures;
75
105
  appearance?: AgentWebAppAppearance;
106
+ identity?: AgentWebAppIdentityConfig;
76
107
  status?: AgentWebAppStatus;
77
108
  }
78
109
 
@@ -0,0 +1,104 @@
1
+ import { MessageChunkTypes, type MessageChunk } from "./MessageProtocol";
2
+ import {
3
+ parseAgentWebAppGenUIBlock,
4
+ type AgentWebAppInterrupt,
5
+ type AgentWebAppStreamEvent,
6
+ } from "./AgentWebAppRuntimeProtocol";
7
+
8
+ /** Per-consumer projection metadata; it does not control Agent execution. */
9
+ export interface AgentWebAppStreamProjectionContext {
10
+ readonly startedToolIds: Set<string>;
11
+ readonly allowInterrupts: boolean;
12
+ readonly allowGenUI: boolean;
13
+ }
14
+
15
+ /** Create projection metadata shared by all chunks in one public stream. */
16
+ export function createAgentWebAppStreamProjectionContext(options: { allowInterrupts?: boolean; allowGenUI?: boolean } = {}): AgentWebAppStreamProjectionContext {
17
+ return { startedToolIds: new Set<string>(), allowInterrupts: options.allowInterrupts ?? true, allowGenUI: options.allowGenUI ?? false };
18
+ }
19
+
20
+ /** Purely project one internal Agent chunk onto zero or more stable public events. */
21
+ export function projectAgentWebAppChunk(
22
+ chunk: MessageChunk,
23
+ context?: AgentWebAppStreamProjectionContext,
24
+ ): AgentWebAppStreamEvent[] {
25
+ if (chunk.type === MessageChunkTypes.AI) {
26
+ const events: AgentWebAppStreamEvent[] = [];
27
+ const startedToolIds = context?.startedToolIds ?? new Set<string>();
28
+ if (typeof chunk.data.content === "string" && chunk.data.content) events.push({ type: "message.delta", text: chunk.data.content });
29
+ if (context?.allowGenUI && Array.isArray(chunk.data.content)) {
30
+ for (const value of chunk.data.content) {
31
+ const block = parseAgentWebAppGenUIBlock(value);
32
+ if (block) events.push({ type: "genui.render", block });
33
+ }
34
+ }
35
+ for (const call of chunk.data.tool_call_chunks ?? []) {
36
+ if (call.id && call.name && !startedToolIds.has(call.id)) {
37
+ startedToolIds.add(call.id);
38
+ events.push({ type: "tool.started", id: call.id, name: call.name });
39
+ }
40
+ }
41
+ for (const call of chunk.data.tool_calls ?? []) {
42
+ if (!startedToolIds.has(call.id)) {
43
+ startedToolIds.add(call.id);
44
+ events.push({ type: "tool.started", id: call.id, name: call.name });
45
+ }
46
+ }
47
+ return events;
48
+ }
49
+ if (chunk.type === MessageChunkTypes.TOOL && chunk.data.tool_call_id) {
50
+ return [{ type: "tool.completed", id: chunk.data.tool_call_id }];
51
+ }
52
+ if (chunk.type === MessageChunkTypes.INTERRUPT) {
53
+ if (context && !context.allowInterrupts) return [{ type: "stream.completed" }];
54
+ const interrupt = publicInterrupt(chunk);
55
+ return interrupt
56
+ ? [{ type: "interrupt.created", interrupt }, { type: "stream.completed" }]
57
+ : [{ type: "stream.completed" }];
58
+ }
59
+ if (chunk.type === MessageChunkTypes.MESSAGE_COMPLETED) {
60
+ return [
61
+ { type: "message.completed", messageId: chunk.data.id },
62
+ { type: "stream.completed" },
63
+ ];
64
+ }
65
+ if (chunk.type === MessageChunkTypes.MESSAGE_FAILED) {
66
+ return [
67
+ { type: "error", error: { code: "STREAM_FAILED", message: "Stream failed", retryable: true } },
68
+ { type: "stream.completed" },
69
+ ];
70
+ }
71
+ return [];
72
+ }
73
+
74
+ function publicInterrupt(chunk: MessageChunk): AgentWebAppInterrupt | undefined {
75
+ const value = parseInterruptContent(chunk.data.content);
76
+ if (!value) return undefined;
77
+ const topLevelId = (chunk as unknown as { id?: unknown }).id;
78
+ const id = typeof topLevelId === "string" ? topLevelId : chunk.data.id;
79
+ if (!id) return undefined;
80
+ return {
81
+ id,
82
+ type: typeof value.type === "string" ? value.type : "input",
83
+ prompt: typeof value.prompt === "string"
84
+ ? value.prompt
85
+ : typeof value.message === "string" ? value.message : "Input required",
86
+ ...(isRecord(value.data) ? { data: value.data } : {}),
87
+ };
88
+ }
89
+
90
+ function parseInterruptContent(content: unknown): Record<string, unknown> | undefined {
91
+ if (!content) return {};
92
+ if (isRecord(content)) return content;
93
+ if (typeof content !== "string") return undefined;
94
+ try {
95
+ const parsed: unknown = JSON.parse(content);
96
+ return isRecord(parsed) ? parsed : { prompt: content };
97
+ } catch {
98
+ return { prompt: content };
99
+ }
100
+ }
101
+
102
+ function isRecord(value: unknown): value is Record<string, unknown> {
103
+ return typeof value === "object" && value !== null && !Array.isArray(value);
104
+ }
@@ -29,8 +29,14 @@ export interface McpServerConfig {
29
29
  args?: string[];
30
30
  /** URL for HTTP/SSE transport */
31
31
  url?: string;
32
- /** Environment variables */
32
+ /** Environment variables (stdio transport; credentials passed to the server process) */
33
33
  env?: Record<string, string>;
34
+ /**
35
+ * Custom HTTP headers sent with every request (streamable_http / sse only).
36
+ * Commonly used for authentication, e.g. `{ Authorization: "Bearer <token>" }`.
37
+ * Ignored for stdio transport. Values are encrypted at rest by config stores.
38
+ */
39
+ headers?: Record<string, string>;
34
40
  /** Connection timeout in milliseconds */
35
41
  timeout?: number;
36
42
  /** Retry attempts on connection failure */
@@ -38,7 +38,8 @@ export interface McpServerConfigEntry {
38
38
  selectedTools: string[];
39
39
 
40
40
  /**
41
- * Whether the env field is encrypted in storage
41
+ * Whether secret values (env vars and headers) are encrypted at rest.
42
+ * Field name kept for backward compatibility; it covers config.headers too.
42
43
  */
43
44
  isEnvEncrypted: boolean;
44
45
 
@@ -23,4 +23,12 @@ export interface ProjectRoomMessageStore {
23
23
 
24
24
  /** Finds a room message by identifier within a tenant. */
25
25
  findById(tenantId: string, id: string): Promise<ProjectRoomMessage | null>;
26
+
27
+ /** Counts messages created strictly after a horizon, optionally excluding one human author. */
28
+ countAfter(input: {
29
+ tenantId: string;
30
+ roomId: string;
31
+ after: Date;
32
+ excludeAuthorUserId?: string;
33
+ }): Promise<number>;
26
34
  }
@@ -0,0 +1,20 @@
1
+ /** A user's per-room read marker used to compute unread counts. */
2
+ export interface ProjectRoomReadState {
3
+ tenantId: string;
4
+ roomId: string;
5
+ userId: string;
6
+ /** Read horizon: messages created strictly after this instant are unread. */
7
+ lastReadAt: Date;
8
+ updatedAt: Date;
9
+ }
10
+
11
+ /** Persistence operations for per-user, per-room read markers. */
12
+ export interface ProjectRoomReadStateStore {
13
+ /** Returns the user's read marker for a room, or null when never reported. */
14
+ get(tenantId: string, roomId: string, userId: string): Promise<ProjectRoomReadState | null>;
15
+ /**
16
+ * Upserts the read marker monotonically: an earlier lastReadAt never moves
17
+ * the marker backwards. Returns the stored state after the write.
18
+ */
19
+ markRead(input: { tenantId: string; roomId: string; userId: string; lastReadAt: Date }): Promise<ProjectRoomReadState>;
20
+ }
@@ -83,25 +83,41 @@ export type ProjectRoomTaskChangedEvent = ProjectRoomEventOf<
83
83
  }
84
84
  >;
85
85
 
86
+ /** A read-marker update broadcast on the acting user's channel. */
87
+ export type ProjectRoomReadChangedEvent = ProjectRoomEventOf<
88
+ "read.changed",
89
+ { projectId: string; roomId: string; lastReadAt: string }
90
+ >;
91
+
92
+ /** A membership mutation that may change a user's room subscription set. */
93
+ export type ProjectRoomMembershipAffectedEvent = ProjectRoomEventOf<
94
+ "membership.affected",
95
+ { change: "added" | "removed" | "role_changed"; projectId: string; roomId: string }
96
+ >;
97
+
86
98
  /** All identified business events retained by the realtime broker. */
87
99
  export type ProjectRoomBusinessEvent =
88
100
  | ProjectRoomMessageCreatedEvent
89
101
  | ProjectRoomRosterChangedEvent
90
102
  | ProjectRoomMembershipChangedEvent
91
- | ProjectRoomTaskChangedEvent;
103
+ | ProjectRoomTaskChangedEvent
104
+ | ProjectRoomReadChangedEvent
105
+ | ProjectRoomMembershipAffectedEvent;
92
106
 
93
107
  /** A business event before the broker assigns its process-local ID. */
94
108
  export type ProjectRoomBusinessEventDraft =
95
109
  | Omit<ProjectRoomMessageCreatedEvent, "id">
96
110
  | Omit<ProjectRoomRosterChangedEvent, "id">
97
111
  | Omit<ProjectRoomMembershipChangedEvent, "id">
98
- | Omit<ProjectRoomTaskChangedEvent, "id">;
112
+ | Omit<ProjectRoomTaskChangedEvent, "id">
113
+ | Omit<ProjectRoomReadChangedEvent, "id">
114
+ | Omit<ProjectRoomMembershipAffectedEvent, "id">;
99
115
 
100
116
  /** A connection control event; control events are never replayed. */
101
117
  export type ProjectRoomControlEvent =
102
118
  | { type: "ready"; data: { epoch: string; headEventId: string | null } }
103
119
  | { type: "resync"; data: { reason: "SERVER_RESTART" | "CURSOR_EXPIRED" | "SLOW_CONSUMER" } }
104
- | { type: "access.revoked"; data: { reason: "PROJECT_ACCESS_REVOKED" | "TOKEN_EXPIRED" } };
120
+ | { type: "access.revoked"; data: { reason: "PROJECT_ACCESS_REVOKED" | "TOKEN_EXPIRED" | "CONNECTION_SUPERSEDED" } };
105
121
 
106
122
  /** The authenticated identity used by Project Room realtime access checks. */
107
123
  export interface ProjectRoomRealtimeActor {
@@ -194,6 +210,15 @@ export function isProjectRoomEventId(value: unknown): value is string {
194
210
  return parseProjectRoomEventId(value) !== undefined;
195
211
  }
196
212
 
213
+ /** projectId sentinel marking a per-user event channel inside the room broker. */
214
+ export const PROJECT_ROOM_USER_CHANNEL_PROJECT = "__user_channel__";
215
+
216
+ /** Builds the per-user broker scope used for membership and read broadcasts. */
217
+ export function projectRoomUserEventScope(tenantId: string, userId: string): ProjectRoomEventScope {
218
+ if (!tenantId || !userId) throw new TypeError("Project Room user scope requires non-empty identifiers");
219
+ return { tenantId, roomId: `__user__:${userId}`, projectId: PROJECT_ROOM_USER_CHANNEL_PROJECT };
220
+ }
221
+
197
222
  function isoDate(value: unknown): string | undefined {
198
223
  if (typeof value !== "object" || value === null) return undefined;
199
224
  try {
@@ -0,0 +1,16 @@
1
+ import { projectRoomUserEventScope, parseProjectRoomEventId } from "../ProjectRoomRealtimeProtocol";
2
+
3
+ describe("projectRoomUserEventScope", () => {
4
+ it("builds a stable per-user scope that cannot collide with room scopes", () => {
5
+ expect(projectRoomUserEventScope("t1", "u1")).toEqual({
6
+ tenantId: "t1", roomId: "__user__:u1", projectId: "__user_channel__",
7
+ });
8
+ expect(projectRoomUserEventScope("t1", "u1")).toEqual(projectRoomUserEventScope("t1", "u1"));
9
+ expect(projectRoomUserEventScope("t1", "u2")).not.toEqual(projectRoomUserEventScope("t1", "u1"));
10
+ });
11
+ it("user scope ids are valid broker event id candidates", () => {
12
+ const scope = projectRoomUserEventScope("t1", "u1");
13
+ expect(typeof scope.roomId).toBe("string");
14
+ expect(parseProjectRoomEventId(`${"0a1b2c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d"}:1`)?.sequence).toBe(1);
15
+ });
16
+ });
@@ -264,6 +264,10 @@ class FakeProjectRoomMessageStore implements ProjectRoomMessageStore {
264
264
  const [tenantId, id] = args;
265
265
  return tenantId === "tenant-1" && id === "message-1" ? message : null;
266
266
  }
267
+
268
+ async countAfter(input: Parameters<ProjectRoomMessageStore["countAfter"]>[0]): Promise<number> {
269
+ return input.tenantId === "tenant-1" && input.roomId === "room-1" ? 1 : 0;
270
+ }
267
271
  }
268
272
 
269
273
  describe("ProjectRoomStore", () => {
package/src/index.ts CHANGED
@@ -48,6 +48,7 @@ export * from "./A2AApiKeyStoreProtocol";
48
48
  export * from "./ConversationStoreProtocol";
49
49
  export * from "./AgentWebAppStoreProtocol";
50
50
  export * from "./AgentWebAppRuntimeProtocol";
51
+ export * from "./AgentWebAppStreamProjection";
51
52
  export * from "./CapabilityBundleStoreProtocol";
52
53
  export * from "./CapabilityRuntimeProtocol";
53
54
  export * from "./ProjectRoomProtocol";
@@ -55,6 +56,7 @@ export * from "./ProjectRoomStoreProtocol";
55
56
  export * from "./ProjectMembershipStoreProtocol";
56
57
  export * from "./ProjectBotMembershipStoreProtocol";
57
58
  export * from "./ProjectRoomMessageStoreProtocol";
59
+ export * from "./ProjectRoomReadStateProtocol";
58
60
  export * from "./ExactDataSnapshot";
59
61
  export * from "./ProjectRoomRealtimeProtocol";
60
62