@webex/internal-plugin-call-ai-summary 3.12.0-next.6 → 3.12.0-next.60

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.
@@ -0,0 +1,372 @@
1
+ ---
2
+ type: Module Spec
3
+ title: 'AI call summary plugin specification'
4
+ description: Responsibilities, boundaries, design, invariants, and verification for the AI call summary plugin.
5
+ tags: [module, specification]
6
+ ---
7
+ <!-- sdd-generated-metadata
8
+ doc_kind: module-spec
9
+ generated_from: module-spec@0.3.0
10
+ generated_by: claude-code
11
+ approved_by: "@riag"
12
+ updated_at: 2026-09-23T14:03:22Z
13
+ validation_status: pass
14
+ -->
15
+
16
+ # AI call summary plugin
17
+
18
+ This source-local document at `src/docs/README.md` owns the stable
19
+ specification for **the AI call summary plugin**. Ground every claim in repository evidence
20
+ and link to the
21
+ [repository architecture](../../docs/architecture.md)
22
+ instead of repeating broader facts.
23
+
24
+ Related context: [documentation index](../../docs/index.md) ·
25
+ [repository agent instructions](../../AGENTS.md)
26
+
27
+ ## Metadata
28
+
29
+ | Field | Value |
30
+ | ------------- | ------------------------------------------------------------ |
31
+ | Owner | Webex JS SDK Team |
32
+ | Source path | `src/` |
33
+ | Resource kind | package |
34
+ | Status | Active |
35
+ | Last verified | 2026-09-23 at `bc61c78ba5` |
36
+ | Module id | `src/` |
37
+ | Parent spec | — |
38
+ | Doc kind | Module spec |
39
+ | Coverage score | 100% assessed 2026-09-23 — six of six public methods and ten of ten exported types specced; independent validation found no drift |
40
+ | Validation status | pass, validator codex, assessed 2026-09-23 |
41
+
42
+ ## Applicability
43
+
44
+ | Condition ID | Status | Evidence or reason | Owned section |
45
+ | ------------------------------------ | ------ | ------------------ | ----------------------------- |
46
+ | `module.has_tiers` | N/A | The repository assigns no operational tiers to packages. `package.json` | Tier |
47
+ | `module.has_ui` | N/A | No UI surface; the module returns data only. `src/ai-summary.ts` | UI use-case flow |
48
+ | `module.crosses_service_boundaries` | Applicable | Calls the Pragya and AI Bridge services over HTTPS. `src/ai-summary.ts` | Cross-boundary use-case flow |
49
+ | `module.holds_client_state` | N/A | No state is retained between method calls. `src/ai-summary.ts` | Client state model |
50
+ | `module.enforces_domain_rules` | N/A | Validation is input checking, not domain-rule enforcement. `src/ai-summary.ts` | Business rules and invariants |
51
+ | `module.is_concurrent_async` | Applicable | Snippet decryption runs concurrently via Promise.all. `src/ai-summary.ts` | Concurrency and reactive flow |
52
+ | `module.owns_persistence` | N/A | The module persists nothing. `src/ai-summary.ts` | Data, schema, and migration |
53
+ | `module.stateful_transitions` | N/A | No state machine; each method is a single request-response. `src/ai-summary.ts` | State machine |
54
+ | `module.exposes_wire_protocol` | N/A | Consumes upstream HTTP; exposes no wire format of its own. `src/ai-summary.ts` | Protocol and wire format |
55
+ | `module.ui_multi_screen` | N/A | No UI surface. `src/ai-summary.ts` | UI flow |
56
+ | `module.large_data_model` | N/A | Ten interfaces, all flat DTOs. `src/types.ts` | Data model |
57
+ | `module.returns_caller_errors` | Applicable | The _handleError helper normalizes upstream failures into caller-facing Error messages. `src/ai-summary.ts` | Caller-visible failure modes |
58
+ | `module.module_specific_conventions` | N/A | Follows repository-wide plugin conventions only. `src/index.ts` | Module-specific rules |
59
+ | `module.published_package` | Applicable | Published to npm via the deploy:npm script. `package.json` | Export stability |
60
+ | `module.embedded_in_host` | Applicable | Registers into a host Webex SDK instance. `src/index.ts` | Host integration and theming |
61
+ | `module.has_design_tradeoff` | Applicable | Response flattening and single-request summary retrieval are deliberate trade-offs. `src/ai-summary.ts` | Key design trade-off |
62
+ | `module.has_submodules` | N/A | Computed has_submodules false; the module is a flat set of files with no child module owning its own spec. `src/ai-summary.ts` | Sub-modules |
63
+
64
+ ## Evidence register
65
+
66
+ | Evidence | What it establishes |
67
+ | ---------------- | ------------------- |
68
+ | `src/ai-summary.ts` | The six public methods, their validation, request, decryption, and error-normalization behavior |
69
+ | `src/types.ts` | The ten exported request and response interfaces and their optionality |
70
+ | `src/index.ts` | Plugin registration as `aisummary` and the side-effect import of the encryption plugin |
71
+ | `src/constants.ts` | Service name, container resource path, and the exact caller-visible error message strings |
72
+ | `src/config.ts` | The plugin config namespace, currently empty |
73
+ | `package.json` | Published package identity, engines, dependencies, and the documented command set |
74
+ | `src/manual-pragya-api-test.js` | A manual script validating the Pragya container response structure |
75
+ | `src/manual-integration-test.js` | A manual script exercising the end-to-end flow against live services |
76
+
77
+ Prior source basis: repository design and agent-guidance documentation, reconciled under the
78
+ `reconcile` policy; three stale testing claims were corrected against the current tree and one
79
+ omitted method was restored from code.
80
+
81
+ ## Purpose and boundary
82
+
83
+ - Responsibility: resolve a Pragya container for a call and return decrypted AI-generated summary, notes, action items, and transcript content.
84
+ - In scope: container resolution, content retrieval from Pragya-supplied URLs, KMS decryption of every content field, and normalization of upstream errors.
85
+ - Out of scope: starting or stopping the AI assistant during a call, generating or regenerating summaries, real-time in-call AI responses, recording storage or deletion, and feedback UI components.
86
+ - Consumers: Webex SDK applications reaching the module through `webex.internal.aisummary`.
87
+
88
+ This plugin provides methods to:
89
+
90
+ 1. Resolve a **Pragya container** by ID (returns metadata, summary URLs, and encryption key)
91
+ 2. Fetch and decrypt **AI-generated summaries** (note, short note, action items) in a single call
92
+ 3. Fetch and decrypt **AI-generated notes** via a dedicated notes endpoint
93
+ 4. Fetch and decrypt **AI-generated action items** via a dedicated action items endpoint
94
+ 5. Retrieve the **transcript URL** for a call
95
+ 6. Fetch and decrypt the full call **transcript**
96
+
97
+ This is an internal Cisco Webex plugin. As such, it does not strictly adhere to semantic versioning. Use at your own risk.
98
+
99
+ ## Structure and key files
100
+
101
+ | File | Description |
102
+ | ---- | ----------- |
103
+ | `src/index.ts` | Entry point. Registers the plugin via `registerInternalPlugin('aisummary', ...)`. |
104
+ | `src/ai-summary.ts` | Main plugin class extending `WebexPlugin`. Contains all public and private methods. |
105
+ | `src/types.ts` | TypeScript interfaces for request/response DTOs. |
106
+ | `src/constants.ts` | Service name, resource path, and error message constants. |
107
+ | `src/config.ts` | Plugin configuration (currently empty). |
108
+ | `src/manual-pragya-api-test.js` | Manual script validating the Pragya container response structure. |
109
+ | `src/manual-integration-test.js` | Manual script exercising device registration through transcript fetch. |
110
+
111
+ Unit tests live at `test/unit/spec/ai-summary.ts` with upstream wire fixtures at `test/unit/fixture/responses.ts`. The two
112
+ manual scripts remain the only verification against live services.
113
+
114
+ ## Public surface
115
+
116
+ All methods are accessible via `webex.internal.aisummary`. Exact declarations are authoritative in
117
+ the linked sources and are not restated here.
118
+
119
+ | Surface | Consumer | Compatibility commitment | Source |
120
+ | -------- | ---------- | ------------------------ | -------- |
121
+ | `getContainer` | Webex SDK consumers | Internal; no semantic-versioning guarantee | `src/ai-summary.ts` |
122
+ | `getSummary` | Webex SDK consumers | Internal; recommended entry point for summary content | `src/ai-summary.ts` |
123
+ | `getNotes` | Webex SDK consumers | Internal; depends on optional `notesUrl` | `src/ai-summary.ts` |
124
+ | `getActionItems` | Webex SDK consumers | Internal; depends on optional `actionItemsUrl` | `src/ai-summary.ts` |
125
+ | `getTranscriptUrl` | Webex SDK consumers | Internal; synchronous, returns a string | `src/ai-summary.ts` |
126
+ | `getTranscript` | Webex SDK consumers | Internal; returns decrypted transcript snippets | `src/ai-summary.ts` |
127
+ | Request and response types | TypeScript consumers | Internal; shipped as declarations | `src/types.ts` |
128
+
129
+ Request shapes, all issued through `this.webex.request` with the SDK auth interceptor supplying
130
+ `Authorization: Bearer {user_access_token}` and `Accept: application/json`:
131
+
132
+ - `getContainer` — `GET /pragya/api/v1/containers/{containerId} HTTP/1.1`, resolved through the service catalog as `service: 'pragya'`.
133
+ - `getSummary` — `GET {summaryData.summaryUrl}?fields=note,shortnote,actionitems HTTP/1.1`, as an absolute URI.
134
+ - `getNotes` — `GET {summaryData.notesUrl} HTTP/1.1`, as an absolute URI.
135
+ - `getActionItems` — `GET {summaryData.actionItemsUrl} HTTP/1.1`, as an absolute URI.
136
+ - `getTranscript` — `GET {summaryData.transcriptUrl} HTTP/1.1`, as an absolute URI.
137
+
138
+ Behavior of each method, in current-code terms:
139
+
140
+ - `getContainer` resolves a Pragya container by ID. Returns container metadata including summary URLs and the KMS encryption key URL. The raw response nests URLs under `summaryData.data`. The plugin's `getContainer()` flattens this automatically. `summaryData` contains summary URLs (`summaryUrl`, `transcriptUrl`, `status`, `summarizeAfterCall`); `encryptionKeyUrl` is the KMS key URL for decrypting content; and the response also carries `kmsResourceObjectUrl`, `aclUrl`, `forkSessionId`, `callSessionId`, `ownerUserId`, `orgId`, `start`, `end`. **Returns:** `Promise<PragyaContainerResponse>`
141
+ - `getSummary` fetches all AI-generated summary content (note, short note, and action items) from a single request to the summary URL, and decrypts each field. It issues `GET {summaryUrl}?fields=note,shortnote,actionitems`. It returns `id` (summary identifier), `note` (decrypted full note, HTML string), `shortNote` (decrypted short note, HTML string), `actionItems` (array of `ActionItemSnippet` objects), and `feedbackUrl` extracted from the `links` array (`rel: "feedback"`), if available. This is the **recommended** method for retrieving summary content.
142
+ - `getNotes` fetches AI-generated notes from the dedicated notes endpoint and decrypts via KMS. It issues `GET {notesUrl}` and requires `notesUrl` to be present in the container's `summaryData`.
143
+ - `getActionItems` fetches AI-generated action items from the dedicated action items endpoint and decrypts each snippet via KMS. It issues `GET {actionItemsUrl}` and requires `actionItemsUrl` to be present. When the response array is empty it returns `{id: undefined, snippets: []}`.
144
+ - `getTranscriptUrl` returns the transcript URL from the container info. Does not fetch or decrypt content. It is synchronous and returns a plain `string`.
145
+ - `getTranscript` fetches and decrypts the full call transcript, returning `id`, `totalCount`, and decrypted `snippets` each carrying `startTime`, `endTime`, `content`, `audioCSI`, and `speaker`.
146
+
147
+ Private helpers `_validateContainerId`, `_validateContainerInfo`, `_decryptContent`, and
148
+ `_handleError` are not part of the public surface.
149
+
150
+ ## Dependencies
151
+
152
+ | Dependency | Why it is required | Failure behavior |
153
+ | --------------------- | ------------------ | ----------------------------------- |
154
+ | `@webex/webex-core` | Base plugin class, request handling, auth interceptor | Request rejections are normalized by `_handleError` |
155
+ | `@webex/internal-plugin-encryption` | KMS decryption | Decryption rejection propagates to the caller through `_handleError` |
156
+ | Pragya service | Container metadata; provides content URLs and encryption key | 401/403/404 mapped to normalized error messages |
157
+ | AI Bridge content endpoints | Serve encrypted AI-generated content | 404 becomes `Summary content not available or expired` |
158
+ | KMS | Encryption key management | Key fetch failure rejects the calling method |
159
+
160
+ The Pragya and AI Bridge APIs require a valid Webex access token. The SDK's auth interceptor automatically attaches the token for URLs in the service catalog and for the absolute content URLs returned by Pragya.
161
+
162
+ ## Requirements
163
+
164
+ | ID | WHAT | WHY | Source evidence | Test or example evidence | Assumptions or gaps | Confidence |
165
+ | --------- | ---------------------------------------------- | ------------------------------ | --------------- | ----------------------------------- | ------------------- | --------------------------------- |
166
+ | `MOD-001` | `getContainer` flattens `summaryData.data` onto `summaryData` before returning | Consumers access `summaryData.summaryUrl` directly without knowing the upstream nesting | `src/ai-summary.ts` | `test/unit/spec/ai-summary.ts` | none | Present |
167
+ | `MOD-002` | Every content field is decrypted before it is returned to the caller | Callers must never receive JWE ciphertext | `src/ai-summary.ts` | `test/unit/spec/ai-summary.ts` | none | Present |
168
+ | `MOD-003` | A per-response `keyUrl` takes precedence over `containerInfo.encryptionKeyUrl` | Content responses may be sealed under a different key than the container | `src/ai-summary.ts` | `test/unit/spec/ai-summary.ts` | none | Present |
169
+ | `MOD-004` | Invalid input throws synchronously before any request is issued | Callers get immediate, actionable validation errors | `src/ai-summary.ts` | `test/unit/spec/ai-summary.ts` | none | Present |
170
+ | `MOD-005` | Upstream 401, 403, and 404 responses are normalized to fixed caller-facing messages | Callers branch on stable messages rather than transport details | `src/constants.ts` | `test/unit/spec/ai-summary.ts` | none | Present |
171
+ | `MOD-006` | `getTranscriptUrl` is synchronous and performs no network or decryption work | Callers can obtain the URL for downstream processing without cost | `src/ai-summary.ts` | `test/unit/spec/ai-summary.ts` | none | Present |
172
+ | `MOD-007` | The plugin self-registers as `aisummary` on import with zero changes to other packages | Consumers import the package directly; no bundle edit is required | `src/index.ts` | `test/unit/spec/ai-summary.ts` | none | Present |
173
+
174
+ ## Design overview
175
+
176
+ The module is a single `WebexPlugin.extend` object with six public methods, four private helpers, and
177
+ no retained state. Each public method follows the same three-step shape: validate the caller's input,
178
+ issue exactly one HTTP request through `this.webex.request`, then decrypt every content field before
179
+ returning a typed DTO. Configuration is an empty `aisummary` namespace, so behavior is fully
180
+ determined by the container passed in by the caller.
181
+
182
+ Two upstream services are involved and the split matters. Pragya owns container metadata and is the
183
+ source of truth for both the content URLs and the encryption key. Because Pragya returns
184
+ fully-qualified, region-correct URLs, the module performs no separate service discovery for the
185
+ content endpoints — it fetches whatever absolute URL the container supplies. Only the container
186
+ lookup itself resolves through the service catalog, as `service: 'pragya'`.
187
+
188
+ The module deliberately owns all of its types, constants, and logic rather than extending shared SDK
189
+ types, which keeps it self-contained; the rationale and its costs are recorded in
190
+ [ADR-0001](../../docs/adr/0001-flatten-container-response-and-prefer-single-request-summary.md).
191
+
192
+ The complete implementation — the per-method validate/request/decrypt bodies, the
193
+ `summaryData.data` flattening, the `Promise.all` snippet decryption and the `keyUrl` fallback — is
194
+ authoritative in `src/ai-summary.ts` and is linked rather than restated here, so this specification
195
+ does not become a second contract surface that drifts from the code.
196
+
197
+ ## Data flow and sequence coverage
198
+
199
+ The transport is HTTPS via `this.webex.request`, once per public method.
200
+
201
+ | Operation group | Entry and outcome | Diagram or evidence | Failure and recovery coverage |
202
+ | --------------- | ----------------- | ------------------- | ----------------------------- |
203
+ | Container resolution | `getContainer({containerId})` → flattened `PragyaContainerResponse` | `src/ai-summary.ts` | Empty id throws; 401/403/404 normalized |
204
+ | Single-request summary | `getSummary({containerInfo})` → decrypted note, short note, action items | `src/ai-summary.ts` | Missing `summaryUrl` or key throws; 404 normalized |
205
+ | Standalone notes | `getNotes({containerInfo})` → decrypted note content | `src/ai-summary.ts` | Missing `notesUrl` throws before any request |
206
+ | Standalone action items | `getActionItems({containerInfo})` → decrypted snippets | `src/ai-summary.ts` | Empty array returns `{id: undefined, snippets: []}` |
207
+ | Transcript URL | `getTranscriptUrl({containerInfo})` → URL string | `src/ai-summary.ts` | Missing `transcriptUrl` throws; no network call |
208
+ | Transcript content | `getTranscript({containerInfo})` → decrypted snippets | `src/ai-summary.ts` | Missing `transcriptUrl` throws; 404 normalized |
209
+
210
+ ```mermaid
211
+ flowchart LR
212
+ Caller[Caller] --> Validate[Validate options]
213
+ Validate -->|invalid| Throw[Throw validation Error]
214
+ Validate -->|valid| Request[webex.request]
215
+ Request -->|error| Normalize[_handleError]
216
+ Request -->|body| Decrypt[_decryptContent per field]
217
+ Decrypt --> Result[Typed DTO]
218
+ ```
219
+
220
+ Each operation group keeps its own sequence rather than being collapsed into the shape above.
221
+
222
+ **Get container info flow.** Client calls `webex.internal.aisummary.getContainer({ containerId })`;
223
+ Validate containerId (non-empty string); `webex.request` with `method: 'GET'`, `service: 'pragya'`,
224
+ `resource: containers/${containerId}`; Flatten: if body.summaryData.data exists, set
225
+ body.summaryData = body.summaryData.data; Return PragyaContainerResponse (with flat summaryData).
226
+
227
+ **Get notes flow.** Client calls `webex.internal.aisummary.getNotes(containerInfo)`; Validate
228
+ containerInfo has summaryData.notesUrl and encryptionKeyUrl; `webex.request` with `method: 'GET'`,
229
+ `uri: containerInfo.summaryData.notesUrl`; Response: `{ id, aiGeneratedContent: "<encrypted>",
230
+ feedbackUrl?, keyUrl }`; Decrypt aiGeneratedContent using containerInfo.encryptionKeyUrl; Return
231
+ decrypted SummaryNotes.
232
+
233
+ **Get action items flow.** Client calls `webex.internal.aisummary.getActionItems(containerInfo)`;
234
+ Validate containerInfo has summaryData.actionItemsUrl and encryptionKeyUrl; `webex.request` with
235
+ `method: 'GET'`, `uri: containerInfo.summaryData.actionItemsUrl`; Response: `[{ id, keyUrl,
236
+ snippets: [{ id, content, aiGeneratedContent }] }]`; Decrypt all aiGeneratedContent fields using
237
+ containerInfo.encryptionKeyUrl; Return decrypted SummaryActionItems.
238
+
239
+ ## Class and component relationships
240
+
241
+ ```mermaid
242
+ classDiagram
243
+ class AISummary {
244
+ +getContainer()
245
+ +getSummary()
246
+ +getNotes()
247
+ +getActionItems()
248
+ +getTranscriptUrl()
249
+ +getTranscript()
250
+ -_validateContainerId()
251
+ -_validateContainerInfo()
252
+ -_decryptContent()
253
+ -_handleError()
254
+ }
255
+ WebexPlugin <|-- AISummary
256
+ AISummary --> EncryptionPlugin : decryptText
257
+ AISummary --> WebexRequest : request
258
+ ```
259
+
260
+ The module is registered onto the host SDK by `registerInternalPlugin('aisummary', AISummary, {config})` in `src/index.ts`, which also imports `@webex/internal-plugin-encryption` for its side effects and re-exports the plugin as the package default. Importing the package is what performs that registration; the exact entry-point declarations are authoritative in `src/index.ts`.
261
+
262
+ ## Use cases and flows
263
+
264
+ | Use case | Actor or caller | Primary steps and outcome | Failure or boundary behavior | Evidence |
265
+ | -------- | --------------- | ----------------------------- | ---------------------------- | ------------------------- |
266
+ | `UC-001` | SDK consumer | Read `extensionPayload.callingContainerIds` from call history, call `getContainer`, then `getSummary`; receive decrypted note, short note, and action items | Any upstream 401/403/404 surfaces as a normalized `Error` | `src/ai-summary.ts` |
267
+ | `UC-002` | SDK consumer | Call `getNotes` for note content only | Throws when `notesUrl` is absent from the container | `src/ai-summary.ts` |
268
+ | `UC-003` | SDK consumer | Call `getActionItems` for action items only | Returns an empty snippet list when the response array is empty | `src/ai-summary.ts` |
269
+ | `UC-004` | SDK consumer | Call `getTranscriptUrl` to hand the URL to downstream processing | Synchronous throw when `transcriptUrl` is absent | `src/ai-summary.ts` |
270
+ | `UC-005` | SDK consumer | Call `getTranscript` to obtain timed, decrypted transcript snippets | Throws when `transcriptUrl` or the key is absent | `src/ai-summary.ts` |
271
+
272
+ ### Cross-boundary use-case flow
273
+
274
+ Every use case crosses a network boundary twice: once to Pragya through the service catalog, and once
275
+ to an AI Bridge URL supplied by that container. Ordering is strict — a container must be resolved
276
+ before any content call, because the content URLs and the encryption key both come from it. There is
277
+ no retry, timeout, or circuit-breaking logic in this module; those are inherited from the SDK HTTP
278
+ layer. Compatibility is loose by design: `notesUrl` and `actionItemsUrl` are optional, so the module
279
+ must tolerate their absence rather than assume a fixed upstream version.
280
+
281
+ Decryption depends on a prerequisite chain that the SDK satisfies automatically: a registered device (`webex.internal.device.register()`), a Mercury WebSocket connection (initiated automatically during KMS key fetch), an ECDHE key exchange with KMS, and key retrieval from KMS using the `encryptionKeyUrl`. The SDK handles these steps automatically when `decryptText` is called.
282
+
283
+ ## Concurrency and reactive flow
284
+
285
+ - Execution model: promise-based async methods on a stateless plugin object; no workers, timers, or subscriptions.
286
+ - Ordering guarantees: container resolution must precede content retrieval; within a method, decryption follows the single request.
287
+ - Idempotency and retry: every method is a read and is therefore naturally idempotent; the module itself performs no retries.
288
+ - Shared-state protection: none required — the module holds no mutable state between calls.
289
+ - Blocking restrictions: snippet collections are decrypted concurrently with `Promise.all` rather than sequentially, so a large transcript does not serialize KMS round-trips.
290
+
291
+ ## Caller-visible failure modes
292
+
293
+ The plugin normalizes HTTP errors into descriptive messages. Validation errors are thrown synchronously, before any request is issued.
294
+
295
+ | Error Type | HTTP Status | SDK Error Message | Recovery Action |
296
+ | ---------- | ----------- | ----------------- | --------------- |
297
+ | Invalid Container ID | N/A (client) | "containerId is required and must be a non-empty string" | Validate input |
298
+ | Invalid Container Info | N/A (client) | "containerInfo with valid summaryData and encryptionKeyUrl is required" | Ensure getContainer was called first |
299
+ | Authentication Failed | 401 | "Authentication failed: Invalid or expired token" | Re-authenticate user |
300
+ | Access Denied | 403 | "Access denied: User not authorized to view this summary" | Check user permissions |
301
+ | Container Not Found | 404 | "Container not found" | Verify containerId from Janus |
302
+ | Content Not Found | 404 (non-getContainer) | "Summary content not available or expired" | Content may have been deleted or expired |
303
+ | Summary Not Ready | N/A | summaryData.status !== "Active" | Retry after delay |
304
+ | Unmapped failure | Any other | "{methodName} failed: {error.message}" | Depends on the underlying cause |
305
+
306
+ The exact message strings are declared in `src/constants.ts` and the status mapping in
307
+ `_handleError` in `src/ai-summary.ts`; callers branch on these strings, so changing one is a
308
+ breaking change.
309
+
310
+ ## Pitfalls and constraints
311
+
312
+ - The raw Pragya response nests URLs under `summaryData.data`; `getContainer` flattens it automatically, so code that re-reads `summaryData.data` after calling `getContainer` will find nothing there.
313
+ - `notesUrl` and `actionItemsUrl` may not be present in all API versions. Prefer `getSummary()`, which returns notes, short notes, and action items in one request.
314
+ - A per-response `keyUrl` overrides `containerInfo.encryptionKeyUrl`; code that decrypts with the container key alone can fail on content sealed under a different key.
315
+ - `getTranscriptUrl` is synchronous and returns a `string`, unlike every other public method — awaiting it yields the string, but treating it as a promise-returning API is a mistake.
316
+ - Summary availability is not guaranteed: `summaryData.status` must be `Active`, org-level AI features must be enabled, and the AI assistant must have been enabled during the call.
317
+ - Decryption requires a registered device and a working Mercury connection; calling the module on an unregistered SDK instance fails inside KMS key retrieval rather than at validation.
318
+
319
+ ## Export stability
320
+
321
+ | Export or entry point | Consumer | Stability | Versioning and deprecation rule | Declaration or API report |
322
+ | --------------------- | ---------- | --------- | ------------------------------- | ------------------------- |
323
+ | default export `AISummary` | Webex SDK consumers | Internal | Does not strictly adhere to semantic versioning | `src/index.ts` |
324
+ | `webex.internal.aisummary` namespace | Webex SDK consumers | Internal | Registered on import; renaming is a breaking change | `src/index.ts` |
325
+ | Request and response interfaces | TypeScript consumers | Internal | Shipped as declarations built to `dist/` | `src/types.ts` |
326
+
327
+ ## Host integration and theming
328
+
329
+ - Mount or entry contract: importing `@webex/internal-plugin-call-ai-summary` calls `registerInternalPlugin('aisummary', ...)`, making the module reachable at `webex.internal.aisummary`.
330
+ - Required providers, peers, or host versions: an authenticated Webex SDK instance with a registered device, plus `@webex/internal-plugin-encryption`, which the entry point imports for its side effects.
331
+ - Theme and design-token contract: N/A — the module renders nothing.
332
+ - Accessibility and lifecycle obligations: N/A — no UI surface; note that `note` and `shortNote` are HTML strings, so consumers that render them own their own sanitization and accessibility.
333
+
334
+ ## Key design trade-off
335
+
336
+ | Chosen trade-off | Preserved invariant or benefit | Cost or limitation | Decision evidence |
337
+ | ---------------- | ------------------------------ | ------------------ | ----------------- |
338
+ | Flatten `summaryData.data` onto `summaryData` inside `getContainer` | Consumers access `summaryData.summaryUrl` directly and are insulated from the upstream nesting | The returned object no longer matches the raw Pragya wire body, so wire-level debugging must account for the transform | `docs/adr/0001-flatten-container-response-and-prefer-single-request-summary.md` |
339
+ | Prefer one `getSummary` request over three standalone calls | One round trip returns note, short note, and action items, and tolerates optional URLs being absent | Callers wanting only one content type still pay for the combined response | `docs/adr/0001-flatten-container-response-and-prefer-single-request-summary.md` |
340
+ | Self-contained plugin owning its own types and constants | Zero changes to existing packages; the plugin can ship independently | Type definitions overlap conceptually with neighbouring plugins and must be maintained in parallel | `src/types.ts` |
341
+ | No separate service discovery for content URLs | Region correctness comes from Pragya for free | The module trusts absolute URLs supplied by an upstream response | `src/ai-summary.ts` |
342
+
343
+ ## Verification
344
+
345
+ | Requirement or invariant | Test level | Positive evidence | Negative or boundary evidence | Gap |
346
+ | ------------------------ | ----------------------------- | ----------------- | ----------------------------- | ---------------------- |
347
+ | `MOD-001` | Unit | `test/unit/spec/ai-summary.ts` | `test/unit/spec/ai-summary.ts` (already-flat body left untouched) | none |
348
+ | `MOD-002` | Unit | `test/unit/spec/ai-summary.ts` | `test/unit/spec/ai-summary.ts` (decryption failure propagates) | none |
349
+ | `MOD-003` | Unit | `test/unit/spec/ai-summary.ts` | `test/unit/spec/ai-summary.ts` (falls back when the response keyUrl is absent) | none |
350
+ | `MOD-004` | Unit | `test/unit/spec/ai-summary.ts` | `test/unit/spec/ai-summary.ts` (empty, whitespace, undefined, non-string) | none |
351
+ | `MOD-005` | Unit | `test/unit/spec/ai-summary.ts` | `test/unit/spec/ai-summary.ts` (unmapped failure is method-prefixed) | none |
352
+ | `MOD-006` | Unit | `test/unit/spec/ai-summary.ts` | `test/unit/spec/ai-summary.ts` (throws when the transcriptUrl field is absent) | none |
353
+ | `MOD-007` | Unit | `test/unit/spec/ai-summary.ts` | `test/unit/spec/ai-summary.ts` (all six methods present on a real WebexCore instance) | none |
354
+
355
+ The module has an automated unit suite at `test/unit/spec/ai-summary.ts`: **38 tests**, run with `yarn test:unit`
356
+ (`webex-legacy-tools test --unit --runner jest`). Upstream wire fixtures live in
357
+ `test/unit/fixture/responses.ts`, which is the repository's only record of the externally owned
358
+ Pragya and AI Bridge response shapes.
359
+
360
+ All six public methods are covered, along with the `summaryData.data` flattening transform, the
361
+ `keyUrl` precedence rule, every validation branch, and each normalized error mapping. Two manual
362
+ scripts under `src/` remain the only end-to-end verification against live services, and both require
363
+ a token and container ID.
364
+
365
+ Registration (`MOD-007`) is covered too: because `registerInternalPlugin` runs as an import side
366
+ effect, those three cases assert against the internal-core plugin registry. They deliberately do
367
+ **not** construct a live `WebexCore` — doing so boots the service catalog, which fires asynchronous
368
+ U2C requests that outlive the test and make the runner exit nonzero even though every assertion
369
+ passes. Every documented requirement now has unit evidence.
370
+
371
+ The suite requires only `engines.node >=16` and is verified on Node 24.10 as well as the monorepo's
372
+ pinned 22.14, so it can be run without `nvm`.
package/src/index.ts CHANGED
@@ -10,4 +10,4 @@ import config from './config';
10
10
 
11
11
  registerInternalPlugin('aisummary', AISummary, {config});
12
12
 
13
- export {default} from './ai-summary';
13
+ export default AISummary;
@@ -12,7 +12,9 @@
12
12
  * WEBEX_TOKEN='<token>' node src/manual-integration-test.js
13
13
  */
14
14
 
15
- /* eslint-disable no-console, require-jsdoc */
15
+ // Standalone Node dev script: it loads the built package by name and uses CommonJS
16
+ // requires, so the module-graph rules below do not apply.
17
+ /* eslint-disable no-console, require-jsdoc, import/no-extraneous-dependencies, @typescript-eslint/no-var-requires */
16
18
 
17
19
  require('@webex/internal-plugin-call-ai-summary');
18
20
 
@@ -9,7 +9,9 @@
9
9
  * Or paste your token directly into WEBEX_TOKEN below.
10
10
  */
11
11
 
12
- /* eslint-disable no-console, require-jsdoc */
12
+ // Standalone Node dev script: it loads the built package by name and uses CommonJS
13
+ // requires, so the module-graph rules below do not apply.
14
+ /* eslint-disable no-console, require-jsdoc, import/no-extraneous-dependencies, @typescript-eslint/no-var-requires */
13
15
 
14
16
  require('@webex/internal-plugin-call-ai-summary');
15
17
 
package/src/types.ts CHANGED
@@ -69,6 +69,18 @@ export interface GetSummaryContentOptions {
69
69
 
70
70
  // --- Summary Response DTOs ---
71
71
 
72
+ /**
73
+ * Single action item snippet.
74
+ */
75
+ export interface ActionItemSnippet {
76
+ /** Unique identifier */
77
+ id: string;
78
+ /** User-edited version (if available) */
79
+ editedContent?: string;
80
+ /** Decrypted AI-generated content */
81
+ aiGeneratedContent: string;
82
+ }
83
+
72
84
  /**
73
85
  * Decrypted AI-generated summary content.
74
86
  * Contains all three content types returned by the summary API.
@@ -98,18 +110,6 @@ export interface SummaryNotes {
98
110
  feedbackUrl?: string;
99
111
  }
100
112
 
101
- /**
102
- * Single action item snippet.
103
- */
104
- export interface ActionItemSnippet {
105
- /** Unique identifier */
106
- id: string;
107
- /** User-edited version (if available) */
108
- editedContent?: string;
109
- /** Decrypted AI-generated content */
110
- aiGeneratedContent: string;
111
- }
112
-
113
113
  /**
114
114
  * Decrypted AI-generated action items.
115
115
  */
@@ -0,0 +1,123 @@
1
+ /*!
2
+ * Copyright (c) 2015-2025 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ /**
6
+ * Mock Pragya and AI Bridge responses.
7
+ *
8
+ * These shapes are the upstream wire bodies as observed against the live Pragya
9
+ * and AI Bridge services. Both surfaces are externally owned, so this file is
10
+ * the repository's only record of them: `src/types.ts` declares only the subset
11
+ * the plugin consumes. Keep these fixtures faithful to the wire, not to the DTOs.
12
+ */
13
+
14
+ /** Raw Pragya container response, with summary URLs still nested under `summaryData.data`. */
15
+ export const rawPragyaContainer = {
16
+ id: '34125120-13b5-11f1-9b36-adb685725098',
17
+ objectType: 'callingAIContainer',
18
+ summaryData: {
19
+ extensionId: 'extension-id',
20
+ objectType: 'extension',
21
+ extensionType: 'callingAISummary',
22
+ data: {
23
+ id: 'summary-id',
24
+ objectType: 'callingAISummary',
25
+ status: 'Active',
26
+ summaryUrl: 'https://aibridge-url/summaries/c635e870',
27
+ transcriptUrl: 'https://aibridge-url/summaries/c635e870/transcripts',
28
+ summarizeAfterCall: true,
29
+ aclUrl: 'https://acl-a.wbx2.com/acl/api/v1/acls/inner',
30
+ kmsResourceObjectUrl: 'kms://kms-cisco.wbx2.com/resources/inner',
31
+ },
32
+ },
33
+ encryptionKeyUrl: 'kms://kms-cisco.wbx2.com/keys/897e4d2d',
34
+ kmsResourceObjectUrl: 'kms://kms-cisco.wbx2.com/resources/f7316435',
35
+ aclUrl: 'https://acl-a.wbx2.com/acl/api/v1/acls/78c4cd90',
36
+ forkSessionId: '123e4567-fork',
37
+ callSessionId: '123e4567-call',
38
+ ownerUserId: '123e4567-owner',
39
+ orgId: '123e4567-org',
40
+ start: '2023-10-01T12:00:00Z',
41
+ end: '2023-10-01T12:00:00Z',
42
+ };
43
+
44
+ /** A container as callers see it after `getContainer` flattens `summaryData.data`. */
45
+ export const flattenedContainer = {
46
+ summaryData: {
47
+ status: 'Active',
48
+ summaryUrl: 'https://aibridge-url/summaries/c635e870',
49
+ notesUrl: 'https://aibridge-url/summaries/c635e870/notes',
50
+ actionItemsUrl: 'https://aibridge-url/summaries/c635e870/action-items',
51
+ transcriptUrl: 'https://aibridge-url/summaries/c635e870/transcripts',
52
+ summarizeAfterCall: true,
53
+ },
54
+ encryptionKeyUrl: 'kms://kms-cisco.wbx2.com/keys/897e4d2d',
55
+ };
56
+
57
+ /** AI Bridge response for `summaryUrl?fields=note,shortnote,actionitems`. */
58
+ export const summaryResponse = {
59
+ id: '10293-dk93-ddie-odir-did932j3kdde',
60
+ keyUrl: 'kms://kms-us-int.wbx2.com/keys/f19d4d28',
61
+ note: {aiGeneratedContent: '<encrypted_note_content>'},
62
+ shortnote: {aiGeneratedContent: '<encrypted_short_note_content>'},
63
+ actionitems: {
64
+ snippets: [
65
+ {
66
+ id: '394r0087',
67
+ content: 'edited version',
68
+ aiGeneratedContent: '<encrypted_ai_generated_content>',
69
+ },
70
+ ],
71
+ },
72
+ links: [
73
+ {
74
+ rel: 'feedback',
75
+ href: 'https://summarizer-r.wbx2.com/summarizer/api/v1/feedback/1',
76
+ },
77
+ ],
78
+ };
79
+
80
+ /** AI Bridge response for the standalone `notesUrl` endpoint. */
81
+ export const notesResponse = {
82
+ id: '10293-dk93-ddie-odir-did932j3kdde',
83
+ aiGeneratedContent: '<encrypted_content>',
84
+ feedbackUrl: 'https://summarizer-r.wbx2.com/summarizer/api/v1/feedback/report/1',
85
+ keyUrl: 'kms://kms-us-int.wbx2.com/keys/f19d4d28',
86
+ };
87
+
88
+ /** AI Bridge response for the standalone `actionItemsUrl` endpoint. Note the array wrapper. */
89
+ export const actionItemsResponse = [
90
+ {
91
+ id: '1234-dk93-ddie-odir-dk93dj33',
92
+ keyUrl: 'kms://kms-us-int.wbx2.com/keys/f19d4d28',
93
+ snippets: [
94
+ {
95
+ id: '394r0087',
96
+ content: 'edited version',
97
+ aiGeneratedContent: '<encrypted_ai_generated_content>',
98
+ },
99
+ ],
100
+ },
101
+ ];
102
+
103
+ /** AI Bridge response for `transcriptUrl`. */
104
+ export const transcriptResponse = {
105
+ id: 'transcript-id',
106
+ totalCount: 2,
107
+ transcriptSnippetList: [
108
+ {
109
+ startTime: '1000',
110
+ endTime: '2000',
111
+ content: '<encrypted_snippet_1>',
112
+ audioCSI: 'csi-1',
113
+ speaker: {speakerName: 'Ada Lovelace', speakerId: 'speaker-1'},
114
+ },
115
+ {
116
+ startTime: '2000',
117
+ endTime: '3000',
118
+ content: '<encrypted_snippet_2>',
119
+ audioCSI: 'csi-2',
120
+ speaker: {speakerName: 'Alan Turing', speakerId: 'speaker-2'},
121
+ },
122
+ ],
123
+ };