@ouro.bot/cli 0.1.0-alpha.50 → 0.1.0-alpha.51

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,16 @@
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.51",
6
+ "changes": [
7
+ "Agents can now adjust their own reasoning depth mid-conversation via a new set_reasoning_effort tool, with effort levels derived from a central model capabilities registry.",
8
+ "Anthropic extended thinking is now enabled with adaptive effort, thinking blocks are captured during streaming (including signatures and redacted blocks), persisted on conversation history, and faithfully round-tripped across turns.",
9
+ "Anthropic max_tokens now uses the model's actual output ceiling from the registry instead of a hardcoded 4096, removing artificial response length constraints.",
10
+ "Codex assistant messages are now annotated with phase labels (commentary vs final_answer) so GPT-5.4 can distinguish intermediate reasoning from completed responses in its own history.",
11
+ "Azure and Codex reasoning effort is now dynamic from the agent loop instead of hardcoded to medium."
12
+ ]
13
+ },
4
14
  {
5
15
  "version": "0.1.0-alpha.50",
6
16
  "changes": [
@@ -331,7 +331,12 @@ async function runAgent(messages, callbacks, channel, signal, options) {
331
331
  // so turn execution remains consistent and non-fatal.
332
332
  if (channel) {
333
333
  try {
334
- const refreshed = await (0, prompt_1.buildSystem)(channel, options, currentContext);
334
+ const buildSystemOptions = {
335
+ ...options,
336
+ providerCapabilities: providerRuntime.capabilities,
337
+ supportedReasoningEfforts: providerRuntime.supportedReasoningEfforts,
338
+ };
339
+ const refreshed = await (0, prompt_1.buildSystem)(channel, buildSystemOptions, currentContext);
335
340
  upsertSystemPrompt(messages, refreshed);
336
341
  }
337
342
  catch (error) {
@@ -362,13 +367,22 @@ async function runAgent(messages, callbacks, channel, signal, options) {
362
367
  let completion;
363
368
  let sawSteeringFollowUp = false;
364
369
  let mustResolveBeforeHandoffActive = options?.mustResolveBeforeHandoff === true;
370
+ let currentReasoningEffort = "medium";
365
371
  // Prevent MaxListenersExceeded warning — each iteration adds a listener
366
372
  try {
367
373
  require("events").setMaxListeners(50, signal);
368
374
  }
369
375
  catch { /* unsupported */ }
370
376
  const toolPreferences = currentContext?.friend?.toolPreferences;
371
- const baseTools = options?.tools ?? (0, tools_1.getToolsForChannel)(channel ? (0, channel_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined, currentContext);
377
+ const baseTools = options?.tools ?? (0, tools_1.getToolsForChannel)(channel ? (0, channel_1.getChannelCapabilities)(channel) : undefined, toolPreferences && Object.keys(toolPreferences).length > 0 ? toolPreferences : undefined, currentContext, providerRuntime.capabilities);
378
+ // Augment tool context with reasoning effort controls from provider
379
+ const augmentedToolContext = options?.toolContext
380
+ ? {
381
+ ...options.toolContext,
382
+ supportedReasoningEfforts: providerRuntime.supportedReasoningEfforts,
383
+ setReasoningEffort: (level) => { currentReasoningEffort = level; },
384
+ }
385
+ : undefined;
372
386
  // Rebase provider-owned turn state from canonical messages at user-turn start.
373
387
  // This prevents stale provider caches from replaying prior-turn context.
374
388
  providerRuntime.resetTurnState(messages);
@@ -412,6 +426,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
412
426
  signal,
413
427
  traceId,
414
428
  toolChoiceRequired,
429
+ reasoningEffort: currentReasoningEffort,
415
430
  });
416
431
  // Track usage from the latest API call
417
432
  if (result.usage)
@@ -435,6 +450,17 @@ async function runAgent(messages, callbacks, channel, signal, options) {
435
450
  if (reasoningItems.length > 0) {
436
451
  msg._reasoning_items = reasoningItems;
437
452
  }
453
+ // Store thinking blocks (Anthropic) on the assistant message for round-tripping
454
+ const thinkingItems = result.outputItems.filter((item) => "type" in item && (item.type === "thinking" || item.type === "redacted_thinking"));
455
+ if (thinkingItems.length > 0) {
456
+ msg._thinking_blocks = thinkingItems;
457
+ }
458
+ // Phase annotation for Codex provider
459
+ const hasPhaseAnnotation = providerRuntime.capabilities.has("phase-annotation");
460
+ const isSoleFinalAnswer = result.toolCalls.length === 1 && result.toolCalls[0].name === "final_answer";
461
+ if (hasPhaseAnnotation) {
462
+ msg.phase = isSoleFinalAnswer ? "final_answer" : "commentary";
463
+ }
438
464
  if (!result.toolCalls.length) {
439
465
  // No tool calls — accept response as-is.
440
466
  // (Kick detection disabled; tool_choice: required + final_answer
@@ -444,7 +470,6 @@ async function runAgent(messages, callbacks, channel, signal, options) {
444
470
  }
445
471
  else {
446
472
  // Check for final_answer sole call: intercept before tool execution
447
- const isSoleFinalAnswer = result.toolCalls.length === 1 && result.toolCalls[0].name === "final_answer";
448
473
  if (isSoleFinalAnswer) {
449
474
  // Extract answer from the tool call arguments.
450
475
  // Supports: {"answer":"text","intent":"..."} or "text" (JSON string).
@@ -536,7 +561,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
536
561
  let success;
537
562
  try {
538
563
  const execToolFn = options?.execTool ?? tools_1.execTool;
539
- toolResult = await execToolFn(tc.name, args, options?.toolContext);
564
+ toolResult = await execToolFn(tc.name, args, augmentedToolContext ?? options?.toolContext);
540
565
  success = true;
541
566
  }
542
567
  catch (e) {
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MODEL_CAPABILITIES = void 0;
4
+ exports.getModelCapabilities = getModelCapabilities;
5
+ const runtime_1 = require("../nerves/runtime");
6
+ exports.MODEL_CAPABILITIES = {
7
+ "claude-opus-4-6": {
8
+ reasoningEffort: ["low", "medium", "high", "max"],
9
+ thinkingFormat: "anthropic",
10
+ maxOutputTokens: 128000,
11
+ },
12
+ "claude-sonnet-4-6": {
13
+ reasoningEffort: ["low", "medium", "high"],
14
+ thinkingFormat: "anthropic",
15
+ maxOutputTokens: 64000,
16
+ },
17
+ "gpt-5.4": {
18
+ reasoningEffort: ["low", "medium", "high"],
19
+ phase: true,
20
+ maxOutputTokens: 100000,
21
+ },
22
+ "gpt-5.3-codex": {
23
+ reasoningEffort: ["low", "medium", "high"],
24
+ phase: true,
25
+ maxOutputTokens: 100000,
26
+ },
27
+ };
28
+ const EMPTY_CAPABILITIES = Object.freeze({});
29
+ function getModelCapabilities(modelId) {
30
+ (0, runtime_1.emitNervesEvent)({
31
+ component: "engine",
32
+ event: "engine.model_capabilities_lookup",
33
+ message: `model capabilities lookup: ${modelId}`,
34
+ meta: { modelId, found: modelId in exports.MODEL_CAPABILITIES },
35
+ });
36
+ const entry = exports.MODEL_CAPABILITIES[modelId];
37
+ if (entry)
38
+ return entry;
39
+ return { ...EMPTY_CAPABILITIES };
40
+ }
@@ -3,12 +3,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.toAnthropicMessages = toAnthropicMessages;
6
7
  exports.createAnthropicProviderRuntime = createAnthropicProviderRuntime;
7
8
  const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
8
9
  const config_1 = require("../config");
9
10
  const identity_1 = require("../identity");
10
11
  const runtime_1 = require("../../nerves/runtime");
11
12
  const streaming_1 = require("../streaming");
13
+ const model_capabilities_1 = require("../model-capabilities");
12
14
  const ANTHROPIC_SETUP_TOKEN_PREFIX = "sk-ant-oat01-";
13
15
  const ANTHROPIC_SETUP_TOKEN_MIN_LENGTH = 80;
14
16
  const ANTHROPIC_OAUTH_BETA_HEADER = "claude-code-20250219,oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14,interleaved-thinking-2025-05-14";
@@ -93,6 +95,18 @@ function toAnthropicMessages(messages) {
93
95
  if (msg.role === "assistant") {
94
96
  const assistant = msg;
95
97
  const blocks = [];
98
+ // Restore thinking blocks before text/tool_use blocks
99
+ const thinkingBlocks = assistant._thinking_blocks;
100
+ if (thinkingBlocks) {
101
+ for (const tb of thinkingBlocks) {
102
+ if (tb.type === "thinking") {
103
+ blocks.push({ type: "thinking", thinking: tb.thinking, signature: tb.signature });
104
+ }
105
+ else {
106
+ blocks.push({ type: "redacted_thinking", data: tb.data });
107
+ }
108
+ }
109
+ }
96
110
  const text = toAnthropicTextContent(assistant.content);
97
111
  if (text) {
98
112
  blocks.push({ type: "text", text });
@@ -195,11 +209,14 @@ function withAnthropicAuthGuidance(error) {
195
209
  async function streamAnthropicMessages(client, model, request) {
196
210
  const { system, messages } = toAnthropicMessages(request.messages);
197
211
  const anthropicTools = toAnthropicTools(request.activeTools);
212
+ const modelCaps = (0, model_capabilities_1.getModelCapabilities)(model);
213
+ const maxTokens = modelCaps.maxOutputTokens ?? 16384;
198
214
  const params = {
199
215
  model,
200
- max_tokens: 4096,
216
+ max_tokens: maxTokens,
201
217
  messages,
202
218
  stream: true,
219
+ thinking: { type: "adaptive", effort: request.reasoningEffort ?? "medium" },
203
220
  };
204
221
  if (system)
205
222
  params.system = system;
@@ -219,6 +236,8 @@ async function streamAnthropicMessages(client, model, request) {
219
236
  let streamStarted = false;
220
237
  let usage;
221
238
  const toolCalls = new Map();
239
+ const thinkingBlocks = new Map();
240
+ const redactedBlocks = new Map();
222
241
  const answerStreamer = new streaming_1.FinalAnswerStreamer(request.callbacks);
223
242
  try {
224
243
  for await (const event of response) {
@@ -227,8 +246,14 @@ async function streamAnthropicMessages(client, model, request) {
227
246
  const eventType = String(event.type ?? "");
228
247
  if (eventType === "content_block_start") {
229
248
  const block = event.content_block;
230
- if (block?.type === "tool_use") {
231
- const index = Number(event.index);
249
+ const index = Number(event.index);
250
+ if (block?.type === "thinking") {
251
+ thinkingBlocks.set(index, { type: "thinking", thinking: "", signature: "" });
252
+ }
253
+ else if (block?.type === "redacted_thinking") {
254
+ redactedBlocks.set(index, { type: "redacted_thinking", data: String(block.data ?? "") });
255
+ }
256
+ else if (block?.type === "tool_use") {
232
257
  const rawInput = block.input;
233
258
  const input = rawInput && typeof rawInput === "object"
234
259
  ? JSON.stringify(rawInput)
@@ -265,7 +290,19 @@ async function streamAnthropicMessages(client, model, request) {
265
290
  request.callbacks.onModelStreamStart();
266
291
  streamStarted = true;
267
292
  }
268
- request.callbacks.onReasoningChunk(String(delta?.thinking ?? ""));
293
+ const thinkingText = String(delta?.thinking ?? "");
294
+ request.callbacks.onReasoningChunk(thinkingText);
295
+ const thinkingIndex = Number(event.index);
296
+ const thinkingBlock = thinkingBlocks.get(thinkingIndex);
297
+ if (thinkingBlock)
298
+ thinkingBlock.thinking += thinkingText;
299
+ continue;
300
+ }
301
+ if (deltaType === "signature_delta") {
302
+ const sigIndex = Number(event.index);
303
+ const sigBlock = thinkingBlocks.get(sigIndex);
304
+ if (sigBlock)
305
+ sigBlock.signature += String(delta?.signature ?? "");
269
306
  continue;
270
307
  }
271
308
  if (deltaType === "input_json_delta") {
@@ -301,10 +338,18 @@ async function streamAnthropicMessages(client, model, request) {
301
338
  catch (error) {
302
339
  throw withAnthropicAuthGuidance(error);
303
340
  }
341
+ // Collect all thinking blocks (regular + redacted) sorted by index to preserve ordering
342
+ const allThinkingIndices = [...thinkingBlocks.keys(), ...redactedBlocks.keys()].sort((a, b) => a - b);
343
+ const outputItems = allThinkingIndices.map((idx) => {
344
+ const tb = thinkingBlocks.get(idx);
345
+ if (tb)
346
+ return tb;
347
+ return redactedBlocks.get(idx);
348
+ });
304
349
  return {
305
350
  content,
306
351
  toolCalls: [...toolCalls.values()],
307
- outputItems: [],
352
+ outputItems,
308
353
  usage,
309
354
  finalAnswerStreamed: answerStreamer.streamed,
310
355
  };
@@ -320,6 +365,10 @@ function createAnthropicProviderRuntime() {
320
365
  if (!(anthropicConfig.model && anthropicConfig.setupToken)) {
321
366
  throw new Error(getAnthropicReauthGuidance("provider 'anthropic' is selected in agent.json but providers.anthropic.model/setupToken is incomplete in secrets.json."));
322
367
  }
368
+ const modelCaps = (0, model_capabilities_1.getModelCapabilities)(anthropicConfig.model);
369
+ const capabilities = new Set();
370
+ if (modelCaps.reasoningEffort)
371
+ capabilities.add("reasoning-effort");
323
372
  const credential = resolveAnthropicSetupTokenCredential();
324
373
  const client = new sdk_1.default({
325
374
  authToken: credential.token,
@@ -333,6 +382,8 @@ function createAnthropicProviderRuntime() {
333
382
  id: "anthropic",
334
383
  model: anthropicConfig.model,
335
384
  client,
385
+ capabilities,
386
+ supportedReasoningEfforts: modelCaps.reasoningEffort,
336
387
  resetTurnState(_messages) {
337
388
  // Anthropic request payload is derived from canonical messages each turn.
338
389
  },
@@ -5,6 +5,7 @@ const openai_1 = require("openai");
5
5
  const config_1 = require("../config");
6
6
  const runtime_1 = require("../../nerves/runtime");
7
7
  const streaming_1 = require("../streaming");
8
+ const model_capabilities_1 = require("../model-capabilities");
8
9
  function createAzureProviderRuntime() {
9
10
  (0, runtime_1.emitNervesEvent)({
10
11
  component: "engine",
@@ -16,6 +17,10 @@ function createAzureProviderRuntime() {
16
17
  if (!(azureConfig.apiKey && azureConfig.endpoint && azureConfig.deployment && azureConfig.modelName)) {
17
18
  throw new Error("provider 'azure' is selected in agent.json but providers.azure is incomplete in secrets.json.");
18
19
  }
20
+ const modelCaps = (0, model_capabilities_1.getModelCapabilities)(azureConfig.modelName);
21
+ const capabilities = new Set();
22
+ if (modelCaps.reasoningEffort)
23
+ capabilities.add("reasoning-effort");
19
24
  const client = new openai_1.AzureOpenAI({
20
25
  apiKey: azureConfig.apiKey,
21
26
  endpoint: azureConfig.endpoint.replace(/\/openai.*$/, ""),
@@ -30,6 +35,8 @@ function createAzureProviderRuntime() {
30
35
  id: "azure",
31
36
  model: azureConfig.modelName,
32
37
  client,
38
+ capabilities,
39
+ supportedReasoningEfforts: modelCaps.reasoningEffort,
33
40
  resetTurnState(messages) {
34
41
  const { instructions, input } = (0, streaming_1.toResponsesInput)(messages);
35
42
  nativeInput = input;
@@ -48,7 +55,7 @@ function createAzureProviderRuntime() {
48
55
  input: nativeInput,
49
56
  instructions: nativeInstructions,
50
57
  tools: (0, streaming_1.toResponsesTools)(request.activeTools),
51
- reasoning: { effort: "medium", summary: "detailed" },
58
+ reasoning: { effort: request.reasoningEffort ?? "medium", summary: "detailed" },
52
59
  stream: true,
53
60
  store: false,
54
61
  include: ["reasoning.encrypted_content"],
@@ -8,6 +8,7 @@ const openai_1 = __importDefault(require("openai"));
8
8
  const config_1 = require("../config");
9
9
  const runtime_1 = require("../../nerves/runtime");
10
10
  const streaming_1 = require("../streaming");
11
+ const model_capabilities_1 = require("../model-capabilities");
11
12
  function createMinimaxProviderRuntime() {
12
13
  (0, runtime_1.emitNervesEvent)({
13
14
  component: "engine",
@@ -19,6 +20,8 @@ function createMinimaxProviderRuntime() {
19
20
  if (!minimaxConfig.apiKey) {
20
21
  throw new Error("provider 'minimax' is selected in agent.json but providers.minimax.apiKey is missing in secrets.json.");
21
22
  }
23
+ // Registry consulted; MiniMax models return empty defaults (no capabilities to derive)
24
+ (0, model_capabilities_1.getModelCapabilities)(minimaxConfig.model);
22
25
  const client = new openai_1.default({
23
26
  apiKey: minimaxConfig.apiKey,
24
27
  baseURL: "https://api.minimaxi.chat/v1",
@@ -29,6 +32,7 @@ function createMinimaxProviderRuntime() {
29
32
  id: "minimax",
30
33
  model: minimaxConfig.model,
31
34
  client,
35
+ capabilities: new Set(),
32
36
  resetTurnState(_messages) {
33
37
  // No provider-owned turn state for chat-completions providers.
34
38
  },
@@ -9,6 +9,7 @@ const config_1 = require("../config");
9
9
  const identity_1 = require("../identity");
10
10
  const runtime_1 = require("../../nerves/runtime");
11
11
  const streaming_1 = require("../streaming");
12
+ const model_capabilities_1 = require("../model-capabilities");
12
13
  const OPENAI_CODEX_AUTH_FAILURE_MARKERS = [
13
14
  "authentication failed",
14
15
  "unauthorized",
@@ -106,6 +107,12 @@ function createOpenAICodexProviderRuntime() {
106
107
  if (!chatgptAccountId) {
107
108
  throw new Error(getOpenAICodexReauthGuidance("OpenAI Codex OAuth access token is missing a chatgpt_account_id claim required for chatgpt.com/backend-api/codex."));
108
109
  }
110
+ const modelCaps = (0, model_capabilities_1.getModelCapabilities)(codexConfig.model);
111
+ const capabilities = new Set();
112
+ if (modelCaps.reasoningEffort)
113
+ capabilities.add("reasoning-effort");
114
+ if (modelCaps.phase)
115
+ capabilities.add("phase-annotation");
109
116
  const client = new openai_1.default({
110
117
  apiKey: token,
111
118
  baseURL: OPENAI_CODEX_BACKEND_BASE_URL,
@@ -123,6 +130,8 @@ function createOpenAICodexProviderRuntime() {
123
130
  id: "openai-codex",
124
131
  model: codexConfig.model,
125
132
  client,
133
+ capabilities,
134
+ supportedReasoningEfforts: modelCaps.reasoningEffort,
126
135
  resetTurnState(messages) {
127
136
  const { instructions, input } = (0, streaming_1.toResponsesInput)(messages);
128
137
  nativeInput = input;
@@ -141,7 +150,7 @@ function createOpenAICodexProviderRuntime() {
141
150
  input: nativeInput,
142
151
  instructions: nativeInstructions,
143
152
  tools: (0, streaming_1.toResponsesTools)(request.activeTools),
144
- reasoning: { effort: "medium", summary: "detailed" },
153
+ reasoning: { effort: request.reasoningEffort ?? "medium", summary: "detailed" },
145
154
  stream: true,
146
155
  store: false,
147
156
  include: ["reasoning.encrypted_content"],
@@ -185,7 +185,10 @@ function toResponsesInput(messages) {
185
185
  }
186
186
  }
187
187
  if (a.content) {
188
- input.push({ role: "assistant", content: typeof a.content === "string" ? a.content : "" });
188
+ const assistantItem = { role: "assistant", content: typeof a.content === "string" ? a.content : "" };
189
+ if (a.phase)
190
+ assistantItem.phase = a.phase;
191
+ input.push(assistantItem);
189
192
  }
190
193
  if (a.tool_calls) {
191
194
  for (const tc of a.tool_calls) {
@@ -311,7 +311,7 @@ function dateSection() {
311
311
  return `current date: ${today}`;
312
312
  }
313
313
  function toolsSection(channel, options, context) {
314
- const channelTools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)(channel), undefined, context);
314
+ const channelTools = (0, tools_1.getToolsForChannel)((0, channel_1.getChannelCapabilities)(channel), undefined, context, options?.providerCapabilities);
315
315
  const activeTools = (options?.toolChoiceRequired ?? true) ? [...channelTools, tools_1.finalAnswerTool] : channelTools;
316
316
  const list = activeTools
317
317
  .map((t) => `- ${t.function.name}: ${t.function.description}`)
@@ -383,6 +383,14 @@ function delegationHintSection(options) {
383
383
  ];
384
384
  return lines.join("\n");
385
385
  }
386
+ function reasoningEffortSection(options) {
387
+ if (!options?.providerCapabilities?.has("reasoning-effort"))
388
+ return "";
389
+ const levels = options.supportedReasoningEfforts ?? [];
390
+ const levelList = levels.length > 0 ? levels.join(", ") : "varies by model";
391
+ return `## reasoning effort
392
+ i can adjust my own reasoning depth using the set_reasoning_effort tool. i use higher effort for complex analysis and lower effort for simple tasks. available levels: ${levelList}.`;
393
+ }
386
394
  function toolBehaviorSection(options) {
387
395
  if (!(options?.toolChoiceRequired ?? true))
388
396
  return "";
@@ -510,6 +518,7 @@ async function buildSystem(channel = "cli", options, context) {
510
518
  providerSection(),
511
519
  dateSection(),
512
520
  toolsSection(channel, options, context),
521
+ reasoningEffortSection(options),
513
522
  toolRestrictionSection(context),
514
523
  mixedTrustGroupSection(context),
515
524
  skillsSection(),
@@ -911,6 +911,40 @@ exports.baseToolDefinitions = [
911
911
  return `message queued for delivery to ${friendId} on ${target}. preview: "${preview}". it will be delivered when their session is next active.`;
912
912
  },
913
913
  },
914
+ {
915
+ tool: {
916
+ type: "function",
917
+ function: {
918
+ name: "set_reasoning_effort",
919
+ description: "adjust your own reasoning depth for subsequent turns. use higher effort for complex analysis, lower for simple tasks.",
920
+ parameters: {
921
+ type: "object",
922
+ properties: {
923
+ level: { type: "string", description: "the reasoning effort level to set" },
924
+ },
925
+ required: ["level"],
926
+ },
927
+ },
928
+ },
929
+ handler: (args, ctx) => {
930
+ if (!ctx?.supportedReasoningEfforts || !ctx.setReasoningEffort) {
931
+ return "reasoning effort adjustment is not available in this context.";
932
+ }
933
+ const level = (args.level || "").trim();
934
+ if (!ctx.supportedReasoningEfforts.includes(level)) {
935
+ return `invalid reasoning effort level "${level}". accepted levels: ${ctx.supportedReasoningEfforts.join(", ")}`;
936
+ }
937
+ ctx.setReasoningEffort(level);
938
+ (0, runtime_1.emitNervesEvent)({
939
+ component: "repertoire",
940
+ event: "repertoire.reasoning_effort_changed",
941
+ message: `reasoning effort set to ${level}`,
942
+ meta: { level },
943
+ });
944
+ return `reasoning effort set to "${level}".`;
945
+ },
946
+ requiredCapability: "reasoning-effort",
947
+ },
914
948
  ...tools_1.codingToolDefinitions,
915
949
  ];
916
950
  exports.tools = exports.baseToolDefinitions.map((d) => d.tool);
@@ -35,9 +35,11 @@ function blockedLocalToolMessage() {
35
35
  return "I can't do that because my trust level with you isn't high enough for local shell/file operations. Ask me for a remote-safe alternative (Graph/ADO/web), or run that operation from CLI.";
36
36
  }
37
37
  function baseToolsForCapabilities(capabilities, context) {
38
+ // Use baseToolDefinitions at call time so dynamically-added tools are included
39
+ const currentTools = tools_base_1.baseToolDefinitions.map((d) => d.tool);
38
40
  if (!shouldBlockLocalTools(capabilities, context))
39
- return tools_base_1.tools;
40
- return tools_base_1.tools.filter((tool) => !exports.REMOTE_BLOCKED_LOCAL_TOOLS.has(tool.function.name));
41
+ return currentTools;
42
+ return currentTools.filter((tool) => !exports.REMOTE_BLOCKED_LOCAL_TOOLS.has(tool.function.name));
41
43
  }
42
44
  // Apply a single tool preference to a tool schema, returning a new object.
43
45
  function applyPreference(tool, pref) {
@@ -49,37 +51,55 @@ function applyPreference(tool, pref) {
49
51
  },
50
52
  };
51
53
  }
54
+ // Filter out tools whose requiredCapability is not in the provider's capability set.
55
+ // Uses baseToolDefinitions at call time so dynamically-added tools are included.
56
+ // Only base tools can have requiredCapability (integration tools do not).
57
+ function filterByCapability(toolList, providerCapabilities) {
58
+ return toolList.filter((tool) => {
59
+ const def = tools_base_1.baseToolDefinitions.find((d) => d.tool.function.name === tool.function.name);
60
+ if (!def?.requiredCapability)
61
+ return true;
62
+ return providerCapabilities?.has(def.requiredCapability) === true;
63
+ });
64
+ }
52
65
  // Return the appropriate tools list based on channel capabilities.
53
66
  // Base tools (no integration) are always included.
54
67
  // Teams/integration tools are included only if their integration is in availableIntegrations.
55
68
  // When toolPreferences is provided, matching preferences are appended to tool descriptions.
56
- function getToolsForChannel(capabilities, toolPreferences, context) {
69
+ // When providerCapabilities is provided, tools with requiredCapability are filtered.
70
+ function getToolsForChannel(capabilities, toolPreferences, context, providerCapabilities) {
57
71
  const baseTools = baseToolsForCapabilities(capabilities, context);
58
72
  const bluebubblesTools = capabilities?.channel === "bluebubbles"
59
73
  ? tools_bluebubbles_1.bluebubblesToolDefinitions.map((d) => d.tool)
60
74
  : [];
75
+ let result;
61
76
  if (!capabilities || capabilities.availableIntegrations.length === 0) {
62
- return [...baseTools, ...bluebubblesTools];
77
+ result = [...baseTools, ...bluebubblesTools];
63
78
  }
64
- const available = new Set(capabilities.availableIntegrations);
65
- const channelDefs = [...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions];
66
- // Include tools whose integration is available, plus channel tools with no integration gate (e.g. teams_send_message)
67
- const integrationDefs = channelDefs.filter((d) => d.integration ? available.has(d.integration) : capabilities.channel === "teams");
68
- if (!toolPreferences || Object.keys(toolPreferences).length === 0) {
69
- return [...baseTools, ...bluebubblesTools, ...integrationDefs.map((d) => d.tool)];
79
+ else {
80
+ const available = new Set(capabilities.availableIntegrations);
81
+ const channelDefs = [...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions];
82
+ // Include tools whose integration is available, plus channel tools with no integration gate (e.g. teams_send_message)
83
+ const integrationDefs = channelDefs.filter((d) => d.integration ? available.has(d.integration) : capabilities.channel === "teams");
84
+ if (!toolPreferences || Object.keys(toolPreferences).length === 0) {
85
+ result = [...baseTools, ...bluebubblesTools, ...integrationDefs.map((d) => d.tool)];
86
+ }
87
+ else {
88
+ // Build a map of integration -> preference text for fast lookup
89
+ const prefMap = new Map();
90
+ for (const [key, value] of Object.entries(toolPreferences)) {
91
+ prefMap.set(key, value);
92
+ }
93
+ // Apply preferences to matching integration tools (new objects, no mutation)
94
+ // d.integration is guaranteed truthy -- integrationDefs are pre-filtered above
95
+ const enrichedIntegrationTools = integrationDefs.map((d) => {
96
+ const pref = prefMap.get(d.integration);
97
+ return pref ? applyPreference(d.tool, pref) : d.tool;
98
+ });
99
+ result = [...baseTools, ...bluebubblesTools, ...enrichedIntegrationTools];
100
+ }
70
101
  }
71
- // Build a map of integration -> preference text for fast lookup
72
- const prefMap = new Map();
73
- for (const [key, value] of Object.entries(toolPreferences)) {
74
- prefMap.set(key, value);
75
- }
76
- // Apply preferences to matching integration tools (new objects, no mutation)
77
- // d.integration is guaranteed truthy -- integrationDefs are pre-filtered above
78
- const enrichedIntegrationTools = integrationDefs.map((d) => {
79
- const pref = prefMap.get(d.integration);
80
- return pref ? applyPreference(d.tool, pref) : d.tool;
81
- });
82
- return [...baseTools, ...bluebubblesTools, ...enrichedIntegrationTools];
102
+ return filterByCapability(result, providerCapabilities);
83
103
  }
84
104
  // Check whether a tool requires user confirmation before execution.
85
105
  // Reads from ToolDefinition.confirmationRequired instead of a separate Set.
@@ -192,6 +212,8 @@ function summarizeArgs(name, args) {
192
212
  return summarizeKeyValues(args, ["sessionId"]);
193
213
  if (name === "bluebubbles_set_reply_target")
194
214
  return summarizeKeyValues(args, ["target", "threadOriginatorGuid"]);
215
+ if (name === "set_reasoning_effort")
216
+ return summarizeKeyValues(args, ["level"]);
195
217
  if (name === "claude")
196
218
  return summarizeKeyValues(args, ["prompt"]);
197
219
  if (name === "web_search")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.50",
3
+ "version": "0.1.0-alpha.51",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",