@xandreed/core 0.13.0 → 0.14.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.
Files changed (148) hide show
  1. package/README.md +1 -1
  2. package/dist/conformance.functions.d.ts +34 -0
  3. package/dist/conformance.functions.js +25 -0
  4. package/dist/domain/bash.entity.d.ts +82 -0
  5. package/dist/domain/bash.entity.functions.d.ts +17 -0
  6. package/dist/domain/bash.entity.functions.js +26 -0
  7. package/dist/domain/bash.entity.functions.test.d.ts +1 -0
  8. package/dist/domain/bash.entity.js +30 -0
  9. package/dist/domain/file-system.entity.d.ts +91 -0
  10. package/dist/domain/file-system.entity.functions.d.ts +18 -0
  11. package/dist/domain/file-system.entity.functions.js +30 -0
  12. package/dist/domain/file-system.entity.js +37 -0
  13. package/dist/domain/loop-event.entity.d.ts +9 -0
  14. package/dist/domain/media.entity.d.ts +79 -0
  15. package/dist/domain/media.entity.functions.d.ts +76 -0
  16. package/dist/domain/media.entity.functions.js +110 -0
  17. package/dist/domain/media.entity.js +38 -0
  18. package/dist/domain/media.entity.test.d.ts +1 -0
  19. package/dist/domain/message.entity.d.ts +157 -5
  20. package/dist/domain/message.entity.js +8 -4
  21. package/dist/domain/model-catalog.entity.d.ts +2 -0
  22. package/dist/domain/model-catalog.entity.js +2 -1
  23. package/dist/domain/token-usage.entity.d.ts +45 -1
  24. package/dist/domain/token-usage.entity.functions.d.ts +30 -1
  25. package/dist/domain/token-usage.entity.functions.js +24 -4
  26. package/dist/domain/token-usage.entity.js +19 -2
  27. package/dist/domain/token-usage.entity.test.d.ts +1 -0
  28. package/dist/harness/capability.entity.functions.js +2 -1
  29. package/dist/harness/plugin.adapter.d.ts +3 -0
  30. package/dist/harness/plugin.adapter.js +7 -1
  31. package/dist/harness/skill-files.entity.d.ts +101 -0
  32. package/dist/harness/skill-files.entity.functions.d.ts +58 -0
  33. package/dist/harness/skill-files.entity.functions.js +150 -0
  34. package/dist/harness/skill-files.entity.functions.test.d.ts +1 -0
  35. package/dist/harness/skill-files.entity.js +25 -0
  36. package/dist/index.d.ts +48 -8
  37. package/dist/index.js +67 -6
  38. package/dist/loop/mapping.d.ts +12 -6
  39. package/dist/loop/mapping.js +30 -6
  40. package/dist/loop/stepContext.d.ts +8 -0
  41. package/dist/loop/stepContext.js +1 -1
  42. package/dist/loop/traceContent.d.ts +29 -0
  43. package/dist/loop/traceContent.js +4 -0
  44. package/dist/loop/traceContent.test.d.ts +1 -0
  45. package/dist/memory/memory-log.entity.d.ts +152 -14
  46. package/dist/memory/memory-log.entity.functions.d.ts +35 -5
  47. package/dist/memory/memory-log.entity.functions.js +59 -20
  48. package/dist/memory/memory-log.entity.js +19 -6
  49. package/dist/memory/memory-session.js +10 -6
  50. package/dist/memory/memory.conformance.d.ts +8 -1
  51. package/dist/memory/memory.conformance.js +32 -0
  52. package/dist/models/auth-store.conformance.d.ts +13 -0
  53. package/dist/models/auth-store.conformance.js +73 -0
  54. package/dist/models/auth-store.conformance.test.d.ts +1 -0
  55. package/dist/models/model-catalog.conformance.d.ts +15 -0
  56. package/dist/models/model-catalog.conformance.js +27 -0
  57. package/dist/models/model-catalog.conformance.test.d.ts +1 -0
  58. package/dist/models/settings-store.conformance.d.ts +26 -0
  59. package/dist/models/settings-store.conformance.js +80 -0
  60. package/dist/models/settings-store.conformance.test.d.ts +1 -0
  61. package/dist/ports/activity.port.d.ts +35 -0
  62. package/dist/ports/activity.port.js +7 -0
  63. package/dist/ports/auth-store.port.d.ts +7 -5
  64. package/dist/ports/bash.port.d.ts +22 -0
  65. package/dist/ports/bash.port.js +4 -0
  66. package/dist/ports/capability.port.d.ts +11 -1
  67. package/dist/ports/file-system.port.d.ts +44 -15
  68. package/dist/ports/file-system.port.js +1 -7
  69. package/dist/ports/memory.port.d.ts +13 -1
  70. package/dist/ports/model-catalog.port.d.ts +4 -1
  71. package/dist/ports/run-context.port.d.ts +6 -1
  72. package/dist/ports/session-log.port.d.ts +4 -1
  73. package/dist/ports/sessions.port.d.ts +35 -6
  74. package/dist/ports/settings-store.port.d.ts +11 -7
  75. package/dist/ports/step-loop.port.d.ts +30 -2
  76. package/dist/ports/tasks.port.d.ts +5 -1
  77. package/dist/ports/turn-admission.port.d.ts +8 -2
  78. package/dist/ports/turn-events.port.d.ts +8 -1
  79. package/dist/ports/turn-scope.port.d.ts +19 -1
  80. package/dist/ports/turn.port.d.ts +70 -5
  81. package/dist/ports/utility-llm.port.d.ts +2 -0
  82. package/dist/session/activity.entity.d.ts +42 -0
  83. package/dist/session/activity.entity.functions.d.ts +21 -0
  84. package/dist/session/activity.entity.functions.js +23 -0
  85. package/dist/session/activity.entity.functions.test.d.ts +1 -0
  86. package/dist/session/activity.entity.js +15 -0
  87. package/dist/session/chassis.js +2 -1
  88. package/dist/session/session-event.entity.d.ts +68 -6
  89. package/dist/session/session-event.entity.functions.d.ts +34 -2
  90. package/dist/session/session-event.entity.functions.js +54 -16
  91. package/dist/session/session-event.entity.js +19 -2
  92. package/dist/session/session-log.conformance.d.ts +4 -3
  93. package/dist/session/session-log.conformance.js +16 -14
  94. package/dist/session/session-log.recovery.conformance.d.ts +40 -0
  95. package/dist/session/session-log.recovery.conformance.js +143 -0
  96. package/dist/session/session-log.recovery.conformance.test.d.ts +1 -0
  97. package/dist/session/sessions.conformance.d.ts +79 -0
  98. package/dist/session/sessions.conformance.js +355 -0
  99. package/dist/session/sessions.entity.d.ts +36 -3
  100. package/dist/session/sessions.entity.functions.d.ts +42 -0
  101. package/dist/session/sessions.entity.functions.js +26 -0
  102. package/dist/session/sessions.entity.js +17 -4
  103. package/dist/session/sessions.entity.test.d.ts +1 -0
  104. package/dist/session/tasks.conformance.d.ts +70 -0
  105. package/dist/session/tasks.conformance.js +213 -0
  106. package/dist/session/tasks.conformance.test.d.ts +1 -0
  107. package/dist/turn/model-request.entity.d.ts +4 -0
  108. package/dist/turn/model-request.entity.js +7 -2
  109. package/dist/turn/step-loop.conformance.d.ts +4 -1
  110. package/dist/turn/step-loop.conformance.js +73 -7
  111. package/dist/turn/tool-registry.conformance.d.ts +38 -0
  112. package/dist/turn/tool-registry.conformance.js +206 -0
  113. package/dist/turn/turn-bus.js +4 -3
  114. package/dist/turn/turn-event.entity.d.ts +134 -34
  115. package/dist/turn/turn-event.entity.functions.d.ts +11 -0
  116. package/dist/turn/turn-event.entity.functions.js +9 -1
  117. package/dist/turn/turn-event.entity.js +76 -3
  118. package/dist/turn/turn-event.entity.test.d.ts +1 -0
  119. package/dist/turn/turn-lifecycle.d.ts +46 -8
  120. package/dist/turn/turn-lifecycle.js +33 -3
  121. package/dist/turn/turn-lifecycle.test.d.ts +1 -0
  122. package/dist/turn/turn-run.d.ts +15 -3
  123. package/dist/turn/turn-run.js +37 -9
  124. package/dist/turn/turn.adapter.d.ts +6 -5
  125. package/dist/turn/turn.adapter.js +15 -3
  126. package/dist/turn/turn.conformance.d.ts +11 -2
  127. package/dist/turn/turn.conformance.js +63 -2
  128. package/dist/turn/user-message.entity.d.ts +14 -4
  129. package/dist/turn/user-message.entity.functions.d.ts +21 -0
  130. package/dist/turn/user-message.entity.functions.js +24 -0
  131. package/dist/turn/user-message.entity.js +6 -8
  132. package/dist/turn/user-message.entity.test.d.ts +1 -0
  133. package/dist/util/glob.d.ts +30 -0
  134. package/dist/util/glob.js +91 -0
  135. package/dist/util/glob.test.d.ts +1 -0
  136. package/dist/workspace/bash.conformance.d.ts +37 -0
  137. package/dist/workspace/bash.conformance.js +120 -0
  138. package/dist/workspace/bash.conformance.test.d.ts +1 -0
  139. package/dist/workspace/file-system.conformance.d.ts +38 -0
  140. package/dist/workspace/file-system.conformance.js +254 -0
  141. package/dist/workspace/file-system.conformance.test.d.ts +1 -0
  142. package/dist/workspace/file-system.memory.adapter.d.ts +23 -0
  143. package/dist/workspace/file-system.memory.adapter.js +132 -0
  144. package/package.json +1 -1
  145. package/dist/harness/skill-files.d.ts +0 -17
  146. package/dist/harness/skill-files.js +0 -25
  147. package/dist/ports/shell.port.d.ts +0 -32
  148. package/dist/ports/shell.port.js +0 -16
@@ -0,0 +1,110 @@
1
+ import { Effect, Option, Schema } from "effect";
2
+ import { AttachmentRejected, ImageMediaType, MEDIA_LIMITS, MediaFingerprint } from "./media.entity.js";
3
+ export const IMAGE_TOKEN_ESTIMATE = 1600, base64Length = (bytes) => Math.ceil(bytes / 3) * 4;
4
+ const byteAt = (bytes, index) => bytes[index] ?? 0, u16be = (bytes, index) => byteAt(bytes, index) << 8 | byteAt(bytes, index + 1), u16le = (bytes, index) => byteAt(bytes, index) | byteAt(bytes, index + 1) << 8, u24le = (bytes, index) => byteAt(bytes, index) | byteAt(bytes, index + 1) << 8 | byteAt(bytes, index + 2) << 16, u32be = (bytes, index) => byteAt(bytes, index) * 16777216 + (byteAt(bytes, index + 1) << 16 | byteAt(bytes, index + 2) << 8 | byteAt(bytes, index + 3)), ascii = (text) => Array.from(text, (char) => char.charCodeAt(0)), startsWith = (bytes, offset, signature) => bytes.length >= offset + signature.length && signature.every((byte, index) => bytes[offset + index] === byte), PNG = [137, 80, 78, 71, 13, 10, 26, 10], JPEG = [255, 216, 255];
5
+ export const sniffImageType = (bytes) => startsWith(bytes, 0, PNG) ? Option.some("image/png") : startsWith(bytes, 0, JPEG) ? Option.some("image/jpeg") : startsWith(bytes, 0, ascii("GIF87a")) || startsWith(bytes, 0, ascii("GIF89a")) ? Option.some("image/gif") : startsWith(bytes, 0, ascii("RIFF")) && startsWith(bytes, 8, ascii("WEBP")) ? Option.some("image/webp") : Option.none();
6
+ const sized = (width, height) => width > 0 && height > 0 ? Option.some({ width, height }) : Option.none(), JPEG_FRAMES = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]), JPEG_MAX_SEGMENTS = 4096, jpegSize = (bytes, offset, segments) => {
7
+ if (segments > JPEG_MAX_SEGMENTS || offset + 4 > bytes.length || byteAt(bytes, offset) !== 255)
8
+ return Option.none();
9
+ const marker = byteAt(bytes, offset + 1);
10
+ if (marker === 255)
11
+ return jpegSize(bytes, offset + 1, segments + 1);
12
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215)
13
+ return jpegSize(bytes, offset + 2, segments + 1);
14
+ if (JPEG_FRAMES.has(marker))
15
+ return offset + 9 <= bytes.length ? sized(u16be(bytes, offset + 7), u16be(bytes, offset + 5)) : Option.none();
16
+ const length = u16be(bytes, offset + 2);
17
+ return marker === 217 || marker === 218 || length < 2 ? Option.none() : jpegSize(bytes, offset + 2 + length, segments + 1);
18
+ }, webpSize = (bytes) => {
19
+ if (bytes.length < 30)
20
+ return Option.none();
21
+ if (startsWith(bytes, 12, ascii("VP8 ")))
22
+ return startsWith(bytes, 23, [157, 1, 42]) ? sized(u16le(bytes, 26) & 16383, u16le(bytes, 28) & 16383) : Option.none();
23
+ if (startsWith(bytes, 12, ascii("VP8L"))) {
24
+ const [b0, b1, b2, b3] = [byteAt(bytes, 21), byteAt(bytes, 22), byteAt(bytes, 23), byteAt(bytes, 24)];
25
+ return byteAt(bytes, 20) === 47 ? sized(1 + ((b1 & 63) << 8 | b0), 1 + ((b3 & 15) << 10 | b2 << 2 | (b1 & 192) >> 6)) : Option.none();
26
+ }
27
+ return startsWith(bytes, 12, ascii("VP8X")) ? sized(1 + u24le(bytes, 24), 1 + u24le(bytes, 27)) : Option.none();
28
+ };
29
+ export const imageSizeOf = (bytes, mediaType) => {
30
+ if (mediaType === "image/png")
31
+ return bytes.length >= 24 && startsWith(bytes, 12, ascii("IHDR")) ? sized(u32be(bytes, 16), u32be(bytes, 20)) : Option.none();
32
+ if (mediaType === "image/gif")
33
+ return bytes.length >= 10 ? sized(u16le(bytes, 6), u16le(bytes, 8)) : Option.none();
34
+ if (mediaType === "image/webp")
35
+ return webpSize(bytes);
36
+ return jpegSize(bytes, 2, 0);
37
+ };
38
+ const hex8 = (value) => value.toString(16).padStart(8, "0");
39
+ export const mediaFingerprintOf = (bytes) => {
40
+ const fnv = bytes.reduce((hash, byte) => Math.imul(hash ^ byte, 16777619) >>> 0, 2166136261), mixed = bytes.reduce((hash, byte) => {
41
+ const product = Math.imul(hash ^ byte, 1540483477);
42
+ return (product ^ product >>> 15) >>> 0;
43
+ }, 2538058380);
44
+ return MediaFingerprint.make(`${hex8(fnv)}${hex8(mixed)}${bytes.length.toString(16)}`);
45
+ };
46
+ const rejected = (reason, message) => new AttachmentRejected({ reason, message }), described = (description) => ({
47
+ ...description.name === void 0 ? {} : { name: description.name },
48
+ ...description.alt === void 0 ? {} : { alt: description.alt }
49
+ }), encodeBase64 = Schema.encodeSync(Schema.Uint8ArrayFromBase64), decodeBase64 = Schema.decodeUnknownOption(Schema.Uint8ArrayFromBase64);
50
+ export const mediaFromBytes = (bytes, description = {}) => {
51
+ if (bytes.length === 0)
52
+ return Effect.fail(rejected("Empty", "the image has no bytes"));
53
+ if (bytes.length > MEDIA_LIMITS.imageBytes)
54
+ return Effect.fail(rejected("TooLarge", `the image has ${bytes.length} bytes, more than ${MEDIA_LIMITS.imageBytes}`));
55
+ return Option.match(sniffImageType(bytes), {
56
+ onNone: () => Effect.fail(rejected("UnsupportedType", "the bytes are not a PNG, JPEG, GIF or WebP image")),
57
+ onSome: (mediaType) => Option.match(imageSizeOf(bytes, mediaType), {
58
+ onNone: () => Effect.fail(rejected("Unreadable", `the ${mediaType} header does not give the image's size`)),
59
+ onSome: (size) => size.width > MEDIA_LIMITS.maxPixels || size.height > MEDIA_LIMITS.maxPixels ? Effect.fail(rejected("TooManyPixels", `the image is ${size.width}x${size.height} pixels, more than ${MEDIA_LIMITS.maxPixels} a side`)) : Effect.succeed({
60
+ type: "media",
61
+ mediaType,
62
+ data: encodeBase64(bytes),
63
+ fingerprint: mediaFingerprintOf(bytes),
64
+ bytes: bytes.length,
65
+ ...described(description)
66
+ })
67
+ })
68
+ });
69
+ }, mediaFromBase64 = (data, description = {}) => {
70
+ const stripped = data.replace(/^data:[^,]*;base64,/i, "").replace(/\s+/g, "");
71
+ if (stripped.length === 0)
72
+ return Effect.fail(rejected("Empty", "the image has no data"));
73
+ if (stripped.length > base64Length(MEDIA_LIMITS.imageBytes))
74
+ return Effect.fail(rejected("TooLarge", `the image's base64 has ${stripped.length} characters, more than ${base64Length(MEDIA_LIMITS.imageBytes)}`));
75
+ return Option.match(decodeBase64(stripped), {
76
+ onNone: () => Effect.fail(rejected("InvalidBase64", "the image's data is not base64")),
77
+ onSome: (bytes) => mediaFromBytes(bytes, description)
78
+ });
79
+ }, normalizeMedia = (media) => mediaFromBase64(media.data, { ...media.name === void 0 ? {} : { name: media.name }, ...media.alt === void 0 ? {} : { alt: media.alt } });
80
+ const sizeText = (bytes) => bytes < 1024 ? `${bytes} B` : bytes < 1048576 ? `${Math.round(bytes / 1024)} KB` : `${(bytes / 1048576).toFixed(1)} MB`;
81
+ export const mediaLabel = (media) => `[image omitted: ${[media.mediaType, sizeText(media.bytes), ...media.name === void 0 ? [] : [media.name]].join(", ")}${media.alt === void 0 ? "" : `: ${media.alt}`}]`;
82
+ const IMAGE_TYPES = ImageMediaType.literals, isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
83
+ export const isMediaValue = (value) => isRecord(value) && value.type === "media" && typeof value.mediaType === "string" && IMAGE_TYPES.includes(value.mediaType) && typeof value.data === "string" && typeof value.fingerprint === "string" && typeof value.bytes === "number";
84
+ const slotsAt = (value, path) => isMediaValue(value) ? [{ path, fingerprint: value.fingerprint, mediaType: value.mediaType, bytes: value.bytes }] : Array.isArray(value) ? value.flatMap((item, index) => slotsAt(item, [...path, index])) : isRecord(value) ? Object.keys(value).sort().flatMap((key) => slotsAt(value[key], [...path, key])) : [];
85
+ export const mediaSlotsOf = (value) => slotsAt(value, []), mediaAt = (value, slot) => slot.path.reduce((at, key) => Option.flatMap(at, (current) => typeof key === "number" ? Array.isArray(current) ? Option.fromNullishOr(current[key]) : Option.none() : isRecord(current) && Object.hasOwn(current, key) ? Option.fromNullishOr(current[key]) : Option.none()), Option.some(value)).pipe(Option.filter(isMediaValue), Option.filter((media) => media.fingerprint === slot.fingerprint && media.mediaType === slot.mediaType && media.bytes === slot.bytes));
86
+ const unavailable = (reason) => `[image unavailable: ${reason}]`, noTally = { slots: [], labels: [], fingerprints: new Set, chars: 0 };
87
+ export const toolResultMedia = (encoded, slots) => Effect.reduce(slots, () => noTally, (tally, slot) => Option.match(mediaAt(encoded, slot), {
88
+ onNone: () => Effect.succeed({ ...tally, labels: [...tally.labels, unavailable("the result holds no such image")] }),
89
+ onSome: (media) => tally.fingerprints.has(media.fingerprint) ? Effect.succeed(tally) : normalizeMedia(media).pipe(Effect.match({
90
+ onFailure: (error) => ({ ...tally, labels: [...tally.labels, unavailable(error.message)] }),
91
+ onSuccess: (checked) => {
92
+ if (checked.data !== media.data || checked.fingerprint !== media.fingerprint || checked.mediaType !== media.mediaType || checked.bytes !== media.bytes)
93
+ return { ...tally, labels: [...tally.labels, unavailable("its type, size or fingerprint is not what its data gives; build it with mediaFromBytes")] };
94
+ const fingerprints = new Set([...tally.fingerprints, media.fingerprint]);
95
+ return tally.slots.length >= MEDIA_LIMITS.toolResultImages || tally.chars + media.data.length > MEDIA_LIMITS.requestChars ? { ...tally, fingerprints, labels: [...tally.labels, mediaLabel(media)] } : { ...tally, fingerprints, slots: [...tally.slots, slot], chars: tally.chars + media.data.length };
96
+ }
97
+ }))
98
+ })).pipe(Effect.map(({ slots: sent, labels }) => ({ slots: sent, labels }))), elideMediaData = (value) => {
99
+ if (isMediaValue(value))
100
+ return { ...value, data: value.fingerprint };
101
+ if (Array.isArray(value)) {
102
+ const mapped = value.map(elideMediaData);
103
+ return mapped.every((item, index) => item === value[index]) ? value : mapped;
104
+ }
105
+ if (isRecord(value)) {
106
+ const entries = Object.keys(value).map((key) => [key, value[key], elideMediaData(value[key])]);
107
+ return entries.every(([, before, after]) => before === after) ? value : Object.fromEntries(entries.map(([key, , after]) => [key, after]));
108
+ }
109
+ return value;
110
+ };
@@ -0,0 +1,38 @@
1
+ import { Schema } from "effect";
2
+ export const MEDIA_LIMITS = {
3
+ imageBytes: 3932160,
4
+ maxPixels: 8000,
5
+ attachments: 8,
6
+ requestImages: 8,
7
+ requestChars: 20000000,
8
+ toolResultImages: 4
9
+ }, ImageMediaType = Schema.Literals(["image/png", "image/jpeg", "image/gif", "image/webp"]), MediaFingerprint = Schema.String.check(Schema.isPattern(/^[0-9a-f]{16,}$/)).pipe(Schema.brand("MediaFingerprint")), Media = Schema.Struct({
10
+ type: Schema.Literal("media"),
11
+ mediaType: ImageMediaType,
12
+ data: Schema.String.check(Schema.isNonEmpty(), Schema.isMaxLength(Math.ceil(MEDIA_LIMITS.imageBytes / 3) * 4), Schema.isBase64()),
13
+ fingerprint: MediaFingerprint,
14
+ bytes: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(MEDIA_LIMITS.imageBytes)),
15
+ name: Schema.optionalKey(Schema.String),
16
+ alt: Schema.optionalKey(Schema.String)
17
+ }), MediaSlot = Schema.Struct({
18
+ path: Schema.Array(Schema.Union([Schema.String, Schema.Int])),
19
+ fingerprint: MediaFingerprint,
20
+ mediaType: ImageMediaType,
21
+ bytes: Schema.Int
22
+ }), AttachmentRejectedReason = Schema.Literals([
23
+ "Empty",
24
+ "InvalidBase64",
25
+ "UnsupportedType",
26
+ "TooLarge",
27
+ "TooManyPixels",
28
+ "Unreadable",
29
+ "TooMany",
30
+ "TooLargeTogether"
31
+ ]);
32
+
33
+ export class AttachmentRejected extends Schema.TaggedError()("AttachmentRejected", {
34
+ reason: AttachmentRejectedReason,
35
+ index: Schema.optionalKey(Schema.Int),
36
+ message: Schema.String
37
+ }) {
38
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -48,12 +48,59 @@ export declare const ToolResultPart: Schema.Struct<{
48
48
  readonly output: Schema.Unknown;
49
49
  readonly providerOptions: Schema.optional<Schema.Unknown>;
50
50
  readonly isError: Schema.optional<Schema.Boolean>;
51
+ /**
52
+ * Images sent with the result, after `output`'s text: memory's render of
53
+ * the images a result's slots name (`LogBody.ToolResult.media`). Only a
54
+ * rendered message has them; the loop's own tail keeps a result's images
55
+ * inside `output`, as the tool encoded them.
56
+ */
57
+ readonly media: Schema.optionalKey<Schema.$Array<Schema.Struct<{
58
+ readonly type: Schema.Literal<"media">;
59
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
60
+ readonly data: Schema.String;
61
+ readonly fingerprint: Schema.brand<Schema.String, "MediaFingerprint">;
62
+ readonly bytes: Schema.Int;
63
+ readonly name: Schema.optionalKey<Schema.String>;
64
+ readonly alt: Schema.optionalKey<Schema.String>;
65
+ }>>>;
51
66
  }>;
52
67
  export type ToolResultPart = typeof ToolResultPart.Type;
53
- /** A model-visible message in the user role (the user's own words, turn context, host notes). */
68
+ /** One part of a multi-part user message: text, or an image sent inline. */
69
+ export declare const UserContentPart: Schema.Union<readonly [Schema.Struct<{
70
+ readonly type: Schema.Literal<"text">;
71
+ readonly text: Schema.String;
72
+ readonly providerOptions: Schema.optional<Schema.Unknown>;
73
+ }>, Schema.Struct<{
74
+ readonly type: Schema.Literal<"media">;
75
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
76
+ readonly data: Schema.String;
77
+ readonly fingerprint: Schema.brand<Schema.String, "MediaFingerprint">;
78
+ readonly bytes: Schema.Int;
79
+ readonly name: Schema.optionalKey<Schema.String>;
80
+ readonly alt: Schema.optionalKey<Schema.String>;
81
+ }>]>;
82
+ export type UserContentPart = typeof UserContentPart.Type;
83
+ /**
84
+ * A model-visible message in the user role (the user's own words, turn
85
+ * context, host notes): text, or parts when it carries images (a user's
86
+ * attachments). Narrow with `typeof content === "string"` before reading it
87
+ * as text.
88
+ */
54
89
  export declare const UserRoleMessage: Schema.Struct<{
55
90
  readonly role: Schema.Literal<"user">;
56
- readonly content: Schema.String;
91
+ readonly content: Schema.Union<readonly [Schema.String, Schema.$Array<Schema.Union<readonly [Schema.Struct<{
92
+ readonly type: Schema.Literal<"text">;
93
+ readonly text: Schema.String;
94
+ readonly providerOptions: Schema.optional<Schema.Unknown>;
95
+ }>, Schema.Struct<{
96
+ readonly type: Schema.Literal<"media">;
97
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
98
+ readonly data: Schema.String;
99
+ readonly fingerprint: Schema.brand<Schema.String, "MediaFingerprint">;
100
+ readonly bytes: Schema.Int;
101
+ readonly name: Schema.optionalKey<Schema.String>;
102
+ readonly alt: Schema.optionalKey<Schema.String>;
103
+ }>]>>]>;
57
104
  readonly providerOptions: Schema.optional<Schema.Unknown>;
58
105
  }>;
59
106
  export type UserRoleMessage = typeof UserRoleMessage.Type;
@@ -87,6 +134,21 @@ export declare const ToolMessage: Schema.Struct<{
87
134
  readonly output: Schema.Unknown;
88
135
  readonly providerOptions: Schema.optional<Schema.Unknown>;
89
136
  readonly isError: Schema.optional<Schema.Boolean>;
137
+ /**
138
+ * Images sent with the result, after `output`'s text: memory's render of
139
+ * the images a result's slots name (`LogBody.ToolResult.media`). Only a
140
+ * rendered message has them; the loop's own tail keeps a result's images
141
+ * inside `output`, as the tool encoded them.
142
+ */
143
+ readonly media: Schema.optionalKey<Schema.$Array<Schema.Struct<{
144
+ readonly type: Schema.Literal<"media">;
145
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
146
+ readonly data: Schema.String;
147
+ readonly fingerprint: Schema.brand<Schema.String, "MediaFingerprint">;
148
+ readonly bytes: Schema.Int;
149
+ readonly name: Schema.optionalKey<Schema.String>;
150
+ readonly alt: Schema.optionalKey<Schema.String>;
151
+ }>>>;
90
152
  }>>;
91
153
  readonly providerOptions: Schema.optional<Schema.Unknown>;
92
154
  }>;
@@ -94,7 +156,19 @@ export type ToolMessage = typeof ToolMessage.Type;
94
156
  /** The single conversation unit — persisted as-is, one row per entry. */
95
157
  export declare const AgentMessage: Schema.Union<readonly [Schema.Struct<{
96
158
  readonly role: Schema.Literal<"user">;
97
- readonly content: Schema.String;
159
+ readonly content: Schema.Union<readonly [Schema.String, Schema.$Array<Schema.Union<readonly [Schema.Struct<{
160
+ readonly type: Schema.Literal<"text">;
161
+ readonly text: Schema.String;
162
+ readonly providerOptions: Schema.optional<Schema.Unknown>;
163
+ }>, Schema.Struct<{
164
+ readonly type: Schema.Literal<"media">;
165
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
166
+ readonly data: Schema.String;
167
+ readonly fingerprint: Schema.brand<Schema.String, "MediaFingerprint">;
168
+ readonly bytes: Schema.Int;
169
+ readonly name: Schema.optionalKey<Schema.String>;
170
+ readonly alt: Schema.optionalKey<Schema.String>;
171
+ }>]>>]>;
98
172
  readonly providerOptions: Schema.optional<Schema.Unknown>;
99
173
  }>, Schema.Struct<{
100
174
  readonly role: Schema.Literal<"assistant">;
@@ -124,6 +198,21 @@ export declare const AgentMessage: Schema.Union<readonly [Schema.Struct<{
124
198
  readonly output: Schema.Unknown;
125
199
  readonly providerOptions: Schema.optional<Schema.Unknown>;
126
200
  readonly isError: Schema.optional<Schema.Boolean>;
201
+ /**
202
+ * Images sent with the result, after `output`'s text: memory's render of
203
+ * the images a result's slots name (`LogBody.ToolResult.media`). Only a
204
+ * rendered message has them; the loop's own tail keeps a result's images
205
+ * inside `output`, as the tool encoded them.
206
+ */
207
+ readonly media: Schema.optionalKey<Schema.$Array<Schema.Struct<{
208
+ readonly type: Schema.Literal<"media">;
209
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
210
+ readonly data: Schema.String;
211
+ readonly fingerprint: Schema.brand<Schema.String, "MediaFingerprint">;
212
+ readonly bytes: Schema.Int;
213
+ readonly name: Schema.optionalKey<Schema.String>;
214
+ readonly alt: Schema.optionalKey<Schema.String>;
215
+ }>>>;
127
216
  }>>;
128
217
  readonly providerOptions: Schema.optional<Schema.Unknown>;
129
218
  }>]>;
@@ -137,7 +226,19 @@ export declare const AgentResult: Schema.Struct<{
137
226
  readonly finalText: Schema.String;
138
227
  readonly messages: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
139
228
  readonly role: Schema.Literal<"user">;
140
- readonly content: Schema.String;
229
+ readonly content: Schema.Union<readonly [Schema.String, Schema.$Array<Schema.Union<readonly [Schema.Struct<{
230
+ readonly type: Schema.Literal<"text">;
231
+ readonly text: Schema.String;
232
+ readonly providerOptions: Schema.optional<Schema.Unknown>;
233
+ }>, Schema.Struct<{
234
+ readonly type: Schema.Literal<"media">;
235
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
236
+ readonly data: Schema.String;
237
+ readonly fingerprint: Schema.brand<Schema.String, "MediaFingerprint">;
238
+ readonly bytes: Schema.Int;
239
+ readonly name: Schema.optionalKey<Schema.String>;
240
+ readonly alt: Schema.optionalKey<Schema.String>;
241
+ }>]>>]>;
141
242
  readonly providerOptions: Schema.optional<Schema.Unknown>;
142
243
  }>, Schema.Struct<{
143
244
  readonly role: Schema.Literal<"assistant">;
@@ -167,12 +268,39 @@ export declare const AgentResult: Schema.Struct<{
167
268
  readonly output: Schema.Unknown;
168
269
  readonly providerOptions: Schema.optional<Schema.Unknown>;
169
270
  readonly isError: Schema.optional<Schema.Boolean>;
271
+ /**
272
+ * Images sent with the result, after `output`'s text: memory's render of
273
+ * the images a result's slots name (`LogBody.ToolResult.media`). Only a
274
+ * rendered message has them; the loop's own tail keeps a result's images
275
+ * inside `output`, as the tool encoded them.
276
+ */
277
+ readonly media: Schema.optionalKey<Schema.$Array<Schema.Struct<{
278
+ readonly type: Schema.Literal<"media">;
279
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
280
+ readonly data: Schema.String;
281
+ readonly fingerprint: Schema.brand<Schema.String, "MediaFingerprint">;
282
+ readonly bytes: Schema.Int;
283
+ readonly name: Schema.optionalKey<Schema.String>;
284
+ readonly alt: Schema.optionalKey<Schema.String>;
285
+ }>>>;
170
286
  }>>;
171
287
  readonly providerOptions: Schema.optional<Schema.Unknown>;
172
288
  }>]>>;
173
289
  readonly newTail: Schema.$Array<Schema.Union<readonly [Schema.Struct<{
174
290
  readonly role: Schema.Literal<"user">;
175
- readonly content: Schema.String;
291
+ readonly content: Schema.Union<readonly [Schema.String, Schema.$Array<Schema.Union<readonly [Schema.Struct<{
292
+ readonly type: Schema.Literal<"text">;
293
+ readonly text: Schema.String;
294
+ readonly providerOptions: Schema.optional<Schema.Unknown>;
295
+ }>, Schema.Struct<{
296
+ readonly type: Schema.Literal<"media">;
297
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
298
+ readonly data: Schema.String;
299
+ readonly fingerprint: Schema.brand<Schema.String, "MediaFingerprint">;
300
+ readonly bytes: Schema.Int;
301
+ readonly name: Schema.optionalKey<Schema.String>;
302
+ readonly alt: Schema.optionalKey<Schema.String>;
303
+ }>]>>]>;
176
304
  readonly providerOptions: Schema.optional<Schema.Unknown>;
177
305
  }>, Schema.Struct<{
178
306
  readonly role: Schema.Literal<"assistant">;
@@ -202,6 +330,21 @@ export declare const AgentResult: Schema.Struct<{
202
330
  readonly output: Schema.Unknown;
203
331
  readonly providerOptions: Schema.optional<Schema.Unknown>;
204
332
  readonly isError: Schema.optional<Schema.Boolean>;
333
+ /**
334
+ * Images sent with the result, after `output`'s text: memory's render of
335
+ * the images a result's slots name (`LogBody.ToolResult.media`). Only a
336
+ * rendered message has them; the loop's own tail keeps a result's images
337
+ * inside `output`, as the tool encoded them.
338
+ */
339
+ readonly media: Schema.optionalKey<Schema.$Array<Schema.Struct<{
340
+ readonly type: Schema.Literal<"media">;
341
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
342
+ readonly data: Schema.String;
343
+ readonly fingerprint: Schema.brand<Schema.String, "MediaFingerprint">;
344
+ readonly bytes: Schema.Int;
345
+ readonly name: Schema.optionalKey<Schema.String>;
346
+ readonly alt: Schema.optionalKey<Schema.String>;
347
+ }>>>;
205
348
  }>>;
206
349
  readonly providerOptions: Schema.optional<Schema.Unknown>;
207
350
  }>]>>;
@@ -209,6 +352,15 @@ export declare const AgentResult: Schema.Struct<{
209
352
  * degenerate-loop breaker stopped it, so `finalText` is NOT a deliverable. */
210
353
  readonly outcome: Schema.Literals<readonly ["ok", "partial"]>;
211
354
  readonly reason: Schema.Literals<readonly ["completed", "step-cap", "degenerate-loop"]>;
355
+ /** The run's provider calls summed (its assistant messages' usage). */
356
+ readonly usage: Schema.Struct<{
357
+ readonly inputTokens: Schema.Number;
358
+ readonly outputTokens: Schema.Number;
359
+ readonly totalTokens: Schema.Number;
360
+ readonly cacheReadTokens: Schema.Number;
361
+ readonly cacheWriteTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
362
+ readonly reasoningTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
363
+ }>;
212
364
  }>;
213
365
  export type AgentResult = typeof AgentResult.Type;
214
366
  export {};
@@ -1,4 +1,6 @@
1
1
  import { Schema } from "effect";
2
+ import { Media } from "./media.entity.js";
3
+ import { TokenUsage } from "./token-usage.entity.js";
2
4
  export const ConversationId = Schema.String.check(Schema.isUUID()).pipe(Schema.brand("ConversationId")), ToolCallId = Schema.String.pipe(Schema.brand("ToolCallId"));
3
5
 
4
6
  export class Checkpoint extends Schema.Class("Checkpoint")({
@@ -29,10 +31,11 @@ export const TextPart = Schema.Struct({
29
31
  toolName: Schema.String,
30
32
  output: Schema.Unknown,
31
33
  providerOptions: Schema.optional(Schema.Unknown),
32
- isError: Schema.optional(Schema.Boolean)
33
- }), UserRoleMessage = Schema.Struct({
34
+ isError: Schema.optional(Schema.Boolean),
35
+ media: Schema.optionalKey(Schema.Array(Media))
36
+ }), UserContentPart = Schema.Union([TextPart, Media]), UserRoleMessage = Schema.Struct({
34
37
  role: Schema.Literal("user"),
35
- content: Schema.String,
38
+ content: Schema.Union([Schema.String, Schema.Array(UserContentPart)]),
36
39
  providerOptions: Schema.optional(Schema.Unknown)
37
40
  }), AssistantMessage = Schema.Struct({
38
41
  role: Schema.Literal("assistant"),
@@ -47,5 +50,6 @@ export const TextPart = Schema.Struct({
47
50
  messages: Schema.Array(AgentMessage),
48
51
  newTail: Schema.Array(AgentMessage),
49
52
  outcome: Schema.Literals(["ok", "partial"]),
50
- reason: Schema.Literals(["completed", "step-cap", "degenerate-loop"])
53
+ reason: Schema.Literals(["completed", "step-cap", "degenerate-loop"]),
54
+ usage: TokenUsage
51
55
  });
@@ -6,5 +6,7 @@ export declare const ModelCatalogEntry: Schema.Struct<{
6
6
  readonly label: Schema.optional<Schema.String>;
7
7
  readonly provider: Schema.String;
8
8
  readonly credential: Schema.Literals<readonly ["api_key", "oauth", "local"]>;
9
+ /** What the model reads: text, and images when its route sends them (a host can grey out an attach button). Absent: not stated. */
10
+ readonly input: Schema.optionalKey<Schema.$Array<Schema.Literals<readonly ["text", "image"]>>>;
9
11
  }>;
10
12
  export type ModelCatalogEntry = typeof ModelCatalogEntry.Type;
@@ -3,5 +3,6 @@ export const ModelCatalogEntry = Schema.Struct({
3
3
  selection: Schema.String,
4
4
  label: Schema.optional(Schema.String),
5
5
  provider: Schema.String,
6
- credential: Schema.Literals(["api_key", "oauth", "local"])
6
+ credential: Schema.Literals(["api_key", "oauth", "local"]),
7
+ input: Schema.optionalKey(Schema.Array(Schema.Literals(["text", "image"])))
7
8
  });
@@ -1,9 +1,53 @@
1
1
  import { Schema } from "effect";
2
- /** Billed tokens for one model call (or a sum of them). */
2
+ /**
3
+ * Billed tokens for one model call (or a sum of them). The invariants
4
+ * (`tokenUsage` and `extractUsage` hold them):
5
+ * - `inputTokens` is the full input: uncached input, cache reads and cache
6
+ * writes (`uncachedInputTokens(u)` is the rest);
7
+ * - `outputTokens` is the full output, reasoning included;
8
+ * - `totalTokens = inputTokens + outputTokens`.
9
+ * `cacheWriteTokens` and `reasoningTokens` are missing from records written
10
+ * before 0.14.0; those decode with 0.
11
+ */
3
12
  export declare const TokenUsage: Schema.Struct<{
4
13
  readonly inputTokens: Schema.Number;
5
14
  readonly outputTokens: Schema.Number;
6
15
  readonly totalTokens: Schema.Number;
7
16
  readonly cacheReadTokens: Schema.Number;
17
+ readonly cacheWriteTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
18
+ readonly reasoningTokens: Schema.withDecodingDefaultKey<Schema.Number, never>;
8
19
  }>;
9
20
  export type TokenUsage = typeof TokenUsage.Type;
21
+ /**
22
+ * One usage split into the five kinds a call is billed by. Each is at least
23
+ * 0, and for a usage that holds the invariants they sum to `totalTokens`:
24
+ * `input` is the uncached input, `output` the output without reasoning.
25
+ */
26
+ export declare const UsageBreakdown: Schema.Struct<{
27
+ readonly input: Schema.Number;
28
+ readonly cacheRead: Schema.Number;
29
+ readonly cacheWrite: Schema.Number;
30
+ readonly reasoning: Schema.Number;
31
+ readonly output: Schema.Number;
32
+ }>;
33
+ export type UsageBreakdown = typeof UsageBreakdown.Type;
34
+ /**
35
+ * A request's token budget (`budgetTokens`): an integer of at least 1. It is
36
+ * the target memory compaction keeps each request under (system prompt,
37
+ * tool schemas and messages, estimated), not a provider limit.
38
+ */
39
+ export declare const BudgetTokens: Schema.Int;
40
+ export type BudgetTokens = typeof BudgetTokens.Type;
41
+ /**
42
+ * A model's prices, per million tokens (in the host's currency, usually
43
+ * USD). `reasoning` is optional: reasoning tokens are priced as output
44
+ * without it.
45
+ */
46
+ export declare const ModelPrice: Schema.Struct<{
47
+ readonly input: Schema.Finite;
48
+ readonly output: Schema.Finite;
49
+ readonly cacheRead: Schema.Finite;
50
+ readonly cacheWrite: Schema.Finite;
51
+ readonly reasoning: Schema.optionalKey<Schema.Finite>;
52
+ }>;
53
+ export type ModelPrice = typeof ModelPrice.Type;
@@ -1,3 +1,32 @@
1
- import type { TokenUsage } from "./token-usage.entity.js";
1
+ import { BudgetTokens } from "./token-usage.entity.js";
2
+ import type { ModelPrice, TokenUsage, UsageBreakdown } from "./token-usage.entity.js";
3
+ /** Whether a value is a valid token budget: an integer of at least 1 (`BudgetTokens`). */
4
+ export declare const isBudgetTokens: (value: unknown) => value is BudgetTokens;
2
5
  export declare const zeroUsage: TokenUsage;
6
+ /**
7
+ * A usage from a call's counts, with the invariants held: the input covers
8
+ * its cache reads and writes, the output covers its reasoning (a provider
9
+ * reporting more reasoning than output gets output = reasoning), the total
10
+ * is input plus output, and no count is negative.
11
+ */
12
+ export declare const tokenUsage: (counts: {
13
+ readonly inputTokens: number;
14
+ readonly outputTokens: number;
15
+ readonly cacheReadTokens?: number;
16
+ readonly cacheWriteTokens?: number;
17
+ readonly reasoningTokens?: number;
18
+ }) => TokenUsage;
3
19
  export declare const addUsage: (a: TokenUsage, b: TokenUsage) => TokenUsage;
20
+ /** The sum of many usages; `zeroUsage` for none. */
21
+ export declare const sumUsage: (usages: ReadonlyArray<TokenUsage>) => TokenUsage;
22
+ /** The input neither read from nor written to a cache: input minus both, at least 0. */
23
+ export declare const uncachedInputTokens: (usage: TokenUsage) => number;
24
+ /** The five billed kinds of a usage, each at least 0 (see `UsageBreakdown`). */
25
+ export declare const usageBreakdown: (usage: TokenUsage) => UsageBreakdown;
26
+ /**
27
+ * What a usage costs at `price` (per million tokens): each kind of
28
+ * `usageBreakdown` at its price, reasoning at `price.reasoning` or else
29
+ * the output price. Cache writes have one price (a provider's different
30
+ * write durations are not told apart).
31
+ */
32
+ export declare const costOf: (usage: TokenUsage, price: ModelPrice) => number;
@@ -1,11 +1,31 @@
1
- export const zeroUsage = {
1
+ import { Schema } from "effect";
2
+ import { BudgetTokens } from "./token-usage.entity.js";
3
+ export const isBudgetTokens = Schema.is(BudgetTokens), zeroUsage = {
2
4
  inputTokens: 0,
3
5
  outputTokens: 0,
4
6
  totalTokens: 0,
5
- cacheReadTokens: 0
7
+ cacheReadTokens: 0,
8
+ cacheWriteTokens: 0,
9
+ reasoningTokens: 0
10
+ };
11
+ const count = (value) => Math.max(0, value ?? 0);
12
+ export const tokenUsage = (counts) => {
13
+ const cacheReadTokens = count(counts.cacheReadTokens), cacheWriteTokens = count(counts.cacheWriteTokens), reasoningTokens = count(counts.reasoningTokens), inputTokens = Math.max(count(counts.inputTokens), cacheReadTokens + cacheWriteTokens), outputTokens = Math.max(count(counts.outputTokens), reasoningTokens);
14
+ return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens };
6
15
  }, addUsage = (a, b) => ({
7
16
  inputTokens: a.inputTokens + b.inputTokens,
8
17
  outputTokens: a.outputTokens + b.outputTokens,
9
18
  totalTokens: a.totalTokens + b.totalTokens,
10
- cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens
11
- });
19
+ cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
20
+ cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens,
21
+ reasoningTokens: a.reasoningTokens + b.reasoningTokens
22
+ }), sumUsage = (usages) => usages.reduce(addUsage, zeroUsage), uncachedInputTokens = (usage) => Math.max(0, usage.inputTokens - usage.cacheReadTokens - usage.cacheWriteTokens), usageBreakdown = (usage) => ({
23
+ input: uncachedInputTokens(usage),
24
+ cacheRead: Math.max(0, usage.cacheReadTokens),
25
+ cacheWrite: Math.max(0, usage.cacheWriteTokens),
26
+ reasoning: Math.max(0, usage.reasoningTokens),
27
+ output: Math.max(0, usage.outputTokens - usage.reasoningTokens)
28
+ }), costOf = (usage, price) => {
29
+ const kinds = usageBreakdown(usage);
30
+ return (kinds.input * price.input + kinds.cacheRead * price.cacheRead + kinds.cacheWrite * price.cacheWrite + kinds.reasoning * (price.reasoning ?? price.output) + kinds.output * price.output) / 1e6;
31
+ };
@@ -1,7 +1,24 @@
1
- import { Schema } from "effect";
1
+ import { Effect, Schema } from "effect";
2
+ const LaterCount = Schema.Number.pipe(Schema.withDecodingDefaultKey(Effect.succeed(0)));
2
3
  export const TokenUsage = Schema.Struct({
3
4
  inputTokens: Schema.Number,
4
5
  outputTokens: Schema.Number,
5
6
  totalTokens: Schema.Number,
6
- cacheReadTokens: Schema.Number
7
+ cacheReadTokens: Schema.Number,
8
+ cacheWriteTokens: LaterCount,
9
+ reasoningTokens: LaterCount
10
+ }), UsageBreakdown = Schema.Struct({
11
+ input: Schema.Number,
12
+ cacheRead: Schema.Number,
13
+ cacheWrite: Schema.Number,
14
+ reasoning: Schema.Number,
15
+ output: Schema.Number
16
+ }), BudgetTokens = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(1)));
17
+ const Price = Schema.Finite.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)));
18
+ export const ModelPrice = Schema.Struct({
19
+ input: Price,
20
+ output: Price,
21
+ cacheRead: Price,
22
+ cacheWrite: Price,
23
+ reasoning: Schema.optionalKey(Price)
7
24
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -18,7 +18,8 @@ export const defaultAnnotations = {
18
18
  compact: Option.fromNullishOr(view.compact),
19
19
  subjects: view.subjects ?? (() => []),
20
20
  artifacts: view.artifacts ?? (() => []),
21
- digest: Option.fromNullishOr(view.digest)
21
+ digest: Option.fromNullishOr(view.digest),
22
+ media: Option.fromNullishOr(view.media)
22
23
  })),
23
24
  annotations: { ...defaultAnnotations, ...definition.annotations }
24
25
  }), defineSkill = (skill) => ({
@@ -5,6 +5,9 @@ import type { TypedPlugin } from "./plugin.entity.js";
5
5
  * `build` (the graph's) and `live` (a host's own composition) share one
6
6
  * decode: the options merged over the defaults, extra keys refused — so a
7
7
  * graph caller, whose options already hold the defaults, decodes the same.
8
+ * A `build` whose layer fails with a `HarnessError` fails with that error's
9
+ * code, as `live` does (and names the plugin when the error does not); any
10
+ * other failure is `plugin.activation`, and a defect `plugin.defect`.
8
11
  */
9
12
  export declare const definePlugin: <A extends Readonly<Record<string, unknown>>, I, Out, E, In>(definition: {
10
13
  readonly id: string;
@@ -1,5 +1,11 @@
1
1
  import { Context, Effect, Layer, Schema } from "effect";
2
2
  import { HarnessError, PLUGIN_API_VERSION } from "./plugin.entity.js";
3
+ const isHarnessError = Schema.is(Schema.Struct({
4
+ _tag: Schema.Literal("HarnessError"),
5
+ code: Schema.String,
6
+ message: Schema.String,
7
+ plugin: Schema.optional(Schema.String)
8
+ }));
3
9
  export const definePlugin = (definition) => {
4
10
  const decodeOptions = (options) => Schema.decodeUnknownEffect(definition.config)(typeof options === "object" && options !== null ? { ...definition.defaults, ...options } : options ?? definition.defaults, { onExcessProperty: "error" });
5
11
  return {
@@ -14,7 +20,7 @@ export const definePlugin = (definition) => {
14
20
  schema: definition.config,
15
21
  config: definition.config,
16
22
  defaults: definition.defaults,
17
- build: (options, services) => decodeOptions(options).pipe(Effect.flatMap((config) => Layer.build(Layer.fresh(definition.layer(config))).pipe(Effect.provide(Context.makeUnsafe(services.mapUnsafe)), Effect.map((built) => Context.makeUnsafe(Context.omit(Layer.CurrentMemoMap)(built).mapUnsafe)))), Effect.mapError((error) => new HarnessError({ code: "plugin.activation", plugin: definition.id, message: String(error) })), Effect.catchDefect((error) => Effect.fail(new HarnessError({ code: "plugin.defect", plugin: definition.id, message: String(error) })))),
23
+ build: (options, services) => decodeOptions(options).pipe(Effect.flatMap((config) => Layer.build(Layer.fresh(definition.layer(config))).pipe(Effect.provide(Context.makeUnsafe(services.mapUnsafe)), Effect.map((built) => Context.makeUnsafe(Context.omit(Layer.CurrentMemoMap)(built).mapUnsafe)))), Effect.mapError((error) => isHarnessError(error) ? new HarnessError({ code: error.code, message: error.message, plugin: error.plugin ?? definition.id }) : new HarnessError({ code: "plugin.activation", plugin: definition.id, message: String(error) })), Effect.catchDefect((error) => Effect.fail(new HarnessError({ code: "plugin.defect", plugin: definition.id, message: String(error) })))),
18
24
  live: (options) => Layer.unwrap(decodeOptions(options).pipe(Effect.mapError((error) => new HarnessError({ code: "config.options", plugin: definition.id, message: String(error) })), Effect.map(definition.layer)))
19
25
  };
20
26
  };