Skip to content

Commit 292217b

Browse files
Implement frontend visualization for RecallObservation & Stop issueing recall action for agent message (#7566)
Co-authored-by: openhands <[email protected]>
1 parent a828318 commit 292217b

File tree

12 files changed

+202
-64
lines changed

12 files changed

+202
-64
lines changed

frontend/src/i18n/declaration.ts

+1
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ export enum I18nKey {
331331
OBSERVATION_MESSAGE$EDIT = "OBSERVATION_MESSAGE$EDIT",
332332
OBSERVATION_MESSAGE$WRITE = "OBSERVATION_MESSAGE$WRITE",
333333
OBSERVATION_MESSAGE$BROWSE = "OBSERVATION_MESSAGE$BROWSE",
334+
OBSERVATION_MESSAGE$RECALL = "OBSERVATION_MESSAGE$RECALL",
334335
EXPANDABLE_MESSAGE$SHOW_DETAILS = "EXPANDABLE_MESSAGE$SHOW_DETAILS",
335336
EXPANDABLE_MESSAGE$HIDE_DETAILS = "EXPANDABLE_MESSAGE$HIDE_DETAILS",
336337
AI_SETTINGS$TITLE = "AI_SETTINGS$TITLE",

frontend/src/i18n/translation.json

+15
Original file line numberDiff line numberDiff line change
@@ -4945,6 +4945,21 @@
49454945
"es": "Navegación completada",
49464946
"tr": "Gezinme tamamlandı"
49474947
},
4948+
"OBSERVATION_MESSAGE$RECALL": {
4949+
"en": "MicroAgent Activated",
4950+
"ja": "マイクロエージェントが有効化されました",
4951+
"zh-CN": "微代理已激活",
4952+
"zh-TW": "微代理已啟動",
4953+
"ko-KR": "마이크로에이전트 활성화됨",
4954+
"no": "MikroAgent aktivert",
4955+
"it": "MicroAgent attivato",
4956+
"pt": "MicroAgent ativado",
4957+
"es": "MicroAgent activado",
4958+
"ar": "تم تنشيط الوكيل المصغر",
4959+
"fr": "MicroAgent activé",
4960+
"tr": "MikroAjan Etkinleştirildi",
4961+
"de": "MicroAgent aktiviert"
4962+
},
49484963
"EXPANDABLE_MESSAGE$SHOW_DETAILS": {
49494964
"en": "Show details",
49504965
"zh-CN": "显示详情",

frontend/src/services/observations.ts

+16
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export function handleObservationMessage(message: ObservationMessage) {
5151
case ObservationType.EDIT:
5252
case ObservationType.THINK:
5353
case ObservationType.NULL:
54+
case ObservationType.RECALL:
5455
break; // We don't display the default message for these observations
5556
default:
5657
store.dispatch(addAssistantMessage(message.message));
@@ -76,6 +77,21 @@ export function handleObservationMessage(message: ObservationMessage) {
7677
}),
7778
);
7879
break;
80+
case "recall":
81+
store.dispatch(
82+
addAssistantObservation({
83+
...baseObservation,
84+
observation: "recall" as const,
85+
extras: {
86+
...(message.extras || {}),
87+
recall_type:
88+
(message.extras?.recall_type as
89+
| "workspace_context"
90+
| "knowledge") || "knowledge",
91+
},
92+
}),
93+
);
94+
break;
7995
case "run":
8096
store.dispatch(
8197
addAssistantObservation({

frontend/src/state/chat-slice.ts

+72
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
OpenHandsObservation,
77
CommandObservation,
88
IPythonObservation,
9+
RecallObservation,
910
} from "#/types/core/observations";
1011
import { OpenHandsAction } from "#/types/core/actions";
1112
import { OpenHandsEventType } from "#/types/core/base";
@@ -22,6 +23,7 @@ const HANDLED_ACTIONS: OpenHandsEventType[] = [
2223
"browse",
2324
"browse_interactive",
2425
"edit",
26+
"recall",
2527
];
2628

2729
function getRiskText(risk: ActionSecurityRisk) {
@@ -112,6 +114,9 @@ export const chatSlice = createSlice({
112114
} else if (actionID === "browse_interactive") {
113115
// Include the browser_actions in the content
114116
text = `**Action:**\n\n\`\`\`python\n${action.payload.args.browser_actions}\n\`\`\``;
117+
} else if (actionID === "recall") {
118+
// skip recall actions
119+
return;
115120
}
116121
if (actionID === "run" || actionID === "run_ipython") {
117122
if (
@@ -143,6 +148,73 @@ export const chatSlice = createSlice({
143148
if (!HANDLED_ACTIONS.includes(observationID)) {
144149
return;
145150
}
151+
152+
// Special handling for RecallObservation - create a new message instead of updating an existing one
153+
if (observationID === "recall") {
154+
const recallObs = observation.payload as RecallObservation;
155+
let content = ``;
156+
157+
// Handle workspace context
158+
if (recallObs.extras.recall_type === "workspace_context") {
159+
if (recallObs.extras.repo_name) {
160+
content += `\n\n**Repository:** ${recallObs.extras.repo_name}`;
161+
}
162+
if (recallObs.extras.repo_directory) {
163+
content += `\n\n**Directory:** ${recallObs.extras.repo_directory}`;
164+
}
165+
if (recallObs.extras.date) {
166+
content += `\n\n**Date:** ${recallObs.extras.date}`;
167+
}
168+
if (
169+
recallObs.extras.runtime_hosts &&
170+
Object.keys(recallObs.extras.runtime_hosts).length > 0
171+
) {
172+
content += `\n\n**Available Hosts**`;
173+
for (const [host, port] of Object.entries(
174+
recallObs.extras.runtime_hosts,
175+
)) {
176+
content += `\n\n- ${host} (port ${port})`;
177+
}
178+
}
179+
if (recallObs.extras.repo_instructions) {
180+
content += `\n\n**Repository Instructions:**\n\n${recallObs.extras.repo_instructions}`;
181+
}
182+
if (recallObs.extras.additional_agent_instructions) {
183+
content += `\n\n**Additional Instructions:**\n\n${recallObs.extras.additional_agent_instructions}`;
184+
}
185+
}
186+
187+
// Create a new message for the observation
188+
// Use the correct translation ID format that matches what's in the i18n file
189+
const translationID = `OBSERVATION_MESSAGE$${observationID.toUpperCase()}`;
190+
191+
// Handle microagent knowledge
192+
if (
193+
recallObs.extras.microagent_knowledge &&
194+
recallObs.extras.microagent_knowledge.length > 0
195+
) {
196+
content += `\n\n**Triggered Microagent Knowledge:**`;
197+
for (const knowledge of recallObs.extras.microagent_knowledge) {
198+
content += `\n\n- **${knowledge.name}** (triggered by keyword: ${knowledge.trigger})\n\n\`\`\`\n${knowledge.content}\n\`\`\``;
199+
}
200+
}
201+
202+
const message: Message = {
203+
type: "action",
204+
sender: "assistant",
205+
translationID,
206+
eventID: observation.payload.id,
207+
content,
208+
imageUrls: [],
209+
timestamp: new Date().toISOString(),
210+
success: true,
211+
};
212+
213+
state.messages.push(message);
214+
return; // Skip the normal observation handling below
215+
}
216+
217+
// Normal handling for other observation types
146218
const translationID = `OBSERVATION_MESSAGE$${observationID.toUpperCase()}`;
147219
const causeID = observation.payload.cause;
148220
const causeMessage = state.messages.find(

frontend/src/types/core/actions.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,15 @@ export interface RejectAction extends OpenHandsActionEvent<"reject"> {
133133
};
134134
}
135135

136+
export interface RecallAction extends OpenHandsActionEvent<"recall"> {
137+
source: "agent";
138+
args: {
139+
recall_type: "workspace_context" | "knowledge";
140+
query: string;
141+
thought: string;
142+
};
143+
}
144+
136145
export type OpenHandsAction =
137146
| UserMessageAction
138147
| AssistantMessageAction
@@ -146,4 +155,5 @@ export type OpenHandsAction =
146155
| FileReadAction
147156
| FileEditAction
148157
| FileWriteAction
149-
| RejectAction;
158+
| RejectAction
159+
| RecallAction;

frontend/src/types/core/base.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export type OpenHandsEventType =
1212
| "reject"
1313
| "think"
1414
| "finish"
15-
| "error";
15+
| "error"
16+
| "recall";
1617

1718
interface OpenHandsBaseEvent {
1819
id: number;

frontend/src/types/core/observations.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,26 @@ export interface AgentThinkObservation
109109
};
110110
}
111111

112+
export interface MicroagentKnowledge {
113+
name: string;
114+
trigger: string;
115+
content: string;
116+
}
117+
118+
export interface RecallObservation extends OpenHandsObservationEvent<"recall"> {
119+
source: "agent";
120+
extras: {
121+
recall_type?: "workspace_context" | "knowledge";
122+
repo_name?: string;
123+
repo_directory?: string;
124+
repo_instructions?: string;
125+
runtime_hosts?: Record<string, number>;
126+
additional_agent_instructions?: string;
127+
date?: string;
128+
microagent_knowledge?: MicroagentKnowledge[];
129+
};
130+
}
131+
112132
export type OpenHandsObservation =
113133
| AgentStateChangeObservation
114134
| AgentThinkObservation
@@ -120,4 +140,5 @@ export type OpenHandsObservation =
120140
| WriteObservation
121141
| ReadObservation
122142
| EditObservation
123-
| ErrorObservation;
143+
| ErrorObservation
144+
| RecallObservation;

frontend/src/types/observation-type.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ enum ObservationType {
2929
// A response to the agent's thought (usually a static message)
3030
THINK = "think",
3131

32+
// An observation that shows agent's context extension
33+
RECALL = "recall",
34+
3235
// A no-op observation
3336
NULL = "null",
3437
}

openhands/controller/agent_controller.py

+3-46
Original file line numberDiff line numberDiff line change
@@ -490,15 +490,8 @@ async def _handle_message_action(self, action: MessageAction) -> None:
490490

491491
if self.get_agent_state() != AgentState.RUNNING:
492492
await self.set_agent_state_to(AgentState.RUNNING)
493-
elif action.source == EventSource.AGENT:
494-
# Check if we need to trigger microagents based on agent message content
495-
recall_action = RecallAction(
496-
query=action.content, recall_type=RecallType.KNOWLEDGE
497-
)
498-
self._pending_action = recall_action
499-
# This is source=AGENT because the agent message is the trigger for the microagent retrieval
500-
self.event_stream.add_event(recall_action, EventSource.AGENT)
501493

494+
elif action.source == EventSource.AGENT:
502495
# If the agent is waiting for a response, set the appropriate state
503496
if action.wait_for_response:
504497
await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)
@@ -1084,44 +1077,8 @@ def _apply_conversation_window(self, events: list[Event]) -> list[Event]:
10841077
# cut in half
10851078
mid_point = max(1, len(events) // 2)
10861079
kept_events = events[mid_point:]
1087-
1088-
# Handle first event in truncated history
1089-
if kept_events:
1090-
i = 0
1091-
while i < len(kept_events):
1092-
first_event = kept_events[i]
1093-
if isinstance(first_event, Observation) and first_event.cause:
1094-
# Find its action and include it
1095-
matching_action = next(
1096-
(
1097-
e
1098-
for e in reversed(events[:mid_point])
1099-
if isinstance(e, Action) and e.id == first_event.cause
1100-
),
1101-
None,
1102-
)
1103-
if matching_action:
1104-
kept_events = [matching_action] + kept_events
1105-
else:
1106-
self.log(
1107-
'warning',
1108-
f'Found Observation without matching Action at id={first_event.id}',
1109-
)
1110-
# drop this observation
1111-
kept_events = kept_events[1:]
1112-
break
1113-
1114-
elif isinstance(first_event, MessageAction) or (
1115-
isinstance(first_event, Action)
1116-
and first_event.source == EventSource.USER
1117-
):
1118-
# if it's a message action or a user action, keep it and continue to find the next event
1119-
i += 1
1120-
continue
1121-
1122-
else:
1123-
# if it's an action with source == EventSource.AGENT, we're good
1124-
break
1080+
if len(kept_events) > 0 and isinstance(kept_events[0], Observation):
1081+
kept_events = kept_events[1:]
11251082

11261083
# Ensure first user message is included
11271084
if first_user_msg and first_user_msg not in kept_events:

openhands/server/listen_socket.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
)
1515
from openhands.events.observation.agent import (
1616
AgentStateChangedObservation,
17-
RecallObservation,
1817
)
1918
from openhands.events.serialization import event_to_dict
2019
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderToken
@@ -91,7 +90,7 @@ async def connect(connection_id: str, environ):
9190
logger.debug(f'oh_event: {event.__class__.__name__}')
9291
if isinstance(
9392
event,
94-
(NullAction, NullObservation, RecallAction, RecallObservation),
93+
(NullAction, NullObservation, RecallAction),
9594
):
9695
continue
9796
elif isinstance(event, AgentStateChangedObservation):

openhands/server/session/session.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
CmdOutputObservation,
2020
NullObservation,
2121
)
22+
from openhands.events.observation.agent import RecallObservation
2223
from openhands.events.observation.error import ErrorObservation
2324
from openhands.events.serialization import event_from_dict, event_to_dict
2425
from openhands.events.stream import EventStreamSubscriber
@@ -199,7 +200,7 @@ async def _on_event(self, event: Event):
199200
await self.send(event_to_dict(event))
200201
# NOTE: ipython observations are not sent here currently
201202
elif event.source == EventSource.ENVIRONMENT and isinstance(
202-
event, (CmdOutputObservation, AgentStateChangedObservation)
203+
event, (CmdOutputObservation, AgentStateChangedObservation, RecallObservation)
203204
):
204205
# feedback from the environment to agent actions is understood as agent events by the UI
205206
event_dict = event_to_dict(event)

0 commit comments

Comments
 (0)