Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export enum ConversationRoundStepType {
reasoning = 'reasoning',
compaction = 'compaction',
backgroundAgentComplete = 'background_agent_complete',
updateTodos = 'update_todos',
}

// tool call step
Expand Down Expand Up @@ -208,11 +209,48 @@ export const isBackgroundAgentCompleteStep = (
return step.type === ConversationRoundStepType.backgroundAgentComplete;
};

export interface TodosStepData {
todos: TodoItem[];
/** True when todos were inherited from the previous round, not written by the agent this round */
carried_over?: boolean;
}

export type TodosStep = ConversationRoundStepMixin<
ConversationRoundStepType.updateTodos,
TodosStepData
>;

export const isTodosStep = (step: ConversationRoundStep): step is TodosStep => {
return step.type === ConversationRoundStepType.updateTodos;
};

/**
* Returns the (single) todos step from a list of steps, if present.
* A round only ever has at most one todos step, which is updated in place.
*/
export const findTodosStep = (
steps: ConversationRoundStep[] | undefined
): TodosStep | undefined => {
return steps?.find(isTodosStep);
};

/**
* Returns the todo list to carry over from the previous round, or undefined if nothing should carry over.
* Carryover only happens when at least one item is still incomplete (pending / in_progress).
* When carried over, both complete and incomplete items are included so the full plan is visible.
*/
export const carriedOverTodos = (todos: TodoItem[] | undefined): TodoItem[] | undefined => {
if (!todos?.length) return undefined;
const hasIncomplete = todos.some((t) => t.status !== 'completed' && t.status !== 'cancelled');
return hasIncomplete ? todos : undefined;
};

export type ConversationRoundStep =
| ToolCallStep
| ReasoningStep
| CompactionStep
| BackgroundAgentCompleteStep;
| BackgroundAgentCompleteStep
| TodosStep;

export enum ConversationRoundStatus {
/** round is currently being processed */
Expand Down Expand Up @@ -309,6 +347,13 @@ export interface Conversation {
state?: ConversationInternalState;
}

export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';

export interface TodoItem {
content: string;
status: TodoStatus;
}

/**
* Internal storage for the conversation's arbitrary state.
* Used for example to keep track of the prompt responses.
Expand All @@ -328,6 +373,8 @@ export interface ConversationInternalState {
compaction_summary?: CompactionSummary;
/** Background sub-agent executions keyed by execution ID. */
background_executions?: Record<string, BackgroundExecutionState>;
/** Active todo list for the current conversation. Replaced wholesale on each write. */
todos?: TodoItem[];
}

export interface BackgroundExecutionCompletedAt {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
ConversationInternalState,
ConversationRound,
BackgroundExecutionState,
TodoItem,
} from './conversation';
import type { PromptRequestSource, PromptRequest } from '../agents/prompts';
import type { VersionedAttachment } from '../attachments';
Expand Down Expand Up @@ -352,6 +353,19 @@ export const isBackgroundAgentCompleteEvent = (
return event.type === ChatEventType.backgroundAgentComplete;
};

export const TODOS_UPDATED_UI_EVENT = 'todos_updated' as const;

export interface TodosUpdatedUiEventData {
todos: TodoItem[];
}

export const isTodosUpdatedEvent = (event: AgentBuilderEvent<string, any>) => {
return isToolUiEvent<typeof TODOS_UPDATED_UI_EVENT, TodosUpdatedUiEventData>(
event,
TODOS_UPDATED_UI_EVENT
);
};

/**
* All types of events that can be emitted from an agent execution.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ export {
type ConversationRound,
type Conversation,
type ConversationInternalState,
type TodoItem,
type TodoStatus,
type BackgroundExecutionState,
type BackgroundExecutionCompletedAt,
type BackgroundAgentCompleteStep,
isBackgroundAgentCompleteStep,
type TodosStep,
type TodosStepData,
isTodosStep,
findTodosStep,
carriedOverTodos,
type ConversationWithoutRounds,
type ConversationRoundStepMixin,
type ToolCallStep,
Expand Down Expand Up @@ -94,6 +101,9 @@ export {
type BackgroundAgentCompleteEvent,
type BackgroundAgentCompleteEventData,
isBackgroundAgentCompleteEvent,
isTodosUpdatedEvent,
TODOS_UPDATED_UI_EVENT,
type TodosUpdatedUiEventData,
} from './events';
export type { RoundState } from './round_state';
export type { ConversationListOptions } from './conversation_list';
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ export {
isCompactionStep,
isBackgroundAgentCompleteStep,
type BackgroundAgentCompleteStep,
isTodosStep,
findTodosStep,
type TodosStep,
carriedOverTodos,
ChatEventType,
ConversationRoundStatus,
type ChatEventBase,
Expand Down Expand Up @@ -212,6 +216,9 @@ export {
type BackgroundAgentCompleteEvent,
type BackgroundAgentCompleteEventData,
isBackgroundAgentCompleteEvent,
isTodosUpdatedEvent,
TODOS_UPDATED_UI_EVENT,
type TodosUpdatedUiEventData,
type ConversationListOptions,
} from './chat';
export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const filestoreTools = {
export const internalTools = {
subAgentTool: 'run_subagent',
sleepTool: 'sleep',
writeTodosTool: 'write_todos',
};

export const isAttachmentTool = (toolName: string) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
SkillsService,
PluginsService,
ToolManager,
TodoStateManager,
} from '../runner';
import type { IFileStore } from '../runner/filestore';
import type { AttachmentStateManager } from '../attachments';
Expand Down Expand Up @@ -100,6 +101,8 @@ export interface ExperimentalFeatures {
skills: boolean;
/** Whether the sub-agent execution feature is enabled */
subagents: boolean;
/** Whether the todo list tool and task-management prompt are enabled */
todos: boolean;
}

export interface AgentHandlerContext {
Expand Down Expand Up @@ -172,6 +175,10 @@ export interface AgentHandlerContext {
* Attachment state manager to manage conversation attachments during execution.
*/
attachmentStateManager: AttachmentStateManager;
/**
* Manages the active todo list for this conversation execution.
*/
todoStateManager: TodoStateManager;
/**
* Used to manage interruptions.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export { ToolManagerToolType } from './tool_manager';
export type { SkillsStore, WritableSkillsStore } from './skills_store';
export type { PromptManager, ToolPromptManager, ConfirmationInfo } from './prompt_manager';
export type { ConversationStateManager, ToolStateManager } from './state_manager';
export type { TodoStateManager } from './todo_state_manager';
export { createTodoStateManager } from './todo_state_manager';
export { FileEntryType } from './filestore';
export type {
IToolFileStore,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { TodoItem } from '@kbn/agent-builder-common/chat/conversation';

export interface TodoStateManager {
set(todos: TodoItem[]): void;
get(): TodoItem[] | undefined;
}

export const createTodoStateManager = (initial?: TodoItem[]): TodoStateManager => {
let state: TodoItem[] | undefined = initial?.length ? initial : undefined;
return {
set: (todos) => {
state = todos;
},
get: () => state,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
} from '@kbn/agent-builder-common/attachments';
import { ATTACHMENT_REF_ACTOR } from '@kbn/agent-builder-common/attachments';
import { ConversationRoundStatus } from '@kbn/agent-builder-common';
import { findTodosStep } from '@kbn/agent-builder-common/chat/conversation';
import { isConfirmationPrompt } from '@kbn/agent-builder-common/agents';
import { RoundInput } from './round_input';
import { RoundThinking } from './round_thinking/round_thinking';
Expand All @@ -24,6 +25,7 @@ import { useSendMessage } from '../../../context/send_message/send_message_conte
import { RoundError } from './round_error/round_error';
import { ConfirmationPrompt } from './round_prompt';
import { RoundAttachmentReferences } from './round_attachment_references';
import { TodosStepDisplay } from './round_thinking/steps/todos_step_display';

interface RoundLayoutProps {
isCurrentRound: boolean;
Expand Down Expand Up @@ -80,6 +82,7 @@ export const RoundLayout: React.FC<RoundLayoutProps> = ({
const [hasBeenLoading, setHasBeenLoading] = useState(false);
const [promptResponses, setPromptResponses] = useState<Record<string, { allow: boolean }>>({});
const { steps, response, input, status, pending_prompts: pendingPrompts } = rawRound;
const todosStep = useMemo(() => findTodosStep(steps), [steps]);

const {
isResponseLoading,
Expand Down Expand Up @@ -165,7 +168,7 @@ export const RoundLayout: React.FC<RoundLayoutProps> = ({
return (
<EuiFlexGroup
direction="column"
gutterSize="m"
gutterSize="s"
aria-label={labels.container}
css={roundContainerStyles}
>
Expand All @@ -192,6 +195,13 @@ export const RoundLayout: React.FC<RoundLayoutProps> = ({
)}
</EuiFlexItem>

{/* Todos */}
{todosStep && (
<EuiFlexItem grow={false}>
<TodosStepDisplay step={todosStep} />
</EuiFlexItem>
)}

{/* Confirmation Prompts */}
{isAwaitingPrompt &&
confirmationPrompts.map((prompt) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ describe('chat_message_text', () => {
addBackgroundExecutionCompleteStep: jest.fn(),
addCompactionStep: jest.fn(),
setCompactionStepComplete: jest.fn(),
addOrUpdateTodosStep: jest.fn(),
},
});
});
Expand Down
Loading
Loading