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.
Files changed (51) hide show
  1. opencomputer/__init__.py +3 -0
  2. opencomputer/agent/__init__.py +1 -0
  3. opencomputer/agent/compaction.py +245 -0
  4. opencomputer/agent/config.py +108 -0
  5. opencomputer/agent/config_store.py +210 -0
  6. opencomputer/agent/injection.py +60 -0
  7. opencomputer/agent/loop.py +326 -0
  8. opencomputer/agent/memory.py +132 -0
  9. opencomputer/agent/prompt_builder.py +66 -0
  10. opencomputer/agent/prompts/base.j2 +23 -0
  11. opencomputer/agent/state.py +251 -0
  12. opencomputer/agent/step.py +31 -0
  13. opencomputer/cli.py +483 -0
  14. opencomputer/doctor.py +216 -0
  15. opencomputer/gateway/__init__.py +1 -0
  16. opencomputer/gateway/dispatch.py +89 -0
  17. opencomputer/gateway/protocol.py +84 -0
  18. opencomputer/gateway/server.py +77 -0
  19. opencomputer/gateway/wire_server.py +256 -0
  20. opencomputer/hooks/__init__.py +1 -0
  21. opencomputer/hooks/engine.py +79 -0
  22. opencomputer/hooks/runner.py +42 -0
  23. opencomputer/mcp/__init__.py +1 -0
  24. opencomputer/mcp/client.py +208 -0
  25. opencomputer/plugins/__init__.py +1 -0
  26. opencomputer/plugins/discovery.py +107 -0
  27. opencomputer/plugins/loader.py +155 -0
  28. opencomputer/plugins/registry.py +56 -0
  29. opencomputer/setup_wizard.py +235 -0
  30. opencomputer/skills/debug-python-import-error/SKILL.md +58 -0
  31. opencomputer/tools/__init__.py +1 -0
  32. opencomputer/tools/bash.py +78 -0
  33. opencomputer/tools/delegate.py +98 -0
  34. opencomputer/tools/glob.py +70 -0
  35. opencomputer/tools/grep.py +117 -0
  36. opencomputer/tools/read.py +81 -0
  37. opencomputer/tools/registry.py +69 -0
  38. opencomputer/tools/skill_manage.py +265 -0
  39. opencomputer/tools/write.py +58 -0
  40. opencomputer-0.1.0.dist-info/METADATA +190 -0
  41. opencomputer-0.1.0.dist-info/RECORD +51 -0
  42. opencomputer-0.1.0.dist-info/WHEEL +4 -0
  43. opencomputer-0.1.0.dist-info/entry_points.txt +3 -0
  44. plugin_sdk/__init__.py +66 -0
  45. plugin_sdk/channel_contract.py +74 -0
  46. plugin_sdk/core.py +129 -0
  47. plugin_sdk/hooks.py +80 -0
  48. plugin_sdk/injection.py +60 -0
  49. plugin_sdk/provider_contract.py +95 -0
  50. plugin_sdk/runtime_context.py +39 -0
  51. 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"]