unbrowse 2.12.2 → 2.12.7
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/README.md +8 -44
- package/dist/cli.js +514 -20723
- package/package.json +4 -10
- package/runtime-src/api/routes.ts +15 -801
- package/runtime-src/auth/index.ts +32 -142
- package/runtime-src/capture/index.ts +101 -436
- package/runtime-src/cli.ts +371 -956
- package/runtime-src/client/index.ts +29 -622
- package/runtime-src/execution/index.ts +85 -345
- package/runtime-src/graph/index.ts +10 -128
- package/runtime-src/intent-match.ts +27 -27
- package/runtime-src/kuri/client.ts +82 -543
- package/runtime-src/orchestrator/index.ts +462 -2246
- package/runtime-src/reverse-engineer/index.ts +22 -220
- package/runtime-src/runtime/local-server.ts +16 -149
- package/runtime-src/runtime/paths.ts +5 -9
- package/runtime-src/runtime/setup.ts +1 -52
- package/runtime-src/server.ts +11 -6
- package/runtime-src/transform/schema-hints.ts +358 -0
- package/runtime-src/types/skill.ts +2 -49
- package/runtime-src/verification/index.ts +0 -15
- package/runtime-src/version.ts +13 -13
- package/vendor/kuri/darwin-arm64/kuri +0 -0
- package/vendor/kuri/darwin-x64/kuri +0 -0
- package/vendor/kuri/linux-arm64/kuri +0 -0
- package/vendor/kuri/linux-x64/kuri +0 -0
- package/bin/unbrowse-wrapper.mjs +0 -39
- package/bin/unbrowse.js +0 -38
- package/runtime-src/analytics-session.ts +0 -33
- package/runtime-src/api/browse-index.ts +0 -254
- package/runtime-src/api/browse-session.ts +0 -179
- package/runtime-src/api/browse-submit.ts +0 -455
- package/runtime-src/auth/runtime.ts +0 -116
- package/runtime-src/browser/index.ts +0 -635
- package/runtime-src/browser/types.ts +0 -41
- package/runtime-src/capture/prefetch.ts +0 -122
- package/runtime-src/capture/rsc.ts +0 -45
- package/runtime-src/cli/shortcuts.ts +0 -273
- package/runtime-src/client/graph-client.ts +0 -99
- package/runtime-src/execution/robots.ts +0 -167
- package/runtime-src/execution/search-forms.ts +0 -188
- package/runtime-src/graph/planner.ts +0 -411
- package/runtime-src/graph/session.ts +0 -294
- package/runtime-src/graph/trace-store.ts +0 -136
- package/runtime-src/indexer/index.ts +0 -480
- package/runtime-src/orchestrator/browser-agent.ts +0 -374
- package/runtime-src/orchestrator/dag-advisor.ts +0 -59
- package/runtime-src/orchestrator/dag-feedback.ts +0 -256
- package/runtime-src/orchestrator/first-pass-action.ts +0 -362
- package/runtime-src/orchestrator/passive-publish.ts +0 -152
- package/runtime-src/orchestrator/timing-economics.ts +0 -80
- package/runtime-src/payments/cascade.ts +0 -137
- package/runtime-src/payments/index.ts +0 -268
- package/runtime-src/payments/wallet.ts +0 -33
- package/runtime-src/reverse-engineer/description-prompt.ts +0 -132
- package/runtime-src/router.ts +0 -17
- package/runtime-src/runtime/browser-access.ts +0 -11
- package/runtime-src/runtime/browser-host.ts +0 -48
- package/runtime-src/runtime/lifecycle.ts +0 -17
- package/runtime-src/runtime/supervisor.ts +0 -69
- package/runtime-src/single-binary.ts +0 -141
- package/runtime-src/telemetry.ts +0 -253
- package/runtime-src/verification/matrix.ts +0 -30
- package/scripts/postinstall.mjs +0 -81
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
export interface SearchFormField {
|
|
2
|
-
name: string;
|
|
3
|
-
type: "text" | "select" | "radio" | "checkbox" | "date" | "hidden";
|
|
4
|
-
selector: string;
|
|
5
|
-
options?: string[];
|
|
6
|
-
required: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface SearchFormSpec {
|
|
10
|
-
form_selector: string;
|
|
11
|
-
submit_selector: string;
|
|
12
|
-
fields: SearchFormField[];
|
|
13
|
-
result_selector?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function isStructuredSearchForm(spec: SearchFormSpec): boolean {
|
|
17
|
-
return spec.fields.length > 0 && !!spec.submit_selector;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// HTML detection — parse raw HTML to discover search forms
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
const SEARCH_FIELD_NAMES = new Set([
|
|
25
|
-
"q", "query", "search", "keyword", "keywords", "term", "terms",
|
|
26
|
-
"find", "lookup", "filter", "s", "text", "input",
|
|
27
|
-
]);
|
|
28
|
-
|
|
29
|
-
const LOGIN_FIELD_NAMES = new Set([
|
|
30
|
-
"password", "passwd", "pass", "pwd", "confirm_password",
|
|
31
|
-
"username", "email", "login", "user",
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
const SUPPORTED_INPUT_TYPES = new Set([
|
|
35
|
-
"text", "search", "hidden", "date", "number", "tel", "email",
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
|
-
function formSelectorFromElement(
|
|
39
|
-
attribs: Record<string, string>,
|
|
40
|
-
index: number,
|
|
41
|
-
): string {
|
|
42
|
-
const id = attribs.id;
|
|
43
|
-
if (id) return `form#${id}`;
|
|
44
|
-
const name = attribs.name;
|
|
45
|
-
if (name) return `form[name="${name}"]`;
|
|
46
|
-
const action = attribs.action;
|
|
47
|
-
if (action) return `form[action="${action}"]`;
|
|
48
|
-
return `form:nth-of-type(${index + 1})`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function inputSelectorFromElement(
|
|
52
|
-
attribs: Record<string, string>,
|
|
53
|
-
tagName: string,
|
|
54
|
-
): string {
|
|
55
|
-
const id = attribs.id;
|
|
56
|
-
if (id) return `#${id}`;
|
|
57
|
-
const name = attribs.name;
|
|
58
|
-
if (name) return `${tagName}[name="${name}"]`;
|
|
59
|
-
return tagName;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function mapInputType(
|
|
63
|
-
typeAttr: string | undefined,
|
|
64
|
-
tagName: string,
|
|
65
|
-
): SearchFormField["type"] | null {
|
|
66
|
-
if (tagName === "select") return "select";
|
|
67
|
-
if (tagName === "textarea") return "text";
|
|
68
|
-
const t = (typeAttr ?? "text").toLowerCase();
|
|
69
|
-
if (t === "radio") return "radio";
|
|
70
|
-
if (t === "checkbox") return "checkbox";
|
|
71
|
-
if (t === "date") return "date";
|
|
72
|
-
if (t === "hidden") return "hidden";
|
|
73
|
-
if (t === "submit" || t === "button" || t === "image" || t === "reset") return null;
|
|
74
|
-
if (t === "password" || t === "file") return null;
|
|
75
|
-
if (SUPPORTED_INPUT_TYPES.has(t)) return "text";
|
|
76
|
-
return "text";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function parseAttrs(attrStr: string): Record<string, string> {
|
|
80
|
-
const attrs: Record<string, string> = {};
|
|
81
|
-
const attrRegex = /(\w[\w-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
82
|
-
let m: RegExpExecArray | null;
|
|
83
|
-
while ((m = attrRegex.exec(attrStr)) !== null) {
|
|
84
|
-
attrs[m[1]] = m[2] ?? m[3] ?? m[4] ?? "";
|
|
85
|
-
}
|
|
86
|
-
return attrs;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Detect structured search forms from raw HTML.
|
|
91
|
-
* Returns a SearchFormSpec for each form that looks like a search/filter form
|
|
92
|
-
* (has at least one search-like field and a submit mechanism).
|
|
93
|
-
* Login/password forms are excluded.
|
|
94
|
-
*/
|
|
95
|
-
export function detectSearchForms(html: string): SearchFormSpec[] {
|
|
96
|
-
const results: SearchFormSpec[] = [];
|
|
97
|
-
const formRegex = /<form([^>]*)>([\s\S]*?)<\/form>/gi;
|
|
98
|
-
let formMatch: RegExpExecArray | null;
|
|
99
|
-
let formIndex = 0;
|
|
100
|
-
|
|
101
|
-
while ((formMatch = formRegex.exec(html)) !== null) {
|
|
102
|
-
const formAttrs = formMatch[1];
|
|
103
|
-
const formBody = formMatch[2];
|
|
104
|
-
|
|
105
|
-
const formElAttrs = parseAttrs(formAttrs);
|
|
106
|
-
|
|
107
|
-
// Find all input/select/textarea elements
|
|
108
|
-
const fieldRegex = /<(input|select|textarea)([^>]*)\/?>/gi;
|
|
109
|
-
let fieldMatch: RegExpExecArray | null;
|
|
110
|
-
const fields: SearchFormField[] = [];
|
|
111
|
-
const seenNames = new Set<string>();
|
|
112
|
-
let hasLoginField = false;
|
|
113
|
-
let hasSearchLikeField = false;
|
|
114
|
-
|
|
115
|
-
while ((fieldMatch = fieldRegex.exec(formBody)) !== null) {
|
|
116
|
-
const tagName = fieldMatch[1].toLowerCase();
|
|
117
|
-
const fieldAttrs = parseAttrs(fieldMatch[2]);
|
|
118
|
-
const name = fieldAttrs.name ?? "";
|
|
119
|
-
const typeAttr = fieldAttrs.type;
|
|
120
|
-
|
|
121
|
-
// Check for login-form indicators
|
|
122
|
-
if (LOGIN_FIELD_NAMES.has(name.toLowerCase()) || typeAttr === "password") {
|
|
123
|
-
hasLoginField = true;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Check for search-like fields
|
|
127
|
-
if (SEARCH_FIELD_NAMES.has(name.toLowerCase())) {
|
|
128
|
-
hasSearchLikeField = true;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const mappedType = mapInputType(typeAttr, tagName);
|
|
132
|
-
if (!mappedType) continue;
|
|
133
|
-
if (!name && mappedType !== "text") continue;
|
|
134
|
-
if (seenNames.has(name) && mappedType !== "radio") continue;
|
|
135
|
-
if (name) seenNames.add(name);
|
|
136
|
-
|
|
137
|
-
// Collect select options
|
|
138
|
-
let options: string[] | undefined;
|
|
139
|
-
if (tagName === "select") {
|
|
140
|
-
const optRegex = /<option[^>]*value="([^"]*)"[^>]*>/gi;
|
|
141
|
-
let optMatch: RegExpExecArray | null;
|
|
142
|
-
options = [];
|
|
143
|
-
while ((optMatch = optRegex.exec(formBody)) !== null) {
|
|
144
|
-
options.push(optMatch[1]);
|
|
145
|
-
}
|
|
146
|
-
if (options.length === 0) options = undefined;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
fields.push({
|
|
150
|
-
name: name || `unnamed_${fields.length}`,
|
|
151
|
-
type: mappedType,
|
|
152
|
-
selector: inputSelectorFromElement(fieldAttrs, tagName),
|
|
153
|
-
...(options ? { options } : {}),
|
|
154
|
-
required: fieldAttrs.required !== undefined,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Detect submit mechanism
|
|
159
|
-
let submitSelector = "";
|
|
160
|
-
if (/<button[^>]*type\s*=\s*"submit"/i.test(formBody)) {
|
|
161
|
-
submitSelector = "button[type=submit]";
|
|
162
|
-
} else if (/<input[^>]*type\s*=\s*"submit"/i.test(formBody)) {
|
|
163
|
-
submitSelector = 'input[type="submit"]';
|
|
164
|
-
} else if (/<button/i.test(formBody)) {
|
|
165
|
-
submitSelector = "button";
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Skip login forms, require at least one meaningful field
|
|
169
|
-
const nonHiddenFields = fields.filter((f) => f.type !== "hidden");
|
|
170
|
-
if (
|
|
171
|
-
!hasLoginField &&
|
|
172
|
-
nonHiddenFields.length > 0 &&
|
|
173
|
-
submitSelector &&
|
|
174
|
-
(hasSearchLikeField || nonHiddenFields.length >= 1)
|
|
175
|
-
) {
|
|
176
|
-
const formSelector = formSelectorFromElement(formElAttrs, formIndex);
|
|
177
|
-
results.push({
|
|
178
|
-
form_selector: formSelector,
|
|
179
|
-
submit_selector: submitSelector,
|
|
180
|
-
fields,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
formIndex++;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return results;
|
|
188
|
-
}
|
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DAG execution planner — advisory only.
|
|
3
|
-
*
|
|
4
|
-
* Provides topological ordering, prerequisite analysis, and session-level
|
|
5
|
-
* feedback tracking for the operation graph. Nothing here auto-executes;
|
|
6
|
-
* callers use the plan to boost/penalize ranked endpoints and to suggest
|
|
7
|
-
* next steps to the agent.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type {
|
|
11
|
-
SkillManifest,
|
|
12
|
-
SkillOperationGraph,
|
|
13
|
-
SkillOperationNode,
|
|
14
|
-
SkillOperationEdge,
|
|
15
|
-
} from "../types/index.js";
|
|
16
|
-
import { ensureSkillOperationGraph } from "./index.js";
|
|
17
|
-
import { deriveAuthDependencies } from "../auth/runtime.js";
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Types
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
export interface ExecutionStep {
|
|
24
|
-
operation_id: string;
|
|
25
|
-
endpoint_id: string;
|
|
26
|
-
method: string;
|
|
27
|
-
produces_bindings: string[];
|
|
28
|
-
requires_confirmation: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface ExecutionPlan {
|
|
32
|
-
steps: ExecutionStep[];
|
|
33
|
-
/** True when all prerequisite bindings for the target are already known. */
|
|
34
|
-
chain_ready: boolean;
|
|
35
|
-
/** Operations that were unreachable and therefore excluded. */
|
|
36
|
-
unreachable: string[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface DagAdvisoryPlan {
|
|
40
|
-
/** All prerequisite bindings are satisfied by known bindings. */
|
|
41
|
-
chain_ready: boolean;
|
|
42
|
-
/** Operations that become runnable after the target completes. */
|
|
43
|
-
predicted_next: string[];
|
|
44
|
-
/** Ordered prerequisite operations leading to the target. */
|
|
45
|
-
prerequisite_order: string[];
|
|
46
|
-
/** Operations in the graph that are not needed for the target. */
|
|
47
|
-
skippable: string[];
|
|
48
|
-
/** Auth dependencies detected from endpoint semantics. */
|
|
49
|
-
auth_dependencies: import("../auth/runtime.js").AuthDependency[];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// Mutable-method detection
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
const MUTABLE_METHODS = new Set(["POST", "PUT", "DELETE", "PATCH"]);
|
|
57
|
-
|
|
58
|
-
function isMutable(method: string): boolean {
|
|
59
|
-
return MUTABLE_METHODS.has(method.toUpperCase());
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// 1. buildExecutionPlan
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Topological sort from entry points to `targetOperationId`, producing
|
|
68
|
-
* ordered steps. Each step carries the bindings it will produce that
|
|
69
|
-
* downstream steps need. Unreachable nodes are excluded. Mutable
|
|
70
|
-
* operations (POST/PUT/DELETE/PATCH) are flagged `requires_confirmation`.
|
|
71
|
-
*/
|
|
72
|
-
export function buildExecutionPlan(
|
|
73
|
-
graph: SkillOperationGraph,
|
|
74
|
-
targetOperationId: string,
|
|
75
|
-
knownBindings: Set<string>,
|
|
76
|
-
): ExecutionPlan {
|
|
77
|
-
const opMap = new Map<string, SkillOperationNode>();
|
|
78
|
-
for (const op of graph.operations) {
|
|
79
|
-
opMap.set(op.operation_id, op);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Build adjacency lists from the edge list.
|
|
83
|
-
// predecessors: operation_id -> set of operation_ids that must run first
|
|
84
|
-
const predecessors = new Map<string, Set<string>>();
|
|
85
|
-
for (const op of graph.operations) {
|
|
86
|
-
predecessors.set(op.operation_id, new Set());
|
|
87
|
-
}
|
|
88
|
-
for (const edge of graph.edges) {
|
|
89
|
-
if (edge.kind !== "dependency" && edge.kind !== "parent_child" && edge.kind !== "auth") continue;
|
|
90
|
-
const deps = predecessors.get(edge.to_operation_id);
|
|
91
|
-
if (deps) deps.add(edge.from_operation_id);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Walk backwards from target to find all ancestors (required ops).
|
|
95
|
-
const needed = new Set<string>();
|
|
96
|
-
const queue: string[] = [targetOperationId];
|
|
97
|
-
while (queue.length > 0) {
|
|
98
|
-
const current = queue.pop()!;
|
|
99
|
-
if (needed.has(current)) continue;
|
|
100
|
-
needed.add(current);
|
|
101
|
-
const deps = predecessors.get(current);
|
|
102
|
-
if (deps) {
|
|
103
|
-
for (const dep of deps) {
|
|
104
|
-
queue.push(dep);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Filter to only check if we can skip a needed node because its
|
|
110
|
-
// required bindings are already known.
|
|
111
|
-
const satisfied = new Set<string>(knownBindings);
|
|
112
|
-
|
|
113
|
-
// Kahn's algorithm over the needed subgraph.
|
|
114
|
-
const inDegree = new Map<string, number>();
|
|
115
|
-
for (const id of needed) {
|
|
116
|
-
inDegree.set(id, 0);
|
|
117
|
-
}
|
|
118
|
-
for (const edge of graph.edges) {
|
|
119
|
-
if (edge.kind !== "dependency" && edge.kind !== "parent_child" && edge.kind !== "auth") continue;
|
|
120
|
-
if (!needed.has(edge.from_operation_id) || !needed.has(edge.to_operation_id)) continue;
|
|
121
|
-
inDegree.set(edge.to_operation_id, (inDegree.get(edge.to_operation_id) ?? 0) + 1);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const ready: string[] = [];
|
|
125
|
-
for (const [id, deg] of inDegree) {
|
|
126
|
-
if (deg === 0) ready.push(id);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const steps: ExecutionStep[] = [];
|
|
130
|
-
const visited = new Set<string>();
|
|
131
|
-
|
|
132
|
-
while (ready.length > 0) {
|
|
133
|
-
// Sort for determinism: prefer lower operation_id.
|
|
134
|
-
ready.sort();
|
|
135
|
-
const current = ready.shift()!;
|
|
136
|
-
if (visited.has(current)) continue;
|
|
137
|
-
visited.add(current);
|
|
138
|
-
|
|
139
|
-
const op = opMap.get(current);
|
|
140
|
-
if (!op) continue;
|
|
141
|
-
|
|
142
|
-
const producesBindings = op.provides.map((b) => b.key);
|
|
143
|
-
steps.push({
|
|
144
|
-
operation_id: op.operation_id,
|
|
145
|
-
endpoint_id: op.endpoint_id,
|
|
146
|
-
method: op.method,
|
|
147
|
-
produces_bindings: producesBindings,
|
|
148
|
-
requires_confirmation: isMutable(op.method),
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Mark produced bindings as satisfied.
|
|
152
|
-
for (const key of producesBindings) {
|
|
153
|
-
satisfied.add(key);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Decrement in-degree for successors.
|
|
157
|
-
for (const edge of graph.edges) {
|
|
158
|
-
if (edge.kind !== "dependency" && edge.kind !== "parent_child" && edge.kind !== "auth") continue;
|
|
159
|
-
if (edge.from_operation_id !== current) continue;
|
|
160
|
-
if (!needed.has(edge.to_operation_id)) continue;
|
|
161
|
-
const deg = (inDegree.get(edge.to_operation_id) ?? 1) - 1;
|
|
162
|
-
inDegree.set(edge.to_operation_id, deg);
|
|
163
|
-
if (deg === 0) ready.push(edge.to_operation_id);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Determine chain_ready: check whether the target's required bindings are
|
|
168
|
-
// all either in the original known set or produced by steps before it.
|
|
169
|
-
const targetOp = opMap.get(targetOperationId);
|
|
170
|
-
const chainReady = targetOp
|
|
171
|
-
? targetOp.requires.every(
|
|
172
|
-
(b) => !b.required || satisfied.has(b.key) || knownBindings.has(b.key),
|
|
173
|
-
)
|
|
174
|
-
: false;
|
|
175
|
-
|
|
176
|
-
// Unreachable = operations in the graph that are not in the needed set.
|
|
177
|
-
const unreachable = graph.operations
|
|
178
|
-
.filter((op) => !needed.has(op.operation_id))
|
|
179
|
-
.map((op) => op.operation_id);
|
|
180
|
-
|
|
181
|
-
return { steps, chain_ready: chainReady, unreachable };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ---------------------------------------------------------------------------
|
|
185
|
-
// 2. fetchDagAdvisoryPlan
|
|
186
|
-
// ---------------------------------------------------------------------------
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Uses the local graph to build an advisory execution plan.
|
|
190
|
-
*
|
|
191
|
-
* @param skill The skill manifest (used to build/get the graph).
|
|
192
|
-
* @param targetEndpointId The endpoint we want to execute.
|
|
193
|
-
* @param knownBindingKeys Binding keys already available from context/params.
|
|
194
|
-
*/
|
|
195
|
-
export function fetchDagAdvisoryPlan(
|
|
196
|
-
skill: SkillManifest,
|
|
197
|
-
targetEndpointId: string,
|
|
198
|
-
knownBindingKeys: string[],
|
|
199
|
-
): DagAdvisoryPlan {
|
|
200
|
-
const graph = ensureSkillOperationGraph(skill);
|
|
201
|
-
const knownSet = new Set(knownBindingKeys);
|
|
202
|
-
|
|
203
|
-
// Derive auth dependencies from the target endpoint's semantics.
|
|
204
|
-
const authDeps = deriveAuthDependencies(skill, targetEndpointId);
|
|
205
|
-
|
|
206
|
-
// Find the target operation by endpoint_id.
|
|
207
|
-
const targetOp = graph.operations.find(
|
|
208
|
-
(op) => op.endpoint_id === targetEndpointId,
|
|
209
|
-
);
|
|
210
|
-
if (!targetOp) {
|
|
211
|
-
return {
|
|
212
|
-
chain_ready: true,
|
|
213
|
-
predicted_next: [],
|
|
214
|
-
prerequisite_order: [],
|
|
215
|
-
skippable: [],
|
|
216
|
-
auth_dependencies: authDeps,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const plan = buildExecutionPlan(graph, targetOp.operation_id, knownSet);
|
|
221
|
-
|
|
222
|
-
// prerequisite_order: steps before the target (excluding target itself).
|
|
223
|
-
const prerequisiteOrder = plan.steps
|
|
224
|
-
.filter((s) => s.operation_id !== targetOp.operation_id)
|
|
225
|
-
.map((s) => s.endpoint_id);
|
|
226
|
-
|
|
227
|
-
// predicted_next: operations that list the target's provides as a
|
|
228
|
-
// required binding and are not in the plan themselves.
|
|
229
|
-
const planOpIds = new Set(plan.steps.map((s) => s.operation_id));
|
|
230
|
-
const targetProvides = new Set(targetOp.provides.map((b) => b.key));
|
|
231
|
-
const predictedNext: string[] = [];
|
|
232
|
-
for (const op of graph.operations) {
|
|
233
|
-
if (planOpIds.has(op.operation_id)) continue;
|
|
234
|
-
const needsTargetOutput = op.requires.some(
|
|
235
|
-
(b) => b.required && targetProvides.has(b.key),
|
|
236
|
-
);
|
|
237
|
-
if (needsTargetOutput) predictedNext.push(op.endpoint_id);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// skippable: unreachable operations (not needed for this target).
|
|
241
|
-
const skippable = plan.unreachable.map((opId) => {
|
|
242
|
-
const op = graph.operations.find((o) => o.operation_id === opId);
|
|
243
|
-
return op?.endpoint_id ?? opId;
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
chain_ready: plan.chain_ready,
|
|
248
|
-
predicted_next: predictedNext,
|
|
249
|
-
prerequisite_order: prerequisiteOrder,
|
|
250
|
-
skippable,
|
|
251
|
-
auth_dependencies: authDeps,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ---------------------------------------------------------------------------
|
|
256
|
-
// 3. applyDagAdvisoryBoosts
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Boost/penalize ranked endpoints based on the DAG advisory plan.
|
|
261
|
-
*
|
|
262
|
-
* - Endpoints on the critical path (`prerequisite_order`) get a boost.
|
|
263
|
-
* - Endpoints in `skippable` get a penalty.
|
|
264
|
-
* - Endpoints in `predicted_next` get a small boost (good follow-ups).
|
|
265
|
-
*
|
|
266
|
-
* Returns a new array, sorted by adjusted score descending.
|
|
267
|
-
*/
|
|
268
|
-
export function applyDagAdvisoryBoosts<
|
|
269
|
-
T extends { endpoint_id: string; score: number },
|
|
270
|
-
>(
|
|
271
|
-
ranked: T[],
|
|
272
|
-
dagPlan: DagAdvisoryPlan,
|
|
273
|
-
): T[] {
|
|
274
|
-
const prerequisiteSet = new Set(dagPlan.prerequisite_order);
|
|
275
|
-
const skippableSet = new Set(dagPlan.skippable);
|
|
276
|
-
const predictedNextSet = new Set(dagPlan.predicted_next);
|
|
277
|
-
|
|
278
|
-
// Check session negatives: don't boost endpoints that have failed.
|
|
279
|
-
const negativeEndpoints = new Set<string>();
|
|
280
|
-
for (const [, negatives] of sessionNegatives) {
|
|
281
|
-
for (const epId of negatives) {
|
|
282
|
-
negativeEndpoints.add(epId);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const boosted = ranked.map((item) => {
|
|
287
|
-
let adjustment = 0;
|
|
288
|
-
|
|
289
|
-
// Don't boost endpoints that have failed in this session.
|
|
290
|
-
if (negativeEndpoints.has(item.endpoint_id)) {
|
|
291
|
-
return { ...item, score: item.score - 0.1 };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (prerequisiteSet.has(item.endpoint_id)) {
|
|
295
|
-
adjustment += 0.15; // Critical path boost
|
|
296
|
-
}
|
|
297
|
-
if (predictedNextSet.has(item.endpoint_id)) {
|
|
298
|
-
adjustment += 0.05; // Follow-up boost
|
|
299
|
-
}
|
|
300
|
-
if (skippableSet.has(item.endpoint_id)) {
|
|
301
|
-
adjustment -= 0.1; // Not needed penalty
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return { ...item, score: item.score + adjustment };
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
return boosted.sort((a, b) => b.score - a.score);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ---------------------------------------------------------------------------
|
|
311
|
-
// 4. recordDagNegative
|
|
312
|
-
// ---------------------------------------------------------------------------
|
|
313
|
-
|
|
314
|
-
/** Session-level store of failed endpoints, keyed by skill_id. */
|
|
315
|
-
const sessionNegatives = new Map<string, Set<string>>();
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Record that this endpoint failed for this session. Prevents the planner
|
|
319
|
-
* from re-boosting it in `applyDagAdvisoryBoosts`.
|
|
320
|
-
*/
|
|
321
|
-
export function recordDagNegative(
|
|
322
|
-
skill: { skill_id: string },
|
|
323
|
-
endpointId: string,
|
|
324
|
-
): void {
|
|
325
|
-
const existing = sessionNegatives.get(skill.skill_id);
|
|
326
|
-
if (existing) {
|
|
327
|
-
existing.add(endpointId);
|
|
328
|
-
} else {
|
|
329
|
-
sessionNegatives.set(skill.skill_id, new Set([endpointId]));
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Check whether an endpoint has been recorded as failed for a skill.
|
|
335
|
-
* Exposed for testing.
|
|
336
|
-
*/
|
|
337
|
-
export function isDagNegative(
|
|
338
|
-
skill: { skill_id: string },
|
|
339
|
-
endpointId: string,
|
|
340
|
-
): boolean {
|
|
341
|
-
return sessionNegatives.get(skill.skill_id)?.has(endpointId) ?? false;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Clear all session negatives. Exposed for testing.
|
|
346
|
-
*/
|
|
347
|
-
export function clearDagSessionState(): void {
|
|
348
|
-
sessionNegatives.clear();
|
|
349
|
-
sessionTraces.clear();
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// ---------------------------------------------------------------------------
|
|
353
|
-
// 5. recordDagSessionAction
|
|
354
|
-
// ---------------------------------------------------------------------------
|
|
355
|
-
|
|
356
|
-
/** Session-level store of execution traces, keyed by skill_id. */
|
|
357
|
-
const sessionTraces = new Map<
|
|
358
|
-
string,
|
|
359
|
-
Array<{ endpoint_id: string; success: boolean; timestamp: number }>
|
|
360
|
-
>();
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Record that this endpoint was executed in this session. Accumulates an
|
|
364
|
-
* ordered trace of operations per skill.
|
|
365
|
-
*/
|
|
366
|
-
export function recordDagSessionAction(
|
|
367
|
-
skill: { skill_id: string },
|
|
368
|
-
endpointId: string,
|
|
369
|
-
success: boolean,
|
|
370
|
-
): void {
|
|
371
|
-
const existing = sessionTraces.get(skill.skill_id);
|
|
372
|
-
const entry = { endpoint_id: endpointId, success, timestamp: Date.now() };
|
|
373
|
-
if (existing) {
|
|
374
|
-
existing.push(entry);
|
|
375
|
-
} else {
|
|
376
|
-
sessionTraces.set(skill.skill_id, [entry]);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Retrieve the session trace for a skill. Exposed for testing.
|
|
382
|
-
*/
|
|
383
|
-
export function getDagSessionTrace(
|
|
384
|
-
skill: { skill_id: string },
|
|
385
|
-
): Array<{ endpoint_id: string; success: boolean; timestamp: number }> {
|
|
386
|
-
return sessionTraces.get(skill.skill_id) ?? [];
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// ---------------------------------------------------------------------------
|
|
390
|
-
// 6. upsertDagEdgesFromOperationGraph
|
|
391
|
-
// ---------------------------------------------------------------------------
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Merge new edges from a freshly-built graph into the skill manifest's
|
|
395
|
-
* persisted operation_graph. If the skill has no graph yet, assign the
|
|
396
|
-
* provided one directly. Existing edges are preserved (dedup by edge_id),
|
|
397
|
-
* and the generated_at timestamp is updated.
|
|
398
|
-
*/
|
|
399
|
-
export function upsertDagEdgesFromOperationGraph(
|
|
400
|
-
skill: SkillManifest,
|
|
401
|
-
graph: SkillOperationGraph,
|
|
402
|
-
): void {
|
|
403
|
-
if (!skill.operation_graph) { skill.operation_graph = graph; return; }
|
|
404
|
-
const existingEdgeIds = new Set(skill.operation_graph.edges.map(e => e.edge_id));
|
|
405
|
-
for (const edge of graph.edges) {
|
|
406
|
-
if (!existingEdgeIds.has(edge.edge_id)) {
|
|
407
|
-
skill.operation_graph.edges.push(edge);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
skill.operation_graph.generated_at = graph.generated_at;
|
|
411
|
-
}
|