php-agentic/Models/WorkspaceState.php
darbs-claude 7fadbcb96c
Some checks failed
CI / PHP 8.3 (pull_request) Failing after 1s
CI / PHP 8.4 (pull_request) Failing after 1s
refactor: consolidate duplicate state models into WorkspaceState (#18)
- Delete Models/AgentWorkspaceState.php (legacy port, no backing table)
- Rewrite Models/WorkspaceState.php as the single canonical state model
  backed by agent_workspace_states table with array value cast,
  type helpers, scopeForPlan/scopeOfType, static getValue/setValue,
  and toMcpContext() for MCP tool output
- Update AgentPlan::states() relation and setState() return type
- Update StateSet MCP tool import
- Update SecurityTest to use WorkspaceState
- Add WorkspaceStateTest covering table, casts, type helpers, scopes,
  static helpers, toMcpContext, and AgentPlan integration
- Mark CQ-001 done in TODO.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 13:26:23 +00:00

221 lines
5.4 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Mod\Agentic\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Workspace State Model
*
* Persistent key-value state storage for agent plans.
* Stores typed values shared across agent sessions within a plan,
* enabling context sharing and state recovery.
*
* @property int $id
* @property int $agent_plan_id
* @property string $key
* @property array $value
* @property string $type
* @property string|null $description
* @property \Carbon\Carbon|null $created_at
* @property \Carbon\Carbon|null $updated_at
*/
class WorkspaceState extends Model
{
use HasFactory;
protected $table = 'agent_workspace_states';
public const TYPE_JSON = 'json';
public const TYPE_MARKDOWN = 'markdown';
public const TYPE_CODE = 'code';
public const TYPE_REFERENCE = 'reference';
protected $fillable = [
'agent_plan_id',
'key',
'value',
'type',
'description',
];
protected $casts = [
'value' => 'array',
];
// Relationships
public function plan(): BelongsTo
{
return $this->belongsTo(AgentPlan::class, 'agent_plan_id');
}
// Scopes
public function scopeForPlan($query, AgentPlan|int $plan): mixed
{
$planId = $plan instanceof AgentPlan ? $plan->id : $plan;
<<<<<<< HEAD
/**
* Set typed value.
*/
public function setTypedValue(mixed $value): void
{
$storedValue = match ($this->type) {
self::TYPE_JSON => json_encode($value),
default => (string) $value,
};
$this->update(['value' => $storedValue]);
}
/**
* Get or create state for a plan.
*/
public static function getOrCreate(AgentPlan $plan, string $key, mixed $default = null, string $type = self::TYPE_JSON): self
{
$state = static::where('agent_plan_id', $plan->id)
->where('key', $key)
->first();
if (! $state) {
$value = match ($type) {
self::TYPE_JSON => json_encode($default),
default => (string) ($default ?? ''),
};
$state = static::create([
'agent_plan_id' => $plan->id,
'key' => $key,
'value' => $value,
'type' => $type,
]);
}
return $state;
}
/**
* Set state value for a plan.
*/
public static function set(AgentPlan $plan, string $key, mixed $value, string $type = self::TYPE_JSON): self
{
$storedValue = match ($type) {
self::TYPE_JSON => json_encode($value),
default => (string) $value,
};
return static::updateOrCreate(
['agent_plan_id' => $plan->id, 'key' => $key],
['value' => $storedValue, 'type' => $type]
);
}
/**
* Get state value for a plan.
*/
public static function get(AgentPlan $plan, string $key, mixed $default = null): mixed
{
$state = static::where('agent_plan_id', $plan->id)
->where('key', $key)
->first();
if (! $state) {
return $default;
}
return $state->getTypedValue();
}
/**
* Scope: for plan.
*/
public function scopeForPlan(Builder $query, int $planId): Builder
{
return $query->where('agent_plan_id', $planId);
}
/**
* Scope: by type.
*/
public function scopeByType(Builder $query, string $type): Builder
{
return $query->where('type', $type);
}
// Type helpers
public function isJson(): bool
{
return $this->type === self::TYPE_JSON;
}
public function isMarkdown(): bool
{
return $this->type === self::TYPE_MARKDOWN;
}
public function isCode(): bool
{
return $this->type === self::TYPE_CODE;
}
public function isReference(): bool
{
return $this->type === self::TYPE_REFERENCE;
}
public function getFormattedValue(): string
{
if ($this->isMarkdown() || $this->isCode()) {
return is_string($this->value) ? $this->value : json_encode($this->value, JSON_PRETTY_PRINT);
}
return json_encode($this->value, JSON_PRETTY_PRINT);
}
// Static helpers
/**
* Get a state value for a plan, returning $default if not set.
*/
public static function getValue(AgentPlan $plan, string $key, mixed $default = null): mixed
{
$state = static::where('agent_plan_id', $plan->id)->where('key', $key)->first();
return $state !== null ? $state->value : $default;
}
/**
* Set (upsert) a state value for a plan.
*/
public static function setValue(AgentPlan $plan, string $key, mixed $value, string $type = self::TYPE_JSON): self
{
return static::updateOrCreate(
['agent_plan_id' => $plan->id, 'key' => $key],
['value' => $value, 'type' => $type]
);
}
// MCP output
public function toMcpContext(): array
{
return [
'key' => $this->key,
'type' => $this->type,
'description' => $this->description,
'value' => $this->value,
'updated_at' => $this->updated_at?->toIso8601String(),
];
}
}