@ddwang/magnitude-core 0.3.1-ddwang.1

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 (142) hide show
  1. package/dist/actions/desktopActions.d.ts +58 -0
  2. package/dist/actions/desktopActions.js +154 -0
  3. package/dist/actions/index.d.ts +26 -0
  4. package/dist/actions/index.js +11 -0
  5. package/dist/actions/taskActions.d.ts +8 -0
  6. package/dist/actions/taskActions.js +27 -0
  7. package/dist/actions/types.d.ts +31 -0
  8. package/dist/actions/types.js +5 -0
  9. package/dist/actions/util.d.ts +20 -0
  10. package/dist/actions/util.js +297 -0
  11. package/dist/actions/webActions.d.ts +79 -0
  12. package/dist/actions/webActions.js +190 -0
  13. package/dist/agent/browserAgent.d.ts +29 -0
  14. package/dist/agent/browserAgent.js +122 -0
  15. package/dist/agent/errors.d.ts +8 -0
  16. package/dist/agent/errors.js +68 -0
  17. package/dist/agent/index.d.ts +53 -0
  18. package/dist/agent/index.js +377 -0
  19. package/dist/agent/narrator.d.ts +4 -0
  20. package/dist/agent/narrator.js +63 -0
  21. package/dist/agent/pause.test.d.ts +1 -0
  22. package/dist/agent/pause.test.js +194 -0
  23. package/dist/ai/baml_client/async_client.d.ts +73 -0
  24. package/dist/ai/baml_client/async_client.js +152 -0
  25. package/dist/ai/baml_client/async_request.d.ts +35 -0
  26. package/dist/ai/baml_client/async_request.js +98 -0
  27. package/dist/ai/baml_client/config.d.ts +11 -0
  28. package/dist/ai/baml_client/config.js +20 -0
  29. package/dist/ai/baml_client/globals.d.ts +16 -0
  30. package/dist/ai/baml_client/globals.js +37 -0
  31. package/dist/ai/baml_client/index.d.ts +16 -0
  32. package/dist/ai/baml_client/index.js +41 -0
  33. package/dist/ai/baml_client/inlinedbaml.d.ts +19 -0
  34. package/dist/ai/baml_client/inlinedbaml.js +31 -0
  35. package/dist/ai/baml_client/parser.d.ts +53 -0
  36. package/dist/ai/baml_client/parser.js +86 -0
  37. package/dist/ai/baml_client/partial_types.d.ts +61 -0
  38. package/dist/ai/baml_client/partial_types.js +10 -0
  39. package/dist/ai/baml_client/sync_client.d.ts +50 -0
  40. package/dist/ai/baml_client/sync_client.js +100 -0
  41. package/dist/ai/baml_client/sync_request.d.ts +35 -0
  42. package/dist/ai/baml_client/sync_request.js +98 -0
  43. package/dist/ai/baml_client/tracing.d.ts +16 -0
  44. package/dist/ai/baml_client/tracing.js +18 -0
  45. package/dist/ai/baml_client/type_builder.d.ts +38 -0
  46. package/dist/ai/baml_client/type_builder.js +96 -0
  47. package/dist/ai/baml_client/types.d.ts +69 -0
  48. package/dist/ai/baml_client/types.js +15 -0
  49. package/dist/ai/claudeCode.d.ts +1 -0
  50. package/dist/ai/claudeCode.js +186 -0
  51. package/dist/ai/modelHarness.d.ts +39 -0
  52. package/dist/ai/modelHarness.js +189 -0
  53. package/dist/ai/multiModelHarness.d.ts +27 -0
  54. package/dist/ai/multiModelHarness.js +52 -0
  55. package/dist/ai/types.d.ts +90 -0
  56. package/dist/ai/types.js +5 -0
  57. package/dist/ai/util.d.ts +13 -0
  58. package/dist/ai/util.js +165 -0
  59. package/dist/cli.d.ts +0 -0
  60. package/dist/cli.js +3 -0
  61. package/dist/common/actions.d.ts +0 -0
  62. package/dist/common/actions.js +14 -0
  63. package/dist/common/events.d.ts +15 -0
  64. package/dist/common/events.js +3 -0
  65. package/dist/common/failure.d.ts +68 -0
  66. package/dist/common/failure.js +1 -0
  67. package/dist/common/index.d.ts +3 -0
  68. package/dist/common/index.js +3 -0
  69. package/dist/common/retry.d.ts +16 -0
  70. package/dist/common/retry.js +30 -0
  71. package/dist/common/util.d.ts +23 -0
  72. package/dist/common/util.js +132 -0
  73. package/dist/connectors/browserConnector.d.ts +40 -0
  74. package/dist/connectors/browserConnector.js +127 -0
  75. package/dist/connectors/desktopConnector.d.ts +58 -0
  76. package/dist/connectors/desktopConnector.js +68 -0
  77. package/dist/connectors/index.d.ts +10 -0
  78. package/dist/connectors/index.js +2 -0
  79. package/dist/index.cjs +6143 -0
  80. package/dist/index.d.cts +825 -0
  81. package/dist/index.d.ts +20 -0
  82. package/dist/index.js +26 -0
  83. package/dist/index.mjs +6097 -0
  84. package/dist/logger.d.ts +3 -0
  85. package/dist/logger.js +15 -0
  86. package/dist/memory/agentMemory.d.ts +37 -0
  87. package/dist/memory/agentMemory.js +121 -0
  88. package/dist/memory/image.d.ts +26 -0
  89. package/dist/memory/image.js +106 -0
  90. package/dist/memory/index.d.ts +3 -0
  91. package/dist/memory/index.js +3 -0
  92. package/dist/memory/masking.d.ts +7 -0
  93. package/dist/memory/masking.js +141 -0
  94. package/dist/memory/masking.test.d.ts +1 -0
  95. package/dist/memory/masking.test.js +117 -0
  96. package/dist/memory/observation.d.ts +38 -0
  97. package/dist/memory/observation.js +79 -0
  98. package/dist/memory/rendering/index.d.ts +10 -0
  99. package/dist/memory/rendering/index.js +13 -0
  100. package/dist/memory/rendering/renderJsonParts.d.ts +3 -0
  101. package/dist/memory/rendering/renderJsonParts.js +115 -0
  102. package/dist/memory/rendering/renderJsonParts.test.d.ts +1 -0
  103. package/dist/memory/rendering/renderJsonParts.test.js +217 -0
  104. package/dist/memory/rendering/renderXmlParts.d.ts +7 -0
  105. package/dist/memory/rendering/renderXmlParts.js +127 -0
  106. package/dist/memory/serde.d.ts +20 -0
  107. package/dist/memory/serde.js +81 -0
  108. package/dist/memory/util.d.ts +2 -0
  109. package/dist/memory/util.js +17 -0
  110. package/dist/telemetry/events.d.ts +9 -0
  111. package/dist/telemetry/events.js +80 -0
  112. package/dist/telemetry/index.d.ts +6 -0
  113. package/dist/telemetry/index.js +91 -0
  114. package/dist/types.d.ts +30 -0
  115. package/dist/types.js +1 -0
  116. package/dist/util.d.ts +1 -0
  117. package/dist/util.js +12 -0
  118. package/dist/version.d.ts +1 -0
  119. package/dist/version.js +3 -0
  120. package/dist/web/browserProvider.d.ts +23 -0
  121. package/dist/web/browserProvider.js +153 -0
  122. package/dist/web/harness.d.ts +102 -0
  123. package/dist/web/harness.js +340 -0
  124. package/dist/web/stability.d.ts +57 -0
  125. package/dist/web/stability.js +316 -0
  126. package/dist/web/tabs.d.ts +58 -0
  127. package/dist/web/tabs.js +293 -0
  128. package/dist/web/transformer.d.ts +7 -0
  129. package/dist/web/transformer.js +41 -0
  130. package/dist/web/types.d.ts +32 -0
  131. package/dist/web/types.js +2 -0
  132. package/dist/web/util.d.ts +3 -0
  133. package/dist/web/util.js +81 -0
  134. package/dist/web/visualizer/cursor.d.ts +16 -0
  135. package/dist/web/visualizer/cursor.js +152 -0
  136. package/dist/web/visualizer/index.d.ts +22 -0
  137. package/dist/web/visualizer/index.js +47 -0
  138. package/dist/web/visualizer/mouseEffects.d.ts +18 -0
  139. package/dist/web/visualizer/mouseEffects.js +287 -0
  140. package/dist/web/visualizer/typeEffects.d.ts +10 -0
  141. package/dist/web/visualizer/typeEffects.js +190 -0
  142. package/package.json +71 -0
@@ -0,0 +1,3 @@
1
+ import pino from 'pino';
2
+ export declare const logger: pino.Logger<never, boolean>;
3
+ export default logger;
package/dist/logger.js ADDED
@@ -0,0 +1,15 @@
1
+ import pino from 'pino';
2
+ export const logger = pino({
3
+ level: process.env.MAGNITUDE_LOG_LEVEL || 'warn',
4
+ transport: process.stdout.isTTY ? {
5
+ target: 'pino-pretty',
6
+ options: {
7
+ colorize: !process.env.NO_COLOR,
8
+ translateTime: 'SYS:HH:MM:ss.l',
9
+ ignore: 'pid,hostname'
10
+ }
11
+ } : undefined
12
+ }).child({
13
+ name: "agent"
14
+ });
15
+ export default logger;
@@ -0,0 +1,37 @@
1
+ import { MultiMediaMessage } from '@/ai/baml_client';
2
+ import { Observation, ObservationRetentionOptions, ObservationRole, ObservationSource } from './observation';
3
+ import { MultiMediaJson } from './serde';
4
+ import { Image as BamlImage } from '@boundaryml/baml';
5
+ export interface SerializedAgentMemory {
6
+ instructions?: string;
7
+ observations: {
8
+ source: ObservationSource;
9
+ role: ObservationRole;
10
+ timestamp: number;
11
+ data: MultiMediaJson;
12
+ options?: ObservationRetentionOptions;
13
+ }[];
14
+ }
15
+ export interface AgentMemoryOptions {
16
+ instructions?: string | null;
17
+ promptCaching?: boolean;
18
+ thoughtLimit?: number;
19
+ }
20
+ export interface MemoryRenderOptions {
21
+ }
22
+ export declare class AgentMemory {
23
+ private options;
24
+ private observations;
25
+ private freezeMask?;
26
+ private cacheControlIndices;
27
+ constructor(options?: AgentMemoryOptions);
28
+ get instructions(): string | null;
29
+ render(options?: MemoryRenderOptions): Promise<MultiMediaMessage[]>;
30
+ simpleRender(): Promise<(BamlImage | string)[]>;
31
+ isEmpty(): boolean;
32
+ recordThought(content: string): void;
33
+ recordObservation(obs: Observation): void;
34
+ getLastThoughtMessage(): string | null;
35
+ toJSON(): Promise<SerializedAgentMemory>;
36
+ loadJSON(data: SerializedAgentMemory): Promise<void>;
37
+ }
@@ -0,0 +1,121 @@
1
+ import { Observation } from './observation';
2
+ import { jsonToObservableData, observableDataToJson } from './serde';
3
+ import { applyMask, maskObservations } from './masking';
4
+ // export interface FreezeState {
5
+ // //lastFrozenObservationIndex: number,
6
+ // // ^ just use length of mask
7
+ // visibilityMask: boolean[],
8
+ // }
9
+ const CACHE_CONTROL_LIMIT = 3; // Anthropic allows max of 4, we use static one on system, 3 can be cyclic
10
+ export class AgentMemory {
11
+ //public readonly events: EventEmitter<AgentMemoryEvents> = new EventEmitter();
12
+ options;
13
+ // Custom instructions relating to this memory instance (e.g. agent-level and/or task-level instructions)
14
+ //public readonly instructions: string | null;
15
+ observations = [];
16
+ //private freezeState?: FreezeState;
17
+ freezeMask;
18
+ cacheControlIndices = [];
19
+ constructor(options) {
20
+ //this.instructions = instructions ?? null;
21
+ this.options = {
22
+ instructions: options?.instructions ?? null,
23
+ promptCaching: options?.promptCaching ?? false,
24
+ //optimizeForPromptCaching: false,
25
+ thoughtLimit: options?.thoughtLimit ?? 20
26
+ };
27
+ }
28
+ get instructions() {
29
+ // why is this on memory? prob should just be on agent
30
+ return this.options.instructions;
31
+ }
32
+ async render(options) {
33
+ if (this.options.promptCaching && this.cacheControlIndices.length >= CACHE_CONTROL_LIMIT) {
34
+ this.freezeMask = undefined;
35
+ this.cacheControlIndices = [];
36
+ }
37
+ const mask = await maskObservations(this.observations, this.freezeMask);
38
+ const visibleObservations = applyMask(this.observations, mask);
39
+ const lastVisible = visibleObservations.at(-1);
40
+ if (lastVisible)
41
+ this.cacheControlIndices.push(lastVisible.index); // index WRT full observation list
42
+ let messages = [];
43
+ for (const { observation, index } of visibleObservations) {
44
+ const message = await observation.render({
45
+ prefix: observation.source.startsWith('action:taken') || observation.source.startsWith('thought') ?
46
+ [`[${new Date(observation.timestamp).toTimeString().split(' ')[0]}]: `] : [],
47
+ cacheControl: this.options.promptCaching && this.cacheControlIndices.includes(index)
48
+ });
49
+ messages.push(message);
50
+ }
51
+ if (this.options.promptCaching) {
52
+ this.freezeMask = mask;
53
+ }
54
+ return messages;
55
+ }
56
+ async simpleRender() {
57
+ // Render with no filtering, no masking, no cache control
58
+ //let messages: MultiMediaMessage[] = [];
59
+ let content = [];
60
+ for (const observation of this.observations) {
61
+ const message = await observation.render({
62
+ prefix: observation.source.startsWith('action:taken') || observation.source.startsWith('thought') ?
63
+ [`[${new Date(observation.timestamp).toTimeString().split(' ')[0]}]: `] : []
64
+ });
65
+ // ignore message stuff, just push content
66
+ content = [...content, ...message.content];
67
+ }
68
+ return content;
69
+ }
70
+ isEmpty() {
71
+ return this.observations.length === 0;
72
+ }
73
+ recordThought(content) {
74
+ this.observations.push(Observation.fromThought(content, { type: 'thought', limit: this.options.thoughtLimit }));
75
+ //this.events.emit('thought', content);
76
+ }
77
+ recordObservation(obs) {
78
+ this.observations.push(obs);
79
+ }
80
+ getLastThoughtMessage() {
81
+ for (let i = this.observations.length - 1; i >= 0; i--) {
82
+ const obs = this.observations[i];
83
+ // toString() is a little funky here, or the idea that thought might not just be text
84
+ if (obs.source.startsWith('thought'))
85
+ return obs.toString();
86
+ }
87
+ return null;
88
+ }
89
+ async toJSON() {
90
+ const observations = [];
91
+ for (const observation of this.observations) {
92
+ observations.push({
93
+ source: observation.source,
94
+ role: observation.role,
95
+ timestamp: observation.timestamp,
96
+ data: await observableDataToJson(observation.content),
97
+ options: observation.retention,
98
+ });
99
+ }
100
+ return {
101
+ // TODO: include other options as well
102
+ ...(this.options.instructions ? { instructions: this.options.instructions } : {}),
103
+ observations: observations
104
+ };
105
+ }
106
+ // TODO: turn into class static method / rework cons
107
+ async loadJSON(data) {
108
+ //jsonToObservableData(data);
109
+ const observations = [];
110
+ for (const observation of data.observations) {
111
+ observations.push(new Observation(observation.source, observation.role, await jsonToObservableData(observation.data), observation.options, observation.timestamp));
112
+ }
113
+ // nvm
114
+ //this.instructions = this.instructions;
115
+ this.observations = observations;
116
+ // return {
117
+ // ...(this.instructions ? { instructions: this.instructions } : {}),
118
+ // observations: observations
119
+ // };
120
+ }
121
+ }
@@ -0,0 +1,26 @@
1
+ import { Image as BamlImage } from '@boundaryml/baml';
2
+ import { StoredMedia } from './serde';
3
+ import { Sharp } from 'sharp';
4
+ import sharp from 'sharp';
5
+ export type ImageMediaType = `image/${string}`;
6
+ export declare class Image {
7
+ /**
8
+ * Wrapper for a Sharp image with conveniences to go to/from base64, convert to BAML, or serialize as JSON
9
+ */
10
+ private img;
11
+ constructor(img: Sharp);
12
+ static fromBase64(base64: string): Image;
13
+ getFormat(): Promise<keyof sharp.FormatEnum>;
14
+ /**
15
+ * Convert the image to a JSON representation
16
+ */
17
+ toJson(): Promise<StoredMedia>;
18
+ toBase64(): Promise<string>;
19
+ toBaml(): Promise<BamlImage>;
20
+ saveToFile(filepath: string): Promise<void>;
21
+ getDimensions(): Promise<{
22
+ width: number;
23
+ height: number;
24
+ }>;
25
+ resize(width: number, height: number): Promise<Image>;
26
+ }
@@ -0,0 +1,106 @@
1
+ import { Image as BamlImage } from '@boundaryml/baml';
2
+ import sharp from 'sharp';
3
+ export class Image {
4
+ /**
5
+ * Wrapper for a Sharp image with conveniences to go to/from base64, convert to BAML, or serialize as JSON
6
+ */
7
+ // represents the start of a pipeline
8
+ img;
9
+ // Cached metadata property for sync access + required width/height properties
10
+ //private metadata: Sharp['metadata'] & { width: number, height: number };
11
+ //private content: string;
12
+ //private mediaType: ImageMediaType;
13
+ //constructor(type: 'url' | 'base64', content: string, mediaType: ImageMediaType) {
14
+ constructor(img) {
15
+ this.img = img;
16
+ }
17
+ static fromBase64(base64) {
18
+ // if (!mediaType) {
19
+ // const match = base64.match(/^data:(.*?);base64,/);
20
+ // mediaType = match ? `image/${match[1]}` : undefined;
21
+ // }
22
+ // if (!mediaType) {
23
+ // throw new Error("Image media type must be specified either in base64 encoded string or in mediaType parameter");
24
+ // }
25
+ const base64Data = base64.replace(/^data:.*?;base64,/, '');
26
+ //return new Image('base64', base64Data, mediaType);
27
+ return new Image(sharp(Buffer.from(base64Data, 'base64')));
28
+ }
29
+ async getFormat() {
30
+ const format = (await this.img.clone().metadata()).format;
31
+ if (!format)
32
+ throw new Error("Unable to get image format");
33
+ return format;
34
+ }
35
+ /**
36
+ * Convert the image to a JSON representation
37
+ */
38
+ async toJson() {
39
+ // if (this.type === 'url') {
40
+ // return {
41
+ // type: 'media',
42
+ // mediaType: this.mediaType,//`image/${this.mediaType}`,
43
+ // storageType: 'url',
44
+ // url: this.content
45
+ // };
46
+ // } else {
47
+ return {
48
+ type: 'media',
49
+ //mediaType: this.mediaType,//`image/${this.mediaType}`,
50
+ format: await this.getFormat(),
51
+ storage: 'base64',
52
+ base64: await this.toBase64() //this.content
53
+ };
54
+ //}
55
+ }
56
+ async toBase64() {
57
+ const base64data = (await this.img.clone().toBuffer()).toString('base64');
58
+ //console.log("DATA (Image):", base64data.substring(0, 100));
59
+ return base64data;
60
+ //return `data:image/png;base64,${base64data}`;
61
+ }
62
+ async toBaml() {
63
+ // if (this.type === 'url') {
64
+ // return BamlImage.fromUrl(this.content, this.mediaType);
65
+ // }
66
+ // else {//if (this.type === 'base64') {
67
+ // return BamlImage.fromBase64(this.mediaType, this.content);
68
+ // }
69
+ const format = await this.getFormat();
70
+ const data = await this.toBase64();
71
+ //console.log("FORMAT:", format);
72
+ //console.log("DATA:", data.substring(0, 100));
73
+ return BamlImage.fromBase64(`image/${format}`, data);
74
+ }
75
+ async saveToFile(filepath) {
76
+ // We clone here to ensure the original sharp instance remains usable for other operations
77
+ await this.img.clone().toFile(filepath);
78
+ //console.log(`Image saved to ${filepath}`);
79
+ }
80
+ async getDimensions() {
81
+ //const { width, height } = await this.img.clone().metadata();
82
+ // Need to convert to buffer in order for metadata to be updated - otherwise it returns metadata of the original image
83
+ const { info: { width, height } } = await this.img.clone().toBuffer({ resolveWithObject: true });
84
+ if (!width || !height)
85
+ throw new Error("Unable to get dimensions from image");
86
+ return { width, height };
87
+ }
88
+ async resize(width, height) {
89
+ // if (this.type != 'base64') throw new Error("Only base64 images can be resized");
90
+ // const img = sharp(Buffer.from(this.content));
91
+ // const metadata = await img.metadata();
92
+ //console.log(`resizing to: ${width}, ${height}`);
93
+ //console.log("Before resizing:", await this.getDimensions());
94
+ // if (!metadata.width || !metadata.height)
95
+ const resizedImage = new Image(await this.img.clone().resize({
96
+ // Round width/height since sometimes they are floats due to rounding errors - sharp will throw if not integers
97
+ width: Math.round(width),
98
+ height: Math.round(height),
99
+ fit: 'fill', // exact size, no cropping
100
+ kernel: sharp.kernel.lanczos3
101
+ }));
102
+ //resizedImage.saveToFile('foo.png');
103
+ //console.log("After resizing:", await resizedImage.getDimensions());
104
+ return resizedImage;
105
+ }
106
+ }
@@ -0,0 +1,3 @@
1
+ export * from './agentMemory';
2
+ export * from './observation';
3
+ export * from './util';
@@ -0,0 +1,3 @@
1
+ export * from './agentMemory';
2
+ export * from './observation';
3
+ export * from './util';
@@ -0,0 +1,7 @@
1
+ import { Observation } from './observation';
2
+ export declare function maskObservations(observations: Observation[], freezeMask?: boolean[]): Promise<boolean[]>;
3
+ export interface MaskedObservation {
4
+ observation: Observation;
5
+ index: number;
6
+ }
7
+ export declare function applyMask(observations: Observation[], mask: boolean[]): MaskedObservation[];
@@ -0,0 +1,141 @@
1
+ export async function maskObservations(observations, freezeMask) {
2
+ // If freezeMask provided, will guarantee that first freezeMask.length observations return the same mask provided in freezeMask
3
+ // - for dedupe: adjacent types in frozen section should survive if unfrozen contains that type with an equivalent value, instead of keeping most recent
4
+ // - for limit: limit should only apply to unfrozen section and should ignore (not count) anything in frozen section
5
+ /**
6
+ * Returns a boolean mask indicating which observations should be visible,
7
+ * based on type-specific `dedupe` and `limit` options specified in `obs.retention`.
8
+ *
9
+ * For each type:
10
+ * 1. If `dedupe` is true, marks older adjacent identical observations as false,
11
+ * keeping only the last in such a sequence.
12
+ * 2. If `limit` is set, marks only the last N observations of that type as true.
13
+ *
14
+ * Deduplication is applied before limiting for each type.
15
+ * Untyped observations (no `obs.retention.type`) are always marked as true.
16
+ *
17
+ * If freezeMask is provided:
18
+ * - The first freezeMask.length observations will have their mask values frozen
19
+ * - Dedupe will preserve frozen observations if equivalent values exist in unfrozen section
20
+ * - Limit only applies to unfrozen observations
21
+ *
22
+ * @returns A boolean array where true indicates the observation at that index should be visible
23
+ */
24
+ const frozenCount = freezeMask?.length ?? 0;
25
+ // Initialize mask
26
+ const mask = new Array(observations.length).fill(true);
27
+ // Copy frozen mask values if provided
28
+ if (freezeMask) {
29
+ for (let i = 0; i < frozenCount && i < observations.length; i++) {
30
+ mask[i] = freezeMask[i];
31
+ }
32
+ }
33
+ // Group observations by type, tracking their original indices
34
+ const observationsByType = new Map();
35
+ observations.forEach((obs, index) => {
36
+ if (obs.retention && obs.retention.type) {
37
+ const type = obs.retention.type;
38
+ if (!observationsByType.has(type)) {
39
+ observationsByType.set(type, {
40
+ frozenIndices: [],
41
+ unfrozenIndices: [],
42
+ frozenObs: [],
43
+ unfrozenObs: [],
44
+ limit: obs.retention.limit,
45
+ dedupe: obs.retention.dedupe,
46
+ });
47
+ }
48
+ const typeData = observationsByType.get(type);
49
+ if (index < frozenCount) {
50
+ typeData.frozenIndices.push(index);
51
+ typeData.frozenObs.push(obs);
52
+ }
53
+ else {
54
+ typeData.unfrozenIndices.push(index);
55
+ typeData.unfrozenObs.push(obs);
56
+ }
57
+ }
58
+ // Untyped observations: frozen ones keep their freezeMask value, unfrozen remain true
59
+ });
60
+ // Process each type
61
+ for (const [type, data] of observationsByType.entries()) {
62
+ // Only process unfrozen observations for dedupe and limit
63
+ let visibleUnfrozenIndices = [...data.unfrozenIndices];
64
+ // Apply deduplication to unfrozen section
65
+ if (data.dedupe && data.unfrozenObs.length > 1) {
66
+ const dedupedIndices = [];
67
+ dedupedIndices.unshift(data.unfrozenIndices[data.unfrozenIndices.length - 1]);
68
+ for (let i = data.unfrozenIndices.length - 2; i >= 0; i--) {
69
+ const currentObs = data.unfrozenObs[i];
70
+ const lastKeptIdx = dedupedIndices[0];
71
+ const lastKeptObs = observations[lastKeptIdx];
72
+ if (!(await currentObs.equals(lastKeptObs))) {
73
+ dedupedIndices.unshift(data.unfrozenIndices[i]);
74
+ }
75
+ }
76
+ visibleUnfrozenIndices = dedupedIndices;
77
+ }
78
+ // Special handling for dedupe with frozen observations
79
+ if (data.dedupe && freezeMask) {
80
+ // Check if any frozen observations should be preserved
81
+ // because equivalent values exist in unfrozen section
82
+ const frozenToPreserve = new Set();
83
+ for (let i = 0; i < data.frozenIndices.length; i++) {
84
+ const frozenIdx = data.frozenIndices[i];
85
+ if (mask[frozenIdx]) { // Only check if currently visible in freezeMask
86
+ const frozenObs = data.frozenObs[i];
87
+ // Check if any unfrozen observation equals this frozen one
88
+ for (const unfrozenObs of data.unfrozenObs) {
89
+ if (await frozenObs.equals(unfrozenObs)) {
90
+ frozenToPreserve.add(frozenIdx);
91
+ break;
92
+ }
93
+ }
94
+ }
95
+ }
96
+ // Ensure preserved frozen observations stay visible
97
+ frozenToPreserve.forEach(idx => {
98
+ mask[idx] = true;
99
+ });
100
+ }
101
+ // Apply limit only to unfrozen observations
102
+ if (data.limit !== undefined && data.limit >= 0) {
103
+ if (data.limit === 0) {
104
+ visibleUnfrozenIndices = [];
105
+ }
106
+ else {
107
+ // Limit only counts unfrozen observations
108
+ visibleUnfrozenIndices = visibleUnfrozenIndices.slice(-data.limit);
109
+ }
110
+ }
111
+ // Mark non-visible unfrozen observations as false
112
+ const visibleSet = new Set(visibleUnfrozenIndices);
113
+ data.unfrozenIndices.forEach(idx => {
114
+ if (!visibleSet.has(idx)) {
115
+ mask[idx] = false;
116
+ }
117
+ });
118
+ // Frozen observations keep their freezeMask values (already set above)
119
+ }
120
+ return mask;
121
+ }
122
+ export function applyMask(observations, mask) {
123
+ /**
124
+ * Applies a boolean mask to an array of observations, returning only those
125
+ * observations where the corresponding mask value is true, along with their original indices.
126
+ *
127
+ * @param observations - The array of observations to filter
128
+ * @param mask - Boolean array where true indicates the observation should be included
129
+ * @returns Array of objects containing the observation and its original index
130
+ */
131
+ if (observations.length !== mask.length) {
132
+ throw new Error(`Mask length (${mask.length}) must match observations length (${observations.length})`);
133
+ }
134
+ const result = [];
135
+ observations.forEach((observation, index) => {
136
+ if (mask[index]) {
137
+ result.push({ observation, index });
138
+ }
139
+ });
140
+ return result;
141
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,117 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { Observation } from './observation';
3
+ import { maskObservations, applyMask } from './masking';
4
+ describe('maskObservations with freezeMask', () => {
5
+ test('freezeMask preserves frozen observation mask values', async () => {
6
+ const observations = [
7
+ new Observation('connector:test', 'user', 'obs1'),
8
+ new Observation('connector:test', 'user', 'obs2'),
9
+ new Observation('connector:test', 'user', 'obs3'),
10
+ new Observation('connector:test', 'user', 'obs4'),
11
+ ];
12
+ const freezeMask = [true, false, true];
13
+ const mask = await maskObservations(observations, freezeMask);
14
+ // First 3 observations should match freezeMask exactly
15
+ expect(mask[0]).toBe(true);
16
+ expect(mask[1]).toBe(false);
17
+ expect(mask[2]).toBe(true);
18
+ // Fourth observation (unfrozen) should be true by default
19
+ expect(mask[3]).toBe(true);
20
+ });
21
+ test('basic dedupe behavior', async () => {
22
+ // First test basic dedupe to understand behavior
23
+ const observations = [
24
+ new Observation('connector:test', 'user', 'content1', { type: 'msg', dedupe: true }),
25
+ new Observation('connector:test', 'user', 'content1', { type: 'msg', dedupe: true }),
26
+ new Observation('connector:test', 'user', 'content2', { type: 'msg', dedupe: true }),
27
+ ];
28
+ const mask = await maskObservations(observations);
29
+ // Dedupe keeps the last occurrence when adjacent
30
+ expect(mask).toEqual([false, true, true]);
31
+ });
32
+ test('dedupe preserves frozen observations when equivalent exists in unfrozen', async () => {
33
+ const observations = [
34
+ new Observation('connector:test', 'user', 'duplicate', { type: 'msg', dedupe: true }),
35
+ new Observation('connector:test', 'user', 'unique', { type: 'msg', dedupe: true }),
36
+ new Observation('connector:test', 'user', 'duplicate', { type: 'msg', dedupe: true }), // This duplicates frozen[0]
37
+ new Observation('connector:test', 'user', 'another', { type: 'msg', dedupe: true }),
38
+ ];
39
+ // With freezeMask, frozen observation should be preserved since equivalent exists in unfrozen
40
+ const freezeMask = [true, true];
41
+ const maskWithFreeze = await maskObservations(observations, freezeMask);
42
+ expect(maskWithFreeze).toEqual([true, true, true, true]);
43
+ });
44
+ test('limit only applies to unfrozen observations', async () => {
45
+ const observations = [
46
+ new Observation('connector:test', 'user', 'frozen1', { type: 'limited', limit: 2 }),
47
+ new Observation('connector:test', 'user', 'frozen2', { type: 'limited', limit: 2 }),
48
+ new Observation('connector:test', 'user', 'frozen3', { type: 'limited', limit: 2 }),
49
+ new Observation('connector:test', 'user', 'unfrozen1', { type: 'limited', limit: 2 }),
50
+ new Observation('connector:test', 'user', 'unfrozen2', { type: 'limited', limit: 2 }),
51
+ new Observation('connector:test', 'user', 'unfrozen3', { type: 'limited', limit: 2 }),
52
+ ];
53
+ // Without freezeMask, only last 2 observations would be kept
54
+ const normalMask = await maskObservations(observations);
55
+ expect(normalMask).toEqual([false, false, false, false, true, true]);
56
+ // With freezeMask, frozen observations don't count towards limit
57
+ // So we keep all 3 frozen + last 2 unfrozen
58
+ const freezeMask = [true, true, true];
59
+ const maskWithFreeze = await maskObservations(observations, freezeMask);
60
+ expect(maskWithFreeze).toEqual([true, true, true, false, true, true]);
61
+ });
62
+ test('freezeMask with mixed frozen values and limit', async () => {
63
+ const observations = [
64
+ new Observation('connector:test', 'user', 'frozen1', { type: 'mixed', limit: 1 }),
65
+ new Observation('connector:test', 'user', 'frozen2', { type: 'mixed', limit: 1 }),
66
+ new Observation('connector:test', 'user', 'unfrozen1', { type: 'mixed', limit: 1 }),
67
+ new Observation('connector:test', 'user', 'unfrozen2', { type: 'mixed', limit: 1 }),
68
+ ];
69
+ // Freeze mask says to hide frozen2 but show frozen1
70
+ const freezeMask = [true, false];
71
+ const mask = await maskObservations(observations, freezeMask);
72
+ // frozen1: true (from freezeMask)
73
+ // frozen2: false (from freezeMask)
74
+ // unfrozen1: false (exceeded limit of 1 for unfrozen)
75
+ // unfrozen2: true (last unfrozen, within limit)
76
+ expect(mask).toEqual([true, false, false, true]);
77
+ });
78
+ test('applyMask filters observations correctly', () => {
79
+ const observations = [
80
+ new Observation('connector:test', 'user', 'obs1'),
81
+ new Observation('connector:test', 'user', 'obs2'),
82
+ new Observation('connector:test', 'user', 'obs3'),
83
+ ];
84
+ const mask = [true, false, true];
85
+ const filtered = applyMask(observations, mask);
86
+ expect(filtered.length).toBe(2);
87
+ expect(filtered[0].observation.content).toBe('obs1');
88
+ expect(filtered[0].index).toBe(0);
89
+ expect(filtered[1].observation.content).toBe('obs3');
90
+ expect(filtered[1].index).toBe(2);
91
+ });
92
+ test('freezeMask with no typed observations', async () => {
93
+ // Test that untyped observations respect freezeMask
94
+ const observations = [
95
+ new Observation('connector:test', 'user', 'frozen1'),
96
+ new Observation('connector:test', 'user', 'frozen2'),
97
+ new Observation('connector:test', 'user', 'unfrozen1'),
98
+ new Observation('connector:test', 'user', 'unfrozen2'),
99
+ ];
100
+ const freezeMask = [false, true];
101
+ const mask = await maskObservations(observations, freezeMask);
102
+ // Frozen observations keep their freezeMask values
103
+ // Unfrozen untyped observations remain true
104
+ expect(mask).toEqual([false, true, true, true]);
105
+ });
106
+ test('freezeMask longer than observations array', async () => {
107
+ const observations = [
108
+ new Observation('connector:test', 'user', 'obs1'),
109
+ new Observation('connector:test', 'user', 'obs2'),
110
+ ];
111
+ const freezeMask = [true, false, true, false];
112
+ const mask = await maskObservations(observations, freezeMask);
113
+ // Should only apply freezeMask to existing observations
114
+ expect(mask.length).toBe(2);
115
+ expect(mask).toEqual([true, false]);
116
+ });
117
+ });
@@ -0,0 +1,38 @@
1
+ import { Image } from './image';
2
+ import { MultiMediaJson } from './serde';
3
+ import { MultiMediaMessage } from '@/ai/baml_client';
4
+ import { type MultiMediaContentPart } from './rendering';
5
+ export type ObservableDataPrimitive = Image | string | number | boolean | null | undefined;
6
+ export type ObservableDataArray = Array<RenderableContent>;
7
+ export type ObservableDataObject = {
8
+ [key: string]: RenderableContent;
9
+ };
10
+ export type RenderableContent = ObservableDataPrimitive | ObservableDataArray | ObservableDataObject;
11
+ export type ObservationRole = 'user' | 'assistant';
12
+ export interface ObservationRetentionOptions {
13
+ type: string;
14
+ limit?: number;
15
+ dedupe?: boolean;
16
+ }
17
+ export type ObservationSource = `connector:${string}` | `action:taken:${string}` | `action:result:${string}` | `thought`;
18
+ export declare class Observation {
19
+ readonly source: ObservationSource;
20
+ readonly role: ObservationRole;
21
+ readonly timestamp: number;
22
+ readonly content: RenderableContent;
23
+ readonly retention?: ObservationRetentionOptions;
24
+ constructor(source: ObservationSource, role: ObservationRole, content: RenderableContent, retention?: ObservationRetentionOptions, timestamp?: number);
25
+ static fromConnector(connectorId: string, content: RenderableContent, options?: ObservationRetentionOptions): Observation;
26
+ static fromActionTaken(actionId: string, content: RenderableContent, options?: ObservationRetentionOptions): Observation;
27
+ static fromActionResult(actionId: string, content: RenderableContent, options?: ObservationRetentionOptions): Observation;
28
+ static fromThought(content: RenderableContent, options?: ObservationRetentionOptions): Observation;
29
+ toString(): string;
30
+ toJson(): Promise<MultiMediaJson>;
31
+ render(options?: {
32
+ prefix?: MultiMediaContentPart[];
33
+ postfix?: MultiMediaContentPart[];
34
+ cacheControl?: boolean;
35
+ }): Promise<MultiMediaMessage>;
36
+ hash(): Promise<string>;
37
+ equals(obs: Observation): Promise<boolean>;
38
+ }