@modelprofile.com/flexharness 10.17.0 → 10.18.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 (104) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/errors.d.ts +4 -0
  3. package/dist_ts/errors.js +9 -1
  4. package/dist_ts/functions.toolpermissions.d.ts +24 -0
  5. package/dist_ts/functions.toolpermissions.js +38 -0
  6. package/dist_ts/index.d.ts +1 -0
  7. package/dist_ts/index.js +2 -1
  8. package/dist_ts/plugins.d.ts +1 -1
  9. package/dist_ts/plugins.js +1 -1
  10. package/dist_ts_chat_harness/classes.chatclient.d.ts +95 -0
  11. package/dist_ts_chat_harness/classes.chatclient.js +263 -0
  12. package/dist_ts_chat_harness/classes.chatrequesterror.d.ts +4 -0
  13. package/dist_ts_chat_harness/classes.chatrequesterror.js +8 -0
  14. package/dist_ts_chat_harness/classes.transcript.d.ts +1 -1
  15. package/dist_ts_chat_harness/classes.transcript.js +1 -1
  16. package/dist_ts_chat_harness/functions.intents.d.ts +1 -2
  17. package/dist_ts_chat_harness/functions.intents.js +1 -1
  18. package/dist_ts_chat_harness/functions.mapping.d.ts +1 -1
  19. package/dist_ts_chat_harness/functions.mapping.js +1 -1
  20. package/dist_ts_chat_harness/index.d.ts +3 -0
  21. package/dist_ts_chat_harness/index.js +3 -1
  22. package/dist_ts_chat_harness/interfaces.chat.d.ts +136 -0
  23. package/dist_ts_chat_harness/interfaces.chat.js +2 -0
  24. package/dist_ts_chat_harness/plugins.js +1 -1
  25. package/dist_ts_chat_harness_server/classes.chathub.d.ts +78 -0
  26. package/dist_ts_chat_harness_server/classes.chathub.js +332 -0
  27. package/dist_ts_chat_harness_server/classes.chatsubscription.d.ts +55 -0
  28. package/dist_ts_chat_harness_server/classes.chatsubscription.js +236 -0
  29. package/dist_ts_chat_harness_server/classes.sessionlists.d.ts +21 -0
  30. package/dist_ts_chat_harness_server/classes.sessionlists.js +70 -0
  31. package/dist_ts_chat_harness_server/classes.turngate.d.ts +38 -0
  32. package/dist_ts_chat_harness_server/classes.turngate.js +207 -0
  33. package/dist_ts_chat_harness_server/errors.d.ts +5 -0
  34. package/dist_ts_chat_harness_server/errors.js +9 -0
  35. package/dist_ts_chat_harness_server/functions.errorprojector.d.ts +11 -0
  36. package/dist_ts_chat_harness_server/functions.errorprojector.js +32 -0
  37. package/dist_ts_chat_harness_server/functions.validation.d.ts +18 -0
  38. package/dist_ts_chat_harness_server/functions.validation.js +88 -0
  39. package/dist_ts_chat_harness_server/index.d.ts +4 -0
  40. package/dist_ts_chat_harness_server/index.js +4 -0
  41. package/dist_ts_chat_harness_server/interfaces.d.ts +67 -0
  42. package/dist_ts_chat_harness_server/interfaces.js +2 -0
  43. package/dist_ts_chat_harness_server/plugins.d.ts +7 -0
  44. package/dist_ts_chat_harness_server/plugins.js +6 -0
  45. package/dist_ts_chat_harness_typedsocket/classes.clienttransport.d.ts +22 -0
  46. package/dist_ts_chat_harness_typedsocket/classes.clienttransport.js +55 -0
  47. package/dist_ts_chat_harness_typedsocket/classes.serverbinding.d.ts +46 -0
  48. package/dist_ts_chat_harness_typedsocket/classes.serverbinding.js +110 -0
  49. package/dist_ts_chat_harness_typedsocket/index.d.ts +3 -0
  50. package/dist_ts_chat_harness_typedsocket/index.js +3 -0
  51. package/dist_ts_chat_harness_typedsocket/interfaces.requests.d.ts +38 -0
  52. package/dist_ts_chat_harness_typedsocket/interfaces.requests.js +2 -0
  53. package/dist_ts_chat_harness_typedsocket/plugins.d.ts +9 -0
  54. package/dist_ts_chat_harness_typedsocket/plugins.js +8 -0
  55. package/dist_ts_tools/tool.filesystem.d.ts +2 -0
  56. package/dist_ts_tools/tool.filesystem.js +42 -1
  57. package/dist_ts_tools_node/classes.workspacereversion.d.ts +48 -0
  58. package/dist_ts_tools_node/classes.workspacereversion.js +202 -0
  59. package/dist_ts_tools_node/functions.fileimages.d.ts +22 -0
  60. package/dist_ts_tools_node/functions.fileimages.js +87 -0
  61. package/dist_ts_tools_node/index.d.ts +3 -1
  62. package/dist_ts_tools_node/index.js +2 -1
  63. package/dist_ts_tools_node/plugins.d.ts +1 -0
  64. package/dist_ts_tools_node/tool.context.d.ts +20 -0
  65. package/dist_ts_tools_node/tool.context.js +25 -4
  66. package/package.json +28 -4
  67. package/ts/00_commitinfo_data.ts +1 -1
  68. package/ts/errors.ts +10 -0
  69. package/ts/functions.toolpermissions.ts +60 -0
  70. package/ts/index.ts +6 -0
  71. package/ts/plugins.ts +1 -0
  72. package/ts_chat_harness/classes.chatclient.ts +343 -0
  73. package/ts_chat_harness/classes.chatrequesterror.ts +7 -0
  74. package/ts_chat_harness/classes.transcript.ts +1 -1
  75. package/ts_chat_harness/functions.intents.ts +2 -2
  76. package/ts_chat_harness/functions.mapping.ts +1 -1
  77. package/ts_chat_harness/index.ts +20 -0
  78. package/ts_chat_harness/interfaces.chat.ts +108 -0
  79. package/ts_chat_harness/readme.md +24 -0
  80. package/ts_chat_harness_server/classes.chathub.ts +400 -0
  81. package/ts_chat_harness_server/classes.chatsubscription.ts +257 -0
  82. package/ts_chat_harness_server/classes.sessionlists.ts +90 -0
  83. package/ts_chat_harness_server/classes.turngate.ts +247 -0
  84. package/ts_chat_harness_server/errors.ts +9 -0
  85. package/ts_chat_harness_server/functions.errorprojector.ts +40 -0
  86. package/ts_chat_harness_server/functions.validation.ts +104 -0
  87. package/ts_chat_harness_server/index.ts +19 -0
  88. package/ts_chat_harness_server/interfaces.ts +92 -0
  89. package/ts_chat_harness_server/plugins.ts +62 -0
  90. package/ts_chat_harness_server/readme.md +91 -0
  91. package/ts_chat_harness_typedsocket/classes.clienttransport.ts +87 -0
  92. package/ts_chat_harness_typedsocket/classes.serverbinding.ts +172 -0
  93. package/ts_chat_harness_typedsocket/index.ts +17 -0
  94. package/ts_chat_harness_typedsocket/interfaces.requests.ts +40 -0
  95. package/ts_chat_harness_typedsocket/plugins.ts +28 -0
  96. package/ts_chat_harness_typedsocket/readme.md +57 -0
  97. package/ts_tools/readme.md +10 -0
  98. package/ts_tools/tool.filesystem.ts +55 -0
  99. package/ts_tools_node/classes.workspacereversion.ts +255 -0
  100. package/ts_tools_node/functions.fileimages.ts +89 -0
  101. package/ts_tools_node/index.ts +7 -1
  102. package/ts_tools_node/plugins.ts +11 -0
  103. package/ts_tools_node/readme.md +27 -0
  104. package/ts_tools_node/tool.context.ts +49 -3
@@ -0,0 +1,343 @@
1
+ import type * as plugins from './plugins.js';
2
+ import type { TJsonValue } from '@modelprofile.com/flexharness';
3
+ import type {
4
+ IFlexHarnessChatHistoryState,
5
+ IFlexHarnessChatSessionsPush,
6
+ IFlexHarnessChatSteerOutcome,
7
+ IFlexHarnessChatToolRequest,
8
+ IFlexHarnessChatUpdate,
9
+ TFlexHarnessChatMethod,
10
+ TFlexHarnessChatRequest,
11
+ TFlexHarnessChatResponse,
12
+ } from './interfaces.chat.js';
13
+
14
+ /** Where a `FlexHarnessChatClient` hands the hub's pushes. */
15
+ export interface IFlexHarnessChatPushHandlers {
16
+ update(push: IFlexHarnessChatUpdate): void;
17
+ sessions(push: IFlexHarnessChatSessionsPush): void;
18
+ /** Runs a tool of a turn; settles at once when the signal aborts. */
19
+ tool(request: IFlexHarnessChatToolRequest, abortSignal: AbortSignal | undefined): Promise<TJsonValue>;
20
+ }
21
+
22
+ /** The tab's connection to the hub. It names the tab in every request it carries. */
23
+ export interface IFlexHarnessChatClientTransport {
24
+ request<TMethod extends TFlexHarnessChatMethod>(
25
+ method: TMethod,
26
+ request: TFlexHarnessChatRequest<TMethod>,
27
+ ): Promise<TFlexHarnessChatResponse<TMethod>>;
28
+ /** Routes the hub's pushes to the handlers until the returned function is called. */
29
+ listen(handlers: IFlexHarnessChatPushHandlers): () => void;
30
+ /** Calls back whenever the connection was lost and is restored, until the returned function is called. */
31
+ onReconnect(callback: () => void): () => void;
32
+ }
33
+
34
+ /** What the chat view learns; every member is optional. */
35
+ export interface IFlexHarnessChatClientListener {
36
+ /** the tab (re)attached; its conversations, newest first */
37
+ attached?(sessions: plugins.IHarnessSessionMeta[]): void;
38
+ sessions?(sessions: plugins.IHarnessSessionMeta[]): void;
39
+ /** the rows of the open conversation, in order; deltas mutate these objects */
40
+ rows?(rows: plugins.IHarnessMessage[]): void;
41
+ delta?(delta: plugins.THarnessStreamDelta): void;
42
+ status?(status: plugins.IHarnessStatus): void;
43
+ usage?(usage: plugins.IHarnessUsage): void;
44
+ permissions?(permissions: plugins.IHarnessPermissionRequest[]): void;
45
+ history?(history: IFlexHarnessChatHistoryState): void;
46
+ steer?(outcome: IFlexHarnessChatSteerOutcome): void;
47
+ /** the open conversation could not be followed again and was closed; the message says why */
48
+ closed?(message: string): void;
49
+ /** attaching again after a reconnect failed; the message says why */
50
+ error?(message: string): void;
51
+ }
52
+
53
+ /** Runs one tool of a turn in this tab and answers with what the model reads. */
54
+ export type TFlexHarnessChatToolExecutor = (
55
+ input: TJsonValue,
56
+ context: { toolCallId: string; abortSignal?: AbortSignal },
57
+ ) => Promise<TJsonValue>;
58
+
59
+ export interface IFlexHarnessChatClientOptions {
60
+ transport: IFlexHarnessChatClientTransport;
61
+ }
62
+
63
+ interface IOpenConversation {
64
+ sessionId: string;
65
+ subscriptionId: string;
66
+ /** the seq of the next update */
67
+ nextSeq: number;
68
+ rows: Map<string, plugins.IHarnessMessage>;
69
+ order: string[];
70
+ }
71
+
72
+ const errorText = (errorArg: unknown, fallbackArg: string): string =>
73
+ (errorArg instanceof Error && errorArg.message) || fallbackArg;
74
+
75
+ /** 128 random bits as hex; `getRandomValues` works on plain http too, unlike `randomUUID`. */
76
+ const randomId = (): string =>
77
+ Array.from(globalThis.crypto.getRandomValues(new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, '0')).join('');
78
+
79
+ /**
80
+ * A tab's side of a `FlexHarnessChatHub`: attaches the tab, follows one conversation through
81
+ * numbered updates and opens it again after a gap or a reconnect, sends what the composer emits,
82
+ * and runs the tools of the tab's turns, one after another. `dispose()` releases the transport.
83
+ */
84
+ export class FlexHarnessChatClient {
85
+ private readonly transport: IFlexHarnessChatClientTransport;
86
+ private readonly tools = new Map<string, TFlexHarnessChatToolExecutor>();
87
+ private readonly stopListening: () => void;
88
+ private readonly stopWatchingReconnects: () => void;
89
+ private listener: IFlexHarnessChatClientListener | null = null;
90
+ private sessionsById = new Map<string, plugins.IHarnessSessionMeta>();
91
+ private open: IOpenConversation | null = null;
92
+ /** counts opens and closes, so a failed reopen closes only what nothing replaced meanwhile */
93
+ private openEpoch = 0;
94
+ /** tools run one after another, so a tool never races the one the model called before it */
95
+ private toolQueue: Promise<unknown> = Promise.resolve();
96
+ private disposed = false;
97
+
98
+ constructor(optionsArg: IFlexHarnessChatClientOptions) {
99
+ this.transport = optionsArg.transport;
100
+ this.stopListening = this.transport.listen({
101
+ update: (pushArg) => this.applyUpdate(pushArg),
102
+ sessions: (pushArg) => this.applySessions(pushArg),
103
+ tool: (requestArg, abortSignalArg) => this.runTool(requestArg, abortSignalArg),
104
+ });
105
+ this.stopWatchingReconnects = this.transport.onReconnect(() => {
106
+ void this.attach().catch((error) =>
107
+ this.listener?.error?.(errorText(error, 'The conversation could not be reattached after a reconnect.')),
108
+ );
109
+ });
110
+ }
111
+
112
+ public setListener(listenerArg: IFlexHarnessChatClientListener | null): void {
113
+ this.listener = listenerArg;
114
+ }
115
+
116
+ /** Runs the named tool when a turn of this tab calls it, until the returned function is called. */
117
+ public registerTool(nameArg: string, executorArg: TFlexHarnessChatToolExecutor): () => void {
118
+ this.tools.set(nameArg, executorArg);
119
+ return () => {
120
+ if (this.tools.get(nameArg) === executorArg) this.tools.delete(nameArg);
121
+ };
122
+ }
123
+
124
+ /** The conversation the tab follows, if any. */
125
+ public get sessionId(): string | null {
126
+ return this.open?.sessionId ?? null;
127
+ }
128
+
129
+ /** Attaches the tab, and follows its open conversation again. */
130
+ public async attach(): Promise<plugins.IHarnessSessionMeta[]> {
131
+ const result = await this.transport.request('flexHarnessChatAttach', {});
132
+ this.sessionsById = new Map(result.sessions.map((session) => [session.id, session]));
133
+ // attaching ends the hub's subscription of this tab
134
+ const reopen = this.open?.sessionId;
135
+ this.open = null;
136
+ this.listener?.attached?.(result.sessions);
137
+ if (reopen && this.sessionsById.has(reopen)) await this.reopen(reopen);
138
+ else if (reopen) this.clear();
139
+ return result.sessions;
140
+ }
141
+
142
+ /** Follows a conversation; its transcript arrives as updates. */
143
+ public async openSession(sessionIdArg: string): Promise<TFlexHarnessChatResponse<'flexHarnessChatOpen'>> {
144
+ this.openEpoch++;
145
+ const open: IOpenConversation = {
146
+ sessionId: sessionIdArg,
147
+ subscriptionId: randomId(),
148
+ nextSeq: 1,
149
+ rows: new Map(),
150
+ order: [],
151
+ };
152
+ // registered before the request: updates can overtake its answer
153
+ this.open = open;
154
+ try {
155
+ return await this.transport.request('flexHarnessChatOpen', {
156
+ sessionId: sessionIdArg,
157
+ subscriptionId: open.subscriptionId,
158
+ });
159
+ } catch (error) {
160
+ if (this.open === open) this.open = null;
161
+ throw error;
162
+ }
163
+ }
164
+
165
+ /** Stops following the open conversation; the next message starts a new one. */
166
+ public async newConversation(): Promise<void> {
167
+ const open = this.open;
168
+ this.clear();
169
+ if (open) await this.transport.request('flexHarnessChatClose', { subscriptionId: open.subscriptionId });
170
+ }
171
+
172
+ /** Sends a message to the open conversation, or starts a new one with it. */
173
+ public async send(
174
+ detailArg: plugins.IHarnessComposerSendDetail,
175
+ contextArg: TJsonValue,
176
+ ): Promise<TFlexHarnessChatResponse<'flexHarnessChatSend'>> {
177
+ const sessionId = this.open?.sessionId;
178
+ const result = await this.transport.request('flexHarnessChatSend', {
179
+ ...(sessionId ? { sessionId } : {}),
180
+ send: { delivery: detailArg.delivery, text: detailArg.text, attachments: detailArg.attachments },
181
+ context: contextArg,
182
+ });
183
+ if (result.sessionId !== this.open?.sessionId) await this.openSession(result.sessionId);
184
+ return result;
185
+ }
186
+
187
+ public async abort(): Promise<void> {
188
+ const sessionId = this.open?.sessionId;
189
+ if (sessionId) await this.transport.request('flexHarnessChatAbort', { sessionId });
190
+ }
191
+
192
+ /** Answers a permission card of the open conversation (`harness-permission-response`). */
193
+ public async respondToPermission(detailArg: plugins.IHarnessPermissionResponseDetail): Promise<void> {
194
+ // the card reports a remembered approval as `remember`, which FlexHarness calls `always`
195
+ await this.transport.request('flexHarnessChatPermission', {
196
+ sessionId: this.requireOpenSessionId(),
197
+ permissionId: detailArg.requestId,
198
+ decision: detailArg.response === 'reject' ? 'reject' : detailArg.remember ? 'always' : 'once',
199
+ });
200
+ }
201
+
202
+ /** Undoes the open conversation's last turn; the conversation is shown again from a reset. */
203
+ public async undo(): Promise<string> {
204
+ return (await this.transport.request('flexHarnessChatHistory', {
205
+ sessionId: this.requireOpenSessionId(),
206
+ direction: 'undo',
207
+ })).runId;
208
+ }
209
+
210
+ public async redo(): Promise<string> {
211
+ return (await this.transport.request('flexHarnessChatHistory', {
212
+ sessionId: this.requireOpenSessionId(),
213
+ direction: 'redo',
214
+ })).runId;
215
+ }
216
+
217
+ public async renameSession(sessionIdArg: string, titleArg: string): Promise<void> {
218
+ const result = await this.transport.request('flexHarnessChatSessionUpdate', { sessionId: sessionIdArg, title: titleArg });
219
+ this.sessionsById.set(result.session.id, result.session);
220
+ this.listener?.sessions?.(this.sessionList());
221
+ }
222
+
223
+ public async deleteSession(sessionIdArg: string): Promise<void> {
224
+ await this.transport.request('flexHarnessChatSessionDelete', { sessionId: sessionIdArg });
225
+ this.sessionsById.delete(sessionIdArg);
226
+ this.listener?.sessions?.(this.sessionList());
227
+ if (this.open?.sessionId === sessionIdArg) this.clear();
228
+ }
229
+
230
+ /** Stops taking pushes and reconnects; the tab's tools are no longer run. */
231
+ public dispose(): void {
232
+ if (this.disposed) return;
233
+ this.disposed = true;
234
+ this.stopListening();
235
+ this.stopWatchingReconnects();
236
+ this.tools.clear();
237
+ this.listener = null;
238
+ this.open = null;
239
+ }
240
+
241
+ private requireOpenSessionId(): string {
242
+ const sessionId = this.open?.sessionId;
243
+ if (!sessionId) throw new Error('No conversation is open.');
244
+ return sessionId;
245
+ }
246
+
247
+ private sessionList(): plugins.IHarnessSessionMeta[] {
248
+ return [...this.sessionsById.values()].sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0));
249
+ }
250
+
251
+ private clear(): void {
252
+ this.openEpoch++;
253
+ this.open = null;
254
+ this.listener?.rows?.([]);
255
+ this.listener?.permissions?.([]);
256
+ this.listener?.status?.({ type: 'idle' });
257
+ this.listener?.usage?.({});
258
+ }
259
+
260
+ /** Follows the conversation again; when that fails, closes it and says why. */
261
+ private async reopen(sessionIdArg: string): Promise<void> {
262
+ const reopening = this.openSession(sessionIdArg);
263
+ const epoch = this.openEpoch;
264
+ try {
265
+ await reopening;
266
+ } catch (error) {
267
+ // another conversation was opened or a new one started meanwhile: that one stays
268
+ if (this.openEpoch !== epoch) return;
269
+ this.clear();
270
+ this.listener?.closed?.(errorText(error, 'The conversation could not be opened again.'));
271
+ }
272
+ }
273
+
274
+ private applySessions(pushArg: IFlexHarnessChatSessionsPush): void {
275
+ for (const id of pushArg.deletedIds) this.sessionsById.delete(id);
276
+ for (const session of pushArg.upserts) this.sessionsById.set(session.id, session);
277
+ this.listener?.sessions?.(this.sessionList());
278
+ }
279
+
280
+ private applyUpdate(updateArg: IFlexHarnessChatUpdate): void {
281
+ const open = this.open;
282
+ if (!open || updateArg.subscriptionId !== open.subscriptionId) return;
283
+ if (updateArg.seq !== open.nextSeq) {
284
+ // an update was lost: read the conversation again
285
+ void this.reopen(open.sessionId);
286
+ return;
287
+ }
288
+ open.nextSeq++;
289
+ if (updateArg.reset) open.rows.clear();
290
+ if (updateArg.rows) {
291
+ for (const row of updateArg.rows.upserts) open.rows.set(row.id, row);
292
+ open.order = updateArg.rows.order;
293
+ const kept = new Set(open.order);
294
+ for (const id of [...open.rows.keys()]) {
295
+ if (!kept.has(id)) open.rows.delete(id);
296
+ }
297
+ this.listener?.rows?.(open.order.flatMap((id) => open.rows.get(id) ?? []));
298
+ }
299
+ for (const delta of updateArg.deltas ?? []) this.listener?.delta?.(delta);
300
+ if (updateArg.status) this.listener?.status?.(updateArg.status);
301
+ if (updateArg.usage) this.listener?.usage?.(updateArg.usage);
302
+ if (updateArg.permissions) this.listener?.permissions?.(updateArg.permissions);
303
+ if (updateArg.history) this.listener?.history?.(updateArg.history);
304
+ for (const steer of updateArg.steers ?? []) this.listener?.steer?.(steer);
305
+ }
306
+
307
+ private runTool(requestArg: IFlexHarnessChatToolRequest, abortSignalArg: AbortSignal | undefined): Promise<TJsonValue> {
308
+ const run = this.toolQueue.then(() => {
309
+ abortSignalArg?.throwIfAborted();
310
+ const executor = this.tools.get(requestArg.name);
311
+ if (!executor) throw new Error(`This tab does not run the tool "${requestArg.name}".`);
312
+ return untilAborted(
313
+ executor(requestArg.input, {
314
+ toolCallId: requestArg.toolCallId,
315
+ ...(abortSignalArg ? { abortSignal: abortSignalArg } : {}),
316
+ }),
317
+ abortSignalArg,
318
+ );
319
+ });
320
+ this.toolQueue = run.catch(() => undefined);
321
+ return run;
322
+ }
323
+ }
324
+
325
+ /** The work's result, or a rejection as soon as the signal aborts; the work itself may run on. */
326
+ const untilAborted = <T>(workArg: Promise<T>, signalArg: AbortSignal | undefined): Promise<T> => {
327
+ if (!signalArg) return workArg;
328
+ if (signalArg.aborted) return Promise.reject(signalArg.reason ?? new Error('The tool call was cancelled.'));
329
+ return new Promise<T>((resolve, reject) => {
330
+ const onAbort = () => reject(signalArg.reason ?? new Error('The tool call was cancelled.'));
331
+ signalArg.addEventListener('abort', onAbort, { once: true });
332
+ workArg.then(
333
+ (value) => {
334
+ signalArg.removeEventListener('abort', onAbort);
335
+ resolve(value);
336
+ },
337
+ (error) => {
338
+ signalArg.removeEventListener('abort', onAbort);
339
+ reject(error);
340
+ },
341
+ );
342
+ });
343
+ };
@@ -0,0 +1,7 @@
1
+ /** A chat request the hub refuses; its message is written for the person in the tab. */
2
+ export class FlexHarnessChatRequestError extends Error {
3
+ constructor(message: string) {
4
+ super(message);
5
+ this.name = 'FlexHarnessChatRequestError';
6
+ }
7
+ }
@@ -6,7 +6,7 @@ import type {
6
6
  IFlexSession,
7
7
  TFlexHarnessEvent,
8
8
  TFlexMessagePart,
9
- } from '../ts/interfaces.js';
9
+ } from '@modelprofile.com/flexharness';
10
10
  import {
11
11
  flexMessageToHarnessRows,
12
12
  flexPartRowId,
@@ -1,11 +1,11 @@
1
1
  import type * as plugins from './plugins.js';
2
- import type { FlexHarness } from '../ts/classes.flexharness.js';
3
2
  import type {
3
+ FlexHarness,
4
4
  IFlexPromptOptions,
5
5
  IFlexPromptQueueAdmission,
6
6
  TFlexPermissionDecision,
7
7
  TFlexPromptPart,
8
- } from '../ts/interfaces.js';
8
+ } from '@modelprofile.com/flexharness';
9
9
 
10
10
  /** The session a chat view shows. */
11
11
  export interface IFlexHarnessChatTarget {
@@ -9,7 +9,7 @@ import type {
9
9
  IFlexToolMessagePart,
10
10
  IFlexUsage,
11
11
  TFlexToolPartStatus,
12
- } from '../ts/interfaces.js';
12
+ } from '@modelprofile.com/flexharness';
13
13
 
14
14
  /** When a streamed part started and ended, as the events that carried it stated. */
15
15
  export interface IFlexPartTiming {
@@ -31,3 +31,23 @@ export {
31
31
  type TFlexHarnessChatHost,
32
32
  type TFlexHarnessChatIntent,
33
33
  } from './functions.intents.js';
34
+ export { FlexHarnessChatRequestError } from './classes.chatrequesterror.js';
35
+ export {
36
+ FlexHarnessChatClient,
37
+ type IFlexHarnessChatClientListener,
38
+ type IFlexHarnessChatClientOptions,
39
+ type IFlexHarnessChatClientTransport,
40
+ type IFlexHarnessChatPushHandlers,
41
+ type TFlexHarnessChatToolExecutor,
42
+ } from './classes.chatclient.js';
43
+ export type {
44
+ IFlexHarnessChatHistoryState,
45
+ IFlexHarnessChatMethods,
46
+ IFlexHarnessChatSessionsPush,
47
+ IFlexHarnessChatSteerOutcome,
48
+ IFlexHarnessChatToolRequest,
49
+ IFlexHarnessChatUpdate,
50
+ TFlexHarnessChatMethod,
51
+ TFlexHarnessChatRequest,
52
+ TFlexHarnessChatResponse,
53
+ } from './interfaces.chat.js';
@@ -0,0 +1,108 @@
1
+ import type * as plugins from './plugins.js';
2
+ import type { TFlexPermissionDecision, TJsonValue } from '@modelprofile.com/flexharness';
3
+
4
+ // The contract between a `FlexHarnessChatHub` on the server and a `FlexHarnessChatClient` in a
5
+ // browser tab, as plain data any transport can carry. Requests go from the tab to the hub; pushes
6
+ // go from the hub to the tab. The transport names the tab; requests carry no caller identity.
7
+
8
+ /** What became of a steer: the running turn read it, or it ended without reading it. */
9
+ export interface IFlexHarnessChatSteerOutcome {
10
+ steerId: string;
11
+ outcome: 'applied' | 'unapplied';
12
+ }
13
+
14
+ /** Whether the conversation's last turn can be undone, or an undone one redone. */
15
+ export interface IFlexHarnessChatHistoryState {
16
+ undoAvailable: boolean;
17
+ redoAvailable: boolean;
18
+ }
19
+
20
+ /**
21
+ * Push: what changed in the conversation a tab follows. Apply in `seq` order, the parts in the order
22
+ * listed here: `rows`, then `deltas`, then the rest. A tab that sees a gap in `seq` opens the
23
+ * conversation again, which starts with a `reset`.
24
+ */
25
+ export interface IFlexHarnessChatUpdate {
26
+ subscriptionId: string;
27
+ /** 1 for the first update of a subscription, one more for every later one */
28
+ seq: number;
29
+ /** forget every row: `rows` lists them all */
30
+ reset?: true;
31
+ /** the rows that are new or changed since the last update, and the order of all of them */
32
+ rows?: { upserts: plugins.IHarnessMessage[]; order: string[] };
33
+ /** streamed text and tool progress, for `dees-harness-chat.applyDelta()` */
34
+ deltas?: plugins.THarnessStreamDelta[];
35
+ status?: plugins.IHarnessStatus;
36
+ usage?: plugins.IHarnessUsage;
37
+ /** every pending permission request of the conversation */
38
+ permissions?: plugins.IHarnessPermissionRequest[];
39
+ history?: IFlexHarnessChatHistoryState;
40
+ steers?: IFlexHarnessChatSteerOutcome[];
41
+ }
42
+
43
+ /** Push: conversations of the tab's list that were added, changed or deleted. */
44
+ export interface IFlexHarnessChatSessionsPush {
45
+ upserts: plugins.IHarnessSessionMeta[];
46
+ deletedIds: string[];
47
+ }
48
+
49
+ /** Push: run one tool of a turn in the tab that started the turn; the answer is the tool's output. */
50
+ export interface IFlexHarnessChatToolRequest {
51
+ toolCallId: string;
52
+ name: string;
53
+ input: TJsonValue;
54
+ }
55
+
56
+ /** The requests a tab makes, by method name. */
57
+ export interface IFlexHarnessChatMethods {
58
+ /** Attach the tab: lists its conversations, newest first. A tab attaches after every reconnect. */
59
+ flexHarnessChatAttach: {
60
+ request: Record<string, never>;
61
+ response: { sessions: plugins.IHarnessSessionMeta[] };
62
+ };
63
+ /**
64
+ * Follow a conversation. Its state arrives as updates under `subscriptionId`, chosen by the tab
65
+ * (16 random bytes, hex), starting with a `reset`. A tab follows one conversation at a time.
66
+ */
67
+ flexHarnessChatOpen: {
68
+ request: { sessionId: string; subscriptionId: string };
69
+ response: { session: plugins.IHarnessSessionMeta; readOnly: boolean };
70
+ };
71
+ flexHarnessChatClose: {
72
+ request: { subscriptionId: string };
73
+ response: Record<string, never>;
74
+ };
75
+ /**
76
+ * Send a message; without `sessionId` it starts a conversation. While a turn runs, the message
77
+ * steers it. The answer comes once the message is admitted.
78
+ */
79
+ flexHarnessChatSend: {
80
+ request: { sessionId?: string; send: plugins.IHarnessComposerSendDetail; context: TJsonValue };
81
+ response: { sessionId: string; queueId?: string; steerId?: string };
82
+ };
83
+ flexHarnessChatAbort: {
84
+ request: { sessionId: string };
85
+ response: Record<string, never>;
86
+ };
87
+ flexHarnessChatPermission: {
88
+ request: { sessionId: string; permissionId: string; decision: TFlexPermissionDecision };
89
+ response: Record<string, never>;
90
+ };
91
+ /** Undo the conversation's last turn or redo the last undone one, files included when captured. */
92
+ flexHarnessChatHistory: {
93
+ request: { sessionId: string; direction: 'undo' | 'redo' };
94
+ response: { runId: string };
95
+ };
96
+ flexHarnessChatSessionUpdate: {
97
+ request: { sessionId: string; title: string };
98
+ response: { session: plugins.IHarnessSessionMeta };
99
+ };
100
+ flexHarnessChatSessionDelete: {
101
+ request: { sessionId: string };
102
+ response: Record<string, never>;
103
+ };
104
+ }
105
+
106
+ export type TFlexHarnessChatMethod = keyof IFlexHarnessChatMethods;
107
+ export type TFlexHarnessChatRequest<TMethod extends TFlexHarnessChatMethod> = IFlexHarnessChatMethods[TMethod]['request'];
108
+ export type TFlexHarnessChatResponse<TMethod extends TFlexHarnessChatMethod> = IFlexHarnessChatMethods[TMethod]['response'];
@@ -93,6 +93,30 @@ Intents are untrusted client input: a client can name any `scopeId` and `session
93
93
  - `harness-permission-response`: `reject` rejects; an approval with `remember` is `always`, otherwise `once`. A request with `requiresExplicitResponse` refuses `always`.
94
94
  - `harness-abort`: `abort()` of the session.
95
95
 
96
+ ## Chat client
97
+
98
+ `FlexHarnessChatClient` is a tab's side of a `FlexHarnessChatHub` (`/chat/harness/server`), over any `IFlexHarnessChatClientTransport`; `/chat/harness/typedsocket` provides one. The wire contract is `IFlexHarnessChatMethods` (requests by method name), `IFlexHarnessChatUpdate`, `IFlexHarnessChatSessionsPush` and `IFlexHarnessChatToolRequest`, plain data any transport can carry.
99
+
100
+ ```typescript
101
+ const client = new FlexHarnessChatClient({ transport });
102
+ client.setListener({
103
+ attached: (sessions) => (sessionList.sessions = sessions),
104
+ sessions: (sessions) => (sessionList.sessions = sessions),
105
+ rows: (rows) => (chat.messages = rows),
106
+ delta: (delta) => chat.applyDelta(delta),
107
+ status: (status) => (chat.status = status),
108
+ usage: (usage) => (chat.usage = usage),
109
+ permissions: (permissions) => (chat.permissions = permissions),
110
+ });
111
+ client.registerTool('snapshot', async (input, { abortSignal }) => takeSnapshot(input, abortSignal));
112
+ await client.attach();
113
+ chat.addEventListener('harness-send', (event) => client.send(event.detail, currentContext()));
114
+ chat.addEventListener('harness-permission-response', (event) => client.respondToPermission(event.detail));
115
+ chat.addEventListener('harness-abort', () => client.abort());
116
+ ```
117
+
118
+ The client follows one conversation. Updates apply in `seq` order; a gap opens the conversation again from a reset, and a restored connection attaches again and reopens it. `send()` without an open conversation starts one and follows it. A message sent while a turn runs steers it, so the chat offers `busyDeliveryModes: ['steer']`. Tools of the tab's turns run one after another, so a tool never races the one the model called before it; a cancelled call settles at once. `undo()` and `redo()` move the open conversation's history, files included when the host captures them. `dispose()` stops listening to the transport and running tools.
119
+
96
120
  ## Issue Reporting and Security
97
121
 
98
122
  Report issues and security concerns through [community.foss.global](https://community.foss.global).