@langchain/quickjs 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ import { shouldInterruptAfterDeadline } from "quickjs-emscripten";
6
6
  import { newQuickJSAsyncWASMModuleFromVariant } from "quickjs-emscripten-core";
7
7
  import { compile } from "json-schema-to-typescript";
8
8
  import { toJsonSchema } from "@langchain/core/utils/json_schema";
9
+ import { isCommand } from "@langchain/langgraph";
10
+ import { BaseMessage } from "@langchain/core/messages";
9
11
  import { Parser } from "acorn";
10
12
  import { tsPlugin } from "@sveltejs/acorn-typescript";
11
13
  import { walk } from "estree-walker";
@@ -95,6 +97,59 @@ async function toolToTypeSignature(name, description, jsonSchema) {
95
97
  `;
96
98
  }
97
99
  //#endregion
100
+ //#region src/coerce.ts
101
+ /**
102
+ * Coercion of tool / subagent return values for the QuickJS bridge.
103
+ *
104
+ * The deepagents `task` tool resolves to a LangGraph `Command` whose payload
105
+ * carries the subagent's final message(s) under `update.messages`; some tools
106
+ * return a `ToolMessage` or a list of messages. The interpreter bridges need
107
+ * the underlying output, not the envelope, so this unwraps those shapes to the
108
+ * content the model actually cares about.
109
+ */
110
+ /**
111
+ * Return the trailing message content from a `Command`'s `update.messages`,
112
+ * scanning from the end for the last message that actually has content. Returns
113
+ * the command unchanged when it has no message-shaped payload.
114
+ */
115
+ function extractCommandContent(command) {
116
+ const update = command.update;
117
+ const messages = update !== null && typeof update === "object" ? update.messages : void 0;
118
+ if (Array.isArray(messages)) for (let i = messages.length - 1; i >= 0; i--) {
119
+ const message = messages[i];
120
+ if (BaseMessage.isInstance(message) && message.content != null) return message.content;
121
+ }
122
+ return command;
123
+ }
124
+ /**
125
+ * Unwrap a LangChain `Command` / `ToolMessage` / message-list envelope to the
126
+ * underlying content. Non-envelope values (strings, content-block arrays, plain
127
+ * objects) are returned unchanged.
128
+ *
129
+ * @param value The raw value returned by a tool or subagent dispatch.
130
+ * @returns The unwrapped content, or `value` itself when it isn't an envelope.
131
+ */
132
+ function unwrapToolEnvelope(value) {
133
+ if (typeof value === "string") return value;
134
+ if (isCommand(value)) {
135
+ const inner = extractCommandContent(value);
136
+ return inner === value ? value : unwrapToolEnvelope(inner);
137
+ }
138
+ if (BaseMessage.isInstance(value)) return unwrapToolEnvelope(value.content);
139
+ if (Array.isArray(value)) {
140
+ for (let i = value.length - 1; i >= 0; i--) {
141
+ const entry = value[i];
142
+ if (BaseMessage.isInstance(entry)) return unwrapToolEnvelope(entry.content);
143
+ if (isCommand(entry)) {
144
+ const inner = extractCommandContent(entry);
145
+ if (inner !== entry) return unwrapToolEnvelope(inner);
146
+ }
147
+ }
148
+ return value;
149
+ }
150
+ return value;
151
+ }
152
+ //#endregion
98
153
  //#region src/transform.ts
99
154
  /**
100
155
  * AST-based code transform pipeline for the REPL.
@@ -396,6 +451,7 @@ function getSharedModule() {
396
451
  * @returns Plain string representation of the tool output.
397
452
  */
398
453
  function extractToolText(result) {
454
+ result = unwrapToolEnvelope(result);
399
455
  if (typeof result === "string") return result;
400
456
  if (Array.isArray(result)) {
401
457
  const texts = [];
@@ -490,7 +546,7 @@ var ReplSession = class ReplSession {
490
546
  subagentQueue = null;
491
547
  bridgeDispatchRef = null;
492
548
  /** Allowed keys in the subagent input object. */
493
- static SUBAGENT_ALLOWED_KEYS = new Set([
549
+ static SUBAGENT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
494
550
  "description",
495
551
  "subagentType",
496
552
  "responseSchema"
@@ -900,8 +956,10 @@ function renderSubagentPrompt(toolName) {
900
956
  ### Dispatching Subagents with \`task\`
901
957
 
902
958
  \`task\` is your primitive for running configured subagents from inside the
903
- JavaScript REPL. You orchestrate everything else - fan-out, filtering,
904
- deduplication, multi-stage flow, and synthesis - in plain JavaScript.
959
+ JavaScript REPL. Your job here is to DISTRIBUTE work, not to do it yourself:
960
+ write JavaScript that fans work out to subagents and assembles their results.
961
+ You handle the orchestration - fan-out, filtering, deduplication, multi-stage
962
+ flow, and synthesis - in plain JavaScript.
905
963
 
906
964
  #### The primitive
907
965
 
@@ -919,13 +977,19 @@ function renderSubagentPrompt(toolName) {
919
977
  the configured subagent names.
920
978
 
921
979
  \`description\` is the only prompt the subagent receives for this dispatch. Make
922
- it complete: include the goal, constraints, relevant context, what to inspect,
923
- and the exact shape or level of detail you expect back. Each dispatch is
924
- stateless from the caller's perspective; you cannot send follow-up messages to
925
- the same subagent run.
980
+ it complete: the goal, the constraints, what to inspect, and the exact shape
981
+ or level of detail you expect back. Give context as locators — file paths and
982
+ symbol names not as pasted file contents. If you already read a file while
983
+ exploring, still pass its path and let the subagent read it; do not paste back
984
+ what you read. Each dispatch is stateless from the caller's perspective; you
985
+ cannot send follow-up messages to the same subagent run.
926
986
 
927
- \`responseSchema\` is optional. When provided, the resolved value is already a
928
- typed JavaScript value matching the schema. Do not call \`JSON.parse\` unless the
987
+ \`responseSchema\` is optional, but set it on any dispatch whose result feeds
988
+ later code. A deterministic, typed shape is what lets you compose the next
989
+ stage reliably — index it, sort it, compare fields, branch on it, merge it —
990
+ instead of parsing free-form text. This is what makes a whole workflow
991
+ composable as one script. When provided, the resolved value is already a typed
992
+ JavaScript value matching the schema; do not call \`JSON.parse\` unless the
929
993
  subagent intentionally returned a JSON string. Dynamic schemas work for
930
994
  declarative subagents; runnable-backed subagents reject dynamic schemas because
931
995
  their runnable is already compiled.
@@ -946,9 +1010,12 @@ function renderSubagentPrompt(toolName) {
946
1010
  dispatch result back onto its item. Multi-stage analysis means: run a pass,
947
1011
  filter or regroup the array in JS, then run another pass over the survivors.
948
1012
 
949
- Prefer one \`${toolName}\` call that performs the whole workflow. Splitting the
950
- workflow across multiple \`${toolName}\` calls costs model turns and forces you to
951
- re-establish state.
1013
+ You can run the whole workflow in one \`${toolName}\` call or split it across
1014
+ several both are fine. A single end-to-end script (generate, compare, pick a
1015
+ winner; or review every item, then synthesize) is clean when you can write it
1016
+ in one go; splitting is also fine when you want to inspect results between
1017
+ stages. Either way, don't redo work across calls — reuse what is already in
1018
+ scope (see "Reuse what earlier evals left in scope" below).
952
1019
 
953
1020
  #### Fan out with bounded concurrency
954
1021
 
@@ -957,13 +1024,15 @@ function renderSubagentPrompt(toolName) {
957
1024
  enforces a hard per-REPL cap of 32 concurrent subagent calls.
958
1025
 
959
1026
  \`\`\`javascript
1027
+ const files = ["/src/a.ts", "/src/b.ts", "/src/c.ts"]; // found while exploring
960
1028
  const batchSize = 10;
961
1029
  const reviewed = [];
962
- for (let i = 0; i < items.length; i += batchSize) {
963
- const batch = items.slice(i, i + batchSize);
964
- reviewed.push(...(await Promise.all(batch.map(async (it) => {
1030
+ for (let i = 0; i < files.length; i += batchSize) {
1031
+ const batch = files.slice(i, i + batchSize);
1032
+ reviewed.push(...(await Promise.all(batch.map(async (file) => {
965
1033
  const result = await task({
966
- description: "Review " + it.file + " for SQL injection. Cite line numbers.",
1034
+ description: "Read " + file + " and review it for SQL injection. " +
1035
+ "Cite line numbers.",
967
1036
  subagentType: "reviewer",
968
1037
  responseSchema: {
969
1038
  type: "object",
@@ -984,61 +1053,44 @@ function renderSubagentPrompt(toolName) {
984
1053
  required: ["vulnerabilities"],
985
1054
  },
986
1055
  });
987
- return { ...it, ...result };
1056
+ return { file, ...result };
988
1057
  }))));
989
1058
  }
990
1059
  \`\`\`
991
1060
 
992
- #### Use parent JS for cheap work; use subagents for agentic work
1061
+ #### Explore with your own tools first, then distribute
993
1062
 
994
- Use JavaScript in the parent REPL for deterministic orchestration: joining
995
- arrays, deduping, sorting, filtering, grouping, batching, and merging results.
996
- If the \`tools.*\` namespace is exposed, also use it to pre-read files or collect
997
- shared data once, then pass only the relevant content to each subagent in
998
- \`description\`.
1063
+ You already have your normal tools for reading, listing, globbing, and
1064
+ grepping files. Use them to explore and understand the task BEFORE you write
1065
+ the orchestration script. These are ordinary tool calls, separate from the
1066
+ \`${toolName}\` tool: read the data file, list or glob the directory, grep for
1067
+ what matters, then decide how to split the work.
999
1068
 
1000
- Use \`task\` for work that benefits from an autonomous agentic loop: reading
1001
- or searching with the subagent's own tools, inspecting multiple files, following
1002
- leads, making judgment calls, or producing a final synthesized report.
1069
+ Never write \`${toolName}\` code that spawns a subagent just to read or parse a
1070
+ file or list a directory. That is a deterministic step you do yourself with a
1071
+ direct tool call; spending a whole agent loop on it is wasteful.
1003
1072
 
1004
- #### Pre-read shared context in the parent when useful
1073
+ Once you understand the shape of the work, you have creative freedom in how
1074
+ you split it:
1005
1075
 
1006
- If many subagents need the same source list or file content and \`tools.*\` is
1007
- available, gather that context once in the parent REPL before dispatching:
1076
+ - One dispatch per file or per record, when the items are already separate.
1077
+ - Chunk a large input yourself read it, split it, optionally write a small
1078
+ input file per chunk — and dispatch one subagent per chunk.
1079
+ - A cheap classification pass first, then deeper dispatches only for the items
1080
+ that warrant them.
1008
1081
 
1009
- \`\`\`javascript
1010
- const files = (await tools.glob({ pattern: "src/**/*.ts" }))
1011
- .split("\\n")
1012
- .filter(Boolean);
1082
+ Then write JavaScript in the \`${toolName}\` tool that distributes the heavy,
1083
+ agentic work to subagents with \`task()\`: analyzing file contents, exploring a
1084
+ codebase, making judgment calls, rewriting code, or synthesizing a report.
1013
1085
 
1014
- const items = await Promise.all(files.map(async (file) => {
1015
- const content = await tools.readFile({ file_path: file });
1016
- return { file, content };
1017
- }));
1018
-
1019
- const batchSize = 10;
1020
- const results = [];
1021
- for (let i = 0; i < items.length; i += batchSize) {
1022
- const batch = items.slice(i, i + batchSize);
1023
- results.push(...(await Promise.all(batch.map(async (it) => {
1024
- const finding = await task({
1025
- description:
1026
- "Review this file for auth bypasses. Return concrete findings only.\\n\\n" +
1027
- "File: " + it.file + "\\n\\n" +
1028
- it.content,
1029
- subagentType: "reviewer",
1030
- responseSchema: {
1031
- type: "object",
1032
- properties: {
1033
- findings: { type: "array", items: { type: "object" } },
1034
- },
1035
- required: ["findings"],
1036
- },
1037
- });
1038
- return { ...it, ...finding };
1039
- }))));
1040
- }
1041
- \`\`\`
1086
+ Hand each subagent a locator, not a payload. Subagents have their own file
1087
+ tools, so for anything that lives in a file — a file to review, rewrite, or
1088
+ audit pass the path and let the subagent read it. Do NOT read a whole file
1089
+ just to paste its contents into the description; that bloats every dispatch
1090
+ and duplicates the file across them. Reserve inline content for small or
1091
+ derived data that has no path of its own: a single parsed record, or a chunk
1092
+ you split out of a larger input (write the chunk to its own file and pass that
1093
+ path if it is large). Assemble the results in JS.
1042
1094
 
1043
1095
  #### Compose multiple stages
1044
1096
 
@@ -1047,67 +1099,89 @@ function renderSubagentPrompt(toolName) {
1047
1099
  only for those items.
1048
1100
 
1049
1101
  \`\`\`javascript
1050
- const tagged = [];
1051
- for (let i = 0; i < items.length; i += 10) {
1052
- const batch = items.slice(i, i + 10);
1053
- tagged.push(...(await Promise.all(batch.map(async (it) => {
1054
- const tag = await task({
1055
- description: "Classify " + it.file + " as handler, util, test, or config.",
1056
- subagentType: "reviewer",
1057
- responseSchema: {
1058
- type: "object",
1059
- properties: { kind: { type: "string" }, risky: { type: "boolean" } },
1060
- required: ["kind", "risky"],
1061
- },
1062
- });
1063
- return { ...it, ...tag };
1064
- }))));
1065
- }
1102
+ const tagged = await Promise.all(files.map((file) =>
1103
+ task({
1104
+ description: "Read " + file + " and classify it as handler, util, " +
1105
+ "test, or config.",
1106
+ subagentType: "reviewer",
1107
+ responseSchema: {
1108
+ type: "object",
1109
+ properties: { kind: { type: "string" }, risky: { type: "boolean" } },
1110
+ required: ["kind", "risky"],
1111
+ },
1112
+ }).then((tag) => ({ file, ...tag }))
1113
+ ));
1066
1114
 
1067
1115
  const riskyHandlers = tagged.filter((it) => it.kind === "handler" && it.risky);
1068
- const deepReviews = [];
1069
- for (let i = 0; i < riskyHandlers.length; i += 10) {
1070
- const batch = riskyHandlers.slice(i, i + 10);
1071
- deepReviews.push(...(await Promise.all(batch.map(async (it) => {
1072
- const review = await task({
1073
- description: "Deep security review of " + it.file + ". Cite line numbers.",
1074
- subagentType: "reviewer",
1075
- });
1076
- return { ...it, review };
1077
- }))));
1078
- }
1116
+ const deepReviews = await Promise.all(riskyHandlers.map((it) =>
1117
+ task({
1118
+ description: "Deep security review of " + it.file + ". Cite line numbers.",
1119
+ subagentType: "reviewer",
1120
+ }).then((review) => ({ ...it, review }))
1121
+ ));
1079
1122
  \`\`\`
1080
1123
 
1081
- #### Get results out without flooding your context
1124
+ #### Return results via the last expression, not \`console.log\`
1125
+
1126
+ The value of the last expression in an \`${toolName}\` call (or a resolved
1127
+ top-level \`await\`) is returned to you as the result. Make that final
1128
+ expression the variable holding your result and read it from there.
1129
+ \`console.log\` is only for incidental debugging: its output is capped and
1130
+ truncated, while the returned value is not, so never \`console.log\` your
1131
+ actual results.
1132
+
1133
+ Keep large intermediate sets in JS variables and return only a compact
1134
+ summary or a small slice, not the entire dataset. To persist full output,
1135
+ have a subagent write it, or write it with your own file tool outside the
1136
+ \`${toolName}\` call.
1137
+
1138
+ #### Reuse what earlier evals left in scope
1082
1139
 
1083
- Keep large result sets in JS variables. Do not \`console.log\` the full result set.
1084
- If \`tools.writeFile\` is exposed, persist structured output from inside the eval:
1140
+ The REPL is persistent within a turn: every top-level variable, function, and
1141
+ class you declare is kept and is available in your next \`${toolName}\` call
1142
+ (each is hoisted to global scope). So if a later step needs something an
1143
+ earlier eval produced or bound, **reference that variable by name** — do not
1144
+ write a new literal that re-types data a previous eval already returned or
1145
+ computed.
1146
+
1147
+ If you catch yourself pasting a big array or object of values you produced in
1148
+ an earlier call, that is the tell: the variable is still in scope, so use it.
1149
+ Re-typing prior results as a fresh literal wastes tokens and drifts from what
1150
+ actually ran.
1085
1151
 
1086
1152
  \`\`\`javascript
1087
- await tools.writeFile({
1088
- file_path: "/results/subagent-output.json",
1089
- content: JSON.stringify(deepReviews),
1090
- });
1091
- \`\`\`
1153
+ // An earlier eval bound this:
1154
+ // const auditResults = await Promise.all(files.map(/* ...audit... */));
1092
1155
 
1093
- Otherwise return a compact summary or a small slice of the results, not the
1094
- entire intermediate dataset.
1156
+ // A later eval reference it; do NOT paste the findings back in as a literal:
1157
+ const findings = auditResults.flatMap((r) =>
1158
+ r.findings.map((f) => ({ ...f, file: r.file }))
1159
+ );
1160
+ const verified = await Promise.all(findings.map((f) =>
1161
+ task({ description: "Verify this finding: " + f.evidence, subagentType: "verifier" })
1162
+ .then((v) => ({ ...f, ...v }))
1163
+ ));
1164
+ \`\`\`
1095
1165
 
1096
- #### Across evals
1166
+ #### When the user asks for a "workflow"
1097
1167
 
1098
- Variables persist according to the interpreter persistence mode above, but
1099
- re-establish what you need in each eval. Doing the whole workflow in one
1100
- \`${toolName}\` call is usually simplest.
1168
+ If the user's request mentions running a "workflow" (or otherwise uses the
1169
+ word "workflow"), fan the work out to subagents rather than doing it all
1170
+ yourself. Explore with your own tools first as needed, then write JavaScript
1171
+ in the \`${toolName}\` tool that dispatches subagents with \`task()\` and
1172
+ assembles their results. The point is to distribute the heavy work in
1173
+ parallel, not to grind through it one tool call at a time.
1101
1174
  `;
1102
1175
  }
1103
1176
  function renderReplSystemPrompt(opts) {
1177
+ const sideEffects = opts.hasPtc ? " External side effects from inside the REPL are reachable only via the `tools.*` namespace documented below." : " The REPL is pure computation; do any filesystem or other I/O with your normal tools, outside this tool.";
1104
1178
  return dedent`
1105
1179
  ### Interpreter
1106
1180
 
1107
1181
  An \`${opts.toolName}\` tool is available. It runs JavaScript in a persistent REPL.
1108
1182
  - State (variables, functions) persists across tool calls within a single turn of conversation. They DO NOT persist across multiple turns.
1109
1183
  - Top-level \`await\` works; Promises resolve before the call returns.
1110
- - 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.
1184
+ - Runtime sandbox: no built-in filesystem, network, stdlib, or wall-clock APIs (\`fetch\`, \`require\`, \`fs\`, \`process\`, real \`Date.now()\` are unavailable or stubbed).${sideEffects}
1111
1185
  - Timeout: ${opts.timeout}s per call. Memory: ${opts.memoryLimitMb} MB total.
1112
1186
  - \`console.log\` output is captured and returned alongside the result.
1113
1187
  `;
@@ -1152,10 +1226,14 @@ async function generatePtcPrompt(tools) {
1152
1226
  * StructuredToolInterface objects. Strings are looked up by name in agentTools;
1153
1227
  * instances are included directly without requiring agent registration. Strings
1154
1228
  * that don't match any agent tool are silently omitted.
1229
+ *
1230
+ * Throws if the subagent `task` tool is requested (by name or instance): it is
1231
+ * reserved for the `task()` global and cannot be a `tools.*` PTC member.
1155
1232
  */
1156
1233
  function resolveToolList(items, agentTools) {
1157
1234
  const agentByName = new Map(agentTools.map((t) => [t.name, t]));
1158
1235
  return items.flatMap((item) => {
1236
+ if ((typeof item === "string" ? item : item.name) === "task") throw new Error("The subagent `task` tool cannot be exposed via `ptc`. It is always available as the top-level `task()` global inside the REPL (with `subagentType` and `responseSchema` support); exposing it through the `tools.*` namespace would create a second, conflicting dispatch path that drops `responseSchema`. Remove \"task\" from `ptc`.");
1159
1237
  if (typeof item === "string") {
1160
1238
  const found = agentByName.get(item);
1161
1239
  return found ? [found] : [];
@@ -1170,11 +1248,6 @@ function createCodeInterpreterMiddleware(options = {}) {
1170
1248
  const { ptc, memoryLimitBytes = DEFAULT_MEMORY_LIMIT, maxStackSizeBytes = DEFAULT_MAX_STACK_SIZE, executionTimeoutMs = DEFAULT_EXECUTION_TIMEOUT, systemPrompt: customSystemPrompt = null, maxPtcCalls = 256, maxResultChars = DEFAULT_MAX_RESULTS_CHARS, toolName = DEFAULT_TOOL_NAME, captureConsole = true, subagents = true } = options;
1171
1249
  const maxSubagentConcurrency = subagents ? 32 : 0;
1172
1250
  if (maxPtcCalls !== null && maxPtcCalls !== void 0 && maxPtcCalls < 1) throw new Error("`maxPtcCalls` must be >= 1 or null");
1173
- const baseSystemPrompt = customSystemPrompt || renderReplSystemPrompt({
1174
- toolName,
1175
- timeout: executionTimeoutMs / 1e3,
1176
- memoryLimitMb: Math.floor(memoryLimitBytes / (1024 * 1024))
1177
- });
1178
1251
  const middlewareId = crypto.randomUUID();
1179
1252
  let cachedPtcPrompt = null;
1180
1253
  let ptcTools = [];
@@ -1197,16 +1270,16 @@ function createCodeInterpreterMiddleware(options = {}) {
1197
1270
  ...hasSchema && { [SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY]: input.responseSchema }
1198
1271
  }
1199
1272
  };
1200
- const result = await subagentTaskTool.invoke({
1273
+ const content = unwrapToolEnvelope(await subagentTaskTool.invoke({
1201
1274
  description: input.description,
1202
1275
  subagent_type: input.subagentType
1203
- }, toolConfig);
1204
- if (hasSchema && typeof result === "string") try {
1205
- return JSON.parse(result);
1276
+ }, toolConfig));
1277
+ if (hasSchema && typeof content === "string") try {
1278
+ return JSON.parse(content);
1206
1279
  } catch {
1207
- return result;
1280
+ return content;
1208
1281
  }
1209
- return result;
1282
+ return content;
1210
1283
  };
1211
1284
  }
1212
1285
  return createMiddleware({
@@ -1244,6 +1317,12 @@ function createCodeInterpreterMiddleware(options = {}) {
1244
1317
  ptcTools = filterToolsForPtc(agentTools);
1245
1318
  if (!taskTool && maxSubagentConcurrency > 0) taskTool = findTaskTool(agentTools);
1246
1319
  if (ptcTools.length > 0 && !cachedPtcPrompt) cachedPtcPrompt = await generatePtcPrompt(ptcTools);
1320
+ const baseSystemPrompt = customSystemPrompt || renderReplSystemPrompt({
1321
+ toolName,
1322
+ timeout: executionTimeoutMs / 1e3,
1323
+ memoryLimitMb: Math.floor(memoryLimitBytes / (1024 * 1024)),
1324
+ hasPtc: ptcTools.length > 0
1325
+ });
1247
1326
  const subagentPrompt = taskTool && maxSubagentConcurrency > 0 ? renderSubagentPrompt(toolName) : "";
1248
1327
  const systemMessage = request.systemMessage.concat(baseSystemPrompt).concat(subagentPrompt).concat(cachedPtcPrompt || "");
1249
1328
  return handler({