connectonion 0.0.20 → 0.1.0
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/remote-agent.d.ts +27 -30
- package/dist/connect/remote-agent.d.ts.map +1 -1
- package/dist/connect/remote-agent.js +412 -171
- package/dist/react/use-agent-for-human.d.ts +2 -9
- package/dist/react/use-agent-for-human.d.ts.map +1 -1
- package/dist/react/use-agent-for-human.js +4 -5
- package/package.json +1 -1
- package/dist/connect/ws-handlers.d.ts +0 -14
- package/dist/connect/ws-handlers.d.ts.map +0 -1
- package/dist/connect/ws-handlers.js +0 -188
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @llm-note
|
|
3
|
-
* Dependencies: imports from [src/connect/types, src/connect/endpoint, src/connect/auth, src/connect/
|
|
4
|
-
* Data flow:
|
|
5
|
-
* State/Effects: owns WebSocket
|
|
3
|
+
* Dependencies: imports from [src/connect/types, src/connect/endpoint, src/connect/auth, src/connect/chat-item-mapper, src/address]
|
|
4
|
+
* Data flow: ensureConnected() opens persistent WS + INIT auth → input() sends INPUT on existing WS → handleMessage() dispatches events → resolves on OUTPUT
|
|
5
|
+
* State/Effects: owns persistent WebSocket + mutable _chatItems + _currentSession
|
|
6
6
|
* Integration: public API consumed by connect() factory and React useAgentForHuman hook
|
|
7
7
|
*/
|
|
8
8
|
import * as address from '../address';
|
|
9
|
-
import { AgentStatus, ApprovalMode, ChatItem, ChatItemType, ConnectionState, ConnectOptions, ResolvedEndpoint, Response, SessionState, WebSocketCtor
|
|
9
|
+
import { AgentStatus, ApprovalMode, ChatItem, ChatItemType, ConnectionState, ConnectOptions, ResolvedEndpoint, Response, SessionState, WebSocketCtor } from './types';
|
|
10
10
|
export declare class RemoteAgent {
|
|
11
11
|
readonly address: string;
|
|
12
12
|
_keys?: address.AddressData;
|
|
@@ -19,16 +19,19 @@ export declare class RemoteAgent {
|
|
|
19
19
|
_connectionState: ConnectionState;
|
|
20
20
|
_currentSession: SessionState | null;
|
|
21
21
|
_chatItems: ChatItem[];
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
_error: Error | null;
|
|
23
|
+
private _ws;
|
|
24
|
+
private _authenticated;
|
|
25
|
+
private _inputResolve;
|
|
26
|
+
private _inputReject;
|
|
27
|
+
private _inputTimer;
|
|
28
|
+
private _pendingRetry;
|
|
29
|
+
private _lastPingTime;
|
|
30
|
+
private _pingTimer;
|
|
31
|
+
private _connectResolve;
|
|
32
|
+
private _connectReject;
|
|
33
|
+
_onMessage: (() => void) | null;
|
|
34
|
+
set onMessage(fn: (() => void) | null);
|
|
32
35
|
constructor(agentAddress: string, options?: ConnectOptions);
|
|
33
36
|
get agentAddress(): string;
|
|
34
37
|
get status(): AgentStatus;
|
|
@@ -37,43 +40,37 @@ export declare class RemoteAgent {
|
|
|
37
40
|
get ui(): ChatItem[];
|
|
38
41
|
get mode(): ApprovalMode;
|
|
39
42
|
get error(): Error | null;
|
|
40
|
-
_error: Error | null;
|
|
41
|
-
_onMessage: (() => void) | null;
|
|
42
|
-
set onMessage(fn: (() => void) | null);
|
|
43
43
|
input(prompt: string, options?: {
|
|
44
44
|
images?: string[];
|
|
45
45
|
timeoutMs?: number;
|
|
46
46
|
}): Promise<Response>;
|
|
47
|
-
/** Reconnect to an existing session to receive pending output without adding duplicate UI items */
|
|
48
47
|
reconnect(sessionId?: string): Promise<Response>;
|
|
49
|
-
|
|
50
|
-
images?: string[];
|
|
51
|
-
timeoutMs?: number;
|
|
52
|
-
}): Promise<Response>;
|
|
48
|
+
send(message: Record<string, unknown>): void;
|
|
53
49
|
setMode(mode: ApprovalMode, options?: {
|
|
54
50
|
turns?: number;
|
|
55
51
|
}): void;
|
|
56
52
|
reset(): void;
|
|
57
53
|
resetConversation(): void;
|
|
58
|
-
send(message: Record<string, unknown>): void;
|
|
59
54
|
signOnboard(options: {
|
|
60
55
|
inviteCode?: string;
|
|
61
56
|
payment?: number;
|
|
62
57
|
}): Record<string, unknown>;
|
|
58
|
+
checkSessionStatus(sessionId: string): Promise<'executing' | 'suspended' | 'connected' | 'not_found'>;
|
|
63
59
|
checkSession(sessionId?: string): Promise<'running' | 'done' | 'not_found'>;
|
|
64
|
-
|
|
60
|
+
toString(): string;
|
|
65
61
|
_addChatItem(event: Partial<ChatItem> & {
|
|
66
62
|
type: ChatItemType;
|
|
67
63
|
}): void;
|
|
68
64
|
_clearPlaceholder(): void;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
private _ensureConnected;
|
|
66
|
+
private _handleMessage;
|
|
67
|
+
private _handleConnectionLoss;
|
|
68
|
+
private _settleInput;
|
|
69
|
+
private _closeWs;
|
|
72
70
|
private _startPingMonitor;
|
|
71
|
+
private _stopPingMonitor;
|
|
72
|
+
private _isDirect;
|
|
73
73
|
private _resolveWsUrl;
|
|
74
74
|
private _resolveEndpointOnce;
|
|
75
|
-
private _connectAndSend;
|
|
76
|
-
private _reconnect;
|
|
77
|
-
toString(): string;
|
|
78
75
|
}
|
|
79
76
|
//# sourceMappingURL=remote-agent.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-agent.d.ts","sourceRoot":"","sources":["../../src/connect/remote-agent.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,OAAO,MAAM,YAAY,CAAC;AACtC,OAAO,EACL,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,eAAe,EAClE,cAAc,EAAE,gBAAgB,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"remote-agent.d.ts","sourceRoot":"","sources":["../../src/connect/remote-agent.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,25 +3,32 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.RemoteAgent = void 0;
|
|
4
4
|
const endpoint_1 = require("./endpoint");
|
|
5
5
|
const auth_1 = require("./auth");
|
|
6
|
-
const
|
|
6
|
+
const chat_item_mapper_1 = require("./chat-item-mapper");
|
|
7
7
|
class RemoteAgent {
|
|
8
|
+
set onMessage(fn) { this._onMessage = fn; }
|
|
8
9
|
constructor(agentAddress, options = {}) {
|
|
9
10
|
this._endpointResolutionAttempted = false;
|
|
11
|
+
// Public reactive state
|
|
10
12
|
this._status = 'idle';
|
|
11
13
|
this._connectionState = 'disconnected';
|
|
12
14
|
this._currentSession = null;
|
|
13
15
|
this._chatItems = [];
|
|
14
|
-
this._activeWs = null;
|
|
15
|
-
this._pendingPrompt = null;
|
|
16
|
-
this._pendingInputId = null;
|
|
17
|
-
this._pendingSessionId = null;
|
|
18
|
-
this._lastPingTime = 0;
|
|
19
|
-
this._healthCheckInterval = null;
|
|
20
|
-
this._reconnectAttempts = 0;
|
|
21
|
-
this._maxReconnectAttempts = 3;
|
|
22
|
-
this._reconnectBaseDelay = 1000;
|
|
23
|
-
this._shouldReconnect = false;
|
|
24
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
|
|
27
|
+
this._lastPingTime = 0;
|
|
28
|
+
this._pingTimer = null;
|
|
29
|
+
// Callback + promise for ensureConnected
|
|
30
|
+
this._connectResolve = null;
|
|
31
|
+
this._connectReject = null;
|
|
25
32
|
this._onMessage = null;
|
|
26
33
|
this.address = agentAddress;
|
|
27
34
|
this._relayUrl = (0, endpoint_1.normalizeRelayUrl)(options.relayUrl || 'wss://oo.openonion.ai');
|
|
@@ -30,6 +37,7 @@ class RemoteAgent {
|
|
|
30
37
|
if (options.keys)
|
|
31
38
|
this._keys = options.keys;
|
|
32
39
|
}
|
|
40
|
+
// --- Public getters ---
|
|
33
41
|
get agentAddress() { return this.address; }
|
|
34
42
|
get status() { return this._status; }
|
|
35
43
|
get connectionState() { return this._connectionState; }
|
|
@@ -37,52 +45,84 @@ class RemoteAgent {
|
|
|
37
45
|
get ui() { return this._chatItems; }
|
|
38
46
|
get mode() { return this._currentSession?.mode || 'safe'; }
|
|
39
47
|
get error() { return this._error || null; }
|
|
40
|
-
set onMessage(fn) { this._onMessage = fn; }
|
|
41
48
|
// --- Public API ---
|
|
42
49
|
async input(prompt, options) {
|
|
43
|
-
|
|
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
|
+
});
|
|
44
74
|
}
|
|
45
|
-
/** Reconnect to an existing session to receive pending output without adding duplicate UI items */
|
|
46
75
|
async reconnect(sessionId) {
|
|
47
76
|
const sid = sessionId || this._currentSession?.session_id;
|
|
48
77
|
if (!sid)
|
|
49
78
|
throw new Error('No session to reconnect');
|
|
50
|
-
this._keys = (0, auth_1.ensureKeys)(this._keys);
|
|
51
|
-
await this._resolveEndpointOnce();
|
|
52
79
|
if (!this._currentSession)
|
|
53
80
|
this._currentSession = { session_id: sid };
|
|
54
81
|
this._status = 'working';
|
|
55
|
-
|
|
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();
|
|
56
87
|
const { wsUrl, isDirect } = this._resolveWsUrl();
|
|
57
88
|
const ws = new this._WS(wsUrl);
|
|
58
|
-
this.
|
|
59
|
-
this.
|
|
89
|
+
this._ws = ws;
|
|
90
|
+
this._connectionState = 'reconnecting';
|
|
91
|
+
this._onMessage?.();
|
|
60
92
|
return new Promise((resolve, reject) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
reject(new Error('Reconnect timed out'));
|
|
71
|
-
}
|
|
72
|
-
}, 60000),
|
|
73
|
-
};
|
|
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);
|
|
74
102
|
ws.onopen = () => {
|
|
75
103
|
this._connectionState = 'connected';
|
|
76
104
|
this._lastPingTime = Date.now();
|
|
77
|
-
this._startPingMonitor(
|
|
78
|
-
// Send
|
|
79
|
-
|
|
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));
|
|
80
116
|
};
|
|
81
|
-
|
|
117
|
+
ws.onmessage = (evt) => this._handleMessage(evt);
|
|
118
|
+
ws.onerror = () => this._handleConnectionLoss();
|
|
119
|
+
ws.onclose = () => this._handleConnectionLoss();
|
|
82
120
|
});
|
|
83
121
|
}
|
|
84
|
-
|
|
85
|
-
|
|
122
|
+
send(message) {
|
|
123
|
+
if (!this._ws)
|
|
124
|
+
throw new Error('No active connection');
|
|
125
|
+
this._ws.send(JSON.stringify(message));
|
|
86
126
|
}
|
|
87
127
|
setMode(mode, options) {
|
|
88
128
|
if (!this._currentSession) {
|
|
@@ -95,31 +135,24 @@ class RemoteAgent {
|
|
|
95
135
|
this._currentSession.ulw_turns = options?.turns || 100;
|
|
96
136
|
this._currentSession.ulw_turns_used = 0;
|
|
97
137
|
}
|
|
98
|
-
if (this.
|
|
138
|
+
if (this._ws) {
|
|
99
139
|
const msg = { type: 'mode_change', mode };
|
|
100
140
|
if (mode === 'ulw' && options?.turns)
|
|
101
141
|
msg.turns = options.turns;
|
|
102
|
-
this.
|
|
142
|
+
this._ws.send(JSON.stringify(msg));
|
|
103
143
|
}
|
|
104
144
|
}
|
|
105
145
|
reset() {
|
|
106
|
-
|
|
107
|
-
this._activeWs.close();
|
|
108
|
-
this._activeWs = null;
|
|
109
|
-
}
|
|
146
|
+
this._closeWs();
|
|
110
147
|
this._currentSession = null;
|
|
111
148
|
this._chatItems = [];
|
|
112
149
|
this._status = 'idle';
|
|
113
150
|
this._connectionState = 'disconnected';
|
|
114
|
-
this.
|
|
115
|
-
this.
|
|
151
|
+
this._error = null;
|
|
152
|
+
this._settleInput();
|
|
153
|
+
this._pendingRetry = null;
|
|
116
154
|
}
|
|
117
155
|
resetConversation() { this.reset(); }
|
|
118
|
-
send(message) {
|
|
119
|
-
if (!this._activeWs)
|
|
120
|
-
throw new Error('No active connection');
|
|
121
|
-
this._activeWs.send(JSON.stringify(message));
|
|
122
|
-
}
|
|
123
156
|
signOnboard(options) {
|
|
124
157
|
const payload = { timestamp: Math.floor(Date.now() / 1000) };
|
|
125
158
|
if (options.inviteCode)
|
|
@@ -128,21 +161,33 @@ class RemoteAgent {
|
|
|
128
161
|
payload.payment = options.payment;
|
|
129
162
|
return { type: 'ONBOARD_SUBMIT', ...(0, auth_1.signPayload)(this._keys, payload) };
|
|
130
163
|
}
|
|
131
|
-
async checkSession(sessionId) {
|
|
132
|
-
const sid = sessionId || this._currentSession?.session_id;
|
|
133
|
-
if (!sid)
|
|
134
|
-
return 'not_found';
|
|
135
|
-
await this._resolveEndpointOnce();
|
|
136
|
-
const httpUrl = this._directUrl || this._resolvedEndpoint?.httpUrl;
|
|
137
|
-
if (!httpUrl)
|
|
138
|
-
return 'not_found';
|
|
139
|
-
const res = await fetch(`${httpUrl}/sessions/${sid}`);
|
|
140
|
-
if (!res.ok)
|
|
141
|
-
return 'not_found';
|
|
142
|
-
const data = await res.json();
|
|
143
|
-
return data?.status === 'running' ? 'running' : 'done';
|
|
144
|
-
}
|
|
145
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
|
+
});
|
|
189
|
+
}
|
|
190
|
+
// No active connection — open a short-lived WS just for the check
|
|
146
191
|
this._keys = (0, auth_1.ensureKeys)(this._keys);
|
|
147
192
|
await this._resolveEndpointOnce();
|
|
148
193
|
const { wsUrl, isDirect } = this._resolveWsUrl();
|
|
@@ -167,7 +212,25 @@ class RemoteAgent {
|
|
|
167
212
|
ws.onerror = () => { clearTimeout(timeout); ws.close(); resolve('not_found'); };
|
|
168
213
|
});
|
|
169
214
|
}
|
|
170
|
-
|
|
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';
|
|
228
|
+
}
|
|
229
|
+
toString() {
|
|
230
|
+
const short = this.address.length > 12 ? this.address.slice(0, 12) + '...' : this.address;
|
|
231
|
+
return `RemoteAgent(${short})`;
|
|
232
|
+
}
|
|
233
|
+
// --- Internal helpers (used by useAgentForHuman) ---
|
|
171
234
|
_addChatItem(event) {
|
|
172
235
|
const id = event.id || (0, endpoint_1.generateUUID)();
|
|
173
236
|
this._chatItems.push({ ...event, id });
|
|
@@ -177,56 +240,304 @@ class RemoteAgent {
|
|
|
177
240
|
if (idx !== -1)
|
|
178
241
|
this._chatItems.splice(idx, 1);
|
|
179
242
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
243
|
+
// --- Private: connection lifecycle ---
|
|
244
|
+
async _ensureConnected() {
|
|
245
|
+
if (this._ws && this._authenticated)
|
|
246
|
+
return;
|
|
247
|
+
this._keys = (0, auth_1.ensureKeys)(this._keys);
|
|
248
|
+
await this._resolveEndpointOnce();
|
|
249
|
+
const { wsUrl, isDirect } = this._resolveWsUrl();
|
|
250
|
+
const ws = new this._WS(wsUrl);
|
|
251
|
+
this._ws = ws;
|
|
252
|
+
// Wait for open
|
|
253
|
+
await new Promise((resolve, reject) => {
|
|
254
|
+
ws.onopen = () => {
|
|
255
|
+
this._connectionState = 'connected';
|
|
256
|
+
this._lastPingTime = Date.now();
|
|
257
|
+
this._startPingMonitor();
|
|
258
|
+
resolve();
|
|
259
|
+
};
|
|
260
|
+
ws.onerror = (err) => reject(new Error(`WebSocket connection failed: ${String(err)}`));
|
|
261
|
+
});
|
|
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'));
|
|
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 };
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
this._currentSession.session_id = sid;
|
|
299
|
+
}
|
|
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?.();
|
|
184
309
|
}
|
|
185
310
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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;
|
|
329
|
+
}
|
|
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;
|
|
333
|
+
}
|
|
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];
|
|
340
|
+
}
|
|
341
|
+
const reconnectSid = data.session_id;
|
|
342
|
+
if (reconnectSid && this._currentSession) {
|
|
343
|
+
this._currentSession.session_id = reconnectSid;
|
|
344
|
+
}
|
|
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') {
|
|
348
|
+
this._status = 'idle';
|
|
349
|
+
const resolve = this._inputResolve;
|
|
350
|
+
this._settleInput();
|
|
351
|
+
resolve?.({ text: '', done: true });
|
|
352
|
+
}
|
|
353
|
+
// If status is "executing", events will stream in via _handleMessage — don't resolve yet
|
|
354
|
+
this._onMessage?.();
|
|
189
355
|
return;
|
|
190
356
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.
|
|
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 };
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
this._currentSession.mode = data.mode;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
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;
|
|
380
|
+
}
|
|
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;
|
|
397
|
+
}
|
|
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));
|
|
443
|
+
}
|
|
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;
|
|
451
|
+
}
|
|
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];
|
|
456
|
+
}
|
|
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 });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
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';
|
|
194
472
|
this._connectionState = 'disconnected';
|
|
195
|
-
|
|
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'));
|
|
196
490
|
return;
|
|
197
491
|
}
|
|
198
|
-
|
|
199
|
-
this.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
}
|
|
203
501
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (!isDirect)
|
|
212
|
-
msg.to = this.address;
|
|
213
|
-
msg.session = this._currentSession
|
|
214
|
-
? { ...this._currentSession, session_id: sessionId }
|
|
215
|
-
: { session_id: sessionId };
|
|
216
|
-
console.log(`[ConnectOnion] Sending INPUT via ${isDirect ? 'direct' : 'relay'}, from: ${signed.from?.slice(0, 12)}...`);
|
|
217
|
-
ws.send(JSON.stringify(msg));
|
|
502
|
+
_settleInput() {
|
|
503
|
+
if (this._inputTimer) {
|
|
504
|
+
clearTimeout(this._inputTimer);
|
|
505
|
+
this._inputTimer = null;
|
|
506
|
+
}
|
|
507
|
+
this._inputResolve = null;
|
|
508
|
+
this._inputReject = null;
|
|
218
509
|
}
|
|
219
|
-
|
|
220
|
-
|
|
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() {
|
|
221
524
|
this._stopPingMonitor();
|
|
222
|
-
this.
|
|
525
|
+
this._pingTimer = setInterval(() => {
|
|
223
526
|
if (Date.now() - this._lastPingTime > 60000) {
|
|
224
527
|
this._stopPingMonitor();
|
|
225
|
-
|
|
226
|
-
reject(new Error('Connection health check failed: No PING received for 60 seconds'));
|
|
528
|
+
this._ws?.close();
|
|
227
529
|
}
|
|
228
530
|
}, 10000);
|
|
229
531
|
}
|
|
532
|
+
_stopPingMonitor() {
|
|
533
|
+
if (this._pingTimer) {
|
|
534
|
+
clearInterval(this._pingTimer);
|
|
535
|
+
this._pingTimer = null;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
_isDirect() {
|
|
539
|
+
return !!this._directUrl || !!this._resolvedEndpoint;
|
|
540
|
+
}
|
|
230
541
|
_resolveWsUrl() {
|
|
231
542
|
if (this._directUrl) {
|
|
232
543
|
const base = this._directUrl.replace(/^https?:\/\//, '');
|
|
@@ -247,75 +558,5 @@ class RemoteAgent {
|
|
|
247
558
|
if (resolved)
|
|
248
559
|
this._resolvedEndpoint = resolved;
|
|
249
560
|
}
|
|
250
|
-
async _connectAndSend(prompt, timeoutMs, images) {
|
|
251
|
-
this._keys = (0, auth_1.ensureKeys)(this._keys);
|
|
252
|
-
await this._resolveEndpointOnce();
|
|
253
|
-
this._pendingPrompt = prompt;
|
|
254
|
-
this._addChatItem({ type: 'user', content: prompt, images });
|
|
255
|
-
this._addChatItem({ type: 'thinking', id: '__optimistic__', status: 'running' });
|
|
256
|
-
this._status = 'working';
|
|
257
|
-
const inputId = (0, endpoint_1.generateUUID)();
|
|
258
|
-
const sessionId = this._currentSession?.session_id || (0, endpoint_1.generateUUID)();
|
|
259
|
-
const { wsUrl, isDirect } = this._resolveWsUrl();
|
|
260
|
-
const ws = new this._WS(wsUrl);
|
|
261
|
-
this._activeWs = ws;
|
|
262
|
-
this._shouldReconnect = true;
|
|
263
|
-
return new Promise((resolve, reject) => {
|
|
264
|
-
const state = {
|
|
265
|
-
settled: false,
|
|
266
|
-
timer: setTimeout(() => {
|
|
267
|
-
if (!state.settled) {
|
|
268
|
-
state.settled = true;
|
|
269
|
-
this._status = 'idle';
|
|
270
|
-
this._shouldReconnect = false;
|
|
271
|
-
this._connectionState = 'disconnected';
|
|
272
|
-
ws.close();
|
|
273
|
-
reject(new Error('Connection timed out'));
|
|
274
|
-
}
|
|
275
|
-
}, timeoutMs),
|
|
276
|
-
};
|
|
277
|
-
ws.onopen = () => {
|
|
278
|
-
this._connectionState = 'connected';
|
|
279
|
-
this._lastPingTime = Date.now();
|
|
280
|
-
this._startPingMonitor(ws, reject);
|
|
281
|
-
this._sendInput(ws, inputId, prompt, sessionId, isDirect, images);
|
|
282
|
-
};
|
|
283
|
-
(0, ws_handlers_1.attachWsHandlers)(this, ws, inputId, isDirect, state, resolve, reject);
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
_reconnect(resolve, reject) {
|
|
287
|
-
const sessionId = this._currentSession?.session_id;
|
|
288
|
-
if (!sessionId) {
|
|
289
|
-
reject(new Error('No session to reconnect'));
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
const { wsUrl, isDirect } = this._resolveWsUrl();
|
|
293
|
-
const ws = new this._WS(wsUrl);
|
|
294
|
-
this._activeWs = ws;
|
|
295
|
-
this._status = 'working';
|
|
296
|
-
const state = {
|
|
297
|
-
settled: false,
|
|
298
|
-
timer: setTimeout(() => {
|
|
299
|
-
if (!state.settled) {
|
|
300
|
-
state.settled = true;
|
|
301
|
-
this._status = 'idle';
|
|
302
|
-
ws.close();
|
|
303
|
-
this._attemptReconnect(resolve, reject);
|
|
304
|
-
}
|
|
305
|
-
}, 600000),
|
|
306
|
-
};
|
|
307
|
-
ws.onopen = () => {
|
|
308
|
-
console.log('[ConnectOnion] Reconnected successfully');
|
|
309
|
-
this._connectionState = 'connected';
|
|
310
|
-
this._lastPingTime = Date.now();
|
|
311
|
-
this._startPingMonitor(ws, reject);
|
|
312
|
-
this._sendInput(ws, (0, endpoint_1.generateUUID)(), this._pendingPrompt || '', sessionId, isDirect);
|
|
313
|
-
};
|
|
314
|
-
(0, ws_handlers_1.attachWsHandlers)(this, ws, '', isDirect, state, resolve, reject);
|
|
315
|
-
}
|
|
316
|
-
toString() {
|
|
317
|
-
const short = this.address.length > 12 ? this.address.slice(0, 12) + '...' : this.address;
|
|
318
|
-
return `RemoteAgent(${short})`;
|
|
319
|
-
}
|
|
320
561
|
}
|
|
321
562
|
exports.RemoteAgent = RemoteAgent;
|
|
@@ -35,16 +35,9 @@ export interface UseAgentForHumanReturn {
|
|
|
35
35
|
* The caller decides when and how often to invoke this — no built-in interval.
|
|
36
36
|
*
|
|
37
37
|
* @param sessionId - Session UUID to probe
|
|
38
|
-
* @returns '
|
|
38
|
+
* @returns 'executing' | 'suspended' | 'connected' | 'not_found'
|
|
39
39
|
*/
|
|
40
|
-
checkSessionStatus: (sessionId: string) => Promise<'
|
|
41
|
-
/**
|
|
42
|
-
* Lightweight HTTP check for the current session (no WebSocket relay required).
|
|
43
|
-
* Prefer this over `checkSessionStatus` when you only need a quick alive/dead signal.
|
|
44
|
-
*
|
|
45
|
-
* @returns 'running' | 'done' | 'not_found'
|
|
46
|
-
*/
|
|
47
|
-
checkSession: () => Promise<'running' | 'done' | 'not_found'>;
|
|
40
|
+
checkSessionStatus: (sessionId: string) => Promise<'executing' | 'suspended' | 'connected' | 'not_found'>;
|
|
48
41
|
/** Current approval mode. Defaults to 'safe' when no session exists yet. */
|
|
49
42
|
mode: ApprovalMode;
|
|
50
43
|
/** Maximum turns before ULW mode pauses. null when mode is not 'ulw'. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-agent-for-human.d.ts","sourceRoot":"","sources":["../../src/react/use-agent-for-human.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,QAAQ,EACR,WAAW,EACX,eAAe,EAEf,YAAY,EACZ,eAAe,EAChB,MAAM,YAAY,CAAC;AAGpB;;;;;;;;;;GAUG;AACH,MAAM,WAAW,sBAAsB;IACrC,oEAAoE;IACpE,MAAM,EAAE,WAAW,CAAC;IAEpB;;;OAGG;IACH,eAAe,EAAE,eAAe,CAAC;IAEjC;;;;OAIG;IACH,EAAE,EAAE,QAAQ,EAAE,CAAC;IAEf,2FAA2F;IAC3F,SAAS,EAAE,MAAM,CAAC;IAElB,4EAA4E;IAC5E,YAAY,EAAE,OAAO,CAAC;IAEtB,+FAA+F;IAC/F,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAEpB;;;;;;OAMG;IACH,kBAAkB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"use-agent-for-human.d.ts","sourceRoot":"","sources":["../../src/react/use-agent-for-human.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,QAAQ,EACR,WAAW,EACX,eAAe,EAEf,YAAY,EACZ,eAAe,EAChB,MAAM,YAAY,CAAC;AAGpB;;;;;;;;;;GAUG;AACH,MAAM,WAAW,sBAAsB;IACrC,oEAAoE;IACpE,MAAM,EAAE,WAAW,CAAC;IAEpB;;;OAGG;IACH,eAAe,EAAE,eAAe,CAAC;IAEjC;;;;OAIG;IACH,EAAE,EAAE,QAAQ,EAAE,CAAC;IAEf,2FAA2F;IAC3F,SAAS,EAAE,MAAM,CAAC;IAElB,4EAA4E;IAC5E,YAAY,EAAE,OAAO,CAAC;IAEtB,+FAA+F;IAC/F,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAEpB;;;;;;OAMG;IACH,kBAAkB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC,CAAC;IAG1G,4EAA4E;IAC5E,IAAI,EAAE,YAAY,CAAC;IAEnB,yEAAyE;IACzE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAExB,oFAAoF;IACpF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B;;;;;;;OAOG;IACH,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,KAAK,IAAI,CAAC;IAEjE;;;;OAIG;IACH,WAAW,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;IAEhD,qFAAqF;IACrF,WAAW,EAAE,CAAC,OAAO,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,eAAe,CAAC;IAErF;;;;;;;OAOG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAEpE,8DAA8D;IAC9D,SAAS,EAAE,MAAM,IAAI,CAAC;IAEtB,gFAAgF;IAChF,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,sBAAsB,CAwJxB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,QAAQ,CAAC,MAAM,CAAC,EACvD,IAAI,EAAE,QAAQ,EACd,IAAI,EAAE,CAAC,GACN,IAAI,IAAI,OAAO,CAAC,QAAQ,EAAE;IAAE,IAAI,EAAE,CAAC,CAAA;CAAE,CAAC,CAExC;AAED,6CAA6C;AAC7C,eAAO,MAAM,WAAW,uBAAiB,CAAC"}
|
|
@@ -98,19 +98,19 @@ function useAgentForHuman(address, sessionId) {
|
|
|
98
98
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
99
99
|
}, [agent]);
|
|
100
100
|
// Restore persisted session into the RemoteAgent on mount or when sessionId changes.
|
|
101
|
-
//
|
|
102
|
-
// receive a sessionless request and start a brand-new conversation instead of resuming.
|
|
101
|
+
// Then auto-reconnect to sync with server (get newer data, resume executing agent, etc.)
|
|
103
102
|
(0, react_1.useEffect)(() => {
|
|
104
103
|
if (session) {
|
|
105
104
|
agent._currentSession = { ...session, session_id: sessionId };
|
|
106
105
|
agent._chatItems = [...ui];
|
|
107
106
|
}
|
|
108
107
|
else if (messages.length > 0) {
|
|
109
|
-
// No full session snapshot yet, but we have raw messages — build a minimal session
|
|
110
|
-
// so the server can reconstruct conversation history.
|
|
111
108
|
agent._currentSession = { session_id: sessionId, messages };
|
|
112
109
|
agent._chatItems = [...ui];
|
|
113
110
|
}
|
|
111
|
+
// No auto-reconnect on mount. Show cached conversation from localStorage.
|
|
112
|
+
// When user sends next message, input() → _ensureConnected() → CONNECT
|
|
113
|
+
// will sync with server (session merge, server_newer, etc.).
|
|
114
114
|
}, [sessionId]);
|
|
115
115
|
const input = (prompt, options) => {
|
|
116
116
|
setError(null);
|
|
@@ -172,7 +172,6 @@ function useAgentForHuman(address, sessionId) {
|
|
|
172
172
|
isProcessing: status !== 'idle',
|
|
173
173
|
error,
|
|
174
174
|
checkSessionStatus: (sid) => agent.checkSessionStatus(sid),
|
|
175
|
-
checkSession: () => agent.checkSession(sessionId),
|
|
176
175
|
mode: session?.mode || 'safe',
|
|
177
176
|
ulwTurns: session?.ulw_turns ?? null,
|
|
178
177
|
ulwTurnsUsed: session?.ulw_turns_used ?? null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "connectonion",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Connect to Python AI agents from TypeScript apps - Use powerful Python agents in your React, Next.js, Node.js, and Electron applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @llm-note
|
|
3
|
-
* Dependencies: imports from [src/connect/types, src/connect/stream-events, src/connect/endpoint, src/connect/remote-agent (type-only)]
|
|
4
|
-
* Data flow: onmessage parses JSON → dispatches by type → mutates agent fields → resolves/rejects the input() Promise
|
|
5
|
-
* State/Effects: mutates RemoteAgent fields (session, status, chatItems, connection state) via _ prefix access
|
|
6
|
-
* Integration: attachWsHandlers() wires onmessage/onerror/onclose; called by RemoteAgent._connectAndSend and _reconnect
|
|
7
|
-
*/
|
|
8
|
-
import type { RemoteAgent } from './remote-agent';
|
|
9
|
-
import { Response, WebSocketLike } from './types';
|
|
10
|
-
export declare function attachWsHandlers(agent: RemoteAgent, ws: WebSocketLike, inputId: string, isDirect: boolean, state: {
|
|
11
|
-
settled: boolean;
|
|
12
|
-
timer: ReturnType<typeof setTimeout>;
|
|
13
|
-
}, resolve: (value: Response) => void, reject: (reason?: unknown) => void): void;
|
|
14
|
-
//# sourceMappingURL=ws-handlers.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ws-handlers.d.ts","sourceRoot":"","sources":["../../src/connect/ws-handlers.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGlD,OAAO,EAAY,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE5D,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,WAAW,EAClB,EAAE,EAAE,aAAa,EACjB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,OAAO,EACjB,KAAK,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAA;CAAE,EACjE,OAAO,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,EAClC,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,GACjC,IAAI,CA+MN"}
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.attachWsHandlers = attachWsHandlers;
|
|
4
|
-
const chat_item_mapper_1 = require("./chat-item-mapper");
|
|
5
|
-
const endpoint_1 = require("./endpoint");
|
|
6
|
-
function attachWsHandlers(agent, ws, inputId, isDirect, state, resolve, reject) {
|
|
7
|
-
ws.onmessage = (evt) => {
|
|
8
|
-
if (state.settled)
|
|
9
|
-
return;
|
|
10
|
-
const raw = typeof evt.data === 'string' ? evt.data : String(evt.data);
|
|
11
|
-
const data = JSON.parse(raw);
|
|
12
|
-
if (data?.type === 'PING') {
|
|
13
|
-
agent._lastPingTime = Date.now();
|
|
14
|
-
ws.send(JSON.stringify({ type: 'PONG' }));
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
if (data?.type === 'session_sync' && data.session) {
|
|
18
|
-
agent._currentSession = data.session;
|
|
19
|
-
}
|
|
20
|
-
if (data?.type === 'RECONNECTED') {
|
|
21
|
-
console.log('[RemoteAgent] Reconnected to session:', data.session_id);
|
|
22
|
-
}
|
|
23
|
-
if (data?.type === 'SESSION_MERGED' && data.server_newer) {
|
|
24
|
-
console.log('[RemoteAgent] Server had newer session, merged');
|
|
25
|
-
}
|
|
26
|
-
if (data?.type === 'mode_changed' && data.mode) {
|
|
27
|
-
if (!agent._currentSession) {
|
|
28
|
-
agent._currentSession = { mode: data.mode };
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
agent._currentSession.mode = data.mode;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
if (data?.type === 'ulw_turns_reached') {
|
|
35
|
-
agent._status = 'waiting';
|
|
36
|
-
if (agent._currentSession) {
|
|
37
|
-
agent._currentSession.ulw_turns_used = data.turns_used;
|
|
38
|
-
}
|
|
39
|
-
agent._addChatItem({
|
|
40
|
-
type: 'ulw_turns_reached',
|
|
41
|
-
turns_used: data.turns_used,
|
|
42
|
-
max_turns: data.max_turns,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
if (data?.type === 'llm_call' || data?.type === 'llm_result' ||
|
|
46
|
-
data?.type === 'tool_call' || data?.type === 'tool_result' ||
|
|
47
|
-
data?.type === 'thinking' || data?.type === 'assistant' ||
|
|
48
|
-
data?.type === 'intent' || data?.type === 'eval' || data?.type === 'compact' ||
|
|
49
|
-
data?.type === 'tool_blocked') {
|
|
50
|
-
agent._clearPlaceholder();
|
|
51
|
-
(0, chat_item_mapper_1.mapEventToChatItem)(agent._chatItems, data, (item) => agent._addChatItem(item));
|
|
52
|
-
if (data.session) {
|
|
53
|
-
agent._currentSession = data.session;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (data?.type === 'ask_user') {
|
|
57
|
-
agent._status = 'waiting';
|
|
58
|
-
agent._addChatItem({ type: 'ask_user', text: data.text || '', options: data.options || [], multi_select: data.multi_select || false });
|
|
59
|
-
}
|
|
60
|
-
if (data?.type === 'approval_needed') {
|
|
61
|
-
agent._status = 'waiting';
|
|
62
|
-
agent._addChatItem({
|
|
63
|
-
type: 'approval_needed',
|
|
64
|
-
tool: data.tool,
|
|
65
|
-
arguments: data.arguments,
|
|
66
|
-
...(data.description && { description: data.description }),
|
|
67
|
-
...(data.batch_remaining && { batch_remaining: data.batch_remaining }),
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
if (data?.type === 'plan_review') {
|
|
71
|
-
agent._status = 'waiting';
|
|
72
|
-
agent._addChatItem({
|
|
73
|
-
type: 'plan_review',
|
|
74
|
-
plan_content: data.plan_content,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
if (data?.type === 'ONBOARD_REQUIRED') {
|
|
78
|
-
agent._status = 'waiting';
|
|
79
|
-
agent._pendingPrompt = data.prompt || '';
|
|
80
|
-
agent._pendingInputId = inputId;
|
|
81
|
-
agent._pendingSessionId = agent._currentSession?.session_id || null;
|
|
82
|
-
agent._addChatItem({
|
|
83
|
-
type: 'onboard_required',
|
|
84
|
-
methods: (data.methods || []),
|
|
85
|
-
paymentAmount: data.payment_amount,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
if (data?.type === 'ONBOARD_SUCCESS') {
|
|
89
|
-
agent._addChatItem({
|
|
90
|
-
type: 'onboard_success',
|
|
91
|
-
level: data.level,
|
|
92
|
-
message: data.message,
|
|
93
|
-
});
|
|
94
|
-
if (agent._pendingPrompt && agent._activeWs) {
|
|
95
|
-
agent._status = 'working';
|
|
96
|
-
const retryPrompt = agent._pendingPrompt;
|
|
97
|
-
const retryInputId = agent._pendingInputId || (0, endpoint_1.generateUUID)();
|
|
98
|
-
agent._pendingPrompt = null;
|
|
99
|
-
agent._pendingInputId = null;
|
|
100
|
-
agent._sendInput(agent._activeWs, retryInputId, retryPrompt, agent._pendingSessionId || (0, endpoint_1.generateUUID)(), isDirect);
|
|
101
|
-
agent._pendingSessionId = null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (data?.type === 'OUTPUT') {
|
|
105
|
-
state.settled = true;
|
|
106
|
-
clearTimeout(state.timer);
|
|
107
|
-
agent._stopPingMonitor();
|
|
108
|
-
agent._clearPlaceholder();
|
|
109
|
-
agent._status = 'idle';
|
|
110
|
-
agent._shouldReconnect = false;
|
|
111
|
-
agent._connectionState = 'disconnected';
|
|
112
|
-
agent._reconnectAttempts = 0;
|
|
113
|
-
if (data.session) {
|
|
114
|
-
agent._currentSession = data.session;
|
|
115
|
-
}
|
|
116
|
-
if (data.server_newer && data.chat_items && Array.isArray(data.chat_items)) {
|
|
117
|
-
console.log('[RemoteAgent] Session was merged with newer server state');
|
|
118
|
-
const userItems = agent._chatItems.filter(item => item.type === 'user');
|
|
119
|
-
const serverNonUserItems = data.chat_items.filter(item => item.type !== 'user');
|
|
120
|
-
agent._chatItems = [...userItems, ...serverNonUserItems];
|
|
121
|
-
}
|
|
122
|
-
const result = data.result || '';
|
|
123
|
-
if (result) {
|
|
124
|
-
const lastAgent = agent._chatItems.filter((e) => e.type === 'agent').pop();
|
|
125
|
-
if (!lastAgent || lastAgent.content !== result) {
|
|
126
|
-
agent._addChatItem({ type: 'agent', content: result });
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
agent._activeWs = null;
|
|
130
|
-
ws.close();
|
|
131
|
-
resolve({ text: result, done: true });
|
|
132
|
-
}
|
|
133
|
-
if (data?.type === 'ERROR') {
|
|
134
|
-
state.settled = true;
|
|
135
|
-
clearTimeout(state.timer);
|
|
136
|
-
agent._stopPingMonitor();
|
|
137
|
-
agent._status = 'idle';
|
|
138
|
-
agent._shouldReconnect = false;
|
|
139
|
-
agent._connectionState = 'disconnected';
|
|
140
|
-
agent._activeWs = null;
|
|
141
|
-
ws.close();
|
|
142
|
-
reject(new Error(`Agent error: ${String(data.message || data.error || 'Unknown error')}`));
|
|
143
|
-
}
|
|
144
|
-
agent._onMessage?.();
|
|
145
|
-
};
|
|
146
|
-
ws.onerror = async (err) => {
|
|
147
|
-
if (state.settled)
|
|
148
|
-
return;
|
|
149
|
-
agent._stopPingMonitor();
|
|
150
|
-
clearTimeout(state.timer);
|
|
151
|
-
ws.close();
|
|
152
|
-
if (isDirect && !agent._directUrl) {
|
|
153
|
-
agent._resolvedEndpoint = undefined;
|
|
154
|
-
agent._endpointResolutionAttempted = false;
|
|
155
|
-
}
|
|
156
|
-
if (agent._shouldReconnect && agent._reconnectAttempts < agent._maxReconnectAttempts) {
|
|
157
|
-
agent._attemptReconnect(resolve, reject);
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
state.settled = true;
|
|
161
|
-
agent._status = 'idle';
|
|
162
|
-
agent._shouldReconnect = false;
|
|
163
|
-
agent._connectionState = 'disconnected';
|
|
164
|
-
reject(new Error(`WebSocket error: ${String(err)}`));
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
ws.onclose = async () => {
|
|
168
|
-
agent._activeWs = null;
|
|
169
|
-
agent._stopPingMonitor();
|
|
170
|
-
if (!state.settled) {
|
|
171
|
-
clearTimeout(state.timer);
|
|
172
|
-
if (isDirect && !agent._directUrl) {
|
|
173
|
-
agent._resolvedEndpoint = undefined;
|
|
174
|
-
agent._endpointResolutionAttempted = false;
|
|
175
|
-
}
|
|
176
|
-
if (agent._shouldReconnect && agent._reconnectAttempts < agent._maxReconnectAttempts) {
|
|
177
|
-
agent._attemptReconnect(resolve, reject);
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
state.settled = true;
|
|
181
|
-
agent._status = 'idle';
|
|
182
|
-
agent._shouldReconnect = false;
|
|
183
|
-
agent._connectionState = 'disconnected';
|
|
184
|
-
reject(new Error('Connection closed before response'));
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
}
|