@modelprofile.com/flexharness 10.15.0 → 10.16.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 (46) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.codesandbox.js +6 -2
  3. package/dist_ts/classes.flexharness.d.ts +33 -0
  4. package/dist_ts/classes.flexharness.js +328 -45
  5. package/dist_ts/classes.stores.d.ts +3 -1
  6. package/dist_ts/classes.stores.js +59 -1
  7. package/dist_ts/classes.toolcatalog.d.ts +9 -1
  8. package/dist_ts/classes.toolcatalog.js +82 -11
  9. package/dist_ts/interfaces.d.ts +59 -0
  10. package/dist_ts/interfaces.js +9 -1
  11. package/dist_ts/plugins.d.ts +3 -3
  12. package/dist_ts/plugins.js +3 -3
  13. package/dist_ts/utils.json.d.ts +5 -0
  14. package/dist_ts/utils.json.js +35 -1
  15. package/dist_ts/utils.results.d.ts +21 -0
  16. package/dist_ts/utils.results.js +158 -0
  17. package/dist_ts/utils.tooloutput.d.ts +5 -2
  18. package/dist_ts/utils.tooloutput.js +10 -3
  19. package/dist_ts_document/index.js +21 -8
  20. package/dist_ts_document/plugins.d.ts +2 -2
  21. package/dist_ts_document/plugins.js +3 -3
  22. package/dist_ts_media/index.js +14 -3
  23. package/dist_ts_media/plugins.d.ts +2 -2
  24. package/dist_ts_media/plugins.js +3 -3
  25. package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +2 -1
  26. package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +37 -4
  27. package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +4 -3
  28. package/dist_ts_stores_nosqldb/classes.recordstore.js +2 -2
  29. package/package.json +3 -3
  30. package/ts/00_commitinfo_data.ts +1 -1
  31. package/ts/classes.codesandbox.ts +5 -1
  32. package/ts/classes.flexharness.ts +382 -44
  33. package/ts/classes.stores.ts +66 -0
  34. package/ts/classes.toolcatalog.ts +87 -7
  35. package/ts/interfaces.ts +60 -0
  36. package/ts/plugins.ts +3 -0
  37. package/ts/utils.json.ts +30 -0
  38. package/ts/utils.results.ts +164 -0
  39. package/ts/utils.tooloutput.ts +9 -2
  40. package/ts_document/index.ts +18 -7
  41. package/ts_document/plugins.ts +2 -2
  42. package/ts_document/readme.md +2 -0
  43. package/ts_media/index.ts +11 -2
  44. package/ts_media/plugins.ts +2 -2
  45. package/ts_stores_nosqldb/classes.nosqldbstores.ts +39 -2
  46. package/ts_stores_nosqldb/classes.recordstore.ts +6 -4
@@ -489,10 +489,10 @@ export async function analyzeDocumentsWithUsage(options: IDocumentOptions): Prom
489
489
  ? plugins.wrapLanguageModel({
490
490
  model: options.model,
491
491
  middleware: {
492
- wrapGenerate: async ({ doGenerate, params }) => {
492
+ wrapStream: async ({ doStream, params }) => {
493
493
  modelCalls.start();
494
494
  try {
495
- return await doGenerate();
495
+ return await doStream();
496
496
  } catch (error) {
497
497
  modelCalls.settle(params.abortSignal?.aborted ? 'aborted' : 'failed');
498
498
  throw error;
@@ -515,14 +515,20 @@ export async function analyzeDocumentsWithUsage(options: IDocumentOptions): Prom
515
515
  ],
516
516
  },
517
517
  ];
518
- let result: Awaited<ReturnType<typeof plugins.generateText>>;
518
+ // The call is streamed: some providers, such as the ChatGPT Codex backend, refuse a call that is not.
519
+ let streamError: unknown;
520
+ let text = '';
521
+ let usage: Awaited<ReturnType<typeof plugins.streamText>['totalUsage']>;
519
522
  try {
520
- result = await plugins.generateText({
523
+ const result = plugins.streamText({
521
524
  model,
522
525
  instructions: options.systemMessage,
523
526
  messages,
524
527
  providerOptions: options.providerOptions,
525
528
  abortSignal,
529
+ onError: ({ error }) => {
530
+ streamError ??= error;
531
+ },
526
532
  ...(modelCalls
527
533
  ? {
528
534
  onLanguageModelCallEnd: ({ modelId, usage: callUsage }) => {
@@ -531,16 +537,21 @@ export async function analyzeDocumentsWithUsage(options: IDocumentOptions): Prom
531
537
  }
532
538
  : {}),
533
539
  });
540
+ for await (const delta of result.textStream) text += delta;
541
+ // A stream that fails delivers its error to onError and ends, and an aborted stream just ends;
542
+ // either way the text is incomplete.
543
+ if (streamError !== undefined) throw streamError;
544
+ abortSignal?.throwIfAborted();
545
+ usage = await result.totalUsage;
534
546
  modelCalls?.settle('missing');
535
547
  } catch (error) {
536
548
  modelCalls?.settle(abortSignal?.aborted ? 'aborted' : 'failed');
537
- throw error;
549
+ throw streamError ?? error;
538
550
  }
539
- const usage = result.totalUsage;
540
551
  const inputTokens = usage.inputTokens ?? 0;
541
552
  const outputTokens = usage.outputTokens ?? 0;
542
553
  return {
543
- text: result.text,
554
+ text,
544
555
  usage: {
545
556
  inputTokens,
546
557
  outputTokens,
@@ -1,7 +1,7 @@
1
- import { generateText, wrapLanguageModel } from 'ai';
1
+ import { streamText, wrapLanguageModel } from 'ai';
2
2
  import * as smartpdf from '@push.rocks/smartpdf';
3
3
  import { AgentModelCallUsageRecorder } from '@modelprofile.com/flexharness-agent';
4
- export { generateText, wrapLanguageModel, smartpdf, AgentModelCallUsageRecorder };
4
+ export { streamText, wrapLanguageModel, smartpdf, AgentModelCallUsageRecorder };
5
5
  export type { ModelMessage } from 'ai';
6
6
  export type { TSmartAiLanguageModel, TSmartAiProviderOptions } from '@modelprofile.com/flexharness-models';
7
7
  export type { TAgentModelCallUsageReporter } from '@modelprofile.com/flexharness-agent';
@@ -67,6 +67,8 @@ Both functions accept these request options:
67
67
  - `maxImageEdge` is the longest edge, in pixels, of an image document as it is sent; see above.
68
68
  - `onUsage` receives the usage of each model call of the request, also of a request that fails or is aborted; see [Usage of a request that fails or is aborted](#usage-of-a-request-that-fails-or-is-aborted).
69
69
 
70
+ A request makes one streamed model call, retries aside, so it also reads documents on providers that answer streamed calls only, such as the Codex backend of a ChatGPT account.
71
+
70
72
  `extractTextFromPdfWithUsage` and `analyzeDocumentsWithUsage` take the same arguments and return `{ text, usage, pageCount }`. `usage` has the shape of an agent run's usage (`inputTokens`, `outputTokens`, `totalTokens`, `cacheReadTokens`, `cacheWriteTokens`), summed over the request's model steps, with usage the provider leaves out counted as zero; `pageCount` is the number of rendered pages sent to the model.
71
73
 
72
74
  ```typescript
package/ts_media/index.ts CHANGED
@@ -8,7 +8,9 @@ export interface IVisionOptions {
8
8
  }
9
9
 
10
10
  export async function analyzeImage(options: IVisionOptions): Promise<string> {
11
- const result = await plugins.generateText({
11
+ // The call is streamed: some providers, such as the ChatGPT Codex backend, refuse a call that is not.
12
+ let streamError: unknown;
13
+ const result = plugins.streamText({
12
14
  model: options.model,
13
15
  messages: [
14
16
  {
@@ -23,6 +25,13 @@ export async function analyzeImage(options: IVisionOptions): Promise<string> {
23
25
  ],
24
26
  },
25
27
  ],
28
+ onError: ({ error }) => {
29
+ streamError ??= error;
30
+ },
26
31
  });
27
- return result.text;
32
+ let text = '';
33
+ for await (const delta of result.textStream) text += delta;
34
+ // A stream that fails delivers its error to onError and ends; its text is incomplete.
35
+ if (streamError !== undefined) throw streamError;
36
+ return text;
28
37
  }
@@ -1,3 +1,3 @@
1
- import { generateText } from 'ai';
2
- export { generateText };
1
+ import { streamText } from 'ai';
2
+ export { streamText };
3
3
  export type { TSmartAiLanguageModel } from '@modelprofile.com/flexharness-models';
@@ -11,16 +11,19 @@ import type {
11
11
  IFlexProjectManagementWriteContext,
12
12
  IFlexProjectionSnapshotV3,
13
13
  IFlexProjectionStore,
14
+ IFlexResultStore,
14
15
  IFlexScopeSnapshot,
15
16
  IFlexScopeStore,
16
17
  IFlexToolJobStoreProvider,
17
18
  TFlexProjectionSnapshot,
18
19
  TFlexProjectManagementRecord,
20
+ TJsonValue,
19
21
  } from '../ts/interfaces.js';
20
22
  import {
21
23
  assertFlexPermissionSnapshot,
22
24
  assertFlexProjectionSnapshot,
23
25
  assertFlexScopeSnapshot,
26
+ assertJsonSerializable,
24
27
  } from '../ts/utils.json.js';
25
28
  import {
26
29
  assertFlexProjectManagementRecord,
@@ -297,7 +300,7 @@ class NoSqlAgentEventStore implements plugins.IAgentEventStoreV2 {
297
300
  domain: 'agentEventArchive' as const,
298
301
  storageKey: this.storageKey,
299
302
  sessionId: this.sessionId,
300
- archiveId: archive.archiveId,
303
+ entryId: archive.archiveId,
301
304
  };
302
305
  await this.records.mutate(address, {
303
306
  validate: (value) => validateAgentEventArchive(value, this.sessionId, archive.archiveId),
@@ -318,7 +321,7 @@ class NoSqlAgentEventStore implements plugins.IAgentEventStoreV2 {
318
321
  this.assertSession(sessionId);
319
322
  validateIdentifier(archiveId, 'archiveId');
320
323
  return this.records.read(
321
- { domain: 'agentEventArchive', storageKey: this.storageKey, sessionId, archiveId },
324
+ { domain: 'agentEventArchive', storageKey: this.storageKey, sessionId, entryId: archiveId },
322
325
  (value) => validateAgentEventArchive(value, this.sessionId, archiveId),
323
326
  );
324
327
  }
@@ -428,6 +431,38 @@ export interface INoSqlFlexHarnessStoresOptions {
428
431
  operationTimeoutMs?: number;
429
432
  }
430
433
 
434
+ class NoSqlResultStore implements IFlexResultStore {
435
+ constructor(private readonly records: FlexRecordStore) {}
436
+
437
+ public async save(storageKey: string, sessionId: string, handle: string, value: TJsonValue): Promise<void> {
438
+ validateIdentifier(handle, 'handle');
439
+ assertJsonSerializable(value);
440
+ const conflict = () => new FlexHarnessStoreConflictError(`${providerKey(storageKey, sessionId)}:${handle}`, 0, 1);
441
+ await this.records.mutate({ domain: 'result', storageKey, sessionId, entryId: handle }, {
442
+ validate: validateResult,
443
+ decide: (current) => {
444
+ if (current !== undefined) throw conflict();
445
+ return { revision: 1, value };
446
+ },
447
+ contention: conflict,
448
+ });
449
+ }
450
+
451
+ public load(storageKey: string, sessionId: string, handle: string): Promise<TJsonValue | undefined> {
452
+ validateIdentifier(handle, 'handle');
453
+ return this.records.read({ domain: 'result', storageKey, sessionId, entryId: handle }, validateResult);
454
+ }
455
+
456
+ public deleteSession(storageKey: string, sessionId: string): Promise<void> {
457
+ return this.records.deleteOwner(storageKey, sessionId, ['result']);
458
+ }
459
+ }
460
+
461
+ function validateResult(value: unknown): TJsonValue {
462
+ assertJsonSerializable(value);
463
+ return value as TJsonValue;
464
+ }
465
+
431
466
  /**
432
467
  * Every FlexHarness persistence domain on `@lossless.org/client/nosqldb`, with compare-and-swap
433
468
  * that holds across processes. Records live in `flexharness_records`; payloads longer than one
@@ -440,6 +475,7 @@ export class NoSqlFlexHarnessStores implements IFlexHarnessStores {
440
475
  public readonly projectManagement: IFlexProjectManagementStore;
441
476
  public readonly agentEvents: IFlexAgentEventStoreProvider;
442
477
  public readonly jobs: IFlexToolJobStoreProvider;
478
+ public readonly results: IFlexResultStore;
443
479
  private readonly records: FlexRecordStore;
444
480
 
445
481
  constructor(options: INoSqlFlexHarnessStoresOptions) {
@@ -453,6 +489,7 @@ export class NoSqlFlexHarnessStores implements IFlexHarnessStores {
453
489
  this.projectManagement = new NoSqlProjectManagementStore(this.records);
454
490
  this.agentEvents = new NoSqlAgentEventStoreProvider(this.records);
455
491
  this.jobs = new NoSqlToolJobStoreProvider(this.records);
492
+ this.results = new NoSqlResultStore(this.records);
456
493
  }
457
494
 
458
495
  /**
@@ -12,14 +12,16 @@ export type TFlexRecordDomain =
12
12
  | 'projectManagement'
13
13
  | 'agentEvents'
14
14
  | 'agentEventArchive'
15
- | 'jobs';
15
+ | 'jobs'
16
+ | 'result';
16
17
 
17
- /** Names one record: a domain snapshot of a storage key, a session or an archive. */
18
+ /** Names one record: a domain snapshot of a storage key or a session, or one entry of a session. */
18
19
  export interface IFlexRecordAddress {
19
20
  domain: TFlexRecordDomain;
20
21
  storageKey: string;
21
22
  sessionId?: string;
22
- archiveId?: string;
23
+ /** The archive or result the record holds. */
24
+ entryId?: string;
23
25
  }
24
26
 
25
27
  /** The next stored value a mutation decided on. */
@@ -256,7 +258,7 @@ export class FlexRecordStore {
256
258
  address.domain,
257
259
  address.storageKey,
258
260
  address.sessionId ?? null,
259
- address.archiveId ?? null,
261
+ address.entryId ?? null,
260
262
  ]));
261
263
  return {
262
264
  recordId,