@plico-ai/runtime 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 (188) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +11 -2
  4. package/dist/artifacts.d.ts +10 -0
  5. package/dist/artifacts.js +106 -0
  6. package/dist/engine/context.d.ts +16 -0
  7. package/dist/engine/context.js +77 -0
  8. package/dist/engine/engine.d.ts +12 -0
  9. package/dist/engine/engine.js +235 -0
  10. package/dist/engine/errors.d.ts +12 -0
  11. package/dist/engine/errors.js +19 -0
  12. package/dist/engine/index.d.ts +7 -0
  13. package/dist/engine/index.js +5 -0
  14. package/dist/engine/journal.d.ts +65 -0
  15. package/dist/engine/journal.js +1 -0
  16. package/dist/engine/memory-journal.d.ts +2 -0
  17. package/dist/engine/memory-journal.js +125 -0
  18. package/dist/engine/sqlite-engine.d.ts +7 -0
  19. package/dist/engine/sqlite-engine.js +6 -0
  20. package/dist/engine/sqlite-journal.d.ts +5 -0
  21. package/dist/engine/sqlite-journal.js +136 -0
  22. package/dist/engine/types.d.ts +97 -0
  23. package/dist/engine/types.js +1 -0
  24. package/dist/env.d.ts +7 -0
  25. package/dist/env.js +19 -0
  26. package/dist/errors.d.ts +18 -0
  27. package/dist/errors.js +40 -0
  28. package/dist/evals/discover.d.ts +3 -0
  29. package/dist/evals/discover.js +49 -0
  30. package/dist/evals/index.d.ts +3 -0
  31. package/dist/evals/index.js +3 -0
  32. package/dist/evals/promote.d.ts +7 -0
  33. package/dist/evals/promote.js +32 -0
  34. package/dist/evals/run-evals.d.ts +2 -0
  35. package/dist/evals/run-evals.js +69 -0
  36. package/dist/events.d.ts +28 -0
  37. package/dist/events.js +104 -0
  38. package/dist/guards.d.ts +1 -0
  39. package/dist/guards.js +3 -0
  40. package/dist/index.d.ts +18 -0
  41. package/dist/index.js +17 -0
  42. package/dist/instructions.d.ts +4 -0
  43. package/dist/instructions.js +23 -0
  44. package/dist/policy/approval.d.ts +8 -0
  45. package/dist/policy/approval.js +27 -0
  46. package/dist/providers/ai-sdk.d.ts +10 -0
  47. package/dist/providers/ai-sdk.js +154 -0
  48. package/dist/providers/configured.d.ts +17 -0
  49. package/dist/providers/configured.js +60 -0
  50. package/dist/providers/openai-compatible.d.ts +8 -0
  51. package/dist/providers/openai-compatible.js +14 -0
  52. package/dist/providers/project-module.d.ts +7 -0
  53. package/dist/providers/project-module.js +29 -0
  54. package/dist/providers/registry.d.ts +20 -0
  55. package/dist/providers/registry.js +53 -0
  56. package/dist/readiness/check-readiness.d.ts +2 -0
  57. package/dist/readiness/check-readiness.js +33 -0
  58. package/dist/readiness/checks/approval-policy.d.ts +5 -0
  59. package/dist/readiness/checks/approval-policy.js +19 -0
  60. package/dist/readiness/checks/artifact-access.d.ts +3 -0
  61. package/dist/readiness/checks/artifact-access.js +5 -0
  62. package/dist/readiness/checks/artifacts.d.ts +6 -0
  63. package/dist/readiness/checks/artifacts.js +22 -0
  64. package/dist/readiness/checks/env.d.ts +3 -0
  65. package/dist/readiness/checks/env.js +10 -0
  66. package/dist/readiness/checks/evals.d.ts +2 -0
  67. package/dist/readiness/checks/evals.js +53 -0
  68. package/dist/readiness/checks/persistence.d.ts +3 -0
  69. package/dist/readiness/checks/persistence.js +61 -0
  70. package/dist/readiness/checks/project.d.ts +7 -0
  71. package/dist/readiness/checks/project.js +21 -0
  72. package/dist/readiness/checks/provider.d.ts +2 -0
  73. package/dist/readiness/checks/provider.js +20 -0
  74. package/dist/readiness/checks/server-config.d.ts +2 -0
  75. package/dist/readiness/checks/server-config.js +13 -0
  76. package/dist/readiness/checks/studio.d.ts +2 -0
  77. package/dist/readiness/checks/studio.js +16 -0
  78. package/dist/readiness/checks/thread-store.d.ts +3 -0
  79. package/dist/readiness/checks/thread-store.js +12 -0
  80. package/dist/readiness/checks/tools.d.ts +3 -0
  81. package/dist/readiness/checks/tools.js +42 -0
  82. package/dist/readiness/diagnostic.d.ts +2 -0
  83. package/dist/readiness/diagnostic.js +7 -0
  84. package/dist/readiness/index.d.ts +1 -0
  85. package/dist/readiness/index.js +1 -0
  86. package/dist/readiness/open-database.d.ts +8 -0
  87. package/dist/readiness/open-database.js +15 -0
  88. package/dist/run/artifact-tool-context.d.ts +19 -0
  89. package/dist/run/artifact-tool-context.js +74 -0
  90. package/dist/run/finalize.d.ts +16 -0
  91. package/dist/run/finalize.js +11 -0
  92. package/dist/run/index.d.ts +1 -0
  93. package/dist/run/index.js +1 -0
  94. package/dist/run/pinned-instructions.d.ts +18 -0
  95. package/dist/run/pinned-instructions.js +36 -0
  96. package/dist/run/record-tools.d.ts +8 -0
  97. package/dist/run/record-tools.js +21 -0
  98. package/dist/run/run-project.d.ts +17 -0
  99. package/dist/run/run-project.js +222 -0
  100. package/dist/run/strategies/approval.d.ts +9 -0
  101. package/dist/run/strategies/approval.js +42 -0
  102. package/dist/run/strategies/default-dry.d.ts +3 -0
  103. package/dist/run/strategies/default-dry.js +5 -0
  104. package/dist/run/strategies/provider.d.ts +6 -0
  105. package/dist/run/strategies/provider.js +193 -0
  106. package/dist/run/strategies/scripted.d.ts +5 -0
  107. package/dist/run/strategies/scripted.js +106 -0
  108. package/dist/run/tool-fingerprint.d.ts +2 -0
  109. package/dist/run/tool-fingerprint.js +26 -0
  110. package/dist/run/tool-step.d.ts +4 -0
  111. package/dist/run/tool-step.js +62 -0
  112. package/dist/run/tracing.d.ts +12 -0
  113. package/dist/run/tracing.js +92 -0
  114. package/dist/script.d.ts +4 -0
  115. package/dist/script.js +51 -0
  116. package/dist/storage/index.d.ts +1 -0
  117. package/dist/storage/index.js +1 -0
  118. package/dist/storage/metadata.d.ts +5 -0
  119. package/dist/storage/metadata.js +26 -0
  120. package/dist/storage/sqlite/append-event.d.ts +3 -0
  121. package/dist/storage/sqlite/append-event.js +108 -0
  122. package/dist/storage/sqlite/driver.d.ts +9 -0
  123. package/dist/storage/sqlite/driver.js +13 -0
  124. package/dist/storage/sqlite/index.d.ts +1 -0
  125. package/dist/storage/sqlite/index.js +1 -0
  126. package/dist/storage/sqlite/open.d.ts +3 -0
  127. package/dist/storage/sqlite/open.js +54 -0
  128. package/dist/storage/sqlite/owner-lock.d.ts +5 -0
  129. package/dist/storage/sqlite/owner-lock.js +55 -0
  130. package/dist/storage/sqlite/project-thread.d.ts +4 -0
  131. package/dist/storage/sqlite/project-thread.js +33 -0
  132. package/dist/storage/sqlite/queries.d.ts +9 -0
  133. package/dist/storage/sqlite/queries.js +134 -0
  134. package/dist/storage/sqlite/recover-interrupted.d.ts +2 -0
  135. package/dist/storage/sqlite/recover-interrupted.js +46 -0
  136. package/dist/storage/sqlite/schema.d.ts +10 -0
  137. package/dist/storage/sqlite/schema.js +205 -0
  138. package/dist/storage/sqlite/store.d.ts +2 -0
  139. package/dist/storage/sqlite/store.js +51 -0
  140. package/dist/storage/sqlite/thread-log.d.ts +4 -0
  141. package/dist/storage/sqlite/thread-log.js +9 -0
  142. package/dist/storage/sqlite/thread-title.d.ts +24 -0
  143. package/dist/storage/sqlite/thread-title.js +85 -0
  144. package/dist/storage/sqlite/threads.d.ts +47 -0
  145. package/dist/storage/sqlite/threads.js +156 -0
  146. package/dist/threads/stopwords.d.ts +1 -0
  147. package/dist/threads/stopwords.js +16 -0
  148. package/dist/threads/suggest-title.d.ts +9 -0
  149. package/dist/threads/suggest-title.js +102 -0
  150. package/dist/threads/titler.d.ts +10 -0
  151. package/dist/threads/titler.js +63 -0
  152. package/dist/threads/words.d.ts +12 -0
  153. package/dist/threads/words.js +39 -0
  154. package/dist/tools/artifact-model-tool-operations.d.ts +29 -0
  155. package/dist/tools/artifact-model-tool-operations.js +124 -0
  156. package/dist/tools/artifact-model-tools.d.ts +2 -0
  157. package/dist/tools/artifact-model-tools.js +101 -0
  158. package/dist/tools/discover.d.ts +4 -0
  159. package/dist/tools/discover.js +34 -0
  160. package/dist/tools/index.d.ts +3 -0
  161. package/dist/tools/index.js +3 -0
  162. package/dist/tools/load.d.ts +2 -0
  163. package/dist/tools/load.js +83 -0
  164. package/dist/tools/normalize.d.ts +2 -0
  165. package/dist/tools/normalize.js +48 -0
  166. package/dist/tools/schema.d.ts +7 -0
  167. package/dist/tools/schema.js +101 -0
  168. package/dist/tools/skill-model-tool.d.ts +7 -0
  169. package/dist/tools/skill-model-tool.js +38 -0
  170. package/dist/types/artifacts.d.ts +5 -0
  171. package/dist/types/artifacts.js +1 -0
  172. package/dist/types/evals.d.ts +16 -0
  173. package/dist/types/evals.js +1 -0
  174. package/dist/types/index.d.ts +10 -0
  175. package/dist/types/index.js +1 -0
  176. package/dist/types/provider.d.ts +45 -0
  177. package/dist/types/provider.js +1 -0
  178. package/dist/types/readiness.d.ts +9 -0
  179. package/dist/types/readiness.js +1 -0
  180. package/dist/types/run.d.ts +28 -0
  181. package/dist/types/run.js +1 -0
  182. package/dist/types/script.d.ts +16 -0
  183. package/dist/types/script.js +1 -0
  184. package/dist/types/store.d.ts +24 -0
  185. package/dist/types/store.js +1 -0
  186. package/dist/types/tools.d.ts +15 -0
  187. package/dist/types/tools.js +1 -0
  188. package/package.json +77 -3
@@ -0,0 +1,154 @@
1
+ import { APICallError, dynamicTool, jsonSchema, streamText, } from "ai";
2
+ import { ModelProviderError } from "../errors.js";
3
+ function toMessages(request) {
4
+ return request.messages.map((message) => {
5
+ if (message.role === "system" || message.role === "user")
6
+ return { role: message.role, content: message.content };
7
+ if (message.role === "tool")
8
+ return {
9
+ role: "tool",
10
+ content: [
11
+ {
12
+ type: "tool-result",
13
+ toolCallId: message.toolCallId ?? "",
14
+ toolName: message.toolName ?? "",
15
+ output: { type: "text", value: message.content },
16
+ },
17
+ ],
18
+ };
19
+ const metadata = message.providerMetadata;
20
+ const envelope = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : undefined;
21
+ const adapter = envelope?.plicoAdapter;
22
+ const reasoning = adapter &&
23
+ typeof adapter === "object" &&
24
+ !Array.isArray(adapter) &&
25
+ typeof adapter.reasoning === "string"
26
+ ? adapter.reasoning
27
+ : undefined;
28
+ const providerOptions = (envelope?.sdk ?? metadata);
29
+ return {
30
+ role: "assistant",
31
+ ...(providerOptions === undefined ? {} : { providerOptions }),
32
+ content: [
33
+ ...(reasoning ? [{ type: "reasoning", text: reasoning }] : []),
34
+ ...(message.content ? [{ type: "text", text: message.content }] : []),
35
+ ...(message.toolCalls ?? []).map((call) => ({
36
+ type: "tool-call",
37
+ toolCallId: call.id ?? "",
38
+ toolName: call.name,
39
+ input: call.arguments,
40
+ ...(call.providerMetadata === undefined
41
+ ? {}
42
+ : {
43
+ providerOptions: call.providerMetadata,
44
+ }),
45
+ })),
46
+ ],
47
+ };
48
+ });
49
+ }
50
+ function toTools(request) {
51
+ return Object.fromEntries((request.tools ?? []).map((entry) => [
52
+ entry.name,
53
+ dynamicTool({ description: entry.description, inputSchema: jsonSchema(entry.inputSchema) }),
54
+ ]));
55
+ }
56
+ /** Wraps an AI SDK language model behind the Plico model port. Provider retries are off. */
57
+ export function createAiSdkModelProvider(options) {
58
+ return {
59
+ async *stream(request, { signal }) {
60
+ try {
61
+ const apiKey = options.apiKeyEnv ? process.env[options.apiKeyEnv] : undefined;
62
+ if (options.apiKeyEnv && !apiKey)
63
+ throw new ModelProviderError("auth", `Missing model provider credential: set ${options.apiKeyEnv}.`);
64
+ // Setup problems (a missing adapter package, a broken provider module) are local and
65
+ // actionable, so their message is kept. They are not a network failure and not retried.
66
+ let model;
67
+ try {
68
+ model = await options.createModel(apiKey);
69
+ }
70
+ catch (error) {
71
+ throw new ModelProviderError("bad_request", error instanceof Error ? error.message : "Model provider setup failed.");
72
+ }
73
+ const response = streamText({
74
+ model,
75
+ messages: toMessages(request),
76
+ tools: toTools(request),
77
+ allowSystemInMessages: true,
78
+ maxRetries: 0,
79
+ streamRetries: 0,
80
+ abortSignal: signal,
81
+ onError: () => { },
82
+ });
83
+ let metadata;
84
+ let reasoning = "";
85
+ for await (const part of response.fullStream) {
86
+ if (part.type === "text-delta")
87
+ yield { type: "text", text: part.text };
88
+ else if (part.type === "reasoning-delta")
89
+ reasoning += part.text;
90
+ else if (part.type === "tool-call")
91
+ yield {
92
+ type: "tool.call",
93
+ call: {
94
+ id: part.toolCallId,
95
+ name: part.toolName,
96
+ arguments: part.input,
97
+ ...(part.providerMetadata
98
+ ? { providerMetadata: part.providerMetadata }
99
+ : {}),
100
+ },
101
+ };
102
+ else if (part.type === "finish-step")
103
+ metadata = part.providerMetadata;
104
+ else if (part.type === "error")
105
+ throw part.error;
106
+ else if (part.type === "finish") {
107
+ if (part.finishReason !== "stop" && part.finishReason !== "tool-calls")
108
+ throw new ModelProviderError("bad_request", `Model response did not complete (finish reason: ${part.finishReason}).`);
109
+ yield {
110
+ type: "finish",
111
+ usage: {
112
+ inputTokens: part.totalUsage.inputTokens ?? 0,
113
+ outputTokens: part.totalUsage.outputTokens ?? 0,
114
+ },
115
+ ...(!reasoning && metadata === undefined
116
+ ? {}
117
+ : {
118
+ providerMetadata: {
119
+ ...(metadata === undefined ? {} : { sdk: metadata }),
120
+ ...(reasoning ? { plicoAdapter: { reasoning } } : {}),
121
+ },
122
+ }),
123
+ };
124
+ }
125
+ }
126
+ }
127
+ catch (error) {
128
+ if (signal.aborted)
129
+ throw new ModelProviderError("aborted", "Model request aborted.");
130
+ if (error instanceof ModelProviderError)
131
+ throw error;
132
+ if (APICallError.isInstance(error)) {
133
+ const status = error.statusCode;
134
+ if (status === 401 || status === 403)
135
+ throw new ModelProviderError("auth", "Model provider authentication failed. Check credentials.");
136
+ if (status === 429)
137
+ throw new ModelProviderError("rate_limit", "Model provider rate limit reached.");
138
+ if (status === 503 || (status !== undefined && status >= 500))
139
+ throw new ModelProviderError("overloaded", "Model provider is unavailable.");
140
+ if (status !== undefined && status >= 400 && isContextLengthError(error))
141
+ throw new ModelProviderError("context_length", "The conversation is too long for the model's context window. Start a new thread, or choose a model with a larger context.");
142
+ if (status !== undefined && status >= 400)
143
+ throw new ModelProviderError("bad_request", `Model provider rejected the request (HTTP ${status}).`);
144
+ }
145
+ throw new ModelProviderError("network", "Model provider request failed.");
146
+ }
147
+ },
148
+ };
149
+ }
150
+ const CONTEXT_LENGTH = /context[_ ]length|context window|prompt is too long|too many tokens|exceeds? the maximum (number of )?tokens|input token count/i;
151
+ /** Providers word this differently, so match the code and message they return for an over-long input. */
152
+ function isContextLengthError(error) {
153
+ return CONTEXT_LENGTH.test(`${error.message} ${error.responseBody ?? ""}`);
154
+ }
@@ -0,0 +1,17 @@
1
+ import type { ProjectConfig } from "@plico-ai/contracts";
2
+ import type { ModelProvider } from "../types/index.js";
3
+ type ModelConfig = NonNullable<ProjectConfig["model"]>;
4
+ export declare function supportsModelProvider(provider: string): boolean;
5
+ /**
6
+ * The model port for a project's configured model. A provider outside the native list comes from
7
+ * `providers/<name>.ts` and needs the project root; without it there is no adapter.
8
+ */
9
+ export declare function createConfiguredModelProvider(model: ModelConfig, projectRoot?: string): ModelProvider | undefined;
10
+ /** Hash the provider target and credential without storing either secret in the journal. */
11
+ export declare function modelProviderRecoveryKey(model: ModelConfig): string;
12
+ /**
13
+ * Hash only the account and credential. A queued run has not called a model yet, so changing the
14
+ * configured model must not strand its reservation.
15
+ */
16
+ export declare function modelProviderAccountKey(model: ModelConfig): string;
17
+ export {};
@@ -0,0 +1,60 @@
1
+ import { createHash } from "node:crypto";
2
+ import { NATIVE_MODEL_PROVIDERS } from "@plico-ai/core";
3
+ import { createAiSdkModelProvider } from "./ai-sdk.js";
4
+ import { createOpenAICompatibleProvider } from "./openai-compatible.js";
5
+ import { createProjectModuleProvider } from "./project-module.js";
6
+ import { nativeProvider, resolveModelEndpoint } from "./registry.js";
7
+ export function supportsModelProvider(provider) {
8
+ return NATIVE_MODEL_PROVIDERS.includes(provider);
9
+ }
10
+ /**
11
+ * The model port for a project's configured model. A provider outside the native list comes from
12
+ * `providers/<name>.ts` and needs the project root; without it there is no adapter.
13
+ */
14
+ export function createConfiguredModelProvider(model, projectRoot) {
15
+ const { apiKeyEnv, baseURL } = resolveModelEndpoint(model);
16
+ const native = nativeProvider(model.provider);
17
+ if (native)
18
+ return createAiSdkModelProvider({
19
+ apiKeyEnv,
20
+ createModel: async (apiKey) => (await native.create({
21
+ model: model.name,
22
+ apiKey: apiKey ?? "",
23
+ baseURL,
24
+ })),
25
+ });
26
+ if (model.provider === "openai-compatible")
27
+ return createOpenAICompatibleProvider({
28
+ model: model.name,
29
+ baseURL,
30
+ ...(apiKeyEnv ? { apiKeyEnv } : {}),
31
+ });
32
+ if (!projectRoot)
33
+ return undefined;
34
+ return createProjectModuleProvider(projectRoot, model.provider, {
35
+ model: model.name,
36
+ apiKeyEnv,
37
+ ...(model.baseURL ? { baseURL: model.baseURL } : {}),
38
+ });
39
+ }
40
+ function hashParts(parts) {
41
+ return createHash("sha256").update(JSON.stringify(parts)).digest("hex");
42
+ }
43
+ /** Hash the provider target and credential without storing either secret in the journal. */
44
+ export function modelProviderRecoveryKey(model) {
45
+ const { apiKeyEnv, baseURL } = resolveModelEndpoint(model);
46
+ return hashParts([
47
+ model.provider,
48
+ model.name,
49
+ baseURL,
50
+ (apiKeyEnv ? process.env[apiKeyEnv] : "") ?? "",
51
+ ]);
52
+ }
53
+ /**
54
+ * Hash only the account and credential. A queued run has not called a model yet, so changing the
55
+ * configured model must not strand its reservation.
56
+ */
57
+ export function modelProviderAccountKey(model) {
58
+ const { apiKeyEnv, baseURL } = resolveModelEndpoint(model);
59
+ return hashParts([model.provider, baseURL, (apiKeyEnv ? process.env[apiKeyEnv] : "") ?? ""]);
60
+ }
@@ -0,0 +1,8 @@
1
+ import type { ModelProvider } from "../types/index.js";
2
+ export interface OpenAICompatibleProviderOptions {
3
+ model: string;
4
+ apiKeyEnv?: string;
5
+ baseURL?: string;
6
+ fetch?: typeof globalThis.fetch;
7
+ }
8
+ export declare function createOpenAICompatibleProvider(options: OpenAICompatibleProviderOptions): ModelProvider;
@@ -0,0 +1,14 @@
1
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
2
+ import { createAiSdkModelProvider } from "./ai-sdk.js";
3
+ export function createOpenAICompatibleProvider(options) {
4
+ return createAiSdkModelProvider({
5
+ apiKeyEnv: options.apiKeyEnv ?? "OPENAI_API_KEY",
6
+ createModel: (apiKey) => createOpenAICompatible({
7
+ name: "plico",
8
+ baseURL: options.baseURL ?? "https://api.openai.com/v1",
9
+ ...(apiKey ? { apiKey } : {}),
10
+ includeUsage: true,
11
+ ...(options.fetch ? { fetch: options.fetch } : {}),
12
+ })(options.model),
13
+ });
14
+ }
@@ -0,0 +1,7 @@
1
+ import type { ModelProvider } from "../types/index.js";
2
+ /** A provider the project supplies in `providers/<name>.ts`, run behind the Plico model port. */
3
+ export declare function createProjectModuleProvider(root: string, provider: string, input: {
4
+ model: string;
5
+ apiKeyEnv: string | undefined;
6
+ baseURL?: string;
7
+ }): ModelProvider;
@@ -0,0 +1,29 @@
1
+ import { pathToFileURL } from "node:url";
2
+ import { findProjectProviderFile } from "@plico-ai/core";
3
+ import { register } from "tsx/esm/api";
4
+ import { createAiSdkModelProvider } from "./ai-sdk.js";
5
+ let registered = false;
6
+ /** A provider the project supplies in `providers/<name>.ts`, run behind the Plico model port. */
7
+ export function createProjectModuleProvider(root, provider, input) {
8
+ return createAiSdkModelProvider({
9
+ apiKeyEnv: input.apiKeyEnv,
10
+ createModel: async (apiKey) => {
11
+ const file = await findProjectProviderFile(root, provider);
12
+ if (!file)
13
+ throw new Error(`Missing model provider module: providers/${provider}.ts`);
14
+ if (!registered) {
15
+ register();
16
+ registered = true;
17
+ }
18
+ const loaded = (await import(pathToFileURL(file).href));
19
+ if (typeof loaded.default !== "function")
20
+ throw new Error(`providers/${provider} must export a function by default.`);
21
+ const options = {
22
+ model: input.model,
23
+ ...(apiKey ? { apiKey } : {}),
24
+ ...(input.baseURL ? { baseURL: input.baseURL } : {}),
25
+ };
26
+ return (await loaded.default(options));
27
+ },
28
+ });
29
+ }
@@ -0,0 +1,20 @@
1
+ import type { ProjectConfig } from "@plico-ai/contracts";
2
+ type ModelConfig = NonNullable<ProjectConfig["model"]>;
3
+ interface NativeProvider {
4
+ apiKeyEnv: string;
5
+ /** The endpoint used when neither the config nor the environment sets one. */
6
+ baseURL: string;
7
+ /** Returns an AI SDK language model. Adapter packages may pin another `@ai-sdk/provider` copy, so it is typed loosely here. */
8
+ create: (input: {
9
+ model: string;
10
+ apiKey: string;
11
+ baseURL: string;
12
+ }) => Promise<unknown>;
13
+ }
14
+ export declare function nativeProvider(provider: string): NativeProvider | undefined;
15
+ /** The credential variable and endpoint a configured model uses, for the run and the recovery key. */
16
+ export declare function resolveModelEndpoint(model: ModelConfig): {
17
+ apiKeyEnv: string | undefined;
18
+ baseURL: string;
19
+ };
20
+ export {};
@@ -0,0 +1,53 @@
1
+ /** The adapters are optional peer packages, so each loads only when its provider is used. */
2
+ async function load(packageName, importer) {
3
+ try {
4
+ return await importer();
5
+ }
6
+ catch {
7
+ throw new Error(`Install ${packageName} to use this model provider: pnpm add ${packageName}`);
8
+ }
9
+ }
10
+ const OPENAI_URL = "https://api.openai.com/v1";
11
+ const NATIVE = {
12
+ anthropic: {
13
+ apiKeyEnv: "ANTHROPIC_API_KEY",
14
+ baseURL: "https://api.anthropic.com/v1",
15
+ create: async ({ model, apiKey, baseURL }) => (await load("@ai-sdk/anthropic", () => import("@ai-sdk/anthropic"))).createAnthropic({
16
+ apiKey,
17
+ baseURL,
18
+ })(model),
19
+ },
20
+ google: {
21
+ apiKeyEnv: "GOOGLE_GENERATIVE_AI_API_KEY",
22
+ baseURL: "https://generativelanguage.googleapis.com/v1beta",
23
+ create: async ({ model, apiKey, baseURL }) => (await load("@ai-sdk/google", () => import("@ai-sdk/google"))).createGoogleGenerativeAI({
24
+ apiKey,
25
+ baseURL,
26
+ })(model),
27
+ },
28
+ openai: {
29
+ apiKeyEnv: "OPENAI_API_KEY",
30
+ baseURL: OPENAI_URL,
31
+ // The chat endpoint is the one every OpenAI-compatible server also speaks.
32
+ create: async ({ model, apiKey, baseURL }) => (await load("@ai-sdk/openai", () => import("@ai-sdk/openai")))
33
+ .createOpenAI({ apiKey, baseURL })
34
+ .chat(model),
35
+ },
36
+ };
37
+ export function nativeProvider(provider) {
38
+ return NATIVE[provider];
39
+ }
40
+ /** The credential variable and endpoint a configured model uses, for the run and the recovery key. */
41
+ export function resolveModelEndpoint(model) {
42
+ const native = nativeProvider(model.provider);
43
+ const usesOpenAIEnv = model.provider === "openai" || model.provider === "openai-compatible";
44
+ return {
45
+ apiKeyEnv: model.apiKeyEnv ??
46
+ native?.apiKeyEnv ??
47
+ (model.provider === "openai-compatible" ? "OPENAI_API_KEY" : undefined),
48
+ baseURL: model.baseURL ??
49
+ (usesOpenAIEnv ? process.env.OPENAI_BASE_URL : undefined) ??
50
+ native?.baseURL ??
51
+ OPENAI_URL,
52
+ };
53
+ }
@@ -0,0 +1,2 @@
1
+ import type { ReadinessOptions, ReadinessReport } from "../types/index.js";
2
+ export declare function checkReadiness(projectRoot: string, options?: ReadinessOptions): Promise<ReadinessReport>;
@@ -0,0 +1,33 @@
1
+ import { join } from "node:path";
2
+ import { checkArtifactAccess } from "./checks/artifact-access.js";
3
+ import { checkArtifacts } from "./checks/artifacts.js";
4
+ import { checkEnv } from "./checks/env.js";
5
+ import { checkEvals } from "./checks/evals.js";
6
+ import { checkPersistence } from "./checks/persistence.js";
7
+ import { checkProject } from "./checks/project.js";
8
+ import { checkProvider } from "./checks/provider.js";
9
+ import { checkServerConfig } from "./checks/server-config.js";
10
+ import { checkStudio } from "./checks/studio.js";
11
+ import { checkThreadStore } from "./checks/thread-store.js";
12
+ import { checkTools } from "./checks/tools.js";
13
+ import { openDatabase } from "./open-database.js";
14
+ export async function checkReadiness(projectRoot, options = {}) {
15
+ const projectCheck = await checkProject(projectRoot);
16
+ const diagnostics = [...projectCheck.diagnostics];
17
+ if (!projectCheck.project) {
18
+ return summarize(projectRoot, diagnostics);
19
+ }
20
+ const databasePath = options.databasePath ?? join(projectRoot, ".plico", "plico.sqlite");
21
+ const database = openDatabase(databasePath, options.eventStore);
22
+ diagnostics.push(...checkEnv(projectRoot), ...checkProvider(projectCheck.project.config.model), checkArtifactAccess(projectCheck.project.config), ...(await checkTools(projectCheck.project)), ...(await checkEvals(projectRoot, database.eventStore)), ...checkThreadStore(database.eventStore, options.threadStore), ...(await checkPersistence(databasePath, database)), ...(await checkArtifacts(projectRoot)), ...(await checkServerConfig(projectRoot)), ...(await checkStudio(options.studioPath)));
23
+ return summarize(projectRoot, diagnostics);
24
+ }
25
+ function summarize(projectRoot, diagnostics) {
26
+ const count = (severity) => diagnostics.filter((d) => d.severity === severity).length;
27
+ return {
28
+ projectRoot,
29
+ timestamp: new Date().toISOString(),
30
+ diagnostics,
31
+ summary: { ok: count("ok"), warning: count("warning"), error: count("error") },
32
+ };
33
+ }
@@ -0,0 +1,5 @@
1
+ import type { LoadedProject } from "@plico-ai/core";
2
+ import type { DeclaredTool, ReadinessDiagnostic } from "../../types/index.js";
3
+ export declare function checkApprovalPolicy(project: LoadedProject, tools: DeclaredTool[], options?: {
4
+ toolsComplete?: boolean;
5
+ }): ReadinessDiagnostic[];
@@ -0,0 +1,19 @@
1
+ import { makeDiagnostic } from "../diagnostic.js";
2
+ export function checkApprovalPolicy(project, tools, options = {}) {
3
+ const toolsComplete = options.toolsComplete ?? true;
4
+ const names = new Set(tools.map((tool) => tool.name));
5
+ const diagnostics = toolsComplete
6
+ ? Object.keys(project.config.approvals?.tools ?? {})
7
+ .filter((name) => !names.has(name))
8
+ .map((name) => makeDiagnostic("warning", "approvals.policy", `Approval policy refers to undeclared tool "${name}".`, "plico.config.ts"))
9
+ : [];
10
+ for (const tool of tools) {
11
+ if (tool.approval.required && project.config.approvals?.tools?.[tool.name] === "none") {
12
+ diagnostics.push(makeDiagnostic("warning", "approvals.policy", `Tool "${tool.name}" requires approval in its declaration; the "none" override has no effect.`));
13
+ }
14
+ }
15
+ if (diagnostics.length === 0 && toolsComplete) {
16
+ diagnostics.push(makeDiagnostic("ok", "approvals.policy", `Approval policy checked against ${tools.length} declared tool(s).`));
17
+ }
18
+ return diagnostics;
19
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProjectConfig } from "@plico-ai/contracts";
2
+ import type { ReadinessDiagnostic } from "../../types/index.js";
3
+ export declare function checkArtifactAccess(config: ProjectConfig): ReadinessDiagnostic;
@@ -0,0 +1,5 @@
1
+ import { makeDiagnostic } from "../diagnostic.js";
2
+ export function checkArtifactAccess(config) {
3
+ const access = config.artifacts?.access ?? "read-write";
4
+ return makeDiagnostic("ok", "artifacts.access", `Model-facing artifact tools access is "${access}".`, "plico.config.ts");
5
+ }
@@ -0,0 +1,6 @@
1
+ import type { ReadinessDiagnostic } from "../../types/index.js";
2
+ /**
3
+ * Files a run writes are events in the log (ADR 0002), not files in `artifacts/`, so the folder
4
+ * is only part of the project layout. Its writability says nothing about artifacts and is not checked.
5
+ */
6
+ export declare function checkArtifacts(projectRoot: string): Promise<ReadinessDiagnostic[]>;
@@ -0,0 +1,22 @@
1
+ import { access } from "node:fs/promises";
2
+ import { getProjectPaths, getProjectRelativePaths } from "@plico-ai/core";
3
+ import { makeDiagnostic } from "../diagnostic.js";
4
+ /**
5
+ * Files a run writes are events in the log (ADR 0002), not files in `artifacts/`, so the folder
6
+ * is only part of the project layout. Its writability says nothing about artifacts and is not checked.
7
+ */
8
+ export async function checkArtifacts(projectRoot) {
9
+ const directory = getProjectPaths(projectRoot).directories.artifacts;
10
+ const folder = `${getProjectRelativePaths().directories.artifacts}/`;
11
+ try {
12
+ await access(directory);
13
+ }
14
+ catch {
15
+ return [
16
+ makeDiagnostic("warning", "artifacts.workspace", `${folder} directory not found. Create it to keep the project layout complete.`),
17
+ ];
18
+ }
19
+ return [
20
+ makeDiagnostic("ok", "artifacts.workspace", "Artifacts a run writes are stored in the event log, not in this folder", folder),
21
+ ];
22
+ }
@@ -0,0 +1,3 @@
1
+ import type { ReadinessDiagnostic } from "@plico-ai/contracts";
2
+ /** Says which `.env` file was loaded. It never reads or prints the values. */
3
+ export declare function checkEnv(projectRoot: string): ReadinessDiagnostic[];
@@ -0,0 +1,10 @@
1
+ import { join } from "node:path";
2
+ import { loadedEnvFiles } from "../../env.js";
3
+ import { makeDiagnostic } from "../diagnostic.js";
4
+ /** Says which `.env` file was loaded. It never reads or prints the values. */
5
+ export function checkEnv(projectRoot) {
6
+ const file = join(projectRoot, ".env");
7
+ if (!loadedEnvFiles().includes(file))
8
+ return [];
9
+ return [makeDiagnostic("ok", "env.loaded", "Environment variables loaded from .env.", ".env")];
10
+ }
@@ -0,0 +1,2 @@
1
+ import type { EventStore, ReadinessDiagnostic } from "../../types/index.js";
2
+ export declare function checkEvals(projectRoot: string, eventStore: EventStore | undefined): Promise<ReadinessDiagnostic[]>;
@@ -0,0 +1,53 @@
1
+ import { access } from "node:fs/promises";
2
+ import { getProjectPaths, getProjectRelativePaths } from "@plico-ai/core";
3
+ import { digestEvalFixtures, discoverEvalCases } from "../../evals/discover.js";
4
+ import { makeDiagnostic } from "../diagnostic.js";
5
+ export async function checkEvals(projectRoot, eventStore) {
6
+ const evalsDir = getProjectPaths(projectRoot).directories.evals;
7
+ const folder = `${getProjectRelativePaths().directories.evals}/`;
8
+ try {
9
+ await access(evalsDir);
10
+ }
11
+ catch {
12
+ return [
13
+ makeDiagnostic("warning", "evals.fixtures", `${folder} directory not found. Create ${folder} to add eval fixtures.`),
14
+ ];
15
+ }
16
+ let cases;
17
+ try {
18
+ cases = await discoverEvalCases(evalsDir);
19
+ }
20
+ catch (error) {
21
+ return [makeDiagnostic("error", "evals.fixtures", error.message)];
22
+ }
23
+ const fixtures = cases.length === 0
24
+ ? makeDiagnostic("warning", "evals.fixtures", "No .script.json eval fixtures found. Add evals to track behavior.")
25
+ : makeDiagnostic("ok", "evals.fixtures", `${cases.length} eval fixture(s) found`);
26
+ return eventStore
27
+ ? [fixtures, await readEvalResults(eventStore, await digestEvalFixtures(evalsDir))]
28
+ : [fixtures];
29
+ }
30
+ async function readEvalResults(eventStore, fixturesDigest) {
31
+ try {
32
+ return describeEvalResults(await eventStore.getLatestEvalReport(), fixturesDigest);
33
+ }
34
+ catch (error) {
35
+ return makeDiagnostic("error", "evals.results", `Could not read eval results: ${error.message}`);
36
+ }
37
+ }
38
+ function describeEvalResults(report, fixturesDigest) {
39
+ if (!report) {
40
+ return makeDiagnostic("warning", "evals.results", "No eval runs have been executed. Run plico eval to see results.");
41
+ }
42
+ if (report.fixturesDigest !== fixturesDigest) {
43
+ return makeDiagnostic("warning", "evals.results", "Eval fixtures changed since the latest run. Run plico eval to refresh results.");
44
+ }
45
+ if (report.total === 0) {
46
+ return makeDiagnostic("warning", "evals.results", "The latest eval run found no fixtures.");
47
+ }
48
+ if (report.failed > 0) {
49
+ const names = report.results.filter((r) => r.status === "fail").map((r) => r.name);
50
+ return makeDiagnostic("error", "evals.results", `${report.failed}/${report.total} eval(s) failed in the latest run: ${names.join(", ")}`);
51
+ }
52
+ return makeDiagnostic("ok", "evals.results", `${report.passed}/${report.total} eval(s) passed (${report.evaluatedAt})`);
53
+ }
@@ -0,0 +1,3 @@
1
+ import type { ReadinessDiagnostic } from "../../types/index.js";
2
+ import type { ReadinessDatabase } from "../open-database.js";
3
+ export declare function checkPersistence(dbPath: string, database: ReadinessDatabase): Promise<ReadinessDiagnostic[]>;
@@ -0,0 +1,61 @@
1
+ import { access, constants } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { makeDiagnostic } from "../diagnostic.js";
4
+ export async function checkPersistence(dbPath, database) {
5
+ if (database.openError !== undefined) {
6
+ return [cannotRead(database.openError, dbPath)];
7
+ }
8
+ if (database.eventStore) {
9
+ try {
10
+ await database.eventStore.listRuns();
11
+ }
12
+ catch (error) {
13
+ return [cannotRead(error.message, dbPath)];
14
+ }
15
+ }
16
+ // An attached store is already open for writing, so only its reads need proving.
17
+ if (database.attached) {
18
+ return [makeDiagnostic("ok", "persistence.storage", "Event store is attached")];
19
+ }
20
+ if (database.eventStore) {
21
+ const writable = (await exists(dbPath, constants.W_OK)) && (await exists(dirname(dbPath), constants.W_OK));
22
+ return writable
23
+ ? [makeDiagnostic("ok", "persistence.storage", "Database is writable", dbPath)]
24
+ : [makeDiagnostic("error", "persistence.storage", "Database is not writable", dbPath)];
25
+ }
26
+ // Read-only probe: the database can be created later if the nearest existing
27
+ // ancestor directory is writable. Readiness must never touch the filesystem.
28
+ if (await isCreatable(dbPath)) {
29
+ return [
30
+ makeDiagnostic("warning", "persistence.storage", "Database file does not exist yet but its location is writable. Run plico run --dry --persist to initialize.", dbPath),
31
+ ];
32
+ }
33
+ return [
34
+ makeDiagnostic("error", "persistence.storage", "Persistence path is not writable", dbPath),
35
+ ];
36
+ }
37
+ function cannotRead(reason, dbPath) {
38
+ return makeDiagnostic("error", "persistence.storage", `Database cannot be read: ${reason}`, dbPath);
39
+ }
40
+ async function isCreatable(path) {
41
+ let current = dirname(path);
42
+ while (true) {
43
+ if (await exists(current, constants.W_OK))
44
+ return true;
45
+ if (await exists(current, constants.F_OK))
46
+ return false;
47
+ const parent = dirname(current);
48
+ if (parent === current)
49
+ return false;
50
+ current = parent;
51
+ }
52
+ }
53
+ async function exists(path, mode) {
54
+ try {
55
+ await access(path, mode);
56
+ return true;
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
@@ -0,0 +1,7 @@
1
+ import { type LoadedProject } from "@plico-ai/core";
2
+ import type { ReadinessDiagnostic } from "../../types/index.js";
3
+ export interface ProjectCheck {
4
+ diagnostics: ReadinessDiagnostic[];
5
+ project: LoadedProject | undefined;
6
+ }
7
+ export declare function checkProject(projectRoot: string): Promise<ProjectCheck>;