@salesforce/sfdx-agent-harness-openai 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/LICENSE.txt +21 -0
  3. package/README.md +55 -0
  4. package/dist/gen-sink.d.ts +8 -0
  5. package/dist/gen-sink.js +13 -0
  6. package/dist/gen-sink.js.map +1 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +16 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/mcp-error-classifier.d.ts +36 -0
  11. package/dist/mcp-error-classifier.js +166 -0
  12. package/dist/mcp-error-classifier.js.map +1 -0
  13. package/dist/openai-agents-harness-factory.d.ts +36 -0
  14. package/dist/openai-agents-harness-factory.js +39 -0
  15. package/dist/openai-agents-harness-factory.js.map +1 -0
  16. package/dist/openai-agents-harness.d.ts +302 -0
  17. package/dist/openai-agents-harness.js +1014 -0
  18. package/dist/openai-agents-harness.js.map +1 -0
  19. package/dist/openai-approval-coordinator.d.ts +231 -0
  20. package/dist/openai-approval-coordinator.js +422 -0
  21. package/dist/openai-approval-coordinator.js.map +1 -0
  22. package/dist/openai-built-in-policies.d.ts +29 -0
  23. package/dist/openai-built-in-policies.js +33 -0
  24. package/dist/openai-built-in-policies.js.map +1 -0
  25. package/dist/openai-event-adapter.d.ts +119 -0
  26. package/dist/openai-event-adapter.js +322 -0
  27. package/dist/openai-event-adapter.js.map +1 -0
  28. package/dist/openai-mcp-config-mapper.d.ts +58 -0
  29. package/dist/openai-mcp-config-mapper.js +133 -0
  30. package/dist/openai-mcp-config-mapper.js.map +1 -0
  31. package/dist/openai-mcp-state.d.ts +67 -0
  32. package/dist/openai-mcp-state.js +6 -0
  33. package/dist/openai-mcp-state.js.map +1 -0
  34. package/dist/openai-message-mapper.d.ts +79 -0
  35. package/dist/openai-message-mapper.js +374 -0
  36. package/dist/openai-message-mapper.js.map +1 -0
  37. package/dist/openai-model-provider.d.ts +46 -0
  38. package/dist/openai-model-provider.js +144 -0
  39. package/dist/openai-model-provider.js.map +1 -0
  40. package/dist/openai-session-store.d.ts +149 -0
  41. package/dist/openai-session-store.js +328 -0
  42. package/dist/openai-session-store.js.map +1 -0
  43. package/dist/openai-tool-mapper.d.ts +121 -0
  44. package/dist/openai-tool-mapper.js +231 -0
  45. package/dist/openai-tool-mapper.js.map +1 -0
  46. package/dist/openai-tool-redaction.d.ts +55 -0
  47. package/dist/openai-tool-redaction.js +82 -0
  48. package/dist/openai-tool-redaction.js.map +1 -0
  49. package/dist/test/tsconfig.tsbuildinfo +1 -0
  50. package/dist/text-stream.d.ts +30 -0
  51. package/dist/text-stream.js +103 -0
  52. package/dist/text-stream.js.map +1 -0
  53. package/package.json +66 -0
@@ -0,0 +1,231 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import { tool } from '@openai/agents';
6
+ import { AgentSDKError, AgentSDKErrorType } from '@salesforce/sfdx-agent-sdk';
7
+ import { applyRedaction } from './openai-tool-redaction.js';
8
+ /**
9
+ * Per-turn registry for consumer-executed (client-side) tools — those declared
10
+ * in `AgentConfig.tools` with no `execute`. The harness registers them with the
11
+ * model so it can call them, but never runs them: the mapped tool's `execute`
12
+ * PARKS on a promise this registry hands out, and the consumer resolves it
13
+ * out-of-band via `submitToolResult`. A parked `execute` resolved later lets the
14
+ * `@openai/agents` run loop continue on the same run — no in-process MCP bridge
15
+ * needed.
16
+ *
17
+ * Keyed by `toolCallId`, read directly off `details.toolCall.callId` at execute
18
+ * entry (the SDK populates it before invoking the tool), so binding is a direct
19
+ * map insert — no FIFO-by-name matching.
20
+ *
21
+ * Idempotent settle (#589): a second `settle` on the same id, or a settle after
22
+ * teardown rejected it, is a silent no-op; a `toolCallId` that never parked is
23
+ * genuine misuse and throws `TOOL_CALL_NOT_FOUND`.
24
+ */
25
+ export class ConsumerToolRegistry {
26
+ toolNames;
27
+ /** Parked calls awaiting a consumer result, by `toolCallId`. */
28
+ pending = new Map();
29
+ /**
30
+ * Outcomes the consumer submitted BEFORE the tool's `execute` parked, by
31
+ * `toolCallId`. The harness emits the `tool-call` ChatEvent the moment the SDK
32
+ * enqueues `tool_called` — which is BEFORE it awaits `execute` — so a consumer
33
+ * iterating the stream can call `submitToolResult` before the parker
34
+ * registers. Buffer it here; `park` drains it immediately so the result is
35
+ * never lost to the race.
36
+ */
37
+ preSubmitted = new Map();
38
+ /** Ids already settled (by consumer or teardown), so a repeat settle is a no-op. */
39
+ settled = new Set();
40
+ /**
41
+ * Consumer-tool call ids the consumer settled with `isError: true`. The run
42
+ * loop persists the resulting `function_call_result` with no `isError` field
43
+ * (the OpenAI item shape can't carry it), so the coordinator reads this after
44
+ * the turn settles and stamps `isError` onto those records out-of-band,
45
+ * satisfying the #647 "isError survives to history" contract.
46
+ */
47
+ erroredIds = new Set();
48
+ /**
49
+ * Consumer-tool call ids the coordinator has surfaced as a `tool-call`
50
+ * ChatEvent this turn. A `settle` for a not-yet-parked id is buffered only
51
+ * when the id is here (an early-arriving result); an id never seen is genuine
52
+ * misuse and throws.
53
+ */
54
+ emitted = new Set();
55
+ /** Whether the turn has torn down — a park after teardown resolves immediately as an error. */
56
+ tornDown = false;
57
+ /** Error to resolve late parks with after teardown. */
58
+ teardownError;
59
+ /** The bare names of the consumer tools registered this turn (for dual-enforcement checks). */
60
+ constructor(toolNames) {
61
+ this.toolNames = toolNames;
62
+ }
63
+ /**
64
+ * Called by a mapped tool's `execute` at invocation. Returns a promise that
65
+ * settles when the consumer calls `submitToolResult` for `toolCallId`. It
66
+ * REJECTS only on teardown (so the run loop unblocks rather than hanging on a
67
+ * promise that will never settle); a normal settle — including an
68
+ * `isError: true` one — RESOLVES with the outcome, because a consumer-reported
69
+ * tool failure must reach the model as a recoverable tool output, not throw
70
+ * out of the run loop (the {@link ToolResultInfo.isError} contract). The
71
+ * mapper's `execute` turns the outcome into the model-visible value. A park
72
+ * after teardown rejects immediately.
73
+ */
74
+ park(toolCallId) {
75
+ if (this.tornDown)
76
+ return Promise.reject(this.teardownError ?? new Error('Turn ended.'));
77
+ // Drain a pre-submitted outcome (consumer settled before execute parked).
78
+ const early = this.preSubmitted.get(toolCallId);
79
+ if (early !== undefined) {
80
+ this.preSubmitted.delete(toolCallId);
81
+ return Promise.resolve(early);
82
+ }
83
+ return new Promise((resolve, reject) => {
84
+ this.pending.set(toolCallId, { resolve, reject });
85
+ });
86
+ }
87
+ /**
88
+ * Record that the coordinator surfaced `toolCallId` as a `tool-call`
89
+ * ChatEvent — so a `submitToolResult` that races ahead of the parker (the
90
+ * SDK emits `tool_called` before awaiting `execute`) is buffered rather than
91
+ * rejected.
92
+ */
93
+ markEmitted(toolCallId) {
94
+ this.emitted.add(toolCallId);
95
+ }
96
+ /**
97
+ * Settle a consumer tool call with the consumer's result. Resolves the parked
98
+ * `execute` if it has registered; otherwise buffers the outcome for the
99
+ * imminent `park` (the emit-before-await race) when the id was surfaced this
100
+ * turn. Idempotent (#589); throws `TOOL_CALL_NOT_FOUND` for an id the
101
+ * coordinator never surfaced.
102
+ */
103
+ settle(toolCallId, outcome) {
104
+ if (this.settled.has(toolCallId))
105
+ return;
106
+ const parked = this.pending.get(toolCallId);
107
+ if (parked !== undefined) {
108
+ this.settled.add(toolCallId);
109
+ if (outcome.isError)
110
+ this.erroredIds.add(toolCallId);
111
+ this.pending.delete(toolCallId);
112
+ parked.resolve(outcome);
113
+ return;
114
+ }
115
+ // Not parked yet. Buffer for the imminent `park` iff the coordinator
116
+ // surfaced this id this turn; a never-seen id is genuine misuse.
117
+ if (this.emitted.has(toolCallId)) {
118
+ this.settled.add(toolCallId);
119
+ if (outcome.isError)
120
+ this.erroredIds.add(toolCallId);
121
+ this.preSubmitted.set(toolCallId, outcome);
122
+ return;
123
+ }
124
+ throw new AgentSDKError(`No parked consumer tool call for id "${toolCallId}".`, AgentSDKErrorType.TOOL_CALL_NOT_FOUND);
125
+ }
126
+ /**
127
+ * The consumer-tool call ids settled with `isError: true` this turn — the
128
+ * records the coordinator stamps `isError` on after the run settles, since
129
+ * the run loop persists the `function_call_result` without it.
130
+ */
131
+ erroredToolCallIds() {
132
+ return [...this.erroredIds];
133
+ }
134
+ /**
135
+ * Reject every parked call on teardown so the run loop unblocks instead of
136
+ * hanging on a promise that will never settle. Marks all pending ids settled
137
+ * so a late consumer `submitToolResult` is a silent no-op (#589).
138
+ */
139
+ rejectAll(error) {
140
+ this.tornDown = true;
141
+ this.teardownError = error;
142
+ for (const [toolCallId, parked] of this.pending) {
143
+ this.settled.add(toolCallId);
144
+ // Reject the parked execute so the SDK run loop unblocks and the pump's
145
+ // catch synthesizes the terminal error + finish pair. (A normal settle
146
+ // resolves; only teardown rejects.)
147
+ parked.reject(error);
148
+ }
149
+ this.pending.clear();
150
+ }
151
+ }
152
+ /**
153
+ * Map an agent's consumer-tool `ToolDefinition[]` to `@openai/agents` function
154
+ * tools whose `execute` parks on the shared {@link ConsumerToolRegistry}. Every
155
+ * tool gets `needsApproval: false` — consumer tools are the consumer's
156
+ * responsibility via `submitToolResult` and are NEVER approval-gated (the
157
+ * dual-enforcement contract: the coordinator also short-circuits them by name
158
+ * before consulting the policy resolver).
159
+ *
160
+ * The mapped `execute` reads its own `toolCallId` from `details.toolCall.callId`
161
+ * (populated by the SDK before invocation) and parks under it; the returned
162
+ * value becomes the tool's output the model sees.
163
+ *
164
+ * When `redaction` is supplied (the agent has an `onToolResult` hook), the
165
+ * parked result passes through the redactor at this seam — before it becomes the
166
+ * model-visible output — with full fidelity (`agentId` / `threadId` /
167
+ * `toolCallId` / `toolName` / `serverName` all known). A redactor throw
168
+ * propagates out of `execute` (the pump's `catch` synthesizes the terminal
169
+ * error + finish pair), per the SDK's throws-propagate contract.
170
+ */
171
+ export function mapConsumerTools(defs, registry, redaction) {
172
+ return defs.map((def) => {
173
+ const execute = async (_input, _context, details) => {
174
+ const toolCallId = details?.toolCall?.callId;
175
+ if (toolCallId === undefined) {
176
+ // Without a callId the harness can't pair a `submitToolResult`, so
177
+ // fail the call rather than park forever.
178
+ throw new AgentSDKError(`Consumer tool "${def.name}" was invoked without a tool call id.`, AgentSDKErrorType.TOOL_CALL_NOT_FOUND);
179
+ }
180
+ const { result, isError } = await registry.park(toolCallId);
181
+ // A consumer-reported failure must reach the model as a recoverable
182
+ // tool output (the `isError` contract), not throw. The gateway's
183
+ // `function_call_result` has no `isError` field, so the signal rides
184
+ // in the output text; the harness persists the structured `isError`
185
+ // on the session record for history (#647).
186
+ const modelVisible = isError ? errorOutput(result) : result;
187
+ if (redaction === undefined)
188
+ return modelVisible;
189
+ // Redact the model-visible value in place. `output` is what the model
190
+ // would otherwise see (error-prefixed on the failure path), so the
191
+ // redactor scrubs the same string that reaches the transcript.
192
+ return applyRedaction(redaction, {
193
+ toolCallId,
194
+ toolName: def.name,
195
+ output: modelVisible,
196
+ isError: isError === true,
197
+ });
198
+ };
199
+ // A consumer `inputSchema` is an arbitrary JSON schema. `strict: false`
200
+ // selects the non-strict tool branch (Zod-only schemas need `strict: true`);
201
+ // the options are cast as one object because a consumer's free-form schema
202
+ // won't structurally match the SDK's `JsonObjectSchemaNonStrict` generic and
203
+ // the execute arg is `unknown` on the non-strict branch. Consumer tools are
204
+ // never gated (`needsApproval: false`) — they run client-side by definition.
205
+ return tool({
206
+ name: def.name,
207
+ description: def.description ?? '',
208
+ parameters: def.inputSchema,
209
+ strict: false,
210
+ needsApproval: false,
211
+ execute,
212
+ });
213
+ });
214
+ }
215
+ /**
216
+ * Shape a consumer-reported error outcome into a model-visible tool output. The
217
+ * consumer's `result` is passed through when it is already a string/serializable
218
+ * value; an `Error` becomes its message. Prefixed so the model reads it as a
219
+ * failure it can recover from rather than a normal result.
220
+ */
221
+ function errorOutput(result) {
222
+ const detail = result instanceof Error
223
+ ? result.message
224
+ : typeof result === 'string'
225
+ ? result
226
+ : result === undefined
227
+ ? 'unknown error'
228
+ : JSON.stringify(result);
229
+ return `Tool execution failed: ${detail}`;
230
+ }
231
+ //# sourceMappingURL=openai-tool-mapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-tool-mapper.js","sourceRoot":"","sources":["../src/openai-tool-mapper.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import { type MCPServer, type Tool } from '@openai/agents';
2
+ import type { ToolResultRedactor } from '@salesforce/sfdx-agent-sdk';
3
+ import type { McpCatalogEntry } from './openai-mcp-state.js';
4
+ /**
5
+ * Everything a redaction seam needs to call the consumer's
6
+ * {@link ToolResultRedactor} with a faithful {@link ToolResultRedactionInput}.
7
+ * Built per-turn in `stream()` (so `threadId` is stable across approval
8
+ * resumes) and threaded into both the consumer-tool `execute` seam and the MCP
9
+ * `invoke`-wrap seam. `undefined` when the agent has no `onToolResult` hook —
10
+ * both seams then take their pass-through path with zero overhead.
11
+ */
12
+ export type RedactionContext = {
13
+ readonly redactor: ToolResultRedactor;
14
+ readonly agentId: string;
15
+ readonly threadId: string;
16
+ /** Bare-tool-name → `{ serverName, annotations }`; supplies `serverName` for MCP tools. */
17
+ readonly mcpCatalog: ReadonlyMap<string, McpCatalogEntry>;
18
+ };
19
+ /**
20
+ * Run the consumer's redactor over one tool result and return the value the
21
+ * model should see. `{ output }` replaces; `undefined` (or a malformed return)
22
+ * passes the original through. **Throws propagate** — the SDK contract requires
23
+ * a redactor exception to ride the harness's native error path so the original
24
+ * value never reaches the model; callers MUST NOT catch it.
25
+ *
26
+ * `serverName` is looked up from the catalog (populated for MCP tools, absent
27
+ * for consumer / built-in tools, matching the cross-harness `tool-result`
28
+ * enrichment shape).
29
+ */
30
+ export declare function applyRedaction(ctx: RedactionContext, args: {
31
+ toolCallId: string;
32
+ toolName: string;
33
+ output: unknown;
34
+ isError: boolean;
35
+ }): Promise<unknown>;
36
+ /**
37
+ * Materialize an agent's MCP tools as function tools (via {@link getAllMcpTools},
38
+ * the same call the SDK makes internally for a native `Agent({ mcpServers })`
39
+ * attach) and wrap each one's `invoke` with a redaction shim. Used only when the
40
+ * agent has an `onToolResult` hook — otherwise the harness keeps the native
41
+ * `mcpServers` attach unchanged (the #541-proven path).
42
+ *
43
+ * The `@openai/agents` MCP attach exposes no per-result rewrite hook, so owning
44
+ * the function-tool `invoke` is the only model-visible seam that also carries
45
+ * the `toolCallId` (`details.toolCall.callId`). The shim closure-captures the
46
+ * {@link RedactionContext}, so `threadId` / `agentId` need no `RunContext`
47
+ * plumbing.
48
+ *
49
+ * **#541 is preserved:** `getAllMcpTools` calls `listTools()` per server, which
50
+ * returns the warm instance cache (`cacheToolsList: true`) with no network
51
+ * `tools/list`. **`isError` caveat:** the wrapped `invoke` sees only the
52
+ * model-visible string output, not the SDK's internal error flag, so MCP
53
+ * `isError` is best-effort (duck-typed) — matching Claude's `PostToolUse`.
54
+ */
55
+ export declare function buildRedactedMcpTools(servers: MCPServer[], ctx: RedactionContext): Promise<Tool[]>;
@@ -0,0 +1,82 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import { getAllMcpTools } from '@openai/agents';
6
+ /**
7
+ * Run the consumer's redactor over one tool result and return the value the
8
+ * model should see. `{ output }` replaces; `undefined` (or a malformed return)
9
+ * passes the original through. **Throws propagate** — the SDK contract requires
10
+ * a redactor exception to ride the harness's native error path so the original
11
+ * value never reaches the model; callers MUST NOT catch it.
12
+ *
13
+ * `serverName` is looked up from the catalog (populated for MCP tools, absent
14
+ * for consumer / built-in tools, matching the cross-harness `tool-result`
15
+ * enrichment shape).
16
+ */
17
+ export async function applyRedaction(ctx, args) {
18
+ const serverName = ctx.mcpCatalog.get(args.toolName)?.serverName;
19
+ const result = await ctx.redactor({
20
+ agentId: ctx.agentId,
21
+ threadId: ctx.threadId,
22
+ toolCallId: args.toolCallId,
23
+ toolName: args.toolName,
24
+ ...(serverName !== undefined ? { serverName } : {}),
25
+ output: args.output,
26
+ isError: args.isError,
27
+ });
28
+ // `{ output }` replaces; anything else (incl. `undefined`) passes through.
29
+ return result && typeof result === 'object' && 'output' in result ? result.output : args.output;
30
+ }
31
+ /**
32
+ * Materialize an agent's MCP tools as function tools (via {@link getAllMcpTools},
33
+ * the same call the SDK makes internally for a native `Agent({ mcpServers })`
34
+ * attach) and wrap each one's `invoke` with a redaction shim. Used only when the
35
+ * agent has an `onToolResult` hook — otherwise the harness keeps the native
36
+ * `mcpServers` attach unchanged (the #541-proven path).
37
+ *
38
+ * The `@openai/agents` MCP attach exposes no per-result rewrite hook, so owning
39
+ * the function-tool `invoke` is the only model-visible seam that also carries
40
+ * the `toolCallId` (`details.toolCall.callId`). The shim closure-captures the
41
+ * {@link RedactionContext}, so `threadId` / `agentId` need no `RunContext`
42
+ * plumbing.
43
+ *
44
+ * **#541 is preserved:** `getAllMcpTools` calls `listTools()` per server, which
45
+ * returns the warm instance cache (`cacheToolsList: true`) with no network
46
+ * `tools/list`. **`isError` caveat:** the wrapped `invoke` sees only the
47
+ * model-visible string output, not the SDK's internal error flag, so MCP
48
+ * `isError` is best-effort (duck-typed) — matching Claude's `PostToolUse`.
49
+ */
50
+ export async function buildRedactedMcpTools(servers, ctx) {
51
+ const tools = await getAllMcpTools({ mcpServers: servers });
52
+ return tools.map((tool) => (tool.type === 'function' ? wrapFunctionToolInvoke(tool, ctx) : tool));
53
+ }
54
+ /** Wrap a function tool's `invoke` so its result passes through the redactor before the model sees it. */
55
+ function wrapFunctionToolInvoke(tool, ctx) {
56
+ const originalInvoke = tool.invoke.bind(tool);
57
+ return {
58
+ ...tool,
59
+ invoke: async (runContext, input, details) => {
60
+ const output = await originalInvoke(runContext, input, details);
61
+ const toolCallId = details?.toolCall?.callId;
62
+ // Without a callId the redaction input can't be faithfully attributed;
63
+ // still redact (the redactor keys on toolName/output), passing an empty
64
+ // id rather than dropping the hook.
65
+ const redacted = await applyRedaction(ctx, {
66
+ toolCallId: toolCallId ?? '',
67
+ toolName: tool.name,
68
+ output,
69
+ isError: isErrorOutput(output),
70
+ });
71
+ // `invoke` must return `string | Result`; a redactor may return any
72
+ // shape (the consumer owns matching the tool's expected shape, per the
73
+ // ToolResultRedactor contract — the harness does not validate it).
74
+ return redacted;
75
+ },
76
+ };
77
+ }
78
+ /** Best-effort MCP error detection: the model-visible output carries no structured flag, so duck-type it. */
79
+ function isErrorOutput(output) {
80
+ return typeof output === 'object' && output !== null && output.isError === true;
81
+ }
82
+ //# sourceMappingURL=openai-tool-redaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-tool-redaction.js","sourceRoot":"","sources":["../src/openai-tool-redaction.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,cAAc,EAA6B,MAAM,gBAAgB,CAAC;AAoB3E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAChC,GAAqB,EACrB,IAAiF;IAEjF,MAAM,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC;IACjE,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC;QAC9B,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnD,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO;KACxB,CAAC,CAAC;IACH,2EAA2E;IAC3E,OAAO,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,QAAQ,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;AACpG,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,OAAoB,EAAE,GAAqB;IACnF,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;IAC5D,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,sBAAsB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACtG,CAAC;AAED,0GAA0G;AAC1G,SAAS,sBAAsB,CAAC,IAAyC,EAAE,GAAqB;IAC5F,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9C,OAAO;QACH,GAAG,IAAI;QACP,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACzC,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YAChE,MAAM,UAAU,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC;YAC7C,uEAAuE;YACvE,wEAAwE;YACxE,oCAAoC;YACpC,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,GAAG,EAAE;gBACvC,UAAU,EAAE,UAAU,IAAI,EAAE;gBAC5B,QAAQ,EAAE,IAAI,CAAC,IAAI;gBACnB,MAAM;gBACN,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC;aACjC,CAAC,CAAC;YACH,oEAAoE;YACpE,uEAAuE;YACvE,mEAAmE;YACnE,OAAO,QAAkB,CAAC;QAC9B,CAAC;KACJ,CAAC;AACN,CAAC;AAED,6GAA6G;AAC7G,SAAS,aAAa,CAAC,MAAe;IAClC,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAK,MAAgC,CAAC,OAAO,KAAK,IAAI,CAAC;AAC/G,CAAC"}