@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.
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/classes.codesandbox.js +1 -1
- package/dist_ts/classes.flexharness.admission.d.ts +17 -0
- package/dist_ts/classes.flexharness.admission.js +263 -0
- package/dist_ts/classes.flexharness.callbacks.d.ts +38 -0
- package/dist_ts/classes.flexharness.callbacks.js +313 -0
- package/dist_ts/classes.flexharness.compaction.d.ts +25 -0
- package/dist_ts/classes.flexharness.compaction.js +87 -0
- package/dist_ts/classes.flexharness.configurations.d.ts +11 -0
- package/dist_ts/classes.flexharness.configurations.js +137 -0
- package/dist_ts/classes.flexharness.core.d.ts +143 -0
- package/dist_ts/classes.flexharness.core.js +1538 -0
- package/dist_ts/classes.flexharness.d.ts +2 -437
- package/dist_ts/classes.flexharness.deletion.d.ts +31 -0
- package/dist_ts/classes.flexharness.deletion.js +396 -0
- package/dist_ts/classes.flexharness.imports.d.ts +24 -0
- package/dist_ts/classes.flexharness.imports.js +345 -0
- package/dist_ts/classes.flexharness.js +119 -9776
- package/dist_ts/classes.flexharness.lifecycle.d.ts +47 -0
- package/dist_ts/classes.flexharness.lifecycle.js +625 -0
- package/dist_ts/classes.flexharness.permissions.d.ts +19 -0
- package/dist_ts/classes.flexharness.permissions.js +450 -0
- package/dist_ts/classes.flexharness.projects.d.ts +55 -0
- package/dist_ts/classes.flexharness.projects.js +575 -0
- package/dist_ts/classes.flexharness.prompts.d.ts +44 -0
- package/dist_ts/classes.flexharness.prompts.js +659 -0
- package/dist_ts/classes.flexharness.providers.d.ts +18 -0
- package/dist_ts/classes.flexharness.providers.js +267 -0
- package/dist_ts/classes.flexharness.results.d.ts +26 -0
- package/dist_ts/classes.flexharness.results.js +89 -0
- package/dist_ts/classes.flexharness.reversions.d.ts +52 -0
- package/dist_ts/classes.flexharness.reversions.js +1306 -0
- package/dist_ts/classes.flexharness.runs.d.ts +63 -0
- package/dist_ts/classes.flexharness.runs.js +1058 -0
- package/dist_ts/classes.flexharness.sessions.d.ts +20 -0
- package/dist_ts/classes.flexharness.sessions.js +381 -0
- package/dist_ts/classes.flexharness.slashcommands.d.ts +40 -0
- package/dist_ts/classes.flexharness.slashcommands.js +458 -0
- package/dist_ts/classes.flexharness.subagents.d.ts +31 -0
- package/dist_ts/classes.flexharness.subagents.js +690 -0
- package/dist_ts/classes.flexharness.tooldispatch.d.ts +53 -0
- package/dist_ts/classes.flexharness.tooldispatch.js +418 -0
- package/dist_ts/classes.stores.js +1 -1
- package/dist_ts/classes.toolcatalog.js +1 -1
- package/dist_ts/errors.js +1 -1
- package/dist_ts/functions.toolpermissions.js +1 -1
- package/dist_ts/interfaces.d.ts +3 -3
- package/dist_ts/interfaces.flexharness.d.ts +455 -0
- package/dist_ts/interfaces.flexharness.js +2 -0
- package/dist_ts/utils.flexharness.d.ts +19 -0
- package/dist_ts/utils.flexharness.js +83 -0
- package/dist_ts/utils.harnessoptions.js +1 -1
- package/dist_ts/utils.json.js +1 -1
- package/dist_ts/utils.schemadeclarations.js +1 -1
- package/dist_ts/utils.sessionimport.js +1 -1
- package/dist_ts/utils.sessionmetrics.d.ts +1 -1
- package/dist_ts_chat/classes.chatsession.js +1 -1
- package/dist_ts_chat_harness/classes.transcript.js +1 -1
- package/dist_ts_chat_harness/functions.intents.js +1 -1
- package/dist_ts_chat_harness/functions.mapping.d.ts +1 -1
- package/dist_ts_chat_harness/functions.mapping.js +1 -1
- package/dist_ts_chat_harness_server/classes.chathub.d.ts +18 -7
- package/dist_ts_chat_harness_server/classes.chathub.js +65 -62
- package/dist_ts_chat_harness_server/classes.chatsubscription.d.ts +5 -3
- package/dist_ts_chat_harness_server/classes.chatsubscription.js +4 -4
- package/dist_ts_chat_harness_server/classes.scopedirectory.d.ts +17 -0
- package/dist_ts_chat_harness_server/classes.scopedirectory.js +49 -0
- package/dist_ts_chat_harness_server/classes.sessionlists.d.ts +3 -5
- package/dist_ts_chat_harness_server/classes.sessionlists.js +11 -12
- package/dist_ts_chat_harness_server/classes.turngate.d.ts +8 -2
- package/dist_ts_chat_harness_server/classes.turngate.js +22 -21
- package/dist_ts_chat_harness_server/functions.errorprojector.js +1 -1
- package/dist_ts_chat_harness_server/index.d.ts +3 -2
- package/dist_ts_chat_harness_server/index.js +3 -2
- package/dist_ts_chat_harness_server/interfaces.d.ts +35 -2
- package/dist_ts_chat_harness_server/plugins.d.ts +1 -1
- package/dist_ts_chat_harness_typedsocket/classes.clienttransport.d.ts +28 -3
- package/dist_ts_chat_harness_typedsocket/classes.clienttransport.js +71 -15
- package/dist_ts_chat_harness_typedsocket/classes.serverbinding.d.ts +5 -3
- package/dist_ts_chat_harness_typedsocket/classes.serverbinding.js +79 -16
- package/dist_ts_chat_harness_typedsocket/index.d.ts +2 -2
- package/dist_ts_chat_harness_typedsocket/index.js +1 -1
- package/dist_ts_chat_harness_typedsocket/interfaces.requests.d.ts +20 -11
- package/dist_ts_chat_web/flexchat-input.js +1 -1
- package/dist_ts_chat_web/flexchat-message.js +1 -1
- package/dist_ts_chat_web/flexchat-window.js +1 -1
- package/dist_ts_compaction/smartagent.compaction.js +1 -1
- package/dist_ts_kvm/ocr.js +1 -1
- package/dist_ts_ocr/classes.imageocr.js +1 -1
- package/dist_ts_ocr/ocr.mistral.js +1 -1
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +1 -1
- package/dist_ts_tools/tool.browser.js +1 -1
- package/dist_ts_tools/tool.filesystem.js +1 -1
- package/dist_ts_tools/tool.shell.js +1 -1
- package/dist_ts_tools_node/tool.context.js +1 -1
- package/dist_ts_typedrequest/catalog.d.ts +2 -2
- package/dist_ts_typedrequest/catalog.js +1 -1
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.admission.ts +331 -0
- package/ts/classes.flexharness.callbacks.ts +352 -0
- package/ts/classes.flexharness.compaction.ts +167 -0
- package/ts/classes.flexharness.configurations.ts +167 -0
- package/ts/classes.flexharness.core.ts +1830 -0
- package/ts/classes.flexharness.deletion.ts +616 -0
- package/ts/classes.flexharness.imports.ts +414 -0
- package/ts/classes.flexharness.lifecycle.ts +786 -0
- package/ts/classes.flexharness.permissions.ts +535 -0
- package/ts/classes.flexharness.projects.ts +1006 -0
- package/ts/classes.flexharness.prompts.ts +857 -0
- package/ts/classes.flexharness.providers.ts +322 -0
- package/ts/classes.flexharness.results.ts +98 -0
- package/ts/classes.flexharness.reversions.ts +1684 -0
- package/ts/classes.flexharness.runs.ts +1204 -0
- package/ts/classes.flexharness.sessions.ts +483 -0
- package/ts/classes.flexharness.slashcommands.ts +705 -0
- package/ts/classes.flexharness.subagents.ts +860 -0
- package/ts/classes.flexharness.tooldispatch.ts +475 -0
- package/ts/classes.flexharness.ts +147 -12567
- package/ts/interfaces.flexharness.ts +517 -0
- package/ts/utils.flexharness.ts +121 -0
- package/ts_chat_harness_server/classes.chathub.ts +68 -61
- package/ts_chat_harness_server/classes.chatsubscription.ts +7 -5
- package/ts_chat_harness_server/classes.scopedirectory.ts +67 -0
- package/ts_chat_harness_server/classes.sessionlists.ts +11 -19
- package/ts_chat_harness_server/classes.turngate.ts +28 -21
- package/ts_chat_harness_server/index.ts +4 -5
- package/ts_chat_harness_server/interfaces.ts +35 -2
- package/ts_chat_harness_server/plugins.ts +0 -3
- package/ts_chat_harness_server/readme.md +6 -4
- package/ts_chat_harness_typedsocket/classes.clienttransport.ts +92 -23
- package/ts_chat_harness_typedsocket/classes.serverbinding.ts +88 -16
- package/ts_chat_harness_typedsocket/index.ts +4 -2
- package/ts_chat_harness_typedsocket/interfaces.requests.ts +15 -11
- package/ts_chat_harness_typedsocket/readme.md +7 -3
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -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
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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:
|
|
12
|
-
/** The router the client was created with; the transport adds its
|
|
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
|
|
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:
|
|
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:
|
|
75
|
+
caller: this.caller(),
|
|
47
76
|
});
|
|
48
77
|
}
|
|
49
78
|
|
|
50
79
|
public listen(handlersArg: plugins.IFlexHarnessChatPushHandlers): () => void {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6
|
+
IReq_FlexHarnessChatPull,
|
|
6
7
|
IReq_FlexHarnessChatTool,
|
|
7
|
-
|
|
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
|
|
114
|
+
private readonly tab: plugins.IFlexHarnessChatCaller,
|
|
115
|
+
private readonly mailboxes: TypedSocketChatMailboxes,
|
|
52
116
|
) {}
|
|
53
117
|
|
|
54
|
-
public
|
|
55
|
-
|
|
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
|
|
61
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
16
|
+
IReq_FlexHarnessChatPull,
|
|
15
17
|
IReq_FlexHarnessChatTool,
|
|
16
|
-
|
|
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
|
|
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
|
-
/**
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
/**
|
|
29
|
-
export interface
|
|
30
|
-
method: '
|
|
31
|
-
request:
|
|
32
|
-
response:
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|