@peasant-labs/schema 0.15.0 → 0.17.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/dist/index.d.ts +13 -0
- package/dist/index.js +422 -1
- package/dist/internal/generated/content-capabilities.gen.d.ts +2 -0
- package/dist/internal/generated/content-capabilities.gen.js +3 -1
- package/dist/internal/generated/contract/zod.gen.d.ts +1227 -442
- package/dist/internal/generated/contract/zod.gen.js +279 -58
- package/dist/internal/generated/enums.gen.d.ts +84 -7
- package/dist/internal/generated/enums.gen.js +86 -1
- package/dist/internal/generated/local-api.d.ts +54 -0
- package/dist/internal/generated/public-contract.gen.d.ts +2 -2
- package/dist/internal/generated/public-contract.gen.js +1 -1
- package/dist/internal/generated/versions.gen.d.ts +3 -3
- package/dist/internal/generated/versions.gen.js +3 -3
- package/dist/internal/generated/village-api.d.ts +560 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -7,3 +7,16 @@ export declare function isProjectHash(value: unknown): value is ProjectHash;
|
|
|
7
7
|
export declare function validateProjectHash(value: unknown): asserts value is ProjectHash;
|
|
8
8
|
export declare function newProjectHash(raw: string): ProjectHash;
|
|
9
9
|
export type PushContractVersion = import("./internal/generated/contract/zod.gen.js").ContractVersion;
|
|
10
|
+
export interface RawJsonPathPolicy {
|
|
11
|
+
maxDocumentBytes?: number;
|
|
12
|
+
maxDocumentDepth?: number;
|
|
13
|
+
opaqueMetadataPointers?: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function scanRawJsonText(text: string, policy?: RawJsonPathPolicy): void;
|
|
16
|
+
export declare function parseSessionDetailPayloadValue(value: unknown): import("./internal/generated/contract/zod.gen.js").SessionDetailPayload;
|
|
17
|
+
export declare function parseSessionDetailPayloadText(text: string): import("./internal/generated/contract/zod.gen.js").SessionDetailPayload;
|
|
18
|
+
export declare function parseTranscriptContentText(text: string): import("./internal/generated/contract/zod.gen.js").TranscriptContent;
|
|
19
|
+
export declare function parseServerMessageRaw(text: string): import("./internal/generated/contract/zod.gen.js").ServerMessage;
|
|
20
|
+
type Detail = import("./internal/generated/contract/zod.gen.js").SessionDetailPayload;
|
|
21
|
+
export declare function parseNativeMetadataRecordsValue(value: unknown, targets?: unknown): NonNullable<Detail["nativeMetadata"]>;
|
|
22
|
+
export declare function parseNativeMetadataRecordsText(text: string, targets?: unknown): NonNullable<Detail["nativeMetadata"]>;
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export * from "./internal/generated/public-contract.gen.js";
|
|
|
2
2
|
export * from "./internal/generated/enums.gen.js";
|
|
3
3
|
export * from "./internal/generated/content-capabilities.gen.js";
|
|
4
4
|
export * from "./internal/generated/versions.gen.js";
|
|
5
|
-
import { zProjectHash } from "./internal/generated/contract/zod.gen.js";
|
|
5
|
+
import { zNativeMetadataRecord, zTurnDetail, zProjectHash, zServerMessage, zSessionDetailPayload, zTranscriptContent } from "./internal/generated/contract/zod.gen.js";
|
|
6
6
|
function assertProjectHash(value, operation) {
|
|
7
7
|
if (!isProjectHash(value)) {
|
|
8
8
|
const rendered = renderProjectHashInput(value);
|
|
@@ -35,3 +35,424 @@ export function newProjectHash(raw) {
|
|
|
35
35
|
assertProjectHash(raw, "newProjectHash");
|
|
36
36
|
return raw;
|
|
37
37
|
}
|
|
38
|
+
export function scanRawJsonText(text, policy = {}) {
|
|
39
|
+
scanRawJson(text, policy);
|
|
40
|
+
}
|
|
41
|
+
function scanRawJson(text, policy) {
|
|
42
|
+
if (policy.maxDocumentBytes !== undefined && new TextEncoder().encode(text).length > policy.maxDocumentBytes) {
|
|
43
|
+
throw new TypeError("Raw JSON validation failed at @peasant-labs/schema scanRawJsonText during pre-decode scanning: the document exceeds the configured byte limit; parsing it would cross the caller's safety boundary; reduce the document before retrying.");
|
|
44
|
+
}
|
|
45
|
+
const scanner = new RawJsonScanner(text, policy);
|
|
46
|
+
scanner.scan();
|
|
47
|
+
return scanner;
|
|
48
|
+
}
|
|
49
|
+
export function parseSessionDetailPayloadValue(value) {
|
|
50
|
+
rejectExplicitNullEvidence(value);
|
|
51
|
+
const payload = zSessionDetailPayload.parse(value);
|
|
52
|
+
validateSessionDetail(payload);
|
|
53
|
+
return payload;
|
|
54
|
+
}
|
|
55
|
+
export function parseSessionDetailPayloadText(text) {
|
|
56
|
+
scanRawJsonText(text, { maxDocumentBytes: 8 << 20, maxDocumentDepth: 64, opaqueMetadataPointers: ["/nativeMetadata/*/data"] });
|
|
57
|
+
return parseSessionDetailPayloadValue(JSON.parse(text));
|
|
58
|
+
}
|
|
59
|
+
export function parseTranscriptContentText(text) {
|
|
60
|
+
scanRawJsonText(text, { maxDocumentBytes: 8 << 20, maxDocumentDepth: 64, opaqueMetadataPointers: ["/sessionDetail/nativeMetadata/*/data"] });
|
|
61
|
+
const raw = JSON.parse(text);
|
|
62
|
+
if (isRecord(raw))
|
|
63
|
+
rejectExplicitNullEvidence(raw.sessionDetail);
|
|
64
|
+
const content = zTranscriptContent.parse(raw);
|
|
65
|
+
if (content.sessionDetail === undefined || content.sessionDetail === null)
|
|
66
|
+
failSemantic("sessionDetail is required");
|
|
67
|
+
parseSessionDetailPayloadValue(content.sessionDetail);
|
|
68
|
+
return content;
|
|
69
|
+
}
|
|
70
|
+
export function parseServerMessageRaw(text) {
|
|
71
|
+
// Dispatch from the syntax scan, not a lossy full-document JSON.parse. The
|
|
72
|
+
// type member may follow data, so a second selected scan precedes decoding.
|
|
73
|
+
const dispatch = scanRawJson(text, { maxDocumentBytes: 8 << 20, maxDocumentDepth: 64 });
|
|
74
|
+
if (dispatch.rootMessageType === "session_detail") {
|
|
75
|
+
scanRawJsonText(text, { maxDocumentBytes: 8 << 20, maxDocumentDepth: 64, opaqueMetadataPointers: ["/data/nativeMetadata/*/data"] });
|
|
76
|
+
}
|
|
77
|
+
const value = JSON.parse(text);
|
|
78
|
+
if (isRecord(value) && value.type === "session_detail")
|
|
79
|
+
rejectExplicitNullEvidence(value.data);
|
|
80
|
+
const message = zServerMessage.parse(value);
|
|
81
|
+
if (message.type === "session_detail") {
|
|
82
|
+
parseSessionDetailPayloadValue(message.data);
|
|
83
|
+
}
|
|
84
|
+
return message;
|
|
85
|
+
}
|
|
86
|
+
class RawJsonScanner {
|
|
87
|
+
text;
|
|
88
|
+
policy;
|
|
89
|
+
position = 0;
|
|
90
|
+
metadataBytes = 0;
|
|
91
|
+
rootMessageType;
|
|
92
|
+
constructor(text, policy) {
|
|
93
|
+
this.text = text;
|
|
94
|
+
this.policy = policy;
|
|
95
|
+
}
|
|
96
|
+
scan() { this.space(); this.value("", 1, 0); this.space(); if (this.position !== this.text.length)
|
|
97
|
+
this.fail("trailing content"); }
|
|
98
|
+
value(path, depth, metadataDepth) {
|
|
99
|
+
if (metadataDepth === 0 && (this.policy.opaqueMetadataPointers ?? []).some(pattern => pointerMatches(pattern, path)))
|
|
100
|
+
metadataDepth = 1;
|
|
101
|
+
if (this.policy.maxDocumentDepth !== undefined && depth > this.policy.maxDocumentDepth)
|
|
102
|
+
this.fail("document depth exceeds configured limit");
|
|
103
|
+
if (metadataDepth > 32)
|
|
104
|
+
this.fail("metadata depth exceeds 32");
|
|
105
|
+
this.space();
|
|
106
|
+
const start = this.position;
|
|
107
|
+
this.valueBody(path, depth, metadataDepth);
|
|
108
|
+
if (metadataDepth === 1) {
|
|
109
|
+
const bytes = new TextEncoder().encode(this.text.slice(start, this.position)).length;
|
|
110
|
+
this.metadataBytes += bytes;
|
|
111
|
+
if (bytes > 65536 || this.metadataBytes > 1048576)
|
|
112
|
+
this.fail("metadata data exceeds its byte budget (64 KiB per value, 1 MiB aggregate)");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
valueBody(path, depth, metadataDepth) {
|
|
116
|
+
const opaque = metadataDepth > 0;
|
|
117
|
+
this.space();
|
|
118
|
+
const c = this.text[this.position];
|
|
119
|
+
if (c === "{")
|
|
120
|
+
return this.object(path, depth, metadataDepth);
|
|
121
|
+
if (c === "[")
|
|
122
|
+
return this.array(path, depth, metadataDepth);
|
|
123
|
+
if (c === '"') {
|
|
124
|
+
const value = this.string();
|
|
125
|
+
if (path === "/type")
|
|
126
|
+
this.rootMessageType = value;
|
|
127
|
+
if (opaque && new TextEncoder().encode(value).length > 16384)
|
|
128
|
+
this.fail("metadata string exceeds 16 KiB");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (c === "t")
|
|
132
|
+
return this.literal("true");
|
|
133
|
+
if (c === "f")
|
|
134
|
+
return this.literal("false");
|
|
135
|
+
if (c === "n")
|
|
136
|
+
return this.literal("null");
|
|
137
|
+
const match = this.text.slice(this.position).match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/);
|
|
138
|
+
if (match === null)
|
|
139
|
+
this.fail("invalid value");
|
|
140
|
+
this.position += match[0].length;
|
|
141
|
+
if (opaque) {
|
|
142
|
+
const n = Number(match[0]);
|
|
143
|
+
if (n === 0 && !/^-?0(?:\.0*)?(?:[eE][+-]?\d+)?$/.test(match[0]))
|
|
144
|
+
this.fail("metadata number underflows to zero");
|
|
145
|
+
if (!Number.isFinite(n) || (Number.isInteger(n) && !Number.isSafeInteger(n)))
|
|
146
|
+
this.fail("metadata number is outside the finite JS-safe domain");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
object(path, depth, metadataDepth) {
|
|
150
|
+
this.position++;
|
|
151
|
+
this.space();
|
|
152
|
+
const keys = new Set();
|
|
153
|
+
while (this.text[this.position] !== "}") {
|
|
154
|
+
const key = this.string();
|
|
155
|
+
if (metadataDepth > 0 && new TextEncoder().encode(key).length > 512)
|
|
156
|
+
this.fail("metadata key exceeds 512 bytes");
|
|
157
|
+
if (keys.has(key))
|
|
158
|
+
this.fail(`duplicate object key ${JSON.stringify(key)}`);
|
|
159
|
+
keys.add(key);
|
|
160
|
+
if (metadataDepth > 0 && keys.size > 256)
|
|
161
|
+
this.fail("metadata object exceeds 256 members");
|
|
162
|
+
this.space();
|
|
163
|
+
this.expect(":");
|
|
164
|
+
const child = `${path}/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
|
165
|
+
this.value(child, depth + 1, metadataDepth > 0 ? metadataDepth + 1 : 0);
|
|
166
|
+
this.space();
|
|
167
|
+
if (this.text[this.position] !== ",")
|
|
168
|
+
break;
|
|
169
|
+
this.position++;
|
|
170
|
+
this.space();
|
|
171
|
+
if (this.text[this.position] === "}")
|
|
172
|
+
this.fail("trailing comma");
|
|
173
|
+
}
|
|
174
|
+
this.expect("}");
|
|
175
|
+
}
|
|
176
|
+
array(path, depth, metadataDepth) {
|
|
177
|
+
this.position++;
|
|
178
|
+
this.space();
|
|
179
|
+
let count = 0;
|
|
180
|
+
while (this.text[this.position] !== "]") {
|
|
181
|
+
if (metadataDepth > 0 && count >= 4096)
|
|
182
|
+
this.fail("metadata array exceeds 4096 elements");
|
|
183
|
+
this.value(`${path}/${count++}`, depth + 1, metadataDepth > 0 ? metadataDepth + 1 : 0);
|
|
184
|
+
this.space();
|
|
185
|
+
if (this.text[this.position] !== ",")
|
|
186
|
+
break;
|
|
187
|
+
this.position++;
|
|
188
|
+
this.space();
|
|
189
|
+
if (this.text[this.position] === "]")
|
|
190
|
+
this.fail("trailing comma");
|
|
191
|
+
}
|
|
192
|
+
this.expect("]");
|
|
193
|
+
}
|
|
194
|
+
string() { const start = this.position; this.expect('"'); for (;;) {
|
|
195
|
+
const c = this.text[this.position++];
|
|
196
|
+
if (c === undefined || c === "\n" || c === "\r")
|
|
197
|
+
this.fail("unterminated string");
|
|
198
|
+
if (c === '"')
|
|
199
|
+
break;
|
|
200
|
+
if (c === "\\") {
|
|
201
|
+
const escaped = this.text[this.position++];
|
|
202
|
+
if (escaped === "u") {
|
|
203
|
+
const hex = this.text.slice(this.position, this.position + 4);
|
|
204
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex))
|
|
205
|
+
this.fail("invalid Unicode escape");
|
|
206
|
+
const unit = Number.parseInt(hex, 16);
|
|
207
|
+
this.position += 4;
|
|
208
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
209
|
+
if (this.text.slice(this.position, this.position + 2) !== "\\u")
|
|
210
|
+
this.fail("unpaired Unicode surrogate");
|
|
211
|
+
const lowHex = this.text.slice(this.position + 2, this.position + 6);
|
|
212
|
+
if (!/^[0-9a-fA-F]{4}$/.test(lowHex) || Number.parseInt(lowHex, 16) < 0xdc00 || Number.parseInt(lowHex, 16) > 0xdfff)
|
|
213
|
+
this.fail("invalid Unicode surrogate pair");
|
|
214
|
+
this.position += 6;
|
|
215
|
+
}
|
|
216
|
+
else if (unit >= 0xdc00 && unit <= 0xdfff)
|
|
217
|
+
this.fail("unpaired Unicode surrogate");
|
|
218
|
+
}
|
|
219
|
+
else if (!'"\\/bfnrt'.includes(escaped ?? ""))
|
|
220
|
+
this.fail("invalid escape");
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const unit = c.charCodeAt(0);
|
|
224
|
+
if (unit < 0x20)
|
|
225
|
+
this.fail("unescaped control character");
|
|
226
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
227
|
+
const low = this.text.charCodeAt(this.position);
|
|
228
|
+
if (low < 0xdc00 || low > 0xdfff)
|
|
229
|
+
this.fail("unpaired Unicode surrogate");
|
|
230
|
+
this.position++;
|
|
231
|
+
}
|
|
232
|
+
else if (unit >= 0xdc00 && unit <= 0xdfff)
|
|
233
|
+
this.fail("unpaired Unicode surrogate");
|
|
234
|
+
}
|
|
235
|
+
} return JSON.parse(this.text.slice(start, this.position)); }
|
|
236
|
+
literal(value) { if (!this.text.startsWith(value, this.position))
|
|
237
|
+
this.fail("invalid literal"); this.position += value.length; }
|
|
238
|
+
expect(value) { if (this.text[this.position] !== value)
|
|
239
|
+
this.fail(`expected ${value}`); this.position++; }
|
|
240
|
+
space() { while (this.text[this.position] === " " || this.text[this.position] === "\t" || this.text[this.position] === "\n" || this.text[this.position] === "\r")
|
|
241
|
+
this.position++; }
|
|
242
|
+
fail(reason) { throw new TypeError(`Raw JSON validation failed at @peasant-labs/schema scanRawJsonText during pre-decode scanning near character ${this.position}: ${reason}; JSON.parse would otherwise lose or misinterpret evidence; correct the raw JSON and retry.`); }
|
|
243
|
+
}
|
|
244
|
+
function pointerMatches(pattern, path) { const expected = pattern.split("/"); const actual = path.split("/"); return expected.length === actual.length && expected.every((part, index) => part === "*" || part === actual[index]); }
|
|
245
|
+
function validateSessionDetail(payload) {
|
|
246
|
+
const turns = validateTurnEvidence(payload.turns ?? []);
|
|
247
|
+
if ((payload.nativeMetadata?.length ?? 0) > 0 && String(payload.harness) !== "pi")
|
|
248
|
+
failSemantic("Pi native metadata requires harness pi");
|
|
249
|
+
validateMetadataRecords(payload.nativeMetadata ?? [], turns);
|
|
250
|
+
}
|
|
251
|
+
// Producers can validate metadata before attaching it to a harness envelope.
|
|
252
|
+
// The full detail parsers reuse this same record and target validation.
|
|
253
|
+
export function parseNativeMetadataRecordsValue(value, targets = []) {
|
|
254
|
+
rejectExplicitNullEvidence({ nativeMetadata: value, turns: targets });
|
|
255
|
+
const turns = zTurnDetail.array().parse(targets);
|
|
256
|
+
const records = zNativeMetadataRecord.array().parse(value);
|
|
257
|
+
validateMetadataRecords(records, validateTurnEvidence(turns));
|
|
258
|
+
return records;
|
|
259
|
+
}
|
|
260
|
+
export function parseNativeMetadataRecordsText(text, targets = []) {
|
|
261
|
+
scanRawJsonText(text, { maxDocumentBytes: 8 << 20, maxDocumentDepth: 64, opaqueMetadataPointers: ["/*/data"] });
|
|
262
|
+
return parseNativeMetadataRecordsValue(JSON.parse(text), targets);
|
|
263
|
+
}
|
|
264
|
+
function validateTurnEvidence(targets) {
|
|
265
|
+
const owners = new Set();
|
|
266
|
+
const sources = new Set();
|
|
267
|
+
const turnIndexes = new Set();
|
|
268
|
+
const toolIds = new Set();
|
|
269
|
+
const turns = new Map();
|
|
270
|
+
for (const turn of targets) {
|
|
271
|
+
if (turn.observedModel !== undefined && (turn.role !== "assistant" || !validUnicode(turn.observedModel)))
|
|
272
|
+
failSemantic("observedModel requires valid Unicode and an assistant role");
|
|
273
|
+
if (turnIndexes.has(turn.index))
|
|
274
|
+
failSemantic("duplicate turn index makes metadata attachment ambiguous");
|
|
275
|
+
turnIndexes.add(turn.index);
|
|
276
|
+
turns.set(turn.index, turn);
|
|
277
|
+
if (turn.sourceEntryRef !== undefined && !validOptionalRef(turn.sourceEntryRef))
|
|
278
|
+
failSemantic("turn source reference is invalid or exceeds 96 bytes");
|
|
279
|
+
if (turn.usage != null) {
|
|
280
|
+
if (!((turn.role === "assistant" && turn.usage.scope === "assistant") || (turn.role === "system" && turn.usage.scope === "summary")))
|
|
281
|
+
failSemantic("turn role and usage scope disagree");
|
|
282
|
+
if (!turn.sourceEntryRef || turn.usage.sourceEntryRef !== turn.sourceEntryRef)
|
|
283
|
+
failSemantic("turn usage source reference disagrees");
|
|
284
|
+
validateUsage(turn.usage, owners, sources);
|
|
285
|
+
}
|
|
286
|
+
for (const tool of turn.toolCalls ?? []) {
|
|
287
|
+
if (!validOptionalRef(tool.id))
|
|
288
|
+
failSemantic("tool id reference is invalid or exceeds 96 bytes");
|
|
289
|
+
if (tool.id && toolIds.has(tool.id))
|
|
290
|
+
failSemantic("duplicate tool call id makes metadata attachment ambiguous");
|
|
291
|
+
if (tool.id)
|
|
292
|
+
toolIds.add(tool.id);
|
|
293
|
+
if (tool.callEntryRef !== undefined && !validOptionalRef(tool.callEntryRef))
|
|
294
|
+
failSemantic("tool call reference is invalid or exceeds 96 bytes");
|
|
295
|
+
if (tool.resultEntryRef !== undefined && !validOptionalRef(tool.resultEntryRef))
|
|
296
|
+
failSemantic("tool result reference is invalid or exceeds 96 bytes");
|
|
297
|
+
if (tool.usage != null) {
|
|
298
|
+
if (tool.usage.scope !== "tool" || !tool.resultEntryRef || tool.usage.sourceEntryRef !== tool.resultEntryRef)
|
|
299
|
+
failSemantic("tool usage attribution disagrees");
|
|
300
|
+
validateUsage(tool.usage, owners, sources);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return turns;
|
|
305
|
+
}
|
|
306
|
+
function validateMetadataRecords(records, turns) {
|
|
307
|
+
const ids = new Set();
|
|
308
|
+
let metadataBytes = 0;
|
|
309
|
+
if (records.length > 256)
|
|
310
|
+
failSemantic("native metadata record count exceeds 256");
|
|
311
|
+
for (const metadata of records) {
|
|
312
|
+
if (!validRef(metadata.id) || !validRef(metadata.source.entryRef) || ids.has(metadata.id))
|
|
313
|
+
failSemantic("metadata reference is invalid or duplicated");
|
|
314
|
+
ids.add(metadata.id);
|
|
315
|
+
if (metadata.customType !== undefined && (!validUnicode(metadata.customType) || new TextEncoder().encode(metadata.customType).length > 128))
|
|
316
|
+
failSemantic("customType is invalid Unicode or exceeds 128 bytes");
|
|
317
|
+
validateMetadataValue(metadata.data, 1);
|
|
318
|
+
const encoded = new TextEncoder().encode(JSON.stringify(metadata.data)).length;
|
|
319
|
+
metadataBytes += encoded;
|
|
320
|
+
if (encoded > 65536 || metadataBytes > 1048576)
|
|
321
|
+
failSemantic("metadata data exceeds its byte budget");
|
|
322
|
+
const attachment = metadata.attachment;
|
|
323
|
+
const target = attachment?.turnIndex == null ? undefined : turns.get(attachment.turnIndex);
|
|
324
|
+
if (attachment?.toolCallId !== undefined && !validRef(attachment.toolCallId))
|
|
325
|
+
failSemantic("attachment tool reference is invalid or exceeds 96 bytes");
|
|
326
|
+
switch (metadata.kind) {
|
|
327
|
+
case "pi.custom.data":
|
|
328
|
+
if (metadata.source.sourceType !== "pi.custom" || metadata.source.messageRole !== undefined || attachment !== undefined || !metadata.customType)
|
|
329
|
+
failSemantic("custom data metadata matrix disagrees");
|
|
330
|
+
break;
|
|
331
|
+
case "pi.custommessage.details":
|
|
332
|
+
if (metadata.source.sourceType !== "pi.custom_message" || metadata.source.messageRole !== undefined || attachment?.turnIndex === undefined || attachment.toolCallId !== undefined || !metadata.customType || target?.role !== "system" || target.sourceEntryRef !== metadata.source.entryRef)
|
|
333
|
+
failSemantic("custom message metadata matrix disagrees");
|
|
334
|
+
break;
|
|
335
|
+
case "pi.toolresult.details": {
|
|
336
|
+
const tools = target?.toolCalls?.filter(tool => tool.id === attachment?.toolCallId) ?? [];
|
|
337
|
+
const tool = tools[0];
|
|
338
|
+
if (metadata.source.sourceType !== "pi.message" || metadata.source.messageRole !== "toolResult" || attachment?.turnIndex === undefined || !attachment.toolCallId || metadata.customType !== undefined || tools.length !== 1 || tool?.resultEntryRef !== metadata.source.entryRef || (tool.usage != null && tool.usage.sourceEntryRef !== metadata.source.entryRef))
|
|
339
|
+
failSemantic("tool result metadata matrix disagrees");
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
case "pi.compaction.details":
|
|
343
|
+
case "pi.branchsummary.details": {
|
|
344
|
+
const source = metadata.kind === "pi.compaction.details" ? "pi.compaction" : "pi.branch_summary";
|
|
345
|
+
if (metadata.source.sourceType !== source || metadata.source.messageRole !== undefined || attachment?.turnIndex === undefined || attachment.toolCallId !== undefined || metadata.customType !== undefined || target?.role !== "system" || target.sourceEntryRef !== metadata.source.entryRef || target.usage?.scope !== "summary" || target.usage.sourceEntryRef !== metadata.source.entryRef)
|
|
346
|
+
failSemantic("summary metadata matrix disagrees");
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
default: failSemantic("metadata kind is outside its closed set");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function validateUsage(usage, owners, sources) {
|
|
354
|
+
if (!validRef(usage.ownerId) || !validRef(usage.sourceEntryRef) || owners.has(usage.ownerId) || sources.has(usage.sourceEntryRef))
|
|
355
|
+
failSemantic("usage owner/source reference is invalid or duplicated");
|
|
356
|
+
owners.add(usage.ownerId);
|
|
357
|
+
sources.add(usage.sourceEntryRef);
|
|
358
|
+
const tokens = usage.tokens;
|
|
359
|
+
const base = tokens == null ? [] : [tokens.input, tokens.output, tokens.cacheRead, tokens.cacheWrite, tokens.totalTokens];
|
|
360
|
+
const present = base.filter(v => v !== undefined && v !== null).length;
|
|
361
|
+
for (const value of tokens == null ? [] : [...base, tokens.reasoning, tokens.cacheWrite1h])
|
|
362
|
+
if (value != null && (!Number.isSafeInteger(value) || value < 0))
|
|
363
|
+
failSemantic("token is outside JS-safe nonnegative integers");
|
|
364
|
+
const expected = present === 5 ? "complete" : present === 0 ? "unknown" : "partial";
|
|
365
|
+
if (usage.completeness !== expected)
|
|
366
|
+
failSemantic("usage completeness disagrees with token presence");
|
|
367
|
+
if (tokens?.reasoning != null && tokens.output != null && tokens.reasoning > tokens.output)
|
|
368
|
+
failSemantic("reasoning exceeds output");
|
|
369
|
+
if (tokens?.cacheWrite1h != null && tokens.cacheWrite != null && tokens.cacheWrite1h > tokens.cacheWrite)
|
|
370
|
+
failSemantic("cacheWrite1h exceeds cacheWrite");
|
|
371
|
+
if (present === 5 && tokens && tokens.input + tokens.output + tokens.cacheRead + tokens.cacheWrite !== tokens.totalTokens)
|
|
372
|
+
failSemantic("totalTokens disagrees with base sum");
|
|
373
|
+
if (usage.cost != null) {
|
|
374
|
+
if (usage.cost.source !== "recorded_harness_estimate")
|
|
375
|
+
failSemantic("recorded cost source is invalid");
|
|
376
|
+
for (const value of [usage.cost.input, usage.cost.output, usage.cost.cacheRead, usage.cost.cacheWrite, usage.cost.total])
|
|
377
|
+
if (value != null && (!/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(value) || !Number.isFinite(Number(value)) || Number(value) < 0))
|
|
378
|
+
failSemantic("recorded cost is not a finite nonnegative JSON number");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function validRef(value) { return value !== undefined && value.length > 0 && validOptionalRef(value); }
|
|
382
|
+
function validOptionalRef(value) { return validUnicode(value) && new TextEncoder().encode(value).length <= 96; }
|
|
383
|
+
function validUnicode(value) {
|
|
384
|
+
for (let i = 0; i < value.length; i++) {
|
|
385
|
+
const unit = value.charCodeAt(i);
|
|
386
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
387
|
+
const low = value.charCodeAt(++i);
|
|
388
|
+
if (!(low >= 0xdc00 && low <= 0xdfff))
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
else if (unit >= 0xdc00 && unit <= 0xdfff)
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
function validateMetadataValue(value, depth) {
|
|
397
|
+
if (depth > 32)
|
|
398
|
+
failSemantic("metadata data exceeds depth 32");
|
|
399
|
+
if (typeof value === "string" && (!validUnicode(value) || new TextEncoder().encode(value).length > 16384))
|
|
400
|
+
failSemantic("metadata string is invalid Unicode or exceeds 16 KiB");
|
|
401
|
+
if (typeof value === "number" && (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))))
|
|
402
|
+
failSemantic("metadata number is outside the finite JS-safe domain");
|
|
403
|
+
if (value !== null && !["string", "number", "boolean", "object"].includes(typeof value))
|
|
404
|
+
failSemantic("metadata data is not a JSON value");
|
|
405
|
+
if (Array.isArray(value)) {
|
|
406
|
+
if (value.length > 4096)
|
|
407
|
+
failSemantic("metadata array exceeds 4096 elements");
|
|
408
|
+
for (const item of value)
|
|
409
|
+
validateMetadataValue(item, depth + 1);
|
|
410
|
+
}
|
|
411
|
+
else if (isRecord(value)) {
|
|
412
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
413
|
+
failSemantic("metadata data must use plain JSON objects");
|
|
414
|
+
const entries = Object.entries(value);
|
|
415
|
+
if (entries.length > 256)
|
|
416
|
+
failSemantic("metadata object exceeds 256 members");
|
|
417
|
+
for (const [key, item] of entries) {
|
|
418
|
+
if (!validUnicode(key) || new TextEncoder().encode(key).length > 512)
|
|
419
|
+
failSemantic("metadata key is invalid Unicode or exceeds 512 bytes");
|
|
420
|
+
validateMetadataValue(item, depth + 1);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
425
|
+
function rejectExplicitNullEvidence(value) {
|
|
426
|
+
if (!isRecord(value))
|
|
427
|
+
return;
|
|
428
|
+
for (const turn of Array.isArray(value.turns) ? value.turns : []) {
|
|
429
|
+
if (!isRecord(turn))
|
|
430
|
+
continue;
|
|
431
|
+
const tools = Array.isArray(turn.toolCalls) ? turn.toolCalls : [];
|
|
432
|
+
for (const usage of [turn.usage, ...tools.map(tool => isRecord(tool) ? tool.usage : undefined)]) {
|
|
433
|
+
if (!isRecord(usage) || !isRecord(usage.tokens))
|
|
434
|
+
continue;
|
|
435
|
+
for (const [name, token] of Object.entries(usage.tokens))
|
|
436
|
+
if (token === null)
|
|
437
|
+
failSemantic(`token ${name} is explicitly null`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
for (const record of Array.isArray(value.nativeMetadata) ? value.nativeMetadata : []) {
|
|
441
|
+
if (!isRecord(record))
|
|
442
|
+
continue;
|
|
443
|
+
for (const name of ["id", "kind", "source", "data"])
|
|
444
|
+
if (!(name in record) || record[name] === null)
|
|
445
|
+
failSemantic(`metadata required field ${name} is missing or null`);
|
|
446
|
+
if (record.attachment === null)
|
|
447
|
+
failSemantic("metadata attachment is explicitly null");
|
|
448
|
+
if (isRecord(record.attachment)) {
|
|
449
|
+
if (record.attachment.turnIndex === null)
|
|
450
|
+
failSemantic("metadata attachment.turnIndex is explicitly null");
|
|
451
|
+
if ("toolCallId" in record.attachment && record.kind !== "pi.toolresult.details")
|
|
452
|
+
failSemantic("metadata attachment.toolCallId is forbidden for this kind");
|
|
453
|
+
}
|
|
454
|
+
if (isRecord(record.source) && "messageRole" in record.source && record.kind !== "pi.toolresult.details")
|
|
455
|
+
failSemantic("metadata source.messageRole is forbidden for this kind");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function failSemantic(reason) { throw new TypeError(`Session detail validation failed at @peasant-labs/schema public parser during semantic validation: ${reason}; consumers cannot safely attribute transcript evidence; correct the payload and retry.`); }
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* discovered list down to the tokens they understand without stringly typing.
|
|
9
9
|
*/
|
|
10
10
|
export declare const KnownContentCapability: Readonly<{
|
|
11
|
+
readonly DetailedUsageV1: "detailed_usage_v1";
|
|
12
|
+
readonly NativeMetadataV1: "native_metadata_v1";
|
|
11
13
|
readonly ObservedModelV1: "observed_model_v1";
|
|
12
14
|
}>;
|
|
13
15
|
export type KnownContentCapability = (typeof KnownContentCapability)[keyof typeof KnownContentCapability];
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
* discovered list down to the tokens they understand without stringly typing.
|
|
10
10
|
*/
|
|
11
11
|
export const KnownContentCapability = Object.freeze({
|
|
12
|
+
DetailedUsageV1: "detailed_usage_v1",
|
|
13
|
+
NativeMetadataV1: "native_metadata_v1",
|
|
12
14
|
ObservedModelV1: "observed_model_v1",
|
|
13
15
|
});
|
|
14
|
-
export const AllContentCapabilities = Object.freeze([KnownContentCapability.ObservedModelV1]);
|
|
16
|
+
export const AllContentCapabilities = Object.freeze([KnownContentCapability.DetailedUsageV1, KnownContentCapability.NativeMetadataV1, KnownContentCapability.ObservedModelV1]);
|
|
15
17
|
export function isContentCapability(value) {
|
|
16
18
|
return typeof value === "string" && AllContentCapabilities.includes(value);
|
|
17
19
|
}
|