connectonion 0.0.18 → 0.0.21
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.
- package/dist/connect/auth.d.ts +17 -0
- package/dist/connect/auth.d.ts.map +1 -0
- package/dist/connect/auth.js +90 -0
- package/dist/connect/chat-item-mapper.d.ts +12 -0
- package/dist/connect/chat-item-mapper.d.ts.map +1 -0
- package/dist/connect/chat-item-mapper.js +169 -0
- package/dist/connect/endpoint.d.ts +9 -4
- package/dist/connect/endpoint.d.ts.map +1 -1
- package/dist/connect/endpoint.js +9 -25
- package/dist/connect/index.d.ts +7 -0
- package/dist/connect/index.d.ts.map +1 -1
- package/dist/connect/remote-agent.d.ts +55 -73
- package/dist/connect/remote-agent.d.ts.map +1 -1
- package/dist/connect/remote-agent.js +476 -736
- package/dist/connect/types.d.ts +8 -0
- package/dist/connect/types.d.ts.map +1 -1
- package/dist/react/index.d.ts +5 -95
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +8 -230
- package/dist/react/store.d.ts +44 -0
- package/dist/react/store.d.ts.map +1 -0
- package/dist/react/store.js +61 -0
- package/dist/react/use-agent-for-human.d.ts +145 -0
- package/dist/react/use-agent-for-human.d.ts.map +1 -0
- package/dist/react/use-agent-for-human.js +203 -0
- package/package.json +1 -1
|
@@ -1,109 +1,129 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.RemoteAgent = void 0;
|
|
37
|
-
const address = __importStar(require("../address"));
|
|
38
4
|
const endpoint_1 = require("./endpoint");
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
* @example
|
|
43
|
-
* ```typescript
|
|
44
|
-
* const agent = connect("0x123abc");
|
|
45
|
-
*
|
|
46
|
-
* // Simple usage
|
|
47
|
-
* const response = await agent.input("Search for Python docs");
|
|
48
|
-
* console.log(response.text); // Agent's response
|
|
49
|
-
* console.log(response.done); // true if complete
|
|
50
|
-
*
|
|
51
|
-
* // Access UI events for rendering
|
|
52
|
-
* console.log(agent.ui); // Array of UI events
|
|
53
|
-
* console.log(agent.status); // 'idle' | 'working' | 'waiting'
|
|
54
|
-
* ```
|
|
55
|
-
*/
|
|
5
|
+
const auth_1 = require("./auth");
|
|
6
|
+
const chat_item_mapper_1 = require("./chat-item-mapper");
|
|
56
7
|
class RemoteAgent {
|
|
57
|
-
|
|
58
|
-
get agentAddress() {
|
|
59
|
-
return this.address;
|
|
60
|
-
}
|
|
8
|
+
set onMessage(fn) { this._onMessage = fn; }
|
|
61
9
|
constructor(agentAddress, options = {}) {
|
|
62
10
|
this._endpointResolutionAttempted = false;
|
|
11
|
+
// Public reactive state
|
|
63
12
|
this._status = 'idle';
|
|
64
13
|
this._connectionState = 'disconnected';
|
|
65
14
|
this._currentSession = null;
|
|
66
15
|
this._chatItems = [];
|
|
67
|
-
this.
|
|
68
|
-
|
|
69
|
-
this.
|
|
70
|
-
this.
|
|
16
|
+
this._error = null;
|
|
17
|
+
// Persistent WebSocket
|
|
18
|
+
this._ws = null;
|
|
19
|
+
this._authenticated = false;
|
|
20
|
+
// Promise resolution for current input() call
|
|
21
|
+
this._inputResolve = null;
|
|
22
|
+
this._inputReject = null;
|
|
23
|
+
this._inputTimer = null;
|
|
24
|
+
// Pending retry after onboard
|
|
25
|
+
this._pendingRetry = null;
|
|
26
|
+
// PING/PONG health check
|
|
71
27
|
this._lastPingTime = 0;
|
|
72
|
-
this.
|
|
73
|
-
//
|
|
74
|
-
this.
|
|
75
|
-
this.
|
|
76
|
-
this.
|
|
77
|
-
this._shouldReconnect = false;
|
|
28
|
+
this._pingTimer = null;
|
|
29
|
+
// Callback + promise for ensureConnected
|
|
30
|
+
this._connectResolve = null;
|
|
31
|
+
this._connectReject = null;
|
|
32
|
+
this._onMessage = null;
|
|
78
33
|
this.address = agentAddress;
|
|
79
|
-
this._relayUrl = (0, endpoint_1.
|
|
34
|
+
this._relayUrl = (0, endpoint_1.normalizeRelayUrl)(options.relayUrl || 'wss://oo.openonion.ai');
|
|
80
35
|
this._directUrl = options.directUrl?.replace(/\/$/, '');
|
|
81
|
-
this._WS = options.wsCtor || (0, endpoint_1.
|
|
82
|
-
if (options.keys)
|
|
36
|
+
this._WS = options.wsCtor || (0, endpoint_1.getWebSocketCtor)();
|
|
37
|
+
if (options.keys)
|
|
83
38
|
this._keys = options.keys;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
// ==========================================================================
|
|
87
|
-
// Public Properties
|
|
88
|
-
// ==========================================================================
|
|
89
|
-
get status() {
|
|
90
|
-
return this._status;
|
|
91
39
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
get
|
|
96
|
-
|
|
40
|
+
// --- Public getters ---
|
|
41
|
+
get agentAddress() { return this.address; }
|
|
42
|
+
get status() { return this._status; }
|
|
43
|
+
get connectionState() { return this._connectionState; }
|
|
44
|
+
get currentSession() { return this._currentSession; }
|
|
45
|
+
get ui() { return this._chatItems; }
|
|
46
|
+
get mode() { return this._currentSession?.mode || 'safe'; }
|
|
47
|
+
get error() { return this._error || null; }
|
|
48
|
+
// --- Public API ---
|
|
49
|
+
async input(prompt, options) {
|
|
50
|
+
const timeoutMs = options?.timeoutMs ?? 600000;
|
|
51
|
+
this._addChatItem({ type: 'user', content: prompt, images: options?.images });
|
|
52
|
+
this._addChatItem({ type: 'thinking', id: '__optimistic__', status: 'running' });
|
|
53
|
+
this._status = 'working';
|
|
54
|
+
this._onMessage?.();
|
|
55
|
+
await this._ensureConnected();
|
|
56
|
+
const inputId = (0, endpoint_1.generateUUID)();
|
|
57
|
+
const isDirect = this._isDirect();
|
|
58
|
+
const msg = { type: 'INPUT', input_id: inputId, prompt };
|
|
59
|
+
if (options?.images?.length)
|
|
60
|
+
msg.images = options.images;
|
|
61
|
+
if (!isDirect)
|
|
62
|
+
msg.to = this.address;
|
|
63
|
+
this._ws.send(JSON.stringify(msg));
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
this._inputResolve = resolve;
|
|
66
|
+
this._inputReject = reject;
|
|
67
|
+
this._inputTimer = setTimeout(() => {
|
|
68
|
+
this._settleInput();
|
|
69
|
+
this._status = 'idle';
|
|
70
|
+
this._onMessage?.();
|
|
71
|
+
reject(new Error('Request timed out'));
|
|
72
|
+
}, timeoutMs);
|
|
73
|
+
});
|
|
97
74
|
}
|
|
98
|
-
|
|
99
|
-
|
|
75
|
+
async reconnect(sessionId) {
|
|
76
|
+
const sid = sessionId || this._currentSession?.session_id;
|
|
77
|
+
if (!sid)
|
|
78
|
+
throw new Error('No session to reconnect');
|
|
79
|
+
if (!this._currentSession)
|
|
80
|
+
this._currentSession = { session_id: sid };
|
|
81
|
+
this._status = 'working';
|
|
82
|
+
this._onMessage?.();
|
|
83
|
+
// Force new connection for reconnect
|
|
84
|
+
this._closeWs();
|
|
85
|
+
this._keys = (0, auth_1.ensureKeys)(this._keys);
|
|
86
|
+
await this._resolveEndpointOnce();
|
|
87
|
+
const { wsUrl, isDirect } = this._resolveWsUrl();
|
|
88
|
+
const ws = new this._WS(wsUrl);
|
|
89
|
+
this._ws = ws;
|
|
90
|
+
this._connectionState = 'reconnecting';
|
|
91
|
+
this._onMessage?.();
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
this._inputResolve = resolve;
|
|
94
|
+
this._inputReject = reject;
|
|
95
|
+
this._inputTimer = setTimeout(() => {
|
|
96
|
+
this._settleInput();
|
|
97
|
+
this._status = 'idle';
|
|
98
|
+
this._connectionState = 'disconnected';
|
|
99
|
+
this._onMessage?.();
|
|
100
|
+
reject(new Error('Reconnect timed out'));
|
|
101
|
+
}, 60000);
|
|
102
|
+
ws.onopen = () => {
|
|
103
|
+
this._connectionState = 'connected';
|
|
104
|
+
this._lastPingTime = Date.now();
|
|
105
|
+
this._startPingMonitor();
|
|
106
|
+
// Send CONNECT with session_id + session data
|
|
107
|
+
const payload = { timestamp: Math.floor(Date.now() / 1000) };
|
|
108
|
+
payload.to = this.address;
|
|
109
|
+
const signed = (0, auth_1.signPayload)(this._keys, payload);
|
|
110
|
+
const msg = { type: 'CONNECT', session_id: sid, ...signed };
|
|
111
|
+
if (!isDirect)
|
|
112
|
+
msg.to = this.address;
|
|
113
|
+
if (this._currentSession)
|
|
114
|
+
msg.session = { ...this._currentSession };
|
|
115
|
+
ws.send(JSON.stringify(msg));
|
|
116
|
+
};
|
|
117
|
+
ws.onmessage = (evt) => this._handleMessage(evt);
|
|
118
|
+
ws.onerror = () => this._handleConnectionLoss();
|
|
119
|
+
ws.onclose = () => this._handleConnectionLoss();
|
|
120
|
+
});
|
|
100
121
|
}
|
|
101
|
-
|
|
102
|
-
|
|
122
|
+
send(message) {
|
|
123
|
+
if (!this._ws)
|
|
124
|
+
throw new Error('No active connection');
|
|
125
|
+
this._ws.send(JSON.stringify(message));
|
|
103
126
|
}
|
|
104
|
-
// ==========================================================================
|
|
105
|
-
// Public Methods
|
|
106
|
-
// ==========================================================================
|
|
107
127
|
setMode(mode, options) {
|
|
108
128
|
if (!this._currentSession) {
|
|
109
129
|
this._currentSession = { mode };
|
|
@@ -115,708 +135,428 @@ class RemoteAgent {
|
|
|
115
135
|
this._currentSession.ulw_turns = options?.turns || 100;
|
|
116
136
|
this._currentSession.ulw_turns_used = 0;
|
|
117
137
|
}
|
|
118
|
-
if (this.
|
|
138
|
+
if (this._ws) {
|
|
119
139
|
const msg = { type: 'mode_change', mode };
|
|
120
|
-
if (mode === 'ulw' && options?.turns)
|
|
140
|
+
if (mode === 'ulw' && options?.turns)
|
|
121
141
|
msg.turns = options.turns;
|
|
122
|
-
|
|
123
|
-
this._activeWs.send(JSON.stringify(msg));
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
setPrompt(prompt) {
|
|
127
|
-
if (this._activeWs) {
|
|
128
|
-
this._activeWs.send(JSON.stringify({ type: 'prompt_update', prompt }));
|
|
142
|
+
this._ws.send(JSON.stringify(msg));
|
|
129
143
|
}
|
|
130
144
|
}
|
|
131
145
|
reset() {
|
|
132
|
-
|
|
133
|
-
this._activeWs.close();
|
|
134
|
-
this._activeWs = null;
|
|
135
|
-
}
|
|
146
|
+
this._closeWs();
|
|
136
147
|
this._currentSession = null;
|
|
137
148
|
this._chatItems = [];
|
|
138
149
|
this._status = 'idle';
|
|
139
150
|
this._connectionState = 'disconnected';
|
|
140
|
-
this.
|
|
141
|
-
this.
|
|
142
|
-
|
|
143
|
-
resetConversation() {
|
|
144
|
-
this.reset();
|
|
151
|
+
this._error = null;
|
|
152
|
+
this._settleInput();
|
|
153
|
+
this._pendingRetry = null;
|
|
145
154
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
return this._streamInput(prompt, timeoutMs, images);
|
|
150
|
-
}
|
|
151
|
-
async inputAsync(prompt, options) {
|
|
152
|
-
return this.input(prompt, options);
|
|
153
|
-
}
|
|
154
|
-
respond(answer) {
|
|
155
|
-
if (!this._activeWs) {
|
|
156
|
-
throw new Error('No active connection to respond to');
|
|
157
|
-
}
|
|
158
|
-
const answerStr = Array.isArray(answer) ? answer.join(', ') : answer;
|
|
159
|
-
this._activeWs.send(JSON.stringify({
|
|
160
|
-
type: 'ASK_USER_RESPONSE',
|
|
161
|
-
answer: answerStr,
|
|
162
|
-
}));
|
|
163
|
-
}
|
|
164
|
-
respondToApproval(approved, scope = 'once', mode, feedback) {
|
|
165
|
-
if (!this._activeWs) {
|
|
166
|
-
throw new Error('No active connection to respond to');
|
|
167
|
-
}
|
|
168
|
-
this._activeWs.send(JSON.stringify({
|
|
169
|
-
type: 'APPROVAL_RESPONSE',
|
|
170
|
-
approved,
|
|
171
|
-
scope,
|
|
172
|
-
...(mode && { mode }),
|
|
173
|
-
...(feedback && { feedback }),
|
|
174
|
-
}));
|
|
175
|
-
}
|
|
176
|
-
submitOnboard(options) {
|
|
177
|
-
if (!this._activeWs) {
|
|
178
|
-
throw new Error('No active connection to submit onboard');
|
|
179
|
-
}
|
|
180
|
-
const payload = {
|
|
181
|
-
timestamp: Math.floor(Date.now() / 1000),
|
|
182
|
-
};
|
|
155
|
+
resetConversation() { this.reset(); }
|
|
156
|
+
signOnboard(options) {
|
|
157
|
+
const payload = { timestamp: Math.floor(Date.now() / 1000) };
|
|
183
158
|
if (options.inviteCode)
|
|
184
159
|
payload.invite_code = options.inviteCode;
|
|
185
160
|
if (options.payment)
|
|
186
161
|
payload.payment = options.payment;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// Private Methods - Streaming
|
|
215
|
-
// ==========================================================================
|
|
216
|
-
_ensureKeys() {
|
|
217
|
-
if (this._keys)
|
|
218
|
-
return;
|
|
219
|
-
const isBrowser = (0, endpoint_1.isBrowserEnv)();
|
|
220
|
-
const existingKeys = isBrowser ? address.loadBrowser() : address.load();
|
|
221
|
-
if (existingKeys) {
|
|
222
|
-
this._keys = existingKeys;
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
if (isBrowser) {
|
|
226
|
-
this._keys = address.generateBrowser();
|
|
227
|
-
address.saveBrowser(this._keys);
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
this._keys = address.generate();
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
_deriveWsUrl() {
|
|
234
|
-
if (this._directUrl) {
|
|
235
|
-
const base = this._directUrl.replace(/^https?:\/\//, '');
|
|
236
|
-
const protocol = this._directUrl.startsWith('https') ? 'wss' : 'ws';
|
|
237
|
-
return { wsUrl: `${protocol}://${base}/ws`, isDirect: true };
|
|
238
|
-
}
|
|
239
|
-
if (this._resolvedEndpoint) {
|
|
240
|
-
return { wsUrl: this._resolvedEndpoint.wsUrl, isDirect: true };
|
|
162
|
+
return { type: 'ONBOARD_SUBMIT', ...(0, auth_1.signPayload)(this._keys, payload) };
|
|
163
|
+
}
|
|
164
|
+
async checkSessionStatus(sessionId) {
|
|
165
|
+
// If we have a live WS, send SESSION_STATUS over it (no new connection needed)
|
|
166
|
+
if (this._ws && this._authenticated) {
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
const timeout = setTimeout(() => resolve('not_found'), 5000);
|
|
169
|
+
// Temporarily intercept the next SESSION_STATUS response
|
|
170
|
+
const origHandler = this._ws.onmessage;
|
|
171
|
+
this._ws.onmessage = (evt) => {
|
|
172
|
+
const raw = typeof evt.data === 'string' ? evt.data : String(evt.data);
|
|
173
|
+
const data = JSON.parse(raw);
|
|
174
|
+
if (data?.type === 'SESSION_STATUS') {
|
|
175
|
+
clearTimeout(timeout);
|
|
176
|
+
this._ws.onmessage = origHandler;
|
|
177
|
+
resolve(data.status || 'not_found');
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// Not our response — pass to normal handler
|
|
181
|
+
this._handleMessage(evt);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
this._ws.send(JSON.stringify({
|
|
185
|
+
type: 'SESSION_STATUS',
|
|
186
|
+
session: { session_id: sessionId },
|
|
187
|
+
}));
|
|
188
|
+
});
|
|
241
189
|
}
|
|
242
|
-
|
|
190
|
+
// No active connection — open a short-lived WS just for the check
|
|
191
|
+
this._keys = (0, auth_1.ensureKeys)(this._keys);
|
|
192
|
+
await this._resolveEndpointOnce();
|
|
193
|
+
const { wsUrl, isDirect } = this._resolveWsUrl();
|
|
194
|
+
return new Promise((resolve) => {
|
|
195
|
+
const ws = new this._WS(wsUrl);
|
|
196
|
+
const timeout = setTimeout(() => { ws.close(); resolve('not_found'); }, 5000);
|
|
197
|
+
ws.onopen = () => {
|
|
198
|
+
ws.send(JSON.stringify({
|
|
199
|
+
type: 'SESSION_STATUS',
|
|
200
|
+
session: { session_id: sessionId },
|
|
201
|
+
...(!isDirect && { to: this.address }),
|
|
202
|
+
}));
|
|
203
|
+
};
|
|
204
|
+
ws.onmessage = (evt) => {
|
|
205
|
+
const data = JSON.parse(typeof evt.data === 'string' ? evt.data : String(evt.data));
|
|
206
|
+
if (data?.type === 'SESSION_STATUS') {
|
|
207
|
+
clearTimeout(timeout);
|
|
208
|
+
ws.close();
|
|
209
|
+
resolve(data.status || 'not_found');
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
ws.onerror = () => { clearTimeout(timeout); ws.close(); resolve('not_found'); };
|
|
213
|
+
});
|
|
243
214
|
}
|
|
244
|
-
|
|
245
|
-
this.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
215
|
+
async checkSession(sessionId) {
|
|
216
|
+
const sid = sessionId || this._currentSession?.session_id;
|
|
217
|
+
if (!sid)
|
|
218
|
+
return 'not_found';
|
|
219
|
+
await this._resolveEndpointOnce();
|
|
220
|
+
const httpUrl = this._directUrl || this._resolvedEndpoint?.httpUrl;
|
|
221
|
+
if (!httpUrl)
|
|
222
|
+
return 'not_found';
|
|
223
|
+
const res = await fetch(`${httpUrl}/sessions/${sid}`).catch(() => null);
|
|
224
|
+
if (!res || !res.ok)
|
|
225
|
+
return 'not_found';
|
|
226
|
+
const data = await res.json().catch(() => null);
|
|
227
|
+
return data?.status === 'running' ? 'running' : 'done';
|
|
254
228
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
this._healthCheckInterval = null;
|
|
259
|
-
}
|
|
229
|
+
toString() {
|
|
230
|
+
const short = this.address.length > 12 ? this.address.slice(0, 12) + '...' : this.address;
|
|
231
|
+
return `RemoteAgent(${short})`;
|
|
260
232
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
if (this._reconnectAttempts >= this._maxReconnectAttempts) {
|
|
267
|
-
this._reconnectAttempts = 0;
|
|
268
|
-
this._shouldReconnect = false;
|
|
269
|
-
this._connectionState = 'disconnected';
|
|
270
|
-
reject(new Error('Max reconnection attempts reached'));
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
this._connectionState = 'reconnecting';
|
|
274
|
-
this._reconnectAttempts++;
|
|
275
|
-
const delay = Math.min(this._reconnectBaseDelay * Math.pow(2, this._reconnectAttempts - 1), 30000);
|
|
276
|
-
console.log(`[ConnectOnion] Connection lost. Reconnecting in ${delay}ms (attempt ${this._reconnectAttempts}/${this._maxReconnectAttempts})...`);
|
|
277
|
-
setTimeout(() => {
|
|
278
|
-
this._reconnect(resolve, reject);
|
|
279
|
-
}, delay);
|
|
233
|
+
// --- Internal helpers (used by useAgentForHuman) ---
|
|
234
|
+
_addChatItem(event) {
|
|
235
|
+
const id = event.id || (0, endpoint_1.generateUUID)();
|
|
236
|
+
this._chatItems.push({ ...event, id });
|
|
280
237
|
}
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
const { wsUrl, isDirect } = this._deriveWsUrl();
|
|
288
|
-
const ws = new this._WS(wsUrl);
|
|
289
|
-
this._activeWs = ws;
|
|
290
|
-
this._status = 'working';
|
|
291
|
-
const state = {
|
|
292
|
-
settled: false,
|
|
293
|
-
timer: setTimeout(() => {
|
|
294
|
-
if (!state.settled) {
|
|
295
|
-
state.settled = true;
|
|
296
|
-
this._status = 'idle';
|
|
297
|
-
ws.close();
|
|
298
|
-
this._attemptReconnect(resolve, reject);
|
|
299
|
-
}
|
|
300
|
-
}, 600000),
|
|
301
|
-
};
|
|
302
|
-
ws.onopen = () => {
|
|
303
|
-
console.log('[ConnectOnion] Reconnected successfully');
|
|
304
|
-
this._connectionState = 'connected';
|
|
305
|
-
this._reconnectAttempts = 0;
|
|
306
|
-
this._lastPingTime = Date.now();
|
|
307
|
-
this._startHealthCheck(ws, reject);
|
|
308
|
-
// Send full session for server-side merge (iteration count comparison)
|
|
309
|
-
const payload = {
|
|
310
|
-
prompt: '',
|
|
311
|
-
timestamp: Math.floor(Date.now() / 1000),
|
|
312
|
-
};
|
|
313
|
-
if (!isDirect)
|
|
314
|
-
payload.to = this.address;
|
|
315
|
-
const signed = this._signPayload(payload);
|
|
316
|
-
ws.send(JSON.stringify({
|
|
317
|
-
type: 'INPUT',
|
|
318
|
-
input_id: (0, endpoint_1.generateUUID)(),
|
|
319
|
-
prompt: '',
|
|
320
|
-
...signed,
|
|
321
|
-
...(!isDirect && { to: this.address }),
|
|
322
|
-
session: { ...this._currentSession, session_id: sessionId },
|
|
323
|
-
}));
|
|
324
|
-
};
|
|
325
|
-
this._attachMessageHandlers(ws, (0, endpoint_1.generateUUID)(), isDirect, state, resolve, reject);
|
|
238
|
+
_clearPlaceholder() {
|
|
239
|
+
const idx = this._chatItems.findIndex(item => item.id === '__optimistic__');
|
|
240
|
+
if (idx !== -1)
|
|
241
|
+
this._chatItems.splice(idx, 1);
|
|
326
242
|
}
|
|
327
|
-
|
|
328
|
-
|
|
243
|
+
// --- Private: connection lifecycle ---
|
|
244
|
+
async _ensureConnected() {
|
|
245
|
+
if (this._ws && this._authenticated)
|
|
329
246
|
return;
|
|
330
|
-
this.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const resolved = await (0, endpoint_1.resolveEndpoint)(this.address, this._relayUrl);
|
|
334
|
-
if (resolved) {
|
|
335
|
-
this._resolvedEndpoint = resolved;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
async _streamInput(prompt, timeoutMs, images) {
|
|
339
|
-
this._ensureKeys();
|
|
340
|
-
await this._tryResolveEndpoint();
|
|
341
|
-
this._addChatItem({ type: 'user', content: prompt, images });
|
|
342
|
-
this._addChatItem({ type: 'thinking', id: '__optimistic__', status: 'running' });
|
|
343
|
-
this._status = 'working';
|
|
344
|
-
const inputId = (0, endpoint_1.generateUUID)();
|
|
345
|
-
const sessionId = this._currentSession?.session_id || (0, endpoint_1.generateUUID)();
|
|
346
|
-
const { wsUrl, isDirect } = this._deriveWsUrl();
|
|
247
|
+
this._keys = (0, auth_1.ensureKeys)(this._keys);
|
|
248
|
+
await this._resolveEndpointOnce();
|
|
249
|
+
const { wsUrl, isDirect } = this._resolveWsUrl();
|
|
347
250
|
const ws = new this._WS(wsUrl);
|
|
348
|
-
this.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const state = {
|
|
352
|
-
settled: false,
|
|
353
|
-
timer: setTimeout(() => {
|
|
354
|
-
if (!state.settled) {
|
|
355
|
-
state.settled = true;
|
|
356
|
-
this._status = 'idle';
|
|
357
|
-
this._shouldReconnect = false;
|
|
358
|
-
this._connectionState = 'disconnected';
|
|
359
|
-
ws.close();
|
|
360
|
-
reject(new Error('Connection timed out'));
|
|
361
|
-
}
|
|
362
|
-
}, timeoutMs),
|
|
363
|
-
};
|
|
251
|
+
this._ws = ws;
|
|
252
|
+
// Wait for open
|
|
253
|
+
await new Promise((resolve, reject) => {
|
|
364
254
|
ws.onopen = () => {
|
|
365
255
|
this._connectionState = 'connected';
|
|
366
256
|
this._lastPingTime = Date.now();
|
|
367
|
-
this.
|
|
368
|
-
|
|
369
|
-
prompt,
|
|
370
|
-
timestamp: Math.floor(Date.now() / 1000),
|
|
371
|
-
};
|
|
372
|
-
if (!isDirect) {
|
|
373
|
-
payload.to = this.address;
|
|
374
|
-
}
|
|
375
|
-
const signed = this._signPayload(payload);
|
|
376
|
-
const inputMessage = {
|
|
377
|
-
type: 'INPUT',
|
|
378
|
-
input_id: inputId,
|
|
379
|
-
prompt,
|
|
380
|
-
...signed,
|
|
381
|
-
};
|
|
382
|
-
if (images && images.length > 0) {
|
|
383
|
-
inputMessage.images = images;
|
|
384
|
-
}
|
|
385
|
-
if (!isDirect) {
|
|
386
|
-
inputMessage.to = this.address;
|
|
387
|
-
}
|
|
388
|
-
if (this._currentSession) {
|
|
389
|
-
inputMessage.session = { ...this._currentSession, session_id: sessionId };
|
|
390
|
-
}
|
|
391
|
-
else {
|
|
392
|
-
inputMessage.session = { session_id: sessionId };
|
|
393
|
-
}
|
|
394
|
-
ws.send(JSON.stringify(inputMessage));
|
|
257
|
+
this._startPingMonitor();
|
|
258
|
+
resolve();
|
|
395
259
|
};
|
|
396
|
-
|
|
260
|
+
ws.onerror = (err) => reject(new Error(`WebSocket connection failed: ${String(err)}`));
|
|
397
261
|
});
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (data?.type === 'mode_changed' && data.mode) {
|
|
424
|
-
if (!this._currentSession) {
|
|
425
|
-
this._currentSession = { mode: data.mode };
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
this._currentSession.mode = data.mode;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
if (data?.type === 'ulw_turns_reached') {
|
|
432
|
-
this._status = 'waiting';
|
|
433
|
-
if (this._currentSession) {
|
|
434
|
-
this._currentSession.ulw_turns_used = data.turns_used;
|
|
435
|
-
}
|
|
436
|
-
this._addChatItem({
|
|
437
|
-
type: 'ulw_turns_reached',
|
|
438
|
-
turns_used: data.turns_used,
|
|
439
|
-
max_turns: data.max_turns,
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
if (data?.type === 'llm_call' || data?.type === 'llm_result' ||
|
|
443
|
-
data?.type === 'tool_call' || data?.type === 'tool_result' ||
|
|
444
|
-
data?.type === 'thinking' || data?.type === 'assistant' ||
|
|
445
|
-
data?.type === 'intent' || data?.type === 'eval' || data?.type === 'compact' ||
|
|
446
|
-
data?.type === 'tool_blocked') {
|
|
447
|
-
this._handleStreamEvent(data);
|
|
448
|
-
if (data.session) {
|
|
449
|
-
this._currentSession = data.session;
|
|
262
|
+
// Wire up persistent message handler
|
|
263
|
+
ws.onmessage = (evt) => this._handleMessage(evt);
|
|
264
|
+
ws.onerror = () => this._handleConnectionLoss();
|
|
265
|
+
ws.onclose = () => this._handleConnectionLoss();
|
|
266
|
+
// Send CONNECT with session (conversation history)
|
|
267
|
+
const payload = { timestamp: Math.floor(Date.now() / 1000) };
|
|
268
|
+
payload.to = this.address;
|
|
269
|
+
const signed = (0, auth_1.signPayload)(this._keys, payload);
|
|
270
|
+
const connectMsg = { type: 'CONNECT', ...signed };
|
|
271
|
+
if (!isDirect)
|
|
272
|
+
connectMsg.to = this.address;
|
|
273
|
+
if (this._currentSession?.session_id)
|
|
274
|
+
connectMsg.session_id = this._currentSession.session_id;
|
|
275
|
+
if (this._currentSession)
|
|
276
|
+
connectMsg.session = { ...this._currentSession };
|
|
277
|
+
ws.send(JSON.stringify(connectMsg));
|
|
278
|
+
// Wait for CONNECTED response
|
|
279
|
+
const connected = await new Promise((resolve, reject) => {
|
|
280
|
+
this._connectResolve = resolve;
|
|
281
|
+
this._connectReject = reject;
|
|
282
|
+
setTimeout(() => {
|
|
283
|
+
if (this._connectResolve) {
|
|
284
|
+
this._connectResolve = null;
|
|
285
|
+
this._connectReject = null;
|
|
286
|
+
reject(new Error('Authentication timed out'));
|
|
450
287
|
}
|
|
288
|
+
}, 30000);
|
|
289
|
+
});
|
|
290
|
+
this._authenticated = true;
|
|
291
|
+
// Update session from server (may include merged data)
|
|
292
|
+
const sid = connected.session_id;
|
|
293
|
+
if (sid) {
|
|
294
|
+
if (!this._currentSession) {
|
|
295
|
+
this._currentSession = { session_id: sid };
|
|
451
296
|
}
|
|
452
|
-
|
|
453
|
-
this.
|
|
454
|
-
this._addChatItem({ type: 'ask_user', text: data.text || '', options: data.options || [], multi_select: data.multi_select || false });
|
|
297
|
+
else {
|
|
298
|
+
this._currentSession.session_id = sid;
|
|
455
299
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
300
|
+
}
|
|
301
|
+
if (connected.server_newer && connected.session) {
|
|
302
|
+
this._currentSession = connected.session;
|
|
303
|
+
}
|
|
304
|
+
if (connected.server_newer && connected.chat_items && Array.isArray(connected.chat_items)) {
|
|
305
|
+
const userItems = this._chatItems.filter(item => item.type === 'user');
|
|
306
|
+
const serverNonUserItems = connected.chat_items.filter(item => item.type !== 'user');
|
|
307
|
+
this._chatItems = [...userItems, ...serverNonUserItems];
|
|
308
|
+
this._onMessage?.();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
_handleMessage(evt) {
|
|
312
|
+
const raw = typeof evt.data === 'string' ? evt.data : String(evt.data);
|
|
313
|
+
const data = JSON.parse(raw);
|
|
314
|
+
// PING/PONG keepalive
|
|
315
|
+
if (data?.type === 'PING') {
|
|
316
|
+
this._lastPingTime = Date.now();
|
|
317
|
+
this._ws?.send(JSON.stringify({ type: 'PONG' }));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// CONNECTED — resolve ensureConnected() promise
|
|
321
|
+
if (data?.type === 'CONNECTED') {
|
|
322
|
+
if (this._connectResolve) {
|
|
323
|
+
const resolve = this._connectResolve;
|
|
324
|
+
this._connectResolve = null;
|
|
325
|
+
this._connectReject = null;
|
|
326
|
+
resolve(data);
|
|
327
|
+
this._onMessage?.();
|
|
328
|
+
return;
|
|
465
329
|
}
|
|
466
|
-
if
|
|
467
|
-
|
|
468
|
-
this.
|
|
469
|
-
type: 'plan_review',
|
|
470
|
-
plan_content: data.plan_content,
|
|
471
|
-
});
|
|
330
|
+
// CONNECTED during reconnect — update session and UI if server has newer data
|
|
331
|
+
if (data.server_newer && data.session) {
|
|
332
|
+
this._currentSession = data.session;
|
|
472
333
|
}
|
|
473
|
-
if (data
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
this.
|
|
477
|
-
|
|
478
|
-
this.
|
|
479
|
-
type: 'onboard_required',
|
|
480
|
-
methods: (data.methods || []),
|
|
481
|
-
paymentAmount: data.payment_amount,
|
|
482
|
-
});
|
|
334
|
+
if (data.server_newer && data.chat_items && Array.isArray(data.chat_items)) {
|
|
335
|
+
// Server has newer chat items (e.g., agent finished while client was away)
|
|
336
|
+
// Keep user items from client, take everything else from server
|
|
337
|
+
const userItems = this._chatItems.filter(item => item.type === 'user');
|
|
338
|
+
const serverNonUserItems = data.chat_items.filter(item => item.type !== 'user');
|
|
339
|
+
this._chatItems = [...userItems, ...serverNonUserItems];
|
|
483
340
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
level: data.level,
|
|
488
|
-
message: data.message,
|
|
489
|
-
});
|
|
490
|
-
if (this._pendingPrompt && this._activeWs) {
|
|
491
|
-
this._status = 'working';
|
|
492
|
-
const retryPrompt = this._pendingPrompt;
|
|
493
|
-
const retryInputId = this._pendingInputId || (0, endpoint_1.generateUUID)();
|
|
494
|
-
this._pendingPrompt = null;
|
|
495
|
-
this._pendingInputId = null;
|
|
496
|
-
const retryPayload = {
|
|
497
|
-
prompt: retryPrompt,
|
|
498
|
-
timestamp: Math.floor(Date.now() / 1000),
|
|
499
|
-
};
|
|
500
|
-
if (!isDirect)
|
|
501
|
-
retryPayload.to = this.address;
|
|
502
|
-
const retrySigned = this._signPayload(retryPayload);
|
|
503
|
-
const retryMessage = {
|
|
504
|
-
type: 'INPUT',
|
|
505
|
-
input_id: retryInputId,
|
|
506
|
-
prompt: retryPrompt,
|
|
507
|
-
...retrySigned,
|
|
508
|
-
};
|
|
509
|
-
if (!isDirect)
|
|
510
|
-
retryMessage.to = this.address;
|
|
511
|
-
if (this._currentSession) {
|
|
512
|
-
retryMessage.session = { ...this._currentSession, session_id: this._pendingSessionId };
|
|
513
|
-
}
|
|
514
|
-
else {
|
|
515
|
-
retryMessage.session = { session_id: this._pendingSessionId };
|
|
516
|
-
}
|
|
517
|
-
this._activeWs.send(JSON.stringify(retryMessage));
|
|
518
|
-
this._pendingSessionId = null;
|
|
519
|
-
}
|
|
341
|
+
const reconnectSid = data.session_id;
|
|
342
|
+
if (reconnectSid && this._currentSession) {
|
|
343
|
+
this._currentSession.session_id = reconnectSid;
|
|
520
344
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (isOutputForUs) {
|
|
525
|
-
state.settled = true;
|
|
526
|
-
clearTimeout(state.timer);
|
|
527
|
-
this._stopHealthCheck();
|
|
528
|
-
this._removeOptimisticThinking();
|
|
345
|
+
this._authenticated = true;
|
|
346
|
+
// If status is "connected" (idle), resolve immediately — session is alive, no events to wait for
|
|
347
|
+
if (data.status === 'connected' || data.status === 'new') {
|
|
529
348
|
this._status = 'idle';
|
|
530
|
-
|
|
531
|
-
this.
|
|
532
|
-
|
|
533
|
-
if (data.session) {
|
|
534
|
-
this._currentSession = data.session;
|
|
535
|
-
}
|
|
536
|
-
if (data.chat_items && Array.isArray(data.chat_items)) {
|
|
537
|
-
const userItems = this._chatItems.filter(item => item.type === 'user');
|
|
538
|
-
const serverNonUserItems = data.chat_items.filter(item => item.type !== 'user');
|
|
539
|
-
this._chatItems = [...userItems, ...serverNonUserItems];
|
|
540
|
-
}
|
|
541
|
-
if (data.server_newer) {
|
|
542
|
-
console.log('[RemoteAgent] Session was merged with newer server state');
|
|
543
|
-
}
|
|
544
|
-
const result = data.result || '';
|
|
545
|
-
if (result) {
|
|
546
|
-
const lastAgent = this._chatItems.filter((e) => e.type === 'agent').pop();
|
|
547
|
-
if (!lastAgent || lastAgent.content !== result) {
|
|
548
|
-
this._addChatItem({ type: 'agent', content: result });
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
this._activeWs = null;
|
|
552
|
-
ws.close();
|
|
553
|
-
resolve({ text: result, done: true });
|
|
554
|
-
}
|
|
555
|
-
if (data?.type === 'ERROR') {
|
|
556
|
-
state.settled = true;
|
|
557
|
-
clearTimeout(state.timer);
|
|
558
|
-
this._stopHealthCheck();
|
|
559
|
-
this._status = 'idle';
|
|
560
|
-
this._shouldReconnect = false;
|
|
561
|
-
this._connectionState = 'disconnected';
|
|
562
|
-
this._activeWs = null;
|
|
563
|
-
ws.close();
|
|
564
|
-
reject(new Error(`Agent error: ${String(data.message || data.error || 'Unknown error')}`));
|
|
565
|
-
}
|
|
566
|
-
};
|
|
567
|
-
ws.onerror = async (err) => {
|
|
568
|
-
if (state.settled)
|
|
569
|
-
return;
|
|
570
|
-
this._stopHealthCheck();
|
|
571
|
-
clearTimeout(state.timer);
|
|
572
|
-
ws.close();
|
|
573
|
-
if (isDirect && !this._directUrl) {
|
|
574
|
-
this._resolvedEndpoint = undefined;
|
|
575
|
-
this._endpointResolutionAttempted = false;
|
|
349
|
+
const resolve = this._inputResolve;
|
|
350
|
+
this._settleInput();
|
|
351
|
+
resolve?.({ text: '', done: true });
|
|
576
352
|
}
|
|
577
|
-
|
|
578
|
-
|
|
353
|
+
// If status is "executing", events will stream in via _handleMessage — don't resolve yet
|
|
354
|
+
this._onMessage?.();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// Session sync
|
|
358
|
+
if (data?.type === 'session_sync' && data.session) {
|
|
359
|
+
this._currentSession = data.session;
|
|
360
|
+
}
|
|
361
|
+
if (data?.type === 'RECONNECTED') {
|
|
362
|
+
// Server confirmed reconnect — events will follow
|
|
363
|
+
}
|
|
364
|
+
if (data?.type === 'SESSION_MERGED' && data.server_newer) {
|
|
365
|
+
// Server had newer session
|
|
366
|
+
}
|
|
367
|
+
if (data?.type === 'mode_changed' && data.mode) {
|
|
368
|
+
if (!this._currentSession) {
|
|
369
|
+
this._currentSession = { mode: data.mode };
|
|
579
370
|
}
|
|
580
371
|
else {
|
|
581
|
-
|
|
582
|
-
this._status = 'idle';
|
|
583
|
-
this._shouldReconnect = false;
|
|
584
|
-
this._connectionState = 'disconnected';
|
|
585
|
-
reject(new Error(`WebSocket error: ${String(err)}`));
|
|
372
|
+
this._currentSession.mode = data.mode;
|
|
586
373
|
}
|
|
587
|
-
};
|
|
588
|
-
ws.onclose = async () => {
|
|
589
|
-
this._activeWs = null;
|
|
590
|
-
this._stopHealthCheck();
|
|
591
|
-
if (!state.settled) {
|
|
592
|
-
clearTimeout(state.timer);
|
|
593
|
-
if (isDirect && !this._directUrl) {
|
|
594
|
-
this._resolvedEndpoint = undefined;
|
|
595
|
-
this._endpointResolutionAttempted = false;
|
|
596
|
-
}
|
|
597
|
-
if (this._shouldReconnect && this._reconnectAttempts < this._maxReconnectAttempts) {
|
|
598
|
-
this._attemptReconnect(resolve, reject);
|
|
599
|
-
}
|
|
600
|
-
else {
|
|
601
|
-
state.settled = true;
|
|
602
|
-
this._status = 'idle';
|
|
603
|
-
this._shouldReconnect = false;
|
|
604
|
-
this._connectionState = 'disconnected';
|
|
605
|
-
reject(new Error('Connection closed before response'));
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
// ==========================================================================
|
|
611
|
-
// Private Methods - UI Helpers
|
|
612
|
-
// ==========================================================================
|
|
613
|
-
_removeOptimisticThinking() {
|
|
614
|
-
const idx = this._chatItems.findIndex(item => item.id === '__optimistic__');
|
|
615
|
-
if (idx !== -1) {
|
|
616
|
-
this._chatItems.splice(idx, 1);
|
|
617
374
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
case 'tool_call': {
|
|
624
|
-
const toolId = (event.tool_id || event.id);
|
|
625
|
-
this._addChatItem({
|
|
626
|
-
type: 'tool_call',
|
|
627
|
-
id: toolId,
|
|
628
|
-
name: event.name,
|
|
629
|
-
args: event.args,
|
|
630
|
-
status: 'running',
|
|
631
|
-
});
|
|
632
|
-
break;
|
|
633
|
-
}
|
|
634
|
-
case 'tool_result': {
|
|
635
|
-
const toolId = (event.tool_id || event.id);
|
|
636
|
-
const existing = this._chatItems.find((e) => e.type === 'tool_call' && e.id === toolId);
|
|
637
|
-
if (existing) {
|
|
638
|
-
existing.status = event.status === 'error' ? 'error' : 'done';
|
|
639
|
-
existing.result = event.result;
|
|
640
|
-
if (typeof event.timing_ms === 'number') {
|
|
641
|
-
existing.timing_ms = event.timing_ms;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
break;
|
|
645
|
-
}
|
|
646
|
-
case 'llm_call': {
|
|
647
|
-
const llmId = event.id;
|
|
648
|
-
this._addChatItem({
|
|
649
|
-
type: 'thinking',
|
|
650
|
-
id: llmId,
|
|
651
|
-
status: 'running',
|
|
652
|
-
model: event.model,
|
|
653
|
-
});
|
|
654
|
-
break;
|
|
655
|
-
}
|
|
656
|
-
case 'llm_result': {
|
|
657
|
-
const llmId = event.id;
|
|
658
|
-
const existingThinking = this._chatItems.find((e) => e.type === 'thinking' && e.id === llmId);
|
|
659
|
-
if (existingThinking) {
|
|
660
|
-
existingThinking.status = event.status === 'error' ? 'error' : 'done';
|
|
661
|
-
if (typeof event.duration_ms === 'number') {
|
|
662
|
-
existingThinking.duration_ms = event.duration_ms;
|
|
663
|
-
}
|
|
664
|
-
if (event.model) {
|
|
665
|
-
existingThinking.model = event.model;
|
|
666
|
-
}
|
|
667
|
-
if (event.usage) {
|
|
668
|
-
existingThinking.usage = event.usage;
|
|
669
|
-
}
|
|
670
|
-
if (typeof event.context_percent === 'number') {
|
|
671
|
-
existingThinking.context_percent = event.context_percent;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
break;
|
|
675
|
-
}
|
|
676
|
-
case 'thinking': {
|
|
677
|
-
this._addChatItem({
|
|
678
|
-
type: 'thinking',
|
|
679
|
-
id: event.id != null ? String(event.id) : undefined,
|
|
680
|
-
status: 'done',
|
|
681
|
-
content: event.content,
|
|
682
|
-
kind: event.kind,
|
|
683
|
-
});
|
|
684
|
-
break;
|
|
375
|
+
// ULW turns reached
|
|
376
|
+
if (data?.type === 'ulw_turns_reached') {
|
|
377
|
+
this._status = 'waiting';
|
|
378
|
+
if (this._currentSession) {
|
|
379
|
+
this._currentSession.ulw_turns_used = data.turns_used;
|
|
685
380
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
381
|
+
this._addChatItem({
|
|
382
|
+
type: 'ulw_turns_reached',
|
|
383
|
+
turns_used: data.turns_used,
|
|
384
|
+
max_turns: data.max_turns,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
// Stream events → ChatItem mapping
|
|
388
|
+
if (data?.type === 'llm_call' || data?.type === 'llm_result' ||
|
|
389
|
+
data?.type === 'tool_call' || data?.type === 'tool_result' ||
|
|
390
|
+
data?.type === 'thinking' || data?.type === 'assistant' ||
|
|
391
|
+
data?.type === 'intent' || data?.type === 'eval' || data?.type === 'compact' ||
|
|
392
|
+
data?.type === 'tool_blocked') {
|
|
393
|
+
this._clearPlaceholder();
|
|
394
|
+
(0, chat_item_mapper_1.mapEventToChatItem)(this._chatItems, data, (item) => this._addChatItem(item));
|
|
395
|
+
if (data.session) {
|
|
396
|
+
this._currentSession = data.session;
|
|
695
397
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
398
|
+
}
|
|
399
|
+
// Interactive events
|
|
400
|
+
if (data?.type === 'ask_user') {
|
|
401
|
+
this._status = 'waiting';
|
|
402
|
+
this._addChatItem({ type: 'ask_user', text: data.text || '', options: data.options || [], multi_select: data.multi_select || false });
|
|
403
|
+
}
|
|
404
|
+
if (data?.type === 'approval_needed') {
|
|
405
|
+
this._status = 'waiting';
|
|
406
|
+
this._addChatItem({
|
|
407
|
+
type: 'approval_needed',
|
|
408
|
+
tool: data.tool,
|
|
409
|
+
arguments: data.arguments,
|
|
410
|
+
...(data.description && { description: data.description }),
|
|
411
|
+
...(data.batch_remaining && { batch_remaining: data.batch_remaining }),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
if (data?.type === 'plan_review') {
|
|
415
|
+
this._status = 'waiting';
|
|
416
|
+
this._addChatItem({ type: 'plan_review', plan_content: data.plan_content });
|
|
417
|
+
}
|
|
418
|
+
// Onboard flow
|
|
419
|
+
if (data?.type === 'ONBOARD_REQUIRED') {
|
|
420
|
+
this._status = 'waiting';
|
|
421
|
+
this._pendingRetry = { prompt: data.prompt || '', inputId: (0, endpoint_1.generateUUID)() };
|
|
422
|
+
this._addChatItem({
|
|
423
|
+
type: 'onboard_required',
|
|
424
|
+
methods: (data.methods || []),
|
|
425
|
+
paymentAmount: data.payment_amount,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (data?.type === 'ONBOARD_SUCCESS') {
|
|
429
|
+
this._addChatItem({
|
|
430
|
+
type: 'onboard_success',
|
|
431
|
+
level: data.level,
|
|
432
|
+
message: data.message,
|
|
433
|
+
});
|
|
434
|
+
if (this._pendingRetry && this._ws) {
|
|
435
|
+
this._status = 'working';
|
|
436
|
+
const retry = this._pendingRetry;
|
|
437
|
+
this._pendingRetry = null;
|
|
438
|
+
const isDirect = this._isDirect();
|
|
439
|
+
const msg = { type: 'INPUT', input_id: retry.inputId, prompt: retry.prompt };
|
|
440
|
+
if (!isDirect)
|
|
441
|
+
msg.to = this.address;
|
|
442
|
+
this._ws.send(JSON.stringify(msg));
|
|
716
443
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
status: 'analyzing',
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
else if (status === 'understood') {
|
|
728
|
-
const existing = this._chatItems.find((e) => e.type === 'intent' && e.id === intentId);
|
|
729
|
-
if (existing) {
|
|
730
|
-
existing.status = 'understood';
|
|
731
|
-
existing.ack = event.ack;
|
|
732
|
-
existing.is_build = event.is_build;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
break;
|
|
444
|
+
}
|
|
445
|
+
// OUTPUT — resolve input() promise
|
|
446
|
+
if (data?.type === 'OUTPUT') {
|
|
447
|
+
this._clearPlaceholder();
|
|
448
|
+
this._status = 'idle';
|
|
449
|
+
if (data.session) {
|
|
450
|
+
this._currentSession = data.session;
|
|
736
451
|
}
|
|
737
|
-
|
|
738
|
-
const
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
this._addChatItem({
|
|
742
|
-
type: 'eval',
|
|
743
|
-
id: evalId,
|
|
744
|
-
status: 'evaluating',
|
|
745
|
-
expected: event.expected,
|
|
746
|
-
eval_path: event.eval_path,
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
else if (status === 'done') {
|
|
750
|
-
const existing = this._chatItems.find((e) => e.type === 'eval' && e.id === evalId);
|
|
751
|
-
if (existing) {
|
|
752
|
-
existing.status = 'done';
|
|
753
|
-
existing.passed = event.passed;
|
|
754
|
-
existing.summary = event.summary;
|
|
755
|
-
existing.expected = event.expected;
|
|
756
|
-
existing.eval_path = event.eval_path;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
break;
|
|
452
|
+
if (data.server_newer && data.chat_items && Array.isArray(data.chat_items)) {
|
|
453
|
+
const userItems = this._chatItems.filter(item => item.type === 'user');
|
|
454
|
+
const serverNonUserItems = data.chat_items.filter(item => item.type !== 'user');
|
|
455
|
+
this._chatItems = [...userItems, ...serverNonUserItems];
|
|
760
456
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
if (
|
|
765
|
-
this._addChatItem({
|
|
766
|
-
type: 'compact',
|
|
767
|
-
id: compactId,
|
|
768
|
-
status: 'compacting',
|
|
769
|
-
context_percent: event.context_percent,
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
else {
|
|
773
|
-
const existing = this._chatItems.find((e) => e.type === 'compact' && e.id === compactId);
|
|
774
|
-
if (existing) {
|
|
775
|
-
existing.status = status;
|
|
776
|
-
existing.context_before = event.context_before;
|
|
777
|
-
existing.context_after = event.context_after;
|
|
778
|
-
existing.message = event.message;
|
|
779
|
-
existing.error = event.error;
|
|
780
|
-
}
|
|
457
|
+
const result = data.result || '';
|
|
458
|
+
if (result) {
|
|
459
|
+
const lastAgent = this._chatItems.filter((e) => e.type === 'agent').pop();
|
|
460
|
+
if (!lastAgent || lastAgent.content !== result) {
|
|
461
|
+
this._addChatItem({ type: 'agent', content: result });
|
|
781
462
|
}
|
|
782
|
-
break;
|
|
783
463
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
464
|
+
// Don't close WS — keep it for next input()
|
|
465
|
+
const resolve = this._inputResolve;
|
|
466
|
+
this._settleInput();
|
|
467
|
+
resolve?.({ text: result, done: true });
|
|
468
|
+
}
|
|
469
|
+
// ERROR — reject input() promise
|
|
470
|
+
if (data?.type === 'ERROR') {
|
|
471
|
+
this._status = 'idle';
|
|
472
|
+
this._connectionState = 'disconnected';
|
|
473
|
+
this._closeWs();
|
|
474
|
+
const reject = this._inputReject;
|
|
475
|
+
this._settleInput();
|
|
476
|
+
reject?.(new Error(`Agent error: ${String(data.message || data.error || 'Unknown error')}`));
|
|
477
|
+
}
|
|
478
|
+
this._onMessage?.();
|
|
479
|
+
}
|
|
480
|
+
_handleConnectionLoss() {
|
|
481
|
+
this._ws = null;
|
|
482
|
+
this._authenticated = false;
|
|
483
|
+
this._stopPingMonitor();
|
|
484
|
+
// Reject pending connect
|
|
485
|
+
if (this._connectReject) {
|
|
486
|
+
const reject = this._connectReject;
|
|
487
|
+
this._connectResolve = null;
|
|
488
|
+
this._connectReject = null;
|
|
489
|
+
reject(new Error('Connection lost during authentication'));
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
// Reject pending input only if there is one
|
|
493
|
+
if (this._inputReject) {
|
|
494
|
+
this._status = 'idle';
|
|
495
|
+
this._connectionState = 'disconnected';
|
|
496
|
+
const reject = this._inputReject;
|
|
497
|
+
this._settleInput();
|
|
498
|
+
reject(new Error('Connection closed before response'));
|
|
499
|
+
this._onMessage?.();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
_settleInput() {
|
|
503
|
+
if (this._inputTimer) {
|
|
504
|
+
clearTimeout(this._inputTimer);
|
|
505
|
+
this._inputTimer = null;
|
|
506
|
+
}
|
|
507
|
+
this._inputResolve = null;
|
|
508
|
+
this._inputReject = null;
|
|
509
|
+
}
|
|
510
|
+
_closeWs() {
|
|
511
|
+
this._stopPingMonitor();
|
|
512
|
+
if (this._ws) {
|
|
513
|
+
// Prevent close handler from firing during intentional close
|
|
514
|
+
this._ws.onerror = null;
|
|
515
|
+
this._ws.onclose = null;
|
|
516
|
+
this._ws.onmessage = null;
|
|
517
|
+
this._ws.close();
|
|
518
|
+
this._ws = null;
|
|
519
|
+
}
|
|
520
|
+
this._authenticated = false;
|
|
521
|
+
this._connectionState = 'disconnected';
|
|
522
|
+
}
|
|
523
|
+
_startPingMonitor() {
|
|
524
|
+
this._stopPingMonitor();
|
|
525
|
+
this._pingTimer = setInterval(() => {
|
|
526
|
+
if (Date.now() - this._lastPingTime > 60000) {
|
|
527
|
+
this._stopPingMonitor();
|
|
528
|
+
this._ws?.close();
|
|
793
529
|
}
|
|
530
|
+
}, 10000);
|
|
531
|
+
}
|
|
532
|
+
_stopPingMonitor() {
|
|
533
|
+
if (this._pingTimer) {
|
|
534
|
+
clearInterval(this._pingTimer);
|
|
535
|
+
this._pingTimer = null;
|
|
794
536
|
}
|
|
795
537
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
this._chatItems.push({ ...event, id });
|
|
538
|
+
_isDirect() {
|
|
539
|
+
return !!this._directUrl || !!this._resolvedEndpoint;
|
|
799
540
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
return { prompt: payload.prompt };
|
|
541
|
+
_resolveWsUrl() {
|
|
542
|
+
if (this._directUrl) {
|
|
543
|
+
const base = this._directUrl.replace(/^https?:\/\//, '');
|
|
544
|
+
const protocol = this._directUrl.startsWith('https') ? 'wss' : 'ws';
|
|
545
|
+
return { wsUrl: `${protocol}://${base}/ws`, isDirect: true };
|
|
806
546
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
return {
|
|
811
|
-
payload,
|
|
812
|
-
from: this._keys.address,
|
|
813
|
-
signature,
|
|
814
|
-
timestamp: payload.timestamp,
|
|
815
|
-
};
|
|
547
|
+
if (this._resolvedEndpoint)
|
|
548
|
+
return { wsUrl: this._resolvedEndpoint.wsUrl, isDirect: true };
|
|
549
|
+
return { wsUrl: `${this._relayUrl}/ws/input`, isDirect: false };
|
|
816
550
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
551
|
+
async _resolveEndpointOnce() {
|
|
552
|
+
if (this._endpointResolutionAttempted || this._directUrl)
|
|
553
|
+
return;
|
|
554
|
+
this._endpointResolutionAttempted = true;
|
|
555
|
+
if (!this.address.startsWith('0x') || this.address.length !== 66)
|
|
556
|
+
return;
|
|
557
|
+
const resolved = await (0, endpoint_1.resolveEndpoint)(this.address, this._relayUrl);
|
|
558
|
+
if (resolved)
|
|
559
|
+
this._resolvedEndpoint = resolved;
|
|
820
560
|
}
|
|
821
561
|
}
|
|
822
562
|
exports.RemoteAgent = RemoteAgent;
|