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:
commit
5851c2482c
11 changed files with 627 additions and 304 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
18
uv.lock
|
|
@ -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]]
|
||||
|
|
|
|||
Loading…
Reference in a new issue