taskflow-core 0.1.5 → 0.1.6
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 +115 -25
- package/dist/agents.d.ts +20 -0
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +51 -2
- package/dist/agents.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/jsonc.d.ts +30 -0
- package/dist/jsonc.d.ts.map +1 -0
- package/dist/jsonc.js +108 -0
- package/dist/jsonc.js.map +1 -0
- package/dist/library/meta.d.ts +31 -0
- package/dist/library/meta.d.ts.map +1 -0
- package/dist/library/meta.js +154 -0
- package/dist/library/meta.js.map +1 -0
- package/dist/library/search.d.ts +32 -0
- package/dist/library/search.d.ts.map +1 -0
- package/dist/library/search.js +298 -0
- package/dist/library/search.js.map +1 -0
- package/dist/library/types.d.ts +121 -0
- package/dist/library/types.d.ts.map +1 -0
- package/dist/library/types.js +18 -0
- package/dist/library/types.js.map +1 -0
- package/dist/runner-core.d.ts +54 -4
- package/dist/runner-core.d.ts.map +1 -1
- package/dist/runner-core.js +237 -10
- package/dist/runner-core.js.map +1 -1
- package/dist/runtime.js +1 -1
- package/dist/runtime.js.map +1 -1
- package/dist/store.d.ts +32 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +129 -1
- package/dist/store.js.map +1 -1
- package/package.json +6 -7
package/dist/jsonc.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency JSONC (JSON with Comments) strip + parse.
|
|
3
|
+
*
|
|
4
|
+
* Flow definition files are hand-authored, and authors want to annotate them
|
|
5
|
+
* with `//` line comments and `/* block */` comments just like jsonc/JSON5.
|
|
6
|
+
* Standard `JSON.parse` rejects comments outright. This module strips comments
|
|
7
|
+
* (only those OUTSIDE string literals) and trailing commas before parsing, so
|
|
8
|
+
* a flow `.json` file may legally contain comments.
|
|
9
|
+
*
|
|
10
|
+
* Scope: this is used ONLY for user-authored flow definition files
|
|
11
|
+
* (readFlowFile). It must NOT be used to parse LLM/subagent output — that path
|
|
12
|
+
* (safeParse) stays strict by design, since model output is untrusted and we
|
|
13
|
+
* want structural failures to surface.
|
|
14
|
+
*
|
|
15
|
+
* The algorithm is a single linear scan that tracks whether the cursor is
|
|
16
|
+
* inside a string literal (respecting `\"` escapes). Comments are replaced
|
|
17
|
+
* with spaces so that line/column numbers in any error message stay aligned
|
|
18
|
+
* with the original source. Trailing commas before a closing brace/bracket
|
|
19
|
+
* (like `{"a": 1,}` or `[1, 2,]`) are also removed — a common hand-editing
|
|
20
|
+
* convenience that JSON.parse rejects.
|
|
21
|
+
*/
|
|
22
|
+
/** Strip JSONC comments (// line and block) and trailing commas. Exported for testing. */
|
|
23
|
+
export function stripJsonComments(input) {
|
|
24
|
+
let out = "";
|
|
25
|
+
let i = 0;
|
|
26
|
+
const n = input.length;
|
|
27
|
+
while (i < n) {
|
|
28
|
+
const ch = input[i];
|
|
29
|
+
const next = input[i + 1];
|
|
30
|
+
// String literal — copy verbatim, honoring escape sequences. A `//` or
|
|
31
|
+
// `/*` that appears inside a string is part of the value, not a comment,
|
|
32
|
+
// and a `,` inside a string is not a trailing comma.
|
|
33
|
+
if (ch === '"') {
|
|
34
|
+
out += ch;
|
|
35
|
+
i++;
|
|
36
|
+
while (i < n) {
|
|
37
|
+
const c = input[i];
|
|
38
|
+
if (c === "\\" && i + 1 < n) {
|
|
39
|
+
// Keep the escape char and the escaped char together.
|
|
40
|
+
out += c + input[i + 1];
|
|
41
|
+
i += 2;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
out += c;
|
|
45
|
+
i++;
|
|
46
|
+
if (c === '"')
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Line comment `// ... \n` — drop it (do not emit a space, so line
|
|
52
|
+
// numbers stay aligned with the original source).
|
|
53
|
+
if (ch === "/" && next === "/") {
|
|
54
|
+
while (i < n && input[i] !== "\n")
|
|
55
|
+
i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
// Block comment `/* ... */` — replace with a single space so that
|
|
59
|
+
// positions of the surrounding tokens are preserved.
|
|
60
|
+
if (ch === "/" && next === "*") {
|
|
61
|
+
i += 2;
|
|
62
|
+
while (i < n && !(input[i] === "*" && input[i + 1] === "/"))
|
|
63
|
+
i++;
|
|
64
|
+
i += 2; // consume the closing */
|
|
65
|
+
out += " ";
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Trailing comma: skip the comma when the next non-whitespace char is
|
|
69
|
+
// a closing brace or bracket. Only checked outside string literals.
|
|
70
|
+
// Newlines between the comma and the closer are preserved so that
|
|
71
|
+
// line numbers stay aligned with the original source.
|
|
72
|
+
if (ch === ",") {
|
|
73
|
+
let j = i + 1;
|
|
74
|
+
let sawNewline = false;
|
|
75
|
+
while (j < n) {
|
|
76
|
+
const c = input[j];
|
|
77
|
+
if (c === " " || c === "\t" || c === "\r") {
|
|
78
|
+
j++;
|
|
79
|
+
}
|
|
80
|
+
else if (c === "\n") {
|
|
81
|
+
sawNewline = true;
|
|
82
|
+
j++;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (j < n && (input[j] === "}" || input[j] === "]")) {
|
|
89
|
+
if (sawNewline)
|
|
90
|
+
out += "\n";
|
|
91
|
+
i = j;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
out += ch;
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Parse a JSONC string (JSON with comments and trailing commas allowed) into a
|
|
102
|
+
* value of type T. Throws the same SyntaxError `JSON.parse` would throw for any
|
|
103
|
+
* other structural problem, so callers can wrap it in try/catch as usual.
|
|
104
|
+
*/
|
|
105
|
+
export function parseJsonc(input) {
|
|
106
|
+
return JSON.parse(stripJsonComments(input));
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=jsonc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonc.js","sourceRoot":"","sources":["../src/jsonc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library — auto-derived structural metadata.
|
|
3
|
+
*
|
|
4
|
+
* Computed at save time (zero tokens, deterministic) and re-derived live at
|
|
5
|
+
* search time (RFC §5.2.2 staleness). No embedding logic here — that's Phase 2.
|
|
6
|
+
*
|
|
7
|
+
* Refs: docs/rfc-library-reuse.md §3.2 (phaseSignature), §3.3 (generality).
|
|
8
|
+
*/
|
|
9
|
+
import type { Taskflow } from "../schema.ts";
|
|
10
|
+
import type { FlowMeta } from "./types.ts";
|
|
11
|
+
/** Topological phase-type signature, e.g. "agent→map→gate→reduce".
|
|
12
|
+
* Parallel same-layer phases joined with "+". Uses topoLayers() for
|
|
13
|
+
* deterministic ordering. */
|
|
14
|
+
export declare function computePhaseSignature(def: Taskflow): string;
|
|
15
|
+
export declare function countPhases(def: Taskflow): number;
|
|
16
|
+
/** Distinct agent names referenced across phases (sorted, stable). */
|
|
17
|
+
export declare function extractAgentUsage(def: Taskflow): string[];
|
|
18
|
+
/** generality ∈ [0,1]. RFC §3.3 (v2 formula, A8 fix):
|
|
19
|
+
* uses literalTokenRatio = literalChars/(literalChars+placeholderChars)
|
|
20
|
+
* so verbose-but-parameterized flows aren't structurally penalized. */
|
|
21
|
+
export declare function computeGenerality(def: Taskflow): number;
|
|
22
|
+
/** Build a fresh FlowMeta from a def + optional agent-provided fields.
|
|
23
|
+
* Used at save time. reuseCount starts at 0; version starts at 1. */
|
|
24
|
+
export declare function deriveMeta(def: Taskflow, opts: {
|
|
25
|
+
purpose?: string;
|
|
26
|
+
tags?: string[];
|
|
27
|
+
notes?: string;
|
|
28
|
+
derivedFrom?: string;
|
|
29
|
+
prevMeta?: FlowMeta;
|
|
30
|
+
}): FlowMeta;
|
|
31
|
+
//# sourceMappingURL=meta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.d.ts","sourceRoot":"","sources":["../../src/library/meta.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;8BAE8B;AAC9B,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAM3D;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAEjD;AAED,sEAAsE;AACtE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,EAAE,CAazD;AAkFD;;wEAEwE;AACxE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAYvD;AAED;sEACsE;AACtE,wBAAgB,UAAU,CACzB,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAAE,GACpG,QAAQ,CAsBV"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library — auto-derived structural metadata.
|
|
3
|
+
*
|
|
4
|
+
* Computed at save time (zero tokens, deterministic) and re-derived live at
|
|
5
|
+
* search time (RFC §5.2.2 staleness). No embedding logic here — that's Phase 2.
|
|
6
|
+
*
|
|
7
|
+
* Refs: docs/rfc-library-reuse.md §3.2 (phaseSignature), §3.3 (generality).
|
|
8
|
+
*/
|
|
9
|
+
import { topoLayers } from "../schema.js";
|
|
10
|
+
/** Topological phase-type signature, e.g. "agent→map→gate→reduce".
|
|
11
|
+
* Parallel same-layer phases joined with "+". Uses topoLayers() for
|
|
12
|
+
* deterministic ordering. */
|
|
13
|
+
export function computePhaseSignature(def) {
|
|
14
|
+
if (!Array.isArray(def.phases) || def.phases.length === 0)
|
|
15
|
+
return "";
|
|
16
|
+
const layers = topoLayers(def.phases);
|
|
17
|
+
return layers
|
|
18
|
+
.map((layer) => layer.map((p) => p.type ?? "agent").join("+"))
|
|
19
|
+
.join("→");
|
|
20
|
+
}
|
|
21
|
+
export function countPhases(def) {
|
|
22
|
+
return Array.isArray(def.phases) ? def.phases.length : 0;
|
|
23
|
+
}
|
|
24
|
+
/** Distinct agent names referenced across phases (sorted, stable). */
|
|
25
|
+
export function extractAgentUsage(def) {
|
|
26
|
+
if (!Array.isArray(def.phases))
|
|
27
|
+
return [];
|
|
28
|
+
const set = new Set();
|
|
29
|
+
for (const p of def.phases) {
|
|
30
|
+
if (typeof p.agent === "string" && p.agent.trim())
|
|
31
|
+
set.add(p.agent.trim());
|
|
32
|
+
// parallel branches
|
|
33
|
+
if (Array.isArray(p.branches)) {
|
|
34
|
+
for (const b of p.branches ?? []) {
|
|
35
|
+
if (b && typeof b.agent === "string" && b.agent.trim())
|
|
36
|
+
set.add(b.agent.trim());
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return Array.from(set).sort();
|
|
41
|
+
}
|
|
42
|
+
const PLACEHOLDER_RE = /\{(args|steps|item|previous|loop|reflexion)(\.[^}]{1,200})?\}/g;
|
|
43
|
+
function accountContent(def) {
|
|
44
|
+
const acc = {
|
|
45
|
+
literalChars: 0,
|
|
46
|
+
placeholderChars: 0,
|
|
47
|
+
placeholderRefs: 0,
|
|
48
|
+
argCount: 0,
|
|
49
|
+
hasDescription: typeof def.description === "string" && def.description.trim().length > 0,
|
|
50
|
+
productionKnobs: 0,
|
|
51
|
+
};
|
|
52
|
+
// args
|
|
53
|
+
if (def.args && typeof def.args === "object") {
|
|
54
|
+
acc.argCount = Object.keys(def.args).length;
|
|
55
|
+
}
|
|
56
|
+
// production knobs (cap 0.15, each +0.05)
|
|
57
|
+
if (def.budget)
|
|
58
|
+
acc.productionKnobs += 0.05;
|
|
59
|
+
if (def.concurrency && typeof def.concurrency === "number")
|
|
60
|
+
acc.productionKnobs += 0.05;
|
|
61
|
+
let sawRetry = false;
|
|
62
|
+
let sawExpect = false;
|
|
63
|
+
if (Array.isArray(def.phases)) {
|
|
64
|
+
for (const p of def.phases) {
|
|
65
|
+
if (p.retry)
|
|
66
|
+
sawRetry = true;
|
|
67
|
+
if (p.expect)
|
|
68
|
+
sawExpect = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (sawRetry)
|
|
72
|
+
acc.productionKnobs += 0.05;
|
|
73
|
+
if (sawExpect)
|
|
74
|
+
acc.productionKnobs += 0.05;
|
|
75
|
+
acc.productionKnobs = Math.min(0.15, acc.productionKnobs);
|
|
76
|
+
// scan phase string fields for literals vs placeholders
|
|
77
|
+
const scan = (s) => {
|
|
78
|
+
if (typeof s !== "string")
|
|
79
|
+
return;
|
|
80
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
81
|
+
let m;
|
|
82
|
+
let lastEnd = 0;
|
|
83
|
+
let placeholderCharsThis = 0;
|
|
84
|
+
let placeholderRefsThis = 0;
|
|
85
|
+
while ((m = PLACEHOLDER_RE.exec(s)) !== null) {
|
|
86
|
+
// literal chunk before this placeholder
|
|
87
|
+
acc.literalChars += m.index - lastEnd;
|
|
88
|
+
placeholderCharsThis += m[0].length;
|
|
89
|
+
placeholderRefsThis += 1;
|
|
90
|
+
lastEnd = m.index + m[0].length;
|
|
91
|
+
}
|
|
92
|
+
acc.literalChars += s.length - lastEnd;
|
|
93
|
+
acc.placeholderChars += placeholderCharsThis;
|
|
94
|
+
acc.placeholderRefs += placeholderRefsThis;
|
|
95
|
+
};
|
|
96
|
+
if (Array.isArray(def.phases)) {
|
|
97
|
+
for (const p of def.phases) {
|
|
98
|
+
scan(p.task);
|
|
99
|
+
scan(p.over);
|
|
100
|
+
scan(p.run);
|
|
101
|
+
scan(p.input);
|
|
102
|
+
// parallel branch tasks
|
|
103
|
+
if (Array.isArray(p.branches)) {
|
|
104
|
+
for (const b of p.branches ?? []) {
|
|
105
|
+
scan(b?.task);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return acc;
|
|
111
|
+
}
|
|
112
|
+
/** generality ∈ [0,1]. RFC §3.3 (v2 formula, A8 fix):
|
|
113
|
+
* uses literalTokenRatio = literalChars/(literalChars+placeholderChars)
|
|
114
|
+
* so verbose-but-parameterized flows aren't structurally penalized. */
|
|
115
|
+
export function computeGenerality(def) {
|
|
116
|
+
const a = accountContent(def);
|
|
117
|
+
const totalChars = a.literalChars + a.placeholderChars;
|
|
118
|
+
const literalTokenRatio = totalChars > 0 ? a.literalChars / totalChars : 1;
|
|
119
|
+
let g = 0.4 * (1 - literalTokenRatio) +
|
|
120
|
+
0.3 * Math.min(1, a.argCount / 3) +
|
|
121
|
+
0.3 * (a.hasDescription ? 0.3 : 0) +
|
|
122
|
+
a.productionKnobs;
|
|
123
|
+
if (g < 0)
|
|
124
|
+
g = 0;
|
|
125
|
+
if (g > 1)
|
|
126
|
+
g = 1;
|
|
127
|
+
return Math.round(g * 100) / 100;
|
|
128
|
+
}
|
|
129
|
+
/** Build a fresh FlowMeta from a def + optional agent-provided fields.
|
|
130
|
+
* Used at save time. reuseCount starts at 0; version starts at 1. */
|
|
131
|
+
export function deriveMeta(def, opts) {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const prev = opts.prevMeta;
|
|
134
|
+
return {
|
|
135
|
+
schemaVersion: 1,
|
|
136
|
+
purpose: opts.purpose,
|
|
137
|
+
tags: opts.tags,
|
|
138
|
+
notes: opts.notes,
|
|
139
|
+
phaseSignature: computePhaseSignature(def),
|
|
140
|
+
phaseCount: countPhases(def),
|
|
141
|
+
agentUsage: extractAgentUsage(def),
|
|
142
|
+
generality: computeGenerality(def),
|
|
143
|
+
reuseCount: prev?.reuseCount ?? 0,
|
|
144
|
+
lastUsedAt: prev?.lastUsedAt ?? null,
|
|
145
|
+
createdAt: prev?.createdAt ?? now,
|
|
146
|
+
version: prev ? prev.version + 1 : 1,
|
|
147
|
+
derivedFrom: opts.derivedFrom,
|
|
148
|
+
embedding: prev?.embedding ?? null,
|
|
149
|
+
embeddingModel: prev?.embeddingModel,
|
|
150
|
+
embeddingDim: prev?.embeddingDim,
|
|
151
|
+
embeddedAt: prev?.embeddedAt ?? null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","sourceRoot":"","sources":["../../src/library/meta.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library — search ranking (structural + keyword; Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* Phase 2 adds the embedding/cosine path; the seam (LibraryDeps.embedder) is
|
|
5
|
+
* already in place but left unused here. When no embedder is configured OR
|
|
6
|
+
* structureOnly is set, search degrades to keyword + structural ranking.
|
|
7
|
+
*
|
|
8
|
+
* Refs: docs/rfc-library-reuse.md §5.2 (blend), §5.2.1 (structScore),
|
|
9
|
+
* §5.2.2 (staleness), §5.3 (why/reuseHint templates).
|
|
10
|
+
*/
|
|
11
|
+
import { getFlow, readMeta } from "../store.ts";
|
|
12
|
+
import type { Taskflow } from "../schema.ts";
|
|
13
|
+
import type { LibraryDeps, ResolvedCandidate, SearchInput, SearchResponse } from "./types.ts";
|
|
14
|
+
/** Standard Levenshtein edit distance (insert/delete/replace cost 1).
|
|
15
|
+
* O(m·n) DP; signature strings are short (<50 chars) so cost is negligible. */
|
|
16
|
+
export declare function levenshtein(a: string, b: string): number;
|
|
17
|
+
/** Resolve a saved flow + its sidecar into a candidate whose structural fields
|
|
18
|
+
* are ALWAYS freshly derived from the current def, and whose embedding is only
|
|
19
|
+
* trusted when the sidecar's phaseSignature matches the fresh one. */
|
|
20
|
+
export declare function resolveCandidate(def: Taskflow, scope: "user" | "project", name: string, sidecar: ReturnType<typeof readMeta>): ResolvedCandidate;
|
|
21
|
+
/** Search the library. Returns ranked top-N. Degrades to structural/keyword
|
|
22
|
+
* when no embedder is configured or structureOnly is requested. */
|
|
23
|
+
export declare function searchLibrary(deps: LibraryDeps, input: SearchInput): Promise<SearchResponse>;
|
|
24
|
+
/** Pure cosine similarity. Zero-dep. Accepts non-normalized vectors. */
|
|
25
|
+
export declare function cosine(a: number[], b: number[]): number;
|
|
26
|
+
/** Bump reuseCount for a flow by name (RFC §6 reuse flywheel).
|
|
27
|
+
* Phase 1: structural-only; the caller (tool handler) decides whether to
|
|
28
|
+
* bump based on a `reusedFromSearch` flag. Returns the new count or null if
|
|
29
|
+
* the flow has no sidecar yet. The actual sidecar write is done by
|
|
30
|
+
* `bumpReuseInSidecar` in store.ts under withLock. */
|
|
31
|
+
export { getFlow };
|
|
32
|
+
//# sourceMappingURL=search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/library/search.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAa,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAO7C,OAAO,KAAK,EACX,WAAW,EACX,iBAAiB,EACjB,WAAW,EACX,cAAc,EAEd,MAAM,YAAY,CAAC;AAMpB;gFACgF;AAChF,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAmBxD;AAMD;;uEAEuE;AACvE,wBAAgB,gBAAgB,CAC/B,GAAG,EAAE,QAAQ,EACb,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,GAClC,iBAAiB,CAwCnB;AAiHD;oEACoE;AACpE,wBAAsB,aAAa,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CAoFlG;AAED,wEAAwE;AACxE,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAgBvD;AAED;;;;uDAIuD;AACvD,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library — search ranking (structural + keyword; Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* Phase 2 adds the embedding/cosine path; the seam (LibraryDeps.embedder) is
|
|
5
|
+
* already in place but left unused here. When no embedder is configured OR
|
|
6
|
+
* structureOnly is set, search degrades to keyword + structural ranking.
|
|
7
|
+
*
|
|
8
|
+
* Refs: docs/rfc-library-reuse.md §5.2 (blend), §5.2.1 (structScore),
|
|
9
|
+
* §5.2.2 (staleness), §5.3 (why/reuseHint templates).
|
|
10
|
+
*/
|
|
11
|
+
import { getFlow, listFlows, readMeta } from "../store.js";
|
|
12
|
+
import { computeGenerality, computePhaseSignature, countPhases, extractAgentUsage, } from "./meta.js";
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Levenshtein (RFC §5.2.1)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/** Standard Levenshtein edit distance (insert/delete/replace cost 1).
|
|
17
|
+
* O(m·n) DP; signature strings are short (<50 chars) so cost is negligible. */
|
|
18
|
+
export function levenshtein(a, b) {
|
|
19
|
+
if (a === b)
|
|
20
|
+
return 0;
|
|
21
|
+
if (a.length === 0)
|
|
22
|
+
return b.length;
|
|
23
|
+
if (b.length === 0)
|
|
24
|
+
return a.length;
|
|
25
|
+
const m = a.length;
|
|
26
|
+
const n = b.length;
|
|
27
|
+
// Two rolling rows; allocate fresh each call (signatures are tiny).
|
|
28
|
+
let prev = new Array(n + 1);
|
|
29
|
+
for (let j = 0; j <= n; j++)
|
|
30
|
+
prev[j] = j;
|
|
31
|
+
for (let i = 1; i <= m; i++) {
|
|
32
|
+
const curr = new Array(n + 1);
|
|
33
|
+
curr[0] = i;
|
|
34
|
+
for (let j = 1; j <= n; j++) {
|
|
35
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
36
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
37
|
+
}
|
|
38
|
+
prev = curr;
|
|
39
|
+
}
|
|
40
|
+
return prev[n];
|
|
41
|
+
}
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Candidate resolution (RFC §5.2.2 staleness detection)
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
/** Resolve a saved flow + its sidecar into a candidate whose structural fields
|
|
46
|
+
* are ALWAYS freshly derived from the current def, and whose embedding is only
|
|
47
|
+
* trusted when the sidecar's phaseSignature matches the fresh one. */
|
|
48
|
+
export function resolveCandidate(def, scope, name, sidecar) {
|
|
49
|
+
const freshSig = computePhaseSignature(def);
|
|
50
|
+
const freshGen = computeGenerality(def);
|
|
51
|
+
const freshAgents = extractAgentUsage(def);
|
|
52
|
+
const freshCount = countPhases(def);
|
|
53
|
+
if (!sidecar) {
|
|
54
|
+
return {
|
|
55
|
+
name,
|
|
56
|
+
scope,
|
|
57
|
+
def,
|
|
58
|
+
phaseSignature: freshSig,
|
|
59
|
+
phaseCount: freshCount,
|
|
60
|
+
agentUsage: freshAgents,
|
|
61
|
+
generality: freshGen,
|
|
62
|
+
reuseCount: 0,
|
|
63
|
+
version: 1,
|
|
64
|
+
embedding: null,
|
|
65
|
+
embeddingStale: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const sigMatch = sidecar.phaseSignature === freshSig;
|
|
69
|
+
return {
|
|
70
|
+
name,
|
|
71
|
+
scope,
|
|
72
|
+
def,
|
|
73
|
+
phaseSignature: freshSig,
|
|
74
|
+
phaseCount: freshCount,
|
|
75
|
+
agentUsage: freshAgents,
|
|
76
|
+
generality: freshGen,
|
|
77
|
+
purpose: sidecar.purpose,
|
|
78
|
+
tags: sidecar.tags,
|
|
79
|
+
notes: sidecar.notes,
|
|
80
|
+
reuseCount: sidecar.reuseCount ?? 0,
|
|
81
|
+
version: sidecar.version ?? 1,
|
|
82
|
+
derivedFrom: sidecar.derivedFrom,
|
|
83
|
+
embedding: sigMatch ? sidecar.embedding ?? null : null,
|
|
84
|
+
embeddingStale: !sigMatch,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Scoring
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
/** structScore ∈ [0,1]. RFC §5.2.1: 0.7*sigSim + 0.3*countPenalty. */
|
|
91
|
+
function computeStructScore(query, cand) {
|
|
92
|
+
let sigSim = 0;
|
|
93
|
+
if (cand.phaseSignature && query.phaseSignature) {
|
|
94
|
+
const d = levenshtein(query.phaseSignature, cand.phaseSignature);
|
|
95
|
+
const denom = Math.max(query.phaseSignature.length, cand.phaseSignature.length, 1);
|
|
96
|
+
sigSim = 1 - d / denom;
|
|
97
|
+
if (sigSim < 0)
|
|
98
|
+
sigSim = 0;
|
|
99
|
+
}
|
|
100
|
+
let countPenalty = 0;
|
|
101
|
+
if (query.phaseCount > 0 && cand.phaseCount > 0) {
|
|
102
|
+
const diff = Math.abs(query.phaseCount - cand.phaseCount);
|
|
103
|
+
countPenalty = 1 - Math.min(1, diff / Math.max(query.phaseCount, cand.phaseCount));
|
|
104
|
+
}
|
|
105
|
+
return 0.7 * sigSim + 0.3 * countPenalty;
|
|
106
|
+
}
|
|
107
|
+
/** textScore ∈ [0,1]. Token-overlap between query and the flow's text fields
|
|
108
|
+
* (name + purpose + tags + phase task text). CJK-aware: queries tokenized on
|
|
109
|
+
* whitespace AND matched as substrings, so a Chinese query token like "鉴权"
|
|
110
|
+
* still matches a purpose containing "...缺少鉴权检查..." (which whitespace
|
|
111
|
+
* alone would never split out). */
|
|
112
|
+
function computeTextScore(queryTokens, cand) {
|
|
113
|
+
if (queryTokens.length === 0)
|
|
114
|
+
return 0;
|
|
115
|
+
const fieldText = ((cand.name ?? "") +
|
|
116
|
+
" " +
|
|
117
|
+
(cand.purpose ?? "") +
|
|
118
|
+
" " +
|
|
119
|
+
(cand.tags ?? []).join(" ") +
|
|
120
|
+
" " +
|
|
121
|
+
cand.phaseTaskText).toLowerCase();
|
|
122
|
+
if (!fieldText.trim())
|
|
123
|
+
return 0;
|
|
124
|
+
// Substring match per query token (CJK-friendly). Longer tokens weigh more
|
|
125
|
+
// (they carry more signal) so e.g. a 2-char Chinese term counts more than a
|
|
126
|
+
// 2-char English one only marginally; this keeps the [0,1] shape intuitive.
|
|
127
|
+
let hits = 0;
|
|
128
|
+
for (const t of queryTokens) {
|
|
129
|
+
if (!t)
|
|
130
|
+
continue;
|
|
131
|
+
if (fieldText.includes(t))
|
|
132
|
+
hits++;
|
|
133
|
+
}
|
|
134
|
+
return hits / queryTokens.length;
|
|
135
|
+
}
|
|
136
|
+
function tokenize(query) {
|
|
137
|
+
return query
|
|
138
|
+
.toLowerCase()
|
|
139
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
140
|
+
.filter((t) => t.length > 1);
|
|
141
|
+
}
|
|
142
|
+
function phaseTaskText(def) {
|
|
143
|
+
if (!Array.isArray(def.phases))
|
|
144
|
+
return "";
|
|
145
|
+
const parts = [];
|
|
146
|
+
for (const p of def.phases) {
|
|
147
|
+
if (typeof p.task === "string")
|
|
148
|
+
parts.push(p.task);
|
|
149
|
+
if (Array.isArray(p.branches)) {
|
|
150
|
+
for (const b of p.branches ?? []) {
|
|
151
|
+
if (b?.task)
|
|
152
|
+
parts.push(b.task);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return parts.join(" ");
|
|
157
|
+
}
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// why / reuseHint templates (RFC §5.3, Tier 1, zero-token)
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
function buildWhy(s) {
|
|
162
|
+
const hits = [];
|
|
163
|
+
if ((s.searchMode === "semantic" || s.searchMode === "mixed") && s.semScore != null && s.semScore > 0.7) {
|
|
164
|
+
hits.push(`语义命中(sem=${s.semScore.toFixed(2)})`);
|
|
165
|
+
}
|
|
166
|
+
if (s.structScore > 0.8)
|
|
167
|
+
hits.push(`结构一致(struct=${s.structScore.toFixed(2)})`);
|
|
168
|
+
if (s.textScore > 0.3)
|
|
169
|
+
hits.push(`关键词匹配(text=${s.textScore.toFixed(2)})`);
|
|
170
|
+
return hits.length > 0 ? hits.join(" + ") : "低分命中,建议检查";
|
|
171
|
+
}
|
|
172
|
+
function buildReuseHint(r) {
|
|
173
|
+
if (r.score >= 0.8)
|
|
174
|
+
return `直接复用${r.argCount > 0 ? `,注意 ${r.argCount} 个 args 参数` : ""}`;
|
|
175
|
+
if (r.score >= 0.5)
|
|
176
|
+
return `结构相似,建议 copy + 泛化后使用`;
|
|
177
|
+
return `低相关度,建议从头编写或大幅改写`;
|
|
178
|
+
}
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// searchLibrary
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
function argCountOf(def) {
|
|
183
|
+
return def.args && typeof def.args === "object" ? Object.keys(def.args).length : 0;
|
|
184
|
+
}
|
|
185
|
+
/** Search the library. Returns ranked top-N. Degrades to structural/keyword
|
|
186
|
+
* when no embedder is configured or structureOnly is requested. */
|
|
187
|
+
export async function searchLibrary(deps, input) {
|
|
188
|
+
const limit = Math.min(20, Math.max(1, input.limit ?? 5));
|
|
189
|
+
const minScore = input.minScore ?? 0;
|
|
190
|
+
const scope = input.scope ?? deps.settings.scope ?? "both";
|
|
191
|
+
// gather candidates
|
|
192
|
+
const flows = listFlows(deps.cwd).filter((f) => {
|
|
193
|
+
if (scope === "both")
|
|
194
|
+
return true;
|
|
195
|
+
return f.scope === scope;
|
|
196
|
+
});
|
|
197
|
+
const queryTokens = tokenize(input.query);
|
|
198
|
+
// embed the query if an embedder is available and not structureOnly
|
|
199
|
+
let queryVec = null;
|
|
200
|
+
const hasEmbedder = !!deps.embedder && !input.structureOnly;
|
|
201
|
+
if (hasEmbedder && deps.embedder) {
|
|
202
|
+
try {
|
|
203
|
+
queryVec = await deps.embedder.embed(input.query);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
queryVec = null; // degrade
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const scored = [];
|
|
210
|
+
for (const f of flows) {
|
|
211
|
+
const sidecar = readMeta(deps.cwd, f.name);
|
|
212
|
+
const cand = resolveCandidate(f.def, f.scope, f.name, sidecar);
|
|
213
|
+
const qShape = {
|
|
214
|
+
phaseSignature: input.phaseSignatureHint ?? "",
|
|
215
|
+
phaseCount: input.phaseCountHint ?? 0,
|
|
216
|
+
};
|
|
217
|
+
const structScore = computeStructScore(qShape, cand);
|
|
218
|
+
const textScore = computeTextScore(queryTokens, {
|
|
219
|
+
name: cand.name,
|
|
220
|
+
purpose: cand.purpose,
|
|
221
|
+
tags: cand.tags,
|
|
222
|
+
phaseTaskText: phaseTaskText(cand.def),
|
|
223
|
+
});
|
|
224
|
+
let score;
|
|
225
|
+
let semScore;
|
|
226
|
+
const hasVectors = queryVec != null && cand.embedding != null && !cand.embeddingStale;
|
|
227
|
+
if (hasVectors && queryVec && cand.embedding) {
|
|
228
|
+
semScore = cosine(queryVec, cand.embedding);
|
|
229
|
+
score = 0.6 * semScore + 0.25 * structScore + 0.15 * textScore;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
score = 0.6 * textScore + 0.4 * structScore;
|
|
233
|
+
}
|
|
234
|
+
if (score < minScore)
|
|
235
|
+
continue;
|
|
236
|
+
if (score <= 0)
|
|
237
|
+
continue; // zero-relevance results are noise; don't surface them
|
|
238
|
+
scored.push({ cand, score: Math.round(score * 100) / 100, semScore, structScore, textScore });
|
|
239
|
+
}
|
|
240
|
+
scored.sort((a, b) => b.score - a.score);
|
|
241
|
+
const top = scored.slice(0, limit);
|
|
242
|
+
const withVectors = top.filter((s) => s.cand.embedding != null && !s.cand.embeddingStale).length;
|
|
243
|
+
let searchMode;
|
|
244
|
+
if (input.structureOnly || !deps.embedder || withVectors === 0)
|
|
245
|
+
searchMode = "structural";
|
|
246
|
+
else if (withVectors === top.length)
|
|
247
|
+
searchMode = "semantic";
|
|
248
|
+
else
|
|
249
|
+
searchMode = "mixed";
|
|
250
|
+
const results = top.map((s) => ({
|
|
251
|
+
name: s.cand.name,
|
|
252
|
+
scope: s.cand.scope,
|
|
253
|
+
purpose: s.cand.purpose,
|
|
254
|
+
tags: s.cand.tags,
|
|
255
|
+
phaseSignature: s.cand.phaseSignature,
|
|
256
|
+
phaseCount: s.cand.phaseCount,
|
|
257
|
+
generality: s.cand.generality,
|
|
258
|
+
reuseCount: s.cand.reuseCount,
|
|
259
|
+
version: s.cand.version,
|
|
260
|
+
score: s.score,
|
|
261
|
+
why: buildWhy({ semScore: s.semScore, structScore: s.structScore, textScore: s.textScore, searchMode }),
|
|
262
|
+
reuseHint: buildReuseHint({ score: s.score, argCount: argCountOf(s.cand.def) }),
|
|
263
|
+
}));
|
|
264
|
+
return {
|
|
265
|
+
results,
|
|
266
|
+
searchMode,
|
|
267
|
+
embedder: deps.embedder?.model,
|
|
268
|
+
counts: { scanned: flows.length, withVectors },
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/** Pure cosine similarity. Zero-dep. Accepts non-normalized vectors. */
|
|
272
|
+
export function cosine(a, b) {
|
|
273
|
+
const n = Math.min(a.length, b.length);
|
|
274
|
+
if (n === 0)
|
|
275
|
+
return 0;
|
|
276
|
+
let dot = 0;
|
|
277
|
+
let normA = 0;
|
|
278
|
+
let normB = 0;
|
|
279
|
+
for (let i = 0; i < n; i++) {
|
|
280
|
+
const x = a[i];
|
|
281
|
+
const y = b[i];
|
|
282
|
+
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
283
|
+
continue;
|
|
284
|
+
dot += x * y;
|
|
285
|
+
normA += x * x;
|
|
286
|
+
normB += y * y;
|
|
287
|
+
}
|
|
288
|
+
if (normA === 0 || normB === 0)
|
|
289
|
+
return 0;
|
|
290
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
291
|
+
}
|
|
292
|
+
/** Bump reuseCount for a flow by name (RFC §6 reuse flywheel).
|
|
293
|
+
* Phase 1: structural-only; the caller (tool handler) decides whether to
|
|
294
|
+
* bump based on a `reusedFromSearch` flag. Returns the new count or null if
|
|
295
|
+
* the flow has no sidecar yet. The actual sidecar write is done by
|
|
296
|
+
* `bumpReuseInSidecar` in store.ts under withLock. */
|
|
297
|
+
export { getFlow };
|
|
298
|
+
//# sourceMappingURL=search.js.map
|