mirror of
https://github.com/openai/codex.git
synced 2026-09-09 15:58:47 +00:00
feat: add codex wasm harness prototype
This commit is contained in:
12
codex-rs/Cargo.lock
generated
12
codex-rs/Cargo.lock
generated
@@ -2923,6 +2923,18 @@ dependencies = [
|
||||
"v8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-wasm-harness"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-windows-sandbox"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -52,6 +52,7 @@ members = [
|
||||
"tui",
|
||||
"tools",
|
||||
"v8-poc",
|
||||
"wasm-harness",
|
||||
"utils/absolute-path",
|
||||
"utils/cargo-bin",
|
||||
"git-utils",
|
||||
@@ -229,6 +230,7 @@ insta = "1.46.3"
|
||||
inventory = "0.3.19"
|
||||
itertools = "0.14.0"
|
||||
jsonwebtoken = "9.3.1"
|
||||
js-sys = "0.3.85"
|
||||
keyring = { version = "3.6", default-features = false }
|
||||
landlock = "0.4.4"
|
||||
lazy_static = "1"
|
||||
@@ -335,6 +337,8 @@ urlencoding = "2.1"
|
||||
uuid = "1"
|
||||
vt100 = "0.16.2"
|
||||
walkdir = "2.5.0"
|
||||
wasm-bindgen = "0.2.108"
|
||||
wasm-bindgen-futures = "0.4.58"
|
||||
webbrowser = "1.0"
|
||||
which = "8"
|
||||
wildmatch = "2.6.1"
|
||||
|
||||
1
codex-rs/wasm-harness/.gitignore
vendored
Normal file
1
codex-rs/wasm-harness/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/examples/pkg/
|
||||
6
codex-rs/wasm-harness/BUILD.bazel
Normal file
6
codex-rs/wasm-harness/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "wasm-harness",
|
||||
crate_name = "codex_wasm_harness",
|
||||
)
|
||||
23
codex-rs/wasm-harness/Cargo.toml
Normal file
23
codex-rs/wasm-harness/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
name = "codex-wasm-harness"
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
name = "codex_wasm_harness"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
js-sys = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
wasm-bindgen = { workspace = true }
|
||||
wasm-bindgen-futures = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
46
codex-rs/wasm-harness/README.md
Normal file
46
codex-rs/wasm-harness/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Codex WASM Harness Prototype
|
||||
|
||||
This crate is the first browser-facing seam for a Codex harness prototype.
|
||||
|
||||
It does not yet call `codex-core::run_turn` or `RegularTask::run`. Instead, it
|
||||
establishes the intended browser API shape:
|
||||
|
||||
- submit a prompt from JavaScript;
|
||||
- stream Codex-shaped turn events back to the page; and
|
||||
- resolve after a `turn_complete` event.
|
||||
|
||||
The sampler is currently a JavaScript callback so the browser demo can keep
|
||||
network and credential policy outside the WASM bundle. The demo page uses a
|
||||
deterministic local sampler by default, or a direct Responses API request when
|
||||
the user enters an API key.
|
||||
|
||||
The API key field is for local prototype use only: it stores the key in browser
|
||||
`localStorage` and sends it directly from the page. A production browser
|
||||
integration should use a proxy or an ephemeral-token flow instead of persisting
|
||||
long-lived API keys in the page origin.
|
||||
|
||||
The next step is to replace the callback boundary with a real model transport
|
||||
and then wire the facade to the Codex turn loop after host services are
|
||||
injectable.
|
||||
|
||||
## Current Limitations
|
||||
|
||||
This is a boundary prototype, not a port of `codex-core` yet.
|
||||
|
||||
- It does not construct a `Session` or `TurnContext`.
|
||||
- It does not call `RegularTask::run` or `run_turn`.
|
||||
- It does not expose native Codex tools.
|
||||
- It emits Codex-shaped events, but not the full protocol event set.
|
||||
|
||||
The immediate implementation value is that the browser API and demo page can be
|
||||
iterated independently while the host-heavy Codex services are moved behind
|
||||
browser-compatible traits.
|
||||
|
||||
## Build Sketch
|
||||
|
||||
```sh
|
||||
rustup target add wasm32-unknown-unknown
|
||||
codex-rs/wasm-harness/scripts/build-browser-demo.sh
|
||||
```
|
||||
|
||||
Then serve `codex-rs/wasm-harness/examples` and open `/browser/index.html`.
|
||||
344
codex-rs/wasm-harness/examples/browser/index.html
Normal file
344
codex-rs/wasm-harness/examples/browser/index.html
Normal file
@@ -0,0 +1,344 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Codex WASM Harness Prototype</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-family:
|
||||
ui-serif, "Iowan Old Style", "Palatino Linotype", Georgia, serif;
|
||||
background: #f2eddf;
|
||||
color: #1f2a24;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(circle at 20% 10%, #ffc96b 0 9rem, transparent 9rem),
|
||||
linear-gradient(135deg, #f8f2e3 0%, #dfecdc 48%, #c7dde7 100%);
|
||||
}
|
||||
|
||||
main {
|
||||
box-sizing: border-box;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 760px;
|
||||
margin: 0 0 12px;
|
||||
font-size: clamp(2.4rem, 7vw, 5.5rem);
|
||||
line-height: 0.92;
|
||||
letter-spacing: -0.06em;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 720px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 12px;
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
border: 2px solid #1f2a24;
|
||||
border-radius: 999px;
|
||||
font: inherit;
|
||||
font-size: 1.05rem;
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
input {
|
||||
background: #fffaf0;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #1f2a24;
|
||||
color: #fffaf0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.api-key-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 14px;
|
||||
align-items: end;
|
||||
margin: 28px 0 0;
|
||||
border: 2px solid rgb(31 42 36 / 0.26);
|
||||
border-radius: 28px;
|
||||
background: rgb(255 250 240 / 0.68);
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.api-key-panel h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.api-key-panel p {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.api-key-panel label {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.api-key-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
grid-column: 1 / -1;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
background: transparent;
|
||||
color: #1f2a24;
|
||||
}
|
||||
|
||||
.api-key-status {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
#events {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.event {
|
||||
border: 2px solid rgb(31 42 36 / 0.22);
|
||||
border-radius: 20px;
|
||||
background: rgb(255 250 240 / 0.75);
|
||||
padding: 14px 16px;
|
||||
box-shadow: 0 16px 40px rgb(31 42 36 / 0.08);
|
||||
}
|
||||
|
||||
.event strong {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
form,
|
||||
.api-key-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Codex WASM harness prototype</h1>
|
||||
<p>
|
||||
This page exercises the browser boundary: submit a prompt, stream
|
||||
Codex-shaped events from WASM, and resolve when the turn completes.
|
||||
</p>
|
||||
<section class="api-key-panel" aria-labelledby="api-key-title">
|
||||
<div>
|
||||
<h2 id="api-key-title">Responses API key</h2>
|
||||
<p>
|
||||
Local prototype only. The key is stored in this browser's
|
||||
localStorage and is sent directly from this page to the Responses
|
||||
API when you run a turn.
|
||||
</p>
|
||||
</div>
|
||||
<label>
|
||||
API key
|
||||
<input
|
||||
id="api-key"
|
||||
type="password"
|
||||
placeholder="sk-..."
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</label>
|
||||
<div class="api-key-actions">
|
||||
<button id="clear-api-key" class="secondary-button" type="button">
|
||||
Forget key
|
||||
</button>
|
||||
<output id="api-key-status" class="api-key-status"></output>
|
||||
</div>
|
||||
</section>
|
||||
<form id="prompt-form">
|
||||
<input id="prompt" value="write hello world" autocomplete="off" />
|
||||
<button type="submit">Run turn</button>
|
||||
</form>
|
||||
<section id="events" aria-live="polite"></section>
|
||||
</main>
|
||||
<script type="module">
|
||||
import init, { BrowserCodex } from "../pkg/codex_wasm_harness.js";
|
||||
|
||||
await init();
|
||||
|
||||
const form = document.querySelector("#prompt-form");
|
||||
const input = document.querySelector("#prompt");
|
||||
const events = document.querySelector("#events");
|
||||
const apiKeyInput = document.querySelector("#api-key");
|
||||
const apiKeyStatus = document.querySelector("#api-key-status");
|
||||
const clearApiKeyButton = document.querySelector("#clear-api-key");
|
||||
const API_KEY_STORAGE_KEY = "codexWasmHarness.apiKey";
|
||||
const RESPONSE_MODEL = "gpt-5.1";
|
||||
|
||||
apiKeyInput.value = readStoredApiKey();
|
||||
updateApiKeyStatus();
|
||||
|
||||
apiKeyInput.addEventListener("input", () => {
|
||||
writeStoredApiKey(apiKeyInput.value.trim());
|
||||
updateApiKeyStatus();
|
||||
});
|
||||
|
||||
clearApiKeyButton.addEventListener("click", () => {
|
||||
apiKeyInput.value = "";
|
||||
writeStoredApiKey("");
|
||||
updateApiKeyStatus();
|
||||
});
|
||||
|
||||
const sampler = async ({ prompt, instructions }) => {
|
||||
const apiKey = readStoredApiKey();
|
||||
if (apiKey) {
|
||||
return sampleWithResponsesApi({ apiKey, prompt, instructions });
|
||||
}
|
||||
|
||||
if (prompt.toLowerCase().includes("hello world")) {
|
||||
return {
|
||||
message:
|
||||
'Here is a minimal hello world example:\\n\\n```js\\nconsole.log("hello world");\\n```',
|
||||
};
|
||||
}
|
||||
return {
|
||||
message: `Browser sampler placeholder response for: ${prompt}`,
|
||||
};
|
||||
};
|
||||
|
||||
const codex = new BrowserCodex(sampler);
|
||||
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
events.replaceChildren();
|
||||
await codex.submit_turn(input.value, renderEvent);
|
||||
});
|
||||
|
||||
async function sampleWithResponsesApi({ apiKey, prompt, instructions }) {
|
||||
try {
|
||||
const response = await fetch("https://api.openai.com/v1/responses", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: RESPONSE_MODEL,
|
||||
instructions,
|
||||
input: prompt,
|
||||
}),
|
||||
});
|
||||
const body = await response.json();
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
body?.error?.message ?? `Responses API returned ${response.status}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return { message: extractResponseText(body) };
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
message:
|
||||
`Responses API request failed: ${errorMessage}\n\n` +
|
||||
"This browser prototype sends the stored key directly from the page. " +
|
||||
"If the request is blocked, use a local dev proxy or an ephemeral-token flow.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function extractResponseText(response) {
|
||||
if (typeof response.output_text === "string" && response.output_text) {
|
||||
return response.output_text;
|
||||
}
|
||||
|
||||
const chunks = [];
|
||||
for (const item of response.output ?? []) {
|
||||
for (const content of item.content ?? []) {
|
||||
if (typeof content.text === "string") {
|
||||
chunks.push(content.text);
|
||||
} else if (typeof content.output_text === "string") {
|
||||
chunks.push(content.output_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chunks.join("\n") || JSON.stringify(response, null, 2);
|
||||
}
|
||||
|
||||
function readStoredApiKey() {
|
||||
try {
|
||||
return localStorage.getItem(API_KEY_STORAGE_KEY) ?? "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function writeStoredApiKey(apiKey) {
|
||||
try {
|
||||
if (apiKey) {
|
||||
localStorage.setItem(API_KEY_STORAGE_KEY, apiKey);
|
||||
} else {
|
||||
localStorage.removeItem(API_KEY_STORAGE_KEY);
|
||||
}
|
||||
} catch {
|
||||
apiKeyStatus.textContent =
|
||||
"localStorage is unavailable in this browser context.";
|
||||
}
|
||||
}
|
||||
|
||||
function updateApiKeyStatus() {
|
||||
apiKeyStatus.textContent = apiKeyInput.value
|
||||
? "API key stored in localStorage for this browser."
|
||||
: "No API key stored. The deterministic demo sampler will be used.";
|
||||
}
|
||||
|
||||
function renderEvent(event) {
|
||||
const card = document.createElement("article");
|
||||
card.className = "event";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = event.type;
|
||||
const body = document.createElement("pre");
|
||||
body.textContent = JSON.stringify(event, null, 2);
|
||||
card.append(title, body);
|
||||
events.append(card);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
16
codex-rs/wasm-harness/scripts/build-browser-demo.sh
Executable file
16
codex-rs/wasm-harness/scripts/build-browser-demo.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
crate_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
workspace_dir="$(cd "$crate_dir/.." && pwd)"
|
||||
|
||||
cd "$workspace_dir"
|
||||
|
||||
cargo build \
|
||||
-p codex-wasm-harness \
|
||||
--target wasm32-unknown-unknown
|
||||
|
||||
wasm-bindgen \
|
||||
--target web \
|
||||
--out-dir "$crate_dir/examples/pkg" \
|
||||
"$workspace_dir/target/wasm32-unknown-unknown/debug/codex_wasm_harness.wasm"
|
||||
224
codex-rs/wasm-harness/src/lib.rs
Normal file
224
codex-rs/wasm-harness/src/lib.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
//! Browser-facing prototype facade for a future Codex WASM harness.
|
||||
//!
|
||||
//! This crate intentionally starts outside `codex-core`: the first milestone is
|
||||
//! a working browser boundary that streams Codex-shaped turn events. Later
|
||||
//! iterations can replace the sampler callback with the real Codex
|
||||
//! `RegularTask` / `run_turn` path as host services become injectable.
|
||||
|
||||
use js_sys::Function;
|
||||
use js_sys::Promise;
|
||||
use js_sys::Reflect;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
||||
const DEFAULT_INSTRUCTIONS: &str = "You are Codex running in a browser WASM prototype.";
|
||||
|
||||
/// Browser entrypoint for the prototype harness.
|
||||
#[wasm_bindgen]
|
||||
pub struct BrowserCodex {
|
||||
sampler: Option<Function>,
|
||||
next_turn_id: u32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl BrowserCodex {
|
||||
/// Creates a new browser harness.
|
||||
///
|
||||
/// `sampler` may be a JavaScript function that accepts a request object and
|
||||
/// returns either a string, `{ message: string }`, or a Promise for either.
|
||||
/// When omitted, the harness uses a deterministic local demo response.
|
||||
#[wasm_bindgen(constructor)]
|
||||
#[must_use]
|
||||
pub fn new(sampler: JsValue) -> Self {
|
||||
Self {
|
||||
sampler: sampler.dyn_into::<Function>().ok(),
|
||||
next_turn_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Submits one browser turn and calls `on_event` for every emitted event.
|
||||
///
|
||||
/// The method resolves after `turn_complete` has been emitted. This mirrors
|
||||
/// Codex's event-driven shape rather than exposing a `prompt -> string`
|
||||
/// shortcut.
|
||||
pub async fn submit_turn(
|
||||
&mut self,
|
||||
prompt: String,
|
||||
on_event: Function,
|
||||
) -> Result<JsValue, JsValue> {
|
||||
self.next_turn_id += 1;
|
||||
let turn_id = format!("browser-turn-{}", self.next_turn_id);
|
||||
|
||||
emit_event(
|
||||
&on_event,
|
||||
&HarnessEvent::TurnStarted {
|
||||
turn_id: turn_id.clone(),
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: "default",
|
||||
},
|
||||
)?;
|
||||
emit_event(
|
||||
&on_event,
|
||||
&HarnessEvent::UserMessage {
|
||||
turn_id: turn_id.clone(),
|
||||
message: prompt.clone(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let request = SamplingRequest::new(turn_id.clone(), prompt.clone());
|
||||
let agent_message = self.sample(request).await?;
|
||||
|
||||
emit_event(
|
||||
&on_event,
|
||||
&HarnessEvent::AgentMessageDelta {
|
||||
turn_id: turn_id.clone(),
|
||||
delta: agent_message.clone(),
|
||||
},
|
||||
)?;
|
||||
emit_event(
|
||||
&on_event,
|
||||
&HarnessEvent::AgentMessage {
|
||||
turn_id: turn_id.clone(),
|
||||
message: agent_message.clone(),
|
||||
},
|
||||
)?;
|
||||
emit_event(
|
||||
&on_event,
|
||||
&HarnessEvent::TurnComplete {
|
||||
turn_id,
|
||||
last_agent_message: Some(agent_message.clone()),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(JsValue::from_str(&agent_message))
|
||||
}
|
||||
}
|
||||
|
||||
impl BrowserCodex {
|
||||
async fn sample(&self, request: SamplingRequest) -> Result<String, JsValue> {
|
||||
let Some(sampler) = &self.sampler else {
|
||||
return Ok(default_demo_response(&request.prompt));
|
||||
};
|
||||
|
||||
let request_json = serde_json::to_string(&request).map_err(js_error)?;
|
||||
let request_value = js_sys::JSON::parse(&request_json)?;
|
||||
let sampled = sampler.call1(&JsValue::NULL, &request_value)?;
|
||||
let resolved = JsFuture::from(Promise::resolve(&sampled)).await?;
|
||||
extract_message(resolved)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SamplingRequest {
|
||||
turn_id: String,
|
||||
prompt: String,
|
||||
instructions: &'static str,
|
||||
tools: Vec<String>,
|
||||
}
|
||||
|
||||
impl SamplingRequest {
|
||||
fn new(turn_id: String, prompt: String) -> Self {
|
||||
Self {
|
||||
turn_id,
|
||||
prompt,
|
||||
instructions: DEFAULT_INSTRUCTIONS,
|
||||
tools: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum HarnessEvent<'a> {
|
||||
TurnStarted {
|
||||
turn_id: String,
|
||||
model_context_window: Option<i64>,
|
||||
collaboration_mode_kind: &'a str,
|
||||
},
|
||||
UserMessage {
|
||||
turn_id: String,
|
||||
message: String,
|
||||
},
|
||||
AgentMessageDelta {
|
||||
turn_id: String,
|
||||
delta: String,
|
||||
},
|
||||
AgentMessage {
|
||||
turn_id: String,
|
||||
message: String,
|
||||
},
|
||||
TurnComplete {
|
||||
turn_id: String,
|
||||
last_agent_message: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SamplerResponse {
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
fn emit_event(on_event: &Function, event: &HarnessEvent<'_>) -> Result<(), JsValue> {
|
||||
let json = serde_json::to_string(event).map_err(js_error)?;
|
||||
let value = js_sys::JSON::parse(&json)?;
|
||||
on_event.call1(&JsValue::NULL, &value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_message(value: JsValue) -> Result<String, JsValue> {
|
||||
if let Some(message) = value.as_string() {
|
||||
return Ok(message);
|
||||
}
|
||||
|
||||
if value.is_object() {
|
||||
if let Some(message) = Reflect::get(&value, &JsValue::from_str("message"))?.as_string() {
|
||||
return Ok(message);
|
||||
}
|
||||
|
||||
let json = js_sys::JSON::stringify(&value)?;
|
||||
if let Some(json) = json.as_string()
|
||||
&& let Ok(response) = serde_json::from_str::<SamplerResponse>(&json)
|
||||
&& let Some(message) = response.message
|
||||
{
|
||||
return Ok(message);
|
||||
}
|
||||
}
|
||||
|
||||
Err(js_error(
|
||||
"sampler must return a string or an object with a string message field",
|
||||
))
|
||||
}
|
||||
|
||||
fn default_demo_response(prompt: &str) -> String {
|
||||
if prompt.to_ascii_lowercase().contains("hello world") {
|
||||
"Here is a minimal hello world example:\n\n```js\nconsole.log(\"hello world\");\n```"
|
||||
.to_string()
|
||||
} else {
|
||||
format!(
|
||||
"Browser Codex prototype received the prompt, but no model sampler was configured: {prompt}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn js_error(message: impl ToString) -> JsValue {
|
||||
JsValue::from_str(&message.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::default_demo_response;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn default_demo_response_handles_hello_world() {
|
||||
assert_eq!(
|
||||
default_demo_response("write hello world"),
|
||||
"Here is a minimal hello world example:\n\n```js\nconsole.log(\"hello world\");\n```"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user