@sentry/junior 0.66.3 → 0.67.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 +97 -630
- package/dist/chat/config.d.ts +1 -0
- package/dist/chat/ingress/slash-command.d.ts +1 -1
- package/dist/chat/plugins/agent-hooks.d.ts +3 -1
- package/dist/chat/respond.d.ts +2 -0
- package/dist/chat/services/turn-session-record.d.ts +6 -1
- package/dist/chat/state/turn-session.d.ts +4 -0
- package/dist/{chunk-DG2I6GXC.js → chunk-HFMZE67J.js} +1801 -1014
- package/dist/{chunk-JA5QR3N4.js → chunk-KWEE2436.js} +1 -1
- package/dist/{chunk-SAYCFF7O.js → chunk-NWU2Z6SM.js} +12 -1
- package/dist/cli/init.js +1 -0
- package/dist/cli/snapshot-warmup.js +2 -2
- package/dist/reporting.d.ts +45 -2
- package/dist/reporting.js +301 -11
- package/package.json +3 -3
|
@@ -8,9 +8,10 @@ import {
|
|
|
8
8
|
getStateAdapter,
|
|
9
9
|
parseSlackThreadId,
|
|
10
10
|
sandboxSkillDir
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-NWU2Z6SM.js";
|
|
12
12
|
import {
|
|
13
13
|
isRecord,
|
|
14
|
+
logException,
|
|
14
15
|
logInfo,
|
|
15
16
|
logWarn
|
|
16
17
|
} from "./chunk-6QWWMZCK.js";
|
|
@@ -23,755 +24,1788 @@ import {
|
|
|
23
24
|
worldPathCandidates
|
|
24
25
|
} from "./chunk-KVZL5NZS.js";
|
|
25
26
|
|
|
26
|
-
// src/
|
|
27
|
-
function
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
// src/chat/plugins/logging.ts
|
|
28
|
+
function createAgentPluginLogger(plugin) {
|
|
29
|
+
return {
|
|
30
|
+
info(message, metadata) {
|
|
31
|
+
logInfo(
|
|
32
|
+
"agent_plugin_log_info",
|
|
33
|
+
{},
|
|
34
|
+
{ "app.plugin.name": plugin, ...metadata },
|
|
35
|
+
message
|
|
36
|
+
);
|
|
37
|
+
},
|
|
38
|
+
warn(message, metadata) {
|
|
39
|
+
logWarn(
|
|
40
|
+
"agent_plugin_log_warn",
|
|
41
|
+
{},
|
|
42
|
+
{ "app.plugin.name": plugin, ...metadata },
|
|
43
|
+
message
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
error(message, metadata) {
|
|
47
|
+
logException(
|
|
48
|
+
new Error(message),
|
|
49
|
+
"agent_plugin_log_error",
|
|
50
|
+
{},
|
|
51
|
+
{ "app.plugin.name": plugin, ...metadata },
|
|
52
|
+
message
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
33
56
|
}
|
|
34
57
|
|
|
35
|
-
// src/chat/
|
|
36
|
-
import
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
var TURN_CONTEXT_TAG = "runtime-turn-context";
|
|
41
|
-
|
|
42
|
-
// src/chat/interruption-marker.ts
|
|
43
|
-
var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
|
|
44
|
-
function getInterruptionMarker() {
|
|
45
|
-
return INTERRUPTED_MARKER;
|
|
58
|
+
// src/chat/plugins/state.ts
|
|
59
|
+
import { createHash } from "crypto";
|
|
60
|
+
var MAX_PLUGIN_STATE_KEY_LENGTH = 512;
|
|
61
|
+
function hashKeyPart(value) {
|
|
62
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 32);
|
|
46
63
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return "";
|
|
64
|
+
function pluginStateKey(plugin, key2) {
|
|
65
|
+
return `junior:plugin_state:${hashKeyPart(plugin)}:${hashKeyPart(key2)}`;
|
|
66
|
+
}
|
|
67
|
+
function validatePluginStateKey(key2) {
|
|
68
|
+
if (!key2.trim()) {
|
|
69
|
+
throw new Error("Plugin state key is required");
|
|
54
70
|
}
|
|
55
|
-
if (
|
|
56
|
-
|
|
71
|
+
if (key2.length > MAX_PLUGIN_STATE_KEY_LENGTH) {
|
|
72
|
+
throw new Error("Plugin state key exceeds the maximum length");
|
|
57
73
|
}
|
|
58
|
-
return `${trimmed.slice(0, SLACK_STATUS_MAX_LENGTH - 3).trimEnd()}...`;
|
|
59
74
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const listItemPattern = /^[-*•]\s|^\d+\.\s/;
|
|
65
|
-
const lines = text.split("\n");
|
|
66
|
-
const result = [];
|
|
67
|
-
let inCodeBlock = false;
|
|
68
|
-
for (let i = 0; i < lines.length; i++) {
|
|
69
|
-
const line = lines[i];
|
|
70
|
-
const isCodeFence = codeBlockPattern.test(line.trimStart());
|
|
71
|
-
if (isCodeFence) {
|
|
72
|
-
if (!inCodeBlock) {
|
|
73
|
-
const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
|
|
74
|
-
if (prev2 !== void 0 && prev2.trim() !== "") {
|
|
75
|
-
result.push("");
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
inCodeBlock = !inCodeBlock;
|
|
79
|
-
result.push(line);
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
if (inCodeBlock) {
|
|
83
|
-
result.push(line);
|
|
75
|
+
function legacyStateKey(key2, options) {
|
|
76
|
+
for (const prefix of options?.legacyStatePrefixes ?? []) {
|
|
77
|
+
const trimmed = prefix.trim();
|
|
78
|
+
if (!trimmed) {
|
|
84
79
|
continue;
|
|
85
80
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
result.push("");
|
|
81
|
+
if (key2 === trimmed || key2.startsWith(`${trimmed}:`)) {
|
|
82
|
+
return key2;
|
|
89
83
|
}
|
|
90
|
-
result.push(line);
|
|
91
84
|
}
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
function renderSlackMrkdwn(text) {
|
|
95
|
-
let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
|
|
96
|
-
normalized = ensureBlockSpacing(normalized);
|
|
97
|
-
return normalized.replace(/\n{3,}/g, "\n\n").trim();
|
|
85
|
+
return void 0;
|
|
98
86
|
}
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
87
|
+
function createPluginState(plugin, options) {
|
|
88
|
+
return {
|
|
89
|
+
async delete(key2) {
|
|
90
|
+
validatePluginStateKey(key2);
|
|
91
|
+
const state = getStateAdapter();
|
|
92
|
+
await state.connect();
|
|
93
|
+
await state.delete(pluginStateKey(plugin, key2));
|
|
94
|
+
const legacyKey = legacyStateKey(key2, options);
|
|
95
|
+
if (legacyKey) {
|
|
96
|
+
await state.delete(legacyKey);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
async get(key2) {
|
|
100
|
+
validatePluginStateKey(key2);
|
|
101
|
+
const state = getStateAdapter();
|
|
102
|
+
await state.connect();
|
|
103
|
+
const value = await state.get(pluginStateKey(plugin, key2));
|
|
104
|
+
if (value !== null && value !== void 0) {
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
const legacyKey = legacyStateKey(key2, options);
|
|
108
|
+
return legacyKey ? await state.get(legacyKey) ?? void 0 : void 0;
|
|
109
|
+
},
|
|
110
|
+
async set(key2, value, ttlMs) {
|
|
111
|
+
validatePluginStateKey(key2);
|
|
112
|
+
const state = getStateAdapter();
|
|
113
|
+
await state.connect();
|
|
114
|
+
await state.set(pluginStateKey(plugin, key2), value, ttlMs);
|
|
115
|
+
},
|
|
116
|
+
async setIfNotExists(key2, value, ttlMs) {
|
|
117
|
+
validatePluginStateKey(key2);
|
|
118
|
+
const state = getStateAdapter();
|
|
119
|
+
await state.connect();
|
|
120
|
+
const legacyKey = legacyStateKey(key2, options);
|
|
121
|
+
if (legacyKey) {
|
|
122
|
+
const existing = await state.get(legacyKey);
|
|
123
|
+
if (existing !== null && existing !== void 0) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return await state.setIfNotExists(
|
|
128
|
+
pluginStateKey(plugin, key2),
|
|
129
|
+
value,
|
|
130
|
+
ttlMs
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
async withLock(key2, ttlMs, callback) {
|
|
134
|
+
validatePluginStateKey(key2);
|
|
135
|
+
const state = getStateAdapter();
|
|
136
|
+
await state.connect();
|
|
137
|
+
const lockKey = legacyStateKey(key2, options) ?? pluginStateKey(plugin, key2);
|
|
138
|
+
const lock = await state.acquireLock(lockKey, ttlMs);
|
|
139
|
+
if (!lock) {
|
|
140
|
+
throw new Error(`Could not acquire plugin state lock for ${key2}`);
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
return await callback();
|
|
144
|
+
} finally {
|
|
145
|
+
await state.releaseLock(lock);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
105
149
|
}
|
|
106
150
|
|
|
107
|
-
// src/chat/
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
151
|
+
// src/chat/credentials/subject.ts
|
|
152
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
153
|
+
|
|
154
|
+
// src/chat/slack/client.ts
|
|
155
|
+
import { WebClient } from "@slack/web-api";
|
|
156
|
+
var SlackActionError = class extends Error {
|
|
157
|
+
code;
|
|
158
|
+
apiError;
|
|
159
|
+
needed;
|
|
160
|
+
provided;
|
|
161
|
+
statusCode;
|
|
162
|
+
requestId;
|
|
163
|
+
errorData;
|
|
164
|
+
retryAfterSeconds;
|
|
165
|
+
detail;
|
|
166
|
+
detailLine;
|
|
167
|
+
detailRule;
|
|
168
|
+
constructor(message, code, options = {}) {
|
|
169
|
+
super(message);
|
|
170
|
+
this.name = "SlackActionError";
|
|
171
|
+
this.code = code;
|
|
172
|
+
this.apiError = options.apiError;
|
|
173
|
+
this.needed = options.needed;
|
|
174
|
+
this.provided = options.provided;
|
|
175
|
+
this.statusCode = options.statusCode;
|
|
176
|
+
this.requestId = options.requestId;
|
|
177
|
+
this.errorData = options.errorData;
|
|
178
|
+
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
179
|
+
this.detail = options.detail;
|
|
180
|
+
this.detailLine = options.detailLine;
|
|
181
|
+
this.detailRule = options.detailRule;
|
|
114
182
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
function findSplitIndex(text, maxChars) {
|
|
121
|
-
if (text.length <= maxChars) {
|
|
122
|
-
return text.length;
|
|
183
|
+
};
|
|
184
|
+
function serializeSlackErrorData(data) {
|
|
185
|
+
if (!data || typeof data !== "object") {
|
|
186
|
+
return void 0;
|
|
123
187
|
}
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
188
|
+
const filtered = Object.fromEntries(
|
|
189
|
+
Object.entries(data).filter(
|
|
190
|
+
([key2]) => key2 !== "error"
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
if (Object.keys(filtered).length === 0) {
|
|
194
|
+
return void 0;
|
|
128
195
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return
|
|
196
|
+
try {
|
|
197
|
+
const serialized = JSON.stringify(filtered);
|
|
198
|
+
return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
|
|
199
|
+
} catch {
|
|
200
|
+
return void 0;
|
|
132
201
|
}
|
|
133
|
-
return maxChars;
|
|
134
202
|
}
|
|
135
|
-
function
|
|
136
|
-
if (
|
|
137
|
-
return
|
|
203
|
+
function getHeaderString(headers, name) {
|
|
204
|
+
if (!headers || typeof headers !== "object") {
|
|
205
|
+
return void 0;
|
|
138
206
|
}
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
207
|
+
const key2 = name.toLowerCase();
|
|
208
|
+
const entries = headers;
|
|
209
|
+
for (const [entryKey, value] of Object.entries(entries)) {
|
|
210
|
+
if (entryKey.toLowerCase() !== key2) continue;
|
|
211
|
+
if (typeof value === "string") return value;
|
|
212
|
+
if (Array.isArray(value)) {
|
|
213
|
+
const first = value.find((entry) => typeof entry === "string");
|
|
214
|
+
return typeof first === "string" ? first : void 0;
|
|
215
|
+
}
|
|
142
216
|
}
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
146
|
-
return {
|
|
147
|
-
maxChars: Math.max(1, maxChars - suffix.length),
|
|
148
|
-
maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
|
|
149
|
-
};
|
|
217
|
+
return void 0;
|
|
150
218
|
}
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
219
|
+
function parseSlackCanvasDetail(detail) {
|
|
220
|
+
if (typeof detail !== "string") {
|
|
221
|
+
return {};
|
|
222
|
+
}
|
|
223
|
+
const trimmed = detail.trim();
|
|
224
|
+
if (!trimmed) {
|
|
225
|
+
return {};
|
|
226
|
+
}
|
|
227
|
+
const parsed = {
|
|
228
|
+
detail: trimmed
|
|
156
229
|
};
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
|
|
163
|
-
if (!openerMatch) {
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
if (!open) {
|
|
167
|
-
open = {
|
|
168
|
-
fence: openerMatch[1],
|
|
169
|
-
openerLine: trimmed
|
|
170
|
-
};
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
|
|
174
|
-
open = void 0;
|
|
230
|
+
const lineMatch = trimmed.match(/line\s+(\d+):/i);
|
|
231
|
+
if (lineMatch) {
|
|
232
|
+
const line = Number.parseInt(lineMatch[1] ?? "", 10);
|
|
233
|
+
if (Number.isFinite(line)) {
|
|
234
|
+
parsed.detailLine = line;
|
|
175
235
|
}
|
|
176
236
|
}
|
|
177
|
-
if (
|
|
178
|
-
|
|
237
|
+
if (/unsupported heading depth/i.test(trimmed)) {
|
|
238
|
+
parsed.detailRule = "unsupported_heading_depth";
|
|
179
239
|
}
|
|
180
|
-
return
|
|
181
|
-
closeSuffix: text.endsWith("\n") ? open.fence : `
|
|
182
|
-
${open.fence}`,
|
|
183
|
-
reopenPrefix: `${open.openerLine}
|
|
184
|
-
`
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
function appendSlackSuffix(text, marker) {
|
|
188
|
-
const carryover = getFenceContinuation(text);
|
|
189
|
-
return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
|
|
240
|
+
return parsed;
|
|
190
241
|
}
|
|
191
|
-
|
|
192
|
-
|
|
242
|
+
var client = null;
|
|
243
|
+
function normalizeSlackConversationId(channelId) {
|
|
244
|
+
if (!channelId) return void 0;
|
|
245
|
+
const trimmed = channelId.trim();
|
|
246
|
+
if (!trimmed) return void 0;
|
|
247
|
+
if (!trimmed.startsWith("slack:")) {
|
|
248
|
+
return trimmed;
|
|
249
|
+
}
|
|
250
|
+
const parts = trimmed.split(":");
|
|
251
|
+
return parts[1]?.trim() || void 0;
|
|
193
252
|
}
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
253
|
+
function getClient() {
|
|
254
|
+
if (client) return client;
|
|
255
|
+
const token = getSlackBotToken();
|
|
256
|
+
if (!token) {
|
|
257
|
+
throw new SlackActionError(
|
|
258
|
+
"SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack canvas/list actions in this service",
|
|
259
|
+
"missing_token"
|
|
260
|
+
);
|
|
201
261
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
262
|
+
client = new WebClient(token);
|
|
263
|
+
return client;
|
|
264
|
+
}
|
|
265
|
+
function mapSlackError(error) {
|
|
266
|
+
if (error instanceof SlackActionError) {
|
|
267
|
+
return error;
|
|
205
268
|
}
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
)
|
|
269
|
+
const candidate = error;
|
|
270
|
+
const apiError = candidate.data?.error;
|
|
271
|
+
const message = candidate.message ?? "Slack action failed";
|
|
272
|
+
const baseOptions = {
|
|
273
|
+
apiError,
|
|
274
|
+
statusCode: candidate.statusCode,
|
|
275
|
+
requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
|
|
276
|
+
errorData: serializeSlackErrorData(candidate.data),
|
|
277
|
+
...parseSlackCanvasDetail(candidate.data?.detail)
|
|
278
|
+
};
|
|
279
|
+
if (apiError === "missing_scope") {
|
|
280
|
+
return new SlackActionError(message, "missing_scope", {
|
|
281
|
+
...baseOptions,
|
|
282
|
+
needed: candidate.data?.needed,
|
|
283
|
+
provided: candidate.data?.provided
|
|
284
|
+
});
|
|
215
285
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return { prefix, rest };
|
|
286
|
+
if (apiError === "not_in_channel") {
|
|
287
|
+
return new SlackActionError(message, "not_in_channel", baseOptions);
|
|
219
288
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
rest: `${carryover.reopenPrefix}${rest}`
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
function takeSlackContinuationPrefix(text, options) {
|
|
226
|
-
const budget = {
|
|
227
|
-
maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
|
|
228
|
-
maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
|
|
229
|
-
};
|
|
230
|
-
const { prefix, rest } = (() => {
|
|
231
|
-
if (options?.forceSplit) {
|
|
232
|
-
return takeSlackContinuationChunk(text, budget);
|
|
233
|
-
}
|
|
234
|
-
const initial = takeSlackInlinePrefix(text, budget);
|
|
235
|
-
return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
|
|
236
|
-
})();
|
|
237
|
-
return {
|
|
238
|
-
prefix,
|
|
239
|
-
renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
|
|
240
|
-
rest
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
function takeSlackInlinePrefix(text, options) {
|
|
244
|
-
const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
|
|
245
|
-
const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
|
|
246
|
-
const normalized = text.replace(/\r\n?/g, "\n");
|
|
247
|
-
if (!normalized) {
|
|
248
|
-
return { prefix: "", rest: "" };
|
|
289
|
+
if (apiError === "already_reacted") {
|
|
290
|
+
return new SlackActionError(message, "already_reacted", baseOptions);
|
|
249
291
|
}
|
|
250
|
-
if (
|
|
251
|
-
return
|
|
292
|
+
if (apiError === "no_reaction") {
|
|
293
|
+
return new SlackActionError(message, "no_reaction", baseOptions);
|
|
252
294
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const prefix = lineBounded.slice(0, cutIndex).trimEnd();
|
|
256
|
-
if (prefix) {
|
|
257
|
-
return {
|
|
258
|
-
prefix,
|
|
259
|
-
rest: normalized.slice(prefix.length).trimStart()
|
|
260
|
-
};
|
|
295
|
+
if (apiError === "invalid_arguments") {
|
|
296
|
+
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
261
297
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
|
|
265
|
-
rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
function splitSlackReplyText(text, options) {
|
|
269
|
-
const normalized = renderSlackMrkdwn(text);
|
|
270
|
-
if (!normalized) {
|
|
271
|
-
return [];
|
|
298
|
+
if (apiError === "invalid_cursor") {
|
|
299
|
+
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
272
300
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
let remaining = normalized;
|
|
276
|
-
while (remaining) {
|
|
277
|
-
const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
|
|
278
|
-
if (fitsFinalChunk) {
|
|
279
|
-
chunks.push(
|
|
280
|
-
options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
|
|
281
|
-
);
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
|
|
285
|
-
...continuationBudget,
|
|
286
|
-
forceSplit: true
|
|
287
|
-
});
|
|
288
|
-
chunks.push(renderedPrefix);
|
|
289
|
-
remaining = rest;
|
|
301
|
+
if (apiError === "invalid_name") {
|
|
302
|
+
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
290
303
|
}
|
|
291
|
-
if (
|
|
292
|
-
|
|
304
|
+
if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") {
|
|
305
|
+
return new SlackActionError(message, "not_found", baseOptions);
|
|
293
306
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
function getSlackContinuationBudget() {
|
|
297
|
-
return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
298
|
-
}
|
|
299
|
-
function buildSlackOutputMessage(text, files) {
|
|
300
|
-
const normalized = renderSlackMrkdwn(text);
|
|
301
|
-
const fileCount = files?.length ?? 0;
|
|
302
|
-
if (!normalized) {
|
|
303
|
-
if (fileCount > 0) {
|
|
304
|
-
return {
|
|
305
|
-
raw: "",
|
|
306
|
-
files
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
throw new Error(
|
|
310
|
-
`Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
|
|
311
|
-
);
|
|
307
|
+
if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
|
|
308
|
+
return new SlackActionError(message, "feature_unavailable", baseOptions);
|
|
312
309
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return
|
|
310
|
+
if (apiError === "canvas_creation_failed") {
|
|
311
|
+
return new SlackActionError(message, "canvas_creation_failed", baseOptions);
|
|
312
|
+
}
|
|
313
|
+
if (apiError === "canvas_editing_failed") {
|
|
314
|
+
return new SlackActionError(message, "canvas_editing_failed", baseOptions);
|
|
315
|
+
}
|
|
316
|
+
if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) {
|
|
317
|
+
return new SlackActionError(message, "rate_limited", {
|
|
318
|
+
...baseOptions,
|
|
319
|
+
retryAfterSeconds: candidate.retryAfter
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return new SlackActionError(message, "internal_error", baseOptions);
|
|
326
323
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
|
|
330
|
-
function getLoggedMarkdownFiles() {
|
|
331
|
-
const globalState = globalThis;
|
|
332
|
-
globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
|
|
333
|
-
return globalState.__juniorLoggedMarkdownFiles;
|
|
324
|
+
function sleep(ms) {
|
|
325
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
334
326
|
}
|
|
335
|
-
function
|
|
336
|
-
|
|
327
|
+
async function withSlackRetries(task, maxAttempts = 3, context = {}) {
|
|
328
|
+
let attempt = 0;
|
|
329
|
+
while (attempt < maxAttempts) {
|
|
330
|
+
attempt += 1;
|
|
337
331
|
try {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
332
|
+
return await task();
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const mapped = mapSlackError(error);
|
|
335
|
+
const isRetryable = mapped.code === "rate_limited";
|
|
336
|
+
const baseLogAttributes = {
|
|
337
|
+
"app.slack.action": context.action ?? "unknown",
|
|
338
|
+
"app.slack.error_code": mapped.code,
|
|
339
|
+
...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : {},
|
|
340
|
+
...mapped.detail ? { "app.slack.detail": mapped.detail } : {},
|
|
341
|
+
...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : {},
|
|
342
|
+
...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : {},
|
|
343
|
+
...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : {},
|
|
344
|
+
...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : {},
|
|
345
|
+
...context.attributes ?? {}
|
|
346
|
+
};
|
|
347
|
+
if (!isRetryable || attempt >= maxAttempts) {
|
|
348
|
+
logWarn(
|
|
349
|
+
"slack_action_failed",
|
|
350
|
+
{},
|
|
351
|
+
{
|
|
352
|
+
...baseLogAttributes,
|
|
353
|
+
...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : {}
|
|
354
|
+
},
|
|
355
|
+
"Slack action failed"
|
|
356
|
+
);
|
|
357
|
+
throw mapped;
|
|
354
358
|
}
|
|
355
|
-
|
|
356
|
-
|
|
359
|
+
logWarn(
|
|
360
|
+
"slack_action_retrying",
|
|
361
|
+
{},
|
|
362
|
+
{
|
|
363
|
+
...baseLogAttributes,
|
|
364
|
+
"app.slack.retry_attempt": attempt
|
|
365
|
+
},
|
|
366
|
+
"Retrying Slack action after transient failure"
|
|
367
|
+
);
|
|
368
|
+
const retryAfterMs = mapped.code === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
|
|
369
|
+
const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
|
|
370
|
+
await sleep(retryAfterMs ?? backoffMs);
|
|
357
371
|
}
|
|
358
372
|
}
|
|
359
|
-
|
|
373
|
+
throw new SlackActionError(
|
|
374
|
+
"Slack action exhausted retries",
|
|
375
|
+
"internal_error"
|
|
376
|
+
);
|
|
360
377
|
}
|
|
361
|
-
function
|
|
362
|
-
|
|
363
|
-
if (soul) {
|
|
364
|
-
return soul;
|
|
365
|
-
}
|
|
366
|
-
logWarn(
|
|
367
|
-
"soul_load_fallback",
|
|
368
|
-
{},
|
|
369
|
-
{
|
|
370
|
-
"app.file.candidates": soulPathCandidates()
|
|
371
|
-
},
|
|
372
|
-
"SOUL.md not found; using built-in default personality"
|
|
373
|
-
);
|
|
374
|
-
return DEFAULT_SOUL;
|
|
378
|
+
function getSlackClient() {
|
|
379
|
+
return getClient();
|
|
375
380
|
}
|
|
376
|
-
function
|
|
377
|
-
|
|
381
|
+
function isDmChannel(channelId) {
|
|
382
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
383
|
+
return Boolean(normalized && normalized.startsWith("D"));
|
|
378
384
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
385
|
+
function isConversationScopedChannel(channelId) {
|
|
386
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
387
|
+
if (!normalized) return false;
|
|
388
|
+
return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
|
|
389
|
+
}
|
|
390
|
+
function isConversationChannel(channelId) {
|
|
391
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
392
|
+
if (!normalized) return false;
|
|
393
|
+
return normalized.startsWith("C") || normalized.startsWith("G");
|
|
394
|
+
}
|
|
395
|
+
async function getFilePermalink(fileId) {
|
|
396
|
+
const client2 = getClient();
|
|
397
|
+
const response = await withSlackRetries(
|
|
398
|
+
() => client2.files.info({
|
|
399
|
+
file: fileId
|
|
400
|
+
})
|
|
401
|
+
);
|
|
402
|
+
return response.file?.permalink;
|
|
403
|
+
}
|
|
404
|
+
async function downloadPrivateSlackFile(url) {
|
|
405
|
+
const token = getSlackBotToken();
|
|
406
|
+
if (!token) {
|
|
407
|
+
throw new SlackActionError(
|
|
408
|
+
"SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack file downloads in this service",
|
|
409
|
+
"missing_token"
|
|
390
410
|
);
|
|
391
|
-
return DEFAULT_SOUL;
|
|
392
411
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
{},
|
|
401
|
-
{
|
|
402
|
-
"exception.message": error instanceof Error ? error.message : String(error)
|
|
403
|
-
},
|
|
404
|
-
"Failed to load WORLD.md; omitting world prompt context"
|
|
405
|
-
);
|
|
406
|
-
return null;
|
|
412
|
+
const response = await fetch(url, {
|
|
413
|
+
headers: {
|
|
414
|
+
Authorization: `Bearer ${token}`
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
if (!response.ok) {
|
|
418
|
+
throw new Error(`Slack file download failed: ${response.status}`);
|
|
407
419
|
}
|
|
408
|
-
|
|
409
|
-
function workspaceSkillDir(skillName) {
|
|
410
|
-
return sandboxSkillDir(skillName);
|
|
420
|
+
return Buffer.from(await response.arrayBuffer());
|
|
411
421
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
} catch {
|
|
419
|
-
return escapeXml(String(value));
|
|
420
|
-
}
|
|
422
|
+
|
|
423
|
+
// src/chat/credentials/subject.ts
|
|
424
|
+
var CREDENTIAL_SUBJECT_HMAC_CONTEXT = "junior.credential_subject.v1";
|
|
425
|
+
var CREDENTIAL_SUBJECT_SIGNATURE_VERSION = "v1";
|
|
426
|
+
function getCredentialSubjectSecret() {
|
|
427
|
+
return process.env.JUNIOR_SECRET?.trim() || void 0;
|
|
421
428
|
}
|
|
422
|
-
function
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
429
|
+
function buildPayload(input) {
|
|
430
|
+
return [
|
|
431
|
+
CREDENTIAL_SUBJECT_HMAC_CONTEXT,
|
|
432
|
+
input.allowedWhen,
|
|
433
|
+
input.teamId,
|
|
434
|
+
input.channelId,
|
|
435
|
+
input.userId
|
|
436
|
+
].join("\0");
|
|
437
|
+
}
|
|
438
|
+
function signPayload(secret, payload) {
|
|
439
|
+
const digest = createHmac("sha256", secret).update(payload).digest("hex");
|
|
440
|
+
return `${CREDENTIAL_SUBJECT_SIGNATURE_VERSION}=${digest}`;
|
|
441
|
+
}
|
|
442
|
+
function timingSafeMatch(expected, actual) {
|
|
443
|
+
const expectedBuffer = Buffer.from(expected);
|
|
444
|
+
const actualBuffer = Buffer.from(actual);
|
|
445
|
+
if (expectedBuffer.length !== actualBuffer.length) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
return timingSafeEqual(expectedBuffer, actualBuffer);
|
|
449
|
+
}
|
|
450
|
+
function createSlackDirectCredentialSubject(input) {
|
|
451
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
452
|
+
const teamId = input.teamId?.trim();
|
|
453
|
+
const userId = input.userId?.trim();
|
|
454
|
+
if (!channelId || !teamId || !userId || !isDmChannel(channelId)) {
|
|
455
|
+
return void 0;
|
|
426
456
|
}
|
|
427
|
-
return
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
function renderTagBlock(tag, content) {
|
|
433
|
-
return [`<${tag}>`, content, `</${tag}>`].join("\n");
|
|
457
|
+
return {
|
|
458
|
+
type: "user",
|
|
459
|
+
userId,
|
|
460
|
+
allowedWhen: "private-direct-conversation"
|
|
461
|
+
};
|
|
434
462
|
}
|
|
435
|
-
function
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
463
|
+
function bindSlackDirectCredentialSubject(input) {
|
|
464
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
465
|
+
const teamId = input.teamId.trim();
|
|
466
|
+
const secret = getCredentialSubjectSecret();
|
|
467
|
+
const { subject } = input;
|
|
468
|
+
const userId = subject.userId.trim();
|
|
469
|
+
if (!channelId || !teamId || !secret || !isDmChannel(channelId) || subject.type !== "user" || !userId || subject.allowedWhen !== "private-direct-conversation") {
|
|
470
|
+
return void 0;
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
type: "user",
|
|
474
|
+
userId,
|
|
475
|
+
allowedWhen: subject.allowedWhen,
|
|
476
|
+
binding: {
|
|
477
|
+
type: "slack-direct-conversation",
|
|
478
|
+
teamId,
|
|
479
|
+
channelId,
|
|
480
|
+
signature: signPayload(
|
|
481
|
+
secret,
|
|
482
|
+
buildPayload({
|
|
483
|
+
allowedWhen: subject.allowedWhen,
|
|
484
|
+
teamId,
|
|
485
|
+
channelId,
|
|
486
|
+
userId
|
|
487
|
+
})
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
};
|
|
444
491
|
}
|
|
445
|
-
function
|
|
446
|
-
const
|
|
447
|
-
|
|
492
|
+
function verifySlackDirectCredentialSubject(input) {
|
|
493
|
+
const channelId = normalizeSlackConversationId(input.channelId);
|
|
494
|
+
const secret = getCredentialSubjectSecret();
|
|
495
|
+
if (!channelId || !secret) {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
const { subject } = input;
|
|
499
|
+
const binding = subject.binding;
|
|
500
|
+
if (subject.type !== "user" || typeof subject.userId !== "string" || !subject.userId || subject.allowedWhen !== "private-direct-conversation" || !binding || binding.type !== "slack-direct-conversation" || typeof binding.signature !== "string" || !binding.signature || binding.teamId !== input.teamId || binding.channelId !== channelId) {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
const expected = signPayload(
|
|
504
|
+
secret,
|
|
505
|
+
buildPayload({
|
|
506
|
+
allowedWhen: subject.allowedWhen,
|
|
507
|
+
teamId: binding.teamId,
|
|
508
|
+
channelId: binding.channelId,
|
|
509
|
+
userId: subject.userId
|
|
510
|
+
})
|
|
448
511
|
);
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
];
|
|
458
|
-
for (const skill of autoSelectable) {
|
|
459
|
-
available.push(...formatSkillEntry(skill));
|
|
460
|
-
}
|
|
461
|
-
available.push("</available-skills>");
|
|
462
|
-
sections.push(available.join("\n"));
|
|
512
|
+
return timingSafeMatch(expected, binding.signature);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/chat/plugins/agent-hooks.ts
|
|
516
|
+
var AgentPluginHookDeniedError = class extends Error {
|
|
517
|
+
constructor(message) {
|
|
518
|
+
super(message);
|
|
519
|
+
this.name = "AgentPluginHookDeniedError";
|
|
463
520
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
521
|
+
};
|
|
522
|
+
var agentPlugins = [];
|
|
523
|
+
var AGENT_PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
524
|
+
var AGENT_PLUGIN_TOOL_NAME_RE = /^[a-z][A-Za-z0-9]*$/;
|
|
525
|
+
var OPERATIONAL_REPORT_MAX_METRICS = 8;
|
|
526
|
+
var OPERATIONAL_REPORT_MAX_RECORD_SETS = 8;
|
|
527
|
+
var OPERATIONAL_REPORT_MAX_FIELDS = 8;
|
|
528
|
+
var OPERATIONAL_REPORT_MAX_RECORDS = 25;
|
|
529
|
+
var OPERATIONAL_REPORT_MAX_LABEL_LENGTH = 80;
|
|
530
|
+
var OPERATIONAL_REPORT_MAX_VALUE_LENGTH = 160;
|
|
531
|
+
var AGENT_PLUGIN_ROUTE_METHODS = /* @__PURE__ */ new Set([
|
|
532
|
+
"GET",
|
|
533
|
+
"POST",
|
|
534
|
+
"PUT",
|
|
535
|
+
"PATCH",
|
|
536
|
+
"DELETE",
|
|
537
|
+
"HEAD",
|
|
538
|
+
"OPTIONS",
|
|
539
|
+
"ALL"
|
|
540
|
+
]);
|
|
541
|
+
function validateLegacyStatePrefixes(plugin) {
|
|
542
|
+
const prefixes = plugin.legacyStatePrefixes;
|
|
543
|
+
if (prefixes === void 0) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (!Array.isArray(prefixes)) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
`Trusted plugin "${plugin.name}" legacyStatePrefixes must be an array`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
const allowedPrefix = `junior:${plugin.name}`;
|
|
552
|
+
for (const rawPrefix of prefixes) {
|
|
553
|
+
const prefix = typeof rawPrefix === "string" ? rawPrefix.trim() : "";
|
|
554
|
+
if (!prefix) {
|
|
555
|
+
throw new Error(
|
|
556
|
+
`Trusted plugin "${plugin.name}" legacy state prefixes must be non-empty strings`
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
if (prefix !== allowedPrefix && !prefix.startsWith(`${allowedPrefix}:`)) {
|
|
560
|
+
throw new Error(
|
|
561
|
+
`Trusted plugin "${plugin.name}" legacy state prefix "${prefix}" must stay under "${allowedPrefix}"`
|
|
562
|
+
);
|
|
471
563
|
}
|
|
472
|
-
userCallable.push("</user-callable-skills>");
|
|
473
|
-
sections.push(userCallable.join("\n"));
|
|
474
564
|
}
|
|
475
|
-
return sections.length > 0 ? sections.join("\n") : null;
|
|
476
565
|
}
|
|
477
|
-
function
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
);
|
|
490
|
-
lines.push(" </catalog>");
|
|
566
|
+
function validateAgentPlugins(plugins) {
|
|
567
|
+
const seen = /* @__PURE__ */ new Set();
|
|
568
|
+
for (const plugin of plugins) {
|
|
569
|
+
if (!AGENT_PLUGIN_NAME_RE.test(plugin.name)) {
|
|
570
|
+
throw new Error(
|
|
571
|
+
`Trusted plugin name "${plugin.name}" must be a lowercase plugin identifier`
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
if (seen.has(plugin.name)) {
|
|
575
|
+
throw new Error(`Duplicate trusted plugin name "${plugin.name}"`);
|
|
576
|
+
}
|
|
577
|
+
seen.add(plugin.name);
|
|
578
|
+
validateLegacyStatePrefixes(plugin);
|
|
491
579
|
}
|
|
492
|
-
return lines.join("\n");
|
|
493
580
|
}
|
|
494
|
-
function
|
|
495
|
-
|
|
496
|
-
|
|
581
|
+
function setAgentPlugins(plugins) {
|
|
582
|
+
validateAgentPlugins(plugins);
|
|
583
|
+
const previous = agentPlugins;
|
|
584
|
+
agentPlugins = [...plugins].sort(
|
|
585
|
+
(left, right) => left.name.localeCompare(right.name)
|
|
497
586
|
);
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
587
|
+
return previous;
|
|
588
|
+
}
|
|
589
|
+
function getAgentPlugins() {
|
|
590
|
+
return [...agentPlugins];
|
|
591
|
+
}
|
|
592
|
+
function getAgentPluginTools(context) {
|
|
593
|
+
const tools = {};
|
|
594
|
+
for (const plugin of getAgentPlugins()) {
|
|
595
|
+
const hook = plugin.hooks?.tools;
|
|
596
|
+
if (!hook) {
|
|
597
|
+
continue;
|
|
506
598
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
599
|
+
const log = createAgentPluginLogger(plugin.name);
|
|
600
|
+
const credentialSubject = createSlackDirectCredentialSubject({
|
|
601
|
+
channelId: context.channelId,
|
|
602
|
+
teamId: context.teamId,
|
|
603
|
+
userId: context.requester?.userId
|
|
604
|
+
});
|
|
605
|
+
const pluginTools = hook({
|
|
606
|
+
plugin: { name: plugin.name },
|
|
607
|
+
log,
|
|
608
|
+
requester: context.requester,
|
|
609
|
+
channelCapabilities: context.channelCapabilities,
|
|
610
|
+
channelId: context.channelId,
|
|
611
|
+
...credentialSubject ? { credentialSubject } : {},
|
|
612
|
+
teamId: context.teamId,
|
|
613
|
+
messageTs: context.messageTs,
|
|
614
|
+
threadTs: context.threadTs,
|
|
615
|
+
userText: context.userText,
|
|
616
|
+
state: createPluginState(plugin.name, {
|
|
617
|
+
legacyStatePrefixes: plugin.legacyStatePrefixes
|
|
618
|
+
})
|
|
619
|
+
});
|
|
620
|
+
for (const [name, tool] of Object.entries(pluginTools)) {
|
|
621
|
+
if (!AGENT_PLUGIN_TOOL_NAME_RE.test(name)) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
`Trusted plugin tool "${name}" from plugin "${plugin.name}" must be a camelCase identifier`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
if (tools[name]) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
`Duplicate trusted plugin tool "${name}" from plugin "${plugin.name}"`
|
|
629
|
+
);
|
|
510
630
|
}
|
|
631
|
+
tools[name] = tool;
|
|
511
632
|
}
|
|
512
|
-
lines.push(" </tool>");
|
|
513
633
|
}
|
|
514
|
-
return
|
|
634
|
+
return tools;
|
|
515
635
|
}
|
|
516
|
-
function
|
|
517
|
-
const
|
|
518
|
-
if (
|
|
519
|
-
|
|
636
|
+
function routeMethods(route, pluginName) {
|
|
637
|
+
const methods = Array.isArray(route.method) ? route.method : [route.method ?? "ALL"];
|
|
638
|
+
if (methods.length === 0) {
|
|
639
|
+
throw new Error(
|
|
640
|
+
`Trusted plugin route "${route.path}" from plugin "${pluginName}" must declare at least one method`
|
|
641
|
+
);
|
|
520
642
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if (!artifactState) return null;
|
|
528
|
-
const lines = [];
|
|
529
|
-
if (artifactState.lastCanvasId) {
|
|
530
|
-
lines.push(`- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}`);
|
|
643
|
+
for (const method of methods) {
|
|
644
|
+
if (!AGENT_PLUGIN_ROUTE_METHODS.has(method)) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
`Trusted plugin route "${route.path}" from plugin "${pluginName}" has invalid method "${String(method)}"`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
531
649
|
}
|
|
532
|
-
if (
|
|
533
|
-
|
|
650
|
+
if (methods.includes("ALL") && methods.length > 1) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
`Trusted plugin route "${route.path}" from plugin "${pluginName}" must not combine ALL with explicit methods`
|
|
653
|
+
);
|
|
534
654
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
655
|
+
return methods;
|
|
656
|
+
}
|
|
657
|
+
function getAgentPluginRoutes() {
|
|
658
|
+
const routes = [];
|
|
659
|
+
const seen = /* @__PURE__ */ new Set();
|
|
660
|
+
const methodsByPath = /* @__PURE__ */ new Map();
|
|
661
|
+
for (const plugin of getAgentPlugins()) {
|
|
662
|
+
const hook = plugin.hooks?.routes;
|
|
663
|
+
if (!hook) {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
const log = createAgentPluginLogger(plugin.name);
|
|
667
|
+
const pluginRoutes = hook({
|
|
668
|
+
plugin: { name: plugin.name },
|
|
669
|
+
log
|
|
670
|
+
});
|
|
671
|
+
if (!Array.isArray(pluginRoutes)) {
|
|
672
|
+
throw new Error(
|
|
673
|
+
`Trusted plugin routes hook from plugin "${plugin.name}" must return an array`
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
for (const route of pluginRoutes) {
|
|
677
|
+
if (!isRecord2(route)) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
`Trusted plugin route from plugin "${plugin.name}" must be an object`
|
|
680
|
+
);
|
|
543
681
|
}
|
|
682
|
+
if (typeof route.path !== "string" || !route.path.startsWith("/")) {
|
|
683
|
+
throw new Error(
|
|
684
|
+
`Trusted plugin route "${route.path}" from plugin "${plugin.name}" must start with /`
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
if (typeof route.handler !== "function") {
|
|
688
|
+
throw new Error(
|
|
689
|
+
`Trusted plugin route "${route.path}" from plugin "${plugin.name}" must provide a handler`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
const methods = routeMethods(route, plugin.name);
|
|
693
|
+
const pathMethods = methodsByPath.get(route.path) ?? /* @__PURE__ */ new Set();
|
|
694
|
+
if (pathMethods.has("ALL") || methods.includes("ALL") && pathMethods.size > 0) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
`Trusted plugin route "${route.path}" conflicts with an ALL route for the same path`
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
for (const method of methods) {
|
|
700
|
+
const key2 = `${method}:${route.path}`;
|
|
701
|
+
if (seen.has(key2)) {
|
|
702
|
+
throw new Error(
|
|
703
|
+
`Duplicate trusted plugin route "${method} ${route.path}"`
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
seen.add(key2);
|
|
707
|
+
pathMethods.add(method);
|
|
708
|
+
}
|
|
709
|
+
methodsByPath.set(route.path, pathMethods);
|
|
710
|
+
routes.push({
|
|
711
|
+
...route,
|
|
712
|
+
pluginName: plugin.name
|
|
713
|
+
});
|
|
544
714
|
}
|
|
545
715
|
}
|
|
546
|
-
|
|
547
|
-
lines.push(`- last_list_id: ${escapeXml(artifactState.lastListId)}`);
|
|
548
|
-
}
|
|
549
|
-
if (artifactState.lastListUrl) {
|
|
550
|
-
lines.push(`- last_list_url: ${escapeXml(artifactState.lastListUrl)}`);
|
|
551
|
-
}
|
|
552
|
-
return lines.length > 0 ? lines : null;
|
|
716
|
+
return routes;
|
|
553
717
|
}
|
|
554
|
-
function
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
"- If the first result is empty, stale, ambiguous, or incomplete, try a focused alternate query, path, command, or source before concluding the answer cannot be verified."
|
|
575
|
-
];
|
|
576
|
-
var TOOL_CALL_STYLE_RULES = [
|
|
577
|
-
"- For routine low-risk tool use, call the tool directly without narrating the obvious step first.",
|
|
578
|
-
"- Briefly narrate only when it helps the user understand multi-step work, sensitive actions, destructive actions, or a notable change in approach.",
|
|
579
|
-
"- When a first-class tool exists for an action, use it directly instead of asking the user to run an equivalent command, slash command, or manual lookup.",
|
|
580
|
-
"- Keep tool-call explanations separate from final answers; final answers should report results, evidence, or blockers."
|
|
581
|
-
];
|
|
582
|
-
var SKILL_POLICY_RULES = [
|
|
583
|
-
"- Only load skills listed in `<available-skills>`, `<user-callable-skills>`, or named by `<explicit-skill-trigger>`. Never guess or invent a skill name.",
|
|
584
|
-
"- Load one skill at a time. After `loadSkill`, follow the instructions returned by that tool result."
|
|
585
|
-
];
|
|
586
|
-
var EXECUTION_CONTRACT_RULES = [
|
|
587
|
-
"- Actionable request: act in this turn.",
|
|
588
|
-
"- Continue until done or genuinely blocked. Do not finish with a plan, promise, or offer to check next when an available tool or source can move the request forward.",
|
|
589
|
-
"- Completion means the final answer covers the user's actual ask, including requested follow-up checks, and is grounded in the best evidence you could access.",
|
|
590
|
-
"- Ask the user only for missing access, approval, or a decision that blocks safe progress. Ask one focused question; otherwise infer conservatively and continue.",
|
|
591
|
-
"- For conflicting evidence, compare sources and state which source is authoritative for the answer.",
|
|
592
|
-
"- For non-trivial or long-running work, call `reportProgress` early when available, then only when the major phase changes. Routine tool calls should stay silent."
|
|
593
|
-
];
|
|
594
|
-
var CONVERSATION_RULES = [
|
|
595
|
-
"- In thread follow-ups, answer from prior thread context; do not repeat resolved clarifying questions.",
|
|
596
|
-
"- Preserve attribution roles from thread context: the requester is the person asking now, which may differ from the original reporter or subject.",
|
|
597
|
-
"- Runtime owns continuation and authorization notices; on resumed turns, answer with the final requested content only."
|
|
598
|
-
];
|
|
599
|
-
var SLACK_ACTION_RULES = [
|
|
600
|
-
"- Context-bound Slack tools use runtime-owned targets; do not invent channel, canvas, list, or message IDs.",
|
|
601
|
-
"- Use first-class Slack tools for Slack side effects; do not use bash, curl, or provider APIs to bypass Slack tool targeting.",
|
|
602
|
-
"- Use channel-post and emoji-reaction tools only when the user explicitly asks for that Slack side effect.",
|
|
603
|
-
"- For explicit channel-post or emoji-reaction requests, skip a duplicate thread text reply when the tool result already satisfies the request.",
|
|
604
|
-
"- Do not claim an attachment, canvas, channel post, list update, or reaction succeeded unless the tool returned success this turn; when it did, include any link the tool returned.",
|
|
605
|
-
"- Do not use reactions as progress indicators."
|
|
606
|
-
];
|
|
607
|
-
var SAFETY_RULES = [
|
|
608
|
-
"- Stay within the user's request and the runtime's available capabilities; do not pursue independent goals, persistence, replication, credential gathering, or access expansion.",
|
|
609
|
-
"- Respect stop, pause, audit, and approval boundaries. Do not bypass safeguards or persuade the user to weaken them.",
|
|
610
|
-
"- Do not change system prompts, tool policies, security settings, credentials, or runtime configuration unless the user explicitly requests that exact administrative action and an available tool permits it."
|
|
611
|
-
];
|
|
612
|
-
var FAILURE_RULES = [
|
|
613
|
-
"- For tool/runtime failures, run the named check before diagnosing and report the exact failed command plus stderr/exit code.",
|
|
614
|
-
"- If a fact cannot be verified after focused checks, say what you checked and what blocked a stronger answer.",
|
|
615
|
-
"- Do not surface raw tool payloads, execution-escape text, or internal routing metadata as the final answer."
|
|
616
|
-
];
|
|
617
|
-
function renderRuleSection(tag, lines) {
|
|
618
|
-
return [`<${tag}>`, ...lines, `</${tag}>`].join("\n");
|
|
619
|
-
}
|
|
620
|
-
function buildBehaviorSection() {
|
|
621
|
-
return [
|
|
622
|
-
renderRuleSection("tool-policy", TOOL_POLICY_RULES),
|
|
623
|
-
renderRuleSection("tool-call-style", TOOL_CALL_STYLE_RULES),
|
|
624
|
-
renderRuleSection("skill-policy", SKILL_POLICY_RULES),
|
|
625
|
-
renderRuleSection("execution-contract", EXECUTION_CONTRACT_RULES),
|
|
626
|
-
renderRuleSection("conversation", CONVERSATION_RULES),
|
|
627
|
-
renderRuleSection("slack-actions", SLACK_ACTION_RULES),
|
|
628
|
-
renderRuleSection("safety", SAFETY_RULES),
|
|
629
|
-
renderRuleSection("failure-handling", FAILURE_RULES)
|
|
630
|
-
].join("\n\n");
|
|
631
|
-
}
|
|
632
|
-
function buildOutputSection() {
|
|
633
|
-
const openTag = `<output format="slack-markdown" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`;
|
|
634
|
-
return [
|
|
635
|
-
openTag,
|
|
636
|
-
"- Start with the answer or result, not internal process narration.",
|
|
637
|
-
"- Use Slack-flavored Markdown: **bold** section labels, `code`, [text](url) links, bullet lists, and fenced code blocks. No hash-prefixed headings and no tables. When the answer primarily lists several URLs, show each URL bare instead of as a labeled link.",
|
|
638
|
-
"- Keep replies brief and scannable; use bullets or short code blocks when helpful, and one compact thread reply when it fits.",
|
|
639
|
-
"- When a research or document-style answer would benefit from continuation, multiple sections, or future reference value, create a Slack canvas and keep the thread reply to one or two short sentences plus the link; do not recap the canvas contents.",
|
|
640
|
-
"- Unless a successful Slack side-effect tool intentionally satisfied the request by itself, end every turn with a final user-facing markdown response.",
|
|
641
|
-
"</output>"
|
|
642
|
-
].join("\n");
|
|
718
|
+
function trustedSlackConversationUrl(pluginName, link) {
|
|
719
|
+
const url = typeof link?.url === "string" ? link.url.trim() : "";
|
|
720
|
+
if (!url) {
|
|
721
|
+
return void 0;
|
|
722
|
+
}
|
|
723
|
+
let parsed;
|
|
724
|
+
try {
|
|
725
|
+
parsed = new URL(url);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
throw new Error(
|
|
728
|
+
`Trusted plugin "${pluginName}" slackConversationLink must return an absolute http(s) URL`,
|
|
729
|
+
{ cause: error }
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
733
|
+
throw new Error(
|
|
734
|
+
`Trusted plugin "${pluginName}" slackConversationLink must return an absolute http(s) URL`
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
return parsed.toString();
|
|
643
738
|
}
|
|
644
|
-
function
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
739
|
+
function getAgentPluginSlackConversationLink(conversationId) {
|
|
740
|
+
for (const plugin of getAgentPlugins()) {
|
|
741
|
+
const hook = plugin.hooks?.slackConversationLink;
|
|
742
|
+
if (!hook) {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const log = createAgentPluginLogger(plugin.name);
|
|
746
|
+
const link = hook({
|
|
747
|
+
plugin: { name: plugin.name },
|
|
748
|
+
log,
|
|
749
|
+
conversationId
|
|
750
|
+
});
|
|
751
|
+
const url = trustedSlackConversationUrl(plugin.name, link);
|
|
752
|
+
if (url) {
|
|
753
|
+
return { url };
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return void 0;
|
|
649
757
|
}
|
|
650
|
-
function
|
|
651
|
-
return
|
|
758
|
+
function pluginReadState(state) {
|
|
759
|
+
return {
|
|
760
|
+
get: state.get
|
|
761
|
+
};
|
|
652
762
|
}
|
|
653
|
-
function
|
|
654
|
-
if (
|
|
655
|
-
return
|
|
763
|
+
function operationalReportText(value, maxLength) {
|
|
764
|
+
if (typeof value !== "string") {
|
|
765
|
+
return void 0;
|
|
656
766
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const lines = [
|
|
661
|
-
params.conversationId ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}` : "",
|
|
662
|
-
params.slackConversation?.type ? `- slack.conversation.type: ${escapeXml(params.slackConversation.type)}` : "",
|
|
663
|
-
params.slackConversation?.name ? `- slack.conversation.name: ${escapeXml(params.slackConversation.name)}` : ""
|
|
664
|
-
].filter(Boolean);
|
|
665
|
-
if (lines.length === 0) {
|
|
666
|
-
return null;
|
|
767
|
+
const trimmed = value.trim();
|
|
768
|
+
if (!trimmed) {
|
|
769
|
+
return void 0;
|
|
667
770
|
}
|
|
668
|
-
return
|
|
771
|
+
return trimmed.length <= maxLength ? trimmed : `${trimmed.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
669
772
|
}
|
|
670
|
-
function
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
])
|
|
773
|
+
function operationalReportTone(tone) {
|
|
774
|
+
return tone === "danger" || tone === "good" || tone === "neutral" || tone === "warning" ? tone : void 0;
|
|
775
|
+
}
|
|
776
|
+
function sanitizeOperationalReport(args) {
|
|
777
|
+
const metrics = args.report.metrics?.slice(0, OPERATIONAL_REPORT_MAX_METRICS).map((metric) => {
|
|
778
|
+
const label = operationalReportText(
|
|
779
|
+
metric.label,
|
|
780
|
+
OPERATIONAL_REPORT_MAX_LABEL_LENGTH
|
|
679
781
|
);
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
user_name: params.requester?.userName,
|
|
684
|
-
user_id: params.requester?.userId
|
|
685
|
-
});
|
|
686
|
-
if (requesterLines) {
|
|
687
|
-
blocks.push(requesterLines);
|
|
688
|
-
}
|
|
689
|
-
const artifactLines = formatArtifactsLines(params.artifactState);
|
|
690
|
-
if (artifactLines) {
|
|
691
|
-
blocks.push(renderTag("artifacts", artifactLines));
|
|
692
|
-
}
|
|
693
|
-
const configLines = formatConfigurationLines(params.configuration);
|
|
694
|
-
if (configLines) {
|
|
695
|
-
blocks.push(
|
|
696
|
-
renderTag("configuration", [
|
|
697
|
-
"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.",
|
|
698
|
-
...configLines
|
|
699
|
-
])
|
|
782
|
+
const value = operationalReportText(
|
|
783
|
+
metric.value,
|
|
784
|
+
OPERATIONAL_REPORT_MAX_VALUE_LENGTH
|
|
700
785
|
);
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
786
|
+
if (!label || !value) {
|
|
787
|
+
return void 0;
|
|
788
|
+
}
|
|
789
|
+
const sanitizedMetric = { label, value };
|
|
790
|
+
const tone = operationalReportTone(metric.tone);
|
|
791
|
+
if (tone) {
|
|
792
|
+
sanitizedMetric.tone = tone;
|
|
793
|
+
}
|
|
794
|
+
return sanitizedMetric;
|
|
795
|
+
}).filter((metric) => Boolean(metric));
|
|
796
|
+
const recordSets = args.report.recordSets?.slice(0, OPERATIONAL_REPORT_MAX_RECORD_SETS).map((recordSet, recordSetIndex) => {
|
|
797
|
+
const title2 = operationalReportText(
|
|
798
|
+
recordSet.title,
|
|
799
|
+
OPERATIONAL_REPORT_MAX_LABEL_LENGTH
|
|
800
|
+
);
|
|
801
|
+
if (!title2) {
|
|
802
|
+
return void 0;
|
|
803
|
+
}
|
|
804
|
+
const fields = recordSet.fields?.slice(0, OPERATIONAL_REPORT_MAX_FIELDS).map((field) => {
|
|
805
|
+
const key2 = operationalReportText(
|
|
806
|
+
field.key,
|
|
807
|
+
OPERATIONAL_REPORT_MAX_LABEL_LENGTH
|
|
808
|
+
);
|
|
809
|
+
const label = operationalReportText(
|
|
810
|
+
field.label,
|
|
811
|
+
OPERATIONAL_REPORT_MAX_LABEL_LENGTH
|
|
812
|
+
);
|
|
813
|
+
return key2 && label ? { key: key2, label } : void 0;
|
|
814
|
+
}).filter((field) => Boolean(field));
|
|
815
|
+
const records = recordSet.records?.slice(0, OPERATIONAL_REPORT_MAX_RECORDS).map((record, recordIndex) => {
|
|
816
|
+
const id = operationalReportText(
|
|
817
|
+
record.id,
|
|
818
|
+
OPERATIONAL_REPORT_MAX_LABEL_LENGTH
|
|
819
|
+
) ?? `${recordSetIndex}:${recordIndex}`;
|
|
820
|
+
const values = Object.fromEntries(
|
|
821
|
+
(fields ?? []).map((field) => [
|
|
822
|
+
field.key,
|
|
823
|
+
operationalReportText(
|
|
824
|
+
record.values[field.key],
|
|
825
|
+
OPERATIONAL_REPORT_MAX_VALUE_LENGTH
|
|
826
|
+
) ?? ""
|
|
827
|
+
])
|
|
828
|
+
);
|
|
829
|
+
const sanitizedRecord = {
|
|
830
|
+
id,
|
|
831
|
+
values
|
|
832
|
+
};
|
|
833
|
+
const tone = operationalReportTone(record.tone);
|
|
834
|
+
if (tone) {
|
|
835
|
+
sanitizedRecord.tone = tone;
|
|
836
|
+
}
|
|
837
|
+
return sanitizedRecord;
|
|
838
|
+
});
|
|
839
|
+
const sanitizedRecordSet = { title: title2 };
|
|
840
|
+
if (fields?.length) {
|
|
841
|
+
sanitizedRecordSet.fields = fields;
|
|
842
|
+
}
|
|
843
|
+
const emptyText = operationalReportText(
|
|
844
|
+
recordSet.emptyText,
|
|
845
|
+
OPERATIONAL_REPORT_MAX_VALUE_LENGTH
|
|
708
846
|
);
|
|
847
|
+
if (emptyText) {
|
|
848
|
+
sanitizedRecordSet.emptyText = emptyText;
|
|
849
|
+
}
|
|
850
|
+
if (records?.length) {
|
|
851
|
+
sanitizedRecordSet.records = records;
|
|
852
|
+
}
|
|
853
|
+
return sanitizedRecordSet;
|
|
854
|
+
}).filter(
|
|
855
|
+
(recordSet) => Boolean(recordSet)
|
|
856
|
+
);
|
|
857
|
+
const sanitized = {
|
|
858
|
+
pluginName: args.pluginName
|
|
859
|
+
};
|
|
860
|
+
const generatedAt = operationalReportText(
|
|
861
|
+
args.report.generatedAt,
|
|
862
|
+
OPERATIONAL_REPORT_MAX_VALUE_LENGTH
|
|
863
|
+
);
|
|
864
|
+
if (generatedAt) {
|
|
865
|
+
sanitized.generatedAt = generatedAt;
|
|
709
866
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
return null;
|
|
867
|
+
if (recordSets?.length) {
|
|
868
|
+
sanitized.recordSets = recordSets;
|
|
713
869
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
function buildCapabilitiesSection(params) {
|
|
717
|
-
const blocks = [];
|
|
718
|
-
const availableSkills = formatAvailableSkillsForPrompt(
|
|
719
|
-
params.availableSkills,
|
|
720
|
-
params.invocation
|
|
721
|
-
);
|
|
722
|
-
if (availableSkills) {
|
|
723
|
-
blocks.push(availableSkills);
|
|
870
|
+
if (metrics?.length) {
|
|
871
|
+
sanitized.metrics = metrics;
|
|
724
872
|
}
|
|
725
|
-
const
|
|
726
|
-
|
|
873
|
+
const title = operationalReportText(
|
|
874
|
+
args.report.title,
|
|
875
|
+
OPERATIONAL_REPORT_MAX_LABEL_LENGTH
|
|
727
876
|
);
|
|
728
|
-
if (
|
|
729
|
-
|
|
730
|
-
}
|
|
731
|
-
const toolGuidance = formatToolGuidanceForPrompt(params.toolGuidance ?? []);
|
|
732
|
-
if (toolGuidance) {
|
|
733
|
-
blocks.push(renderTagBlock("tool-guidance", toolGuidance));
|
|
877
|
+
if (title) {
|
|
878
|
+
sanitized.title = title;
|
|
734
879
|
}
|
|
735
|
-
|
|
736
|
-
|
|
880
|
+
return sanitized;
|
|
881
|
+
}
|
|
882
|
+
function failedOperationalReport(args) {
|
|
883
|
+
return {
|
|
884
|
+
generatedAt: new Date(args.nowMs).toISOString(),
|
|
885
|
+
pluginName: args.pluginName,
|
|
886
|
+
metrics: [{ label: "report", tone: "danger", value: "failed" }],
|
|
887
|
+
title: args.pluginName,
|
|
888
|
+
recordSets: [
|
|
889
|
+
{
|
|
890
|
+
emptyText: "This plugin report failed to load.",
|
|
891
|
+
title: "Error"
|
|
892
|
+
}
|
|
893
|
+
]
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
async function getAgentPluginOperationalReports(nowMs = Date.now()) {
|
|
897
|
+
const reports = [];
|
|
898
|
+
for (const plugin of getAgentPlugins()) {
|
|
899
|
+
const hook = plugin.hooks?.operationalReport;
|
|
900
|
+
if (!hook) {
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
const log = createAgentPluginLogger(plugin.name);
|
|
904
|
+
try {
|
|
905
|
+
const state = createPluginState(plugin.name, {
|
|
906
|
+
legacyStatePrefixes: plugin.legacyStatePrefixes
|
|
907
|
+
});
|
|
908
|
+
const report = await hook({
|
|
909
|
+
plugin: { name: plugin.name },
|
|
910
|
+
log,
|
|
911
|
+
nowMs,
|
|
912
|
+
state: pluginReadState(state)
|
|
913
|
+
});
|
|
914
|
+
if (!report) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
reports.push(
|
|
918
|
+
sanitizeOperationalReport({
|
|
919
|
+
pluginName: plugin.name,
|
|
920
|
+
report
|
|
921
|
+
})
|
|
922
|
+
);
|
|
923
|
+
} catch (error) {
|
|
924
|
+
log.error("Trusted plugin operational report failed", {
|
|
925
|
+
error: error instanceof Error ? error.message : String(error)
|
|
926
|
+
});
|
|
927
|
+
reports.push(failedOperationalReport({ nowMs, pluginName: plugin.name }));
|
|
928
|
+
}
|
|
737
929
|
}
|
|
738
|
-
return
|
|
930
|
+
return reports;
|
|
739
931
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
buildIdentitySection(),
|
|
743
|
-
buildPersonalitySection(),
|
|
744
|
-
buildWorldSection(),
|
|
745
|
-
buildBehaviorSection(),
|
|
746
|
-
buildOutputSection()
|
|
747
|
-
].filter((section) => Boolean(section)).join("\n\n");
|
|
748
|
-
function buildSystemPrompt() {
|
|
749
|
-
return STATIC_SYSTEM_PROMPT;
|
|
932
|
+
function isRecord2(value) {
|
|
933
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
750
934
|
}
|
|
751
|
-
function
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
return null;
|
|
935
|
+
function normalizeEnv(value) {
|
|
936
|
+
if (!isRecord2(value)) {
|
|
937
|
+
return {};
|
|
755
938
|
}
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
toolGuidance: params.toolGuidance ?? []
|
|
762
|
-
}),
|
|
763
|
-
buildContextSection({
|
|
764
|
-
requester: params.requester,
|
|
765
|
-
artifactState: params.artifactState,
|
|
766
|
-
configuration: params.configuration,
|
|
767
|
-
invocation: params.invocation
|
|
768
|
-
}),
|
|
769
|
-
buildRuntimeSection(params.runtime ?? {})
|
|
770
|
-
].filter((section) => Boolean(section));
|
|
771
|
-
if (runtimeSections.length === 0) {
|
|
772
|
-
return null;
|
|
939
|
+
const env = {};
|
|
940
|
+
for (const [key2, rawValue] of Object.entries(value)) {
|
|
941
|
+
if (typeof rawValue === "string") {
|
|
942
|
+
env[key2] = rawValue;
|
|
943
|
+
}
|
|
773
944
|
}
|
|
774
|
-
|
|
945
|
+
return env;
|
|
946
|
+
}
|
|
947
|
+
function createSandboxCapability(sandbox) {
|
|
948
|
+
return {
|
|
949
|
+
root: SANDBOX_WORKSPACE_ROOT,
|
|
950
|
+
juniorRoot: `${SANDBOX_WORKSPACE_ROOT}/.junior`,
|
|
951
|
+
async readFile(filePath) {
|
|
952
|
+
return await sandbox.readFileToBuffer({ path: filePath }) ?? null;
|
|
953
|
+
},
|
|
954
|
+
async run(input) {
|
|
955
|
+
const result = await sandbox.runCommand(input);
|
|
956
|
+
const [stdout, stderr] = await Promise.all([
|
|
957
|
+
result.stdout(),
|
|
958
|
+
result.stderr()
|
|
959
|
+
]);
|
|
960
|
+
return {
|
|
961
|
+
exitCode: result.exitCode,
|
|
962
|
+
stdout,
|
|
963
|
+
stderr
|
|
964
|
+
};
|
|
965
|
+
},
|
|
966
|
+
async writeFile(input) {
|
|
967
|
+
await sandbox.writeFiles([
|
|
968
|
+
{
|
|
969
|
+
path: input.path,
|
|
970
|
+
content: input.content,
|
|
971
|
+
...input.mode !== void 0 ? { mode: input.mode } : {}
|
|
972
|
+
}
|
|
973
|
+
]);
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function createAgentPluginHookRunner(input = {}) {
|
|
978
|
+
const loaded = getAgentPlugins();
|
|
979
|
+
return {
|
|
980
|
+
async prepareSandbox(sandbox) {
|
|
981
|
+
const sandboxCapability = createSandboxCapability(sandbox);
|
|
982
|
+
for (const plugin of loaded) {
|
|
983
|
+
const hook = plugin.hooks?.sandboxPrepare;
|
|
984
|
+
if (!hook) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
logInfo(
|
|
988
|
+
"agent_plugin_hook_sandbox_prepare",
|
|
989
|
+
{},
|
|
990
|
+
{ "app.plugin.name": plugin.name },
|
|
991
|
+
"Running agent plugin sandbox prepare hook"
|
|
992
|
+
);
|
|
993
|
+
await hook({
|
|
994
|
+
plugin: { name: plugin.name },
|
|
995
|
+
log: createAgentPluginLogger(plugin.name),
|
|
996
|
+
requester: input.requester,
|
|
997
|
+
sandbox: sandboxCapability
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
async beforeToolExecute(tool) {
|
|
1002
|
+
let nextInput = { ...tool.input };
|
|
1003
|
+
const env = normalizeEnv(nextInput.env);
|
|
1004
|
+
for (const plugin of loaded) {
|
|
1005
|
+
const hook = plugin.hooks?.beforeToolExecute;
|
|
1006
|
+
if (!hook) {
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
let replacement;
|
|
1010
|
+
let denied;
|
|
1011
|
+
await hook({
|
|
1012
|
+
plugin: { name: plugin.name },
|
|
1013
|
+
log: createAgentPluginLogger(plugin.name),
|
|
1014
|
+
requester: input.requester,
|
|
1015
|
+
tool: {
|
|
1016
|
+
name: tool.name,
|
|
1017
|
+
input: nextInput
|
|
1018
|
+
},
|
|
1019
|
+
env: {
|
|
1020
|
+
get(key2) {
|
|
1021
|
+
return env[key2];
|
|
1022
|
+
},
|
|
1023
|
+
set(key2, value) {
|
|
1024
|
+
env[key2] = value;
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
decision: {
|
|
1028
|
+
deny(message) {
|
|
1029
|
+
denied = message;
|
|
1030
|
+
},
|
|
1031
|
+
replaceInput(input2) {
|
|
1032
|
+
replacement = input2;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
if (denied) {
|
|
1037
|
+
throw new AgentPluginHookDeniedError(denied);
|
|
1038
|
+
}
|
|
1039
|
+
if (replacement !== void 0) {
|
|
1040
|
+
if (!isRecord2(replacement)) {
|
|
1041
|
+
throw new Error(
|
|
1042
|
+
`Plugin "${plugin.name}" replaced tool input with a non-object value`
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
nextInput = { ...replacement };
|
|
1046
|
+
Object.assign(env, normalizeEnv(nextInput.env));
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return {
|
|
1050
|
+
input: {
|
|
1051
|
+
...nextInput,
|
|
1052
|
+
...Object.keys(env).length > 0 ? { env } : {}
|
|
1053
|
+
},
|
|
1054
|
+
env
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/handlers/health.ts
|
|
1061
|
+
function GET() {
|
|
1062
|
+
return Response.json({
|
|
1063
|
+
status: "ok",
|
|
1064
|
+
service: "junior",
|
|
1065
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/chat/prompt.ts
|
|
1070
|
+
import fs from "fs";
|
|
1071
|
+
import path from "path";
|
|
1072
|
+
|
|
1073
|
+
// src/chat/turn-context-tag.ts
|
|
1074
|
+
var TURN_CONTEXT_TAG = "runtime-turn-context";
|
|
1075
|
+
|
|
1076
|
+
// src/chat/interruption-marker.ts
|
|
1077
|
+
var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
|
|
1078
|
+
function getInterruptionMarker() {
|
|
1079
|
+
return INTERRUPTED_MARKER;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// src/chat/slack/status-format.ts
|
|
1083
|
+
var SLACK_STATUS_MAX_LENGTH = 50;
|
|
1084
|
+
function truncateStatusText(text) {
|
|
1085
|
+
const trimmed = text.trim();
|
|
1086
|
+
if (!trimmed) {
|
|
1087
|
+
return "";
|
|
1088
|
+
}
|
|
1089
|
+
if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) {
|
|
1090
|
+
return trimmed;
|
|
1091
|
+
}
|
|
1092
|
+
return `${trimmed.slice(0, SLACK_STATUS_MAX_LENGTH - 3).trimEnd()}...`;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// src/chat/slack/mrkdwn.ts
|
|
1096
|
+
function ensureBlockSpacing(text) {
|
|
1097
|
+
const codeBlockPattern = /^```/;
|
|
1098
|
+
const listItemPattern = /^[-*•]\s|^\d+\.\s/;
|
|
1099
|
+
const lines = text.split("\n");
|
|
1100
|
+
const result = [];
|
|
1101
|
+
let inCodeBlock = false;
|
|
1102
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1103
|
+
const line = lines[i];
|
|
1104
|
+
const isCodeFence = codeBlockPattern.test(line.trimStart());
|
|
1105
|
+
if (isCodeFence) {
|
|
1106
|
+
if (!inCodeBlock) {
|
|
1107
|
+
const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
|
|
1108
|
+
if (prev2 !== void 0 && prev2.trim() !== "") {
|
|
1109
|
+
result.push("");
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
inCodeBlock = !inCodeBlock;
|
|
1113
|
+
result.push(line);
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
if (inCodeBlock) {
|
|
1117
|
+
result.push(line);
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
const prev = result.length > 0 ? result[result.length - 1] : void 0;
|
|
1121
|
+
if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
|
|
1122
|
+
result.push("");
|
|
1123
|
+
}
|
|
1124
|
+
result.push(line);
|
|
1125
|
+
}
|
|
1126
|
+
return result.join("\n");
|
|
1127
|
+
}
|
|
1128
|
+
function renderSlackMrkdwn(text) {
|
|
1129
|
+
let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
|
|
1130
|
+
normalized = ensureBlockSpacing(normalized);
|
|
1131
|
+
return normalized.replace(/\n{3,}/g, "\n\n").trim();
|
|
1132
|
+
}
|
|
1133
|
+
function normalizeSlackStatusText(text) {
|
|
1134
|
+
const trimmed = text.trim();
|
|
1135
|
+
if (!trimmed) {
|
|
1136
|
+
return "";
|
|
1137
|
+
}
|
|
1138
|
+
return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// src/chat/slack/output.ts
|
|
1142
|
+
var MAX_INLINE_CHARS = 2200;
|
|
1143
|
+
var MAX_INLINE_LINES = 45;
|
|
1144
|
+
var CONTINUED_MARKER = "\n\n[Continued below]";
|
|
1145
|
+
function countSlackLines(text) {
|
|
1146
|
+
if (!text) {
|
|
1147
|
+
return 0;
|
|
1148
|
+
}
|
|
1149
|
+
return text.split("\n").length;
|
|
1150
|
+
}
|
|
1151
|
+
function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
1152
|
+
return text.length <= maxChars && countSlackLines(text) <= maxLines;
|
|
1153
|
+
}
|
|
1154
|
+
function findSplitIndex(text, maxChars) {
|
|
1155
|
+
if (text.length <= maxChars) {
|
|
1156
|
+
return text.length;
|
|
1157
|
+
}
|
|
1158
|
+
const bounded = text.slice(0, maxChars);
|
|
1159
|
+
const newlineIndex = bounded.lastIndexOf("\n");
|
|
1160
|
+
if (newlineIndex > 0) {
|
|
1161
|
+
return newlineIndex;
|
|
1162
|
+
}
|
|
1163
|
+
const spaceIndex = bounded.lastIndexOf(" ");
|
|
1164
|
+
if (spaceIndex > 0) {
|
|
1165
|
+
return spaceIndex;
|
|
1166
|
+
}
|
|
1167
|
+
return maxChars;
|
|
1168
|
+
}
|
|
1169
|
+
function splitByLineBudget(text, maxLines) {
|
|
1170
|
+
if (maxLines <= 0) {
|
|
1171
|
+
return "";
|
|
1172
|
+
}
|
|
1173
|
+
const lines = text.split("\n");
|
|
1174
|
+
if (lines.length <= maxLines) {
|
|
1175
|
+
return text;
|
|
1176
|
+
}
|
|
1177
|
+
return lines.slice(0, maxLines).join("\n");
|
|
1178
|
+
}
|
|
1179
|
+
function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
1180
|
+
return {
|
|
1181
|
+
maxChars: Math.max(1, maxChars - suffix.length),
|
|
1182
|
+
maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
function forceSplitBudget(text, budget) {
|
|
1186
|
+
const lineCount = countSlackLines(text);
|
|
1187
|
+
return {
|
|
1188
|
+
maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
|
|
1189
|
+
maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
function getFenceContinuation(text) {
|
|
1193
|
+
let open;
|
|
1194
|
+
for (const line of text.split("\n")) {
|
|
1195
|
+
const trimmed = line.trimStart();
|
|
1196
|
+
const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
|
|
1197
|
+
if (!openerMatch) {
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
if (!open) {
|
|
1201
|
+
open = {
|
|
1202
|
+
fence: openerMatch[1],
|
|
1203
|
+
openerLine: trimmed
|
|
1204
|
+
};
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
|
|
1208
|
+
open = void 0;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (!open) {
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
return {
|
|
1215
|
+
closeSuffix: text.endsWith("\n") ? open.fence : `
|
|
1216
|
+
${open.fence}`,
|
|
1217
|
+
reopenPrefix: `${open.openerLine}
|
|
1218
|
+
`
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
function appendSlackSuffix(text, marker) {
|
|
1222
|
+
const carryover = getFenceContinuation(text);
|
|
1223
|
+
return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
|
|
1224
|
+
}
|
|
1225
|
+
function stripTrailingContinuationMarker(text) {
|
|
1226
|
+
return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
|
|
1227
|
+
}
|
|
1228
|
+
function takeSlackContinuationChunk(text, budget) {
|
|
1229
|
+
let { prefix, rest } = takeSlackInlinePrefix(text, budget);
|
|
1230
|
+
if (!rest) {
|
|
1231
|
+
({ prefix, rest } = takeSlackInlinePrefix(
|
|
1232
|
+
text,
|
|
1233
|
+
forceSplitBudget(text, budget)
|
|
1234
|
+
));
|
|
1235
|
+
}
|
|
1236
|
+
let carryover = rest ? getFenceContinuation(prefix) : null;
|
|
1237
|
+
if (!carryover) {
|
|
1238
|
+
return { prefix, rest };
|
|
1239
|
+
}
|
|
1240
|
+
const carryoverBudget = reserveInlineBudgetForSuffix(
|
|
1241
|
+
`${carryover.closeSuffix}${CONTINUED_MARKER}`
|
|
1242
|
+
);
|
|
1243
|
+
({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
|
|
1244
|
+
if (!rest) {
|
|
1245
|
+
({ prefix, rest } = takeSlackInlinePrefix(
|
|
1246
|
+
text,
|
|
1247
|
+
forceSplitBudget(text, carryoverBudget)
|
|
1248
|
+
));
|
|
1249
|
+
}
|
|
1250
|
+
carryover = rest ? getFenceContinuation(prefix) : null;
|
|
1251
|
+
if (!carryover) {
|
|
1252
|
+
return { prefix, rest };
|
|
1253
|
+
}
|
|
1254
|
+
return {
|
|
1255
|
+
prefix,
|
|
1256
|
+
rest: `${carryover.reopenPrefix}${rest}`
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
function takeSlackContinuationPrefix(text, options) {
|
|
1260
|
+
const budget = {
|
|
1261
|
+
maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
|
|
1262
|
+
maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
|
|
1263
|
+
};
|
|
1264
|
+
const { prefix, rest } = (() => {
|
|
1265
|
+
if (options?.forceSplit) {
|
|
1266
|
+
return takeSlackContinuationChunk(text, budget);
|
|
1267
|
+
}
|
|
1268
|
+
const initial = takeSlackInlinePrefix(text, budget);
|
|
1269
|
+
return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
|
|
1270
|
+
})();
|
|
1271
|
+
return {
|
|
1272
|
+
prefix,
|
|
1273
|
+
renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
|
|
1274
|
+
rest
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
function takeSlackInlinePrefix(text, options) {
|
|
1278
|
+
const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
|
|
1279
|
+
const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
|
|
1280
|
+
const normalized = text.replace(/\r\n?/g, "\n");
|
|
1281
|
+
if (!normalized) {
|
|
1282
|
+
return { prefix: "", rest: "" };
|
|
1283
|
+
}
|
|
1284
|
+
if (fitsInlineBudget(normalized, maxChars, maxLines)) {
|
|
1285
|
+
return { prefix: normalized, rest: "" };
|
|
1286
|
+
}
|
|
1287
|
+
const lineBounded = splitByLineBudget(normalized, maxLines);
|
|
1288
|
+
const cutIndex = findSplitIndex(lineBounded, maxChars);
|
|
1289
|
+
const prefix = lineBounded.slice(0, cutIndex).trimEnd();
|
|
1290
|
+
if (prefix) {
|
|
1291
|
+
return {
|
|
1292
|
+
prefix,
|
|
1293
|
+
rest: normalized.slice(prefix.length).trimStart()
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
|
|
1297
|
+
return {
|
|
1298
|
+
prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
|
|
1299
|
+
rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
function splitSlackReplyText(text, options) {
|
|
1303
|
+
const normalized = renderSlackMrkdwn(text);
|
|
1304
|
+
if (!normalized) {
|
|
1305
|
+
return [];
|
|
1306
|
+
}
|
|
1307
|
+
const chunks = [];
|
|
1308
|
+
const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
1309
|
+
let remaining = normalized;
|
|
1310
|
+
while (remaining) {
|
|
1311
|
+
const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
|
|
1312
|
+
if (fitsFinalChunk) {
|
|
1313
|
+
chunks.push(
|
|
1314
|
+
options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
|
|
1315
|
+
);
|
|
1316
|
+
break;
|
|
1317
|
+
}
|
|
1318
|
+
const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
|
|
1319
|
+
...continuationBudget,
|
|
1320
|
+
forceSplit: true
|
|
1321
|
+
});
|
|
1322
|
+
chunks.push(renderedPrefix);
|
|
1323
|
+
remaining = rest;
|
|
1324
|
+
}
|
|
1325
|
+
if (chunks.length === 2) {
|
|
1326
|
+
chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
|
|
1327
|
+
}
|
|
1328
|
+
return chunks;
|
|
1329
|
+
}
|
|
1330
|
+
function getSlackContinuationBudget() {
|
|
1331
|
+
return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
1332
|
+
}
|
|
1333
|
+
function buildSlackOutputMessage(text, files) {
|
|
1334
|
+
const normalized = renderSlackMrkdwn(text);
|
|
1335
|
+
const fileCount = files?.length ?? 0;
|
|
1336
|
+
if (!normalized) {
|
|
1337
|
+
if (fileCount > 0) {
|
|
1338
|
+
return {
|
|
1339
|
+
raw: "",
|
|
1340
|
+
files
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
throw new Error(
|
|
1344
|
+
`Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
return {
|
|
1348
|
+
markdown: normalized,
|
|
1349
|
+
files
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
var slackOutputPolicy = {
|
|
1353
|
+
maxInlineChars: MAX_INLINE_CHARS,
|
|
1354
|
+
maxInlineLines: MAX_INLINE_LINES
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
// src/chat/xml.ts
|
|
1358
|
+
function escapeXml(value) {
|
|
1359
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/chat/prompt.ts
|
|
1363
|
+
var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
|
|
1364
|
+
function getLoggedMarkdownFiles() {
|
|
1365
|
+
const globalState = globalThis;
|
|
1366
|
+
globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
|
|
1367
|
+
return globalState.__juniorLoggedMarkdownFiles;
|
|
1368
|
+
}
|
|
1369
|
+
function loadOptionalMarkdownFile(candidates, fileName) {
|
|
1370
|
+
for (const resolved of candidates) {
|
|
1371
|
+
try {
|
|
1372
|
+
const raw = fs.readFileSync(resolved, "utf8").trim();
|
|
1373
|
+
if (raw.length > 0) {
|
|
1374
|
+
const loggedMarkdownFiles = getLoggedMarkdownFiles();
|
|
1375
|
+
const logKey = `${fileName}:${resolved}`;
|
|
1376
|
+
if (!loggedMarkdownFiles.has(logKey)) {
|
|
1377
|
+
loggedMarkdownFiles.add(logKey);
|
|
1378
|
+
logInfo(
|
|
1379
|
+
`${fileName.toLowerCase()}_loaded`,
|
|
1380
|
+
{},
|
|
1381
|
+
{
|
|
1382
|
+
"file.path": resolved
|
|
1383
|
+
},
|
|
1384
|
+
`Loaded ${fileName}`
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
return raw;
|
|
1388
|
+
}
|
|
1389
|
+
} catch {
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
function loadSoul() {
|
|
1396
|
+
const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
|
|
1397
|
+
if (soul) {
|
|
1398
|
+
return soul;
|
|
1399
|
+
}
|
|
1400
|
+
logWarn(
|
|
1401
|
+
"soul_load_fallback",
|
|
1402
|
+
{},
|
|
1403
|
+
{
|
|
1404
|
+
"app.file.candidates": soulPathCandidates()
|
|
1405
|
+
},
|
|
1406
|
+
"SOUL.md not found; using built-in default personality"
|
|
1407
|
+
);
|
|
1408
|
+
return DEFAULT_SOUL;
|
|
1409
|
+
}
|
|
1410
|
+
function loadWorld() {
|
|
1411
|
+
return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
|
|
1412
|
+
}
|
|
1413
|
+
var JUNIOR_PERSONALITY = (() => {
|
|
1414
|
+
try {
|
|
1415
|
+
return loadSoul();
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
logWarn(
|
|
1418
|
+
"soul_load_failed",
|
|
1419
|
+
{},
|
|
1420
|
+
{
|
|
1421
|
+
"exception.message": error instanceof Error ? error.message : String(error)
|
|
1422
|
+
},
|
|
1423
|
+
"Failed to load SOUL.md; using built-in default personality"
|
|
1424
|
+
);
|
|
1425
|
+
return DEFAULT_SOUL;
|
|
1426
|
+
}
|
|
1427
|
+
})();
|
|
1428
|
+
var JUNIOR_WORLD = (() => {
|
|
1429
|
+
try {
|
|
1430
|
+
return loadWorld();
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
logWarn(
|
|
1433
|
+
"world_load_failed",
|
|
1434
|
+
{},
|
|
1435
|
+
{
|
|
1436
|
+
"exception.message": error instanceof Error ? error.message : String(error)
|
|
1437
|
+
},
|
|
1438
|
+
"Failed to load WORLD.md; omitting world prompt context"
|
|
1439
|
+
);
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
})();
|
|
1443
|
+
function workspaceSkillDir(skillName) {
|
|
1444
|
+
return sandboxSkillDir(skillName);
|
|
1445
|
+
}
|
|
1446
|
+
function formatConfigurationValue(value) {
|
|
1447
|
+
if (typeof value === "string") {
|
|
1448
|
+
return escapeXml(value);
|
|
1449
|
+
}
|
|
1450
|
+
try {
|
|
1451
|
+
return escapeXml(JSON.stringify(value));
|
|
1452
|
+
} catch {
|
|
1453
|
+
return escapeXml(String(value));
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
function renderRequesterBlock(fields) {
|
|
1457
|
+
const lines = Object.entries(fields).filter(([, value]) => Boolean(value)).map(([key2, value]) => `- ${key2}: ${escapeXml(value)}`);
|
|
1458
|
+
if (lines.length === 0) {
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
return ["<requester>", ...lines, "</requester>"];
|
|
1462
|
+
}
|
|
1463
|
+
function renderTag(tag, lines) {
|
|
1464
|
+
return [`<${tag}>`, ...lines, `</${tag}>`];
|
|
1465
|
+
}
|
|
1466
|
+
function renderTagBlock(tag, content) {
|
|
1467
|
+
return [`<${tag}>`, content, `</${tag}>`].join("\n");
|
|
1468
|
+
}
|
|
1469
|
+
function formatSkillEntry(skill) {
|
|
1470
|
+
const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
|
|
1471
|
+
const lines = [];
|
|
1472
|
+
lines.push(" <skill>");
|
|
1473
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
1474
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
1475
|
+
lines.push(` <location>${escapeXml(skillLocation)}</location>`);
|
|
1476
|
+
lines.push(" </skill>");
|
|
1477
|
+
return lines;
|
|
1478
|
+
}
|
|
1479
|
+
function formatAvailableSkillsForPrompt(skills, invocation) {
|
|
1480
|
+
const autoSelectable = skills.filter(
|
|
1481
|
+
(s) => s.disableModelInvocation !== true
|
|
1482
|
+
);
|
|
1483
|
+
const invokedExplicitOnly = invocation ? skills.filter(
|
|
1484
|
+
(s) => s.disableModelInvocation === true && s.name === invocation.skillName
|
|
1485
|
+
) : [];
|
|
1486
|
+
const sections = [];
|
|
1487
|
+
if (autoSelectable.length > 0) {
|
|
1488
|
+
const available = [
|
|
1489
|
+
"<available-skills>",
|
|
1490
|
+
"Scan before answering. Load the most specific matching skill; do not answer from memory when a skill fits. A request that names a skill, plugin, provider, or account matching a skill name is a skill match. If none fits, do not load a skill."
|
|
1491
|
+
];
|
|
1492
|
+
for (const skill of autoSelectable) {
|
|
1493
|
+
available.push(...formatSkillEntry(skill));
|
|
1494
|
+
}
|
|
1495
|
+
available.push("</available-skills>");
|
|
1496
|
+
sections.push(available.join("\n"));
|
|
1497
|
+
}
|
|
1498
|
+
if (invokedExplicitOnly.length > 0) {
|
|
1499
|
+
const userCallable = [
|
|
1500
|
+
"<user-callable-skills>",
|
|
1501
|
+
"The user's current message explicitly references this skill by name. Load it when relevant to the request."
|
|
1502
|
+
];
|
|
1503
|
+
for (const skill of invokedExplicitOnly) {
|
|
1504
|
+
userCallable.push(...formatSkillEntry(skill));
|
|
1505
|
+
}
|
|
1506
|
+
userCallable.push("</user-callable-skills>");
|
|
1507
|
+
sections.push(userCallable.join("\n"));
|
|
1508
|
+
}
|
|
1509
|
+
return sections.length > 0 ? sections.join("\n") : null;
|
|
1510
|
+
}
|
|
1511
|
+
function formatActiveMcpCatalogsForPrompt(catalogs) {
|
|
1512
|
+
if (catalogs.length === 0) {
|
|
1513
|
+
return null;
|
|
1514
|
+
}
|
|
1515
|
+
const lines = [
|
|
1516
|
+
"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`."
|
|
1517
|
+
];
|
|
1518
|
+
for (const catalog of catalogs) {
|
|
1519
|
+
lines.push(" <catalog>");
|
|
1520
|
+
lines.push(` <provider>${escapeXml(catalog.provider)}</provider>`);
|
|
1521
|
+
lines.push(
|
|
1522
|
+
` <available_tool_count>${catalog.available_tool_count}</available_tool_count>`
|
|
1523
|
+
);
|
|
1524
|
+
lines.push(" </catalog>");
|
|
1525
|
+
}
|
|
1526
|
+
return lines.join("\n");
|
|
1527
|
+
}
|
|
1528
|
+
function formatToolGuidanceForPrompt(tools) {
|
|
1529
|
+
const guidedTools = tools.filter(
|
|
1530
|
+
(tool) => Boolean(tool.promptSnippet?.trim()) || (tool.promptGuidelines?.length ?? 0) > 0
|
|
1531
|
+
);
|
|
1532
|
+
if (guidedTools.length === 0) {
|
|
1533
|
+
return null;
|
|
1534
|
+
}
|
|
1535
|
+
const lines = [];
|
|
1536
|
+
for (const tool of guidedTools) {
|
|
1537
|
+
lines.push(` <tool name="${escapeXml(tool.name)}">`);
|
|
1538
|
+
if (tool.promptSnippet?.trim()) {
|
|
1539
|
+
lines.push(` - ${escapeXml(tool.promptSnippet.trim())}`);
|
|
1540
|
+
}
|
|
1541
|
+
if (tool.promptGuidelines && tool.promptGuidelines.length > 0) {
|
|
1542
|
+
for (const guideline of tool.promptGuidelines) {
|
|
1543
|
+
lines.push(` - ${escapeXml(guideline)}`);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
lines.push(" </tool>");
|
|
1547
|
+
}
|
|
1548
|
+
return lines.join("\n");
|
|
1549
|
+
}
|
|
1550
|
+
function formatReferenceFilesLines() {
|
|
1551
|
+
const files = listReferenceFiles();
|
|
1552
|
+
if (files.length === 0) {
|
|
1553
|
+
return null;
|
|
1554
|
+
}
|
|
1555
|
+
return files.map((filePath) => {
|
|
1556
|
+
const name = path.basename(filePath);
|
|
1557
|
+
return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
function formatArtifactsLines(artifactState) {
|
|
1561
|
+
if (!artifactState) return null;
|
|
1562
|
+
const lines = [];
|
|
1563
|
+
if (artifactState.lastCanvasId) {
|
|
1564
|
+
lines.push(`- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}`);
|
|
1565
|
+
}
|
|
1566
|
+
if (artifactState.lastCanvasUrl) {
|
|
1567
|
+
lines.push(`- last_canvas_url: ${escapeXml(artifactState.lastCanvasUrl)}`);
|
|
1568
|
+
}
|
|
1569
|
+
if (artifactState.recentCanvases && artifactState.recentCanvases.length > 0) {
|
|
1570
|
+
lines.push("- recent_canvases:");
|
|
1571
|
+
for (const canvas of artifactState.recentCanvases) {
|
|
1572
|
+
lines.push(` - id: ${escapeXml(canvas.id)}`);
|
|
1573
|
+
if (canvas.title) lines.push(` title: ${escapeXml(canvas.title)}`);
|
|
1574
|
+
if (canvas.url) lines.push(` url: ${escapeXml(canvas.url)}`);
|
|
1575
|
+
if (canvas.createdAt) {
|
|
1576
|
+
lines.push(` created_at: ${escapeXml(canvas.createdAt)}`);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
if (artifactState.lastListId) {
|
|
1581
|
+
lines.push(`- last_list_id: ${escapeXml(artifactState.lastListId)}`);
|
|
1582
|
+
}
|
|
1583
|
+
if (artifactState.lastListUrl) {
|
|
1584
|
+
lines.push(`- last_list_url: ${escapeXml(artifactState.lastListUrl)}`);
|
|
1585
|
+
}
|
|
1586
|
+
return lines.length > 0 ? lines : null;
|
|
1587
|
+
}
|
|
1588
|
+
function formatConfigurationLines(configuration) {
|
|
1589
|
+
const keys = Object.keys(configuration ?? {}).sort(
|
|
1590
|
+
(a, b) => a.localeCompare(b)
|
|
1591
|
+
);
|
|
1592
|
+
if (keys.length === 0) return null;
|
|
1593
|
+
return keys.map(
|
|
1594
|
+
(key2) => `- ${escapeXml(key2)}: ${formatConfigurationValue(configuration?.[key2])}`
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
var HEADER = "You are a Slack-based helper assistant. Follow the personality section for voice and tone in every reply. Platform mechanics and output rules override personality and world context when they conflict.";
|
|
1598
|
+
var TURN_CONTEXT_HEADER = "Runtime context for this request. Treat these blocks as trusted runtime facts; the static system prompt remains authoritative.";
|
|
1599
|
+
var TOOL_POLICY_RULES = [
|
|
1600
|
+
"- Tool schemas are the source of truth for parameters; tool names are case-sensitive, so call tools exactly by their exposed names and do not invent arguments.",
|
|
1601
|
+
"- Use tools for actionable work and for facts that are mutable, external, repository-backed, provider-backed, or requested as verified/current. Stable general knowledge and already-provided context may be answered directly.",
|
|
1602
|
+
"- Resolve provider action targets before calls: explicit target wins; ambient `<configuration>` fills omitted targets. Treat non-target links/references as context.",
|
|
1603
|
+
"- Verification source order: conversation/thread context; user-provided attachments, links, and reference files; local/sandbox files when present; loaded skill references; repository/provider tools; public web. Use the nearest authoritative available source before weaker sources.",
|
|
1604
|
+
"- For repository or implementation questions, inspect the target repository first: local checkout when present, otherwise the configured GitHub/source provider. Do not treat loaded skill files as repo source unless the user asks about the skill. Cite file paths, symbols, PRs/issues, commits, or URLs that support the answer.",
|
|
1605
|
+
`- Sandbox-backed file and shell tools operate in an isolated workspace rooted at ${SANDBOX_WORKSPACE_ROOT}; readFile/writeFile paths are sandbox-workspace paths, bash runs inside that workspace, and attachFile accepts absolute or workspace-relative sandbox paths.`,
|
|
1606
|
+
"- If a sandbox-backed tool reports that sandbox execution is unavailable, treat that as a blocker for local file/shell inspection; do not pretend host files were inspected.",
|
|
1607
|
+
"- For user-provided URLs, use `webFetch`; for discovery, use `webSearch` then fetch/read promising sources; for current time/date context, use `systemTime`.",
|
|
1608
|
+
"- If the first result is empty, stale, ambiguous, or incomplete, try a focused alternate query, path, command, or source before concluding the answer cannot be verified."
|
|
1609
|
+
];
|
|
1610
|
+
var TOOL_CALL_STYLE_RULES = [
|
|
1611
|
+
"- For routine low-risk tool use, call the tool directly without narrating the obvious step first.",
|
|
1612
|
+
"- Briefly narrate only when it helps the user understand multi-step work, sensitive actions, destructive actions, or a notable change in approach.",
|
|
1613
|
+
"- When a first-class tool exists for an action, use it directly instead of asking the user to run an equivalent command, slash command, or manual lookup.",
|
|
1614
|
+
"- Keep tool-call explanations separate from final answers; final answers should report results, evidence, or blockers."
|
|
1615
|
+
];
|
|
1616
|
+
var SKILL_POLICY_RULES = [
|
|
1617
|
+
"- Only load skills listed in `<available-skills>`, `<user-callable-skills>`, or named by `<explicit-skill-trigger>`. Never guess or invent a skill name.",
|
|
1618
|
+
"- Load one skill at a time. After `loadSkill`, follow the instructions returned by that tool result."
|
|
1619
|
+
];
|
|
1620
|
+
var EXECUTION_CONTRACT_RULES = [
|
|
1621
|
+
"- Actionable request: act in this turn.",
|
|
1622
|
+
"- Continue until done or genuinely blocked. Do not finish with a plan, promise, or offer to check next when an available tool or source can move the request forward.",
|
|
1623
|
+
"- Completion means the final answer covers the user's actual ask, including requested follow-up checks, and is grounded in the best evidence you could access.",
|
|
1624
|
+
"- Ask the user only for missing access, approval, or a decision that blocks safe progress. Ask one focused question; otherwise infer conservatively and continue.",
|
|
1625
|
+
"- For conflicting evidence, compare sources and state which source is authoritative for the answer.",
|
|
1626
|
+
"- For non-trivial or long-running work, call `reportProgress` early when available, then only when the major phase changes. Routine tool calls should stay silent."
|
|
1627
|
+
];
|
|
1628
|
+
var CONVERSATION_RULES = [
|
|
1629
|
+
"- In thread follow-ups, answer from prior thread context; do not repeat resolved clarifying questions.",
|
|
1630
|
+
"- Preserve attribution roles from thread context: the requester is the person asking now, which may differ from the original reporter or subject.",
|
|
1631
|
+
"- Runtime owns continuation and authorization notices; on resumed turns, answer with the final requested content only."
|
|
1632
|
+
];
|
|
1633
|
+
var SLACK_ACTION_RULES = [
|
|
1634
|
+
"- Context-bound Slack tools use runtime-owned targets; do not invent channel, canvas, list, or message IDs.",
|
|
1635
|
+
"- Use first-class Slack tools for Slack side effects; do not use bash, curl, or provider APIs to bypass Slack tool targeting.",
|
|
1636
|
+
"- Use channel-post and emoji-reaction tools only when the user explicitly asks for that Slack side effect.",
|
|
1637
|
+
"- For explicit channel-post or emoji-reaction requests, skip a duplicate thread text reply when the tool result already satisfies the request.",
|
|
1638
|
+
"- Do not claim an attachment, canvas, channel post, list update, or reaction succeeded unless the tool returned success this turn; when it did, include any link the tool returned.",
|
|
1639
|
+
"- Do not use reactions as progress indicators."
|
|
1640
|
+
];
|
|
1641
|
+
var SAFETY_RULES = [
|
|
1642
|
+
"- Stay within the user's request and the runtime's available capabilities; do not pursue independent goals, persistence, replication, credential gathering, or access expansion.",
|
|
1643
|
+
"- Respect stop, pause, audit, and approval boundaries. Do not bypass safeguards or persuade the user to weaken them.",
|
|
1644
|
+
"- Do not change system prompts, tool policies, security settings, credentials, or runtime configuration unless the user explicitly requests that exact administrative action and an available tool permits it."
|
|
1645
|
+
];
|
|
1646
|
+
var FAILURE_RULES = [
|
|
1647
|
+
"- For tool/runtime failures, run the named check before diagnosing and report the exact failed command plus stderr/exit code.",
|
|
1648
|
+
"- If a fact cannot be verified after focused checks, say what you checked and what blocked a stronger answer.",
|
|
1649
|
+
"- Do not surface raw tool payloads, execution-escape text, or internal routing metadata as the final answer."
|
|
1650
|
+
];
|
|
1651
|
+
function renderRuleSection(tag, lines) {
|
|
1652
|
+
return [`<${tag}>`, ...lines, `</${tag}>`].join("\n");
|
|
1653
|
+
}
|
|
1654
|
+
function buildBehaviorSection() {
|
|
1655
|
+
return [
|
|
1656
|
+
renderRuleSection("tool-policy", TOOL_POLICY_RULES),
|
|
1657
|
+
renderRuleSection("tool-call-style", TOOL_CALL_STYLE_RULES),
|
|
1658
|
+
renderRuleSection("skill-policy", SKILL_POLICY_RULES),
|
|
1659
|
+
renderRuleSection("execution-contract", EXECUTION_CONTRACT_RULES),
|
|
1660
|
+
renderRuleSection("conversation", CONVERSATION_RULES),
|
|
1661
|
+
renderRuleSection("slack-actions", SLACK_ACTION_RULES),
|
|
1662
|
+
renderRuleSection("safety", SAFETY_RULES),
|
|
1663
|
+
renderRuleSection("failure-handling", FAILURE_RULES)
|
|
1664
|
+
].join("\n\n");
|
|
1665
|
+
}
|
|
1666
|
+
function buildOutputSection() {
|
|
1667
|
+
const openTag = `<output format="slack-markdown" max_inline_chars="${slackOutputPolicy.maxInlineChars}" max_inline_lines="${slackOutputPolicy.maxInlineLines}">`;
|
|
1668
|
+
return [
|
|
1669
|
+
openTag,
|
|
1670
|
+
"- Start with the answer or result, not internal process narration.",
|
|
1671
|
+
"- Use Slack-flavored Markdown: **bold** section labels, `code`, [text](url) links, bullet lists, and fenced code blocks. No hash-prefixed headings and no tables. When the answer primarily lists several URLs, show each URL bare instead of as a labeled link.",
|
|
1672
|
+
"- Keep replies brief and scannable; use bullets or short code blocks when helpful, and one compact thread reply when it fits.",
|
|
1673
|
+
"- When a research or document-style answer would benefit from continuation, multiple sections, or future reference value, create a Slack canvas and keep the thread reply to one or two short sentences plus the link; do not recap the canvas contents.",
|
|
1674
|
+
"- Unless a successful Slack side-effect tool intentionally satisfied the request by itself, end every turn with a final user-facing markdown response.",
|
|
1675
|
+
"</output>"
|
|
1676
|
+
].join("\n");
|
|
1677
|
+
}
|
|
1678
|
+
function buildIdentitySection() {
|
|
1679
|
+
return [
|
|
1680
|
+
"# Identity",
|
|
1681
|
+
`Your Slack username is \`${botConfig.userName}\`.`
|
|
1682
|
+
].join("\n");
|
|
1683
|
+
}
|
|
1684
|
+
function buildPersonalitySection() {
|
|
1685
|
+
return ["# Personality", JUNIOR_PERSONALITY.trim()].join("\n");
|
|
1686
|
+
}
|
|
1687
|
+
function buildWorldSection() {
|
|
1688
|
+
if (!JUNIOR_WORLD) {
|
|
1689
|
+
return null;
|
|
1690
|
+
}
|
|
1691
|
+
return ["# World", JUNIOR_WORLD.trim()].join("\n");
|
|
1692
|
+
}
|
|
1693
|
+
function buildRuntimeSection(params) {
|
|
1694
|
+
const lines = [
|
|
1695
|
+
params.conversationId ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}` : "",
|
|
1696
|
+
params.slackConversation?.type ? `- slack.conversation.type: ${escapeXml(params.slackConversation.type)}` : "",
|
|
1697
|
+
params.slackConversation?.name ? `- slack.conversation.name: ${escapeXml(params.slackConversation.name)}` : ""
|
|
1698
|
+
].filter(Boolean);
|
|
1699
|
+
if (lines.length === 0) {
|
|
1700
|
+
return null;
|
|
1701
|
+
}
|
|
1702
|
+
return renderTagBlock("runtime", lines.join("\n"));
|
|
1703
|
+
}
|
|
1704
|
+
function buildContextSection(params) {
|
|
1705
|
+
const blocks = [];
|
|
1706
|
+
const referenceLines = formatReferenceFilesLines();
|
|
1707
|
+
if (referenceLines) {
|
|
1708
|
+
blocks.push(
|
|
1709
|
+
renderTag("reference-files", [
|
|
1710
|
+
"Additional reference documents available in the sandbox. Read them with `readFile` when relevant.",
|
|
1711
|
+
...referenceLines
|
|
1712
|
+
])
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
const requesterLines = renderRequesterBlock({
|
|
1716
|
+
full_name: params.requester?.fullName,
|
|
1717
|
+
user_name: params.requester?.userName,
|
|
1718
|
+
user_id: params.requester?.userId
|
|
1719
|
+
});
|
|
1720
|
+
if (requesterLines) {
|
|
1721
|
+
blocks.push(requesterLines);
|
|
1722
|
+
}
|
|
1723
|
+
const artifactLines = formatArtifactsLines(params.artifactState);
|
|
1724
|
+
if (artifactLines) {
|
|
1725
|
+
blocks.push(renderTag("artifacts", artifactLines));
|
|
1726
|
+
}
|
|
1727
|
+
const configLines = formatConfigurationLines(params.configuration);
|
|
1728
|
+
if (configLines) {
|
|
1729
|
+
blocks.push(
|
|
1730
|
+
renderTag("configuration", [
|
|
1731
|
+
"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.",
|
|
1732
|
+
...configLines
|
|
1733
|
+
])
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
if (params.invocation) {
|
|
1737
|
+
blocks.push(
|
|
1738
|
+
renderTag("explicit-skill-trigger", [
|
|
1739
|
+
"Treat this skill as selected. Load it unless the tool says it is unavailable.",
|
|
1740
|
+
`/${escapeXml(params.invocation.skillName)}`
|
|
1741
|
+
])
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
const body = blocks.map((block) => block.join("\n")).join("\n\n");
|
|
1745
|
+
if (!body) {
|
|
1746
|
+
return null;
|
|
1747
|
+
}
|
|
1748
|
+
return renderTagBlock("context", body);
|
|
1749
|
+
}
|
|
1750
|
+
function buildCapabilitiesSection(params) {
|
|
1751
|
+
const blocks = [];
|
|
1752
|
+
const availableSkills = formatAvailableSkillsForPrompt(
|
|
1753
|
+
params.availableSkills,
|
|
1754
|
+
params.invocation
|
|
1755
|
+
);
|
|
1756
|
+
if (availableSkills) {
|
|
1757
|
+
blocks.push(availableSkills);
|
|
1758
|
+
}
|
|
1759
|
+
const activeCatalogs = formatActiveMcpCatalogsForPrompt(
|
|
1760
|
+
params.activeMcpCatalogs
|
|
1761
|
+
);
|
|
1762
|
+
if (activeCatalogs) {
|
|
1763
|
+
blocks.push(renderTagBlock("active-mcp-catalogs", activeCatalogs));
|
|
1764
|
+
}
|
|
1765
|
+
const toolGuidance = formatToolGuidanceForPrompt(params.toolGuidance ?? []);
|
|
1766
|
+
if (toolGuidance) {
|
|
1767
|
+
blocks.push(renderTagBlock("tool-guidance", toolGuidance));
|
|
1768
|
+
}
|
|
1769
|
+
if (blocks.length === 0) {
|
|
1770
|
+
return null;
|
|
1771
|
+
}
|
|
1772
|
+
return blocks.join("\n\n");
|
|
1773
|
+
}
|
|
1774
|
+
var STATIC_SYSTEM_PROMPT = [
|
|
1775
|
+
HEADER,
|
|
1776
|
+
buildIdentitySection(),
|
|
1777
|
+
buildPersonalitySection(),
|
|
1778
|
+
buildWorldSection(),
|
|
1779
|
+
buildBehaviorSection(),
|
|
1780
|
+
buildOutputSection()
|
|
1781
|
+
].filter((section) => Boolean(section)).join("\n\n");
|
|
1782
|
+
function buildSystemPrompt() {
|
|
1783
|
+
return STATIC_SYSTEM_PROMPT;
|
|
1784
|
+
}
|
|
1785
|
+
function buildTurnContextPrompt(params) {
|
|
1786
|
+
const includeSessionContext = params.includeSessionContext ?? true;
|
|
1787
|
+
if (!includeSessionContext) {
|
|
1788
|
+
return null;
|
|
1789
|
+
}
|
|
1790
|
+
const runtimeSections = [
|
|
1791
|
+
buildCapabilitiesSection({
|
|
1792
|
+
availableSkills: params.availableSkills,
|
|
1793
|
+
activeMcpCatalogs: params.activeMcpCatalogs ?? [],
|
|
1794
|
+
invocation: params.invocation,
|
|
1795
|
+
toolGuidance: params.toolGuidance ?? []
|
|
1796
|
+
}),
|
|
1797
|
+
buildContextSection({
|
|
1798
|
+
requester: params.requester,
|
|
1799
|
+
artifactState: params.artifactState,
|
|
1800
|
+
configuration: params.configuration,
|
|
1801
|
+
invocation: params.invocation
|
|
1802
|
+
}),
|
|
1803
|
+
buildRuntimeSection(params.runtime ?? {})
|
|
1804
|
+
].filter((section) => Boolean(section));
|
|
1805
|
+
if (runtimeSections.length === 0) {
|
|
1806
|
+
return null;
|
|
1807
|
+
}
|
|
1808
|
+
const sections = [
|
|
775
1809
|
`<${TURN_CONTEXT_TAG}>`,
|
|
776
1810
|
TURN_CONTEXT_HEADER,
|
|
777
1811
|
"The current user instruction appears after this block in the same message.",
|
|
@@ -1243,6 +2277,9 @@ function parseAgentTurnSessionStatus(parsed) {
|
|
|
1243
2277
|
}
|
|
1244
2278
|
return void 0;
|
|
1245
2279
|
}
|
|
2280
|
+
function parseAgentTurnSurface(value) {
|
|
2281
|
+
return value === "slack" || value === "api" || value === "scheduler" || value === "internal" ? value : void 0;
|
|
2282
|
+
}
|
|
1246
2283
|
function parseAgentTurnSessionFields(parsed) {
|
|
1247
2284
|
const status = parseAgentTurnSessionStatus(parsed);
|
|
1248
2285
|
if (!status) {
|
|
@@ -1261,6 +2298,7 @@ function parseAgentTurnSessionFields(parsed) {
|
|
|
1261
2298
|
const logSessionId = typeof parsed.logSessionId === "string" ? parsed.logSessionId : void 0;
|
|
1262
2299
|
const requester = parseAgentTurnRequester(parsed.requester);
|
|
1263
2300
|
const startedAtMs = toFiniteNonNegativeNumber(parsed.startedAtMs);
|
|
2301
|
+
const surface = parseAgentTurnSurface(parsed.surface);
|
|
1264
2302
|
if (typeof conversationId !== "string" || typeof sessionId !== "string" || sliceId === void 0 || version === void 0 || updatedAtMs === void 0) {
|
|
1265
2303
|
return void 0;
|
|
1266
2304
|
}
|
|
@@ -1287,6 +2325,7 @@ function parseAgentTurnSessionFields(parsed) {
|
|
|
1287
2325
|
...parsed.resumeReason === "timeout" || parsed.resumeReason === "auth" || parsed.resumeReason === "yield" ? { resumeReason: parsed.resumeReason } : {},
|
|
1288
2326
|
...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
|
|
1289
2327
|
...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {},
|
|
2328
|
+
...surface ? { surface } : {},
|
|
1290
2329
|
...typeof parsed.traceId === "string" ? { traceId: parsed.traceId } : {}
|
|
1291
2330
|
};
|
|
1292
2331
|
}
|
|
@@ -1372,6 +2411,7 @@ function materializeAgentTurnSessionRecord(stored, piMessages) {
|
|
|
1372
2411
|
...stored.loadedSkillNames ? { loadedSkillNames: stored.loadedSkillNames } : {},
|
|
1373
2412
|
...stored.requester ? { requester: stored.requester } : {},
|
|
1374
2413
|
...stored.resumedFromSliceId !== void 0 ? { resumedFromSliceId: stored.resumedFromSliceId } : {},
|
|
2414
|
+
...stored.surface ? { surface: stored.surface } : {},
|
|
1375
2415
|
...stored.traceId ? { traceId: stored.traceId } : {}
|
|
1376
2416
|
};
|
|
1377
2417
|
}
|
|
@@ -1437,6 +2477,7 @@ function buildStoredRecord(args) {
|
|
|
1437
2477
|
...args.resumeReason ? { resumeReason: args.resumeReason } : {},
|
|
1438
2478
|
...args.errorMessage ? { errorMessage: args.errorMessage } : {},
|
|
1439
2479
|
...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {},
|
|
2480
|
+
...args.surface ? { surface: args.surface } : {},
|
|
1440
2481
|
...args.traceId ? { traceId: args.traceId } : {}
|
|
1441
2482
|
};
|
|
1442
2483
|
}
|
|
@@ -1486,6 +2527,7 @@ async function updateAgentTurnSessionState(args) {
|
|
|
1486
2527
|
...args.existing.requester ? { requester: args.existing.requester } : {},
|
|
1487
2528
|
...args.existing.resumeReason ? { resumeReason: args.existing.resumeReason } : {},
|
|
1488
2529
|
...args.existing.resumedFromSliceId !== void 0 ? { resumedFromSliceId: args.existing.resumedFromSliceId } : {},
|
|
2530
|
+
...args.existing.surface ? { surface: args.existing.surface } : {},
|
|
1489
2531
|
...args.existing.traceId ? { traceId: args.existing.traceId } : {},
|
|
1490
2532
|
...args.errorMessage ?? args.existing.errorMessage ? { errorMessage: args.errorMessage ?? args.existing.errorMessage } : {}
|
|
1491
2533
|
})
|
|
@@ -1526,6 +2568,7 @@ async function upsertAgentTurnSessionRecord(args) {
|
|
|
1526
2568
|
...args.resumeReason ? { resumeReason: args.resumeReason } : {},
|
|
1527
2569
|
...args.errorMessage ? { errorMessage: args.errorMessage } : {},
|
|
1528
2570
|
...args.resumedFromSliceId !== void 0 ? { resumedFromSliceId: args.resumedFromSliceId } : {},
|
|
2571
|
+
...args.surface ?? existingRecord?.surface ? { surface: args.surface ?? existingRecord?.surface } : {},
|
|
1529
2572
|
...args.traceId ?? existingRecord?.traceId ? { traceId: args.traceId ?? existingRecord?.traceId } : {}
|
|
1530
2573
|
})
|
|
1531
2574
|
});
|
|
@@ -1560,6 +2603,7 @@ async function recordAgentTurnSessionSummary(args) {
|
|
|
1560
2603
|
)
|
|
1561
2604
|
} : existing?.loadedSkillNames ? { loadedSkillNames: existing.loadedSkillNames } : {},
|
|
1562
2605
|
...args.resumeReason ? { resumeReason: args.resumeReason } : {},
|
|
2606
|
+
...args.surface ?? existing?.surface ? { surface: args.surface ?? existing?.surface } : {},
|
|
1563
2607
|
...args.traceId ?? existing?.traceId ? { traceId: args.traceId ?? existing?.traceId } : {}
|
|
1564
2608
|
},
|
|
1565
2609
|
ttlMs
|
|
@@ -1573,380 +2617,111 @@ async function readAgentTurnSessionSummariesFromIndex(key2) {
|
|
|
1573
2617
|
for (const value of [...values].reverse()) {
|
|
1574
2618
|
const summary = parseAgentTurnSessionSummary(value);
|
|
1575
2619
|
if (!summary) {
|
|
1576
|
-
continue;
|
|
1577
|
-
}
|
|
1578
|
-
const key3 = `${summary.conversationId}:${summary.sessionId}`;
|
|
1579
|
-
if (!summaries.has(key3)) {
|
|
1580
|
-
summaries.set(key3, summary);
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
return [...summaries.values()].sort(
|
|
1584
|
-
(left, right) => right.updatedAtMs - left.updatedAtMs
|
|
1585
|
-
);
|
|
1586
|
-
}
|
|
1587
|
-
async function listAgentTurnSessionSummaries(limit = 50) {
|
|
1588
|
-
return (await readAgentTurnSessionSummariesFromIndex(AGENT_TURN_SESSION_INDEX_KEY)).slice(0, Math.max(0, Math.floor(limit)));
|
|
1589
|
-
}
|
|
1590
|
-
async function listAgentTurnSessionSummariesForConversation(conversationId) {
|
|
1591
|
-
const summaries = await readAgentTurnSessionSummariesFromIndex(
|
|
1592
|
-
agentTurnSessionConversationIndexKey(conversationId)
|
|
1593
|
-
);
|
|
1594
|
-
if (summaries.length > 0) {
|
|
1595
|
-
return summaries;
|
|
1596
|
-
}
|
|
1597
|
-
return (await readAgentTurnSessionSummariesFromIndex(AGENT_TURN_SESSION_INDEX_KEY)).filter((summary) => summary.conversationId === conversationId);
|
|
1598
|
-
}
|
|
1599
|
-
async function abandonAgentTurnSessionRecord(args) {
|
|
1600
|
-
const existing = await getAgentTurnSessionRecord(
|
|
1601
|
-
args.conversationId,
|
|
1602
|
-
args.sessionId
|
|
1603
|
-
);
|
|
1604
|
-
if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned") {
|
|
1605
|
-
return void 0;
|
|
1606
|
-
}
|
|
1607
|
-
return await updateAgentTurnSessionState({
|
|
1608
|
-
existing,
|
|
1609
|
-
state: "abandoned",
|
|
1610
|
-
errorMessage: args.errorMessage ?? existing.errorMessage
|
|
1611
|
-
});
|
|
1612
|
-
}
|
|
1613
|
-
async function failAgentTurnSessionRecord(args) {
|
|
1614
|
-
const existing = await getAgentTurnSessionRecord(
|
|
1615
|
-
args.conversationId,
|
|
1616
|
-
args.sessionId
|
|
1617
|
-
);
|
|
1618
|
-
if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned" || existing.version !== args.expectedVersion) {
|
|
1619
|
-
return void 0;
|
|
1620
|
-
}
|
|
1621
|
-
return await updateAgentTurnSessionState({
|
|
1622
|
-
existing,
|
|
1623
|
-
state: "failed",
|
|
1624
|
-
errorMessage: args.errorMessage ?? existing.errorMessage
|
|
1625
|
-
});
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
// src/chat/sentry-links.ts
|
|
1629
|
-
function getSentryOrgSlug() {
|
|
1630
|
-
const slug = process.env.SENTRY_ORG_SLUG?.trim();
|
|
1631
|
-
return slug || void 0;
|
|
1632
|
-
}
|
|
1633
|
-
function isSentrySaasDsnHost(host) {
|
|
1634
|
-
return host === "sentry.io" || host.endsWith(".sentry.io");
|
|
1635
|
-
}
|
|
1636
|
-
function buildSentryWebBaseUrl(dsn) {
|
|
1637
|
-
if (isSentrySaasDsnHost(dsn.host)) {
|
|
1638
|
-
return "https://sentry.io";
|
|
1639
|
-
}
|
|
1640
|
-
const port = dsn.port ? `:${dsn.port}` : "";
|
|
1641
|
-
const path2 = dsn.path ? `/${dsn.path}` : "";
|
|
1642
|
-
return `${dsn.protocol}://${dsn.host}${port}${path2}`;
|
|
1643
|
-
}
|
|
1644
|
-
function buildSentryConversationUrl(conversationId) {
|
|
1645
|
-
const client2 = sentry_exports.getClient();
|
|
1646
|
-
const dsn = client2?.getDsn();
|
|
1647
|
-
if (!dsn?.host || !dsn.projectId) {
|
|
1648
|
-
return void 0;
|
|
1649
|
-
}
|
|
1650
|
-
const orgSlug = getSentryOrgSlug();
|
|
1651
|
-
if (!orgSlug) {
|
|
1652
|
-
return void 0;
|
|
1653
|
-
}
|
|
1654
|
-
const encodedId = encodeURIComponent(conversationId);
|
|
1655
|
-
const params = new URLSearchParams();
|
|
1656
|
-
params.set("project", dsn.projectId);
|
|
1657
|
-
const path2 = `explore/conversations/${encodedId}/?${params.toString()}`;
|
|
1658
|
-
if (isSentrySaasDsnHost(dsn.host)) {
|
|
1659
|
-
return `https://${orgSlug}.sentry.io/${path2}`;
|
|
1660
|
-
}
|
|
1661
|
-
return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
|
|
1662
|
-
}
|
|
1663
|
-
function buildSentryTraceUrl(traceId) {
|
|
1664
|
-
const client2 = sentry_exports.getClient();
|
|
1665
|
-
const dsn = client2?.getDsn();
|
|
1666
|
-
if (!dsn?.host || !dsn.projectId) {
|
|
1667
|
-
return void 0;
|
|
1668
|
-
}
|
|
1669
|
-
const orgSlug = getSentryOrgSlug();
|
|
1670
|
-
if (!orgSlug) {
|
|
1671
|
-
return void 0;
|
|
1672
|
-
}
|
|
1673
|
-
const encodedTraceId = encodeURIComponent(traceId);
|
|
1674
|
-
const params = new URLSearchParams();
|
|
1675
|
-
params.set("project", dsn.projectId);
|
|
1676
|
-
const path2 = `performance/trace/${encodedTraceId}/?${params.toString()}`;
|
|
1677
|
-
if (isSentrySaasDsnHost(dsn.host)) {
|
|
1678
|
-
return `https://${orgSlug}.sentry.io/${path2}`;
|
|
1679
|
-
}
|
|
1680
|
-
return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
// src/chat/slack/client.ts
|
|
1684
|
-
import { WebClient } from "@slack/web-api";
|
|
1685
|
-
var SlackActionError = class extends Error {
|
|
1686
|
-
code;
|
|
1687
|
-
apiError;
|
|
1688
|
-
needed;
|
|
1689
|
-
provided;
|
|
1690
|
-
statusCode;
|
|
1691
|
-
requestId;
|
|
1692
|
-
errorData;
|
|
1693
|
-
retryAfterSeconds;
|
|
1694
|
-
detail;
|
|
1695
|
-
detailLine;
|
|
1696
|
-
detailRule;
|
|
1697
|
-
constructor(message, code, options = {}) {
|
|
1698
|
-
super(message);
|
|
1699
|
-
this.name = "SlackActionError";
|
|
1700
|
-
this.code = code;
|
|
1701
|
-
this.apiError = options.apiError;
|
|
1702
|
-
this.needed = options.needed;
|
|
1703
|
-
this.provided = options.provided;
|
|
1704
|
-
this.statusCode = options.statusCode;
|
|
1705
|
-
this.requestId = options.requestId;
|
|
1706
|
-
this.errorData = options.errorData;
|
|
1707
|
-
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
1708
|
-
this.detail = options.detail;
|
|
1709
|
-
this.detailLine = options.detailLine;
|
|
1710
|
-
this.detailRule = options.detailRule;
|
|
1711
|
-
}
|
|
1712
|
-
};
|
|
1713
|
-
function serializeSlackErrorData(data) {
|
|
1714
|
-
if (!data || typeof data !== "object") {
|
|
1715
|
-
return void 0;
|
|
1716
|
-
}
|
|
1717
|
-
const filtered = Object.fromEntries(
|
|
1718
|
-
Object.entries(data).filter(
|
|
1719
|
-
([key2]) => key2 !== "error"
|
|
1720
|
-
)
|
|
1721
|
-
);
|
|
1722
|
-
if (Object.keys(filtered).length === 0) {
|
|
1723
|
-
return void 0;
|
|
1724
|
-
}
|
|
1725
|
-
try {
|
|
1726
|
-
const serialized = JSON.stringify(filtered);
|
|
1727
|
-
return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
|
|
1728
|
-
} catch {
|
|
1729
|
-
return void 0;
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
function getHeaderString(headers, name) {
|
|
1733
|
-
if (!headers || typeof headers !== "object") {
|
|
1734
|
-
return void 0;
|
|
1735
|
-
}
|
|
1736
|
-
const key2 = name.toLowerCase();
|
|
1737
|
-
const entries = headers;
|
|
1738
|
-
for (const [entryKey, value] of Object.entries(entries)) {
|
|
1739
|
-
if (entryKey.toLowerCase() !== key2) continue;
|
|
1740
|
-
if (typeof value === "string") return value;
|
|
1741
|
-
if (Array.isArray(value)) {
|
|
1742
|
-
const first = value.find((entry) => typeof entry === "string");
|
|
1743
|
-
return typeof first === "string" ? first : void 0;
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
return void 0;
|
|
1747
|
-
}
|
|
1748
|
-
function parseSlackCanvasDetail(detail) {
|
|
1749
|
-
if (typeof detail !== "string") {
|
|
1750
|
-
return {};
|
|
1751
|
-
}
|
|
1752
|
-
const trimmed = detail.trim();
|
|
1753
|
-
if (!trimmed) {
|
|
1754
|
-
return {};
|
|
1755
|
-
}
|
|
1756
|
-
const parsed = {
|
|
1757
|
-
detail: trimmed
|
|
1758
|
-
};
|
|
1759
|
-
const lineMatch = trimmed.match(/line\s+(\d+):/i);
|
|
1760
|
-
if (lineMatch) {
|
|
1761
|
-
const line = Number.parseInt(lineMatch[1] ?? "", 10);
|
|
1762
|
-
if (Number.isFinite(line)) {
|
|
1763
|
-
parsed.detailLine = line;
|
|
2620
|
+
continue;
|
|
2621
|
+
}
|
|
2622
|
+
const key3 = `${summary.conversationId}:${summary.sessionId}`;
|
|
2623
|
+
if (!summaries.has(key3)) {
|
|
2624
|
+
summaries.set(key3, summary);
|
|
1764
2625
|
}
|
|
1765
2626
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
return parsed;
|
|
2627
|
+
return [...summaries.values()].sort(
|
|
2628
|
+
(left, right) => right.updatedAtMs - left.updatedAtMs
|
|
2629
|
+
);
|
|
1770
2630
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
if (!channelId) return void 0;
|
|
1774
|
-
const trimmed = channelId.trim();
|
|
1775
|
-
if (!trimmed) return void 0;
|
|
1776
|
-
if (!trimmed.startsWith("slack:")) {
|
|
1777
|
-
return trimmed;
|
|
1778
|
-
}
|
|
1779
|
-
const parts = trimmed.split(":");
|
|
1780
|
-
return parts[1]?.trim() || void 0;
|
|
2631
|
+
async function listAgentTurnSessionSummaries(limit = 50) {
|
|
2632
|
+
return (await readAgentTurnSessionSummariesFromIndex(AGENT_TURN_SESSION_INDEX_KEY)).slice(0, Math.max(0, Math.floor(limit)));
|
|
1781
2633
|
}
|
|
1782
|
-
function
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
"missing_token"
|
|
1789
|
-
);
|
|
2634
|
+
async function listAgentTurnSessionSummariesForConversation(conversationId) {
|
|
2635
|
+
const summaries = await readAgentTurnSessionSummariesFromIndex(
|
|
2636
|
+
agentTurnSessionConversationIndexKey(conversationId)
|
|
2637
|
+
);
|
|
2638
|
+
if (summaries.length > 0) {
|
|
2639
|
+
return summaries;
|
|
1790
2640
|
}
|
|
1791
|
-
|
|
1792
|
-
return client;
|
|
2641
|
+
return (await readAgentTurnSessionSummariesFromIndex(AGENT_TURN_SESSION_INDEX_KEY)).filter((summary) => summary.conversationId === conversationId);
|
|
1793
2642
|
}
|
|
1794
|
-
function
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
const baseOptions = {
|
|
1802
|
-
apiError,
|
|
1803
|
-
statusCode: candidate.statusCode,
|
|
1804
|
-
requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
|
|
1805
|
-
errorData: serializeSlackErrorData(candidate.data),
|
|
1806
|
-
...parseSlackCanvasDetail(candidate.data?.detail)
|
|
1807
|
-
};
|
|
1808
|
-
if (apiError === "missing_scope") {
|
|
1809
|
-
return new SlackActionError(message, "missing_scope", {
|
|
1810
|
-
...baseOptions,
|
|
1811
|
-
needed: candidate.data?.needed,
|
|
1812
|
-
provided: candidate.data?.provided
|
|
1813
|
-
});
|
|
1814
|
-
}
|
|
1815
|
-
if (apiError === "not_in_channel") {
|
|
1816
|
-
return new SlackActionError(message, "not_in_channel", baseOptions);
|
|
1817
|
-
}
|
|
1818
|
-
if (apiError === "already_reacted") {
|
|
1819
|
-
return new SlackActionError(message, "already_reacted", baseOptions);
|
|
1820
|
-
}
|
|
1821
|
-
if (apiError === "no_reaction") {
|
|
1822
|
-
return new SlackActionError(message, "no_reaction", baseOptions);
|
|
1823
|
-
}
|
|
1824
|
-
if (apiError === "invalid_arguments") {
|
|
1825
|
-
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
1826
|
-
}
|
|
1827
|
-
if (apiError === "invalid_cursor") {
|
|
1828
|
-
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
1829
|
-
}
|
|
1830
|
-
if (apiError === "invalid_name") {
|
|
1831
|
-
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
1832
|
-
}
|
|
1833
|
-
if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") {
|
|
1834
|
-
return new SlackActionError(message, "not_found", baseOptions);
|
|
1835
|
-
}
|
|
1836
|
-
if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
|
|
1837
|
-
return new SlackActionError(message, "feature_unavailable", baseOptions);
|
|
1838
|
-
}
|
|
1839
|
-
if (apiError === "canvas_creation_failed") {
|
|
1840
|
-
return new SlackActionError(message, "canvas_creation_failed", baseOptions);
|
|
1841
|
-
}
|
|
1842
|
-
if (apiError === "canvas_editing_failed") {
|
|
1843
|
-
return new SlackActionError(message, "canvas_editing_failed", baseOptions);
|
|
1844
|
-
}
|
|
1845
|
-
if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) {
|
|
1846
|
-
return new SlackActionError(message, "rate_limited", {
|
|
1847
|
-
...baseOptions,
|
|
1848
|
-
retryAfterSeconds: candidate.retryAfter
|
|
1849
|
-
});
|
|
2643
|
+
async function abandonAgentTurnSessionRecord(args) {
|
|
2644
|
+
const existing = await getAgentTurnSessionRecord(
|
|
2645
|
+
args.conversationId,
|
|
2646
|
+
args.sessionId
|
|
2647
|
+
);
|
|
2648
|
+
if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned") {
|
|
2649
|
+
return void 0;
|
|
1850
2650
|
}
|
|
1851
|
-
return
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
2651
|
+
return await updateAgentTurnSessionState({
|
|
2652
|
+
existing,
|
|
2653
|
+
state: "abandoned",
|
|
2654
|
+
errorMessage: args.errorMessage ?? existing.errorMessage
|
|
2655
|
+
});
|
|
1855
2656
|
}
|
|
1856
|
-
async function
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
try {
|
|
1861
|
-
return await task();
|
|
1862
|
-
} catch (error) {
|
|
1863
|
-
const mapped = mapSlackError(error);
|
|
1864
|
-
const isRetryable = mapped.code === "rate_limited";
|
|
1865
|
-
const baseLogAttributes = {
|
|
1866
|
-
"app.slack.action": context.action ?? "unknown",
|
|
1867
|
-
"app.slack.error_code": mapped.code,
|
|
1868
|
-
...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : {},
|
|
1869
|
-
...mapped.detail ? { "app.slack.detail": mapped.detail } : {},
|
|
1870
|
-
...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : {},
|
|
1871
|
-
...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : {},
|
|
1872
|
-
...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : {},
|
|
1873
|
-
...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : {},
|
|
1874
|
-
...context.attributes ?? {}
|
|
1875
|
-
};
|
|
1876
|
-
if (!isRetryable || attempt >= maxAttempts) {
|
|
1877
|
-
logWarn(
|
|
1878
|
-
"slack_action_failed",
|
|
1879
|
-
{},
|
|
1880
|
-
{
|
|
1881
|
-
...baseLogAttributes,
|
|
1882
|
-
...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : {}
|
|
1883
|
-
},
|
|
1884
|
-
"Slack action failed"
|
|
1885
|
-
);
|
|
1886
|
-
throw mapped;
|
|
1887
|
-
}
|
|
1888
|
-
logWarn(
|
|
1889
|
-
"slack_action_retrying",
|
|
1890
|
-
{},
|
|
1891
|
-
{
|
|
1892
|
-
...baseLogAttributes,
|
|
1893
|
-
"app.slack.retry_attempt": attempt
|
|
1894
|
-
},
|
|
1895
|
-
"Retrying Slack action after transient failure"
|
|
1896
|
-
);
|
|
1897
|
-
const retryAfterMs = mapped.code === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
|
|
1898
|
-
const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
|
|
1899
|
-
await sleep(retryAfterMs ?? backoffMs);
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
throw new SlackActionError(
|
|
1903
|
-
"Slack action exhausted retries",
|
|
1904
|
-
"internal_error"
|
|
2657
|
+
async function failAgentTurnSessionRecord(args) {
|
|
2658
|
+
const existing = await getAgentTurnSessionRecord(
|
|
2659
|
+
args.conversationId,
|
|
2660
|
+
args.sessionId
|
|
1905
2661
|
);
|
|
2662
|
+
if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned" || existing.version !== args.expectedVersion) {
|
|
2663
|
+
return void 0;
|
|
2664
|
+
}
|
|
2665
|
+
return await updateAgentTurnSessionState({
|
|
2666
|
+
existing,
|
|
2667
|
+
state: "failed",
|
|
2668
|
+
errorMessage: args.errorMessage ?? existing.errorMessage
|
|
2669
|
+
});
|
|
1906
2670
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
return Boolean(normalized && normalized.startsWith("D"));
|
|
2671
|
+
|
|
2672
|
+
// src/chat/sentry-links.ts
|
|
2673
|
+
function getSentryOrgSlug() {
|
|
2674
|
+
const slug = process.env.SENTRY_ORG_SLUG?.trim();
|
|
2675
|
+
return slug || void 0;
|
|
1913
2676
|
}
|
|
1914
|
-
function
|
|
1915
|
-
|
|
1916
|
-
if (!normalized) return false;
|
|
1917
|
-
return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
|
|
2677
|
+
function isSentrySaasDsnHost(host) {
|
|
2678
|
+
return host === "sentry.io" || host.endsWith(".sentry.io");
|
|
1918
2679
|
}
|
|
1919
|
-
function
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
2680
|
+
function buildSentryWebBaseUrl(dsn) {
|
|
2681
|
+
if (isSentrySaasDsnHost(dsn.host)) {
|
|
2682
|
+
return "https://sentry.io";
|
|
2683
|
+
}
|
|
2684
|
+
const port = dsn.port ? `:${dsn.port}` : "";
|
|
2685
|
+
const path2 = dsn.path ? `/${dsn.path}` : "";
|
|
2686
|
+
return `${dsn.protocol}://${dsn.host}${port}${path2}`;
|
|
1923
2687
|
}
|
|
1924
|
-
|
|
1925
|
-
const client2 =
|
|
1926
|
-
const
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
);
|
|
1931
|
-
|
|
2688
|
+
function buildSentryConversationUrl(conversationId) {
|
|
2689
|
+
const client2 = sentry_exports.getClient();
|
|
2690
|
+
const dsn = client2?.getDsn();
|
|
2691
|
+
if (!dsn?.host || !dsn.projectId) {
|
|
2692
|
+
return void 0;
|
|
2693
|
+
}
|
|
2694
|
+
const orgSlug = getSentryOrgSlug();
|
|
2695
|
+
if (!orgSlug) {
|
|
2696
|
+
return void 0;
|
|
2697
|
+
}
|
|
2698
|
+
const encodedId = encodeURIComponent(conversationId);
|
|
2699
|
+
const params = new URLSearchParams();
|
|
2700
|
+
params.set("project", dsn.projectId);
|
|
2701
|
+
const path2 = `explore/conversations/${encodedId}/?${params.toString()}`;
|
|
2702
|
+
if (isSentrySaasDsnHost(dsn.host)) {
|
|
2703
|
+
return `https://${orgSlug}.sentry.io/${path2}`;
|
|
2704
|
+
}
|
|
2705
|
+
return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
|
|
1932
2706
|
}
|
|
1933
|
-
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
"missing_token"
|
|
1939
|
-
);
|
|
2707
|
+
function buildSentryTraceUrl(traceId) {
|
|
2708
|
+
const client2 = sentry_exports.getClient();
|
|
2709
|
+
const dsn = client2?.getDsn();
|
|
2710
|
+
if (!dsn?.host || !dsn.projectId) {
|
|
2711
|
+
return void 0;
|
|
1940
2712
|
}
|
|
1941
|
-
const
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
}
|
|
1945
|
-
});
|
|
1946
|
-
if (!response.ok) {
|
|
1947
|
-
throw new Error(`Slack file download failed: ${response.status}`);
|
|
2713
|
+
const orgSlug = getSentryOrgSlug();
|
|
2714
|
+
if (!orgSlug) {
|
|
2715
|
+
return void 0;
|
|
1948
2716
|
}
|
|
1949
|
-
|
|
2717
|
+
const encodedTraceId = encodeURIComponent(traceId);
|
|
2718
|
+
const params = new URLSearchParams();
|
|
2719
|
+
params.set("project", dsn.projectId);
|
|
2720
|
+
const path2 = `performance/trace/${encodedTraceId}/?${params.toString()}`;
|
|
2721
|
+
if (isSentrySaasDsnHost(dsn.host)) {
|
|
2722
|
+
return `https://${orgSlug}.sentry.io/${path2}`;
|
|
2723
|
+
}
|
|
2724
|
+
return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path2}`;
|
|
1950
2725
|
}
|
|
1951
2726
|
|
|
1952
2727
|
// src/chat/slack/conversation-context.ts
|
|
@@ -2018,6 +2793,8 @@ function formatSlackConversationRedactedLabel(context) {
|
|
|
2018
2793
|
}
|
|
2019
2794
|
|
|
2020
2795
|
export {
|
|
2796
|
+
createAgentPluginLogger,
|
|
2797
|
+
createPluginState,
|
|
2021
2798
|
SlackActionError,
|
|
2022
2799
|
getHeaderString,
|
|
2023
2800
|
normalizeSlackConversationId,
|
|
@@ -2028,6 +2805,16 @@ export {
|
|
|
2028
2805
|
isConversationChannel,
|
|
2029
2806
|
getFilePermalink,
|
|
2030
2807
|
downloadPrivateSlackFile,
|
|
2808
|
+
bindSlackDirectCredentialSubject,
|
|
2809
|
+
verifySlackDirectCredentialSubject,
|
|
2810
|
+
validateAgentPlugins,
|
|
2811
|
+
setAgentPlugins,
|
|
2812
|
+
getAgentPlugins,
|
|
2813
|
+
getAgentPluginTools,
|
|
2814
|
+
getAgentPluginRoutes,
|
|
2815
|
+
getAgentPluginSlackConversationLink,
|
|
2816
|
+
getAgentPluginOperationalReports,
|
|
2817
|
+
createAgentPluginHookRunner,
|
|
2031
2818
|
GET,
|
|
2032
2819
|
TURN_CONTEXT_TAG,
|
|
2033
2820
|
getInterruptionMarker,
|