qwen-agent-server 0.11.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.
@@ -0,0 +1,147 @@
1
+ // SPDX-License-Identifier: MIT
2
+ //
3
+ // openai-compat.ts — shared OpenAI-compatible dispatch primitives.
4
+ //
5
+ // All the direct-HTTP tools (qwen_oneshot_vision, qwen_embed, qwen_rerank,
6
+ // qwen_tokenize) bypass `@qwen-code/sdk` (which is Qwen-CLI/text-chat
7
+ // only) and POST directly to the backend. Before this module, each tool
8
+ // duplicated the fetch/abort/HTTP-error-classification/JSON-parse logic.
9
+ //
10
+ // This module centralises that pattern. Each module supplies:
11
+ // - the endpoint suffix to hit (relative to backend.url, with /v1
12
+ // handling per-endpoint as needed)
13
+ // - the request body
14
+ // - per-module response normalization and specialized error codes
15
+ // (no_data / no_results / no_tokens / wrong_modality / etc.)
16
+ //
17
+ // What's centralised here:
18
+ // - auth header resolution (api_key literal / api_key_env / extra headers)
19
+ // - timeout via AbortController
20
+ // - HTTP error envelope { timeout | backend_error }
21
+ // - JSON parse error envelope
22
+ // - status code + raw response text passthrough so callers can
23
+ // classify specific provider error shapes (e.g. vision's "image
24
+ // input is not supported" -> backend_no_mmproj)
25
+ //
26
+ // What's NOT centralised:
27
+ // - response normalization (each endpoint has its own shape)
28
+ // - module-specific error codes (kept in each caller for type-safety
29
+ // of the public error union)
30
+ /**
31
+ * Resolve the Authorization + extra headers to send to this backend.
32
+ *
33
+ * - If `backend.api_key` is set, use it directly.
34
+ * - Else if `backend.api_key_env` is set, read `process.env[that]` at
35
+ * request time (rotations apply on next call, no supervisor reload).
36
+ * - Then merge `backend.headers` (caller overrides built-ins).
37
+ *
38
+ * Returns an empty object for backends with no auth (the common local
39
+ * llama-server case).
40
+ */
41
+ export function resolveAuthHeaders(backend) {
42
+ const headers = {};
43
+ let key;
44
+ if (backend.api_key !== undefined && backend.api_key !== "") {
45
+ key = backend.api_key;
46
+ }
47
+ else if (backend.api_key_env !== undefined && backend.api_key_env !== "") {
48
+ key = process.env[backend.api_key_env];
49
+ }
50
+ if (key !== undefined && key !== "") {
51
+ headers["Authorization"] = `Bearer ${key}`;
52
+ }
53
+ if (backend.headers !== undefined) {
54
+ Object.assign(headers, backend.headers);
55
+ }
56
+ return headers;
57
+ }
58
+ /**
59
+ * Compose a request URL from a backend's base + an endpoint suffix.
60
+ *
61
+ * Three cases:
62
+ * - `endpoint` starts with `/v1/` and backend.url ends in `/v1` →
63
+ * trim one /v1 to avoid duplication.
64
+ * - `endpoint` starts with `/` and is non-/v1 (e.g. `/tokenize`) →
65
+ * strip the `/v1` suffix from backend.url so the request lands at
66
+ * the server root.
67
+ * - Otherwise just join.
68
+ *
69
+ * Trailing slashes on backend.url are normalised away.
70
+ */
71
+ export function buildRequestUrl(backendUrl, endpoint) {
72
+ const base = backendUrl.replace(/\/$/, "");
73
+ if (endpoint.startsWith("/v1/")) {
74
+ // Backend URL likely ends in /v1; strip it so we don't double up.
75
+ return `${base.replace(/\/v1$/, "")}${endpoint}`;
76
+ }
77
+ if (endpoint.startsWith("/") && !endpoint.startsWith("/v1")) {
78
+ // Root-relative non-v1 (e.g. /tokenize) — strip /v1 from base.
79
+ return `${base.replace(/\/v1$/, "")}${endpoint}`;
80
+ }
81
+ // Default: append as a path under the configured base.
82
+ return `${base}/${endpoint.replace(/^\//, "")}`;
83
+ }
84
+ /**
85
+ * POST a JSON body to an OpenAI-compatible endpoint on a backend.
86
+ *
87
+ * Resolves auth + custom headers, manages the AbortController timeout,
88
+ * classifies network / HTTP failures into the shared envelope, and
89
+ * returns the raw response text on success so each caller can do its
90
+ * own typed parse (the JSON shape is endpoint-specific).
91
+ *
92
+ * Never throws.
93
+ */
94
+ export async function dispatchOpenAIPost(backend, endpoint, body, opts) {
95
+ const start = Date.now();
96
+ const url = buildRequestUrl(backend.url, endpoint);
97
+ const headers = {
98
+ "Content-Type": "application/json",
99
+ ...resolveAuthHeaders(backend),
100
+ };
101
+ const controller = new AbortController();
102
+ const timer = setTimeout(() => controller.abort(), opts.timeout_ms);
103
+ let resp;
104
+ try {
105
+ resp = await fetch(url, {
106
+ method: "POST",
107
+ headers,
108
+ body: JSON.stringify(body),
109
+ signal: controller.signal,
110
+ });
111
+ }
112
+ catch (err) {
113
+ clearTimeout(timer);
114
+ const aborted = err?.name === "AbortError";
115
+ return {
116
+ ok: false,
117
+ elapsed_ms: Date.now() - start,
118
+ error: aborted
119
+ ? { code: "timeout", message: `request aborted after ${opts.timeout_ms}ms` }
120
+ : {
121
+ code: "backend_error",
122
+ message: err instanceof Error ? err.message : String(err),
123
+ },
124
+ };
125
+ }
126
+ clearTimeout(timer);
127
+ const text = await resp.text();
128
+ if (!resp.ok) {
129
+ return {
130
+ ok: false,
131
+ elapsed_ms: Date.now() - start,
132
+ status: resp.status,
133
+ body_text: text,
134
+ error: {
135
+ code: "backend_error",
136
+ message: `HTTP ${resp.status}: ${text.slice(0, 300)}`,
137
+ },
138
+ };
139
+ }
140
+ return {
141
+ ok: true,
142
+ status: resp.status,
143
+ body_text: text,
144
+ elapsed_ms: Date.now() - start,
145
+ };
146
+ }
147
+ //# sourceMappingURL=openai-compat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-compat.js","sourceRoot":"","sources":["../src/openai-compat.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ // SPDX-License-Identifier: MIT
2
+ //
3
+ // makeCanUseTool — factory for the CanUseTool callback injected into the
4
+ // @qwen-code/sdk QueryOptions.
5
+ //
6
+ // Scope (post-2026-05-04 spike): canUseTool is responsible ONLY for
7
+ // write-tool permission gating. The original RDR §Q1 design also had
8
+ // canUseTool intercept ask_user_question to deliver answers via the
9
+ // deny-message field; that mechanism was empirically confirmed to fail
10
+ // (probe-tool-result.mjs, 2026-05-04 — the model treats the deny as
11
+ // "user cancelled with reason X", not "user answered X"). The
12
+ // supervisor now excludes ask_user_question from the inner Qwen's
13
+ // tool surface entirely (see session.ts DEFAULT_EXCLUDED_TOOLS); the
14
+ // model is told to ask in plain text and the user replies via
15
+ // streamInput-driven multi-turn input.
16
+ //
17
+ // Critical pins (RDR-001):
18
+ // §S4 Write tools when write_authority=false: emit a synthetic
19
+ // permission_denied event AND return deny. The event is
20
+ // important — without it, denials are silently swallowed by
21
+ // the SDK and the supervisor has no visibility into what the
22
+ // inner Qwen tried to do.
23
+ // §S4 Write tools when write_authority=true: this callback isn't
24
+ // invoked — permissionMode='yolo' bypasses canUseTool entirely.
25
+ // ─────────────────────────────────────────────────────────────────
26
+ // Write-tool set
27
+ //
28
+ // These tool names require write_authority===true to execute. Must be
29
+ // revisited if Qwen adds new write tools (names sourced from Qwen Code
30
+ // core tool registry as of SDK 0.1.7).
31
+ export const WRITE_TOOLS = new Set([
32
+ "write_file",
33
+ "edit",
34
+ "run_shell_command",
35
+ "replace",
36
+ "multi_edit",
37
+ ]);
38
+ // ─────────────────────────────────────────────────────────────────
39
+ // Factory
40
+ /**
41
+ * Returns a CanUseTool callback for use with permissionMode='default'.
42
+ *
43
+ * Routing (in order):
44
+ * - `ask_user_question` → emit permission_denied event + return deny
45
+ * with a hint message. Defense-in-depth: this tool is in the SDK
46
+ * excludeTools list and should never reach the callback. If it does
47
+ * (future SDK change, model bypass), denying is the safe choice —
48
+ * the SDK can't execute ask_user_question in headless mode anyway.
49
+ * - write tool → emit permission_denied event + return deny.
50
+ * - everything else → return allow (read tools, search, web_fetch, etc.).
51
+ */
52
+ export function makeCanUseTool(session) {
53
+ return async (toolName, input, _opts) => {
54
+ // Defense-in-depth: ask_user_question shouldn't reach us (excluded
55
+ // at the SDK level); if it does, deny with a clear hint.
56
+ if (toolName === "ask_user_question") {
57
+ session.pushEvent("permission_denied", `ask_user_question reached canUseTool — should be excluded`, { tool_name: toolName, input });
58
+ return {
59
+ behavior: "deny",
60
+ message: "ask_user_question is not available; ask in plain text in your response and the user will reply.",
61
+ };
62
+ }
63
+ if (WRITE_TOOLS.has(toolName)) {
64
+ session.pushEvent("permission_denied", `write_authority not granted for ${toolName}`, { tool_name: toolName, input });
65
+ return { behavior: "deny", message: "write_authority not granted" };
66
+ }
67
+ // Read tools / search tools / web_fetch / etc. — auto-allow.
68
+ return { behavior: "allow", updatedInput: input };
69
+ };
70
+ }
71
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.js","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,EAAE;AACF,yEAAyE;AACzE,+BAA+B;AAC/B,EAAE;AACF,oEAAoE;AACpE,qEAAqE;AACrE,oEAAoE;AACpE,uEAAuE;AACvE,oEAAoE;AACpE,8DAA8D;AAC9D,kEAAkE;AAClE,qEAAqE;AACrE,8DAA8D;AAC9D,uCAAuC;AACvC,EAAE;AACF,2BAA2B;AAC3B,kEAAkE;AAClE,+DAA+D;AAC/D,mEAAmE;AACnE,oEAAoE;AACpE,iCAAiC;AACjC,oEAAoE;AACpE,uEAAuE;AAKvE,oEAAoE;AACpE,iBAAiB;AACjB,EAAE;AACF,sEAAsE;AACtE,uEAAuE;AACvE,uCAAuC;AAEvC,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAS;IACzC,YAAY;IACZ,MAAM;IACN,mBAAmB;IACnB,SAAS;IACT,YAAY;CACb,CAAC,CAAC;AAEH,oEAAoE;AACpE,UAAU;AAEV;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAAC,OAAoB;IACjD,OAAO,KAAK,EACV,QAAgB,EAChB,KAAgB,EAChB,KAA8B,EACH,EAAE;QAC7B,mEAAmE;QACnE,yDAAyD;QACzD,IAAI,QAAQ,KAAK,mBAAmB,EAAE,CAAC;YACrC,OAAO,CAAC,SAAS,CACf,mBAAmB,EACnB,2DAA2D,EAC3D,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,CAC/B,CAAC;YACF,OAAO;gBACL,QAAQ,EAAE,MAAM;gBAChB,OAAO,EACL,iGAAiG;aACpG,CAAC;QACJ,CAAC;QAED,IAAI,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9B,OAAO,CAAC,SAAS,CACf,mBAAmB,EACnB,mCAAmC,QAAQ,EAAE,EAC7C,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,CAC/B,CAAC;YACF,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,6BAA6B,EAAE,CAAC;QACtE,CAAC;QAED,6DAA6D;QAC7D,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;IACpD,CAAC,CAAC;AACJ,CAAC"}
package/dist/pool.js ADDED
@@ -0,0 +1,155 @@
1
+ // SPDX-License-Identifier: MIT
2
+ //
3
+ // Session pool: cap enforcement, LRU eviction, idle reaper.
4
+ //
5
+ // Pool semantics (per RDR-001 §Q4):
6
+ // - Hard cap: QWEN_SUPERVISOR_MAX_SESSIONS (default 3)
7
+ // - Idle TTL: QWEN_SUPERVISOR_IDLE_TTL_MS (default 30 min)
8
+ // - Eviction pass 1: terminal (complete/error) sessions first
9
+ // - Eviction pass 2: least-recently-polled by last_polled_at
10
+ // - Reaper: sweeps idle sessions every 5 min (interval.unref()d)
11
+ import { createLogger } from "./log.js";
12
+ import { QwenSession } from "./session.js";
13
+ import { chooseBackend, getSessionBudgetDefaults, loadBackends } from "./backends.js";
14
+ const log = createLogger("qwen-pool");
15
+ // ─────────────────────────────────────────────────────────────────
16
+ // Factory
17
+ export function createPool(opts = {}) {
18
+ const maxSessions = parseInt(process.env["QWEN_SUPERVISOR_MAX_SESSIONS"] ?? "", 10) || 3;
19
+ const idleTtlMs = parseInt(process.env["QWEN_SUPERVISOR_IDLE_TTL_MS"] ?? "", 10) || 30 * 60 * 1000;
20
+ const backends = loadBackends();
21
+ return {
22
+ sessions: new Map(),
23
+ maxSessions,
24
+ idleTtlMs,
25
+ backends,
26
+ qwenRealBin: opts.qwenRealBin ?? "",
27
+ wrapperPath: opts.wrapperPath ?? "",
28
+ };
29
+ }
30
+ // ─────────────────────────────────────────────────────────────────
31
+ // LRU eviction
32
+ /**
33
+ * Evict one session when the pool is at or above cap.
34
+ *
35
+ * Pass 1: drop any complete/error session (they're done; cheap to evict).
36
+ * Pass 2: evict the session with the smallest last_polled_at.
37
+ */
38
+ export function lruEvict(pool) {
39
+ if (pool.sessions.size < pool.maxSessions)
40
+ return;
41
+ // Pass 1: terminal sessions
42
+ for (const [id, session] of pool.sessions) {
43
+ if (session.state === "complete" || session.state === "error") {
44
+ session.stop();
45
+ pool.sessions.delete(id);
46
+ log.info({ task_id: id, state: session.state, event_type: "evict" }, "evicted terminal session at cap");
47
+ return;
48
+ }
49
+ }
50
+ // Pass 2: oldest by last_polled_at
51
+ let oldest;
52
+ let oldestId;
53
+ for (const [id, session] of pool.sessions) {
54
+ if (!oldest || session.last_polled_at < oldest.last_polled_at) {
55
+ oldest = session;
56
+ oldestId = id;
57
+ }
58
+ }
59
+ if (oldest && oldestId !== undefined) {
60
+ oldest.stop();
61
+ pool.sessions.delete(oldestId);
62
+ log.info({ task_id: oldestId, event_type: "evict" }, "evicted LRU session at cap");
63
+ }
64
+ }
65
+ // ─────────────────────────────────────────────────────────────────
66
+ // Reaper
67
+ /**
68
+ * Sweep sessions idle beyond idleTtlMs. Called periodically by the
69
+ * server's setInterval reaper (every 5 min).
70
+ *
71
+ * Reaps `idle`, `complete`, and `error` sessions that haven't been
72
+ * polled within idleTtlMs. Sessions in the `running` state are SKIPPED
73
+ * regardless of poll age — the inner Qwen may be processing a long
74
+ * tool call (codebase scan, web fetch, etc.) and killing it because
75
+ * the caller hasn't polled would terminate active work. The cap
76
+ * (lruEvict) is the backstop for runaway running sessions.
77
+ */
78
+ export function reapSweep(pool) {
79
+ const now = Date.now();
80
+ const toReap = [];
81
+ for (const [id, session] of pool.sessions) {
82
+ if (session.state === "running")
83
+ continue;
84
+ if (now - session.last_polled_at > pool.idleTtlMs) {
85
+ toReap.push(id);
86
+ }
87
+ }
88
+ for (const id of toReap) {
89
+ const session = pool.sessions.get(id);
90
+ if (session) {
91
+ session.stop();
92
+ pool.sessions.delete(id);
93
+ log.info({ task_id: id, state: session.state, event_type: "reap" }, "reaped idle session");
94
+ }
95
+ }
96
+ }
97
+ // ─────────────────────────────────────────────────────────────────
98
+ // Spawn
99
+ /**
100
+ * Spawn a new session, applying LRU eviction if at cap first.
101
+ * Returns the new QwenSession.
102
+ *
103
+ * `resolvedExtensions` is the output of `resolveExtensions()` from the
104
+ * qwen_spawn handler — pre-validated; spawnSession does not re-validate.
105
+ * Pass undefined for code paths (notably tests) that don't supply a
106
+ * resolution; the session will fall through to default SDK behaviour.
107
+ */
108
+ export async function spawnSession(pool, task, opts, resolvedExtensions) {
109
+ const spawnOpts = {
110
+ write_authority: opts.write_authority ?? false,
111
+ allow_subagents: opts.allow_subagents ?? false,
112
+ ...opts,
113
+ };
114
+ // Evict before adding — ensures we never exceed cap
115
+ while (pool.sessions.size >= pool.maxSessions) {
116
+ lruEvict(pool);
117
+ }
118
+ const backend = await chooseBackend(pool.backends, spawnOpts, task);
119
+ if (!backend) {
120
+ throw new Error("no backend available");
121
+ }
122
+ // Fill in budget defaults now that the backend is known. Caller-set
123
+ // opts win; otherwise env / config / floor(0.85 * backend.ctx_size) /
124
+ // hardcoded fall through (RDR-002 v0.7 amendment). Done here rather
125
+ // than in qwen_spawn so the resolution can reflect the chosen
126
+ // backend's declared context window.
127
+ if (spawnOpts.max_context_tokens === undefined || spawnOpts.max_tool_calls === undefined) {
128
+ const defaults = getSessionBudgetDefaults(process.env, backend);
129
+ if (spawnOpts.max_context_tokens === undefined) {
130
+ spawnOpts.max_context_tokens = defaults.max_context_tokens;
131
+ }
132
+ if (spawnOpts.max_tool_calls === undefined) {
133
+ spawnOpts.max_tool_calls = defaults.max_tool_calls;
134
+ }
135
+ }
136
+ const session = new QwenSession(backend, task, spawnOpts, {
137
+ qwenRealBin: pool.qwenRealBin,
138
+ wrapperPath: pool.wrapperPath,
139
+ }, resolvedExtensions);
140
+ const pooledSession = Object.assign(session, { last_polled_at: Date.now() });
141
+ pool.sessions.set(session.task_id, pooledSession);
142
+ log.info({
143
+ task_id: session.task_id,
144
+ backend_id: backend.id,
145
+ event_type: "spawn",
146
+ state: "running",
147
+ }, "session spawned");
148
+ return pooledSession;
149
+ }
150
+ // ─────────────────────────────────────────────────────────────────
151
+ // Remove
152
+ export function removeSession(pool, task_id) {
153
+ pool.sessions.delete(task_id);
154
+ }
155
+ //# sourceMappingURL=pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.js","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"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"}
package/dist/rerank.js ADDED
@@ -0,0 +1,93 @@
1
+ // SPDX-License-Identifier: MIT
2
+ //
3
+ // rerank.ts — direct-HTTP dispatch for `qwen_rerank`.
4
+ //
5
+ // llama-server exposes /v1/rerank (with aliases /rerank, /reranking,
6
+ // /v1/reranking) when started with `--reranking` and a reranker model
7
+ // (e.g. qwen3-reranker, bge-reranker). Returns relevance scores for
8
+ // each document against the query.
9
+ import { createLogger } from "./log.js";
10
+ import { dispatchOpenAIPost } from "./openai-compat.js";
11
+ const log = createLogger("qwen-rerank");
12
+ const DEFAULT_TIMEOUT_MS = 60_000;
13
+ export async function dispatchRerank(backend, query, documents, opts = {}) {
14
+ const timeout_ms = opts.timeout_ms ?? DEFAULT_TIMEOUT_MS;
15
+ const body = {
16
+ model: backend.model,
17
+ query,
18
+ documents,
19
+ };
20
+ if (opts.top_n !== undefined)
21
+ body.top_n = opts.top_n;
22
+ if (opts.return_documents !== undefined) {
23
+ body.return_documents = opts.return_documents;
24
+ }
25
+ const outcome = await dispatchOpenAIPost(backend, "/v1/rerank", body, {
26
+ timeout_ms,
27
+ });
28
+ if (!outcome.ok) {
29
+ if (outcome.status !== undefined) {
30
+ log.warn({
31
+ backend_id: backend.id,
32
+ status: outcome.status,
33
+ body_excerpt: outcome.body_text?.slice(0, 200),
34
+ }, "rerank dispatch HTTP failure");
35
+ }
36
+ return {
37
+ ok: false,
38
+ elapsed_ms: outcome.elapsed_ms,
39
+ backend_id: backend.id,
40
+ error: outcome.error,
41
+ };
42
+ }
43
+ let parsed;
44
+ try {
45
+ parsed = JSON.parse(outcome.body_text);
46
+ }
47
+ catch (err) {
48
+ return {
49
+ ok: false,
50
+ elapsed_ms: outcome.elapsed_ms,
51
+ backend_id: backend.id,
52
+ error: {
53
+ code: "backend_error",
54
+ message: `non-JSON response: ${err.message}`,
55
+ },
56
+ };
57
+ }
58
+ if (!Array.isArray(parsed.results) || parsed.results.length === 0) {
59
+ return {
60
+ ok: false,
61
+ elapsed_ms: outcome.elapsed_ms,
62
+ backend_id: backend.id,
63
+ error: { code: "no_results", message: "backend returned empty results" },
64
+ };
65
+ }
66
+ // Normalize: ensure index + relevance_score present; flatten
67
+ // document.{text} shape that some servers emit.
68
+ const results = parsed.results
69
+ .filter((r) => typeof r.index === "number" && typeof r.relevance_score === "number")
70
+ .map((r) => {
71
+ const out = {
72
+ index: r.index,
73
+ relevance_score: r.relevance_score,
74
+ };
75
+ if (typeof r.document === "string") {
76
+ out.document = r.document;
77
+ }
78
+ else if (r.document && typeof r.document === "object" && typeof r.document.text === "string") {
79
+ out.document = r.document.text;
80
+ }
81
+ return out;
82
+ })
83
+ .sort((a, b) => b.relevance_score - a.relevance_score);
84
+ return {
85
+ ok: true,
86
+ results,
87
+ ...(parsed.usage !== undefined ? { usage: parsed.usage } : {}),
88
+ ...(parsed.model !== undefined ? { model: parsed.model } : {}),
89
+ elapsed_ms: outcome.elapsed_ms,
90
+ backend_id: backend.id,
91
+ };
92
+ }
93
+ //# sourceMappingURL=rerank.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rerank.js","sourceRoot":"","sources":["../src/rerank.ts"],"names":[],"mappings":"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"}