@tomflow/proflow-platform-host 0.1.21 → 0.1.23
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 +7 -0
- package/DOCS.md +3 -2
- package/dist/deployment/adapter.d.ts +10 -17
- package/dist/deployment/adapter.js +30 -43
- package/dist/deployment/descriptor.d.ts +4 -7
- package/dist/deployment/descriptor.js +4 -8
- package/dist/src/cli.js +0 -4
- package/dist/src/index.d.ts +18 -8
- package/dist/src/index.js +269 -270
- package/dist/src/reconciliation-coordinator.d.ts +37 -0
- package/dist/src/reconciliation-coordinator.js +336 -0
- package/dist/src/role-operations.d.ts +4 -6
- package/dist/src/role-operations.js +37 -12
- package/dist/src/task-observer.d.ts +49 -0
- package/dist/src/task-observer.js +90 -0
- package/package.json +7 -7
- package/proflow.module.json +4 -8
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type TaskDriveProjection, type TaskResumeSignal } from "./task-observer.ts";
|
|
2
|
+
export type ReconciliationCoordinatorOptions = {
|
|
3
|
+
listTaskPage(input: {
|
|
4
|
+
afterTaskId?: string;
|
|
5
|
+
limit: number;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
taskIds: string[];
|
|
8
|
+
nextAfterTaskId?: string;
|
|
9
|
+
}>;
|
|
10
|
+
listExecutionSignals(): Promise<unknown[]>;
|
|
11
|
+
acknowledgeExecutionSignal(signalRef: string): Promise<void>;
|
|
12
|
+
ensureWorkers(taskId: string): Promise<void>;
|
|
13
|
+
getProjection(taskId: string): Promise<TaskDriveProjection>;
|
|
14
|
+
requestWake(input: {
|
|
15
|
+
taskId: string;
|
|
16
|
+
nodeId: string;
|
|
17
|
+
runNo: number;
|
|
18
|
+
roleRef: string;
|
|
19
|
+
workerRef: string;
|
|
20
|
+
trigger: string;
|
|
21
|
+
conversationLocator: string;
|
|
22
|
+
underlyingRef?: string;
|
|
23
|
+
}): Promise<unknown>;
|
|
24
|
+
intervalMs?: number;
|
|
25
|
+
pageSize?: number;
|
|
26
|
+
concurrency?: number;
|
|
27
|
+
maxPendingTasks?: number;
|
|
28
|
+
maxPendingSignals?: number;
|
|
29
|
+
now?: () => number;
|
|
30
|
+
};
|
|
31
|
+
export declare function createReconciliationCoordinator(options: ReconciliationCoordinatorOptions): Readonly<{
|
|
32
|
+
start(): void;
|
|
33
|
+
kick(taskId: string, signal?: TaskResumeSignal): void;
|
|
34
|
+
reconcile: (taskId: string, signal?: TaskResumeSignal) => Promise<void>;
|
|
35
|
+
sweep: () => Promise<void>;
|
|
36
|
+
stop(): void;
|
|
37
|
+
}>;
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { decideTaskProgression, } from "./task-observer.js";
|
|
2
|
+
function positiveBound(value, fallback, max) {
|
|
3
|
+
if (value === undefined)
|
|
4
|
+
return fallback;
|
|
5
|
+
if (!Number.isInteger(value) || value < 1 || value > max)
|
|
6
|
+
throw new RangeError(`reconciliation bound must be between 1 and ${max}`);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
export function createReconciliationCoordinator(options) {
|
|
10
|
+
const intervalMs = positiveBound(options.intervalMs, 10_000, 60_000);
|
|
11
|
+
const pageSize = positiveBound(options.pageSize, 100, 1_000);
|
|
12
|
+
const concurrency = positiveBound(options.concurrency, 4, 64);
|
|
13
|
+
const maxPendingTasks = positiveBound(options.maxPendingTasks, 1_024, 10_000);
|
|
14
|
+
const maxPendingSignals = positiveBound(options.maxPendingSignals, 4_096, 50_000);
|
|
15
|
+
const now = options.now ?? Date.now;
|
|
16
|
+
let taskCursor;
|
|
17
|
+
let stopped = false;
|
|
18
|
+
let started = false;
|
|
19
|
+
let timer;
|
|
20
|
+
let sweepInFlight = null;
|
|
21
|
+
let activeReconciliations = 0;
|
|
22
|
+
let pendingSignalCount = 0;
|
|
23
|
+
const slotWaiters = [];
|
|
24
|
+
const admittedTasks = new Set();
|
|
25
|
+
const taskInFlight = new Map();
|
|
26
|
+
const pendingSignals = new Map();
|
|
27
|
+
const failures = new Map();
|
|
28
|
+
const appliedIntents = new Map();
|
|
29
|
+
const retryTimers = new Map();
|
|
30
|
+
const acquireSlot = async () => {
|
|
31
|
+
if (stopped)
|
|
32
|
+
return false;
|
|
33
|
+
if (activeReconciliations < concurrency) {
|
|
34
|
+
activeReconciliations += 1;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return new Promise((resolve) => slotWaiters.push(resolve));
|
|
38
|
+
};
|
|
39
|
+
const releaseSlot = () => {
|
|
40
|
+
const next = stopped ? undefined : slotWaiters.shift();
|
|
41
|
+
if (next)
|
|
42
|
+
next(true);
|
|
43
|
+
else
|
|
44
|
+
activeReconciliations = Math.max(0, activeReconciliations - 1);
|
|
45
|
+
};
|
|
46
|
+
const intentKey = (decision) => JSON.stringify([
|
|
47
|
+
decision.taskId,
|
|
48
|
+
decision.nodeId,
|
|
49
|
+
decision.runNo,
|
|
50
|
+
decision.workerRef,
|
|
51
|
+
decision.trigger,
|
|
52
|
+
decision.underlyingRef ?? null,
|
|
53
|
+
]);
|
|
54
|
+
const signalKey = (signal) => JSON.stringify([
|
|
55
|
+
signal.trigger,
|
|
56
|
+
signal.ref,
|
|
57
|
+
signal.targetWorkerRef,
|
|
58
|
+
signal.nodeId,
|
|
59
|
+
signal.runNo,
|
|
60
|
+
]);
|
|
61
|
+
const hasApplied = (taskId, key) => appliedIntents.get(taskId)?.has(key) === true;
|
|
62
|
+
const rememberApplied = (taskId, key) => {
|
|
63
|
+
const keys = appliedIntents.get(taskId) ?? new Set();
|
|
64
|
+
keys.add(key);
|
|
65
|
+
// This is only a bounded cache. Execution owns durable effect deduplication.
|
|
66
|
+
if (keys.size > 128) {
|
|
67
|
+
const oldest = keys.values().next().value;
|
|
68
|
+
if (oldest !== undefined)
|
|
69
|
+
keys.delete(oldest);
|
|
70
|
+
}
|
|
71
|
+
appliedIntents.set(taskId, keys);
|
|
72
|
+
};
|
|
73
|
+
const consumePendingSignal = (taskId, signal) => {
|
|
74
|
+
if (!signal)
|
|
75
|
+
return;
|
|
76
|
+
const signals = pendingSignals.get(taskId);
|
|
77
|
+
if (signals?.delete(signalKey(signal)))
|
|
78
|
+
pendingSignalCount -= 1;
|
|
79
|
+
if (signals?.size === 0)
|
|
80
|
+
pendingSignals.delete(taskId);
|
|
81
|
+
};
|
|
82
|
+
const clearPendingSignals = (taskId) => {
|
|
83
|
+
pendingSignalCount -= pendingSignals.get(taskId)?.size ?? 0;
|
|
84
|
+
pendingSignals.delete(taskId);
|
|
85
|
+
};
|
|
86
|
+
const markFailure = (taskId) => {
|
|
87
|
+
const attempt = Math.min((failures.get(taskId)?.attempt ?? 0) + 1, 8);
|
|
88
|
+
const delay = Math.min(30_000, 500 * 2 ** Math.max(0, attempt - 1));
|
|
89
|
+
failures.set(taskId, { attempt, nextAt: now() + delay });
|
|
90
|
+
};
|
|
91
|
+
const canAttempt = (taskId) => (failures.get(taskId)?.nextAt ?? 0) <= now();
|
|
92
|
+
const schedulePendingRetry = (taskId) => {
|
|
93
|
+
if (stopped || !pendingSignals.has(taskId) || retryTimers.has(taskId))
|
|
94
|
+
return;
|
|
95
|
+
const delay = Math.max(1, (failures.get(taskId)?.nextAt ?? now()) - now());
|
|
96
|
+
const retry = setTimeout(() => {
|
|
97
|
+
retryTimers.delete(taskId);
|
|
98
|
+
if (!stopped)
|
|
99
|
+
void reconcile(taskId);
|
|
100
|
+
}, delay);
|
|
101
|
+
retry.unref?.();
|
|
102
|
+
retryTimers.set(taskId, retry);
|
|
103
|
+
};
|
|
104
|
+
const retainable = (reason) => reason === "BINDING_NOT_READY" ||
|
|
105
|
+
reason === "RESUME_TARGET_NOT_CURRENT_WORKER";
|
|
106
|
+
const reconcileOnce = async (taskId) => {
|
|
107
|
+
if (stopped || !canAttempt(taskId))
|
|
108
|
+
return;
|
|
109
|
+
try {
|
|
110
|
+
let projection = await options.getProjection(taskId);
|
|
111
|
+
if (stopped)
|
|
112
|
+
return;
|
|
113
|
+
if (projection.terminal) {
|
|
114
|
+
clearPendingSignals(taskId);
|
|
115
|
+
appliedIntents.delete(taskId);
|
|
116
|
+
failures.delete(taskId);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
await options.ensureWorkers(taskId);
|
|
120
|
+
if (stopped)
|
|
121
|
+
return;
|
|
122
|
+
projection = await options.getProjection(taskId);
|
|
123
|
+
if (stopped)
|
|
124
|
+
return;
|
|
125
|
+
const signals = pendingSignals.get(taskId);
|
|
126
|
+
const signal = signals?.values().next().value;
|
|
127
|
+
const decision = decideTaskProgression(projection, signal);
|
|
128
|
+
if (decision.kind === "STOP_DRIVING") {
|
|
129
|
+
clearPendingSignals(taskId);
|
|
130
|
+
appliedIntents.delete(taskId);
|
|
131
|
+
failures.delete(taskId);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (decision.kind === "NOOP") {
|
|
135
|
+
if (signal && retainable(decision.reason)) {
|
|
136
|
+
// Retain blocked intent, rotating it so another signal can progress.
|
|
137
|
+
const key = signalKey(signal);
|
|
138
|
+
signals?.delete(key);
|
|
139
|
+
signals?.set(key, signal);
|
|
140
|
+
markFailure(taskId);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
consumePendingSignal(taskId, signal);
|
|
144
|
+
failures.delete(taskId);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const key = intentKey(decision);
|
|
149
|
+
if (!hasApplied(taskId, key)) {
|
|
150
|
+
await options.requestWake(decision);
|
|
151
|
+
if (stopped)
|
|
152
|
+
return;
|
|
153
|
+
rememberApplied(taskId, key);
|
|
154
|
+
}
|
|
155
|
+
consumePendingSignal(taskId, signal);
|
|
156
|
+
failures.delete(taskId);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
if (!stopped)
|
|
160
|
+
markFailure(taskId);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const reconcile = (taskId, signal) => {
|
|
164
|
+
if (stopped)
|
|
165
|
+
return Promise.resolve();
|
|
166
|
+
const signals = pendingSignals.get(taskId);
|
|
167
|
+
const key = signal ? signalKey(signal) : undefined;
|
|
168
|
+
if (!admittedTasks.has(taskId) && admittedTasks.size >= maxPendingTasks)
|
|
169
|
+
throw new Error("TASK_RECONCILIATION_CAPACITY_EXCEEDED");
|
|
170
|
+
if (key && !signals?.has(key) && pendingSignalCount >= maxPendingSignals)
|
|
171
|
+
throw new Error("TASK_RECONCILIATION_SIGNAL_CAPACITY_EXCEEDED");
|
|
172
|
+
admittedTasks.add(taskId);
|
|
173
|
+
if (signal && key && !signals?.has(key)) {
|
|
174
|
+
const queue = signals ?? new Map();
|
|
175
|
+
queue.set(key, signal);
|
|
176
|
+
pendingSignals.set(taskId, queue);
|
|
177
|
+
pendingSignalCount += 1;
|
|
178
|
+
}
|
|
179
|
+
const retry = retryTimers.get(taskId);
|
|
180
|
+
if (retry) {
|
|
181
|
+
clearTimeout(retry);
|
|
182
|
+
retryTimers.delete(taskId);
|
|
183
|
+
}
|
|
184
|
+
const current = taskInFlight.get(taskId);
|
|
185
|
+
if (current)
|
|
186
|
+
return current;
|
|
187
|
+
const run = (async () => {
|
|
188
|
+
if (!(await acquireSlot()))
|
|
189
|
+
return;
|
|
190
|
+
try {
|
|
191
|
+
await reconcileOnce(taskId);
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
releaseSlot();
|
|
195
|
+
}
|
|
196
|
+
})().finally(() => {
|
|
197
|
+
taskInFlight.delete(taskId);
|
|
198
|
+
if (!stopped && pendingSignals.has(taskId))
|
|
199
|
+
schedulePendingRetry(taskId);
|
|
200
|
+
else
|
|
201
|
+
admittedTasks.delete(taskId);
|
|
202
|
+
});
|
|
203
|
+
taskInFlight.set(taskId, run);
|
|
204
|
+
return run;
|
|
205
|
+
};
|
|
206
|
+
const processExecutionSignal = async (raw) => {
|
|
207
|
+
if (stopped ||
|
|
208
|
+
typeof raw !== "object" ||
|
|
209
|
+
raw === null ||
|
|
210
|
+
Array.isArray(raw))
|
|
211
|
+
return;
|
|
212
|
+
const signal = raw;
|
|
213
|
+
if (typeof signal.signalRef !== "string")
|
|
214
|
+
return;
|
|
215
|
+
if (signal.kind === "UNKNOWN_REALITY") {
|
|
216
|
+
await options.acknowledgeExecutionSignal(signal.signalRef);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (signal.kind !== "RECOVERY_RESUME")
|
|
220
|
+
return;
|
|
221
|
+
if (typeof signal.executionRef !== "string" ||
|
|
222
|
+
typeof signal.taskId !== "string" ||
|
|
223
|
+
typeof signal.workerRef !== "string")
|
|
224
|
+
return;
|
|
225
|
+
if (typeof signal.nodeId !== "string" ||
|
|
226
|
+
!Number.isInteger(signal.runNo) ||
|
|
227
|
+
Number(signal.runNo) <= 0) {
|
|
228
|
+
await options.acknowledgeExecutionSignal(signal.signalRef);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const resumeSignal = {
|
|
232
|
+
trigger: "RECOVERY_RESUME",
|
|
233
|
+
ref: signal.executionRef,
|
|
234
|
+
targetWorkerRef: signal.workerRef,
|
|
235
|
+
nodeId: signal.nodeId,
|
|
236
|
+
runNo: Number(signal.runNo),
|
|
237
|
+
};
|
|
238
|
+
await reconcile(signal.taskId, resumeSignal);
|
|
239
|
+
if (stopped)
|
|
240
|
+
return;
|
|
241
|
+
const projection = await options.getProjection(signal.taskId);
|
|
242
|
+
if (stopped)
|
|
243
|
+
return;
|
|
244
|
+
const decision = decideTaskProgression(projection, resumeSignal);
|
|
245
|
+
if (decision.kind === "STOP_DRIVING" ||
|
|
246
|
+
(decision.kind === "WAKE" &&
|
|
247
|
+
hasApplied(signal.taskId, intentKey(decision))) ||
|
|
248
|
+
(decision.kind === "NOOP" && !retainable(decision.reason)))
|
|
249
|
+
await options.acknowledgeExecutionSignal(signal.signalRef);
|
|
250
|
+
};
|
|
251
|
+
const processExecutionSignals = async () => {
|
|
252
|
+
if (stopped)
|
|
253
|
+
return;
|
|
254
|
+
const signals = (await options.listExecutionSignals()).slice(0, 100);
|
|
255
|
+
for (let index = 0; !stopped && index < signals.length; index += concurrency)
|
|
256
|
+
await Promise.all(signals
|
|
257
|
+
.slice(index, index + concurrency)
|
|
258
|
+
.map((signal) => processExecutionSignal(signal).catch(() => undefined)));
|
|
259
|
+
};
|
|
260
|
+
const sweep = () => {
|
|
261
|
+
if (stopped)
|
|
262
|
+
return Promise.resolve();
|
|
263
|
+
if (sweepInFlight)
|
|
264
|
+
return sweepInFlight;
|
|
265
|
+
sweepInFlight = (async () => {
|
|
266
|
+
await processExecutionSignals().catch(() => undefined);
|
|
267
|
+
if (stopped)
|
|
268
|
+
return;
|
|
269
|
+
let page = await options.listTaskPage({
|
|
270
|
+
...(taskCursor ? { afterTaskId: taskCursor } : {}),
|
|
271
|
+
limit: pageSize,
|
|
272
|
+
});
|
|
273
|
+
if (stopped)
|
|
274
|
+
return;
|
|
275
|
+
if (page.taskIds.length === 0 && taskCursor !== undefined) {
|
|
276
|
+
taskCursor = undefined;
|
|
277
|
+
page = await options.listTaskPage({ limit: pageSize });
|
|
278
|
+
if (stopped)
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
taskCursor = page.nextAfterTaskId;
|
|
282
|
+
// A page uses the same bounded admission and slots as kicks and retries.
|
|
283
|
+
for (let index = 0; !stopped && index < page.taskIds.length; index += concurrency)
|
|
284
|
+
await Promise.all(page.taskIds.slice(index, index + concurrency).map(async (taskId) => {
|
|
285
|
+
await reconcile(taskId);
|
|
286
|
+
}));
|
|
287
|
+
})()
|
|
288
|
+
.catch(() => undefined)
|
|
289
|
+
.finally(() => {
|
|
290
|
+
sweepInFlight = null;
|
|
291
|
+
});
|
|
292
|
+
return sweepInFlight;
|
|
293
|
+
};
|
|
294
|
+
const schedule = () => {
|
|
295
|
+
if (stopped)
|
|
296
|
+
return;
|
|
297
|
+
timer = setTimeout(() => {
|
|
298
|
+
void sweep().finally(schedule);
|
|
299
|
+
}, intervalMs);
|
|
300
|
+
timer.unref?.();
|
|
301
|
+
};
|
|
302
|
+
return Object.freeze({
|
|
303
|
+
start() {
|
|
304
|
+
if (stopped)
|
|
305
|
+
throw new Error("RECONCILIATION_COORDINATOR_STOPPED");
|
|
306
|
+
if (started)
|
|
307
|
+
return;
|
|
308
|
+
started = true;
|
|
309
|
+
void sweep();
|
|
310
|
+
schedule();
|
|
311
|
+
},
|
|
312
|
+
kick(taskId, signal) {
|
|
313
|
+
// Capacity errors are synchronous: never report acceptance after dropping intent.
|
|
314
|
+
void reconcile(taskId, signal);
|
|
315
|
+
},
|
|
316
|
+
reconcile,
|
|
317
|
+
sweep,
|
|
318
|
+
stop() {
|
|
319
|
+
if (stopped)
|
|
320
|
+
return;
|
|
321
|
+
stopped = true;
|
|
322
|
+
if (timer)
|
|
323
|
+
clearTimeout(timer);
|
|
324
|
+
for (const retry of retryTimers.values())
|
|
325
|
+
clearTimeout(retry);
|
|
326
|
+
retryTimers.clear();
|
|
327
|
+
for (const waiter of slotWaiters.splice(0))
|
|
328
|
+
waiter(false);
|
|
329
|
+
pendingSignals.clear();
|
|
330
|
+
pendingSignalCount = 0;
|
|
331
|
+
admittedTasks.clear();
|
|
332
|
+
failures.clear();
|
|
333
|
+
appliedIntents.clear();
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
export declare const rolePackageRefs: readonly ["@tomflow/proflow-agent-product", "@tomflow/proflow-agent-controller-dev", "@tomflow/proflow-agent-test-ops"];
|
|
2
2
|
export type RolePackageRef = (typeof rolePackageRefs)[number];
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* authorization. Batch 6 mechanically reconciles the two inventories.
|
|
8
|
-
*/
|
|
3
|
+
export declare const directToolActionIds: readonly ["repomix", "localDev", "codeGraph"];
|
|
4
|
+
export type DirectToolActionId = (typeof directToolActionIds)[number];
|
|
5
|
+
export declare function roleAllowsDirectToolOperation(role: RolePackageRef, tool: DirectToolActionId, operation: string, input: Record<string, unknown>): boolean;
|
|
6
|
+
/** Canonical platform-host authorization inventory for shipped Custom GPT operations. */
|
|
9
7
|
export declare const roleOperations: Record<RolePackageRef, ReadonlySet<string>>;
|
|
@@ -3,12 +3,40 @@ export const rolePackageRefs = [
|
|
|
3
3
|
"@tomflow/proflow-agent-controller-dev",
|
|
4
4
|
"@tomflow/proflow-agent-test-ops",
|
|
5
5
|
];
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
export const directToolActionIds = [
|
|
7
|
+
"repomix",
|
|
8
|
+
"localDev",
|
|
9
|
+
"codeGraph",
|
|
10
|
+
];
|
|
11
|
+
const directToolOperations = {
|
|
12
|
+
"@tomflow/proflow-agent-product": {
|
|
13
|
+
repomix: new Set(["pack", "grep", "read"]),
|
|
14
|
+
localDev: new Set(["read", "list", "search", "process"]),
|
|
15
|
+
codeGraph: new Set(["explore"]),
|
|
16
|
+
},
|
|
17
|
+
"@tomflow/proflow-agent-controller-dev": {
|
|
18
|
+
repomix: new Set(["pack", "grep", "read"]),
|
|
19
|
+
localDev: new Set(["read", "list", "search", "mutate", "run", "process"]),
|
|
20
|
+
codeGraph: new Set(["explore"]),
|
|
21
|
+
},
|
|
22
|
+
"@tomflow/proflow-agent-test-ops": {
|
|
23
|
+
repomix: new Set(["pack", "grep", "read"]),
|
|
24
|
+
localDev: new Set(["read", "list", "search", "run", "process"]),
|
|
25
|
+
codeGraph: new Set(["explore"]),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const productProcessReads = new Set(["list", "ports", "status", "read"]);
|
|
29
|
+
export function roleAllowsDirectToolOperation(role, tool, operation, input) {
|
|
30
|
+
if (!directToolOperations[role][tool].has(operation))
|
|
31
|
+
return false;
|
|
32
|
+
if (role === "@tomflow/proflow-agent-product" &&
|
|
33
|
+
tool === "localDev" &&
|
|
34
|
+
operation === "process") {
|
|
35
|
+
return (typeof input.action === "string" && productProcessReads.has(input.action));
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
/** Canonical platform-host authorization inventory for shipped Custom GPT operations. */
|
|
12
40
|
export const roleOperations = {
|
|
13
41
|
"@tomflow/proflow-agent-product": new Set([
|
|
14
42
|
"getTask",
|
|
@@ -16,6 +44,7 @@ export const roleOperations = {
|
|
|
16
44
|
"getTaskDocument",
|
|
17
45
|
"askPeer",
|
|
18
46
|
"replyPeer",
|
|
47
|
+
...directToolActionIds,
|
|
19
48
|
]),
|
|
20
49
|
"@tomflow/proflow-agent-controller-dev": new Set([
|
|
21
50
|
"getTask",
|
|
@@ -29,9 +58,7 @@ export const roleOperations = {
|
|
|
29
58
|
"putTaskDocument",
|
|
30
59
|
"askPeer",
|
|
31
60
|
"replyPeer",
|
|
32
|
-
|
|
33
|
-
"getExecution",
|
|
34
|
-
"readExecutionOutput",
|
|
61
|
+
...directToolActionIds,
|
|
35
62
|
]),
|
|
36
63
|
"@tomflow/proflow-agent-test-ops": new Set([
|
|
37
64
|
"getTask",
|
|
@@ -44,8 +71,6 @@ export const roleOperations = {
|
|
|
44
71
|
"putTaskDocument",
|
|
45
72
|
"askPeer",
|
|
46
73
|
"replyPeer",
|
|
47
|
-
|
|
48
|
-
"getExecution",
|
|
49
|
-
"readExecutionOutput",
|
|
74
|
+
...directToolActionIds,
|
|
50
75
|
]),
|
|
51
76
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type TaskDriveProjection = {
|
|
2
|
+
taskId: string;
|
|
3
|
+
taskStatus: string;
|
|
4
|
+
taskVersion: number;
|
|
5
|
+
terminal: boolean;
|
|
6
|
+
currentNode: {
|
|
7
|
+
nodeId: string;
|
|
8
|
+
status: string;
|
|
9
|
+
version: number;
|
|
10
|
+
runNo: number;
|
|
11
|
+
requiredAgentPackageRef: string;
|
|
12
|
+
} | null;
|
|
13
|
+
roleBinding: {
|
|
14
|
+
agentPackageRef: string;
|
|
15
|
+
roleRef: string;
|
|
16
|
+
workerRef: string | null;
|
|
17
|
+
conversationLocator: string | null;
|
|
18
|
+
} | null;
|
|
19
|
+
canDrive: boolean;
|
|
20
|
+
blockedReason: string | null;
|
|
21
|
+
resumeSignalRef: string | null;
|
|
22
|
+
};
|
|
23
|
+
export type TaskResumeSignal = {
|
|
24
|
+
trigger: "EXECUTION_RESULT_READY" | "PEER_REPLY_READY" | "RECOVERY_RESUME" | "TASK_RESUMED";
|
|
25
|
+
ref: string;
|
|
26
|
+
targetWorkerRef: string;
|
|
27
|
+
nodeId: string;
|
|
28
|
+
runNo: number;
|
|
29
|
+
};
|
|
30
|
+
export type TaskProgressionDecision = {
|
|
31
|
+
kind: "WAKE";
|
|
32
|
+
taskId: string;
|
|
33
|
+
nodeId: string;
|
|
34
|
+
runNo: number;
|
|
35
|
+
roleRef: string;
|
|
36
|
+
workerRef: string;
|
|
37
|
+
trigger: string;
|
|
38
|
+
conversationLocator: string;
|
|
39
|
+
underlyingRef?: string;
|
|
40
|
+
} | {
|
|
41
|
+
kind: "STOP_DRIVING";
|
|
42
|
+
taskId: string;
|
|
43
|
+
reason: "TERMINAL";
|
|
44
|
+
} | {
|
|
45
|
+
kind: "NOOP";
|
|
46
|
+
taskId: string;
|
|
47
|
+
reason: string;
|
|
48
|
+
};
|
|
49
|
+
export declare function decideTaskProgression(projection: TaskDriveProjection, resumeSignal?: TaskResumeSignal): TaskProgressionDecision;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export function decideTaskProgression(projection, resumeSignal) {
|
|
2
|
+
if (projection.terminal)
|
|
3
|
+
return {
|
|
4
|
+
kind: "STOP_DRIVING",
|
|
5
|
+
taskId: projection.taskId,
|
|
6
|
+
reason: "TERMINAL",
|
|
7
|
+
};
|
|
8
|
+
if (projection.currentNode === null)
|
|
9
|
+
return {
|
|
10
|
+
kind: "NOOP",
|
|
11
|
+
taskId: projection.taskId,
|
|
12
|
+
reason: "NO_CURRENT_NODE",
|
|
13
|
+
};
|
|
14
|
+
const node = projection.currentNode;
|
|
15
|
+
const binding = projection.roleBinding;
|
|
16
|
+
if (!binding?.workerRef ||
|
|
17
|
+
!binding.conversationLocator ||
|
|
18
|
+
binding.agentPackageRef !== node.requiredAgentPackageRef)
|
|
19
|
+
return {
|
|
20
|
+
kind: "NOOP",
|
|
21
|
+
taskId: projection.taskId,
|
|
22
|
+
reason: "BINDING_NOT_READY",
|
|
23
|
+
};
|
|
24
|
+
if (node.status === "READY") {
|
|
25
|
+
if (!projection.canDrive)
|
|
26
|
+
return {
|
|
27
|
+
kind: "NOOP",
|
|
28
|
+
taskId: projection.taskId,
|
|
29
|
+
reason: "BINDING_NOT_READY",
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
kind: "WAKE",
|
|
33
|
+
taskId: projection.taskId,
|
|
34
|
+
nodeId: node.nodeId,
|
|
35
|
+
runNo: node.runNo,
|
|
36
|
+
roleRef: binding.roleRef,
|
|
37
|
+
workerRef: binding.workerRef,
|
|
38
|
+
trigger: node.runNo > 1 ? "REOPEN" : "NODE_READY",
|
|
39
|
+
conversationLocator: binding.conversationLocator,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const effectiveResumeSignal = resumeSignal ??
|
|
43
|
+
(projection.resumeSignalRef &&
|
|
44
|
+
projection.taskStatus === "ACTIVE" &&
|
|
45
|
+
node.status === "IN_PROGRESS"
|
|
46
|
+
? {
|
|
47
|
+
trigger: "TASK_RESUMED",
|
|
48
|
+
ref: projection.resumeSignalRef,
|
|
49
|
+
targetWorkerRef: binding.workerRef,
|
|
50
|
+
nodeId: node.nodeId,
|
|
51
|
+
runNo: node.runNo,
|
|
52
|
+
}
|
|
53
|
+
: undefined);
|
|
54
|
+
if (!effectiveResumeSignal)
|
|
55
|
+
return {
|
|
56
|
+
kind: "NOOP",
|
|
57
|
+
taskId: projection.taskId,
|
|
58
|
+
reason: "NO_PROGRESS_INTENT",
|
|
59
|
+
};
|
|
60
|
+
if (projection.taskStatus !== "ACTIVE" || node.status !== "IN_PROGRESS")
|
|
61
|
+
return {
|
|
62
|
+
kind: "NOOP",
|
|
63
|
+
taskId: projection.taskId,
|
|
64
|
+
reason: "BINDING_NOT_READY",
|
|
65
|
+
};
|
|
66
|
+
if (effectiveResumeSignal.nodeId !== node.nodeId ||
|
|
67
|
+
effectiveResumeSignal.runNo !== node.runNo)
|
|
68
|
+
return {
|
|
69
|
+
kind: "NOOP",
|
|
70
|
+
taskId: projection.taskId,
|
|
71
|
+
reason: "RESUME_GENERATION_MISMATCH",
|
|
72
|
+
};
|
|
73
|
+
if (effectiveResumeSignal.targetWorkerRef !== binding.workerRef)
|
|
74
|
+
return {
|
|
75
|
+
kind: "NOOP",
|
|
76
|
+
taskId: projection.taskId,
|
|
77
|
+
reason: "RESUME_TARGET_NOT_CURRENT_WORKER",
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
kind: "WAKE",
|
|
81
|
+
taskId: projection.taskId,
|
|
82
|
+
nodeId: node.nodeId,
|
|
83
|
+
runNo: node.runNo,
|
|
84
|
+
roleRef: binding.roleRef,
|
|
85
|
+
workerRef: binding.workerRef,
|
|
86
|
+
trigger: effectiveResumeSignal.trigger,
|
|
87
|
+
conversationLocator: binding.conversationLocator,
|
|
88
|
+
underlyingRef: effectiveResumeSignal.ref,
|
|
89
|
+
};
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomflow/proflow-platform-host",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -24,19 +24,19 @@
|
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"zod": "4.1.12",
|
|
27
|
+
"@tomflow/proflow-execution-browser-extension": "^0.1.51",
|
|
27
28
|
"@tomflow/proflow-agent-runtime": "^0.1.14",
|
|
28
|
-
"@tomflow/proflow-task-orchestration": "^0.1.11",
|
|
29
|
-
"@tomflow/proflow-execution-contracts": "^0.1.10",
|
|
30
29
|
"@tomflow/proflow-module-contract": "^0.1.13",
|
|
31
|
-
"@tomflow/proflow-
|
|
30
|
+
"@tomflow/proflow-task-orchestration": "^0.1.11",
|
|
32
31
|
"@tomflow/proflow-task-migration-runner": "^0.1.11",
|
|
33
|
-
"@tomflow/proflow-
|
|
32
|
+
"@tomflow/proflow-execution-contracts": "^0.1.11",
|
|
33
|
+
"@tomflow/proflow-task-store-sqlite": "^0.1.12"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@tomflow/proflow-agent-gateway": "^0.1.
|
|
36
|
+
"@tomflow/proflow-agent-gateway": "^0.1.17",
|
|
37
37
|
"@tomflow/proflow-deployment-conformance": "^0.1.13",
|
|
38
38
|
"@tomflow/proflow-model-runtime": "^0.1.24",
|
|
39
|
-
"@tomflow/proflow-execution-runtime": "^0.1.
|
|
39
|
+
"@tomflow/proflow-execution-runtime": "^0.1.19"
|
|
40
40
|
},
|
|
41
41
|
"description": "Provides the ProFlow local application composition root that binds Task, Agent, Execution and Model owner transports.",
|
|
42
42
|
"keywords": [
|
package/proflow.module.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"contractVersion": "1.0.0",
|
|
4
4
|
"moduleRef": "platform-host",
|
|
5
5
|
"packageName": "@tomflow/proflow-platform-host",
|
|
6
|
-
"moduleVersion": "0.1.
|
|
6
|
+
"moduleVersion": "0.1.23",
|
|
7
7
|
"kind": "service",
|
|
8
8
|
"templateVersion": "1.0.0",
|
|
9
9
|
"platformCompatibility": ">=1.0.0 <2.0.0",
|
|
@@ -19,19 +19,15 @@
|
|
|
19
19
|
],
|
|
20
20
|
"requires": [
|
|
21
21
|
{
|
|
22
|
-
"contractRef": "
|
|
22
|
+
"contractRef": "local-tool-bridge",
|
|
23
23
|
"versionRange": ">=1.0.0 <2.0.0"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
|
-
"contractRef": "
|
|
27
|
-
"versionRange": ">=1.0.0 <2.0.0"
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"contractRef": "execution",
|
|
26
|
+
"contractRef": "task-orchestration",
|
|
31
27
|
"versionRange": ">=1.0.0 <2.0.0"
|
|
32
28
|
},
|
|
33
29
|
{
|
|
34
|
-
"contractRef": "
|
|
30
|
+
"contractRef": "agent-runtime",
|
|
35
31
|
"versionRange": ">=1.0.0 <2.0.0"
|
|
36
32
|
}
|
|
37
33
|
],
|