cartethyia 2.7.6 → 2.7.8

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 (193) hide show
  1. package/.env.example +4 -2
  2. package/README.md +26 -2
  3. package/dist/dashboard/assets/{BannedPage-B1N603cM.js → BannedPage-DeIbIiBU.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-9q4_4NoK.js → CliToolDetailPage-BRWKPQPf.js} +2 -2
  6. package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-DwshGtK-.js → CliToolsPage-BP-Y5iHN.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-D762MIkp.js → CombosPage-B7JHozVa.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-KRYjHZsX.js → ConsoleLogPage-GX7iGGcu.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-d5v3K_Yc.js → InjectSystemPromptPanel-Cv-2Ol_K.js} +2 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-Ccv4tjNS.js → LoginPage-bKfz_b1K.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-C6WlpU5E.js → ModelPicker-B-WQxx_H.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
  23. package/dist/dashboard/assets/{OverviewPage-DN9fYNWA.js → OverviewPage-Z9pJZFo8.js} +1 -1
  24. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +7 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
  27. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +1 -0
  28. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
  29. package/dist/dashboard/assets/{ProxyPage-DxjvUzSD.js → ProxyPage-emlTuxz6.js} +1 -1
  30. package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
  31. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js +1 -0
  32. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
  33. package/dist/dashboard/assets/{SettingsPage-BW4AKj2H.js → SettingsPage-BSy7EwGo.js} +1 -1
  34. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
  35. package/dist/dashboard/assets/{SetupPage-44BZrxFC.js → SetupPage-CUZrKs9r.js} +1 -1
  36. package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-DRPavCdU.js → StudioPage-jWmUdDpy.js} +2 -2
  38. package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
  39. package/dist/dashboard/assets/{UsagePage-B7M7LvMM.js → UsagePage-5F9tGVGG.js} +2 -2
  40. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
  41. package/dist/dashboard/assets/app-BEsU8hKs.css +2 -0
  42. package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
  43. package/dist/dashboard/assets/{badge-BaX2GTrn.js → badge-DyI_mJfi.js} +1 -1
  44. package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
  45. package/dist/dashboard/assets/{banner-DFH4Bkte.js → banner-B-yXtCqb.js} +1 -1
  46. package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
  47. package/dist/dashboard/assets/{card-CxDw0lK6.js → card-_HwAEqFF.js} +1 -1
  48. package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
  49. package/dist/dashboard/assets/{clipboard-button-DfS1ntiq.js → clipboard-button-ByjCk0o9.js} +1 -1
  50. package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
  51. package/dist/dashboard/assets/{dialog-BK-NRr5u.js → dialog-BZhOlfz-.js} +1 -1
  52. package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
  53. package/dist/dashboard/assets/{drawer-QqPul6sC.js → drawer-BADqIRYP.js} +1 -1
  54. package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
  55. package/dist/dashboard/assets/{entry-BMlYJVFj.js → entry-COaC-irN.js} +1 -1
  56. package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
  57. package/dist/dashboard/assets/{entry-D6WvojeD.js → entry-Ca7LSCet.js} +3 -3
  58. package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
  59. package/dist/dashboard/assets/{entry-Dmx7gBiN.js → entry-PDd27uCd.js} +1 -1
  60. package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
  61. package/dist/dashboard/assets/formatters-DcHJyRiu.js +1 -0
  62. package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
  63. package/dist/dashboard/assets/{index-Ko4dLRnV.js → index-CgKmFngv.js} +2 -2
  64. package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
  65. package/dist/dashboard/assets/{inline-CHJQwxLa.js → inline-B2BirKry.js} +1 -1
  66. package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
  67. package/dist/dashboard/assets/{input-BlmlizVV.js → input-D-qV9dKY.js} +1 -1
  68. package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
  69. package/dist/dashboard/assets/{layout-Bz93I-yE.js → layout-B7tVWhtV.js} +1 -1
  70. package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
  71. package/dist/dashboard/assets/{list-row-Ml16KCfu.js → list-row-CtkGE3bF.js} +1 -1
  72. package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
  73. package/dist/dashboard/assets/{live-7_QpnYGu.js → live-DGizF06-.js} +1 -1
  74. package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
  75. package/dist/dashboard/assets/{network-CuMDaTzT.js → network-C9UE-bOr.js} +1 -1
  76. package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
  77. package/dist/dashboard/assets/{numbers-CoqpaDMx.js → numbers-BnAd22a_.js} +1 -1
  78. package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
  79. package/dist/dashboard/assets/{pagination-B5aOoweZ.js → pagination-BAywyMzS.js} +1 -1
  80. package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
  81. package/dist/dashboard/assets/{select-BHqpd-bg.js → select-BCOcv8WK.js} +1 -1
  82. package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
  83. package/dist/dashboard/assets/{stack-j7cXjgYX.js → stack-ByKt3Q1g.js} +1 -1
  84. package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
  85. package/dist/dashboard/assets/{state-DHmEnLa2.js → state-DXbGTx7N.js} +1 -1
  86. package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
  87. package/dist/dashboard/assets/{switch-DmZcyTIC.js → switch-ZeTsW623.js} +1 -1
  88. package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
  89. package/dist/dashboard/index.html +2 -2
  90. package/dist/dashboard/index.html.gz +0 -0
  91. package/dist/dashboard/precompressed-assets.json +44 -44
  92. package/migrations/full-postgres/0043_add_provider_custom_user_agent.sql +14 -0
  93. package/migrations/lite-sqlite3/0043_add_provider_custom_user_agent.sql +10 -0
  94. package/package.json +3 -1
  95. package/src/config/security.ts +1 -1
  96. package/src/config/spec.ts +13 -6
  97. package/src/console/cli-tools/cartethyia-websearch-plugin.ts +186 -0
  98. package/src/console/cli-tools/injectors/opencode.ts +42 -14
  99. package/src/console/providers/catalog/contracts.ts +4 -0
  100. package/src/console/providers/catalog/routes.ts +1 -0
  101. package/src/console/providers/detail/routes.ts +8 -0
  102. package/src/console/providers/detail/store.ts +4 -0
  103. package/src/console/quota/account-quota-tenant.ts +74 -0
  104. package/src/observability/telemetry-buffer.ts +2 -1
  105. package/src/persistence/boot.ts +13 -4
  106. package/src/persistence/schema.ts +7 -1
  107. package/src/protocol/canonical-model.ts +7 -0
  108. package/src/protocol/request/gemini.ts +20 -1
  109. package/src/protocol/tool-identity.ts +15 -1
  110. package/src/providers/default-registry.ts +8 -2
  111. package/src/providers/discovery/discovery-types.ts +18 -0
  112. package/src/providers/discovery/probe-phases.ts +45 -0
  113. package/src/providers/discovery/probing-service.ts +41 -6
  114. package/src/providers/integrations/antigravity/antigravity-protocol.ts +20 -2
  115. package/src/providers/integrations/antigravity/antigravity.ts +24 -0
  116. package/src/providers/integrations/codex/codex.ts +28 -1
  117. package/src/providers/integrations/qoder/chat-proto.ts +328 -0
  118. package/src/providers/integrations/qoder/claim-qoder-credits.ts +258 -0
  119. package/src/providers/integrations/qoder/qoder-adapter.ts +275 -0
  120. package/src/providers/integrations/qoder/qoder-auth.ts +201 -0
  121. package/src/providers/integrations/qoder/qoder-body.ts +78 -0
  122. package/src/providers/integrations/qoder/qoder-catalog.ts +5 -1
  123. package/src/providers/integrations/qoder/qoder-chat.ts +514 -0
  124. package/src/providers/integrations/qoder/qoder-cosy.ts +162 -0
  125. package/src/providers/integrations/qoder/qoder-grpc.ts +667 -0
  126. package/src/providers/integrations/qoder/qoder-ide.ts +584 -0
  127. package/src/providers/integrations/qoder/qoder-oauth.ts +4 -4
  128. package/src/providers/integrations/qoder/qoder-quota.ts +1 -1
  129. package/src/providers/integrations/qoder/qoder-realm.ts +40 -4
  130. package/src/providers/integrations/qoder/qoder-types.ts +51 -0
  131. package/src/providers/integrations/qoder/qoder-umid.ts +123 -8
  132. package/src/providers/integrations/qoder/qoder.ts +147 -1889
  133. package/src/providers/operations/byok-wire-profile.ts +9 -12
  134. package/src/providers/provider-contracts.ts +7 -0
  135. package/src/providers/provider-usage.ts +9 -6
  136. package/src/security/abuse.ts +5 -5
  137. package/src/transport/dispatch/upstream.ts +4 -2
  138. package/src/transport/routing/route-catalog.ts +8 -0
  139. package/src/transport/routing/route-model.ts +4 -0
  140. package/src/transport/routing/router.ts +8 -117
  141. package/dist/dashboard/assets/BannedPage-B1N603cM.js.gz +0 -0
  142. package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js.gz +0 -0
  143. package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js.gz +0 -0
  144. package/dist/dashboard/assets/CombosPage-D762MIkp.js.gz +0 -0
  145. package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js.gz +0 -0
  146. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js +0 -1
  147. package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js.gz +0 -0
  148. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js +0 -1
  149. package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js.gz +0 -0
  150. package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js.gz +0 -0
  151. package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js.gz +0 -0
  152. package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
  153. package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js.gz +0 -0
  154. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js +0 -5
  155. package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js.gz +0 -0
  156. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js +0 -1
  157. package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js.gz +0 -0
  158. package/dist/dashboard/assets/ProxyPage-DxjvUzSD.js.gz +0 -0
  159. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js +0 -1
  160. package/dist/dashboard/assets/QuotaPage-OaHScjpX.js.gz +0 -0
  161. package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js.gz +0 -0
  162. package/dist/dashboard/assets/SetupPage-44BZrxFC.js.gz +0 -0
  163. package/dist/dashboard/assets/StudioPage-DRPavCdU.js.gz +0 -0
  164. package/dist/dashboard/assets/UsagePage-B7M7LvMM.js.gz +0 -0
  165. package/dist/dashboard/assets/app-mweRcsKp.css +0 -2
  166. package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
  167. package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
  168. package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
  169. package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
  170. package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
  171. package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
  172. package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
  173. package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
  174. package/dist/dashboard/assets/entry-D6WvojeD.js.gz +0 -0
  175. package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
  176. package/dist/dashboard/assets/formatters-u7JPB5BX.js +0 -1
  177. package/dist/dashboard/assets/formatters-u7JPB5BX.js.gz +0 -0
  178. package/dist/dashboard/assets/index-Ko4dLRnV.js.gz +0 -0
  179. package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
  180. package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
  181. package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
  182. package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
  183. package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
  184. package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
  185. package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
  186. package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
  187. package/dist/dashboard/assets/select-BHqpd-bg.js.gz +0 -0
  188. package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
  189. package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
  190. package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
  191. package/src/providers/integrations/qoder/qoder-text-tools.ts +0 -277
  192. /package/dist/dashboard/assets/{app-DRnQLhsc.js → app-CTxoqyYB.js} +0 -0
  193. /package/dist/dashboard/assets/{app-DRnQLhsc.js.gz → app-CTxoqyYB.js.gz} +0 -0
@@ -1,277 +0,0 @@
1
- /**
2
- * Recovery for Qoder's INTL text-mode tool-call echo.
3
- *
4
- * Qoder's own wire is structured `tool_calls` deltas, but the DeepSeek model
5
- * sometimes *also* writes a tool call as prose with the marker:
6
- *
7
- * assistant 请求调用工具
8
- * {"name":"write","arguments":"…"},{"name":"write","arguments":"…"}
9
- *
10
- * Left alone the JSON reaches the caller as visible content while nothing runs.
11
- * This module owns the single tolerant parser for that echo family (JSON
12
- * array/object, bare comma-separated list, unescaped inner quotes). It does
13
- * NOT parse DSML/XML markup — that family is gone; only the marker+JSON echo
14
- * the model actually emits is recovered here.
15
- */
16
- import { randomUUID } from "node:crypto";
17
-
18
- /** One recovered call, in the shape the canonical events want. */
19
- export interface QoderEchoCall {
20
- readonly id: string;
21
- readonly name: string;
22
- readonly arguments: string;
23
- }
24
-
25
- /** Any spelling of the INTL tool-call leak marker. */
26
- export const LEAK_MARKER_ANY_RE = /[\[<(]*\s*assistant\s*请求调用工具\s*[\]>)]?/;
27
-
28
- /** Canonical marker literals used to seed prefix matching. */
29
- export const LEAK_MARKER_LITERALS: readonly string[] = [
30
- "[assistant 请求调用工具]",
31
- "<assistant 请求调用工具>",
32
- "assistant 请求调用工具",
33
- ];
34
-
35
- /**
36
- * Escapes literal control characters that occur inside JSON string values.
37
- * DeepSeek often writes multi-line argument content as real newlines instead
38
- * of `\n` escapes; only characters inside strings are escaped.
39
- */
40
- function escapeControlInsideStrings(text: string): string {
41
- let out = "";
42
- let inString = false;
43
- for (let i = 0; i < text.length; i += 1) {
44
- const ch = text[i] ?? "";
45
- if (inString) {
46
- if (ch === "\\") {
47
- out += ch;
48
- i += 1;
49
- if (i < text.length) out += text[i];
50
- continue;
51
- }
52
- if (ch === '"') {
53
- inString = false;
54
- out += ch;
55
- continue;
56
- }
57
- if (ch === "\n") {
58
- out += "\\n";
59
- continue;
60
- }
61
- if (ch === "\r") {
62
- out += "\\r";
63
- continue;
64
- }
65
- if (ch === "\t") {
66
- out += "\\t";
67
- continue;
68
- }
69
- if (ch.charCodeAt(0) < 0x20) {
70
- out += "\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0");
71
- continue;
72
- }
73
- out += ch;
74
- continue;
75
- }
76
- if (ch === '"') inString = true;
77
- out += ch;
78
- }
79
- return out;
80
- }
81
-
82
- /** Escapes bare quotes that belong inside a JSON string value. */
83
- function repairJsonQuotes(text: string): string {
84
- const ph = "\u0000";
85
- return text.replace(/\\"/g, ph).replace(/"/g, '\\"').replaceAll(ph, '\\"');
86
- }
87
-
88
- /**
89
- * Rebuilds the leaked echo when strict parsing fails: inner quotes unescaped
90
- * (`[{"name":"read","arguments":"{"path":"a"}"},…]`) or a bare comma-separated
91
- * object list. Scans top-level objects without relying on JSON validity.
92
- * Returns null when the text is not that shape at all.
93
- */
94
- function salvageEchoArray(rest: string): unknown {
95
- const src = rest.trim();
96
- if (src.length === 0) return null;
97
- const calls: Array<{ name: string; arguments: string }> = [];
98
- let i = 0;
99
- const bracketed = src[0] === "[";
100
- if (bracketed) i = 1;
101
- const skipWs = (): void => {
102
- while (
103
- i < src.length &&
104
- (src[i] === " " || src[i] === "\t" || src[i] === "\n" || src[i] === "\r")
105
- ) {
106
- i += 1;
107
- }
108
- };
109
- const NAME_RE = /"name"\s*:\s*"([^"]{1,200})"/y;
110
- const ARG_KEY_RE = /"arguments"\s*:\s*"/y;
111
-
112
- for (;;) {
113
- skipWs();
114
- if (i >= src.length) break;
115
- if (bracketed && src[i] === "]") break;
116
- if (src[i] !== "{") return null;
117
- i += 1;
118
- skipWs();
119
- NAME_RE.lastIndex = i;
120
- const nameMatch = NAME_RE.exec(src);
121
- if (!nameMatch) return null;
122
- const name = nameMatch[1] ?? "";
123
- i = NAME_RE.lastIndex;
124
- skipWs();
125
- if (i < src.length && src[i] === ",") {
126
- i += 1;
127
- skipWs();
128
- }
129
- ARG_KEY_RE.lastIndex = i;
130
- if (!ARG_KEY_RE.exec(src)) return null;
131
- const argStart = ARG_KEY_RE.lastIndex;
132
- let rawEnd = -1;
133
- for (let j = argStart; j + 1 < src.length; j += 1) {
134
- if (src[j] === "}" && src[j + 1] === '"') {
135
- rawEnd = j;
136
- break;
137
- }
138
- }
139
- if (rawEnd < 0) return null;
140
- const raw = src.slice(argStart, rawEnd + 1);
141
- const parsedArgs = [
142
- raw,
143
- escapeControlInsideStrings(raw),
144
- repairJsonQuotes(raw),
145
- escapeControlInsideStrings(repairJsonQuotes(raw)),
146
- ].find((candidate) => {
147
- try {
148
- JSON.parse(candidate);
149
- return true;
150
- } catch {
151
- return false;
152
- }
153
- });
154
- if (parsedArgs === undefined) return null;
155
- calls.push({ name, arguments: parsedArgs });
156
- i = rawEnd + 2;
157
- skipWs();
158
- if (i >= src.length || src[i] !== "}") return null;
159
- i += 1;
160
- skipWs();
161
- if (i < src.length && src[i] === ",") {
162
- i += 1;
163
- continue;
164
- }
165
- if (i < src.length && bracketed && src[i] === "]") continue;
166
- if (i < src.length) return null;
167
- }
168
- return calls.length > 0 ? calls : null;
169
- }
170
-
171
- /**
172
- * True when the text is clearly an attempted tool-protocol echo rather than
173
- * ordinary prose: a leak marker followed by the start of a JSON array/object,
174
- * or of a tool-style tag (`<tool_call>`, `<execute …/>`, angle-wrapped
175
- * `<{…}>`). Used by the fail-closed decision: an echo that cannot be recovered
176
- * fails the turn instead of surfacing protocol text as visible content.
177
- */
178
- export function isQoderEchoAttempt(text: string): boolean {
179
- const fence = text.match(/^```[a-z]*\s*\n?/i);
180
- const src = fence && fence[0] ? text.slice(fence[0].length) : text;
181
- const marker = LEAK_MARKER_ANY_RE.exec(src);
182
- if (!marker) return false;
183
- const rest = src.slice(marker.index + marker[0].length).trim();
184
- return (
185
- rest.startsWith("[") ||
186
- rest.startsWith("{") ||
187
- /^<[a-zA-Z0-9_.:-]+\b/.test(rest) ||
188
- (rest.startsWith("<[") || rest.startsWith("<{"))
189
- );
190
- }
191
-
192
- /**
193
- * Recovers tool calls the model echoed back as text. Only a well-formed echo
194
- * whose every entry names a tool and carries parseable arguments is accepted;
195
- * anything else returns no calls and the original text.
196
- */
197
- export function parseQoderTextToolCalls(
198
- text: string,
199
- allowedNames?: ReadonlySet<string>,
200
- ): { calls: QoderEchoCall[]; clean: string } {
201
- if (text.length === 0) {
202
- return { calls: [], clean: text };
203
- }
204
- const fence = text.match(/^```[a-z]*\s*\n?/i);
205
- const src = fence && fence[0] ? text.slice(fence[0].length) : text;
206
- const marker = LEAK_MARKER_ANY_RE.exec(src);
207
- if (!marker) {
208
- return { calls: [], clean: text };
209
- }
210
- const head = src.slice(0, marker.index).trim();
211
- let rest = src.slice(marker.index + marker[0].length).trim();
212
- rest = rest.replace(/\s*```\s*$/, "").trim();
213
-
214
- let parsed: unknown;
215
- try {
216
- parsed = JSON.parse(rest);
217
- } catch {
218
- try {
219
- parsed = JSON.parse(escapeControlInsideStrings(rest));
220
- } catch {
221
- // A bare comma-separated object list (`{...},{...}`) is not valid JSON
222
- // on its own, but is exactly the echo the model emits. Wrap it in an
223
- // array before falling back to salvage.
224
- let wrapped: unknown;
225
- try {
226
- wrapped = JSON.parse(`[${rest}]`);
227
- } catch {
228
- wrapped = undefined;
229
- }
230
- const salvaged = salvageEchoArray(rest);
231
- if (salvaged === null && wrapped === undefined) {
232
- return { calls: [], clean: text };
233
- }
234
- parsed = salvaged ?? wrapped;
235
- }
236
- }
237
- if (Array.isArray(parsed) && parsed.length === 0) return { calls: [], clean: text };
238
-
239
- const items: unknown[] = Array.isArray(parsed) ? parsed : [parsed];
240
- const calls: QoderEchoCall[] = [];
241
- for (const item of items) {
242
- if (typeof item !== "object" || item === null) return { calls: [], clean: text };
243
- const rec = item as Record<string, unknown>;
244
- const name = typeof rec["name"] === "string" ? rec["name"].trim() : "";
245
- if (name.length === 0) return { calls: [], clean: text };
246
- if (allowedNames && !allowedNames.has(name)) return { calls: [], clean: text };
247
- const rawArgs = rec["arguments"];
248
- let args: string;
249
- if (typeof rawArgs === "object" && rawArgs !== null) args = JSON.stringify(rawArgs);
250
- else if (typeof rawArgs === "string") args = rawArgs;
251
- else return { calls: [], clean: text };
252
- const argsText = args.trim();
253
- if (argsText.length > 0) {
254
- let accepted = argsText;
255
- let ok = false;
256
- try {
257
- JSON.parse(accepted);
258
- ok = true;
259
- } catch {
260
- // fall through to normalization
261
- }
262
- if (!ok) {
263
- accepted = escapeControlInsideStrings(argsText);
264
- try {
265
- JSON.parse(accepted);
266
- ok = true;
267
- } catch {
268
- // still invalid
269
- }
270
- }
271
- if (!ok) return { calls: [], clean: text };
272
- args = accepted;
273
- }
274
- calls.push({ id: `call_${randomUUID().replace(/-/g, "").slice(0, 16)}`, name, arguments: args });
275
- }
276
- return { calls, clean: head };
277
- }