@silbercue/chrome 0.2.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/LICENSE +21 -0
- package/README.md +229 -0
- package/build/cache/a11y-tree.d.ts +252 -0
- package/build/cache/a11y-tree.js +1956 -0
- package/build/cache/index.d.ts +8 -0
- package/build/cache/index.js +4 -0
- package/build/cache/selector-cache.d.ts +47 -0
- package/build/cache/selector-cache.js +119 -0
- package/build/cache/session-defaults.d.ts +27 -0
- package/build/cache/session-defaults.js +130 -0
- package/build/cache/tab-state-cache.d.ts +39 -0
- package/build/cache/tab-state-cache.js +171 -0
- package/build/cdp/cdp-client.d.ts +25 -0
- package/build/cdp/cdp-client.js +146 -0
- package/build/cdp/chrome-launcher.d.ts +85 -0
- package/build/cdp/chrome-launcher.js +502 -0
- package/build/cdp/console-collector.d.ts +53 -0
- package/build/cdp/console-collector.js +147 -0
- package/build/cdp/debug.d.ts +1 -0
- package/build/cdp/debug.js +6 -0
- package/build/cdp/dialog-handler.d.ts +54 -0
- package/build/cdp/dialog-handler.js +129 -0
- package/build/cdp/dom-watcher.d.ts +45 -0
- package/build/cdp/dom-watcher.js +195 -0
- package/build/cdp/emulation.d.ts +12 -0
- package/build/cdp/emulation.js +17 -0
- package/build/cdp/index.d.ts +11 -0
- package/build/cdp/index.js +6 -0
- package/build/cdp/network-collector.d.ts +77 -0
- package/build/cdp/network-collector.js +257 -0
- package/build/cdp/protocol.d.ts +20 -0
- package/build/cdp/protocol.js +1 -0
- package/build/cdp/session-manager.d.ts +62 -0
- package/build/cdp/session-manager.js +205 -0
- package/build/cdp/settle.d.ts +16 -0
- package/build/cdp/settle.js +71 -0
- package/build/cli/license-commands.d.ts +19 -0
- package/build/cli/license-commands.js +199 -0
- package/build/cli/top-level-commands.d.ts +49 -0
- package/build/cli/top-level-commands.js +222 -0
- package/build/hooks/index.d.ts +2 -0
- package/build/hooks/index.js +1 -0
- package/build/hooks/pro-hooks.d.ts +126 -0
- package/build/hooks/pro-hooks.js +17 -0
- package/build/index.d.ts +4 -0
- package/build/index.js +86 -0
- package/build/license/free-tier-config.d.ts +14 -0
- package/build/license/free-tier-config.js +18 -0
- package/build/license/index.d.ts +4 -0
- package/build/license/index.js +2 -0
- package/build/license/license-status.d.ts +15 -0
- package/build/license/license-status.js +9 -0
- package/build/overlay/session-overlay.d.ts +22 -0
- package/build/overlay/session-overlay.js +372 -0
- package/build/plan/index.d.ts +7 -0
- package/build/plan/index.js +4 -0
- package/build/plan/plan-conditions.d.ts +12 -0
- package/build/plan/plan-conditions.js +242 -0
- package/build/plan/plan-executor.d.ts +49 -0
- package/build/plan/plan-executor.js +259 -0
- package/build/plan/plan-state-store.d.ts +24 -0
- package/build/plan/plan-state-store.js +43 -0
- package/build/plan/plan-variables.d.ts +16 -0
- package/build/plan/plan-variables.js +71 -0
- package/build/registry.d.ts +124 -0
- package/build/registry.js +884 -0
- package/build/server.d.ts +1 -0
- package/build/server.js +245 -0
- package/build/tools/click.d.ts +34 -0
- package/build/tools/click.js +293 -0
- package/build/tools/configure-session.d.ts +15 -0
- package/build/tools/configure-session.js +45 -0
- package/build/tools/console-logs.d.ts +18 -0
- package/build/tools/console-logs.js +44 -0
- package/build/tools/dom-snapshot.d.ts +13 -0
- package/build/tools/dom-snapshot.js +259 -0
- package/build/tools/element-utils.d.ts +23 -0
- package/build/tools/element-utils.js +133 -0
- package/build/tools/error-utils.d.ts +8 -0
- package/build/tools/error-utils.js +27 -0
- package/build/tools/evaluate.d.ts +34 -0
- package/build/tools/evaluate.js +217 -0
- package/build/tools/file-upload.d.ts +20 -0
- package/build/tools/file-upload.js +174 -0
- package/build/tools/fill-form.d.ts +39 -0
- package/build/tools/fill-form.js +256 -0
- package/build/tools/handle-dialog.d.ts +15 -0
- package/build/tools/handle-dialog.js +48 -0
- package/build/tools/index.d.ts +35 -0
- package/build/tools/index.js +18 -0
- package/build/tools/navigate.d.ts +18 -0
- package/build/tools/navigate.js +111 -0
- package/build/tools/network-monitor.d.ts +18 -0
- package/build/tools/network-monitor.js +66 -0
- package/build/tools/observe.d.ts +44 -0
- package/build/tools/observe.js +339 -0
- package/build/tools/press-key.d.ts +33 -0
- package/build/tools/press-key.js +155 -0
- package/build/tools/read-page.d.ts +22 -0
- package/build/tools/read-page.js +100 -0
- package/build/tools/run-plan.d.ts +205 -0
- package/build/tools/run-plan.js +215 -0
- package/build/tools/screenshot.d.ts +16 -0
- package/build/tools/screenshot.js +283 -0
- package/build/tools/scroll.d.ts +28 -0
- package/build/tools/scroll.js +143 -0
- package/build/tools/switch-tab.d.ts +26 -0
- package/build/tools/switch-tab.js +355 -0
- package/build/tools/tab-status.d.ts +7 -0
- package/build/tools/tab-status.js +50 -0
- package/build/tools/type.d.ts +31 -0
- package/build/tools/type.js +247 -0
- package/build/tools/virtual-desk.d.ts +7 -0
- package/build/tools/virtual-desk.js +108 -0
- package/build/tools/visual-constants.d.ts +3 -0
- package/build/tools/visual-constants.js +10 -0
- package/build/tools/wait-for.d.ts +26 -0
- package/build/tools/wait-for.js +323 -0
- package/build/transport/index.d.ts +3 -0
- package/build/transport/index.js +2 -0
- package/build/transport/pipe-transport.d.ts +18 -0
- package/build/transport/pipe-transport.js +63 -0
- package/build/transport/transport.d.ts +8 -0
- package/build/transport/transport.js +1 -0
- package/build/transport/websocket-transport.d.ts +22 -0
- package/build/transport/websocket-transport.js +200 -0
- package/build/types.d.ts +21 -0
- package/build/types.js +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// --- Tokenizer ---
|
|
2
|
+
const OPERATORS = ["===", "!==", "==", "!=", ">=", "<=", "&&", "||", ">", "<"];
|
|
3
|
+
function tokenize(expression) {
|
|
4
|
+
const tokens = [];
|
|
5
|
+
let i = 0;
|
|
6
|
+
while (i < expression.length) {
|
|
7
|
+
// Skip whitespace
|
|
8
|
+
if (/\s/.test(expression[i])) {
|
|
9
|
+
i++;
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
// Parentheses
|
|
13
|
+
if (expression[i] === "(") {
|
|
14
|
+
tokens.push({ type: "lparen", value: "(", raw: "(" });
|
|
15
|
+
i++;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (expression[i] === ")") {
|
|
19
|
+
tokens.push({ type: "rparen", value: ")", raw: ")" });
|
|
20
|
+
i++;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
// Operators (multi-char first)
|
|
24
|
+
let matchedOp = false;
|
|
25
|
+
for (const op of OPERATORS) {
|
|
26
|
+
if (expression.startsWith(op, i)) {
|
|
27
|
+
tokens.push({ type: "operator", value: op, raw: op });
|
|
28
|
+
i += op.length;
|
|
29
|
+
matchedOp = true;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (matchedOp)
|
|
34
|
+
continue;
|
|
35
|
+
// NOT operator (! not followed by =)
|
|
36
|
+
if (expression[i] === "!" && expression[i + 1] !== "=") {
|
|
37
|
+
tokens.push({ type: "not", value: "!", raw: "!" });
|
|
38
|
+
i++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
// Variable: $varName
|
|
42
|
+
if (expression[i] === "$") {
|
|
43
|
+
const match = expression.slice(i).match(/^\$(\w+)/);
|
|
44
|
+
if (match) {
|
|
45
|
+
tokens.push({ type: "variable", value: match[1], raw: match[0] });
|
|
46
|
+
i += match[0].length;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// String literals (single or double quoted)
|
|
51
|
+
if (expression[i] === "'" || expression[i] === '"') {
|
|
52
|
+
const quote = expression[i];
|
|
53
|
+
let str = "";
|
|
54
|
+
i++; // skip opening quote
|
|
55
|
+
while (i < expression.length && expression[i] !== quote) {
|
|
56
|
+
if (expression[i] === "\\" && i + 1 < expression.length) {
|
|
57
|
+
str += expression[i + 1];
|
|
58
|
+
i += 2;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
str += expression[i];
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (i >= expression.length) {
|
|
66
|
+
throw new Error(`Unterminated string literal starting at position ${i - str.length - 1}`);
|
|
67
|
+
}
|
|
68
|
+
i++; // skip closing quote
|
|
69
|
+
tokens.push({ type: "string", value: str, raw: `${quote}${str}${quote}` });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// Number literals (including negative)
|
|
73
|
+
const numMatch = expression.slice(i).match(/^-?\d+(\.\d+)?/);
|
|
74
|
+
if (numMatch) {
|
|
75
|
+
// Make sure a negative number isn't just a minus operator
|
|
76
|
+
// A negative number is valid at start, after an operator, after lparen, or after 'not'
|
|
77
|
+
const prev = tokens.length > 0 ? tokens[tokens.length - 1] : null;
|
|
78
|
+
const isNegativeNum = expression[i] === "-"
|
|
79
|
+
? (!prev || prev.type === "operator" || prev.type === "lparen" || prev.type === "not")
|
|
80
|
+
: true;
|
|
81
|
+
if (isNegativeNum) {
|
|
82
|
+
tokens.push({ type: "number", value: Number(numMatch[0]), raw: numMatch[0] });
|
|
83
|
+
i += numMatch[0].length;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Boolean and null keywords
|
|
88
|
+
if (expression.startsWith("true", i) && !/\w/.test(expression[i + 4] || "")) {
|
|
89
|
+
tokens.push({ type: "boolean", value: true, raw: "true" });
|
|
90
|
+
i += 4;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (expression.startsWith("false", i) && !/\w/.test(expression[i + 5] || "")) {
|
|
94
|
+
tokens.push({ type: "boolean", value: false, raw: "false" });
|
|
95
|
+
i += 5;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (expression.startsWith("null", i) && !/\w/.test(expression[i + 4] || "")) {
|
|
99
|
+
tokens.push({ type: "null", value: null, raw: "null" });
|
|
100
|
+
i += 4;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// Unknown character — error
|
|
104
|
+
throw new Error(`Unexpected character '${expression[i]}' at position ${i}`);
|
|
105
|
+
}
|
|
106
|
+
tokens.push({ type: "eof", value: null, raw: "" });
|
|
107
|
+
return tokens;
|
|
108
|
+
}
|
|
109
|
+
// --- Recursive Descent Parser ---
|
|
110
|
+
class Parser {
|
|
111
|
+
tokens;
|
|
112
|
+
vars;
|
|
113
|
+
pos = 0;
|
|
114
|
+
constructor(tokens, vars) {
|
|
115
|
+
this.tokens = tokens;
|
|
116
|
+
this.vars = vars;
|
|
117
|
+
}
|
|
118
|
+
parse() {
|
|
119
|
+
const result = this.parseOr();
|
|
120
|
+
if (this.current().type !== "eof") {
|
|
121
|
+
throw new Error(`Unexpected token '${this.current().raw}' at end of expression`);
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
current() {
|
|
126
|
+
return this.tokens[this.pos];
|
|
127
|
+
}
|
|
128
|
+
advance() {
|
|
129
|
+
const token = this.tokens[this.pos];
|
|
130
|
+
this.pos++;
|
|
131
|
+
return token;
|
|
132
|
+
}
|
|
133
|
+
parseOr() {
|
|
134
|
+
let left = this.parseAnd();
|
|
135
|
+
while (this.current().type === "operator" && this.current().value === "||") {
|
|
136
|
+
this.advance();
|
|
137
|
+
const right = this.parseAnd();
|
|
138
|
+
left = left || right;
|
|
139
|
+
}
|
|
140
|
+
return left;
|
|
141
|
+
}
|
|
142
|
+
parseAnd() {
|
|
143
|
+
let left = this.parseComparison();
|
|
144
|
+
while (this.current().type === "operator" && this.current().value === "&&") {
|
|
145
|
+
this.advance();
|
|
146
|
+
const right = this.parseComparison();
|
|
147
|
+
left = left && right;
|
|
148
|
+
}
|
|
149
|
+
return left;
|
|
150
|
+
}
|
|
151
|
+
parseComparison() {
|
|
152
|
+
let left = this.parseUnary();
|
|
153
|
+
const compOps = ["===", "==", "!==", "!=", ">", "<", ">=", "<="];
|
|
154
|
+
while (this.current().type === "operator" && compOps.includes(this.current().value)) {
|
|
155
|
+
const op = this.advance().value;
|
|
156
|
+
const right = this.parseUnary();
|
|
157
|
+
left = this.applyComparison(left, op, right);
|
|
158
|
+
}
|
|
159
|
+
return left;
|
|
160
|
+
}
|
|
161
|
+
parseUnary() {
|
|
162
|
+
if (this.current().type === "not") {
|
|
163
|
+
this.advance();
|
|
164
|
+
const operand = this.parseUnary();
|
|
165
|
+
return !operand;
|
|
166
|
+
}
|
|
167
|
+
return this.parsePrimary();
|
|
168
|
+
}
|
|
169
|
+
parsePrimary() {
|
|
170
|
+
const token = this.current();
|
|
171
|
+
if (token.type === "lparen") {
|
|
172
|
+
this.advance(); // skip (
|
|
173
|
+
const value = this.parseOr();
|
|
174
|
+
if (this.current().type !== "rparen") {
|
|
175
|
+
throw new Error("Expected closing parenthesis");
|
|
176
|
+
}
|
|
177
|
+
this.advance(); // skip )
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
if (token.type === "variable") {
|
|
181
|
+
this.advance();
|
|
182
|
+
const varName = token.value;
|
|
183
|
+
return varName in this.vars ? this.vars[varName] : undefined;
|
|
184
|
+
}
|
|
185
|
+
if (token.type === "string") {
|
|
186
|
+
this.advance();
|
|
187
|
+
return token.value;
|
|
188
|
+
}
|
|
189
|
+
if (token.type === "number") {
|
|
190
|
+
this.advance();
|
|
191
|
+
return token.value;
|
|
192
|
+
}
|
|
193
|
+
if (token.type === "boolean") {
|
|
194
|
+
this.advance();
|
|
195
|
+
return token.value;
|
|
196
|
+
}
|
|
197
|
+
if (token.type === "null") {
|
|
198
|
+
this.advance();
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
throw new Error(`Unexpected token '${token.raw}'`);
|
|
202
|
+
}
|
|
203
|
+
applyComparison(left, op, right) {
|
|
204
|
+
switch (op) {
|
|
205
|
+
case "===": return left === right;
|
|
206
|
+
case "==": return left == right;
|
|
207
|
+
case "!==": return left !== right;
|
|
208
|
+
case "!=": return left != right;
|
|
209
|
+
case ">": return left > right;
|
|
210
|
+
case "<": return left < right;
|
|
211
|
+
case ">=": return left >= right;
|
|
212
|
+
case "<=": return left <= right;
|
|
213
|
+
default: return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Evaluate a simple condition expression against the vars map.
|
|
219
|
+
* Supports: ==, ===, !=, !==, >, <, >=, <=, &&, ||, !
|
|
220
|
+
* Variables via $varName syntax.
|
|
221
|
+
* String literals via single or double quotes.
|
|
222
|
+
* Number literals, boolean literals (true/false), null.
|
|
223
|
+
*
|
|
224
|
+
* SECURITY: No dynamic code evaluation. Parser-based.
|
|
225
|
+
* Unknown variables evaluate to undefined.
|
|
226
|
+
*/
|
|
227
|
+
export function evaluateCondition(expression, vars) {
|
|
228
|
+
// Empty expression → true (step executes)
|
|
229
|
+
if (!expression || expression.trim() === "") {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const tokens = tokenize(expression.trim());
|
|
234
|
+
const parser = new Parser(tokens, vars);
|
|
235
|
+
const result = parser.parse();
|
|
236
|
+
return !!result;
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
console.error(`[plan-conditions] Invalid expression: "${expression}" — ${err instanceof Error ? err.message : String(err)}`);
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ToolRegistry } from "../registry.js";
|
|
2
|
+
import type { ToolResponse, ToolMeta } from "../types.js";
|
|
3
|
+
import type { VarsMap } from "./plan-variables.js";
|
|
4
|
+
import type { PlanStateStore } from "./plan-state-store.js";
|
|
5
|
+
export type ErrorStrategy = "abort" | "continue" | "screenshot";
|
|
6
|
+
export interface SuspendConfig {
|
|
7
|
+
/** Frage an den Agent */
|
|
8
|
+
question?: string;
|
|
9
|
+
/** Context-Typ: "screenshot" erzeugt automatisch einen Screenshot */
|
|
10
|
+
context?: "screenshot";
|
|
11
|
+
/** Bedingung: Plan pausiert NACH Step-Ausfuehrung wenn Bedingung true */
|
|
12
|
+
condition?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface PlanStep {
|
|
15
|
+
tool: string;
|
|
16
|
+
params?: Record<string, unknown>;
|
|
17
|
+
saveAs?: string;
|
|
18
|
+
if?: string;
|
|
19
|
+
suspend?: SuspendConfig;
|
|
20
|
+
}
|
|
21
|
+
export interface StepResult {
|
|
22
|
+
step: number;
|
|
23
|
+
tool: string;
|
|
24
|
+
result: ToolResponse;
|
|
25
|
+
skipped?: boolean;
|
|
26
|
+
condition?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface PlanOptions {
|
|
29
|
+
vars?: VarsMap;
|
|
30
|
+
errorStrategy?: ErrorStrategy;
|
|
31
|
+
/** Fuer Resume: der gespeicherte Plan-State */
|
|
32
|
+
resumeState?: {
|
|
33
|
+
suspendedAtIndex: number;
|
|
34
|
+
completedResults: StepResult[];
|
|
35
|
+
vars: VarsMap;
|
|
36
|
+
answer: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export interface SuspendedPlanResponse {
|
|
40
|
+
status: "suspended";
|
|
41
|
+
planId: string;
|
|
42
|
+
question: string;
|
|
43
|
+
completedSteps: StepResult[];
|
|
44
|
+
screenshot?: string;
|
|
45
|
+
_meta?: ToolMeta;
|
|
46
|
+
}
|
|
47
|
+
/** executePlan kann jetzt entweder ToolResponse oder SuspendedPlanResponse zurueckgeben */
|
|
48
|
+
export type PlanExecutionResult = ToolResponse | SuspendedPlanResponse;
|
|
49
|
+
export declare function executePlan(steps: PlanStep[], registry: ToolRegistry, options?: PlanOptions, stateStore?: PlanStateStore): Promise<PlanExecutionResult>;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { substituteVars, extractResultValue } from "./plan-variables.js";
|
|
2
|
+
import { evaluateCondition } from "./plan-conditions.js";
|
|
3
|
+
const DEFAULT_SUSPEND_QUESTION = "Plan pausiert -- Bedingung erfuellt. Wie fortfahren?";
|
|
4
|
+
export async function executePlan(steps, registry, options, stateStore) {
|
|
5
|
+
const start = performance.now();
|
|
6
|
+
let results = [];
|
|
7
|
+
const vars = { ...(options?.vars ?? {}) };
|
|
8
|
+
const errorStrategy = options?.errorStrategy ?? "abort";
|
|
9
|
+
let startIndex = 0;
|
|
10
|
+
let isResumeFirstStep = false;
|
|
11
|
+
// --- Resume: restore state from previous suspend ---
|
|
12
|
+
if (options?.resumeState) {
|
|
13
|
+
const rs = options.resumeState;
|
|
14
|
+
Object.assign(vars, rs.vars);
|
|
15
|
+
vars["answer"] = rs.answer;
|
|
16
|
+
results = [...rs.completedResults];
|
|
17
|
+
startIndex = rs.suspendedAtIndex;
|
|
18
|
+
isResumeFirstStep = true;
|
|
19
|
+
}
|
|
20
|
+
for (let i = startIndex; i < steps.length; i++) {
|
|
21
|
+
const step = steps[i];
|
|
22
|
+
// --- Conditional: evaluate if clause ---
|
|
23
|
+
if (step.if !== undefined && step.if !== "") {
|
|
24
|
+
const conditionResult = evaluateCondition(step.if, vars);
|
|
25
|
+
if (!conditionResult) {
|
|
26
|
+
results.push({
|
|
27
|
+
step: i + 1,
|
|
28
|
+
tool: step.tool,
|
|
29
|
+
result: {
|
|
30
|
+
content: [{ type: "text", text: `Skipped: condition "${step.if}" was false` }],
|
|
31
|
+
_meta: { elapsedMs: 0, method: step.tool },
|
|
32
|
+
},
|
|
33
|
+
skipped: true,
|
|
34
|
+
condition: step.if,
|
|
35
|
+
});
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// --- Pre-Suspend (without condition): pause BEFORE step execution ---
|
|
40
|
+
// Skip pre-suspend on the first step of a resume (agent already answered)
|
|
41
|
+
if (step.suspend && !step.suspend.condition && !isResumeFirstStep) {
|
|
42
|
+
if (!stateStore) {
|
|
43
|
+
console.warn("[plan-executor] suspend config on step but no stateStore provided — ignoring suspend");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const question = step.suspend.question ?? DEFAULT_SUSPEND_QUESTION;
|
|
47
|
+
let screenshot;
|
|
48
|
+
if (step.suspend.context === "screenshot") {
|
|
49
|
+
try {
|
|
50
|
+
const ssResult = await registry.executeTool("screenshot", {});
|
|
51
|
+
if (!ssResult.isError) {
|
|
52
|
+
for (const block of ssResult.content) {
|
|
53
|
+
if (block.type === "image") {
|
|
54
|
+
screenshot = block.data;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Screenshot is best-effort
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const planId = stateStore.suspend({
|
|
65
|
+
steps,
|
|
66
|
+
suspendedAtIndex: i,
|
|
67
|
+
vars: { ...vars },
|
|
68
|
+
errorStrategy,
|
|
69
|
+
completedResults: [...results],
|
|
70
|
+
question,
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
status: "suspended",
|
|
74
|
+
planId,
|
|
75
|
+
question,
|
|
76
|
+
completedSteps: [...results],
|
|
77
|
+
screenshot,
|
|
78
|
+
_meta: {
|
|
79
|
+
elapsedMs: Math.round(performance.now() - start),
|
|
80
|
+
method: "run_plan",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Reset resume-first-step flag after pre-suspend check
|
|
86
|
+
isResumeFirstStep = false;
|
|
87
|
+
// --- Variable substitution ---
|
|
88
|
+
const resolvedParams = step.params
|
|
89
|
+
? substituteVars(step.params, vars)
|
|
90
|
+
: {};
|
|
91
|
+
// --- Execute step ---
|
|
92
|
+
let stepResult;
|
|
93
|
+
try {
|
|
94
|
+
stepResult = await registry.executeTool(step.tool, resolvedParams);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
98
|
+
stepResult = {
|
|
99
|
+
content: [{ type: "text", text: `Exception in ${step.tool}: ${message}` }],
|
|
100
|
+
isError: true,
|
|
101
|
+
_meta: { elapsedMs: 0, method: step.tool },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// --- saveAs: store result as variable ---
|
|
105
|
+
if (!stepResult.isError && step.saveAs) {
|
|
106
|
+
vars[step.saveAs] = extractResultValue(stepResult);
|
|
107
|
+
}
|
|
108
|
+
results.push({ step: i + 1, tool: step.tool, result: stepResult });
|
|
109
|
+
// --- Post-Suspend (with condition): pause AFTER step execution if condition is true ---
|
|
110
|
+
if (step.suspend?.condition && !stepResult.isError) {
|
|
111
|
+
const suspendConditionResult = evaluateCondition(step.suspend.condition, vars);
|
|
112
|
+
if (suspendConditionResult) {
|
|
113
|
+
if (!stateStore) {
|
|
114
|
+
console.warn("[plan-executor] suspend condition met but no stateStore provided — ignoring suspend");
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const question = step.suspend.question ?? DEFAULT_SUSPEND_QUESTION;
|
|
118
|
+
let screenshot;
|
|
119
|
+
if (step.suspend.context === "screenshot") {
|
|
120
|
+
try {
|
|
121
|
+
const ssResult = await registry.executeTool("screenshot", {});
|
|
122
|
+
if (!ssResult.isError) {
|
|
123
|
+
for (const block of ssResult.content) {
|
|
124
|
+
if (block.type === "image") {
|
|
125
|
+
screenshot = block.data;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Screenshot is best-effort
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const planId = stateStore.suspend({
|
|
136
|
+
steps,
|
|
137
|
+
suspendedAtIndex: i + 1,
|
|
138
|
+
vars: { ...vars },
|
|
139
|
+
errorStrategy,
|
|
140
|
+
completedResults: [...results],
|
|
141
|
+
question,
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
status: "suspended",
|
|
145
|
+
planId,
|
|
146
|
+
question,
|
|
147
|
+
completedSteps: [...results],
|
|
148
|
+
screenshot,
|
|
149
|
+
_meta: {
|
|
150
|
+
elapsedMs: Math.round(performance.now() - start),
|
|
151
|
+
method: "run_plan",
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// --- Error handling based on strategy ---
|
|
158
|
+
if (stepResult.isError) {
|
|
159
|
+
if (errorStrategy === "abort") {
|
|
160
|
+
return buildPlanResponse(results, steps.length, start, true, errorStrategy);
|
|
161
|
+
}
|
|
162
|
+
if (errorStrategy === "screenshot") {
|
|
163
|
+
// Take screenshot and append to the failed step
|
|
164
|
+
try {
|
|
165
|
+
const screenshotResult = await registry.executeTool("screenshot", {});
|
|
166
|
+
// Append screenshot content to the failed step's result
|
|
167
|
+
const lastResult = results[results.length - 1];
|
|
168
|
+
if (!screenshotResult.isError) {
|
|
169
|
+
for (const block of screenshotResult.content) {
|
|
170
|
+
if (block.type === "image") {
|
|
171
|
+
lastResult.result = {
|
|
172
|
+
...lastResult.result,
|
|
173
|
+
content: [...lastResult.result.content, block],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Screenshot is best-effort
|
|
181
|
+
}
|
|
182
|
+
return buildPlanResponse(results, steps.length, start, true, errorStrategy);
|
|
183
|
+
}
|
|
184
|
+
// errorStrategy === "continue": just keep going
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return buildPlanResponse(results, steps.length, start, false, errorStrategy);
|
|
188
|
+
}
|
|
189
|
+
function buildPlanResponse(results, stepsTotal, startTime, aborted, errorStrategy = "abort") {
|
|
190
|
+
const elapsedMs = Math.round(performance.now() - startTime);
|
|
191
|
+
const contentBlocks = [];
|
|
192
|
+
let okCount = 0;
|
|
193
|
+
let failCount = 0;
|
|
194
|
+
let skipCount = 0;
|
|
195
|
+
for (const r of results) {
|
|
196
|
+
if (r.skipped) {
|
|
197
|
+
skipCount++;
|
|
198
|
+
contentBlocks.push({
|
|
199
|
+
type: "text",
|
|
200
|
+
text: `[${r.step}/${stepsTotal}] SKIP ${r.tool} (condition: ${r.condition})`,
|
|
201
|
+
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const status = r.result.isError ? "FAIL" : "OK";
|
|
205
|
+
if (r.result.isError)
|
|
206
|
+
failCount++;
|
|
207
|
+
else
|
|
208
|
+
okCount++;
|
|
209
|
+
const stepMs = r.result._meta?.elapsedMs ?? 0;
|
|
210
|
+
// Build step header text from text content blocks
|
|
211
|
+
const textParts = r.result.content
|
|
212
|
+
.filter((c) => c.type === "text")
|
|
213
|
+
.map((c) => c.text)
|
|
214
|
+
.join("\n");
|
|
215
|
+
contentBlocks.push({
|
|
216
|
+
type: "text",
|
|
217
|
+
text: `[${r.step}/${stepsTotal}] ${status} ${r.tool} (${stepMs}ms): ${textParts}`,
|
|
218
|
+
});
|
|
219
|
+
// Preserve non-text content blocks (e.g. screenshot images)
|
|
220
|
+
for (const block of r.result.content) {
|
|
221
|
+
if (block.type !== "text") {
|
|
222
|
+
contentBlocks.push(block);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (aborted) {
|
|
227
|
+
contentBlocks.push({
|
|
228
|
+
type: "text",
|
|
229
|
+
text: `\nPlan aborted at step ${results.length}/${stepsTotal}`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
// Summary for continue strategy with errors
|
|
233
|
+
if (errorStrategy === "continue" && failCount > 0 && !aborted) {
|
|
234
|
+
const parts = [`${okCount}/${stepsTotal} OK`, `${failCount} FAIL`];
|
|
235
|
+
if (skipCount > 0)
|
|
236
|
+
parts.push(`${skipCount} SKIP`);
|
|
237
|
+
contentBlocks.push({
|
|
238
|
+
type: "text",
|
|
239
|
+
text: `\nPlan completed with errors: ${parts.join(", ")}`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
// Determine isError:
|
|
243
|
+
// - abort/screenshot: aborted flag
|
|
244
|
+
// - continue: only if ALL executed (non-skipped) steps failed
|
|
245
|
+
const executedCount = okCount + failCount;
|
|
246
|
+
const isError = errorStrategy === "continue" && !aborted
|
|
247
|
+
? executedCount > 0 && failCount === executedCount
|
|
248
|
+
: aborted;
|
|
249
|
+
return {
|
|
250
|
+
content: contentBlocks,
|
|
251
|
+
isError: isError || undefined,
|
|
252
|
+
_meta: {
|
|
253
|
+
elapsedMs,
|
|
254
|
+
method: "run_plan",
|
|
255
|
+
stepsTotal,
|
|
256
|
+
stepsCompleted: okCount,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PlanStep, StepResult, ErrorStrategy } from "./plan-executor.js";
|
|
2
|
+
import type { VarsMap } from "./plan-variables.js";
|
|
3
|
+
export interface SuspendedPlanState {
|
|
4
|
+
planId: string;
|
|
5
|
+
steps: PlanStep[];
|
|
6
|
+
/** Index des Steps an dem der Plan pausiert wurde (naechster auszufuehrender Step) */
|
|
7
|
+
suspendedAtIndex: number;
|
|
8
|
+
vars: VarsMap;
|
|
9
|
+
errorStrategy: ErrorStrategy;
|
|
10
|
+
completedResults: StepResult[];
|
|
11
|
+
question: string;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class PlanStateStore {
|
|
15
|
+
private store;
|
|
16
|
+
private readonly ttlMs;
|
|
17
|
+
constructor(ttlMs?: number);
|
|
18
|
+
/** Suspend speichert den Plan-State und gibt eine planId zurueck */
|
|
19
|
+
suspend(state: Omit<SuspendedPlanState, "planId" | "createdAt">): string;
|
|
20
|
+
/** Resume laedt und entfernt den Plan-State. Gibt null zurueck wenn abgelaufen oder nicht gefunden. */
|
|
21
|
+
resume(planId: string): SuspendedPlanState | null;
|
|
22
|
+
/** Bereinigt abgelaufene Eintraege (aufgerufen bei suspend) */
|
|
23
|
+
private cleanup;
|
|
24
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export class PlanStateStore {
|
|
3
|
+
store = new Map();
|
|
4
|
+
ttlMs;
|
|
5
|
+
constructor(ttlMs = 5 * 60 * 1000) {
|
|
6
|
+
this.ttlMs = ttlMs;
|
|
7
|
+
}
|
|
8
|
+
/** Suspend speichert den Plan-State und gibt eine planId zurueck */
|
|
9
|
+
suspend(state) {
|
|
10
|
+
this.cleanup();
|
|
11
|
+
const planId = randomUUID();
|
|
12
|
+
this.store.set(planId, {
|
|
13
|
+
...state,
|
|
14
|
+
planId,
|
|
15
|
+
createdAt: Date.now(),
|
|
16
|
+
});
|
|
17
|
+
return planId;
|
|
18
|
+
}
|
|
19
|
+
/** Resume laedt und entfernt den Plan-State. Gibt null zurueck wenn abgelaufen oder nicht gefunden. */
|
|
20
|
+
resume(planId) {
|
|
21
|
+
this.cleanup();
|
|
22
|
+
const state = this.store.get(planId);
|
|
23
|
+
if (!state) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
// TTL check (>= so that TTL=0 expires immediately)
|
|
27
|
+
if (Date.now() - state.createdAt >= this.ttlMs) {
|
|
28
|
+
this.store.delete(planId);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
this.store.delete(planId);
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
34
|
+
/** Bereinigt abgelaufene Eintraege (aufgerufen bei suspend) */
|
|
35
|
+
cleanup() {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
for (const [id, state] of this.store) {
|
|
38
|
+
if (now - state.createdAt >= this.ttlMs) {
|
|
39
|
+
this.store.delete(id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ToolResponse } from "../types.js";
|
|
2
|
+
export type VarsMap = Record<string, unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* Substitute $var references in a params object.
|
|
5
|
+
* Replaces string values matching "$varName" pattern with the value from vars.
|
|
6
|
+
* Supports nested objects and arrays.
|
|
7
|
+
* Unresolved $var references remain as-is (no error — the tool will handle invalid params).
|
|
8
|
+
*/
|
|
9
|
+
export declare function substituteVars(params: Record<string, unknown>, vars: VarsMap): Record<string, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Extract the text content from a ToolResponse for saveAs.
|
|
12
|
+
* Concatenates all text content blocks into a single string.
|
|
13
|
+
* If the text is valid JSON, parse it and return the parsed value.
|
|
14
|
+
* Otherwise return the raw text string.
|
|
15
|
+
*/
|
|
16
|
+
export declare function extractResultValue(result: ToolResponse): unknown;
|