@vercel/factory 0.0.17 → 0.0.18
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/CHANGELOG.md +46 -0
- package/dist/api-contracts.d.mts +665 -715
- package/dist/api-contracts.mjs +91 -4
- package/dist/api-contracts.mjs.map +1 -1
- package/dist/api-task-graph.mjs +14 -5
- package/dist/api-task-graph.mjs.map +1 -1
- package/dist/api.d.mts +58 -11
- package/dist/api.mjs +715 -399
- package/dist/api.mjs.map +1 -1
- package/dist/approval-access.d.mts +1 -1
- package/dist/approval-contracts.d.mts +3 -4
- package/dist/approval-contracts.mjs +1 -2
- package/dist/approval-contracts.mjs.map +1 -1
- package/dist/blob/index.d.mts +1 -1
- package/dist/budget.mjs +20 -3
- package/dist/budget.mjs.map +1 -1
- package/dist/change-verification/brief.d.mts +1 -1
- package/dist/change-verification/dispatch.mjs +8 -1
- package/dist/change-verification/dispatch.mjs.map +1 -1
- package/dist/changes/eve-record-change.mjs +4 -1
- package/dist/changes/eve-record-change.mjs.map +1 -1
- package/dist/client-stream.d.mts +19 -2
- package/dist/client-stream.mjs +115 -53
- package/dist/client-stream.mjs.map +1 -1
- package/dist/client-transcript.d.mts +25 -2
- package/dist/client-transcript.mjs +95 -13
- package/dist/client-transcript.mjs.map +1 -1
- package/dist/client.d.mts +145 -19
- package/dist/client.mjs +89 -16
- package/dist/client.mjs.map +1 -1
- package/dist/code-review/contracts.d.mts +1 -0
- package/dist/code-review/eve-post-review.d.mts +4 -2
- package/dist/code-review/eve-post-review.mjs +8 -0
- package/dist/code-review/eve-post-review.mjs.map +1 -1
- package/dist/code-review/github-reporter.d.mts +6 -2
- package/dist/code-review/github-reporter.mjs +86 -35
- package/dist/code-review/github-reporter.mjs.map +1 -1
- package/dist/code-review/preparation.d.mts +2 -1
- package/dist/code-review/preparation.mjs +2 -1
- package/dist/code-review/preparation.mjs.map +1 -1
- package/dist/code-review/stall.mjs +11 -2
- package/dist/code-review/stall.mjs.map +1 -1
- package/dist/connectors/github.d.mts +1 -1
- package/dist/connectors/index.d.mts +1 -1
- package/dist/connectors/slack.d.mts +1 -1
- package/dist/delivery-metrics/metrics.d.mts +1 -1
- package/dist/dispatch.mjs +3 -3
- package/dist/dispatch.mjs.map +1 -1
- package/dist/eve/index.d.mts +1 -1
- package/dist/eve/index.mjs +8 -1
- package/dist/eve/index.mjs.map +1 -1
- package/dist/eve/task-execution.d.mts +4 -4
- package/dist/eve/transcript.d.mts +3 -1
- package/dist/eve/transcript.mjs.map +1 -1
- package/dist/finding-remediation/admission.d.mts +1 -1
- package/dist/finding-remediation/brief.d.mts +1 -1
- package/dist/github-app.d.mts +1 -1
- package/dist/github-publication.d.mts +1 -1
- package/dist/github.d.mts +9 -1
- package/dist/github.mjs +29 -11
- package/dist/github.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/intake-contracts.d.mts +1 -0
- package/dist/intake-contracts.mjs +2 -1
- package/dist/intake-contracts.mjs.map +1 -1
- package/dist/integrations/github-issues.d.mts +53 -0
- package/dist/integrations/github-issues.mjs +170 -0
- package/dist/integrations/github-issues.mjs.map +1 -0
- package/dist/integrations/github.d.mts +2 -1
- package/dist/integrations/github.mjs +2 -1
- package/dist/multi-repository-changes/coordination.mjs +1 -1
- package/dist/planning/reconcile.mjs +15 -2
- package/dist/planning/reconcile.mjs.map +1 -1
- package/dist/postgres/index.d.mts +2 -1
- package/dist/postgres/index.mjs +11 -5
- package/dist/postgres/index.mjs.map +1 -1
- package/dist/postgres/indexed-codec.mjs +137 -0
- package/dist/postgres/indexed-codec.mjs.map +1 -0
- package/dist/postgres/indexed-schema.mjs +99 -0
- package/dist/postgres/indexed-schema.mjs.map +1 -0
- package/dist/postgres/indexed.d.mts +28 -0
- package/dist/postgres/indexed.mjs +277 -0
- package/dist/postgres/indexed.mjs.map +1 -0
- package/dist/postgres/record-predicates.mjs +23 -0
- package/dist/postgres/record-predicates.mjs.map +1 -0
- package/dist/presets/software-development/deepsec-brief.d.mts +1 -1
- package/dist/presets/software-development/dispatch.mjs +1 -1
- package/dist/presets/software-development/dispatch.mjs.map +1 -1
- package/dist/presets/software-development/recovery.mjs +9 -2
- package/dist/presets/software-development/recovery.mjs.map +1 -1
- package/dist/pull-requests/eve-fetch.d.mts +4 -1
- package/dist/pull-requests/eve-fetch.mjs +9 -0
- package/dist/pull-requests/eve-fetch.mjs.map +1 -1
- package/dist/pull-requests/github-publisher.d.mts +1 -1
- package/dist/pull-requests/github-publisher.mjs +19 -7
- package/dist/pull-requests/github-publisher.mjs.map +1 -1
- package/dist/repositories.mjs +20 -13
- package/dist/repositories.mjs.map +1 -1
- package/dist/sandbox/execution-run-ledger.d.mts +2 -2
- package/dist/sandbox/execution-run-ledger.mjs.map +1 -1
- package/dist/sandbox/execution.d.mts +2 -2
- package/dist/sandbox/execution.mjs.map +1 -1
- package/dist/schema/catalog.mjs +1 -1
- package/dist/schema/change.d.mts +1 -1
- package/dist/schema/communication.d.mts +2 -2
- package/dist/schema/factory-config.d.mts +1 -1
- package/dist/schema/outbox.d.mts +6 -6
- package/dist/schema/session-summary.d.mts +218 -0
- package/dist/schema/session-summary.mjs +68 -0
- package/dist/schema/session-summary.mjs.map +1 -0
- package/dist/schema/session.d.mts +39 -59
- package/dist/schema/session.mjs +6 -6
- package/dist/schema/session.mjs.map +1 -1
- package/dist/schema/task-graph.d.mts +24 -24
- package/dist/schema/transcript.d.mts +141 -3
- package/dist/schema/transcript.mjs +19 -3
- package/dist/schema/transcript.mjs.map +1 -1
- package/dist/schema/transitions.d.mts +1 -1
- package/dist/schema/transitions.mjs +1 -1
- package/dist/schema/transitions.mjs.map +1 -1
- package/dist/signals.d.mts +1 -1
- package/dist/stall.mjs +10 -2
- package/dist/stall.mjs.map +1 -1
- package/dist/storage.d.mts +10 -1
- package/dist/storage.mjs +7 -1
- package/dist/store/approvals.d.mts +14 -0
- package/dist/store/approvals.mjs +78 -0
- package/dist/store/approvals.mjs.map +1 -0
- package/dist/store/conversations.d.mts +59 -0
- package/dist/store/conversations.mjs +549 -0
- package/dist/store/conversations.mjs.map +1 -0
- package/dist/store/driver.d.mts +1 -1
- package/dist/store/driver.mjs.map +1 -1
- package/dist/store/engine.d.mts +38 -16
- package/dist/store/engine.mjs +229 -106
- package/dist/store/engine.mjs.map +1 -1
- package/dist/store/indexed-engine.d.mts +57 -0
- package/dist/store/indexed-engine.mjs +155 -0
- package/dist/store/indexed-engine.mjs.map +1 -0
- package/dist/store/indexed-memory.d.mts +7 -0
- package/dist/store/indexed-memory.mjs +200 -0
- package/dist/store/indexed-memory.mjs.map +1 -0
- package/dist/store/indexed-projections.d.mts +280 -0
- package/dist/store/indexed-projections.mjs +481 -0
- package/dist/store/indexed-projections.mjs.map +1 -0
- package/dist/store/indexed.d.mts +101 -0
- package/dist/store/indexed.mjs +19 -0
- package/dist/store/indexed.mjs.map +1 -0
- package/dist/store/iterate.d.mts +9 -0
- package/dist/store/iterate.mjs +23 -0
- package/dist/store/iterate.mjs.map +1 -0
- package/dist/store/query-scope.mjs +62 -0
- package/dist/store/query-scope.mjs.map +1 -0
- package/dist/store/query.d.mts +32 -0
- package/dist/store/query.mjs +103 -0
- package/dist/store/query.mjs.map +1 -0
- package/dist/store/session-history.mjs +24 -0
- package/dist/store/session-history.mjs.map +1 -0
- package/dist/store/slack-pr-notifications.d.mts +2 -2
- package/dist/store/slack-pr-notifications.mjs.map +1 -1
- package/dist/store/state.d.mts +26 -0
- package/dist/store/state.mjs +95 -0
- package/dist/store/state.mjs.map +1 -0
- package/dist/store/task-effects.d.mts +1 -1
- package/dist/store/task-graphs.d.mts +1 -1
- package/dist/store/task-messages.d.mts +1 -1
- package/dist/store/task-work.d.mts +1 -1
- package/dist/store/task-work.mjs +1 -1
- package/dist/store/transcripts.mjs +308 -0
- package/dist/store/transcripts.mjs.map +1 -0
- package/dist/sweep.d.mts +1 -1
- package/dist/sweep.mjs +12 -7
- package/dist/sweep.mjs.map +1 -1
- package/dist/task-communication.mjs +15 -1
- package/dist/task-communication.mjs.map +1 -1
- package/dist/task-protocols.d.mts +3 -3
- package/dist/tasks.d.mts +1 -1
- package/dist/vercel.d.mts +1 -1
- package/dist/work-triage/find-existing-work.d.mts +1 -1
- package/dist/work-triage/find-existing-work.mjs +12 -2
- package/dist/work-triage/find-existing-work.mjs.map +1 -1
- package/docs/index.md +4 -0
- package/docs/recipes/persistence-recovery.md +15 -9
- package/docs/recipes/postgres-indexed-engine.md +108 -0
- package/docs/recipes/postgres-indexed-storage.md +145 -0
- package/package.json +3 -1
package/dist/api.mjs
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
|
+
import { collectFactoryRecords } from "./store/iterate.mjs";
|
|
1
2
|
import { repositoryIdSchema, signalIdSchema, taskIdSchema } from "./schema/id.mjs";
|
|
2
3
|
import { repositorySlugSchema } from "./schema/repository.mjs";
|
|
3
4
|
import { parseAgentRouteBinding } from "./schema/agent-route.mjs";
|
|
4
5
|
import { taskStateSchema } from "./schema/task.mjs";
|
|
6
|
+
import { RecordNotFoundError, VersionConflictError } from "./store/driver.mjs";
|
|
5
7
|
import { InvalidTransitionError } from "./schema/transitions.mjs";
|
|
6
|
-
import { readEvents } from "./client-events.mjs";
|
|
7
8
|
import { MAX_IMAGE_BYTES, imageIdSchema, imagePartFilename, imageUploadSchema } from "./image-input.mjs";
|
|
8
9
|
import { Transcript } from "./client-transcript.mjs";
|
|
9
10
|
import { routeTaskWork } from "./agent-routes.mjs";
|
|
10
|
-
import { RecordNotFoundError, VersionConflictError } from "./store/driver.mjs";
|
|
11
11
|
import { BudgetExhaustedError, DispatchRefusedError, dispatchTask } from "./dispatch.mjs";
|
|
12
|
+
import { findingSchema } from "./schema/finding.mjs";
|
|
12
13
|
import { ApprovalAccessError, createApprovalAccess } from "./approval-access.mjs";
|
|
13
14
|
import { agentSessionIdSchema, agentSessionSchema } from "./schema/session.mjs";
|
|
14
15
|
import { ApprovalError, ImageInputError, SessionConflictError } from "./store/engine.mjs";
|
|
16
|
+
import { InvalidStoreQueryError } from "./store/indexed.mjs";
|
|
17
|
+
import { answerSessionQuestionRequestSchema, apiAgentSchema, controlSessionRequestSchema, createSessionRequestSchema, repositoryDetailsSchema, repositorySummarySchema, sendSessionMessageRequestSchema, sessionInputResponseSchema, sessionPageSchema, sessionPullRequestSchema, sessionPullRequestStateSchema, sessionViewSchema, sessionWorkGroupSchema, sessionWorkResultSchema, sessionWorkTaskSchema, submitSessionInputRequestSchema } from "./api-contracts.mjs";
|
|
15
18
|
import { inboxActivityItemSchema, inboxActivitySchema } from "./inbox-activity.mjs";
|
|
16
19
|
import { agentModelConfigSchema, modelSettingsSchema, updateModelDefaultsSchema } from "./model-settings.mjs";
|
|
17
20
|
import { sessionPreviewSchema, sessionPreviewsSchema } from "./session-previews.mjs";
|
|
18
21
|
import { sessionReviewCheckSchema, sessionReviewIssueSchema, sessionReviewSchema, sessionReviewScreenshotSchema } from "./session-review.mjs";
|
|
19
22
|
import { workspaceFileContentSchema, workspaceFileEntrySchema, workspaceFilesRequestSchema, workspaceFilesSchema, workspacePathSchema } from "./workspace-files.mjs";
|
|
20
23
|
import { intakeViewSchema, retryIntakeSchema } from "./intake-contracts.mjs";
|
|
21
|
-
import { answerSessionQuestionRequestSchema, apiAgentSchema, controlSessionRequestSchema, createSessionRequestSchema, repositoryDetailsSchema, repositorySummarySchema, sendSessionMessageRequestSchema, sessionInputResponseSchema, sessionPageSchema, sessionViewSchema, submitSessionInputRequestSchema } from "./api-contracts.mjs";
|
|
22
24
|
import { unavailableWorkspaceDiff, workspaceDiffFileSchema, workspaceDiffSchema } from "./workspace-diff.mjs";
|
|
23
25
|
import { taskGraphViewSchema } from "./task-graph-view.mjs";
|
|
24
26
|
import { workspaceGitFilesScript } from "./workspace-files-git.mjs";
|
|
@@ -39,14 +41,18 @@ const json = (value, status = 200) => Response.json(value, {
|
|
|
39
41
|
status,
|
|
40
42
|
headers: { "cache-control": "no-store" }
|
|
41
43
|
});
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const pullRequestStatusObservationSchema = z.strictObject({
|
|
45
|
+
changeId: sessionPullRequestSchema.shape.changeId,
|
|
46
|
+
state: sessionPullRequestStateSchema.nullable(),
|
|
47
|
+
sync: sessionPullRequestSchema.shape.sync,
|
|
48
|
+
checkedAt: sessionPullRequestSchema.shape.checkedAt,
|
|
49
|
+
observedAt: sessionPullRequestSchema.shape.observedAt,
|
|
50
|
+
syncMessage: sessionPullRequestSchema.shape.syncMessage
|
|
51
|
+
});
|
|
47
52
|
/** Creates the authenticated request handler for the public Factory HTTP API. */
|
|
48
53
|
function createFactoryApi(options) {
|
|
49
54
|
const { stores } = options;
|
|
55
|
+
const maxSessionPullRequests = z.number().int().min(1).max(100).parse(options.maxSessionPullRequests ?? 50);
|
|
50
56
|
const agents = options.agents.map((agent) => apiAgentSchema.parse(agent));
|
|
51
57
|
const agentModels = (options.agentModels ?? []).map((entry) => agentModelConfigSchema.parse(entry));
|
|
52
58
|
if (new Set(agentModels.map((entry) => entry.agent)).size !== agentModels.length) throw new Error("Duplicate agent model catalog");
|
|
@@ -59,64 +65,64 @@ function createFactoryApi(options) {
|
|
|
59
65
|
if (new Set(agents.map((agent) => agent.id)).size !== agents.length) throw new Error("Duplicate API agent");
|
|
60
66
|
if (options.continuationAgent !== void 0 && !agents.some((agent) => agent.id === options.continuationAgent && agent.route === void 0 && agent.canStart)) throw new Error("continuationAgent must be a directly startable conversation agent");
|
|
61
67
|
for (const agent of agents) if (agent.route !== void 0 && options.routes.get(agent.route)?.agent !== agent.id) throw new Error(`API agent ${agent.id} must select one of its registered routes`);
|
|
68
|
+
async function configuredRepositories() {
|
|
69
|
+
return mapConcurrent(options.repositories, async (slug) => {
|
|
70
|
+
return (await stores.repositories.list({
|
|
71
|
+
limit: 1,
|
|
72
|
+
match: { slug }
|
|
73
|
+
})).items[0] ?? options.ensureRepository(slug);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
62
76
|
const approvals = createApprovalAccess({
|
|
63
77
|
stores,
|
|
64
78
|
operators: options.operators ?? [],
|
|
65
79
|
repositories: options.repositories
|
|
66
80
|
});
|
|
67
|
-
const approvalView = approvals.view;
|
|
68
81
|
async function approvalPage(url, actor) {
|
|
69
82
|
const input = z.object({
|
|
70
83
|
status: z.enum([
|
|
71
84
|
"pending",
|
|
72
85
|
"decided",
|
|
73
86
|
"all"
|
|
74
|
-
]).default("pending")
|
|
75
|
-
repository: repositorySlugSchema.optional()
|
|
76
|
-
query: z.string().trim().max(200).optional()
|
|
87
|
+
]).default("pending"),
|
|
88
|
+
repository: repositorySlugSchema.optional(),
|
|
89
|
+
query: z.string().trim().max(200).optional(),
|
|
77
90
|
sort: z.enum([
|
|
78
91
|
"created",
|
|
79
92
|
"priority",
|
|
80
93
|
"decided"
|
|
81
|
-
]).default("created")
|
|
82
|
-
limit: z.coerce.number().int().min(1).max(100).default(30)
|
|
83
|
-
cursor:
|
|
94
|
+
]).default("created"),
|
|
95
|
+
limit: z.coerce.number().int().min(1).max(100).default(30),
|
|
96
|
+
cursor: z.string().max(8192).optional()
|
|
84
97
|
}).parse(Object.fromEntries(url.searchParams));
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
...
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (input.sort === "priority") views.sort((a, b) => (a.finding?.priority ?? "P4").localeCompare(b.finding?.priority ?? "P4") || a.task.createdAt.localeCompare(b.task.createdAt) || a.task.id.localeCompare(b.task.id));
|
|
105
|
-
if (input.sort === "decided") views.sort((a, b) => (b.decision?.recordedAt ?? b.task.createdAt).localeCompare(a.decision?.recordedAt ?? a.task.createdAt) || a.task.createdAt.localeCompare(b.task.createdAt) || a.task.id.localeCompare(b.task.id));
|
|
106
|
-
}
|
|
107
|
-
const ordered = views?.map((view) => view.task) ?? tasks;
|
|
108
|
-
const start = input.cursor ? ordered.findIndex((task) => task.id === input.cursor) + 1 : 0;
|
|
109
|
-
if (input.cursor && start === 0) throw new ApiError(409, "Approval list changed; refresh from the first page");
|
|
110
|
-
const page = ordered.slice(start, start + input.limit);
|
|
98
|
+
const repos = (await configuredRepositories()).filter((repo) => repo.enabled);
|
|
99
|
+
const selected = input.repository ? repos.find((repo) => repo.slug === input.repository) : void 0;
|
|
100
|
+
if (input.repository && !selected) throw new ApiError(403, "Repository is not configured");
|
|
101
|
+
const page = await stores.approvals.list({
|
|
102
|
+
limit: input.limit,
|
|
103
|
+
cursor: input.cursor,
|
|
104
|
+
sort: input.sort,
|
|
105
|
+
search: input.query || void 0,
|
|
106
|
+
includeTotal: true,
|
|
107
|
+
match: {
|
|
108
|
+
...selected ? { repositoryIds: [selected.id] } : {},
|
|
109
|
+
...input.status === "all" ? {} : { status: input.status }
|
|
110
|
+
},
|
|
111
|
+
fields: [{
|
|
112
|
+
path: ["repositoryIds"],
|
|
113
|
+
test: "subset",
|
|
114
|
+
value: repos.map((repo) => repo.id)
|
|
115
|
+
}]
|
|
116
|
+
});
|
|
111
117
|
return json({
|
|
112
118
|
actor,
|
|
113
|
-
items:
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
items: await mapConcurrent(page.items, (id) => approvals.get(actor, id)),
|
|
120
|
+
nextCursor: page.nextCursor,
|
|
121
|
+
totalCount: page.totalCount
|
|
116
122
|
});
|
|
117
123
|
}
|
|
118
124
|
async function start(session, task, message, operationId) {
|
|
119
|
-
const continuation = task ?
|
|
125
|
+
const continuation = task ? await stores.sessions.getContinuation(session.id, task.id) : void 0;
|
|
120
126
|
const attachments = continuation ? continuation.contextAttachments ?? continuation.attachments : session.attachments;
|
|
121
127
|
return options.start({
|
|
122
128
|
session,
|
|
@@ -130,14 +136,109 @@ function createFactoryApi(options) {
|
|
|
130
136
|
const binding = task.work.route;
|
|
131
137
|
return binding ? options.routes.get(binding) : void 0;
|
|
132
138
|
}
|
|
133
|
-
function taskAgent(session, task) {
|
|
139
|
+
async function taskAgent(session, task) {
|
|
134
140
|
const route = taskRoute(task);
|
|
135
141
|
if (route) return agents.some((agent) => agent.id === route.agent) ? route.agent : void 0;
|
|
136
|
-
const continuation =
|
|
142
|
+
const continuation = await stores.sessions.getContinuation(session.id, task.id);
|
|
137
143
|
return task.work.route?.id === "conversation" && task.work.route.version === 1 && continuation?.agent === options.continuationAgent ? options.continuationAgent : void 0;
|
|
138
144
|
}
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
const terminalWorkStates = /* @__PURE__ */ new Set([
|
|
146
|
+
"succeeded",
|
|
147
|
+
"failed",
|
|
148
|
+
"cancelled"
|
|
149
|
+
]);
|
|
150
|
+
function aggregateWorkState(tasks, root) {
|
|
151
|
+
const states = new Set(tasks.map((task) => task.state));
|
|
152
|
+
for (const state of [
|
|
153
|
+
"needs_human",
|
|
154
|
+
"verifying",
|
|
155
|
+
"running",
|
|
156
|
+
"queued",
|
|
157
|
+
"stalled"
|
|
158
|
+
]) if (states.has(state)) return state;
|
|
159
|
+
if (root && terminalWorkStates.has(root.state)) return root.state;
|
|
160
|
+
for (const state of [
|
|
161
|
+
"failed",
|
|
162
|
+
"cancelled",
|
|
163
|
+
"succeeded"
|
|
164
|
+
]) if (states.has(state)) return state;
|
|
165
|
+
return "queued";
|
|
166
|
+
}
|
|
167
|
+
async function sessionWorkGroups(session, selectedTask, relatedTasks, pullRequests) {
|
|
168
|
+
const knownTasks = /* @__PURE__ */ new Map();
|
|
169
|
+
for (const task of relatedTasks) knownTasks.set(task.id, task);
|
|
170
|
+
if (selectedTask) knownTasks.set(selectedTask.id, selectedTask);
|
|
171
|
+
async function ancestor(id) {
|
|
172
|
+
const cached = knownTasks.get(id);
|
|
173
|
+
if (cached) return cached;
|
|
174
|
+
const fetched = await stores.tasks.get(id);
|
|
175
|
+
if (fetched) knownTasks.set(id, fetched);
|
|
176
|
+
return fetched;
|
|
177
|
+
}
|
|
178
|
+
async function requestRootId(task) {
|
|
179
|
+
let current = task;
|
|
180
|
+
const visited = /* @__PURE__ */ new Set([task.id]);
|
|
181
|
+
for (;;) {
|
|
182
|
+
if (current.parentTaskId === void 0 || visited.has(current.parentTaskId)) return current.id;
|
|
183
|
+
const parent = await ancestor(current.parentTaskId);
|
|
184
|
+
if (!parent || parent.work.route?.id === "conversation") return current.id;
|
|
185
|
+
visited.add(parent.id);
|
|
186
|
+
current = parent;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const tasks = [...new Map([...relatedTasks, ...selectedTask ? [selectedTask] : []].filter((task) => task.work.route?.id !== "conversation").map((task) => [task.id, task])).values()];
|
|
190
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
191
|
+
for (const task of tasks) {
|
|
192
|
+
if (!task.work.route) continue;
|
|
193
|
+
const requestId = await requestRootId(task);
|
|
194
|
+
const group = grouped.get(requestId) ?? [];
|
|
195
|
+
group.push(task);
|
|
196
|
+
grouped.set(requestId, group);
|
|
197
|
+
}
|
|
198
|
+
return await mapConcurrent([...grouped], async ([requestId, groupedTasks]) => {
|
|
199
|
+
let root = groupedTasks.find((task) => task.id === requestId) ?? null;
|
|
200
|
+
if (!root) {
|
|
201
|
+
const candidate = await ancestor(requestId);
|
|
202
|
+
root = candidate ? await authorizedRelatedTask(session, candidate) : null;
|
|
203
|
+
if (root && root.work.route?.id === "conversation") root = null;
|
|
204
|
+
}
|
|
205
|
+
const orderedTasks = [...root ? [root] : [], ...groupedTasks.filter((task) => task.id !== root?.id)];
|
|
206
|
+
const anchor = root ?? orderedTasks[0];
|
|
207
|
+
const changeIds = [...new Set(orderedTasks.map((task) => task.changeId).filter((value) => value !== void 0))];
|
|
208
|
+
const state = aggregateWorkState(orderedTasks, root);
|
|
209
|
+
let result;
|
|
210
|
+
if (root && options.presentSessionWork && orderedTasks.every((task) => terminalWorkStates.has(task.state))) try {
|
|
211
|
+
const presented = await options.presentSessionWork({
|
|
212
|
+
root,
|
|
213
|
+
tasks: orderedTasks,
|
|
214
|
+
pullRequests: pullRequests.filter((pullRequest) => changeIds.includes(pullRequest.changeId))
|
|
215
|
+
});
|
|
216
|
+
if (presented) result = sessionWorkResultSchema.parse(presented);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
options.reportError?.(error, {
|
|
219
|
+
method: "GET",
|
|
220
|
+
path: "session-work-presentation"
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return sessionWorkGroupSchema.parse({
|
|
224
|
+
requestId,
|
|
225
|
+
title: anchor.work.title,
|
|
226
|
+
agent: taskRoute(anchor)?.agent ?? anchor.work.route?.id ?? "Workflow",
|
|
227
|
+
state,
|
|
228
|
+
tasks: orderedTasks.map((task) => ({
|
|
229
|
+
taskId: task.id,
|
|
230
|
+
title: task.work.title,
|
|
231
|
+
agent: taskRoute(task)?.agent ?? task.work.route?.id ?? "Workflow",
|
|
232
|
+
state: task.state,
|
|
233
|
+
attempt: task.attempt
|
|
234
|
+
})),
|
|
235
|
+
changeIds,
|
|
236
|
+
result
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async function executionSession(session, task, agent) {
|
|
241
|
+
const activeAgent = agent ?? (task ? await taskAgent(session, task) : session.agent);
|
|
141
242
|
if (!activeAgent) throw new ApiError(409, "Task has no configured agent route");
|
|
142
243
|
return agentSessionSchema.parse({
|
|
143
244
|
...session,
|
|
@@ -147,282 +248,369 @@ function createFactoryApi(options) {
|
|
|
147
248
|
external: task ? void 0 : session.external
|
|
148
249
|
});
|
|
149
250
|
}
|
|
150
|
-
function
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
251
|
+
function workflowTasks(session, tasks) {
|
|
252
|
+
return [...tasks].filter((task) => task.work.route === void 0 && !task.question && task.replyTo.channel === "api" && task.replyTo.address === session.id && task.repositoryIds.length === 1 && task.repositoryIds[0] === session.repositoryId);
|
|
253
|
+
}
|
|
254
|
+
async function mayAccessTask(owner, task) {
|
|
255
|
+
return options.canAccessTask ? options.canAccessTask({ id: owner }, task) : options.sharedWorkspaceOwner === owner;
|
|
256
|
+
}
|
|
257
|
+
async function sessionFinding(session, task) {
|
|
258
|
+
if (!task?.origin.signalId) return null;
|
|
259
|
+
const repositoryIds = session.external?.repositoryIds ?? (session.repositoryId ? [session.repositoryId] : []);
|
|
260
|
+
if (!task.repositoryIds.every((id) => repositoryIds.includes(id)) || (session.attached || session.external) && !await mayAccessTask(session.owner, task)) return null;
|
|
261
|
+
const signal = await stores.signals.get(task.origin.signalId);
|
|
262
|
+
if (!signal || !task.repositoryIds.includes(signal.repositoryId)) return null;
|
|
263
|
+
const finding = findingSchema.safeParse(signal.payload);
|
|
264
|
+
return finding.success ? finding.data : null;
|
|
265
|
+
}
|
|
266
|
+
async function findingSummary(summary) {
|
|
267
|
+
if (!summary.task?.approval) return summary;
|
|
268
|
+
const task = await stores.tasks.get(summary.task.id);
|
|
269
|
+
const finding = await sessionFinding(summary.session, task);
|
|
167
270
|
return {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
271
|
+
...summary,
|
|
272
|
+
finding: finding ? {
|
|
273
|
+
priority: finding.priority,
|
|
274
|
+
category: finding.category
|
|
275
|
+
} : null
|
|
171
276
|
};
|
|
172
277
|
}
|
|
173
|
-
function
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
278
|
+
async function authorizedSummary(summary, owner) {
|
|
279
|
+
const { session } = summary;
|
|
280
|
+
if (!agents.some((agent) => agent.id === session.agent)) return null;
|
|
281
|
+
if (session.attached) {
|
|
282
|
+
const root = session.taskId ? await stores.tasks.get(session.taskId) : null;
|
|
283
|
+
if (!root || !await mayAccessTask(owner, root)) return null;
|
|
284
|
+
}
|
|
285
|
+
if (summary.task && (session.attached || session.external) && options.canAccessTask) {
|
|
286
|
+
const selected = await stores.tasks.get(summary.task.id);
|
|
287
|
+
if (!selected || !await mayAccessTask(owner, selected)) return null;
|
|
177
288
|
}
|
|
178
|
-
|
|
289
|
+
let question = summary.question;
|
|
290
|
+
if (question && session.external) {
|
|
291
|
+
const task = await stores.tasks.get(question.id);
|
|
292
|
+
const parent = task?.parentTaskId ? await stores.tasks.get(task.parentTaskId) : null;
|
|
293
|
+
if (!task || !parent || !await mayAccessTask(owner, task) || !await mayAccessTask(owner, parent)) question = null;
|
|
294
|
+
}
|
|
295
|
+
const route = summary.task?.work.route ? options.routes.get(summary.task.work.route) : void 0;
|
|
296
|
+
const candidate = summary.task ? route?.agent ?? (summary.task.work.route?.id === "conversation" && summary.task.work.route.version === 1 ? options.continuationAgent : void 0) : session.agent;
|
|
297
|
+
const activeAgent = agents.some((agent) => agent.id === candidate) && (!summary.task || summary.activeAgent === candidate) ? candidate : void 0;
|
|
298
|
+
let attention = summary.attention;
|
|
299
|
+
if (summary.question && !question) {
|
|
300
|
+
const head = summary.execution ? await stores.transcripts.head(summary.execution) : null;
|
|
301
|
+
attention = head?.state === "failed" ? "failed" : session.external && (head?.state === "waiting" || head?.state === "needs input") ? "conversation" : void 0;
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
...summary,
|
|
305
|
+
question,
|
|
306
|
+
attention,
|
|
307
|
+
activeAgent,
|
|
308
|
+
execution: activeAgent ? summary.execution : null
|
|
309
|
+
};
|
|
179
310
|
}
|
|
180
|
-
function
|
|
181
|
-
return
|
|
311
|
+
function relatedQuery(session) {
|
|
312
|
+
return {
|
|
313
|
+
match: { replyTo: session.external?.replyTo ?? {
|
|
314
|
+
channel: "api",
|
|
315
|
+
address: session.id
|
|
316
|
+
} },
|
|
317
|
+
...!session.external ? { fields: [
|
|
318
|
+
{
|
|
319
|
+
path: ["repositoryIds"],
|
|
320
|
+
test: "eq",
|
|
321
|
+
value: session.repositoryId ? [session.repositoryId] : []
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
path: ["work", "route"],
|
|
325
|
+
test: "exists"
|
|
326
|
+
},
|
|
327
|
+
...session.taskId ? [{
|
|
328
|
+
path: ["id"],
|
|
329
|
+
test: "ne",
|
|
330
|
+
value: session.taskId
|
|
331
|
+
}] : []
|
|
332
|
+
] } : {}
|
|
333
|
+
};
|
|
182
334
|
}
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
|
|
335
|
+
async function authorizedRelatedTask(session, task) {
|
|
336
|
+
if (session.external && (!task.repositoryIds.every((id) => session.external.repositoryIds.includes(id)) || !await mayAccessTask(session.owner, task))) return null;
|
|
337
|
+
if (!session.external && (task.repositoryIds.length !== 1 || task.repositoryIds[0] !== session.repositoryId)) return null;
|
|
338
|
+
return task;
|
|
187
339
|
}
|
|
188
|
-
async function
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
340
|
+
async function sessionPullRequests(session, selectedTask, signal) {
|
|
341
|
+
const maxRelatedTaskPages = 10;
|
|
342
|
+
const changeIds = /* @__PURE__ */ new Set();
|
|
343
|
+
const addTask = (task) => {
|
|
344
|
+
if (task?.changeId) changeIds.add(task.changeId);
|
|
345
|
+
};
|
|
346
|
+
addTask(selectedTask);
|
|
347
|
+
if (session.changeId) changeIds.add(session.changeId);
|
|
348
|
+
if (session.taskId && selectedTask?.id !== session.taskId) {
|
|
349
|
+
const root = await stores.tasks.get(session.taskId);
|
|
350
|
+
if (root) addTask(await authorizedRelatedTask(session, root));
|
|
351
|
+
}
|
|
352
|
+
let cursor;
|
|
353
|
+
let pages = 0;
|
|
354
|
+
let truncated = changeIds.size > maxSessionPullRequests;
|
|
355
|
+
let limitReached = false;
|
|
356
|
+
do {
|
|
357
|
+
pages += 1;
|
|
358
|
+
const page = await stores.tasks.list({
|
|
359
|
+
limit: 100,
|
|
360
|
+
cursor,
|
|
361
|
+
...relatedQuery(session)
|
|
362
|
+
});
|
|
363
|
+
const tasks = await mapConcurrent(page.items, (task) => authorizedRelatedTask(session, task));
|
|
364
|
+
for (const task of tasks) {
|
|
365
|
+
const changeId = task?.changeId;
|
|
366
|
+
if (!changeId || changeIds.has(changeId)) continue;
|
|
367
|
+
if (changeIds.size >= maxSessionPullRequests) {
|
|
368
|
+
truncated = true;
|
|
369
|
+
limitReached = true;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
changeIds.add(changeId);
|
|
373
|
+
}
|
|
374
|
+
cursor = page.nextCursor ?? void 0;
|
|
375
|
+
if (cursor && pages >= maxRelatedTaskPages) {
|
|
376
|
+
truncated = true;
|
|
377
|
+
limitReached = true;
|
|
378
|
+
}
|
|
379
|
+
} while (cursor && !limitReached);
|
|
380
|
+
const targets = (await mapConcurrent([...changeIds].slice(0, maxSessionPullRequests), async (changeId) => {
|
|
381
|
+
const change = await stores.changes.get(changeId);
|
|
382
|
+
if (!change) return null;
|
|
383
|
+
const repository = await stores.repositories.get(change.repositoryId);
|
|
384
|
+
const allowedRepositoryIds = session.external?.repositoryIds ?? (session.repositoryId ? [session.repositoryId] : []);
|
|
385
|
+
if (!repository || !allowedRepositoryIds.includes(repository.id)) return null;
|
|
195
386
|
return {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
execution: session.execution ?? null,
|
|
199
|
-
activeAgent: session.agent,
|
|
200
|
-
source: session.external.replyTo.channel,
|
|
201
|
-
relatedTasks,
|
|
202
|
-
question,
|
|
203
|
-
canFollowUp: false,
|
|
204
|
-
...humanAttention ? { attention: humanAttention } : {}
|
|
387
|
+
change,
|
|
388
|
+
repository
|
|
205
389
|
};
|
|
390
|
+
})).filter((target) => target !== null);
|
|
391
|
+
if (!targets.length) return {
|
|
392
|
+
pullRequests: [],
|
|
393
|
+
truncated
|
|
394
|
+
};
|
|
395
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
396
|
+
let observations;
|
|
397
|
+
if (!options.pullRequestStatuses) observations = targets.map(({ change }) => ({
|
|
398
|
+
changeId: change.id,
|
|
399
|
+
state: null,
|
|
400
|
+
sync: "unavailable",
|
|
401
|
+
checkedAt,
|
|
402
|
+
syncMessage: "Live pull request status is not configured for this factory."
|
|
403
|
+
}));
|
|
404
|
+
else try {
|
|
405
|
+
observations = z.array(pullRequestStatusObservationSchema).max(maxSessionPullRequests).parse(await options.pullRequestStatuses({
|
|
406
|
+
pullRequests: targets,
|
|
407
|
+
signal
|
|
408
|
+
}));
|
|
409
|
+
} catch (error) {
|
|
410
|
+
options.reportError?.(error, {
|
|
411
|
+
method: "GET",
|
|
412
|
+
path: "session-pull-requests"
|
|
413
|
+
});
|
|
414
|
+
observations = targets.map(({ change }) => ({
|
|
415
|
+
changeId: change.id,
|
|
416
|
+
state: null,
|
|
417
|
+
sync: "unavailable",
|
|
418
|
+
checkedAt,
|
|
419
|
+
syncMessage: "Live pull request status could not be refreshed."
|
|
420
|
+
}));
|
|
206
421
|
}
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}) : [];
|
|
212
|
-
const byId = new Map([...snapshot?.tasks ?? [], ...addressed.map((task) => [task.id, task])]);
|
|
213
|
-
if (boundTask) byId.set(boundTask.id, boundTask);
|
|
214
|
-
const { root, relatedTasks, tasks } = sessionTasks(session, byId);
|
|
215
|
-
const task = tasks.at(-1) ?? null;
|
|
216
|
-
const questionParents = [...tasks, ...workflowTasks(session, byId.values())];
|
|
217
|
-
if (!snapshot) await mapConcurrent(questionParents.filter((item) => item.state === "needs_human" && item.questionTaskId), async (item) => {
|
|
218
|
-
const question = await stores.tasks.get(item.questionTaskId);
|
|
219
|
-
if (question) byId.set(question.id, question);
|
|
220
|
-
});
|
|
221
|
-
const question = conversationQuestion(questionParents, byId);
|
|
222
|
-
const history = [];
|
|
223
|
-
const includeHistory = detail === "full" && snapshot?.receipts !== null;
|
|
224
|
-
const receipts = tasks.length && includeHistory ? snapshot?.receipts ?? await stores.receipts.list() : [];
|
|
225
|
-
for (const item of includeHistory ? tasks : []) {
|
|
226
|
-
const agent = taskAgent(session, item);
|
|
227
|
-
if (!agent) continue;
|
|
228
|
-
const message = session.continuations?.find((entry) => entry.taskId === item.id)?.message ?? (item.id === root?.id ? session.message : `${item.kind.replaceAll("_", " ")} · ${item.id}`);
|
|
229
|
-
const executions = receipts.filter((receipt) => receipt.taskId === item.id && receipt.execution !== void 0).sort((a, b) => a.attempt - b.attempt || a.recordedAt.localeCompare(b.recordedAt)).flatMap((receipt) => receipt.execution ? [receipt.execution] : []);
|
|
230
|
-
if (item.execution) executions.push(item.execution);
|
|
231
|
-
for (const execution of executions) {
|
|
232
|
-
if (history.some((entry) => entry.execution.provider === execution.provider && entry.execution.sessionId === execution.sessionId)) continue;
|
|
233
|
-
const continuation = session.continuations?.find((entry) => entry.taskId === item.id);
|
|
234
|
-
const attachments = continuation ? continuation.attachments ?? [] : item.id === root?.id ? session.attachments : void 0;
|
|
235
|
-
history.push({
|
|
236
|
-
taskId: item.id,
|
|
237
|
-
agent,
|
|
238
|
-
message,
|
|
239
|
-
execution,
|
|
240
|
-
...attachments ? { attachments } : {}
|
|
241
|
-
});
|
|
242
|
-
}
|
|
422
|
+
const byChange = /* @__PURE__ */ new Map();
|
|
423
|
+
for (const observation of observations) {
|
|
424
|
+
if (!changeIds.has(observation.changeId) || byChange.has(observation.changeId)) continue;
|
|
425
|
+
byChange.set(observation.changeId, observation);
|
|
243
426
|
}
|
|
244
|
-
const activeAgent = task ? taskAgent(session, task) : session.agent;
|
|
245
|
-
const execution = (activeAgent ? task?.execution ?? (task === null ? session.execution : void 0) : void 0) ?? null;
|
|
246
|
-
const dependencyTaskIds = (task ? await stores.graphs.get(task.rootTaskId) : null)?.nodes.find((node) => node.taskId === task?.id)?.dependencies;
|
|
247
|
-
const signal = root?.origin.signalId ? await stores.signals.get(root.origin.signalId) : null;
|
|
248
|
-
const changeId = task?.changeId ?? root?.changeId ?? session.changeId;
|
|
249
|
-
const change = changeId ? snapshot?.changes ? snapshot.changes.get(changeId) ?? null : await stores.changes.get(changeId) : null;
|
|
250
|
-
const delegated = workflowTasks(session, byId.values());
|
|
251
|
-
const hasWorkflowFailure = delegated.some((item) => item.state === "failed");
|
|
252
|
-
const canCancel = task === null || !["succeeded", "cancelled"].includes(task.state) || delegated.some((item) => ![
|
|
253
|
-
"succeeded",
|
|
254
|
-
"failed",
|
|
255
|
-
"cancelled"
|
|
256
|
-
].includes(item.state));
|
|
257
|
-
const humanAttention = question ? "question" : workflowAttention(session, byId.values()) ?? attention(task, change);
|
|
258
427
|
return {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
428
|
+
pullRequests: (await mapConcurrent(targets, async ({ change: observed, repository }) => {
|
|
429
|
+
const change = await stores.changes.get(observed.id) ?? observed;
|
|
430
|
+
const observation = byChange.get(change.id) ?? {
|
|
431
|
+
changeId: change.id,
|
|
432
|
+
state: null,
|
|
433
|
+
sync: "unavailable",
|
|
434
|
+
checkedAt,
|
|
435
|
+
syncMessage: "Live pull request status was not returned by the configured reader."
|
|
436
|
+
};
|
|
437
|
+
const state = (change.state === "merged" || change.state === "deployed" || change.state === "rolled_out" || change.state === "rolled_back" ? "merged" : change.state === "closed" ? "closed" : null) ?? observation.state;
|
|
438
|
+
const verification = change.state === "verifying" ? "pending" : change.state === "closed" ? "unknown" : "passed";
|
|
439
|
+
const reviewReadiness = state === "draft" ? "draft" : state === "merged" ? "complete" : state === "closed" ? "closed" : change.state === "verifying" ? "not_ready" : change.state === "ready" ? "ready" : "unknown";
|
|
440
|
+
return sessionPullRequestSchema.parse({
|
|
441
|
+
changeId: change.id,
|
|
442
|
+
repositoryId: repository.id,
|
|
443
|
+
repository: repository.slug,
|
|
444
|
+
number: change.prNumber,
|
|
445
|
+
url: change.prUrl,
|
|
446
|
+
state,
|
|
447
|
+
changeState: change.state,
|
|
448
|
+
verification,
|
|
449
|
+
reviewReadiness,
|
|
450
|
+
sync: observation.sync,
|
|
451
|
+
observedAt: observation.observedAt,
|
|
452
|
+
checkedAt: observation.checkedAt,
|
|
453
|
+
syncMessage: observation.syncMessage
|
|
454
|
+
});
|
|
455
|
+
})).sort((a, b) => a.repository.localeCompare(b.repository) || a.number - b.number),
|
|
456
|
+
truncated
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
async function historyPage(session, query) {
|
|
460
|
+
const page = await stores.conversations.history(session.id, query);
|
|
461
|
+
const items = (await mapConcurrent(page.items, async (entry) => await accessibleExecution(session, entry) ? entry : null)).filter((entry) => entry !== null);
|
|
462
|
+
return {
|
|
463
|
+
...page,
|
|
464
|
+
items
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
async function accessibleExecution(session, entry) {
|
|
468
|
+
if (!agents.some((agent) => agent.id === entry.agent)) return false;
|
|
469
|
+
if (!entry.taskId) return true;
|
|
470
|
+
if (!session.taskId) return false;
|
|
471
|
+
const task = await stores.tasks.get(entry.taskId);
|
|
472
|
+
if (!task || await taskAgent(session, task) !== entry.agent) return false;
|
|
473
|
+
if (task.repositoryIds.length !== 1 || task.repositoryIds[0] !== session.repositoryId) return false;
|
|
474
|
+
if (session.attached && !await mayAccessTask(session.owner, task)) return false;
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
async function view(session, _snapshot, detail = "full", includePullRequests = false, signal = new AbortController().signal) {
|
|
478
|
+
const storedSummary = await stores.conversations.get(session.id);
|
|
479
|
+
const summary = storedSummary ? await authorizedSummary(storedSummary, session.owner) : null;
|
|
480
|
+
if (!summary) throw new ApiError(503, "Conversation projection is unavailable");
|
|
481
|
+
const [task, question, notifications, history, related] = await Promise.all([
|
|
482
|
+
summary.task ? stores.tasks.get(summary.task.id) : null,
|
|
483
|
+
summary.question ? stores.tasks.get(summary.question.id) : null,
|
|
484
|
+
detail === "full" ? stores.sessions.listNotifications(session.id, { limit: 30 }) : {
|
|
485
|
+
items: [],
|
|
486
|
+
nextCursor: null
|
|
487
|
+
},
|
|
488
|
+
detail === "full" ? historyPage(session, { limit: 20 }) : {
|
|
489
|
+
items: [],
|
|
490
|
+
nextCursor: null
|
|
262
491
|
},
|
|
492
|
+
detail === "full" ? stores.tasks.list({
|
|
493
|
+
limit: 30,
|
|
494
|
+
...relatedQuery(session)
|
|
495
|
+
}) : {
|
|
496
|
+
items: [],
|
|
497
|
+
nextCursor: null
|
|
498
|
+
}
|
|
499
|
+
]);
|
|
500
|
+
const continuation = task ? await stores.sessions.getContinuation(session.id, task.id) : null;
|
|
501
|
+
const relatedTasks = (await mapConcurrent(related.items, (item) => authorizedRelatedTask(session, item))).filter((item) => item !== null);
|
|
502
|
+
const associated = includePullRequests ? await sessionPullRequests(session, task, signal) : {
|
|
503
|
+
pullRequests: [],
|
|
504
|
+
truncated: false
|
|
505
|
+
};
|
|
506
|
+
const workGroups = detail === "full" ? await sessionWorkGroups(session, task, relatedTasks, associated.pullRequests) : [];
|
|
507
|
+
return sessionViewSchema.parse({
|
|
508
|
+
...summary,
|
|
509
|
+
session,
|
|
263
510
|
task,
|
|
264
|
-
|
|
511
|
+
finding: await sessionFinding(session, task),
|
|
512
|
+
question,
|
|
513
|
+
continuation,
|
|
514
|
+
notifications: [...notifications.items].reverse(),
|
|
515
|
+
notificationsNextCursor: notifications.nextCursor,
|
|
516
|
+
history: detail === "full" && !session.external ? [...history.items].reverse().filter((item) => item.execution.sessionId !== summary.execution?.sessionId || item.execution.provider !== summary.execution?.provider) : void 0,
|
|
517
|
+
historyNextCursor: history.nextCursor,
|
|
265
518
|
relatedTasks,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
canCancel,
|
|
273
|
-
canFollowUp: options.continuationAgent !== void 0 && root !== null,
|
|
274
|
-
history: includeHistory ? history.filter((entry) => entry.execution.sessionId !== execution?.sessionId || entry.execution.provider !== execution?.provider) : void 0,
|
|
275
|
-
question
|
|
276
|
-
};
|
|
519
|
+
relatedTasksNextCursor: related.nextCursor,
|
|
520
|
+
pullRequests: associated.pullRequests,
|
|
521
|
+
pullRequestsTruncated: associated.truncated,
|
|
522
|
+
workGroups,
|
|
523
|
+
prUrl: summary.prUrl ?? associated.pullRequests.at(-1)?.url
|
|
524
|
+
});
|
|
277
525
|
}
|
|
278
526
|
async function listSessions(url, owner, repositories) {
|
|
279
|
-
const scope = z.enum(["all", "interactive"]).parse(url.searchParams.get("scope") ?? "all");
|
|
280
|
-
const detail = z.enum(["full", "summary"]).parse(url.searchParams.get("detail") ?? "full");
|
|
281
|
-
const limit = url.searchParams.has("limit") ? z.coerce.number().int().min(1).max(100).parse(url.searchParams.get("limit")) : void 0;
|
|
282
527
|
const filters = z.object({
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
528
|
+
detail: z.enum(["full", "summary"]).optional(),
|
|
529
|
+
scope: z.enum(["all", "interactive"]).default("all"),
|
|
530
|
+
limit: z.coerce.number().int().min(1).max(100).default(30),
|
|
531
|
+
cursor: z.string().max(8192).optional(),
|
|
532
|
+
query: z.string().trim().max(200).optional(),
|
|
533
|
+
repository: repositorySlugSchema.optional(),
|
|
534
|
+
agent: z.string().min(1).max(31).optional(),
|
|
535
|
+
source: z.literal("automatic").optional(),
|
|
536
|
+
state: z.union([taskStateSchema, z.literal("conversation")]).optional(),
|
|
537
|
+
attention: z.enum(["true", "false"]).optional(),
|
|
538
|
+
topLevel: z.enum(["true", "false"]).optional(),
|
|
539
|
+
includeTotal: z.enum(["true", "false"]).optional()
|
|
291
540
|
}).parse(Object.fromEntries(url.searchParams));
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
cursor = z.tuple([z.iso.datetime(), z.string().min(1).max(200)]).parse(JSON.parse(Buffer.from(url.searchParams.get("cursor"), "base64url").toString()));
|
|
297
|
-
} catch {
|
|
298
|
-
throw new ApiError(400, "Invalid session cursor");
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
const enabled = (id) => {
|
|
302
|
-
const repository = repositories.get(id);
|
|
303
|
-
return repository?.enabled && options.repositories.includes(repository.slug);
|
|
304
|
-
};
|
|
541
|
+
const enabled = [...repositories.values()].filter((repo) => repo.enabled && options.repositories.includes(repo.slug));
|
|
542
|
+
const selected = filters.repository ? enabled.find((repo) => repo.slug === filters.repository) : void 0;
|
|
543
|
+
if (filters.repository && !selected) throw new ApiError(403, "Repository is not configured");
|
|
305
544
|
const requestedIds = url.searchParams.get("ids");
|
|
306
545
|
if (requestedIds !== null) {
|
|
307
|
-
if (
|
|
546
|
+
if ([...url.searchParams.keys()].some((key) => key !== "ids" && key !== "detail")) throw new ApiError(400, "Session ids cannot be combined with list filters");
|
|
308
547
|
const ids = [...new Set(requestedIds.split(",").filter(Boolean).map((id) => agentSessionIdSchema.parse(id)))];
|
|
309
548
|
if (ids.length === 0 || ids.length > 50) throw new ApiError(400, "Session ids must contain 1–50 session IDs");
|
|
310
549
|
const sessions = await mapConcurrent(ids, async (id) => {
|
|
311
550
|
try {
|
|
312
|
-
|
|
551
|
+
await owned(id, owner);
|
|
552
|
+
const summary = await stores.conversations.get(id);
|
|
553
|
+
const authorized = summary ? await authorizedSummary(summary, owner) : null;
|
|
554
|
+
return authorized ? await findingSummary(authorized) : null;
|
|
313
555
|
} catch (error) {
|
|
314
556
|
if (error instanceof ApiError && error.status === 404) return null;
|
|
315
557
|
throw error;
|
|
316
558
|
}
|
|
317
559
|
});
|
|
318
560
|
return json({
|
|
319
|
-
sessions:
|
|
561
|
+
sessions: sessions.filter((session) => session !== null),
|
|
320
562
|
nextCursor: null
|
|
321
563
|
});
|
|
322
564
|
}
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
const changes = filters.attention === "true" ? new Map((await stores.changes.list()).map((change) => [change.id, change])) : void 0;
|
|
370
|
-
const matches = (filtering ? await mapConcurrent(candidates, async (candidate) => {
|
|
371
|
-
const session = candidate.session;
|
|
372
|
-
const work = session ? sessionTasks(session, byId, linkedTasks.get(session.id) ?? []) : null;
|
|
373
|
-
const task = work?.tasks.at(-1) ?? candidate.task ?? null;
|
|
374
|
-
const root = work?.root ?? candidate.task;
|
|
375
|
-
const entryTask = session?.taskId ? byId.get(session.taskId) : candidate.task;
|
|
376
|
-
if (filters.topLevel === "true" && (entryTask === void 0 || entryTask.parentTaskId !== void 0)) return null;
|
|
377
|
-
const slugs = [.../* @__PURE__ */ new Set([
|
|
378
|
-
...root?.repositoryIds ?? [],
|
|
379
|
-
...task?.repositoryIds ?? [],
|
|
380
|
-
...session?.repositoryId ? [session.repositoryId] : [],
|
|
381
|
-
...session?.external?.repositoryIds ?? []
|
|
382
|
-
])].flatMap((id) => repositories.get(id)?.slug ?? []);
|
|
383
|
-
const agent = session ? task ? taskAgent(session, task) : session.agent : task ? taskRoute(task)?.agent : void 0;
|
|
384
|
-
if (filters.repository && !slugs.some((slug) => slug === filters.repository)) return null;
|
|
385
|
-
if (filters.agent && agent !== filters.agent) return null;
|
|
386
|
-
if (filters.state && (task?.state ?? "conversation") !== filters.state) return null;
|
|
387
|
-
if (filters.source && session && !session.attached && !session.external) return null;
|
|
388
|
-
if (filters.attention === "true") {
|
|
389
|
-
const changeId = task?.changeId ?? root?.changeId ?? session?.changeId;
|
|
390
|
-
const change = changeId ? changes?.get(changeId) ?? null : null;
|
|
391
|
-
if (session?.external ? !(await view(session, void 0, "summary")).attention : !attention(task, change) && !(session && workflowAttention(session, linkedTasks.get(session.id) ?? [])) && !conversationQuestion([...work?.tasks ?? [], ...session ? workflowTasks(session, linkedTasks.get(session.id) ?? []) : []], byId)) return null;
|
|
392
|
-
}
|
|
393
|
-
if (filters.query) {
|
|
394
|
-
const signal = root?.origin.signalId ? await stores.signals.get(root.origin.signalId) : null;
|
|
395
|
-
const route = root ? taskRoute(root) : void 0;
|
|
396
|
-
const searchable = [
|
|
397
|
-
session?.id,
|
|
398
|
-
session?.message,
|
|
399
|
-
...session?.attachments?.map((item) => item.filename) ?? [],
|
|
400
|
-
task?.id,
|
|
401
|
-
agent,
|
|
402
|
-
...slugs,
|
|
403
|
-
route?.id.replaceAll("-", " "),
|
|
404
|
-
signal?.source ?? (session?.attached || candidate.task ? "factory" : ""),
|
|
405
|
-
root?.replyTo.address,
|
|
406
|
-
session?.external?.replyTo.channel,
|
|
407
|
-
session?.external?.replyTo.address
|
|
408
|
-
].join(" ").toLowerCase();
|
|
409
|
-
if (!filters.query.toLowerCase().split(/\s+/u).every((word) => searchable.includes(word))) return null;
|
|
410
|
-
}
|
|
411
|
-
return candidate;
|
|
412
|
-
}) : candidates).filter((candidate) => candidate !== null);
|
|
413
|
-
const ordered = matches.sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.key.localeCompare(a.key)).filter((candidate) => !cursor || candidate.createdAt.localeCompare(cursor[0]) < 0 || candidate.createdAt === cursor[0] && candidate.key.localeCompare(cursor[1]) < 0);
|
|
414
|
-
const page = limit === void 0 ? ordered : ordered.slice(0, limit);
|
|
415
|
-
const visible = await mapConcurrent(page, (candidate) => candidate.load());
|
|
416
|
-
const receipts = detail === "summary" ? null : visible.some((session) => session.taskId !== void 0) ? await stores.receipts.list() : [];
|
|
417
|
-
const last = page.at(-1);
|
|
565
|
+
const shared = options.sharedWorkspaceOwner === owner;
|
|
566
|
+
const page = await stores.conversations.list({
|
|
567
|
+
owner,
|
|
568
|
+
repositoryId: selected?.id,
|
|
569
|
+
attention: filters.attention === "true",
|
|
570
|
+
limit: filters.limit,
|
|
571
|
+
cursor: filters.cursor,
|
|
572
|
+
search: filters.query || void 0,
|
|
573
|
+
includeTotal: filters.includeTotal === "true" && !options.canAccessTask,
|
|
574
|
+
match: {
|
|
575
|
+
...filters.scope === "interactive" ? { interactive: true } : {},
|
|
576
|
+
...filters.source ? { automatic: true } : {},
|
|
577
|
+
...!shared && !options.canAccessTask ? { attached: false } : {},
|
|
578
|
+
...filters.agent ? { agent: filters.agent } : {},
|
|
579
|
+
...filters.state ? { state: filters.state } : {},
|
|
580
|
+
...filters.topLevel === "true" ? { topLevel: true } : {}
|
|
581
|
+
},
|
|
582
|
+
fields: [
|
|
583
|
+
{
|
|
584
|
+
path: ["repositoryIds"],
|
|
585
|
+
test: "subset",
|
|
586
|
+
value: enabled.map((repo) => repo.id)
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
path: ["sessionAgent"],
|
|
590
|
+
test: "oneOf",
|
|
591
|
+
value: agents.map((agent) => agent.id)
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
path: ["agent"],
|
|
595
|
+
test: "oneOf",
|
|
596
|
+
value: agents.map((agent) => agent.id)
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
path: ["routeAgent"],
|
|
600
|
+
test: "oneOf",
|
|
601
|
+
value: [
|
|
602
|
+
...agents.map((agent) => JSON.stringify(["", agent.id])),
|
|
603
|
+
...options.continuationAgent ? [JSON.stringify(["conversation@1", options.continuationAgent])] : [],
|
|
604
|
+
...options.routes.routes.filter((route) => agents.some((agent) => agent.id === route.agent)).map((route) => JSON.stringify([`${route.id}@${route.version}`, route.agent]))
|
|
605
|
+
]
|
|
606
|
+
}
|
|
607
|
+
]
|
|
608
|
+
});
|
|
609
|
+
const sessions = options.canAccessTask ? (await mapConcurrent(page.items, (summary) => authorizedSummary(summary, owner))).filter((summary) => summary !== null) : page.items;
|
|
418
610
|
return json({
|
|
419
|
-
sessions: await mapConcurrent(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
changes
|
|
423
|
-
}, detail)),
|
|
424
|
-
...filters.includeTotal === "true" ? { totalCount: matches.length } : {},
|
|
425
|
-
...limit === void 0 ? {} : { nextCursor: last && ordered.length > page.length ? Buffer.from(JSON.stringify([last.createdAt, last.key])).toString("base64url") : null }
|
|
611
|
+
sessions: await mapConcurrent(sessions, findingSummary),
|
|
612
|
+
nextCursor: page.nextCursor,
|
|
613
|
+
...page.totalCount === void 0 ? {} : { totalCount: page.totalCount }
|
|
426
614
|
});
|
|
427
615
|
}
|
|
428
616
|
async function assertRepository(id) {
|
|
@@ -438,7 +626,7 @@ function createFactoryApi(options) {
|
|
|
438
626
|
if (session.external) await mapConcurrent(session.external.repositoryIds, assertRepository);
|
|
439
627
|
if (session.attached) {
|
|
440
628
|
const task = session.taskId ? await stores.tasks.get(session.taskId) : null;
|
|
441
|
-
if (!task || !await
|
|
629
|
+
if (!task || !await mayAccessTask(owner, task)) throw new ApiError(404, "Session not found");
|
|
442
630
|
}
|
|
443
631
|
return session;
|
|
444
632
|
}
|
|
@@ -451,15 +639,41 @@ function createFactoryApi(options) {
|
|
|
451
639
|
}
|
|
452
640
|
} else {
|
|
453
641
|
const boundTask = session.taskId === current.task.id ? current.task : session.taskId ? await stores.tasks.get(session.taskId) : null;
|
|
454
|
-
const
|
|
455
|
-
|
|
642
|
+
const recovery = recoveryTaskId ? await stores.tasks.get(taskIdSchema.parse(recoveryTaskId)) : null;
|
|
643
|
+
if (recoveryTaskId && (!recovery || recovery.id !== session.taskId && (recovery.replyTo.channel !== "api" || recovery.replyTo.address !== session.id || recovery.repositoryIds.length !== 1 || recovery.repositoryIds[0] !== session.repositoryId))) throw new ApiError(403, "Recovery Task does not belong to this interaction");
|
|
644
|
+
const answered = recoveryTaskId ? null : (await stores.tasks.list({
|
|
645
|
+
limit: 1,
|
|
646
|
+
match: {
|
|
647
|
+
replyTo: {
|
|
648
|
+
channel: "api",
|
|
649
|
+
address: session.id
|
|
650
|
+
},
|
|
651
|
+
state: "queued"
|
|
652
|
+
},
|
|
653
|
+
fields: [
|
|
654
|
+
{
|
|
655
|
+
path: ["repositoryIds"],
|
|
656
|
+
test: "eq",
|
|
657
|
+
value: session.repositoryId ? [session.repositoryId] : []
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
path: ["work", "route"],
|
|
661
|
+
test: "exists"
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
path: ["answerEventIds"],
|
|
665
|
+
test: "notEmpty"
|
|
666
|
+
}
|
|
667
|
+
]
|
|
668
|
+
})).items[0];
|
|
669
|
+
const task = recovery ?? (boundTask?.state === "queued" && boundTask.answerEventIds?.length ? boundTask : answered) ?? current.task;
|
|
456
670
|
if (task?.state === "queued") {
|
|
457
|
-
if (!taskAgent(session, task)) throw new ApiError(409, "Task has no configured agent route");
|
|
671
|
+
if (!await taskAgent(session, task)) throw new ApiError(409, "Task has no configured agent route");
|
|
458
672
|
const answers = await Promise.all((task.answerEventIds ?? []).map((id) => stores.answers.get(id)));
|
|
459
673
|
const message = [
|
|
460
674
|
`You are assigned Factory task ${task.id}. Read it before starting.`,
|
|
461
675
|
session.changeId === void 0 ? "" : `Existing change: ${session.changeId}.`,
|
|
462
|
-
|
|
676
|
+
(await stores.sessions.getContinuation(session.id, task.id))?.message ?? session.message,
|
|
463
677
|
...answers.flatMap((answer) => answer === null ? [] : [`${answer.question}\nAnswer: ${answer.answer}`])
|
|
464
678
|
].filter(Boolean).join("\n\n");
|
|
465
679
|
try {
|
|
@@ -468,14 +682,14 @@ function createFactoryApi(options) {
|
|
|
468
682
|
route: task.work.route,
|
|
469
683
|
budget: options.budget,
|
|
470
684
|
launch: async (running) => {
|
|
471
|
-
const continuation =
|
|
472
|
-
if (continuation && (continuation.agent !== options.continuationAgent || taskAgent(session, running) !== continuation.agent)) throw new ApiError(409, "Conversation agent is no longer configured");
|
|
473
|
-
if (continuation) return start(executionSession(session, running), running, await continuationBrief(session, current, continuation.message, running.id), `factory-task:${running.id}:${running.attempt}`);
|
|
685
|
+
const continuation = await stores.sessions.getContinuation(session.id, running.id);
|
|
686
|
+
if (continuation && (continuation.agent !== options.continuationAgent || await taskAgent(session, running) !== continuation.agent)) throw new ApiError(409, "Conversation agent is no longer configured");
|
|
687
|
+
if (continuation) return start(await executionSession(session, running), running, await continuationBrief(session, current, continuation.message, running.id), `factory-task:${running.id}:${running.attempt}`);
|
|
474
688
|
if (session.attached || running.id !== session.taskId) {
|
|
475
689
|
if (!options.resumeTask) throw new ApiError(409, "Recovery for existing work is not configured");
|
|
476
690
|
return options.resumeTask(running);
|
|
477
691
|
}
|
|
478
|
-
return start(executionSession(session, running), running, message, `factory-task:${running.id}:${running.attempt}`);
|
|
692
|
+
return start(await executionSession(session, running), running, message, `factory-task:${running.id}:${running.attempt}`);
|
|
479
693
|
}
|
|
480
694
|
});
|
|
481
695
|
} catch (error) {
|
|
@@ -575,11 +789,20 @@ function createFactoryApi(options) {
|
|
|
575
789
|
const repository = await stores.repositories.get(repositoryId);
|
|
576
790
|
if (!repository) throw new ApiError(404, "Repository not found");
|
|
577
791
|
await assertRepository(repositoryId);
|
|
578
|
-
const active = (await stores.tasks.list(
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
792
|
+
const active = (await stores.tasks.list({
|
|
793
|
+
limit: 1,
|
|
794
|
+
match: [
|
|
795
|
+
"queued",
|
|
796
|
+
"running",
|
|
797
|
+
"needs_human",
|
|
798
|
+
"stalled",
|
|
799
|
+
"verifying"
|
|
800
|
+
].map((state) => ({
|
|
801
|
+
kind: "security_scan",
|
|
802
|
+
repositoryIds: [repository.id],
|
|
803
|
+
state
|
|
804
|
+
}))
|
|
805
|
+
})).items[0];
|
|
583
806
|
if (active) return json({
|
|
584
807
|
taskId: active.id,
|
|
585
808
|
reused: true
|
|
@@ -609,7 +832,7 @@ function createFactoryApi(options) {
|
|
|
609
832
|
}, 202);
|
|
610
833
|
}
|
|
611
834
|
async function continuationBrief(session, current, message, taskId) {
|
|
612
|
-
const continuation =
|
|
835
|
+
const continuation = await stores.sessions.getContinuation(session.id, taskId);
|
|
613
836
|
const saved = continuation?.context;
|
|
614
837
|
const historicalImages = [];
|
|
615
838
|
const task = await stores.tasks.get(taskId);
|
|
@@ -625,9 +848,9 @@ function createFactoryApi(options) {
|
|
|
625
848
|
execution: current.execution
|
|
626
849
|
});
|
|
627
850
|
const sections = await Promise.all(entries.slice(-12).map(async (entry, index) => {
|
|
628
|
-
const task = entry.taskId ? await stores.tasks.get(entry.taskId) : null;
|
|
629
851
|
try {
|
|
630
|
-
const snapshot = await
|
|
852
|
+
const snapshot = (await stores.transcripts.page(entry.execution, { limit: 100 }))?.snapshot;
|
|
853
|
+
if (!snapshot || snapshot.cursor === 0) throw new ApiError(404, "Transcript has not been persisted yet");
|
|
631
854
|
const transcript = new Transcript();
|
|
632
855
|
transcript.hydrate(snapshot);
|
|
633
856
|
historicalImages[index] = transcript.blocks.flatMap((block) => block.attachments ?? []);
|
|
@@ -642,7 +865,7 @@ function createFactoryApi(options) {
|
|
|
642
865
|
}));
|
|
643
866
|
const candidates = [
|
|
644
867
|
...session.attachments ?? [],
|
|
645
|
-
...session.
|
|
868
|
+
...(await stores.sessions.listContinuations(session.id, { limit: 12 })).items.flatMap((item) => item.contextAttachments ?? item.attachments ?? []),
|
|
646
869
|
...historicalImages.flat(),
|
|
647
870
|
...continuation?.attachments ?? []
|
|
648
871
|
];
|
|
@@ -688,38 +911,32 @@ function createFactoryApi(options) {
|
|
|
688
911
|
attachments: selected
|
|
689
912
|
}));
|
|
690
913
|
}
|
|
691
|
-
async function
|
|
692
|
-
const
|
|
693
|
-
if (
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
})}`;
|
|
700
|
-
const response = await options.forward({
|
|
701
|
-
session: executionSession(session, task, agent),
|
|
702
|
-
execution,
|
|
703
|
-
path,
|
|
704
|
-
request: new Request("http://factory/transcript", { signal: AbortSignal.any([signal, AbortSignal.timeout(15e3)]) })
|
|
705
|
-
});
|
|
706
|
-
if (!response.ok || !response.body) {
|
|
707
|
-
await response.body?.cancel();
|
|
708
|
-
throw new ApiError(response.status || 502, response.status === 404 ? "Execution transcript is unavailable on this deployment. It may belong to another deployment or have expired." : "Execution transcript is unavailable");
|
|
914
|
+
async function transcriptResponse(url, execution) {
|
|
915
|
+
const blockId = url.searchParams.get("blockId");
|
|
916
|
+
if (blockId !== null) {
|
|
917
|
+
z.string().min(1).max(1e3).parse(blockId);
|
|
918
|
+
if (url.searchParams.has("cursor") || url.searchParams.has("limit")) throw new ApiError(400, "Block reads cannot include page parameters");
|
|
919
|
+
const block = await stores.transcripts.block(execution, blockId);
|
|
920
|
+
if (!block) throw new ApiError(404, "Transcript block not found");
|
|
921
|
+
return json(block);
|
|
709
922
|
}
|
|
710
|
-
const
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
923
|
+
const limit = z.coerce.number().int().min(1).max(100).parse(url.searchParams.get("limit") ?? 30);
|
|
924
|
+
const cursor = url.searchParams.get("cursor") ?? void 0;
|
|
925
|
+
const page = await stores.transcripts.page(execution, {
|
|
926
|
+
limit,
|
|
927
|
+
cursor
|
|
928
|
+
});
|
|
929
|
+
return page ? json(page) : json({
|
|
930
|
+
snapshot: {
|
|
931
|
+
blocks: [],
|
|
932
|
+
inputs: [],
|
|
933
|
+
cursor: 0,
|
|
934
|
+
state: "connecting",
|
|
935
|
+
tokens: 0
|
|
936
|
+
},
|
|
937
|
+
nextCursor: null,
|
|
938
|
+
totalBlocks: 0
|
|
939
|
+
}, 202);
|
|
723
940
|
}
|
|
724
941
|
async function proxy(session, request, suffix, payload) {
|
|
725
942
|
const current = await view(session, void 0, "summary");
|
|
@@ -747,7 +964,7 @@ function createFactoryApi(options) {
|
|
|
747
964
|
...request.method === "POST" ? { body: JSON.stringify(payload !== null && typeof payload === "object" ? Object.fromEntries(Object.entries(payload).filter(([key]) => key !== "executionId" && key !== "operationId")) : {}) } : {}
|
|
748
965
|
});
|
|
749
966
|
const result = await options.forward({
|
|
750
|
-
session: executionSession(session, current.task),
|
|
967
|
+
session: await executionSession(session, current.task),
|
|
751
968
|
execution: current.execution,
|
|
752
969
|
path: suffix,
|
|
753
970
|
request: upstream
|
|
@@ -803,36 +1020,50 @@ function createFactoryApi(options) {
|
|
|
803
1020
|
if (path.length > 3) throw new ApiError(404, "API route not found");
|
|
804
1021
|
if (resource === "intake") {
|
|
805
1022
|
if (!auth.actor || !(options.operators ?? []).includes(auth.actor)) throw new ApiError(403, "Intake requires an individually authenticated configured operator");
|
|
806
|
-
const repos = (await
|
|
1023
|
+
const repos = (await configuredRepositories()).filter((repo) => repo.enabled && options.repositories.includes(repo.slug));
|
|
807
1024
|
const repository = url.searchParams.get("repository");
|
|
808
1025
|
if (repository && !options.repositories.some((slug) => slug === repository)) throw new ApiError(403, "Repository is not configured");
|
|
809
1026
|
const allowed = new Map(repos.filter((repo) => !repository || repo.slug === repository).map((repo) => [repo.id, repo.slug]));
|
|
810
|
-
const
|
|
811
|
-
|
|
1027
|
+
const view = async (signal) => {
|
|
1028
|
+
const tasks = await stores.tasks.list({
|
|
1029
|
+
limit: 30,
|
|
1030
|
+
match: { origin: { signalId: signal.id } }
|
|
1031
|
+
});
|
|
812
1032
|
const { payload: _payload, ...record } = signal;
|
|
813
1033
|
return intakeViewSchema.parse({
|
|
814
1034
|
...record,
|
|
815
1035
|
repository: allowed.get(signal.repositoryId),
|
|
816
|
-
taskIds: tasks.
|
|
1036
|
+
taskIds: tasks.items.map((task) => task.id),
|
|
1037
|
+
tasksNextCursor: tasks.nextCursor
|
|
817
1038
|
});
|
|
818
1039
|
};
|
|
819
1040
|
if (path.length === 1 && request.method === "GET") {
|
|
820
1041
|
const limit = z.coerce.number().int().min(1).max(100).parse(url.searchParams.get("limit") ?? 100);
|
|
821
1042
|
const cursor = url.searchParams.get("cursor");
|
|
822
|
-
|
|
823
|
-
|
|
1043
|
+
if (allowed.size === 0) return json({
|
|
1044
|
+
items: [],
|
|
1045
|
+
nextCursor: null
|
|
1046
|
+
});
|
|
1047
|
+
const page = await stores.signals.list({
|
|
1048
|
+
limit,
|
|
1049
|
+
cursor: cursor ?? void 0,
|
|
1050
|
+
match: [...allowed.keys()].map((repositoryId) => ({ repositoryId }))
|
|
1051
|
+
});
|
|
824
1052
|
return json({
|
|
825
|
-
items,
|
|
826
|
-
nextCursor:
|
|
1053
|
+
items: await mapConcurrent(page.items, view),
|
|
1054
|
+
nextCursor: page.nextCursor
|
|
827
1055
|
});
|
|
828
1056
|
}
|
|
829
1057
|
if (id && path.length === 3 && action === "retry" && request.method === "POST") {
|
|
830
1058
|
const signal = await stores.signals.get(signalIdSchema.parse(id));
|
|
831
1059
|
if (!signal || !allowed.has(signal.repositoryId)) throw new ApiError(404, "Intake not found");
|
|
832
1060
|
const input = retryIntakeSchema.parse(await body(request));
|
|
833
|
-
if (signal.outcome !== "pending" || signal.triage?.state !== "blocked" || signal.triage.attempt !== input.expectedAttempt ||
|
|
1061
|
+
if (signal.outcome !== "pending" || signal.triage?.state !== "blocked" || signal.triage.attempt !== input.expectedAttempt || (await stores.tasks.list({
|
|
1062
|
+
limit: 1,
|
|
1063
|
+
match: { origin: { signalId: signal.id } }
|
|
1064
|
+
})).items.length > 0) throw new ApiError(409, "Only the observed blocked attempt with no linked task can be retried; refresh intake");
|
|
834
1065
|
if (!options.retryIntake) throw new ApiError(409, "This factory has no retry launcher for this intake source");
|
|
835
|
-
return json(view(await options.retryIntake(signal, {
|
|
1066
|
+
return json(await view(await options.retryIntake(signal, {
|
|
836
1067
|
...input,
|
|
837
1068
|
by: auth.actor
|
|
838
1069
|
})));
|
|
@@ -885,7 +1116,7 @@ function createFactoryApi(options) {
|
|
|
885
1116
|
reason: "External activity is not configured."
|
|
886
1117
|
});
|
|
887
1118
|
const allowedRepositories = async () => {
|
|
888
|
-
const disabled = new Set((await
|
|
1119
|
+
const disabled = new Set((await configuredRepositories()).filter((repo) => !repo.enabled).map((repo) => repo.slug));
|
|
889
1120
|
return options.repositories.filter((slug) => !disabled.has(slug));
|
|
890
1121
|
};
|
|
891
1122
|
const repositories = await allowedRepositories();
|
|
@@ -908,7 +1139,7 @@ function createFactoryApi(options) {
|
|
|
908
1139
|
if (resource === "agents") return json({ agents });
|
|
909
1140
|
if (resource === "repositories") return json({
|
|
910
1141
|
repositories: options.repositories,
|
|
911
|
-
details: (await
|
|
1142
|
+
details: (await configuredRepositories()).filter((repo) => options.repositories.includes(repo.slug)).map(({ id, slug }) => ({
|
|
912
1143
|
id,
|
|
913
1144
|
slug
|
|
914
1145
|
}))
|
|
@@ -916,14 +1147,44 @@ function createFactoryApi(options) {
|
|
|
916
1147
|
const requestedRepositoryValue = resource === "changes" ? url.searchParams.get("repository") : null;
|
|
917
1148
|
const requestedRepository = requestedRepositoryValue === null ? null : repositorySlugSchema.parse(requestedRepositoryValue);
|
|
918
1149
|
if (requestedRepository !== null && !options.repositories.includes(requestedRepository)) throw new ApiError(403, "Repository is not configured");
|
|
919
|
-
const repositories = new Map((await
|
|
1150
|
+
const repositories = new Map((await configuredRepositories()).map((repo) => [repo.id, repo]));
|
|
920
1151
|
const enabledIds = new Set([...repositories.values()].filter((repo) => repo.enabled && options.repositories.includes(repo.slug) && (requestedRepository === null || repo.slug === requestedRepository)).map((repo) => repo.id));
|
|
921
|
-
|
|
1152
|
+
const query = {
|
|
1153
|
+
limit: z.coerce.number().int().min(1).max(100).parse(url.searchParams.get("limit") ?? 30),
|
|
1154
|
+
cursor: url.searchParams.get("cursor") ?? void 0
|
|
1155
|
+
};
|
|
1156
|
+
if (resource === "changes") {
|
|
1157
|
+
if (!enabledIds.size) return json({
|
|
1158
|
+
changes: [],
|
|
1159
|
+
nextCursor: null
|
|
1160
|
+
});
|
|
1161
|
+
const page = await stores.changes.list({
|
|
1162
|
+
...query,
|
|
1163
|
+
match: [...enabledIds].flatMap((repositoryId) => ["verifying", "ready"].map((state) => ({
|
|
1164
|
+
repositoryId,
|
|
1165
|
+
state
|
|
1166
|
+
})))
|
|
1167
|
+
});
|
|
1168
|
+
return json({
|
|
1169
|
+
changes: page.items,
|
|
1170
|
+
nextCursor: page.nextCursor
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
922
1173
|
if (resource === "sessions") return await listSessions(url, auth.id, repositories);
|
|
923
1174
|
if (resource === "tasks") {
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
1175
|
+
const page = await stores.tasks.list({
|
|
1176
|
+
...query,
|
|
1177
|
+
fields: [{
|
|
1178
|
+
path: ["repositoryIds"],
|
|
1179
|
+
test: "subset",
|
|
1180
|
+
value: [...enabledIds]
|
|
1181
|
+
}]
|
|
1182
|
+
});
|
|
1183
|
+
const tasks = (await mapConcurrent(page.items, async (task) => task.origin.operator === auth.id || task.replyTo.channel !== "api" && await mayAccessTask(auth.id, task) ? task : null)).filter((task) => task !== null);
|
|
1184
|
+
return json({
|
|
1185
|
+
tasks,
|
|
1186
|
+
nextCursor: page.nextCursor
|
|
1187
|
+
});
|
|
927
1188
|
}
|
|
928
1189
|
}
|
|
929
1190
|
if (request.method === "POST" && path.length === 1 && (resource === "signals" || resource === "sessions")) return await create(request, auth.id, resource === "sessions");
|
|
@@ -932,10 +1193,11 @@ function createFactoryApi(options) {
|
|
|
932
1193
|
if (session.external && request.method === "POST") throw new ApiError(403, "External conversations are read-only; continue in the original channel");
|
|
933
1194
|
if (request.method === "GET" && action === void 0) {
|
|
934
1195
|
const detail = z.enum(["full", "summary"]).parse(url.searchParams.get("detail") ?? "full");
|
|
935
|
-
return json(await view(session, void 0, detail));
|
|
1196
|
+
return json(await view(session, void 0, detail, true, request.signal));
|
|
936
1197
|
}
|
|
937
1198
|
if (request.method === "GET" && (action === "graph" || action === "graph-transcript" || action === "graph-stream")) {
|
|
938
|
-
const
|
|
1199
|
+
const taskId = (action === "graph" ? taskIdSchema.optional() : taskIdSchema).parse(url.searchParams.get("taskId") ?? void 0);
|
|
1200
|
+
const graph = await readSessionGraph(options, session, taskId);
|
|
939
1201
|
if (action === "graph") {
|
|
940
1202
|
const current = await view(session, void 0, "summary");
|
|
941
1203
|
const currentTaskId = graph.nodes.find((node) => node.task.id === current.task?.id)?.task.id ?? null;
|
|
@@ -945,7 +1207,6 @@ function createFactoryApi(options) {
|
|
|
945
1207
|
nodes: graph.nodes.map(({ session: _session, ...node }) => node)
|
|
946
1208
|
}));
|
|
947
1209
|
}
|
|
948
|
-
const taskId = taskIdSchema.parse(url.searchParams.get("taskId"));
|
|
949
1210
|
const node = graph.nodes.find((item) => item.task.id === taskId);
|
|
950
1211
|
if (!node) throw new ApiError(404, "Task is not part of this graph");
|
|
951
1212
|
const execution = node.task.execution;
|
|
@@ -958,7 +1219,7 @@ function createFactoryApi(options) {
|
|
|
958
1219
|
if (value !== null) query.set(name, value);
|
|
959
1220
|
}
|
|
960
1221
|
const response = await options.forward({
|
|
961
|
-
session: executionSession(node.session, node.task, node.agent),
|
|
1222
|
+
session: await executionSession(node.session, node.task, node.agent),
|
|
962
1223
|
execution,
|
|
963
1224
|
path: `/stream?${query}`,
|
|
964
1225
|
request
|
|
@@ -978,10 +1239,10 @@ function createFactoryApi(options) {
|
|
|
978
1239
|
}
|
|
979
1240
|
});
|
|
980
1241
|
}
|
|
981
|
-
const
|
|
1242
|
+
const response = await transcriptResponse(url, execution);
|
|
982
1243
|
const latest = await stores.tasks.get(node.task.id);
|
|
983
1244
|
if (latest?.execution?.sessionId !== execution.sessionId || latest.execution.provider !== execution.provider) throw new ApiError(409, "Execution changed; refresh the graph.");
|
|
984
|
-
return
|
|
1245
|
+
return response;
|
|
985
1246
|
}
|
|
986
1247
|
if (request.method === "GET" && (action === "review" || action === "previews")) {
|
|
987
1248
|
const current = await view(session, void 0, "summary");
|
|
@@ -995,17 +1256,31 @@ function createFactoryApi(options) {
|
|
|
995
1256
|
reason: action === "review" ? "Review evidence is not configured for this factory." : "Live previews are not configured for this factory."
|
|
996
1257
|
});
|
|
997
1258
|
const reviewTasks = async (value) => {
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1259
|
+
const page = value.session.external ? {
|
|
1260
|
+
items: [],
|
|
1261
|
+
nextCursor: null
|
|
1262
|
+
} : await stores.tasks.list({
|
|
1263
|
+
limit: 100,
|
|
1264
|
+
match: { replyTo: {
|
|
1265
|
+
channel: "api",
|
|
1266
|
+
address: value.session.id
|
|
1267
|
+
} },
|
|
1268
|
+
fields: [{
|
|
1269
|
+
path: ["repositoryIds"],
|
|
1270
|
+
test: "eq",
|
|
1271
|
+
value: value.session.repositoryId ? [value.session.repositoryId] : []
|
|
1272
|
+
}, {
|
|
1273
|
+
path: ["question"],
|
|
1274
|
+
test: "missing"
|
|
1275
|
+
}]
|
|
1276
|
+
});
|
|
1277
|
+
return {
|
|
1278
|
+
tasks: [...new Map([...value.task ? [value.task] : [], ...page.items].map((task) => [task.id, task])).values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)),
|
|
1279
|
+
truncated: page.nextCursor !== null
|
|
1280
|
+
};
|
|
1007
1281
|
};
|
|
1008
|
-
const
|
|
1282
|
+
const selected = await reviewTasks(current);
|
|
1283
|
+
const tasks = selected.tasks;
|
|
1009
1284
|
const identity = (value, work) => JSON.stringify([
|
|
1010
1285
|
value.execution,
|
|
1011
1286
|
value.task?.id,
|
|
@@ -1023,9 +1298,13 @@ function createFactoryApi(options) {
|
|
|
1023
1298
|
signal: request.signal
|
|
1024
1299
|
}));
|
|
1025
1300
|
const latest = await view(await owned(id, auth.id), void 0, "summary");
|
|
1026
|
-
if (identity(current, tasks) !== identity(latest, await reviewTasks(latest))) throw new ApiError(409, "Execution changed; refresh the session before reviewing work");
|
|
1301
|
+
if (identity(current, tasks) !== identity(latest, (await reviewTasks(latest)).tasks)) throw new ApiError(409, "Execution changed; refresh the session before reviewing work");
|
|
1027
1302
|
if (result.executionId !== executionId) throw new Error("Review reader returned a different execution");
|
|
1028
|
-
return json(result
|
|
1303
|
+
return json(result.state === "ready" && selected.truncated ? {
|
|
1304
|
+
...result,
|
|
1305
|
+
truncated: true,
|
|
1306
|
+
limitations: [...result.limitations, "Evidence covers the 100 most recent related Tasks and the current Task."]
|
|
1307
|
+
} : result);
|
|
1029
1308
|
}
|
|
1030
1309
|
if (request.method === "GET" && action === "files") {
|
|
1031
1310
|
const query = workspaceFilesRequestSchema.parse({
|
|
@@ -1042,7 +1321,7 @@ function createFactoryApi(options) {
|
|
|
1042
1321
|
reason: execution ? "File browsing is not configured for this factory." : "The workspace is not available yet."
|
|
1043
1322
|
});
|
|
1044
1323
|
const result = workspaceFilesSchema.parse(await options.workspaceFiles({
|
|
1045
|
-
session: executionSession(session, current.task),
|
|
1324
|
+
session: await executionSession(session, current.task),
|
|
1046
1325
|
execution,
|
|
1047
1326
|
query,
|
|
1048
1327
|
signal: request.signal
|
|
@@ -1061,7 +1340,7 @@ function createFactoryApi(options) {
|
|
|
1061
1340
|
reason: execution ? "Workspace inspection is not configured for this factory." : "The workspace is not available yet."
|
|
1062
1341
|
}));
|
|
1063
1342
|
const result = workspaceDiffSchema.parse(await options.workspaceDiff({
|
|
1064
|
-
session: executionSession(session, current.task),
|
|
1343
|
+
session: await executionSession(session, current.task),
|
|
1065
1344
|
execution,
|
|
1066
1345
|
signal: request.signal
|
|
1067
1346
|
}));
|
|
@@ -1070,17 +1349,37 @@ function createFactoryApi(options) {
|
|
|
1070
1349
|
if (result.executionId !== execution.sessionId) throw new Error("Workspace inspection returned a different execution");
|
|
1071
1350
|
return json(result);
|
|
1072
1351
|
}
|
|
1352
|
+
if (request.method === "GET" && [
|
|
1353
|
+
"history",
|
|
1354
|
+
"notifications",
|
|
1355
|
+
"related-tasks"
|
|
1356
|
+
].includes(action ?? "")) {
|
|
1357
|
+
const query = {
|
|
1358
|
+
limit: z.coerce.number().int().min(1).max(100).parse(url.searchParams.get("limit") ?? 30),
|
|
1359
|
+
cursor: url.searchParams.get("cursor") ?? void 0
|
|
1360
|
+
};
|
|
1361
|
+
if (action === "history") return json(await historyPage(session, query));
|
|
1362
|
+
if (action === "notifications") return json(await stores.sessions.listNotifications(session.id, query));
|
|
1363
|
+
const page = await stores.tasks.list({
|
|
1364
|
+
...query,
|
|
1365
|
+
...relatedQuery(session)
|
|
1366
|
+
});
|
|
1367
|
+
const items = (await mapConcurrent(page.items, async (task) => session.external ? task.repositoryIds.every((id) => session.external.repositoryIds.includes(id)) && await mayAccessTask(session.owner, task) ? task : null : task.repositoryIds.length === 1 && task.repositoryIds[0] === session.repositoryId ? task : null)).filter((task) => task !== null);
|
|
1368
|
+
return json({
|
|
1369
|
+
items,
|
|
1370
|
+
nextCursor: page.nextCursor
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1073
1373
|
if (request.method === "GET" && action === "transcript") {
|
|
1074
1374
|
const executionId = z.string().min(1).parse(url.searchParams.get("executionId"));
|
|
1075
1375
|
if (url.searchParams.get("history") === "1") {
|
|
1076
|
-
const entry =
|
|
1077
|
-
if (!entry) throw new ApiError(404, "Execution is not part of this conversation history");
|
|
1078
|
-
|
|
1079
|
-
return json(await materializedTranscript(session, task, entry.agent, entry.execution, request.signal));
|
|
1376
|
+
const entry = await stores.conversations.execution(session.id, executionId);
|
|
1377
|
+
if (!entry || !await accessibleExecution(session, entry)) throw new ApiError(404, "Execution is not part of this conversation history");
|
|
1378
|
+
return await transcriptResponse(url, entry.execution);
|
|
1080
1379
|
}
|
|
1081
1380
|
const current = await view(session, void 0, "summary");
|
|
1082
1381
|
if (current.execution?.sessionId !== executionId) throw new ApiError(409, "Execution changed; refresh the session before loading its transcript");
|
|
1083
|
-
return
|
|
1382
|
+
return await transcriptResponse(url, current.execution);
|
|
1084
1383
|
}
|
|
1085
1384
|
if (request.method === "GET" && action === "stream") {
|
|
1086
1385
|
const query = new URLSearchParams();
|
|
@@ -1089,12 +1388,12 @@ function createFactoryApi(options) {
|
|
|
1089
1388
|
if (value !== null) query.set(name, value);
|
|
1090
1389
|
}
|
|
1091
1390
|
if (url.searchParams.get("history") === "1") {
|
|
1092
|
-
const entry =
|
|
1093
|
-
if (!entry) throw new ApiError(404, "Execution is not part of this conversation history");
|
|
1391
|
+
const entry = await stores.conversations.execution(session.id, z.string().min(1).parse(url.searchParams.get("executionId")));
|
|
1392
|
+
if (!entry || !await accessibleExecution(session, entry)) throw new ApiError(404, "Execution is not part of this conversation history");
|
|
1094
1393
|
query.set("includeTailIndex", "1");
|
|
1095
1394
|
const task = entry.taskId ? await stores.tasks.get(entry.taskId) : null;
|
|
1096
1395
|
const response = await options.forward({
|
|
1097
|
-
session: executionSession(session, task, entry.agent),
|
|
1396
|
+
session: await executionSession(session, task, entry.agent),
|
|
1098
1397
|
execution: entry.execution,
|
|
1099
1398
|
path: `/stream?${query}`,
|
|
1100
1399
|
request
|
|
@@ -1114,7 +1413,7 @@ function createFactoryApi(options) {
|
|
|
1114
1413
|
if (request.method !== "POST") throw new ApiError(405, "Use GET or POST for this route");
|
|
1115
1414
|
if (action === "messages") {
|
|
1116
1415
|
const input = sendSessionMessageRequestSchema.parse(await body(request));
|
|
1117
|
-
const previous = input.operationId ?
|
|
1416
|
+
const previous = input.operationId ? await stores.sessions.getContinuationByOperation(session.id, input.operationId) : void 0;
|
|
1118
1417
|
if (previous) {
|
|
1119
1418
|
if (previous.message !== input.message || JSON.stringify(previous.attachments ?? []) !== JSON.stringify(input.attachments ?? [])) throw new ApiError(409, "Follow-up operationId was reused with different input");
|
|
1120
1419
|
const recovered = await stores.sessions.continue(session.id, previous);
|
|
@@ -1122,7 +1421,7 @@ function createFactoryApi(options) {
|
|
|
1122
1421
|
return json(current.task?.id === previous.taskId ? await launch(recovered) : current, 202);
|
|
1123
1422
|
}
|
|
1124
1423
|
const current = await view(session);
|
|
1125
|
-
if (current.task?.state === "running" &&
|
|
1424
|
+
if (current.task?.state === "running" && await stores.sessions.getContinuation(session.id, current.task.id)) throw new ApiError(409, "Wait for this discussion turn to finish before sending another follow-up");
|
|
1126
1425
|
if ((current.execution?.sessionId ?? null) !== input.executionId) throw new ApiError(409, "Execution changed; refresh the session before continuing");
|
|
1127
1426
|
if (current.task && [
|
|
1128
1427
|
"succeeded",
|
|
@@ -1148,11 +1447,8 @@ function createFactoryApi(options) {
|
|
|
1148
1447
|
const input = answerSessionQuestionRequestSchema.parse(await body(request));
|
|
1149
1448
|
const current = await view(session);
|
|
1150
1449
|
const question = await stores.tasks.get(input.questionId);
|
|
1151
|
-
const parent = session.taskId && question?.parentTaskId === session.taskId ? await stores.tasks.get(session.taskId) : current.relatedTasks?.find((task) => task.id === question?.parentTaskId) ??
|
|
1152
|
-
|
|
1153
|
-
address: session.id
|
|
1154
|
-
})).find((task) => task.id === question?.parentTaskId);
|
|
1155
|
-
if (!question?.question || !parent || question.replyTo.channel !== parent.replyTo.channel || question.replyTo.address !== parent.replyTo.address) throw new ApiError(403, "Question does not belong to this interaction");
|
|
1450
|
+
const parent = session.taskId && question?.parentTaskId === session.taskId ? await stores.tasks.get(session.taskId) : current.relatedTasks?.find((task) => task.id === question?.parentTaskId) ?? (question?.parentTaskId ? await stores.tasks.get(question.parentTaskId) : null);
|
|
1451
|
+
if (!question?.question || !parent || parent.id !== session.taskId && (parent.replyTo.channel !== "api" || parent.replyTo.address !== session.id || parent.repositoryIds.length !== 1 || parent.repositoryIds[0] !== session.repositoryId) || question.replyTo.channel !== parent.replyTo.channel || question.replyTo.address !== parent.replyTo.address) throw new ApiError(403, "Question does not belong to this interaction");
|
|
1156
1452
|
await stores.tasks.answerQuestion({
|
|
1157
1453
|
questionTaskId: question.id,
|
|
1158
1454
|
answer: input.answer,
|
|
@@ -1171,9 +1467,28 @@ function createFactoryApi(options) {
|
|
|
1171
1467
|
const current = await view(session);
|
|
1172
1468
|
if (input.executionId !== void 0 && input.executionId !== current.execution?.sessionId) throw new ApiError(409, "Execution changed; refresh the session");
|
|
1173
1469
|
if (action === "cancel" || action === "reset") {
|
|
1174
|
-
const delegated = workflowTasks(session, await stores.tasks.
|
|
1175
|
-
|
|
1176
|
-
|
|
1470
|
+
const delegated = workflowTasks(session, await collectFactoryRecords(stores.tasks.list, {
|
|
1471
|
+
limit: 100,
|
|
1472
|
+
match: [
|
|
1473
|
+
"queued",
|
|
1474
|
+
"running",
|
|
1475
|
+
"verifying",
|
|
1476
|
+
"needs_human",
|
|
1477
|
+
"stalled"
|
|
1478
|
+
].map((state) => ({
|
|
1479
|
+
replyTo: {
|
|
1480
|
+
channel: "api",
|
|
1481
|
+
address: session.id
|
|
1482
|
+
},
|
|
1483
|
+
state
|
|
1484
|
+
})),
|
|
1485
|
+
fields: [{
|
|
1486
|
+
path: ["work", "route"],
|
|
1487
|
+
test: "missing"
|
|
1488
|
+
}, {
|
|
1489
|
+
path: ["question"],
|
|
1490
|
+
test: "missing"
|
|
1491
|
+
}]
|
|
1177
1492
|
}));
|
|
1178
1493
|
for (const task of delegated) if (![
|
|
1179
1494
|
"succeeded",
|
|
@@ -1210,6 +1525,7 @@ function createFactoryApi(options) {
|
|
|
1210
1525
|
error: error.message,
|
|
1211
1526
|
...error instanceof ApprovalAccessError && error.actor ? { actor: error.actor } : {}
|
|
1212
1527
|
}, error.status);
|
|
1528
|
+
if (error instanceof InvalidStoreQueryError) return json({ error: error.message }, 400);
|
|
1213
1529
|
if (error instanceof z.ZodError) return json({
|
|
1214
1530
|
error: "Invalid request",
|
|
1215
1531
|
issues: error.issues
|
|
@@ -1225,9 +1541,9 @@ function createFactoryApi(options) {
|
|
|
1225
1541
|
}
|
|
1226
1542
|
async function startContinuation(task) {
|
|
1227
1543
|
const session = task.replyTo.channel === "api" ? await stores.sessions.get(agentSessionIdSchema.parse(task.replyTo.address)) : null;
|
|
1228
|
-
const continuation = session
|
|
1229
|
-
if (!session || !continuation || continuation.agent !== options.continuationAgent || taskAgent(session, task) !== continuation.agent) throw new ApiError(409, "Conversation continuation is no longer configured");
|
|
1230
|
-
return start(executionSession(session, task), task, await continuationBrief(session, await view(session), continuation.message, task.id), `factory-task:${task.id}:${task.attempt}`);
|
|
1544
|
+
const continuation = session ? await stores.sessions.getContinuation(session.id, task.id) : null;
|
|
1545
|
+
if (!session || !continuation || continuation.agent !== options.continuationAgent || await taskAgent(session, task) !== continuation.agent) throw new ApiError(409, "Conversation continuation is no longer configured");
|
|
1546
|
+
return start(await executionSession(session, task), task, await continuationBrief(session, await view(session), continuation.message, task.id), `factory-task:${task.id}:${task.attempt}`);
|
|
1231
1547
|
}
|
|
1232
1548
|
return {
|
|
1233
1549
|
handle,
|
|
@@ -1271,6 +1587,6 @@ async function body(request, limit = 64e3) {
|
|
|
1271
1587
|
}
|
|
1272
1588
|
}
|
|
1273
1589
|
//#endregion
|
|
1274
|
-
export { ImageInputError, SessionConflictError, agentModelConfigSchema, agentSessionIdSchema, agentSessionSchema, answerSessionQuestionRequestSchema, apiAgentSchema, assembleWorkspaceDiff, controlSessionRequestSchema, createFactoryApi, createSessionRequestSchema, imagePartFilename, inboxActivityItemSchema, inboxActivitySchema, modelSettingsSchema, repositoryDetailsSchema, repositorySummarySchema, resolveImageMessage, sendSessionMessageRequestSchema, sessionInputResponseSchema, sessionPageSchema, sessionPreviewSchema, sessionPreviewsSchema, sessionReviewCheckSchema, sessionReviewIssueSchema, sessionReviewSchema, sessionReviewScreenshotSchema, sessionViewSchema, submitSessionInputRequestSchema, unavailableWorkspaceDiff, updateModelDefaultsSchema, workspaceDiffFileSchema, workspaceDiffSchema, workspaceFileContentSchema, workspaceFileEntrySchema, workspaceFilesRequestSchema, workspaceFilesSchema, workspaceGitFilesScript, workspacePathSchema, workspaceSnapshotSchema, workspaceSnapshotScript };
|
|
1590
|
+
export { ImageInputError, SessionConflictError, agentModelConfigSchema, agentSessionIdSchema, agentSessionSchema, answerSessionQuestionRequestSchema, apiAgentSchema, assembleWorkspaceDiff, controlSessionRequestSchema, createFactoryApi, createSessionRequestSchema, imagePartFilename, inboxActivityItemSchema, inboxActivitySchema, modelSettingsSchema, repositoryDetailsSchema, repositorySummarySchema, resolveImageMessage, sendSessionMessageRequestSchema, sessionInputResponseSchema, sessionPageSchema, sessionPreviewSchema, sessionPreviewsSchema, sessionPullRequestSchema, sessionPullRequestStateSchema, sessionReviewCheckSchema, sessionReviewIssueSchema, sessionReviewSchema, sessionReviewScreenshotSchema, sessionViewSchema, sessionWorkGroupSchema, sessionWorkResultSchema, sessionWorkTaskSchema, submitSessionInputRequestSchema, unavailableWorkspaceDiff, updateModelDefaultsSchema, workspaceDiffFileSchema, workspaceDiffSchema, workspaceFileContentSchema, workspaceFileEntrySchema, workspaceFilesRequestSchema, workspaceFilesSchema, workspaceGitFilesScript, workspacePathSchema, workspaceSnapshotSchema, workspaceSnapshotScript };
|
|
1275
1591
|
|
|
1276
1592
|
//# sourceMappingURL=api.mjs.map
|