opencode-mastra-om 0.1.15 → 0.1.17
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 +60 -191
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -178099,81 +178099,65 @@ var MastraPlugin = async (ctx) => {
|
|
|
178099
178099
|
omLog(`[credentials] set ${envVars.join(", ")} from config.apiKey`);
|
|
178100
178100
|
}
|
|
178101
178101
|
}
|
|
178102
|
-
|
|
178103
|
-
|
|
178104
|
-
|
|
178105
|
-
|
|
178106
|
-
|
|
178107
|
-
|
|
178108
|
-
|
|
178109
|
-
|
|
178110
|
-
|
|
178111
|
-
|
|
178112
|
-
for (const envVar of provider.env) {
|
|
178113
|
-
if (!process.env[envVar]) {
|
|
178114
|
-
process.env[envVar] = key;
|
|
178115
|
-
omLog(`[credentials] set ${envVar} from provider store`);
|
|
178116
|
-
}
|
|
178102
|
+
try {
|
|
178103
|
+
const providersResponse = await ctx.client.config.providers();
|
|
178104
|
+
if (providersResponse.data) {
|
|
178105
|
+
for (const provider of providersResponse.data.providers) {
|
|
178106
|
+
const key = provider.key ?? provider.apiKey ?? provider.token;
|
|
178107
|
+
if (key && provider.env) {
|
|
178108
|
+
for (const envVar of provider.env) {
|
|
178109
|
+
if (!process.env[envVar]) {
|
|
178110
|
+
process.env[envVar] = key;
|
|
178111
|
+
omLog(`[credentials] set ${envVar} from provider store`);
|
|
178117
178112
|
}
|
|
178118
178113
|
}
|
|
178119
178114
|
}
|
|
178120
178115
|
}
|
|
178121
|
-
} catch (e2) {
|
|
178122
|
-
omLog(`[credentials] provider store unavailable: ${e2}`);
|
|
178123
178116
|
}
|
|
178117
|
+
} catch (e2) {
|
|
178118
|
+
omLog(`[credentials] provider store unavailable: ${e2}`);
|
|
178124
178119
|
}
|
|
178125
178120
|
credentialsReady = true;
|
|
178126
178121
|
omLog(`[credentials] resolved. GOOGLE_GENERATIVE_AI_API_KEY=${process.env.GOOGLE_GENERATIVE_AI_API_KEY ? "set" : "missing"}`);
|
|
178127
178122
|
};
|
|
178128
178123
|
let store;
|
|
178129
|
-
|
|
178130
|
-
|
|
178131
|
-
|
|
178132
|
-
|
|
178133
|
-
|
|
178134
|
-
|
|
178135
|
-
|
|
178136
|
-
|
|
178137
|
-
|
|
178138
|
-
|
|
178139
|
-
|
|
178140
|
-
|
|
178141
|
-
|
|
178142
|
-
|
|
178143
|
-
|
|
178144
|
-
|
|
178145
|
-
|
|
178146
|
-
|
|
178147
|
-
|
|
178148
|
-
|
|
178149
|
-
|
|
178150
|
-
|
|
178151
|
-
|
|
178152
|
-
|
|
178153
|
-
|
|
178154
|
-
|
|
178155
|
-
|
|
178156
|
-
|
|
178157
|
-
|
|
178158
|
-
|
|
178159
|
-
...config2.observationModel ? { model: config2.observationModel } : {}
|
|
178160
|
-
},
|
|
178161
|
-
reflection: {
|
|
178162
|
-
...config2.reflection,
|
|
178163
|
-
...config2.reflectionModel ? { model: config2.reflectionModel } : {}
|
|
178164
|
-
}
|
|
178165
|
-
};
|
|
178166
|
-
if (config2.model && !config2.observationModel && !config2.reflectionModel) {
|
|
178167
|
-
omOptions2.model = config2.model;
|
|
178168
|
-
}
|
|
178169
|
-
om = new ObservationalMemory(omOptions2);
|
|
178170
|
-
omLog(`[init] ObservationalMemory created, model=${config2.model ?? "default"}`);
|
|
178124
|
+
if (config2.storageUrl && (config2.storageUrl.startsWith("postgresql://") || config2.storageUrl.startsWith("postgres://"))) {
|
|
178125
|
+
omLog(`[init] using PostgreSQL storage: ${config2.storageUrl.replace(/:\/\/[^@]+@/, "://<redacted>@")}`);
|
|
178126
|
+
const pgMod = await new Function('return import("@mastra/pg")')();
|
|
178127
|
+
const PostgresStore = pgMod.PostgresStore;
|
|
178128
|
+
store = new PostgresStore({ connectionString: config2.storageUrl });
|
|
178129
|
+
await store.init();
|
|
178130
|
+
} else {
|
|
178131
|
+
const url2 = config2.storageUrl ?? `file:${join5(ctx.directory, config2.storagePath ?? DEFAULT_STORAGE_PATH)}`;
|
|
178132
|
+
if (!config2.storageUrl) {
|
|
178133
|
+
const dbAbsolutePath = join5(ctx.directory, config2.storagePath ?? DEFAULT_STORAGE_PATH);
|
|
178134
|
+
await mkdir2(dirname4(dbAbsolutePath), { recursive: true });
|
|
178135
|
+
}
|
|
178136
|
+
omLog(`[init] using SQLite/LibSQL storage: ${url2}`);
|
|
178137
|
+
store = new LibSQLStore({ id: "mastra-om", url: url2 });
|
|
178138
|
+
await store.init();
|
|
178139
|
+
}
|
|
178140
|
+
const storage = await store.getStore("memory");
|
|
178141
|
+
if (!storage)
|
|
178142
|
+
throw new Error(`mastra-om: failed to initialize storage`);
|
|
178143
|
+
const omOptions = {
|
|
178144
|
+
storage,
|
|
178145
|
+
scope: config2.scope,
|
|
178146
|
+
shareTokenBudget: config2.shareTokenBudget,
|
|
178147
|
+
observation: {
|
|
178148
|
+
...config2.observation,
|
|
178149
|
+
...config2.observationModel ? { model: config2.observationModel } : {}
|
|
178150
|
+
},
|
|
178151
|
+
reflection: {
|
|
178152
|
+
...config2.reflection,
|
|
178153
|
+
...config2.reflectionModel ? { model: config2.reflectionModel } : {}
|
|
178171
178154
|
}
|
|
178172
|
-
}
|
|
178173
|
-
|
|
178174
|
-
|
|
178175
|
-
initFailed = true;
|
|
178155
|
+
};
|
|
178156
|
+
if (config2.model && !config2.observationModel && !config2.reflectionModel) {
|
|
178157
|
+
omOptions.model = config2.model;
|
|
178176
178158
|
}
|
|
178159
|
+
const om = new ObservationalMemory(omOptions);
|
|
178160
|
+
omLog(`[init] ObservationalMemory created, model=${config2.model ?? "default"}`);
|
|
178177
178161
|
const backupObservations = async (threadId, trigger) => {
|
|
178178
178162
|
try {
|
|
178179
178163
|
const record3 = await om.getRecord(threadId);
|
|
@@ -178192,15 +178176,15 @@ var MastraPlugin = async (ctx) => {
|
|
|
178192
178176
|
lastReflectionAt: record3.lastReflectionAt ?? null,
|
|
178193
178177
|
pendingMessageTokens: record3.pendingMessageTokens ?? 0,
|
|
178194
178178
|
observedMessageIds: record3.observedMessageIds ?? "[]",
|
|
178195
|
-
|
|
178179
|
+
trigger,
|
|
178196
178180
|
savedAt: new Date().toISOString()
|
|
178197
178181
|
};
|
|
178198
178182
|
await db.execute({
|
|
178199
178183
|
sql: `INSERT INTO mastra_om_backups
|
|
178200
178184
|
(id, lookupKey, slot, generationCount, observations, observationTokenCount,
|
|
178201
|
-
lastObservedAt, lastReflectionAt, pendingMessageTokens, observedMessageIds,
|
|
178185
|
+
lastObservedAt, lastReflectionAt, pendingMessageTokens, observedMessageIds, trigger, savedAt)
|
|
178202
178186
|
SELECT hex(randomblob(16)), lookupKey, 2, generationCount, observations, observationTokenCount,
|
|
178203
|
-
lastObservedAt, lastReflectionAt, pendingMessageTokens, observedMessageIds,
|
|
178187
|
+
lastObservedAt, lastReflectionAt, pendingMessageTokens, observedMessageIds, trigger, savedAt
|
|
178204
178188
|
FROM mastra_om_backups WHERE lookupKey = ? AND slot = 1
|
|
178205
178189
|
ON CONFLICT(lookupKey, slot) DO UPDATE SET
|
|
178206
178190
|
generationCount = excluded.generationCount,
|
|
@@ -178210,14 +178194,14 @@ var MastraPlugin = async (ctx) => {
|
|
|
178210
178194
|
lastReflectionAt = excluded.lastReflectionAt,
|
|
178211
178195
|
pendingMessageTokens = excluded.pendingMessageTokens,
|
|
178212
178196
|
observedMessageIds = excluded.observedMessageIds,
|
|
178213
|
-
|
|
178197
|
+
trigger = excluded.trigger,
|
|
178214
178198
|
savedAt = excluded.savedAt`,
|
|
178215
178199
|
args: [threadId]
|
|
178216
178200
|
});
|
|
178217
178201
|
await db.execute({
|
|
178218
178202
|
sql: `INSERT INTO mastra_om_backups
|
|
178219
178203
|
(id, lookupKey, slot, generationCount, observations, observationTokenCount,
|
|
178220
|
-
lastObservedAt, lastReflectionAt, pendingMessageTokens, observedMessageIds,
|
|
178204
|
+
lastObservedAt, lastReflectionAt, pendingMessageTokens, observedMessageIds, trigger, savedAt)
|
|
178221
178205
|
VALUES (hex(randomblob(16)), ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
178222
178206
|
ON CONFLICT(lookupKey, slot) DO UPDATE SET
|
|
178223
178207
|
generationCount = excluded.generationCount,
|
|
@@ -178227,7 +178211,7 @@ var MastraPlugin = async (ctx) => {
|
|
|
178227
178211
|
lastReflectionAt = excluded.lastReflectionAt,
|
|
178228
178212
|
pendingMessageTokens = excluded.pendingMessageTokens,
|
|
178229
178213
|
observedMessageIds = excluded.observedMessageIds,
|
|
178230
|
-
|
|
178214
|
+
trigger = excluded.trigger,
|
|
178231
178215
|
savedAt = excluded.savedAt`,
|
|
178232
178216
|
args: [
|
|
178233
178217
|
threadId,
|
|
@@ -178278,12 +178262,8 @@ var MastraPlugin = async (ctx) => {
|
|
|
178278
178262
|
};
|
|
178279
178263
|
return {
|
|
178280
178264
|
event: async ({ event }) => {
|
|
178281
|
-
omLog(`[event] received event type=${event.type}`);
|
|
178282
|
-
if (initFailed || !om)
|
|
178283
|
-
return;
|
|
178284
178265
|
if (event.type === "session.created") {
|
|
178285
178266
|
const sessionId = event.properties.info.id;
|
|
178286
|
-
omLog(`[session] creating record for ${sessionId}`);
|
|
178287
178267
|
try {
|
|
178288
178268
|
await om.getOrCreateRecord(sessionId);
|
|
178289
178269
|
omLog(`[session] initialized record for ${sessionId}`);
|
|
@@ -178294,32 +178274,16 @@ var MastraPlugin = async (ctx) => {
|
|
|
178294
178274
|
}
|
|
178295
178275
|
},
|
|
178296
178276
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
178297
|
-
omLog(`[transform] messages.transform called, messages=${output.messages.length}`);
|
|
178298
|
-
if (initFailed || !om) {
|
|
178299
|
-
omLog(`[transform] OM not initialized, skipping`);
|
|
178300
|
-
return;
|
|
178301
|
-
}
|
|
178302
178277
|
const sessionId = output.messages[0]?.info.sessionID;
|
|
178303
|
-
if (!sessionId)
|
|
178304
|
-
omLog(`[transform] no sessionId, skipping`);
|
|
178278
|
+
if (!sessionId)
|
|
178305
178279
|
return;
|
|
178306
|
-
}
|
|
178307
|
-
omLog(`[transform] sessionId=${sessionId}`);
|
|
178308
|
-
omLog(`[transform] calling resolveCredentials`);
|
|
178309
178280
|
await resolveCredentials();
|
|
178310
|
-
omLog(`[transform] resolveCredentials done`);
|
|
178311
178281
|
try {
|
|
178312
|
-
omLog(`[transform] converting messages`);
|
|
178313
178282
|
const mastraMessages = convertMessages2(output.messages, sessionId);
|
|
178314
|
-
omLog(`[transform] converted ${mastraMessages.length} messages`);
|
|
178315
178283
|
if (mastraMessages.length > 0) {
|
|
178316
|
-
omLog(`[transform] calling runObserve`);
|
|
178317
178284
|
await runObserve(sessionId, mastraMessages);
|
|
178318
|
-
omLog(`[transform] runObserve done`);
|
|
178319
178285
|
}
|
|
178320
|
-
omLog(`[transform] getting record`);
|
|
178321
178286
|
const record3 = await om.getRecord(sessionId);
|
|
178322
|
-
omLog(`[transform] got record, lastObservedAt=${record3?.lastObservedAt}`);
|
|
178323
178287
|
if (record3?.lastObservedAt) {
|
|
178324
178288
|
const lastObservedAt = new Date(record3.lastObservedAt);
|
|
178325
178289
|
output.messages = output.messages.filter(({ info }) => {
|
|
@@ -178327,7 +178291,6 @@ var MastraPlugin = async (ctx) => {
|
|
|
178327
178291
|
});
|
|
178328
178292
|
}
|
|
178329
178293
|
lastError = null;
|
|
178330
|
-
omLog(`[transform] done`);
|
|
178331
178294
|
} catch (err) {
|
|
178332
178295
|
lastError = err instanceof Error ? err.message : String(err);
|
|
178333
178296
|
omLog(`[error] transform failed: ${lastError}`);
|
|
@@ -178337,20 +178300,11 @@ var MastraPlugin = async (ctx) => {
|
|
|
178337
178300
|
}
|
|
178338
178301
|
},
|
|
178339
178302
|
"experimental.chat.system.transform": async (input, output) => {
|
|
178340
|
-
omLog(`[system.transform] called, sessionID=${input.sessionID}`);
|
|
178341
|
-
if (initFailed || !om) {
|
|
178342
|
-
omLog(`[system.transform] OM not initialized, skipping`);
|
|
178343
|
-
return;
|
|
178344
|
-
}
|
|
178345
178303
|
const sessionId = input.sessionID;
|
|
178346
|
-
if (!sessionId)
|
|
178347
|
-
omLog(`[system.transform] no sessionId, skipping`);
|
|
178304
|
+
if (!sessionId)
|
|
178348
178305
|
return;
|
|
178349
|
-
}
|
|
178350
178306
|
try {
|
|
178351
|
-
omLog(`[system.transform] getting observations`);
|
|
178352
178307
|
const observations = await om.getObservations(sessionId);
|
|
178353
|
-
omLog(`[system.transform] got observations, length=${observations?.length ?? 0}`);
|
|
178354
178308
|
if (!observations)
|
|
178355
178309
|
return;
|
|
178356
178310
|
const optimized = optimizeObservationsForContext(observations);
|
|
@@ -178363,10 +178317,7 @@ ${optimized}
|
|
|
178363
178317
|
${OBSERVATION_CONTEXT_INSTRUCTIONS}
|
|
178364
178318
|
|
|
178365
178319
|
${OBSERVATION_CONTINUATION_HINT}`);
|
|
178366
|
-
|
|
178367
|
-
} catch (err) {
|
|
178368
|
-
omLog(`[system.transform] error: ${err instanceof Error ? err.message : String(err)}`);
|
|
178369
|
-
}
|
|
178320
|
+
} catch {}
|
|
178370
178321
|
},
|
|
178371
178322
|
tool: {
|
|
178372
178323
|
om_status: tool5({
|
|
@@ -178426,7 +178377,7 @@ ${OBSERVATION_CONTINUATION_HINT}`);
|
|
|
178426
178377
|
}
|
|
178427
178378
|
}),
|
|
178428
178379
|
om_observe: tool5({
|
|
178429
|
-
description: "Manually trigger an observation cycle right now, without waiting for the token threshold.
|
|
178380
|
+
description: "Manually trigger an observation cycle right now, without waiting for the token threshold.",
|
|
178430
178381
|
args: {},
|
|
178431
178382
|
async execute(_args, context2) {
|
|
178432
178383
|
const threadId = context2.sessionID;
|
|
@@ -178437,53 +178388,8 @@ ${OBSERVATION_CONTINUATION_HINT}`);
|
|
|
178437
178388
|
return "No messages to observe.";
|
|
178438
178389
|
const mastraMessages = convertMessages2(resp.data, threadId);
|
|
178439
178390
|
await backupObservations(threadId, "pre-observe");
|
|
178440
|
-
|
|
178441
|
-
|
|
178442
|
-
await runObserve(threadId, mastraMessages);
|
|
178443
|
-
return "Observation cycle triggered. Check om_status for results.";
|
|
178444
|
-
}
|
|
178445
|
-
const chunks = [];
|
|
178446
|
-
let currentChunk = [];
|
|
178447
|
-
let currentBytes = 0;
|
|
178448
|
-
for (const msg of mastraMessages) {
|
|
178449
|
-
const msgBytes = Buffer.byteLength(JSON.stringify(msg), "utf8");
|
|
178450
|
-
if (currentBytes + msgBytes > chunkBytes && currentChunk.length > 0) {
|
|
178451
|
-
chunks.push(currentChunk);
|
|
178452
|
-
currentChunk = [msg];
|
|
178453
|
-
currentBytes = msgBytes;
|
|
178454
|
-
} else {
|
|
178455
|
-
currentChunk.push(msg);
|
|
178456
|
-
currentBytes += msgBytes;
|
|
178457
|
-
}
|
|
178458
|
-
}
|
|
178459
|
-
if (currentChunk.length > 0)
|
|
178460
|
-
chunks.push(currentChunk);
|
|
178461
|
-
if (chunks.length === 1) {
|
|
178462
|
-
await runObserve(threadId, mastraMessages);
|
|
178463
|
-
return "Observation cycle triggered (single chunk). Check om_status for results.";
|
|
178464
|
-
}
|
|
178465
|
-
omLog(`[observe] chunked into ${chunks.length} chunks of ~${Math.round(chunkBytes / 1024)}KB each`);
|
|
178466
|
-
ctx.client.tui.showToast({
|
|
178467
|
-
body: { title: "Mastra OM", message: `Observing in ${chunks.length} chunks (~${Math.round(chunkBytes / 1024)}KB each)...`, variant: "info", duration: 5000 }
|
|
178468
|
-
});
|
|
178469
|
-
const refThreshold = resolveThreshold(omOptions.reflection?.observationTokens ?? 60000);
|
|
178470
|
-
const reflectAt = Math.floor(refThreshold * 0.8);
|
|
178471
|
-
for (let i = 0;i < chunks.length; i++) {
|
|
178472
|
-
omLog(`[observe] processing chunk ${i + 1}/${chunks.length} (${chunks[i].length} messages)`);
|
|
178473
|
-
await runObserve(threadId, chunks[i]);
|
|
178474
|
-
if (i < chunks.length - 1) {
|
|
178475
|
-
const record3 = await om.getRecord(threadId);
|
|
178476
|
-
const obsTokens = record3?.observationTokenCount ?? 0;
|
|
178477
|
-
if (obsTokens >= reflectAt) {
|
|
178478
|
-
omLog(`[observe] observations at ${obsTokens} tokens (>= ${reflectAt}), reflecting before next chunk`);
|
|
178479
|
-
ctx.client.tui.showToast({
|
|
178480
|
-
body: { title: "Mastra OM", message: `Reflecting between chunks (${i + 1}/${chunks.length})...`, variant: "info", duration: 5000 }
|
|
178481
|
-
});
|
|
178482
|
-
await om.reflect(threadId);
|
|
178483
|
-
}
|
|
178484
|
-
}
|
|
178485
|
-
}
|
|
178486
|
-
return `Observation complete — processed ${chunks.length} chunks, ${mastraMessages.length} messages total. Check om_status for results.`;
|
|
178391
|
+
await runObserve(threadId, mastraMessages);
|
|
178392
|
+
return "Observation cycle triggered. Check memory_status for results.";
|
|
178487
178393
|
} catch (err) {
|
|
178488
178394
|
const msg = err instanceof Error ? err.message : String(err);
|
|
178489
178395
|
lastError = msg;
|
|
@@ -178569,7 +178475,7 @@ ${OBSERVATION_CONTINUATION_HINT}`);
|
|
|
178569
178475
|
`✅ Restored from slot ${slot}`,
|
|
178570
178476
|
` Generation: ${row.generationCount}`,
|
|
178571
178477
|
` Saved at: ${row.savedAt}`,
|
|
178572
|
-
` Trigger: ${row.
|
|
178478
|
+
` Trigger: ${row.trigger}`,
|
|
178573
178479
|
` Observation tokens: ${row.observationTokenCount}`,
|
|
178574
178480
|
` Last observed: ${row.lastObservedAt ?? "never"}`,
|
|
178575
178481
|
` Last reflection: ${row.lastReflectionAt ?? "never"}`
|
|
@@ -178581,43 +178487,6 @@ ${OBSERVATION_CONTINUATION_HINT}`);
|
|
|
178581
178487
|
}
|
|
178582
178488
|
}
|
|
178583
178489
|
}),
|
|
178584
|
-
om_reset: tool5({
|
|
178585
|
-
description: "Reset observational memory for this session to a clean slate. Backs up current state first so it can be restored via om_restore.",
|
|
178586
|
-
args: {},
|
|
178587
|
-
async execute(_args, context2) {
|
|
178588
|
-
const threadId = context2.sessionID;
|
|
178589
|
-
try {
|
|
178590
|
-
const db = store.turso;
|
|
178591
|
-
if (!db)
|
|
178592
|
-
return "Raw DB access unavailable.";
|
|
178593
|
-
await backupObservations(threadId, "pre-reset");
|
|
178594
|
-
await db.execute({
|
|
178595
|
-
sql: `UPDATE mastra_observational_memory SET
|
|
178596
|
-
activeObservations = '',
|
|
178597
|
-
generationCount = 0,
|
|
178598
|
-
observationTokenCount = 0,
|
|
178599
|
-
lastObservedAt = NULL,
|
|
178600
|
-
lastReflectionAt = NULL,
|
|
178601
|
-
pendingMessageTokens = 0,
|
|
178602
|
-
observedMessageIds = '[]',
|
|
178603
|
-
bufferedObservations = NULL,
|
|
178604
|
-
bufferedObservationTokens = 0,
|
|
178605
|
-
bufferedMessageIds = NULL,
|
|
178606
|
-
bufferedReflection = NULL,
|
|
178607
|
-
bufferedReflectionTokens = 0,
|
|
178608
|
-
bufferedReflectionInputTokens = 0,
|
|
178609
|
-
reflectedObservationLineCount = 0
|
|
178610
|
-
WHERE lookupKey = ?`,
|
|
178611
|
-
args: [threadId]
|
|
178612
|
-
});
|
|
178613
|
-
omLog(`[reset] observations cleared for ${threadId}`);
|
|
178614
|
-
return "✅ Observational memory reset. Previous state saved to backup slot 1 — use om_restore to recover if needed.";
|
|
178615
|
-
} catch (err) {
|
|
178616
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
178617
|
-
return `Reset failed: ${msg}`;
|
|
178618
|
-
}
|
|
178619
|
-
}
|
|
178620
|
-
}),
|
|
178621
178490
|
om_config: tool5({
|
|
178622
178491
|
description: "Show the current Mastra Observational Memory configuration.",
|
|
178623
178492
|
args: {},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "opencode-mastra-om",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.17",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"description": "Enhanced Mastra Observational Memory plugin for OpenCode — persistent cross-session memory with observation, reflection, and manual trigger tools",
|