Skip to content

feat(storage): add PostgreSQL storage backend - #26

Closed
delete-cloud wants to merge 1 commit into
mainfrom
feat/pg-storage
Closed

delete-cloud wants to merge 1 commit into
mainfrom
feat/pg-storage

Conversation

@delete-cloud

Copy link
Copy Markdown
Owner

Summary

  • add the PostgreSQL storage backend as an isolated persistence lane
  • keep the storage backend work independently reviewable from bootstrap and UI changes

Copilot AI review requested due to automatic review settings April 12, 2026 02:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR introduces a PostgreSQL-backed persistence option and expands the runtime/CLI surfaces to support richer interactive operation (streaming output, shell mode, incremental context building), alongside related plumbing (hook specs/validation, tape/windowing, tracing) and docs.

Changes:

  • Added PostgreSQL storage primitives (session store, tape store, advisory session lock) built around asyncpg.
  • Refactored REPL UX: prompt-toolkit based input handling (multiline, paste folding, shell mode), unified terminal output helpers, status footer updates.
  • Upgraded agentkit runtime/tape/tool plumbing: incremental context build path via TapeView, typed anchors, tool output validation, hook return-type validation, and tracing.

Reviewed changes

Copilot reviewed 59 out of 190 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
coding-agent/src/coding_agent/cli/terminal_output.py Adds prompt-toolkit output wiring helpers for consistent REPL printing.
coding-agent/src/coding_agent/cli/repl.py Reworks REPL loop (streaming renderer/consumer, shell mode, status footer, prompt output).
coding-agent/src/coding_agent/cli/input_handler.py Adds prompt-toolkit sessions, keybindings, paste folding, toolbar, and shell/chat mode switching.
coding-agent/src/coding_agent/cli/commands.py Refactors slash commands output to prompt-toolkit HTML and adds new commands.
coding-agent/src/coding_agent/cli/bash_executor.py Adds an async shell executor for REPL shell mode.
coding-agent/src/coding_agent/cli/init.py Exports additional CLI helpers.
coding-agent/src/coding_agent/approval/store.py Cleans up pending approval removal semantics and formatting.
coding-agent/src/coding_agent/agents/subagent.py Removes legacy SubAgent stub types/module.
coding-agent/src/coding_agent/agent.toml Adds [kb] configuration defaults.
coding-agent/src/coding_agent/adapter.py Extends adapter to emit thinking/tool-result/usage events, approval bridge, and metrics.
coding-agent/src/coding_agent/main.py Large wiring changes: plugin factories, tool filtering, KB CLI, child pipeline builder.
coding-agent/src/agentkit/tracing.py Adds optional structlog-based tracing configuration.
coding-agent/src/agentkit/tools/schema.py Adds output_model to tool schema for typed outputs.
coding-agent/src/agentkit/tools/registry.py Adds tool retention/filtering and output validation; filters kwargs to tool signature.
coding-agent/src/agentkit/tools/decorator.py Extends @tool to support output_model and tighter parameter schema.
coding-agent/src/agentkit/tape/view.py Introduces TapeView abstraction for windowed tape consumption.
coding-agent/src/agentkit/tape/tape.py Adds windowing/persistence bookkeeping and append-only JSONL save semantics.
coding-agent/src/agentkit/tape/store.py Changes fork store commit to persist only deltas.
coding-agent/src/agentkit/tape/models.py Adds typed Anchor model and more robust (legacy-compatible) de/serialization.
coding-agent/src/agentkit/tape/anchor.py Re-exports anchor types for convenience/import hygiene.
coding-agent/src/agentkit/tape/init.py Exports Anchor/TapeView from tape package.
coding-agent/src/agentkit/storage/pg.py Adds PostgreSQL session/tape storage and advisory-lock helper.
coding-agent/src/agentkit/storage/init.py Minor export formatting.
coding-agent/src/agentkit/runtime/pipeline.py Adds incremental context build, new event types, structured tool result scoping, and tracing hooks.
coding-agent/src/agentkit/runtime/hookspecs.py Expands hook specs and adds declared return-type validation metadata.
coding-agent/src/agentkit/runtime/hook_runtime.py Validates hook return types against HookSpecs.
coding-agent/src/agentkit/runtime/init.py Adds Lifecycle protocol and reorganizes exports.
coding-agent/src/agentkit/providers/models.py Adds thinking/tool-result/usage stream events.
coding-agent/src/agentkit/providers/init.py Exports new event types.
coding-agent/src/agentkit/plugin/registry.py Warns on unknown hooks when HookSpecs are provided.
coding-agent/src/agentkit/errors.py Adds HookTypeError.
coding-agent/src/agentkit/directive/types.py Adds metadata to AskUser directive.
coding-agent/src/agentkit/directive/executor.py Adds optional tracing around directive execution.
coding-agent/src/agentkit/context/builder.py Adds TapeView support, incremental patching helpers, and anchor filtering rules.
coding-agent/src/agentkit/init.py Exports new runtime/provider symbols.
coding-agent/pyproject.toml Adds optional dependency group redis.
coding-agent/docs/** Adds multiple specs/plans and large TUI checklist documentation.
coding-agent/README.md Updates repo description and usage docs.
coding-agent/.gitignore Ignores .sisyphus/, data/, and other local caches.
coding-agent/KIMI-TUI-ARCHITECTURE.md Adds TUI architecture analysis doc.
coding-agent/CLAUDE-CODE-TUI-ARCHITECTURE.md Adds TUI architecture analysis doc.
Comments suppressed due to low confidence (1)

coding-agent/src/agentkit/tape/store.py:1

  • This changes ForkTapeStore.commit() semantics from saving the full tape snapshot to saving only a delta slice, without changing the TapeStore.save() API. If existing TapeStore implementations interpret save() as overwrite/replace (common for file/kv stores), this will silently drop historical entries. Either (mandatory) update the TapeStore protocol and all implementations to make save() append/delta-aware (e.g., append(tape_id, entries)), or keep committing the full snapshot here.
"""ForkTapeStore — transactional tape operations."""

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +7
from __future__ import annotations

import asyncio
import importlib
import json
from collections.abc import Awaitable, Callable
from typing import Final, Protocol, cast

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title/description focuses on adding a PostgreSQL storage backend, but this PR also includes sweeping changes to REPL input/output, hook/runtime APIs, tape/anchor models, incremental context building, KB CLI/config, and extensive docs. This makes review and rollback risk substantially higher. Consider splitting into smaller PRs (e.g., storage backend; runtime/hook changes; CLI/TUI changes; docs) or updating the PR description to accurately enumerate the broader scope.

Copilot uses AI. Check for mistakes.
Comment on lines 38 to +40
self._active.pop(fork.tape_id, None)
await self._backing.save(fork.tape_id, fork.to_list())
delta = fork.to_list()[self._base_lengths[fork.tape_id]:]
await self._backing.save(fork.tape_id, delta)

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_base_lengths is never cleaned up on commit or rollback. Over time, repeated forks will leak memory (and, in a long-running service, can grow unbounded). Also, indexing self._base_lengths[fork.tape_id] will raise KeyError if internal state gets out of sync. Recommended: pop the base length in both commit() and rollback() (and treat missing entries as a ValueError with a clear message).

Copilot uses AI. Check for mistakes.
Comment on lines +310 to +317
@self.bindings.add(Keys.Any, filter=Condition(lambda: not self._shell_mode))
def _(event):
buf = event.app.current_buffer
self._move_cursor_out_of_placeholder(buf)
data = event.data
if data:
buf.insert_text(data)

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Binding Keys.Any overrides prompt-toolkit's default key handling for many keys, including navigation/history keys (e.g., Up/Down, Home/End) and editing keys that don't have explicit bindings elsewhere. Since this handler doesn't replicate default behaviors, it can break standard line editing and history interaction. Fix: remove this Keys.Any handler and rely on prompt-toolkit’s default insertion, or restrict it to printable characters only (e.g., a filter based on event.data being a single printable char and no modifiers), while leaving non-text keys to prompt-toolkit.

Suggested change
@self.bindings.add(Keys.Any, filter=Condition(lambda: not self._shell_mode))
def _(event):
buf = event.app.current_buffer
self._move_cursor_out_of_placeholder(buf)
data = event.data
if data:
buf.insert_text(data)

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +49

for _sequence in ("\x1b[27;2;13~", "\x1b[13;2u"):
ANSI_SEQUENCES[_sequence] = _SHIFT_ENTER_SEQUENCE

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mutates prompt_toolkit.input.ansi_escape_sequences.ANSI_SEQUENCES at import time, which is a global side effect affecting any prompt-toolkit usage in the same process (including tests or other CLIs). Prefer registering these sequences in a more localized way (e.g., during InputHandler.__init__() with an idempotent guard, or via prompt-toolkit configuration/hooks if available) so importing the module doesn’t change global terminal parsing.

Suggested change
for _sequence in ("\x1b[27;2;13~", "\x1b[13;2u"):
ANSI_SEQUENCES[_sequence] = _SHIFT_ENTER_SEQUENCE
_SHIFT_ENTER_ESCAPE_SEQUENCES = ("\x1b[27;2;13~", "\x1b[13;2u")
_SHIFT_ENTER_SEQUENCES_REGISTERED = False
def _register_shift_enter_sequences() -> None:
global _SHIFT_ENTER_SEQUENCES_REGISTERED
if _SHIFT_ENTER_SEQUENCES_REGISTERED:
return
for _sequence in _SHIFT_ENTER_ESCAPE_SEQUENCES:
ANSI_SEQUENCES[_sequence] = _SHIFT_ENTER_SEQUENCE
_SHIFT_ENTER_SEQUENCES_REGISTERED = True

Copilot uses AI. Check for mistakes.
async def cmd_clear(args: list[str], context: dict[str, Any]) -> None:
"""Clear the screen."""
console.clear()
print("\033[2J\033[H", end="")

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/clear writes directly via print() rather than using the prompt-toolkit output instance (get_prompt_output() / print_pt). In a prompt-toolkit REPL (especially with patch_stdout()), writing to the wrong output stream can cause garbled rendering or no-op clears. Use the same output channel as other commands (e.g., write the escape sequence via print_pt(..., output=_out(), end=\"\") or write directly to the output object).

Suggested change
print("\033[2J\033[H", end="")
print_pt("\033[2J\033[H", output=_out(), end="")

Copilot uses AI. Check for mistakes.
Comment on lines +239 to +246
async def release(self) -> None:
if self._conn is None:
return
try:
_ = await self._conn.execute("SELECT pg_advisory_unlock_all()")
finally:
await self._pool.release(self._conn)
self._conn = None

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pg_advisory_unlock_all() releases all advisory locks held by the connection, not just the session-specific lock acquired in acquire(). If this connection is ever reused for other locks (now or later), this can cause incorrect unlock behavior. Safer: store the exact lock key used in acquire() and call pg_advisory_unlock(key) for that key.

Copilot uses AI. Check for mistakes.
Comment on lines +169 to +174
payload = result.model_dump()
return payload, json.dumps(payload)
if isinstance(result, str):
return result, result
if type(result) is dict:
return result, json.dumps(result)

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseModel.model_dump() can produce non-JSON-serializable values (e.g., datetime, Decimal) depending on the model config; json.dumps(payload) may raise at runtime. Prefer using a JSON-safe dump mode (e.g., model_dump(mode=\"json\")) or model_dump_json() for the display string, and consider json.dumps(..., default=str) if you want to guarantee non-failure for display.

Suggested change
payload = result.model_dump()
return payload, json.dumps(payload)
if isinstance(result, str):
return result, result
if type(result) is dict:
return result, json.dumps(result)
payload = result.model_dump(mode="json")
return payload, json.dumps(payload)
if isinstance(result, str):
return result, result
if type(result) is dict:
return result, json.dumps(result, default=str)

Copilot uses AI. Check for mistakes.
Comment on lines 150 to 155
plugin = self._registry.get(plugin_id)
mount_hook = plugin.hooks().get("mount")
if mount_hook is not None:
state = mount_hook()
state = mount_hook(ctx=ctx, runtime=self._runtime)
if state is not None:
ctx.plugin_states[plugin_id] = state

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling mount hooks with ctx/runtime kwargs is a breaking hook API change compared to a no-arg mount() convention. If third-party or older in-repo plugins exist that implement mount(self) without **kwargs, this will raise TypeError at runtime. If backward compatibility is required, consider inspecting the hook signature and calling it with no args when it doesn’t accept parameters, or standardize the hook signature and document it clearly as a versioned breaking change.

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +87
class PGSessionStore:
_CREATE_TABLE_SQL: Final[str] = """
CREATE TABLE IF NOT EXISTS agent_sessions (
session_id TEXT PRIMARY KEY,
payload JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
"""

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New PostgreSQL storage backends are introduced without accompanying unit tests. Given this is persistence infrastructure, it’s important to add tests for: schema creation idempotency, session upsert/load/delete, tape save/load ordering (seq increments), and lock acquire/release behavior. You can test these without a real database by injecting a fake pool_factory / fake pool implementing the AsyncPGPool/LockPool protocols and asserting the expected SQL calls/arguments.

Copilot uses AI. Check for mistakes.
[project.optional-dependencies]
redis = [
"redis>=5.0.0",
]

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A PostgreSQL backend was added (agentkit/storage/pg.py) but there is no corresponding optional dependency for asyncpg. Relying on a runtime ImportError works, but it makes installation/discovery harder for users. Consider adding a postgres (or pg) optional dependency group that includes asyncpg, similar to the new redis extra.

Suggested change
]
]
postgres = [
"asyncpg>=0.29.0",
]

Copilot uses AI. Check for mistakes.
Add a PostgreSQL-backed storage path so tapes and sessions can move beyond JSONL files when a durable backend is needed.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@delete-cloud

Copy link
Copy Markdown
Owner Author

Closing stale stacked PR after Task 0 landing. A clean replacement PR will follow from the rebased branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants