open-agents-ai 0.62.1 → 0.63.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.
Files changed (3) hide show
  1. package/README.md +16 -4
  2. package/dist/index.js +165 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -519,7 +519,7 @@ The agent stack includes a real-time emotion system that modulates behavior base
519
519
  - **Valence** (-1 to +1): displeasure ↔ pleasure
520
520
  - **Arousal** (0 to 1): calm ↔ energized
521
521
 
522
- Every agent event (tool success/failure, task completion, errors, context pressure) is appraised and shifts the emotional state, which decays back toward a baseline over ~60 seconds. The emotional state modulates agent behavior:
522
+ Every agent event (tool success/failure, task completion, errors, context pressure) is appraised and shifts the emotional state, which decays back toward a baseline over ~5 minutes. The emotional state modulates agent behavior across all layers: system prompt behavioral hints, voice narration tone, and decision-making style:
523
523
 
524
524
  | Quadrant | Valence | Arousal | Behavioral Effect |
525
525
  |----------|---------|---------|-------------------|
@@ -854,19 +854,31 @@ The TUI features an animated multilingual phrase carousel, live metrics bar with
854
854
 
855
855
  All settings commands accept `--local` to save to project `.oa/settings.json` instead of global config.
856
856
 
857
- ### Mid-Task Steering
857
+ ### Mid-Task Steering (Sub-Agent Architecture)
858
858
 
859
- While the agent is working (shown by the `+` prompt), type to add context:
859
+ While the agent is working (shown by the `+` prompt), type to add context. A **dedicated steering sub-agent** spins up in the background to process your input:
860
+
861
+ 1. **Immediate acknowledgment** — the steering agent speaks a brief response via TTS (e.g., "Got it, I'll adjust the approach")
862
+ 2. **Context expansion** — your terse input is expanded into a structured steering instruction grounded in the current task goal and recent agent activity
863
+ 3. **Non-blocking injection** — the expanded instruction is injected into the main agent's context at the next turn boundary, without interrupting the current tool call
860
864
 
861
865
  ```
862
866
  > fix the auth bug
863
867
  ⎿ Read: src/auth.ts
864
868
  + also check the session handling ← typed while agent works
865
- Context added: also check the session handling
869
+ 🔊 "Got it, adjusting to include session handling"
870
+ ↪ USER STEERING: Check session handling in addition to auth...
866
871
  ⎿ Search: session
867
872
  ⎿ Edit: src/auth.ts
868
873
  ```
869
874
 
875
+ The steering sub-agent uses the same model and backend as the main agent with `maxTurns: 3` and `maxTokens: 512` for fast response. If the steering agent fails, the raw input is injected as a fallback.
876
+
877
+ **Research foundations:**
878
+ - **ReAct** (Yao et al., 2023) — interleaved reasoning + acting benefits from external course corrections grounded in current state
879
+ - **LATS** (Zhou et al., 2024) — mid-execution replanning with user-provided value signals improves task completion on complex multi-step problems
880
+ - **AutoGen** (Wu et al., 2023) — human-in-the-loop patterns work best when user messages are expanded into structured instructions, reducing ambiguity for the primary agent
881
+
870
882
  ## Tools (47)
871
883
 
872
884
  | Tool | Description |
package/dist/index.js CHANGED
@@ -27076,7 +27076,52 @@ function getTier(personality) {
27076
27076
  return "conv";
27077
27077
  return "chatty";
27078
27078
  }
27079
- function describeToolCall(toolName, args, personality = 2) {
27079
+ function emotionColor(emotion) {
27080
+ if (!emotion)
27081
+ return "";
27082
+ const vDist = Math.abs(emotion.valence - 0.1);
27083
+ const aDist = Math.abs(emotion.arousal - 0.3);
27084
+ if (vDist < 0.15 && aDist < 0.15)
27085
+ return "";
27086
+ if (Math.random() > 0.3)
27087
+ return "";
27088
+ if (emotion.valence > 0.5 && emotion.arousal > 0.6) {
27089
+ return pick("emo_excited", [
27090
+ "Feeling good about this. ",
27091
+ "On a roll here. ",
27092
+ "This is going great. ",
27093
+ "Momentum building. ",
27094
+ "Loving this. "
27095
+ ]);
27096
+ }
27097
+ if (emotion.valence < -0.3 && emotion.arousal > 0.5) {
27098
+ return pick("emo_stressed", [
27099
+ "Pushing through. ",
27100
+ "Staying focused. ",
27101
+ "Not giving up. ",
27102
+ "Determined to crack this. ",
27103
+ "Bear with me. "
27104
+ ]);
27105
+ }
27106
+ if (emotion.valence > 0.3 && emotion.arousal < 0.3) {
27107
+ return pick("emo_calm", [
27108
+ "Nice and steady. ",
27109
+ "Smooth sailing. ",
27110
+ "Easy does it. ",
27111
+ "Taking it easy. "
27112
+ ]);
27113
+ }
27114
+ if (emotion.valence < -0.2 && emotion.arousal < 0.3) {
27115
+ return pick("emo_subdued", [
27116
+ "Being careful here. ",
27117
+ "Treading carefully. ",
27118
+ "Let me be precise. ",
27119
+ "Taking extra care. "
27120
+ ]);
27121
+ }
27122
+ return "";
27123
+ }
27124
+ function describeToolCall(toolName, args, personality = 2, emotion) {
27080
27125
  const path = args["path"];
27081
27126
  const file = path ? path.split("/").pop() ?? path : "";
27082
27127
  const tier = getTier(personality);
@@ -27235,13 +27280,15 @@ function describeToolCall(toolName, args, personality = 2) {
27235
27280
  break;
27236
27281
  }
27237
27282
  }
27283
+ const emoPrefix = personality >= 3 ? emotionColor(emotion) : "";
27238
27284
  let result;
27239
- if (!prefix) {
27285
+ const fullPrefix = emoPrefix + prefix;
27286
+ if (!fullPrefix) {
27240
27287
  result = base;
27241
- } else if (prefix.endsWith(". ") || prefix.endsWith("! ")) {
27242
- result = prefix + base;
27288
+ } else if (fullPrefix.endsWith(". ") || fullPrefix.endsWith("! ")) {
27289
+ result = fullPrefix + base;
27243
27290
  } else if (base.length > 0) {
27244
- result = prefix + base.charAt(0).toLowerCase() + base.slice(1);
27291
+ result = fullPrefix + base.charAt(0).toLowerCase() + base.slice(1);
27245
27292
  } else {
27246
27293
  result = base;
27247
27294
  }
@@ -27305,7 +27352,7 @@ function extractResultDigest(toolName, content) {
27305
27352
  const digest = nuggets.slice(0, 3).join(", ");
27306
27353
  return digest.length > 100 ? digest.slice(0, 97) + "..." : digest;
27307
27354
  }
27308
- function describeToolResult(toolName, success, personality = 2, resultContent) {
27355
+ function describeToolResult(toolName, success, personality = 2, resultContent, emotion) {
27309
27356
  if (toolName === "task_complete")
27310
27357
  return "";
27311
27358
  const tier = getTier(personality);
@@ -27313,24 +27360,25 @@ function describeToolResult(toolName, success, personality = 2, resultContent) {
27313
27360
  if (digest) {
27314
27361
  narration.lastResultDigest = digest;
27315
27362
  }
27363
+ const emo = personality >= 3 ? emotionColor(emotion) : "";
27316
27364
  if (success) {
27317
27365
  narration.consecutiveErrors = 0;
27318
- if (!digest && personality >= 3 && Math.random() < 0.4)
27366
+ if (!digest && !emo && personality >= 3 && Math.random() < 0.4)
27319
27367
  return "";
27320
27368
  const base = pick(`result_ok_${tier}`, RESULT_SUCCESS_VARIANTS[tier] ?? RESULT_SUCCESS_VARIANTS.terse);
27321
27369
  if (digest && personality >= 2) {
27322
27370
  const connectors = tier === "chatty" ? [` \u2014 ${digest}`, `, I see ${digest}`, `. Looks like ${digest}`, `, showing ${digest}`] : tier === "conv" ? [` \u2014 ${digest}`, `, ${digest}`, `. Shows ${digest}`] : [`: ${digest}`];
27323
- return base + pick("digest_conn_ok", connectors);
27371
+ return emo + base + pick("digest_conn_ok", connectors);
27324
27372
  }
27325
- return base;
27373
+ return emo + base;
27326
27374
  }
27327
27375
  narration.consecutiveErrors++;
27328
27376
  narration.totalErrors++;
27329
27377
  const failBase = narration.consecutiveErrors >= 3 ? pick(`result_multifail_${tier}`, RESULT_MULTI_FAIL_VARIANTS[tier] ?? RESULT_MULTI_FAIL_VARIANTS.terse) : pick(`result_fail_${tier}`, RESULT_FAIL_VARIANTS[tier] ?? RESULT_FAIL_VARIANTS.terse);
27330
27378
  if (digest && personality >= 2) {
27331
- return `${failBase} \u2014 ${digest}`;
27379
+ return emo + `${failBase} \u2014 ${digest}`;
27332
27380
  }
27333
- return failBase;
27381
+ return emo + failBase;
27334
27382
  }
27335
27383
  function describeTaskComplete(summary, completed, personality = 2) {
27336
27384
  const truncated = summary.length > 300 ? summary.slice(0, 300) + "..." : summary;
@@ -31523,22 +31571,22 @@ function appraiseEvent(event) {
31523
31571
  switch (event.type) {
31524
31572
  case "tool_result":
31525
31573
  if (event.success) {
31526
- return { valence: 0.08, arousal: 0.05 };
31574
+ return { valence: 0.12, arousal: 0.06 };
31527
31575
  } else {
31528
- return { valence: -0.15, arousal: 0.12 };
31576
+ return { valence: -0.2, arousal: 0.15 };
31529
31577
  }
31530
31578
  case "complete":
31531
- return { valence: 0.3, arousal: 0.25 };
31579
+ return { valence: 0.4, arousal: 0.3 };
31532
31580
  case "error":
31533
- return { valence: -0.25, arousal: 0.2 };
31581
+ return { valence: -0.35, arousal: 0.25 };
31534
31582
  case "compaction":
31535
- return { valence: -0.05, arousal: 0.08 };
31583
+ return { valence: -0.1, arousal: 0.12 };
31536
31584
  case "tool_call":
31537
- return { valence: 0.02, arousal: 0.03 };
31585
+ return { valence: 0.03, arousal: 0.05 };
31538
31586
  case "stream_start":
31539
- return { valence: 0.03, arousal: 0.04 };
31587
+ return { valence: 0.05, arousal: 0.06 };
31540
31588
  case "user_interrupt":
31541
- return { valence: -0.08, arousal: 0.06 };
31589
+ return { valence: -0.1, arousal: 0.1 };
31542
31590
  default:
31543
31591
  return null;
31544
31592
  }
@@ -31553,12 +31601,12 @@ var init_emotion_engine = __esm({
31553
31601
  init_dist5();
31554
31602
  BASELINE_VALENCE = 0.1;
31555
31603
  BASELINE_AROUSAL = 0.3;
31556
- DECAY_HALF_LIFE_MS = 6e4;
31557
- LABEL_UPDATE_INTERVAL_MS = 3e4;
31604
+ DECAY_HALF_LIFE_MS = 3e5;
31605
+ LABEL_UPDATE_INTERVAL_MS = 15e3;
31558
31606
  EXCITEMENT_THRESHOLD = 0.85;
31559
31607
  DISTRESS_THRESHOLD = -0.7;
31560
31608
  OUTREACH_COOLDOWN_MS = 3e5;
31561
- LABEL_REGEN_THRESHOLD = 0.15;
31609
+ LABEL_REGEN_THRESHOLD = 0.06;
31562
31610
  EmotionEngine = class {
31563
31611
  state = {
31564
31612
  valence: BASELINE_VALENCE,
@@ -31572,6 +31620,9 @@ var init_emotion_engine = __esm({
31572
31620
  lastLabelUpdate = 0;
31573
31621
  lastOutreach = 0;
31574
31622
  labelUpdatePending = false;
31623
+ /** Valence/arousal snapshot at last label regen — for change detection */
31624
+ lastLabelValence = BASELINE_VALENCE;
31625
+ lastLabelArousal = BASELINE_AROUSAL;
31575
31626
  /** Running counters for context-aware appraisal */
31576
31627
  consecutiveFailures = 0;
31577
31628
  consecutiveSuccesses = 0;
@@ -31623,22 +31674,21 @@ ${behavioralHint}`;
31623
31674
  }
31624
31675
  }
31625
31676
  let momentum = 1;
31626
- if (this.consecutiveSuccesses >= 3) {
31627
- momentum = 1 + (this.consecutiveSuccesses - 2) * 0.15;
31677
+ if (this.consecutiveSuccesses >= 2) {
31678
+ momentum = 1 + (this.consecutiveSuccesses - 1) * 0.2;
31628
31679
  }
31629
31680
  if (this.consecutiveFailures >= 2) {
31630
- momentum = 1 + (this.consecutiveFailures - 1) * 0.2;
31681
+ momentum = 1 + (this.consecutiveFailures - 1) * 0.25;
31631
31682
  }
31632
- const prevValence = this.state.valence;
31633
- const prevArousal = this.state.arousal;
31634
31683
  this.state.valence = clamp(this.state.valence + delta.valence * momentum, -1, 1);
31635
31684
  this.state.arousal = clamp(this.state.arousal + delta.arousal * momentum, 0, 1);
31636
31685
  this.state.updatedAt = Date.now();
31637
- const valenceChange = Math.abs(this.state.valence - prevValence);
31638
- const arousalChange = Math.abs(this.state.arousal - prevArousal);
31639
- const significantChange = valenceChange > LABEL_REGEN_THRESHOLD || arousalChange > LABEL_REGEN_THRESHOLD;
31686
+ const valenceShift = Math.abs(this.state.valence - this.lastLabelValence);
31687
+ const arousalShift = Math.abs(this.state.arousal - this.lastLabelArousal);
31688
+ const significantDrift = valenceShift > LABEL_REGEN_THRESHOLD || arousalShift > LABEL_REGEN_THRESHOLD;
31640
31689
  const now = Date.now();
31641
- if (significantChange && now - this.lastLabelUpdate > LABEL_UPDATE_INTERVAL_MS && !this.labelUpdatePending) {
31690
+ const cooldownElapsed = now - this.lastLabelUpdate > LABEL_UPDATE_INTERVAL_MS;
31691
+ if (cooldownElapsed && !this.labelUpdatePending && significantDrift) {
31642
31692
  this.regenerateLabel();
31643
31693
  }
31644
31694
  this.config.onEmotionUpdate?.(this.getState());
@@ -31732,6 +31782,8 @@ Example: \u{1F30A} flowing`;
31732
31782
  this.state.label = wordMatch[0].toLowerCase();
31733
31783
  }
31734
31784
  this.lastLabelUpdate = Date.now();
31785
+ this.lastLabelValence = this.state.valence;
31786
+ this.lastLabelArousal = this.state.arousal;
31735
31787
  this.config.onEmotionUpdate?.(this.getState());
31736
31788
  } catch {
31737
31789
  } finally {
@@ -34581,7 +34633,9 @@ ${entry.fullContent}`
34581
34633
  statusBar?.setActiveTool(event.toolName ?? null);
34582
34634
  contentWrite(() => {
34583
34635
  if (voice?.enabled) {
34584
- const desc = describeToolCall(event.toolName ?? "unknown", event.toolArgs ?? {}, vLevel);
34636
+ const emoState = emotionEngine?.getState();
34637
+ const emoCtx = emoState ? { valence: emoState.valence, arousal: emoState.arousal, label: emoState.label, emoji: emoState.emoji } : void 0;
34638
+ const desc = describeToolCall(event.toolName ?? "unknown", event.toolArgs ?? {}, vLevel, emoCtx);
34585
34639
  renderVoiceText(desc);
34586
34640
  voice.speak(desc);
34587
34641
  }
@@ -34616,7 +34670,9 @@ ${entry.fullContent}`
34616
34670
  renderVerbose(`${event.toolName ?? "unknown"}: ${durStr}${sizeStr}`);
34617
34671
  }
34618
34672
  if (voice?.enabled) {
34619
- const desc = describeToolResult(event.toolName ?? "unknown", event.success ?? false, vLevel, event.content ?? void 0);
34673
+ const emoState2 = emotionEngine?.getState();
34674
+ const emoCtx2 = emoState2 ? { valence: emoState2.valence, arousal: emoState2.arousal, label: emoState2.label, emoji: emoState2.emoji } : void 0;
34675
+ const desc = describeToolResult(event.toolName ?? "unknown", event.success ?? false, vLevel, event.content ?? void 0, emoCtx2);
34620
34676
  if (desc) {
34621
34677
  renderVoiceText(desc);
34622
34678
  voice.speak(desc);
@@ -36190,13 +36246,88 @@ ${result.text}`;
36190
36246
  writeContent(() => renderUserInterrupt(`[Media: ${cleanPath}]`));
36191
36247
  }
36192
36248
  } else {
36193
- activeTask.runner.injectUserMessage(input);
36194
36249
  const lineCount = input.split("\n").length;
36195
36250
  if (lineCount > 1) {
36196
36251
  writeContent(() => renderUserInterrupt(`[pasted ${lineCount} lines]`));
36197
36252
  } else {
36198
36253
  writeContent(() => renderUserInterrupt(input));
36199
36254
  }
36255
+ const steerRunner = activeTask.runner;
36256
+ const steerTaskGoal = lastSubmittedPrompt;
36257
+ const steerFeed = getActivityFeed();
36258
+ (async () => {
36259
+ try {
36260
+ const steerBackend = new OllamaAgenticBackend(currentConfig.backendUrl, currentConfig.model, currentConfig.apiKey);
36261
+ const steerAgent = new AgenticRunner(steerBackend, {
36262
+ maxTurns: 3,
36263
+ maxTokens: 512,
36264
+ temperature: 0.3,
36265
+ requestTimeoutMs: 15e3,
36266
+ taskTimeoutMs: 3e4,
36267
+ streamEnabled: false
36268
+ });
36269
+ steerAgent.setWorkingDirectory(repoRoot);
36270
+ steerAgent.registerTool({
36271
+ name: "task_complete",
36272
+ description: "Return the steering instruction for the main agent.",
36273
+ parameters: {
36274
+ type: "object",
36275
+ properties: {
36276
+ summary: {
36277
+ type: "string",
36278
+ description: "The expanded steering instruction to inject into the main agent's context."
36279
+ },
36280
+ acknowledgment: {
36281
+ type: "string",
36282
+ description: "A brief (1 sentence) spoken acknowledgment to the user. Will be spoken aloud via TTS."
36283
+ }
36284
+ },
36285
+ required: ["summary", "acknowledgment"]
36286
+ },
36287
+ async execute(args) {
36288
+ return { success: true, output: JSON.stringify(args) };
36289
+ }
36290
+ });
36291
+ const recentActivity = steerFeed.getSummary(10, false);
36292
+ const steerPrompt = [
36293
+ `The user typed a mid-task message while the agent is working. Your job:`,
36294
+ `1. Understand what the user wants changed/added/corrected`,
36295
+ `2. Produce a brief spoken acknowledgment (1 sentence, conversational)`,
36296
+ `3. Expand their input into a clear, structured steering instruction for the main agent`,
36297
+ ``,
36298
+ `Current task goal: "${steerTaskGoal.slice(0, 500)}"`,
36299
+ ``,
36300
+ `Recent agent activity:`,
36301
+ recentActivity,
36302
+ ``,
36303
+ `User's mid-task message: "${input}"`,
36304
+ ``,
36305
+ `Call task_complete with:`,
36306
+ `- acknowledgment: brief spoken response to the user (e.g. "Got it, I'll adjust the approach")`,
36307
+ `- summary: expanded instruction for the main agent (e.g. "USER STEERING: The user wants X instead of Y. Adjust your approach to prioritize Z. Specifically, they are asking you to...")`
36308
+ ].join("\n");
36309
+ const result = await steerAgent.run(steerPrompt, "Steering sub-agent \u2014 interpret user input and produce instruction.");
36310
+ let acknowledgment = "Got it, adjusting.";
36311
+ let steering = `USER STEERING: ${input}`;
36312
+ try {
36313
+ const parsed = JSON.parse(result.summary || "{}");
36314
+ if (parsed.acknowledgment)
36315
+ acknowledgment = parsed.acknowledgment;
36316
+ if (parsed.summary)
36317
+ steering = parsed.summary;
36318
+ } catch {
36319
+ if (result.summary)
36320
+ steering = `USER STEERING: ${result.summary}`;
36321
+ }
36322
+ if (voiceEngine?.enabled) {
36323
+ writeContent(() => renderVoiceText(acknowledgment));
36324
+ voiceEngine.speak(acknowledgment);
36325
+ }
36326
+ steerRunner.injectUserMessage(steering);
36327
+ } catch {
36328
+ steerRunner.injectUserMessage(input);
36329
+ }
36330
+ })();
36200
36331
  }
36201
36332
  showPrompt();
36202
36333
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.62.1",
3
+ "version": "0.63.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",