Fix final-answer routing and branch model events

This commit is contained in:
2026-11-07 21:39:58 +00:00
parent 361cfffb32
commit 4ae1ded5b6
7 changed files with 198 additions and 2 deletions
+3
View File
@@ -8,6 +8,7 @@ They use world files and world git. They do not use native filesystem or process
`branch` requires an absolute `repo` path and a prompt.
It forks a listed tree root and writes `board.started.by` in the same transaction.
The coordinator definition supplies the branch configuration.
The branch start event reports the coordinator's model, not the inherited root model.
Worktree isolation requires the later workspaces port. The default is a shared checkout.
The tool admits the prompt as a reply-enabled message.
@@ -22,6 +23,8 @@ A branch reports to its starter when its work ends.
While a child, landing, or reply remains pending, its final answer becomes a passive note.
One-way updates to the starter also become passive notes.
The starter can answer a report without an endless exchange of routed answers.
The starter's final answer reaches the branch without a faulted routing task.
A static test checks free runtime identifiers in every extension module, including function bodies.
The grant chain contains fork ancestry before starter ancestry.
Each conversation occurs once. A root fork that @ultimate starts includes `[branch, root, ultimate]`.
+1 -1
View File
@@ -22,7 +22,7 @@ export const branchTool = (host: any) => defineTool({
const coordinator = definitions.find((definition: any) => definition.name === 'coordinator')
if (!coordinator) throw new Error('Missing coordinator definition')
const rootAgent = await (await host.harness().conversation(root, ctx))!.agent(ctx)
const branch = await lineage.branch({from: root, repo: input.repo, prompt: input.prompt, title: input.title, deliver: false, by: api.conversationId, receipt: {taskId: api.taskId, callId: api.callId}}, ctx)
const branch = await lineage.branch({from: root, repo: input.repo, prompt: input.prompt, title: input.title, model: coordinator.runs.model, deliver: false, by: api.conversationId, receipt: {taskId: api.taskId, callId: api.callId}}, ctx)
const existing = await workspaces?.get(branch.id, ctx)
const cwd = existing ? (await (await host.harness().conversation(branch.id, ctx)).agent(ctx)).cwd : input.repo
const ancestors = workspaces ? (await workspaces.chain(branch.id, ctx)).slice(1) : []
+1
View File
@@ -14,6 +14,7 @@ import type {
import type { Host as ExtensionHost } from '../../host/js/extensions.ts'
import { Subagents } from '../subagents/api.ts'
import { ConversationsApi } from '../conversations/api.ts'
import { Messages } from './api.ts'
import { terminal } from './log.ts'
+11
View File
@@ -31696,6 +31696,8 @@ var until2 = async (test) => {
var models = createModels();
var faux = fauxProvider({ tokensPerSecond: 1e5 });
models.setProvider(faux.provider);
var template = models.getModels("faux")[0];
models.setProvider({ ...faux.provider, getModels: () => [template, { ...template, id: "coordinator" }] });
var registry = createRegistry();
var storage = await openStorage(globalThis.storagePath);
var harness = await Harness.open(storage, { models, registry, settings: { get extensions() {
@@ -31814,12 +31816,21 @@ try {
assert((await entries2(branch)).some((e) => e.kind === "board.message" && e.data.text === "first task"), "The first message did not reach the branch");
assert((await harness.commit((tx) => tx.conversation(branch.id), BACKGROUND_CONTEXT)).parent.conversationId === root.id, "Branch must fork its root");
assert((await harness.snapshot(Started2, branch.id, BACKGROUND_CONTEXT)).by === primary.id, "Missing starter");
const starts = (await globalThis.testHost.eventLog.recent()).filter((event) => event.name === "subagents.started" && event.data.id === branch.id);
assert(starts.length === 1 && starts[0].data.model === "faux/coordinator", `Effective branch model: ${JSON.stringify(starts)}`);
assert((await branch.agent(BACKGROUND_CONTEXT)).model.modelId === "coordinator", "The branch must use its advertised model");
assert(JSON.stringify(await conversationChains(harness, BACKGROUND_CONTEXT)(branch.id)) === JSON.stringify([String(branch.id), String(root.id), String(primary.id)]), "Fork and starter grant ancestry");
const before = (await entries2(primary)).length;
await (await branch.submit({ type: "input", content: "later work" }, BACKGROUND_CONTEXT)).wait(BACKGROUND_CONTEXT);
await until2(async () => (await entries2(primary)).some((e) => e.kind === "board.message" && e.data.from === branch.id && e.data.text === "Answer: later work" && e.data.delivery !== "note"));
await harness.waitForIdle(BACKGROUND_CONTEXT);
assert((await entries2(primary)).length > before, "An idle branch must report");
assert((await entries2(branch)).some((entry2) => entry2.kind === "board.message" && entry2.data.from === primary.id && entry2.data.routed === true), "The final answer must route back to the branch");
await until2(async () => {
const routeTasks = (await harness.commit((tx) => tx.scanTasks({ kind: "board.route" }, 1e3), BACKGROUND_CONTEXT)).items;
assert(routeTasks.every((task) => task.state.status !== "terminal" || task.state.outcome.status === "completed"), `Final-answer routing fault: ${JSON.stringify(routeTasks)}`);
return routeTasks.length > 0 && routeTasks.every((task) => task.state.status === "terminal");
});
await tool(branch, "subagent", { action: "start", agent: "scout", task: "HOLD", background: true });
await until2(() => holding.size > 0);
await until2(async () => (await entries2(primary)).some((e) => e.kind === "board.message" && e.data.from === branch.id && e.data.delivery === "note"));
+11
View File
@@ -10,6 +10,8 @@ import {openStorage} from './sqlite';
const assert = (value: any, message: string) => {if (!value) throw new Error(message)};
const until = async (test: () => any) => {const deadline = Date.now() + 15000; while (!await test()) {if (Date.now() > deadline) throw new Error('Messages deadline'); await new Promise(r => setTimeout(r, 0))}};
const models = createModels(), faux = fauxProvider({tokensPerSecond: 100000}); models.setProvider(faux.provider);
const template = models.getModels('faux')[0];
models.setProvider({...faux.provider, getModels: () => [template, {...template, id: 'coordinator'}]});
const registry = createRegistry(), storage = await openStorage(globalThis.storagePath);
const harness = await Harness.open(storage, {models, registry, settings: {get extensions() {return registry.snapshot().installed()}}}, context);
const root = await harness.root(context, {agent: {model: {provider: 'faux', modelId: 'faux-1'}, cwd: '/src/sausage'}});
@@ -99,12 +101,21 @@ try {
assert((await entries(branch)).some((e: any) => e.kind === 'board.message' && e.data.text === 'first task'), 'The first message did not reach the branch');
assert((await harness.commit((tx: any) => tx.conversation(branch.id), context)).parent.conversationId === root.id, 'Branch must fork its root');
assert((await harness.snapshot(Started, branch.id, context)).by === primary.id, 'Missing starter');
const starts = (await globalThis.testHost.eventLog.recent()).filter((event: any) => event.name === 'subagents.started' && event.data.id === branch.id);
assert(starts.length === 1 && starts[0].data.model === 'faux/coordinator', `Effective branch model: ${JSON.stringify(starts)}`);
assert((await branch.agent(context)).model.modelId === 'coordinator', 'The branch must use its advertised model');
assert(JSON.stringify(await conversationChains(harness, context)(branch.id)) === JSON.stringify([String(branch.id), String(root.id), String(primary.id)]), 'Fork and starter grant ancestry');
const before = (await entries(primary)).length;
await (await branch.submit({type: 'input', content: 'later work'}, context)).wait(context);
await until(async () => (await entries(primary)).some((e: any) => e.kind === 'board.message' && e.data.from === branch.id && e.data.text === 'Answer: later work' && e.data.delivery !== 'note'));
await harness.waitForIdle(context);
assert((await entries(primary)).length > before, 'An idle branch must report');
assert((await entries(branch)).some((entry: any) => entry.kind === 'board.message' && entry.data.from === primary.id && entry.data.routed === true), 'The final answer must route back to the branch');
await until(async () => {
const routeTasks = (await harness.commit((tx: any) => tx.scanTasks({kind: 'board.route'}, 1000), context)).items;
assert(routeTasks.every((task: any) => task.state.status !== 'terminal' || task.state.outcome.status === 'completed'), `Final-answer routing fault: ${JSON.stringify(routeTasks)}`);
return routeTasks.length > 0 && routeTasks.every((task: any) => task.state.status === 'terminal');
});
await tool(branch, 'subagent', {action: 'start', agent: 'scout', task: 'HOLD', background: true});
await until(() => holding.size > 0);
await until(async () => (await entries(primary)).some((e: any) => e.kind === 'board.message' && e.data.from === branch.id && e.data.delivery === 'note'));
+165
View File
@@ -0,0 +1,165 @@
//! Parse extension modules and reject free runtime identifiers outside the world runtime globals.
use deno_ast::swc::{
ast::{Ident, TsInterfaceDecl, TsType},
common::SyntaxContext,
ecma_visit::{Visit, VisitWith},
};
use deno_ast::{MediaType, ParseParams, ProgramRef, parse_module};
use std::path::Path;
// Standard JavaScript globals. World extensions also see Deno.
const GLOBALS: &[&str] = &[
"undefined",
"globalThis",
"Deno",
"console",
"crypto",
"fetch",
"performance",
"setTimeout",
"clearTimeout",
"setInterval",
"clearInterval",
"queueMicrotask",
"structuredClone",
"atob",
"btoa",
"parseInt",
"parseFloat",
"isNaN",
"Infinity",
"NaN",
"Object",
"Function",
"Boolean",
"Number",
"BigInt",
"String",
"Symbol",
"Array",
"Map",
"Set",
"WeakMap",
"WeakSet",
"WeakRef",
"FinalizationRegistry",
"Promise",
"Date",
"RegExp",
"Error",
"TypeError",
"RangeError",
"SyntaxError",
"AggregateError",
"JSON",
"Math",
"Reflect",
"Proxy",
"Intl",
"Iterator",
"Uint8Array",
"ArrayBuffer",
"TextEncoder",
"TextDecoder",
"URL",
"URLSearchParams",
"AbortController",
"AbortSignal",
"Request",
"Response",
"Headers",
"ReadableStream",
"WritableStream",
"TransformStream",
"Blob",
"File",
"FormData",
"DOMException",
"encodeURIComponent",
"decodeURIComponent",
];
fn undefined(source: &str) -> Vec<String> {
let parsed = parse_module(ParseParams {
specifier: "file:///extension.ts".parse().unwrap(),
text: source.into(),
media_type: MediaType::TypeScript,
capture_tokens: false,
scope_analysis: true,
maybe_syntax: None,
})
.unwrap();
struct FreeNames {
context: SyntaxContext,
names: Vec<String>,
}
impl Visit for FreeNames {
// Type signatures contain parameter names but do not execute.
fn visit_ts_type(&mut self, _node: &TsType) {}
fn visit_ts_interface_decl(&mut self, _node: &TsInterfaceDecl) {}
fn visit_ident(&mut self, ident: &Ident) {
if ident.ctxt == self.context && !GLOBALS.contains(&ident.sym.as_ref()) {
self.names.push(ident.sym.to_string());
}
}
}
let mut free = FreeNames {
context: parsed.unresolved_context(),
names: Vec::new(),
};
match parsed.program_ref() {
ProgramRef::Module(module) => module.visit_with(&mut free),
ProgramRef::Script(script) => script.visit_with(&mut free),
}
free.names.sort();
free.names.dedup();
free.names
}
fn scan(directory: &Path, failures: &mut Vec<String>) {
for entry in std::fs::read_dir(directory).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
scan(&path, failures);
} else if path.extension().is_some_and(|ext| ext == "ts")
&& !path.to_string_lossy().ends_with(".test.ts")
{
let names = undefined(&std::fs::read_to_string(&path).unwrap());
if !names.is_empty() {
failures.push(format!("{}: {}", path.display(), names.join(", ")));
}
}
}
}
#[test]
fn extension_identifiers_are_defined() {
let mut failures = Vec::new();
scan(
&Path::new(env!("CARGO_MANIFEST_DIR")).join("../extensions"),
&mut failures,
);
assert!(
failures.is_empty(),
"Undefined identifiers:\n{}",
failures.join("\n")
);
}
#[test]
fn catches_the_missing_route_import() {
let source = std::fs::read_to_string(
Path::new(env!("CARGO_MANIFEST_DIR")).join("../extensions/messages/route.ts"),
)
.unwrap();
let old = source.replace(
"import { ConversationsApi } from '../conversations/api.ts'\n",
"",
);
assert!(
undefined(&old)
.iter()
.any(|name| name == "ConversationsApi")
);
assert!(undefined("import { Api } from 'service'; type Callback = (arg: UnknownType) => void; interface Service { use(parameter: UnknownType): void }; function use(host: any) { const local = Api; return host.need(local) }").is_empty());
assert_eq!(
undefined("function use(host: any) { return host.need(MissingApi) }"),
["MissingApi"]
);
}
+6 -1
View File
@@ -48,6 +48,7 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
"extensions/lineage",
"extensions/models",
"extensions/core",
"extensions/conversations",
"agents",
] {
std::fs::create_dir_all(repo.join(directory))?;
@@ -70,6 +71,10 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
repo.join("extensions/probe/server.ts"),
"export default host => { globalThis.testHost = host; globalThis.subagentService = host.need({name:'subagents'}); globalThis.messageService = host.need({name:'messages'}); globalThis.lineageService = host.need({name:'lineage'}); host.implement({name:'conversations'}, {summaryModel: async () => globalThis.summaryRef, concludedIn: async () => new Set(), generatedTitle: async () => {}, setStanding: async () => {}, setAutomaticSummaries: async () => {}}); return {name:'probe'}; }",
)?;
std::fs::copy(
source.join("extensions/conversations/api.ts"),
repo.join("extensions/conversations/api.ts"),
)?;
std::fs::copy(
source.join("extensions/core/agents.ts"),
repo.join("extensions/core/agents.ts"),
@@ -88,7 +93,7 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
)?;
std::fs::write(
repo.join("agents/coordinator.md"),
"---\nname: coordinator\ndescription: Coordinate\nisolate: none\n---\nCoordinator agent\n",
"---\nname: coordinator\ndescription: Coordinate\nmodel: faux/coordinator\nisolate: none\n---\nCoordinator agent\n",
)?;
for args in [
vec!["init", "-b", "main"],