mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
feat(quickjs): prompt tuning on task global (#4066)
This commit is contained in:
@@ -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)
|
||||
|
||||
+126
-108
@@ -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
|
||||
|
||||
+126
-108
@@ -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
|
||||
|
||||
+126
-108
@@ -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
|
||||
|
||||
+126
-108
@@ -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.
|
||||
|
||||
+126
-108
@@ -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.
|
||||
|
||||
+126
-108
@@ -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",
|
||||
)
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user