🧅 feat: Reveal Subagent Thread History and Event Detail on Demand (#15283)

* ✨ feat: Refine Subagent Thread Activity UX

* test: align subagent completion lifecycle

* chore: satisfy subagent panel static checks

* fix: harden subagent activity pagination

* fix: preserve paged subagent history

* fix: surface unrecoverable activity boundaries

* fix: rebase paged activity after live advances

* fix: bound child activity history state

* fix: preserve rebased child history order

* fix: retain unavailable child history boundary

* fix: fence child history cursor generations

* fix: preserve subagent history continuity

* fix: retain live turns during history rebase

* fix: close subagent history edge cases

* fix: harden subagent history phase transitions
This commit is contained in:
Danny Avila 2026-08-27 08:56:06 -04:00 • committed by GitHub
parent 151dc9e03e
commit 41ba808e6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 2025 additions and 134 deletions

View file

@ -490,6 +490,9 @@ class BaseClient {
sender: 'User',
text,
isCreatedByUser: true,
...(this.options?.req?._agentEventTriggerProjection != null && {
subagentTriggerProjection: this.options.req._agentEventTriggerProjection,
}),
};
}

View file

@ -101,6 +101,26 @@ describe('BaseClient', () => {
});
});
test('persists only the host-authored external event display projection on the user turn', () => {
const projection = {
version: 1,
eventType: 'chess.turn.ready',
sourceType: 'speed-chess',
occurredAt: new Date('2026-08-21T12:00:00.000Z'),
expectedActionToolName: 'submit_move',
};
TestClient.options.req = { _agentEventTriggerProjection: projection };
expect(
TestClient.createUserMessage({
messageId: 'event:user',
parentMessageId: 'parent',
conversationId: 'event-thread',
text: 'Private event payload',
}),
).toEqual(expect.objectContaining({ subagentTriggerProjection: projection }));
});
test('returns the input messages without instructions when addInstructions() is called with empty instructions', () => {
const messages = [{ content: 'Hello' }, { content: 'How are you?' }, { content: 'Goodbye' }];
const instructions = '';

View file

@ -4021,9 +4021,9 @@ describe('ResumableAgentController resume metadata', () => {
});
const event = {
id: 'game-1:ply-8',
type: 'chess.turn',
type: 'chess.\u202Eturn',
occurredAt: Date.now(),
source: { id: 'speed-chess', type: 'mcp' },
source: { id: 'speed-chess', type: 'm\u2066cp' },
payload: { gameId: 'game-1', expectedPly: 8 },
};
const req = {
@ -4037,7 +4037,7 @@ describe('ResumableAgentController resume metadata', () => {
deliveryKey: 'req-event-fork',
target: { bindingId: 'binding-1' },
event,
expectedAction: { toolName: 'submit_move', argumentSubset: { expectedPly: 8 } },
expectedAction: { toolName: 'submit_\u200Fmove', argumentSubset: { expectedPly: 8 } },
},
},
config: {
@ -4064,7 +4064,7 @@ describe('ResumableAgentController resume metadata', () => {
conversationId: 'child-conversation',
invocationId: 'req-event-fork',
event,
expectedAction: { toolName: 'submit_move', argumentSubset: { expectedPly: 8 } },
expectedAction: { toolName: 'submit_\u200Fmove', argumentSubset: { expectedPly: 8 } },
}),
{
getSnapshot: expect.any(Function),
@ -4079,6 +4079,13 @@ describe('ResumableAgentController resume metadata', () => {
},
);
expect(mockExecuteAgentEventActor.mock.calls[0][0]).not.toHaveProperty('tenantId');
expect(req._agentEventTriggerProjection).toEqual({
version: 1,
eventType: 'chess. turn',
sourceType: 'm cp',
occurredAt: new Date(event.occurredAt),
expectedActionToolName: 'submit_ move',
});
expect(client).toMatchObject({
checkpointNamespace: 'event-actor/fork',
eventActorCheckpointId: 'checkpoint-base',

View file

@ -140,6 +140,7 @@ function getPreliminaryResponseMessageId({ messageId, responseMessageId }) {
function getPreliminaryUserMessage(
{ messageId, parentMessageId, text, quotes, files, manualSkills, alwaysAppliedSkills },
conversationId,
subagentTriggerProjection,
) {
if (typeof messageId !== 'string' || messageId.length === 0) {
return null;
@ -170,6 +171,36 @@ function getPreliminaryUserMessage(
...(Array.isArray(manualSkills) && manualSkills.length > 0 && { manualSkills }),
...(Array.isArray(alwaysAppliedSkills) &&
alwaysAppliedSkills.length > 0 && { alwaysAppliedSkills }),
...(subagentTriggerProjection != null && { subagentTriggerProjection }),
};
}
const DISPLAY_IDENTITY_CONTROLS = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu;
function sanitizeEventDisplayIdentity(value) {
if (typeof value !== 'string') return undefined;
const bounded = Array.from(value).slice(0, 512).join('');
const sanitized = bounded.normalize('NFC').replace(DISPLAY_IDENTITY_CONTROLS, ' ').trim();
return sanitized.length === 0 ? undefined : Array.from(sanitized).slice(0, 256).join('');
}
function getAgentEventTriggerProjection(agentEventDelivery) {
const event = agentEventDelivery?.event;
const occurredAt = new Date(event?.occurredAt);
const eventType = sanitizeEventDisplayIdentity(event?.type);
const sourceType = sanitizeEventDisplayIdentity(event?.source?.type);
if (eventType == null || sourceType == null || Number.isNaN(occurredAt.getTime())) {
return undefined;
}
const expectedActionToolName = sanitizeEventDisplayIdentity(
agentEventDelivery?.expectedAction?.toolName,
);
return {
version: 1,
eventType,
sourceType,
occurredAt,
...(expectedActionToolName == null ? {} : { expectedActionToolName }),
};
}
@ -271,7 +302,7 @@ async function saveErrorTurn(
alwaysAppliedSkills: req.body.alwaysAppliedSkills,
}),
}
: getPreliminaryUserMessage(req.body, conversationId);
: getPreliminaryUserMessage(req.body, conversationId, req._agentEventTriggerProjection);
if (!userMessage) {
return;
}
@ -1271,6 +1302,7 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit
rawAgentEventDelivery.deliveryKey === clientRequestId
? rawAgentEventDelivery
: undefined;
req._agentEventTriggerProjection = getAgentEventTriggerProjection(agentEventDelivery);
try {
logger.debug(`[ResumableAgentController] Creating job`, {
@ -1285,6 +1317,7 @@ const ResumableAgentController = async (req, res, next, initializeClient, addTit
const preliminaryUserMessage = getPreliminaryUserMessage(
{ ...req.body, messageId: preallocatedUserMessageId },
conversationId,
req._agentEventTriggerProjection,
);
const job = await GenerationJobManager.createJob(streamId, userId, conversationId, {
startupTelemetry,