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.
Files changed (64) hide show
  1. package/README.md +8 -44
  2. package/dist/cli.js +514 -20723
  3. package/package.json +4 -10
  4. package/runtime-src/api/routes.ts +15 -801
  5. package/runtime-src/auth/index.ts +32 -142
  6. package/runtime-src/capture/index.ts +101 -436
  7. package/runtime-src/cli.ts +371 -956
  8. package/runtime-src/client/index.ts +29 -622
  9. package/runtime-src/execution/index.ts +85 -345
  10. package/runtime-src/graph/index.ts +10 -128
  11. package/runtime-src/intent-match.ts +27 -27
  12. package/runtime-src/kuri/client.ts +82 -543
  13. package/runtime-src/orchestrator/index.ts +462 -2246
  14. package/runtime-src/reverse-engineer/index.ts +22 -220
  15. package/runtime-src/runtime/local-server.ts +16 -149
  16. package/runtime-src/runtime/paths.ts +5 -9
  17. package/runtime-src/runtime/setup.ts +1 -52
  18. package/runtime-src/server.ts +11 -6
  19. package/runtime-src/transform/schema-hints.ts +358 -0
  20. package/runtime-src/types/skill.ts +2 -49
  21. package/runtime-src/verification/index.ts +0 -15
  22. package/runtime-src/version.ts +13 -13
  23. package/vendor/kuri/darwin-arm64/kuri +0 -0
  24. package/vendor/kuri/darwin-x64/kuri +0 -0
  25. package/vendor/kuri/linux-arm64/kuri +0 -0
  26. package/vendor/kuri/linux-x64/kuri +0 -0
  27. package/bin/unbrowse-wrapper.mjs +0 -39
  28. package/bin/unbrowse.js +0 -38
  29. package/runtime-src/analytics-session.ts +0 -33
  30. package/runtime-src/api/browse-index.ts +0 -254
  31. package/runtime-src/api/browse-session.ts +0 -179
  32. package/runtime-src/api/browse-submit.ts +0 -455
  33. package/runtime-src/auth/runtime.ts +0 -116
  34. package/runtime-src/browser/index.ts +0 -635
  35. package/runtime-src/browser/types.ts +0 -41
  36. package/runtime-src/capture/prefetch.ts +0 -122
  37. package/runtime-src/capture/rsc.ts +0 -45
  38. package/runtime-src/cli/shortcuts.ts +0 -273
  39. package/runtime-src/client/graph-client.ts +0 -99
  40. package/runtime-src/execution/robots.ts +0 -167
  41. package/runtime-src/execution/search-forms.ts +0 -188
  42. package/runtime-src/graph/planner.ts +0 -411
  43. package/runtime-src/graph/session.ts +0 -294
  44. package/runtime-src/graph/trace-store.ts +0 -136
  45. package/runtime-src/indexer/index.ts +0 -480
  46. package/runtime-src/orchestrator/browser-agent.ts +0 -374
  47. package/runtime-src/orchestrator/dag-advisor.ts +0 -59
  48. package/runtime-src/orchestrator/dag-feedback.ts +0 -256
  49. package/runtime-src/orchestrator/first-pass-action.ts +0 -362
  50. package/runtime-src/orchestrator/passive-publish.ts +0 -152
  51. package/runtime-src/orchestrator/timing-economics.ts +0 -80
  52. package/runtime-src/payments/cascade.ts +0 -137
  53. package/runtime-src/payments/index.ts +0 -268
  54. package/runtime-src/payments/wallet.ts +0 -33
  55. package/runtime-src/reverse-engineer/description-prompt.ts +0 -132
  56. package/runtime-src/router.ts +0 -17
  57. package/runtime-src/runtime/browser-access.ts +0 -11
  58. package/runtime-src/runtime/browser-host.ts +0 -48
  59. package/runtime-src/runtime/lifecycle.ts +0 -17
  60. package/runtime-src/runtime/supervisor.ts +0 -69
  61. package/runtime-src/single-binary.ts +0 -141
  62. package/runtime-src/telemetry.ts +0 -253
  63. package/runtime-src/verification/matrix.ts +0 -30
  64. 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
- }