Skip to content
Open
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
7 changes: 3 additions & 4 deletions agent_subrun/README.mbt.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ Layers:
extracts. What the child RUNS to produce that report is not here: the
bounded turn itself (`execute_kind`, `capture_tool`) lives in
`agent_kind`, which never spawns and never knows it is in a child.
- `SubrunBudget`: the per-turn CALL allowance shared by model-initiated
subrun tools — a runaway backstop, reserved before launch; every granted
child runs at its kind's full step ceiling (engine-initiated subruns
like the goal-met gate bypass it).
- `SubrunBudget`: a reusable per-turn CALL allowance for direct consumers.
The CLI's model-initiated delegation uses hosted workflows and their
reserved child blocks instead; the automatic goal-met gate has its own allowance.
Known limits: a hard-killed child can orphan its own tool subprocesses (the
upstream group-kill gap) — the stdin-EOF grace path is the mitigation;
Windows support is deferred with background jobs.
4 changes: 4 additions & 0 deletions agent_subrun/host/moon.pkg
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import {
}

import {
"moonbitlang/async",
"moonbitlang/async/fs",
"bobzhang/openseek/agent_review",
"moonbitlang/workflow",
"moonbitlang/workflow/hosted",
"moonbitlang/core/json",
} for "test"
Expand Down
66 changes: 66 additions & 0 deletions agent_subrun/host/review_test.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
///|
/// Exercise the hosted process contract offline. The fixture rejects the
/// scout-shaped input and verifies the review criteria and step envelope.
async test "hosted review carries criteria and returns a structured report" {
let dir = @fs.tmpdir(prefix="hosted-review-")
defer (@fs.rmdir(dir, recursive=true) catch { _ => () })
let child = "\{dir}/review.mbtx"
@fs.write_file(
child,
(
#|import {
#| "moonbitlang/async",
#| "moonbitlang/async/stdio",
#| "moonbitlang/core/json",
#|}
#|async fn main {
#| guard @stdio.stdin.read_until("\n") is Some(line) else { return }
#| let request = @json.parse(line)
#| guard request is {
#| "workflow_contract": 1,
#| "kind": "review",
#| "max_steps": 100,
#| "input": { "goal": "Check CSV CRLF handling", "sha": "abc", "dirty": true, .. },
#| ..
#| } else { fail("unexpected review request") }
#| let response : Json = { "subrun_report": {
#| "schema_version": 1,
#| "scope": { "base": "abc", "head": "WORKTREE", "files": [] },
#| "findings": [],
#| "summary": "offline fixture report",
#| "stats": { "files_reviewed": 0, "findings": 0, "build": "skipped", "tests": "skipped" },
#| }}
#| println(response.stringify())
#|}
),
)
guard @hosted.parse({
"v": 1,
"exe": "moon",
"child_args": ["run", child],
"child_id": "review-test-sr-{n}",
"ids": [1, 1],
"journal": "\{dir}/journal.jsonl",
"events": "\{dir}/events.jsonl",
})
is Some(ctx) else {
fail("expected hosted context")
}
let json = ctx.run(wf => {
wf.agent_call(
kind="review",
input={ "goal": "Check CSV CRLF handling", "sha": "abc", "dirty": true },
label="CSV review",
max_steps=100,
)
})
guard @agent_review.ReviewReport::parse(json) is Ok(report) else {
fail("expected review report")
}
assert_eq(report.validate(), [])
assert_eq(report.finding_count(), 0)
let journal = @fs.read_file("\{dir}/journal.jsonl").text()
assert_true(journal.contains("review-test-sr-1"))
let events = @fs.read_file("\{dir}/events.jsonl").text()
assert_true(events.contains("\"status\":\"captured\""))
}
51 changes: 51 additions & 0 deletions agent_tool/mbtx/README.mbt.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,54 @@ and letting its subagents vanish. And whether or not the flag is set,
`WORKFLOW_HOST` is always DECIDED by the policy — the handoff, or nothing — so
a child engine's snippet can never inherit its grandparent's handoff and mint
child ids from a block that is not its own.

### Independent review

The model delegates an independent worktree audit through the same hosted
workflow as scouts. Set `subrun: true` on the `mbtx` tool call and supply this
program as `source`:

```mbt nocheck
///|
import {
"moonbitlang/async",
"moonbitlang/workflow",
"moonbitlang/workflow/hosted",
}

///|
async fn main {
guard @hosted.context() is Some(ctx) else {
println("Hosted delegation is unavailable in this session")
return
}
let report = ctx.run(wf => {
wf.agent_call(
kind="review",
input={
"goal": "Check the CSV parser's CRLF and escaped-quote handling.",
},
label="CSV review",
max_steps=100,
)
})
println(report.stringify())
}
```

Use `agent_call`, not `agent`: review requires `input.goal`, whereas `agent`
builds the scout input `{query, hints?}`. Criteria are explicit; this path
does not automatically read the standing goal or its baseline. If known,
include `sha` and `dirty` alongside `goal` to describe the baseline commit
and whether the worktree was dirty when it was recorded.

The child runs `agent_review.run_goal_audit` and validates its submission
before returning the full report JSON. The workflow does not reduce it to a
digest or mark blocker findings as a tool error: the caller reads the findings.
Failure to obtain a report raises instead of returning a clean verdict.
Hosted child limits apply (32 launches per snippet, 10-minute default child
deadline, and the explicit step ceiling); there is no shared per-turn review
budget. `--review-deadline` applies only to the automatic goal-met gate.
Without a durable session, hosted delegation is unavailable. The standalone
`openseek review --base REF` remains available and calls the review engine
directly, without hosted workflow.
11 changes: 11 additions & 0 deletions agent_tool/mbtx/mbtx.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -1583,6 +1583,17 @@ fn description(
#|A snippet may start at most 32 children. Without the flag no child sessions are
#|reserved and the engine is not on the spawn allowlist: a workflow written
#|without it runs with no handoff and its subagents are refused.
#|
#|For an independent worktree review, use `wf.agent_call`, NOT `wf.agent`
#|(which sends a scout's `query` field). Pass your actual audit criteria explicitly:
#|`wf.agent_call(kind="review", input={"goal": "Check the CSV parser's CRLF and escaped-quote handling"}, label="CSV review", max_steps=100)`.
#|The reviewer reads the current worktree and returns a full structured report
#|with `findings`, `summary`, and `stats`; print the report with `.stringify()`
#|and address its findings. A missing report raises an error, not a clean verdict.
#|No standing goal or baseline is filled in automatically. Optionally include
#|`sha` and `dirty` in input only when you know the baseline commit and whether
#|the worktree was already dirty when it was recorded. Hosted children default
#|to a 10-minute deadline; `--review-deadline` controls only the automatic gate.
)
} else {
""
Expand Down
12 changes: 9 additions & 3 deletions cmd/openseek/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,15 @@ with `OPENSEEK_SYSTEM_PROMPT_FILE` and
`OPENSEEK_SYSTEM_PROMPT_ADDENDUM_FILE`. `--session` explicitly creates or
resumes that session under `--session-root` (default `.openseek`). Relative
session roots are resolved under `--dir`.
`--review-deadline` bounds one review audit (the model-callable `review` tool or
the `--review-gate` audit) to that many milliseconds; the default is 900000
(15 minutes).
`--review-deadline` bounds one automatic `--review-gate` audit to that many
milliseconds; the default is 900000 (15 minutes). Model-initiated reviews use
hosted `mbtx` workflows with explicit audit criteria, the hosted child limit
(32 per snippet), and a per-call `max_steps`. Their default child deadline is
600000 (10 minutes), independent of this flag. See the
[hosted review example](../../agent_tool/mbtx/README.mbt.md#independent-review).
Hosted delegation requires a durable session; `--no-session` cannot delegate.
The standalone `openseek review --base REF` command still runs the review
engine directly, without a workflow.

Every run records a durable session: without `--session`, a generated
`cli-YYYYMMDD-HHMMSS-mmm` id is used and announced by a `session_started` event
Expand Down
15 changes: 6 additions & 9 deletions cmd/openseek/gate.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,9 @@ fn review_subrun_input(
/// only external cancellation re-raises (through run_subrun's own
/// contract).
///
/// The gate does NOT draw from the shared `SubrunBudget`: that allowance
/// meters MODEL-initiated delegation, while the gate is engine-initiated
/// and fires at most once per met claim. Sharing let a turn's model-initiated
/// subrun calls starve the audit into an underpowered run that died at its step
/// ceiling unreported (ccomp-run10) — the one subrun that must not be
/// shortchanged is the one auditing the claim everything else rests on.
/// The gate has its own step allowance and fires at most once per met claim,
/// independently of model-initiated hosted workflows. The audit must retain
/// enough steps to inspect the claim even after the model delegated other work.
fn build_goal_met_gate(
self_exe~ : String,
model~ : @deepseek.Model,
Expand Down Expand Up @@ -109,14 +106,14 @@ fn review_gate_flag() -> @argparse.FlagArg {

///|
/// The `--review-deadline` option (milliseconds), shared by `run` and `serve`
/// like `--review-gate`: the wall clock one review audit (the model-callable
/// review tool or the `--review-gate` audit) may take before it is treated as
/// like `--review-gate`: the wall clock one automatic goal-met audit
/// may take before it is treated as
/// stuck. Absent, the audit falls back to `GateDeadlineMs`.
fn review_deadline_option() -> @argparse.OptionArg {
OptionArg(
"review-deadline",
long="review-deadline",
about="Wall-clock deadline in milliseconds for one review audit (the review tool or --review-gate); default 900000 (15 minutes).",
about="Wall-clock deadline in milliseconds for one --review-gate audit; default 900000 (15 minutes).",
)
}

Expand Down
29 changes: 4 additions & 25 deletions cmd/openseek/main.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -298,31 +298,14 @@ async fn run_task_turn(
)
let runtime = @agent_runtime.AgentRuntime(workspace_root~, approval?)
let scope = @agent_runtime.AgentTaskScope(group)
// One-shot = one turn, so a fresh per-turn subrun budget needs no reset.
let subrun_budget = @agent_subrun.SubrunBudget()
let exe = self_executable()
// Mirror the latest appended session so the review tool audits against
// the LIVE standing goal, not the turn-start snapshot.
let latest_session : Ref[@agent_session.Session] = { val: session, }
let review_context = () => {
let goal = latest_session.val.current_goal()
(
goal.map(goal => goal.text()),
latest_session.val.current_goal_baseline(),
)
}
let extra_tools = build_extra_tools(
matches,
scope,
self_exe=exe,
model~,
api_key~,
workspace_root~,
budget=subrun_budget,
review_context~,
review_deadline_ms~,
api_url?,
child_session?,
)
// A snippet's workflow delegates to children named from the SAME ordinal
// counter `run_subrun` uses, so a snippet's child and a `review` child
Expand Down Expand Up @@ -353,11 +336,7 @@ async fn run_task_turn(
model~,
session~,
task~,
append_item=(current, item) => {
let next = append_item(current, item)
latest_session.val = next
next
},
append_item~,
max_steps?,
thinking~,
api_url?,
Expand Down Expand Up @@ -881,12 +860,12 @@ fn retry_overrides(matches : @argparse.Matches) -> (Int?, Int?) raise {
///
/// `--retry-attempts 2` and `OPENSEEK_RETRY_ATTEMPTS=2` must mean the same
/// thing to a subagent, and before this a child got only the second: nothing
/// injects these on the child command line, and four separate spawn sites
/// would each have to — the review gate, the model-callable review tool, and
/// injects these on the child command line, and separate spawn sites
/// would each have to — the review gate and
/// the workflow scout and worker runners, the last two reachable only through
/// the `SubrunInjection` handoff an `mbtx` snippet receives. Children do
/// inherit the environment (`@process.spawn(inherit_env=true)`), so writing
/// the flag back closes all four at once. It is also the shape `--api-key`
/// the flag back covers them all at once. It is also the shape `--api-key`
/// already has: argv carries what a child cannot otherwise know, and the
/// environment carries the rest.
///
Expand Down
Loading
Loading