17 KiB
description: YAMS-first agent with blackboard coordination and persistent memory argument-hint: [TASK=] [PBI=] [PHASE=<start|checkpoint|complete>]
Agent Workflow (YAMS + Blackboard)
YAMS is the single source of truth for agent memory. When multiple agents are involved, use the blackboard tools to coordinate work.
Core Identity
- Default mode: use YAMS for memory (code + notes + decisions + research).
- Coordination mode: always try to register on the blackboard and use it to share findings/tasks.
- If blackboard tools are unavailable, fall back to the YAMS-only workflow (file claiming + metadata).
Agent ID Convention
- Use a stable, readable ID:
opencode-<task-slug>. - Keep
<task-slug>lowercase ASCII with dashes (example:opencode-index-speedup).
Do / Don't
Do
- Always register on the blackboard at the start of a task.
- Use the blackboard to coordinate: post findings, create/claim tasks, group work in contexts.
- Search before acting:
yams grepfor code patterns;yams searchfor semantic/concept queries. - Index as you learn: add code, notes, decisions, and research to YAMS as you go.
- Use metadata consistently so knowledge is queryable across sessions.
Don't
- Never
git pushwithout first indexing the work in YAMS. - Never delete files without first indexing the work in YAMS.
- Don't duplicate the skill docs in this file; keep this file focused on behavior and workflow.
Safety & Permissions
Allowed Without Asking
- Read/list files, run targeted tests/lints, run builds when explicitly requested.
- YAMS operations: add/search/grep/graph/session/watch/status/doctor.
- Blackboard operations: post/query/search/claim/update/complete findings and tasks.
Ask First (Always)
git push(must index in YAMS first)- Deleting files (must index in YAMS first)
- Installing new dependencies
Required Metadata (Memory + PBI Tracking)
Attach metadata to every yams add.
task- short task slug (example:index-speedup)phase-start|checkpoint|completeowner-opencode(shared owner for multi-agent retrieval)source-code|note|decision|research
Optional (when applicable):
pbi- PBI identifier (example:PBI-043)agent_id- your canonical agent ID (example:opencode-index-speedup)
Project Structure (Where To Look First)
- CLI entry points and command wiring:
src/cli/ - Service layer and command handlers:
src/app/services/ - Search implementations (grep/semantic):
src/search/ - Storage engines and backends:
src/storage/ - Vector DB + embeddings:
src/vector/ - Daemon client/server:
src/daemon/ - MCP server implementation:
src/mcp/
Workflow
0) Register (Always)
Try blackboard registration first. If it fails, continue with YAMS-only flow.
bb_register_agent({ id: "opencode-<task-slug>", name: "OpenCode Agent", capabilities: ["yams", "code", "coordination"] })
Blackboard Coordination (Minimal)
Keep coordination lightweight: findings capture what was discovered; tasks capture what needs doing.
bb_search_findings({ query: "<keywords>" })
bb_post_finding({ agent_id: "opencode-<task-slug>", topic: "other", title: "<title>", content: "<markdown>" })
bb_create_task({ title: "<work item>", type: "fix", priority: 2, created_by: "opencode-<task-slug>" })
bb_claim_task({ task_id: "<task-id>", agent_id: "opencode-<task-slug>" })
bb_update_task({ task_id: "<task-id>", status: "working" })
1) Search Existing Knowledge
# Code patterns first
yams grep "<pattern>" --cwd .
# Semantic/concept search
yams search "$TASK" --limit 20
# Structured metadata lookups
yams search "task=$TASK" --type keyword --limit 20
2) Start Work (Index Baseline + Claim)
# Index baseline
yams add . --recursive \
--include "*.cpp,*.hpp,*.h,*.py,*.ts,*.js,*.md" \
--label "Working on: $TASK" \
--metadata "pbi=$PBI,task=$TASK,phase=start,owner=opencode,source=code,agent_id=opencode-$TASK"
If blackboard is available, claim or create a task there. If not, "claim" files via YAMS metadata:
yams add - --name "claim-$TASK.md" \
--metadata "pbi=$PBI,task=$TASK,phase=start,owner=opencode,source=note,agent_id=opencode-$TASK" \
<<'EOF'
## Claim
Agent: opencode-$TASK
Scope: <paths or subsystems>
Goal: <one sentence>
EOF
3) Checkpoint (Index What Changed)
yams add <changed-files> \
--label "$TASK: checkpoint" \
--metadata "pbi=$PBI,task=$TASK,phase=checkpoint,owner=opencode,source=code,agent_id=opencode-$TASK"
4) Complete (Index + Close Loop)
yams add . --recursive \
--include "*.cpp,*.hpp,*.h,*.py,*.ts,*.js,*.md" \
--label "Completed: $TASK" \
--metadata "pbi=$PBI,task=$TASK,phase=complete,owner=opencode,source=code,agent_id=opencode-$TASK"
Response Template
TASK: $TASK
PBI: $PBI
PHASE: $PHASE
AGENT: opencode-$TASK
CONTEXT FOUND:
- Blackboard: <notes/findings/tasks>
- YAMS: <docs/paths>
ACTIONS:
- <what changed and why>
INDEXED:
- <files/notes indexed>
- Metadata: task=$TASK,phase=$PHASE,owner=opencode,agent_id=opencode-$TASK
NEXT:
- <next step>
PR Checklist
- All modified/new files indexed in YAMS with metadata
- Any coordinated work reflected in blackboard tasks/findings (if available)
- No secret material added to the repo or indexed notes
- Commit/PR message explains "why" (not just "what")
When Stuck
- Ask one targeted clarifying question, with a recommended default.
- Post a blackboard finding if the answer should help other agents.
- Prefer small, reversible changes; avoid speculative rewrites.
Context Recovery
If the chat context is compacted or lost, rebuild it from YAMS using yams list filtered by owner=opencode, task, and recent time.
References (Skills)
yams(YAMS memory + search + graph)yams-blackboard(blackboard coordination on top of YAMS)
C++ Style Guide (YAMS Patterns)
.clang-format handles mechanical formatting (LLVM base, 4-space indent, 100-col limit, Attach braces, c++20). This section covers project-specific patterns that clang-format cannot enforce.
Formatting Notes
- Include order (enforced by
.clang-formatpriorities):"yams/..."project headers<tracy/...>profiler<sqlite3...>/<sqlite_vec...>- Third-party:
<spdlog/...>,<nlohmann/...>,<CLI11/...>,<magic_enum/...>,<toml/...>,<onnxruntime/...> - Standard library
<algorithm>,<string>, etc.
- Pointer/reference alignment: left (
int* p,const std::string& s). - Requires clauses go on their own line.
Naming Conventions
| Element | Style | Example |
|---|---|---|
| Functions, variables | camelCase |
loadConfig(), chunkSize |
| Types (class/struct/enum/concept) | PascalCase |
ResourceSnapshot, ErrorCode |
| Compile-time constants | kPascalCase |
kBase, kNumPriorities, kStartupGracePeriod |
| Member variables | trailing _ |
running_, strand_, autoEmbedPolicy_ |
| Static inline atomics | trailing _ |
postExtractionConcurrentDynamicCap_ |
| Namespaces | lowercase |
yams::features, yams::config |
| Files | snake_case |
dispatch_utils.hpp, indexing_service.cpp |
| Test files | *_catch2_test.cpp |
tuning_reconciliation_catch2_test.cpp |
Error Handling: Result<T>
Defined in include/yams/core/types.h. Variant-based (std::variant<T, Error>).
// Returning errors
Result<Config> loadConfig(const std::string& path) {
if (!exists(path))
return Error{ErrorCode::FileNotFound, "Config not found: " + path};
// ...
return Config{...};
}
// Consuming results
auto result = loadConfig(path);
if (!result) // explicit operator bool
return result.error(); // propagate
auto& cfg = result.value(); // access (throws if error)
// Result<void> for side-effect operations
Result<void> saveConfig(const Config& cfg);
When to use what:
| Mechanism | Use for |
|---|---|
Result<T> |
Fallible operations where the caller must handle errors |
std::optional<T> |
"Absent" values with no error detail (lookups, parsing) |
| Exceptions | Truly unrecoverable / programming errors (std::runtime_error) |
Future: when YAMS_HAS_EXPECTED is available on all targets, Result<T> may migrate to std::expected<T, Error> (APIs are similar).
Feature Gates (YAMS_HAS_*)
Defined in include/yams/core/cpp23_features.hpp. Always use these macros, never raw __cplusplus checks.
Available macros:
| Macro | Feature | Minimum |
|---|---|---|
YAMS_HAS_CONSTEXPR_VECTOR |
constexpr std::vector |
GCC 13, Clang 16 |
YAMS_HAS_CONSTEXPR_STRING |
constexpr std::string |
GCC 13, Clang 16 |
YAMS_HAS_CONSTEXPR_CONTAINERS |
Both of the above | GCC 13, Clang 16 |
YAMS_HAS_EXPECTED |
std::expected |
GCC 12, Clang 16 |
YAMS_HAS_RANGES |
std::ranges / std::views |
GCC 12, Clang 15 |
YAMS_HAS_FLAT_MAP |
std::flat_map |
GCC 12, Clang 17 |
YAMS_HAS_MOVE_ONLY_FUNCTION |
std::move_only_function |
GCC 12, Clang 17 |
YAMS_HAS_STRING_CONTAINS |
std::string::contains() |
GCC 12, Clang 12 |
YAMS_HAS_LIKELY_UNLIKELY |
[[likely]] / [[unlikely]] |
GCC 7, Clang 9 |
YAMS_HAS_PROFILES |
C++26 Profiles (experimental) | -- |
YAMS_HAS_REFLECTION |
C++26 Reflection (experimental) | -- |
Pattern:
#include <yams/core/cpp23_features.hpp>
#if YAMS_HAS_RANGES
#include <ranges>
auto filtered = items | std::views::filter(pred);
#else
std::vector<Item> filtered;
std::copy_if(items.begin(), items.end(), std::back_inserter(filtered), pred);
#endif
Convenience macro: YAMS_CONSTEXPR_IF_SUPPORTED expands to constexpr (C++23) or inline (C++20).
Compatibility helpers in yams::features::: string_contains(), string_starts_with(), string_ends_with() -- work on both C++20 and C++23.
Adding a new gate:
- Add a
#ifndef/#if defined(__cpp_lib_xxx)/#defineblock incpp23_features.hpp. - Add it to the
FeatureInfostruct. - Provide a C++20 fallback everywhere the feature is used.
- Test with both C++20 and C++23 build configs.
TuneAdvisor Knob Pattern
Defined in include/yams/daemon/components/TuneAdvisor.h (~2800 lines). Central registry for all runtime-tunable parameters.
Static inline atomic triplet (override -> env -> default):
// 1. Storage (private)
static inline std::atomic<uint32_t> chunkSizeOverride_{0};
// 2. Getter (priority: override -> env -> computed default)
static uint32_t chunkSize() {
uint32_t ovr = chunkSizeOverride_.load(std::memory_order_relaxed);
if (ovr) return ovr;
if (const char* s = std::getenv("YAMS_CHUNK_SIZE")) {
try { return static_cast<uint32_t>(std::stoull(s)); }
catch (...) {}
}
return 512u * 1024u; // default: 512 KiB
}
// 3. Setter
static void setChunkSize(uint32_t v) {
chunkSizeOverride_.store(v, std::memory_order_relaxed);
}
memory_order_relaxed is correct here because these knobs are best-effort advisory values read on hot paths; sequential consistency is not required.
DynamicCap vs Override (ratchet-down fix):
The tuning system has two write layers for post-ingest concurrency:
| Layer | Written by | Purpose |
|---|---|---|
*Override_ |
User / env | Hard cap, base budget input |
*DynamicCap_ |
TuningManager::tick_once() |
Temporary scaling cap, can recover to base |
postIngestBudgetedConcurrency(bool includeDynamicCaps):
false-- returns base budget (forResourceGovernor::updateScalingCaps())true-- returns effective value clamped by DynamicCaps (for runtime consumers:PostIngestQueue,EmbeddingService)
Setting a DynamicCap to 0 clears it (effective value returns to base budget).
InternalEventBus
Defined in include/yams/daemon/components/InternalEventBus.h. Singleton with typed SPSC channels.
// Create or get a typed channel
auto channel = InternalEventBus::instance()
.get_or_create_channel<InternalEventBus::PostIngestTask>(
"post_ingest_tasks", 4096);
// Push (non-blocking)
channel->try_push(InternalEventBus::PostIngestTask{hash, mimeType});
// Pop (non-blocking, returns std::optional or bool)
InternalEventBus::PostIngestTask task;
if (channel->try_pop(task)) { /* process */ }
Job types are nested structs: EmbedJob, PostIngestTask, KgJob, StoreDocumentTask, etc.
Channels are keyed by string name and cached in a std::unordered_map<std::string, std::shared_ptr<void>>. Template type is recovered via static_pointer_cast.
ResourceGovernor Interaction
Singleton in include/yams/daemon/components/ResourceGovernor.h. Components should check admission before resource-intensive work.
auto& gov = ResourceGovernor::instance();
// Admission checks (call before starting expensive work)
if (!gov.canAdmitWork()) return; // backpressure
if (!gov.canScaleUp("embed", 2)) return; // would exceed budget
if (!gov.canLoadModel(modelBytes)) return; // OOM risk
// Query current scaling limits
uint32_t maxWorkers = gov.maxIngestWorkers();
uint32_t maxEmbed = gov.maxEmbedConcurrency();
Pressure levels (ResourcePressureLevel): Normal -> Warning -> Critical -> Emergency. The governor tracks consecutive ticks at each level and progressively reduces scaling headroom.
ResourceSnapshot captures a point-in-time view of RSS, CPU, model memory, worker counts, and the computed pressure level.
Profiling (profiling.h)
All macros are no-ops when TRACY_ENABLE is not defined. Zero runtime cost in release builds.
#include <yams/profiling.h>
void processChunk(ByteSpan data) {
YAMS_ZONE_SCOPED_N("processChunk"); // named zone
YAMS_PLOT("ChunkSize", data.size()); // numeric telemetry
// ...
}
Domain-specific macros:
| Macro | Use in |
|---|---|
YAMS_STORAGE_ZONE(op) |
Storage engine operations (blue) |
YAMS_ONNX_ZONE(op) |
ONNX inference calls (green) |
YAMS_EMBEDDING_ZONE_BATCH(size) |
Embedding batch processing |
YAMS_VECTOR_SEARCH_ZONE(k, threshold) |
Vector search operations |
YAMS_SET_THREAD_NAME(name) |
Thread pool workers |
Async Patterns
The daemon uses boost::asio coroutines (C++20 coroutines with Boost wrappers).
Coroutine signature:
boost::asio::awaitable<void> tuningLoop();
boost::asio::awaitable<Response> process(const Request& request);
boost::asio::awaitable<bool> co_openDatabase(const fs::path& dbPath, int timeout_ms,
yams::compat::stop_token token);
Launching:
// co_spawn with future (joinable)
tuningFuture_ = boost::asio::co_spawn(strand_, tuningLoop(), boost::asio::use_future);
// co_spawn detached (fire-and-forget)
boost::asio::co_spawn(ioContext_, [&]() -> boost::asio::awaitable<void> {
co_await someAsyncOp();
}, boost::asio::detached);
Timed loop pattern:
boost::asio::awaitable<void> TuningManager::tuningLoop() {
boost::asio::steady_timer timer(co_await boost::asio::this_coro::executor);
while (running_) {
timer.expires_after(std::chrono::seconds(1));
co_await timer.async_wait(boost::asio::use_awaitable);
tick_once();
}
}
Testing Patterns
Framework: Catch2. Test files live in tests/unit/ and are named *_catch2_test.cpp.
RAII guards for global state:
TuneAdvisor uses static inline atomics, so tests must save/restore state to avoid cross-test contamination.
// Saves/restores TuneAdvisor::tuningProfile() across scope
class ProfileGuard {
public:
explicit ProfileGuard(TuneAdvisor::Profile p) : prev_(TuneAdvisor::tuningProfile()) {
TuneAdvisor::setTuningProfile(p);
}
~ProfileGuard() { TuneAdvisor::setTuningProfile(prev_); }
ProfileGuard(const ProfileGuard&) = delete;
ProfileGuard& operator=(const ProfileGuard&) = delete;
private:
TuneAdvisor::Profile prev_;
};
// Saves/restores an environment variable
class EnvGuard {
public:
EnvGuard(const char* name, const char* value);
~EnvGuard(); // restores or unsets
// non-copyable
};
// Saves/restores TuneAdvisor::hardwareConcurrency()
class HwGuard { /* similar pattern */ };
Usage:
TEST_CASE("Budget scales with profile", "[tuning]") {
EnvGuard envClean("YAMS_POST_INGEST_TOTAL_CONCURRENT", "0");
HwGuard hw(16);
ProfileGuard profile(TuneAdvisor::Profile::Balanced);
auto budget = TuneAdvisor::postIngestBudgetedConcurrency(false);
REQUIRE(budget.extraction >= 1);
REQUIRE(budget.total() <= 16);
}
Clearing atomics between tests: Set overrides to 0 (or use guards) before each TEST_CASE to ensure a clean slate. DynamicCap atomics reset to 0 = "no cap".