@opentag/dispatcher 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +6 -3
- package/dist/admission.d.ts +44 -0
- package/dist/admission.d.ts.map +1 -0
- package/dist/callbacks.d.ts +13 -0
- package/dist/callbacks.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1678 -78
- package/dist/index.js.map +1 -1
- package/dist/presentation.d.ts +27 -0
- package/dist/presentation.d.ts.map +1 -0
- package/dist/server.d.ts +33 -1
- package/dist/server.d.ts.map +1 -1
- package/package.json +11 -6
package/dist/index.js
CHANGED
|
@@ -1,45 +1,101 @@
|
|
|
1
1
|
// src/callbacks.ts
|
|
2
|
-
import {
|
|
2
|
+
import { createLarkReplyClient, parseLarkThreadKey, replyLarkMessage } from "@opentag/lark";
|
|
3
|
+
import { createSlackPostMessagePayload, createSlackUpdateMessagePayload, parseSlackThreadKey } from "@opentag/slack";
|
|
4
|
+
import { createTelegramSendMessageDraftPayload, createTelegramSendMessagePayload, parseTelegramThreadKey } from "@opentag/telegram";
|
|
5
|
+
function slackUpdateUriFrom(postMessageUri) {
|
|
6
|
+
return postMessageUri.replace(/\/chat\.postMessage$/, "/chat.update");
|
|
7
|
+
}
|
|
8
|
+
function githubCommentUriFrom(input) {
|
|
9
|
+
if (input.responseBody.url) return input.responseBody.url;
|
|
10
|
+
if (typeof input.responseBody.id === "number") {
|
|
11
|
+
return input.commentsUri.replace(/\/comments$/, `/comments/${input.responseBody.id}`);
|
|
12
|
+
}
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
function slackBotTokenFor(input) {
|
|
16
|
+
if (input.agentId && input.botTokensByAgentId && Object.hasOwn(input.botTokensByAgentId, input.agentId) && typeof input.botTokensByAgentId[input.agentId] === "string") {
|
|
17
|
+
return input.botTokensByAgentId[input.agentId];
|
|
18
|
+
}
|
|
19
|
+
return input.botToken;
|
|
20
|
+
}
|
|
3
21
|
function createGitHubCallbackSink(input) {
|
|
4
22
|
const fetchImpl = input.fetchImpl ?? fetch;
|
|
23
|
+
const commentUriByKey = /* @__PURE__ */ new Map();
|
|
24
|
+
const deliveryByKey = /* @__PURE__ */ new Map();
|
|
5
25
|
return {
|
|
6
26
|
async deliver(message) {
|
|
7
27
|
if (message.provider !== "github") return;
|
|
8
28
|
if (!input.token) return;
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
29
|
+
const statusKey = message.statusMessageKey ?? `${message.runId}:status`;
|
|
30
|
+
const previous = deliveryByKey.get(statusKey) ?? Promise.resolve();
|
|
31
|
+
const current = previous.then(async () => {
|
|
32
|
+
const existingCommentUri = commentUriByKey.get(statusKey);
|
|
33
|
+
const response = await fetchImpl(existingCommentUri ?? message.uri, {
|
|
34
|
+
method: existingCommentUri ? "PATCH" : "POST",
|
|
35
|
+
headers: {
|
|
36
|
+
accept: "application/vnd.github+json",
|
|
37
|
+
authorization: `Bearer ${input.token}`,
|
|
38
|
+
"content-type": "application/json",
|
|
39
|
+
"x-github-api-version": "2022-11-28"
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify({ body: message.body })
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error(`deliver GitHub callback failed: ${response.status} ${await response.text()}`);
|
|
45
|
+
}
|
|
46
|
+
if (!existingCommentUri) {
|
|
47
|
+
const body = await response.json();
|
|
48
|
+
const commentUri = githubCommentUriFrom({ commentsUri: message.uri, responseBody: body });
|
|
49
|
+
if (commentUri) {
|
|
50
|
+
commentUriByKey.set(statusKey, commentUri);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (message.kind === "final") {
|
|
54
|
+
commentUriByKey.delete(statusKey);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
deliveryByKey.set(statusKey, current);
|
|
58
|
+
await current.finally(() => {
|
|
59
|
+
if (deliveryByKey.get(statusKey) === current) {
|
|
60
|
+
deliveryByKey.delete(statusKey);
|
|
61
|
+
}
|
|
18
62
|
});
|
|
19
|
-
if (!response.ok) {
|
|
20
|
-
throw new Error(`deliver GitHub callback failed: ${response.status} ${await response.text()}`);
|
|
21
|
-
}
|
|
22
63
|
}
|
|
23
64
|
};
|
|
24
65
|
}
|
|
25
66
|
function createSlackCallbackSink(input) {
|
|
26
67
|
const fetchImpl = input.fetchImpl ?? fetch;
|
|
68
|
+
const statusMessageTsByKey = /* @__PURE__ */ new Map();
|
|
27
69
|
return {
|
|
28
70
|
async deliver(message) {
|
|
29
71
|
if (message.provider !== "slack") return;
|
|
30
|
-
|
|
72
|
+
const botToken = slackBotTokenFor({
|
|
73
|
+
...input.botToken ? { botToken: input.botToken } : {},
|
|
74
|
+
...input.botTokensByAgentId ? { botTokensByAgentId: input.botTokensByAgentId } : {},
|
|
75
|
+
...message.agentId ? { agentId: message.agentId } : {}
|
|
76
|
+
});
|
|
77
|
+
if (!botToken) return;
|
|
31
78
|
const thread = parseSlackThreadKey(message.threadKey ?? "");
|
|
32
|
-
const
|
|
79
|
+
const existingStatusTs = message.statusMessageKey ? statusMessageTsByKey.get(message.statusMessageKey) : void 0;
|
|
80
|
+
const response = await fetchImpl(existingStatusTs ? slackUpdateUriFrom(message.uri) : message.uri, {
|
|
33
81
|
method: "POST",
|
|
34
82
|
headers: {
|
|
35
|
-
authorization: `Bearer ${
|
|
83
|
+
authorization: `Bearer ${botToken}`,
|
|
36
84
|
"content-type": "application/json"
|
|
37
85
|
},
|
|
38
|
-
body: JSON.stringify(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
86
|
+
body: JSON.stringify(
|
|
87
|
+
existingStatusTs ? createSlackUpdateMessagePayload({
|
|
88
|
+
channelId: thread.channelId,
|
|
89
|
+
text: message.body,
|
|
90
|
+
messageTs: existingStatusTs,
|
|
91
|
+
...message.blocks?.length ? { blocks: message.blocks } : {}
|
|
92
|
+
}) : createSlackPostMessagePayload({
|
|
93
|
+
channelId: thread.channelId,
|
|
94
|
+
text: message.body,
|
|
95
|
+
threadTs: thread.threadTs,
|
|
96
|
+
...message.blocks?.length ? { blocks: message.blocks } : {}
|
|
97
|
+
})
|
|
98
|
+
)
|
|
43
99
|
});
|
|
44
100
|
if (!response.ok) {
|
|
45
101
|
throw new Error(`deliver Slack callback failed: ${response.status} ${await response.text()}`);
|
|
@@ -48,6 +104,87 @@ function createSlackCallbackSink(input) {
|
|
|
48
104
|
if (body.ok === false) {
|
|
49
105
|
throw new Error(`deliver Slack callback failed: ${body.error ?? "unknown_error"}`);
|
|
50
106
|
}
|
|
107
|
+
if (message.statusMessageKey && !existingStatusTs && body.ts) {
|
|
108
|
+
statusMessageTsByKey.set(message.statusMessageKey, body.ts);
|
|
109
|
+
}
|
|
110
|
+
if (message.kind === "final") {
|
|
111
|
+
for (const key of statusMessageTsByKey.keys()) {
|
|
112
|
+
if (key.startsWith(`${message.runId}:`)) {
|
|
113
|
+
statusMessageTsByKey.delete(key);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function createLarkCallbackSink(input) {
|
|
121
|
+
if (!input.client && Boolean(input.appId) !== Boolean(input.appSecret)) {
|
|
122
|
+
throw new Error("Lark callback sink requires both appId and appSecret (or neither).");
|
|
123
|
+
}
|
|
124
|
+
const client = input.client ?? (input.appId && input.appSecret ? createLarkReplyClient({ appId: input.appId, appSecret: input.appSecret, ...input.domain ? { domain: input.domain } : {} }) : void 0);
|
|
125
|
+
return {
|
|
126
|
+
async deliver(message) {
|
|
127
|
+
if (message.provider !== "lark") return;
|
|
128
|
+
if (!client) {
|
|
129
|
+
throw new Error("Lark callback sink received a lark message but has no client configured (missing appId/appSecret).");
|
|
130
|
+
}
|
|
131
|
+
if (!message.threadKey) {
|
|
132
|
+
throw new Error("Lark callback message is missing threadKey.");
|
|
133
|
+
}
|
|
134
|
+
const { messageId } = parseLarkThreadKey(message.threadKey);
|
|
135
|
+
await replyLarkMessage(client, { messageId, text: message.body });
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function createTelegramCallbackSink(input) {
|
|
140
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
141
|
+
const draftIdByKey = /* @__PURE__ */ new Map();
|
|
142
|
+
let nextDraftId = 1;
|
|
143
|
+
return {
|
|
144
|
+
async deliver(message) {
|
|
145
|
+
if (message.provider !== "telegram") return;
|
|
146
|
+
const botToken = slackBotTokenFor({
|
|
147
|
+
...input.botToken ? { botToken: input.botToken } : {},
|
|
148
|
+
...input.botTokensByAgentId ? { botTokensByAgentId: input.botTokensByAgentId } : {},
|
|
149
|
+
...message.agentId ? { agentId: message.agentId } : {}
|
|
150
|
+
});
|
|
151
|
+
if (!botToken) return;
|
|
152
|
+
const thread = parseTelegramThreadKey(message.threadKey ?? "");
|
|
153
|
+
const statusKey = message.statusMessageKey ?? `${message.runId}:status`;
|
|
154
|
+
const isDraft = message.kind === "progress";
|
|
155
|
+
const draftId = isDraft ? draftIdByKey.get(statusKey) ?? nextDraftId++ : void 0;
|
|
156
|
+
if (isDraft && draftId && !draftIdByKey.has(statusKey)) {
|
|
157
|
+
draftIdByKey.set(statusKey, draftId);
|
|
158
|
+
}
|
|
159
|
+
const response = await fetchImpl(`https://api.telegram.org/bot${botToken}/${isDraft ? "sendMessageDraft" : "sendMessage"}`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: {
|
|
162
|
+
"content-type": "application/json"
|
|
163
|
+
},
|
|
164
|
+
body: JSON.stringify(
|
|
165
|
+
isDraft ? createTelegramSendMessageDraftPayload({
|
|
166
|
+
chatId: thread.chatId,
|
|
167
|
+
text: message.body,
|
|
168
|
+
draftId,
|
|
169
|
+
...thread.messageThreadId ? { messageThreadId: thread.messageThreadId } : {}
|
|
170
|
+
}) : createTelegramSendMessagePayload({
|
|
171
|
+
chatId: thread.chatId,
|
|
172
|
+
text: message.body,
|
|
173
|
+
replyToMessageId: thread.replyToMessageId,
|
|
174
|
+
...thread.messageThreadId ? { messageThreadId: thread.messageThreadId } : {}
|
|
175
|
+
})
|
|
176
|
+
)
|
|
177
|
+
});
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
throw new Error(`deliver Telegram callback failed: ${response.status} ${await response.text()}`);
|
|
180
|
+
}
|
|
181
|
+
const body = await response.json();
|
|
182
|
+
if (body.ok === false) {
|
|
183
|
+
throw new Error(`deliver Telegram callback failed: ${body.description ?? "unknown_error"}`);
|
|
184
|
+
}
|
|
185
|
+
if (message.kind === "final") {
|
|
186
|
+
draftIdByKey.delete(statusKey);
|
|
187
|
+
}
|
|
51
188
|
}
|
|
52
189
|
};
|
|
53
190
|
}
|
|
@@ -61,14 +198,224 @@ function createCompositeCallbackSink(sinks) {
|
|
|
61
198
|
};
|
|
62
199
|
}
|
|
63
200
|
|
|
64
|
-
// src/
|
|
65
|
-
import { OpenTagEventSchema, OpenTagRunResultSchema } from "@opentag/core";
|
|
201
|
+
// src/presentation.ts
|
|
66
202
|
import { renderAcknowledgement, renderFinalResult, renderProgress } from "@opentag/github";
|
|
203
|
+
import { renderLarkAcknowledgement, renderLarkFinalResult } from "@opentag/lark";
|
|
204
|
+
import { createSlackFinalResultBlocks, renderSlackAcknowledgement, renderSlackFinalResult } from "@opentag/slack";
|
|
205
|
+
import { renderTelegramAcknowledgement, renderTelegramFinalResult, renderTelegramProgress } from "@opentag/telegram";
|
|
206
|
+
function createDefaultCallbackPresentation() {
|
|
207
|
+
return {
|
|
208
|
+
shouldDeliverAcknowledgement(provider) {
|
|
209
|
+
return provider !== "lark";
|
|
210
|
+
},
|
|
211
|
+
shouldDeliverProgress(provider) {
|
|
212
|
+
return provider !== "slack" && provider !== "lark";
|
|
213
|
+
},
|
|
214
|
+
acknowledgement(input) {
|
|
215
|
+
if (input.provider === "slack") {
|
|
216
|
+
return renderSlackAcknowledgement(input.runId);
|
|
217
|
+
}
|
|
218
|
+
if (input.provider === "lark") {
|
|
219
|
+
return renderLarkAcknowledgement(input.runId);
|
|
220
|
+
}
|
|
221
|
+
if (input.provider === "telegram") {
|
|
222
|
+
return renderTelegramAcknowledgement(input.runId);
|
|
223
|
+
}
|
|
224
|
+
return renderAcknowledgement(input.runId);
|
|
225
|
+
},
|
|
226
|
+
progress(input) {
|
|
227
|
+
if (input.provider === "telegram") {
|
|
228
|
+
return renderTelegramProgress(input.message);
|
|
229
|
+
}
|
|
230
|
+
return renderProgress({ runId: input.runId, message: input.message });
|
|
231
|
+
},
|
|
232
|
+
final(input) {
|
|
233
|
+
if (input.provider === "slack") {
|
|
234
|
+
return {
|
|
235
|
+
body: renderSlackFinalResult(input.result),
|
|
236
|
+
blocks: createSlackFinalResultBlocks(input.result)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (input.provider === "lark") {
|
|
240
|
+
return { body: renderLarkFinalResult(input.result) };
|
|
241
|
+
}
|
|
242
|
+
if (input.provider === "telegram") {
|
|
243
|
+
return { body: renderTelegramFinalResult(input.result) };
|
|
244
|
+
}
|
|
245
|
+
return { body: renderFinalResult(input.result) };
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/server.ts
|
|
251
|
+
import { createHash } from "crypto";
|
|
252
|
+
import {
|
|
253
|
+
AdapterMutationMappingSchema,
|
|
254
|
+
ActorIdentitySchema,
|
|
255
|
+
ActionHintSchema,
|
|
256
|
+
conversationKeysFromEvent as conversationKeysFromEvent2,
|
|
257
|
+
parseThreadActionCommand,
|
|
258
|
+
projectTargetRefFromEvent as projectTargetRefFromEvent2,
|
|
259
|
+
suggestedActionCandidatesFromSnapshots,
|
|
260
|
+
createAdapterMutationCompilerRegistry,
|
|
261
|
+
OpenTagEventSchema,
|
|
262
|
+
OpenTagRunResultSchema,
|
|
263
|
+
PolicyRuleSchema,
|
|
264
|
+
RunEventImportanceSchema,
|
|
265
|
+
RunEventVisibilitySchema
|
|
266
|
+
} from "@opentag/core";
|
|
267
|
+
import {
|
|
268
|
+
applyGitHubIssueMutationOperation,
|
|
269
|
+
createGitHubIssueMutationCompiler
|
|
270
|
+
} from "@opentag/github";
|
|
67
271
|
import { createOpenTagRepository, migrateSchema } from "@opentag/store";
|
|
68
272
|
import Database from "better-sqlite3";
|
|
69
273
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
70
274
|
import { Hono } from "hono";
|
|
71
275
|
import { z } from "zod";
|
|
276
|
+
|
|
277
|
+
// src/admission.ts
|
|
278
|
+
import {
|
|
279
|
+
conversationKeysFromEvent,
|
|
280
|
+
projectTargetRefFromEvent,
|
|
281
|
+
RunAdmissionDecisionSchema
|
|
282
|
+
} from "@opentag/core";
|
|
283
|
+
function isWriteCapable(event) {
|
|
284
|
+
return event.permissions.some((permission) => ["repo:write", "pr:create", "pr:update"].includes(permission.scope));
|
|
285
|
+
}
|
|
286
|
+
function actorIsAllowed(event, allowedActors) {
|
|
287
|
+
if (!allowedActors?.length) return true;
|
|
288
|
+
return allowedActors.includes(event.actor.handle ?? "") || allowedActors.includes(event.actor.providerUserId);
|
|
289
|
+
}
|
|
290
|
+
function admissionDecision(input) {
|
|
291
|
+
return RunAdmissionDecisionSchema.parse({
|
|
292
|
+
action: input.action,
|
|
293
|
+
reason: input.reason,
|
|
294
|
+
reasonCode: input.reasonCode,
|
|
295
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
296
|
+
...input.activeRunId ? { activeRunId: input.activeRunId } : {},
|
|
297
|
+
eventId: input.event.id
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
async function defaultAgentAccessProfileCheck() {
|
|
301
|
+
return { allowed: true };
|
|
302
|
+
}
|
|
303
|
+
function createAdmissionRuntime(input) {
|
|
304
|
+
const agentAccessProfileCheck = input.agentAccessProfileCheck ?? defaultAgentAccessProfileCheck;
|
|
305
|
+
return {
|
|
306
|
+
async admitRun(request) {
|
|
307
|
+
const existingRun = await input.repo.getRunByEventId({ eventId: request.event.id });
|
|
308
|
+
if (existingRun) {
|
|
309
|
+
return {
|
|
310
|
+
outcome: "drop_duplicate",
|
|
311
|
+
decision: admissionDecision({
|
|
312
|
+
action: "drop_duplicate",
|
|
313
|
+
reason: "Source event already created a run.",
|
|
314
|
+
reasonCode: "duplicate_source_event",
|
|
315
|
+
event: request.event,
|
|
316
|
+
activeRunId: existingRun.run.id
|
|
317
|
+
}),
|
|
318
|
+
run: existingRun.run,
|
|
319
|
+
idempotentReplay: true
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const repoKey = projectTargetRefFromEvent(request.event);
|
|
323
|
+
if (!repoKey) {
|
|
324
|
+
return {
|
|
325
|
+
outcome: "needs_human_decision",
|
|
326
|
+
decision: admissionDecision({
|
|
327
|
+
action: "needs_human_decision",
|
|
328
|
+
reason: "The event did not resolve to a repository context.",
|
|
329
|
+
reasonCode: "repo_context_missing",
|
|
330
|
+
event: request.event
|
|
331
|
+
})
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const binding = await input.repo.getRepoBinding(repoKey);
|
|
335
|
+
if (!binding) {
|
|
336
|
+
return {
|
|
337
|
+
outcome: "needs_human_decision",
|
|
338
|
+
decision: admissionDecision({
|
|
339
|
+
action: "needs_human_decision",
|
|
340
|
+
reason: "No repository binding is configured for this work context.",
|
|
341
|
+
reasonCode: "repo_not_bound",
|
|
342
|
+
event: request.event
|
|
343
|
+
})
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (isWriteCapable(request.event) && !actorIsAllowed(request.event, binding.allowedActors)) {
|
|
347
|
+
return {
|
|
348
|
+
outcome: "needs_human_decision",
|
|
349
|
+
decision: admissionDecision({
|
|
350
|
+
action: "needs_human_decision",
|
|
351
|
+
reason: "The requesting actor is not allowed to start a write-capable run in this repository.",
|
|
352
|
+
reasonCode: "actor_not_allowed_for_write",
|
|
353
|
+
event: request.event
|
|
354
|
+
})
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const accessDecision = await agentAccessProfileCheck({ event: request.event, binding });
|
|
358
|
+
if (!accessDecision.allowed) {
|
|
359
|
+
return {
|
|
360
|
+
outcome: "needs_human_decision",
|
|
361
|
+
decision: admissionDecision({
|
|
362
|
+
action: "needs_human_decision",
|
|
363
|
+
reason: accessDecision.reason,
|
|
364
|
+
reasonCode: accessDecision.reasonCode ?? "agent_access_profile_denied",
|
|
365
|
+
event: request.event
|
|
366
|
+
})
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
let activeRun = null;
|
|
370
|
+
for (const conversationKey of conversationKeysFromEvent(request.event)) {
|
|
371
|
+
activeRun = await input.repo.findActiveRunForConversation({ conversationKey });
|
|
372
|
+
if (activeRun) break;
|
|
373
|
+
}
|
|
374
|
+
if (activeRun) {
|
|
375
|
+
const decision = admissionDecision({
|
|
376
|
+
action: "queue_follow_up",
|
|
377
|
+
reason: "A run is already active for this thread; queue the new request as follow-up work.",
|
|
378
|
+
reasonCode: isWriteCapable(request.event) ? "active_write_run_same_thread" : "active_run_same_thread",
|
|
379
|
+
event: request.event,
|
|
380
|
+
activeRunId: activeRun.run.id
|
|
381
|
+
});
|
|
382
|
+
const { followUpRequest, created } = await input.repo.createFollowUpRequest({
|
|
383
|
+
id: request.requestId,
|
|
384
|
+
event: request.event,
|
|
385
|
+
decision,
|
|
386
|
+
activeRunId: activeRun.run.id
|
|
387
|
+
});
|
|
388
|
+
if (created) {
|
|
389
|
+
await input.repo.appendRunEvent({
|
|
390
|
+
runId: activeRun.run.id,
|
|
391
|
+
type: "follow_up_request.queued",
|
|
392
|
+
payload: { followUpRequestId: followUpRequest.id, sourceEventId: request.event.id },
|
|
393
|
+
visibility: "audit",
|
|
394
|
+
importance: "normal",
|
|
395
|
+
message: decision.reason
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
outcome: "follow_up_queued",
|
|
400
|
+
decision,
|
|
401
|
+
followUpRequest
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
outcome: "start",
|
|
406
|
+
decision: admissionDecision({
|
|
407
|
+
action: "start",
|
|
408
|
+
reason: "Source event accepted and ready to create a run.",
|
|
409
|
+
reasonCode: "new_event",
|
|
410
|
+
event: request.event
|
|
411
|
+
}),
|
|
412
|
+
binding
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/server.ts
|
|
72
419
|
var CreateRunnerSchema = z.object({
|
|
73
420
|
runnerId: z.string().min(1),
|
|
74
421
|
name: z.string().min(1)
|
|
@@ -85,49 +432,621 @@ var CreateRepoBindingSchema = z.object({
|
|
|
85
432
|
var CreateSlackChannelBindingSchema = z.object({
|
|
86
433
|
teamId: z.string().min(1),
|
|
87
434
|
channelId: z.string().min(1),
|
|
435
|
+
repoProvider: z.string().min(1).default("github"),
|
|
88
436
|
owner: z.string().min(1),
|
|
89
437
|
repo: z.string().min(1)
|
|
90
438
|
});
|
|
439
|
+
var CreateChannelBindingSchema = z.object({
|
|
440
|
+
provider: z.string().min(1),
|
|
441
|
+
accountId: z.string().min(1),
|
|
442
|
+
conversationId: z.string().min(1),
|
|
443
|
+
repoProvider: z.string().min(1),
|
|
444
|
+
owner: z.string().min(1),
|
|
445
|
+
repo: z.string().min(1),
|
|
446
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
447
|
+
});
|
|
448
|
+
var UpsertPolicyRuleSchema = z.object({
|
|
449
|
+
rule: PolicyRuleSchema
|
|
450
|
+
});
|
|
451
|
+
var UpsertMutationMappingSchema = z.object({
|
|
452
|
+
mapping: AdapterMutationMappingSchema
|
|
453
|
+
});
|
|
91
454
|
var CreateRunSchema = z.object({
|
|
92
455
|
runId: z.string().min(1),
|
|
93
456
|
event: OpenTagEventSchema
|
|
94
457
|
});
|
|
458
|
+
var PromoteFollowUpRequestSchema = z.object({
|
|
459
|
+
runId: z.string().min(1)
|
|
460
|
+
});
|
|
95
461
|
var CompleteRunSchema = z.object({
|
|
96
462
|
result: OpenTagRunResultSchema
|
|
97
463
|
});
|
|
464
|
+
var ApprovalDecisionInputSchema = z.object({
|
|
465
|
+
id: z.string().min(1).optional(),
|
|
466
|
+
approvedIntentIds: z.array(z.string().min(1)),
|
|
467
|
+
rejectedIntentIds: z.array(z.string().min(1)).optional(),
|
|
468
|
+
approvedBy: ActorIdentitySchema,
|
|
469
|
+
approvedAt: z.string().datetime().optional(),
|
|
470
|
+
scope: z.enum(["manual", "policy"]).default("manual"),
|
|
471
|
+
reason: z.string().min(1).optional(),
|
|
472
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
473
|
+
}).refine((value) => {
|
|
474
|
+
const rejected = new Set(value.rejectedIntentIds ?? []);
|
|
475
|
+
return value.approvedIntentIds.every((intentId) => !rejected.has(intentId));
|
|
476
|
+
}, {
|
|
477
|
+
message: "approvedIntentIds and rejectedIntentIds must not overlap"
|
|
478
|
+
});
|
|
479
|
+
var ApplyPlanInputSchema = z.object({
|
|
480
|
+
id: z.string().min(1).optional(),
|
|
481
|
+
approvalDecisionId: z.string().min(1),
|
|
482
|
+
selectedIntentIds: z.array(z.string().min(1)).optional(),
|
|
483
|
+
adapter: z.string().min(1).optional(),
|
|
484
|
+
execute: z.boolean().optional()
|
|
485
|
+
});
|
|
486
|
+
var ThreadActionInputSchema = z.object({
|
|
487
|
+
id: z.string().min(1).optional(),
|
|
488
|
+
rawText: z.string().min(1),
|
|
489
|
+
actor: ActorIdentitySchema,
|
|
490
|
+
callback: z.object({
|
|
491
|
+
provider: z.string().min(1),
|
|
492
|
+
uri: z.string().min(1),
|
|
493
|
+
threadKey: z.string().min(1).optional()
|
|
494
|
+
}),
|
|
495
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
496
|
+
});
|
|
497
|
+
var ChildRunInputSchema = z.object({
|
|
498
|
+
runId: z.string().min(1),
|
|
499
|
+
action: ActionHintSchema,
|
|
500
|
+
commandText: z.string().min(1).optional(),
|
|
501
|
+
sourceProposalId: z.string().min(1).optional(),
|
|
502
|
+
sourceApplyPlanId: z.string().min(1).optional()
|
|
503
|
+
});
|
|
98
504
|
var ProgressSchema = z.object({
|
|
99
505
|
type: z.string().min(1).optional(),
|
|
100
506
|
message: z.string().min(1),
|
|
101
|
-
at: z.string().datetime().optional()
|
|
507
|
+
at: z.string().datetime().optional(),
|
|
508
|
+
visibility: RunEventVisibilitySchema.optional(),
|
|
509
|
+
importance: RunEventImportanceSchema.optional()
|
|
102
510
|
});
|
|
103
|
-
function
|
|
104
|
-
const owner = event.metadata["owner"];
|
|
105
|
-
const repo = event.metadata["repo"];
|
|
106
|
-
if (typeof owner !== "string" || typeof repo !== "string") return null;
|
|
511
|
+
function childEventFromParent(input) {
|
|
107
512
|
return {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
513
|
+
...input.parentEvent,
|
|
514
|
+
id: `evt_${input.childRunId}`,
|
|
515
|
+
sourceEventId: `${input.parentEvent.sourceEventId}:${input.childRunId}`,
|
|
516
|
+
receivedAt: input.receivedAt,
|
|
517
|
+
context: [...input.parentEvent.context, ...input.extraContext ?? []],
|
|
518
|
+
command: {
|
|
519
|
+
rawText: input.commandText ?? `Execute next action: ${input.actionKind}`,
|
|
520
|
+
intent: "run",
|
|
521
|
+
args: {
|
|
522
|
+
parentSourceEventId: input.parentEvent.sourceEventId,
|
|
523
|
+
actionKind: input.actionKind
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
metadata: {
|
|
527
|
+
...input.parentEvent.metadata,
|
|
528
|
+
...input.metadata ?? {}
|
|
529
|
+
}
|
|
111
530
|
};
|
|
112
531
|
}
|
|
113
|
-
function
|
|
114
|
-
|
|
532
|
+
function mappingsFromAdapterPlan(adapterPlan) {
|
|
533
|
+
if (!adapterPlan || typeof adapterPlan !== "object" || Array.isArray(adapterPlan)) return [];
|
|
534
|
+
const mappings = adapterPlan.mappings;
|
|
535
|
+
if (!Array.isArray(mappings)) return [];
|
|
536
|
+
return mappings.map((mapping) => AdapterMutationMappingSchema.parse(mapping));
|
|
115
537
|
}
|
|
116
|
-
function
|
|
538
|
+
function conversationKeyFromCallback(input) {
|
|
539
|
+
return `${input.provider}:${input.threadKey ?? input.uri}`;
|
|
540
|
+
}
|
|
541
|
+
function metadataIssueNumber(metadata) {
|
|
542
|
+
const value = metadata?.["issueNumber"];
|
|
543
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) return String(value);
|
|
544
|
+
if (typeof value === "string" && /^[1-9]\d*$/.test(value)) return value;
|
|
545
|
+
return void 0;
|
|
546
|
+
}
|
|
547
|
+
function metadataString(metadata, key) {
|
|
548
|
+
const value = metadata?.[key];
|
|
549
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
550
|
+
}
|
|
551
|
+
function githubIssueWorkItemExternalId(metadata) {
|
|
552
|
+
const owner = metadataString(metadata, "owner");
|
|
553
|
+
const repo = metadataString(metadata, "repo");
|
|
554
|
+
const issueNumber = metadataIssueNumber(metadata);
|
|
555
|
+
if (!owner || !repo || !issueNumber) return void 0;
|
|
556
|
+
return `${owner}/${repo}#${issueNumber}`;
|
|
557
|
+
}
|
|
558
|
+
function conversationKeysFromThreadAction(input) {
|
|
559
|
+
const primary = conversationKeyFromCallback(input.callback);
|
|
560
|
+
const keys = [primary];
|
|
561
|
+
const issueNumber = metadataIssueNumber(input.metadata);
|
|
562
|
+
if (input.callback.provider === "github" && input.callback.threadKey && issueNumber) {
|
|
563
|
+
const suffix = `#${issueNumber}`;
|
|
564
|
+
if (input.callback.threadKey.endsWith(suffix)) {
|
|
565
|
+
keys.push(`github:${input.callback.threadKey.slice(0, -suffix.length)}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return [...new Set(keys)];
|
|
569
|
+
}
|
|
570
|
+
function proposalMatchesWorkItem(proposal, externalId) {
|
|
571
|
+
return proposal.snapshot.workThread?.workItemReference.externalId === externalId || proposal.event.workItem?.externalId === externalId;
|
|
572
|
+
}
|
|
573
|
+
function stableHash(value) {
|
|
574
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
575
|
+
}
|
|
576
|
+
function stableId(prefix, parts) {
|
|
577
|
+
return `${prefix}_${stableHash(JSON.stringify(parts))}`;
|
|
578
|
+
}
|
|
579
|
+
function actorKeys(actor) {
|
|
580
|
+
return [
|
|
581
|
+
actor.providerUserId,
|
|
582
|
+
actor.handle,
|
|
583
|
+
`${actor.provider}:${actor.providerUserId}`,
|
|
584
|
+
actor.handle ? `${actor.provider}:${actor.handle}` : void 0
|
|
585
|
+
].filter((value) => typeof value === "string" && value.length > 0);
|
|
586
|
+
}
|
|
587
|
+
function actorAllowedByList(actor, allowedActors) {
|
|
117
588
|
if (!allowedActors?.length) return true;
|
|
118
|
-
|
|
589
|
+
const keys = new Set(actorKeys(actor));
|
|
590
|
+
return allowedActors.some((allowedActor) => keys.has(allowedActor));
|
|
591
|
+
}
|
|
592
|
+
function actionCandidatesFor(proposals) {
|
|
593
|
+
const candidates = [];
|
|
594
|
+
let startIndex = 1;
|
|
595
|
+
for (const proposal of proposals) {
|
|
596
|
+
const proposalCandidates = suggestedActionCandidatesFromSnapshots([proposal.snapshot], startIndex).map((candidate) => ({
|
|
597
|
+
...candidate,
|
|
598
|
+
proposal
|
|
599
|
+
}));
|
|
600
|
+
candidates.push(...proposalCandidates);
|
|
601
|
+
startIndex += proposalCandidates.length;
|
|
602
|
+
}
|
|
603
|
+
return candidates;
|
|
604
|
+
}
|
|
605
|
+
function resolveCandidateSelection(input) {
|
|
606
|
+
const candidates = actionCandidatesFor(input.proposals);
|
|
607
|
+
if (candidates.length === 0) {
|
|
608
|
+
return { ok: false, reason: "no_proposal", message: "I could not find any suggested actions for this thread." };
|
|
609
|
+
}
|
|
610
|
+
let selected = [];
|
|
611
|
+
const selection = input.command.selection;
|
|
612
|
+
if (selection.kind === "all") {
|
|
613
|
+
selected = candidates;
|
|
614
|
+
} else if (selection.kind === "index") {
|
|
615
|
+
selected = candidates.filter((candidate) => candidate.index === selection.index);
|
|
616
|
+
} else if (selection.kind === "proposal") {
|
|
617
|
+
selected = candidates.filter((candidate) => candidate.proposalId === selection.proposalId);
|
|
618
|
+
} else if (selection.kind === "intent") {
|
|
619
|
+
selected = candidates.filter((candidate) => candidate.intent.intentId === selection.intentId);
|
|
620
|
+
} else if (selection.kind === "domain") {
|
|
621
|
+
selected = candidates.filter((candidate) => candidate.intent.domain === selection.domain);
|
|
622
|
+
} else if (candidates.length === 1) {
|
|
623
|
+
selected = candidates;
|
|
624
|
+
} else {
|
|
625
|
+
return {
|
|
626
|
+
ok: false,
|
|
627
|
+
reason: "ambiguous",
|
|
628
|
+
runId: candidates[0]?.proposal.runId,
|
|
629
|
+
message: `I found ${candidates.length} suggested actions. Please reply with ${candidates.map((candidate) => `\`${input.command.verb} ${candidate.index}\``).join(", ")} or \`${input.command.verb} all\`.`
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
if (selected.length === 0) {
|
|
633
|
+
return {
|
|
634
|
+
ok: false,
|
|
635
|
+
reason: "no_match",
|
|
636
|
+
runId: candidates[0]?.proposal.runId,
|
|
637
|
+
message: "I could not match that reply to a suggested action. Please use an action number like `apply 1`."
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
const proposalIds = new Set(selected.map((candidate) => candidate.proposalId));
|
|
641
|
+
if (proposalIds.size !== 1) {
|
|
642
|
+
return {
|
|
643
|
+
ok: false,
|
|
644
|
+
reason: "ambiguous",
|
|
645
|
+
runId: selected[0]?.proposal.runId,
|
|
646
|
+
message: "That selection spans multiple proposals. Please apply or approve one proposal at a time using its action number."
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
ok: true,
|
|
651
|
+
resolved: {
|
|
652
|
+
proposal: selected[0].proposal,
|
|
653
|
+
selectedIntentIds: selected.map((candidate) => candidate.intent.intentId),
|
|
654
|
+
selectedCandidates: selected
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
async function resolveThreadAction(input) {
|
|
659
|
+
const conversationKeys = conversationKeysFromThreadAction({
|
|
660
|
+
callback: input.callback,
|
|
661
|
+
...input.metadata ? { metadata: input.metadata } : {}
|
|
662
|
+
});
|
|
663
|
+
const primaryConversationKey = conversationKeys[0];
|
|
664
|
+
const targetWorkItemExternalId = githubIssueWorkItemExternalId(input.metadata);
|
|
665
|
+
if (input.command.selection.kind === "proposal") {
|
|
666
|
+
const stored = await input.repo.getSuggestedChanges({ proposalId: input.command.selection.proposalId });
|
|
667
|
+
if (!stored) {
|
|
668
|
+
return { ok: false, reason: "no_proposal", message: `I could not find proposal \`${input.command.selection.proposalId}\`.` };
|
|
669
|
+
}
|
|
670
|
+
const claimed = await input.repo.getRun({ runId: stored.runId });
|
|
671
|
+
if (!claimed) {
|
|
672
|
+
return { ok: false, reason: "no_proposal", message: `I found the proposal but not its source run.` };
|
|
673
|
+
}
|
|
674
|
+
const proposalConversationKeys = conversationKeysFromEvent2(claimed.event);
|
|
675
|
+
if (!proposalConversationKeys.some((key) => conversationKeys.includes(key))) {
|
|
676
|
+
return { ok: false, reason: "no_match", runId: stored.runId, message: "That proposal does not belong to this source thread." };
|
|
677
|
+
}
|
|
678
|
+
const proposal = { runId: stored.runId, run: claimed.run, event: claimed.event, snapshot: stored.snapshot };
|
|
679
|
+
if (targetWorkItemExternalId && !proposalMatchesWorkItem(proposal, targetWorkItemExternalId)) {
|
|
680
|
+
return { ok: false, reason: "no_match", runId: stored.runId, message: "That proposal does not belong to this source thread." };
|
|
681
|
+
}
|
|
682
|
+
return resolveCandidateSelection({
|
|
683
|
+
command: input.command,
|
|
684
|
+
proposals: [proposal]
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
for (const conversationKey of conversationKeys) {
|
|
688
|
+
const proposals = await input.repo.listLatestSuggestedChangesForConversation({ conversationKey });
|
|
689
|
+
const scopedProposals = conversationKey !== primaryConversationKey && targetWorkItemExternalId ? proposals.filter((proposal) => proposalMatchesWorkItem(proposal, targetWorkItemExternalId)) : proposals;
|
|
690
|
+
if (scopedProposals.length > 0) return resolveCandidateSelection({ command: input.command, proposals: scopedProposals });
|
|
691
|
+
}
|
|
692
|
+
return resolveCandidateSelection({ command: input.command, proposals: [] });
|
|
693
|
+
}
|
|
694
|
+
function isGitHubRepoEvent(event) {
|
|
695
|
+
const repoProvider = event.metadata["repoProvider"];
|
|
696
|
+
return repoProvider === "github" || event.source === "github" && repoProvider === void 0;
|
|
697
|
+
}
|
|
698
|
+
function hasGitHubRepoTarget(event) {
|
|
699
|
+
return isGitHubRepoEvent(event) && typeof event.metadata["owner"] === "string" && typeof event.metadata["repo"] === "string";
|
|
700
|
+
}
|
|
701
|
+
function hasGitHubIssueOrPullTarget(event) {
|
|
702
|
+
return typeof event.metadata["issueNumber"] === "number" || typeof event.metadata["pullRequestNumber"] === "number";
|
|
703
|
+
}
|
|
704
|
+
function isRepoLevelGitHubIntent(intent) {
|
|
705
|
+
return intent.action === "create_pull_request";
|
|
706
|
+
}
|
|
707
|
+
function adapterForAction(input) {
|
|
708
|
+
return hasGitHubRepoTarget(input.event) && (hasGitHubIssueOrPullTarget(input.event) || input.selectedIntents.length > 0 && input.selectedIntents.every((intent) => isRepoLevelGitHubIntent(intent))) ? "github" : input.callbackProvider;
|
|
709
|
+
}
|
|
710
|
+
async function authorizeThreadAction(input) {
|
|
711
|
+
const repoKey = projectTargetRefFromEvent2(input.resolved.proposal.event);
|
|
712
|
+
if (!repoKey) {
|
|
713
|
+
return { ok: false, reason: "repo_context_missing", message: "The proposal does not resolve to a repository binding." };
|
|
714
|
+
}
|
|
715
|
+
const binding = await input.repo.getRepoBinding(repoKey);
|
|
716
|
+
if (!binding) {
|
|
717
|
+
return { ok: false, reason: "repo_binding_not_found", message: "No repository binding is configured for this proposal." };
|
|
718
|
+
}
|
|
719
|
+
if (!actorAllowedByList(input.actor, binding.allowedActors)) {
|
|
720
|
+
return {
|
|
721
|
+
ok: false,
|
|
722
|
+
reason: "actor_not_allowed",
|
|
723
|
+
message: "This actor is not allowed to approve or apply actions for the bound repository."
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
if (input.resolved.proposal.event.source === "slack") {
|
|
727
|
+
const teamId = input.resolved.proposal.event.metadata["teamId"];
|
|
728
|
+
const channelId = input.resolved.proposal.event.metadata["channelId"];
|
|
729
|
+
if (typeof teamId === "string" && typeof channelId === "string") {
|
|
730
|
+
const channelBinding = await input.repo.getChannelBinding({
|
|
731
|
+
provider: "slack",
|
|
732
|
+
accountId: teamId,
|
|
733
|
+
conversationId: channelId
|
|
734
|
+
});
|
|
735
|
+
if (!channelBinding || channelBinding.repoProvider !== repoKey.provider || channelBinding.owner !== repoKey.owner || channelBinding.repo !== repoKey.repo) {
|
|
736
|
+
return {
|
|
737
|
+
ok: false,
|
|
738
|
+
reason: "channel_binding_mismatch",
|
|
739
|
+
message: "The source channel binding is missing or no longer points at the proposal repository."
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return { ok: true };
|
|
745
|
+
}
|
|
746
|
+
function stableApprovalId(input) {
|
|
747
|
+
return input.providedId ?? stableId("approval", [
|
|
748
|
+
input.resolved.proposal.snapshot.proposalId,
|
|
749
|
+
input.command.verb,
|
|
750
|
+
[...input.resolved.selectedIntentIds].sort(),
|
|
751
|
+
actorKeys(input.actor).sort()
|
|
752
|
+
]);
|
|
753
|
+
}
|
|
754
|
+
function sortedValues(values) {
|
|
755
|
+
return [...values ?? []].sort();
|
|
756
|
+
}
|
|
757
|
+
function sameStringSet(left, right) {
|
|
758
|
+
return JSON.stringify(sortedValues(left)) === JSON.stringify(sortedValues(right));
|
|
759
|
+
}
|
|
760
|
+
function sameActor(left, right) {
|
|
761
|
+
return left.provider === right.provider && left.providerUserId === right.providerUserId && (left.handle ?? "") === (right.handle ?? "") && (left.organizationId ?? "") === (right.organizationId ?? "");
|
|
762
|
+
}
|
|
763
|
+
function approvalDecisionMatchesThreadAction(input) {
|
|
764
|
+
const approvedIntentIds = input.command.verb === "reject" ? [] : input.resolved.selectedIntentIds;
|
|
765
|
+
const rejectedIntentIds = input.command.verb === "reject" ? input.resolved.selectedIntentIds : [];
|
|
766
|
+
const metadata = input.decision.metadata;
|
|
767
|
+
const verb = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata["verb"] : void 0;
|
|
768
|
+
return input.decision.proposalId === input.resolved.proposal.snapshot.proposalId && sameStringSet(input.decision.approvedIntentIds, approvedIntentIds) && sameStringSet(input.decision.rejectedIntentIds, rejectedIntentIds) && sameActor(input.decision.approvedBy, input.actor) && verb === input.command.verb;
|
|
769
|
+
}
|
|
770
|
+
function stableApplyPlanId(input) {
|
|
771
|
+
return stableId("apply", [
|
|
772
|
+
input.resolved.proposal.snapshot.proposalId,
|
|
773
|
+
input.adapter,
|
|
774
|
+
[...input.resolved.selectedIntentIds].sort()
|
|
775
|
+
]);
|
|
776
|
+
}
|
|
777
|
+
function stableChildRunId(input) {
|
|
778
|
+
return stableId("run_child", [
|
|
779
|
+
input.resolved.proposal.runId,
|
|
780
|
+
input.resolved.proposal.snapshot.proposalId,
|
|
781
|
+
input.command.verb,
|
|
782
|
+
[...input.resolved.selectedIntentIds].sort(),
|
|
783
|
+
input.sourceApplyPlanId ?? "",
|
|
784
|
+
input.fallbackReason ?? ""
|
|
785
|
+
]);
|
|
786
|
+
}
|
|
787
|
+
function selectedIntentsAlreadyApplied(input) {
|
|
788
|
+
return input.selectedIntentIds.every(
|
|
789
|
+
(intentId) => input.plan.outcomes?.some((outcome) => outcome.intentId === intentId && outcome.outcome === "applied")
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
function githubTargetFromEvent(event) {
|
|
793
|
+
const owner = event.metadata["owner"];
|
|
794
|
+
const repoName = event.metadata["repo"];
|
|
795
|
+
const issueNumber = event.metadata["issueNumber"];
|
|
796
|
+
const pullRequestNumber = event.metadata["pullRequestNumber"];
|
|
797
|
+
if (!hasGitHubRepoTarget(event)) return null;
|
|
798
|
+
if (typeof owner !== "string" || typeof repoName !== "string") return null;
|
|
799
|
+
if (typeof pullRequestNumber === "number") {
|
|
800
|
+
return { owner, repoName, issueNumber: pullRequestNumber, pullRequestNumber, targetKind: "pull_request" };
|
|
801
|
+
}
|
|
802
|
+
if (typeof issueNumber === "number") {
|
|
803
|
+
return { owner, repoName, issueNumber, targetKind: "issue" };
|
|
804
|
+
}
|
|
805
|
+
return { owner, repoName };
|
|
806
|
+
}
|
|
807
|
+
function selectedActionSummary(candidates) {
|
|
808
|
+
return candidates.map((candidate) => `${candidate.index}. ${candidate.intent.summary}`).join("; ");
|
|
809
|
+
}
|
|
810
|
+
function childRunContextLines(input) {
|
|
811
|
+
const previousSummary = input.resolved.proposal.run.result?.summary ?? input.resolved.proposal.snapshot.summary;
|
|
812
|
+
return [
|
|
813
|
+
`- Proposal: \`${input.resolved.proposal.snapshot.proposalId}\``,
|
|
814
|
+
`- Selected intents: ${input.resolved.selectedIntentIds.map((intentId) => `\`${intentId}\``).join(", ")}`,
|
|
815
|
+
`- Previous run: \`${input.resolved.proposal.runId}\``,
|
|
816
|
+
...input.approvalDecisionId ? [`- Approval decision: \`${input.approvalDecisionId}\``] : [],
|
|
817
|
+
`- Previous result: ${previousSummary}`,
|
|
818
|
+
...input.sourceApplyPlanId ? [`- Apply plan: \`${input.sourceApplyPlanId}\``] : [],
|
|
819
|
+
...input.fallbackReason ? [`- Fallback reason: ${input.fallbackReason}`] : []
|
|
820
|
+
];
|
|
821
|
+
}
|
|
822
|
+
function renderChildRunCreatedBody(input) {
|
|
823
|
+
return [
|
|
824
|
+
input.lead,
|
|
825
|
+
"",
|
|
826
|
+
`Child run: \`${input.childRun.id}\``,
|
|
827
|
+
"",
|
|
828
|
+
"Context carried into the child run:",
|
|
829
|
+
...childRunContextLines(input),
|
|
830
|
+
"",
|
|
831
|
+
"The model will continue from this approved proposal instead of starting from a fresh mention."
|
|
832
|
+
].join("\n");
|
|
833
|
+
}
|
|
834
|
+
function actionContextPointer(input) {
|
|
835
|
+
const lines = [
|
|
836
|
+
"OpenTag thread action continuation.",
|
|
837
|
+
`User reply: ${input.command.rawText}`,
|
|
838
|
+
`Action: ${input.command.verb}`,
|
|
839
|
+
`Proposal: ${input.resolved.proposal.snapshot.proposalId}`,
|
|
840
|
+
`Proposal summary: ${input.resolved.proposal.snapshot.summary}`,
|
|
841
|
+
`Selected actions: ${selectedActionSummary(input.resolved.selectedCandidates)}`,
|
|
842
|
+
`Selected intents: ${input.resolved.selectedIntentIds.join(", ")}`,
|
|
843
|
+
`Previous run: ${input.resolved.proposal.runId}`,
|
|
844
|
+
`Previous summary: ${input.resolved.proposal.run.result?.summary ?? input.resolved.proposal.snapshot.summary}`
|
|
845
|
+
];
|
|
846
|
+
if (input.approvalDecisionId) lines.push(`Approval decision: ${input.approvalDecisionId}`);
|
|
847
|
+
if (input.applyPlanId) lines.push(`Apply plan: ${input.applyPlanId}`);
|
|
848
|
+
if (input.fallbackReason) lines.push(`Fallback reason: ${input.fallbackReason}`);
|
|
849
|
+
return {
|
|
850
|
+
kind: "text",
|
|
851
|
+
uri: lines.join("\n"),
|
|
852
|
+
visibility: input.resolved.proposal.event.source === "github" ? "public" : "organization",
|
|
853
|
+
title: "OpenTag approved action context"
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
async function createChildRunForThreadAction(input) {
|
|
857
|
+
const runId = input.runId ?? stableChildRunId(input);
|
|
858
|
+
const action = ActionHintSchema.parse({
|
|
859
|
+
kind: "apply_suggested_changes",
|
|
860
|
+
targetId: input.resolved.proposal.snapshot.proposalId,
|
|
861
|
+
selectedIntentIds: input.resolved.selectedIntentIds,
|
|
862
|
+
metadata: {
|
|
863
|
+
threadActionVerb: input.command.verb,
|
|
864
|
+
rawText: input.command.rawText,
|
|
865
|
+
...input.command.reason ? { reason: input.command.reason } : {},
|
|
866
|
+
...input.approvalDecisionId ? { approvalDecisionId: input.approvalDecisionId } : {},
|
|
867
|
+
...input.fallbackReason ? { fallbackReason: input.fallbackReason } : {}
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
const previousRunSummary = input.resolved.proposal.run.result?.summary ?? input.resolved.proposal.snapshot.summary;
|
|
871
|
+
const commandText = input.command.verb === "continue" ? `Continue approved OpenTag action: ${selectedActionSummary(input.resolved.selectedCandidates)}` : `Continue because OpenTag could not directly apply approved action: ${selectedActionSummary(input.resolved.selectedCandidates)}`;
|
|
872
|
+
const { run } = await input.repo.createRun({
|
|
873
|
+
id: runId,
|
|
874
|
+
event: childEventFromParent({
|
|
875
|
+
parentEvent: input.resolved.proposal.event,
|
|
876
|
+
childRunId: runId,
|
|
877
|
+
actionKind: action.kind,
|
|
878
|
+
commandText,
|
|
879
|
+
receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
880
|
+
extraContext: [
|
|
881
|
+
actionContextPointer({
|
|
882
|
+
command: input.command,
|
|
883
|
+
resolved: input.resolved,
|
|
884
|
+
...input.approvalDecisionId ? { approvalDecisionId: input.approvalDecisionId } : {},
|
|
885
|
+
...input.sourceApplyPlanId ? { applyPlanId: input.sourceApplyPlanId } : {},
|
|
886
|
+
...input.fallbackReason ? { fallbackReason: input.fallbackReason } : {}
|
|
887
|
+
})
|
|
888
|
+
],
|
|
889
|
+
metadata: {
|
|
890
|
+
parentRunId: input.resolved.proposal.runId,
|
|
891
|
+
sourceProposalId: input.resolved.proposal.snapshot.proposalId,
|
|
892
|
+
selectedIntentIds: input.resolved.selectedIntentIds,
|
|
893
|
+
threadActionVerb: input.command.verb,
|
|
894
|
+
previousRunSummary,
|
|
895
|
+
...input.approvalDecisionId ? { approvalDecisionId: input.approvalDecisionId } : {},
|
|
896
|
+
...input.sourceApplyPlanId ? { sourceApplyPlanId: input.sourceApplyPlanId } : {},
|
|
897
|
+
...input.fallbackReason ? { fallbackReason: input.fallbackReason } : {}
|
|
898
|
+
}
|
|
899
|
+
}),
|
|
900
|
+
parentRunId: input.resolved.proposal.runId,
|
|
901
|
+
triggeredByAction: action,
|
|
902
|
+
sourceProposalId: input.resolved.proposal.snapshot.proposalId,
|
|
903
|
+
...input.sourceApplyPlanId ? { sourceApplyPlanId: input.sourceApplyPlanId } : {}
|
|
904
|
+
});
|
|
905
|
+
return run;
|
|
906
|
+
}
|
|
907
|
+
async function executeGitHubApplyPlan(input) {
|
|
908
|
+
if (input.plan.adapter !== "github") {
|
|
909
|
+
return { plan: input.plan, executed: false, fallbackReason: `Adapter ${input.plan.adapter ?? "unknown"} is not directly executable yet.` };
|
|
910
|
+
}
|
|
911
|
+
if (!input.githubApply) {
|
|
912
|
+
return { plan: input.plan, executed: false, fallbackReason: "GitHub apply is not configured on this dispatcher." };
|
|
913
|
+
}
|
|
914
|
+
const target = githubTargetFromEvent(input.resolved.proposal.event);
|
|
915
|
+
if (!target) {
|
|
916
|
+
return { plan: input.plan, executed: false, fallbackReason: "The source run does not include a GitHub issue or pull request target." };
|
|
917
|
+
}
|
|
918
|
+
const preflightOutcomeByIntentId = new Map((input.plan.outcomes ?? []).map((outcome) => [outcome.intentId, outcome]));
|
|
919
|
+
const executableIntents = input.resolved.proposal.snapshot.intents.filter((intent) => {
|
|
920
|
+
if (!input.resolved.selectedIntentIds.includes(intent.intentId)) return false;
|
|
921
|
+
const outcome = preflightOutcomeByIntentId.get(intent.intentId);
|
|
922
|
+
return outcome?.outcome === "skipped" && outcome.message?.startsWith("Preflight passed");
|
|
923
|
+
});
|
|
924
|
+
if (executableIntents.length === 0) {
|
|
925
|
+
return { plan: input.plan, executed: false, fallbackReason: "No selected intent has a direct adapter execution path." };
|
|
926
|
+
}
|
|
927
|
+
const executedOutcomes = [];
|
|
928
|
+
const compilerRegistry = createAdapterMutationCompilerRegistry([
|
|
929
|
+
createGitHubIssueMutationCompiler({
|
|
930
|
+
mappings: mappingsFromAdapterPlan(input.plan.adapterPlan),
|
|
931
|
+
...target.targetKind ? { targetKind: target.targetKind } : {}
|
|
932
|
+
})
|
|
933
|
+
]);
|
|
934
|
+
for (const compilation of compilerRegistry.compile("github", executableIntents)) {
|
|
935
|
+
if (!compilation.ok) {
|
|
936
|
+
executedOutcomes.push(compilation.outcome);
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
executedOutcomes.push(
|
|
940
|
+
await applyGitHubIssueMutationOperation({
|
|
941
|
+
target: {
|
|
942
|
+
token: input.githubApply.token,
|
|
943
|
+
owner: target.owner,
|
|
944
|
+
repo: target.repoName,
|
|
945
|
+
...typeof target.issueNumber === "number" ? { issueNumber: target.issueNumber } : {},
|
|
946
|
+
...target.pullRequestNumber ? { pullRequestNumber: target.pullRequestNumber } : {}
|
|
947
|
+
},
|
|
948
|
+
operation: compilation.operation,
|
|
949
|
+
...input.githubApply.fetchImpl ? { fetchImpl: input.githubApply.fetchImpl } : {}
|
|
950
|
+
})
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
const executedOutcomeByIntentId = new Map(executedOutcomes.map((outcome) => [outcome.intentId, outcome]));
|
|
954
|
+
const mergedOutcomes = (input.plan.outcomes ?? []).map((outcome) => executedOutcomeByIntentId.get(outcome.intentId) ?? outcome);
|
|
955
|
+
const updated = await input.repo.updateApplyPlanOutcomes({
|
|
956
|
+
id: input.plan.id,
|
|
957
|
+
outcomes: mergedOutcomes,
|
|
958
|
+
externalWritesExecuted: true
|
|
959
|
+
});
|
|
960
|
+
const plan = updated ?? input.plan;
|
|
961
|
+
const allSelectedApplied = input.resolved.selectedIntentIds.every(
|
|
962
|
+
(intentId) => plan.outcomes?.some((outcome) => outcome.intentId === intentId && outcome.outcome === "applied")
|
|
963
|
+
);
|
|
964
|
+
return {
|
|
965
|
+
plan,
|
|
966
|
+
executed: allSelectedApplied,
|
|
967
|
+
...allSelectedApplied ? {} : { fallbackReason: "Some selected intents were not directly applied." }
|
|
968
|
+
};
|
|
119
969
|
}
|
|
120
970
|
var noopCallbackSink = {
|
|
121
971
|
async deliver() {
|
|
122
972
|
return;
|
|
123
973
|
}
|
|
124
974
|
};
|
|
975
|
+
function nextCallbackAttemptAt(input) {
|
|
976
|
+
const maxAttempts = input.maxAttempts ?? 5;
|
|
977
|
+
const nextAttempt = input.attempts + 1;
|
|
978
|
+
if (nextAttempt >= maxAttempts) return void 0;
|
|
979
|
+
const baseDelayMs = input.baseDelayMs ?? 5e3;
|
|
980
|
+
const maxDelayMs = input.maxDelayMs ?? 3e5;
|
|
981
|
+
const delayMs = Math.min(maxDelayMs, baseDelayMs * 2 ** Math.max(0, input.attempts));
|
|
982
|
+
return new Date((input.now ?? /* @__PURE__ */ new Date()).getTime() + delayMs).toISOString();
|
|
983
|
+
}
|
|
984
|
+
async function deliverCallbackDelivery(input) {
|
|
985
|
+
try {
|
|
986
|
+
await input.sink.deliver({
|
|
987
|
+
runId: input.delivery.runId,
|
|
988
|
+
kind: input.delivery.kind,
|
|
989
|
+
provider: input.delivery.provider,
|
|
990
|
+
uri: input.delivery.uri,
|
|
991
|
+
body: input.delivery.body,
|
|
992
|
+
...input.delivery.threadKey ? { threadKey: input.delivery.threadKey } : {},
|
|
993
|
+
...input.delivery.agentId ? { agentId: input.delivery.agentId } : {},
|
|
994
|
+
...input.delivery.statusMessageKey ? { statusMessageKey: input.delivery.statusMessageKey } : {},
|
|
995
|
+
...input.delivery.blocks ? { blocks: input.delivery.blocks } : {}
|
|
996
|
+
});
|
|
997
|
+
await input.repo.markCallbackDelivered({ deliveryId: input.delivery.id });
|
|
998
|
+
return true;
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
const nextAttemptAt = nextCallbackAttemptAt({ attempts: input.delivery.attempts, ...input.retry ?? {} });
|
|
1001
|
+
await input.repo.markCallbackFailed({
|
|
1002
|
+
deliveryId: input.delivery.id,
|
|
1003
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1004
|
+
...nextAttemptAt ? { nextAttemptAt } : {}
|
|
1005
|
+
});
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
async function processPendingCallbacks(input) {
|
|
1010
|
+
const maxAttempts = input.retry?.maxAttempts ?? 5;
|
|
1011
|
+
const deliveries = await input.repo.claimPendingCallbackDeliveries({
|
|
1012
|
+
limit: input.limit ?? 20,
|
|
1013
|
+
...input.retry?.now ? { now: input.retry.now } : {},
|
|
1014
|
+
maxAttempts
|
|
1015
|
+
});
|
|
1016
|
+
const result = { processed: 0, delivered: 0, failed: 0 };
|
|
1017
|
+
for (const delivery of deliveries) {
|
|
1018
|
+
result.processed += 1;
|
|
1019
|
+
const delivered = await deliverCallbackDelivery({
|
|
1020
|
+
repo: input.repo,
|
|
1021
|
+
sink: input.sink,
|
|
1022
|
+
delivery,
|
|
1023
|
+
...input.retry ? { retry: input.retry } : {}
|
|
1024
|
+
});
|
|
1025
|
+
if (delivered) {
|
|
1026
|
+
result.delivered += 1;
|
|
1027
|
+
} else {
|
|
1028
|
+
result.failed += 1;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return result;
|
|
1032
|
+
}
|
|
125
1033
|
async function deliverAndAudit(input) {
|
|
126
|
-
await input.
|
|
127
|
-
await input.repo.appendRunEvent({
|
|
1034
|
+
const delivery = await input.repo.enqueueCallbackDelivery({
|
|
128
1035
|
runId: input.message.runId,
|
|
129
|
-
|
|
130
|
-
|
|
1036
|
+
kind: input.message.kind,
|
|
1037
|
+
provider: input.message.provider,
|
|
1038
|
+
uri: input.message.uri,
|
|
1039
|
+
body: input.message.body,
|
|
1040
|
+
...input.message.threadKey ? { threadKey: input.message.threadKey } : {},
|
|
1041
|
+
...input.message.agentId ? { agentId: input.message.agentId } : {},
|
|
1042
|
+
...input.message.statusMessageKey ? { statusMessageKey: input.message.statusMessageKey } : {},
|
|
1043
|
+
...input.message.blocks ? { blocks: input.message.blocks } : {}
|
|
1044
|
+
});
|
|
1045
|
+
await deliverCallbackDelivery({
|
|
1046
|
+
repo: input.repo,
|
|
1047
|
+
sink: input.sink,
|
|
1048
|
+
delivery,
|
|
1049
|
+
...input.retry ? { retry: input.retry } : {}
|
|
131
1050
|
});
|
|
132
1051
|
}
|
|
133
1052
|
function isAuthorized(request, pairingToken) {
|
|
@@ -140,6 +1059,12 @@ function createDispatcherApp(input) {
|
|
|
140
1059
|
const repo = createOpenTagRepository(drizzle(sqlite));
|
|
141
1060
|
const app = new Hono();
|
|
142
1061
|
const callbackSink = input.callbackSink ?? noopCallbackSink;
|
|
1062
|
+
const presentation = input.presentation ?? createDefaultCallbackPresentation();
|
|
1063
|
+
const callbackRetry = input.callbackRetry ?? {};
|
|
1064
|
+
const admission = createAdmissionRuntime({
|
|
1065
|
+
repo,
|
|
1066
|
+
...input.agentAccessProfileCheck ? { agentAccessProfileCheck: input.agentAccessProfileCheck } : {}
|
|
1067
|
+
});
|
|
143
1068
|
app.get("/healthz", (c) => c.json({ ok: true }));
|
|
144
1069
|
app.use("/v1/*", async (c, next) => {
|
|
145
1070
|
if (!isAuthorized(c.req.raw, input.pairingToken)) {
|
|
@@ -152,6 +1077,11 @@ function createDispatcherApp(input) {
|
|
|
152
1077
|
await repo.registerRunner(parsed);
|
|
153
1078
|
return c.json({ ok: true }, 201);
|
|
154
1079
|
});
|
|
1080
|
+
app.get("/v1/runners/:runnerId", async (c) => {
|
|
1081
|
+
const runner = await repo.getRunner({ runnerId: c.req.param("runnerId") });
|
|
1082
|
+
if (!runner) return c.json({ error: "runner_not_found" }, 404);
|
|
1083
|
+
return c.json({ runner });
|
|
1084
|
+
});
|
|
155
1085
|
app.post("/v1/repo-bindings", async (c) => {
|
|
156
1086
|
const parsed = CreateRepoBindingSchema.parse(await c.req.json());
|
|
157
1087
|
await repo.createRepoBinding({
|
|
@@ -174,6 +1104,78 @@ function createDispatcherApp(input) {
|
|
|
174
1104
|
if (!binding) return c.json({ error: "repo_binding_not_found" }, 404);
|
|
175
1105
|
return c.json({ binding });
|
|
176
1106
|
});
|
|
1107
|
+
app.post("/v1/repo-bindings/:provider/:owner/:repo/policy-rules", async (c) => {
|
|
1108
|
+
const parsed = UpsertPolicyRuleSchema.parse(await c.req.json());
|
|
1109
|
+
const rule = await repo.upsertRepoPolicyRule({
|
|
1110
|
+
provider: c.req.param("provider"),
|
|
1111
|
+
owner: c.req.param("owner"),
|
|
1112
|
+
repo: c.req.param("repo"),
|
|
1113
|
+
rule: parsed.rule
|
|
1114
|
+
});
|
|
1115
|
+
return c.json({ rule }, 201);
|
|
1116
|
+
});
|
|
1117
|
+
app.get("/v1/repo-bindings/:provider/:owner/:repo/policy-rules", async (c) => {
|
|
1118
|
+
const rules = await repo.listRepoPolicyRules({
|
|
1119
|
+
provider: c.req.param("provider"),
|
|
1120
|
+
owner: c.req.param("owner"),
|
|
1121
|
+
repo: c.req.param("repo")
|
|
1122
|
+
});
|
|
1123
|
+
return c.json({ rules });
|
|
1124
|
+
});
|
|
1125
|
+
app.post("/v1/repo-bindings/:provider/:owner/:repo/mutation-mappings", async (c) => {
|
|
1126
|
+
const parsed = UpsertMutationMappingSchema.parse(await c.req.json());
|
|
1127
|
+
const mapping = await repo.upsertRepoMutationMapping({
|
|
1128
|
+
provider: c.req.param("provider"),
|
|
1129
|
+
owner: c.req.param("owner"),
|
|
1130
|
+
repo: c.req.param("repo"),
|
|
1131
|
+
mapping: parsed.mapping
|
|
1132
|
+
});
|
|
1133
|
+
return c.json({ mapping }, 201);
|
|
1134
|
+
});
|
|
1135
|
+
app.get("/v1/repo-bindings/:provider/:owner/:repo/mutation-mappings", async (c) => {
|
|
1136
|
+
const mappings = await repo.listRepoMutationMappings({
|
|
1137
|
+
provider: c.req.param("provider"),
|
|
1138
|
+
owner: c.req.param("owner"),
|
|
1139
|
+
repo: c.req.param("repo")
|
|
1140
|
+
});
|
|
1141
|
+
return c.json({ mappings });
|
|
1142
|
+
});
|
|
1143
|
+
app.get("/v1/repo-bindings/:provider/:owner/:repo/metrics", async (c) => {
|
|
1144
|
+
const metrics = await repo.getRepoMetrics({
|
|
1145
|
+
provider: c.req.param("provider"),
|
|
1146
|
+
owner: c.req.param("owner"),
|
|
1147
|
+
repo: c.req.param("repo")
|
|
1148
|
+
});
|
|
1149
|
+
return c.json({ metrics });
|
|
1150
|
+
});
|
|
1151
|
+
app.get("/v1/work-thread-metrics", async (c) => {
|
|
1152
|
+
const threadId = c.req.query("threadId");
|
|
1153
|
+
if (!threadId) return c.json({ error: "thread_id_required" }, 422);
|
|
1154
|
+
const metrics = await repo.getWorkThreadMetrics({ threadId });
|
|
1155
|
+
return c.json({ metrics });
|
|
1156
|
+
});
|
|
1157
|
+
app.post("/v1/channel-bindings", async (c) => {
|
|
1158
|
+
const parsed = CreateChannelBindingSchema.parse(await c.req.json());
|
|
1159
|
+
await repo.upsertChannelBinding({
|
|
1160
|
+
provider: parsed.provider,
|
|
1161
|
+
accountId: parsed.accountId,
|
|
1162
|
+
conversationId: parsed.conversationId,
|
|
1163
|
+
repoProvider: parsed.repoProvider,
|
|
1164
|
+
owner: parsed.owner,
|
|
1165
|
+
repo: parsed.repo,
|
|
1166
|
+
...parsed.metadata ? { metadata: parsed.metadata } : {}
|
|
1167
|
+
});
|
|
1168
|
+
return c.json({ ok: true }, 201);
|
|
1169
|
+
});
|
|
1170
|
+
app.get("/v1/channel-bindings/:provider/:accountId/:conversationId", async (c) => {
|
|
1171
|
+
const binding = await repo.getChannelBinding({
|
|
1172
|
+
provider: c.req.param("provider"),
|
|
1173
|
+
accountId: c.req.param("accountId"),
|
|
1174
|
+
conversationId: c.req.param("conversationId")
|
|
1175
|
+
});
|
|
1176
|
+
if (!binding) return c.json({ error: "channel_binding_not_found" }, 404);
|
|
1177
|
+
return c.json({ binding });
|
|
1178
|
+
});
|
|
177
1179
|
app.post("/v1/slack-channel-bindings", async (c) => {
|
|
178
1180
|
const parsed = CreateSlackChannelBindingSchema.parse(await c.req.json());
|
|
179
1181
|
await repo.createSlackChannelBinding(parsed);
|
|
@@ -189,31 +1191,425 @@ function createDispatcherApp(input) {
|
|
|
189
1191
|
});
|
|
190
1192
|
app.post("/v1/runs", async (c) => {
|
|
191
1193
|
const parsed = CreateRunSchema.parse(await c.req.json());
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
194
|
-
return c.json({
|
|
1194
|
+
const admitted = await admission.admitRun({ requestId: parsed.runId, event: parsed.event });
|
|
1195
|
+
if (admitted.outcome === "needs_human_decision") {
|
|
1196
|
+
return c.json({ decision: admitted.decision }, 202);
|
|
195
1197
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
1198
|
+
if (admitted.outcome === "drop_duplicate") {
|
|
1199
|
+
await repo.appendRunEvent({
|
|
1200
|
+
runId: admitted.run.id,
|
|
1201
|
+
type: "admission.decided",
|
|
1202
|
+
payload: admitted.decision,
|
|
1203
|
+
visibility: "audit",
|
|
1204
|
+
importance: "normal",
|
|
1205
|
+
message: admitted.decision.reason
|
|
1206
|
+
});
|
|
1207
|
+
await repo.appendRunEvent({
|
|
1208
|
+
runId: admitted.run.id,
|
|
1209
|
+
type: "run.create_idempotent_replay",
|
|
1210
|
+
payload: { requestedRunId: parsed.runId, eventId: parsed.event.id },
|
|
1211
|
+
visibility: "audit",
|
|
1212
|
+
importance: "low"
|
|
1213
|
+
});
|
|
1214
|
+
return c.json({ decision: admitted.decision, run: admitted.run, idempotentReplay: true }, 200);
|
|
199
1215
|
}
|
|
200
|
-
if (
|
|
201
|
-
return c.json({
|
|
1216
|
+
if (admitted.outcome === "follow_up_queued") {
|
|
1217
|
+
return c.json({ decision: admitted.decision, followUpRequest: admitted.followUpRequest }, 202);
|
|
202
1218
|
}
|
|
203
|
-
const
|
|
1219
|
+
const createdRun = await repo.createRun({ id: parsed.runId, event: parsed.event });
|
|
1220
|
+
if (!createdRun.created) {
|
|
1221
|
+
return c.json(
|
|
1222
|
+
{
|
|
1223
|
+
decision: {
|
|
1224
|
+
...admitted.decision,
|
|
1225
|
+
action: "drop_duplicate",
|
|
1226
|
+
reason: "Source event already created a run.",
|
|
1227
|
+
reasonCode: "duplicate_source_event",
|
|
1228
|
+
activeRunId: createdRun.run.id
|
|
1229
|
+
},
|
|
1230
|
+
run: createdRun.run,
|
|
1231
|
+
idempotentReplay: true
|
|
1232
|
+
},
|
|
1233
|
+
200
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
const { run } = createdRun;
|
|
1237
|
+
if (presentation.shouldDeliverAcknowledgement(parsed.event.callback.provider)) {
|
|
1238
|
+
await deliverAndAudit({
|
|
1239
|
+
repo,
|
|
1240
|
+
sink: callbackSink,
|
|
1241
|
+
retry: callbackRetry,
|
|
1242
|
+
message: {
|
|
1243
|
+
runId: run.id,
|
|
1244
|
+
kind: "acknowledgement",
|
|
1245
|
+
provider: parsed.event.callback.provider,
|
|
1246
|
+
uri: parsed.event.callback.uri,
|
|
1247
|
+
body: presentation.acknowledgement({ provider: parsed.event.callback.provider, runId: run.id }),
|
|
1248
|
+
...parsed.event.target.agentId ? { agentId: parsed.event.target.agentId } : {},
|
|
1249
|
+
...parsed.event.callback.threadKey ? { threadKey: parsed.event.callback.threadKey } : {}
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
return c.json({ decision: admitted.decision, run }, 201);
|
|
1254
|
+
});
|
|
1255
|
+
app.post("/v1/thread-actions", async (c) => {
|
|
1256
|
+
const parsed = ThreadActionInputSchema.parse(await c.req.json());
|
|
1257
|
+
const command = parseThreadActionCommand(parsed.rawText);
|
|
1258
|
+
if (!command) {
|
|
1259
|
+
return c.json({ outcome: "ignored", reason: "not_action_command" }, 202);
|
|
1260
|
+
}
|
|
1261
|
+
const resolved = await resolveThreadAction({
|
|
1262
|
+
repo,
|
|
1263
|
+
command,
|
|
1264
|
+
callback: parsed.callback,
|
|
1265
|
+
...parsed.metadata ? { metadata: parsed.metadata } : {}
|
|
1266
|
+
});
|
|
1267
|
+
if (!resolved.ok) {
|
|
1268
|
+
if (resolved.runId) {
|
|
1269
|
+
await deliverAndAudit({
|
|
1270
|
+
repo,
|
|
1271
|
+
sink: callbackSink,
|
|
1272
|
+
retry: callbackRetry,
|
|
1273
|
+
message: {
|
|
1274
|
+
runId: resolved.runId,
|
|
1275
|
+
kind: "final",
|
|
1276
|
+
provider: parsed.callback.provider,
|
|
1277
|
+
uri: parsed.callback.uri,
|
|
1278
|
+
body: resolved.message,
|
|
1279
|
+
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
return c.json({ outcome: resolved.reason, message: resolved.message }, resolved.reason === "no_proposal" ? 404 : 409);
|
|
1284
|
+
}
|
|
1285
|
+
const authorization = await authorizeThreadAction({
|
|
1286
|
+
repo,
|
|
1287
|
+
resolved: resolved.resolved,
|
|
1288
|
+
actor: parsed.actor
|
|
1289
|
+
});
|
|
1290
|
+
if (!authorization.ok) {
|
|
1291
|
+
return c.json({ outcome: "unauthorized", reason: authorization.reason, message: authorization.message }, 403);
|
|
1292
|
+
}
|
|
1293
|
+
const selectionText = selectedActionSummary(resolved.resolved.selectedCandidates);
|
|
1294
|
+
const selectedIntents = resolved.resolved.proposal.snapshot.intents.filter(
|
|
1295
|
+
(intent) => resolved.resolved.selectedIntentIds.includes(intent.intentId)
|
|
1296
|
+
);
|
|
1297
|
+
const adapter = adapterForAction({
|
|
1298
|
+
event: resolved.resolved.proposal.event,
|
|
1299
|
+
callbackProvider: parsed.callback.provider,
|
|
1300
|
+
selectedIntents
|
|
1301
|
+
});
|
|
1302
|
+
const applyPlanId = stableApplyPlanId({ resolved: resolved.resolved, adapter });
|
|
1303
|
+
if (command.verb === "apply") {
|
|
1304
|
+
const existingPlan = await repo.getApplyPlan({ id: applyPlanId });
|
|
1305
|
+
if (existingPlan) {
|
|
1306
|
+
const existingDecision2 = await repo.getApprovalDecision({ id: existingPlan.approvalDecisionId });
|
|
1307
|
+
if (selectedIntentsAlreadyApplied({ plan: existingPlan, selectedIntentIds: resolved.resolved.selectedIntentIds })) {
|
|
1308
|
+
return c.json({ outcome: "already_applied", decision: existingDecision2, plan: existingPlan }, 200);
|
|
1309
|
+
}
|
|
1310
|
+
const fallbackReason = "An apply plan already exists for this selected action; OpenTag will not execute it again from a repeated thread reply.";
|
|
1311
|
+
const childRun2 = await createChildRunForThreadAction({
|
|
1312
|
+
repo,
|
|
1313
|
+
command,
|
|
1314
|
+
resolved: resolved.resolved,
|
|
1315
|
+
runId: stableChildRunId({
|
|
1316
|
+
command,
|
|
1317
|
+
resolved: resolved.resolved,
|
|
1318
|
+
sourceApplyPlanId: existingPlan.id,
|
|
1319
|
+
fallbackReason
|
|
1320
|
+
}),
|
|
1321
|
+
approvalDecisionId: existingPlan.approvalDecisionId,
|
|
1322
|
+
sourceApplyPlanId: existingPlan.id,
|
|
1323
|
+
fallbackReason
|
|
1324
|
+
});
|
|
1325
|
+
await deliverAndAudit({
|
|
1326
|
+
repo,
|
|
1327
|
+
sink: callbackSink,
|
|
1328
|
+
retry: callbackRetry,
|
|
1329
|
+
message: {
|
|
1330
|
+
runId: resolved.resolved.proposal.runId,
|
|
1331
|
+
kind: "final",
|
|
1332
|
+
provider: parsed.callback.provider,
|
|
1333
|
+
uri: parsed.callback.uri,
|
|
1334
|
+
body: renderChildRunCreatedBody({
|
|
1335
|
+
lead: "This action was already planned, so OpenTag will not execute the external write again.",
|
|
1336
|
+
resolved: resolved.resolved,
|
|
1337
|
+
childRun: childRun2,
|
|
1338
|
+
approvalDecisionId: existingPlan.approvalDecisionId,
|
|
1339
|
+
sourceApplyPlanId: existingPlan.id,
|
|
1340
|
+
fallbackReason
|
|
1341
|
+
}),
|
|
1342
|
+
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
return c.json({ outcome: "already_planned", decision: existingDecision2, plan: existingPlan, run: childRun2 }, 200);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const providedDecision = parsed.id ? await repo.getApprovalDecision({ id: parsed.id }) : null;
|
|
1349
|
+
const canReuseProvidedDecision = providedDecision ? approvalDecisionMatchesThreadAction({
|
|
1350
|
+
decision: providedDecision,
|
|
1351
|
+
command,
|
|
1352
|
+
resolved: resolved.resolved,
|
|
1353
|
+
actor: parsed.actor
|
|
1354
|
+
}) : false;
|
|
1355
|
+
const approvalId = parsed.id && (!providedDecision || canReuseProvidedDecision) ? parsed.id : stableApprovalId({
|
|
1356
|
+
command,
|
|
1357
|
+
resolved: resolved.resolved,
|
|
1358
|
+
actor: parsed.actor
|
|
1359
|
+
});
|
|
1360
|
+
const existingDecision = canReuseProvidedDecision ? providedDecision : await repo.getApprovalDecision({ id: approvalId });
|
|
1361
|
+
const decision = existingDecision ?? await repo.recordApprovalDecision({
|
|
1362
|
+
id: approvalId,
|
|
1363
|
+
proposalId: resolved.resolved.proposal.snapshot.proposalId,
|
|
1364
|
+
approvedIntentIds: command.verb === "reject" ? [] : resolved.resolved.selectedIntentIds,
|
|
1365
|
+
...command.verb === "reject" ? { rejectedIntentIds: resolved.resolved.selectedIntentIds } : {},
|
|
1366
|
+
approvedBy: parsed.actor,
|
|
1367
|
+
approvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1368
|
+
scope: "manual",
|
|
1369
|
+
...command.reason ? { reason: command.reason } : {},
|
|
1370
|
+
metadata: {
|
|
1371
|
+
source: "thread_action",
|
|
1372
|
+
rawText: command.rawText,
|
|
1373
|
+
verb: command.verb,
|
|
1374
|
+
selection: command.selection,
|
|
1375
|
+
callback: parsed.callback,
|
|
1376
|
+
...parsed.metadata ? { ingressMetadata: parsed.metadata } : {}
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
if (!decision) {
|
|
1380
|
+
return c.json({ error: "proposal_not_found" }, 404);
|
|
1381
|
+
}
|
|
1382
|
+
if (command.verb === "reject") {
|
|
1383
|
+
if (existingDecision) {
|
|
1384
|
+
return c.json({ outcome: "already_rejected", decision }, 200);
|
|
1385
|
+
}
|
|
1386
|
+
const body2 = `Rejected ${selectionText} from \`${resolved.resolved.proposal.snapshot.proposalId}\`.`;
|
|
1387
|
+
await deliverAndAudit({
|
|
1388
|
+
repo,
|
|
1389
|
+
sink: callbackSink,
|
|
1390
|
+
retry: callbackRetry,
|
|
1391
|
+
message: {
|
|
1392
|
+
runId: resolved.resolved.proposal.runId,
|
|
1393
|
+
kind: "final",
|
|
1394
|
+
provider: parsed.callback.provider,
|
|
1395
|
+
uri: parsed.callback.uri,
|
|
1396
|
+
body: body2,
|
|
1397
|
+
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
return c.json({ outcome: "rejected", decision }, 201);
|
|
1401
|
+
}
|
|
1402
|
+
if (command.verb === "approve") {
|
|
1403
|
+
if (existingDecision) {
|
|
1404
|
+
return c.json({ outcome: "already_approved", decision }, 200);
|
|
1405
|
+
}
|
|
1406
|
+
const body2 = `Approved ${selectionText} from \`${resolved.resolved.proposal.snapshot.proposalId}\`.
|
|
1407
|
+
|
|
1408
|
+
Reply with \`apply ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to apply it, or \`continue ${resolved.resolved.selectedCandidates[0]?.index ?? 1}\` to continue with a follow-up run.`;
|
|
1409
|
+
await deliverAndAudit({
|
|
1410
|
+
repo,
|
|
1411
|
+
sink: callbackSink,
|
|
1412
|
+
retry: callbackRetry,
|
|
1413
|
+
message: {
|
|
1414
|
+
runId: resolved.resolved.proposal.runId,
|
|
1415
|
+
kind: "final",
|
|
1416
|
+
provider: parsed.callback.provider,
|
|
1417
|
+
uri: parsed.callback.uri,
|
|
1418
|
+
body: body2,
|
|
1419
|
+
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
return c.json({ outcome: "approved", decision }, 201);
|
|
1423
|
+
}
|
|
1424
|
+
if (command.verb === "continue") {
|
|
1425
|
+
const childRun2 = await createChildRunForThreadAction({
|
|
1426
|
+
repo,
|
|
1427
|
+
command,
|
|
1428
|
+
resolved: resolved.resolved,
|
|
1429
|
+
runId: stableChildRunId({ command, resolved: resolved.resolved }),
|
|
1430
|
+
approvalDecisionId: decision.id
|
|
1431
|
+
});
|
|
1432
|
+
const body2 = renderChildRunCreatedBody({
|
|
1433
|
+
lead: `Continuing from ${selectionText} in \`${resolved.resolved.proposal.snapshot.proposalId}\`.`,
|
|
1434
|
+
resolved: resolved.resolved,
|
|
1435
|
+
childRun: childRun2,
|
|
1436
|
+
approvalDecisionId: decision.id
|
|
1437
|
+
});
|
|
1438
|
+
await deliverAndAudit({
|
|
1439
|
+
repo,
|
|
1440
|
+
sink: callbackSink,
|
|
1441
|
+
retry: callbackRetry,
|
|
1442
|
+
message: {
|
|
1443
|
+
runId: resolved.resolved.proposal.runId,
|
|
1444
|
+
kind: "final",
|
|
1445
|
+
provider: parsed.callback.provider,
|
|
1446
|
+
uri: parsed.callback.uri,
|
|
1447
|
+
body: body2,
|
|
1448
|
+
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
return c.json({ outcome: "child_run_created", decision, run: childRun2 }, 201);
|
|
1452
|
+
}
|
|
1453
|
+
const planResult = await repo.createApplyPlanOnce({
|
|
1454
|
+
id: applyPlanId,
|
|
1455
|
+
proposalId: resolved.resolved.proposal.snapshot.proposalId,
|
|
1456
|
+
approvalDecisionId: decision.id,
|
|
1457
|
+
selectedIntentIds: resolved.resolved.selectedIntentIds,
|
|
1458
|
+
adapter
|
|
1459
|
+
});
|
|
1460
|
+
if (!planResult) {
|
|
1461
|
+
return c.json({ error: "proposal_or_approval_not_found" }, 404);
|
|
1462
|
+
}
|
|
1463
|
+
if (!planResult.created) {
|
|
1464
|
+
if (selectedIntentsAlreadyApplied({ plan: planResult.plan, selectedIntentIds: resolved.resolved.selectedIntentIds })) {
|
|
1465
|
+
return c.json({ outcome: "already_applied", decision, plan: planResult.plan }, 200);
|
|
1466
|
+
}
|
|
1467
|
+
const fallbackReason = "An apply plan already exists for this selected action; OpenTag will not execute it again from a repeated thread reply.";
|
|
1468
|
+
const childRun2 = await createChildRunForThreadAction({
|
|
1469
|
+
repo,
|
|
1470
|
+
command,
|
|
1471
|
+
resolved: resolved.resolved,
|
|
1472
|
+
runId: stableChildRunId({
|
|
1473
|
+
command,
|
|
1474
|
+
resolved: resolved.resolved,
|
|
1475
|
+
sourceApplyPlanId: planResult.plan.id,
|
|
1476
|
+
fallbackReason
|
|
1477
|
+
}),
|
|
1478
|
+
approvalDecisionId: decision.id,
|
|
1479
|
+
sourceApplyPlanId: planResult.plan.id,
|
|
1480
|
+
fallbackReason
|
|
1481
|
+
});
|
|
1482
|
+
await deliverAndAudit({
|
|
1483
|
+
repo,
|
|
1484
|
+
sink: callbackSink,
|
|
1485
|
+
retry: callbackRetry,
|
|
1486
|
+
message: {
|
|
1487
|
+
runId: resolved.resolved.proposal.runId,
|
|
1488
|
+
kind: "final",
|
|
1489
|
+
provider: parsed.callback.provider,
|
|
1490
|
+
uri: parsed.callback.uri,
|
|
1491
|
+
body: renderChildRunCreatedBody({
|
|
1492
|
+
lead: "This action was already planned, so OpenTag will not execute the external write again.",
|
|
1493
|
+
resolved: resolved.resolved,
|
|
1494
|
+
childRun: childRun2,
|
|
1495
|
+
approvalDecisionId: decision.id,
|
|
1496
|
+
sourceApplyPlanId: planResult.plan.id,
|
|
1497
|
+
fallbackReason
|
|
1498
|
+
}),
|
|
1499
|
+
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1502
|
+
return c.json({ outcome: "already_planned", decision, plan: planResult.plan, run: childRun2 }, 200);
|
|
1503
|
+
}
|
|
1504
|
+
const plan = planResult.plan;
|
|
1505
|
+
const execution = await executeGitHubApplyPlan({
|
|
1506
|
+
repo,
|
|
1507
|
+
plan,
|
|
1508
|
+
resolved: resolved.resolved,
|
|
1509
|
+
...input.githubApply ? { githubApply: input.githubApply } : {}
|
|
1510
|
+
});
|
|
1511
|
+
if (execution.executed) {
|
|
1512
|
+
const outcomes = execution.plan.outcomes ?? [];
|
|
1513
|
+
const body2 = [
|
|
1514
|
+
`Applied ${selectionText} from \`${resolved.resolved.proposal.snapshot.proposalId}\`.`,
|
|
1515
|
+
"",
|
|
1516
|
+
"Result:",
|
|
1517
|
+
...outcomes.filter((outcome) => resolved.resolved.selectedIntentIds.includes(outcome.intentId)).map((outcome) => `- \`${outcome.intentId}\`: ${outcome.outcome}${outcome.externalUri ? ` (${outcome.externalUri})` : ""}`)
|
|
1518
|
+
].join("\n");
|
|
1519
|
+
await deliverAndAudit({
|
|
1520
|
+
repo,
|
|
1521
|
+
sink: callbackSink,
|
|
1522
|
+
retry: callbackRetry,
|
|
1523
|
+
message: {
|
|
1524
|
+
runId: resolved.resolved.proposal.runId,
|
|
1525
|
+
kind: "final",
|
|
1526
|
+
provider: parsed.callback.provider,
|
|
1527
|
+
uri: parsed.callback.uri,
|
|
1528
|
+
body: body2,
|
|
1529
|
+
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
return c.json({ outcome: "applied", decision, plan: execution.plan }, 201);
|
|
1533
|
+
}
|
|
1534
|
+
const childRun = await createChildRunForThreadAction({
|
|
1535
|
+
repo,
|
|
1536
|
+
command,
|
|
1537
|
+
resolved: resolved.resolved,
|
|
1538
|
+
runId: stableChildRunId({
|
|
1539
|
+
command,
|
|
1540
|
+
resolved: resolved.resolved,
|
|
1541
|
+
sourceApplyPlanId: execution.plan.id,
|
|
1542
|
+
fallbackReason: execution.fallbackReason ?? "OpenTag cannot directly apply this intent yet."
|
|
1543
|
+
}),
|
|
1544
|
+
approvalDecisionId: decision.id,
|
|
1545
|
+
sourceApplyPlanId: execution.plan.id,
|
|
1546
|
+
fallbackReason: execution.fallbackReason ?? "OpenTag cannot directly apply this intent yet."
|
|
1547
|
+
});
|
|
1548
|
+
const body = renderChildRunCreatedBody({
|
|
1549
|
+
lead: `Action ${selectionText} was approved, but OpenTag cannot directly apply it yet.`,
|
|
1550
|
+
resolved: resolved.resolved,
|
|
1551
|
+
childRun,
|
|
1552
|
+
approvalDecisionId: decision.id,
|
|
1553
|
+
sourceApplyPlanId: execution.plan.id,
|
|
1554
|
+
fallbackReason: execution.fallbackReason ?? "The adapter could not execute the selected intent."
|
|
1555
|
+
});
|
|
204
1556
|
await deliverAndAudit({
|
|
205
1557
|
repo,
|
|
206
1558
|
sink: callbackSink,
|
|
1559
|
+
retry: callbackRetry,
|
|
207
1560
|
message: {
|
|
208
|
-
runId:
|
|
209
|
-
kind: "
|
|
210
|
-
provider: parsed.
|
|
211
|
-
uri: parsed.
|
|
212
|
-
body
|
|
213
|
-
...parsed.
|
|
1561
|
+
runId: resolved.resolved.proposal.runId,
|
|
1562
|
+
kind: "final",
|
|
1563
|
+
provider: parsed.callback.provider,
|
|
1564
|
+
uri: parsed.callback.uri,
|
|
1565
|
+
body,
|
|
1566
|
+
...parsed.callback.threadKey ? { threadKey: parsed.callback.threadKey } : {}
|
|
214
1567
|
}
|
|
215
1568
|
});
|
|
216
|
-
return c.json({ run }, 201);
|
|
1569
|
+
return c.json({ outcome: "child_run_created", decision, plan: execution.plan, run: childRun }, 201);
|
|
1570
|
+
});
|
|
1571
|
+
app.get("/v1/follow-up-requests/:id", async (c) => {
|
|
1572
|
+
const followUpRequest = await repo.getFollowUpRequest({ id: c.req.param("id") });
|
|
1573
|
+
if (!followUpRequest) return c.json({ error: "follow_up_request_not_found" }, 404);
|
|
1574
|
+
return c.json({ followUpRequest });
|
|
1575
|
+
});
|
|
1576
|
+
app.post("/v1/follow-up-requests/:id/create-run", async (c) => {
|
|
1577
|
+
const parsed = PromoteFollowUpRequestSchema.parse(await c.req.json());
|
|
1578
|
+
let promoted;
|
|
1579
|
+
try {
|
|
1580
|
+
promoted = await repo.createRunFromFollowUpRequest({
|
|
1581
|
+
followUpRequestId: c.req.param("id"),
|
|
1582
|
+
runId: parsed.runId
|
|
1583
|
+
});
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1586
|
+
if (message.startsWith("Follow-up request not found:")) {
|
|
1587
|
+
return c.json({ error: "follow_up_request_not_found" }, 404);
|
|
1588
|
+
}
|
|
1589
|
+
if (message.includes("is not queued")) {
|
|
1590
|
+
return c.json({ error: "follow_up_request_not_queued" }, 409);
|
|
1591
|
+
}
|
|
1592
|
+
throw error;
|
|
1593
|
+
}
|
|
1594
|
+
const followUpRequest = promoted.followUpRequest;
|
|
1595
|
+
const event = followUpRequest.event;
|
|
1596
|
+
if (presentation.shouldDeliverAcknowledgement(event.callback.provider)) {
|
|
1597
|
+
await deliverAndAudit({
|
|
1598
|
+
repo,
|
|
1599
|
+
sink: callbackSink,
|
|
1600
|
+
retry: callbackRetry,
|
|
1601
|
+
message: {
|
|
1602
|
+
runId: promoted.run.id,
|
|
1603
|
+
kind: "acknowledgement",
|
|
1604
|
+
provider: event.callback.provider,
|
|
1605
|
+
uri: event.callback.uri,
|
|
1606
|
+
body: presentation.acknowledgement({ provider: event.callback.provider, runId: promoted.run.id }),
|
|
1607
|
+
...event.target.agentId ? { agentId: event.target.agentId } : {},
|
|
1608
|
+
...event.callback.threadKey ? { threadKey: event.callback.threadKey } : {}
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
return c.json({ followUpRequest, run: promoted.run }, 201);
|
|
217
1613
|
});
|
|
218
1614
|
app.post("/v1/runners/:runnerId/claim", async (c) => {
|
|
219
1615
|
const claimed = await repo.claimNextRun({ runnerId: c.req.param("runnerId"), leaseSeconds: 60 });
|
|
@@ -226,60 +1622,260 @@ function createDispatcherApp(input) {
|
|
|
226
1622
|
return c.json({ ok: true });
|
|
227
1623
|
});
|
|
228
1624
|
app.post("/v1/runs/:runId/running", async (c) => {
|
|
1625
|
+
return c.json({
|
|
1626
|
+
error: "runner_scoped_endpoint_required",
|
|
1627
|
+
message: "Use /v1/runners/:runnerId/runs/:runId/running, /progress, or /complete."
|
|
1628
|
+
}, 410);
|
|
1629
|
+
});
|
|
1630
|
+
app.post("/v1/runners/:runnerId/runs/:runId/running", async (c) => {
|
|
229
1631
|
const body = z.object({ executor: z.string().min(1) }).parse(await c.req.json());
|
|
230
|
-
await repo.markRunning({
|
|
1632
|
+
const ok = await repo.markRunning({
|
|
1633
|
+
runId: c.req.param("runId"),
|
|
1634
|
+
runnerId: c.req.param("runnerId"),
|
|
1635
|
+
executor: body.executor
|
|
1636
|
+
});
|
|
1637
|
+
if (!ok) return c.json({ error: "run_not_claimed_by_runner" }, 404);
|
|
231
1638
|
return c.json({ ok: true });
|
|
232
1639
|
});
|
|
233
|
-
app.post("/v1/runs/:runId/progress", async (
|
|
1640
|
+
app.post("/v1/runs/:runId/progress", async () => {
|
|
1641
|
+
return new Response(JSON.stringify({
|
|
1642
|
+
error: "runner_scoped_endpoint_required",
|
|
1643
|
+
message: "Use /v1/runners/:runnerId/runs/:runId/running, /progress, or /complete."
|
|
1644
|
+
}), { status: 410, headers: { "content-type": "application/json" } });
|
|
1645
|
+
});
|
|
1646
|
+
app.post("/v1/runners/:runnerId/runs/:runId/progress", async (c) => {
|
|
234
1647
|
const runId = c.req.param("runId");
|
|
235
1648
|
const body = ProgressSchema.parse(await c.req.json());
|
|
236
|
-
const
|
|
237
|
-
if (!stored) return c.json({ error: "run_not_found" }, 404);
|
|
238
|
-
await repo.recordProgress({
|
|
1649
|
+
const ok = await repo.recordProgress({
|
|
239
1650
|
runId,
|
|
1651
|
+
runnerId: c.req.param("runnerId"),
|
|
240
1652
|
message: body.message,
|
|
241
1653
|
...body.type ? { type: body.type } : {},
|
|
242
|
-
...body.at ? { at: body.at } : {}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
repo,
|
|
246
|
-
sink: callbackSink,
|
|
247
|
-
message: {
|
|
248
|
-
runId,
|
|
249
|
-
kind: "progress",
|
|
250
|
-
provider: stored.event.callback.provider,
|
|
251
|
-
uri: stored.event.callback.uri,
|
|
252
|
-
body: renderProgress({ runId, message: body.message }),
|
|
253
|
-
...stored.event.callback.threadKey ? { threadKey: stored.event.callback.threadKey } : {}
|
|
254
|
-
}
|
|
1654
|
+
...body.at ? { at: body.at } : {},
|
|
1655
|
+
...body.visibility ? { visibility: body.visibility } : {},
|
|
1656
|
+
...body.importance ? { importance: body.importance } : {}
|
|
255
1657
|
});
|
|
1658
|
+
if (!ok) return c.json({ error: "run_not_claimed_by_runner" }, 404);
|
|
1659
|
+
const stored = await repo.getRun({ runId });
|
|
1660
|
+
if (!stored) return c.json({ error: "run_not_found" }, 404);
|
|
1661
|
+
if (presentation.shouldDeliverProgress(stored.event.callback.provider)) {
|
|
1662
|
+
await deliverAndAudit({
|
|
1663
|
+
repo,
|
|
1664
|
+
sink: callbackSink,
|
|
1665
|
+
retry: callbackRetry,
|
|
1666
|
+
message: {
|
|
1667
|
+
runId,
|
|
1668
|
+
kind: "progress",
|
|
1669
|
+
provider: stored.event.callback.provider,
|
|
1670
|
+
uri: stored.event.callback.uri,
|
|
1671
|
+
body: presentation.progress({ provider: stored.event.callback.provider, runId, message: body.message }),
|
|
1672
|
+
...stored.event.target.agentId ? { agentId: stored.event.target.agentId } : {},
|
|
1673
|
+
...stored.event.callback.threadKey ? { threadKey: stored.event.callback.threadKey } : {},
|
|
1674
|
+
statusMessageKey: `${runId}:status`
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
256
1678
|
return c.json({ ok: true });
|
|
257
1679
|
});
|
|
258
|
-
app.post("/v1/runs/:runId/complete", async (
|
|
1680
|
+
app.post("/v1/runs/:runId/complete", async () => {
|
|
1681
|
+
return new Response(JSON.stringify({
|
|
1682
|
+
error: "runner_scoped_endpoint_required",
|
|
1683
|
+
message: "Use /v1/runners/:runnerId/runs/:runId/running, /progress, or /complete."
|
|
1684
|
+
}), { status: 410, headers: { "content-type": "application/json" } });
|
|
1685
|
+
});
|
|
1686
|
+
app.post("/v1/runners/:runnerId/runs/:runId/complete", async (c) => {
|
|
259
1687
|
const runId = c.req.param("runId");
|
|
260
1688
|
const parsed = CompleteRunSchema.parse(await c.req.json());
|
|
1689
|
+
const ok = await repo.completeRun({ runId, runnerId: c.req.param("runnerId"), result: parsed.result });
|
|
1690
|
+
if (!ok) return c.json({ error: "run_not_claimed_by_runner" }, 404);
|
|
261
1691
|
const stored = await repo.getRun({ runId });
|
|
262
1692
|
if (!stored) return c.json({ error: "run_not_found" }, 404);
|
|
263
|
-
|
|
1693
|
+
const finalPresentation = presentation.final({ provider: stored.event.callback.provider, result: parsed.result });
|
|
264
1694
|
await deliverAndAudit({
|
|
265
1695
|
repo,
|
|
266
1696
|
sink: callbackSink,
|
|
1697
|
+
retry: callbackRetry,
|
|
267
1698
|
message: {
|
|
268
1699
|
runId,
|
|
269
1700
|
kind: "final",
|
|
270
1701
|
provider: stored.event.callback.provider,
|
|
271
1702
|
uri: stored.event.callback.uri,
|
|
272
|
-
body:
|
|
273
|
-
...stored.event.
|
|
1703
|
+
body: finalPresentation.body,
|
|
1704
|
+
...stored.event.target.agentId ? { agentId: stored.event.target.agentId } : {},
|
|
1705
|
+
...stored.event.callback.threadKey ? { threadKey: stored.event.callback.threadKey } : {},
|
|
1706
|
+
...finalPresentation.blocks?.length ? { blocks: finalPresentation.blocks } : {}
|
|
274
1707
|
}
|
|
275
1708
|
});
|
|
276
1709
|
return c.json({ ok: true });
|
|
277
1710
|
});
|
|
1711
|
+
app.get("/v1/proposals/:proposalId", async (c) => {
|
|
1712
|
+
const proposal = await repo.getSuggestedChanges({ proposalId: c.req.param("proposalId") });
|
|
1713
|
+
if (!proposal) return c.json({ error: "proposal_not_found" }, 404);
|
|
1714
|
+
return c.json(proposal);
|
|
1715
|
+
});
|
|
1716
|
+
app.get("/v1/proposals/:proposalId/lineage", async (c) => {
|
|
1717
|
+
const lineage = await repo.getProposalLineage({ proposalId: c.req.param("proposalId") });
|
|
1718
|
+
if (!lineage) return c.json({ error: "proposal_not_found" }, 404);
|
|
1719
|
+
return c.json({ lineage });
|
|
1720
|
+
});
|
|
1721
|
+
app.get("/v1/proposals/:proposalId/current-intents", async (c) => {
|
|
1722
|
+
const intents = await repo.listCurrentMutationIntents({ proposalId: c.req.param("proposalId") });
|
|
1723
|
+
if (!intents) return c.json({ error: "proposal_not_found" }, 404);
|
|
1724
|
+
return c.json({ intents });
|
|
1725
|
+
});
|
|
1726
|
+
app.post("/v1/proposals/:proposalId/approvals", async (c) => {
|
|
1727
|
+
const proposalId = c.req.param("proposalId");
|
|
1728
|
+
const parsedBody = ApprovalDecisionInputSchema.safeParse(await c.req.json());
|
|
1729
|
+
if (!parsedBody.success) return c.json({ error: "invalid_approval_decision" }, 400);
|
|
1730
|
+
const body = parsedBody.data;
|
|
1731
|
+
const decision = await repo.recordApprovalDecision({
|
|
1732
|
+
id: body.id ?? `approval_${proposalId}_${Date.now()}`,
|
|
1733
|
+
proposalId,
|
|
1734
|
+
approvedIntentIds: body.approvedIntentIds,
|
|
1735
|
+
...body.rejectedIntentIds?.length ? { rejectedIntentIds: body.rejectedIntentIds } : {},
|
|
1736
|
+
approvedBy: body.approvedBy,
|
|
1737
|
+
approvedAt: body.approvedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1738
|
+
scope: body.scope,
|
|
1739
|
+
...body.reason ? { reason: body.reason } : {},
|
|
1740
|
+
...body.metadata ? { metadata: body.metadata } : {}
|
|
1741
|
+
});
|
|
1742
|
+
if (!decision) return c.json({ error: "proposal_not_found" }, 404);
|
|
1743
|
+
return c.json({ decision }, 201);
|
|
1744
|
+
});
|
|
1745
|
+
app.get("/v1/approvals/:approvalDecisionId", async (c) => {
|
|
1746
|
+
const decision = await repo.getApprovalDecision({ id: c.req.param("approvalDecisionId") });
|
|
1747
|
+
if (!decision) return c.json({ error: "approval_decision_not_found" }, 404);
|
|
1748
|
+
return c.json({ decision });
|
|
1749
|
+
});
|
|
1750
|
+
app.post("/v1/proposals/:proposalId/apply-plans", async (c) => {
|
|
1751
|
+
const proposalId = c.req.param("proposalId");
|
|
1752
|
+
const body = ApplyPlanInputSchema.parse(await c.req.json());
|
|
1753
|
+
let executableTarget;
|
|
1754
|
+
if (body.execute) {
|
|
1755
|
+
if (body.adapter !== "github") {
|
|
1756
|
+
return c.json({ error: "apply_execution_adapter_not_supported" }, 422);
|
|
1757
|
+
}
|
|
1758
|
+
if (!input.githubApply) {
|
|
1759
|
+
return c.json({ error: "github_apply_not_configured" }, 422);
|
|
1760
|
+
}
|
|
1761
|
+
const proposal = await repo.getSuggestedChanges({ proposalId });
|
|
1762
|
+
if (!proposal) return c.json({ error: "proposal_not_found" }, 404);
|
|
1763
|
+
const stored = await repo.getRun({ runId: proposal.runId });
|
|
1764
|
+
if (!stored) return c.json({ error: "run_not_found" }, 404);
|
|
1765
|
+
const target = githubTargetFromEvent(stored.event);
|
|
1766
|
+
if (!target) {
|
|
1767
|
+
return c.json({ error: "github_target_missing" }, 422);
|
|
1768
|
+
}
|
|
1769
|
+
executableTarget = { proposal, target };
|
|
1770
|
+
}
|
|
1771
|
+
const applyPlanInput = {
|
|
1772
|
+
id: body.id ?? `apply_${proposalId}_${Date.now()}`,
|
|
1773
|
+
proposalId,
|
|
1774
|
+
approvalDecisionId: body.approvalDecisionId,
|
|
1775
|
+
...body.selectedIntentIds !== void 0 ? { selectedIntentIds: body.selectedIntentIds } : {},
|
|
1776
|
+
...body.adapter ? { adapter: body.adapter } : {}
|
|
1777
|
+
};
|
|
1778
|
+
let plan;
|
|
1779
|
+
if (body.execute) {
|
|
1780
|
+
const planResult = await repo.createApplyPlanOnce(applyPlanInput);
|
|
1781
|
+
if (!planResult) return c.json({ error: "proposal_or_approval_not_found" }, 404);
|
|
1782
|
+
plan = planResult.plan;
|
|
1783
|
+
if (!planResult.created) {
|
|
1784
|
+
return c.json({ plan, alreadyPlanned: true }, 200);
|
|
1785
|
+
}
|
|
1786
|
+
} else {
|
|
1787
|
+
const planResult = await repo.createApplyPlan(applyPlanInput);
|
|
1788
|
+
if (!planResult) return c.json({ error: "proposal_or_approval_not_found" }, 404);
|
|
1789
|
+
plan = planResult;
|
|
1790
|
+
}
|
|
1791
|
+
if (body.execute && executableTarget) {
|
|
1792
|
+
const githubApply = input.githubApply;
|
|
1793
|
+
if (!githubApply) {
|
|
1794
|
+
return c.json({ error: "github_apply_not_configured" }, 422);
|
|
1795
|
+
}
|
|
1796
|
+
const preflightOutcomeByIntentId = new Map((plan.outcomes ?? []).map((outcome) => [outcome.intentId, outcome]));
|
|
1797
|
+
const executableIntents = executableTarget.proposal.snapshot.intents.filter((intent) => {
|
|
1798
|
+
const outcome = preflightOutcomeByIntentId.get(intent.intentId);
|
|
1799
|
+
return outcome?.outcome === "skipped" && outcome.message?.startsWith("Preflight passed");
|
|
1800
|
+
});
|
|
1801
|
+
const target = {
|
|
1802
|
+
token: githubApply.token,
|
|
1803
|
+
owner: executableTarget.target.owner,
|
|
1804
|
+
repo: executableTarget.target.repoName,
|
|
1805
|
+
...typeof executableTarget.target.issueNumber === "number" ? { issueNumber: executableTarget.target.issueNumber } : {},
|
|
1806
|
+
...executableTarget.target.pullRequestNumber ? { pullRequestNumber: executableTarget.target.pullRequestNumber } : {}
|
|
1807
|
+
};
|
|
1808
|
+
const executedOutcomes = [];
|
|
1809
|
+
const compilerRegistry = createAdapterMutationCompilerRegistry([
|
|
1810
|
+
createGitHubIssueMutationCompiler({
|
|
1811
|
+
mappings: mappingsFromAdapterPlan(plan.adapterPlan),
|
|
1812
|
+
...executableTarget.target.targetKind ? { targetKind: executableTarget.target.targetKind } : {}
|
|
1813
|
+
})
|
|
1814
|
+
]);
|
|
1815
|
+
for (const compilation of compilerRegistry.compile("github", executableIntents)) {
|
|
1816
|
+
if (!compilation.ok) {
|
|
1817
|
+
executedOutcomes.push(compilation.outcome);
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
executedOutcomes.push(
|
|
1821
|
+
await applyGitHubIssueMutationOperation({
|
|
1822
|
+
target,
|
|
1823
|
+
operation: compilation.operation,
|
|
1824
|
+
...githubApply.fetchImpl ? { fetchImpl: githubApply.fetchImpl } : {}
|
|
1825
|
+
})
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
const executedOutcomeByIntentId = new Map(executedOutcomes.map((outcome) => [outcome.intentId, outcome]));
|
|
1829
|
+
const mergedOutcomes = (plan.outcomes ?? []).map((outcome) => executedOutcomeByIntentId.get(outcome.intentId) ?? outcome);
|
|
1830
|
+
const executedPlan = await repo.updateApplyPlanOutcomes({
|
|
1831
|
+
id: plan.id,
|
|
1832
|
+
outcomes: mergedOutcomes,
|
|
1833
|
+
externalWritesExecuted: true
|
|
1834
|
+
});
|
|
1835
|
+
return c.json({ plan: executedPlan ?? plan }, 201);
|
|
1836
|
+
}
|
|
1837
|
+
return c.json({ plan }, 201);
|
|
1838
|
+
});
|
|
1839
|
+
app.get("/v1/apply-plans/:applyPlanId", async (c) => {
|
|
1840
|
+
const plan = await repo.getApplyPlan({ id: c.req.param("applyPlanId") });
|
|
1841
|
+
if (!plan) return c.json({ error: "apply_plan_not_found" }, 404);
|
|
1842
|
+
return c.json({ plan });
|
|
1843
|
+
});
|
|
1844
|
+
app.post("/v1/runs/:runId/child-runs", async (c) => {
|
|
1845
|
+
const parentRunId = c.req.param("runId");
|
|
1846
|
+
const body = ChildRunInputSchema.parse(await c.req.json());
|
|
1847
|
+
const parent = await repo.getRun({ runId: parentRunId });
|
|
1848
|
+
if (!parent) return c.json({ error: "parent_run_not_found" }, 404);
|
|
1849
|
+
const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1850
|
+
const sourceProposalId = body.sourceProposalId ?? body.action.targetId;
|
|
1851
|
+
const { run } = await repo.createRun({
|
|
1852
|
+
id: body.runId,
|
|
1853
|
+
event: childEventFromParent({
|
|
1854
|
+
parentEvent: parent.event,
|
|
1855
|
+
childRunId: body.runId,
|
|
1856
|
+
...body.commandText ? { commandText: body.commandText } : {},
|
|
1857
|
+
actionKind: body.action.kind,
|
|
1858
|
+
receivedAt
|
|
1859
|
+
}),
|
|
1860
|
+
parentRunId,
|
|
1861
|
+
triggeredByAction: body.action,
|
|
1862
|
+
...sourceProposalId ? { sourceProposalId } : {},
|
|
1863
|
+
...body.sourceApplyPlanId ? { sourceApplyPlanId: body.sourceApplyPlanId } : {}
|
|
1864
|
+
});
|
|
1865
|
+
return c.json({ run }, 201);
|
|
1866
|
+
});
|
|
278
1867
|
app.get("/v1/runs/:runId", async (c) => {
|
|
279
1868
|
const stored = await repo.getRun({ runId: c.req.param("runId") });
|
|
280
1869
|
if (!stored) return c.json({ error: "run_not_found" }, 404);
|
|
281
1870
|
return c.json(stored);
|
|
282
1871
|
});
|
|
1872
|
+
app.get("/v1/runs/:runId/metrics", async (c) => {
|
|
1873
|
+
const runId = c.req.param("runId");
|
|
1874
|
+
const stored = await repo.getRun({ runId });
|
|
1875
|
+
if (!stored) return c.json({ error: "run_not_found" }, 404);
|
|
1876
|
+
const metrics = await repo.getRunMetrics({ runId });
|
|
1877
|
+
return c.json({ metrics });
|
|
1878
|
+
});
|
|
283
1879
|
app.get("/v1/runs/:runId/events", async (c) => {
|
|
284
1880
|
const events = await repo.listRunEvents({ runId: c.req.param("runId") });
|
|
285
1881
|
return c.json({ events });
|
|
@@ -288,8 +1884,12 @@ function createDispatcherApp(input) {
|
|
|
288
1884
|
}
|
|
289
1885
|
export {
|
|
290
1886
|
createCompositeCallbackSink,
|
|
1887
|
+
createDefaultCallbackPresentation,
|
|
291
1888
|
createDispatcherApp,
|
|
292
1889
|
createGitHubCallbackSink,
|
|
293
|
-
|
|
1890
|
+
createLarkCallbackSink,
|
|
1891
|
+
createSlackCallbackSink,
|
|
1892
|
+
createTelegramCallbackSink,
|
|
1893
|
+
processPendingCallbacks
|
|
294
1894
|
};
|
|
295
1895
|
//# sourceMappingURL=index.js.map
|