@assistant-ui/react-ag-ui 0.0.29 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +41 -0
  2. package/dist/index.d.ts +2 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +11 -0
  5. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  6. package/dist/runtime/AgUiThreadRuntimeCore.js +185 -12
  7. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  8. package/dist/runtime/adapter/run-aggregator.d.ts +10 -1
  9. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  10. package/dist/runtime/adapter/run-aggregator.js +52 -6
  11. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  12. package/dist/runtime/adapter/subscriber.d.ts +5 -0
  13. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  14. package/dist/runtime/adapter/subscriber.js +55 -36
  15. package/dist/runtime/adapter/subscriber.js.map +1 -1
  16. package/dist/runtime/event-parser.d.ts +5 -1
  17. package/dist/runtime/event-parser.d.ts.map +1 -1
  18. package/dist/runtime/event-parser.js +48 -2
  19. package/dist/runtime/event-parser.js.map +1 -1
  20. package/dist/runtime/types.d.ts +22 -0
  21. package/dist/runtime/types.d.ts.map +1 -1
  22. package/dist/useAgUiRuntime.d.ts +6 -2
  23. package/dist/useAgUiRuntime.d.ts.map +1 -1
  24. package/dist/useAgUiRuntime.js +7 -1
  25. package/dist/useAgUiRuntime.js.map +1 -1
  26. package/package.json +4 -5
  27. package/src/index.ts +5 -0
  28. package/src/runtime/AgUiThreadRuntimeCore.ts +250 -11
  29. package/src/runtime/adapter/run-aggregator.ts +67 -9
  30. package/src/runtime/adapter/subscriber.ts +53 -43
  31. package/src/runtime/event-parser.ts +64 -3
  32. package/src/runtime/types.ts +31 -1
  33. package/src/useAgUiRuntime.ts +23 -3
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { AgUiEvent } from "../types";
4
4
  import { parseAgUiEvent } from "../event-parser";
5
+ import type { Logger } from "../logger";
5
6
 
6
7
  type Dispatch = (event: AgUiEvent) => void;
7
8
 
@@ -31,6 +32,7 @@ type Subscriber = {
31
32
  onMessagesSnapshotEvent?: (payload: { event: unknown }) => void;
32
33
  onCustomEvent?: (payload: { event: unknown }) => void;
33
34
  onRawEvent?: (payload: { event: unknown }) => void;
35
+ onRunFinishedEvent?: (payload: { event: unknown }) => void;
34
36
  onRunFinalized?: () => void;
35
37
  onRunFailed?: (payload: { error: Error }) => void;
36
38
  };
@@ -38,37 +40,40 @@ type Subscriber = {
38
40
  const ensureEvent = (
39
41
  raw: unknown,
40
42
  type: AgUiEvent["type"],
43
+ logger?: Logger,
41
44
  ): AgUiEvent | null => {
45
+ const parseOptions = logger ? { logger } : undefined;
46
+ let parsed: AgUiEvent | null;
42
47
  if (raw && typeof raw === "object") {
43
48
  const payload = raw as Record<string, unknown>;
44
49
  if (typeof payload.type === "string") {
45
- return parseAgUiEvent(payload);
50
+ parsed = parseAgUiEvent(payload, parseOptions);
51
+ } else {
52
+ parsed = parseAgUiEvent({ type, ...payload }, parseOptions);
46
53
  }
47
- return parseAgUiEvent({ type, ...payload });
54
+ } else {
55
+ parsed = parseAgUiEvent({ type }, parseOptions);
48
56
  }
49
- return parseAgUiEvent({ type });
50
- };
51
-
52
- const dispatchIfValid = (
53
- dispatch: Dispatch,
54
- raw: unknown,
55
- type: AgUiEvent["type"],
56
- ) => {
57
- const event = ensureEvent(raw, type);
58
- if (!event) return;
59
- dispatch(event);
57
+ return parsed && parsed.type === type ? parsed : null;
60
58
  };
61
59
 
62
60
  type SubscriberOptions = {
63
61
  dispatch: Dispatch;
64
62
  runId: string;
63
+ logger?: Logger;
65
64
  onRunFailed?: (error: Error) => void;
66
65
  };
67
66
 
68
67
  export const createAgUiSubscriber = (
69
68
  options: SubscriberOptions,
70
69
  ): Subscriber => {
71
- const { dispatch, runId, onRunFailed } = options;
70
+ const { dispatch, runId, logger, onRunFailed } = options;
71
+ let runFinishedDispatched = false;
72
+ const dispatchIfValid = (raw: unknown, type: AgUiEvent["type"]) => {
73
+ const event = ensureEvent(raw, type, logger);
74
+ if (!event) return;
75
+ dispatch(event);
76
+ };
72
77
  return {
73
78
  onEvent: ({ event }) => {
74
79
  const typeCandidate =
@@ -83,52 +88,57 @@ export const createAgUiSubscriber = (
83
88
  if (parsed) dispatch(parsed);
84
89
  },
85
90
  onTextMessageStartEvent: ({ event }) =>
86
- dispatchIfValid(dispatch, event, "TEXT_MESSAGE_START"),
91
+ dispatchIfValid(event, "TEXT_MESSAGE_START"),
87
92
  onTextMessageContentEvent: ({ event }) =>
88
- dispatchIfValid(dispatch, event, "TEXT_MESSAGE_CONTENT"),
93
+ dispatchIfValid(event, "TEXT_MESSAGE_CONTENT"),
89
94
  onTextMessageEndEvent: ({ event }) =>
90
- dispatchIfValid(dispatch, event, "TEXT_MESSAGE_END"),
95
+ dispatchIfValid(event, "TEXT_MESSAGE_END"),
91
96
  onTextMessageChunkEvent: ({ event }) =>
92
- dispatchIfValid(dispatch, event, "TEXT_MESSAGE_CHUNK"),
97
+ dispatchIfValid(event, "TEXT_MESSAGE_CHUNK"),
93
98
  onThinkingStartEvent: ({ event }) =>
94
- dispatchIfValid(dispatch, event, "THINKING_START"),
95
- onThinkingEndEvent: ({ event }) =>
96
- dispatchIfValid(dispatch, event, "THINKING_END"),
99
+ dispatchIfValid(event, "THINKING_START"),
100
+ onThinkingEndEvent: ({ event }) => dispatchIfValid(event, "THINKING_END"),
97
101
  onThinkingTextMessageStartEvent: ({ event }) =>
98
- dispatchIfValid(dispatch, event, "THINKING_TEXT_MESSAGE_START"),
102
+ dispatchIfValid(event, "THINKING_TEXT_MESSAGE_START"),
99
103
  onThinkingTextMessageContentEvent: ({ event }) =>
100
- dispatchIfValid(dispatch, event, "THINKING_TEXT_MESSAGE_CONTENT"),
104
+ dispatchIfValid(event, "THINKING_TEXT_MESSAGE_CONTENT"),
101
105
  onThinkingTextMessageEndEvent: ({ event }) =>
102
- dispatchIfValid(dispatch, event, "THINKING_TEXT_MESSAGE_END"),
106
+ dispatchIfValid(event, "THINKING_TEXT_MESSAGE_END"),
103
107
  onReasoningStartEvent: ({ event }) =>
104
- dispatchIfValid(dispatch, event, "REASONING_START"),
105
- onReasoningEndEvent: ({ event }) =>
106
- dispatchIfValid(dispatch, event, "REASONING_END"),
108
+ dispatchIfValid(event, "REASONING_START"),
109
+ onReasoningEndEvent: ({ event }) => dispatchIfValid(event, "REASONING_END"),
107
110
  onReasoningMessageStartEvent: ({ event }) =>
108
- dispatchIfValid(dispatch, event, "REASONING_MESSAGE_START"),
111
+ dispatchIfValid(event, "REASONING_MESSAGE_START"),
109
112
  onReasoningMessageContentEvent: ({ event }) =>
110
- dispatchIfValid(dispatch, event, "REASONING_MESSAGE_CONTENT"),
113
+ dispatchIfValid(event, "REASONING_MESSAGE_CONTENT"),
111
114
  onReasoningMessageEndEvent: ({ event }) =>
112
- dispatchIfValid(dispatch, event, "REASONING_MESSAGE_END"),
115
+ dispatchIfValid(event, "REASONING_MESSAGE_END"),
113
116
  onToolCallStartEvent: ({ event }) =>
114
- dispatchIfValid(dispatch, event, "TOOL_CALL_START"),
117
+ dispatchIfValid(event, "TOOL_CALL_START"),
115
118
  onToolCallArgsEvent: ({ event }) =>
116
- dispatchIfValid(dispatch, event, "TOOL_CALL_ARGS"),
117
- onToolCallEndEvent: ({ event }) =>
118
- dispatchIfValid(dispatch, event, "TOOL_CALL_END"),
119
+ dispatchIfValid(event, "TOOL_CALL_ARGS"),
120
+ onToolCallEndEvent: ({ event }) => dispatchIfValid(event, "TOOL_CALL_END"),
119
121
  onToolCallChunkEvent: ({ event }) =>
120
- dispatchIfValid(dispatch, event, "TOOL_CALL_CHUNK"),
122
+ dispatchIfValid(event, "TOOL_CALL_CHUNK"),
121
123
  onToolCallResultEvent: ({ event }) =>
122
- dispatchIfValid(dispatch, event, "TOOL_CALL_RESULT"),
124
+ dispatchIfValid(event, "TOOL_CALL_RESULT"),
123
125
  onStateSnapshotEvent: ({ event }) =>
124
- dispatchIfValid(dispatch, event, "STATE_SNAPSHOT"),
125
- onStateDeltaEvent: ({ event }) =>
126
- dispatchIfValid(dispatch, event, "STATE_DELTA"),
126
+ dispatchIfValid(event, "STATE_SNAPSHOT"),
127
+ onStateDeltaEvent: ({ event }) => dispatchIfValid(event, "STATE_DELTA"),
127
128
  onMessagesSnapshotEvent: ({ event }) =>
128
- dispatchIfValid(dispatch, event, "MESSAGES_SNAPSHOT"),
129
- onCustomEvent: ({ event }) => dispatchIfValid(dispatch, event, "CUSTOM"),
130
- onRawEvent: ({ event }) => dispatchIfValid(dispatch, event, "RAW"),
131
- onRunFinalized: () => dispatch({ type: "RUN_FINISHED", runId }),
129
+ dispatchIfValid(event, "MESSAGES_SNAPSHOT"),
130
+ onCustomEvent: ({ event }) => dispatchIfValid(event, "CUSTOM"),
131
+ onRawEvent: ({ event }) => dispatchIfValid(event, "RAW"),
132
+ onRunFinishedEvent: ({ event }) => {
133
+ const parsed = ensureEvent(event, "RUN_FINISHED", logger);
134
+ if (!parsed) return;
135
+ runFinishedDispatched = true;
136
+ dispatch(parsed);
137
+ },
138
+ onRunFinalized: () => {
139
+ if (runFinishedDispatched) return;
140
+ dispatch({ type: "RUN_FINISHED", runId });
141
+ },
132
142
  onRunFailed: ({ error }) => {
133
143
  onRunFailed?.(error);
134
144
  const message =
@@ -1,4 +1,9 @@
1
- import type { AgUiEvent } from "./types";
1
+ import type { AgUiEvent, AgUiInterrupt, AgUiRunFinishedOutcome } from "./types";
2
+ import type { Logger } from "./logger";
3
+
4
+ export type ParseAgUiEventOptions = {
5
+ logger?: Logger;
6
+ };
2
7
 
3
8
  const isString = (value: unknown): value is string => typeof value === "string";
4
9
  const isNonEmptyString = (value: unknown): value is string =>
@@ -16,7 +21,57 @@ const withOptional = <T extends object>(
16
21
  : ({ ...base, ...Object.fromEntries(definedEntries) } as T);
17
22
  };
18
23
 
19
- export const parseAgUiEvent = (event: unknown): AgUiEvent | null => {
24
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
25
+ !!value && typeof value === "object" && !Array.isArray(value);
26
+
27
+ const parseInterrupt = (raw: unknown): AgUiInterrupt | null => {
28
+ if (!isPlainObject(raw)) return null;
29
+ const id = raw.id;
30
+ const reason = raw.reason;
31
+ if (typeof id !== "string" || typeof reason !== "string") return null;
32
+ const interrupt: AgUiInterrupt = { id, reason };
33
+ if (typeof raw.message === "string") interrupt.message = raw.message;
34
+ if (typeof raw.toolCallId === "string") interrupt.toolCallId = raw.toolCallId;
35
+ if (typeof raw.expiresAt === "string") interrupt.expiresAt = raw.expiresAt;
36
+ if (isPlainObject(raw.responseSchema))
37
+ interrupt.responseSchema = raw.responseSchema;
38
+ if (isPlainObject(raw.metadata)) interrupt.metadata = raw.metadata;
39
+ return interrupt;
40
+ };
41
+
42
+ const parseRunFinishedOutcome = (
43
+ raw: unknown,
44
+ logger: Logger | undefined,
45
+ ): AgUiRunFinishedOutcome | undefined => {
46
+ if (!isPlainObject(raw)) return undefined;
47
+ if (raw.type === "success") return { type: "success" };
48
+ if (raw.type === "interrupt") {
49
+ if (!Array.isArray(raw.interrupts)) {
50
+ logger?.debug?.(
51
+ "[agui] RUN_FINISHED interrupt outcome missing interrupts array",
52
+ raw,
53
+ );
54
+ return undefined;
55
+ }
56
+ const parsed = raw.interrupts
57
+ .map((entry) => parseInterrupt(entry))
58
+ .filter((entry): entry is AgUiInterrupt => entry !== null);
59
+ if (parsed.length === 0) {
60
+ logger?.debug?.(
61
+ "[agui] RUN_FINISHED interrupt outcome has no valid interrupts",
62
+ raw.interrupts,
63
+ );
64
+ return undefined;
65
+ }
66
+ return { type: "interrupt", interrupts: parsed };
67
+ }
68
+ return undefined;
69
+ };
70
+
71
+ export const parseAgUiEvent = (
72
+ event: unknown,
73
+ options?: ParseAgUiEventOptions,
74
+ ): AgUiEvent | null => {
20
75
  if (!event || typeof event !== "object") return null;
21
76
  const payload = event as Record<string, unknown>;
22
77
  const typeValue = payload.type;
@@ -32,7 +87,13 @@ export const parseAgUiEvent = (event: unknown): AgUiEvent | null => {
32
87
  }
33
88
  case "RUN_FINISHED": {
34
89
  const runId = getString("runId");
35
- return runId ? { type: "RUN_FINISHED", runId } : null;
90
+ if (!runId) return null;
91
+ return withOptional(
92
+ { type: "RUN_FINISHED" as const, runId },
93
+ {
94
+ outcome: parseRunFinishedOutcome(payload.outcome, options?.logger),
95
+ },
96
+ );
36
97
  }
37
98
  case "RUN_CANCELLED": {
38
99
  const runId = getString("runId");
@@ -47,9 +47,39 @@ export type UseAgUiRuntimeOptions = {
47
47
  adapters?: UseAgUiRuntimeAdapters;
48
48
  };
49
49
 
50
+ export type AgUiInterruptReason =
51
+ | "tool_call"
52
+ | "input_required"
53
+ | "confirmation"
54
+ | (string & {});
55
+
56
+ export type AgUiInterrupt = {
57
+ id: string;
58
+ reason: AgUiInterruptReason;
59
+ message?: string;
60
+ toolCallId?: string;
61
+ responseSchema?: Record<string, unknown>;
62
+ expiresAt?: string;
63
+ metadata?: Record<string, unknown>;
64
+ };
65
+
66
+ export type AgUiResumeEntry = {
67
+ interruptId: string;
68
+ status: "resolved" | "cancelled";
69
+ payload?: unknown;
70
+ };
71
+
72
+ export type AgUiRunFinishedOutcome =
73
+ | { type: "success" }
74
+ | { type: "interrupt"; interrupts: AgUiInterrupt[] };
75
+
50
76
  export type AgUiEvent =
51
77
  | { type: "RUN_STARTED"; runId: string }
52
- | { type: "RUN_FINISHED"; runId: string }
78
+ | {
79
+ type: "RUN_FINISHED";
80
+ runId: string;
81
+ outcome?: AgUiRunFinishedOutcome;
82
+ }
53
83
  | { type: "RUN_CANCELLED"; runId?: string }
54
84
  | { type: "RUN_ERROR"; message?: string; code?: string }
55
85
  | { type: "TEXT_MESSAGE_START"; messageId?: string }
@@ -15,12 +15,23 @@ import type {
15
15
  } from "@assistant-ui/core";
16
16
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
17
17
  import { makeLogger } from "./runtime/logger";
18
- import type { UseAgUiRuntimeOptions } from "./runtime/types";
18
+ import type {
19
+ AgUiInterrupt,
20
+ AgUiResumeEntry,
21
+ UseAgUiRuntimeOptions,
22
+ } from "./runtime/types";
19
23
  import { AgUiThreadRuntimeCore } from "./runtime/AgUiThreadRuntimeCore";
20
24
 
25
+ export type AgUiAssistantRuntime = AssistantRuntime & {
26
+ unstable_getPendingInterrupts: () => readonly AgUiInterrupt[];
27
+ unstable_submitInterruptResponses: (
28
+ responses: readonly AgUiResumeEntry[],
29
+ ) => Promise<void>;
30
+ };
31
+
21
32
  export function useAgUiRuntime(
22
33
  options: UseAgUiRuntimeOptions,
23
- ): AssistantRuntime {
34
+ ): AgUiAssistantRuntime {
24
35
  const logger = useMemo(() => makeLogger(options.logger), [options.logger]);
25
36
  const [_version, setVersion] = useState(0);
26
37
  const notifyUpdate = useCallback(() => setVersion((v) => v + 1), []);
@@ -174,7 +185,16 @@ export function useAgUiRuntime(
174
185
  [adapterAdapters, core, _version, hasExecutingTools],
175
186
  );
176
187
 
177
- const runtime = useExternalStoreRuntime(store);
188
+ const baseRuntime = useExternalStoreRuntime(store);
189
+
190
+ const runtime = useMemo<AgUiAssistantRuntime>(() => {
191
+ const wrapper = Object.create(baseRuntime) as AgUiAssistantRuntime;
192
+ wrapper.unstable_getPendingInterrupts = () =>
193
+ core.getPendingInterrupts()?.interrupts ?? [];
194
+ wrapper.unstable_submitInterruptResponses = (responses) =>
195
+ core.submitInterruptResponses(responses);
196
+ return wrapper;
197
+ }, [baseRuntime, core]);
178
198
 
179
199
  useEffect(() => {
180
200
  core.attachRuntime(runtime);