@modelprofile.com/flexharness 3.8.0 → 4.0.1
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 +20 -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
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
errorToInfo,
|
|
19
19
|
} from './errors.js';
|
|
20
20
|
import {
|
|
21
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS,
|
|
21
22
|
FLEX_REVERSION_MAX_AFFECTED_WORKSPACES,
|
|
22
23
|
FLEX_REVERSION_DEFAULT_LIMITS,
|
|
23
24
|
FLEX_REVERSION_MAXIMUM_LIMITS,
|
|
@@ -33,6 +34,7 @@ import type {
|
|
|
33
34
|
IFlexAttachmentMessagePart,
|
|
34
35
|
IFlexBackgroundExecution,
|
|
35
36
|
IFlexCallbackLimits,
|
|
37
|
+
IFlexCreateProjectTaskInput,
|
|
36
38
|
IFlexCreateSessionOptions,
|
|
37
39
|
IFlexErrorInfo,
|
|
38
40
|
IFlexEventArchiveMetadata,
|
|
@@ -47,6 +49,15 @@ import type {
|
|
|
47
49
|
IFlexPermissionRequest,
|
|
48
50
|
IFlexPermissionRequestInput,
|
|
49
51
|
IFlexPermissionSnapshot,
|
|
52
|
+
IFlexProjectGoalResult,
|
|
53
|
+
IFlexProjectManagementSnapshot,
|
|
54
|
+
IFlexProjectManagementTombstone,
|
|
55
|
+
IFlexProjectManagementWriteContext,
|
|
56
|
+
IFlexProjectScratchpadResult,
|
|
57
|
+
IFlexProjectStateResult,
|
|
58
|
+
IFlexProjectTask,
|
|
59
|
+
IFlexProjectTaskResult,
|
|
60
|
+
IFlexProjectTasksResult,
|
|
50
61
|
IFlexPromptAdmission,
|
|
51
62
|
IFlexPromptQueueAdmission,
|
|
52
63
|
IFlexPromptQueueEntry,
|
|
@@ -81,6 +92,7 @@ import type {
|
|
|
81
92
|
IFlexUncertainToolExecution,
|
|
82
93
|
IFlexUndoSessionResult,
|
|
83
94
|
IFlexUpdateSessionOptions,
|
|
95
|
+
IFlexUpdateProjectTaskInput,
|
|
84
96
|
IFlexUsage,
|
|
85
97
|
IJsonObject,
|
|
86
98
|
TFlexAgentRunResult,
|
|
@@ -93,6 +105,8 @@ import type {
|
|
|
93
105
|
TFlexHarnessEventListener,
|
|
94
106
|
TFlexMessagePart,
|
|
95
107
|
TFlexPermissionDecision,
|
|
108
|
+
TFlexProjectTaskPriority,
|
|
109
|
+
TFlexProjectTaskStatus,
|
|
96
110
|
TFlexPrompt,
|
|
97
111
|
TFlexPromptPart,
|
|
98
112
|
TFlexPromptQueueStatus,
|
|
@@ -114,6 +128,11 @@ import {
|
|
|
114
128
|
normalizeJsonValue,
|
|
115
129
|
resolveJsonLimits,
|
|
116
130
|
} from './utils.json.js';
|
|
131
|
+
import {
|
|
132
|
+
assertFlexProjectManagementRecord,
|
|
133
|
+
assertFlexProjectManagementSnapshot,
|
|
134
|
+
createEmptyFlexProjectManagementSnapshot,
|
|
135
|
+
} from './utils.projectmanagement.js';
|
|
117
136
|
import {
|
|
118
137
|
hydrateAgentMessages,
|
|
119
138
|
normalizeFlexPrompt,
|
|
@@ -397,6 +416,47 @@ interface IFlexSubagentAcquisition {
|
|
|
397
416
|
created: boolean;
|
|
398
417
|
}
|
|
399
418
|
|
|
419
|
+
interface INormalizedProjectManagementTools {
|
|
420
|
+
readonly task: boolean;
|
|
421
|
+
readonly goal: boolean;
|
|
422
|
+
readonly scratchpad: boolean;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
interface INormalizedBuiltInTools {
|
|
426
|
+
readonly renameSession: boolean;
|
|
427
|
+
readonly projectManagement?: INormalizedProjectManagementTools;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
type TProjectTaskToolInput =
|
|
431
|
+
| { action: 'list' }
|
|
432
|
+
| {
|
|
433
|
+
action: 'create';
|
|
434
|
+
id?: string;
|
|
435
|
+
content: string;
|
|
436
|
+
status?: TFlexProjectTaskStatus;
|
|
437
|
+
priority?: TFlexProjectTaskPriority;
|
|
438
|
+
}
|
|
439
|
+
| {
|
|
440
|
+
action: 'update';
|
|
441
|
+
id: string;
|
|
442
|
+
content?: string;
|
|
443
|
+
status?: TFlexProjectTaskStatus;
|
|
444
|
+
priority?: TFlexProjectTaskPriority;
|
|
445
|
+
}
|
|
446
|
+
| { action: 'delete'; id: string }
|
|
447
|
+
| { action: 'clear' };
|
|
448
|
+
|
|
449
|
+
type TProjectGoalToolInput =
|
|
450
|
+
| { action: 'get' }
|
|
451
|
+
| { action: 'set'; goal: string }
|
|
452
|
+
| { action: 'clear' };
|
|
453
|
+
|
|
454
|
+
type TProjectScratchpadToolInput =
|
|
455
|
+
| { action: 'get' }
|
|
456
|
+
| { action: 'set'; content: string }
|
|
457
|
+
| { action: 'append'; content: string }
|
|
458
|
+
| { action: 'clear' };
|
|
459
|
+
|
|
400
460
|
type TEventDetails = Record<string, unknown> & {
|
|
401
461
|
type: TFlexHarnessEvent['type'];
|
|
402
462
|
};
|
|
@@ -444,6 +504,7 @@ const maxResourceToolNameBytes = 512;
|
|
|
444
504
|
const maxSlashCommandRegistrations = 128;
|
|
445
505
|
const maxSlashCommandDescriptionBytes = 2048;
|
|
446
506
|
const maxSlashCommandTemplateBytes = FLEX_SLASH_COMMAND_MAX_INPUT_BYTES;
|
|
507
|
+
const maxProjectManagementTombstoneConflicts = 8;
|
|
447
508
|
const resourceToolStemLength = 16;
|
|
448
509
|
const repairCancellationMessage = 'The process stopped before this run completed.';
|
|
449
510
|
const scopeRetirementMessage = 'The scope is being retired.';
|
|
@@ -455,6 +516,43 @@ const externalErrorFallback: IFlexErrorInfo = Object.freeze({
|
|
|
455
516
|
code: 'FLEX_EXTERNAL_ERROR',
|
|
456
517
|
});
|
|
457
518
|
|
|
519
|
+
function createSessionGenerationId(): string {
|
|
520
|
+
return `generation_${plugins.crypto.randomUUID()}`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function deriveLegacySessionGenerationId(
|
|
524
|
+
storageKey: string,
|
|
525
|
+
sessionId: string,
|
|
526
|
+
createdAt: string,
|
|
527
|
+
): string {
|
|
528
|
+
const digest = plugins.crypto.createHash('sha256').update(JSON.stringify([
|
|
529
|
+
'flexharness-legacy-session-generation-v1',
|
|
530
|
+
storageKey,
|
|
531
|
+
sessionId,
|
|
532
|
+
createdAt,
|
|
533
|
+
])).digest('hex');
|
|
534
|
+
return `legacy_${digest}`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function requireSessionGeneration(
|
|
538
|
+
session: Pick<
|
|
539
|
+
IFlexSession | IFlexSessionTombstone,
|
|
540
|
+
'sessionGenerationId' | 'sessionGenerationSequence'
|
|
541
|
+
>,
|
|
542
|
+
): { sessionGenerationId: string; sessionGenerationSequence: number } {
|
|
543
|
+
if (
|
|
544
|
+
!session.sessionGenerationId
|
|
545
|
+
|| !Number.isSafeInteger(session.sessionGenerationSequence)
|
|
546
|
+
|| Number(session.sessionGenerationSequence) < 1
|
|
547
|
+
) {
|
|
548
|
+
throw new FlexHarnessValidationError('Session generation metadata is missing.');
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
sessionGenerationId: session.sessionGenerationId,
|
|
552
|
+
sessionGenerationSequence: session.sessionGenerationSequence as number,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
458
556
|
function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
|
|
459
557
|
return Boolean(
|
|
460
558
|
value
|
|
@@ -663,6 +761,58 @@ function normalizeSubagents(
|
|
|
663
761
|
return Object.freeze(normalized);
|
|
664
762
|
}
|
|
665
763
|
|
|
764
|
+
function normalizeBuiltInTools(
|
|
765
|
+
options: IFlexHarnessOptions<unknown>['builtInTools'],
|
|
766
|
+
): INormalizedBuiltInTools {
|
|
767
|
+
if (options === undefined) return Object.freeze({ renameSession: false });
|
|
768
|
+
if (
|
|
769
|
+
!options
|
|
770
|
+
|| typeof options !== 'object'
|
|
771
|
+
|| Array.isArray(options)
|
|
772
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(options))
|
|
773
|
+
) {
|
|
774
|
+
throw new FlexHarnessValidationError('builtInTools must be a plain object.');
|
|
775
|
+
}
|
|
776
|
+
const unsupported = Object.keys(options)
|
|
777
|
+
.find((key) => !['renameSession', 'projectManagement'].includes(key));
|
|
778
|
+
if (unsupported) throw new FlexHarnessValidationError(`builtInTools does not support "${unsupported}".`);
|
|
779
|
+
if (options.renameSession !== undefined && typeof options.renameSession !== 'boolean') {
|
|
780
|
+
throw new FlexHarnessValidationError('builtInTools.renameSession must be a boolean.');
|
|
781
|
+
}
|
|
782
|
+
const projectManagement = options.projectManagement;
|
|
783
|
+
if (projectManagement === undefined) {
|
|
784
|
+
return Object.freeze({ renameSession: options.renameSession ?? false });
|
|
785
|
+
}
|
|
786
|
+
if (
|
|
787
|
+
!projectManagement
|
|
788
|
+
|| typeof projectManagement !== 'object'
|
|
789
|
+
|| Array.isArray(projectManagement)
|
|
790
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(projectManagement))
|
|
791
|
+
) {
|
|
792
|
+
throw new FlexHarnessValidationError('builtInTools.projectManagement must be a plain object.');
|
|
793
|
+
}
|
|
794
|
+
const unsupportedProjectOption = Object.keys(projectManagement)
|
|
795
|
+
.find((key) => !['task', 'goal', 'scratchpad'].includes(key));
|
|
796
|
+
if (unsupportedProjectOption) {
|
|
797
|
+
throw new FlexHarnessValidationError(
|
|
798
|
+
`builtInTools.projectManagement does not support "${unsupportedProjectOption}".`,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
for (const key of ['task', 'goal', 'scratchpad'] as const) {
|
|
802
|
+
if (projectManagement[key] !== undefined && typeof projectManagement[key] !== 'boolean') {
|
|
803
|
+
throw new FlexHarnessValidationError(`builtInTools.projectManagement.${key} must be a boolean.`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return Object.freeze({
|
|
807
|
+
renameSession: options.renameSession ?? false,
|
|
808
|
+
projectManagement: Object.freeze({
|
|
809
|
+
task: projectManagement.task ?? true,
|
|
810
|
+
goal: projectManagement.goal ?? true,
|
|
811
|
+
scratchpad: projectManagement.scratchpad ?? true,
|
|
812
|
+
}),
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
666
816
|
function normalizeSlashCommands<TScope>(
|
|
667
817
|
registrations: readonly TFlexSlashCommandRegistration<TScope>[] | undefined,
|
|
668
818
|
): ReadonlyMap<string, TRegisteredSlashCommand<TScope>> {
|
|
@@ -997,13 +1147,81 @@ function validateUpdateSessionOptions(options: IFlexUpdateSessionOptions): void
|
|
|
997
1147
|
const unsupportedKey = keys.find((key) => key !== 'title' && key !== 'archived');
|
|
998
1148
|
if (unsupportedKey) throw new FlexHarnessValidationError(`updateSession does not support "${unsupportedKey}".`);
|
|
999
1149
|
if (Object.prototype.hasOwnProperty.call(options, 'title') && options.title !== null) {
|
|
1000
|
-
|
|
1150
|
+
validateUtf8String(
|
|
1151
|
+
options.title,
|
|
1152
|
+
'title',
|
|
1153
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTitleBytes,
|
|
1154
|
+
true,
|
|
1155
|
+
);
|
|
1001
1156
|
}
|
|
1002
1157
|
if (Object.prototype.hasOwnProperty.call(options, 'archived') && typeof options.archived !== 'boolean') {
|
|
1003
1158
|
throw new FlexHarnessValidationError('archived must be a boolean.');
|
|
1004
1159
|
}
|
|
1005
1160
|
}
|
|
1006
1161
|
|
|
1162
|
+
function validateProjectTaskStatus(value: unknown): asserts value is TFlexProjectTaskStatus {
|
|
1163
|
+
if (!['pending', 'in_progress', 'completed', 'cancelled'].includes(String(value))) {
|
|
1164
|
+
throw new FlexHarnessValidationError('Project task status is invalid.');
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function validateProjectTaskPriority(value: unknown): asserts value is TFlexProjectTaskPriority {
|
|
1169
|
+
if (!['high', 'medium', 'low'].includes(String(value))) {
|
|
1170
|
+
throw new FlexHarnessValidationError('Project task priority is invalid.');
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function validateProjectTaskId(value: unknown): asserts value is string {
|
|
1175
|
+
validateUtf8String(
|
|
1176
|
+
value,
|
|
1177
|
+
'project task id',
|
|
1178
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskIdBytes,
|
|
1179
|
+
true,
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function validateProjectTaskContent(value: unknown): asserts value is string {
|
|
1184
|
+
validateUtf8String(
|
|
1185
|
+
value,
|
|
1186
|
+
'project task content',
|
|
1187
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskContentBytes,
|
|
1188
|
+
true,
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function validateCreateProjectTaskInput(input: IFlexCreateProjectTaskInput): void {
|
|
1193
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
1194
|
+
throw new FlexHarnessValidationError('Project task create input must be a plain object.');
|
|
1195
|
+
}
|
|
1196
|
+
const unsupported = Object.keys(input)
|
|
1197
|
+
.find((key) => !['id', 'content', 'status', 'priority'].includes(key));
|
|
1198
|
+
if (unsupported) {
|
|
1199
|
+
throw new FlexHarnessValidationError(`Project task create input does not support "${unsupported}".`);
|
|
1200
|
+
}
|
|
1201
|
+
validateProjectTaskId(input.id);
|
|
1202
|
+
validateProjectTaskContent(input.content);
|
|
1203
|
+
if (input.status !== undefined) validateProjectTaskStatus(input.status);
|
|
1204
|
+
if (input.priority !== undefined) validateProjectTaskPriority(input.priority);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function validateUpdateProjectTaskInput(input: IFlexUpdateProjectTaskInput): void {
|
|
1208
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
1209
|
+
throw new FlexHarnessValidationError('Project task update input must be a plain object.');
|
|
1210
|
+
}
|
|
1211
|
+
const keys = Object.keys(input);
|
|
1212
|
+
const unsupported = keys.find((key) => !['id', 'content', 'status', 'priority'].includes(key));
|
|
1213
|
+
if (unsupported) {
|
|
1214
|
+
throw new FlexHarnessValidationError(`Project task update input does not support "${unsupported}".`);
|
|
1215
|
+
}
|
|
1216
|
+
if (!keys.some((key) => key !== 'id')) {
|
|
1217
|
+
throw new FlexHarnessValidationError('Project task update requires a changed field.');
|
|
1218
|
+
}
|
|
1219
|
+
validateProjectTaskId(input.id);
|
|
1220
|
+
if (input.content !== undefined) validateProjectTaskContent(input.content);
|
|
1221
|
+
if (input.status !== undefined) validateProjectTaskStatus(input.status);
|
|
1222
|
+
if (input.priority !== undefined) validateProjectTaskPriority(input.priority);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1007
1225
|
function validatePromptOptions(options: IFlexPromptOptions, scheduled: boolean): void {
|
|
1008
1226
|
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
1009
1227
|
throw new FlexHarnessValidationError('Prompt options must be a plain object.');
|
|
@@ -1128,6 +1346,22 @@ function normalizeAgentSessionPolicy<TScope>(
|
|
|
1128
1346
|
};
|
|
1129
1347
|
}
|
|
1130
1348
|
|
|
1349
|
+
function requireHarnessStores(stores: IFlexHarnessStores): IFlexHarnessStores {
|
|
1350
|
+
const projectManagement = stores?.projectManagement;
|
|
1351
|
+
if (
|
|
1352
|
+
!projectManagement
|
|
1353
|
+
|| typeof projectManagement.load !== 'function'
|
|
1354
|
+
|| typeof projectManagement.save !== 'function'
|
|
1355
|
+
|| typeof projectManagement.tombstoneSession !== 'function'
|
|
1356
|
+
|| typeof projectManagement.purgeNamespace !== 'function'
|
|
1357
|
+
) {
|
|
1358
|
+
throw new FlexHarnessValidationError(
|
|
1359
|
+
'stores.projectManagement requires load/save/tombstoneSession/purgeNamespace methods.',
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
return stores;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1131
1365
|
export class FlexHarness<TScope = unknown> {
|
|
1132
1366
|
private readonly scopeResolver: IFlexHarnessOptions<TScope>['scopeResolver'];
|
|
1133
1367
|
private readonly modelResolver: IFlexHarnessOptions<TScope>['modelResolver'];
|
|
@@ -1144,6 +1378,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1144
1378
|
private readonly reversionPolicy: TFlexReversionPolicy;
|
|
1145
1379
|
private readonly externalErrorProjector?: TFlexExternalErrorProjector;
|
|
1146
1380
|
private readonly subagents: ReadonlyMap<string, Readonly<IFlexSubagentDefinition>>;
|
|
1381
|
+
private readonly builtInTools: INormalizedBuiltInTools;
|
|
1147
1382
|
private readonly slashCommands: ReadonlyMap<string, TRegisteredSlashCommand<TScope>>;
|
|
1148
1383
|
private readonly maxSubagentDepth: number;
|
|
1149
1384
|
private readonly maxSubagentCallsPerRun: number;
|
|
@@ -1164,6 +1399,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1164
1399
|
private readonly orphanedTombstoneCleanups = new Map<string, IOrphanedTombstoneCleanup>();
|
|
1165
1400
|
private readonly orphanedTombstoneOwners = new Map<string, IOrphanedTombstoneOwner<TScope>>();
|
|
1166
1401
|
private readonly pendingPromptAdmissionOwners = new Set<IPendingPromptAdmission>();
|
|
1402
|
+
private readonly projectManagementQueues = new Map<string, Promise<void>>();
|
|
1167
1403
|
private readonly activeSlashCommandExecutions = new Map<string, IActiveSlashCommandExecution>();
|
|
1168
1404
|
private readonly activeSlashCommandListings = new Set<IActiveSlashCommandListing>();
|
|
1169
1405
|
private readonly compactorInvocationContext = new plugins.AsyncLocalStorage<
|
|
@@ -1195,7 +1431,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1195
1431
|
this.toolProvider = options.toolProvider;
|
|
1196
1432
|
this.resourceToolProviderResolver = options.resourceToolProviderResolver;
|
|
1197
1433
|
this.executionContextProvider = options.executionContextProvider;
|
|
1198
|
-
this.stores = options.stores ?? new InMemoryFlexHarnessStores();
|
|
1434
|
+
this.stores = requireHarnessStores(options.stores ?? new InMemoryFlexHarnessStores());
|
|
1199
1435
|
this.agentSessionPolicy = normalizeAgentSessionPolicy(options.agentSessionPolicy);
|
|
1200
1436
|
this.toolOutputLimits = resolveJsonLimits(options.toolOutputLimits);
|
|
1201
1437
|
this.callbackLimits = resolveCallbackLimits(options.callbackLimits);
|
|
@@ -1222,6 +1458,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1222
1458
|
}
|
|
1223
1459
|
this.externalErrorProjector = options.externalErrorProjector;
|
|
1224
1460
|
this.subagents = normalizeSubagents(options.subagents);
|
|
1461
|
+
this.builtInTools = normalizeBuiltInTools(options.builtInTools);
|
|
1225
1462
|
this.slashCommands = normalizeSlashCommands(options.slashCommands);
|
|
1226
1463
|
this.maxSubagentDepth = resolveBoundedPositiveInteger(
|
|
1227
1464
|
options.maxSubagentDepth,
|
|
@@ -1261,7 +1498,14 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1261
1498
|
const sessionId = options.sessionId ?? plugins.crypto.randomUUID();
|
|
1262
1499
|
validateIdentifier(sessionId, 'sessionId');
|
|
1263
1500
|
requireTransferIdentifier(sessionId, 'sessionId');
|
|
1264
|
-
if (options.title !== undefined)
|
|
1501
|
+
if (options.title !== undefined) {
|
|
1502
|
+
validateUtf8String(
|
|
1503
|
+
options.title,
|
|
1504
|
+
'title',
|
|
1505
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTitleBytes,
|
|
1506
|
+
true,
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1265
1509
|
await resolved.state.scopeQueue;
|
|
1266
1510
|
if (
|
|
1267
1511
|
resolved.state.sessions.has(sessionId)
|
|
@@ -1296,6 +1540,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1296
1540
|
metadata = {
|
|
1297
1541
|
scopeId,
|
|
1298
1542
|
sessionId,
|
|
1543
|
+
sessionGenerationId: createSessionGenerationId(),
|
|
1544
|
+
sessionGenerationSequence: resolved.state.revision + 1,
|
|
1299
1545
|
...(options.title ? { title: options.title } : {}),
|
|
1300
1546
|
createdAt: timestamp,
|
|
1301
1547
|
updatedAt: timestamp,
|
|
@@ -1341,6 +1587,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1341
1587
|
if (!scopeReserved) throw projectedError;
|
|
1342
1588
|
const tombstone: IFlexSessionTombstone = {
|
|
1343
1589
|
sessionId,
|
|
1590
|
+
...requireSessionGeneration(metadata),
|
|
1344
1591
|
deletedAt: new Date().toISOString(),
|
|
1345
1592
|
rootSessionId: sessionId,
|
|
1346
1593
|
depth: 0,
|
|
@@ -1468,6 +1715,181 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1468
1715
|
return result;
|
|
1469
1716
|
}
|
|
1470
1717
|
|
|
1718
|
+
public async getProjectState(
|
|
1719
|
+
scopeId: string,
|
|
1720
|
+
sessionId: string,
|
|
1721
|
+
): Promise<IFlexProjectStateResult> {
|
|
1722
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1723
|
+
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1724
|
+
return this.projectStateResult(snapshot);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
public async listProjectTasks(
|
|
1728
|
+
scopeId: string,
|
|
1729
|
+
sessionId: string,
|
|
1730
|
+
): Promise<IFlexProjectTasksResult> {
|
|
1731
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1732
|
+
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1733
|
+
return this.projectTasksResult(snapshot, snapshot.tasks);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
public async createProjectTask(
|
|
1737
|
+
scopeId: string,
|
|
1738
|
+
sessionId: string,
|
|
1739
|
+
input: IFlexCreateProjectTaskInput,
|
|
1740
|
+
): Promise<IFlexProjectTaskResult> {
|
|
1741
|
+
validateCreateProjectTaskInput(input);
|
|
1742
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1743
|
+
return this.createProjectTaskInState(
|
|
1744
|
+
state,
|
|
1745
|
+
stored,
|
|
1746
|
+
input,
|
|
1747
|
+
Object.freeze({ actor: 'application' }),
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
public async updateProjectTask(
|
|
1752
|
+
scopeId: string,
|
|
1753
|
+
sessionId: string,
|
|
1754
|
+
input: IFlexUpdateProjectTaskInput,
|
|
1755
|
+
): Promise<IFlexProjectTaskResult> {
|
|
1756
|
+
validateUpdateProjectTaskInput(input);
|
|
1757
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1758
|
+
return this.updateProjectTaskInState(
|
|
1759
|
+
state,
|
|
1760
|
+
stored,
|
|
1761
|
+
input,
|
|
1762
|
+
Object.freeze({ actor: 'application' }),
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
public async deleteProjectTask(
|
|
1767
|
+
scopeId: string,
|
|
1768
|
+
sessionId: string,
|
|
1769
|
+
taskId: string,
|
|
1770
|
+
): Promise<IFlexProjectTaskResult> {
|
|
1771
|
+
validateProjectTaskId(taskId);
|
|
1772
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1773
|
+
return this.deleteProjectTaskInState(
|
|
1774
|
+
state,
|
|
1775
|
+
stored,
|
|
1776
|
+
taskId,
|
|
1777
|
+
Object.freeze({ actor: 'application' }),
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
public async clearProjectTasks(
|
|
1782
|
+
scopeId: string,
|
|
1783
|
+
sessionId: string,
|
|
1784
|
+
): Promise<IFlexProjectTasksResult> {
|
|
1785
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1786
|
+
return this.clearProjectTasksInState(
|
|
1787
|
+
state,
|
|
1788
|
+
stored,
|
|
1789
|
+
Object.freeze({ actor: 'application' }),
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
public async getProjectGoal(
|
|
1794
|
+
scopeId: string,
|
|
1795
|
+
sessionId: string,
|
|
1796
|
+
): Promise<IFlexProjectGoalResult> {
|
|
1797
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1798
|
+
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1799
|
+
return this.projectGoalResult(snapshot);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
public async setProjectGoal(
|
|
1803
|
+
scopeId: string,
|
|
1804
|
+
sessionId: string,
|
|
1805
|
+
goal: string,
|
|
1806
|
+
): Promise<IFlexProjectGoalResult> {
|
|
1807
|
+
validateUtf8String(
|
|
1808
|
+
goal,
|
|
1809
|
+
'project goal',
|
|
1810
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxGoalBytes,
|
|
1811
|
+
true,
|
|
1812
|
+
);
|
|
1813
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1814
|
+
return this.setProjectGoalInState(
|
|
1815
|
+
state,
|
|
1816
|
+
stored,
|
|
1817
|
+
goal,
|
|
1818
|
+
Object.freeze({ actor: 'application' }),
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
public async clearProjectGoal(
|
|
1823
|
+
scopeId: string,
|
|
1824
|
+
sessionId: string,
|
|
1825
|
+
): Promise<IFlexProjectGoalResult> {
|
|
1826
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1827
|
+
return this.clearProjectGoalInState(
|
|
1828
|
+
state,
|
|
1829
|
+
stored,
|
|
1830
|
+
Object.freeze({ actor: 'application' }),
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
public async getProjectScratchpad(
|
|
1835
|
+
scopeId: string,
|
|
1836
|
+
sessionId: string,
|
|
1837
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
1838
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1839
|
+
const snapshot = await this.readProjectManagementState(state, stored);
|
|
1840
|
+
return this.projectScratchpadResult(snapshot);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
public async setProjectScratchpad(
|
|
1844
|
+
scopeId: string,
|
|
1845
|
+
sessionId: string,
|
|
1846
|
+
content: string,
|
|
1847
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
1848
|
+
validateUtf8String(
|
|
1849
|
+
content,
|
|
1850
|
+
'project scratchpad',
|
|
1851
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
1852
|
+
);
|
|
1853
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1854
|
+
return this.setProjectScratchpadInState(
|
|
1855
|
+
state,
|
|
1856
|
+
stored,
|
|
1857
|
+
content,
|
|
1858
|
+
Object.freeze({ actor: 'application' }),
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
public async appendProjectScratchpad(
|
|
1863
|
+
scopeId: string,
|
|
1864
|
+
sessionId: string,
|
|
1865
|
+
content: string,
|
|
1866
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
1867
|
+
validateUtf8String(
|
|
1868
|
+
content,
|
|
1869
|
+
'project scratchpad append',
|
|
1870
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
1871
|
+
);
|
|
1872
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1873
|
+
return this.appendProjectScratchpadInState(
|
|
1874
|
+
state,
|
|
1875
|
+
stored,
|
|
1876
|
+
content,
|
|
1877
|
+
Object.freeze({ actor: 'application' }),
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
public async clearProjectScratchpad(
|
|
1882
|
+
scopeId: string,
|
|
1883
|
+
sessionId: string,
|
|
1884
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
1885
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId);
|
|
1886
|
+
return this.clearProjectScratchpadInState(
|
|
1887
|
+
state,
|
|
1888
|
+
stored,
|
|
1889
|
+
Object.freeze({ actor: 'application' }),
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1471
1893
|
public async deleteSession(scopeId: string, sessionId: string): Promise<void> {
|
|
1472
1894
|
this.assertNoAmbientSlashCommandTeardown({ scopeId, sessionId });
|
|
1473
1895
|
const { scope, state } = await this.resolveState(scopeId);
|
|
@@ -1536,6 +1958,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
1536
1958
|
});
|
|
1537
1959
|
state.tombstones.set(entrySessionId, {
|
|
1538
1960
|
sessionId: entrySessionId,
|
|
1961
|
+
...requireSessionGeneration(entry.session),
|
|
1539
1962
|
deletedAt,
|
|
1540
1963
|
rootSessionId: sessionId,
|
|
1541
1964
|
depth: (entry.session.depth ?? rootDepth) - rootDepth,
|
|
@@ -2007,7 +2430,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
2007
2430
|
)) return 'Session already has an active slash command execution.';
|
|
2008
2431
|
if (stored.pendingReversion) return 'Session has a pending reversion operation.';
|
|
2009
2432
|
if (stored.session.agent !== undefined) {
|
|
2010
|
-
return 'Subagent sessions can only be prompted through the foreground
|
|
2433
|
+
return 'Subagent sessions can only be prompted through the foreground delegate tool.';
|
|
2011
2434
|
}
|
|
2012
2435
|
if (stored.outstandingPromptsById.size >= this.promptQueueLimits.maxOutstandingPromptsPerSession) {
|
|
2013
2436
|
return 'Session has reached its outstanding prompt limit.';
|
|
@@ -3287,7 +3710,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3287
3710
|
}
|
|
3288
3711
|
if (stored.session.agent !== undefined && !subagentAdmission) {
|
|
3289
3712
|
throw new FlexHarnessValidationError(
|
|
3290
|
-
'Subagent sessions can only be prompted through the foreground
|
|
3713
|
+
'Subagent sessions can only be prompted through the foreground delegate tool.',
|
|
3291
3714
|
);
|
|
3292
3715
|
}
|
|
3293
3716
|
if (stored.outstandingPromptsById.size >= this.promptQueueLimits.maxOutstandingPromptsPerSession) {
|
|
@@ -3894,6 +4317,412 @@ export class FlexHarness<TScope = unknown> {
|
|
|
3894
4317
|
throw errors.length > 1 ? combineErrors(errors) : terminalError;
|
|
3895
4318
|
}
|
|
3896
4319
|
|
|
4320
|
+
private requireProjectManagementTools(): INormalizedProjectManagementTools {
|
|
4321
|
+
const projectManagement = this.builtInTools.projectManagement;
|
|
4322
|
+
if (!projectManagement) {
|
|
4323
|
+
throw new FlexHarnessValidationError('Project management is not configured for this FlexHarness.');
|
|
4324
|
+
}
|
|
4325
|
+
return projectManagement;
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4328
|
+
private projectManagementKey(storageKey: string, sessionId: string): string {
|
|
4329
|
+
return JSON.stringify([storageKey, sessionId]);
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
private queueProjectManagementOperation<TResult>(
|
|
4333
|
+
storageKey: string,
|
|
4334
|
+
sessionId: string,
|
|
4335
|
+
operation: () => Promise<TResult>,
|
|
4336
|
+
): Promise<TResult> {
|
|
4337
|
+
const key = this.projectManagementKey(storageKey, sessionId);
|
|
4338
|
+
const previous = this.projectManagementQueues.get(key) ?? Promise.resolve();
|
|
4339
|
+
const result = previous.then(operation);
|
|
4340
|
+
const barrier = result.then(() => undefined, () => undefined);
|
|
4341
|
+
this.projectManagementQueues.set(key, barrier);
|
|
4342
|
+
void barrier.then(() => {
|
|
4343
|
+
if (this.projectManagementQueues.get(key) === barrier) {
|
|
4344
|
+
this.projectManagementQueues.delete(key);
|
|
4345
|
+
}
|
|
4346
|
+
});
|
|
4347
|
+
return result;
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4350
|
+
private async awaitProjectManagementOperations(
|
|
4351
|
+
storageKey: string,
|
|
4352
|
+
sessionIds: Iterable<string>,
|
|
4353
|
+
): Promise<void> {
|
|
4354
|
+
const barriers = [...new Set(sessionIds)]
|
|
4355
|
+
.map((sessionId) => this.projectManagementQueues.get(
|
|
4356
|
+
this.projectManagementKey(storageKey, sessionId),
|
|
4357
|
+
))
|
|
4358
|
+
.filter((barrier): barrier is Promise<void> => barrier !== undefined);
|
|
4359
|
+
await Promise.all(barriers);
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
private async resolveProjectManagementSession(
|
|
4363
|
+
scopeId: string,
|
|
4364
|
+
sessionId: string,
|
|
4365
|
+
): Promise<{ state: IStorageState; stored: IStoredSessionState }> {
|
|
4366
|
+
this.requireProjectManagementTools();
|
|
4367
|
+
const { state } = await this.resolveState(scopeId);
|
|
4368
|
+
await state.scopeQueue;
|
|
4369
|
+
this.assertOpen();
|
|
4370
|
+
this.assertStateAcceptingWork(state);
|
|
4371
|
+
return { state, stored: this.requireSession(state, sessionId) };
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
private assertProjectManagementSession(
|
|
4375
|
+
state: IStorageState,
|
|
4376
|
+
stored: IStoredSessionState,
|
|
4377
|
+
): void {
|
|
4378
|
+
this.assertOpen();
|
|
4379
|
+
this.assertStateAcceptingWork(state);
|
|
4380
|
+
if (
|
|
4381
|
+
state.sessions.get(stored.session.sessionId) !== stored
|
|
4382
|
+
|| state.tombstones.has(stored.session.sessionId)
|
|
4383
|
+
) {
|
|
4384
|
+
throw new FlexHarnessNotFoundError('Session', stored.session.sessionId);
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
|
|
4388
|
+
private async loadProjectManagementState(
|
|
4389
|
+
state: IStorageState,
|
|
4390
|
+
stored: IStoredSessionState,
|
|
4391
|
+
): Promise<IFlexProjectManagementSnapshot> {
|
|
4392
|
+
const store = this.stores.projectManagement;
|
|
4393
|
+
const generation = requireSessionGeneration(stored.session);
|
|
4394
|
+
const record = await store.load(state.storageKey, stored.session.sessionId);
|
|
4395
|
+
if (record === undefined) {
|
|
4396
|
+
return createEmptyFlexProjectManagementSnapshot(
|
|
4397
|
+
generation.sessionGenerationId,
|
|
4398
|
+
generation.sessionGenerationSequence,
|
|
4399
|
+
);
|
|
4400
|
+
}
|
|
4401
|
+
assertFlexProjectManagementRecord(record);
|
|
4402
|
+
if (record.sessionGenerationId === generation.sessionGenerationId) {
|
|
4403
|
+
if (record.sessionGenerationSequence !== generation.sessionGenerationSequence) {
|
|
4404
|
+
throw new FlexHarnessStoreConflictError(
|
|
4405
|
+
this.projectManagementKey(state.storageKey, stored.session.sessionId),
|
|
4406
|
+
0,
|
|
4407
|
+
record.revision,
|
|
4408
|
+
);
|
|
4409
|
+
}
|
|
4410
|
+
if (!('deletedAt' in record)) return cloneSerializable(record);
|
|
4411
|
+
throw new FlexHarnessNotFoundError('Project management state', stored.session.sessionId);
|
|
4412
|
+
}
|
|
4413
|
+
if (
|
|
4414
|
+
'deletedAt' in record
|
|
4415
|
+
&& record.sessionGenerationSequence < generation.sessionGenerationSequence
|
|
4416
|
+
) {
|
|
4417
|
+
return createEmptyFlexProjectManagementSnapshot(
|
|
4418
|
+
generation.sessionGenerationId,
|
|
4419
|
+
generation.sessionGenerationSequence,
|
|
4420
|
+
);
|
|
4421
|
+
}
|
|
4422
|
+
throw new FlexHarnessStoreConflictError(
|
|
4423
|
+
this.projectManagementKey(state.storageKey, stored.session.sessionId),
|
|
4424
|
+
0,
|
|
4425
|
+
record.revision,
|
|
4426
|
+
);
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4429
|
+
private readProjectManagementState(
|
|
4430
|
+
state: IStorageState,
|
|
4431
|
+
stored: IStoredSessionState,
|
|
4432
|
+
): Promise<IFlexProjectManagementSnapshot> {
|
|
4433
|
+
return this.queueProjectManagementOperation(
|
|
4434
|
+
state.storageKey,
|
|
4435
|
+
stored.session.sessionId,
|
|
4436
|
+
async () => {
|
|
4437
|
+
this.assertProjectManagementSession(state, stored);
|
|
4438
|
+
const snapshot = await this.loadProjectManagementState(state, stored);
|
|
4439
|
+
this.assertProjectManagementSession(state, stored);
|
|
4440
|
+
return publicSnapshot(snapshot);
|
|
4441
|
+
},
|
|
4442
|
+
);
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
private mutateProjectManagementState<TResult>(
|
|
4446
|
+
state: IStorageState,
|
|
4447
|
+
stored: IStoredSessionState,
|
|
4448
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4449
|
+
mutation: (
|
|
4450
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
4451
|
+
) => { changed: boolean; value: TResult },
|
|
4452
|
+
): Promise<{ snapshot: IFlexProjectManagementSnapshot; value: TResult }> {
|
|
4453
|
+
return this.queueProjectManagementOperation(
|
|
4454
|
+
state.storageKey,
|
|
4455
|
+
stored.session.sessionId,
|
|
4456
|
+
async () => {
|
|
4457
|
+
this.assertProjectManagementSession(state, stored);
|
|
4458
|
+
const snapshot = await this.loadProjectManagementState(state, stored);
|
|
4459
|
+
this.assertProjectManagementSession(state, stored);
|
|
4460
|
+
const expectedRevision = snapshot.revision;
|
|
4461
|
+
const result = mutation(snapshot);
|
|
4462
|
+
if (result.changed) {
|
|
4463
|
+
snapshot.revision = expectedRevision + 1;
|
|
4464
|
+
assertFlexProjectManagementSnapshot(snapshot);
|
|
4465
|
+
this.assertProjectManagementSession(state, stored);
|
|
4466
|
+
await this.stores.projectManagement.save(
|
|
4467
|
+
state.storageKey,
|
|
4468
|
+
stored.session.sessionId,
|
|
4469
|
+
cloneSerializable(snapshot),
|
|
4470
|
+
expectedRevision,
|
|
4471
|
+
Object.freeze(cloneSerializable(writeContext)),
|
|
4472
|
+
);
|
|
4473
|
+
}
|
|
4474
|
+
return {
|
|
4475
|
+
snapshot: publicSnapshot(snapshot),
|
|
4476
|
+
value: result.value === undefined ? result.value : publicSnapshot(result.value),
|
|
4477
|
+
};
|
|
4478
|
+
},
|
|
4479
|
+
);
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4482
|
+
private projectStateResult(snapshot: IFlexProjectManagementSnapshot): IFlexProjectStateResult {
|
|
4483
|
+
return publicSnapshot({ revision: snapshot.revision, state: snapshot });
|
|
4484
|
+
}
|
|
4485
|
+
|
|
4486
|
+
private projectTaskResult(
|
|
4487
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
4488
|
+
task: IFlexProjectTask,
|
|
4489
|
+
): IFlexProjectTaskResult {
|
|
4490
|
+
return publicSnapshot({ revision: snapshot.revision, task, state: snapshot });
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
private projectTasksResult(
|
|
4494
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
4495
|
+
tasks: IFlexProjectTask[],
|
|
4496
|
+
): IFlexProjectTasksResult {
|
|
4497
|
+
return publicSnapshot({ revision: snapshot.revision, tasks, state: snapshot });
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4500
|
+
private projectGoalResult(snapshot: IFlexProjectManagementSnapshot): IFlexProjectGoalResult {
|
|
4501
|
+
return publicSnapshot({
|
|
4502
|
+
revision: snapshot.revision,
|
|
4503
|
+
goal: snapshot.goal ?? null,
|
|
4504
|
+
state: snapshot,
|
|
4505
|
+
});
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4508
|
+
private projectScratchpadResult(
|
|
4509
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
4510
|
+
): IFlexProjectScratchpadResult {
|
|
4511
|
+
return publicSnapshot({
|
|
4512
|
+
revision: snapshot.revision,
|
|
4513
|
+
scratchpad: snapshot.scratchpad,
|
|
4514
|
+
state: snapshot,
|
|
4515
|
+
});
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
private async createProjectTaskInState(
|
|
4519
|
+
state: IStorageState,
|
|
4520
|
+
stored: IStoredSessionState,
|
|
4521
|
+
input: IFlexCreateProjectTaskInput,
|
|
4522
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4523
|
+
): Promise<IFlexProjectTaskResult> {
|
|
4524
|
+
const status = input.status ?? 'pending';
|
|
4525
|
+
const priority = input.priority ?? 'medium';
|
|
4526
|
+
const result = await this.mutateProjectManagementState(
|
|
4527
|
+
state,
|
|
4528
|
+
stored,
|
|
4529
|
+
writeContext,
|
|
4530
|
+
(snapshot) => {
|
|
4531
|
+
const existing = snapshot.tasks.find((task) => task.id === input.id);
|
|
4532
|
+
if (existing) {
|
|
4533
|
+
if (
|
|
4534
|
+
existing.content === input.content
|
|
4535
|
+
&& existing.status === status
|
|
4536
|
+
&& existing.priority === priority
|
|
4537
|
+
) return { changed: false, value: existing };
|
|
4538
|
+
throw new FlexHarnessValidationError(
|
|
4539
|
+
`Project task "${input.id}" already exists with different creation data.`,
|
|
4540
|
+
);
|
|
4541
|
+
}
|
|
4542
|
+
if (snapshot.tasks.length >= FLEX_PROJECT_MANAGEMENT_LIMITS.maxTasks) {
|
|
4543
|
+
throw new FlexHarnessValidationError('Project task count has reached its limit.');
|
|
4544
|
+
}
|
|
4545
|
+
const timestamp = new Date().toISOString();
|
|
4546
|
+
const task: IFlexProjectTask = {
|
|
4547
|
+
id: input.id,
|
|
4548
|
+
content: input.content,
|
|
4549
|
+
status,
|
|
4550
|
+
priority,
|
|
4551
|
+
createdAt: timestamp,
|
|
4552
|
+
updatedAt: timestamp,
|
|
4553
|
+
};
|
|
4554
|
+
snapshot.tasks.push(task);
|
|
4555
|
+
return { changed: true, value: task };
|
|
4556
|
+
},
|
|
4557
|
+
);
|
|
4558
|
+
return this.projectTaskResult(result.snapshot, result.value);
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
private async updateProjectTaskInState(
|
|
4562
|
+
state: IStorageState,
|
|
4563
|
+
stored: IStoredSessionState,
|
|
4564
|
+
input: IFlexUpdateProjectTaskInput,
|
|
4565
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4566
|
+
): Promise<IFlexProjectTaskResult> {
|
|
4567
|
+
const result = await this.mutateProjectManagementState(
|
|
4568
|
+
state,
|
|
4569
|
+
stored,
|
|
4570
|
+
writeContext,
|
|
4571
|
+
(snapshot) => {
|
|
4572
|
+
const task = snapshot.tasks.find((entry) => entry.id === input.id);
|
|
4573
|
+
if (!task) throw new FlexHarnessNotFoundError('Project task', input.id);
|
|
4574
|
+
const changed = (input.content !== undefined && input.content !== task.content)
|
|
4575
|
+
|| (input.status !== undefined && input.status !== task.status)
|
|
4576
|
+
|| (input.priority !== undefined && input.priority !== task.priority);
|
|
4577
|
+
if (!changed) return { changed: false, value: task };
|
|
4578
|
+
if (input.content !== undefined) task.content = input.content;
|
|
4579
|
+
if (input.status !== undefined) task.status = input.status;
|
|
4580
|
+
if (input.priority !== undefined) task.priority = input.priority;
|
|
4581
|
+
task.updatedAt = new Date().toISOString();
|
|
4582
|
+
return { changed: true, value: task };
|
|
4583
|
+
},
|
|
4584
|
+
);
|
|
4585
|
+
return this.projectTaskResult(result.snapshot, result.value);
|
|
4586
|
+
}
|
|
4587
|
+
|
|
4588
|
+
private async deleteProjectTaskInState(
|
|
4589
|
+
state: IStorageState,
|
|
4590
|
+
stored: IStoredSessionState,
|
|
4591
|
+
taskId: string,
|
|
4592
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4593
|
+
): Promise<IFlexProjectTaskResult> {
|
|
4594
|
+
const result = await this.mutateProjectManagementState(
|
|
4595
|
+
state,
|
|
4596
|
+
stored,
|
|
4597
|
+
writeContext,
|
|
4598
|
+
(snapshot) => {
|
|
4599
|
+
const index = snapshot.tasks.findIndex((task) => task.id === taskId);
|
|
4600
|
+
if (index < 0) throw new FlexHarnessNotFoundError('Project task', taskId);
|
|
4601
|
+
const [task] = snapshot.tasks.splice(index, 1);
|
|
4602
|
+
return { changed: true, value: task };
|
|
4603
|
+
},
|
|
4604
|
+
);
|
|
4605
|
+
return this.projectTaskResult(result.snapshot, result.value);
|
|
4606
|
+
}
|
|
4607
|
+
|
|
4608
|
+
private async clearProjectTasksInState(
|
|
4609
|
+
state: IStorageState,
|
|
4610
|
+
stored: IStoredSessionState,
|
|
4611
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4612
|
+
): Promise<IFlexProjectTasksResult> {
|
|
4613
|
+
const result = await this.mutateProjectManagementState(
|
|
4614
|
+
state,
|
|
4615
|
+
stored,
|
|
4616
|
+
writeContext,
|
|
4617
|
+
(snapshot) => {
|
|
4618
|
+
const tasks = snapshot.tasks;
|
|
4619
|
+
if (tasks.length === 0) return { changed: false, value: [] as IFlexProjectTask[] };
|
|
4620
|
+
snapshot.tasks = [];
|
|
4621
|
+
return { changed: true, value: tasks };
|
|
4622
|
+
},
|
|
4623
|
+
);
|
|
4624
|
+
return this.projectTasksResult(result.snapshot, result.value);
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
private async setProjectGoalInState(
|
|
4628
|
+
state: IStorageState,
|
|
4629
|
+
stored: IStoredSessionState,
|
|
4630
|
+
goal: string,
|
|
4631
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4632
|
+
): Promise<IFlexProjectGoalResult> {
|
|
4633
|
+
const result = await this.mutateProjectManagementState(
|
|
4634
|
+
state,
|
|
4635
|
+
stored,
|
|
4636
|
+
writeContext,
|
|
4637
|
+
(snapshot) => {
|
|
4638
|
+
if (snapshot.goal === goal) return { changed: false, value: undefined };
|
|
4639
|
+
snapshot.goal = goal;
|
|
4640
|
+
return { changed: true, value: undefined };
|
|
4641
|
+
},
|
|
4642
|
+
);
|
|
4643
|
+
return this.projectGoalResult(result.snapshot);
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
private async clearProjectGoalInState(
|
|
4647
|
+
state: IStorageState,
|
|
4648
|
+
stored: IStoredSessionState,
|
|
4649
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4650
|
+
): Promise<IFlexProjectGoalResult> {
|
|
4651
|
+
const result = await this.mutateProjectManagementState(
|
|
4652
|
+
state,
|
|
4653
|
+
stored,
|
|
4654
|
+
writeContext,
|
|
4655
|
+
(snapshot) => {
|
|
4656
|
+
if (snapshot.goal === undefined) return { changed: false, value: undefined };
|
|
4657
|
+
delete snapshot.goal;
|
|
4658
|
+
return { changed: true, value: undefined };
|
|
4659
|
+
},
|
|
4660
|
+
);
|
|
4661
|
+
return this.projectGoalResult(result.snapshot);
|
|
4662
|
+
}
|
|
4663
|
+
|
|
4664
|
+
private async setProjectScratchpadInState(
|
|
4665
|
+
state: IStorageState,
|
|
4666
|
+
stored: IStoredSessionState,
|
|
4667
|
+
content: string,
|
|
4668
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4669
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
4670
|
+
const result = await this.mutateProjectManagementState(
|
|
4671
|
+
state,
|
|
4672
|
+
stored,
|
|
4673
|
+
writeContext,
|
|
4674
|
+
(snapshot) => {
|
|
4675
|
+
if (snapshot.scratchpad === content) return { changed: false, value: undefined };
|
|
4676
|
+
snapshot.scratchpad = content;
|
|
4677
|
+
return { changed: true, value: undefined };
|
|
4678
|
+
},
|
|
4679
|
+
);
|
|
4680
|
+
return this.projectScratchpadResult(result.snapshot);
|
|
4681
|
+
}
|
|
4682
|
+
|
|
4683
|
+
private async appendProjectScratchpadInState(
|
|
4684
|
+
state: IStorageState,
|
|
4685
|
+
stored: IStoredSessionState,
|
|
4686
|
+
content: string,
|
|
4687
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4688
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
4689
|
+
const result = await this.mutateProjectManagementState(
|
|
4690
|
+
state,
|
|
4691
|
+
stored,
|
|
4692
|
+
writeContext,
|
|
4693
|
+
(snapshot) => {
|
|
4694
|
+
if (content.length === 0) return { changed: false, value: undefined };
|
|
4695
|
+
const appended = `${snapshot.scratchpad}${content}`;
|
|
4696
|
+
validateUtf8String(
|
|
4697
|
+
appended,
|
|
4698
|
+
'project scratchpad',
|
|
4699
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
4700
|
+
);
|
|
4701
|
+
snapshot.scratchpad = appended;
|
|
4702
|
+
return { changed: true, value: undefined };
|
|
4703
|
+
},
|
|
4704
|
+
);
|
|
4705
|
+
return this.projectScratchpadResult(result.snapshot);
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4708
|
+
private async clearProjectScratchpadInState(
|
|
4709
|
+
state: IStorageState,
|
|
4710
|
+
stored: IStoredSessionState,
|
|
4711
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
4712
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
4713
|
+
const result = await this.mutateProjectManagementState(
|
|
4714
|
+
state,
|
|
4715
|
+
stored,
|
|
4716
|
+
writeContext,
|
|
4717
|
+
(snapshot) => {
|
|
4718
|
+
if (snapshot.scratchpad.length === 0) return { changed: false, value: undefined };
|
|
4719
|
+
snapshot.scratchpad = '';
|
|
4720
|
+
return { changed: true, value: undefined };
|
|
4721
|
+
},
|
|
4722
|
+
);
|
|
4723
|
+
return this.projectScratchpadResult(result.snapshot);
|
|
4724
|
+
}
|
|
4725
|
+
|
|
3897
4726
|
private async prepareGeneration(
|
|
3898
4727
|
run: IActiveRun,
|
|
3899
4728
|
options: IFlexPromptOptions,
|
|
@@ -4006,17 +4835,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4006
4835
|
let tools: TFlexAgentToolSet | undefined;
|
|
4007
4836
|
try {
|
|
4008
4837
|
const providedTools = toolHandle?.tools;
|
|
4009
|
-
if (
|
|
4010
|
-
this.subagents.size > 0
|
|
4011
|
-
&& providedTools
|
|
4012
|
-
&& Object.prototype.hasOwnProperty.call(providedTools, 'task')
|
|
4013
|
-
) {
|
|
4014
|
-
throw new FlexHarnessValidationError('The application tool provider cannot define reserved tool "task".');
|
|
4015
|
-
}
|
|
4016
4838
|
const combinedTools: Record<string, unknown> = { ...(providedTools ?? {}) };
|
|
4017
|
-
|
|
4018
|
-
combinedTools.task = this.createSubagentTool(run);
|
|
4019
|
-
}
|
|
4839
|
+
Object.assign(combinedTools, this.createBuiltInTools(run));
|
|
4020
4840
|
tools = Object.keys(combinedTools).length > 0
|
|
4021
4841
|
? wrapToolSet(
|
|
4022
4842
|
combinedTools as TFlexAgentToolSet,
|
|
@@ -4067,6 +4887,283 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4067
4887
|
}
|
|
4068
4888
|
}
|
|
4069
4889
|
|
|
4890
|
+
private enabledBuiltInToolNames(run: IActiveRun): Set<string> {
|
|
4891
|
+
const names = new Set<string>();
|
|
4892
|
+
if (this.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
|
|
4893
|
+
names.add('delegate');
|
|
4894
|
+
}
|
|
4895
|
+
if (this.builtInTools.renameSession) names.add('rename_session');
|
|
4896
|
+
const projectManagement = this.builtInTools.projectManagement;
|
|
4897
|
+
if (projectManagement?.task) names.add('task');
|
|
4898
|
+
if (projectManagement?.goal) names.add('goal');
|
|
4899
|
+
if (projectManagement?.scratchpad) names.add('scratchpad');
|
|
4900
|
+
return names;
|
|
4901
|
+
}
|
|
4902
|
+
|
|
4903
|
+
private assertNoBuiltInToolCollisions(run: IActiveRun, tools: TFlexAgentToolSet): void {
|
|
4904
|
+
const reserved = this.enabledBuiltInToolNames(run);
|
|
4905
|
+
const collision = Object.keys(tools).find((name) => reserved.has(name));
|
|
4906
|
+
if (collision) {
|
|
4907
|
+
throw new FlexHarnessValidationError(
|
|
4908
|
+
`Tool provider cannot define enabled built-in tool "${collision}".`,
|
|
4909
|
+
);
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4913
|
+
private createBuiltInTools(run: IActiveRun): Record<string, unknown> {
|
|
4914
|
+
const tools: Record<string, unknown> = {};
|
|
4915
|
+
if (this.subagents.size > 0 && (run.stored.session.depth ?? 0) < this.maxSubagentDepth) {
|
|
4916
|
+
tools.delegate = this.createSubagentTool(run);
|
|
4917
|
+
}
|
|
4918
|
+
if (this.builtInTools.renameSession) tools.rename_session = this.createRenameSessionTool(run);
|
|
4919
|
+
const projectManagement = this.builtInTools.projectManagement;
|
|
4920
|
+
if (projectManagement?.task) tools.task = this.createProjectTaskTool(run);
|
|
4921
|
+
if (projectManagement?.goal) tools.goal = this.createProjectGoalTool(run);
|
|
4922
|
+
if (projectManagement?.scratchpad) tools.scratchpad = this.createProjectScratchpadTool(run);
|
|
4923
|
+
return tools;
|
|
4924
|
+
}
|
|
4925
|
+
|
|
4926
|
+
private createRenameSessionTool(run: IActiveRun): unknown {
|
|
4927
|
+
return plugins.tool({
|
|
4928
|
+
description: 'Set the current FlexHarness session title.',
|
|
4929
|
+
inputSchema: plugins.z.object({
|
|
4930
|
+
title: plugins.z.string().min(1).max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxTitleBytes),
|
|
4931
|
+
}).strict(),
|
|
4932
|
+
execute: async ({ title }: { title: string }) => {
|
|
4933
|
+
validateUtf8String(
|
|
4934
|
+
title,
|
|
4935
|
+
'title',
|
|
4936
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxTitleBytes,
|
|
4937
|
+
true,
|
|
4938
|
+
);
|
|
4939
|
+
let session!: IFlexSession;
|
|
4940
|
+
await this.mutateScope(run.state, () => {
|
|
4941
|
+
if (run.state.sessions.get(run.sessionId) !== run.stored) {
|
|
4942
|
+
throw new FlexHarnessNotFoundError('Session', run.sessionId);
|
|
4943
|
+
}
|
|
4944
|
+
run.stored.session.title = title;
|
|
4945
|
+
run.stored.session.updatedAt = new Date().toISOString();
|
|
4946
|
+
session = publicSnapshot(run.stored.session);
|
|
4947
|
+
});
|
|
4948
|
+
this.emitEvent(run.scopeId, run.sessionId, {
|
|
4949
|
+
type: 'session.updated',
|
|
4950
|
+
session,
|
|
4951
|
+
});
|
|
4952
|
+
return { session };
|
|
4953
|
+
},
|
|
4954
|
+
});
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4957
|
+
private createProjectTaskTool(run: IActiveRun): unknown {
|
|
4958
|
+
const status = plugins.z.enum(['pending', 'in_progress', 'completed', 'cancelled']);
|
|
4959
|
+
const priority = plugins.z.enum(['high', 'medium', 'low']);
|
|
4960
|
+
const id = plugins.z.string().min(1).max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskIdBytes);
|
|
4961
|
+
const content = plugins.z.string().min(1).max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskContentBytes);
|
|
4962
|
+
return plugins.tool({
|
|
4963
|
+
description: 'List, create, update, delete, or clear session-local project tasks.',
|
|
4964
|
+
inputSchema: plugins.z.discriminatedUnion('action', [
|
|
4965
|
+
plugins.z.object({ action: plugins.z.literal('list') }).strict(),
|
|
4966
|
+
plugins.z.object({
|
|
4967
|
+
action: plugins.z.literal('create'),
|
|
4968
|
+
id: id.optional(),
|
|
4969
|
+
content,
|
|
4970
|
+
status: status.optional(),
|
|
4971
|
+
priority: priority.optional(),
|
|
4972
|
+
}).strict(),
|
|
4973
|
+
plugins.z.object({
|
|
4974
|
+
action: plugins.z.literal('update'),
|
|
4975
|
+
id,
|
|
4976
|
+
content: content.optional(),
|
|
4977
|
+
status: status.optional(),
|
|
4978
|
+
priority: priority.optional(),
|
|
4979
|
+
}).strict().refine(
|
|
4980
|
+
(input) => input.content !== undefined
|
|
4981
|
+
|| input.status !== undefined
|
|
4982
|
+
|| input.priority !== undefined,
|
|
4983
|
+
{ message: 'Task update requires content, status, or priority.' },
|
|
4984
|
+
),
|
|
4985
|
+
plugins.z.object({ action: plugins.z.literal('delete'), id }).strict(),
|
|
4986
|
+
plugins.z.object({ action: plugins.z.literal('clear') }).strict(),
|
|
4987
|
+
]),
|
|
4988
|
+
execute: (
|
|
4989
|
+
input: TProjectTaskToolInput,
|
|
4990
|
+
options?: { toolCallId?: string },
|
|
4991
|
+
) => this.executeProjectTaskTool(run, input, options?.toolCallId),
|
|
4992
|
+
});
|
|
4993
|
+
}
|
|
4994
|
+
|
|
4995
|
+
private createProjectGoalTool(run: IActiveRun): unknown {
|
|
4996
|
+
return plugins.tool({
|
|
4997
|
+
description: 'Get, set, or clear the session-local project goal.',
|
|
4998
|
+
inputSchema: plugins.z.discriminatedUnion('action', [
|
|
4999
|
+
plugins.z.object({ action: plugins.z.literal('get') }).strict(),
|
|
5000
|
+
plugins.z.object({
|
|
5001
|
+
action: plugins.z.literal('set'),
|
|
5002
|
+
goal: plugins.z.string().min(1).max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxGoalBytes),
|
|
5003
|
+
}).strict(),
|
|
5004
|
+
plugins.z.object({ action: plugins.z.literal('clear') }).strict(),
|
|
5005
|
+
]),
|
|
5006
|
+
execute: (
|
|
5007
|
+
input: TProjectGoalToolInput,
|
|
5008
|
+
options?: { toolCallId?: string },
|
|
5009
|
+
) => this.executeProjectGoalTool(run, input, options?.toolCallId),
|
|
5010
|
+
});
|
|
5011
|
+
}
|
|
5012
|
+
|
|
5013
|
+
private createProjectScratchpadTool(run: IActiveRun): unknown {
|
|
5014
|
+
const content = plugins.z.string().max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes);
|
|
5015
|
+
return plugins.tool({
|
|
5016
|
+
description: 'Get, set, append to, or clear the session-local project scratchpad.',
|
|
5017
|
+
inputSchema: plugins.z.discriminatedUnion('action', [
|
|
5018
|
+
plugins.z.object({ action: plugins.z.literal('get') }).strict(),
|
|
5019
|
+
plugins.z.object({ action: plugins.z.literal('set'), content }).strict(),
|
|
5020
|
+
plugins.z.object({ action: plugins.z.literal('append'), content }).strict(),
|
|
5021
|
+
plugins.z.object({ action: plugins.z.literal('clear') }).strict(),
|
|
5022
|
+
]),
|
|
5023
|
+
execute: (
|
|
5024
|
+
input: TProjectScratchpadToolInput,
|
|
5025
|
+
options?: { toolCallId?: string },
|
|
5026
|
+
) => this.executeProjectScratchpadTool(run, input, options?.toolCallId),
|
|
5027
|
+
});
|
|
5028
|
+
}
|
|
5029
|
+
|
|
5030
|
+
private projectManagementAgentWriteContext(
|
|
5031
|
+
run: IActiveRun,
|
|
5032
|
+
toolCallId: string | undefined,
|
|
5033
|
+
): IFlexProjectManagementWriteContext {
|
|
5034
|
+
return Object.freeze({
|
|
5035
|
+
actor: 'agent' as const,
|
|
5036
|
+
runId: run.runId,
|
|
5037
|
+
...(run.stored.session.agent === undefined ? {} : { agent: run.stored.session.agent }),
|
|
5038
|
+
...(toolCallId === undefined ? {} : { toolCallId }),
|
|
5039
|
+
});
|
|
5040
|
+
}
|
|
5041
|
+
|
|
5042
|
+
private async executeProjectTaskTool(
|
|
5043
|
+
run: IActiveRun,
|
|
5044
|
+
input: TProjectTaskToolInput,
|
|
5045
|
+
toolCallId: string | undefined,
|
|
5046
|
+
): Promise<IFlexProjectStateResult | IFlexProjectTaskResult | IFlexProjectTasksResult> {
|
|
5047
|
+
this.assertProjectManagementSession(run.state, run.stored);
|
|
5048
|
+
if (input.action === 'list') {
|
|
5049
|
+
const snapshot = await this.readProjectManagementState(run.state, run.stored);
|
|
5050
|
+
return this.projectTasksResult(snapshot, snapshot.tasks);
|
|
5051
|
+
}
|
|
5052
|
+
const writeContext = this.projectManagementAgentWriteContext(run, toolCallId);
|
|
5053
|
+
if (input.action === 'create') {
|
|
5054
|
+
const id = input.id ?? (() => {
|
|
5055
|
+
validateUtf8String(
|
|
5056
|
+
toolCallId,
|
|
5057
|
+
'task toolCallId',
|
|
5058
|
+
maxTransferIdentifierBytes,
|
|
5059
|
+
true,
|
|
5060
|
+
);
|
|
5061
|
+
return this.createProjectTaskId(
|
|
5062
|
+
run.state.storageKey,
|
|
5063
|
+
run.sessionId,
|
|
5064
|
+
run.runId,
|
|
5065
|
+
toolCallId,
|
|
5066
|
+
);
|
|
5067
|
+
})();
|
|
5068
|
+
const createInput: IFlexCreateProjectTaskInput = {
|
|
5069
|
+
id,
|
|
5070
|
+
content: input.content,
|
|
5071
|
+
...(input.status === undefined ? {} : { status: input.status }),
|
|
5072
|
+
...(input.priority === undefined ? {} : { priority: input.priority }),
|
|
5073
|
+
};
|
|
5074
|
+
validateCreateProjectTaskInput(createInput);
|
|
5075
|
+
return this.createProjectTaskInState(run.state, run.stored, createInput, writeContext);
|
|
5076
|
+
}
|
|
5077
|
+
if (input.action === 'update') {
|
|
5078
|
+
const updateInput: IFlexUpdateProjectTaskInput = {
|
|
5079
|
+
id: input.id,
|
|
5080
|
+
...(input.content === undefined ? {} : { content: input.content }),
|
|
5081
|
+
...(input.status === undefined ? {} : { status: input.status }),
|
|
5082
|
+
...(input.priority === undefined ? {} : { priority: input.priority }),
|
|
5083
|
+
};
|
|
5084
|
+
validateUpdateProjectTaskInput(updateInput);
|
|
5085
|
+
return this.updateProjectTaskInState(run.state, run.stored, updateInput, writeContext);
|
|
5086
|
+
}
|
|
5087
|
+
if (input.action === 'delete') {
|
|
5088
|
+
validateProjectTaskId(input.id);
|
|
5089
|
+
return this.deleteProjectTaskInState(run.state, run.stored, input.id, writeContext);
|
|
5090
|
+
}
|
|
5091
|
+
return this.clearProjectTasksInState(run.state, run.stored, writeContext);
|
|
5092
|
+
}
|
|
5093
|
+
|
|
5094
|
+
private async executeProjectGoalTool(
|
|
5095
|
+
run: IActiveRun,
|
|
5096
|
+
input: TProjectGoalToolInput,
|
|
5097
|
+
toolCallId: string | undefined,
|
|
5098
|
+
): Promise<IFlexProjectGoalResult> {
|
|
5099
|
+
this.assertProjectManagementSession(run.state, run.stored);
|
|
5100
|
+
if (input.action === 'get') {
|
|
5101
|
+
return this.projectGoalResult(await this.readProjectManagementState(run.state, run.stored));
|
|
5102
|
+
}
|
|
5103
|
+
const writeContext = this.projectManagementAgentWriteContext(run, toolCallId);
|
|
5104
|
+
if (input.action === 'set') {
|
|
5105
|
+
validateUtf8String(
|
|
5106
|
+
input.goal,
|
|
5107
|
+
'project goal',
|
|
5108
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxGoalBytes,
|
|
5109
|
+
true,
|
|
5110
|
+
);
|
|
5111
|
+
return this.setProjectGoalInState(run.state, run.stored, input.goal, writeContext);
|
|
5112
|
+
}
|
|
5113
|
+
return this.clearProjectGoalInState(run.state, run.stored, writeContext);
|
|
5114
|
+
}
|
|
5115
|
+
|
|
5116
|
+
private async executeProjectScratchpadTool(
|
|
5117
|
+
run: IActiveRun,
|
|
5118
|
+
input: TProjectScratchpadToolInput,
|
|
5119
|
+
toolCallId: string | undefined,
|
|
5120
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
5121
|
+
this.assertProjectManagementSession(run.state, run.stored);
|
|
5122
|
+
if (input.action === 'get') {
|
|
5123
|
+
return this.projectScratchpadResult(
|
|
5124
|
+
await this.readProjectManagementState(run.state, run.stored),
|
|
5125
|
+
);
|
|
5126
|
+
}
|
|
5127
|
+
const writeContext = this.projectManagementAgentWriteContext(run, toolCallId);
|
|
5128
|
+
if (input.action === 'set') {
|
|
5129
|
+
validateUtf8String(
|
|
5130
|
+
input.content,
|
|
5131
|
+
'project scratchpad',
|
|
5132
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
5133
|
+
);
|
|
5134
|
+
return this.setProjectScratchpadInState(run.state, run.stored, input.content, writeContext);
|
|
5135
|
+
}
|
|
5136
|
+
if (input.action === 'append') {
|
|
5137
|
+
validateUtf8String(
|
|
5138
|
+
input.content,
|
|
5139
|
+
'project scratchpad append',
|
|
5140
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
5141
|
+
);
|
|
5142
|
+
return this.appendProjectScratchpadInState(
|
|
5143
|
+
run.state,
|
|
5144
|
+
run.stored,
|
|
5145
|
+
input.content,
|
|
5146
|
+
writeContext,
|
|
5147
|
+
);
|
|
5148
|
+
}
|
|
5149
|
+
return this.clearProjectScratchpadInState(run.state, run.stored, writeContext);
|
|
5150
|
+
}
|
|
5151
|
+
|
|
5152
|
+
private createProjectTaskId(
|
|
5153
|
+
storageKey: string,
|
|
5154
|
+
sessionId: string,
|
|
5155
|
+
runId: string,
|
|
5156
|
+
toolCallId: string,
|
|
5157
|
+
): string {
|
|
5158
|
+
return `task_${sha256Hex(JSON.stringify([
|
|
5159
|
+
'flexharness-project-task-v1',
|
|
5160
|
+
storageKey,
|
|
5161
|
+
sessionId,
|
|
5162
|
+
runId,
|
|
5163
|
+
toolCallId,
|
|
5164
|
+
]))}`;
|
|
5165
|
+
}
|
|
5166
|
+
|
|
4070
5167
|
private createSubagentTool(run: IActiveRun): unknown {
|
|
4071
5168
|
const available = [...this.subagents.values()]
|
|
4072
5169
|
.map((definition) => `- ${definition.name}: ${definition.description}`)
|
|
@@ -4097,16 +5194,16 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4097
5194
|
}
|
|
4098
5195
|
validateUtf8String(
|
|
4099
5196
|
input.description,
|
|
4100
|
-
'
|
|
5197
|
+
'delegate description',
|
|
4101
5198
|
maxSubagentTaskDescriptionBytes,
|
|
4102
5199
|
true,
|
|
4103
5200
|
);
|
|
4104
|
-
validateUtf8String(input.prompt, '
|
|
5201
|
+
validateUtf8String(input.prompt, 'delegate prompt', maxSubagentPromptBytes, true);
|
|
4105
5202
|
validateUtf8String(input.subagentType, 'subagentType', maxSubagentNameBytes, true);
|
|
4106
5203
|
if (input.taskId !== undefined) {
|
|
4107
5204
|
validateUtf8String(input.taskId, 'taskId', maxSubagentTaskIdBytes, true);
|
|
4108
5205
|
}
|
|
4109
|
-
validateUtf8String(toolCallId, '
|
|
5206
|
+
validateUtf8String(toolCallId, 'delegate toolCallId', maxTransferIdentifierBytes, true);
|
|
4110
5207
|
const definition = this.subagents.get(input.subagentType);
|
|
4111
5208
|
if (!definition || (run.stored.session.depth ?? 0) >= this.maxSubagentDepth) {
|
|
4112
5209
|
throw new FlexHarnessValidationError(`Subagent "${input.subagentType}" is not available.`);
|
|
@@ -4330,6 +5427,8 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4330
5427
|
metadata = {
|
|
4331
5428
|
scopeId: run.scopeId,
|
|
4332
5429
|
sessionId,
|
|
5430
|
+
sessionGenerationId: createSessionGenerationId(),
|
|
5431
|
+
sessionGenerationSequence: state.revision + 1,
|
|
4333
5432
|
title: `Subagent: ${definition.name}`,
|
|
4334
5433
|
createdAt: timestamp,
|
|
4335
5434
|
updatedAt: timestamp,
|
|
@@ -4403,6 +5502,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4403
5502
|
state.sessions.delete(sessionId);
|
|
4404
5503
|
state.tombstones.set(sessionId, {
|
|
4405
5504
|
sessionId,
|
|
5505
|
+
...requireSessionGeneration(metadata!),
|
|
4406
5506
|
deletedAt: new Date().toISOString(),
|
|
4407
5507
|
rootSessionId: sessionId,
|
|
4408
5508
|
depth: 0,
|
|
@@ -4458,12 +5558,12 @@ export class FlexHarness<TScope = unknown> {
|
|
|
4458
5558
|
const partId = run.toolPartIds.get(toolCallId);
|
|
4459
5559
|
const part = run.callbackParts.find((entry) => entry.partId === partId && entry.type === 'tool');
|
|
4460
5560
|
if (!part || part.type !== 'tool' || part.status !== 'running') {
|
|
4461
|
-
throw new FlexHarnessValidationError(`Running
|
|
5561
|
+
throw new FlexHarnessValidationError(`Running delegate part "${toolCallId}" is unavailable.`);
|
|
4462
5562
|
}
|
|
4463
5563
|
const bytes = Buffer.byteLength(childSessionId, 'utf8')
|
|
4464
5564
|
+ (model === undefined ? 0 : jsonBytes(model));
|
|
4465
5565
|
if (!this.reserveCallbackCapacity(run, 1, bytes, 0)) {
|
|
4466
|
-
throw run.callbackError ?? new FlexHarnessCallbackOverflowError('
|
|
5566
|
+
throw run.callbackError ?? new FlexHarnessCallbackOverflowError('Delegate metadata exceeded callback limits.');
|
|
4467
5567
|
}
|
|
4468
5568
|
part.childSessionId = childSessionId;
|
|
4469
5569
|
if (model !== undefined) part.model = cloneSerializable(model);
|
|
@@ -6264,6 +7364,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6264
7364
|
throw new FlexHarnessValidationError('Tool handle close must be a function.');
|
|
6265
7365
|
}
|
|
6266
7366
|
const tools = handle.tools;
|
|
7367
|
+
this.assertNoBuiltInToolCollisions(run, tools);
|
|
6267
7368
|
const closeHandle = close;
|
|
6268
7369
|
return {
|
|
6269
7370
|
tools,
|
|
@@ -6603,6 +7704,28 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6603
7704
|
sessions: [],
|
|
6604
7705
|
tombstones: [],
|
|
6605
7706
|
} satisfies IFlexScopeSnapshot);
|
|
7707
|
+
let scopeChanged = false;
|
|
7708
|
+
const repairedGenerationSequence = snapshot.revision + 1;
|
|
7709
|
+
for (const session of snapshot.sessions) {
|
|
7710
|
+
if (session.sessionGenerationId !== undefined) continue;
|
|
7711
|
+
session.sessionGenerationId = deriveLegacySessionGenerationId(
|
|
7712
|
+
storageKey,
|
|
7713
|
+
session.sessionId,
|
|
7714
|
+
session.createdAt,
|
|
7715
|
+
);
|
|
7716
|
+
session.sessionGenerationSequence = repairedGenerationSequence;
|
|
7717
|
+
scopeChanged = true;
|
|
7718
|
+
}
|
|
7719
|
+
for (const tombstone of snapshot.tombstones) {
|
|
7720
|
+
if (tombstone.sessionGenerationId !== undefined) continue;
|
|
7721
|
+
tombstone.sessionGenerationId = deriveLegacySessionGenerationId(
|
|
7722
|
+
storageKey,
|
|
7723
|
+
tombstone.sessionId,
|
|
7724
|
+
tombstone.deletedAt,
|
|
7725
|
+
);
|
|
7726
|
+
tombstone.sessionGenerationSequence = repairedGenerationSequence;
|
|
7727
|
+
scopeChanged = true;
|
|
7728
|
+
}
|
|
6606
7729
|
const state: IStorageState = {
|
|
6607
7730
|
storageKey,
|
|
6608
7731
|
compactorLifecycleController,
|
|
@@ -6625,7 +7748,6 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6625
7748
|
detachedCleanups: new Set(),
|
|
6626
7749
|
};
|
|
6627
7750
|
const loadedSessions: IStoredSessionState[] = [];
|
|
6628
|
-
let scopeChanged = false;
|
|
6629
7751
|
try {
|
|
6630
7752
|
for (const metadata of snapshot.sessions) {
|
|
6631
7753
|
if (state.tombstones.has(metadata.sessionId)) continue;
|
|
@@ -6677,7 +7799,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
6677
7799
|
scopeId,
|
|
6678
7800
|
scope,
|
|
6679
7801
|
);
|
|
6680
|
-
await this.cleanupSessionDomains(storageKey, tombstone
|
|
7802
|
+
await this.cleanupSessionDomains(storageKey, tombstone);
|
|
6681
7803
|
}
|
|
6682
7804
|
for (const tombstone of group) state.tombstones.delete(tombstone.sessionId);
|
|
6683
7805
|
scopeChanged = true;
|
|
@@ -7637,12 +8759,26 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7637
8759
|
stored.projectionReconciliationRequired = false;
|
|
7638
8760
|
}
|
|
7639
8761
|
|
|
7640
|
-
private async cleanupSessionDomains(
|
|
8762
|
+
private async cleanupSessionDomains(
|
|
8763
|
+
storageKey: string,
|
|
8764
|
+
tombstone: IFlexSessionTombstone,
|
|
8765
|
+
): Promise<void> {
|
|
8766
|
+
const sessionId = tombstone.sessionId;
|
|
8767
|
+
const projectManagementCleanup = async (): Promise<void> => {
|
|
8768
|
+
await this.projectManagementQueues.get(
|
|
8769
|
+
this.projectManagementKey(storageKey, sessionId),
|
|
8770
|
+
);
|
|
8771
|
+
await this.tombstoneProjectManagementSession(
|
|
8772
|
+
storageKey,
|
|
8773
|
+
tombstone,
|
|
8774
|
+
);
|
|
8775
|
+
};
|
|
7641
8776
|
const results = await Promise.allSettled([
|
|
7642
8777
|
this.stores.agentEvents.deleteSession(storageKey, sessionId),
|
|
7643
8778
|
this.stores.jobs.deleteSession(storageKey, sessionId),
|
|
7644
8779
|
this.stores.projections.deleteSession(storageKey, sessionId),
|
|
7645
8780
|
this.stores.permissions.deleteSession(storageKey, sessionId),
|
|
8781
|
+
projectManagementCleanup(),
|
|
7646
8782
|
]);
|
|
7647
8783
|
const errors = results
|
|
7648
8784
|
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
@@ -7650,6 +8786,66 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7650
8786
|
if (errors.length > 0) throw combineErrors(errors);
|
|
7651
8787
|
}
|
|
7652
8788
|
|
|
8789
|
+
private async tombstoneProjectManagementSession(
|
|
8790
|
+
storageKey: string,
|
|
8791
|
+
sessionTombstone: IFlexSessionTombstone,
|
|
8792
|
+
): Promise<void> {
|
|
8793
|
+
const store = this.stores.projectManagement;
|
|
8794
|
+
const sessionId = sessionTombstone.sessionId;
|
|
8795
|
+
const generation = requireSessionGeneration(sessionTombstone);
|
|
8796
|
+
let lastConflict: FlexHarnessStoreConflictError | undefined;
|
|
8797
|
+
for (let attempt = 0; attempt <= maxProjectManagementTombstoneConflicts; attempt++) {
|
|
8798
|
+
const current = await store.load(storageKey, sessionId);
|
|
8799
|
+
if (current !== undefined) {
|
|
8800
|
+
assertFlexProjectManagementRecord(current);
|
|
8801
|
+
if (current.sessionGenerationId === generation.sessionGenerationId) {
|
|
8802
|
+
if (current.sessionGenerationSequence !== generation.sessionGenerationSequence) {
|
|
8803
|
+
throw new FlexHarnessStoreConflictError(
|
|
8804
|
+
this.projectManagementKey(storageKey, sessionId),
|
|
8805
|
+
0,
|
|
8806
|
+
current.revision,
|
|
8807
|
+
);
|
|
8808
|
+
}
|
|
8809
|
+
if ('deletedAt' in current) return;
|
|
8810
|
+
} else if (
|
|
8811
|
+
!('deletedAt' in current)
|
|
8812
|
+
|| current.sessionGenerationSequence >= generation.sessionGenerationSequence
|
|
8813
|
+
) {
|
|
8814
|
+
throw new FlexHarnessStoreConflictError(
|
|
8815
|
+
this.projectManagementKey(storageKey, sessionId),
|
|
8816
|
+
0,
|
|
8817
|
+
current.revision,
|
|
8818
|
+
);
|
|
8819
|
+
}
|
|
8820
|
+
}
|
|
8821
|
+
if (attempt === maxProjectManagementTombstoneConflicts) break;
|
|
8822
|
+
const expectedRevision = current === undefined
|
|
8823
|
+
|| current.sessionGenerationId !== generation.sessionGenerationId
|
|
8824
|
+
? 0
|
|
8825
|
+
: current.revision;
|
|
8826
|
+
const tombstone: IFlexProjectManagementTombstone = {
|
|
8827
|
+
schemaVersion: 1,
|
|
8828
|
+
revision: expectedRevision + 1,
|
|
8829
|
+
...generation,
|
|
8830
|
+
deletedAt: sessionTombstone.deletedAt,
|
|
8831
|
+
};
|
|
8832
|
+
try {
|
|
8833
|
+
await store.tombstoneSession(
|
|
8834
|
+
storageKey,
|
|
8835
|
+
sessionId,
|
|
8836
|
+
tombstone,
|
|
8837
|
+
expectedRevision,
|
|
8838
|
+
);
|
|
8839
|
+
} catch (error) {
|
|
8840
|
+
if (!(error instanceof FlexHarnessStoreConflictError)) throw error;
|
|
8841
|
+
lastConflict = error;
|
|
8842
|
+
}
|
|
8843
|
+
}
|
|
8844
|
+
throw lastConflict ?? new FlexHarnessValidationError(
|
|
8845
|
+
`Project management tombstone for session "${sessionId}" could not be durably confirmed.`,
|
|
8846
|
+
);
|
|
8847
|
+
}
|
|
8848
|
+
|
|
7653
8849
|
private collectSessionSubtree(
|
|
7654
8850
|
state: IStorageState,
|
|
7655
8851
|
rootSessionId: string,
|
|
@@ -7845,7 +9041,7 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7845
9041
|
releaseContext.scope,
|
|
7846
9042
|
retained?.stored,
|
|
7847
9043
|
);
|
|
7848
|
-
await this.cleanupSessionDomains(state.storageKey,
|
|
9044
|
+
await this.cleanupSessionDomains(state.storageKey, tombstone);
|
|
7849
9045
|
if (retained) retained.domainsCompleted = true;
|
|
7850
9046
|
}
|
|
7851
9047
|
}
|
|
@@ -7925,6 +9121,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
7925
9121
|
await Promise.allSettled([...state.sessionInitializations.values()]);
|
|
7926
9122
|
cleanupErrors.push(...await this.closeOrphanedResources(state.storageKey));
|
|
7927
9123
|
await state.scopeQueue;
|
|
9124
|
+
await this.awaitProjectManagementOperations(
|
|
9125
|
+
state.storageKey,
|
|
9126
|
+
[
|
|
9127
|
+
...state.sessions.keys(),
|
|
9128
|
+
...state.retainedSessionCleanups.keys(),
|
|
9129
|
+
],
|
|
9130
|
+
);
|
|
7928
9131
|
const currentTombstoneRoot = state.tombstones.get(currentRun.sessionId)?.rootSessionId
|
|
7929
9132
|
?? currentRun.sessionId;
|
|
7930
9133
|
const currentTombstoneCleanup = state.tombstoneCleanups.get(currentTombstoneRoot);
|
|
@@ -8458,6 +9661,13 @@ export class FlexHarness<TScope = unknown> {
|
|
|
8458
9661
|
await Promise.allSettled([...state.sessionInitializations.values()]);
|
|
8459
9662
|
errors.push(...await this.closeOrphanedResources(state.storageKey));
|
|
8460
9663
|
await state.scopeQueue;
|
|
9664
|
+
await this.awaitProjectManagementOperations(
|
|
9665
|
+
state.storageKey,
|
|
9666
|
+
[
|
|
9667
|
+
...state.sessions.keys(),
|
|
9668
|
+
...state.retainedSessionCleanups.keys(),
|
|
9669
|
+
],
|
|
9670
|
+
);
|
|
8461
9671
|
await Promise.all([...state.sessions.values()].flatMap((stored) => [
|
|
8462
9672
|
stored.projectionQueue,
|
|
8463
9673
|
stored.permissionQueue,
|