feat(brain): add BrainMemory model and migration
UUID-keyed brain_memories table with workspace scoping, self-referential supersession chain, TTL expiry, and confidence scoring. Eloquent model includes all scopes and helpers needed by the MCP tool layer. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
daa11bab39
commit
627813cc4d
2 changed files with 245 additions and 0 deletions
57
Migrations/0001_01_01_000008_create_brain_memories_table.php
Normal file
57
Migrations/0001_01_01_000008_create_brain_memories_table.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Create brain_memories table for OpenBrain shared knowledge store.
|
||||
*
|
||||
* Guarded with hasTable() so this migration is idempotent and
|
||||
* can coexist with the consolidated app-level migration.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
|
||||
if (! Schema::hasTable('brain_memories')) {
|
||||
Schema::create('brain_memories', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignId('workspace_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('agent_id', 64);
|
||||
$table->string('type', 32)->index();
|
||||
$table->text('content');
|
||||
$table->json('tags')->nullable();
|
||||
$table->string('project', 128)->nullable()->index();
|
||||
$table->float('confidence')->default(1.0);
|
||||
$table->uuid('supersedes_id')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('workspace_id');
|
||||
$table->index('agent_id');
|
||||
$table->index(['workspace_id', 'type']);
|
||||
$table->index(['workspace_id', 'project']);
|
||||
|
||||
$table->foreign('supersedes_id')
|
||||
->references('id')
|
||||
->on('brain_memories')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
Schema::enableForeignKeyConstraints();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
Schema::dropIfExists('brain_memories');
|
||||
Schema::enableForeignKeyConstraints();
|
||||
}
|
||||
};
|
||||
188
Models/BrainMemory.php
Normal file
188
Models/BrainMemory.php
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Mod\Agentic\Models;
|
||||
|
||||
use Core\Tenant\Concerns\BelongsToWorkspace;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* Brain Memory - a unit of shared knowledge in the OpenBrain store.
|
||||
*
|
||||
* Agents write observations, decisions, conventions, and research
|
||||
* into the brain so that other agents (and future sessions) can
|
||||
* recall organisational knowledge without re-discovering it.
|
||||
*
|
||||
* @property string $id
|
||||
* @property int $workspace_id
|
||||
* @property string $agent_id
|
||||
* @property string $type
|
||||
* @property string $content
|
||||
* @property array|null $tags
|
||||
* @property string|null $project
|
||||
* @property float $confidence
|
||||
* @property string|null $supersedes_id
|
||||
* @property \Carbon\Carbon|null $expires_at
|
||||
* @property \Carbon\Carbon|null $created_at
|
||||
* @property \Carbon\Carbon|null $updated_at
|
||||
* @property \Carbon\Carbon|null $deleted_at
|
||||
*/
|
||||
class BrainMemory extends Model
|
||||
{
|
||||
use BelongsToWorkspace;
|
||||
use HasUuids;
|
||||
use SoftDeletes;
|
||||
|
||||
/** Valid memory types. */
|
||||
public const VALID_TYPES = [
|
||||
'decision',
|
||||
'observation',
|
||||
'convention',
|
||||
'research',
|
||||
'plan',
|
||||
'bug',
|
||||
'architecture',
|
||||
];
|
||||
|
||||
protected $table = 'brain_memories';
|
||||
|
||||
protected $fillable = [
|
||||
'workspace_id',
|
||||
'agent_id',
|
||||
'type',
|
||||
'content',
|
||||
'tags',
|
||||
'project',
|
||||
'confidence',
|
||||
'supersedes_id',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'tags' => 'array',
|
||||
'confidence' => 'float',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Relationships
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
public function workspace(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Workspace::class);
|
||||
}
|
||||
|
||||
/** The older memory this one replaces. */
|
||||
public function supersedes(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'supersedes_id');
|
||||
}
|
||||
|
||||
/** Newer memories that replaced this one. */
|
||||
public function supersededBy(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'supersedes_id');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Scopes
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
public function scopeForWorkspace(Builder $query, int $workspaceId): Builder
|
||||
{
|
||||
return $query->where('workspace_id', $workspaceId);
|
||||
}
|
||||
|
||||
public function scopeOfType(Builder $query, string|array $type): Builder
|
||||
{
|
||||
return is_array($type)
|
||||
? $query->whereIn('type', $type)
|
||||
: $query->where('type', $type);
|
||||
}
|
||||
|
||||
public function scopeForProject(Builder $query, ?string $project): Builder
|
||||
{
|
||||
return $project
|
||||
? $query->where('project', $project)
|
||||
: $query;
|
||||
}
|
||||
|
||||
public function scopeByAgent(Builder $query, ?string $agentId): Builder
|
||||
{
|
||||
return $agentId
|
||||
? $query->where('agent_id', $agentId)
|
||||
: $query;
|
||||
}
|
||||
|
||||
/** Exclude memories whose TTL has passed. */
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where(function (Builder $q) {
|
||||
$q->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
});
|
||||
}
|
||||
|
||||
/** Exclude memories that have been superseded by a newer version. */
|
||||
public function scopeLatestVersions(Builder $query): Builder
|
||||
{
|
||||
return $query->whereDoesntHave('supersededBy', function (Builder $q) {
|
||||
$q->whereNull('deleted_at');
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Helpers
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Walk the supersession chain and return its depth.
|
||||
*
|
||||
* A memory that supersedes nothing returns 0.
|
||||
* Capped at 50 to prevent runaway loops.
|
||||
*/
|
||||
public function getSupersessionDepth(): int
|
||||
{
|
||||
$depth = 0;
|
||||
$current = $this;
|
||||
$maxDepth = 50;
|
||||
|
||||
while ($current->supersedes_id !== null && $depth < $maxDepth) {
|
||||
$current = $current->supersedes;
|
||||
|
||||
if ($current === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
$depth++;
|
||||
}
|
||||
|
||||
return $depth;
|
||||
}
|
||||
|
||||
/** Format the memory for MCP tool responses. */
|
||||
public function toMcpContext(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'agent_id' => $this->agent_id,
|
||||
'type' => $this->type,
|
||||
'content' => $this->content,
|
||||
'tags' => $this->tags,
|
||||
'project' => $this->project,
|
||||
'confidence' => $this->confidence,
|
||||
'supersedes_id' => $this->supersedes_id,
|
||||
'expires_at' => $this->expires_at?->toIso8601String(),
|
||||
'created_at' => $this->created_at?->toIso8601String(),
|
||||
'updated_at' => $this->updated_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in a new issue