@plico-ai/server 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +10 -2
  4. package/dist/api.d.ts +32 -0
  5. package/dist/api.js +87 -0
  6. package/dist/app/assemble.d.ts +10 -0
  7. package/dist/app/assemble.js +49 -0
  8. package/dist/app/errors.d.ts +6 -0
  9. package/dist/app/errors.js +24 -0
  10. package/dist/app/events.d.ts +9 -0
  11. package/dist/app/events.js +52 -0
  12. package/dist/app/metadata.d.ts +4 -0
  13. package/dist/app/metadata.js +24 -0
  14. package/dist/app/runs.d.ts +31 -0
  15. package/dist/app/runs.js +81 -0
  16. package/dist/app/scope.d.ts +8 -0
  17. package/dist/app/scope.js +18 -0
  18. package/dist/app/threads.d.ts +15 -0
  19. package/dist/app/threads.js +133 -0
  20. package/dist/app/types.d.ts +103 -0
  21. package/dist/app/types.js +1 -0
  22. package/dist/body.d.ts +4 -0
  23. package/dist/body.js +54 -0
  24. package/dist/create-plico.d.ts +18 -0
  25. package/dist/create-plico.js +75 -0
  26. package/dist/errors.d.ts +7 -0
  27. package/dist/errors.js +19 -0
  28. package/dist/index.d.ts +6 -0
  29. package/dist/index.js +3 -0
  30. package/dist/listener.d.ts +6 -0
  31. package/dist/listener.js +8 -0
  32. package/dist/mount.d.ts +32 -0
  33. package/dist/mount.js +34 -0
  34. package/dist/origin-guard.d.ts +15 -0
  35. package/dist/origin-guard.js +39 -0
  36. package/dist/project.d.ts +2 -0
  37. package/dist/project.js +10 -0
  38. package/dist/recovery.d.ts +19 -0
  39. package/dist/recovery.js +84 -0
  40. package/dist/responses.d.ts +1 -0
  41. package/dist/responses.js +6 -0
  42. package/dist/routes/artifacts.d.ts +2 -0
  43. package/dist/routes/artifacts.js +165 -0
  44. package/dist/routes/eval.d.ts +2 -0
  45. package/dist/routes/eval.js +38 -0
  46. package/dist/routes/health.d.ts +2 -0
  47. package/dist/routes/health.js +8 -0
  48. package/dist/routes/index.d.ts +11 -0
  49. package/dist/routes/index.js +72 -0
  50. package/dist/routes/metadata-query.d.ts +4 -0
  51. package/dist/routes/metadata-query.js +13 -0
  52. package/dist/routes/project.d.ts +2 -0
  53. package/dist/routes/project.js +23 -0
  54. package/dist/routes/readiness.d.ts +2 -0
  55. package/dist/routes/readiness.js +17 -0
  56. package/dist/routes/runs.d.ts +2 -0
  57. package/dist/routes/runs.js +82 -0
  58. package/dist/routes/threads.d.ts +2 -0
  59. package/dist/routes/threads.js +60 -0
  60. package/dist/routes/types.d.ts +30 -0
  61. package/dist/routes/types.js +1 -0
  62. package/dist/script.d.ts +8 -0
  63. package/dist/script.js +35 -0
  64. package/dist/serve-static.d.ts +2 -0
  65. package/dist/serve-static.js +37 -0
  66. package/dist/server.d.ts +4 -0
  67. package/dist/server.js +25 -0
  68. package/dist/sse.d.ts +3 -0
  69. package/dist/sse.js +12 -0
  70. package/dist/stream.d.ts +13 -0
  71. package/dist/stream.js +58 -0
  72. package/dist/thread-failure.d.ts +7 -0
  73. package/dist/thread-failure.js +42 -0
  74. package/dist/thread-titles.d.ts +11 -0
  75. package/dist/thread-titles.js +37 -0
  76. package/dist/types.d.ts +12 -0
  77. package/dist/types.js +1 -0
  78. package/package.json +47 -3
@@ -0,0 +1,84 @@
1
+ import { resolve } from "node:path";
2
+ import { recoverRuns, runProject, } from "@plico-ai/runtime";
3
+ import { finishFailedThreadRun } from "./thread-failure.js";
4
+ /** Continue runs a dead process left behind, settling any failed thread reservations. */
5
+ export async function recoverPlicoState(options) {
6
+ const dryRuns = recoverRuns({
7
+ engine: options.engine,
8
+ eventStore: options.eventStore,
9
+ recoveryFilter: { projectRoot: options.projectRoot },
10
+ });
11
+ const modelRuns = options.provider && options.providerRecoveryKey
12
+ ? recoverRuns({
13
+ engine: options.engine,
14
+ eventStore: options.eventStore,
15
+ provider: options.provider,
16
+ recoveryFilter: {
17
+ projectRoot: options.projectRoot,
18
+ providerRecoveryKey: options.providerRecoveryKey,
19
+ providerOnly: true,
20
+ },
21
+ })
22
+ : Promise.resolve([]);
23
+ const [dry, model] = await Promise.all([dryRuns, modelRuns]);
24
+ for (const run of [...dry, ...model]) {
25
+ if (run.state !== "failed")
26
+ continue;
27
+ console.error(`Recovered run ${run.runId}: failed${run.error ? ` (${run.error})` : ""}`);
28
+ if (!options.threadStore || options.isClosed?.())
29
+ continue;
30
+ const threadId = await options.threadStore.getThreadForRun(run.runId);
31
+ if (!threadId || options.isClosed?.())
32
+ continue;
33
+ const metadata = (await options.threadStore.getThread(threadId))?.metadata;
34
+ await finishFailedThreadRun(run.runId, threadId, options.projectRoot, options.engine, options.eventStore, run.error, metadata);
35
+ }
36
+ }
37
+ /** Start thread runs that were reserved but never reached the engine. */
38
+ export async function resumeQueuedRuns(options) {
39
+ if (!options.provider || !options.providerAccountKey || !options.threadStore)
40
+ return;
41
+ if (!options.threadStore.storageIdentity ||
42
+ options.threadStore.storageIdentity !== options.eventStore.storageIdentity)
43
+ throw new Error("Thread and run stores must use the same database.");
44
+ const queued = await options.threadStore.listQueuedRuns();
45
+ for (const run of queued) {
46
+ if (options.isClosed?.())
47
+ return;
48
+ if (!run.projectRoot ||
49
+ identity(run.projectRoot) !== identity(options.projectRoot) ||
50
+ run.providerKey !== options.providerAccountKey)
51
+ continue;
52
+ const metadata = (await options.threadStore.getThread(run.threadId))?.metadata;
53
+ const status = await options.engine.status(run.runId);
54
+ if (status) {
55
+ if (status.state === "failed" || status.state === "cancelled" || status.state === "completed")
56
+ await finishFailedThreadRun(run.runId, run.threadId, options.projectRoot, options.engine, options.eventStore, status.state === "failed" ? "Queued run failed before it could be resumed." : undefined, metadata).catch((error) => console.error("Could not settle queued thread run", error));
57
+ continue;
58
+ }
59
+ void runProject(options.projectRoot, {
60
+ runId: run.runId,
61
+ threadId: run.threadId,
62
+ ...(metadata === undefined ? {} : { metadata }),
63
+ ...(run.context === undefined ? {} : { context: run.context }),
64
+ history: run.history,
65
+ message: run.content,
66
+ provider: options.provider,
67
+ providerRecoveryKey: options.providerRecoveryKey ?? options.providerAccountKey,
68
+ engine: options.engine,
69
+ eventStore: options.eventStore,
70
+ }).catch(async (error) => {
71
+ try {
72
+ await finishFailedThreadRun(run.runId, run.threadId, options.projectRoot, options.engine, options.eventStore, error, metadata);
73
+ }
74
+ catch (settleError) {
75
+ console.error("Could not record failed queued thread run", settleError);
76
+ }
77
+ console.error(`Queued run ${run.runId} failed: ${error instanceof Error ? error.message : String(error)}`);
78
+ });
79
+ }
80
+ }
81
+ function identity(path) {
82
+ const absolute = resolve(path);
83
+ return process.platform === "win32" ? absolute.toLowerCase() : absolute;
84
+ }
@@ -0,0 +1 @@
1
+ export declare function jsonResponse(status: number, body: unknown, headers?: Record<string, string>): Response;
@@ -0,0 +1,6 @@
1
+ export function jsonResponse(status, body, headers) {
2
+ return new Response(JSON.stringify(body), {
3
+ status,
4
+ headers: { "content-type": "application/json; charset=utf-8", ...headers },
5
+ });
6
+ }
@@ -0,0 +1,2 @@
1
+ import type { Route } from "./types.js";
2
+ export declare const artifactRoutes: Route[];
@@ -0,0 +1,165 @@
1
+ import { buildArtifactWorkspace, readArtifactSnapshot, readArtifactVersion, } from "@plico-ai/runtime";
2
+ import { createTwoFilesPatch } from "diff";
3
+ import { requireRun } from "../app/runs.js";
4
+ import { HttpError } from "../errors.js";
5
+ import { jsonResponse } from "../responses.js";
6
+ async function readRunEvents(context, runId) {
7
+ const { eventStore, threadStore } = context;
8
+ await requireRun(context.app, runId);
9
+ const threadId = await threadStore?.getThreadForRun(runId);
10
+ if (!threadId)
11
+ return eventStore.getEvents(runId);
12
+ if (eventStore.getThreadRunEventsThrough)
13
+ return eventStore.getThreadRunEventsThrough(threadId, runId);
14
+ if (!eventStore.getThreadRunEvents)
15
+ throw new HttpError(503, "workspace.unavailable", "Thread workspace projection is unavailable.");
16
+ const events = await eventStore.getThreadRunEvents(threadId);
17
+ let lastRunEvent = -1;
18
+ events.forEach((event, index) => {
19
+ if (event.runId === runId)
20
+ lastRunEvent = index;
21
+ });
22
+ return lastRunEvent < 0 ? [] : events.slice(0, lastRunEvent + 1);
23
+ }
24
+ function requireWorkspace(runId, events) {
25
+ const workspace = buildArtifactWorkspace(runId, events);
26
+ if (!workspace) {
27
+ throw new HttpError(404, "workspace.not_found", `No artifact workspace for run: ${runId}`);
28
+ }
29
+ return workspace;
30
+ }
31
+ async function readWorkspace(context, runId) {
32
+ return requireWorkspace(runId, await readRunEvents(context, runId));
33
+ }
34
+ export const artifactRoutes = [
35
+ {
36
+ method: "GET",
37
+ pattern: /^\/v1\/runs\/([^/]+)\/workspace\/diffs$/,
38
+ handler: async (context) => {
39
+ const [runId = ""] = context.params;
40
+ const events = await readRunEvents(context, runId);
41
+ const workspace = requireWorkspace(runId, events);
42
+ const path = context.url.searchParams.get("path");
43
+ if (!path) {
44
+ throw new HttpError(400, "artifact.path_required", "A path is required to diff artifacts.");
45
+ }
46
+ if (!workspace.files.some((file) => file.path === path)) {
47
+ throw new HttpError(404, "artifact.not_found", `Unknown artifact file: ${path}`);
48
+ }
49
+ const fromVersion = readVersionQuery(context.url, "fromVersion");
50
+ const toVersion = readVersionQuery(context.url, "toVersion");
51
+ const from = readArtifactVersion(events, path, fromVersion);
52
+ const to = readArtifactVersion(events, path, toVersion);
53
+ if (!from || !to) {
54
+ throw new HttpError(404, "artifact.version_not_found", `Unknown artifact version for ${path}: ${!from ? fromVersion : toVersion}`);
55
+ }
56
+ if (typeof from.content !== "string" ||
57
+ typeof to.content !== "string" ||
58
+ !isDiffableMimeType(from.mimeType) ||
59
+ !isDiffableMimeType(to.mimeType)) {
60
+ throw new HttpError(415, "artifact.diff_unsupported_media_type", "Artifact diffs support text, JSON, and XML content.");
61
+ }
62
+ if (Buffer.byteLength(from.content, "utf8") > maxDiffContentBytes ||
63
+ Buffer.byteLength(to.content, "utf8") > maxDiffContentBytes) {
64
+ throw new HttpError(413, "artifact.diff_too_large", `Artifact versions must be at most ${maxDiffContentBytes} bytes to diff.`);
65
+ }
66
+ return jsonResponse(200, {
67
+ path,
68
+ fromVersion,
69
+ toVersion,
70
+ diff: createTwoFilesPatch(path, path, from.content, to.content, `version ${fromVersion}`, `version ${toVersion}`),
71
+ });
72
+ },
73
+ },
74
+ {
75
+ method: "GET",
76
+ pattern: /^\/v1\/runs\/([^/]+)\/workspace$/,
77
+ handler: async (context) => {
78
+ const runId = context.params[0] ?? "";
79
+ const workspace = await readWorkspace(context, runId);
80
+ const threadId = await context.threadStore?.getThreadForRun(runId);
81
+ return jsonResponse(200, threadId ? { ...workspace, threadId } : workspace);
82
+ },
83
+ },
84
+ {
85
+ method: "GET",
86
+ pattern: /^\/v1\/runs\/([^/]+)\/workspace\/files\/(.+)$/,
87
+ handler: async (context) => {
88
+ const [runId = "", path = ""] = context.params;
89
+ const workspace = await readWorkspace(context, runId);
90
+ const file = workspace.files.find((candidate) => candidate.path === path);
91
+ if (!file) {
92
+ throw new HttpError(404, "artifact.not_found", `Unknown artifact file: ${path}`);
93
+ }
94
+ return jsonResponse(200, file);
95
+ },
96
+ },
97
+ {
98
+ method: "GET",
99
+ pattern: /^\/v1\/runs\/([^/]+)\/workspace\/exports$/,
100
+ handler: async (context) => {
101
+ const [runId = ""] = context.params;
102
+ const workspace = await readWorkspace(context, runId);
103
+ const path = context.url.searchParams.get("path");
104
+ if (!path) {
105
+ throw new HttpError(400, "artifact.path_required", "A path is required to export an artifact.");
106
+ }
107
+ const file = workspace.files.find((candidate) => candidate.path === path);
108
+ if (!file) {
109
+ throw new HttpError(404, "artifact.not_found", `Unknown artifact file: ${path}`);
110
+ }
111
+ const filename = path.split("/").at(-1) || "artifact";
112
+ const encodedFilename = encodeURIComponent(filename).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
113
+ return new Response(typeof file.content === "string" ? file.content : Buffer.from(file.content.data, "base64"), {
114
+ headers: {
115
+ "x-content-type-options": "nosniff",
116
+ "content-type": exportMediaType(file.mimeType),
117
+ "content-disposition": `attachment; filename*=UTF-8''${encodedFilename}`,
118
+ },
119
+ });
120
+ },
121
+ },
122
+ {
123
+ method: "GET",
124
+ pattern: /^\/v1\/runs\/([^/]+)\/workspace\/snapshots\/([^/]+)$/,
125
+ handler: async (context) => {
126
+ const [runId = "", snapshotId = ""] = context.params;
127
+ const events = await readRunEvents(context, runId);
128
+ requireWorkspace(runId, events);
129
+ const snapshot = readArtifactSnapshot(events, snapshotId);
130
+ if (!snapshot) {
131
+ throw new HttpError(404, "artifact.not_found", `Unknown artifact snapshot: ${snapshotId}`);
132
+ }
133
+ return jsonResponse(200, snapshot);
134
+ },
135
+ },
136
+ ];
137
+ function exportMediaType(mimeType) {
138
+ const mediaType = mimeType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
139
+ if (mediaType === "text/html" ||
140
+ mediaType === "application/xhtml+xml" ||
141
+ mediaType === "image/svg+xml" ||
142
+ /\/(?:x-)?(?:java|ecma)script$/.test(mediaType) ||
143
+ /\+(?:java|ecma)script$/.test(mediaType)) {
144
+ return "application/octet-stream";
145
+ }
146
+ return /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/.test(mediaType)
147
+ ? mediaType
148
+ : "application/octet-stream";
149
+ }
150
+ function readVersionQuery(url, name) {
151
+ const value = url.searchParams.get(name);
152
+ if (!value || !/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) {
153
+ throw new HttpError(400, "artifact.invalid_version", `${name} must be a positive integer.`);
154
+ }
155
+ return Number(value);
156
+ }
157
+ const maxDiffContentBytes = 1024 * 1024;
158
+ function isDiffableMimeType(mimeType) {
159
+ const mediaType = mimeType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
160
+ return (mediaType.startsWith("text/") ||
161
+ mediaType === "application/json" ||
162
+ mediaType.endsWith("+json") ||
163
+ mediaType === "application/xml" ||
164
+ mediaType.endsWith("+xml"));
165
+ }
@@ -0,0 +1,2 @@
1
+ import type { Route } from "./types.js";
2
+ export declare const evalRoutes: Route[];
@@ -0,0 +1,38 @@
1
+ import { runEvals } from "@plico-ai/runtime";
2
+ import { HttpError } from "../errors.js";
3
+ import { jsonResponse } from "../responses.js";
4
+ async function readLatestReport(eventStore) {
5
+ const report = await eventStore.getLatestEvalReport();
6
+ if (!report) {
7
+ throw new HttpError(404, "eval.not_found", "No eval results yet. Run evals first.");
8
+ }
9
+ return report;
10
+ }
11
+ export const evalRoutes = [
12
+ {
13
+ method: "POST",
14
+ hostOnly: true,
15
+ pattern: /^\/v1\/eval$/,
16
+ handler: async ({ projectRoot, eventStore }) => jsonResponse(200, await runEvals(projectRoot, { eventStore })),
17
+ },
18
+ {
19
+ method: "GET",
20
+ hostOnly: true,
21
+ pattern: /^\/v1\/eval$/,
22
+ handler: async ({ eventStore }) => jsonResponse(200, await readLatestReport(eventStore)),
23
+ },
24
+ {
25
+ method: "GET",
26
+ hostOnly: true,
27
+ pattern: /^\/v1\/eval\/([^/]+)$/,
28
+ handler: async ({ eventStore, params }) => {
29
+ const name = params[0] ?? "";
30
+ const report = await readLatestReport(eventStore);
31
+ const result = report.results.find((candidate) => candidate.name === name);
32
+ if (!result) {
33
+ throw new HttpError(404, "eval.not_found", `Unknown eval: ${name}`);
34
+ }
35
+ return jsonResponse(200, result);
36
+ },
37
+ },
38
+ ];
@@ -0,0 +1,2 @@
1
+ import type { Route } from "./types.js";
2
+ export declare const healthRoutes: Route[];
@@ -0,0 +1,8 @@
1
+ import { jsonResponse } from "../responses.js";
2
+ export const healthRoutes = [
3
+ {
4
+ method: "GET",
5
+ pattern: /^\/health$/,
6
+ handler: async ({ app }) => jsonResponse(200, app.inspect ? { ok: true, inspection: true } : { ok: true }),
7
+ },
8
+ ];
@@ -0,0 +1,11 @@
1
+ import type { AppDeps } from "../app/types.js";
2
+ interface HandleRequestOptions {
3
+ app: AppDeps;
4
+ /** Aborts when the host closes Plico, so open event streams end. */
5
+ stop: AbortSignal;
6
+ studioPath: string | undefined;
7
+ allowScript: boolean;
8
+ request: Request;
9
+ }
10
+ export declare function handleRequest(options: HandleRequestOptions): Promise<Response>;
11
+ export {};
@@ -0,0 +1,72 @@
1
+ import { PlicoError, statusForCode } from "../app/errors.js";
2
+ import { errorMessage, errorResponse, HttpError } from "../errors.js";
3
+ import { artifactRoutes } from "./artifacts.js";
4
+ import { evalRoutes } from "./eval.js";
5
+ import { healthRoutes } from "./health.js";
6
+ import { projectRoutes } from "./project.js";
7
+ import { readinessRoutes } from "./readiness.js";
8
+ import { runRoutes } from "./runs.js";
9
+ import { threadRoutes } from "./threads.js";
10
+ const routes = [
11
+ ...healthRoutes,
12
+ ...projectRoutes,
13
+ ...runRoutes,
14
+ ...threadRoutes,
15
+ ...artifactRoutes,
16
+ ...evalRoutes,
17
+ ...readinessRoutes,
18
+ ];
19
+ export async function handleRequest(options) {
20
+ const { app, stop, studioPath, allowScript, request } = options;
21
+ const url = new URL(request.url);
22
+ try {
23
+ if (app.inspect && request.method !== "GET" && request.method !== "HEAD")
24
+ return errorResponse(403, "inspection.read_only", "This Plico server is inspection-only and does not change runs or threads.");
25
+ const pathMatches = routes.filter((route) => route.pattern.test(url.pathname) && !(app.scope && route.hostOnly));
26
+ const route = pathMatches.find((candidate) => candidate.method === request.method);
27
+ if (route) {
28
+ const match = route.pattern.exec(url.pathname);
29
+ const params = (match?.slice(1) ?? []).map(decodeParam);
30
+ return await route.handler({
31
+ app,
32
+ stop,
33
+ projectRoot: app.projectRoot,
34
+ eventStore: app.eventStore,
35
+ threadStore: app.threadStore,
36
+ provider: app.provider,
37
+ providerRecoveryKey: app.providerRecoveryKey,
38
+ providerAccountKey: app.providerAccountKey,
39
+ engine: app.engine,
40
+ studioPath,
41
+ allowScript,
42
+ request,
43
+ url,
44
+ params,
45
+ });
46
+ }
47
+ if (pathMatches.length > 0) {
48
+ return errorResponse(405, "method.not_allowed", "Method not allowed.", {
49
+ allow: [...new Set(pathMatches.map((r) => r.method))].join(", "),
50
+ });
51
+ }
52
+ return errorResponse(404, "route.not_found", "Unknown route.");
53
+ }
54
+ catch (error) {
55
+ if (error instanceof HttpError) {
56
+ return errorResponse(error.status, error.code, error.message);
57
+ }
58
+ if (error instanceof PlicoError) {
59
+ return errorResponse(statusForCode(error.code), error.code, error.message);
60
+ }
61
+ console.error(`plico server error: ${errorMessage(error)}`);
62
+ return errorResponse(500, "server.error", "Internal server error.");
63
+ }
64
+ }
65
+ function decodeParam(value) {
66
+ try {
67
+ return decodeURIComponent(value);
68
+ }
69
+ catch {
70
+ throw new HttpError(400, "request.invalid_path", "Malformed percent-encoding in path.");
71
+ }
72
+ }
@@ -0,0 +1,4 @@
1
+ /** Reads the `metadata` query parameter, a URL-encoded JSON object, as a list filter. */
2
+ export declare function readMetadataFilter(url: URL): {
3
+ metadata?: unknown;
4
+ };
@@ -0,0 +1,13 @@
1
+ import { HttpError } from "../errors.js";
2
+ /** Reads the `metadata` query parameter, a URL-encoded JSON object, as a list filter. */
3
+ export function readMetadataFilter(url) {
4
+ const raw = url.searchParams.get("metadata");
5
+ if (raw === null)
6
+ return {};
7
+ try {
8
+ return { metadata: JSON.parse(raw) };
9
+ }
10
+ catch {
11
+ throw new HttpError(400, "request.invalid", "metadata must be a URL-encoded JSON object.");
12
+ }
13
+ }
@@ -0,0 +1,2 @@
1
+ import type { Route } from "./types.js";
2
+ export declare const projectRoutes: Route[];
@@ -0,0 +1,23 @@
1
+ import { composeInstructions } from "@plico-ai/runtime";
2
+ import { loadProjectOrThrow } from "../project.js";
3
+ import { jsonResponse } from "../responses.js";
4
+ export const projectRoutes = [
5
+ {
6
+ method: "GET",
7
+ hostOnly: true,
8
+ pattern: /^\/v1\/project$/,
9
+ handler: async ({ projectRoot }) => {
10
+ const project = await loadProjectOrThrow(projectRoot);
11
+ return jsonResponse(200, { project });
12
+ },
13
+ },
14
+ {
15
+ method: "GET",
16
+ hostOnly: true,
17
+ pattern: /^\/v1\/instructions$/,
18
+ handler: async ({ projectRoot }) => {
19
+ const project = await loadProjectOrThrow(projectRoot);
20
+ return jsonResponse(200, await composeInstructions(project));
21
+ },
22
+ },
23
+ ];
@@ -0,0 +1,2 @@
1
+ import type { Route } from "./types.js";
2
+ export declare const readinessRoutes: Route[];
@@ -0,0 +1,17 @@
1
+ import { checkReadiness } from "@plico-ai/runtime";
2
+ import { jsonResponse } from "../responses.js";
3
+ export const readinessRoutes = [
4
+ {
5
+ method: "GET",
6
+ hostOnly: true,
7
+ pattern: /^\/v1\/readiness$/,
8
+ handler: async ({ projectRoot, eventStore, threadStore, studioPath }) => {
9
+ const report = await checkReadiness(projectRoot, {
10
+ eventStore,
11
+ ...(threadStore ? { threadStore } : {}),
12
+ studioPath,
13
+ });
14
+ return jsonResponse(200, report);
15
+ },
16
+ },
17
+ ];
@@ -0,0 +1,2 @@
1
+ import type { Route } from "./types.js";
2
+ export declare const runRoutes: Route[];
@@ -0,0 +1,82 @@
1
+ import { cancelRun, listRunEvents, listRuns, requireRun, runDry, signalRun, startRun, } from "../app/runs.js";
2
+ import { readJsonBody } from "../body.js";
3
+ import { jsonResponse } from "../responses.js";
4
+ import { readScript } from "../script.js";
5
+ import { eventStreamResponse, parseAfterCursor } from "../stream.js";
6
+ import { readMetadataFilter } from "./metadata-query.js";
7
+ function readAfterCursor(url) {
8
+ const after = url.searchParams.get("after");
9
+ return after === null ? undefined : parseAfterCursor(after);
10
+ }
11
+ export const runRoutes = [
12
+ {
13
+ method: "POST",
14
+ pattern: /^\/v1\/runs$/,
15
+ handler: async ({ app, allowScript, request }) => {
16
+ const body = await readJsonBody(request);
17
+ const script = readScript(body, allowScript);
18
+ return jsonResponse(202, await startRun(app, {
19
+ // A script sent over HTTP comes from the caller, so its tool steps never run handlers.
20
+ ...(script === undefined ? {} : { script, mockTools: true }),
21
+ metadata: body.metadata,
22
+ }));
23
+ },
24
+ },
25
+ {
26
+ method: "POST",
27
+ hostOnly: true,
28
+ pattern: /^\/v1\/runs\/dry$/,
29
+ handler: async ({ app, allowScript, request }) => {
30
+ const script = readScript(await readJsonBody(request), allowScript);
31
+ return jsonResponse(200, await runDry(app, script === undefined ? {} : { script, mockTools: true }));
32
+ },
33
+ },
34
+ {
35
+ method: "GET",
36
+ pattern: /^\/v1\/runs$/,
37
+ handler: async ({ app, url }) => jsonResponse(200, await listRuns(app, readMetadataFilter(url))),
38
+ },
39
+ {
40
+ method: "GET",
41
+ pattern: /^\/v1\/runs\/([^/]+)$/,
42
+ handler: async ({ app, params }) => jsonResponse(200, await requireRun(app, params[0] ?? "")),
43
+ },
44
+ {
45
+ method: "GET",
46
+ pattern: /^\/v1\/runs\/([^/]+)\/events\/stream$/,
47
+ handler: async (context) => {
48
+ const lastEventId = context.request.headers.get("last-event-id");
49
+ const afterSequence = lastEventId === null || lastEventId === ""
50
+ ? readAfterCursor(context.url)
51
+ : parseAfterCursor(lastEventId);
52
+ return eventStreamResponse({
53
+ app: context.app,
54
+ stop: context.stop,
55
+ request: context.request,
56
+ runId: context.params[0] ?? "",
57
+ afterSequence,
58
+ });
59
+ },
60
+ },
61
+ {
62
+ method: "GET",
63
+ pattern: /^\/v1\/runs\/([^/]+)\/events$/,
64
+ handler: async ({ app, params, url }) => jsonResponse(200, await listRunEvents(app, params[0] ?? "", readAfterCursor(url))),
65
+ },
66
+ {
67
+ method: "POST",
68
+ pattern: /^\/v1\/runs\/([^/]+)\/cancel$/,
69
+ handler: async ({ app, params }) => jsonResponse(200, { result: await cancelRun(app, params[0] ?? "") }),
70
+ },
71
+ {
72
+ method: "POST",
73
+ pattern: /^\/v1\/runs\/([^/]+)\/signal$/,
74
+ handler: async ({ app, params, request }) => {
75
+ const runId = params[0] ?? "";
76
+ await requireRun(app, runId);
77
+ const body = await readJsonBody(request);
78
+ const name = typeof body.name === "string" ? body.name : "";
79
+ return jsonResponse(200, { result: await signalRun(app, runId, name, body.value) });
80
+ },
81
+ },
82
+ ];
@@ -0,0 +1,2 @@
1
+ import type { Route } from "./types.js";
2
+ export declare const threadRoutes: Route[];
@@ -0,0 +1,60 @@
1
+ import { createThread, listThreads, renameThread, requireThread, sendMessage, threadMessages, threadWorkspace, } from "../app/threads.js";
2
+ import { readJsonBody, readOptionalJsonBody } from "../body.js";
3
+ import { HttpError } from "../errors.js";
4
+ import { jsonResponse } from "../responses.js";
5
+ import { readMetadataFilter } from "./metadata-query.js";
6
+ export const threadRoutes = [
7
+ {
8
+ method: "POST",
9
+ pattern: /^\/v1\/threads$/,
10
+ handler: async (ctx) => {
11
+ const { metadata } = await readOptionalJsonBody(ctx.request);
12
+ return jsonResponse(201, await createThread(ctx.app, { metadata }));
13
+ },
14
+ },
15
+ {
16
+ method: "GET",
17
+ pattern: /^\/v1\/threads$/,
18
+ handler: async (ctx) => jsonResponse(200, await listThreads(ctx.app, readMetadataFilter(ctx.url))),
19
+ },
20
+ {
21
+ method: "GET",
22
+ pattern: /^\/v1\/threads\/([^/]+)$/,
23
+ handler: async (ctx) => jsonResponse(200, await requireThread(ctx.app, ctx.params[0] ?? "")),
24
+ },
25
+ {
26
+ method: "PATCH",
27
+ pattern: /^\/v1\/threads\/([^/]+)$/,
28
+ handler: async (ctx) => {
29
+ const threadId = ctx.params[0] ?? "";
30
+ await requireThread(ctx.app, threadId);
31
+ const { title } = await readJsonBody(ctx.request);
32
+ return jsonResponse(200, await renameThread(ctx.app, threadId, title));
33
+ },
34
+ },
35
+ {
36
+ method: "GET",
37
+ pattern: /^\/v1\/threads\/([^/]+)\/workspace$/,
38
+ handler: async (ctx) => jsonResponse(200, await threadWorkspace(ctx.app, ctx.params[0] ?? "")),
39
+ },
40
+ {
41
+ method: "GET",
42
+ pattern: /^\/v1\/threads\/([^/]+)\/messages$/,
43
+ handler: async (ctx) => jsonResponse(200, await threadMessages(ctx.app, ctx.params[0] ?? "")),
44
+ },
45
+ {
46
+ method: "POST",
47
+ pattern: /^\/v1\/threads\/([^/]+)\/messages$/,
48
+ handler: async (ctx) => {
49
+ const threadId = ctx.params[0] ?? "";
50
+ await requireThread(ctx.app, threadId);
51
+ const body = await readJsonBody(ctx.request);
52
+ if (body.startRun !== undefined && typeof body.startRun !== "boolean")
53
+ throw new HttpError(400, "message.invalid", "startRun must be a boolean.");
54
+ const message = await sendMessage(ctx.app, threadId, body.content, {
55
+ startRun: body.startRun !== false,
56
+ });
57
+ return jsonResponse(201, message);
58
+ },
59
+ },
60
+ ];
@@ -0,0 +1,30 @@
1
+ import type { Engine, EventStore, ModelProvider, ThreadStore } from "@plico-ai/runtime";
2
+ import type { AppDeps } from "../app/types.js";
3
+ export interface RouteContext {
4
+ /** The shared layer the thread and run routes call. */
5
+ app: AppDeps;
6
+ /** Aborts when the host closes Plico, so open event streams end. */
7
+ stop: AbortSignal;
8
+ projectRoot: string;
9
+ eventStore: EventStore;
10
+ threadStore: ThreadStore | undefined;
11
+ provider: ModelProvider | undefined;
12
+ providerRecoveryKey: string | undefined;
13
+ /** Account-only identity used to resume queued runs after a model change. */
14
+ providerAccountKey: string | undefined;
15
+ engine: Engine | undefined;
16
+ studioPath: string | undefined;
17
+ /** Whether a request may carry a `script` that drives project tools without a model. */
18
+ allowScript: boolean;
19
+ request: Request;
20
+ url: URL;
21
+ /** Decoded capture groups from the route pattern, in order. */
22
+ params: string[];
23
+ }
24
+ export interface Route {
25
+ method: "GET" | "POST" | "PATCH";
26
+ pattern: RegExp;
27
+ /** Project-level routes. A scoped mount does not serve them. */
28
+ hostOnly?: true;
29
+ handler: (context: RouteContext) => Promise<Response>;
30
+ }
@@ -0,0 +1 @@
1
+ export {};