diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index c6decbc..0441f3f 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -54,6 +54,7 @@ jobs: run: | mkdir -p _site cp site/index.html site/zh.html site/styles.css site/app.js site/language.js site/robots.txt site/sitemap.xml _site/ + cp site/agent-harness.html site/agent-harness.css site/agent-harness.js _site/ cp -r site/assets site/docs site/files _site/ - uses: actions/upload-pages-artifact@v4 if: github.event_name != 'pull_request' diff --git a/README.md b/README.md index e5d4d1f..2e0399b 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,9 @@ is overhead. +Watch all three in the [nine-second overview](https://simplejev.github.io/JevAny/assets/media/docs/jev-agent-harness.mp4), +or open the [agent harness demos](https://simplejev.github.io/JevAny/agent-harness.html). + Broader paired evaluations show that gains vary by task. The [full results](https://simplejev.github.io/JevAny/docs/reports-JevAny_Tech_Report_Agent_Harness_Appendix.html), [delegation protocol](https://simplejev.github.io/JevAny/docs/experiments-AGENT_HARNESS_FRONTIER_PROTOCOL.html), and [technical report](https://simplejev.github.io/JevAny/files/reports/JevAny_Tech_Report.pdf) describe diff --git a/README.zh-CN.md b/README.zh-CN.md index de5a6f4..46623cc 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -96,6 +96,9 @@ +三个案例也可在[九秒总览](https://simplejev.github.io/JevAny/assets/media/docs/jev-agent-harness.mp4)中连续观看,或打开 +[Agent harness 演示页](https://simplejev.github.io/JevAny/agent-harness.html)查看单独回放。 + 下表展示更多配对评测结果,收益随任务而变化。[完整结果](https://simplejev.github.io/JevAny/docs/reports-JevAny_Tech_Report_Agent_Harness_Appendix.html)、 [委托协议](https://simplejev.github.io/JevAny/docs/experiments-AGENT_HARNESS_FRONTIER_PROTOCOL.html)和 [技术报告](https://simplejev.github.io/JevAny/files/reports/JevAny_Tech_Report.pdf)说明了 Jev 适合处理哪些选择,以及何时交回 LLM。 diff --git a/docs/demos/jev-agent-harness.gif b/docs/demos/jev-agent-harness.gif index bbd666a..c7560a6 100644 Binary files a/docs/demos/jev-agent-harness.gif and b/docs/demos/jev-agent-harness.gif differ diff --git a/docs/demos/jev-decision-frozen-lake-v2.gif b/docs/demos/jev-decision-frozen-lake-v2.gif index 501b808..c1f28d6 100644 Binary files a/docs/demos/jev-decision-frozen-lake-v2.gif and b/docs/demos/jev-decision-frozen-lake-v2.gif differ diff --git a/docs/demos/jev-decision-terminal-v2.gif b/docs/demos/jev-decision-terminal-v2.gif index 511801f..30c1f01 100644 Binary files a/docs/demos/jev-decision-terminal-v2.gif and b/docs/demos/jev-decision-terminal-v2.gif differ diff --git a/docs/demos/jev-decision-webshop-v2.gif b/docs/demos/jev-decision-webshop-v2.gif index 26d09b5..d912dba 100644 Binary files a/docs/demos/jev-decision-webshop-v2.gif and b/docs/demos/jev-decision-webshop-v2.gif differ diff --git a/scripts/render_agent_decision_demos_v2.py b/scripts/render_agent_decision_demos_v2.py index 6bbe99b..09655d8 100644 --- a/scripts/render_agent_decision_demos_v2.py +++ b/scripts/render_agent_decision_demos_v2.py @@ -12,10 +12,13 @@ from functools import lru_cache import json from pathlib import Path +import shutil +import subprocess -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageChops, ImageColor, ImageDraw, ImageFont -from render_agent_harness_demo import ACCENT, BLUE, BORDER, MUTED, PANEL, PANEL_2, TEXT +from render_agent_harness_demo import ACCENT, AMBER, BLUE, BORDER, MUTED, PANEL, PANEL_2, TEXT +from render_agent_robot_icons import ROBOT_COLORS, actor_icon WIDTH, HEIGHT = 1080, 700 @@ -24,6 +27,13 @@ END_HOLD_MS = 600 BACKGROUND = "#070b14" GREEN_PANEL = "#0b2426" +BLUE_PANEL = "#14223a" +AMBER_PANEL = "#292313" +TASK_CONTEXT = { + "webshop": "z-dark green · small · under $80", + "frozen_lake": "Start [1, 0] → goal [0, 3]", + "sqlite": "Target: 10 verified rows", +} @lru_cache @@ -83,7 +93,125 @@ def text(draw: ImageDraw.ImageDraw, x: float, y: float, value: str, draw.text((x, y), value, font=font(size, bold, mono), fill=color) -def webshop_scene(draw: ImageDraw.ImageDraw, x: int, state: int, color: str) -> None: +def right_text(draw: ImageDraw.ImageDraw, right: float, y: float, value: str, + size: int = 18, color: str = MUTED, bold: bool = False) -> None: + text(draw, right - draw.textlength(value, font=font(size, bold)), y, + value, size, color, bold) + + +def actor_style(actor: str) -> tuple[str, str]: + return (ACCENT, GREEN_PANEL) if actor == "Jev" else ( + (BLUE, BLUE_PANEL) if actor == "LLM" else (AMBER, AMBER_PANEL) + ) + + +def wrapped_text(draw: ImageDraw.ImageDraw, x: int, y: int, value: str, + width: int, size: int = 17, color: str = MUTED) -> None: + lines = [""] + for word in value.split(): + candidate_line = f"{lines[-1]} {word}".strip() + if draw.textlength(candidate_line, font=font(size)) > width and lines[-1]: + lines.append(word) + else: + lines[-1] = candidate_line + for index, line in enumerate(lines): + text(draw, x, y + index * 20, line, size, color) + + +def phase_panel(image: Image.Image, draw: ImageDraw.ImageDraw, x: int, key: str, + step: Step, index: int, count: int, finished: bool, tick: int) -> None: + actor = ("Verifier" if key == "sqlite" else "Env") if finished else step.actor + color, fill = actor_style(actor) + draw.rounded_rectangle((x + 20, 414, x + 484, 533), 12, fill=PANEL_2) + draw.rounded_rectangle((x + 29, 423, x + 123, 524), 10, + fill=fill, outline=color, width=2) + portrait = actor_icon(actor, 78, phase=0 if finished else tick // 100 % 6) + image.paste(portrait, (x + 37, 424), portrait) + label = "CHECK" if actor == "Verifier" else "RESULT" if actor == "Env" else actor.upper() + center = x + 76 + text(draw, center - draw.textlength(label, font=font(13, True)) / 2, + 503, label, 13, color, True) + text(draw, x + 141, 429, + "VERIFIED RESULT" if finished else f"PHASE {index + 1} / {count}", 13, MUTED) + title = "✓ Task complete" if finished else step.title + text(draw, x + 141, 452, title, 24, ACCENT if finished else TEXT, True) + outcome = {"webshop": "Purchased, reward 1", "frozen_lake": "Goal reached, reward 1", + "sqlite": "10 rows recovered, reward 1"}[key] + wrapped_text(draw, x + 141, 491, outcome if finished else step.detail, 322) + + +def actor_rail(image: Image.Image, draw: ImageDraw.ImageDraw, x: int, steps: list[Step], + current: int, finished: bool, tick: int) -> None: + # Each controller keeps one position, including when it regains control. + actors = list(dict.fromkeys(step.actor for step in steps)) + gap = 8 + width = (460 - gap * (len(actors) - 1)) / len(actors) + for index, actor in enumerate(actors): + left = x + 22 + index * (width + gap) + color, fill = actor_style(actor) + active = not finished and steps[current].actor == actor + draw.rounded_rectangle((left, 546, left + width, 589), 9, + fill=fill if active else PANEL, + outline=color if active else BORDER, width=2 if active else 1) + portrait = actor_icon(actor, 36, active, tick // 100 % 6 if active else 0) + image.paste(portrait, (round(left + 10), 548), portrait) + text(draw, left + 55, 557, actor.upper(), 15, color if active else MUTED, True) + if active: + draw.ellipse((left + width - 18, 564, left + width - 12, 570), fill=color) + text(draw, left + width - 69, 561, "ACTIVE", 9, color, True) + text(draw, x + 22, 594, "TASK COMPLETE" if finished else "ACTIVE CONTROLLER", 10, MUTED) + + +def savings_strip(draw: ImageDraw.ImageDraw, baseline: dict, assisted: dict) -> None: + # Static totals from this recorded pair; percentages use the baseline. + tokens_saved = baseline["tokens"] - assisted["tokens"] + time_saved = baseline["wall_time_seconds"] - assisted["wall_time_seconds"] + rewards = baseline["reward"], assisted["reward"] + both_succeeded = all(reward == 1 for reward in rewards) + outcome = "Both succeeded" if both_succeeded else ( + "Same reward" if rewards[0] == rewards[1] else "Reward changed" + ) + cards = [ + ("LLM TOKENS SAVED", f"{tokens_saved:,}", + f"{tokens_saved / baseline['tokens']:.1%}", + f"{baseline['tokens']:,} → {assisted['tokens']:,}", ACCENT), + ("TIME SAVED", f"{time_saved:g} s", + f"{time_saved / baseline['wall_time_seconds']:.1%}", + f"{baseline['wall_time_seconds']:g} s → {assisted['wall_time_seconds']:g} s", ACCENT), + ("TASK RESULT", outcome, "✓" if both_succeeded else "", + f"Reward {rewards[0]:g} → {rewards[1]:g}", AMBER), + ] + for index, (label, value, badge, detail, color) in enumerate(cards): + x = 24 + index * 348 + metric = index < 2 + draw.rounded_rectangle((x, 616, x + 336, 676), 10, + fill=GREEN_PANEL if metric else PANEL, + outline=ACCENT if metric else BORDER) + text(draw, x + 14, 619, label, 14, MUTED, True) + if metric: + text(draw, x + 14, 631, badge, 42, color, True) + right_text(draw, x + 322, 620, value, 28, TEXT, True) + right_text(draw, x + 322, 651, detail, 15) + else: + text(draw, x + 14, 632, value, 26, TEXT, True) + right_text(draw, x + 322, 637, badge, 24, color, True) + text(draw, x + 14, 657, detail, 15, MUTED) + + +def candidate(draw: ImageDraw.ImageDraw, x: int, y: int, width: int, + label: str, selected: bool, color: str = ACCENT) -> None: + fill = BLUE_PANEL if color == BLUE else GREEN_PANEL + draw.rounded_rectangle((x, y, x + width, y + 41), 8, + fill=fill if selected else PANEL_2, + outline=color if selected else BORDER, width=2) + text(draw, x + 13, y + 7, label, 21, color if selected else MUTED, selected) + if selected: + text(draw, x + width - 32, y + 5, "✓", 23, color, True) + + +def webshop_scene(draw: ImageDraw.ImageDraw, x: int, trace: dict, step: Step, + assisted: bool, finished: bool, color: str) -> None: + state = step.state if state == 0: text(draw, x + 22, 220, "SEARCH", 22, MUTED, True) draw.rounded_rectangle((x + 20, 264, x + 444, 324), 12, fill=PANEL_2, outline=BORDER) @@ -99,10 +227,20 @@ def webshop_scene(draw: ImageDraw.ImageDraw, x: int, state: int, color: str) -> ] draw.polygon(jacket, fill="#277464" if state >= 2 else "#3d4e67") draw.line((x + 93, 222, x + 91, 357), fill="#9fbab5", width=3) + if assisted and step.actor == "Jev" and not finished: + decision = trace["candidate_groups"][state - 2] + kind = "color" if state == 2 else "size" + text(draw, x + 179, 216, f"LLM {kind} menu", 24, BLUE, True) + for index, action in enumerate(decision["actions"]): + candidate(draw, x + 176, 256 + index * 47, 265, + action.removeprefix("click[").removesuffix("]"), + action == decision["selected"]) + text(draw, x + 29, 377, "3 alternatives", 17, MUTED) + return text(draw, x + 179, 216, "Women's blazer", 26, bold=True) for y, label, selected in ((265, "z-dark green", state >= 2), (327, "small", state >= 3)): draw.rounded_rectangle((x + 176, y, x + 441, y + 48), 10, - fill=GREEN_PANEL if selected else PANEL_2, + fill=(GREEN_PANEL if assisted else BLUE_PANEL) if selected else PANEL_2, outline=color if selected else BORDER, width=2) text(draw, x + 192, y + 9, label, 24, color if selected else MUTED) if selected: @@ -110,7 +248,7 @@ def webshop_scene(draw: ImageDraw.ImageDraw, x: int, state: int, color: str) -> def lake_scene(draw: ImageDraw.ImageDraw, x: int, trace: dict, step: Step, - fraction: float, finished: bool, color: str) -> None: + fraction: float, finished: bool, color: str, assisted: bool) -> None: positions = [tuple(s["player"]) for s in trace["steps"]] + [(0, 3)] state = step.state player = positions[state] @@ -129,28 +267,31 @@ def lake_scene(draw: ImageDraw.ImageDraw, x: int, trace: dict, step: Step, text(draw, left + 10, top + 4, "G" if goal else "×", 23, ACCENT if goal else "#fb7185", True) cy, cx = gy + player[0] * cell + 19, gx + player[1] * cell + 19 draw.ellipse((cx - 15, cy - 15, cx + 15, cy + 15), fill=color) - text(draw, x + 241, 216, "4 directions", 24, MUTED) + text(draw, x + 241, 216, "1 LLM plan" if assisted else "4 directions", + 24, BLUE if assisted else MUTED) for i, action in enumerate(trace["action_menu"]): left, top = x + 238 + (i % 2) * 105, 262 + (i // 2) * 57 selected = state > 0 and action == trace["steps"][state - 1]["action"] draw.rounded_rectangle((left, top, left + 96, top + 44), 9, - fill=GREEN_PANEL if selected else PANEL_2, + fill=(GREEN_PANEL if assisted else BLUE_PANEL) if selected else PANEL_2, outline=color if selected else BORDER, width=2) text(draw, left + 12, top + 8, action, 23, color if selected else MUTED, selected) -def terminal_scene(draw: ImageDraw.ImageDraw, x: int, state: int, +def terminal_scene(draw: ImageDraw.ImageDraw, x: int, trace: dict, state: int, assisted: bool, finished: bool) -> None: draw.rounded_rectangle((x + 20, 207, x + 444, 393), 12, fill="#080e1b", outline=BORDER) + if assisted and state < 2: + text(draw, x + 38, 220, "trunc.db · 3 inspections", 19, MUTED, mono=True) + for index, option in enumerate(trace["options"]): + candidate(draw, x + 32, 250 + index * 46, 398, option["name"], + state == 1 and option["name"] == trace["jev_choice"]) + return text(draw, x + 38, 220, "trunc.db", 23, MUTED, mono=True) if state == 0: - lines = ("raw page | schema | tools", "Jev chooses the inspection") if assisted else ( - "damaged SQLite page", "LLM chooses inspections", - ) + lines = ("damaged SQLite page", "LLM chooses inspections") elif state == 1: - lines = ("$ od ... trunc.db", "0d 00 00 00 0a 0f 49 ...") if assisted else ( - "inspect page bytes", "interpret page structure", - ) + lines = ("inspect page bytes", "interpret page structure") elif state == 2: lines = ("cell pointers → records", "parse recoverable rows") else: @@ -184,6 +325,7 @@ def render_frame(key: str, trace: dict, tick: int) -> Image.Image: image = Image.new("RGB", (WIDTH, HEIGHT), BACKGROUND) draw = ImageDraw.Draw(image) text(draw, 28, 21, titles[key], 28, bold=True) + right_text(draw, WIDTH - 28, 30, TASK_CONTEXT[key], 17) for lane, run in enumerate((baseline, assisted)): x = 24 + lane * 528 color = ACCENT if lane else BLUE @@ -198,43 +340,36 @@ def render_frame(key: str, trace: dict, tick: int) -> Image.Image: draw.rounded_rectangle((x, 73, x + 504, 608), 18, fill=PANEL, outline=color, width=2) text(draw, x + 22, 90, "LLM + Jev" if lane else "LLM only", 30, color, True) - text(draw, x + 22, 131, f"{elapsed:.2f} s" if key == "webshop" else f"{elapsed:.1f} s", 40, bold=True) - text(draw, x + 280, 148, "Completed" if finished else "Running…", 28, ACCENT if finished else MUTED, True) + calls = str(run["llm_calls"]) + text(draw, x + 22, 124, calls, 44, color, True) + label_x = x + 34 + draw.textlength(calls, font=font(44, True)) + text(draw, label_x, 131, "RECORDED TOTAL", 10, MUTED) + text(draw, label_x, 149, "LLM call" if run["llm_calls"] == 1 else "LLM calls", 21, TEXT) + text(draw, x + 300, 105, "COMPLETED" if finished else "ELAPSED TIME", + 11, ACCENT if finished else MUTED, True) + draw.rounded_rectangle((x + 300, 125, x + 484, 188), 10, + fill=BACKGROUND, outline=BORDER) + text(draw, x + 314, 133, + f"{elapsed:.2f} s" if key == "webshop" else f"{elapsed:.1f} s", 29, TEXT, True) + draw.rounded_rectangle((x + 314, 178, x + 470, 182), 2, fill=BORDER) + length = round(156 * progress) + if length: + draw.rounded_rectangle((x + 314, 178, x + 314 + length, 182), 2, fill=color) inner = x + 20 if key == "webshop": - webshop_scene(draw, inner, step.state, color) + webshop_scene(draw, inner, trace, step, bool(lane), finished, color) elif key == "frozen_lake": - lake_scene(draw, inner, trace, step, fraction, finished, color) + lake_scene(draw, inner, trace, step, fraction, finished, color, bool(lane)) else: - terminal_scene(draw, inner, step.state, bool(lane), finished) - - draw.rounded_rectangle((x + 20, 414, x + 484, 533), 12, - fill=GREEN_PANEL if finished else PANEL_2) - if finished: - text(draw, x + 36, 427, "✓ Completed", 32, ACCENT, True) - outcome = {"webshop": "Purchased, reward 1", "frozen_lake": "Goal reached, reward 1", - "sqlite": "10 rows recovered, reward 1"}[key] - text(draw, x + 36, 480, outcome, 25) - else: - actor_color = ACCENT if step.actor == "Jev" else BLUE - text(draw, x + 36, 425, f"{step.actor}: {step.title}", 26, actor_color, True) - text(draw, x + 36, 477, step.detail, 24, MUTED) - draw.rounded_rectangle((x + 22, 551, x + 482, 560), 4, fill=BORDER) - length = round(460 * progress) - if length: - draw.rounded_rectangle((x + 22, 551, x + 22 + length, 560), 4, fill=color) - text(draw, x + 22, 577, - f"{run['steps']} {run['step_label']} · {run['llm_calls']} LLM calls · {run['tokens']:,} tokens", - 20, MUTED) - saved = baseline["wall_time_seconds"] - assisted["wall_time_seconds"] - step_saved = baseline["steps"] - assisted["steps"] - call_saved = baseline["llm_calls"] - assisted["llm_calls"] - token_saved = baseline["tokens"] - assisted["tokens"] - text(draw, 28, 626, - f"Saved: {step_saved} {baseline['step_label']} · {call_saved} LLM calls · " - f"{token_saved:,} tokens · {saved:.2f} s", - 25, ACCENT, True) - text(draw, 28, 668, f"Illustrated steps; measured totals at {longest * 1000 / PLAY_MS:.2f}× playback", 23, MUTED) + terminal_scene(draw, inner, trace, step.state, bool(lane), finished) + + phase_panel(image, draw, x, key, step, index, len(steps), finished, tick) + actor_rail(image, draw, x, steps, index, finished, tick) + savings_strip(draw, baseline, assisted) + text(draw, 28, 681, + f"One recorded pair · illustrated steps · measured totals at {longest * 1000 / PLAY_MS:.2f}× playback", + 13, MUTED) + right_text(draw, WIDTH - 28, 681, "LLM only → LLM + Jev", 13) return image @@ -254,10 +389,29 @@ def save(path: str, key: str, trace: dict) -> None: for i, sample in enumerate(samples): sheet.paste(sample, (0, 175 * i)) palette = sheet.quantize(colors=128) - frames = [ - render_frame(key, trace, tick).quantize(palette=palette, dither=Image.Dither.NONE) - for tick in ticks - ] + # Keep controller and portrait colors exact even when small art is undersampled. + # Remaining entries retain the sampled colors for antialiased text and art. + theme = (BACKGROUND, PANEL, PANEL_2, BORDER, TEXT, MUTED, ACCENT, BLUE, + AMBER, GREEN_PANEL, BLUE_PANEL, AMBER_PANEL) + ROBOT_COLORS + colors = palette.getpalette()[:128 * 3] + colors += [channel for color in theme for channel in ImageColor.getrgb(color)] + colors += list(ImageColor.getrgb(BACKGROUND)) * ((768 - len(colors)) // 3) + palette.putpalette(colors) + frames = [] + for tick in ticks: + original = render_frame(key, trace, tick) + frame = original.quantize(palette=palette, dither=Image.Dither.NONE) + # Quantization can reuse a nearby swatch even for an exact palette color. + # Pin flat theme pixels; retain sampled colors for antialiased edges. + channels = original.split() + for index, color in enumerate(theme, start=128): + masks = [ + channel.point([255 if value == target else 0 for value in range(256)]) + for channel, target in zip(channels, ImageColor.getrgb(color)) + ] + mask = ImageChops.multiply(ImageChops.multiply(masks[0], masks[1]), masks[2]) + frame.paste(index, mask=mask) + frames.append(frame) output = Path(path) output.parent.mkdir(parents=True, exist_ok=True) frames[0].save(output, save_all=True, append_images=frames[1:], duration=durations, @@ -267,17 +421,80 @@ def save(path: str, key: str, trace: dict) -> None: f"{output.stat().st_size / 1024:.0f} KiB") +def save_overview(path: str, sources: list[str]) -> None: + """Join the three approved loops without changing their pixels or timing.""" + frames = [] + durations = [] + for source in sources: + with Image.open(source) as clip: + if clip.size != (WIDTH, HEIGHT): + raise ValueError(f"{source}: expected a {WIDTH}×{HEIGHT} replay") + for index in range(clip.n_frames): + clip.seek(index) + original = clip.convert("RGB") + frame = original.quantize(colors=256, dither=Image.Dither.NONE) + if ImageChops.difference(original, frame.convert("RGB")).getbbox(): + raise ValueError(f"{source}: frame {index} cannot be preserved exactly") + frames.append(frame) + durations.append(clip.info["duration"]) + output = Path(path) + output.parent.mkdir(parents=True, exist_ok=True) + frames[0].save(output, save_all=True, append_images=frames[1:], duration=durations, + loop=0, optimize=False, disposal=1) + print(f"{output}: {len(frames)} frames, {sum(durations) / 1000:.1f}s, " + f"{output.stat().st_size / 1024:.0f} KiB") + + +def save_site_media(sources: list[str]) -> None: + """Export only this presentation's videos and posters at native resolution.""" + executable = shutil.which("ffmpeg") + if not executable: + try: + from imageio_ffmpeg import get_ffmpeg_exe + except ImportError as error: + raise SystemExit("Install ffmpeg or imageio-ffmpeg to export site media.") from error + executable = get_ffmpeg_exe() + directory = Path(__file__).resolve().parents[1] / "site/assets/media/docs" + directory.mkdir(parents=True, exist_ok=True) + for source in sources: + name = Path(source).stem + common = [ + executable, "-nostdin", "-hide_banner", "-loglevel", "error", "-y", + "-ignore_loop", "1", "-i", source, "-an", "-vf", "setsar=1,fps=25", + "-pix_fmt", "yuv420p", "-threads", "2", + ] + subprocess.run(common + [ + "-c:v", "libx264", "-crf", "26", "-preset", "fast", + "-movflags", "+faststart", str(directory / f"{name}.mp4"), + ], check=True) + subprocess.run(common + [ + "-c:v", "libvpx-vp9", "-crf", "36", "-b:v", "0", "-cpu-used", "3", + "-row-mt", "1", str(directory / f"{name}.webm"), + ], check=True) + with Image.open(source) as image: + image.seek(min(12, image.n_frames - 1)) + image.convert("RGB").save(directory / f"{name}.webp", quality=88, method=6) + print(f"Prepared site media: {name}") + + def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--trace", default="docs/demos/jev-agent-harness-traces.json") - parser.add_argument("--terminal-out", default="docs/demos/jev-decision-terminal-v2.gif") + parser.add_argument("--terminal-out", "--sqlite-out", default="docs/demos/jev-decision-terminal-v2.gif") parser.add_argument("--frozen-lake-out", default="docs/demos/jev-decision-frozen-lake-v2.gif") parser.add_argument("--webshop-out", default="docs/demos/jev-decision-webshop-v2.gif") + parser.add_argument("--overview-out", "--out", default="docs/demos/jev-agent-harness.gif") + parser.add_argument("--site-media", action="store_true", + help="Also export MP4, WebM, and WebP for these four GIFs") args = parser.parse_args() trace = json.loads(Path(args.trace).read_text(encoding="utf-8")) for key, output in (("webshop", args.webshop_out), ("frozen_lake", args.frozen_lake_out), ("sqlite", args.terminal_out)): save(output, key, trace[key]) + clips = [args.webshop_out, args.frozen_lake_out, args.terminal_out] + save_overview(args.overview_out, clips) + if args.site_media: + save_site_media([args.overview_out, *clips]) if __name__ == "__main__": diff --git a/scripts/render_agent_harness_demo.py b/scripts/render_agent_harness_demo.py index 6c76a5d..17e02f3 100644 --- a/scripts/render_agent_harness_demo.py +++ b/scripts/render_agent_harness_demo.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Render recorded Terminal-Bench, WebShop, and FrozenLake harness traces.""" +"""Shared theme and legacy helpers; the CLI exports the current paired demos.""" from __future__ import annotations @@ -444,7 +444,7 @@ def summary_frame() -> Image.Image: return image -def main() -> None: +def legacy_main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--out", default="docs/demos/jev-agent-harness.gif") parser.add_argument("--sqlite-out", default="docs/demos/jev-agent-harness-sqlite.gif") @@ -563,5 +563,11 @@ def save(output_name: str, frames: list[Image.Image], durations: list[int]) -> N ) +def main() -> None: + from render_agent_decision_demos_v2 import main as render_current_demos + + render_current_demos() + + if __name__ == "__main__": main() diff --git a/scripts/render_agent_robot_icons.py b/scripts/render_agent_robot_icons.py new file mode 100644 index 0000000..76c811e --- /dev/null +++ b/scripts/render_agent_robot_icons.py @@ -0,0 +1,127 @@ +"""Small, deterministic robot portraits for the recorded agent demos.""" + +from __future__ import annotations + +from functools import lru_cache + +from PIL import Image, ImageColor, ImageDraw + +from render_agent_harness_demo import ACCENT, AMBER, BLUE + + +PORTRAIT_STYLES = { + "LLM": ("#c3dcff", "#4674a9", "#10233f"), + "Jev": ("#a9f0df", "#237768", "#092d2b"), + "idle": ("#43516a", "#334158", "#121b2c"), +} +IDLE_EDGE, EYE_LIGHT, IDLE_LIGHT = "#52617a", "#f1fbff", "#8290a6" +ROBOT_COLORS = tuple(color for style in PORTRAIT_STYLES.values() for color in style) + ( + IDLE_EDGE, EYE_LIGHT, IDLE_LIGHT, +) + + +@lru_cache(maxsize=128) +def actor_icon(actor: str, size: int, active: bool = True, phase: int = 0) -> Image.Image: + """Return an antialiased portrait; motion is decorative, not measured time.""" + image = Image.new("RGBA", (size * 3, size * 3)) + draw = ImageDraw.Draw(image) + scale = size * 3 / 100 + accent = BLUE if actor == "LLM" else ACCENT if actor == "Jev" else AMBER + edge = accent if active else IDLE_EDGE + shell, joint, face = PORTRAIT_STYLES[ + "idle" if not active else "LLM" if actor == "LLM" else "Jev" + ] + light = EYE_LIGHT if active else IDLE_LIGHT + phase = phase % 6 if active else 0 + + def coords(values: tuple | list) -> tuple: + return tuple(round(value * scale) for value in values) + + def rgba(color: str, alpha: int) -> tuple[int, int, int, int]: + return (*ImageColor.getrgb(color), alpha) + + def line(points: tuple | list, fill: str | tuple, width: float = 2) -> None: + draw.line(coords(points), fill=fill, width=max(1, round(width * scale)), + joint="curve") + + def rect(box: tuple, radius: float, fill: str | tuple, + outline: str | tuple | None = None, width: float = 1.5) -> None: + draw.rounded_rectangle(coords(box), round(radius * scale), fill=fill, + outline=outline, width=max(1, round(width * scale))) + + def ellipse(box: tuple, fill: str | tuple, outline: str | tuple | None = None, + width: float = 1) -> None: + draw.ellipse(coords(box), fill=fill, outline=outline, + width=max(1, round(width * scale))) + + def polygon(points: list[tuple], fill: str | tuple) -> None: + draw.polygon([coords(point) for point in points], fill=fill) + + if active: + ellipse((7, 13, 95, 98), rgba(accent, 13)) + ellipse((15, 22, 89, 94), rgba(accent, 14)) + ellipse((24, 88, 80, 95), rgba(accent if active else edge, 35)) + + if actor == "LLM": + # Broad shell, stable stance, and three small planning lights. + rect((30, 68, 73, 85), 8, joint, edge) + rect((32, 82, 45, 90), 4, shell, edge) + rect((59, 82, 72, 90), 4, shell, edge) + rect((20, 69, 30, 80), 4, joint, edge) + rect((74, 69, 84, 80), 4, joint, edge) + rect((11, 42, 21, 59), 4, joint, edge) + rect((82, 42, 92, 59), 4, joint, edge) + line((51, 18, 51, 28), edge, 3) + ellipse((47, 11, 55, 19), shell, edge) + rect((19, 26, 84, 73), 15, shell, edge, 2) + rect((26, 36, 77, 65), 10, face) + line((36, 31, 65, 31), rgba(light, 190), 2) + rect((36, 44, 42, 55), 3, edge) + rect((61, 44, 67, 55), 3, edge) + ellipse((37, 45, 40, 48), light) + ellipse((62, 45, 65, 48), light) + rect((46, 59, 57, 61), 1, rgba(edge, 150)) + for index in range(3): + left = 42 + index * 8 + ellipse((left, 76, left + 4, 80), edge if active else joint) + if active: + line((66, 20, 75, 11, 87, 17), rgba(accent, 100), 1) + for index, (cx, cy) in enumerate(((66, 20), (75, 11), (87, 17))): + focused = index == phase // 2 + radius = 3 if focused else 2 + ellipse((cx - radius, cy - radius, cx + radius, cy + radius), + light if focused else accent) + elif actor == "Jev": + # Compact shell, forward stance, and short moving trails. + if active: + for index, y in enumerate((43, 55, 67)): + shift = (phase * 3 + index * 4) % 10 + line((3 + shift, y, 23 + shift // 2, y), + rgba(accent, 115 + index * 45), 2) + rect((42, 66, 77, 80), 7, joint, edge) + polygon([(43, 77), (53, 79), (44, 88), (31, 88)], shell) + polygon([(66, 78), (75, 75), (87, 86), (77, 89)], shell) + line((34, 89, 44, 89), edge, 2) + line((77, 89, 87, 86), edge, 2) + rect((27, 59, 39, 68), 4, joint, edge) + rect((82, 56, 92, 64), 4, joint, edge) + rect((32, 31, 87, 69), 12, shell, edge, 2) + line((45, 35, 74, 32), rgba(light, 190), 2) + rect((40, 42, 83, 62), 8, face) + rect((48, 47, 54, 55), 3, edge) + rect((68, 46, 74, 54), 3, edge) + ellipse((49, 47, 52, 49), light) + ellipse((69, 46, 72, 48), light) + polygon([(65, 12), (54, 29), (63, 28), (59, 39), + (77, 20), (67, 21), (73, 12)], edge) + polygon([(60, 69), (54, 76), (59, 76), (56, 81), + (66, 73), (61, 73)], light) + else: + # Environment feedback and verification share the amber result symbol. + polygon([(50, 17), (82, 29), (78, 60), (67, 78), (50, 90), + (31, 78), (21, 60), (18, 29)], rgba(accent, 40)) + line((50, 17, 82, 29, 78, 60, 67, 78, 50, 90, + 31, 78, 21, 60, 18, 29, 50, 17), edge, 2) + line((33, 51, 46, 64, 69, 38), light, 6) + + return image.resize((size, size), Image.Resampling.LANCZOS) diff --git a/site/README.md b/site/README.md index fb6aa55..bfedbc0 100644 --- a/site/README.md +++ b/site/README.md @@ -25,6 +25,60 @@ released checkpoints and the supported-model catalog. Guides, the technical report and linked reference files are hosted on the site. GitHub remains an explicit source-code link; checkpoint download links point to Hugging Face. +The README and `agent-harness.html` use the same paired comparisons from +`scripts/render_agent_decision_demos_v2.py`. Three individual GIFs show WebShop, +FrozenLake, and SQLite recovery. `docs/demos/jev-agent-harness.gif` joins those +replays in that order into a nine-second overview, preserving every frame and +its timing. The gallery uses matching MP4/WebM videos and WebP posters. + +Both lanes retain the measured completion-time ratio within each three-second loop. +Recorded LLM call totals are prominent alongside each lane's clock. Task +constraints stay visible; large actor panels identify the current phase, +and recorded menus highlight the selected option. The footer shows LLM tokens +saved, wall time saved, and the task result for this recorded pair. Large +highlighted percentages lead the two savings cards, with absolute savings and +original totals alongside them. Percentages use the LLM-only baseline. These +are full-run measurements for one recorded pair. +A fixed role bar shows each controller once and highlights only +the current controller. When control returns to the LLM, the same LLM position +lights up again. Both roles dim when the task completes, and completed lanes +remain frozen. + +Shared robot portraits in `scripts/render_agent_robot_icons.py` distinguish the +blue LLM from the smaller teal Jev. Planning lights and short motion trails +animate only on the active controller; inactive portraits stay gray and still. +These decorative animations do not represent measured inference time. Amber +result symbols mark completion. Icons are drawn and antialiased locally with +Pillow, so all three cases use the same artwork without external image assets. + +`agent-harness.html` leads with the overview, followed by the three individual +replays and a short explanation of each delegation: choosing within LLM-provided +menus, retaining one plan across local moves, and choosing useful evidence. +It uses the same navy, blue, teal, and amber palette as the GIFs. +Playback controls provide pause, seek, and fullscreen beneath the frame, +keeping the savings visible even when paused. Native controls remain available +when JavaScript is disabled. Only the overview +starts automatically; videos pause offscreen and when the tab is hidden, and +only one plays at a time. Reduced-motion preferences disable automatic playback. +A manual pause or selection takes precedence over automatic playback. + +The gallery links to the recorded traces, delegation protocol, and full results. +WebShop uses seed 3107 and Jev 4B from the D4 supplement; SQLite and FrozenLake +keep their recorded cases. Savings describe one recorded pair, with the broader +D2 matrix, separate D4 supplement, fallback cases, and uncertainty documented +in the report. Rendering uses existing recordings and runs no model or environment. + +Regenerate all four GIFs and their site media from the repository root: + +```bash +python3 -m pip install pillow imageio-ffmpeg +python3 scripts/render_agent_decision_demos_v2.py --site-media +``` + +Omit `--site-media` to export only GIFs. Video conversion uses ffmpeg; +imageio-ffmpeg is optional when ffmpeg is already on PATH. The legacy command +`python3 scripts/render_agent_harness_demo.py` also exports this presentation. + Regenerate the homepage's data sections and documentation from the repository: ```bash diff --git a/site/agent-harness.css b/site/agent-harness.css new file mode 100644 index 0000000..e69d5ad --- /dev/null +++ b/site/agent-harness.css @@ -0,0 +1,89 @@ +@font-face { + font-family: "DM Sans"; + src: url("assets/fonts/dm-sans-latin.woff2") format("woff2"); + font-weight: 100 1000; + font-display: swap; +} + +:root { + color-scheme: dark; + --background: #070b14; + --panel: #11182a; + --border: #35415d; + --text: #f7f8ff; + --muted: #9ca8c2; + --llm: #60a5fa; + --jev: #2dd4bf; + --result: #f59e0b; +} + +* { box-sizing: border-box; } +html { scroll-padding-top: 24px; } +body { + margin: 0; + background: var(--background); + color: var(--text); + font-family: "DM Sans", sans-serif; + line-height: 1.6; +} +a { color: var(--llm); text-underline-offset: 4px; } +a:focus-visible { outline: 2px solid var(--llm); outline-offset: 5px; } +.wrap { width: min(100% - 64px, 1328px); margin-inline: auto; } +.skip-link { position: absolute; top: -80px; left: 24px; padding: 10px 16px; background: var(--panel); z-index: 2; } +.skip-link:focus { top: 12px; } +.page-header, .page-footer { display: flex; align-items: center; justify-content: space-between; gap: 24px; } +.page-header { padding-block: 24px; border-bottom: 1px solid var(--border); } +.brand { display: inline-flex; align-items: center; gap: 10px; color: var(--text); font-size: 21px; font-weight: 750; text-decoration: none; } +nav, .evidence-links { display: flex; gap: 24px; flex-wrap: wrap; } +nav { font-size: 14px; } +.intro { padding-block: 48px 36px; } +.eyebrow { margin: 0 0 10px; color: var(--jev); font-size: 12px; font-weight: 650; letter-spacing: 1.3px; } +h1, h2, h3 { line-height: 1.1; letter-spacing: -.035em; } +h1 { margin: 10px 0 20px; font-size: clamp(36px, 5vw, 62px); font-weight: 650; } +h2 { margin: 0; font-size: clamp(26px, 3vw, 36px); font-weight: 600; } +h3 { margin: 12px 0; font-size: 28px; font-weight: 600; } +.lead { max-width: 720px; color: var(--muted); font-size: 19px; } +.role-key { display: flex; flex-wrap: wrap; gap: 24px; margin-top: 24px; color: var(--muted); font-size: 14px; } +.role-key span { display: inline-flex; align-items: center; gap: 8px; } +.role-key i { display: inline-block; width: 8px; height: 8px; border-radius: 50%; } +.role-key .llm { background: var(--llm); } +.role-key .jev { background: var(--jev); } +.role-key .result { background: var(--result); } +.section-heading { display: flex; align-items: end; justify-content: space-between; flex-wrap: wrap; gap: 20px; margin-bottom: 24px; } +.section-heading > a { font-size: 14px; } +.overview { padding-block: 24px 48px; border-bottom: 1px solid var(--border); } +figure { max-width: 1080px; margin: 0; } +.harness-video { display: block; width: 100%; height: auto; aspect-ratio: 1080 / 700; border-radius: 10px; background: var(--background); } +.player { min-width: 0; } +.playback-controls { display: grid; grid-template-columns: 66px minmax(0, 1fr) auto auto; align-items: center; gap: 12px; margin-top: 12px; } +.playback-controls button { min-height: 36px; padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--panel); color: var(--text); font: inherit; font-size: 13px; cursor: pointer; } +.playback-controls button:hover { border-color: var(--llm); } +.playback-controls button:focus-visible, .playback-controls input:focus-visible { outline: 2px solid var(--llm); outline-offset: 4px; } +.playback-controls input { width: 100%; margin: 0; accent-color: var(--jev); cursor: pointer; } +.playback-time { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; white-space: nowrap; } +.player:fullscreen { display: flex; flex-direction: column; justify-content: center; padding: 24px; background: var(--background); } +.player:fullscreen .harness-video { max-height: calc(100dvh - 108px); object-fit: contain; } +figcaption { margin-top: 16px; color: var(--muted); font-size: 14px; } +.cases { padding-top: 48px; } +.case { display: grid; grid-template-columns: minmax(0, 2.4fr) minmax(230px, 1fr); align-items: center; gap: 36px; padding-block: 36px; border-bottom: 1px solid var(--border); } +.case-copy p { color: var(--muted); font-size: 16px; } +.case-copy .eyebrow { color: var(--jev); font-size: 12px; } +.case-copy .scope { margin-block: 20px; font-size: 13px; } +.case-copy a { font-size: 14px; } +.evidence { max-width: 860px; padding-block: 52px; } +.evidence p:not(.eyebrow) { color: var(--muted); } +.evidence-links { margin-top: 24px; font-size: 14px; } +.page-footer { padding-block: 24px; border-top: 1px solid var(--border); color: var(--muted); font-size: 13px; } + +@media (max-width: 800px) { + .wrap { width: calc(100% - 32px); } + .page-header { align-items: start; } + nav { justify-content: end; gap: 8px 16px; max-width: 220px; font-size: 13px; } + .intro { padding-block: 32px 24px; } + .lead { font-size: 17px; } + .case { grid-template-columns: minmax(0, 1fr); gap: 16px; padding-block: 28px; } + .case-copy .scope { margin-block: 14px; } + .overview { padding-bottom: 32px; } + .cases { padding-top: 32px; } + .evidence { padding-block: 36px; } +} diff --git a/site/agent-harness.html b/site/agent-harness.html new file mode 100644 index 0000000..ca63068 --- /dev/null +++ b/site/agent-harness.html @@ -0,0 +1,123 @@ + + + + + + + + Agent harness demos — JevAny + + + + + + + +
+
+

JEVANY / AGENT HARNESS

+

The same task.
Fewer LLM calls.

+

Watch the LLM and Jev take turns. Compare recorded calls, token use, completion time, and the task outcome.

+
+ LLM + Jev + Task result +
+
+ +
+
+

THE OVERVIEW

Three tasks. One replay.

+ Download overview video +
+
+ +
WebShop → FrozenLake → SQLite. Three seconds per case, using the same replays shown below.
+
+
+ +
+
+

THE INDIVIDUAL DEMOS

See where Jev takes over.

+
+ +
+ +
+

01 / WEBSHOP

+

Bound the choices.

+

The LLM provides two menus. Jev selects the requested color and size; the LLM completes the purchase.

+

D4 supplement · seed 3107 · one recorded pair

+ Download video +
+
+ +
+ +
+

02 / FROZENLAKE

+

Keep the plan.

+

One LLM objective stays in place. Jev chooses a direction from each new state, making four local moves to reach the goal.

+

Seed 3000 · one recorded pair

+ Download video +
+
+ +
+ +
+

03 / SQLITE RECOVERY

+

Choose the evidence.

+

The LLM proposes three inspections. Jev selects raw-page inspection; the LLM recovers ten rows, and an independent verifier checks the output.

+

Terminal-Bench 2 · sqlite-db-truncate · one recorded pair

+ Download video +
+
+
+ +
+

READ THE RESULTS IN CONTEXT

+

One replay shows one recorded pair.

+

Calls, tokens, total time, and rewards come from the recorded runs. Savings use the LLM-only baseline. Playback preserves each pair’s measured completion-time ratio; individual step durations are illustrated.

+

Broader results vary by task. The report includes the original D2 matrix, the separate D4 WebShop supplement, fallbacks, and uncertainty.

+ +
+
+ + + diff --git a/site/agent-harness.js b/site/agent-harness.js new file mode 100644 index 0000000..ef44dfd --- /dev/null +++ b/site/agent-harness.js @@ -0,0 +1,116 @@ +(() => { + const videos = [...document.querySelectorAll(".harness-video")]; + const overview = document.querySelector("#overview-video"); + if (!overview) return; + + const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); + let overviewVisible = false; + let manualPlayback = false; + + function pauseAll() { + videos.forEach(video => video.pause()); + } + + function playOverview() { + if (manualPlayback || reducedMotion.matches || document.hidden || !overviewVisible) return; + overview.play().catch(() => {}); + } + + function formatTime(seconds) { + if (!Number.isFinite(seconds)) return "0:00"; + const whole = Math.floor(seconds); + return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`; + } + + function addControls(video) { + const player = document.createElement("div"); + player.className = "player"; + video.before(player); + player.append(video); + + const controls = document.createElement("div"); + controls.className = "playback-controls"; + controls.setAttribute("role", "group"); + controls.setAttribute("aria-label", `${video.getAttribute("aria-label")} — playback controls`); + controls.innerHTML = ` + + + 0:00 + `; + player.append(controls); + const toggle = controls.querySelector(".playback-toggle"); + const seek = controls.querySelector("input"); + const time = controls.querySelector(".playback-time"); + const expand = controls.querySelector(".playback-expand"); + + function update() { + toggle.textContent = video.paused ? "Play" : "Pause"; + const loaded = Number.isFinite(video.duration) && video.duration > 0; + seek.disabled = !loaded; + if (loaded) { + seek.max = video.duration; + seek.value = video.currentTime; + } + time.textContent = loaded ? `${formatTime(video.currentTime)} / ${formatTime(video.duration)}` : formatTime(video.currentTime); + } + + controls.addEventListener("pointerdown", () => { manualPlayback = true; }); + controls.addEventListener("keydown", () => { manualPlayback = true; }); + toggle.addEventListener("click", () => { + manualPlayback = true; + if (video.paused) video.play().catch(() => {}); + else video.pause(); + }); + seek.addEventListener("input", () => { + manualPlayback = true; + video.currentTime = Number(seek.value); + }); + expand.hidden = !document.fullscreenEnabled; + expand.addEventListener("click", () => { + manualPlayback = true; + const request = document.fullscreenElement ? document.exitFullscreen() : player.requestFullscreen(); + request.catch(() => {}); + }); + document.addEventListener("fullscreenchange", () => { + expand.textContent = document.fullscreenElement === player ? "Collapse" : "Expand"; + }); + for (const event of ["loadedmetadata", "durationchange", "timeupdate", "play", "pause", "seeked"]) { + video.addEventListener(event, update); + } + video.controls = false; + update(); + } + + for (const video of videos) { + video.muted = true; + addControls(video); + video.addEventListener("pointerdown", () => { manualPlayback = true; }); + video.addEventListener("keydown", () => { manualPlayback = true; }); + video.addEventListener("play", () => { + if (video !== overview) manualPlayback = true; + videos.forEach(other => { if (other !== video) other.pause(); }); + }); + } + + const visibility = new IntersectionObserver(entries => { + for (const entry of entries) { + if (entry.target === overview) { + overviewVisible = entry.isIntersecting && entry.intersectionRatio >= 0.35; + } + if (!entry.isIntersecting || (entry.target === overview && !overviewVisible)) { + entry.target.pause(); + } + } + playOverview(); + }, { threshold: [0, 0.35] }); + videos.forEach(video => visibility.observe(video)); + + document.addEventListener("visibilitychange", () => { + if (document.hidden) pauseAll(); + else playOverview(); + }); + reducedMotion.addEventListener("change", () => { + if (reducedMotion.matches) pauseAll(); + else playOverview(); + }); +})(); diff --git a/site/assets/i18n/zh-CN.js b/site/assets/i18n/zh-CN.js index 8613d05..6065984 100644 --- a/site/assets/i18n/zh-CN.js +++ b/site/assets/i18n/zh-CN.js @@ -10,6 +10,7 @@ window.jevanyMessages = { "Main navigation": "主导航", "Demos": "演示", "Benchmarks": "评测", + "Agent harness": "Agent 协作演示", "Models": "模型", "Docs": "文档", "Language": "语言", diff --git a/site/assets/media/docs/jev-agent-harness.mp4 b/site/assets/media/docs/jev-agent-harness.mp4 index 311fa91..434d4c6 100644 Binary files a/site/assets/media/docs/jev-agent-harness.mp4 and b/site/assets/media/docs/jev-agent-harness.mp4 differ diff --git a/site/assets/media/docs/jev-agent-harness.webm b/site/assets/media/docs/jev-agent-harness.webm index a224daf..1b53946 100644 Binary files a/site/assets/media/docs/jev-agent-harness.webm and b/site/assets/media/docs/jev-agent-harness.webm differ diff --git a/site/assets/media/docs/jev-agent-harness.webp b/site/assets/media/docs/jev-agent-harness.webp index a5b1384..233c1de 100644 Binary files a/site/assets/media/docs/jev-agent-harness.webp and b/site/assets/media/docs/jev-agent-harness.webp differ diff --git a/site/assets/media/docs/jev-decision-frozen-lake-v2.mp4 b/site/assets/media/docs/jev-decision-frozen-lake-v2.mp4 index 413f066..e5b68b1 100644 Binary files a/site/assets/media/docs/jev-decision-frozen-lake-v2.mp4 and b/site/assets/media/docs/jev-decision-frozen-lake-v2.mp4 differ diff --git a/site/assets/media/docs/jev-decision-frozen-lake-v2.webm b/site/assets/media/docs/jev-decision-frozen-lake-v2.webm index 3ddd448..4b20a5e 100644 Binary files a/site/assets/media/docs/jev-decision-frozen-lake-v2.webm and b/site/assets/media/docs/jev-decision-frozen-lake-v2.webm differ diff --git a/site/assets/media/docs/jev-decision-frozen-lake-v2.webp b/site/assets/media/docs/jev-decision-frozen-lake-v2.webp index 0a5582d..e7ba168 100644 Binary files a/site/assets/media/docs/jev-decision-frozen-lake-v2.webp and b/site/assets/media/docs/jev-decision-frozen-lake-v2.webp differ diff --git a/site/assets/media/docs/jev-decision-terminal-v2.mp4 b/site/assets/media/docs/jev-decision-terminal-v2.mp4 index ef565f6..faffd92 100644 Binary files a/site/assets/media/docs/jev-decision-terminal-v2.mp4 and b/site/assets/media/docs/jev-decision-terminal-v2.mp4 differ diff --git a/site/assets/media/docs/jev-decision-terminal-v2.webm b/site/assets/media/docs/jev-decision-terminal-v2.webm index c6fd208..dd3b005 100644 Binary files a/site/assets/media/docs/jev-decision-terminal-v2.webm and b/site/assets/media/docs/jev-decision-terminal-v2.webm differ diff --git a/site/assets/media/docs/jev-decision-terminal-v2.webp b/site/assets/media/docs/jev-decision-terminal-v2.webp index cffdcbe..f7e9776 100644 Binary files a/site/assets/media/docs/jev-decision-terminal-v2.webp and b/site/assets/media/docs/jev-decision-terminal-v2.webp differ diff --git a/site/assets/media/docs/jev-decision-webshop-v2.mp4 b/site/assets/media/docs/jev-decision-webshop-v2.mp4 index ed56e56..1dd5d29 100644 Binary files a/site/assets/media/docs/jev-decision-webshop-v2.mp4 and b/site/assets/media/docs/jev-decision-webshop-v2.mp4 differ diff --git a/site/assets/media/docs/jev-decision-webshop-v2.webm b/site/assets/media/docs/jev-decision-webshop-v2.webm index dc6cea4..a4d8bad 100644 Binary files a/site/assets/media/docs/jev-decision-webshop-v2.webm and b/site/assets/media/docs/jev-decision-webshop-v2.webm differ diff --git a/site/assets/media/docs/jev-decision-webshop-v2.webp b/site/assets/media/docs/jev-decision-webshop-v2.webp index 9733bb2..233c1de 100644 Binary files a/site/assets/media/docs/jev-decision-webshop-v2.webp and b/site/assets/media/docs/jev-decision-webshop-v2.webp differ diff --git a/site/docs/quickstart.html b/site/docs/quickstart.html index e1c4bbc..0240b5f 100644 --- a/site/docs/quickstart.html +++ b/site/docs/quickstart.html @@ -124,6 +124,8 @@

⚡ Jev inside LLM agent loops

+

Watch all three in the nine-second overview, +or open the agent harness demos.

Broader paired evaluations show that gains vary by task. The full results, delegation protocol, and technical report describe diff --git a/site/docs/zh.html b/site/docs/zh.html index 4b4446b..b47227b 100644 --- a/site/docs/zh.html +++ b/site/docs/zh.html @@ -112,6 +112,8 @@

⚡ Jev 加入 LLM Agent 循环

+

三个案例也可在九秒总览中连续观看,或打开 +Agent harness 演示页查看单独回放。

下表展示更多配对评测结果,收益随任务而变化。完整结果、 委托协议和 技术报告说明了 Jev 适合处理哪些选择,以及何时交回 LLM。

diff --git a/site/index.html b/site/index.html index c70873c..2aebe28 100644 --- a/site/index.html +++ b/site/index.html @@ -44,6 +44,7 @@