percolate-core 0.1.0__tar.gz
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.
- percolate_core-0.1.0/.gitignore +9 -0
- percolate_core-0.1.0/LICENSE +21 -0
- percolate_core-0.1.0/PKG-INFO +195 -0
- percolate_core-0.1.0/README.md +151 -0
- percolate_core-0.1.0/percolate_core/__init__.py +13 -0
- percolate_core-0.1.0/percolate_core/agentic/__init__.py +48 -0
- percolate_core-0.1.0/percolate_core/agentic/api.py +283 -0
- percolate_core-0.1.0/percolate_core/agentic/authoring.py +165 -0
- percolate_core-0.1.0/percolate_core/agentic/cli.py +425 -0
- percolate_core-0.1.0/percolate_core/agentic/contracts/__init__.py +87 -0
- percolate_core-0.1.0/percolate_core/agentic/contracts/agents.py +148 -0
- percolate_core-0.1.0/percolate_core/agentic/contracts/context.py +122 -0
- percolate_core-0.1.0/percolate_core/agentic/contracts/conversation.py +154 -0
- percolate_core-0.1.0/percolate_core/agentic/contracts/protocol.py +150 -0
- percolate_core-0.1.0/percolate_core/agentic/contracts/tools.py +102 -0
- percolate_core-0.1.0/percolate_core/agentic/db/__init__.py +32 -0
- percolate_core-0.1.0/percolate_core/agentic/db/client.py +174 -0
- percolate_core-0.1.0/percolate_core/agentic/db/repository.py +503 -0
- percolate_core-0.1.0/percolate_core/agentic/gateway.py +352 -0
- percolate_core-0.1.0/percolate_core/agentic/phases.py +110 -0
- percolate_core-0.1.0/percolate_core/agentic/runtime/__init__.py +41 -0
- percolate_core-0.1.0/percolate_core/agentic/runtime/chained_action.py +125 -0
- percolate_core-0.1.0/percolate_core/agentic/runtime/citations.py +75 -0
- percolate_core-0.1.0/percolate_core/agentic/runtime/delegation.py +137 -0
- percolate_core-0.1.0/percolate_core/agentic/runtime/engine.py +176 -0
- percolate_core-0.1.0/percolate_core/agentic/runtime/injection.py +58 -0
- percolate_core-0.1.0/percolate_core/agentic/runtime/persistence.py +314 -0
- percolate_core-0.1.0/percolate_core/agentic/runtime/protocol_adapter.py +114 -0
- percolate_core-0.1.0/percolate_core/agentic/sequencer.py +597 -0
- percolate_core-0.1.0/percolate_core/agentic/settings.py +82 -0
- percolate_core-0.1.0/percolate_core/agentic/testing.py +295 -0
- percolate_core-0.1.0/percolate_core/agentic/tools/__init__.py +24 -0
- percolate_core-0.1.0/percolate_core/agentic/tools/mcp.py +140 -0
- percolate_core-0.1.0/percolate_core/agentic/tools/openapi.py +294 -0
- percolate_core-0.1.0/percolate_core/agentic/tools/provider.py +89 -0
- percolate_core-0.1.0/percolate_core/agentic/tools/registry.py +142 -0
- percolate_core-0.1.0/percolate_core/agentic/tools/toolset.py +83 -0
- percolate_core-0.1.0/percolate_core/cli.py +64 -0
- percolate_core-0.1.0/percolate_core/content/__init__.py +5 -0
- percolate_core-0.1.0/percolate_core/content/server.py +115 -0
- percolate_core-0.1.0/percolate_core/content/storage.py +63 -0
- percolate_core-0.1.0/percolate_core/core/__init__.py +14 -0
- percolate_core-0.1.0/percolate_core/core/config.py +48 -0
- percolate_core-0.1.0/percolate_core/core/identity.py +128 -0
- percolate_core-0.1.0/percolate_core/worker/__init__.py +8 -0
- percolate_core-0.1.0/percolate_core/worker/http.py +100 -0
- percolate_core-0.1.0/percolate_core/worker/loop.py +111 -0
- percolate_core-0.1.0/percolate_core/worker/templates.py +84 -0
- percolate_core-0.1.0/pyproject.toml +52 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Percolate
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: percolate-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Percolate core: the worker, Content Server and Agent Runtime for a Postgres-native stack
|
|
5
|
+
Project-URL: Homepage, https://github.com/percolating-sirsh/percolate-core
|
|
6
|
+
Project-URL: Repository, https://github.com/percolating-sirsh/percolate-core
|
|
7
|
+
Project-URL: Specs, https://github.com/percolating-sirsh/p8-subsystems
|
|
8
|
+
Author: Percolate
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,pgvector,postgres,rag,workflow
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Database
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: asyncpg>=0.30
|
|
20
|
+
Requires-Dist: httpx>=0.28
|
|
21
|
+
Requires-Dist: typer>=0.15
|
|
22
|
+
Provides-Extra: agent
|
|
23
|
+
Requires-Dist: fastapi>=0.115; extra == 'agent'
|
|
24
|
+
Requires-Dist: mcp>=1.2; extra == 'agent'
|
|
25
|
+
Requires-Dist: pydantic-ai-slim[openai]>=0.4; extra == 'agent'
|
|
26
|
+
Requires-Dist: pydantic>=2.9; extra == 'agent'
|
|
27
|
+
Requires-Dist: pyyaml>=6.0; extra == 'agent'
|
|
28
|
+
Requires-Dist: rich>=13.9; extra == 'agent'
|
|
29
|
+
Requires-Dist: uvicorn>=0.34; extra == 'agent'
|
|
30
|
+
Provides-Extra: all
|
|
31
|
+
Requires-Dist: boto3>=1.35; extra == 'all'
|
|
32
|
+
Requires-Dist: fastapi>=0.115; extra == 'all'
|
|
33
|
+
Requires-Dist: mcp>=1.2; extra == 'all'
|
|
34
|
+
Requires-Dist: pydantic-ai-slim[openai]>=0.4; extra == 'all'
|
|
35
|
+
Requires-Dist: pydantic>=2.9; extra == 'all'
|
|
36
|
+
Requires-Dist: pyyaml>=6.0; extra == 'all'
|
|
37
|
+
Requires-Dist: rich>=13.9; extra == 'all'
|
|
38
|
+
Requires-Dist: uvicorn>=0.34; extra == 'all'
|
|
39
|
+
Provides-Extra: content
|
|
40
|
+
Requires-Dist: boto3>=1.35; extra == 'content'
|
|
41
|
+
Requires-Dist: fastapi>=0.115; extra == 'content'
|
|
42
|
+
Requires-Dist: uvicorn>=0.34; extra == 'content'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# p8
|
|
46
|
+
|
|
47
|
+
The processes that sit in front of the database. `../../INSTALL.md` covers the
|
|
48
|
+
database itself; nothing here is required to use it.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install p8 # the worker -- asyncpg, httpx, typer
|
|
52
|
+
pip install 'p8[content]' # + Content Server (boto3, fastapi)
|
|
53
|
+
pip install 'p8[agent]' # + Agent Runtime (pydantic-ai, mcp)
|
|
54
|
+
pip install 'p8[all]'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
p8 worker --queue http # claim and execute tasks
|
|
59
|
+
p8 content serve # uploads, scraping, http_call execution
|
|
60
|
+
p8 agent serve # agents, streaming, delegation
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Layout
|
|
66
|
+
|
|
67
|
+
| Module | Extra | What it is |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `p8.core` | — | connecting **as the caller**, configuration, credential resolution |
|
|
70
|
+
| `p8.worker` | — | the step loop and the `@handler` registry |
|
|
71
|
+
| `p8.content` | `content` | the Content Server |
|
|
72
|
+
| `p8.agentic` | `agent` | the Agent Runtime |
|
|
73
|
+
|
|
74
|
+
**One distribution rather than three.** Three packages means a version matrix
|
|
75
|
+
(`p8content 0.3` requiring `p8core >=0.2,<0.3`) resolved forever, for services
|
|
76
|
+
that release together and are written by the same people. Splitting later is
|
|
77
|
+
mechanical; merging two that have drifted is not.
|
|
78
|
+
|
|
79
|
+
**The base install stays small on purpose.** The common case is someone writing
|
|
80
|
+
their own worker, and they should not pull boto3 and pydantic-ai to do it. The
|
|
81
|
+
CLI imports each subpackage lazily, so `p8 worker` runs without either
|
|
82
|
+
installed and `p8 content serve` fails with the extra to install rather than an
|
|
83
|
+
`ImportError`.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## `p8.core` — the one that matters
|
|
88
|
+
|
|
89
|
+
It decides **whose** RLS applies, and it exists because that logic was
|
|
90
|
+
previously written three times on two different database drivers.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from p8.core import as_caller
|
|
94
|
+
|
|
95
|
+
async with as_caller(claims) as conn: # claims = the VERIFIED JWT payload
|
|
96
|
+
rows = await conn.fetch("select * from content.resources")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Every service connects as a low-privilege role and sets the caller's claims
|
|
100
|
+
**per transaction**, exactly as PostgREST does. A service that queried as
|
|
101
|
+
*itself* would bypass every policy in the collection — not by exploiting
|
|
102
|
+
anything, just by never presenting an identity for the policies to filter on.
|
|
103
|
+
|
|
104
|
+
Transaction-local (`set_config(..., true)`) is not a detail: an unregistered
|
|
105
|
+
GUC left at session scope survives into the next transaction on a pooled
|
|
106
|
+
connection, so the following request would inherit the previous caller's
|
|
107
|
+
identity.
|
|
108
|
+
|
|
109
|
+
`as_service()` exists for work with genuinely no user behind it — a scheduled
|
|
110
|
+
poll, a reconciliation sweep. Deliberately a separate function rather than
|
|
111
|
+
`as_caller(None)`, so "this query has no user" is something someone wrote down.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Writing your own worker
|
|
116
|
+
|
|
117
|
+
Most steps need no worker from you:
|
|
118
|
+
|
|
119
|
+
| kind | who runs it |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `sql` / `p8ql` | **nobody** — executes inside Postgres |
|
|
122
|
+
| `http_call` | the built-in handler |
|
|
123
|
+
| `timer` / `signal` / `decision` / `sub_workflow` | the engine |
|
|
124
|
+
| `work` | **you** |
|
|
125
|
+
|
|
126
|
+
When you do need one, it is this loop with a handler registered — not a
|
|
127
|
+
different program:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from p8.worker import handler, run
|
|
131
|
+
|
|
132
|
+
@handler("transcode")
|
|
133
|
+
async def transcode(spec, ctx):
|
|
134
|
+
return {"duration": await ffmpeg(ctx["run_input"]["file_key"])}
|
|
135
|
+
|
|
136
|
+
run(queue="media")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`ctx` comes from `workflow.get_task_context()`: `run_input`, the accumulated
|
|
140
|
+
`context` (so a later step reads an earlier step's output), `task_input`,
|
|
141
|
+
`step_key`, and `trace_id`/`span_id`.
|
|
142
|
+
|
|
143
|
+
**The worker holds no table grants.** Every interaction is a `SECURITY DEFINER`
|
|
144
|
+
function call — `claim_task`, `get_task_context`, `complete_task`, `fail_task`
|
|
145
|
+
— which is why "bring your own worker" is safe to offer: a compromised worker
|
|
146
|
+
can claim and complete tasks, and nothing else.
|
|
147
|
+
|
|
148
|
+
**Raise `TerminalError` for what will not get better.** A bad argument, a
|
|
149
|
+
missing credential, a 404. Anything else is retried with backoff. The worker is
|
|
150
|
+
the only thing that knows what a failure means, so it decides and the engine
|
|
151
|
+
honours the verdict.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Configuration
|
|
156
|
+
|
|
157
|
+
Environment only. Credentials by **reference**, never by value:
|
|
158
|
+
`credential_ref: "LLM_API_KEY"` on a task names a variable this process
|
|
159
|
+
resolves, so `workflow.tasks` stays inspectable and replayable.
|
|
160
|
+
|
|
161
|
+
| | Used by |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `P8_DSN` | all |
|
|
164
|
+
| `P8_JWT_SECRET` | services verifying bearer tokens (same secret PostgREST uses) |
|
|
165
|
+
| `P8_QUEUE`, `P8_WORKER_ID`, `P8_POLL_SECONDS` | worker |
|
|
166
|
+
| `P8_S3_ENDPOINT`, `P8_S3_KEY`, `P8_S3_SECRET`, `P8_BUCKET` | content |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Deployment
|
|
171
|
+
|
|
172
|
+
One image, three entrypoints — they share `p8.core`, so three images would be
|
|
173
|
+
three builds of the same base and three tags to keep in step.
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
content: { image: p8/runtime:0.1, command: ["p8","content","serve"] }
|
|
177
|
+
agent: { image: p8/runtime:0.1, command: ["p8","agent","serve"] }
|
|
178
|
+
worker: { image: p8/runtime:0.1, command: ["p8","worker","--queue","http"] }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
See [`../../PACKAGING.md`](../../PACKAGING.md) for the reasoning and the
|
|
182
|
+
remaining work.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Status
|
|
187
|
+
|
|
188
|
+
- `p8.core`, `p8.worker`, `p8.content` — built, and exercised against a live
|
|
189
|
+
PG19 instance with MinIO.
|
|
190
|
+
- `p8.agentic` — moved from `experiments/agentic-runtime` unchanged (it uses
|
|
191
|
+
relative imports throughout, so the move needed no edits). Its own tests have
|
|
192
|
+
not been re-run under the new namespace.
|
|
193
|
+
- No Dockerfile yet. No service-surface entries in `../../surface.sql`, which
|
|
194
|
+
by `meta/skills/spec-driven-development` §7 should exist **before** the
|
|
195
|
+
endpoints they describe.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# p8
|
|
2
|
+
|
|
3
|
+
The processes that sit in front of the database. `../../INSTALL.md` covers the
|
|
4
|
+
database itself; nothing here is required to use it.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install p8 # the worker -- asyncpg, httpx, typer
|
|
8
|
+
pip install 'p8[content]' # + Content Server (boto3, fastapi)
|
|
9
|
+
pip install 'p8[agent]' # + Agent Runtime (pydantic-ai, mcp)
|
|
10
|
+
pip install 'p8[all]'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
p8 worker --queue http # claim and execute tasks
|
|
15
|
+
p8 content serve # uploads, scraping, http_call execution
|
|
16
|
+
p8 agent serve # agents, streaming, delegation
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Layout
|
|
22
|
+
|
|
23
|
+
| Module | Extra | What it is |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `p8.core` | — | connecting **as the caller**, configuration, credential resolution |
|
|
26
|
+
| `p8.worker` | — | the step loop and the `@handler` registry |
|
|
27
|
+
| `p8.content` | `content` | the Content Server |
|
|
28
|
+
| `p8.agentic` | `agent` | the Agent Runtime |
|
|
29
|
+
|
|
30
|
+
**One distribution rather than three.** Three packages means a version matrix
|
|
31
|
+
(`p8content 0.3` requiring `p8core >=0.2,<0.3`) resolved forever, for services
|
|
32
|
+
that release together and are written by the same people. Splitting later is
|
|
33
|
+
mechanical; merging two that have drifted is not.
|
|
34
|
+
|
|
35
|
+
**The base install stays small on purpose.** The common case is someone writing
|
|
36
|
+
their own worker, and they should not pull boto3 and pydantic-ai to do it. The
|
|
37
|
+
CLI imports each subpackage lazily, so `p8 worker` runs without either
|
|
38
|
+
installed and `p8 content serve` fails with the extra to install rather than an
|
|
39
|
+
`ImportError`.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## `p8.core` — the one that matters
|
|
44
|
+
|
|
45
|
+
It decides **whose** RLS applies, and it exists because that logic was
|
|
46
|
+
previously written three times on two different database drivers.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from p8.core import as_caller
|
|
50
|
+
|
|
51
|
+
async with as_caller(claims) as conn: # claims = the VERIFIED JWT payload
|
|
52
|
+
rows = await conn.fetch("select * from content.resources")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Every service connects as a low-privilege role and sets the caller's claims
|
|
56
|
+
**per transaction**, exactly as PostgREST does. A service that queried as
|
|
57
|
+
*itself* would bypass every policy in the collection — not by exploiting
|
|
58
|
+
anything, just by never presenting an identity for the policies to filter on.
|
|
59
|
+
|
|
60
|
+
Transaction-local (`set_config(..., true)`) is not a detail: an unregistered
|
|
61
|
+
GUC left at session scope survives into the next transaction on a pooled
|
|
62
|
+
connection, so the following request would inherit the previous caller's
|
|
63
|
+
identity.
|
|
64
|
+
|
|
65
|
+
`as_service()` exists for work with genuinely no user behind it — a scheduled
|
|
66
|
+
poll, a reconciliation sweep. Deliberately a separate function rather than
|
|
67
|
+
`as_caller(None)`, so "this query has no user" is something someone wrote down.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Writing your own worker
|
|
72
|
+
|
|
73
|
+
Most steps need no worker from you:
|
|
74
|
+
|
|
75
|
+
| kind | who runs it |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `sql` / `p8ql` | **nobody** — executes inside Postgres |
|
|
78
|
+
| `http_call` | the built-in handler |
|
|
79
|
+
| `timer` / `signal` / `decision` / `sub_workflow` | the engine |
|
|
80
|
+
| `work` | **you** |
|
|
81
|
+
|
|
82
|
+
When you do need one, it is this loop with a handler registered — not a
|
|
83
|
+
different program:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from p8.worker import handler, run
|
|
87
|
+
|
|
88
|
+
@handler("transcode")
|
|
89
|
+
async def transcode(spec, ctx):
|
|
90
|
+
return {"duration": await ffmpeg(ctx["run_input"]["file_key"])}
|
|
91
|
+
|
|
92
|
+
run(queue="media")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`ctx` comes from `workflow.get_task_context()`: `run_input`, the accumulated
|
|
96
|
+
`context` (so a later step reads an earlier step's output), `task_input`,
|
|
97
|
+
`step_key`, and `trace_id`/`span_id`.
|
|
98
|
+
|
|
99
|
+
**The worker holds no table grants.** Every interaction is a `SECURITY DEFINER`
|
|
100
|
+
function call — `claim_task`, `get_task_context`, `complete_task`, `fail_task`
|
|
101
|
+
— which is why "bring your own worker" is safe to offer: a compromised worker
|
|
102
|
+
can claim and complete tasks, and nothing else.
|
|
103
|
+
|
|
104
|
+
**Raise `TerminalError` for what will not get better.** A bad argument, a
|
|
105
|
+
missing credential, a 404. Anything else is retried with backoff. The worker is
|
|
106
|
+
the only thing that knows what a failure means, so it decides and the engine
|
|
107
|
+
honours the verdict.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Configuration
|
|
112
|
+
|
|
113
|
+
Environment only. Credentials by **reference**, never by value:
|
|
114
|
+
`credential_ref: "LLM_API_KEY"` on a task names a variable this process
|
|
115
|
+
resolves, so `workflow.tasks` stays inspectable and replayable.
|
|
116
|
+
|
|
117
|
+
| | Used by |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `P8_DSN` | all |
|
|
120
|
+
| `P8_JWT_SECRET` | services verifying bearer tokens (same secret PostgREST uses) |
|
|
121
|
+
| `P8_QUEUE`, `P8_WORKER_ID`, `P8_POLL_SECONDS` | worker |
|
|
122
|
+
| `P8_S3_ENDPOINT`, `P8_S3_KEY`, `P8_S3_SECRET`, `P8_BUCKET` | content |
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Deployment
|
|
127
|
+
|
|
128
|
+
One image, three entrypoints — they share `p8.core`, so three images would be
|
|
129
|
+
three builds of the same base and three tags to keep in step.
|
|
130
|
+
|
|
131
|
+
```yaml
|
|
132
|
+
content: { image: p8/runtime:0.1, command: ["p8","content","serve"] }
|
|
133
|
+
agent: { image: p8/runtime:0.1, command: ["p8","agent","serve"] }
|
|
134
|
+
worker: { image: p8/runtime:0.1, command: ["p8","worker","--queue","http"] }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
See [`../../PACKAGING.md`](../../PACKAGING.md) for the reasoning and the
|
|
138
|
+
remaining work.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Status
|
|
143
|
+
|
|
144
|
+
- `p8.core`, `p8.worker`, `p8.content` — built, and exercised against a live
|
|
145
|
+
PG19 instance with MinIO.
|
|
146
|
+
- `p8.agentic` — moved from `experiments/agentic-runtime` unchanged (it uses
|
|
147
|
+
relative imports throughout, so the move needed no edits). Its own tests have
|
|
148
|
+
not been re-run under the new namespace.
|
|
149
|
+
- No Dockerfile yet. No service-surface entries in `../../surface.sql`, which
|
|
150
|
+
by `meta/skills/spec-driven-development` §7 should exist **before** the
|
|
151
|
+
endpoints they describe.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""percolate-core — Postgres-native subsystems.
|
|
2
|
+
|
|
3
|
+
The database is the system; these are the processes that sit in front of it.
|
|
4
|
+
Everything here connects through the same RLS-scoped roles and holds no table
|
|
5
|
+
grants: every interaction is a SECURITY DEFINER function call.
|
|
6
|
+
|
|
7
|
+
percolate_core.core connecting as the caller, configuration (always)
|
|
8
|
+
percolate_core.worker the step loop and @handler registry (always)
|
|
9
|
+
percolate_core.content the Content Server [content]
|
|
10
|
+
percolate_core.agentic the Agent Runtime [agent]
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""p8agentic — the agent runtime speced in ``specs/agentic``.
|
|
2
|
+
|
|
3
|
+
Agents are rows in Postgres, tools are MCP/OpenAPI services reached over the
|
|
4
|
+
network, a turn's rows are written through RLS as an ordinary ``authenticated``
|
|
5
|
+
caller, AG-UI events stream out over ``NOTIFY``, and delegation is an MCP call
|
|
6
|
+
into this runtime's own agent gateway. ``dev/verify.py`` asserts all of that
|
|
7
|
+
against a live LLM.
|
|
8
|
+
|
|
9
|
+
Layout, mapped onto ``specs/agentic/brief.md``:
|
|
10
|
+
|
|
11
|
+
- ``contracts`` — the ontology as pydantic types (brief §7, ontology.md), plus
|
|
12
|
+
the two wire protocols (brief §5.1 item 2).
|
|
13
|
+
- ``db`` — a small asyncpg client and the ``Repository`` protocol every
|
|
14
|
+
read/write in this runtime goes through (brief §5: "through the same functions
|
|
15
|
+
and RLS any other client would go through, no privileged backdoor").
|
|
16
|
+
- ``tools`` — tool-server resolution and invocation: MCP is the canonical
|
|
17
|
+
shape, OpenAPI is an adapter onto it (brief §5.2). Zero built-in tools.
|
|
18
|
+
- ``runtime`` — the four components of brief §5.1, one module each, plus the
|
|
19
|
+
chained action (§4) and citation (§5.3) mechanisms.
|
|
20
|
+
- ``sequencer`` — the ordered steps of one agent turn, and the mapping from
|
|
21
|
+
``specs/agentic/sequence.md``'s phase gates onto demos that prove them.
|
|
22
|
+
- ``fakes`` — pydantic-ai fake models and an in-memory repository, so the
|
|
23
|
+
whole path above runs with no Postgres and no LLM credentials.
|
|
24
|
+
- ``cli`` — the operator surface. FastAPI wraps this later (brief §4:
|
|
25
|
+
streaming is the one piece that structurally can't be plain PostgREST).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
__version__ = "0.0.1"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_fingerprint() -> str:
|
|
32
|
+
"""A short hash of this package's source on disk.
|
|
33
|
+
|
|
34
|
+
Exposed by every long-running surface (`/_build`) so a caller can tell
|
|
35
|
+
whether the process it is talking to is running the code in the working
|
|
36
|
+
tree. Three separate debugging sessions in this module's history began with
|
|
37
|
+
a confusing failure whose cause was a server started before the fix — a
|
|
38
|
+
stale process fails in the shape of a bug, and that is worth fifteen lines
|
|
39
|
+
to make impossible.
|
|
40
|
+
"""
|
|
41
|
+
import hashlib
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
digest = hashlib.sha256()
|
|
45
|
+
for path in sorted(Path(__file__).parent.rglob("*.py")):
|
|
46
|
+
digest.update(path.name.encode())
|
|
47
|
+
digest.update(str(path.stat().st_mtime_ns).encode())
|
|
48
|
+
return digest.hexdigest()[:12]
|