@oxygen-agent/cli 1.987.20 → 1.1010.1
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 +1 -1
- package/dist/admin-primary-providers-render.d.ts +0 -2
- package/dist/admin-primary-providers-render.js +1 -1
- package/dist/browser-login.js +1 -4
- package/dist/column-decision-options.d.ts +20 -0
- package/dist/column-decision-options.js +54 -0
- package/dist/command-manifest.d.ts +3 -2
- package/dist/command-manifest.js +25 -2
- package/dist/credentials.d.ts +1 -1
- package/dist/functions-commands.js +13 -9
- package/dist/help.d.ts +8 -0
- package/dist/help.js +46 -0
- package/dist/index.js +2751 -242
- package/dist/knowledge-mirror.d.ts +2 -2
- package/dist/runtime.d.ts +0 -15
- package/dist/runtime.js +1 -1
- package/dist/search-ai-filter-notice.d.ts +17 -0
- package/dist/search-ai-filter-notice.js +38 -0
- package/dist/session.d.ts +4 -3
- package/dist/skills.d.ts +8 -7
- package/dist/skills.js +58 -20
- package/dist/transcript.d.ts +2 -1
- package/dist/util.d.ts +10 -1
- package/dist/util.js +14 -2
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +296 -140
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.d.ts +9 -0
- package/node_modules/@oxygen/cli-ugc/dist/field-parser.js +34 -0
- package/node_modules/@oxygen/formula/dist/coerce.d.ts +10 -0
- package/node_modules/@oxygen/formula/dist/coerce.js +10 -0
- package/node_modules/@oxygen/formula/dist/formula-functions.js +65 -0
- package/node_modules/@oxygen/formula/dist/hash.d.ts +19 -0
- package/node_modules/@oxygen/formula/dist/hash.js +199 -0
- package/node_modules/@oxygen/formula/dist/value-cleaners.d.ts +6 -1
- package/node_modules/@oxygen/formula/dist/value-cleaners.js +10 -26
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/array-utils.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/array-utils.js +11 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +99 -22
- package/node_modules/@oxygen/shared/dist/billing.js +195 -40
- package/node_modules/@oxygen/shared/dist/byok-connect.js +5 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +27 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +311 -28
- package/node_modules/@oxygen/shared/dist/cli-http-error.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/cli-http-error.js +8 -0
- package/node_modules/@oxygen/shared/dist/cli-result.js +1 -0
- package/node_modules/@oxygen/shared/dist/column-autofill.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/column-autofill.js +62 -0
- package/node_modules/@oxygen/shared/dist/column-decision.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/column-decision.js +228 -0
- package/node_modules/@oxygen/shared/dist/column-output-fields.js +2 -6
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +113 -0
- package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +548 -0
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +6 -6
- package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +6 -6
- package/node_modules/@oxygen/shared/dist/cutover-freeze.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/cutover-freeze.js +52 -0
- package/node_modules/@oxygen/shared/dist/data-suppliers.d.ts +57 -0
- package/node_modules/@oxygen/shared/dist/data-suppliers.js +59 -0
- package/node_modules/@oxygen/shared/dist/deploy-env.d.ts +74 -0
- package/node_modules/@oxygen/shared/dist/deploy-env.js +82 -0
- package/node_modules/@oxygen/shared/dist/dnc-rules.d.ts +130 -0
- package/node_modules/@oxygen/shared/dist/dnc-rules.js +221 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +107 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +809 -0
- package/node_modules/@oxygen/shared/dist/error-message.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/error-message.js +3 -0
- package/node_modules/@oxygen/shared/dist/error-redaction.js +1 -3
- package/node_modules/@oxygen/shared/dist/external-write-policy.d.ts +33 -0
- package/node_modules/@oxygen/shared/dist/external-write-policy.js +68 -0
- package/node_modules/@oxygen/shared/dist/format-percent.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/format-percent.js +13 -0
- package/node_modules/@oxygen/shared/dist/freemail-domains.d.ts +81 -0
- package/node_modules/@oxygen/shared/dist/freemail-domains.js +157 -0
- package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/future-signup-lifecycle-projection.js +1 -1
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +60 -4
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +125 -10
- package/node_modules/@oxygen/shared/dist/index.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/index.js +15 -0
- package/node_modules/@oxygen/shared/dist/json-path.js +1 -3
- package/node_modules/@oxygen/shared/dist/knowledge-bases.js +1 -3
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +2 -2
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +2 -2
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +44 -1
- package/node_modules/@oxygen/shared/dist/langfuse.js +416 -14
- package/node_modules/@oxygen/shared/dist/linkedin-countries.d.ts +33 -0
- package/node_modules/@oxygen/shared/dist/linkedin-countries.js +361 -0
- package/node_modules/@oxygen/shared/dist/linkedin-country-timezones.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/linkedin-country-timezones.js +276 -0
- package/node_modules/@oxygen/shared/dist/linkedin-message-deletion.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/linkedin-message-deletion.js +5 -0
- package/node_modules/@oxygen/shared/dist/linkedin-post-keywords.d.ts +44 -0
- package/node_modules/@oxygen/shared/dist/linkedin-post-keywords.js +116 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +96 -0
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +123 -0
- package/node_modules/@oxygen/shared/dist/llm-durable-capture.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/llm-durable-capture.js +89 -0
- package/node_modules/@oxygen/shared/dist/llm-prompts.d.ts +75 -0
- package/node_modules/@oxygen/shared/dist/llm-prompts.js +161 -0
- package/node_modules/@oxygen/shared/dist/log-sink-selector.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/log-sink-selector.js +56 -0
- package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/log.js +6 -1
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +90 -0
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.js +130 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +17 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +21 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/operational-telemetry.js +73 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +79 -0
- package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +366 -0
- package/node_modules/@oxygen/shared/dist/plan-capabilities.js +1 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +23 -22
- package/node_modules/@oxygen/shared/dist/plan-limits.js +45 -18
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +48 -41
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +36 -25
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +22 -22
- package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +40 -34
- package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +9 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +36 -2
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +36 -1
- package/node_modules/@oxygen/shared/dist/rate-window.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/rate-window.js +8 -0
- package/node_modules/@oxygen/shared/dist/redaction.js +4 -1
- package/node_modules/@oxygen/shared/dist/research-output-contract.js +1 -3
- package/node_modules/@oxygen/shared/dist/scraper-lane-credential.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/scraper-lane-credential.js +23 -0
- package/node_modules/@oxygen/shared/dist/search-vocab.js +4 -5
- package/node_modules/@oxygen/shared/dist/select-options.js +6 -1
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +1 -5
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +115 -5
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +80 -0
- package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +198 -0
- package/node_modules/@oxygen/shared/dist/social-capabilities.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/social-capabilities.js +25 -16
- package/node_modules/@oxygen/shared/dist/social-post-metrics-core.d.ts +32 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-core.js +32 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-linkedin.d.ts +31 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-linkedin.js +103 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-series.d.ts +96 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +213 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-x.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics-x.js +78 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/social-post-metrics.js +51 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +22 -10
- package/node_modules/@oxygen/shared/dist/spend-safety.js +15 -21
- package/node_modules/@oxygen/shared/dist/sql-rows.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/sql-rows.js +3 -0
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +184 -0
- package/node_modules/@oxygen/shared/dist/stripe-subscription-kind.d.ts +41 -0
- package/node_modules/@oxygen/shared/dist/stripe-subscription-kind.js +44 -0
- package/node_modules/@oxygen/shared/dist/table-limits.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/table-limits.js +3 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.d.ts +94 -0
- package/node_modules/@oxygen/shared/dist/telemetry-export-observer.js +298 -0
- package/node_modules/@oxygen/shared/dist/telemetry.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/telemetry.js +28 -2
- package/node_modules/@oxygen/shared/dist/type-guards.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/type-guards.js +35 -0
- package/node_modules/@oxygen/shared/dist/ugc.d.ts +22 -11
- package/node_modules/@oxygen/shared/dist/ugc.js +10 -0
- package/node_modules/@oxygen/shared/dist/value-readers.d.ts +21 -0
- package/node_modules/@oxygen/shared/dist/value-readers.js +59 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/version.generated.js +2 -0
- package/node_modules/@oxygen/shared/dist/version.js +8 -1
- package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +0 -23
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +31 -0
- package/node_modules/@oxygen/shared/package.json +59 -0
- package/node_modules/@oxygen/workflows/dist/graph/expression.js +2 -5
- package/node_modules/@oxygen/workflows/dist/graph/params.js +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export declare const LINKEDIN_POST_KEYWORD_FIELDS: {
|
|
2
|
+
readonly all: "all_keywords";
|
|
3
|
+
readonly any: "any_keywords";
|
|
4
|
+
readonly exclude: "exclude_keywords";
|
|
5
|
+
};
|
|
6
|
+
/** Each "any of" keyword is one upstream search; this bounds the fan-out. */
|
|
7
|
+
export declare const MAX_ANY_KEYWORDS = 5;
|
|
8
|
+
export declare const MAX_ALL_KEYWORDS = 5;
|
|
9
|
+
export declare const MAX_EXCLUDE_KEYWORDS = 20;
|
|
10
|
+
export declare const MAX_KEYWORD_LENGTH = 100;
|
|
11
|
+
export type LinkedInPostKeywordRules = {
|
|
12
|
+
/** Every one of these must appear in the post. */
|
|
13
|
+
all: string[];
|
|
14
|
+
/** At least one of these must appear in the post. */
|
|
15
|
+
any: string[];
|
|
16
|
+
/** A post containing any of these is dropped. */
|
|
17
|
+
exclude: string[];
|
|
18
|
+
};
|
|
19
|
+
export type LinkedInPostKeywordMatch = {
|
|
20
|
+
keep: boolean;
|
|
21
|
+
/** The "all of" and "any of" keywords found in the post, in rule order. */
|
|
22
|
+
matched: string[];
|
|
23
|
+
};
|
|
24
|
+
/** Trim, drop wrapping quotes and collapse inner whitespace. Case is kept for display. */
|
|
25
|
+
export declare function normalizeLinkedInPostKeyword(value: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Reads the three rule lists from a request (arrays, or comma-separated strings
|
|
28
|
+
* from a CLI flag). Returns null when no rule is present, so callers can keep
|
|
29
|
+
* the plain single-search path untouched.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readLinkedInPostKeywordRules(input: Record<string, unknown>): LinkedInPostKeywordRules | null;
|
|
32
|
+
/** Everything wrong with a rule set, as customer-readable sentences. Empty when valid. */
|
|
33
|
+
export declare function linkedInPostKeywordRuleProblems(rules: LinkedInPostKeywordRules): string[];
|
|
34
|
+
/**
|
|
35
|
+
* The keyword strings sent upstream, one search each. `search` is the caller's
|
|
36
|
+
* free-text query and leads every search; "all of" keywords follow; each "any
|
|
37
|
+
* of" keyword gets its own search. Exclusions are never sent — they are only
|
|
38
|
+
* enforced on the returned text. Empty when there is no positive keyword at all.
|
|
39
|
+
*/
|
|
40
|
+
export declare function linkedInPostSearchQueries(search: string | null | undefined, rules: LinkedInPostKeywordRules): string[];
|
|
41
|
+
/** How many upstream searches a request fans out to (at least one). */
|
|
42
|
+
export declare function linkedInPostSearchCount(input: Record<string, unknown>): number;
|
|
43
|
+
/** Applies the rules to one post's text. A post with no text matches nothing positive. */
|
|
44
|
+
export declare function matchLinkedInPostKeywords(text: string | null | undefined, rules: LinkedInPostKeywordRules): LinkedInPostKeywordMatch;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Boolean keyword rules for LinkedIn post search.
|
|
2
|
+
//
|
|
3
|
+
// The managed post-search endpoint takes a keyword list and documents neither
|
|
4
|
+
// AND/OR semantics between entries nor any boolean syntax, so OXYGEN does not
|
|
5
|
+
// rely on the provider for them. Instead:
|
|
6
|
+
//
|
|
7
|
+
// - every "any of" keyword becomes its own upstream search (the "all of"
|
|
8
|
+
// keywords ride along in each one), which is what makes an OR recall-safe;
|
|
9
|
+
// - the rules are then enforced here, on the returned post text, which is
|
|
10
|
+
// what makes them exact: case-insensitive, whitespace-normalized substring
|
|
11
|
+
// matching, the same way a person scanning the post would read it.
|
|
12
|
+
//
|
|
13
|
+
// The provider caps searches per hour for the whole account, so the number of
|
|
14
|
+
// upstream searches one request may fan out to is bounded by MAX_ANY_KEYWORDS.
|
|
15
|
+
// Pure and dependency-free: the provider adapter, the pricing resolver, the
|
|
16
|
+
// signal planner and the web filter rail all read the same rules.
|
|
17
|
+
export const LINKEDIN_POST_KEYWORD_FIELDS = {
|
|
18
|
+
all: "all_keywords",
|
|
19
|
+
any: "any_keywords",
|
|
20
|
+
exclude: "exclude_keywords",
|
|
21
|
+
};
|
|
22
|
+
/** Each "any of" keyword is one upstream search; this bounds the fan-out. */
|
|
23
|
+
export const MAX_ANY_KEYWORDS = 5;
|
|
24
|
+
export const MAX_ALL_KEYWORDS = 5;
|
|
25
|
+
export const MAX_EXCLUDE_KEYWORDS = 20;
|
|
26
|
+
export const MAX_KEYWORD_LENGTH = 100;
|
|
27
|
+
/** Trim, drop wrapping quotes and collapse inner whitespace. Case is kept for display. */
|
|
28
|
+
export function normalizeLinkedInPostKeyword(value) {
|
|
29
|
+
return value.trim().replace(/^["'“”‘’]+|["'“”‘’]+$/g, "").replace(/\s+/g, " ").trim();
|
|
30
|
+
}
|
|
31
|
+
// A hyphen or dash reads as a space: LinkedIn's own search matches "founder led
|
|
32
|
+
// sales" to posts that say "founder-led sales", and on 2026-09-24 all 25 posts it
|
|
33
|
+
// returned for that keyword were dropped here for the hyphen alone.
|
|
34
|
+
function comparable(value) {
|
|
35
|
+
return value.toLowerCase().replace(/[-\u2010-\u2015]/g, " ").replace(/\s+/g, " ");
|
|
36
|
+
}
|
|
37
|
+
function readTerms(value) {
|
|
38
|
+
if (value === undefined || value === null || value === "")
|
|
39
|
+
return [];
|
|
40
|
+
const raw = Array.isArray(value) ? value.map((entry) => String(entry)) : String(value).split(",");
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
const terms = [];
|
|
43
|
+
for (const entry of raw) {
|
|
44
|
+
const term = normalizeLinkedInPostKeyword(entry);
|
|
45
|
+
if (!term)
|
|
46
|
+
continue;
|
|
47
|
+
const key = comparable(term);
|
|
48
|
+
if (seen.has(key))
|
|
49
|
+
continue;
|
|
50
|
+
seen.add(key);
|
|
51
|
+
terms.push(term);
|
|
52
|
+
}
|
|
53
|
+
return terms;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Reads the three rule lists from a request (arrays, or comma-separated strings
|
|
57
|
+
* from a CLI flag). Returns null when no rule is present, so callers can keep
|
|
58
|
+
* the plain single-search path untouched.
|
|
59
|
+
*/
|
|
60
|
+
export function readLinkedInPostKeywordRules(input) {
|
|
61
|
+
const rules = {
|
|
62
|
+
all: readTerms(input[LINKEDIN_POST_KEYWORD_FIELDS.all]),
|
|
63
|
+
any: readTerms(input[LINKEDIN_POST_KEYWORD_FIELDS.any]),
|
|
64
|
+
exclude: readTerms(input[LINKEDIN_POST_KEYWORD_FIELDS.exclude]),
|
|
65
|
+
};
|
|
66
|
+
return rules.all.length + rules.any.length + rules.exclude.length > 0 ? rules : null;
|
|
67
|
+
}
|
|
68
|
+
/** Everything wrong with a rule set, as customer-readable sentences. Empty when valid. */
|
|
69
|
+
export function linkedInPostKeywordRuleProblems(rules) {
|
|
70
|
+
const problems = [];
|
|
71
|
+
if (rules.any.length > MAX_ANY_KEYWORDS) {
|
|
72
|
+
problems.push(`any_keywords takes at most ${MAX_ANY_KEYWORDS} keywords; each one is a separate LinkedIn search.`);
|
|
73
|
+
}
|
|
74
|
+
if (rules.all.length > MAX_ALL_KEYWORDS)
|
|
75
|
+
problems.push(`all_keywords takes at most ${MAX_ALL_KEYWORDS} keywords.`);
|
|
76
|
+
if (rules.exclude.length > MAX_EXCLUDE_KEYWORDS)
|
|
77
|
+
problems.push(`exclude_keywords takes at most ${MAX_EXCLUDE_KEYWORDS} keywords.`);
|
|
78
|
+
const long = [...rules.all, ...rules.any, ...rules.exclude].find((term) => term.length > MAX_KEYWORD_LENGTH);
|
|
79
|
+
if (long)
|
|
80
|
+
problems.push(`Keywords must be at most ${MAX_KEYWORD_LENGTH} characters.`);
|
|
81
|
+
const excluded = new Set(rules.exclude.map(comparable));
|
|
82
|
+
const conflict = [...rules.all, ...rules.any].find((term) => excluded.has(comparable(term)));
|
|
83
|
+
if (conflict)
|
|
84
|
+
problems.push(`"${conflict}" is both required and excluded.`);
|
|
85
|
+
return problems;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The keyword strings sent upstream, one search each. `search` is the caller's
|
|
89
|
+
* free-text query and leads every search; "all of" keywords follow; each "any
|
|
90
|
+
* of" keyword gets its own search. Exclusions are never sent — they are only
|
|
91
|
+
* enforced on the returned text. Empty when there is no positive keyword at all.
|
|
92
|
+
*/
|
|
93
|
+
export function linkedInPostSearchQueries(search, rules) {
|
|
94
|
+
const base = [search?.trim() ?? "", ...rules.all].filter(Boolean);
|
|
95
|
+
if (rules.any.length === 0)
|
|
96
|
+
return base.length > 0 ? [base.join(" ")] : [];
|
|
97
|
+
return rules.any.map((term) => [...base, term].join(" "));
|
|
98
|
+
}
|
|
99
|
+
/** How many upstream searches a request fans out to (at least one). */
|
|
100
|
+
export function linkedInPostSearchCount(input) {
|
|
101
|
+
const rules = readLinkedInPostKeywordRules(input);
|
|
102
|
+
if (!rules)
|
|
103
|
+
return 1;
|
|
104
|
+
return Math.max(1, Math.min(MAX_ANY_KEYWORDS, rules.any.length));
|
|
105
|
+
}
|
|
106
|
+
/** Applies the rules to one post's text. A post with no text matches nothing positive. */
|
|
107
|
+
export function matchLinkedInPostKeywords(text, rules) {
|
|
108
|
+
const body = comparable(text ?? "");
|
|
109
|
+
const has = (term) => body.includes(comparable(term));
|
|
110
|
+
const matchedAll = rules.all.filter(has);
|
|
111
|
+
const matchedAny = rules.any.filter(has);
|
|
112
|
+
const keep = matchedAll.length === rules.all.length
|
|
113
|
+
&& (rules.any.length === 0 || matchedAny.length > 0)
|
|
114
|
+
&& !rules.exclude.some(has);
|
|
115
|
+
return { keep, matched: [...matchedAll, ...matchedAny] };
|
|
116
|
+
}
|
|
@@ -151,4 +151,100 @@ export declare function waitStepDelayMs(step: LinkedInWaitStep): number;
|
|
|
151
151
|
* seed is derived.
|
|
152
152
|
*/
|
|
153
153
|
export declare function renderLinkedInTemplate(template: string, values: Record<string, unknown>, options?: RenderTemplateOptions): string;
|
|
154
|
+
/**
|
|
155
|
+
* LinkedIn's connection-note ceiling for a PREMIUM sending account.
|
|
156
|
+
*
|
|
157
|
+
* Basic accounts are capped lower — see {@link LINKEDIN_INVITE_NOTE_LIMIT_BASIC}.
|
|
158
|
+
* One constant, three consumers (the definition validator, the launch copy
|
|
159
|
+
* preflight and the dispatcher); they carried three independent `300`s before.
|
|
160
|
+
*/
|
|
161
|
+
export declare const LINKEDIN_INVITE_NOTE_LIMIT = 300;
|
|
162
|
+
/**
|
|
163
|
+
* The ceiling that actually holds for a sending account whose tier we do not
|
|
164
|
+
* know, which today is every account: nothing in `sender_accounts.metadata`
|
|
165
|
+
* records a LinkedIn Premium signal.
|
|
166
|
+
*
|
|
167
|
+
* Measured on production (2026-09-20, one tenant, 30 days). Two senders, same
|
|
168
|
+
* tenant, same table:
|
|
169
|
+
*
|
|
170
|
+
* eca988d5 notes > 200 chars: 74 sent, 1 failed, 0 length refusals
|
|
171
|
+
* d44fe895 notes > 200 chars: 0 sent, 138 failed, 138 length refusals
|
|
172
|
+
*
|
|
173
|
+
* d44fe895 has never delivered a single noted invite over 200 characters, and
|
|
174
|
+
* the length refusal never fires below 200 on either account. Sequence f729fc2c
|
|
175
|
+
* rendered 205–238 character notes onto that sender and destroyed 138 leads at a
|
|
176
|
+
* 100% invite failure rate, because a validator that only knows the Premium
|
|
177
|
+
* ceiling let every one of them through.
|
|
178
|
+
*
|
|
179
|
+
* 200 is safe on any tier, so it is the default and the account may only be
|
|
180
|
+
* relaxed upward from evidence.
|
|
181
|
+
*/
|
|
182
|
+
export declare const LINKEDIN_INVITE_NOTE_LIMIT_BASIC = 200;
|
|
183
|
+
/**
|
|
184
|
+
* What a LinkedIn refusal is actually ABOUT — the question the HTTP status band
|
|
185
|
+
* cannot answer.
|
|
186
|
+
*
|
|
187
|
+
* Measured on production (2026-09-21, 14 days, one tenant), a single 403 covered
|
|
188
|
+
* a sender-side allowance, an unreadable profile and a step sent to a
|
|
189
|
+
* non-connection, and all three terminated the lead as a recipient bounce. The
|
|
190
|
+
* provider's own sentence separates them; the status never could.
|
|
191
|
+
*/
|
|
192
|
+
export type LinkedInRefusalScope =
|
|
193
|
+
/** Our request was malformed for this account. Ours to fix; the lead is fine. */
|
|
194
|
+
"oxygen_request_invalid"
|
|
195
|
+
/** We could not READ the lead's profile. Not a verdict on a send. */
|
|
196
|
+
| "profile_unreadable"
|
|
197
|
+
/** This sender has exhausted a LinkedIn allowance. Nothing to do with the lead. */
|
|
198
|
+
| "sender_allowance"
|
|
199
|
+
/** The step needs a connection this lead has not accepted. Ours to sequence. */
|
|
200
|
+
| "relationship_required"
|
|
201
|
+
/** An invite already exists for this lead. The journey is effectively running. */
|
|
202
|
+
| "already_engaged"
|
|
203
|
+
/** Unrecognised. Callers MUST preserve their existing behaviour. */
|
|
204
|
+
| "unknown";
|
|
205
|
+
/**
|
|
206
|
+
* Classify a LinkedIn refusal from the provider's own account of it.
|
|
207
|
+
*
|
|
208
|
+
* Deliberately a CLOSED set of anchored, full-string matches, in the same
|
|
209
|
+
* discipline as the sequence-recovery allowlists: matching vendor prose is
|
|
210
|
+
* fragile, so an unrecognised message must degrade to `unknown` and leave the
|
|
211
|
+
* caller's behaviour exactly as it was. Extending the set is an evidence-driven
|
|
212
|
+
* act — callers log what they could not classify.
|
|
213
|
+
*
|
|
214
|
+
* Pure and total; never throws.
|
|
215
|
+
*/
|
|
216
|
+
export declare function classifyLinkedInRefusal(input: {
|
|
217
|
+
providerType: string | null;
|
|
218
|
+
providerMessage: string | null;
|
|
219
|
+
status: number | null;
|
|
220
|
+
operation: string | null;
|
|
221
|
+
}): LinkedInRefusalScope;
|
|
222
|
+
/**
|
|
223
|
+
* Does this refusal scope say the LEAD is unreachable?
|
|
224
|
+
*
|
|
225
|
+
* Only `already_engaged` is genuinely about the lead, and even that one means
|
|
226
|
+
* "an invite exists", not "this person is dead". Everything else is about our
|
|
227
|
+
* request, our sender or our sequencing, and must never brand the lead.
|
|
228
|
+
*/
|
|
229
|
+
export declare function linkedInRefusalBlamesLead(scope: LinkedInRefusalScope): boolean;
|
|
230
|
+
/**
|
|
231
|
+
* Trim a rendered connection note to a LinkedIn account's ceiling, at a word
|
|
232
|
+
* boundary.
|
|
233
|
+
*
|
|
234
|
+
* Counts CODE POINTS, matching the launch copy preflight exactly — `.length`
|
|
235
|
+
* would count an emoji or a ZWJ sequence as two or more and the two surfaces
|
|
236
|
+
* would then disagree about the same note.
|
|
237
|
+
*
|
|
238
|
+
* Cuts at the last whitespace inside the limit so the note never ends mid-word;
|
|
239
|
+
* a single unbroken run longer than the limit has no boundary to find and is cut
|
|
240
|
+
* hard, which is the only case where that can happen.
|
|
241
|
+
*
|
|
242
|
+
* Returns the note unchanged (and `truncated: false`) when it already fits, so
|
|
243
|
+
* callers can record the trim only when there was one.
|
|
244
|
+
*/
|
|
245
|
+
export declare function truncateLinkedInInviteNote(note: string, limit: number): {
|
|
246
|
+
note: string;
|
|
247
|
+
truncated: boolean;
|
|
248
|
+
originalLength: number;
|
|
249
|
+
};
|
|
154
250
|
export {};
|
|
@@ -73,3 +73,126 @@ export function waitStepDelayMs(step) {
|
|
|
73
73
|
export function renderLinkedInTemplate(template, values, options) {
|
|
74
74
|
return renderTemplate(template, values, options);
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* LinkedIn's connection-note ceiling for a PREMIUM sending account.
|
|
78
|
+
*
|
|
79
|
+
* Basic accounts are capped lower — see {@link LINKEDIN_INVITE_NOTE_LIMIT_BASIC}.
|
|
80
|
+
* One constant, three consumers (the definition validator, the launch copy
|
|
81
|
+
* preflight and the dispatcher); they carried three independent `300`s before.
|
|
82
|
+
*/
|
|
83
|
+
export const LINKEDIN_INVITE_NOTE_LIMIT = 300;
|
|
84
|
+
/**
|
|
85
|
+
* The ceiling that actually holds for a sending account whose tier we do not
|
|
86
|
+
* know, which today is every account: nothing in `sender_accounts.metadata`
|
|
87
|
+
* records a LinkedIn Premium signal.
|
|
88
|
+
*
|
|
89
|
+
* Measured on production (2026-09-20, one tenant, 30 days). Two senders, same
|
|
90
|
+
* tenant, same table:
|
|
91
|
+
*
|
|
92
|
+
* eca988d5 notes > 200 chars: 74 sent, 1 failed, 0 length refusals
|
|
93
|
+
* d44fe895 notes > 200 chars: 0 sent, 138 failed, 138 length refusals
|
|
94
|
+
*
|
|
95
|
+
* d44fe895 has never delivered a single noted invite over 200 characters, and
|
|
96
|
+
* the length refusal never fires below 200 on either account. Sequence f729fc2c
|
|
97
|
+
* rendered 205–238 character notes onto that sender and destroyed 138 leads at a
|
|
98
|
+
* 100% invite failure rate, because a validator that only knows the Premium
|
|
99
|
+
* ceiling let every one of them through.
|
|
100
|
+
*
|
|
101
|
+
* 200 is safe on any tier, so it is the default and the account may only be
|
|
102
|
+
* relaxed upward from evidence.
|
|
103
|
+
*/
|
|
104
|
+
export const LINKEDIN_INVITE_NOTE_LIMIT_BASIC = 200;
|
|
105
|
+
/**
|
|
106
|
+
* Classify a LinkedIn refusal from the provider's own account of it.
|
|
107
|
+
*
|
|
108
|
+
* Deliberately a CLOSED set of anchored, full-string matches, in the same
|
|
109
|
+
* discipline as the sequence-recovery allowlists: matching vendor prose is
|
|
110
|
+
* fragile, so an unrecognised message must degrade to `unknown` and leave the
|
|
111
|
+
* caller's behaviour exactly as it was. Extending the set is an evidence-driven
|
|
112
|
+
* act — callers log what they could not classify.
|
|
113
|
+
*
|
|
114
|
+
* Pure and total; never throws.
|
|
115
|
+
*/
|
|
116
|
+
export function classifyLinkedInRefusal(input) {
|
|
117
|
+
const message = typeof input.providerMessage === "string" ? input.providerMessage.trim() : "";
|
|
118
|
+
if (message.length === 0)
|
|
119
|
+
return "unknown";
|
|
120
|
+
// Our invite note is longer than THIS account's ceiling. The 164 rows this
|
|
121
|
+
// matched in 14 days are leads we broke, filed as dead recipients.
|
|
122
|
+
if (input.status === 400
|
|
123
|
+
&& input.providerType === "provider/invalid_parameters"
|
|
124
|
+
&& /^Invalid parameters: You have reached the maximum number of characters for the custom message\.$/u.test(message)) {
|
|
125
|
+
return "oxygen_request_invalid";
|
|
126
|
+
}
|
|
127
|
+
// The profile READ failed. 121 of 318 failures in 14 days were this, on
|
|
128
|
+
// `users_get` — the lookup that runs BEFORE the invite or message, so LinkedIn
|
|
129
|
+
// never saw a send at all. It can be a genuinely unreachable profile or this
|
|
130
|
+
// sender's own profile-view allowance, and the message cannot tell them apart,
|
|
131
|
+
// which is exactly why it must not terminate the lead on the first attempt.
|
|
132
|
+
if (input.status === 403
|
|
133
|
+
&& input.operation === "users_get"
|
|
134
|
+
&& /^Insufficient permissions: This profile can't be accessed$/u.test(message)) {
|
|
135
|
+
return "profile_unreadable";
|
|
136
|
+
}
|
|
137
|
+
if (input.status === 403
|
|
138
|
+
&& input.providerType === "provider/access_restricted"
|
|
139
|
+
&& /^Access restricted: You have reached the limit for custom messages\.$/u.test(message)) {
|
|
140
|
+
return "sender_allowance";
|
|
141
|
+
}
|
|
142
|
+
if (input.status === 403
|
|
143
|
+
&& input.providerType === "provider/access_restricted"
|
|
144
|
+
&& /^Access restricted: This user is not a relation\.$/u.test(message)) {
|
|
145
|
+
return "relationship_required";
|
|
146
|
+
}
|
|
147
|
+
if (input.status === 422
|
|
148
|
+
&& input.providerType === "provider/unprocessable_entity"
|
|
149
|
+
&& /^Unprocessable entity: This user has already handled a request from you in the past 3 weeks\./u.test(message)) {
|
|
150
|
+
return "already_engaged";
|
|
151
|
+
}
|
|
152
|
+
if (input.status === 409
|
|
153
|
+
&& input.providerType === "provider/conflict"
|
|
154
|
+
&& /^Conflict: A relation request has already been sent to this user\.$/u.test(message)) {
|
|
155
|
+
return "already_engaged";
|
|
156
|
+
}
|
|
157
|
+
return "unknown";
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Does this refusal scope say the LEAD is unreachable?
|
|
161
|
+
*
|
|
162
|
+
* Only `already_engaged` is genuinely about the lead, and even that one means
|
|
163
|
+
* "an invite exists", not "this person is dead". Everything else is about our
|
|
164
|
+
* request, our sender or our sequencing, and must never brand the lead.
|
|
165
|
+
*/
|
|
166
|
+
export function linkedInRefusalBlamesLead(scope) {
|
|
167
|
+
return scope === "already_engaged";
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Trim a rendered connection note to a LinkedIn account's ceiling, at a word
|
|
171
|
+
* boundary.
|
|
172
|
+
*
|
|
173
|
+
* Counts CODE POINTS, matching the launch copy preflight exactly — `.length`
|
|
174
|
+
* would count an emoji or a ZWJ sequence as two or more and the two surfaces
|
|
175
|
+
* would then disagree about the same note.
|
|
176
|
+
*
|
|
177
|
+
* Cuts at the last whitespace inside the limit so the note never ends mid-word;
|
|
178
|
+
* a single unbroken run longer than the limit has no boundary to find and is cut
|
|
179
|
+
* hard, which is the only case where that can happen.
|
|
180
|
+
*
|
|
181
|
+
* Returns the note unchanged (and `truncated: false`) when it already fits, so
|
|
182
|
+
* callers can record the trim only when there was one.
|
|
183
|
+
*/
|
|
184
|
+
export function truncateLinkedInInviteNote(note, limit) {
|
|
185
|
+
const points = [...note];
|
|
186
|
+
const originalLength = points.length;
|
|
187
|
+
if (limit <= 0 || originalLength <= limit) {
|
|
188
|
+
return { note, truncated: false, originalLength };
|
|
189
|
+
}
|
|
190
|
+
const head = points.slice(0, limit);
|
|
191
|
+
let cut = head.length;
|
|
192
|
+
while (cut > 0 && !/\s/u.test(head[cut - 1]))
|
|
193
|
+
cut -= 1;
|
|
194
|
+
// No whitespace inside the window: one unbroken run, so a hard cut is the
|
|
195
|
+
// only option left.
|
|
196
|
+
const kept = cut === 0 ? head : head.slice(0, cut);
|
|
197
|
+
return { note: kept.join("").trimEnd(), truncated: true, originalLength };
|
|
198
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LlmTraceBody, LlmTracingClient } from "./langfuse.js";
|
|
2
|
+
export type LlmCaptureTimestampLookup = {
|
|
3
|
+
type: "root";
|
|
4
|
+
id: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: "event";
|
|
7
|
+
key: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: "approval";
|
|
10
|
+
id: string;
|
|
11
|
+
phase: "requested" | "decided";
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Langfuse v3 deduplication includes the start date. Stable ids alone do not
|
|
15
|
+
* prevent cross-day duplicates. This per-trace adapter obtains the original
|
|
16
|
+
* ledger clock without blocking runtime methods or borrowing their transaction.
|
|
17
|
+
* The underlying transport still owns identity, payloads, isolation and flush.
|
|
18
|
+
*/
|
|
19
|
+
export declare function withDurableLlmCapture(input: {
|
|
20
|
+
client: LlmTracingClient;
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
root: LlmTraceBody;
|
|
23
|
+
readTimestamp?: ((lookup: LlmCaptureTimestampLookup) => Promise<Date | null>) | undefined;
|
|
24
|
+
}): LlmTracingClient;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { log } from "./log.js";
|
|
2
|
+
/**
|
|
3
|
+
* Langfuse v3 deduplication includes the start date. Stable ids alone do not
|
|
4
|
+
* prevent cross-day duplicates. This per-trace adapter obtains the original
|
|
5
|
+
* ledger clock without blocking runtime methods or borrowing their transaction.
|
|
6
|
+
* The underlying transport still owns identity, payloads, isolation and flush.
|
|
7
|
+
*/
|
|
8
|
+
export function withDurableLlmCapture(input) {
|
|
9
|
+
if (!input.enabled)
|
|
10
|
+
return input.client;
|
|
11
|
+
const pending = new Set();
|
|
12
|
+
const warned = new Set();
|
|
13
|
+
const missing = (stage) => {
|
|
14
|
+
if (warned.has(stage))
|
|
15
|
+
return;
|
|
16
|
+
warned.add(stage);
|
|
17
|
+
log("warn", "llm_tracing.missing_capture", { provider: "langfuse", stage, error_code: "langfuse_immutable_timestamp_unavailable" });
|
|
18
|
+
};
|
|
19
|
+
const valid = (value) => value instanceof Date && Number.isFinite(value.getTime());
|
|
20
|
+
const read = (lookup) => new Promise((resolve) => {
|
|
21
|
+
// Bounds injected readers too. A DB reader must independently bound its
|
|
22
|
+
// statement and release its own scope when a late acquisition completes.
|
|
23
|
+
const timer = setTimeout(() => resolve(null), 750);
|
|
24
|
+
timer.unref?.();
|
|
25
|
+
void Promise.resolve().then(() => input.readTimestamp?.(lookup) ?? null).then((date) => resolve(valid(date) ? date : null), () => resolve(null)).finally(() => clearTimeout(timer));
|
|
26
|
+
});
|
|
27
|
+
const seed = (startTime) => {
|
|
28
|
+
if (!valid(startTime)) {
|
|
29
|
+
missing("root_timestamp");
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
input.client.trace({ ...input.root, startTime, endTime: startTime });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
missing("root_emission");
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return startTime;
|
|
40
|
+
};
|
|
41
|
+
// Seed synchronously when the caller already has the run/turn row. A web
|
|
42
|
+
// approval-only tracer can instead look up its root before queued children.
|
|
43
|
+
const knownRoot = valid(input.root.startTime) ? seed(input.root.startTime) : undefined;
|
|
44
|
+
const rootReady = knownRoot !== undefined ? Promise.resolve(knownRoot)
|
|
45
|
+
: read({ type: "root", id: input.root.id }).then(seed);
|
|
46
|
+
const enqueue = (action) => {
|
|
47
|
+
const task = rootReady.then(async (start) => { if (start)
|
|
48
|
+
await action(start); }).catch(() => missing("capture"));
|
|
49
|
+
pending.add(task);
|
|
50
|
+
void task.finally(() => pending.delete(task));
|
|
51
|
+
};
|
|
52
|
+
const eventLookup = (body) => {
|
|
53
|
+
const key = body.metadata?.oxygen_durable_event_key;
|
|
54
|
+
if (typeof key === "string" && key) {
|
|
55
|
+
// Auto-approval requests have a persisted approval card before an event
|
|
56
|
+
// append. The card clock is canonical for both approval capture paths.
|
|
57
|
+
const approvalKey = /^approval_(requested|decided):(.+)$/.exec(key);
|
|
58
|
+
if (approvalKey)
|
|
59
|
+
return { type: "approval", phase: approvalKey[1], id: approvalKey[2] };
|
|
60
|
+
return { type: "event", key };
|
|
61
|
+
}
|
|
62
|
+
const approval = /^apr:(requested|decided):(.+)$/.exec(body.id);
|
|
63
|
+
return approval ? { type: "approval", phase: approval[1], id: approval[2] } : null;
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
trace: (body) => enqueue((startTime) => input.client.trace({ ...body, startTime })),
|
|
67
|
+
generation: (body) => enqueue(() => input.client.generation(body)),
|
|
68
|
+
embedding: (body) => enqueue(() => input.client.embedding(body)),
|
|
69
|
+
span: (body) => enqueue(() => input.client.span(body)),
|
|
70
|
+
event: (body) => {
|
|
71
|
+
const lookup = eventLookup(body);
|
|
72
|
+
enqueue(async () => {
|
|
73
|
+
if (!lookup) {
|
|
74
|
+
input.client.event(body);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const startTime = valid(body.startTime) ? body.startTime : await read(lookup);
|
|
78
|
+
if (!startTime) {
|
|
79
|
+
missing("event_timestamp");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
input.client.event({ ...body, startTime });
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
score: (body) => input.client.score(body),
|
|
86
|
+
flush: async () => { await rootReady; await Promise.allSettled([...pending]); await input.client.flush(); },
|
|
87
|
+
shutdown: async () => { await rootReady; await Promise.allSettled([...pending]); await input.client.shutdown(); },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type LlmPromptRef = {
|
|
2
|
+
/** Langfuse prompt name, e.g. `copilot.system` or `table_column`. */
|
|
3
|
+
name: string;
|
|
4
|
+
/** First 12 hex chars of sha256(template); changes whenever the template does. */
|
|
5
|
+
hash: string;
|
|
6
|
+
/** True only for code-owned templates published by the prompt sync. */
|
|
7
|
+
registered: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type LlmPromptDefinition = LlmPromptRef & {
|
|
10
|
+
registered: true;
|
|
11
|
+
template: string;
|
|
12
|
+
};
|
|
13
|
+
/** Deterministic content hash; object key order never changes it. */
|
|
14
|
+
export declare function llmPromptHash(template: unknown): string;
|
|
15
|
+
/** The Langfuse label that identifies one exact template version. */
|
|
16
|
+
export declare function llmPromptLabel(hash: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Declare a code-owned prompt template. Call once at module scope with the
|
|
19
|
+
* template the call site renders from. Placeholders stay literal (`{{name}}`)
|
|
20
|
+
* so the hash moves only when the instructions do, not per request.
|
|
21
|
+
*/
|
|
22
|
+
export declare function defineLlmPrompt(name: string, template: string): LlmPromptDefinition;
|
|
23
|
+
/** Names declared twice with different templates (a catalog defect). */
|
|
24
|
+
export declare function conflictingLlmPromptNames(): string[];
|
|
25
|
+
/** The Langfuse-safe name shape code prompts must use. */
|
|
26
|
+
export declare function isValidLlmPromptName(name: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* An AI column's prompt as the text Langfuse stores: the instruction template
|
|
29
|
+
* (placeholders unresolved) plus its output schema, so a schema change is a new
|
|
30
|
+
* prompt version too. Customer columns and code-owned helpers share it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function aiColumnPromptTemplate(definition: {
|
|
33
|
+
prompt?: unknown;
|
|
34
|
+
outputSchema?: unknown;
|
|
35
|
+
}): string;
|
|
36
|
+
/** Declare a code-owned AI helper prompt (knowledge synthesis, drafts, ...). */
|
|
37
|
+
export declare function defineAiColumnPrompt(name: string, definition: {
|
|
38
|
+
prompt: string;
|
|
39
|
+
outputSchema?: Record<string, unknown> | null;
|
|
40
|
+
}): LlmPromptDefinition;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a lazy prompt getter on a product path. Prompt identity is telemetry:
|
|
43
|
+
* a template renderer that throws must degrade to an unlinked trace, never
|
|
44
|
+
* fail the model call it describes.
|
|
45
|
+
*/
|
|
46
|
+
export declare function safeLlmPrompt<T extends LlmPromptRef>(getter: () => T | null): T | null;
|
|
47
|
+
/** Reference a customer-authored or per-request template: metadata only. */
|
|
48
|
+
export declare function llmPromptRef(name: string, template: unknown): LlmPromptRef;
|
|
49
|
+
/**
|
|
50
|
+
* The registered code prompt whose template is exactly this text, if any: lets
|
|
51
|
+
* a tracer that only sees the rendered system message recognise a static
|
|
52
|
+
* template (compaction, sub-agent instructions) without importing its owner.
|
|
53
|
+
*/
|
|
54
|
+
export declare function findLlmPromptByTemplate(template: string): LlmPromptDefinition | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* The declared code prompt behind a reference, only when its template still
|
|
57
|
+
* hashes to that reference: lets the tracing client publish the exact text a
|
|
58
|
+
* generation was rendered from, and never a same-named neighbour.
|
|
59
|
+
*/
|
|
60
|
+
export declare function registeredLlmPrompt(ref: LlmPromptRef): LlmPromptDefinition | undefined;
|
|
61
|
+
/** Every code prompt declared by the modules imported so far, sorted by name. */
|
|
62
|
+
export declare function listLlmPrompts(): LlmPromptDefinition[];
|
|
63
|
+
/**
|
|
64
|
+
* The prompt a Copilot/Agent model call ran on, from the exact messages sent.
|
|
65
|
+
* A main-loop call (`kind` "turn") uses the surface's versioned system
|
|
66
|
+
* template; compaction and sub-agent calls match a registered static template
|
|
67
|
+
* by the content of one of their system messages; anything else is traced by
|
|
68
|
+
* content hash only.
|
|
69
|
+
*/
|
|
70
|
+
export declare function llmPromptForModelCall(input: {
|
|
71
|
+
surface: "copilot" | "agent";
|
|
72
|
+
kind: string;
|
|
73
|
+
messages: unknown;
|
|
74
|
+
turnPrompt?: LlmPromptRef | null;
|
|
75
|
+
}): LlmPromptRef | null;
|