PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 All 128 releases
extendify / src / Agent / state / chat.js

chat.js in Extendify 3.2.2, at src/Agent/state/chat.js

138 lines 4.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { buildToolMessages } from '@agent/lib/tool-messages';
2 import { makeId } from '@agent/lib/util';
3 import { useStatusStore } from '@agent/state/status';
4 import apiFetch from '@wordpress/api-fetch';
5 import { create } from 'zustand';
6 import { createJSONStorage, devtools, persist } from 'zustand/middleware';
7
8 const { chatHistory } = window.extAgentData;
9
10 const state = (set, get) => ({
11 messages: chatHistory?.length ? chatHistory.toReversed() : [],
12 // API messages, back to the last finished workflow.
13 getCurrentMessages: ({ includeTools = true } = {}) => {
14 const messages = [];
15 let foundUserMessage = false;
16 for (const { type, details } of get().messages.toReversed()) {
17 const finished = ['completed', 'canceled'].includes(details.status);
18 if (type === 'workflow' && finished) break;
19 if (type === 'workflow-component' && finished) break;
20 if (type === 'message' && details.error) break;
21 // A call with no result leaves the model answering a phantom.
22 if (type === 'tool' && !('result' in details)) continue;
23 if (type === 'tool' && includeTools) {
24 // buildToolMessages returns [call, result]; push reversed so the
25 // final toReversed() restores call-before-result order.
26 for (const m of buildToolMessages(details).toReversed())
27 messages.push(m);
28 }
29 // This prevents a loop of assistant messages from being at the end
30 if (type === 'message' && details.role === 'user') {
31 foundUserMessage = true;
32 }
33 if (type === 'message' && !foundUserMessage) continue;
34 if (type === 'message') messages.push(details);
35 }
36 return messages.toReversed();
37 },
38 // Most recent consecutive runs — another workflow in between resets the
39 // memory.
40 getMessagesFor: (workflowId) => {
41 if (!workflowId) return [];
42 const segments = [];
43 let segment = [];
44 for (const { type, details } of get().messages) {
45 const finished = ['completed', 'canceled'].includes(details.status);
46 if (['workflow', 'workflow-component'].includes(type) && finished) {
47 // A canceled run's reply claims a change the next run then skips.
48 const kept =
49 details.status === 'canceled'
50 ? segment.filter(
51 (m) => m.role !== 'assistant' || typeof m.content !== 'string',
52 )
53 : segment;
54 segments.push({ workflowId: details.workflowId, segment: kept });
55 segment = [];
56 continue;
57 }
58 // An error ended a run without a marker; what precedes it is dead.
59 if (type === 'message' && details.error) {
60 segment = [];
61 continue;
62 }
63 if (type === 'tool' && 'result' in details) {
64 segment.push(...buildToolMessages(details, { summarize: true }));
65 continue;
66 }
67 if (type === 'message') segment.push(details);
68 }
69 const broken = segments.findLastIndex(
70 (finished) => finished.workflowId !== workflowId,
71 );
72 return segments.slice(broken + 1).flatMap(({ segment }) => segment);
73 },
74 getLastAssistantMessage: () =>
75 get()?.messages?.findLast(
76 (message) =>
77 message.type === 'message' && message.details?.role === 'assistant',
78 ),
79 hasMessages: () => get().messages.length > 0,
80 addMessage: (type, details) => {
81 const id = makeId();
82 set((state) => {
83 // max 250 messages
84 const max = Math.max(0, state.messages.length - 249);
85 const next = { id, type, details };
86 return {
87 // { id: 1, type: message, details: { role: 'user', content: 'Hello' } }
88 // { id: 2, type: message, details: { role: 'assistant', content: 'Hi there!' } }
89 // { id: 3, type: workflow, details: { name: 'Workflow 1' } }
90 messages: [...state.messages.toSpliced(0, max), next],
91 };
92 });
93 // A real message supersedes any in-flight progress status.
94 useStatusStore.getState().clearStatuses();
95 return id;
96 },
97 updateMessage: (id, details) =>
98 set((state) => ({
99 messages: state.messages.map((message) =>
100 message.id === id
101 ? { ...message, details: { ...message.details, ...details } }
102 : message,
103 ),
104 })),
105 // pop messages all the way back to the last agent message
106 popMessage: () => {
107 set((state) => ({
108 messages: state.messages?.slice(0, -1) || [],
109 }));
110 },
111 clearMessages: () => set({ messages: [] }),
112 });
113
114 const path = '/extendify/v1/agent/chat-events';
115 let lastSave = Promise.resolve();
116 const storage = {
117 getItem: async () => await apiFetch({ path }),
118 setItem: (_name, state) => {
119 lastSave = apiFetch({ path, method: 'POST', data: { state } });
120 return lastSave;
121 },
122 };
123
124 // Await before navigating — an aborted in-flight save loses the newest messages.
125 export const flushChatStorage = (timeout = 5000) =>
126 Promise.race([
127 lastSave.catch(() => null),
128 new Promise((resolve) => setTimeout(resolve, timeout)),
129 ]);
130
131 export const useChatStore = create()(
132 persist(devtools(state, { name: 'Extendify Agent Chat' }), {
133 name: `extendify-agent-chat-${window.extSharedData.siteId}`,
134 storage: createJSONStorage(() => storage),
135 skipHydration: true,
136 }),
137 );
138