@cubicecho/agent-core 2.23.0 → 2.24.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.
- package/README.md +4 -2
- package/dist/agent-loop.d.ts +3 -2
- package/dist/agent-loop.js +385 -303
- package/dist/calibration.js +2 -1
- package/dist/capabilities.d.ts +36 -0
- package/dist/capabilities.js +106 -56
- package/dist/client.js +11 -10
- package/dist/compaction.js +5 -9
- package/dist/continuation.d.ts +4 -4
- package/dist/continuation.js +1 -4
- package/dist/events.d.ts +13 -0
- package/dist/events.js +28 -21
- package/dist/guards.d.ts +22 -0
- package/dist/guards.js +22 -0
- package/dist/hook-events.d.ts +22 -0
- package/dist/hook-events.js +22 -0
- package/dist/hooks.d.ts +13 -14
- package/dist/hooks.js +35 -36
- package/dist/index.d.ts +5 -4
- package/dist/index.js +5 -4
- package/dist/retry.d.ts +13 -7
- package/dist/retry.js +14 -7
- package/dist/run-turn.d.ts +8 -1
- package/dist/schema-compat.d.ts +23 -0
- package/dist/schema-compat.js +76 -59
- package/dist/side-task.d.ts +5 -0
- package/dist/side-task.js +18 -47
- package/dist/snapshot.d.ts +2 -2
- package/dist/snapshot.js +27 -48
- package/dist/spec.d.ts +11 -12
- package/dist/spec.js +36 -66
- package/dist/stream.d.ts +7 -0
- package/dist/stream.js +8 -1
- package/dist/thinking.js +6 -4
- package/dist/tokens.d.ts +9 -2
- package/dist/tokens.js +10 -3
- package/dist/tool-calls.d.ts +9 -0
- package/dist/tool-calls.js +11 -6
- package/dist/tool-loading.d.ts +10 -0
- package/dist/tool-loading.js +15 -7
- package/llms.txt +12 -5
- package/package.json +1 -1
package/dist/hooks.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type OpenAI from "openai";
|
|
2
|
+
import { type HookEvent } from "./hook-events.ts";
|
|
2
3
|
/**
|
|
3
4
|
* Lifecycle hooks, from the host's side: what a session looks like to them, where their context
|
|
4
5
|
* lands in a request, and what is said about each one.
|
|
@@ -9,20 +10,6 @@ import type OpenAI from "openai";
|
|
|
9
10
|
* `runHooks` is a runner as it stands and its outcomes pass straight through, without this
|
|
10
11
|
* package depending on it.
|
|
11
12
|
*/
|
|
12
|
-
/**
|
|
13
|
-
* A point in a session a hook can be bound to. Named after Claude Code's hooks of the same shape
|
|
14
|
-
* — `sessionStart` (SessionStart), `beforeTurn` (UserPromptSubmit), `afterTurn` (Stop),
|
|
15
|
-
* `beforeCompact` (PreCompact), `sessionEnd` (SessionEnd) — plus `sessionDelete`, for when the host
|
|
16
|
-
* deletes a session's record.
|
|
17
|
-
*/
|
|
18
|
-
export type HookEvent = "sessionStart" | "beforeTurn" | "afterTurn" | "beforeCompact" | "sessionEnd" | "sessionDelete";
|
|
19
|
-
/** Every event a hook can be bound to, in the order a session meets them. */
|
|
20
|
-
export declare const HOOK_EVENTS: readonly HookEvent[];
|
|
21
|
-
/**
|
|
22
|
-
* The events whose hooks run before a request, and so the only ones whose output can reach it.
|
|
23
|
-
* Anything later runs once the model has already answered.
|
|
24
|
-
*/
|
|
25
|
-
export declare const INJECT_EVENTS: ReadonlySet<HookEvent>;
|
|
26
13
|
/**
|
|
27
14
|
* One message of a session as a hook is handed it. The shape a memory server's `remember` takes,
|
|
28
15
|
* so a template can pass a turn straight through.
|
|
@@ -248,6 +235,18 @@ export declare const UNTRUSTED_PREFACE: string;
|
|
|
248
235
|
export declare function untrusted(text: string, { source }?: {
|
|
249
236
|
source?: string;
|
|
250
237
|
}): string;
|
|
238
|
+
/**
|
|
239
|
+
* A message's text, whether its content is a string or a list of parts.
|
|
240
|
+
*
|
|
241
|
+
* Parts are joined with nothing between them, because they are one text an API split rather than
|
|
242
|
+
* several: a space put in is a character the sender never wrote, and `turnMessages` hashes this
|
|
243
|
+
* into the uuid a memory server dedups on. Compaction reads the same function, so the summariser
|
|
244
|
+
* and the hook told about a fold see one message the same way. A refusal part is left out.
|
|
245
|
+
*
|
|
246
|
+
* @param content A message's `content`, in any of the shapes the API allows. One that is neither
|
|
247
|
+
* a string nor a list reads as empty.
|
|
248
|
+
*/
|
|
249
|
+
export declare const textOf: (content: unknown) => string;
|
|
251
250
|
/**
|
|
252
251
|
* A stretch of a transcript as a hook reads it: what the user and the assistant said, and nothing
|
|
253
252
|
* else.
|
package/dist/hooks.js
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { errorMessage } from "./errors.js";
|
|
3
|
+
import { isPositive } from "./guards.js";
|
|
4
|
+
import { INJECT_EVENTS } from "./hook-events.js";
|
|
3
5
|
import { scoped } from "./scope.js";
|
|
4
|
-
import { estimateTokens } from "./tokens.js";
|
|
5
|
-
/** Every event a hook can be bound to, in the order a session meets them. */
|
|
6
|
-
export const HOOK_EVENTS = [
|
|
7
|
-
"sessionStart",
|
|
8
|
-
"beforeTurn",
|
|
9
|
-
"afterTurn",
|
|
10
|
-
"beforeCompact",
|
|
11
|
-
"sessionEnd",
|
|
12
|
-
"sessionDelete",
|
|
13
|
-
];
|
|
14
|
-
/**
|
|
15
|
-
* The events whose hooks run before a request, and so the only ones whose output can reach it.
|
|
16
|
-
* Anything later runs once the model has already answered.
|
|
17
|
-
*/
|
|
18
|
-
export const INJECT_EVENTS = new Set(["sessionStart", "beforeTurn"]);
|
|
6
|
+
import { CHARS_PER_TOKEN, estimateTokens } from "./tokens.js";
|
|
19
7
|
/**
|
|
20
8
|
* The most context all of a request's hooks add between them by default, in estimated tokens.
|
|
21
9
|
*
|
|
@@ -55,9 +43,8 @@ const hookSettings = scoped(() => ({ ...HOOK_DEFAULTS }));
|
|
|
55
43
|
*/
|
|
56
44
|
export function configureHooks(options = {}) {
|
|
57
45
|
const { contextTokens, preface } = options;
|
|
58
|
-
if (
|
|
46
|
+
if (isPositive(contextTokens))
|
|
59
47
|
hookSettings().contextTokens = contextTokens;
|
|
60
|
-
}
|
|
61
48
|
if (typeof preface === "string")
|
|
62
49
|
hookSettings().preface = preface;
|
|
63
50
|
return { ...hookSettings() };
|
|
@@ -75,8 +62,19 @@ export const resetHooks = () => {
|
|
|
75
62
|
* same rule `configureHooks` applies, so a `0` threaded through for "no opinion" does not quietly
|
|
76
63
|
* turn every hook's context off.
|
|
77
64
|
*/
|
|
78
|
-
const budget = (given) =>
|
|
65
|
+
const budget = (given) => (isPositive(given) ? given : hookSettings().contextTokens);
|
|
79
66
|
const attribute = (text) => text.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
67
|
+
/** What every note about an outcome opens with: which hook it was, and on which event. */
|
|
68
|
+
const noteOf = (outcome) => ({
|
|
69
|
+
event: outcome.event,
|
|
70
|
+
source: outcome.label,
|
|
71
|
+
hookId: outcome.hookId,
|
|
72
|
+
});
|
|
73
|
+
/** The note a failed outcome leaves, the same whichever of the three callers reports it. */
|
|
74
|
+
const failureNote = (outcome) => ({
|
|
75
|
+
...noteOf(outcome),
|
|
76
|
+
error: outcome.error ?? "failed",
|
|
77
|
+
});
|
|
80
78
|
/**
|
|
81
79
|
* Builds the context a set of outcomes adds and the notes that go with it.
|
|
82
80
|
*
|
|
@@ -100,9 +98,8 @@ export function assembleContext(outcomes, maxTokens) {
|
|
|
100
98
|
const notes = [];
|
|
101
99
|
let remaining = budget(maxTokens);
|
|
102
100
|
for (const outcome of outcomes) {
|
|
103
|
-
const base = { event: outcome.event, source: outcome.label, hookId: outcome.hookId };
|
|
104
101
|
if (!outcome.ok) {
|
|
105
|
-
notes.push(
|
|
102
|
+
notes.push(failureNote(outcome));
|
|
106
103
|
continue;
|
|
107
104
|
}
|
|
108
105
|
if (!outcome.inject || !INJECT_EVENTS.has(outcome.event))
|
|
@@ -114,11 +111,11 @@ export function assembleContext(outcomes, maxTokens) {
|
|
|
114
111
|
if (cap <= 0)
|
|
115
112
|
continue;
|
|
116
113
|
if (estimateTokens(text) > cap)
|
|
117
|
-
text = `${text.slice(0, cap *
|
|
114
|
+
text = `${text.slice(0, cap * CHARS_PER_TOKEN - 1).trimEnd()}…`;
|
|
118
115
|
const tokens = estimateTokens(text);
|
|
119
116
|
remaining -= tokens;
|
|
120
117
|
blocks.push(`<context source="${attribute(outcome.label)}">\n${text}\n</context>`);
|
|
121
|
-
notes.push({ ...
|
|
118
|
+
notes.push({ ...noteOf(outcome), tokens, text });
|
|
122
119
|
}
|
|
123
120
|
return { context: blocks.join("\n\n"), notes };
|
|
124
121
|
}
|
|
@@ -187,8 +184,18 @@ export function untrusted(text, { source } = {}) {
|
|
|
187
184
|
const attrs = source === undefined ? "" : ` source="${attribute(source)}"`;
|
|
188
185
|
return `<untrusted${attrs}>\n${text.replace(UNTRUSTED_TAG, "<$1")}\n</untrusted>`;
|
|
189
186
|
}
|
|
190
|
-
/**
|
|
191
|
-
|
|
187
|
+
/**
|
|
188
|
+
* A message's text, whether its content is a string or a list of parts.
|
|
189
|
+
*
|
|
190
|
+
* Parts are joined with nothing between them, because they are one text an API split rather than
|
|
191
|
+
* several: a space put in is a character the sender never wrote, and `turnMessages` hashes this
|
|
192
|
+
* into the uuid a memory server dedups on. Compaction reads the same function, so the summariser
|
|
193
|
+
* and the hook told about a fold see one message the same way. A refusal part is left out.
|
|
194
|
+
*
|
|
195
|
+
* @param content A message's `content`, in any of the shapes the API allows. One that is neither
|
|
196
|
+
* a string nor a list reads as empty.
|
|
197
|
+
*/
|
|
198
|
+
export const textOf = (content) => {
|
|
192
199
|
if (typeof content === "string")
|
|
193
200
|
return content;
|
|
194
201
|
if (!Array.isArray(content))
|
|
@@ -318,9 +325,7 @@ export async function gather(run, events, context, { signal, onNote, maxTokens,
|
|
|
318
325
|
*/
|
|
319
326
|
export async function notify(run, event, context, onNote) {
|
|
320
327
|
const outcomes = await runSafely(run, event, context);
|
|
321
|
-
const notes = outcomes
|
|
322
|
-
.filter((outcome) => !outcome.ok)
|
|
323
|
-
.map((outcome) => assembleContext([outcome]).notes[0]);
|
|
328
|
+
const notes = outcomes.filter((outcome) => !outcome.ok).map(failureNote);
|
|
324
329
|
for (const note of notes)
|
|
325
330
|
onNote?.(note);
|
|
326
331
|
return notes;
|
|
@@ -345,15 +350,9 @@ export async function consult(run, event, context, onNote) {
|
|
|
345
350
|
const notes = [];
|
|
346
351
|
for (const outcome of outcomes) {
|
|
347
352
|
if (!outcome.ok)
|
|
348
|
-
notes.push(
|
|
349
|
-
else if (outcome.veto === true)
|
|
350
|
-
notes.push({
|
|
351
|
-
event: outcome.event,
|
|
352
|
-
source: outcome.label,
|
|
353
|
-
hookId: outcome.hookId,
|
|
354
|
-
veto: true,
|
|
355
|
-
});
|
|
356
|
-
}
|
|
353
|
+
notes.push(failureNote(outcome));
|
|
354
|
+
else if (outcome.veto === true)
|
|
355
|
+
notes.push({ ...noteOf(outcome), veto: true });
|
|
357
356
|
}
|
|
358
357
|
for (const note of notes)
|
|
359
358
|
onNote?.(note);
|
package/dist/index.d.ts
CHANGED
|
@@ -19,18 +19,19 @@ export type { AgentConfig, Endpoint, ModelParams, RetryPolicy, ToolPolicy, } fro
|
|
|
19
19
|
export { type ContinueTurnOptions, continueTurn, isContinuable, } from "./continuation.ts";
|
|
20
20
|
export { AgentLoopError, type AgentLoopFailure, AgentLoopOverflow, errorMessage, failedRun, ToolIterationLimit, } from "./errors.ts";
|
|
21
21
|
export { configureEvents, type EventBusOptions, emit, endRun, fold, history, type RunEvent, type RunEventInput, type RunEventKind, type RunMetrics, type RunMetricsOptions, type RunUsage, resetEvents, runMetrics, type TurnReport, watch, } from "./events.ts";
|
|
22
|
-
export {
|
|
22
|
+
export { HOOK_EVENTS, type HookEvent, INJECT_EVENTS } from "./hook-events.ts";
|
|
23
|
+
export { assembleContext, configureHooks, consult, type Gathered, gather, HOOK_CONTEXT_TOKENS, HOOK_PREFACE, type HookContext, type HookMessage, type HookNote, type HookOptions, type HookOutcome, type HookRunner, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from "./hooks.ts";
|
|
23
24
|
export { estimateFrom, type LedgerEntry, type LedgerEstimateOptions, type LedgerRequest, rebaseLedger, recordRequest, type TokenLedger, tokensBetween, } from "./ledger.ts";
|
|
24
25
|
export { resetAll } from "./reset.ts";
|
|
25
|
-
export { backoffMs,
|
|
26
|
+
export { backoffMs, type ContextBreakdown, type ContextBreakdownOptions, ContextOverflow, compact, contextChars, contextTokens, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestChars, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, type TokenEstimateOptions, toolsChars, } from "./retry.ts";
|
|
26
27
|
export { type RunTurnOptions, runTurn } from "./run-turn.ts";
|
|
27
28
|
export { createRuntime, defaultRuntime, type Runtime, type RuntimeOptions } from "./runtime.ts";
|
|
28
|
-
export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.ts";
|
|
29
|
+
export { isGrammarError, relaxSchema, relaxTools, sanitizeSchema, sanitizeTools, } from "./schema-compat.ts";
|
|
29
30
|
export { type AskJsonOptions, ask, askJson, clean, listLines, parseJson, resetHints, type SideTask, type SideTaskInput, type SideTaskOptions, tryAsk, } from "./side-task.ts";
|
|
30
31
|
export { CAPABILITY_SNAPSHOT_VERSION, type CapabilitySnapshot, type EndpointSnapshot, exportCapabilities, importCapabilities, type ModelSnapshot, } from "./snapshot.ts";
|
|
31
32
|
export { AGENT_SPEC, AGENT_SPEC_VERSION, AGENT_TASKS, type AgentHook, type AgentSpec, type EndpointSpec, type ExportSpecOptions, exportSpec, type ModelSpec, type ParsedSpec, type ParseSpecOptions, type PromptPart, parseSpec, RESOLVED_DEFAULTS, type ResolvedAgent, type ResolvedTask, type RetrySpec, resolveAgentSpec, SPEC_EVENTS, type SpecBundle, type SpecServer, type TaskSpec, type ToolsSpec, } from "./spec.ts";
|
|
32
33
|
export { type Produced, type StreamTurnOptions, streamTurn, type Turn, type TurnUsage, } from "./stream.ts";
|
|
33
34
|
export { ALL_FENCES, DEFAULT_FENCES, type Fence, FenceSplitter, type FenceSplitterOptions, type Split, stripThinking, THINK_FENCE, } from "./thinking.ts";
|
|
34
|
-
export { estimateTokens } from "./tokens.ts";
|
|
35
|
+
export { CHARS_PER_TOKEN, estimateTokens } from "./tokens.ts";
|
|
35
36
|
export { parseToolArguments, recoverToolCalls, ToolArgumentsError, type ToolCall, } from "./tool-calls.ts";
|
|
36
37
|
export { CALL_TOOL, carryOver, catalogList, catalogPrompt, expandNames, holdsDefinitions, inCatalog, KEYWORD_DROPOFF, KEYWORD_MIN_SCORE, type KeywordPreselection, type KeywordPreselectOptions, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, MAX_CARRIED, MAX_PER_LOAD, orderTools, PRESELECT_SCHEMA, PRESELECT_SYSTEM, PROXY_TOOLS, preselectByKeywords, preselectInput, preselection, preselectSystem, proxiedCall, proxyCatalogPrompt, proxyLoadResult, requestedNames, shownCall, type ToolMatch, type ToolOrder, } from "./tool-loading.ts";
|
package/dist/index.js
CHANGED
|
@@ -17,18 +17,19 @@ export { applyCompaction, COMPACT_AT, compactTranscript, KEEP_RATIO, planCompact
|
|
|
17
17
|
export { continueTurn, isContinuable, } from "./continuation.js";
|
|
18
18
|
export { AgentLoopError, AgentLoopOverflow, errorMessage, failedRun, ToolIterationLimit, } from "./errors.js";
|
|
19
19
|
export { configureEvents, emit, endRun, fold, history, resetEvents, runMetrics, watch, } from "./events.js";
|
|
20
|
-
export {
|
|
20
|
+
export { HOOK_EVENTS, INJECT_EVENTS } from "./hook-events.js";
|
|
21
|
+
export { assembleContext, configureHooks, consult, gather, HOOK_CONTEXT_TOKENS, HOOK_PREFACE, notify, resetHooks, turnIndex, turnMessages, UNTRUSTED_PREFACE, untrusted, withContext, } from "./hooks.js";
|
|
21
22
|
export { estimateFrom, rebaseLedger, recordRequest, tokensBetween, } from "./ledger.js";
|
|
22
23
|
export { resetAll } from "./reset.js";
|
|
23
|
-
export { backoffMs,
|
|
24
|
+
export { backoffMs, ContextOverflow, compact, contextChars, contextTokens, EndpointSilent, isModelLoading, isOverflow, isTransient, LOADING_POLL_MS, LOADING_TIMEOUT_MS, messageTokens, requestChars, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, toolsChars, } from "./retry.js";
|
|
24
25
|
export { runTurn } from "./run-turn.js";
|
|
25
26
|
export { createRuntime, defaultRuntime } from "./runtime.js";
|
|
26
|
-
export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.js";
|
|
27
|
+
export { isGrammarError, relaxSchema, relaxTools, sanitizeSchema, sanitizeTools, } from "./schema-compat.js";
|
|
27
28
|
export { ask, askJson, clean, listLines, parseJson, resetHints, tryAsk, } from "./side-task.js";
|
|
28
29
|
export { CAPABILITY_SNAPSHOT_VERSION, exportCapabilities, importCapabilities, } from "./snapshot.js";
|
|
29
30
|
export { AGENT_SPEC, AGENT_SPEC_VERSION, AGENT_TASKS, exportSpec, parseSpec, RESOLVED_DEFAULTS, resolveAgentSpec, SPEC_EVENTS, } from "./spec.js";
|
|
30
31
|
export { streamTurn, } from "./stream.js";
|
|
31
32
|
export { ALL_FENCES, DEFAULT_FENCES, FenceSplitter, stripThinking, THINK_FENCE, } from "./thinking.js";
|
|
32
|
-
export { estimateTokens } from "./tokens.js";
|
|
33
|
+
export { CHARS_PER_TOKEN, estimateTokens } from "./tokens.js";
|
|
33
34
|
export { parseToolArguments, recoverToolCalls, ToolArgumentsError, } from "./tool-calls.js";
|
|
34
35
|
export { CALL_TOOL, carryOver, catalogList, catalogPrompt, expandNames, holdsDefinitions, inCatalog, KEYWORD_DROPOFF, KEYWORD_MIN_SCORE, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadedTools, loadResult, MAX_CARRIED, MAX_PER_LOAD, orderTools, PRESELECT_SCHEMA, PRESELECT_SYSTEM, PROXY_TOOLS, preselectByKeywords, preselectInput, preselection, preselectSystem, proxiedCall, proxyCatalogPrompt, proxyLoadResult, requestedNames, shownCall, } from "./tool-loading.js";
|
package/dist/retry.d.ts
CHANGED
|
@@ -29,13 +29,6 @@ export declare class ContextOverflow extends Error {
|
|
|
29
29
|
* @param tokens The count to render.
|
|
30
30
|
*/
|
|
31
31
|
export declare const compact: (tokens: number) => string;
|
|
32
|
-
/**
|
|
33
|
-
* The divisor behind `estimateTokens`, applied here to a character count rather than a string.
|
|
34
|
-
*
|
|
35
|
-
* The fallback, not the rule: once a turn has come back with a reported prompt count, `runTurn`
|
|
36
|
-
* divides by what that endpoint's model was measured at instead. See `charsPerTokenFor`.
|
|
37
|
-
*/
|
|
38
|
-
export declare const CHARS_PER_TOKEN = 4;
|
|
39
32
|
/** What the two token estimates below take besides what they measure. */
|
|
40
33
|
export interface TokenEstimateOptions {
|
|
41
34
|
/**
|
|
@@ -192,6 +185,19 @@ export declare const SMALLEST_LIKELY_WINDOW = 8192;
|
|
|
192
185
|
* @param error The rejection, as caught. What is not an SDK error is not transient.
|
|
193
186
|
*/
|
|
194
187
|
export declare function isTransient(error: unknown): boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Whether a failure is the endpoint refusing the request as written, rather than losing it.
|
|
190
|
+
*
|
|
191
|
+
* 400 and 422 are what a server says when it read the body and disliked it, which makes them the
|
|
192
|
+
* only statuses worth answering by sending something different. Any 4xx is too wide: 401, 404 and
|
|
193
|
+
* 429 are 4xx and none of them is about the fields, and a 429 is one `isTransient` accepts — a
|
|
194
|
+
* caller that re-sent on it at once doubled the rate against a server that had just asked for less.
|
|
195
|
+
* Catching everything is wider still, since an abort or a connection that never landed then
|
|
196
|
+
* latches off whatever the re-send left out.
|
|
197
|
+
*
|
|
198
|
+
* @param error The rejection, as caught. What is not an SDK error refuses nothing.
|
|
199
|
+
*/
|
|
200
|
+
export declare const refusesRequest: (error: unknown) => boolean;
|
|
195
201
|
/**
|
|
196
202
|
* Whether a failure is a local server still loading the model, rather than one failing to serve.
|
|
197
203
|
*
|
package/dist/retry.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
+
import { CHARS_PER_TOKEN } from "./tokens.js";
|
|
2
3
|
/**
|
|
3
4
|
* Everything about a request failing that is not about what the request said.
|
|
4
5
|
*
|
|
@@ -62,13 +63,6 @@ const REFUSAL_PART = 32;
|
|
|
62
63
|
const REASONING_KEY = 23;
|
|
63
64
|
/** The same for `"reasoning":"",`, OpenRouter's spelling of it. */
|
|
64
65
|
const REASONING_ALT_KEY = 15;
|
|
65
|
-
/**
|
|
66
|
-
* The divisor behind `estimateTokens`, applied here to a character count rather than a string.
|
|
67
|
-
*
|
|
68
|
-
* The fallback, not the rule: once a turn has come back with a reported prompt count, `runTurn`
|
|
69
|
-
* divides by what that endpoint's model was measured at instead. See `charsPerTokenFor`.
|
|
70
|
-
*/
|
|
71
|
-
export const CHARS_PER_TOKEN = 4;
|
|
72
66
|
/** The divisor an option asked for, or the fallback when it asked for nothing usable. */
|
|
73
67
|
const divisor = (charsPerToken) => charsPerToken !== undefined && charsPerToken > 0 ? charsPerToken : CHARS_PER_TOKEN;
|
|
74
68
|
/**
|
|
@@ -349,6 +343,19 @@ export function isTransient(error) {
|
|
|
349
343
|
const { status } = error;
|
|
350
344
|
return status === 408 || status === 409 || status === 429 || (status ?? 0) >= 500;
|
|
351
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Whether a failure is the endpoint refusing the request as written, rather than losing it.
|
|
348
|
+
*
|
|
349
|
+
* 400 and 422 are what a server says when it read the body and disliked it, which makes them the
|
|
350
|
+
* only statuses worth answering by sending something different. Any 4xx is too wide: 401, 404 and
|
|
351
|
+
* 429 are 4xx and none of them is about the fields, and a 429 is one `isTransient` accepts — a
|
|
352
|
+
* caller that re-sent on it at once doubled the rate against a server that had just asked for less.
|
|
353
|
+
* Catching everything is wider still, since an abort or a connection that never landed then
|
|
354
|
+
* latches off whatever the re-send left out.
|
|
355
|
+
*
|
|
356
|
+
* @param error The rejection, as caught. What is not an SDK error refuses nothing.
|
|
357
|
+
*/
|
|
358
|
+
export const refusesRequest = (error) => error instanceof OpenAI.APIError && (error.status === 400 || error.status === 422);
|
|
352
359
|
/**
|
|
353
360
|
* Whether a failure is a local server still loading the model, rather than one failing to serve.
|
|
354
361
|
*
|
package/dist/run-turn.d.ts
CHANGED
|
@@ -77,6 +77,13 @@ export interface RunTurnOptions extends Omit<StreamTurnOptions, "produced"> {
|
|
|
77
77
|
*/
|
|
78
78
|
loadingTimeoutMs?: number;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Builds a turn's request body from what the endpoint and the model have refused so far.
|
|
82
|
+
*
|
|
83
|
+
* A function rather than a body because a downgrade changes what is sent, so it is called again
|
|
84
|
+
* per attempt. Its second argument is absent where no model was named.
|
|
85
|
+
*/
|
|
86
|
+
export type RequestBuilder = (supports: Capabilities, model: ModelCapabilities | undefined) => OpenAI.ChatCompletionCreateParamsStreaming;
|
|
80
87
|
/**
|
|
81
88
|
* `request` is a callback rather than a body because the body has to be rebuilt from whatever
|
|
82
89
|
* the last attempt latched off: the tools it sends depend on `strictSchemas`, and `relaxTools`
|
|
@@ -90,4 +97,4 @@ export interface RunTurnOptions extends Omit<StreamTurnOptions, "produced"> {
|
|
|
90
97
|
* @param options Retry budget, context limit, the model to negotiate for, notices, and the
|
|
91
98
|
* stream's own callbacks.
|
|
92
99
|
*/
|
|
93
|
-
export declare function runTurn(client: OpenAI, supports: Capabilities, request:
|
|
100
|
+
export declare function runTurn(client: OpenAI, supports: Capabilities, request: RequestBuilder, { maxRetries, onNotice, contextLimit, model, droppable, loadingTimeoutMs, ...stream }?: RunTurnOptions): Promise<Turn>;
|
package/dist/schema-compat.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import type OpenAI from "openai";
|
|
2
|
+
/**
|
|
3
|
+
* One JSON Schema as a strict server will accept it, for a schema that is not a tool's parameters.
|
|
4
|
+
*
|
|
5
|
+
* What `sanitizeTools` does to each tool, without the tool: a `response_format` schema goes through
|
|
6
|
+
* the same grammar converter a tool's parameters do, and a caller holding only the schema had to
|
|
7
|
+
* wrap it in a definition to get here. Nothing is remembered — the cache is keyed on a tool
|
|
8
|
+
* definition's identity, and a bare schema has no object that stands for it across requests.
|
|
9
|
+
*
|
|
10
|
+
* @param schema The schema as written. Never mutated. Its root is held to an object, as a tool's
|
|
11
|
+
* parameters are, and anything that is not a schema at all comes back as an object with no
|
|
12
|
+
* properties.
|
|
13
|
+
*/
|
|
14
|
+
export declare function sanitizeSchema(schema: unknown): Record<string, unknown>;
|
|
2
15
|
/**
|
|
3
16
|
* Tool definitions a strict server will accept, remembered per definition object.
|
|
4
17
|
*
|
|
@@ -9,6 +22,16 @@ import type OpenAI from "openai";
|
|
|
9
22
|
* changed, a new definition is returned in its place.
|
|
10
23
|
*/
|
|
11
24
|
export declare const sanitizeTools: (tools: OpenAI.ChatCompletionTool[]) => OpenAI.ChatCompletionTool[];
|
|
25
|
+
/**
|
|
26
|
+
* One schema without its `pattern` and `format` keywords, for a schema that is not a tool's.
|
|
27
|
+
*
|
|
28
|
+
* The retry `relaxTools` makes, on a bare schema. Only the keywords go: a property that happens to
|
|
29
|
+
* be called `format` is an argument name and stays.
|
|
30
|
+
*
|
|
31
|
+
* @param schema Already sanitised. Relaxing is the retry, not a substitute for `sanitizeSchema`.
|
|
32
|
+
* Never mutated; anything that is not a schema comes back as an object with no properties.
|
|
33
|
+
*/
|
|
34
|
+
export declare function relaxSchema(schema: unknown): Record<string, unknown>;
|
|
12
35
|
/**
|
|
13
36
|
* The retry shape: llama.cpp's converter rejects regex escape classes (`\d`, `\w`, `\s`) in
|
|
14
37
|
* `pattern` and most `format` values, both of which only ever narrowed a string the tool
|
package/dist/schema-compat.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { isRecord } from "./guards.js";
|
|
2
2
|
/** Lookahead and lookbehind: `(?=`, `(?!`, `(?<=`, `(?<!`. */
|
|
3
3
|
const LOOKAROUND = /\(\?<?[=!]/;
|
|
4
4
|
const PRIMITIVES = new Set(["object", "string", "number", "integer", "boolean", "array", "null"]);
|
|
@@ -20,6 +20,24 @@ const SCHEMA_KEYS = new Set([
|
|
|
20
20
|
]);
|
|
21
21
|
/** Keys whose value is a name -> schema map. */
|
|
22
22
|
const SCHEMA_MAPS = new Set(["properties", "patternProperties", "$defs", "definitions"]);
|
|
23
|
+
/** The two spellings of a schema's own pool of named definitions. */
|
|
24
|
+
const POOL_KEYS = ["definitions", "$defs"];
|
|
25
|
+
/**
|
|
26
|
+
* One keyword's value with `fn` applied wherever it holds a schema, and untouched where it holds
|
|
27
|
+
* data — which is what keeps a walk out of `default`, `enum` and `const`.
|
|
28
|
+
*/
|
|
29
|
+
const mapChildren = (key, value, fn) => {
|
|
30
|
+
if (SCHEMA_KEYS.has(key))
|
|
31
|
+
return Array.isArray(value) ? value.map((sub) => fn(sub)) : fn(value);
|
|
32
|
+
// The keys here are argument names; only the values are schemas.
|
|
33
|
+
if (SCHEMA_MAPS.has(key) && isRecord(value))
|
|
34
|
+
return Object.fromEntries(Object.entries(value).map(([name, sub]) => [name, fn(sub)]));
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
37
|
+
/** The names a schema's `required` lists, without whatever in it is not a name. */
|
|
38
|
+
const requiredOf = (node) => Array.isArray(node.required)
|
|
39
|
+
? node.required.filter((name) => typeof name === "string")
|
|
40
|
+
: [];
|
|
23
41
|
/**
|
|
24
42
|
* Coerces one schema position. Malformed MCP output sometimes puts a bare type name where a
|
|
25
43
|
* whole schema belongs, which the grammar converter reports as `Unrecognized schema: "object"`.
|
|
@@ -29,7 +47,7 @@ function asSchema(node) {
|
|
|
29
47
|
return PRIMITIVES.has(node) && node !== "object" ? { type: node } : EMPTY_OBJECT();
|
|
30
48
|
if (typeof node === "boolean")
|
|
31
49
|
return node;
|
|
32
|
-
if (!
|
|
50
|
+
if (!isRecord(node))
|
|
33
51
|
return EMPTY_OBJECT();
|
|
34
52
|
return normalize(node);
|
|
35
53
|
}
|
|
@@ -50,14 +68,8 @@ function normalize(node) {
|
|
|
50
68
|
else
|
|
51
69
|
out.type = "null";
|
|
52
70
|
}
|
|
53
|
-
else if (SCHEMA_KEYS.has(key)) {
|
|
54
|
-
out[key] = Array.isArray(value) ? value.map(asSchema) : asSchema(value);
|
|
55
|
-
}
|
|
56
|
-
else if (SCHEMA_MAPS.has(key) && isObject(value)) {
|
|
57
|
-
out[key] = Object.fromEntries(Object.entries(value).map(([name, sub]) => [name, asSchema(sub)]));
|
|
58
|
-
}
|
|
59
71
|
else {
|
|
60
|
-
out[key] = value;
|
|
72
|
+
out[key] = mapChildren(key, value, asSchema);
|
|
61
73
|
}
|
|
62
74
|
}
|
|
63
75
|
collapseNullableUnion(out);
|
|
@@ -66,7 +78,7 @@ function normalize(node) {
|
|
|
66
78
|
if (typeof out.pattern === "string" && LOOKAROUND.test(out.pattern))
|
|
67
79
|
delete out.pattern;
|
|
68
80
|
// `{"type": "object"}` with no properties produces invalid GBNF.
|
|
69
|
-
if (out.type === "object" && !
|
|
81
|
+
if (out.type === "object" && !isRecord(out.properties))
|
|
70
82
|
out.properties = {};
|
|
71
83
|
// Strict validators reject any sibling of `$ref`, and draft-07 ignores them, so a reference
|
|
72
84
|
// stands alone or not at all. This is not hypothetical tidying: collapsing `anyOf: [{$ref},
|
|
@@ -88,11 +100,11 @@ function collapseNullableUnion(node) {
|
|
|
88
100
|
const variants = node[key];
|
|
89
101
|
if (!Array.isArray(variants))
|
|
90
102
|
continue;
|
|
91
|
-
const concrete = variants.filter((item) => !(
|
|
103
|
+
const concrete = variants.filter((item) => !(isRecord(item) && item.type === "null"));
|
|
92
104
|
if (concrete.length !== 1 || concrete.length === variants.length)
|
|
93
105
|
continue;
|
|
94
106
|
delete node[key];
|
|
95
|
-
Object.assign(node, { nullable: true, ...(
|
|
107
|
+
Object.assign(node, { nullable: true, ...(isRecord(concrete[0]) ? concrete[0] : {}) });
|
|
96
108
|
}
|
|
97
109
|
}
|
|
98
110
|
/** Combinators at the top level of a parameters schema; strict backends reject them outright. */
|
|
@@ -102,8 +114,8 @@ const LOCAL_POINTER = /^#\/(definitions|\$defs)\/([^/]+)$/;
|
|
|
102
114
|
/** The `definitions` and `$defs` that a local pointer in this schema resolves against. */
|
|
103
115
|
function poolsOf(parameters) {
|
|
104
116
|
const defs = {};
|
|
105
|
-
for (const key of
|
|
106
|
-
if (
|
|
117
|
+
for (const key of POOL_KEYS)
|
|
118
|
+
if (isRecord(parameters[key]))
|
|
107
119
|
defs[key] = parameters[key];
|
|
108
120
|
return defs;
|
|
109
121
|
}
|
|
@@ -119,16 +131,16 @@ function poolsOf(parameters) {
|
|
|
119
131
|
function resolveRef(node, defs) {
|
|
120
132
|
const seen = new Set();
|
|
121
133
|
let current = node;
|
|
122
|
-
while (
|
|
134
|
+
while (isRecord(current) && typeof current.$ref === "string") {
|
|
123
135
|
const pointer = current.$ref;
|
|
124
136
|
const target = LOCAL_POINTER.exec(pointer);
|
|
125
137
|
if (!target || seen.has(pointer))
|
|
126
138
|
return undefined;
|
|
127
139
|
seen.add(pointer);
|
|
128
140
|
const pool = defs[target[1]];
|
|
129
|
-
current =
|
|
141
|
+
current = isRecord(pool) ? pool[target[2]] : undefined;
|
|
130
142
|
}
|
|
131
|
-
return
|
|
143
|
+
return isRecord(current) ? current : undefined;
|
|
132
144
|
}
|
|
133
145
|
/**
|
|
134
146
|
* Replaces a root-level `$ref` with what it points at.
|
|
@@ -164,18 +176,16 @@ function mergeRootAllOf(out) {
|
|
|
164
176
|
if (!Array.isArray(branches))
|
|
165
177
|
return;
|
|
166
178
|
const defs = poolsOf(out);
|
|
167
|
-
const properties =
|
|
168
|
-
const required = new Set(
|
|
179
|
+
const properties = isRecord(out.properties) ? { ...out.properties } : {};
|
|
180
|
+
const required = new Set(requiredOf(out));
|
|
169
181
|
for (const raw of branches) {
|
|
170
182
|
const branch = resolveRef(raw, defs);
|
|
171
183
|
if (!branch)
|
|
172
184
|
continue;
|
|
173
|
-
if (
|
|
185
|
+
if (isRecord(branch.properties))
|
|
174
186
|
Object.assign(properties, branch.properties);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (typeof name === "string")
|
|
178
|
-
required.add(name);
|
|
187
|
+
for (const name of requiredOf(branch))
|
|
188
|
+
required.add(name);
|
|
179
189
|
}
|
|
180
190
|
if (!Object.keys(properties).length)
|
|
181
191
|
return;
|
|
@@ -214,22 +224,16 @@ function mergeRootUnion(out) {
|
|
|
214
224
|
shared = new Set();
|
|
215
225
|
continue;
|
|
216
226
|
}
|
|
217
|
-
if (
|
|
227
|
+
if (isRecord(branch.properties))
|
|
218
228
|
Object.assign(properties, branch.properties);
|
|
219
|
-
const names =
|
|
220
|
-
? branch.required.filter((name) => typeof name === "string")
|
|
221
|
-
: [];
|
|
229
|
+
const names = requiredOf(branch);
|
|
222
230
|
shared = previous === null ? new Set(names) : new Set(names.filter((n) => previous.has(n)));
|
|
223
231
|
}
|
|
224
232
|
if (!Object.keys(properties).length)
|
|
225
233
|
continue;
|
|
226
|
-
out.properties = { ...(
|
|
227
|
-
if (shared?.size)
|
|
228
|
-
|
|
229
|
-
? out.required.filter((name) => typeof name === "string")
|
|
230
|
-
: [];
|
|
231
|
-
out.required = [...new Set([...already, ...shared])];
|
|
232
|
-
}
|
|
234
|
+
out.properties = { ...(isRecord(out.properties) ? out.properties : {}), ...properties };
|
|
235
|
+
if (shared?.size)
|
|
236
|
+
out.required = [...new Set([...requiredOf(out), ...shared])];
|
|
233
237
|
}
|
|
234
238
|
}
|
|
235
239
|
/**
|
|
@@ -246,7 +250,7 @@ function collectRefs(node, into) {
|
|
|
246
250
|
collectRefs(item, into);
|
|
247
251
|
return;
|
|
248
252
|
}
|
|
249
|
-
if (!
|
|
253
|
+
if (!isRecord(node))
|
|
250
254
|
return;
|
|
251
255
|
for (const [key, value] of Object.entries(node)) {
|
|
252
256
|
if (key === "$ref" && typeof value === "string")
|
|
@@ -269,7 +273,7 @@ function collectRefs(node, into) {
|
|
|
269
273
|
* outside it still refers in.
|
|
270
274
|
*/
|
|
271
275
|
function pruneDefs(out) {
|
|
272
|
-
const pools =
|
|
276
|
+
const pools = POOL_KEYS.filter((key) => isRecord(out[key]));
|
|
273
277
|
if (!pools.length)
|
|
274
278
|
return;
|
|
275
279
|
const live = {};
|
|
@@ -282,7 +286,7 @@ function pruneDefs(out) {
|
|
|
282
286
|
continue;
|
|
283
287
|
const [, poolKey, name] = target;
|
|
284
288
|
const pool = out[poolKey];
|
|
285
|
-
if (!
|
|
289
|
+
if (!isRecord(pool) || !(name in pool))
|
|
286
290
|
continue;
|
|
287
291
|
live[poolKey] ??= new Set();
|
|
288
292
|
const names = live[poolKey];
|
|
@@ -317,24 +321,36 @@ function pruneDefs(out) {
|
|
|
317
321
|
function pruneRequired(out) {
|
|
318
322
|
if (!Array.isArray(out.required))
|
|
319
323
|
return;
|
|
320
|
-
const properties =
|
|
324
|
+
const properties = isRecord(out.properties) ? out.properties : {};
|
|
321
325
|
const kept = out.required.filter((name) => typeof name === "string" && name in properties);
|
|
322
326
|
if (kept.length)
|
|
323
327
|
out.required = kept;
|
|
324
328
|
else
|
|
325
329
|
delete out.required;
|
|
326
330
|
}
|
|
327
|
-
|
|
328
|
-
|
|
331
|
+
/**
|
|
332
|
+
* One JSON Schema as a strict server will accept it, for a schema that is not a tool's parameters.
|
|
333
|
+
*
|
|
334
|
+
* What `sanitizeTools` does to each tool, without the tool: a `response_format` schema goes through
|
|
335
|
+
* the same grammar converter a tool's parameters do, and a caller holding only the schema had to
|
|
336
|
+
* wrap it in a definition to get here. Nothing is remembered — the cache is keyed on a tool
|
|
337
|
+
* definition's identity, and a bare schema has no object that stands for it across requests.
|
|
338
|
+
*
|
|
339
|
+
* @param schema The schema as written. Never mutated. Its root is held to an object, as a tool's
|
|
340
|
+
* parameters are, and anything that is not a schema at all comes back as an object with no
|
|
341
|
+
* properties.
|
|
342
|
+
*/
|
|
343
|
+
export function sanitizeSchema(schema) {
|
|
344
|
+
if (!isRecord(schema))
|
|
329
345
|
return EMPTY_OBJECT();
|
|
330
|
-
const out = normalize(inlineRootRef(
|
|
346
|
+
const out = normalize(inlineRootRef(schema));
|
|
331
347
|
mergeRootAllOf(out);
|
|
332
348
|
mergeRootUnion(out);
|
|
333
349
|
for (const key of TOP_LEVEL_COMBINATORS)
|
|
334
350
|
delete out[key];
|
|
335
351
|
if (out.type !== "object")
|
|
336
352
|
out.type = "object";
|
|
337
|
-
if (!
|
|
353
|
+
if (!isRecord(out.properties))
|
|
338
354
|
out.properties = {};
|
|
339
355
|
pruneRequired(out);
|
|
340
356
|
pruneDefs(out);
|
|
@@ -386,7 +402,7 @@ const through = (cache, tools, fn) => tools.map((tool) => {
|
|
|
386
402
|
* @param tools The definitions as the pool hands them over. Never mutated — where a schema
|
|
387
403
|
* changed, a new definition is returned in its place.
|
|
388
404
|
*/
|
|
389
|
-
export const sanitizeTools = (tools) => through(sanitized, tools,
|
|
405
|
+
export const sanitizeTools = (tools) => through(sanitized, tools, sanitizeSchema);
|
|
390
406
|
/**
|
|
391
407
|
* Walked as a schema rather than as arbitrary JSON, because `pattern` and `format` are keyword
|
|
392
408
|
* names and perfectly ordinary argument names at once. Matching on the key alone deleted a
|
|
@@ -394,29 +410,33 @@ export const sanitizeTools = (tools) => through(sanitized, tools, sanitizeParame
|
|
|
394
410
|
* argument that no longer existed — which every strict validator rejects, so the retry produced
|
|
395
411
|
* the failure it was reaching for. The same distinction keeps the walk out of `default`, `enum`
|
|
396
412
|
* and `const`, whose contents are data, not schema.
|
|
397
|
-
*
|
|
398
|
-
* At module scope rather than inside `relaxTools`, so the closure is made once rather than per
|
|
399
|
-
* call — which, on the path this is on, is per request.
|
|
400
413
|
*/
|
|
401
414
|
const strip = (node) => {
|
|
402
415
|
if (Array.isArray(node))
|
|
403
416
|
return node.map(strip);
|
|
404
|
-
if (!
|
|
417
|
+
if (!isRecord(node))
|
|
405
418
|
return node;
|
|
406
419
|
const out = {};
|
|
407
420
|
for (const [key, value] of Object.entries(node)) {
|
|
408
421
|
if (key === "pattern" || key === "format")
|
|
409
422
|
continue;
|
|
410
|
-
|
|
411
|
-
out[key] = Array.isArray(value) ? value.map(strip) : strip(value);
|
|
412
|
-
else if (SCHEMA_MAPS.has(key) && isObject(value))
|
|
413
|
-
// The keys here are argument names; only the values are schemas.
|
|
414
|
-
out[key] = Object.fromEntries(Object.entries(value).map(([name, sub]) => [name, strip(sub)]));
|
|
415
|
-
else
|
|
416
|
-
out[key] = value;
|
|
423
|
+
out[key] = mapChildren(key, value, strip);
|
|
417
424
|
}
|
|
418
425
|
return out;
|
|
419
426
|
};
|
|
427
|
+
/**
|
|
428
|
+
* One schema without its `pattern` and `format` keywords, for a schema that is not a tool's.
|
|
429
|
+
*
|
|
430
|
+
* The retry `relaxTools` makes, on a bare schema. Only the keywords go: a property that happens to
|
|
431
|
+
* be called `format` is an argument name and stays.
|
|
432
|
+
*
|
|
433
|
+
* @param schema Already sanitised. Relaxing is the retry, not a substitute for `sanitizeSchema`.
|
|
434
|
+
* Never mutated; anything that is not a schema comes back as an object with no properties.
|
|
435
|
+
*/
|
|
436
|
+
export function relaxSchema(schema) {
|
|
437
|
+
const stripped = strip(schema);
|
|
438
|
+
return isRecord(stripped) ? stripped : EMPTY_OBJECT();
|
|
439
|
+
}
|
|
420
440
|
/**
|
|
421
441
|
* The retry shape: llama.cpp's converter rejects regex escape classes (`\d`, `\w`, `\s`) in
|
|
422
442
|
* `pattern` and most `format` values, both of which only ever narrowed a string the tool
|
|
@@ -424,10 +444,7 @@ const strip = (node) => {
|
|
|
424
444
|
*
|
|
425
445
|
* @param tools Already sanitised. Relaxing is the retry, not a substitute for `sanitizeTools`.
|
|
426
446
|
*/
|
|
427
|
-
export const relaxTools = (tools) => through(relaxed, tools,
|
|
428
|
-
const stripped = strip(parameters);
|
|
429
|
-
return isObject(stripped) ? stripped : EMPTY_OBJECT();
|
|
430
|
-
});
|
|
447
|
+
export const relaxTools = (tools) => through(relaxed, tools, relaxSchema);
|
|
431
448
|
/**
|
|
432
449
|
* Qwen chat templates raise this when the transcript has no user turn. Some servers wrap it
|
|
433
450
|
* in the same "unable to generate parser" wording as a real schema failure, and stripping
|