@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
@@ -26,6 +26,12 @@ export interface ICompactMessagesOptions {
26
26
  * cost more than the history saves. A context compactor passes its options' `tools` through.
27
27
  */
28
28
  tools?: plugins.ToolSet;
29
+ /**
30
+ * Reads the attachments the history names by a URL of one of its schemes, so the summary call
31
+ * sends them as the turns do. A context compactor passes its options' `attachmentResolver`
32
+ * through.
33
+ */
34
+ attachmentResolver?: plugins.IAgentAttachmentResolver;
29
35
  }
30
36
  /**
31
37
  * Compacts a message history into a summary.
@@ -90,6 +90,8 @@ export async function compactMessages(model, messages, options = {}) {
90
90
  },
91
91
  })
92
92
  : model;
93
+ // The summary's attempts read each attachment once.
94
+ const download = plugins.createAttachmentDownload(options.attachmentResolver, options.abortSignal);
93
95
  // The summary is asked for in the shape of a turn, the tools offered as the turns offer them. A
94
96
  // model that calls a tool instead of answering is asked again with tool use turned off, which
95
97
  // keeps the tool definitions and so still the cached tools and instructions.
@@ -104,6 +106,7 @@ export async function compactMessages(model, messages, options = {}) {
104
106
  ...(tools ? { tools, ...(toolChoice ? { toolChoice } : {}) } : {}),
105
107
  ...(options.providerOptions ? { providerOptions: options.providerOptions } : {}),
106
108
  abortSignal: options.abortSignal,
109
+ ...(download ? { experimental_download: download } : {}),
107
110
  onError: ({ error }) => {
108
111
  streamError ??= error;
109
112
  },
@@ -147,4 +150,4 @@ function summaryMessages(summary) {
147
150
  },
148
151
  ];
149
152
  }
150
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRhZ2VudC5jb21wYWN0aW9uLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHNfY29tcGFjdGlvbi9zbWFydGFnZW50LmNvbXBhY3Rpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFFeEMsTUFBTSxpQkFBaUIsR0FBRzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7SUFvQnRCLENBQUM7QUFFTCw0RkFBNEY7QUFDNUYsTUFBTSx1QkFBdUIsR0FBRywyRUFBMkUsQ0FBQztBQUU1Ryw2RUFBNkU7QUFDN0UsTUFBTSxhQUFhLEdBQUcseURBQXlELENBQUM7QUErQmhGOzs7OztHQUtHO0FBQ0gsS0FBSyxVQUFVLHFCQUFxQixDQUFDLEtBQXNCLEVBQUUsUUFBZ0M7SUFDM0YsSUFBSSxTQUFTLEdBQUcsQ0FBQyxDQUFDO0lBQ2xCLEtBQUssTUFBTSxDQUFDLElBQUksRUFBRSxVQUFVLENBQUMsSUFBSSxNQUFNLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDdkQsTUFBTSxNQUFNLEdBQUcsVUFBVSxDQUFDLElBQUksS0FBSyxVQUFVLENBQUMsQ0FBQyxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLE1BQU0sT0FBTyxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQUMsV0FBVyxDQUFDLENBQUMsVUFBVSxDQUFDO1FBQzVILFNBQVMsSUFBSSxJQUFJLENBQUMsTUFBTSxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsQ0FBQyxVQUFVLENBQUMsV0FBVyxJQUFJLEVBQUUsRUFBRSxNQUFNLElBQUksSUFBSSxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUM7SUFDbkcsQ0FBQztJQUNELE9BQU8sU0FBUyxJQUFJLENBQUMsR0FBRyxJQUFJLENBQUMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxDQUFDLE1BQU0sQ0FBQztBQUMxRCxDQUFDO0FBRUQsNkZBQTZGO0FBQzdGLFNB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
153
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRhZ2VudC5jb21wYWN0aW9uLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHNfY29tcGFjdGlvbi9zbWFydGFnZW50LmNvbXBhY3Rpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFFeEMsTUFBTSxpQkFBaUIsR0FBRzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7SUFvQnRCLENBQUM7QUFFTCw0RkFBNEY7QUFDNUYsTUFBTSx1QkFBdUIsR0FBRywyRUFBMkUsQ0FBQztBQUU1Ryw2RUFBNkU7QUFDN0UsTUFBTSxhQUFhLEdBQUcseURBQXlELENBQUM7QUFxQ2hGOzs7OztHQUtHO0FBQ0gsS0FBSyxVQUFVLHFCQUFxQixDQUFDLEtBQXNCLEVBQUUsUUFBZ0M7SUFDM0YsSUFBSSxTQUFTLEdBQUcsQ0FBQyxDQUFDO0lBQ2xCLEtBQUssTUFBTSxDQUFDLElBQUksRUFBRSxVQUFVLENBQUMsSUFBSSxNQUFNLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDdkQsTUFBTSxNQUFNLEdBQUcsVUFBVSxDQUFDLElBQUksS0FBSyxVQUFVLENBQUMsQ0FBQyxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLE1BQU0sT0FBTyxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQUMsV0FBVyxDQUFDLENBQUMsVUFBVSxDQUFDO1FBQzVILFNBQVMsSUFBSSxJQUFJLENBQUMsTUFBTSxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsQ0FBQyxVQUFVLENBQUMsV0FBVyxJQUFJLEVBQUUsRUFBRSxNQUFNLElBQUksSUFBSSxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUM7SUFDbkcsQ0FBQztJQUNELE9BQU8sU0FBUyxJQUFJLENBQUMsR0FBRyxJQUFJLENBQUMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxDQUFDLE1BQU0sQ0FBQztBQUMxRCxDQUFDO0FBRUQsNkZBQTZGO0FBQzdGLFNB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
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "Task Venture Capital GmbH",
3
3
  "license": "MIT",
4
4
  "name": "@modelprofile.com/flexharness",
5
- "version": "12.3.0",
5
+ "version": "12.5.0",
6
6
  "type": "module",
7
7
  "description": "Managed agent sessions with tools, MCP, chat, OCR, documents and browser control through focused subpath imports.",
8
8
  "main": "./dist_ts/index.js",
@@ -91,8 +91,8 @@
91
91
  },
92
92
  "dependencies": {
93
93
  "@cfworker/json-schema": "^4.1.1",
94
- "@modelprofile.com/flexharness-agent": "12.3.0",
95
- "@modelprofile.com/flexharness-models": "12.3.0",
94
+ "@modelprofile.com/flexharness-agent": "12.5.0",
95
+ "@modelprofile.com/flexharness-models": "12.5.0",
96
96
  "@push.rocks/smartrequest": "^5.5.1",
97
97
  "ai": "^7.0.111",
98
98
  "zod": "^4.6.5"
package/readme.md CHANGED
@@ -578,10 +578,32 @@ console.log(result.usage);
578
578
  `TFlexPrompt` is deliberately JSON-safe. It accepts a string or an ordered array of:
579
579
 
580
580
  - `{ type: 'text', text }`
581
- - `{ type: 'image', data, mediaType?, name? }`
582
- - `{ type: 'file', data, mediaType, name? }`
581
+ - `{ type: 'image', data, mediaType?, name?, sizeBytes? }`
582
+ - `{ type: 'file', data, mediaType, name?, sizeBytes? }`
583
583
 
584
- Attachment `data` is a string containing base64, a data URL, or a remote URL. Public input never requires `Buffer` or `URL` objects. Remote URL strings are converted only at the private AgentSession invocation boundary.
584
+ Attachment `data` is a string containing base64, a data URL, a remote URL, or a reference. Public input never requires `Buffer` or `URL` objects. Remote URL strings are converted only at the private AgentSession invocation boundary.
585
+
586
+ A reference is a URL of a scheme the harness's `attachmentResolver` reads, for files the host keeps itself, such as uploads too large to travel inline:
587
+
588
+ ```typescript
589
+ const harness = new FlexHarness({
590
+ // ...
591
+ attachmentResolver: {
592
+ schemes: ['exampleattachment'],
593
+ async resolve({ scopeId, scope, storageKey, sessionId, url, abortSignal }) {
594
+ const upload = await uploads.read(storageKey, sessionId, url.pathname, abortSignal);
595
+ return { data: upload.bytes, mediaType: upload.mediaType };
596
+ },
597
+ },
598
+ });
599
+
600
+ await harness.prompt(scopeId, sessionId, [
601
+ { type: 'text', text: 'Book this receipt.' },
602
+ { type: 'file', data: 'exampleattachment:3f2a', mediaType: 'application/pdf', name: 'receipt.pdf', sizeBytes: 48231 },
603
+ ]);
604
+ ```
605
+
606
+ The history keeps the reference, never the bytes. Each model call that sends it, the turns' and a compaction's summary call alike, asks the resolver for the bytes and sends them to the provider; the steps of one turn ask once per reference. The resolver receives the scope and session the reference is sent in, so it reads only that conversation's files, and a reference it cannot read fails the turn. Schemes are lowercase, without the colon, and cannot be `data`, `file`, `http` or `https`; the harness refuses a malformed resolver with `FlexHarnessValidationError` when it is made. `sizeBytes`, which a reference states because the harness cannot count its bytes, is accepted on references only; a URL of a scheme the resolver does not name is no reference and is sent as before.
585
607
 
586
608
  Attachment payloads are never copied into public audit messages or events. Public attachment parts contain metadata only:
587
609
 
@@ -590,7 +612,7 @@ Attachment payloads are never copied into public audit messages or events. Publi
590
612
  type: 'attachment',
591
613
  partId: '...',
592
614
  attachmentType: 'file',
593
- source: 'inline-base64', // or data-url / remote-url
615
+ source: 'inline-base64', // or data-url / remote-url / reference
594
616
  sizeBytes: 48231, // omitted when it cannot be determined
595
617
  mediaType: 'application/pdf',
596
618
  name: 'invoice.pdf',
@@ -1038,7 +1060,7 @@ A `contextCompactor` states which model to compact with from the model the compa
1038
1060
 
1039
1061
  A compactor uses `resolvedModel.model` directly, and resolves `modelHint` the way its model resolver resolves a prompt's `modelHint`. Only when neither member is present, because nothing dispatched states a model, does the host choose the compaction model itself.
1040
1062
 
1041
- A `context-overflow` or `context-budget` compaction also carries `system` and `providerOptions`: the instructions and the provider options (cache options applied, so `store: false` on OpenAI) the run's turns are sent with. A `context-budget` compaction on a provider that caches the prefix also carries the turns' `tools`. `compactMessages()` sends its summary call with them as one streamed call, the shape of every turn, with the tools' definitions but none of their code, so a compactor that passes its options through repeats the run's prefix and reads it from the provider's prompt cache; definitions that outweigh the history nine times are left out. Without `system` it sends its own instructions.
1063
+ A `context-overflow` or `context-budget` compaction also carries `system` and `providerOptions`: the instructions and the provider options (cache options applied, so `store: false` on OpenAI) the run's turns are sent with. A `context-budget` compaction on a provider that caches the prefix also carries the turns' `tools`. `compactMessages()` sends its summary call with them as one streamed call, the shape of every turn, with the tools' definitions but none of their code, so a compactor that passes its options through repeats the run's prefix and reads it from the provider's prompt cache; definitions that outweigh the history nine times are left out. Without `system` it sends its own instructions. With an `attachmentResolver` on the harness, every compaction also carries `attachmentResolver`, bound to the session, so `compactMessages()` sends the history's referenced files as the turns do.
1042
1064
 
1043
1065
  ```typescript
1044
1066
  agentSessionPolicy: {
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/flexharness',
6
- version: '12.3.0',
6
+ version: '12.5.0',
7
7
  description: 'The FlexHarness toolbox source repository and shared dependency catalog.'
8
8
  }
@@ -55,6 +55,7 @@ import { hydrateAgentMessages } from './utils.prompt.js';
55
55
  import { createProjectionChange, freezeProjectionEntries } from './utils.projectionchange.js';
56
56
  import {
57
57
  normalizeAgentSessionPolicy,
58
+ normalizeAttachmentResolver,
58
59
  normalizeBuiltInTools,
59
60
  normalizeSlashCommands,
60
61
  normalizeToolCatalogOptions,
@@ -151,6 +152,9 @@ export class FlexHarnessCore<TScope> {
151
152
  public readonly resourceToolProviderResolver: IFlexHarnessOptions<TScope>['resourceToolProviderResolver'];
152
153
  private readonly executionContextProvider: IFlexHarnessOptions<TScope>['executionContextProvider'];
153
154
  public readonly delegatedRunAdmissionProvider: IFlexHarnessOptions<TScope>['delegatedRunAdmissionProvider'];
155
+ private readonly attachmentResolver: IFlexHarnessOptions<TScope>['attachmentResolver'];
156
+ /** The URL protocols, colon included, of the attachment references prompts may carry. */
157
+ public readonly attachmentReferenceProtocols: ReadonlySet<string>;
154
158
  public readonly stores: IFlexHarnessStores;
155
159
  public readonly agentSessionPolicy: IFlexAgentSessionPolicy<TScope>;
156
160
  public readonly toolOutputLimits: Required<NonNullable<IFlexHarnessOptions<TScope>['toolOutputLimits']>>;
@@ -206,6 +210,8 @@ export class FlexHarnessCore<TScope> {
206
210
  this.resourceToolProviderResolver = options.resourceToolProviderResolver;
207
211
  this.executionContextProvider = options.executionContextProvider;
208
212
  this.delegatedRunAdmissionProvider = options.delegatedRunAdmissionProvider;
213
+ this.attachmentReferenceProtocols = normalizeAttachmentResolver(options.attachmentResolver);
214
+ this.attachmentResolver = options.attachmentResolver;
209
215
  this.stores = requireHarnessStores(options.stores ?? new InMemoryFlexHarnessStores());
210
216
  this.agentSessionPolicy = normalizeAgentSessionPolicy(options.agentSessionPolicy);
211
217
  this.toolOutputLimits = resolveJsonLimits(options.toolOutputLimits);
@@ -743,6 +749,21 @@ export class FlexHarnessCore<TScope> {
743
749
  sessionId,
744
750
  eventStore,
745
751
  executionContext: contextualExecutionContext,
752
+ ...(this.attachmentResolver
753
+ ? {
754
+ attachmentResolver: {
755
+ schemes: [...this.attachmentReferenceProtocols].map((protocol) => protocol.slice(0, -1)),
756
+ resolve: ({ url, abortSignal }) => this.attachmentResolver!.resolve({
757
+ scopeId,
758
+ scope,
759
+ storageKey: state.storageKey,
760
+ sessionId,
761
+ url,
762
+ ...(abortSignal ? { abortSignal } : {}),
763
+ }),
764
+ },
765
+ }
766
+ : {}),
746
767
  contextBuilder: ({ events }) => hydrateAgentMessages(
747
768
  (this.agentSessionPolicy.contextBuilder ?? ((options) => plugins.buildModelMessages(options.events)))({
748
769
  events: this.reversions.filterReversionContextEvents(stored, events),
@@ -73,7 +73,7 @@ export class FlexHarnessPromptQueue<TScope> {
73
73
  debounceMs?: number,
74
74
  ): number {
75
75
  return jsonBytes({
76
- prompt: normalizeFlexPrompt(prompt),
76
+ prompt: normalizeFlexPrompt(prompt, this.core.attachmentReferenceProtocols),
77
77
  options: cloneSerializable(options),
78
78
  scheduleKey,
79
79
  debounceMs,
@@ -225,7 +225,7 @@ export class FlexHarnessPromptQueue<TScope> {
225
225
  validateIdentifier(steer.steerId, 'steerId');
226
226
  requireTransferIdentifier(steer.steerId, 'steerId');
227
227
  this.core.assertOpen();
228
- const prompt = normalizeFlexPrompt(steer.prompt);
228
+ const prompt = normalizeFlexPrompt(steer.prompt, this.core.attachmentReferenceProtocols);
229
229
  const byteSize = this.promptAdmissionByteSize(steer.prompt, {});
230
230
  const { state } = await this.core.resolveState(scopeId);
231
231
  this.core.assertStateAcceptingWork(state);
@@ -323,7 +323,7 @@ export class FlexHarnessPromptQueue<TScope> {
323
323
  queued?: IQueuedPrompt;
324
324
  }> {
325
325
  this.core.assertOpen();
326
- const normalizedPrompt = normalizeFlexPrompt(prompt);
326
+ const normalizedPrompt = normalizeFlexPrompt(prompt, this.core.attachmentReferenceProtocols);
327
327
  const normalizedOptions = cloneSerializable(options);
328
328
  const byteSize = this.promptAdmissionByteSize(prompt, options, scheduleKey, debounceMs);
329
329
  const releasePendingAdmission = this.reservePendingPromptAdmission(byteSize);
@@ -706,7 +706,7 @@ export class FlexHarnessRuns<TScope> {
706
706
  partId: plugins.crypto.randomUUID(),
707
707
  type: 'attachment' as const,
708
708
  attachmentType: part.type,
709
- ...describeAttachment(part),
709
+ ...describeAttachment(part, this.core.attachmentReferenceProtocols),
710
710
  };
711
711
  });
712
712
  }
@@ -345,7 +345,7 @@ export class FlexHarnessSlashCommands<TScope> {
345
345
  ? FLEX_SLASH_COMMAND_INITIALIZE_TEMPLATE
346
346
  : registration!.template!;
347
347
  const expanded = expandSlashCommandTemplate(template, parsed.rawArguments, parsed.arguments);
348
- normalizeFlexPrompt(expanded);
348
+ normalizeFlexPrompt(expanded, this.core.attachmentReferenceProtocols);
349
349
  this.requireSlashCommandPromptAvailable(
350
350
  state,
351
351
  stored,
@@ -84,7 +84,7 @@ export class FlexHarnessSubagents<TScope> {
84
84
  validateUtf8String(taskId, 'taskId', maxSubagentTaskIdBytes, true);
85
85
  validateIdentifier(steer.steerId, 'steerId');
86
86
  requireTransferIdentifier(steer.steerId, 'steerId');
87
- const prompt = normalizeFlexPrompt(steer.prompt);
87
+ const prompt = normalizeFlexPrompt(steer.prompt, this.core.attachmentReferenceProtocols);
88
88
  const byteSize = this.core.prompts.promptAdmissionByteSize(steer.prompt, {});
89
89
  this.core.assertStateAcceptingWork(parent.state);
90
90
  const child = parent.state.activeRuns.get(taskId);
package/ts/interfaces.ts CHANGED
@@ -39,18 +39,27 @@ export interface IFlexTextPromptPart {
39
39
  text: string;
40
40
  }
41
41
 
42
+ /**
43
+ * `data` is base64, a data URL, an `http(s)` URL, or a reference: a URL of a scheme the
44
+ * harness's `attachmentResolver` reads, such as `exampleattachment:3f2a`.
45
+ */
42
46
  export interface IFlexImagePromptPart {
43
47
  type: 'image';
44
48
  data: string;
45
49
  mediaType?: string;
46
50
  name?: string;
51
+ /** The attachment's size, for a reference only: the transcript shows it. */
52
+ sizeBytes?: number;
47
53
  }
48
54
 
55
+ /** `data` takes the forms an image part's does. */
49
56
  export interface IFlexFilePromptPart {
50
57
  type: 'file';
51
58
  data: string;
52
59
  mediaType: string;
53
60
  name?: string;
61
+ /** The attachment's size, for a reference only: the transcript shows it. */
62
+ sizeBytes?: number;
54
63
  }
55
64
 
56
65
  export type TFlexPromptPart = IFlexTextPromptPart | IFlexImagePromptPart | IFlexFilePromptPart;
@@ -129,7 +138,7 @@ export type TFlexActivityStatus =
129
138
  | 'cancelled';
130
139
  export type TFlexMessageStatus = 'streaming' | 'completed' | 'failed' | 'cancelled';
131
140
  export type TFlexToolPartStatus = 'running' | 'completed' | 'failed' | 'cancelled';
132
- export type TFlexAttachmentSource = 'inline-base64' | 'data-url' | 'remote-url';
141
+ export type TFlexAttachmentSource = 'inline-base64' | 'data-url' | 'remote-url' | 'reference';
133
142
 
134
143
  export interface IFlexTextMessagePart {
135
144
  partId: string;
@@ -1859,6 +1868,28 @@ export interface IFlexBuiltInToolsOptions {
1859
1868
  projectManagement?: IFlexProjectManagementBuiltInToolsOptions;
1860
1869
  }
1861
1870
 
1871
+ /** What an attachment resolver is asked for: the attachment `url` names, for one session. */
1872
+ export interface IFlexAttachmentRequest<TScope = unknown> extends IFlexAgentContextInvocation<TScope> {
1873
+ readonly url: URL;
1874
+ readonly abortSignal?: AbortSignal;
1875
+ }
1876
+
1877
+ /**
1878
+ * Reads the attachments that prompts name by a URL of the host's own scheme, such as
1879
+ * `exampleattachment:3f2a`, instead of carrying their content. The transcript keeps the URL, and
1880
+ * each model call that sends the attachment, compaction's included, reads its content here, so
1881
+ * neither the session's stores nor its prompts hold the bytes.
1882
+ */
1883
+ export interface IFlexAttachmentResolver<TScope = unknown> {
1884
+ /** The URL schemes it reads, without the colon, in lowercase: `exampleattachment`. */
1885
+ schemes: readonly string[];
1886
+ /**
1887
+ * Returns the attachment's content for the session asking. Check that the session may read it,
1888
+ * and throw when it may not or the attachment is gone: the model call fails with the error.
1889
+ */
1890
+ resolve(request: IFlexAttachmentRequest<TScope>): Promise<plugins.IAgentResolvedAttachment>;
1891
+ }
1892
+
1862
1893
  export interface IFlexHarnessOptions<TScope> {
1863
1894
  scopeResolver: IFlexScopeResolver<TScope>;
1864
1895
  modelResolver: IFlexModelResolver<TScope>;
@@ -1867,6 +1898,8 @@ export interface IFlexHarnessOptions<TScope> {
1867
1898
  resourceToolProviderResolver?: IFlexResourceToolProviderResolver<TScope>;
1868
1899
  executionContextProvider?: IFlexExecutionContextProvider<TScope>;
1869
1900
  delegatedRunAdmissionProvider?: IFlexDelegatedRunAdmissionProvider<TScope>;
1901
+ /** Lets prompts name attachments by reference; see {@link IFlexAttachmentResolver}. */
1902
+ attachmentResolver?: IFlexAttachmentResolver<TScope>;
1870
1903
  stores?: IFlexHarnessStores;
1871
1904
  agentSessionPolicy?: IFlexAgentSessionPolicy<TScope>;
1872
1905
  toolOutputLimits?: IFlexJsonLimits;
package/ts/plugins.ts CHANGED
@@ -96,6 +96,7 @@ import {
96
96
  AgentPendingInputQueue,
97
97
  applyAgentEventChange,
98
98
  assertAgentEventChange,
99
+ assertAttachmentResolver,
99
100
  AgentPendingInputRejectedError,
100
101
  AgentSession,
101
102
  AgentToolCallScheduler,
@@ -117,6 +118,7 @@ export {
117
118
  AgentPendingInputQueue,
118
119
  applyAgentEventChange,
119
120
  assertAgentEventChange,
121
+ assertAttachmentResolver,
120
122
  AgentPendingInputRejectedError,
121
123
  AgentSession,
122
124
  AgentToolCallScheduler,
@@ -134,6 +136,7 @@ export {
134
136
  };
135
137
  export type {
136
138
  IAgentAppliedInput,
139
+ IAgentResolvedAttachment,
137
140
  ISmartAiCacheDispatch,
138
141
  IAgentEventArchive,
139
142
  IAgentEventChange,
package/ts/readme.md CHANGED
@@ -578,10 +578,32 @@ console.log(result.usage);
578
578
  `TFlexPrompt` is deliberately JSON-safe. It accepts a string or an ordered array of:
579
579
 
580
580
  - `{ type: 'text', text }`
581
- - `{ type: 'image', data, mediaType?, name? }`
582
- - `{ type: 'file', data, mediaType, name? }`
581
+ - `{ type: 'image', data, mediaType?, name?, sizeBytes? }`
582
+ - `{ type: 'file', data, mediaType, name?, sizeBytes? }`
583
583
 
584
- Attachment `data` is a string containing base64, a data URL, or a remote URL. Public input never requires `Buffer` or `URL` objects. Remote URL strings are converted only at the private AgentSession invocation boundary.
584
+ Attachment `data` is a string containing base64, a data URL, a remote URL, or a reference. Public input never requires `Buffer` or `URL` objects. Remote URL strings are converted only at the private AgentSession invocation boundary.
585
+
586
+ A reference is a URL of a scheme the harness's `attachmentResolver` reads, for files the host keeps itself, such as uploads too large to travel inline:
587
+
588
+ ```typescript
589
+ const harness = new FlexHarness({
590
+ // ...
591
+ attachmentResolver: {
592
+ schemes: ['exampleattachment'],
593
+ async resolve({ scopeId, scope, storageKey, sessionId, url, abortSignal }) {
594
+ const upload = await uploads.read(storageKey, sessionId, url.pathname, abortSignal);
595
+ return { data: upload.bytes, mediaType: upload.mediaType };
596
+ },
597
+ },
598
+ });
599
+
600
+ await harness.prompt(scopeId, sessionId, [
601
+ { type: 'text', text: 'Book this receipt.' },
602
+ { type: 'file', data: 'exampleattachment:3f2a', mediaType: 'application/pdf', name: 'receipt.pdf', sizeBytes: 48231 },
603
+ ]);
604
+ ```
605
+
606
+ The history keeps the reference, never the bytes. Each model call that sends it, the turns' and a compaction's summary call alike, asks the resolver for the bytes and sends them to the provider; the steps of one turn ask once per reference. The resolver receives the scope and session the reference is sent in, so it reads only that conversation's files, and a reference it cannot read fails the turn. Schemes are lowercase, without the colon, and cannot be `data`, `file`, `http` or `https`; the harness refuses a malformed resolver with `FlexHarnessValidationError` when it is made. `sizeBytes`, which a reference states because the harness cannot count its bytes, is accepted on references only; a URL of a scheme the resolver does not name is no reference and is sent as before.
585
607
 
586
608
  Attachment payloads are never copied into public audit messages or events. Public attachment parts contain metadata only:
587
609
 
@@ -590,7 +612,7 @@ Attachment payloads are never copied into public audit messages or events. Publi
590
612
  type: 'attachment',
591
613
  partId: '...',
592
614
  attachmentType: 'file',
593
- source: 'inline-base64', // or data-url / remote-url
615
+ source: 'inline-base64', // or data-url / remote-url / reference
594
616
  sizeBytes: 48231, // omitted when it cannot be determined
595
617
  mediaType: 'application/pdf',
596
618
  name: 'invoice.pdf',
@@ -1038,7 +1060,7 @@ A `contextCompactor` states which model to compact with from the model the compa
1038
1060
 
1039
1061
  A compactor uses `resolvedModel.model` directly, and resolves `modelHint` the way its model resolver resolves a prompt's `modelHint`. Only when neither member is present, because nothing dispatched states a model, does the host choose the compaction model itself.
1040
1062
 
1041
- A `context-overflow` or `context-budget` compaction also carries `system` and `providerOptions`: the instructions and the provider options (cache options applied, so `store: false` on OpenAI) the run's turns are sent with. A `context-budget` compaction on a provider that caches the prefix also carries the turns' `tools`. `compactMessages()` sends its summary call with them as one streamed call, the shape of every turn, with the tools' definitions but none of their code, so a compactor that passes its options through repeats the run's prefix and reads it from the provider's prompt cache; definitions that outweigh the history nine times are left out. Without `system` it sends its own instructions.
1063
+ A `context-overflow` or `context-budget` compaction also carries `system` and `providerOptions`: the instructions and the provider options (cache options applied, so `store: false` on OpenAI) the run's turns are sent with. A `context-budget` compaction on a provider that caches the prefix also carries the turns' `tools`. `compactMessages()` sends its summary call with them as one streamed call, the shape of every turn, with the tools' definitions but none of their code, so a compactor that passes its options through repeats the run's prefix and reads it from the provider's prompt cache; definitions that outweigh the history nine times are left out. Without `system` it sends its own instructions. With an `attachmentResolver` on the harness, every compaction also carries `attachmentResolver`, bound to the session, so `compactMessages()` sends the history's referenced files as the turns do.
1042
1064
 
1043
1065
  ```typescript
1044
1066
  agentSessionPolicy: {
@@ -1,4 +1,5 @@
1
1
  import { FlexHarnessValidationError } from './errors.js';
2
+ import * as plugins from './plugins.js';
2
3
  import {
3
4
  FLEX_CODE_MODE_DEFAULT_LIMITS,
4
5
  FLEX_CODE_MODE_MAXIMUM_LIMITS,
@@ -10,6 +11,7 @@ import {
10
11
  } from './interfaces.js';
11
12
  import type {
12
13
  IFlexAgentSessionPolicy,
14
+ IFlexAttachmentResolver,
13
15
  IFlexCallbackLimits,
14
16
  IFlexHarnessOptions,
15
17
  IFlexHarnessStores,
@@ -500,6 +502,19 @@ export function normalizeAgentSessionPolicy<TScope>(
500
502
  };
501
503
  }
502
504
 
505
+ /** The URL protocols, colon included, of the references an attachment resolver reads. */
506
+ export function normalizeAttachmentResolver<TScope>(
507
+ resolver: IFlexAttachmentResolver<TScope> | undefined,
508
+ ): ReadonlySet<string> {
509
+ if (resolver === undefined) return new Set();
510
+ try {
511
+ plugins.assertAttachmentResolver(resolver);
512
+ } catch (error) {
513
+ throw new FlexHarnessValidationError((error as Error).message, { cause: error });
514
+ }
515
+ return new Set(resolver.schemes.map((scheme) => `${scheme}:`));
516
+ }
517
+
503
518
  export function requireHarnessStores(stores: IFlexHarnessStores): IFlexHarnessStores {
504
519
  const projectManagement = stores?.projectManagement;
505
520
  if (
package/ts/utils.json.ts CHANGED
@@ -864,7 +864,7 @@ function validateMessage(value: unknown, path: string): IValidatedMessageIdentit
864
864
  if (!['image', 'file'].includes(String(part.attachmentType))) {
865
865
  throw new FlexHarnessStoreFormatError(`${partPath}.attachmentType is invalid.`);
866
866
  }
867
- if (!['inline-base64', 'data-url', 'remote-url'].includes(String(part.source))) {
867
+ if (!['inline-base64', 'data-url', 'remote-url', 'reference'].includes(String(part.source))) {
868
868
  throw new FlexHarnessStoreFormatError(`${partPath}.source is invalid.`);
869
869
  }
870
870
  if (part.sizeBytes !== undefined) {
@@ -52,7 +52,39 @@ function toAgentData(data: string): string | URL {
52
52
  return data;
53
53
  }
54
54
 
55
- function validatePart(value: unknown, index: number): TFlexPromptPart {
55
+ /** Whether `data` is a reference: a URL of one of `referenceProtocols`. */
56
+ export function isAttachmentReference(data: string, referenceProtocols: ReadonlySet<string>): boolean {
57
+ if (referenceProtocols.size === 0) return false;
58
+ try {
59
+ return referenceProtocols.has(new URL(data).protocol);
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ function validateSizeBytes(
66
+ value: Record<string, unknown>,
67
+ data: string,
68
+ referenceProtocols: ReadonlySet<string>,
69
+ path: string,
70
+ ): number | undefined {
71
+ if (value.sizeBytes === undefined) return undefined;
72
+ if (!isAttachmentReference(data, referenceProtocols)) {
73
+ throw new FlexHarnessValidationError(
74
+ `${path}.sizeBytes is supported only for an attachment reference.`,
75
+ );
76
+ }
77
+ if (!Number.isSafeInteger(value.sizeBytes) || (value.sizeBytes as number) < 0) {
78
+ throw new FlexHarnessValidationError(`${path}.sizeBytes must be a non-negative integer.`);
79
+ }
80
+ return value.sizeBytes as number;
81
+ }
82
+
83
+ function validatePart(
84
+ value: unknown,
85
+ index: number,
86
+ referenceProtocols: ReadonlySet<string>,
87
+ ): TFlexPromptPart {
56
88
  const path = `prompt[${index}]`;
57
89
  if (!isPlainObject(value)) {
58
90
  throw new FlexHarnessValidationError(`${path} must be a plain object.`);
@@ -65,26 +97,30 @@ function validatePart(value: unknown, index: number): TFlexPromptPart {
65
97
  };
66
98
  }
67
99
  if (value.type === 'image') {
68
- rejectUnknownKeys(value, ['type', 'data', 'mediaType', 'name'], path);
100
+ rejectUnknownKeys(value, ['type', 'data', 'mediaType', 'name', 'sizeBytes'], path);
69
101
  const part: IFlexImagePromptPart = {
70
102
  type: 'image',
71
103
  data: requireNonEmptyString(value.data, `${path}.data`),
72
104
  };
73
105
  const mediaType = validateOptionalString(value.mediaType, `${path}.mediaType`);
74
106
  const name = validateOptionalString(value.name, `${path}.name`);
107
+ const sizeBytes = validateSizeBytes(value, part.data, referenceProtocols, path);
75
108
  if (mediaType) part.mediaType = mediaType;
76
109
  if (name) part.name = name;
110
+ if (sizeBytes !== undefined) part.sizeBytes = sizeBytes;
77
111
  return part;
78
112
  }
79
113
  if (value.type === 'file') {
80
- rejectUnknownKeys(value, ['type', 'data', 'mediaType', 'name'], path);
114
+ rejectUnknownKeys(value, ['type', 'data', 'mediaType', 'name', 'sizeBytes'], path);
81
115
  const part: IFlexFilePromptPart = {
82
116
  type: 'file',
83
117
  data: requireNonEmptyString(value.data, `${path}.data`),
84
118
  mediaType: requireNonEmptyString(value.mediaType, `${path}.mediaType`),
85
119
  };
86
120
  const name = validateOptionalString(value.name, `${path}.name`);
121
+ const sizeBytes = validateSizeBytes(value, part.data, referenceProtocols, path);
87
122
  if (name) part.name = name;
123
+ if (sizeBytes !== undefined) part.sizeBytes = sizeBytes;
88
124
  return part;
89
125
  }
90
126
  throw new FlexHarnessValidationError(`${path}.type must be text, image, or file.`);
@@ -122,7 +158,14 @@ function toStoredModelParts(parts: TFlexPromptPart[]): TFlexAgentPrompt {
122
158
  }) as TFlexAgentPrompt;
123
159
  }
124
160
 
125
- export function normalizeFlexPrompt(prompt: TFlexPrompt): INormalizedFlexPrompt {
161
+ /**
162
+ * Validates a prompt. `referenceProtocols` are the URL protocols, colon included, of the
163
+ * attachment references the harness's attachment resolver reads.
164
+ */
165
+ export function normalizeFlexPrompt(
166
+ prompt: TFlexPrompt,
167
+ referenceProtocols: ReadonlySet<string>,
168
+ ): INormalizedFlexPrompt {
126
169
  try {
127
170
  assertJsonSerializable(prompt, '$prompt');
128
171
  } catch (error) {
@@ -142,7 +185,7 @@ export function normalizeFlexPrompt(prompt: TFlexPrompt): INormalizedFlexPrompt
142
185
  if (!Array.isArray(prompt) || prompt.length === 0) {
143
186
  throw new FlexHarnessValidationError('prompt must be a string or a non-empty array of parts.');
144
187
  }
145
- const parts = prompt.map(validatePart);
188
+ const parts = prompt.map((part, index) => validatePart(part, index, referenceProtocols));
146
189
  return {
147
190
  modelMessage: {
148
191
  role: 'user',
@@ -10,6 +10,7 @@ import type {
10
10
  TFlexPromptPart,
11
11
  } from './interfaces.js';
12
12
  import { cloneSerializable, deepFreeze } from './utils.json.js';
13
+ import { isAttachmentReference } from './utils.prompt.js';
13
14
  import { validateIdentifier } from './utils.validation.js';
14
15
 
15
16
  export interface IRunResultProjection {
@@ -126,11 +127,15 @@ function base64Size(data: string): number | undefined {
126
127
 
127
128
  export function describeAttachment(
128
129
  part: Exclude<TFlexPromptPart, { type: 'text' }>,
130
+ referenceProtocols: ReadonlySet<string>,
129
131
  ): Omit<IFlexAttachmentMessagePart, 'partId' | 'type' | 'attachmentType'> {
130
132
  let source: TFlexAttachmentSource = 'inline-base64';
131
133
  let sizeBytes: number | undefined;
132
134
  let inferredMediaType: string | undefined;
133
- if (/^data:/i.test(part.data)) {
135
+ if (isAttachmentReference(part.data, referenceProtocols)) {
136
+ source = 'reference';
137
+ sizeBytes = part.sizeBytes;
138
+ } else if (/^data:/i.test(part.data)) {
134
139
  source = 'data-url';
135
140
  const commaIndex = part.data.indexOf(',');
136
141
  if (commaIndex >= 0) {
@@ -45,6 +45,12 @@ export interface IFlexHarnessSendIntentOptions {
45
45
  * accounts or reasoning efforts encodes them in its own hint scheme here.
46
46
  */
47
47
  modelHint?: string;
48
+ /**
49
+ * The prompt parts of the message's attachments, in their order, as the host made them, such as
50
+ * references to the files the tab uploaded to it. Without them, each attachment is sent with the
51
+ * content the composer read.
52
+ */
53
+ attachmentParts?: readonly TFlexPromptPart[];
48
54
  }
49
55
 
50
56
  function base64Utf8(text: string): string {
@@ -103,9 +109,12 @@ export function flexHarnessSendIntent(
103
109
  detail: plugins.IHarnessComposerSendDetail,
104
110
  options: IFlexHarnessSendIntentOptions = {},
105
111
  ): TFlexHarnessChatIntent {
112
+ if (options.attachmentParts && options.attachmentParts.length !== detail.attachments.length) {
113
+ throw new Error('Each attachment needs one prompt part.');
114
+ }
106
115
  const prompt: TFlexPromptPart[] = [
107
116
  ...(detail.text.length > 0 ? [{ type: 'text' as const, text: detail.text }] : []),
108
- ...detail.attachments.map(attachmentToPromptPart),
117
+ ...(options.attachmentParts ?? detail.attachments.map(attachmentToPromptPart)),
109
118
  ];
110
119
  const modelHint = options.modelHint ?? detail.model;
111
120
  const promptOptions: IFlexPromptOptions = modelHint === undefined ? {} : { modelHint };
@@ -89,7 +89,7 @@ The intent functions map component events to plain `TFlexHarnessChatIntent` valu
89
89
 
90
90
  Intents are untrusted client input: a client can name any `scopeId` and `sessionId`. Before calling `runFlexHarnessChatIntent`, the server authenticates the caller and checks, or replaces, the intent's target with a session the caller may act on. The harness itself checks only that a permission belongs to the session the intent names, not who is asking.
91
91
 
92
- - `harness-send`: text and attachments become prompt parts. A text attachment is sent as a UTF-8 file part; when its media type does not already name text (`text/*`, JSON, XML, YAML, CSV or a `+json`, `+xml` or `+yaml` type), it gains `charset=utf-8`, for example `application/x-openscad; charset=utf-8`, so a provider that reads only some files natively can still send it as text; a truncated attachment, or one without a payload, throws. A message sent while idle, queued behind the running turn, or delivered as `next-turn` becomes `enqueuePrompt`. A steer joins the running turn, `transcript.activeQueueId`, through `steerPrompt`, and answers on that turn's model; with no running turn, or when its turn ended before the harness took it, it is the next turn's input and is queued with the prompt options. A steer id comes from `crypto.randomUUID()`, which browsers offer only in secure contexts (HTTPS or localhost). The composer's `model` becomes the prompt's `modelHint`; a host that offers accounts or reasoning efforts passes its own `modelHint`.
92
+ - `harness-send`: text and attachments become prompt parts. A text attachment is sent as a UTF-8 file part; when its media type does not already name text (`text/*`, JSON, XML, YAML, CSV or a `+json`, `+xml` or `+yaml` type), it gains `charset=utf-8`, for example `application/x-openscad; charset=utf-8`, so a provider that reads only some files natively can still send it as text; a truncated attachment, or one without a payload, throws. A host that makes the attachments' prompt parts itself, such as references to files the tab uploaded to it, passes them as `attachmentParts`, one per attachment in order. A message sent while idle, queued behind the running turn, or delivered as `next-turn` becomes `enqueuePrompt`. A steer joins the running turn, `transcript.activeQueueId`, through `steerPrompt`, and answers on that turn's model; with no running turn, or when its turn ended before the harness took it, it is the next turn's input and is queued with the prompt options. A steer id comes from `crypto.randomUUID()`, which browsers offer only in secure contexts (HTTPS or localhost). The composer's `model` becomes the prompt's `modelHint`; a host that offers accounts or reasoning efforts passes its own `modelHint`.
93
93
  - `harness-permission-response`: `reject` rejects; an approval with `remember` is `always`, otherwise `once`. A request with `requiresExplicitResponse` refuses `always`.
94
94
  - `harness-abort`: `abort()` of the session.
95
95
 
@@ -87,7 +87,11 @@ export class FlexHarnessChatHub<TContext> {
87
87
  this.titleOf = optionsArg.titleOf ?? titleFromMessage;
88
88
  this.directory = optionsArg.directory ?? new FlexHarnessChatScopeDirectory(this.harness);
89
89
  this.listsFromHarness = !optionsArg.directory;
90
- this.gate = new FlexHarnessChatTurnGate<TContext>(this.harness, this.limits);
90
+ this.gate = new FlexHarnessChatTurnGate<TContext>(
91
+ this.harness,
92
+ this.limits,
93
+ optionsArg.attachmentPart?.bind(optionsArg),
94
+ );
91
95
  this.sessionLists = new FlexHarnessChatSessionLists(this.peers);
92
96
  this.unsubscribe = this.harness.subscribe((eventArg) => this.takeEvent(eventArg));
93
97
  this.sweepTimer = setInterval(() => void this.forgetClosedTabs(), this.limits.sweepIntervalMs);