@modelprofile.com/flexharness 10.24.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/classes.codesandbox.js +1 -1
- package/dist_ts/classes.flexharness.admission.d.ts +17 -0
- package/dist_ts/classes.flexharness.admission.js +263 -0
- package/dist_ts/classes.flexharness.callbacks.d.ts +38 -0
- package/dist_ts/classes.flexharness.callbacks.js +313 -0
- package/dist_ts/classes.flexharness.compaction.d.ts +25 -0
- package/dist_ts/classes.flexharness.compaction.js +87 -0
- package/dist_ts/classes.flexharness.configurations.d.ts +11 -0
- package/dist_ts/classes.flexharness.configurations.js +137 -0
- package/dist_ts/classes.flexharness.core.d.ts +143 -0
- package/dist_ts/classes.flexharness.core.js +1538 -0
- package/dist_ts/classes.flexharness.d.ts +2 -437
- package/dist_ts/classes.flexharness.deletion.d.ts +31 -0
- package/dist_ts/classes.flexharness.deletion.js +396 -0
- package/dist_ts/classes.flexharness.imports.d.ts +24 -0
- package/dist_ts/classes.flexharness.imports.js +345 -0
- package/dist_ts/classes.flexharness.js +119 -9776
- package/dist_ts/classes.flexharness.lifecycle.d.ts +47 -0
- package/dist_ts/classes.flexharness.lifecycle.js +625 -0
- package/dist_ts/classes.flexharness.permissions.d.ts +19 -0
- package/dist_ts/classes.flexharness.permissions.js +450 -0
- package/dist_ts/classes.flexharness.projects.d.ts +55 -0
- package/dist_ts/classes.flexharness.projects.js +575 -0
- package/dist_ts/classes.flexharness.prompts.d.ts +44 -0
- package/dist_ts/classes.flexharness.prompts.js +659 -0
- package/dist_ts/classes.flexharness.providers.d.ts +18 -0
- package/dist_ts/classes.flexharness.providers.js +267 -0
- package/dist_ts/classes.flexharness.results.d.ts +26 -0
- package/dist_ts/classes.flexharness.results.js +89 -0
- package/dist_ts/classes.flexharness.reversions.d.ts +52 -0
- package/dist_ts/classes.flexharness.reversions.js +1306 -0
- package/dist_ts/classes.flexharness.runs.d.ts +63 -0
- package/dist_ts/classes.flexharness.runs.js +1058 -0
- package/dist_ts/classes.flexharness.sessions.d.ts +20 -0
- package/dist_ts/classes.flexharness.sessions.js +381 -0
- package/dist_ts/classes.flexharness.slashcommands.d.ts +40 -0
- package/dist_ts/classes.flexharness.slashcommands.js +458 -0
- package/dist_ts/classes.flexharness.subagents.d.ts +31 -0
- package/dist_ts/classes.flexharness.subagents.js +690 -0
- package/dist_ts/classes.flexharness.tooldispatch.d.ts +53 -0
- package/dist_ts/classes.flexharness.tooldispatch.js +418 -0
- package/dist_ts/classes.stores.js +1 -1
- package/dist_ts/classes.toolcatalog.js +1 -1
- package/dist_ts/errors.js +1 -1
- package/dist_ts/functions.toolpermissions.js +1 -1
- package/dist_ts/interfaces.d.ts +3 -3
- package/dist_ts/interfaces.flexharness.d.ts +455 -0
- package/dist_ts/interfaces.flexharness.js +2 -0
- package/dist_ts/utils.flexharness.d.ts +19 -0
- package/dist_ts/utils.flexharness.js +83 -0
- package/dist_ts/utils.harnessoptions.js +1 -1
- package/dist_ts/utils.json.js +1 -1
- package/dist_ts/utils.schemadeclarations.js +1 -1
- package/dist_ts/utils.sessionimport.js +1 -1
- package/dist_ts/utils.sessionmetrics.d.ts +1 -1
- package/dist_ts_chat/classes.chatsession.js +1 -1
- package/dist_ts_chat_harness/classes.transcript.js +1 -1
- package/dist_ts_chat_harness/functions.intents.js +1 -1
- package/dist_ts_chat_harness/functions.mapping.d.ts +1 -1
- package/dist_ts_chat_harness/functions.mapping.js +1 -1
- package/dist_ts_chat_harness_server/classes.chathub.d.ts +18 -7
- package/dist_ts_chat_harness_server/classes.chathub.js +65 -62
- package/dist_ts_chat_harness_server/classes.chatsubscription.d.ts +5 -3
- package/dist_ts_chat_harness_server/classes.chatsubscription.js +4 -4
- package/dist_ts_chat_harness_server/classes.scopedirectory.d.ts +17 -0
- package/dist_ts_chat_harness_server/classes.scopedirectory.js +49 -0
- package/dist_ts_chat_harness_server/classes.sessionlists.d.ts +3 -5
- package/dist_ts_chat_harness_server/classes.sessionlists.js +11 -12
- package/dist_ts_chat_harness_server/classes.turngate.d.ts +8 -2
- package/dist_ts_chat_harness_server/classes.turngate.js +22 -21
- package/dist_ts_chat_harness_server/functions.errorprojector.js +1 -1
- package/dist_ts_chat_harness_server/index.d.ts +3 -2
- package/dist_ts_chat_harness_server/index.js +3 -2
- package/dist_ts_chat_harness_server/interfaces.d.ts +35 -2
- package/dist_ts_chat_harness_server/plugins.d.ts +1 -1
- package/dist_ts_chat_harness_typedsocket/classes.clienttransport.d.ts +28 -3
- package/dist_ts_chat_harness_typedsocket/classes.clienttransport.js +71 -15
- package/dist_ts_chat_harness_typedsocket/classes.serverbinding.d.ts +5 -3
- package/dist_ts_chat_harness_typedsocket/classes.serverbinding.js +79 -16
- package/dist_ts_chat_harness_typedsocket/index.d.ts +2 -2
- package/dist_ts_chat_harness_typedsocket/index.js +1 -1
- package/dist_ts_chat_harness_typedsocket/interfaces.requests.d.ts +20 -11
- package/dist_ts_chat_web/flexchat-input.js +1 -1
- package/dist_ts_chat_web/flexchat-message.js +1 -1
- package/dist_ts_chat_web/flexchat-window.js +1 -1
- package/dist_ts_compaction/smartagent.compaction.js +1 -1
- package/dist_ts_kvm/ocr.js +1 -1
- package/dist_ts_ocr/classes.imageocr.js +1 -1
- package/dist_ts_ocr/ocr.mistral.js +1 -1
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +1 -1
- package/dist_ts_tools/tool.browser.js +1 -1
- package/dist_ts_tools/tool.filesystem.js +1 -1
- package/dist_ts_tools/tool.shell.js +1 -1
- package/dist_ts_tools_node/tool.context.js +1 -1
- package/dist_ts_typedrequest/catalog.d.ts +2 -2
- package/dist_ts_typedrequest/catalog.js +1 -1
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.admission.ts +331 -0
- package/ts/classes.flexharness.callbacks.ts +352 -0
- package/ts/classes.flexharness.compaction.ts +167 -0
- package/ts/classes.flexharness.configurations.ts +167 -0
- package/ts/classes.flexharness.core.ts +1830 -0
- package/ts/classes.flexharness.deletion.ts +616 -0
- package/ts/classes.flexharness.imports.ts +414 -0
- package/ts/classes.flexharness.lifecycle.ts +786 -0
- package/ts/classes.flexharness.permissions.ts +535 -0
- package/ts/classes.flexharness.projects.ts +1006 -0
- package/ts/classes.flexharness.prompts.ts +857 -0
- package/ts/classes.flexharness.providers.ts +322 -0
- package/ts/classes.flexharness.results.ts +98 -0
- package/ts/classes.flexharness.reversions.ts +1684 -0
- package/ts/classes.flexharness.runs.ts +1204 -0
- package/ts/classes.flexharness.sessions.ts +483 -0
- package/ts/classes.flexharness.slashcommands.ts +705 -0
- package/ts/classes.flexharness.subagents.ts +860 -0
- package/ts/classes.flexharness.tooldispatch.ts +475 -0
- package/ts/classes.flexharness.ts +147 -12567
- package/ts/interfaces.flexharness.ts +517 -0
- package/ts/utils.flexharness.ts +121 -0
- package/ts_chat_harness_server/classes.chathub.ts +68 -61
- package/ts_chat_harness_server/classes.chatsubscription.ts +7 -5
- package/ts_chat_harness_server/classes.scopedirectory.ts +67 -0
- package/ts_chat_harness_server/classes.sessionlists.ts +11 -19
- package/ts_chat_harness_server/classes.turngate.ts +28 -21
- package/ts_chat_harness_server/index.ts +4 -5
- package/ts_chat_harness_server/interfaces.ts +35 -2
- package/ts_chat_harness_server/plugins.ts +0 -3
- package/ts_chat_harness_server/readme.md +6 -4
- package/ts_chat_harness_typedsocket/classes.clienttransport.ts +92 -23
- package/ts_chat_harness_typedsocket/classes.serverbinding.ts +88 -16
- package/ts_chat_harness_typedsocket/index.ts +4 -2
- package/ts_chat_harness_typedsocket/interfaces.requests.ts +15 -11
- package/ts_chat_harness_typedsocket/readme.md +7 -3
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
FlexHarnessNotFoundError,
|
|
4
|
+
FlexHarnessStoreConflictError,
|
|
5
|
+
FlexHarnessValidationError,
|
|
6
|
+
} from './errors.js';
|
|
7
|
+
import {
|
|
8
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS,
|
|
9
|
+
type IFlexCreateProjectTaskInput,
|
|
10
|
+
type IFlexProjectGoalResult,
|
|
11
|
+
type IFlexProjectManagementSessionContext,
|
|
12
|
+
type IFlexProjectManagementSnapshot,
|
|
13
|
+
type IFlexProjectManagementTombstone,
|
|
14
|
+
type IFlexProjectManagementWriteContext,
|
|
15
|
+
type IFlexProjectScratchpadResult,
|
|
16
|
+
type IFlexProjectStateResult,
|
|
17
|
+
type IFlexProjectTask,
|
|
18
|
+
type IFlexProjectTaskResult,
|
|
19
|
+
type IFlexProjectTasksResult,
|
|
20
|
+
type IFlexSession,
|
|
21
|
+
type IFlexSessionTombstone,
|
|
22
|
+
type IFlexUpdateProjectTaskInput,
|
|
23
|
+
} from './interfaces.js';
|
|
24
|
+
import { cloneSerializable } from './utils.json.js';
|
|
25
|
+
import {
|
|
26
|
+
assertFlexProjectManagementRecord,
|
|
27
|
+
assertFlexProjectManagementSnapshot,
|
|
28
|
+
createEmptyFlexProjectManagementSnapshot,
|
|
29
|
+
} from './utils.projectmanagement.js';
|
|
30
|
+
import type { INormalizedProjectManagementTools } from './utils.harnessoptions.js';
|
|
31
|
+
import { maxTransferIdentifierBytes, publicSnapshot } from './utils.transfer.js';
|
|
32
|
+
import {
|
|
33
|
+
validateCreateProjectTaskInput,
|
|
34
|
+
validateProjectTaskId,
|
|
35
|
+
validateUpdateProjectTaskInput,
|
|
36
|
+
validateUtf8String,
|
|
37
|
+
} from './utils.validation.js';
|
|
38
|
+
import type {
|
|
39
|
+
IStoredSessionState,
|
|
40
|
+
IStorageState,
|
|
41
|
+
IActiveRun,
|
|
42
|
+
TProjectTaskToolInput,
|
|
43
|
+
TProjectGoalToolInput,
|
|
44
|
+
TProjectScratchpadToolInput,
|
|
45
|
+
} from './interfaces.flexharness.js';
|
|
46
|
+
import { queueOperation, requireSessionGeneration, sha256Hex } from './utils.flexharness.js';
|
|
47
|
+
import type { FlexHarnessCore } from './classes.flexharness.core.js';
|
|
48
|
+
|
|
49
|
+
const maxProjectManagementTombstoneConflicts = 8;
|
|
50
|
+
|
|
51
|
+
/** The project tasks, goal and scratchpad that a session tree shares, and their agent tools. */
|
|
52
|
+
export class FlexHarnessProjectManagement<TScope> {
|
|
53
|
+
public readonly projectManagementQueues = new Map<string, Promise<void>>();
|
|
54
|
+
|
|
55
|
+
constructor(private readonly core: FlexHarnessCore<TScope>) {}
|
|
56
|
+
|
|
57
|
+
public async getProjectState(
|
|
58
|
+
scopeId: string,
|
|
59
|
+
sessionId: string,
|
|
60
|
+
): Promise<IFlexProjectStateResult> {
|
|
61
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
62
|
+
const snapshot = await this.readProjectManagementState(state, stored);
|
|
63
|
+
return this.projectStateResult(snapshot);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public async listProjectTasks(
|
|
67
|
+
scopeId: string,
|
|
68
|
+
sessionId: string,
|
|
69
|
+
): Promise<IFlexProjectTasksResult> {
|
|
70
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
71
|
+
const snapshot = await this.readProjectManagementState(state, stored);
|
|
72
|
+
return this.projectTasksResult(snapshot, snapshot.tasks);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public async createProjectTask(
|
|
76
|
+
scopeId: string,
|
|
77
|
+
sessionId: string,
|
|
78
|
+
input: IFlexCreateProjectTaskInput,
|
|
79
|
+
): Promise<IFlexProjectTaskResult> {
|
|
80
|
+
validateCreateProjectTaskInput(input);
|
|
81
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
82
|
+
return this.createProjectTaskInState(
|
|
83
|
+
state,
|
|
84
|
+
stored,
|
|
85
|
+
input,
|
|
86
|
+
Object.freeze({ actor: 'application' }),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public async updateProjectTask(
|
|
91
|
+
scopeId: string,
|
|
92
|
+
sessionId: string,
|
|
93
|
+
input: IFlexUpdateProjectTaskInput,
|
|
94
|
+
): Promise<IFlexProjectTaskResult> {
|
|
95
|
+
validateUpdateProjectTaskInput(input);
|
|
96
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
97
|
+
return this.updateProjectTaskInState(
|
|
98
|
+
state,
|
|
99
|
+
stored,
|
|
100
|
+
input,
|
|
101
|
+
Object.freeze({ actor: 'application' }),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public async deleteProjectTask(
|
|
106
|
+
scopeId: string,
|
|
107
|
+
sessionId: string,
|
|
108
|
+
taskId: string,
|
|
109
|
+
): Promise<IFlexProjectTaskResult> {
|
|
110
|
+
validateProjectTaskId(taskId);
|
|
111
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
112
|
+
return this.deleteProjectTaskInState(
|
|
113
|
+
state,
|
|
114
|
+
stored,
|
|
115
|
+
taskId,
|
|
116
|
+
Object.freeze({ actor: 'application' }),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public async clearProjectTasks(
|
|
121
|
+
scopeId: string,
|
|
122
|
+
sessionId: string,
|
|
123
|
+
): Promise<IFlexProjectTasksResult> {
|
|
124
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
125
|
+
return this.clearProjectTasksInState(
|
|
126
|
+
state,
|
|
127
|
+
stored,
|
|
128
|
+
Object.freeze({ actor: 'application' }),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public async getProjectGoal(
|
|
133
|
+
scopeId: string,
|
|
134
|
+
sessionId: string,
|
|
135
|
+
): Promise<IFlexProjectGoalResult> {
|
|
136
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
137
|
+
const snapshot = await this.readProjectManagementState(state, stored);
|
|
138
|
+
return this.projectGoalResult(snapshot);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public async setProjectGoal(
|
|
142
|
+
scopeId: string,
|
|
143
|
+
sessionId: string,
|
|
144
|
+
goal: string,
|
|
145
|
+
): Promise<IFlexProjectGoalResult> {
|
|
146
|
+
validateUtf8String(
|
|
147
|
+
goal,
|
|
148
|
+
'project goal',
|
|
149
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxGoalBytes,
|
|
150
|
+
true,
|
|
151
|
+
);
|
|
152
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
153
|
+
return this.setProjectGoalInState(
|
|
154
|
+
state,
|
|
155
|
+
stored,
|
|
156
|
+
goal,
|
|
157
|
+
Object.freeze({ actor: 'application' }),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public async clearProjectGoal(
|
|
162
|
+
scopeId: string,
|
|
163
|
+
sessionId: string,
|
|
164
|
+
): Promise<IFlexProjectGoalResult> {
|
|
165
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
166
|
+
return this.clearProjectGoalInState(
|
|
167
|
+
state,
|
|
168
|
+
stored,
|
|
169
|
+
Object.freeze({ actor: 'application' }),
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public async getProjectScratchpad(
|
|
174
|
+
scopeId: string,
|
|
175
|
+
sessionId: string,
|
|
176
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
177
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'read');
|
|
178
|
+
const snapshot = await this.readProjectManagementState(state, stored);
|
|
179
|
+
return this.projectScratchpadResult(snapshot);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public async setProjectScratchpad(
|
|
183
|
+
scopeId: string,
|
|
184
|
+
sessionId: string,
|
|
185
|
+
content: string,
|
|
186
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
187
|
+
validateUtf8String(
|
|
188
|
+
content,
|
|
189
|
+
'project scratchpad',
|
|
190
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
191
|
+
);
|
|
192
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
193
|
+
return this.setProjectScratchpadInState(
|
|
194
|
+
state,
|
|
195
|
+
stored,
|
|
196
|
+
content,
|
|
197
|
+
Object.freeze({ actor: 'application' }),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
public async appendProjectScratchpad(
|
|
202
|
+
scopeId: string,
|
|
203
|
+
sessionId: string,
|
|
204
|
+
content: string,
|
|
205
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
206
|
+
validateUtf8String(
|
|
207
|
+
content,
|
|
208
|
+
'project scratchpad append',
|
|
209
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
210
|
+
);
|
|
211
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
212
|
+
return this.appendProjectScratchpadInState(
|
|
213
|
+
state,
|
|
214
|
+
stored,
|
|
215
|
+
content,
|
|
216
|
+
Object.freeze({ actor: 'application' }),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
public async clearProjectScratchpad(
|
|
221
|
+
scopeId: string,
|
|
222
|
+
sessionId: string,
|
|
223
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
224
|
+
const { state, stored } = await this.resolveProjectManagementSession(scopeId, sessionId, 'write');
|
|
225
|
+
return this.clearProjectScratchpadInState(
|
|
226
|
+
state,
|
|
227
|
+
stored,
|
|
228
|
+
Object.freeze({ actor: 'application' }),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private requireProjectManagementTools(): INormalizedProjectManagementTools {
|
|
233
|
+
const projectManagement = this.core.builtInTools.projectManagement;
|
|
234
|
+
if (!projectManagement) {
|
|
235
|
+
throw new FlexHarnessValidationError('Project management is not configured for this FlexHarness.');
|
|
236
|
+
}
|
|
237
|
+
return projectManagement;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
public projectManagementKey(storageKey: string, sessionId: string): string {
|
|
241
|
+
return JSON.stringify([storageKey, sessionId]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private queueProjectManagementOperation<TResult>(
|
|
245
|
+
storageKey: string,
|
|
246
|
+
sessionId: string,
|
|
247
|
+
operation: () => Promise<TResult>,
|
|
248
|
+
): Promise<TResult> {
|
|
249
|
+
return queueOperation(this.projectManagementQueues, this.projectManagementKey(storageKey, sessionId), operation);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
public async awaitProjectManagementOperations(
|
|
253
|
+
storageKey: string,
|
|
254
|
+
sessionIds: Iterable<string>,
|
|
255
|
+
): Promise<void> {
|
|
256
|
+
const barriers = [...new Set(sessionIds)]
|
|
257
|
+
.map((sessionId) => this.projectManagementQueues.get(
|
|
258
|
+
this.projectManagementKey(storageKey, sessionId),
|
|
259
|
+
))
|
|
260
|
+
.filter((barrier): barrier is Promise<void> => barrier !== undefined);
|
|
261
|
+
await Promise.all(barriers);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private async resolveProjectManagementSession(
|
|
265
|
+
scopeId: string,
|
|
266
|
+
sessionId: string,
|
|
267
|
+
access: 'read' | 'write',
|
|
268
|
+
): Promise<{ state: IStorageState; stored: IStoredSessionState }> {
|
|
269
|
+
this.requireProjectManagementTools();
|
|
270
|
+
const { state } = await this.core.resolveState(scopeId);
|
|
271
|
+
await state.scopeQueue;
|
|
272
|
+
this.core.assertOpen();
|
|
273
|
+
this.core.assertStateAcceptingWork(state);
|
|
274
|
+
const stored = this.core.requireSession(state, sessionId);
|
|
275
|
+
if (access === 'write') this.core.assertSessionWritable(stored);
|
|
276
|
+
return { state, stored };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private assertProjectManagementSession(
|
|
280
|
+
state: IStorageState,
|
|
281
|
+
stored: IStoredSessionState,
|
|
282
|
+
): void {
|
|
283
|
+
this.core.assertOpen();
|
|
284
|
+
this.core.assertStateAcceptingWork(state);
|
|
285
|
+
if (
|
|
286
|
+
state.sessions.get(stored.session.sessionId) !== stored
|
|
287
|
+
|| state.tombstones.has(stored.session.sessionId)
|
|
288
|
+
) {
|
|
289
|
+
throw new FlexHarnessNotFoundError('Session', stored.session.sessionId);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private async loadProjectManagementState(
|
|
294
|
+
state: IStorageState,
|
|
295
|
+
stored: IStoredSessionState,
|
|
296
|
+
): Promise<IFlexProjectManagementSnapshot> {
|
|
297
|
+
const store = this.core.stores.projectManagement;
|
|
298
|
+
const generation = requireSessionGeneration(stored.session);
|
|
299
|
+
const sessionContext = this.createProjectManagementSessionContext(state, stored.session);
|
|
300
|
+
const record = await store.load(
|
|
301
|
+
state.storageKey,
|
|
302
|
+
stored.session.sessionId,
|
|
303
|
+
sessionContext,
|
|
304
|
+
);
|
|
305
|
+
if (record === undefined) {
|
|
306
|
+
return createEmptyFlexProjectManagementSnapshot(
|
|
307
|
+
generation.sessionGenerationId,
|
|
308
|
+
generation.sessionGenerationSequence,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
assertFlexProjectManagementRecord(record);
|
|
312
|
+
if (record.sessionGenerationId === generation.sessionGenerationId) {
|
|
313
|
+
if (record.sessionGenerationSequence !== generation.sessionGenerationSequence) {
|
|
314
|
+
throw new FlexHarnessStoreConflictError(
|
|
315
|
+
this.projectManagementKey(state.storageKey, stored.session.sessionId),
|
|
316
|
+
0,
|
|
317
|
+
record.revision,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
if (!('deletedAt' in record)) return cloneSerializable(record);
|
|
321
|
+
throw new FlexHarnessNotFoundError('Project management state', stored.session.sessionId);
|
|
322
|
+
}
|
|
323
|
+
if (
|
|
324
|
+
'deletedAt' in record
|
|
325
|
+
&& record.sessionGenerationSequence < generation.sessionGenerationSequence
|
|
326
|
+
) {
|
|
327
|
+
return createEmptyFlexProjectManagementSnapshot(
|
|
328
|
+
generation.sessionGenerationId,
|
|
329
|
+
generation.sessionGenerationSequence,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
throw new FlexHarnessStoreConflictError(
|
|
333
|
+
this.projectManagementKey(state.storageKey, stored.session.sessionId),
|
|
334
|
+
0,
|
|
335
|
+
record.revision,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private readProjectManagementState(
|
|
340
|
+
state: IStorageState,
|
|
341
|
+
stored: IStoredSessionState,
|
|
342
|
+
): Promise<IFlexProjectManagementSnapshot> {
|
|
343
|
+
return this.queueProjectManagementOperation(
|
|
344
|
+
state.storageKey,
|
|
345
|
+
stored.session.sessionId,
|
|
346
|
+
async () => {
|
|
347
|
+
this.assertProjectManagementSession(state, stored);
|
|
348
|
+
const snapshot = await this.loadProjectManagementState(state, stored);
|
|
349
|
+
this.assertProjectManagementSession(state, stored);
|
|
350
|
+
return publicSnapshot(snapshot);
|
|
351
|
+
},
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private mutateProjectManagementState<TResult>(
|
|
356
|
+
state: IStorageState,
|
|
357
|
+
stored: IStoredSessionState,
|
|
358
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
359
|
+
mutation: (
|
|
360
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
361
|
+
) => { changed: boolean; value: TResult },
|
|
362
|
+
): Promise<{ snapshot: IFlexProjectManagementSnapshot; value: TResult }> {
|
|
363
|
+
return this.queueProjectManagementOperation(
|
|
364
|
+
state.storageKey,
|
|
365
|
+
stored.session.sessionId,
|
|
366
|
+
async () => {
|
|
367
|
+
this.assertProjectManagementSession(state, stored);
|
|
368
|
+
const snapshot = await this.loadProjectManagementState(state, stored);
|
|
369
|
+
this.assertProjectManagementSession(state, stored);
|
|
370
|
+
const expectedRevision = snapshot.revision;
|
|
371
|
+
const result = mutation(snapshot);
|
|
372
|
+
if (result.changed) {
|
|
373
|
+
snapshot.revision = expectedRevision + 1;
|
|
374
|
+
assertFlexProjectManagementSnapshot(snapshot);
|
|
375
|
+
this.assertProjectManagementSession(state, stored);
|
|
376
|
+
await this.core.stores.projectManagement.save(
|
|
377
|
+
state.storageKey,
|
|
378
|
+
stored.session.sessionId,
|
|
379
|
+
cloneSerializable(snapshot),
|
|
380
|
+
expectedRevision,
|
|
381
|
+
Object.freeze(cloneSerializable(writeContext)),
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
snapshot: publicSnapshot(snapshot),
|
|
386
|
+
value: result.value === undefined ? result.value : publicSnapshot(result.value),
|
|
387
|
+
};
|
|
388
|
+
},
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private projectStateResult(snapshot: IFlexProjectManagementSnapshot): IFlexProjectStateResult {
|
|
393
|
+
return publicSnapshot({ revision: snapshot.revision, state: snapshot });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private createProjectManagementSessionContext(
|
|
397
|
+
state: IStorageState,
|
|
398
|
+
session: Readonly<IFlexSession | IFlexSessionTombstone>,
|
|
399
|
+
): Readonly<IFlexProjectManagementSessionContext> {
|
|
400
|
+
const source = 'deletedAt' in session
|
|
401
|
+
? session.subagent
|
|
402
|
+
: this.core.subagentTasks.createSubagentProvenance(state, session);
|
|
403
|
+
const subagent = source === undefined
|
|
404
|
+
? undefined
|
|
405
|
+
: Object.freeze({ ...cloneSerializable(source) });
|
|
406
|
+
return Object.freeze({
|
|
407
|
+
...requireSessionGeneration(session),
|
|
408
|
+
...(subagent === undefined ? {} : { subagent }),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private projectTaskResult(
|
|
413
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
414
|
+
task: IFlexProjectTask,
|
|
415
|
+
): IFlexProjectTaskResult {
|
|
416
|
+
return publicSnapshot({ revision: snapshot.revision, task, state: snapshot });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private projectTasksResult(
|
|
420
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
421
|
+
tasks: IFlexProjectTask[],
|
|
422
|
+
): IFlexProjectTasksResult {
|
|
423
|
+
return publicSnapshot({ revision: snapshot.revision, tasks, state: snapshot });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private projectGoalResult(snapshot: IFlexProjectManagementSnapshot): IFlexProjectGoalResult {
|
|
427
|
+
return publicSnapshot({
|
|
428
|
+
revision: snapshot.revision,
|
|
429
|
+
goal: snapshot.goal ?? null,
|
|
430
|
+
state: snapshot,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private projectScratchpadResult(
|
|
435
|
+
snapshot: IFlexProjectManagementSnapshot,
|
|
436
|
+
): IFlexProjectScratchpadResult {
|
|
437
|
+
return publicSnapshot({
|
|
438
|
+
revision: snapshot.revision,
|
|
439
|
+
scratchpad: snapshot.scratchpad,
|
|
440
|
+
state: snapshot,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private async createProjectTaskInState(
|
|
445
|
+
state: IStorageState,
|
|
446
|
+
stored: IStoredSessionState,
|
|
447
|
+
input: IFlexCreateProjectTaskInput,
|
|
448
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
449
|
+
): Promise<IFlexProjectTaskResult> {
|
|
450
|
+
const status = input.status ?? 'pending';
|
|
451
|
+
const priority = input.priority ?? 'medium';
|
|
452
|
+
const result = await this.mutateProjectManagementState(
|
|
453
|
+
state,
|
|
454
|
+
stored,
|
|
455
|
+
writeContext,
|
|
456
|
+
(snapshot) => {
|
|
457
|
+
const existing = snapshot.tasks.find((task) => task.id === input.id);
|
|
458
|
+
if (existing) {
|
|
459
|
+
if (
|
|
460
|
+
existing.content === input.content
|
|
461
|
+
&& existing.status === status
|
|
462
|
+
&& existing.priority === priority
|
|
463
|
+
) return { changed: false, value: existing };
|
|
464
|
+
throw new FlexHarnessValidationError(
|
|
465
|
+
`Project task "${input.id}" already exists with different creation data.`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
if (snapshot.tasks.length >= FLEX_PROJECT_MANAGEMENT_LIMITS.maxTasks) {
|
|
469
|
+
throw new FlexHarnessValidationError('Project task count has reached its limit.');
|
|
470
|
+
}
|
|
471
|
+
const timestamp = new Date().toISOString();
|
|
472
|
+
const task: IFlexProjectTask = {
|
|
473
|
+
id: input.id,
|
|
474
|
+
content: input.content,
|
|
475
|
+
status,
|
|
476
|
+
priority,
|
|
477
|
+
createdAt: timestamp,
|
|
478
|
+
updatedAt: timestamp,
|
|
479
|
+
};
|
|
480
|
+
snapshot.tasks.push(task);
|
|
481
|
+
return { changed: true, value: task };
|
|
482
|
+
},
|
|
483
|
+
);
|
|
484
|
+
return this.projectTaskResult(result.snapshot, result.value);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private async updateProjectTaskInState(
|
|
488
|
+
state: IStorageState,
|
|
489
|
+
stored: IStoredSessionState,
|
|
490
|
+
input: IFlexUpdateProjectTaskInput,
|
|
491
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
492
|
+
): Promise<IFlexProjectTaskResult> {
|
|
493
|
+
const result = await this.mutateProjectManagementState(
|
|
494
|
+
state,
|
|
495
|
+
stored,
|
|
496
|
+
writeContext,
|
|
497
|
+
(snapshot) => {
|
|
498
|
+
const task = snapshot.tasks.find((entry) => entry.id === input.id);
|
|
499
|
+
if (!task) throw new FlexHarnessNotFoundError('Project task', input.id);
|
|
500
|
+
const changed = (input.content !== undefined && input.content !== task.content)
|
|
501
|
+
|| (input.status !== undefined && input.status !== task.status)
|
|
502
|
+
|| (input.priority !== undefined && input.priority !== task.priority);
|
|
503
|
+
if (!changed) return { changed: false, value: task };
|
|
504
|
+
if (input.content !== undefined) task.content = input.content;
|
|
505
|
+
if (input.status !== undefined) task.status = input.status;
|
|
506
|
+
if (input.priority !== undefined) task.priority = input.priority;
|
|
507
|
+
task.updatedAt = new Date().toISOString();
|
|
508
|
+
return { changed: true, value: task };
|
|
509
|
+
},
|
|
510
|
+
);
|
|
511
|
+
return this.projectTaskResult(result.snapshot, result.value);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private async deleteProjectTaskInState(
|
|
515
|
+
state: IStorageState,
|
|
516
|
+
stored: IStoredSessionState,
|
|
517
|
+
taskId: string,
|
|
518
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
519
|
+
): Promise<IFlexProjectTaskResult> {
|
|
520
|
+
const result = await this.mutateProjectManagementState(
|
|
521
|
+
state,
|
|
522
|
+
stored,
|
|
523
|
+
writeContext,
|
|
524
|
+
(snapshot) => {
|
|
525
|
+
const index = snapshot.tasks.findIndex((task) => task.id === taskId);
|
|
526
|
+
if (index < 0) throw new FlexHarnessNotFoundError('Project task', taskId);
|
|
527
|
+
const [task] = snapshot.tasks.splice(index, 1);
|
|
528
|
+
return { changed: true, value: task };
|
|
529
|
+
},
|
|
530
|
+
);
|
|
531
|
+
return this.projectTaskResult(result.snapshot, result.value);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private async clearProjectTasksInState(
|
|
535
|
+
state: IStorageState,
|
|
536
|
+
stored: IStoredSessionState,
|
|
537
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
538
|
+
): Promise<IFlexProjectTasksResult> {
|
|
539
|
+
const result = await this.mutateProjectManagementState(
|
|
540
|
+
state,
|
|
541
|
+
stored,
|
|
542
|
+
writeContext,
|
|
543
|
+
(snapshot) => {
|
|
544
|
+
const tasks = snapshot.tasks;
|
|
545
|
+
if (tasks.length === 0) return { changed: false, value: [] as IFlexProjectTask[] };
|
|
546
|
+
snapshot.tasks = [];
|
|
547
|
+
return { changed: true, value: tasks };
|
|
548
|
+
},
|
|
549
|
+
);
|
|
550
|
+
return this.projectTasksResult(result.snapshot, result.value);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private async setProjectGoalInState(
|
|
554
|
+
state: IStorageState,
|
|
555
|
+
stored: IStoredSessionState,
|
|
556
|
+
goal: string,
|
|
557
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
558
|
+
): Promise<IFlexProjectGoalResult> {
|
|
559
|
+
const result = await this.mutateProjectManagementState(
|
|
560
|
+
state,
|
|
561
|
+
stored,
|
|
562
|
+
writeContext,
|
|
563
|
+
(snapshot) => {
|
|
564
|
+
if (snapshot.goal === goal) return { changed: false, value: undefined };
|
|
565
|
+
snapshot.goal = goal;
|
|
566
|
+
return { changed: true, value: undefined };
|
|
567
|
+
},
|
|
568
|
+
);
|
|
569
|
+
return this.projectGoalResult(result.snapshot);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private async clearProjectGoalInState(
|
|
573
|
+
state: IStorageState,
|
|
574
|
+
stored: IStoredSessionState,
|
|
575
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
576
|
+
): Promise<IFlexProjectGoalResult> {
|
|
577
|
+
const result = await this.mutateProjectManagementState(
|
|
578
|
+
state,
|
|
579
|
+
stored,
|
|
580
|
+
writeContext,
|
|
581
|
+
(snapshot) => {
|
|
582
|
+
if (snapshot.goal === undefined) return { changed: false, value: undefined };
|
|
583
|
+
delete snapshot.goal;
|
|
584
|
+
return { changed: true, value: undefined };
|
|
585
|
+
},
|
|
586
|
+
);
|
|
587
|
+
return this.projectGoalResult(result.snapshot);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private async setProjectScratchpadInState(
|
|
591
|
+
state: IStorageState,
|
|
592
|
+
stored: IStoredSessionState,
|
|
593
|
+
content: string,
|
|
594
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
595
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
596
|
+
const result = await this.mutateProjectManagementState(
|
|
597
|
+
state,
|
|
598
|
+
stored,
|
|
599
|
+
writeContext,
|
|
600
|
+
(snapshot) => {
|
|
601
|
+
if (snapshot.scratchpad === content) return { changed: false, value: undefined };
|
|
602
|
+
snapshot.scratchpad = content;
|
|
603
|
+
return { changed: true, value: undefined };
|
|
604
|
+
},
|
|
605
|
+
);
|
|
606
|
+
return this.projectScratchpadResult(result.snapshot);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private async appendProjectScratchpadInState(
|
|
610
|
+
state: IStorageState,
|
|
611
|
+
stored: IStoredSessionState,
|
|
612
|
+
content: string,
|
|
613
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
614
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
615
|
+
const result = await this.mutateProjectManagementState(
|
|
616
|
+
state,
|
|
617
|
+
stored,
|
|
618
|
+
writeContext,
|
|
619
|
+
(snapshot) => {
|
|
620
|
+
if (content.length === 0) return { changed: false, value: undefined };
|
|
621
|
+
const appended = `${snapshot.scratchpad}${content}`;
|
|
622
|
+
validateUtf8String(
|
|
623
|
+
appended,
|
|
624
|
+
'project scratchpad',
|
|
625
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
626
|
+
);
|
|
627
|
+
snapshot.scratchpad = appended;
|
|
628
|
+
return { changed: true, value: undefined };
|
|
629
|
+
},
|
|
630
|
+
);
|
|
631
|
+
return this.projectScratchpadResult(result.snapshot);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private async clearProjectScratchpadInState(
|
|
635
|
+
state: IStorageState,
|
|
636
|
+
stored: IStoredSessionState,
|
|
637
|
+
writeContext: IFlexProjectManagementWriteContext,
|
|
638
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
639
|
+
const result = await this.mutateProjectManagementState(
|
|
640
|
+
state,
|
|
641
|
+
stored,
|
|
642
|
+
writeContext,
|
|
643
|
+
(snapshot) => {
|
|
644
|
+
if (snapshot.scratchpad.length === 0) return { changed: false, value: undefined };
|
|
645
|
+
snapshot.scratchpad = '';
|
|
646
|
+
return { changed: true, value: undefined };
|
|
647
|
+
},
|
|
648
|
+
);
|
|
649
|
+
return this.projectScratchpadResult(result.snapshot);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
public createProjectTaskTool(run: IActiveRun): unknown {
|
|
653
|
+
const status = plugins.z.enum(['pending', 'in_progress', 'completed', 'cancelled']);
|
|
654
|
+
const priority = plugins.z.enum(['high', 'medium', 'low']);
|
|
655
|
+
const id = plugins.z.string().min(1).max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskIdBytes);
|
|
656
|
+
const content = plugins.z.string().min(1).max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxTaskContentBytes);
|
|
657
|
+
return plugins.tool({
|
|
658
|
+
description: 'List, create, update, delete, or clear session-local project tasks.',
|
|
659
|
+
strict: false,
|
|
660
|
+
inputSchema: plugins.z.object({
|
|
661
|
+
action: plugins.z.enum(['list', 'create', 'update', 'delete', 'clear']),
|
|
662
|
+
id: id.optional(),
|
|
663
|
+
content: content.optional(),
|
|
664
|
+
status: status.optional(),
|
|
665
|
+
priority: priority.optional(),
|
|
666
|
+
}).strict().superRefine((input, context) => {
|
|
667
|
+
const rejectFields = (...fields: Array<keyof typeof input>): void => {
|
|
668
|
+
for (const field of fields) {
|
|
669
|
+
if (input[field] === undefined) continue;
|
|
670
|
+
context.addIssue({
|
|
671
|
+
code: 'custom',
|
|
672
|
+
path: [field],
|
|
673
|
+
message: `Task action "${input.action}" does not accept ${field}.`,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
if (input.action === 'list' || input.action === 'clear') {
|
|
678
|
+
rejectFields('id', 'content', 'status', 'priority');
|
|
679
|
+
} else if (input.action === 'create') {
|
|
680
|
+
if (input.content === undefined) {
|
|
681
|
+
context.addIssue({
|
|
682
|
+
code: 'custom',
|
|
683
|
+
path: ['content'],
|
|
684
|
+
message: 'Task creation requires content.',
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
} else if (input.action === 'update') {
|
|
688
|
+
if (input.id === undefined) {
|
|
689
|
+
context.addIssue({
|
|
690
|
+
code: 'custom',
|
|
691
|
+
path: ['id'],
|
|
692
|
+
message: 'Task update requires an id.',
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
if (
|
|
696
|
+
input.content === undefined
|
|
697
|
+
&& input.status === undefined
|
|
698
|
+
&& input.priority === undefined
|
|
699
|
+
) {
|
|
700
|
+
context.addIssue({
|
|
701
|
+
code: 'custom',
|
|
702
|
+
message: 'Task update requires content, status, or priority.',
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
} else {
|
|
706
|
+
if (input.id === undefined) {
|
|
707
|
+
context.addIssue({
|
|
708
|
+
code: 'custom',
|
|
709
|
+
path: ['id'],
|
|
710
|
+
message: 'Task deletion requires an id.',
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
rejectFields('content', 'status', 'priority');
|
|
714
|
+
}
|
|
715
|
+
}),
|
|
716
|
+
execute: (
|
|
717
|
+
input,
|
|
718
|
+
options?: { toolCallId?: string },
|
|
719
|
+
) => this.executeProjectTaskTool(
|
|
720
|
+
run,
|
|
721
|
+
input as TProjectTaskToolInput,
|
|
722
|
+
options?.toolCallId,
|
|
723
|
+
),
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
public createProjectGoalTool(run: IActiveRun): unknown {
|
|
728
|
+
return plugins.tool({
|
|
729
|
+
description: 'Get, set, or clear the session-local project goal.',
|
|
730
|
+
strict: false,
|
|
731
|
+
inputSchema: plugins.z.object({
|
|
732
|
+
action: plugins.z.enum(['get', 'set', 'clear']),
|
|
733
|
+
goal: plugins.z.string()
|
|
734
|
+
.min(1)
|
|
735
|
+
.max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxGoalBytes)
|
|
736
|
+
.optional(),
|
|
737
|
+
}).strict().superRefine((input, context) => {
|
|
738
|
+
if (input.action === 'set' && input.goal === undefined) {
|
|
739
|
+
context.addIssue({
|
|
740
|
+
code: 'custom',
|
|
741
|
+
path: ['goal'],
|
|
742
|
+
message: 'Goal set requires a goal.',
|
|
743
|
+
});
|
|
744
|
+
} else if (input.action !== 'set' && input.goal !== undefined) {
|
|
745
|
+
context.addIssue({
|
|
746
|
+
code: 'custom',
|
|
747
|
+
path: ['goal'],
|
|
748
|
+
message: `Goal action "${input.action}" does not accept goal.`,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}),
|
|
752
|
+
execute: (
|
|
753
|
+
input,
|
|
754
|
+
options?: { toolCallId?: string },
|
|
755
|
+
) => this.executeProjectGoalTool(
|
|
756
|
+
run,
|
|
757
|
+
input as TProjectGoalToolInput,
|
|
758
|
+
options?.toolCallId,
|
|
759
|
+
),
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
public createProjectScratchpadTool(run: IActiveRun): unknown {
|
|
764
|
+
const content = plugins.z.string().max(FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes);
|
|
765
|
+
return plugins.tool({
|
|
766
|
+
description: 'Get, set, append to, or clear the session-local project scratchpad.',
|
|
767
|
+
strict: false,
|
|
768
|
+
inputSchema: plugins.z.object({
|
|
769
|
+
action: plugins.z.enum(['get', 'set', 'append', 'clear']),
|
|
770
|
+
content: content.optional(),
|
|
771
|
+
}).strict().superRefine((input, context) => {
|
|
772
|
+
if (
|
|
773
|
+
(input.action === 'set' || input.action === 'append')
|
|
774
|
+
&& input.content === undefined
|
|
775
|
+
) {
|
|
776
|
+
context.addIssue({
|
|
777
|
+
code: 'custom',
|
|
778
|
+
path: ['content'],
|
|
779
|
+
message: `Scratchpad ${input.action} requires content.`,
|
|
780
|
+
});
|
|
781
|
+
} else if (
|
|
782
|
+
(input.action === 'get' || input.action === 'clear')
|
|
783
|
+
&& input.content !== undefined
|
|
784
|
+
) {
|
|
785
|
+
context.addIssue({
|
|
786
|
+
code: 'custom',
|
|
787
|
+
path: ['content'],
|
|
788
|
+
message: `Scratchpad action "${input.action}" does not accept content.`,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}),
|
|
792
|
+
execute: (
|
|
793
|
+
input,
|
|
794
|
+
options?: { toolCallId?: string },
|
|
795
|
+
) => this.executeProjectScratchpadTool(
|
|
796
|
+
run,
|
|
797
|
+
input as TProjectScratchpadToolInput,
|
|
798
|
+
options?.toolCallId,
|
|
799
|
+
),
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private projectManagementAgentWriteContext(
|
|
804
|
+
run: IActiveRun,
|
|
805
|
+
toolCallId: string | undefined,
|
|
806
|
+
): IFlexProjectManagementWriteContext {
|
|
807
|
+
return Object.freeze({
|
|
808
|
+
actor: 'agent' as const,
|
|
809
|
+
runId: run.runId,
|
|
810
|
+
...(run.stored.session.agent === undefined ? {} : { agent: run.stored.session.agent }),
|
|
811
|
+
...(toolCallId === undefined ? {} : { toolCallId }),
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
private async executeProjectTaskTool(
|
|
816
|
+
run: IActiveRun,
|
|
817
|
+
input: TProjectTaskToolInput,
|
|
818
|
+
toolCallId: string | undefined,
|
|
819
|
+
): Promise<IFlexProjectStateResult | IFlexProjectTaskResult | IFlexProjectTasksResult> {
|
|
820
|
+
this.assertProjectManagementSession(run.state, run.stored);
|
|
821
|
+
if (input.action === 'list') {
|
|
822
|
+
const snapshot = await this.readProjectManagementState(run.state, run.stored);
|
|
823
|
+
return this.projectTasksResult(snapshot, snapshot.tasks);
|
|
824
|
+
}
|
|
825
|
+
const writeContext = this.projectManagementAgentWriteContext(run, toolCallId);
|
|
826
|
+
if (input.action === 'create') {
|
|
827
|
+
const id = input.id ?? (() => {
|
|
828
|
+
validateUtf8String(
|
|
829
|
+
toolCallId,
|
|
830
|
+
'task toolCallId',
|
|
831
|
+
maxTransferIdentifierBytes,
|
|
832
|
+
true,
|
|
833
|
+
);
|
|
834
|
+
return this.createProjectTaskId(
|
|
835
|
+
run.state.storageKey,
|
|
836
|
+
run.sessionId,
|
|
837
|
+
run.runId,
|
|
838
|
+
toolCallId,
|
|
839
|
+
);
|
|
840
|
+
})();
|
|
841
|
+
const createInput: IFlexCreateProjectTaskInput = {
|
|
842
|
+
id,
|
|
843
|
+
content: input.content,
|
|
844
|
+
...(input.status === undefined ? {} : { status: input.status }),
|
|
845
|
+
...(input.priority === undefined ? {} : { priority: input.priority }),
|
|
846
|
+
};
|
|
847
|
+
validateCreateProjectTaskInput(createInput);
|
|
848
|
+
return this.createProjectTaskInState(run.state, run.stored, createInput, writeContext);
|
|
849
|
+
}
|
|
850
|
+
if (input.action === 'update') {
|
|
851
|
+
const updateInput: IFlexUpdateProjectTaskInput = {
|
|
852
|
+
id: input.id,
|
|
853
|
+
...(input.content === undefined ? {} : { content: input.content }),
|
|
854
|
+
...(input.status === undefined ? {} : { status: input.status }),
|
|
855
|
+
...(input.priority === undefined ? {} : { priority: input.priority }),
|
|
856
|
+
};
|
|
857
|
+
validateUpdateProjectTaskInput(updateInput);
|
|
858
|
+
return this.updateProjectTaskInState(run.state, run.stored, updateInput, writeContext);
|
|
859
|
+
}
|
|
860
|
+
if (input.action === 'delete') {
|
|
861
|
+
validateProjectTaskId(input.id);
|
|
862
|
+
return this.deleteProjectTaskInState(run.state, run.stored, input.id, writeContext);
|
|
863
|
+
}
|
|
864
|
+
return this.clearProjectTasksInState(run.state, run.stored, writeContext);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
private async executeProjectGoalTool(
|
|
868
|
+
run: IActiveRun,
|
|
869
|
+
input: TProjectGoalToolInput,
|
|
870
|
+
toolCallId: string | undefined,
|
|
871
|
+
): Promise<IFlexProjectGoalResult> {
|
|
872
|
+
this.assertProjectManagementSession(run.state, run.stored);
|
|
873
|
+
if (input.action === 'get') {
|
|
874
|
+
return this.projectGoalResult(await this.readProjectManagementState(run.state, run.stored));
|
|
875
|
+
}
|
|
876
|
+
const writeContext = this.projectManagementAgentWriteContext(run, toolCallId);
|
|
877
|
+
if (input.action === 'set') {
|
|
878
|
+
validateUtf8String(
|
|
879
|
+
input.goal,
|
|
880
|
+
'project goal',
|
|
881
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxGoalBytes,
|
|
882
|
+
true,
|
|
883
|
+
);
|
|
884
|
+
return this.setProjectGoalInState(run.state, run.stored, input.goal, writeContext);
|
|
885
|
+
}
|
|
886
|
+
return this.clearProjectGoalInState(run.state, run.stored, writeContext);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
private async executeProjectScratchpadTool(
|
|
890
|
+
run: IActiveRun,
|
|
891
|
+
input: TProjectScratchpadToolInput,
|
|
892
|
+
toolCallId: string | undefined,
|
|
893
|
+
): Promise<IFlexProjectScratchpadResult> {
|
|
894
|
+
this.assertProjectManagementSession(run.state, run.stored);
|
|
895
|
+
if (input.action === 'get') {
|
|
896
|
+
return this.projectScratchpadResult(
|
|
897
|
+
await this.readProjectManagementState(run.state, run.stored),
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
const writeContext = this.projectManagementAgentWriteContext(run, toolCallId);
|
|
901
|
+
if (input.action === 'set') {
|
|
902
|
+
validateUtf8String(
|
|
903
|
+
input.content,
|
|
904
|
+
'project scratchpad',
|
|
905
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
906
|
+
);
|
|
907
|
+
return this.setProjectScratchpadInState(run.state, run.stored, input.content, writeContext);
|
|
908
|
+
}
|
|
909
|
+
if (input.action === 'append') {
|
|
910
|
+
validateUtf8String(
|
|
911
|
+
input.content,
|
|
912
|
+
'project scratchpad append',
|
|
913
|
+
FLEX_PROJECT_MANAGEMENT_LIMITS.maxScratchpadBytes,
|
|
914
|
+
);
|
|
915
|
+
return this.appendProjectScratchpadInState(
|
|
916
|
+
run.state,
|
|
917
|
+
run.stored,
|
|
918
|
+
input.content,
|
|
919
|
+
writeContext,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
return this.clearProjectScratchpadInState(run.state, run.stored, writeContext);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
private createProjectTaskId(
|
|
926
|
+
storageKey: string,
|
|
927
|
+
sessionId: string,
|
|
928
|
+
runId: string,
|
|
929
|
+
toolCallId: string,
|
|
930
|
+
): string {
|
|
931
|
+
return `task_${sha256Hex(JSON.stringify([
|
|
932
|
+
'flexharness-project-task-v1',
|
|
933
|
+
storageKey,
|
|
934
|
+
sessionId,
|
|
935
|
+
runId,
|
|
936
|
+
toolCallId,
|
|
937
|
+
]))}`;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
public async tombstoneProjectManagementSession(
|
|
941
|
+
state: IStorageState,
|
|
942
|
+
sessionTombstone: IFlexSessionTombstone,
|
|
943
|
+
): Promise<void> {
|
|
944
|
+
const storageKey = state.storageKey;
|
|
945
|
+
const store = this.core.stores.projectManagement;
|
|
946
|
+
const sessionId = sessionTombstone.sessionId;
|
|
947
|
+
const generation = requireSessionGeneration(sessionTombstone);
|
|
948
|
+
const sessionContext = this.createProjectManagementSessionContext(state, sessionTombstone);
|
|
949
|
+
let lastConflict: FlexHarnessStoreConflictError | undefined;
|
|
950
|
+
for (let attempt = 0; attempt <= maxProjectManagementTombstoneConflicts; attempt++) {
|
|
951
|
+
const current = await store.load(
|
|
952
|
+
storageKey,
|
|
953
|
+
sessionId,
|
|
954
|
+
sessionContext,
|
|
955
|
+
);
|
|
956
|
+
if (current !== undefined) {
|
|
957
|
+
assertFlexProjectManagementRecord(current);
|
|
958
|
+
if (current.sessionGenerationId === generation.sessionGenerationId) {
|
|
959
|
+
if (current.sessionGenerationSequence !== generation.sessionGenerationSequence) {
|
|
960
|
+
throw new FlexHarnessStoreConflictError(
|
|
961
|
+
this.projectManagementKey(storageKey, sessionId),
|
|
962
|
+
0,
|
|
963
|
+
current.revision,
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
if ('deletedAt' in current) return;
|
|
967
|
+
} else if (
|
|
968
|
+
!('deletedAt' in current)
|
|
969
|
+
|| current.sessionGenerationSequence >= generation.sessionGenerationSequence
|
|
970
|
+
) {
|
|
971
|
+
throw new FlexHarnessStoreConflictError(
|
|
972
|
+
this.projectManagementKey(storageKey, sessionId),
|
|
973
|
+
0,
|
|
974
|
+
current.revision,
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (attempt === maxProjectManagementTombstoneConflicts) break;
|
|
979
|
+
const expectedRevision = current === undefined
|
|
980
|
+
|| current.sessionGenerationId !== generation.sessionGenerationId
|
|
981
|
+
? 0
|
|
982
|
+
: current.revision;
|
|
983
|
+
const tombstone: IFlexProjectManagementTombstone = {
|
|
984
|
+
schemaVersion: 1,
|
|
985
|
+
revision: expectedRevision + 1,
|
|
986
|
+
...generation,
|
|
987
|
+
deletedAt: sessionTombstone.deletedAt,
|
|
988
|
+
};
|
|
989
|
+
try {
|
|
990
|
+
await store.tombstoneSession(
|
|
991
|
+
storageKey,
|
|
992
|
+
sessionId,
|
|
993
|
+
tombstone,
|
|
994
|
+
expectedRevision,
|
|
995
|
+
sessionContext,
|
|
996
|
+
);
|
|
997
|
+
} catch (error) {
|
|
998
|
+
if (!(error instanceof FlexHarnessStoreConflictError)) throw error;
|
|
999
|
+
lastConflict = error;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
throw lastConflict ?? new FlexHarnessValidationError(
|
|
1003
|
+
`Project management tombstone for session "${sessionId}" could not be durably confirmed.`,
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
}
|