@robmclarty/volley 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,17 +1,18 @@
1
- import { Engine, FinishReason, StreamChunk, Throughput, UsageTotals } from "fascicle";
1
+ import { Engine, FinishReason, StreamChunk, Throughput, Tool, ToolApprovalHandler, TrajectoryLogger, UsageTotals } from "fascicle";
2
2
  import { z } from "zod";
3
3
  //#region src/types.d.ts
4
4
  type Verdict = 'approved' | 'changes_requested';
5
5
  type CriticPreset = 'reviewer' | 'optimizer' | 'researcher';
6
6
  /** Transport for the critic role. The critic is read-only + schema-constrained,
7
- * so it has always been able to run on a local model. */
8
- type CriticProvider = 'claude_cli' | 'ollama' | 'lmstudio';
7
+ * so it has always been able to run on a local model. `openrouter` reaches a
8
+ * hosted model over one API key and takes the local critic's tools and
9
+ * degradation ladder. */
10
+ type CriticProvider = 'claude_cli' | 'ollama' | 'lmstudio' | 'openrouter';
9
11
  /** Transport for the builder role. `claude_cli` is a full agentic Claude Code
10
- * session; the local providers run the builder as a volley-driven tool loop
11
- * (write/exec tools supplied by the harness). The execution path for the local
12
- * providers lands with the builder tool/termination work; this type and its
13
- * plumbing exist so that work has somewhere to attach (spec v3). */
14
- type BuilderProvider = 'claude_cli' | 'ollama' | 'lmstudio';
12
+ * session; every other provider runs the builder as a volley-driven tool loop
13
+ * (write/exec tools supplied by the harness), whether its model is local
14
+ * (`ollama`, `lmstudio`) or hosted (`openrouter`). */
15
+ type BuilderProvider = 'claude_cli' | 'ollama' | 'lmstudio' | 'openrouter';
15
16
  type BuilderPermissionMode = 'acceptEdits' | 'bypassPermissions';
16
17
  /** `auto`, `none`, or any shell command (the `string & {}` keeps the literal
17
18
  * hints in editor completions without collapsing the union). */
@@ -22,6 +23,11 @@ type CostSource = 'provider_reported' | 'engine_derived' | 'unknown';
22
23
  * died. Recorded alongside a critic retry so a degraded run says
23
24
  * *what* failed, not just that it retried. */
24
25
  type CauseKind = 'provider_5xx' | 'network' | 'unknown';
26
+ /** Why a local critic's verdict came from the tool-less fallback:
27
+ * `stream_error`, the tool-bearing call kept dying on the provider's stream
28
+ * after the bounded retry; `step_exhaustion`, it spent its step budget (or
29
+ * stopped on a tool call) without producing a verdict. */
30
+ type DegradeCause = 'stream_error' | 'step_exhaustion';
25
31
  type RunStatus = 'running' | 'success' | 'budget_exhausted' | 'cost_cap_reached' |
26
32
  /** The builder edited the gate that judges it, under `--fail-on-gate-edit`:
27
33
  * the run halts on the spot rather than iterating, because a check the builder
@@ -51,13 +57,14 @@ type VolleyConfig = {
51
57
  gate_paths?: string[];
52
58
  fail_on_gate_edit?: boolean;
53
59
  sandbox_image?: string;
60
+ state_dir?: string;
54
61
  verbose?: boolean;
55
62
  quiet?: boolean;
56
63
  json?: boolean;
57
64
  show_thinking?: boolean;
58
65
  dry_run?: boolean;
59
66
  };
60
- /** Fully resolved run configuration; `.volley/config.json` mirrors this. */
67
+ /** Fully resolved run configuration; the run's `config.json` mirrors this. */
61
68
  type ResolvedConfig = {
62
69
  version: 2;
63
70
  run_id: string;
@@ -72,12 +79,13 @@ type ResolvedConfig = {
72
79
  * It is normalized, and crossed to the host LLM endpoint when contained, only
73
80
  * where it is used, because the crossing depends on where the run executes.
74
81
  * Null falls back to the provider's env var (`VOLLEY_OLLAMA_URL` /
75
- * `VOLLEY_LMSTUDIO_URL`), then its default, never to the critic's URL. Only
76
- * a provider volley reaches at a base URL (ollama, lmstudio) takes one. */
82
+ * `VOLLEY_LMSTUDIO_URL` / `VOLLEY_OPENROUTER_URL`), then its default, never to
83
+ * the critic's URL. Only a provider volley reaches at a base URL (ollama,
84
+ * lmstudio, openrouter) takes one. */
77
85
  builder_url: string | null;
78
86
  builder_max_steps: number;
79
- /** Operator opt-out permitting a local (non-`claude_cli`) builder to run
80
- * without a sandbox — it gets a real host bash. Refused by default. */
87
+ /** Operator opt-out permitting a non-`claude_cli` builder to run without a
88
+ * sandbox — it gets a real host bash. Refused by default. */
81
89
  allow_unsandboxed_builder: boolean;
82
90
  critic_model: string;
83
91
  critic_provider: CriticProvider;
@@ -92,7 +100,7 @@ type ResolvedConfig = {
92
100
  git_checkpoints: boolean;
93
101
  /** Isolate the builder run's effects in a per-run git worktree: the
94
102
  * builder/critic file tools' containment root moves to the worktree, while
95
- * `.volley/` state stays under `workspace` (the control plane). Off by
103
+ * the run's state stays where `state_dir` puts it (the control plane). Off by
96
104
  * default; requires the workspace to be a git repository. */
97
105
  worktree: boolean;
98
106
  /** Throw a `--worktree` run's effects away at teardown even when it converged:
@@ -119,6 +127,10 @@ type ResolvedConfig = {
119
127
  * `VOLLEY_SANDBOX_IMAGE` override it. Meaningful only for a local builder —
120
128
  * the `claude_cli` path never uses Docker. */
121
129
  sandbox_image: string;
130
+ /** The root that holds each run's state at `<state_dir>/<run_id>/`
131
+ * (`--state-dir`), resolved to an absolute path. Null keeps it at
132
+ * `<workspace>/.volley/`, rotated aside by the next fresh run. */
133
+ state_dir: string | null;
122
134
  workspace: string;
123
135
  verbose: boolean;
124
136
  quiet: boolean;
@@ -209,12 +221,15 @@ type PhaseRecord = {
209
221
  retry_cause_kind?: CauseKind;
210
222
  /** This local-critic verdict was rendered by the tool-less fallback rung:
211
223
  * the tool-bearing critique kept dying on the provider's stream
212
- * even after the bounded retry, so the critic judged with no read access —
213
- * grounded only by the criteria, the raw check artifacts, and a workspace file
214
- * inventory. Set (true) on a degraded critic record; absent on a full
215
- * critique and on the builder. Additive, and a degraded verdict is *always*
216
- * marked so it never passes silently as a full one. */
224
+ * even after the bounded retry, or ran out of steps before its verdict, so the
225
+ * critic judged with no read access — grounded only by the criteria, the raw
226
+ * check artifacts, and a workspace file inventory. Set (true) on a degraded
227
+ * critic record; absent on a full critique and on the builder. Additive, and a
228
+ * degraded verdict is *always* marked so it never passes silently as a full
229
+ * one. */
217
230
  critic_degraded?: boolean;
231
+ /** Why the verdict degraded, set with `critic_degraded`. Additive. */
232
+ degrade_cause?: DegradeCause;
218
233
  /** Output tokens per second over the model's own turns (tool execution and
219
234
  * retry backoff excluded), from fascicle's per-step timing. `basis` is
220
235
  * `decode` when every turn streamed, `blended` when any round trip counted
@@ -287,12 +302,92 @@ type ResolveOptions = {
287
302
  run_id?: string;
288
303
  started_at?: string;
289
304
  env?: Record<string, string | undefined>;
305
+ /** What happens to a tool-loop builder with nothing between its commands
306
+ * and the host (`unguarded_builder`): the CLI `refuse`s it, the default,
307
+ * while under `volley acp` each of its commands `ask`s a person first, which
308
+ * takes the container's place. */
309
+ uncontained_builder?: 'refuse' | 'ask';
290
310
  };
291
311
  /** Merge, expand, and validate a raw `VolleyConfig` into a `ResolvedConfig`.
292
312
  * `check_resolved` comes from check detection against the workspace, so the
293
313
  * SDK and the CLI resolve `auto` and a command check the same way. */
294
314
  export declare function resolve_config(raw: VolleyConfig, options?: ResolveOptions): ResolvedConfig;
295
315
  //#endregion
316
+ //#region src/builder/tools.d.ts
317
+ /** The raw outcome of running one bash command, before the model-facing
318
+ * truncation + timeout marker the `bash` tool applies. `status` is the exit code
319
+ * (null when the process was killed by a signal — e.g. the timeout SIGKILL);
320
+ * `timed_out` marks the timeout path. */
321
+ type BashOutcome = {
322
+ status: number | null;
323
+ stdout: string;
324
+ stderr: string;
325
+ timed_out: boolean;
326
+ };
327
+ /** How the `bash` tool runs a command: the local `host_bash_executor`
328
+ * (`spawnSync`) — which under whole-process containment runs *inside*
329
+ * volley's own hardened container, no `docker exec` hop. The executor runs one
330
+ * command to completion and returns its raw outcome; the `bash` tool owns the
331
+ * truncation + timeout marker. Kept as an injectable seam so tests can supply a
332
+ * fake and a future shape could re-point it (the `docker exec` executor was
333
+ * retired). */
334
+ type BashExecutor = (command: string, options: {
335
+ timeout_ms: number;
336
+ max_capture_bytes: number;
337
+ }) => BashOutcome;
338
+ //#endregion
339
+ //#region src/workspace_tools.d.ts
340
+ /** Which tool set a seat holds, so its prompt names the tools it has: volley's
341
+ * own (`'volley'`), or fascicle's workspace tools under `volley acp`
342
+ * (`'acp'`). */
343
+ type ToolSet = 'volley' | 'acp';
344
+ //#endregion
345
+ //#region src/builder.d.ts
346
+ type BuilderDeps = {
347
+ engine: Engine;
348
+ config: ResolvedConfig;
349
+ on_chunk: (chunk: StreamChunk) => void;
350
+ /** The `bash` executor for a sandboxed local builder (`docker exec` against
351
+ * the run's container), or null to use the host `spawnSync` default (the
352
+ * unsandboxed escape hatch, or `claude_cli` which supplies no volley tools). */
353
+ bash_executor?: BashExecutor | null;
354
+ /** Tools to hand the builder in place of volley's own set; absent, it gets
355
+ * that set. A `claude_cli` builder brings its own tools and never takes
356
+ * these. */
357
+ tools?: ReadonlyArray<Tool>;
358
+ /** Which set `tools` is, so the prompt names the tools the builder holds;
359
+ * absent, volley's own. */
360
+ tool_set?: ToolSet;
361
+ /** Answers each call to a tool that asks first (`needs_approval`). Like
362
+ * `tools`, it reaches every builder but `claude_cli`. Absent, such a call
363
+ * fails closed. */
364
+ on_tool_approval?: ToolApprovalHandler;
365
+ };
366
+ //#endregion
367
+ //#region src/critic/run.d.ts
368
+ export declare const verdict_schema: z.ZodObject<{
369
+ verdict: z.ZodEnum<{
370
+ approved: "approved";
371
+ changes_requested: "changes_requested";
372
+ }>;
373
+ feedback: z.ZodString;
374
+ unmet_criteria: z.ZodArray<z.ZodString>;
375
+ }, z.core.$strip>;
376
+ type VerdictOutput = z.infer<typeof verdict_schema>;
377
+ type CriticDeps = {
378
+ engine: Engine;
379
+ config: ResolvedConfig;
380
+ on_chunk: (chunk: StreamChunk) => void;
381
+ /** Tools to hand the critic's tool-bearing call in place of volley's reads;
382
+ * absent, it gets those. A `claude_cli` critic reads through the CLI's own
383
+ * tools and never takes these. The critic takes no approval handler, since
384
+ * nothing it reads with should ever ask. */
385
+ tools?: ReadonlyArray<Tool>;
386
+ /** Which set `tools` is, so the harness append names the reads the critic
387
+ * holds; absent, volley's own. */
388
+ tool_set?: ToolSet;
389
+ };
390
+ //#endregion
296
391
  //#region src/render/format.d.ts
297
392
  type Role = 'builder' | 'critic' | 'check';
298
393
  //#endregion
@@ -303,6 +398,9 @@ type RendererOptions = {
303
398
  show_thinking: boolean;
304
399
  color: boolean;
305
400
  max_cost_usd: number | null;
401
+ /** Where the run's trajectory is written, named when verbose output is cut
402
+ * short (default: `.volley/trajectory.jsonl`). */
403
+ trajectory_path?: string;
306
404
  write?: (text: string) => void;
307
405
  };
308
406
  type Renderer = {
@@ -318,6 +416,30 @@ type Renderer = {
318
416
  };
319
417
  export declare function create_renderer(options: RendererOptions): Renderer;
320
418
  //#endregion
419
+ //#region src/phases.d.ts
420
+ /** How the check runs when the caller sets it, as `volley acp` does. Left
421
+ * out, the check inherits volley's environment and runs unasked, as on the
422
+ * CLI. */
423
+ type CheckGate = {
424
+ /** The whole environment the check runs with, in place of volley's own. */
425
+ env?: Readonly<Record<string, string>>;
426
+ /** Asked on the check's card before each check, the way a model call asks
427
+ * before a gated tool runs. Anything short of an allow keeps the check from
428
+ * running, and it's recorded as failed. */
429
+ on_approval?: ToolApprovalHandler;
430
+ };
431
+ //#endregion
432
+ //#region src/flow.d.ts
433
+ /** The builder seat: the engine its calls go through, and, when the caller
434
+ * injects them, the tools that replace volley's own set, the name of that set
435
+ * (which its prompt names), and the handler that answers their approval
436
+ * requests. */
437
+ type BuilderSeat = Pick<BuilderDeps, 'engine' | 'tools' | 'tool_set' | 'on_tool_approval'>;
438
+ /** The critic seat: its engine and, when injected, the tools that replace its
439
+ * reads and the name of their set. It takes no approval handler, since its
440
+ * reads never ask. */
441
+ type CriticSeat = Pick<CriticDeps, 'engine' | 'tools' | 'tool_set'>;
442
+ //#endregion
321
443
  //#region src/orchestrator.d.ts
322
444
  type OrchestratorDeps = {
323
445
  renderer: Renderer;
@@ -325,6 +447,22 @@ type OrchestratorDeps = {
325
447
  * config. It serves both seats as-is, so the seat URLs then steer only
326
448
  * volley's own requests (prewarm and the context probe), not model calls. */
327
449
  engine?: Engine;
450
+ /** What to hand each seat in place of volley's own wiring: the builder's
451
+ * tools and the handler that answers their approval requests, and the
452
+ * critic's tools. A seat or a field left out keeps today's wiring, and a
453
+ * `claude_cli` seat, which brings its own tools, takes none of it. */
454
+ seats?: {
455
+ builder?: Omit<BuilderSeat, 'engine'>;
456
+ critic?: Omit<CriticSeat, 'engine'>;
457
+ };
458
+ /** A second sink for the run's trajectory, beside the run's
459
+ * `trajectory.jsonl`. With one, the flow runs through `run.stream`, so the
460
+ * sink also receives each model call's streamed chunks as `model_chunk`
461
+ * events, which the file never keeps. */
462
+ trajectory?: TrajectoryLogger;
463
+ /** The environment the check runs with and the approval it asks first, in
464
+ * place of inheriting volley's environment and running unasked. */
465
+ check?: CheckGate;
328
466
  abort?: AbortSignal;
329
467
  install_signal_handlers?: boolean;
330
468
  };
@@ -346,17 +484,6 @@ export declare function gate(config: ResolvedConfig, state: LoopState): {
346
484
  };
347
485
  export declare function status_of(value: LoopState, converged: boolean): RunStatus;
348
486
  //#endregion
349
- //#region src/critic/run.d.ts
350
- export declare const verdict_schema: z.ZodObject<{
351
- verdict: z.ZodEnum<{
352
- approved: "approved";
353
- changes_requested: "changes_requested";
354
- }>;
355
- feedback: z.ZodString;
356
- unmet_criteria: z.ZodArray<z.ZodString>;
357
- }, z.core.$strip>;
358
- type VerdictOutput = z.infer<typeof verdict_schema>;
359
- //#endregion
360
487
  //#region src/sandbox.d.ts
361
488
  /** Canonical identity of the allowlist bridge the examples create (`docker
362
489
  * network create --subnet <SANDBOX_NETWORK_SUBNET> <SANDBOX_NETWORK_NAME>`) and