@loomwork/llm 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +80 -2
- package/dist/assembleStream.d.ts +95 -0
- package/dist/assembleStream.d.ts.map +1 -0
- package/dist/assembleStream.js +180 -0
- package/dist/assembleStream.js.map +1 -0
- package/dist/capabilities.d.ts +43 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +77 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/createLlmEffect.d.ts +110 -0
- package/dist/createLlmEffect.d.ts.map +1 -0
- package/dist/createLlmEffect.js +177 -0
- package/dist/createLlmEffect.js.map +1 -0
- package/dist/errors.d.ts +96 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +280 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +71 -0
- package/dist/index.js.map +1 -0
- package/dist/parts.d.ts +220 -0
- package/dist/parts.d.ts.map +1 -0
- package/dist/parts.js +231 -0
- package/dist/parts.js.map +1 -0
- package/dist/providers/AnthropicProvider.d.ts +53 -0
- package/dist/providers/AnthropicProvider.d.ts.map +1 -0
- package/dist/providers/AnthropicProvider.js +443 -0
- package/dist/providers/AnthropicProvider.js.map +1 -0
- package/dist/providers/GeminiProvider.d.ts +66 -0
- package/dist/providers/GeminiProvider.d.ts.map +1 -0
- package/dist/providers/GeminiProvider.js +281 -0
- package/dist/providers/GeminiProvider.js.map +1 -0
- package/dist/providers/OpenAiProvider.d.ts +56 -0
- package/dist/providers/OpenAiProvider.d.ts.map +1 -0
- package/dist/providers/OpenAiProvider.js +402 -0
- package/dist/providers/OpenAiProvider.js.map +1 -0
- package/dist/providers/extraHeaders.d.ts +7 -0
- package/dist/providers/extraHeaders.d.ts.map +1 -0
- package/dist/providers/extraHeaders.js +40 -0
- package/dist/providers/extraHeaders.js.map +1 -0
- package/dist/providers/systemPrompt.d.ts +10 -0
- package/dist/providers/systemPrompt.d.ts.map +1 -0
- package/dist/providers/systemPrompt.js +22 -0
- package/dist/providers/systemPrompt.js.map +1 -0
- package/dist/reasoning.d.ts +154 -0
- package/dist/reasoning.d.ts.map +1 -0
- package/dist/reasoning.js +256 -0
- package/dist/reasoning.js.map +1 -0
- package/dist/resolveContent.d.ts +74 -0
- package/dist/resolveContent.d.ts.map +1 -0
- package/dist/resolveContent.js +335 -0
- package/dist/resolveContent.js.map +1 -0
- package/dist/types.d.ts +152 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/untrusted.d.ts +91 -0
- package/dist/untrusted.d.ts.map +1 -0
- package/dist/untrusted.js +385 -0
- package/dist/untrusted.js.map +1 -0
- package/package.json +54 -3
- package/src/assembleStream.ts +219 -0
- package/src/capabilities.ts +96 -0
- package/src/createLlmEffect.ts +287 -0
- package/src/errors.ts +269 -0
- package/src/index.ts +137 -0
- package/src/parts.ts +409 -0
- package/src/providers/AnthropicProvider.ts +550 -0
- package/src/providers/GeminiProvider.ts +380 -0
- package/src/providers/OpenAiProvider.ts +511 -0
- package/src/providers/extraHeaders.ts +41 -0
- package/src/providers/systemPrompt.ts +19 -0
- package/src/reasoning.ts +386 -0
- package/src/resolveContent.ts +425 -0
- package/src/types.ts +175 -0
- package/src/untrusted.ts +440 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { type TaintedValue } from "@loomwork/classify";
|
|
2
|
+
import type { EffectContext } from "@loomwork/effect-runtime";
|
|
3
|
+
import type { LlmReasoning, LlmReasoningKind, LlmReasoningPart } from "./parts";
|
|
4
|
+
import type { ContentResolver } from "./resolveContent";
|
|
5
|
+
/** The event the capture emits (userspace: the fold ignores it). */
|
|
6
|
+
export declare const LLM_REASONING_CAPTURED = "LLM_REASONING_CAPTURED";
|
|
7
|
+
/**
|
|
8
|
+
* Write and read view of the sidecar store (structural: a `ContentStore` satisfies it, with no
|
|
9
|
+
* package dependency). Content-addressed and idempotent by hash, so a crash-recovery rerun that
|
|
10
|
+
* produces the same bytes stores nothing new.
|
|
11
|
+
*/
|
|
12
|
+
export interface ReasoningStore {
|
|
13
|
+
put(bytes: Uint8Array): Promise<{
|
|
14
|
+
readonly __loomContent: string;
|
|
15
|
+
}>;
|
|
16
|
+
get(ref: {
|
|
17
|
+
readonly __loomContent: string;
|
|
18
|
+
}): Promise<Uint8Array | null>;
|
|
19
|
+
}
|
|
20
|
+
/** Read-only view, for {@link readReasoning}. */
|
|
21
|
+
export type ReasoningReader = Pick<ReasoningStore, "get">;
|
|
22
|
+
export interface LlmReasoningOptions {
|
|
23
|
+
/** Where readable text is kept. Without one, text is dropped and only signatures are recorded. */
|
|
24
|
+
store?: ReasoningStore;
|
|
25
|
+
/** Default: on iff a store is configured. `false` is the explicit opt-out. */
|
|
26
|
+
capture?: boolean | ((ctx: EffectContext) => boolean);
|
|
27
|
+
/** Deliver readable reasoning deltas live (D6): through the per-call sink under a runtime pipe, else to `onDelta`. Off by default. */
|
|
28
|
+
live?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The configured live sink for reasoning, the analogue of `createLlmEffect`'s text `onDelta`
|
|
31
|
+
* (a transport such as `ConversationHub.reasoningSink()`). Called only when `live` is on and the
|
|
32
|
+
* call is streamed, with the increment still WRAPPED as untrusted content
|
|
33
|
+
* (`llm-reasoning:<stepId>`): the sink unwraps it at its own transport edge, never earlier.
|
|
34
|
+
* Never journaled; not called on replay.
|
|
35
|
+
*/
|
|
36
|
+
onDelta?: (delta: TaintedValue<string>, ctx: EffectContext) => void;
|
|
37
|
+
/** Recorded on the audit event (the provider SPI does not expose its model id). */
|
|
38
|
+
model?: string;
|
|
39
|
+
/**
|
|
40
|
+
* An AUTHORIZING resolver for a reasoning part's `textRef` (the ADR 0120 `ContentResolver` seam,
|
|
41
|
+
* called with `origin: "reasoning"` and the call's executionId / stepId / principal). It returns the
|
|
42
|
+
* readable text's bytes, or `null` for absent / not yours; a throw is an outage and fails the call.
|
|
43
|
+
* Default: {@link reasoningStoreResolver} over `store`, which serves only blobs sealed to the
|
|
44
|
+
* calling execution.
|
|
45
|
+
*/
|
|
46
|
+
resolve?: ContentResolver;
|
|
47
|
+
}
|
|
48
|
+
/** One part of an `LLM_REASONING_CAPTURED` payload: references and digests, never text. */
|
|
49
|
+
export interface CapturedReasoningPart {
|
|
50
|
+
kind: LlmReasoningKind;
|
|
51
|
+
ref?: string;
|
|
52
|
+
digest?: string;
|
|
53
|
+
size?: number;
|
|
54
|
+
}
|
|
55
|
+
export interface LlmReasoningCaptured {
|
|
56
|
+
stepId: string;
|
|
57
|
+
attempt: number;
|
|
58
|
+
provider: string;
|
|
59
|
+
model?: string;
|
|
60
|
+
parts: CapturedReasoningPart[];
|
|
61
|
+
}
|
|
62
|
+
/** The bytes stored for `text`: sealed to `executionId`. */
|
|
63
|
+
export declare function sealReasoningText(text: string, executionId: string): Uint8Array;
|
|
64
|
+
/** The text bytes inside a sealed blob, or `null` when it is not sealed to `executionId` (or is not a sealed blob). */
|
|
65
|
+
export declare function openReasoningText(bytes: Uint8Array, executionId: string): Uint8Array | null;
|
|
66
|
+
/** The default reasoning resolver: reads the sidecar and serves a blob only to the execution it is sealed to. */
|
|
67
|
+
export declare function reasoningStoreResolver(store: ReasoningReader): ContentResolver;
|
|
68
|
+
/** SHA-256 hex of the canonical UTF-8 bytes of `text`. */
|
|
69
|
+
export declare function reasoningDigest(bytes: Uint8Array): string;
|
|
70
|
+
/** Whether capture is on for this call: an explicit choice wins, else on iff a store is configured. */
|
|
71
|
+
export declare function captureOn(options: LlmReasoningOptions | undefined, ctx: EffectContext): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Reduce a response's reasoning to its recorded parts and (when anything was captured) emit the
|
|
74
|
+
* audit event. Pure with respect to the response; the only side effects are the store `put`s and
|
|
75
|
+
* the one `ctx.emit`. A failing `put` throws: audit evidence that cannot be kept fails the step
|
|
76
|
+
* (and retries it) instead of silently recording less than was configured.
|
|
77
|
+
*/
|
|
78
|
+
export declare function captureReasoning(reasoning: readonly LlmReasoning[], options: LlmReasoningOptions | undefined, ctx: EffectContext): Promise<LlmReasoningPart[]>;
|
|
79
|
+
/**
|
|
80
|
+
* ADR 0133 D2 - the text-free form of anything reasoning-shaped: a recorded part loses any `text`
|
|
81
|
+
* it carries; a raw {@link LlmReasoning} becomes a part with kind, provider and signature, flagged
|
|
82
|
+
* `omitted` when it had readable text (the text was not kept, so it cannot be continued). For a
|
|
83
|
+
* layer that sits between a provider and a journal (a decision, a cache) and has no sidecar:
|
|
84
|
+
* readable reasoning must reach a sidecar via `createLlmEffect`, or nowhere.
|
|
85
|
+
*/
|
|
86
|
+
export declare function textFreeReasoning(reasoning: readonly (LlmReasoning | LlmReasoningPart)[]): LlmReasoningPart[];
|
|
87
|
+
/** The minimal log view `readReasoning` needs: an `EventStore` satisfies it. */
|
|
88
|
+
export interface ReasoningLogReader {
|
|
89
|
+
read(executionId: string): Promise<readonly {
|
|
90
|
+
eventType: string;
|
|
91
|
+
payload: unknown;
|
|
92
|
+
}[]>;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* `redacted`: the vendor withheld this block (opaque data in `signature`, never any text), so there is
|
|
96
|
+
* nothing to verify or erase; it is NOT `ok` (which would read as "readable and checked").
|
|
97
|
+
*/
|
|
98
|
+
export type ReasoningPartStatus = "ok" | "redacted" | "erased" | "digest-mismatch";
|
|
99
|
+
export interface ReasoningPartRead {
|
|
100
|
+
kind: LlmReasoningKind;
|
|
101
|
+
provider: string;
|
|
102
|
+
status: ReasoningPartStatus;
|
|
103
|
+
/** Present only when `status` is `"ok"` and the part had readable text; wrapped as untrusted. */
|
|
104
|
+
text?: TaintedValue<string>;
|
|
105
|
+
}
|
|
106
|
+
export interface ReasoningAttemptRead {
|
|
107
|
+
attempt: number;
|
|
108
|
+
provider: string;
|
|
109
|
+
model?: string;
|
|
110
|
+
/** This attempt's value is the one the step recorded (the last capture before `STEP_COMPLETED`). */
|
|
111
|
+
recorded: boolean;
|
|
112
|
+
parts: ReasoningPartRead[];
|
|
113
|
+
}
|
|
114
|
+
export interface ReasoningStepRead {
|
|
115
|
+
stepId: string;
|
|
116
|
+
/** In log order; superseded attempts are kept. */
|
|
117
|
+
attempts: ReasoningAttemptRead[];
|
|
118
|
+
/** The attempt whose value the step recorded, when the step completed. */
|
|
119
|
+
recordedAttempt?: number;
|
|
120
|
+
}
|
|
121
|
+
/** Which steps to read and how much: every bound is applied BEFORE a blob is fetched. */
|
|
122
|
+
export interface ReasoningQuery {
|
|
123
|
+
executionId: string;
|
|
124
|
+
/** Exactly this step. */
|
|
125
|
+
stepId?: string;
|
|
126
|
+
/** Only steps whose id starts with this (a conversation turn's steps), filtered before any `store.get`. */
|
|
127
|
+
stepPrefix?: string;
|
|
128
|
+
}
|
|
129
|
+
/** Bounds for {@link readReasoningBounded}. Reading stops at the first one reached. */
|
|
130
|
+
export interface ReasoningReadLimits {
|
|
131
|
+
/** Most parts read (each part with a stored text costs one `store.get`). */
|
|
132
|
+
maxParts?: number;
|
|
133
|
+
/** Most readable-text bytes read in total. */
|
|
134
|
+
maxBytes?: number;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Join `LLM_REASONING_CAPTURED` events to their blobs. Each part is checked against its digest:
|
|
138
|
+
* an absent blob reports `erased`, an altered one `digest-mismatch`. Readable text comes back
|
|
139
|
+
* wrapped as untrusted content (origin `llm-reasoning:<stepId>`), so it cannot be put in a prompt
|
|
140
|
+
* instruction position. Grouped by step with every attempt; the attempt whose value the step
|
|
141
|
+
* recorded is the last capture before that step's `STEP_COMPLETED`.
|
|
142
|
+
*/
|
|
143
|
+
export declare function readReasoning(log: ReasoningLogReader, store: ReasoningReader, query: ReasoningQuery): Promise<ReasoningStepRead[]>;
|
|
144
|
+
/**
|
|
145
|
+
* {@link readReasoning} for an endpoint: the same join, but bounded. `stepId` / `stepPrefix` select
|
|
146
|
+
* events before any blob is fetched, and `limits` cap the parts and the readable bytes read; when a
|
|
147
|
+
* cap is reached the first parts (in log order) come back with `truncated: true` and nothing past
|
|
148
|
+
* the cap is fetched. A part whose declared text size would cross `maxBytes` is neither read nor included.
|
|
149
|
+
*/
|
|
150
|
+
export declare function readReasoningBounded(log: ReasoningLogReader, store: ReasoningReader, query: ReasoningQuery, limits?: ReasoningReadLimits): Promise<{
|
|
151
|
+
steps: ReasoningStepRead[];
|
|
152
|
+
truncated: boolean;
|
|
153
|
+
}>;
|
|
154
|
+
//# sourceMappingURL=reasoning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reasoning.d.ts","sourceRoot":"","sources":["../src/reasoning.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LLM_REASONING_CAPTURED = void 0;
|
|
4
|
+
exports.sealReasoningText = sealReasoningText;
|
|
5
|
+
exports.openReasoningText = openReasoningText;
|
|
6
|
+
exports.reasoningStoreResolver = reasoningStoreResolver;
|
|
7
|
+
exports.reasoningDigest = reasoningDigest;
|
|
8
|
+
exports.captureOn = captureOn;
|
|
9
|
+
exports.captureReasoning = captureReasoning;
|
|
10
|
+
exports.textFreeReasoning = textFreeReasoning;
|
|
11
|
+
exports.readReasoning = readReasoning;
|
|
12
|
+
exports.readReasoningBounded = readReasoningBounded;
|
|
13
|
+
const node_crypto_1 = require("node:crypto");
|
|
14
|
+
const classify_1 = require("@loomwork/classify");
|
|
15
|
+
// ── ADR 0133 — model reasoning as audit evidence ─────────────────────────────
|
|
16
|
+
//
|
|
17
|
+
// The provider returns reasoning next to its answer. `captureReasoning` runs BEFORE `parse`
|
|
18
|
+
// and reduces it to its recorded form: kind, provider, signature and a reference to the readable
|
|
19
|
+
// text in a sidecar store. The text never reaches a step value; the audit fact is a userspace
|
|
20
|
+
// event naming references and digests, never text and never signatures. `readReasoning` joins
|
|
21
|
+
// those events back to their blobs for an auditor.
|
|
22
|
+
/** The event the capture emits (userspace: the fold ignores it). */
|
|
23
|
+
exports.LLM_REASONING_CAPTURED = "LLM_REASONING_CAPTURED";
|
|
24
|
+
const enc = new TextEncoder();
|
|
25
|
+
const dec = new TextDecoder();
|
|
26
|
+
// ── the execution seal (ADR 0133 D5, "who may resolve a ref") ─────────────────
|
|
27
|
+
//
|
|
28
|
+
// The sidecar is content-addressed and, like every ContentStore, has no tenancy of its own: whoever
|
|
29
|
+
// can name an address can read it, and a reasoning part's `textRef` rides in a transcript a caller
|
|
30
|
+
// may have assembled. So the stored bytes are SEALED to the execution that captured them (a tiny
|
|
31
|
+
// JSON envelope naming it), and the default resolver serves a blob only to that execution. A forged
|
|
32
|
+
// or copied ref to another execution's blob resolves to `null` (not yours), which the drop rule
|
|
33
|
+
// treats as a hole. The part's `digest` and the audit event's `digest`/`size` stay those of the
|
|
34
|
+
// readable text itself.
|
|
35
|
+
/** The bytes stored for `text`: sealed to `executionId`. */
|
|
36
|
+
function sealReasoningText(text, executionId) {
|
|
37
|
+
return enc.encode(JSON.stringify({ e: executionId, t: text }));
|
|
38
|
+
}
|
|
39
|
+
/** The text bytes inside a sealed blob, or `null` when it is not sealed to `executionId` (or is not a sealed blob). */
|
|
40
|
+
function openReasoningText(bytes, executionId) {
|
|
41
|
+
try {
|
|
42
|
+
const v = JSON.parse(dec.decode(bytes));
|
|
43
|
+
if (v === null || typeof v !== "object" || v.e !== executionId || typeof v.t !== "string") {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return enc.encode(v.t);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** The default reasoning resolver: reads the sidecar and serves a blob only to the execution it is sealed to. */
|
|
53
|
+
function reasoningStoreResolver(store) {
|
|
54
|
+
return async (ref, ctx) => {
|
|
55
|
+
const bytes = await store.get({ __loomContent: ref.address });
|
|
56
|
+
return bytes === null ? null : openReasoningText(bytes, ctx.executionId);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** SHA-256 hex of the canonical UTF-8 bytes of `text`. */
|
|
60
|
+
function reasoningDigest(bytes) {
|
|
61
|
+
return (0, node_crypto_1.createHash)("sha256").update(bytes).digest("hex");
|
|
62
|
+
}
|
|
63
|
+
/** Whether capture is on for this call: an explicit choice wins, else on iff a store is configured. */
|
|
64
|
+
function captureOn(options, ctx) {
|
|
65
|
+
if (options === undefined || options.store === undefined)
|
|
66
|
+
return false;
|
|
67
|
+
const c = options.capture;
|
|
68
|
+
return c === undefined ? true : typeof c === "function" ? c(ctx) : c;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Reduce a response's reasoning to its recorded parts and (when anything was captured) emit the
|
|
72
|
+
* audit event. Pure with respect to the response; the only side effects are the store `put`s and
|
|
73
|
+
* the one `ctx.emit`. A failing `put` throws: audit evidence that cannot be kept fails the step
|
|
74
|
+
* (and retries it) instead of silently recording less than was configured.
|
|
75
|
+
*/
|
|
76
|
+
async function captureReasoning(reasoning, options, ctx) {
|
|
77
|
+
const store = captureOn(options, ctx) ? options?.store : undefined;
|
|
78
|
+
const parts = [];
|
|
79
|
+
const audit = [];
|
|
80
|
+
for (const r of reasoning) {
|
|
81
|
+
const part = {
|
|
82
|
+
type: "reasoning",
|
|
83
|
+
kind: r.kind,
|
|
84
|
+
provider: r.provider,
|
|
85
|
+
...(r.signature !== undefined && { signature: r.signature }),
|
|
86
|
+
};
|
|
87
|
+
const text = r.text ?? "";
|
|
88
|
+
if (text !== "") {
|
|
89
|
+
if (store !== undefined) {
|
|
90
|
+
const bytes = enc.encode(text);
|
|
91
|
+
const ref = await store.put(sealReasoningText(text, ctx.executionId));
|
|
92
|
+
part.textRef = { address: ref.__loomContent, size: bytes.byteLength };
|
|
93
|
+
part.digest = reasoningDigest(bytes);
|
|
94
|
+
audit.push({
|
|
95
|
+
kind: r.kind,
|
|
96
|
+
ref: ref.__loomContent,
|
|
97
|
+
digest: part.digest,
|
|
98
|
+
size: bytes.byteLength,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Readable text existed and was not kept: the part cannot be reproduced for continuation.
|
|
103
|
+
part.omitted = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (store !== undefined) {
|
|
107
|
+
audit.push({ kind: r.kind });
|
|
108
|
+
}
|
|
109
|
+
parts.push(part);
|
|
110
|
+
}
|
|
111
|
+
if (store !== undefined && audit.length > 0) {
|
|
112
|
+
const payload = {
|
|
113
|
+
stepId: ctx.stepId,
|
|
114
|
+
attempt: ctx.attempt,
|
|
115
|
+
provider: reasoning[0]?.provider ?? "",
|
|
116
|
+
...(options?.model !== undefined && { model: options.model }),
|
|
117
|
+
parts: audit,
|
|
118
|
+
};
|
|
119
|
+
ctx.emit(exports.LLM_REASONING_CAPTURED, payload);
|
|
120
|
+
}
|
|
121
|
+
return parts;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* ADR 0133 D2 - the text-free form of anything reasoning-shaped: a recorded part loses any `text`
|
|
125
|
+
* it carries; a raw {@link LlmReasoning} becomes a part with kind, provider and signature, flagged
|
|
126
|
+
* `omitted` when it had readable text (the text was not kept, so it cannot be continued). For a
|
|
127
|
+
* layer that sits between a provider and a journal (a decision, a cache) and has no sidecar:
|
|
128
|
+
* readable reasoning must reach a sidecar via `createLlmEffect`, or nowhere.
|
|
129
|
+
*/
|
|
130
|
+
function textFreeReasoning(reasoning) {
|
|
131
|
+
return reasoning.map((r) => {
|
|
132
|
+
if (r.type === "reasoning") {
|
|
133
|
+
const { text: _text, ...recorded } = r;
|
|
134
|
+
return recorded;
|
|
135
|
+
}
|
|
136
|
+
const raw = r;
|
|
137
|
+
return {
|
|
138
|
+
type: "reasoning",
|
|
139
|
+
kind: raw.kind,
|
|
140
|
+
provider: raw.provider,
|
|
141
|
+
...(raw.signature !== undefined && { signature: raw.signature }),
|
|
142
|
+
...((raw.text ?? "") !== "" && { omitted: true }),
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Join `LLM_REASONING_CAPTURED` events to their blobs. Each part is checked against its digest:
|
|
148
|
+
* an absent blob reports `erased`, an altered one `digest-mismatch`. Readable text comes back
|
|
149
|
+
* wrapped as untrusted content (origin `llm-reasoning:<stepId>`), so it cannot be put in a prompt
|
|
150
|
+
* instruction position. Grouped by step with every attempt; the attempt whose value the step
|
|
151
|
+
* recorded is the last capture before that step's `STEP_COMPLETED`.
|
|
152
|
+
*/
|
|
153
|
+
async function readReasoning(log, store, query) {
|
|
154
|
+
return (await readReasoningBounded(log, store, query)).steps;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* {@link readReasoning} for an endpoint: the same join, but bounded. `stepId` / `stepPrefix` select
|
|
158
|
+
* events before any blob is fetched, and `limits` cap the parts and the readable bytes read; when a
|
|
159
|
+
* cap is reached the first parts (in log order) come back with `truncated: true` and nothing past
|
|
160
|
+
* the cap is fetched. A part whose declared text size would cross `maxBytes` is neither read nor included.
|
|
161
|
+
*/
|
|
162
|
+
async function readReasoningBounded(log, store, query, limits = {}) {
|
|
163
|
+
const maxParts = limits.maxParts ?? Number.POSITIVE_INFINITY;
|
|
164
|
+
const maxBytes = limits.maxBytes ?? Number.POSITIVE_INFINITY;
|
|
165
|
+
const events = await log.read(query.executionId);
|
|
166
|
+
const steps = new Map();
|
|
167
|
+
const latest = new Map();
|
|
168
|
+
let parts = 0;
|
|
169
|
+
let bytes = 0;
|
|
170
|
+
let truncated = false;
|
|
171
|
+
for (const ev of events) {
|
|
172
|
+
if (ev.eventType === exports.LLM_REASONING_CAPTURED) {
|
|
173
|
+
if (truncated)
|
|
174
|
+
continue;
|
|
175
|
+
const p = asCaptured(ev.payload);
|
|
176
|
+
if (p === undefined ||
|
|
177
|
+
(query.stepId !== undefined && p.stepId !== query.stepId) ||
|
|
178
|
+
(query.stepPrefix !== undefined && !p.stepId.startsWith(query.stepPrefix))) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const attempt = {
|
|
182
|
+
attempt: p.attempt,
|
|
183
|
+
provider: p.provider,
|
|
184
|
+
...(p.model !== undefined && { model: p.model }),
|
|
185
|
+
recorded: false,
|
|
186
|
+
parts: [],
|
|
187
|
+
};
|
|
188
|
+
for (const part of p.parts) {
|
|
189
|
+
if (parts >= maxParts) {
|
|
190
|
+
truncated = true;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
// The event's own `size` (the text's bytes) gates the fetch, so nothing past the cap is read.
|
|
194
|
+
const size = part.ref !== undefined ? (part.size ?? 0) : 0;
|
|
195
|
+
if (bytes + size > maxBytes) {
|
|
196
|
+
truncated = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
parts += 1;
|
|
200
|
+
bytes += size;
|
|
201
|
+
attempt.parts.push(await readPart(store, part, p.provider, p.stepId, query.executionId));
|
|
202
|
+
}
|
|
203
|
+
if (attempt.parts.length > 0 || !truncated) {
|
|
204
|
+
let step = steps.get(p.stepId);
|
|
205
|
+
if (step === undefined) {
|
|
206
|
+
step = { stepId: p.stepId, attempts: [] };
|
|
207
|
+
steps.set(p.stepId, step);
|
|
208
|
+
}
|
|
209
|
+
step.attempts.push(attempt);
|
|
210
|
+
latest.set(p.stepId, attempt);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (ev.eventType === "STEP_COMPLETED") {
|
|
214
|
+
const id = ev.payload?.stepId;
|
|
215
|
+
const attempt = typeof id === "string" ? latest.get(id) : undefined;
|
|
216
|
+
if (attempt !== undefined && typeof id === "string") {
|
|
217
|
+
attempt.recorded = true;
|
|
218
|
+
const step = steps.get(id);
|
|
219
|
+
if (step !== undefined)
|
|
220
|
+
step.recordedAttempt = attempt.attempt;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { steps: [...steps.values()], truncated };
|
|
225
|
+
}
|
|
226
|
+
function asCaptured(payload) {
|
|
227
|
+
const p = payload;
|
|
228
|
+
if (p === null ||
|
|
229
|
+
typeof p !== "object" ||
|
|
230
|
+
typeof p.stepId !== "string" ||
|
|
231
|
+
typeof p.attempt !== "number" ||
|
|
232
|
+
typeof p.provider !== "string" ||
|
|
233
|
+
!Array.isArray(p.parts)) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
return p;
|
|
237
|
+
}
|
|
238
|
+
async function readPart(store, part, provider, stepId, executionId) {
|
|
239
|
+
const base = { kind: part.kind, provider };
|
|
240
|
+
if (part.kind === "redacted")
|
|
241
|
+
return { ...base, status: "redacted" };
|
|
242
|
+
if (part.ref === undefined)
|
|
243
|
+
return { ...base, status: "ok" };
|
|
244
|
+
const stored = await store.get({ __loomContent: part.ref });
|
|
245
|
+
if (stored === null)
|
|
246
|
+
return { ...base, status: "erased" };
|
|
247
|
+
// A blob not sealed to this execution is not what this execution captured.
|
|
248
|
+
const bytes = openReasoningText(stored, executionId);
|
|
249
|
+
if (bytes === null)
|
|
250
|
+
return { ...base, status: "digest-mismatch" };
|
|
251
|
+
if (part.digest === undefined || reasoningDigest(bytes) !== part.digest) {
|
|
252
|
+
return { ...base, status: "digest-mismatch" };
|
|
253
|
+
}
|
|
254
|
+
return { ...base, status: "ok", text: (0, classify_1.tainted)(dec.decode(bytes), `llm-reasoning:${stepId}`) };
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=reasoning.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reasoning.js","sourceRoot":"","sources":["../src/reasoning.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { PrincipalChain } from "@loomwork/effect-runtime";
|
|
2
|
+
import type { LlmContentRef, LlmMediaPart } from "./parts";
|
|
3
|
+
import type { LlmRequest } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* What a resolver is told about the call it is serving — enough for a store to AUTHORIZE the read.
|
|
6
|
+
*
|
|
7
|
+
* **A resolver MUST authorize the ref.** The ContentStore is content-addressed and has no tenancy:
|
|
8
|
+
* whoever can name an address can read its bytes. `executionId` / `principal` identify who is asking;
|
|
9
|
+
* a store shared across tenants must check them (typically against the execution that wrote the blob)
|
|
10
|
+
* rather than serving any address it is handed. `origin: "tool"` marks a ref that came out of a tool
|
|
11
|
+
* result (a tool's output is data from outside the prompt author), `"app"` one the application put in
|
|
12
|
+
* a message itself. A ref named by an untrusted party never reaches a resolver: the guard refuses it.
|
|
13
|
+
*/
|
|
14
|
+
export interface ContentResolveContext {
|
|
15
|
+
readonly executionId: string;
|
|
16
|
+
/**
|
|
17
|
+
* The workflow of the asking execution (ADR 0143). A store that seals per subject maps
|
|
18
|
+
* `(executionId, workflowId)` to the erasure subject with the SAME `subjectOf` the event store runs, and a
|
|
19
|
+
* `subjectOf` keyed by workflow needs it: pass it on as `store.get(ref, { executionId, workflowId })`.
|
|
20
|
+
* Absent only on a hand-built context (a caller that did not know it).
|
|
21
|
+
*/
|
|
22
|
+
readonly workflowId?: string;
|
|
23
|
+
readonly stepId?: string;
|
|
24
|
+
readonly principal?: PrincipalChain;
|
|
25
|
+
readonly origin: "tool" | "app" | "reasoning";
|
|
26
|
+
/** Bytes still available under the request's budget; return fewer, or `null`/throw, rather than exceed it. */
|
|
27
|
+
readonly maxBytes: number;
|
|
28
|
+
/** Aborts when the call is cancelled or the request's byte budget is already spent. */
|
|
29
|
+
readonly signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
/** Fetch a blob by address. `null` means erased/absent (never throw for absence). */
|
|
32
|
+
export type ContentResolver = (ref: LlmContentRef, context: ContentResolveContext) => Promise<Uint8Array | null>;
|
|
33
|
+
/** A `ref` source reached a provider (or the effect) with no resolver able to turn it into bytes. */
|
|
34
|
+
export declare class UnresolvedContentError extends Error {
|
|
35
|
+
readonly address: string;
|
|
36
|
+
constructor(address: string, reason: string);
|
|
37
|
+
}
|
|
38
|
+
/** Default cap on the TOTAL resolved media bytes in one request (providers cap well below this). */
|
|
39
|
+
export declare const MAX_RESOLVED_BYTES: number;
|
|
40
|
+
/**
|
|
41
|
+
* Replace every `ref` source with inline base64 `data`, fetching each distinct address once.
|
|
42
|
+
* Returns the request unchanged (same object) when it has no refs.
|
|
43
|
+
*
|
|
44
|
+
* The byte budget is a RUNNING one: fetches start at most {@link FETCH_CONCURRENCY} at a time, each is
|
|
45
|
+
* told what is left, and the first to push the total over `maxBytes` aborts the rest (their signal
|
|
46
|
+
* fires and no queued fetch starts). A blob whose actual size differs from the size its ref declares
|
|
47
|
+
* is refused. Overshoot is bounded, not zero: at most {@link FETCH_CONCURRENCY} fetches are in flight when
|
|
48
|
+
* the budget is spent, so a resolver that ignores `maxBytes` can hold at most that many blobs before the
|
|
49
|
+
* abort lands. Resolvers SHOULD honour `maxBytes` (return `null` or throw rather than read more).
|
|
50
|
+
* A failure — absent, over budget, size mismatch, a throwing resolver — for a ref that
|
|
51
|
+
* sits inside a `tool_result` does NOT fail the call: the part becomes a text error and the
|
|
52
|
+
* `tool_result` is marked `isError`, so the model sees that the tool's media is unavailable and can
|
|
53
|
+
* carry on (the agent loop feeds a tool failure back the same way). The same failure for a ref the
|
|
54
|
+
* application put in a message itself throws {@link UnresolvedContentError}: the app asked for that
|
|
55
|
+
* content, so continuing without it would answer a different question.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveRequestContent(request: LlmRequest, resolver: ContentResolver | undefined, options: {
|
|
58
|
+
maxBytes?: number;
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
/** REQUIRED: who is asking. A resolver cannot authorize a read it cannot attribute. */
|
|
61
|
+
context: Pick<ContentResolveContext, "executionId" | "workflowId" | "stepId" | "principal">;
|
|
62
|
+
/**
|
|
63
|
+
* ADR 0133 D5 - how an assistant `reasoning` part's `textRef` is read. Defaults to `resolver`.
|
|
64
|
+
* A request with no reasoning parts never calls it.
|
|
65
|
+
*/
|
|
66
|
+
reasoningResolver?: ContentResolver;
|
|
67
|
+
}): Promise<LlmRequest>;
|
|
68
|
+
/**
|
|
69
|
+
* The base64 bytes of a media part for a provider mapper. A provider only ever sees
|
|
70
|
+
* `data` sources (the effect resolves refs first); a `ref` here means it was called
|
|
71
|
+
* directly with an unresolved request, which fails closed rather than sending nothing.
|
|
72
|
+
*/
|
|
73
|
+
export declare function inlineData(p: LlmMediaPart): string;
|
|
74
|
+
//# sourceMappingURL=resolveContent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveContent.d.ts","sourceRoot":"","sources":["../src/resolveContent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAEV,aAAa,EACb,YAAY,EAGb,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAc,UAAU,EAAE,MAAM,SAAS,CAAC;AAWtD;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,cAAc,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,WAAW,CAAC;IAC9C,8GAA8G;IAC9G,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uFAAuF;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,qFAAqF;AACrF,MAAM,MAAM,eAAe,GAAG,CAC5B,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,qBAAqB,KAC3B,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;AAEhC,qGAAqG;AACrG,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBACb,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAK5C;AAED,oGAAoG;AACpG,eAAO,MAAM,kBAAkB,QAAmB,CAAC;AAwCnD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,eAAe,GAAG,SAAS,EACrC,OAAO,EAAE;IACP,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,uFAAuF;IACvF,OAAO,EAAE,IAAI,CAAC,qBAAqB,EAAE,aAAa,GAAG,YAAY,GAAG,QAAQ,GAAG,WAAW,CAAC,CAAC;IAC5F;;;OAGG;IACH,iBAAiB,CAAC,EAAE,eAAe,CAAC;CACrC,GACA,OAAO,CAAC,UAAU,CAAC,CAOrB;AA8QD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAMlD"}
|