@sentry/junior 0.69.0 → 0.70.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/dist/app.js +323 -22
- package/dist/chat/plugins/credential-hooks.d.ts +19 -0
- package/dist/chat/sandbox/egress-credentials.d.ts +1 -0
- package/dist/chat/sandbox/egress-schemas.d.ts +1 -1
- package/dist/chat/state/conversation-details.d.ts +46 -0
- package/dist/chat/state/turn-session.d.ts +0 -3
- package/dist/{chunk-N3MORKTH.js → chunk-HOGQL2H6.js} +123 -12
- package/dist/cli/init.js +18 -1
- package/dist/reporting.d.ts +6 -2
- package/dist/reporting.js +62 -21
- package/package.json +3 -3
package/dist/app.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
getAgentPlugins,
|
|
20
20
|
getAgentTurnSessionRecord,
|
|
21
21
|
getInterruptionMarker,
|
|
22
|
+
initConversationContext,
|
|
22
23
|
listAgentTurnSessionSummaries,
|
|
23
24
|
listAgentTurnSessionSummariesForConversation,
|
|
24
25
|
loadConnectedMcpProviders,
|
|
@@ -32,12 +33,13 @@ import {
|
|
|
32
33
|
resolveSlackChannelTypeFromMessage,
|
|
33
34
|
resolveSlackConversationContext,
|
|
34
35
|
setAgentPlugins,
|
|
36
|
+
setConversationTitle,
|
|
35
37
|
splitSlackReplyText,
|
|
36
38
|
truncateStatusText,
|
|
37
39
|
upsertAgentTurnSessionRecord,
|
|
38
40
|
validateAgentPlugins,
|
|
39
41
|
verifySlackDirectCredentialSubject
|
|
40
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-HOGQL2H6.js";
|
|
41
43
|
import {
|
|
42
44
|
discoverSkills,
|
|
43
45
|
findSkillByName,
|
|
@@ -6545,6 +6547,7 @@ import {
|
|
|
6545
6547
|
agentPluginProviderAccountSchema
|
|
6546
6548
|
} from "@sentry/junior-plugin-api";
|
|
6547
6549
|
var finiteNumberSchema = z.number().refine(Number.isFinite);
|
|
6550
|
+
var httpStatusSchema = z.number().int().min(100).max(599);
|
|
6548
6551
|
var providerNameSchema = z.string().regex(/^[a-z][a-z0-9-]*$/);
|
|
6549
6552
|
var sandboxEgressGrantSchema = agentPluginGrantSchema;
|
|
6550
6553
|
var sandboxEgressCredentialContextSchema = z.object({
|
|
@@ -6584,7 +6587,7 @@ var sandboxEgressPermissionDeniedSignalSchema = z.object({
|
|
|
6584
6587
|
provider: providerNameSchema,
|
|
6585
6588
|
source: z.literal("upstream"),
|
|
6586
6589
|
sso: z.string().optional(),
|
|
6587
|
-
status:
|
|
6590
|
+
status: httpStatusSchema,
|
|
6588
6591
|
upstreamHost: z.string().min(1),
|
|
6589
6592
|
upstreamPath: z.string().min(1),
|
|
6590
6593
|
createdAtMs: finiteNumberSchema
|
|
@@ -8482,6 +8485,10 @@ function stringField(record, key) {
|
|
|
8482
8485
|
const value = record[key];
|
|
8483
8486
|
return typeof value === "string" ? value : "";
|
|
8484
8487
|
}
|
|
8488
|
+
function numberField(record, key) {
|
|
8489
|
+
const value = record[key];
|
|
8490
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
8491
|
+
}
|
|
8485
8492
|
function stringListField(record, key) {
|
|
8486
8493
|
const value = record[key];
|
|
8487
8494
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
@@ -8502,14 +8509,15 @@ function upstreamPermissionDeniedText(value) {
|
|
|
8502
8509
|
return void 0;
|
|
8503
8510
|
}
|
|
8504
8511
|
const signal = value.permission_denied;
|
|
8505
|
-
if (signal.source !== "upstream"
|
|
8512
|
+
if (signal.source !== "upstream") {
|
|
8506
8513
|
return void 0;
|
|
8507
8514
|
}
|
|
8508
8515
|
const provider = stringField(signal, "provider");
|
|
8509
8516
|
const message = stringField(signal, "message");
|
|
8510
8517
|
const upstreamHost = stringField(signal, "upstreamHost");
|
|
8511
8518
|
const upstreamPath2 = stringField(signal, "upstreamPath");
|
|
8512
|
-
|
|
8519
|
+
const status = numberField(signal, "status");
|
|
8520
|
+
if (!provider || !message || !upstreamHost || !upstreamPath2 || !status) {
|
|
8513
8521
|
return void 0;
|
|
8514
8522
|
}
|
|
8515
8523
|
const grant = isRecord3(signal.grant) ? signal.grant : {};
|
|
@@ -8535,7 +8543,7 @@ function upstreamPermissionDeniedText(value) {
|
|
|
8535
8543
|
...grantRequirements.map((item) => `- ${item}`)
|
|
8536
8544
|
] : [],
|
|
8537
8545
|
`Upstream: ${upstreamHost}${upstreamPath2}`,
|
|
8538
|
-
|
|
8546
|
+
`Status: ${status}`,
|
|
8539
8547
|
...acceptedPermissions ? [`Accepted provider permissions: ${acceptedPermissions}`] : [],
|
|
8540
8548
|
...sso ? [`Provider SSO: ${sso}`] : [],
|
|
8541
8549
|
...command ? [`Command: ${command}`] : [],
|
|
@@ -8953,12 +8961,41 @@ async function selectPluginGrant(input) {
|
|
|
8953
8961
|
plugin: { name: plugin.name },
|
|
8954
8962
|
log: createAgentPluginLogger(plugin.name),
|
|
8955
8963
|
request: {
|
|
8964
|
+
...input.bodyText !== void 0 ? { bodyText: input.bodyText } : {},
|
|
8956
8965
|
method: input.method,
|
|
8957
8966
|
url: input.upstreamUrl.toString()
|
|
8958
8967
|
}
|
|
8959
8968
|
});
|
|
8960
8969
|
return result === void 0 ? void 0 : parseGrant(result, plugin.name);
|
|
8961
8970
|
}
|
|
8971
|
+
async function onPluginEgressResponse(input) {
|
|
8972
|
+
const plugin = agentPluginFor(input.provider);
|
|
8973
|
+
const hook = plugin?.hooks?.onEgressResponse;
|
|
8974
|
+
if (!plugin || !hook) {
|
|
8975
|
+
return {};
|
|
8976
|
+
}
|
|
8977
|
+
let permissionDenied;
|
|
8978
|
+
await hook({
|
|
8979
|
+
plugin: { name: plugin.name },
|
|
8980
|
+
log: createAgentPluginLogger(plugin.name),
|
|
8981
|
+
grant: input.grant,
|
|
8982
|
+
permissionDenied(message) {
|
|
8983
|
+
const trimmed = message.trim();
|
|
8984
|
+
if (!trimmed) {
|
|
8985
|
+
throw new Error(
|
|
8986
|
+
`Plugin "${plugin.name}" onEgressResponse permissionDenied message is empty`
|
|
8987
|
+
);
|
|
8988
|
+
}
|
|
8989
|
+
permissionDenied = { message: trimmed };
|
|
8990
|
+
},
|
|
8991
|
+
request: {
|
|
8992
|
+
method: input.method,
|
|
8993
|
+
url: input.upstreamUrl.toString()
|
|
8994
|
+
},
|
|
8995
|
+
response: input.response
|
|
8996
|
+
});
|
|
8997
|
+
return permissionDenied ? { permissionDenied } : {};
|
|
8998
|
+
}
|
|
8962
8999
|
function hasEgressCredentialHooks(provider) {
|
|
8963
9000
|
const hooks = agentPluginFor(provider)?.hooks;
|
|
8964
9001
|
return Boolean(hooks?.grantForEgress || hooks?.issueCredential);
|
|
@@ -17426,6 +17463,7 @@ async function selectSandboxEgressGrant(input) {
|
|
|
17426
17463
|
return defaultGrantForProvider(input);
|
|
17427
17464
|
}
|
|
17428
17465
|
const pluginGrant = await selectPluginGrant({
|
|
17466
|
+
...input.bodyText !== void 0 ? { bodyText: input.bodyText } : {},
|
|
17429
17467
|
provider: input.provider,
|
|
17430
17468
|
method: input.method,
|
|
17431
17469
|
upstreamUrl: input.upstreamUrl
|
|
@@ -17587,6 +17625,7 @@ async function verifyVercelSandboxOidcToken(token) {
|
|
|
17587
17625
|
}
|
|
17588
17626
|
|
|
17589
17627
|
// src/chat/sandbox/egress-proxy.ts
|
|
17628
|
+
import { EgressAuthRequired } from "@sentry/junior-plugin-api";
|
|
17590
17629
|
var OIDC_TOKEN_HEADER = "vercel-sandbox-oidc-token";
|
|
17591
17630
|
var FORWARDED_HOST_HEADER = "vercel-forwarded-host";
|
|
17592
17631
|
var FORWARDED_SCHEME_HEADER = "vercel-forwarded-scheme";
|
|
@@ -17616,6 +17655,8 @@ var DECODED_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
|
17616
17655
|
]);
|
|
17617
17656
|
var UPSTREAM_TOKEN_REJECTION_STATUS = 401;
|
|
17618
17657
|
var UPSTREAM_PERMISSION_REJECTION_STATUS = 403;
|
|
17658
|
+
var GRANT_SELECTION_BODY_TEXT_LIMIT_BYTES = 64 * 1024;
|
|
17659
|
+
var RESPONSE_BODY_TEXT_LIMIT_BYTES = 64 * 1024;
|
|
17619
17660
|
function jsonError(message, status) {
|
|
17620
17661
|
return Response.json({ error: message }, { status });
|
|
17621
17662
|
}
|
|
@@ -17714,6 +17755,9 @@ function githubPermissionHeaders(upstream) {
|
|
|
17714
17755
|
function permissionDeniedMessage(provider, grant) {
|
|
17715
17756
|
return `${provider} returned HTTP 403 after Junior injected the ${grant.name} grant. Junior forwarded the request; this is not a local runtime block.`;
|
|
17716
17757
|
}
|
|
17758
|
+
function isEgressAuthRequired(error) {
|
|
17759
|
+
return error instanceof EgressAuthRequired || error instanceof Error && error.name === "EgressAuthRequired";
|
|
17760
|
+
}
|
|
17717
17761
|
function logSandboxEgressUpstreamRequest(input) {
|
|
17718
17762
|
if (!shouldLogSandboxEgressInfo()) {
|
|
17719
17763
|
return;
|
|
@@ -17822,6 +17866,78 @@ async function requestBodyBytes(request) {
|
|
|
17822
17866
|
}
|
|
17823
17867
|
return await request.arrayBuffer();
|
|
17824
17868
|
}
|
|
17869
|
+
function isGrantSelectionBodyVisible(input) {
|
|
17870
|
+
return input.provider === "github" && input.upstreamUrl.hostname.toLowerCase() === "api.github.com" && input.upstreamUrl.pathname.toLowerCase().endsWith("/graphql");
|
|
17871
|
+
}
|
|
17872
|
+
function requestBodyText(body) {
|
|
17873
|
+
if (body === void 0 || body.byteLength > GRANT_SELECTION_BODY_TEXT_LIMIT_BYTES) {
|
|
17874
|
+
return void 0;
|
|
17875
|
+
}
|
|
17876
|
+
return new TextDecoder().decode(body);
|
|
17877
|
+
}
|
|
17878
|
+
function responseContentLength(upstream) {
|
|
17879
|
+
const raw = upstream.headers.get("content-length");
|
|
17880
|
+
if (!raw) {
|
|
17881
|
+
return void 0;
|
|
17882
|
+
}
|
|
17883
|
+
const parsed = Number(raw);
|
|
17884
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
|
|
17885
|
+
}
|
|
17886
|
+
async function responseTextWithinLimit(upstream, maxBytes) {
|
|
17887
|
+
const limit = Math.min(
|
|
17888
|
+
Math.max(0, Math.floor(maxBytes)),
|
|
17889
|
+
RESPONSE_BODY_TEXT_LIMIT_BYTES
|
|
17890
|
+
);
|
|
17891
|
+
if (limit <= 0) {
|
|
17892
|
+
return void 0;
|
|
17893
|
+
}
|
|
17894
|
+
const contentLength = responseContentLength(upstream);
|
|
17895
|
+
if (contentLength !== void 0 && contentLength > limit) {
|
|
17896
|
+
return void 0;
|
|
17897
|
+
}
|
|
17898
|
+
let clone;
|
|
17899
|
+
try {
|
|
17900
|
+
clone = upstream.clone();
|
|
17901
|
+
} catch {
|
|
17902
|
+
return void 0;
|
|
17903
|
+
}
|
|
17904
|
+
const body = clone.body;
|
|
17905
|
+
if (!body) {
|
|
17906
|
+
return "";
|
|
17907
|
+
}
|
|
17908
|
+
const reader = body.getReader();
|
|
17909
|
+
const chunks = [];
|
|
17910
|
+
let bytes = 0;
|
|
17911
|
+
try {
|
|
17912
|
+
while (true) {
|
|
17913
|
+
const { done, value } = await reader.read();
|
|
17914
|
+
if (done) {
|
|
17915
|
+
break;
|
|
17916
|
+
}
|
|
17917
|
+
if (!value) {
|
|
17918
|
+
continue;
|
|
17919
|
+
}
|
|
17920
|
+
bytes += value.byteLength;
|
|
17921
|
+
if (bytes > limit) {
|
|
17922
|
+
await reader.cancel().catch(() => void 0);
|
|
17923
|
+
return void 0;
|
|
17924
|
+
}
|
|
17925
|
+
chunks.push(value);
|
|
17926
|
+
}
|
|
17927
|
+
} catch {
|
|
17928
|
+
await reader.cancel().catch(() => void 0);
|
|
17929
|
+
return void 0;
|
|
17930
|
+
} finally {
|
|
17931
|
+
reader.releaseLock();
|
|
17932
|
+
}
|
|
17933
|
+
const combined = new Uint8Array(bytes);
|
|
17934
|
+
let offset = 0;
|
|
17935
|
+
for (const chunk of chunks) {
|
|
17936
|
+
combined.set(chunk, offset);
|
|
17937
|
+
offset += chunk.byteLength;
|
|
17938
|
+
}
|
|
17939
|
+
return new TextDecoder().decode(combined);
|
|
17940
|
+
}
|
|
17825
17941
|
function requestHeaders(request, lease, upstreamHost) {
|
|
17826
17942
|
const headers = new Headers();
|
|
17827
17943
|
request.headers.forEach((value, key) => {
|
|
@@ -17956,7 +18072,14 @@ async function proxySandboxEgressRequest(request, deps = {}) {
|
|
|
17956
18072
|
403
|
|
17957
18073
|
);
|
|
17958
18074
|
}
|
|
18075
|
+
let body;
|
|
18076
|
+
let bodyRead = false;
|
|
18077
|
+
if (isGrantSelectionBodyVisible({ provider, upstreamUrl })) {
|
|
18078
|
+
body = await requestBodyBytes(request);
|
|
18079
|
+
bodyRead = true;
|
|
18080
|
+
}
|
|
17959
18081
|
const grantSelection = await selectSandboxEgressGrant({
|
|
18082
|
+
bodyText: requestBodyText(body),
|
|
17960
18083
|
provider,
|
|
17961
18084
|
method: request.method,
|
|
17962
18085
|
upstreamUrl
|
|
@@ -18067,7 +18190,9 @@ async function proxySandboxEgressRequest(request, deps = {}) {
|
|
|
18067
18190
|
}
|
|
18068
18191
|
const fetchImpl = deps.fetch ?? fetch;
|
|
18069
18192
|
const headers = requestHeaders(request, lease, upstreamUrl.hostname);
|
|
18070
|
-
|
|
18193
|
+
if (!bodyRead) {
|
|
18194
|
+
body = await requestBodyBytes(request);
|
|
18195
|
+
}
|
|
18071
18196
|
const intercepted = await deps.interceptHttp?.({
|
|
18072
18197
|
provider,
|
|
18073
18198
|
request: new Request(upstreamUrl, {
|
|
@@ -18086,6 +18211,93 @@ async function proxySandboxEgressRequest(request, deps = {}) {
|
|
|
18086
18211
|
...body !== void 0 ? { body } : {},
|
|
18087
18212
|
redirect: "manual"
|
|
18088
18213
|
});
|
|
18214
|
+
try {
|
|
18215
|
+
const effects = await onPluginEgressResponse({
|
|
18216
|
+
provider,
|
|
18217
|
+
grant: lease.grant,
|
|
18218
|
+
method: request.method,
|
|
18219
|
+
upstreamUrl,
|
|
18220
|
+
response: {
|
|
18221
|
+
headers: new Headers(upstream.headers),
|
|
18222
|
+
readText: async (maxBytes) => await responseTextWithinLimit(upstream, maxBytes),
|
|
18223
|
+
status: upstream.status
|
|
18224
|
+
}
|
|
18225
|
+
});
|
|
18226
|
+
if (effects.permissionDenied) {
|
|
18227
|
+
await setSandboxEgressPermissionDeniedSignal(credentialContext, {
|
|
18228
|
+
provider,
|
|
18229
|
+
grant: lease.grant,
|
|
18230
|
+
...lease.account ? { account: lease.account } : {},
|
|
18231
|
+
message: effects.permissionDenied.message,
|
|
18232
|
+
source: "upstream",
|
|
18233
|
+
status: upstream.status,
|
|
18234
|
+
upstreamHost: upstreamUrl.hostname,
|
|
18235
|
+
upstreamPath: displayedUpstreamPath(upstreamUrl),
|
|
18236
|
+
...provider === "github" ? githubPermissionHeaders(upstream) : {}
|
|
18237
|
+
});
|
|
18238
|
+
logWarn(
|
|
18239
|
+
"sandbox_egress_upstream_permission_classified",
|
|
18240
|
+
{},
|
|
18241
|
+
{
|
|
18242
|
+
...egressAttributes({
|
|
18243
|
+
egressId: activeEgressId,
|
|
18244
|
+
grantAccess: lease.grant.access,
|
|
18245
|
+
grantName: lease.grant.name,
|
|
18246
|
+
grantReason: lease.grant.reason,
|
|
18247
|
+
host: upstreamUrl.hostname,
|
|
18248
|
+
method: request.method,
|
|
18249
|
+
path: upstreamUrl.pathname,
|
|
18250
|
+
provider,
|
|
18251
|
+
status: upstream.status
|
|
18252
|
+
}),
|
|
18253
|
+
...routingAttributes(request, upstreamUrl),
|
|
18254
|
+
...upstreamPermissionAttributes(provider, upstream)
|
|
18255
|
+
},
|
|
18256
|
+
"Sandbox egress plugin classified upstream response as permission denied"
|
|
18257
|
+
);
|
|
18258
|
+
}
|
|
18259
|
+
} catch (error) {
|
|
18260
|
+
if (!isEgressAuthRequired(error)) {
|
|
18261
|
+
throw error;
|
|
18262
|
+
}
|
|
18263
|
+
await clearSandboxEgressCredentialLease(
|
|
18264
|
+
provider,
|
|
18265
|
+
lease.grant.name,
|
|
18266
|
+
credentialContext
|
|
18267
|
+
);
|
|
18268
|
+
await setSandboxEgressAuthRequiredSignal(credentialContext, {
|
|
18269
|
+
provider,
|
|
18270
|
+
grant: lease.grant,
|
|
18271
|
+
...error.authorization ?? lease.authorization ? { authorization: error.authorization ?? lease.authorization } : {},
|
|
18272
|
+
message: error.message
|
|
18273
|
+
});
|
|
18274
|
+
logWarn(
|
|
18275
|
+
"sandbox_egress_upstream_auth_required_classified",
|
|
18276
|
+
{},
|
|
18277
|
+
{
|
|
18278
|
+
...egressAttributes({
|
|
18279
|
+
egressId: activeEgressId,
|
|
18280
|
+
grantAccess: lease.grant.access,
|
|
18281
|
+
grantName: lease.grant.name,
|
|
18282
|
+
grantReason: lease.grant.reason,
|
|
18283
|
+
host: upstreamUrl.hostname,
|
|
18284
|
+
method: request.method,
|
|
18285
|
+
path: upstreamUrl.pathname,
|
|
18286
|
+
provider,
|
|
18287
|
+
status: upstream.status
|
|
18288
|
+
}),
|
|
18289
|
+
...routingAttributes(request, upstreamUrl),
|
|
18290
|
+
...upstreamPermissionAttributes(provider, upstream)
|
|
18291
|
+
},
|
|
18292
|
+
"Sandbox egress plugin classified upstream response as auth required"
|
|
18293
|
+
);
|
|
18294
|
+
await upstream.body?.cancel().catch(() => void 0);
|
|
18295
|
+
return authRequiredResponse({
|
|
18296
|
+
provider,
|
|
18297
|
+
grant: lease.grant,
|
|
18298
|
+
message: error.message
|
|
18299
|
+
});
|
|
18300
|
+
}
|
|
18089
18301
|
logSandboxEgressUpstreamRequest({
|
|
18090
18302
|
egressId: activeEgressId,
|
|
18091
18303
|
grantAccess: lease.grant.access,
|
|
@@ -19034,6 +19246,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
19034
19246
|
text: args.text
|
|
19035
19247
|
});
|
|
19036
19248
|
}
|
|
19249
|
+
await args.onInputCommitted?.();
|
|
19037
19250
|
};
|
|
19038
19251
|
return {
|
|
19039
19252
|
async handleNewMention(thread, message, hooks) {
|
|
@@ -19222,6 +19435,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
19222
19435
|
message,
|
|
19223
19436
|
decision: { shouldReply: false, reason },
|
|
19224
19437
|
context: threadContext,
|
|
19438
|
+
onInputCommitted: hooks.onInputCommitted,
|
|
19225
19439
|
text: combinedText
|
|
19226
19440
|
});
|
|
19227
19441
|
return;
|
|
@@ -19258,6 +19472,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
19258
19472
|
message,
|
|
19259
19473
|
decision,
|
|
19260
19474
|
context: threadContext,
|
|
19475
|
+
onInputCommitted: hooks.onInputCommitted,
|
|
19261
19476
|
preparedState,
|
|
19262
19477
|
text: combinedText
|
|
19263
19478
|
});
|
|
@@ -19269,6 +19484,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
19269
19484
|
message,
|
|
19270
19485
|
decision,
|
|
19271
19486
|
context: threadContext,
|
|
19487
|
+
onInputCommitted: hooks.onInputCommitted,
|
|
19272
19488
|
preparedState,
|
|
19273
19489
|
text: combinedText
|
|
19274
19490
|
});
|
|
@@ -20694,12 +20910,13 @@ function createReplyToThread(deps) {
|
|
|
20694
20910
|
updateConversationStats
|
|
20695
20911
|
});
|
|
20696
20912
|
if (conversationId) {
|
|
20913
|
+
const turnStartedAtMs = message.metadata.dateSent.getTime();
|
|
20697
20914
|
void recordAgentTurnSessionSummary({
|
|
20698
20915
|
channelName,
|
|
20699
20916
|
conversationId,
|
|
20700
20917
|
sessionId: turnId,
|
|
20701
20918
|
sliceId: 1,
|
|
20702
|
-
startedAtMs:
|
|
20919
|
+
startedAtMs: turnStartedAtMs,
|
|
20703
20920
|
state: "running",
|
|
20704
20921
|
surface: "slack",
|
|
20705
20922
|
requester,
|
|
@@ -20710,12 +20927,41 @@ function createReplyToThread(deps) {
|
|
|
20710
20927
|
error,
|
|
20711
20928
|
"agent_turn_summary_record_failed",
|
|
20712
20929
|
turnTraceContext,
|
|
20713
|
-
{
|
|
20714
|
-
"app.agent.turn.state": "running"
|
|
20715
|
-
},
|
|
20930
|
+
{ "app.agent.turn.state": "running" },
|
|
20716
20931
|
"Failed to record running turn summary"
|
|
20717
20932
|
);
|
|
20718
20933
|
});
|
|
20934
|
+
void initConversationContext(conversationId, {
|
|
20935
|
+
channelName,
|
|
20936
|
+
originSurface: "slack",
|
|
20937
|
+
originRequester: requester,
|
|
20938
|
+
startedAtMs: turnStartedAtMs
|
|
20939
|
+
}).catch((error) => {
|
|
20940
|
+
logException(
|
|
20941
|
+
error,
|
|
20942
|
+
"conversation_details_context_init_failed",
|
|
20943
|
+
turnTraceContext,
|
|
20944
|
+
{ "app.agent.turn.state": "running" },
|
|
20945
|
+
"Failed to init conversation context at turn start"
|
|
20946
|
+
);
|
|
20947
|
+
});
|
|
20948
|
+
const existingAssistantTitle = preparedState.artifacts.assistantTitle?.trim();
|
|
20949
|
+
if (existingAssistantTitle) {
|
|
20950
|
+
void setConversationTitle(conversationId, {
|
|
20951
|
+
displayTitle: existingAssistantTitle,
|
|
20952
|
+
...preparedState.artifacts.assistantTitleSourceMessageId ? {
|
|
20953
|
+
titleSourceMessageId: preparedState.artifacts.assistantTitleSourceMessageId
|
|
20954
|
+
} : {}
|
|
20955
|
+
}).catch((error) => {
|
|
20956
|
+
logException(
|
|
20957
|
+
error,
|
|
20958
|
+
"conversation_details_title_refresh_failed",
|
|
20959
|
+
turnTraceContext,
|
|
20960
|
+
{ "app.agent.turn.state": "running" },
|
|
20961
|
+
"Failed to refresh conversation title from artifacts"
|
|
20962
|
+
);
|
|
20963
|
+
});
|
|
20964
|
+
}
|
|
20719
20965
|
}
|
|
20720
20966
|
setTags({
|
|
20721
20967
|
conversationId
|
|
@@ -20791,6 +21037,7 @@ function createReplyToThread(deps) {
|
|
|
20791
21037
|
let persistedAtLeastOnce = false;
|
|
20792
21038
|
let shouldPersistFailureState = true;
|
|
20793
21039
|
let latestArtifacts = preparedState.artifacts;
|
|
21040
|
+
let assistantTitleArtifacts = {};
|
|
20794
21041
|
try {
|
|
20795
21042
|
const loadedPiMessages = await loadPiMessagesForTurn({
|
|
20796
21043
|
conversationId,
|
|
@@ -20833,6 +21080,54 @@ function createReplyToThread(deps) {
|
|
|
20833
21080
|
runId,
|
|
20834
21081
|
threadId
|
|
20835
21082
|
});
|
|
21083
|
+
void assistantTitleTask.then(async (titleUpdateResult) => {
|
|
21084
|
+
if (!titleUpdateResult) return;
|
|
21085
|
+
assistantTitleArtifacts = {
|
|
21086
|
+
assistantTitleSourceMessageId: titleUpdateResult.sourceMessageId,
|
|
21087
|
+
...titleUpdateResult.title ? { assistantTitle: titleUpdateResult.title } : {}
|
|
21088
|
+
};
|
|
21089
|
+
latestArtifacts = {
|
|
21090
|
+
...latestArtifacts,
|
|
21091
|
+
...assistantTitleArtifacts
|
|
21092
|
+
};
|
|
21093
|
+
if (conversationId && titleUpdateResult.title) {
|
|
21094
|
+
try {
|
|
21095
|
+
await setConversationTitle(conversationId, {
|
|
21096
|
+
displayTitle: titleUpdateResult.title,
|
|
21097
|
+
titleSourceMessageId: titleUpdateResult.sourceMessageId
|
|
21098
|
+
});
|
|
21099
|
+
} catch (error) {
|
|
21100
|
+
logException(
|
|
21101
|
+
error,
|
|
21102
|
+
"conversation_details_title_set_failed",
|
|
21103
|
+
turnTraceContext,
|
|
21104
|
+
{},
|
|
21105
|
+
"Failed to set conversation title in details record"
|
|
21106
|
+
);
|
|
21107
|
+
}
|
|
21108
|
+
}
|
|
21109
|
+
try {
|
|
21110
|
+
await persistThreadState(thread, {
|
|
21111
|
+
artifacts: latestArtifacts
|
|
21112
|
+
});
|
|
21113
|
+
} catch (error) {
|
|
21114
|
+
logException(
|
|
21115
|
+
error,
|
|
21116
|
+
"assistant_title_artifact_persist_failed",
|
|
21117
|
+
turnTraceContext,
|
|
21118
|
+
{},
|
|
21119
|
+
"Failed to persist async assistant title artifact state"
|
|
21120
|
+
);
|
|
21121
|
+
}
|
|
21122
|
+
}).catch((error) => {
|
|
21123
|
+
logException(
|
|
21124
|
+
error,
|
|
21125
|
+
"assistant_title_task_failed",
|
|
21126
|
+
turnTraceContext,
|
|
21127
|
+
{},
|
|
21128
|
+
"Async assistant title task failed"
|
|
21129
|
+
);
|
|
21130
|
+
});
|
|
20836
21131
|
const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
|
|
20837
21132
|
const resolveSteeringMessages = async (queuedMessages) => {
|
|
20838
21133
|
return await Promise.all(
|
|
@@ -20911,8 +21206,13 @@ function createReplyToThread(deps) {
|
|
|
20911
21206
|
});
|
|
20912
21207
|
},
|
|
20913
21208
|
onArtifactStateUpdated: async (artifacts) => {
|
|
20914
|
-
latestArtifacts =
|
|
20915
|
-
|
|
21209
|
+
latestArtifacts = {
|
|
21210
|
+
...artifacts,
|
|
21211
|
+
...assistantTitleArtifacts
|
|
21212
|
+
};
|
|
21213
|
+
await persistThreadState(thread, {
|
|
21214
|
+
artifacts: latestArtifacts
|
|
21215
|
+
});
|
|
20916
21216
|
},
|
|
20917
21217
|
onAuthPending: async (pendingAuth) => {
|
|
20918
21218
|
await applyPendingAuthUpdate({
|
|
@@ -21013,24 +21313,26 @@ function createReplyToThread(deps) {
|
|
|
21013
21313
|
await sent.delete();
|
|
21014
21314
|
}
|
|
21015
21315
|
}
|
|
21016
|
-
const titleUpdateResult = await assistantTitleTask;
|
|
21017
|
-
if (titleUpdateResult) {
|
|
21018
|
-
artifactStatePatch.assistantTitleSourceMessageId = titleUpdateResult.sourceMessageId;
|
|
21019
|
-
if (titleUpdateResult.title) {
|
|
21020
|
-
artifactStatePatch.assistantTitle = titleUpdateResult.title;
|
|
21021
|
-
}
|
|
21022
|
-
}
|
|
21023
21316
|
const completedState = buildDeliveredTurnStatePatch({
|
|
21024
|
-
artifactStatePatch
|
|
21025
|
-
|
|
21317
|
+
artifactStatePatch: {
|
|
21318
|
+
...artifactStatePatch,
|
|
21319
|
+
...assistantTitleArtifacts
|
|
21320
|
+
},
|
|
21321
|
+
artifacts: latestArtifacts,
|
|
21026
21322
|
conversation: preparedState.conversation,
|
|
21027
21323
|
reply,
|
|
21028
21324
|
sessionId: turnId,
|
|
21029
21325
|
userMessageId: preparedState.userMessageId
|
|
21030
21326
|
});
|
|
21327
|
+
if (completedState.artifacts) {
|
|
21328
|
+
latestArtifacts = completedState.artifacts;
|
|
21329
|
+
}
|
|
21031
21330
|
await persistThreadState(thread, {
|
|
21032
21331
|
...completedState
|
|
21033
21332
|
});
|
|
21333
|
+
if (completedState.artifacts && (assistantTitleArtifacts.assistantTitle !== void 0 || assistantTitleArtifacts.assistantTitleSourceMessageId !== void 0) && (completedState.artifacts.assistantTitle !== assistantTitleArtifacts.assistantTitle || completedState.artifacts.assistantTitleSourceMessageId !== assistantTitleArtifacts.assistantTitleSourceMessageId)) {
|
|
21334
|
+
await persistThreadState(thread, { artifacts: latestArtifacts });
|
|
21335
|
+
}
|
|
21034
21336
|
if (conversationId) {
|
|
21035
21337
|
await recordAgentTurnSessionSummary({
|
|
21036
21338
|
channelName,
|
|
@@ -21041,7 +21343,6 @@ function createReplyToThread(deps) {
|
|
|
21041
21343
|
sliceId: 1,
|
|
21042
21344
|
startedAtMs: message.metadata.dateSent.getTime(),
|
|
21043
21345
|
state: "completed",
|
|
21044
|
-
conversationTitle: titleUpdateResult?.title,
|
|
21045
21346
|
requester,
|
|
21046
21347
|
destination,
|
|
21047
21348
|
traceId: getActiveTraceId()
|
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
import { type AgentPluginCredentialResult, type AgentPluginGrant, type AgentPluginProviderAccount } from "@sentry/junior-plugin-api";
|
|
2
2
|
import type { StoredTokens, UserTokenStore } from "@/chat/credentials/user-token-store";
|
|
3
3
|
export interface EgressGrantInput {
|
|
4
|
+
bodyText?: string;
|
|
4
5
|
method: string;
|
|
5
6
|
provider: string;
|
|
6
7
|
upstreamUrl: URL;
|
|
7
8
|
}
|
|
8
9
|
/** Ask a plugin which grant an outbound request needs. */
|
|
9
10
|
export declare function selectPluginGrant(input: EgressGrantInput): Promise<AgentPluginGrant | undefined>;
|
|
11
|
+
export interface EgressResponseInput {
|
|
12
|
+
grant: AgentPluginGrant;
|
|
13
|
+
method: string;
|
|
14
|
+
provider: string;
|
|
15
|
+
response: {
|
|
16
|
+
headers: Headers;
|
|
17
|
+
readText(maxBytes: number): Promise<string | undefined>;
|
|
18
|
+
status: number;
|
|
19
|
+
};
|
|
20
|
+
upstreamUrl: URL;
|
|
21
|
+
}
|
|
22
|
+
export interface EgressResponseEffects {
|
|
23
|
+
permissionDenied?: {
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Let the owning plugin inspect an upstream response without changing pass-through behavior. */
|
|
28
|
+
export declare function onPluginEgressResponse(input: EgressResponseInput): Promise<EgressResponseEffects>;
|
|
10
29
|
/** Return whether a plugin owns credential issuance for egress. */
|
|
11
30
|
export declare function hasEgressCredentialHooks(provider: string): boolean;
|
|
12
31
|
export interface IssueCredentialInput {
|
|
@@ -21,6 +21,7 @@ export declare class SandboxEgressCredentialNeededError extends Error {
|
|
|
21
21
|
}
|
|
22
22
|
/** Select the plugin-defined or default grant needed for one outbound request. */
|
|
23
23
|
export declare function selectSandboxEgressGrant(input: {
|
|
24
|
+
bodyText?: string;
|
|
24
25
|
method: string;
|
|
25
26
|
provider: string;
|
|
26
27
|
upstreamUrl: URL;
|
|
@@ -89,7 +89,7 @@ export declare const sandboxEgressPermissionDeniedSignalSchema: z.ZodObject<{
|
|
|
89
89
|
provider: z.ZodString;
|
|
90
90
|
source: z.ZodLiteral<"upstream">;
|
|
91
91
|
sso: z.ZodOptional<z.ZodString>;
|
|
92
|
-
status: z.
|
|
92
|
+
status: z.ZodNumber;
|
|
93
93
|
upstreamHost: z.ZodString;
|
|
94
94
|
upstreamPath: z.ZodString;
|
|
95
95
|
createdAtMs: z.ZodNumber;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AgentTurnRequester, AgentTurnSurface } from "./turn-session";
|
|
2
|
+
export interface ConversationDetailsRecord {
|
|
3
|
+
conversationId: string;
|
|
4
|
+
/** Generated display title from the LLM. Absent until title has been produced. */
|
|
5
|
+
displayTitle?: string;
|
|
6
|
+
/** The message id used as input when generating the title. */
|
|
7
|
+
titleSourceMessageId?: string;
|
|
8
|
+
/** Slack channel name or equivalent location label. */
|
|
9
|
+
channelName?: string;
|
|
10
|
+
/** Surface on which the conversation was started. */
|
|
11
|
+
originSurface?: AgentTurnSurface;
|
|
12
|
+
/** Requester who initiated the conversation (first turn). */
|
|
13
|
+
originRequester?: AgentTurnRequester;
|
|
14
|
+
/** Timestamp of the first turn in the conversation. */
|
|
15
|
+
startedAtMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Record the origin context for a conversation the first time it is seen and
|
|
19
|
+
* refresh the context TTL on later turns without changing the stored origin.
|
|
20
|
+
*/
|
|
21
|
+
export declare function initConversationContext(conversationId: string, context: {
|
|
22
|
+
channelName?: string;
|
|
23
|
+
originSurface?: AgentTurnSurface;
|
|
24
|
+
originRequester?: AgentTurnRequester;
|
|
25
|
+
startedAtMs: number;
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Persist or refresh the LLM-generated title for a conversation.
|
|
29
|
+
*
|
|
30
|
+
* Plain set — no read, no lock.
|
|
31
|
+
*/
|
|
32
|
+
export declare function setConversationTitle(conversationId: string, title: {
|
|
33
|
+
displayTitle: string;
|
|
34
|
+
titleSourceMessageId?: string;
|
|
35
|
+
}): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Read conversation details for a single conversation.
|
|
38
|
+
* Assembles the context and title records in parallel.
|
|
39
|
+
* Returns undefined only when neither context nor title details exist yet.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getConversationDetails(conversationId: string): Promise<ConversationDetailsRecord | undefined>;
|
|
42
|
+
/**
|
|
43
|
+
* Bulk-fetch conversation details for a set of conversation ids in parallel.
|
|
44
|
+
* Returns a map from conversationId → record (omits ids with no details).
|
|
45
|
+
*/
|
|
46
|
+
export declare function getConversationDetailsForIds(conversationIds: Iterable<string>): Promise<Map<string, ConversationDetailsRecord>>;
|
|
@@ -12,7 +12,6 @@ export interface AgentTurnRequester {
|
|
|
12
12
|
}
|
|
13
13
|
export interface AgentTurnSessionRecord {
|
|
14
14
|
channelName?: string;
|
|
15
|
-
conversationTitle?: string;
|
|
16
15
|
version: number;
|
|
17
16
|
conversationId: string;
|
|
18
17
|
cumulativeDurationMs: number;
|
|
@@ -39,7 +38,6 @@ export declare function getAgentTurnSessionRecord(conversationId: string, sessio
|
|
|
39
38
|
/** Commit stable Pi session state and advance the turn session record. */
|
|
40
39
|
export declare function upsertAgentTurnSessionRecord(args: {
|
|
41
40
|
channelName?: string;
|
|
42
|
-
conversationTitle?: string;
|
|
43
41
|
conversationId: string;
|
|
44
42
|
cumulativeDurationMs?: number;
|
|
45
43
|
cumulativeUsage?: AgentTurnUsage;
|
|
@@ -61,7 +59,6 @@ export declare function upsertAgentTurnSessionRecord(args: {
|
|
|
61
59
|
/** Record turn-session metadata without storing conversation messages. */
|
|
62
60
|
export declare function recordAgentTurnSessionSummary(args: {
|
|
63
61
|
channelName?: string;
|
|
64
|
-
conversationTitle?: string;
|
|
65
62
|
conversationId: string;
|
|
66
63
|
cumulativeDurationMs?: number;
|
|
67
64
|
cumulativeUsage?: AgentTurnUsage;
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
logInfo,
|
|
30
30
|
logWarn,
|
|
31
31
|
soulPathCandidates,
|
|
32
|
+
toOptionalNumber,
|
|
32
33
|
worldPathCandidates
|
|
33
34
|
} from "./chunk-BBXYXOJW.js";
|
|
34
35
|
import {
|
|
@@ -2174,7 +2175,6 @@ function parseAgentTurnSessionFields(parsed) {
|
|
|
2174
2175
|
return void 0;
|
|
2175
2176
|
}
|
|
2176
2177
|
const channelName = typeof parsed.channelName === "string" && parsed.channelName.trim() ? parsed.channelName.trim() : void 0;
|
|
2177
|
-
const conversationTitle = typeof parsed.conversationTitle === "string" && parsed.conversationTitle.trim() ? parsed.conversationTitle.trim() : void 0;
|
|
2178
2178
|
const conversationId = parsed.conversationId;
|
|
2179
2179
|
const sessionId = parsed.sessionId;
|
|
2180
2180
|
const sliceId = toFiniteNonNegativeNumber(parsed.sliceId);
|
|
@@ -2194,7 +2194,6 @@ function parseAgentTurnSessionFields(parsed) {
|
|
|
2194
2194
|
return {
|
|
2195
2195
|
version,
|
|
2196
2196
|
...channelName ? { channelName } : {},
|
|
2197
|
-
...conversationTitle ? { conversationTitle } : {},
|
|
2198
2197
|
conversationId,
|
|
2199
2198
|
sessionId,
|
|
2200
2199
|
sliceId,
|
|
@@ -2285,7 +2284,6 @@ function materializeAgentTurnSessionRecord(stored, piMessages) {
|
|
|
2285
2284
|
return {
|
|
2286
2285
|
version: stored.version,
|
|
2287
2286
|
...stored.channelName ? { channelName: stored.channelName } : {},
|
|
2288
|
-
...stored.conversationTitle ? { conversationTitle: stored.conversationTitle } : {},
|
|
2289
2287
|
conversationId: stored.conversationId,
|
|
2290
2288
|
sessionId: stored.sessionId,
|
|
2291
2289
|
sliceId: stored.sliceId,
|
|
@@ -2347,7 +2345,6 @@ function buildStoredRecord(args) {
|
|
|
2347
2345
|
return {
|
|
2348
2346
|
version: (args.previousVersion ?? 0) + 1,
|
|
2349
2347
|
...args.channelName ? { channelName: args.channelName } : {},
|
|
2350
|
-
...args.conversationTitle ? { conversationTitle: args.conversationTitle } : {},
|
|
2351
2348
|
conversationId: args.conversationId,
|
|
2352
2349
|
sessionId: args.sessionId,
|
|
2353
2350
|
sliceId: args.sliceId,
|
|
@@ -2408,7 +2405,6 @@ async function updateAgentTurnSessionState(args) {
|
|
|
2408
2405
|
state: args.state,
|
|
2409
2406
|
committedMessageCount: parsed.committedMessageCount,
|
|
2410
2407
|
...parsed.channelName ? { channelName: parsed.channelName } : {},
|
|
2411
|
-
...parsed.conversationTitle ? { conversationTitle: parsed.conversationTitle } : {},
|
|
2412
2408
|
startedAtMs: parsed.startedAtMs,
|
|
2413
2409
|
lastProgressAtMs: parsed.lastProgressAtMs,
|
|
2414
2410
|
previousVersion: parsed.version,
|
|
@@ -2442,9 +2438,6 @@ async function upsertAgentTurnSessionRecord(args) {
|
|
|
2442
2438
|
ttlMs,
|
|
2443
2439
|
record: buildStoredRecord({
|
|
2444
2440
|
...args.channelName ?? existingRecord?.channelName ? { channelName: args.channelName ?? existingRecord?.channelName } : {},
|
|
2445
|
-
...args.conversationTitle ?? existingRecord?.conversationTitle ? {
|
|
2446
|
-
conversationTitle: args.conversationTitle ?? existingRecord?.conversationTitle
|
|
2447
|
-
} : {},
|
|
2448
2441
|
conversationId: args.conversationId,
|
|
2449
2442
|
sessionId: args.sessionId,
|
|
2450
2443
|
sliceId: args.sliceId,
|
|
@@ -2478,9 +2471,6 @@ async function recordAgentTurnSessionSummary(args) {
|
|
|
2478
2471
|
{
|
|
2479
2472
|
version: existing?.version ?? 0,
|
|
2480
2473
|
...args.channelName ?? existing?.channelName ? { channelName: args.channelName ?? existing?.channelName } : {},
|
|
2481
|
-
...args.conversationTitle ?? existing?.conversationTitle ? {
|
|
2482
|
-
conversationTitle: args.conversationTitle ?? existing?.conversationTitle
|
|
2483
|
-
} : {},
|
|
2484
2474
|
conversationId: args.conversationId,
|
|
2485
2475
|
sessionId: args.sessionId,
|
|
2486
2476
|
sliceId: args.sliceId,
|
|
@@ -2687,6 +2677,123 @@ function formatSlackConversationRedactedLabel(context) {
|
|
|
2687
2677
|
return formatSlackConversationTypeLabel(context.type);
|
|
2688
2678
|
}
|
|
2689
2679
|
|
|
2680
|
+
// src/chat/state/conversation-details.ts
|
|
2681
|
+
import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS2 } from "chat";
|
|
2682
|
+
var CONVERSATION_PREFIX = "junior:conversation";
|
|
2683
|
+
var CONVERSATION_DETAILS_TTL_MS = THREAD_STATE_TTL_MS2;
|
|
2684
|
+
function conversationContextKey(conversationId) {
|
|
2685
|
+
return `${CONVERSATION_PREFIX}:${conversationId}:context`;
|
|
2686
|
+
}
|
|
2687
|
+
function conversationTitleKey(conversationId) {
|
|
2688
|
+
return `${CONVERSATION_PREFIX}:${conversationId}:title`;
|
|
2689
|
+
}
|
|
2690
|
+
function parseAgentTurnRequester2(value) {
|
|
2691
|
+
if (!isRecord(value)) return void 0;
|
|
2692
|
+
const requester = {
|
|
2693
|
+
...typeof value.email === "string" ? { email: value.email } : {},
|
|
2694
|
+
...typeof value.fullName === "string" ? { fullName: value.fullName } : {},
|
|
2695
|
+
...typeof value.slackUserId === "string" ? { slackUserId: value.slackUserId } : {},
|
|
2696
|
+
...typeof value.slackUserName === "string" ? { slackUserName: value.slackUserName } : {}
|
|
2697
|
+
};
|
|
2698
|
+
return Object.keys(requester).length > 0 ? requester : void 0;
|
|
2699
|
+
}
|
|
2700
|
+
function parseOriginSurface(value) {
|
|
2701
|
+
if (value === "slack" || value === "api" || value === "scheduler" || value === "internal") {
|
|
2702
|
+
return value;
|
|
2703
|
+
}
|
|
2704
|
+
return void 0;
|
|
2705
|
+
}
|
|
2706
|
+
function storedContextFromInput(context) {
|
|
2707
|
+
return {
|
|
2708
|
+
...context.channelName ? { channelName: context.channelName } : {},
|
|
2709
|
+
...context.originSurface ? { originSurface: context.originSurface } : {},
|
|
2710
|
+
...context.originRequester ? { originRequester: context.originRequester } : {},
|
|
2711
|
+
startedAtMs: context.startedAtMs
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
function parseContext(value) {
|
|
2715
|
+
if (!isRecord(value)) return void 0;
|
|
2716
|
+
const startedAtMs = toOptionalNumber(value.startedAtMs);
|
|
2717
|
+
if (startedAtMs === void 0) return void 0;
|
|
2718
|
+
return {
|
|
2719
|
+
...typeof value.channelName === "string" && value.channelName.trim() ? { channelName: value.channelName.trim() } : {},
|
|
2720
|
+
...parseOriginSurface(value.originSurface) ? { originSurface: parseOriginSurface(value.originSurface) } : {},
|
|
2721
|
+
...parseAgentTurnRequester2(value.originRequester) ? { originRequester: parseAgentTurnRequester2(value.originRequester) } : {},
|
|
2722
|
+
startedAtMs
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
function parseTitle(value) {
|
|
2726
|
+
if (!isRecord(value)) return void 0;
|
|
2727
|
+
const displayTitle = typeof value.displayTitle === "string" && value.displayTitle.trim() ? value.displayTitle.trim() : void 0;
|
|
2728
|
+
if (!displayTitle) return void 0;
|
|
2729
|
+
return {
|
|
2730
|
+
displayTitle,
|
|
2731
|
+
...typeof value.titleSourceMessageId === "string" ? { titleSourceMessageId: value.titleSourceMessageId } : {}
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
async function initConversationContext(conversationId, context) {
|
|
2735
|
+
const stateAdapter = getStateAdapter();
|
|
2736
|
+
await stateAdapter.connect();
|
|
2737
|
+
const key2 = conversationContextKey(conversationId);
|
|
2738
|
+
const inserted = await stateAdapter.setIfNotExists(
|
|
2739
|
+
key2,
|
|
2740
|
+
storedContextFromInput(context),
|
|
2741
|
+
CONVERSATION_DETAILS_TTL_MS
|
|
2742
|
+
);
|
|
2743
|
+
if (inserted) return;
|
|
2744
|
+
const existing = parseContext(await stateAdapter.get(key2));
|
|
2745
|
+
if (!existing) {
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
await stateAdapter.set(key2, existing, CONVERSATION_DETAILS_TTL_MS);
|
|
2749
|
+
}
|
|
2750
|
+
async function setConversationTitle(conversationId, title) {
|
|
2751
|
+
const stateAdapter = getStateAdapter();
|
|
2752
|
+
await stateAdapter.connect();
|
|
2753
|
+
await stateAdapter.set(
|
|
2754
|
+
conversationTitleKey(conversationId),
|
|
2755
|
+
{
|
|
2756
|
+
displayTitle: title.displayTitle,
|
|
2757
|
+
...title.titleSourceMessageId ? { titleSourceMessageId: title.titleSourceMessageId } : {}
|
|
2758
|
+
},
|
|
2759
|
+
CONVERSATION_DETAILS_TTL_MS
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2762
|
+
async function getConversationDetails(conversationId) {
|
|
2763
|
+
const stateAdapter = getStateAdapter();
|
|
2764
|
+
await stateAdapter.connect();
|
|
2765
|
+
const [rawContext, rawTitle] = await Promise.all([
|
|
2766
|
+
stateAdapter.get(conversationContextKey(conversationId)),
|
|
2767
|
+
stateAdapter.get(conversationTitleKey(conversationId))
|
|
2768
|
+
]);
|
|
2769
|
+
const context = parseContext(rawContext);
|
|
2770
|
+
const title = parseTitle(rawTitle);
|
|
2771
|
+
if (!context && !title) return void 0;
|
|
2772
|
+
return {
|
|
2773
|
+
conversationId,
|
|
2774
|
+
...title?.displayTitle ? { displayTitle: title.displayTitle } : {},
|
|
2775
|
+
...title?.titleSourceMessageId ? { titleSourceMessageId: title.titleSourceMessageId } : {},
|
|
2776
|
+
...context?.channelName ? { channelName: context.channelName } : {},
|
|
2777
|
+
...context?.originSurface ? { originSurface: context.originSurface } : {},
|
|
2778
|
+
...context?.originRequester ? { originRequester: context.originRequester } : {},
|
|
2779
|
+
...context?.startedAtMs !== void 0 ? { startedAtMs: context.startedAtMs } : {}
|
|
2780
|
+
};
|
|
2781
|
+
}
|
|
2782
|
+
async function getConversationDetailsForIds(conversationIds) {
|
|
2783
|
+
const uniqueIds = [...new Set(conversationIds)].filter(Boolean);
|
|
2784
|
+
const entries = await Promise.all(
|
|
2785
|
+
uniqueIds.map(async (id) => {
|
|
2786
|
+
const details = await getConversationDetails(id);
|
|
2787
|
+
return details ? [id, details] : void 0;
|
|
2788
|
+
})
|
|
2789
|
+
);
|
|
2790
|
+
const result = /* @__PURE__ */ new Map();
|
|
2791
|
+
for (const entry of entries) {
|
|
2792
|
+
if (entry) result.set(entry[0], entry[1]);
|
|
2793
|
+
}
|
|
2794
|
+
return result;
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2690
2797
|
export {
|
|
2691
2798
|
createAgentPluginLogger,
|
|
2692
2799
|
createPluginState,
|
|
@@ -2729,5 +2836,9 @@ export {
|
|
|
2729
2836
|
resolveSlackChannelTypeFromMessage,
|
|
2730
2837
|
resolveSlackConversationContext,
|
|
2731
2838
|
resolveSlackConversationContextFromThreadId,
|
|
2732
|
-
formatSlackConversationRedactedLabel
|
|
2839
|
+
formatSlackConversationRedactedLabel,
|
|
2840
|
+
initConversationContext,
|
|
2841
|
+
setConversationTitle,
|
|
2842
|
+
getConversationDetails,
|
|
2843
|
+
getConversationDetailsForIds
|
|
2733
2844
|
};
|
package/dist/cli/init.js
CHANGED
|
@@ -31,6 +31,17 @@ export const POST = (request: Request) => app.fetch(request);
|
|
|
31
31
|
`
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
|
+
function writePluginsFile(targetDir) {
|
|
35
|
+
fs.writeFileSync(
|
|
36
|
+
path.join(targetDir, "plugins.ts"),
|
|
37
|
+
`import { defineJuniorPlugins } from "@sentry/junior";
|
|
38
|
+
|
|
39
|
+
export const plugins = defineJuniorPlugins([
|
|
40
|
+
"@sentry/junior-maintenance",
|
|
41
|
+
]);
|
|
42
|
+
`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
34
45
|
function writeNitroConfig(targetDir) {
|
|
35
46
|
fs.writeFileSync(
|
|
36
47
|
path.join(targetDir, "nitro.config.ts"),
|
|
@@ -39,7 +50,11 @@ import { juniorNitro } from "@sentry/junior/nitro";
|
|
|
39
50
|
|
|
40
51
|
export default defineConfig({
|
|
41
52
|
preset: "vercel",
|
|
42
|
-
modules: [
|
|
53
|
+
modules: [
|
|
54
|
+
juniorNitro({
|
|
55
|
+
plugins: "./plugins",
|
|
56
|
+
}),
|
|
57
|
+
],
|
|
43
58
|
routes: {
|
|
44
59
|
"/**": { handler: "./server.ts" },
|
|
45
60
|
},
|
|
@@ -129,6 +144,7 @@ async function runInit(dir, log = console.log) {
|
|
|
129
144
|
},
|
|
130
145
|
dependencies: {
|
|
131
146
|
"@sentry/junior": "latest",
|
|
147
|
+
"@sentry/junior-maintenance": "latest",
|
|
132
148
|
hono: "^4.12.0"
|
|
133
149
|
},
|
|
134
150
|
devDependencies: {
|
|
@@ -199,6 +215,7 @@ SENTRY_ORG_SLUG=
|
|
|
199
215
|
);
|
|
200
216
|
writeServerEntry(target);
|
|
201
217
|
writeQueueConsumerEntry(target);
|
|
218
|
+
writePluginsFile(target);
|
|
202
219
|
writeNitroConfig(target);
|
|
203
220
|
writeViteConfig(target);
|
|
204
221
|
writeVercelJson(target);
|
package/dist/reporting.d.ts
CHANGED
|
@@ -37,7 +37,10 @@ export interface DashboardRequesterIdentity {
|
|
|
37
37
|
slackUserName?: string;
|
|
38
38
|
}
|
|
39
39
|
export interface DashboardSessionReport {
|
|
40
|
-
|
|
40
|
+
/** Always-populated display title. LLM-generated title when available, otherwise the
|
|
41
|
+
* Slack channel/conversation location label or a generic fallback. Privacy redaction
|
|
42
|
+
* wins over everything else for non-public conversations. */
|
|
43
|
+
displayTitle: string;
|
|
41
44
|
cumulativeDurationMs: number;
|
|
42
45
|
cumulativeUsage?: DashboardTurnUsage;
|
|
43
46
|
conversationId: string;
|
|
@@ -48,7 +51,6 @@ export interface DashboardSessionReport {
|
|
|
48
51
|
lastProgressAt: string;
|
|
49
52
|
completedAt?: string;
|
|
50
53
|
surface: DashboardSurface;
|
|
51
|
-
title: string;
|
|
52
54
|
requesterIdentity?: DashboardRequesterIdentity;
|
|
53
55
|
channel?: string;
|
|
54
56
|
channelName?: string;
|
|
@@ -93,6 +95,8 @@ export interface DashboardTurnReport extends DashboardSessionReport {
|
|
|
93
95
|
}
|
|
94
96
|
export interface DashboardConversationReport {
|
|
95
97
|
conversationId: string;
|
|
98
|
+
/** Always-populated display title, computed the same way as DashboardSessionReport.displayTitle. */
|
|
99
|
+
displayTitle: string;
|
|
96
100
|
generatedAt: string;
|
|
97
101
|
turns: DashboardTurnReport[];
|
|
98
102
|
}
|
package/dist/reporting.js
CHANGED
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
formatSlackConversationRedactedLabel,
|
|
7
7
|
getAgentPluginOperationalReports,
|
|
8
8
|
getAgentTurnSessionRecord,
|
|
9
|
+
getConversationDetails,
|
|
10
|
+
getConversationDetailsForIds,
|
|
9
11
|
listAgentTurnSessionSummaries,
|
|
10
12
|
listAgentTurnSessionSummariesForConversation,
|
|
11
13
|
resolveSlackConversationContextFromThreadId
|
|
12
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-HOGQL2H6.js";
|
|
13
15
|
import {
|
|
14
16
|
discoverSkills
|
|
15
17
|
} from "./chunk-GT67ZWZQ.js";
|
|
@@ -89,12 +91,6 @@ function surfaceFromConversationId(conversationId) {
|
|
|
89
91
|
function surfaceFromSummary(summary) {
|
|
90
92
|
return summary.surface ?? surfaceFromConversationId(summary.conversationId);
|
|
91
93
|
}
|
|
92
|
-
function titleFromSummary(summary) {
|
|
93
|
-
if (summary.state === "awaiting_resume" && summary.resumeReason) {
|
|
94
|
-
return `Awaiting ${summary.resumeReason} resume`;
|
|
95
|
-
}
|
|
96
|
-
return `Turn ${summary.sessionId}`;
|
|
97
|
-
}
|
|
98
94
|
function requesterIdentityReport(requester) {
|
|
99
95
|
if (!requester) return void 0;
|
|
100
96
|
const identity = {
|
|
@@ -116,27 +112,33 @@ function turnUsageReport(usage) {
|
|
|
116
112
|
};
|
|
117
113
|
return Object.keys(report).length > 0 ? report : void 0;
|
|
118
114
|
}
|
|
119
|
-
function sessionReportFromSummary(summary, nowMs = Date.now()) {
|
|
115
|
+
function sessionReportFromSummary(summary, nowMs = Date.now(), details) {
|
|
120
116
|
const slackThread = parseSlackThreadId(summary.conversationId);
|
|
121
117
|
const privacy = resolveConversationPrivacy({
|
|
122
118
|
conversationId: summary.conversationId
|
|
123
119
|
});
|
|
120
|
+
const effectiveChannelName = details?.channelName ?? summary.channelName;
|
|
124
121
|
const slackConversation = resolveSlackConversationContextFromThreadId({
|
|
125
122
|
threadId: summary.conversationId,
|
|
126
|
-
channelName:
|
|
123
|
+
channelName: effectiveChannelName
|
|
127
124
|
});
|
|
128
125
|
const privateLabel = privacy !== "public" ? slackConversation ? formatSlackConversationRedactedLabel(slackConversation) : PRIVATE_CONVERSATION_LABEL : void 0;
|
|
129
|
-
const
|
|
130
|
-
const
|
|
126
|
+
const channelName = privateLabel ?? effectiveChannelName;
|
|
127
|
+
const effectiveSurface = details?.originSurface ?? surfaceFromSummary(summary);
|
|
128
|
+
const displayTitle = privateLabel ?? details?.displayTitle ?? slackStatsLocationLabel({
|
|
129
|
+
channel: slackThread?.channelId,
|
|
130
|
+
channelName: effectiveChannelName
|
|
131
|
+
}) ?? surfaceFallbackLabel(effectiveSurface);
|
|
132
|
+
const effectiveRequester = details?.originRequester ?? summary.requester;
|
|
131
133
|
const sentryConversationUrl = buildSentryConversationUrl(
|
|
132
134
|
summary.conversationId
|
|
133
135
|
);
|
|
134
136
|
const sentryTraceUrl = summary.traceId ? buildSentryTraceUrl(summary.traceId) : void 0;
|
|
135
|
-
const requesterIdentity = requesterIdentityReport(
|
|
137
|
+
const requesterIdentity = requesterIdentityReport(effectiveRequester);
|
|
136
138
|
const cumulativeUsage = turnUsageReport(summary.cumulativeUsage);
|
|
137
139
|
return {
|
|
138
140
|
conversationId: summary.conversationId,
|
|
139
|
-
|
|
141
|
+
displayTitle,
|
|
140
142
|
id: summary.sessionId,
|
|
141
143
|
status: statusFromCheckpoint(summary, nowMs),
|
|
142
144
|
startedAt: new Date(summary.startedAtMs).toISOString(),
|
|
@@ -145,8 +147,7 @@ function sessionReportFromSummary(summary, nowMs = Date.now()) {
|
|
|
145
147
|
...summary.state === "completed" ? { completedAt: new Date(summary.updatedAtMs).toISOString() } : {},
|
|
146
148
|
cumulativeDurationMs: summary.cumulativeDurationMs,
|
|
147
149
|
...cumulativeUsage ? { cumulativeUsage } : {},
|
|
148
|
-
surface:
|
|
149
|
-
title: titleFromSummary(summary),
|
|
150
|
+
surface: effectiveSurface,
|
|
150
151
|
...requesterIdentity ? { requesterIdentity } : {},
|
|
151
152
|
...slackThread ? { channel: slackThread.channelId } : {},
|
|
152
153
|
...channelName ? { channelName } : {},
|
|
@@ -237,8 +238,27 @@ function slackStatsLocationLabel(input) {
|
|
|
237
238
|
}
|
|
238
239
|
return name || channelId;
|
|
239
240
|
}
|
|
241
|
+
function surfaceFallbackLabel(surface) {
|
|
242
|
+
if (surface === "scheduler") return "Scheduler";
|
|
243
|
+
if (surface === "api") return "API";
|
|
244
|
+
if (surface === "internal") return "Internal";
|
|
245
|
+
return "Conversation";
|
|
246
|
+
}
|
|
247
|
+
function displayTitleFromDetails(conversationId, details) {
|
|
248
|
+
if (!details) return void 0;
|
|
249
|
+
const slackThread = parseSlackThreadId(conversationId);
|
|
250
|
+
const slackConversation = resolveSlackConversationContextFromThreadId({
|
|
251
|
+
threadId: conversationId,
|
|
252
|
+
channelName: details.channelName
|
|
253
|
+
});
|
|
254
|
+
const privateLabel = resolveConversationPrivacy({ conversationId }) !== "public" ? formatSlackConversationRedactedLabel(slackConversation) ?? PRIVATE_CONVERSATION_LABEL : void 0;
|
|
255
|
+
return privateLabel ?? details.displayTitle ?? slackStatsLocationLabel({
|
|
256
|
+
channel: slackThread?.channelId,
|
|
257
|
+
channelName: details.channelName
|
|
258
|
+
}) ?? (details.originSurface ? surfaceFallbackLabel(details.originSurface) : void 0);
|
|
259
|
+
}
|
|
240
260
|
function locationLabel(turn) {
|
|
241
|
-
return slackStatsLocationLabel(turn) ?? (turn.surface
|
|
261
|
+
return slackStatsLocationLabel(turn) ?? surfaceFallbackLabel(turn.surface);
|
|
242
262
|
}
|
|
243
263
|
function emptyStatsItem(label) {
|
|
244
264
|
return {
|
|
@@ -265,7 +285,7 @@ function statusSignals(turns) {
|
|
|
265
285
|
}
|
|
266
286
|
function statsItems(map) {
|
|
267
287
|
return [...map.values()].sort(
|
|
268
|
-
(left, right) => right.conversations - left.conversations || right.durationMs - left.durationMs || left.label.localeCompare(right.label)
|
|
288
|
+
(left, right) => right.conversations - left.conversations || right.turns - left.turns || right.durationMs - left.durationMs || left.label.localeCompare(right.label)
|
|
269
289
|
);
|
|
270
290
|
}
|
|
271
291
|
function newestTurn(turns) {
|
|
@@ -620,11 +640,18 @@ async function readSessions() {
|
|
|
620
640
|
const summaries = await listAgentTurnSessionSummaries(
|
|
621
641
|
DASHBOARD_SESSION_FEED_LIMIT
|
|
622
642
|
);
|
|
643
|
+
const detailsByConversationId = await getConversationDetailsForIds(
|
|
644
|
+
summaries.map((s) => s.conversationId)
|
|
645
|
+
);
|
|
623
646
|
return {
|
|
624
647
|
source: "turn_session_records",
|
|
625
648
|
generatedAt: new Date(nowMs).toISOString(),
|
|
626
649
|
sessions: summaries.map(
|
|
627
|
-
(summary) => sessionReportFromSummary(
|
|
650
|
+
(summary) => sessionReportFromSummary(
|
|
651
|
+
summary,
|
|
652
|
+
nowMs,
|
|
653
|
+
detailsByConversationId.get(summary.conversationId)
|
|
654
|
+
)
|
|
628
655
|
)
|
|
629
656
|
};
|
|
630
657
|
}
|
|
@@ -643,13 +670,20 @@ async function readConversationStats() {
|
|
|
643
670
|
summaries: sampledSummaries,
|
|
644
671
|
truncated
|
|
645
672
|
});
|
|
673
|
+
const detailsByConversationId = await getConversationDetailsForIds(
|
|
674
|
+
reportSummaries.map((summary) => summary.conversationId)
|
|
675
|
+
);
|
|
646
676
|
return buildConversationStatsReport({
|
|
647
677
|
generatedAt,
|
|
648
678
|
nowMs,
|
|
649
679
|
sampleLimit: DASHBOARD_CONVERSATION_STATS_LIMIT,
|
|
650
680
|
sampleSize: sampledSummaries.length,
|
|
651
681
|
sessions: reportSummaries.map(
|
|
652
|
-
(summary) => sessionReportFromSummary(
|
|
682
|
+
(summary) => sessionReportFromSummary(
|
|
683
|
+
summary,
|
|
684
|
+
nowMs,
|
|
685
|
+
detailsByConversationId.get(summary.conversationId)
|
|
686
|
+
)
|
|
653
687
|
),
|
|
654
688
|
truncated
|
|
655
689
|
});
|
|
@@ -663,7 +697,11 @@ async function readPluginOperationalReports() {
|
|
|
663
697
|
};
|
|
664
698
|
}
|
|
665
699
|
async function readConversation(conversationId) {
|
|
666
|
-
const
|
|
700
|
+
const [rawSummaries, details] = await Promise.all([
|
|
701
|
+
listAgentTurnSessionSummariesForConversation(conversationId),
|
|
702
|
+
getConversationDetails(conversationId)
|
|
703
|
+
]);
|
|
704
|
+
const summaries = rawSummaries.sort(
|
|
667
705
|
(left, right) => left.startedAtMs - right.startedAtMs || left.updatedAtMs - right.updatedAtMs || left.sessionId.localeCompare(right.sessionId)
|
|
668
706
|
);
|
|
669
707
|
const turns = await Promise.all(
|
|
@@ -686,7 +724,7 @@ async function readConversation(conversationId) {
|
|
|
686
724
|
const traceId = summary.traceId ?? sessionRecord?.traceId ?? (canExposeTranscript ? traceIdFromTranscript(transcript) : void 0);
|
|
687
725
|
const sentryTraceUrl = traceId ? buildSentryTraceUrl(traceId) : void 0;
|
|
688
726
|
return {
|
|
689
|
-
...sessionReportFromSummary(summary),
|
|
727
|
+
...sessionReportFromSummary(summary, Date.now(), details),
|
|
690
728
|
...traceId ? { traceId } : {},
|
|
691
729
|
...sentryTraceUrl ? { sentryTraceUrl } : {},
|
|
692
730
|
transcriptAvailable: Boolean(sessionRecord) && canExposeTranscript,
|
|
@@ -700,8 +738,11 @@ async function readConversation(conversationId) {
|
|
|
700
738
|
};
|
|
701
739
|
})
|
|
702
740
|
);
|
|
741
|
+
const firstTurn = turns[0];
|
|
742
|
+
const displayTitle = firstTurn?.displayTitle ?? displayTitleFromDetails(conversationId, details) ?? surfaceFallbackLabel(firstTurn?.surface ?? "slack");
|
|
703
743
|
return {
|
|
704
744
|
conversationId,
|
|
745
|
+
displayTitle,
|
|
705
746
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
706
747
|
turns
|
|
707
748
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.70.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"node-html-markdown": "^2.0.0",
|
|
66
66
|
"yaml": "^2.9.0",
|
|
67
67
|
"zod": "^4.4.3",
|
|
68
|
-
"@sentry/junior-plugin-api": "0.
|
|
68
|
+
"@sentry/junior-plugin-api": "0.70.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/node": "^25.9.1",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"typescript": "^6.0.3",
|
|
79
79
|
"vercel": "^54.4.0",
|
|
80
80
|
"vitest": "^4.1.7",
|
|
81
|
-
"@sentry/junior-scheduler": "0.
|
|
81
|
+
"@sentry/junior-scheduler": "0.70.0"
|
|
82
82
|
},
|
|
83
83
|
"scripts": {
|
|
84
84
|
"build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
|