render-lab-tasks-agent 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.
- render_lab_tasks_agent-0.1.0/.gitignore +11 -0
- render_lab_tasks_agent-0.1.0/LICENSE +21 -0
- render_lab_tasks_agent-0.1.0/PKG-INFO +39 -0
- render_lab_tasks_agent-0.1.0/README.md +25 -0
- render_lab_tasks_agent-0.1.0/pyproject.toml +23 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/__init__.py +1 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/_app.py +3 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/client.py +64 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/compress_history.py +34 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/loop.py +64 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/memory.py +41 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/plan.py +37 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/prompts.py +140 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/py.typed +0 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/reflect.py +37 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/retry.py +3 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/route.py +48 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/run_tool.py +34 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/step.py +46 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/tasks.py +29 -0
- render_lab_tasks_agent-0.1.0/src/render_lab_tasks_agent/types.py +140 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Render Lab
|
|
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,39 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: render-lab-tasks-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: agent tasks for Render Workflows
|
|
5
|
+
Project-URL: Repository, https://github.com/render-lab/render-tasks-python
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: httpx<0.29,>=0.28
|
|
10
|
+
Requires-Dist: render-lab-tasks-core<0.2,>=0.1.1
|
|
11
|
+
Requires-Dist: render-lab-tasks-llm<0.2,>=0.1.1
|
|
12
|
+
Requires-Dist: render==1.0.1
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Agent tasks for Python
|
|
16
|
+
|
|
17
|
+
Eight registered tasks cover planning, single decisions, loops, reflection,
|
|
18
|
+
routing, history compression, named tools, and injected KV memory.
|
|
19
|
+
Import the tasks and `app` from `render_lab_tasks_agent.tasks`.
|
|
20
|
+
|
|
21
|
+
## Composition and environment
|
|
22
|
+
|
|
23
|
+
LLM calls inherit the `render-lab-tasks-llm` provider/model/ledger environment
|
|
24
|
+
contract. Credentials remain lazy. Raw decision implementations accept
|
|
25
|
+
`CompletionDeps(complete=...)`; loops take `AgentDeps(step=..., tools=...)`.
|
|
26
|
+
The default loop dispatches wrapped `agent.step` through `ctx.run` so every
|
|
27
|
+
model decision is a durable child run. Register `ToolBinding` values with
|
|
28
|
+
`register_agent_tool`; each binding's `run(ctx, args)` must dispatch wrapped
|
|
29
|
+
Render tasks via `ctx.run` and return a string observation.
|
|
30
|
+
|
|
31
|
+
`memory_impl` requires an injected `MemoryDeps(kv=...)`; there is no implicit
|
|
32
|
+
in-memory fallback or default KV account. Re-register a wrapper under your own
|
|
33
|
+
namespace to supply the store. No live agent/provider run verified this port.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
pip install render-lab-tasks-agent==0.1.0
|
|
39
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Agent tasks for Python
|
|
2
|
+
|
|
3
|
+
Eight registered tasks cover planning, single decisions, loops, reflection,
|
|
4
|
+
routing, history compression, named tools, and injected KV memory.
|
|
5
|
+
Import the tasks and `app` from `render_lab_tasks_agent.tasks`.
|
|
6
|
+
|
|
7
|
+
## Composition and environment
|
|
8
|
+
|
|
9
|
+
LLM calls inherit the `render-lab-tasks-llm` provider/model/ledger environment
|
|
10
|
+
contract. Credentials remain lazy. Raw decision implementations accept
|
|
11
|
+
`CompletionDeps(complete=...)`; loops take `AgentDeps(step=..., tools=...)`.
|
|
12
|
+
The default loop dispatches wrapped `agent.step` through `ctx.run` so every
|
|
13
|
+
model decision is a durable child run. Register `ToolBinding` values with
|
|
14
|
+
`register_agent_tool`; each binding's `run(ctx, args)` must dispatch wrapped
|
|
15
|
+
Render tasks via `ctx.run` and return a string observation.
|
|
16
|
+
|
|
17
|
+
`memory_impl` requires an injected `MemoryDeps(kv=...)`; there is no implicit
|
|
18
|
+
in-memory fallback or default KV account. Re-register a wrapper under your own
|
|
19
|
+
namespace to supply the store. No live agent/provider run verified this port.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
pip install render-lab-tasks-agent==0.1.0
|
|
25
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "render-lab-tasks-agent"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "agent tasks for Render Workflows"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.12"
|
|
13
|
+
dependencies = ["render-lab-tasks-llm>=0.1.1,<0.2","render==1.0.1", "httpx>=0.28,<0.29", "render-lab-tasks-core>=0.1.1,<0.2"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Repository = "https://github.com/render-lab/render-tasks-python"
|
|
17
|
+
|
|
18
|
+
[tool.uv.sources]
|
|
19
|
+
render-lab-tasks-llm = { workspace = true }
|
|
20
|
+
render-lab-tasks-core = { workspace = true }
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/render_lab_tasks_agent"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Import .tasks explicitly to register durable tasks."""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Injected completions and a registry of durable tool bindings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from render import TaskContext
|
|
9
|
+
from render_lab_tasks_llm.types import CompleteInput, CompleteResult
|
|
10
|
+
|
|
11
|
+
from .types import AgentTool, Json, StepInput, StepResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ToolBinding:
|
|
16
|
+
spec: AgentTool
|
|
17
|
+
run: Callable[[TaskContext, dict[str, Json]], Awaitable[str]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
tool_registry: dict[str, ToolBinding] = {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def register_agent_tool(binding: ToolBinding) -> None:
|
|
24
|
+
tool_registry[binding.spec["name"]] = binding
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class CompletionDeps:
|
|
29
|
+
complete: Callable[[CompleteInput], Awaitable[CompleteResult]]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def completion_deps(ctx: TaskContext, deps: CompletionDeps | None) -> CompletionDeps:
|
|
33
|
+
if deps is not None:
|
|
34
|
+
return deps
|
|
35
|
+
from render_lab_tasks_llm.complete import complete_impl
|
|
36
|
+
|
|
37
|
+
async def complete(input: CompleteInput) -> CompleteResult:
|
|
38
|
+
return await complete_impl(ctx, input)
|
|
39
|
+
|
|
40
|
+
return CompletionDeps(complete=complete)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class AgentDeps:
|
|
45
|
+
step: Callable[[StepInput], Awaitable[StepResult]]
|
|
46
|
+
tools: dict[str, ToolBinding] = field(default_factory=lambda: tool_registry)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def agent_deps(ctx: TaskContext, deps: AgentDeps | None) -> AgentDeps:
|
|
50
|
+
if deps is not None:
|
|
51
|
+
return deps
|
|
52
|
+
from .step import step
|
|
53
|
+
|
|
54
|
+
async def decide(input: StepInput) -> StepResult:
|
|
55
|
+
return await ctx.run(step, input)
|
|
56
|
+
|
|
57
|
+
return AgentDeps(step=decide)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
PlanDeps = CompletionDeps
|
|
61
|
+
StepDeps = CompletionDeps
|
|
62
|
+
ReflectDeps = CompletionDeps
|
|
63
|
+
RouteDeps = CompletionDeps
|
|
64
|
+
CompressHistoryDeps = CompletionDeps
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from render import TaskContext
|
|
2
|
+
from render_lab_tasks_llm.chat import resolve_model
|
|
3
|
+
from render_lab_tasks_llm.types import CompleteInput
|
|
4
|
+
|
|
5
|
+
from . import types as t
|
|
6
|
+
from ._app import app
|
|
7
|
+
from .client import CompletionDeps, completion_deps
|
|
8
|
+
from .prompts import build_compress_prompt
|
|
9
|
+
from .retry import AGENT_RETRY
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def compress_history_impl(
|
|
13
|
+
ctx: TaskContext, input: t.CompressHistoryInput, *, deps: CompletionDeps | None = None
|
|
14
|
+
) -> t.CompressHistoryResult:
|
|
15
|
+
if not input["history"]:
|
|
16
|
+
return {"summary": ""}
|
|
17
|
+
resolved = completion_deps(ctx, deps)
|
|
18
|
+
system, prompt = build_compress_prompt(input)
|
|
19
|
+
request: CompleteInput = {
|
|
20
|
+
"prompt": prompt,
|
|
21
|
+
"system": system,
|
|
22
|
+
"model": resolve_model(input.get("model")),
|
|
23
|
+
}
|
|
24
|
+
if "ledger" in input:
|
|
25
|
+
request["ledger"] = input["ledger"]
|
|
26
|
+
response = await resolved.complete(request)
|
|
27
|
+
return {"summary": response["text"].strip()}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.task(name="agent.compressHistory", retry=AGENT_RETRY)
|
|
31
|
+
async def compress_history(
|
|
32
|
+
ctx: TaskContext, input: t.CompressHistoryInput
|
|
33
|
+
) -> t.CompressHistoryResult:
|
|
34
|
+
return await compress_history_impl(ctx, input)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from render import TaskContext
|
|
2
|
+
from render_lab_tasks_core.http import nullish
|
|
3
|
+
|
|
4
|
+
from ._app import app
|
|
5
|
+
from .client import AgentDeps, agent_deps
|
|
6
|
+
from .retry import AGENT_RETRY
|
|
7
|
+
from .types import AgentStepRecord, LoopInput, LoopResult, StepInput
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def loop_impl(
|
|
11
|
+
ctx: TaskContext, input: LoopInput, *, deps: AgentDeps | None = None
|
|
12
|
+
) -> LoopResult:
|
|
13
|
+
resolved = agent_deps(ctx, deps)
|
|
14
|
+
tools = (
|
|
15
|
+
{name: resolved.tools[name] for name in input["toolNames"] if name in resolved.tools}
|
|
16
|
+
if input.get("toolNames") is not None
|
|
17
|
+
else resolved.tools
|
|
18
|
+
)
|
|
19
|
+
specs = [binding.spec for binding in tools.values()]
|
|
20
|
+
history: list[AgentStepRecord] = []
|
|
21
|
+
last = ""
|
|
22
|
+
i = 0
|
|
23
|
+
while i < nullish(input.get("maxSteps"), 10):
|
|
24
|
+
i += 1
|
|
25
|
+
request: StepInput = {"goal": input["goal"], "history": history.copy(), "tools": specs}
|
|
26
|
+
if "system" in input:
|
|
27
|
+
request["system"] = input["system"]
|
|
28
|
+
if "model" in input:
|
|
29
|
+
request["model"] = input["model"]
|
|
30
|
+
if "ledger" in input:
|
|
31
|
+
request["ledger"] = input["ledger"]
|
|
32
|
+
action = await resolved.step(request)
|
|
33
|
+
if action["type"] == "final":
|
|
34
|
+
return {
|
|
35
|
+
"answer": nullish(action.get("answer"), ""),
|
|
36
|
+
"steps": history,
|
|
37
|
+
"stepCount": len(history),
|
|
38
|
+
"stoppedReason": "final",
|
|
39
|
+
}
|
|
40
|
+
name = nullish(action.get("tool"), "")
|
|
41
|
+
record: AgentStepRecord = {"tool": name}
|
|
42
|
+
if "thought" in action:
|
|
43
|
+
record["thought"] = action["thought"]
|
|
44
|
+
if "args" in action:
|
|
45
|
+
record["args"] = action["args"]
|
|
46
|
+
if name not in tools:
|
|
47
|
+
record["observation"] = f'error: unknown tool "{name}"'
|
|
48
|
+
else:
|
|
49
|
+
last = await tools[name].run(ctx, nullish(action.get("args"), {}))
|
|
50
|
+
if not isinstance(last, str):
|
|
51
|
+
raise TypeError("Agent tools must return a string observation")
|
|
52
|
+
record["observation"] = last
|
|
53
|
+
history.append(record)
|
|
54
|
+
return {
|
|
55
|
+
"answer": last,
|
|
56
|
+
"steps": history,
|
|
57
|
+
"stepCount": len(history),
|
|
58
|
+
"stoppedReason": "maxSteps",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.task(name="agent.loop", retry=AGENT_RETRY)
|
|
63
|
+
async def loop(ctx: TaskContext, input: LoopInput) -> LoopResult:
|
|
64
|
+
return await loop_impl(ctx, input)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
from render import TaskContext
|
|
5
|
+
|
|
6
|
+
from ._app import app
|
|
7
|
+
from .retry import AGENT_RETRY
|
|
8
|
+
from .types import MemoryInput, MemoryResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MemoryKvPort(Protocol):
|
|
12
|
+
async def get(self, key: str) -> str | None: ...
|
|
13
|
+
async def set(self, key: str, value: str) -> None: ...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class MemoryDeps:
|
|
18
|
+
kv: MemoryKvPort | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def memory_impl(
|
|
22
|
+
ctx: TaskContext, input: MemoryInput, *, deps: MemoryDeps | None = None
|
|
23
|
+
) -> MemoryResult:
|
|
24
|
+
if deps is None or deps.kv is None:
|
|
25
|
+
raise ValueError(
|
|
26
|
+
"agent.memory: no KV store configured — inject deps.kv (e.g. from "
|
|
27
|
+
"@render-lab/tasks-render-kv) with { get(key), set(key, value) }."
|
|
28
|
+
)
|
|
29
|
+
if input["op"] == "get":
|
|
30
|
+
return {"key": input["key"], "value": await deps.kv.get(input["key"])}
|
|
31
|
+
if input["op"] == "set":
|
|
32
|
+
if "value" not in input:
|
|
33
|
+
raise ValueError(f'agent.memory: "set" requires a value for key "{input["key"]}".')
|
|
34
|
+
await deps.kv.set(input["key"], input["value"])
|
|
35
|
+
return {"key": input["key"], "ok": True}
|
|
36
|
+
raise ValueError(f'agent.memory: unknown op "{input["op"]}" (expected "get" or "set").')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.task(name="agent.memory", retry=AGENT_RETRY)
|
|
40
|
+
async def memory(ctx: TaskContext, input: MemoryInput) -> MemoryResult:
|
|
41
|
+
return await memory_impl(ctx, input)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from render import TaskContext
|
|
2
|
+
from render_lab_tasks_core.http import js_string
|
|
3
|
+
from render_lab_tasks_llm.chat import resolve_model
|
|
4
|
+
from render_lab_tasks_llm.json import extract_json
|
|
5
|
+
from render_lab_tasks_llm.types import CompleteInput
|
|
6
|
+
|
|
7
|
+
from . import types as t
|
|
8
|
+
from ._app import app
|
|
9
|
+
from .client import CompletionDeps, completion_deps
|
|
10
|
+
from .prompts import build_plan_prompt
|
|
11
|
+
from .retry import AGENT_RETRY
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def plan_impl(
|
|
15
|
+
ctx: TaskContext, input: t.PlanInput, *, deps: CompletionDeps | None = None
|
|
16
|
+
) -> t.PlanResult:
|
|
17
|
+
resolved = completion_deps(ctx, deps)
|
|
18
|
+
system, prompt = build_plan_prompt(input)
|
|
19
|
+
request: CompleteInput = {
|
|
20
|
+
"prompt": prompt,
|
|
21
|
+
"system": system,
|
|
22
|
+
"model": resolve_model(input.get("model")),
|
|
23
|
+
}
|
|
24
|
+
if "ledger" in input:
|
|
25
|
+
request["ledger"] = input["ledger"]
|
|
26
|
+
response = await resolved.complete(request)
|
|
27
|
+
parsed = extract_json(response["text"])
|
|
28
|
+
steps = parsed.get("steps") if isinstance(parsed, dict) else None
|
|
29
|
+
return {
|
|
30
|
+
"steps": [js_string(s) for s in steps] if isinstance(steps, list) else [],
|
|
31
|
+
"model": response["model"],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.task(name="agent.plan", retry=AGENT_RETRY)
|
|
36
|
+
async def plan(ctx: TaskContext, input: t.PlanInput) -> t.PlanResult:
|
|
37
|
+
return await plan_impl(ctx, input)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Pure prompt builders for observable agent decisions."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from render_lab_tasks_core.http import js_string, nullish
|
|
6
|
+
|
|
7
|
+
from . import types as t
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def compact(value: t.Json) -> str:
|
|
11
|
+
return json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(",", ":"))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def format_history(history: list[t.AgentStepRecord]) -> str:
|
|
15
|
+
if not history:
|
|
16
|
+
return "(none yet)"
|
|
17
|
+
lines = []
|
|
18
|
+
for i, item in enumerate(history):
|
|
19
|
+
parts = [f"{i + 1}."]
|
|
20
|
+
if item.get("thought"):
|
|
21
|
+
parts.append("thought: " + item["thought"])
|
|
22
|
+
if item.get("tool"):
|
|
23
|
+
parts.append(f"tool: {item['tool']}({compact(item.get('args', {}))})")
|
|
24
|
+
if "observation" in item:
|
|
25
|
+
parts.append("observation: " + item["observation"])
|
|
26
|
+
lines.append(" ".join(parts))
|
|
27
|
+
return "\n".join(lines)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_step_prompt(input: t.StepInput) -> tuple[str, str]:
|
|
31
|
+
tools = (
|
|
32
|
+
"\n".join(
|
|
33
|
+
f"- {tool['name']}: {tool['description']} (args JSON Schema: "
|
|
34
|
+
f"{compact(tool['parameters'])})"
|
|
35
|
+
for tool in input["tools"]
|
|
36
|
+
)
|
|
37
|
+
or "(no tools available)"
|
|
38
|
+
)
|
|
39
|
+
lines = [input["system"].strip()] if "system" in input else []
|
|
40
|
+
system = "\n".join(
|
|
41
|
+
lines
|
|
42
|
+
+ [
|
|
43
|
+
"You are a ReAct agent. You accomplish a goal by choosing one action at a time,",
|
|
44
|
+
"observing the result, and repeating until you can answer.",
|
|
45
|
+
"",
|
|
46
|
+
"Available tools:",
|
|
47
|
+
tools,
|
|
48
|
+
"",
|
|
49
|
+
"Respond with ONLY a single JSON object. No prose, no explanation, no markdown fences.",
|
|
50
|
+
'To call a tool: {"type":"tool","tool":"<name>","args":{ ... },"thought":"<why>"}',
|
|
51
|
+
'To finish: {"type":"final","answer":"<the answer>","thought":"<why>"}',
|
|
52
|
+
'The tool name MUST be one of the names listed above. Use type "final" '
|
|
53
|
+
"once you have enough information.",
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
prompt = "\n".join(
|
|
57
|
+
[
|
|
58
|
+
f"Goal: {input['goal']}",
|
|
59
|
+
"",
|
|
60
|
+
"Steps so far:",
|
|
61
|
+
format_history(input["history"]),
|
|
62
|
+
"",
|
|
63
|
+
"What is your next action? Respond with ONLY the JSON object.",
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
return system, prompt
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_plan_prompt(input: t.PlanInput) -> tuple[str, str]:
|
|
70
|
+
system = "\n".join(
|
|
71
|
+
[
|
|
72
|
+
"You are a planning assistant. Break a goal into a short, ordered list of",
|
|
73
|
+
"concrete steps an agent could execute.",
|
|
74
|
+
"",
|
|
75
|
+
'Respond with ONLY a JSON object of the form {"steps":["first step","second step"]}.',
|
|
76
|
+
"No prose, no markdown fences. Keep it to at most 6 steps.",
|
|
77
|
+
]
|
|
78
|
+
)
|
|
79
|
+
return system, f"Goal: {input['goal']}" + (
|
|
80
|
+
f"\nContext:\n{input['context']}" if input.get("context") else ""
|
|
81
|
+
) + "\nProduce the plan as JSON now."
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def build_reflect_prompt(input: t.ReflectInput) -> tuple[str, str]:
|
|
85
|
+
system = "\n".join(
|
|
86
|
+
[
|
|
87
|
+
"You are a critical reviewer of an agent's progress. Decide whether the goal",
|
|
88
|
+
"has been achieved and give a one-line critique of what to do next.",
|
|
89
|
+
"",
|
|
90
|
+
'Respond with ONLY a JSON object of the form {"done":true|false,"critique":"..."}.',
|
|
91
|
+
"No prose, no markdown fences.",
|
|
92
|
+
]
|
|
93
|
+
)
|
|
94
|
+
return system, "\n".join(
|
|
95
|
+
[
|
|
96
|
+
f"Goal: {input['goal']}",
|
|
97
|
+
"",
|
|
98
|
+
"Steps so far:",
|
|
99
|
+
format_history(input["history"]),
|
|
100
|
+
"",
|
|
101
|
+
"Is the goal achieved? Respond with ONLY the JSON object.",
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def build_compress_prompt(input: t.CompressHistoryInput) -> tuple[str, str]:
|
|
107
|
+
maximum = js_string(nullish(input.get("maxWords"), 150))
|
|
108
|
+
system = "\n".join(
|
|
109
|
+
[
|
|
110
|
+
"You compress an AI agent's scratchpad into a short summary so it fits in a",
|
|
111
|
+
"limited context window. Preserve the key facts, decisions, and open threads;",
|
|
112
|
+
"drop redundancy and verbatim tool output.",
|
|
113
|
+
"",
|
|
114
|
+
f"Respond with ONLY the summary as plain prose, at most {maximum} words.",
|
|
115
|
+
"No preamble, no markdown fences, no bullet headers.",
|
|
116
|
+
]
|
|
117
|
+
)
|
|
118
|
+
return system, "\n".join(
|
|
119
|
+
["History to summarize:", format_history(input["history"]), "", "Write the summary now."]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def build_route_prompt(input: t.RouteInput) -> tuple[str, str]:
|
|
124
|
+
routes = "\n".join(f"- {r['name']}: {r['description']}" for r in input["routes"])
|
|
125
|
+
system = "\n".join(
|
|
126
|
+
[
|
|
127
|
+
"You are a router. Given a goal and a set of named routes, pick the single",
|
|
128
|
+
"best route to handle the goal.",
|
|
129
|
+
"",
|
|
130
|
+
"Available routes:",
|
|
131
|
+
routes,
|
|
132
|
+
"",
|
|
133
|
+
'Respond with ONLY a JSON object of the form {"route":"<name>","reasoning":"..."}.',
|
|
134
|
+
"The route MUST be exactly one of the names listed above. No prose, "
|
|
135
|
+
"no markdown fences.",
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
return system, "\n".join(
|
|
139
|
+
[f"Goal: {input['goal']}", "", "Which route? Respond with ONLY the JSON object."]
|
|
140
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from render import TaskContext
|
|
2
|
+
from render_lab_tasks_llm.chat import resolve_model
|
|
3
|
+
from render_lab_tasks_llm.json import extract_json
|
|
4
|
+
from render_lab_tasks_llm.types import CompleteInput
|
|
5
|
+
|
|
6
|
+
from . import types as t
|
|
7
|
+
from ._app import app
|
|
8
|
+
from .client import CompletionDeps, completion_deps
|
|
9
|
+
from .prompts import build_reflect_prompt
|
|
10
|
+
from .retry import AGENT_RETRY
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def reflect_impl(
|
|
14
|
+
ctx: TaskContext, input: t.ReflectInput, *, deps: CompletionDeps | None = None
|
|
15
|
+
) -> t.ReflectResult:
|
|
16
|
+
resolved = completion_deps(ctx, deps)
|
|
17
|
+
system, prompt = build_reflect_prompt(input)
|
|
18
|
+
request: CompleteInput = {
|
|
19
|
+
"prompt": prompt,
|
|
20
|
+
"system": system,
|
|
21
|
+
"model": resolve_model(input.get("model")),
|
|
22
|
+
}
|
|
23
|
+
if "ledger" in input:
|
|
24
|
+
request["ledger"] = input["ledger"]
|
|
25
|
+
response = await resolved.complete(request)
|
|
26
|
+
parsed = extract_json(response["text"])
|
|
27
|
+
parsed = parsed if isinstance(parsed, dict) else {}
|
|
28
|
+
return {
|
|
29
|
+
"done": parsed.get("done") is True,
|
|
30
|
+
"critique": parsed["critique"] if isinstance(parsed.get("critique"), str) else "",
|
|
31
|
+
"model": response["model"],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.task(name="agent.reflect", retry=AGENT_RETRY)
|
|
36
|
+
async def reflect(ctx: TaskContext, input: t.ReflectInput) -> t.ReflectResult:
|
|
37
|
+
return await reflect_impl(ctx, input)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from render import TaskContext
|
|
2
|
+
from render_lab_tasks_llm.chat import resolve_model
|
|
3
|
+
from render_lab_tasks_llm.json import extract_json
|
|
4
|
+
from render_lab_tasks_llm.types import CompleteInput
|
|
5
|
+
|
|
6
|
+
from . import types as t
|
|
7
|
+
from ._app import app
|
|
8
|
+
from .client import CompletionDeps, completion_deps
|
|
9
|
+
from .prompts import build_route_prompt
|
|
10
|
+
from .retry import AGENT_RETRY
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def route_impl(
|
|
14
|
+
ctx: TaskContext, input: t.RouteInput, *, deps: CompletionDeps | None = None
|
|
15
|
+
) -> t.RouteResult:
|
|
16
|
+
if not input["routes"]:
|
|
17
|
+
raise ValueError("agent.route: no routes provided to choose from.")
|
|
18
|
+
resolved = completion_deps(ctx, deps)
|
|
19
|
+
system, prompt = build_route_prompt(input)
|
|
20
|
+
request: CompleteInput = {
|
|
21
|
+
"prompt": prompt,
|
|
22
|
+
"system": system,
|
|
23
|
+
"model": resolve_model(input.get("model")),
|
|
24
|
+
}
|
|
25
|
+
if "ledger" in input:
|
|
26
|
+
request["ledger"] = input["ledger"]
|
|
27
|
+
response = await resolved.complete(request)
|
|
28
|
+
parsed = extract_json(response["text"])
|
|
29
|
+
parsed = parsed if isinstance(parsed, dict) else {}
|
|
30
|
+
chosen = parsed.get("route")
|
|
31
|
+
reasoning = parsed.get("reasoning")
|
|
32
|
+
reasoning = reasoning if isinstance(reasoning, str) else None
|
|
33
|
+
if isinstance(chosen, str) and any(r["name"] == chosen for r in input["routes"]):
|
|
34
|
+
result: t.RouteResult = {"route": chosen}
|
|
35
|
+
if reasoning is not None:
|
|
36
|
+
result["reasoning"] = reasoning
|
|
37
|
+
return result
|
|
38
|
+
return {
|
|
39
|
+
"route": input["routes"][0]["name"],
|
|
40
|
+
"reasoning": reasoning + " (fell back to first route)"
|
|
41
|
+
if reasoning
|
|
42
|
+
else "fell back to first route (model returned no valid route)",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.task(name="agent.route", retry=AGENT_RETRY)
|
|
47
|
+
async def route(ctx: TaskContext, input: t.RouteInput) -> t.RouteResult:
|
|
48
|
+
return await route_impl(ctx, input)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from render import TaskContext
|
|
4
|
+
from render_lab_tasks_core.http import nullish
|
|
5
|
+
|
|
6
|
+
from ._app import app
|
|
7
|
+
from .client import ToolBinding, tool_registry
|
|
8
|
+
from .retry import AGENT_RETRY
|
|
9
|
+
from .types import RunToolInput, RunToolResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class RunToolDeps:
|
|
14
|
+
tools: dict[str, ToolBinding] = field(default_factory=lambda: tool_registry)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def run_tool_impl(
|
|
18
|
+
ctx: TaskContext, input: RunToolInput, *, deps: RunToolDeps | None = None
|
|
19
|
+
) -> RunToolResult:
|
|
20
|
+
tools = (deps or RunToolDeps()).tools
|
|
21
|
+
if input["tool"] not in tools:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
f'agent.runTool: unknown tool "{input["tool"]}". Register it with '
|
|
24
|
+
f"registerAgentTool(...) before calling it."
|
|
25
|
+
)
|
|
26
|
+
output = await tools[input["tool"]].run(ctx, nullish(input.get("args"), {}))
|
|
27
|
+
if not isinstance(output, str):
|
|
28
|
+
raise TypeError("Agent tools must return a string observation")
|
|
29
|
+
return {"tool": input["tool"], "output": output}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.task(name="agent.runTool", retry=AGENT_RETRY)
|
|
33
|
+
async def run_tool(ctx: TaskContext, input: RunToolInput) -> RunToolResult:
|
|
34
|
+
return await run_tool_impl(ctx, input)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from render import TaskContext
|
|
5
|
+
from render_lab_tasks_llm.chat import resolve_model
|
|
6
|
+
from render_lab_tasks_llm.json import extract_json
|
|
7
|
+
from render_lab_tasks_llm.types import CompleteInput
|
|
8
|
+
|
|
9
|
+
from . import types as t
|
|
10
|
+
from ._app import app
|
|
11
|
+
from .client import CompletionDeps, completion_deps
|
|
12
|
+
from .prompts import build_step_prompt
|
|
13
|
+
from .retry import AGENT_RETRY
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def step_impl(
|
|
17
|
+
ctx: TaskContext, input: t.StepInput, *, deps: CompletionDeps | None = None
|
|
18
|
+
) -> t.StepResult:
|
|
19
|
+
resolved = completion_deps(ctx, deps)
|
|
20
|
+
system, prompt = build_step_prompt(input)
|
|
21
|
+
request: CompleteInput = {
|
|
22
|
+
"prompt": prompt,
|
|
23
|
+
"system": system,
|
|
24
|
+
"model": resolve_model(input.get("model")),
|
|
25
|
+
}
|
|
26
|
+
if "ledger" in input:
|
|
27
|
+
request["ledger"] = input["ledger"]
|
|
28
|
+
response = await resolved.complete(request)
|
|
29
|
+
action = extract_json(response["text"])
|
|
30
|
+
if not isinstance(action, dict) or action.get("type") not in ("tool", "final"):
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"agent.step: could not parse a valid AgentAction from model output: "
|
|
33
|
+
f"{response['text'][:200]}"
|
|
34
|
+
)
|
|
35
|
+
if action["type"] == "tool" and not action.get("tool"):
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"agent.step: model returned a tool action without a tool name: "
|
|
38
|
+
f"{response['text'][:200]}"
|
|
39
|
+
)
|
|
40
|
+
json.dumps(action, allow_nan=False)
|
|
41
|
+
return cast(t.StepResult, {**action, "model": response["model"]})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.task(name="agent.step", retry=AGENT_RETRY)
|
|
45
|
+
async def step(ctx: TaskContext, input: t.StepInput) -> t.StepResult:
|
|
46
|
+
return await step_impl(ctx, input)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from ._app import app
|
|
2
|
+
from .compress_history import compress_history, compress_history_impl
|
|
3
|
+
from .loop import loop, loop_impl
|
|
4
|
+
from .memory import memory, memory_impl
|
|
5
|
+
from .plan import plan, plan_impl
|
|
6
|
+
from .reflect import reflect, reflect_impl
|
|
7
|
+
from .route import route, route_impl
|
|
8
|
+
from .run_tool import run_tool, run_tool_impl
|
|
9
|
+
from .step import step, step_impl
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"app",
|
|
13
|
+
"compress_history",
|
|
14
|
+
"compress_history_impl",
|
|
15
|
+
"loop",
|
|
16
|
+
"loop_impl",
|
|
17
|
+
"memory",
|
|
18
|
+
"memory_impl",
|
|
19
|
+
"plan",
|
|
20
|
+
"plan_impl",
|
|
21
|
+
"reflect",
|
|
22
|
+
"reflect_impl",
|
|
23
|
+
"route",
|
|
24
|
+
"route_impl",
|
|
25
|
+
"run_tool",
|
|
26
|
+
"run_tool_impl",
|
|
27
|
+
"step",
|
|
28
|
+
"step_impl",
|
|
29
|
+
]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""JSON contracts ported from the pinned TypeScript pack."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal, NotRequired, TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MemorySetResult(TypedDict):
|
|
9
|
+
key: str
|
|
10
|
+
ok: Literal[True]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MemoryGetResult(TypedDict):
|
|
14
|
+
key: str
|
|
15
|
+
value: str | None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
type Json = str | float | bool | None | list[Json] | dict[str, Json]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentTool(TypedDict):
|
|
22
|
+
name: str
|
|
23
|
+
description: str
|
|
24
|
+
parameters: dict[str, Json]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AgentStepRecord(TypedDict):
|
|
28
|
+
thought: NotRequired[str]
|
|
29
|
+
tool: NotRequired[str]
|
|
30
|
+
args: NotRequired[dict[str, Json]]
|
|
31
|
+
observation: NotRequired[str]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AgentAction(TypedDict):
|
|
35
|
+
type: Literal["tool"] | Literal["final"]
|
|
36
|
+
thought: NotRequired[str]
|
|
37
|
+
tool: NotRequired[str]
|
|
38
|
+
args: NotRequired[dict[str, Json]]
|
|
39
|
+
answer: NotRequired[str]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PlanInput(TypedDict):
|
|
43
|
+
goal: str
|
|
44
|
+
context: NotRequired[str]
|
|
45
|
+
model: NotRequired[str]
|
|
46
|
+
ledger: NotRequired[str]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PlanResult(TypedDict):
|
|
50
|
+
steps: list[str]
|
|
51
|
+
model: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class StepInput(TypedDict):
|
|
55
|
+
goal: str
|
|
56
|
+
history: list[AgentStepRecord]
|
|
57
|
+
tools: list[AgentTool]
|
|
58
|
+
system: NotRequired[str]
|
|
59
|
+
model: NotRequired[str]
|
|
60
|
+
ledger: NotRequired[str]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class StepResult(AgentAction):
|
|
64
|
+
model: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ReflectInput(TypedDict):
|
|
68
|
+
goal: str
|
|
69
|
+
history: list[AgentStepRecord]
|
|
70
|
+
model: NotRequired[str]
|
|
71
|
+
ledger: NotRequired[str]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ReflectResult(TypedDict):
|
|
75
|
+
done: bool
|
|
76
|
+
critique: str
|
|
77
|
+
model: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class LoopInput(TypedDict):
|
|
81
|
+
goal: str
|
|
82
|
+
toolNames: NotRequired[list[str]]
|
|
83
|
+
maxSteps: NotRequired[float]
|
|
84
|
+
system: NotRequired[str]
|
|
85
|
+
model: NotRequired[str]
|
|
86
|
+
ledger: NotRequired[str]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class LoopResult(TypedDict):
|
|
90
|
+
answer: str
|
|
91
|
+
steps: list[AgentStepRecord]
|
|
92
|
+
stepCount: float
|
|
93
|
+
stoppedReason: Literal["final"] | Literal["maxSteps"]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class RunToolInput(TypedDict):
|
|
97
|
+
tool: str
|
|
98
|
+
args: NotRequired[dict[str, Json]]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class RunToolResult(TypedDict):
|
|
102
|
+
tool: str
|
|
103
|
+
output: str
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class CompressHistoryInput(TypedDict):
|
|
107
|
+
history: list[AgentStepRecord]
|
|
108
|
+
maxWords: NotRequired[float]
|
|
109
|
+
model: NotRequired[str]
|
|
110
|
+
ledger: NotRequired[str]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class CompressHistoryResult(TypedDict):
|
|
114
|
+
summary: str
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class AgentRoute(TypedDict):
|
|
118
|
+
name: str
|
|
119
|
+
description: str
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class RouteInput(TypedDict):
|
|
123
|
+
goal: str
|
|
124
|
+
routes: list[AgentRoute]
|
|
125
|
+
model: NotRequired[str]
|
|
126
|
+
ledger: NotRequired[str]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class RouteResult(TypedDict):
|
|
130
|
+
route: str
|
|
131
|
+
reasoning: NotRequired[str]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class MemoryInput(TypedDict):
|
|
135
|
+
op: Literal["get"] | Literal["set"]
|
|
136
|
+
key: str
|
|
137
|
+
value: NotRequired[str]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
type MemoryResult = MemoryGetResult | MemorySetResult
|