@modelprofile.com/flexharness 3.8.0 → 4.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/changelog.md +12 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.d.ts +52 -1
- package/dist_ts/classes.flexharness.js +736 -25
- package/dist_ts/classes.stores.d.ts +10 -1
- package/dist_ts/classes.stores.js +164 -2
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.d.ts +94 -0
- package/dist_ts/interfaces.js +12 -1
- package/dist_ts/utils.json.js +37 -3
- package/dist_ts/utils.projectmanagement.d.ts +5 -0
- package/dist_ts/utils.projectmanagement.js +141 -0
- package/package.json +1 -1
- package/readme.hints.md +4 -2
- package/readme.md +121 -15
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.ts +1234 -24
- package/ts/classes.stores.ts +262 -3
- package/ts/index.ts +6 -0
- package/ts/interfaces.ts +129 -0
- package/ts/utils.json.ts +50 -1
- package/ts/utils.projectmanagement.ts +226 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { FlexHarnessStoreFormatError, } from './errors.js';
|
|
2
|
+
import { FLEX_PROJECT_MANAGEMENT_LIMITS, FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION, FLEX_SESSION_GENERATION_ID_MAX_BYTES, } from './interfaces.js';
|
|
3
|
+
import { assertJsonSerializable } from './utils.json.js';
|
|
4
|
+
function requireRecord(value, path) {
|
|
5
|
+
if (!value
|
|
6
|
+
|| typeof value !== 'object'
|
|
7
|
+
|| Array.isArray(value)
|
|
8
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value))) {
|
|
9
|
+
throw new FlexHarnessStoreFormatError(`${path} must be a plain object.`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function requireOnlyKeys(value, keys, path) {
|
|
14
|
+
const unsupported = Object.keys(value).find((key) => !keys.includes(key));
|
|
15
|
+
if (unsupported) {
|
|
16
|
+
throw new FlexHarnessStoreFormatError(`${path}.${unsupported} is not supported.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function requireBoundedString(value, path, maxBytes, nonEmpty = false) {
|
|
20
|
+
if (typeof value !== 'string'
|
|
21
|
+
|| (nonEmpty && !value.trim())
|
|
22
|
+
|| Buffer.byteLength(value, 'utf8') > maxBytes) {
|
|
23
|
+
throw new FlexHarnessStoreFormatError(`${path} must be ${nonEmpty ? 'a non-empty ' : 'a '}string of at most ${maxBytes} UTF-8 bytes.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function requireTimestamp(value, path) {
|
|
27
|
+
if (typeof value !== 'string') {
|
|
28
|
+
throw new FlexHarnessStoreFormatError(`${path} must be a canonical ISO timestamp.`);
|
|
29
|
+
}
|
|
30
|
+
const timestamp = Date.parse(value);
|
|
31
|
+
if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString() !== value) {
|
|
32
|
+
throw new FlexHarnessStoreFormatError(`${path} must be a canonical ISO timestamp.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function requireSessionGeneration(value, path) {
|
|
36
|
+
requireBoundedString(value.sessionGenerationId, `${path}.sessionGenerationId`, FLEX_SESSION_GENERATION_ID_MAX_BYTES, true);
|
|
37
|
+
if (!Number.isSafeInteger(value.sessionGenerationSequence)
|
|
38
|
+
|| Number(value.sessionGenerationSequence) < 1) {
|
|
39
|
+
throw new FlexHarnessStoreFormatError(`${path}.sessionGenerationSequence must be a positive integer.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function validateTask(value, path) {
|
|
43
|
+
const task = requireRecord(value, path);
|
|
44
|
+
requireOnlyKeys(task, ['id', 'content', 'status', 'priority', 'createdAt', 'updatedAt'], path);
|
|
45
|
+
requireBoundedString(task.id, `${path}.id`, FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskIdBytes, true);
|
|
46
|
+
requireBoundedString(task.content, `${path}.content`, FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskContentBytes, true);
|
|
47
|
+
if (!['pending', 'in_progress', 'completed', 'cancelled'].includes(String(task.status))) {
|
|
48
|
+
throw new FlexHarnessStoreFormatError(`${path}.status is invalid.`);
|
|
49
|
+
}
|
|
50
|
+
if (!['high', 'medium', 'low'].includes(String(task.priority))) {
|
|
51
|
+
throw new FlexHarnessStoreFormatError(`${path}.priority is invalid.`);
|
|
52
|
+
}
|
|
53
|
+
requireTimestamp(task.createdAt, `${path}.createdAt`);
|
|
54
|
+
requireTimestamp(task.updatedAt, `${path}.updatedAt`);
|
|
55
|
+
if (task.updatedAt < task.createdAt) {
|
|
56
|
+
throw new FlexHarnessStoreFormatError(`${path}.updatedAt cannot precede createdAt.`);
|
|
57
|
+
}
|
|
58
|
+
return task;
|
|
59
|
+
}
|
|
60
|
+
export function createEmptyFlexProjectManagementSnapshot(sessionGenerationId, sessionGenerationSequence) {
|
|
61
|
+
const snapshot = {
|
|
62
|
+
schemaVersion: FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION,
|
|
63
|
+
revision: 0,
|
|
64
|
+
sessionGenerationId,
|
|
65
|
+
sessionGenerationSequence,
|
|
66
|
+
scratchpad: '',
|
|
67
|
+
tasks: [],
|
|
68
|
+
};
|
|
69
|
+
assertFlexProjectManagementSnapshot(snapshot);
|
|
70
|
+
return snapshot;
|
|
71
|
+
}
|
|
72
|
+
export function assertFlexProjectManagementSnapshot(value) {
|
|
73
|
+
assertJsonSerializable(value, '$snapshot');
|
|
74
|
+
const snapshot = requireRecord(value, '$snapshot');
|
|
75
|
+
requireOnlyKeys(snapshot, [
|
|
76
|
+
'schemaVersion',
|
|
77
|
+
'revision',
|
|
78
|
+
'sessionGenerationId',
|
|
79
|
+
'sessionGenerationSequence',
|
|
80
|
+
'goal',
|
|
81
|
+
'scratchpad',
|
|
82
|
+
'tasks',
|
|
83
|
+
], '$snapshot');
|
|
84
|
+
if (snapshot.schemaVersion !== FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION) {
|
|
85
|
+
throw new FlexHarnessStoreFormatError(`Project management snapshot schemaVersion must be ${FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION}.`);
|
|
86
|
+
}
|
|
87
|
+
if (!Number.isSafeInteger(snapshot.revision) || Number(snapshot.revision) < 0) {
|
|
88
|
+
throw new FlexHarnessStoreFormatError('Project management snapshot revision must be a non-negative integer.');
|
|
89
|
+
}
|
|
90
|
+
requireSessionGeneration(snapshot, '$snapshot');
|
|
91
|
+
if (snapshot.goal !== undefined) {
|
|
92
|
+
requireBoundedString(snapshot.goal, '$snapshot.goal', FLEX_PROJECT_MANAGEMENT_LIMITS.maxGoalBytes, true);
|
|
93
|
+
}
|
|
94
|
+
requireBoundedString(snapshot.scratchpad, '$snapshot.scratchpad', FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes);
|
|
95
|
+
if (!Array.isArray(snapshot.tasks)) {
|
|
96
|
+
throw new FlexHarnessStoreFormatError('$snapshot.tasks must be an array.');
|
|
97
|
+
}
|
|
98
|
+
if (snapshot.tasks.length > FLEX_PROJECT_MANAGEMENT_LIMITS.maxTasks) {
|
|
99
|
+
throw new FlexHarnessStoreFormatError('$snapshot.tasks exceeds its task-count limit.');
|
|
100
|
+
}
|
|
101
|
+
const taskIds = new Set();
|
|
102
|
+
for (let index = 0; index < snapshot.tasks.length; index++) {
|
|
103
|
+
const task = validateTask(snapshot.tasks[index], `$snapshot.tasks[${index}]`);
|
|
104
|
+
if (taskIds.has(task.id)) {
|
|
105
|
+
throw new FlexHarnessStoreFormatError(`$snapshot.tasks contains duplicate task "${task.id}".`);
|
|
106
|
+
}
|
|
107
|
+
taskIds.add(task.id);
|
|
108
|
+
}
|
|
109
|
+
if (Buffer.byteLength(JSON.stringify(snapshot), 'utf8') > FLEX_PROJECT_MANAGEMENT_LIMITS.maxSnapshotBytes) {
|
|
110
|
+
throw new FlexHarnessStoreFormatError('Project management snapshot exceeds its serialized byte limit.');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function assertFlexProjectManagementTombstone(value) {
|
|
114
|
+
assertJsonSerializable(value, '$tombstone');
|
|
115
|
+
const tombstone = requireRecord(value, '$tombstone');
|
|
116
|
+
requireOnlyKeys(tombstone, [
|
|
117
|
+
'schemaVersion',
|
|
118
|
+
'revision',
|
|
119
|
+
'sessionGenerationId',
|
|
120
|
+
'sessionGenerationSequence',
|
|
121
|
+
'deletedAt',
|
|
122
|
+
], '$tombstone');
|
|
123
|
+
if (tombstone.schemaVersion !== FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION) {
|
|
124
|
+
throw new FlexHarnessStoreFormatError(`Project management tombstone schemaVersion must be ${FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION}.`);
|
|
125
|
+
}
|
|
126
|
+
if (!Number.isSafeInteger(tombstone.revision) || Number(tombstone.revision) < 1) {
|
|
127
|
+
throw new FlexHarnessStoreFormatError('Project management tombstone revision must be a positive integer.');
|
|
128
|
+
}
|
|
129
|
+
requireSessionGeneration(tombstone, '$tombstone');
|
|
130
|
+
requireTimestamp(tombstone.deletedAt, '$tombstone.deletedAt');
|
|
131
|
+
}
|
|
132
|
+
export function assertFlexProjectManagementRecord(value) {
|
|
133
|
+
const record = requireRecord(value, '$record');
|
|
134
|
+
if (Object.prototype.hasOwnProperty.call(record, 'deletedAt')) {
|
|
135
|
+
assertFlexProjectManagementTombstone(value);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
assertFlexProjectManagementSnapshot(value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMucHJvamVjdG1hbmFnZW1lbnQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy91dGlscy5wcm9qZWN0bWFuYWdlbWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsMkJBQTJCLEdBQzVCLE1BQU0sYUFBYSxDQUFDO0FBQ3JCLE9BQU8sRUFDTCw4QkFBOEIsRUFDOUIsc0NBQXNDLEVBQ3RDLG9DQUFvQyxHQUNyQyxNQUFNLGlCQUFpQixDQUFDO0FBT3pCLE9BQU8sRUFBRSxzQkFBc0IsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBRXpELFNBQVMsYUFBYSxDQUFDLEtBQWMsRUFBRSxJQUFZO0lBQ2pELElBQ0UsQ0FBQyxLQUFLO1dBQ0gsT0FBTyxLQUFLLEtBQUssUUFBUTtXQUN6QixLQUFLLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQztXQUNwQixDQUFDLENBQUMsTUFBTSxDQUFDLFNBQVMsRUFBRSxJQUFJLENBQUMsQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLGNBQWMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUNuRSxDQUFDO1FBQ0QsTUFBTSxJQUFJLDJCQUEyQixDQUFDLEdBQUcsSUFBSSwwQkFBMEIsQ0FBQyxDQUFDO0lBQzNFLENBQUM7SUFDRCxPQUFPLEtBQWdDLENBQUM7QUFDMUMsQ0FBQztBQUVELFNBQVMsZUFBZSxDQUN0QixLQUE4QixFQUM5QixJQUF1QixFQUN2QixJQUFZO0lBRVosTUFBTSxXQUFXLEdBQUcsTUFBTSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO0lBQzFFLElBQUksV0FBVyxFQUFFLENBQUM7UUFDaEIsTUFBTSxJQUFJLDJCQUEyQixDQUFDLEdBQUcsSUFBSSxJQUFJLFdBQVcsb0JBQW9CLENBQUMsQ0FBQztJQUNwRixD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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelprofile.com/flexharness",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
package/readme.hints.md
CHANGED
|
@@ -9,6 +9,7 @@ Implementation findings for flexharness.
|
|
|
9
9
|
- Prompt attachment payloads exist only in canonical private Agent events. Public messages, prompt results, projection snapshots, and events expose only `attachmentType`, source kind, optional media/name, and decoded size when determinable.
|
|
10
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
11
|
- Resource tool composition resolves descriptors per run before provider acquisition, validates duplicate resources and derived namespaces as one set, preserves legacy tool names, and derives bounded resource names from canonical resource/revision and original-tool hashes. Permission identity uses the complete resource digest while provider metadata remains nested below harness-owned attachment metadata.
|
|
12
|
+
- Enabled harness-built-in names are checked on every application and resource handle before model execution. The reserved set is run-specific, so a disabled project tool or depth-disabled delegate does not reserve its wire name.
|
|
12
13
|
|
|
13
14
|
## Persistence boundary
|
|
14
15
|
|
|
@@ -16,7 +17,7 @@ Implementation findings for flexharness.
|
|
|
16
17
|
- Every queued mutation snapshots its revision and domain state first. A thrown save is reconciled against the store before memory is restored: a proven prior snapshot rolls back, a proven new snapshot remains current, and an unknown or explicitly uncertain commit fences the namespace. Code that crosses the save `await` must re-fetch session records because a proven rollback intentionally restores them.
|
|
17
18
|
- `JsonFileFlexHarnessStores` is intentionally not cross-process safe. It provides atomic rename and in-process CAS, not an operating-system lock.
|
|
18
19
|
- Loaded state is repaired from canonical generation outcomes. Accepted hidden terminal stages are promoted, while interrupted or incomplete public messages and tool parts become cancelled so a restarted process never presents them as still running.
|
|
19
|
-
- Session updates reject while runtime work is active. Deletion tombstones the session, cancels active work, and retains failed runtime cleanup for a later deletion, retirement, or disposal retry before removing
|
|
20
|
+
- Session updates reject while runtime work is active. Deletion tombstones the session, cancels active work, and retains failed runtime cleanup for a later deletion, retirement, or disposal retry before removing destructive core domains and confirming the retained PM generation fence.
|
|
20
21
|
- Tool close settles before canonical acceptance. A completed hidden terminal projection is staged first, the Agent generation is finalized second, and only then is the public projection promoted. Earlier execution failures finalize as interrupted and publish from that durable outcome.
|
|
21
22
|
- Disposal waits admitted session initialization, finalizers, every loaded-state save tail, tombstone cleanup, provider release, and detached tool cleanup. Listeners always clear; state caches clear only after cleanup succeeds and otherwise remain owned for retry.
|
|
22
23
|
- Canonical finalization is the cancellation linearization point. Abort returns false once a run starts committing, while the active-run entry continues to block new prompts until finalization and public promotion settle.
|
|
@@ -32,6 +33,7 @@ Implementation findings for flexharness.
|
|
|
32
33
|
- A loaded schema-1 or schema-2 projection retains its exact durable baseline until the first schema-3 save is confirmed, so a non-committing upgrade failure rolls back without fencing while a post-commit throw reconciles the new schema.
|
|
33
34
|
- Workspace-required traversal keeps the cursor in completed-root group coordinates. Candidate operations may jump across trailing no-change groups, while pending, nonrevertible, mixed, and legacy transcript groups remain barriers without changing transcript-optional grouping.
|
|
34
35
|
- `JsonFileFlexHarnessStores.dispose()` is the explicit final drain for file handles whose close failed on the last store operation; failed disposal retains the handle for another call.
|
|
36
|
+
- Project-management state is a required exact `(storageKey, sessionId)` member of `IFlexHarnessStores`; `builtInTools.projectManagement` only controls tool exposure. Harness-local operations serialize without retrying external mutation conflicts, no-op mutations do not save, and tool writes retain agent/run/tool-call attribution. Session cleanup always runs, waits the local queue, and boundedly reloads before writing a generation-owned tombstone. Random generation identity plus a monotonic core-scope generation sequence permits a newer session to replace only an older PM tombstone while fencing stale saves and tombstones. Normal cleanup never removes the fence; `purgeNamespace()` is reserved for serialized application-owned whole-namespace destruction after non-destructive retirement and core-scope purge.
|
|
35
37
|
|
|
36
38
|
## Tool output boundary
|
|
37
39
|
|
|
@@ -50,7 +52,7 @@ Implementation findings for flexharness.
|
|
|
50
52
|
## Subagent ownership
|
|
51
53
|
|
|
52
54
|
- Subagents are first-class FlexHarness sessions. Public callers cannot assign relationship fields; private creation persists a complete parent/origin/agent/depth relation under the storage-key scope mutation before runtime initialization.
|
|
53
|
-
- The built-in `
|
|
55
|
+
- The built-in `delegate` tool consumes its per-run call slot when schema-valid execution starts, before semantic validation. Tool-schema rejection never enters execution and consumes no slot. Successful semantic validation reserves the child ID before requesting `subagent.start` permission; that ID remains consumed after rejection or failure, but permission rejection creates no child session. New child IDs hash the version marker, raw storage key, parent session, parent run, and parent tool call; the storage key itself is never exposed.
|
|
54
56
|
- SmartAgent's parent tool intent remains crash authority. A deterministic child with messages is treated as an uncertain prior execution, while explicit `taskId` resume is limited to a later run of the same parent and same configured agent.
|
|
55
57
|
- The SmartAgent tool-start callback establishes the correlated running parent part before execution. Harness-owned child ID/model metadata mutates that part synchronously and emits cumulative `part.updated` snapshots within the existing callback budgets.
|
|
56
58
|
- Parent abort listeners recheck immediately after registration and abort only the captured active child run ID. Listener removal is in `finally`, including child failures and cancellation.
|
package/readme.md
CHANGED
|
@@ -32,6 +32,10 @@ interface IProjectScope {
|
|
|
32
32
|
projectRoot: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const stores = new JsonFileFlexHarnessStores({
|
|
36
|
+
directory: '/var/lib/my-app/model-sessions',
|
|
37
|
+
});
|
|
38
|
+
|
|
35
39
|
const harness = new FlexHarness<IProjectScope>({
|
|
36
40
|
scopeResolver: {
|
|
37
41
|
async resolveScope(scopeId) {
|
|
@@ -70,9 +74,13 @@ const harness = new FlexHarness<IProjectScope>({
|
|
|
70
74
|
};
|
|
71
75
|
},
|
|
72
76
|
},
|
|
73
|
-
stores
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
stores,
|
|
78
|
+
builtInTools: {
|
|
79
|
+
renameSession: true,
|
|
80
|
+
projectManagement: {
|
|
81
|
+
// task, goal, and scratchpad default to true when this block exists.
|
|
82
|
+
},
|
|
83
|
+
},
|
|
76
84
|
toolOutputLimits: {
|
|
77
85
|
maxDepth: 12,
|
|
78
86
|
maxBytes: 256 * 1024,
|
|
@@ -148,16 +156,109 @@ The resolver accepts at most 128 descriptors per run. `resourceId` must be non-e
|
|
|
148
156
|
|
|
149
157
|
Resource permission requests are scoped with the complete 64-character `resourceIdentity`, not the shortened tool namespace. FlexHarness rewrites `kind` to `resource.<resourceIdentity>.<providerKind>` and an optional `rememberKey` to `resource:<resourceIdentity>:<providerRememberKey>`. Harness-owned metadata contains `resourceId`, `attachmentRevision`, `resourceIdentity`, and `toolNamespace`; provider metadata is nested under `providerMetadata`, so it cannot override attachment identity.
|
|
150
158
|
|
|
151
|
-
FlexHarness owns every acquired handle. Normal close and partial-failure cleanup run in reverse acquisition order, attempt every handle, aggregate multiple failures, and retain failed cleanup for retirement or disposal retry. Cancellation uses the same path. If model resolution fails while a resource provider is still settling, a late returned handle remains tracked and disposal waits for its closure.
|
|
159
|
+
FlexHarness owns every acquired handle. Normal close and partial-failure cleanup run in reverse acquisition order, attempt every handle, aggregate multiple failures, and retain failed cleanup for retirement or disposal retry. Cancellation uses the same path. If model resolution fails while a resource provider is still settling, a late returned handle remains tracked and disposal waits for its closure. Application and resource providers may not define a harness built-in name while that built-in is enabled for the current run. Disabled names are not reserved.
|
|
160
|
+
|
|
161
|
+
## Project Management Tools
|
|
162
|
+
|
|
163
|
+
Harness-owned project tools are opt-in and session-local:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
builtInTools: {
|
|
167
|
+
renameSession: true,
|
|
168
|
+
projectManagement: {
|
|
169
|
+
task: true,
|
|
170
|
+
goal: true,
|
|
171
|
+
scratchpad: true,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`renameSession` enables `rename_session`. The `projectManagement` block enables the public project-management APIs and contains the model-tool flags; `task`, `goal`, and `scratchpad` each default to enabled unless explicitly set to `false`. Without that block, the public project-management APIs reject with `FlexHarnessValidationError`, while the required `stores.projectManagement` domain still participates in session cleanup. With no `builtInTools` configuration, none of these four tools is present. Constructor options are copied and frozen.
|
|
177
|
+
|
|
178
|
+
Project-management records use `FLEX_PROJECT_MANAGEMENT_SCHEMA_VERSION`, currently `1`, and form a strict live-or-tombstone union:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
interface IFlexProjectManagementSnapshot {
|
|
182
|
+
schemaVersion: 1;
|
|
183
|
+
revision: number;
|
|
184
|
+
sessionGenerationId: string;
|
|
185
|
+
sessionGenerationSequence: number;
|
|
186
|
+
goal?: string;
|
|
187
|
+
scratchpad: string;
|
|
188
|
+
tasks: Array<{
|
|
189
|
+
id: string;
|
|
190
|
+
content: string;
|
|
191
|
+
status: 'pending' | 'in_progress' | 'completed' | 'cancelled';
|
|
192
|
+
priority: 'high' | 'medium' | 'low';
|
|
193
|
+
createdAt: string;
|
|
194
|
+
updatedAt: string;
|
|
195
|
+
}>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
interface IFlexProjectManagementTombstone {
|
|
199
|
+
schemaVersion: 1;
|
|
200
|
+
revision: number;
|
|
201
|
+
sessionGenerationId: string;
|
|
202
|
+
sessionGenerationSequence: number;
|
|
203
|
+
deletedAt: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
type TFlexProjectManagementRecord =
|
|
207
|
+
| IFlexProjectManagementSnapshot
|
|
208
|
+
| IFlexProjectManagementTombstone;
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The tools use strict action-discriminated inputs:
|
|
212
|
+
|
|
213
|
+
- `task`: `list`, `create`, `update`, `delete`, or `clear`. Create defaults to `pending` and `medium`.
|
|
214
|
+
- `goal`: `get`, `set`, or `clear`.
|
|
215
|
+
- `scratchpad`: `get`, `set`, `append`, or `clear`. Append concatenates the supplied content exactly.
|
|
216
|
+
- `rename_session`: sets the active session title and returns the authoritative session.
|
|
217
|
+
|
|
218
|
+
Every project action returns the authoritative revision and state; task mutations also return the affected task, and clear returns the removed tasks. Reads never save. A set, clear, append, update, idempotent create, or empty task clear that makes no state change returns the current revision without writing. Mutations load once, apply once, validate the complete next snapshot, and issue one compare-and-swap save at `revision + 1`. FlexHarness never retries or merges an external conflict.
|
|
219
|
+
|
|
220
|
+
Tool task creation accepts an optional `id`. When omitted, FlexHarness requires the stable SmartAgent `toolCallId` and derives `task_` plus the SHA-256 of `JSON.stringify(['flexharness-project-task-v1', storageKey, sessionId, runId, toolCallId])`. Repeating an explicit or deterministic ID with identical content, status, and priority is idempotent; different creation data conflicts. Application callers must supply an explicit `id` to `createProjectTask()` because no tool-call identity exists at that boundary.
|
|
221
|
+
|
|
222
|
+
The same engine is available to applications:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
await harness.getProjectState(scopeId, sessionId);
|
|
226
|
+
await harness.listProjectTasks(scopeId, sessionId);
|
|
227
|
+
await harness.createProjectTask(scopeId, sessionId, { id, content, status, priority });
|
|
228
|
+
await harness.updateProjectTask(scopeId, sessionId, { id, content, status, priority });
|
|
229
|
+
await harness.deleteProjectTask(scopeId, sessionId, id);
|
|
230
|
+
await harness.clearProjectTasks(scopeId, sessionId);
|
|
231
|
+
await harness.getProjectGoal(scopeId, sessionId);
|
|
232
|
+
await harness.setProjectGoal(scopeId, sessionId, goal);
|
|
233
|
+
await harness.clearProjectGoal(scopeId, sessionId);
|
|
234
|
+
await harness.getProjectScratchpad(scopeId, sessionId);
|
|
235
|
+
await harness.setProjectScratchpad(scopeId, sessionId, content);
|
|
236
|
+
await harness.appendProjectScratchpad(scopeId, sessionId, content);
|
|
237
|
+
await harness.clearProjectScratchpad(scopeId, sessionId);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Public writes use `{ actor: 'application' }`. Tool writes use `{ actor: 'agent', runId, toolCallId, agent? }`, allowing custom stores to preserve attribution. Project side effects commit independently of the later model outcome and are intentionally outside transcript undo/redo.
|
|
241
|
+
|
|
242
|
+
`FLEX_PROJECT_MANAGEMENT_LIMITS` exports the hard UTF-8 and aggregate limits: goal 8 KiB, scratchpad 64 KiB, task content 8 KiB, task ID 512 bytes, title 2048 bytes, 512 tasks, and a 96 KiB serialized snapshot. Loaded snapshots reject extra fields, duplicate IDs, invalid status/priority/timestamps, non-JSON data, wrong schema/revision, and every exceeded bound before use.
|
|
243
|
+
|
|
244
|
+
`IFlexProjectManagementStore` is exact per `(storageKey, sessionId)`: `load`, CAS `save`, CAS `tombstoneSession`, and `purgeNamespace` must not collapse multiple sessions or storage namespaces. `load()` returns `TFlexProjectManagementRecord | undefined`. Same-generation live saves use normal revision CAS, and a same-generation tombstone permanently rejects later live saves. A higher `sessionGenerationSequence` with a different strong `sessionGenerationId` may replace only an older tombstone using expected revision `0`; it cannot replace a live record. This resets the PM revision for a recreated core session while stale saves and tombstones from older generations remain fenced. Deleting a recreated session that made no PM writes still replaces the prior-generation tombstone with a revision-1 tombstone for the new generation.
|
|
245
|
+
|
|
246
|
+
Every newly created core session exposes and persists a strong random `sessionGenerationId` plus its monotonic `sessionGenerationSequence`. A legacy scope session without those fields is assigned a deterministic bounded ID derived from its immutable `storageKey`, `sessionId`, and `createdAt`; FlexHarness persists the repaired scope snapshot before accepting work. Grouped core deletion tombstones retain both fields after live metadata is removed.
|
|
247
|
+
|
|
248
|
+
Normal Flex session cleanup always waits in-flight local project operations, then loads and CAS-tombstones `stores.projectManagement`, regardless of whether PM tools are enabled in that harness. If a concurrent same-generation save wins first, cleanup reloads and retries within a bounded attempt count; unresolved conflict or store failure retains the core Flex session cleanup tombstone for a later retry. The durable PM tombstone is not physically removed during normal session cleanup.
|
|
249
|
+
|
|
250
|
+
`purgeNamespace(storageKey)` is the explicit destructive reclamation operation and physically removes every live record and tombstone in that exact PM namespace. Applications may call it only after serializing every scope alias, preventing new admission, awaiting `retireScope()` on every harness owner, and deleting or purging the application-owned core scope namespace. `retireScope()` itself remains non-destructive and never calls `purgeNamespace()`. Purging PM first, purging only one alias, or racing a stale harness can remove the fence that makes session-generation reuse safe.
|
|
251
|
+
|
|
252
|
+
`InMemoryFlexProjectManagementStore` is the standalone in-memory implementation. `InMemoryFlexHarnessStores` and `JsonFileFlexHarnessStores` include `projectManagement` as a required bundle member. `assertFlexProjectManagementSnapshot()` validates live records, `assertFlexProjectManagementTombstone()` validates tombstones, and `assertFlexProjectManagementRecord()` validates the union. `createEmptyFlexProjectManagementSnapshot(sessionGenerationId, sessionGenerationSequence)` returns a revision-0 live state for the supplied current generation.
|
|
152
253
|
|
|
153
254
|
## Foreground Subagents
|
|
154
255
|
|
|
155
|
-
`subagents` enables a harness-owned built-in tool named `
|
|
256
|
+
`subagents` enables a harness-owned built-in tool named `delegate`. It is available only when at least one definition exists and the current session depth is below `maxSubagentDepth`. An application or resource `toolProvider` must not return its own `delegate` tool while the built-in is enabled for that run. The built-in is foreground-only: the parent tool call does not complete until the child prompt reaches a terminal outcome.
|
|
156
257
|
|
|
157
258
|
The model calls it with this exact input shape:
|
|
158
259
|
|
|
159
260
|
```typescript
|
|
160
|
-
interface
|
|
261
|
+
interface IDelegateInput {
|
|
161
262
|
description: string;
|
|
162
263
|
prompt: string;
|
|
163
264
|
subagentType: string;
|
|
@@ -167,9 +268,9 @@ interface ITaskInput {
|
|
|
167
268
|
|
|
168
269
|
Before creating or resuming a child, FlexHarness requests permission on the parent run with `kind: 'subagent.start'`, the parent `toolCallId`, and bounded agent/task metadata. The controller answers it through the normal permission APIs. This request has no `rememberKey`, so `always` is invalid; controllers use `once` or `reject`.
|
|
169
270
|
|
|
170
|
-
Each new invocation creates a durable child `IFlexSession` with immutable `parentSessionId`, origin `parentRunId`, origin `parentToolCallId`, `agent`, and `depth`. New public roots persist `depth: 0`; legacy schema-1 roots may omit it. These fields are harness-owned; public `createSession()` remains limited to `sessionId` and `title`. Child sessions reject direct `prompt()`, `startPrompt()`, `enqueuePrompt()`, and `schedulePrompt()` calls and run only through the foreground `
|
|
271
|
+
Each new invocation creates a durable child `IFlexSession` with immutable `parentSessionId`, origin `parentRunId`, origin `parentToolCallId`, `agent`, and `depth`. New public roots persist `depth: 0`; legacy schema-1 roots may omit it. These fields are harness-owned; public `createSession()` remains limited to `sessionId` and `title`. Child sessions reject direct `prompt()`, `startPrompt()`, `enqueuePrompt()`, and `schedulePrompt()` calls and run only through the foreground `delegate` tool. The model and tool resolver contexts receive optional immutable `parentSessionId` and `agent` values so integrations can apply agent-specific model and tool policy. Child prompts use the definition's `modelHint`, `system`, and `maxSteps`.
|
|
171
272
|
|
|
172
|
-
The parent tool part receives `childSessionId` in a cumulative `part.updated` event as soon as the child is acquired. If child model resolution completes, a later cumulative update adds `model`; failures before model resolution leave it absent. The terminal tool part retains every value that became available. A successful
|
|
273
|
+
The parent tool part receives `childSessionId` in a cumulative `part.updated` event as soon as the child is acquired. If child model resolution completes, a later cumulative update adds `model`; failures before model resolution leave it absent. The terminal tool part retains every value that became available. A successful delegate call always has model identity and returns bounded JSON:
|
|
173
274
|
|
|
174
275
|
```typescript
|
|
175
276
|
{
|
|
@@ -182,9 +283,9 @@ The parent tool part receives `childSessionId` in a cumulative `part.updated` ev
|
|
|
182
283
|
|
|
183
284
|
Omitting `taskId` creates a deterministic child for the parent session, run, and tool call. Repeating that same invocation does not create another child. If the deterministic child already has messages, FlexHarness reports an uncertain prior execution and never silently reruns it. This preserves SmartAgent's durable parent tool intent as crash authority; controllers use `listUncertainToolExecutions()` and `reconcileToolExecution()` for uncertain parent calls.
|
|
184
285
|
|
|
185
|
-
Supplying `taskId` deliberately resumes an idle, live child from a later run of the same immutable parent session and the same configured agent. It starts a new child prompt while retaining the child's original parent run and tool-call origin. A child owned by another parent or agent, a deleted child, an active child, a same-run resume, or a second acquisition of the same child within one later parent run is rejected. Parent cancellation propagates only to the exact child run started by that
|
|
286
|
+
Supplying `taskId` deliberately resumes an idle, live child from a later run of the same immutable parent session and the same configured agent. It starts a new child prompt while retaining the child's original parent run and tool-call origin. A child owned by another parent or agent, a deleted child, an active child, a same-run resume, or a second acquisition of the same child within one later parent run is rejected. Parent cancellation propagates only to the exact child run started by that delegate call.
|
|
186
287
|
|
|
187
|
-
Limits are validated and frozen at construction: at most 32 unique definitions; names are non-empty and at most 128 UTF-8 bytes; descriptions 2048 bytes; optional model hints 512 bytes; optional system prompts 64 KiB; and optional `maxSteps` a positive safe integer. `maxSubagentDepth` defaults to 1 and must be a positive safe integer at most 8. `maxSubagentCallsPerRun` defaults to 32 and must be a positive safe integer at most 128. A call slot is consumed synchronously at the start of every schema-valid
|
|
288
|
+
Limits are validated and frozen at construction: at most 32 unique definitions; names are non-empty and at most 128 UTF-8 bytes; descriptions 2048 bytes; optional model hints 512 bytes; optional system prompts 64 KiB; and optional `maxSteps` a positive safe integer. `maxSubagentDepth` defaults to 1 and must be a positive safe integer at most 8. `maxSubagentCallsPerRun` defaults to 32 and must be a positive safe integer at most 128. A call slot is consumed synchronously at the start of every schema-valid delegate execution, before semantic bounds, subagent type/depth validation, permission, or child work. Inputs rejected by the tool schema never start delegate execution and do not consume a slot. After successful semantic validation, the child ID is reserved for the rest of the parent run, including after permission rejection or later failure. Permission rejection creates no child session. Omitting `taskId` reserves a deterministic new child ID; supplying `taskId` reserves and resumes that existing child after permission. Delegate descriptions are non-empty and at most 256 UTF-8 bytes, prompts non-empty and at most 64 KiB, subagent types at most 128 bytes, and task IDs at most 512 bytes.
|
|
188
289
|
|
|
189
290
|
## Sessions And Prompts
|
|
190
291
|
|
|
@@ -253,6 +354,10 @@ if (command.type === 'prompt-admission') {
|
|
|
253
354
|
}
|
|
254
355
|
await harness.updateSession(scopeId, sessionId, { title: 'Renamed', archived: true });
|
|
255
356
|
await harness.updateSession(scopeId, sessionId, { title: null, archived: false });
|
|
357
|
+
await harness.getProjectState(scopeId, sessionId);
|
|
358
|
+
await harness.createProjectTask(scopeId, sessionId, { id: 'tests', content: 'Add tests' });
|
|
359
|
+
await harness.setProjectGoal(scopeId, sessionId, 'Ship the next release');
|
|
360
|
+
await harness.appendProjectScratchpad(scopeId, sessionId, 'One durable note.');
|
|
256
361
|
await harness.deleteSession(scopeId, sessionId);
|
|
257
362
|
await harness.prompt(scopeId, sessionId, prompt, options);
|
|
258
363
|
const queued = await harness.enqueuePrompt(scopeId, sessionId, prompt, options);
|
|
@@ -311,7 +416,7 @@ The reservation save is the admission point. A save failure produces no start ev
|
|
|
311
416
|
|
|
312
417
|
`listMessagePage()` returns the newest contiguous page in chronological order. `limit` must be an integer from 1 through 50 and defaults to 50. `nextCursor` is opaque, limited to 4096 UTF-8 bytes, bound to the resolved storage namespace and session, and remains stable when newer messages are appended. Mismatched and stale cursors fail validation. `getMessage()` performs an exact lookup. Transfer identifiers are limited to 512 bytes, text and reasoning parts to 96 KiB, complete messages to 480 KiB, and complete page envelopes to 512 KiB. A page may therefore contain fewer messages than requested. Oversized text is truncated and an otherwise oversized parts collection is replaced with an explicit elision marker; metadata that still cannot fit fails validation. Canonical private Agent events are unchanged.
|
|
313
418
|
|
|
314
|
-
`updateSession()` supports title replacement, explicit title clearing with `null`, and archive state through `archived`. Title-only updates remain available while prompts are queued or running, while permission is pending, and after archival. Requests containing `archived` are rejected while the session has any outstanding prompt or pending permission; a mixed title-and-archive request is rejected atomically without changing the title. Archived sessions expose `archivedAt`. Deleting a session cascades through its complete descendant subtree. One durable root-keyed tombstone group hides every newly affected live session, and the delete also joins any already-separate descendant cleanup groups without rewriting their roots. FlexHarness then cancels queued and active subtree work, emits terminal queue events, waits for admitted initialization, and purges runtime queue status while cleaning runtime and persisted domains child-first. The requested root tombstone is removed last. A successful live `deleteSession()` call emits `session.deleted` for each session it newly tombstoned; retries of an existing tombstone and automatic load, retirement, or disposal cleanup emit no deletion events. Direct deletion of a descendant cascades only through that descendant's subtree. Cleanup authority follows the resolved storage namespace, so scope aliases share the same groups. A partial failure retains durable ownership for retry by a later `deleteSession()`, namespace load, `retireScope()`, or `dispose()` call.
|
|
419
|
+
`updateSession()` supports title replacement, explicit title clearing with `null`, and archive state through `archived`. Title-only updates remain available while prompts are queued or running, while permission is pending, and after archival. Requests containing `archived` are rejected while the session has any outstanding prompt or pending permission; a mixed title-and-archive request is rejected atomically without changing the title. Archived sessions expose `archivedAt`. Deleting a session cascades through its complete descendant subtree. One durable root-keyed tombstone group hides every newly affected live session, and the delete also joins any already-separate descendant cleanup groups without rewriting their roots. FlexHarness then cancels queued and active subtree work, emits terminal queue events, waits for admitted initialization, and purges runtime queue status while cleaning runtime and persisted domains child-first. The requested root tombstone is removed last after every domain confirms cleanup; project-management cleanup confirmation is a retained durable project tombstone rather than physical removal. A successful live `deleteSession()` call emits `session.deleted` for each session it newly tombstoned; retries of an existing tombstone and automatic load, retirement, or disposal cleanup emit no deletion events. Direct deletion of a descendant cascades only through that descendant's subtree. Cleanup authority follows the resolved storage namespace, so scope aliases share the same groups. A partial failure retains durable ownership for retry by a later `deleteSession()`, namespace load, `retireScope()`, or `dispose()` call.
|
|
315
420
|
|
|
316
421
|
`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.
|
|
317
422
|
|
|
@@ -584,19 +689,20 @@ Events are discriminated, sequenced, deeply immutable snapshots. Listener except
|
|
|
584
689
|
|
|
585
690
|
## Stores
|
|
586
691
|
|
|
587
|
-
FlexHarness
|
|
692
|
+
Current FlexHarness persistence is separated by trust and lifecycle domain through `IFlexHarnessStores`:
|
|
588
693
|
|
|
589
694
|
- `scopes`: session metadata and deletion tombstones for a resolved storage namespace.
|
|
590
695
|
- `projections`: public audit messages and hidden terminal stages per session.
|
|
591
696
|
- `permissions`: remembered permission keys per session.
|
|
697
|
+
- `projectManagement`: generation-fenced task, goal, and scratchpad state per session.
|
|
592
698
|
- `agentEvents`: canonical private SmartAgent events and archives per session.
|
|
593
699
|
- `jobs`: private background execution state per session.
|
|
594
700
|
|
|
595
|
-
`InMemoryFlexHarnessStores` implements all
|
|
701
|
+
`InMemoryFlexHarnessStores` implements all six required domains with revision-based compare-and-swap behavior for tests and ephemeral processes. It is the default when `stores` is omitted. Custom `IFlexHarnessStores` implementations must provide `projectManagement` even when project-management tools are disabled, because deletion cleanup always writes the generation fence.
|
|
596
702
|
|
|
597
703
|
Custom Agent event and job providers may implement `releaseSession(storageKey, sessionId)` to release session-bound wrappers, handles, or caches without deleting durable data. FlexHarness calls these hooks only after the corresponding AgentSession or execution context has released runtime ownership. A failed release remains owned for a later retirement or disposal retry. `deleteSession()` remains the separate destructive operation for durable session data.
|
|
598
704
|
|
|
599
|
-
`JsonFileFlexHarnessStores` stores the domains in separate `scopes`, `projections`, `permissions`, `events`, `archives`, and `jobs` directories. Storage and session identifiers are SHA-256 hashed for filenames. It provides:
|
|
705
|
+
`JsonFileFlexHarnessStores` stores the domains in separate `scopes`, `projections`, `permissions`, `projectManagement`, `events`, `archives`, and `jobs` directories. Storage and session identifiers are SHA-256 hashed for filenames. Passing the store bundle supplies lifecycle persistence but does not enable any built-in tool. It provides:
|
|
600
706
|
|
|
601
707
|
- Strict domain-specific schema validation and optimistic revisions.
|
|
602
708
|
- Static process-wide queues shared by all store instances for the same absolute file.
|
|
@@ -607,7 +713,7 @@ Custom Agent event and job providers may implement `releaseSession(storageKey, s
|
|
|
607
713
|
|
|
608
714
|
After every harness using a `JsonFileFlexHarnessStores` instance has been disposed and no store operation remains active, call `await stores.dispose()` to retry and drain any file handle whose earlier close failed. A failed store disposal retains that handle so the call can be retried.
|
|
609
715
|
|
|
610
|
-
The JSON stores are explicitly not cross-process safe.
|
|
716
|
+
The JSON stores are explicitly not cross-process safe. When several processes can access the same storage namespace, every core `IFlexHarnessStores` domain and `stores.projectManagement` must use database-backed or equivalent cross-process CAS. Process-local CAS for the core stores or for PM alone is insufficient: session generation creation, cleanup tombstones, PM replacement, and stale-writer rejection must all retain their respective atomic preconditions across processes.
|
|
611
717
|
|
|
612
718
|
Direct store operations and non-run session mutations surface conflicts as `FlexHarnessStoreConflictError` or the corresponding SmartAgent store conflict. Malformed, wrong-schema, or non-JSON snapshots are surfaced as `FlexHarnessStoreFormatError`. A write or deletion that changed its target but cannot confirm parent-directory durability surfaces `FlexHarnessStoreCommitUncertainError` with the affected path, operation, and cause. Run persistence failures cross the external error boundary and therefore become `FlexHarnessExternalError`. FlexHarness does not merge conflicts.
|
|
613
719
|
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@modelprofile.com/flexharness',
|
|
6
|
-
version: '
|
|
6
|
+
version: '4.0.0',
|
|
7
7
|
description: 'Provider-neutral model-session runtime with durable history, permissions, typed events, and pluggable local or remote tool execution.'
|
|
8
8
|
}
|