@vercel/factory 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval-CwhTP1XB.mjs +269 -0
- package/dist/approval-CwhTP1XB.mjs.map +1 -0
- package/dist/blob/index.d.mts +1 -1
- package/dist/build-factory.d.mts +1 -1
- package/dist/build-factory.mjs +1 -1
- package/dist/change-CzEAoFVE.mjs +620 -0
- package/dist/change-CzEAoFVE.mjs.map +1 -0
- package/dist/connectors/slack.d.mts +4 -4
- package/dist/connectors/slack.mjs +81 -30
- package/dist/connectors/slack.mjs.map +1 -1
- package/dist/{approval-CIXB8IF8.d.mts → dispatch-B77cuPxh.d.mts} +290 -99
- package/dist/{driver-CrZsy7sm.d.mts → driver-BJutB0PE.d.mts} +2 -2
- package/dist/driver-Bbd25rNM.mjs.map +1 -1
- package/dist/eve/index.d.mts +546 -120
- package/dist/eve/index.mjs +358 -82
- package/dist/eve/index.mjs.map +1 -1
- package/dist/{factory-config-ifd3JxiU.mjs → factory-config-BJcjsPKl.mjs} +42 -5
- package/dist/factory-config-BJcjsPKl.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-DXsxboUL.d.mts} +25 -2
- package/dist/github.d.mts +2 -2
- package/dist/github.mjs +47 -1
- package/dist/github.mjs.map +1 -1
- package/dist/index.d.mts +119 -59
- package/dist/index.mjs +289 -330
- package/dist/index.mjs.map +1 -1
- package/dist/postgres/index.d.mts +1 -1
- package/package.json +1 -1
- package/dist/approval-DdvEKLiE.mjs +0 -132
- package/dist/approval-DdvEKLiE.mjs.map +0 -1
- 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/eve/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { i as repositoryIdSchema, o as signalIdSchema, p as vocabularySchema, s as taskIdSchema, t as changeIdSchema } from "../id-DwCihQr4.mjs";
|
|
2
|
-
import { c as findingSchema, i as requiresApproval, o as findingFingerprint, r as defaultApprovalPolicy } from "../approval-
|
|
3
|
-
import {
|
|
2
|
+
import { b as taskQuestionSchema, c as findingSchema, i as requiresApproval, o as findingFingerprint, r as defaultApprovalPolicy } from "../approval-CwhTP1XB.mjs";
|
|
3
|
+
import { b as createCoordination, c as dispatchTask, i as levelSchema, t as changeEvidenceSchema } from "../change-CzEAoFVE.mjs";
|
|
4
4
|
import { answerReviewComment, readPullRequestHead, readReviewComment, submitPullRequestReview } from "../github.mjs";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
6
7
|
import { defineHook } from "eve/hooks";
|
|
7
8
|
import { defineTool } from "eve/tools";
|
|
8
9
|
//#region src/eve/conflict.ts
|
|
@@ -369,7 +370,7 @@ function fetchPullRequestTool(stores, options) {
|
|
|
369
370
|
const repoPath = options.repoPath ?? "repo";
|
|
370
371
|
const depth = options.depth ?? 50;
|
|
371
372
|
return defineTool({
|
|
372
|
-
description: "Fetch the pull request
|
|
373
|
+
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.",
|
|
373
374
|
inputSchema: z.object({ changeId: changeIdSchema.describe("The change under review, from your starting message.") }),
|
|
374
375
|
async execute({ changeId }, ctx) {
|
|
375
376
|
const change = await stores.changes.get(changeId);
|
|
@@ -388,19 +389,31 @@ function fetchPullRequestTool(stores, options) {
|
|
|
388
389
|
};
|
|
389
390
|
const sandbox = await ctx.getSandbox();
|
|
390
391
|
const tracking = `refs/remotes/origin/pr-${change.prNumber}`;
|
|
392
|
+
const baseTracking = `refs/remotes/origin/${repository.defaultBranch}`;
|
|
391
393
|
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}`);
|
|
394
|
+
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
395
|
if (typeof result.exitCode === "number" && result.exitCode !== 0) return {
|
|
394
396
|
fetched: false,
|
|
395
397
|
reason: `git fetch of pull request ${change.prNumber} failed: ${(result.stderr ?? "").trim()}`
|
|
396
398
|
};
|
|
397
399
|
const head = await sandbox.run({ command: `${git} rev-parse HEAD` });
|
|
400
|
+
const base = await sandbox.run({ command: `${git} rev-parse ${baseTracking}` });
|
|
401
|
+
const mergeBase = await sandbox.run({ command: `${git} merge-base ${baseTracking} pr` });
|
|
402
|
+
if (typeof mergeBase.exitCode === "number" && mergeBase.exitCode !== 0) return {
|
|
403
|
+
fetched: false,
|
|
404
|
+
reason: `could not determine the pull request merge base: ${(mergeBase.stderr ?? "").trim()}`
|
|
405
|
+
};
|
|
406
|
+
const mergeBaseSha = mergeBase.stdout.trim();
|
|
398
407
|
return {
|
|
399
408
|
fetched: true,
|
|
400
409
|
prNumber: change.prNumber,
|
|
401
410
|
prUrl: change.prUrl,
|
|
402
411
|
branch: "pr",
|
|
403
|
-
headSha: head.stdout.trim()
|
|
412
|
+
headSha: head.stdout.trim(),
|
|
413
|
+
baseBranch: repository.defaultBranch,
|
|
414
|
+
baseSha: base.stdout.trim(),
|
|
415
|
+
mergeBaseSha,
|
|
416
|
+
diffCommand: `${git} diff ${mergeBaseSha}..pr`
|
|
404
417
|
};
|
|
405
418
|
}
|
|
406
419
|
});
|
|
@@ -439,6 +452,7 @@ function invokeAgentTool(stores, options) {
|
|
|
439
452
|
});
|
|
440
453
|
const running = await dispatchTask(stores, task.id, {
|
|
441
454
|
maxLaunchAttempts: 1,
|
|
455
|
+
budget: options.budget?.(),
|
|
442
456
|
launch: async (dispatched) => {
|
|
443
457
|
const headers = {
|
|
444
458
|
"content-type": "application/json",
|
|
@@ -474,20 +488,147 @@ function invokeAgentTool(stores, options) {
|
|
|
474
488
|
}
|
|
475
489
|
});
|
|
476
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* Give a conversational agent one bounded delegation surface backed by the
|
|
493
|
+
* application's route registry. The caller selects a declared route; trusted
|
|
494
|
+
* code owns task creation, budget enforcement, and the fresh target session.
|
|
495
|
+
*/
|
|
496
|
+
function delegateAgentTool(stores, options) {
|
|
497
|
+
const doFetch = options.fetch ?? fetch;
|
|
498
|
+
const routeSummary = options.routes.routes.map((route) => `${route.id}: ${route.description}`).join("\n");
|
|
499
|
+
return defineTool({
|
|
500
|
+
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,
|
|
501
|
+
inputSchema: z.object({
|
|
502
|
+
routeId: z.string().min(1).describe("One route id listed in this tool's description."),
|
|
503
|
+
repositoryIds: z.array(repositoryIdSchema).min(1),
|
|
504
|
+
signalId: signalIdSchema.optional().describe("Pending external signal this delegation accepts."),
|
|
505
|
+
parentTaskId: taskIdSchema.optional().describe("Existing task this work continues."),
|
|
506
|
+
changeId: changeIdSchema.optional().describe("Existing change the specialist should inspect."),
|
|
507
|
+
channel: z.string().min(1).describe("Originating conversation channel."),
|
|
508
|
+
address: z.string().min(1).describe("Originating conversation reply address."),
|
|
509
|
+
brief: z.string().min(1).max(4e3).describe("Facts and desired outcome, without hidden reasoning.")
|
|
510
|
+
}),
|
|
511
|
+
async execute(input, ctx) {
|
|
512
|
+
const route = options.routes.get(input.routeId);
|
|
513
|
+
if (route === void 0) throw new Error(`Unknown agent route ${input.routeId}.`);
|
|
514
|
+
const replyTo = {
|
|
515
|
+
channel: input.channel,
|
|
516
|
+
address: input.address
|
|
517
|
+
};
|
|
518
|
+
const attributes = ctx.session.auth.initiator?.attributes;
|
|
519
|
+
const repositoryAttribute = attributes?.factoryRepositoryIds;
|
|
520
|
+
const boundRepositoryIds = typeof repositoryAttribute === "string" ? repositoryAttribute.split(",").filter(Boolean) : repositoryAttribute;
|
|
521
|
+
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.");
|
|
522
|
+
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.");
|
|
523
|
+
for (const repositoryId of input.repositoryIds) {
|
|
524
|
+
const repository = await stores.repositories.get(repositoryId);
|
|
525
|
+
if (repository === null || !repository.enabled) throw new Error(`Repository ${repositoryId} is not enabled in this factory.`);
|
|
526
|
+
}
|
|
527
|
+
if (input.signalId !== void 0) {
|
|
528
|
+
const signal = await stores.signals.get(input.signalId);
|
|
529
|
+
if (signal === null) throw new Error(`Signal ${input.signalId} does not exist.`);
|
|
530
|
+
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.");
|
|
531
|
+
if (signal.outcome === "pending") await stores.signals.decide(signal.id, "accepted", { reason: `Foreman selected ${route.id}.` });
|
|
532
|
+
else if (signal.outcome !== "accepted") throw new Error(`Signal ${signal.id} is already ${signal.outcome}.`);
|
|
533
|
+
}
|
|
534
|
+
const operator = ctx.session.auth.initiator?.principalId ?? "foreman";
|
|
535
|
+
if (route.taskKind === "code_change" && input.repositoryIds.length > 1) {
|
|
536
|
+
const [first, second, ...rest] = input.repositoryIds;
|
|
537
|
+
if (first === void 0 || second === void 0) throw new Error("Coordination requires two repositories.");
|
|
538
|
+
const coordination = await createCoordination(stores, {
|
|
539
|
+
repositoryIds: [
|
|
540
|
+
first,
|
|
541
|
+
second,
|
|
542
|
+
...rest
|
|
543
|
+
],
|
|
544
|
+
replyTo,
|
|
545
|
+
...input.signalId === void 0 ? {} : { dedupeKey: `route:${input.signalId}:${route.id}` },
|
|
546
|
+
...input.signalId === void 0 ? { operator } : { signalId: input.signalId }
|
|
547
|
+
});
|
|
548
|
+
await Promise.all(coordination.children.filter((child) => child.state === "queued").map((child) => dispatchRouteTask(stores, options, doFetch, route, child, input.changeId, input.brief)));
|
|
549
|
+
return {
|
|
550
|
+
taskId: coordination.parent.id,
|
|
551
|
+
childTaskIds: coordination.children.map(({ id }) => id),
|
|
552
|
+
routeId: route.id,
|
|
553
|
+
agent: route.agent,
|
|
554
|
+
state: coordination.parent.state
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
const task = await stores.tasks.create({
|
|
558
|
+
repositoryIds: input.repositoryIds,
|
|
559
|
+
kind: route.taskKind,
|
|
560
|
+
origin: input.signalId !== void 0 ? { signalId: input.signalId } : input.parentTaskId !== void 0 ? { parentTaskId: input.parentTaskId } : { operator },
|
|
561
|
+
replyTo,
|
|
562
|
+
changeId: input.changeId,
|
|
563
|
+
...input.signalId === void 0 ? {} : { dedupeKey: `route:${input.signalId}:${route.id}` }
|
|
564
|
+
});
|
|
565
|
+
const running = task.state === "queued" ? await dispatchRouteTask(stores, options, doFetch, route, task, input.changeId, input.brief) : task;
|
|
566
|
+
return {
|
|
567
|
+
taskId: running.id,
|
|
568
|
+
routeId: route.id,
|
|
569
|
+
agent: route.agent,
|
|
570
|
+
state: running.state
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
async function dispatchRouteTask(stores, options, doFetch, route, task, changeId, brief) {
|
|
576
|
+
return dispatchTask(stores, task.id, {
|
|
577
|
+
maxLaunchAttempts: 1,
|
|
578
|
+
budget: options.budget?.(route),
|
|
579
|
+
launch: async (dispatched) => {
|
|
580
|
+
const headers = {
|
|
581
|
+
"content-type": "application/json",
|
|
582
|
+
...await options.headers?.(dispatched, route)
|
|
583
|
+
};
|
|
584
|
+
const token = await options.token?.(route);
|
|
585
|
+
if (token !== void 0) headers.authorization = `Bearer ${token}`;
|
|
586
|
+
const message = [
|
|
587
|
+
`You are assigned factory task ${dispatched.id} through route ${route.id}.`,
|
|
588
|
+
changeId === void 0 ? null : `Change under review: ${changeId}.`,
|
|
589
|
+
brief,
|
|
590
|
+
"Read the task first and return exactly one bounded Factory outcome."
|
|
591
|
+
].filter((line) => line !== null).join("\n\n");
|
|
592
|
+
const response = await doFetch(`${options.targetUrl(route)}/eve/v1/session`, {
|
|
593
|
+
method: "POST",
|
|
594
|
+
headers,
|
|
595
|
+
body: JSON.stringify({
|
|
596
|
+
message,
|
|
597
|
+
operationId: `factory-task:${dispatched.id}`
|
|
598
|
+
})
|
|
599
|
+
});
|
|
600
|
+
if (!response.ok) throw new Error(`${route.agent} refused the session: ${response.status}`);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
477
604
|
//#endregion
|
|
478
605
|
//#region src/eve/post-review.ts
|
|
479
606
|
const reviewCommentSchema = z.strictObject({
|
|
607
|
+
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."),
|
|
608
|
+
title: z.string().min(1).max(200).optional().describe("Short finding title."),
|
|
480
609
|
path: z.string().min(1).max(500).describe("Repository-relative file path."),
|
|
481
610
|
line: z.number().int().min(1).describe("Line in the pull request's new version."),
|
|
482
611
|
body: z.string().min(1).max(5e3).describe("Markdown explaining the finding and a concrete fix.")
|
|
483
612
|
});
|
|
613
|
+
const reviewCommentsSchema = z.array(reviewCommentSchema).max(50).default([]).superRefine((comments, context) => {
|
|
614
|
+
const seen = /* @__PURE__ */ new Set();
|
|
615
|
+
comments.forEach((comment, index) => {
|
|
616
|
+
if (comment.key === void 0) return;
|
|
617
|
+
if (seen.has(comment.key)) context.addIssue({
|
|
618
|
+
code: "custom",
|
|
619
|
+
message: `duplicate finding key: ${comment.key}`,
|
|
620
|
+
path: [index, "key"]
|
|
621
|
+
});
|
|
622
|
+
seen.add(comment.key);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
484
625
|
/**
|
|
485
626
|
* Publish an independent review of an existing pull request and finish the
|
|
486
627
|
* review task without changing the pull request's factory lifecycle.
|
|
487
628
|
*/
|
|
488
629
|
function postReviewTool(stores, options) {
|
|
489
630
|
return defineTool({
|
|
490
|
-
description: "
|
|
631
|
+
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
632
|
inputSchema: z.object({
|
|
492
633
|
taskId: taskIdSchema.describe("Your code_review task."),
|
|
493
634
|
changeId: changeIdSchema.describe("The pull request change from your starting message."),
|
|
@@ -498,7 +639,7 @@ function postReviewTool(stores, options) {
|
|
|
498
639
|
]),
|
|
499
640
|
headSha: z.string().regex(/^[0-9a-f]{40}$/).describe("Commit returned by fetch_pull_request."),
|
|
500
641
|
summary: z.string().min(1).max(5e3).describe("Markdown summary of what you reviewed and found."),
|
|
501
|
-
comments:
|
|
642
|
+
comments: reviewCommentsSchema
|
|
502
643
|
}),
|
|
503
644
|
async execute({ taskId, changeId, verdict, headSha, summary, comments }) {
|
|
504
645
|
const task = await stores.tasks.get(taskId);
|
|
@@ -524,36 +665,68 @@ function postReviewTool(stores, options) {
|
|
|
524
665
|
recorded: false,
|
|
525
666
|
reason: `repository ${change.repositoryId} does not exist`
|
|
526
667
|
};
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
668
|
+
const token = await options.tokenForRepository(repository.slug);
|
|
669
|
+
const completedComments = comments.map((comment) => {
|
|
670
|
+
const key = comment.key ?? legacyFindingKey(comment);
|
|
671
|
+
return {
|
|
672
|
+
...comment,
|
|
673
|
+
key,
|
|
674
|
+
title: comment.title ?? "Review finding",
|
|
675
|
+
fingerprint: createHash("sha256").update(`${repository.slug.toLowerCase()}#${change.prNumber}:${key}`).digest("hex")
|
|
676
|
+
};
|
|
536
677
|
});
|
|
537
|
-
|
|
678
|
+
const completed = {
|
|
538
679
|
taskId,
|
|
539
680
|
changeId,
|
|
540
681
|
repository: repository.slug,
|
|
682
|
+
pullRequestNumber: change.prNumber,
|
|
541
683
|
headSha,
|
|
542
684
|
verdict,
|
|
543
685
|
summary,
|
|
544
|
-
comments
|
|
545
|
-
|
|
546
|
-
|
|
686
|
+
comments: completedComments
|
|
687
|
+
};
|
|
688
|
+
const publish = options.publishReviews !== false;
|
|
689
|
+
let review;
|
|
690
|
+
if (publish) {
|
|
691
|
+
review = await submitPullRequestReview({
|
|
692
|
+
repository: repository.slug,
|
|
693
|
+
token,
|
|
694
|
+
fetch: options.fetch,
|
|
695
|
+
pullRequestNumber: change.prNumber,
|
|
696
|
+
commitId: headSha,
|
|
697
|
+
event: verdict === "approve" ? "APPROVE" : verdict === "request_changes" ? "REQUEST_CHANGES" : "COMMENT",
|
|
698
|
+
body: summary,
|
|
699
|
+
comments: completedComments
|
|
700
|
+
});
|
|
701
|
+
await options.onPublished?.({
|
|
702
|
+
...completed,
|
|
703
|
+
review
|
|
704
|
+
});
|
|
705
|
+
} else {
|
|
706
|
+
const head = await readPullRequestHead({
|
|
707
|
+
repository: repository.slug,
|
|
708
|
+
token,
|
|
709
|
+
fetch: options.fetch,
|
|
710
|
+
pullRequestNumber: change.prNumber
|
|
711
|
+
});
|
|
712
|
+
if (head.sha !== headSha) throw new Error(`pull request changed after review: expected ${headSha}, found ${head.sha}`);
|
|
713
|
+
}
|
|
714
|
+
await options.onCompleted?.(completed);
|
|
547
715
|
await stores.tasks.transition(taskId, "verifying", { reason: summary });
|
|
716
|
+
const finished = await stores.tasks.transition(taskId, "succeeded");
|
|
548
717
|
return {
|
|
549
718
|
recorded: true,
|
|
550
719
|
verdict,
|
|
551
|
-
review,
|
|
552
|
-
|
|
720
|
+
published: review !== void 0,
|
|
721
|
+
...review === void 0 ? {} : { review },
|
|
722
|
+
taskState: finished.state
|
|
553
723
|
};
|
|
554
724
|
}
|
|
555
725
|
});
|
|
556
726
|
}
|
|
727
|
+
function legacyFindingKey(comment) {
|
|
728
|
+
return `legacy-${createHash("sha256").update(`${comment.path}:${comment.line}:${comment.body}`).digest("hex").slice(0, 24)}`;
|
|
729
|
+
}
|
|
557
730
|
//#endregion
|
|
558
731
|
//#region src/eve/review.ts
|
|
559
732
|
function reviewCommentTools(stores, options) {
|
|
@@ -686,7 +859,7 @@ function reviewCommentTools(stores, options) {
|
|
|
686
859
|
* as still in progress. Fail closed: the agent cannot mark a change ready
|
|
687
860
|
* without supplying every evidence field.
|
|
688
861
|
*/
|
|
689
|
-
function recordVerificationTool(stores) {
|
|
862
|
+
function recordVerificationTool(stores, options = {}) {
|
|
690
863
|
return defineTool({
|
|
691
864
|
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
865
|
inputSchema: z.object({
|
|
@@ -728,6 +901,12 @@ function recordVerificationTool(stores) {
|
|
|
728
901
|
if (change.taskId !== void 0) {
|
|
729
902
|
if ((await stores.tasks.get(change.taskId))?.state === "verifying") await stores.tasks.transition(change.taskId, verdict === "pass" ? "succeeded" : "failed", verdict === "pass" ? {} : { reason: `verification failed: ${summary}` }).catch(() => void 0);
|
|
730
903
|
}
|
|
904
|
+
await options.presentResult?.({
|
|
905
|
+
taskId,
|
|
906
|
+
changeId,
|
|
907
|
+
verdict,
|
|
908
|
+
summary
|
|
909
|
+
});
|
|
731
910
|
return {
|
|
732
911
|
recorded: true,
|
|
733
912
|
verdict,
|
|
@@ -755,6 +934,25 @@ const recordChangeInputSchema = z.object({
|
|
|
755
934
|
confidence: levelSchema.describe("How certain you are the change is correct."),
|
|
756
935
|
summary: z.string().min(1).describe("One-sentence description of the change.")
|
|
757
936
|
});
|
|
937
|
+
async function createAndPresentQuestion(stores, parent, question, presentQuestion) {
|
|
938
|
+
const task = await stores.tasks.create({
|
|
939
|
+
repositoryIds: parent.repositoryIds,
|
|
940
|
+
kind: "question",
|
|
941
|
+
origin: { parentTaskId: parent.id },
|
|
942
|
+
replyTo: parent.replyTo,
|
|
943
|
+
question
|
|
944
|
+
});
|
|
945
|
+
const waiting = await stores.tasks.transition(task.id, "needs_human");
|
|
946
|
+
try {
|
|
947
|
+
if (parent.state === "running") await stores.tasks.transition(parent.id, "needs_human", { reason: `waiting for answer to question ${waiting.id}` });
|
|
948
|
+
await presentQuestion(waiting);
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if ((await stores.tasks.get(parent.id))?.state === "needs_human") await stores.tasks.transition(parent.id, "queued", { reason: `delivery of question ${waiting.id} failed` });
|
|
951
|
+
if ((await stores.tasks.get(waiting.id))?.state === "needs_human") await stores.tasks.transition(waiting.id, "cancelled", { reason: "question delivery failed" });
|
|
952
|
+
throw error;
|
|
953
|
+
}
|
|
954
|
+
return waiting;
|
|
955
|
+
}
|
|
758
956
|
/**
|
|
759
957
|
* The factory's store operations wrapped as eve tools. Expose a tool to an
|
|
760
958
|
* agent by re-exporting it from a file under agent/tools/, e.g.
|
|
@@ -763,6 +961,7 @@ const recordChangeInputSchema = z.object({
|
|
|
763
961
|
*/
|
|
764
962
|
function factoryTools(stores, options = {}) {
|
|
765
963
|
const recordChangeGate = options.recordChangeGate;
|
|
964
|
+
const verification = options.verification ?? "required";
|
|
766
965
|
return {
|
|
767
966
|
get_task: defineTool({
|
|
768
967
|
description: "Read one factory task by id: its kind, state, repositories, origin, and results.",
|
|
@@ -790,7 +989,7 @@ function factoryTools(stores, options = {}) {
|
|
|
790
989
|
}
|
|
791
990
|
}),
|
|
792
991
|
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.",
|
|
992
|
+
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
993
|
inputSchema: recordChangeInputSchema,
|
|
795
994
|
...recordChangeGate === void 0 ? {} : { approval: async ({ toolInput }) => {
|
|
796
995
|
const parsed = recordChangeInputSchema.safeParse(toolInput);
|
|
@@ -805,28 +1004,51 @@ function factoryTools(stores, options = {}) {
|
|
|
805
1004
|
};
|
|
806
1005
|
} },
|
|
807
1006
|
async execute({ taskId, repositoryId, prNumber, prUrl, risk, confidence, summary }) {
|
|
808
|
-
const
|
|
809
|
-
|
|
1007
|
+
const currentTask = await stores.tasks.get(taskId);
|
|
1008
|
+
if (currentTask === null) throw new Error(`task ${taskId} does not exist`);
|
|
1009
|
+
if (currentTask.state !== "running" && currentTask.state !== "verifying" && currentTask.state !== "succeeded") throw new Error(`task ${taskId} is ${currentTask.state}, not running`);
|
|
1010
|
+
const linkedChange = currentTask.changeId === void 0 ? void 0 : await stores.changes.get(currentTask.changeId);
|
|
1011
|
+
if (currentTask.changeId !== void 0 && linkedChange === null) throw new Error(`change ${currentTask.changeId} does not exist`);
|
|
1012
|
+
let change = linkedChange ?? (await stores.changes.list()).find((candidate) => candidate.taskId === taskId);
|
|
1013
|
+
if (change === void 0) {
|
|
1014
|
+
if (currentTask.state !== "running") throw new Error(`task ${taskId} has no recorded change`);
|
|
1015
|
+
change = await stores.changes.create({
|
|
1016
|
+
repositoryId,
|
|
1017
|
+
taskId,
|
|
1018
|
+
prNumber,
|
|
1019
|
+
prUrl,
|
|
1020
|
+
author: "factory",
|
|
1021
|
+
risk,
|
|
1022
|
+
confidence,
|
|
1023
|
+
evidence: {
|
|
1024
|
+
reproVerified: false,
|
|
1025
|
+
testsAdded: 0,
|
|
1026
|
+
checksGreen: false,
|
|
1027
|
+
attackPassed: false,
|
|
1028
|
+
scopeMatches: false
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
if (currentTask.state === "running") await options.presentChange?.({
|
|
810
1033
|
taskId,
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
checksGreen: false,
|
|
820
|
-
attackPassed: false,
|
|
821
|
-
scopeMatches: false
|
|
822
|
-
}
|
|
1034
|
+
changeId: change.id,
|
|
1035
|
+
verification,
|
|
1036
|
+
summary
|
|
1037
|
+
});
|
|
1038
|
+
let task = currentTask;
|
|
1039
|
+
if (task.state === "running") task = await stores.tasks.transition(taskId, "verifying", {
|
|
1040
|
+
patch: { changeId: change.id },
|
|
1041
|
+
reason: summary
|
|
823
1042
|
});
|
|
1043
|
+
if (verification === "required") return {
|
|
1044
|
+
change,
|
|
1045
|
+
task
|
|
1046
|
+
};
|
|
1047
|
+
if (change.state === "verifying") change = await stores.changes.transition(change.id, "ready", { reason: "independent verification is not configured" });
|
|
1048
|
+
if (task.state === "verifying") task = await stores.tasks.transition(taskId, "succeeded", { reason: "change recorded for human review without independent verification" });
|
|
824
1049
|
return {
|
|
825
1050
|
change,
|
|
826
|
-
task
|
|
827
|
-
patch: { changeId: change.id },
|
|
828
|
-
reason: summary
|
|
829
|
-
})
|
|
1051
|
+
task
|
|
830
1052
|
};
|
|
831
1053
|
}
|
|
832
1054
|
}),
|
|
@@ -857,40 +1079,57 @@ function factoryTools(stores, options = {}) {
|
|
|
857
1079
|
};
|
|
858
1080
|
}
|
|
859
1081
|
}),
|
|
860
|
-
|
|
861
|
-
description: "
|
|
862
|
-
inputSchema: z.object({
|
|
863
|
-
taskId: taskIdSchema,
|
|
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.",
|
|
1082
|
+
request_human_input: defineTool({
|
|
1083
|
+
description: "Request human input or action and pause your task until the human replies. The parent task determines the repositories and reply destination.",
|
|
872
1084
|
inputSchema: z.object({
|
|
873
|
-
|
|
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."),
|
|
1085
|
+
parentTaskId: taskIdSchema.describe("Your running task, which the answer will unblock."),
|
|
877
1086
|
question: taskQuestionSchema.omit({
|
|
878
1087
|
answer: true,
|
|
879
1088
|
answeredBy: true
|
|
880
1089
|
})
|
|
881
1090
|
}),
|
|
882
|
-
async execute({
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1091
|
+
async execute({ parentTaskId, question }) {
|
|
1092
|
+
if (options.presentQuestion === void 0) throw new Error("request_human_input requires a configured question presenter.");
|
|
1093
|
+
const parent = await stores.tasks.get(parentTaskId);
|
|
1094
|
+
if (parent === null) throw new Error(`Parent task ${parentTaskId} does not exist.`);
|
|
1095
|
+
if (parent.state !== "running" && parent.state !== "needs_human") throw new Error(`Parent task ${parentTaskId} is ${parent.state}, not running.`);
|
|
1096
|
+
return createAndPresentQuestion(stores, parent, question, options.presentQuestion);
|
|
1097
|
+
}
|
|
1098
|
+
}),
|
|
1099
|
+
answer_question: defineTool({
|
|
1100
|
+
description: "Record the authenticated user's answer to a waiting question and requeue the parent specialist task.",
|
|
1101
|
+
inputSchema: z.object({
|
|
1102
|
+
questionTaskId: taskIdSchema,
|
|
1103
|
+
answer: z.string().min(1).max(4e3)
|
|
1104
|
+
}),
|
|
1105
|
+
async execute({ questionTaskId, answer }, ctx) {
|
|
1106
|
+
const questionTask = await stores.tasks.get(questionTaskId);
|
|
1107
|
+
if (questionTask === null || questionTask.kind !== "question" || questionTask.state !== "needs_human" || questionTask.question === void 0) throw new Error(`Question task ${questionTaskId} is not waiting for an answer.`);
|
|
1108
|
+
const attributes = ctx.session.auth.initiator?.attributes;
|
|
1109
|
+
if (attributes?.factoryReplyChannel !== questionTask.replyTo.channel || attributes.factoryReplyAddress !== questionTask.replyTo.address) throw new Error("Question reply does not match the authenticated conversation.");
|
|
1110
|
+
const repositoryAttribute = attributes.factoryRepositoryIds;
|
|
1111
|
+
const boundRepositoryIds = typeof repositoryAttribute === "string" ? repositoryAttribute.split(",").filter(Boolean) : repositoryAttribute;
|
|
1112
|
+
if (boundRepositoryIds === void 0 || boundRepositoryIds.length !== questionTask.repositoryIds.length || boundRepositoryIds.some((id) => !questionTask.repositoryIds.includes(id))) throw new Error("Question repositories do not match the authenticated conversation.");
|
|
1113
|
+
const answeredBy = ctx.session.auth.initiator?.principalId ?? "operator";
|
|
1114
|
+
const answered = await stores.tasks.transition(questionTask.id, "succeeded", {
|
|
1115
|
+
patch: { question: {
|
|
1116
|
+
...questionTask.question,
|
|
1117
|
+
answer,
|
|
1118
|
+
answeredBy
|
|
1119
|
+
} },
|
|
1120
|
+
reason: "question answered through Foreman"
|
|
892
1121
|
});
|
|
893
|
-
|
|
1122
|
+
const parentTaskId = questionTask.origin.parentTaskId;
|
|
1123
|
+
if (parentTaskId === void 0) return {
|
|
1124
|
+
questionTask: answered,
|
|
1125
|
+
parentTask: null
|
|
1126
|
+
};
|
|
1127
|
+
const parent = await stores.tasks.get(parentTaskId);
|
|
1128
|
+
if (parent === null) throw new Error(`Parent task ${parentTaskId} does not exist.`);
|
|
1129
|
+
return {
|
|
1130
|
+
questionTask: answered,
|
|
1131
|
+
parentTask: parent.state === "needs_human" ? await stores.tasks.transition(parent.id, "queued", { reason: `question ${questionTask.id} answered by ${answeredBy}` }) : parent
|
|
1132
|
+
};
|
|
894
1133
|
}
|
|
895
1134
|
}),
|
|
896
1135
|
append_receipt: defineTool({
|
|
@@ -910,25 +1149,62 @@ function factoryTools(stores, options = {}) {
|
|
|
910
1149
|
})
|
|
911
1150
|
};
|
|
912
1151
|
}
|
|
1152
|
+
const terminalTaskStates = /* @__PURE__ */ new Set([
|
|
1153
|
+
"succeeded",
|
|
1154
|
+
"failed",
|
|
1155
|
+
"cancelled"
|
|
1156
|
+
]);
|
|
913
1157
|
/**
|
|
914
1158
|
* Observe-only eve hooks that keep the ledger honest about session outcomes.
|
|
915
1159
|
* Mount by re-exporting from agent/hooks/, e.g.
|
|
916
|
-
* `export default factoryHooks(stores).factory;`.
|
|
1160
|
+
* `export default factoryHooks(stores, { presentQuestion }).factory;`.
|
|
1161
|
+
*
|
|
1162
|
+
* step.completed: records the step's tokens and AI Gateway cost on the bound
|
|
1163
|
+
* task. Delivery is at least once; recordUsage dedupes by (turnId, stepIndex).
|
|
1164
|
+
*
|
|
1165
|
+
* turn.completed: settles the bound task's recorded usage if the task is
|
|
1166
|
+
* already terminal (the sweep handles tasks that end later), then moves any
|
|
1167
|
+
* still-running task at this session's reply address behind a surfaced human
|
|
1168
|
+
* question, because the agent finished talking without recording an outcome.
|
|
917
1169
|
*
|
|
918
|
-
*
|
|
919
|
-
* agent finished talking without recording an outcome; the task moves to
|
|
920
|
-
* needs_human instead of rotting. The task is found by the session's
|
|
921
|
-
* channel address, which dispatch set from the task's replyTo.
|
|
1170
|
+
* Every store call is swallowed: a hook throw fails the turn.
|
|
922
1171
|
*/
|
|
923
|
-
function factoryHooks(stores) {
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1172
|
+
function factoryHooks(stores, options) {
|
|
1173
|
+
const taskIdAttribute = options.taskIdAttribute ?? "factoryTaskId";
|
|
1174
|
+
function boundTaskId(ctx) {
|
|
1175
|
+
const parsed = taskIdSchema.safeParse(ctx.session.auth.initiator?.attributes[taskIdAttribute]);
|
|
1176
|
+
return parsed.success ? parsed.data : void 0;
|
|
1177
|
+
}
|
|
1178
|
+
async function settleIfTerminal(taskId) {
|
|
1179
|
+
const task = await stores.tasks.get(taskId);
|
|
1180
|
+
if (task?.usage === void 0 || task.settledUsd !== void 0 || !terminalTaskStates.has(task.state)) return;
|
|
1181
|
+
await stores.tasks.settle(task.id, { totalUsd: task.usage.costUsd }, { reason: "measured usage settled at turn end" });
|
|
1182
|
+
}
|
|
1183
|
+
return { factory: defineHook({ events: {
|
|
1184
|
+
async "step.completed"(event, ctx) {
|
|
1185
|
+
const taskId = boundTaskId(ctx);
|
|
1186
|
+
const usage = event.data.usage;
|
|
1187
|
+
if (taskId === void 0 || usage === void 0) return;
|
|
1188
|
+
await stores.tasks.recordUsage(taskId, {
|
|
1189
|
+
turnId: event.data.turnId,
|
|
1190
|
+
stepIndex: event.data.stepIndex,
|
|
1191
|
+
...usage
|
|
1192
|
+
}).catch(() => void 0);
|
|
1193
|
+
},
|
|
1194
|
+
async "turn.completed"(_event, ctx) {
|
|
1195
|
+
const taskId = boundTaskId(ctx);
|
|
1196
|
+
if (taskId !== void 0) await settleIfTerminal(taskId).catch(() => void 0);
|
|
1197
|
+
const address = ctx.channel.continuationToken;
|
|
1198
|
+
if (address === void 0) return;
|
|
1199
|
+
const abandoned = (await stores.tasks.list()).filter((task) => task.state === "running" && task.replyTo.address === address);
|
|
1200
|
+
for (const task of abandoned) await createAndPresentQuestion(stores, task, {
|
|
1201
|
+
text: "The worker stopped without recording an outcome. Please review the task and reply when Factory should retry it.",
|
|
1202
|
+
options: ["Retry the task"]
|
|
1203
|
+
}, options.presentQuestion).catch(() => void 0);
|
|
1204
|
+
}
|
|
1205
|
+
} }) };
|
|
930
1206
|
}
|
|
931
1207
|
//#endregion
|
|
932
|
-
export { deepsecScanTool, factoryHooks, factoryTools, fetchPullRequestTool, findingFromDeepsec, invokeAgentTool, mergeConflictPayloadSchema, mergeConflictTools, postReviewTool, recordFinding, recordFindingTool, recordVerificationTool, reviewCommentTools };
|
|
1208
|
+
export { deepsecScanTool, delegateAgentTool, factoryHooks, factoryTools, fetchPullRequestTool, findingFromDeepsec, invokeAgentTool, mergeConflictPayloadSchema, mergeConflictTools, postReviewTool, recordFinding, recordFindingTool, recordVerificationTool, reviewCommentTools };
|
|
933
1209
|
|
|
934
1210
|
//# sourceMappingURL=index.mjs.map
|