@esankhan3/anvil-agent-core 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/gemini-adapter-recording.test.d.ts +11 -0
  3. package/dist/__tests__/gemini-adapter-recording.test.d.ts.map +1 -0
  4. package/dist/__tests__/gemini-adapter-recording.test.js +108 -0
  5. package/dist/__tests__/gemini-adapter-recording.test.js.map +1 -0
  6. package/dist/__tests__/legacy-adapter-language-model.test.d.ts +11 -0
  7. package/dist/__tests__/legacy-adapter-language-model.test.d.ts.map +1 -0
  8. package/dist/__tests__/legacy-adapter-language-model.test.js +102 -0
  9. package/dist/__tests__/legacy-adapter-language-model.test.js.map +1 -0
  10. package/dist/__tests__/ollama-adapter-recording.test.d.ts +15 -0
  11. package/dist/__tests__/ollama-adapter-recording.test.d.ts.map +1 -0
  12. package/dist/__tests__/ollama-adapter-recording.test.js +128 -0
  13. package/dist/__tests__/ollama-adapter-recording.test.js.map +1 -0
  14. package/dist/__tests__/openrouter-prefill.integration.test.d.ts +19 -0
  15. package/dist/__tests__/openrouter-prefill.integration.test.d.ts.map +1 -0
  16. package/dist/__tests__/openrouter-prefill.integration.test.js +410 -0
  17. package/dist/__tests__/openrouter-prefill.integration.test.js.map +1 -0
  18. package/dist/__tests__/prefill-truncate.test.d.ts +8 -0
  19. package/dist/__tests__/prefill-truncate.test.d.ts.map +1 -0
  20. package/dist/__tests__/prefill-truncate.test.js +75 -0
  21. package/dist/__tests__/prefill-truncate.test.js.map +1 -0
  22. package/dist/__tests__/router-circuit-breaker.test.js +1 -1
  23. package/dist/__tests__/router-circuit-breaker.test.js.map +1 -1
  24. package/dist/__tests__/router-classify-unified.test.d.ts +12 -0
  25. package/dist/__tests__/router-classify-unified.test.d.ts.map +1 -0
  26. package/dist/__tests__/router-classify-unified.test.js +108 -0
  27. package/dist/__tests__/router-classify-unified.test.js.map +1 -0
  28. package/dist/__tests__/router-fallback.test.js +1 -1
  29. package/dist/__tests__/router-fallback.test.js.map +1 -1
  30. package/dist/__tests__/router-model-registry.test.js +24 -0
  31. package/dist/__tests__/router-model-registry.test.js.map +1 -1
  32. package/dist/__tests__/router-retry.test.js +3 -5
  33. package/dist/__tests__/router-retry.test.js.map +1 -1
  34. package/dist/__tests__/router-runagent.test.d.ts +14 -0
  35. package/dist/__tests__/router-runagent.test.d.ts.map +1 -0
  36. package/dist/__tests__/router-runagent.test.js +95 -0
  37. package/dist/__tests__/router-runagent.test.js.map +1 -0
  38. package/dist/__tests__/router-scaffold.test.js +2 -1
  39. package/dist/__tests__/router-scaffold.test.js.map +1 -1
  40. package/dist/__tests__/router-spend-ledger.test.js +1 -1
  41. package/dist/__tests__/router-spend-ledger.test.js.map +1 -1
  42. package/dist/__tests__/router-stream-fallback.test.d.ts +13 -0
  43. package/dist/__tests__/router-stream-fallback.test.d.ts.map +1 -0
  44. package/dist/__tests__/router-stream-fallback.test.js +129 -0
  45. package/dist/__tests__/router-stream-fallback.test.js.map +1 -0
  46. package/dist/__tests__/router-telemetry.test.js +3 -1
  47. package/dist/__tests__/router-telemetry.test.js.map +1 -1
  48. package/dist/__tests__/util/mock-upstream.d.ts +77 -0
  49. package/dist/__tests__/util/mock-upstream.d.ts.map +1 -0
  50. package/dist/__tests__/util/mock-upstream.js +219 -0
  51. package/dist/__tests__/util/mock-upstream.js.map +1 -0
  52. package/dist/adk-adapter.d.ts.map +1 -1
  53. package/dist/adk-adapter.js +51 -0
  54. package/dist/adk-adapter.js.map +1 -1
  55. package/dist/agent/session/adapter.d.ts +15 -0
  56. package/dist/agent/session/adapter.d.ts.map +1 -1
  57. package/dist/agent/session/adapter.js +3 -0
  58. package/dist/agent/session/adapter.js.map +1 -1
  59. package/dist/agent/session/index.d.ts +1 -0
  60. package/dist/agent/session/index.d.ts.map +1 -1
  61. package/dist/agent/session/index.js +1 -0
  62. package/dist/agent/session/index.js.map +1 -1
  63. package/dist/agent/session/language-model-bridge.d.ts.map +1 -1
  64. package/dist/agent/session/language-model-bridge.js +22 -0
  65. package/dist/agent/session/language-model-bridge.js.map +1 -1
  66. package/dist/agent/session/legacy-adapter-language-model.d.ts +27 -0
  67. package/dist/agent/session/legacy-adapter-language-model.d.ts.map +1 -0
  68. package/dist/agent/session/legacy-adapter-language-model.js +221 -0
  69. package/dist/agent/session/legacy-adapter-language-model.js.map +1 -0
  70. package/dist/agent/session/types.d.ts +20 -0
  71. package/dist/agent/session/types.d.ts.map +1 -1
  72. package/dist/agent/session/types.js.map +1 -1
  73. package/dist/claude-adapter.d.ts.map +1 -1
  74. package/dist/claude-adapter.js +27 -0
  75. package/dist/claude-adapter.js.map +1 -1
  76. package/dist/gemini-adapter.d.ts.map +1 -1
  77. package/dist/gemini-adapter.js +46 -0
  78. package/dist/gemini-adapter.js.map +1 -1
  79. package/dist/index.d.ts +9 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +5 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/ollama-adapter.d.ts.map +1 -1
  84. package/dist/ollama-adapter.js +118 -8
  85. package/dist/ollama-adapter.js.map +1 -1
  86. package/dist/openrouter-adapter.d.ts.map +1 -1
  87. package/dist/openrouter-adapter.js +289 -15
  88. package/dist/openrouter-adapter.js.map +1 -1
  89. package/dist/prefill/strip.d.ts +49 -0
  90. package/dist/prefill/strip.d.ts.map +1 -0
  91. package/dist/prefill/strip.js +93 -0
  92. package/dist/prefill/strip.js.map +1 -0
  93. package/dist/prefill/translate.d.ts +55 -0
  94. package/dist/prefill/translate.d.ts.map +1 -0
  95. package/dist/prefill/translate.js +162 -0
  96. package/dist/prefill/translate.js.map +1 -0
  97. package/dist/prefill/truncate.d.ts +62 -0
  98. package/dist/prefill/truncate.d.ts.map +1 -0
  99. package/dist/prefill/truncate.js +77 -0
  100. package/dist/prefill/truncate.js.map +1 -0
  101. package/dist/router/errors.d.ts +24 -7
  102. package/dist/router/errors.d.ts.map +1 -1
  103. package/dist/router/errors.js +71 -14
  104. package/dist/router/errors.js.map +1 -1
  105. package/dist/router/index.d.ts +3 -2
  106. package/dist/router/index.d.ts.map +1 -1
  107. package/dist/router/index.js +2 -1
  108. package/dist/router/index.js.map +1 -1
  109. package/dist/router/model-registry.d.ts +22 -0
  110. package/dist/router/model-registry.d.ts.map +1 -1
  111. package/dist/router/model-registry.js +64 -1
  112. package/dist/router/model-registry.js.map +1 -1
  113. package/dist/router/provider-registry-resolver.d.ts +25 -0
  114. package/dist/router/provider-registry-resolver.d.ts.map +1 -0
  115. package/dist/router/provider-registry-resolver.js +84 -0
  116. package/dist/router/provider-registry-resolver.js.map +1 -0
  117. package/dist/router/retry.d.ts +3 -0
  118. package/dist/router/retry.d.ts.map +1 -1
  119. package/dist/router/retry.js +4 -1
  120. package/dist/router/retry.js.map +1 -1
  121. package/dist/router/router.d.ts +79 -1
  122. package/dist/router/router.d.ts.map +1 -1
  123. package/dist/router/router.js +256 -2
  124. package/dist/router/router.js.map +1 -1
  125. package/dist/router/types.d.ts +1 -1
  126. package/dist/router/types.d.ts.map +1 -1
  127. package/dist/router/types.js +1 -0
  128. package/dist/router/types.js.map +1 -1
  129. package/dist/turn-recorder/__tests__/turn-recorder.test.d.ts +20 -0
  130. package/dist/turn-recorder/__tests__/turn-recorder.test.d.ts.map +1 -0
  131. package/dist/turn-recorder/__tests__/turn-recorder.test.js +282 -0
  132. package/dist/turn-recorder/__tests__/turn-recorder.test.js.map +1 -0
  133. package/dist/turn-recorder/hash.d.ts +10 -0
  134. package/dist/turn-recorder/hash.d.ts.map +1 -0
  135. package/dist/turn-recorder/hash.js +35 -0
  136. package/dist/turn-recorder/hash.js.map +1 -0
  137. package/dist/turn-recorder/index.d.ts +119 -0
  138. package/dist/turn-recorder/index.d.ts.map +1 -0
  139. package/dist/turn-recorder/index.js +326 -0
  140. package/dist/turn-recorder/index.js.map +1 -0
  141. package/dist/turn-recorder/types.d.ts +227 -0
  142. package/dist/turn-recorder/types.d.ts.map +1 -0
  143. package/dist/turn-recorder/types.js +20 -0
  144. package/dist/turn-recorder/types.js.map +1 -0
  145. package/dist/types.d.ts +35 -2
  146. package/dist/types.d.ts.map +1 -1
  147. package/package.json +2 -2
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Cross-provider strip policy (v2 ADR §2.3.1).
3
+ *
4
+ * Vendor-specific message blocks DO NOT survive a chain-fallback swap
5
+ * across providers. `stripForTarget` removes fields the target provider
6
+ * would reject (or silently misinterpret) when the conversation
7
+ * originated on a different vendor.
8
+ *
9
+ * The matrix (§2.3.1):
10
+ * - reasoning_details[] / reasoning — OpenRouter reasoning models;
11
+ * strip when source !== target (Anthropic + OpenAI reject them).
12
+ * - cache_control — Anthropic; strip when target is non-Anthropic.
13
+ * - prompt_cache_key — OpenAI; strip when target is non-OpenAI.
14
+ *
15
+ * Same-vendor swaps short-circuit: nothing is stripped (hot path), and
16
+ * they additionally qualify for the §5.1 prefix-cache fast-path.
17
+ *
18
+ * The function is shape-agnostic: it walks each message object and
19
+ * deletes the known vendor keys (including inside content-block arrays
20
+ * for Anthropic's `cache_control`). It never mutates the input — every
21
+ * message is shallow-cloned before a key is removed.
22
+ */
23
+ /** Anthropic family: native `messages` API consumers.
24
+ *
25
+ * NOTE on 'adk': ADK is provider-agnostic at runtime (claude-via-adk =
26
+ * Anthropic wire; gemini-via-adk = Google wire). Per ADR §2.8, ADK
27
+ * prefill is materialized via `Session.appendEvent`, NOT through
28
+ * `materializePrefill`/`stripForTarget` — so this predicate is NOT
29
+ * exercised for ADK targets in the H2 path. When ADK is ported (H4),
30
+ * its wire format must be detected from the underlying model id, not
31
+ * assumed Anthropic here. Kept in the Anthropic bucket only so the
32
+ * §5.1 same-vendor cache fast-path can recognise `claude`↔`claude`;
33
+ * revisit when ADK joins the prefill path. */
34
+ function isAnthropic(p) {
35
+ return p === 'adk' || p === 'claude';
36
+ }
37
+ /** OpenAI-compatible chat-completions consumers. */
38
+ function isOpenAICompat(p) {
39
+ return p === 'openai' || p === 'openrouter' || p === 'opencode' || p === 'ollama';
40
+ }
41
+ /**
42
+ * Return a new message array safe to send to `targetProvider` given it
43
+ * originated on `sourceProvider`. Same-vendor → returned as a shallow
44
+ * copy untouched.
45
+ */
46
+ export function stripForTarget(messages, ctx) {
47
+ if (ctx.sourceProvider === ctx.targetProvider) {
48
+ return messages.slice();
49
+ }
50
+ const dropReasoning = !isOpenRouterFamily(ctx.targetProvider);
51
+ const dropCacheControl = !isAnthropic(ctx.targetProvider);
52
+ const dropPromptCacheKey = !isOpenAIExact(ctx.targetProvider);
53
+ return messages.map((raw) => {
54
+ if (!raw || typeof raw !== 'object')
55
+ return raw;
56
+ const msg = { ...raw };
57
+ if (dropReasoning) {
58
+ delete msg.reasoning;
59
+ delete msg.reasoning_details;
60
+ }
61
+ if (dropPromptCacheKey) {
62
+ delete msg.prompt_cache_key;
63
+ }
64
+ if (dropCacheControl) {
65
+ // top-level marker
66
+ delete msg.cache_control;
67
+ // Anthropic puts cache_control inside content-block arrays.
68
+ if (Array.isArray(msg.content)) {
69
+ msg.content = msg.content.map((block) => {
70
+ if (block && typeof block === 'object' && 'cache_control' in block) {
71
+ const clone = { ...block };
72
+ delete clone.cache_control;
73
+ return clone;
74
+ }
75
+ return block;
76
+ });
77
+ }
78
+ }
79
+ return msg;
80
+ });
81
+ }
82
+ /** OpenRouter (and OpenCode, which proxies OpenRouter-style models) are
83
+ * the only consumers that accept echoed reasoning_details. */
84
+ function isOpenRouterFamily(p) {
85
+ return p === 'openrouter' || p === 'opencode';
86
+ }
87
+ /** Exact OpenAI — the only consumer that honors `prompt_cache_key`. */
88
+ function isOpenAIExact(p) {
89
+ return p === 'openai';
90
+ }
91
+ // Re-export the family predicates for the translate layer + tests.
92
+ export { isAnthropic, isOpenAICompat };
93
+ //# sourceMappingURL=strip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strip.js","sourceRoot":"","sources":["../../src/prefill/strip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AASH;;;;;;;;;;8CAU8C;AAC9C,SAAS,WAAW,CAAC,CAAe;IAClC,OAAO,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,QAAQ,CAAC;AACvC,CAAC;AAED,oDAAoD;AACpD,SAAS,cAAc,CAAC,CAAe;IACrC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,YAAY,IAAI,CAAC,KAAK,UAAU,IAAI,CAAC,KAAK,QAAQ,CAAC;AACpF,CAAC;AAID;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,QAA4B,EAAE,GAAiB;IAC5E,IAAI,GAAG,CAAC,cAAc,KAAK,GAAG,CAAC,cAAc,EAAE,CAAC;QAC9C,OAAO,QAAQ,CAAC,KAAK,EAAE,CAAC;IAC1B,CAAC;IAED,MAAM,aAAa,GAAG,CAAC,kBAAkB,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9D,MAAM,gBAAgB,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC1D,MAAM,kBAAkB,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAE9D,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QAC1B,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAO,GAAG,CAAC;QAChD,MAAM,GAAG,GAAG,EAAE,GAAI,GAAoB,EAAE,CAAC;QAEzC,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO,GAAG,CAAC,SAAS,CAAC;YACrB,OAAO,GAAG,CAAC,iBAAiB,CAAC;QAC/B,CAAC;QACD,IAAI,kBAAkB,EAAE,CAAC;YACvB,OAAO,GAAG,CAAC,gBAAgB,CAAC;QAC9B,CAAC;QACD,IAAI,gBAAgB,EAAE,CAAC;YACrB,mBAAmB;YACnB,OAAO,GAAG,CAAC,aAAa,CAAC;YACzB,4DAA4D;YAC5D,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC/B,GAAG,CAAC,OAAO,GAAI,GAAG,CAAC,OAAqB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;oBACrD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,eAAe,IAAK,KAAgB,EAAE,CAAC;wBAC/E,MAAM,KAAK,GAAG,EAAE,GAAI,KAAiC,EAAE,CAAC;wBACxD,OAAO,KAAK,CAAC,aAAa,CAAC;wBAC3B,OAAO,KAAK,CAAC;oBACf,CAAC;oBACD,OAAO,KAAK,CAAC;gBACf,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC,CAAC;AACL,CAAC;AAED;+DAC+D;AAC/D,SAAS,kBAAkB,CAAC,CAAe;IACzC,OAAO,CAAC,KAAK,YAAY,IAAI,CAAC,KAAK,UAAU,CAAC;AAChD,CAAC;AAED,uEAAuE;AACvE,SAAS,aAAa,CAAC,CAAe;IACpC,OAAO,CAAC,KAAK,QAAQ,CAAC;AACxB,CAAC;AAED,mEAAmE;AACnE,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Tool-result + prefill shape translation (v2 ADR §2.3.2 / §2.3).
3
+ *
4
+ * The durable log stores tool results in a neutral shape
5
+ * (`NeutralToolResult`). When a chain-fallback swap re-injects a prior
6
+ * model's turn into a NEW provider, the adapter materializes that turn
7
+ * into the target provider's wire format. This module owns that
8
+ * translation.
9
+ *
10
+ * - Anthropic native:
11
+ * tool_use → { role: 'assistant', content: [{ type: 'tool_use', id, name, input }] }
12
+ * result → { role: 'user', content: [{ type: 'tool_result', tool_use_id, content, is_error }] }
13
+ * - OpenAI-compat (openrouter, opencode, openai, ollama):
14
+ * tool_use → { role: 'assistant', tool_calls: [{ id, type:'function', function:{ name, arguments } }] }
15
+ * result → { role: 'tool', tool_call_id, content: stringify }
16
+ * - ADK: materialized via Session.appendEvent inside the adk adapter
17
+ * (functionCall + functionResponse events) — NOT via this module.
18
+ *
19
+ * `materializePrefill` turns a `Prefill` into the ordered list of wire
20
+ * messages to splice into the request BEFORE the continuation: for each
21
+ * recorded tool use, an assistant(tool_call) + tool(result) pair, then
22
+ * a trailing assistant message carrying the partial text the prior
23
+ * model streamed. The target model continues from that trailing
24
+ * assistant message (verified empirically — Anthropic + OpenAI-compat
25
+ * both honor a trailing assistant message as a continuation prefix).
26
+ */
27
+ import type { ProviderName } from '../types.js';
28
+ import type { NeutralToolResult, Prefill, PrefillTurn } from '../turn-recorder/types.js';
29
+ export interface TranslatedToolResult {
30
+ /** Target-specific wire object the adapter appends to messages[]. */
31
+ message: unknown;
32
+ }
33
+ /** Convert a `NeutralToolResult` to the target provider's result message. */
34
+ export declare function translateToolResult(result: NeutralToolResult, target: ProviderName): TranslatedToolResult;
35
+ /**
36
+ * Build the ordered wire messages that re-present a `Prefill` to the
37
+ * target provider. The caller splices these onto the end of its
38
+ * `messages[]` (after system + user), then lets the model continue.
39
+ *
40
+ * Ordering: each recorded tool use becomes an assistant(tool_call) +
41
+ * result pair, in recorded order; finally a trailing assistant message
42
+ * carrying the partial text (only when non-empty — an empty trailing
43
+ * assistant message is rejected by some providers).
44
+ */
45
+ export declare function materializePrefill(prefill: Prefill, target: ProviderName): unknown[];
46
+ /**
47
+ * §Tier 2 — materialize a list of COMPLETED prior turns into the target
48
+ * provider's wire format, spliced BEFORE the new user message so a stateful
49
+ * (non-claude) session resume re-presents the full conversation. Each turn
50
+ * emits, in order: its opening user message (when `userPrompt` is set — the
51
+ * first turn of a phase), then one combined assistant(text+tool_calls), then
52
+ * its tool results. Empty list → [] (START phase / no prior turns).
53
+ */
54
+ export declare function materializePriorTurns(turns: PrefillTurn[], target: ProviderName): unknown[];
55
+ //# sourceMappingURL=translate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../src/prefill/translate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,WAAW,EAAkB,MAAM,2BAA2B,CAAC;AAGzG,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,OAAO,EAAE,OAAO,CAAC;CAClB;AAMD,6EAA6E;AAC7E,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,iBAAiB,EACzB,MAAM,EAAE,YAAY,GACnB,oBAAoB,CA6BtB;AA6BD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,EAAE,CAUpF;AAsCD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,EAAE,CAe3F"}
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Tool-result + prefill shape translation (v2 ADR §2.3.2 / §2.3).
3
+ *
4
+ * The durable log stores tool results in a neutral shape
5
+ * (`NeutralToolResult`). When a chain-fallback swap re-injects a prior
6
+ * model's turn into a NEW provider, the adapter materializes that turn
7
+ * into the target provider's wire format. This module owns that
8
+ * translation.
9
+ *
10
+ * - Anthropic native:
11
+ * tool_use → { role: 'assistant', content: [{ type: 'tool_use', id, name, input }] }
12
+ * result → { role: 'user', content: [{ type: 'tool_result', tool_use_id, content, is_error }] }
13
+ * - OpenAI-compat (openrouter, opencode, openai, ollama):
14
+ * tool_use → { role: 'assistant', tool_calls: [{ id, type:'function', function:{ name, arguments } }] }
15
+ * result → { role: 'tool', tool_call_id, content: stringify }
16
+ * - ADK: materialized via Session.appendEvent inside the adk adapter
17
+ * (functionCall + functionResponse events) — NOT via this module.
18
+ *
19
+ * `materializePrefill` turns a `Prefill` into the ordered list of wire
20
+ * messages to splice into the request BEFORE the continuation: for each
21
+ * recorded tool use, an assistant(tool_call) + tool(result) pair, then
22
+ * a trailing assistant message carrying the partial text the prior
23
+ * model streamed. The target model continues from that trailing
24
+ * assistant message (verified empirically — Anthropic + OpenAI-compat
25
+ * both honor a trailing assistant message as a continuation prefix).
26
+ */
27
+ import { isAnthropic } from './strip.js';
28
+ function stringifyContent(content) {
29
+ return typeof content === 'string' ? content : JSON.stringify(content ?? null);
30
+ }
31
+ /** Convert a `NeutralToolResult` to the target provider's result message. */
32
+ export function translateToolResult(result, target) {
33
+ // Defensive guard (review finding): an empty tool_use_id / tool_call_id
34
+ // makes the upstream reject with "tool_use_id does not reference a known
35
+ // tool_use". The ADK path can produce `id ?? ''`; fail loud here rather
36
+ // than ship a malformed request.
37
+ if (!result.toolUseId) {
38
+ throw new Error('translateToolResult: NeutralToolResult.toolUseId is empty — cannot bind tool result to a tool_use');
39
+ }
40
+ if (isAnthropic(target)) {
41
+ return {
42
+ message: {
43
+ role: 'user',
44
+ content: [{
45
+ type: 'tool_result',
46
+ tool_use_id: result.toolUseId,
47
+ content: stringifyContent(result.content),
48
+ is_error: !result.ok,
49
+ }],
50
+ },
51
+ };
52
+ }
53
+ // OpenAI-compatible default.
54
+ return {
55
+ message: {
56
+ role: 'tool',
57
+ tool_call_id: result.toolUseId,
58
+ content: stringifyContent(result.content),
59
+ },
60
+ };
61
+ }
62
+ /** The assistant message that re-presents a recorded tool_use to the
63
+ * target so the following tool-result message is well-formed. */
64
+ function translateToolUseAssistant(use, target) {
65
+ if (isAnthropic(target)) {
66
+ return {
67
+ role: 'assistant',
68
+ content: [{ type: 'tool_use', id: use.id, name: use.name, input: use.input ?? {} }],
69
+ };
70
+ }
71
+ return {
72
+ // Empty string, not null: an assistant message that only carries a
73
+ // tool_call has no text preamble. While most OpenAI-compat upstreams
74
+ // accept `content: null` here, `''` is universally valid and removes
75
+ // any ambiguity across the openai/openrouter/opencode/ollama set.
76
+ role: 'assistant',
77
+ content: '',
78
+ tool_calls: [{
79
+ id: use.id,
80
+ type: 'function',
81
+ function: { name: use.name, arguments: JSON.stringify(use.input ?? {}) },
82
+ }],
83
+ };
84
+ }
85
+ /**
86
+ * Build the ordered wire messages that re-present a `Prefill` to the
87
+ * target provider. The caller splices these onto the end of its
88
+ * `messages[]` (after system + user), then lets the model continue.
89
+ *
90
+ * Ordering: each recorded tool use becomes an assistant(tool_call) +
91
+ * result pair, in recorded order; finally a trailing assistant message
92
+ * carrying the partial text (only when non-empty — an empty trailing
93
+ * assistant message is rejected by some providers).
94
+ */
95
+ export function materializePrefill(prefill, target) {
96
+ const out = [];
97
+ for (const use of prefill.toolUses) {
98
+ out.push(translateToolUseAssistant({ id: use.id, name: use.name, input: use.input }, target));
99
+ out.push(translateToolResult(use.result, target).message);
100
+ }
101
+ if (prefill.text.length > 0) {
102
+ out.push({ role: 'assistant', content: prefill.text });
103
+ }
104
+ return out;
105
+ }
106
+ /**
107
+ * One COMPLETED prior turn as a single assistant message (text + tool_calls
108
+ * combined) followed by its tool-result messages. UNLIKE `materializePrefill`
109
+ * — which keeps the partial text as a separate TRAILING assistant message so
110
+ * the live model continues from it — a completed turn's text and tool calls
111
+ * belong to one wire message. Emitting them split would put a phase's
112
+ * tool-loop turns back-to-back as consecutive assistant messages (no
113
+ * intervening tool/user), which OpenAI-compat upstreams reject or silently
114
+ * merge. The combined shape reproduces the exact tool-loop wire sequence:
115
+ * assistant(text,tool_calls) → tool(result)* → (next turn).
116
+ */
117
+ function combinedAssistantMessage(text, toolUses, target) {
118
+ if (isAnthropic(target)) {
119
+ const content = [];
120
+ if (text.length > 0)
121
+ content.push({ type: 'text', text });
122
+ for (const u of toolUses) {
123
+ content.push({ type: 'tool_use', id: u.id, name: u.name, input: u.input ?? {} });
124
+ }
125
+ return { role: 'assistant', content };
126
+ }
127
+ // OpenAI-compat: text + tool_calls live on ONE assistant message.
128
+ const msg = { role: 'assistant', content: text };
129
+ if (toolUses.length > 0) {
130
+ msg.tool_calls = toolUses.map((u) => ({
131
+ id: u.id,
132
+ type: 'function',
133
+ function: { name: u.name, arguments: JSON.stringify(u.input ?? {}) },
134
+ }));
135
+ }
136
+ return msg;
137
+ }
138
+ /**
139
+ * §Tier 2 — materialize a list of COMPLETED prior turns into the target
140
+ * provider's wire format, spliced BEFORE the new user message so a stateful
141
+ * (non-claude) session resume re-presents the full conversation. Each turn
142
+ * emits, in order: its opening user message (when `userPrompt` is set — the
143
+ * first turn of a phase), then one combined assistant(text+tool_calls), then
144
+ * its tool results. Empty list → [] (START phase / no prior turns).
145
+ */
146
+ export function materializePriorTurns(turns, target) {
147
+ const out = [];
148
+ for (const turn of turns) {
149
+ if (typeof turn.userPrompt === 'string' && turn.userPrompt.length > 0) {
150
+ out.push({ role: 'user', content: turn.userPrompt });
151
+ }
152
+ // A completed turn always carries text and/or tools; guard the empty case.
153
+ if (turn.text.length > 0 || turn.toolUses.length > 0) {
154
+ out.push(combinedAssistantMessage(turn.text, turn.toolUses, target));
155
+ }
156
+ for (const use of turn.toolUses) {
157
+ out.push(translateToolResult(use.result, target).message);
158
+ }
159
+ }
160
+ return out;
161
+ }
162
+ //# sourceMappingURL=translate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translate.js","sourceRoot":"","sources":["../../src/prefill/translate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * §2.3.3 context-window truncation policy.
3
+ *
4
+ * `prefill.text` (the partial assistant continuation) plus the recorded
5
+ * tool history may push the resuming request over the TARGET model's
6
+ * input window even though it fit on the source model. Before the chain
7
+ * walker hands a prefill to the next attempt, it runs this gate:
8
+ *
9
+ * budget = target.maxInputTokens − margin(8K) − baseTokens
10
+ * − sourceTokens − sum(toolUseTokens)
11
+ *
12
+ * 1. If the partial text alone won't fit (budget-before-tools < 0),
13
+ * the prefill CANNOT be served → return undefined. The walker then
14
+ * retries the next model WITHOUT a prefill (a clean re-run beats a
15
+ * failed run, per §2.3.3 step 3).
16
+ * 2. Otherwise drop the OLDEST tool_use/result pairs (from the start)
17
+ * until the tool history fits, preserving the most-recent tools and
18
+ * the trailing partial text. Those dropped tools already executed on
19
+ * the source model; the resuming model simply won't see them in its
20
+ * re-presented history.
21
+ *
22
+ * `maxInputTokens` comes from `cost.ts`'s model-prices table; a model
23
+ * missing from the table conservatively assumes 32K (§2.3.3 step 4).
24
+ *
25
+ * Token counts are estimates (default ~chars/4) — exact tokenization is
26
+ * deferred (§ ADR open question). The 8K margin absorbs the slack; if the
27
+ * estimate is too aggressive the walker just drops an extra tool pair,
28
+ * which is a slower chain walk, not a correctness loss.
29
+ */
30
+ import type { Prefill } from '../turn-recorder/types.js';
31
+ /** Conservative floor when the model is absent from the price table. */
32
+ export declare const DEFAULT_MAX_INPUT_TOKENS = 32000;
33
+ /** Headroom reserved below the model's max input window. */
34
+ export declare const DEFAULT_MARGIN_TOKENS = 8000;
35
+ export interface TruncatePrefillArgs {
36
+ prefill: Prefill;
37
+ /** Model id the prefill is about to be served to. */
38
+ targetModel: string;
39
+ /**
40
+ * Tokens already committed to the request before the prefill is
41
+ * spliced (system prompt + the user turn). Best-effort; default 0.
42
+ * Counted against the budget so a large system prompt + big prefill
43
+ * don't silently blow the window.
44
+ */
45
+ baseTokens?: number;
46
+ /** Headroom under the model's max input window. Default 8000. */
47
+ marginTokens?: number;
48
+ /** Estimator over an arbitrary string. Default ≈ chars/4. */
49
+ estimateTokens?: (text: string) => number;
50
+ /**
51
+ * Override the target's max-input-token lookup (tests / models the
52
+ * cost table can't price). Default reads `cost.ts`.
53
+ */
54
+ maxInputTokensFor?: (model: string) => number | undefined;
55
+ }
56
+ /**
57
+ * Returns the (possibly tool-trimmed) prefill that fits the target
58
+ * model's input budget, or `undefined` when even the bare partial text
59
+ * won't fit — signalling the walker to retry without a prefill.
60
+ */
61
+ export declare function truncatePrefillForBudget(args: TruncatePrefillArgs): Prefill | undefined;
62
+ //# sourceMappingURL=truncate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../../src/prefill/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAEzD,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAC/C,4DAA4D;AAC5D,eAAO,MAAM,qBAAqB,OAAQ,CAAC;AAE3C,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1C;;;OAGG;IACH,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CAC3D;AAeD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,GAAG,SAAS,CAiCvF"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * §2.3.3 context-window truncation policy.
3
+ *
4
+ * `prefill.text` (the partial assistant continuation) plus the recorded
5
+ * tool history may push the resuming request over the TARGET model's
6
+ * input window even though it fit on the source model. Before the chain
7
+ * walker hands a prefill to the next attempt, it runs this gate:
8
+ *
9
+ * budget = target.maxInputTokens − margin(8K) − baseTokens
10
+ * − sourceTokens − sum(toolUseTokens)
11
+ *
12
+ * 1. If the partial text alone won't fit (budget-before-tools < 0),
13
+ * the prefill CANNOT be served → return undefined. The walker then
14
+ * retries the next model WITHOUT a prefill (a clean re-run beats a
15
+ * failed run, per §2.3.3 step 3).
16
+ * 2. Otherwise drop the OLDEST tool_use/result pairs (from the start)
17
+ * until the tool history fits, preserving the most-recent tools and
18
+ * the trailing partial text. Those dropped tools already executed on
19
+ * the source model; the resuming model simply won't see them in its
20
+ * re-presented history.
21
+ *
22
+ * `maxInputTokens` comes from `cost.ts`'s model-prices table; a model
23
+ * missing from the table conservatively assumes 32K (§2.3.3 step 4).
24
+ *
25
+ * Token counts are estimates (default ~chars/4) — exact tokenization is
26
+ * deferred (§ ADR open question). The 8K margin absorbs the slack; if the
27
+ * estimate is too aggressive the walker just drops an extra tool pair,
28
+ * which is a slower chain walk, not a correctness loss.
29
+ */
30
+ import { getDetailedPricing } from '../cost.js';
31
+ /** Conservative floor when the model is absent from the price table. */
32
+ export const DEFAULT_MAX_INPUT_TOKENS = 32_000;
33
+ /** Headroom reserved below the model's max input window. */
34
+ export const DEFAULT_MARGIN_TOKENS = 8_000;
35
+ const defaultEstimate = (text) => Math.ceil(text.length / 4);
36
+ /** Rough token cost of re-presenting one recorded tool_use/result pair. */
37
+ function toolUseTokens(tu, estimate) {
38
+ const content = typeof tu.result?.content === 'string'
39
+ ? tu.result.content
40
+ : JSON.stringify(tu.result?.content ?? '');
41
+ return estimate(`${tu.name}${JSON.stringify(tu.input ?? {})}${content}`);
42
+ }
43
+ /**
44
+ * Returns the (possibly tool-trimmed) prefill that fits the target
45
+ * model's input budget, or `undefined` when even the bare partial text
46
+ * won't fit — signalling the walker to retry without a prefill.
47
+ */
48
+ export function truncatePrefillForBudget(args) {
49
+ const estimate = args.estimateTokens ?? defaultEstimate;
50
+ const margin = args.marginTokens ?? DEFAULT_MARGIN_TOKENS;
51
+ const baseTokens = args.baseTokens ?? 0;
52
+ const maxInput = (args.maxInputTokensFor
53
+ ? args.maxInputTokensFor(args.targetModel)
54
+ : getDetailedPricing(args.targetModel)?.maxInputTokens)
55
+ ?? DEFAULT_MAX_INPUT_TOKENS;
56
+ // Budget remaining for the tool history AFTER the fixed costs (system +
57
+ // user + margin + the partial-text reinjection). If this is already
58
+ // negative the partial alone overflows the window → cannot serve.
59
+ const budgetForTools = maxInput - margin - baseTokens - args.prefill.sourceTokens;
60
+ if (budgetForTools < 0)
61
+ return undefined;
62
+ // Per-tool token estimates, oldest-first (prefill.toolUses preserves
63
+ // recording order). Drop from the FRONT until the sum fits.
64
+ const tools = args.prefill.toolUses.slice();
65
+ let toolSum = tools.reduce((acc, tu) => acc + toolUseTokens(tu, estimate), 0);
66
+ if (toolSum <= budgetForTools) {
67
+ return args.prefill; // fits as-is
68
+ }
69
+ while (tools.length > 0 && toolSum > budgetForTools) {
70
+ const dropped = tools.shift();
71
+ toolSum -= toolUseTokens(dropped, estimate);
72
+ }
73
+ // budgetForTools >= 0 guarantees the empty-tools case fits; return the
74
+ // trimmed prefill (which may have zero tools but always keeps the text).
75
+ return { ...args.prefill, toolUses: tools };
76
+ }
77
+ //# sourceMappingURL=truncate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"truncate.js","sourceRoot":"","sources":["../../src/prefill/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGhD,wEAAwE;AACxE,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC;AAC/C,4DAA4D;AAC5D,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,CAAC;AAwB3C,MAAM,eAAe,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAE7E,2EAA2E;AAC3E,SAAS,aAAa,CACpB,EAA+B,EAC/B,QAA+B;IAE/B,MAAM,OAAO,GAAG,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,QAAQ;QACpD,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,OAAO;QACnB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC;IAC7C,OAAO,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,CAAC;AAC3E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAyB;IAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,IAAI,eAAe,CAAC;IACxD,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,IAAI,qBAAqB,CAAC;IAC1D,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;IAExC,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,iBAAiB;QACtC,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC;QAC1C,CAAC,CAAC,kBAAkB,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,cAAc,CAAC;WACpD,wBAAwB,CAAC;IAE9B,wEAAwE;IACxE,oEAAoE;IACpE,kEAAkE;IAClE,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;IAClF,IAAI,cAAc,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAEzC,qEAAqE;IACrE,4DAA4D;IAC5D,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IAC5C,IAAI,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,GAAG,aAAa,CAAC,EAAE,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IAE9E,IAAI,OAAO,IAAI,cAAc,EAAE,CAAC;QAC9B,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,aAAa;IACpC,CAAC;IAED,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,GAAG,cAAc,EAAE,CAAC;QACpD,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,EAAG,CAAC;QAC/B,OAAO,IAAI,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED,uEAAuE;IACvE,yEAAyE;IACzE,OAAO,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC9C,CAAC"}
@@ -19,18 +19,35 @@ export declare class RouterError extends Error {
19
19
  });
20
20
  }
21
21
  /**
22
- * Map a raw error to an `ErrorClass`.
22
+ * Map a raw error to an `ErrorClass`. This is the single source of truth for
23
+ * error classification across the whole stack — `LlmRouter.invoke` (single
24
+ * shot), `LlmRouter.invokeStream` (streaming), and `LlmRouter.runAgent`
25
+ * (agentic chain walk) all route through here.
23
26
  *
24
27
  * Recognizes:
25
- * - 429 → rate_limit
26
- * - 5xx → server_5xx
27
- * - 401/403 → auth
28
- * - 400 + content/safety hints → content_policy
29
- * - 400 → invalid_request
30
- * - timeout/abort/etimedout/econnreset/socket-hang-up → timeout
28
+ * - network (status 0 / fetch failed / ECONNRESET / EPIPE / abort / timeout) → timeout
29
+ * - 429 → rate_limit; 5xx → server_5xx; 401/403 → auth
30
+ * - 400 + content/safety hints → content_policy; other 400 → invalid_request
31
+ * - body: phantom-model / hard-billing → model_unavailable
32
+ * - body: quota / rate-limit / overloaded / resource-exhausted → rate_limit
33
+ * - body: auth/permission → auth
34
+ * - explicit `.retryable === true` with no better signal → timeout (fall-back eligible)
31
35
  * - otherwise → unknown
32
36
  */
33
37
  export declare function classifyError(err: unknown): ErrorClass;
38
+ /**
39
+ * Terminal classes never trigger cross-model fallback (a different model from
40
+ * the same wall won't help) and never count toward the circuit breaker.
41
+ */
42
+ export declare function isTerminalErrorClass(cls: ErrorClass): boolean;
43
+ /**
44
+ * Fall-back-eligible classes: a transient/provider-side failure where trying a
45
+ * DIFFERENT model in the chain is the right move. This is the unified
46
+ * replacement for the former binary `isRetryableUpstreamError` duck-type. Note
47
+ * `unknown` is intentionally excluded — a generic error is more likely a bug
48
+ * than a provider fault, so it should surface rather than burn the chain.
49
+ */
50
+ export declare function isFallbackEligibleErrorClass(cls: ErrorClass): boolean;
34
51
  /**
35
52
  * Parse a `Retry-After` header value (seconds-since-now or HTTP-date)
36
53
  * into a millisecond delay. Returns `null` if absent or unparseable.
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/router/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE3D;;;GAGG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;gBAGrB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;QAAE,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAOjE;AAcD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,CAkDtD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,GAAG,IAAI,CAY7F"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/router/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE3D;;;GAGG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;gBAGrB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;QAAE,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAOjE;AAkCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,CAqEtD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAE7D;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAKrE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,GAAG,IAAI,CAY7F"}