fix(eval): delegated python uri reads to host resolver

- Routed non-local URI reads through the session read tool.
- Preserved offset and limit as host line selectors.
- Covered artifact delegation with the shipped Python prelude.

Fixes #5353
This commit is contained in:
roboomp
2026-07-14 19:12:06 +00:00
parent edd959a381
commit 5d6fcff2f1
6 changed files with 91 additions and 5 deletions
+1
View File
@@ -32,6 +32,7 @@
### Fixed
- Fixed Python eval `read()` rejecting every non-`local://` URI before the host resolver could handle it; URI reads now delegate through the session `read` tool with `offset`/`limit` preserved as line selectors, including spilled `artifact://` output ([#5353](https://github.com/can1357/oh-my-pi/issues/5353)).
- Fixed `/tan` and `/fork` clones cold-missing the provider prompt cache: the per-turn supersede/useless-result prune rewrote the live context without persisting it, so file-based forks and resume rebuilt a divergent (un-pruned) prefix and re-wrote the entire cache
- Fixed `/tan` pinning the clone's prompt-cache key to the parent's session id instead of the parent's effective cache key, dropping shard affinity when the parent was itself a fork or tan
- Fixed inconsistent history rendering when toggling the display setting for compacted items
@@ -1,6 +1,30 @@
import { describe, expect, it } from "bun:test";
import { PYTHON_PRELUDE } from "../prelude";
const pythonPath = Bun.env.PYTHON ?? "python3";
async function runPrelude(
code: string,
env: Record<string, string>,
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const prelude = PYTHON_PRELUDE.replace(
"from __future__ import annotations",
"from __future__ import annotations\n__omp_display = lambda *args, **kwargs: None",
);
const script = `${prelude}\n${code}`;
const proc = Bun.spawn([pythonPath, "-c", script], {
stdout: "pipe",
stderr: "pipe",
env: { ...process.env, ...env },
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
return { stdout, stderr, exitCode };
}
describe("python prelude", () => {
it("exposes read(path, offset?, limit?) with positional optional args", () => {
// The eval docs advertise `read(path, offset?=1, limit?=None)`. A
@@ -17,6 +41,41 @@ describe("python prelude", () => {
expect(signature).toContain("limit");
});
it("delegates artifact URI reads through the host read tool with line selectors", async () => {
const requests: unknown[] = [];
const server = Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch: async request => {
requests.push(await request.json());
return Response.json({
ok: true,
value: { text: "artifact contents", details: { resolvedPath: "/tmp/21.txt" } },
});
},
});
try {
const result = await runPrelude(`print(read("artifact://21", 3, 2))`, {
PI_TOOL_BRIDGE_URL: server.url.toString(),
PI_TOOL_BRIDGE_TOKEN: "test-token",
PI_TOOL_BRIDGE_SESSION: "test-session",
});
expect(result).toEqual({ stdout: "artifact contents\n", stderr: "", exitCode: 0 });
expect(requests).toEqual([
{
session: "test-session",
run: null,
name: "read",
args: { path: "artifact://21:3-4" },
},
]);
} finally {
server.stop(true);
}
});
it("exposes isolation artifacts on the agent() handle node", () => {
// agent(..., handle=True) is the only escape hatch for
// recovering apply=False patch/branch/nested artifacts (the bare
+27 -1
View File
@@ -57,6 +57,29 @@ if "__omp_prelude_loaded__" not in globals():
_OMP_INTERNAL_URL_RE = re.compile(r"^([a-z][a-z0-9+.-]*)://(.*)$", re.IGNORECASE)
def _should_delegate_read(path: str | Path) -> bool:
return (
isinstance(path, str)
and _OMP_INTERNAL_URL_RE.match(path) is not None
and not path.lower().startswith("local://")
)
def _with_read_line_selector(path: str, offset: int, limit: int | None) -> str | None:
if offset <= 1 and limit is None:
return path
if limit is not None and limit <= 0:
return None
start = max(1, offset)
if limit is None:
return f"{path}:{start}-"
return f"{path}:{start}-{start + limit - 1}"
def _read_tool_text(path: str) -> str:
result = _bridge_call("read", {"path": path})
if isinstance(result, dict) and "text" in result:
return result["text"]
return result
def _resolve_omp_path(path: str | Path) -> Path:
"""Map a helper path to a real filesystem Path.
@@ -94,7 +117,10 @@ if "__omp_prelude_loaded__" not in globals():
return Path(resolved)
def read(path: str | Path, offset: int = 1, limit: int | None = None) -> str:
"""Read file contents. offset/limit are 1-indexed line numbers."""
"""Read file or read-tool URI contents. offset/limit are 1-indexed lines."""
if _should_delegate_read(path):
tool_path = _with_read_line_selector(path, offset, limit)
return "" if tool_path is None else _read_tool_text(tool_path)
p = _resolve_omp_path(path)
data = p.read_text(encoding="utf-8")
lines = data.splitlines(keepends=True)
@@ -1,5 +1,5 @@
<system-notice cause="fork">
The conversation above belongs to your parent session.
The conversation above belongs to your parent session.
You are a fork created solely to handle the user's request below.
Your parent agent is still working on the original task — that responsibility is
@@ -28,7 +28,7 @@ display(value) → None
print(value, ...) → None
Text output.
read(path, offset?=1, limit?=None) → str
File as text; offset/limit 1-indexed lines. Accepts `local://…`.
File/resource text; offset/limit = 1-indexed lines. `local://…` works everywhere; Python/JS also accept top-level `read` URI schemes.
write(path, content) → str
Write file (creates parents) → resolved path. `local://…` persists across turns/subagents.
env(key?=None, value?=None) → str | None | dict
@@ -114,7 +114,7 @@ describe("AgentSession per-turn prune persistence", () => {
const message = session.agent.state.messages.find(
candidate => candidate.role === "toolResult" && candidate.toolCallId === BIG_CALL_ID,
);
if (!message || message.role !== "toolResult" || !Array.isArray(message.content)) {
if (message?.role !== "toolResult" || !Array.isArray(message.content)) {
throw new Error("Expected the seeded tool result in live agent state");
}
const text = message.content.find(block => block.type === "text");
@@ -156,7 +156,7 @@ describe("AgentSession per-turn prune persistence", () => {
const rebuilt = reloaded
.buildSessionContext()
.messages.find(candidate => candidate.role === "toolResult" && candidate.toolCallId === BIG_CALL_ID);
if (!rebuilt || rebuilt.role !== "toolResult" || !Array.isArray(rebuilt.content)) {
if (rebuilt?.role !== "toolResult" || !Array.isArray(rebuilt.content)) {
throw new Error("Expected the seeded tool result in the from-disk rebuild");
}
const rebuiltText = rebuilt.content.find(block => block.type === "text");