@modelprofile.com/flexharness 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.smartconfig.json +34 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.flexharness.d.ts +61 -0
- package/dist_ts/classes.flexharness.js +1312 -0
- package/dist_ts/classes.stores.d.ts +26 -0
- package/dist_ts/classes.stores.js +156 -0
- package/dist_ts/errors.d.ts +44 -0
- package/dist_ts/errors.js +105 -0
- package/dist_ts/index.d.ts +5 -0
- package/dist_ts/index.js +6 -0
- package/dist_ts/interfaces.d.ts +288 -0
- package/dist_ts/interfaces.js +2 -0
- package/dist_ts/plugins.d.ts +7 -0
- package/dist_ts/plugins.js +9 -0
- package/dist_ts/utils.json.d.ts +8 -0
- package/dist_ts/utils.json.js +540 -0
- package/dist_ts/utils.prompt.d.ts +8 -0
- package/dist_ts/utils.prompt.js +170 -0
- package/license.md +21 -0
- package/package.json +36 -0
- package/readme.hints.md +29 -0
- package/readme.md +285 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.flexharness.ts +1648 -0
- package/ts/classes.stores.ts +195 -0
- package/ts/errors.ts +125 -0
- package/ts/index.ts +5 -0
- package/ts/interfaces.ts +369 -0
- package/ts/plugins.ts +15 -0
- package/ts/utils.json.ts +651 -0
- package/ts/utils.prompt.ts +193 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { FlexHarnessValidationError } from './errors.js';
|
|
2
|
+
import { cloneSerializable } from './utils.json.js';
|
|
3
|
+
import { assertJsonSerializable } from './utils.json.js';
|
|
4
|
+
function isPlainObject(value) {
|
|
5
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const prototype = Object.getPrototypeOf(value);
|
|
9
|
+
return prototype === Object.prototype || prototype === null;
|
|
10
|
+
}
|
|
11
|
+
function requireNonEmptyString(value, path) {
|
|
12
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
13
|
+
throw new FlexHarnessValidationError(`${path} must be a non-empty string.`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function validateOptionalString(value, path) {
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return requireNonEmptyString(value, path);
|
|
22
|
+
}
|
|
23
|
+
function toAgentData(data) {
|
|
24
|
+
try {
|
|
25
|
+
const url = new URL(data);
|
|
26
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
27
|
+
return url;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Plain base64 and data URLs stay strings at the SmartAgent boundary.
|
|
32
|
+
}
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
function validatePart(value, index) {
|
|
36
|
+
const path = `prompt[${index}]`;
|
|
37
|
+
if (!isPlainObject(value)) {
|
|
38
|
+
throw new FlexHarnessValidationError(`${path} must be a plain object.`);
|
|
39
|
+
}
|
|
40
|
+
if (value.type === 'text') {
|
|
41
|
+
rejectUnknownKeys(value, ['type', 'text'], path);
|
|
42
|
+
return {
|
|
43
|
+
type: 'text',
|
|
44
|
+
text: requireNonEmptyString(value.text, `${path}.text`),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (value.type === 'image') {
|
|
48
|
+
rejectUnknownKeys(value, ['type', 'data', 'mediaType', 'name'], path);
|
|
49
|
+
const part = {
|
|
50
|
+
type: 'image',
|
|
51
|
+
data: requireNonEmptyString(value.data, `${path}.data`),
|
|
52
|
+
};
|
|
53
|
+
const mediaType = validateOptionalString(value.mediaType, `${path}.mediaType`);
|
|
54
|
+
const name = validateOptionalString(value.name, `${path}.name`);
|
|
55
|
+
if (mediaType)
|
|
56
|
+
part.mediaType = mediaType;
|
|
57
|
+
if (name)
|
|
58
|
+
part.name = name;
|
|
59
|
+
return part;
|
|
60
|
+
}
|
|
61
|
+
if (value.type === 'file') {
|
|
62
|
+
rejectUnknownKeys(value, ['type', 'data', 'mediaType', 'name'], path);
|
|
63
|
+
const part = {
|
|
64
|
+
type: 'file',
|
|
65
|
+
data: requireNonEmptyString(value.data, `${path}.data`),
|
|
66
|
+
mediaType: requireNonEmptyString(value.mediaType, `${path}.mediaType`),
|
|
67
|
+
};
|
|
68
|
+
const name = validateOptionalString(value.name, `${path}.name`);
|
|
69
|
+
if (name)
|
|
70
|
+
part.name = name;
|
|
71
|
+
return part;
|
|
72
|
+
}
|
|
73
|
+
throw new FlexHarnessValidationError(`${path}.type must be text, image, or file.`);
|
|
74
|
+
}
|
|
75
|
+
function rejectUnknownKeys(value, allowedKeys, path) {
|
|
76
|
+
const unknownKey = Object.keys(value).find((key) => !allowedKeys.includes(key));
|
|
77
|
+
if (unknownKey) {
|
|
78
|
+
throw new FlexHarnessValidationError(`${path}.${unknownKey} is not a supported prompt field.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function toAgentParts(parts) {
|
|
82
|
+
return parts.map((part) => {
|
|
83
|
+
if (part.type === 'text') {
|
|
84
|
+
return { type: 'text', text: part.text };
|
|
85
|
+
}
|
|
86
|
+
if (part.type === 'image') {
|
|
87
|
+
return {
|
|
88
|
+
type: 'image',
|
|
89
|
+
image: toAgentData(part.data),
|
|
90
|
+
...(part.mediaType ? { mediaType: part.mediaType } : {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
type: 'file',
|
|
95
|
+
data: toAgentData(part.data),
|
|
96
|
+
mediaType: part.mediaType,
|
|
97
|
+
...(part.name ? { filename: part.name } : {}),
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function toStoredModelParts(parts) {
|
|
102
|
+
return parts.map((part) => {
|
|
103
|
+
if (part.type === 'text') {
|
|
104
|
+
return { type: 'text', text: part.text };
|
|
105
|
+
}
|
|
106
|
+
if (part.type === 'image') {
|
|
107
|
+
return {
|
|
108
|
+
type: 'image',
|
|
109
|
+
image: part.data,
|
|
110
|
+
...(part.mediaType ? { mediaType: part.mediaType } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
type: 'file',
|
|
115
|
+
data: part.data,
|
|
116
|
+
mediaType: part.mediaType,
|
|
117
|
+
...(part.name ? { filename: part.name } : {}),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
export function normalizeFlexPrompt(prompt) {
|
|
122
|
+
try {
|
|
123
|
+
assertJsonSerializable(prompt, '$prompt');
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
throw new FlexHarnessValidationError('prompt must contain only JSON-safe values.', {
|
|
127
|
+
cause: error,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (typeof prompt === 'string') {
|
|
131
|
+
if (prompt.length === 0) {
|
|
132
|
+
throw new FlexHarnessValidationError('prompt must not be empty.');
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
agentPrompt: prompt,
|
|
136
|
+
modelMessage: { role: 'user', content: prompt },
|
|
137
|
+
parts: [{ type: 'text', text: prompt }],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (!Array.isArray(prompt) || prompt.length === 0) {
|
|
141
|
+
throw new FlexHarnessValidationError('prompt must be a string or a non-empty array of parts.');
|
|
142
|
+
}
|
|
143
|
+
const parts = prompt.map(validatePart);
|
|
144
|
+
return {
|
|
145
|
+
agentPrompt: toAgentParts(parts),
|
|
146
|
+
modelMessage: {
|
|
147
|
+
role: 'user',
|
|
148
|
+
content: toStoredModelParts(parts),
|
|
149
|
+
},
|
|
150
|
+
parts: cloneSerializable(parts),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function hydrateAgentMessages(messages) {
|
|
154
|
+
const hydrated = cloneSerializable(messages);
|
|
155
|
+
for (const message of hydrated) {
|
|
156
|
+
if (!Array.isArray(message.content)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
for (const part of message.content) {
|
|
160
|
+
if (message.role === 'user' && part.type === 'image' && typeof part.image === 'string') {
|
|
161
|
+
part.image = toAgentData(part.image);
|
|
162
|
+
}
|
|
163
|
+
else if (part.type === 'file' && typeof part.data === 'string') {
|
|
164
|
+
part.data = toAgentData(part.data);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return hydrated;
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMucHJvbXB0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvdXRpbHMucHJvbXB0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxNQUFNLGFBQWEsQ0FBQztBQVN6RCxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUNwRCxPQUFPLEVBQUUsc0JBQXNCLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQVF6RCxTQUFTLGFBQWEsQ0FBQyxLQUFjO0lBQ25DLElBQUksQ0FBQyxLQUFLLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLEtBQUssQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUNoRSxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUM7SUFDRCxNQUFNLFNBQVMsR0FBRyxNQUFNLENBQUMsY0FBYyxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQy9DLE9BQU8sU0FBUyxLQUFLLE1BQU0sQ0FBQyxTQUFTLElBQUksU0FBUyxLQUFLLElBQUksQ0FBQztBQUM5RCxDQUFDO0FBRUQsU0FBUyxxQkFBcUIsQ0FBQyxLQUFjLEVBQUUsSUFBWTtJQUN6RCxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ3BELE1BQU0sSUFBSSwwQkFBMEIsQ0FBQyxHQUFHLElBQUksOEJBQThCLENBQUMsQ0FBQztJQUM5RSxDQUFDO0lBQ0QsT0FBTyxLQUFLLENBQUM7QUFDZixDQUFDO0FBRUQsU0FBUyxzQkFBc0IsQ0FBQyxLQUFjLEVBQUUsSUFBWTtJQUMxRCxJQUFJLEtBQUssS0FBSyxTQUFTLEVBQUUsQ0FBQztRQUN4QixPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0lBQ0QsT0FBTyxxQkFBcUIsQ0FBQyxLQUFLLEVBQUUsSUFBSSxDQUFDLENBQUM7QUFDNUMsQ0FBQztBQUVELFNBQVMsV0FB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
|
package/license.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Task Venture Capital GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@modelprofile.com/flexharness",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.",
|
|
6
|
+
"main": "dist_ts/index.js",
|
|
7
|
+
"typings": "dist_ts/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"author": "Task Venture Capital GmbH",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=24"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@push.rocks/smartagent": "^4.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@git.zone/tsbuild": "4.4.2",
|
|
19
|
+
"@git.zone/tsrun": "2.0.6",
|
|
20
|
+
"@git.zone/tstest": "4.0.0",
|
|
21
|
+
"@types/json-schema": "7.0.15",
|
|
22
|
+
"@types/node": "26.1.2"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"ts/**/*",
|
|
26
|
+
"dist_ts/**/*",
|
|
27
|
+
".smartconfig.json",
|
|
28
|
+
"readme.md",
|
|
29
|
+
"license.md"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsbuild tsfolders",
|
|
33
|
+
"test": "pnpm run build && tstest test/ --verbose --logfile",
|
|
34
|
+
"check:test": "tsbuild check 'test/**/*'"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/readme.hints.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# readme.hints.md
|
|
2
|
+
|
|
3
|
+
Implementation findings for flexharness.
|
|
4
|
+
|
|
5
|
+
## SmartAgent boundary
|
|
6
|
+
|
|
7
|
+
- SmartAgent is the only external runtime dependency. FlexHarness imports only `@push.rocks/smartagent`; its model, prompt, message, provider-option, tool-set, runner-option, and runner-result aliases are derived from SmartAgent's exported `IAgentRunOptions` and `IAgentRunResult`.
|
|
8
|
+
- Public prompts remain JSON-safe. URL strings are converted to `URL` instances only at the private SmartAgent invocation boundary.
|
|
9
|
+
- Prompt attachment payloads exist only in the private SmartAgent prompt and, for successful turns, persisted model history. Public messages, prompt results, store audit messages, and events project them to `attachmentType`, source kind, optional media/name, and decoded size when determinable.
|
|
10
|
+
- Resolver calls start in promise continuations so synchronous throws are observed. The first failure aborts the shared internal signal without awaiting an ignoring sibling; detached tool-provider settlement is observed and late handles are closed.
|
|
11
|
+
|
|
12
|
+
## Persistence boundary
|
|
13
|
+
|
|
14
|
+
- Snapshot schema version 1 uses optimistic revisions. Harness mutations are serialized per resolved storage key, while the JSON store adds a process-wide per-file queue shared by all instances.
|
|
15
|
+
- Every queued mutation snapshots `revision` and persistent sessions first. Mutation, schema, save, and CAS failures restore that snapshot without replacing `activeRuns`, pending permission objects, or the save queue. Code that crosses the save `await` must re-fetch session records because restoration intentionally recreates them.
|
|
16
|
+
- `JsonFileFlexHarnessStore` is intentionally not cross-process safe. It provides atomic rename and in-process CAS, not an operating-system lock.
|
|
17
|
+
- Active persisted session states are normalized to idle when loaded. Incomplete messages and tool parts are marked cancelled in memory so a restarted process never presents them as still running.
|
|
18
|
+
- Session update/delete checks runtime runs and pending permissions inside the queued mutation. Deletion removes the entire stored session record, including private history and remembered grants.
|
|
19
|
+
- Tool close settles before success/history is decided. Final persistence is still attempted after original or close failure. A final save failure rolls back the store mutation, then terminalizes only the current in-memory audit before emitting terminal events once.
|
|
20
|
+
- Disposal waits finalizers and every loaded state save tail before clearing listeners, session histories, and the state-load cache.
|
|
21
|
+
- Terminal persistence is the cancellation linearization point. Abort returns false once a run starts committing, while the active-run entry continues to block new prompts until the commit settles.
|
|
22
|
+
- Permission callbacks wait for the current save tail and then revalidate their run before reading remembered grants, so neither uncommitted grants nor captured callbacks can authorize later work.
|
|
23
|
+
- Detached tool-provider settlement remains tracked after prompt finalization. Disposal awaits late handle closure and reports cleanup failure.
|
|
24
|
+
|
|
25
|
+
## Tool output boundary
|
|
26
|
+
|
|
27
|
+
- Every tool `execute` result and every async-iterable yield is converted to bounded JSON before SmartAgent observes it. Thrown errors and iterator failures are not converted or swallowed.
|
|
28
|
+
- JSON byte limits are propagated through traversal. Large strings never enter normalized output, and array/object traversal stops once the remaining allowance is reserved for deterministic truncation metadata.
|
|
29
|
+
- Model callbacks use bounded synchronous run-local parts. Adjacent text/reasoning deltas coalesce; no per-delta snapshot is written. Callback event, byte, or part overflow aborts internally and is classified as failure, not owner cancellation.
|
package/readme.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# @modelprofile.com/flexharness
|
|
2
|
+
|
|
3
|
+
Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.
|
|
4
|
+
|
|
5
|
+
## Issue Reporting and Security
|
|
6
|
+
|
|
7
|
+
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @modelprofile.com/flexharness
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Node.js 24 or newer is required.
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
FlexHarness owns session state, audit messages, successful model context, permission decisions, event delivery, cancellation, and persistence. Model selection and tool execution remain application-defined extension points. The package depends on SmartAgent but does not expose AI SDK or SmartAI imports as part of its API.
|
|
20
|
+
|
|
21
|
+
## Core Setup
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import {
|
|
25
|
+
FlexHarness,
|
|
26
|
+
JsonFileFlexHarnessStore,
|
|
27
|
+
type IFlexResolvedModel,
|
|
28
|
+
type TFlexAgentToolSet,
|
|
29
|
+
} from '@modelprofile.com/flexharness';
|
|
30
|
+
|
|
31
|
+
interface IProjectScope {
|
|
32
|
+
projectRoot: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const harness = new FlexHarness<IProjectScope>({
|
|
36
|
+
scopeResolver: {
|
|
37
|
+
async resolveScope(scopeId) {
|
|
38
|
+
const project = await projectRegistry.get(scopeId);
|
|
39
|
+
return {
|
|
40
|
+
// Aliases that resolve to this same key share sessions and save ordering.
|
|
41
|
+
storageKey: project.accountAndProjectKey,
|
|
42
|
+
scope: { projectRoot: project.root },
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
modelResolver: {
|
|
47
|
+
async resolveModel({ scope, modelHint, signal }): Promise<IFlexResolvedModel> {
|
|
48
|
+
const configured = await modelRegistry.resolve({ scope, modelHint, signal });
|
|
49
|
+
return {
|
|
50
|
+
model: configured.model,
|
|
51
|
+
identity: {
|
|
52
|
+
provider: configured.providerId,
|
|
53
|
+
model: configured.modelId,
|
|
54
|
+
displayName: configured.label,
|
|
55
|
+
},
|
|
56
|
+
providerOptions: configured.providerOptions,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
toolProvider: {
|
|
61
|
+
async provideTools(context) {
|
|
62
|
+
const tools: TFlexAgentToolSet = await createProjectTools({
|
|
63
|
+
root: context.scope.projectRoot,
|
|
64
|
+
signal: context.signal,
|
|
65
|
+
requestPermission: context.requestPermission,
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
tools,
|
|
69
|
+
close: async () => closeProjectTools(tools),
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
store: new JsonFileFlexHarnessStore({
|
|
74
|
+
directory: '/var/lib/my-app/model-sessions',
|
|
75
|
+
}),
|
|
76
|
+
toolOutputLimits: {
|
|
77
|
+
maxDepth: 12,
|
|
78
|
+
maxBytes: 256 * 1024,
|
|
79
|
+
},
|
|
80
|
+
callbackLimits: {
|
|
81
|
+
maxEvents: 10_000,
|
|
82
|
+
maxOutputBytes: 1024 * 1024,
|
|
83
|
+
maxParts: 2_000,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`modelRegistry`, `projectRegistry`, `createProjectTools`, and `closeProjectTools` in this example are application-owned integrations. FlexHarness passes the same run `AbortSignal` to the model resolver and tool provider.
|
|
89
|
+
|
|
90
|
+
## Sessions And Prompts
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const session = await harness.createSession('project:billing', {
|
|
94
|
+
title: 'Invoice import',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const result = await harness.prompt(
|
|
98
|
+
'project:billing',
|
|
99
|
+
session.sessionId,
|
|
100
|
+
[
|
|
101
|
+
{ type: 'text', text: 'Extract the invoice totals.' },
|
|
102
|
+
{
|
|
103
|
+
type: 'file',
|
|
104
|
+
data: invoicePdfBase64,
|
|
105
|
+
mediaType: 'application/pdf',
|
|
106
|
+
name: 'invoice.pdf',
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
{ modelHint: 'document-model', maxSteps: 12 },
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
console.log(result.assistantMessage.parts);
|
|
113
|
+
console.log(result.usage);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`TFlexPrompt` is deliberately JSON-safe. It accepts a string or an ordered array of:
|
|
117
|
+
|
|
118
|
+
- `{ type: 'text', text }`
|
|
119
|
+
- `{ type: 'image', data, mediaType?, name? }`
|
|
120
|
+
- `{ type: 'file', data, mediaType, name? }`
|
|
121
|
+
|
|
122
|
+
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 SmartAgent invocation boundary.
|
|
123
|
+
|
|
124
|
+
Attachment payloads are never copied into public audit messages or events. Public attachment parts contain metadata only:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
{
|
|
128
|
+
type: 'attachment',
|
|
129
|
+
partId: '...',
|
|
130
|
+
attachmentType: 'file',
|
|
131
|
+
source: 'inline-base64', // or data-url / remote-url
|
|
132
|
+
sizeBytes: 48231, // omitted when it cannot be determined
|
|
133
|
+
mediaType: 'application/pdf',
|
|
134
|
+
name: 'invoice.pdf',
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
When the turn succeeds, the original string remains only in private persisted model history so a later model turn can receive the attachment again. Failed, cancelled, resolver-failed, cleanup-failed, and persistence-failed turns do not add it to future context.
|
|
139
|
+
|
|
140
|
+
The main session methods are:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
await harness.listSessions(scopeId);
|
|
144
|
+
await harness.createSession(scopeId, { sessionId, title });
|
|
145
|
+
await harness.getSession(scopeId, sessionId);
|
|
146
|
+
await harness.getMessages(scopeId, sessionId);
|
|
147
|
+
await harness.updateSession(scopeId, sessionId, { title: 'Renamed', archived: true });
|
|
148
|
+
await harness.updateSession(scopeId, sessionId, { title: null, archived: false });
|
|
149
|
+
await harness.deleteSession(scopeId, sessionId);
|
|
150
|
+
await harness.prompt(scopeId, sessionId, prompt, options);
|
|
151
|
+
await harness.abort(scopeId, sessionId, 'Cancelled by the user');
|
|
152
|
+
await harness.listPendingPermissions(scopeId, sessionId);
|
|
153
|
+
await harness.respondToPermission(scopeId, sessionId, permissionId, 'once');
|
|
154
|
+
await harness.dispose();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Only one run may be active in a session. Different sessions can run concurrently. The run ID and audit messages are reserved and persisted before asynchronous model or tool resolution begins.
|
|
158
|
+
|
|
159
|
+
`updateSession()` supports title replacement, explicit title clearing with `null`, and archive state through `archived`. Archived sessions expose `archivedAt`. Update and deletion are rejected while the session has an active run or pending permission. Deletion removes the complete persisted session, including messages, private model history, and remembered permission grants.
|
|
160
|
+
|
|
161
|
+
`abort()` returns `true` only while cancellation is still accepted. Terminal persistence is the run's commit point; once it starts, `abort()` returns `false` and the already-fixed terminal outcome completes while the session remains busy.
|
|
162
|
+
|
|
163
|
+
## History And Audit Behavior
|
|
164
|
+
|
|
165
|
+
Successful model context is accumulated as:
|
|
166
|
+
|
|
167
|
+
1. Previous successful model history.
|
|
168
|
+
2. The normalized current user message.
|
|
169
|
+
3. SmartAgent's result messages.
|
|
170
|
+
|
|
171
|
+
A failed or cancelled prompt remains visible through `getMessages()`, with `failed` or `cancelled` status, but is not included in future model context. Model history is held only in the store snapshot and is not exposed by the session or message APIs.
|
|
172
|
+
|
|
173
|
+
Public audit history is safe to send to controllers: attachment parts contain source and size metadata, never inline base64, data URLs, or remote URL payloads. Private model history retains those values solely for subsequent model turns.
|
|
174
|
+
|
|
175
|
+
Sessions expose `idle`, `running`, `waiting_permission`, `failed`, and `cancelled` status. Persisted `running` and `waiting_permission` states normalize to `idle` after process restart; incomplete messages and parts normalize to `cancelled`.
|
|
176
|
+
|
|
177
|
+
## Permissions
|
|
178
|
+
|
|
179
|
+
Tools request permission through the run-scoped provider context:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
await context.requestPermission({
|
|
183
|
+
kind: 'filesystem.write',
|
|
184
|
+
description: 'Write generated files into the project',
|
|
185
|
+
toolCallId,
|
|
186
|
+
rememberKey: 'filesystem.write:project-output',
|
|
187
|
+
metadata: { target: 'generated/' },
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Pending requests are runtime-only and queryable with `listPendingPermissions()`. A controller answers with:
|
|
192
|
+
|
|
193
|
+
- `once`: allow this request.
|
|
194
|
+
- `always`: allow and remember the request's `rememberKey` for this session.
|
|
195
|
+
- `reject`: reject the tool execution.
|
|
196
|
+
|
|
197
|
+
`always` is invalid when the request has no `rememberKey`. Remembered decisions are persisted before the waiting tool resolves. If persistence fails, the key is rolled back and the request remains pending so the response can be retried. Concurrent response attempts are serialized and exactly one successful response settles a request.
|
|
198
|
+
|
|
199
|
+
## Tool Output Safety
|
|
200
|
+
|
|
201
|
+
FlexHarness wraps every provided tool `execute` method before SmartAgent receives it. Direct outputs and every `AsyncIterable` yield are converted into bounded JSON-safe values. Circular references, functions, symbols, bigint values, dates, URLs, and binary values receive deterministic descriptions or records. Thrown errors and iterator failures remain errors.
|
|
202
|
+
|
|
203
|
+
`toolOutputLimits` in the complete setup above bounds traversal depth and encoded bytes. The normalizer enforces its byte allowance incrementally: oversized strings are replaced before entering output, and arrays/objects stop reading entries once only truncation metadata fits.
|
|
204
|
+
|
|
205
|
+
Streaming callbacks use run-local synchronous state rather than one persistence promise per delta. Adjacent text and reasoning deltas coalesce. `callbackLimits` bounds callback events, accumulated output bytes, and part count; overflow aborts internally with `FlexHarnessCallbackOverflowError` and the turn is recorded as failed. Reservation and terminal finalization are the normal persistence checkpoints, with permission state changes as explicit additional checkpoints.
|
|
206
|
+
|
|
207
|
+
`normalizeJsonValue()` is also exported for integrations that need the same conversion independently.
|
|
208
|
+
|
|
209
|
+
## Events
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
const unsubscribe = harness.subscribe((event) => {
|
|
213
|
+
switch (event.type) {
|
|
214
|
+
case 'part.delta':
|
|
215
|
+
renderDelta(event.sessionId, event.messageId, event.partId, event.delta);
|
|
216
|
+
break;
|
|
217
|
+
case 'permission.requested':
|
|
218
|
+
showPermission(event.request);
|
|
219
|
+
break;
|
|
220
|
+
case 'session.updated':
|
|
221
|
+
renderSession(event.session);
|
|
222
|
+
break;
|
|
223
|
+
case 'session.deleted':
|
|
224
|
+
removeSession(event.sessionId);
|
|
225
|
+
break;
|
|
226
|
+
case 'run.finished':
|
|
227
|
+
markRunFinished(event.runId, event.status);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
unsubscribe();
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Events are discriminated, sequenced, deeply immutable snapshots. Listener exceptions are isolated from runs and other listeners. Events contain public IDs and snapshots only; they do not expose the resolved scope object, storage key, model object, or provider options.
|
|
236
|
+
|
|
237
|
+
## Stores
|
|
238
|
+
|
|
239
|
+
`InMemoryFlexHarnessStore` provides revision-based compare-and-swap behavior for tests and ephemeral processes.
|
|
240
|
+
|
|
241
|
+
`JsonFileFlexHarnessStore` stores one `sha256(storageKey).json` file per resolved key. It provides:
|
|
242
|
+
|
|
243
|
+
- Snapshot schema version 1 and optimistic revisions.
|
|
244
|
+
- Static process-wide queues shared by all store instances for the same absolute file.
|
|
245
|
+
- Revision re-reads inside the queue before every save.
|
|
246
|
+
- Atomic temporary-file write and rename.
|
|
247
|
+
- Directory mode `0700` and file mode `0600`, including existing paths.
|
|
248
|
+
- Stale temporary-file cleanup and strict snapshot validation.
|
|
249
|
+
|
|
250
|
+
The JSON file store is explicitly not cross-process safe. Use a custom `IFlexHarnessStore` backed by a database or another cross-process CAS mechanism when several processes write the same storage key.
|
|
251
|
+
|
|
252
|
+
Store conflicts are surfaced as `FlexHarnessStoreConflictError`; malformed, wrong-schema, or non-JSON snapshots are surfaced as `FlexHarnessStoreFormatError`. FlexHarness does not merge conflicts.
|
|
253
|
+
|
|
254
|
+
Every harness mutation snapshots the persistent session state inside its per-storage queue. If the mutation itself or `store.save()` fails, the in-memory revision and sessions are restored before the queue settles. Runtime run controllers, pending permission objects, and queue identity are preserved. CAS conflicts therefore expose neither an uncommitted create/update/delete nor an automatic merge.
|
|
255
|
+
|
|
256
|
+
## Shutdown
|
|
257
|
+
|
|
258
|
+
Model and tool resolution share the run signal. Synchronous throws are observed as resolver failures; the first failure aborts that signal and finalizes immediately without waiting for an unresponsive sibling. A detached tool provider that resolves later is observed and its handle is closed; disposal waits for that settlement and reports a late close failure.
|
|
259
|
+
|
|
260
|
+
Tool-handle close settles before a turn can be successful. Final persistence is attempted even when model execution or close fails. Cleanup failure prevents history append. If final persistence also fails, the stored reserved snapshot remains unchanged while the current in-memory audit is terminalized and terminal events are emitted exactly once.
|
|
261
|
+
|
|
262
|
+
`dispose()` is asynchronous and idempotent. It marks the harness closed, prevents operations waiting on persistence from reserving a run, aborts cancellable active runs, rejects pending permissions, waits for committing runs, all run finalizers, and state save tails, then clears listeners and loaded state caches. Multiple run or cleanup failures are reported through `FlexHarnessRunError`.
|
|
263
|
+
|
|
264
|
+
Cancellation is cooperative: model resolvers, tool providers, runners, tools, and cleanup functions must observe the supplied `AbortSignal` and settle their work. `dispose()` deliberately waits for owned work instead of abandoning resources. A process host that needs a hard shutdown deadline must enforce that deadline outside FlexHarness and terminate only the process it owns.
|
|
265
|
+
|
|
266
|
+
## License and Legal Information
|
|
267
|
+
|
|
268
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
|
|
269
|
+
|
|
270
|
+
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
271
|
+
|
|
272
|
+
### Trademarks
|
|
273
|
+
|
|
274
|
+
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
|
275
|
+
|
|
276
|
+
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
|
277
|
+
|
|
278
|
+
### Company Information
|
|
279
|
+
|
|
280
|
+
Task Venture Capital GmbH<br>
|
|
281
|
+
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
282
|
+
|
|
283
|
+
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
284
|
+
|
|
285
|
+
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* autocreated commitinfo by @push.rocks/commitinfo
|
|
3
|
+
*/
|
|
4
|
+
export const commitinfo = {
|
|
5
|
+
name: '@modelprofile.com/flexharness',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
description: 'Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.'
|
|
8
|
+
}
|