@axiom-lattice/protocols 4.3.1 → 4.4.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +42 -0
- package/dist/index.d.mts +184 -8
- package/dist/index.d.ts +184 -8
- package/dist/index.js +63 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +60 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/CapabilityBundleStoreProtocol.ts +3 -4
- package/src/McpUiProtocol.ts +76 -0
- package/src/MenuProtocol.ts +50 -2
- package/src/MessageProtocol.ts +30 -0
- package/src/OpenProtocol.ts +1 -1
- package/src/PluginProtocol.ts +36 -0
- package/src/SandboxPluginProtocol.ts +3 -1
- package/src/TaskStoreProtocol.ts +19 -0
- package/src/TrustedRunContextProtocol.ts +40 -15
- package/src/__tests__/McpUiProtocol.test.ts +43 -0
- package/src/__tests__/MenuProtocol.test.ts +28 -0
- package/src/__tests__/ProjectRoomStores.test.ts +6 -0
- package/src/__tests__/TaskMetadataFilter.test.ts +27 -0
- package/src/__tests__/ToolCallUi.test.ts +46 -0
- package/src/__tests__/TrustedRunContextProtocol.test.ts +75 -3
- package/src/__tests__/open-ui-meta.test.ts +23 -0
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -47,9 +47,9 @@ export interface UpdateCapabilityBundleInput extends Partial<CreateCapabilityBun
|
|
|
47
47
|
expectedUpdatedAt: string;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/** Store input
|
|
50
|
+
/** Store input for updating a bundle; the stable key is immutable and the revision is always required for CAS. */
|
|
51
51
|
export interface InternalUpdateCapabilityBundleInput extends Partial<CreateCapabilityBundleInput> {
|
|
52
|
-
expectedUpdatedAt
|
|
52
|
+
expectedUpdatedAt: string;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** Result returned when an update's expected revision no longer matches. */
|
|
@@ -70,8 +70,7 @@ export interface CapabilityBundleStore {
|
|
|
70
70
|
getManyByIds(tenantId: string, ids: string[]): Promise<CapabilityBundle[]>;
|
|
71
71
|
/** Creates a bundle for a tenant. */
|
|
72
72
|
create(tenantId: string, input: InternalCreateCapabilityBundleInput): Promise<CapabilityBundle>;
|
|
73
|
-
/** Updates a bundle, or returns null when it does not exist. */
|
|
74
|
-
/** Omitted expectedUpdatedAt is reserved for internal maintenance callers. */
|
|
73
|
+
/** Updates a bundle with mandatory revision CAS, or returns null when it does not exist. */
|
|
75
74
|
update(tenantId: string, id: string, input: InternalUpdateCapabilityBundleInput): Promise<CapabilityBundle | CapabilityBundleUpdateConflict | null>;
|
|
76
75
|
/** Atomically deletes a bundle unless a tenant project references it. */
|
|
77
76
|
deleteIfUnreferenced(tenantId: string, id: string): Promise<CapabilityBundleDeleteResult>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Display modes supported by MCP Apps. */
|
|
2
|
+
export type McpUiDisplayMode = "inline" | "fullscreen" | "pip";
|
|
3
|
+
|
|
4
|
+
/** CSP origins declared by a UI resource. */
|
|
5
|
+
export interface McpUiCsp {
|
|
6
|
+
connectDomains?: string[];
|
|
7
|
+
resourceDomains?: string[];
|
|
8
|
+
frameDomains?: string[];
|
|
9
|
+
baseUriDomains?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Sandbox permissions requested by a UI resource. */
|
|
13
|
+
export interface McpUiPermissions {
|
|
14
|
+
camera?: boolean;
|
|
15
|
+
microphone?: boolean;
|
|
16
|
+
geolocation?: boolean;
|
|
17
|
+
clipboardWrite?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A UI ref carried in the `mcp_app` content fence. */
|
|
21
|
+
export type McpUiRef =
|
|
22
|
+
| {
|
|
23
|
+
kind: "mcp";
|
|
24
|
+
serverKey: string;
|
|
25
|
+
resourceUri: string;
|
|
26
|
+
displayMode: McpUiDisplayMode;
|
|
27
|
+
title?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Retained only for backward compatibility with pre-unification
|
|
30
|
+
* messages; no longer emitted. Rich tool results now travel via
|
|
31
|
+
* `ToolCall.ui`.
|
|
32
|
+
*/
|
|
33
|
+
structuredContent?: unknown;
|
|
34
|
+
}
|
|
35
|
+
| { kind: "plugin"; pluginType: string; resource: string; displayMode: McpUiDisplayMode; title?: string };
|
|
36
|
+
|
|
37
|
+
/** Resolved UI resource body returned by the gateway. */
|
|
38
|
+
export interface McpUiResourcePayload {
|
|
39
|
+
mimeType: string;
|
|
40
|
+
html: string;
|
|
41
|
+
csp?: McpUiCsp;
|
|
42
|
+
permissions?: McpUiPermissions;
|
|
43
|
+
prefersBorder?: boolean;
|
|
44
|
+
stale?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const MODES: McpUiDisplayMode[] = ["inline", "fullscreen", "pip"];
|
|
48
|
+
const isObj = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null;
|
|
49
|
+
|
|
50
|
+
/** Type guard for a parsed `McpUiRef`. */
|
|
51
|
+
export function isMcpUiRef(value: unknown): value is McpUiRef {
|
|
52
|
+
return parseMcpUiRef(value) !== null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Parse and normalize an unknown value into a `McpUiRef`, or `null`. */
|
|
56
|
+
export function parseMcpUiRef(value: unknown): McpUiRef | null {
|
|
57
|
+
if (!isObj(value)) return null;
|
|
58
|
+
const displayMode = MODES.includes(value.displayMode as McpUiDisplayMode)
|
|
59
|
+
? (value.displayMode as McpUiDisplayMode)
|
|
60
|
+
: "inline";
|
|
61
|
+
const title = typeof value.title === "string" ? value.title : undefined;
|
|
62
|
+
if (value.kind === "mcp" && typeof value.serverKey === "string" && typeof value.resourceUri === "string") {
|
|
63
|
+
return {
|
|
64
|
+
kind: "mcp",
|
|
65
|
+
serverKey: value.serverKey,
|
|
66
|
+
resourceUri: value.resourceUri,
|
|
67
|
+
displayMode,
|
|
68
|
+
...(title ? { title } : {}),
|
|
69
|
+
...(value.structuredContent !== undefined ? { structuredContent: value.structuredContent } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (value.kind === "plugin" && typeof value.pluginType === "string" && typeof value.resource === "string") {
|
|
73
|
+
return { kind: "plugin", pluginType: value.pluginType, resource: value.resource, displayMode, ...(title ? { title } : {}) };
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
package/src/MenuProtocol.ts
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* Menu items are merged with built-in defaults at the React SDK layer.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { McpUiDisplayMode } from "./McpUiProtocol";
|
|
9
|
+
import type { ToolCallUi } from "./MessageProtocol";
|
|
10
|
+
|
|
8
11
|
export type MenuTarget = 'sidebar' | 'workspace';
|
|
9
12
|
|
|
10
|
-
export type MenuContentType = 'agent' | 'html' | 'custom';
|
|
13
|
+
export type MenuContentType = 'agent' | 'html' | 'custom' | 'file' | 'mcpApp';
|
|
11
14
|
|
|
12
15
|
export interface AgentMenuConfig {
|
|
13
16
|
agentId: string;
|
|
@@ -25,7 +28,52 @@ export interface CustomMenuConfig {
|
|
|
25
28
|
componentKey: string;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
/**
|
|
32
|
+
* A pinned MCP App menu item.
|
|
33
|
+
*
|
|
34
|
+
* The identity (`kind` + server/plugin + resource) is enough to re-open the
|
|
35
|
+
* App; `toolInput` lets a self-refreshing App re-run the same query. The pin
|
|
36
|
+
* flow also captures the current `toolResult` and `ui` so the pinned view has
|
|
37
|
+
* the same initial data as the chat view did (an App that refreshes itself can
|
|
38
|
+
* still re-query).
|
|
39
|
+
*/
|
|
40
|
+
export type McpAppMenuConfig =
|
|
41
|
+
| {
|
|
42
|
+
kind: 'mcp';
|
|
43
|
+
serverKey: string;
|
|
44
|
+
resourceUri: string;
|
|
45
|
+
displayMode?: McpUiDisplayMode;
|
|
46
|
+
title?: string;
|
|
47
|
+
toolInput?: Record<string, unknown>;
|
|
48
|
+
toolResult?: unknown;
|
|
49
|
+
ui?: ToolCallUi;
|
|
50
|
+
}
|
|
51
|
+
| {
|
|
52
|
+
kind: 'plugin';
|
|
53
|
+
pluginType: string;
|
|
54
|
+
resource: string;
|
|
55
|
+
displayMode?: McpUiDisplayMode;
|
|
56
|
+
title?: string;
|
|
57
|
+
toolInput?: Record<string, unknown>;
|
|
58
|
+
toolResult?: unknown;
|
|
59
|
+
ui?: ToolCallUi;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** A pinned workspace file rendered by the file viewer. */
|
|
63
|
+
export interface FileMenuConfig {
|
|
64
|
+
resourcePath: string;
|
|
65
|
+
fileName?: string;
|
|
66
|
+
workspaceId?: string;
|
|
67
|
+
projectId?: string;
|
|
68
|
+
assistantId?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type MenuContentConfig =
|
|
72
|
+
| AgentMenuConfig
|
|
73
|
+
| HtmlMenuConfig
|
|
74
|
+
| CustomMenuConfig
|
|
75
|
+
| FileMenuConfig
|
|
76
|
+
| McpAppMenuConfig;
|
|
29
77
|
|
|
30
78
|
export interface MenuItem {
|
|
31
79
|
id: string;
|
package/src/MessageProtocol.ts
CHANGED
|
@@ -24,6 +24,22 @@ export interface UserMessage extends BaseMessage {
|
|
|
24
24
|
files?: Array<{ name: string; id: string }>; // Optional files attached to the message
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* UI-only rich payload for a tool call.
|
|
29
|
+
*
|
|
30
|
+
* Populated by the client-side message merger from the tool message's raw
|
|
31
|
+
* LangChain `artifact` and non-text content blocks. It is never part of the
|
|
32
|
+
* model-visible text: the model only sees `ToolCall.response`.
|
|
33
|
+
*/
|
|
34
|
+
export interface ToolCallUi {
|
|
35
|
+
/** MCP `CallToolResult.structuredContent`, when the tool produced one. */
|
|
36
|
+
structuredContent?: unknown;
|
|
37
|
+
/** MCP `CallToolResult._meta`, when present. */
|
|
38
|
+
meta?: Record<string, unknown>;
|
|
39
|
+
/** Non-text content blocks (image / audio / resource / file). */
|
|
40
|
+
contentBlocks?: Array<{ type: string; [key: string]: unknown }>;
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
/**
|
|
28
44
|
* Tool call interface
|
|
29
45
|
*/
|
|
@@ -33,6 +49,12 @@ export interface ToolCall {
|
|
|
33
49
|
args: Record<string, any>; // Arguments as an object
|
|
34
50
|
type: "tool_call"; // Type of tool call
|
|
35
51
|
response?: string; // Optional response from the tool execution
|
|
52
|
+
/** UI-only rich result (MCP Apps). Never part of the model-visible text. */
|
|
53
|
+
ui?: ToolCallUi;
|
|
54
|
+
/** Execution status reported by the client-side message merger. */
|
|
55
|
+
status?: "success" | "pending" | "error";
|
|
56
|
+
/** Correlation id of the originating tool message (internal). */
|
|
57
|
+
original_tool_message_id?: string;
|
|
36
58
|
}
|
|
37
59
|
|
|
38
60
|
/**
|
|
@@ -106,6 +128,8 @@ export interface MessageChunk {
|
|
|
106
128
|
id?: string;
|
|
107
129
|
index: number;
|
|
108
130
|
}>;
|
|
131
|
+
// Wire shape: intentionally lacks `ui`, which is client-populated on ToolCall
|
|
132
|
+
// from the tool message's `artifact` and never sent by the server.
|
|
109
133
|
tool_calls?: Array<{
|
|
110
134
|
name: string;
|
|
111
135
|
args: Record<string, any>;
|
|
@@ -120,6 +144,12 @@ export interface MessageChunk {
|
|
|
120
144
|
}>;
|
|
121
145
|
};
|
|
122
146
|
tool_call_id?: string;
|
|
147
|
+
/**
|
|
148
|
+
* Raw LangChain tool message `artifact` (e.g. MCP `structuredContent`
|
|
149
|
+
* and non-text content blocks). Present on `tool` chunks; consumed by the
|
|
150
|
+
* client-side message merger to populate `ToolCall.ui`.
|
|
151
|
+
*/
|
|
152
|
+
artifact?: unknown;
|
|
123
153
|
};
|
|
124
154
|
}
|
|
125
155
|
|
package/src/OpenProtocol.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface OpenExecutionContext {
|
|
|
20
20
|
projectId: string;
|
|
21
21
|
/** Addressing only, not an authorization dimension (see design §6.2). */
|
|
22
22
|
workspaceId?: string;
|
|
23
|
-
/** Synthesized
|
|
23
|
+
/** Synthesized run identity (mirrors the Agent path's runConfig; plugin tools resolve their own connections). */
|
|
24
24
|
runConfig: Record<string, unknown>;
|
|
25
25
|
}
|
|
26
26
|
|
package/src/PluginProtocol.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentConfig } from "./AgentLatticeProtocol";
|
|
2
|
+
import type { McpUiDisplayMode, McpUiCsp, McpUiPermissions } from "./McpUiProtocol";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Plugin Connection 字段 schema(驱动前端表单渲染)
|
|
@@ -80,6 +81,37 @@ export interface PluginConnection {
|
|
|
80
81
|
resourceLabel?: string;
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/**
|
|
85
|
+
* UI declaration for a plugin tool: an HTML file rendered as an MCP App when
|
|
86
|
+
* the tool runs.
|
|
87
|
+
*
|
|
88
|
+
* @property resource - Safe relative path (within the plugin directory) to an `.html` file.
|
|
89
|
+
* @property displayMode - Preferred display mode; defaults to `inline` (resolved by the compiler).
|
|
90
|
+
* @property csp - Origins the UI needs (see `McpUiCsp`).
|
|
91
|
+
* @property permissions - Sandbox permissions requested (see `McpUiPermissions`).
|
|
92
|
+
* @property prefersBorder - Visual boundary preference.
|
|
93
|
+
*/
|
|
94
|
+
export interface PluginToolUi {
|
|
95
|
+
resource: string;
|
|
96
|
+
displayMode?: McpUiDisplayMode;
|
|
97
|
+
csp?: McpUiCsp;
|
|
98
|
+
permissions?: McpUiPermissions;
|
|
99
|
+
prefersBorder?: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Inline UI resource served by a GLOBAL (non-sandbox) plugin.
|
|
104
|
+
*
|
|
105
|
+
* @property html - Full HTML document served for a `ui://` resource.
|
|
106
|
+
* @property mimeType - Defaults to `text/html;profile=mcp-app` when omitted.
|
|
107
|
+
* @property csp - Origins the UI needs.
|
|
108
|
+
*/
|
|
109
|
+
export interface PluginUiResource {
|
|
110
|
+
html: string;
|
|
111
|
+
mimeType?: string;
|
|
112
|
+
csp?: McpUiCsp;
|
|
113
|
+
}
|
|
114
|
+
|
|
83
115
|
/**
|
|
84
116
|
* 工具元信息(用于前端 allowedTools 筛选)
|
|
85
117
|
*
|
|
@@ -92,6 +124,8 @@ export interface PluginToolMeta {
|
|
|
92
124
|
description: string;
|
|
93
125
|
/** When true, this tool surfaces on the Open API (MCP) surface. */
|
|
94
126
|
expose?: boolean;
|
|
127
|
+
/** Optional UI (HTML) declaration for this tool. */
|
|
128
|
+
ui?: PluginToolUi;
|
|
95
129
|
}
|
|
96
130
|
|
|
97
131
|
/** Open-surface exposure entry with MCP annotation hints. */
|
|
@@ -163,6 +197,8 @@ export interface PluginMeta {
|
|
|
163
197
|
icon?: string;
|
|
164
198
|
/** 工具清单(可选,middleware 能自动提取时不需要写) */
|
|
165
199
|
tools?: PluginToolMeta[];
|
|
200
|
+
/** Inline `ui://` resources for this plugin's tool UIs (global plugins only). */
|
|
201
|
+
uiResources?: Record<string, PluginUiResource>;
|
|
166
202
|
/**
|
|
167
203
|
* Open 面(MCP)暴露的工具名清单——独立于 tools 声明,一行即生长。
|
|
168
204
|
* 声明后 OpenCredentialService 的 grants 匹配 domain=<meta.type>。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentConfig } from "./AgentLatticeProtocol";
|
|
2
|
-
import type { PluginConnectionFieldSchema, PluginSkillDefinition } from "./PluginProtocol";
|
|
2
|
+
import type { PluginConnectionFieldSchema, PluginSkillDefinition, PluginToolUi } from "./PluginProtocol";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* A single tool contributed by a sandbox-authored tenant plugin.
|
|
@@ -19,6 +19,8 @@ export interface SandboxPluginToolDef {
|
|
|
19
19
|
destructive?: boolean;
|
|
20
20
|
timeoutMs?: number;
|
|
21
21
|
maxResultBytes?: number;
|
|
22
|
+
/** Optional UI (HTML) rendered as an MCP App when this tool runs. */
|
|
23
|
+
ui?: PluginToolUi;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|
package/src/TaskStoreProtocol.ts
CHANGED
|
@@ -331,6 +331,25 @@ export interface UpdateTaskRequest {
|
|
|
331
331
|
files?: TaskFileRef[];
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Normalizes a task metadata scalar to its JSON text form so every task store
|
|
336
|
+
* backend compares metadata filters identically.
|
|
337
|
+
*
|
|
338
|
+
* The contract is JSON scalar value equality, type-tolerant: `1` and `"1"`
|
|
339
|
+
* are equal, `true` and `"true"` are equal, and `true` never equals `1`.
|
|
340
|
+
* PostgreSQL's `metadata->>` naturally yields this form; InMemory and SQLite
|
|
341
|
+
* use this helper to match it.
|
|
342
|
+
*
|
|
343
|
+
* @param value Candidate filter or stored metadata value.
|
|
344
|
+
* @returns JSON text form for string/number/boolean, otherwise `undefined`.
|
|
345
|
+
*/
|
|
346
|
+
export function normalizeTaskMetadataScalar(value: unknown): string | undefined {
|
|
347
|
+
if (typeof value === "string") return value;
|
|
348
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : undefined;
|
|
349
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
334
353
|
/**
|
|
335
354
|
* Task list filter criteria
|
|
336
355
|
*/
|
|
@@ -16,6 +16,12 @@ export interface ProjectRoomTrustedRunContext {
|
|
|
16
16
|
role: "coordinator" | "specialist";
|
|
17
17
|
title: string;
|
|
18
18
|
responsibility?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Host-provided provenance string, persisted unchanged. Optional; a non-empty string when present;
|
|
21
|
+
* `undefined` is treated as absent. Core copies and compares this value without interpreting or
|
|
22
|
+
* naming any specific value (ADR-118).
|
|
23
|
+
*/
|
|
24
|
+
source?: string;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
/** Trusted Project Task identity persisted with a privileged queue message. */
|
|
@@ -29,6 +35,12 @@ export interface ProjectTaskTrustedRunContext {
|
|
|
29
35
|
taskId: string;
|
|
30
36
|
threadId: string;
|
|
31
37
|
inputMessageId: string;
|
|
38
|
+
/**
|
|
39
|
+
* Host-provided provenance string, persisted unchanged. Optional; a non-empty string when present;
|
|
40
|
+
* `undefined` is treated as absent. Core copies and compares this value without interpreting or
|
|
41
|
+
* naming any specific value (ADR-118).
|
|
42
|
+
*/
|
|
43
|
+
source?: string;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
/** Host-authenticated metadata that cannot be supplied through public Agent APIs. */
|
|
@@ -54,37 +66,47 @@ export function parseTrustedRunContext(value: unknown): TrustedRunContext {
|
|
|
54
66
|
"tenantId", "workspaceId", "projectId", "roomId", "membershipId",
|
|
55
67
|
"assistantId", "taskId", "threadId", "inputMessageId",
|
|
56
68
|
] as const;
|
|
57
|
-
const projectTaskValues = snapshotExactRecord(contextValues.projectTask, requiredKeys);
|
|
69
|
+
const projectTaskValues = snapshotExactRecord(contextValues.projectTask, requiredKeys, ["source"]);
|
|
70
|
+
const projectTaskHasSource = projectTaskValues !== undefined
|
|
71
|
+
&& Object.prototype.hasOwnProperty.call(projectTaskValues, "source");
|
|
58
72
|
if (!projectTaskValues
|
|
59
|
-
|| requiredKeys.some((key) => typeof projectTaskValues[key] !== "string" || projectTaskValues[key].length === 0)
|
|
73
|
+
|| requiredKeys.some((key) => typeof projectTaskValues[key] !== "string" || projectTaskValues[key].length === 0)
|
|
74
|
+
|| (projectTaskHasSource && projectTaskValues.source !== undefined
|
|
75
|
+
&& (typeof projectTaskValues.source !== "string" || projectTaskValues.source.length === 0))) {
|
|
60
76
|
throw new Error("Invalid trusted agent run context");
|
|
61
77
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
inputMessageId: projectTaskValues.inputMessageId as string,
|
|
73
|
-
},
|
|
78
|
+
const parsedProjectTask: ProjectTaskTrustedRunContext = {
|
|
79
|
+
tenantId: projectTaskValues.tenantId as string,
|
|
80
|
+
workspaceId: projectTaskValues.workspaceId as string,
|
|
81
|
+
projectId: projectTaskValues.projectId as string,
|
|
82
|
+
roomId: projectTaskValues.roomId as string,
|
|
83
|
+
membershipId: projectTaskValues.membershipId as string,
|
|
84
|
+
assistantId: projectTaskValues.assistantId as string,
|
|
85
|
+
taskId: projectTaskValues.taskId as string,
|
|
86
|
+
threadId: projectTaskValues.threadId as string,
|
|
87
|
+
inputMessageId: projectTaskValues.inputMessageId as string,
|
|
74
88
|
};
|
|
89
|
+
if (projectTaskHasSource && typeof projectTaskValues.source === "string") {
|
|
90
|
+
parsedProjectTask.source = projectTaskValues.source;
|
|
91
|
+
}
|
|
92
|
+
return { projectTask: parsedProjectTask };
|
|
75
93
|
}
|
|
76
94
|
const projectRoomValue = contextValues?.projectRoom;
|
|
77
95
|
const requiredKeys = [
|
|
78
96
|
"tenantId", "workspaceId", "projectId", "roomId", "sourceRoomMessageId", "membershipId",
|
|
79
97
|
"assistantId", "inputMessageId", "role", "title",
|
|
80
98
|
] as const;
|
|
81
|
-
const projectRoomValues = snapshotExactRecord(projectRoomValue, requiredKeys, ["responsibility"]);
|
|
99
|
+
const projectRoomValues = snapshotExactRecord(projectRoomValue, requiredKeys, ["responsibility", "source"]);
|
|
82
100
|
const hasResponsibility = projectRoomValues !== undefined
|
|
83
101
|
&& Object.prototype.hasOwnProperty.call(projectRoomValues, "responsibility");
|
|
102
|
+
const hasSource = projectRoomValues !== undefined
|
|
103
|
+
&& Object.prototype.hasOwnProperty.call(projectRoomValues, "source");
|
|
84
104
|
if (!projectRoomValues
|
|
85
105
|
|| requiredKeys.some((key) => typeof projectRoomValues[key] !== "string" || projectRoomValues[key].length === 0)
|
|
86
106
|
|| (hasResponsibility && projectRoomValues.responsibility !== undefined
|
|
87
107
|
&& (typeof projectRoomValues.responsibility !== "string" || projectRoomValues.responsibility.length === 0))
|
|
108
|
+
|| (hasSource && projectRoomValues.source !== undefined
|
|
109
|
+
&& (typeof projectRoomValues.source !== "string" || projectRoomValues.source.length === 0))
|
|
88
110
|
|| (projectRoomValues.role !== "coordinator" && projectRoomValues.role !== "specialist")) {
|
|
89
111
|
throw new Error("Invalid trusted agent run context");
|
|
90
112
|
}
|
|
@@ -103,6 +125,9 @@ export function parseTrustedRunContext(value: unknown): TrustedRunContext {
|
|
|
103
125
|
if (hasResponsibility && typeof projectRoomValues.responsibility === "string") {
|
|
104
126
|
parsedProjectRoom.responsibility = projectRoomValues.responsibility;
|
|
105
127
|
}
|
|
128
|
+
if (hasSource && typeof projectRoomValues.source === "string") {
|
|
129
|
+
parsedProjectRoom.source = projectRoomValues.source;
|
|
130
|
+
}
|
|
106
131
|
return { projectRoom: parsedProjectRoom };
|
|
107
132
|
}
|
|
108
133
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { parseMcpUiRef, isMcpUiRef } from "../McpUiProtocol";
|
|
2
|
+
|
|
3
|
+
describe("parseMcpUiRef", () => {
|
|
4
|
+
it("parses a valid mcp ref", () => {
|
|
5
|
+
const ref = { kind: "mcp", serverKey: "weather", resourceUri: "ui://weather/dashboard", displayMode: "inline" };
|
|
6
|
+
expect(parseMcpUiRef(ref)).toEqual(ref);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("defaults displayMode to inline", () => {
|
|
10
|
+
const ref = parseMcpUiRef({ kind: "mcp", serverKey: "a", resourceUri: "ui://a/x" });
|
|
11
|
+
expect(ref?.displayMode).toBe("inline");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("preserves structuredContent on an mcp ref", () => {
|
|
15
|
+
const ref = parseMcpUiRef({
|
|
16
|
+
kind: "mcp",
|
|
17
|
+
serverKey: "weather",
|
|
18
|
+
resourceUri: "ui://weather/dashboard",
|
|
19
|
+
structuredContent: { a: 1 },
|
|
20
|
+
});
|
|
21
|
+
expect(ref).toEqual({
|
|
22
|
+
kind: "mcp",
|
|
23
|
+
serverKey: "weather",
|
|
24
|
+
resourceUri: "ui://weather/dashboard",
|
|
25
|
+
displayMode: "inline",
|
|
26
|
+
structuredContent: { a: 1 },
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("omits structuredContent when absent", () => {
|
|
31
|
+
const ref = parseMcpUiRef({ kind: "mcp", serverKey: "a", resourceUri: "ui://a/x" });
|
|
32
|
+
expect(ref && "structuredContent" in ref).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("rejects unknown kind", () => {
|
|
36
|
+
expect(parseMcpUiRef({ kind: "nope" })).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("isMcpUiRef narrows", () => {
|
|
40
|
+
expect(isMcpUiRef({ kind: "plugin", pluginType: "p", resource: "ui/x.html", displayMode: "inline" })).toBe(true);
|
|
41
|
+
expect(isMcpUiRef("x")).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { McpAppMenuConfig, MenuContentConfig, MenuContentType } from "../MenuProtocol";
|
|
2
|
+
|
|
3
|
+
describe("MenuProtocol mcpApp content type", () => {
|
|
4
|
+
it("round-trips both mcp and plugin McpAppMenuConfig variants", () => {
|
|
5
|
+
const mcp: McpAppMenuConfig = {
|
|
6
|
+
kind: "mcp",
|
|
7
|
+
serverKey: "srv",
|
|
8
|
+
resourceUri: "ui://srv/a.html",
|
|
9
|
+
displayMode: "inline",
|
|
10
|
+
title: "App",
|
|
11
|
+
toolInput: { area: "eu" },
|
|
12
|
+
};
|
|
13
|
+
const plugin: McpAppMenuConfig = {
|
|
14
|
+
kind: "plugin",
|
|
15
|
+
pluginType: "my-plugin",
|
|
16
|
+
resource: "ui/report.html",
|
|
17
|
+
title: "Report",
|
|
18
|
+
};
|
|
19
|
+
for (const cfg of [mcp, plugin] as MenuContentConfig[]) {
|
|
20
|
+
expect(JSON.parse(JSON.stringify(cfg))).toEqual(cfg);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("includes file and mcpApp in MenuContentType", () => {
|
|
25
|
+
const types: MenuContentType[] = ["agent", "html", "custom", "file", "mcpApp"];
|
|
26
|
+
expect(new Set(types).size).toBe(5);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -268,6 +268,10 @@ class FakeProjectRoomMessageStore implements ProjectRoomMessageStore {
|
|
|
268
268
|
async countAfter(input: Parameters<ProjectRoomMessageStore["countAfter"]>[0]): Promise<number> {
|
|
269
269
|
return input.tenantId === "tenant-1" && input.roomId === "room-1" ? 1 : 0;
|
|
270
270
|
}
|
|
271
|
+
|
|
272
|
+
async deleteChatOlderThan(input: Parameters<ProjectRoomMessageStore["deleteChatOlderThan"]>[0]): Promise<number> {
|
|
273
|
+
return input.limit > 0 ? 1 : 0;
|
|
274
|
+
}
|
|
271
275
|
}
|
|
272
276
|
|
|
273
277
|
describe("ProjectRoomStore", () => {
|
|
@@ -363,5 +367,7 @@ describe("ProjectRoomMessageStore", () => {
|
|
|
363
367
|
expect(await store.list({ tenantId: "tenant-1", roomId: "room-1", before: cursor, limit: 10 })).toEqual([message]);
|
|
364
368
|
expect(await store.findById("tenant-1", "message-1")).toEqual(message);
|
|
365
369
|
expect(await store.findById("tenant-1", "missing-message")).toBeNull();
|
|
370
|
+
expect(await store.deleteChatOlderThan({ before: message.createdAt, limit: 10 })).toBe(1);
|
|
371
|
+
expect(await store.deleteChatOlderThan({ before: message.createdAt, limit: 0 })).toBe(0);
|
|
366
372
|
});
|
|
367
373
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { normalizeTaskMetadataScalar } from "../TaskStoreProtocol";
|
|
2
|
+
|
|
3
|
+
describe("normalizeTaskMetadataScalar", () => {
|
|
4
|
+
it("returns strings unchanged", () => {
|
|
5
|
+
expect(normalizeTaskMetadataScalar("p1")).toBe("p1");
|
|
6
|
+
expect(normalizeTaskMetadataScalar("1")).toBe("1");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("stringifies finite numbers and rejects non-finite ones", () => {
|
|
10
|
+
expect(normalizeTaskMetadataScalar(1)).toBe("1");
|
|
11
|
+
expect(normalizeTaskMetadataScalar(1.5)).toBe("1.5");
|
|
12
|
+
expect(normalizeTaskMetadataScalar(Number.NaN)).toBeUndefined();
|
|
13
|
+
expect(normalizeTaskMetadataScalar(Infinity)).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("stringifies booleans as JSON text", () => {
|
|
17
|
+
expect(normalizeTaskMetadataScalar(true)).toBe("true");
|
|
18
|
+
expect(normalizeTaskMetadataScalar(false)).toBe("false");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("rejects non-scalar values", () => {
|
|
22
|
+
expect(normalizeTaskMetadataScalar(null)).toBeUndefined();
|
|
23
|
+
expect(normalizeTaskMetadataScalar(undefined)).toBeUndefined();
|
|
24
|
+
expect(normalizeTaskMetadataScalar({ a: 1 })).toBeUndefined();
|
|
25
|
+
expect(normalizeTaskMetadataScalar([1])).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { MessageChunk, ToolCall } from "../MessageProtocol";
|
|
2
|
+
|
|
3
|
+
describe("ToolCall.ui", () => {
|
|
4
|
+
it("round-trips structuredContent, meta, and content blocks through JSON", () => {
|
|
5
|
+
const toolCall: ToolCall = {
|
|
6
|
+
id: "t1",
|
|
7
|
+
name: "get_widget",
|
|
8
|
+
args: {},
|
|
9
|
+
type: "tool_call",
|
|
10
|
+
response: "ok",
|
|
11
|
+
ui: {
|
|
12
|
+
structuredContent: { cpu: 42 },
|
|
13
|
+
meta: { source: "mock" },
|
|
14
|
+
contentBlocks: [
|
|
15
|
+
{ type: "image", source_type: "base64", data: "AAAA", mime_type: "image/png" },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const parsed = JSON.parse(JSON.stringify(toolCall)) as ToolCall;
|
|
21
|
+
|
|
22
|
+
expect(parsed.ui?.structuredContent).toEqual({ cpu: 42 });
|
|
23
|
+
expect(parsed.ui?.meta).toEqual({ source: "mock" });
|
|
24
|
+
expect(parsed.ui?.contentBlocks).toHaveLength(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("has no ui property after a JSON round-trip of a plain tool call", () => {
|
|
28
|
+
const parsed = JSON.parse(
|
|
29
|
+
JSON.stringify({ id: "t2", name: "echo", args: {}, type: "tool_call" })
|
|
30
|
+
) as ToolCall;
|
|
31
|
+
expect("ui" in parsed).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("lets a tool chunk carry the raw LangChain artifact", () => {
|
|
35
|
+
const chunk: MessageChunk = {
|
|
36
|
+
type: "tool",
|
|
37
|
+
data: {
|
|
38
|
+
id: "t3",
|
|
39
|
+
tool_call_id: "c3",
|
|
40
|
+
content: "done",
|
|
41
|
+
artifact: [{ type: "mcp_structured_content", data: { a: 1 } }],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
expect(Array.isArray(chunk.data.artifact)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|