* feat: add OpenCode persistent memory plugin
Add hindsight-opencode integration with:
- Three custom tools: hindsight_retain, hindsight_recall, hindsight_reflect
- Auto-retain on session.idle with document_id deduplication
- Memory injection on session start via system transform hook
- Memory preservation during context window compaction
- Sliding window retain with retainOverlapTurns support
- 4-level config hierarchy (defaults, user file, plugin options, env vars)
- Dynamic bank ID derivation (agent, project, channel, user dimensions)
- CI job, release script entry, docs page
79 tests across 6 test files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address review findings for opencode integration
1. Pre-compaction retain now uses shared retainSession() helper,
respecting retainMode, documentId, and session_id metadata
consistently with idle-retain (was bypassing retention policy).
2. System transform recall is only consumed after successful injection.
If Hindsight is briefly unavailable, the plugin retries on the next
LLM call instead of permanently skipping recall for the session.
3. Config validation for retainMode and recallBudget — typos like
"full_session" or "maximum" now log a warning and fall back to
the default instead of silently changing retention semantics.
85 tests (6 new covering compaction documentId, recall retry, and
config validation).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: docs/tools findings from second review round
1. Remove "session" from supported dynamic bank fields in docs —
the implementation can't vary bank ID per session since it's
derived once at plugin startup.
2. Explicit tools (retain, reflect) now call ensureBankMission()
before API calls, so bankMission/retainMission are applied even
when the agent uses tools exclusively without triggering hooks.
3. Added tests for mission setup via tools path.
88 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: recall retry semantics and README bank scoping clarity
1. recallForContext now returns { context, ok } to distinguish
"no results" (ok=true) from "API error" (ok=false). System
transform consumes the session on ok=true even with 0 results,
so empty banks don't cause repeated queries. Only transient API
failures preserve retry.
2. README clarifies that channel/user bank dimensions are process-
scoped (set via env vars before launch), not per-session dynamic
within a running OpenCode process.
89 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: review fixes for opencode integration
- Rename CI job from build-opencode-integration to test-opencode-integration
to match naming convention for integrations that run tests
- Fix tsconfig module resolution to Node16 (consistent with other integrations)
- Extract shared makeConfig test helper to avoid duplication across 3 test files
* fix: remove unused PluginState import from tools.ts
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
109 lines
4.3 KiB
TypeScript
109 lines
4.3 KiB
TypeScript
/**
|
|
* Custom tool definitions for the Hindsight OpenCode plugin.
|
|
*
|
|
* Registers hindsight_retain, hindsight_recall, and hindsight_reflect
|
|
* as tools the agent can call explicitly.
|
|
*/
|
|
|
|
import { tool } from '@opencode-ai/plugin/tool';
|
|
import type { ToolDefinition } from '@opencode-ai/plugin/tool';
|
|
import type { HindsightClient } from '@vectorize-io/hindsight-client';
|
|
import type { HindsightConfig } from './config.js';
|
|
import { formatMemories, formatCurrentTime } from './content.js';
|
|
import { ensureBankMission } from './bank.js';
|
|
|
|
export interface HindsightTools {
|
|
hindsight_retain: ToolDefinition;
|
|
hindsight_recall: ToolDefinition;
|
|
hindsight_reflect: ToolDefinition;
|
|
}
|
|
|
|
export function createTools(
|
|
client: HindsightClient,
|
|
bankId: string,
|
|
config: HindsightConfig,
|
|
missionsSet?: Set<string>,
|
|
): HindsightTools {
|
|
const hindsight_retain = tool({
|
|
description:
|
|
'Store information in long-term memory. Use this to remember important facts, ' +
|
|
'user preferences, project context, decisions, and anything worth recalling in future sessions. ' +
|
|
'Be specific — include who, what, when, and why.',
|
|
args: {
|
|
content: tool.schema.string().describe(
|
|
'The information to remember. Be specific and self-contained.',
|
|
),
|
|
context: tool.schema
|
|
.string()
|
|
.optional()
|
|
.describe('Optional context about where this information came from.'),
|
|
},
|
|
async execute(args) {
|
|
if (missionsSet) {
|
|
await ensureBankMission(client, bankId, config, missionsSet);
|
|
}
|
|
await client.retain(bankId, args.content, {
|
|
context: args.context || config.retainContext,
|
|
tags: config.retainTags.length ? config.retainTags : undefined,
|
|
metadata: Object.keys(config.retainMetadata).length
|
|
? config.retainMetadata
|
|
: undefined,
|
|
});
|
|
return 'Memory stored successfully.';
|
|
},
|
|
});
|
|
|
|
const hindsight_recall = tool({
|
|
description:
|
|
'Search long-term memory for relevant information. Use this proactively before ' +
|
|
'answering questions about past conversations, user preferences, project history, ' +
|
|
'or any topic where prior context would help. When in doubt, recall first.',
|
|
args: {
|
|
query: tool.schema.string().describe(
|
|
'Natural language search query. Be specific about what you need to know.',
|
|
),
|
|
},
|
|
async execute(args) {
|
|
const response = await client.recall(bankId, args.query, {
|
|
budget: config.recallBudget as 'low' | 'mid' | 'high',
|
|
maxTokens: config.recallMaxTokens,
|
|
types: config.recallTypes,
|
|
});
|
|
|
|
const results = response.results || [];
|
|
if (!results.length) return 'No relevant memories found.';
|
|
|
|
const formatted = formatMemories(results);
|
|
return `Found ${results.length} relevant memories (as of ${formatCurrentTime()} UTC):\n\n${formatted}`;
|
|
},
|
|
});
|
|
|
|
const hindsight_reflect = tool({
|
|
description:
|
|
'Generate a thoughtful answer using long-term memory. Unlike recall (which returns ' +
|
|
'raw memories), reflect synthesizes memories into a coherent answer. Use for questions ' +
|
|
'like "What do you know about this user?" or "Summarize our project decisions."',
|
|
args: {
|
|
query: tool.schema.string().describe(
|
|
'The question to answer using long-term memory.',
|
|
),
|
|
context: tool.schema
|
|
.string()
|
|
.optional()
|
|
.describe('Optional additional context to guide the reflection.'),
|
|
},
|
|
async execute(args) {
|
|
if (missionsSet) {
|
|
await ensureBankMission(client, bankId, config, missionsSet);
|
|
}
|
|
const response = await client.reflect(bankId, args.query, {
|
|
context: args.context,
|
|
budget: config.recallBudget as 'low' | 'mid' | 'high',
|
|
});
|
|
|
|
return response.text || 'No relevant information found to reflect on.';
|
|
},
|
|
});
|
|
|
|
return { hindsight_retain, hindsight_recall, hindsight_reflect };
|
|
}
|