@plico-ai/server 0.0.0-reserved.0 → 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 (77) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +8 -3
  3. package/dist/api.d.ts +32 -0
  4. package/dist/api.js +87 -0
  5. package/dist/app/assemble.d.ts +10 -0
  6. package/dist/app/assemble.js +49 -0
  7. package/dist/app/errors.d.ts +6 -0
  8. package/dist/app/errors.js +24 -0
  9. package/dist/app/events.d.ts +9 -0
  10. package/dist/app/events.js +52 -0
  11. package/dist/app/metadata.d.ts +4 -0
  12. package/dist/app/metadata.js +24 -0
  13. package/dist/app/runs.d.ts +31 -0
  14. package/dist/app/runs.js +81 -0
  15. package/dist/app/scope.d.ts +8 -0
  16. package/dist/app/scope.js +18 -0
  17. package/dist/app/threads.d.ts +15 -0
  18. package/dist/app/threads.js +133 -0
  19. package/dist/app/types.d.ts +103 -0
  20. package/dist/app/types.js +1 -0
  21. package/dist/body.d.ts +4 -0
  22. package/dist/body.js +54 -0
  23. package/dist/create-plico.d.ts +18 -0
  24. package/dist/create-plico.js +75 -0
  25. package/dist/errors.d.ts +7 -0
  26. package/dist/errors.js +19 -0
  27. package/dist/index.d.ts +6 -0
  28. package/dist/index.js +3 -0
  29. package/dist/listener.d.ts +6 -0
  30. package/dist/listener.js +8 -0
  31. package/dist/mount.d.ts +32 -0
  32. package/dist/mount.js +34 -0
  33. package/dist/origin-guard.d.ts +15 -0
  34. package/dist/origin-guard.js +39 -0
  35. package/dist/project.d.ts +2 -0
  36. package/dist/project.js +10 -0
  37. package/dist/recovery.d.ts +19 -0
  38. package/dist/recovery.js +84 -0
  39. package/dist/responses.d.ts +1 -0
  40. package/dist/responses.js +6 -0
  41. package/dist/routes/artifacts.d.ts +2 -0
  42. package/dist/routes/artifacts.js +165 -0
  43. package/dist/routes/eval.d.ts +2 -0
  44. package/dist/routes/eval.js +38 -0
  45. package/dist/routes/health.d.ts +2 -0
  46. package/dist/routes/health.js +8 -0
  47. package/dist/routes/index.d.ts +11 -0
  48. package/dist/routes/index.js +72 -0
  49. package/dist/routes/metadata-query.d.ts +4 -0
  50. package/dist/routes/metadata-query.js +13 -0
  51. package/dist/routes/project.d.ts +2 -0
  52. package/dist/routes/project.js +23 -0
  53. package/dist/routes/readiness.d.ts +2 -0
  54. package/dist/routes/readiness.js +17 -0
  55. package/dist/routes/runs.d.ts +2 -0
  56. package/dist/routes/runs.js +82 -0
  57. package/dist/routes/threads.d.ts +2 -0
  58. package/dist/routes/threads.js +60 -0
  59. package/dist/routes/types.d.ts +30 -0
  60. package/dist/routes/types.js +1 -0
  61. package/dist/script.d.ts +8 -0
  62. package/dist/script.js +35 -0
  63. package/dist/serve-static.d.ts +2 -0
  64. package/dist/serve-static.js +37 -0
  65. package/dist/server.d.ts +4 -0
  66. package/dist/server.js +25 -0
  67. package/dist/sse.d.ts +3 -0
  68. package/dist/sse.js +12 -0
  69. package/dist/stream.d.ts +13 -0
  70. package/dist/stream.js +58 -0
  71. package/dist/thread-failure.d.ts +7 -0
  72. package/dist/thread-failure.js +42 -0
  73. package/dist/thread-titles.d.ts +11 -0
  74. package/dist/thread-titles.js +37 -0
  75. package/dist/types.d.ts +12 -0
  76. package/dist/types.js +1 -0
  77. package/package.json +37 -8
package/dist/server.js ADDED
@@ -0,0 +1,25 @@
1
+ import { createServer } from "node:http";
2
+ import { openPlico } from "./create-plico.js";
3
+ import { createRequestListener } from "./listener.js";
4
+ /** `createPlico` plus an HTTP listener over its `/v1` API. Closing the server closes Plico. */
5
+ export async function servePlico(options) {
6
+ const plico = await openPlico({ projectRoot: options.projectRoot, databasePath: options.databasePath }, options.inspect === true);
7
+ const server = createServer(createRequestListener(plico, {
8
+ ...(options.studioPath === undefined ? {} : { studioPath: options.studioPath }),
9
+ ...(options.allowRemote === undefined ? {} : { allowRemote: options.allowRemote }),
10
+ ...(options.publicOrigin === undefined ? {} : { publicOrigin: options.publicOrigin }),
11
+ ...(options.allowScript === undefined ? {} : { allowScript: options.allowScript }),
12
+ }));
13
+ server.once("close", () => void plico.close());
14
+ try {
15
+ await new Promise((resolve, reject) => {
16
+ server.once("error", reject);
17
+ server.listen(options.port ?? 3000, options.host ?? "127.0.0.1", () => resolve());
18
+ });
19
+ }
20
+ catch (error) {
21
+ await plico.close();
22
+ throw error;
23
+ }
24
+ return server;
25
+ }
package/dist/sse.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare const sseHeaders: Record<string, string>;
2
+ export declare function sseEvent(event: string, data: unknown, id?: string): Uint8Array;
3
+ export declare function sseHeartbeat(): Uint8Array;
package/dist/sse.js ADDED
@@ -0,0 +1,12 @@
1
+ const encoder = new TextEncoder();
2
+ export const sseHeaders = {
3
+ "content-type": "text/event-stream",
4
+ "cache-control": "no-cache",
5
+ };
6
+ export function sseEvent(event, data, id) {
7
+ const idLine = id === undefined ? "" : `id: ${id}\n`;
8
+ return encoder.encode(`${idLine}event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
9
+ }
10
+ export function sseHeartbeat() {
11
+ return encoder.encode(": heartbeat\n\n");
12
+ }
@@ -0,0 +1,13 @@
1
+ import type { AppDeps } from "./app/types.js";
2
+ export interface EventStreamOptions {
3
+ app: AppDeps;
4
+ /** Aborts when the host closes Plico. */
5
+ stop: AbortSignal;
6
+ request: Request;
7
+ runId: string;
8
+ afterSequence: number | undefined;
9
+ heartbeatInterval?: number;
10
+ }
11
+ /** Answers with a run's events as server-sent events until the run ends or the client leaves. */
12
+ export declare function eventStreamResponse(options: EventStreamOptions): Promise<Response>;
13
+ export declare function parseAfterCursor(value: string): number;
package/dist/stream.js ADDED
@@ -0,0 +1,58 @@
1
+ import { followEvents } from "./app/events.js";
2
+ import { requireRun } from "./app/runs.js";
3
+ import { HttpError } from "./errors.js";
4
+ import { sseEvent, sseHeaders, sseHeartbeat } from "./sse.js";
5
+ /** Answers with a run's events as server-sent events until the run ends or the client leaves. */
6
+ export async function eventStreamResponse(options) {
7
+ const { app, stop, request, runId, heartbeatInterval = 2000 } = options;
8
+ await requireRun(app, runId);
9
+ const cancelled = new AbortController();
10
+ const disconnected = AbortSignal.any([request.signal, cancelled.signal]);
11
+ const body = new ReadableStream({
12
+ start(controller) {
13
+ const write = (chunk) => {
14
+ try {
15
+ controller.enqueue(chunk);
16
+ }
17
+ catch {
18
+ // The client already left.
19
+ }
20
+ };
21
+ const heartbeat = setInterval(() => write(sseHeartbeat()), heartbeatInterval);
22
+ void (async () => {
23
+ try {
24
+ const events = followEvents(app, runId, {
25
+ ...(options.afterSequence === undefined ? {} : { after: options.afterSequence }),
26
+ signal: disconnected,
27
+ stop,
28
+ pollMs: heartbeatInterval,
29
+ });
30
+ for await (const event of events) {
31
+ write(sseEvent("plico.event", event, String(event.sequence)));
32
+ }
33
+ write(sseHeartbeat());
34
+ }
35
+ finally {
36
+ clearInterval(heartbeat);
37
+ try {
38
+ controller.close();
39
+ }
40
+ catch {
41
+ // Already closed by the client.
42
+ }
43
+ }
44
+ })();
45
+ },
46
+ cancel() {
47
+ cancelled.abort();
48
+ },
49
+ });
50
+ return new Response(body, { headers: sseHeaders });
51
+ }
52
+ export function parseAfterCursor(value) {
53
+ const parsed = Number(value);
54
+ if (!Number.isInteger(parsed) || parsed < 0) {
55
+ throw new HttpError(400, "cursor.invalid", `Malformed cursor: ${value}`);
56
+ }
57
+ return parsed;
58
+ }
@@ -0,0 +1,7 @@
1
+ import { type Engine, type EventStore } from "@plico-ai/runtime";
2
+ import type { Metadata } from "./app/metadata.js";
3
+ /**
4
+ * Persist a terminal outcome after the engine confirms no worker is still running. A run whose
5
+ * journal write never happened is also safe to fail: nothing can be executing it.
6
+ */
7
+ export declare function finishFailedThreadRun(runId: string, threadId: string, projectRoot: string, engine: Engine, eventStore: EventStore, error?: unknown, metadata?: Metadata): Promise<void>;
@@ -0,0 +1,42 @@
1
+ import { runFailureEvents } from "@plico-ai/runtime";
2
+ /**
3
+ * Persist a terminal outcome after the engine confirms no worker is still running. A run whose
4
+ * journal write never happened is also safe to fail: nothing can be executing it.
5
+ */
6
+ export async function finishFailedThreadRun(runId, threadId, projectRoot, engine, eventStore, error, metadata) {
7
+ const state = await engine.status(runId);
8
+ if (state && state.state !== "failed")
9
+ return;
10
+ let run = await eventStore.getRun(runId);
11
+ if (!run) {
12
+ await eventStore.appendEvent({
13
+ schemaVersion: 1,
14
+ streamType: "run",
15
+ streamId: runId,
16
+ runId,
17
+ id: "evt-0001",
18
+ sequence: 1,
19
+ timestamp: new Date().toISOString(),
20
+ type: "run.started",
21
+ payload: {
22
+ projectRoot,
23
+ projectName: "",
24
+ threadId,
25
+ ...(metadata === undefined ? {} : { metadata }),
26
+ },
27
+ });
28
+ run = await eventStore.getRun(runId);
29
+ }
30
+ if (run?.status !== "running")
31
+ return;
32
+ const events = await eventStore.getEvents(runId);
33
+ for (const event of runFailureEvents(runId, events.at(-1), failureMessage(error)))
34
+ await eventStore.appendEvent(event);
35
+ }
36
+ function failureMessage(error) {
37
+ if (typeof error === "string" && error)
38
+ return error;
39
+ if (error instanceof Error && error.message)
40
+ return error.message;
41
+ return "Run execution failed before it could finish.";
42
+ }
@@ -0,0 +1,11 @@
1
+ import { type EventStore, type ModelProvider, type ThreadStore } from "@plico-ai/runtime";
2
+ /**
3
+ * After a run in a thread completes, asks the project's titler for a title and stores it. The
4
+ * keyword title is already stored, so a titler that fails or never answers changes nothing.
5
+ */
6
+ export declare function withThreadTitles(deps: {
7
+ projectRoot: string;
8
+ eventStore: EventStore;
9
+ threadStore?: ThreadStore | undefined;
10
+ provider?: ModelProvider | undefined;
11
+ }): EventStore;
@@ -0,0 +1,37 @@
1
+ import { loadProject } from "@plico-ai/core";
2
+ import { createThreadTitler, } from "@plico-ai/runtime";
3
+ /**
4
+ * After a run in a thread completes, asks the project's titler for a title and stores it. The
5
+ * keyword title is already stored, so a titler that fails or never answers changes nothing.
6
+ */
7
+ export function withThreadTitles(deps) {
8
+ const { threadStore } = deps;
9
+ if (!threadStore)
10
+ return deps.eventStore;
11
+ let titler;
12
+ const resolve = () => {
13
+ titler ??= loadProject(deps.projectRoot).then((project) => createThreadTitler(deps.projectRoot, project.config, deps.provider));
14
+ return titler;
15
+ };
16
+ async function name(runId) {
17
+ const threadId = await threadStore?.getThreadForRun(runId);
18
+ if (!threadStore || !threadId)
19
+ return;
20
+ // Ask the store first so a user-titled or finished thread never pays for a title.
21
+ const request = await threadStore.getTitleRequest(threadId);
22
+ const write = request && (await resolve());
23
+ if (!request || !write)
24
+ return;
25
+ const title = await write(request.messages);
26
+ if (title)
27
+ await threadStore.setGeneratedTitle(threadId, title, request.revision);
28
+ }
29
+ return {
30
+ ...deps.eventStore,
31
+ async appendEvent(event) {
32
+ await deps.eventStore.appendEvent(event);
33
+ if (event.type === "run.completed" && event.payload.output !== undefined)
34
+ void name(event.runId).catch(() => { });
35
+ },
36
+ };
37
+ }
@@ -0,0 +1,12 @@
1
+ import type { ListenerOptions } from "./listener.js";
2
+ export interface ServePlicoOptions extends ListenerOptions {
3
+ projectRoot: string;
4
+ /**
5
+ * Serve for inspection beside a host that owns the database: nothing is recovered or resumed,
6
+ * and every request that would change state is refused with `inspection.read_only`.
7
+ */
8
+ inspect?: boolean;
9
+ databasePath: string;
10
+ host?: string;
11
+ port?: number;
12
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,21 +1,50 @@
1
1
  {
2
2
  "name": "@plico-ai/server",
3
- "version": "0.0.0-reserved.0",
4
- "description": "The Plico HTTP API and embeddable library. Not released yet.",
3
+ "version": "0.1.0",
4
+ "description": "Plico native HTTP API over the runtime and event store",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://plico.sohab.dev",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/ITZSHOAIB/plico.git"
9
+ "url": "git+https://github.com/ITZSHOAIB/plico.git",
10
+ "directory": "packages/server"
10
11
  },
11
12
  "keywords": [
12
13
  "plico",
13
- "agent"
14
- ],
15
- "files": [
16
- "README.md"
14
+ "agent",
15
+ "http",
16
+ "api"
17
17
  ],
18
18
  "publishConfig": {
19
19
  "access": "public"
20
+ },
21
+ "type": "module",
22
+ "main": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "NOTICE",
32
+ "dist"
33
+ ],
34
+ "dependencies": {
35
+ "@hono/node-server": "2.1.6",
36
+ "diff": "8.0.4",
37
+ "hono": "4.13.17",
38
+ "@plico-ai/contracts": "0.1.0",
39
+ "@plico-ai/core": "0.1.0",
40
+ "@plico-ai/runtime": "0.1.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=22.18.0"
44
+ },
45
+ "scripts": {
46
+ "typecheck": "tsc -p tsconfig.json --noEmit",
47
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
48
+ "test": "vitest run"
20
49
  }
21
- }
50
+ }