@modelprofile.com/flexharness 12.3.0 → 12.5.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 (55) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.flexharness.core.d.ts +3 -0
  3. package/dist_ts/classes.flexharness.core.js +22 -2
  4. package/dist_ts/classes.flexharness.prompts.js +4 -4
  5. package/dist_ts/classes.flexharness.runs.js +2 -2
  6. package/dist_ts/classes.flexharness.slashcommands.js +2 -2
  7. package/dist_ts/classes.flexharness.subagents.js +2 -2
  8. package/dist_ts/interfaces.d.ts +32 -1
  9. package/dist_ts/interfaces.js +1 -1
  10. package/dist_ts/plugins.d.ts +3 -3
  11. package/dist_ts/plugins.js +3 -3
  12. package/dist_ts/utils.harnessoptions.d.ts +3 -1
  13. package/dist_ts/utils.harnessoptions.js +14 -1
  14. package/dist_ts/utils.json.js +2 -2
  15. package/dist_ts/utils.prompt.d.ts +7 -1
  16. package/dist_ts/utils.prompt.js +38 -6
  17. package/dist_ts/utils.transfer.d.ts +1 -1
  18. package/dist_ts/utils.transfer.js +8 -3
  19. package/dist_ts_chat_harness/functions.intents.d.ts +6 -0
  20. package/dist_ts_chat_harness/functions.intents.js +5 -2
  21. package/dist_ts_chat_harness_server/classes.chathub.js +2 -2
  22. package/dist_ts_chat_harness_server/classes.turngate.d.ts +5 -2
  23. package/dist_ts_chat_harness_server/classes.turngate.js +31 -8
  24. package/dist_ts_chat_harness_server/index.d.ts +1 -1
  25. package/dist_ts_chat_harness_server/interfaces.d.ts +20 -0
  26. package/dist_ts_chat_harness_server/plugins.d.ts +1 -1
  27. package/dist_ts_compaction/plugins.d.ts +3 -3
  28. package/dist_ts_compaction/plugins.js +3 -3
  29. package/dist_ts_compaction/smartagent.compaction.d.ts +6 -0
  30. package/dist_ts_compaction/smartagent.compaction.js +4 -1
  31. package/package.json +3 -3
  32. package/readme.md +27 -5
  33. package/ts/00_commitinfo_data.ts +1 -1
  34. package/ts/classes.flexharness.core.ts +21 -0
  35. package/ts/classes.flexharness.prompts.ts +3 -3
  36. package/ts/classes.flexharness.runs.ts +1 -1
  37. package/ts/classes.flexharness.slashcommands.ts +1 -1
  38. package/ts/classes.flexharness.subagents.ts +1 -1
  39. package/ts/interfaces.ts +34 -1
  40. package/ts/plugins.ts +3 -0
  41. package/ts/readme.md +27 -5
  42. package/ts/utils.harnessoptions.ts +15 -0
  43. package/ts/utils.json.ts +1 -1
  44. package/ts/utils.prompt.ts +48 -5
  45. package/ts/utils.transfer.ts +6 -1
  46. package/ts_chat_harness/functions.intents.ts +10 -1
  47. package/ts_chat_harness/readme.md +1 -1
  48. package/ts_chat_harness_server/classes.chathub.ts +5 -1
  49. package/ts_chat_harness_server/classes.turngate.ts +45 -7
  50. package/ts_chat_harness_server/index.ts +1 -0
  51. package/ts_chat_harness_server/interfaces.ts +21 -0
  52. package/ts_chat_harness_server/plugins.ts +1 -0
  53. package/ts_chat_harness_server/readme.md +2 -0
  54. package/ts_compaction/plugins.ts +11 -3
  55. package/ts_compaction/smartagent.compaction.ts +9 -0
@@ -1,7 +1,12 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import { FlexHarnessChatRequestError } from './errors.js';
3
3
  import { STOPPED_MESSAGE, isSameTab } from './functions.validation.js';
4
- import type { IFlexHarnessChatCaller, IFlexHarnessChatLimits, IFlexHarnessChatTurn } from './interfaces.js';
4
+ import type {
5
+ IFlexHarnessChatCaller,
6
+ IFlexHarnessChatHubOptions,
7
+ IFlexHarnessChatLimits,
8
+ IFlexHarnessChatTurn,
9
+ } from './interfaces.js';
5
10
 
6
11
  const STILL_FINISHING_MESSAGE =
7
12
  'The assistant is still finishing its previous reply. Send your message again in a moment.';
@@ -53,6 +58,7 @@ const settlesBefore = async (promiseArg: Promise<unknown>, deadlineArg: number):
53
58
  export class FlexHarnessChatTurnGate<TContext> {
54
59
  private readonly harness: TFlexHarnessChatGateHarness;
55
60
  private readonly limits: IFlexHarnessChatLimits;
61
+ private readonly attachmentPart: IFlexHarnessChatHubOptions<TContext>['attachmentPart'];
56
62
  private readonly bindings = new Map<string, ITurnBinding<TContext>>();
57
63
  /** serializes the admission of messages per conversation */
58
64
  private readonly locks = new Map<string, Promise<void>>();
@@ -60,9 +66,14 @@ export class FlexHarnessChatTurnGate<TContext> {
60
66
  private readonly admissions = new Set<Promise<void>>();
61
67
  private stopped = false;
62
68
 
63
- constructor(harnessArg: TFlexHarnessChatGateHarness, limitsArg: IFlexHarnessChatLimits) {
69
+ constructor(
70
+ harnessArg: TFlexHarnessChatGateHarness,
71
+ limitsArg: IFlexHarnessChatLimits,
72
+ attachmentPartArg?: IFlexHarnessChatHubOptions<TContext>['attachmentPart'],
73
+ ) {
64
74
  this.harness = harnessArg;
65
75
  this.limits = limitsArg;
76
+ this.attachmentPart = attachmentPartArg;
66
77
  }
67
78
 
68
79
  public turnOf(scopeIdArg: string, sessionIdArg: string): IFlexHarnessChatTurn<TContext> | undefined {
@@ -160,10 +171,12 @@ export class FlexHarnessChatTurnGate<TContext> {
160
171
  if (!(await settlesBefore(promiseArg, deadlineArg))) throw new FlexHarnessChatRequestError(STILL_FINISHING_MESSAGE);
161
172
  };
162
173
  this.assertOpen();
174
+ const attachmentParts = await this.attachmentPartsOf(callerArg, target, sendArg, contextArg);
175
+ this.assertOpen();
163
176
  const binding = this.bindings.get(bindingKey(target.scopeId, target.sessionId));
164
177
  if (binding?.queueId) {
165
178
  for (let attempt = 0; attempt < 2; attempt++) {
166
- const intent = intentOf(target, { ...sendArg, delivery: 'steer' }, binding.queueId);
179
+ const intent = intentOf(target, { ...sendArg, delivery: 'steer' }, { activeQueueId: binding.queueId, attachmentParts });
167
180
  if (intent.type !== 'steerPrompt') break;
168
181
  this.assertOpen();
169
182
  try {
@@ -187,7 +200,28 @@ export class FlexHarnessChatTurnGate<TContext> {
187
200
  await waitFor(binding.settled);
188
201
  }
189
202
  this.assertOpen();
190
- return this.startTurn(callerArg, target, sendArg, contextArg);
203
+ return this.startTurn(callerArg, target, sendArg, contextArg, attachmentParts);
204
+ }
205
+
206
+ /** The prompt parts the host makes of the message's attachments, when it makes them. */
207
+ private async attachmentPartsOf(
208
+ callerArg: IFlexHarnessChatCaller,
209
+ target: ITarget,
210
+ sendArg: plugins.IHarnessComposerSendDetail,
211
+ contextArg: TContext,
212
+ ): Promise<plugins.TFlexPromptPart[] | undefined> {
213
+ if (!this.attachmentPart || sendArg.attachments.length === 0) return undefined;
214
+ const parts: plugins.TFlexPromptPart[] = [];
215
+ for (const attachment of sendArg.attachments) {
216
+ parts.push(await this.attachmentPart({
217
+ tab: { tabId: callerArg.tabId, scopeId: callerArg.scopeId },
218
+ scopeId: target.scopeId,
219
+ sessionId: target.sessionId,
220
+ context: contextArg,
221
+ attachment,
222
+ }));
223
+ }
224
+ return parts;
191
225
  }
192
226
 
193
227
  private async startTurn(
@@ -195,10 +229,11 @@ export class FlexHarnessChatTurnGate<TContext> {
195
229
  target: ITarget,
196
230
  sendArg: plugins.IHarnessComposerSendDetail,
197
231
  contextArg: TContext,
232
+ attachmentPartsArg: plugins.TFlexPromptPart[] | undefined,
198
233
  ): Promise<plugins.TFlexHarnessChatResponse<'flexHarnessChatSend'>> {
199
234
  const { scopeId, sessionId } = target;
200
235
  const key = bindingKey(scopeId, sessionId);
201
- const intent = intentOf(target, { ...sendArg, delivery: 'turn' });
236
+ const intent = intentOf(target, { ...sendArg, delivery: 'turn' }, { attachmentParts: attachmentPartsArg });
202
237
  if (intent.type !== 'enqueuePrompt') throw new Error('A new turn must be queued.');
203
238
  let markSettled!: () => void;
204
239
  let markRunning!: () => void;
@@ -244,10 +279,13 @@ export class FlexHarnessChatTurnGate<TContext> {
244
279
  const intentOf = (
245
280
  targetArg: { scopeId: string; sessionId: string },
246
281
  sendArg: plugins.IHarnessComposerSendDetail,
247
- activeQueueIdArg?: string,
282
+ optionsArg: { activeQueueId?: string; attachmentParts?: plugins.TFlexPromptPart[] },
248
283
  ) => {
249
284
  try {
250
- return plugins.flexHarnessSendIntent(targetArg, sendArg, activeQueueIdArg ? { activeQueueId: activeQueueIdArg } : {});
285
+ return plugins.flexHarnessSendIntent(targetArg, sendArg, {
286
+ ...(optionsArg.activeQueueId ? { activeQueueId: optionsArg.activeQueueId } : {}),
287
+ ...(optionsArg.attachmentParts ? { attachmentParts: optionsArg.attachmentParts } : {}),
288
+ });
251
289
  } catch (error) {
252
290
  throw new FlexHarnessChatRequestError((error as Error)?.message || 'The message could not be read.');
253
291
  }
@@ -6,6 +6,7 @@ export {
6
6
  type IFlexHarnessChatErrorProjectorOptions,
7
7
  } from './functions.errorprojector.js';
8
8
  export type {
9
+ IFlexHarnessChatAttachmentRequest,
9
10
  IFlexHarnessChatCaller,
10
11
  IFlexHarnessChatConversation,
11
12
  IFlexHarnessChatDirectory,
@@ -114,6 +114,18 @@ export type TFlexHarnessChatHubHarness = Pick<
114
114
  | 'redoSession'
115
115
  >;
116
116
 
117
+ /** An attachment of a message, as the hub asks the host to turn it into the prompt part the turn sends. */
118
+ export interface IFlexHarnessChatAttachmentRequest<TContext> {
119
+ /** the tab that sent the message */
120
+ tab: IFlexHarnessChatCaller;
121
+ /** the conversation the message goes to: its harness scope and session */
122
+ scopeId: string;
123
+ sessionId: string;
124
+ /** the context the message was sent with, as `readContext` returned it */
125
+ context: TContext;
126
+ attachment: plugins.IHarnessAttachment;
127
+ }
128
+
117
129
  export interface IFlexHarnessChatHubOptions<TContext> {
118
130
  /**
119
131
  * The harness the conversations run on. The host keeps configuring it: its model resolver and
@@ -127,5 +139,14 @@ export interface IFlexHarnessChatHubOptions<TContext> {
127
139
  titleOf?(send: plugins.IHarnessComposerSendDetail): string;
128
140
  /** Where the tabs' conversations are. Defaults to the root sessions of the tab's scope. */
129
141
  directory?: IFlexHarnessChatDirectory;
142
+ /**
143
+ * Turns each attachment of a message into the prompt part the turn sends, before the message is
144
+ * admitted; throw `FlexHarnessChatRequestError` to refuse the message. A host whose tabs upload
145
+ * their files to it first gets the attachments without content, under the ids it gave the
146
+ * uploads, and returns references its harness's `attachmentResolver` reads. The same message
147
+ * may come again after a refusal. Without it, each attachment is sent with the content the
148
+ * composer read, and one without content is refused.
149
+ */
150
+ attachmentPart?(request: IFlexHarnessChatAttachmentRequest<TContext>): Promise<plugins.TFlexPromptPart>;
130
151
  limits?: Partial<IFlexHarnessChatLimits>;
131
152
  }
@@ -36,6 +36,7 @@ export type {
36
36
  IFlexSession,
37
37
  TFlexExternalErrorProjector,
38
38
  TFlexHarnessEvent,
39
+ TFlexPromptPart,
39
40
  TJsonValue,
40
41
  } from '@modelprofile.com/flexharness';
41
42
  export type {
@@ -53,6 +53,8 @@ The host keeps its harness, models, system prompt, tools and scopes. It authenti
53
53
 
54
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.
55
55
 
56
+ A message's attachments are sent with the content the composer read, within `maxAttachments` and `maxAttachmentChars`. A host whose tabs upload their files to it first, for files larger than a message may carry, passes `attachmentPart(request)`: before the message is admitted, the hub asks it for the prompt part of each attachment, in order, with the tab, the conversation's `scopeId` and `sessionId` and the message's context. The attachments then arrive as the composer describes them, under the ids the host gave the uploads; the host checks that the tab may send each one into that conversation and returns a reference its harness's `attachmentResolver` reads, with `sizeBytes`, so the transcript shows the file by name and size. Throw `FlexHarnessChatRequestError` to refuse the message; nothing of it is sent, and a conversation created for it is deleted again. The same message may come again after a refusal, and a steer's attachments go through the hook as a new turn's do.
57
+
56
58
  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.
57
59
 
58
60
  ## Turns and tools in the tab
@@ -5,8 +5,16 @@ export { asSchema, streamText, wrapLanguageModel };
5
5
  export type { ModelMessage, ToolSet } from 'ai';
6
6
  export type { TSmartAiLanguageModel } from '@modelprofile.com/flexharness-models';
7
7
 
8
- import { AgentModelCallUsageRecorder, endModelStreamAtAbort } from '@modelprofile.com/flexharness-agent';
8
+ import {
9
+ AgentModelCallUsageRecorder,
10
+ createAttachmentDownload,
11
+ endModelStreamAtAbort,
12
+ } from '@modelprofile.com/flexharness-agent';
9
13
 
10
- export { AgentModelCallUsageRecorder, endModelStreamAtAbort };
14
+ export { AgentModelCallUsageRecorder, createAttachmentDownload, endModelStreamAtAbort };
11
15
 
12
- export type { ProviderOptions, TAgentModelCallUsageReporter } from '@modelprofile.com/flexharness-agent';
16
+ export type {
17
+ IAgentAttachmentResolver,
18
+ ProviderOptions,
19
+ TAgentModelCallUsageReporter,
20
+ } from '@modelprofile.com/flexharness-agent';
@@ -55,6 +55,12 @@ export interface ICompactMessagesOptions {
55
55
  * cost more than the history saves. A context compactor passes its options' `tools` through.
56
56
  */
57
57
  tools?: plugins.ToolSet;
58
+ /**
59
+ * Reads the attachments the history names by a URL of one of its schemes, so the summary call
60
+ * sends them as the turns do. A context compactor passes its options' `attachmentResolver`
61
+ * through.
62
+ */
63
+ attachmentResolver?: plugins.IAgentAttachmentResolver;
58
64
  }
59
65
 
60
66
  /**
@@ -134,6 +140,8 @@ export async function compactMessages(
134
140
  },
135
141
  })
136
142
  : model;
143
+ // The summary's attempts read each attachment once.
144
+ const download = plugins.createAttachmentDownload(options.attachmentResolver, options.abortSignal);
137
145
  // The summary is asked for in the shape of a turn, the tools offered as the turns offer them. A
138
146
  // model that calls a tool instead of answering is asked again with tool use turned off, which
139
147
  // keeps the tool definitions and so still the cached tools and instructions.
@@ -148,6 +156,7 @@ export async function compactMessages(
148
156
  ...(tools ? { tools, ...(toolChoice ? { toolChoice } : {}) } : {}),
149
157
  ...(options.providerOptions ? { providerOptions: options.providerOptions } : {}),
150
158
  abortSignal: options.abortSignal,
159
+ ...(download ? { experimental_download: download } : {}),
151
160
  onError: ({ error }) => {
152
161
  streamError ??= error;
153
162
  },