@parlor/agent-ui 0.0.0-stage → 0.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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/dist/client/agent-ui-client.d.ts +60 -0
  4. package/dist/client/agent-ui-client.d.ts.map +1 -0
  5. package/dist/client/agent-ui-client.js +407 -0
  6. package/dist/client/agent-ui-client.js.map +1 -0
  7. package/dist/client/confirmations.d.ts +34 -0
  8. package/dist/client/confirmations.d.ts.map +1 -0
  9. package/dist/client/confirmations.js +95 -0
  10. package/dist/client/confirmations.js.map +1 -0
  11. package/dist/client/environment.d.ts +37 -0
  12. package/dist/client/environment.d.ts.map +1 -0
  13. package/dist/client/environment.js +79 -0
  14. package/dist/client/environment.js.map +1 -0
  15. package/dist/client/highlight.d.ts +12 -0
  16. package/dist/client/highlight.d.ts.map +1 -0
  17. package/dist/client/highlight.js +98 -0
  18. package/dist/client/highlight.js.map +1 -0
  19. package/dist/client/index.d.ts +7 -0
  20. package/dist/client/index.d.ts.map +1 -0
  21. package/dist/client/index.js +7 -0
  22. package/dist/client/index.js.map +1 -0
  23. package/dist/client/json-equal.d.ts +2 -0
  24. package/dist/client/json-equal.d.ts.map +1 -0
  25. package/dist/client/json-equal.js +20 -0
  26. package/dist/client/json-equal.js.map +1 -0
  27. package/dist/client/registry.d.ts +54 -0
  28. package/dist/client/registry.d.ts.map +1 -0
  29. package/dist/client/registry.js +280 -0
  30. package/dist/client/registry.js.map +1 -0
  31. package/dist/client/screen.d.ts +20 -0
  32. package/dist/client/screen.d.ts.map +1 -0
  33. package/dist/client/screen.js +196 -0
  34. package/dist/client/screen.js.map +1 -0
  35. package/dist/client/write.d.ts +13 -0
  36. package/dist/client/write.d.ts.map +1 -0
  37. package/dist/client/write.js +101 -0
  38. package/dist/client/write.js.map +1 -0
  39. package/dist/index.d.ts +5 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +5 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/manifest.d.ts +44 -0
  44. package/dist/manifest.d.ts.map +1 -0
  45. package/dist/manifest.js +23 -0
  46. package/dist/manifest.js.map +1 -0
  47. package/dist/pages.d.ts +11 -0
  48. package/dist/pages.d.ts.map +1 -0
  49. package/dist/pages.js +42 -0
  50. package/dist/pages.js.map +1 -0
  51. package/dist/protocol.d.ts +206 -0
  52. package/dist/protocol.d.ts.map +1 -0
  53. package/dist/protocol.js +152 -0
  54. package/dist/protocol.js.map +1 -0
  55. package/dist/server/agent-ui.d.ts +36 -0
  56. package/dist/server/agent-ui.d.ts.map +1 -0
  57. package/dist/server/agent-ui.js +89 -0
  58. package/dist/server/agent-ui.js.map +1 -0
  59. package/dist/server/audience.d.ts +9 -0
  60. package/dist/server/audience.d.ts.map +1 -0
  61. package/dist/server/audience.js +15 -0
  62. package/dist/server/audience.js.map +1 -0
  63. package/dist/server/broker.d.ts +29 -0
  64. package/dist/server/broker.d.ts.map +1 -0
  65. package/dist/server/broker.js +152 -0
  66. package/dist/server/broker.js.map +1 -0
  67. package/dist/server/confirmations.d.ts +40 -0
  68. package/dist/server/confirmations.d.ts.map +1 -0
  69. package/dist/server/confirmations.js +230 -0
  70. package/dist/server/confirmations.js.map +1 -0
  71. package/dist/server/directory.d.ts +41 -0
  72. package/dist/server/directory.d.ts.map +1 -0
  73. package/dist/server/directory.js +193 -0
  74. package/dist/server/directory.js.map +1 -0
  75. package/dist/server/index.d.ts +10 -0
  76. package/dist/server/index.d.ts.map +1 -0
  77. package/dist/server/index.js +10 -0
  78. package/dist/server/index.js.map +1 -0
  79. package/dist/server/manifest-source.d.ts +10 -0
  80. package/dist/server/manifest-source.d.ts.map +1 -0
  81. package/dist/server/manifest-source.js +36 -0
  82. package/dist/server/manifest-source.js.map +1 -0
  83. package/dist/server/routes.d.ts +11 -0
  84. package/dist/server/routes.d.ts.map +1 -0
  85. package/dist/server/routes.js +38 -0
  86. package/dist/server/routes.js.map +1 -0
  87. package/dist/server/schemas.d.ts +69 -0
  88. package/dist/server/schemas.d.ts.map +1 -0
  89. package/dist/server/schemas.js +78 -0
  90. package/dist/server/schemas.js.map +1 -0
  91. package/dist/server/tools.d.ts +49 -0
  92. package/dist/server/tools.d.ts.map +1 -0
  93. package/dist/server/tools.js +312 -0
  94. package/dist/server/tools.js.map +1 -0
  95. package/dist/view.d.ts +54 -0
  96. package/dist/view.d.ts.map +1 -0
  97. package/dist/view.js +15 -0
  98. package/dist/view.js.map +1 -0
  99. package/dist/vite/index.d.ts +3 -0
  100. package/dist/vite/index.d.ts.map +1 -0
  101. package/dist/vite/index.js +3 -0
  102. package/dist/vite/index.js.map +1 -0
  103. package/dist/vite/manifest.d.ts +5 -0
  104. package/dist/vite/manifest.d.ts.map +1 -0
  105. package/dist/vite/manifest.js +107 -0
  106. package/dist/vite/manifest.js.map +1 -0
  107. package/dist/vite/plugin.d.ts +16 -0
  108. package/dist/vite/plugin.d.ts.map +1 -0
  109. package/dist/vite/plugin.js +192 -0
  110. package/dist/vite/plugin.js.map +1 -0
  111. package/package.json +65 -3
@@ -0,0 +1,152 @@
1
+ export const UI_CHANNEL = "ui";
2
+ /** Presence names at most this many watched chat threads; a client sending more is truncated. */
3
+ export const MAX_WATCHED_THREADS = 8;
4
+ export const uiOps = {
5
+ screen: { readOnly: true, input: (value) => (isFields(value) ? {} : undefined) },
6
+ navigate: { readOnly: false, input: (value) => navigateInputOf(value) },
7
+ set: { readOnly: false, input: (value) => setInputOf(value) },
8
+ highlight: { readOnly: false, input: (value) => highlightInputOf(value) },
9
+ };
10
+ export const UI_OPS = Object.keys(uiOps);
11
+ export const isUiOp = (value) => typeof value === "string" && Object.hasOwn(uiOps, value);
12
+ export const LEGACY_UI_OPS = ["screen", "navigate"];
13
+ export const uiOpReadOnly = {
14
+ screen: uiOps.screen.readOnly,
15
+ navigate: uiOps.navigate.readOnly,
16
+ set: uiOps.set.readOnly,
17
+ highlight: uiOps.highlight.readOnly,
18
+ };
19
+ export const UI_ERROR_CODES = [
20
+ "unknown_route",
21
+ "unknown_client",
22
+ "ambiguous_client",
23
+ "not_mounted",
24
+ "not_writable",
25
+ "invalid_value",
26
+ "user_editing",
27
+ "unsupported_op",
28
+ "busy",
29
+ "timeout",
30
+ "outcome_unknown",
31
+ "internal",
32
+ ];
33
+ const isFields = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
34
+ const isString = (value) => typeof value === "string";
35
+ const isBoolean = (value) => typeof value === "boolean";
36
+ const isPositiveInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
37
+ const isStringFields = (value) => isFields(value) && Object.values(value).every(isString);
38
+ const absentOr = (value, guard) => value === undefined || guard(value);
39
+ function originOf(value) {
40
+ if (!isFields(value))
41
+ return undefined;
42
+ const { via, name } = value;
43
+ if (via !== "mcp" && via !== "agent")
44
+ return undefined;
45
+ if (!absentOr(name, isString))
46
+ return undefined;
47
+ return name === undefined ? { via } : { via, name };
48
+ }
49
+ function navigateInputOf(value) {
50
+ if (!isFields(value) || !isString(value.to))
51
+ return undefined;
52
+ const { to, params, search, replace } = value;
53
+ if (!absentOr(params, isStringFields) ||
54
+ !absentOr(search, isFields) ||
55
+ !absentOr(replace, isBoolean)) {
56
+ return undefined;
57
+ }
58
+ return {
59
+ to,
60
+ ...(params === undefined ? {} : { params }),
61
+ ...(search === undefined ? {} : { search }),
62
+ ...(replace === undefined ? {} : { replace }),
63
+ };
64
+ }
65
+ function setInputOf(value) {
66
+ if (!isFields(value) || !isString(value.view) || !isFields(value.value)) {
67
+ return undefined;
68
+ }
69
+ if (!absentOr(value.key, isString))
70
+ return undefined;
71
+ const { view, key } = value;
72
+ return key === undefined
73
+ ? { view, value: value.value }
74
+ : { view, key, value: value.value };
75
+ }
76
+ function highlightInputOf(value) {
77
+ if (!isFields(value) || !isString(value.view))
78
+ return undefined;
79
+ if (!absentOr(value.key, isString) || !absentOr(value.field, isString))
80
+ return undefined;
81
+ const { view, key, field } = value;
82
+ return {
83
+ view,
84
+ ...(key === undefined ? {} : { key }),
85
+ ...(field === undefined ? {} : { field }),
86
+ };
87
+ }
88
+ function requestOf(json) {
89
+ if (!isFields(json))
90
+ return undefined;
91
+ const { id, op, input, timeoutMs } = json;
92
+ const origin = originOf(json.origin);
93
+ if (!isString(id) || id === "" || !origin || !isPositiveInteger(timeoutMs)) {
94
+ return undefined;
95
+ }
96
+ if (!isUiOp(op))
97
+ return undefined;
98
+ const parsed = uiOps[op].input(input);
99
+ return parsed === undefined
100
+ ? undefined
101
+ : { id, origin, timeoutMs, op, input: parsed };
102
+ }
103
+ const isNonNegativeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
104
+ function promptOf(json) {
105
+ if (!isFields(json))
106
+ return undefined;
107
+ const { id, action, description, input, expiresInMs } = json;
108
+ const origin = originOf(json.origin);
109
+ if (!isString(id) || id === "" || !isString(action) || !isString(description)) {
110
+ return undefined;
111
+ }
112
+ if (!origin || !isNonNegativeInteger(expiresInMs))
113
+ return undefined;
114
+ const { threadId } = json;
115
+ return {
116
+ id,
117
+ action,
118
+ description,
119
+ input,
120
+ origin,
121
+ expiresInMs,
122
+ ...(isString(threadId) && threadId !== "" ? { threadId } : {}),
123
+ };
124
+ }
125
+ function confirmationsOf(json) {
126
+ if (!Array.isArray(json.items))
127
+ return undefined;
128
+ const items = json.items.flatMap((item) => promptOf(item) ?? []);
129
+ return { type: "confirmations", items };
130
+ }
131
+ function parseJson(data) {
132
+ try {
133
+ return JSON.parse(data);
134
+ }
135
+ catch {
136
+ return undefined;
137
+ }
138
+ }
139
+ const idOf = (json) => {
140
+ const id = isFields(json) ? json.id : undefined;
141
+ return isString(id) && id !== "" ? id : undefined;
142
+ };
143
+ export function parseUiFrame(data) {
144
+ const json = parseJson(data);
145
+ if (isFields(json) && json.type === "confirmations") {
146
+ const frame = confirmationsOf(json);
147
+ return frame ? { ok: true, frame } : { ok: false, id: undefined };
148
+ }
149
+ const request = requestOf(json);
150
+ return request ? { ok: true, frame: request } : { ok: false, id: idOf(json) };
151
+ }
152
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ActionSource } from \"@parlor/core\";\n\nimport type { ViewRef } from \"./view.js\";\n\nexport const UI_CHANNEL = \"ui\";\n\n/** Presence names at most this many watched chat threads; a client sending more is truncated. */\nexport const MAX_WATCHED_THREADS = 8;\n\nexport interface UiOpInputs {\n readonly screen: Readonly<Record<string, never>>;\n readonly navigate: {\n readonly to: string;\n readonly params?: Readonly<Record<string, string>> | undefined;\n readonly search?: Readonly<Record<string, unknown>> | undefined;\n readonly replace?: boolean | undefined;\n };\n readonly set: {\n readonly view: string;\n readonly key?: string | undefined;\n readonly value: JsonObject;\n };\n readonly highlight: {\n readonly view: string;\n readonly key?: string | undefined;\n readonly field?: string | undefined;\n };\n}\n\nexport type UiOp = keyof UiOpInputs;\n\nexport type UiOpInput<O extends UiOp> = UiOpInputs[O];\n\nexport interface UiOpResults {\n readonly screen: ScreenSnapshot;\n readonly navigate: { readonly screen: ScreenSnapshot };\n readonly set: { readonly value: JsonObject; readonly agentFields: readonly string[] };\n readonly highlight: { readonly shown: number };\n}\n\ninterface UiOpSpec<O extends UiOp> {\n readonly readOnly: boolean;\n readonly input: (value: unknown) => UiOpInput<O> | undefined;\n}\n\nexport const uiOps: { readonly [O in UiOp]: UiOpSpec<O> } = {\n screen: { readOnly: true, input: (value) => (isFields(value) ? {} : undefined) },\n navigate: { readOnly: false, input: (value) => navigateInputOf(value) },\n set: { readOnly: false, input: (value) => setInputOf(value) },\n highlight: { readOnly: false, input: (value) => highlightInputOf(value) },\n};\n\nexport const UI_OPS = Object.keys(uiOps) as readonly UiOp[];\n\nexport const isUiOp = (value: unknown): value is UiOp =>\n typeof value === \"string\" && Object.hasOwn(uiOps, value);\n\nexport const LEGACY_UI_OPS: readonly UiOp[] = [\"screen\", \"navigate\"];\n\nexport const uiOpReadOnly: { readonly [O in UiOp]: boolean } = {\n screen: uiOps.screen.readOnly,\n navigate: uiOps.navigate.readOnly,\n set: uiOps.set.readOnly,\n highlight: uiOps.highlight.readOnly,\n};\n\nexport interface AgentOrigin {\n readonly via: Extract<ActionSource, \"mcp\" | \"agent\">;\n readonly name?: string | undefined;\n}\n\nexport type UiRequest = {\n readonly [O in UiOp]: {\n readonly id: string;\n readonly op: O;\n readonly input: UiOpInput<O>;\n readonly origin: AgentOrigin;\n readonly timeoutMs: number;\n };\n}[UiOp];\n\nexport const UI_ERROR_CODES = [\n \"unknown_route\",\n \"unknown_client\",\n \"ambiguous_client\",\n \"not_mounted\",\n \"not_writable\",\n \"invalid_value\",\n \"user_editing\",\n \"unsupported_op\",\n \"busy\",\n \"timeout\",\n \"outcome_unknown\",\n \"internal\",\n] as const;\n\nexport type UiErrorCode = (typeof UI_ERROR_CODES)[number];\n\nexport interface UiError {\n readonly code: UiErrorCode;\n readonly message: string;\n readonly issues?:\n | readonly { readonly path: string; readonly message: string }[]\n | undefined;\n}\n\nexport type UiReply =\n | {\n readonly type: \"reply\";\n readonly id: string;\n readonly ok: true;\n readonly result: unknown;\n }\n | {\n readonly type: \"reply\";\n readonly id: string;\n readonly ok: false;\n readonly error: UiError;\n };\n\nexport interface PresenceUpdate {\n readonly type: \"presence\";\n readonly focused: boolean;\n readonly visible: boolean;\n readonly platform: string;\n readonly buildId?: string | undefined;\n readonly ops?: readonly string[] | undefined;\n readonly canConfirm?: boolean | undefined;\n /** Chat threads this client shows; a confirmation bound to one reaches every watcher. */\n readonly watching?: readonly string[] | undefined;\n}\n\nexport interface LeaveNotice {\n readonly type: \"leave\";\n}\n\nexport interface ConfirmationAnswer {\n readonly type: \"answer\";\n readonly id: string;\n readonly approve: boolean;\n}\n\nexport type ClientMessage = UiReply | PresenceUpdate | LeaveNotice | ConfirmationAnswer;\n\nexport interface ConfirmationPrompt {\n readonly id: string;\n readonly action: string;\n readonly description: string;\n readonly input: unknown;\n readonly origin: AgentOrigin;\n readonly expiresInMs: number;\n /** The chat thread whose turn asked; the chat renders the card inline there. */\n readonly threadId?: string | undefined;\n}\n\nexport interface ConfirmationsFrame {\n readonly type: \"confirmations\";\n readonly items: readonly ConfirmationPrompt[];\n}\n\nexport type UiFrame = UiRequest | ConfirmationsFrame;\n\nexport type JsonObject = { readonly [key: string]: unknown };\n\nexport interface TruncatedValue {\n readonly truncated: true;\n readonly bytes: number;\n}\n\nexport interface MatchedRouteSnapshot {\n readonly notFound: false;\n readonly path: string;\n readonly params: Readonly<Record<string, string>>;\n readonly search: JsonObject;\n readonly url: string;\n}\n\nexport interface NotFoundRouteSnapshot {\n readonly notFound: true;\n readonly url: string;\n}\n\nexport type RouteSnapshot = MatchedRouteSnapshot | NotFoundRouteSnapshot;\n\nexport type QueryStatus = \"pending\" | \"error\" | \"success\";\n\nexport interface QuerySnapshot {\n readonly action: string;\n readonly input: unknown;\n readonly status: QueryStatus;\n}\n\nexport interface SingleViewSnapshot {\n readonly value: JsonObject | TruncatedValue;\n readonly shown?: number | undefined;\n}\n\nexport interface ItemSnapshot {\n readonly key: string;\n readonly value: JsonObject | TruncatedValue;\n readonly shown: number;\n}\n\nexport interface KeyedViewSnapshot {\n readonly items: readonly ItemSnapshot[];\n readonly offscreen: number;\n}\n\nexport type ViewSnapshot = SingleViewSnapshot | KeyedViewSnapshot;\n\nexport interface RecentRef extends ViewRef {\n readonly agoMs: number;\n}\n\nexport interface FocusSnapshot {\n readonly view?: string | undefined;\n readonly key?: string | undefined;\n readonly field?: string | undefined;\n readonly label?: string | undefined;\n}\n\nexport interface SelectionSnapshot {\n readonly text: string;\n readonly within: ViewRef;\n readonly truncated: boolean;\n}\n\nexport interface AttentionSnapshot {\n readonly focus: FocusSnapshot | null;\n readonly selection: SelectionSnapshot | null;\n readonly lastInteracted: RecentRef | null;\n readonly lastEntered: RecentRef | null;\n readonly focal: ViewRef | null;\n}\n\nexport interface ScreenSnapshot {\n readonly route: RouteSnapshot | null;\n readonly queries: readonly QuerySnapshot[];\n readonly views: Readonly<Record<string, ViewSnapshot>>;\n readonly attention: AttentionSnapshot;\n readonly truncated: boolean;\n}\n\nexport interface ClientSummary {\n readonly id: string;\n readonly platform: string;\n readonly buildId?: string | undefined;\n readonly focused: boolean;\n readonly visible: boolean;\n readonly seenAgoMs: number;\n}\n\nexport type ParsedUiFrame =\n | { readonly ok: true; readonly frame: UiFrame }\n | { readonly ok: false; readonly id: string | undefined };\n\ntype Fields = { readonly [key: string]: unknown };\n\nconst isFields = (value: unknown): value is Fields =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isString = (value: unknown): value is string => typeof value === \"string\";\n\nconst isBoolean = (value: unknown): value is boolean => typeof value === \"boolean\";\n\nconst isPositiveInteger = (value: unknown): value is number =>\n typeof value === \"number\" && Number.isSafeInteger(value) && value > 0;\n\nconst isStringFields = (value: unknown): value is Readonly<Record<string, string>> =>\n isFields(value) && Object.values(value).every(isString);\n\nconst absentOr = <T>(\n value: unknown,\n guard: (value: unknown) => value is T,\n): value is T | undefined => value === undefined || guard(value);\n\nfunction originOf(value: unknown): AgentOrigin | undefined {\n if (!isFields(value)) return undefined;\n const { via, name } = value;\n if (via !== \"mcp\" && via !== \"agent\") return undefined;\n if (!absentOr(name, isString)) return undefined;\n return name === undefined ? { via } : { via, name };\n}\n\nfunction navigateInputOf(value: unknown): UiOpInput<\"navigate\"> | undefined {\n if (!isFields(value) || !isString(value.to)) return undefined;\n const { to, params, search, replace } = value;\n if (\n !absentOr(params, isStringFields) ||\n !absentOr(search, isFields) ||\n !absentOr(replace, isBoolean)\n ) {\n return undefined;\n }\n return {\n to,\n ...(params === undefined ? {} : { params }),\n ...(search === undefined ? {} : { search }),\n ...(replace === undefined ? {} : { replace }),\n };\n}\n\nfunction setInputOf(value: unknown): UiOpInput<\"set\"> | undefined {\n if (!isFields(value) || !isString(value.view) || !isFields(value.value)) {\n return undefined;\n }\n if (!absentOr(value.key, isString)) return undefined;\n const { view, key } = value;\n return key === undefined\n ? { view, value: value.value }\n : { view, key, value: value.value };\n}\n\nfunction highlightInputOf(value: unknown): UiOpInput<\"highlight\"> | undefined {\n if (!isFields(value) || !isString(value.view)) return undefined;\n if (!absentOr(value.key, isString) || !absentOr(value.field, isString))\n return undefined;\n const { view, key, field } = value;\n return {\n view,\n ...(key === undefined ? {} : { key }),\n ...(field === undefined ? {} : { field }),\n };\n}\n\nfunction requestOf(json: unknown): UiRequest | undefined {\n if (!isFields(json)) return undefined;\n const { id, op, input, timeoutMs } = json;\n const origin = originOf(json.origin);\n if (!isString(id) || id === \"\" || !origin || !isPositiveInteger(timeoutMs)) {\n return undefined;\n }\n if (!isUiOp(op)) return undefined;\n const parsed = uiOps[op].input(input);\n return parsed === undefined\n ? undefined\n : ({ id, origin, timeoutMs, op, input: parsed } as UiRequest);\n}\n\nconst isNonNegativeInteger = (value: unknown): value is number =>\n typeof value === \"number\" && Number.isSafeInteger(value) && value >= 0;\n\nfunction promptOf(json: unknown): ConfirmationPrompt | undefined {\n if (!isFields(json)) return undefined;\n const { id, action, description, input, expiresInMs } = json;\n const origin = originOf(json.origin);\n if (!isString(id) || id === \"\" || !isString(action) || !isString(description)) {\n return undefined;\n }\n if (!origin || !isNonNegativeInteger(expiresInMs)) return undefined;\n const { threadId } = json;\n return {\n id,\n action,\n description,\n input,\n origin,\n expiresInMs,\n ...(isString(threadId) && threadId !== \"\" ? { threadId } : {}),\n };\n}\n\nfunction confirmationsOf(json: Fields): ConfirmationsFrame | undefined {\n if (!Array.isArray(json.items)) return undefined;\n const items = json.items.flatMap((item) => promptOf(item) ?? []);\n return { type: \"confirmations\", items };\n}\n\nfunction parseJson(data: string): unknown {\n try {\n return JSON.parse(data);\n } catch {\n return undefined;\n }\n}\n\nconst idOf = (json: unknown) => {\n const id = isFields(json) ? json.id : undefined;\n return isString(id) && id !== \"\" ? id : undefined;\n};\n\nexport function parseUiFrame(data: string): ParsedUiFrame {\n const json = parseJson(data);\n if (isFields(json) && json.type === \"confirmations\") {\n const frame = confirmationsOf(json);\n return frame ? { ok: true, frame } : { ok: false, id: undefined };\n }\n const request = requestOf(json);\n return request ? { ok: true, frame: request } : { ok: false, id: idOf(json) };\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import type { ServerChannel } from "@parlor/sync";
2
+ import type { UiManifest } from "../manifest.js";
3
+ import { type AgentOrigin, type ClientMessage } from "../protocol.js";
4
+ import { type UiReceipt } from "./broker.js";
5
+ import { type ConfirmationOutcome, type ConfirmationRequest } from "./confirmations.js";
6
+ import { type UiManifestSource } from "./manifest-source.js";
7
+ import { type AgentTool, type AgentToolContext } from "./tools.js";
8
+ export type ExternalUiPolicy = boolean | "read-only";
9
+ export interface AgentUiOptions {
10
+ readonly external?: ExternalUiPolicy | undefined;
11
+ readonly timeoutMs?: number | undefined;
12
+ readonly confirmTimeoutMs?: number | undefined;
13
+ }
14
+ export interface AgentUiDependencies {
15
+ readonly audience: (request: Request) => string | Promise<string>;
16
+ readonly webOrigin?: string | undefined;
17
+ }
18
+ export interface AgentUi {
19
+ readonly channel: ServerChannel;
20
+ touch(clientId: string | undefined, audience: string): void;
21
+ receive(clientId: string, audience: string, message: ClientMessage): UiReceipt;
22
+ tools(surface: "mcp" | "agent"): readonly AgentTool[];
23
+ /** `chat` binds an inline agent's `ui_*` calls to the client its turn's prompt came from. */
24
+ contextFor(request: Request | undefined, origin: AgentOrigin, signal?: AbortSignal, chat?: {
25
+ readonly clientId: string;
26
+ }): Promise<AgentToolContext>;
27
+ instructions(surface: "mcp" | "agent"): string | undefined;
28
+ confirm(request: ConfirmationRequest): Promise<ConfirmationOutcome>;
29
+ manifest(): UiManifest | undefined;
30
+ install(source: UiManifestSource): void;
31
+ close(): void;
32
+ }
33
+ export declare const DEFAULT_UI_TIMEOUT_MS = 10000;
34
+ export declare function webOriginFromEnv(value?: string | undefined): string | undefined;
35
+ export declare function createAgentUi(options: AgentUiOptions & AgentUiDependencies): AgentUi;
36
+ //# sourceMappingURL=agent-ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-ui.d.ts","sourceRoot":"","sources":["../../src/server/agent-ui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAc,MAAM,gBAAgB,CAAC;AAClF,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EAEzB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,KAAK,SAAS,EACd,KAAK,gBAAgB,EAGtB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,WAAW,CAAC;AAErD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACjD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5D,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,SAAS,CAAC;IAC/E,KAAK,CAAC,OAAO,EAAE,KAAK,GAAG,OAAO,GAAG,SAAS,SAAS,EAAE,CAAC;IACtD,6FAA6F;IAC7F,UAAU,CACR,OAAO,EAAE,OAAO,GAAG,SAAS,EAC5B,MAAM,EAAE,WAAW,EACnB,MAAM,CAAC,EAAE,WAAW,EACpB,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GACnC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC7B,YAAY,CAAC,OAAO,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IAC3D,OAAO,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACpE,QAAQ,IAAI,UAAU,GAAG,SAAS,CAAC;IACnC,OAAO,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACxC,KAAK,IAAI,IAAI,CAAC;CACf;AAED,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAE5C,wBAAgB,gBAAgB,CAC9B,KAAK,GAAE,MAAM,GAAG,SAAyC,GACxD,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,mBAAmB,GAAG,OAAO,CA0EpF"}
@@ -0,0 +1,89 @@
1
+ import { UI_CHANNEL } from "../protocol.js";
2
+ import { UiBroker } from "./broker.js";
3
+ import { ConfirmationBoard, DEFAULT_CONFIRM_TIMEOUT_MS, } from "./confirmations.js";
4
+ import { ClientDirectory } from "./directory.js";
5
+ import { normalizeManifest, } from "./manifest-source.js";
6
+ import { createUiTools, uiInstructions, } from "./tools.js";
7
+ export const DEFAULT_UI_TIMEOUT_MS = 10_000;
8
+ export function webOriginFromEnv(value = process.env.PARLOR_WEB_ORIGIN) {
9
+ return value && /^https?:\/\//.test(value) ? value : undefined;
10
+ }
11
+ export function createAgentUi(options) {
12
+ const external = options.external ?? true;
13
+ const webOrigin = "webOrigin" in options ? options.webOrigin : webOriginFromEnv();
14
+ const directory = new ClientDirectory();
15
+ const confirmations = new ConfirmationBoard({
16
+ directory,
17
+ timeoutMs: options.confirmTimeoutMs ?? DEFAULT_CONFIRM_TIMEOUT_MS,
18
+ });
19
+ const broker = new UiBroker({ timeoutMs: options.timeoutMs ?? DEFAULT_UI_TIMEOUT_MS });
20
+ const toolsFor = createUiTools({ directory, broker });
21
+ let source = () => undefined;
22
+ let normalized;
23
+ const manifest = () => {
24
+ const input = source();
25
+ if (!input)
26
+ return undefined;
27
+ if (normalized?.input !== input) {
28
+ normalized = { input, manifest: normalizeManifest(input) };
29
+ }
30
+ return normalized.manifest;
31
+ };
32
+ const tools = (surface) => {
33
+ const allTools = toolsFor(manifest());
34
+ if (surface === "agent" || external === true)
35
+ return allTools;
36
+ if (external === "read-only")
37
+ return allTools.filter((tool) => tool.readOnly);
38
+ return [];
39
+ };
40
+ return {
41
+ channel: {
42
+ event: UI_CHANNEL,
43
+ connect: (connection) => directory.connect(connection),
44
+ },
45
+ touch: (clientId, audience) => directory.touch(clientId, audience),
46
+ receive(clientId, audience, message) {
47
+ switch (message.type) {
48
+ case "presence": {
49
+ const outcome = directory.presence(clientId, audience, message);
50
+ const target = directory.target(clientId);
51
+ if (outcome === "registered" && target)
52
+ broker.reconnected(target);
53
+ return { accepted: outcome !== "rejected" };
54
+ }
55
+ case "leave": {
56
+ const left = directory.leave(clientId, audience);
57
+ if (left)
58
+ broker.abandon(clientId);
59
+ return { accepted: left };
60
+ }
61
+ case "reply":
62
+ return broker.reply(clientId, message);
63
+ case "answer":
64
+ return { accepted: confirmations.answer(clientId, audience, message) };
65
+ }
66
+ },
67
+ tools,
68
+ async contextFor(request, origin, signal, chat) {
69
+ return {
70
+ scope: request ? await options.audience(request) : "default",
71
+ ...(chat ? { clientId: chat.clientId } : {}),
72
+ origin,
73
+ signal,
74
+ webOrigin: webOrigin ?? (request ? new URL(request.url).origin : undefined),
75
+ };
76
+ },
77
+ instructions: (surface) => (tools(surface).length > 0 ? uiInstructions() : undefined),
78
+ confirm: (request) => confirmations.ask(request),
79
+ manifest,
80
+ install(next) {
81
+ source = next;
82
+ },
83
+ close: () => {
84
+ broker.close();
85
+ confirmations.close();
86
+ },
87
+ };
88
+ }
89
+ //# sourceMappingURL=agent-ui.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-ui.js","sourceRoot":"","sources":["../../src/server/agent-ui.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ServerChannel } from \"@parlor/sync\";\n\nimport type { UiManifest } from \"../manifest.js\";\nimport { type AgentOrigin, type ClientMessage, UI_CHANNEL } from \"../protocol.js\";\nimport { UiBroker, type UiReceipt } from \"./broker.js\";\nimport {\n ConfirmationBoard,\n type ConfirmationOutcome,\n type ConfirmationRequest,\n DEFAULT_CONFIRM_TIMEOUT_MS,\n} from \"./confirmations.js\";\nimport { ClientDirectory } from \"./directory.js\";\nimport {\n normalizeManifest,\n type UiManifestInput,\n type UiManifestSource,\n} from \"./manifest-source.js\";\nimport {\n type AgentTool,\n type AgentToolContext,\n createUiTools,\n uiInstructions,\n} from \"./tools.js\";\n\nexport type ExternalUiPolicy = boolean | \"read-only\";\n\nexport interface AgentUiOptions {\n readonly external?: ExternalUiPolicy | undefined;\n readonly timeoutMs?: number | undefined;\n readonly confirmTimeoutMs?: number | undefined;\n}\n\nexport interface AgentUiDependencies {\n readonly audience: (request: Request) => string | Promise<string>;\n readonly webOrigin?: string | undefined;\n}\n\nexport interface AgentUi {\n readonly channel: ServerChannel;\n touch(clientId: string | undefined, audience: string): void;\n receive(clientId: string, audience: string, message: ClientMessage): UiReceipt;\n tools(surface: \"mcp\" | \"agent\"): readonly AgentTool[];\n /** `chat` binds an inline agent's `ui_*` calls to the client its turn's prompt came from. */\n contextFor(\n request: Request | undefined,\n origin: AgentOrigin,\n signal?: AbortSignal,\n chat?: { readonly clientId: string },\n ): Promise<AgentToolContext>;\n instructions(surface: \"mcp\" | \"agent\"): string | undefined;\n confirm(request: ConfirmationRequest): Promise<ConfirmationOutcome>;\n manifest(): UiManifest | undefined;\n install(source: UiManifestSource): void;\n close(): void;\n}\n\nexport const DEFAULT_UI_TIMEOUT_MS = 10_000;\n\nexport function webOriginFromEnv(\n value: string | undefined = process.env.PARLOR_WEB_ORIGIN,\n): string | undefined {\n return value && /^https?:\\/\\//.test(value) ? value : undefined;\n}\n\nexport function createAgentUi(options: AgentUiOptions & AgentUiDependencies): AgentUi {\n const external = options.external ?? true;\n const webOrigin = \"webOrigin\" in options ? options.webOrigin : webOriginFromEnv();\n const directory = new ClientDirectory();\n const confirmations = new ConfirmationBoard({\n directory,\n timeoutMs: options.confirmTimeoutMs ?? DEFAULT_CONFIRM_TIMEOUT_MS,\n });\n const broker = new UiBroker({ timeoutMs: options.timeoutMs ?? DEFAULT_UI_TIMEOUT_MS });\n const toolsFor = createUiTools({ directory, broker });\n let source: UiManifestSource = () => undefined;\n let normalized: { input: UiManifestInput; manifest: UiManifest } | undefined;\n const manifest = (): UiManifest | undefined => {\n const input = source();\n if (!input) return undefined;\n if (normalized?.input !== input) {\n normalized = { input, manifest: normalizeManifest(input) };\n }\n return normalized.manifest;\n };\n\n const tools = (surface: \"mcp\" | \"agent\") => {\n const allTools = toolsFor(manifest());\n if (surface === \"agent\" || external === true) return allTools;\n if (external === \"read-only\") return allTools.filter((tool) => tool.readOnly);\n return [];\n };\n\n return {\n channel: {\n event: UI_CHANNEL,\n connect: (connection) => directory.connect(connection),\n },\n touch: (clientId, audience) => directory.touch(clientId, audience),\n receive(clientId, audience, message) {\n switch (message.type) {\n case \"presence\": {\n const outcome = directory.presence(clientId, audience, message);\n const target = directory.target(clientId);\n if (outcome === \"registered\" && target) broker.reconnected(target);\n return { accepted: outcome !== \"rejected\" };\n }\n case \"leave\": {\n const left = directory.leave(clientId, audience);\n if (left) broker.abandon(clientId);\n return { accepted: left };\n }\n case \"reply\":\n return broker.reply(clientId, message);\n case \"answer\":\n return { accepted: confirmations.answer(clientId, audience, message) };\n }\n },\n tools,\n async contextFor(request, origin, signal, chat) {\n return {\n scope: request ? await options.audience(request) : \"default\",\n ...(chat ? { clientId: chat.clientId } : {}),\n origin,\n signal,\n webOrigin: webOrigin ?? (request ? new URL(request.url).origin : undefined),\n };\n },\n instructions: (surface) => (tools(surface).length > 0 ? uiInstructions() : undefined),\n confirm: (request) => confirmations.ask(request),\n manifest,\n install(next) {\n source = next;\n },\n close: () => {\n broker.close();\n confirmations.close();\n },\n };\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import { type Principal, type UserId } from "@parlor/core";
2
+ /**
3
+ * Whose screens a request may see and drive: its scope and, once signed in, its user.
4
+ * Without auth, or as the dev user, it is the scope itself.
5
+ */
6
+ export declare function audienceOf(scope: string, principal: Principal | undefined): string;
7
+ /** `audienceOf` for a known user: the sync stream only that user's tabs and computers hear. */
8
+ export declare function userAudience(scope: string, userId: UserId | undefined): string;
9
+ //# sourceMappingURL=audience.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audience.d.ts","sourceRoot":"","sources":["../../src/server/audience.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,SAAS,EAAE,KAAK,MAAM,EAAU,MAAM,cAAc,CAAC;AAEhF;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAElF;AAED,+FAA+F;AAC/F,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAI9E"}
@@ -0,0 +1,15 @@
1
+ import { DEV_USER_ID, userOf } from "@parlor/core";
2
+ /**
3
+ * Whose screens a request may see and drive: its scope and, once signed in, its user.
4
+ * Without auth, or as the dev user, it is the scope itself.
5
+ */
6
+ export function audienceOf(scope, principal) {
7
+ return userAudience(scope, userOf(principal));
8
+ }
9
+ /** `audienceOf` for a known user: the sync stream only that user's tabs and computers hear. */
10
+ export function userAudience(scope, userId) {
11
+ return userId === undefined || userId === DEV_USER_ID
12
+ ? scope
13
+ : `${scope}\u001f${userId}`;
14
+ }
15
+ //# sourceMappingURL=audience.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audience.js","sourceRoot":"","sources":["../../src/server/audience.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAA+B,MAAM,EAAE,MAAM,cAAc,CAAC;AAEhF;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,SAAgC;IACxE,OAAO,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;AAChD,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,YAAY,CAAC,KAAa,EAAE,MAA0B;IACpE,OAAO,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,WAAW;QACnD,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,GAAG,KAAK,SAAS,MAAM,EAAE,CAAC;AAChC,CAAC","sourcesContent":["import { DEV_USER_ID, type Principal, type UserId, userOf } from \"@parlor/core\";\n\n/**\n * Whose screens a request may see and drive: its scope and, once signed in, its user.\n * Without auth, or as the dev user, it is the scope itself.\n */\nexport function audienceOf(scope: string, principal: Principal | undefined): string {\n return userAudience(scope, userOf(principal));\n}\n\n/** `audienceOf` for a known user: the sync stream only that user's tabs and computers hear. */\nexport function userAudience(scope: string, userId: UserId | undefined): string {\n return userId === undefined || userId === DEV_USER_ID\n ? scope\n : `${scope}\\u001f${userId}`;\n}\n"]}
@@ -0,0 +1,29 @@
1
+ import { type AgentOrigin, type UiError, type UiOp, type UiOpInput, type UiOpResults, type UiReply } from "../protocol.js";
2
+ import type { ClientTarget } from "./directory.js";
3
+ export declare class UiFailure extends Error {
4
+ readonly error: UiError;
5
+ constructor(error: UiError);
6
+ }
7
+ export interface BrokerOptions {
8
+ readonly timeoutMs: number;
9
+ readonly maxPendingPerClient?: number | undefined;
10
+ readonly maxPending?: number | undefined;
11
+ readonly newId?: (() => string) | undefined;
12
+ readonly now?: (() => number) | undefined;
13
+ readonly setTimeout?: typeof globalThis.setTimeout | undefined;
14
+ readonly clearTimeout?: typeof globalThis.clearTimeout | undefined;
15
+ }
16
+ export interface UiReceipt {
17
+ readonly accepted: boolean;
18
+ readonly next?: string | undefined;
19
+ }
20
+ export declare class UiBroker {
21
+ #private;
22
+ constructor(options: BrokerOptions);
23
+ request<O extends UiOp>(target: ClientTarget, op: O, input: UiOpInput<O>, origin: AgentOrigin, signal?: AbortSignal): Promise<UiOpResults[O]>;
24
+ reply(clientId: string, reply: UiReply): UiReceipt;
25
+ reconnected(target: ClientTarget): void;
26
+ abandon(clientId: string): void;
27
+ close(): void;
28
+ }
29
+ //# sourceMappingURL=broker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broker.d.ts","sourceRoot":"","sources":["../../src/server/broker.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,IAAI,EACT,KAAK,SAAS,EACd,KAAK,WAAW,EAEhB,KAAK,OAAO,EACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,qBAAa,SAAU,SAAQ,KAAK;IACtB,QAAQ,CAAC,KAAK,EAAE,OAAO;IAAnC,YAAqB,KAAK,EAAE,OAAO,EAElC;CACF;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,UAAU,CAAC,UAAU,GAAG,SAAS,CAAC;IAC/D,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,UAAU,CAAC,YAAY,GAAG,SAAS,CAAC;CACpE;AAiBD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC;AAqBD,qBAAa,QAAQ;;IAWnB,YAAY,OAAO,EAAE,aAAa,EASjC;IAED,OAAO,CAAC,CAAC,SAAS,IAAI,EACpB,MAAM,EAAE,YAAY,EACpB,EAAE,EAAE,CAAC,EACL,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EACnB,MAAM,EAAE,WAAW,EACnB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CA+CzB;IAED,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,CAKjD;IAED,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAMtC;IAED,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAO9B;IAED,KAAK,IAAI,IAAI,CAOZ;CA+BF"}
@@ -0,0 +1,152 @@
1
+ import { uiOpReadOnly, } from "../protocol.js";
2
+ export class UiFailure extends Error {
3
+ error;
4
+ constructor(error) {
5
+ super(error.message);
6
+ this.error = error;
7
+ }
8
+ }
9
+ function unanswered(pending, reason) {
10
+ if (!pending.dispatched) {
11
+ return { code: "timeout", message: `${reason} The app did not receive it.` };
12
+ }
13
+ if (pending.readOnly) {
14
+ return {
15
+ code: "timeout",
16
+ message: `${reason} Nothing on screen changed; retrying is safe.`,
17
+ };
18
+ }
19
+ return {
20
+ code: "outcome_unknown",
21
+ message: `${reason} The app may still have applied it; read the screen before retrying.`,
22
+ };
23
+ }
24
+ const DEFAULT_MAX_PENDING_PER_CLIENT = 8;
25
+ const DEFAULT_MAX_PENDING = 256;
26
+ export class UiBroker {
27
+ #timeoutMs;
28
+ #maxPendingPerClient;
29
+ #maxPending;
30
+ #newId;
31
+ #now;
32
+ #setTimeout;
33
+ #clearTimeout;
34
+ #pending = new Map();
35
+ #lanes = new Map();
36
+ constructor(options) {
37
+ this.#timeoutMs = options.timeoutMs;
38
+ this.#maxPendingPerClient =
39
+ options.maxPendingPerClient ?? DEFAULT_MAX_PENDING_PER_CLIENT;
40
+ this.#maxPending = options.maxPending ?? DEFAULT_MAX_PENDING;
41
+ this.#newId = options.newId ?? (() => crypto.randomUUID());
42
+ this.#now = options.now ?? (() => Date.now());
43
+ this.#setTimeout = options.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms));
44
+ this.#clearTimeout = options.clearTimeout ?? ((id) => globalThis.clearTimeout(id));
45
+ }
46
+ request(target, op, input, origin, signal) {
47
+ const clientId = target.client.id;
48
+ if (this.#pending.size >= this.#maxPending ||
49
+ (this.#lanes.get(clientId)?.queue.length ?? 0) >= this.#maxPendingPerClient) {
50
+ return Promise.reject(new UiFailure({
51
+ code: "busy",
52
+ message: "Too many UI requests are waiting for this app; try again shortly.",
53
+ }));
54
+ }
55
+ const id = this.#newId();
56
+ const frame = JSON.stringify({ id, op, input, origin, timeoutMs: this.#timeoutMs });
57
+ return new Promise((resolve, reject) => {
58
+ const giveUp = (reason) => this.#giveUp(pending, reason);
59
+ const onAbort = () => giveUp("The UI request was cancelled.");
60
+ const timer = this.#setTimeout(() => giveUp(`The app did not answer within ${this.#timeoutMs} ms.`), this.#timeoutMs);
61
+ const pending = {
62
+ id,
63
+ clientId,
64
+ frame,
65
+ readOnly: uiOpReadOnly[op],
66
+ deadline: this.#now() + this.#timeoutMs,
67
+ dispatched: false,
68
+ settle: (outcome) => {
69
+ this.#clearTimeout(timer);
70
+ signal?.removeEventListener("abort", onAbort);
71
+ if (!("type" in outcome))
72
+ reject(new UiFailure(outcome));
73
+ else if (outcome.ok)
74
+ resolve(outcome.result);
75
+ else
76
+ reject(new UiFailure(outcome.error));
77
+ },
78
+ };
79
+ this.#pending.set(id, pending);
80
+ const lane = this.#lanes.get(clientId) ?? { target, queue: [] };
81
+ lane.target = target;
82
+ lane.queue.push(pending);
83
+ this.#lanes.set(clientId, lane);
84
+ signal?.addEventListener("abort", onAbort, { once: true });
85
+ if (lane.queue[0] === pending)
86
+ pending.dispatched = target.send(frame);
87
+ if (signal?.aborted)
88
+ onAbort();
89
+ });
90
+ }
91
+ reply(clientId, reply) {
92
+ const pending = this.#pending.get(reply.id);
93
+ if (!pending || pending.clientId !== clientId)
94
+ return { accepted: false };
95
+ this.#finish(pending, reply);
96
+ return { accepted: true, next: this.#handOver(clientId) };
97
+ }
98
+ reconnected(target) {
99
+ const lane = this.#lanes.get(target.client.id);
100
+ if (!lane)
101
+ return;
102
+ lane.target = target;
103
+ const head = lane.queue[0];
104
+ if (head && target.send(head.frame))
105
+ head.dispatched = true;
106
+ }
107
+ abandon(clientId) {
108
+ for (const pending of [...(this.#lanes.get(clientId)?.queue ?? [])]) {
109
+ this.#finish(pending, unanswered(pending, "The app client closed before it answered."));
110
+ }
111
+ }
112
+ close() {
113
+ for (const pending of [...this.#pending.values()]) {
114
+ this.#finish(pending, {
115
+ code: "internal",
116
+ message: "The server is shutting down.",
117
+ });
118
+ }
119
+ }
120
+ #giveUp(pending, reason) {
121
+ if (!this.#finish(pending, unanswered(pending, reason)))
122
+ return;
123
+ const lane = this.#lanes.get(pending.clientId);
124
+ const next = this.#waiting(pending.clientId);
125
+ if (lane && next)
126
+ next.dispatched = lane.target.send(next.frame);
127
+ }
128
+ #handOver(clientId) {
129
+ const next = this.#waiting(clientId);
130
+ if (!next)
131
+ return undefined;
132
+ next.dispatched = true;
133
+ return next.frame;
134
+ }
135
+ #waiting(clientId) {
136
+ const head = this.#lanes.get(clientId)?.queue[0];
137
+ return head && !head.dispatched && head.deadline > this.#now() ? head : undefined;
138
+ }
139
+ #finish(pending, outcome) {
140
+ if (!this.#pending.delete(pending.id))
141
+ return false;
142
+ const lane = this.#lanes.get(pending.clientId);
143
+ if (lane) {
144
+ lane.queue.splice(lane.queue.indexOf(pending), 1);
145
+ if (lane.queue.length === 0)
146
+ this.#lanes.delete(pending.clientId);
147
+ }
148
+ pending.settle(outcome);
149
+ return true;
150
+ }
151
+ }
152
+ //# sourceMappingURL=broker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broker.js","sourceRoot":"","sources":["../../src/server/broker.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type AgentOrigin,\n type UiError,\n type UiOp,\n type UiOpInput,\n type UiOpResults,\n uiOpReadOnly,\n type UiReply,\n} from \"../protocol.js\";\nimport type { ClientTarget } from \"./directory.js\";\n\nexport class UiFailure extends Error {\n constructor(readonly error: UiError) {\n super(error.message);\n }\n}\n\nexport interface BrokerOptions {\n readonly timeoutMs: number;\n readonly maxPendingPerClient?: number | undefined;\n readonly maxPending?: number | undefined;\n readonly newId?: (() => string) | undefined;\n readonly now?: (() => number) | undefined;\n readonly setTimeout?: typeof globalThis.setTimeout | undefined;\n readonly clearTimeout?: typeof globalThis.clearTimeout | undefined;\n}\n\ninterface Pending {\n readonly id: string;\n readonly clientId: string;\n readonly frame: string;\n readonly readOnly: boolean;\n readonly deadline: number;\n dispatched: boolean;\n settle(outcome: UiReply | UiError): void;\n}\n\ninterface Lane {\n target: ClientTarget;\n readonly queue: Pending[];\n}\n\nexport interface UiReceipt {\n readonly accepted: boolean;\n readonly next?: string | undefined;\n}\n\nfunction unanswered(pending: Pending, reason: string): UiError {\n if (!pending.dispatched) {\n return { code: \"timeout\", message: `${reason} The app did not receive it.` };\n }\n if (pending.readOnly) {\n return {\n code: \"timeout\",\n message: `${reason} Nothing on screen changed; retrying is safe.`,\n };\n }\n return {\n code: \"outcome_unknown\",\n message: `${reason} The app may still have applied it; read the screen before retrying.`,\n };\n}\n\nconst DEFAULT_MAX_PENDING_PER_CLIENT = 8;\nconst DEFAULT_MAX_PENDING = 256;\n\nexport class UiBroker {\n readonly #timeoutMs: number;\n readonly #maxPendingPerClient: number;\n readonly #maxPending: number;\n readonly #newId: () => string;\n readonly #now: () => number;\n readonly #setTimeout: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;\n readonly #clearTimeout: (id: ReturnType<typeof setTimeout>) => void;\n readonly #pending = new Map<string, Pending>();\n readonly #lanes = new Map<string, Lane>();\n\n constructor(options: BrokerOptions) {\n this.#timeoutMs = options.timeoutMs;\n this.#maxPendingPerClient =\n options.maxPendingPerClient ?? DEFAULT_MAX_PENDING_PER_CLIENT;\n this.#maxPending = options.maxPending ?? DEFAULT_MAX_PENDING;\n this.#newId = options.newId ?? (() => crypto.randomUUID());\n this.#now = options.now ?? (() => Date.now());\n this.#setTimeout = options.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms));\n this.#clearTimeout = options.clearTimeout ?? ((id) => globalThis.clearTimeout(id));\n }\n\n request<O extends UiOp>(\n target: ClientTarget,\n op: O,\n input: UiOpInput<O>,\n origin: AgentOrigin,\n signal?: AbortSignal,\n ): Promise<UiOpResults[O]> {\n const clientId = target.client.id;\n if (\n this.#pending.size >= this.#maxPending ||\n (this.#lanes.get(clientId)?.queue.length ?? 0) >= this.#maxPendingPerClient\n ) {\n return Promise.reject(\n new UiFailure({\n code: \"busy\",\n message: \"Too many UI requests are waiting for this app; try again shortly.\",\n }),\n );\n }\n const id = this.#newId();\n const frame = JSON.stringify({ id, op, input, origin, timeoutMs: this.#timeoutMs });\n\n return new Promise<UiOpResults[O]>((resolve, reject) => {\n const giveUp = (reason: string) => this.#giveUp(pending, reason);\n const onAbort = () => giveUp(\"The UI request was cancelled.\");\n const timer = this.#setTimeout(\n () => giveUp(`The app did not answer within ${this.#timeoutMs} ms.`),\n this.#timeoutMs,\n );\n const pending: Pending = {\n id,\n clientId,\n frame,\n readOnly: uiOpReadOnly[op],\n deadline: this.#now() + this.#timeoutMs,\n dispatched: false,\n settle: (outcome) => {\n this.#clearTimeout(timer);\n signal?.removeEventListener(\"abort\", onAbort);\n if (!(\"type\" in outcome)) reject(new UiFailure(outcome));\n else if (outcome.ok) resolve(outcome.result as UiOpResults[O]);\n else reject(new UiFailure(outcome.error));\n },\n };\n this.#pending.set(id, pending);\n const lane = this.#lanes.get(clientId) ?? { target, queue: [] };\n lane.target = target;\n lane.queue.push(pending);\n this.#lanes.set(clientId, lane);\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n if (lane.queue[0] === pending) pending.dispatched = target.send(frame);\n if (signal?.aborted) onAbort();\n });\n }\n\n reply(clientId: string, reply: UiReply): UiReceipt {\n const pending = this.#pending.get(reply.id);\n if (!pending || pending.clientId !== clientId) return { accepted: false };\n this.#finish(pending, reply);\n return { accepted: true, next: this.#handOver(clientId) };\n }\n\n reconnected(target: ClientTarget): void {\n const lane = this.#lanes.get(target.client.id);\n if (!lane) return;\n lane.target = target;\n const head = lane.queue[0];\n if (head && target.send(head.frame)) head.dispatched = true;\n }\n\n abandon(clientId: string): void {\n for (const pending of [...(this.#lanes.get(clientId)?.queue ?? [])]) {\n this.#finish(\n pending,\n unanswered(pending, \"The app client closed before it answered.\"),\n );\n }\n }\n\n close(): void {\n for (const pending of [...this.#pending.values()]) {\n this.#finish(pending, {\n code: \"internal\",\n message: \"The server is shutting down.\",\n });\n }\n }\n\n #giveUp(pending: Pending, reason: string): void {\n if (!this.#finish(pending, unanswered(pending, reason))) return;\n const lane = this.#lanes.get(pending.clientId);\n const next = this.#waiting(pending.clientId);\n if (lane && next) next.dispatched = lane.target.send(next.frame);\n }\n\n #handOver(clientId: string): string | undefined {\n const next = this.#waiting(clientId);\n if (!next) return undefined;\n next.dispatched = true;\n return next.frame;\n }\n\n #waiting(clientId: string): Pending | undefined {\n const head = this.#lanes.get(clientId)?.queue[0];\n return head && !head.dispatched && head.deadline > this.#now() ? head : undefined;\n }\n\n #finish(pending: Pending, outcome: UiReply | UiError): boolean {\n if (!this.#pending.delete(pending.id)) return false;\n const lane = this.#lanes.get(pending.clientId);\n if (lane) {\n lane.queue.splice(lane.queue.indexOf(pending), 1);\n if (lane.queue.length === 0) this.#lanes.delete(pending.clientId);\n }\n pending.settle(outcome);\n return true;\n }\n}\n"]}
@@ -0,0 +1,40 @@
1
+ import type { AgentOrigin, ConfirmationAnswer } from "../protocol.js";
2
+ import type { ClientDirectory } from "./directory.js";
3
+ /** The 60 s the user gets, less 1 s so a client on MCP's default 60 s timeout hears our code. */
4
+ export declare const DEFAULT_CONFIRM_TIMEOUT_MS = 59000;
5
+ export declare const ORPHAN_GRACE_MS = 5000;
6
+ export declare const MAX_PENDING_PER_SCOPE = 8;
7
+ export interface ConfirmationRequest {
8
+ readonly scope: string;
9
+ readonly clientId?: string | undefined;
10
+ /**
11
+ * Set when an inline agent asks inside a chat turn: the audience is then every client
12
+ * watching the thread plus `clientId`, not the presence selection.
13
+ */
14
+ readonly thread?: ConfirmationThread | undefined;
15
+ readonly action: string;
16
+ readonly description: string;
17
+ readonly input: unknown;
18
+ readonly origin: AgentOrigin;
19
+ readonly signal?: AbortSignal | undefined;
20
+ }
21
+ export interface ConfirmationThread {
22
+ readonly threadId: string;
23
+ readonly turnId: string;
24
+ }
25
+ export type ConfirmationOutcome = "approved" | "declined" | "timeout" | "unavailable" | "cancelled" | "shutdown";
26
+ export interface ConfirmationBoardOptions {
27
+ readonly directory: ClientDirectory;
28
+ readonly timeoutMs: number;
29
+ readonly orphanGraceMs?: number | undefined;
30
+ readonly maxPendingPerScope?: number | undefined;
31
+ readonly newId?: (() => string) | undefined;
32
+ }
33
+ export declare class ConfirmationBoard {
34
+ #private;
35
+ constructor(options: ConfirmationBoardOptions);
36
+ ask(request: ConfirmationRequest): Promise<ConfirmationOutcome>;
37
+ answer(clientId: string, scope: string, answer: ConfirmationAnswer): boolean;
38
+ close(): void;
39
+ }
40
+ //# sourceMappingURL=confirmations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confirmations.d.ts","sourceRoot":"","sources":["../../src/server/confirmations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAGnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,eAAe,EAAqB,MAAM,gBAAgB,CAAC;AAEzE,iGAAiG;AACjG,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAEjD,eAAO,MAAM,eAAe,OAAQ,CAAC;AAErC,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;CAC3C;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,mBAAmB,GAC3B,UAAU,GACV,UAAU,GACV,SAAS,GACT,aAAa,GACb,WAAW,GACX,UAAU,CAAC;AAEf,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;CAC7C;AA8CD,qBAAa,iBAAiB;;IAY5B,YAAY,OAAO,EAAE,wBAAwB,EAQ5C;IAED,GAAG,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAmC9D;IAED,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAO3E;IAED,KAAK,IAAI,IAAI,CAKZ;CAsHF"}