@modelprofile.com/flexharness 11.0.1 → 11.1.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.codesandbox.d.ts +12 -4
- package/dist_ts/classes.codesandbox.js +119 -287
- package/dist_ts/classes.codesandbox.worker.d.ts +52 -0
- package/dist_ts/classes.codesandbox.worker.js +310 -0
- package/dist_ts/classes.flexharness.callbacks.d.ts +37 -1
- package/dist_ts/classes.flexharness.callbacks.js +162 -53
- package/dist_ts/classes.flexharness.core.d.ts +2 -1
- package/dist_ts/classes.flexharness.core.js +22 -18
- package/dist_ts/classes.flexharness.results.js +4 -4
- package/dist_ts/classes.flexharness.reversions.d.ts +7 -0
- package/dist_ts/classes.flexharness.reversions.js +40 -39
- package/dist_ts/classes.flexharness.runs.js +7 -3
- package/dist_ts/classes.flexharness.tooldispatch.d.ts +2 -1
- package/dist_ts/classes.flexharness.tooldispatch.js +27 -9
- package/dist_ts/classes.stores.js +15 -13
- package/dist_ts/errors.d.ts +5 -0
- package/dist_ts/errors.js +10 -1
- package/dist_ts/index.d.ts +2 -1
- package/dist_ts/index.js +3 -2
- package/dist_ts/interfaces.d.ts +31 -9
- package/dist_ts/interfaces.flexharness.d.ts +14 -0
- package/dist_ts/interfaces.js +3 -3
- package/dist_ts/plugins.d.ts +6 -4
- package/dist_ts/plugins.js +10 -11
- package/dist_ts/utils.harnessoptions.d.ts +3 -1
- package/dist_ts/utils.harnessoptions.js +15 -2
- package/dist_ts/utils.json.d.ts +9 -1
- package/dist_ts/utils.json.js +92 -33
- package/dist_ts/utils.storecodecs.d.ts +1 -1
- package/dist_ts/utils.storecodecs.js +4 -4
- package/dist_ts/utils.storerules.d.ts +3 -3
- package/dist_ts/utils.storerules.js +1 -14
- package/dist_ts_mcp/index.d.ts +3 -1
- package/dist_ts_mcp/index.js +17 -2
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +18 -6
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +27 -16
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +33 -3
- package/dist_ts_stores_nosqldb/classes.recordstore.js +78 -18
- package/dist_ts_stores_nosqldb/index.d.ts +2 -2
- package/dist_ts_stores_nosqldb/index.js +2 -2
- package/package.json +3 -3
- package/readme.md +8 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.codesandbox.ts +132 -276
- package/ts/classes.codesandbox.worker.ts +345 -0
- package/ts/classes.flexharness.callbacks.ts +207 -55
- package/ts/classes.flexharness.core.ts +22 -16
- package/ts/classes.flexharness.results.ts +3 -3
- package/ts/classes.flexharness.reversions.ts +43 -32
- package/ts/classes.flexharness.runs.ts +6 -2
- package/ts/classes.flexharness.tooldispatch.ts +23 -7
- package/ts/classes.stores.ts +21 -12
- package/ts/errors.ts +14 -0
- package/ts/index.ts +7 -1
- package/ts/interfaces.flexharness.ts +15 -0
- package/ts/interfaces.ts +32 -9
- package/ts/plugins.ts +9 -12
- package/ts/readme.md +8 -2
- package/ts/utils.harnessoptions.ts +22 -0
- package/ts/utils.json.ts +112 -35
- package/ts/utils.storecodecs.ts +3 -3
- package/ts/utils.storerules.ts +3 -24
- package/ts_mcp/index.ts +17 -1
- package/ts_mcp/readme.md +3 -1
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +39 -15
- package/ts_stores_nosqldb/classes.recordstore.ts +105 -13
- package/ts_stores_nosqldb/index.ts +2 -0
- package/ts_stores_nosqldb/readme.md +10 -2
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
IFlexReversionSegment,
|
|
27
27
|
IFlexPendingReversionRelease,
|
|
28
28
|
IFlexResolvedScope,
|
|
29
|
+
IFlexResultLimits,
|
|
29
30
|
IFlexScopeSnapshot,
|
|
30
31
|
IFlexSession,
|
|
31
32
|
IFlexSessionImportLimits,
|
|
@@ -59,6 +60,7 @@ import {
|
|
|
59
60
|
resolveBoundedPositiveInteger,
|
|
60
61
|
resolveCallbackLimits,
|
|
61
62
|
resolvePromptQueueLimits,
|
|
63
|
+
resolveResultLimits,
|
|
62
64
|
resolveReversionMaintenanceTimeoutMs,
|
|
63
65
|
resolveReversionLimits,
|
|
64
66
|
type INormalizedBuiltInTools,
|
|
@@ -153,6 +155,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
153
155
|
public readonly promptQueueLimits: Required<IFlexPromptQueueLimits>;
|
|
154
156
|
public readonly reversionLimits: Required<IFlexReversionLimits>;
|
|
155
157
|
public readonly sessionImportLimits: Required<IFlexSessionImportLimits>;
|
|
158
|
+
public readonly resultLimits: Required<IFlexResultLimits>;
|
|
156
159
|
public readonly reversionMaintenanceTimeoutMs: number;
|
|
157
160
|
public readonly turnReversionProvider: IFlexHarnessOptions<TScope>['turnReversionProvider'];
|
|
158
161
|
public readonly reversionPolicy: TFlexReversionPolicy;
|
|
@@ -206,6 +209,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
206
209
|
this.promptQueueLimits = resolvePromptQueueLimits(options.promptQueueLimits);
|
|
207
210
|
this.reversionLimits = resolveReversionLimits(options.reversionLimits);
|
|
208
211
|
this.sessionImportLimits = resolveSessionImportLimits(options.sessionImportLimits);
|
|
212
|
+
this.resultLimits = resolveResultLimits(options.resultLimits, this.toolOutputLimits);
|
|
209
213
|
this.reversionMaintenanceTimeoutMs = resolveReversionMaintenanceTimeoutMs(options);
|
|
210
214
|
this.turnReversionProvider = options.turnReversionProvider;
|
|
211
215
|
this.reversionPolicy = options.reversionPolicy ?? 'transcript-optional';
|
|
@@ -819,6 +823,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
819
823
|
onReasoningStart: (id) => this.callbacks.onReasoningStart(state, sessionId, id),
|
|
820
824
|
onReasoningDelta: (id, delta) => this.callbacks.onReasoningDelta(state, sessionId, id, delta),
|
|
821
825
|
onReasoningEnd: (id, text) => this.callbacks.onReasoningEnd(state, sessionId, id, text),
|
|
826
|
+
onStepCommitted: () => this.callbacks.onStepCommitted(state, sessionId),
|
|
822
827
|
onToolCallStart: (event) => this.callbacks.onToolStart(state, sessionId, event),
|
|
823
828
|
onToolCallUpdate: (event) => this.callbacks.onToolUpdate(state, sessionId, event),
|
|
824
829
|
onToolCallFinish: (event) => this.callbacks.onToolFinish(state, sessionId, event),
|
|
@@ -1145,16 +1150,17 @@ export class FlexHarnessCore<TScope> {
|
|
|
1145
1150
|
}]));
|
|
1146
1151
|
const beforeTombstones = new Map([...state.tombstones].map(([id, value]) => [id, cloneSerializable(value)]));
|
|
1147
1152
|
const beforeRetainedCleanups = new Map(state.retainedSessionCleanups);
|
|
1148
|
-
|
|
1153
|
+
// The copies above stay untouched until a restore, so the scope as it was before the mutation
|
|
1154
|
+
// is built from them only when a failed save has to be reconciled.
|
|
1155
|
+
const beforeSnapshot = (): IFlexScopeSnapshot => ({
|
|
1149
1156
|
schemaVersion: 1,
|
|
1150
1157
|
revision: beforeRevision,
|
|
1151
1158
|
sessions: [...beforeSessions.values()]
|
|
1152
|
-
.map((entry) =>
|
|
1159
|
+
.map((entry) => entry.metadata)
|
|
1153
1160
|
.sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
|
|
1154
1161
|
tombstones: [...beforeTombstones.values()]
|
|
1155
|
-
.map((value) => cloneSerializable(value))
|
|
1156
1162
|
.sort((left, right) => left.sessionId.localeCompare(right.sessionId)),
|
|
1157
|
-
};
|
|
1163
|
+
});
|
|
1158
1164
|
const restore = () => {
|
|
1159
1165
|
state.revision = beforeRevision;
|
|
1160
1166
|
state.sessions.clear();
|
|
@@ -1204,7 +1210,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
1204
1210
|
}
|
|
1205
1211
|
if (
|
|
1206
1212
|
(current === undefined && beforeRevision === 0)
|
|
1207
|
-
|| (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot))
|
|
1213
|
+
|| (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot()))
|
|
1208
1214
|
) {
|
|
1209
1215
|
restore();
|
|
1210
1216
|
throw error;
|
|
@@ -1240,19 +1246,19 @@ export class FlexHarnessCore<TScope> {
|
|
|
1240
1246
|
? undefined
|
|
1241
1247
|
: cloneSerializable(stored.pendingReversion);
|
|
1242
1248
|
const beforePendingReversionReleases = cloneSerializable(stored.pendingReversionReleases);
|
|
1243
|
-
|
|
1249
|
+
// The copies above stay untouched until a restore, so the projection as it was before the
|
|
1250
|
+
// mutation is built from them only when a failed save has to be reconciled.
|
|
1251
|
+
const beforeSnapshot = (): TFlexProjectionSnapshot => beforeBaseline ?? {
|
|
1244
1252
|
schemaVersion: 3,
|
|
1245
1253
|
revision: beforeRevision,
|
|
1246
|
-
metrics:
|
|
1247
|
-
messages:
|
|
1248
|
-
stagedTerminals:
|
|
1249
|
-
reversionSegments:
|
|
1254
|
+
metrics: beforeMetrics,
|
|
1255
|
+
messages: beforeMessages,
|
|
1256
|
+
stagedTerminals: beforeStages,
|
|
1257
|
+
reversionSegments: beforeSegments,
|
|
1250
1258
|
revertCursor: beforeCursor,
|
|
1251
|
-
excludedRunIds:
|
|
1252
|
-
...(beforePendingReversion === undefined
|
|
1253
|
-
|
|
1254
|
-
: { pendingReversion: cloneSerializable(beforePendingReversion) }),
|
|
1255
|
-
pendingReversionReleases: cloneSerializable(beforePendingReversionReleases),
|
|
1259
|
+
excludedRunIds: beforeExcludedRunIds,
|
|
1260
|
+
...(beforePendingReversion === undefined ? {} : { pendingReversion: beforePendingReversion }),
|
|
1261
|
+
pendingReversionReleases: beforePendingReversionReleases,
|
|
1256
1262
|
};
|
|
1257
1263
|
const restore = () => {
|
|
1258
1264
|
stored.projectionSchemaVersion = beforeSchemaVersion;
|
|
@@ -1321,7 +1327,7 @@ export class FlexHarnessCore<TScope> {
|
|
|
1321
1327
|
}
|
|
1322
1328
|
if (
|
|
1323
1329
|
(current === undefined && beforeRevision === 0)
|
|
1324
|
-
|| (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot))
|
|
1330
|
+
|| (current !== undefined && JSON.stringify(current) === JSON.stringify(beforeSnapshot()))
|
|
1325
1331
|
) {
|
|
1326
1332
|
restore();
|
|
1327
1333
|
throw error;
|
|
@@ -29,7 +29,7 @@ export class FlexHarnessResults<TScope> {
|
|
|
29
29
|
if (!results || !run.toolCatalog) return normalizeJsonValue(output, this.core.toolOutputLimits);
|
|
30
30
|
const stored = normalizeJsonValue(output, {
|
|
31
31
|
...this.core.toolOutputLimits,
|
|
32
|
-
maxBytes:
|
|
32
|
+
maxBytes: this.core.resultLimits.maxStoredBytes,
|
|
33
33
|
});
|
|
34
34
|
const bytes = jsonBytes(stored);
|
|
35
35
|
if (bytes <= maxBytes) return stored;
|
|
@@ -53,7 +53,7 @@ export class FlexHarnessResults<TScope> {
|
|
|
53
53
|
await results.save(storageKey, run.sessionId, handle, stored);
|
|
54
54
|
// The session keeps its latest results; older ones are deleted.
|
|
55
55
|
const handles = await results.list(storageKey, run.sessionId);
|
|
56
|
-
for (const old of handles.slice(0, Math.max(0, handles.length -
|
|
56
|
+
for (const old of handles.slice(0, Math.max(0, handles.length - this.core.resultLimits.maxResultsPerSession))) {
|
|
57
57
|
await results.delete(storageKey, run.sessionId, old);
|
|
58
58
|
}
|
|
59
59
|
});
|
|
@@ -77,7 +77,7 @@ export class FlexHarnessResults<TScope> {
|
|
|
77
77
|
const method = FLEX_RESULT_METHOD;
|
|
78
78
|
const stored = await this.core.stores.results!.load(run.state.storageKey, run.sessionId, params.handle);
|
|
79
79
|
if (stored === undefined) {
|
|
80
|
-
return { ok: false, error: { error: 'unknown_handle', method, handle: params.handle, hint: `A handle reads one of the latest ${
|
|
80
|
+
return { ok: false, error: { error: 'unknown_handle', method, handle: params.handle, hint: `A handle reads one of the latest ${this.core.resultLimits.maxResultsPerSession} results this session stored.` } };
|
|
81
81
|
}
|
|
82
82
|
const path = params.path ?? '';
|
|
83
83
|
const value = selectPath(stored, path);
|
|
@@ -997,38 +997,37 @@ export class FlexHarnessReversions<TScope> {
|
|
|
997
997
|
run: IActiveRun,
|
|
998
998
|
status: 'completed' | 'failed' | 'cancelled',
|
|
999
999
|
): Promise<void> {
|
|
1000
|
-
|
|
1000
|
+
const close = await this.prepareRunReversionClose(run, status);
|
|
1001
|
+
if (close) await this.core.mutateProjection(run.state, run.stored, close, true);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Finalizes a run's workspace capture and returns the projection mutation that closes the run's
|
|
1006
|
+
* reversion segment, so a completed run saves the close together with its staged terminal.
|
|
1007
|
+
*/
|
|
1008
|
+
public async prepareRunReversionClose(
|
|
1009
|
+
run: IActiveRun,
|
|
1010
|
+
status: 'completed' | 'failed' | 'cancelled',
|
|
1011
|
+
): Promise<(() => void) | undefined> {
|
|
1012
|
+
if (run.stored.session.agent !== undefined) return undefined;
|
|
1001
1013
|
const segment = run.stored.reversionSegments.find((entry) => entry.runId === run.runId);
|
|
1002
|
-
if (!segment) return;
|
|
1003
|
-
const eventIds = run.stored.agentSession.getEvents()
|
|
1004
|
-
.filter((event) => event.generationId === run.runId)
|
|
1005
|
-
.map((event) => event.id);
|
|
1014
|
+
if (!segment) return undefined;
|
|
1006
1015
|
if (!this.core.turnReversionProvider || segment.provenance === 'transcript') {
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
segment.status = status;
|
|
1010
|
-
segment.eventIds = eventIds;
|
|
1011
|
-
if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
|
|
1012
|
-
else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
|
|
1016
|
+
return () => {
|
|
1017
|
+
this.closeRunReversionSegment(run, status);
|
|
1013
1018
|
delete run.stored.pendingReversion;
|
|
1014
1019
|
this.pruneReversionState(run.stored);
|
|
1015
|
-
}
|
|
1016
|
-
return;
|
|
1020
|
+
};
|
|
1017
1021
|
}
|
|
1018
1022
|
if (!segment.captureId) {
|
|
1019
1023
|
throw new FlexHarnessValidationError('Workspace reversion segment has no capture ownership.');
|
|
1020
1024
|
}
|
|
1021
1025
|
const captureId = segment.captureId;
|
|
1022
1026
|
if (segment.workspaceReference !== undefined) {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
segment.status = status;
|
|
1026
|
-
segment.eventIds = eventIds;
|
|
1027
|
-
if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
|
|
1028
|
-
else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
|
|
1027
|
+
return () => {
|
|
1028
|
+
this.closeRunReversionSegment(run, status);
|
|
1029
1029
|
delete run.stored.pendingReversion;
|
|
1030
|
-
}
|
|
1031
|
-
return;
|
|
1030
|
+
};
|
|
1032
1031
|
}
|
|
1033
1032
|
const pending = run.stored.pendingReversion;
|
|
1034
1033
|
if (pending?.kind !== 'capture' || pending.runId !== run.runId) {
|
|
@@ -1059,28 +1058,40 @@ export class FlexHarnessReversions<TScope> {
|
|
|
1059
1058
|
pending.state === 'prepared' ? 'finalizing' : pending.state,
|
|
1060
1059
|
);
|
|
1061
1060
|
if (outcome === undefined) {
|
|
1062
|
-
|
|
1061
|
+
return () => {
|
|
1063
1062
|
run.stored.reversionSegments = run.stored.reversionSegments.filter(
|
|
1064
1063
|
(entry) => entry.runId !== run.runId,
|
|
1065
1064
|
);
|
|
1066
1065
|
delete run.stored.pendingReversion;
|
|
1067
|
-
}
|
|
1068
|
-
return;
|
|
1066
|
+
};
|
|
1069
1067
|
}
|
|
1070
|
-
|
|
1068
|
+
return () => {
|
|
1071
1069
|
const current = run.stored.pendingReversion;
|
|
1072
1070
|
if (current?.kind !== 'capture' || current.runId !== run.runId) {
|
|
1073
1071
|
throw new FlexHarnessValidationError('Reversion capture intent changed before finalization.');
|
|
1074
1072
|
}
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
segment.eventIds = eventIds;
|
|
1078
|
-
this.applyFinalizedReversionOutcome(run.stored, segment, outcome);
|
|
1079
|
-
if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
|
|
1080
|
-
else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
|
|
1073
|
+
const closed = this.closeRunReversionSegment(run, status);
|
|
1074
|
+
this.applyFinalizedReversionOutcome(run.stored, closed, outcome);
|
|
1081
1075
|
delete run.stored.pendingReversion;
|
|
1082
1076
|
this.pruneReversionState(run.stored);
|
|
1083
|
-
}
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/** Records a run's terminal status and events on its segment and moves the revert cursor. */
|
|
1081
|
+
private closeRunReversionSegment(
|
|
1082
|
+
run: IActiveRun,
|
|
1083
|
+
status: 'completed' | 'failed' | 'cancelled',
|
|
1084
|
+
): IFlexReversionSegment {
|
|
1085
|
+
const segment = run.stored.reversionSegments.find((entry) => entry.runId === run.runId);
|
|
1086
|
+
if (!segment) throw new FlexHarnessValidationError('Reversion segment changed before finalization.');
|
|
1087
|
+
const wasCompleted = segment.status === 'completed';
|
|
1088
|
+
segment.status = status;
|
|
1089
|
+
segment.eventIds = run.stored.agentSession.getEvents()
|
|
1090
|
+
.filter((event) => event.generationId === run.runId)
|
|
1091
|
+
.map((event) => event.id);
|
|
1092
|
+
if (!wasCompleted && status === 'completed') run.stored.revertCursor++;
|
|
1093
|
+
else if (wasCompleted && status !== 'completed') run.stored.revertCursor--;
|
|
1094
|
+
return segment;
|
|
1084
1095
|
}
|
|
1085
1096
|
|
|
1086
1097
|
private applyFinalizedReversionOutcome(
|
|
@@ -119,6 +119,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
119
119
|
cacheWriteTokens: 0,
|
|
120
120
|
},
|
|
121
121
|
reasoningPartIds: new Map(),
|
|
122
|
+
completedTextPartIds: new Set(),
|
|
122
123
|
toolPartIds: new Map(),
|
|
123
124
|
trustedToolExecutionErrors: new Map(),
|
|
124
125
|
unconfirmedToolCalls: new Map(),
|
|
@@ -158,11 +159,13 @@ export class FlexHarnessRuns<TScope> {
|
|
|
158
159
|
if (run.controller.signal.aborted) throw run.controller.signal.reason;
|
|
159
160
|
const result = normalizeRunResult(rawResult);
|
|
160
161
|
if (!run.modelResolution) throw new FlexHarnessValidationError('Generation completed without a resolved model.');
|
|
161
|
-
await this.core.reversions.
|
|
162
|
+
const closeReversion = await this.core.reversions.prepareRunReversionClose(run, 'completed');
|
|
162
163
|
await this.core.admission.closeRunDelegatedRunAdmission(run);
|
|
163
164
|
const terminal = this.buildTerminal(run, 'completed', result);
|
|
164
165
|
try {
|
|
166
|
+
// The reversion segment closes in the save that stages the terminal.
|
|
165
167
|
await this.core.mutateProjection(run.state, run.stored, () => {
|
|
168
|
+
closeReversion?.();
|
|
166
169
|
this.replaceStagedTerminal(run.stored, terminal);
|
|
167
170
|
}, true);
|
|
168
171
|
} catch (error) {
|
|
@@ -808,6 +811,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
808
811
|
run.reservedAssistantMessage = publicSnapshot(nextAssistant);
|
|
809
812
|
run.callbackParts = [];
|
|
810
813
|
run.reasoningPartIds.clear();
|
|
814
|
+
run.completedTextPartIds.clear();
|
|
811
815
|
this.core.emitEvent(run.scopeId, run.stored.session, {
|
|
812
816
|
type: 'message.updated',
|
|
813
817
|
runId: run.runId,
|
|
@@ -1026,7 +1030,7 @@ export class FlexHarnessRuns<TScope> {
|
|
|
1026
1030
|
for (const part of terminal.assistantMessage.parts) {
|
|
1027
1031
|
const callbackPart = run.callbackParts.find((entry) => entry.partId === part.partId);
|
|
1028
1032
|
const shouldComplete = part.type === 'text'
|
|
1029
|
-
? callbackPart?.type === 'text'
|
|
1033
|
+
? callbackPart?.type === 'text' && !run.completedTextPartIds.has(part.partId)
|
|
1030
1034
|
: part.type === 'reasoning'
|
|
1031
1035
|
? callbackPart?.type === 'reasoning' && callbackPart.status === 'running'
|
|
1032
1036
|
: part.type === 'tool'
|
|
@@ -329,7 +329,8 @@ export class FlexHarnessToolDispatch<TScope> {
|
|
|
329
329
|
* (validation, `onCall`, the method's run with its permission requests, the output limits, error
|
|
330
330
|
* projection) and recorded as a tool part of its own, named by the method, with the `run_code`
|
|
331
331
|
* call as its parent. Proposals go to `onProposals` as one batch once the program finished; a
|
|
332
|
-
* program that fails proposes nothing.
|
|
332
|
+
* program that fails proposes nothing. The calls are recorded in the program's share of the run's
|
|
333
|
+
* callback limits; a call the share cannot hold stops the program at its `callbacks` limit.
|
|
333
334
|
*/
|
|
334
335
|
private async runCatalogProgram(
|
|
335
336
|
run: IActiveRun,
|
|
@@ -369,9 +370,24 @@ export class FlexHarnessToolDispatch<TScope> {
|
|
|
369
370
|
let callNumber = 0;
|
|
370
371
|
// Read-only calls run concurrently; any other call runs after every call the program made before it.
|
|
371
372
|
const order = new plugins.AgentToolCallScheduler(abortSignal);
|
|
373
|
+
const share = this.core.callbacks.programCallbackShare(run.state, run.sessionId);
|
|
374
|
+
let recorded = 0;
|
|
375
|
+
/** Records the start of a call; false when the run cannot record it, and the program stops. */
|
|
376
|
+
const record = (id: string, method: string, input: unknown): boolean => {
|
|
377
|
+
if (!this.core.callbacks.onProgramCallStart(run.state, run.sessionId, share, {
|
|
378
|
+
toolCallId: id, toolName: method, input, parentToolCallId: toolCallId,
|
|
379
|
+
})) return false;
|
|
380
|
+
recorded += 1;
|
|
381
|
+
return true;
|
|
382
|
+
};
|
|
383
|
+
const unrecorded = (): TFlexCodeCallOutcome => ({
|
|
384
|
+
ok: false,
|
|
385
|
+
limit: 'callbacks',
|
|
386
|
+
message: `The run cannot record more than ${recorded} call${recorded === 1 ? '' : 's'} of this program.`,
|
|
387
|
+
});
|
|
372
388
|
const finish = (id: string, method: string, input: unknown, outcome: { output: unknown } | { error: Error }) => {
|
|
373
389
|
if ('error' in outcome) run.trustedToolExecutionErrors.set(id, outcome.error);
|
|
374
|
-
this.core.callbacks.
|
|
390
|
+
this.core.callbacks.onProgramCallFinish(run.state, run.sessionId, share, 'error' in outcome
|
|
375
391
|
? { toolCallId: id, toolName: method, input, success: false, error: outcome.error.message }
|
|
376
392
|
: { toolCallId: id, toolName: method, input, success: true, output: outcome.output });
|
|
377
393
|
};
|
|
@@ -385,7 +401,7 @@ export class FlexHarnessToolDispatch<TScope> {
|
|
|
385
401
|
const call = await catalog.prepareCall(method, params);
|
|
386
402
|
if (!call.success) {
|
|
387
403
|
if (catalog.has(method)) {
|
|
388
|
-
|
|
404
|
+
if (!record(id, method, params ?? {})) return unrecorded();
|
|
389
405
|
finish(id, method, params, { error: this.catalogCallError(run, id, call.error) });
|
|
390
406
|
}
|
|
391
407
|
return { ok: false, error: call.error as unknown as TJsonValue };
|
|
@@ -396,9 +412,7 @@ export class FlexHarnessToolDispatch<TScope> {
|
|
|
396
412
|
return stopped(call.method);
|
|
397
413
|
}
|
|
398
414
|
if (signal.aborted) return stopped(call.method);
|
|
399
|
-
|
|
400
|
-
toolCallId: id, toolName: call.method, input: call.params, parentToolCallId: toolCallId,
|
|
401
|
-
});
|
|
415
|
+
if (!record(id, call.method, call.params)) return unrecorded();
|
|
402
416
|
if ('error' in classification) {
|
|
403
417
|
finish(id, call.method, call.params, { error: classification.error });
|
|
404
418
|
return { ok: false, error: { error: 'failed', method: call.method, message: classification.error.message } };
|
|
@@ -462,7 +476,9 @@ export class FlexHarnessToolDispatch<TScope> {
|
|
|
462
476
|
},
|
|
463
477
|
};
|
|
464
478
|
const result = await run.codeSandbox!.run(code, host, catalog.codeMode!, abortSignal);
|
|
465
|
-
if (!result.ok)
|
|
479
|
+
if (!result.ok) {
|
|
480
|
+
throw this.catalogCallError(run, toolCallId, normalizeJsonValue(result.failure, this.core.toolOutputLimits));
|
|
481
|
+
}
|
|
466
482
|
return {
|
|
467
483
|
result: result.value,
|
|
468
484
|
calls: result.calls,
|
package/ts/classes.stores.ts
CHANGED
|
@@ -37,15 +37,14 @@ import {
|
|
|
37
37
|
assertFlexProjectManagementTombstone,
|
|
38
38
|
} from './utils.projectmanagement.js';
|
|
39
39
|
import {
|
|
40
|
+
assertFlexToolJobSnapshot,
|
|
40
41
|
createAgentEventSnapshot,
|
|
41
42
|
createToolJobSnapshot,
|
|
42
43
|
normalizeJsonObjectUndefined,
|
|
43
44
|
validateAgentEventArchive,
|
|
44
45
|
validateAgentEventSnapshot,
|
|
45
|
-
validateToolJobSnapshot,
|
|
46
46
|
} from './utils.storecodecs.js';
|
|
47
47
|
import {
|
|
48
|
-
assertProjectionSession,
|
|
49
48
|
assertProjectManagementSaveAdmitted,
|
|
50
49
|
assertSameArchiveEvents,
|
|
51
50
|
isProjectManagementTombstoneAdmitted,
|
|
@@ -100,8 +99,11 @@ class InMemoryProjectionStore implements IFlexProjectionStore {
|
|
|
100
99
|
expectedRevision: number,
|
|
101
100
|
): Promise<void> {
|
|
102
101
|
const key = providerKey(storageKey, sessionId);
|
|
103
|
-
const validated = validateSnapshotSave(
|
|
104
|
-
|
|
102
|
+
const validated = validateSnapshotSave(
|
|
103
|
+
snapshot,
|
|
104
|
+
expectedRevision,
|
|
105
|
+
(value) => assertFlexProjectionSnapshot(value, sessionId),
|
|
106
|
+
);
|
|
105
107
|
const actualRevision = this.snapshots.get(key)?.revision ?? 0;
|
|
106
108
|
if (actualRevision !== expectedRevision) {
|
|
107
109
|
throw new FlexHarnessStoreConflictError(key, expectedRevision, actualRevision);
|
|
@@ -820,8 +822,7 @@ class JsonProjectionStore implements IFlexProjectionStore {
|
|
|
820
822
|
const filePath = this.root.sessionPath('projections', storageKey, sessionId);
|
|
821
823
|
return JsonFileRoot.inQueue(filePath, async () => {
|
|
822
824
|
const snapshot = await this.root.read(filePath, (value) => {
|
|
823
|
-
assertFlexProjectionSnapshot(value);
|
|
824
|
-
assertProjectionSession(value, sessionId);
|
|
825
|
+
assertFlexProjectionSnapshot(value, sessionId);
|
|
825
826
|
return value;
|
|
826
827
|
});
|
|
827
828
|
return snapshot ? cloneSerializable(snapshot) : undefined;
|
|
@@ -834,13 +835,15 @@ class JsonProjectionStore implements IFlexProjectionStore {
|
|
|
834
835
|
snapshot: IFlexProjectionSnapshotV3,
|
|
835
836
|
expectedRevision: number,
|
|
836
837
|
): Promise<void> {
|
|
837
|
-
const validated = validateSnapshotSave(
|
|
838
|
-
|
|
838
|
+
const validated = validateSnapshotSave(
|
|
839
|
+
snapshot,
|
|
840
|
+
expectedRevision,
|
|
841
|
+
(value) => assertFlexProjectionSnapshot(value, sessionId),
|
|
842
|
+
);
|
|
839
843
|
const filePath = this.root.sessionPath('projections', storageKey, sessionId);
|
|
840
844
|
await JsonFileRoot.inQueue(filePath, async () => {
|
|
841
845
|
const current = await this.root.read(filePath, (value) => {
|
|
842
|
-
assertFlexProjectionSnapshot(value);
|
|
843
|
-
assertProjectionSession(value, sessionId);
|
|
846
|
+
assertFlexProjectionSnapshot(value, sessionId);
|
|
844
847
|
return value;
|
|
845
848
|
});
|
|
846
849
|
const actualRevision = current?.revision ?? 0;
|
|
@@ -1214,7 +1217,10 @@ class JsonToolJobStore implements plugins.IToolJobStore {
|
|
|
1214
1217
|
const filePath = this.filePath();
|
|
1215
1218
|
return JsonFileRoot.inQueue(filePath, async () => {
|
|
1216
1219
|
abortSignal?.throwIfAborted();
|
|
1217
|
-
const snapshot = await this.root.read(filePath,
|
|
1220
|
+
const snapshot = await this.root.read(filePath, (value) => {
|
|
1221
|
+
assertFlexToolJobSnapshot(value);
|
|
1222
|
+
return value;
|
|
1223
|
+
});
|
|
1218
1224
|
abortSignal?.throwIfAborted();
|
|
1219
1225
|
return snapshot ? cloneSerializable(snapshot) : undefined;
|
|
1220
1226
|
});
|
|
@@ -1231,7 +1237,10 @@ class JsonToolJobStore implements plugins.IToolJobStore {
|
|
|
1231
1237
|
const filePath = this.filePath();
|
|
1232
1238
|
return JsonFileRoot.inQueue(filePath, async () => {
|
|
1233
1239
|
abortSignal?.throwIfAborted();
|
|
1234
|
-
const current = await this.root.read(filePath,
|
|
1240
|
+
const current = await this.root.read(filePath, (value) => {
|
|
1241
|
+
assertFlexToolJobSnapshot(value);
|
|
1242
|
+
return value;
|
|
1243
|
+
});
|
|
1235
1244
|
const actualRevision = current?.revision ?? 0;
|
|
1236
1245
|
if (actualRevision !== expectedRevision) {
|
|
1237
1246
|
throw new plugins.ToolJobStoreConflictError(expectedRevision, actualRevision);
|
package/ts/errors.ts
CHANGED
|
@@ -217,6 +217,20 @@ export class FlexHarnessStoreCommitUncertainError extends FlexHarnessError {
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/** A record the store refused to create because a fenced purge removed its storage namespace. */
|
|
221
|
+
export class FlexHarnessStorePurgedError extends FlexHarnessError {
|
|
222
|
+
public readonly path: string;
|
|
223
|
+
|
|
224
|
+
constructor(path: string) {
|
|
225
|
+
super(
|
|
226
|
+
'FLEX_STORE_PURGED',
|
|
227
|
+
`Store write at "${path}" was refused: a fenced purge removed its storage namespace.`,
|
|
228
|
+
);
|
|
229
|
+
this.name = 'FlexHarnessStorePurgedError';
|
|
230
|
+
this.path = path;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
220
234
|
export class FlexHarnessRunError extends AggregateError {
|
|
221
235
|
public readonly code = 'FLEX_RUN_FAILED';
|
|
222
236
|
|
package/ts/index.ts
CHANGED
|
@@ -9,13 +9,19 @@ export {
|
|
|
9
9
|
type TFlexToolPermissionDecision,
|
|
10
10
|
type TFlexToolPermissionRequester,
|
|
11
11
|
} from './functions.toolpermissions.js';
|
|
12
|
-
export {
|
|
12
|
+
export {
|
|
13
|
+
assertFlexPermissionSnapshot,
|
|
14
|
+
assertFlexProjectionSnapshot,
|
|
15
|
+
assertFlexScopeSnapshot,
|
|
16
|
+
normalizeJsonValue,
|
|
17
|
+
} from './utils.json.js';
|
|
13
18
|
export {
|
|
14
19
|
assertFlexProjectManagementSnapshot,
|
|
15
20
|
assertFlexProjectManagementRecord,
|
|
16
21
|
assertFlexProjectManagementTombstone,
|
|
17
22
|
createEmptyFlexProjectManagementSnapshot,
|
|
18
23
|
} from './utils.projectmanagement.js';
|
|
24
|
+
export { assertFlexToolJobSnapshot } from './utils.storecodecs.js';
|
|
19
25
|
export {
|
|
20
26
|
FLEX_SLASH_COMMAND_MAX_INPUT_BYTES,
|
|
21
27
|
isReservedSlashCommandName,
|
|
@@ -224,6 +224,11 @@ export interface IActiveRun {
|
|
|
224
224
|
usage: IFlexUsage;
|
|
225
225
|
metricsOperation?: ISessionMetricsOperation;
|
|
226
226
|
reasoningPartIds: Map<string, string>;
|
|
227
|
+
/**
|
|
228
|
+
* The text parts whose model step is committed. Each emitted its `part.completed`; text that
|
|
229
|
+
* follows starts a new part.
|
|
230
|
+
*/
|
|
231
|
+
completedTextPartIds: Set<string>;
|
|
227
232
|
toolPartIds: Map<string, string>;
|
|
228
233
|
trustedToolExecutionErrors: Map<string, Error>;
|
|
229
234
|
/**
|
|
@@ -281,6 +286,16 @@ export interface IRunReadCall {
|
|
|
281
286
|
readonly output: Promise<unknown>;
|
|
282
287
|
}
|
|
283
288
|
|
|
289
|
+
/**
|
|
290
|
+
* What is left of the room a `run_code` program's calls may take in its run's callback limits:
|
|
291
|
+
* events, output bytes and parts.
|
|
292
|
+
*/
|
|
293
|
+
export interface IProgramCallbackShare {
|
|
294
|
+
events: number;
|
|
295
|
+
outputBytes: number;
|
|
296
|
+
parts: number;
|
|
297
|
+
}
|
|
298
|
+
|
|
284
299
|
/** The steers of a run: the input its generation takes in, and the prompts it accepted for it. */
|
|
285
300
|
export interface IRunSteering {
|
|
286
301
|
readonly input: plugins.AgentPendingInputQueue;
|
package/ts/interfaces.ts
CHANGED
|
@@ -706,10 +706,28 @@ export interface IFlexToolProvider<TScope> {
|
|
|
706
706
|
/** The catalog method that reads stored results, reserved while a run stores results. */
|
|
707
707
|
export const FLEX_RESULT_METHOD = 'results.get';
|
|
708
708
|
|
|
709
|
+
/**
|
|
710
|
+
* Bounds what a session stores in the result store: at most `maxResultsPerSession` results of at
|
|
711
|
+
* most `maxStoredBytes` each.
|
|
712
|
+
*/
|
|
713
|
+
export interface IFlexResultLimits {
|
|
714
|
+
/**
|
|
715
|
+
* The most JSON bytes of one output a result stores; beyond it the stored value is truncated. At
|
|
716
|
+
* least `toolOutputLimits.maxBytes`. Defaults to `FLEX_RESULT_LIMITS.maxStoredBytes`, or to
|
|
717
|
+
* `toolOutputLimits.maxBytes` if that is larger.
|
|
718
|
+
*/
|
|
719
|
+
maxStoredBytes?: number;
|
|
720
|
+
/**
|
|
721
|
+
* The results a session keeps; a new one beyond them deletes the oldest. Defaults to
|
|
722
|
+
* `FLEX_RESULT_LIMITS.maxResultsPerSession`.
|
|
723
|
+
*/
|
|
724
|
+
maxResultsPerSession?: number;
|
|
725
|
+
}
|
|
726
|
+
|
|
709
727
|
export const FLEX_RESULT_LIMITS = Object.freeze({
|
|
710
|
-
/** The
|
|
728
|
+
/** The default of `resultLimits.maxStoredBytes`. */
|
|
711
729
|
maxStoredBytes: 8 * 1024 * 1024,
|
|
712
|
-
/** The
|
|
730
|
+
/** The default of `resultLimits.maxResultsPerSession`. */
|
|
713
731
|
maxResultsPerSession: 32,
|
|
714
732
|
/** The size of the summary the model gets in place of the output. */
|
|
715
733
|
summaryBytes: 2 * 1024,
|
|
@@ -1044,8 +1062,11 @@ export const FLEX_CODE_MODE_MAXIMUM_LIMITS: Readonly<IFlexCodeModeLimits> = Obje
|
|
|
1044
1062
|
/** What `run_code` reports to the model, as JSON, when a program does not finish. */
|
|
1045
1063
|
export interface IFlexCodeRunFailure {
|
|
1046
1064
|
error: 'syntax_error' | 'program_error' | 'limit_exceeded' | 'aborted';
|
|
1047
|
-
/**
|
|
1048
|
-
|
|
1065
|
+
/**
|
|
1066
|
+
* The limit that stopped the program, for `limit_exceeded`; `callbacks` when its run could not
|
|
1067
|
+
* record another of its calls.
|
|
1068
|
+
*/
|
|
1069
|
+
limit?: 'source' | 'cpu' | 'wall' | 'memory' | 'stack' | 'calls' | 'callbacks';
|
|
1049
1070
|
name?: string;
|
|
1050
1071
|
message: string;
|
|
1051
1072
|
/** The program's line, when known. */
|
|
@@ -1456,9 +1477,9 @@ export interface IFlexToolJobStoreProvider {
|
|
|
1456
1477
|
/**
|
|
1457
1478
|
* Tool outputs over the output budget, stored whole for the session that produced them. A result
|
|
1458
1479
|
* is written once under its handle and read by `results.get`; FlexHarness keeps a session's latest
|
|
1459
|
-
* `
|
|
1460
|
-
* session's results one operation at a time,
|
|
1461
|
-
* session.
|
|
1480
|
+
* `resultLimits.maxResultsPerSession` results, each of at most `resultLimits.maxStoredBytes` JSON
|
|
1481
|
+
* bytes, and deletes older ones. A FlexHarness writes a session's results one operation at a time,
|
|
1482
|
+
* so a store need not order concurrent writes of one session.
|
|
1462
1483
|
*/
|
|
1463
1484
|
export interface IFlexResultStore {
|
|
1464
1485
|
save(storageKey: string, sessionId: string, handle: string, value: TJsonValue): Promise<void>;
|
|
@@ -1478,8 +1499,8 @@ export interface IFlexHarnessStores {
|
|
|
1478
1499
|
jobs: IFlexToolJobStoreProvider;
|
|
1479
1500
|
/**
|
|
1480
1501
|
* Stores the outputs of a run with a tool catalog that exceed the output budget: the model gets a
|
|
1481
|
-
* summary and a handle it reads with `results.get
|
|
1482
|
-
* the budget. The built-in stores provide it.
|
|
1502
|
+
* summary and a handle it reads with `results.get`; `resultLimits` bounds what a session stores.
|
|
1503
|
+
* Without it, such outputs are truncated to the budget. The built-in stores provide it.
|
|
1483
1504
|
*/
|
|
1484
1505
|
results?: IFlexResultStore;
|
|
1485
1506
|
}
|
|
@@ -1826,6 +1847,8 @@ export interface IFlexHarnessOptions<TScope> {
|
|
|
1826
1847
|
promptQueueLimits?: IFlexPromptQueueLimits;
|
|
1827
1848
|
reversionLimits?: IFlexReversionLimits;
|
|
1828
1849
|
sessionImportLimits?: IFlexSessionImportLimits;
|
|
1850
|
+
/** Bounds the outputs over the budget that a session stores; see `stores.results`. */
|
|
1851
|
+
resultLimits?: IFlexResultLimits;
|
|
1829
1852
|
/**
|
|
1830
1853
|
* Bounds each turn-reversion maintenance operation. Falls back to
|
|
1831
1854
|
* agentSessionPolicy.generationLeaseCleanupTimeoutMs, then 30 seconds.
|
package/ts/plugins.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
3
3
|
import * as crypto from 'node:crypto';
|
|
4
4
|
import * as fs from 'node:fs/promises';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
|
+
import { Worker } from 'node:worker_threads';
|
|
6
7
|
|
|
7
|
-
export { AsyncLocalStorage, crypto, fs, path };
|
|
8
|
+
export { AsyncLocalStorage, crypto, fs, path, Worker };
|
|
8
9
|
|
|
9
10
|
// third party scope
|
|
10
11
|
import { asSchema, jsonSchema } from 'ai';
|
|
@@ -70,23 +71,19 @@ export interface IAmaroError {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
export interface ICodeSandboxEngine {
|
|
73
|
-
/** A new WebAssembly instance of QuickJS-ng, with a memory of its own. */
|
|
74
|
-
newModule(): Promise<IQuickJsModule>;
|
|
75
74
|
/** Removes TypeScript type syntax, keeping every line where it was. */
|
|
76
75
|
stripTypes(source: string): string;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Loads amaro, and finds QuickJS: the optional peers code mode needs. Every program's worker loads
|
|
80
|
+
* QuickJS itself.
|
|
81
|
+
*/
|
|
80
82
|
export const importCodeSandboxEngine = async (): Promise<ICodeSandboxEngine> => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
import('amaro'),
|
|
85
|
-
]);
|
|
86
|
-
// The variant's typings resolve as CommonJS; at run time the default export is the variant.
|
|
87
|
-
const quickjsVariant = (variant as { default: unknown }).default as Parameters<typeof core.newQuickJSWASMModuleFromVariant>[0];
|
|
83
|
+
import.meta.resolve('quickjs-emscripten-core');
|
|
84
|
+
import.meta.resolve('@jitl/quickjs-ng-wasmfile-release-sync');
|
|
85
|
+
const amaro = await import('amaro');
|
|
88
86
|
return {
|
|
89
|
-
newModule: async () => await core.newQuickJSWASMModuleFromVariant(quickjsVariant) as unknown as IQuickJsModule,
|
|
90
87
|
stripTypes: (source) => amaro.transformSync(source, { mode: 'strip-only' }).code,
|
|
91
88
|
};
|
|
92
89
|
};
|