@modelprofile.com/flexharness 10.24.0 → 11.0.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.
Files changed (135) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/classes.codesandbox.js +1 -1
  3. package/dist_ts/classes.flexharness.admission.d.ts +17 -0
  4. package/dist_ts/classes.flexharness.admission.js +263 -0
  5. package/dist_ts/classes.flexharness.callbacks.d.ts +38 -0
  6. package/dist_ts/classes.flexharness.callbacks.js +313 -0
  7. package/dist_ts/classes.flexharness.compaction.d.ts +25 -0
  8. package/dist_ts/classes.flexharness.compaction.js +87 -0
  9. package/dist_ts/classes.flexharness.configurations.d.ts +11 -0
  10. package/dist_ts/classes.flexharness.configurations.js +137 -0
  11. package/dist_ts/classes.flexharness.core.d.ts +143 -0
  12. package/dist_ts/classes.flexharness.core.js +1538 -0
  13. package/dist_ts/classes.flexharness.d.ts +2 -437
  14. package/dist_ts/classes.flexharness.deletion.d.ts +31 -0
  15. package/dist_ts/classes.flexharness.deletion.js +396 -0
  16. package/dist_ts/classes.flexharness.imports.d.ts +24 -0
  17. package/dist_ts/classes.flexharness.imports.js +345 -0
  18. package/dist_ts/classes.flexharness.js +119 -9776
  19. package/dist_ts/classes.flexharness.lifecycle.d.ts +47 -0
  20. package/dist_ts/classes.flexharness.lifecycle.js +625 -0
  21. package/dist_ts/classes.flexharness.permissions.d.ts +19 -0
  22. package/dist_ts/classes.flexharness.permissions.js +450 -0
  23. package/dist_ts/classes.flexharness.projects.d.ts +55 -0
  24. package/dist_ts/classes.flexharness.projects.js +575 -0
  25. package/dist_ts/classes.flexharness.prompts.d.ts +44 -0
  26. package/dist_ts/classes.flexharness.prompts.js +659 -0
  27. package/dist_ts/classes.flexharness.providers.d.ts +18 -0
  28. package/dist_ts/classes.flexharness.providers.js +267 -0
  29. package/dist_ts/classes.flexharness.results.d.ts +26 -0
  30. package/dist_ts/classes.flexharness.results.js +89 -0
  31. package/dist_ts/classes.flexharness.reversions.d.ts +52 -0
  32. package/dist_ts/classes.flexharness.reversions.js +1306 -0
  33. package/dist_ts/classes.flexharness.runs.d.ts +63 -0
  34. package/dist_ts/classes.flexharness.runs.js +1058 -0
  35. package/dist_ts/classes.flexharness.sessions.d.ts +20 -0
  36. package/dist_ts/classes.flexharness.sessions.js +381 -0
  37. package/dist_ts/classes.flexharness.slashcommands.d.ts +40 -0
  38. package/dist_ts/classes.flexharness.slashcommands.js +458 -0
  39. package/dist_ts/classes.flexharness.subagents.d.ts +31 -0
  40. package/dist_ts/classes.flexharness.subagents.js +690 -0
  41. package/dist_ts/classes.flexharness.tooldispatch.d.ts +53 -0
  42. package/dist_ts/classes.flexharness.tooldispatch.js +418 -0
  43. package/dist_ts/classes.stores.js +1 -1
  44. package/dist_ts/classes.toolcatalog.js +1 -1
  45. package/dist_ts/errors.js +1 -1
  46. package/dist_ts/functions.toolpermissions.js +1 -1
  47. package/dist_ts/interfaces.d.ts +3 -3
  48. package/dist_ts/interfaces.flexharness.d.ts +455 -0
  49. package/dist_ts/interfaces.flexharness.js +2 -0
  50. package/dist_ts/utils.flexharness.d.ts +19 -0
  51. package/dist_ts/utils.flexharness.js +83 -0
  52. package/dist_ts/utils.harnessoptions.js +1 -1
  53. package/dist_ts/utils.json.js +1 -1
  54. package/dist_ts/utils.schemadeclarations.js +1 -1
  55. package/dist_ts/utils.sessionimport.js +1 -1
  56. package/dist_ts/utils.sessionmetrics.d.ts +1 -1
  57. package/dist_ts_chat/classes.chatsession.js +1 -1
  58. package/dist_ts_chat_harness/classes.transcript.js +1 -1
  59. package/dist_ts_chat_harness/functions.intents.js +1 -1
  60. package/dist_ts_chat_harness/functions.mapping.d.ts +1 -1
  61. package/dist_ts_chat_harness/functions.mapping.js +1 -1
  62. package/dist_ts_chat_harness_server/classes.chathub.d.ts +18 -7
  63. package/dist_ts_chat_harness_server/classes.chathub.js +65 -62
  64. package/dist_ts_chat_harness_server/classes.chatsubscription.d.ts +5 -3
  65. package/dist_ts_chat_harness_server/classes.chatsubscription.js +4 -4
  66. package/dist_ts_chat_harness_server/classes.scopedirectory.d.ts +17 -0
  67. package/dist_ts_chat_harness_server/classes.scopedirectory.js +49 -0
  68. package/dist_ts_chat_harness_server/classes.sessionlists.d.ts +3 -5
  69. package/dist_ts_chat_harness_server/classes.sessionlists.js +11 -12
  70. package/dist_ts_chat_harness_server/classes.turngate.d.ts +8 -2
  71. package/dist_ts_chat_harness_server/classes.turngate.js +22 -21
  72. package/dist_ts_chat_harness_server/functions.errorprojector.js +1 -1
  73. package/dist_ts_chat_harness_server/index.d.ts +3 -2
  74. package/dist_ts_chat_harness_server/index.js +3 -2
  75. package/dist_ts_chat_harness_server/interfaces.d.ts +35 -2
  76. package/dist_ts_chat_harness_server/plugins.d.ts +1 -1
  77. package/dist_ts_chat_harness_typedsocket/classes.clienttransport.d.ts +28 -3
  78. package/dist_ts_chat_harness_typedsocket/classes.clienttransport.js +71 -15
  79. package/dist_ts_chat_harness_typedsocket/classes.serverbinding.d.ts +5 -3
  80. package/dist_ts_chat_harness_typedsocket/classes.serverbinding.js +79 -16
  81. package/dist_ts_chat_harness_typedsocket/index.d.ts +2 -2
  82. package/dist_ts_chat_harness_typedsocket/index.js +1 -1
  83. package/dist_ts_chat_harness_typedsocket/interfaces.requests.d.ts +20 -11
  84. package/dist_ts_chat_web/flexchat-input.js +1 -1
  85. package/dist_ts_chat_web/flexchat-message.js +1 -1
  86. package/dist_ts_chat_web/flexchat-window.js +1 -1
  87. package/dist_ts_compaction/smartagent.compaction.js +1 -1
  88. package/dist_ts_kvm/ocr.js +1 -1
  89. package/dist_ts_ocr/classes.imageocr.js +1 -1
  90. package/dist_ts_ocr/ocr.mistral.js +1 -1
  91. package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +1 -1
  92. package/dist_ts_tools/tool.browser.js +1 -1
  93. package/dist_ts_tools/tool.filesystem.js +1 -1
  94. package/dist_ts_tools/tool.shell.js +1 -1
  95. package/dist_ts_tools_node/tool.context.js +1 -1
  96. package/dist_ts_typedrequest/catalog.d.ts +2 -2
  97. package/dist_ts_typedrequest/catalog.js +1 -1
  98. package/package.json +3 -3
  99. package/ts/00_commitinfo_data.ts +1 -1
  100. package/ts/classes.flexharness.admission.ts +331 -0
  101. package/ts/classes.flexharness.callbacks.ts +352 -0
  102. package/ts/classes.flexharness.compaction.ts +167 -0
  103. package/ts/classes.flexharness.configurations.ts +167 -0
  104. package/ts/classes.flexharness.core.ts +1830 -0
  105. package/ts/classes.flexharness.deletion.ts +616 -0
  106. package/ts/classes.flexharness.imports.ts +414 -0
  107. package/ts/classes.flexharness.lifecycle.ts +786 -0
  108. package/ts/classes.flexharness.permissions.ts +535 -0
  109. package/ts/classes.flexharness.projects.ts +1006 -0
  110. package/ts/classes.flexharness.prompts.ts +857 -0
  111. package/ts/classes.flexharness.providers.ts +322 -0
  112. package/ts/classes.flexharness.results.ts +98 -0
  113. package/ts/classes.flexharness.reversions.ts +1684 -0
  114. package/ts/classes.flexharness.runs.ts +1204 -0
  115. package/ts/classes.flexharness.sessions.ts +483 -0
  116. package/ts/classes.flexharness.slashcommands.ts +705 -0
  117. package/ts/classes.flexharness.subagents.ts +860 -0
  118. package/ts/classes.flexharness.tooldispatch.ts +475 -0
  119. package/ts/classes.flexharness.ts +147 -12567
  120. package/ts/interfaces.flexharness.ts +517 -0
  121. package/ts/utils.flexharness.ts +121 -0
  122. package/ts_chat_harness_server/classes.chathub.ts +68 -61
  123. package/ts_chat_harness_server/classes.chatsubscription.ts +7 -5
  124. package/ts_chat_harness_server/classes.scopedirectory.ts +67 -0
  125. package/ts_chat_harness_server/classes.sessionlists.ts +11 -19
  126. package/ts_chat_harness_server/classes.turngate.ts +28 -21
  127. package/ts_chat_harness_server/index.ts +4 -5
  128. package/ts_chat_harness_server/interfaces.ts +35 -2
  129. package/ts_chat_harness_server/plugins.ts +0 -3
  130. package/ts_chat_harness_server/readme.md +6 -4
  131. package/ts_chat_harness_typedsocket/classes.clienttransport.ts +92 -23
  132. package/ts_chat_harness_typedsocket/classes.serverbinding.ts +88 -16
  133. package/ts_chat_harness_typedsocket/index.ts +4 -2
  134. package/ts_chat_harness_typedsocket/interfaces.requests.ts +15 -11
  135. 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. `stop()` stops the turns the hub started, its
60
- * subscriptions and its timers; the harness stays the host's.
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, flexHarnessChatSessionMeta);
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
- const sessions = await this.call(() => this.harness.listSessions(callerArg.scopeId));
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 session = await this.requireSession(callerArg, requestArg.sessionId);
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: callerArg.scopeId,
129
- sessionId: session.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: flexHarnessChatSessionMeta(session), readOnly: session.origin?.kind === 'import' };
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 sessionId: string;
171
+ let conversation: IFlexHarnessChatConversation;
174
172
  let created = false;
175
173
  if (requestArg.sessionId === undefined) {
176
- sessionId = (await this.call(() => this.harness.createSession(callerArg.scopeId, { title: this.titleOf(send) }))).sessionId;
174
+ conversation = await this.call(() => this.directory.create(callerArg, { title: this.titleOf(send) }));
177
175
  created = true;
178
176
  } else {
179
- const session = await this.requireSession(callerArg, requestArg.sessionId);
180
- if (session.origin?.kind === 'import') throw new FlexHarnessChatRequestError(READ_ONLY_MESSAGE);
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, sessionId, send, context, deadlineArg);
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.harness.deleteSession(callerArg.scopeId, sessionId).catch(() => undefined);
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 session = await this.requireSession(callerArg, requestArg?.sessionId);
201
- await this.call(() => this.harness.abort(callerArg.scopeId, session.sessionId));
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 session = await this.requireSession(callerArg, requestArg?.sessionId);
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
- callerArg.scopeId,
215
- session.sessionId,
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 session = await this.requireSession(callerArg, requestArg?.sessionId);
223
+ const conversation = await this.requireConversation(callerArg, requestArg?.sessionId);
227
224
  if (requestArg.direction === 'undo') {
228
- const result = await this.call(() => this.harness.undoSession(callerArg.scopeId, session.sessionId));
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(callerArg.scopeId, session.sessionId));
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 session = await this.requireSession(callerArg, requestArg.sessionId);
244
- const updated = await this.call(() => this.harness.updateSession(callerArg.scopeId, session.sessionId, { title }));
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 session = await this.requireSession(callerArg, requestArg?.sessionId);
253
- await this.call(() => this.harness.deleteSession(callerArg.scopeId, session.sessionId));
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
- /** The running turn of a conversation; the host's model resolver and tool provider read it. */
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({ tabId: turnArg.tabId, scopeId: turnArg.scopeId });
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 (!eventArg.session.parentSessionId) this.sessionLists.publish(eventArg);
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), { tabId: tab.tabId, scopeId: tab.scopeId });
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 root session of the caller's scope; another scope's session id reads as not found. */
370
- private async requireSession(callerArg: IFlexHarnessChatCaller, sessionIdArg: unknown): Promise<plugins.IFlexSession> {
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
- let session: plugins.IFlexSession;
373
- try {
374
- session = await this.harness.getSession(callerArg.scopeId, sessionId);
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
- tabId: string;
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 tabId: string;
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.tabId = optionsArg.tabId;
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({ tabId: this.tabId, scopeId: this.scopeId });
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
- public publish(eventArg: TSessionEvent): void {
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 !== eventArg.scopeId) continue;
60
- if (eventArg.type === 'session.deleted') {
61
- entry.upserts.delete(eventArg.sessionId);
62
- entry.deletedIds.add(eventArg.sessionId);
63
- } else {
64
- entry.deletedIds.delete(eventArg.sessionId);
65
- entry.upserts.set(eventArg.sessionId, this.toSessionMeta(eventArg.session));
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, tabId: binding.tabId, context: binding.context }
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.tabId === tabArg.tabId && binding.scopeId === tabArg.scopeId) return true;
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
- sessionIdArg: string,
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 admission = this.withLock(bindingKey(callerArg.scopeId, sessionIdArg), () =>
94
- this.admitLocked(callerArg, sessionIdArg, sendArg, contextArg, deadlineArg),
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
- sessionIdArg: string,
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: sessionIdArg, steerId: intent.steerId };
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, sessionIdArg, sendArg, contextArg);
190
+ return this.startTurn(callerArg, target, sendArg, contextArg);
185
191
  }
186
192
 
187
193
  private async startTurn(
188
194
  callerArg: IFlexHarnessChatCaller,
189
- sessionIdArg: string,
195
+ target: ITarget,
190
196
  sendArg: plugins.IHarnessComposerSendDetail,
191
197
  contextArg: TContext,
192
198
  ): Promise<plugins.TFlexHarnessChatResponse<'flexHarnessChatSend'>> {
193
- const key = bindingKey(callerArg.scopeId, sessionIdArg);
194
- const intent = intentOf({ scopeId: callerArg.scopeId, sessionId: sessionIdArg }, { ...sendArg, delivery: 'turn' });
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: callerArg.scopeId,
200
- sessionId: sessionIdArg,
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(callerArg.scopeId, sessionIdArg, intent.prompt);
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(callerArg.scopeId, sessionIdArg, queued.queueId);
234
+ this.cancel(scopeId, sessionId, queued.queueId);
228
235
  throw new FlexHarnessChatRequestError(STOPPED_MESSAGE);
229
236
  }
230
- binding.timer = setTimeout(() => this.cancel(callerArg.scopeId, sessionIdArg, queued.queueId), this.limits.turnTimeoutMs);
237
+ binding.timer = setTimeout(() => this.cancel(scopeId, sessionId, queued.queueId), this.limits.turnTimeoutMs);
231
238
  binding.timer.unref?.();
232
- return { sessionId: sessionIdArg, queueId: queued.queueId };
239
+ return { sessionId, queueId: queued.queueId };
233
240
  }
234
241
  }
235
242
 
@@ -1,8 +1,5 @@
1
- export {
2
- FlexHarnessChatHub,
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,