@inferior-ai/sdk 2.0.0-beta.3

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 (52) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE.md +37 -0
  3. package/NOTICE.md +56 -0
  4. package/README.md +454 -0
  5. package/SECURITY.md +62 -0
  6. package/STABILITY.md +95 -0
  7. package/SUPPORT.md +66 -0
  8. package/dist/__tests__/with_mock_client.test.d.ts +9 -0
  9. package/dist/__tests__/with_mock_client.test.d.ts.map +1 -0
  10. package/dist/__tests__/with_mock_client.test.js +154 -0
  11. package/dist/__tests__/with_mock_client.test.js.map +1 -0
  12. package/dist/_worthiness.d.ts +34 -0
  13. package/dist/_worthiness.d.ts.map +1 -0
  14. package/dist/_worthiness.js +201 -0
  15. package/dist/_worthiness.js.map +1 -0
  16. package/dist/client.d.ts +155 -0
  17. package/dist/client.d.ts.map +1 -0
  18. package/dist/client.js +900 -0
  19. package/dist/client.js.map +1 -0
  20. package/dist/constants.d.ts +31 -0
  21. package/dist/constants.d.ts.map +1 -0
  22. package/dist/constants.js +35 -0
  23. package/dist/constants.js.map +1 -0
  24. package/dist/errors.d.ts +71 -0
  25. package/dist/errors.d.ts.map +1 -0
  26. package/dist/errors.js +114 -0
  27. package/dist/errors.js.map +1 -0
  28. package/dist/index.d.ts +8 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +7 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/models.d.ts +603 -0
  33. package/dist/models.d.ts.map +1 -0
  34. package/dist/models.js +10 -0
  35. package/dist/models.js.map +1 -0
  36. package/dist/retry.d.ts +35 -0
  37. package/dist/retry.d.ts.map +1 -0
  38. package/dist/retry.js +83 -0
  39. package/dist/retry.js.map +1 -0
  40. package/dist/testing/index.d.ts +16 -0
  41. package/dist/testing/index.d.ts.map +1 -0
  42. package/dist/testing/index.js +15 -0
  43. package/dist/testing/index.js.map +1 -0
  44. package/dist/testing/mock-client.d.ts +102 -0
  45. package/dist/testing/mock-client.d.ts.map +1 -0
  46. package/dist/testing/mock-client.js +217 -0
  47. package/dist/testing/mock-client.js.map +1 -0
  48. package/dist/webhooks.d.ts +52 -0
  49. package/dist/webhooks.d.ts.map +1 -0
  50. package/dist/webhooks.js +90 -0
  51. package/dist/webhooks.js.map +1 -0
  52. package/package.json +61 -0
package/dist/client.js ADDED
@@ -0,0 +1,900 @@
1
+ /**
2
+ * Inferior TypeScript SDK client.
3
+ *
4
+ * Async-first HTTP client for the Inferior REST API. Mirrors the Python SDK
5
+ * exactly — same methods, same parameters, same response types.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { InferiorClient } from '@inferior-ai/sdk'
10
+ *
11
+ * const client = new InferiorClient({ apiKey: 'cw_full_...' })
12
+ * const response = await client.search('stripe webhook timeout')
13
+ * console.log(response.results[0].title)
14
+ * ```
15
+ */
16
+ import { readFile } from "node:fs/promises";
17
+ import { basename } from "node:path";
18
+ import { DEFAULT_HEADERS, HEADER_REQUEST_ID, HEADER_RESOLVED_VERSION, INFERIOR_API_VERSION, } from "./constants.js";
19
+ import { AuthenticationError, DuplicateError, ForbiddenError, InferiorError, InsufficientScopeError, NotFoundError, PoisoningDetectedError, RateLimitError, ServerError, ValidationError, } from "./errors.js";
20
+ import { DEFAULT_RETRY, withRetry } from "./retry.js";
21
+ import { previewWorthiness } from "./_worthiness.js";
22
+ import { SCHEMA_VERSION } from "./models.js";
23
+ const DEFAULT_BASE_URL = "https://api.inferior.ai";
24
+ const DEFAULT_TIMEOUT = 15_000; // ms
25
+ function detectHostOs() {
26
+ const map = { darwin: "macos", linux: "linux", win32: "windows" };
27
+ return map[process.platform] ?? process.platform;
28
+ }
29
+ async function raiseForStatus(resp) {
30
+ if (resp.ok)
31
+ return;
32
+ const status = resp.status;
33
+ const requestId = resp.headers.get(HEADER_REQUEST_ID) ?? undefined;
34
+ let body = {};
35
+ try {
36
+ body = (await resp.json());
37
+ }
38
+ catch {
39
+ /* empty */
40
+ }
41
+ const errorType = body.error ?? "";
42
+ const message = body.message ?? resp.statusText;
43
+ const details = body.details ?? {};
44
+ if (status === 401)
45
+ throw new AuthenticationError(message, status, errorType, details, requestId);
46
+ if (status === 403) {
47
+ if (errorType === "InsufficientScopeError")
48
+ throw new InsufficientScopeError(message, status, errorType, details, requestId);
49
+ throw new ForbiddenError(message, status, errorType, details, requestId);
50
+ }
51
+ if (status === 404)
52
+ throw new NotFoundError(message, status, errorType, details, requestId);
53
+ if (status === 409)
54
+ throw new DuplicateError(message, status, errorType, details, requestId);
55
+ if (status === 422) {
56
+ // Phase C: distinguish poisoning-scanner rejections from PII/quality
57
+ // so callers can catch PoisoningDetectedError specifically.
58
+ const reason = details.rejection_reason ?? "";
59
+ const gate = details.gate ?? "";
60
+ const isPoisoning = errorType.toLowerCase().includes("poisoning") ||
61
+ reason === "poisoning_detected" ||
62
+ gate === "poisoning";
63
+ if (isPoisoning) {
64
+ throw new PoisoningDetectedError(message, status, errorType, details, requestId);
65
+ }
66
+ throw new ValidationError(message, status, errorType, details, requestId);
67
+ }
68
+ if (status === 429)
69
+ throw new RateLimitError(message, status, errorType, details, requestId);
70
+ if (status >= 500)
71
+ throw new ServerError(message, status, errorType, details, requestId);
72
+ throw new InferiorError(message, status, errorType, details, requestId);
73
+ }
74
+ export class InferiorClient {
75
+ apiKey;
76
+ baseUrl;
77
+ timeout;
78
+ hostOs;
79
+ baseModel;
80
+ framework;
81
+ retry;
82
+ fetcher;
83
+ /**
84
+ * Track whether we've warned about schema-version mismatch already.
85
+ * Kept instance-local so callers using multiple clients each warn once.
86
+ */
87
+ schemaMismatchWarned = false;
88
+ /**
89
+ * Same once-per-client pattern for the request-side API version
90
+ * handshake (Phase C2). Compares the backend's resolved version
91
+ * (header `X-Inferior-Resolved-Version`) to the SDK's compiled
92
+ * `INFERIOR_API_VERSION`; warns to console.warn once on mismatch.
93
+ */
94
+ apiVersionMismatchWarned = false;
95
+ constructor(options) {
96
+ this.apiKey = options.apiKey;
97
+ this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
98
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
99
+ this.hostOs = detectHostOs();
100
+ this.baseModel = options.baseModel;
101
+ this.framework = options.framework;
102
+ this.retry = options.retry === false ? undefined : { ...DEFAULT_RETRY, ...(options.retry ?? {}) };
103
+ this.fetcher = options.fetch ?? fetch;
104
+ }
105
+ /**
106
+ * Phase G: emit a console.warn when response.schema_version's major
107
+ * component differs from ``SCHEMA_VERSION``. Missing schema_version is
108
+ * treated as "unknown" and silently ignored. Only warns once per client.
109
+ */
110
+ checkSchemaVersion(data) {
111
+ if (this.schemaMismatchWarned)
112
+ return;
113
+ const backendVersion = data?.schema_version;
114
+ if (typeof backendVersion !== "string" || !backendVersion)
115
+ return;
116
+ const backendMajor = Number.parseInt(backendVersion.split(".", 1)[0] ?? "", 10);
117
+ const sdkMajor = Number.parseInt(SCHEMA_VERSION.split(".", 1)[0] ?? "", 10);
118
+ if (Number.isNaN(backendMajor) || Number.isNaN(sdkMajor))
119
+ return;
120
+ if (backendMajor !== sdkMajor) {
121
+ this.schemaMismatchWarned = true;
122
+ // eslint-disable-next-line no-console
123
+ console.warn(`[inferior-sdk] Backend schema_version=${JSON.stringify(backendVersion)} ` +
124
+ `differs from SDK SCHEMA_VERSION=${JSON.stringify(SCHEMA_VERSION)}. ` +
125
+ `Upgrade the SDK to match the backend major version.`);
126
+ }
127
+ }
128
+ async request(method, path, options) {
129
+ let url = `${this.baseUrl}${path}`;
130
+ if (options?.params) {
131
+ const qs = new URLSearchParams(options.params).toString();
132
+ url += `?${qs}`;
133
+ }
134
+ const headers = {
135
+ ...DEFAULT_HEADERS,
136
+ Authorization: `Bearer ${this.apiKey}`,
137
+ "Content-Type": "application/json",
138
+ ...(options?.headers ?? {}),
139
+ };
140
+ const doFetch = async () => {
141
+ const controller = new AbortController();
142
+ const timer = setTimeout(() => controller.abort(), this.timeout);
143
+ try {
144
+ return await this.fetcher(url, {
145
+ method,
146
+ headers,
147
+ body: options?.body ? JSON.stringify(options.body) : undefined,
148
+ signal: controller.signal,
149
+ });
150
+ }
151
+ finally {
152
+ clearTimeout(timer);
153
+ }
154
+ };
155
+ const resp = !this.retry ? await doFetch() : await withRetry(this.retry, method, headers, doFetch);
156
+ this.checkApiVersion(resp);
157
+ return resp;
158
+ }
159
+ /**
160
+ * Phase C2: warn once per client when backend's resolved API version
161
+ * differs from the SDK's compiled `INFERIOR_API_VERSION`. Cheap; no
162
+ * body access. Backend B2 ships the `X-Inferior-Resolved-Version`
163
+ * header on every response.
164
+ */
165
+ checkApiVersion(resp) {
166
+ if (this.apiVersionMismatchWarned)
167
+ return;
168
+ const resolved = resp.headers.get(HEADER_RESOLVED_VERSION);
169
+ if (!resolved || resolved === INFERIOR_API_VERSION)
170
+ return;
171
+ this.apiVersionMismatchWarned = true;
172
+ // eslint-disable-next-line no-console
173
+ console.warn(`[inferior-sdk] Backend resolved API version=${JSON.stringify(resolved)} ` +
174
+ `differs from SDK INFERIOR_API_VERSION=${JSON.stringify(INFERIOR_API_VERSION)}. ` +
175
+ `Backend likely supports your version transparently; if you see ` +
176
+ `incorrect responses, upgrade the SDK.`);
177
+ }
178
+ async json(method, path, options) {
179
+ const resp = await this.request(method, path, options);
180
+ await raiseForStatus(resp);
181
+ return (await resp.json());
182
+ }
183
+ // ── Search ──────────────────────────────────────────────────────────
184
+ async search(query, options) {
185
+ // Auto-populate conditions with detected environment
186
+ const mergedConditions = { ...(options?.conditions ?? {}) };
187
+ if (this.hostOs && !mergedConditions.host_os)
188
+ mergedConditions.host_os = this.hostOs;
189
+ if (this.baseModel && !mergedConditions.base_model)
190
+ mergedConditions.base_model = this.baseModel;
191
+ if (this.framework && !mergedConditions.framework)
192
+ mergedConditions.framework = this.framework;
193
+ const params = {
194
+ q: query,
195
+ limit: String(options?.limit ?? 5),
196
+ scope: options?.scope ?? "collective",
197
+ };
198
+ if (Object.keys(mergedConditions).length > 0)
199
+ params.conditions = JSON.stringify(mergedConditions);
200
+ if (options?.tags)
201
+ params.tags = options.tags.join(",");
202
+ if (options?.compact)
203
+ params.compact = "true";
204
+ if (options?.error_message)
205
+ params.error_message = options.error_message;
206
+ if (options?.your_conditions)
207
+ params.your_conditions = JSON.stringify(options.your_conditions);
208
+ // Phase E filter params — camelCase at the SDK boundary, snake_case on the wire.
209
+ if (options?.minCausalDepth != null)
210
+ params.min_causal_depth = String(options.minCausalDepth);
211
+ if (options?.minBoundaryPrecision != null)
212
+ params.min_boundary_precision = String(options.minBoundaryPrecision);
213
+ if (options?.minInsightTransferability != null)
214
+ params.min_insight_transferability = String(options.minInsightTransferability);
215
+ if (options?.evidenceClass)
216
+ params.evidence_class = options.evidenceClass;
217
+ if (options?.includeDrafts)
218
+ params.include_drafts = "true";
219
+ const data = await this.json("GET", "/v1/experiences/search", { params });
220
+ this.checkSchemaVersion(data);
221
+ const metadata = data.metadata ?? { cached: false };
222
+ const schemaVersion = data.schema_version ?? undefined;
223
+ if (options?.compact) {
224
+ return {
225
+ results: (data.results ?? []).map((r) => ({
226
+ id: r.id ?? "",
227
+ title: r.title ?? "",
228
+ compact_summary: r.compact_summary,
229
+ tags: r.tags ?? [],
230
+ transfer_warnings: (r.transfer_warnings ?? []).map((tw) => tw.message),
231
+ })),
232
+ total_results: data.total_results ?? 0,
233
+ metadata,
234
+ schema_version: schemaVersion,
235
+ };
236
+ }
237
+ return {
238
+ results: data.results ?? [],
239
+ total_results: data.total_results ?? 0,
240
+ metadata,
241
+ schema_version: schemaVersion,
242
+ };
243
+ }
244
+ // ── Deposit ─────────────────────────────────────────────────────────
245
+ async deposit(input) {
246
+ const successful_approach = { method: input.solution };
247
+ if (input.implementation)
248
+ successful_approach.implementation = input.implementation;
249
+ if (input.time_to_resolution_minutes != null)
250
+ successful_approach.time_to_resolution_minutes = input.time_to_resolution_minutes;
251
+ const outcome = { status: input.outcome_status ?? "resolved" };
252
+ if (input.outcome_evidence)
253
+ outcome.evidence = input.outcome_evidence;
254
+ if (input.outcome_side_effects)
255
+ outcome.side_effects = input.outcome_side_effects;
256
+ // Phase B
257
+ if (input.evidence_class)
258
+ outcome.evidence_class = input.evidence_class;
259
+ const payload = {
260
+ title: input.title,
261
+ problem: input.problem,
262
+ successful_approach,
263
+ outcome,
264
+ root_cause: input.root_cause,
265
+ insight: input.insight,
266
+ tags: input.tags,
267
+ failed_approaches: input.failed_approaches ?? [],
268
+ applies_when: input.applies_when ?? [],
269
+ does_not_apply_when: input.does_not_apply_when ?? [],
270
+ creation_mode: input.creation_mode ?? "structured",
271
+ modality: input.modality ?? "text",
272
+ // Phase A: default "public" when caller omits.
273
+ visibility_scope: input.visibility_scope ?? "public",
274
+ };
275
+ if (input.origin)
276
+ payload.origin = input.origin;
277
+ // Auto-populate context.environment with detected host_os and framework.
278
+ // Phase B: merge env_versions into context.environment.versions.
279
+ const mergedContext = { ...(input.context ?? {}) };
280
+ const env = { ...(mergedContext.environment ?? {}) };
281
+ if (this.hostOs && !env.host_os)
282
+ env.host_os = this.hostOs;
283
+ if (this.framework && !env.framework)
284
+ env.framework = this.framework;
285
+ if (input.env_versions) {
286
+ const existingVersions = env.versions ?? {};
287
+ env.versions = { ...existingVersions, ...input.env_versions };
288
+ }
289
+ if (Object.keys(env).length > 0)
290
+ mergedContext.environment = env;
291
+ if (Object.keys(mergedContext).length > 0)
292
+ payload.context = mergedContext;
293
+ const headers = input.idempotencyKey ? { "Idempotency-Key": input.idempotencyKey } : undefined;
294
+ const resp = await this.json("POST", "/v1/experiences", { body: payload, headers });
295
+ this.checkSchemaVersion(resp);
296
+ return resp;
297
+ }
298
+ async depositRaw(input) {
299
+ if (!input.content && !input.problem) {
300
+ throw new Error("Either 'content' or 'problem' must be provided.");
301
+ }
302
+ const payload = { creation_mode: input.creation_mode ?? "raw" };
303
+ if (input.content)
304
+ payload.content = input.content;
305
+ if (input.problem)
306
+ payload.problem = input.problem;
307
+ if (input.what_worked)
308
+ payload.what_worked = input.what_worked;
309
+ if (input.context)
310
+ payload.context = input.context;
311
+ if (input.what_was_tried)
312
+ payload.what_was_tried = input.what_was_tried;
313
+ if (input.outcome)
314
+ payload.outcome = input.outcome;
315
+ if (input.root_cause)
316
+ payload.root_cause = input.root_cause;
317
+ if (input.insight)
318
+ payload.insight = input.insight;
319
+ if (input.tags)
320
+ payload.tags = input.tags;
321
+ const headers = input.idempotencyKey ? { "Idempotency-Key": input.idempotencyKey } : undefined;
322
+ return this.json("POST", "/v1/experiences/raw", { body: payload, headers });
323
+ }
324
+ async depositFile(path, options) {
325
+ const content = await readFile(path, "utf-8");
326
+ const fileName = basename(path);
327
+ const formData = new FormData();
328
+ formData.append("file", new Blob([content], { type: "text/plain" }), fileName);
329
+ if (options?.tags)
330
+ formData.append("tags", JSON.stringify(options.tags));
331
+ const controller = new AbortController();
332
+ const timer = setTimeout(() => controller.abort(), this.timeout);
333
+ try {
334
+ const resp = await this.fetcher(`${this.baseUrl}/v1/experiences/raw/file`, {
335
+ method: "POST",
336
+ headers: { Authorization: `Bearer ${this.apiKey}` },
337
+ body: formData,
338
+ signal: controller.signal,
339
+ });
340
+ await raiseForStatus(resp);
341
+ return (await resp.json());
342
+ }
343
+ finally {
344
+ clearTimeout(timer);
345
+ }
346
+ }
347
+ // ── Feedback ────────────────────────────────────────────────────────
348
+ async feedback(experienceId, input) {
349
+ const payload = { was_helpful: input.was_helpful };
350
+ if (input.helpfulness_detail)
351
+ payload.helpfulness_detail = input.helpfulness_detail;
352
+ if (input.context_note)
353
+ payload.context_note = input.context_note;
354
+ if (input.time_saved_minutes != null)
355
+ payload.time_saved_minutes = input.time_saved_minutes;
356
+ if (input.your_conditions)
357
+ payload.your_conditions = input.your_conditions;
358
+ return this.json("POST", `/v1/experiences/${experienceId}/feedback`, { body: payload });
359
+ }
360
+ // ── Experience ──────────────────────────────────────────────────────
361
+ async getExperience(id) {
362
+ return this.json("GET", `/v1/experiences/${id}`);
363
+ }
364
+ async retractExperience(id, reason) {
365
+ const body = reason ? { reason } : {};
366
+ return this.json("POST", `/v1/experiences/${id}/retract`, { body });
367
+ }
368
+ // ── Agent Registration ──────────────────────────────────────────────
369
+ static async register(options = {}) {
370
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
371
+ const payload = {
372
+ type: options.agentType ?? "ai_agent",
373
+ host_os: detectHostOs(),
374
+ };
375
+ if (options.inviteCode)
376
+ payload.invite_code = options.inviteCode;
377
+ if (options.name)
378
+ payload.agent_name = options.name;
379
+ if (options.platform)
380
+ payload.platform = options.platform;
381
+ if (options.baseModel)
382
+ payload.base_model = options.baseModel;
383
+ if (options.framework)
384
+ payload.framework = options.framework;
385
+ const resp = await fetch(`${baseUrl}/v1/agents/register`, {
386
+ method: "POST",
387
+ headers: { "Content-Type": "application/json" },
388
+ body: JSON.stringify(payload),
389
+ });
390
+ await raiseForStatus(resp);
391
+ return (await resp.json());
392
+ }
393
+ // ── Agent Info & Keys ───────────────────────────────────────────────
394
+ async getMe() {
395
+ return this.json("GET", "/v1/agents/me");
396
+ }
397
+ async getKeys(contributorId) {
398
+ return this.json("GET", `/v1/agents/${contributorId}/keys`);
399
+ }
400
+ async createKey(contributorId, input) {
401
+ const body = {
402
+ name: input.name,
403
+ scope: input.scope ?? "full",
404
+ expires_at: input.expires_at ?? null,
405
+ };
406
+ // Phase A: optional workspace scope for team-visibility access.
407
+ if (input.workspace_id != null)
408
+ body.workspace_id = input.workspace_id;
409
+ return this.json("POST", `/v1/agents/${contributorId}/keys`, { body });
410
+ }
411
+ async revokeKey(contributorId, keyId) {
412
+ const resp = await this.request("DELETE", `/v1/agents/${contributorId}/keys/${keyId}`);
413
+ await raiseForStatus(resp);
414
+ }
415
+ // ── Profile & Stats ─────────────────────────────────────────────────
416
+ async getProfile() {
417
+ return this.json("GET", "/v1/agents/me/profile");
418
+ }
419
+ async contextCheck(input) {
420
+ const payload = { task_description: input.task_description };
421
+ if (input.tools)
422
+ payload.tools = input.tools;
423
+ if (input.environment)
424
+ payload.environment = input.environment;
425
+ return this.json("POST", "/v1/experiences/context-check", { body: payload });
426
+ }
427
+ async getStats() {
428
+ return this.json("GET", "/v1/stats");
429
+ }
430
+ async batchSearch(queries) {
431
+ return this.json("POST", "/v1/experiences/search/batch", { body: { queries } });
432
+ }
433
+ /**
434
+ * Phase E: retrieve agent-demand hotspots — queries where agents
435
+ * searched but found no high-quality results.
436
+ *
437
+ * Requires an API key with **full** scope (admin-only endpoint).
438
+ *
439
+ * @throws {InsufficientScopeError} Key lacks ``full`` scope.
440
+ */
441
+ async demandHotspots(options) {
442
+ const params = {
443
+ days: String(options?.days ?? 7),
444
+ max_top_score: String(options?.maxTopScore ?? 0.3),
445
+ limit: String(options?.limit ?? 50),
446
+ };
447
+ if (options?.domain)
448
+ params.domain = options.domain;
449
+ return this.json("GET", "/v1/demand/hotspots", { params });
450
+ }
451
+ // ── Local Decision Helpers (v1.0+) ──────────────────────────────────
452
+ //
453
+ // Pure functions supporting the framework's local-first model. No
454
+ // network, no I/O. Exposed as static methods so callers can use them
455
+ // either on an instance or on the class itself.
456
+ /** v1.2+: mid-session search limit. Set to 0 to disable the budget gate. */
457
+ static MID_SESSION_SEARCH_LIMIT = 2;
458
+ /** v1.2+: correction-language regex patterns used by the signal detectors. */
459
+ static CORRECTION_LANGUAGE_PATTERNS = [
460
+ "\\bno\\b",
461
+ "\\bactually\\b",
462
+ "\\binstead of\\b",
463
+ "\\bredo\\b",
464
+ "\\bfix\\b",
465
+ "\\bimprove\\b",
466
+ "\\bwrong\\b",
467
+ "not what I (meant|want)",
468
+ "the tone is",
469
+ "too (formal|casual|long|short|terse|verbose)",
470
+ "\\breword\\b",
471
+ "\\bshorten\\b",
472
+ "\\bexpand\\b",
473
+ "add more detail",
474
+ "remove the",
475
+ "try again",
476
+ "let'?s also",
477
+ "\\bmissing\\b",
478
+ "\\bforgot\\b",
479
+ ];
480
+ /**
481
+ * Local gate: decide whether to call ``search``. Pure function.
482
+ *
483
+ * Anti-triggers override triggers — if any anti-trigger fires, return
484
+ * false regardless of triggers. Otherwise any trigger returns true.
485
+ * No triggers → false (framework bias: when in doubt, skip).
486
+ *
487
+ * v1.2+ additions:
488
+ * - If ``trace`` is provided, auto-derive ``isErrorShaped``,
489
+ * ``hasRejectedOutputThisSession``, and ``userMessageHasWarning``
490
+ * from the trace.
491
+ * - Enforces mid-session search budget: when
492
+ * ``signals.searchesThisSession >= MID_SESSION_SEARCH_LIMIT`` and
493
+ * the limit > 0, returns false with a ``console.warn``.
494
+ */
495
+ static shouldSearch(signals, trace) {
496
+ let isErrorShaped = signals.isErrorShaped;
497
+ let hasRejectedOutput = signals.hasRejectedOutputThisSession;
498
+ let userWarning = signals.userMessageHasWarning;
499
+ if (trace) {
500
+ if ((trace.toolCalls ?? []).some((tc) => tc.outcome === "error")) {
501
+ isErrorShaped = true;
502
+ }
503
+ if ((trace.outputs ?? []).some((o) => o.accepted === false)) {
504
+ hasRejectedOutput = true;
505
+ }
506
+ const patterns = InferiorClient.CORRECTION_LANGUAGE_PATTERNS.map((p) => new RegExp(p, "i"));
507
+ if ((trace.userMessages ?? []).some((um) => um.index > 0 && patterns.some((p) => p.test(um.content)))) {
508
+ userWarning = true;
509
+ }
510
+ }
511
+ const limit = InferiorClient.MID_SESSION_SEARCH_LIMIT;
512
+ if (limit > 0 && (signals.searchesThisSession ?? 0) >= limit) {
513
+ // eslint-disable-next-line no-console
514
+ console.warn(`[inferior-sdk] Mid-session search budget exhausted ` +
515
+ `(searchesThisSession=${signals.searchesThisSession ?? 0} >= ` +
516
+ `MID_SESSION_SEARCH_LIMIT=${limit}). Skipping. ` +
517
+ `Set InferiorClient.MID_SESSION_SEARCH_LIMIT = 0 to disable.`);
518
+ return false;
519
+ }
520
+ if (signals.isTrivial ||
521
+ signals.isCreative ||
522
+ signals.repeatWithinSession ||
523
+ signals.isNearZeroCoverage ||
524
+ signals.isProprietaryClosed ||
525
+ signals.isLatencySensitive ||
526
+ signals.isMidFragment) {
527
+ return false;
528
+ }
529
+ if (isErrorShaped)
530
+ return true;
531
+ if (signals.isHighStakes)
532
+ return true;
533
+ if (signals.isUnfamiliar)
534
+ return true;
535
+ if (signals.isLongCommitment)
536
+ return true;
537
+ if (signals.isRegulatory)
538
+ return true;
539
+ // v1.2 additions.
540
+ if (hasRejectedOutput)
541
+ return true;
542
+ if (signals.isRepeatedPattern)
543
+ return true;
544
+ if (userWarning)
545
+ return true;
546
+ // Stuck signal: must have BOTH a failed attempt AND elapsed time.
547
+ if ((signals.failedAttempts ?? 0) >= 1 && (signals.elapsedMinutes ?? 0) >= 5) {
548
+ return true;
549
+ }
550
+ return false;
551
+ }
552
+ /**
553
+ * Derive worthiness signals from an execution trace (v1.2+).
554
+ * Framework Phase 1 for deposits, domain-agnostic.
555
+ */
556
+ static detectDepositSignals(trace) {
557
+ const signals = [];
558
+ const toolCalls = trace.toolCalls ?? [];
559
+ const outputs = trace.outputs ?? [];
560
+ const userMessages = trace.userMessages ?? [];
561
+ const searches = trace.searchesPerformed ?? [];
562
+ const hasToolError = toolCalls.some((tc) => tc.outcome === "error");
563
+ const hasOutputRejection = outputs.some((o) => o.accepted === false);
564
+ const hasExplicitAccept = outputs.some((o) => o.accepted === true);
565
+ const sessionSucceeded = (trace.outcome ?? "success") === "success";
566
+ // Signal 1: error_recovery (broadened).
567
+ if ((hasToolError || hasOutputRejection) && sessionSucceeded) {
568
+ signals.push({
569
+ type: "error_recovery",
570
+ count: toolCalls.filter((tc) => tc.outcome === "error").length +
571
+ outputs.filter((o) => o.accepted === false).length,
572
+ evidence: "attempt rejected → subsequent success",
573
+ });
574
+ }
575
+ // NEW: output_rejection_recovery.
576
+ if (hasOutputRejection && hasExplicitAccept) {
577
+ signals.push({
578
+ type: "output_rejection_recovery",
579
+ count: outputs.filter((o) => o.accepted === false).length,
580
+ evidence: "explicit accepted=false → accepted=true pair",
581
+ });
582
+ }
583
+ // Signal 2: high_retry.
584
+ const goalCounts = {};
585
+ for (const tc of toolCalls) {
586
+ const key = tc.goal ?? tc.name;
587
+ if (key)
588
+ goalCounts[key] = (goalCounts[key] ?? 0) + 1;
589
+ }
590
+ const maxToolRetry = Object.values(goalCounts).reduce((a, b) => Math.max(a, b), 0);
591
+ const outputRetryCount = outputs.length;
592
+ if (maxToolRetry >= 3 || outputRetryCount >= 3) {
593
+ const count = Math.max(maxToolRetry, outputRetryCount);
594
+ signals.push({
595
+ type: "high_retry",
596
+ count,
597
+ evidence: `goal/output retried ${count} times`,
598
+ });
599
+ }
600
+ // Signal 3: plan_deviation.
601
+ const successfulTools = toolCalls.filter((tc) => tc.outcome === "success");
602
+ if (successfulTools.length >= 2 && toolCalls.length > 0) {
603
+ const first = toolCalls[0].name;
604
+ const last = successfulTools[successfulTools.length - 1].name;
605
+ if (first && last && first !== last) {
606
+ signals.push({
607
+ type: "plan_deviation",
608
+ count: 1,
609
+ evidence: `first tool '${first}' → final successful '${last}'`,
610
+ });
611
+ }
612
+ }
613
+ // Signal 4: human_correction.
614
+ const patterns = InferiorClient.CORRECTION_LANGUAGE_PATTERNS.map((p) => new RegExp(p, "i"));
615
+ const correctionSamples = [];
616
+ for (const um of userMessages) {
617
+ if (um.index > 0 && patterns.some((p) => p.test(um.content))) {
618
+ correctionSamples.push(um.content.slice(0, 80));
619
+ }
620
+ }
621
+ for (const o of outputs) {
622
+ if (o.userFeedback && patterns.some((p) => p.test(o.userFeedback))) {
623
+ correctionSamples.push(o.userFeedback.slice(0, 80));
624
+ }
625
+ }
626
+ if (correctionSamples.length > 0) {
627
+ signals.push({
628
+ type: "human_correction",
629
+ count: correctionSamples.length,
630
+ evidence: `${correctionSamples.length} correction-language match(es)`,
631
+ metadata: { samples: correctionSamples.slice(0, 3) },
632
+ });
633
+ }
634
+ // Signal 5: search_miss.
635
+ const misses = searches.filter((s) => s.resultCount === 0 || s.topScore < 0.3);
636
+ if (misses.length > 0 && sessionSucceeded) {
637
+ signals.push({
638
+ type: "search_miss",
639
+ count: misses.length,
640
+ evidence: `${misses.length} unmet search(es) preceded success`,
641
+ metadata: { queries: misses.slice(0, 3).map((m) => m.query) },
642
+ });
643
+ }
644
+ return signals;
645
+ }
646
+ /** v1.2+: search-side signal detection from an execution trace. */
647
+ static detectSearchSignals(trace) {
648
+ const signals = [];
649
+ const toolCalls = trace.toolCalls ?? [];
650
+ const outputs = trace.outputs ?? [];
651
+ const userMessages = trace.userMessages ?? [];
652
+ const errors = toolCalls.filter((tc) => tc.outcome === "error");
653
+ if (errors.length > 0) {
654
+ signals.push({
655
+ type: "error_state",
656
+ count: errors.length,
657
+ evidence: `${errors.length} tool-call error(s) in trace`,
658
+ });
659
+ }
660
+ const rejections = outputs.filter((o) => o.accepted === false);
661
+ if (rejections.length > 0) {
662
+ signals.push({
663
+ type: "output_rejection_state",
664
+ count: rejections.length,
665
+ evidence: `${rejections.length} output(s) rejected by user`,
666
+ });
667
+ }
668
+ const patterns = InferiorClient.CORRECTION_LANGUAGE_PATTERNS.map((p) => new RegExp(p, "i"));
669
+ for (const um of userMessages) {
670
+ if (um.index > 0 && patterns.some((p) => p.test(um.content))) {
671
+ signals.push({
672
+ type: "user_warning",
673
+ count: 1,
674
+ evidence: "mid-task user message with correction language",
675
+ });
676
+ break;
677
+ }
678
+ }
679
+ return signals;
680
+ }
681
+ /**
682
+ * Build a search-worthy query from a draft and context (v1.2+).
683
+ * Framework Phase 2 for search.
684
+ */
685
+ static formSearchQuery(draftQuery, context) {
686
+ const ctx = context ?? {};
687
+ const pieces = draftQuery.trim() ? [draftQuery.trim()] : [];
688
+ const added = [];
689
+ const lower = (draftQuery ?? "").toLowerCase();
690
+ for (const tech of ctx.techStack ?? []) {
691
+ if (!lower.includes(tech.toLowerCase()))
692
+ added.push(tech);
693
+ }
694
+ for (const [name, version] of Object.entries(ctx.versions ?? {})) {
695
+ const token = `${name} ${version}`;
696
+ if (!lower.includes(token.toLowerCase()))
697
+ added.push(token);
698
+ }
699
+ for (const [key, val] of Object.entries(ctx.environment ?? {})) {
700
+ const token = key ? `${key}=${val}` : val;
701
+ if (!lower.includes(token.toLowerCase()))
702
+ added.push(token);
703
+ }
704
+ for (const c of ctx.constraints ?? []) {
705
+ if (!lower.includes(c.toLowerCase()))
706
+ added.push(c);
707
+ }
708
+ if (ctx.errorMessage) {
709
+ const safety = InferiorClient.isQuerySafe(ctx.errorMessage);
710
+ if (safety.isSafe) {
711
+ added.push(ctx.errorMessage.slice(0, 120));
712
+ }
713
+ // else: silently skip unsafe error message.
714
+ }
715
+ const formed = [...pieces, ...added].join(" ").trim();
716
+ const wordCount = formed ? formed.split(/\s+/).length : 0;
717
+ const reasons = [];
718
+ let isUsable = true;
719
+ if (wordCount < 3) {
720
+ isUsable = false;
721
+ reasons.push(`Query too vague (${wordCount} word(s); need ≥3).`);
722
+ }
723
+ const finalSafety = InferiorClient.isQuerySafe(formed);
724
+ if (!finalSafety.isSafe) {
725
+ isUsable = false;
726
+ for (const r of finalSafety.reasons)
727
+ reasons.push(`Unsafe: ${r}`);
728
+ }
729
+ return {
730
+ formedQuery: formed,
731
+ isUsable,
732
+ wordCount,
733
+ reasons,
734
+ droppedProjectTerms: [],
735
+ };
736
+ }
737
+ /**
738
+ * Local gate: score a deposit draft before sending. Pure function.
739
+ *
740
+ * Checks four locally-evaluable hard dimensions (causal depth,
741
+ * boundary precision, evidence honesty, non-triviality) AND (v1.2+)
742
+ * produces a 5-dimension preview of the server's worthiness scoring
743
+ * (novelty, transferability, consequence, evidence, recurrence).
744
+ *
745
+ * Does NOT make any network calls. Caller should still search
746
+ * Inferior before depositing to confirm the novelty dimension.
747
+ */
748
+ static depositWorthiness(draft, signalsFired) {
749
+ const reasons = [];
750
+ const failed = [];
751
+ const addFailed = (dim) => {
752
+ if (!failed.includes(dim))
753
+ failed.push(dim);
754
+ };
755
+ const nonTrivialChecks = [
756
+ [draft.isTrivialFix, "Task is a trivial fix (rename/format/typo); skip."],
757
+ [draft.isDocumentationOnly, "Following documentation verbatim; skip."],
758
+ [
759
+ draft.isWellKnownPattern,
760
+ "Well-known pattern already in Stack Overflow / official docs / training data; skip.",
761
+ ],
762
+ [draft.isTrialAndErrorGuess, "Trial-and-error fix you don't understand; skip."],
763
+ [
764
+ draft.isContextSpecificOnly,
765
+ "Solution is context-specific to this codebase; skip or use visibility_scope='private'.",
766
+ ],
767
+ ];
768
+ for (const [flag, reason] of nonTrivialChecks) {
769
+ if (flag) {
770
+ addFailed("non_trivial");
771
+ reasons.push(reason);
772
+ }
773
+ }
774
+ const rc = (draft.root_cause ?? "").trim();
775
+ if (rc.length < 30) {
776
+ addFailed("causal_depth");
777
+ reasons.push("root_cause is too short (<30 chars). Explain WHY the problem occurred, not just what happened.");
778
+ }
779
+ else if (draft.problem && rc.toLowerCase() === draft.problem.trim().toLowerCase()) {
780
+ addFailed("causal_depth");
781
+ reasons.push("root_cause just restates the problem. Explain the causal mechanism.");
782
+ }
783
+ if (!draft.does_not_apply_when || draft.does_not_apply_when.length === 0) {
784
+ addFailed("boundary_precision");
785
+ reasons.push("does_not_apply_when is empty. State at least one condition where the solution does NOT apply — if you can't name one, you don't yet understand the boundary.");
786
+ }
787
+ if (draft.evidence_class == null || draft.evidence_class === undefined) {
788
+ addFailed("evidence");
789
+ reasons.push("evidence_class is unset. Tag honestly: production_validated / integration_tested / local_tested / self_reported / speculative.");
790
+ }
791
+ else if (draft.hasBeenVerified === false &&
792
+ ["production_validated", "integration_tested", "local_tested"].includes(draft.evidence_class)) {
793
+ addFailed("evidence");
794
+ reasons.push(`evidence_class='${draft.evidence_class}' claims verification but hasBeenVerified=false. Downgrade to 'self_reported' or 'speculative'.`);
795
+ }
796
+ // Soft nudges.
797
+ if (!draft.failed_approaches || draft.failed_approaches.length === 0) {
798
+ reasons.push("(soft) No failed_approaches listed — failed attempts are often the most valuable part of a deposit.");
799
+ }
800
+ if (!(draft.insight ?? "").trim()) {
801
+ reasons.push("(soft) insight is empty — add the transferable lesson so other agents can apply it elsewhere.");
802
+ }
803
+ const hardDimensions = 4;
804
+ const failedCount = failed.length;
805
+ const score = Math.max(0, (hardDimensions - failedCount) / hardDimensions);
806
+ const shouldDeposit = failedCount === 0;
807
+ if (shouldDeposit) {
808
+ reasons.unshift("All four local dimensions (causal depth, boundary precision, evidence, non-triviality) pass. Still search Inferior before depositing to confirm novelty.");
809
+ }
810
+ // v1.2+: 5-dimension preview using shared math.
811
+ const preview = previewWorthiness(draft, signalsFired);
812
+ reasons.push(`--- Server worthiness preview ` +
813
+ `(estimated score: ${preview.estimatedServerScore.toFixed(2)}, ` +
814
+ `base: ${preview.baseScore.toFixed(2)}) ---`);
815
+ for (const dim of Object.keys(preview.dimensionScores)) {
816
+ const s = preview.dimensionScores[dim];
817
+ const conf = preview.dimensionConfidence[dim];
818
+ reasons.push(` ${dim}: ${s.toFixed(2)} (${conf}) — ${preview.dimensionReasons[dim]}`);
819
+ }
820
+ for (const br of preview.boostReasons) {
821
+ reasons.push(` boost: ${br}`);
822
+ }
823
+ return {
824
+ shouldDeposit,
825
+ score,
826
+ reasons,
827
+ failedDimensions: failed,
828
+ dimensionScores: preview.dimensionScores,
829
+ dimensionConfidence: preview.dimensionConfidence,
830
+ estimatedServerScore: preview.estimatedServerScore,
831
+ wouldLikelyAccept: preview.wouldLikelyAccept,
832
+ };
833
+ }
834
+ /**
835
+ * Local privacy gate: classify a query as safe to send. Pure function.
836
+ * Classifies, does not redact — caller decides whether to abort,
837
+ * rewrite, or proceed. Default policy is conservative.
838
+ */
839
+ static isQuerySafe(query, policy) {
840
+ const blockSecrets = policy?.blockSecrets ?? true;
841
+ const blockInternalHosts = policy?.blockInternalHosts ?? true;
842
+ const blockCustomerData = policy?.blockCustomerData ?? true;
843
+ const customDenyPatterns = policy?.customDenyPatterns ?? [];
844
+ const maxLength = policy?.maxLength ?? 2000;
845
+ const reasons = [];
846
+ if (query.length > maxLength) {
847
+ reasons.push(`Query length (${query.length}) exceeds maxLength (${maxLength}). Shorten to avoid accidentally dumping file contents.`);
848
+ }
849
+ if (blockSecrets) {
850
+ const secretPatterns = [
851
+ /(aws_access_key|aws_secret|sk_live_|sk_test_|ghp_|github_pat_)/i,
852
+ /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/,
853
+ /(api[_-]?key|bearer|token)["'\s:=]+[A-Za-z0-9_\-]{20,}/i,
854
+ ];
855
+ if (secretPatterns.some((pat) => pat.test(query))) {
856
+ reasons.push("Query appears to contain a secret (API key, token, or private key). Do not send.");
857
+ }
858
+ }
859
+ if (blockInternalHosts) {
860
+ const hostPatterns = [
861
+ /\b[\w-]+\.internal\b/,
862
+ /\b[\w-]+\.corp\b/,
863
+ /\b[\w-]+\.local\b/,
864
+ /\blocalhost:\d+\b/,
865
+ /\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/,
866
+ /\b192\.168\.\d{1,3}\.\d{1,3}\b/,
867
+ /\b172\.(1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}\b/,
868
+ ];
869
+ if (hostPatterns.some((pat) => pat.test(query))) {
870
+ reasons.push("Query contains an internal hostname or private IP. Abstract it before sending.");
871
+ }
872
+ }
873
+ if (blockCustomerData) {
874
+ const dataPatterns = [
875
+ [/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/, "email address"],
876
+ [/\b\d{3}-\d{2}-\d{4}\b/, "SSN"],
877
+ [/\b(?:\d[ -]?){13,19}\b/, "credit card / long number sequence"],
878
+ [/\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/, "phone number"],
879
+ ];
880
+ for (const [pat, label] of dataPatterns) {
881
+ if (pat.test(query)) {
882
+ reasons.push(`Query appears to contain a ${label}. Abstract it before sending.`);
883
+ }
884
+ }
885
+ }
886
+ for (const patStr of customDenyPatterns) {
887
+ try {
888
+ const pat = new RegExp(patStr);
889
+ if (pat.test(query)) {
890
+ reasons.push(`Query matches custom deny pattern: ${patStr}`);
891
+ }
892
+ }
893
+ catch {
894
+ // Invalid regex — caller's problem; don't crash.
895
+ }
896
+ }
897
+ return { isSafe: reasons.length === 0, reasons };
898
+ }
899
+ }
900
+ //# sourceMappingURL=client.js.map