forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreact-core.ts
More file actions
156 lines (138 loc) · 5.63 KB
/
Copy pathreact-core.ts
File metadata and controls
156 lines (138 loc) · 5.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import React from "react";
import { ReactActivityMessageRenderer, ReactToolCallRenderer } from "../types";
import { ReactCustomMessageRenderer } from "../types/react-custom-message-renderer";
import {
CopilotKitCore,
type CopilotKitCoreConfig,
type CopilotKitCoreSubscriber,
type CopilotKitCoreSubscription,
} from "@copilotkit/core";
export interface CopilotKitCoreReactConfig extends CopilotKitCoreConfig {
// Add any additional configuration properties specific to the React implementation
renderToolCalls?: ReactToolCallRenderer<any>[];
renderActivityMessages?: ReactActivityMessageRenderer<any>[];
// Add custom message renderers
renderCustomMessages?: ReactCustomMessageRenderer[];
}
export interface CopilotKitCoreReactSubscriber extends CopilotKitCoreSubscriber {
onRenderToolCallsChanged?: (event: {
copilotkit: CopilotKitCore;
renderToolCalls: ReactToolCallRenderer<any>[];
}) => void | Promise<void>;
onInterruptElementChanged?: (event: {
copilotkit: CopilotKitCore;
interruptElement: React.ReactElement | null;
}) => void | Promise<void>;
}
export class CopilotKitCoreReact extends CopilotKitCore {
private _renderToolCalls: ReactToolCallRenderer<any>[] = [];
private _hookRenderToolCalls: Map<string, ReactToolCallRenderer<any>> =
new Map();
private _cachedMergedRenderToolCalls: ReactToolCallRenderer<any>[] | null =
null;
private _renderCustomMessages: ReactCustomMessageRenderer[] = [];
private _renderActivityMessages: ReactActivityMessageRenderer<any>[] = [];
private _interruptElement: React.ReactElement | null = null;
constructor(config: CopilotKitCoreReactConfig) {
super(config);
this._renderToolCalls = config.renderToolCalls ?? [];
this._renderCustomMessages = config.renderCustomMessages ?? [];
this._renderActivityMessages = config.renderActivityMessages ?? [];
}
get renderCustomMessages(): Readonly<ReactCustomMessageRenderer[]> {
return this._renderCustomMessages;
}
get renderActivityMessages(): Readonly<ReactActivityMessageRenderer<any>>[] {
return this._renderActivityMessages;
}
get renderToolCalls(): Readonly<ReactToolCallRenderer<any>>[] {
if (this._hookRenderToolCalls.size === 0) {
return this._renderToolCalls;
}
if (this._cachedMergedRenderToolCalls) {
return this._cachedMergedRenderToolCalls;
}
// Merge: hook entries override prop entries with the same key
const merged = new Map<string, ReactToolCallRenderer<any>>();
for (const rc of this._renderToolCalls) {
merged.set(`${rc.agentId ?? ""}:${rc.name}`, rc);
}
for (const [key, rc] of this._hookRenderToolCalls) {
merged.set(key, rc);
}
this._cachedMergedRenderToolCalls = Array.from(merged.values());
return this._cachedMergedRenderToolCalls;
}
setRenderActivityMessages(
renderers: ReactActivityMessageRenderer<any>[],
): void {
this._renderActivityMessages = renderers;
}
setRenderCustomMessages(renderers: ReactCustomMessageRenderer[]): void {
this._renderCustomMessages = renderers;
}
setRenderToolCalls(renderToolCalls: ReactToolCallRenderer<any>[]): void {
this._renderToolCalls = renderToolCalls;
this._cachedMergedRenderToolCalls = null;
this._notifyRenderToolCallsChanged();
}
addHookRenderToolCall(entry: ReactToolCallRenderer<any>): void {
const key = `${entry.agentId ?? ""}:${entry.name}`;
this._hookRenderToolCalls.set(key, entry);
this._cachedMergedRenderToolCalls = null;
this._notifyRenderToolCallsChanged();
}
removeHookRenderToolCall(name: string, agentId?: string): void {
const key = `${agentId ?? ""}:${name}`;
if (this._hookRenderToolCalls.delete(key)) {
this._cachedMergedRenderToolCalls = null;
this._notifyRenderToolCallsChanged();
}
}
private _notifyRenderToolCallsChanged(): void {
void this.notifySubscribers((subscriber) => {
const reactSubscriber = subscriber as CopilotKitCoreReactSubscriber;
if (reactSubscriber.onRenderToolCallsChanged) {
reactSubscriber.onRenderToolCallsChanged({
copilotkit: this,
renderToolCalls: this.renderToolCalls,
});
}
}, "Subscriber onRenderToolCallsChanged error:");
}
get interruptElement(): React.ReactElement | null {
return this._interruptElement;
}
setInterruptElement(element: React.ReactElement | null): void {
this._interruptElement = element;
void this.notifySubscribers((subscriber) => {
const reactSubscriber = subscriber as CopilotKitCoreReactSubscriber;
reactSubscriber.onInterruptElementChanged?.({
copilotkit: this,
interruptElement: this._interruptElement,
});
}, "Subscriber onInterruptElementChanged error:");
}
// Override to accept React-specific subscriber type
subscribe(
subscriber: CopilotKitCoreReactSubscriber,
): CopilotKitCoreSubscription {
return super.subscribe(subscriber);
}
/**
* Wait for pending React state updates before the follow-up agent run.
*
* When a frontend tool handler calls setState(), React 18 batches the update
* and schedules a commit via its internal scheduler (MessageChannel). The
* useAgentContext hook registers context via useLayoutEffect, which runs
* synchronously after React commits that batch.
*
* Awaiting a zero-delay timeout yields to the macrotask queue. React's
* MessageChannel task runs first, committing the pending state and running
* useLayoutEffect (which updates the context store). The follow-up runAgent
* call then reads fresh context.
*/
async waitForPendingFrameworkUpdates(): Promise<void> {
await new Promise<void>((resolve) => setTimeout(resolve, 0));
}
}