feat(smoke): OPENPENCIL_SMOKE_PROGRAM mode runs a batch_design program headless

Reads a program file, runs it through op_mcp::batch_design_snapshot, applies, saves
the .op (postProcess off = raw structure) — the headless harness for benchmarking
the program-DSL path vs flat JSONL across weak models.
This commit is contained in:
Fini 2026-07-02 21:21:41 +08:00
parent b43cee87e5
commit 9e9cdec7cf
3 changed files with 84 additions and 7 deletions

1
Cargo.lock generated
View file

@ -3433,6 +3433,7 @@ dependencies = [
"op-ai-skills",
"op-editor-core",
"op-host-services",
"op-mcp",
"op-orchestrator",
"op-pen-loader",
"reqwest 0.12.28",

View file

@ -33,6 +33,12 @@ jian-ops-schema = { path = "../../vendor/jian/crates/jian-ops-schema" }
# harvested component library (.lib.op) into the doc before generation so the
# generator can instantiate its reusable masters.
op-pen-loader = { path = "../op-pen-loader" }
# `batch_design_snapshot` + `BatchDesign` MCP tool — the program-mode
# (OPENPENCIL_SMOKE_PROGRAM=<file>) runs a Pencil-style batch_design DSL program
# (binding=I(parent,{...}) tree-builder) against the doc, bypassing the
# orchestrator, to test whether emitting a PROGRAM (parent-by-reference) instead
# of flat JSONL cures weak-model row decomposition / missing rows.
op-mcp = { path = "../op-mcp" }
# Loop mode (OPENPENCIL_SMOKE_LOOP=1) — the headless agentic tool-loop reuses
# the production builtin provider + design-tool apply path from op-host-services
# (`ConfiguredBuiltinProvider`, `design_tool_defs`, `execute_agent_tool`) and the

View file

@ -249,13 +249,15 @@ impl LlmClient for DirectOpenAiClient {
obj.insert("thinking".into(), serde_json::json!({ "type": "disabled" }));
}
}
let resp = match reqwest::Client::new()
.post(&url)
.bearer_auth(&key)
.json(&body)
.send()
.await
{
// Connect + overall deadlines so a hung provider endpoint surfaces
// as an error instead of pinning the headless harness forever
// (mirrors the desktop's builtin_http_client).
let client = reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(15))
.timeout(std::time::Duration::from_secs(300))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
let resp = match client.post(&url).bearer_auth(&key).json(&body).send().await {
Ok(r) => r,
Err(e) => {
let _ = tx.unbounded_send(Err(LlmError {
@ -656,6 +658,74 @@ async fn main() -> std::process::ExitCode {
if let Err(code) = maybe_merge_smoke_library(&mut sink.state) {
return code;
}
// `OPENPENCIL_SMOKE_PROGRAM=<path>` runs a Pencil-style batch_design DSL
// PROGRAM (a `binding=I(parent,{...})` tree-builder) against the doc and
// saves it, bypassing the orchestrator entirely. This is the experiment
// harness for "can a weak model emit a structurally-stable PROGRAM
// (parent-by-reference) instead of fragile flat JSONL?". postProcess stays
// OFF so the saved doc is the RAW structure the program builds — no cleanup
// post-pass — which is the whole point: the program needs no repair pass.
if let Ok(program_path) = std::env::var("OPENPENCIL_SMOKE_PROGRAM") {
let program = match std::fs::read_to_string(&program_path) {
Ok(s) => s,
Err(e) => {
eprintln!("[PROGRAM] read {program_path}: {e}");
return std::process::ExitCode::from(3);
}
};
let tool = op_mcp::batch_design_snapshot(&sink.state);
let mut args: std::collections::BTreeMap<String, String> =
std::collections::BTreeMap::new();
args.insert("operations".into(), program);
let applied = match op_mcp::McpTool::call(&tool, &args) {
op_mcp::ToolOutcome::OkJsonWithCommand(json, cmd) => {
eprintln!("[PROGRAM] result envelope: {json}");
let ok = sink.state.apply(cmd);
eprintln!("[PROGRAM] apply -> {ok}");
ok
}
op_mcp::ToolOutcome::OkJson(json) => {
eprintln!("[PROGRAM] no command produced: {json}");
false
}
other => {
eprintln!("[PROGRAM] unexpected outcome: {other:?}");
false
}
};
let code = match std::env::var("OPENPENCIL_SMOKE_OUT") {
Ok(out) if !out.is_empty() => match serde_json::to_string_pretty(&sink.state.doc) {
Ok(j) => match std::fs::write(&out, j) {
Ok(()) => {
eprintln!("[PROGRAM] saved doc -> {out}");
if applied {
std::process::ExitCode::SUCCESS
} else {
std::process::ExitCode::from(1)
}
}
Err(e) => {
eprintln!("[PROGRAM] save failed ({out}): {e}");
std::process::ExitCode::from(4)
}
},
Err(e) => {
eprintln!("[PROGRAM] serialize failed: {e}");
std::process::ExitCode::from(4)
}
},
_ => {
if applied {
std::process::ExitCode::SUCCESS
} else {
std::process::ExitCode::from(1)
}
}
};
return code;
}
let request = DesignRequest {
prompt,
model: Some(model),