): string, undo: callable(array): (true|\WP_Error)}> */ protected array $handlers = []; protected ?ToolContext $context = null; /** * Make `$tool` a write tool: how to describe a call before approval, and how to undo a recorded change. * * @since 0.1.0 * @param callable(array): string $preview * @param callable(array): (true|\WP_Error) $undo */ public function handler(string $tool, callable $preview, callable $undo): void { $this->handlers[$tool] = ['preview' => $preview, 'undo' => $undo]; } /** * @since 0.1.0 */ public function undoable(string $tool): bool { return isset($this->handlers[$tool]); } /** * @since 0.1.0 * @param array $args */ public function preview(string $tool, array $args): string { return isset($this->handlers[$tool]) ? (string) ($this->handlers[$tool]['preview'])($args) : ''; } /** * @since 0.1.0 */ public function open(ToolContext $context): void { $this->context = $context; } /** * @since 0.1.0 */ public function close(): void { $this->context = null; } /** * The call being run, or null outside an approved chat call. * * @since 0.1.0 */ public function current(): ?ToolContext { return $this->context; } /** * Record a change made by the call that is open. Returns the change id. * * @since 0.1.0 * @param array $before * @param array $after */ public function record(string $tool, string $objectType, int $objectId, array $before, array $after, string $summary): int { if ($this->context === null) { throw new \LogicException('No tool call is open.'); } return $this->insert([ 'session_id' => $this->context->sessionId, 'user_id' => $this->context->userId, 'run_id' => $this->context->runId, 'call_id' => $this->context->callId, 'idem_key' => $this->context->idempotencyKey(), 'tool' => $tool, 'object_type' => $objectType, 'object_id' => $objectId, 'summary' => $summary, 'before' => $before, 'after' => $after, ]); } /** * Undo change `$id` for `$userId`. True when it was undone now; a `WP_Error` explains why not. * * @since 0.1.0 * @return true|\WP_Error */ public function undo(int $userId, int $id): bool|\WP_Error { $change = $this->find($id); if ($change === null || (int) $change['user_id'] !== $userId) { return new \WP_Error('maze_graph_no_change', __('That change does not exist.', 'maze-graph')); } if ($change['status'] !== 'applied') { return new \WP_Error('maze_graph_already_undone', __('That change was already undone.', 'maze-graph')); } if (!isset($this->handlers[$change['tool']])) { return new \WP_Error('maze_graph_no_undo', __('That tool is no longer available, so its change cannot be undone here.', 'maze-graph')); } if (!$this->markUndone($id)) { return new \WP_Error('maze_graph_already_undone', __('That change was already undone.', 'maze-graph')); } $result = ($this->handlers[$change['tool']]['undo'])($change); if ($result !== true) { $this->markApplied($id); } return $result; } /** * The change recorded under an idempotency key (`run:call`), or null. * * @since 0.1.0 * @return array|null */ abstract public function findByKey(string $key): ?array; /** * @since 0.1.0 * @return array|null */ abstract public function find(int $id): ?array; /** * The latest applied change by one of `$tools` in each of `$sessionIds`: session id => change. One query, for a list. * * @since 0.1.4 * @param list $sessionIds * @param list $tools * @return array> */ abstract public function latestForSessions(array $sessionIds, array $tools): array; /** * The latest applied change by one of `$tools` to each of `$objectIds`, made by `$userId`: object id => change. * One query, for a list screen. * * @since 0.1.4 * @param list $objectIds * @param list $tools * @return array> */ abstract public function latestForObjects(int $userId, string $objectType, array $objectIds, array $tools): array; /** * The latest applied change by one of `$tools` to one object (a post), whoever made it, or null. * * @since 0.1.4 * @param list $tools * @return array|null */ abstract public function latestForObject(string $objectType, int $objectId, array $tools): ?array; /** * A bounded history projection for one object. Implementations must omit session identifiers, arguments, * snapshots, and any other fields not needed by the caller. Older journal adapters may safely return no rows. * * @param list $tools * @return list */ public function forObject(string $objectType, int $objectId, array $tools, int $limit = 20): array { return []; } /** * @since 0.1.0 * @param array $row */ abstract protected function insert(array $row): int; /** * Flip `applied` to `undone` in one conditional write; false when another request already did. * * @since 0.1.0 */ abstract protected function markUndone(int $id): bool; /** * @since 0.1.0 */ abstract protected function markApplied(int $id): void; /** * The newest changes, for the Activity page: one person's, or everyone's when `$userId` is 0. Each row has the * session's public id as `session_uuid`. * * @since 0.1.0 * @return list> */ abstract public function recent(int $userId, string $status = '', int $limit = 50, int $offset = 0): array; /** * Count the changes visible to the Activity page, optionally restricted to one status. * * @since 0.1.0 */ abstract public function countRecent(int $userId, string $status = ''): int; /** * The newest change a session made with one of `$tools` that is still applied, or null. * * @since 0.1.0 * @param list $tools * @return array|null */ abstract public function latestFor(int $sessionId, array $tools): ?array; /** * Forget the changes of sessions that are being deleted. * * @since 0.1.0 * @param list $sessionIds */ abstract public function forgetSessions(array $sessionIds): void; }