@ouro.bot/cli 0.1.0-alpha.31 → 0.1.0-alpha.32

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/changelog.json CHANGED
@@ -1,6 +1,13 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.32",
6
+ "changes": [
7
+ "BlueBubbles now treats thread-vs-top-level placement as an agent choice instead of a hard harness mirror. Slugger can deliberately stay in the current reply lane, widen back to top-level, or target another active thread when that makes the conversation flow better.",
8
+ "BlueBubbles turns now surface inbound lane metadata and recent active lanes from the shared chat trunk, so the model gets enough context to choose the right reply placement without splitting the conversation into separate persisted thread sessions."
9
+ ]
10
+ },
4
11
  {
5
12
  "version": "0.1.0-alpha.31",
6
13
  "changes": [
@@ -221,6 +221,7 @@ function runtimeInfoSection(channel) {
221
221
  }
222
222
  else if (channel === "bluebubbles") {
223
223
  lines.push("i am responding in iMessage through BlueBubbles. i keep replies short and phone-native. i do not use markdown. i do not introduce myself on boot.");
224
+ lines.push("when a bluebubbles turn arrives from a thread, the harness tells me the current lane and any recent active thread ids. if widening back to top-level or routing into a different active thread is the better move, i use bluebubbles_set_reply_target before final_answer.");
224
225
  }
225
226
  else {
226
227
  lines.push("i am responding in Microsoft Teams. i keep responses concise. i use markdown formatting. i do not introduce myself on boot.");
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bluebubblesToolDefinitions = void 0;
4
+ const runtime_1 = require("../nerves/runtime");
5
+ exports.bluebubblesToolDefinitions = [
6
+ {
7
+ tool: {
8
+ type: "function",
9
+ function: {
10
+ name: "bluebubbles_set_reply_target",
11
+ description: "choose where the current iMessage turn should land. use this when you want to widen back to top-level or route your update into a specific active thread in the same chat.",
12
+ parameters: {
13
+ type: "object",
14
+ properties: {
15
+ target: {
16
+ type: "string",
17
+ enum: ["current_lane", "top_level", "thread"],
18
+ description: "current_lane mirrors the current inbound lane, top_level answers in the main chat, and thread targets a specific active thread.",
19
+ },
20
+ threadOriginatorGuid: {
21
+ type: "string",
22
+ description: "required when target=thread; use one of the thread ids surfaced in the inbound iMessage context.",
23
+ },
24
+ },
25
+ required: ["target"],
26
+ },
27
+ },
28
+ },
29
+ handler: (args, ctx) => {
30
+ const target = typeof args.target === "string" ? args.target.trim() : "";
31
+ const controller = ctx?.bluebubblesReplyTarget;
32
+ if (!controller) {
33
+ (0, runtime_1.emitNervesEvent)({
34
+ level: "warn",
35
+ component: "tools",
36
+ event: "tool.error",
37
+ message: "bluebubbles reply target missing controller",
38
+ meta: { target },
39
+ });
40
+ return "bluebubbles reply targeting is not available in this context.";
41
+ }
42
+ if (target === "current_lane") {
43
+ const result = controller.setSelection({ target: "current_lane" });
44
+ (0, runtime_1.emitNervesEvent)({
45
+ component: "tools",
46
+ event: "tool.end",
47
+ message: "bluebubbles reply target updated",
48
+ meta: { target: "current_lane", success: true },
49
+ });
50
+ return result;
51
+ }
52
+ if (target === "top_level") {
53
+ const result = controller.setSelection({ target: "top_level" });
54
+ (0, runtime_1.emitNervesEvent)({
55
+ component: "tools",
56
+ event: "tool.end",
57
+ message: "bluebubbles reply target updated",
58
+ meta: { target: "top_level", success: true },
59
+ });
60
+ return result;
61
+ }
62
+ if (target === "thread") {
63
+ const threadOriginatorGuid = typeof args.threadOriginatorGuid === "string" ? args.threadOriginatorGuid.trim() : "";
64
+ if (!threadOriginatorGuid) {
65
+ (0, runtime_1.emitNervesEvent)({
66
+ level: "warn",
67
+ component: "tools",
68
+ event: "tool.error",
69
+ message: "bluebubbles reply target missing thread id",
70
+ meta: { target: "thread" },
71
+ });
72
+ return "threadOriginatorGuid is required when target=thread.";
73
+ }
74
+ const result = controller.setSelection({ target: "thread", threadOriginatorGuid });
75
+ (0, runtime_1.emitNervesEvent)({
76
+ component: "tools",
77
+ event: "tool.end",
78
+ message: "bluebubbles reply target updated",
79
+ meta: { target: "thread", success: true },
80
+ });
81
+ return result;
82
+ }
83
+ (0, runtime_1.emitNervesEvent)({
84
+ level: "warn",
85
+ component: "tools",
86
+ event: "tool.error",
87
+ message: "bluebubbles reply target invalid target",
88
+ meta: { target },
89
+ });
90
+ return "target must be one of: current_lane, top_level, thread.";
91
+ },
92
+ },
93
+ ];
@@ -7,6 +7,7 @@ exports.execTool = execTool;
7
7
  exports.summarizeArgs = summarizeArgs;
8
8
  const tools_base_1 = require("./tools-base");
9
9
  const tools_teams_1 = require("./tools-teams");
10
+ const tools_bluebubbles_1 = require("./tools-bluebubbles");
10
11
  const ado_semantic_1 = require("./ado-semantic");
11
12
  const tools_github_1 = require("./tools-github");
12
13
  const runtime_1 = require("../nerves/runtime");
@@ -17,7 +18,13 @@ Object.defineProperty(exports, "finalAnswerTool", { enumerable: true, get: funct
17
18
  var tools_teams_2 = require("./tools-teams");
18
19
  Object.defineProperty(exports, "teamsTools", { enumerable: true, get: function () { return tools_teams_2.teamsTools; } });
19
20
  // All tool definitions in a single registry
20
- const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions];
21
+ const allDefinitions = [
22
+ ...tools_base_1.baseToolDefinitions,
23
+ ...tools_bluebubbles_1.bluebubblesToolDefinitions,
24
+ ...tools_teams_1.teamsToolDefinitions,
25
+ ...ado_semantic_1.adoSemanticToolDefinitions,
26
+ ...tools_github_1.githubToolDefinitions,
27
+ ];
21
28
  const REMOTE_BLOCKED_LOCAL_TOOLS = new Set(["shell", "read_file", "write_file", "git_commit", "gh_cli"]);
22
29
  function isRemoteChannel(capabilities) {
23
30
  return capabilities?.channel === "teams" || capabilities?.channel === "bluebubbles";
@@ -61,15 +68,18 @@ function applyPreference(tool, pref) {
61
68
  // When toolPreferences is provided, matching preferences are appended to tool descriptions.
62
69
  function getToolsForChannel(capabilities, toolPreferences, context) {
63
70
  const baseTools = baseToolsForCapabilities(capabilities, context);
71
+ const bluebubblesTools = capabilities?.channel === "bluebubbles"
72
+ ? tools_bluebubbles_1.bluebubblesToolDefinitions.map((d) => d.tool)
73
+ : [];
64
74
  if (!capabilities || capabilities.availableIntegrations.length === 0) {
65
- return baseTools;
75
+ return [...baseTools, ...bluebubblesTools];
66
76
  }
67
77
  const available = new Set(capabilities.availableIntegrations);
68
78
  const channelDefs = [...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions];
69
79
  // Include tools whose integration is available, plus channel tools with no integration gate (e.g. teams_send_message)
70
80
  const integrationDefs = channelDefs.filter((d) => d.integration ? available.has(d.integration) : capabilities.channel === "teams");
71
81
  if (!toolPreferences || Object.keys(toolPreferences).length === 0) {
72
- return [...baseTools, ...integrationDefs.map((d) => d.tool)];
82
+ return [...baseTools, ...bluebubblesTools, ...integrationDefs.map((d) => d.tool)];
73
83
  }
74
84
  // Build a map of integration -> preference text for fast lookup
75
85
  const prefMap = new Map();
@@ -82,7 +92,7 @@ function getToolsForChannel(capabilities, toolPreferences, context) {
82
92
  const pref = prefMap.get(d.integration);
83
93
  return pref ? applyPreference(d.tool, pref) : d.tool;
84
94
  });
85
- return [...baseTools, ...enrichedIntegrationTools];
95
+ return [...baseTools, ...bluebubblesTools, ...enrichedIntegrationTools];
86
96
  }
87
97
  // Check whether a tool requires user confirmation before execution.
88
98
  // Reads from ToolDefinition.confirmationRequired instead of a separate Set.
@@ -205,6 +215,8 @@ function summarizeArgs(name, args) {
205
215
  return summarizeKeyValues(args, ["sessionId", "input"]);
206
216
  if (name === "coding_kill")
207
217
  return summarizeKeyValues(args, ["sessionId"]);
218
+ if (name === "bluebubbles_set_reply_target")
219
+ return summarizeKeyValues(args, ["target", "threadOriginatorGuid"]);
208
220
  if (name === "claude")
209
221
  return summarizeKeyValues(args, ["prompt"]);
210
222
  if (name === "web_search")
@@ -84,8 +84,82 @@ function resolveFriendParams(event) {
84
84
  channel: "bluebubbles",
85
85
  };
86
86
  }
87
- function buildInboundText(event) {
88
- const metadataPrefix = buildConversationScopePrefix(event);
87
+ function extractMessageText(content) {
88
+ if (typeof content === "string")
89
+ return content;
90
+ if (!Array.isArray(content))
91
+ return "";
92
+ return content
93
+ .map((part) => {
94
+ if (part && typeof part === "object" && "type" in part && part.type === "text" && typeof part.text === "string") {
95
+ return part.text;
96
+ }
97
+ return "";
98
+ })
99
+ .filter(Boolean)
100
+ .join("\n");
101
+ }
102
+ function extractHistoricalLaneSummary(messages) {
103
+ const seen = new Set();
104
+ const summaries = [];
105
+ for (let index = messages.length - 1; index >= 0; index--) {
106
+ const message = messages[index];
107
+ if (message.role !== "user")
108
+ continue;
109
+ const text = extractMessageText(message.content);
110
+ if (!text)
111
+ continue;
112
+ const firstLine = text.split("\n")[0].trim();
113
+ const threadMatch = firstLine.match(/thread id: ([^\]|]+)/i);
114
+ const laneKey = threadMatch
115
+ ? `thread:${threadMatch[1].trim()}`
116
+ : /top[-_]level/i.test(firstLine)
117
+ ? "top_level"
118
+ : null;
119
+ if (!laneKey || seen.has(laneKey))
120
+ continue;
121
+ seen.add(laneKey);
122
+ const snippet = text
123
+ .split("\n")
124
+ .slice(1)
125
+ .map((line) => line.trim())
126
+ .find(Boolean)
127
+ ?.slice(0, 80) ?? "(no recent text)";
128
+ summaries.push({
129
+ key: laneKey,
130
+ label: laneKey === "top_level" ? "top_level" : laneKey,
131
+ snippet,
132
+ });
133
+ if (summaries.length >= 5)
134
+ break;
135
+ }
136
+ return summaries;
137
+ }
138
+ function buildConversationScopePrefix(event, existingMessages) {
139
+ if (event.kind !== "message") {
140
+ return "";
141
+ }
142
+ const summaries = extractHistoricalLaneSummary(existingMessages);
143
+ const lines = [];
144
+ if (event.threadOriginatorGuid?.trim()) {
145
+ lines.push(`[conversation scope: existing chat trunk | current inbound lane: thread | current thread id: ${event.threadOriginatorGuid.trim()} | default outbound target: current_lane]`);
146
+ }
147
+ else {
148
+ lines.push("[conversation scope: existing chat trunk | current inbound lane: top_level | default outbound target: top_level]");
149
+ }
150
+ if (summaries.length > 0) {
151
+ lines.push("[recent active lanes]");
152
+ for (const summary of summaries) {
153
+ lines.push(`- ${summary.label}: ${summary.snippet}`);
154
+ }
155
+ }
156
+ if (event.threadOriginatorGuid?.trim() || summaries.some((summary) => summary.key.startsWith("thread:"))) {
157
+ lines.push("[routing control: use bluebubbles_set_reply_target with target=top_level to widen back out, or target=thread plus a listed thread id to route into a specific active thread]");
158
+ }
159
+ return lines.join("\n");
160
+ }
161
+ function buildInboundText(event, existingMessages) {
162
+ const metadataPrefix = buildConversationScopePrefix(event, existingMessages);
89
163
  const baseText = event.repairNotice?.trim()
90
164
  ? `${event.textForAgent}\n[${event.repairNotice.trim()}]`
91
165
  : event.textForAgent;
@@ -98,17 +172,8 @@ function buildInboundText(event) {
98
172
  }
99
173
  return `${event.sender.displayName}: ${scopedText}`;
100
174
  }
101
- function buildConversationScopePrefix(event) {
102
- if (event.kind !== "message") {
103
- return "";
104
- }
105
- if (event.threadOriginatorGuid?.trim()) {
106
- return `[conversation scope: existing chat trunk | current turn: thread reply | thread id: ${event.threadOriginatorGuid.trim()}]`;
107
- }
108
- return "[conversation scope: existing chat trunk | current turn: top-level]";
109
- }
110
- function buildInboundContent(event) {
111
- const text = buildInboundText(event);
175
+ function buildInboundContent(event, existingMessages) {
176
+ const text = buildInboundText(event, existingMessages);
112
177
  if (event.kind !== "message" || !event.inputPartsForAgent || event.inputPartsForAgent.length === 0) {
113
178
  return text;
114
179
  }
@@ -117,7 +182,33 @@ function buildInboundContent(event) {
117
182
  ...event.inputPartsForAgent,
118
183
  ];
119
184
  }
120
- function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
185
+ function createReplyTargetController(event) {
186
+ let selection = event.kind === "message" && event.threadOriginatorGuid?.trim()
187
+ ? { target: "current_lane" }
188
+ : { target: "top_level" };
189
+ return {
190
+ getReplyToMessageGuid() {
191
+ if (event.kind !== "message")
192
+ return undefined;
193
+ if (selection.target === "top_level")
194
+ return undefined;
195
+ if (selection.target === "thread")
196
+ return selection.threadOriginatorGuid.trim();
197
+ return event.threadOriginatorGuid?.trim() ? event.messageGuid : undefined;
198
+ },
199
+ setSelection(next) {
200
+ selection = next;
201
+ if (next.target === "top_level") {
202
+ return "bluebubbles reply target set to top_level";
203
+ }
204
+ if (next.target === "thread") {
205
+ return `bluebubbles reply target set to thread:${next.threadOriginatorGuid}`;
206
+ }
207
+ return "bluebubbles reply target set to current_lane";
208
+ },
209
+ };
210
+ }
211
+ function createBlueBubblesCallbacks(client, chat, replyTarget) {
121
212
  let textBuffer = "";
122
213
  const phrases = (0, phrases_1.getPhrases)();
123
214
  const activity = (0, debug_activity_1.createDebugActivityController)({
@@ -128,7 +219,7 @@ function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
128
219
  const sent = await client.sendText({
129
220
  chat,
130
221
  text,
131
- replyToMessageGuid,
222
+ replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
132
223
  });
133
224
  return sent.messageGuid;
134
225
  },
@@ -136,7 +227,7 @@ function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
136
227
  await client.sendText({
137
228
  chat,
138
229
  text,
139
- replyToMessageGuid,
230
+ replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
140
231
  });
141
232
  },
142
233
  setTyping: async (active) => {
@@ -222,7 +313,7 @@ function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
222
313
  await client.sendText({
223
314
  chat,
224
315
  text: trimmed,
225
- replyToMessageGuid,
316
+ replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
226
317
  });
227
318
  },
228
319
  async finish() {
@@ -295,17 +386,21 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
295
386
  const store = resolvedDeps.createFriendStore();
296
387
  const resolver = resolvedDeps.createFriendResolver(store, resolveFriendParams(event));
297
388
  const context = await resolver.resolve();
389
+ const replyTarget = createReplyTargetController(event);
298
390
  const toolContext = {
299
391
  signin: async () => undefined,
300
392
  friendStore: store,
301
393
  summarize: (0, core_1.createSummarize)(),
302
394
  context,
395
+ bluebubblesReplyTarget: {
396
+ setSelection: (selection) => replyTarget.setSelection(selection),
397
+ },
303
398
  codingFeedback: {
304
399
  send: async (message) => {
305
400
  await client.sendText({
306
401
  chat: event.chat,
307
402
  text: message,
308
- replyToMessageGuid: event.kind === "message" ? event.messageGuid : undefined,
403
+ replyToMessageGuid: replyTarget.getReplyToMessageGuid(),
309
404
  });
310
405
  },
311
406
  },
@@ -331,8 +426,8 @@ async function handleBlueBubblesEvent(payload, deps = {}) {
331
426
  const messages = existing?.messages && existing.messages.length > 0
332
427
  ? existing.messages
333
428
  : [{ role: "system", content: await resolvedDeps.buildSystem("bluebubbles", undefined, context) }];
334
- messages.push({ role: "user", content: buildInboundContent(event) });
335
- const callbacks = createBlueBubblesCallbacks(client, event.chat, event.kind === "message" ? event.messageGuid : undefined);
429
+ messages.push({ role: "user", content: buildInboundContent(event, existing?.messages ?? messages) });
430
+ const callbacks = createBlueBubblesCallbacks(client, event.chat, replyTarget);
336
431
  const controller = new AbortController();
337
432
  const agentOptions = {
338
433
  toolContext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.31",
3
+ "version": "0.1.0-alpha.32",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",