@librechat/agents 3.3.6 → 3.3.8
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/dist/cjs/graphs/MultiAgentGraph.cjs +21 -4
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/messages/format.cjs +124 -15
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/injected.cjs +10 -1
- package/dist/cjs/messages/injected.cjs.map +1 -1
- package/dist/cjs/prompts/activityLabel.cjs +29 -1
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
- package/dist/cjs/run.cjs +7 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +55 -0
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/intentArg.cjs +78 -52
- package/dist/cjs/tools/intentArg.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +5 -5
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +21 -4
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -2
- package/dist/esm/messages/format.mjs +124 -15
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/injected.mjs +10 -1
- package/dist/esm/messages/injected.mjs.map +1 -1
- package/dist/esm/prompts/activityLabel.mjs +29 -1
- package/dist/esm/prompts/activityLabel.mjs.map +1 -1
- package/dist/esm/run.mjs +7 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +55 -0
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/intentArg.mjs +77 -53
- package/dist/esm/tools/intentArg.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +5 -5
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/types/messages/format.d.ts +9 -8
- package/dist/types/prompts/activityLabel.d.ts +8 -1
- package/dist/types/run.d.ts +1 -1
- package/dist/types/tools/intentArg.d.ts +74 -12
- package/dist/types/tools/search/tool.d.ts +5 -5
- package/dist/types/types/activityLabel.d.ts +8 -0
- package/dist/types/types/stream.d.ts +27 -2
- package/package.json +1 -1
- package/src/graphs/MultiAgentGraph.ts +18 -4
- package/src/messages/format.ts +222 -50
- package/src/messages/formatAgentMessages.test.ts +308 -6
- package/src/messages/injected.test.ts +18 -1
- package/src/messages/injected.ts +8 -1
- package/src/prompts/activityLabel.ts +48 -0
- package/src/run.ts +10 -1
- package/src/specs/activity-label-prompt.test.ts +93 -0
- package/src/summarization/__tests__/node.test.ts +188 -0
- package/src/summarization/node.ts +67 -0
- package/src/tools/__tests__/intentArg.test.ts +101 -25
- package/src/tools/intentArg.ts +102 -68
- package/src/tools/search/outcome.test.ts +1 -1
- package/src/tools/search/tool.ts +5 -5
- package/src/types/activityLabel.ts +8 -0
- package/src/types/stream.ts +28 -2
|
@@ -43,7 +43,16 @@ function convertInjectedMessages(messages) {
|
|
|
43
43
|
const converted = [];
|
|
44
44
|
for (const msg of messages) {
|
|
45
45
|
if (isEmptyInjectedContent(msg.content)) continue;
|
|
46
|
-
|
|
46
|
+
/** Provenance, recorded here because this is the only place that knows it.
|
|
47
|
+
* `isMeta` and `source` are both optional on `InjectedMessage`, so a bare
|
|
48
|
+
* entry is otherwise indistinguishable from a message replayed out of the
|
|
49
|
+
* payload — and downstream consumers such as compaction coverage need to
|
|
50
|
+
* know that this message has no persisted source ID to name. Kept separate
|
|
51
|
+
* from `isMeta`, which carries UI and cache meaning of its own. */
|
|
52
|
+
const additional_kwargs = {
|
|
53
|
+
role: msg.role,
|
|
54
|
+
injected: true
|
|
55
|
+
};
|
|
47
56
|
if (msg.isMeta != null) additional_kwargs.isMeta = msg.isMeta;
|
|
48
57
|
if (msg.source != null) additional_kwargs.source = msg.source;
|
|
49
58
|
if (msg.skillName != null) additional_kwargs.skillName = msg.skillName;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"injected.mjs","names":[],"sources":["../../../src/messages/injected.ts"],"sourcesContent":["// src/messages/injected.ts\nimport { HumanMessage } from '@langchain/core/messages';\nimport type { BaseMessage } from '@langchain/core/messages';\nimport type { InjectedMessage } from '@/types/tools';\nimport {
|
|
1
|
+
{"version":3,"file":"injected.mjs","names":[],"sources":["../../../src/messages/injected.ts"],"sourcesContent":["// src/messages/injected.ts\nimport { HumanMessage } from '@langchain/core/messages';\nimport type { BaseMessage } from '@langchain/core/messages';\nimport type { InjectedMessage } from '@/types/tools';\nimport { toLangChainContent } from './langchain';\nimport { ContentTypes } from '@/common';\n\n/**\n * Converts `InjectedMessage` instances to LangChain `HumanMessage` objects.\n * Both 'user' and 'system' roles become `HumanMessage` to avoid provider\n * rejections (Anthropic/Google reject non-leading SystemMessages). The\n * original role is preserved in `additional_kwargs` for downstream consumers.\n *\n * Shared by both injection boundaries — `ToolNode`'s tool-batch dispatch and\n * `StandardGraph`'s preempt-boundary dispatch. Keeping one implementation is\n * load-bearing rather than tidy: the provider-safety argument for sealing a\n * stream mid-generation rests on the injected turn having byte-identical\n * shape to the one the already-shipped tool boundary emits, so the two sites\n * must not be able to drift.\n */\n/**\n * True when an entry carries nothing a provider will accept as a turn.\n *\n * The public `InjectedMessage` type admits `content: ''` and `content: []`,\n * and a host hook is free to return one. Converting it anyway produces a\n * `HumanMessage` that passes the caller's `length > 0` test, so the graph\n * resumes and sends a trailing EMPTY user turn — which Anthropic and other\n * strict providers reject outright, turning a cooperative seal into a failed\n * run. Permissive providers merely burn a model call.\n *\n * Whitespace counts as empty, matching the standard `canSealPreempt` applies\n * to the assistant side of the same pair. Non-text blocks count as content: a\n * media-only steer is a real turn and must survive.\n */\nfunction isEmptyInjectedContent(content: InjectedMessage['content']): boolean {\n if (typeof content === 'string') {\n return content.trim() === '';\n }\n if (content.length === 0) {\n return true;\n }\n return content.every((block) => {\n if (block.type !== ContentTypes.TEXT) {\n return false;\n }\n const text = block[ContentTypes.TEXT];\n return typeof text !== 'string' || text.trim() === '';\n });\n}\n\nexport function convertInjectedMessages(\n messages: InjectedMessage[]\n): BaseMessage[] {\n const converted: BaseMessage[] = [];\n for (const msg of messages) {\n if (isEmptyInjectedContent(msg.content)) {\n continue;\n }\n /** Provenance, recorded here because this is the only place that knows it.\n * `isMeta` and `source` are both optional on `InjectedMessage`, so a bare\n * entry is otherwise indistinguishable from a message replayed out of the\n * payload — and downstream consumers such as compaction coverage need to\n * know that this message has no persisted source ID to name. Kept separate\n * from `isMeta`, which carries UI and cache meaning of its own. */\n const additional_kwargs: Record<string, unknown> = {\n role: msg.role,\n injected: true,\n };\n if (msg.isMeta != null) additional_kwargs.isMeta = msg.isMeta;\n if (msg.source != null) additional_kwargs.source = msg.source;\n if (msg.skillName != null) additional_kwargs.skillName = msg.skillName;\n\n converted.push(\n new HumanMessage({\n content: toLangChainContent(msg.content),\n additional_kwargs,\n })\n );\n }\n return converted;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAS,uBAAuB,SAA8C;CAC5E,IAAI,OAAO,YAAY,UACrB,OAAO,QAAQ,KAAK,MAAM;CAE5B,IAAI,QAAQ,WAAW,GACrB,OAAO;CAET,OAAO,QAAQ,OAAO,UAAU;EAC9B,IAAI,MAAM,SAAA,QACR,OAAO;EAET,MAAM,OAAO,MAAA;EACb,OAAO,OAAO,SAAS,YAAY,KAAK,KAAK,MAAM;CACrD,CAAC;AACH;AAEA,SAAgB,wBACd,UACe;CACf,MAAM,YAA2B,CAAC;CAClC,KAAK,MAAM,OAAO,UAAU;EAC1B,IAAI,uBAAuB,IAAI,OAAO,GACpC;;;;;;;EAQF,MAAM,oBAA6C;GACjD,MAAM,IAAI;GACV,UAAU;EACZ;EACA,IAAI,IAAI,UAAU,MAAM,kBAAkB,SAAS,IAAI;EACvD,IAAI,IAAI,UAAU,MAAM,kBAAkB,SAAS,IAAI;EACvD,IAAI,IAAI,aAAa,MAAM,kBAAkB,YAAY,IAAI;EAE7D,UAAU,KACR,IAAI,aAAa;GACf,SAAS,mBAAmB,IAAI,OAAO;GACvC;EACF,CAAC,CACH;CACF;CACA,OAAO;AACT"}
|
|
@@ -27,6 +27,22 @@ function truncateForLabel(value, maxLength) {
|
|
|
27
27
|
if (value.length <= maxLength) return value;
|
|
28
28
|
return value.slice(0, Math.max(0, maxLength - 1)) + "…";
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Reduces a committed label to bounded single-line data.
|
|
32
|
+
*
|
|
33
|
+
* Sections in this prompt are delimited by blank lines, so a label carrying
|
|
34
|
+
* embedded newlines could otherwise forge an apparent `Tool calls:` or
|
|
35
|
+
* `Label:` section. Unlike every other input here, previous labels re-enter
|
|
36
|
+
* the prompt on EVERY later batch, so one malformed result — plain model
|
|
37
|
+
* noncompliance, or injection surfacing through a tool result — would
|
|
38
|
+
* persistently steer unrelated later labels rather than affecting one. The
|
|
39
|
+
* clip bounds the same way `lastAssistantText` and reasoning excerpts are
|
|
40
|
+
* bounded: oversized headers must not inflate later requests past the fast
|
|
41
|
+
* model's window and starve the run of labels entirely.
|
|
42
|
+
*/
|
|
43
|
+
function sanitizePreviousLabel(label) {
|
|
44
|
+
return truncateForLabel(label.replace(/\s+/g, " ").trim(), PREVIOUS_LABEL_LIMIT);
|
|
45
|
+
}
|
|
30
46
|
const ABORT_SERIALIZATION = Symbol("abort-label-serialization");
|
|
31
47
|
/**
|
|
32
48
|
* Serializes a tool value for the prompt WITHOUT materializing huge JSON:
|
|
@@ -57,6 +73,10 @@ function serializeForLabel(value, limit) {
|
|
|
57
73
|
}
|
|
58
74
|
const INPUT_CONTEXT_LIMIT = 200;
|
|
59
75
|
const MAX_THINKING_EXCERPTS = 4;
|
|
76
|
+
/** Per-label bound. A header is 5-9 words; anything past this is
|
|
77
|
+
* noncompliance or payload, and previous labels are the one input that
|
|
78
|
+
* RE-ENTERS the prompt on every later batch of the run. */
|
|
79
|
+
const PREVIOUS_LABEL_LIMIT = 200;
|
|
60
80
|
/** A label is 5-9 words; no batch needs more than this many entries to
|
|
61
81
|
* produce one, and the cap keeps a 200-call programmatic batch from
|
|
62
82
|
* building an enormous prompt out of per-field-bounded pieces. */
|
|
@@ -65,7 +85,7 @@ const MAX_PROMPT_ENTRIES = 12;
|
|
|
65
85
|
* Builds the user prompt for a fast-model activity label. Pure — exported
|
|
66
86
|
* for direct testing of redaction and truncation behavior.
|
|
67
87
|
*/
|
|
68
|
-
function buildActivityLabelPrompt({ entries, charLimit, thinkingExcerpts, lastAssistantText, redaction }) {
|
|
88
|
+
function buildActivityLabelPrompt({ entries, charLimit, thinkingExcerpts, lastAssistantText, previousLabels, redaction }) {
|
|
69
89
|
const clip = truncateForLabel;
|
|
70
90
|
/** Reasoning and intent text can quote tool output verbatim — including
|
|
71
91
|
* output from EARLIER calls to a redacted tool that this batch does not
|
|
@@ -74,6 +94,14 @@ function buildActivityLabelPrompt({ entries, charLimit, thinkingExcerpts, lastAs
|
|
|
74
94
|
* scrub a quoted fragment out of free-form model prose. */
|
|
75
95
|
const excerptsRedacted = redaction != null && (redaction.enabled === false || redaction.redactedToolNames.size > 0);
|
|
76
96
|
const sections = [];
|
|
97
|
+
/** Previous labels are free-form model prose too, and per-agent overlays
|
|
98
|
+
* mean an earlier header may have been generated under ANOTHER agent's
|
|
99
|
+
* weaker policy — so they share the excerpts' wholesale drop rather than
|
|
100
|
+
* letting a handoff leak a looser agent's phrasing into this trace. */
|
|
101
|
+
if (!excerptsRedacted && previousLabels != null && previousLabels.length > 0) {
|
|
102
|
+
const recent = previousLabels.slice(-3).map(sanitizePreviousLabel).filter((label) => label.length > 0);
|
|
103
|
+
if (recent.length > 0) sections.push("Previous headers in this run (most recent last):\n" + recent.map((label) => `- ${label}`).join("\n"));
|
|
104
|
+
}
|
|
77
105
|
/** Intent text is free-form assistant prose that can quote a redacted
|
|
78
106
|
* tool result just as reasoning can, so it shares the excerpts' fate. */
|
|
79
107
|
if (!excerptsRedacted && lastAssistantText != null && lastAssistantText.length > 0) sections.push(`Intent (assistant's last message): ${clip(lastAssistantText, INPUT_CONTEXT_LIMIT)}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activityLabel.mjs","names":[],"sources":["../../../src/prompts/activityLabel.ts"],"sourcesContent":["import type { ResolvedLangfuseToolOutputTracingConfig } from '@/langfuseRuntimeContext';\nimport type { ActivityLabelToolEntry } from '@/types/activityLabel';\nimport { shouldRedactTool } from '@/langfuseToolOutputTracing';\n\n/**\n * Default system prompt for fast-model activity labeling.\n *\n * Style synthesized from Claude Code's tool-use summary prompt (git-subject\n * register, past tense, distinctive nouns) and claude.ai's observed group\n * headers (5–9 words describing a mixed reasoning + tool block, e.g.\n * \"Synthesized version data and curated comparative framework\").\n */\nexport const ACTIVITY_LABEL_PROMPT = `Write a short label describing what this block of agent activity accomplished. It appears as the header of a collapsed activity group in a chat UI.\n\nRules:\n- 5 to 9 words, past-tense verb first\n- Name the most distinctive subject (file, API, topic); drop articles and filler\n- Describe outcomes, not mechanics; if something failed, say so plainly\n- Output only the label — no quotes, no punctuation at the end, no preamble\n\nExamples:\n- Searched Node.js release notes and changelogs\n- Compared runtime versions across official sources\n- Fixed failing auth middleware tests\n- Read project config and dependency manifests\n- Attempted database migration, hit permission errors`;\n\n/** Truncates a serialized value for the label prompt. */\nexport function truncateForLabel(value: string, maxLength: number): string {\n if (value.length <= maxLength) {\n return value;\n }\n return value.slice(0, Math.max(0, maxLength - 1)) + '…';\n}\n\nconst ABORT_SERIALIZATION = Symbol('abort-label-serialization');\n\n/**\n * Serializes a tool value for the prompt WITHOUT materializing huge JSON:\n * the output is clipped to a few hundred characters anyway, so a multi-\n * megabyte tool result must not be stringified in full on the label path.\n * Strings clip immediately; structured values serialize under a character\n * budget and degrade to a shape summary once it is exhausted.\n */\nfunction serializeForLabel(value: unknown, limit: number): string {\n if (value == null) {\n return '';\n }\n if (typeof value === 'string') {\n return value.length > limit ? value.slice(0, limit + 1) : value;\n }\n let budget = limit * 4;\n try {\n return (\n JSON.stringify(value, (_key, nested: unknown) => {\n if (budget <= 0) {\n throw ABORT_SERIALIZATION;\n }\n if (typeof nested === 'string') {\n const clipped =\n nested.length > limit ? nested.slice(0, limit) : nested;\n budget -= clipped.length;\n return clipped;\n }\n budget -= 8;\n return nested;\n }) ?? ''\n );\n } catch (error) {\n if (error === ABORT_SERIALIZATION) {\n return Array.isArray(value) ? `[Array(${value.length})]` : '[Object]';\n }\n return String(value);\n }\n}\n\nconst INPUT_CONTEXT_LIMIT = 200;\nconst MAX_THINKING_EXCERPTS = 4;\n/** A label is 5-9 words; no batch needs more than this many entries to\n * produce one, and the cap keeps a 200-call programmatic batch from\n * building an enormous prompt out of per-field-bounded pieces. */\nconst MAX_PROMPT_ENTRIES = 12;\n\nexport type BuildActivityLabelPromptParams = {\n entries: ActivityLabelToolEntry[];\n charLimit: number;\n thinkingExcerpts?: string[];\n lastAssistantText?: string;\n /**\n * Resolved tool-output tracing policy. The label prompt becomes Langfuse\n * generation input, so outputs/errors excluded from tracing (global\n * disable or `redactedToolNames`) must never appear in it — the same\n * redaction the span processor applies to structured tool observations.\n */\n redaction?: ResolvedLangfuseToolOutputTracingConfig;\n};\n\n/**\n * Builds the user prompt for a fast-model activity label. Pure — exported\n * for direct testing of redaction and truncation behavior.\n */\nexport function buildActivityLabelPrompt({\n entries,\n charLimit,\n thinkingExcerpts,\n lastAssistantText,\n redaction,\n}: BuildActivityLabelPromptParams): string {\n const clip = truncateForLabel;\n /** Reasoning and intent text can quote tool output verbatim — including\n * output from EARLIER calls to a redacted tool that this batch does not\n * contain — so any active policy (global disable or a configured\n * redacted-name list) drops both wholesale. There is no reliable way to\n * scrub a quoted fragment out of free-form model prose. */\n const excerptsRedacted =\n redaction != null &&\n (redaction.enabled === false || redaction.redactedToolNames.size > 0);\n const sections: string[] = [];\n /** Intent text is free-form assistant prose that can quote a redacted\n * tool result just as reasoning can, so it shares the excerpts' fate. */\n if (\n !excerptsRedacted &&\n lastAssistantText != null &&\n lastAssistantText.length > 0\n ) {\n sections.push(\n `Intent (assistant's last message): ${clip(lastAssistantText, INPUT_CONTEXT_LIMIT)}`\n );\n }\n if (\n !excerptsRedacted &&\n thinkingExcerpts != null &&\n thinkingExcerpts.length > 0\n ) {\n sections.push(\n 'Reasoning excerpts:\\n' +\n thinkingExcerpts\n .slice(0, MAX_THINKING_EXCERPTS)\n .map((excerpt) => `- ${clip(excerpt, charLimit)}`)\n .join('\\n')\n );\n }\n if (entries.length > 0) {\n const shown = entries.slice(0, MAX_PROMPT_ENTRIES);\n const omitted = entries.length - shown.length;\n sections.push(\n 'Tool calls:\\n' +\n shown\n .map((entry) => {\n const input = clip(\n serializeForLabel(entry.toolInput, charLimit),\n charLimit\n );\n const redacted =\n redaction != null && shouldRedactTool(entry.toolName, redaction);\n let outcome: string;\n if (redacted) {\n outcome = redaction.redactionText;\n } else if (entry.status === 'error') {\n outcome = `ERROR: ${clip(entry.error ?? 'unknown error', charLimit)}`;\n } else {\n outcome = clip(\n serializeForLabel(entry.toolOutput, charLimit),\n charLimit\n );\n }\n return `- ${entry.toolName}(${input}) → ${outcome}`;\n })\n .join('\\n') +\n (omitted > 0\n ? `\\n- …and ${omitted} more tool ${omitted === 1 ? 'call' : 'calls'}`\n : '')\n );\n }\n sections.push('Label:');\n return sections.join('\\n\\n');\n}\n"],"mappings":";;;;;;;;;;AAYA,MAAa,wBAAwB;;;;;;;;;;;;;;;AAgBrC,SAAgB,iBAAiB,OAAe,WAA2B;CACzE,IAAI,MAAM,UAAU,WAClB,OAAO;CAET,OAAO,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,YAAY,CAAC,CAAC,IAAI;AACtD;AAEA,MAAM,sBAAsB,OAAO,2BAA2B;;;;;;;;AAS9D,SAAS,kBAAkB,OAAgB,OAAuB;CAChE,IAAI,SAAS,MACX,OAAO;CAET,IAAI,OAAO,UAAU,UACnB,OAAO,MAAM,SAAS,QAAQ,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI;CAE5D,IAAI,SAAS,QAAQ;CACrB,IAAI;EACF,OACE,KAAK,UAAU,QAAQ,MAAM,WAAoB;GAC/C,IAAI,UAAU,GACZ,MAAM;GAER,IAAI,OAAO,WAAW,UAAU;IAC9B,MAAM,UACJ,OAAO,SAAS,QAAQ,OAAO,MAAM,GAAG,KAAK,IAAI;IACnD,UAAU,QAAQ;IAClB,OAAO;GACT;GACA,UAAU;GACV,OAAO;EACT,CAAC,KAAK;CAEV,SAAS,OAAO;EACd,IAAI,UAAU,qBACZ,OAAO,MAAM,QAAQ,KAAK,IAAI,UAAU,MAAM,OAAO,MAAM;EAE7D,OAAO,OAAO,KAAK;CACrB;AACF;AAEA,MAAM,sBAAsB;AAC5B,MAAM,wBAAwB;;;;AAI9B,MAAM,qBAAqB;;;;;AAoB3B,SAAgB,yBAAyB,EACvC,SACA,WACA,kBACA,mBACA,aACyC;CACzC,MAAM,OAAO;;;;;;CAMb,MAAM,mBACJ,aAAa,SACZ,UAAU,YAAY,SAAS,UAAU,kBAAkB,OAAO;CACrE,MAAM,WAAqB,CAAC;;;CAG5B,IACE,CAAC,oBACD,qBAAqB,QACrB,kBAAkB,SAAS,GAE3B,SAAS,KACP,sCAAsC,KAAK,mBAAmB,mBAAmB,GACnF;CAEF,IACE,CAAC,oBACD,oBAAoB,QACpB,iBAAiB,SAAS,GAE1B,SAAS,KACP,0BACE,iBACG,MAAM,GAAG,qBAAqB,CAAC,CAC/B,KAAK,YAAY,KAAK,KAAK,SAAS,SAAS,GAAG,CAAC,CACjD,KAAK,IAAI,CAChB;CAEF,IAAI,QAAQ,SAAS,GAAG;EACtB,MAAM,QAAQ,QAAQ,MAAM,GAAG,kBAAkB;EACjD,MAAM,UAAU,QAAQ,SAAS,MAAM;EACvC,SAAS,KACP,kBACE,MACG,KAAK,UAAU;GACd,MAAM,QAAQ,KACZ,kBAAkB,MAAM,WAAW,SAAS,GAC5C,SACF;GACA,MAAM,WACJ,aAAa,QAAQ,iBAAiB,MAAM,UAAU,SAAS;GACjE,IAAI;GACJ,IAAI,UACF,UAAU,UAAU;QACf,IAAI,MAAM,WAAW,SAC1B,UAAU,UAAU,KAAK,MAAM,SAAS,iBAAiB,SAAS;QAElE,UAAU,KACR,kBAAkB,MAAM,YAAY,SAAS,GAC7C,SACF;GAEF,OAAO,KAAK,MAAM,SAAS,GAAG,MAAM,MAAM;EAC5C,CAAC,CAAC,CACD,KAAK,IAAI,KACX,UAAU,IACP,YAAY,QAAQ,aAAa,YAAY,IAAI,SAAS,YAC1D,GACR;CACF;CACA,SAAS,KAAK,QAAQ;CACtB,OAAO,SAAS,KAAK,MAAM;AAC7B"}
|
|
1
|
+
{"version":3,"file":"activityLabel.mjs","names":[],"sources":["../../../src/prompts/activityLabel.ts"],"sourcesContent":["import type { ResolvedLangfuseToolOutputTracingConfig } from '@/langfuseRuntimeContext';\nimport type { ActivityLabelToolEntry } from '@/types/activityLabel';\nimport { shouldRedactTool } from '@/langfuseToolOutputTracing';\n\n/**\n * Default system prompt for fast-model activity labeling.\n *\n * Style synthesized from Claude Code's tool-use summary prompt (git-subject\n * register, past tense, distinctive nouns) and claude.ai's observed group\n * headers (5–9 words describing a mixed reasoning + tool block, e.g.\n * \"Synthesized version data and curated comparative framework\").\n */\nexport const ACTIVITY_LABEL_PROMPT = `Write a short label describing what this block of agent activity accomplished. It appears as the header of a collapsed activity group in a chat UI.\n\nRules:\n- 5 to 9 words, past-tense verb first\n- Name the most distinctive subject (file, API, topic); drop articles and filler\n- Describe outcomes, not mechanics; if something failed, say so plainly\n- Output only the label — no quotes, no punctuation at the end, no preamble\n\nExamples:\n- Searched Node.js release notes and changelogs\n- Compared runtime versions across official sources\n- Fixed failing auth middleware tests\n- Read project config and dependency manifests\n- Attempted database migration, hit permission errors`;\n\n/** Truncates a serialized value for the label prompt. */\nexport function truncateForLabel(value: string, maxLength: number): string {\n if (value.length <= maxLength) {\n return value;\n }\n return value.slice(0, Math.max(0, maxLength - 1)) + '…';\n}\n\n/**\n * Reduces a committed label to bounded single-line data.\n *\n * Sections in this prompt are delimited by blank lines, so a label carrying\n * embedded newlines could otherwise forge an apparent `Tool calls:` or\n * `Label:` section. Unlike every other input here, previous labels re-enter\n * the prompt on EVERY later batch, so one malformed result — plain model\n * noncompliance, or injection surfacing through a tool result — would\n * persistently steer unrelated later labels rather than affecting one. The\n * clip bounds the same way `lastAssistantText` and reasoning excerpts are\n * bounded: oversized headers must not inflate later requests past the fast\n * model's window and starve the run of labels entirely.\n */\nfunction sanitizePreviousLabel(label: string): string {\n return truncateForLabel(label.replace(/\\s+/g, ' ').trim(), PREVIOUS_LABEL_LIMIT);\n}\n\nconst ABORT_SERIALIZATION = Symbol('abort-label-serialization');\n\n/**\n * Serializes a tool value for the prompt WITHOUT materializing huge JSON:\n * the output is clipped to a few hundred characters anyway, so a multi-\n * megabyte tool result must not be stringified in full on the label path.\n * Strings clip immediately; structured values serialize under a character\n * budget and degrade to a shape summary once it is exhausted.\n */\nfunction serializeForLabel(value: unknown, limit: number): string {\n if (value == null) {\n return '';\n }\n if (typeof value === 'string') {\n return value.length > limit ? value.slice(0, limit + 1) : value;\n }\n let budget = limit * 4;\n try {\n return (\n JSON.stringify(value, (_key, nested: unknown) => {\n if (budget <= 0) {\n throw ABORT_SERIALIZATION;\n }\n if (typeof nested === 'string') {\n const clipped =\n nested.length > limit ? nested.slice(0, limit) : nested;\n budget -= clipped.length;\n return clipped;\n }\n budget -= 8;\n return nested;\n }) ?? ''\n );\n } catch (error) {\n if (error === ABORT_SERIALIZATION) {\n return Array.isArray(value) ? `[Array(${value.length})]` : '[Object]';\n }\n return String(value);\n }\n}\n\nconst INPUT_CONTEXT_LIMIT = 200;\nconst MAX_THINKING_EXCERPTS = 4;\nconst MAX_PREVIOUS_LABELS = 3;\n/** Per-label bound. A header is 5-9 words; anything past this is\n * noncompliance or payload, and previous labels are the one input that\n * RE-ENTERS the prompt on every later batch of the run. */\nconst PREVIOUS_LABEL_LIMIT = 200;\n/** A label is 5-9 words; no batch needs more than this many entries to\n * produce one, and the cap keeps a 200-call programmatic batch from\n * building an enormous prompt out of per-field-bounded pieces. */\nconst MAX_PROMPT_ENTRIES = 12;\n\nexport type BuildActivityLabelPromptParams = {\n entries: ActivityLabelToolEntry[];\n charLimit: number;\n thinkingExcerpts?: string[];\n lastAssistantText?: string;\n /**\n * Headers already committed for earlier batches in this run, in run order\n * with the most recent last. Rendered ahead of the block context so the\n * label continues the run's story instead of restating a line the user is\n * already reading. Capped at {@link MAX_PREVIOUS_LABELS}.\n */\n previousLabels?: string[];\n /**\n * Resolved tool-output tracing policy. The label prompt becomes Langfuse\n * generation input, so outputs/errors excluded from tracing (global\n * disable or `redactedToolNames`) must never appear in it — the same\n * redaction the span processor applies to structured tool observations.\n */\n redaction?: ResolvedLangfuseToolOutputTracingConfig;\n};\n\n/**\n * Builds the user prompt for a fast-model activity label. Pure — exported\n * for direct testing of redaction and truncation behavior.\n */\nexport function buildActivityLabelPrompt({\n entries,\n charLimit,\n thinkingExcerpts,\n lastAssistantText,\n previousLabels,\n redaction,\n}: BuildActivityLabelPromptParams): string {\n const clip = truncateForLabel;\n /** Reasoning and intent text can quote tool output verbatim — including\n * output from EARLIER calls to a redacted tool that this batch does not\n * contain — so any active policy (global disable or a configured\n * redacted-name list) drops both wholesale. There is no reliable way to\n * scrub a quoted fragment out of free-form model prose. */\n const excerptsRedacted =\n redaction != null &&\n (redaction.enabled === false || redaction.redactedToolNames.size > 0);\n const sections: string[] = [];\n /** Previous labels are free-form model prose too, and per-agent overlays\n * mean an earlier header may have been generated under ANOTHER agent's\n * weaker policy — so they share the excerpts' wholesale drop rather than\n * letting a handoff leak a looser agent's phrasing into this trace. */\n if (!excerptsRedacted && previousLabels != null && previousLabels.length > 0) {\n const recent = previousLabels\n .slice(-MAX_PREVIOUS_LABELS)\n .map(sanitizePreviousLabel)\n /** A label that sanitizes to nothing carries no story to continue;\n * rendering it would leave a bare bullet implying a missing header. */\n .filter((label) => label.length > 0);\n if (recent.length > 0) {\n sections.push(\n 'Previous headers in this run (most recent last):\\n' +\n recent.map((label) => `- ${label}`).join('\\n')\n );\n }\n }\n /** Intent text is free-form assistant prose that can quote a redacted\n * tool result just as reasoning can, so it shares the excerpts' fate. */\n if (\n !excerptsRedacted &&\n lastAssistantText != null &&\n lastAssistantText.length > 0\n ) {\n sections.push(\n `Intent (assistant's last message): ${clip(lastAssistantText, INPUT_CONTEXT_LIMIT)}`\n );\n }\n if (\n !excerptsRedacted &&\n thinkingExcerpts != null &&\n thinkingExcerpts.length > 0\n ) {\n sections.push(\n 'Reasoning excerpts:\\n' +\n thinkingExcerpts\n .slice(0, MAX_THINKING_EXCERPTS)\n .map((excerpt) => `- ${clip(excerpt, charLimit)}`)\n .join('\\n')\n );\n }\n if (entries.length > 0) {\n const shown = entries.slice(0, MAX_PROMPT_ENTRIES);\n const omitted = entries.length - shown.length;\n sections.push(\n 'Tool calls:\\n' +\n shown\n .map((entry) => {\n const input = clip(\n serializeForLabel(entry.toolInput, charLimit),\n charLimit\n );\n const redacted =\n redaction != null && shouldRedactTool(entry.toolName, redaction);\n let outcome: string;\n if (redacted) {\n outcome = redaction.redactionText;\n } else if (entry.status === 'error') {\n outcome = `ERROR: ${clip(entry.error ?? 'unknown error', charLimit)}`;\n } else {\n outcome = clip(\n serializeForLabel(entry.toolOutput, charLimit),\n charLimit\n );\n }\n return `- ${entry.toolName}(${input}) → ${outcome}`;\n })\n .join('\\n') +\n (omitted > 0\n ? `\\n- …and ${omitted} more tool ${omitted === 1 ? 'call' : 'calls'}`\n : '')\n );\n }\n sections.push('Label:');\n return sections.join('\\n\\n');\n}\n"],"mappings":"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"}
|
package/dist/esm/run.mjs
CHANGED
|
@@ -1059,7 +1059,7 @@ var Run = class Run {
|
|
|
1059
1059
|
* comes from `lastAssistantText`, content from reasoning excerpts and
|
|
1060
1060
|
* tool entries.
|
|
1061
1061
|
*/
|
|
1062
|
-
async generateActivityLabel({ provider, clientOptions, entries, thinkingExcerpts, lastAssistantText, prompt, charLimit = 600, chainOptions, traceSeed, agentId }) {
|
|
1062
|
+
async generateActivityLabel({ provider, clientOptions, entries, thinkingExcerpts, lastAssistantText, previousLabels, prompt, charLimit = 600, chainOptions, traceSeed, agentId }) {
|
|
1063
1063
|
if (entries.length === 0 && !(thinkingExcerpts && thinkingExcerpts.length > 0)) return {};
|
|
1064
1064
|
const labelSeq = ++this.activityLabelSeq;
|
|
1065
1065
|
/** Resolve the LABELED agent's context: its Langfuse overlay carries the
|
|
@@ -1151,6 +1151,7 @@ var Run = class Run {
|
|
|
1151
1151
|
charLimit,
|
|
1152
1152
|
thinkingExcerpts,
|
|
1153
1153
|
lastAssistantText,
|
|
1154
|
+
previousLabels,
|
|
1154
1155
|
redaction
|
|
1155
1156
|
});
|
|
1156
1157
|
const model = initializeModel({
|
|
@@ -1182,7 +1183,11 @@ var Run = class Run {
|
|
|
1182
1183
|
let text = "";
|
|
1183
1184
|
if (typeof content === "string") text = content;
|
|
1184
1185
|
else if (Array.isArray(content)) text = content.map((block) => typeof block === "string" ? block : block.text ?? "").join("");
|
|
1185
|
-
|
|
1186
|
+
/** Collapsed to one line at the source: a header renders as a single
|
|
1187
|
+
* row, and hosts feed committed labels back as continuity context —
|
|
1188
|
+
* so a multi-line result would carry its line breaks into every later
|
|
1189
|
+
* prompt in the run. */
|
|
1190
|
+
return text.replace(/\s+/g, " ").trim().replace(/^["']|["']$/g, "");
|
|
1186
1191
|
};
|
|
1187
1192
|
try {
|
|
1188
1193
|
let response;
|