Skip to content

Commit af70d7f

Browse files
authored
feat(core): add useToolArgsStatus hook (#3712)
1 parent 876f75d commit af70d7f

File tree

8 files changed

+101
-0
lines changed

8 files changed

+101
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@assistant-ui/core": patch
3+
"@assistant-ui/react": patch
4+
"@assistant-ui/react-native": patch
5+
"@assistant-ui/react-ink": patch
6+
---
7+
8+
feat: add useToolArgsStatus hook for per-prop streaming status
9+
10+
Add a convenience hook that derives per-property streaming completion status from tool call args using structural partial JSON analysis.

apps/docs/content/docs/(docs)/guides/tool-ui.mdx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,35 @@ useAssistantToolUI({
748748
server.
749749
</Callout>
750750

751+
## Per-Property Streaming Status
752+
753+
When rendering a tool UI, you can track which arguments have finished streaming using `useToolArgsStatus`. This must be used inside a tool-call message part context.
754+
755+
```tsx
756+
import { useToolArgsStatus } from "@assistant-ui/react";
757+
758+
const WeatherUI = makeAssistantToolUI({
759+
toolName: "weather",
760+
render: ({ args }) => {
761+
const { status, propStatus } = useToolArgsStatus<{
762+
location: string;
763+
unit: string;
764+
}>();
765+
766+
return (
767+
<div>
768+
<span className={propStatus.location === "streaming" ? "animate-pulse" : ""}>
769+
{args.location ?? "..."}
770+
</span>
771+
{status === "complete" && <WeatherChart data={args} />}
772+
</div>
773+
);
774+
},
775+
});
776+
```
777+
778+
`propStatus` maps each key to `"streaming"` | `"complete"` once the key appears in the partial JSON. Keys not yet present in the stream are absent from `propStatus`.
779+
751780
## Related Guides
752781

753782
- [Tools Guide](/docs/guides/tools) - Learn how to create and use tools with AI models

packages/assistant-stream/src/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export {
22
parsePartialJsonObject,
33
getPartialJsonObjectFieldState,
4+
getPartialJsonObjectMeta,
45
} from "./utils/json/parse-partial-json-object";
56
export {
67
type AsyncIterableStream,

packages/core/src/react/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export {
3333
type AssistantInteractableProps,
3434
} from "./model-context/useAssistantInteractable";
3535
export { useInteractableState } from "./model-context/useInteractableState";
36+
export {
37+
useToolArgsStatus,
38+
type ToolArgsStatus,
39+
} from "./model-context/useToolArgsStatus";
3640

3741
// client
3842
export { Tools } from "./client/Tools";
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useMemo } from "react";
2+
import { useAuiState } from "@assistant-ui/store";
3+
import {
4+
getPartialJsonObjectFieldState,
5+
getPartialJsonObjectMeta,
6+
} from "assistant-stream/utils";
7+
8+
type PropFieldStatus = "streaming" | "complete";
9+
10+
export type ToolArgsStatus<
11+
TArgs extends Record<string, unknown> = Record<string, unknown>,
12+
> = {
13+
status: "running" | "complete" | "incomplete" | "requires-action";
14+
propStatus: Partial<Record<keyof TArgs, PropFieldStatus>>;
15+
};
16+
17+
export const useToolArgsStatus = <
18+
TArgs extends Record<string, unknown> = Record<string, unknown>,
19+
>(): ToolArgsStatus<TArgs> => {
20+
const part = useAuiState((s) => s.part);
21+
22+
return useMemo(() => {
23+
const statusType = part.status.type;
24+
25+
if (part.type !== "tool-call") {
26+
throw new Error(
27+
"useToolArgsStatus can only be used inside tool-call message parts",
28+
);
29+
}
30+
31+
const isStreaming = statusType === "running";
32+
const args = part.args as Record<string, unknown>;
33+
const meta = getPartialJsonObjectMeta(args as Record<symbol, unknown>);
34+
const propStatus: Partial<Record<string, PropFieldStatus>> = {};
35+
36+
for (const key of Object.keys(args)) {
37+
if (meta) {
38+
const fieldState = getPartialJsonObjectFieldState(args, [key]);
39+
propStatus[key] =
40+
fieldState === "complete" || !isStreaming ? "complete" : "streaming";
41+
} else {
42+
propStatus[key] = isStreaming ? "streaming" : "complete";
43+
}
44+
}
45+
46+
return {
47+
status: statusType,
48+
propStatus: propStatus as Partial<Record<keyof TArgs, PropFieldStatus>>,
49+
};
50+
}, [part]);
51+
};

packages/react-ink/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ export {
152152
useAssistantInteractable,
153153
type AssistantInteractableProps,
154154
useInteractableState,
155+
useToolArgsStatus,
156+
type ToolArgsStatus,
155157
} from "@assistant-ui/core/react";
156158
export type {
157159
ModelContext,

packages/react-native/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ export {
149149
useAssistantInteractable,
150150
type AssistantInteractableProps,
151151
useInteractableState,
152+
useToolArgsStatus,
153+
type ToolArgsStatus,
152154
} from "@assistant-ui/core/react";
153155

154156
export type {

packages/react/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ export {
203203
useAssistantInteractable,
204204
type AssistantInteractableProps,
205205
useInteractableState,
206+
useToolArgsStatus,
207+
type ToolArgsStatus,
206208
} from "@assistant-ui/core/react";
207209

208210
// Core pass-through (unchanged)

0 commit comments

Comments
 (0)