@inflexa-ai/harness 0.19.0 → 0.19.2
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/dist/agents/report-session-agent.d.ts +32 -9
- package/dist/agents/report-session-agent.js +22 -6
- package/dist/app/report-session-runtime.js +26 -0
- package/dist/app/spawn-report-session.d.ts +32 -5
- package/dist/app/spawn-report-session.js +19 -0
- package/dist/auth/local-run-authorizer.d.ts +3 -2
- package/dist/auth/local-run-authorizer.js +4 -2
- package/dist/config/environment-stores.d.ts +12 -7
- package/dist/config/resource-limits.d.ts +6 -10
- package/dist/config/resource-limits.js +6 -44
- package/dist/contracts/report-reference.d.ts +4 -0
- package/dist/contracts/report-reference.js +3 -0
- package/dist/execution/run-authorizer.d.ts +18 -0
- package/dist/execution/run-canceler.d.ts +67 -0
- package/dist/execution/run-canceler.js +130 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +6 -1
- package/dist/lib/chrome.d.ts +14 -0
- package/dist/lib/chrome.js +7 -0
- package/dist/lib/page-capture.d.ts +47 -0
- package/dist/lib/page-capture.js +83 -0
- package/dist/loop/run-agent.js +104 -15
- package/dist/memory/ai-sdk-message-storage.d.ts +9 -4
- package/dist/memory/ai-sdk-message-storage.js +9 -4
- package/dist/memory/count-tokens.js +34 -5
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +21 -0
- package/dist/providers/ai-sdk.d.ts +81 -3
- package/dist/providers/ai-sdk.js +91 -10
- package/dist/providers/types.d.ts +17 -0
- package/dist/report-model/assert-rules.d.ts +44 -0
- package/dist/report-model/assert-rules.js +97 -0
- package/dist/report-model/draft-hash.d.ts +14 -0
- package/dist/report-model/draft-hash.js +36 -0
- package/dist/report-model/fixture-resolver.js +2 -71
- package/dist/report-model/production-resolver.d.ts +124 -0
- package/dist/report-model/production-resolver.js +801 -0
- package/dist/report-model/reference-resolver.d.ts +6 -0
- package/dist/report-model/validate.js +5 -0
- package/dist/report-render/assets.d.ts +42 -0
- package/dist/report-render/assets.js +61 -0
- package/dist/report-render/chart.js +31 -10
- package/dist/report-render/design.d.ts +120 -0
- package/dist/report-render/design.js +808 -0
- package/dist/report-render/page.d.ts +51 -111
- package/dist/report-render/page.js +158 -268
- package/dist/report-render/render.js +34 -11
- package/dist/report-render/views/chart-view.d.ts +6 -3
- package/dist/report-render/views/chart-view.js +10 -5
- package/dist/report-render/views/page-view.d.ts +19 -8
- package/dist/report-render/views/page-view.js +31 -11
- package/dist/report-render/views/prose.d.ts +1 -0
- package/dist/report-render/views/prose.js +7 -13
- package/dist/report-render/views/references-view.js +5 -5
- package/dist/report-render/views/values.d.ts +15 -6
- package/dist/report-render/views/values.js +29 -10
- package/dist/runtime/assemble.d.ts +38 -4
- package/dist/runtime/assemble.js +43 -3
- package/dist/sandbox/k8s-client.js +18 -0
- package/dist/sandbox/mount-plan.d.ts +7 -0
- package/dist/sandbox/mount-plan.js +7 -2
- package/dist/sandbox/types.d.ts +6 -0
- package/dist/state/analyses.d.ts +3 -26
- package/dist/state/analyses.js +6 -64
- package/dist/state/index.d.ts +2 -2
- package/dist/state/index.js +2 -2
- package/dist/state/init.js +23 -10
- package/dist/state/report-session-state.d.ts +27 -0
- package/dist/state/report-session-state.js +29 -2
- package/dist/state/runs.d.ts +10 -0
- package/dist/state/runs.js +21 -0
- package/dist/tasks/data-profile.d.ts +4 -0
- package/dist/tasks/data-profile.js +14 -0
- package/dist/tasks/extract-values-script.d.ts +64 -0
- package/dist/tasks/extract-values-script.js +180 -0
- package/dist/tasks/extract-values.d.ts +124 -0
- package/dist/tasks/extract-values.js +193 -0
- package/dist/tools/approval/gateway.d.ts +8 -1
- package/dist/tools/approval/gateway.js +9 -2
- package/dist/tools/approval/queries.d.ts +9 -2
- package/dist/tools/approval/queries.js +12 -4
- package/dist/tools/define-tool.d.ts +29 -0
- package/dist/tools/define-tool.js +26 -0
- package/dist/tools/report/preview-snapshot.d.ts +1 -1
- package/dist/tools/report/preview-snapshot.js +11 -50
- package/dist/tools/report-authoring/authoring-tools.d.ts +54 -4
- package/dist/tools/report-authoring/authoring-tools.js +1 -1
- package/dist/tools/report-session/examine-page.d.ts +90 -0
- package/dist/tools/report-session/examine-page.js +136 -0
- package/dist/tools/report-session/index.d.ts +3 -1
- package/dist/tools/report-session/index.js +3 -1
- package/dist/tools/report-session/preview-report.d.ts +31 -6
- package/dist/tools/report-session/preview-report.js +63 -16
- package/dist/tools/report-session/record-version.d.ts +102 -0
- package/dist/tools/report-session/record-version.js +139 -0
- package/dist/tools/sandbox/list-available-packages.js +2 -1
- package/dist/tools/sandbox/list-available-refs.d.ts +5 -1
- package/dist/tools/sandbox/list-available-refs.js +13 -6
- package/dist/workflows/sandbox-step.d.ts +4 -0
- package/dist/workflows/sandbox-step.js +21 -2
- package/package.json +14 -8
|
@@ -73,8 +73,9 @@ export function isInterruptedMessage(message) {
|
|
|
73
73
|
return message.providerOptions?.[HARNESS_PROVIDER_NAMESPACE]?.[INTERRUPTED_MESSAGE_KEY] === true;
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
|
-
* Build a `user` message the LOOP synthesized rather than a human typing it —
|
|
77
|
-
* continues a reply the model truncated at its output-token limit
|
|
76
|
+
* Build a `user` message the LOOP synthesized rather than a human typing it — the nudge that
|
|
77
|
+
* continues a reply the model truncated at its output-token limit, and the message that carries a
|
|
78
|
+
* tool picture that the tool result cannot carry.
|
|
78
79
|
*
|
|
79
80
|
* It has to carry the `user` role because the wire format requires a user turn after a truncated
|
|
80
81
|
* assistant message, but it is not user input, and the difference is load-bearing: a `user` message is
|
|
@@ -82,14 +83,18 @@ export function isInterruptedMessage(message) {
|
|
|
82
83
|
* splitting one turn into two for the token window and, worse, giving a tail-turn removal a cut point in
|
|
83
84
|
* the middle of a turn. Marking it is what lets {@link isSyntheticUserMessage} keep those readers honest.
|
|
84
85
|
*
|
|
86
|
+
* `content` takes prose or the parts of a user message, because a picture rides a file part and no
|
|
87
|
+
* string can carry one. The marker is the same either way: what makes a message synthetic is who wrote
|
|
88
|
+
* it, not what it holds.
|
|
89
|
+
*
|
|
85
90
|
* This one is loop MACHINERY and is not rendered: showing it would put words on screen nobody typed.
|
|
86
91
|
* An embedder recording out-of-band work — an analysis run's outcome — wants
|
|
87
92
|
* {@link syntheticRecordMessage} instead, which is equally non-turn-opening but IS displayed.
|
|
88
93
|
*/
|
|
89
|
-
export function syntheticUserMessage(
|
|
94
|
+
export function syntheticUserMessage(content) {
|
|
90
95
|
return {
|
|
91
96
|
role: "user",
|
|
92
|
-
content
|
|
97
|
+
content,
|
|
93
98
|
providerOptions: { [HARNESS_PROVIDER_NAMESPACE]: { [SYNTHETIC_MESSAGE_KEY]: true } },
|
|
94
99
|
};
|
|
95
100
|
}
|
|
@@ -17,10 +17,39 @@ function enc() {
|
|
|
17
17
|
return encoder;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* The token-bearing text of one
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* The token-bearing text of one tool-result output.
|
|
21
|
+
*
|
|
22
|
+
* A `content` output holds the text parts and the attachments side by side. Only
|
|
23
|
+
* the text is prompt text. A file rides the wire as an attachment, and the
|
|
24
|
+
* provider bills it at its own rate, thus its bytes count as `0` — the same rule
|
|
25
|
+
* as a top-level `file` block. A file that is stringified into the count inflates
|
|
26
|
+
* the row of the message by tens of thousands of tokens, and the load window then
|
|
27
|
+
* drops the true history.
|
|
28
|
+
*
|
|
29
|
+
* Each other output arm is plain JSON, thus it is stringified whole.
|
|
30
|
+
*/
|
|
31
|
+
function toolResultText(output) {
|
|
32
|
+
if (typeof output !== "object" || output === null)
|
|
33
|
+
return JSON.stringify(output ?? {});
|
|
34
|
+
const out = output;
|
|
35
|
+
if (out.type !== "content" || !Array.isArray(out.value))
|
|
36
|
+
return JSON.stringify(out);
|
|
37
|
+
const texts = [];
|
|
38
|
+
for (const item of out.value) {
|
|
39
|
+
if (typeof item !== "object" || item === null)
|
|
40
|
+
continue;
|
|
41
|
+
const nested = item;
|
|
42
|
+
if (nested.type === "text" && typeof nested.text === "string")
|
|
43
|
+
texts.push(nested.text);
|
|
44
|
+
}
|
|
45
|
+
return texts.join(" ");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The token-bearing text of one content block. A text-carrying field is
|
|
49
|
+
* extracted directly. A `tool_use` input is JSON-stringified, and a
|
|
50
|
+
* `tool_result` output goes through {@link toolResultText}. A signed `thinking`
|
|
51
|
+
* block counts only its reasoning text, because the opaque `signature` is
|
|
52
|
+
* metadata and not prompt tokens.
|
|
24
53
|
*/
|
|
25
54
|
function tokenizableText(block) {
|
|
26
55
|
if (typeof block === "string")
|
|
@@ -40,7 +69,7 @@ function tokenizableText(block) {
|
|
|
40
69
|
case "tool-call":
|
|
41
70
|
return `${String(part.toolName ?? "")} ${JSON.stringify(part.input ?? {})}`;
|
|
42
71
|
case "tool-result": {
|
|
43
|
-
return `${String(part.toolName ?? "")} ${
|
|
72
|
+
return `${String(part.toolName ?? "")} ${toolResultText(part.output)}`;
|
|
44
73
|
}
|
|
45
74
|
default:
|
|
46
75
|
return JSON.stringify(part);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const reportSessionPrompt = "# Report Builder\n\nYou compose a scientific report for the user, one block at a time, and you ground\nevery claim in the analysis. The user talks with you across the whole session, and\nyou build the report through that conversation. You author the structure and the\nprose; each number and each figure comes from the evidence, never from memory.\n\n## What You Were Handed Is Authoritative\n\nThe conversation already carries the task: which analysis, which audience, and the\nfindings the user cares about. Start from what is in front of you. Reach for a read\ntool only where the context is thin, and reach in a targeted way:\n\n- `inspect_data_profile` \u2014 what the dataset is: its design, its organism, its\n per-file types. Read it when you must state a fact about the data itself.\n- `inspect_run` \u2014 what a run produced, and where its outputs are. Read it when you\n must ground a claim in a run's results.\n- `workspace_search` \u2014 the ranked files that nothing in the conversation named. It\n gives descriptions and metadata, not contents.\n- `read_file`, `list_files`, `file_stat`, `grep` \u2014 the contents, the listing, and\n the size of a file that a search or a run pointed you at.\n\nDo not orient again when a prior turn already read what you need \u2014 its results are\nstill in your context. To reach further is targeted, not a fresh sweep.\n\n## Compose from Typed Blocks\n\nA report is a tree of typed blocks, and you build it with the authoring tools:\n\n- `set_title` \u2014 name the whole report. A draft starts with none, and it needs one.\n- `add_block` \u2014 add one section, or one atom inside a section. You choose the id,\n and it is unique in the draft. A section holds atoms. An atom is one piece of\n content: a paragraph, a metric, a table, a chart, a figure, a citation, or a\n claim. The block schema of `add_block` gives every kind and its fields.\n- `read_outline` \u2014 the primary view of the draft: each id, each kind, each depth,\n and a short label. Read the outline, not the whole draft, to see where you are.\n- `read_block` \u2014 one block in full, when its outline label is not enough.\n\nBuild the report as a shape first, then fill each section. Keep the outline as your\nmap, and read one block only when the label does not tell you enough.\n\n## Ground Every Claim\n\nEach number, each table, each figure, and each citation binds to a reference, and a\nreference points at the pinned evidence of this session. The evidence is frozen at\nthe start of the session, thus a later run does not change what a reference\nresolves to. Bind every value. Never transcribe a number that you remember.\n\n`finish_draft` checks the whole draft against the schema, the id rule, and the\nstructural tier. It returns each completeness gap, or the finished document. Read\nthe gaps, repair the draft, and finish again.\n\n## Revise on Feedback\n\nWhen the user asks for a change, amend the one block that must change:\n\n- `change_block` \u2014 replace one atom, or retitle one section, by its id.\n- `move_block` \u2014 move one block to a new place by its id.\n- `remove_block` \u2014 remove one block, and its subtree, by its id.\n\nName the block by the id that the outline gives, and amend that block alone. Do not\nrebuild the report to make one change.\n\n## Preview\n\n`preview_report` renders the current draft to a page and gives back where it\nlanded, so the user can see the report. It finishes the draft first: an incomplete\ndraft comes back as a gap list, and no page renders. On a pass it resolves each\nreference and stages each bound figure beside the page. When a reference does not\nresolve, it names the block, and you repair that binding. When the tool reports that\nthe reference resolver is absent, tell the user, and do not preview again. Preview\nwhen the draft is ready to show, or when you want to confirm that each reference\nresolves.\n\n## Do NOT\n\n- **Transcribe a number from memory.** Every metric, every table cell, and every\n figure binds to a reference that resolves against the pinned evidence.\n- **Start a run, or change the analysis.** You read the analysis; you never run it\n and never write to it. You hold no tool that does either, and that is by design.\n- **Invent a path.** Name a file by what a search or a run gave you. Never guess a\n location, and never hardcode one.\n- **Reach outside the pinned evidence.** A reference binds to the frozen snapshot\n of this session alone. Do not cite an artifact that the snapshot does not hold.\n- **Rebuild when one amend serves.** One feedback is one block change. Change, move,\n or remove that block by its id, and do not re-author the report.\n- **Leave a gap unread.** When `finish_draft` or `preview_report` reports a gap or\n an unresolved reference, repair it. Do not present a report that does not finish.\n";
|
|
1
|
+
export declare const reportSessionPrompt = "# Report Builder\n\nYou compose a scientific report for the user, one block at a time, and you ground\nevery claim in the analysis. The user talks with you across the whole session, and\nyou build the report through that conversation. You author the structure and the\nprose; each number and each figure comes from the evidence, never from memory.\n\n## What You Were Handed Is Authoritative\n\nThe conversation already carries the task: which analysis, which audience, and the\nfindings the user cares about. Start from what is in front of you. Reach for a read\ntool only where the context is thin, and reach in a targeted way:\n\n- `inspect_data_profile` \u2014 what the dataset is: its design, its organism, its\n per-file types. Read it when you must state a fact about the data itself.\n- `inspect_run` \u2014 what a run produced, and where its outputs are. Read it when you\n must ground a claim in a run's results.\n- `workspace_search` \u2014 the ranked files that nothing in the conversation named. It\n gives descriptions and metadata, not contents.\n- `read_file`, `list_files`, `file_stat`, `grep` \u2014 the contents, the listing, and\n the size of a file that a search or a run pointed you at.\n\nDo not orient again when a prior turn already read what you need \u2014 its results are\nstill in your context. To reach further is targeted, not a fresh sweep.\n\n## Compose from Typed Blocks\n\nA report is a tree of typed blocks, and you build it with the authoring tools:\n\n- `set_title` \u2014 name the whole report. A draft starts with none, and it needs one.\n- `add_block` \u2014 add one section, or one atom inside a section. You choose the id,\n and it is unique in the draft. A section holds atoms. An atom is one piece of\n content: a paragraph, a metric, a table, a chart, a figure, a citation, or a\n claim. The block schema of `add_block` gives every kind and its fields.\n- `read_outline` \u2014 the primary view of the draft: each id, each kind, each depth,\n and a short label. Read the outline, not the whole draft, to see where you are.\n- `read_block` \u2014 one block in full, when its outline label is not enough.\n\nBuild the report as a shape first, then fill each section. Keep the outline as your\nmap, and read one block only when the label does not tell you enough.\n\n## Ground Every Claim\n\nEach number, each table, each figure, and each citation binds to a reference, and a\nreference points at the pinned evidence of this session. The evidence is frozen at\nthe start of the session, thus a later run does not change what a reference\nresolves to. Bind every value. Never transcribe a number that you remember.\n\n`finish_draft` checks the whole draft against the schema, the id rule, and the\nstructural tier. It returns each completeness gap, or the finished document. Read\nthe gaps, repair the draft, and finish again.\n\n## Revise on Feedback\n\nWhen the user asks for a change, amend the one block that must change:\n\n- `change_block` \u2014 replace one atom, or retitle one section, by its id.\n- `move_block` \u2014 move one block to a new place by its id.\n- `remove_block` \u2014 remove one block, and its subtree, by its id.\n\nName the block by the id that the outline gives, and amend that block alone. Do not\nrebuild the report to make one change.\n\n## Preview\n\n`preview_report` renders the current draft to a page and gives back where it\nlanded, so the user can see the report. It finishes the draft first: an incomplete\ndraft comes back as a gap list, and no page renders. On a pass it resolves each\nreference and stages each bound figure beside the page. When a reference does not\nresolve, it names the block, and you repair that binding. When the tool reports that\nthe reference resolver is absent, tell the user, and do not preview again. Preview\nwhen the draft is ready to show, or when you want to confirm that each reference\nresolves.\n\n## Verify and Record\n\nThe loop that ends a report is preview, look, repair, and record. Run it in order:\n\n- `preview_report` renders the current draft to a page. The Preview section above\n gives its rules.\n- `examine_page` opens the rendered page in a real browser. It gives back a\n screenshot, the console errors, and the failed requests. Look at the page.\n- Repair each fault that the look shows. A broken layout, an absent chart, and a\n failed request each name a block or a binding that you repair.\n- `record_report_version` records the report as one version. It records only after\n you look at the current page. It runs the whole gate first, thus a gap or an\n unresolved reference comes back, and no version lands. A thread holds one version.\n\nLook again after each repair, because a repair changes the page. Record when the\npage reads clean.\n\n## Do NOT\n\n- **Transcribe a number from memory.** Every metric, every table cell, and every\n figure binds to a reference that resolves against the pinned evidence.\n- **Start a run, or change the analysis.** You read the analysis; you never run it\n and never write to it. You hold no tool that does either, and that is by design.\n- **Invent a path.** Name a file by what a search or a run gave you. Never guess a\n location, and never hardcode one.\n- **Reach outside the pinned evidence.** A reference binds to the frozen snapshot\n of this session alone. Do not cite an artifact that the snapshot does not hold.\n- **Rebuild when one amend serves.** One feedback is one block change. Change, move,\n or remove that block by its id, and do not re-author the report.\n- **Leave a gap unread.** When `finish_draft` or `preview_report` reports a gap or\n an unresolved reference, repair it. Do not present a report that does not finish.\n- **Spiral on a cosmetic doubt.** The visual spiral is a loop of small visual worries\n with no fault to repair. Look one time, then repair a real fault: a broken layout, an\n absent chart, or a failed request. A matter of taste is not a fault. When the page\n reads clean, record.\n";
|
|
@@ -72,6 +72,23 @@ the reference resolver is absent, tell the user, and do not preview again. Previ
|
|
|
72
72
|
when the draft is ready to show, or when you want to confirm that each reference
|
|
73
73
|
resolves.
|
|
74
74
|
|
|
75
|
+
## Verify and Record
|
|
76
|
+
|
|
77
|
+
The loop that ends a report is preview, look, repair, and record. Run it in order:
|
|
78
|
+
|
|
79
|
+
- \`preview_report\` renders the current draft to a page. The Preview section above
|
|
80
|
+
gives its rules.
|
|
81
|
+
- \`examine_page\` opens the rendered page in a real browser. It gives back a
|
|
82
|
+
screenshot, the console errors, and the failed requests. Look at the page.
|
|
83
|
+
- Repair each fault that the look shows. A broken layout, an absent chart, and a
|
|
84
|
+
failed request each name a block or a binding that you repair.
|
|
85
|
+
- \`record_report_version\` records the report as one version. It records only after
|
|
86
|
+
you look at the current page. It runs the whole gate first, thus a gap or an
|
|
87
|
+
unresolved reference comes back, and no version lands. A thread holds one version.
|
|
88
|
+
|
|
89
|
+
Look again after each repair, because a repair changes the page. Record when the
|
|
90
|
+
page reads clean.
|
|
91
|
+
|
|
75
92
|
## Do NOT
|
|
76
93
|
|
|
77
94
|
- **Transcribe a number from memory.** Every metric, every table cell, and every
|
|
@@ -86,4 +103,8 @@ resolves.
|
|
|
86
103
|
or remove that block by its id, and do not re-author the report.
|
|
87
104
|
- **Leave a gap unread.** When \`finish_draft\` or \`preview_report\` reports a gap or
|
|
88
105
|
an unresolved reference, repair it. Do not present a report that does not finish.
|
|
106
|
+
- **Spiral on a cosmetic doubt.** The visual spiral is a loop of small visual worries
|
|
107
|
+
with no fault to repair. Look one time, then repair a real fault: a broken layout, an
|
|
108
|
+
absent chart, or a failed request. A matter of taste is not a fault. When the page
|
|
109
|
+
reads clean, record.
|
|
89
110
|
`;
|
|
@@ -23,6 +23,11 @@ export declare const RETRY_MAX_DELAY_MS = 30000;
|
|
|
23
23
|
*
|
|
24
24
|
* Lower it per-config for servers that validate `prompt + max_tokens <= context`
|
|
25
25
|
* (vLLM and similar) against a small-context model.
|
|
26
|
+
*
|
|
27
|
+
* The openai arm takes no value from this default. `@ai-sdk/openai` clamps
|
|
28
|
+
* nothing per model, and it puts the number on the wire as it is. Thus a model
|
|
29
|
+
* whose own cap is smaller answers 400 on each call. That arm sends no ceiling,
|
|
30
|
+
* and the server then enforces the true cap of the model.
|
|
26
31
|
*/
|
|
27
32
|
export declare const DEFAULT_MAX_OUTPUT_TOKENS = 64000;
|
|
28
33
|
export interface AiSdkProviderDeps {
|
|
@@ -30,8 +35,14 @@ export interface AiSdkProviderDeps {
|
|
|
30
35
|
readonly resolveBilling: ResolveBilling;
|
|
31
36
|
readonly capabilities?: Partial<ProviderCapabilities>;
|
|
32
37
|
readonly logger?: Logger;
|
|
33
|
-
/**
|
|
34
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Output-token ceiling per request. Defaults to
|
|
40
|
+
* {@link DEFAULT_MAX_OUTPUT_TOKENS}. The value `"provider-maximum"` sends the
|
|
41
|
+
* SDK no ceiling, and the server then enforces the true cap of the model. Use
|
|
42
|
+
* the sentinel for a wire whose SDK puts the number on the request as it is,
|
|
43
|
+
* because a ceiling above the cap of the model fails the call.
|
|
44
|
+
*/
|
|
45
|
+
readonly maxOutputTokens?: number | "provider-maximum";
|
|
35
46
|
/** The retry limit of the harness envelope. Defaults to {@link RETRY_MAX_RETRIES}. */
|
|
36
47
|
readonly maxRetries?: number;
|
|
37
48
|
/**
|
|
@@ -45,7 +56,7 @@ export interface AiSdkProviderDeps {
|
|
|
45
56
|
}
|
|
46
57
|
/**
|
|
47
58
|
* Connection + model configuration for one `ChatProvider`, discriminated by
|
|
48
|
-
* wire protocol (`anthropic` | `openai-compatible`). The `model` is the wire
|
|
59
|
+
* wire protocol (`anthropic` | `openai` | `openai-compatible`). The `model` is the wire
|
|
49
60
|
* model bound into the provider at construction: `ChatRequest` carries no model
|
|
50
61
|
* field (see `./types.ts`), so a config value describes exactly one model over
|
|
51
62
|
* one connection. An embedder running distinct models builds one provider per
|
|
@@ -83,6 +94,73 @@ export type AiSdkProviderConfig = {
|
|
|
83
94
|
* keeps the default of 10.
|
|
84
95
|
*/
|
|
85
96
|
readonly maxRetries?: number;
|
|
97
|
+
} | {
|
|
98
|
+
readonly kind: "openai";
|
|
99
|
+
readonly baseURL?: string;
|
|
100
|
+
readonly apiKey: string;
|
|
101
|
+
readonly model: string;
|
|
102
|
+
readonly fetch?: FetchLike;
|
|
103
|
+
readonly capabilities?: Partial<ProviderCapabilities>;
|
|
104
|
+
/**
|
|
105
|
+
* Output-token ceiling per request. An unset value sends no ceiling, and
|
|
106
|
+
* the model cap of the server governs the reply. The Responses wire holds
|
|
107
|
+
* no low default, thus the omission cannot truncate a reply. Set a number
|
|
108
|
+
* to hold the reply under a lower ceiling than the cap of the model.
|
|
109
|
+
*/
|
|
110
|
+
readonly maxOutputTokens?: number;
|
|
111
|
+
/**
|
|
112
|
+
* The bound for each silent interval of a request attempt, in
|
|
113
|
+
* milliseconds. The value bounds the wait until the response starts, and
|
|
114
|
+
* each gap between two content chunks of a stream, per attempt. A chunk
|
|
115
|
+
* that carries no content, for example a keep-alive comment, does not
|
|
116
|
+
* reset the gap bound. When the field is absent, the provider installs no
|
|
117
|
+
* guard, and its behavior does not change.
|
|
118
|
+
*
|
|
119
|
+
* The harness lifts the transport floor of Bun: the guard adds
|
|
120
|
+
* `timeout: false` to the fetch init, thus the idle cut of 300 seconds
|
|
121
|
+
* does not apply. Under Node the floor of undici stays. Thus a Node
|
|
122
|
+
* embedder that sets a value above 300 seconds supplies a `fetch` whose
|
|
123
|
+
* dispatcher raises that limit.
|
|
124
|
+
*/
|
|
125
|
+
readonly requestTimeoutMs?: number;
|
|
126
|
+
/**
|
|
127
|
+
* The retry limit of the harness envelope. The envelope uses this value
|
|
128
|
+
* in place of the default limit. When the field is absent, the envelope
|
|
129
|
+
* keeps the default of 10.
|
|
130
|
+
*/
|
|
131
|
+
readonly maxRetries?: number;
|
|
132
|
+
/**
|
|
133
|
+
* NOTICE: this value is the retention directive of the Responses wire.
|
|
134
|
+
*
|
|
135
|
+
* An unset value lets the OpenAI server keep the response for 30 days.
|
|
136
|
+
* The dashboard logs show that response, and a caller can retrieve it by
|
|
137
|
+
* id. Thus the arm always sends an explicit value. The default is
|
|
138
|
+
* `false`, and an analysis payload does not persist at OpenAI.
|
|
139
|
+
*
|
|
140
|
+
* An unset value is also not neutral inside the SDK. When the SDK sees
|
|
141
|
+
* no explicit `false`, its input converter assumes storage. It then
|
|
142
|
+
* replaces a round-tripped item with an `item_reference` entry. Such a
|
|
143
|
+
* reference fails with a 404 under Zero Data Retention, and it fails
|
|
144
|
+
* after the 30-day expiry.
|
|
145
|
+
*
|
|
146
|
+
* An explicit `false` selects the stateless reasoning path of the SDK.
|
|
147
|
+
* The SDK adds `include: ["reasoning.encrypted_content"]` for a
|
|
148
|
+
* reasoning-family model id. It captures the blob on `providerMetadata`,
|
|
149
|
+
* and it replays the blob from the history of the thread. The SDK strips
|
|
150
|
+
* a reasoning part that lost its blob, and it warns. That warning reaches
|
|
151
|
+
* the harness logger through the SDK warning hook.
|
|
152
|
+
*
|
|
153
|
+
* One thread must keep one mode. The wire captures the history of a turn
|
|
154
|
+
* under the mode of that turn. If a later turn replays a history of the
|
|
155
|
+
* `false` mode under storage, the SDK emits a reference onto an item that
|
|
156
|
+
* the server never stored.
|
|
157
|
+
*
|
|
158
|
+
* An operator can override the default with `true`. The dashboard then
|
|
159
|
+
* shows the trail of the thread, and a caller can retrieve a response by
|
|
160
|
+
* id. The harness does not use `previous_response_id`, and it does not
|
|
161
|
+
* use `conversation`. This is true for the two modes.
|
|
162
|
+
*/
|
|
163
|
+
readonly store?: boolean;
|
|
86
164
|
} | {
|
|
87
165
|
readonly kind: "openai-compatible";
|
|
88
166
|
readonly name: string;
|
package/dist/providers/ai-sdk.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { generateText, streamText, wrapLanguageModel, } from "ai";
|
|
2
2
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
3
4
|
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
4
5
|
import { APICallError } from "@ai-sdk/provider";
|
|
5
6
|
import { retryWithExponentialBackoff } from "@ai-sdk/provider-utils";
|
|
@@ -28,6 +29,11 @@ export const RETRY_MAX_DELAY_MS = 30_000;
|
|
|
28
29
|
*
|
|
29
30
|
* Lower it per-config for servers that validate `prompt + max_tokens <= context`
|
|
30
31
|
* (vLLM and similar) against a small-context model.
|
|
32
|
+
*
|
|
33
|
+
* The openai arm takes no value from this default. `@ai-sdk/openai` clamps
|
|
34
|
+
* nothing per model, and it puts the number on the wire as it is. Thus a model
|
|
35
|
+
* whose own cap is smaller answers 400 on each call. That arm sends no ceiling,
|
|
36
|
+
* and the server then enforces the true cap of the model.
|
|
31
37
|
*/
|
|
32
38
|
export const DEFAULT_MAX_OUTPUT_TOKENS = 64_000;
|
|
33
39
|
/**
|
|
@@ -370,9 +376,20 @@ function abortedStreamFailure(signal, reason, requestTimeoutMs) {
|
|
|
370
376
|
return new Error(reason ?? "The provider aborted the stream.");
|
|
371
377
|
}
|
|
372
378
|
export function createAiSdkProvider(deps) {
|
|
373
|
-
const capabilities = {
|
|
379
|
+
const capabilities = {
|
|
380
|
+
toolCalling: deps.capabilities?.toolCalling ?? true,
|
|
381
|
+
// Propagate a picture flag only when the caller states it. An absent
|
|
382
|
+
// flag stays absent, thus the loop degrades to the next placement for a
|
|
383
|
+
// wire that carries no picture there.
|
|
384
|
+
...(deps.capabilities?.imageToolResults !== undefined ? { imageToolResults: deps.capabilities.imageToolResults } : {}),
|
|
385
|
+
...(deps.capabilities?.imageUserMessages !== undefined ? { imageUserMessages: deps.capabilities.imageUserMessages } : {}),
|
|
386
|
+
};
|
|
374
387
|
const logger = (deps.logger ?? createNoopLogger()).named("providers.ai-sdk");
|
|
375
388
|
const maxOutputTokens = deps.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
|
|
389
|
+
// The sentinel hands the SDK no ceiling, thus the request carries none and the
|
|
390
|
+
// server enforces the cap of the model. One binding feeds the call and the
|
|
391
|
+
// stream, thus the two cannot disagree about the ceiling of a request.
|
|
392
|
+
const outputCeiling = maxOutputTokens === "provider-maximum" ? {} : { maxOutputTokens };
|
|
376
393
|
const maxRetries = deps.maxRetries ?? RETRY_MAX_RETRIES;
|
|
377
394
|
const requestTimeoutMs = deps.requestTimeoutMs;
|
|
378
395
|
const requestedModelId = requestedModelIdOf(deps.model);
|
|
@@ -392,7 +409,7 @@ export function createAiSdkProvider(deps) {
|
|
|
392
409
|
messages: sanitizeMessages(req.messages),
|
|
393
410
|
tools: req.tools,
|
|
394
411
|
toolChoice: req.toolChoice ?? "auto",
|
|
395
|
-
|
|
412
|
+
...outputCeiling,
|
|
396
413
|
stopWhen: [],
|
|
397
414
|
// The harness envelope owns retries; leaving the SDK default in
|
|
398
415
|
// place would multiply attempts (10 outer × 3 inner).
|
|
@@ -436,17 +453,19 @@ export function createAiSdkProvider(deps) {
|
|
|
436
453
|
messages: sanitizeMessages(req.messages),
|
|
437
454
|
tools: req.tools,
|
|
438
455
|
toolChoice: req.toolChoice ?? "auto",
|
|
439
|
-
|
|
456
|
+
...outputCeiling,
|
|
440
457
|
stopWhen: [],
|
|
441
458
|
maxRetries: 0,
|
|
442
459
|
headers,
|
|
443
460
|
abortSignal: signal,
|
|
444
|
-
// The
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
|
|
461
|
+
// The fetch guard bounds the wait until the headers. Past the
|
|
462
|
+
// headers the SDK owns two bounds: `firstChunkMs` covers the wait
|
|
463
|
+
// for the first content chunk, and `chunkMs` covers each later
|
|
464
|
+
// gap. The two take the same value, thus each silent interval of
|
|
465
|
+
// the stream has the one configured bound. A chunk that carries
|
|
466
|
+
// no content, for example a keep-alive comment, produces no part,
|
|
467
|
+
// thus it feeds neither bound.
|
|
468
|
+
...(requestTimeoutMs !== undefined ? { timeout: { firstChunkMs: requestTimeoutMs, chunkMs: requestTimeoutMs } } : {}),
|
|
450
469
|
providerOptions: req.providerOptions,
|
|
451
470
|
});
|
|
452
471
|
const iterator = result.fullStream[Symbol.asyncIterator]();
|
|
@@ -581,6 +600,28 @@ export function wrapFetchWithRequestTimeout(fetchImpl, requestTimeoutMs) {
|
|
|
581
600
|
}
|
|
582
601
|
};
|
|
583
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* Bind one explicit `store` directive onto each call of a model.
|
|
605
|
+
*
|
|
606
|
+
* The middleware merges the value into the `openai` namespace of
|
|
607
|
+
* `providerOptions`. It keeps each other namespace, and it keeps each other key
|
|
608
|
+
* of the `openai` namespace, thus a cache directive of a different vendor rides
|
|
609
|
+
* through untouched. The arm value wins over a request-level `store`, because
|
|
610
|
+
* the retention mode belongs to the connection and not to one turn. A thread
|
|
611
|
+
* that mixes the two modes replays a reference onto an item that the server
|
|
612
|
+
* never stored.
|
|
613
|
+
*/
|
|
614
|
+
function withStoreDirective(model, store) {
|
|
615
|
+
return wrapLanguageModel({
|
|
616
|
+
model,
|
|
617
|
+
middleware: {
|
|
618
|
+
transformParams: async ({ params }) => ({
|
|
619
|
+
...params,
|
|
620
|
+
providerOptions: { ...params.providerOptions, openai: { ...params.providerOptions?.openai, store } },
|
|
621
|
+
}),
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
}
|
|
584
625
|
/**
|
|
585
626
|
* Realize an `AiSdkProviderConfig` into a `ChatProvider` bound to that config's
|
|
586
627
|
* connection and model. The model is closed into the returned provider here (it
|
|
@@ -604,16 +645,56 @@ export function createConfiguredAiSdkProvider(deps) {
|
|
|
604
645
|
apiKey: config.apiKey,
|
|
605
646
|
fetch: effectiveFetch,
|
|
606
647
|
});
|
|
648
|
+
// The Anthropic Messages wire renders an image block inside a
|
|
649
|
+
// tool_result, thus the loop can send a picture. A custom baseURL can
|
|
650
|
+
// front a different backend that stringifies the block. Thus the
|
|
651
|
+
// harness asserts the capability only for the default endpoint. An
|
|
652
|
+
// absent flag means "cannot carry", thus a config with a custom baseURL
|
|
653
|
+
// must declare the capability. A config value overrides this default in
|
|
654
|
+
// both directions.
|
|
655
|
+
const pictureDefault = config.baseURL === undefined ? { imageToolResults: true } : {};
|
|
607
656
|
return createAiSdkProvider({
|
|
608
657
|
model: provider.chat(config.model),
|
|
609
658
|
resolveBilling: deps.resolveBilling,
|
|
610
|
-
capabilities: config.capabilities,
|
|
659
|
+
capabilities: { ...pictureDefault, ...config.capabilities },
|
|
611
660
|
logger: deps.logger,
|
|
612
661
|
...(config.maxOutputTokens !== undefined ? { maxOutputTokens: config.maxOutputTokens } : {}),
|
|
613
662
|
...(config.maxRetries !== undefined ? { maxRetries: config.maxRetries } : {}),
|
|
614
663
|
...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}),
|
|
615
664
|
});
|
|
616
665
|
}
|
|
666
|
+
if (config.kind === "openai") {
|
|
667
|
+
const provider = createOpenAI({
|
|
668
|
+
baseURL: config.baseURL,
|
|
669
|
+
apiKey: config.apiKey,
|
|
670
|
+
fetch: effectiveFetch,
|
|
671
|
+
});
|
|
672
|
+
// The Responses wire renders an image block inside a function-call
|
|
673
|
+
// output, thus the loop can send a picture. A custom baseURL can front
|
|
674
|
+
// a different backend that stringifies the block. Thus the harness
|
|
675
|
+
// asserts the capability only for the default endpoint. An absent flag
|
|
676
|
+
// means "cannot carry", thus a config with a custom baseURL must
|
|
677
|
+
// declare the capability. A config value overrides this default in both
|
|
678
|
+
// directions.
|
|
679
|
+
const pictureDefault = config.baseURL === undefined ? { imageToolResults: true } : {};
|
|
680
|
+
return createAiSdkProvider({
|
|
681
|
+
// The explicit Responses binding pins the wire here. The bare
|
|
682
|
+
// factory callable of the package resolves to the same path today,
|
|
683
|
+
// but nothing in the package holds it there.
|
|
684
|
+
model: withStoreDirective(provider.responses(config.model), config.store ?? false),
|
|
685
|
+
resolveBilling: deps.resolveBilling,
|
|
686
|
+
capabilities: { ...pictureDefault, ...config.capabilities },
|
|
687
|
+
logger: deps.logger,
|
|
688
|
+
// The package puts this number on the wire as it is, and it clamps
|
|
689
|
+
// nothing per model. Thus the shared default of 64000 breaks each call
|
|
690
|
+
// to a model with a smaller cap, with a 400 that no retry can pass. An
|
|
691
|
+
// unset config sends no ceiling, and the server holds the reply at the
|
|
692
|
+
// cap of the model.
|
|
693
|
+
maxOutputTokens: config.maxOutputTokens ?? "provider-maximum",
|
|
694
|
+
...(config.maxRetries !== undefined ? { maxRetries: config.maxRetries } : {}),
|
|
695
|
+
...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}),
|
|
696
|
+
});
|
|
697
|
+
}
|
|
617
698
|
const provider = createOpenAICompatible({
|
|
618
699
|
name: config.name,
|
|
619
700
|
baseURL: config.baseURL,
|
|
@@ -13,6 +13,23 @@ import type { ProviderError } from "./errors.js";
|
|
|
13
13
|
export type { FinishReason, LanguageModel, ModelMessage, ProviderOptions, ToolSet };
|
|
14
14
|
export interface ProviderCapabilities {
|
|
15
15
|
readonly toolCalling: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the wire can carry a picture inside a tool result. The loop moves a
|
|
18
|
+
* tool's picture into an image content block on the tool-result message. A
|
|
19
|
+
* wire that renders that block sets this flag. A wire that stringifies the
|
|
20
|
+
* block into text leaves it absent, and the loop then drops the picture and
|
|
21
|
+
* keeps the text, because base64 text floods the context and the model cannot
|
|
22
|
+
* see it. Absent means "cannot carry", never "unknown".
|
|
23
|
+
*/
|
|
24
|
+
readonly imageToolResults?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Whether the wire can carry a picture inside a user message. The loop uses
|
|
27
|
+
* this placement only when the tool result cannot carry the picture. The tool
|
|
28
|
+
* result then keeps its JSON text, and one user message after the tool message
|
|
29
|
+
* of the round carries the bytes. Absent means "cannot carry", never
|
|
30
|
+
* "unknown".
|
|
31
|
+
*/
|
|
32
|
+
readonly imageUserMessages?: boolean;
|
|
16
33
|
}
|
|
17
34
|
export interface ChatRequest {
|
|
18
35
|
readonly system: string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The assert rules of the value tier. A resolver reads a fresh value, and these functions match it
|
|
3
|
+
* against the authored belief.
|
|
4
|
+
*
|
|
5
|
+
* Each realization of the resolver calls these functions. Thus one semantics exists, and the fixture
|
|
6
|
+
* stays the executable specification of the value tier.
|
|
7
|
+
*/
|
|
8
|
+
import type { Reference, UnresolvedReference } from "../contracts/report-reference.js";
|
|
9
|
+
/**
|
|
10
|
+
* Read an authored value or a resolved cell as a finite number.
|
|
11
|
+
*
|
|
12
|
+
* A text-backed artifact such as a CSV holds every cell as a string, thus a numeric column arrives as
|
|
13
|
+
* `"0.01"` and not as `0.01`. A comparison that respects the JavaScript type would fail an author who
|
|
14
|
+
* states the number, and the arithmetic of a derivation could never run over a real CSV.
|
|
15
|
+
*
|
|
16
|
+
* The parse reads the whole string, thus `"12 genes"` stays text and never reads as 12. `Number` accepts
|
|
17
|
+
* the exponent form that a p-value uses, for example `"1.2e-45"`. A blank string is not a number, because
|
|
18
|
+
* `Number("")` gives 0 and an empty cell must never read as zero.
|
|
19
|
+
*/
|
|
20
|
+
export declare function asFiniteNumber(value: string | number): number | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Match a row-filter value against a resolved cell.
|
|
23
|
+
*
|
|
24
|
+
* A text-backed artifact such as a CSV holds every cell as a string, thus a filter value of `3` must
|
|
25
|
+
* select the cell `"3"`. The compare reads both sides as a finite number, and it selects the row where the
|
|
26
|
+
* two numbers are equal. A side that is not a finite number compares by exact value, thus a text key
|
|
27
|
+
* selects by its exact text. The match is exact and carries no tolerance, because a filter names a
|
|
28
|
+
* discrete key and not a computed value with float noise.
|
|
29
|
+
*/
|
|
30
|
+
export declare function cellMatchesFilterValue(cell: string | number | null | undefined, filterValue: string | number): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Match a resolved value against an authored one.
|
|
33
|
+
*
|
|
34
|
+
* The comparison is numeric whenever both sides read as a finite number, thus the CSV cell `"0.01"`
|
|
35
|
+
* matches the number 0.01 that an author states. An authored tolerance is an absolute difference, and it
|
|
36
|
+
* is the author's own statement of how close is close enough. With no tolerance the comparison is
|
|
37
|
+
* relative, because an exact match would fail on the float noise of a computed value. Any other pair
|
|
38
|
+
* matches on exact equality.
|
|
39
|
+
*/
|
|
40
|
+
export declare function valuesMatch(expected: string | number, actual: string | number, tolerance: number | undefined): boolean;
|
|
41
|
+
/** Match a resolved scalar against the authored value, under the authored tolerance. */
|
|
42
|
+
export declare function checkValueAssertion(reference: Reference, expected: string | number | undefined, tolerance: number | undefined, resolved: string | number): UnresolvedReference | undefined;
|
|
43
|
+
/** Match the resolved citation key against the authored value. The key carries its `idKind:` prefix. */
|
|
44
|
+
export declare function checkCitationAssertion(reference: Reference, expected: string | undefined, resolved: string): UnresolvedReference | undefined;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The assert rules of the value tier. A resolver reads a fresh value, and these functions match it
|
|
3
|
+
* against the authored belief.
|
|
4
|
+
*
|
|
5
|
+
* Each realization of the resolver calls these functions. Thus one semantics exists, and the fixture
|
|
6
|
+
* stays the executable specification of the value tier.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The relative epsilon of a value match with no authored tolerance.
|
|
10
|
+
*
|
|
11
|
+
* Float arithmetic shifts a computed value in its last bits. For example, `0.3 - 0.1` gives
|
|
12
|
+
* 0.19999999999999998, and exact equality would report that noise as a fabrication.
|
|
13
|
+
*
|
|
14
|
+
* The epsilon absorbs the noise and nothing else. It is far below the difference that a rounded figure
|
|
15
|
+
* makes, thus an author who writes 0.05 against a true 0.0499 still fails. An author who wants a rounded
|
|
16
|
+
* figure to pass states a `tolerance`, which is the one place where the intent belongs.
|
|
17
|
+
*/
|
|
18
|
+
const RELATIVE_EPSILON = 1e-9;
|
|
19
|
+
/**
|
|
20
|
+
* Read an authored value or a resolved cell as a finite number.
|
|
21
|
+
*
|
|
22
|
+
* A text-backed artifact such as a CSV holds every cell as a string, thus a numeric column arrives as
|
|
23
|
+
* `"0.01"` and not as `0.01`. A comparison that respects the JavaScript type would fail an author who
|
|
24
|
+
* states the number, and the arithmetic of a derivation could never run over a real CSV.
|
|
25
|
+
*
|
|
26
|
+
* The parse reads the whole string, thus `"12 genes"` stays text and never reads as 12. `Number` accepts
|
|
27
|
+
* the exponent form that a p-value uses, for example `"1.2e-45"`. A blank string is not a number, because
|
|
28
|
+
* `Number("")` gives 0 and an empty cell must never read as zero.
|
|
29
|
+
*/
|
|
30
|
+
export function asFiniteNumber(value) {
|
|
31
|
+
if (typeof value === "number") {
|
|
32
|
+
return Number.isFinite(value) ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
if (value.trim() === "") {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const parsed = Number(value);
|
|
38
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Match a row-filter value against a resolved cell.
|
|
42
|
+
*
|
|
43
|
+
* A text-backed artifact such as a CSV holds every cell as a string, thus a filter value of `3` must
|
|
44
|
+
* select the cell `"3"`. The compare reads both sides as a finite number, and it selects the row where the
|
|
45
|
+
* two numbers are equal. A side that is not a finite number compares by exact value, thus a text key
|
|
46
|
+
* selects by its exact text. The match is exact and carries no tolerance, because a filter names a
|
|
47
|
+
* discrete key and not a computed value with float noise.
|
|
48
|
+
*/
|
|
49
|
+
export function cellMatchesFilterValue(cell, filterValue) {
|
|
50
|
+
if (cell === null || cell === undefined) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const cellNumber = asFiniteNumber(cell);
|
|
54
|
+
const filterNumber = asFiniteNumber(filterValue);
|
|
55
|
+
if (cellNumber !== undefined && filterNumber !== undefined) {
|
|
56
|
+
return cellNumber === filterNumber;
|
|
57
|
+
}
|
|
58
|
+
return cell === filterValue;
|
|
59
|
+
}
|
|
60
|
+
/** Name a value with its type, so that a failed match never reads as two identical values. */
|
|
61
|
+
function describeValue(value) {
|
|
62
|
+
return typeof value === "string" ? `the string ${JSON.stringify(value)}` : `the number ${value}`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Match a resolved value against an authored one.
|
|
66
|
+
*
|
|
67
|
+
* The comparison is numeric whenever both sides read as a finite number, thus the CSV cell `"0.01"`
|
|
68
|
+
* matches the number 0.01 that an author states. An authored tolerance is an absolute difference, and it
|
|
69
|
+
* is the author's own statement of how close is close enough. With no tolerance the comparison is
|
|
70
|
+
* relative, because an exact match would fail on the float noise of a computed value. Any other pair
|
|
71
|
+
* matches on exact equality.
|
|
72
|
+
*/
|
|
73
|
+
export function valuesMatch(expected, actual, tolerance) {
|
|
74
|
+
const expectedNumber = asFiniteNumber(expected);
|
|
75
|
+
const actualNumber = asFiniteNumber(actual);
|
|
76
|
+
if (expectedNumber !== undefined && actualNumber !== undefined) {
|
|
77
|
+
if (tolerance !== undefined) {
|
|
78
|
+
return Math.abs(expectedNumber - actualNumber) <= tolerance;
|
|
79
|
+
}
|
|
80
|
+
return Math.abs(expectedNumber - actualNumber) <= RELATIVE_EPSILON * Math.max(1, Math.abs(expectedNumber), Math.abs(actualNumber));
|
|
81
|
+
}
|
|
82
|
+
return expected === actual;
|
|
83
|
+
}
|
|
84
|
+
/** Match a resolved scalar against the authored value, under the authored tolerance. */
|
|
85
|
+
export function checkValueAssertion(reference, expected, tolerance, resolved) {
|
|
86
|
+
if (expected === undefined || valuesMatch(expected, resolved, tolerance)) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
return { reference, reason: "assertion-failed", detail: `expected ${describeValue(expected)} but resolved ${describeValue(resolved)}` };
|
|
90
|
+
}
|
|
91
|
+
/** Match the resolved citation key against the authored value. The key carries its `idKind:` prefix. */
|
|
92
|
+
export function checkCitationAssertion(reference, expected, resolved) {
|
|
93
|
+
if (expected === undefined || expected === resolved) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
return { reference, reason: "assertion-failed", detail: `expected ${String(expected)} but resolved citation ${resolved}` };
|
|
97
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one hash of a draft document.
|
|
3
|
+
*
|
|
4
|
+
* The look-before-record rule rides two durable markers, and each marker holds a hash of the draft. A
|
|
5
|
+
* stamp writes a hash, and a compare reads one. Both call this one function, thus two serializations of one
|
|
6
|
+
* document cannot split.
|
|
7
|
+
*
|
|
8
|
+
* The function serializes the draft with sorted object keys, and it hashes the text with sha256. The sort
|
|
9
|
+
* makes the hash independent of key order. Thus two objects with the same values and a different key order
|
|
10
|
+
* give one hash, and a marker survives a round trip through storage.
|
|
11
|
+
*/
|
|
12
|
+
import type { DraftDocument } from "./draft.js";
|
|
13
|
+
/** The sha256 hash of the draft, over a JSON serialization with sorted keys. */
|
|
14
|
+
export declare function computeDraftHash(document: DraftDocument): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one hash of a draft document.
|
|
3
|
+
*
|
|
4
|
+
* The look-before-record rule rides two durable markers, and each marker holds a hash of the draft. A
|
|
5
|
+
* stamp writes a hash, and a compare reads one. Both call this one function, thus two serializations of one
|
|
6
|
+
* document cannot split.
|
|
7
|
+
*
|
|
8
|
+
* The function serializes the draft with sorted object keys, and it hashes the text with sha256. The sort
|
|
9
|
+
* makes the hash independent of key order. Thus two objects with the same values and a different key order
|
|
10
|
+
* give one hash, and a marker survives a round trip through storage.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
/**
|
|
14
|
+
* Give back a copy of a value with each object key in sorted order, at every depth.
|
|
15
|
+
*
|
|
16
|
+
* An array keeps its order, because the order of an array is data. An object gets its keys in one canonical
|
|
17
|
+
* order, thus the serialization does not depend on the order that a producer wrote the keys.
|
|
18
|
+
*/
|
|
19
|
+
function canonicalize(value) {
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return value.map(canonicalize);
|
|
22
|
+
}
|
|
23
|
+
if (value !== null && typeof value === "object") {
|
|
24
|
+
const sorted = {};
|
|
25
|
+
for (const key of Object.keys(value).sort()) {
|
|
26
|
+
sorted[key] = canonicalize(value[key]);
|
|
27
|
+
}
|
|
28
|
+
return sorted;
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
/** The sha256 hash of the draft, over a JSON serialization with sorted keys. */
|
|
33
|
+
export function computeDraftHash(document) {
|
|
34
|
+
const canonical = JSON.stringify(canonicalize(document));
|
|
35
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
36
|
+
}
|