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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to `simplicio-prompt` are documented here.
Format roughly follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
versioning is [SemVer](https://semver.org/).

## [1.12.0] — 2026-05-29 — SendSprint batch JSONL

### Added
- `simplicio-prompt --batch tasks.jsonl --subagents N --json` batch execution
mode for SendSprint fan-out. It reuses one provider/runtime boot for many
task prompts, emits one NDJSON result per input line, preserves `task_id`,
honors existing runtime flags such as `--dry-run`, `--provider`, `--model`,
`--subagents`, `--no-cache` and `--diversify`, and keeps processing after
malformed lines or per-task errors.

## [1.11.0] — 2026-05-29 — Phase 1 + integration + infra

Roadmap **Phase 1 (Diverse-prompt fan-out)** kernel-side delivery plus the
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ Opt-in paths to BATCH:
BATCH directive.
- Run `npx simplicio-prompt --batch` / `--batch --raw` to print the BATCH
template.
- Run `npx simplicio-prompt --batch tasks.jsonl --subagents 600 --json` to
process many SendSprint items with one runtime/provider boot and emit one
NDJSON result per task.
- Import `getBatchPrompt()` / `getBatchPromptSection()` from the npm API,
or use the `simplicio-prompt/batch-prompt` package export.
- Have your coding agent invoke `simplicio-subagents` via shell when the
Expand Down Expand Up @@ -174,6 +177,21 @@ python kernel/subagent_runtime.py --provider mimo --task "..."
python kernel/subagent_runtime.py --provider local --subagents 50 --task "..." # Ollama
```

For orchestrators that already have many task prompts, use JSONL batch mode.
Each input line is one object:

```json
{"task_id":"WS-101","prompt":"Audit this change","system":"You are a reviewer"}
```

```bash
simplicio-prompt --batch tasks.jsonl --subagents 600 --dry-run --json > out.ndjson
```

Each output line is `{ "task_id": "...", "status": "ok", "result": ... }` or
`{ "task_id": "...", "status": "error", "error": "..." }`. A malformed line or
single task error does not stop the rest of the batch.

Or programmatically:

```python
Expand Down
68 changes: 68 additions & 0 deletions bin/simplicio-prompt.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
* npx simplicio-prompt --install AGENTS.md
* npx simplicio-prompt --install .cursorrules
*/
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import {
Expand Down Expand Up @@ -56,6 +57,10 @@ function parseArgs(argv) {
target: null, // legacy --install path
raw: false,
batch: false, // --batch selects the BATCH template (fan-out runtime)
batchFile: null, // --batch <tasks.jsonl> runs tasks through one kernel boot
json: false,
dryRun: false,
passthrough: [],
pluginTargets: [], // --target <id>, repeatable
installAll: false,
};
Expand Down Expand Up @@ -84,6 +89,36 @@ function parseArgs(argv) {
args.raw = true;
} else if (k === "--batch" || k === "--full-runtime") {
args.batch = true;
const next = argv[i + 1];
if (next && !next.startsWith("--")) {
args.mode = "batch-run";
args.batchFile = next;
i++;
}
} else if (k === "--json") {
args.json = true;
} else if (k === "--dry-run") {
args.dryRun = true;
} else if (
[
"--subagents",
"--provider",
"--model",
"--lane",
"--max-tokens",
"--temperature",
"--overall-timeout-s",
"--show",
"--diversify-seed",
].includes(k)
) {
const next = argv[i + 1];
if (next && !next.startsWith("--")) {
args.passthrough.push(k, next);
i++;
}
} else if (["--no-cache", "--diversify"].includes(k)) {
args.passthrough.push(k);
} else if (k === "--path") {
args.mode = "path";
} else if (k === "--help" || k === "-h") {
Expand Down Expand Up @@ -134,6 +169,8 @@ Two templates ship:
Print / inspect:
simplicio-prompt Print ONE-SHOT prompt to stdout
simplicio-prompt --batch Print BATCH prompt to stdout
simplicio-prompt --batch tasks.jsonl --subagents 600 --json
Run JSONL batch and emit NDJSON
simplicio-prompt --raw Print only the Prompt section
simplicio-prompt --batch --raw Print only the BATCH Prompt body
simplicio-prompt --path Print absolute path of the prompt file
Expand All @@ -158,6 +195,7 @@ Examples:
npx simplicio-prompt --target claude-code
npx simplicio-prompt --target cursor --target copilot
npx simplicio-prompt --install-all
npx simplicio-prompt --batch tasks.jsonl --subagents 600 --dry-run --json
npx simplicio-prompt --raw > my-prompt.md
`);
}
Expand Down Expand Up @@ -256,13 +294,43 @@ function printTargetList() {
}
}

function runBatch(args) {
if (!args.batchFile) {
throw new Error("batch mode requires a JSONL file after --batch");
}
const python = process.env.PYTHON || process.env.PYTHON3 || "python3";
const runner = resolve(PKG_ROOT, "kernel", "subagent_runtime.py");
const pyArgs = [
runner,
"--batch",
resolve(process.cwd(), args.batchFile),
...args.passthrough,
];
if (args.dryRun) pyArgs.push("--dry-run");
if (args.json) pyArgs.push("--json");

const result = spawnSync(python, pyArgs, {
cwd: PKG_ROOT,
encoding: "utf-8",
});
if (result.error) {
throw result.error;
}
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}

function main() {
const args = parseArgs(process.argv.slice(2));

if (args.mode === "help") return printHelp();
if (args.mode === "version") return console.log(loadVersion());
if (args.mode === "path") return console.log(selectPromptPath(args));
if (args.mode === "list-targets") return printTargetList();
if (args.mode === "batch-run") return runBatch(args);

const promptPath = selectPromptPath(args);
const { full, section: body } = loadPromptAsset(promptPath);
Expand Down
12 changes: 12 additions & 0 deletions kernel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ python kernel/subagent_runtime.py --provider deepseek --subagents 600 \
--task "..." --dry-run # opt-in to max-breadth
```

Batch JSONL mode reuses one provider/runtime boot for many tasks and emits one
line per result when `--json` is set:

```bash
python kernel/subagent_runtime.py --batch tasks.jsonl --subagents 600 \
--dry-run --json > out.ndjson
```

Each input line is `{ "task_id": "...", "prompt": "...", "system": "..." }`.
Malformed lines and per-task failures emit `status: "error"` and do not stop the
remaining tasks.

## Safe speed model

The reference kernel increases throughput without provider-ban risk by avoiding
Expand Down
2 changes: 1 addition & 1 deletion kernel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
``batch_spawn`` model into real subagents.
"""

__version__ = "1.11.0"
__version__ = "1.12.0"
125 changes: 122 additions & 3 deletions kernel/subagent_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
# offline cost projection / demo, no API key, no network:
python kernel/subagent_runtime.py --provider deepseek --subagents 600 \\
--task "..." --dry-run

# batch JSONL mode, one runtime/provider boot for many SendSprint items:
python kernel/subagent_runtime.py --batch tasks.jsonl --subagents 600 \\
--dry-run --json
"""

from __future__ import annotations
Expand Down Expand Up @@ -92,6 +96,18 @@ def default_prompt_builder(task: str, index: int, total: int) -> Dict[str, Any]:
}


def batch_prompt_builder(system: str | None = None) -> PromptBuilder:
"""Build per-subagent prompts for one batch item, preserving its system text."""

def build(task: str, index: int, total: int) -> Dict[str, Any]:
payload = default_prompt_builder(task, index, total)
if system:
payload["system"] = f"{system}\n\n{payload['system']}"
return payload

return build


@dataclass
class SubagentResult:
agent_id: int
Expand Down Expand Up @@ -344,7 +360,12 @@ def _parse_args(argv: List[str]) -> argparse.Namespace:
)
parser.add_argument("--provider", default=None, help="deepseek | mimo | local | <preset>")
parser.add_argument("--model", default=None, help="override the provider model")
parser.add_argument("--task", required=True, help="the task to fan out")
parser.add_argument("--task", default=None, help="the task to fan out")
parser.add_argument(
"--batch",
default=None,
help="JSONL file with one {task_id,prompt,system?} object per line",
)
parser.add_argument(
"--subagents",
type=int,
Expand Down Expand Up @@ -395,7 +416,102 @@ def _parse_args(argv: List[str]) -> argparse.Namespace:
parser.add_argument(
"--show", type=int, default=3, help="how many sample outputs to print (text mode)"
)
return parser.parse_args(argv)
args = parser.parse_args(argv)
if not args.batch and not args.task:
parser.error("--task is required unless --batch is provided")
return args


def _batch_error(task_id: str, message: str, *, line: int | None = None) -> Dict[str, Any]:
payload: Dict[str, Any] = {
"task_id": task_id,
"status": "error",
"error": message,
}
if line is not None:
payload["line"] = line
return payload


def _iter_batch_tasks(path: str):
with open(path, "r", encoding="utf-8") as handle:
for line_no, raw in enumerate(handle, start=1):
text = raw.strip()
if not text:
continue
try:
payload = json.loads(text)
except json.JSONDecodeError as error:
yield None, _batch_error(
f"line:{line_no}",
f"malformed JSON: {error.msg}",
line=line_no,
)
continue
if not isinstance(payload, dict):
yield None, _batch_error(
f"line:{line_no}",
"batch line must be a JSON object",
line=line_no,
)
continue
task_id = str(payload.get("task_id") or f"line:{line_no}")
prompt = payload.get("prompt")
if not isinstance(prompt, str) or not prompt.strip():
yield None, _batch_error(task_id, "prompt is required", line=line_no)
continue
system = payload.get("system")
if system is not None and not isinstance(system, str):
yield None, _batch_error(task_id, "system must be a string", line=line_no)
continue
yield {
"task_id": task_id,
"prompt": prompt,
"system": system,
"line": line_no,
}, None


def _run_batch(args: argparse.Namespace, runtime: SubagentRuntime) -> int:
assert args.batch is not None
for task, error in _iter_batch_tasks(args.batch):
if error is not None:
if args.json:
print(json.dumps(error, ensure_ascii=False))
else:
print(f"{error['task_id']}: ERROR {error['error']}")
continue
assert task is not None
try:
report = runtime.run(
task["prompt"],
subagents=args.subagents,
lane=args.lane,
prompt_builder=batch_prompt_builder(task.get("system")),
use_cache=not args.no_cache,
diversify=args.diversify,
diversify_seed=args.diversify_seed,
overall_timeout_s=args.overall_timeout_s,
)
payload = {
"task_id": task["task_id"],
"status": "ok" if report.status == "ok" else report.status,
"result": report.to_dict(),
}
except Exception as error: # noqa: BLE001 - keep batch items isolated
payload = _batch_error(task["task_id"], str(error), line=task["line"])
if args.json:
print(json.dumps(payload, ensure_ascii=False))
else:
if payload["status"] == "ok":
result = payload["result"]
print(
f"{payload['task_id']}: ok "
f"{result['completed']}/{result['requested']} subagents"
)
else:
print(f"{payload['task_id']}: ERROR {payload['error']}")
return 0


def main(argv: Optional[List[str]] = None) -> int:
Expand All @@ -409,8 +525,11 @@ def main(argv: Optional[List[str]] = None) -> int:
runtime = SubagentRuntime(
provider, max_tokens=args.max_tokens, temperature=args.temperature
)
if args.batch:
return _run_batch(args, runtime)

report = runtime.run(
args.task,
args.task or "",
subagents=args.subagents,
lane=args.lane,
use_cache=not args.no_cache,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "simplicio-prompt",
"version": "1.11.0",
"version": "1.12.0",
"description": "Multi-IDE plugin: Tuple-Space + Yool safe-speed runtime prompt for Claude Code, Codex, Hermes, OpenCode/OpenClaw, Cursor, GitHub Copilot, Cline, Aider, Gemini CLI. One installer drops the runtime contract into the right rule file for each agent; the Claude Code plugin ships a UserPromptSubmit hook for always-on invocation. Includes a dependency-free Python runtime for real subagents on DeepSeek / MiMo / OpenRouter / local LLMs plus PromptFanout adapters and token/cost observability.",
"type": "module",
"main": "./index.mjs",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "simplicio-prompt"
version = "1.11.0"
version = "1.12.0"
description = "Tuple-Space + Yool safe-speed runtime kernel: lazy 1,000,000+ subagent batch_spawn, adaptive lane concurrency, and a dependency-free OpenAI-compatible provider client for real subagents on DeepSeek / MiMo / OpenRouter / local LLMs."
readme = "README.md"
requires-python = ">=3.8"
Expand Down
Loading
Loading