Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion integrations/pi-villani/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pi install npm:@mmprotest/pi-villani
```
Provides `/villani <task>`.

Runtime version: `v0.1.3`.
Runtime version: `v0.1.4`.

## Usage

Expand Down
2 changes: 1 addition & 1 deletion integrations/pi-villani/docs/release.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Release checklist
1. Publish matching GitHub runtime release `pi-villani-runtime-v0.1.3`.
1. Publish matching GitHub runtime release `pi-villani-runtime-v0.1.4`.
2. Verify assets and checksums.
3. Publish `@mmprotest/pi-villani`.
Install: `pi install npm:@mmprotest/pi-villani`.
26 changes: 18 additions & 8 deletions integrations/pi-villani/src/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ function bridgeScript(body: string) {
}
const readyPrelude =
"process.stdout.write(JSON.stringify({type:'ready'})+'\\n');\nconst send=e=>process.stdout.write(JSON.stringify(e)+'\\n');\n";
function fakeBridgeLaunch(fakeBridgeScriptPath: string) {
return {
executableOverride: process.execPath,
bridgeArgsOverride: [fakeBridgeScriptPath],
};
}

test("/villani sends command notification and only reports run started after run_started event", async () => {
const old = process.env.VILLANI_COMMAND;
Expand All @@ -61,6 +67,7 @@ test("/villani sends command notification and only reports run started after run
{ sendMessage: (m: string) => notes.push(m) },
{
ui: { notify: (m: string) => notes.push(m) },
bridgeLaunchOptions: fakeBridgeLaunch(p),
cwd: "/tmp",
model: { id: "m" },
},
Expand Down Expand Up @@ -93,7 +100,7 @@ test("/villani missing run_started timeout reports visible error instead of star
runVillani(
"task",
{},
{ ui: { notify: (m: string) => notes.push(m) }, cwd: "/tmp" },
{ ui: { notify: (m: string) => notes.push(m) }, bridgeLaunchOptions: fakeBridgeLaunch(p), cwd: "/tmp" },
),
/did not acknowledge run command within 10 seconds.*no ack/s,
);
Expand Down Expand Up @@ -145,7 +152,7 @@ setInterval(()=>{},1000);
runVillani(
"task",
{},
{ ui: { notify: (m: string) => notes.push(m) }, cwd: "/tmp" },
{ ui: { notify: (m: string) => notes.push(m) }, bridgeLaunchOptions: fakeBridgeLaunch(p), cwd: "/tmp" },
),
/did not acknowledge run command within 10 seconds.*cleanup no ack/s,
);
Expand All @@ -168,7 +175,7 @@ test("bridge exit before ready with ModuleNotFoundError produces pip install dia
process.env.VILLANI_COMMAND = p;
try {
await assert.rejects(
() => bridgePing({ ui: { notify: () => {} } }),
() => bridgePing({ ui: { notify: () => {} }, bridgeLaunchOptions: fakeBridgeLaunch(p) }),
/pip install -e/,
);
} finally {
Expand All @@ -186,7 +193,7 @@ test("/villani-bridge-ping succeeds with fake bridge", async () => {
process.env.VILLANI_COMMAND = p;
const notes: string[] = [];
try {
await bridgePing({ ui: { notify: (m: string) => notes.push(m) } });
await bridgePing({ ui: { notify: (m: string) => notes.push(m) }, bridgeLaunchOptions: fakeBridgeLaunch(p) });
} finally {
if (old === undefined) delete process.env.VILLANI_COMMAND;
else process.env.VILLANI_COMMAND = old;
Expand All @@ -200,7 +207,7 @@ test("/villani-bridge-ping surfaces stderr on failure", async () => {
process.env.VILLANI_COMMAND = p;
try {
await assert.rejects(
() => bridgePing({ ui: { notify: () => {} } }),
() => bridgePing({ ui: { notify: () => {} }, bridgeLaunchOptions: fakeBridgeLaunch(p) }),
/boom stderr/,
);
} finally {
Expand All @@ -225,7 +232,7 @@ test("/villani surfaces bridge error before run_started without waiting 10 secon
runVillani(
"task",
{},
{ ui: { notify: (m: string) => notes.push(m) }, cwd: "/tmp" },
{ ui: { notify: (m: string) => notes.push(m) }, bridgeLaunchOptions: fakeBridgeLaunch(p), cwd: "/tmp" },
),
/bad run command/,
);
Expand Down Expand Up @@ -253,7 +260,7 @@ test("/villani launches bridge with ctx cwd", async () => {
await runVillani(
"task",
{ sendMessage: () => {} },
{ ui: { notify: () => {} }, cwd: d, model: { id: "m" } },
{ ui: { notify: () => {} }, bridgeLaunchOptions: fakeBridgeLaunch(p), cwd: d, model: { id: "m" } },
);
assert.equal(readFileSync(cwdFile, "utf8"), d);
} finally {
Expand Down Expand Up @@ -359,7 +366,7 @@ test("/villani sends exactly one iterable final durable message with summary met
await runVillani(
"task",
{ sendMessage: (m: any) => sent.push(m) },
{ ui: { notify: () => {} }, cwd: "/tmp", model: { id: "m" } },
{ ui: { notify: () => {} }, bridgeLaunchOptions: fakeBridgeLaunch(p), cwd: "/tmp", model: { id: "m" } },
);
} finally {
if (old === undefined) delete process.env.VILLANI_COMMAND;
Expand Down Expand Up @@ -448,6 +455,7 @@ test("/villani approval pending clears widget, sets status, confirms, and accept
return true;
},
},
bridgeLaunchOptions: fakeBridgeLaunch(p),
cwd: "/tmp",
model: { id: "m" },
},
Expand Down Expand Up @@ -526,6 +534,7 @@ test("/villani keeps waiting after nonzero tool_finished and renders next model
notify: (m: string) => notes.push(m),
setStatus: (_: string, m: string) => statuses.push(m),
},
bridgeLaunchOptions: fakeBridgeLaunch(p),
cwd: "/tmp",
model: { id: "m" },
},
Expand Down Expand Up @@ -632,6 +641,7 @@ test("command lifecycle sets then clears widget through final result", async ()
setWidget: (...a: any[]) => widgets.push(a),
setStatus: () => {},
},
bridgeLaunchOptions: fakeBridgeLaunch(p),
cwd: "/tmp",
model: { id: "m" },
},
Expand Down
9 changes: 8 additions & 1 deletion integrations/pi-villani/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
VILLANI_RUNTIME_TAG,
VILLANI_RUNTIME_VERSION,
} from "./runtimeConfig.js";
import { VillaniBridgeProcess } from "./process.js";
import { VillaniBridgeProcess, type BridgeLaunchOptions } from "./process.js";
import {
resolvePiModel,
sanitizeError,
Expand Down Expand Up @@ -159,6 +159,10 @@ function denyPending(run: ActiveRun) {
function bridgeStderr(bridge: VillaniBridgeProcess) {
return bridge.getRecentStderr?.() ?? bridge.stderr ?? "";
}

function bridgeLaunchOptions(ctx: any): BridgeLaunchOptions {
return ctx?.bridgeLaunchOptions ?? ctx?.villaniBridgeLaunchOptions ?? {};
}
function bridgeDiagnosticMessage(e: unknown) {
const msg = sanitizeError(e);
if (/ModuleNotFoundError: No module named ['"]villani_code['"]/.test(msg))
Expand All @@ -174,8 +178,10 @@ async function assertBridgePing(
ctx: any,
env?: NodeJS.ProcessEnv,
signal?: AbortSignal,
launchOptions: BridgeLaunchOptions = bridgeLaunchOptions(ctx),
) {
const bridge = new VillaniBridgeProcess(executable, {
...launchOptions,
env,
startupTimeoutMs: 30000,
cwd: ctx.cwd ?? process.cwd(),
Expand Down Expand Up @@ -319,6 +325,7 @@ export async function runVillani(
}
await setStatus(ctx, nextVillaniStatus("thinking", "runtime-start") ?? "Villaniplan forming...");
const bridge = new VillaniBridgeProcess(executable, {
...bridgeLaunchOptions(ctx),
env,
startupTimeoutMs: 30000,
proxyMode: usePi,
Expand Down
4 changes: 2 additions & 2 deletions integrations/pi-villani/src/process.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { spawn, ChildProcessWithoutNullStreams } from 'node:child_process'; import { EventEmitter } from 'node:events'; import type { BridgeEvent } from './protocol.js';
export interface BridgeProcessOptions{proxyMode?:boolean; explicitConfigMode?:boolean; env?:NodeJS.ProcessEnv; cwd?:string; startupTimeoutMs?:number; bridgeArgs?:string[]} export type LaunchOptions=BridgeProcessOptions;
export interface BridgeLaunchOptions{executableOverride?:string; bridgeArgsOverride?:string[]} export interface BridgeProcessOptions extends BridgeLaunchOptions{proxyMode?:boolean; explicitConfigMode?:boolean; env?:NodeJS.ProcessEnv; cwd?:string; startupTimeoutMs?:number; bridgeArgs?:string[]} export type LaunchOptions=BridgeProcessOptions;
export function sanitizedEnv(opts:BridgeProcessOptions={}):NodeJS.ProcessEnv{const e={...(opts.env||process.env)}; if(opts.proxyMode&&!opts.explicitConfigMode){for(const k of Object.keys(e)){if(['OPENAI_API_KEY','ANTHROPIC_API_KEY','VILLANI_API_KEY'].includes(k)||/(_API_KEY|_TOKEN|_AUTH|_BEARER|TOKEN|AUTH|BEARER)$/i.test(k)) delete e[k];}} return e;}
export class VillaniBridgeProcess extends EventEmitter{proc:ChildProcessWithoutNullStreams; ready=false; stderr=''; buffer=''; startupTimeoutMs:number; readonly shell=false; finals=new Map<string,BridgeEvent>(); private exitInfo?:{code:number|null;signal:NodeJS.Signals|null};
constructor(executable:string, opts:BridgeProcessOptions={}){super(); const args=opts.bridgeArgs??['bridge','--stdio']; this.startupTimeoutMs=opts.startupTimeoutMs??30000; this.proc=spawn(executable,args,{shell:false,env:sanitizedEnv(opts),cwd:opts.cwd}); this.proc.stderr.on('data',d=>{this.stderr=(this.stderr+d.toString()).slice(-8000)}); this.proc.stdout.on('data',d=>this.onout(d.toString())); this.proc.once('error',e=>this.emit('error',e)); this.proc.once('exit',(code,signal)=>{this.exitInfo={code,signal}; this.emit('exit',{code,signal});});}
constructor(executable:string, opts:BridgeProcessOptions={}){super(); const launchExecutable=opts.executableOverride??executable; const args=opts.bridgeArgsOverride??opts.bridgeArgs??['bridge','--stdio']; this.startupTimeoutMs=opts.startupTimeoutMs??30000; this.proc=spawn(launchExecutable,args,{shell:false,env:sanitizedEnv(opts),cwd:opts.cwd}); this.proc.stderr.on('data',d=>{this.stderr=(this.stderr+d.toString()).slice(-8000)}); this.proc.stdout.on('data',d=>this.onout(d.toString())); this.proc.once('error',e=>this.emit('error',e)); this.proc.once('exit',(code,signal)=>{this.exitInfo={code,signal}; this.emit('exit',{code,signal});});}
getRecentStderr(){return this.stderr;}
onout(s:string){this.buffer+=s; let i; while((i=this.buffer.indexOf('\n'))>=0){const line=this.buffer.slice(0,i); this.buffer=this.buffer.slice(i+1); if(!line.trim())continue; try{const e=JSON.parse(line); if(e.type==='ready')this.ready=true; if(['run_completed','run_failed','run_aborted'].includes(e.type)&&e.id)this.finals.set(e.id,e); this.emit('event',e);}catch{const err=new Error('Malformed JSONL from bridge'); this.kill(); this.emit('error',err);}}}
send(c:unknown){try{if(this.proc.stdin.destroyed||!this.proc.stdin.writable)return false; return this.proc.stdin.write(JSON.stringify(c)+'\n');}catch(e){this.emit('error',e); return false;}} abort(runId:string){this.send({type:'abort',id:runId});} respondToApproval(runId:string,requestId:string,approved:boolean){this.send({type:'approval_response',id:runId,request_id:requestId,approved});} kill(){if(!this.proc.killed)this.proc.kill();}
Expand Down
2 changes: 1 addition & 1 deletion integrations/pi-villani/src/runtimeConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const VILLANI_RUNTIME_VERSION = "0.1.3";
export const VILLANI_RUNTIME_VERSION = "0.1.4";
export const VILLANI_RUNTIME_REPOSITORY = "mmprotest/villani-code";
export const VILLANI_RUNTIME_TAG = `pi-villani-runtime-v${VILLANI_RUNTIME_VERSION}`;
export type PlatformKey = "win32-x64"|"darwin-arm64"|"darwin-x64"|"linux-x64";
Expand Down
Loading