feat(agent): formalize first-class tool lifecycle states and status mapping (#24993)

This commit is contained in:
Michael Bleigh
2026-05-05 11:45:12 -07:00
parent f5e2cf59fd
commit 005e0cfc53
5 changed files with 122 additions and 10 deletions
+16 -8
View File
@@ -224,14 +224,17 @@ export const useAgentStream = ({
if (tc.callId !== event.requestId) return tc;
const legacyState = event._meta?.legacyState;
const evtStatus = legacyState?.status;
let status = tc.status;
if (evtStatus === 'executing')
if (event.status === 'executing')
status = CoreToolCallStatus.Executing;
else if (evtStatus === 'error') status = CoreToolCallStatus.Error;
else if (evtStatus === 'success')
else if (event.status === 'pending_input')
status = CoreToolCallStatus.AwaitingApproval;
else if (event.status === 'errored')
status = CoreToolCallStatus.Error;
else if (event.status === 'succeeded')
status = CoreToolCallStatus.Success;
else if (event.status === 'aborted')
status = CoreToolCallStatus.Cancelled;
const display = event.display?.result;
const liveOutput =
@@ -272,11 +275,16 @@ export const useAgentStream = ({
const resultDisplay =
displayContentToString(display) ?? tc.resultDisplay;
let status = CoreToolCallStatus.Success;
if (event.status === 'errored') status = CoreToolCallStatus.Error;
else if (event.status === 'aborted')
status = CoreToolCallStatus.Cancelled;
else if (event.status === 'succeeded')
status = CoreToolCallStatus.Success;
return {
...tc,
status: event.isError
? CoreToolCallStatus.Error
: CoreToolCallStatus.Success,
status,
display: event.display
? { ...tc.display, ...event.display }
: tc.display,
@@ -235,6 +235,7 @@ export function translateEvent(
makeEvent('tool_request', state, {
requestId: event.value.callId,
name: event.value.name,
status: 'pending',
args: event.value.args,
display: event.value.display,
}),
@@ -257,6 +258,7 @@ export function translateEvent(
makeEvent('tool_response', state, {
requestId: event.value.callId,
name: state.pendingToolNames.get(event.value.callId) ?? 'unknown',
status: event.value.error ? 'errored' : 'succeeded',
content: event.value.error
? [{ type: 'text', text: event.value.error.message }]
: geminiPartsToContentParts(event.value.responseParts),
@@ -39,7 +39,12 @@ import type {
ContentPart,
StreamEndReason,
Unsubscribe,
ToolEventStatus,
} from './types.js';
import {
MessageBusType,
type ToolCallsUpdateMessage,
} from '../confirmation-bus/types.js';
function isAbortLikeError(err: unknown): boolean {
return err instanceof Error && err.name === 'AbortError';
@@ -64,6 +69,7 @@ export class LegacyAgentProtocol implements AgentProtocol {
private _activeStreamId?: string;
private _abortController = new AbortController();
private _nextStreamIdOverride?: string;
private _lastToolStatuses = new Map<string, ToolEventStatus>();
private readonly _client: GeminiClient;
private readonly _scheduler: Scheduler;
@@ -92,6 +98,11 @@ export class LegacyAgentProtocol implements AgentProtocol {
}
this._scheduler = scheduler;
}
this._config.messageBus.subscribe(
MessageBusType.TOOL_CALLS_UPDATE,
this._handleToolCallsUpdate.bind(this),
);
}
get events(): readonly AgentEvent[] {
@@ -274,6 +285,11 @@ export class LegacyAgentProtocol implements AgentProtocol {
this._makeToolResponseEvent({
requestId: request.callId,
name: request.name,
status: response.error
? 'errored'
: tc.status === 'cancelled'
? 'aborted'
: 'succeeded',
content,
isError: response.error !== undefined,
...(display ? { display } : {}),
@@ -487,6 +503,71 @@ export class LegacyAgentProtocol implements AgentProtocol {
} satisfies AgentEvent<'error'>;
return event;
}
private _handleToolCallsUpdate(msg: ToolCallsUpdateMessage): void {
if (!this._activeStreamId) {
return;
}
const eventsToEmit: AgentEvent[] = [];
for (const tc of msg.toolCalls) {
const callId = tc.request.callId;
let status: ToolEventStatus = 'pending';
if (tc.status === 'validating' || tc.status === 'scheduled') {
status = 'pending';
} else if (tc.status === 'awaiting_approval') {
status = 'pending_input';
} else if (tc.status === 'executing') {
status = 'executing';
} else if (tc.status === 'success') {
status = 'succeeded';
} else if (tc.status === 'error') {
status = 'errored';
} else if (tc.status === 'cancelled') {
status = 'aborted';
}
const lastStatus = this._lastToolStatuses.get(callId);
if (lastStatus !== status) {
this._lastToolStatuses.set(callId, status);
const display = populateToolDisplay({
name: tc.request.name,
invocation: 'invocation' in tc ? tc.invocation : undefined,
displayName: 'tool' in tc ? tc.tool?.displayName : undefined,
display: 'response' in tc ? tc.response?.display : undefined,
});
eventsToEmit.push(
this._makeToolUpdateEvent({
requestId: callId,
status,
...(display ? { display } : {}),
}),
);
}
}
if (eventsToEmit.length > 0) {
this._emit(eventsToEmit);
}
}
private _makeToolUpdateEvent(
payload: Omit<
AgentEvent<'tool_update'>,
'id' | 'timestamp' | 'streamId' | 'type'
>,
): AgentEvent<'tool_update'> {
const event = {
...this._nextEventFields(),
type: 'tool_update',
...payload,
} satisfies AgentEvent<'tool_update'>;
return event;
}
}
export class LegacyAgentSession extends AgentSession {
+14
View File
@@ -227,11 +227,21 @@ export interface ToolDisplay {
format?: ToolDisplayFormat;
}
export type ToolEventStatus =
| 'pending'
| 'pending_input'
| 'executing'
| 'succeeded'
| 'errored'
| 'aborted';
export interface ToolRequest {
/** A unique identifier for this tool request to be correlated by the response. */
requestId: string;
/** The name of the tool being requested. */
name: string;
/** The status of the tool execution. */
status: ToolEventStatus;
/** The arguments for the tool. */
/** Tool-controlled display information. */
display?: ToolDisplay;
@@ -255,6 +265,8 @@ export interface ToolRequest {
*/
export interface ToolUpdate {
requestId: string;
/** The status of the tool execution. */
status: ToolEventStatus;
/** Tool-controlled display information. */
display?: ToolDisplay;
content?: ContentPart[];
@@ -276,6 +288,8 @@ export interface ToolUpdate {
export interface ToolResponse {
requestId: string;
name: string;
/** The status of the tool execution. */
status: ToolEventStatus;
/** Tool-controlled display information. */
display?: ToolDisplay;
/** Multi-part content to be sent to the model. */
+9 -2
View File
@@ -18,7 +18,7 @@
// limitations under the License.
import { execSync } from 'node:child_process';
import { writeFileSync, existsSync, cpSync } from 'node:fs';
import { writeFileSync, existsSync, cpSync, rmSync } from 'node:fs';
import { join, basename } from 'node:path';
if (!process.cwd().includes('packages')) {
@@ -48,7 +48,14 @@ if (packageName === 'core') {
const docsSource = join(process.cwd(), '..', '..', 'docs');
const docsTarget = join(process.cwd(), 'dist', 'docs');
if (existsSync(docsSource)) {
cpSync(docsSource, docsTarget, { recursive: true, dereference: true });
if (existsSync(docsTarget)) {
rmSync(docsTarget, { recursive: true, force: true });
}
cpSync(docsSource, docsTarget, {
recursive: true,
dereference: true,
force: true,
});
console.log('Copied documentation to dist/docs');
}
}