@theokit/sdk 4.4.2 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `@theokit/sdk/interactive` — pluggable interactive-session backend seam.
3
+ *
4
+ * The streaming twin of `@theokit/sdk/sandbox` (one-shot `execute`) and sibling
5
+ * of `@theokit/sdk/filesystem`. Ship an `InteractiveBackend` (e.g. the local
6
+ * `@theokit/sdk-pty`, a container/E2B backend for the cluster, or a Tauri
7
+ * backend) to give agent shell tools a surface-agnostic REPL/stdin capability —
8
+ * with NO native dependency in core.
9
+ *
10
+ * @public
11
+ */
12
+ export { InteractiveBackend, type InteractiveProvider, InteractiveUnavailableError, NoSuchSessionError, resolveInteractive, type StartInteractiveOptions, type StartInteractiveResult, type WriteStdinOptions, type WriteStdinResult, } from "./types.js";
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Interactive-session backend protocol — the streaming twin of `SandboxBackend`
3
+ * (which is one-shot `execute`). A surface-agnostic contract for driving a
4
+ * long-lived interactive process (a REPL, `git rebase -i`, any command that
5
+ * PROMPTS for stdin): start → `session_id`, write to stdin, read incremental
6
+ * output, kill.
7
+ *
8
+ * Injected exactly like {@link FilesystemProvider} (SE31) — the tool depends on
9
+ * the interface, the HOST supplies the implementation, so the SAME tool runs on
10
+ * a local PTY (`@theokit/sdk-pty`), a container/E2B backend (cluster/web), or a
11
+ * desktop backend (Tauri) with NO tool change and NO native dependency in core.
12
+ *
13
+ * @public
14
+ */
15
+ /** Thrown when the interactive path is requested but no backend can provide it
16
+ * (no provider injected, or a local backend whose native module / spawn failed).
17
+ * The caller falls back to non-interactive exec. */
18
+ export declare class InteractiveUnavailableError extends Error {
19
+ readonly code: "interactive_unavailable";
20
+ constructor(message: string);
21
+ }
22
+ /** Thrown (typed) when a write/kill targets an unknown or already-exited session,
23
+ * so callers branch on the type instead of string-matching a message. */
24
+ export declare class NoSuchSessionError extends Error {
25
+ readonly code: "no_such_session";
26
+ constructor(sessionId: string);
27
+ }
28
+ /** Result of starting a session: its id + whatever the program printed on startup. */
29
+ export interface StartInteractiveResult {
30
+ sessionId: string;
31
+ output: string;
32
+ }
33
+ /** Result of writing to a session: the output produced during the yield window + liveness. */
34
+ export interface WriteStdinResult {
35
+ output: string;
36
+ alive: boolean;
37
+ }
38
+ /** Bounds a start call. All optional; a backend clamps/defaults each. */
39
+ export interface StartInteractiveOptions {
40
+ /** Working directory for the session. Defaults to the backend's root. */
41
+ cwd?: string;
42
+ /** How long to wait, in ms, before returning the startup output (clamped by the backend). */
43
+ yieldMs?: number;
44
+ /** Idle time, in ms, after which the backend reaps a forgotten session. */
45
+ ttlMs?: number;
46
+ /** Cap on the returned output bytes (tail kept). */
47
+ maxBytes?: number;
48
+ /** Terminal geometry, when the backend allocates a real TTY. */
49
+ cols?: number;
50
+ rows?: number;
51
+ }
52
+ /** Bounds a write call. */
53
+ export interface WriteStdinOptions {
54
+ yieldMs?: number;
55
+ ttlMs?: number;
56
+ maxBytes?: number;
57
+ }
58
+ /**
59
+ * Pluggable interactive-session backend. Implement the three abstract methods.
60
+ * A backend that cannot provide interactive sessions should not be constructed —
61
+ * callers detect absence by catching {@link InteractiveUnavailableError}.
62
+ *
63
+ * @public
64
+ */
65
+ export declare abstract class InteractiveBackend {
66
+ /** Spawn `command` as an interactive session; resolve after the yield window with the
67
+ * `session_id` + startup output. Throws {@link InteractiveUnavailableError} when the
68
+ * session cannot be allocated. */
69
+ abstract startInteractive(command: string, opts?: StartInteractiveOptions): Promise<StartInteractiveResult>;
70
+ /** Write `chars` to a live session's stdin; resolve after the yield window with the output it
71
+ * produced + whether it is still alive. Throws {@link NoSuchSessionError} on an unknown session. */
72
+ abstract writeStdin(sessionId: string, chars: string, opts?: WriteStdinOptions): Promise<WriteStdinResult>;
73
+ /** Kill a session (idempotent) and free its slot. */
74
+ abstract kill(sessionId: string): void;
75
+ }
76
+ /**
77
+ * A backend OR a per-request resolver of one — mirrors {@link FilesystemProvider}. A resolver runs
78
+ * at tool-execution time (request scope), so multi-tenant / multi-role agents get a distinct backend
79
+ * per request without a shared mutable one.
80
+ *
81
+ * @public
82
+ */
83
+ export type InteractiveProvider<Ctx = unknown> = InteractiveBackend | ((ctx: Ctx) => InteractiveBackend | Promise<InteractiveBackend>);
84
+ /** Resolve an {@link InteractiveProvider} to a concrete backend for `ctx`. */
85
+ export declare function resolveInteractive<Ctx>(provider: InteractiveProvider<Ctx>, ctx: Ctx): Promise<InteractiveBackend>;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * SE41 — `assertEval(run, thresholds)`: the CI gate for evals.
3
+ *
4
+ * A pure function over a completed {@link EvalRun}. It reads only
5
+ * `run.aggregate`, collects EVERY unmet threshold (not just the first), and
6
+ * throws {@link EvalThresholdError} carrying the full failure list. Passing
7
+ * silently returns `void` — drop it straight into a Vitest `it(...)` or a
8
+ * standalone eval script whose non-zero exit fails the CI job.
9
+ *
10
+ * @public
11
+ */
12
+ import type { EvalRun, EvalThresholdFailure, EvalThresholds } from "../../types/eval.js";
13
+ /** Thrown by {@link assertEval} when a run misses one or more thresholds. */
14
+ export declare class EvalThresholdError extends Error {
15
+ readonly name = "EvalThresholdError";
16
+ /** The eval's name (`EvalRun.name`). */
17
+ readonly evalName: string;
18
+ /** Every unmet threshold, in check order. */
19
+ readonly failures: readonly EvalThresholdFailure[];
20
+ constructor(evalName: string, failures: readonly EvalThresholdFailure[]);
21
+ }
22
+ /**
23
+ * Assert a run meets every set threshold. Throws {@link EvalThresholdError}
24
+ * with the complete list of failures when it does not; returns `void` on pass.
25
+ */
26
+ export declare function assertEval(run: EvalRun, thresholds: EvalThresholds): void;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * SE41 — trial expansion + collapse for `EvalOptions.trials`.
3
+ *
4
+ * Strategy: EXPAND each dataset entry into `trials` tagged copies, run them
5
+ * through the existing execution paths untouched, then COLLAPSE the resulting
6
+ * per-trial rows back into one row per original entry. Per-scorer score is the
7
+ * mean over the trials — an errored trial contributes 0 (a reliability signal),
8
+ * so the denominator is always `trials`, not the count of successful trials.
9
+ *
10
+ * @internal
11
+ */
12
+ /** Reserved metadata key: the original dataset-row index a trial belongs to. */
13
+ export declare const TRIAL_INDEX_KEY = "__evalRowIndex";
14
+ /** Reserved metadata key: the 0-based trial number within a row. */
15
+ export declare const TRIAL_NUM_KEY = "__evalTrial";
16
+ /** Repeat each entry `trials` times, tagging each copy with its origin + trial number. */
17
+ export declare function expandForTrials(entries: ReadonlyArray<DatasetEntry>, trials: number): DatasetEntry[];
18
+ /** Group per-trial rows by their original entry index and collapse each group. */
19
+ export declare function collapseTrials(rows: ReadonlyArray<EvalRowResult>, trials: number): EvalRowResult[];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * SE41 — Levenshtein edit distance for `Scorers.levenshtein`.
3
+ *
4
+ * Classic two-row dynamic-programming distance (O(n) memory). Callers MUST
5
+ * bound input length via {@link LEVENSHTEIN_MAX_LEN} before calling — the
6
+ * algorithm is O(n*m) time, so unbounded LLM output would be a DoS vector.
7
+ *
8
+ * @internal
9
+ */
10
+ /** Minimum single-edit distance between `a` and `b` (two-row DP, O(n) memory). */
11
+ export declare function levenshteinDistance(a: string, b: string): number;
package/dist/scorers.d.ts CHANGED
@@ -15,6 +15,40 @@
15
15
  import type { ZodType } from "zod";
16
16
  import { type LlmJudgeOptions } from "./internal/scorers/llm-judge.js";
17
17
  import type { NamedScorer, VerifyGateOptions } from "./types/eval.js";
18
+ /** SE41 — options for the deterministic fuzzy `Scorers.levenshtein`. */
19
+ interface LevenshteinOptions {
20
+ /** Case-sensitive compare. Default: false (fuzzy matching is usually case-insensitive). */
21
+ caseSensitive?: boolean;
22
+ /**
23
+ * When set, binarize: `score = normalizedSimilarity >= threshold ? 1 : 0`.
24
+ * When omitted, return the continuous similarity in `[0, 1]`.
25
+ */
26
+ threshold?: number;
27
+ }
28
+ /** SE41 — options for the deterministic `Scorers.numericDiff`. */
29
+ interface NumericDiffOptions {
30
+ /**
31
+ * When set, binarize: `score = |output - expected| <= tolerance ? 1 : 0`.
32
+ * When omitted, return the continuous relative closeness in `[0, 1]`.
33
+ */
34
+ tolerance?: number;
35
+ }
36
+ /** SE41 — options for `Scorers.embeddingSimilarity`. */
37
+ interface EmbeddingSimilarityOptions {
38
+ /** Embedding API key. Defaults to `OPENROUTER_API_KEY` from the environment. */
39
+ apiKey?: string;
40
+ /** Embedding model id (OpenRouter catalog). Default: `openai/text-embedding-3-small`. */
41
+ model?: string;
42
+ /** Override the embeddings HTTP base URL. */
43
+ baseUrl?: string;
44
+ /** When set, binarize: `score = cosine >= threshold ? 1 : 0`. Else continuous. */
45
+ threshold?: number;
46
+ /**
47
+ * Inject an embedding function (DIP): `(texts) => vectors`. When provided, the
48
+ * OpenRouter runtime is NOT constructed — used by tests and custom providers.
49
+ */
50
+ embed?: (texts: ReadonlyArray<string>) => Promise<number[][]>;
51
+ }
18
52
  interface ExactMatchOptions {
19
53
  /** Case-sensitive compare. Default: true. */
20
54
  caseSensitive?: boolean;
@@ -47,6 +81,29 @@ export declare const Scorers: {
47
81
  * inputs before using in production.
48
82
  */
49
83
  regex(pattern: RegExp): NamedScorer;
84
+ /**
85
+ * SE41 — normalized Levenshtein similarity: `1 - editDistance / max(len)`.
86
+ * Deterministic (no LLM), so it always runs in CI. `threshold` binarizes.
87
+ *
88
+ * Refuses empty/non-string `expected` (EC-1 parity) and caps input at
89
+ * {@link LEVENSHTEIN_MAX_LEN} chars to bound the O(n*m) cost on adversarial output.
90
+ */
91
+ levenshtein(opts?: LevenshteinOptions): NamedScorer;
92
+ /**
93
+ * SE41 — numeric closeness. Parses `output` and `expected` as numbers and
94
+ * scores continuous relative closeness `1 - |o-e| / max(|o|,|e|)` (both 0 ⇒ 1),
95
+ * or a binary pass when `tolerance` is set. Deterministic (no LLM).
96
+ */
97
+ numericDiff(opts?: NumericDiffOptions): NamedScorer;
98
+ /**
99
+ * SE41 — semantic similarity via embeddings: cosine of `embed(output)` vs
100
+ * `embed(expected)`, clamped to `[0, 1]` (negatives → 0). `threshold` binarizes.
101
+ *
102
+ * By default routes through OpenRouter's embeddings endpoint
103
+ * (`OPENROUTER_API_KEY`); inject `embed` to use another provider or to test
104
+ * deterministically. Each scored row costs one embeddings call.
105
+ */
106
+ embeddingSimilarity(opts: EmbeddingSimilarityOptions): NamedScorer;
50
107
  /**
51
108
  * Parse `output` as JSON and validate against a Zod schema.
52
109
  *
@@ -71,6 +71,15 @@ export interface EvalOptions {
71
71
  * `[1, 64]` (EC-3 — 0 deadlocks the semaphore, Infinity DoSs the provider).
72
72
  */
73
73
  readonly concurrency?: number;
74
+ /**
75
+ * Repeat each dataset row `trials` times to smooth non-determinism (SE41).
76
+ * Default 1 (no repeat; behavior byte-identical to a non-trialed run). MUST
77
+ * be an integer in `[1, 100]`. With `trials > 1` the returned `EvalRun.rows`
78
+ * still has ONE row per dataset entry — see {@link EvalRowResult.trialCount}
79
+ * for the collapse semantics. Reserved metadata keys `__evalTrial` /
80
+ * `__evalRowIndex` are attached to each per-trial persisted row.
81
+ */
82
+ readonly trials?: number;
74
83
  /** Optional metadata persisted to `EvalRun.metadata` (tags, env, version). */
75
84
  readonly metadata?: Record<string, unknown>;
76
85
  /** Optional progress / lifecycle hooks. */
@@ -98,6 +107,14 @@ export interface EvalRowResult {
98
107
  * Persisted alongside the row when `persist` is set.
99
108
  */
100
109
  readonly outcome?: string;
110
+ /**
111
+ * How many trials produced this row (SE41). Present only when
112
+ * `EvalOptions.trials > 1`: the row is a COLLAPSE of `trialCount` executions
113
+ * of the same dataset entry — each scorer's `score` is the mean over the
114
+ * trials (an errored trial contributes 0, a reliability signal), and
115
+ * `durationMs` / `tokensIn` / `tokensOut` are summed. Absent ⇒ single run.
116
+ */
117
+ readonly trialCount?: number;
101
118
  /**
102
119
  * Captured code change (M6-4): the working-tree `git diff` an agent produced
103
120
  * and whether it reverse-applies cleanly. Produced by `captureArtifact`; the
@@ -193,6 +210,34 @@ export interface EvalPersistOptions {
193
210
  */
194
211
  readonly resume?: boolean;
195
212
  }
213
+ /**
214
+ * Threshold contract for `assertEval(run, thresholds)` (SE41) — the CI gate.
215
+ * Every field is optional; only the ones you set are checked. A run passes
216
+ * iff EVERY set threshold is satisfied; otherwise `assertEval` throws
217
+ * `EvalThresholdError` carrying the full list of failures.
218
+ */
219
+ export interface EvalThresholds {
220
+ /** `aggregate.meanScore` MUST be `>=` this. */
221
+ readonly minMeanScore?: number;
222
+ /** `aggregate.passRatio` (rows with meanScore >= 0.5) MUST be `>=` this. */
223
+ readonly minPassRatio?: number;
224
+ /** `errorRows / totalRows` MUST be `<=` this (0 rows ⇒ ratio 0). */
225
+ readonly maxErrorRatio?: number;
226
+ /**
227
+ * Per-scorer floor on `aggregate.perScorer[name].mean`. A named scorer that
228
+ * never ran (absent from `perScorer`) is itself a failure ("scorer not found").
229
+ */
230
+ readonly perScorer?: Readonly<Record<string, number>>;
231
+ }
232
+ /** One unmet threshold, surfaced on `EvalThresholdError.failures`. */
233
+ export interface EvalThresholdFailure {
234
+ /** Stable metric id, e.g. `"meanScore"`, `"passRatio"`, `"perScorer.exact-match"`. */
235
+ readonly metric: string;
236
+ /** The floor (or ceiling, for `maxErrorRatio`) that was required. */
237
+ readonly threshold: number;
238
+ /** The observed value that violated the threshold (`NaN` when the scorer was absent). */
239
+ readonly actual: number;
240
+ }
196
241
  /** Per-call options for `eval.run(...)`. */
197
242
  export interface EvalRunOptions {
198
243
  /** Cancels pending rows; in-flight rows complete (D140 pattern). */
@@ -0,0 +1,157 @@
1
+ # `@theokit/sdk` Error Codes Reference
2
+
3
+ Canonical reference for `AgentRunError.code` values + provider-to-code mapping (Production-Readiness #3, ADRs D311-D314).
4
+
5
+ ## `AgentRunErrorCode` union (16 codes)
6
+
7
+ | Code | Origin | Retriable | When |
8
+ |---|---|:---:|---|
9
+ | `auth_failed` | provider HTTP 401/403 | no | bad API key, revoked token |
10
+ | `rate_limit` | provider HTTP 429 | **yes** | back off using `retryAfterMs` |
11
+ | `quota_exceeded` | provider HTTP 402 / billing body code | no | billing limit hit |
12
+ | `invalid_request` | provider HTTP 400 (generic) | no | malformed payload |
13
+ | `invalid_model` | provider HTTP 400 + "model not found" | no | model id wrong/unavailable |
14
+ | `context_too_long` | provider 400 + context_length code | no | input exceeds model window |
15
+ | `content_filtered` | provider safety filter | no | safety filter blocked |
16
+ | `safety_blocked` | provider safety filter (alias) | no | reserved for stricter mapping |
17
+ | `model_unavailable` | provider 400 + model_unavailable code | no | model temporarily unavailable |
18
+ | `timeout` | HTTP 408 | **yes** | retry with backoff |
19
+ | `network` | DNS/TCP/transport | **yes** | retry with backoff |
20
+ | `server_error` | HTTP 5xx | **yes** | retry with backoff |
21
+ | `provider_unreachable` | DNS/TCP/timeout/5xx (alias) | **yes** | reserved for stricter mapping |
22
+ | `tool_runtime_error` | tool handler throw inside dispatch | no | bug in handler |
23
+ | `aborted` | `AbortSignal` fired (Phase 4) | no | user/lifecycle cancel |
24
+ | `unknown` | unmapped | no | provider returned shape we don't recognize |
25
+
26
+ ## Exhaustive `switch` pattern
27
+
28
+ ```ts
29
+ import { AgentRunError } from "@theokit/sdk";
30
+
31
+ try {
32
+ await agent.send(message);
33
+ } catch (err) {
34
+ if (!(err instanceof AgentRunError)) throw err;
35
+ switch (err.code) {
36
+ case "auth_failed":
37
+ // bad key — show login UI
38
+ break;
39
+ case "rate_limit":
40
+ if (err.retryAfterMs !== undefined) {
41
+ setTimeout(retry, err.retryAfterMs);
42
+ }
43
+ break;
44
+ case "quota_exceeded":
45
+ // billing — upsell page
46
+ break;
47
+ case "tool_runtime_error":
48
+ // handler bug — log + tell user
49
+ break;
50
+ case "aborted":
51
+ // user cancelled — no UI noise
52
+ break;
53
+ default:
54
+ // unknown / new code — generic fallback
55
+ break;
56
+ }
57
+ }
58
+ ```
59
+
60
+ ## Provider mapping table
61
+
62
+ ### OpenAI (and OpenAI-compat: OpenRouter, DeepSeek, Together, Mistral, Voyage, DeepInfra)
63
+
64
+ | Status | Body hint | → ErrorCode | → AgentRunErrorCode |
65
+ |---|---|---|---|
66
+ | 401 / 403 | any | `auth_failed` | `auth_failed` |
67
+ | 429 | any | `rate_limit` | `rate_limit` |
68
+ | 402 | any | `invalid_request` | `quota_exceeded` (at AgentRunError layer) |
69
+ | 400 | `code: "context_length_exceeded"` | `context_too_long` | `context_too_long` |
70
+ | 400 | `code: "content_policy_violation"` | `content_filtered` | `content_filtered` |
71
+ | 400 | `code: "model_not_found"` | `model_unavailable` | `model_unavailable` |
72
+ | 400 | `code: "insufficient_quota"` | `invalid_request` | `quota_exceeded` |
73
+ | 400 | other | `invalid_request` | `invalid_request` |
74
+ | 408 | any | `timeout` | `timeout` |
75
+ | 5xx | any | `server_error` | `server_error` |
76
+ | other | any | `unknown` | `unknown` |
77
+
78
+ ### Anthropic
79
+
80
+ Same status-based map as OpenAI. Body code hints are dialect-specific (`overloaded_error` → `server_error`, etc.) — see `internal/error-mappers/anthropic.ts` for the authoritative list.
81
+
82
+ ### Vertex AI
83
+
84
+ | GCP status | Canonical | Code |
85
+ |---|---|---|
86
+ | `429` / `RESOURCE_EXHAUSTED` | `RateLimitError` | `rate_limit` |
87
+ | `401` / `UNAUTHENTICATED` | `AuthenticationError` | `auth_failed` |
88
+ | `403` / `PERMISSION_DENIED` | `AuthenticationError` | `auth_failed` |
89
+ | `400` / `INVALID_ARGUMENT` | `ConfigurationError` | `invalid_request` |
90
+ | `408` / `DEADLINE_EXCEEDED` | `NetworkError` | `timeout` |
91
+ | `5xx` | `NetworkError` | `server_error` |
92
+ | other | `UnknownAgentError` | `unknown` |
93
+
94
+ ### Bedrock
95
+
96
+ | AWS status / type | Canonical | Code |
97
+ |---|---|---|
98
+ | 429 / `ThrottlingException` | `RateLimitError` | `rate_limit` |
99
+ | 401/403 / `AccessDeniedException` | `AuthenticationError` | `auth_failed` |
100
+ | 400 / `ValidationException` | `ConfigurationError` | `invalid_request` |
101
+ | 5xx | `NetworkError` | `server_error` |
102
+
103
+ ### Ollama (local)
104
+
105
+ | Failure mode | Code |
106
+ |---|---|
107
+ | Connection refused | `network` |
108
+ | Timeout | `timeout` |
109
+ | Unknown response | `unknown` |
110
+
111
+ Ollama has no billing, no rate limit, no auth → `quota_exceeded`/`rate_limit`/`auth_failed` never fire.
112
+
113
+ ## Fields
114
+
115
+ ```ts
116
+ class AgentRunError extends TheokitAgentError {
117
+ readonly code: AgentRunErrorCode;
118
+ readonly provider?: string;
119
+ readonly raw?: string;
120
+ readonly requestId?: string; // x-request-id / request-id header
121
+ readonly conversationId?: string; // SDK agentId where error fired
122
+
123
+ get retriable(): boolean; // alias for isRetryable
124
+ get retryAfterMs(): number | undefined; // metadata.retryAfter * 1000
125
+ get providerError(): unknown; // metadata.raw alias
126
+
127
+ readonly metadata?: ErrorMetadata; // full structured context
128
+ }
129
+ ```
130
+
131
+ ## `retryAfterMs` semantics
132
+
133
+ Returns milliseconds derived from `metadata.retryAfter` (seconds). Use with `setTimeout`:
134
+
135
+ ```ts
136
+ if (err.retryAfterMs !== undefined) {
137
+ setTimeout(retry, err.retryAfterMs);
138
+ }
139
+ ```
140
+
141
+ **EC-11: Use `=== undefined` check, NOT truthy check.** `retryAfterMs === 0` is a legitimate value (provider asked for immediate retry — `setTimeout(0)` is valid).
142
+
143
+ ## Anti-leak invariant
144
+
145
+ `AgentRunError.message` NEVER contains `providerError` content (raw response body may carry sensitive data — internal field names, log fragments, etc). To inspect the raw body:
146
+
147
+ ```ts
148
+ console.log(err.providerError); // alias for err.metadata?.raw
149
+ console.log(err.metadata?.raw); // same value (already redacted via D68)
150
+ ```
151
+
152
+ The redacted body is safe to log — `redactSecrets` strips known secret patterns (API keys, JWT, Authorization headers).
153
+
154
+ ## See also
155
+
156
+ - `internal/error-mappers/` — per-provider mapping implementations
157
+ - ADRs D311-D314, D65-D68 (the broader error system)