@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.core.d.ts +3 -0
- package/dist_ts/classes.flexharness.core.js +22 -2
- package/dist_ts/classes.flexharness.prompts.js +4 -4
- package/dist_ts/classes.flexharness.runs.js +2 -2
- package/dist_ts/classes.flexharness.slashcommands.js +2 -2
- package/dist_ts/classes.flexharness.subagents.js +2 -2
- package/dist_ts/interfaces.d.ts +32 -1
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +3 -3
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.harnessoptions.d.ts +3 -1
- package/dist_ts/utils.harnessoptions.js +14 -1
- package/dist_ts/utils.json.js +2 -2
- package/dist_ts/utils.prompt.d.ts +7 -1
- package/dist_ts/utils.prompt.js +38 -6
- package/dist_ts/utils.transfer.d.ts +1 -1
- package/dist_ts/utils.transfer.js +8 -3
- package/dist_ts_chat_harness/functions.intents.d.ts +6 -0
- package/dist_ts_chat_harness/functions.intents.js +5 -2
- package/dist_ts_chat_harness_server/classes.chathub.js +2 -2
- package/dist_ts_chat_harness_server/classes.turngate.d.ts +5 -2
- package/dist_ts_chat_harness_server/classes.turngate.js +31 -8
- package/dist_ts_chat_harness_server/index.d.ts +1 -1
- package/dist_ts_chat_harness_server/interfaces.d.ts +20 -0
- package/dist_ts_chat_harness_server/plugins.d.ts +1 -1
- package/dist_ts_compaction/plugins.d.ts +3 -3
- package/dist_ts_compaction/plugins.js +3 -3
- package/dist_ts_compaction/smartagent.compaction.d.ts +6 -0
- package/dist_ts_compaction/smartagent.compaction.js +4 -1
- package/package.json +3 -3
- package/readme.md +27 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.core.ts +21 -0
- package/ts/classes.flexharness.prompts.ts +3 -3
- package/ts/classes.flexharness.runs.ts +1 -1
- package/ts/classes.flexharness.slashcommands.ts +1 -1
- package/ts/classes.flexharness.subagents.ts +1 -1
- package/ts/interfaces.ts +34 -1
- package/ts/plugins.ts +3 -0
- package/ts/readme.md +27 -5
- package/ts/utils.harnessoptions.ts +15 -0
- package/ts/utils.json.ts +1 -1
- package/ts/utils.prompt.ts +48 -5
- package/ts/utils.transfer.ts +6 -1
- package/ts_chat_harness/functions.intents.ts +10 -1
- package/ts_chat_harness/readme.md +1 -1
- package/ts_chat_harness_server/classes.chathub.ts +5 -1
- package/ts_chat_harness_server/classes.turngate.ts +45 -7
- package/ts_chat_harness_server/index.ts +1 -0
- package/ts_chat_harness_server/interfaces.ts +21 -0
- package/ts_chat_harness_server/plugins.ts +1 -0
- package/ts_chat_harness_server/readme.md +2 -0
- package/ts_compaction/plugins.ts +11 -3
- 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,
|
|
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.
|
|
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.
|
|
95
|
-
"@modelprofile.com/flexharness-models": "12.
|
|
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,
|
|
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: {
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -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,
|
|
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) {
|
package/ts/utils.prompt.ts
CHANGED
|
@@ -52,7 +52,39 @@ function toAgentData(data: string): string | URL {
|
|
|
52
52
|
return data;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
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
|
-
|
|
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',
|
package/ts/utils.transfer.ts
CHANGED
|
@@ -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 (
|
|
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>(
|
|
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);
|