@dexto/orchestration 1.5.8
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/LICENSE +44 -0
- package/dist/agent-controller.cjs +265 -0
- package/dist/agent-controller.d.cts +116 -0
- package/dist/agent-controller.d.ts +116 -0
- package/dist/agent-controller.js +241 -0
- package/dist/condition-engine.cjs +276 -0
- package/dist/condition-engine.d.cts +87 -0
- package/dist/condition-engine.d.ts +87 -0
- package/dist/condition-engine.js +252 -0
- package/dist/index.cjs +57 -0
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +32 -0
- package/dist/signal-bus.cjs +186 -0
- package/dist/signal-bus.d.cts +78 -0
- package/dist/signal-bus.d.ts +78 -0
- package/dist/signal-bus.js +162 -0
- package/dist/task-registry.cjs +345 -0
- package/dist/task-registry.d.cts +124 -0
- package/dist/task-registry.d.ts +124 -0
- package/dist/task-registry.js +321 -0
- package/dist/tools/check-task.cjs +65 -0
- package/dist/tools/check-task.d.cts +56 -0
- package/dist/tools/check-task.d.ts +56 -0
- package/dist/tools/check-task.js +40 -0
- package/dist/tools/index.cjs +51 -0
- package/dist/tools/index.d.cts +10 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +19 -0
- package/dist/tools/list-tasks.cjs +80 -0
- package/dist/tools/list-tasks.d.cts +64 -0
- package/dist/tools/list-tasks.d.ts +64 -0
- package/dist/tools/list-tasks.js +55 -0
- package/dist/tools/start-task.cjs +149 -0
- package/dist/tools/start-task.d.cts +102 -0
- package/dist/tools/start-task.d.ts +102 -0
- package/dist/tools/start-task.js +123 -0
- package/dist/tools/types.cjs +16 -0
- package/dist/tools/types.d.cts +30 -0
- package/dist/tools/types.d.ts +30 -0
- package/dist/tools/types.js +0 -0
- package/dist/tools/wait-for.cjs +116 -0
- package/dist/tools/wait-for.d.cts +74 -0
- package/dist/tools/wait-for.d.ts +74 -0
- package/dist/tools/wait-for.js +91 -0
- package/dist/types.cjs +16 -0
- package/dist/types.d.cts +165 -0
- package/dist/types.d.ts +165 -0
- package/dist/types.js +0 -0
- package/package.json +38 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { SignalBus } from "./signal-bus.js";
|
|
2
|
+
import { TaskRegistry } from "./task-registry.js";
|
|
3
|
+
import { ConditionEngine } from "./condition-engine.js";
|
|
4
|
+
class AgentController {
|
|
5
|
+
agent;
|
|
6
|
+
logger;
|
|
7
|
+
state = "idle";
|
|
8
|
+
sessionId;
|
|
9
|
+
/** Signal bus for event routing */
|
|
10
|
+
signalBus;
|
|
11
|
+
/** Task registry for tracking background tasks */
|
|
12
|
+
taskRegistry;
|
|
13
|
+
/** Condition engine for evaluating wait conditions */
|
|
14
|
+
conditionEngine;
|
|
15
|
+
/** Signals that arrived while agent was busy */
|
|
16
|
+
pendingSignals = [];
|
|
17
|
+
/** Unsubscribe function for notify listener */
|
|
18
|
+
notifyUnsubscribe;
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.agent = config.agent;
|
|
21
|
+
if (config.logger) {
|
|
22
|
+
this.logger = config.logger;
|
|
23
|
+
}
|
|
24
|
+
this.sessionId = config.sessionId ?? `session-${Date.now()}`;
|
|
25
|
+
this.signalBus = new SignalBus();
|
|
26
|
+
this.taskRegistry = new TaskRegistry(this.signalBus, config.taskRegistry);
|
|
27
|
+
this.conditionEngine = new ConditionEngine(this.taskRegistry, this.signalBus, this.logger);
|
|
28
|
+
this.setupNotifyListener();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Set up listener for tasks with notify=true
|
|
32
|
+
*/
|
|
33
|
+
setupNotifyListener() {
|
|
34
|
+
this.notifyUnsubscribe = this.signalBus.onAny((signal) => {
|
|
35
|
+
if (signal.type === "task:completed" || signal.type === "task:failed" || signal.type === "task:cancelled") {
|
|
36
|
+
const entry = this.taskRegistry.get(signal.taskId);
|
|
37
|
+
if (entry?.notify) {
|
|
38
|
+
if (this.state === "idle") {
|
|
39
|
+
this.logger?.debug(`Auto-notify triggered for task ${signal.taskId}`);
|
|
40
|
+
void this.processNotify(signal).catch((error) => {
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
const signalContext = `signal.type=${signal.type} taskId=${signal.taskId}`;
|
|
43
|
+
this.logger?.error?.(
|
|
44
|
+
`AgentController.processNotify failed for ${signalContext}: ${message}`
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
} else {
|
|
48
|
+
this.pendingSignals.push(signal);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Process an auto-notify task completion
|
|
56
|
+
*/
|
|
57
|
+
async processNotify(signal) {
|
|
58
|
+
if (this.state !== "idle") {
|
|
59
|
+
this.pendingSignals.push(signal);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
this.state = "processing";
|
|
64
|
+
const taskInfo = signal.type === "task:completed" || signal.type === "task:failed" || signal.type === "task:cancelled" ? this.taskRegistry.getInfo(signal.taskId) : void 0;
|
|
65
|
+
const contextMessage = this.buildNotifyContext(signal, taskInfo);
|
|
66
|
+
await this.agent.generate(contextMessage, this.sessionId);
|
|
67
|
+
if (taskInfo) {
|
|
68
|
+
this.taskRegistry.acknowledgeNotify([taskInfo.taskId]);
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
this.state = "idle";
|
|
72
|
+
this.processPendingSignals();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build context message for auto-notify
|
|
77
|
+
*/
|
|
78
|
+
buildNotifyContext(signal, taskInfo) {
|
|
79
|
+
if (signal.type === "task:completed" && taskInfo) {
|
|
80
|
+
const resultStr = typeof taskInfo.result === "string" ? taskInfo.result : JSON.stringify(taskInfo.result, null, 2);
|
|
81
|
+
const durationLine = taskInfo.duration !== void 0 ? `Duration: ${taskInfo.duration}ms
|
|
82
|
+
` : "";
|
|
83
|
+
return `[Background Task Completed]
|
|
84
|
+
Task ID: ${taskInfo.taskId}
|
|
85
|
+
Type: ${taskInfo.type}
|
|
86
|
+
Description: ${taskInfo.description}
|
|
87
|
+
` + durationLine + `Result:
|
|
88
|
+
${resultStr}`;
|
|
89
|
+
}
|
|
90
|
+
if (signal.type === "task:failed" && taskInfo) {
|
|
91
|
+
return `[Background Task Failed]
|
|
92
|
+
Task ID: ${taskInfo.taskId}
|
|
93
|
+
Type: ${taskInfo.type}
|
|
94
|
+
Description: ${taskInfo.description}
|
|
95
|
+
Error: ${taskInfo.error}`;
|
|
96
|
+
}
|
|
97
|
+
if (signal.type === "task:cancelled" && taskInfo) {
|
|
98
|
+
const cancelReason = taskInfo.error ?? "Cancelled";
|
|
99
|
+
return `[Background Task Cancelled]
|
|
100
|
+
Task ID: ${taskInfo.taskId}
|
|
101
|
+
Type: ${taskInfo.type}
|
|
102
|
+
Description: ${taskInfo.description}
|
|
103
|
+
Reason: ${cancelReason}`;
|
|
104
|
+
}
|
|
105
|
+
return `[Background Signal]
|
|
106
|
+
${JSON.stringify(signal, null, 2)}`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Process any pending signals
|
|
110
|
+
*/
|
|
111
|
+
processPendingSignals() {
|
|
112
|
+
while (this.pendingSignals.length > 0 && this.state === "idle") {
|
|
113
|
+
const signal = this.pendingSignals.shift();
|
|
114
|
+
if (signal) {
|
|
115
|
+
void this.processNotify(signal).catch((error) => {
|
|
116
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
117
|
+
const signalContext = signal.type === "task:completed" || signal.type === "task:failed" || signal.type === "task:cancelled" ? `signal.type=${signal.type} taskId=${signal.taskId}` : `signal.type=${signal.type}`;
|
|
118
|
+
this.logger?.error?.(
|
|
119
|
+
`AgentController.processNotify failed for ${signalContext}: ${message}`
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Process user input and generate response
|
|
127
|
+
* @param content User message content
|
|
128
|
+
* @returns Agent response
|
|
129
|
+
*/
|
|
130
|
+
async process(content) {
|
|
131
|
+
if (this.state !== "idle") {
|
|
132
|
+
throw new Error(`Cannot process while agent is ${this.state}`);
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
this.state = "processing";
|
|
136
|
+
const { contextPrefix, notifyTaskIds } = this.buildTaskContext();
|
|
137
|
+
const fullContent = contextPrefix ? `${contextPrefix}
|
|
138
|
+
|
|
139
|
+
${content}` : content;
|
|
140
|
+
const response = await this.agent.generate(fullContent, this.sessionId);
|
|
141
|
+
if (notifyTaskIds.length > 0) {
|
|
142
|
+
this.taskRegistry.acknowledgeNotify(notifyTaskIds);
|
|
143
|
+
}
|
|
144
|
+
return response.content;
|
|
145
|
+
} finally {
|
|
146
|
+
this.state = "idle";
|
|
147
|
+
this.processPendingSignals();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Process a signal trigger (e.g., from external source)
|
|
152
|
+
*/
|
|
153
|
+
async processSignal(signal) {
|
|
154
|
+
if (this.state !== "idle") {
|
|
155
|
+
this.pendingSignals.push(signal);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
await this.processNotify(signal);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Build context about pending/completed tasks
|
|
162
|
+
*/
|
|
163
|
+
buildTaskContext() {
|
|
164
|
+
const running = this.taskRegistry.list({ status: "running" });
|
|
165
|
+
const notifyPending = this.taskRegistry.getNotifyPending();
|
|
166
|
+
if (running.length === 0 && notifyPending.length === 0) {
|
|
167
|
+
return { contextPrefix: "", notifyTaskIds: [] };
|
|
168
|
+
}
|
|
169
|
+
const parts = [];
|
|
170
|
+
if (running.length > 0) {
|
|
171
|
+
parts.push(
|
|
172
|
+
`[Background Tasks Running: ${running.length}]
|
|
173
|
+
` + running.map((t) => `- ${t.taskId}: ${t.description}`).join("\n")
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (notifyPending.length > 0) {
|
|
177
|
+
parts.push(
|
|
178
|
+
`[Background Tasks Completed: ${notifyPending.length}]
|
|
179
|
+
` + notifyPending.map((t) => {
|
|
180
|
+
const status = t.error ? `FAILED: ${t.error}` : t.status === "cancelled" ? "CANCELLED" : "SUCCESS";
|
|
181
|
+
return `- ${t.taskId}: ${t.description} [${status}]`;
|
|
182
|
+
}).join("\n")
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
contextPrefix: parts.join("\n\n"),
|
|
187
|
+
notifyTaskIds: notifyPending.map((task) => task.taskId)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get current agent state
|
|
192
|
+
*/
|
|
193
|
+
getState() {
|
|
194
|
+
return this.state;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get the wrapped agent
|
|
198
|
+
*/
|
|
199
|
+
getAgent() {
|
|
200
|
+
return this.agent;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get session ID
|
|
204
|
+
*/
|
|
205
|
+
getSessionId() {
|
|
206
|
+
return this.sessionId;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Inject a signal for processing
|
|
210
|
+
*/
|
|
211
|
+
injectSignal(signal) {
|
|
212
|
+
this.signalBus.emit(signal);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Clean up resources
|
|
216
|
+
*/
|
|
217
|
+
cleanup() {
|
|
218
|
+
if (this.notifyUnsubscribe) {
|
|
219
|
+
this.notifyUnsubscribe();
|
|
220
|
+
}
|
|
221
|
+
this.pendingSignals = [];
|
|
222
|
+
this.signalBus.clear();
|
|
223
|
+
this.taskRegistry.clear();
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Start the agent (delegates to wrapped agent)
|
|
227
|
+
*/
|
|
228
|
+
async start() {
|
|
229
|
+
await this.agent.start();
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Stop the agent (delegates to wrapped agent)
|
|
233
|
+
*/
|
|
234
|
+
async stop() {
|
|
235
|
+
this.cleanup();
|
|
236
|
+
await this.agent.stop();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
export {
|
|
240
|
+
AgentController
|
|
241
|
+
};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var condition_engine_exports = {};
|
|
20
|
+
__export(condition_engine_exports, {
|
|
21
|
+
ConditionEngine: () => ConditionEngine
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(condition_engine_exports);
|
|
24
|
+
var import_crypto = require("crypto");
|
|
25
|
+
class ConditionEngine {
|
|
26
|
+
constructor(taskRegistry, signalBus, logger) {
|
|
27
|
+
this.taskRegistry = taskRegistry;
|
|
28
|
+
this.signalBus = signalBus;
|
|
29
|
+
this.logger = logger;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Wait for a condition to be met
|
|
33
|
+
* @param condition Wait condition to evaluate
|
|
34
|
+
* @returns Promise resolving to the signal(s) that satisfied the condition
|
|
35
|
+
*/
|
|
36
|
+
async wait(condition) {
|
|
37
|
+
const immediate = this.check(condition);
|
|
38
|
+
if (immediate) {
|
|
39
|
+
return immediate;
|
|
40
|
+
}
|
|
41
|
+
return this.evaluate(condition);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if a condition is already satisfied (non-blocking)
|
|
45
|
+
* @returns WaitResult if satisfied, null if not
|
|
46
|
+
*/
|
|
47
|
+
check(condition) {
|
|
48
|
+
switch (condition.type) {
|
|
49
|
+
case "task":
|
|
50
|
+
return this.checkTask(condition.taskId);
|
|
51
|
+
case "any":
|
|
52
|
+
return this.checkAny(condition.conditions);
|
|
53
|
+
case "all":
|
|
54
|
+
return this.checkAll(condition.conditions);
|
|
55
|
+
case "timeout":
|
|
56
|
+
return null;
|
|
57
|
+
case "race":
|
|
58
|
+
return this.check(condition.task);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Check if a single task is completed
|
|
63
|
+
*/
|
|
64
|
+
checkTask(taskId) {
|
|
65
|
+
const result = this.taskRegistry.getResult(taskId);
|
|
66
|
+
if (!result) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (result.status === "completed") {
|
|
70
|
+
return {
|
|
71
|
+
signal: {
|
|
72
|
+
type: "task:completed",
|
|
73
|
+
taskId,
|
|
74
|
+
result: result.result
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (result.status === "failed") {
|
|
79
|
+
return {
|
|
80
|
+
signal: {
|
|
81
|
+
type: "task:failed",
|
|
82
|
+
taskId,
|
|
83
|
+
error: result.error ?? "Unknown error"
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (result.status === "cancelled") {
|
|
88
|
+
return {
|
|
89
|
+
signal: {
|
|
90
|
+
type: "task:cancelled",
|
|
91
|
+
taskId
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Check if any of the conditions is satisfied
|
|
99
|
+
*/
|
|
100
|
+
checkAny(conditions) {
|
|
101
|
+
for (const condition of conditions) {
|
|
102
|
+
const result = this.check(condition);
|
|
103
|
+
if (result) {
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if all conditions are satisfied
|
|
111
|
+
*/
|
|
112
|
+
checkAll(conditions) {
|
|
113
|
+
const signals = [];
|
|
114
|
+
for (const condition of conditions) {
|
|
115
|
+
const result = this.check(condition);
|
|
116
|
+
if (!result) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
signals.push(result.signal);
|
|
120
|
+
}
|
|
121
|
+
const primarySignal = signals[0];
|
|
122
|
+
if (!primarySignal) {
|
|
123
|
+
throw new Error("Internal error: no signals in checkAll result");
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
signal: primarySignal,
|
|
127
|
+
allSignals: signals
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Evaluate a condition asynchronously
|
|
132
|
+
*/
|
|
133
|
+
async evaluate(condition) {
|
|
134
|
+
switch (condition.type) {
|
|
135
|
+
case "task":
|
|
136
|
+
return this.evaluateTask(condition.taskId);
|
|
137
|
+
case "any":
|
|
138
|
+
return this.evaluateAny(condition.conditions);
|
|
139
|
+
case "all":
|
|
140
|
+
return this.evaluateAll(condition.conditions);
|
|
141
|
+
case "timeout":
|
|
142
|
+
return this.evaluateTimeout(condition.ms, condition.conditionId);
|
|
143
|
+
case "race":
|
|
144
|
+
return this.evaluateRace(condition.task, condition.timeout);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Wait for a single task to complete
|
|
149
|
+
*
|
|
150
|
+
* Uses subscribe-then-check pattern to avoid race conditions where
|
|
151
|
+
* the task completes between checking and subscribing.
|
|
152
|
+
*/
|
|
153
|
+
async evaluateTask(taskId) {
|
|
154
|
+
this.logger?.debug(`[ConditionEngine] evaluateTask called for taskId=${taskId}`);
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
let unsubscribe;
|
|
157
|
+
const handler = (signal) => {
|
|
158
|
+
this.logger?.debug(
|
|
159
|
+
`[ConditionEngine] Received signal: type=${signal.type}, taskId=${"taskId" in signal ? signal.taskId : "N/A"}`
|
|
160
|
+
);
|
|
161
|
+
if ((signal.type === "task:completed" || signal.type === "task:failed" || signal.type === "task:cancelled") && signal.taskId === taskId) {
|
|
162
|
+
this.logger?.debug(
|
|
163
|
+
`[ConditionEngine] Signal matches taskId=${taskId}, resolving`
|
|
164
|
+
);
|
|
165
|
+
if (unsubscribe) {
|
|
166
|
+
unsubscribe();
|
|
167
|
+
}
|
|
168
|
+
resolve({ signal });
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
unsubscribe = this.signalBus.onAny(handler);
|
|
172
|
+
this.logger?.debug(`[ConditionEngine] Subscribed to signals for taskId=${taskId}`);
|
|
173
|
+
const immediate = this.checkTask(taskId);
|
|
174
|
+
this.logger?.debug(
|
|
175
|
+
`[ConditionEngine] checkTask(${taskId}) returned: ${immediate ? "found" : "null"}`
|
|
176
|
+
);
|
|
177
|
+
if (immediate) {
|
|
178
|
+
unsubscribe();
|
|
179
|
+
resolve(immediate);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Wait for any of the conditions to be satisfied
|
|
185
|
+
*/
|
|
186
|
+
async evaluateAny(conditions) {
|
|
187
|
+
const immediate = this.checkAny(conditions);
|
|
188
|
+
if (immediate) {
|
|
189
|
+
return immediate;
|
|
190
|
+
}
|
|
191
|
+
const promises = conditions.map((c) => this.evaluate(c));
|
|
192
|
+
const result = await Promise.race(promises);
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Wait for all conditions to be satisfied
|
|
197
|
+
*/
|
|
198
|
+
async evaluateAll(conditions) {
|
|
199
|
+
const immediate = this.checkAll(conditions);
|
|
200
|
+
if (immediate) {
|
|
201
|
+
return immediate;
|
|
202
|
+
}
|
|
203
|
+
const results = await Promise.all(conditions.map((c) => this.evaluate(c)));
|
|
204
|
+
const signals = results.map((r) => r.signal);
|
|
205
|
+
const primarySignal = signals[0];
|
|
206
|
+
if (!primarySignal) {
|
|
207
|
+
throw new Error("Internal error: no signals in evaluateAll result");
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
signal: primarySignal,
|
|
211
|
+
allSignals: signals
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Wait for a timeout
|
|
216
|
+
*/
|
|
217
|
+
async evaluateTimeout(ms, conditionId) {
|
|
218
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
219
|
+
const signal = {
|
|
220
|
+
type: "timeout",
|
|
221
|
+
conditionId
|
|
222
|
+
};
|
|
223
|
+
this.signalBus.emit(signal);
|
|
224
|
+
return { signal };
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Race a task condition against a timeout
|
|
228
|
+
*/
|
|
229
|
+
async evaluateRace(taskCondition, timeoutCondition) {
|
|
230
|
+
const immediate = this.check(taskCondition);
|
|
231
|
+
if (immediate) {
|
|
232
|
+
return immediate;
|
|
233
|
+
}
|
|
234
|
+
const result = await Promise.race([
|
|
235
|
+
this.evaluate(taskCondition),
|
|
236
|
+
this.evaluate(timeoutCondition)
|
|
237
|
+
]);
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Helper to create a race condition with timeout
|
|
242
|
+
*/
|
|
243
|
+
static createRaceWithTimeout(taskId, timeoutMs) {
|
|
244
|
+
return {
|
|
245
|
+
type: "race",
|
|
246
|
+
task: { type: "task", taskId },
|
|
247
|
+
timeout: {
|
|
248
|
+
type: "timeout",
|
|
249
|
+
ms: timeoutMs,
|
|
250
|
+
conditionId: `timeout-${(0, import_crypto.randomUUID)().slice(0, 8)}`
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Helper to create an 'any' condition from task IDs
|
|
256
|
+
*/
|
|
257
|
+
static createAnyTask(taskIds) {
|
|
258
|
+
return {
|
|
259
|
+
type: "any",
|
|
260
|
+
conditions: taskIds.map((taskId) => ({ type: "task", taskId }))
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Helper to create an 'all' condition from task IDs
|
|
265
|
+
*/
|
|
266
|
+
static createAllTasks(taskIds) {
|
|
267
|
+
return {
|
|
268
|
+
type: "all",
|
|
269
|
+
conditions: taskIds.map((taskId) => ({ type: "task", taskId }))
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
274
|
+
0 && (module.exports = {
|
|
275
|
+
ConditionEngine
|
|
276
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { WaitCondition, WaitResult } from './types.cjs';
|
|
2
|
+
import { SignalBus } from './signal-bus.cjs';
|
|
3
|
+
import { TaskRegistry } from './task-registry.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ConditionEngine
|
|
7
|
+
*
|
|
8
|
+
* Evaluates wait conditions and resolves when met.
|
|
9
|
+
* Supports single task, any/all of multiple tasks, timeouts, and races.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
type LoggerLike = {
|
|
13
|
+
debug: (message: string) => void;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* ConditionEngine - Evaluates composable wait conditions
|
|
17
|
+
*/
|
|
18
|
+
declare class ConditionEngine {
|
|
19
|
+
private taskRegistry;
|
|
20
|
+
private signalBus;
|
|
21
|
+
private logger?;
|
|
22
|
+
constructor(taskRegistry: TaskRegistry, signalBus: SignalBus, logger?: LoggerLike | undefined);
|
|
23
|
+
/**
|
|
24
|
+
* Wait for a condition to be met
|
|
25
|
+
* @param condition Wait condition to evaluate
|
|
26
|
+
* @returns Promise resolving to the signal(s) that satisfied the condition
|
|
27
|
+
*/
|
|
28
|
+
wait(condition: WaitCondition): Promise<WaitResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Check if a condition is already satisfied (non-blocking)
|
|
31
|
+
* @returns WaitResult if satisfied, null if not
|
|
32
|
+
*/
|
|
33
|
+
check(condition: WaitCondition): WaitResult | null;
|
|
34
|
+
/**
|
|
35
|
+
* Check if a single task is completed
|
|
36
|
+
*/
|
|
37
|
+
private checkTask;
|
|
38
|
+
/**
|
|
39
|
+
* Check if any of the conditions is satisfied
|
|
40
|
+
*/
|
|
41
|
+
private checkAny;
|
|
42
|
+
/**
|
|
43
|
+
* Check if all conditions are satisfied
|
|
44
|
+
*/
|
|
45
|
+
private checkAll;
|
|
46
|
+
/**
|
|
47
|
+
* Evaluate a condition asynchronously
|
|
48
|
+
*/
|
|
49
|
+
private evaluate;
|
|
50
|
+
/**
|
|
51
|
+
* Wait for a single task to complete
|
|
52
|
+
*
|
|
53
|
+
* Uses subscribe-then-check pattern to avoid race conditions where
|
|
54
|
+
* the task completes between checking and subscribing.
|
|
55
|
+
*/
|
|
56
|
+
private evaluateTask;
|
|
57
|
+
/**
|
|
58
|
+
* Wait for any of the conditions to be satisfied
|
|
59
|
+
*/
|
|
60
|
+
private evaluateAny;
|
|
61
|
+
/**
|
|
62
|
+
* Wait for all conditions to be satisfied
|
|
63
|
+
*/
|
|
64
|
+
private evaluateAll;
|
|
65
|
+
/**
|
|
66
|
+
* Wait for a timeout
|
|
67
|
+
*/
|
|
68
|
+
private evaluateTimeout;
|
|
69
|
+
/**
|
|
70
|
+
* Race a task condition against a timeout
|
|
71
|
+
*/
|
|
72
|
+
private evaluateRace;
|
|
73
|
+
/**
|
|
74
|
+
* Helper to create a race condition with timeout
|
|
75
|
+
*/
|
|
76
|
+
static createRaceWithTimeout(taskId: string, timeoutMs: number): WaitCondition;
|
|
77
|
+
/**
|
|
78
|
+
* Helper to create an 'any' condition from task IDs
|
|
79
|
+
*/
|
|
80
|
+
static createAnyTask(taskIds: string[]): WaitCondition;
|
|
81
|
+
/**
|
|
82
|
+
* Helper to create an 'all' condition from task IDs
|
|
83
|
+
*/
|
|
84
|
+
static createAllTasks(taskIds: string[]): WaitCondition;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { ConditionEngine };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { WaitCondition, WaitResult } from './types.js';
|
|
2
|
+
import { SignalBus } from './signal-bus.js';
|
|
3
|
+
import { TaskRegistry } from './task-registry.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ConditionEngine
|
|
7
|
+
*
|
|
8
|
+
* Evaluates wait conditions and resolves when met.
|
|
9
|
+
* Supports single task, any/all of multiple tasks, timeouts, and races.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
type LoggerLike = {
|
|
13
|
+
debug: (message: string) => void;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* ConditionEngine - Evaluates composable wait conditions
|
|
17
|
+
*/
|
|
18
|
+
declare class ConditionEngine {
|
|
19
|
+
private taskRegistry;
|
|
20
|
+
private signalBus;
|
|
21
|
+
private logger?;
|
|
22
|
+
constructor(taskRegistry: TaskRegistry, signalBus: SignalBus, logger?: LoggerLike | undefined);
|
|
23
|
+
/**
|
|
24
|
+
* Wait for a condition to be met
|
|
25
|
+
* @param condition Wait condition to evaluate
|
|
26
|
+
* @returns Promise resolving to the signal(s) that satisfied the condition
|
|
27
|
+
*/
|
|
28
|
+
wait(condition: WaitCondition): Promise<WaitResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Check if a condition is already satisfied (non-blocking)
|
|
31
|
+
* @returns WaitResult if satisfied, null if not
|
|
32
|
+
*/
|
|
33
|
+
check(condition: WaitCondition): WaitResult | null;
|
|
34
|
+
/**
|
|
35
|
+
* Check if a single task is completed
|
|
36
|
+
*/
|
|
37
|
+
private checkTask;
|
|
38
|
+
/**
|
|
39
|
+
* Check if any of the conditions is satisfied
|
|
40
|
+
*/
|
|
41
|
+
private checkAny;
|
|
42
|
+
/**
|
|
43
|
+
* Check if all conditions are satisfied
|
|
44
|
+
*/
|
|
45
|
+
private checkAll;
|
|
46
|
+
/**
|
|
47
|
+
* Evaluate a condition asynchronously
|
|
48
|
+
*/
|
|
49
|
+
private evaluate;
|
|
50
|
+
/**
|
|
51
|
+
* Wait for a single task to complete
|
|
52
|
+
*
|
|
53
|
+
* Uses subscribe-then-check pattern to avoid race conditions where
|
|
54
|
+
* the task completes between checking and subscribing.
|
|
55
|
+
*/
|
|
56
|
+
private evaluateTask;
|
|
57
|
+
/**
|
|
58
|
+
* Wait for any of the conditions to be satisfied
|
|
59
|
+
*/
|
|
60
|
+
private evaluateAny;
|
|
61
|
+
/**
|
|
62
|
+
* Wait for all conditions to be satisfied
|
|
63
|
+
*/
|
|
64
|
+
private evaluateAll;
|
|
65
|
+
/**
|
|
66
|
+
* Wait for a timeout
|
|
67
|
+
*/
|
|
68
|
+
private evaluateTimeout;
|
|
69
|
+
/**
|
|
70
|
+
* Race a task condition against a timeout
|
|
71
|
+
*/
|
|
72
|
+
private evaluateRace;
|
|
73
|
+
/**
|
|
74
|
+
* Helper to create a race condition with timeout
|
|
75
|
+
*/
|
|
76
|
+
static createRaceWithTimeout(taskId: string, timeoutMs: number): WaitCondition;
|
|
77
|
+
/**
|
|
78
|
+
* Helper to create an 'any' condition from task IDs
|
|
79
|
+
*/
|
|
80
|
+
static createAnyTask(taskIds: string[]): WaitCondition;
|
|
81
|
+
/**
|
|
82
|
+
* Helper to create an 'all' condition from task IDs
|
|
83
|
+
*/
|
|
84
|
+
static createAllTasks(taskIds: string[]): WaitCondition;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { ConditionEngine };
|