@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
@@ -2,7 +2,8 @@ import type * as plugins from './plugins.js';
2
2
 
3
3
  /**
4
4
  * Who a request is from, as the host established it: the tab, and the scope whose conversations it
5
- * may reach. A tab is named by both: the same tab id in two scopes names two tabs.
5
+ * may reach. A tab is named by both: the same tab id in two scopes names two tabs. The scope names
6
+ * the tab's reach; with a directory, the conversations it reaches may run in scopes of their own.
6
7
  */
7
8
  export interface IFlexHarnessChatCaller {
8
9
  /** the browser tab, a per-page secret; pushes and tool calls of its turns go to it */
@@ -29,12 +30,42 @@ export interface IFlexHarnessChatPeers {
29
30
 
30
31
  /** The running turn of a conversation: the tab that started it and the context it was sent with. */
31
32
  export interface IFlexHarnessChatTurn<TContext> {
33
+ /** the harness scope of the conversation */
32
34
  readonly scopeId: string;
33
35
  readonly sessionId: string;
34
- readonly tabId: string;
36
+ /** the tab that started the turn; its tool calls go to it */
37
+ readonly tab: IFlexHarnessChatCaller;
35
38
  readonly context: TContext;
36
39
  }
37
40
 
41
+ /** A conversation a tab reaches: where its session runs, and how its list shows it. */
42
+ export interface IFlexHarnessChatConversation {
43
+ /** the harness scope of the conversation's session */
44
+ scopeId: string;
45
+ sessionId: string;
46
+ meta: plugins.IHarnessSessionMeta;
47
+ /** an imported conversation can only be read */
48
+ readOnly: boolean;
49
+ }
50
+
51
+ /**
52
+ * The conversations of the tabs: which a tab lists and reaches, and how they are created, renamed
53
+ * and deleted. Without one, a tab's conversations are the root sessions of its scope. A host whose
54
+ * conversations run in scopes of their own, or that keeps records of its own, gives the hub one and
55
+ * tells the tabs of every change of their lists with `hub.publishSessions()`.
56
+ */
57
+ export interface IFlexHarnessChatDirectory {
58
+ /** The conversations the tab lists, most recent first. */
59
+ list(tab: IFlexHarnessChatCaller): Promise<plugins.IHarnessSessionMeta[]>;
60
+ /** A conversation the tab reaches, or null: one it does not reach reads as not found. */
61
+ find(tab: IFlexHarnessChatCaller, sessionId: string): Promise<IFlexHarnessChatConversation | null>;
62
+ /** Creates a conversation for the tab's first message; throw `FlexHarnessChatRequestError` to refuse. */
63
+ create(tab: IFlexHarnessChatCaller, init: { title: string }): Promise<IFlexHarnessChatConversation>;
64
+ rename(tab: IFlexHarnessChatCaller, conversation: IFlexHarnessChatConversation, title: string): Promise<plugins.IHarnessSessionMeta>;
65
+ /** Deletes a conversation; also one created for a message that was then refused. */
66
+ delete(tab: IFlexHarnessChatCaller, conversation: IFlexHarnessChatConversation): Promise<void>;
67
+ }
68
+
38
69
  export interface IFlexHarnessChatLimits {
39
70
  /** A turn that runs longer is stopped. Default: 10 minutes. */
40
71
  turnTimeoutMs: number;
@@ -88,5 +119,7 @@ export interface IFlexHarnessChatHubOptions<TContext> {
88
119
  readContext(value: plugins.TJsonValue): TContext;
89
120
  /** The title of a conversation a message starts. Defaults to the message's first line. */
90
121
  titleOf?(send: plugins.IHarnessComposerSendDetail): string;
122
+ /** Where the tabs' conversations are. Defaults to the root sessions of the tab's scope. */
123
+ directory?: IFlexHarnessChatDirectory;
91
124
  limits?: Partial<IFlexHarnessChatLimits>;
92
125
  }
@@ -34,9 +34,6 @@ export type {
34
34
  IFlexExternalErrorContext,
35
35
  IFlexPromptQueueAdmission,
36
36
  IFlexSession,
37
- IFlexSessionCreatedEvent,
38
- IFlexSessionDeletedEvent,
39
- IFlexSessionUpdatedEvent,
40
37
  TFlexExternalErrorProjector,
41
38
  TFlexHarnessEvent,
42
39
  TJsonValue,
@@ -42,20 +42,22 @@ const harness = new FlexHarness<IAppScope>({
42
42
  hub = new FlexHarnessChatHub<IEditorContext>({ harness, peers, readContext: parseEditorContext });
43
43
  ```
44
44
 
45
- The host keeps its harness, models, system prompt, tools and scopes. It authenticates every request and calls the hub with the caller it established: the tab and the scope the tab may reach. A conversation of another scope reads as not found. A tab is named by its scope and its id together: the same tab id in two scopes names two tabs, and `IFlexHarnessChatPeers.find()` receives both. Tab ids are per-page secrets all the same; a page never shares its own.
45
+ The host keeps its harness, models, system prompt, tools and scopes. It authenticates every request and calls the hub with the caller it established: the tab and the scope the tab may reach. A conversation the tab does not reach reads as not found. A tab is named by its scope and its id together: the same tab id in two scopes names two tabs, and `IFlexHarnessChatPeers.find()` receives both. Tab ids are per-page secrets all the same; a page never shares its own.
46
46
 
47
47
  ## Conversations
48
48
 
49
- - `attach(caller)` lists the scope's conversations, newest first, and forgets what the tab followed. A tab attaches after loading and after every reconnect; conversations created, renamed or deleted afterwards reach it as `flexHarnessChatSessions` pushes.
49
+ - `attach(caller)` lists the tab's conversations, newest first, and forgets what the tab followed. A tab attaches after loading and after every reconnect; conversations created, renamed or deleted afterwards reach it as list pushes (`IFlexHarnessChatPeer.sessions`, an `IFlexHarnessChatSessionsPush`).
50
50
  - `open(caller, { sessionId, subscriptionId })` follows a conversation. Its state reaches the tab as numbered `IFlexHarnessChatUpdate` pushes, starting with a `reset`: changed rows, streamed deltas, status, usage, pending permissions, undo and redo availability, and what became of steers. A tab follows one conversation at a time. An update the tab does not acknowledge makes the next change a `reset`; a tab that sees a gap in `seq` opens the conversation again.
51
51
  - `send(caller, { sessionId?, send, context })` starts a conversation without `sessionId`. While a turn runs, the message steers it; one the turn no longer takes waits up to `admissionWaitMs` for the ending turn and starts the next one. `readContext` checks the context the message is sent with; the turn carries it. The composer's model, account and effort choices are not used: the host's model resolver decides from the context.
52
- - `abort`, `respondToPermission`, `moveHistory` (FlexHarness undo and redo), `renameSession` and `deleteSession` act on a conversation of the caller's scope. An imported conversation can be read, not continued.
52
+ - `abort`, `respondToPermission`, `moveHistory` (FlexHarness undo and redo), `renameSession` and `deleteSession` act on a conversation the caller reaches. An imported conversation can be read, not continued.
53
+
54
+ Without a directory, a tab's conversations are the root sessions of its scope, and the harness's session events update the tabs' lists. A host that runs each conversation in a scope of its own, or keeps records of its own, passes `directory`, an `IFlexHarnessChatDirectory`: it lists the conversations a tab reaches, finds one by its session id (one the tab does not reach is null, and reads as not found), creates one for a message without `sessionId`, renames and deletes. The caller's `scopeId` is then the tab's reach; each conversation names the harness scope its session runs in, and the hub subscribes, runs turns and calls `turnOf` in that scope. A conversation created for a message that then never starts, for instance because its session does not exist, is deleted again through the directory. With a directory, the harness's session events no longer reach the lists: the host calls `hub.publishSessions(reach, { upserts, deletedIds })` for every change, and the tabs of that reach get it as a list push.
53
55
 
54
56
  Refusals are `FlexHarnessChatRequestError`s with a message for the person in the tab; FlexHarness refusals keep their message. Every other error stays an internal error.
55
57
 
56
58
  ## Turns and tools in the tab
57
59
 
58
- FlexHarness carries no per-prompt context, so the hub binds each turn to the tab that started it and the context it was sent with: `turnOf(scopeId, sessionId)`. A turn stops after `turnTimeoutMs`. `runClientTool(turn, request, options)` sends the call to that tab and answers with its output; it fails with `FlexHarnessChatTabUnavailableError` when the tab is gone or does not answer within `toolTimeoutMs`, and passes on the cancellation of an aborted turn.
60
+ FlexHarness carries no per-prompt context, so the hub binds each turn to the tab that started it and the context it was sent with: `turnOf(scopeId, sessionId)` with the conversation's scope returns `{ scopeId, sessionId, tab, context }`, where `tab` is the caller that sent the message. A turn stops after `turnTimeoutMs`. `runClientTool(turn, request, options)` sends the call to that tab and answers with its output; it fails with `FlexHarnessChatTabUnavailableError` when the tab is gone or does not answer within `toolTimeoutMs`, and passes on the cancellation of an aborted turn.
59
61
 
60
62
  `createFlexHarnessChatErrorProjector()` is an `externalErrorProjector` for chat conversations: the host's own projection first, then a tab that is gone, model limits, the provider's HTTP status, and a fixed message, never a provider's response text.
61
63
 
@@ -1,15 +1,36 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import type {
3
+ IEvent_FlexHarnessChatPending,
4
+ IFlexHarnessChatCallerPayload,
3
5
  IReq_FlexHarnessChat,
4
- IReq_FlexHarnessChatSessions,
6
+ IReq_FlexHarnessChatPull,
5
7
  IReq_FlexHarnessChatTool,
6
- IReq_FlexHarnessChatUpdate,
7
8
  } from './interfaces.requests.js';
8
9
 
10
+ /**
11
+ * What the transport uses of the tab's TypedSocket client. A `TypedSocket` client is one; a host
12
+ * whose own connection object carries the client can hand that object instead.
13
+ */
14
+ export interface IFlexHarnessChatTypedSocketClient {
15
+ createTypedRequest<T extends plugins.typedrequestInterfaces.ITypedRequest>(
16
+ method: T['method'],
17
+ ): { fire(request: T['request']): Promise<T['response']> };
18
+ onEvent<TEvent extends plugins.typedsocket.ITypedSocketEvent>(
19
+ name: TEvent['name'],
20
+ handler: (payload: TEvent['payload']) => void | Promise<void>,
21
+ ): () => void;
22
+ readonly eventResyncSubject: {
23
+ subscribe(observer: (resync: plugins.typedsocket.ITypedSocketEventResync) => void): { unsubscribe(): void };
24
+ };
25
+ readonly statusSubject: {
26
+ subscribe(observer: (status: plugins.typedsocket.TConnectionStatus) => void): { unsubscribe(): void };
27
+ };
28
+ }
29
+
9
30
  export interface IFlexHarnessChatTypedSocketTransportOptions {
10
31
  /** The tab's TypedSocket client. */
11
- typedsocket: plugins.typedsocket.TypedSocket;
12
- /** The router the client was created with; the transport adds its push handlers to it. */
32
+ typedsocket: IFlexHarnessChatTypedSocketClient;
33
+ /** The router the client was created with; the transport adds its tool handler to it. */
13
34
  typedrouter: plugins.typedrequest.TypedRouter;
14
35
  /** The tab: 16 random bytes in hex. Defaults to a new random id for this transport's lifetime. */
15
36
  tabId?: string;
@@ -17,15 +38,24 @@ export interface IFlexHarnessChatTypedSocketTransportOptions {
17
38
  callerToken?: () => string | undefined;
18
39
  }
19
40
 
41
+ const PENDING_EVENT: IEvent_FlexHarnessChatPending['name'] = 'flexHarnessChatPending';
42
+
20
43
  /** 128 random bits as hex; `getRandomValues` works on plain http too, unlike `randomUUID`. */
21
44
  const randomTabId = (): string =>
22
45
  Array.from(globalThis.crypto.getRandomValues(new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, '0')).join('');
23
46
 
24
- /** A `FlexHarnessChatClient` transport over the tab's TypedSocket client. */
47
+ /** A push the tab's handler failed on is reported; the server counts it as delivered all the same. */
48
+ const reportHandlerError = (error: unknown): void => console.warn('FLEXHARNESS_CHAT_PUSH_HANDLER_ERROR', error);
49
+
50
+ /**
51
+ * A `FlexHarnessChatClient` transport over the tab's TypedSocket client. The hub's pushes wait on
52
+ * the server; an event says so, and the transport pulls them, one pull at a time. It also pulls
53
+ * whenever TypedSocket says events may have been missed: after every (re)connect, and after a gap.
54
+ */
25
55
  export class FlexHarnessChatTypedSocketTransport implements plugins.IFlexHarnessChatClientTransport {
26
56
  public readonly tabId: string;
27
57
 
28
- private readonly socket: plugins.typedsocket.TypedSocket;
58
+ private readonly socket: IFlexHarnessChatTypedSocketClient;
29
59
  private readonly router: plugins.typedrequest.TypedRouter;
30
60
  private readonly callerToken: () => string | undefined;
31
61
 
@@ -40,35 +70,69 @@ export class FlexHarnessChatTypedSocketTransport implements plugins.IFlexHarness
40
70
  methodArg: TMethod,
41
71
  requestArg: plugins.TFlexHarnessChatRequest<TMethod>,
42
72
  ): Promise<plugins.TFlexHarnessChatResponse<TMethod>> {
43
- const token = this.callerToken();
44
73
  return this.socket.createTypedRequest<IReq_FlexHarnessChat<TMethod>>(methodArg).fire({
45
74
  ...requestArg,
46
- caller: { tabId: this.tabId, ...(token === undefined ? {} : { token }) },
75
+ caller: this.caller(),
47
76
  });
48
77
  }
49
78
 
50
79
  public listen(handlersArg: plugins.IFlexHarnessChatPushHandlers): () => void {
51
- const pushes = new plugins.typedrequest.TypedRouter();
52
- pushes.addTypedHandler(
53
- new plugins.typedrequest.TypedHandler<IReq_FlexHarnessChatUpdate>('flexHarnessChatUpdate', async (pushArg) => {
54
- handlersArg.update(pushArg);
55
- return {};
56
- }),
57
- );
58
- pushes.addTypedHandler(
59
- new plugins.typedrequest.TypedHandler<IReq_FlexHarnessChatSessions>('flexHarnessChatSessions', async (pushArg) => {
60
- handlersArg.sessions(pushArg);
61
- return {};
62
- }),
63
- );
64
- pushes.addTypedHandler(
80
+ let listening = true;
81
+ let pulling = false;
82
+ let pullAgain = false;
83
+ // one pull at a time; a signal that comes in meanwhile makes it pull once more
84
+ const pull = async (): Promise<void> => {
85
+ if (pulling) {
86
+ pullAgain = true;
87
+ return;
88
+ }
89
+ pulling = true;
90
+ try {
91
+ do {
92
+ pullAgain = false;
93
+ let pushes: IReq_FlexHarnessChatPull['response']['pushes'];
94
+ try {
95
+ ({ pushes } = await this.socket
96
+ .createTypedRequest<IReq_FlexHarnessChatPull>('flexHarnessChatPull')
97
+ .fire({ caller: this.caller() }));
98
+ } catch {
99
+ // the pushes keep waiting for the next pull; one the server gives up on makes the hub start over
100
+ return;
101
+ }
102
+ for (const push of pushes) {
103
+ if (!listening) return;
104
+ // a handler that throws takes nothing from the pushes after it
105
+ try {
106
+ if ('update' in push) handlersArg.update(push.update);
107
+ else handlersArg.sessions(push.sessions);
108
+ } catch (error) {
109
+ reportHandlerError(error);
110
+ }
111
+ }
112
+ } while (pullAgain && listening);
113
+ } finally {
114
+ pulling = false;
115
+ }
116
+ };
117
+ const tools = new plugins.typedrequest.TypedRouter();
118
+ tools.addTypedHandler(
65
119
  new plugins.typedrequest.TypedHandler<IReq_FlexHarnessChatTool>('flexHarnessChatTool', async (requestArg, toolsArg) => ({
66
120
  // the server cancels the request when the turn stops or the call times out; the handler then
67
121
  // settles at once, since TypedSocket fails a connection whose handlers outlive their requests
68
122
  output: await handlersArg.tool(requestArg, toolsArg?.abortSignal),
69
123
  })),
70
124
  );
71
- return this.router.addTypedRouter(pushes);
125
+ const removeTools = this.router.addTypedRouter(tools);
126
+ const removeEvent = this.socket.onEvent<IEvent_FlexHarnessChatPending>(PENDING_EVENT, () => pull());
127
+ const resyncs = this.socket.eventResyncSubject.subscribe((resyncArg) => {
128
+ if (resyncArg.names.includes(PENDING_EVENT)) void pull();
129
+ });
130
+ return () => {
131
+ listening = false;
132
+ resyncs.unsubscribe();
133
+ removeEvent();
134
+ removeTools();
135
+ };
72
136
  }
73
137
 
74
138
  public onReconnect(callbackArg: () => void): () => void {
@@ -84,4 +148,9 @@ export class FlexHarnessChatTypedSocketTransport implements plugins.IFlexHarness
84
148
  });
85
149
  return () => subscription.unsubscribe();
86
150
  }
151
+
152
+ private caller(): IFlexHarnessChatCallerPayload {
153
+ const token = this.callerToken();
154
+ return { tabId: this.tabId, ...(token === undefined ? {} : { token }) };
155
+ }
87
156
  }
@@ -1,10 +1,11 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import type {
3
+ IEvent_FlexHarnessChatPending,
3
4
  IFlexHarnessChatCallerPayload,
4
5
  IReq_FlexHarnessChat,
5
- IReq_FlexHarnessChatSessions,
6
+ IReq_FlexHarnessChatPull,
6
7
  IReq_FlexHarnessChatTool,
7
- IReq_FlexHarnessChatUpdate,
8
+ TFlexHarnessChatPush,
8
9
  } from './interfaces.requests.js';
9
10
 
10
11
  /** The hub requests the binding routes; any `FlexHarnessChatHub` serves them. */
@@ -30,7 +31,7 @@ export interface IFlexHarnessChatTypedSocketBindingOptions {
30
31
  ): Promise<IFlexHarnessChatResolvedCaller>;
31
32
  /** The server-owned connection tag that names a tab. Default: `flexHarnessChatTab`. */
32
33
  tag?: string;
33
- /** How long an update or a session-list push may wait for the tab. Default: 30 seconds. */
34
+ /** How long an update or a session-list push may wait for the tab to pull it. Default: 30 seconds. */
34
35
  pushTimeoutMs?: number;
35
36
  }
36
37
 
@@ -44,23 +45,82 @@ const tagPayload = (tabArg: plugins.IFlexHarnessChatCaller): { scopeId: string;
44
45
  tabId: tabArg.tabId,
45
46
  });
46
47
 
48
+ /** One key per tab and scope, as the hub names tabs. */
49
+ const tabKeyOf = (tabArg: plugins.IFlexHarnessChatCaller): string => `${tabArg.scopeId}\n${tabArg.tabId}`;
50
+
51
+ /** A push waiting for its tab, until the tab pulls it or its time is up. */
52
+ interface IWaitingPush {
53
+ push: TFlexHarnessChatPush;
54
+ taken: () => void;
55
+ timer: ReturnType<typeof setTimeout>;
56
+ }
57
+
58
+ /**
59
+ * The pushes waiting for each tab, by tab and scope. A push resolves once the tab pulled it and
60
+ * fails when the tab does not pull it in time; it never reaches the tab after that.
61
+ */
62
+ class TypedSocketChatMailboxes {
63
+ private readonly waiting = new Map<string, IWaitingPush[]>();
64
+
65
+ constructor(private readonly pushTimeoutMs: number) {}
66
+
67
+ public put(tabArg: plugins.IFlexHarnessChatCaller, pushArg: TFlexHarnessChatPush): Promise<void> {
68
+ const key = tabKeyOf(tabArg);
69
+ return new Promise<void>((resolve, reject) => {
70
+ const entry: IWaitingPush = {
71
+ push: pushArg,
72
+ taken: resolve,
73
+ timer: setTimeout(() => {
74
+ this.remove(key, entry);
75
+ reject(new Error('The tab did not pull a push in time.'));
76
+ }, this.pushTimeoutMs),
77
+ };
78
+ entry.timer.unref?.();
79
+ const mailbox = this.waiting.get(key);
80
+ if (mailbox) mailbox.push(entry);
81
+ else this.waiting.set(key, [entry]);
82
+ });
83
+ }
84
+
85
+ public take(tabArg: plugins.IFlexHarnessChatCaller): TFlexHarnessChatPush[] {
86
+ const key = tabKeyOf(tabArg);
87
+ const mailbox = this.waiting.get(key) ?? [];
88
+ this.waiting.delete(key);
89
+ for (const entry of mailbox) {
90
+ clearTimeout(entry.timer);
91
+ entry.taken();
92
+ }
93
+ return mailbox.map((entry) => entry.push);
94
+ }
95
+
96
+ private remove(keyArg: string, entryArg: IWaitingPush): void {
97
+ const mailbox = this.waiting.get(keyArg);
98
+ if (!mailbox) return;
99
+ const rest = mailbox.filter((entry) => entry !== entryArg);
100
+ if (rest.length) this.waiting.set(keyArg, rest);
101
+ else this.waiting.delete(keyArg);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * One tab, reached over its current connection: a push waits in the tab's mailbox and an event
107
+ * tells the tab to pull it. An event the connection skips or drops is not lost: the tab pulls
108
+ * whenever TypedSocket tells it it may have missed events, after every (re)connect included.
109
+ */
47
110
  class TypedSocketChatPeer implements plugins.IFlexHarnessChatPeer {
48
111
  constructor(
49
112
  private readonly socket: plugins.typedsocket.TypedSocket,
50
113
  private readonly connection: plugins.typedsocket.ISmartServeConnectionWrapper,
51
- private readonly pushTimeoutMs: number,
114
+ private readonly tab: plugins.IFlexHarnessChatCaller,
115
+ private readonly mailboxes: TypedSocketChatMailboxes,
52
116
  ) {}
53
117
 
54
- public async update(pushArg: plugins.IFlexHarnessChatUpdate): Promise<void> {
55
- await this.socket
56
- .createTypedRequest<IReq_FlexHarnessChatUpdate>('flexHarnessChatUpdate', this.connection, { timeoutMs: this.pushTimeoutMs })
57
- .fire(pushArg);
118
+ public update(pushArg: plugins.IFlexHarnessChatUpdate): Promise<void> {
119
+ return this.deliver({ update: pushArg });
58
120
  }
59
121
 
60
- public async sessions(pushArg: plugins.IFlexHarnessChatSessionsPush): Promise<void> {
61
- await this.socket
62
- .createTypedRequest<IReq_FlexHarnessChatSessions>('flexHarnessChatSessions', this.connection, { timeoutMs: this.pushTimeoutMs })
63
- .fire(pushArg);
122
+ public sessions(pushArg: plugins.IFlexHarnessChatSessionsPush): Promise<void> {
123
+ return this.deliver({ sessions: pushArg });
64
124
  }
65
125
 
66
126
  public async runTool(
@@ -72,25 +132,33 @@ class TypedSocketChatPeer implements plugins.IFlexHarnessChatPeer {
72
132
  .fire(requestArg);
73
133
  return answer.output;
74
134
  }
135
+
136
+ private async deliver(pushArg: TFlexHarnessChatPush): Promise<void> {
137
+ // the event goes first: a pull cannot come in between, and an event that throws leaves no push behind
138
+ this.socket.sendEvent<IEvent_FlexHarnessChatPending>(this.connection, 'flexHarnessChatPending', {});
139
+ await this.mailboxes.put(this.tab, pushArg);
140
+ }
75
141
  }
76
142
 
77
143
  /**
78
144
  * Serves a `FlexHarnessChatHub` over TypedSocket. Add `typedrouter` to the server's router, hand
79
145
  * the server's TypedSocket to `setTypedSocket()` once it runs, and give the hub `peers`. A tab's
80
- * connection carries the server-owned tag with its tab id; every request claims it again.
146
+ * connection carries the server-owned tag with its tab id; every request claims it again. The
147
+ * hub's pushes wait for the tab, which an event tells to pull them: TypedSocket events are
148
+ * signals, the pushes themselves travel in the answer of an authenticated request.
81
149
  */
82
150
  export class FlexHarnessChatTypedSocketBinding implements plugins.IFlexHarnessChatPeers {
83
151
  public readonly typedrouter = new plugins.typedrequest.TypedRouter();
84
152
 
85
153
  private readonly resolveCaller: IFlexHarnessChatTypedSocketBindingOptions['resolve'];
86
154
  private readonly tag: string;
87
- private readonly pushTimeoutMs: number;
155
+ private readonly mailboxes: TypedSocketChatMailboxes;
88
156
  private socket: plugins.typedsocket.TypedSocket | null = null;
89
157
 
90
158
  constructor(optionsArg: IFlexHarnessChatTypedSocketBindingOptions) {
91
159
  this.resolveCaller = optionsArg.resolve;
92
160
  this.tag = optionsArg.tag ?? 'flexHarnessChatTab';
93
- this.pushTimeoutMs = optionsArg.pushTimeoutMs ?? 30 * 1000;
161
+ this.mailboxes = new TypedSocketChatMailboxes(optionsArg.pushTimeoutMs ?? 30 * 1000);
94
162
  this.route<'flexHarnessChatAttach'>('flexHarnessChatAttach', (request, tools) =>
95
163
  this.serve(request.caller, tools, (hub, caller) => hub.attach(caller)));
96
164
  this.route<'flexHarnessChatOpen'>('flexHarnessChatOpen', (request, tools) =>
@@ -109,6 +177,10 @@ export class FlexHarnessChatTypedSocketBinding implements plugins.IFlexHarnessCh
109
177
  this.serve(request.caller, tools, (hub, caller) => hub.renameSession(caller, request)));
110
178
  this.route<'flexHarnessChatSessionDelete'>('flexHarnessChatSessionDelete', (request, tools) =>
111
179
  this.serve(request.caller, tools, (hub, caller) => hub.deleteSession(caller, request)));
180
+ this.typedrouter.addTypedHandler(
181
+ new plugins.typedrequest.TypedHandler<IReq_FlexHarnessChatPull>('flexHarnessChatPull', (request, tools) =>
182
+ this.serve(request?.caller, tools, async (_hub, caller) => ({ pushes: this.mailboxes.take(caller) }))),
183
+ );
112
184
  }
113
185
 
114
186
  /** The server's TypedSocket; it exists once the server runs. */
@@ -120,7 +192,7 @@ export class FlexHarnessChatTypedSocketBinding implements plugins.IFlexHarnessCh
120
192
  public async find(tabArg: plugins.IFlexHarnessChatCaller): Promise<plugins.IFlexHarnessChatPeer | null> {
121
193
  if (!this.socket) return null;
122
194
  const connection = await this.socket.findTargetConnectionByTag(this.tag, tagPayload(tabArg));
123
- return connection ? new TypedSocketChatPeer(this.socket, connection, this.pushTimeoutMs) : null;
195
+ return connection ? new TypedSocketChatPeer(this.socket, connection, tagPayload(tabArg), this.mailboxes) : null;
124
196
  }
125
197
 
126
198
  private route<TMethod extends plugins.TFlexHarnessChatMethod>(
@@ -6,12 +6,14 @@ export {
6
6
  } from './classes.serverbinding.js';
7
7
  export {
8
8
  FlexHarnessChatTypedSocketTransport,
9
+ type IFlexHarnessChatTypedSocketClient,
9
10
  type IFlexHarnessChatTypedSocketTransportOptions,
10
11
  } from './classes.clienttransport.js';
11
12
  export type {
13
+ IEvent_FlexHarnessChatPending,
12
14
  IFlexHarnessChatCallerPayload,
13
15
  IReq_FlexHarnessChat,
14
- IReq_FlexHarnessChatSessions,
16
+ IReq_FlexHarnessChatPull,
15
17
  IReq_FlexHarnessChatTool,
16
- IReq_FlexHarnessChatUpdate,
18
+ TFlexHarnessChatPush,
17
19
  } from './interfaces.requests.js';
@@ -4,7 +4,7 @@ import type * as plugins from './plugins.js';
4
4
  export interface IFlexHarnessChatCallerPayload {
5
5
  /**
6
6
  * The tab: 16 random bytes in hex, kept for the tab's lifetime and never shared. Every request
7
- * claims the connection it arrives on for the tab, so pushes and tool calls follow a reconnect.
7
+ * claims the connection it arrives on for the tab, so events and tool calls follow a reconnect.
8
8
  */
9
9
  tabId: string;
10
10
  token?: string;
@@ -18,18 +18,22 @@ export interface IReq_FlexHarnessChat<TMethod extends plugins.TFlexHarnessChatMe
18
18
  response: plugins.TFlexHarnessChatResponse<TMethod>;
19
19
  }
20
20
 
21
- /** Push: what changed in the conversation a tab follows. */
22
- export interface IReq_FlexHarnessChatUpdate extends plugins.typedrequestInterfaces.ITypedRequest {
23
- method: 'flexHarnessChatUpdate';
24
- request: plugins.IFlexHarnessChatUpdate;
25
- response: Record<string, never>;
21
+ /** One push the hub has for a tab: an update of the conversation it follows, or a change of its list. */
22
+ export type TFlexHarnessChatPush =
23
+ | { update: plugins.IFlexHarnessChatUpdate }
24
+ | { sessions: plugins.IFlexHarnessChatSessionsPush };
25
+
26
+ /** Event: the tab has pushes waiting; it takes them with `flexHarnessChatPull`. */
27
+ export interface IEvent_FlexHarnessChatPending extends plugins.typedsocket.ITypedSocketEvent {
28
+ name: 'flexHarnessChatPending';
29
+ payload: Record<string, never>;
26
30
  }
27
31
 
28
- /** Push: changes of the tab's conversation list. */
29
- export interface IReq_FlexHarnessChatSessions extends plugins.typedrequestInterfaces.ITypedRequest {
30
- method: 'flexHarnessChatSessions';
31
- request: plugins.IFlexHarnessChatSessionsPush;
32
- response: Record<string, never>;
32
+ /** Takes the tab's waiting pushes, in the order the hub made them. */
33
+ export interface IReq_FlexHarnessChatPull extends plugins.typedrequestInterfaces.ITypedRequest {
34
+ method: 'flexHarnessChatPull';
35
+ request: { caller: IFlexHarnessChatCallerPayload };
36
+ response: { pushes: TFlexHarnessChatPush[] };
33
37
  }
34
38
 
35
39
  /** Push: run a tool of a turn in the tab; the answer is the tool's output. */
@@ -4,7 +4,7 @@
4
4
  pnpm add @modelprofile.com/flexharness @api.global/typedsocket @api.global/typedrequest @api.global/typedrequest-interfaces
5
5
  ```
6
6
 
7
- The TypedSocket packages are optional peers: `@api.global/typedsocket` `^9.0.2` with `@api.global/typedrequest` `^9.1.0`, the major TypedSocket uses.
7
+ The TypedSocket packages are optional peers: `@api.global/typedsocket` `^9.1.0`, which brings events, with `@api.global/typedrequest` `^9.1.0`, the major TypedSocket uses. The server and the tab both need 9.1.0 or later.
8
8
 
9
9
  ```typescript
10
10
  // server
@@ -27,9 +27,13 @@ const transport = new FlexHarnessChatTypedSocketTransport({ typedsocket, typedro
27
27
  const client = new FlexHarnessChatClient({ transport });
28
28
  ```
29
29
 
30
- Every request carries `caller: { tabId, token? }`. `resolve` reads it and the request's TypedSocket context and returns the hub and scope it reaches; a `FlexHarnessChatRequestError` it throws reaches the tab as a `TypedResponseError` with its message. The tab id is 16 random bytes in hex, kept for the tab's lifetime and never shared: every request claims the connection it arrives on for the tab with the server-owned tag (`flexHarnessChatTab` by default), so updates, list pushes and tool calls follow a reconnect. The tag's payload is the resolved scope and the tab id: a caller of another scope that sends the same tab id names a tab of its own and never takes over this one's connection, its updates or its tool calls. Pushes wait `pushTimeoutMs` (30 seconds) for the tab.
30
+ Every request carries `caller: { tabId, token? }`. `resolve` reads it and the request's TypedSocket context and returns the hub and scope it reaches; a `FlexHarnessChatRequestError` it throws reaches the tab as a `TypedResponseError` with its message. The tab id is 16 random bytes in hex, kept for the tab's lifetime and never shared: every request claims the connection it arrives on for the tab with the server-owned tag (`flexHarnessChatTab` by default), so updates, list pushes and tool calls follow a reconnect. The tag's payload is the resolved scope and the tab id: a caller of another scope that sends the same tab id names a tab of its own and never takes over this one's connection, its updates or its tool calls.
31
31
 
32
- The transport adds its push handlers (`flexHarnessChatUpdate`, `flexHarnessChatSessions`, `flexHarnessChatTool`) to the tab's router and removes them when the client is disposed. A tool call the server cancels settles at once in the tab. A restored connection makes the client attach again.
32
+ Updates and session-list pushes travel as a TypedSocket event and a pull. The hub's push waits in the tab's mailbox on the server, and the binding sends the tab the event `flexHarnessChatPending` (no payload). The tab answers with the request `flexHarnessChatPull`, which runs through `resolve` like every other request and returns the waiting pushes in order. A push settles when the tab pulls it; one the tab does not pull within `pushTimeoutMs` (30 seconds) fails, leaves the mailbox and never reaches the tab; a failed update makes the hub send that subscription a reset with its next change. Tool calls stay requests from the server to the tab (`flexHarnessChatTool`).
33
+
34
+ The transport pulls on the event and whenever TypedSocket reports that events may have been missed: after it registers the event on a (re)connected socket, and after a sequence gap. Pulls run one at a time; a signal that arrives during a pull makes it pull once more. A pull that fails leaves the pushes waiting for the next one. A handler that throws on a push is reported with `console.warn('FLEXHARNESS_CHAT_PUSH_HANDLER_ERROR', error)`, and the pushes after it still reach the tab; the server counts the push as delivered. `listen` adds the tool handler to the tab's router and the event handler to its client, and the client's dispose removes both. A tool call the server cancels settles at once in the tab. A restored connection makes the client attach again.
35
+
36
+ The transport's `typedsocket` option takes any object with the members it uses (`IFlexHarnessChatTypedSocketClient`: `createTypedRequest`, `onEvent`, `eventResyncSubject`, `statusSubject`). A `TypedSocket` client is one; a host whose own connection object carries the client can pass that object instead.
33
37
 
34
38
  ## Issue Reporting and Security
35
39