mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
fix: route agent slash through normal chat (#653)
/agent should behave like a normal session prompt that tells the main model to use the selected Agent tool. The previous forked command path buffered the subagent run behind local command output and forced desktop clients to wait for command completion instead of seeing the normal Agent interaction stream. Constraint: Desktop /agent must preserve ordinary chat/session semantics and still require /agent <agent> <prompt>. Rejected: Keep slash_agent SDK event bridging | it preserves the separate forked execution path instead of fixing the product flow. Confidence: high Scope-risk: moderate Directive: Do not move /agent back to context: fork without proving the desktop normal Agent tool stream still works. Tested: bun test src/commands/agent.test.ts src/utils/processUserInput/processSlashCommand.test.ts src/server/__tests__/ws-memory-events.test.ts Tested: cd desktop && bun run test -- --run src/stores/chatStore.test.ts src/components/chat/MessageList.test.tsx Tested: git diff --check Not-tested: bun run check:server is blocked before source checks by expired quarantine entries: server:cron-scheduler, server:providers-real, server:tasks, server:e2e:business-flow, server:e2e:full-flow
This commit is contained in:
parent
54f3c00414
commit
349955dbf2
@ -1,125 +0,0 @@
|
||||
import { beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { setIsInteractive } from './bootstrap/state.js'
|
||||
import { drainSdkEvents, enqueueSdkEvent } from './utils/sdkEventQueue.js'
|
||||
|
||||
process.env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY ?? 'test-key'
|
||||
|
||||
type Deferred<T> = {
|
||||
promise: Promise<T>
|
||||
resolve: (value: T) => void
|
||||
}
|
||||
|
||||
function createDeferred<T>(): Deferred<T> {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>(value => {
|
||||
resolve = value
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
const processUserInputMock = mock(async () => ({
|
||||
messages: [],
|
||||
shouldQuery: false,
|
||||
resultText: 'done',
|
||||
}))
|
||||
|
||||
mock.module('./utils/processUserInput/processUserInput.js', () => ({
|
||||
processUserInput: processUserInputMock,
|
||||
}))
|
||||
|
||||
mock.module('./utils/queryContext.js', () => ({
|
||||
fetchSystemPromptParts: mock(async () => ({
|
||||
defaultSystemPrompt: [],
|
||||
userContext: {},
|
||||
systemContext: {},
|
||||
})),
|
||||
}))
|
||||
|
||||
mock.module('./utils/messages/systemInit.js', () => ({
|
||||
buildSystemInitMessage: mock(() => ({
|
||||
type: 'system',
|
||||
subtype: 'init',
|
||||
session_id: 'test-session',
|
||||
uuid: 'init-message',
|
||||
model: 'test-model',
|
||||
})),
|
||||
sdkCompatToolName: (name: string) => name,
|
||||
}))
|
||||
|
||||
const { QueryEngine } = await import('./QueryEngine.js')
|
||||
|
||||
function makeEngine() {
|
||||
return new QueryEngine({
|
||||
cwd: process.cwd(),
|
||||
tools: [],
|
||||
commands: [],
|
||||
mcpClients: [],
|
||||
agents: [],
|
||||
canUseTool: async () => ({ behavior: 'allow' }) as never,
|
||||
getAppState: () => ({
|
||||
fastMode: false,
|
||||
mcp: { clients: [], tools: [] },
|
||||
toolPermissionContext: {
|
||||
mode: 'default',
|
||||
alwaysAllowRules: { command: [] },
|
||||
additionalWorkingDirectories: new Map(),
|
||||
},
|
||||
}) as never,
|
||||
setAppState: () => {},
|
||||
readFileCache: new Map() as never,
|
||||
})
|
||||
}
|
||||
|
||||
function timeout<T>(ms: number, value: T): Promise<T> {
|
||||
return new Promise(resolve => setTimeout(() => resolve(value), ms))
|
||||
}
|
||||
|
||||
describe('QueryEngine slash command event streaming', () => {
|
||||
beforeEach(() => {
|
||||
processUserInputMock.mockClear()
|
||||
drainSdkEvents()
|
||||
setIsInteractive(false)
|
||||
})
|
||||
|
||||
test('drains SDK task events while pre-query slash command processing is still pending', async () => {
|
||||
const deferred = createDeferred<{
|
||||
messages: []
|
||||
shouldQuery: false
|
||||
resultText: string
|
||||
}>()
|
||||
processUserInputMock.mockImplementationOnce(async () => {
|
||||
enqueueSdkEvent({
|
||||
type: 'system',
|
||||
subtype: 'task_started',
|
||||
task_id: 'slash-agent-task',
|
||||
description: 'Agent Explore',
|
||||
task_type: 'slash_agent',
|
||||
prompt: 'inspect recent changes',
|
||||
})
|
||||
return deferred.promise
|
||||
})
|
||||
|
||||
const iterator = makeEngine().submitMessage('/agent Explore inspect recent changes')
|
||||
const first = await Promise.race([
|
||||
iterator.next(),
|
||||
timeout(350, null),
|
||||
])
|
||||
|
||||
expect(first).not.toBeNull()
|
||||
expect(first?.value).toMatchObject({
|
||||
type: 'system',
|
||||
subtype: 'task_started',
|
||||
task_id: 'slash-agent-task',
|
||||
description: 'Agent Explore',
|
||||
})
|
||||
|
||||
deferred.resolve({
|
||||
messages: [],
|
||||
shouldQuery: false,
|
||||
resultText: 'done',
|
||||
})
|
||||
for await (const _message of iterator) {
|
||||
// Drain the generator after releasing the pending slash command.
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -74,12 +74,10 @@ import {
|
||||
} from './utils/processUserInput/processUserInput.js'
|
||||
import { fetchSystemPromptParts } from './utils/queryContext.js'
|
||||
import { setCwd } from './utils/Shell.js'
|
||||
import { drainSdkEvents } from './utils/sdkEventQueue.js'
|
||||
import {
|
||||
flushSessionStorage,
|
||||
recordTranscript,
|
||||
} from './utils/sessionStorage.js'
|
||||
import { sleep } from './utils/sleep.js'
|
||||
import { asSystemPrompt } from './utils/systemPromptType.js'
|
||||
import { resolveThemeSetting } from './utils/systemTheme.js'
|
||||
import {
|
||||
@ -122,8 +120,6 @@ const getCoordinatorUserContext: (
|
||||
: () => ({})
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
const PRE_QUERY_SDK_EVENT_DRAIN_INTERVAL_MS = 100
|
||||
|
||||
// Dead code elimination: conditional import for snip compaction
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const snipModule = feature('HISTORY_SNIP')
|
||||
@ -414,7 +410,13 @@ export class QueryEngine {
|
||||
}
|
||||
}
|
||||
|
||||
const processUserInputPromise = processUserInput({
|
||||
const {
|
||||
messages: messagesFromUserInput,
|
||||
shouldQuery,
|
||||
allowedTools,
|
||||
model: modelFromUserInput,
|
||||
resultText,
|
||||
}: ProcessUserInputBaseResult = await processUserInput({
|
||||
input: prompt,
|
||||
mode: 'prompt',
|
||||
setToolJSX: () => {},
|
||||
@ -427,36 +429,6 @@ export class QueryEngine {
|
||||
isMeta: options?.isMeta,
|
||||
querySource: 'sdk',
|
||||
})
|
||||
.then(
|
||||
value => ({ ok: true as const, value }),
|
||||
error => ({ ok: false as const, error }),
|
||||
)
|
||||
|
||||
let processUserInputOutcome: Awaited<typeof processUserInputPromise> | null = null
|
||||
while (!processUserInputOutcome) {
|
||||
const outcome = await Promise.race([
|
||||
processUserInputPromise,
|
||||
sleep(PRE_QUERY_SDK_EVENT_DRAIN_INTERVAL_MS).then(() => null),
|
||||
])
|
||||
for (const event of drainSdkEvents()) {
|
||||
yield event
|
||||
}
|
||||
if (outcome) {
|
||||
processUserInputOutcome = outcome
|
||||
}
|
||||
}
|
||||
|
||||
if (!processUserInputOutcome.ok) {
|
||||
throw processUserInputOutcome.error
|
||||
}
|
||||
|
||||
const {
|
||||
messages: messagesFromUserInput,
|
||||
shouldQuery,
|
||||
allowedTools,
|
||||
model: modelFromUserInput,
|
||||
resultText,
|
||||
}: ProcessUserInputBaseResult = processUserInputOutcome.value
|
||||
|
||||
// Push new messages, including user input and any attachments
|
||||
this.mutableMessages.push(...messagesFromUserInput)
|
||||
|
||||
@ -14,9 +14,23 @@ describe('/agent command', () => {
|
||||
expect(parseAgentCommandArgs('debugger')).toBeNull()
|
||||
})
|
||||
|
||||
test('passes only the prompt body to forked execution', async () => {
|
||||
test('instructs the normal chat loop to use the selected agent', async () => {
|
||||
await expect(agentCommand.getPromptForCommand('debugger inspect auth', {} as never)).resolves.toEqual([
|
||||
{ type: 'text', text: 'inspect auth' },
|
||||
{
|
||||
type: 'text',
|
||||
text: [
|
||||
'Use the Agent tool with subagent_type "debugger" to handle this request.',
|
||||
'Pass this exact prompt to that agent:',
|
||||
'',
|
||||
'inspect auth',
|
||||
].join('\n'),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('shows usage when building a prompt without an agent prompt', async () => {
|
||||
await expect(agentCommand.getPromptForCommand('debugger', {} as never)).rejects.toThrow(
|
||||
'Usage: /agent <agent> <prompt>',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js'
|
||||
import type { Command } from '../commands.js'
|
||||
import { AGENT_TOOL_NAME } from '../tools/AgentTool/constants.js'
|
||||
import { MalformedCommandError } from '../utils/errors.js'
|
||||
|
||||
export type ParsedAgentCommandArgs = {
|
||||
agentType: string
|
||||
@ -26,13 +28,22 @@ const agentCommand: Command = {
|
||||
progressMessage: 'running agent',
|
||||
contentLength: 0,
|
||||
source: 'builtin',
|
||||
context: 'fork',
|
||||
allowedTools: [AGENT_TOOL_NAME],
|
||||
async getPromptForCommand(args): Promise<ContentBlockParam[]> {
|
||||
const parsed = parseAgentCommandArgs(args)
|
||||
if (!parsed) {
|
||||
throw new MalformedCommandError('Usage: /agent <agent> <prompt>')
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text: parsed?.prompt ?? args.trim(),
|
||||
text: [
|
||||
`Use the ${AGENT_TOOL_NAME} tool with subagent_type "${parsed.agentType}" to handle this request.`,
|
||||
'Pass this exact prompt to that agent:',
|
||||
'',
|
||||
parsed.prompt,
|
||||
].join('\n'),
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
@ -395,44 +395,6 @@ describe('WebSocket goal command events', () => {
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('allows current /agent task events through the pre-turn mute gate', () => {
|
||||
const shouldForward = createCurrentTurnLocalCommandForwarder(
|
||||
parseSlashCommand('/agent Explore inspect recent changes'),
|
||||
)
|
||||
|
||||
expect(shouldForward({
|
||||
type: 'system',
|
||||
subtype: 'task_started',
|
||||
task_type: 'slash_agent',
|
||||
task_id: 'slash-agent-1',
|
||||
description: 'Agent Explore',
|
||||
})).toBe(true)
|
||||
expect(shouldForward({
|
||||
type: 'system',
|
||||
subtype: 'task_progress',
|
||||
task_type: 'slash_agent',
|
||||
task_id: 'slash-agent-1',
|
||||
summary: 'Using Read',
|
||||
})).toBe(true)
|
||||
expect(shouldForward({
|
||||
type: 'system',
|
||||
subtype: 'task_notification',
|
||||
task_type: 'slash_agent',
|
||||
task_id: 'slash-agent-1',
|
||||
status: 'completed',
|
||||
})).toBe(true)
|
||||
|
||||
const unrelatedCommandForwarder = createCurrentTurnLocalCommandForwarder(
|
||||
parseSlashCommand('/goal ship the smoke test'),
|
||||
)
|
||||
expect(unrelatedCommandForwarder({
|
||||
type: 'system',
|
||||
subtype: 'task_started',
|
||||
task_type: 'slash_agent',
|
||||
task_id: 'slash-agent-1',
|
||||
description: 'Agent Explore',
|
||||
})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('WebSocket stream event translation', () => {
|
||||
|
||||
@ -1809,9 +1809,6 @@ export function createCurrentTurnLocalCommandForwarder(
|
||||
let awaitingCurrentTurnLocalCommandOutput = false
|
||||
|
||||
return (cliMsg: any) => {
|
||||
if (command?.commandName === 'agent' && isSlashAgentTaskEvent(cliMsg)) {
|
||||
return true
|
||||
}
|
||||
if (command && isMatchingCurrentTurnLocalCommand(cliMsg, command)) {
|
||||
awaitingCurrentTurnLocalCommandOutput = true
|
||||
return true
|
||||
@ -1837,17 +1834,6 @@ export function createCurrentTurnLocalCommandForwarder(
|
||||
}
|
||||
}
|
||||
|
||||
function isSlashAgentTaskEvent(cliMsg: any): boolean {
|
||||
if (cliMsg?.type !== 'system' || cliMsg?.task_type !== 'slash_agent') {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
cliMsg.subtype === 'task_started' ||
|
||||
cliMsg.subtype === 'task_progress' ||
|
||||
cliMsg.subtype === 'task_notification'
|
||||
)
|
||||
}
|
||||
|
||||
function isMatchingCurrentTurnLocalCommand(
|
||||
cliMsg: any,
|
||||
command: NonNullable<ReturnType<typeof parseSlashCommand>>,
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { setIsInteractive } from '../../bootstrap/state.js'
|
||||
import agentCommand from '../../commands/agent.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js'
|
||||
import { createAssistantMessage } from '../messages.js'
|
||||
import { drainSdkEvents } from '../sdkEventQueue.js'
|
||||
|
||||
process.env.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY ?? 'test-key'
|
||||
|
||||
@ -51,11 +49,9 @@ function makeContext(activeAgents: AgentDefinition[]): ToolUseContext {
|
||||
describe('/agent slash command processing', () => {
|
||||
beforeEach(() => {
|
||||
runAgentMock.mockClear()
|
||||
drainSdkEvents()
|
||||
setIsInteractive(true)
|
||||
})
|
||||
|
||||
test('runs the selected agent with only the prompt body', async () => {
|
||||
test('routes the selected agent through the normal chat loop', async () => {
|
||||
const result = await processSlashCommand(
|
||||
'/agent debugger fix failing tests',
|
||||
[],
|
||||
@ -65,23 +61,28 @@ describe('/agent slash command processing', () => {
|
||||
() => {},
|
||||
)
|
||||
|
||||
expect(result.shouldQuery).toBe(false)
|
||||
expect(runAgentMock.mock.calls.length).toBe(1)
|
||||
expect(result.shouldQuery).toBe(true)
|
||||
expect(runAgentMock.mock.calls.length).toBe(0)
|
||||
|
||||
const params = runAgentMock.mock.calls[0]?.[0] as {
|
||||
agentDefinition: AgentDefinition
|
||||
promptMessages: Array<{ message: { content: string } }>
|
||||
}
|
||||
expect(params.agentDefinition.agentType).toBe('debugger')
|
||||
expect(params.promptMessages[0]?.message.content).toBe('fix failing tests')
|
||||
expect(
|
||||
result.messages.some(
|
||||
message =>
|
||||
message.type === 'user' &&
|
||||
typeof message.message.content === 'string' &&
|
||||
message.message.content.includes('<local-command-stdout>'),
|
||||
),
|
||||
).toBe(false)
|
||||
|
||||
const stdout = result.messages.find(
|
||||
message =>
|
||||
message.type === 'user' &&
|
||||
typeof message.message.content === 'string' &&
|
||||
message.message.content.includes('<local-command-stdout>'),
|
||||
const metaPrompt = result.messages.find(
|
||||
message => message.type === 'user' && message.isMeta,
|
||||
)
|
||||
expect(stdout?.message.content).toContain('debugger result')
|
||||
const metaPromptText = Array.isArray(metaPrompt?.message.content)
|
||||
? metaPrompt.message.content
|
||||
.map(block => ('text' in block ? block.text : ''))
|
||||
.join('\n')
|
||||
: ''
|
||||
expect(metaPromptText).toContain('subagent_type "debugger"')
|
||||
expect(metaPromptText).toContain('fix failing tests')
|
||||
})
|
||||
|
||||
test('shows usage when the agent prompt is missing', async () => {
|
||||
@ -103,37 +104,4 @@ describe('/agent slash command processing', () => {
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('emits foreground agent task events for desktop streaming', async () => {
|
||||
setIsInteractive(false)
|
||||
|
||||
await processSlashCommand(
|
||||
'/agent debugger fix failing tests',
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
makeContext([makeAgent('general-purpose'), makeAgent('debugger')]),
|
||||
() => {},
|
||||
)
|
||||
|
||||
const events = drainSdkEvents()
|
||||
expect(events.map(event => event.subtype)).toEqual([
|
||||
'task_started',
|
||||
'task_progress',
|
||||
'task_notification',
|
||||
])
|
||||
expect(events[0]).toMatchObject({
|
||||
type: 'system',
|
||||
subtype: 'task_started',
|
||||
description: 'Agent debugger',
|
||||
task_type: 'slash_agent',
|
||||
prompt: 'fix failing tests',
|
||||
})
|
||||
expect(events[2]).toMatchObject({
|
||||
type: 'system',
|
||||
subtype: 'task_notification',
|
||||
status: 'completed',
|
||||
summary: 'Agent debugger completed',
|
||||
result: 'debugger result',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,7 +2,6 @@ import { feature } from 'bun:bundle';
|
||||
import type { ContentBlockParam, TextBlockParam } from '@anthropic-ai/sdk/resources';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { setPromptId } from 'src/bootstrap/state.js';
|
||||
import { parseAgentCommandArgs } from 'src/commands/agent.js';
|
||||
import { builtInCommandNames, type Command, type CommandBase, findCommand, getCommand, getCommandName, hasCommand, type PromptCommand } from 'src/commands.js';
|
||||
import { NO_CONTENT_MESSAGE } from 'src/constants/messages.js';
|
||||
import type { SetToolJSXFn, ToolUseContext } from 'src/Tool.js';
|
||||
@ -39,7 +38,6 @@ import { hasPermissionsToUseTool } from '../permissions/permissions.js';
|
||||
import { isOfficialMarketplaceName, parsePluginIdentifier } from '../plugins/pluginIdentifier.js';
|
||||
import { isRestrictedToPluginOnly, isSourceAdminTrusted } from '../settings/pluginOnlyPolicy.js';
|
||||
import { parseSlashCommand } from '../slashCommandParsing.js';
|
||||
import { enqueueSdkEvent } from '../sdkEventQueue.js';
|
||||
import { sleep } from '../sleep.js';
|
||||
import { recordSkillUsage } from '../suggestions/skillUsageTracking.js';
|
||||
import { logOTelEvent, redactIfDisabled } from '../telemetry/events.js';
|
||||
@ -63,10 +61,6 @@ const MCP_SETTLE_TIMEOUT_MS = 10_000;
|
||||
*/
|
||||
async function executeForkedSlashCommand(command: CommandBase & PromptCommand, args: string, context: ProcessUserInputContext, precedingInputBlocks: ContentBlockParam[], setToolJSX: SetToolJSXFn, canUseTool: CanUseToolFn): Promise<SlashCommandResult> {
|
||||
const agentId = createAgentId();
|
||||
const dynamicAgentCommand = command.name === 'agent' ? parseAgentCommandArgs(args) : null;
|
||||
if (command.name === 'agent' && !dynamicAgentCommand) {
|
||||
throw new MalformedCommandError('Usage: /agent <agent> <prompt>');
|
||||
}
|
||||
const pluginMarketplace = command.pluginInfo ? parsePluginIdentifier(command.pluginInfo.repository).marketplace : undefined;
|
||||
logEvent('tengu_slash_command_forked', {
|
||||
command_name: command.name as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
@ -84,10 +78,7 @@ async function executeForkedSlashCommand(command: CommandBase & PromptCommand, a
|
||||
modifiedGetAppState,
|
||||
baseAgent,
|
||||
promptMessages
|
||||
} = await prepareForkedCommandContext(command, args, context, dynamicAgentCommand ? {
|
||||
agentType: dynamicAgentCommand.agentType,
|
||||
requireAgentType: true
|
||||
} : undefined);
|
||||
} = await prepareForkedCommandContext(command, args, context);
|
||||
|
||||
// Merge skill's effort into the agent definition so runAgent applies it
|
||||
const agentDefinition = command.effort !== undefined ? {
|
||||
@ -198,26 +189,6 @@ async function executeForkedSlashCommand(command: CommandBase & PromptCommand, a
|
||||
const progressMessages: ProgressMessage<AgentProgress>[] = [];
|
||||
const parentToolUseID = `forked-command-${command.name}`;
|
||||
let toolUseCounter = 0;
|
||||
const shouldEmitAgentTaskEvents = command.name === 'agent' && dynamicAgentCommand !== null;
|
||||
const sdkTaskId = shouldEmitAgentTaskEvents ? `slash-agent-${agentId}` : null;
|
||||
const sdkToolUseId = shouldEmitAgentTaskEvents ? `${parentToolUseID}-${agentId}` : undefined;
|
||||
const sdkDescription = dynamicAgentCommand ? `Agent ${dynamicAgentCommand.agentType}` : `/${getCommandName(command)}`;
|
||||
const sdkPrompt = dynamicAgentCommand?.prompt ?? args;
|
||||
const sdkStartTime = Date.now();
|
||||
let sdkProgressTokens = 0;
|
||||
let sdkToolUses = 0;
|
||||
|
||||
if (sdkTaskId) {
|
||||
enqueueSdkEvent({
|
||||
type: 'system',
|
||||
subtype: 'task_started',
|
||||
task_id: sdkTaskId,
|
||||
tool_use_id: sdkToolUseId,
|
||||
description: sdkDescription,
|
||||
task_type: 'slash_agent',
|
||||
prompt: sdkPrompt
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to create a progress message from an agent message
|
||||
const createProgressMessage = (message: AssistantMessage | NormalizedUserMessage): ProgressMessage<AgentProgress> => {
|
||||
@ -250,28 +221,6 @@ async function executeForkedSlashCommand(command: CommandBase & PromptCommand, a
|
||||
});
|
||||
};
|
||||
|
||||
const emitAgentTaskProgress = (message: AssistantMessage | NormalizedUserMessage): void => {
|
||||
if (!sdkTaskId) return;
|
||||
const summary = summarizeAgentProgressMessage(message);
|
||||
sdkProgressTokens += getApproximateMessageTokenCount(message);
|
||||
sdkToolUses += countAgentToolUses(message);
|
||||
enqueueSdkEvent({
|
||||
type: 'system',
|
||||
subtype: 'task_progress',
|
||||
task_id: sdkTaskId,
|
||||
tool_use_id: sdkToolUseId,
|
||||
task_type: 'slash_agent',
|
||||
description: sdkDescription,
|
||||
summary: summary.summary,
|
||||
last_tool_name: summary.lastToolName,
|
||||
usage: {
|
||||
total_tokens: sdkProgressTokens,
|
||||
tool_uses: sdkToolUses,
|
||||
duration_ms: Date.now() - sdkStartTime
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Show initial "Initializing…" state
|
||||
updateProgress();
|
||||
|
||||
@ -303,7 +252,6 @@ async function executeForkedSlashCommand(command: CommandBase & PromptCommand, a
|
||||
const normalizedMsg = normalizedNew[0];
|
||||
if (normalizedMsg && normalizedMsg.type === 'assistant') {
|
||||
progressMessages.push(createProgressMessage(message));
|
||||
emitAgentTaskProgress(message);
|
||||
updateProgress();
|
||||
}
|
||||
}
|
||||
@ -313,30 +261,11 @@ async function executeForkedSlashCommand(command: CommandBase & PromptCommand, a
|
||||
const normalizedMsg = normalizedNew[0];
|
||||
if (normalizedMsg && normalizedMsg.type === 'user') {
|
||||
progressMessages.push(createProgressMessage(normalizedMsg));
|
||||
emitAgentTaskProgress(normalizedMsg);
|
||||
updateProgress();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (sdkTaskId) {
|
||||
enqueueSdkEvent({
|
||||
type: 'system',
|
||||
subtype: 'task_notification',
|
||||
task_id: sdkTaskId,
|
||||
tool_use_id: sdkToolUseId,
|
||||
task_type: 'slash_agent',
|
||||
status: 'failed',
|
||||
output_file: '',
|
||||
summary: `${sdkDescription} failed`,
|
||||
result: err instanceof Error ? err.message : String(err),
|
||||
usage: {
|
||||
total_tokens: sdkProgressTokens,
|
||||
tool_uses: sdkToolUses,
|
||||
duration_ms: Date.now() - sdkStartTime
|
||||
}
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
// Clear the progress display
|
||||
@ -350,25 +279,6 @@ async function executeForkedSlashCommand(command: CommandBase & PromptCommand, a
|
||||
resultText = `[ANT-ONLY] API calls: ${getDisplayPath(getDumpPromptsPath(agentId))}\n${resultText}`;
|
||||
}
|
||||
|
||||
if (sdkTaskId) {
|
||||
enqueueSdkEvent({
|
||||
type: 'system',
|
||||
subtype: 'task_notification',
|
||||
task_id: sdkTaskId,
|
||||
tool_use_id: sdkToolUseId,
|
||||
task_type: 'slash_agent',
|
||||
status: 'completed',
|
||||
output_file: '',
|
||||
summary: `${sdkDescription} completed`,
|
||||
result: resultText,
|
||||
usage: {
|
||||
total_tokens: sdkProgressTokens,
|
||||
tool_uses: sdkToolUses,
|
||||
duration_ms: Date.now() - sdkStartTime
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Return the result as a user message (simulates the agent's output)
|
||||
const messages: UserMessage[] = [createUserMessage({
|
||||
content: prepareUserContent({
|
||||
@ -399,61 +309,6 @@ export function looksLikeCommand(commandName: string): boolean {
|
||||
return !/[^a-zA-Z0-9:\-_]/.test(commandName);
|
||||
}
|
||||
|
||||
function summarizeAgentProgressMessage(message: AssistantMessage | NormalizedUserMessage): { summary: string; lastToolName?: string } {
|
||||
const content = message.message.content;
|
||||
const blocks = Array.isArray(content) ? content : [{
|
||||
type: 'text',
|
||||
text: typeof content === 'string' ? content : ''
|
||||
}];
|
||||
let firstText: string | null = null;
|
||||
let lastToolName: string | undefined;
|
||||
|
||||
for (const block of blocks) {
|
||||
if (!block || typeof block !== 'object') continue;
|
||||
const typedBlock = block as { type?: unknown; text?: unknown; name?: unknown };
|
||||
if (typedBlock.type === 'tool_use' && typeof typedBlock.name === 'string') {
|
||||
lastToolName = typedBlock.name;
|
||||
}
|
||||
if (!firstText && typedBlock.type === 'text' && typeof typedBlock.text === 'string') {
|
||||
firstText = typedBlock.text.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (lastToolName) return { summary: `Using ${lastToolName}`, lastToolName };
|
||||
if (firstText) return { summary: truncateProgressSummary(firstText) };
|
||||
return { summary: 'Agent is working' };
|
||||
}
|
||||
|
||||
function getApproximateMessageTokenCount(message: AssistantMessage | NormalizedUserMessage): number {
|
||||
const content = message.message.content;
|
||||
if (typeof content === 'string') return Math.ceil(content.length / 4);
|
||||
if (!Array.isArray(content)) return 0;
|
||||
const textLength = content.reduce((total, block) => {
|
||||
if (!block || typeof block !== 'object') return total;
|
||||
const text = (block as { text?: unknown; content?: unknown }).text;
|
||||
if (typeof text === 'string') return total + text.length;
|
||||
const nestedContent = (block as { content?: unknown }).content;
|
||||
if (typeof nestedContent === 'string') return total + nestedContent.length;
|
||||
return total;
|
||||
}, 0);
|
||||
return Math.ceil(textLength / 4);
|
||||
}
|
||||
|
||||
function countAgentToolUses(message: AssistantMessage | NormalizedUserMessage): number {
|
||||
const content = message.message.content;
|
||||
if (!Array.isArray(content)) return 0;
|
||||
return content.filter(block => {
|
||||
if (!block || typeof block !== 'object') return false;
|
||||
return (block as { type?: unknown }).type === 'tool_use';
|
||||
}).length;
|
||||
}
|
||||
|
||||
function truncateProgressSummary(text: string): string {
|
||||
const normalized = text.replace(/\s+/g, ' ').trim();
|
||||
if (normalized.length <= 120) return normalized;
|
||||
return `${normalized.slice(0, 117)}...`;
|
||||
}
|
||||
|
||||
export async function processSlashCommand(inputString: string, precedingInputBlocks: ContentBlockParam[], imageContentBlocks: ContentBlockParam[], attachmentMessages: AttachmentMessage[], context: ProcessUserInputContext, setToolJSX: SetToolJSXFn, uuid?: string, isAlreadyProcessing?: boolean, canUseTool?: CanUseToolFn): Promise<ProcessUserInputBaseResult> {
|
||||
const parsed = parseSlashCommand(inputString);
|
||||
if (!parsed) {
|
||||
|
||||
@ -20,7 +20,6 @@ type TaskProgressEvent = {
|
||||
task_id: string
|
||||
tool_use_id?: string
|
||||
description: string
|
||||
task_type?: string
|
||||
usage: {
|
||||
total_tokens: number
|
||||
tool_uses: number
|
||||
@ -44,7 +43,6 @@ type TaskNotificationSdkEvent = {
|
||||
subtype: 'task_notification'
|
||||
task_id: string
|
||||
tool_use_id?: string
|
||||
task_type?: string
|
||||
status: 'completed' | 'failed' | 'stopped'
|
||||
output_file: string
|
||||
summary: string
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user