@sentry/junior 0.48.0 → 0.50.0
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/app.d.ts +3 -1
- package/dist/app.js +825 -176
- package/dist/{chunk-ELM6HJ6S.js → chunk-AQ4RO2WA.js} +1 -1
- package/dist/{chunk-ZUUJTQ2H.js → chunk-AYM42AN3.js} +42 -15
- package/dist/{chunk-BCG3I2T2.js → chunk-UKR24HLJ.js} +143 -5
- package/dist/cli/check.js +43 -2
- package/dist/cli/init.js +31 -0
- package/dist/cli/snapshot-warmup.js +2 -2
- package/dist/nitro.d.ts +4 -1
- package/dist/nitro.js +6 -6
- package/dist/types-X_iCClPb.d.ts +75 -0
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
findSkillByName,
|
|
4
4
|
loadSkillsByName,
|
|
5
5
|
parseSkillInvocation
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-AYM42AN3.js";
|
|
7
7
|
import {
|
|
8
8
|
GEN_AI_PROVIDER_NAME,
|
|
9
9
|
MISSING_GATEWAY_CREDENTIALS_ERROR,
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
runNonInteractiveCommand,
|
|
32
32
|
sandboxSkillDir,
|
|
33
33
|
sandboxSkillFile
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-AQ4RO2WA.js";
|
|
35
35
|
import {
|
|
36
36
|
CredentialUnavailableError,
|
|
37
37
|
buildOAuthTokenRequest,
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
resolveAuthTokenPlaceholder,
|
|
62
62
|
resolvePluginCommandEnv,
|
|
63
63
|
serializeGenAiAttribute,
|
|
64
|
+
setPluginConfig,
|
|
64
65
|
setSpanAttributes,
|
|
65
66
|
setSpanStatus,
|
|
66
67
|
setTags,
|
|
@@ -68,7 +69,7 @@ import {
|
|
|
68
69
|
toOptionalString,
|
|
69
70
|
withContext,
|
|
70
71
|
withSpan
|
|
71
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-UKR24HLJ.js";
|
|
72
73
|
import {
|
|
73
74
|
sentry_exports
|
|
74
75
|
} from "./chunk-Z3YD6NHK.js";
|
|
@@ -2448,6 +2449,12 @@ import { Agent as Agent2 } from "@mariozechner/pi-agent-core";
|
|
|
2448
2449
|
import fs from "fs";
|
|
2449
2450
|
import path2 from "path";
|
|
2450
2451
|
|
|
2452
|
+
// src/chat/interruption-marker.ts
|
|
2453
|
+
var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
|
|
2454
|
+
function getInterruptionMarker() {
|
|
2455
|
+
return INTERRUPTED_MARKER;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2451
2458
|
// src/chat/slack/status-format.ts
|
|
2452
2459
|
var SLACK_STATUS_MAX_LENGTH = 50;
|
|
2453
2460
|
function truncateStatusText(text) {
|
|
@@ -2511,7 +2518,6 @@ function normalizeSlackStatusText(text) {
|
|
|
2511
2518
|
var MAX_INLINE_CHARS = 2200;
|
|
2512
2519
|
var MAX_INLINE_LINES = 45;
|
|
2513
2520
|
var CONTINUED_MARKER = "\n\n[Continued below]";
|
|
2514
|
-
var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
|
|
2515
2521
|
function countSlackLines(text) {
|
|
2516
2522
|
if (!text) {
|
|
2517
2523
|
return 0;
|
|
@@ -2678,10 +2684,10 @@ function splitSlackReplyText(text, options) {
|
|
|
2678
2684
|
const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
2679
2685
|
let remaining = normalized;
|
|
2680
2686
|
while (remaining) {
|
|
2681
|
-
const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining,
|
|
2687
|
+
const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
|
|
2682
2688
|
if (fitsFinalChunk) {
|
|
2683
2689
|
chunks.push(
|
|
2684
|
-
options?.interrupted ? appendSlackSuffix(remaining,
|
|
2690
|
+
options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
|
|
2685
2691
|
);
|
|
2686
2692
|
break;
|
|
2687
2693
|
}
|
|
@@ -2831,26 +2837,50 @@ function renderTag(tag, lines) {
|
|
|
2831
2837
|
function renderTagBlock(tag, content) {
|
|
2832
2838
|
return [`<${tag}>`, content, `</${tag}>`].join("\n");
|
|
2833
2839
|
}
|
|
2834
|
-
function
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
lines.push(` <
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2840
|
+
function formatSkillEntry(skill) {
|
|
2841
|
+
const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
|
|
2842
|
+
const lines = [];
|
|
2843
|
+
lines.push(" <skill>");
|
|
2844
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
2845
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
2846
|
+
lines.push(` <location>${escapeXml(skillLocation)}</location>`);
|
|
2847
|
+
if (skill.pluginProvider) {
|
|
2848
|
+
lines.push(` <provider>${escapeXml(skill.pluginProvider)}</provider>`);
|
|
2849
|
+
}
|
|
2850
|
+
lines.push(" </skill>");
|
|
2851
|
+
return lines;
|
|
2852
|
+
}
|
|
2853
|
+
function formatAvailableSkillsForPrompt(skills, invocation) {
|
|
2854
|
+
const autoSelectable = skills.filter(
|
|
2855
|
+
(s) => s.disableModelInvocation !== true
|
|
2856
|
+
);
|
|
2857
|
+
const invokedExplicitOnly = invocation ? skills.filter(
|
|
2858
|
+
(s) => s.disableModelInvocation === true && s.name === invocation.skillName
|
|
2859
|
+
) : [];
|
|
2860
|
+
const sections = [];
|
|
2861
|
+
const available = [
|
|
2862
|
+
"<available-skills>",
|
|
2863
|
+
...autoSelectable.length > 0 ? [
|
|
2864
|
+
"Scan before answering. Load the most specific matching skill; do not answer from memory when a skill fits. If none fits, do not load a skill."
|
|
2865
|
+
] : []
|
|
2866
|
+
];
|
|
2867
|
+
for (const skill of autoSelectable) {
|
|
2868
|
+
available.push(...formatSkillEntry(skill));
|
|
2869
|
+
}
|
|
2870
|
+
available.push("</available-skills>");
|
|
2871
|
+
sections.push(available.join("\n"));
|
|
2872
|
+
if (invokedExplicitOnly.length > 0) {
|
|
2873
|
+
const userCallable = [
|
|
2874
|
+
"<user-callable-skills>",
|
|
2875
|
+
"The user's current message explicitly references this skill by name. Load it when relevant to the request."
|
|
2876
|
+
];
|
|
2877
|
+
for (const skill of invokedExplicitOnly) {
|
|
2878
|
+
userCallable.push(...formatSkillEntry(skill));
|
|
2849
2879
|
}
|
|
2850
|
-
|
|
2880
|
+
userCallable.push("</user-callable-skills>");
|
|
2881
|
+
sections.push(userCallable.join("\n"));
|
|
2851
2882
|
}
|
|
2852
|
-
|
|
2853
|
-
return lines.join("\n");
|
|
2883
|
+
return sections.join("\n");
|
|
2854
2884
|
}
|
|
2855
2885
|
function formatLoadedSkillsForPrompt(skills) {
|
|
2856
2886
|
if (skills.length === 0) {
|
|
@@ -2878,7 +2908,7 @@ function formatProviderCatalogForPrompt() {
|
|
|
2878
2908
|
return null;
|
|
2879
2909
|
}
|
|
2880
2910
|
const lines = [
|
|
2881
|
-
"Config keys and default targets per provider; use after a skill is loaded."
|
|
2911
|
+
"Config keys and default targets per provider; use after a skill is loaded. Run authenticated provider commands directly after resolving target defaults; let the runtime handle auth pauses/resumes."
|
|
2882
2912
|
];
|
|
2883
2913
|
for (const provider of providers) {
|
|
2884
2914
|
lines.push(`- provider: ${escapeXml(provider.name)}`);
|
|
@@ -2898,7 +2928,7 @@ function formatActiveMcpCatalogsForPrompt(catalogs) {
|
|
|
2898
2928
|
return null;
|
|
2899
2929
|
}
|
|
2900
2930
|
const lines = [
|
|
2901
|
-
"Active MCP provider catalogs are available through `searchMcpTools`. Call it with provider to list descriptors or with query to narrow results, then pass the exact returned `tool_name` to `callMcpTool`."
|
|
2931
|
+
"Active MCP provider catalogs are available through `searchMcpTools`. Call it with provider to list descriptors or with query to narrow results, then pass the exact returned `tool_name` to `callMcpTool`. Put provider fields inside `arguments`."
|
|
2902
2932
|
];
|
|
2903
2933
|
for (const catalog of catalogs) {
|
|
2904
2934
|
lines.push(" <catalog>");
|
|
@@ -3008,12 +3038,8 @@ var TOOL_CALL_STYLE_RULES = [
|
|
|
3008
3038
|
"- Keep tool-call explanations separate from final answers; final answers should report results, evidence, or blockers."
|
|
3009
3039
|
];
|
|
3010
3040
|
var SKILL_POLICY_RULES = [
|
|
3011
|
-
"-
|
|
3012
|
-
"-
|
|
3013
|
-
"- For explicit `/skill` triggers, treat that skill as selected unless the tool says it is unavailable.",
|
|
3014
|
-
"- For active MCP catalogs, use `searchMcpTools` to inspect descriptors before `callMcpTool`; pass exact returned `tool_name` values and put provider fields inside `arguments`.",
|
|
3015
|
-
"- Run authenticated provider commands directly after resolving target defaults; let the runtime handle auth pauses/resumes.",
|
|
3016
|
-
"- Run `jr-rpc config get|set|unset|list` as standalone bash commands for conversation-scoped provider defaults; do not chain them with `cd`, `&&`, pipes, or provider commands."
|
|
3041
|
+
"- Only load skills listed in `<available-skills>`, `<user-callable-skills>`, or named by `<explicit-skill-trigger>`. Never guess or invent a skill name.",
|
|
3042
|
+
"- Load one skill at a time. After `loadSkill`, follow the instructions in `<loaded-skills>`."
|
|
3017
3043
|
];
|
|
3018
3044
|
var EXECUTION_CONTRACT_RULES = [
|
|
3019
3045
|
"- Actionable request: act in this turn.",
|
|
@@ -3026,7 +3052,7 @@ var EXECUTION_CONTRACT_RULES = [
|
|
|
3026
3052
|
var CONVERSATION_RULES = [
|
|
3027
3053
|
"- In thread follow-ups, answer from prior thread context; do not repeat resolved clarifying questions.",
|
|
3028
3054
|
"- Preserve attribution roles from thread context: the requester is the person asking now, which may differ from the original reporter or subject.",
|
|
3029
|
-
"-
|
|
3055
|
+
"- Runtime owns continuation and authorization notices; on resumed turns, answer with the final requested content only."
|
|
3030
3056
|
];
|
|
3031
3057
|
var SLACK_ACTION_RULES = [
|
|
3032
3058
|
"- Context-bound Slack tools use runtime-owned targets; do not invent channel, canvas, list, or message IDs.",
|
|
@@ -3123,7 +3149,7 @@ function buildContextSection(params) {
|
|
|
3123
3149
|
if (configLines) {
|
|
3124
3150
|
blocks.push(
|
|
3125
3151
|
renderTag("configuration", [
|
|
3126
|
-
"Ambient provider defaults; explicit targets win.",
|
|
3152
|
+
"Ambient provider defaults; explicit targets win. Run `jr-rpc config get|set|unset|list` as standalone bash commands; do not chain with `cd`, `&&`, pipes, or provider commands.",
|
|
3127
3153
|
...configLines
|
|
3128
3154
|
])
|
|
3129
3155
|
);
|
|
@@ -3135,16 +3161,21 @@ function buildContextSection(params) {
|
|
|
3135
3161
|
]);
|
|
3136
3162
|
}
|
|
3137
3163
|
if (params.invocation) {
|
|
3138
|
-
blocks.push(
|
|
3139
|
-
|
|
3140
|
-
|
|
3164
|
+
blocks.push(
|
|
3165
|
+
renderTag("explicit-skill-trigger", [
|
|
3166
|
+
"Treat this skill as selected. Load it unless the tool says it is unavailable.",
|
|
3167
|
+
`/${escapeXml(params.invocation.skillName)}`
|
|
3168
|
+
])
|
|
3169
|
+
);
|
|
3141
3170
|
}
|
|
3142
3171
|
const body = blocks.map((block) => block.join("\n")).join("\n\n");
|
|
3143
3172
|
return renderTagBlock("context", body);
|
|
3144
3173
|
}
|
|
3145
3174
|
function buildCapabilitiesSection(params) {
|
|
3146
3175
|
const blocks = [];
|
|
3147
|
-
blocks.push(
|
|
3176
|
+
blocks.push(
|
|
3177
|
+
formatAvailableSkillsForPrompt(params.availableSkills, params.invocation)
|
|
3178
|
+
);
|
|
3148
3179
|
blocks.push(formatLoadedSkillsForPrompt(params.activeSkills));
|
|
3149
3180
|
const activeCatalogs = formatActiveMcpCatalogsForPrompt(
|
|
3150
3181
|
params.activeMcpCatalogs
|
|
@@ -3181,6 +3212,7 @@ function buildTurnContextPrompt(params) {
|
|
|
3181
3212
|
availableSkills: params.availableSkills,
|
|
3182
3213
|
activeSkills: params.activeSkills,
|
|
3183
3214
|
activeMcpCatalogs: params.activeMcpCatalogs ?? [],
|
|
3215
|
+
invocation: params.invocation,
|
|
3184
3216
|
toolGuidance: params.toolGuidance ?? []
|
|
3185
3217
|
}),
|
|
3186
3218
|
buildContextSection({
|
|
@@ -4442,6 +4474,16 @@ function createBashTool() {
|
|
|
4442
4474
|
|
|
4443
4475
|
// src/chat/tools/sandbox/file-utils.ts
|
|
4444
4476
|
import path4 from "path";
|
|
4477
|
+
|
|
4478
|
+
// src/chat/tools/execution/tool-input-error.ts
|
|
4479
|
+
var ToolInputError = class extends Error {
|
|
4480
|
+
constructor(message, options) {
|
|
4481
|
+
super(message, options);
|
|
4482
|
+
this.name = "ToolInputError";
|
|
4483
|
+
}
|
|
4484
|
+
};
|
|
4485
|
+
|
|
4486
|
+
// src/chat/tools/sandbox/file-utils.ts
|
|
4445
4487
|
var MAX_TEXT_CHARS = 6e4;
|
|
4446
4488
|
var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
4447
4489
|
function positiveInteger(value) {
|
|
@@ -4534,7 +4576,7 @@ function resolveWorkspacePath(input, fallback = ".") {
|
|
|
4534
4576
|
const absolute = requested.startsWith("/") ? requested : path4.posix.join(SANDBOX_WORKSPACE_ROOT, requested);
|
|
4535
4577
|
const normalized = path4.posix.normalize(absolute);
|
|
4536
4578
|
if (normalized !== SANDBOX_WORKSPACE_ROOT && !normalized.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)) {
|
|
4537
|
-
throw new
|
|
4579
|
+
throw new ToolInputError(
|
|
4538
4580
|
`Path must stay within ${SANDBOX_WORKSPACE_ROOT}: ${requested}`
|
|
4539
4581
|
);
|
|
4540
4582
|
}
|
|
@@ -4733,16 +4775,16 @@ function buildCompactDiff(oldContent, newContent) {
|
|
|
4733
4775
|
}
|
|
4734
4776
|
function validateAndApplyTextEdits(content, edits, targetName) {
|
|
4735
4777
|
if (!Array.isArray(edits) || edits.length === 0) {
|
|
4736
|
-
throw new
|
|
4778
|
+
throw new ToolInputError(`${targetName} requires at least one edit.`);
|
|
4737
4779
|
}
|
|
4738
4780
|
const normalizedEdits = edits.map((edit, index) => {
|
|
4739
4781
|
if (typeof edit.oldText !== "string" || edit.oldText.length === 0) {
|
|
4740
|
-
throw new
|
|
4782
|
+
throw new ToolInputError(
|
|
4741
4783
|
`edits[${index}].oldText must not be empty in ${targetName}.`
|
|
4742
4784
|
);
|
|
4743
4785
|
}
|
|
4744
4786
|
if (typeof edit.newText !== "string") {
|
|
4745
|
-
throw new
|
|
4787
|
+
throw new ToolInputError(
|
|
4746
4788
|
`edits[${index}].newText must be a string in ${targetName}.`
|
|
4747
4789
|
);
|
|
4748
4790
|
}
|
|
@@ -4756,13 +4798,13 @@ function validateAndApplyTextEdits(content, edits, targetName) {
|
|
|
4756
4798
|
const edit = normalizedEdits[index];
|
|
4757
4799
|
const matchIndex = content.indexOf(edit.oldText);
|
|
4758
4800
|
if (matchIndex === -1) {
|
|
4759
|
-
throw new
|
|
4801
|
+
throw new ToolInputError(
|
|
4760
4802
|
`Could not find edits[${index}] in ${targetName}. oldText must match exactly including whitespace and newlines.`
|
|
4761
4803
|
);
|
|
4762
4804
|
}
|
|
4763
4805
|
const occurrences = countOccurrences(content, edit.oldText);
|
|
4764
4806
|
if (occurrences > 1) {
|
|
4765
|
-
throw new
|
|
4807
|
+
throw new ToolInputError(
|
|
4766
4808
|
`Found ${occurrences} occurrences of edits[${index}] in ${targetName}. Each oldText must be unique.`
|
|
4767
4809
|
);
|
|
4768
4810
|
}
|
|
@@ -4778,7 +4820,7 @@ function validateAndApplyTextEdits(content, edits, targetName) {
|
|
|
4778
4820
|
const previous = matchedEdits[index - 1];
|
|
4779
4821
|
const current = matchedEdits[index];
|
|
4780
4822
|
if (previous.matchIndex + previous.matchLength > current.matchIndex) {
|
|
4781
|
-
throw new
|
|
4823
|
+
throw new ToolInputError(
|
|
4782
4824
|
`edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${targetName}. Merge overlapping replacements into one edit.`
|
|
4783
4825
|
);
|
|
4784
4826
|
}
|
|
@@ -4789,7 +4831,7 @@ function validateAndApplyTextEdits(content, edits, targetName) {
|
|
|
4789
4831
|
newContent = newContent.slice(0, edit.matchIndex) + edit.newText + newContent.slice(edit.matchIndex + edit.matchLength);
|
|
4790
4832
|
}
|
|
4791
4833
|
if (newContent === content) {
|
|
4792
|
-
throw new
|
|
4834
|
+
throw new ToolInputError(`No changes made to ${targetName}.`);
|
|
4793
4835
|
}
|
|
4794
4836
|
return { baseContent: content, newContent };
|
|
4795
4837
|
}
|
|
@@ -4801,7 +4843,17 @@ function prepareEditFileArguments(input) {
|
|
|
4801
4843
|
}
|
|
4802
4844
|
async function editFile(params) {
|
|
4803
4845
|
const filePath = resolveWorkspacePath(params.path);
|
|
4804
|
-
|
|
4846
|
+
let rawContent;
|
|
4847
|
+
try {
|
|
4848
|
+
rawContent = await params.fs.readFile(filePath, { encoding: "utf8" });
|
|
4849
|
+
} catch (error) {
|
|
4850
|
+
if (isMissingPathError(error)) {
|
|
4851
|
+
throw new ToolInputError(`File not found: ${params.path}`, {
|
|
4852
|
+
cause: error
|
|
4853
|
+
});
|
|
4854
|
+
}
|
|
4855
|
+
throw error;
|
|
4856
|
+
}
|
|
4805
4857
|
const { bom, text } = stripBom(rawContent);
|
|
4806
4858
|
const lineEnding = detectLineEnding(text);
|
|
4807
4859
|
const normalizedContent = normalizeToLf(text);
|
|
@@ -4993,7 +5045,16 @@ async function grepFiles(params) {
|
|
|
4993
5045
|
const root = resolveWorkspacePath(params.path);
|
|
4994
5046
|
const limit = positiveInteger(params.limit) ?? DEFAULT_GREP_LIMIT;
|
|
4995
5047
|
const context = positiveInteger(params.context) ?? 0;
|
|
4996
|
-
|
|
5048
|
+
let regex;
|
|
5049
|
+
if (!params.literal) {
|
|
5050
|
+
try {
|
|
5051
|
+
regex = new RegExp(params.pattern, params.ignoreCase ? "i" : "");
|
|
5052
|
+
} catch (error) {
|
|
5053
|
+
throw new ToolInputError(`Invalid regex pattern: ${params.pattern}`, {
|
|
5054
|
+
cause: error
|
|
5055
|
+
});
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
4997
5058
|
const { files, missingPath, missingRoot } = await collectFiles({
|
|
4998
5059
|
fs: params.fs,
|
|
4999
5060
|
root,
|
|
@@ -5311,7 +5372,7 @@ async function listDir(params) {
|
|
|
5311
5372
|
throw error;
|
|
5312
5373
|
}
|
|
5313
5374
|
if (!stat.isDirectory()) {
|
|
5314
|
-
throw new
|
|
5375
|
+
throw new ToolInputError(`Not a directory: ${params.path ?? "."}`);
|
|
5315
5376
|
}
|
|
5316
5377
|
let entries;
|
|
5317
5378
|
try {
|
|
@@ -8220,7 +8281,7 @@ var FETCH_TIMEOUT_MS = 8e3;
|
|
|
8220
8281
|
var MAX_REDIRECTS = 3;
|
|
8221
8282
|
var DEFAULT_MAX_CHARS = 6e3;
|
|
8222
8283
|
var MAX_FETCH_CHARS = 12e3;
|
|
8223
|
-
var MAX_FETCH_BYTES =
|
|
8284
|
+
var MAX_FETCH_BYTES = 1e6;
|
|
8224
8285
|
|
|
8225
8286
|
// src/chat/tools/web/network.ts
|
|
8226
8287
|
import dns from "dns/promises";
|
|
@@ -8488,37 +8549,120 @@ function normalizeWhitespace(text) {
|
|
|
8488
8549
|
}
|
|
8489
8550
|
function truncateAtWordBoundary(text, maxChars) {
|
|
8490
8551
|
if (text.length <= maxChars) {
|
|
8491
|
-
return text;
|
|
8552
|
+
return { content: text, truncated: false };
|
|
8492
8553
|
}
|
|
8493
8554
|
const shortened = text.slice(0, maxChars);
|
|
8494
8555
|
const lastSpace = shortened.lastIndexOf(" ");
|
|
8495
8556
|
if (lastSpace > maxChars * 0.8) {
|
|
8496
|
-
return
|
|
8557
|
+
return {
|
|
8558
|
+
content: `${shortened.slice(0, lastSpace).trimEnd()}...`,
|
|
8559
|
+
truncated: true
|
|
8560
|
+
};
|
|
8561
|
+
}
|
|
8562
|
+
return { content: `${shortened.trimEnd()}...`, truncated: true };
|
|
8563
|
+
}
|
|
8564
|
+
function decodeHtmlEntities(value) {
|
|
8565
|
+
return value.replace(/"/g, '"').replace(/'|'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
8566
|
+
}
|
|
8567
|
+
function extractTitle(html) {
|
|
8568
|
+
const match = html.match(/<title\b[^>]*>([\s\S]*?)<\/title>/i);
|
|
8569
|
+
const title = match ? normalizeWhitespace(decodeHtmlEntities(match[1])) : "";
|
|
8570
|
+
return title.length > 0 ? title : void 0;
|
|
8571
|
+
}
|
|
8572
|
+
function escapeRegex(value) {
|
|
8573
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8574
|
+
}
|
|
8575
|
+
function getBalancedElementHtml(args) {
|
|
8576
|
+
const tagPattern = new RegExp(
|
|
8577
|
+
`</?${escapeRegex(args.tagName)}\\b[^>]*>`,
|
|
8578
|
+
"gi"
|
|
8579
|
+
);
|
|
8580
|
+
tagPattern.lastIndex = args.startIndex;
|
|
8581
|
+
let depth = 0;
|
|
8582
|
+
for (const match of args.html.matchAll(tagPattern)) {
|
|
8583
|
+
const tag = match[0];
|
|
8584
|
+
if (tag.startsWith("</")) {
|
|
8585
|
+
depth -= 1;
|
|
8586
|
+
if (depth === 0) {
|
|
8587
|
+
return args.html.slice(args.startIndex, match.index + tag.length);
|
|
8588
|
+
}
|
|
8589
|
+
continue;
|
|
8590
|
+
}
|
|
8591
|
+
if (!tag.endsWith("/>")) {
|
|
8592
|
+
depth += 1;
|
|
8593
|
+
}
|
|
8594
|
+
}
|
|
8595
|
+
return void 0;
|
|
8596
|
+
}
|
|
8597
|
+
function findElementHtml(html, predicate) {
|
|
8598
|
+
const openingTagPattern = /<([a-z][\w:-]*)\b[^>]*>/gi;
|
|
8599
|
+
for (const match of html.matchAll(openingTagPattern)) {
|
|
8600
|
+
const tagName = match[1];
|
|
8601
|
+
if (!tagName || !predicate(tagName.toLowerCase(), match[0])) {
|
|
8602
|
+
continue;
|
|
8603
|
+
}
|
|
8604
|
+
const balanced = getBalancedElementHtml({
|
|
8605
|
+
html,
|
|
8606
|
+
startIndex: match.index,
|
|
8607
|
+
tagName
|
|
8608
|
+
});
|
|
8609
|
+
if (balanced) {
|
|
8610
|
+
return balanced;
|
|
8611
|
+
}
|
|
8497
8612
|
}
|
|
8498
|
-
return
|
|
8613
|
+
return void 0;
|
|
8614
|
+
}
|
|
8615
|
+
function extractMainHtml(html) {
|
|
8616
|
+
return findElementHtml(html, (tagName) => tagName === "main") ?? findElementHtml(html, (tagName) => tagName === "article") ?? findElementHtml(
|
|
8617
|
+
html,
|
|
8618
|
+
(_tagName, tag) => /\brole\s*=\s*(["'])main\1/i.test(tag)
|
|
8619
|
+
) ?? html;
|
|
8499
8620
|
}
|
|
8500
|
-
function
|
|
8621
|
+
function extractContentDetails(body, contentType, maxChars) {
|
|
8501
8622
|
const loweredContentType = contentType.toLowerCase();
|
|
8502
8623
|
const normalizedBody = body.trim();
|
|
8503
8624
|
if (loweredContentType.includes("html")) {
|
|
8504
8625
|
try {
|
|
8505
|
-
const
|
|
8506
|
-
|
|
8626
|
+
const sourceHtml = extractMainHtml(normalizedBody);
|
|
8627
|
+
const markdown = htmlToMarkdownConverter.translate(sourceHtml);
|
|
8628
|
+
const normalizedMarkdown = normalizeWhitespace(markdown);
|
|
8629
|
+
const truncated2 = truncateAtWordBoundary(normalizedMarkdown, maxChars);
|
|
8630
|
+
return {
|
|
8631
|
+
content: truncated2.content,
|
|
8632
|
+
title: extractTitle(normalizedBody),
|
|
8633
|
+
truncated: truncated2.truncated,
|
|
8634
|
+
extractedChars: normalizedMarkdown.length
|
|
8635
|
+
};
|
|
8507
8636
|
} catch {
|
|
8508
8637
|
}
|
|
8509
8638
|
}
|
|
8510
8639
|
if (loweredContentType.includes("json")) {
|
|
8511
8640
|
try {
|
|
8512
8641
|
const parsed = JSON.parse(normalizedBody);
|
|
8513
|
-
|
|
8642
|
+
const formatted = JSON.stringify(parsed, null, 2);
|
|
8643
|
+
const truncated2 = truncateAtWordBoundary(formatted, maxChars);
|
|
8644
|
+
return {
|
|
8645
|
+
content: truncated2.content,
|
|
8646
|
+
truncated: truncated2.truncated,
|
|
8647
|
+
extractedChars: formatted.length
|
|
8648
|
+
};
|
|
8514
8649
|
} catch {
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8650
|
+
const normalizedText2 = normalizeWhitespace(normalizedBody);
|
|
8651
|
+
const truncated2 = truncateAtWordBoundary(normalizedText2, maxChars);
|
|
8652
|
+
return {
|
|
8653
|
+
content: truncated2.content,
|
|
8654
|
+
truncated: truncated2.truncated,
|
|
8655
|
+
extractedChars: normalizedText2.length
|
|
8656
|
+
};
|
|
8519
8657
|
}
|
|
8520
8658
|
}
|
|
8521
|
-
|
|
8659
|
+
const normalizedText = normalizeWhitespace(normalizedBody);
|
|
8660
|
+
const truncated = truncateAtWordBoundary(normalizedText, maxChars);
|
|
8661
|
+
return {
|
|
8662
|
+
content: truncated.content,
|
|
8663
|
+
truncated: truncated.truncated,
|
|
8664
|
+
extractedChars: normalizedText.length
|
|
8665
|
+
};
|
|
8522
8666
|
}
|
|
8523
8667
|
async function extractWebFetchResponse(url, response, maxChars = DEFAULT_MAX_CHARS) {
|
|
8524
8668
|
const safeMaxChars = Math.max(500, Math.min(maxChars, MAX_FETCH_CHARS));
|
|
@@ -8534,8 +8678,16 @@ async function extractWebFetchResponse(url, response, maxChars = DEFAULT_MAX_CHA
|
|
|
8534
8678
|
FETCH_TIMEOUT_MS,
|
|
8535
8679
|
"read"
|
|
8536
8680
|
);
|
|
8537
|
-
const
|
|
8538
|
-
return {
|
|
8681
|
+
const extracted = extractContentDetails(body, contentType, safeMaxChars);
|
|
8682
|
+
return {
|
|
8683
|
+
url: url.toString(),
|
|
8684
|
+
content: extracted.content,
|
|
8685
|
+
...extracted.title ? { title: extracted.title } : {},
|
|
8686
|
+
content_type: contentType || "unknown",
|
|
8687
|
+
source_bytes: Buffer.byteLength(body, "utf8"),
|
|
8688
|
+
extracted_chars: extracted.extractedChars,
|
|
8689
|
+
truncated: extracted.truncated
|
|
8690
|
+
};
|
|
8539
8691
|
}
|
|
8540
8692
|
|
|
8541
8693
|
// src/chat/tools/web/fetch-tool.ts
|
|
@@ -8558,6 +8710,7 @@ function extractHttpStatusFromMessage(message) {
|
|
|
8558
8710
|
return Number.isFinite(parsed) ? parsed : null;
|
|
8559
8711
|
}
|
|
8560
8712
|
function createWebFetchTool(hooks) {
|
|
8713
|
+
const override = hooks.toolOverrides?.webFetch;
|
|
8561
8714
|
return tool({
|
|
8562
8715
|
description: "Fetch and extract readable content from a specific URL. Use when you need details from a known page or document. Do not use for discovery when search is the first step.",
|
|
8563
8716
|
annotations: {
|
|
@@ -8579,6 +8732,9 @@ function createWebFetchTool(hooks) {
|
|
|
8579
8732
|
)
|
|
8580
8733
|
}),
|
|
8581
8734
|
execute: async ({ url, max_chars }) => {
|
|
8735
|
+
if (override?.execute) {
|
|
8736
|
+
return override.execute({ url, max_chars });
|
|
8737
|
+
}
|
|
8582
8738
|
try {
|
|
8583
8739
|
const safeUrl = await assertPublicUrl(url);
|
|
8584
8740
|
const response = await withTimeout(
|
|
@@ -8671,7 +8827,7 @@ function isAuthFailure(message) {
|
|
|
8671
8827
|
const normalized = message.toLowerCase();
|
|
8672
8828
|
return normalized.includes("missing ai gateway credentials") || normalized.includes("authentication failed");
|
|
8673
8829
|
}
|
|
8674
|
-
function createWebSearchTool() {
|
|
8830
|
+
function createWebSearchTool(override) {
|
|
8675
8831
|
return tool({
|
|
8676
8832
|
description: "Search public web sources and return top snippets/URLs. Use when you need discovery or source candidates. Do not use when the user already provided a specific URL to inspect.",
|
|
8677
8833
|
annotations: {
|
|
@@ -8694,6 +8850,9 @@ function createWebSearchTool() {
|
|
|
8694
8850
|
)
|
|
8695
8851
|
}),
|
|
8696
8852
|
execute: async ({ query, max_results }) => {
|
|
8853
|
+
if (override?.execute) {
|
|
8854
|
+
return override.execute({ query, max_results });
|
|
8855
|
+
}
|
|
8697
8856
|
const maxResults = max_results ?? 3;
|
|
8698
8857
|
const model = process.env.AI_WEB_SEARCH_MODEL ?? DEFAULT_SEARCH_MODEL;
|
|
8699
8858
|
const controller = new AbortController();
|
|
@@ -8827,7 +8986,7 @@ function createTools(availableSkills, hooks = {}, context) {
|
|
|
8827
8986
|
findFiles: createFindFilesTool(),
|
|
8828
8987
|
listDir: createListDirTool(),
|
|
8829
8988
|
writeFile: createWriteFileTool(),
|
|
8830
|
-
webSearch: createWebSearchTool(),
|
|
8989
|
+
webSearch: createWebSearchTool(hooks.toolOverrides?.webSearch),
|
|
8831
8990
|
webFetch: createWebFetchTool(hooks),
|
|
8832
8991
|
imageGenerate: createImageGenerateTool(
|
|
8833
8992
|
hooks,
|
|
@@ -8935,14 +9094,11 @@ function buildSandboxEgressNetworkPolicy() {
|
|
|
8935
9094
|
}
|
|
8936
9095
|
return { allow };
|
|
8937
9096
|
}
|
|
8938
|
-
async function resolveSandboxCommandEnvironment(
|
|
9097
|
+
async function resolveSandboxCommandEnvironment() {
|
|
8939
9098
|
const env = {};
|
|
8940
9099
|
for (const plugin of getPluginProviders().sort(
|
|
8941
9100
|
(left, right) => left.manifest.name.localeCompare(right.manifest.name)
|
|
8942
9101
|
)) {
|
|
8943
|
-
if (provider && plugin.manifest.name !== provider) {
|
|
8944
|
-
continue;
|
|
8945
|
-
}
|
|
8946
9102
|
Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
|
|
8947
9103
|
const credentials = plugin.manifest.credentials;
|
|
8948
9104
|
if (credentials) {
|
|
@@ -9213,6 +9369,14 @@ function isSnapshottingError(error) {
|
|
|
9213
9369
|
return searchable.includes("sandbox_snapshotting") || searchable.includes("creating a snapshot") || searchable.includes("stopped shortly");
|
|
9214
9370
|
});
|
|
9215
9371
|
}
|
|
9372
|
+
function isSandboxCommandStreamInterruptedError(error) {
|
|
9373
|
+
return findInErrorChain(error, (candidate) => {
|
|
9374
|
+
if (!(candidate instanceof Error)) {
|
|
9375
|
+
return false;
|
|
9376
|
+
}
|
|
9377
|
+
return candidate.name === "StreamError" && candidate.message.toLowerCase().includes("stream ended before command finished");
|
|
9378
|
+
});
|
|
9379
|
+
}
|
|
9216
9380
|
function wrapSandboxSetupError(error) {
|
|
9217
9381
|
try {
|
|
9218
9382
|
const details = getSandboxErrorDetails(error);
|
|
@@ -9258,6 +9422,25 @@ import { randomUUID as randomUUID4 } from "crypto";
|
|
|
9258
9422
|
import { Sandbox } from "@vercel/sandbox";
|
|
9259
9423
|
import { createBashTool as createBashTool2 } from "bash-tool";
|
|
9260
9424
|
|
|
9425
|
+
// src/chat/sandbox/fault-injection.ts
|
|
9426
|
+
var STREAM_INTERRUPT_FAULT_ENV = "JUNIOR_EVAL_FAULT_SANDBOX_BASH_STREAM_INTERRUPTS";
|
|
9427
|
+
function consumeSandboxBashStreamInterruptFault() {
|
|
9428
|
+
if (process.env.JUNIOR_EVAL_ENABLE_FAULTS !== "1") {
|
|
9429
|
+
return void 0;
|
|
9430
|
+
}
|
|
9431
|
+
const remaining = Number.parseInt(
|
|
9432
|
+
process.env[STREAM_INTERRUPT_FAULT_ENV] ?? "0",
|
|
9433
|
+
10
|
|
9434
|
+
);
|
|
9435
|
+
if (!Number.isFinite(remaining) || remaining <= 0) {
|
|
9436
|
+
return void 0;
|
|
9437
|
+
}
|
|
9438
|
+
process.env[STREAM_INTERRUPT_FAULT_ENV] = String(remaining - 1);
|
|
9439
|
+
return Object.assign(new Error("Stream ended before command finished"), {
|
|
9440
|
+
name: "StreamError"
|
|
9441
|
+
});
|
|
9442
|
+
}
|
|
9443
|
+
|
|
9261
9444
|
// src/chat/sandbox/skill-sync.ts
|
|
9262
9445
|
import fs3 from "fs/promises";
|
|
9263
9446
|
import path9 from "path";
|
|
@@ -9386,6 +9569,62 @@ function outputText(value) {
|
|
|
9386
9569
|
fs.writeFileSync(process.stdout.fd, value);
|
|
9387
9570
|
}
|
|
9388
9571
|
|
|
9572
|
+
const repoFiles = {
|
|
9573
|
+
"packages/junior/src/chat/sandbox/egress-policy.ts": \`import { resolveAuthTokenPlaceholder } from "@/chat/plugins/auth/auth-token-placeholder";
|
|
9574
|
+
import { resolvePluginCommandEnv } from "@/chat/plugins/command-env";
|
|
9575
|
+
import { getPluginProviders } from "@/chat/plugins/registry";
|
|
9576
|
+
|
|
9577
|
+
/** Build the policy that forwards provider requests back to Junior for credentials. */
|
|
9578
|
+
export function buildSandboxEgressNetworkPolicy() {
|
|
9579
|
+
// Plugin credential domains are forwarded through the host so the sandbox can
|
|
9580
|
+
// activate requester-bound credentials for the current turn.
|
|
9581
|
+
}
|
|
9582
|
+
|
|
9583
|
+
/** Resolve non-secret command environment values for registered sandbox providers. */
|
|
9584
|
+
export async function resolveSandboxCommandEnvironment() {
|
|
9585
|
+
const env = {};
|
|
9586
|
+
for (const plugin of getPluginProviders()) {
|
|
9587
|
+
Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
|
|
9588
|
+
const credentials = plugin.manifest.credentials;
|
|
9589
|
+
if (credentials) {
|
|
9590
|
+
env[credentials.authTokenEnv] = resolveAuthTokenPlaceholder(credentials);
|
|
9591
|
+
}
|
|
9592
|
+
}
|
|
9593
|
+
return env;
|
|
9594
|
+
}
|
|
9595
|
+
\`,
|
|
9596
|
+
"packages/junior/src/chat/plugins/registry.ts": \`import { createGitHubAppBroker } from "@/chat/plugins/auth/github-app-broker";
|
|
9597
|
+
|
|
9598
|
+
export function createPluginBroker(provider, deps) {
|
|
9599
|
+
const plugin = ensurePluginsLoaded().pluginsByName.get(provider);
|
|
9600
|
+
const { credentials, name } = plugin.manifest;
|
|
9601
|
+
if (credentials.type === "github-app") {
|
|
9602
|
+
return createGitHubAppBroker(plugin.manifest, credentials);
|
|
9603
|
+
}
|
|
9604
|
+
}
|
|
9605
|
+
\`,
|
|
9606
|
+
"packages/junior-github/plugin.yaml": \`name: github
|
|
9607
|
+
description: GitHub issue, pull request, and repository workflows via GitHub App
|
|
9608
|
+
|
|
9609
|
+
credentials:
|
|
9610
|
+
type: github-app
|
|
9611
|
+
domains:
|
|
9612
|
+
- api.github.com
|
|
9613
|
+
- github.com
|
|
9614
|
+
auth-token-env: GITHUB_TOKEN
|
|
9615
|
+
auth-token-placeholder: ghp_host_managed_credential
|
|
9616
|
+
\`,
|
|
9617
|
+
};
|
|
9618
|
+
|
|
9619
|
+
function writeRepoFixture(targetDir) {
|
|
9620
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
9621
|
+
for (const [relativePath, content] of Object.entries(repoFiles)) {
|
|
9622
|
+
const filePath = path.join(targetDir, relativePath);
|
|
9623
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
9624
|
+
fs.writeFileSync(filePath, content);
|
|
9625
|
+
}
|
|
9626
|
+
}
|
|
9627
|
+
|
|
9389
9628
|
function fallbackToRealGh() {
|
|
9390
9629
|
for (const binary of fallbackBinaries) {
|
|
9391
9630
|
if (!fs.existsSync(binary)) {
|
|
@@ -9433,9 +9672,33 @@ if (args[0] === "repo" && args[1] === "view") {
|
|
|
9433
9672
|
process.exit(0);
|
|
9434
9673
|
}
|
|
9435
9674
|
|
|
9675
|
+
if (args[0] === "repo" && args[1] === "clone") {
|
|
9676
|
+
const positionals = getPositionals();
|
|
9677
|
+
const repo = positionals[2] || repoValue();
|
|
9678
|
+
const targetDir = positionals[3] || repo.split("/").pop() || "repo";
|
|
9679
|
+
writeRepoFixture(path.resolve(process.cwd(), targetDir));
|
|
9680
|
+
outputText("Cloning into '" + targetDir + "'...\\n");
|
|
9681
|
+
process.exit(0);
|
|
9682
|
+
}
|
|
9683
|
+
|
|
9436
9684
|
if (args[0] === "api") {
|
|
9437
9685
|
const positionals = getPositionals();
|
|
9438
9686
|
const route = positionals[1] || "";
|
|
9687
|
+
if (route.includes("/git/trees/")) {
|
|
9688
|
+
const paths = Object.keys(repoFiles);
|
|
9689
|
+
const jq = getFlag("--jq");
|
|
9690
|
+
if (jq && jq.includes(".tree[].path")) {
|
|
9691
|
+
outputText(paths.join("\\n") + "\\n");
|
|
9692
|
+
} else {
|
|
9693
|
+
outputJson({
|
|
9694
|
+
tree: paths.map((filePath) => ({
|
|
9695
|
+
path: filePath,
|
|
9696
|
+
type: "blob",
|
|
9697
|
+
})),
|
|
9698
|
+
});
|
|
9699
|
+
}
|
|
9700
|
+
process.exit(0);
|
|
9701
|
+
}
|
|
9439
9702
|
if (route.includes("/comments")) {
|
|
9440
9703
|
outputJson([]);
|
|
9441
9704
|
process.exit(0);
|
|
@@ -9887,6 +10150,15 @@ function parseKeepAliveMs() {
|
|
|
9887
10150
|
);
|
|
9888
10151
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
9889
10152
|
}
|
|
10153
|
+
function getCommandStreamInterruptedResult() {
|
|
10154
|
+
return {
|
|
10155
|
+
stdout: "",
|
|
10156
|
+
stderr: "Command stream ended before the command finished. The command may still have produced side effects; inspect the workspace or rerun only if it is safe.",
|
|
10157
|
+
exitCode: 125,
|
|
10158
|
+
stdoutTruncated: false,
|
|
10159
|
+
stderrTruncated: false
|
|
10160
|
+
};
|
|
10161
|
+
}
|
|
9890
10162
|
function createSandboxSessionManager(options) {
|
|
9891
10163
|
let sandbox = null;
|
|
9892
10164
|
let sandboxIdHint = options?.sandboxId;
|
|
@@ -10298,6 +10570,10 @@ function createSandboxSessionManager(options) {
|
|
|
10298
10570
|
timedOut = true;
|
|
10299
10571
|
controller.abort();
|
|
10300
10572
|
}, input.timeoutMs) : void 0;
|
|
10573
|
+
const streamInterruptFault = consumeSandboxBashStreamInterruptFault();
|
|
10574
|
+
if (streamInterruptFault) {
|
|
10575
|
+
throw streamInterruptFault;
|
|
10576
|
+
}
|
|
10301
10577
|
const commandResult2 = await sandboxInstance.runCommand({
|
|
10302
10578
|
cmd: "bash",
|
|
10303
10579
|
args: ["-c", script],
|
|
@@ -10317,6 +10593,9 @@ function createSandboxSessionManager(options) {
|
|
|
10317
10593
|
timedOut: true
|
|
10318
10594
|
};
|
|
10319
10595
|
}
|
|
10596
|
+
if (isSandboxCommandStreamInterruptedError(error)) {
|
|
10597
|
+
return getCommandStreamInterruptedResult();
|
|
10598
|
+
}
|
|
10320
10599
|
throw error;
|
|
10321
10600
|
} finally {
|
|
10322
10601
|
if (timeoutId) {
|
|
@@ -10426,10 +10705,7 @@ function createSandboxExecutor(options) {
|
|
|
10426
10705
|
sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
|
|
10427
10706
|
timeoutMs: options?.timeoutMs,
|
|
10428
10707
|
traceContext,
|
|
10429
|
-
commandEnv: credentialEgress ? async () =>
|
|
10430
|
-
const provider = credentialEgress.activeProvider?.();
|
|
10431
|
-
return provider ? await resolveSandboxCommandEnvironment(provider) : {};
|
|
10432
|
-
} : void 0,
|
|
10708
|
+
commandEnv: credentialEgress ? async () => await resolveSandboxCommandEnvironment() : void 0,
|
|
10433
10709
|
createNetworkPolicy: credentialEgress ? buildSandboxEgressNetworkPolicy : void 0,
|
|
10434
10710
|
beforeCommand: authorizeSandboxEgressForCommand,
|
|
10435
10711
|
afterCommand: clearSandboxEgressForCommand,
|
|
@@ -10513,7 +10789,7 @@ function createSandboxExecutor(options) {
|
|
|
10513
10789
|
const executeReadFileTool = async (rawInput) => {
|
|
10514
10790
|
const filePath = String(rawInput.path ?? "").trim();
|
|
10515
10791
|
if (!filePath) {
|
|
10516
|
-
throw new
|
|
10792
|
+
throw new ToolInputError("path is required");
|
|
10517
10793
|
}
|
|
10518
10794
|
const offset = positiveInteger(rawInput.offset);
|
|
10519
10795
|
const limit = positiveInteger(rawInput.limit);
|
|
@@ -10586,7 +10862,7 @@ function createSandboxExecutor(options) {
|
|
|
10586
10862
|
const executeWriteFileTool = async (rawInput) => {
|
|
10587
10863
|
const filePath = String(rawInput.path ?? "").trim();
|
|
10588
10864
|
if (!filePath) {
|
|
10589
|
-
throw new
|
|
10865
|
+
throw new ToolInputError("path is required");
|
|
10590
10866
|
}
|
|
10591
10867
|
const content = String(rawInput.content ?? "");
|
|
10592
10868
|
logSandboxBootRequest("tool.writeFile", {
|
|
@@ -10620,10 +10896,10 @@ function createSandboxExecutor(options) {
|
|
|
10620
10896
|
const executeEditFileTool = async (rawInput) => {
|
|
10621
10897
|
const filePath = String(rawInput.path ?? "").trim();
|
|
10622
10898
|
if (!filePath) {
|
|
10623
|
-
throw new
|
|
10899
|
+
throw new ToolInputError("path is required");
|
|
10624
10900
|
}
|
|
10625
10901
|
if (!Array.isArray(rawInput.edits)) {
|
|
10626
|
-
throw new
|
|
10902
|
+
throw new ToolInputError("edits is required");
|
|
10627
10903
|
}
|
|
10628
10904
|
logSandboxBootRequest("tool.editFile", {
|
|
10629
10905
|
"file.path": filePath
|
|
@@ -10651,7 +10927,7 @@ function createSandboxExecutor(options) {
|
|
|
10651
10927
|
const executeGrepTool = async (rawInput) => {
|
|
10652
10928
|
const pattern = String(rawInput.pattern ?? "");
|
|
10653
10929
|
if (!pattern) {
|
|
10654
|
-
throw new
|
|
10930
|
+
throw new ToolInputError("pattern is required");
|
|
10655
10931
|
}
|
|
10656
10932
|
logSandboxBootRequest("tool.grep");
|
|
10657
10933
|
const contextLines = positiveInteger(rawInput.context);
|
|
@@ -10683,7 +10959,7 @@ function createSandboxExecutor(options) {
|
|
|
10683
10959
|
const executeFindFilesTool = async (rawInput) => {
|
|
10684
10960
|
const pattern = String(rawInput.pattern ?? "");
|
|
10685
10961
|
if (!pattern) {
|
|
10686
|
-
throw new
|
|
10962
|
+
throw new ToolInputError("pattern is required");
|
|
10687
10963
|
}
|
|
10688
10964
|
logSandboxBootRequest("tool.findFiles");
|
|
10689
10965
|
const limit = positiveInteger(rawInput.limit);
|
|
@@ -10732,7 +11008,7 @@ function createSandboxExecutor(options) {
|
|
|
10732
11008
|
const bashCommand = params.toolName === "bash" ? String(rawInput.command ?? "").trim() : void 0;
|
|
10733
11009
|
if (params.toolName === "bash") {
|
|
10734
11010
|
if (!bashCommand) {
|
|
10735
|
-
throw new
|
|
11011
|
+
throw new ToolInputError("command is required");
|
|
10736
11012
|
}
|
|
10737
11013
|
if (options?.runBashCustomCommand) {
|
|
10738
11014
|
const custom = await options.runBashCustomCommand(bashCommand);
|
|
@@ -10934,6 +11210,28 @@ var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
|
10934
11210
|
function agentTurnSessionKey(conversationId, sessionId) {
|
|
10935
11211
|
return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
|
|
10936
11212
|
}
|
|
11213
|
+
function toFiniteNonNegativeNumber(value) {
|
|
11214
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
|
|
11215
|
+
}
|
|
11216
|
+
function parseAgentTurnUsage(value) {
|
|
11217
|
+
if (!isRecord(value)) {
|
|
11218
|
+
return void 0;
|
|
11219
|
+
}
|
|
11220
|
+
const usage = {};
|
|
11221
|
+
for (const field of [
|
|
11222
|
+
"inputTokens",
|
|
11223
|
+
"outputTokens",
|
|
11224
|
+
"cachedInputTokens",
|
|
11225
|
+
"cacheCreationTokens",
|
|
11226
|
+
"totalTokens"
|
|
11227
|
+
]) {
|
|
11228
|
+
const count = toFiniteNonNegativeNumber(value[field]);
|
|
11229
|
+
if (count !== void 0) {
|
|
11230
|
+
usage[field] = count;
|
|
11231
|
+
}
|
|
11232
|
+
}
|
|
11233
|
+
return Object.keys(usage).length > 0 ? usage : void 0;
|
|
11234
|
+
}
|
|
10937
11235
|
function parseAgentTurnSessionCheckpoint(value) {
|
|
10938
11236
|
if (typeof value !== "string") {
|
|
10939
11237
|
return void 0;
|
|
@@ -10952,6 +11250,10 @@ function parseAgentTurnSessionCheckpoint(value) {
|
|
|
10952
11250
|
const sliceId = parsed.sliceId;
|
|
10953
11251
|
const checkpointVersion = parsed.checkpointVersion;
|
|
10954
11252
|
const updatedAtMs = parsed.updatedAtMs;
|
|
11253
|
+
const cumulativeDurationMs = toFiniteNonNegativeNumber(
|
|
11254
|
+
parsed.cumulativeDurationMs
|
|
11255
|
+
);
|
|
11256
|
+
const cumulativeUsage = parseAgentTurnUsage(parsed.cumulativeUsage);
|
|
10955
11257
|
if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
|
|
10956
11258
|
return void 0;
|
|
10957
11259
|
}
|
|
@@ -10962,6 +11264,8 @@ function parseAgentTurnSessionCheckpoint(value) {
|
|
|
10962
11264
|
sliceId,
|
|
10963
11265
|
state: status,
|
|
10964
11266
|
updatedAtMs,
|
|
11267
|
+
...cumulativeDurationMs !== void 0 ? { cumulativeDurationMs } : {},
|
|
11268
|
+
...cumulativeUsage ? { cumulativeUsage } : {},
|
|
10965
11269
|
piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
|
|
10966
11270
|
...Array.isArray(parsed.loadedSkillNames) ? {
|
|
10967
11271
|
loadedSkillNames: parsed.loadedSkillNames.filter(
|
|
@@ -10999,6 +11303,13 @@ async function upsertAgentTurnSessionCheckpoint(args) {
|
|
|
10999
11303
|
state: args.state,
|
|
11000
11304
|
updatedAtMs: Date.now(),
|
|
11001
11305
|
piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
|
|
11306
|
+
...typeof args.cumulativeDurationMs === "number" && Number.isFinite(args.cumulativeDurationMs) ? {
|
|
11307
|
+
cumulativeDurationMs: Math.max(
|
|
11308
|
+
0,
|
|
11309
|
+
Math.floor(args.cumulativeDurationMs)
|
|
11310
|
+
)
|
|
11311
|
+
} : {},
|
|
11312
|
+
...args.cumulativeUsage ? { cumulativeUsage: args.cumulativeUsage } : {},
|
|
11002
11313
|
...Array.isArray(args.loadedSkillNames) ? {
|
|
11003
11314
|
loadedSkillNames: args.loadedSkillNames.filter(
|
|
11004
11315
|
(value) => typeof value === "string"
|
|
@@ -11030,6 +11341,8 @@ async function supersedeAgentTurnSessionCheckpoint(args) {
|
|
|
11030
11341
|
sliceId: existing.sliceId,
|
|
11031
11342
|
state: "superseded",
|
|
11032
11343
|
piMessages: existing.piMessages,
|
|
11344
|
+
cumulativeDurationMs: existing.cumulativeDurationMs,
|
|
11345
|
+
cumulativeUsage: existing.cumulativeUsage,
|
|
11033
11346
|
loadedSkillNames: existing.loadedSkillNames,
|
|
11034
11347
|
resumeReason: existing.resumeReason,
|
|
11035
11348
|
resumedFromSliceId: existing.resumedFromSliceId,
|
|
@@ -11291,6 +11604,11 @@ function createPluginAuthOrchestration(deps, abortAgent) {
|
|
|
11291
11604
|
}
|
|
11292
11605
|
|
|
11293
11606
|
// src/chat/tools/execution/tool-error-handler.ts
|
|
11607
|
+
function getToolErrorType(error) {
|
|
11608
|
+
if (error instanceof McpToolError) return "tool_error";
|
|
11609
|
+
if (error instanceof ToolInputError) return "tool_input_error";
|
|
11610
|
+
return error instanceof Error ? error.name : "tool_execution_error";
|
|
11611
|
+
}
|
|
11294
11612
|
function getToolErrorAttributes(error) {
|
|
11295
11613
|
if (!(error instanceof SlackActionError)) {
|
|
11296
11614
|
return {};
|
|
@@ -11304,7 +11622,7 @@ function getToolErrorAttributes(error) {
|
|
|
11304
11622
|
};
|
|
11305
11623
|
}
|
|
11306
11624
|
function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, traceContext) {
|
|
11307
|
-
const errorType =
|
|
11625
|
+
const errorType = getToolErrorType(error);
|
|
11308
11626
|
const errorMessage = getMcpAwareErrorMessage(error);
|
|
11309
11627
|
setSpanAttributes({
|
|
11310
11628
|
"error.type": errorType,
|
|
@@ -11343,7 +11661,8 @@ function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, trac
|
|
|
11343
11661
|
"Agent tool call failed"
|
|
11344
11662
|
);
|
|
11345
11663
|
}
|
|
11346
|
-
|
|
11664
|
+
const isExpectedToolFailure = error instanceof McpToolError || error instanceof ToolInputError;
|
|
11665
|
+
if (!isExpectedToolFailure) {
|
|
11347
11666
|
logException(
|
|
11348
11667
|
error,
|
|
11349
11668
|
"agent_tool_call_failed",
|
|
@@ -11898,7 +12217,95 @@ function toAgentThinkingLevel(level) {
|
|
|
11898
12217
|
}
|
|
11899
12218
|
}
|
|
11900
12219
|
|
|
12220
|
+
// src/chat/usage.ts
|
|
12221
|
+
var COMPONENT_USAGE_FIELDS = [
|
|
12222
|
+
"inputTokens",
|
|
12223
|
+
"outputTokens",
|
|
12224
|
+
"cachedInputTokens",
|
|
12225
|
+
"cacheCreationTokens"
|
|
12226
|
+
];
|
|
12227
|
+
function hasAgentTurnUsage(usage) {
|
|
12228
|
+
return Boolean(
|
|
12229
|
+
usage && Object.values(usage).some(
|
|
12230
|
+
(value) => typeof value === "number" && Number.isFinite(value)
|
|
12231
|
+
)
|
|
12232
|
+
);
|
|
12233
|
+
}
|
|
12234
|
+
function getFiniteCount(value) {
|
|
12235
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
|
|
12236
|
+
}
|
|
12237
|
+
function getComponentTotal(usage) {
|
|
12238
|
+
let total;
|
|
12239
|
+
for (const field of COMPONENT_USAGE_FIELDS) {
|
|
12240
|
+
const value = getFiniteCount(usage[field]);
|
|
12241
|
+
if (value === void 0) continue;
|
|
12242
|
+
total = (total ?? 0) + value;
|
|
12243
|
+
}
|
|
12244
|
+
return total;
|
|
12245
|
+
}
|
|
12246
|
+
function addAgentTurnUsage(...usages) {
|
|
12247
|
+
const components = {};
|
|
12248
|
+
let componentTotal;
|
|
12249
|
+
let totalOnlyTokens;
|
|
12250
|
+
for (const usage of usages) {
|
|
12251
|
+
if (!usage) continue;
|
|
12252
|
+
const usageComponentTotal = getComponentTotal(usage);
|
|
12253
|
+
if (usageComponentTotal !== void 0) {
|
|
12254
|
+
componentTotal = (componentTotal ?? 0) + usageComponentTotal;
|
|
12255
|
+
for (const field of COMPONENT_USAGE_FIELDS) {
|
|
12256
|
+
const value = getFiniteCount(usage[field]);
|
|
12257
|
+
if (value === void 0) continue;
|
|
12258
|
+
components[field] = (components[field] ?? 0) + value;
|
|
12259
|
+
}
|
|
12260
|
+
continue;
|
|
12261
|
+
}
|
|
12262
|
+
const totalTokens = getFiniteCount(usage.totalTokens);
|
|
12263
|
+
if (totalTokens !== void 0) {
|
|
12264
|
+
totalOnlyTokens = (totalOnlyTokens ?? 0) + totalTokens;
|
|
12265
|
+
}
|
|
12266
|
+
}
|
|
12267
|
+
if (totalOnlyTokens !== void 0) {
|
|
12268
|
+
return {
|
|
12269
|
+
totalTokens: totalOnlyTokens + (componentTotal ?? 0)
|
|
12270
|
+
};
|
|
12271
|
+
}
|
|
12272
|
+
return hasAgentTurnUsage(components) ? components : void 0;
|
|
12273
|
+
}
|
|
12274
|
+
|
|
11901
12275
|
// src/chat/services/turn-checkpoint.ts
|
|
12276
|
+
function logCheckpointError(error, eventName, args, attributes, message) {
|
|
12277
|
+
logException(
|
|
12278
|
+
error,
|
|
12279
|
+
eventName,
|
|
12280
|
+
{
|
|
12281
|
+
slackThreadId: args.logContext.threadId,
|
|
12282
|
+
slackUserId: args.logContext.requesterId,
|
|
12283
|
+
slackChannelId: args.logContext.channelId,
|
|
12284
|
+
runId: args.logContext.runId,
|
|
12285
|
+
assistantUserName: args.logContext.assistantUserName,
|
|
12286
|
+
modelId: args.logContext.modelId
|
|
12287
|
+
},
|
|
12288
|
+
{
|
|
12289
|
+
"app.ai.resume_conversation_id": args.conversationId,
|
|
12290
|
+
"app.ai.resume_session_id": args.sessionId,
|
|
12291
|
+
...attributes
|
|
12292
|
+
},
|
|
12293
|
+
message
|
|
12294
|
+
);
|
|
12295
|
+
}
|
|
12296
|
+
function addDurationMs(prior, current) {
|
|
12297
|
+
const total = [prior, current].reduce((sum, value) => {
|
|
12298
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
12299
|
+
return sum;
|
|
12300
|
+
}
|
|
12301
|
+
return (sum ?? 0) + Math.max(0, Math.floor(value));
|
|
12302
|
+
}, void 0);
|
|
12303
|
+
return total;
|
|
12304
|
+
}
|
|
12305
|
+
function isContinuableBoundary(messages) {
|
|
12306
|
+
const lastRole = getPiMessageRole(messages.at(-1));
|
|
12307
|
+
return lastRole === "user" || lastRole === "toolResult";
|
|
12308
|
+
}
|
|
11902
12309
|
async function loadTurnCheckpoint(ctx) {
|
|
11903
12310
|
const canUseTurnSession = Boolean(ctx.conversationId && ctx.sessionId);
|
|
11904
12311
|
const existingCheckpoint = canUseTurnSession && ctx.conversationId && ctx.sessionId ? await getAgentTurnSessionCheckpoint(ctx.conversationId, ctx.sessionId) : void 0;
|
|
@@ -11912,15 +12319,70 @@ async function loadTurnCheckpoint(ctx) {
|
|
|
11912
12319
|
existingCheckpoint
|
|
11913
12320
|
};
|
|
11914
12321
|
}
|
|
12322
|
+
async function persistRunningCheckpoint(args) {
|
|
12323
|
+
if (args.messages.length === 0 || !isContinuableBoundary(args.messages)) {
|
|
12324
|
+
return;
|
|
12325
|
+
}
|
|
12326
|
+
try {
|
|
12327
|
+
const latestCheckpoint = await getAgentTurnSessionCheckpoint(
|
|
12328
|
+
args.conversationId,
|
|
12329
|
+
args.sessionId
|
|
12330
|
+
);
|
|
12331
|
+
await upsertAgentTurnSessionCheckpoint({
|
|
12332
|
+
conversationId: args.conversationId,
|
|
12333
|
+
cumulativeDurationMs: latestCheckpoint?.cumulativeDurationMs,
|
|
12334
|
+
cumulativeUsage: latestCheckpoint?.cumulativeUsage,
|
|
12335
|
+
sessionId: args.sessionId,
|
|
12336
|
+
sliceId: args.sliceId,
|
|
12337
|
+
state: "running",
|
|
12338
|
+
piMessages: args.messages,
|
|
12339
|
+
loadedSkillNames: args.loadedSkillNames
|
|
12340
|
+
});
|
|
12341
|
+
} catch (checkpointError) {
|
|
12342
|
+
logCheckpointError(
|
|
12343
|
+
checkpointError,
|
|
12344
|
+
"agent_turn_running_checkpoint_failed",
|
|
12345
|
+
args,
|
|
12346
|
+
{
|
|
12347
|
+
"app.ai.resume_slice_id": args.sliceId
|
|
12348
|
+
},
|
|
12349
|
+
"Failed to persist running turn checkpoint"
|
|
12350
|
+
);
|
|
12351
|
+
}
|
|
12352
|
+
}
|
|
11915
12353
|
async function persistCompletedCheckpoint(args) {
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
|
|
12354
|
+
try {
|
|
12355
|
+
const latestCheckpoint = await getAgentTurnSessionCheckpoint(
|
|
12356
|
+
args.conversationId,
|
|
12357
|
+
args.sessionId
|
|
12358
|
+
);
|
|
12359
|
+
await upsertAgentTurnSessionCheckpoint({
|
|
12360
|
+
conversationId: args.conversationId,
|
|
12361
|
+
cumulativeDurationMs: addDurationMs(
|
|
12362
|
+
latestCheckpoint?.cumulativeDurationMs,
|
|
12363
|
+
args.currentDurationMs
|
|
12364
|
+
),
|
|
12365
|
+
cumulativeUsage: addAgentTurnUsage(
|
|
12366
|
+
latestCheckpoint?.cumulativeUsage,
|
|
12367
|
+
args.currentUsage
|
|
12368
|
+
),
|
|
12369
|
+
sessionId: args.sessionId,
|
|
12370
|
+
sliceId: args.sliceId,
|
|
12371
|
+
state: "completed",
|
|
12372
|
+
piMessages: args.allMessages,
|
|
12373
|
+
loadedSkillNames: args.loadedSkillNames
|
|
12374
|
+
});
|
|
12375
|
+
} catch (checkpointError) {
|
|
12376
|
+
logCheckpointError(
|
|
12377
|
+
checkpointError,
|
|
12378
|
+
"agent_turn_completed_checkpoint_failed",
|
|
12379
|
+
args,
|
|
12380
|
+
{
|
|
12381
|
+
"app.ai.resume_slice_id": args.sliceId
|
|
12382
|
+
},
|
|
12383
|
+
"Failed to persist completed turn checkpoint"
|
|
12384
|
+
);
|
|
12385
|
+
}
|
|
11924
12386
|
}
|
|
11925
12387
|
async function persistAuthPauseCheckpoint(args) {
|
|
11926
12388
|
const nextSliceId = args.currentSliceId + 1;
|
|
@@ -11934,6 +12396,14 @@ async function persistAuthPauseCheckpoint(args) {
|
|
|
11934
12396
|
);
|
|
11935
12397
|
await upsertAgentTurnSessionCheckpoint({
|
|
11936
12398
|
conversationId: args.conversationId,
|
|
12399
|
+
cumulativeDurationMs: addDurationMs(
|
|
12400
|
+
latestCheckpoint?.cumulativeDurationMs,
|
|
12401
|
+
args.currentDurationMs
|
|
12402
|
+
),
|
|
12403
|
+
cumulativeUsage: addAgentTurnUsage(
|
|
12404
|
+
latestCheckpoint?.cumulativeUsage,
|
|
12405
|
+
args.currentUsage
|
|
12406
|
+
),
|
|
11937
12407
|
sessionId: args.sessionId,
|
|
11938
12408
|
sliceId: nextSliceId,
|
|
11939
12409
|
state: "awaiting_resume",
|
|
@@ -11944,20 +12414,11 @@ async function persistAuthPauseCheckpoint(args) {
|
|
|
11944
12414
|
errorMessage: args.errorMessage
|
|
11945
12415
|
});
|
|
11946
12416
|
} catch (checkpointError) {
|
|
11947
|
-
|
|
12417
|
+
logCheckpointError(
|
|
11948
12418
|
checkpointError,
|
|
11949
12419
|
"agent_turn_auth_resume_checkpoint_failed",
|
|
12420
|
+
args,
|
|
11950
12421
|
{
|
|
11951
|
-
slackThreadId: args.logContext.threadId,
|
|
11952
|
-
slackUserId: args.logContext.requesterId,
|
|
11953
|
-
slackChannelId: args.logContext.channelId,
|
|
11954
|
-
runId: args.logContext.runId,
|
|
11955
|
-
assistantUserName: args.logContext.assistantUserName,
|
|
11956
|
-
modelId: args.logContext.modelId
|
|
11957
|
-
},
|
|
11958
|
-
{
|
|
11959
|
-
"app.ai.resume_conversation_id": args.conversationId,
|
|
11960
|
-
"app.ai.resume_session_id": args.sessionId,
|
|
11961
12422
|
"app.ai.resume_from_slice_id": args.currentSliceId,
|
|
11962
12423
|
"app.ai.resume_next_slice_id": nextSliceId
|
|
11963
12424
|
},
|
|
@@ -11978,6 +12439,14 @@ async function persistTimeoutCheckpoint(args) {
|
|
|
11978
12439
|
);
|
|
11979
12440
|
return await upsertAgentTurnSessionCheckpoint({
|
|
11980
12441
|
conversationId: args.conversationId,
|
|
12442
|
+
cumulativeDurationMs: addDurationMs(
|
|
12443
|
+
latestCheckpoint?.cumulativeDurationMs,
|
|
12444
|
+
args.currentDurationMs
|
|
12445
|
+
),
|
|
12446
|
+
cumulativeUsage: addAgentTurnUsage(
|
|
12447
|
+
latestCheckpoint?.cumulativeUsage,
|
|
12448
|
+
args.currentUsage
|
|
12449
|
+
),
|
|
11981
12450
|
sessionId: args.sessionId,
|
|
11982
12451
|
sliceId: nextSliceId,
|
|
11983
12452
|
state: "awaiting_resume",
|
|
@@ -11988,20 +12457,11 @@ async function persistTimeoutCheckpoint(args) {
|
|
|
11988
12457
|
errorMessage: args.errorMessage
|
|
11989
12458
|
});
|
|
11990
12459
|
} catch (checkpointError) {
|
|
11991
|
-
|
|
12460
|
+
logCheckpointError(
|
|
11992
12461
|
checkpointError,
|
|
11993
12462
|
"agent_turn_timeout_resume_checkpoint_failed",
|
|
12463
|
+
args,
|
|
11994
12464
|
{
|
|
11995
|
-
slackThreadId: args.logContext.threadId,
|
|
11996
|
-
slackUserId: args.logContext.requesterId,
|
|
11997
|
-
slackChannelId: args.logContext.channelId,
|
|
11998
|
-
runId: args.logContext.runId,
|
|
11999
|
-
assistantUserName: args.logContext.assistantUserName,
|
|
12000
|
-
modelId: args.logContext.modelId
|
|
12001
|
-
},
|
|
12002
|
-
{
|
|
12003
|
-
"app.ai.resume_conversation_id": args.conversationId,
|
|
12004
|
-
"app.ai.resume_session_id": args.sessionId,
|
|
12005
12465
|
"app.ai.resume_from_slice_id": args.currentSliceId,
|
|
12006
12466
|
"app.ai.resume_next_slice_id": nextSliceId
|
|
12007
12467
|
},
|
|
@@ -12123,6 +12583,12 @@ function trimRouterAttachmentText(text) {
|
|
|
12123
12583
|
}
|
|
12124
12584
|
return normalized.length <= MAX_ROUTER_ATTACHMENT_PREVIEW_CHARS ? normalized : `${normalized.slice(0, MAX_ROUTER_ATTACHMENT_PREVIEW_CHARS)}...`;
|
|
12125
12585
|
}
|
|
12586
|
+
function extractSliceUsage(messages, beforeMessageCount) {
|
|
12587
|
+
const usage = extractGenAiUsageSummary(
|
|
12588
|
+
...messages.slice(beforeMessageCount).filter(isAssistantMessage)
|
|
12589
|
+
);
|
|
12590
|
+
return hasAgentTurnUsage(usage) ? usage : void 0;
|
|
12591
|
+
}
|
|
12126
12592
|
function supportsRouterTextPreview(mediaType) {
|
|
12127
12593
|
const baseMediaType = mediaType.split(";", 1)[0]?.trim().toLowerCase();
|
|
12128
12594
|
if (!baseMediaType) {
|
|
@@ -12277,6 +12743,14 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
12277
12743
|
let timedOut = false;
|
|
12278
12744
|
let turnUsage;
|
|
12279
12745
|
let thinkingSelection;
|
|
12746
|
+
const checkpointLogContext = {
|
|
12747
|
+
threadId: context.correlation?.threadId,
|
|
12748
|
+
requesterId: context.correlation?.requesterId,
|
|
12749
|
+
channelId: context.correlation?.channelId,
|
|
12750
|
+
runId: context.correlation?.runId,
|
|
12751
|
+
assistantUserName: botConfig.userName,
|
|
12752
|
+
modelId: botConfig.modelId
|
|
12753
|
+
};
|
|
12280
12754
|
const getSandboxMetadata = () => sandboxExecutor ? {
|
|
12281
12755
|
sandboxId: sandboxExecutor.getSandboxId(),
|
|
12282
12756
|
sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash()
|
|
@@ -12365,8 +12839,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
12365
12839
|
sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
|
|
12366
12840
|
traceContext: spanContext,
|
|
12367
12841
|
credentialEgress: requesterId ? {
|
|
12368
|
-
requesterId
|
|
12369
|
-
activeProvider: () => skillSandbox.getActiveSkill()?.pluginProvider
|
|
12842
|
+
requesterId
|
|
12370
12843
|
} : void 0,
|
|
12371
12844
|
onSandboxAcquired: async (sandbox2) => {
|
|
12372
12845
|
lastKnownSandboxId = sandbox2.sandboxId;
|
|
@@ -12542,8 +13015,11 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
12542
13015
|
});
|
|
12543
13016
|
const toolChannelId = context.toolChannelId ?? context.correlation?.channelId;
|
|
12544
13017
|
const channelCapabilities = resolveChannelCapabilities(toolChannelId);
|
|
13018
|
+
const loadableSkills = availableSkills.filter(
|
|
13019
|
+
(skill) => skill.disableModelInvocation !== true || skill.name === invokedSkill?.name
|
|
13020
|
+
);
|
|
12545
13021
|
const tools = createTools(
|
|
12546
|
-
|
|
13022
|
+
loadableSkills,
|
|
12547
13023
|
{
|
|
12548
13024
|
getGeneratedFile: (filename) => generatedFiles.find((file) => file.filename === filename),
|
|
12549
13025
|
onGeneratedArtifactFiles: (files) => {
|
|
@@ -12707,7 +13183,23 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
12707
13183
|
});
|
|
12708
13184
|
let hasEmittedText = false;
|
|
12709
13185
|
let needsSeparator = false;
|
|
13186
|
+
const persistSafeBoundary = async (messages) => {
|
|
13187
|
+
if (!checkpointState.canUseTurnSession || !sessionConversationId || !sessionId) {
|
|
13188
|
+
return;
|
|
13189
|
+
}
|
|
13190
|
+
await persistRunningCheckpoint({
|
|
13191
|
+
conversationId: sessionConversationId,
|
|
13192
|
+
sessionId,
|
|
13193
|
+
sliceId: currentSliceId,
|
|
13194
|
+
messages,
|
|
13195
|
+
loadedSkillNames: loadedSkillNamesForResume,
|
|
13196
|
+
logContext: checkpointLogContext
|
|
13197
|
+
});
|
|
13198
|
+
};
|
|
12710
13199
|
const unsubscribe = agent.subscribe((event) => {
|
|
13200
|
+
if (event.type === "turn_end" && event.toolResults.length > 0) {
|
|
13201
|
+
return persistSafeBoundary([...agent.state.messages]);
|
|
13202
|
+
}
|
|
12711
13203
|
if (event.type === "message_start") {
|
|
12712
13204
|
Promise.resolve(context.onAssistantMessageStart?.()).catch((error) => {
|
|
12713
13205
|
logWarn(
|
|
@@ -12760,11 +13252,18 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
12760
13252
|
spanContext,
|
|
12761
13253
|
async () => {
|
|
12762
13254
|
let promptResult;
|
|
12763
|
-
const
|
|
13255
|
+
const freshPromptMessage = {
|
|
12764
13256
|
role: "user",
|
|
12765
13257
|
content: promptContentParts,
|
|
12766
13258
|
timestamp: Date.now()
|
|
12767
|
-
}
|
|
13259
|
+
};
|
|
13260
|
+
if (!resumedFromCheckpoint) {
|
|
13261
|
+
await persistSafeBoundary([
|
|
13262
|
+
...agent.state.messages,
|
|
13263
|
+
freshPromptMessage
|
|
13264
|
+
]);
|
|
13265
|
+
}
|
|
13266
|
+
const promptPromise = resumedFromCheckpoint ? agent.continue() : agent.prompt(freshPromptMessage);
|
|
12768
13267
|
let timeoutId;
|
|
12769
13268
|
const timeoutPromise = new Promise((_, reject) => {
|
|
12770
13269
|
timeoutId = setTimeout(() => {
|
|
@@ -12817,9 +13316,7 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
12817
13316
|
agent.state,
|
|
12818
13317
|
...outputMessages
|
|
12819
13318
|
);
|
|
12820
|
-
turnUsage =
|
|
12821
|
-
(value) => value !== void 0
|
|
12822
|
-
) ? usageSummary : void 0;
|
|
13319
|
+
turnUsage = hasAgentTurnUsage(usageSummary) ? usageSummary : void 0;
|
|
12823
13320
|
setSpanAttributes({
|
|
12824
13321
|
...outputMessagesAttribute ? { "gen_ai.output.messages": outputMessagesAttribute } : {},
|
|
12825
13322
|
...extractGenAiUsageAttributes(usageSummary)
|
|
@@ -12843,10 +13340,13 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
12843
13340
|
if (checkpointState.canUseTurnSession && sessionConversationId && sessionId) {
|
|
12844
13341
|
await persistCompletedCheckpoint({
|
|
12845
13342
|
conversationId: sessionConversationId,
|
|
13343
|
+
currentDurationMs: Date.now() - replyStartedAtMs,
|
|
13344
|
+
currentUsage: turnUsage,
|
|
12846
13345
|
sessionId,
|
|
12847
13346
|
sliceId: currentSliceId,
|
|
12848
13347
|
allMessages: agent.state.messages,
|
|
12849
|
-
loadedSkillNames: activeSkills.map((skill) => skill.name)
|
|
13348
|
+
loadedSkillNames: activeSkills.map((skill) => skill.name),
|
|
13349
|
+
logContext: checkpointLogContext
|
|
12850
13350
|
});
|
|
12851
13351
|
}
|
|
12852
13352
|
return buildTurnResult({
|
|
@@ -12872,21 +13372,17 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
12872
13372
|
});
|
|
12873
13373
|
} catch (error) {
|
|
12874
13374
|
if (timedOut && timeoutResumeConversationId && timeoutResumeSessionId) {
|
|
13375
|
+
turnUsage = turnUsage ?? extractSliceUsage(timeoutResumeMessages, beforeMessageCount);
|
|
12875
13376
|
const checkpoint = await persistTimeoutCheckpoint({
|
|
12876
13377
|
conversationId: timeoutResumeConversationId,
|
|
12877
13378
|
sessionId: timeoutResumeSessionId,
|
|
12878
13379
|
currentSliceId: timeoutResumeSliceId,
|
|
13380
|
+
currentDurationMs: Date.now() - replyStartedAtMs,
|
|
13381
|
+
currentUsage: turnUsage,
|
|
12879
13382
|
messages: timeoutResumeMessages,
|
|
12880
13383
|
loadedSkillNames: loadedSkillNamesForResume,
|
|
12881
13384
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
12882
|
-
logContext:
|
|
12883
|
-
threadId: context.correlation?.threadId,
|
|
12884
|
-
requesterId: context.correlation?.requesterId,
|
|
12885
|
-
channelId: context.correlation?.channelId,
|
|
12886
|
-
runId: context.correlation?.runId,
|
|
12887
|
-
assistantUserName: botConfig.userName,
|
|
12888
|
-
modelId: botConfig.modelId
|
|
12889
|
-
}
|
|
13385
|
+
logContext: checkpointLogContext
|
|
12890
13386
|
});
|
|
12891
13387
|
if (checkpoint) {
|
|
12892
13388
|
throw new RetryableTurnError(
|
|
@@ -12903,28 +13399,21 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
12903
13399
|
}
|
|
12904
13400
|
if (error instanceof AuthorizationPauseError && timeoutResumeConversationId && timeoutResumeSessionId) {
|
|
12905
13401
|
if (!turnUsage && timeoutResumeMessages.length > 0) {
|
|
12906
|
-
|
|
12907
|
-
|
|
13402
|
+
turnUsage = extractSliceUsage(
|
|
13403
|
+
timeoutResumeMessages,
|
|
13404
|
+
beforeMessageCount
|
|
12908
13405
|
);
|
|
12909
|
-
turnUsage = Object.values(fallbackUsage).some(
|
|
12910
|
-
(value) => value !== void 0
|
|
12911
|
-
) ? fallbackUsage : void 0;
|
|
12912
13406
|
}
|
|
12913
13407
|
const nextSliceId = await persistAuthPauseCheckpoint({
|
|
12914
13408
|
conversationId: timeoutResumeConversationId,
|
|
12915
13409
|
sessionId: timeoutResumeSessionId,
|
|
12916
13410
|
currentSliceId: timeoutResumeSliceId,
|
|
13411
|
+
currentDurationMs: Date.now() - replyStartedAtMs,
|
|
13412
|
+
currentUsage: turnUsage,
|
|
12917
13413
|
messages: timeoutResumeMessages,
|
|
12918
13414
|
loadedSkillNames: loadedSkillNamesForResume,
|
|
12919
13415
|
errorMessage: error.message,
|
|
12920
|
-
logContext:
|
|
12921
|
-
threadId: context.correlation?.threadId,
|
|
12922
|
-
requesterId: context.correlation?.requesterId,
|
|
12923
|
-
channelId: context.correlation?.channelId,
|
|
12924
|
-
runId: context.correlation?.runId,
|
|
12925
|
-
assistantUserName: botConfig.userName,
|
|
12926
|
-
modelId: botConfig.modelId
|
|
12927
|
-
}
|
|
13416
|
+
logContext: checkpointLogContext
|
|
12928
13417
|
});
|
|
12929
13418
|
throw new RetryableTurnError(
|
|
12930
13419
|
error.kind === "plugin" ? "plugin_auth_resume" : "mcp_auth_resume",
|
|
@@ -13074,9 +13563,10 @@ function finalizeFailedTurnReply(args) {
|
|
|
13074
13563
|
),
|
|
13075
13564
|
capture.eventName
|
|
13076
13565
|
);
|
|
13566
|
+
const providerPartialText = args.reply.diagnostics.outcome === "provider_error" ? args.reply.text.trim() : "";
|
|
13077
13567
|
return {
|
|
13078
13568
|
...args.reply,
|
|
13079
|
-
text: buildTurnFailureResponse(eventId),
|
|
13569
|
+
text: providerPartialText ? `${providerPartialText}${getInterruptionMarker()}` : buildTurnFailureResponse(eventId),
|
|
13080
13570
|
deliveryMode: "thread",
|
|
13081
13571
|
deliveryPlan: {
|
|
13082
13572
|
mode: "thread",
|
|
@@ -13086,12 +13576,6 @@ function finalizeFailedTurnReply(args) {
|
|
|
13086
13576
|
};
|
|
13087
13577
|
}
|
|
13088
13578
|
|
|
13089
|
-
// src/chat/services/turn-continuation-response.ts
|
|
13090
|
-
var TURN_CONTINUATION_RESPONSE = "I'm still working on this in the background. I'll post the final response here when it finishes.";
|
|
13091
|
-
function buildTurnContinuationResponse() {
|
|
13092
|
-
return TURN_CONTINUATION_RESPONSE;
|
|
13093
|
-
}
|
|
13094
|
-
|
|
13095
13579
|
// src/chat/slack/assistant-thread/status-render.ts
|
|
13096
13580
|
var DEFAULT_STATUS_CONTEXTS = {
|
|
13097
13581
|
thinking: "\u2026",
|
|
@@ -13451,16 +13935,12 @@ function createSlackWebApiAssistantStatusSession(args) {
|
|
|
13451
13935
|
}
|
|
13452
13936
|
|
|
13453
13937
|
// src/chat/slack/footer.ts
|
|
13454
|
-
var SENTRY_CONVERSATION_SEARCH_STATS_PERIOD = "14d";
|
|
13455
13938
|
function escapeSlackMrkdwn(text) {
|
|
13456
13939
|
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
13457
13940
|
}
|
|
13458
13941
|
function escapeSlackLinkUrl(url) {
|
|
13459
13942
|
return url.replaceAll("&", "&").replaceAll("<", "%3C").replaceAll(">", "%3E");
|
|
13460
13943
|
}
|
|
13461
|
-
function quoteSentrySearchValue(value) {
|
|
13462
|
-
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
13463
|
-
}
|
|
13464
13944
|
function getSentryOrgSlug() {
|
|
13465
13945
|
const slug = process.env.SENTRY_ORG_SLUG?.trim();
|
|
13466
13946
|
return slug || void 0;
|
|
@@ -13476,7 +13956,7 @@ function buildSentryWebBaseUrl(dsn) {
|
|
|
13476
13956
|
const path11 = dsn.path ? `/${dsn.path}` : "";
|
|
13477
13957
|
return `${dsn.protocol}://${dsn.host}${port}${path11}`;
|
|
13478
13958
|
}
|
|
13479
|
-
function
|
|
13959
|
+
function getSentryConversationUrl(conversationId) {
|
|
13480
13960
|
const client2 = sentry_exports.getClient();
|
|
13481
13961
|
const dsn = client2?.getDsn();
|
|
13482
13962
|
if (!dsn?.host || !dsn.projectId) {
|
|
@@ -13486,18 +13966,14 @@ function getSentryConversationSearchUrl(conversationId) {
|
|
|
13486
13966
|
if (!orgSlug) {
|
|
13487
13967
|
return void 0;
|
|
13488
13968
|
}
|
|
13969
|
+
const encodedId = encodeURIComponent(conversationId);
|
|
13489
13970
|
const params = new URLSearchParams();
|
|
13490
|
-
params.set(
|
|
13491
|
-
"query",
|
|
13492
|
-
`gen_ai.conversation.id:${quoteSentrySearchValue(conversationId)}`
|
|
13493
|
-
);
|
|
13494
13971
|
params.set("project", dsn.projectId);
|
|
13495
|
-
params.
|
|
13496
|
-
const search = `explore/traces/?${params.toString()}`;
|
|
13972
|
+
const path11 = `explore/conversations/${encodedId}/?${params.toString()}`;
|
|
13497
13973
|
if (isSentrySaasDsnHost(dsn.host)) {
|
|
13498
|
-
return `https://${orgSlug}.sentry.io/${
|
|
13974
|
+
return `https://${orgSlug}.sentry.io/${path11}`;
|
|
13499
13975
|
}
|
|
13500
|
-
return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${
|
|
13976
|
+
return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path11}`;
|
|
13501
13977
|
}
|
|
13502
13978
|
function formatSlackTokenCount(value) {
|
|
13503
13979
|
if (value >= 1e6) {
|
|
@@ -13552,7 +14028,7 @@ function buildSlackReplyFooter(args) {
|
|
|
13552
14028
|
label: "ID",
|
|
13553
14029
|
value: conversationId
|
|
13554
14030
|
};
|
|
13555
|
-
const conversationUrl =
|
|
14031
|
+
const conversationUrl = getSentryConversationUrl(conversationId);
|
|
13556
14032
|
if (conversationUrl) {
|
|
13557
14033
|
idItem.url = conversationUrl;
|
|
13558
14034
|
}
|
|
@@ -13748,6 +14224,25 @@ async function postSlackApiReplyPosts(args) {
|
|
|
13748
14224
|
return lastPostedMessageTs;
|
|
13749
14225
|
}
|
|
13750
14226
|
|
|
14227
|
+
// src/chat/services/turn-continuation-response.ts
|
|
14228
|
+
var TURN_CONTINUATION_RESPONSE = "I'm still working on this in the background. I'll post the final response here when it finishes.";
|
|
14229
|
+
function buildTurnContinuationResponse() {
|
|
14230
|
+
return TURN_CONTINUATION_RESPONSE;
|
|
14231
|
+
}
|
|
14232
|
+
|
|
14233
|
+
// src/chat/slack/turn-continuation-notice.ts
|
|
14234
|
+
function buildSlackTurnContinuationNotice(args) {
|
|
14235
|
+
const text = buildTurnContinuationResponse();
|
|
14236
|
+
const footer = buildSlackReplyFooter({
|
|
14237
|
+
conversationId: args.conversationId
|
|
14238
|
+
});
|
|
14239
|
+
const blocks = footer ? buildSlackReplyBlocks(text, footer) : void 0;
|
|
14240
|
+
return {
|
|
14241
|
+
text,
|
|
14242
|
+
...blocks ? { blocks } : {}
|
|
14243
|
+
};
|
|
14244
|
+
}
|
|
14245
|
+
|
|
13751
14246
|
// src/chat/slack/errors.ts
|
|
13752
14247
|
function getSlackApiErrorCode(error) {
|
|
13753
14248
|
if (!error || typeof error !== "object") {
|
|
@@ -14079,11 +14574,14 @@ async function postResumeFailureReply(args) {
|
|
|
14079
14574
|
}
|
|
14080
14575
|
}
|
|
14081
14576
|
async function postTurnContinuationNoticeBestEffort(args) {
|
|
14577
|
+
const notice = buildSlackTurnContinuationNotice({
|
|
14578
|
+
conversationId: args.resumeArgs.replyContext?.correlation?.conversationId ?? args.lockKey
|
|
14579
|
+
});
|
|
14082
14580
|
try {
|
|
14083
14581
|
await postSlackMessage({
|
|
14084
14582
|
channelId: args.resumeArgs.channelId,
|
|
14085
14583
|
threadTs: args.resumeArgs.threadTs,
|
|
14086
|
-
|
|
14584
|
+
...notice
|
|
14087
14585
|
});
|
|
14088
14586
|
} catch (error) {
|
|
14089
14587
|
logException(
|
|
@@ -14187,6 +14685,10 @@ async function resumeSlackTurn(args) {
|
|
|
14187
14685
|
status.start();
|
|
14188
14686
|
const generateReply = args.generateReply ?? generateAssistantReply;
|
|
14189
14687
|
const replyContext = createResumeReplyContext(args, status);
|
|
14688
|
+
const priorCheckpoint = replyContext.correlation?.conversationId && replyContext.correlation?.turnId ? await getAgentTurnSessionCheckpoint(
|
|
14689
|
+
replyContext.correlation.conversationId,
|
|
14690
|
+
replyContext.correlation.turnId
|
|
14691
|
+
) : void 0;
|
|
14190
14692
|
const replyPromise = generateReply(args.messageText, replyContext);
|
|
14191
14693
|
const replyTimeoutMs = resolveReplyTimeoutMs(args.replyTimeoutMs);
|
|
14192
14694
|
let reply = typeof replyTimeoutMs === "number" ? await Promise.race([
|
|
@@ -14210,9 +14712,12 @@ async function resumeSlackTurn(args) {
|
|
|
14210
14712
|
await status.stop();
|
|
14211
14713
|
const footer = buildSlackReplyFooter({
|
|
14212
14714
|
conversationId: args.replyContext?.correlation?.conversationId ?? lockKey,
|
|
14213
|
-
durationMs: reply.diagnostics.durationMs,
|
|
14715
|
+
durationMs: typeof priorCheckpoint?.cumulativeDurationMs === "number" || typeof reply.diagnostics.durationMs === "number" ? (priorCheckpoint?.cumulativeDurationMs ?? 0) + (reply.diagnostics.durationMs ?? 0) : void 0,
|
|
14214
14716
|
thinkingLevel: reply.diagnostics.thinkingLevel,
|
|
14215
|
-
usage:
|
|
14717
|
+
usage: addAgentTurnUsage(
|
|
14718
|
+
priorCheckpoint?.cumulativeUsage,
|
|
14719
|
+
reply.diagnostics.usage
|
|
14720
|
+
) ?? reply.diagnostics.usage
|
|
14216
14721
|
});
|
|
14217
14722
|
await postSlackApiReplyPosts({
|
|
14218
14723
|
channelId: args.channelId,
|
|
@@ -15063,6 +15568,8 @@ async function resumeCheckpointedOAuthTurn(stored) {
|
|
|
15063
15568
|
fullName: userMessage.author.fullName
|
|
15064
15569
|
},
|
|
15065
15570
|
correlation: {
|
|
15571
|
+
conversationId: stored.resumeConversationId,
|
|
15572
|
+
turnId: resolvedSessionId,
|
|
15066
15573
|
channelId: stored.channelId,
|
|
15067
15574
|
threadTs: stored.threadTs,
|
|
15068
15575
|
requesterId: userMessage.author.userId
|
|
@@ -17563,7 +18070,51 @@ function maybeUpdateAssistantTitle(args) {
|
|
|
17563
18070
|
})();
|
|
17564
18071
|
}
|
|
17565
18072
|
|
|
18073
|
+
// src/chat/services/provider-default-config.ts
|
|
18074
|
+
var GITHUB_REPO_PART = String.raw`[A-Za-z0-9_.-]*[A-Za-z0-9_-]`;
|
|
18075
|
+
var GITHUB_REPO_RE = new RegExp(
|
|
18076
|
+
String.raw`^\s*(?:set|use)\s+(?:the\s+)?default\s+(?:github\s+)?repo(?:sitory)?\s+(?:to|as)\s+(${GITHUB_REPO_PART}/${GITHUB_REPO_PART})(?:\s+for\s+this\s+channel)?[.!?]?\s*$`,
|
|
18077
|
+
"i"
|
|
18078
|
+
);
|
|
18079
|
+
async function maybeApplyProviderDefaultConfigRequest(args) {
|
|
18080
|
+
const match = GITHUB_REPO_RE.exec(args.text);
|
|
18081
|
+
const repo = match?.[1];
|
|
18082
|
+
if (!repo || !args.channelConfiguration) {
|
|
18083
|
+
return null;
|
|
18084
|
+
}
|
|
18085
|
+
await args.channelConfiguration.set({
|
|
18086
|
+
key: "github.repo",
|
|
18087
|
+
value: repo,
|
|
18088
|
+
updatedBy: args.requesterId,
|
|
18089
|
+
source: "provider-default-config"
|
|
18090
|
+
});
|
|
18091
|
+
return {
|
|
18092
|
+
text: `Default GitHub repo set to \`${repo}\`.`
|
|
18093
|
+
};
|
|
18094
|
+
}
|
|
18095
|
+
|
|
17566
18096
|
// src/chat/runtime/reply-executor.ts
|
|
18097
|
+
function collectCanvasUrls(artifacts) {
|
|
18098
|
+
return new Set(
|
|
18099
|
+
[
|
|
18100
|
+
artifacts.lastCanvasUrl,
|
|
18101
|
+
...artifacts.recentCanvases?.map((canvas) => canvas.url) ?? []
|
|
18102
|
+
].filter((url) => typeof url === "string" && url !== "")
|
|
18103
|
+
);
|
|
18104
|
+
}
|
|
18105
|
+
function getCurrentTurnCanvasUrl(args) {
|
|
18106
|
+
const previousUrls = collectCanvasUrls(args.before);
|
|
18107
|
+
const latestUrls = collectCanvasUrls(args.after);
|
|
18108
|
+
for (const url of latestUrls) {
|
|
18109
|
+
if (!previousUrls.has(url)) {
|
|
18110
|
+
return url;
|
|
18111
|
+
}
|
|
18112
|
+
}
|
|
18113
|
+
return void 0;
|
|
18114
|
+
}
|
|
18115
|
+
function buildCanvasRecoveryReply(canvasUrl) {
|
|
18116
|
+
return `I created the canvas, but the turn was interrupted before I could finish the thread reply: ${canvasUrl}`;
|
|
18117
|
+
}
|
|
17567
18118
|
function createReplyToThread(deps) {
|
|
17568
18119
|
return async function replyToThread(thread, message, options = {}) {
|
|
17569
18120
|
if (message.author.isMe) {
|
|
@@ -17632,9 +18183,17 @@ function createReplyToThread(deps) {
|
|
|
17632
18183
|
const postTurnContinuationNotice = async () => {
|
|
17633
18184
|
try {
|
|
17634
18185
|
await beforeFirstResponsePost();
|
|
17635
|
-
|
|
17636
|
-
|
|
17637
|
-
)
|
|
18186
|
+
const notice = buildSlackTurnContinuationNotice({ conversationId });
|
|
18187
|
+
const shouldUseSlackFooter = Boolean(notice.blocks?.length) && Boolean(channelId && threadTs) && thread.adapter?.name === "slack";
|
|
18188
|
+
if (shouldUseSlackFooter && channelId && threadTs) {
|
|
18189
|
+
await postSlackMessage({
|
|
18190
|
+
channelId,
|
|
18191
|
+
threadTs,
|
|
18192
|
+
...notice
|
|
18193
|
+
});
|
|
18194
|
+
return;
|
|
18195
|
+
}
|
|
18196
|
+
await thread.post(buildSlackOutputMessage(notice.text));
|
|
17638
18197
|
} catch (error) {
|
|
17639
18198
|
logException(
|
|
17640
18199
|
error,
|
|
@@ -17708,6 +18267,40 @@ function createReplyToThread(deps) {
|
|
|
17708
18267
|
return;
|
|
17709
18268
|
}
|
|
17710
18269
|
}
|
|
18270
|
+
const configReply = await maybeApplyProviderDefaultConfigRequest({
|
|
18271
|
+
channelConfiguration: preparedState.channelConfiguration,
|
|
18272
|
+
requesterId: message.author.userId,
|
|
18273
|
+
text: userText
|
|
18274
|
+
});
|
|
18275
|
+
if (configReply) {
|
|
18276
|
+
await beforeFirstResponsePost();
|
|
18277
|
+
await thread.post(buildSlackOutputMessage(configReply.text));
|
|
18278
|
+
markConversationMessage(
|
|
18279
|
+
preparedState.conversation,
|
|
18280
|
+
preparedState.userMessageId,
|
|
18281
|
+
{
|
|
18282
|
+
replied: true,
|
|
18283
|
+
skippedReason: void 0
|
|
18284
|
+
}
|
|
18285
|
+
);
|
|
18286
|
+
upsertConversationMessage(preparedState.conversation, {
|
|
18287
|
+
id: generateConversationId("assistant"),
|
|
18288
|
+
role: "assistant",
|
|
18289
|
+
text: normalizeConversationText(configReply.text),
|
|
18290
|
+
createdAtMs: Date.now(),
|
|
18291
|
+
author: {
|
|
18292
|
+
userName: botConfig.userName,
|
|
18293
|
+
isBot: true
|
|
18294
|
+
},
|
|
18295
|
+
meta: {
|
|
18296
|
+
replied: true
|
|
18297
|
+
}
|
|
18298
|
+
});
|
|
18299
|
+
await persistThreadState(thread, {
|
|
18300
|
+
conversation: preparedState.conversation
|
|
18301
|
+
});
|
|
18302
|
+
return;
|
|
18303
|
+
}
|
|
17711
18304
|
startActiveTurn({
|
|
17712
18305
|
conversation: preparedState.conversation,
|
|
17713
18306
|
nextTurnId: turnId,
|
|
@@ -17789,6 +18382,7 @@ function createReplyToThread(deps) {
|
|
|
17789
18382
|
});
|
|
17790
18383
|
let persistedAtLeastOnce = false;
|
|
17791
18384
|
let shouldPersistFailureState = true;
|
|
18385
|
+
let latestArtifacts = preparedState.artifacts;
|
|
17792
18386
|
try {
|
|
17793
18387
|
const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
|
|
17794
18388
|
let reply = await deps.services.generateAssistantReply(userText, {
|
|
@@ -17828,6 +18422,7 @@ function createReplyToThread(deps) {
|
|
|
17828
18422
|
});
|
|
17829
18423
|
},
|
|
17830
18424
|
onArtifactStateUpdated: async (artifacts) => {
|
|
18425
|
+
latestArtifacts = artifacts;
|
|
17831
18426
|
await persistThreadState(thread, { artifacts });
|
|
17832
18427
|
},
|
|
17833
18428
|
onAuthPending: async (pendingAuth) => {
|
|
@@ -18045,6 +18640,60 @@ function createReplyToThread(deps) {
|
|
|
18045
18640
|
}
|
|
18046
18641
|
}
|
|
18047
18642
|
shouldPersistFailureState = true;
|
|
18643
|
+
const createdCanvasUrl = getCurrentTurnCanvasUrl({
|
|
18644
|
+
before: preparedState.artifacts,
|
|
18645
|
+
after: latestArtifacts
|
|
18646
|
+
});
|
|
18647
|
+
if (createdCanvasUrl) {
|
|
18648
|
+
logException(
|
|
18649
|
+
error,
|
|
18650
|
+
"agent_turn_failed_after_canvas_created",
|
|
18651
|
+
turnTraceContext,
|
|
18652
|
+
{
|
|
18653
|
+
...messageTs ? { "messaging.message.id": messageTs } : {},
|
|
18654
|
+
"app.slack.canvas.has_url": true
|
|
18655
|
+
},
|
|
18656
|
+
"Agent turn failed after creating a Slack canvas"
|
|
18657
|
+
);
|
|
18658
|
+
const recoveryText = buildCanvasRecoveryReply(createdCanvasUrl);
|
|
18659
|
+
await postThreadReply(
|
|
18660
|
+
buildSlackOutputMessage(recoveryText),
|
|
18661
|
+
"thread_reply"
|
|
18662
|
+
);
|
|
18663
|
+
markConversationMessage(
|
|
18664
|
+
preparedState.conversation,
|
|
18665
|
+
preparedState.userMessageId,
|
|
18666
|
+
{
|
|
18667
|
+
replied: true,
|
|
18668
|
+
skippedReason: void 0
|
|
18669
|
+
}
|
|
18670
|
+
);
|
|
18671
|
+
upsertConversationMessage(preparedState.conversation, {
|
|
18672
|
+
id: generateConversationId("assistant"),
|
|
18673
|
+
role: "assistant",
|
|
18674
|
+
text: normalizeConversationText(recoveryText),
|
|
18675
|
+
createdAtMs: Date.now(),
|
|
18676
|
+
author: {
|
|
18677
|
+
userName: botConfig.userName,
|
|
18678
|
+
isBot: true
|
|
18679
|
+
},
|
|
18680
|
+
meta: {
|
|
18681
|
+
replied: true
|
|
18682
|
+
}
|
|
18683
|
+
});
|
|
18684
|
+
markTurnCompleted({
|
|
18685
|
+
conversation: preparedState.conversation,
|
|
18686
|
+
nowMs: Date.now(),
|
|
18687
|
+
updateConversationStats
|
|
18688
|
+
});
|
|
18689
|
+
await persistThreadState(thread, {
|
|
18690
|
+
artifacts: latestArtifacts,
|
|
18691
|
+
conversation: preparedState.conversation
|
|
18692
|
+
});
|
|
18693
|
+
persistedAtLeastOnce = true;
|
|
18694
|
+
shouldPersistFailureState = false;
|
|
18695
|
+
return;
|
|
18696
|
+
}
|
|
18048
18697
|
throw error;
|
|
18049
18698
|
} finally {
|
|
18050
18699
|
if (!persistedAtLeastOnce && shouldPersistFailureState) {
|
|
@@ -19185,15 +19834,15 @@ async function defaultWaitUntil() {
|
|
|
19185
19834
|
};
|
|
19186
19835
|
}
|
|
19187
19836
|
}
|
|
19188
|
-
async function
|
|
19837
|
+
async function resolveBuildPluginConfig() {
|
|
19189
19838
|
try {
|
|
19190
19839
|
const mod = await import("#junior/config");
|
|
19191
|
-
return mod.
|
|
19840
|
+
return mod.plugins;
|
|
19192
19841
|
} catch {
|
|
19193
19842
|
const env = process.env.JUNIOR_PLUGIN_PACKAGES;
|
|
19194
19843
|
if (env) {
|
|
19195
19844
|
try {
|
|
19196
|
-
return JSON.parse(env);
|
|
19845
|
+
return { packages: JSON.parse(env) };
|
|
19197
19846
|
} catch {
|
|
19198
19847
|
}
|
|
19199
19848
|
}
|
|
@@ -19201,9 +19850,9 @@ async function resolveBuildPluginPackages() {
|
|
|
19201
19850
|
}
|
|
19202
19851
|
}
|
|
19203
19852
|
async function createApp(options) {
|
|
19204
|
-
|
|
19205
|
-
|
|
19206
|
-
);
|
|
19853
|
+
const pluginConfig = options?.plugins ?? await resolveBuildPluginConfig();
|
|
19854
|
+
setPluginPackages(pluginConfig?.packages);
|
|
19855
|
+
setPluginConfig(pluginConfig);
|
|
19207
19856
|
setConfigDefaults(options?.configDefaults);
|
|
19208
19857
|
const waitUntil = options?.waitUntil ?? await defaultWaitUntil();
|
|
19209
19858
|
const app = new Hono();
|