Skip to content

Commit 39561e0

Browse files
committed
export mem, act option for mem injection
1 parent 082a355 commit 39561e0

4 files changed

Lines changed: 171 additions & 167 deletions

File tree

packages/magnitude-core/src/agent/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export interface AgentOptions {
3232
export interface ActOptions {
3333
prompt?: string // additional task-level system prompt instructions
3434
// TODO: reimpl, or maybe for tc agent specifically
35-
data?: RenderableContent//string | Record<string, string>
35+
data?: RenderableContent,//string | Record<string, string>
36+
memory?: AgentMemory,// optional memory starting point
3637
}
3738

3839
// Options for the startAgent helper function
@@ -241,7 +242,7 @@ export class Agent {
241242
...(this.options.prompt ? [this.options.prompt] : []),
242243
...(options.prompt ? [options.prompt] : []),
243244
].join('\n');
244-
const taskMemory = new AgentMemory({ ...this.memoryOptions, instructions: instructions === '' ? undefined : instructions });
245+
const taskMemory = options.memory ?? new AgentMemory({ ...this.memoryOptions, instructions: instructions === '' ? undefined : instructions });
245246

246247
if (Array.isArray(taskOrSteps)) {
247248
const steps = taskOrSteps;
@@ -270,9 +271,9 @@ export class Agent {
270271

271272
async _traceAct(task: string, memory: AgentMemory, options: ActOptions = {}) {
272273
// memory not serializable to trace so bake it
273-
await (traceAsync('act', async (task: string, options: ActOptions) => {
274+
await (traceAsync('act', async (task: string) => {
274275
await this._act(task, memory, options);
275-
})(task, options));
276+
})(task));
276277
}
277278

278279
private async _buildContext(memory: AgentMemory): Promise<AgentContext> {

packages/magnitude-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export { BrowserAgent, startBrowserAgent } from '@/agent/browserAgent';
77
//export { Magnus } from "@/agent/magnus";
88
//export type { AgentOptions as TestCaseAgentOptions } from "@/agent";
99
export * from "@/agent";
10+
export * from "@/memory";
1011
export * from "@/web/harness";
1112
export * from "@/actions";
1213
export * from "@/connectors";
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import {
2+
MultiMediaMessage
3+
} from '@/ai/baml_client';
4+
import { Observation, ObservationRetentionOptions, ObservationRole, ObservationSource } from './observation';
5+
import z from 'zod';
6+
import EventEmitter from 'eventemitter3';
7+
import { jsonToObservableData, MultiMediaJson, observableDataToJson } from './serde';
8+
import { applyMask, maskObservations } from './masking';
9+
10+
// export interface AgentMemoryEvents {
11+
// 'thought': (thought: string) => void;
12+
// }
13+
14+
export interface SerializedAgentMemory {
15+
instructions?: string;
16+
observations: {
17+
source: ObservationSource,
18+
role: ObservationRole,
19+
timestamp: number,
20+
data: MultiMediaJson,
21+
options?: ObservationRetentionOptions,
22+
}[];
23+
}
24+
25+
export interface AgentMemoryOptions {
26+
instructions?: string | null,
27+
promptCaching?: boolean,
28+
thoughtLimit?: number, // TTL for thoughts
29+
}
30+
31+
// export interface FreezeState {
32+
// //lastFrozenObservationIndex: number,
33+
// // ^ just use length of mask
34+
// visibilityMask: boolean[],
35+
// }
36+
37+
const CACHE_CONTROL_LIMIT = 3; // Anthropic allows max of 4, we use static one on system, 3 can be cyclic
38+
39+
export class AgentMemory {
40+
//public readonly events: EventEmitter<AgentMemoryEvents> = new EventEmitter();
41+
private options: Required<AgentMemoryOptions>;
42+
43+
// Custom instructions relating to this memory instance (e.g. agent-level and/or task-level instructions)
44+
//public readonly instructions: string | null;
45+
46+
private observations: Observation[] = [];
47+
48+
//private freezeState?: FreezeState;
49+
private freezeMask?: boolean[];
50+
private cacheControlIndices: number[] = [];
51+
52+
constructor(options?: AgentMemoryOptions) {
53+
//this.instructions = instructions ?? null;
54+
this.options = {
55+
instructions: options?.instructions ?? null,
56+
promptCaching: options?.promptCaching ?? false,
57+
//optimizeForPromptCaching: false,
58+
thoughtLimit: options?.thoughtLimit ?? 20
59+
};
60+
}
61+
62+
public get instructions() {
63+
// why is this on memory? prob should just be on agent
64+
return this.options.instructions;
65+
}
66+
67+
public async render(): Promise<MultiMediaMessage[]> {
68+
if (this.options.promptCaching && this.cacheControlIndices.length >= CACHE_CONTROL_LIMIT) {
69+
this.freezeMask = undefined;
70+
this.cacheControlIndices = [];
71+
}
72+
const mask = await maskObservations(this.observations, this.freezeMask);
73+
74+
const visibleObservations = applyMask(this.observations, mask);
75+
76+
const lastVisible = visibleObservations.at(-1);
77+
if (lastVisible) this.cacheControlIndices.push(lastVisible.index); // index WRT full observation list
78+
79+
let messages: MultiMediaMessage[] = [];
80+
for (const { observation, index } of visibleObservations) {
81+
const message = await observation.render({
82+
prefix: observation.source.startsWith('action:taken') || observation.source.startsWith('thought') ?
83+
[`[${new Date(observation.timestamp).toTimeString().split(' ')[0]}]: `] : [],
84+
cacheControl: this.options.promptCaching && this.cacheControlIndices.includes(index)
85+
});
86+
messages.push(message);
87+
}
88+
89+
if (this.options.promptCaching) {
90+
this.freezeMask = mask;
91+
}
92+
93+
return messages;
94+
}
95+
96+
public isEmpty(): boolean {
97+
return this.observations.length === 0;
98+
}
99+
100+
public recordThought(content: string): void {
101+
this.observations.push(
102+
Observation.fromThought(content, { type: 'thought', limit: this.options.thoughtLimit })
103+
);
104+
//this.events.emit('thought', content);
105+
}
106+
107+
public recordObservation(obs: Observation): void {
108+
this.observations.push(obs);
109+
}
110+
111+
public getLastThoughtMessage(): string | null {
112+
for (let i = this.observations.length - 1; i >= 0; i--) {
113+
const obs = this.observations[i];
114+
// toString() is a little funky here, or the idea that thought might not just be text
115+
if (obs.source.startsWith('thought')) return obs.toString();
116+
}
117+
return null;
118+
}
119+
120+
public async toJSON(): Promise<SerializedAgentMemory> {
121+
const observations = [];
122+
for (const observation of this.observations) {
123+
observations.push({
124+
source: observation.source,
125+
role: observation.role,
126+
timestamp: observation.timestamp,
127+
data: await observableDataToJson(observation.content),
128+
options: observation.retention,
129+
});
130+
}
131+
return {
132+
// TODO: include other options as well
133+
...(this.options.instructions ? { instructions: this.options.instructions } : {}),
134+
observations: observations
135+
};
136+
}
137+
138+
// TODO: turn into class static method / rework cons
139+
public async loadJSON(data: SerializedAgentMemory) {
140+
//jsonToObservableData(data);
141+
const observations = [];
142+
for (const observation of data.observations) {
143+
observations.push(new Observation(
144+
observation.source,
145+
observation.role,
146+
await jsonToObservableData(observation.data),
147+
observation.options,
148+
observation.timestamp
149+
));
150+
151+
}
152+
// nvm
153+
//this.instructions = this.instructions;
154+
155+
this.observations = observations;
156+
157+
158+
// return {
159+
// ...(this.instructions ? { instructions: this.instructions } : {}),
160+
// observations: observations
161+
// };
162+
}
163+
}
Lines changed: 2 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -1,163 +1,2 @@
1-
import {
2-
MultiMediaMessage
3-
} from '@/ai/baml_client';
4-
import { Observation, ObservationRetentionOptions, ObservationRole, ObservationSource } from './observation';
5-
import z from 'zod';
6-
import EventEmitter from 'eventemitter3';
7-
import { jsonToObservableData, MultiMediaJson, observableDataToJson } from './serde';
8-
import { applyMask, maskObservations } from './masking';
9-
10-
// export interface AgentMemoryEvents {
11-
// 'thought': (thought: string) => void;
12-
// }
13-
14-
export interface SerializedAgentMemory {
15-
instructions?: string;
16-
observations: {
17-
source: ObservationSource,
18-
role: ObservationRole,
19-
timestamp: number,
20-
data: MultiMediaJson,
21-
options?: ObservationRetentionOptions,
22-
}[];
23-
}
24-
25-
export interface AgentMemoryOptions {
26-
instructions?: string | null,
27-
promptCaching?: boolean,
28-
thoughtLimit?: number, // TTL for thoughts
29-
}
30-
31-
// export interface FreezeState {
32-
// //lastFrozenObservationIndex: number,
33-
// // ^ just use length of mask
34-
// visibilityMask: boolean[],
35-
// }
36-
37-
const CACHE_CONTROL_LIMIT = 3; // Anthropic allows max of 4, we use static one on system, 3 can be cyclic
38-
39-
export class AgentMemory {
40-
//public readonly events: EventEmitter<AgentMemoryEvents> = new EventEmitter();
41-
private options: Required<AgentMemoryOptions>;
42-
43-
// Custom instructions relating to this memory instance (e.g. agent-level and/or task-level instructions)
44-
//public readonly instructions: string | null;
45-
46-
private observations: Observation[] = [];
47-
48-
//private freezeState?: FreezeState;
49-
private freezeMask?: boolean[];
50-
private cacheControlIndices: number[] = [];
51-
52-
constructor(options?: AgentMemoryOptions) {
53-
//this.instructions = instructions ?? null;
54-
this.options = {
55-
instructions: options?.instructions ?? null,
56-
promptCaching: options?.promptCaching ?? false,
57-
//optimizeForPromptCaching: false,
58-
thoughtLimit: options?.thoughtLimit ?? 20
59-
};
60-
}
61-
62-
public get instructions() {
63-
// why is this on memory? prob should just be on agent
64-
return this.options.instructions;
65-
}
66-
67-
public async render(): Promise<MultiMediaMessage[]> {
68-
if (this.options.promptCaching && this.cacheControlIndices.length >= CACHE_CONTROL_LIMIT) {
69-
this.freezeMask = undefined;
70-
this.cacheControlIndices = [];
71-
}
72-
const mask = await maskObservations(this.observations, this.freezeMask);
73-
74-
const visibleObservations = applyMask(this.observations, mask);
75-
76-
const lastVisible = visibleObservations.at(-1);
77-
if (lastVisible) this.cacheControlIndices.push(lastVisible.index); // index WRT full observation list
78-
79-
let messages: MultiMediaMessage[] = [];
80-
for (const { observation, index } of visibleObservations) {
81-
const message = await observation.render({
82-
prefix: observation.source.startsWith('action:taken') || observation.source.startsWith('thought') ?
83-
[`[${new Date(observation.timestamp).toTimeString().split(' ')[0]}]: `] : [],
84-
cacheControl: this.options.promptCaching && this.cacheControlIndices.includes(index)
85-
});
86-
messages.push(message);
87-
}
88-
89-
if (this.options.promptCaching) {
90-
this.freezeMask = mask;
91-
}
92-
93-
return messages;
94-
}
95-
96-
public isEmpty(): boolean {
97-
return this.observations.length === 0;
98-
}
99-
100-
public recordThought(content: string): void {
101-
this.observations.push(
102-
Observation.fromThought(content, { type: 'thought', limit: this.options.thoughtLimit })
103-
);
104-
//this.events.emit('thought', content);
105-
}
106-
107-
public recordObservation(obs: Observation): void {
108-
this.observations.push(obs);
109-
}
110-
111-
public getLastThoughtMessage(): string | null {
112-
for (let i = this.observations.length - 1; i >= 0; i--) {
113-
const obs = this.observations[i];
114-
// toString() is a little funky here, or the idea that thought might not just be text
115-
if (obs.source.startsWith('thought')) return obs.toString();
116-
}
117-
return null;
118-
}
119-
120-
public async toJSON(): Promise<SerializedAgentMemory> {
121-
const observations = [];
122-
for (const observation of this.observations) {
123-
observations.push({
124-
source: observation.source,
125-
role: observation.role,
126-
timestamp: observation.timestamp,
127-
data: await observableDataToJson(observation.content),
128-
options: observation.retention,
129-
});
130-
}
131-
return {
132-
// TODO: include other options as well
133-
...(this.options.instructions ? { instructions: this.options.instructions } : {}),
134-
observations: observations
135-
};
136-
}
137-
138-
// TODO: turn into class static method / rework cons
139-
public async loadJSON(data: SerializedAgentMemory) {
140-
//jsonToObservableData(data);
141-
const observations = [];
142-
for (const observation of data.observations) {
143-
observations.push(new Observation(
144-
observation.source,
145-
observation.role,
146-
await jsonToObservableData(observation.data),
147-
observation.options,
148-
observation.timestamp
149-
));
150-
151-
}
152-
// nvm
153-
//this.instructions = this.instructions;
154-
155-
this.observations = observations;
156-
157-
158-
// return {
159-
// ...(this.instructions ? { instructions: this.instructions } : {}),
160-
// observations: observations
161-
// };
162-
}
163-
}
1+
export * from './agentMemory';
2+
export * from './observation';

0 commit comments

Comments
 (0)