From 95e197323f434bbee3f018737381c78e4271faf9 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Thu, 9 Jul 2026 17:33:09 -0400 Subject: [PATCH 1/2] Rename ai-bot and prompt-assembly identifiers to tool naming The shared prompt pipeline and ai-bot consume the tool-named types and helpers throughout: MarkdownSkillTool / markdownSkillTools / parseMarkdownSkill's `tools` return key, isToolResultEvent, isToolResultStatusApplied, isToolOrCodePatchResult, CHECK_CORRECTNESS_TOOL_NAME (the wire string is unchanged), and the correctness helpers. Enabled skills carry `tools` in their attributes, with a fallback read of `commands` since legacy Skill cards serialize their tools under that card field. runtime-common's tool request/context exports rename (decodeToolRequest, encodeToolRequests, buildToolFunctionName[ FromResolvedRef], ToolContext, ToolInvocation); the pre-rename spellings stay exported since realm content imports them. The ToolContext stamp keeps its registered symbol key for runtime identity with already-loaded content. Part of the command -> tool rename (CS-12041 / CS-11788). Co-Authored-By: Claude Fable 5 --- packages/ai-bot/lib/code-patch-correctness.ts | 21 +- .../ai-bot/lib/matrix/response-publisher.ts | 6 +- packages/ai-bot/lib/responder.ts | 6 +- packages/ai-bot/lib/set-title.ts | 34 +- packages/ai-bot/main.ts | 4 +- packages/ai-bot/tests/chat-titling-test.ts | 16 +- .../tests/code-patch-correctness-test.ts | 16 +- .../ai-bot/tests/prompt-construction-test.ts | 28 +- packages/ai-bot/tests/responding-test.ts | 16 +- packages/base/card-api.gts | 4 +- packages/base/menu-items.ts | 4 +- packages/base/resources/command-data.ts | 8 +- packages/base/spec.gts | 6 +- .../operator-mode/create-listing-modal.gts | 4 +- .../operator-mode/operator-mode-overlays.gts | 4 +- .../components/operator-mode/stack-item.gts | 4 +- .../app/components/operator-mode/stack.gts | 4 +- packages/host/app/lib/host-base-tool.ts | 4 +- packages/host/app/routes/command-runner.ts | 12 +- packages/host/app/services/tool-service.ts | 12 +- .../host/app/tools/patch-card-instance.ts | 4 +- packages/host/app/tools/patch-fields.ts | 4 +- packages/host/app/tools/utils.ts | 10 +- packages/matrix/tests/commands.spec.ts | 16 +- .../matrix/tests/correctness-checks.spec.ts | 40 +- packages/runtime-common/ai/history.ts | 13 +- packages/runtime-common/ai/matrix-utils.ts | 10 +- packages/runtime-common/ai/prompt.ts | 383 +++++++++--------- packages/runtime-common/commands.ts | 37 +- 29 files changed, 364 insertions(+), 366 deletions(-) diff --git a/packages/ai-bot/lib/code-patch-correctness.ts b/packages/ai-bot/lib/code-patch-correctness.ts index 085622ebda0..473497908bd 100644 --- a/packages/ai-bot/lib/code-patch-correctness.ts +++ b/packages/ai-bot/lib/code-patch-correctness.ts @@ -9,19 +9,19 @@ import { APP_BOXEL_CODE_PATCH_CORRECTNESS_REL_TYPE, } from '@cardstack/runtime-common/matrix-constants'; import { - encodeCommandRequests, - type CommandRequest, + encodeToolRequests, + type ToolRequest, } from '@cardstack/runtime-common/commands'; import { MAX_CORRECTNESS_FIX_ATTEMPTS } from '@cardstack/runtime-common/ai/correctness-constants'; -export const CHECK_CORRECTNESS_COMMAND_NAME = 'checkCorrectness'; +export const CHECK_CORRECTNESS_TOOL_NAME = 'checkCorrectness'; export async function publishCodePatchCorrectnessMessage( summary: PendingCodePatchCorrectnessCheck, client: MatrixClient, ) { let body = ''; - let commandRequests = buildCheckCorrectnessCommandRequests(summary); + let toolRequests = buildCheckCorrectnessCommandRequests(summary); let baseContent = { body, msgtype: APP_BOXEL_CODE_PATCH_CORRECTNESS_MSGTYPE, @@ -46,17 +46,16 @@ export async function publishCodePatchCorrectnessMessage( if (Object.keys(data).length > 0) { content.data = data; } - if (commandRequests.length) { - content[APP_BOXEL_TOOL_REQUESTS_KEY] = - encodeCommandRequests(commandRequests); + if (toolRequests.length) { + content[APP_BOXEL_TOOL_REQUESTS_KEY] = encodeToolRequests(toolRequests); } await client.sendEvent(summary.roomId, 'm.room.message', content); } export function buildCheckCorrectnessCommandRequests( summary: PendingCodePatchCorrectnessCheck, -): Partial[] { - let requests: Partial[] = []; +): Partial[] { + let requests: Partial[] = []; let attemptsByTargetKey = summary.attemptsByTargetKey ?? {}; for (let file of summary.files) { let sourceRef = file.sourceUrl || file.displayName; @@ -70,7 +69,7 @@ export function buildCheckCorrectnessCommandRequests( let lintIssues = file.lintIssues; requests.push({ id: `check-${uuidv4()}`, - name: CHECK_CORRECTNESS_COMMAND_NAME, + name: CHECK_CORRECTNESS_TOOL_NAME, arguments: { description: `Check correctness of ${file.displayName}`, attributes: { @@ -94,7 +93,7 @@ export function buildCheckCorrectnessCommandRequests( } requests.push({ id: `check-${uuidv4()}`, - name: CHECK_CORRECTNESS_COMMAND_NAME, + name: CHECK_CORRECTNESS_TOOL_NAME, arguments: { description: `Check correctness of ${card.cardId}`, attributes: { diff --git a/packages/ai-bot/lib/matrix/response-publisher.ts b/packages/ai-bot/lib/matrix/response-publisher.ts index 70ddb866999..ec011067432 100644 --- a/packages/ai-bot/lib/matrix/response-publisher.ts +++ b/packages/ai-bot/lib/matrix/response-publisher.ts @@ -1,5 +1,5 @@ import type { ChatCompletionMessageFunctionToolCall } from 'openai/resources/chat/completions'; -import type { CommandRequest } from '@cardstack/runtime-common/commands'; +import type { ToolRequest } from '@cardstack/runtime-common/commands'; import { AI_BOT_EXECUTOR } from '@cardstack/runtime-common/commands'; import { READ_REALM_FILE_TOOL_NAME, @@ -21,9 +21,9 @@ let log = logger('ai-bot'); function toCommandRequest( toolCall: ChatCompletionMessageFunctionToolCall, -): Partial { +): Partial { let { id, function: f } = toolCall; - let result = {} as Partial; + let result = {} as Partial; if (id) { result['id'] = id; } diff --git a/packages/ai-bot/lib/responder.ts b/packages/ai-bot/lib/responder.ts index af2a287d29f..c265adb13dd 100644 --- a/packages/ai-bot/lib/responder.ts +++ b/packages/ai-bot/lib/responder.ts @@ -1,6 +1,6 @@ import { logger } from '@cardstack/runtime-common'; import { APP_BOXEL_CODE_PATCH_CORRECTNESS_MSGTYPE } from '@cardstack/runtime-common/matrix-constants'; -import { isCommandOrCodePatchResult } from '@cardstack/runtime-common/ai'; +import { isToolOrCodePatchResult } from '@cardstack/runtime-common/ai'; import { errorReporter } from './sentry.ts'; import type { OpenAIError } from 'openai/error'; @@ -35,7 +35,7 @@ export class Responder { } // If it's a command result or a code patch result, we might respond - if (isCommandOrCodePatchResult(event)) { + if (isToolOrCodePatchResult(event)) { return true; } @@ -45,7 +45,7 @@ export class Responder { static eventWillDefinitelyTriggerResponse(event: DiscreteMatrixEvent) { return ( - this.eventMayTriggerResponse(event) && !isCommandOrCodePatchResult(event) + this.eventMayTriggerResponse(event) && !isToolOrCodePatchResult(event) ); } diff --git a/packages/ai-bot/lib/set-title.ts b/packages/ai-bot/lib/set-title.ts index 8ef6b5a102a..41f44134fc2 100644 --- a/packages/ai-bot/lib/set-title.ts +++ b/packages/ai-bot/lib/set-title.ts @@ -3,9 +3,9 @@ import type OpenAI from 'openai'; import type { MatrixClient, IRoomEvent } from 'matrix-js-sdk'; import type { MatrixEvent as DiscreteMatrixEvent, - CommandResultWithOutputContent, - CommandResultWithNoOutputContent, - EncodedCommandRequest, + ToolResultWithOutputContent, + ToolResultWithNoOutputContent, + EncodedToolRequest, CodePatchResultContent, CardMessageContent, } from 'https://cardstack.com/base/matrix-event'; @@ -13,7 +13,7 @@ import type { ChatCompletionMessageParam } from 'openai/resources'; import { type OpenAIPromptMessage, isCodePatchResultStatusApplied, - isCommandResultStatusApplied, + isToolResultStatusApplied, attachedCardsToMessage, getRelevantCards, } from '@cardstack/runtime-common/ai'; @@ -106,8 +106,8 @@ export const getLatestResultMessage = ( return []; } let eventContent = event.getContent() as - | CommandResultWithOutputContent - | CommandResultWithNoOutputContent + | ToolResultWithOutputContent + | ToolResultWithNoOutputContent | CodePatchResultContent; let messageRelation: IEventRelation | undefined = eventContent['m.relates_to']; @@ -122,27 +122,23 @@ export const getLatestResultMessage = ( ); let commandRequestId = ( - eventContent as - | CommandResultWithOutputContent - | CommandResultWithNoOutputContent + eventContent as ToolResultWithOutputContent | ToolResultWithNoOutputContent ).commandRequestId; if (commandRequestId) { - let commandRequests = getToolRequests>( + let toolRequests = getToolRequests>( resultSourceEvent.content as CardMessageContent, ); - if (commandRequests) { - let commandRequest = commandRequests.find( - (cr: Partial) => { - return cr.id === commandRequestId; - }, - ); - if (!commandRequest) { + if (toolRequests) { + let toolRequest = toolRequests.find((cr: Partial) => { + return cr.id === commandRequestId; + }); + if (!toolRequest) { return []; } return [ { role: 'user', - content: `Applying tool call ${commandRequest.name} with args ${commandRequest.arguments}. Cards shared are: ${attachedCardsToMessage( + content: `Applying tool call ${toolRequest.name} with args ${toolRequest.arguments}. Cards shared are: ${attachedCardsToMessage( mostRecentlyAttachedCard, attachedCards, )}`, @@ -190,7 +186,7 @@ export function shouldSetRoomTitle( event?: MatrixEvent, ) { return ( - (isCommandResultStatusApplied(event) || + (isToolResultStatusApplied(event) || isCodePatchResultStatusApplied(event) || userAlreadyHasSentNMessages(rawEventLog, aiBotUserId)) && !roomTitleAlreadySet(rawEventLog) diff --git a/packages/ai-bot/main.ts b/packages/ai-bot/main.ts index 995758bdecc..acd1f96ff67 100644 --- a/packages/ai-bot/main.ts +++ b/packages/ai-bot/main.ts @@ -20,7 +20,7 @@ import { isRecognisedDebugCommand, getPromptParts, isInDebugMode, - isCommandResultStatusApplied, + isToolResultStatusApplied, getRoomEvents, sendPromptAsDebugMessage, constructHistory, @@ -827,7 +827,7 @@ Common issues are: if ( event.event.origin_server_ts! < startTime || !room || - !isCommandResultStatusApplied(event) + !isToolResultStatusApplied(event) ) { return; } diff --git a/packages/ai-bot/tests/chat-titling-test.ts b/packages/ai-bot/tests/chat-titling-test.ts index 78e4faed65f..dd553ce8a13 100644 --- a/packages/ai-bot/tests/chat-titling-test.ts +++ b/packages/ai-bot/tests/chat-titling-test.ts @@ -584,7 +584,7 @@ module('getLatestResultMessage', (hooks) => { ); // Create a command result event that references the command request - const commandResultEvent = { + const toolResultEvent = { getContent: () => ({ 'm.relates_to': { event_id: testEventId, @@ -600,7 +600,7 @@ module('getLatestResultMessage', (hooks) => { const result = getLatestResultMessage( history, '@aibot:localhost', - commandResultEvent, + toolResultEvent, ); // Verify the function returns the expected message @@ -649,7 +649,7 @@ module('getLatestResultMessage', (hooks) => { ]; // Create a command result event that references a non-existent command request - const commandResultEvent = { + const toolResultEvent = { getContent: () => ({ 'm.relates_to': { event_id: 'test-event-id', @@ -665,7 +665,7 @@ module('getLatestResultMessage', (hooks) => { const result = getLatestResultMessage( history, '@aibot:localhost', - commandResultEvent, + toolResultEvent, ); // Function should gracefully handle the missing command request and return an empty array @@ -724,7 +724,7 @@ module('getLatestResultMessage', (hooks) => { ]; // Create a command result event that references the second command request - const commandResultEvent = { + const toolResultEvent = { getContent: () => ({ 'm.relates_to': { event_id: testEventId, @@ -740,7 +740,7 @@ module('getLatestResultMessage', (hooks) => { const result = getLatestResultMessage( history, '@aibot:localhost', - commandResultEvent, + toolResultEvent, ); // Verify the function returns the expected message based on the correct command @@ -847,7 +847,7 @@ module('setTitle', () => { ]; // Create command result event - const commandResultEvent = { + const toolResultEvent = { getContent: () => ({ 'm.relates_to': { event_id: testEventId, @@ -866,7 +866,7 @@ module('setTitle', () => { 'test-room-id', history, '@aibot:localhost', - commandResultEvent, + toolResultEvent, ); // The assertions are inside the mock matrixClient.setRoomName function }); diff --git a/packages/ai-bot/tests/code-patch-correctness-test.ts b/packages/ai-bot/tests/code-patch-correctness-test.ts index e83fb5b420c..683ede37fdf 100644 --- a/packages/ai-bot/tests/code-patch-correctness-test.ts +++ b/packages/ai-bot/tests/code-patch-correctness-test.ts @@ -10,8 +10,8 @@ import { APP_BOXEL_TOOL_REQUESTS_KEY, } from '@cardstack/runtime-common/matrix-constants'; import { - decodeCommandRequest, - type CommandRequest, + decodeToolRequest, + type ToolRequest, } from '@cardstack/runtime-common/commands'; module('code patch correctness helpers', () => { @@ -82,10 +82,10 @@ module('code patch correctness helpers', () => { 'Should request correctness checks for each file and card', ); let decodedRequests = encodedRequests.map((request: any) => - decodeCommandRequest(request), + decodeToolRequest(request), ); let fileRequest = decodedRequests.find( - (request: Partial) => + (request: Partial) => request.arguments?.attributes?.targetType === 'file', ); assert.ok(fileRequest, 'Should include a file correctness request'); @@ -109,7 +109,7 @@ module('code patch correctness helpers', () => { 'File correctness check request should describe the file that changed', ); let cardRequest = decodedRequests.find( - (request: Partial) => + (request: Partial) => request.arguments?.attributes?.targetType === 'card', ); assert.ok(cardRequest, 'Should include a card correctness request'); @@ -152,11 +152,11 @@ module('code patch correctness helpers', () => { let [event] = client.getSentEvents(); let encodedRequests = event.content[APP_BOXEL_TOOL_REQUESTS_KEY]; let decodedRequests = encodedRequests.map((request: any) => - decodeCommandRequest(request), + decodeToolRequest(request), ); let fileRequest = decodedRequests.find( - (request: Partial) => + (request: Partial) => request.arguments?.attributes?.targetType === 'file', ); assert.strictEqual( @@ -171,7 +171,7 @@ module('code patch correctness helpers', () => { ); let cardRequest = decodedRequests.find( - (request: Partial) => + (request: Partial) => request.arguments?.attributes?.targetType === 'card', ); assert.strictEqual( diff --git a/packages/ai-bot/tests/prompt-construction-test.ts b/packages/ai-bot/tests/prompt-construction-test.ts index d5a6d91a4fb..b9d4f011198 100644 --- a/packages/ai-bot/tests/prompt-construction-test.ts +++ b/packages/ai-bot/tests/prompt-construction-test.ts @@ -6704,13 +6704,13 @@ module('set model in prompt', (hooks) => { assert.strictEqual(reasoningEffort, 'minimal'); }); - // Regression coverage for CS-11045: the host's `commandResult` may carry an + // Regression coverage for CS-11045: the host's `toolResult` may carry an // `m.relates_to.event_id` that disagrees with the bot message's canonical // event_id (the matrix server normalizes the bot message's event_id to the // last m.replace's id, while the host captured the streaming/original id). // ai-bot must still pair the result with the bot message via the // commandRequestId. - test('CS-11045: getCommandResults pairs by commandRequestId when m.relates_to.event_id drifts', async () => { + test('CS-11045: getToolResults pairs by commandRequestId when m.relates_to.event_id drifts', async () => { const NEW_EVENT_ID = '$NEW-canonical-id'; const OLD_EVENT_ID = '$OLD-streaming-id'; const TOOL_CALL_ID = 'tool-call-T1'; @@ -6805,7 +6805,7 @@ module('set model in prompt', (hooks) => { assert.equal( (toolMessages[0] as any).tool_call_id, TOOL_CALL_ID, - 'tool message tool_call_id should match the bot message commandRequest id', + 'tool message tool_call_id should match the bot message toolRequest id', ); }); @@ -6898,7 +6898,7 @@ module('set model in prompt', (hooks) => { assert.equal( (toolMessages[0] as any).tool_call_id, TOOL_CALL_ID, - 'tool message tool_call_id should match the bot message commandRequest id', + 'tool message tool_call_id should match the bot message toolRequest id', ); }); @@ -6984,7 +6984,7 @@ module('set model in prompt', (hooks) => { status: EventStatus.SENT, } as DiscreteMatrixEvent, // Bot message #2: write-text-file tool_call. The host emits its - // commandResult with m.relates_to.event_id pointing to the streaming + // toolResult with m.relates_to.event_id pointing to the streaming // id, which is NOT this bot message's canonical event_id. ✗ { type: 'm.room.message', @@ -7386,18 +7386,18 @@ module('markdown skill commands', (hooks) => { ].join('\n'); test('parseMarkdownSkill computes the same functionName the host derives', (assert) => { - let { commands } = parseMarkdownSkill(SKILL_MD, { + let { tools: commands } = parseMarkdownSkill(SKILL_MD, { sourceUrl: 'https://realm/skills/boxel-environment/SKILL.md', } as any); assert.strictEqual(commands.length, 1); - // switch-submode_dd88 is the name the host's buildCommandFunctionName + // switch-submode_dd88 is the name the host's buildToolFunctionName // produces for this code ref (registered prefixes resolve verbatim). assert.strictEqual(commands[0].functionName, 'switch-submode_dd88'); assert.false(commands[0].requiresApproval); }); test('parseMarkdownSkill reads the pre-rename boxel.commands key', (assert) => { - let { commands } = parseMarkdownSkill( + let { tools: commands } = parseMarkdownSkill( SKILL_MD.replace(' tools:', ' commands:'), { sourceUrl: 'https://realm/skills/boxel-environment/SKILL.md', @@ -7420,7 +7420,7 @@ module('markdown skill commands', (hooks) => { '---', 'body', ].join('\n'); - let { commands } = parseMarkdownSkill(md, { + let { tools: commands } = parseMarkdownSkill(md, { sourceUrl: 'https://realm/skills/my-skill/SKILL.md', } as any); assert.true(commands[0].requiresApproval); @@ -7439,7 +7439,7 @@ module('markdown skill commands', (hooks) => { '---', 'body', ].join('\n'); - let { commands } = parseMarkdownSkill(md, { + let { tools: commands } = parseMarkdownSkill(md, { sourceUrl: 'https://realm/skills/my-skill/SKILL.md', } as any); assert.strictEqual( @@ -7461,7 +7461,7 @@ module('markdown skill commands', (hooks) => { '---', 'body', ].join('\n'); - let { commands } = parseMarkdownSkill(md, { + let { tools: commands } = parseMarkdownSkill(md, { sourceUrl: 'https://realm/skills/my-skill/SKILL.md', } as any); assert.strictEqual( @@ -7526,14 +7526,14 @@ module('markdown skill commands', (hooks) => { ] as unknown as DiscreteMatrixEvent[]; // The card-shaped skill getEnabledSkills produces for this SKILL.md. - let { title, body, commands } = parseMarkdownSkill(SKILL_MD, { + let { title, body, tools } = parseMarkdownSkill(SKILL_MD, { sourceUrl: 'https://realm/skills/boxel-environment/SKILL.md', } as any); const enabledSkills = [ { id: 'https://realm/skills/boxel-environment/SKILL.md', type: 'card', - attributes: { title, instructions: body, commands }, + attributes: { title, instructions: body, tools }, } as unknown as LooseCardResource, ]; return { eventList, enabledSkills }; @@ -7551,7 +7551,7 @@ module('markdown skill commands', (hooks) => { assert.strictEqual(tools[0].function.name, 'switch-submode_dd88'); }); - test('getTools reads definitions from the pre-rename commandDefinitions state key', async () => { + test('getTools reads definitions from the pre-rename toolDefinitionFileDefs state key', async () => { const { eventList, enabledSkills } = skillsRoomFixture('commandDefinitions'); const tools = await getTools( diff --git a/packages/ai-bot/tests/responding-test.ts b/packages/ai-bot/tests/responding-test.ts index 62bba20adf9..e95ea113f2c 100644 --- a/packages/ai-bot/tests/responding-test.ts +++ b/packages/ai-bot/tests/responding-test.ts @@ -5,7 +5,7 @@ import { DEFAULT_EVENT_SIZE_MAX } from '../lib/matrix/response-publisher.ts'; import FakeTimers from '@sinonjs/fake-timers'; import { thinkingMessage } from '../constants.ts'; import type { ChatCompletionSnapshot } from 'openai/lib/ChatCompletionStream'; -import type { CommandRequest } from '@cardstack/runtime-common/commands'; +import type { ToolRequest } from '@cardstack/runtime-common/commands'; import { APP_BOXEL_REASONING_CONTENT_KEY, APP_BOXEL_TOOL_REQUESTS_KEY, @@ -57,21 +57,21 @@ function chunkWithReasoning( } function snapshotWithToolCall( - commandRequest: Partial, + toolRequest: Partial, ): ChatCompletionSnapshot { let toolCall = { type: 'function', } as any; - if (commandRequest.arguments) { + if (toolRequest.arguments) { toolCall.function = (toolCall.function ?? {}) as any; - toolCall.function.arguments = JSON.stringify(commandRequest.arguments); + toolCall.function.arguments = JSON.stringify(toolRequest.arguments); } - if (commandRequest.name) { + if (toolRequest.name) { toolCall.function = (toolCall.function ?? {}) as any; - toolCall.function.name = commandRequest.name; + toolCall.function.name = toolRequest.name; } - if (commandRequest.id) { - toolCall.id = commandRequest.id; + if (toolRequest.id) { + toolCall.id = toolRequest.id; } return { choices: [ diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts index 92bfd6b1cf3..a45af43570b 100644 --- a/packages/base/card-api.gts +++ b/packages/base/card-api.gts @@ -23,7 +23,7 @@ import { CardContextName, CardError, CodeRef, - CommandContext, + ToolContext, Deferred, byteStreamToUint8Array, fields, @@ -344,7 +344,7 @@ interface RelationshipOptions extends Options { } export interface CardContext { - commandContext?: CommandContext; + commandContext?: ToolContext; cardComponentModifier?: typeof Modifier<{ Args: { Named: { diff --git a/packages/base/menu-items.ts b/packages/base/menu-items.ts index 9101c9f752a..971a3815089 100644 --- a/packages/base/menu-items.ts +++ b/packages/base/menu-items.ts @@ -13,7 +13,7 @@ import PopulateWithSampleDataTool from '@cardstack/boxel-host/commands/populate- import ShowCardTool from '@cardstack/boxel-host/commands/show-card'; import SwitchSubmodeTool from '@cardstack/boxel-host/commands/switch-submode'; import type { - CommandContext, + ToolContext, Format, ResolvedCodeRef, } from '@cardstack/runtime-common'; @@ -51,7 +51,7 @@ type MenuContext = export type GetMenuItemParams = { canEdit: boolean; cardCrudFunctions: Partial; - commandContext: CommandContext; + commandContext: ToolContext; format?: Format; useBaseTemplate?: boolean; } & MenuContext; diff --git a/packages/base/resources/command-data.ts b/packages/base/resources/command-data.ts index 2b1f620c727..cdc7f9ca531 100644 --- a/packages/base/resources/command-data.ts +++ b/packages/base/resources/command-data.ts @@ -6,7 +6,7 @@ import type { CardInstance, } from '@cardstack/runtime-common'; -import type { CommandContext } from '@cardstack/runtime-common'; +import type { ToolContext } from '@cardstack/runtime-common'; import type { CardContext, CardDefConstructor } from '../card-api'; @@ -69,7 +69,7 @@ export class CommandExecutionState< export function commandData( parent: { args: { context?: CardContext | undefined } }, commandClass: new ( - context: CommandContext, + context: ToolContext, ) => Command, ): CommandExecutionState; export function commandData< @@ -78,7 +78,7 @@ export function commandData< >( parent: { args: { context?: CardContext | undefined } }, commandClass: new ( - context: CommandContext, + context: ToolContext, ) => Command, executeArgs: () => Parameters< Command['execute'] @@ -90,7 +90,7 @@ export function commandData< >( parent: { args: { context?: CardContext | undefined } }, commandClass: new ( - context: CommandContext, + context: ToolContext, ) => Command, executeArgs?: () => Parameters< Command['execute'] diff --git a/packages/base/spec.gts b/packages/base/spec.gts index 1045c4a184c..9b370c47861 100644 --- a/packages/base/spec.gts +++ b/packages/base/spec.gts @@ -35,7 +35,7 @@ import { loadCardDef, Loader, realmURL, - type CommandContext, + type ToolContext, type ResolvedCodeRef, } from '@cardstack/runtime-common'; import { eq, not, type MenuItemOptions } from '@cardstack/boxel-ui/helpers'; @@ -68,7 +68,7 @@ import { export type SpecType = 'card' | 'field' | 'component' | 'app' | 'command'; class PopulateFieldSpecExampleCommand extends PopulateWithSampleDataTool { - constructor(commandContext: CommandContext) { + constructor(commandContext: ToolContext) { super(commandContext); } protected get prompt() { @@ -98,7 +98,7 @@ class PopulateFieldSpecExampleCommand extends PopulateWithSampleDataTool { } class GenerateExamplesForFieldSpecCommand extends GenerateExampleCardsTool { - constructor(commandContext: CommandContext) { + constructor(commandContext: ToolContext) { super(commandContext); } protected getPrompt(count: number) { diff --git a/packages/host/app/components/operator-mode/create-listing-modal.gts b/packages/host/app/components/operator-mode/create-listing-modal.gts index efce3cf7b41..ccbba27ecef 100644 --- a/packages/host/app/components/operator-mode/create-listing-modal.gts +++ b/packages/host/app/components/operator-mode/create-listing-modal.gts @@ -23,7 +23,7 @@ import { isResolvedCodeRef, removeFileExtension, rri, - type CommandContext, + type ToolContext, type ResolvedCodeRef, type SearchEntryWireQuery, } from '@cardstack/runtime-common'; @@ -180,7 +180,7 @@ export default class CreateListingModal extends Component { // specifiers, so the catalog file isn't independently buildable. Going // through the loader is the only path that resolves both correctly. let module = await this.loaderService.loader.import<{ - default: new (commandContext: CommandContext) => { + default: new (commandContext: ToolContext) => { execute: (input: { codeRef: ResolvedCodeRef; targetRealm: string; diff --git a/packages/host/app/components/operator-mode/operator-mode-overlays.gts b/packages/host/app/components/operator-mode/operator-mode-overlays.gts index 8be0672d05c..ebc3d6048cc 100644 --- a/packages/host/app/components/operator-mode/operator-mode-overlays.gts +++ b/packages/host/app/components/operator-mode/operator-mode-overlays.gts @@ -33,7 +33,7 @@ import { import { cardTypeDisplayName, cardTypeIcon, - type CommandContext, + type ToolContext, } from '@cardstack/runtime-common'; import { @@ -86,7 +86,7 @@ export default class OperatorModeOverlays extends Overlays { declare private cardCrudFunctions: CardCrudFunctions; @consume(CommandContextName) - declare private commandContext: CommandContext; + declare private commandContext: ToolContext; get renderedCardsForOverlayActionsWithEvents() { return super diff --git a/packages/host/app/components/operator-mode/stack-item.gts b/packages/host/app/components/operator-mode/stack-item.gts index 2b858727b9b..b3063ce00ec 100644 --- a/packages/host/app/components/operator-mode/stack-item.gts +++ b/packages/host/app/components/operator-mode/stack-item.gts @@ -41,7 +41,7 @@ import { cn, cssVar, optional, not } from '@cardstack/boxel-ui/helpers'; import { IconLink, IconTrash } from '@cardstack/boxel-ui/icons'; -import type { CommandContext } from '@cardstack/runtime-common'; +import type { ToolContext } from '@cardstack/runtime-common'; import { type Permissions, type getCard, @@ -106,7 +106,7 @@ interface Signature { stackItems: StackItem[]; index: number; requestDeleteCard?: (card: CardDef | URL | string) => Promise; - commandContext: CommandContext; + commandContext: ToolContext; close: (item: StackItem) => void; dismissStackedCardsAbove: (stackIndex: number) => Promise; onSelectedCards: ( diff --git a/packages/host/app/components/operator-mode/stack.gts b/packages/host/app/components/operator-mode/stack.gts index 96c7a86bf77..e62bc416e8c 100644 --- a/packages/host/app/components/operator-mode/stack.gts +++ b/packages/host/app/components/operator-mode/stack.gts @@ -9,7 +9,7 @@ import { cn, eq, gte } from '@cardstack/boxel-ui/helpers'; import { CardCrudFunctionsContextName, - type CommandContext, + type ToolContext, } from '@cardstack/runtime-common'; import type { StackItem } from '@cardstack/host/lib/stack-item'; @@ -39,7 +39,7 @@ interface Signature { editCard: EditCardFn; saveCard: SaveCardFn; deleteCard: DeleteCardFn; - commandContext: CommandContext; + commandContext: ToolContext; close: (stackItem: StackItem) => void; onSelectedCards: ( selectedCards: CardDefOrId[], diff --git a/packages/host/app/lib/host-base-tool.ts b/packages/host/app/lib/host-base-tool.ts index 9a1bff973be..3c8bca625c9 100644 --- a/packages/host/app/lib/host-base-tool.ts +++ b/packages/host/app/lib/host-base-tool.ts @@ -1,7 +1,7 @@ import { getOwner, setOwner } from '@ember/-internals/owner'; import { service } from '@ember/service'; -import { Command, type CommandContext } from '@cardstack/runtime-common'; +import { Command, type ToolContext } from '@cardstack/runtime-common'; import { baseRealm } from '@cardstack/runtime-common'; @@ -14,7 +14,7 @@ export default abstract class HostBaseTool< CardInputType extends CardDefConstructor | undefined, CardResultType extends CardDefConstructor | undefined = undefined, > extends Command { - constructor(commandContext: CommandContext) { + constructor(commandContext: ToolContext) { super(commandContext); setOwner(this, getOwner(commandContext)!); } diff --git a/packages/host/app/routes/command-runner.ts b/packages/host/app/routes/command-runner.ts index c29960f9776..2e2569c6fcc 100644 --- a/packages/host/app/routes/command-runner.ts +++ b/packages/host/app/routes/command-runner.ts @@ -8,12 +8,12 @@ import { tracked } from '@glimmer/tracking'; import type { Command, - CommandContext, + ToolContext, CommandInvocation, ResolvedCodeRef, } from '@cardstack/runtime-common'; import { - CommandContextStamp, + ToolContextStamp, getClass, parseBoxelHostCommandSpecifier, rri, @@ -45,7 +45,7 @@ type GenericCommand = Command< CardDefConstructor >; type GenericCommandConstructor = { - new (context: CommandContext): GenericCommand; + new (context: ToolContext): GenericCommand; }; class CommandRunState implements CommandInvocation { @@ -119,10 +119,10 @@ export default class CommandRunnerRoute extends Route { return model; } - get commandContext(): CommandContext { + get commandContext(): ToolContext { let result = { - [CommandContextStamp]: true, - } as CommandContext; + [ToolContextStamp]: true, + } as ToolContext; setOwner(result, getOwner(this)!); return result; } diff --git a/packages/host/app/services/tool-service.ts b/packages/host/app/services/tool-service.ts index d0cefa043f9..3dc38375597 100644 --- a/packages/host/app/services/tool-service.ts +++ b/packages/host/app/services/tool-service.ts @@ -13,10 +13,10 @@ import { task, timeout, all } from 'ember-concurrency'; import { TrackedSet } from 'tracked-built-ins'; import { v4 as uuidv4 } from 'uuid'; -import type { Command, CommandContext } from '@cardstack/runtime-common'; +import type { Command, ToolContext } from '@cardstack/runtime-common'; import { Deferred, - CommandContextStamp, + ToolContextStamp, delay, getClass, identifyCard, @@ -624,9 +624,9 @@ export default class ToolService extends Service { } } - get commandContext(): CommandContext { + get commandContext(): ToolContext { let result = { - [CommandContextStamp]: true, + [ToolContextStamp]: true, }; setOwner(result, getOwner(this)!); @@ -707,7 +707,7 @@ export default class ToolService extends Service { let ToolConstructor = (await getClass( commandCodeRef, this.loaderService.loader, - )) as { new (context: CommandContext): Command }; + )) as { new (context: ToolContext): Command }; commandToRun = new ToolConstructor(this.commandContext); } @@ -815,7 +815,7 @@ export default class ToolService extends Service { let ToolConstructor = (await getClass( commandCodeRef, this.loaderService.loader, - )) as { new (context: CommandContext): Command }; + )) as { new (context: ToolContext): Command }; if (!ToolConstructor) { error = `No command for the name "${command.name}" was found`; } else { diff --git a/packages/host/app/tools/patch-card-instance.ts b/packages/host/app/tools/patch-card-instance.ts index 1fc0832cb21..386322cae12 100644 --- a/packages/host/app/tools/patch-card-instance.ts +++ b/packages/host/app/tools/patch-card-instance.ts @@ -1,6 +1,6 @@ import { service } from '@ember/service'; -import type { CommandContext } from '@cardstack/runtime-common'; +import type { ToolContext } from '@cardstack/runtime-common'; import { type AttributesSchema, type CardSchema, @@ -32,7 +32,7 @@ export default class PatchCardInstanceTool extends HostBaseTool< static actionVerb = 'Update Card'; constructor( - commandContext: CommandContext, + commandContext: ToolContext, private readonly configuration: Configuration, ) { super(commandContext); diff --git a/packages/host/app/tools/patch-fields.ts b/packages/host/app/tools/patch-fields.ts index 47fa7a4d7ff..63d464844be 100644 --- a/packages/host/app/tools/patch-fields.ts +++ b/packages/host/app/tools/patch-fields.ts @@ -1,6 +1,6 @@ import { service } from '@ember/service'; -import type { CommandContext } from '@cardstack/runtime-common'; +import type { ToolContext } from '@cardstack/runtime-common'; import { generateJsonSchemaForCardType, basicMappings, @@ -35,7 +35,7 @@ export default class PatchFieldsTool extends HostBaseTool< requireInputFields = ['cardId', 'fieldUpdates']; constructor( - commandContext: CommandContext, + commandContext: ToolContext, private readonly configuration?: Configuration, ) { super(commandContext); diff --git a/packages/host/app/tools/utils.ts b/packages/host/app/tools/utils.ts index 90a63f64429..c7a6a62c8ba 100644 --- a/packages/host/app/tools/utils.ts +++ b/packages/host/app/tools/utils.ts @@ -5,7 +5,7 @@ import { isToolResultEventType, isToolResultRelType, decodeCommandRequest, - type CommandContext, + type ToolContext, type ToolRequest, } from '@cardstack/runtime-common'; @@ -32,7 +32,7 @@ import type LoaderService from '../services/loader-service'; import type MessageService from '../services/message-service'; export async function waitForMatrixEvent( - commandContext: CommandContext, + commandContext: ToolContext, roomId: string, callback: (matrixEvents: MatrixEvent[]) => boolean, options: { timeoutMs?: number } = {}, @@ -64,7 +64,7 @@ export async function waitForMatrixEvent( } export async function waitForCompletedCommandRequest( - commandContext: CommandContext, + commandContext: ToolContext, roomId: string, commandRequestPredicate: (toolRequest: Partial) => boolean, options: { timeoutMs?: number; afterEventId?: string } = {}, @@ -120,7 +120,7 @@ export async function waitForCompletedCommandRequest( } export async function addPatchTools( - commandContext: CommandContext, + commandContext: ToolContext, patchableCards: CardDef[], cardAPI: typeof CardAPI, ): Promise { @@ -141,7 +141,7 @@ export async function addPatchTools( } export async function waitForRealmState( - commandContext: CommandContext, + commandContext: ToolContext, realmId: string, predicate: (ev: RealmEventContent | undefined) => boolean, options: { timeoutMs?: number; keepRealmSubscription?: boolean } = {}, diff --git a/packages/matrix/tests/commands.spec.ts b/packages/matrix/tests/commands.spec.ts index 079ac472f8c..66e255f3245 100644 --- a/packages/matrix/tests/commands.spec.ts +++ b/packages/matrix/tests/commands.spec.ts @@ -138,7 +138,7 @@ test.describe('Commands', () => { expect(boxelMessageData.context.tools).toMatchObject([]); }); - test(`applying a command dispatches a CommandResultEvent if command is succesful`, async ({ + test(`applying a command dispatches a ToolResultEvent if command is succesful`, async ({ page, }) => { const { username, password, credentials } = @@ -218,10 +218,10 @@ test.describe('Commands', () => { await expect(async () => { let events = await getRoomEvents(username, password, room1); - let commandResultEvent = (events as any).find( + let toolResultEvent = (events as any).find( (e: any) => e.type === APP_BOXEL_TOOL_RESULT_EVENT_TYPE, ); - await expect(commandResultEvent).toBeDefined(); + await expect(toolResultEvent).toBeDefined(); }).toPass(); }); @@ -274,7 +274,7 @@ test.describe('Commands', () => { await expect(async () => { let events = await getRoomEvents(username, password, room1); - let commandResultEvent = (events as any).find( + let toolResultEvent = (events as any).find( (e: any) => e.type === APP_BOXEL_TOOL_RESULT_EVENT_TYPE && e.content?.msgtype === APP_BOXEL_TOOL_RESULT_WITH_OUTPUT_MSGTYPE && @@ -284,11 +284,11 @@ test.describe('Commands', () => { e.content?.['m.relates_to']?.key === 'applied' && e.content?.['m.relates_to']?.event_id === messageEvent?.event_id, ); - await expect(commandResultEvent).toBeDefined(); + await expect(toolResultEvent).toBeDefined(); let commandResultData = - typeof commandResultEvent.content.data === 'string' - ? JSON.parse(commandResultEvent.content.data) - : commandResultEvent.content.data; + typeof toolResultEvent.content.data === 'string' + ? JSON.parse(toolResultEvent.content.data) + : toolResultEvent.content.data; await expect(commandResultData.card).toBeDefined(); }).toPass(); }); diff --git a/packages/matrix/tests/correctness-checks.spec.ts b/packages/matrix/tests/correctness-checks.spec.ts index bf9a278c0b1..899d094826d 100644 --- a/packages/matrix/tests/correctness-checks.spec.ts +++ b/packages/matrix/tests/correctness-checks.spec.ts @@ -99,7 +99,7 @@ test.describe('Correctness Checks', () => { ); // Simulate a correctness check message from the AI bot (same approach as commands.spec.ts) - let commandRequests = [ + let toolRequests = [ { id: commandRequestId, name: 'checkCorrectness', @@ -130,7 +130,7 @@ test.describe('Correctness Checks', () => { agentId, }, }, - [APP_BOXEL_TOOL_REQUESTS_KEY]: commandRequests, + [APP_BOXEL_TOOL_REQUESTS_KEY]: toolRequests, }, ); @@ -154,34 +154,34 @@ test.describe('Correctness Checks', () => { ).toContainText('Check correctness'); // Verify the command result event was dispatched with correct data - let commandResultEvent: any; + let toolResultEvent: any; await expect(async () => { let events = await getRoomEvents(username, password, roomId); - commandResultEvent = events.find( + toolResultEvent = events.find( (e: any) => e.type === APP_BOXEL_TOOL_RESULT_EVENT_TYPE, ); - expect(commandResultEvent).toBeDefined(); + expect(toolResultEvent).toBeDefined(); }).toPass(); // Verify the command result has the correct structure - expect(commandResultEvent!.content.msgtype).toStrictEqual( + expect(toolResultEvent!.content.msgtype).toStrictEqual( APP_BOXEL_TOOL_RESULT_WITH_OUTPUT_MSGTYPE, ); - expect(commandResultEvent!.content['m.relates_to']?.rel_type).toStrictEqual( + expect(toolResultEvent!.content['m.relates_to']?.rel_type).toStrictEqual( APP_BOXEL_TOOL_RESULT_REL_TYPE, ); - expect(commandResultEvent!.content['m.relates_to']?.key).toStrictEqual( + expect(toolResultEvent!.content['m.relates_to']?.key).toStrictEqual( 'applied', ); - expect(commandResultEvent!.content.commandRequestId).toStrictEqual( + expect(toolResultEvent!.content.commandRequestId).toStrictEqual( commandRequestId, ); // Verify the result contains a CorrectnessResultCard FileDef let commandResultData = - typeof commandResultEvent!.content.data === 'string' - ? JSON.parse(commandResultEvent!.content.data) - : commandResultEvent!.content.data; + typeof toolResultEvent!.content.data === 'string' + ? JSON.parse(toolResultEvent!.content.data) + : toolResultEvent!.content.data; expect(commandResultData.card).toBeDefined(); expect(commandResultData.card.contentType).toStrictEqual( 'application/vnd.card+json', @@ -632,7 +632,7 @@ ${brokenModuleContent} description: string, expectedApplyState: 'applied' | 'applied-with-error' = 'applied', ) { - let commandRequests = [ + let toolRequests = [ { id: commandRequestId, name: 'checkCorrectness', @@ -663,7 +663,7 @@ ${brokenModuleContent} agentId, }, }, - [APP_BOXEL_TOOL_REQUESTS_KEY]: commandRequests, + [APP_BOXEL_TOOL_REQUESTS_KEY]: toolRequests, }, ); @@ -675,21 +675,21 @@ ${brokenModuleContent} .locator(`[data-test-apply-state="${expectedApplyState}"]`) .waitFor(); - let commandResultEvent: any; + let toolResultEvent: any; await expect(async () => { let events = await getRoomEvents(username, password, roomId); - commandResultEvent = events.find( + toolResultEvent = events.find( (e: any) => e.type === APP_BOXEL_TOOL_RESULT_EVENT_TYPE && e.content.commandRequestId === commandRequestId, ); - expect(commandResultEvent).toBeDefined(); + expect(toolResultEvent).toBeDefined(); }).toPass(); let commandResultData = - typeof commandResultEvent!.content.data === 'string' - ? JSON.parse(commandResultEvent!.content.data) - : commandResultEvent!.content.data; + typeof toolResultEvent!.content.data === 'string' + ? JSON.parse(toolResultEvent!.content.data) + : toolResultEvent!.content.data; expect(commandResultData.card).toBeDefined(); expect(commandResultData.card.url).toBeDefined(); diff --git a/packages/runtime-common/ai/history.ts b/packages/runtime-common/ai/history.ts index d84e6ae4933..4d37f3cb88d 100644 --- a/packages/runtime-common/ai/history.ts +++ b/packages/runtime-common/ai/history.ts @@ -2,8 +2,8 @@ import type { CardMessageEvent, CardMessageContent, CodePatchResultEvent, - CommandResultEvent, - EncodedCommandRequest, + ToolResultEvent, + EncodedToolRequest, MatrixEvent as DiscreteMatrixEvent, MessageEvent, RealmServerEvent, @@ -61,7 +61,7 @@ export async function constructHistory( // make a copy of the event let event = { ...rawEvent } as | CardMessageEvent - | CommandResultEvent + | ToolResultEvent | CodePatchResultEvent | RealmServerEvent | MessageEvent; // Typescript could have inferred this from the line above @@ -111,11 +111,10 @@ export async function constructHistory( // carried, and drop the legacy key so the merged view has a single // source of truth. content[APP_BOXEL_TOOL_REQUESTS_KEY] = ( - getToolRequests>(content) ?? [] + getToolRequests>(content) ?? [] ).concat( - getToolRequests>( - continuationContent, - ) ?? [], + getToolRequests>(continuationContent) ?? + [], ); delete content[LEGACY_APP_BOXEL_COMMAND_REQUESTS_KEY]; event.origin_server_ts = continuationEvent.origin_server_ts; diff --git a/packages/runtime-common/ai/matrix-utils.ts b/packages/runtime-common/ai/matrix-utils.ts index c866d937e76..210d21ece10 100644 --- a/packages/runtime-common/ai/matrix-utils.ts +++ b/packages/runtime-common/ai/matrix-utils.ts @@ -7,8 +7,8 @@ import { } from '../constants.ts'; import { logger } from '../log.ts'; import { OpenAIError } from 'openai/error'; -import type { CommandRequest } from '../commands.ts'; -import { encodeCommandRequests } from '../commands.ts'; +import type { ToolRequest } from '../commands.ts'; +import { encodeToolRequests } from '../commands.ts'; import { APP_BOXEL_TOOL_REQUESTS_KEY, APP_BOXEL_RELOAD_BILLING_DATA_KEY, @@ -55,7 +55,7 @@ export async function sendMessageEvent( body: string, eventIdToReplace: string | undefined, data: any = {}, - commandRequests: Partial[] = [], + toolRequests: Partial[] = [], reasoning: string | undefined = undefined, ) { getLog().debug('sending message', body); @@ -65,7 +65,7 @@ export async function sendMessageEvent( msgtype: APP_BOXEL_MESSAGE_MSGTYPE, format: 'org.matrix.custom.html', [APP_BOXEL_REASONING_CONTENT_KEY]: reasoning, - [APP_BOXEL_TOOL_REQUESTS_KEY]: encodeCommandRequests(commandRequests), + [APP_BOXEL_TOOL_REQUESTS_KEY]: encodeToolRequests(toolRequests), }, ...data, }; @@ -425,7 +425,7 @@ export async function downloadFileAsBase64DataUrl( return `data:${contentType};base64,${base64}`; } -export function isCommandOrCodePatchResult( +export function isToolOrCodePatchResult( event: MatrixEvent | DiscreteMatrixEvent, ): boolean { let type = diff --git a/packages/runtime-common/ai/prompt.ts b/packages/runtime-common/ai/prompt.ts index 1d11a442df4..9a3d32577b6 100644 --- a/packages/runtime-common/ai/prompt.ts +++ b/packages/runtime-common/ai/prompt.ts @@ -14,7 +14,7 @@ import { downloadFile, downloadFileAsBase64DataUrl, extractCodePatchBlocks, - isCommandOrCodePatchResult, + isToolOrCodePatchResult, } from './matrix-utils.ts'; import { isRecognisedDebugCommand } from './debug.ts'; import { @@ -31,9 +31,9 @@ import type { CardMessageContent, CardMessageEvent, CodePatchResultEvent, - CommandResultEvent, + ToolResultEvent, MatrixEvent as DiscreteMatrixEvent, - EncodedCommandRequest, + EncodedToolRequest, MatrixEventWithBoxelContext, SkillsConfigEvent, Tool, @@ -61,10 +61,10 @@ import { DEFAULT_FALLBACK_MODEL_ID, } from '../matrix-constants.ts'; import { - buildCommandFunctionNameFromResolvedRef, - decodeCommandRequest, + buildToolFunctionNameFromResolvedRef, + decodeToolRequest, } from '../commands.ts'; -import type { CommandRequest } from '../commands.ts'; +import type { ToolRequest } from '../commands.ts'; import type { ReasoningEffort } from 'openai/resources/shared'; import type { CardResource, @@ -85,7 +85,7 @@ import { } from '../constants.ts'; const CARD_PATCH_COMMAND_NAMES = new Set(['patchCardInstance', 'patchFields']); -const CHECK_CORRECTNESS_COMMAND_NAME = 'checkCorrectness'; +const CHECK_CORRECTNESS_TOOL_NAME = 'checkCorrectness'; function getLog() { return logger('ai-bot:prompt'); @@ -159,11 +159,11 @@ function audioFormatFromMime(contentType: string): string | undefined { // ── Read-file command scope helpers ────────────────────────────────── -function isReadFileCommand(request?: CommandRequest): boolean { +function isReadFileCommand(request?: ToolRequest): boolean { return !!request?.name?.startsWith('read-file-for-ai-assistant'); } -function getReadFileUrl(request?: CommandRequest): string | undefined { +function getReadFileUrl(request?: ToolRequest): string | undefined { try { let args = typeof request?.arguments === 'string' @@ -271,7 +271,7 @@ function getShouldRespond(history: DiscreteMatrixEvent[]): boolean { // If the aibot is awaiting command or code patch results, it should not respond yet. let lastEventExcludingResults = findLast( history, - (event) => !isCommandOrCodePatchResult(event), + (event) => !isToolOrCodePatchResult(event), ); if (!lastEventExcludingResults) { @@ -286,8 +286,8 @@ function getShouldRespond(history: DiscreteMatrixEvent[]): boolean { return false; } - let commandRequests = - getToolRequests>( + let toolRequests = + getToolRequests>( lastEventExcludingResults.content as CardMessageContent, ) ?? []; let codePatchBlocks = extractCodePatchBlocks( @@ -296,15 +296,15 @@ function getShouldRespond(history: DiscreteMatrixEvent[]): boolean { let lastEventIndex = history.indexOf(lastEventExcludingResults); let recentEventsToCheck = history.slice(lastEventIndex + 1); - let allCommandsHaveResults = - commandRequests.length === 0 || - commandRequests.every((commandRequest: Partial) => { + let allToolsHaveResults = + toolRequests.length === 0 || + toolRequests.every((toolRequest: Partial) => { return recentEventsToCheck.some((event) => { return ( - isCommandResultEvent(event) && + isToolResultEvent(event) && (isToolResultWithOutputMsgtype(event.content.msgtype) || isToolResultWithNoOutputMsgtype(event.content.msgtype)) && - event.content.commandRequestId === commandRequest.id + event.content.commandRequestId === toolRequest.id ); }); }); @@ -318,16 +318,16 @@ function getShouldRespond(history: DiscreteMatrixEvent[]): boolean { ); }); }); - if (!allCommandsHaveResults || !allCodePatchesHaveResults) { + if (!allToolsHaveResults || !allCodePatchesHaveResults) { return false; } - if (!allCheckCorrectnessCommandsHaveResults(history)) { + if (!allCheckCorrectnessToolsHaveResults(history)) { return false; } return true; } -function allCheckCorrectnessCommandsHaveResults( +function allCheckCorrectnessToolsHaveResults( history: DiscreteMatrixEvent[], ): boolean { let lastEventWithCheckCorrectnessRequests = findLast(history, (event) => { @@ -349,7 +349,7 @@ function allCheckCorrectnessCommandsHaveResults( let subsequentEvents = history.slice(startIndex + 1); return checkCommandRequests.every((request) => { return subsequentEvents.some((event) => - isTerminalCommandResultEventFor(event, request.id!), + isTerminalToolResultEventFor(event, request.id!), ); }); } @@ -359,15 +359,15 @@ function shouldPromptCheckCorrectnessSummary( aiBotUserId: string, ) { let lastEvent = history[history.length - 1]; - if (!isCommandResultEvent(lastEvent)) { + if (!isToolResultEvent(lastEvent)) { return false; } - if (!isCheckCorrectnessCommandResultEvent(lastEvent, history)) { + if (!isCheckCorrectnessToolResultEvent(lastEvent, history)) { return false; } let lastNonResultIndex = findLastIndex( history, - (event) => !isCommandOrCodePatchResult(event), + (event) => !isToolOrCodePatchResult(event), ); if (lastNonResultIndex === -1) { return true; @@ -378,36 +378,36 @@ function shouldPromptCheckCorrectnessSummary( function getCheckCorrectnessCommandRequests( event: DiscreteMatrixEvent, -): CommandRequest[] { +): ToolRequest[] { if (!event || event.type !== 'm.room.message') { return []; } let encodedRequests = - getToolRequests>( + getToolRequests>( event.content as CardMessageContent, ) ?? []; if (!Array.isArray(encodedRequests)) { return []; } - let commandRequests: CommandRequest[] = []; + let toolRequests: ToolRequest[] = []; for (let encodedRequest of encodedRequests) { - let decoded = decodeCommandRequestSafe( - encodedRequest as Partial, + let decoded = decodeToolRequestSafe( + encodedRequest as Partial, ); - if (decoded?.id && decoded.name === CHECK_CORRECTNESS_COMMAND_NAME) { - commandRequests.push(decoded); + if (decoded?.id && decoded.name === CHECK_CORRECTNESS_TOOL_NAME) { + toolRequests.push(decoded); } } - return commandRequests; + return toolRequests; } -function decodeCommandRequestSafe( - request: Partial, -): CommandRequest | undefined { +function decodeToolRequestSafe( + request: Partial, +): ToolRequest | undefined { try { - let decoded = decodeCommandRequest(request); + let decoded = decodeToolRequest(request); if (decoded.id && decoded.name && decoded.arguments !== undefined) { - return decoded as CommandRequest; + return decoded as ToolRequest; } return undefined; } catch { @@ -415,12 +415,12 @@ function decodeCommandRequestSafe( } } -function isTerminalCommandResultEventFor( +function isTerminalToolResultEventFor( event: DiscreteMatrixEvent, commandRequestId: string, ): boolean { if ( - !isCommandResultEvent(event) || + !isToolResultEvent(event) || event.content.commandRequestId !== commandRequestId ) { return false; @@ -458,7 +458,7 @@ async function getEnabledSkills( // choke on non-JSON. The JSON.parse fallthrough is the legacy // pushed-card path; it is removed when the push model is retired. if (isMarkdownSkillFile(cardFileDef)) { - let { title, body, kind, commands } = parseMarkdownSkill( + let { title, body, kind, tools } = parseMarkdownSkill( content, cardFileDef, ); @@ -467,7 +467,7 @@ async function getEnabledSkills( } return { id: cardFileDef.sourceUrl, - attributes: { title, instructions: body, commands }, + attributes: { title, instructions: body, tools }, }; } return (JSON.parse(content) as LooseSingleCardDocument)?.data; @@ -486,6 +486,9 @@ export interface EnabledSkill { attributes?: { title?: string; instructions?: string; + tools?: { functionName: string }[]; + // Legacy Skill cards serialize their tools under the `commands` card + // field; read via the fallback in getTools. commands?: { functionName: string }[]; [key: string]: any; }; @@ -500,7 +503,7 @@ export function isMarkdownSkillFile(fileDef: SerializedFileDef): boolean { // (or the pre-rename `boxel.commands` key), with the functionName the host // derives for the same code ref — so getTools can match it against the room's // uploaded command definitions. -export interface MarkdownSkillCommand { +export interface MarkdownSkillTool { codeRef: { module: string; name: string }; functionName: string; requiresApproval: boolean; @@ -519,12 +522,12 @@ export function parseMarkdownSkill( title: string; body: string; kind?: string; - commands: MarkdownSkillCommand[]; + tools: MarkdownSkillTool[]; } { let body = content; let title: string | undefined; let kind: string | undefined; - let commands: MarkdownSkillCommand[] = []; + let tools: MarkdownSkillTool[] = []; try { let { data, body: parsedBody } = parseFrontmatter(content); body = parsedBody; @@ -538,7 +541,7 @@ export function parseMarkdownSkill( if (typeof boxel?.kind === 'string') { kind = boxel.kind; } - commands = markdownSkillCommands(data, fileDef.sourceUrl); + tools = markdownSkillTools(data, fileDef.sourceUrl); } catch { // Malformed frontmatter: keep the full content as the body. } @@ -546,7 +549,7 @@ export function parseMarkdownSkill( let path = fileDef.sourceUrl ?? fileDef.name ?? ''; title = path.split('/').filter(Boolean).pop() ?? 'Skill'; } - return { title, body: body.trim(), kind, commands }; + return { title, body: body.trim(), kind, tools }; } // Extracts `boxel.tools` (falling back to the pre-rename `boxel.commands` @@ -556,10 +559,10 @@ export function parseMarkdownSkill( // @cardstack/boxel-host/...) resolve verbatim on the host, so hashing the // literal module gives the same name; relative modules resolve against the // skill's own URL. -function markdownSkillCommands( +function markdownSkillTools( frontmatter: Record, skillUrl: string | undefined, -): MarkdownSkillCommand[] { +): MarkdownSkillTool[] { let boxel = frontmatter.boxel && typeof frontmatter.boxel === 'object' && @@ -574,7 +577,7 @@ function markdownSkillCommands( if (!entries) { return []; } - let commands: MarkdownSkillCommand[] = []; + let commands: MarkdownSkillTool[] = []; for (let entry of entries) { let codeRef = (entry as { codeRef?: { module?: string; name?: string } }) ?.codeRef; @@ -602,7 +605,7 @@ function markdownSkillCommands( let resolvedRef = { module, name: codeRef.name }; commands.push({ codeRef: resolvedRef, - functionName: buildCommandFunctionNameFromResolvedRef(resolvedRef), + functionName: buildToolFunctionNameFromResolvedRef(resolvedRef), // Missing means approval required, matching how the host treats an // absent requiresApproval on a skill command. requiresApproval: @@ -891,15 +894,15 @@ export async function getTools( client: MatrixClient, ): Promise { // Build map directly from messages - let enabledCommandNames = new Set(); + let enabledToolNames = new Set(); let toolMap = new Map(); // Get the list of all names from enabled skills for (let skill of enabledSkills) { - if (skill.attributes?.commands) { - let { commands } = skill.attributes; - for (let command of commands) { - enabledCommandNames.add(command.functionName); + let skillTools = skill.attributes?.tools ?? skill.attributes?.commands; + if (skillTools) { + for (let tool of skillTools) { + enabledToolNames.add(tool.functionName); } } } @@ -909,16 +912,16 @@ export async function getTools( (event) => event.type === APP_BOXEL_ROOM_SKILLS_EVENT_TYPE, ) as SkillsConfigEvent; - let commandDefinitions = + let toolDefinitionFileDefs = getToolDefinitions(skillsConfigEvent?.content) ?? []; - for (let commandDefinition of commandDefinitions) { - if (enabledCommandNames.has(commandDefinition.name)) { + for (let toolDefinitionFileDef of toolDefinitionFileDefs) { + if (enabledToolNames.has(toolDefinitionFileDef.name)) { let commandDefinitionContent = await downloadFile( client, - commandDefinition, + toolDefinitionFileDef, ); let commandDefinitionObject = JSON.parse(commandDefinitionContent); - toolMap.set(commandDefinition.name, commandDefinitionObject.tool); + toolMap.set(toolDefinitionFileDef.name, commandDefinitionObject.tool); } } @@ -939,7 +942,7 @@ export async function getTools( // Correctness commands should only be emitted by helper flows, not // directly via LLM tool calls. - toolMap.delete(CHECK_CORRECTNESS_COMMAND_NAME); + toolMap.delete(CHECK_CORRECTNESS_TOOL_NAME); return Array.from(toolMap.values()).sort((a, b) => a.function.name.localeCompare(b.function.name), @@ -984,27 +987,26 @@ export function getToolChoice( return 'auto'; } -function getCommandResults( +function getToolResults( cardMessageEvent: CardMessageEvent, history: DiscreteMatrixEvent[], ) { // CS-11045: pair tool_results with the bot message by commandRequestId in - // addition to m.relates_to.event_id. The host can emit a commandResult whose + // addition to m.relates_to.event_id. The host can emit a toolResult whose // m.relates_to.event_id is the streaming/original event_id while the matrix // server's /messages view normalizes the bot message to the latest m.replace // event_id. Without the commandRequestId fallback the result gets dropped, // leaving an orphan tool_use that Anthropic rejects. let requestIds = new Set( ( - getToolRequests>( - cardMessageEvent.content, - ) ?? [] + getToolRequests>(cardMessageEvent.content) ?? + [] ) .map((r) => r.id) .filter(Boolean) as string[], ); - let commandResultEvents = history.filter((e) => { - if (!isCommandResultEvent(e)) { + let toolResultEvents = history.filter((e) => { + if (!isToolResultEvent(e)) { return false; } if (e.content['m.relates_to']?.event_id === cardMessageEvent.event_id) { @@ -1014,15 +1016,15 @@ function getCommandResults( return true; } return false; - }) as CommandResultEvent[]; - return commandResultEvents; + }) as ToolResultEvent[]; + return toolResultEvents; } -function isCheckCorrectnessCommandResultEvent( - commandResultEvent: CommandResultEvent, +function isCheckCorrectnessToolResultEvent( + toolResultEvent: ToolResultEvent, history: DiscreteMatrixEvent[], ) { - let sourceEventId = commandResultEvent.content['m.relates_to']?.event_id; + let sourceEventId = toolResultEvent.content['m.relates_to']?.event_id; if (!sourceEventId) { return false; } @@ -1035,7 +1037,7 @@ function isCheckCorrectnessCommandResultEvent( return false; } return checkRequests.some( - (request) => request.id === commandResultEvent.content.commandRequestId, + (request) => request.id === toolResultEvent.content.commandRequestId, ); } @@ -1057,13 +1059,13 @@ function getCodePatchResults( function toToolCalls(event: CardMessageEvent): ChatCompletionMessageToolCall[] { const content = event.content as CardMessageContent; - return (getToolRequests>(content) ?? []).map( - (commandRequest: Partial) => { + return (getToolRequests>(content) ?? []).map( + (toolRequest: Partial) => { return { - id: commandRequest.id!, + id: toolRequest.id!, function: { - name: commandRequest.name!, - arguments: commandRequest.arguments!, + name: toolRequest.name!, + arguments: toolRequest.arguments!, }, type: 'function', }; @@ -1073,50 +1075,50 @@ function toToolCalls(event: CardMessageEvent): ChatCompletionMessageToolCall[] { async function toResultMessages( event: CardMessageEvent, - commandResults: CommandResultEvent[] = [], + toolResults: ToolResultEvent[] = [], client: MatrixClient, history: DiscreteMatrixEvent[], ): Promise { const messageContent = event.content as CardMessageContent; - let commandResultEntries = await Promise.all( - (getToolRequests>(messageContent) ?? []).map( - async (commandRequest: Partial) => { - let decodedCommandRequest = decodeCommandRequestSafe(commandRequest); - let commandResult = commandResults.find( - (commandResult) => - (isToolResultWithOutputMsgtype(commandResult.content.msgtype) || - isToolResultWithNoOutputMsgtype(commandResult.content.msgtype)) && - commandResult.content.commandRequestId === commandRequest.id, + let toolResultEntries = await Promise.all( + (getToolRequests>(messageContent) ?? []).map( + async (toolRequest: Partial) => { + let decodedToolRequest = decodeToolRequestSafe(toolRequest); + let toolResult = toolResults.find( + (toolResult) => + (isToolResultWithOutputMsgtype(toolResult.content.msgtype) || + isToolResultWithNoOutputMsgtype(toolResult.content.msgtype)) && + toolResult.content.commandRequestId === toolRequest.id, ); - if (!commandResult) { + if (!toolResult) { return undefined; } let content: string; let followUpUserMessage: string | undefined; - let status = commandResult.content['m.relates_to']?.key; + let status = toolResult.content['m.relates_to']?.key; let isCheckCorrectnessRequest = - decodedCommandRequest?.name === CHECK_CORRECTNESS_COMMAND_NAME; + decodedToolRequest?.name === CHECK_CORRECTNESS_TOOL_NAME; if (isCheckCorrectnessRequest) { let checkCorrectnessContent = buildCheckCorrectnessResultContent( - decodedCommandRequest, - commandResult, + decodedToolRequest, + toolResult, ); content = checkCorrectnessContent.toolMessage; followUpUserMessage = checkCorrectnessContent.followUpUserMessage; - } else if (isReadFileCommand(decodedCommandRequest)) { - let fileUrl = getReadFileUrl(decodedCommandRequest); + } else if (isReadFileCommand(decodedToolRequest)) { + let fileUrl = getReadFileUrl(decodedToolRequest); if (fileUrl && !isFileAttachedInRoom(fileUrl, history)) { content = `Tool call rejected: the file "${fileUrl}" was not previously attached in the room. Only files attached by the user can be read.\n`; } else { content = `Tool call ${status == 'applied' ? 'executed' : status}.\n`; } } else if ( - isToolResultWithOutputContent(commandResult.content) && - commandResult.content.data.card + isToolResultWithOutputContent(toolResult.content) && + toolResult.content.data.card ) { let cardContent = - commandResult.content.data.card.content ?? - commandResult.content.data.card.error; + toolResult.content.data.card.content ?? + toolResult.content.data.card.error; content = `Tool call ${status == 'applied' ? 'executed' : status}, with result card: ${cardContent}.\n`; } else { content = `Tool call ${status == 'applied' ? 'executed' : status}.\n`; @@ -1125,20 +1127,20 @@ async function toResultMessages( // partially fulfilled, e.g. a multi-file read where one file 404ed) // tool call reads as a bare status with nothing to act on. // Check-correctness results fold their reason in above. - let failureReason = commandResult.content.failureReason; + let failureReason = toolResult.content.failureReason; if (!isCheckCorrectnessRequest && failureReason) { content = `${content}${failureReason}\n`; } let attachmentResult = await buildAttachmentsMessagePart( client, - commandResult, + toolResult, history, true, ); content = [content, attachmentResult.text].filter(Boolean).join('\n\n'); let toolMessage: OpenAIPromptMessage = { role: 'tool', - tool_call_id: commandRequest.id, + tool_call_id: toolRequest.id, content, }; let followUpMessage = followUpUserMessage @@ -1152,12 +1154,12 @@ async function toResultMessages( ), ); let toolMessages = - commandResultEntries + toolResultEntries .map((entry) => entry?.toolMessage) .filter((message): message is OpenAIPromptMessage => Boolean(message)) ?? []; let followUpMessages = - commandResultEntries + toolResultEntries .map((entry) => entry?.followUpMessage) .filter((message): message is OpenAIPromptMessage => Boolean(message)) ?? []; @@ -1165,17 +1167,17 @@ async function toResultMessages( } function buildCheckCorrectnessResultContent( - request?: CommandRequest, - commandResult?: CommandResultEvent, + request?: ToolRequest, + toolResult?: ToolResultEvent, ): CheckCorrectnessResultContent { let targetDescription = describeCheckCorrectnessTarget(request); - if (!commandResult) { + if (!toolResult) { return { toolMessage: `Check correctness for ${targetDescription} is still pending.`, }; } - let status = commandResult.content['m.relates_to']?.key ?? 'unknown'; - let resultCard = extractCorrectnessResultCard(commandResult); + let status = toolResult.content['m.relates_to']?.key ?? 'unknown'; + let resultCard = extractCorrectnessResultCard(toolResult); if (resultCard) { let formattedSummary = formatCorrectnessResultSummary( targetDescription, @@ -1193,7 +1195,7 @@ function buildCheckCorrectnessResultContent( toolMessage: `Check correctness passed for ${targetDescription}.`, }; } - let failureReason = commandResult.content.failureReason; + let failureReason = toolResult.content.failureReason; if (failureReason) { return { toolMessage: `Check correctness was marked as ${status} for ${targetDescription}: ${failureReason}`, @@ -1204,7 +1206,7 @@ function buildCheckCorrectnessResultContent( }; } -function describeCheckCorrectnessTarget(request?: CommandRequest) { +function describeCheckCorrectnessTarget(request?: ToolRequest) { if (!request) { return 'the requested target'; } @@ -1246,12 +1248,12 @@ const CORRECTNESS_SUCCESS_SUMMARY_INSTRUCTION = const CORRECTNESS_FAILURE_LIMIT_INSTRUCTION = `Automated correctness fixes have already been attempted ${MAX_CORRECTNESS_FIX_ATTEMPTS} times and the target is still failing validation. Stop proposing further automated patches; instead, summarize the remaining errors and ask the user how they want to proceed. Do not mention correctness or automated checks or tool calls.`; function extractCorrectnessResultCard( - commandResult?: CommandResultEvent, + toolResult?: ToolResultEvent, ): CorrectnessResultSummary | undefined { - if (!commandResult || !isToolResultWithOutputContent(commandResult.content)) { + if (!toolResult || !isToolResultWithOutputContent(toolResult.content)) { return undefined; } - let cardPayload = commandResult.content.data.card; + let cardPayload = toolResult.content.data.card; if (!cardPayload) { return undefined; } @@ -1316,10 +1318,10 @@ function formatCorrectnessResultSummary( }; } -function findCheckCorrectnessCommandRequest( +function findCheckCorrectnessToolRequest( history: DiscreteMatrixEvent[], commandRequestId: string, -): CommandRequest | undefined { +): ToolRequest | undefined { for (let event of history) { let requests = getCheckCorrectnessCommandRequests(event); let match = requests.find((request) => request.id === commandRequestId); @@ -1350,7 +1352,7 @@ type CheckCorrectnessTargetParts = { }; function extractCheckCorrectnessTargetParts( - request?: CommandRequest, + request?: ToolRequest, ): CheckCorrectnessTargetParts { if (!request) { return {}; @@ -1369,7 +1371,7 @@ function extractCheckCorrectnessTargetParts( } function getCheckCorrectnessTargetKey( - request?: CommandRequest, + request?: ToolRequest, ): string | undefined { let { targetRef, targetType, targetEventId } = extractCheckCorrectnessTargetParts(request); @@ -1383,9 +1385,7 @@ function getCheckCorrectnessTargetKey( ); } -function getCorrectnessCheckAttemptFromRequest( - request?: CommandRequest, -): number { +function getCorrectnessCheckAttemptFromRequest(request?: ToolRequest): number { if (!request) { return 0; } @@ -1411,13 +1411,13 @@ function getLatestCorrectnessCheckAttemptInfo( if (!isToolResultEventType(event.type)) { continue; } - let commandResult = event as CommandResultEvent; - if (!isCheckCorrectnessCommandResultEvent(commandResult, history)) { + let toolResult = event as ToolResultEvent; + if (!isCheckCorrectnessToolResultEvent(toolResult, history)) { continue; } - let sourceRequest = findCheckCorrectnessCommandRequest( + let sourceRequest = findCheckCorrectnessToolRequest( history, - commandResult.content.commandRequestId, + toolResult.content.commandRequestId, ); if (!sourceRequest) { continue; @@ -1429,8 +1429,8 @@ function getLatestCorrectnessCheckAttemptInfo( 1, getCorrectnessCheckAttemptFromRequest(sourceRequest), ); - let resultCard = extractCorrectnessResultCard(commandResult); - let status = commandResult.content['m.relates_to']?.key; + let resultCard = extractCorrectnessResultCard(toolResult); + let status = toolResult.content['m.relates_to']?.key; let succeeded = status === 'applied' && Boolean(resultCard) && @@ -1511,13 +1511,13 @@ export async function buildPromptForModel( (event) => event.sender !== aiBotUserId && event.type === 'm.room.message' && - !isCommandOrCodePatchResult(event), + !isToolOrCodePatchResult(event), ); for (let event of history) { if (event.type !== 'm.room.message') { continue; } - if (isCommandOrCodePatchResult(event)) { + if (isToolOrCodePatchResult(event)) { continue; // we'll include these with the tool calls } if ( @@ -1545,14 +1545,11 @@ export async function buildPromptForModel( } historicalMessages.push(historicalMessage); } - let commandResults = getCommandResults( - event as CardMessageEvent, - history, - ); + let toolResults = getToolResults(event as CardMessageEvent, history); ( await toResultMessages( event as CardMessageEvent, - commandResults, + toolResults, client, history, ) @@ -1686,14 +1683,14 @@ function collectPendingCodePatchCorrectnessCheck( // Only consider messages that contain code patches or card patch commands. let content = event.content as CardMessageContent; let codePatchBlocks = extractCodePatchBlocks(content.body || ''); - let commandRequests = ( - getToolRequests>(content) ?? [] - ).map((request) => decodeCommandRequest(request)); - let relevantCommands = commandRequests.filter((request) => + let toolRequests = ( + getToolRequests>(content) ?? [] + ).map((request) => decodeToolRequest(request)); + let relevantTools = toolRequests.filter((request) => isCardPatchCommand(request.name), ); let hasRelevantChanges = - codePatchBlocks.length > 0 || relevantCommands.length > 0; + codePatchBlocks.length > 0 || relevantTools.length > 0; if (!hasRelevantChanges) { continue; } @@ -1702,13 +1699,13 @@ function collectPendingCodePatchCorrectnessCheck( event as CardMessageEvent, history, ); - let commandResults = getCommandResults(event as CardMessageEvent, history); + let toolResults = getToolResults(event as CardMessageEvent, history); let isCancelled = content.isCanceled || (event as any).status === 'cancelled'; let appliedChanges = hasAppliedChanges( codePatchResults, - relevantCommands, - commandResults, + relevantTools, + toolResults, ); if (isCancelled && !appliedChanges) { continue; @@ -1724,17 +1721,17 @@ function collectPendingCodePatchCorrectnessCheck( (result) => result.content.codeBlockIndex === index, ), ); - let allRelevantCommandsResolved = - relevantCommands.length === 0 || - relevantCommands.every((request) => - commandResults.some( + let allRelevantToolsResolved = + relevantTools.length === 0 || + relevantTools.every((request) => + toolResults.some( (result) => result.content.commandRequestId === request.id, ), ); // If the most recent message with patches/commands isn't resolved yet, // don't walk back to earlier messages—wait for the current one to finish. - if (!allCodePatchesResolved || !allRelevantCommandsResolved) { + if (!allCodePatchesResolved || !allRelevantToolsResolved) { return undefined; } @@ -1766,14 +1763,14 @@ function hasUnresolvedCodePatches( } let content = event.content as CardMessageContent; let codePatchBlocks = extractCodePatchBlocks(content.body || ''); - let commandRequests = ( - getToolRequests>(content) ?? [] - ).map((request) => decodeCommandRequest(request)); - let relevantCommands = commandRequests.filter((request) => + let toolRequests = ( + getToolRequests>(content) ?? [] + ).map((request) => decodeToolRequest(request)); + let relevantTools = toolRequests.filter((request) => isCardPatchCommand(request.name), ); let hasRelevantChanges = - codePatchBlocks.length > 0 || relevantCommands.length > 0; + codePatchBlocks.length > 0 || relevantTools.length > 0; if (!hasRelevantChanges) { continue; } @@ -1782,13 +1779,13 @@ function hasUnresolvedCodePatches( event as CardMessageEvent, history, ); - let commandResults = getCommandResults(event as CardMessageEvent, history); + let toolResults = getToolResults(event as CardMessageEvent, history); let isCancelled = content.isCanceled || (event as any).status === 'cancelled'; let appliedChanges = hasAppliedChanges( codePatchResults, - relevantCommands, - commandResults, + relevantTools, + toolResults, ); if (isCancelled && !appliedChanges) { return false; @@ -1802,15 +1799,15 @@ function hasUnresolvedCodePatches( result.content.codeBlockIndex === index, ), ); - let allRelevantCommandsResolved = - relevantCommands.length === 0 || - relevantCommands.every((request) => - commandResults.some( + let allRelevantToolsResolved = + relevantTools.length === 0 || + relevantTools.every((request) => + toolResults.some( (result) => result.content.commandRequestId === request.id, ), ); - return !(allCodePatchesResolved && allRelevantCommandsResolved); + return !(allCodePatchesResolved && allRelevantToolsResolved); } return false; } @@ -1821,14 +1818,14 @@ function buildCodePatchCorrectnessMessage( ): PendingCodePatchCorrectnessCheck | undefined { let content = messageEvent.content as CardMessageContent; let codePatchBlocks = extractCodePatchBlocks(content.body || ''); - let commandRequests = ( - getToolRequests>(content) ?? [] - ).map((request) => decodeCommandRequest(request)); - let relevantCommands = commandRequests.filter((request) => + let toolRequests = ( + getToolRequests>(content) ?? [] + ).map((request) => decodeToolRequest(request)); + let relevantTools = toolRequests.filter((request) => isCardPatchCommand(request.name), ); - if (codePatchBlocks.length === 0 && relevantCommands.length === 0) { + if (codePatchBlocks.length === 0 && relevantTools.length === 0) { return undefined; } @@ -1841,13 +1838,13 @@ function buildCodePatchCorrectnessMessage( } let codePatchResults = getCodePatchResults(messageEvent, history); - let commandResults = getCommandResults(messageEvent, history); + let toolResults = getToolResults(messageEvent, history); let isCancelled = content.isCanceled || (messageEvent as any).status === 'cancelled'; let appliedChanges = hasAppliedChanges( codePatchResults, - relevantCommands, - commandResults, + relevantTools, + toolResults, ); if (isCancelled && !appliedChanges) { return undefined; @@ -1863,20 +1860,20 @@ function buildCodePatchCorrectnessMessage( (result) => result.content.codeBlockIndex === index, ), ); - let allRelevantCommandsResolved = - relevantCommands.length === 0 || - relevantCommands.every((request) => - commandResults.some( + let allRelevantToolsResolved = + relevantTools.length === 0 || + relevantTools.every((request) => + toolResults.some( (result) => result.content.commandRequestId === request.id, ), ); - if (!allCodePatchesResolved || !allRelevantCommandsResolved) { + if (!allCodePatchesResolved || !allRelevantToolsResolved) { return undefined; } let files = gatherPatchedFiles(codePatchResults); - let cards = gatherPatchedCards(relevantCommands, commandResults); + let cards = gatherPatchedCards(relevantTools, toolResults); if (files.length === 0 && cards.length === 0) { return undefined; @@ -1971,14 +1968,14 @@ function mergeLintIssues( } function gatherPatchedCards( - commandRequests: Partial[], - commandResults: CommandResultEvent[], + toolRequests: Partial[], + toolResults: ToolResultEvent[], ): CodePatchCorrectnessCard[] { let cards: CodePatchCorrectnessCard[] = []; let seen = new Set(); - for (let request of commandRequests) { - let result = commandResults.find( - (commandResult) => commandResult.content.commandRequestId === request.id, + for (let request of toolRequests) { + let result = toolResults.find( + (toolResult) => toolResult.content.commandRequestId === request.id, ); if (!result) { continue; @@ -2029,7 +2026,7 @@ function buildCorrectnessCheckAttemptMap( } function extractCardIdFromCommandRequest( - request: Partial, + request: Partial, ): string | undefined { let args = request.arguments as Record | undefined; if (!args) { @@ -2085,8 +2082,8 @@ function formatFileDisplayName(identifier?: string) { function hasAppliedChanges( codePatchResults: CodePatchResultEvent[], - relevantCommands: Partial[], - commandResults: CommandResultEvent[], + relevantTools: Partial[], + toolResults: ToolResultEvent[], ): boolean { if ( codePatchResults.some( @@ -2096,8 +2093,8 @@ function hasAppliedChanges( return true; } - return relevantCommands.some((request) => - commandResults.some( + return relevantTools.some((request) => + toolResults.some( (result) => result.content.commandRequestId === request.id && result.content['m.relates_to']?.key === 'applied', @@ -2245,11 +2242,7 @@ export const buildContextMessage = async ( isToolResultEventType(ev.type) || ev.type === APP_BOXEL_CODE_PATCH_RESULT_EVENT_TYPE ); - }) as - | CardMessageEvent - | CommandResultEvent - | CodePatchResultEvent - | undefined; + }) as CardMessageEvent | ToolResultEvent | CodePatchResultEvent | undefined; let context = lastEventWithContext?.content.data?.context; // Extract room ID from any event in history @@ -2457,12 +2450,12 @@ export function cleanContent(content: string) { return content.trim(); } -export const isCommandResultStatusApplied = (event?: MatrixEvent) => { +export const isToolResultStatusApplied = (event?: MatrixEvent) => { if (event === undefined) { return false; } return ( - isCommandResultEvent(event.event as DiscreteMatrixEvent) && + isToolResultEvent(event.event as DiscreteMatrixEvent) && event.getContent()['m.relates_to']?.key === 'applied' ); }; @@ -2542,16 +2535,16 @@ function normalizeReasoningEffort( return undefined; } -export function isCommandResultEvent( +export function isToolResultEvent( event?: DiscreteMatrixEvent, -): event is CommandResultEvent { +): event is ToolResultEvent { if (event === undefined) { return false; } return ( isToolResultEventType(event.type) && isToolResultRelType( - (event as CommandResultEvent).content['m.relates_to']?.rel_type, + (event as ToolResultEvent).content['m.relates_to']?.rel_type, ) ); } diff --git a/packages/runtime-common/commands.ts b/packages/runtime-common/commands.ts index 03066ef9717..122c34460f6 100644 --- a/packages/runtime-common/commands.ts +++ b/packages/runtime-common/commands.ts @@ -28,12 +28,12 @@ export interface ToolRequest { executedBy?: string; } -export const CommandContextStamp = Symbol.for('CommandContext'); -export interface CommandContext { - [CommandContextStamp]: boolean; +export const ToolContextStamp = Symbol.for('CommandContext'); +export interface ToolContext { + [ToolContextStamp]: boolean; } -export interface CommandInvocation { +export interface ToolInvocation { cardResult: CardInstance | null; error: Error | null; status: 'pending' | 'success' | 'error'; @@ -60,9 +60,9 @@ export abstract class Tool< name: string = this.constructor.name; description = ''; - protected readonly commandContext: CommandContext; + protected readonly commandContext: ToolContext; - constructor(commandContext: CommandContext) { + constructor(commandContext: ToolContext) { this.commandContext = commandContext; } @@ -135,12 +135,12 @@ function friendlyModuleName(fullModuleUrl: string) { .replace(/\.gts$/, ''); } -export function buildCommandFunctionName( +export function buildToolFunctionName( commandCodeRef: ResolvedCodeRef, relativeTo: RealmResourceIdentifier | URL | undefined, // Optional: omit to resolve the code ref in RRI space (no VirtualNetwork). // `functionName` is a recomputed `computeVia` field (never persisted), and - // `buildCommandFunctionName` is its only producer, so dropping the VN keeps + // `buildToolFunctionName` is its only producer, so dropping the VN keeps // every command name self-consistent. virtualNetwork?: VirtualNetwork, ) { @@ -154,7 +154,7 @@ export function buildCommandFunctionName( virtualNetwork, ) as ResolvedCodeRef; - return buildCommandFunctionNameFromResolvedRef(absoluteCodeRef); + return buildToolFunctionNameFromResolvedRef(absoluteCodeRef); } // The host tool modules were published as `@cardstack/boxel-host/commands/*` @@ -179,11 +179,11 @@ export function moduleForFunctionNameHash(module: string): string { return module; } -// The name-construction half of buildCommandFunctionName, for callers that +// The name-construction half of buildToolFunctionName, for callers that // already hold an absolute code ref (registered package prefixes resolve // verbatim, so e.g. ai-bot can produce identical names without a // VirtualNetwork). -export function buildCommandFunctionNameFromResolvedRef(ref: { +export function buildToolFunctionNameFromResolvedRef(ref: { module: string; name: string; }): string { @@ -197,7 +197,7 @@ export function buildCommandFunctionNameFromResolvedRef(ref: { return `${name}_${hashed.slice(0, 4)}`; } -export function decodeCommandRequest( +export function decodeToolRequest( commandRequest: Partial, ): Partial { if (typeof commandRequest.arguments === 'object') { @@ -251,7 +251,7 @@ export function encodeCommandRequest( return encodedCommandRequest; } -export function encodeCommandRequests( +export function encodeToolRequests( commandRequests: Partial[], ): Partial[] { return commandRequests.map(encodeCommandRequest); @@ -259,3 +259,14 @@ export function encodeCommandRequests( // Pre-rename spelling; new code imports `ToolRequest`. export type CommandRequest = ToolRequest; + +// Pre-rename spellings. Realm content and out-of-tree code import these; +// they stay until the content window (CS-12042) confirms nothing does. +export type CommandContext = ToolContext; +export type CommandInvocation = ToolInvocation; +export const CommandContextStamp = ToolContextStamp; +export const decodeCommandRequest = decodeToolRequest; +export const encodeCommandRequests = encodeToolRequests; +export const buildCommandFunctionName = buildToolFunctionName; +export const buildCommandFunctionNameFromResolvedRef = + buildToolFunctionNameFromResolvedRef; From fa2210c0a4ab5550585f911fd6395adbfbe824a2 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Thu, 9 Jul 2026 20:58:54 -0400 Subject: [PATCH 2/2] Address PR review: finish correctness-helper renames and fix test strings getCheckCorrectnessToolRequests / buildCheckCorrectnessToolRequests match what they return; the legacy-state-key test title names the commandDefinitions key it actually exercises; "successful" typo fixed in the matrix specs. Co-Authored-By: Claude Fable 5 --- packages/ai-bot/lib/code-patch-correctness.ts | 4 ++-- packages/ai-bot/tests/prompt-construction-test.ts | 2 +- packages/matrix/tests/commands.spec.ts | 4 ++-- packages/runtime-common/ai/prompt.ts | 14 +++++++------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/ai-bot/lib/code-patch-correctness.ts b/packages/ai-bot/lib/code-patch-correctness.ts index 473497908bd..adac75c80ba 100644 --- a/packages/ai-bot/lib/code-patch-correctness.ts +++ b/packages/ai-bot/lib/code-patch-correctness.ts @@ -21,7 +21,7 @@ export async function publishCodePatchCorrectnessMessage( client: MatrixClient, ) { let body = ''; - let toolRequests = buildCheckCorrectnessCommandRequests(summary); + let toolRequests = buildCheckCorrectnessToolRequests(summary); let baseContent = { body, msgtype: APP_BOXEL_CODE_PATCH_CORRECTNESS_MSGTYPE, @@ -52,7 +52,7 @@ export async function publishCodePatchCorrectnessMessage( await client.sendEvent(summary.roomId, 'm.room.message', content); } -export function buildCheckCorrectnessCommandRequests( +export function buildCheckCorrectnessToolRequests( summary: PendingCodePatchCorrectnessCheck, ): Partial[] { let requests: Partial[] = []; diff --git a/packages/ai-bot/tests/prompt-construction-test.ts b/packages/ai-bot/tests/prompt-construction-test.ts index b9d4f011198..aaf9c1bc5e6 100644 --- a/packages/ai-bot/tests/prompt-construction-test.ts +++ b/packages/ai-bot/tests/prompt-construction-test.ts @@ -7551,7 +7551,7 @@ module('markdown skill commands', (hooks) => { assert.strictEqual(tools[0].function.name, 'switch-submode_dd88'); }); - test('getTools reads definitions from the pre-rename toolDefinitionFileDefs state key', async () => { + test('getTools reads definitions from the pre-rename commandDefinitions state key', async () => { const { eventList, enabledSkills } = skillsRoomFixture('commandDefinitions'); const tools = await getTools( diff --git a/packages/matrix/tests/commands.spec.ts b/packages/matrix/tests/commands.spec.ts index 66e255f3245..160b41b2ea8 100644 --- a/packages/matrix/tests/commands.spec.ts +++ b/packages/matrix/tests/commands.spec.ts @@ -138,7 +138,7 @@ test.describe('Commands', () => { expect(boxelMessageData.context.tools).toMatchObject([]); }); - test(`applying a command dispatches a ToolResultEvent if command is succesful`, async ({ + test(`applying a command dispatches a ToolResultEvent if command is successful`, async ({ page, }) => { const { username, password, credentials } = @@ -225,7 +225,7 @@ test.describe('Commands', () => { }).toPass(); }); - test(`applying a search command dispatches a result event if command is succesful and result is returned`, async ({ + test(`applying a search command dispatches a result event if command is successful and result is returned`, async ({ page, }) => { const { username, password, credentials } = diff --git a/packages/runtime-common/ai/prompt.ts b/packages/runtime-common/ai/prompt.ts index 9a3d32577b6..a37d6632d13 100644 --- a/packages/runtime-common/ai/prompt.ts +++ b/packages/runtime-common/ai/prompt.ts @@ -331,23 +331,23 @@ function allCheckCorrectnessToolsHaveResults( history: DiscreteMatrixEvent[], ): boolean { let lastEventWithCheckCorrectnessRequests = findLast(history, (event) => { - return getCheckCorrectnessCommandRequests(event).length > 0; + return getCheckCorrectnessToolRequests(event).length > 0; }); if (!lastEventWithCheckCorrectnessRequests) { return true; } - let checkCommandRequests = getCheckCorrectnessCommandRequests( + let checkToolRequests = getCheckCorrectnessToolRequests( lastEventWithCheckCorrectnessRequests, ); - if (checkCommandRequests.length === 0) { + if (checkToolRequests.length === 0) { return true; } let startIndex = history.indexOf(lastEventWithCheckCorrectnessRequests); let subsequentEvents = history.slice(startIndex + 1); - return checkCommandRequests.every((request) => { + return checkToolRequests.every((request) => { return subsequentEvents.some((event) => isTerminalToolResultEventFor(event, request.id!), ); @@ -376,7 +376,7 @@ function shouldPromptCheckCorrectnessSummary( return lastNonResultEvent.sender === aiBotUserId; } -function getCheckCorrectnessCommandRequests( +function getCheckCorrectnessToolRequests( event: DiscreteMatrixEvent, ): ToolRequest[] { if (!event || event.type !== 'm.room.message') { @@ -1032,7 +1032,7 @@ function isCheckCorrectnessToolResultEvent( if (!sourceEvent) { return false; } - let checkRequests = getCheckCorrectnessCommandRequests(sourceEvent); + let checkRequests = getCheckCorrectnessToolRequests(sourceEvent); if (checkRequests.length === 0) { return false; } @@ -1323,7 +1323,7 @@ function findCheckCorrectnessToolRequest( commandRequestId: string, ): ToolRequest | undefined { for (let event of history) { - let requests = getCheckCorrectnessCommandRequests(event); + let requests = getCheckCorrectnessToolRequests(event); let match = requests.find((request) => request.id === commandRequestId); if (match) { return match;