open-agents-ai 0.106.0 → 0.108.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
@@ -7557,10 +7557,54 @@ var init_repl = __esm({
7557
7557
  setLlmQueryHandler(handler) {
7558
7558
  this.llmQueryHandler = handler;
7559
7559
  }
7560
+ /** Callback for retrieving externalized handle content (RLM Context OS) */
7561
+ handleResolver = null;
7562
+ /** Set the handle resolver for retrieve() in the REPL (COHERE Layer 2) */
7563
+ setHandleResolver(resolver) {
7564
+ this.handleResolver = resolver;
7565
+ }
7560
7566
  /** Get the number of sub-calls made this session */
7561
7567
  getSubCallCount() {
7562
7568
  return this.subCallCount;
7563
7569
  }
7570
+ /**
7571
+ * Pre-load a large context string into the REPL as the `context` variable.
7572
+ * Used by prompt externalization (RLM paper §2, Principle 1: symbolic handle).
7573
+ * The context is stored as a Python string variable — the model can then
7574
+ * slice, search, and process it via repl_exec without it entering the LLM context.
7575
+ */
7576
+ async loadContext(content) {
7577
+ if (!this.proc || this.proc.killed || this.proc.exitCode !== null) {
7578
+ await this.startProcess();
7579
+ }
7580
+ const tempFile = join18(tmpdir3(), `oa-repl-ctx-${randomBytes2(6).toString("hex")}.txt`);
7581
+ const { writeFileSync: writeFs, unlinkSync: unlinkFs } = await import("node:fs");
7582
+ writeFs(tempFile, content, "utf8");
7583
+ const result = await this.executeCode(`with open(${JSON.stringify(tempFile)}, "r") as _f:
7584
+ context = _f.read()
7585
+ import os; os.unlink(${JSON.stringify(tempFile)})
7586
+ print(f"Context loaded: {len(context)} chars")`);
7587
+ try {
7588
+ unlinkFs(tempFile);
7589
+ } catch {
7590
+ }
7591
+ return result.success;
7592
+ }
7593
+ /**
7594
+ * Read a variable's value from the REPL environment.
7595
+ * Used by FINAL_VAR to extract the task output (RLM paper §2, Principle 2).
7596
+ */
7597
+ async readVariable(varName) {
7598
+ if (!this.proc || this.proc.killed || this.proc.exitCode !== null)
7599
+ return null;
7600
+ const result = await this.executeCode(`try:
7601
+ print(str(${varName}))
7602
+ except NameError:
7603
+ print("__OA_VAR_NOT_FOUND__")`);
7604
+ if (!result.success || result.output.includes("__OA_VAR_NOT_FOUND__"))
7605
+ return null;
7606
+ return result.output.trim();
7607
+ }
7564
7608
  async execute(args) {
7565
7609
  const code = String(args.code ?? "");
7566
7610
  const reset = Boolean(args.reset);
@@ -7646,6 +7690,45 @@ def llm_query(prompt, context=""):
7646
7690
  finally:
7647
7691
  sock.close()
7648
7692
 
7693
+ def retrieve(handle_id):
7694
+ """Retrieve full content of an externalized tool output by its handle ID.
7695
+ Use this to access large outputs that were stored as handles instead of
7696
+ being shown inline. Returns the full content as a string.
7697
+ Args:
7698
+ handle_id: The handle ID (e.g., 'a1b2c3d4')
7699
+ Returns: Full content string, or error message if not found
7700
+ """
7701
+ sock_path = os.environ.get("OA_LLM_QUERY_SOCKET", "")
7702
+ if not sock_path:
7703
+ return "[retrieve unavailable: no IPC socket]"
7704
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
7705
+ try:
7706
+ sock.connect(sock_path)
7707
+ request = json.dumps({"op": "retrieve", "handle_id": str(handle_id)})
7708
+ data = request.encode("utf-8")
7709
+ sock.sendall(len(data).to_bytes(4, "big") + data)
7710
+ length_bytes = b""
7711
+ while len(length_bytes) < 4:
7712
+ chunk = sock.recv(4 - len(length_bytes))
7713
+ if not chunk:
7714
+ return "[retrieve: connection closed]"
7715
+ length_bytes += chunk
7716
+ resp_len = int.from_bytes(length_bytes, "big")
7717
+ resp_data = b""
7718
+ while len(resp_data) < resp_len:
7719
+ chunk = sock.recv(min(resp_len - len(resp_data), 65536))
7720
+ if not chunk:
7721
+ break
7722
+ resp_data += chunk
7723
+ result = json.loads(resp_data.decode("utf-8"))
7724
+ if "error" in result:
7725
+ return f"[retrieve error: {result['error']}]"
7726
+ return result.get("content", "")
7727
+ except Exception as e:
7728
+ return f"[retrieve error: {e}]"
7729
+ finally:
7730
+ sock.close()
7731
+
7649
7732
  # Signal REPL ready
7650
7733
  print("__OA_REPL_READY__")
7651
7734
  `.trim();
@@ -7687,14 +7770,27 @@ print("__OA_REPL_READY__")
7687
7770
  let response;
7688
7771
  try {
7689
7772
  const req = JSON.parse(msgData);
7690
- if (!this.llmQueryHandler) {
7691
- response = { error: "llm_query handler not configured" };
7692
- } else if (this.subCallCount >= this.maxSubCalls) {
7693
- response = { error: `Sub-call limit reached (${this.maxSubCalls})` };
7773
+ if (req.op === "retrieve") {
7774
+ if (!this.handleResolver) {
7775
+ response = { error: "Handle resolver not configured" };
7776
+ } else {
7777
+ const content = this.handleResolver(req.handle_id ?? "");
7778
+ if (content !== null) {
7779
+ response = { content };
7780
+ } else {
7781
+ response = { error: `Handle '${req.handle_id}' not found` };
7782
+ }
7783
+ }
7694
7784
  } else {
7695
- this.subCallCount++;
7696
- const result = await this.llmQueryHandler(req.prompt, req.context);
7697
- response = { response: result };
7785
+ if (!this.llmQueryHandler) {
7786
+ response = { error: "llm_query handler not configured" };
7787
+ } else if (this.subCallCount >= this.maxSubCalls) {
7788
+ response = { error: `Sub-call limit reached (${this.maxSubCalls})` };
7789
+ } else {
7790
+ this.subCallCount++;
7791
+ const result = await this.llmQueryHandler(req.prompt ?? "", req.context);
7792
+ response = { response: result };
7793
+ }
7698
7794
  }
7699
7795
  } catch (err) {
7700
7796
  response = { error: err instanceof Error ? err.message : String(err) };
@@ -19313,7 +19409,8 @@ var init_agenticRunner = __esm({
19313
19409
  modelTier: options?.modelTier ?? "large",
19314
19410
  contextWindowSize: options?.contextWindowSize ?? 0,
19315
19411
  personality: options?.personality ?? PERSONALITY_PRESETS.balanced,
19316
- personalityName: options?.personalityName ?? ""
19412
+ personalityName: options?.personalityName ?? "",
19413
+ finalVarResolver: options?.finalVarResolver ?? void 0
19317
19414
  };
19318
19415
  }
19319
19416
  /** Update context window size (e.g. after querying Ollama /api/show) */
@@ -19953,8 +20050,29 @@ If you're stuck, try a completely different approach. Do NOT repeat what failed
19953
20050
  }
19954
20051
  }
19955
20052
  const { toolOutputMaxChars: maxLen } = this.contextLimits();
19956
- const output = result.success ? result.output.length > maxLen ? this.foldOutput(result.output, maxLen) : result.output : `Error: ${result.error || "unknown error"}
20053
+ let output;
20054
+ if (!result.success) {
20055
+ output = `Error: ${result.error || "unknown error"}
19957
20056
  ${result.output.length > maxLen ? this.foldOutput(result.output, maxLen) : result.output}`;
20057
+ } else if (result.output.length > maxLen) {
20058
+ const handleId = this.quickHash(tc.name + String(tc.arguments?.["path"] ?? "") + String(turn));
20059
+ const lineCount = (result.output.match(/\n/g) || []).length + 1;
20060
+ const preview = result.output.slice(0, 500).replace(/\n/g, " ");
20061
+ this._memexArchive.set(handleId, {
20062
+ id: handleId,
20063
+ toolName: tc.name,
20064
+ summary: `${tc.name} output (${lineCount} lines, ${result.output.length} chars)`,
20065
+ fullContent: result.output,
20066
+ turn,
20067
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
20068
+ });
20069
+ output = `[Output externalized \u2014 ${result.output.length} chars, ${lineCount} lines]
20070
+ Handle: ${handleId}
20071
+ Preview: ${preview}...
20072
+ Full content available via: repl_exec(code="data = retrieve('${handleId}')") or memex_retrieve(id="${handleId}")`;
20073
+ } else {
20074
+ output = result.output;
20075
+ }
19958
20076
  this.emit({
19959
20077
  type: "tool_result",
19960
20078
  toolName: tc.name,
@@ -20344,9 +20462,29 @@ Integrate this guidance into your current approach. Continue working on the task
20344
20462
  }
20345
20463
  }
20346
20464
  const { toolOutputMaxChars: maxLen2 } = this.contextLimits();
20347
- const output = result.success ? result.output.length > maxLen2 ? result.output.slice(0, maxLen2) + `
20348
- ...(truncated)` : result.output : `Error: ${result.error || "unknown error"}
20465
+ let output;
20466
+ if (!result.success) {
20467
+ output = `Error: ${result.error || "unknown error"}
20349
20468
  ${result.output}`;
20469
+ } else if (result.output.length > maxLen2) {
20470
+ const handleId = this.quickHash(tc.name + String(tc.arguments?.["path"] ?? "") + String(turn));
20471
+ const lineCount = (result.output.match(/\n/g) || []).length + 1;
20472
+ const preview = result.output.slice(0, 500).replace(/\n/g, " ");
20473
+ this._memexArchive.set(handleId, {
20474
+ id: handleId,
20475
+ toolName: tc.name,
20476
+ summary: `${tc.name} output (${lineCount} lines, ${result.output.length} chars)`,
20477
+ fullContent: result.output,
20478
+ turn,
20479
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
20480
+ });
20481
+ output = `[Output externalized \u2014 ${result.output.length} chars, ${lineCount} lines]
20482
+ Handle: ${handleId}
20483
+ Preview: ${preview}...
20484
+ Full content available via: repl_exec(code="data = retrieve('${handleId}')") or memex_retrieve(id="${handleId}")`;
20485
+ } else {
20486
+ output = result.output;
20487
+ }
20350
20488
  this.emit({ type: "tool_result", toolName: tc.name, content: output.slice(0, 200), success: result.success, turn, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
20351
20489
  const enoentTools2 = /* @__PURE__ */ new Set(["file_read", "list_directory", "find_files"]);
20352
20490
  if (!result.success && enoentTools2.has(tc.name) && /ENOENT|no such file|does not exist|not found/i.test(result.error ?? result.output)) {
@@ -20382,6 +20520,17 @@ ${result.output}`;
20382
20520
  summary = content;
20383
20521
  break;
20384
20522
  }
20523
+ const finalVarMatch = content.match(/FINAL_VAR\s*\(\s*["']?(\w+)["']?\s*\)/);
20524
+ if (finalVarMatch && this.options.finalVarResolver) {
20525
+ const varName = finalVarMatch[1];
20526
+ const varValue = await this.options.finalVarResolver(varName);
20527
+ if (varValue !== null) {
20528
+ completed = true;
20529
+ summary = varValue;
20530
+ this.emit({ type: "status", content: `FINAL_VAR(${varName}): resolved ${varValue.length} chars from REPL`, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
20531
+ break;
20532
+ }
20533
+ }
20385
20534
  if (consecutiveTextOnly >= MAX_CONSECUTIVE_TEXT_ONLY) {
20386
20535
  this.emit({
20387
20536
  type: "status",
@@ -50240,6 +50389,20 @@ ${context}` : prompt;
50240
50389
  });
50241
50390
  }
50242
50391
  }
50392
+ for (const tool of tools) {
50393
+ if ("setHandleResolver" in tool && typeof tool.setHandleResolver === "function") {
50394
+ tool.setHandleResolver((handleId) => {
50395
+ const entry = runner.getMemexEntry(handleId);
50396
+ return entry?.fullContent ?? null;
50397
+ });
50398
+ }
50399
+ }
50400
+ const replToolForFinalVar = tools.find((t) => t.name === "repl_exec");
50401
+ if (replToolForFinalVar && "readVariable" in replToolForFinalVar) {
50402
+ runner.options.finalVarResolver = async (varName) => {
50403
+ return replToolForFinalVar.readVariable(varName);
50404
+ };
50405
+ }
50243
50406
  runner.registerTools(tools);
50244
50407
  runner.registerTool({
50245
50408
  name: "memex_retrieve",
@@ -50698,7 +50861,30 @@ ${entry.fullContent}`
50698
50861
 
50699
50862
  ${emotionContext}` : `Working directory: ${repoRoot}`;
50700
50863
  resetNarrationContext();
50701
- const promise = runner.run(task, systemContext).then((result) => {
50864
+ let effectiveTask = task;
50865
+ const estimatedTaskTokens = Math.ceil(task.length / 4);
50866
+ const externalizeThreshold = contextWindowSize ? Math.floor(contextWindowSize * 0.4) : 3e4;
50867
+ if (estimatedTaskTokens > externalizeThreshold) {
50868
+ const replTool = tools.find((t) => t.name === "repl_exec");
50869
+ if (replTool && "loadContext" in replTool) {
50870
+ replTool.loadContext(task);
50871
+ const prefix = task.slice(0, 500).replace(/\n/g, " ");
50872
+ effectiveTask = `[RLM MODE \u2014 Large input externalized to REPL]
50873
+ Your input has been stored as the variable \`context\` in the persistent Python REPL.
50874
+ Context length: ${task.length} characters (~${estimatedTaskTokens} tokens)
50875
+ Prefix: "${prefix}..."
50876
+
50877
+ Use repl_exec to examine and process the context. For example:
50878
+ repl_exec(code="print(context[:1000])") # see the beginning
50879
+ repl_exec(code="print(len(context))") # check length
50880
+ repl_exec(code="chunks = context.split('\\n\\n')") # split into chunks
50881
+
50882
+ Use llm_query(prompt, chunk) inside repl_exec to analyze chunks.
50883
+ When done, either call task_complete with your answer, or use FINAL_VAR(variable_name) to return a REPL variable as the output.`;
50884
+ contentWrite(() => renderInfo(`RLM: Externalized ${task.length} chars (~${estimatedTaskTokens} tokens) to REPL context variable`));
50885
+ }
50886
+ }
50887
+ const promise = runner.run(effectiveTask, systemContext).then((result) => {
50702
50888
  const tokens = { total: result.totalTokens, estimated: result.estimatedTokens };
50703
50889
  contentWrite(() => {
50704
50890
  if (result.completed) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.106.0",
3
+ "version": "0.108.0",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -462,3 +462,28 @@ Entries in a directory listing are RELATIVE to the directory you listed.
462
462
  - If an entry is marked "d" (directory), use list_directory on it — NOT file_read
463
463
  - list_directory output includes full relative paths you can copy directly into your next tool call
464
464
  - Prefer list_directory over shell ls — it shows full relative paths you can copy directly into your next tool call
465
+
466
+ ## RLM Context Operating System
467
+
468
+ The repl_exec tool provides a persistent Python REPL where variables persist between calls. Use it for:
469
+
470
+ **Data Processing**: When you need to process, transform, or analyze data across multiple steps, use repl_exec. Variables, functions, and imports survive between calls.
471
+
472
+ **Recursive LLM Calls**: Inside the REPL, `llm_query(prompt, context="")` invokes the language model on a sub-prompt. Use it in loops to analyze chunks of large content:
473
+ ```python
474
+ # Example: analyze each file in a list
475
+ results = []
476
+ for filename in filenames:
477
+ with open(filename) as f:
478
+ content = f.read()
479
+ summary = llm_query("Summarize the key purpose of this code", content)
480
+ results.append(f"{filename}: {summary}")
481
+ ```
482
+
483
+ **Externalized Context**: When your input is very large, it may be stored as the `context` variable in the REPL. Use `print(context[:1000])` to examine it, slice it, and process chunks with llm_query().
484
+
485
+ **Handle Retrieval**: When a tool output is too large for context, it's stored as a handle. Access it via `data = retrieve('handle_id')` in the REPL.
486
+
487
+ **Output Construction**: For very long outputs, build the result as a REPL variable and return it with FINAL_VAR(variable_name) instead of autoregressive generation.
488
+
489
+ **Provenance**: Based on "Recursive Language Models" (Zhang, Kraska, Khattab — MIT CSAIL, arxiv:2512.24601) and Project COHERE Layer 2 architecture.