@vercel/factory 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/api.d.mts +225 -0
- package/dist/api.mjs +312 -0
- package/dist/api.mjs.map +1 -0
- package/dist/{approval-DdvEKLiE.mjs → approval-JX2Px9IC.mjs} +2 -2
- package/dist/{approval-DdvEKLiE.mjs.map → approval-JX2Px9IC.mjs.map} +1 -1
- package/dist/blob/index.d.mts +4 -3
- package/dist/blob/index.mjs +21 -15
- package/dist/blob/index.mjs.map +1 -1
- package/dist/budget-DzSiIA4t.mjs +42 -0
- package/dist/budget-DzSiIA4t.mjs.map +1 -0
- package/dist/build-factory.d.mts +1 -1
- package/dist/build-factory.mjs +1 -1
- package/dist/change-D2U6v7Ek.mjs +554 -0
- package/dist/change-D2U6v7Ek.mjs.map +1 -0
- package/dist/communication-Kd9nU5RP.mjs +30 -0
- package/dist/communication-Kd9nU5RP.mjs.map +1 -0
- package/dist/connectors/index.d.mts +3 -2
- package/dist/connectors/index.mjs +53 -26
- package/dist/connectors/index.mjs.map +1 -1
- package/dist/connectors/slack.d.mts +17 -6
- package/dist/connectors/slack.mjs +99 -32
- package/dist/connectors/slack.mjs.map +1 -1
- package/dist/coordination-Cq08hb1l.mjs +76 -0
- package/dist/coordination-Cq08hb1l.mjs.map +1 -0
- package/dist/dispatch-BaZS3PT8.d.mts +76 -0
- package/dist/{driver-CrZsy7sm.d.mts → driver-BC0Mt5KG.d.mts} +2 -2
- package/dist/driver-Bbd25rNM.mjs.map +1 -1
- package/dist/engine-DFHU2Pe-.d.mts +781 -0
- package/dist/engine-WaUM2cGA.mjs +1068 -0
- package/dist/engine-WaUM2cGA.mjs.map +1 -0
- package/dist/eve/index.d.mts +289 -153
- package/dist/eve/index.mjs +378 -89
- package/dist/eve/index.mjs.map +1 -1
- package/dist/{factory-config-ifd3JxiU.mjs → factory-config-BoRXOw43.mjs} +23 -22
- package/dist/factory-config-BoRXOw43.mjs.map +1 -0
- package/dist/{factory-config-Bu4mklm9.d.mts → factory-config-D2yU3G0c.d.mts} +21 -2
- package/dist/{github-wThBfbm-.d.mts → github-C5HNBN6p.d.mts} +34 -3
- package/dist/github.d.mts +2 -2
- package/dist/github.mjs +61 -1
- package/dist/github.mjs.map +1 -1
- package/dist/{id-DwCihQr4.mjs → id-BLIkCVue.mjs} +4 -2
- package/dist/{id-DwCihQr4.mjs.map → id-BLIkCVue.mjs.map} +1 -1
- package/dist/index-Bw6hggoH.d.mts +40 -0
- package/dist/index.d.mts +291 -61
- package/dist/index.mjs +140 -783
- package/dist/index.mjs.map +1 -1
- package/dist/postgres/index.d.mts +1 -1
- package/dist/task-CcOyOJ3k.mjs +154 -0
- package/dist/task-CcOyOJ3k.mjs.map +1 -0
- package/package.json +5 -1
- package/dist/approval-CIXB8IF8.d.mts +0 -522
- package/dist/change-CFNGXVqD.mjs +0 -230
- package/dist/change-CFNGXVqD.mjs.map +0 -1
- package/dist/factory-config-ifd3JxiU.mjs.map +0 -1
- package/dist/index-Cy4jfXh5.d.mts +0 -67
- package/dist/signal-CHVEmzvI.d.mts +0 -59
package/dist/eve/index.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { c as signalIdSchema, h as vocabularySchema, l as taskIdSchema, n as changeIdSchema, o as repositoryIdSchema } from "../id-BLIkCVue.mjs";
|
|
2
|
+
import { l as taskQuestionSchema } from "../task-CcOyOJ3k.mjs";
|
|
3
|
+
import { c as findingSchema, i as requiresApproval, o as findingFingerprint, r as defaultApprovalPolicy } from "../approval-JX2Px9IC.mjs";
|
|
4
|
+
import { c as dispatchTask, i as levelSchema, t as changeEvidenceSchema } from "../change-D2U6v7Ek.mjs";
|
|
5
|
+
import { t as createCoordination } from "../coordination-Cq08hb1l.mjs";
|
|
4
6
|
import { answerReviewComment, readPullRequestHead, readReviewComment, submitPullRequestReview } from "../github.mjs";
|
|
5
7
|
import { z } from "zod";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
6
9
|
import { defineHook } from "eve/hooks";
|
|
7
10
|
import { defineTool } from "eve/tools";
|
|
8
11
|
//#region src/eve/conflict.ts
|
|
@@ -369,9 +372,21 @@ function fetchPullRequestTool(stores, options) {
|
|
|
369
372
|
const repoPath = options.repoPath ?? "repo";
|
|
370
373
|
const depth = options.depth ?? 50;
|
|
371
374
|
return defineTool({
|
|
372
|
-
description: "Fetch the pull request
|
|
373
|
-
inputSchema: z.object({ changeId: changeIdSchema.describe("The
|
|
375
|
+
description: "Fetch the pull request and the repository's current default branch, then check out the pull request as local branch `pr`. Call this after get_task, before inspecting or testing the change. Returns an exact diff command based on their merge base; use it instead of origin/HEAD.",
|
|
376
|
+
inputSchema: z.object({ changeId: changeIdSchema.describe("The Change linked to your Task, returned by get_task.") }),
|
|
374
377
|
async execute({ changeId }, ctx) {
|
|
378
|
+
const boundTaskId = taskIdSchema.safeParse(ctx.session?.auth.initiator?.attributes.factoryTaskId);
|
|
379
|
+
if (boundTaskId.success) {
|
|
380
|
+
const task = await stores.tasks.get(boundTaskId.data);
|
|
381
|
+
if (task === null) return {
|
|
382
|
+
fetched: false,
|
|
383
|
+
reason: `task ${boundTaskId.data} does not exist`
|
|
384
|
+
};
|
|
385
|
+
if (task.changeId !== void 0 && task.changeId !== changeId) return {
|
|
386
|
+
fetched: false,
|
|
387
|
+
reason: `change ${changeId} is not linked to task ${boundTaskId.data}`
|
|
388
|
+
};
|
|
389
|
+
}
|
|
375
390
|
const change = await stores.changes.get(changeId);
|
|
376
391
|
if (change === null) return {
|
|
377
392
|
fetched: false,
|
|
@@ -388,19 +403,31 @@ function fetchPullRequestTool(stores, options) {
|
|
|
388
403
|
};
|
|
389
404
|
const sandbox = await ctx.getSandbox();
|
|
390
405
|
const tracking = `refs/remotes/origin/pr-${change.prNumber}`;
|
|
406
|
+
const baseTracking = `refs/remotes/origin/${repository.defaultBranch}`;
|
|
391
407
|
const git = `git -C ${repoPath}`;
|
|
392
|
-
const result = await options.withCredential(sandbox, repository.slug, `${git} fetch --depth ${depth} origin +refs/pull/${change.prNumber}/head:${tracking} && ${git} checkout -B pr ${tracking}`);
|
|
408
|
+
const result = await options.withCredential(sandbox, repository.slug, `${git} fetch --depth ${depth} origin +refs/heads/${repository.defaultBranch}:${baseTracking} +refs/pull/${change.prNumber}/head:${tracking} && ${git} checkout -B pr ${tracking}`);
|
|
393
409
|
if (typeof result.exitCode === "number" && result.exitCode !== 0) return {
|
|
394
410
|
fetched: false,
|
|
395
411
|
reason: `git fetch of pull request ${change.prNumber} failed: ${(result.stderr ?? "").trim()}`
|
|
396
412
|
};
|
|
397
413
|
const head = await sandbox.run({ command: `${git} rev-parse HEAD` });
|
|
414
|
+
const base = await sandbox.run({ command: `${git} rev-parse ${baseTracking}` });
|
|
415
|
+
const mergeBase = await sandbox.run({ command: `${git} merge-base ${baseTracking} pr` });
|
|
416
|
+
if (typeof mergeBase.exitCode === "number" && mergeBase.exitCode !== 0) return {
|
|
417
|
+
fetched: false,
|
|
418
|
+
reason: `could not determine the pull request merge base: ${(mergeBase.stderr ?? "").trim()}`
|
|
419
|
+
};
|
|
420
|
+
const mergeBaseSha = mergeBase.stdout.trim();
|
|
398
421
|
return {
|
|
399
422
|
fetched: true,
|
|
400
423
|
prNumber: change.prNumber,
|
|
401
424
|
prUrl: change.prUrl,
|
|
402
425
|
branch: "pr",
|
|
403
|
-
headSha: head.stdout.trim()
|
|
426
|
+
headSha: head.stdout.trim(),
|
|
427
|
+
baseBranch: repository.defaultBranch,
|
|
428
|
+
baseSha: base.stdout.trim(),
|
|
429
|
+
mergeBaseSha,
|
|
430
|
+
diffCommand: `${git} diff ${mergeBaseSha}..pr`
|
|
404
431
|
};
|
|
405
432
|
}
|
|
406
433
|
});
|
|
@@ -439,6 +466,7 @@ function invokeAgentTool(stores, options) {
|
|
|
439
466
|
});
|
|
440
467
|
const running = await dispatchTask(stores, task.id, {
|
|
441
468
|
maxLaunchAttempts: 1,
|
|
469
|
+
budget: options.budget?.(),
|
|
442
470
|
launch: async (dispatched) => {
|
|
443
471
|
const headers = {
|
|
444
472
|
"content-type": "application/json",
|
|
@@ -474,20 +502,156 @@ function invokeAgentTool(stores, options) {
|
|
|
474
502
|
}
|
|
475
503
|
});
|
|
476
504
|
}
|
|
505
|
+
/**
|
|
506
|
+
* Give a conversational agent one bounded delegation surface backed by the
|
|
507
|
+
* application's route registry. The caller selects a declared route; trusted
|
|
508
|
+
* code owns task creation, budget enforcement, and the fresh target session.
|
|
509
|
+
*/
|
|
510
|
+
function delegateAgentTool(stores, options) {
|
|
511
|
+
const doFetch = options.fetch ?? fetch;
|
|
512
|
+
const routeSummary = options.routes.routes.map((route) => `${route.id}: ${route.description}`).join("\n");
|
|
513
|
+
return defineTool({
|
|
514
|
+
description: "Delegate durable work to one registered specialist. Select only a listed route; the specialist runs independently and reports its result through Factory.\n" + routeSummary,
|
|
515
|
+
inputSchema: z.object({
|
|
516
|
+
routeId: z.string().min(1).describe("One route id listed in this tool's description."),
|
|
517
|
+
repositoryIds: z.array(repositoryIdSchema).min(1),
|
|
518
|
+
signalId: signalIdSchema.optional().describe("Pending external signal this delegation accepts."),
|
|
519
|
+
parentTaskId: taskIdSchema.optional().describe("Existing task this work continues."),
|
|
520
|
+
changeId: changeIdSchema.optional().describe("Existing change the specialist should inspect."),
|
|
521
|
+
channel: z.string().min(1).describe("Originating conversation channel."),
|
|
522
|
+
address: z.string().min(1).describe("Originating conversation reply address."),
|
|
523
|
+
brief: z.string().min(1).max(4e3).describe("Facts and desired outcome, without hidden reasoning.")
|
|
524
|
+
}),
|
|
525
|
+
async execute(input, ctx) {
|
|
526
|
+
const route = options.routes.get(input.routeId);
|
|
527
|
+
if (route === void 0) throw new Error(`Unknown agent route ${input.routeId}.`);
|
|
528
|
+
const replyTo = {
|
|
529
|
+
channel: input.channel,
|
|
530
|
+
address: input.address
|
|
531
|
+
};
|
|
532
|
+
const attributes = ctx.session.auth.initiator?.attributes;
|
|
533
|
+
const repositoryAttribute = attributes?.factoryRepositoryIds;
|
|
534
|
+
const boundRepositoryIds = typeof repositoryAttribute === "string" ? repositoryAttribute.split(",").filter(Boolean) : repositoryAttribute;
|
|
535
|
+
if (boundRepositoryIds !== void 0 && (boundRepositoryIds.length !== input.repositoryIds.length || boundRepositoryIds.some((id) => !input.repositoryIds.includes(id)))) throw new Error("Delegation repositories do not match the connector-authenticated context.");
|
|
536
|
+
if (attributes?.factoryReplyChannel !== void 0 && attributes.factoryReplyChannel !== replyTo.channel || attributes?.factoryReplyAddress !== void 0 && attributes.factoryReplyAddress !== replyTo.address) throw new Error("Delegation reply address does not match the authenticated conversation.");
|
|
537
|
+
for (const repositoryId of input.repositoryIds) {
|
|
538
|
+
const repository = await stores.repositories.get(repositoryId);
|
|
539
|
+
if (repository === null || !repository.enabled) throw new Error(`Repository ${repositoryId} is not enabled in this factory.`);
|
|
540
|
+
}
|
|
541
|
+
const boundChange = changeIdSchema.safeParse(attributes?.factoryChangeId);
|
|
542
|
+
if (attributes?.factoryChangeId !== void 0 && !boundChange.success) throw new Error("Connector-authenticated change context is invalid.");
|
|
543
|
+
if (input.changeId !== void 0 && boundChange.success && input.changeId !== boundChange.data) throw new Error("Delegated change does not match the connector-authenticated context.");
|
|
544
|
+
const changeId = input.changeId ?? (boundChange.success ? boundChange.data : void 0);
|
|
545
|
+
if (changeId !== void 0) {
|
|
546
|
+
if (input.repositoryIds.length !== 1) throw new Error("Delegating work on an existing Change requires exactly one repository.");
|
|
547
|
+
const change = await stores.changes.get(changeId);
|
|
548
|
+
if (change === null || change.repositoryId !== input.repositoryIds[0] || change.state !== "verifying" && change.state !== "ready") throw new Error(`Change ${changeId} is not an open pull request in the selected repository.`);
|
|
549
|
+
}
|
|
550
|
+
if (input.signalId !== void 0) {
|
|
551
|
+
const signal = await stores.signals.get(input.signalId);
|
|
552
|
+
if (signal === null) throw new Error(`Signal ${input.signalId} does not exist.`);
|
|
553
|
+
if (!input.repositoryIds.includes(signal.repositoryId) || signal.replyTo.channel !== replyTo.channel || signal.replyTo.address !== replyTo.address) throw new Error("Delegation does not match the signal's authenticated repository and reply address.");
|
|
554
|
+
if (signal.outcome === "pending") await stores.signals.decide(signal.id, "accepted", { reason: `Foreman selected ${route.id}.` });
|
|
555
|
+
else if (signal.outcome !== "accepted") throw new Error(`Signal ${signal.id} is already ${signal.outcome}.`);
|
|
556
|
+
}
|
|
557
|
+
const operator = ctx.session.auth.initiator?.principalId ?? "foreman";
|
|
558
|
+
if (route.taskKind === "code_change" && input.repositoryIds.length > 1) {
|
|
559
|
+
const [first, second, ...rest] = input.repositoryIds;
|
|
560
|
+
if (first === void 0 || second === void 0) throw new Error("Coordination requires two repositories.");
|
|
561
|
+
const coordination = await createCoordination(stores, {
|
|
562
|
+
repositoryIds: [
|
|
563
|
+
first,
|
|
564
|
+
second,
|
|
565
|
+
...rest
|
|
566
|
+
],
|
|
567
|
+
replyTo,
|
|
568
|
+
...input.signalId === void 0 ? {} : { dedupeKey: `route:${input.signalId}:${route.id}` },
|
|
569
|
+
...input.signalId === void 0 ? { operator } : { signalId: input.signalId }
|
|
570
|
+
});
|
|
571
|
+
await Promise.all(coordination.children.filter((child) => child.state === "queued").map((child) => dispatchRouteTask(stores, options, doFetch, route, child, changeId, input.brief)));
|
|
572
|
+
return {
|
|
573
|
+
taskId: coordination.parent.id,
|
|
574
|
+
childTaskIds: coordination.children.map(({ id }) => id),
|
|
575
|
+
routeId: route.id,
|
|
576
|
+
agent: route.agent,
|
|
577
|
+
state: coordination.parent.state
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
const task = await stores.tasks.create({
|
|
581
|
+
repositoryIds: input.repositoryIds,
|
|
582
|
+
kind: route.taskKind,
|
|
583
|
+
origin: input.signalId !== void 0 ? { signalId: input.signalId } : input.parentTaskId !== void 0 ? { parentTaskId: input.parentTaskId } : { operator },
|
|
584
|
+
replyTo,
|
|
585
|
+
changeId,
|
|
586
|
+
...input.signalId === void 0 ? {} : { dedupeKey: `route:${input.signalId}:${route.id}` }
|
|
587
|
+
});
|
|
588
|
+
const running = task.state === "queued" ? await dispatchRouteTask(stores, options, doFetch, route, task, changeId, input.brief) : task;
|
|
589
|
+
return {
|
|
590
|
+
taskId: running.id,
|
|
591
|
+
routeId: route.id,
|
|
592
|
+
agent: route.agent,
|
|
593
|
+
state: running.state
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
async function dispatchRouteTask(stores, options, doFetch, route, task, changeId, brief) {
|
|
599
|
+
return dispatchTask(stores, task.id, {
|
|
600
|
+
maxLaunchAttempts: 1,
|
|
601
|
+
budget: options.budget?.(route),
|
|
602
|
+
launch: async (dispatched) => {
|
|
603
|
+
const headers = {
|
|
604
|
+
"content-type": "application/json",
|
|
605
|
+
...await options.headers?.(dispatched, route)
|
|
606
|
+
};
|
|
607
|
+
const token = await options.token?.(route);
|
|
608
|
+
if (token !== void 0) headers.authorization = `Bearer ${token}`;
|
|
609
|
+
const message = [
|
|
610
|
+
`You are assigned factory task ${dispatched.id} through route ${route.id}.`,
|
|
611
|
+
changeId === void 0 ? null : `Change under review: ${changeId}.`,
|
|
612
|
+
brief,
|
|
613
|
+
"Read the task first and return exactly one bounded Factory outcome."
|
|
614
|
+
].filter((line) => line !== null).join("\n\n");
|
|
615
|
+
const response = await doFetch(`${options.targetUrl(route)}/eve/v1/session`, {
|
|
616
|
+
method: "POST",
|
|
617
|
+
headers,
|
|
618
|
+
body: JSON.stringify({
|
|
619
|
+
message,
|
|
620
|
+
operationId: `factory-task:${dispatched.id}`
|
|
621
|
+
})
|
|
622
|
+
});
|
|
623
|
+
if (!response.ok) throw new Error(`${route.agent} refused the session: ${response.status}`);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}
|
|
477
627
|
//#endregion
|
|
478
628
|
//#region src/eve/post-review.ts
|
|
479
629
|
const reviewCommentSchema = z.strictObject({
|
|
630
|
+
key: z.string().min(1).max(80).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).optional().describe("Stable kebab-case identity for this defect and cause; keep it unchanged across reruns."),
|
|
631
|
+
title: z.string().min(1).max(200).optional().describe("Short finding title."),
|
|
480
632
|
path: z.string().min(1).max(500).describe("Repository-relative file path."),
|
|
481
633
|
line: z.number().int().min(1).describe("Line in the pull request's new version."),
|
|
482
634
|
body: z.string().min(1).max(5e3).describe("Markdown explaining the finding and a concrete fix.")
|
|
483
635
|
});
|
|
636
|
+
const reviewCommentsSchema = z.array(reviewCommentSchema).max(50).default([]).superRefine((comments, context) => {
|
|
637
|
+
const seen = /* @__PURE__ */ new Set();
|
|
638
|
+
comments.forEach((comment, index) => {
|
|
639
|
+
if (comment.key === void 0) return;
|
|
640
|
+
if (seen.has(comment.key)) context.addIssue({
|
|
641
|
+
code: "custom",
|
|
642
|
+
message: `duplicate finding key: ${comment.key}`,
|
|
643
|
+
path: [index, "key"]
|
|
644
|
+
});
|
|
645
|
+
seen.add(comment.key);
|
|
646
|
+
});
|
|
647
|
+
});
|
|
484
648
|
/**
|
|
485
649
|
* Publish an independent review of an existing pull request and finish the
|
|
486
650
|
* review task without changing the pull request's factory lifecycle.
|
|
487
651
|
*/
|
|
488
652
|
function postReviewTool(stores, options) {
|
|
489
653
|
return defineTool({
|
|
490
|
-
description: "
|
|
654
|
+
description: "Record a completed code review and finish your code_review task. Results may be reported through completion callbacks instead of a GitHub review; use request_changes only for blocking findings.",
|
|
491
655
|
inputSchema: z.object({
|
|
492
656
|
taskId: taskIdSchema.describe("Your code_review task."),
|
|
493
657
|
changeId: changeIdSchema.describe("The pull request change from your starting message."),
|
|
@@ -498,7 +662,7 @@ function postReviewTool(stores, options) {
|
|
|
498
662
|
]),
|
|
499
663
|
headSha: z.string().regex(/^[0-9a-f]{40}$/).describe("Commit returned by fetch_pull_request."),
|
|
500
664
|
summary: z.string().min(1).max(5e3).describe("Markdown summary of what you reviewed and found."),
|
|
501
|
-
comments:
|
|
665
|
+
comments: reviewCommentsSchema
|
|
502
666
|
}),
|
|
503
667
|
async execute({ taskId, changeId, verdict, headSha, summary, comments }) {
|
|
504
668
|
const task = await stores.tasks.get(taskId);
|
|
@@ -524,36 +688,68 @@ function postReviewTool(stores, options) {
|
|
|
524
688
|
recorded: false,
|
|
525
689
|
reason: `repository ${change.repositoryId} does not exist`
|
|
526
690
|
};
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
691
|
+
const token = await options.tokenForRepository(repository.slug);
|
|
692
|
+
const completedComments = comments.map((comment) => {
|
|
693
|
+
const key = comment.key ?? legacyFindingKey(comment);
|
|
694
|
+
return {
|
|
695
|
+
...comment,
|
|
696
|
+
key,
|
|
697
|
+
title: comment.title ?? "Review finding",
|
|
698
|
+
fingerprint: createHash("sha256").update(`${repository.slug.toLowerCase()}#${change.prNumber}:${key}`).digest("hex")
|
|
699
|
+
};
|
|
536
700
|
});
|
|
537
|
-
|
|
701
|
+
const completed = {
|
|
538
702
|
taskId,
|
|
539
703
|
changeId,
|
|
540
704
|
repository: repository.slug,
|
|
705
|
+
pullRequestNumber: change.prNumber,
|
|
541
706
|
headSha,
|
|
542
707
|
verdict,
|
|
543
708
|
summary,
|
|
544
|
-
comments
|
|
545
|
-
|
|
546
|
-
|
|
709
|
+
comments: completedComments
|
|
710
|
+
};
|
|
711
|
+
const publish = options.publishReviews !== false;
|
|
712
|
+
let review;
|
|
713
|
+
if (publish) {
|
|
714
|
+
review = await submitPullRequestReview({
|
|
715
|
+
repository: repository.slug,
|
|
716
|
+
token,
|
|
717
|
+
fetch: options.fetch,
|
|
718
|
+
pullRequestNumber: change.prNumber,
|
|
719
|
+
commitId: headSha,
|
|
720
|
+
event: verdict === "approve" ? "APPROVE" : verdict === "request_changes" ? "REQUEST_CHANGES" : "COMMENT",
|
|
721
|
+
body: summary,
|
|
722
|
+
comments: completedComments
|
|
723
|
+
});
|
|
724
|
+
await options.onPublished?.({
|
|
725
|
+
...completed,
|
|
726
|
+
review
|
|
727
|
+
});
|
|
728
|
+
} else {
|
|
729
|
+
const head = await readPullRequestHead({
|
|
730
|
+
repository: repository.slug,
|
|
731
|
+
token,
|
|
732
|
+
fetch: options.fetch,
|
|
733
|
+
pullRequestNumber: change.prNumber
|
|
734
|
+
});
|
|
735
|
+
if (head.sha !== headSha) throw new Error(`pull request changed after review: expected ${headSha}, found ${head.sha}`);
|
|
736
|
+
}
|
|
737
|
+
await options.onCompleted?.(completed);
|
|
547
738
|
await stores.tasks.transition(taskId, "verifying", { reason: summary });
|
|
739
|
+
const finished = await stores.tasks.transition(taskId, "succeeded");
|
|
548
740
|
return {
|
|
549
741
|
recorded: true,
|
|
550
742
|
verdict,
|
|
551
|
-
review,
|
|
552
|
-
|
|
743
|
+
published: review !== void 0,
|
|
744
|
+
...review === void 0 ? {} : { review },
|
|
745
|
+
taskState: finished.state
|
|
553
746
|
};
|
|
554
747
|
}
|
|
555
748
|
});
|
|
556
749
|
}
|
|
750
|
+
function legacyFindingKey(comment) {
|
|
751
|
+
return `legacy-${createHash("sha256").update(`${comment.path}:${comment.line}:${comment.body}`).digest("hex").slice(0, 24)}`;
|
|
752
|
+
}
|
|
557
753
|
//#endregion
|
|
558
754
|
//#region src/eve/review.ts
|
|
559
755
|
function reviewCommentTools(stores, options) {
|
|
@@ -686,7 +882,7 @@ function reviewCommentTools(stores, options) {
|
|
|
686
882
|
* as still in progress. Fail closed: the agent cannot mark a change ready
|
|
687
883
|
* without supplying every evidence field.
|
|
688
884
|
*/
|
|
689
|
-
function recordVerificationTool(stores) {
|
|
885
|
+
function recordVerificationTool(stores, options = {}) {
|
|
690
886
|
return defineTool({
|
|
691
887
|
description: "Record the outcome of a verification task: the evidence you gathered and whether the change passed. Pass moves the change to ready for human review; fail keeps it out of review.",
|
|
692
888
|
inputSchema: z.object({
|
|
@@ -703,9 +899,9 @@ function recordVerificationTool(stores) {
|
|
|
703
899
|
recorded: false,
|
|
704
900
|
reason: `change ${changeId} does not exist`
|
|
705
901
|
};
|
|
706
|
-
if (change.state !== "verifying") return {
|
|
902
|
+
if (change.state !== "verifying" && change.state !== "ready") return {
|
|
707
903
|
recorded: false,
|
|
708
|
-
reason: `change is ${change.state}, not
|
|
904
|
+
reason: `change is ${change.state}, not open for verification; nothing to record`
|
|
709
905
|
};
|
|
710
906
|
const task = await stores.tasks.get(taskId);
|
|
711
907
|
if (task === null || task.kind !== "verification") return {
|
|
@@ -713,21 +909,29 @@ function recordVerificationTool(stores) {
|
|
|
713
909
|
reason: `${taskId} is not a verification task`
|
|
714
910
|
};
|
|
715
911
|
let updatedChange = change;
|
|
716
|
-
if (verdict === "pass") updatedChange = await stores.changes.transition(changeId, "ready", {
|
|
912
|
+
if (verdict === "pass" && change.state === "verifying") updatedChange = await stores.changes.transition(changeId, "ready", {
|
|
717
913
|
patch: {
|
|
718
914
|
evidence,
|
|
719
915
|
confidence
|
|
720
916
|
},
|
|
721
917
|
reason: summary
|
|
722
918
|
});
|
|
919
|
+
else if (verdict === "fail" && change.state === "ready") updatedChange = await stores.changes.transition(changeId, "verifying", { reason: summary });
|
|
723
920
|
let finished;
|
|
724
921
|
if (verdict === "pass") {
|
|
725
922
|
await stores.tasks.transition(taskId, "verifying", { reason: summary });
|
|
726
923
|
finished = await stores.tasks.transition(taskId, "succeeded");
|
|
727
924
|
} else finished = await stores.tasks.transition(taskId, "failed", { reason: summary });
|
|
728
|
-
|
|
729
|
-
|
|
925
|
+
const sourceTaskId = task.origin.parentTaskId ?? change.taskId;
|
|
926
|
+
if (sourceTaskId !== void 0) {
|
|
927
|
+
if ((await stores.tasks.get(sourceTaskId))?.state === "verifying") await stores.tasks.transition(sourceTaskId, verdict === "pass" ? "succeeded" : "failed", verdict === "pass" ? {} : { reason: `verification failed: ${summary}` }).catch(() => void 0);
|
|
730
928
|
}
|
|
929
|
+
await options.presentResult?.({
|
|
930
|
+
taskId,
|
|
931
|
+
changeId,
|
|
932
|
+
verdict,
|
|
933
|
+
summary
|
|
934
|
+
});
|
|
731
935
|
return {
|
|
732
936
|
recorded: true,
|
|
733
937
|
verdict,
|
|
@@ -755,6 +959,28 @@ const recordChangeInputSchema = z.object({
|
|
|
755
959
|
confidence: levelSchema.describe("How certain you are the change is correct."),
|
|
756
960
|
summary: z.string().min(1).describe("One-sentence description of the change.")
|
|
757
961
|
});
|
|
962
|
+
async function createAndPresentQuestion(stores, parent, question, presentQuestion) {
|
|
963
|
+
const task = await stores.tasks.create({
|
|
964
|
+
repositoryIds: parent.repositoryIds,
|
|
965
|
+
kind: "question",
|
|
966
|
+
origin: { parentTaskId: parent.id },
|
|
967
|
+
replyTo: parent.replyTo,
|
|
968
|
+
question
|
|
969
|
+
});
|
|
970
|
+
const waiting = await stores.tasks.transition(task.id, "needs_human");
|
|
971
|
+
try {
|
|
972
|
+
if (parent.state === "running") await stores.tasks.transition(parent.id, "needs_human", {
|
|
973
|
+
reason: `waiting for answer to question ${waiting.id}`,
|
|
974
|
+
questionTaskId: waiting.id
|
|
975
|
+
});
|
|
976
|
+
await presentQuestion(waiting);
|
|
977
|
+
} catch (error) {
|
|
978
|
+
if ((await stores.tasks.get(parent.id))?.state === "needs_human") await stores.tasks.transition(parent.id, "queued", { reason: `delivery of question ${waiting.id} failed` });
|
|
979
|
+
if ((await stores.tasks.get(waiting.id))?.state === "needs_human") await stores.tasks.transition(waiting.id, "cancelled", { reason: "question delivery failed" });
|
|
980
|
+
throw error;
|
|
981
|
+
}
|
|
982
|
+
return waiting;
|
|
983
|
+
}
|
|
758
984
|
/**
|
|
759
985
|
* The factory's store operations wrapped as eve tools. Expose a tool to an
|
|
760
986
|
* agent by re-exporting it from a file under agent/tools/, e.g.
|
|
@@ -763,6 +989,7 @@ const recordChangeInputSchema = z.object({
|
|
|
763
989
|
*/
|
|
764
990
|
function factoryTools(stores, options = {}) {
|
|
765
991
|
const recordChangeGate = options.recordChangeGate;
|
|
992
|
+
const verification = options.verification ?? "required";
|
|
766
993
|
return {
|
|
767
994
|
get_task: defineTool({
|
|
768
995
|
description: "Read one factory task by id: its kind, state, repositories, origin, and results.",
|
|
@@ -790,7 +1017,7 @@ function factoryTools(stores, options = {}) {
|
|
|
790
1017
|
}
|
|
791
1018
|
}),
|
|
792
1019
|
record_change: defineTool({
|
|
793
|
-
description: "Record the pull request produced for a code_change task. Creates the change, links it to the task, and moves the task to verifying.",
|
|
1020
|
+
description: verification === "required" ? "Record the pull request produced for a code_change task. Creates the change, links it to the task, and moves the task to verifying." : "Record the pull request produced for a code_change task. Creates the change and marks it ready for human review without independent verification.",
|
|
794
1021
|
inputSchema: recordChangeInputSchema,
|
|
795
1022
|
...recordChangeGate === void 0 ? {} : { approval: async ({ toolInput }) => {
|
|
796
1023
|
const parsed = recordChangeInputSchema.safeParse(toolInput);
|
|
@@ -805,28 +1032,51 @@ function factoryTools(stores, options = {}) {
|
|
|
805
1032
|
};
|
|
806
1033
|
} },
|
|
807
1034
|
async execute({ taskId, repositoryId, prNumber, prUrl, risk, confidence, summary }) {
|
|
808
|
-
const
|
|
809
|
-
|
|
1035
|
+
const currentTask = await stores.tasks.get(taskId);
|
|
1036
|
+
if (currentTask === null) throw new Error(`task ${taskId} does not exist`);
|
|
1037
|
+
if (currentTask.state !== "running" && currentTask.state !== "verifying" && currentTask.state !== "succeeded") throw new Error(`task ${taskId} is ${currentTask.state}, not running`);
|
|
1038
|
+
const linkedChange = currentTask.changeId === void 0 ? void 0 : await stores.changes.get(currentTask.changeId);
|
|
1039
|
+
if (currentTask.changeId !== void 0 && linkedChange === null) throw new Error(`change ${currentTask.changeId} does not exist`);
|
|
1040
|
+
let change = linkedChange ?? (await stores.changes.list()).find((candidate) => candidate.taskId === taskId);
|
|
1041
|
+
if (change === void 0) {
|
|
1042
|
+
if (currentTask.state !== "running") throw new Error(`task ${taskId} has no recorded change`);
|
|
1043
|
+
change = await stores.changes.create({
|
|
1044
|
+
repositoryId,
|
|
1045
|
+
taskId,
|
|
1046
|
+
prNumber,
|
|
1047
|
+
prUrl,
|
|
1048
|
+
author: "factory",
|
|
1049
|
+
risk,
|
|
1050
|
+
confidence,
|
|
1051
|
+
evidence: {
|
|
1052
|
+
reproVerified: false,
|
|
1053
|
+
testsAdded: 0,
|
|
1054
|
+
checksGreen: false,
|
|
1055
|
+
attackPassed: false,
|
|
1056
|
+
scopeMatches: false
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
if (currentTask.state === "running") await options.presentChange?.({
|
|
810
1061
|
taskId,
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
risk,
|
|
815
|
-
confidence,
|
|
816
|
-
evidence: {
|
|
817
|
-
reproVerified: false,
|
|
818
|
-
testsAdded: 0,
|
|
819
|
-
checksGreen: false,
|
|
820
|
-
attackPassed: false,
|
|
821
|
-
scopeMatches: false
|
|
822
|
-
}
|
|
1062
|
+
changeId: change.id,
|
|
1063
|
+
verification,
|
|
1064
|
+
summary
|
|
823
1065
|
});
|
|
1066
|
+
let task = currentTask;
|
|
1067
|
+
if (task.state === "running") task = await stores.tasks.transition(taskId, "verifying", {
|
|
1068
|
+
patch: { changeId: change.id },
|
|
1069
|
+
reason: summary
|
|
1070
|
+
});
|
|
1071
|
+
if (verification === "required") return {
|
|
1072
|
+
change,
|
|
1073
|
+
task
|
|
1074
|
+
};
|
|
1075
|
+
if (change.state === "verifying") change = await stores.changes.transition(change.id, "ready", { reason: "independent verification is not configured" });
|
|
1076
|
+
if (task.state === "verifying") task = await stores.tasks.transition(taskId, "succeeded", { reason: "change recorded for human review without independent verification" });
|
|
824
1077
|
return {
|
|
825
1078
|
change,
|
|
826
|
-
task
|
|
827
|
-
patch: { changeId: change.id },
|
|
828
|
-
reason: summary
|
|
829
|
-
})
|
|
1079
|
+
task
|
|
830
1080
|
};
|
|
831
1081
|
}
|
|
832
1082
|
}),
|
|
@@ -857,40 +1107,42 @@ function factoryTools(stores, options = {}) {
|
|
|
857
1107
|
};
|
|
858
1108
|
}
|
|
859
1109
|
}),
|
|
860
|
-
|
|
861
|
-
description: "
|
|
1110
|
+
request_human_input: defineTool({
|
|
1111
|
+
description: "Request human input or action and pause your task until the human replies. The parent task determines the repositories and reply destination.",
|
|
862
1112
|
inputSchema: z.object({
|
|
863
|
-
|
|
864
|
-
reason: z.string().min(1).describe("What is blocking, written for the human.")
|
|
865
|
-
}),
|
|
866
|
-
execute({ taskId, reason }) {
|
|
867
|
-
return stores.tasks.transition(taskId, "needs_human", { reason });
|
|
868
|
-
}
|
|
869
|
-
}),
|
|
870
|
-
ask_question: defineTool({
|
|
871
|
-
description: "Raise a question a human operator must answer. Creates a question task that waits for the answer.",
|
|
872
|
-
inputSchema: z.object({
|
|
873
|
-
repositoryIds: z.array(repositoryIdSchema).min(1),
|
|
874
|
-
parentTaskId: taskIdSchema.optional().describe("Task the answer unblocks, when there is one."),
|
|
875
|
-
channel: z.string().min(1).describe("Reply connector, e.g. github."),
|
|
876
|
-
address: z.string().min(1).describe("Reply address, e.g. acme/web#41."),
|
|
1113
|
+
parentTaskId: taskIdSchema.describe("Your running task, which the answer will unblock."),
|
|
877
1114
|
question: taskQuestionSchema.omit({
|
|
878
1115
|
answer: true,
|
|
879
1116
|
answeredBy: true
|
|
880
1117
|
})
|
|
881
1118
|
}),
|
|
882
|
-
async execute({
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1119
|
+
async execute({ parentTaskId, question }) {
|
|
1120
|
+
if (options.presentQuestion === void 0) throw new Error("request_human_input requires a configured question presenter.");
|
|
1121
|
+
const parent = await stores.tasks.get(parentTaskId);
|
|
1122
|
+
if (parent === null) throw new Error(`Parent task ${parentTaskId} does not exist.`);
|
|
1123
|
+
if (parent.state !== "running" && parent.state !== "needs_human") throw new Error(`Parent task ${parentTaskId} is ${parent.state}, not running.`);
|
|
1124
|
+
return createAndPresentQuestion(stores, parent, question, options.presentQuestion);
|
|
1125
|
+
}
|
|
1126
|
+
}),
|
|
1127
|
+
answer_question: defineTool({
|
|
1128
|
+
description: "Record the authenticated user's answer to a waiting question and requeue the parent specialist task. A blocked parent task id is also accepted and resolves its persisted QuestionTask id.",
|
|
1129
|
+
inputSchema: z.object({
|
|
1130
|
+
questionTaskId: taskIdSchema,
|
|
1131
|
+
answer: z.string().min(1).max(4e3)
|
|
1132
|
+
}),
|
|
1133
|
+
async execute({ questionTaskId, answer }, ctx) {
|
|
1134
|
+
const requestedTask = await stores.tasks.get(questionTaskId);
|
|
1135
|
+
const resolvedQuestionTaskId = requestedTask?.kind === "question" ? requestedTask.id : requestedTask?.questionTaskId;
|
|
1136
|
+
const questionTask = resolvedQuestionTaskId === void 0 ? null : await stores.tasks.get(resolvedQuestionTaskId);
|
|
1137
|
+
if (questionTask === null || questionTask.kind !== "question" || questionTask.question === void 0) throw new Error(`Question task ${questionTaskId} does not exist.`);
|
|
1138
|
+
const attributes = ctx.session.auth.initiator?.attributes;
|
|
1139
|
+
if (attributes?.factoryReplyChannel !== questionTask.replyTo.channel || attributes.factoryReplyAddress !== questionTask.replyTo.address) throw new Error("Question reply does not match the authenticated conversation.");
|
|
1140
|
+
const answeredBy = ctx.session.auth.initiator?.principalId ?? "operator";
|
|
1141
|
+
return stores.tasks.answerQuestion({
|
|
1142
|
+
questionTaskId: questionTask.id,
|
|
1143
|
+
answer,
|
|
1144
|
+
answeredBy
|
|
892
1145
|
});
|
|
893
|
-
return stores.tasks.transition(task.id, "needs_human");
|
|
894
1146
|
}
|
|
895
1147
|
}),
|
|
896
1148
|
append_receipt: defineTool({
|
|
@@ -910,25 +1162,62 @@ function factoryTools(stores, options = {}) {
|
|
|
910
1162
|
})
|
|
911
1163
|
};
|
|
912
1164
|
}
|
|
1165
|
+
const terminalTaskStates = /* @__PURE__ */ new Set([
|
|
1166
|
+
"succeeded",
|
|
1167
|
+
"failed",
|
|
1168
|
+
"cancelled"
|
|
1169
|
+
]);
|
|
913
1170
|
/**
|
|
914
1171
|
* Observe-only eve hooks that keep the ledger honest about session outcomes.
|
|
915
1172
|
* Mount by re-exporting from agent/hooks/, e.g.
|
|
916
|
-
* `export default factoryHooks(stores).factory;`.
|
|
1173
|
+
* `export default factoryHooks(stores, { presentQuestion }).factory;`.
|
|
1174
|
+
*
|
|
1175
|
+
* step.completed: records the step's tokens and AI Gateway cost on the bound
|
|
1176
|
+
* task. Delivery is at least once; recordUsage dedupes by (turnId, stepIndex).
|
|
917
1177
|
*
|
|
918
|
-
* turn.completed:
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
1178
|
+
* turn.completed: settles the bound task's recorded usage if the task is
|
|
1179
|
+
* already terminal (the sweep handles tasks that end later), then moves any
|
|
1180
|
+
* still-running task at this session's reply address behind a surfaced human
|
|
1181
|
+
* question, because the agent finished talking without recording an outcome.
|
|
1182
|
+
*
|
|
1183
|
+
* Every store call is swallowed: a hook throw fails the turn.
|
|
922
1184
|
*/
|
|
923
|
-
function factoryHooks(stores) {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1185
|
+
function factoryHooks(stores, options) {
|
|
1186
|
+
const taskIdAttribute = options.taskIdAttribute ?? "factoryTaskId";
|
|
1187
|
+
function boundTaskId(ctx) {
|
|
1188
|
+
const parsed = taskIdSchema.safeParse(ctx.session.auth.initiator?.attributes[taskIdAttribute]);
|
|
1189
|
+
return parsed.success ? parsed.data : void 0;
|
|
1190
|
+
}
|
|
1191
|
+
async function settleIfTerminal(taskId) {
|
|
1192
|
+
const task = await stores.tasks.get(taskId);
|
|
1193
|
+
if (task?.usage === void 0 || task.settledUsd !== void 0 || !terminalTaskStates.has(task.state)) return;
|
|
1194
|
+
await stores.tasks.settle(task.id, { totalUsd: task.usage.costUsd }, { reason: "measured usage settled at turn end" });
|
|
1195
|
+
}
|
|
1196
|
+
return { factory: defineHook({ events: {
|
|
1197
|
+
async "step.completed"(event, ctx) {
|
|
1198
|
+
const taskId = boundTaskId(ctx);
|
|
1199
|
+
const usage = event.data.usage;
|
|
1200
|
+
if (taskId === void 0 || usage === void 0) return;
|
|
1201
|
+
await stores.tasks.recordUsage(taskId, {
|
|
1202
|
+
turnId: event.data.turnId,
|
|
1203
|
+
stepIndex: event.data.stepIndex,
|
|
1204
|
+
...usage
|
|
1205
|
+
}).catch(() => void 0);
|
|
1206
|
+
},
|
|
1207
|
+
async "turn.completed"(_event, ctx) {
|
|
1208
|
+
const taskId = boundTaskId(ctx);
|
|
1209
|
+
if (taskId !== void 0) await settleIfTerminal(taskId).catch(() => void 0);
|
|
1210
|
+
const address = ctx.channel.continuationToken;
|
|
1211
|
+
if (address === void 0) return;
|
|
1212
|
+
const abandoned = (await stores.tasks.list().catch(() => [])).filter((task) => task.state === "running" && task.replyTo.address === address);
|
|
1213
|
+
for (const task of abandoned) await createAndPresentQuestion(stores, task, {
|
|
1214
|
+
text: "The worker stopped without recording an outcome. Please review the task and reply when Factory should retry it.",
|
|
1215
|
+
options: ["Retry the task"]
|
|
1216
|
+
}, options.presentQuestion).catch(() => void 0);
|
|
1217
|
+
}
|
|
1218
|
+
} }) };
|
|
930
1219
|
}
|
|
931
1220
|
//#endregion
|
|
932
|
-
export { deepsecScanTool, factoryHooks, factoryTools, fetchPullRequestTool, findingFromDeepsec, invokeAgentTool, mergeConflictPayloadSchema, mergeConflictTools, postReviewTool, recordFinding, recordFindingTool, recordVerificationTool, reviewCommentTools };
|
|
1221
|
+
export { deepsecScanTool, delegateAgentTool, factoryHooks, factoryTools, fetchPullRequestTool, findingFromDeepsec, invokeAgentTool, mergeConflictPayloadSchema, mergeConflictTools, postReviewTool, recordFinding, recordFindingTool, recordVerificationTool, reviewCommentTools };
|
|
933
1222
|
|
|
934
1223
|
//# sourceMappingURL=index.mjs.map
|