feat(quickjs): prompt tuning on task global (#4066)

This commit is contained in:
Hunter Lovell
2026-06-17 23:04:34 -07:00
committed by GitHub
parent b7eed01fac
commit a47696f6d3
11 changed files with 952 additions and 797 deletions
+144 -111
View File
@@ -24,9 +24,8 @@ _REPL_SYSTEM_PROMPT_TEMPLATE = (
"- Top-level `await` works; Promises resolve before the call returns.\n"
"- Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock "
"APIs (`fetch`, `require`, `fs`, `process`, real `Date.now()` are "
"unavailable or stubbed). External side effects from inside the REPL are "
"only reachable via the `tools.*` namespace when it is exposed (see "
"below); without it, the REPL is pure computation.\n"
"unavailable or stubbed).\n"
"{side_effects_line}\n"
"- Timeout: {timeout}s per call. Memory: {memory_limit_mb} MB total.\n"
"- `console.log` output is captured and returned alongside the result."
)
@@ -35,32 +34,40 @@ _SUBAGENT_SYSTEM_PROMPT_TEMPLATE = """
### Dispatching Subagents with `task`
`task` is your primitive for running configured subagents from inside the
JavaScript REPL. You orchestrate everything else - fan-out, filtering,
deduplication, multi-stage flow, and synthesis - in plain JavaScript.
JavaScript REPL. Your job here is to DISTRIBUTE work, not to do it yourself:
write JavaScript that fans work out to subagents and assembles their results.
You handle the orchestration - fan-out, filtering, deduplication, multi-stage
flow, and synthesis - in plain JavaScript.
#### The primitive
```javascript
await task({
description, // full autonomous task prompt
subagent_type, // configured subagent name
response_schema, // optional JSON Schema for structured output
subagentType, // configured subagent name
responseSchema, // optional JSON Schema for structured output
}); // -> Promise<unknown>
```
`task` runs a full agentic loop for the selected configured subagent. The
subagent can use whatever tools it was configured with, iterate, inspect
context, and return one final result. `subagent_type` is required; use one of
context, and return one final result. `subagentType` is required; use one of
the configured subagent names.
`description` is the only prompt the subagent receives for this dispatch. Make
it complete: include the goal, constraints, relevant context, what to inspect,
and the exact shape or level of detail you expect back. Each dispatch is
stateless from the caller's perspective; you cannot send follow-up messages to
the same subagent run.
it complete: the goal, the constraints, what to inspect, and the exact shape
or level of detail you expect back. Give context as locators — file paths and
symbol names — not as pasted file contents. If you already read a file while
exploring, still pass its path and let the subagent read it; do not paste back
what you read. Each dispatch is stateless from the caller's perspective; you
cannot send follow-up messages to the same subagent run.
`response_schema` is optional. When provided, the resolved value is already a
typed JavaScript value matching the schema. Do not call `JSON.parse` unless the
`responseSchema` is optional, but set it on any dispatch whose result feeds
later code. A deterministic, typed shape is what lets you compose the next
stage reliably — index it, sort it, compare fields, branch on it, merge it —
instead of parsing free-form text. This is what makes a whole workflow
composable as one script. When provided, the resolved value is already a typed
JavaScript value matching the schema; do not call `JSON.parse` unless the
subagent intentionally returned a JSON string. Dynamic schemas work for
declarative subagents; runnable-backed subagents reject dynamic schemas because
their runnable is already compiled.
@@ -81,26 +88,31 @@ Hold your work in JS: an array of items in, an array of results out. Merge each
dispatch result back onto its item. Multi-stage analysis means: run a pass,
filter or regroup the array in JS, then run another pass over the survivors.
Prefer one `{tool_name}` call that performs the whole workflow. Splitting the
workflow across multiple `{tool_name}` calls costs model turns and forces you to
re-establish state.
You can run the whole workflow in one `{tool_name}` call or split it across
several — both are fine. A single end-to-end script (generate, compare, pick a
winner; or review every item, then synthesize) is clean when you can write it
in one go; splitting is also fine when you want to inspect results between
stages. Either way, don't redo work across calls — reuse what is already in
scope (see "Reuse what earlier evals left in scope" below).
#### Fan out with bounded concurrency
Dispatch independent work in parallel with `Promise.all`, but in explicit
batches around 10 so you do not launch hundreds of subagents at once. The bridge
enforces a hard per-REPL cap of 32 concurrent `task` calls.
enforces a hard per-REPL cap of 32 concurrent subagent calls.
```javascript
const files = ["/src/a.ts", "/src/b.ts", "/src/c.ts"]; // found while exploring
const batchSize = 10;
const reviewed = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
reviewed.push(...(await Promise.all(batch.map(async (it) => {
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
reviewed.push(...(await Promise.all(batch.map(async (file) => {
const result = await task({
description: "Review " + it.file + " for SQL injection. Cite line numbers.",
subagent_type: "reviewer",
response_schema: {
description: "Read " + file + " and review it for SQL injection. " +
"Cite line numbers.",
subagentType: "reviewer",
responseSchema: {
type: "object",
properties: {
vulnerabilities: {
@@ -119,61 +131,44 @@ for (let i = 0; i < items.length; i += batchSize) {
required: ["vulnerabilities"],
},
});
return { ...it, ...result };
return { file, ...result };
}))));
}
```
#### Use parent JS for cheap work; use subagents for agentic work
#### Explore with your own tools first, then distribute
Use JavaScript in the parent REPL for deterministic orchestration: joining
arrays, deduping, sorting, filtering, grouping, batching, and merging results.
If the `tools.*` namespace is exposed, also use it to pre-read files or collect
shared data once, then pass only the relevant content to each subagent in
`description`.
You already have your normal tools for reading, listing, globbing, and
grepping files. Use them to explore and understand the task BEFORE you write
the orchestration script. These are ordinary tool calls, separate from the
`{tool_name}` tool: read the data file, list or glob the directory, grep for
what matters, then decide how to split the work.
Use `task` for work that benefits from an autonomous agentic loop: reading
or searching with the subagent's own tools, inspecting multiple files, following
leads, making judgment calls, or producing a final synthesized report.
Never write `{tool_name}` code that spawns a subagent just to read or parse a
file or list a directory. That is a deterministic step you do yourself with a
direct tool call; spending a whole agent loop on it is wasteful.
#### Pre-read shared context in the parent when useful
Once you understand the shape of the work, you have creative freedom in how
you split it:
If many subagents need the same source list or file content and `tools.*` is
available, gather that context once in the parent REPL before dispatching:
- One dispatch per file or per record, when the items are already separate.
- Chunk a large input yourself — read it, split it, optionally write a small
input file per chunk — and dispatch one subagent per chunk.
- A cheap classification pass first, then deeper dispatches only for the items
that warrant them.
```javascript
const files = (await tools.glob({ pattern: "src/**/*.ts" }))
.split("\\n")
.filter(Boolean);
Then write JavaScript in the `{tool_name}` tool that distributes the heavy,
agentic work to subagents with `task()`: analyzing file contents, exploring a
codebase, making judgment calls, rewriting code, or synthesizing a report.
const items = await Promise.all(files.map(async (file) => {
const content = await tools.readFile({ file_path: file });
return { file, content };
}));
const batchSize = 10;
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
results.push(...(await Promise.all(batch.map(async (it) => {
const finding = await task({
description:
"Review this file for auth bypasses. Return concrete findings only.\\n\\n" +
"File: " + it.file + "\\n\\n" +
it.content,
subagent_type: "reviewer",
response_schema: {
type: "object",
properties: {
findings: { type: "array", items: { type: "object" } },
},
required: ["findings"],
},
});
return { ...it, ...finding };
}))));
}
```
Hand each subagent a locator, not a payload. Subagents have their own file
tools, so for anything that lives in a file — a file to review, rewrite, or
audit — pass the path and let the subagent read it. Do NOT read a whole file
just to paste its contents into the description; that bloats every dispatch
and duplicates the file across them. Reserve inline content for small or
derived data that has no path of its own: a single parsed record, or a chunk
you split out of a larger input (write the chunk to its own file and pass that
path if it is large). Assemble the results in JS.
#### Compose multiple stages
@@ -182,57 +177,78 @@ cheap classification, filter to the risky items, then dispatch deeper reviews
only for those items.
```javascript
const tagged = [];
for (let i = 0; i < items.length; i += 10) {
const batch = items.slice(i, i + 10);
tagged.push(...(await Promise.all(batch.map(async (it) => {
const tag = await task({
description: "Classify " + it.file + " as handler, util, test, or config.",
subagent_type: "reviewer",
response_schema: {
type: "object",
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
required: ["kind", "risky"],
},
});
return { ...it, ...tag };
}))));
}
const tagged = await Promise.all(files.map((file) =>
task({
description: "Read " + file + " and classify it as handler, util, " +
"test, or config.",
subagentType: "reviewer",
responseSchema: {
type: "object",
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
required: ["kind", "risky"],
},
}).then((tag) => ({ file, ...tag }))
));
const riskyHandlers = tagged.filter((it) => it.kind === "handler" && it.risky);
const deepReviews = [];
for (let i = 0; i < riskyHandlers.length; i += 10) {
const batch = riskyHandlers.slice(i, i + 10);
deepReviews.push(...(await Promise.all(batch.map(async (it) => {
const review = await task({
description: "Deep security review of " + it.file + ". Cite line numbers.",
subagent_type: "reviewer",
});
return { ...it, review };
}))));
}
const deepReviews = await Promise.all(riskyHandlers.map((it) =>
task({
description: "Deep security review of " + it.file + ". Cite line numbers.",
subagentType: "reviewer",
}).then((review) => ({ ...it, review }))
));
```
#### Get results out without flooding your context
#### Return results via the last expression, not `console.log`
Keep large result sets in JS variables. Do not `console.log` the full result set.
If `tools.writeFile` is exposed, persist structured output from inside the eval:
The value of the last expression in an `{tool_name}` call (or a resolved
top-level `await`) is returned to you as the result. Make that final
expression the variable holding your result and read it from there.
`console.log` is only for incidental debugging: its output is capped and
truncated, while the returned value is not, so never `console.log` your
actual results.
Keep large intermediate sets in JS variables and return only a compact
summary or a small slice, not the entire dataset. To persist full output,
have a subagent write it, or write it with your own file tool outside the
`{tool_name}` call.
#### Reuse what earlier evals left in scope
The REPL is persistent within a turn: every top-level variable, function, and
class you declare is kept and is available in your next `{tool_name}` call
(each is hoisted to global scope). So if a later step needs something an
earlier eval produced or bound, **reference that variable by name** — do not
write a new literal that re-types data a previous eval already returned or
computed.
If you catch yourself pasting a big array or object of values you produced in
an earlier call, that is the tell: the variable is still in scope, so use it.
Re-typing prior results as a fresh literal wastes tokens and drifts from what
actually ran.
```javascript
await tools.writeFile({
file_path: "/results/subagent-output.json",
content: JSON.stringify(deepReviews),
});
// An earlier eval bound this:
// const auditResults = await Promise.all(files.map(/* ...audit... */));
// A later eval — reference it; do NOT paste the findings back in as a literal:
const findings = auditResults.flatMap((r) =>
r.findings.map((f) => ({ ...f, file: r.file }))
);
const verified = await Promise.all(findings.map((f) =>
task({ description: "Verify this finding: " + f.evidence, subagentType: "verifier" })
.then((v) => ({ ...f, ...v }))
));
```
Otherwise return a compact summary or a small slice of the results, not the
entire intermediate dataset.
#### When the user asks for a "workflow"
#### Across evals
Variables persist according to the interpreter persistence mode above, but
re-establish what you need in each eval. Doing the whole workflow in one
`{tool_name}` call is usually simplest.
If the user's request mentions running a "workflow" (or otherwise uses the
word "workflow"), fan the work out to subagents rather than doing it all
yourself. Explore with your own tools first as needed, then write JavaScript
in the `{tool_name}` tool that dispatches subagents with `task()` and
assembles their results. The point is to distribute the heavy work in
parallel, not to grind through it one tool call at a time.
"""
@@ -242,8 +258,24 @@ def render_repl_system_prompt(
timeout: float,
memory_limit_mb: int,
mode: Literal["thread", "turn", "call"],
ptc_attached: bool = False,
) -> str:
"""Render the base REPL system prompt text for ``CodeInterpreterMiddleware``."""
"""Render the base REPL system prompt text for ``CodeInterpreterMiddleware``.
``ptc_attached`` controls the "external side effects" bullet: when host
tools are exposed as the ``tools.*`` namespace it points the model at the
API reference; otherwise it states the REPL is pure computation.
"""
if ptc_attached:
side_effects_line = (
"- External side effects from inside the REPL are only reachable "
"via the `tools.*` namespace documented in the API reference below."
)
else:
side_effects_line = (
"- The REPL has no access to host tools, files, or the network: it "
"is pure computation. Return values to communicate results."
)
if mode == "call":
repl_intro_line = (
f"An `{tool_name}` tool is available. It runs JavaScript in a fresh "
@@ -274,6 +306,7 @@ def render_repl_system_prompt(
return _REPL_SYSTEM_PROMPT_TEMPLATE.format(
repl_intro_line=repl_intro_line,
state_persistence_line=state_persistence_line,
side_effects_line=side_effects_line,
timeout=timeout,
memory_limit_mb=memory_limit_mb,
)
@@ -514,14 +514,16 @@ class _ThreadREPL:
msg = "task() requires non-empty string field `description`"
raise ValueError(msg)
subagent_type = payload.get("subagent_type")
# JS callers use camelCase keys (`subagentType`, `responseSchema`) as
# documented in the system prompt.
subagent_type = payload.get("subagentType")
if not isinstance(subagent_type, str) or not subagent_type:
msg = "task() requires non-empty string field `subagent_type`"
msg = "task() requires non-empty string field `subagentType`"
raise ValueError(msg)
response_schema = payload.get("response_schema")
response_schema = payload.get("responseSchema")
if response_schema is not None and not isinstance(response_schema, dict):
msg = "task() field `response_schema` must be an object when provided"
msg = "task() field `responseSchema` must be an object when provided"
raise ValueError(msg)
async def _call() -> Any:
@@ -282,12 +282,8 @@ class CodeInterpreterMiddleware(AgentMiddleware[REPLState, ContextT, ResponseT])
max_ptc_calls=max_ptc_calls,
subagents_enabled=subagents,
)
self._base_system_prompt = render_repl_system_prompt(
tool_name=tool_name,
timeout=timeout,
memory_limit_mb=memory_limit // (1024 * 1024),
mode=self._mode,
)
self._memory_limit_mb = memory_limit // (1024 * 1024)
self._base_prompt_cache: dict[bool, str] = {}
self._ptc_prompt_cache: tuple[frozenset[str], str] | None = None
self._ptc_tools_by_thread: dict[str, tuple[BaseTool, ...]] = {}
# Stable fallback thread id — used when `thread_id` isn't in
@@ -452,6 +448,25 @@ class CodeInterpreterMiddleware(AgentMiddleware[REPLState, ContextT, ResponseT])
),
)
def _base_prompt(self, *, ptc_attached: bool) -> str:
"""Return the base REPL system prompt, rendered lazily and memoized.
The text depends only on construction-time config and ``ptc_attached``,
so it's computed on first use per boolean and cached. Avoids rendering
the ``tools.*`` variant at all when PTC is disabled.
"""
cached = self._base_prompt_cache.get(ptc_attached)
if cached is None:
cached = render_repl_system_prompt(
tool_name=self._tool_name,
timeout=self._timeout,
memory_limit_mb=self._memory_limit_mb,
mode=self._mode,
ptc_attached=ptc_attached,
)
self._base_prompt_cache[ptc_attached] = cached
return cached
def _prepare_for_call(self, request: ModelRequest[ContextT]) -> str:
"""Install PTC bindings for this turn and return the prompt addendum.
@@ -461,24 +476,20 @@ class CodeInterpreterMiddleware(AgentMiddleware[REPLState, ContextT, ResponseT])
and renders matching API-reference text.
"""
request_tools: list[BaseTool] = list(getattr(request, "tools", []) or [])
prompt = self._base_system_prompt
subagent_section = ""
if self._subagents and find_subagent_task_tool(request_tools) is not None:
prompt += render_subagent_system_prompt(tool_name=self._tool_name)
subagent_section = render_subagent_system_prompt(tool_name=self._tool_name)
if self._ptc is None:
return prompt
return self._base_prompt(ptc_attached=False) + subagent_section
exposed = filter_tools_for_ptc(
request_tools,
self._ptc,
self_tool_name=self._tool_name,
)
# Install on the current thread's REPL. If the thread hasn't
# evaluated anything yet, this creates the context lazily — which
# is fine: PTC bindings must be in place *before* the first eval
# that references them, and the next eval on this thread is the
# earliest that could matter.
prompt = self._base_prompt(ptc_attached=bool(exposed)) + subagent_section
thread_id = _resolve_thread_id(self._fallback_thread_id)
repl = self._registry.get(thread_id)
repl.install_tools(exposed)
@@ -124,39 +124,48 @@ An `eval` tool is available. It runs JavaScript in a persistent REPL.
- State (variables, functions) persists across tool calls and across multiple turns for this conversation thread.
- Top-level `await` works; Promises resolve before the call returns.
- Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (`fetch`, `require`, `fs`, `process`, real `Date.now()` are unavailable or stubbed). External side effects from inside the REPL are only reachable via the `tools.*` namespace when it is exposed (see below); without it, the REPL is pure computation.
- Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (`fetch`, `require`, `fs`, `process`, real `Date.now()` are unavailable or stubbed).
- External side effects from inside the REPL are only reachable via the `tools.*` namespace documented in the API reference below.
- Timeout: 5.0s per call. Memory: 64 MB total.
- `console.log` output is captured and returned alongside the result.
### Dispatching Subagents with `task`
`task` is your primitive for running configured subagents from inside the
JavaScript REPL. You orchestrate everything else - fan-out, filtering,
deduplication, multi-stage flow, and synthesis - in plain JavaScript.
JavaScript REPL. Your job here is to DISTRIBUTE work, not to do it yourself:
write JavaScript that fans work out to subagents and assembles their results.
You handle the orchestration - fan-out, filtering, deduplication, multi-stage
flow, and synthesis - in plain JavaScript.
#### The primitive
```javascript
await task({
description, // full autonomous task prompt
subagent_type, // configured subagent name
response_schema, // optional JSON Schema for structured output
subagentType, // configured subagent name
responseSchema, // optional JSON Schema for structured output
}); // -> Promise<unknown>
```
`task` runs a full agentic loop for the selected configured subagent. The
subagent can use whatever tools it was configured with, iterate, inspect
context, and return one final result. `subagent_type` is required; use one of
context, and return one final result. `subagentType` is required; use one of
the configured subagent names.
`description` is the only prompt the subagent receives for this dispatch. Make
it complete: include the goal, constraints, relevant context, what to inspect,
and the exact shape or level of detail you expect back. Each dispatch is
stateless from the caller's perspective; you cannot send follow-up messages to
the same subagent run.
it complete: the goal, the constraints, what to inspect, and the exact shape
or level of detail you expect back. Give context as locators — file paths and
symbol names — not as pasted file contents. If you already read a file while
exploring, still pass its path and let the subagent read it; do not paste back
what you read. Each dispatch is stateless from the caller's perspective; you
cannot send follow-up messages to the same subagent run.
`response_schema` is optional. When provided, the resolved value is already a
typed JavaScript value matching the schema. Do not call `JSON.parse` unless the
`responseSchema` is optional, but set it on any dispatch whose result feeds
later code. A deterministic, typed shape is what lets you compose the next
stage reliably — index it, sort it, compare fields, branch on it, merge it —
instead of parsing free-form text. This is what makes a whole workflow
composable as one script. When provided, the resolved value is already a typed
JavaScript value matching the schema; do not call `JSON.parse` unless the
subagent intentionally returned a JSON string. Dynamic schemas work for
declarative subagents; runnable-backed subagents reject dynamic schemas because
their runnable is already compiled.
@@ -177,26 +186,31 @@ Hold your work in JS: an array of items in, an array of results out. Merge each
dispatch result back onto its item. Multi-stage analysis means: run a pass,
filter or regroup the array in JS, then run another pass over the survivors.
Prefer one `eval` call that performs the whole workflow. Splitting the
workflow across multiple `eval` calls costs model turns and forces you to
re-establish state.
You can run the whole workflow in one `eval` call or split it across
several — both are fine. A single end-to-end script (generate, compare, pick a
winner; or review every item, then synthesize) is clean when you can write it
in one go; splitting is also fine when you want to inspect results between
stages. Either way, don't redo work across calls — reuse what is already in
scope (see "Reuse what earlier evals left in scope" below).
#### Fan out with bounded concurrency
Dispatch independent work in parallel with `Promise.all`, but in explicit
batches around 10 so you do not launch hundreds of subagents at once. The bridge
enforces a hard per-REPL cap of 32 concurrent `task` calls.
enforces a hard per-REPL cap of 32 concurrent subagent calls.
```javascript
const files = ["/src/a.ts", "/src/b.ts", "/src/c.ts"]; // found while exploring
const batchSize = 10;
const reviewed = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
reviewed.push(...(await Promise.all(batch.map(async (it) => {
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
reviewed.push(...(await Promise.all(batch.map(async (file) => {
const result = await task({
description: "Review " + it.file + " for SQL injection. Cite line numbers.",
subagent_type: "reviewer",
response_schema: {
description: "Read " + file + " and review it for SQL injection. " +
"Cite line numbers.",
subagentType: "reviewer",
responseSchema: {
type: "object",
properties: {
vulnerabilities: {
@@ -215,61 +229,44 @@ for (let i = 0; i < items.length; i += batchSize) {
required: ["vulnerabilities"],
},
});
return { ...it, ...result };
return { file, ...result };
}))));
}
```
#### Use parent JS for cheap work; use subagents for agentic work
#### Explore with your own tools first, then distribute
Use JavaScript in the parent REPL for deterministic orchestration: joining
arrays, deduping, sorting, filtering, grouping, batching, and merging results.
If the `tools.*` namespace is exposed, also use it to pre-read files or collect
shared data once, then pass only the relevant content to each subagent in
`description`.
You already have your normal tools for reading, listing, globbing, and
grepping files. Use them to explore and understand the task BEFORE you write
the orchestration script. These are ordinary tool calls, separate from the
`eval` tool: read the data file, list or glob the directory, grep for
what matters, then decide how to split the work.
Use `task` for work that benefits from an autonomous agentic loop: reading
or searching with the subagent's own tools, inspecting multiple files, following
leads, making judgment calls, or producing a final synthesized report.
Never write `eval` code that spawns a subagent just to read or parse a
file or list a directory. That is a deterministic step you do yourself with a
direct tool call; spending a whole agent loop on it is wasteful.
#### Pre-read shared context in the parent when useful
Once you understand the shape of the work, you have creative freedom in how
you split it:
If many subagents need the same source list or file content and `tools.*` is
available, gather that context once in the parent REPL before dispatching:
- One dispatch per file or per record, when the items are already separate.
- Chunk a large input yourself — read it, split it, optionally write a small
input file per chunk — and dispatch one subagent per chunk.
- A cheap classification pass first, then deeper dispatches only for the items
that warrant them.
```javascript
const files = (await tools.glob({ pattern: "src/**/*.ts" }))
.split("\n")
.filter(Boolean);
Then write JavaScript in the `eval` tool that distributes the heavy,
agentic work to subagents with `task()`: analyzing file contents, exploring a
codebase, making judgment calls, rewriting code, or synthesizing a report.
const items = await Promise.all(files.map(async (file) => {
const content = await tools.readFile({ file_path: file });
return { file, content };
}));
const batchSize = 10;
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
results.push(...(await Promise.all(batch.map(async (it) => {
const finding = await task({
description:
"Review this file for auth bypasses. Return concrete findings only.\n\n" +
"File: " + it.file + "\n\n" +
it.content,
subagent_type: "reviewer",
response_schema: {
type: "object",
properties: {
findings: { type: "array", items: { type: "object" } },
},
required: ["findings"],
},
});
return { ...it, ...finding };
}))));
}
```
Hand each subagent a locator, not a payload. Subagents have their own file
tools, so for anything that lives in a file — a file to review, rewrite, or
audit — pass the path and let the subagent read it. Do NOT read a whole file
just to paste its contents into the description; that bloats every dispatch
and duplicates the file across them. Reserve inline content for small or
derived data that has no path of its own: a single parsed record, or a chunk
you split out of a larger input (write the chunk to its own file and pass that
path if it is large). Assemble the results in JS.
#### Compose multiple stages
@@ -278,57 +275,78 @@ cheap classification, filter to the risky items, then dispatch deeper reviews
only for those items.
```javascript
const tagged = [];
for (let i = 0; i < items.length; i += 10) {
const batch = items.slice(i, i + 10);
tagged.push(...(await Promise.all(batch.map(async (it) => {
const tag = await task({
description: "Classify " + it.file + " as handler, util, test, or config.",
subagent_type: "reviewer",
response_schema: {
type: "object",
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
required: ["kind", "risky"],
},
});
return { ...it, ...tag };
}))));
}
const tagged = await Promise.all(files.map((file) =>
task({
description: "Read " + file + " and classify it as handler, util, " +
"test, or config.",
subagentType: "reviewer",
responseSchema: {
type: "object",
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
required: ["kind", "risky"],
},
}).then((tag) => ({ file, ...tag }))
));
const riskyHandlers = tagged.filter((it) => it.kind === "handler" && it.risky);
const deepReviews = [];
for (let i = 0; i < riskyHandlers.length; i += 10) {
const batch = riskyHandlers.slice(i, i + 10);
deepReviews.push(...(await Promise.all(batch.map(async (it) => {
const review = await task({
description: "Deep security review of " + it.file + ". Cite line numbers.",
subagent_type: "reviewer",
});
return { ...it, review };
}))));
}
const deepReviews = await Promise.all(riskyHandlers.map((it) =>
task({
description: "Deep security review of " + it.file + ". Cite line numbers.",
subagentType: "reviewer",
}).then((review) => ({ ...it, review }))
));
```
#### Get results out without flooding your context
#### Return results via the last expression, not `console.log`
Keep large result sets in JS variables. Do not `console.log` the full result set.
If `tools.writeFile` is exposed, persist structured output from inside the eval:
The value of the last expression in an `eval` call (or a resolved
top-level `await`) is returned to you as the result. Make that final
expression the variable holding your result and read it from there.
`console.log` is only for incidental debugging: its output is capped and
truncated, while the returned value is not, so never `console.log` your
actual results.
Keep large intermediate sets in JS variables and return only a compact
summary or a small slice, not the entire dataset. To persist full output,
have a subagent write it, or write it with your own file tool outside the
`eval` call.
#### Reuse what earlier evals left in scope
The REPL is persistent within a turn: every top-level variable, function, and
class you declare is kept and is available in your next `eval` call
(each is hoisted to global scope). So if a later step needs something an
earlier eval produced or bound, **reference that variable by name** — do not
write a new literal that re-types data a previous eval already returned or
computed.
If you catch yourself pasting a big array or object of values you produced in
an earlier call, that is the tell: the variable is still in scope, so use it.
Re-typing prior results as a fresh literal wastes tokens and drifts from what
actually ran.
```javascript
await tools.writeFile({
file_path: "/results/subagent-output.json",
content: JSON.stringify(deepReviews),
});
// An earlier eval bound this:
// const auditResults = await Promise.all(files.map(/* ...audit... */));
// A later eval — reference it; do NOT paste the findings back in as a literal:
const findings = auditResults.flatMap((r) =>
r.findings.map((f) => ({ ...f, file: r.file }))
);
const verified = await Promise.all(findings.map((f) =>
task({ description: "Verify this finding: " + f.evidence, subagentType: "verifier" })
.then((v) => ({ ...f, ...v }))
));
```
Otherwise return a compact summary or a small slice of the results, not the
entire intermediate dataset.
#### When the user asks for a "workflow"
#### Across evals
Variables persist according to the interpreter persistence mode above, but
re-establish what you need in each eval. Doing the whole workflow in one
`eval` call is usually simplest.
If the user's request mentions running a "workflow" (or otherwise uses the
word "workflow"), fan the work out to subagents rather than doing it all
yourself. Explore with your own tools first as needed, then write JavaScript
in the `eval` tool that dispatches subagents with `task()` and
assembles their results. The point is to distribute the heavy work in
parallel, not to grind through it one tool call at a time.
### API Reference — `tools` namespace
@@ -124,39 +124,48 @@ An `eval` tool is available. It runs JavaScript in a fresh sandboxed REPL for ea
- State (variables, functions) does not persist across tool calls. Each invocation starts from a blank environment.
- Top-level `await` works; Promises resolve before the call returns.
- Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (`fetch`, `require`, `fs`, `process`, real `Date.now()` are unavailable or stubbed). External side effects from inside the REPL are only reachable via the `tools.*` namespace when it is exposed (see below); without it, the REPL is pure computation.
- Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (`fetch`, `require`, `fs`, `process`, real `Date.now()` are unavailable or stubbed).
- External side effects from inside the REPL are only reachable via the `tools.*` namespace documented in the API reference below.
- Timeout: 5.0s per call. Memory: 64 MB total.
- `console.log` output is captured and returned alongside the result.
### Dispatching Subagents with `task`
`task` is your primitive for running configured subagents from inside the
JavaScript REPL. You orchestrate everything else - fan-out, filtering,
deduplication, multi-stage flow, and synthesis - in plain JavaScript.
JavaScript REPL. Your job here is to DISTRIBUTE work, not to do it yourself:
write JavaScript that fans work out to subagents and assembles their results.
You handle the orchestration - fan-out, filtering, deduplication, multi-stage
flow, and synthesis - in plain JavaScript.
#### The primitive
```javascript
await task({
description, // full autonomous task prompt
subagent_type, // configured subagent name
response_schema, // optional JSON Schema for structured output
subagentType, // configured subagent name
responseSchema, // optional JSON Schema for structured output
}); // -> Promise<unknown>
```
`task` runs a full agentic loop for the selected configured subagent. The
subagent can use whatever tools it was configured with, iterate, inspect
context, and return one final result. `subagent_type` is required; use one of
context, and return one final result. `subagentType` is required; use one of
the configured subagent names.
`description` is the only prompt the subagent receives for this dispatch. Make
it complete: include the goal, constraints, relevant context, what to inspect,
and the exact shape or level of detail you expect back. Each dispatch is
stateless from the caller's perspective; you cannot send follow-up messages to
the same subagent run.
it complete: the goal, the constraints, what to inspect, and the exact shape
or level of detail you expect back. Give context as locators — file paths and
symbol names — not as pasted file contents. If you already read a file while
exploring, still pass its path and let the subagent read it; do not paste back
what you read. Each dispatch is stateless from the caller's perspective; you
cannot send follow-up messages to the same subagent run.
`response_schema` is optional. When provided, the resolved value is already a
typed JavaScript value matching the schema. Do not call `JSON.parse` unless the
`responseSchema` is optional, but set it on any dispatch whose result feeds
later code. A deterministic, typed shape is what lets you compose the next
stage reliably — index it, sort it, compare fields, branch on it, merge it —
instead of parsing free-form text. This is what makes a whole workflow
composable as one script. When provided, the resolved value is already a typed
JavaScript value matching the schema; do not call `JSON.parse` unless the
subagent intentionally returned a JSON string. Dynamic schemas work for
declarative subagents; runnable-backed subagents reject dynamic schemas because
their runnable is already compiled.
@@ -177,26 +186,31 @@ Hold your work in JS: an array of items in, an array of results out. Merge each
dispatch result back onto its item. Multi-stage analysis means: run a pass,
filter or regroup the array in JS, then run another pass over the survivors.
Prefer one `eval` call that performs the whole workflow. Splitting the
workflow across multiple `eval` calls costs model turns and forces you to
re-establish state.
You can run the whole workflow in one `eval` call or split it across
several — both are fine. A single end-to-end script (generate, compare, pick a
winner; or review every item, then synthesize) is clean when you can write it
in one go; splitting is also fine when you want to inspect results between
stages. Either way, don't redo work across calls — reuse what is already in
scope (see "Reuse what earlier evals left in scope" below).
#### Fan out with bounded concurrency
Dispatch independent work in parallel with `Promise.all`, but in explicit
batches around 10 so you do not launch hundreds of subagents at once. The bridge
enforces a hard per-REPL cap of 32 concurrent `task` calls.
enforces a hard per-REPL cap of 32 concurrent subagent calls.
```javascript
const files = ["/src/a.ts", "/src/b.ts", "/src/c.ts"]; // found while exploring
const batchSize = 10;
const reviewed = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
reviewed.push(...(await Promise.all(batch.map(async (it) => {
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
reviewed.push(...(await Promise.all(batch.map(async (file) => {
const result = await task({
description: "Review " + it.file + " for SQL injection. Cite line numbers.",
subagent_type: "reviewer",
response_schema: {
description: "Read " + file + " and review it for SQL injection. " +
"Cite line numbers.",
subagentType: "reviewer",
responseSchema: {
type: "object",
properties: {
vulnerabilities: {
@@ -215,61 +229,44 @@ for (let i = 0; i < items.length; i += batchSize) {
required: ["vulnerabilities"],
},
});
return { ...it, ...result };
return { file, ...result };
}))));
}
```
#### Use parent JS for cheap work; use subagents for agentic work
#### Explore with your own tools first, then distribute
Use JavaScript in the parent REPL for deterministic orchestration: joining
arrays, deduping, sorting, filtering, grouping, batching, and merging results.
If the `tools.*` namespace is exposed, also use it to pre-read files or collect
shared data once, then pass only the relevant content to each subagent in
`description`.
You already have your normal tools for reading, listing, globbing, and
grepping files. Use them to explore and understand the task BEFORE you write
the orchestration script. These are ordinary tool calls, separate from the
`eval` tool: read the data file, list or glob the directory, grep for
what matters, then decide how to split the work.
Use `task` for work that benefits from an autonomous agentic loop: reading
or searching with the subagent's own tools, inspecting multiple files, following
leads, making judgment calls, or producing a final synthesized report.
Never write `eval` code that spawns a subagent just to read or parse a
file or list a directory. That is a deterministic step you do yourself with a
direct tool call; spending a whole agent loop on it is wasteful.
#### Pre-read shared context in the parent when useful
Once you understand the shape of the work, you have creative freedom in how
you split it:
If many subagents need the same source list or file content and `tools.*` is
available, gather that context once in the parent REPL before dispatching:
- One dispatch per file or per record, when the items are already separate.
- Chunk a large input yourself — read it, split it, optionally write a small
input file per chunk — and dispatch one subagent per chunk.
- A cheap classification pass first, then deeper dispatches only for the items
that warrant them.
```javascript
const files = (await tools.glob({ pattern: "src/**/*.ts" }))
.split("\n")
.filter(Boolean);
Then write JavaScript in the `eval` tool that distributes the heavy,
agentic work to subagents with `task()`: analyzing file contents, exploring a
codebase, making judgment calls, rewriting code, or synthesizing a report.
const items = await Promise.all(files.map(async (file) => {
const content = await tools.readFile({ file_path: file });
return { file, content };
}));
const batchSize = 10;
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
results.push(...(await Promise.all(batch.map(async (it) => {
const finding = await task({
description:
"Review this file for auth bypasses. Return concrete findings only.\n\n" +
"File: " + it.file + "\n\n" +
it.content,
subagent_type: "reviewer",
response_schema: {
type: "object",
properties: {
findings: { type: "array", items: { type: "object" } },
},
required: ["findings"],
},
});
return { ...it, ...finding };
}))));
}
```
Hand each subagent a locator, not a payload. Subagents have their own file
tools, so for anything that lives in a file — a file to review, rewrite, or
audit — pass the path and let the subagent read it. Do NOT read a whole file
just to paste its contents into the description; that bloats every dispatch
and duplicates the file across them. Reserve inline content for small or
derived data that has no path of its own: a single parsed record, or a chunk
you split out of a larger input (write the chunk to its own file and pass that
path if it is large). Assemble the results in JS.
#### Compose multiple stages
@@ -278,57 +275,78 @@ cheap classification, filter to the risky items, then dispatch deeper reviews
only for those items.
```javascript
const tagged = [];
for (let i = 0; i < items.length; i += 10) {
const batch = items.slice(i, i + 10);
tagged.push(...(await Promise.all(batch.map(async (it) => {
const tag = await task({
description: "Classify " + it.file + " as handler, util, test, or config.",
subagent_type: "reviewer",
response_schema: {
type: "object",
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
required: ["kind", "risky"],
},
});
return { ...it, ...tag };
}))));
}
const tagged = await Promise.all(files.map((file) =>
task({
description: "Read " + file + " and classify it as handler, util, " +
"test, or config.",
subagentType: "reviewer",
responseSchema: {
type: "object",
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
required: ["kind", "risky"],
},
}).then((tag) => ({ file, ...tag }))
));
const riskyHandlers = tagged.filter((it) => it.kind === "handler" && it.risky);
const deepReviews = [];
for (let i = 0; i < riskyHandlers.length; i += 10) {
const batch = riskyHandlers.slice(i, i + 10);
deepReviews.push(...(await Promise.all(batch.map(async (it) => {
const review = await task({
description: "Deep security review of " + it.file + ". Cite line numbers.",
subagent_type: "reviewer",
});
return { ...it, review };
}))));
}
const deepReviews = await Promise.all(riskyHandlers.map((it) =>
task({
description: "Deep security review of " + it.file + ". Cite line numbers.",
subagentType: "reviewer",
}).then((review) => ({ ...it, review }))
));
```
#### Get results out without flooding your context
#### Return results via the last expression, not `console.log`
Keep large result sets in JS variables. Do not `console.log` the full result set.
If `tools.writeFile` is exposed, persist structured output from inside the eval:
The value of the last expression in an `eval` call (or a resolved
top-level `await`) is returned to you as the result. Make that final
expression the variable holding your result and read it from there.
`console.log` is only for incidental debugging: its output is capped and
truncated, while the returned value is not, so never `console.log` your
actual results.
Keep large intermediate sets in JS variables and return only a compact
summary or a small slice, not the entire dataset. To persist full output,
have a subagent write it, or write it with your own file tool outside the
`eval` call.
#### Reuse what earlier evals left in scope
The REPL is persistent within a turn: every top-level variable, function, and
class you declare is kept and is available in your next `eval` call
(each is hoisted to global scope). So if a later step needs something an
earlier eval produced or bound, **reference that variable by name** — do not
write a new literal that re-types data a previous eval already returned or
computed.
If you catch yourself pasting a big array or object of values you produced in
an earlier call, that is the tell: the variable is still in scope, so use it.
Re-typing prior results as a fresh literal wastes tokens and drifts from what
actually ran.
```javascript
await tools.writeFile({
file_path: "/results/subagent-output.json",
content: JSON.stringify(deepReviews),
});
// An earlier eval bound this:
// const auditResults = await Promise.all(files.map(/* ...audit... */));
// A later eval — reference it; do NOT paste the findings back in as a literal:
const findings = auditResults.flatMap((r) =>
r.findings.map((f) => ({ ...f, file: r.file }))
);
const verified = await Promise.all(findings.map((f) =>
task({ description: "Verify this finding: " + f.evidence, subagentType: "verifier" })
.then((v) => ({ ...f, ...v }))
));
```
Otherwise return a compact summary or a small slice of the results, not the
entire intermediate dataset.
#### When the user asks for a "workflow"
#### Across evals
Variables persist according to the interpreter persistence mode above, but
re-establish what you need in each eval. Doing the whole workflow in one
`eval` call is usually simplest.
If the user's request mentions running a "workflow" (or otherwise uses the
word "workflow"), fan the work out to subagents rather than doing it all
yourself. Explore with your own tools first as needed, then write JavaScript
in the `eval` tool that dispatches subagents with `task()` and
assembles their results. The point is to distribute the heavy work in
parallel, not to grind through it one tool call at a time.
### API Reference — `tools` namespace
@@ -124,39 +124,48 @@ An `eval` tool is available. It runs JavaScript in a persistent REPL.
- State (variables, functions) persists across tool calls within a single turn of conversation. They DO NOT persist across multiple turns.
- Top-level `await` works; Promises resolve before the call returns.
- Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (`fetch`, `require`, `fs`, `process`, real `Date.now()` are unavailable or stubbed). External side effects from inside the REPL are only reachable via the `tools.*` namespace when it is exposed (see below); without it, the REPL is pure computation.
- Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (`fetch`, `require`, `fs`, `process`, real `Date.now()` are unavailable or stubbed).
- External side effects from inside the REPL are only reachable via the `tools.*` namespace documented in the API reference below.
- Timeout: 5.0s per call. Memory: 64 MB total.
- `console.log` output is captured and returned alongside the result.
### Dispatching Subagents with `task`
`task` is your primitive for running configured subagents from inside the
JavaScript REPL. You orchestrate everything else - fan-out, filtering,
deduplication, multi-stage flow, and synthesis - in plain JavaScript.
JavaScript REPL. Your job here is to DISTRIBUTE work, not to do it yourself:
write JavaScript that fans work out to subagents and assembles their results.
You handle the orchestration - fan-out, filtering, deduplication, multi-stage
flow, and synthesis - in plain JavaScript.
#### The primitive
```javascript
await task({
description, // full autonomous task prompt
subagent_type, // configured subagent name
response_schema, // optional JSON Schema for structured output
subagentType, // configured subagent name
responseSchema, // optional JSON Schema for structured output
}); // -> Promise<unknown>
```
`task` runs a full agentic loop for the selected configured subagent. The
subagent can use whatever tools it was configured with, iterate, inspect
context, and return one final result. `subagent_type` is required; use one of
context, and return one final result. `subagentType` is required; use one of
the configured subagent names.
`description` is the only prompt the subagent receives for this dispatch. Make
it complete: include the goal, constraints, relevant context, what to inspect,
and the exact shape or level of detail you expect back. Each dispatch is
stateless from the caller's perspective; you cannot send follow-up messages to
the same subagent run.
it complete: the goal, the constraints, what to inspect, and the exact shape
or level of detail you expect back. Give context as locators — file paths and
symbol names — not as pasted file contents. If you already read a file while
exploring, still pass its path and let the subagent read it; do not paste back
what you read. Each dispatch is stateless from the caller's perspective; you
cannot send follow-up messages to the same subagent run.
`response_schema` is optional. When provided, the resolved value is already a
typed JavaScript value matching the schema. Do not call `JSON.parse` unless the
`responseSchema` is optional, but set it on any dispatch whose result feeds
later code. A deterministic, typed shape is what lets you compose the next
stage reliably — index it, sort it, compare fields, branch on it, merge it —
instead of parsing free-form text. This is what makes a whole workflow
composable as one script. When provided, the resolved value is already a typed
JavaScript value matching the schema; do not call `JSON.parse` unless the
subagent intentionally returned a JSON string. Dynamic schemas work for
declarative subagents; runnable-backed subagents reject dynamic schemas because
their runnable is already compiled.
@@ -177,26 +186,31 @@ Hold your work in JS: an array of items in, an array of results out. Merge each
dispatch result back onto its item. Multi-stage analysis means: run a pass,
filter or regroup the array in JS, then run another pass over the survivors.
Prefer one `eval` call that performs the whole workflow. Splitting the
workflow across multiple `eval` calls costs model turns and forces you to
re-establish state.
You can run the whole workflow in one `eval` call or split it across
several — both are fine. A single end-to-end script (generate, compare, pick a
winner; or review every item, then synthesize) is clean when you can write it
in one go; splitting is also fine when you want to inspect results between
stages. Either way, don't redo work across calls — reuse what is already in
scope (see "Reuse what earlier evals left in scope" below).
#### Fan out with bounded concurrency
Dispatch independent work in parallel with `Promise.all`, but in explicit
batches around 10 so you do not launch hundreds of subagents at once. The bridge
enforces a hard per-REPL cap of 32 concurrent `task` calls.
enforces a hard per-REPL cap of 32 concurrent subagent calls.
```javascript
const files = ["/src/a.ts", "/src/b.ts", "/src/c.ts"]; // found while exploring
const batchSize = 10;
const reviewed = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
reviewed.push(...(await Promise.all(batch.map(async (it) => {
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
reviewed.push(...(await Promise.all(batch.map(async (file) => {
const result = await task({
description: "Review " + it.file + " for SQL injection. Cite line numbers.",
subagent_type: "reviewer",
response_schema: {
description: "Read " + file + " and review it for SQL injection. " +
"Cite line numbers.",
subagentType: "reviewer",
responseSchema: {
type: "object",
properties: {
vulnerabilities: {
@@ -215,61 +229,44 @@ for (let i = 0; i < items.length; i += batchSize) {
required: ["vulnerabilities"],
},
});
return { ...it, ...result };
return { file, ...result };
}))));
}
```
#### Use parent JS for cheap work; use subagents for agentic work
#### Explore with your own tools first, then distribute
Use JavaScript in the parent REPL for deterministic orchestration: joining
arrays, deduping, sorting, filtering, grouping, batching, and merging results.
If the `tools.*` namespace is exposed, also use it to pre-read files or collect
shared data once, then pass only the relevant content to each subagent in
`description`.
You already have your normal tools for reading, listing, globbing, and
grepping files. Use them to explore and understand the task BEFORE you write
the orchestration script. These are ordinary tool calls, separate from the
`eval` tool: read the data file, list or glob the directory, grep for
what matters, then decide how to split the work.
Use `task` for work that benefits from an autonomous agentic loop: reading
or searching with the subagent's own tools, inspecting multiple files, following
leads, making judgment calls, or producing a final synthesized report.
Never write `eval` code that spawns a subagent just to read or parse a
file or list a directory. That is a deterministic step you do yourself with a
direct tool call; spending a whole agent loop on it is wasteful.
#### Pre-read shared context in the parent when useful
Once you understand the shape of the work, you have creative freedom in how
you split it:
If many subagents need the same source list or file content and `tools.*` is
available, gather that context once in the parent REPL before dispatching:
- One dispatch per file or per record, when the items are already separate.
- Chunk a large input yourself — read it, split it, optionally write a small
input file per chunk — and dispatch one subagent per chunk.
- A cheap classification pass first, then deeper dispatches only for the items
that warrant them.
```javascript
const files = (await tools.glob({ pattern: "src/**/*.ts" }))
.split("\n")
.filter(Boolean);
Then write JavaScript in the `eval` tool that distributes the heavy,
agentic work to subagents with `task()`: analyzing file contents, exploring a
codebase, making judgment calls, rewriting code, or synthesizing a report.
const items = await Promise.all(files.map(async (file) => {
const content = await tools.readFile({ file_path: file });
return { file, content };
}));
const batchSize = 10;
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
results.push(...(await Promise.all(batch.map(async (it) => {
const finding = await task({
description:
"Review this file for auth bypasses. Return concrete findings only.\n\n" +
"File: " + it.file + "\n\n" +
it.content,
subagent_type: "reviewer",
response_schema: {
type: "object",
properties: {
findings: { type: "array", items: { type: "object" } },
},
required: ["findings"],
},
});
return { ...it, ...finding };
}))));
}
```
Hand each subagent a locator, not a payload. Subagents have their own file
tools, so for anything that lives in a file — a file to review, rewrite, or
audit — pass the path and let the subagent read it. Do NOT read a whole file
just to paste its contents into the description; that bloats every dispatch
and duplicates the file across them. Reserve inline content for small or
derived data that has no path of its own: a single parsed record, or a chunk
you split out of a larger input (write the chunk to its own file and pass that
path if it is large). Assemble the results in JS.
#### Compose multiple stages
@@ -278,57 +275,78 @@ cheap classification, filter to the risky items, then dispatch deeper reviews
only for those items.
```javascript
const tagged = [];
for (let i = 0; i < items.length; i += 10) {
const batch = items.slice(i, i + 10);
tagged.push(...(await Promise.all(batch.map(async (it) => {
const tag = await task({
description: "Classify " + it.file + " as handler, util, test, or config.",
subagent_type: "reviewer",
response_schema: {
type: "object",
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
required: ["kind", "risky"],
},
});
return { ...it, ...tag };
}))));
}
const tagged = await Promise.all(files.map((file) =>
task({
description: "Read " + file + " and classify it as handler, util, " +
"test, or config.",
subagentType: "reviewer",
responseSchema: {
type: "object",
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
required: ["kind", "risky"],
},
}).then((tag) => ({ file, ...tag }))
));
const riskyHandlers = tagged.filter((it) => it.kind === "handler" && it.risky);
const deepReviews = [];
for (let i = 0; i < riskyHandlers.length; i += 10) {
const batch = riskyHandlers.slice(i, i + 10);
deepReviews.push(...(await Promise.all(batch.map(async (it) => {
const review = await task({
description: "Deep security review of " + it.file + ". Cite line numbers.",
subagent_type: "reviewer",
});
return { ...it, review };
}))));
}
const deepReviews = await Promise.all(riskyHandlers.map((it) =>
task({
description: "Deep security review of " + it.file + ". Cite line numbers.",
subagentType: "reviewer",
}).then((review) => ({ ...it, review }))
));
```
#### Get results out without flooding your context
#### Return results via the last expression, not `console.log`
Keep large result sets in JS variables. Do not `console.log` the full result set.
If `tools.writeFile` is exposed, persist structured output from inside the eval:
The value of the last expression in an `eval` call (or a resolved
top-level `await`) is returned to you as the result. Make that final
expression the variable holding your result and read it from there.
`console.log` is only for incidental debugging: its output is capped and
truncated, while the returned value is not, so never `console.log` your
actual results.
Keep large intermediate sets in JS variables and return only a compact
summary or a small slice, not the entire dataset. To persist full output,
have a subagent write it, or write it with your own file tool outside the
`eval` call.
#### Reuse what earlier evals left in scope
The REPL is persistent within a turn: every top-level variable, function, and
class you declare is kept and is available in your next `eval` call
(each is hoisted to global scope). So if a later step needs something an
earlier eval produced or bound, **reference that variable by name** — do not
write a new literal that re-types data a previous eval already returned or
computed.
If you catch yourself pasting a big array or object of values you produced in
an earlier call, that is the tell: the variable is still in scope, so use it.
Re-typing prior results as a fresh literal wastes tokens and drifts from what
actually ran.
```javascript
await tools.writeFile({
file_path: "/results/subagent-output.json",
content: JSON.stringify(deepReviews),
});
// An earlier eval bound this:
// const auditResults = await Promise.all(files.map(/* ...audit... */));
// A later eval — reference it; do NOT paste the findings back in as a literal:
const findings = auditResults.flatMap((r) =>
r.findings.map((f) => ({ ...f, file: r.file }))
);
const verified = await Promise.all(findings.map((f) =>
task({ description: "Verify this finding: " + f.evidence, subagentType: "verifier" })
.then((v) => ({ ...f, ...v }))
));
```
Otherwise return a compact summary or a small slice of the results, not the
entire intermediate dataset.
#### When the user asks for a "workflow"
#### Across evals
Variables persist according to the interpreter persistence mode above, but
re-establish what you need in each eval. Doing the whole workflow in one
`eval` call is usually simplest.
If the user's request mentions running a "workflow" (or otherwise uses the
word "workflow"), fan the work out to subagents rather than doing it all
yourself. Explore with your own tools first as needed, then write JavaScript
in the `eval` tool that dispatches subagents with `task()` and
assembles their results. The point is to distribute the heavy work in
parallel, not to grind through it one tool call at a time.
### API Reference — `tools` namespace
@@ -124,39 +124,48 @@ An `eval` tool is available. It runs JavaScript in a persistent REPL.
- State (variables, functions) persists across tool calls and across multiple turns for this conversation thread.
- Top-level `await` works; Promises resolve before the call returns.
- Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (`fetch`, `require`, `fs`, `process`, real `Date.now()` are unavailable or stubbed). External side effects from inside the REPL are only reachable via the `tools.*` namespace when it is exposed (see below); without it, the REPL is pure computation.
- Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (`fetch`, `require`, `fs`, `process`, real `Date.now()` are unavailable or stubbed).
- The REPL has no access to host tools, files, or the network: it is pure computation. Return values to communicate results.
- Timeout: 5.0s per call. Memory: 64 MB total.
- `console.log` output is captured and returned alongside the result.
### Dispatching Subagents with `task`
`task` is your primitive for running configured subagents from inside the
JavaScript REPL. You orchestrate everything else - fan-out, filtering,
deduplication, multi-stage flow, and synthesis - in plain JavaScript.
JavaScript REPL. Your job here is to DISTRIBUTE work, not to do it yourself:
write JavaScript that fans work out to subagents and assembles their results.
You handle the orchestration - fan-out, filtering, deduplication, multi-stage
flow, and synthesis - in plain JavaScript.
#### The primitive
```javascript
await task({
description, // full autonomous task prompt
subagent_type, // configured subagent name
response_schema, // optional JSON Schema for structured output
subagentType, // configured subagent name
responseSchema, // optional JSON Schema for structured output
}); // -> Promise<unknown>
```
`task` runs a full agentic loop for the selected configured subagent. The
subagent can use whatever tools it was configured with, iterate, inspect
context, and return one final result. `subagent_type` is required; use one of
context, and return one final result. `subagentType` is required; use one of
the configured subagent names.
`description` is the only prompt the subagent receives for this dispatch. Make
it complete: include the goal, constraints, relevant context, what to inspect,
and the exact shape or level of detail you expect back. Each dispatch is
stateless from the caller's perspective; you cannot send follow-up messages to
the same subagent run.
it complete: the goal, the constraints, what to inspect, and the exact shape
or level of detail you expect back. Give context as locators — file paths and
symbol names — not as pasted file contents. If you already read a file while
exploring, still pass its path and let the subagent read it; do not paste back
what you read. Each dispatch is stateless from the caller's perspective; you
cannot send follow-up messages to the same subagent run.
`response_schema` is optional. When provided, the resolved value is already a
typed JavaScript value matching the schema. Do not call `JSON.parse` unless the
`responseSchema` is optional, but set it on any dispatch whose result feeds
later code. A deterministic, typed shape is what lets you compose the next
stage reliably — index it, sort it, compare fields, branch on it, merge it —
instead of parsing free-form text. This is what makes a whole workflow
composable as one script. When provided, the resolved value is already a typed
JavaScript value matching the schema; do not call `JSON.parse` unless the
subagent intentionally returned a JSON string. Dynamic schemas work for
declarative subagents; runnable-backed subagents reject dynamic schemas because
their runnable is already compiled.
@@ -177,26 +186,31 @@ Hold your work in JS: an array of items in, an array of results out. Merge each
dispatch result back onto its item. Multi-stage analysis means: run a pass,
filter or regroup the array in JS, then run another pass over the survivors.
Prefer one `eval` call that performs the whole workflow. Splitting the
workflow across multiple `eval` calls costs model turns and forces you to
re-establish state.
You can run the whole workflow in one `eval` call or split it across
several — both are fine. A single end-to-end script (generate, compare, pick a
winner; or review every item, then synthesize) is clean when you can write it
in one go; splitting is also fine when you want to inspect results between
stages. Either way, don't redo work across calls — reuse what is already in
scope (see "Reuse what earlier evals left in scope" below).
#### Fan out with bounded concurrency
Dispatch independent work in parallel with `Promise.all`, but in explicit
batches around 10 so you do not launch hundreds of subagents at once. The bridge
enforces a hard per-REPL cap of 32 concurrent `task` calls.
enforces a hard per-REPL cap of 32 concurrent subagent calls.
```javascript
const files = ["/src/a.ts", "/src/b.ts", "/src/c.ts"]; // found while exploring
const batchSize = 10;
const reviewed = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
reviewed.push(...(await Promise.all(batch.map(async (it) => {
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
reviewed.push(...(await Promise.all(batch.map(async (file) => {
const result = await task({
description: "Review " + it.file + " for SQL injection. Cite line numbers.",
subagent_type: "reviewer",
response_schema: {
description: "Read " + file + " and review it for SQL injection. " +
"Cite line numbers.",
subagentType: "reviewer",
responseSchema: {
type: "object",
properties: {
vulnerabilities: {
@@ -215,61 +229,44 @@ for (let i = 0; i < items.length; i += batchSize) {
required: ["vulnerabilities"],
},
});
return { ...it, ...result };
return { file, ...result };
}))));
}
```
#### Use parent JS for cheap work; use subagents for agentic work
#### Explore with your own tools first, then distribute
Use JavaScript in the parent REPL for deterministic orchestration: joining
arrays, deduping, sorting, filtering, grouping, batching, and merging results.
If the `tools.*` namespace is exposed, also use it to pre-read files or collect
shared data once, then pass only the relevant content to each subagent in
`description`.
You already have your normal tools for reading, listing, globbing, and
grepping files. Use them to explore and understand the task BEFORE you write
the orchestration script. These are ordinary tool calls, separate from the
`eval` tool: read the data file, list or glob the directory, grep for
what matters, then decide how to split the work.
Use `task` for work that benefits from an autonomous agentic loop: reading
or searching with the subagent's own tools, inspecting multiple files, following
leads, making judgment calls, or producing a final synthesized report.
Never write `eval` code that spawns a subagent just to read or parse a
file or list a directory. That is a deterministic step you do yourself with a
direct tool call; spending a whole agent loop on it is wasteful.
#### Pre-read shared context in the parent when useful
Once you understand the shape of the work, you have creative freedom in how
you split it:
If many subagents need the same source list or file content and `tools.*` is
available, gather that context once in the parent REPL before dispatching:
- One dispatch per file or per record, when the items are already separate.
- Chunk a large input yourself — read it, split it, optionally write a small
input file per chunk — and dispatch one subagent per chunk.
- A cheap classification pass first, then deeper dispatches only for the items
that warrant them.
```javascript
const files = (await tools.glob({ pattern: "src/**/*.ts" }))
.split("\n")
.filter(Boolean);
Then write JavaScript in the `eval` tool that distributes the heavy,
agentic work to subagents with `task()`: analyzing file contents, exploring a
codebase, making judgment calls, rewriting code, or synthesizing a report.
const items = await Promise.all(files.map(async (file) => {
const content = await tools.readFile({ file_path: file });
return { file, content };
}));
const batchSize = 10;
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
results.push(...(await Promise.all(batch.map(async (it) => {
const finding = await task({
description:
"Review this file for auth bypasses. Return concrete findings only.\n\n" +
"File: " + it.file + "\n\n" +
it.content,
subagent_type: "reviewer",
response_schema: {
type: "object",
properties: {
findings: { type: "array", items: { type: "object" } },
},
required: ["findings"],
},
});
return { ...it, ...finding };
}))));
}
```
Hand each subagent a locator, not a payload. Subagents have their own file
tools, so for anything that lives in a file — a file to review, rewrite, or
audit — pass the path and let the subagent read it. Do NOT read a whole file
just to paste its contents into the description; that bloats every dispatch
and duplicates the file across them. Reserve inline content for small or
derived data that has no path of its own: a single parsed record, or a chunk
you split out of a larger input (write the chunk to its own file and pass that
path if it is large). Assemble the results in JS.
#### Compose multiple stages
@@ -278,54 +275,75 @@ cheap classification, filter to the risky items, then dispatch deeper reviews
only for those items.
```javascript
const tagged = [];
for (let i = 0; i < items.length; i += 10) {
const batch = items.slice(i, i + 10);
tagged.push(...(await Promise.all(batch.map(async (it) => {
const tag = await task({
description: "Classify " + it.file + " as handler, util, test, or config.",
subagent_type: "reviewer",
response_schema: {
type: "object",
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
required: ["kind", "risky"],
},
});
return { ...it, ...tag };
}))));
}
const tagged = await Promise.all(files.map((file) =>
task({
description: "Read " + file + " and classify it as handler, util, " +
"test, or config.",
subagentType: "reviewer",
responseSchema: {
type: "object",
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
required: ["kind", "risky"],
},
}).then((tag) => ({ file, ...tag }))
));
const riskyHandlers = tagged.filter((it) => it.kind === "handler" && it.risky);
const deepReviews = [];
for (let i = 0; i < riskyHandlers.length; i += 10) {
const batch = riskyHandlers.slice(i, i + 10);
deepReviews.push(...(await Promise.all(batch.map(async (it) => {
const review = await task({
description: "Deep security review of " + it.file + ". Cite line numbers.",
subagent_type: "reviewer",
});
return { ...it, review };
}))));
}
const deepReviews = await Promise.all(riskyHandlers.map((it) =>
task({
description: "Deep security review of " + it.file + ". Cite line numbers.",
subagentType: "reviewer",
}).then((review) => ({ ...it, review }))
));
```
#### Get results out without flooding your context
#### Return results via the last expression, not `console.log`
Keep large result sets in JS variables. Do not `console.log` the full result set.
If `tools.writeFile` is exposed, persist structured output from inside the eval:
The value of the last expression in an `eval` call (or a resolved
top-level `await`) is returned to you as the result. Make that final
expression the variable holding your result and read it from there.
`console.log` is only for incidental debugging: its output is capped and
truncated, while the returned value is not, so never `console.log` your
actual results.
Keep large intermediate sets in JS variables and return only a compact
summary or a small slice, not the entire dataset. To persist full output,
have a subagent write it, or write it with your own file tool outside the
`eval` call.
#### Reuse what earlier evals left in scope
The REPL is persistent within a turn: every top-level variable, function, and
class you declare is kept and is available in your next `eval` call
(each is hoisted to global scope). So if a later step needs something an
earlier eval produced or bound, **reference that variable by name** — do not
write a new literal that re-types data a previous eval already returned or
computed.
If you catch yourself pasting a big array or object of values you produced in
an earlier call, that is the tell: the variable is still in scope, so use it.
Re-typing prior results as a fresh literal wastes tokens and drifts from what
actually ran.
```javascript
await tools.writeFile({
file_path: "/results/subagent-output.json",
content: JSON.stringify(deepReviews),
});
// An earlier eval bound this:
// const auditResults = await Promise.all(files.map(/* ...audit... */));
// A later eval — reference it; do NOT paste the findings back in as a literal:
const findings = auditResults.flatMap((r) =>
r.findings.map((f) => ({ ...f, file: r.file }))
);
const verified = await Promise.all(findings.map((f) =>
task({ description: "Verify this finding: " + f.evidence, subagentType: "verifier" })
.then((v) => ({ ...f, ...v }))
));
```
Otherwise return a compact summary or a small slice of the results, not the
entire intermediate dataset.
#### When the user asks for a "workflow"
#### Across evals
Variables persist according to the interpreter persistence mode above, but
re-establish what you need in each eval. Doing the whole workflow in one
`eval` call is usually simplest.
If the user's request mentions running a "workflow" (or otherwise uses the
word "workflow"), fan the work out to subagents rather than doing it all
yourself. Explore with your own tools first as needed, then write JavaScript
in the `eval` tool that dispatches subagents with `task()` and
assembles their results. The point is to distribute the heavy work in
parallel, not to grind through it one tool call at a time.
@@ -124,39 +124,48 @@ An `eval` tool is available. It runs JavaScript in a fresh sandboxed REPL for ea
- State (variables, functions) does not persist across tool calls. Each invocation starts from a blank environment.
- Top-level `await` works; Promises resolve before the call returns.
- Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (`fetch`, `require`, `fs`, `process`, real `Date.now()` are unavailable or stubbed). External side effects from inside the REPL are only reachable via the `tools.*` namespace when it is exposed (see below); without it, the REPL is pure computation.
- Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (`fetch`, `require`, `fs`, `process`, real `Date.now()` are unavailable or stubbed).
- The REPL has no access to host tools, files, or the network: it is pure computation. Return values to communicate results.
- Timeout: 5.0s per call. Memory: 64 MB total.
- `console.log` output is captured and returned alongside the result.
### Dispatching Subagents with `task`
`task` is your primitive for running configured subagents from inside the
JavaScript REPL. You orchestrate everything else - fan-out, filtering,
deduplication, multi-stage flow, and synthesis - in plain JavaScript.
JavaScript REPL. Your job here is to DISTRIBUTE work, not to do it yourself:
write JavaScript that fans work out to subagents and assembles their results.
You handle the orchestration - fan-out, filtering, deduplication, multi-stage
flow, and synthesis - in plain JavaScript.
#### The primitive
```javascript
await task({
description, // full autonomous task prompt
subagent_type, // configured subagent name
response_schema, // optional JSON Schema for structured output
subagentType, // configured subagent name
responseSchema, // optional JSON Schema for structured output
}); // -> Promise<unknown>
```
`task` runs a full agentic loop for the selected configured subagent. The
subagent can use whatever tools it was configured with, iterate, inspect
context, and return one final result. `subagent_type` is required; use one of
context, and return one final result. `subagentType` is required; use one of
the configured subagent names.
`description` is the only prompt the subagent receives for this dispatch. Make
it complete: include the goal, constraints, relevant context, what to inspect,
and the exact shape or level of detail you expect back. Each dispatch is
stateless from the caller's perspective; you cannot send follow-up messages to
the same subagent run.
it complete: the goal, the constraints, what to inspect, and the exact shape
or level of detail you expect back. Give context as locators — file paths and
symbol names — not as pasted file contents. If you already read a file while
exploring, still pass its path and let the subagent read it; do not paste back
what you read. Each dispatch is stateless from the caller's perspective; you
cannot send follow-up messages to the same subagent run.
`response_schema` is optional. When provided, the resolved value is already a
typed JavaScript value matching the schema. Do not call `JSON.parse` unless the
`responseSchema` is optional, but set it on any dispatch whose result feeds
later code. A deterministic, typed shape is what lets you compose the next
stage reliably — index it, sort it, compare fields, branch on it, merge it —
instead of parsing free-form text. This is what makes a whole workflow
composable as one script. When provided, the resolved value is already a typed
JavaScript value matching the schema; do not call `JSON.parse` unless the
subagent intentionally returned a JSON string. Dynamic schemas work for
declarative subagents; runnable-backed subagents reject dynamic schemas because
their runnable is already compiled.
@@ -177,26 +186,31 @@ Hold your work in JS: an array of items in, an array of results out. Merge each
dispatch result back onto its item. Multi-stage analysis means: run a pass,
filter or regroup the array in JS, then run another pass over the survivors.
Prefer one `eval` call that performs the whole workflow. Splitting the
workflow across multiple `eval` calls costs model turns and forces you to
re-establish state.
You can run the whole workflow in one `eval` call or split it across
several — both are fine. A single end-to-end script (generate, compare, pick a
winner; or review every item, then synthesize) is clean when you can write it
in one go; splitting is also fine when you want to inspect results between
stages. Either way, don't redo work across calls — reuse what is already in
scope (see "Reuse what earlier evals left in scope" below).
#### Fan out with bounded concurrency
Dispatch independent work in parallel with `Promise.all`, but in explicit
batches around 10 so you do not launch hundreds of subagents at once. The bridge
enforces a hard per-REPL cap of 32 concurrent `task` calls.
enforces a hard per-REPL cap of 32 concurrent subagent calls.
```javascript
const files = ["/src/a.ts", "/src/b.ts", "/src/c.ts"]; // found while exploring
const batchSize = 10;
const reviewed = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
reviewed.push(...(await Promise.all(batch.map(async (it) => {
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
reviewed.push(...(await Promise.all(batch.map(async (file) => {
const result = await task({
description: "Review " + it.file + " for SQL injection. Cite line numbers.",
subagent_type: "reviewer",
response_schema: {
description: "Read " + file + " and review it for SQL injection. " +
"Cite line numbers.",
subagentType: "reviewer",
responseSchema: {
type: "object",
properties: {
vulnerabilities: {
@@ -215,61 +229,44 @@ for (let i = 0; i < items.length; i += batchSize) {
required: ["vulnerabilities"],
},
});
return { ...it, ...result };
return { file, ...result };
}))));
}
```
#### Use parent JS for cheap work; use subagents for agentic work
#### Explore with your own tools first, then distribute
Use JavaScript in the parent REPL for deterministic orchestration: joining
arrays, deduping, sorting, filtering, grouping, batching, and merging results.
If the `tools.*` namespace is exposed, also use it to pre-read files or collect
shared data once, then pass only the relevant content to each subagent in
`description`.
You already have your normal tools for reading, listing, globbing, and
grepping files. Use them to explore and understand the task BEFORE you write
the orchestration script. These are ordinary tool calls, separate from the
`eval` tool: read the data file, list or glob the directory, grep for
what matters, then decide how to split the work.
Use `task` for work that benefits from an autonomous agentic loop: reading
or searching with the subagent's own tools, inspecting multiple files, following
leads, making judgment calls, or producing a final synthesized report.
Never write `eval` code that spawns a subagent just to read or parse a
file or list a directory. That is a deterministic step you do yourself with a
direct tool call; spending a whole agent loop on it is wasteful.
#### Pre-read shared context in the parent when useful
Once you understand the shape of the work, you have creative freedom in how
you split it:
If many subagents need the same source list or file content and `tools.*` is
available, gather that context once in the parent REPL before dispatching:
- One dispatch per file or per record, when the items are already separate.
- Chunk a large input yourself — read it, split it, optionally write a small
input file per chunk — and dispatch one subagent per chunk.
- A cheap classification pass first, then deeper dispatches only for the items
that warrant them.
```javascript
const files = (await tools.glob({ pattern: "src/**/*.ts" }))
.split("\n")
.filter(Boolean);
Then write JavaScript in the `eval` tool that distributes the heavy,
agentic work to subagents with `task()`: analyzing file contents, exploring a
codebase, making judgment calls, rewriting code, or synthesizing a report.
const items = await Promise.all(files.map(async (file) => {
const content = await tools.readFile({ file_path: file });
return { file, content };
}));
const batchSize = 10;
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
results.push(...(await Promise.all(batch.map(async (it) => {
const finding = await task({
description:
"Review this file for auth bypasses. Return concrete findings only.\n\n" +
"File: " + it.file + "\n\n" +
it.content,
subagent_type: "reviewer",
response_schema: {
type: "object",
properties: {
findings: { type: "array", items: { type: "object" } },
},
required: ["findings"],
},
});
return { ...it, ...finding };
}))));
}
```
Hand each subagent a locator, not a payload. Subagents have their own file
tools, so for anything that lives in a file — a file to review, rewrite, or
audit — pass the path and let the subagent read it. Do NOT read a whole file
just to paste its contents into the description; that bloats every dispatch
and duplicates the file across them. Reserve inline content for small or
derived data that has no path of its own: a single parsed record, or a chunk
you split out of a larger input (write the chunk to its own file and pass that
path if it is large). Assemble the results in JS.
#### Compose multiple stages
@@ -278,54 +275,75 @@ cheap classification, filter to the risky items, then dispatch deeper reviews
only for those items.
```javascript
const tagged = [];
for (let i = 0; i < items.length; i += 10) {
const batch = items.slice(i, i + 10);
tagged.push(...(await Promise.all(batch.map(async (it) => {
const tag = await task({
description: "Classify " + it.file + " as handler, util, test, or config.",
subagent_type: "reviewer",
response_schema: {
type: "object",
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
required: ["kind", "risky"],
},
});
return { ...it, ...tag };
}))));
}
const tagged = await Promise.all(files.map((file) =>
task({
description: "Read " + file + " and classify it as handler, util, " +
"test, or config.",
subagentType: "reviewer",
responseSchema: {
type: "object",
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
required: ["kind", "risky"],
},
}).then((tag) => ({ file, ...tag }))
));
const riskyHandlers = tagged.filter((it) => it.kind === "handler" && it.risky);
const deepReviews = [];
for (let i = 0; i < riskyHandlers.length; i += 10) {
const batch = riskyHandlers.slice(i, i + 10);
deepReviews.push(...(await Promise.all(batch.map(async (it) => {
const review = await task({
description: "Deep security review of " + it.file + ". Cite line numbers.",
subagent_type: "reviewer",
});
return { ...it, review };
}))));
}
const deepReviews = await Promise.all(riskyHandlers.map((it) =>
task({
description: "Deep security review of " + it.file + ". Cite line numbers.",
subagentType: "reviewer",
}).then((review) => ({ ...it, review }))
));
```
#### Get results out without flooding your context
#### Return results via the last expression, not `console.log`
Keep large result sets in JS variables. Do not `console.log` the full result set.
If `tools.writeFile` is exposed, persist structured output from inside the eval:
The value of the last expression in an `eval` call (or a resolved
top-level `await`) is returned to you as the result. Make that final
expression the variable holding your result and read it from there.
`console.log` is only for incidental debugging: its output is capped and
truncated, while the returned value is not, so never `console.log` your
actual results.
Keep large intermediate sets in JS variables and return only a compact
summary or a small slice, not the entire dataset. To persist full output,
have a subagent write it, or write it with your own file tool outside the
`eval` call.
#### Reuse what earlier evals left in scope
The REPL is persistent within a turn: every top-level variable, function, and
class you declare is kept and is available in your next `eval` call
(each is hoisted to global scope). So if a later step needs something an
earlier eval produced or bound, **reference that variable by name** — do not
write a new literal that re-types data a previous eval already returned or
computed.
If you catch yourself pasting a big array or object of values you produced in
an earlier call, that is the tell: the variable is still in scope, so use it.
Re-typing prior results as a fresh literal wastes tokens and drifts from what
actually ran.
```javascript
await tools.writeFile({
file_path: "/results/subagent-output.json",
content: JSON.stringify(deepReviews),
});
// An earlier eval bound this:
// const auditResults = await Promise.all(files.map(/* ...audit... */));
// A later eval — reference it; do NOT paste the findings back in as a literal:
const findings = auditResults.flatMap((r) =>
r.findings.map((f) => ({ ...f, file: r.file }))
);
const verified = await Promise.all(findings.map((f) =>
task({ description: "Verify this finding: " + f.evidence, subagentType: "verifier" })
.then((v) => ({ ...f, ...v }))
));
```
Otherwise return a compact summary or a small slice of the results, not the
entire intermediate dataset.
#### When the user asks for a "workflow"
#### Across evals
Variables persist according to the interpreter persistence mode above, but
re-establish what you need in each eval. Doing the whole workflow in one
`eval` call is usually simplest.
If the user's request mentions running a "workflow" (or otherwise uses the
word "workflow"), fan the work out to subagents rather than doing it all
yourself. Explore with your own tools first as needed, then write JavaScript
in the `eval` tool that dispatches subagents with `task()` and
assembles their results. The point is to distribute the heavy work in
parallel, not to grind through it one tool call at a time.
@@ -124,39 +124,48 @@ An `eval` tool is available. It runs JavaScript in a persistent REPL.
- State (variables, functions) persists across tool calls within a single turn of conversation. They DO NOT persist across multiple turns.
- Top-level `await` works; Promises resolve before the call returns.
- Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (`fetch`, `require`, `fs`, `process`, real `Date.now()` are unavailable or stubbed). External side effects from inside the REPL are only reachable via the `tools.*` namespace when it is exposed (see below); without it, the REPL is pure computation.
- Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (`fetch`, `require`, `fs`, `process`, real `Date.now()` are unavailable or stubbed).
- The REPL has no access to host tools, files, or the network: it is pure computation. Return values to communicate results.
- Timeout: 5.0s per call. Memory: 64 MB total.
- `console.log` output is captured and returned alongside the result.
### Dispatching Subagents with `task`
`task` is your primitive for running configured subagents from inside the
JavaScript REPL. You orchestrate everything else - fan-out, filtering,
deduplication, multi-stage flow, and synthesis - in plain JavaScript.
JavaScript REPL. Your job here is to DISTRIBUTE work, not to do it yourself:
write JavaScript that fans work out to subagents and assembles their results.
You handle the orchestration - fan-out, filtering, deduplication, multi-stage
flow, and synthesis - in plain JavaScript.
#### The primitive
```javascript
await task({
description, // full autonomous task prompt
subagent_type, // configured subagent name
response_schema, // optional JSON Schema for structured output
subagentType, // configured subagent name
responseSchema, // optional JSON Schema for structured output
}); // -> Promise<unknown>
```
`task` runs a full agentic loop for the selected configured subagent. The
subagent can use whatever tools it was configured with, iterate, inspect
context, and return one final result. `subagent_type` is required; use one of
context, and return one final result. `subagentType` is required; use one of
the configured subagent names.
`description` is the only prompt the subagent receives for this dispatch. Make
it complete: include the goal, constraints, relevant context, what to inspect,
and the exact shape or level of detail you expect back. Each dispatch is
stateless from the caller's perspective; you cannot send follow-up messages to
the same subagent run.
it complete: the goal, the constraints, what to inspect, and the exact shape
or level of detail you expect back. Give context as locators — file paths and
symbol names — not as pasted file contents. If you already read a file while
exploring, still pass its path and let the subagent read it; do not paste back
what you read. Each dispatch is stateless from the caller's perspective; you
cannot send follow-up messages to the same subagent run.
`response_schema` is optional. When provided, the resolved value is already a
typed JavaScript value matching the schema. Do not call `JSON.parse` unless the
`responseSchema` is optional, but set it on any dispatch whose result feeds
later code. A deterministic, typed shape is what lets you compose the next
stage reliably — index it, sort it, compare fields, branch on it, merge it —
instead of parsing free-form text. This is what makes a whole workflow
composable as one script. When provided, the resolved value is already a typed
JavaScript value matching the schema; do not call `JSON.parse` unless the
subagent intentionally returned a JSON string. Dynamic schemas work for
declarative subagents; runnable-backed subagents reject dynamic schemas because
their runnable is already compiled.
@@ -177,26 +186,31 @@ Hold your work in JS: an array of items in, an array of results out. Merge each
dispatch result back onto its item. Multi-stage analysis means: run a pass,
filter or regroup the array in JS, then run another pass over the survivors.
Prefer one `eval` call that performs the whole workflow. Splitting the
workflow across multiple `eval` calls costs model turns and forces you to
re-establish state.
You can run the whole workflow in one `eval` call or split it across
several — both are fine. A single end-to-end script (generate, compare, pick a
winner; or review every item, then synthesize) is clean when you can write it
in one go; splitting is also fine when you want to inspect results between
stages. Either way, don't redo work across calls — reuse what is already in
scope (see "Reuse what earlier evals left in scope" below).
#### Fan out with bounded concurrency
Dispatch independent work in parallel with `Promise.all`, but in explicit
batches around 10 so you do not launch hundreds of subagents at once. The bridge
enforces a hard per-REPL cap of 32 concurrent `task` calls.
enforces a hard per-REPL cap of 32 concurrent subagent calls.
```javascript
const files = ["/src/a.ts", "/src/b.ts", "/src/c.ts"]; // found while exploring
const batchSize = 10;
const reviewed = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
reviewed.push(...(await Promise.all(batch.map(async (it) => {
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
reviewed.push(...(await Promise.all(batch.map(async (file) => {
const result = await task({
description: "Review " + it.file + " for SQL injection. Cite line numbers.",
subagent_type: "reviewer",
response_schema: {
description: "Read " + file + " and review it for SQL injection. " +
"Cite line numbers.",
subagentType: "reviewer",
responseSchema: {
type: "object",
properties: {
vulnerabilities: {
@@ -215,61 +229,44 @@ for (let i = 0; i < items.length; i += batchSize) {
required: ["vulnerabilities"],
},
});
return { ...it, ...result };
return { file, ...result };
}))));
}
```
#### Use parent JS for cheap work; use subagents for agentic work
#### Explore with your own tools first, then distribute
Use JavaScript in the parent REPL for deterministic orchestration: joining
arrays, deduping, sorting, filtering, grouping, batching, and merging results.
If the `tools.*` namespace is exposed, also use it to pre-read files or collect
shared data once, then pass only the relevant content to each subagent in
`description`.
You already have your normal tools for reading, listing, globbing, and
grepping files. Use them to explore and understand the task BEFORE you write
the orchestration script. These are ordinary tool calls, separate from the
`eval` tool: read the data file, list or glob the directory, grep for
what matters, then decide how to split the work.
Use `task` for work that benefits from an autonomous agentic loop: reading
or searching with the subagent's own tools, inspecting multiple files, following
leads, making judgment calls, or producing a final synthesized report.
Never write `eval` code that spawns a subagent just to read or parse a
file or list a directory. That is a deterministic step you do yourself with a
direct tool call; spending a whole agent loop on it is wasteful.
#### Pre-read shared context in the parent when useful
Once you understand the shape of the work, you have creative freedom in how
you split it:
If many subagents need the same source list or file content and `tools.*` is
available, gather that context once in the parent REPL before dispatching:
- One dispatch per file or per record, when the items are already separate.
- Chunk a large input yourself — read it, split it, optionally write a small
input file per chunk — and dispatch one subagent per chunk.
- A cheap classification pass first, then deeper dispatches only for the items
that warrant them.
```javascript
const files = (await tools.glob({ pattern: "src/**/*.ts" }))
.split("\n")
.filter(Boolean);
Then write JavaScript in the `eval` tool that distributes the heavy,
agentic work to subagents with `task()`: analyzing file contents, exploring a
codebase, making judgment calls, rewriting code, or synthesizing a report.
const items = await Promise.all(files.map(async (file) => {
const content = await tools.readFile({ file_path: file });
return { file, content };
}));
const batchSize = 10;
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
results.push(...(await Promise.all(batch.map(async (it) => {
const finding = await task({
description:
"Review this file for auth bypasses. Return concrete findings only.\n\n" +
"File: " + it.file + "\n\n" +
it.content,
subagent_type: "reviewer",
response_schema: {
type: "object",
properties: {
findings: { type: "array", items: { type: "object" } },
},
required: ["findings"],
},
});
return { ...it, ...finding };
}))));
}
```
Hand each subagent a locator, not a payload. Subagents have their own file
tools, so for anything that lives in a file — a file to review, rewrite, or
audit — pass the path and let the subagent read it. Do NOT read a whole file
just to paste its contents into the description; that bloats every dispatch
and duplicates the file across them. Reserve inline content for small or
derived data that has no path of its own: a single parsed record, or a chunk
you split out of a larger input (write the chunk to its own file and pass that
path if it is large). Assemble the results in JS.
#### Compose multiple stages
@@ -278,54 +275,75 @@ cheap classification, filter to the risky items, then dispatch deeper reviews
only for those items.
```javascript
const tagged = [];
for (let i = 0; i < items.length; i += 10) {
const batch = items.slice(i, i + 10);
tagged.push(...(await Promise.all(batch.map(async (it) => {
const tag = await task({
description: "Classify " + it.file + " as handler, util, test, or config.",
subagent_type: "reviewer",
response_schema: {
type: "object",
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
required: ["kind", "risky"],
},
});
return { ...it, ...tag };
}))));
}
const tagged = await Promise.all(files.map((file) =>
task({
description: "Read " + file + " and classify it as handler, util, " +
"test, or config.",
subagentType: "reviewer",
responseSchema: {
type: "object",
properties: { kind: { type: "string" }, risky: { type: "boolean" } },
required: ["kind", "risky"],
},
}).then((tag) => ({ file, ...tag }))
));
const riskyHandlers = tagged.filter((it) => it.kind === "handler" && it.risky);
const deepReviews = [];
for (let i = 0; i < riskyHandlers.length; i += 10) {
const batch = riskyHandlers.slice(i, i + 10);
deepReviews.push(...(await Promise.all(batch.map(async (it) => {
const review = await task({
description: "Deep security review of " + it.file + ". Cite line numbers.",
subagent_type: "reviewer",
});
return { ...it, review };
}))));
}
const deepReviews = await Promise.all(riskyHandlers.map((it) =>
task({
description: "Deep security review of " + it.file + ". Cite line numbers.",
subagentType: "reviewer",
}).then((review) => ({ ...it, review }))
));
```
#### Get results out without flooding your context
#### Return results via the last expression, not `console.log`
Keep large result sets in JS variables. Do not `console.log` the full result set.
If `tools.writeFile` is exposed, persist structured output from inside the eval:
The value of the last expression in an `eval` call (or a resolved
top-level `await`) is returned to you as the result. Make that final
expression the variable holding your result and read it from there.
`console.log` is only for incidental debugging: its output is capped and
truncated, while the returned value is not, so never `console.log` your
actual results.
Keep large intermediate sets in JS variables and return only a compact
summary or a small slice, not the entire dataset. To persist full output,
have a subagent write it, or write it with your own file tool outside the
`eval` call.
#### Reuse what earlier evals left in scope
The REPL is persistent within a turn: every top-level variable, function, and
class you declare is kept and is available in your next `eval` call
(each is hoisted to global scope). So if a later step needs something an
earlier eval produced or bound, **reference that variable by name** — do not
write a new literal that re-types data a previous eval already returned or
computed.
If you catch yourself pasting a big array or object of values you produced in
an earlier call, that is the tell: the variable is still in scope, so use it.
Re-typing prior results as a fresh literal wastes tokens and drifts from what
actually ran.
```javascript
await tools.writeFile({
file_path: "/results/subagent-output.json",
content: JSON.stringify(deepReviews),
});
// An earlier eval bound this:
// const auditResults = await Promise.all(files.map(/* ...audit... */));
// A later eval — reference it; do NOT paste the findings back in as a literal:
const findings = auditResults.flatMap((r) =>
r.findings.map((f) => ({ ...f, file: r.file }))
);
const verified = await Promise.all(findings.map((f) =>
task({ description: "Verify this finding: " + f.evidence, subagentType: "verifier" })
.then((v) => ({ ...f, ...v }))
));
```
Otherwise return a compact summary or a small slice of the results, not the
entire intermediate dataset.
#### When the user asks for a "workflow"
#### Across evals
Variables persist according to the interpreter persistence mode above, but
re-establish what you need in each eval. Doing the whole workflow in one
`eval` call is usually simplest.
If the user's request mentions running a "workflow" (or otherwise uses the
word "workflow"), fan the work out to subagents rather than doing it all
yourself. Explore with your own tools first as needed, then write JavaScript
in the `eval` tool that dispatches subagents with `task()` and
assembles their results. The point is to distribute the heavy work in
parallel, not to grind through it one tool call at a time.
@@ -267,13 +267,14 @@ def test_system_prompt_omits_subagent_guidance_when_disabled() -> None:
def test_system_prompt_mentions_single_turn_when_snapshots_disabled() -> None:
with pytest.warns(DeprecationWarning, match="snapshot_between_turns"):
mw = CodeInterpreterMiddleware(snapshot_between_turns=False)
assert "DO NOT persist across multiple turns" in mw._base_system_prompt
assert "DO NOT persist across multiple turns" in mw._base_prompt(ptc_attached=False)
def test_system_prompt_mentions_mode_call() -> None:
mw = CodeInterpreterMiddleware(mode="call")
assert "fresh sandboxed REPL for each invocation" in mw._base_system_prompt
assert "does not persist across tool calls" in mw._base_system_prompt
base_prompt = mw._base_prompt(ptc_attached=False)
assert "fresh sandboxed REPL for each invocation" in base_prompt
assert "does not persist across tool calls" in base_prompt
def test_mode_call_defaults_snapshot_between_turns_to_false() -> None:
@@ -1132,7 +1133,7 @@ async def test_async_task_global_invokes_runner(repl: _ThreadREPL) -> None:
"}"
"JSON.stringify(await task({"
"description: 'work', "
"subagent_type: 'worker'"
"subagentType: 'worker'"
"}))",
outer_runtime=_subagent_runtime(runnable),
)
@@ -1167,20 +1168,20 @@ async def test_async_task_global_not_installed_when_disabled(
("code", "message"),
[
(
"await task({subagent_type: 'worker'})",
"await task({subagentType: 'worker'})",
"task() requires non-empty string field `description`",
),
(
"await task({description: 'work'})",
"task() requires non-empty string field `subagent_type`",
"task() requires non-empty string field `subagentType`",
),
(
"await task({"
"description: 'work', "
"subagent_type: 'worker', "
"response_schema: 'bad'"
"subagentType: 'worker', "
"responseSchema: 'bad'"
"})",
"task() field `response_schema` must be an object when provided",
"task() field `responseSchema` must be an object when provided",
),
],
)
@@ -1214,7 +1215,7 @@ async def test_async_task_global_missing_task_tool_surfaces_as_eval_error(
)
outcome = await repl.eval_async(
"await task({description: 'work', subagent_type: 'worker'})",
"await task({description: 'work', subagentType: 'worker'})",
outer_runtime=runtime,
)
@@ -1233,13 +1234,13 @@ async def test_async_task_global_returns_declarative_structured_response_object(
"};"
"const first = await task({"
"description: 'work', "
"subagent_type: 'worker', "
"response_schema: schema"
"subagentType: 'worker', "
"responseSchema: schema"
"});"
"const second = await task({"
"description: 'work again', "
"subagent_type: 'worker', "
"response_schema: schema"
"subagentType: 'worker', "
"responseSchema: schema"
"});"
"JSON.stringify({"
"label: first.label, "
@@ -1266,8 +1267,8 @@ async def test_async_task_global_rejects_compiled_response_schema(
outcome = await repl.eval_async(
"await task({"
"description: 'work', "
"subagent_type: 'worker', "
"response_schema: {"
"subagentType: 'worker', "
"responseSchema: {"
"type: 'object', "
"properties: {ok: {type: 'boolean'}}, "
"required: ['ok']"
@@ -1296,7 +1297,7 @@ async def test_async_task_global_uses_last_non_empty_ai_message(
)
outcome = await repl.eval_async(
"JSON.stringify(await task({description: 'work', subagent_type: 'worker'}))",
"JSON.stringify(await task({description: 'work', subagentType: 'worker'}))",
outer_runtime=_subagent_runtime(runnable),
)
@@ -1317,7 +1318,7 @@ async def test_async_task_global_rejects_state_without_messages(
)
outcome = await repl.eval_async(
"await task({description: 'work', subagent_type: 'worker'})",
"await task({description: 'work', subagentType: 'worker'})",
outer_runtime=_subagent_runtime(runnable),
)
@@ -1369,7 +1370,7 @@ async def test_async_task_global_limits_concurrency_per_repl(
outcome = await repl.eval_async(
"const calls = [];"
"for (let i = 0; i < 64; i++) {"
" calls.push(task({description: String(i), subagent_type: 'worker'}));"
" calls.push(task({description: String(i), subagentType: 'worker'}));"
"}"
"(await Promise.all(calls)).length",
outer_runtime=_subagent_runtime(runnable),
@@ -175,7 +175,7 @@ async def test_quickjs_task_global_available_without_ptc_e2e() -> None:
agent = create_deep_agent(
model=_FakeChatModel(
messages=_script(
"await task({description: 'say hi', subagent_type: 'researcher'})",
"await task({description: 'say hi', subagentType: 'researcher'})",
final_message="done",
)
),