Merge pull request #311 from ggozad/fix/skills-use-activity-events

handle skill sub-agent ActivitySnapshotEvent in TUI and web frontend
This commit is contained in:
Yiorgis Gozadinos 2026-03-13 13:21:32 +02:00 committed by GitHub
commit 5851c2482c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 627 additions and 304 deletions

View file

@ -1,9 +1,14 @@
# Changelog
## [Unreleased]
### Added
- **Activity events**: TUI and web frontend now display skill sub-agent tool calls via `ActivitySnapshotEvent`
### Changed
- **RLM sandbox**: Bumped pydantic-monty to 0.0.8. Removed `regex_*` external functions — the sandbox now has native `re` and `math` modules via `import`. Also adds `filter()` and `getattr()` builtins.
- **Frontend deps**: Upgraded CopilotKit to 1.54.0 and @ag-ui/client to 0.0.47
## [0.33.3] - 2026-03-12

View file

@ -8,7 +8,7 @@ dependencies = [
"uvicorn[standard]>=0.40.0",
"pydantic-ai-slim[ag-ui,anthropic,openai]>=1.46.0",
"python-dotenv>=1.2.1",
"haiku.rag-slim>=0.26.9",
"haiku.rag-slim>=0.33.2",
"logfire[pydantic-ai]>=3.17.0",
]

View file

@ -36,9 +36,30 @@ a {
--ring: #3b82f6;
}
/* Chat message area — use full width instead of max-w-3xl */
.chat-content .max-w-3xl {
max-width: 100%;
/* Remove CopilotKit's max-width constraints so content fills the container */
.chat-container [class*="cpk:max-w-"],
.chat-container .max-w-3xl {
max-width: 100% !important;
}
/* Empty state input — horizontal padding so it doesn't touch the edges */
.chat-container [class*="cpk:max-w-3xl"]:has(.copilotKitInput) {
padding: 0 1rem;
}
/* Input box — breathing room for send button and bottom spacing */
.copilotKitInput {
padding-right: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
/* User message bubble — more generous padding and spacing */
.copilotKitUserMessage {
margin-top: 1.5rem !important;
}
.copilotKitUserMessage [class*="cpk:rounded"] {
padding: 12px 20px !important;
}
/* Render-prop layout: flex column so citations sit between messages and input */
@ -57,7 +78,7 @@ a {
.chat-input-area {
position: relative;
padding: 0 0.75rem 0.5rem;
padding: 0 1rem 1rem;
}
/* Override CopilotKit's absolute positioning on the input container
@ -69,16 +90,6 @@ a {
right: auto;
}
/* User message bubble spacing and padding */
[data-message-id].items-end {
padding-top: 1rem;
}
[data-message-id].items-end > .bg-muted {
color: #1e293b;
padding: 0.75rem 1.25rem;
}
/* Assistant message markdown styling */
.prose[data-message-id] {
line-height: 1.6;
@ -186,14 +197,6 @@ a {
font-weight: 600;
}
.prose[data-message-id] strong {
font-weight: 600;
}
.prose[data-message-id] em {
font-style: italic;
}
/* ── Chat layout ── */
.chat-wrapper {

View file

@ -230,6 +230,39 @@ function ToolCallIndicator({
);
}
// Render an activity message from a skill sub-agent tool call/result
function ActivityIndicator({
message,
isComplete,
}: {
// biome-ignore lint/suspicious/noExplicitAny: AG-UI activity message shape
message: any;
isComplete: boolean;
}) {
const content = message.content ?? {};
const toolName = content.tool_name ?? "tool";
let args: Record<string, unknown> = {};
if (content.args) {
try {
args =
typeof content.args === "string"
? JSON.parse(content.args)
: content.args;
} catch {
// ignore parse errors
}
}
return (
<ToolCallIndicator
toolName={toolName}
status={isComplete ? "complete" : "loading"}
args={args}
/>
);
}
// Context for sharing chat state with the message view
const ChatStateContext = createContext<RAGState | null>(null);
@ -261,6 +294,21 @@ function MessageViewWithCitations({
const ragState = useContext(ChatStateContext);
const citationsHistory = ragState ? deriveCitationsHistory(ragState) : [];
// Collect completed tool_call_ids from skill_tool_result activity messages
const completedToolCallIds = useMemo(() => {
const ids = new Set<string>();
for (const msg of messages) {
if (
msg.role === "activity" &&
msg.activityType === "skill_tool_result" &&
msg.content?.tool_call_id
) {
ids.add(msg.content.tool_call_id);
}
}
return ids;
}, [messages]);
const cursor = isRunning ? (
<div key="cursor" className="streaming-cursor">
<span className="dot" />
@ -269,29 +317,17 @@ function MessageViewWithCitations({
</div>
) : null;
// CopilotChatMessageView renders one element per user/assistant message.
// We interleave activity indicators (skill sub-agent tool calls) and
// optionally inject CitationBlocks after assistant responses that
// followed tool calls.
return (
<CopilotChatMessageView messages={messages} isRunning={isRunning}>
{({ messageElements }) => {
if (!citationsHistory.length) {
return (
<>
{messageElements}
{cursor}
</>
);
}
// CopilotChatMessageView renders one element per user/assistant/activity
// message (tool messages produce nothing). We correlate elements with
// messages to inject CitationBlocks after the right assistant responses.
//
// Both search and ask tools append to citations via qa_history,
// so after each assistant text response that followed tool calls,
// we inject the next citations entry.
const result: React.ReactNode[] = [];
let elemIdx = 0;
let citIdx = 0;
let seenToolCalls = false;
let elemIdx = 0;
for (const msg of messages) {
if (msg.role === "user") {
@ -306,11 +342,25 @@ function MessageViewWithCitations({
seenToolCalls = true;
}
const isRendered =
msg.role === "user" ||
msg.role === "assistant" ||
msg.role === "activity";
if (!isRendered) continue;
// Activity messages are not rendered by CopilotKit —
// render them ourselves without consuming messageElements
if (msg.role === "activity") {
if (msg.activityType === "skill_tool_call") {
const toolCallId = msg.content?.tool_call_id;
result.push(
<ActivityIndicator
key={msg.id}
message={msg}
isComplete={
toolCallId ? completedToolCallIds.has(toolCallId) : false
}
/>,
);
}
continue;
}
if (msg.role !== "user" && msg.role !== "assistant") continue;
if (elemIdx < messageElements.length) {
result.push(messageElements[elemIdx]);

View file

@ -11,9 +11,10 @@
"format": "biome check --write app components lib"
},
"dependencies": {
"@ag-ui/client": "^0.0.43",
"@copilotkit/react-core": "^1.51.4",
"@copilotkit/runtime": "^1.51.4",
"@ag-ui/client": "^0.0.47",
"@copilotkit/react-core": "^1.54.0",
"@copilotkit/runtime": "^1.54.0",
"zod": "^3.24.0",
"next": "^16.1.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"

File diff suppressed because it is too large Load diff

View file

@ -287,10 +287,10 @@ class TestRunOptimization:
return DatasetSpec(
key="test",
db_filename="test.lancedb",
document_loader=lambda: None, # type: ignore[return-value]
document_loader=lambda: None,
document_mapper=lambda doc: None,
qa_loader=lambda: None, # type: ignore[return-value]
qa_case_builder=lambda idx, doc: None, # type: ignore[return-value]
qa_loader=lambda: None,
qa_case_builder=lambda idx, doc: None,
system_prompt="You are a test assistant.",
)
@ -348,10 +348,10 @@ class TestRunOptimization:
spec = DatasetSpec(
key="test",
db_filename="test.lancedb",
document_loader=lambda: None, # type: ignore[return-value]
document_loader=lambda: None,
document_mapper=lambda doc: None,
qa_loader=lambda: None, # type: ignore[return-value]
qa_case_builder=lambda idx, doc: None, # type: ignore[return-value]
qa_loader=lambda: None,
qa_case_builder=lambda idx, doc: None,
)
cases = _make_cases(4)

View file

@ -31,6 +31,7 @@ except ImportError:
try:
import textual_image.widget # noqa: F401 - import early for renderer detection
from ag_ui.core import (
ActivitySnapshotEvent,
AssistantMessage,
EventType,
RunAgentInput,
@ -271,6 +272,30 @@ class ChatApp(App):
elif event.type == EventType.TOOL_CALL_END:
assert isinstance(event, ToolCallEndEvent)
chat_history.mark_tool_complete(event.tool_call_id)
elif event.type == EventType.ACTIVITY_SNAPSHOT:
assert isinstance(event, ActivitySnapshotEvent)
content = event.content
if event.activity_type == "skill_tool_call":
tool_call_id = content["tool_call_id"]
skill_name = content.get("skill", "")
tool_name = content["tool_name"]
display_name = (
f"{skill_name}{tool_name}"
if skill_name
else tool_name
)
args_str = content.get("args", "{}")
chat_history.hide_thinking()
await chat_history.add_tool_call(tool_call_id, display_name)
try:
args = json.loads(args_str)
chat_history.update_tool_args(tool_call_id, args)
except json.JSONDecodeError:
pass
await chat_history.show_thinking("Working...")
elif event.activity_type == "skill_tool_result":
tool_call_id = content["tool_call_id"]
chat_history.mark_tool_complete(tool_call_id)
elif event.type == EventType.STATE_DELTA:
assert isinstance(event, StateDeltaEvent)
patch = JsonPatch(event.delta)

View file

@ -24,7 +24,7 @@ classifiers = [
dependencies = [
"cachetools>=7.0.2",
"docling-core>=2.67.1",
"haiku.skills>=0.7.0",
"haiku.skills>=0.8.0",
"httpx>=0.28.1",
"jsonpatch>=1.33",
"lancedb==0.29.2",

View file

@ -44,9 +44,9 @@ def update_dependency_version(file_path: Path, new_version: str) -> None:
def update_example_dependencies(file_path: Path, new_version: str) -> None:
"""Update haiku.rag and haiku.rag-slim dependency versions in example pyproject.toml files."""
content = file_path.read_text()
# Update haiku.rag-slim[...] >= X.Y.Z
# Update haiku.rag-slim (with or without extras) >= X.Y.Z
updated = re.sub(
r"(haiku\.rag-slim\[.*?\])>=[0-9.]+", rf"\1>={new_version}", content
r"(haiku\.rag-slim(?:\[.*?\])?)>=[0-9.]+", rf"\1>={new_version}", content
)
# Update haiku.rag >= X.Y.Z
updated = re.sub(r"(haiku\.rag)>=[0-9.]+", rf"\1>={new_version}", updated)

18
uv.lock
View file

@ -34,14 +34,14 @@ wheels = [
[[package]]
name = "ag-ui-protocol"
version = "0.1.11"
version = "0.1.13"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a2/c1/33ab11dc829c6c28d0d346988b2f394aa632d3ad63d1d2eb5f16eccd769b/ag_ui_protocol-0.1.11.tar.gz", hash = "sha256:b336dfebb5751e9cc2c676a3008a4bce4819004e6f6f8cba73169823564472ae", size = 6249, upload-time = "2026-02-11T12:41:36.085Z" }
sdist = { url = "https://files.pythonhosted.org/packages/04/b5/fc0b65b561d00d88811c8a7d98ee735833f81554be244340950e7b65820c/ag_ui_protocol-0.1.13.tar.gz", hash = "sha256:811d7d7dcce4783dec252918f40b717ebfa559399bf6b071c4ba47c0c1e21bcb", size = 5671, upload-time = "2026-02-19T18:40:38.602Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/83/5c6f4cb24d27d9cbe0c31ba2f3b4d1ff42bc6f87ba9facfa9e9d44046c6b/ag_ui_protocol-0.1.11-py3-none-any.whl", hash = "sha256:b0cc25570462a8eba8e57a098e0a2d6892a1f571a7bea7da2d4b60efd5d66789", size = 8392, upload-time = "2026-02-11T12:41:35.303Z" },
{ url = "https://files.pythonhosted.org/packages/cd/9f/b833c1ab1999da35ebad54841ae85d2c2764c931da9a6f52d8541b6901b2/ag_ui_protocol-0.1.13-py3-none-any.whl", hash = "sha256:1393fa894c1e8416efe184168a50689e760d05b32f4646eebb8ff423dddf8e8f", size = 8053, upload-time = "2026-02-19T18:40:37.27Z" },
]
[[package]]
@ -1513,7 +1513,7 @@ requires-dist = [
{ name = "cohere", marker = "extra == 'cohere'", specifier = ">=5.20.7" },
{ name = "docling", marker = "extra == 'docling'", specifier = ">=2.76.0" },
{ name = "docling-core", specifier = ">=2.67.1" },
{ name = "haiku-skills", specifier = ">=0.7.0" },
{ name = "haiku-skills", specifier = ">=0.8.0" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "jsonpatch", specifier = ">=1.33" },
{ name = "lancedb", specifier = "==0.29.2" },
@ -1545,17 +1545,19 @@ provides-extras = ["docling", "voyageai", "mxbai", "cohere", "zeroentropy", "jin
[[package]]
name = "haiku-skills"
version = "0.7.0"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ag-ui-protocol" },
{ name = "jsonpatch" },
{ name = "pydantic" },
{ name = "pydantic-ai-slim", extra = ["mcp"] },
{ name = "pydantic-ai-slim", extra = ["mcp", "openai"] },
{ name = "pyyaml" },
{ name = "skills-ref" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8c/a6/4fbdfe95e6ff6a574088b9f3ed7543a6e01b4b6c1740440ce07c1cf4893a/haiku_skills-0.7.0.tar.gz", hash = "sha256:ec5c5176f8feab09cc6aa4cda9a64e2446074f3ed111cb7e2587f7b524711364", size = 133420, upload-time = "2026-03-04T09:25:52.729Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/b0/ea75df7841f0a3ac807f741b6498b22273549838a374c2f11c1c363bac1e/haiku_skills-0.8.0.tar.gz", hash = "sha256:e582b37fa05a8e5cf06c5764248b7d1fd26e258510d9ba8e8a462d66249fcd39", size = 136349, upload-time = "2026-03-13T11:07:00.899Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d3/38/5048252c0b68bc77dd0fcf6b5c4e29074d90b3f6888eb282a0fcae967227/haiku_skills-0.7.0-py3-none-any.whl", hash = "sha256:7b7791890cc230fd53c5a9a7d93129e7901fecc1d64153d0386a83603e5b7b25", size = 25022, upload-time = "2026-03-04T09:25:51.872Z" },
{ url = "https://files.pythonhosted.org/packages/41/cf/8f5ddea650f06d0ced329b6d503b411fd275af41fc514b0402db36ef950f/haiku_skills-0.8.0-py3-none-any.whl", hash = "sha256:85efc8cbf29f78fb43a0538434f66413e3de6ac3a9b531bc1b7dcf440c99536b", size = 25684, upload-time = "2026-03-13T11:06:59.871Z" },
]
[[package]]