opencomputer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opencomputer/__init__.py +3 -0
- opencomputer/agent/__init__.py +1 -0
- opencomputer/agent/compaction.py +245 -0
- opencomputer/agent/config.py +108 -0
- opencomputer/agent/config_store.py +210 -0
- opencomputer/agent/injection.py +60 -0
- opencomputer/agent/loop.py +326 -0
- opencomputer/agent/memory.py +132 -0
- opencomputer/agent/prompt_builder.py +66 -0
- opencomputer/agent/prompts/base.j2 +23 -0
- opencomputer/agent/state.py +251 -0
- opencomputer/agent/step.py +31 -0
- opencomputer/cli.py +483 -0
- opencomputer/doctor.py +216 -0
- opencomputer/gateway/__init__.py +1 -0
- opencomputer/gateway/dispatch.py +89 -0
- opencomputer/gateway/protocol.py +84 -0
- opencomputer/gateway/server.py +77 -0
- opencomputer/gateway/wire_server.py +256 -0
- opencomputer/hooks/__init__.py +1 -0
- opencomputer/hooks/engine.py +79 -0
- opencomputer/hooks/runner.py +42 -0
- opencomputer/mcp/__init__.py +1 -0
- opencomputer/mcp/client.py +208 -0
- opencomputer/plugins/__init__.py +1 -0
- opencomputer/plugins/discovery.py +107 -0
- opencomputer/plugins/loader.py +155 -0
- opencomputer/plugins/registry.py +56 -0
- opencomputer/setup_wizard.py +235 -0
- opencomputer/skills/debug-python-import-error/SKILL.md +58 -0
- opencomputer/tools/__init__.py +1 -0
- opencomputer/tools/bash.py +78 -0
- opencomputer/tools/delegate.py +98 -0
- opencomputer/tools/glob.py +70 -0
- opencomputer/tools/grep.py +117 -0
- opencomputer/tools/read.py +81 -0
- opencomputer/tools/registry.py +69 -0
- opencomputer/tools/skill_manage.py +265 -0
- opencomputer/tools/write.py +58 -0
- opencomputer-0.1.0.dist-info/METADATA +190 -0
- opencomputer-0.1.0.dist-info/RECORD +51 -0
- opencomputer-0.1.0.dist-info/WHEEL +4 -0
- opencomputer-0.1.0.dist-info/entry_points.txt +3 -0
- plugin_sdk/__init__.py +66 -0
- plugin_sdk/channel_contract.py +74 -0
- plugin_sdk/core.py +129 -0
- plugin_sdk/hooks.py +80 -0
- plugin_sdk/injection.py +60 -0
- plugin_sdk/provider_contract.py +95 -0
- plugin_sdk/runtime_context.py +39 -0
- plugin_sdk/tool_contract.py +67 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session state — SQLite with FTS5 full-text search.
|
|
3
|
+
|
|
4
|
+
Schema inspired by hermes-agent/hermes_state.py. Kept minimal:
|
|
5
|
+
- sessions: one row per conversation
|
|
6
|
+
- messages: one row per turn (role + content + tool_calls JSON)
|
|
7
|
+
- messages_fts: FTS5 virtual table for cross-session search
|
|
8
|
+
|
|
9
|
+
Uses WAL mode + application-level retry jitter for concurrency.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import random
|
|
16
|
+
import sqlite3
|
|
17
|
+
import time
|
|
18
|
+
from collections.abc import Iterator
|
|
19
|
+
from contextlib import contextmanager
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from plugin_sdk.core import Message, ToolCall
|
|
24
|
+
|
|
25
|
+
SCHEMA_VERSION = 1
|
|
26
|
+
|
|
27
|
+
DDL = """
|
|
28
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
29
|
+
version INTEGER NOT NULL
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
33
|
+
id TEXT PRIMARY KEY,
|
|
34
|
+
started_at REAL NOT NULL,
|
|
35
|
+
ended_at REAL,
|
|
36
|
+
platform TEXT NOT NULL,
|
|
37
|
+
model TEXT,
|
|
38
|
+
title TEXT,
|
|
39
|
+
message_count INTEGER DEFAULT 0,
|
|
40
|
+
input_tokens INTEGER DEFAULT 0,
|
|
41
|
+
output_tokens INTEGER DEFAULT 0
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
45
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
46
|
+
session_id TEXT NOT NULL,
|
|
47
|
+
role TEXT NOT NULL,
|
|
48
|
+
content TEXT NOT NULL,
|
|
49
|
+
tool_call_id TEXT,
|
|
50
|
+
tool_calls TEXT, -- JSON array if role=assistant + tool calls
|
|
51
|
+
name TEXT, -- tool name for role=tool
|
|
52
|
+
reasoning TEXT, -- extended thinking
|
|
53
|
+
timestamp REAL NOT NULL,
|
|
54
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
|
|
58
|
+
|
|
59
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
60
|
+
content,
|
|
61
|
+
content='messages',
|
|
62
|
+
content_rowid='id',
|
|
63
|
+
tokenize='porter unicode61'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE TRIGGER IF NOT EXISTS messages_fts_insert
|
|
67
|
+
AFTER INSERT ON messages BEGIN
|
|
68
|
+
INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
|
|
69
|
+
END;
|
|
70
|
+
|
|
71
|
+
CREATE TRIGGER IF NOT EXISTS messages_fts_delete
|
|
72
|
+
AFTER DELETE ON messages BEGIN
|
|
73
|
+
INSERT INTO messages_fts(messages_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
74
|
+
END;
|
|
75
|
+
|
|
76
|
+
CREATE TRIGGER IF NOT EXISTS messages_fts_update
|
|
77
|
+
AFTER UPDATE ON messages BEGIN
|
|
78
|
+
INSERT INTO messages_fts(messages_fts, rowid, content) VALUES ('delete', old.id, old.content);
|
|
79
|
+
INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
|
|
80
|
+
END;
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SessionDB:
|
|
85
|
+
"""Lightweight SQLite wrapper for session storage + FTS5 search."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, db_path: Path) -> None:
|
|
88
|
+
self.db_path = db_path
|
|
89
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
self._init_schema()
|
|
91
|
+
|
|
92
|
+
def _connect(self) -> sqlite3.Connection:
|
|
93
|
+
conn = sqlite3.connect(
|
|
94
|
+
self.db_path,
|
|
95
|
+
isolation_level=None, # autocommit; we manage transactions explicitly
|
|
96
|
+
timeout=10.0,
|
|
97
|
+
)
|
|
98
|
+
conn.row_factory = sqlite3.Row
|
|
99
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
100
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
101
|
+
conn.execute("PRAGMA synchronous=NORMAL")
|
|
102
|
+
return conn
|
|
103
|
+
|
|
104
|
+
def _init_schema(self) -> None:
|
|
105
|
+
with self._connect() as conn:
|
|
106
|
+
conn.executescript(DDL)
|
|
107
|
+
cur = conn.execute("SELECT version FROM schema_version LIMIT 1")
|
|
108
|
+
row = cur.fetchone()
|
|
109
|
+
if row is None:
|
|
110
|
+
conn.execute("INSERT INTO schema_version(version) VALUES (?)", (SCHEMA_VERSION,))
|
|
111
|
+
|
|
112
|
+
@contextmanager
|
|
113
|
+
def _txn(self) -> Iterator[sqlite3.Connection]:
|
|
114
|
+
"""Run a transaction with retry+jitter on SQLITE_BUSY (adapted from hermes)."""
|
|
115
|
+
conn = self._connect()
|
|
116
|
+
attempts = 0
|
|
117
|
+
max_attempts = 5
|
|
118
|
+
while True:
|
|
119
|
+
try:
|
|
120
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
121
|
+
yield conn
|
|
122
|
+
conn.execute("COMMIT")
|
|
123
|
+
return
|
|
124
|
+
except sqlite3.OperationalError as e:
|
|
125
|
+
if "locked" not in str(e).lower() and "busy" not in str(e).lower():
|
|
126
|
+
raise
|
|
127
|
+
attempts += 1
|
|
128
|
+
if attempts >= max_attempts:
|
|
129
|
+
raise
|
|
130
|
+
time.sleep(random.uniform(0.02, 0.15))
|
|
131
|
+
finally:
|
|
132
|
+
conn.close()
|
|
133
|
+
|
|
134
|
+
# ─── sessions ─────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
def create_session(
|
|
137
|
+
self, session_id: str, platform: str = "cli", model: str = "", title: str = ""
|
|
138
|
+
) -> None:
|
|
139
|
+
with self._txn() as conn:
|
|
140
|
+
conn.execute(
|
|
141
|
+
"INSERT OR REPLACE INTO sessions (id, started_at, platform, model, title) "
|
|
142
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
143
|
+
(session_id, time.time(), platform, model, title),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def end_session(self, session_id: str) -> None:
|
|
147
|
+
with self._txn() as conn:
|
|
148
|
+
conn.execute(
|
|
149
|
+
"UPDATE sessions SET ended_at = ? WHERE id = ?",
|
|
150
|
+
(time.time(), session_id),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def get_session(self, session_id: str) -> dict[str, Any] | None:
|
|
154
|
+
with self._connect() as conn:
|
|
155
|
+
row = conn.execute(
|
|
156
|
+
"SELECT * FROM sessions WHERE id = ?", (session_id,)
|
|
157
|
+
).fetchone()
|
|
158
|
+
return dict(row) if row else None
|
|
159
|
+
|
|
160
|
+
def list_sessions(self, limit: int = 20) -> list[dict[str, Any]]:
|
|
161
|
+
with self._connect() as conn:
|
|
162
|
+
rows = conn.execute(
|
|
163
|
+
"SELECT * FROM sessions ORDER BY started_at DESC LIMIT ?", (limit,)
|
|
164
|
+
).fetchall()
|
|
165
|
+
return [dict(r) for r in rows]
|
|
166
|
+
|
|
167
|
+
# ─── messages ─────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
def append_message(self, session_id: str, msg: Message) -> int:
|
|
170
|
+
tool_calls_json = (
|
|
171
|
+
json.dumps(
|
|
172
|
+
[
|
|
173
|
+
{"id": tc.id, "name": tc.name, "arguments": tc.arguments}
|
|
174
|
+
for tc in msg.tool_calls
|
|
175
|
+
]
|
|
176
|
+
)
|
|
177
|
+
if msg.tool_calls
|
|
178
|
+
else None
|
|
179
|
+
)
|
|
180
|
+
with self._txn() as conn:
|
|
181
|
+
cur = conn.execute(
|
|
182
|
+
"INSERT INTO messages "
|
|
183
|
+
"(session_id, role, content, tool_call_id, tool_calls, name, reasoning, timestamp) "
|
|
184
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
185
|
+
(
|
|
186
|
+
session_id,
|
|
187
|
+
msg.role,
|
|
188
|
+
msg.content,
|
|
189
|
+
msg.tool_call_id,
|
|
190
|
+
tool_calls_json,
|
|
191
|
+
msg.name,
|
|
192
|
+
msg.reasoning,
|
|
193
|
+
time.time(),
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
conn.execute(
|
|
197
|
+
"UPDATE sessions SET message_count = message_count + 1 WHERE id = ?",
|
|
198
|
+
(session_id,),
|
|
199
|
+
)
|
|
200
|
+
return int(cur.lastrowid or 0)
|
|
201
|
+
|
|
202
|
+
def get_messages(self, session_id: str) -> list[Message]:
|
|
203
|
+
with self._connect() as conn:
|
|
204
|
+
rows = conn.execute(
|
|
205
|
+
"SELECT role, content, tool_call_id, tool_calls, name, reasoning "
|
|
206
|
+
"FROM messages WHERE session_id = ? ORDER BY id",
|
|
207
|
+
(session_id,),
|
|
208
|
+
).fetchall()
|
|
209
|
+
out: list[Message] = []
|
|
210
|
+
for r in rows:
|
|
211
|
+
tool_calls = None
|
|
212
|
+
if r["tool_calls"]:
|
|
213
|
+
raw = json.loads(r["tool_calls"])
|
|
214
|
+
tool_calls = [
|
|
215
|
+
ToolCall(id=tc["id"], name=tc["name"], arguments=tc["arguments"])
|
|
216
|
+
for tc in raw
|
|
217
|
+
]
|
|
218
|
+
out.append(
|
|
219
|
+
Message(
|
|
220
|
+
role=r["role"],
|
|
221
|
+
content=r["content"],
|
|
222
|
+
tool_call_id=r["tool_call_id"],
|
|
223
|
+
tool_calls=tool_calls,
|
|
224
|
+
name=r["name"],
|
|
225
|
+
reasoning=r["reasoning"],
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
return out
|
|
229
|
+
|
|
230
|
+
# ─── FTS5 search ──────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
def search(self, query: str, limit: int = 20) -> list[dict[str, Any]]:
|
|
233
|
+
"""Full-text search across all messages. Returns snippet + metadata."""
|
|
234
|
+
# Simple sanitization — let FTS5 reject truly malformed queries
|
|
235
|
+
safe_q = query.replace('"', '""').strip()
|
|
236
|
+
if not safe_q:
|
|
237
|
+
return []
|
|
238
|
+
with self._connect() as conn:
|
|
239
|
+
rows = conn.execute(
|
|
240
|
+
"SELECT m.session_id, m.role, m.timestamp, "
|
|
241
|
+
"snippet(messages_fts, 0, '[', ']', '…', 20) AS snippet "
|
|
242
|
+
"FROM messages_fts "
|
|
243
|
+
"JOIN messages m ON m.id = messages_fts.rowid "
|
|
244
|
+
"WHERE messages_fts MATCH ? "
|
|
245
|
+
"ORDER BY m.timestamp DESC LIMIT ?",
|
|
246
|
+
(safe_q, limit),
|
|
247
|
+
).fetchall()
|
|
248
|
+
return [dict(r) for r in rows]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
__all__ = ["SessionDB"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
StepOutcome — clean dataclass for one iteration of the agent loop.
|
|
3
|
+
|
|
4
|
+
Inspired by kimi-cli's KimiSoul.Step pattern. Makes the loop testable:
|
|
5
|
+
every iteration returns a StepOutcome the caller can assert against.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from plugin_sdk.core import Message, StopReason
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class StepOutcome:
|
|
17
|
+
"""Result of one iteration of the agent loop."""
|
|
18
|
+
|
|
19
|
+
stop_reason: StopReason
|
|
20
|
+
assistant_message: Message
|
|
21
|
+
tool_calls_made: int = 0 # number of tools invoked this iteration
|
|
22
|
+
input_tokens: int = 0
|
|
23
|
+
output_tokens: int = 0
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def should_continue(self) -> bool:
|
|
27
|
+
"""Whether the loop should keep iterating."""
|
|
28
|
+
return self.stop_reason == StopReason.TOOL_USE
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = ["StepOutcome"]
|