@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.
- package/dist/actions/desktopActions.d.ts +58 -0
- package/dist/actions/desktopActions.js +154 -0
- package/dist/actions/index.d.ts +26 -0
- package/dist/actions/index.js +11 -0
- package/dist/actions/taskActions.d.ts +8 -0
- package/dist/actions/taskActions.js +27 -0
- package/dist/actions/types.d.ts +31 -0
- package/dist/actions/types.js +5 -0
- package/dist/actions/util.d.ts +20 -0
- package/dist/actions/util.js +297 -0
- package/dist/actions/webActions.d.ts +79 -0
- package/dist/actions/webActions.js +190 -0
- package/dist/agent/browserAgent.d.ts +29 -0
- package/dist/agent/browserAgent.js +122 -0
- package/dist/agent/errors.d.ts +8 -0
- package/dist/agent/errors.js +68 -0
- package/dist/agent/index.d.ts +53 -0
- package/dist/agent/index.js +377 -0
- package/dist/agent/narrator.d.ts +4 -0
- package/dist/agent/narrator.js +63 -0
- package/dist/agent/pause.test.d.ts +1 -0
- package/dist/agent/pause.test.js +194 -0
- package/dist/ai/baml_client/async_client.d.ts +73 -0
- package/dist/ai/baml_client/async_client.js +152 -0
- package/dist/ai/baml_client/async_request.d.ts +35 -0
- package/dist/ai/baml_client/async_request.js +98 -0
- package/dist/ai/baml_client/config.d.ts +11 -0
- package/dist/ai/baml_client/config.js +20 -0
- package/dist/ai/baml_client/globals.d.ts +16 -0
- package/dist/ai/baml_client/globals.js +37 -0
- package/dist/ai/baml_client/index.d.ts +16 -0
- package/dist/ai/baml_client/index.js +41 -0
- package/dist/ai/baml_client/inlinedbaml.d.ts +19 -0
- package/dist/ai/baml_client/inlinedbaml.js +31 -0
- package/dist/ai/baml_client/parser.d.ts +53 -0
- package/dist/ai/baml_client/parser.js +86 -0
- package/dist/ai/baml_client/partial_types.d.ts +61 -0
- package/dist/ai/baml_client/partial_types.js +10 -0
- package/dist/ai/baml_client/sync_client.d.ts +50 -0
- package/dist/ai/baml_client/sync_client.js +100 -0
- package/dist/ai/baml_client/sync_request.d.ts +35 -0
- package/dist/ai/baml_client/sync_request.js +98 -0
- package/dist/ai/baml_client/tracing.d.ts +16 -0
- package/dist/ai/baml_client/tracing.js +18 -0
- package/dist/ai/baml_client/type_builder.d.ts +38 -0
- package/dist/ai/baml_client/type_builder.js +96 -0
- package/dist/ai/baml_client/types.d.ts +69 -0
- package/dist/ai/baml_client/types.js +15 -0
- package/dist/ai/claudeCode.d.ts +1 -0
- package/dist/ai/claudeCode.js +186 -0
- package/dist/ai/modelHarness.d.ts +39 -0
- package/dist/ai/modelHarness.js +189 -0
- package/dist/ai/multiModelHarness.d.ts +27 -0
- package/dist/ai/multiModelHarness.js +52 -0
- package/dist/ai/types.d.ts +90 -0
- package/dist/ai/types.js +5 -0
- package/dist/ai/util.d.ts +13 -0
- package/dist/ai/util.js +165 -0
- package/dist/cli.d.ts +0 -0
- package/dist/cli.js +3 -0
- package/dist/common/actions.d.ts +0 -0
- package/dist/common/actions.js +14 -0
- package/dist/common/events.d.ts +15 -0
- package/dist/common/events.js +3 -0
- package/dist/common/failure.d.ts +68 -0
- package/dist/common/failure.js +1 -0
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.js +3 -0
- package/dist/common/retry.d.ts +16 -0
- package/dist/common/retry.js +30 -0
- package/dist/common/util.d.ts +23 -0
- package/dist/common/util.js +132 -0
- package/dist/connectors/browserConnector.d.ts +40 -0
- package/dist/connectors/browserConnector.js +127 -0
- package/dist/connectors/desktopConnector.d.ts +58 -0
- package/dist/connectors/desktopConnector.js +68 -0
- package/dist/connectors/index.d.ts +10 -0
- package/dist/connectors/index.js +2 -0
- package/dist/index.cjs +6143 -0
- package/dist/index.d.cts +825 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +26 -0
- package/dist/index.mjs +6097 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +15 -0
- package/dist/memory/agentMemory.d.ts +37 -0
- package/dist/memory/agentMemory.js +121 -0
- package/dist/memory/image.d.ts +26 -0
- package/dist/memory/image.js +106 -0
- package/dist/memory/index.d.ts +3 -0
- package/dist/memory/index.js +3 -0
- package/dist/memory/masking.d.ts +7 -0
- package/dist/memory/masking.js +141 -0
- package/dist/memory/masking.test.d.ts +1 -0
- package/dist/memory/masking.test.js +117 -0
- package/dist/memory/observation.d.ts +38 -0
- package/dist/memory/observation.js +79 -0
- package/dist/memory/rendering/index.d.ts +10 -0
- package/dist/memory/rendering/index.js +13 -0
- package/dist/memory/rendering/renderJsonParts.d.ts +3 -0
- package/dist/memory/rendering/renderJsonParts.js +115 -0
- package/dist/memory/rendering/renderJsonParts.test.d.ts +1 -0
- package/dist/memory/rendering/renderJsonParts.test.js +217 -0
- package/dist/memory/rendering/renderXmlParts.d.ts +7 -0
- package/dist/memory/rendering/renderXmlParts.js +127 -0
- package/dist/memory/serde.d.ts +20 -0
- package/dist/memory/serde.js +81 -0
- package/dist/memory/util.d.ts +2 -0
- package/dist/memory/util.js +17 -0
- package/dist/telemetry/events.d.ts +9 -0
- package/dist/telemetry/events.js +80 -0
- package/dist/telemetry/index.d.ts +6 -0
- package/dist/telemetry/index.js +91 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +1 -0
- package/dist/util.js +12 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/web/browserProvider.d.ts +23 -0
- package/dist/web/browserProvider.js +153 -0
- package/dist/web/harness.d.ts +102 -0
- package/dist/web/harness.js +340 -0
- package/dist/web/stability.d.ts +57 -0
- package/dist/web/stability.js +316 -0
- package/dist/web/tabs.d.ts +58 -0
- package/dist/web/tabs.js +293 -0
- package/dist/web/transformer.d.ts +7 -0
- package/dist/web/transformer.js +41 -0
- package/dist/web/types.d.ts +32 -0
- package/dist/web/types.js +2 -0
- package/dist/web/util.d.ts +3 -0
- package/dist/web/util.js +81 -0
- package/dist/web/visualizer/cursor.d.ts +16 -0
- package/dist/web/visualizer/cursor.js +152 -0
- package/dist/web/visualizer/index.d.ts +22 -0
- package/dist/web/visualizer/index.js +47 -0
- package/dist/web/visualizer/mouseEffects.d.ts +18 -0
- package/dist/web/visualizer/mouseEffects.js +287 -0
- package/dist/web/visualizer/typeEffects.d.ts +10 -0
- package/dist/web/visualizer/typeEffects.js +190 -0
- package/package.json +71 -0
package/dist/logger.d.ts
ADDED
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,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
|
+
}
|