open-agents-ai 0.60.0 → 0.61.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 (2) hide show
  1. package/dist/index.js +444 -39
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -19081,7 +19081,7 @@ var require_extension = __commonJS({
19081
19081
  var require_websocket = __commonJS({
19082
19082
  "node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/websocket.js"(exports, module) {
19083
19083
  "use strict";
19084
- var EventEmitter3 = __require("events");
19084
+ var EventEmitter4 = __require("events");
19085
19085
  var https = __require("https");
19086
19086
  var http = __require("http");
19087
19087
  var net = __require("net");
@@ -19113,7 +19113,7 @@ var require_websocket = __commonJS({
19113
19113
  var protocolVersions = [8, 13];
19114
19114
  var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
19115
19115
  var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
19116
- var WebSocket2 = class _WebSocket extends EventEmitter3 {
19116
+ var WebSocket2 = class _WebSocket extends EventEmitter4 {
19117
19117
  /**
19118
19118
  * Create a new `WebSocket`.
19119
19119
  *
@@ -20110,7 +20110,7 @@ var require_subprotocol = __commonJS({
20110
20110
  var require_websocket_server = __commonJS({
20111
20111
  "node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/websocket-server.js"(exports, module) {
20112
20112
  "use strict";
20113
- var EventEmitter3 = __require("events");
20113
+ var EventEmitter4 = __require("events");
20114
20114
  var http = __require("http");
20115
20115
  var { Duplex } = __require("stream");
20116
20116
  var { createHash: createHash2 } = __require("crypto");
@@ -20123,7 +20123,7 @@ var require_websocket_server = __commonJS({
20123
20123
  var RUNNING = 0;
20124
20124
  var CLOSING = 1;
20125
20125
  var CLOSED = 2;
20126
- var WebSocketServer2 = class extends EventEmitter3 {
20126
+ var WebSocketServer2 = class extends EventEmitter4 {
20127
20127
  /**
20128
20128
  * Create a `WebSocketServer` instance.
20129
20129
  *
@@ -21604,7 +21604,10 @@ function connect() {
21604
21604
  ws = null;
21605
21605
  }
21606
21606
  const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
21607
- ws = new WebSocket(proto + '//' + location.host + '/ws');
21607
+ const urlParams = new URLSearchParams(location.search);
21608
+ const sessionKey = urlParams.get('key') || '';
21609
+ const keyParam = sessionKey ? '?key=' + encodeURIComponent(sessionKey) : '';
21610
+ ws = new WebSocket(proto + '//' + location.host + '/ws' + keyParam);
21608
21611
  ws.binaryType = 'arraybuffer';
21609
21612
 
21610
21613
  ws.onopen = () => {
@@ -21903,6 +21906,47 @@ var init_voice_session = __esm({
21903
21906
  }
21904
21907
  }
21905
21908
  }
21909
+ /**
21910
+ * Send TTS audio to a specific client by clientId.
21911
+ */
21912
+ sendAudioToClient(clientId, pcmInt16) {
21913
+ const ws = this.wsClients.get(clientId);
21914
+ if (ws) {
21915
+ try {
21916
+ if (ws.readyState === import_websocket.default.OPEN)
21917
+ ws.send(pcmInt16);
21918
+ } catch {
21919
+ }
21920
+ }
21921
+ }
21922
+ /**
21923
+ * Send speaking state change to a specific client.
21924
+ */
21925
+ sendSpeakingStateToClient(clientId, speaking) {
21926
+ const ws = this.wsClients.get(clientId);
21927
+ if (ws) {
21928
+ const msg = JSON.stringify({ type: speaking ? "speaking_start" : "speaking_end" });
21929
+ try {
21930
+ if (ws.readyState === import_websocket.default.OPEN)
21931
+ ws.send(msg);
21932
+ } catch {
21933
+ }
21934
+ }
21935
+ }
21936
+ /**
21937
+ * Send a transcript message to a specific client.
21938
+ */
21939
+ sendTranscriptToClient(clientId, speaker, text) {
21940
+ const ws = this.wsClients.get(clientId);
21941
+ if (ws) {
21942
+ const msg = JSON.stringify({ type: "transcript", speaker, text });
21943
+ try {
21944
+ if (ws.readyState === import_websocket.default.OPEN)
21945
+ ws.send(msg);
21946
+ } catch {
21947
+ }
21948
+ }
21949
+ }
21906
21950
  /**
21907
21951
  * Signal TTS speaking state to clients.
21908
21952
  */
@@ -21951,9 +21995,11 @@ var init_voice_session = __esm({
21951
21995
  // ── WebSocket connection handler (using ws package) ──────────────────
21952
21996
  handleWSConnection(ws, req) {
21953
21997
  const clientId = `ws-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
21998
+ const url = new URL(req.url ?? "/", `http://localhost`);
21999
+ const sessionKey = url.searchParams.get("key") ?? null;
21954
22000
  this.wsClients.set(clientId, ws);
21955
22001
  this.state.connectedUsers.set(clientId, { username: "web-user", connectedAt: Date.now() });
21956
- this.emit("userConnected", clientId, "web-user");
22002
+ this.emit("userConnected", clientId, "web-user", sessionKey);
21957
22003
  this.resetIdleTimer();
21958
22004
  const keepaliveInterval = setInterval(() => {
21959
22005
  if (ws.readyState !== import_websocket.default.OPEN) {
@@ -22079,6 +22125,279 @@ var init_voice_session = __esm({
22079
22125
  }
22080
22126
  });
22081
22127
 
22128
+ // packages/cli/dist/tui/call-agent.js
22129
+ import { EventEmitter as EventEmitter3 } from "node:events";
22130
+ import crypto2 from "node:crypto";
22131
+ function adaptTool(tool) {
22132
+ return {
22133
+ name: tool.name,
22134
+ description: tool.description,
22135
+ parameters: tool.parameters,
22136
+ async execute(args) {
22137
+ const result = await tool.execute(args);
22138
+ return { success: result.success, output: result.output, error: result.error };
22139
+ }
22140
+ };
22141
+ }
22142
+ function getActivityFeed() {
22143
+ if (!_globalFeed)
22144
+ _globalFeed = new ActivityFeed();
22145
+ return _globalFeed;
22146
+ }
22147
+ function generateSessionKey() {
22148
+ return crypto2.randomBytes(16).toString("hex");
22149
+ }
22150
+ var ActivityFeed, _globalFeed, CallSubAgent;
22151
+ var init_call_agent = __esm({
22152
+ "packages/cli/dist/tui/call-agent.js"() {
22153
+ "use strict";
22154
+ init_dist5();
22155
+ init_dist2();
22156
+ ActivityFeed = class {
22157
+ entries = [];
22158
+ maxEntries = 100;
22159
+ push(entry) {
22160
+ this.entries.push(entry);
22161
+ if (this.entries.length > this.maxEntries) {
22162
+ this.entries = this.entries.slice(-this.maxEntries);
22163
+ }
22164
+ }
22165
+ /** Get recent activity as a text summary for sub-agent context */
22166
+ getSummary(maxItems = 20, verbose = false) {
22167
+ const recent = this.entries.slice(-maxItems);
22168
+ if (recent.length === 0)
22169
+ return "No recent activity.";
22170
+ return recent.map((e) => {
22171
+ const src = e.source === "main" ? "Agent" : "Call";
22172
+ const tool = e.toolName ? ` [${e.toolName}]` : "";
22173
+ const status = e.success !== void 0 ? e.success ? " ok" : " fail" : "";
22174
+ if (verbose) {
22175
+ return `[${new Date(e.ts).toLocaleTimeString()}] ${src}${tool}${status}: ${e.summary}`;
22176
+ }
22177
+ return `${src}${tool}${status}: ${e.summary}`;
22178
+ }).join("\n");
22179
+ }
22180
+ /** Get entries since a timestamp */
22181
+ since(ts) {
22182
+ return this.entries.filter((e) => e.ts > ts);
22183
+ }
22184
+ clear() {
22185
+ this.entries.length = 0;
22186
+ }
22187
+ };
22188
+ _globalFeed = null;
22189
+ CallSubAgent = class extends EventEmitter3 {
22190
+ tier;
22191
+ clientId;
22192
+ runner = null;
22193
+ config;
22194
+ repoRoot;
22195
+ opts;
22196
+ processing = false;
22197
+ pendingTranscripts = [];
22198
+ conversationHistory = [];
22199
+ disposed = false;
22200
+ constructor(clientId, opts) {
22201
+ super();
22202
+ this.clientId = clientId;
22203
+ this.tier = opts.tier;
22204
+ this.config = opts.config;
22205
+ this.repoRoot = opts.repoRoot;
22206
+ this.opts = opts;
22207
+ }
22208
+ /** Initialize the runner with appropriate tools for the access tier */
22209
+ async init() {
22210
+ const backend = new OllamaAgenticBackend(this.config.backendUrl, this.config.model, this.config.apiKey);
22211
+ const feed = getActivityFeed();
22212
+ const systemPrompt = this.buildSystemPrompt();
22213
+ const runnerOpts = {
22214
+ maxTurns: this.tier === "admin" ? 15 : 5,
22215
+ maxTokens: 4096,
22216
+ temperature: 0.3,
22217
+ requestTimeoutMs: 3e4,
22218
+ taskTimeoutMs: 12e4,
22219
+ modelTier: this.opts.modelTier ?? "large",
22220
+ contextWindowSize: this.opts.contextWindowSize,
22221
+ personality: this.opts.personality,
22222
+ streamEnabled: false,
22223
+ bruteForce: false,
22224
+ dynamicContext: systemPrompt
22225
+ };
22226
+ this.runner = new AgenticRunner(backend, runnerOpts);
22227
+ this.runner.setWorkingDirectory(this.repoRoot);
22228
+ const tools = this.buildTools();
22229
+ this.runner.registerTools(tools);
22230
+ const clientId = this.clientId;
22231
+ const self = this;
22232
+ this.runner.registerTool({
22233
+ name: "task_complete",
22234
+ description: "Call this when you have fully answered the user's question or completed their request. The summary should be your spoken response to the user.",
22235
+ parameters: {
22236
+ type: "object",
22237
+ properties: {
22238
+ summary: {
22239
+ type: "string",
22240
+ description: "Your spoken response to the user. Keep it concise and conversational \u2014 this will be spoken aloud via TTS."
22241
+ }
22242
+ },
22243
+ required: ["summary"]
22244
+ },
22245
+ execute: async (args) => {
22246
+ const summary = String(args["summary"] ?? "");
22247
+ self.emit("response", summary);
22248
+ feed.push({
22249
+ ts: Date.now(),
22250
+ source: "call",
22251
+ sourceId: clientId,
22252
+ summary: `Responded: ${summary.slice(0, 100)}`
22253
+ });
22254
+ return { success: true, output: summary };
22255
+ }
22256
+ });
22257
+ this.runner.onEvent((event) => {
22258
+ if (event.type === "tool_call") {
22259
+ const toolName = event.toolName ?? "unknown";
22260
+ const args = event.toolArgs ?? {};
22261
+ this.emit("toolCall", toolName, args);
22262
+ feed.push({
22263
+ ts: Date.now(),
22264
+ source: "call",
22265
+ sourceId: this.clientId,
22266
+ summary: `${toolName}(${Object.keys(args).join(", ")})`,
22267
+ toolName
22268
+ });
22269
+ }
22270
+ if (event.type === "tool_result") {
22271
+ const toolName = event.toolName ?? "unknown";
22272
+ const success = event.success ?? false;
22273
+ const content = String(event.content ?? "").slice(0, 100);
22274
+ this.emit("toolResult", toolName, success, content);
22275
+ feed.push({
22276
+ ts: Date.now(),
22277
+ source: "call",
22278
+ sourceId: this.clientId,
22279
+ summary: content,
22280
+ toolName,
22281
+ success
22282
+ });
22283
+ }
22284
+ if (event.type === "model_response" && event.content) {
22285
+ this.emit("response", event.content);
22286
+ }
22287
+ });
22288
+ }
22289
+ /** Process a voice transcript — queues if already processing */
22290
+ handleTranscript(text) {
22291
+ if (this.disposed)
22292
+ return;
22293
+ this.conversationHistory.push({ role: "user", text });
22294
+ if (this.processing) {
22295
+ this.pendingTranscripts.push(text);
22296
+ return;
22297
+ }
22298
+ this.processTranscript(text).catch((err) => {
22299
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
22300
+ });
22301
+ }
22302
+ /** Dispose and clean up */
22303
+ dispose() {
22304
+ this.disposed = true;
22305
+ this.pendingTranscripts.length = 0;
22306
+ this.runner = null;
22307
+ }
22308
+ // ── Private ──────────────────────────────────────────────────────────
22309
+ async processTranscript(text) {
22310
+ if (!this.runner || this.disposed)
22311
+ return;
22312
+ this.processing = true;
22313
+ try {
22314
+ const historyContext = this.conversationHistory.slice(-10).map((h) => `${h.role === "user" ? "User" : "You"}: ${h.text}`).join("\n");
22315
+ const feed = getActivityFeed();
22316
+ const activitySummary = feed.getSummary(this.tier === "admin" ? 20 : 10, this.tier === "admin");
22317
+ const taskPrompt = [
22318
+ `The user said (via voice): "${text}"`,
22319
+ "",
22320
+ "Recent conversation:",
22321
+ historyContext,
22322
+ "",
22323
+ "Recent agent activity:",
22324
+ activitySummary,
22325
+ "",
22326
+ "Respond conversationally and concisely. Your response will be spoken aloud via TTS.",
22327
+ "Call task_complete with your spoken response as the summary."
22328
+ ].join("\n");
22329
+ const result = await this.runner.run(taskPrompt, `Working directory: ${this.repoRoot}`);
22330
+ if (result.summary) {
22331
+ this.conversationHistory.push({ role: "assistant", text: result.summary });
22332
+ }
22333
+ } catch (err) {
22334
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
22335
+ } finally {
22336
+ this.processing = false;
22337
+ this.emit("done");
22338
+ if (this.pendingTranscripts.length > 0) {
22339
+ const next = this.pendingTranscripts.shift();
22340
+ this.processTranscript(next).catch((err) => {
22341
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
22342
+ });
22343
+ }
22344
+ }
22345
+ }
22346
+ buildSystemPrompt() {
22347
+ const base = [
22348
+ "You are a voice assistant for an AI coding agent. Users are speaking to you through a live audio call.",
22349
+ "Keep responses SHORT and conversational \u2014 they will be spoken aloud via text-to-speech.",
22350
+ "Avoid code blocks, markdown formatting, or long technical explanations in your spoken responses.",
22351
+ "If the user asks about what the agent is doing, summarize the recent activity concisely."
22352
+ ];
22353
+ if (this.opts.emotionContext) {
22354
+ base.push("", "Current emotional context:", this.opts.emotionContext);
22355
+ }
22356
+ if (this.tier === "admin") {
22357
+ base.push("", "This is an ADMIN call \u2014 you have full access to all tools and can execute commands, read/write files, search code, etc.", "You can take actions on behalf of the user just like the main agent.");
22358
+ } else {
22359
+ base.push("", "This is a PUBLIC call \u2014 you have read-only access. You can read files and search code but cannot modify anything.", "You can answer questions about the project and explain what the agent is working on.", "Be helpful and friendly but do not execute any write operations.");
22360
+ }
22361
+ return base.join("\n");
22362
+ }
22363
+ buildTools() {
22364
+ if (this.tier === "admin") {
22365
+ return this.buildAdminTools();
22366
+ }
22367
+ return this.buildPublicTools();
22368
+ }
22369
+ buildAdminTools() {
22370
+ const tools = [
22371
+ new FileReadTool(this.repoRoot),
22372
+ new FileWriteTool(this.repoRoot),
22373
+ new FileEditTool(this.repoRoot),
22374
+ new ShellTool(this.repoRoot),
22375
+ new GrepSearchTool(this.repoRoot),
22376
+ new GlobFindTool(this.repoRoot),
22377
+ new ListDirectoryTool(this.repoRoot),
22378
+ new WebSearchTool(),
22379
+ new WebFetchTool(),
22380
+ new MemoryReadTool(this.repoRoot),
22381
+ new MemoryWriteTool(this.repoRoot),
22382
+ new MemorySearchTool(this.repoRoot)
22383
+ ];
22384
+ return tools.map(adaptTool);
22385
+ }
22386
+ buildPublicTools() {
22387
+ const tools = [
22388
+ new FileReadTool(this.repoRoot),
22389
+ new GrepSearchTool(this.repoRoot),
22390
+ new GlobFindTool(this.repoRoot),
22391
+ new ListDirectoryTool(this.repoRoot),
22392
+ new MemoryReadTool(this.repoRoot),
22393
+ new MemorySearchTool(this.repoRoot)
22394
+ ];
22395
+ return tools.map(adaptTool);
22396
+ }
22397
+ };
22398
+ }
22399
+ });
22400
+
22082
22401
  // packages/cli/dist/tui/model-picker.js
22083
22402
  async function fetchOllamaModels(baseUrl) {
22084
22403
  const url = `${normalizeBaseUrl(baseUrl)}/api/tags`;
@@ -28517,7 +28836,7 @@ ${sections.join("\n\n")}`;
28517
28836
  return "";
28518
28837
  }
28519
28838
  }
28520
- function adaptTool(tool) {
28839
+ function adaptTool2(tool) {
28521
28840
  return {
28522
28841
  name: tool.name,
28523
28842
  description: tool.description,
@@ -29219,7 +29538,7 @@ After synthesis, call task_complete with the final prioritized summary.`, toolMo
29219
29538
  new MemoryReadTool(this.repoRoot),
29220
29539
  new MemorySearchTool(this.repoRoot)
29221
29540
  ];
29222
- return [...tools.map(adaptTool), taskComplete];
29541
+ return [...tools.map(adaptTool2), taskComplete];
29223
29542
  }
29224
29543
  case "monitor": {
29225
29544
  const tools = [
@@ -29229,7 +29548,7 @@ After synthesis, call task_complete with the final prioritized summary.`, toolMo
29229
29548
  new AutoresearchTool(this.repoRoot)
29230
29549
  // status-only in prompt
29231
29550
  ];
29232
- return [...tools.map(adaptTool), taskComplete];
29551
+ return [...tools.map(adaptTool2), taskComplete];
29233
29552
  }
29234
29553
  case "evaluator": {
29235
29554
  const tools = [
@@ -29241,7 +29560,7 @@ After synthesis, call task_complete with the final prioritized summary.`, toolMo
29241
29560
  new MemoryWriteTool(this.repoRoot),
29242
29561
  new GrepSearchTool(this.repoRoot)
29243
29562
  ];
29244
- return [...tools.map(adaptTool), taskComplete];
29563
+ return [...tools.map(adaptTool2), taskComplete];
29245
29564
  }
29246
29565
  case "critic": {
29247
29566
  const tools = [
@@ -29250,7 +29569,7 @@ After synthesis, call task_complete with the final prioritized summary.`, toolMo
29250
29569
  new MemorySearchTool(this.repoRoot),
29251
29570
  new GrepSearchTool(this.repoRoot)
29252
29571
  ];
29253
- return [...tools.map(adaptTool), taskComplete];
29572
+ return [...tools.map(adaptTool2), taskComplete];
29254
29573
  }
29255
29574
  case "flow_maintainer": {
29256
29575
  const tools = [
@@ -29258,7 +29577,7 @@ After synthesis, call task_complete with the final prioritized summary.`, toolMo
29258
29577
  new MemoryWriteTool(this.repoRoot),
29259
29578
  new MemorySearchTool(this.repoRoot)
29260
29579
  ];
29261
- return [...tools.map(adaptTool), taskComplete];
29580
+ return [...tools.map(adaptTool2), taskComplete];
29262
29581
  }
29263
29582
  }
29264
29583
  }
@@ -29616,7 +29935,7 @@ ${summaryResult}
29616
29935
  new WebSearchTool()
29617
29936
  ];
29618
29937
  return [
29619
- ...tools.map(adaptTool),
29938
+ ...tools.map(adaptTool2),
29620
29939
  this.createTaskCompleteTool()
29621
29940
  ];
29622
29941
  }
@@ -29635,8 +29954,8 @@ ${summaryResult}
29635
29954
  new DreamShellTool(this.repoRoot)
29636
29955
  ];
29637
29956
  return [
29638
- ...readTools.map(adaptTool),
29639
- ...dreamWriteTools.map(adaptTool),
29957
+ ...readTools.map(adaptTool2),
29958
+ ...dreamWriteTools.map(adaptTool2),
29640
29959
  this.createTaskCompleteTool()
29641
29960
  ];
29642
29961
  }
@@ -30044,7 +30363,7 @@ Write consolidation insights and new reflections to memory before selecting a ta
30044
30363
  The next DMN cycle (and the main agent) will benefit from anything you store now.
30045
30364
  `;
30046
30365
  }
30047
- function adaptTool2(tool) {
30366
+ function adaptTool3(tool) {
30048
30367
  return {
30049
30368
  name: tool.name,
30050
30369
  description: tool.description,
@@ -30312,7 +30631,7 @@ DMN state directory: ${this.stateDir}`);
30312
30631
  new WebSearchTool()
30313
30632
  ];
30314
30633
  return [
30315
- ...tools.map(adaptTool2),
30634
+ ...tools.map(adaptTool3),
30316
30635
  this.createTaskCompleteTool()
30317
30636
  ];
30318
30637
  }
@@ -30587,7 +30906,7 @@ OUTPUT: Call task_complete with JSON:
30587
30906
  tools.push(new MemoryWriteTool(this.repoRoot));
30588
30907
  }
30589
30908
  runner.registerTools([
30590
- ...tools.map(adaptTool2),
30909
+ ...tools.map(adaptTool3),
30591
30910
  {
30592
30911
  name: "task_complete",
30593
30912
  description: `Signal that the ${role} analysis is complete.`,
@@ -30831,7 +31150,7 @@ function computeSparsity(entries) {
30831
31150
  const avgOverlap = totalJaccard / totalPairs;
30832
31151
  return Math.max(0, Math.min(1, 1 - avgOverlap));
30833
31152
  }
30834
- function adaptTool3(tool) {
31153
+ function adaptTool4(tool) {
30835
31154
  return {
30836
31155
  name: tool.name,
30837
31156
  description: tool.description,
@@ -31008,7 +31327,7 @@ Call task_complete with the JSON array when done.`, onEvent)
31008
31327
  new MemorySearchTool(this.repoRoot)
31009
31328
  ];
31010
31329
  runner.registerTools([
31011
- ...tools.map(adaptTool3),
31330
+ ...tools.map(adaptTool4),
31012
31331
  {
31013
31332
  name: "task_complete",
31014
31333
  description: "Signal evaluation is complete with your scored results.",
@@ -31498,7 +31817,7 @@ function formatIntermediateState(event) {
31498
31817
  }
31499
31818
  return null;
31500
31819
  }
31501
- function adaptTool4(tool) {
31820
+ function adaptTool5(tool) {
31502
31821
  return {
31503
31822
  name: tool.name,
31504
31823
  description: tool.description,
@@ -32157,7 +32476,7 @@ Telegram admin: @${msg.username}` : `Telegram ${isGroup ? "group" : "public"} ch
32157
32476
  new TranscribeFileTool(repoRoot),
32158
32477
  new TranscribeUrlTool(repoRoot)
32159
32478
  ];
32160
- let adaptedTools = allTools.map(adaptTool4);
32479
+ let adaptedTools = allTools.map(adaptTool5);
32161
32480
  adaptedTools = applyToolPolicy(adaptedTools, context, this.toolPolicyConfig);
32162
32481
  if (context !== "telegram-admin-dm") {
32163
32482
  const memWriteIdx = adaptedTools.findIndex((t) => t.name === "memory_write");
@@ -33790,7 +34109,7 @@ function getVersion() {
33790
34109
  }
33791
34110
  return "0.0.0";
33792
34111
  }
33793
- function adaptTool5(tool) {
34112
+ function adaptTool6(tool) {
33794
34113
  return {
33795
34114
  name: tool.name,
33796
34115
  description: tool.description,
@@ -33888,7 +34207,7 @@ function buildTools(repoRoot, config, contextWindowSize) {
33888
34207
  new AgendaTool(repoRoot)
33889
34208
  ];
33890
34209
  return [
33891
- ...executionTools.map(adaptTool5),
34210
+ ...executionTools.map(adaptTool6),
33892
34211
  createSubAgentTool(config, repoRoot, contextWindowSize),
33893
34212
  createTaskCompleteTool()
33894
34213
  ];
@@ -33940,7 +34259,7 @@ function createSubAgentTool(config, repoRoot, ctxWindowSize) {
33940
34259
  new MemoryReadTool(repoRoot),
33941
34260
  new MemoryWriteTool(repoRoot)
33942
34261
  ];
33943
- subRunner.registerTools(subTools.map(adaptTool5));
34262
+ subRunner.registerTools(subTools.map(adaptTool6));
33944
34263
  subRunner.registerTool(createTaskCompleteTool());
33945
34264
  if (background) {
33946
34265
  const promise = subRunner.run(task, `Working directory: ${repoRoot}`).then((result2) => {
@@ -34176,6 +34495,13 @@ ${entry.fullContent}`
34176
34495
  filesTouched.add(event.toolArgs.path);
34177
34496
  }
34178
34497
  }
34498
+ getActivityFeed().push({
34499
+ ts: Date.now(),
34500
+ source: "main",
34501
+ sourceId: "main",
34502
+ summary: `${event.toolName ?? "unknown"}(${Object.keys(event.toolArgs ?? {}).join(", ")})`,
34503
+ toolName: event.toolName ?? "unknown"
34504
+ });
34179
34505
  lastToolCall = { name: event.toolName ?? "unknown", args: event.toolArgs ?? {} };
34180
34506
  statusBar?.recordSpeedToolCall(event.toolName ?? "unknown");
34181
34507
  toolCallStartMs = Date.now();
@@ -34194,6 +34520,14 @@ ${entry.fullContent}`
34194
34520
  editHistory.logToolCall(lastToolCall.name, lastToolCall.args, event.success ?? false);
34195
34521
  lastToolCall = null;
34196
34522
  }
34523
+ getActivityFeed().push({
34524
+ ts: Date.now(),
34525
+ source: "main",
34526
+ sourceId: "main",
34527
+ summary: String(event.content ?? "").slice(0, 120),
34528
+ toolName: event.toolName ?? "unknown",
34529
+ success: event.success ?? false
34530
+ });
34197
34531
  const resultLen = event.content?.length ?? 0;
34198
34532
  if (resultLen > 0) {
34199
34533
  statusBar?.recordSpeedToolResult(event.toolName ?? "unknown", resultLen);
@@ -34571,6 +34905,8 @@ async function startInteractive(config, repoPath) {
34571
34905
  });
34572
34906
  const voiceEngine = new VoiceEngine();
34573
34907
  let voiceSession = null;
34908
+ let adminSessionKey = null;
34909
+ const callSubAgents = /* @__PURE__ */ new Map();
34574
34910
  const streamRenderer = new StreamRenderer();
34575
34911
  if (savedSettings.voice) {
34576
34912
  voiceEngine.toggle().catch(() => {
@@ -35238,34 +35574,83 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
35238
35574
  return text ? `Stopped. Last transcript: "${text}"` : "Stopped listening.";
35239
35575
  },
35240
35576
  // Voice call session — standalone cloudflared tunnel for /call
35241
- // Wires: WebSocket audio ASR transcription → agent input → TTS → WebSocket broadcast
35577
+ // Each connecting WebSocket client gets a dedicated CallSubAgent.
35578
+ // Admin callers (matching session key) get full tool access; public callers get read-only.
35242
35579
  async callStart() {
35243
35580
  if (voiceSession?.isActive) {
35244
35581
  return voiceSession.tunnelUrl;
35245
35582
  }
35583
+ if (!adminSessionKey) {
35584
+ adminSessionKey = generateSessionKey();
35585
+ }
35246
35586
  voiceSession = new VoiceSession();
35247
- const callState = { transcriber: null, loading: false };
35587
+ const callState = {
35588
+ transcribers: /* @__PURE__ */ new Map(),
35589
+ loading: false,
35590
+ sharedTranscriber: null
35591
+ };
35248
35592
  const engine = getListenEngine();
35249
35593
  voiceSession.onUserAudio = (pcmChunk, userId) => {
35250
- if (callState.transcriber)
35251
- callState.transcriber.write(pcmChunk);
35594
+ if (callState.sharedTranscriber)
35595
+ callState.sharedTranscriber.write(pcmChunk);
35252
35596
  };
35253
- voiceSession.on("userConnected", (id, username) => {
35597
+ voiceSession.on("userConnected", (id, username, sessionKey) => {
35254
35598
  writeContent(() => renderVoiceSessionUser("connected", username));
35255
- if (!callState.transcriber && !callState.loading) {
35599
+ const tier = sessionKey && sessionKey === adminSessionKey ? "admin" : "public";
35600
+ writeContent(() => renderInfo(`Call client ${id} connected as ${tier.toUpperCase()}`));
35601
+ const emotionState = emotionEngine?.getState?.();
35602
+ const subAgent = new CallSubAgent(id, {
35603
+ config: currentConfig,
35604
+ repoRoot,
35605
+ tier,
35606
+ emotionContext: emotionState?.label ? `Current mood: ${emotionState.emoji ?? ""} ${emotionState.label}` : void 0,
35607
+ modelTier: getModelTier(currentConfig.model),
35608
+ contextWindowSize: resolvedContextWindowSize > 0 ? resolvedContextWindowSize : void 0
35609
+ });
35610
+ subAgent.on("response", (text) => {
35611
+ writeContent(() => renderVoiceSessionTranscript("agent", text));
35612
+ voiceSession?.sendTranscriptToClient(id, "agent", text);
35613
+ if (voiceEngine.enabled && voiceEngine.ready) {
35614
+ const session = voiceSession;
35615
+ voiceEngine.synthesizeToPCM(text).then((result) => {
35616
+ if (result && session?.isActive) {
35617
+ const { pcm, sampleRate } = result;
35618
+ session.sendSpeakingStateToClient(id, true);
35619
+ session.sendAudioToClient(id, pcm);
35620
+ const durationMs = pcm.length / 2 / sampleRate * 1e3;
35621
+ setTimeout(() => session.sendSpeakingStateToClient(id, false), durationMs);
35622
+ }
35623
+ }).catch(() => {
35624
+ });
35625
+ }
35626
+ });
35627
+ subAgent.on("error", (err) => {
35628
+ writeContent(() => renderWarning(`Call sub-agent error (${id}): ${err.message}`));
35629
+ });
35630
+ subAgent.init().then(() => {
35631
+ callSubAgents.set(id, subAgent);
35632
+ }).catch((err) => {
35633
+ writeContent(() => renderWarning(`Call sub-agent init failed (${id}): ${err instanceof Error ? err.message : String(err)}`));
35634
+ });
35635
+ if (!callState.sharedTranscriber && !callState.loading) {
35256
35636
  callState.loading = true;
35257
35637
  engine.createCallTranscriber().then((t) => {
35258
35638
  if (!t || !voiceSession?.isActive)
35259
35639
  return;
35260
- callState.transcriber = t;
35261
- callState.transcriber.on("transcript", (evt) => {
35640
+ callState.sharedTranscriber = t;
35641
+ callState.sharedTranscriber.on("transcript", (evt) => {
35262
35642
  if (!evt.text?.trim() || !voiceSession?.isActive)
35263
35643
  return;
35264
35644
  const text = evt.text.trim();
35265
35645
  writeContent(() => renderVoiceSessionTranscript("user", text));
35266
35646
  voiceSession?.sendTranscript("user", text);
35267
35647
  if (evt.isFinal) {
35268
- rl.write(text + "\n");
35648
+ for (const [clientId, agent] of callSubAgents) {
35649
+ agent.handleTranscript(text);
35650
+ }
35651
+ if (callSubAgents.size === 0) {
35652
+ rl.write(text + "\n");
35653
+ }
35269
35654
  }
35270
35655
  });
35271
35656
  writeContent(() => renderInfo("Call ASR ready \u2014 listening for speech"));
@@ -35276,6 +35661,11 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
35276
35661
  });
35277
35662
  voiceSession.on("userDisconnected", (id) => {
35278
35663
  writeContent(() => renderVoiceSessionUser("disconnected", id));
35664
+ const subAgent = callSubAgents.get(id);
35665
+ if (subAgent) {
35666
+ subAgent.dispose();
35667
+ callSubAgents.delete(id);
35668
+ }
35279
35669
  });
35280
35670
  voiceSession.on("wsClose", (id, code, reason) => {
35281
35671
  if (code > 0)
@@ -35286,15 +35676,21 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
35286
35676
  });
35287
35677
  voiceSession.on("idle_timeout", () => {
35288
35678
  writeContent(() => renderWarning("Call session auto-closed (1 min idle \u2014 no users connected)"));
35289
- if (callState.transcriber) {
35290
- callState.transcriber.stop();
35291
- callState.transcriber = null;
35679
+ if (callState.sharedTranscriber) {
35680
+ callState.sharedTranscriber.stop();
35681
+ callState.sharedTranscriber = null;
35292
35682
  }
35683
+ for (const agent of callSubAgents.values())
35684
+ agent.dispose();
35685
+ callSubAgents.clear();
35293
35686
  voiceSession = null;
35294
35687
  });
35295
35688
  try {
35296
35689
  const tunnelUrl = await voiceSession.start();
35690
+ const adminUrl = adminSessionKey ? `${tunnelUrl}?key=${adminSessionKey}` : tunnelUrl;
35297
35691
  writeContent(() => renderVoiceSessionStart(tunnelUrl));
35692
+ writeContent(() => renderInfo(`Admin call URL (includes key): ${adminUrl}`));
35693
+ writeContent(() => renderInfo(`Public call URL (read-only): ${tunnelUrl}`));
35298
35694
  if (voiceEngine.enabled && voiceEngine.ready) {
35299
35695
  const session = voiceSession;
35300
35696
  voiceEngine.onPCMOutput = (pcm, sampleRate) => {
@@ -35309,8 +35705,13 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
35309
35705
  return tunnelUrl;
35310
35706
  } catch (err) {
35311
35707
  writeContent(() => renderWarning(`Voice session failed: ${err instanceof Error ? err.message : String(err)}`));
35312
- callState.transcriber?.stop();
35313
- callState.transcriber = null;
35708
+ if (callState.sharedTranscriber) {
35709
+ callState.sharedTranscriber.stop();
35710
+ callState.sharedTranscriber = null;
35711
+ }
35712
+ for (const agent of callSubAgents.values())
35713
+ agent.dispose();
35714
+ callSubAgents.clear();
35314
35715
  voiceSession = null;
35315
35716
  return null;
35316
35717
  }
@@ -35322,6 +35723,9 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
35322
35723
  writeContent(() => renderVoiceSessionStop(runtime));
35323
35724
  voiceSession = null;
35324
35725
  voiceEngine.onPCMOutput = null;
35726
+ for (const agent of callSubAgents.values())
35727
+ agent.dispose();
35728
+ callSubAgents.clear();
35325
35729
  }
35326
35730
  },
35327
35731
  isCallActive() {
@@ -36008,6 +36412,7 @@ var init_interactive = __esm({
36008
36412
  init_dist();
36009
36413
  init_listen();
36010
36414
  init_voice_session();
36415
+ init_call_agent();
36011
36416
  init_config();
36012
36417
  init_updater();
36013
36418
  init_commands();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.60.0",
3
+ "version": "0.61.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",