@mastra/opencode 0.1.11-alpha.0 → 0.1.11-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,318 +1,285 @@
1
- import { mkdir, readFile } from 'fs/promises';
2
- import { join, dirname } from 'path';
3
- import { LibSQLStore } from '@mastra/libsql';
4
- import { ObservationalMemory, TokenCounter, optimizeObservationsForContext, OBSERVATION_CONTEXT_PROMPT, OBSERVATION_CONTEXT_INSTRUCTIONS, OBSERVATION_CONTINUATION_HINT } from '@mastra/memory/processors';
5
- import { tool } from '@opencode-ai/plugin';
6
-
7
- // src/index.ts
8
- var CONFIG_FILE = ".opencode/mastra.json";
9
- var DEFAULT_STORAGE_PATH = ".opencode/memory/observations.db";
1
+ import { mkdir, readFile } from "fs/promises";
2
+ import { dirname, join } from "path";
3
+ import { LibSQLStore } from "@mastra/libsql";
4
+ import { OBSERVATION_CONTEXT_INSTRUCTIONS, OBSERVATION_CONTEXT_PROMPT, OBSERVATION_CONTINUATION_HINT, ObservationalMemory, TokenCounter, optimizeObservationsForContext } from "@mastra/memory/processors";
5
+ import { tool } from "@opencode-ai/plugin";
6
+ //#region src/index.ts
7
+ /**
8
+ * @mastra/opencode
9
+ *
10
+ * OpenCode plugin that brings Mastra Observational Memory into opencode sessions.
11
+ *
12
+ * Mastra OM compresses long conversation history into structured observations
13
+ * using an Observer (extract) and Reflector (condense) architecture.
14
+ *
15
+ * Configuration is read from .opencode/mastra.json in the project root.
16
+ *
17
+ * @example .opencode/mastra.json
18
+ * ```json
19
+ * {
20
+ * "model": "google/gemini-2.5-flash",
21
+ * "observation": { "messageTokens": 20000 },
22
+ * "reflection": { "observationTokens": 90000 },
23
+ * "storagePath": ".opencode/memory/observations.db"
24
+ * }
25
+ * ```
26
+ */
27
+ const CONFIG_FILE = ".opencode/mastra.json";
28
+ const DEFAULT_STORAGE_PATH = ".opencode/memory/observations.db";
10
29
  async function loadConfig(directory) {
11
- try {
12
- const configPath = join(directory, CONFIG_FILE);
13
- const raw = await readFile(configPath, "utf-8");
14
- return JSON.parse(raw);
15
- } catch {
16
- return {};
17
- }
30
+ try {
31
+ const raw = await readFile(join(directory, CONFIG_FILE), "utf-8");
32
+ return JSON.parse(raw);
33
+ } catch {
34
+ return {};
35
+ }
18
36
  }
37
+ /** Convert opencode messages to MastraDBMessage format.
38
+ * Preserves all part types including tool invocations, files, images, and reasoning.
39
+ */
19
40
  function convertMessages(messages, sessionId) {
20
- return messages.map(({ info, parts }) => {
21
- const convertedParts = parts.map((part) => {
22
- const p = part;
23
- const type = p.type;
24
- if (type === "text" && p.text) {
25
- return { type: "text", text: p.text };
26
- }
27
- if (type === "tool-invocation") {
28
- return {
29
- type: "tool-invocation",
30
- toolInvocation: {
31
- toolCallId: p.toolCallId,
32
- toolName: p.toolName,
33
- args: p.args,
34
- result: p.result,
35
- state: p.state
36
- }
37
- };
38
- }
39
- if (type === "file") {
40
- return {
41
- type: "file",
42
- url: p.url,
43
- mediaType: p.mediaType
44
- };
45
- }
46
- if (type === "image") {
47
- return {
48
- type: "image",
49
- image: p.image
50
- };
51
- }
52
- if (type === "reasoning" && p.reasoning) {
53
- return { type: "reasoning", reasoning: p.reasoning };
54
- }
55
- if (type?.startsWith("data-om-")) {
56
- return null;
57
- }
58
- return null;
59
- }).filter((p) => p !== null);
60
- if (convertedParts.length === 0) return null;
61
- if (info.role !== "user" && info.role !== "assistant") return null;
62
- return {
63
- id: info.id,
64
- role: info.role,
65
- // opencode timestamps are already in milliseconds (JavaScript Date)
66
- createdAt: new Date(info.time.created),
67
- threadId: sessionId,
68
- resourceId: sessionId,
69
- content: {
70
- format: 2,
71
- parts: convertedParts
72
- }
73
- };
74
- }).filter((m) => m !== null);
41
+ return messages.map(({ info, parts }) => {
42
+ const convertedParts = parts.map((part) => {
43
+ const p = part;
44
+ const type = p.type;
45
+ if (type === "text" && p.text) return {
46
+ type: "text",
47
+ text: p.text
48
+ };
49
+ if (type === "tool-invocation") return {
50
+ type: "tool-invocation",
51
+ toolInvocation: {
52
+ toolCallId: p.toolCallId,
53
+ toolName: p.toolName,
54
+ args: p.args,
55
+ result: p.result,
56
+ state: p.state
57
+ }
58
+ };
59
+ if (type === "file") return {
60
+ type: "file",
61
+ url: p.url,
62
+ mediaType: p.mediaType
63
+ };
64
+ if (type === "image") return {
65
+ type: "image",
66
+ image: p.image
67
+ };
68
+ if (type === "reasoning" && p.reasoning) return {
69
+ type: "reasoning",
70
+ reasoning: p.reasoning
71
+ };
72
+ if (type?.startsWith("data-om-")) return null;
73
+ return null;
74
+ }).filter((p) => p !== null);
75
+ if (convertedParts.length === 0) return null;
76
+ if (info.role !== "user" && info.role !== "assistant") return null;
77
+ return {
78
+ id: info.id,
79
+ role: info.role,
80
+ createdAt: new Date(info.time.created),
81
+ threadId: sessionId,
82
+ resourceId: sessionId,
83
+ content: {
84
+ format: 2,
85
+ parts: convertedParts
86
+ }
87
+ };
88
+ }).filter((m) => m !== null);
75
89
  }
76
90
  function progressBar(current, total, width = 20) {
77
- const pct = total > 0 ? Math.min(current / total, 1) : 0;
78
- const filled = Math.round(pct * width);
79
- return `[${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}] ${(pct * 100).toFixed(1)}%`;
91
+ const pct = total > 0 ? Math.min(current / total, 1) : 0;
92
+ const filled = Math.round(pct * width);
93
+ return `[${"".repeat(filled)}${"".repeat(width - filled)}] ${(pct * 100).toFixed(1)}%`;
80
94
  }
81
95
  function formatTokens(n) {
82
- return n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
96
+ return n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n);
83
97
  }
84
98
  function resolveThreshold(t) {
85
- return typeof t === "number" ? t : t.max;
99
+ return typeof t === "number" ? t : t.max;
86
100
  }
87
- var MastraPlugin = async (ctx) => {
88
- const config = await loadConfig(ctx.directory);
89
- let credentialsReady = false;
90
- const resolveCredentials = async () => {
91
- if (credentialsReady) return;
92
- try {
93
- const providersResponse = await ctx.client.config.providers();
94
- if (providersResponse.data) {
95
- for (const provider of providersResponse.data.providers) {
96
- if (provider.key && provider.env) {
97
- for (const envVar of provider.env) {
98
- if (!process.env[envVar]) {
99
- process.env[envVar] = provider.key;
100
- }
101
- }
102
- }
103
- }
104
- }
105
- } catch {
106
- }
107
- credentialsReady = true;
108
- };
109
- const dbRelativePath = config.storagePath ?? DEFAULT_STORAGE_PATH;
110
- const dbAbsolutePath = join(ctx.directory, dbRelativePath);
111
- await mkdir(dirname(dbAbsolutePath), { recursive: true });
112
- const storagePath = `file:${dbAbsolutePath}`;
113
- const store = new LibSQLStore({ id: "mastra-om", url: storagePath });
114
- await store.init();
115
- const storage = await store.getStore("memory");
116
- if (!storage) {
117
- throw new Error(`@mastra/opencode: failed to initialize memory storage from ${storagePath}`);
118
- }
119
- const om = new ObservationalMemory({
120
- storage,
121
- model: config.model,
122
- observation: config.observation,
123
- reflection: config.reflection,
124
- scope: config.scope,
125
- shareTokenBudget: config.shareTokenBudget
126
- });
127
- setTimeout(() => {
128
- void ctx.client.tui.showToast({
129
- body: {
130
- title: "Mastra",
131
- message: "Observational Memory activated",
132
- variant: "success",
133
- duration: 3e3
134
- }
135
- });
136
- }, 500);
137
- return {
138
- // Hook: Eagerly initialize OM record on session creation
139
- // so diagnostic tools work immediately (before first observation cycle).
140
- event: async ({ event }) => {
141
- if (event.type === "session.created") {
142
- const sessionId = event.properties.info.id;
143
- try {
144
- await om.getOrCreateRecord(sessionId);
145
- } catch (err) {
146
- void ctx.client.tui.showToast({
147
- body: {
148
- title: "Mastra",
149
- message: `Failed to initialize Observational Memory: ${err instanceof Error ? err.message : String(err)}`,
150
- variant: "error",
151
- duration: 5e3
152
- }
153
- });
154
- }
155
- }
156
- },
157
- // Hook: Transform messages before they reach the model.
158
- // This is the core integration point — observe and shape context in one pass:
159
- // 1. Convert opencode messages → MastraDBMessage format
160
- // 2. Run observation if threshold is met (with toast notifications)
161
- // 3. Inject observation summary and filter out already-observed messages
162
- "experimental.chat.messages.transform": async (_input, output) => {
163
- const sessionId = output.messages[0]?.info.sessionID;
164
- if (!sessionId) return;
165
- await resolveCredentials();
166
- try {
167
- const mastraMessages = convertMessages(output.messages, sessionId);
168
- if (mastraMessages.length > 0) {
169
- await om.observe({
170
- threadId: sessionId,
171
- messages: mastraMessages,
172
- hooks: {
173
- onObservationStart: () => {
174
- void ctx.client.tui.showToast({
175
- body: {
176
- title: "Mastra",
177
- message: "Observing conversation...",
178
- variant: "info",
179
- duration: 1e4
180
- }
181
- });
182
- },
183
- onObservationEnd: () => {
184
- void ctx.client.tui.showToast({
185
- body: {
186
- title: "Mastra",
187
- message: "Observation complete",
188
- variant: "success",
189
- duration: 3e3
190
- }
191
- });
192
- },
193
- onReflectionStart: () => {
194
- void ctx.client.tui.showToast({
195
- body: {
196
- title: "Mastra",
197
- message: "Reflecting on observations...",
198
- variant: "info",
199
- duration: 1e4
200
- }
201
- });
202
- },
203
- onReflectionEnd: () => {
204
- void ctx.client.tui.showToast({
205
- body: {
206
- title: "Mastra",
207
- message: "Reflection complete",
208
- variant: "success",
209
- duration: 3e3
210
- }
211
- });
212
- }
213
- }
214
- });
215
- }
216
- const record = await om.getRecord(sessionId);
217
- if (record?.lastObservedAt) {
218
- const lastObservedAt = new Date(record.lastObservedAt);
219
- output.messages = output.messages.filter(({ info }) => {
220
- const msgTime = new Date(info.time.created);
221
- return msgTime > lastObservedAt;
222
- });
223
- }
224
- } catch (err) {
225
- void ctx.client.tui.showToast({
226
- body: {
227
- title: "Mastra",
228
- message: `Observational Memory error: ${err instanceof Error ? err.message : String(err)}`,
229
- variant: "error",
230
- duration: 5e3
231
- }
232
- });
233
- }
234
- },
235
- // Hook: Inject observations into the system prompt so the model has compressed context.
236
- "experimental.chat.system.transform": async (input, output) => {
237
- const sessionId = input.sessionID;
238
- if (!sessionId) return;
239
- try {
240
- const observations = await om.getObservations(sessionId);
241
- if (!observations) return;
242
- const optimized = optimizeObservationsForContext(observations);
243
- output.system.push(
244
- `${OBSERVATION_CONTEXT_PROMPT}
245
-
246
- <observations>
247
- ${optimized}
248
- </observations>
249
-
250
- ${OBSERVATION_CONTEXT_INSTRUCTIONS}
251
-
252
- ${OBSERVATION_CONTINUATION_HINT}`
253
- );
254
- } catch {
255
- }
256
- },
257
- // Diagnostic tools for inspecting OM state
258
- tool: {
259
- memory_status: tool({
260
- description: "Show Observational Memory progress \u2014 how close the session is to the next observation and reflection cycle.",
261
- args: {},
262
- async execute(_args, context) {
263
- const threadId = context.sessionID;
264
- const record = await om.getRecord(threadId);
265
- if (!record) {
266
- return "No Observational Memory record found for this session.";
267
- }
268
- const omConfig = om.config;
269
- const obsThreshold = resolveThreshold(omConfig.observation.messageTokens);
270
- const refThreshold = resolveThreshold(omConfig.reflection.observationTokens);
271
- const obsTokens = record.observationTokenCount ?? 0;
272
- const tokenCounter = new TokenCounter();
273
- let unobservedTokens = 0;
274
- try {
275
- const resp = await ctx.client.session.messages({ path: { id: threadId } });
276
- if (resp.data) {
277
- const allMastra = convertMessages(resp.data, threadId);
278
- const unobserved = record.lastObservedAt ? allMastra.filter((m) => m.createdAt > new Date(record.lastObservedAt)) : allMastra;
279
- unobservedTokens = tokenCounter.countMessages(unobserved);
280
- }
281
- } catch {
282
- unobservedTokens = record.pendingMessageTokens ?? 0;
283
- }
284
- const lines = [
285
- `Observational Memory`,
286
- `Scope: ${record.scope} | Generations: ${record.generationCount ?? 0}`,
287
- ``,
288
- `\u2500\u2500 Observation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
289
- `Unobserved: ${formatTokens(unobservedTokens)} / ${formatTokens(obsThreshold)} tokens`,
290
- progressBar(unobservedTokens, obsThreshold),
291
- ``,
292
- `\u2500\u2500 Reflection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
293
- `Observations: ${formatTokens(obsTokens)} / ${formatTokens(refThreshold)} tokens`,
294
- progressBar(obsTokens, refThreshold),
295
- ``,
296
- `\u2500\u2500 Status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
297
- `Last observed: ${record.lastObservedAt ?? "never"}`,
298
- `Observing: ${record.isObserving ? "yes" : "no"} | Reflecting: ${record.isReflecting ? "yes" : "no"}`
299
- ];
300
- return lines.join("\n");
301
- }
302
- }),
303
- memory_observations: tool({
304
- description: "Show the current active observations stored in Observational Memory.",
305
- args: {},
306
- async execute(_args, context) {
307
- const threadId = context.sessionID;
308
- const observations = await om.getObservations(threadId);
309
- return observations ?? "No observations stored yet.";
310
- }
311
- })
312
- }
313
- };
101
+ const MastraPlugin = async (ctx) => {
102
+ const config = await loadConfig(ctx.directory);
103
+ let credentialsReady = false;
104
+ const resolveCredentials = async () => {
105
+ if (credentialsReady) return;
106
+ try {
107
+ const providersResponse = await ctx.client.config.providers();
108
+ if (providersResponse.data) {
109
+ for (const provider of providersResponse.data.providers) if (provider.key && provider.env) {
110
+ for (const envVar of provider.env) if (!process.env[envVar]) process.env[envVar] = provider.key;
111
+ }
112
+ }
113
+ } catch {}
114
+ credentialsReady = true;
115
+ };
116
+ const dbRelativePath = config.storagePath ?? DEFAULT_STORAGE_PATH;
117
+ const dbAbsolutePath = join(ctx.directory, dbRelativePath);
118
+ await mkdir(dirname(dbAbsolutePath), { recursive: true });
119
+ const storagePath = `file:${dbAbsolutePath}`;
120
+ const store = new LibSQLStore({
121
+ id: "mastra-om",
122
+ url: storagePath
123
+ });
124
+ await store.init();
125
+ const storage = await store.getStore("memory");
126
+ if (!storage) throw new Error(`@mastra/opencode: failed to initialize memory storage from ${storagePath}`);
127
+ const om = new ObservationalMemory({
128
+ storage,
129
+ model: config.model,
130
+ observation: config.observation,
131
+ reflection: config.reflection,
132
+ scope: config.scope,
133
+ shareTokenBudget: config.shareTokenBudget
134
+ });
135
+ setTimeout(() => {
136
+ ctx.client.tui.showToast({ body: {
137
+ title: "Mastra",
138
+ message: "Observational Memory activated",
139
+ variant: "success",
140
+ duration: 3e3
141
+ } });
142
+ }, 500);
143
+ return {
144
+ event: async ({ event }) => {
145
+ if (event.type === "session.created") {
146
+ const sessionId = event.properties.info.id;
147
+ try {
148
+ await om.getOrCreateRecord(sessionId);
149
+ } catch (err) {
150
+ ctx.client.tui.showToast({ body: {
151
+ title: "Mastra",
152
+ message: `Failed to initialize Observational Memory: ${err instanceof Error ? err.message : String(err)}`,
153
+ variant: "error",
154
+ duration: 5e3
155
+ } });
156
+ }
157
+ }
158
+ },
159
+ "experimental.chat.messages.transform": async (_input, output) => {
160
+ const sessionId = output.messages[0]?.info.sessionID;
161
+ if (!sessionId) return;
162
+ await resolveCredentials();
163
+ try {
164
+ const mastraMessages = convertMessages(output.messages, sessionId);
165
+ if (mastraMessages.length > 0) await om.observe({
166
+ threadId: sessionId,
167
+ messages: mastraMessages,
168
+ hooks: {
169
+ onObservationStart: () => {
170
+ ctx.client.tui.showToast({ body: {
171
+ title: "Mastra",
172
+ message: "Observing conversation...",
173
+ variant: "info",
174
+ duration: 1e4
175
+ } });
176
+ },
177
+ onObservationEnd: () => {
178
+ ctx.client.tui.showToast({ body: {
179
+ title: "Mastra",
180
+ message: "Observation complete",
181
+ variant: "success",
182
+ duration: 3e3
183
+ } });
184
+ },
185
+ onReflectionStart: () => {
186
+ ctx.client.tui.showToast({ body: {
187
+ title: "Mastra",
188
+ message: "Reflecting on observations...",
189
+ variant: "info",
190
+ duration: 1e4
191
+ } });
192
+ },
193
+ onReflectionEnd: () => {
194
+ ctx.client.tui.showToast({ body: {
195
+ title: "Mastra",
196
+ message: "Reflection complete",
197
+ variant: "success",
198
+ duration: 3e3
199
+ } });
200
+ }
201
+ }
202
+ });
203
+ const record = await om.getRecord(sessionId);
204
+ if (record?.lastObservedAt) {
205
+ const lastObservedAt = new Date(record.lastObservedAt);
206
+ output.messages = output.messages.filter(({ info }) => {
207
+ return new Date(info.time.created) > lastObservedAt;
208
+ });
209
+ }
210
+ } catch (err) {
211
+ ctx.client.tui.showToast({ body: {
212
+ title: "Mastra",
213
+ message: `Observational Memory error: ${err instanceof Error ? err.message : String(err)}`,
214
+ variant: "error",
215
+ duration: 5e3
216
+ } });
217
+ }
218
+ },
219
+ "experimental.chat.system.transform": async (input, output) => {
220
+ const sessionId = input.sessionID;
221
+ if (!sessionId) return;
222
+ try {
223
+ const observations = await om.getObservations(sessionId);
224
+ if (!observations) return;
225
+ const optimized = optimizeObservationsForContext(observations);
226
+ output.system.push(`${OBSERVATION_CONTEXT_PROMPT}\n\n<observations>\n${optimized}\n</observations>\n\n${OBSERVATION_CONTEXT_INSTRUCTIONS}\n\n${OBSERVATION_CONTINUATION_HINT}`);
227
+ } catch {}
228
+ },
229
+ tool: {
230
+ memory_status: tool({
231
+ description: "Show Observational Memory progress — how close the session is to the next observation and reflection cycle.",
232
+ args: {},
233
+ async execute(_args, context) {
234
+ const threadId = context.sessionID;
235
+ const record = await om.getRecord(threadId);
236
+ if (!record) return "No Observational Memory record found for this session.";
237
+ const omConfig = om.config;
238
+ const obsThreshold = resolveThreshold(omConfig.observation.messageTokens);
239
+ const refThreshold = resolveThreshold(omConfig.reflection.observationTokens);
240
+ const obsTokens = record.observationTokenCount ?? 0;
241
+ const tokenCounter = new TokenCounter();
242
+ let unobservedTokens = 0;
243
+ try {
244
+ const resp = await ctx.client.session.messages({ path: { id: threadId } });
245
+ if (resp.data) {
246
+ const allMastra = convertMessages(resp.data, threadId);
247
+ const unobserved = record.lastObservedAt ? allMastra.filter((m) => m.createdAt > new Date(record.lastObservedAt)) : allMastra;
248
+ unobservedTokens = tokenCounter.countMessages(unobserved);
249
+ }
250
+ } catch {
251
+ unobservedTokens = record.pendingMessageTokens ?? 0;
252
+ }
253
+ return [
254
+ `Observational Memory`,
255
+ `Scope: ${record.scope} | Generations: ${record.generationCount ?? 0}`,
256
+ ``,
257
+ `── Observation ──────────────────────────────`,
258
+ `Unobserved: ${formatTokens(unobservedTokens)} / ${formatTokens(obsThreshold)} tokens`,
259
+ progressBar(unobservedTokens, obsThreshold),
260
+ ``,
261
+ `── Reflection ──────────────────────────────`,
262
+ `Observations: ${formatTokens(obsTokens)} / ${formatTokens(refThreshold)} tokens`,
263
+ progressBar(obsTokens, refThreshold),
264
+ ``,
265
+ `── Status ──────────────────────────────────`,
266
+ `Last observed: ${record.lastObservedAt ?? "never"}`,
267
+ `Observing: ${record.isObserving ? "yes" : "no"} | Reflecting: ${record.isReflecting ? "yes" : "no"}`
268
+ ].join("\n");
269
+ }
270
+ }),
271
+ memory_observations: tool({
272
+ description: "Show the current active observations stored in Observational Memory.",
273
+ args: {},
274
+ async execute(_args, context) {
275
+ const threadId = context.sessionID;
276
+ return await om.getObservations(threadId) ?? "No observations stored yet.";
277
+ }
278
+ })
279
+ }
280
+ };
314
281
  };
315
-
282
+ //#endregion
316
283
  export { MastraPlugin };
317
- //# sourceMappingURL=index.js.map
284
+
318
285
  //# sourceMappingURL=index.js.map