rungent 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.
- rungent-0.1.0/.gitignore +73 -0
- rungent-0.1.0/LICENSE +18 -0
- rungent-0.1.0/PKG-INFO +111 -0
- rungent-0.1.0/README.md +74 -0
- rungent-0.1.0/RUNGENT.md +99 -0
- rungent-0.1.0/pyproject.toml +80 -0
- rungent-0.1.0/src/rungent/RUNGENT.md +99 -0
- rungent-0.1.0/src/rungent/__init__.py +30 -0
- rungent-0.1.0/src/rungent/acs.py +42 -0
- rungent-0.1.0/src/rungent/agent.py +96 -0
- rungent-0.1.0/src/rungent/cli.py +98 -0
- rungent-0.1.0/src/rungent/fastapi.py +224 -0
- rungent-0.1.0/src/rungent/llm.py +280 -0
- rungent-0.1.0/src/rungent/py.typed +1 -0
- rungent-0.1.0/src/rungent/runtime.py +1834 -0
- rungent-0.1.0/src/rungent/sqlalchemy.py +329 -0
- rungent-0.1.0/src/rungent/state.py +217 -0
- rungent-0.1.0/src/rungent/store.py +195 -0
- rungent-0.1.0/src/rungent/testing.py +266 -0
- rungent-0.1.0/src/rungent/tools.py +316 -0
rungent-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
.venv/
|
|
3
|
+
venv/
|
|
4
|
+
.uv-cache/
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
*$py.class
|
|
8
|
+
*.so
|
|
9
|
+
.Python
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.pyright/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
.coverage.*
|
|
16
|
+
htmlcov/
|
|
17
|
+
.tox/
|
|
18
|
+
.nox/
|
|
19
|
+
dist/
|
|
20
|
+
build/
|
|
21
|
+
wheels/
|
|
22
|
+
*.egg-info/
|
|
23
|
+
*.egg
|
|
24
|
+
MANIFEST
|
|
25
|
+
|
|
26
|
+
# Node / pnpm
|
|
27
|
+
node_modules/
|
|
28
|
+
.pnpm-store/
|
|
29
|
+
.pnpm-debug.log*
|
|
30
|
+
npm-debug.log*
|
|
31
|
+
yarn-debug.log*
|
|
32
|
+
yarn-error.log*
|
|
33
|
+
*.tsbuildinfo
|
|
34
|
+
|
|
35
|
+
# Docs / Next / Fumadocs
|
|
36
|
+
.next/
|
|
37
|
+
.turbo/
|
|
38
|
+
.source/
|
|
39
|
+
out/
|
|
40
|
+
apps/docs/.next/
|
|
41
|
+
apps/docs/.source/
|
|
42
|
+
|
|
43
|
+
# Test & coverage (JS)
|
|
44
|
+
coverage/
|
|
45
|
+
*.lcov
|
|
46
|
+
|
|
47
|
+
# Env & secrets
|
|
48
|
+
.env
|
|
49
|
+
.env.*
|
|
50
|
+
!.env.example
|
|
51
|
+
!.env.*.example
|
|
52
|
+
*.pem
|
|
53
|
+
*.key
|
|
54
|
+
credentials.json
|
|
55
|
+
secrets.json
|
|
56
|
+
|
|
57
|
+
# OS / editors
|
|
58
|
+
.DS_Store
|
|
59
|
+
Thumbs.db
|
|
60
|
+
.idea/
|
|
61
|
+
.vscode/*
|
|
62
|
+
!.vscode/extensions.json
|
|
63
|
+
!.vscode/settings.json
|
|
64
|
+
*.swp
|
|
65
|
+
*.swo
|
|
66
|
+
*~
|
|
67
|
+
|
|
68
|
+
# Local / misc
|
|
69
|
+
*.log
|
|
70
|
+
tmp/
|
|
71
|
+
temp/
|
|
72
|
+
.cache/
|
|
73
|
+
.direnv/
|
rungent-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Rungent Contributors
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
18
|
+
|
rungent-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rungent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small, typed agent runtime for Python applications
|
|
5
|
+
Project-URL: Homepage, https://github.com/rungent/rungent
|
|
6
|
+
Project-URL: Documentation, https://github.com/rungent/rungent/tree/main/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/rungent/rungent
|
|
8
|
+
Project-URL: Issues, https://github.com/rungent/rungent/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/rungent/rungent/releases
|
|
10
|
+
Author: Rungent Contributors
|
|
11
|
+
License-Expression: Apache-2.0
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: agent,fastapi,llm,sse,tool-calling
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: httpx>=0.28.0
|
|
23
|
+
Requires-Dist: pydantic>=2.10.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: aiosqlite>=0.20.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: fastapi>=0.115.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pyright>=1.1.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.8.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.36; extra == 'dev'
|
|
32
|
+
Provides-Extra: fastapi
|
|
33
|
+
Requires-Dist: fastapi>=0.115.0; extra == 'fastapi'
|
|
34
|
+
Provides-Extra: sqlalchemy
|
|
35
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.36; extra == 'sqlalchemy'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# Rungent
|
|
39
|
+
|
|
40
|
+
Rungent is a small, typed agent runtime that embeds into an existing Python application. It
|
|
41
|
+
standardizes tool calling, the harness loop, durable sessions and runs, user interactions,
|
|
42
|
+
deterministic approval gates, and an SSE event protocol.
|
|
43
|
+
|
|
44
|
+
Its headless activity protocol exposes model steps, safe public progress, tools, and interactions as
|
|
45
|
+
persisted events. It never exposes raw provider reasoning or hidden chain of thought.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from typing import Annotated
|
|
49
|
+
|
|
50
|
+
from rungent import Agent, RunActivity, Runtime, ToolContext, ToolResult, tool
|
|
51
|
+
from rungent.llm import OpenAICompatibleModel
|
|
52
|
+
from rungent.store import MemoryStore
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@tool(effect="write", approval="never")
|
|
56
|
+
async def move_place(
|
|
57
|
+
ctx: ToolContext,
|
|
58
|
+
name: Annotated[str, "Exact place name"],
|
|
59
|
+
day_number: Annotated[int | None, "Target day; null means unplanned"],
|
|
60
|
+
) -> ToolResult:
|
|
61
|
+
await ctx.deps["trips"].move(ctx.resource["trip_id"], name, day_number)
|
|
62
|
+
return ToolResult(message=f"Moved {name}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
agent = Agent(
|
|
66
|
+
name="trip_planner",
|
|
67
|
+
instructions="Help the user update their trip. Use tools for every change.",
|
|
68
|
+
tools=[move_place],
|
|
69
|
+
run_activity=lambda _ctx, _content: RunActivity(
|
|
70
|
+
message="Received the request",
|
|
71
|
+
waiting_message="Still preparing; no changes have been made",
|
|
72
|
+
long_wait_message="The request is detailed; still preparing",
|
|
73
|
+
continuation_message="Preparing the result",
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
runtime = Runtime(
|
|
78
|
+
agents=[agent],
|
|
79
|
+
model=OpenAICompatibleModel.from_env(),
|
|
80
|
+
store=MemoryStore(),
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The canonical documentation lives in `docs/` and is rendered by the Fumadocs app in `apps/docs`.
|
|
85
|
+
AI coding agents should start with [`RUNGENT.md`](RUNGENT.md); the running site also exposes
|
|
86
|
+
`/llms.txt`, `/llms-full.txt`, and per-page raw Markdown under `/markdown/*`.
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
uv sync --all-extras
|
|
92
|
+
uv run pytest -W error
|
|
93
|
+
uv run ruff check src tests
|
|
94
|
+
|
|
95
|
+
pnpm install
|
|
96
|
+
pnpm dev:docs
|
|
97
|
+
pnpm test
|
|
98
|
+
pnpm build
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Release
|
|
102
|
+
|
|
103
|
+
`rungent` (PyPI) and `@rungent/sdk` (npm) share one semver. Publishing is done by GitHub Actions
|
|
104
|
+
on `v*` tags — do not `uv publish` / `npm publish` from your laptop.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
./scripts/release.sh 0.2.0
|
|
108
|
+
git push origin main --tags
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
One-time Trusted Publishing setup: see [`RELEASING.md`](RELEASING.md).
|
rungent-0.1.0/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Rungent
|
|
2
|
+
|
|
3
|
+
Rungent is a small, typed agent runtime that embeds into an existing Python application. It
|
|
4
|
+
standardizes tool calling, the harness loop, durable sessions and runs, user interactions,
|
|
5
|
+
deterministic approval gates, and an SSE event protocol.
|
|
6
|
+
|
|
7
|
+
Its headless activity protocol exposes model steps, safe public progress, tools, and interactions as
|
|
8
|
+
persisted events. It never exposes raw provider reasoning or hidden chain of thought.
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from typing import Annotated
|
|
12
|
+
|
|
13
|
+
from rungent import Agent, RunActivity, Runtime, ToolContext, ToolResult, tool
|
|
14
|
+
from rungent.llm import OpenAICompatibleModel
|
|
15
|
+
from rungent.store import MemoryStore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@tool(effect="write", approval="never")
|
|
19
|
+
async def move_place(
|
|
20
|
+
ctx: ToolContext,
|
|
21
|
+
name: Annotated[str, "Exact place name"],
|
|
22
|
+
day_number: Annotated[int | None, "Target day; null means unplanned"],
|
|
23
|
+
) -> ToolResult:
|
|
24
|
+
await ctx.deps["trips"].move(ctx.resource["trip_id"], name, day_number)
|
|
25
|
+
return ToolResult(message=f"Moved {name}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
agent = Agent(
|
|
29
|
+
name="trip_planner",
|
|
30
|
+
instructions="Help the user update their trip. Use tools for every change.",
|
|
31
|
+
tools=[move_place],
|
|
32
|
+
run_activity=lambda _ctx, _content: RunActivity(
|
|
33
|
+
message="Received the request",
|
|
34
|
+
waiting_message="Still preparing; no changes have been made",
|
|
35
|
+
long_wait_message="The request is detailed; still preparing",
|
|
36
|
+
continuation_message="Preparing the result",
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
runtime = Runtime(
|
|
41
|
+
agents=[agent],
|
|
42
|
+
model=OpenAICompatibleModel.from_env(),
|
|
43
|
+
store=MemoryStore(),
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The canonical documentation lives in `docs/` and is rendered by the Fumadocs app in `apps/docs`.
|
|
48
|
+
AI coding agents should start with [`RUNGENT.md`](RUNGENT.md); the running site also exposes
|
|
49
|
+
`/llms.txt`, `/llms-full.txt`, and per-page raw Markdown under `/markdown/*`.
|
|
50
|
+
|
|
51
|
+
## Development
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv sync --all-extras
|
|
55
|
+
uv run pytest -W error
|
|
56
|
+
uv run ruff check src tests
|
|
57
|
+
|
|
58
|
+
pnpm install
|
|
59
|
+
pnpm dev:docs
|
|
60
|
+
pnpm test
|
|
61
|
+
pnpm build
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Release
|
|
65
|
+
|
|
66
|
+
`rungent` (PyPI) and `@rungent/sdk` (npm) share one semver. Publishing is done by GitHub Actions
|
|
67
|
+
on `v*` tags — do not `uv publish` / `npm publish` from your laptop.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
./scripts/release.sh 0.2.0
|
|
71
|
+
git push origin main --tags
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
One-time Trusted Publishing setup: see [`RELEASING.md`](RELEASING.md).
|
rungent-0.1.0/RUNGENT.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Rungent integration guide for coding agents
|
|
2
|
+
|
|
3
|
+
This is the canonical short context for an AI coding agent integrating Rungent.
|
|
4
|
+
|
|
5
|
+
1. Define one `Agent` for one product assistant. Do not introduce skills or routing unless the
|
|
6
|
+
product genuinely has multiple independent capability domains.
|
|
7
|
+
2. Define business operations with `@tool`. The first parameter is `ToolContext`; all other typed
|
|
8
|
+
parameters become the model-facing JSON Schema.
|
|
9
|
+
3. Declare `effect` and `approval` explicitly. Never infer safety from a function name.
|
|
10
|
+
Every `approval="always"` tool must provide a concrete `confirmation` string or callable that
|
|
11
|
+
identifies the affected object and impact; never ask users to approve only a generic tool title.
|
|
12
|
+
Equivalent successful non-read calls are deduplicated per Run; prefer a batch/count argument and
|
|
13
|
+
use `deduplicate=False` only when identical repeated writes are intentional.
|
|
14
|
+
4. Put durable service factories in `ToolContext.deps`, stable resource identifiers in
|
|
15
|
+
`ToolContext.resource`, and read the accepted input from `ToolContext.current_input`. After
|
|
16
|
+
`request_input` resumes, `ToolContext.interaction_response`
|
|
17
|
+
contains the runtime-validated interaction ID, kind, prompt, and value for that response only;
|
|
18
|
+
declare `requires_interaction_response=True` when a host operation must reject model-invented
|
|
19
|
+
answers. Rungent exposes that tool only after a real answer and consumes the answer once.
|
|
20
|
+
5. Use `Runtime` for session/run lifecycle. Do not write a second tool loop. The default bounded
|
|
21
|
+
loop allows 16 model steps. Retryable timeouts, transport failures, HTTP 408/429, and server
|
|
22
|
+
errors retry inside the same step up to three times; empty completions consume a new bounded
|
|
23
|
+
step and never become empty user messages.
|
|
24
|
+
6. Use the FastAPI router from `rungent.fastapi` when exposing the assistant over HTTP. Run
|
|
25
|
+
creation returns `202` with a Run ID; subscribe separately so disconnecting the event stream
|
|
26
|
+
never cancels execution. Pass `Idempotency-Key` when a client may retry creation.
|
|
27
|
+
7. Consume the SSE stream with `@rungent/sdk`. Do not parse provider-specific model chunks in the
|
|
28
|
+
frontend.
|
|
29
|
+
8. Use `request_input` for missing information. Group two to eight independent, already-known
|
|
30
|
+
questions in one `form`; ask sequentially only when a later question depends on an earlier
|
|
31
|
+
answer. A `choice` may declare `multiple` and `allow_custom`; choice responses use
|
|
32
|
+
`{selected: string[], custom?: string}`. Form responses use `{answers: {question_id: value}}`.
|
|
33
|
+
All responses are validated by the runtime. Destructive approvals are generated by Rungent,
|
|
34
|
+
not by prompting the model to ask “confirm?”.
|
|
35
|
+
9. Rungent exposes public work activity, not raw chain of thought. The model may call the built-in
|
|
36
|
+
`report_progress` alongside business tool calls during multi-step work, never as a dedicated
|
|
37
|
+
model step; model steps, tools, interactions, and progress are
|
|
38
|
+
persisted as ordered public events and projected by the SDK as `state.activities`.
|
|
39
|
+
Terminal Run events settle every unfinished activity; transport failures should call
|
|
40
|
+
`failRungentState` so a stale spinner can never survive an interrupted request.
|
|
41
|
+
Long-running host tools may call `await ctx.report_progress(message, public=...)`; Rungent
|
|
42
|
+
persists and streams each update while the tool is still executing. Updates from one tool call
|
|
43
|
+
share a stable activity ID, remain `running` while work continues, and replace one projected SDK
|
|
44
|
+
row; the ordered event log still retains every update for replay and audit.
|
|
45
|
+
10. Add deterministic baseline cases before enabling a real model baseline. Export a
|
|
46
|
+
`BaselineSuite` and run `rungent baseline module:suite` in the host project.
|
|
47
|
+
Tool and interaction expectations are exact by default; use their `contains` match modes only
|
|
48
|
+
for live-model cases where safe retries or optional branches are acceptable, and always assert
|
|
49
|
+
final state.
|
|
50
|
+
11. Read `docs/ai-reference.mdx` for the full public API and `examples/roasea/` for the reference
|
|
51
|
+
application pattern.
|
|
52
|
+
12. Model user-sized business intents as atomic host tools. Stage large imports in the host
|
|
53
|
+
application, return their diff through `ToolResult.public`, clarify with `request_input`, and
|
|
54
|
+
commit the reviewed draft with one revision-checked atomic tool and one approval. Likewise, a
|
|
55
|
+
reversible “change every leg” request should be one host batch tool, not many single-item calls;
|
|
56
|
+
increasing the model-step limit cannot repair an undersized business interface.
|
|
57
|
+
13. Model-facing tool results carry `tool_status: success|error`. Only explicit success envelopes
|
|
58
|
+
participate in deduplication; exceptions, timeouts, and `ToolResult(data={"ok": false})` remain
|
|
59
|
+
retryable. Call `client.cancel(runId)` before aborting the stream's `AbortSignal`; the cancel
|
|
60
|
+
endpoint terminates the active drive and persists `run.cancelled`. Treat `cancelled` as a
|
|
61
|
+
distinct terminal state.
|
|
62
|
+
14. Add trusted provider-specific top-level request fields through
|
|
63
|
+
`OpenAICompatibleModel(extra_body=...)` or the `LLM_EXTRA_BODY` JSON object. Core request fields
|
|
64
|
+
cannot be overridden. For latency-sensitive Qwen runs, for example, use
|
|
65
|
+
`{"enable_thinking":false}`.
|
|
66
|
+
15. Malformed or truncated tool-argument JSON is retried inside the same model step. Rungent resets
|
|
67
|
+
partial text and asks the provider for compact valid JSON before any tool can execute.
|
|
68
|
+
16. A host tool may return `ToolResult.interaction=InteractionRequest(...)` with a frozen
|
|
69
|
+
continuation tool. Rungent persists and displays that question immediately, then executes the
|
|
70
|
+
typed `requires_interaction_response` continuation directly after a validated answer without
|
|
71
|
+
spending another model step. Use `allow_skip` only when skipping is safe for the workflow.
|
|
72
|
+
17. Latency-sensitive agents should declare `run_activity`, returning public `RunActivity`
|
|
73
|
+
messages authored by the host application. Rungent emits the first activity immediately and,
|
|
74
|
+
while a provider stream is silent, updates the same persisted activity after three seconds and
|
|
75
|
+
every five seconds thereafter. Progress activities remain `running` until real model work
|
|
76
|
+
advances or the Run terminates; never use this channel for hidden reasoning. Products that
|
|
77
|
+
require finite deadlines may set per-attempt `model_step_timeout_seconds` and overall
|
|
78
|
+
`model_step_total_timeout_seconds`; retries receive a fresh per-attempt budget.
|
|
79
|
+
18. Bounded-loop exhaustion keeps the numeric model-step limit in operator state and emits a safe
|
|
80
|
+
`run.failed` envelope with `code=model_step_limit_exceeded` and `retryable=true`. Product UIs
|
|
81
|
+
should localize recovery guidance from the code instead of displaying internals.
|
|
82
|
+
19. `request_input` rejects numbered alternatives embedded in a `text` prompt; use `choice` so the
|
|
83
|
+
client can render typed options and a custom-answer row. Tool activities expose stable failure
|
|
84
|
+
codes, allowing product UIs to suppress or localize technical validation noise.
|
|
85
|
+
19. Assistant message content is an opaque string to Rungent and `@rungent/sdk`. The host Agent owns
|
|
86
|
+
its writing-format instructions, and the product frontend owns Markdown parsing, sanitization,
|
|
87
|
+
link policy, and styling. Rungent intentionally does not declare a content format or ship a
|
|
88
|
+
renderer.
|
|
89
|
+
20. A host tool may return `ToolResult.deferred=DeferredRequest(...)` for durable work that depends
|
|
90
|
+
on a browser or worker observation. Rungent persists the task binding, moves the Run to
|
|
91
|
+
`waiting_external`, and spends no model steps while waiting. Host code reports public progress
|
|
92
|
+
and calls `Runtime.resume_deferred` exactly once with an authoritative terminal result. The
|
|
93
|
+
runtime atomically claims and leases that resumption before returning to the model, so recovery
|
|
94
|
+
workers cannot mistake active continuation work for an interrupted Run. Connect
|
|
95
|
+
`external_task_canceller` when cancelling the Run must cancel host-owned work. Keep domain job
|
|
96
|
+
state in the application, not Rungent.
|
|
97
|
+
21. Applications with an obvious deterministic entry path may declare `Agent.run_initializer`.
|
|
98
|
+
It returns at most one registered typed `ToolCall` before the first model step; Rungent applies
|
|
99
|
+
the same validation, interaction, deferred, and event rules as it does to model calls.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rungent"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A small, typed agent runtime for Python applications"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
authors = [{ name = "Rungent Contributors" }]
|
|
9
|
+
keywords = ["agent", "llm", "tool-calling", "fastapi", "sse"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"License :: OSI Approved :: Apache Software License",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"httpx>=0.28.0",
|
|
21
|
+
"pydantic>=2.10.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/rungent/rungent"
|
|
26
|
+
Documentation = "https://github.com/rungent/rungent/tree/main/docs"
|
|
27
|
+
Repository = "https://github.com/rungent/rungent"
|
|
28
|
+
Issues = "https://github.com/rungent/rungent/issues"
|
|
29
|
+
Changelog = "https://github.com/rungent/rungent/releases"
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
fastapi = ["fastapi>=0.115.0"]
|
|
33
|
+
sqlalchemy = ["sqlalchemy[asyncio]>=2.0.36"]
|
|
34
|
+
dev = [
|
|
35
|
+
"aiosqlite>=0.20.0",
|
|
36
|
+
"fastapi>=0.115.0",
|
|
37
|
+
"pytest>=8.3.0",
|
|
38
|
+
"pytest-asyncio>=0.24.0",
|
|
39
|
+
"pyright>=1.1.0",
|
|
40
|
+
"ruff>=0.8.0",
|
|
41
|
+
"sqlalchemy[asyncio]>=2.0.36",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
rungent = "rungent.cli:main"
|
|
46
|
+
|
|
47
|
+
[build-system]
|
|
48
|
+
requires = ["hatchling"]
|
|
49
|
+
build-backend = "hatchling.build"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/rungent"]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.sdist]
|
|
55
|
+
include = [
|
|
56
|
+
"/src/rungent",
|
|
57
|
+
"/README.md",
|
|
58
|
+
"/RUNGENT.md",
|
|
59
|
+
"/LICENSE",
|
|
60
|
+
"/pyproject.toml",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
asyncio_mode = "auto"
|
|
65
|
+
testpaths = ["tests"]
|
|
66
|
+
filterwarnings = ["error"]
|
|
67
|
+
|
|
68
|
+
[tool.ruff]
|
|
69
|
+
line-length = 100
|
|
70
|
+
target-version = "py311"
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint]
|
|
73
|
+
select = ["E", "F", "I", "UP"]
|
|
74
|
+
|
|
75
|
+
[tool.pyright]
|
|
76
|
+
pythonVersion = "3.11"
|
|
77
|
+
typeCheckingMode = "standard"
|
|
78
|
+
venvPath = "."
|
|
79
|
+
venv = ".venv"
|
|
80
|
+
extraPaths = ["src"]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Rungent integration guide for coding agents
|
|
2
|
+
|
|
3
|
+
This is the canonical short context for an AI coding agent integrating Rungent.
|
|
4
|
+
|
|
5
|
+
1. Define one `Agent` for one product assistant. Do not introduce skills or routing unless the
|
|
6
|
+
product genuinely has multiple independent capability domains.
|
|
7
|
+
2. Define business operations with `@tool`. The first parameter is `ToolContext`; all other typed
|
|
8
|
+
parameters become the model-facing JSON Schema.
|
|
9
|
+
3. Declare `effect` and `approval` explicitly. Never infer safety from a function name.
|
|
10
|
+
Every `approval="always"` tool must provide a concrete `confirmation` string or callable that
|
|
11
|
+
identifies the affected object and impact; never ask users to approve only a generic tool title.
|
|
12
|
+
Equivalent successful non-read calls are deduplicated per Run; prefer a batch/count argument and
|
|
13
|
+
use `deduplicate=False` only when identical repeated writes are intentional.
|
|
14
|
+
4. Put durable service factories in `ToolContext.deps`, stable resource identifiers in
|
|
15
|
+
`ToolContext.resource`, and read the accepted input from `ToolContext.current_input`. After
|
|
16
|
+
`request_input` resumes, `ToolContext.interaction_response`
|
|
17
|
+
contains the runtime-validated interaction ID, kind, prompt, and value for that response only;
|
|
18
|
+
declare `requires_interaction_response=True` when a host operation must reject model-invented
|
|
19
|
+
answers. Rungent exposes that tool only after a real answer and consumes the answer once.
|
|
20
|
+
5. Use `Runtime` for session/run lifecycle. Do not write a second tool loop. The default bounded
|
|
21
|
+
loop allows 16 model steps. Retryable timeouts, transport failures, HTTP 408/429, and server
|
|
22
|
+
errors retry inside the same step up to three times; empty completions consume a new bounded
|
|
23
|
+
step and never become empty user messages.
|
|
24
|
+
6. Use the FastAPI router from `rungent.fastapi` when exposing the assistant over HTTP. Run
|
|
25
|
+
creation returns `202` with a Run ID; subscribe separately so disconnecting the event stream
|
|
26
|
+
never cancels execution. Pass `Idempotency-Key` when a client may retry creation.
|
|
27
|
+
7. Consume the SSE stream with `@rungent/sdk`. Do not parse provider-specific model chunks in the
|
|
28
|
+
frontend.
|
|
29
|
+
8. Use `request_input` for missing information. Group two to eight independent, already-known
|
|
30
|
+
questions in one `form`; ask sequentially only when a later question depends on an earlier
|
|
31
|
+
answer. A `choice` may declare `multiple` and `allow_custom`; choice responses use
|
|
32
|
+
`{selected: string[], custom?: string}`. Form responses use `{answers: {question_id: value}}`.
|
|
33
|
+
All responses are validated by the runtime. Destructive approvals are generated by Rungent,
|
|
34
|
+
not by prompting the model to ask “confirm?”.
|
|
35
|
+
9. Rungent exposes public work activity, not raw chain of thought. The model may call the built-in
|
|
36
|
+
`report_progress` alongside business tool calls during multi-step work, never as a dedicated
|
|
37
|
+
model step; model steps, tools, interactions, and progress are
|
|
38
|
+
persisted as ordered public events and projected by the SDK as `state.activities`.
|
|
39
|
+
Terminal Run events settle every unfinished activity; transport failures should call
|
|
40
|
+
`failRungentState` so a stale spinner can never survive an interrupted request.
|
|
41
|
+
Long-running host tools may call `await ctx.report_progress(message, public=...)`; Rungent
|
|
42
|
+
persists and streams each update while the tool is still executing. Updates from one tool call
|
|
43
|
+
share a stable activity ID, remain `running` while work continues, and replace one projected SDK
|
|
44
|
+
row; the ordered event log still retains every update for replay and audit.
|
|
45
|
+
10. Add deterministic baseline cases before enabling a real model baseline. Export a
|
|
46
|
+
`BaselineSuite` and run `rungent baseline module:suite` in the host project.
|
|
47
|
+
Tool and interaction expectations are exact by default; use their `contains` match modes only
|
|
48
|
+
for live-model cases where safe retries or optional branches are acceptable, and always assert
|
|
49
|
+
final state.
|
|
50
|
+
11. Read `docs/ai-reference.mdx` for the full public API and `examples/roasea/` for the reference
|
|
51
|
+
application pattern.
|
|
52
|
+
12. Model user-sized business intents as atomic host tools. Stage large imports in the host
|
|
53
|
+
application, return their diff through `ToolResult.public`, clarify with `request_input`, and
|
|
54
|
+
commit the reviewed draft with one revision-checked atomic tool and one approval. Likewise, a
|
|
55
|
+
reversible “change every leg” request should be one host batch tool, not many single-item calls;
|
|
56
|
+
increasing the model-step limit cannot repair an undersized business interface.
|
|
57
|
+
13. Model-facing tool results carry `tool_status: success|error`. Only explicit success envelopes
|
|
58
|
+
participate in deduplication; exceptions, timeouts, and `ToolResult(data={"ok": false})` remain
|
|
59
|
+
retryable. Call `client.cancel(runId)` before aborting the stream's `AbortSignal`; the cancel
|
|
60
|
+
endpoint terminates the active drive and persists `run.cancelled`. Treat `cancelled` as a
|
|
61
|
+
distinct terminal state.
|
|
62
|
+
14. Add trusted provider-specific top-level request fields through
|
|
63
|
+
`OpenAICompatibleModel(extra_body=...)` or the `LLM_EXTRA_BODY` JSON object. Core request fields
|
|
64
|
+
cannot be overridden. For latency-sensitive Qwen runs, for example, use
|
|
65
|
+
`{"enable_thinking":false}`.
|
|
66
|
+
15. Malformed or truncated tool-argument JSON is retried inside the same model step. Rungent resets
|
|
67
|
+
partial text and asks the provider for compact valid JSON before any tool can execute.
|
|
68
|
+
16. A host tool may return `ToolResult.interaction=InteractionRequest(...)` with a frozen
|
|
69
|
+
continuation tool. Rungent persists and displays that question immediately, then executes the
|
|
70
|
+
typed `requires_interaction_response` continuation directly after a validated answer without
|
|
71
|
+
spending another model step. Use `allow_skip` only when skipping is safe for the workflow.
|
|
72
|
+
17. Latency-sensitive agents should declare `run_activity`, returning public `RunActivity`
|
|
73
|
+
messages authored by the host application. Rungent emits the first activity immediately and,
|
|
74
|
+
while a provider stream is silent, updates the same persisted activity after three seconds and
|
|
75
|
+
every five seconds thereafter. Progress activities remain `running` until real model work
|
|
76
|
+
advances or the Run terminates; never use this channel for hidden reasoning. Products that
|
|
77
|
+
require finite deadlines may set per-attempt `model_step_timeout_seconds` and overall
|
|
78
|
+
`model_step_total_timeout_seconds`; retries receive a fresh per-attempt budget.
|
|
79
|
+
18. Bounded-loop exhaustion keeps the numeric model-step limit in operator state and emits a safe
|
|
80
|
+
`run.failed` envelope with `code=model_step_limit_exceeded` and `retryable=true`. Product UIs
|
|
81
|
+
should localize recovery guidance from the code instead of displaying internals.
|
|
82
|
+
19. `request_input` rejects numbered alternatives embedded in a `text` prompt; use `choice` so the
|
|
83
|
+
client can render typed options and a custom-answer row. Tool activities expose stable failure
|
|
84
|
+
codes, allowing product UIs to suppress or localize technical validation noise.
|
|
85
|
+
19. Assistant message content is an opaque string to Rungent and `@rungent/sdk`. The host Agent owns
|
|
86
|
+
its writing-format instructions, and the product frontend owns Markdown parsing, sanitization,
|
|
87
|
+
link policy, and styling. Rungent intentionally does not declare a content format or ship a
|
|
88
|
+
renderer.
|
|
89
|
+
20. A host tool may return `ToolResult.deferred=DeferredRequest(...)` for durable work that depends
|
|
90
|
+
on a browser or worker observation. Rungent persists the task binding, moves the Run to
|
|
91
|
+
`waiting_external`, and spends no model steps while waiting. Host code reports public progress
|
|
92
|
+
and calls `Runtime.resume_deferred` exactly once with an authoritative terminal result. The
|
|
93
|
+
runtime atomically claims and leases that resumption before returning to the model, so recovery
|
|
94
|
+
workers cannot mistake active continuation work for an interrupted Run. Connect
|
|
95
|
+
`external_task_canceller` when cancelling the Run must cancel host-owned work. Keep domain job
|
|
96
|
+
state in the application, not Rungent.
|
|
97
|
+
21. Applications with an obvious deterministic entry path may declare `Agent.run_initializer`.
|
|
98
|
+
It returns at most one registered typed `ToolCall` before the first model step; Rungent applies
|
|
99
|
+
the same validation, interaction, deferred, and event rules as it does to model calls.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from .agent import Agent, RunActivity
|
|
2
|
+
from .runtime import Runtime
|
|
3
|
+
from .state import (
|
|
4
|
+
DeferredRequest,
|
|
5
|
+
Identity,
|
|
6
|
+
InteractionRequest,
|
|
7
|
+
InteractionResponse,
|
|
8
|
+
ToolContinuation,
|
|
9
|
+
ToolResult,
|
|
10
|
+
TrustedInteractionResponse,
|
|
11
|
+
)
|
|
12
|
+
from .tools import ApprovalPolicy, Tool, ToolContext, ToolEffect, tool
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Agent",
|
|
16
|
+
"ApprovalPolicy",
|
|
17
|
+
"DeferredRequest",
|
|
18
|
+
"Identity",
|
|
19
|
+
"InteractionRequest",
|
|
20
|
+
"InteractionResponse",
|
|
21
|
+
"Runtime",
|
|
22
|
+
"RunActivity",
|
|
23
|
+
"Tool",
|
|
24
|
+
"ToolContext",
|
|
25
|
+
"ToolContinuation",
|
|
26
|
+
"ToolEffect",
|
|
27
|
+
"ToolResult",
|
|
28
|
+
"TrustedInteractionResponse",
|
|
29
|
+
"tool",
|
|
30
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from .state import new_id, now
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Event(BaseModel):
|
|
14
|
+
v: int = 1
|
|
15
|
+
id: str = Field(default_factory=lambda: new_id("evt"))
|
|
16
|
+
seq: int
|
|
17
|
+
type: str
|
|
18
|
+
session_id: str
|
|
19
|
+
run_id: str
|
|
20
|
+
created_at: datetime = Field(default_factory=now)
|
|
21
|
+
data: dict[str, Any] = Field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(slots=True)
|
|
25
|
+
class EventEmitter:
|
|
26
|
+
session_id: str
|
|
27
|
+
run_id: str
|
|
28
|
+
sequence: int = 0
|
|
29
|
+
|
|
30
|
+
def emit(self, event_type: str, **data: Any) -> Event:
|
|
31
|
+
self.sequence += 1
|
|
32
|
+
return Event(
|
|
33
|
+
seq=self.sequence,
|
|
34
|
+
type=event_type,
|
|
35
|
+
session_id=self.session_id,
|
|
36
|
+
run_id=self.run_id,
|
|
37
|
+
data=data,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def encode_sse(event: Event) -> str:
|
|
42
|
+
return f"data: {json.dumps(event.model_dump(mode='json'), ensure_ascii=False)}\n\n"
|