@modelprofile.com/flexharness 10.24.1 → 11.0.1
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_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/classes.codesandbox.js +1 -1
- package/dist_ts/classes.flexharness.admission.js +1 -1
- package/dist_ts/classes.flexharness.callbacks.js +1 -1
- package/dist_ts/classes.flexharness.compaction.js +1 -1
- package/dist_ts/classes.flexharness.configurations.js +1 -1
- package/dist_ts/classes.flexharness.deletion.js +1 -1
- package/dist_ts/classes.flexharness.imports.js +1 -1
- package/dist_ts/classes.flexharness.lifecycle.js +1 -1
- package/dist_ts/classes.flexharness.permissions.js +1 -1
- package/dist_ts/classes.flexharness.projects.js +1 -1
- package/dist_ts/classes.flexharness.prompts.js +1 -1
- package/dist_ts/classes.flexharness.providers.js +1 -1
- package/dist_ts/classes.flexharness.results.js +1 -1
- package/dist_ts/classes.flexharness.reversions.js +1 -1
- package/dist_ts/classes.flexharness.runs.js +1 -1
- package/dist_ts/classes.flexharness.sessions.js +1 -1
- package/dist_ts/classes.flexharness.slashcommands.js +1 -1
- package/dist_ts/classes.flexharness.subagents.js +1 -1
- package/dist_ts/classes.flexharness.tooldispatch.js +1 -1
- package/dist_ts/classes.stores.js +1 -1
- package/dist_ts/classes.toolcatalog.js +1 -1
- package/dist_ts/errors.js +1 -1
- package/dist_ts/functions.toolpermissions.js +1 -1
- package/dist_ts/interfaces.d.ts +3 -3
- package/dist_ts/utils.harnessoptions.js +1 -1
- package/dist_ts/utils.json.js +1 -1
- package/dist_ts/utils.schemadeclarations.js +1 -1
- package/dist_ts/utils.sessionimport.js +1 -1
- package/dist_ts/utils.sessionmetrics.d.ts +1 -1
- package/dist_ts_chat/classes.chatsession.js +1 -1
- package/dist_ts_chat_harness/classes.transcript.js +1 -1
- package/dist_ts_chat_harness/functions.intents.js +1 -1
- package/dist_ts_chat_harness/functions.mapping.d.ts +1 -1
- package/dist_ts_chat_harness/functions.mapping.js +1 -1
- package/dist_ts_chat_harness_server/classes.chathub.d.ts +18 -7
- package/dist_ts_chat_harness_server/classes.chathub.js +65 -62
- package/dist_ts_chat_harness_server/classes.chatsubscription.d.ts +5 -3
- package/dist_ts_chat_harness_server/classes.chatsubscription.js +4 -4
- package/dist_ts_chat_harness_server/classes.scopedirectory.d.ts +17 -0
- package/dist_ts_chat_harness_server/classes.scopedirectory.js +49 -0
- package/dist_ts_chat_harness_server/classes.sessionlists.d.ts +3 -5
- package/dist_ts_chat_harness_server/classes.sessionlists.js +11 -12
- package/dist_ts_chat_harness_server/classes.turngate.d.ts +8 -2
- package/dist_ts_chat_harness_server/classes.turngate.js +22 -21
- package/dist_ts_chat_harness_server/functions.errorprojector.js +1 -1
- package/dist_ts_chat_harness_server/index.d.ts +3 -2
- package/dist_ts_chat_harness_server/index.js +3 -2
- package/dist_ts_chat_harness_server/interfaces.d.ts +35 -2
- package/dist_ts_chat_harness_server/plugins.d.ts +1 -1
- package/dist_ts_chat_harness_typedsocket/classes.clienttransport.d.ts +28 -3
- package/dist_ts_chat_harness_typedsocket/classes.clienttransport.js +71 -15
- package/dist_ts_chat_harness_typedsocket/classes.serverbinding.d.ts +5 -3
- package/dist_ts_chat_harness_typedsocket/classes.serverbinding.js +79 -16
- package/dist_ts_chat_harness_typedsocket/index.d.ts +2 -2
- package/dist_ts_chat_harness_typedsocket/index.js +1 -1
- package/dist_ts_chat_harness_typedsocket/interfaces.requests.d.ts +20 -11
- package/dist_ts_chat_web/flexchat-input.js +1 -1
- package/dist_ts_chat_web/flexchat-message.js +1 -1
- package/dist_ts_chat_web/flexchat-window.js +1 -1
- package/dist_ts_compaction/smartagent.compaction.js +1 -1
- package/dist_ts_kvm/ocr.js +1 -1
- package/dist_ts_ocr/classes.imageocr.js +1 -1
- package/dist_ts_ocr/ocr.mistral.js +1 -1
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +1 -1
- package/dist_ts_tools/tool.browser.js +1 -1
- package/dist_ts_tools/tool.filesystem.js +1 -1
- package/dist_ts_tools/tool.shell.js +1 -1
- package/dist_ts_tools_node/tool.context.js +1 -1
- package/dist_ts_typedrequest/catalog.d.ts +2 -2
- package/dist_ts_typedrequest/catalog.js +1 -1
- package/package.json +4 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_chat_harness_server/classes.chathub.ts +68 -61
- package/ts_chat_harness_server/classes.chatsubscription.ts +7 -5
- package/ts_chat_harness_server/classes.scopedirectory.ts +67 -0
- package/ts_chat_harness_server/classes.sessionlists.ts +11 -19
- package/ts_chat_harness_server/classes.turngate.ts +28 -21
- package/ts_chat_harness_server/index.ts +4 -5
- package/ts_chat_harness_server/interfaces.ts +35 -2
- package/ts_chat_harness_server/plugins.ts +0 -3
- package/ts_chat_harness_server/readme.md +6 -4
- package/ts_chat_harness_typedsocket/classes.clienttransport.ts +92 -23
- package/ts_chat_harness_typedsocket/classes.serverbinding.ts +88 -16
- package/ts_chat_harness_typedsocket/index.ts +4 -2
- package/ts_chat_harness_typedsocket/interfaces.requests.ts +15 -11
- package/ts_chat_harness_typedsocket/readme.md +7 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import { FlexHarnessChatScopeDirectory, flexHarnessChatSessionMeta } from './classes.scopedirectory.js';
|
|
2
3
|
import { FlexHarnessChatSessionLists } from './classes.sessionlists.js';
|
|
3
4
|
import { FlexHarnessChatSubscription } from './classes.chatsubscription.js';
|
|
4
5
|
import { FlexHarnessChatTurnGate } from './classes.turngate.js';
|
|
@@ -16,6 +17,8 @@ import {
|
|
|
16
17
|
} from './functions.validation.js';
|
|
17
18
|
import type {
|
|
18
19
|
IFlexHarnessChatCaller,
|
|
20
|
+
IFlexHarnessChatConversation,
|
|
21
|
+
IFlexHarnessChatDirectory,
|
|
19
22
|
IFlexHarnessChatHubOptions,
|
|
20
23
|
IFlexHarnessChatLimits,
|
|
21
24
|
IFlexHarnessChatPeers,
|
|
@@ -42,12 +45,6 @@ export const defaultFlexHarnessChatLimits: Readonly<IFlexHarnessChatLimits> = Ob
|
|
|
42
45
|
sweepIntervalMs: 5 * 60 * 1000,
|
|
43
46
|
});
|
|
44
47
|
|
|
45
|
-
/** A conversation as a session list shows it; an imported one says so. */
|
|
46
|
-
export const flexHarnessChatSessionMeta = (sessionArg: plugins.IFlexSession): plugins.IHarnessSessionMeta => ({
|
|
47
|
-
...plugins.flexSessionToHarnessMeta(sessionArg),
|
|
48
|
-
...(sessionArg.origin?.kind === 'import' ? { statusLabel: 'Imported' } : {}),
|
|
49
|
-
});
|
|
50
|
-
|
|
51
48
|
/**
|
|
52
49
|
* Serves `dees-harness-chat` conversations of one FlexHarness to browser tabs, over any transport:
|
|
53
50
|
* a tab attaches, follows one conversation through numbered updates, sends messages that start or
|
|
@@ -56,8 +53,10 @@ export const flexHarnessChatSessionMeta = (sessionArg: plugins.IFlexSession): pl
|
|
|
56
53
|
* tab that started the turn.
|
|
57
54
|
*
|
|
58
55
|
* The host authenticates every request and names its caller: the tab and the scope it may reach. A
|
|
59
|
-
* session of another scope reads as not found.
|
|
60
|
-
*
|
|
56
|
+
* session of another scope reads as not found. A host whose conversations run in scopes of their
|
|
57
|
+
* own gives the hub a directory: it says which conversations a tab reaches, and where they run.
|
|
58
|
+
* `stop()` stops the turns the hub started, its subscriptions and its timers; the harness stays the
|
|
59
|
+
* host's.
|
|
61
60
|
*/
|
|
62
61
|
export class FlexHarnessChatHub<TContext> {
|
|
63
62
|
private readonly limits: Readonly<IFlexHarnessChatLimits>;
|
|
@@ -65,6 +64,9 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
65
64
|
private readonly peers: IFlexHarnessChatPeers;
|
|
66
65
|
private readonly readContext: (valueArg: plugins.TJsonValue) => TContext;
|
|
67
66
|
private readonly titleOf: (sendArg: plugins.IHarnessComposerSendDetail) => string;
|
|
67
|
+
private readonly directory: IFlexHarnessChatDirectory;
|
|
68
|
+
/** without a directory of the host's, the harness's session events change the lists */
|
|
69
|
+
private readonly listsFromHarness: boolean;
|
|
68
70
|
private readonly gate: FlexHarnessChatTurnGate<TContext>;
|
|
69
71
|
private readonly sessionLists: FlexHarnessChatSessionLists;
|
|
70
72
|
private readonly subscriptions = new Map<string, FlexHarnessChatSubscription>();
|
|
@@ -82,8 +84,10 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
82
84
|
this.peers = optionsArg.peers;
|
|
83
85
|
this.readContext = optionsArg.readContext;
|
|
84
86
|
this.titleOf = optionsArg.titleOf ?? titleFromMessage;
|
|
87
|
+
this.directory = optionsArg.directory ?? new FlexHarnessChatScopeDirectory(this.harness);
|
|
88
|
+
this.listsFromHarness = !optionsArg.directory;
|
|
85
89
|
this.gate = new FlexHarnessChatTurnGate<TContext>(this.harness, this.limits);
|
|
86
|
-
this.sessionLists = new FlexHarnessChatSessionLists(this.peers
|
|
90
|
+
this.sessionLists = new FlexHarnessChatSessionLists(this.peers);
|
|
87
91
|
this.unsubscribe = this.harness.subscribe((eventArg) => this.takeEvent(eventArg));
|
|
88
92
|
this.sweepTimer = setInterval(() => void this.forgetClosedTabs(), this.limits.sweepIntervalMs);
|
|
89
93
|
this.sweepTimer.unref?.();
|
|
@@ -96,13 +100,7 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
96
100
|
this.assertRunning();
|
|
97
101
|
this.closeSubscriptionsOf(callerArg);
|
|
98
102
|
this.sessionLists.attach(callerArg);
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
sessions: sessions
|
|
102
|
-
.filter((session) => !session.parentSessionId)
|
|
103
|
-
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
|
104
|
-
.map(flexHarnessChatSessionMeta),
|
|
105
|
-
};
|
|
103
|
+
return { sessions: await this.call(() => this.directory.list(callerArg)) };
|
|
106
104
|
}
|
|
107
105
|
|
|
108
106
|
public open(
|
|
@@ -118,15 +116,15 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
118
116
|
): Promise<TResponse<'flexHarnessChatOpen'>> {
|
|
119
117
|
this.assertRunning();
|
|
120
118
|
const subscriptionId = readRandomId(requestArg?.subscriptionId);
|
|
121
|
-
const
|
|
119
|
+
const conversation = await this.requireConversation(callerArg, requestArg.sessionId);
|
|
122
120
|
this.assertRunning();
|
|
123
121
|
if (this.subscriptions.has(subscriptionId)) throw new FlexHarnessChatRequestError(BAD_REQUEST_MESSAGE);
|
|
124
122
|
this.closeSubscriptionsOf(callerArg);
|
|
125
123
|
const subscription = new FlexHarnessChatSubscription({
|
|
126
124
|
id: subscriptionId,
|
|
127
|
-
tabId: callerArg.tabId,
|
|
128
|
-
scopeId:
|
|
129
|
-
sessionId:
|
|
125
|
+
tab: { tabId: callerArg.tabId, scopeId: callerArg.scopeId },
|
|
126
|
+
scopeId: conversation.scopeId,
|
|
127
|
+
sessionId: conversation.sessionId,
|
|
130
128
|
harness: this.harness,
|
|
131
129
|
peers: this.peers,
|
|
132
130
|
limits: this.limits,
|
|
@@ -141,7 +139,7 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
141
139
|
this.closeSubscription(subscription);
|
|
142
140
|
throw this.toRequestError(error);
|
|
143
141
|
}
|
|
144
|
-
return { session:
|
|
142
|
+
return { session: conversation.meta, readOnly: conversation.readOnly };
|
|
145
143
|
}
|
|
146
144
|
|
|
147
145
|
public async close(
|
|
@@ -149,7 +147,7 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
149
147
|
requestArg: TRequest<'flexHarnessChatClose'>,
|
|
150
148
|
): Promise<TResponse<'flexHarnessChatClose'>> {
|
|
151
149
|
const subscription = this.subscriptions.get(requestArg?.subscriptionId);
|
|
152
|
-
if (subscription && isSameTab(subscription, callerArg)) this.closeSubscription(subscription);
|
|
150
|
+
if (subscription && isSameTab(subscription.tab, callerArg)) this.closeSubscription(subscription);
|
|
153
151
|
return {};
|
|
154
152
|
}
|
|
155
153
|
|
|
@@ -170,24 +168,23 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
170
168
|
this.assertRunning();
|
|
171
169
|
const send = readSendDetail(requestArg?.send, this.limits);
|
|
172
170
|
const context = this.readSendContext(requestArg.context);
|
|
173
|
-
let
|
|
171
|
+
let conversation: IFlexHarnessChatConversation;
|
|
174
172
|
let created = false;
|
|
175
173
|
if (requestArg.sessionId === undefined) {
|
|
176
|
-
|
|
174
|
+
conversation = await this.call(() => this.directory.create(callerArg, { title: this.titleOf(send) }));
|
|
177
175
|
created = true;
|
|
178
176
|
} else {
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
sessionId = session.sessionId;
|
|
177
|
+
conversation = await this.requireConversation(callerArg, requestArg.sessionId);
|
|
178
|
+
if (conversation.readOnly) throw new FlexHarnessChatRequestError(READ_ONLY_MESSAGE);
|
|
182
179
|
}
|
|
183
180
|
try {
|
|
184
181
|
// the hub may have stopped while the conversation was read or created
|
|
185
182
|
this.assertRunning();
|
|
186
|
-
return await this.gate.admit(callerArg,
|
|
183
|
+
return await this.gate.admit(callerArg, conversation, send, context, deadlineArg);
|
|
187
184
|
} catch (error) {
|
|
188
185
|
// a conversation created for this message that never got it is not kept
|
|
189
186
|
if (created && !(error instanceof plugins.FlexHarnessClosedError)) {
|
|
190
|
-
await this.
|
|
187
|
+
await this.directory.delete(callerArg, conversation).catch(() => undefined);
|
|
191
188
|
}
|
|
192
189
|
throw this.toRequestError(error);
|
|
193
190
|
}
|
|
@@ -197,8 +194,8 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
197
194
|
callerArg: IFlexHarnessChatCaller,
|
|
198
195
|
requestArg: TRequest<'flexHarnessChatAbort'>,
|
|
199
196
|
): Promise<TResponse<'flexHarnessChatAbort'>> {
|
|
200
|
-
const
|
|
201
|
-
await this.call(() => this.harness.abort(
|
|
197
|
+
const conversation = await this.requireConversation(callerArg, requestArg?.sessionId);
|
|
198
|
+
await this.call(() => this.harness.abort(conversation.scopeId, conversation.sessionId));
|
|
202
199
|
return {};
|
|
203
200
|
}
|
|
204
201
|
|
|
@@ -206,13 +203,13 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
206
203
|
callerArg: IFlexHarnessChatCaller,
|
|
207
204
|
requestArg: TRequest<'flexHarnessChatPermission'>,
|
|
208
205
|
): Promise<TResponse<'flexHarnessChatPermission'>> {
|
|
209
|
-
const
|
|
206
|
+
const conversation = await this.requireConversation(callerArg, requestArg?.sessionId);
|
|
210
207
|
if (typeof requestArg.permissionId !== 'string' || !DECISIONS.includes(requestArg.decision)) {
|
|
211
208
|
throw new FlexHarnessChatRequestError(BAD_REQUEST_MESSAGE);
|
|
212
209
|
}
|
|
213
210
|
await this.call(() => this.harness.respondToPermission(
|
|
214
|
-
|
|
215
|
-
|
|
211
|
+
conversation.scopeId,
|
|
212
|
+
conversation.sessionId,
|
|
216
213
|
requestArg.permissionId,
|
|
217
214
|
requestArg.decision,
|
|
218
215
|
));
|
|
@@ -223,13 +220,13 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
223
220
|
callerArg: IFlexHarnessChatCaller,
|
|
224
221
|
requestArg: TRequest<'flexHarnessChatHistory'>,
|
|
225
222
|
): Promise<TResponse<'flexHarnessChatHistory'>> {
|
|
226
|
-
const
|
|
223
|
+
const conversation = await this.requireConversation(callerArg, requestArg?.sessionId);
|
|
227
224
|
if (requestArg.direction === 'undo') {
|
|
228
|
-
const result = await this.call(() => this.harness.undoSession(
|
|
225
|
+
const result = await this.call(() => this.harness.undoSession(conversation.scopeId, conversation.sessionId));
|
|
229
226
|
return { runId: result.revertedRunId };
|
|
230
227
|
}
|
|
231
228
|
if (requestArg.direction === 'redo') {
|
|
232
|
-
const result = await this.call(() => this.harness.redoSession(
|
|
229
|
+
const result = await this.call(() => this.harness.redoSession(conversation.scopeId, conversation.sessionId));
|
|
233
230
|
return { runId: result.restoredRunId };
|
|
234
231
|
}
|
|
235
232
|
throw new FlexHarnessChatRequestError(BAD_REQUEST_MESSAGE);
|
|
@@ -240,23 +237,25 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
240
237
|
requestArg: TRequest<'flexHarnessChatSessionUpdate'>,
|
|
241
238
|
): Promise<TResponse<'flexHarnessChatSessionUpdate'>> {
|
|
242
239
|
const title = readTitle(requestArg?.title, this.limits);
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
return { session: flexHarnessChatSessionMeta(updated) };
|
|
240
|
+
const conversation = await this.requireConversation(callerArg, requestArg.sessionId);
|
|
241
|
+
return { session: await this.call(() => this.directory.rename(callerArg, conversation, title)) };
|
|
246
242
|
}
|
|
247
243
|
|
|
248
244
|
public async deleteSession(
|
|
249
245
|
callerArg: IFlexHarnessChatCaller,
|
|
250
246
|
requestArg: TRequest<'flexHarnessChatSessionDelete'>,
|
|
251
247
|
): Promise<TResponse<'flexHarnessChatSessionDelete'>> {
|
|
252
|
-
const
|
|
253
|
-
await this.call(() => this.
|
|
248
|
+
const conversation = await this.requireConversation(callerArg, requestArg?.sessionId);
|
|
249
|
+
await this.call(() => this.directory.delete(callerArg, conversation));
|
|
254
250
|
return {};
|
|
255
251
|
}
|
|
256
252
|
|
|
257
253
|
// ---------------- turns ----------------
|
|
258
254
|
|
|
259
|
-
/**
|
|
255
|
+
/**
|
|
256
|
+
* The running turn of a conversation, by the harness scope it runs in; the host's model resolver
|
|
257
|
+
* and tool provider read it.
|
|
258
|
+
*/
|
|
260
259
|
public turnOf(scopeIdArg: string, sessionIdArg: string): IFlexHarnessChatTurn<TContext> | undefined {
|
|
261
260
|
return this.gate.turnOf(scopeIdArg, sessionIdArg);
|
|
262
261
|
}
|
|
@@ -270,7 +269,7 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
270
269
|
requestArg: plugins.IFlexHarnessChatToolRequest,
|
|
271
270
|
optionsArg: { timeoutMs?: number; abortSignal?: AbortSignal } = {},
|
|
272
271
|
): Promise<plugins.TJsonValue> {
|
|
273
|
-
const peer = await this.peers.find(
|
|
272
|
+
const peer = await this.peers.find(turnArg.tab);
|
|
274
273
|
if (!peer) throw new FlexHarnessChatTabUnavailableError();
|
|
275
274
|
try {
|
|
276
275
|
return await peer.runTool(requestArg, {
|
|
@@ -285,6 +284,15 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
285
284
|
|
|
286
285
|
// ---------------- tabs and lifecycle ----------------
|
|
287
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Tells the tabs of a scope that conversations of their list changed. A host with a directory of
|
|
289
|
+
* its own calls it for every change; without one, the harness's session events do.
|
|
290
|
+
*/
|
|
291
|
+
public publishSessions(scopeIdArg: string, changesArg: Partial<plugins.IFlexHarnessChatSessionsPush>): void {
|
|
292
|
+
if (this.stopped) return;
|
|
293
|
+
this.sessionLists.publish(scopeIdArg, changesArg);
|
|
294
|
+
}
|
|
295
|
+
|
|
288
296
|
/** Forgets a tab that is gone: its conversation list and the conversation it followed. */
|
|
289
297
|
public forgetTab(tabArg: IFlexHarnessChatCaller): void {
|
|
290
298
|
this.closeSubscriptionsOf(tabArg);
|
|
@@ -317,16 +325,20 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
317
325
|
this.lastSequence = eventArg.sequence;
|
|
318
326
|
this.gate.takeEvent(eventArg);
|
|
319
327
|
for (const subscription of this.subscriptions.values()) subscription.takeEvent(eventArg, preceding);
|
|
328
|
+
if (!this.listsFromHarness) return;
|
|
320
329
|
if (eventArg.type === 'session.created' || eventArg.type === 'session.updated' || eventArg.type === 'session.deleted') {
|
|
321
|
-
if (
|
|
330
|
+
if (eventArg.session.parentSessionId) return;
|
|
331
|
+
this.sessionLists.publish(eventArg.scopeId, eventArg.type === 'session.deleted'
|
|
332
|
+
? { deletedIds: [eventArg.sessionId] }
|
|
333
|
+
: { upserts: [flexHarnessChatSessionMeta(eventArg.session)] });
|
|
322
334
|
}
|
|
323
335
|
}
|
|
324
336
|
|
|
325
337
|
/** Forgets the tabs that are gone and run no turn; runs every `sweepIntervalMs` on its own. */
|
|
326
338
|
private async forgetClosedTabs(): Promise<void> {
|
|
327
339
|
const tabs = new Map<string, IFlexHarnessChatCaller>();
|
|
328
|
-
for (const tab of [...this.sessionLists.tabs, ...this.subscriptions.values()]) {
|
|
329
|
-
tabs.set(tabKey(tab),
|
|
340
|
+
for (const tab of [...this.sessionLists.tabs, ...[...this.subscriptions.values()].map((subscription) => subscription.tab)]) {
|
|
341
|
+
tabs.set(tabKey(tab), tab);
|
|
330
342
|
}
|
|
331
343
|
for (const tab of tabs.values()) {
|
|
332
344
|
if (this.stopped) return;
|
|
@@ -338,7 +350,7 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
338
350
|
|
|
339
351
|
private closeSubscriptionsOf(tabArg: IFlexHarnessChatCaller): void {
|
|
340
352
|
for (const subscription of [...this.subscriptions.values()]) {
|
|
341
|
-
if (isSameTab(subscription, tabArg)) this.closeSubscription(subscription);
|
|
353
|
+
if (isSameTab(subscription.tab, tabArg)) this.closeSubscription(subscription);
|
|
342
354
|
}
|
|
343
355
|
}
|
|
344
356
|
|
|
@@ -366,20 +378,15 @@ export class FlexHarnessChatHub<TContext> {
|
|
|
366
378
|
}
|
|
367
379
|
}
|
|
368
380
|
|
|
369
|
-
/** A
|
|
370
|
-
private async
|
|
381
|
+
/** A conversation the caller reaches; one it does not reach reads as not found. */
|
|
382
|
+
private async requireConversation(
|
|
383
|
+
callerArg: IFlexHarnessChatCaller,
|
|
384
|
+
sessionIdArg: unknown,
|
|
385
|
+
): Promise<IFlexHarnessChatConversation> {
|
|
371
386
|
const sessionId = readSessionId(sessionIdArg, NOT_FOUND_MESSAGE);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
} catch (error) {
|
|
376
|
-
if (error instanceof plugins.FlexHarnessNotFoundError || error instanceof plugins.FlexHarnessValidationError) {
|
|
377
|
-
throw new FlexHarnessChatRequestError(NOT_FOUND_MESSAGE);
|
|
378
|
-
}
|
|
379
|
-
throw this.toRequestError(error);
|
|
380
|
-
}
|
|
381
|
-
if (session.parentSessionId) throw new FlexHarnessChatRequestError(NOT_FOUND_MESSAGE);
|
|
382
|
-
return session;
|
|
387
|
+
const conversation = await this.call(() => this.directory.find(callerArg, sessionId));
|
|
388
|
+
if (!conversation) throw new FlexHarnessChatRequestError(NOT_FOUND_MESSAGE);
|
|
389
|
+
return conversation;
|
|
383
390
|
}
|
|
384
391
|
|
|
385
392
|
private async call<T>(fnArg: () => Promise<T>): Promise<T> {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
-
import type { IFlexHarnessChatLimits, IFlexHarnessChatPeers } from './interfaces.js';
|
|
2
|
+
import type { IFlexHarnessChatCaller, IFlexHarnessChatLimits, IFlexHarnessChatPeers } from './interfaces.js';
|
|
3
3
|
|
|
4
4
|
type TUpdate = plugins.IFlexHarnessChatUpdate;
|
|
5
5
|
|
|
@@ -44,7 +44,9 @@ export type TFlexHarnessChatSubscriptionHarness = Pick<
|
|
|
44
44
|
|
|
45
45
|
export interface IFlexHarnessChatSubscriptionOptions {
|
|
46
46
|
id: string;
|
|
47
|
-
|
|
47
|
+
/** the tab that follows the conversation */
|
|
48
|
+
tab: IFlexHarnessChatCaller;
|
|
49
|
+
/** the harness scope of the conversation */
|
|
48
50
|
scopeId: string;
|
|
49
51
|
sessionId: string;
|
|
50
52
|
harness: TFlexHarnessChatSubscriptionHarness;
|
|
@@ -61,7 +63,7 @@ export interface IFlexHarnessChatSubscriptionOptions {
|
|
|
61
63
|
*/
|
|
62
64
|
export class FlexHarnessChatSubscription {
|
|
63
65
|
public readonly id: string;
|
|
64
|
-
public readonly
|
|
66
|
+
public readonly tab: IFlexHarnessChatCaller;
|
|
65
67
|
public readonly scopeId: string;
|
|
66
68
|
public readonly sessionId: string;
|
|
67
69
|
|
|
@@ -82,7 +84,7 @@ export class FlexHarnessChatSubscription {
|
|
|
82
84
|
|
|
83
85
|
constructor(optionsArg: IFlexHarnessChatSubscriptionOptions) {
|
|
84
86
|
this.id = optionsArg.id;
|
|
85
|
-
this.
|
|
87
|
+
this.tab = optionsArg.tab;
|
|
86
88
|
this.scopeId = optionsArg.scopeId;
|
|
87
89
|
this.sessionId = optionsArg.sessionId;
|
|
88
90
|
this.harness = optionsArg.harness;
|
|
@@ -181,7 +183,7 @@ export class FlexHarnessChatSubscription {
|
|
|
181
183
|
let inFlight: TUpdate | null = null;
|
|
182
184
|
try {
|
|
183
185
|
while (!this.closed && this.state === 'live' && hasPending(this.pending)) {
|
|
184
|
-
const peer = await this.peers.find(
|
|
186
|
+
const peer = await this.peers.find(this.tab);
|
|
185
187
|
if (!peer) {
|
|
186
188
|
this.closeSelf();
|
|
187
189
|
return;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type {
|
|
3
|
+
IFlexHarnessChatCaller,
|
|
4
|
+
IFlexHarnessChatConversation,
|
|
5
|
+
IFlexHarnessChatDirectory,
|
|
6
|
+
TFlexHarnessChatHubHarness,
|
|
7
|
+
} from './interfaces.js';
|
|
8
|
+
|
|
9
|
+
/** A conversation as a session list shows it; an imported one says so. */
|
|
10
|
+
export const flexHarnessChatSessionMeta = (sessionArg: plugins.IFlexSession): plugins.IHarnessSessionMeta => ({
|
|
11
|
+
...plugins.flexSessionToHarnessMeta(sessionArg),
|
|
12
|
+
...(sessionArg.origin?.kind === 'import' ? { statusLabel: 'Imported' } : {}),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const conversationOf = (sessionArg: plugins.IFlexSession): IFlexHarnessChatConversation => ({
|
|
16
|
+
scopeId: sessionArg.scopeId,
|
|
17
|
+
sessionId: sessionArg.sessionId,
|
|
18
|
+
meta: flexHarnessChatSessionMeta(sessionArg),
|
|
19
|
+
readOnly: sessionArg.origin?.kind === 'import',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** The directory a hub has without one of the host's: a tab's conversations are the root sessions of its scope. */
|
|
23
|
+
export class FlexHarnessChatScopeDirectory implements IFlexHarnessChatDirectory {
|
|
24
|
+
private readonly harness: TFlexHarnessChatHubHarness;
|
|
25
|
+
|
|
26
|
+
constructor(harnessArg: TFlexHarnessChatHubHarness) {
|
|
27
|
+
this.harness = harnessArg;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async list(tabArg: IFlexHarnessChatCaller): Promise<plugins.IHarnessSessionMeta[]> {
|
|
31
|
+
const sessions = await this.harness.listSessions(tabArg.scopeId);
|
|
32
|
+
return sessions
|
|
33
|
+
.filter((session) => !session.parentSessionId)
|
|
34
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
|
35
|
+
.map(flexHarnessChatSessionMeta);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A root session of the tab's scope; another scope's session id reads as not found. */
|
|
39
|
+
public async find(tabArg: IFlexHarnessChatCaller, sessionIdArg: string): Promise<IFlexHarnessChatConversation | null> {
|
|
40
|
+
let session: plugins.IFlexSession;
|
|
41
|
+
try {
|
|
42
|
+
session = await this.harness.getSession(tabArg.scopeId, sessionIdArg);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof plugins.FlexHarnessNotFoundError || error instanceof plugins.FlexHarnessValidationError) return null;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
return session.parentSessionId ? null : conversationOf(session);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async create(tabArg: IFlexHarnessChatCaller, initArg: { title: string }): Promise<IFlexHarnessChatConversation> {
|
|
51
|
+
return conversationOf(await this.harness.createSession(tabArg.scopeId, { title: initArg.title }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public async rename(
|
|
55
|
+
_tabArg: IFlexHarnessChatCaller,
|
|
56
|
+
conversationArg: IFlexHarnessChatConversation,
|
|
57
|
+
titleArg: string,
|
|
58
|
+
): Promise<plugins.IHarnessSessionMeta> {
|
|
59
|
+
return flexHarnessChatSessionMeta(
|
|
60
|
+
await this.harness.updateSession(conversationArg.scopeId, conversationArg.sessionId, { title: titleArg }),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public async delete(_tabArg: IFlexHarnessChatCaller, conversationArg: IFlexHarnessChatConversation): Promise<void> {
|
|
65
|
+
await this.harness.deleteSession(conversationArg.scopeId, conversationArg.sessionId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -2,11 +2,6 @@ import type * as plugins from './plugins.js';
|
|
|
2
2
|
import { tabKey } from './functions.validation.js';
|
|
3
3
|
import type { IFlexHarnessChatCaller, IFlexHarnessChatPeers } from './interfaces.js';
|
|
4
4
|
|
|
5
|
-
type TSessionEvent =
|
|
6
|
-
| plugins.IFlexSessionCreatedEvent
|
|
7
|
-
| plugins.IFlexSessionUpdatedEvent
|
|
8
|
-
| plugins.IFlexSessionDeletedEvent;
|
|
9
|
-
|
|
10
5
|
/** A tab and the changes of its conversation list it has not received yet. */
|
|
11
6
|
interface IAttachedTab {
|
|
12
7
|
tab: IFlexHarnessChatCaller;
|
|
@@ -22,15 +17,10 @@ interface IAttachedTab {
|
|
|
22
17
|
*/
|
|
23
18
|
export class FlexHarnessChatSessionLists {
|
|
24
19
|
private readonly peers: IFlexHarnessChatPeers;
|
|
25
|
-
private readonly toSessionMeta: (sessionArg: plugins.IFlexSession) => plugins.IHarnessSessionMeta;
|
|
26
20
|
private readonly attached = new Map<string, IAttachedTab>();
|
|
27
21
|
|
|
28
|
-
constructor(
|
|
29
|
-
peersArg: IFlexHarnessChatPeers,
|
|
30
|
-
toSessionMetaArg: (sessionArg: plugins.IFlexSession) => plugins.IHarnessSessionMeta,
|
|
31
|
-
) {
|
|
22
|
+
constructor(peersArg: IFlexHarnessChatPeers) {
|
|
32
23
|
this.peers = peersArg;
|
|
33
|
-
this.toSessionMeta = toSessionMetaArg;
|
|
34
24
|
}
|
|
35
25
|
|
|
36
26
|
public get tabs(): IFlexHarnessChatCaller[] {
|
|
@@ -54,15 +44,17 @@ export class FlexHarnessChatSessionLists {
|
|
|
54
44
|
this.attached.clear();
|
|
55
45
|
}
|
|
56
46
|
|
|
57
|
-
|
|
47
|
+
/** Changes of the lists of the tabs of a scope; a deletion and an upsert of one id: the upsert wins. */
|
|
48
|
+
public publish(scopeIdArg: string, changesArg: Partial<plugins.IFlexHarnessChatSessionsPush>): void {
|
|
58
49
|
for (const [key, entry] of this.attached) {
|
|
59
|
-
if (entry.tab.scopeId !==
|
|
60
|
-
|
|
61
|
-
entry.upserts.delete(
|
|
62
|
-
entry.deletedIds.add(
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
entry.
|
|
50
|
+
if (entry.tab.scopeId !== scopeIdArg) continue;
|
|
51
|
+
for (const sessionId of changesArg.deletedIds ?? []) {
|
|
52
|
+
entry.upserts.delete(sessionId);
|
|
53
|
+
entry.deletedIds.add(sessionId);
|
|
54
|
+
}
|
|
55
|
+
for (const session of changesArg.upserts ?? []) {
|
|
56
|
+
entry.deletedIds.delete(session.id);
|
|
57
|
+
entry.upserts.set(session.id, session);
|
|
66
58
|
}
|
|
67
59
|
void this.pump(key, entry);
|
|
68
60
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { FlexHarnessChatRequestError } from './errors.js';
|
|
3
|
-
import { STOPPED_MESSAGE } from './functions.validation.js';
|
|
3
|
+
import { STOPPED_MESSAGE, isSameTab } from './functions.validation.js';
|
|
4
4
|
import type { IFlexHarnessChatCaller, IFlexHarnessChatLimits, IFlexHarnessChatTurn } from './interfaces.js';
|
|
5
5
|
|
|
6
6
|
const STILL_FINISHING_MESSAGE =
|
|
@@ -12,6 +12,12 @@ export type TFlexHarnessChatGateHarness = Pick<
|
|
|
12
12
|
'enqueuePrompt' | 'steerPrompt' | 'getPromptQueueEntry' | 'cancelPrompt'
|
|
13
13
|
>;
|
|
14
14
|
|
|
15
|
+
/** A conversation as the gate reaches it: the harness scope it runs in, and its session. */
|
|
16
|
+
interface ITarget {
|
|
17
|
+
scopeId: string;
|
|
18
|
+
sessionId: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
/** A turn as the gate tracks it, from its admission until its run ended. */
|
|
16
22
|
interface ITurnBinding<TContext> extends IFlexHarnessChatTurn<TContext> {
|
|
17
23
|
/** set once the harness queued the prompt */
|
|
@@ -62,14 +68,14 @@ export class FlexHarnessChatTurnGate<TContext> {
|
|
|
62
68
|
public turnOf(scopeIdArg: string, sessionIdArg: string): IFlexHarnessChatTurn<TContext> | undefined {
|
|
63
69
|
const binding = this.bindings.get(bindingKey(scopeIdArg, sessionIdArg));
|
|
64
70
|
return binding
|
|
65
|
-
? { scopeId: binding.scopeId, sessionId: binding.sessionId,
|
|
71
|
+
? { scopeId: binding.scopeId, sessionId: binding.sessionId, tab: binding.tab, context: binding.context }
|
|
66
72
|
: undefined;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
/** Whether a turn the tab started still runs. */
|
|
70
76
|
public hasTurnOf(tabArg: IFlexHarnessChatCaller): boolean {
|
|
71
77
|
for (const binding of this.bindings.values()) {
|
|
72
|
-
if (binding.
|
|
78
|
+
if (isSameTab(binding.tab, tabArg)) return true;
|
|
73
79
|
}
|
|
74
80
|
return false;
|
|
75
81
|
}
|
|
@@ -81,17 +87,18 @@ export class FlexHarnessChatTurnGate<TContext> {
|
|
|
81
87
|
if (binding && binding.queueId === eventArg.queueId) binding.markRunning();
|
|
82
88
|
}
|
|
83
89
|
|
|
84
|
-
/** Admits a message under the conversation's lock; every wait ends at the deadline. */
|
|
90
|
+
/** Admits a message of the caller's tab under the conversation's lock; every wait ends at the deadline. */
|
|
85
91
|
public admit(
|
|
86
92
|
callerArg: IFlexHarnessChatCaller,
|
|
87
|
-
|
|
93
|
+
targetArg: ITarget,
|
|
88
94
|
sendArg: plugins.IHarnessComposerSendDetail,
|
|
89
95
|
contextArg: TContext,
|
|
90
96
|
deadlineArg: number,
|
|
91
97
|
): Promise<plugins.TFlexHarnessChatResponse<'flexHarnessChatSend'>> {
|
|
92
98
|
if (this.stopped) return Promise.reject(new FlexHarnessChatRequestError(STOPPED_MESSAGE));
|
|
93
|
-
const
|
|
94
|
-
|
|
99
|
+
const target = { scopeId: targetArg.scopeId, sessionId: targetArg.sessionId };
|
|
100
|
+
const admission = this.withLock(bindingKey(target.scopeId, target.sessionId), () =>
|
|
101
|
+
this.admitLocked(callerArg, target, sendArg, contextArg, deadlineArg),
|
|
95
102
|
);
|
|
96
103
|
const settled = admission.then(() => undefined, () => undefined);
|
|
97
104
|
this.admissions.add(settled);
|
|
@@ -144,7 +151,7 @@ export class FlexHarnessChatTurnGate<TContext> {
|
|
|
144
151
|
|
|
145
152
|
private async admitLocked(
|
|
146
153
|
callerArg: IFlexHarnessChatCaller,
|
|
147
|
-
|
|
154
|
+
target: ITarget,
|
|
148
155
|
sendArg: plugins.IHarnessComposerSendDetail,
|
|
149
156
|
contextArg: TContext,
|
|
150
157
|
deadlineArg: number,
|
|
@@ -153,7 +160,6 @@ export class FlexHarnessChatTurnGate<TContext> {
|
|
|
153
160
|
if (!(await settlesBefore(promiseArg, deadlineArg))) throw new FlexHarnessChatRequestError(STILL_FINISHING_MESSAGE);
|
|
154
161
|
};
|
|
155
162
|
this.assertOpen();
|
|
156
|
-
const target = { scopeId: callerArg.scopeId, sessionId: sessionIdArg };
|
|
157
163
|
const binding = this.bindings.get(bindingKey(target.scopeId, target.sessionId));
|
|
158
164
|
if (binding?.queueId) {
|
|
159
165
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
@@ -165,7 +171,7 @@ export class FlexHarnessChatTurnGate<TContext> {
|
|
|
165
171
|
steerId: intent.steerId,
|
|
166
172
|
prompt: intent.prompt,
|
|
167
173
|
});
|
|
168
|
-
return { sessionId:
|
|
174
|
+
return { sessionId: target.sessionId, steerId: intent.steerId };
|
|
169
175
|
} catch (error) {
|
|
170
176
|
if (!(error instanceof plugins.FlexHarnessSteerRejectedError) || error.reason !== 'not-running') throw error;
|
|
171
177
|
}
|
|
@@ -181,24 +187,25 @@ export class FlexHarnessChatTurnGate<TContext> {
|
|
|
181
187
|
await waitFor(binding.settled);
|
|
182
188
|
}
|
|
183
189
|
this.assertOpen();
|
|
184
|
-
return this.startTurn(callerArg,
|
|
190
|
+
return this.startTurn(callerArg, target, sendArg, contextArg);
|
|
185
191
|
}
|
|
186
192
|
|
|
187
193
|
private async startTurn(
|
|
188
194
|
callerArg: IFlexHarnessChatCaller,
|
|
189
|
-
|
|
195
|
+
target: ITarget,
|
|
190
196
|
sendArg: plugins.IHarnessComposerSendDetail,
|
|
191
197
|
contextArg: TContext,
|
|
192
198
|
): Promise<plugins.TFlexHarnessChatResponse<'flexHarnessChatSend'>> {
|
|
193
|
-
const
|
|
194
|
-
const
|
|
199
|
+
const { scopeId, sessionId } = target;
|
|
200
|
+
const key = bindingKey(scopeId, sessionId);
|
|
201
|
+
const intent = intentOf(target, { ...sendArg, delivery: 'turn' });
|
|
195
202
|
if (intent.type !== 'enqueuePrompt') throw new Error('A new turn must be queued.');
|
|
196
203
|
let markSettled!: () => void;
|
|
197
204
|
let markRunning!: () => void;
|
|
198
205
|
const binding: ITurnBinding<TContext> = {
|
|
199
|
-
scopeId
|
|
200
|
-
sessionId
|
|
201
|
-
tabId: callerArg.tabId,
|
|
206
|
+
scopeId,
|
|
207
|
+
sessionId,
|
|
208
|
+
tab: { tabId: callerArg.tabId, scopeId: callerArg.scopeId },
|
|
202
209
|
context: contextArg,
|
|
203
210
|
running: new Promise<void>((resolve) => (markRunning = resolve)),
|
|
204
211
|
markRunning: () => markRunning(),
|
|
@@ -214,7 +221,7 @@ export class FlexHarnessChatTurnGate<TContext> {
|
|
|
214
221
|
this.bindings.set(key, binding);
|
|
215
222
|
let queued: plugins.IFlexPromptQueueAdmission;
|
|
216
223
|
try {
|
|
217
|
-
queued = await this.harness.enqueuePrompt(
|
|
224
|
+
queued = await this.harness.enqueuePrompt(scopeId, sessionId, intent.prompt);
|
|
218
225
|
} catch (error) {
|
|
219
226
|
end();
|
|
220
227
|
throw error;
|
|
@@ -224,12 +231,12 @@ export class FlexHarnessChatTurnGate<TContext> {
|
|
|
224
231
|
void queued.completion.then(() => undefined, () => undefined).finally(end);
|
|
225
232
|
if (this.stopped) {
|
|
226
233
|
// the gate stopped while the prompt was being queued: the turn never runs on
|
|
227
|
-
this.cancel(
|
|
234
|
+
this.cancel(scopeId, sessionId, queued.queueId);
|
|
228
235
|
throw new FlexHarnessChatRequestError(STOPPED_MESSAGE);
|
|
229
236
|
}
|
|
230
|
-
binding.timer = setTimeout(() => this.cancel(
|
|
237
|
+
binding.timer = setTimeout(() => this.cancel(scopeId, sessionId, queued.queueId), this.limits.turnTimeoutMs);
|
|
231
238
|
binding.timer.unref?.();
|
|
232
|
-
return { sessionId
|
|
239
|
+
return { sessionId, queueId: queued.queueId };
|
|
233
240
|
}
|
|
234
241
|
}
|
|
235
242
|
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
3
|
-
defaultFlexHarnessChatLimits,
|
|
4
|
-
flexHarnessChatSessionMeta,
|
|
5
|
-
} from './classes.chathub.js';
|
|
1
|
+
export { FlexHarnessChatHub, defaultFlexHarnessChatLimits } from './classes.chathub.js';
|
|
2
|
+
export { flexHarnessChatSessionMeta } from './classes.scopedirectory.js';
|
|
6
3
|
export { FlexHarnessChatRequestError, FlexHarnessChatTabUnavailableError } from './errors.js';
|
|
7
4
|
export {
|
|
8
5
|
createFlexHarnessChatErrorProjector,
|
|
@@ -10,6 +7,8 @@ export {
|
|
|
10
7
|
} from './functions.errorprojector.js';
|
|
11
8
|
export type {
|
|
12
9
|
IFlexHarnessChatCaller,
|
|
10
|
+
IFlexHarnessChatConversation,
|
|
11
|
+
IFlexHarnessChatDirectory,
|
|
13
12
|
IFlexHarnessChatHubOptions,
|
|
14
13
|
IFlexHarnessChatLimits,
|
|
15
14
|
IFlexHarnessChatPeer,
|