@plico-ai/runtime 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.
- package/NOTICE +4 -0
- package/README.md +9 -3
- package/dist/artifacts.d.ts +10 -0
- package/dist/artifacts.js +106 -0
- package/dist/engine/context.d.ts +16 -0
- package/dist/engine/context.js +77 -0
- package/dist/engine/engine.d.ts +12 -0
- package/dist/engine/engine.js +235 -0
- package/dist/engine/errors.d.ts +12 -0
- package/dist/engine/errors.js +19 -0
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.js +5 -0
- package/dist/engine/journal.d.ts +65 -0
- package/dist/engine/journal.js +1 -0
- package/dist/engine/memory-journal.d.ts +2 -0
- package/dist/engine/memory-journal.js +125 -0
- package/dist/engine/sqlite-engine.d.ts +7 -0
- package/dist/engine/sqlite-engine.js +6 -0
- package/dist/engine/sqlite-journal.d.ts +5 -0
- package/dist/engine/sqlite-journal.js +136 -0
- package/dist/engine/types.d.ts +97 -0
- package/dist/engine/types.js +1 -0
- package/dist/env.d.ts +7 -0
- package/dist/env.js +19 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +40 -0
- package/dist/evals/discover.d.ts +3 -0
- package/dist/evals/discover.js +49 -0
- package/dist/evals/index.d.ts +3 -0
- package/dist/evals/index.js +3 -0
- package/dist/evals/promote.d.ts +7 -0
- package/dist/evals/promote.js +32 -0
- package/dist/evals/run-evals.d.ts +2 -0
- package/dist/evals/run-evals.js +69 -0
- package/dist/events.d.ts +28 -0
- package/dist/events.js +104 -0
- package/dist/guards.d.ts +1 -0
- package/dist/guards.js +3 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +17 -0
- package/dist/instructions.d.ts +4 -0
- package/dist/instructions.js +23 -0
- package/dist/policy/approval.d.ts +8 -0
- package/dist/policy/approval.js +27 -0
- package/dist/providers/ai-sdk.d.ts +10 -0
- package/dist/providers/ai-sdk.js +154 -0
- package/dist/providers/configured.d.ts +17 -0
- package/dist/providers/configured.js +60 -0
- package/dist/providers/openai-compatible.d.ts +8 -0
- package/dist/providers/openai-compatible.js +14 -0
- package/dist/providers/project-module.d.ts +7 -0
- package/dist/providers/project-module.js +29 -0
- package/dist/providers/registry.d.ts +20 -0
- package/dist/providers/registry.js +53 -0
- package/dist/readiness/check-readiness.d.ts +2 -0
- package/dist/readiness/check-readiness.js +33 -0
- package/dist/readiness/checks/approval-policy.d.ts +5 -0
- package/dist/readiness/checks/approval-policy.js +19 -0
- package/dist/readiness/checks/artifact-access.d.ts +3 -0
- package/dist/readiness/checks/artifact-access.js +5 -0
- package/dist/readiness/checks/artifacts.d.ts +6 -0
- package/dist/readiness/checks/artifacts.js +22 -0
- package/dist/readiness/checks/env.d.ts +3 -0
- package/dist/readiness/checks/env.js +10 -0
- package/dist/readiness/checks/evals.d.ts +2 -0
- package/dist/readiness/checks/evals.js +53 -0
- package/dist/readiness/checks/persistence.d.ts +3 -0
- package/dist/readiness/checks/persistence.js +61 -0
- package/dist/readiness/checks/project.d.ts +7 -0
- package/dist/readiness/checks/project.js +21 -0
- package/dist/readiness/checks/provider.d.ts +2 -0
- package/dist/readiness/checks/provider.js +20 -0
- package/dist/readiness/checks/server-config.d.ts +2 -0
- package/dist/readiness/checks/server-config.js +13 -0
- package/dist/readiness/checks/studio.d.ts +2 -0
- package/dist/readiness/checks/studio.js +16 -0
- package/dist/readiness/checks/thread-store.d.ts +3 -0
- package/dist/readiness/checks/thread-store.js +12 -0
- package/dist/readiness/checks/tools.d.ts +3 -0
- package/dist/readiness/checks/tools.js +42 -0
- package/dist/readiness/diagnostic.d.ts +2 -0
- package/dist/readiness/diagnostic.js +7 -0
- package/dist/readiness/index.d.ts +1 -0
- package/dist/readiness/index.js +1 -0
- package/dist/readiness/open-database.d.ts +8 -0
- package/dist/readiness/open-database.js +15 -0
- package/dist/run/artifact-tool-context.d.ts +19 -0
- package/dist/run/artifact-tool-context.js +74 -0
- package/dist/run/finalize.d.ts +16 -0
- package/dist/run/finalize.js +11 -0
- package/dist/run/index.d.ts +1 -0
- package/dist/run/index.js +1 -0
- package/dist/run/pinned-instructions.d.ts +18 -0
- package/dist/run/pinned-instructions.js +36 -0
- package/dist/run/record-tools.d.ts +8 -0
- package/dist/run/record-tools.js +21 -0
- package/dist/run/run-project.d.ts +17 -0
- package/dist/run/run-project.js +222 -0
- package/dist/run/strategies/approval.d.ts +9 -0
- package/dist/run/strategies/approval.js +42 -0
- package/dist/run/strategies/default-dry.d.ts +3 -0
- package/dist/run/strategies/default-dry.js +5 -0
- package/dist/run/strategies/provider.d.ts +6 -0
- package/dist/run/strategies/provider.js +193 -0
- package/dist/run/strategies/scripted.d.ts +5 -0
- package/dist/run/strategies/scripted.js +106 -0
- package/dist/run/tool-fingerprint.d.ts +2 -0
- package/dist/run/tool-fingerprint.js +26 -0
- package/dist/run/tool-step.d.ts +4 -0
- package/dist/run/tool-step.js +62 -0
- package/dist/run/tracing.d.ts +12 -0
- package/dist/run/tracing.js +92 -0
- package/dist/script.d.ts +4 -0
- package/dist/script.js +51 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/metadata.d.ts +5 -0
- package/dist/storage/metadata.js +26 -0
- package/dist/storage/sqlite/append-event.d.ts +3 -0
- package/dist/storage/sqlite/append-event.js +108 -0
- package/dist/storage/sqlite/driver.d.ts +9 -0
- package/dist/storage/sqlite/driver.js +13 -0
- package/dist/storage/sqlite/index.d.ts +1 -0
- package/dist/storage/sqlite/index.js +1 -0
- package/dist/storage/sqlite/open.d.ts +3 -0
- package/dist/storage/sqlite/open.js +54 -0
- package/dist/storage/sqlite/owner-lock.d.ts +5 -0
- package/dist/storage/sqlite/owner-lock.js +55 -0
- package/dist/storage/sqlite/project-thread.d.ts +4 -0
- package/dist/storage/sqlite/project-thread.js +33 -0
- package/dist/storage/sqlite/queries.d.ts +9 -0
- package/dist/storage/sqlite/queries.js +134 -0
- package/dist/storage/sqlite/recover-interrupted.d.ts +2 -0
- package/dist/storage/sqlite/recover-interrupted.js +46 -0
- package/dist/storage/sqlite/schema.d.ts +10 -0
- package/dist/storage/sqlite/schema.js +205 -0
- package/dist/storage/sqlite/store.d.ts +2 -0
- package/dist/storage/sqlite/store.js +51 -0
- package/dist/storage/sqlite/thread-log.d.ts +4 -0
- package/dist/storage/sqlite/thread-log.js +9 -0
- package/dist/storage/sqlite/thread-title.d.ts +24 -0
- package/dist/storage/sqlite/thread-title.js +85 -0
- package/dist/storage/sqlite/threads.d.ts +47 -0
- package/dist/storage/sqlite/threads.js +156 -0
- package/dist/threads/stopwords.d.ts +1 -0
- package/dist/threads/stopwords.js +16 -0
- package/dist/threads/suggest-title.d.ts +9 -0
- package/dist/threads/suggest-title.js +102 -0
- package/dist/threads/titler.d.ts +10 -0
- package/dist/threads/titler.js +63 -0
- package/dist/threads/words.d.ts +12 -0
- package/dist/threads/words.js +39 -0
- package/dist/tools/artifact-model-tool-operations.d.ts +29 -0
- package/dist/tools/artifact-model-tool-operations.js +124 -0
- package/dist/tools/artifact-model-tools.d.ts +2 -0
- package/dist/tools/artifact-model-tools.js +101 -0
- package/dist/tools/discover.d.ts +4 -0
- package/dist/tools/discover.js +34 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/load.d.ts +2 -0
- package/dist/tools/load.js +83 -0
- package/dist/tools/normalize.d.ts +2 -0
- package/dist/tools/normalize.js +48 -0
- package/dist/tools/schema.d.ts +7 -0
- package/dist/tools/schema.js +101 -0
- package/dist/tools/skill-model-tool.d.ts +7 -0
- package/dist/tools/skill-model-tool.js +38 -0
- package/dist/types/artifacts.d.ts +5 -0
- package/dist/types/artifacts.js +1 -0
- package/dist/types/evals.d.ts +16 -0
- package/dist/types/evals.js +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.js +1 -0
- package/dist/types/provider.d.ts +45 -0
- package/dist/types/provider.js +1 -0
- package/dist/types/readiness.d.ts +9 -0
- package/dist/types/readiness.js +1 -0
- package/dist/types/run.d.ts +28 -0
- package/dist/types/run.js +1 -0
- package/dist/types/script.d.ts +16 -0
- package/dist/types/script.js +1 -0
- package/dist/types/store.d.ts +24 -0
- package/dist/types/store.js +1 -0
- package/dist/types/tools.d.ts +15 -0
- package/dist/types/tools.js +1 -0
- package/package.json +67 -8
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import mime from "mime";
|
|
2
|
+
import picomatch from "picomatch";
|
|
3
|
+
import { validateArtifactPath } from "../run/artifact-tool-context.js";
|
|
4
|
+
const defaultListLimit = 100;
|
|
5
|
+
const defaultReadLimit = 200;
|
|
6
|
+
const maxReadBytes = 32 * 1024;
|
|
7
|
+
export async function listFiles(input, artifacts) {
|
|
8
|
+
const value = input;
|
|
9
|
+
const offset = value.offset ?? 0;
|
|
10
|
+
const limit = value.limit ?? defaultListLimit;
|
|
11
|
+
let matches = await artifacts.list();
|
|
12
|
+
if (value.glob !== undefined) {
|
|
13
|
+
let match;
|
|
14
|
+
try {
|
|
15
|
+
match = picomatch(value.glob, { strictBrackets: true, maxLength: 1024, dot: true });
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
throw new Error(`Invalid file glob: ${error instanceof Error ? error.message : String(error)}`);
|
|
19
|
+
}
|
|
20
|
+
matches = matches.filter((file) => match(file.path));
|
|
21
|
+
}
|
|
22
|
+
matches.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
23
|
+
const files = matches
|
|
24
|
+
.slice(offset, offset + limit)
|
|
25
|
+
.map(({ path, sizeBytes, mimeType, version }) => ({
|
|
26
|
+
path,
|
|
27
|
+
sizeBytes,
|
|
28
|
+
mimeType,
|
|
29
|
+
version,
|
|
30
|
+
}));
|
|
31
|
+
const nextOffset = offset + files.length;
|
|
32
|
+
return {
|
|
33
|
+
files,
|
|
34
|
+
offset,
|
|
35
|
+
...(nextOffset < matches.length ? { nextOffset } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export async function readFile(input, artifacts) {
|
|
39
|
+
const value = input;
|
|
40
|
+
validateArtifactPath(value.path);
|
|
41
|
+
const file = await artifacts.read(value.path);
|
|
42
|
+
if (!file)
|
|
43
|
+
throw new Error(`Artifact not found: ${value.path}`);
|
|
44
|
+
if (file.content instanceof Uint8Array)
|
|
45
|
+
return { ...metadata(file), path: file.path, binary: true };
|
|
46
|
+
const lines = file.content.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
47
|
+
const offset = value.offset ?? 1;
|
|
48
|
+
const limit = value.limit ?? defaultReadLimit;
|
|
49
|
+
const start = offset - 1;
|
|
50
|
+
const page = [];
|
|
51
|
+
let pageBytes = 0;
|
|
52
|
+
for (let index = start; index < Math.min(lines.length, start + limit); index++) {
|
|
53
|
+
const line = lines[index] ?? "";
|
|
54
|
+
const lineBytes = Buffer.byteLength(line, "utf8");
|
|
55
|
+
if (pageBytes + lineBytes > maxReadBytes) {
|
|
56
|
+
if (page.length === 0)
|
|
57
|
+
throw new Error(`Line ${index + 1} of ${value.path} exceeds the 32 KiB read limit.`);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
page.push(line);
|
|
61
|
+
pageBytes += lineBytes;
|
|
62
|
+
}
|
|
63
|
+
const nextOffset = start + page.length + 1;
|
|
64
|
+
return {
|
|
65
|
+
path: file.path,
|
|
66
|
+
content: page.join(""),
|
|
67
|
+
mimeType: file.mimeType,
|
|
68
|
+
version: file.version,
|
|
69
|
+
offset,
|
|
70
|
+
...(nextOffset <= lines.length ? { nextOffset } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export async function writeFile(input, artifacts) {
|
|
74
|
+
const value = input;
|
|
75
|
+
const mimeType = value.mimeType ?? mime.getType(value.path) ?? "text/plain";
|
|
76
|
+
return artifacts.write(value.path, value.content, { mimeType });
|
|
77
|
+
}
|
|
78
|
+
export async function editFile(input, artifacts) {
|
|
79
|
+
const value = input;
|
|
80
|
+
if (value.old === value.new)
|
|
81
|
+
throw new Error("The old and new text are identical; no edit was made.");
|
|
82
|
+
validateArtifactPath(value.path);
|
|
83
|
+
const file = await artifacts.read(value.path);
|
|
84
|
+
if (!file)
|
|
85
|
+
throw new Error(`Artifact not found: ${value.path}`);
|
|
86
|
+
if (file.content instanceof Uint8Array)
|
|
87
|
+
throw new Error(`Cannot edit binary artifact ${value.path}; edit_file accepts text only.`);
|
|
88
|
+
const occurrences = countMatches(file.content, value.old);
|
|
89
|
+
if (occurrences === 0)
|
|
90
|
+
throw new Error(`Text to replace was not found in ${value.path}.`);
|
|
91
|
+
if (occurrences > 1 && value.replaceAll !== true)
|
|
92
|
+
throw new Error(`Text to replace occurs ${occurrences} times in ${value.path}; set replaceAll to true.`);
|
|
93
|
+
const content = value.replaceAll
|
|
94
|
+
? file.content.replaceAll(value.old, () => value.new)
|
|
95
|
+
: replaceFirst(file.content, value.old, value.new);
|
|
96
|
+
const write = await artifacts.write(value.path, content, { mimeType: file.mimeType });
|
|
97
|
+
return { ...write, replacementCount: value.replaceAll ? occurrences : 1 };
|
|
98
|
+
}
|
|
99
|
+
function metadata(file) {
|
|
100
|
+
return {
|
|
101
|
+
path: file.path,
|
|
102
|
+
sizeBytes: file.content instanceof Uint8Array
|
|
103
|
+
? file.content.byteLength
|
|
104
|
+
: Buffer.byteLength(file.content, "utf8"),
|
|
105
|
+
mimeType: file.mimeType,
|
|
106
|
+
version: file.version,
|
|
107
|
+
contentHash: file.contentHash,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function countMatches(content, search) {
|
|
111
|
+
let count = 0;
|
|
112
|
+
let offset = 0;
|
|
113
|
+
while (true) {
|
|
114
|
+
const index = content.indexOf(search, offset);
|
|
115
|
+
if (index < 0)
|
|
116
|
+
return count;
|
|
117
|
+
count++;
|
|
118
|
+
offset = index + search.length;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function replaceFirst(content, search, replacement) {
|
|
122
|
+
const index = content.indexOf(search);
|
|
123
|
+
return `${content.slice(0, index)}${replacement}${content.slice(index + search.length)}`;
|
|
124
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ARTIFACT_TOOL_NAMES } from "@plico-ai/core";
|
|
2
|
+
import { editFile, listFiles, readFile, writeFile } from "./artifact-model-tool-operations.js";
|
|
3
|
+
import { normalizeInputSchema } from "./schema.js";
|
|
4
|
+
const source = "plico:artifacts";
|
|
5
|
+
const maxListLimit = 1000;
|
|
6
|
+
const maxReadLimit = 1000;
|
|
7
|
+
export async function createArtifactModelTools() {
|
|
8
|
+
return Promise.all([
|
|
9
|
+
createDeclaredTool({
|
|
10
|
+
name: "list_files",
|
|
11
|
+
description: "List artifact files in the current workspace. Optionally filter paths with a glob pattern. Results are ordered by path and paginated.",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
glob: { type: "string", minLength: 1, maxLength: 1024 },
|
|
16
|
+
offset: { type: "integer", minimum: 0 },
|
|
17
|
+
limit: { type: "integer", minimum: 1, maximum: maxListLimit },
|
|
18
|
+
},
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
},
|
|
21
|
+
capabilities: ["artifacts:read"],
|
|
22
|
+
approval: { required: false },
|
|
23
|
+
handler: async (input, { artifacts }) => handleArtifactTool(() => listFiles(input, artifacts)),
|
|
24
|
+
}),
|
|
25
|
+
createDeclaredTool({
|
|
26
|
+
name: "read_file",
|
|
27
|
+
description: "Read a text artifact from the current workspace by line range. Binary artifacts return metadata only. Each response is capped at 32 KiB.",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
path: { type: "string", minLength: 1, maxLength: 1024 },
|
|
32
|
+
offset: { type: "integer", minimum: 1 },
|
|
33
|
+
limit: { type: "integer", minimum: 1, maximum: maxReadLimit },
|
|
34
|
+
},
|
|
35
|
+
required: ["path"],
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
},
|
|
38
|
+
capabilities: ["artifacts:read"],
|
|
39
|
+
approval: { required: false },
|
|
40
|
+
handler: async (input, { artifacts }) => handleArtifactTool(() => readFile(input, artifacts)),
|
|
41
|
+
}),
|
|
42
|
+
createDeclaredTool({
|
|
43
|
+
name: "write_file",
|
|
44
|
+
description: "Create or overwrite a text artifact in the current workspace. Writes are versioned. Content is limited to 1 MiB.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
path: { type: "string", minLength: 1, maxLength: 1024 },
|
|
49
|
+
content: { type: "string" },
|
|
50
|
+
mimeType: { type: "string", minLength: 1 },
|
|
51
|
+
},
|
|
52
|
+
required: ["path", "content"],
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
},
|
|
55
|
+
capabilities: ["artifacts:write"],
|
|
56
|
+
approval: { required: false },
|
|
57
|
+
handler: async (input, { artifacts }) => handleArtifactTool(() => writeFile(input, artifacts)),
|
|
58
|
+
}),
|
|
59
|
+
createDeclaredTool({
|
|
60
|
+
name: "edit_file",
|
|
61
|
+
description: "Replace exact text in a text artifact. The old text must match once unless replaceAll is true. Writes are versioned.",
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
path: { type: "string", minLength: 1, maxLength: 1024 },
|
|
66
|
+
old: { type: "string", minLength: 1 },
|
|
67
|
+
new: { type: "string" },
|
|
68
|
+
replaceAll: { type: "boolean" },
|
|
69
|
+
},
|
|
70
|
+
required: ["path", "old", "new"],
|
|
71
|
+
additionalProperties: false,
|
|
72
|
+
},
|
|
73
|
+
capabilities: ["artifacts:write"],
|
|
74
|
+
approval: { required: false },
|
|
75
|
+
handler: async (input, { artifacts }) => handleArtifactTool(() => editFile(input, artifacts)),
|
|
76
|
+
}),
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
async function handleArtifactTool(operation) {
|
|
80
|
+
try {
|
|
81
|
+
return await operation();
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function createDeclaredTool(definition) {
|
|
88
|
+
if (!ARTIFACT_TOOL_NAMES.includes(definition.name))
|
|
89
|
+
throw new Error(`Unknown built-in artifact tool: ${definition.name}`);
|
|
90
|
+
const schema = await normalizeInputSchema(definition.inputSchema, source, source);
|
|
91
|
+
return {
|
|
92
|
+
...definition,
|
|
93
|
+
source,
|
|
94
|
+
inputSchema: schema.jsonSchema,
|
|
95
|
+
validateInput: async (input) => {
|
|
96
|
+
const result = await schema.parse(input);
|
|
97
|
+
return result.success ? undefined : result.error;
|
|
98
|
+
},
|
|
99
|
+
parseInput: schema.parse,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type LoadedProject } from "@plico-ai/core";
|
|
2
|
+
import type { DeclaredTool } from "../types/index.js";
|
|
3
|
+
export declare function discoverToolFiles(project: LoadedProject): Promise<string[]>;
|
|
4
|
+
export declare function discoverTools(project: LoadedProject): Promise<DeclaredTool[]>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { ARTIFACT_TOOL_NAMES, getProjectPaths, getProjectRelativePaths, SKILL_TOOL_NAME, } from "@plico-ai/core";
|
|
4
|
+
import { loadToolFile } from "./load.js";
|
|
5
|
+
export async function discoverToolFiles(project) {
|
|
6
|
+
const projectPaths = getProjectPaths(project.root);
|
|
7
|
+
const entries = (await readdir(projectPaths.directories.tools, { withFileTypes: true }))
|
|
8
|
+
.filter((entry) => !entry.isDirectory() && entry.name.endsWith(".tool.ts"))
|
|
9
|
+
.map((entry) => entry.name)
|
|
10
|
+
.sort((left, right) => left.localeCompare(right));
|
|
11
|
+
return entries;
|
|
12
|
+
}
|
|
13
|
+
export async function discoverTools(project) {
|
|
14
|
+
const relativePaths = getProjectRelativePaths();
|
|
15
|
+
const projectPaths = getProjectPaths(project.root);
|
|
16
|
+
const entries = await discoverToolFiles(project);
|
|
17
|
+
const tools = [];
|
|
18
|
+
const sources = new Map();
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const source = `${relativePaths.directories.tools}/${entry}`;
|
|
21
|
+
const tool = await loadToolFile(join(projectPaths.directories.tools, entry), source);
|
|
22
|
+
if (ARTIFACT_TOOL_NAMES.includes(tool.name))
|
|
23
|
+
throw new Error(`Tool name "${tool.name}" is reserved by Plico's artifact tools`);
|
|
24
|
+
if (tool.name === SKILL_TOOL_NAME)
|
|
25
|
+
throw new Error(`Tool name "${tool.name}" is reserved by Plico's skill loader`);
|
|
26
|
+
const first = sources.get(tool.name);
|
|
27
|
+
if (first) {
|
|
28
|
+
throw new Error(`Duplicate tool name "${tool.name}" declared in ${first} and ${source}`);
|
|
29
|
+
}
|
|
30
|
+
sources.set(tool.name, source);
|
|
31
|
+
tools.push(tool);
|
|
32
|
+
}
|
|
33
|
+
return tools;
|
|
34
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, extname, isAbsolute, join, parse, relative } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { installAuthoringFallback } from "@plico-ai/core";
|
|
6
|
+
import { register as registerCjs, require as tsxRequire } from "tsx/cjs/api";
|
|
7
|
+
import { register as registerEsm } from "tsx/esm/api";
|
|
8
|
+
import { isRecord } from "../guards.js";
|
|
9
|
+
import { normalizeDeclaredTool } from "./normalize.js";
|
|
10
|
+
let cjsRegistered = false;
|
|
11
|
+
let esmRegistered = false;
|
|
12
|
+
let loadSequence = 0;
|
|
13
|
+
export async function loadToolFile(toolPath, source) {
|
|
14
|
+
installAuthoringFallback();
|
|
15
|
+
const module = (await hasModulePackageScope(toolPath))
|
|
16
|
+
? await importToolModule(toolPath)
|
|
17
|
+
: importCommonJsTool(toolPath);
|
|
18
|
+
const exportedTool = readDefaultExport(module, source);
|
|
19
|
+
return normalizeDeclaredTool(exportedTool, source, toolPath);
|
|
20
|
+
}
|
|
21
|
+
async function importToolModule(toolPath) {
|
|
22
|
+
if (!esmRegistered) {
|
|
23
|
+
registerEsm();
|
|
24
|
+
esmRegistered = true;
|
|
25
|
+
}
|
|
26
|
+
const url = new URL(pathToFileURL(toolPath).href);
|
|
27
|
+
url.searchParams.set("plicoLoad", String(++loadSequence));
|
|
28
|
+
return import(url.href);
|
|
29
|
+
}
|
|
30
|
+
function importCommonJsTool(toolPath) {
|
|
31
|
+
if (!cjsRegistered) {
|
|
32
|
+
registerCjs();
|
|
33
|
+
cjsRegistered = true;
|
|
34
|
+
}
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
clearProjectToolCache(toolPath);
|
|
37
|
+
return tsxRequire(require.resolve(toolPath), import.meta.url);
|
|
38
|
+
}
|
|
39
|
+
function clearProjectToolCache(toolPath) {
|
|
40
|
+
const toolsDirectory = findToolsDirectory(dirname(toolPath));
|
|
41
|
+
for (const cachedPath of Object.keys(tsxRequire.cache)) {
|
|
42
|
+
const sourcePath = cachedPath.split("?", 1)[0];
|
|
43
|
+
if (!sourcePath)
|
|
44
|
+
continue;
|
|
45
|
+
if (extname(sourcePath) !== ".ts")
|
|
46
|
+
continue;
|
|
47
|
+
const pathFromTools = relative(toolsDirectory, sourcePath);
|
|
48
|
+
if (pathFromTools === "" || (!pathFromTools.startsWith("..") && !isAbsolute(pathFromTools))) {
|
|
49
|
+
delete tsxRequire.cache[cachedPath];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function findToolsDirectory(directory) {
|
|
54
|
+
if (directory === parse(directory).root)
|
|
55
|
+
return directory;
|
|
56
|
+
if (directory.split(/[\\/]/).at(-1) === "tools")
|
|
57
|
+
return directory;
|
|
58
|
+
return findToolsDirectory(dirname(directory));
|
|
59
|
+
}
|
|
60
|
+
async function hasModulePackageScope(filePath) {
|
|
61
|
+
let directory = dirname(filePath);
|
|
62
|
+
const filesystemRoot = parse(directory).root;
|
|
63
|
+
while (true) {
|
|
64
|
+
try {
|
|
65
|
+
const packageJson = JSON.parse(await readFile(join(directory, "package.json"), "utf8"));
|
|
66
|
+
return packageJson.type === "module";
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "ENOENT") {
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (directory === filesystemRoot)
|
|
74
|
+
return false;
|
|
75
|
+
directory = dirname(directory);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function readDefaultExport(toolModule, source) {
|
|
79
|
+
if (!isRecord(toolModule) || !Object.hasOwn(toolModule, "default")) {
|
|
80
|
+
throw new Error(`${source} must default export a declared tool`);
|
|
81
|
+
}
|
|
82
|
+
return toolModule.default;
|
|
83
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isRecord } from "../guards.js";
|
|
2
|
+
import { normalizeInputSchema } from "./schema.js";
|
|
3
|
+
export async function normalizeDeclaredTool(value, source, toolPath) {
|
|
4
|
+
if (!isRecord(value)) {
|
|
5
|
+
throw new Error(`${source} must default export a declared tool object`);
|
|
6
|
+
}
|
|
7
|
+
const { name, description, inputSchema, capabilities, approval, handler } = value;
|
|
8
|
+
if (typeof name !== "string") {
|
|
9
|
+
throw new Error(`${source} tool name must be a string`);
|
|
10
|
+
}
|
|
11
|
+
if (!/^[a-z][a-z0-9_]*$/.test(name)) {
|
|
12
|
+
throw new Error(`${source} tool name must match /^[a-z][a-z0-9_]*$/`);
|
|
13
|
+
}
|
|
14
|
+
if (typeof description !== "string") {
|
|
15
|
+
throw new Error(`${source} tool description must be a string`);
|
|
16
|
+
}
|
|
17
|
+
const normalizedInputSchema = await normalizeInputSchema(inputSchema, source, toolPath);
|
|
18
|
+
if (!Array.isArray(capabilities) ||
|
|
19
|
+
!capabilities.every((capability) => typeof capability === "string")) {
|
|
20
|
+
throw new Error(`${source} capabilities must be an array of strings`);
|
|
21
|
+
}
|
|
22
|
+
if (!isRecord(approval) || typeof approval.required !== "boolean") {
|
|
23
|
+
throw new Error(`${source} approval must declare a required boolean`);
|
|
24
|
+
}
|
|
25
|
+
if (!isToolHandler(handler)) {
|
|
26
|
+
throw new Error(`${source} handler must be a function`);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
source,
|
|
30
|
+
name,
|
|
31
|
+
description,
|
|
32
|
+
inputSchema: normalizedInputSchema.jsonSchema,
|
|
33
|
+
async validateInput(input) {
|
|
34
|
+
const result = await normalizedInputSchema.parse(input);
|
|
35
|
+
return result.success ? undefined : result.error;
|
|
36
|
+
},
|
|
37
|
+
parseInput: normalizedInputSchema.parse,
|
|
38
|
+
capabilities,
|
|
39
|
+
approval: {
|
|
40
|
+
required: approval.required,
|
|
41
|
+
...(typeof approval.reason === "string" ? { reason: approval.reason } : {}),
|
|
42
|
+
},
|
|
43
|
+
handler,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function isToolHandler(value) {
|
|
47
|
+
return typeof value === "function";
|
|
48
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JsonSchema, ToolInputParseResult } from "../types/index.js";
|
|
2
|
+
export interface NormalizedInputSchema {
|
|
3
|
+
jsonSchema: JsonSchema;
|
|
4
|
+
parse(value: unknown): Promise<ToolInputParseResult>;
|
|
5
|
+
}
|
|
6
|
+
export declare function normalizeInputSchema(value: unknown, source: string, toolPath: string): Promise<NormalizedInputSchema>;
|
|
7
|
+
export declare function isJsonSchema(value: unknown): value is JsonSchema;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { Ajv } from "ajv";
|
|
4
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
5
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
6
|
+
const addFormats = createRequire(import.meta.url)("ajv-formats");
|
|
7
|
+
addFormats(ajv);
|
|
8
|
+
export async function normalizeInputSchema(value, source, toolPath) {
|
|
9
|
+
if (isZodSchema(value)) {
|
|
10
|
+
return {
|
|
11
|
+
jsonSchema: await convertZodSchema(value, source, toolPath),
|
|
12
|
+
async parse(input) {
|
|
13
|
+
const result = await value.safeParseAsync(input);
|
|
14
|
+
if (result.success)
|
|
15
|
+
return { success: true, value: result.data };
|
|
16
|
+
return {
|
|
17
|
+
success: false,
|
|
18
|
+
error: result.error.issues
|
|
19
|
+
.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`)
|
|
20
|
+
.join("; "),
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (!isJsonSchema(value)) {
|
|
26
|
+
throw new Error(`${source} inputSchema must be a Zod schema or JSON Schema`);
|
|
27
|
+
}
|
|
28
|
+
let validator;
|
|
29
|
+
try {
|
|
30
|
+
validator = ajv.compile(value);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
throw new Error(`${source} inputSchema is not a valid JSON Schema: ${message}`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
jsonSchema: value,
|
|
38
|
+
async parse(input) {
|
|
39
|
+
const error = validateJsonSchemaWithAjv(input, validator);
|
|
40
|
+
return error ? { success: false, error } : { success: true, value: input };
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function isJsonSchema(value) {
|
|
45
|
+
return typeof value === "boolean" || isJsonObject(value);
|
|
46
|
+
}
|
|
47
|
+
function validateJsonSchemaWithAjv(value, validator) {
|
|
48
|
+
if (validator(value))
|
|
49
|
+
return undefined;
|
|
50
|
+
const issues = validator.errors?.map(formatAjvIssue) ?? [];
|
|
51
|
+
return issues.length === 0 ? "input does not match the schema" : issues.join("; ");
|
|
52
|
+
}
|
|
53
|
+
function formatAjvIssue(issue) {
|
|
54
|
+
const path = jsonPointerPath(issue.instancePath);
|
|
55
|
+
if (issue.keyword === "required") {
|
|
56
|
+
const property = issue.params.missingProperty;
|
|
57
|
+
return `${location(path ? `${path}.${property}` : property)}missing required property ${property}`;
|
|
58
|
+
}
|
|
59
|
+
if (issue.keyword === "additionalProperties") {
|
|
60
|
+
const property = issue.params.additionalProperty;
|
|
61
|
+
return `${location(path ? `${path}.${property}` : property)}unknown property ${property}`;
|
|
62
|
+
}
|
|
63
|
+
if (issue.keyword === "type") {
|
|
64
|
+
return `${location(path)}expected ${issue.params.type}`;
|
|
65
|
+
}
|
|
66
|
+
return `${location(path)}${issue.message ?? "invalid"}`;
|
|
67
|
+
}
|
|
68
|
+
function location(path) {
|
|
69
|
+
return path ? `${path}: ` : "";
|
|
70
|
+
}
|
|
71
|
+
function jsonPointerPath(instancePath) {
|
|
72
|
+
return instancePath
|
|
73
|
+
.split("/")
|
|
74
|
+
.slice(1)
|
|
75
|
+
.map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"))
|
|
76
|
+
.join(".");
|
|
77
|
+
}
|
|
78
|
+
async function convertZodSchema(schema, source, toolPath) {
|
|
79
|
+
if (!isJsonObject(schema) || !("_zod" in schema)) {
|
|
80
|
+
return zodToJsonSchema(schema);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const requireFromTool = createRequire(toolPath);
|
|
84
|
+
const zodV4ModulePath = requireFromTool.resolve("zod/v4");
|
|
85
|
+
const zodV4 = (await import(pathToFileURL(zodV4ModulePath).href));
|
|
86
|
+
if (typeof zodV4.toJSONSchema !== "function") {
|
|
87
|
+
throw new Error("The installed Zod v4 package does not export toJSONSchema");
|
|
88
|
+
}
|
|
89
|
+
return zodV4.toJSONSchema(schema, { target: "draft-7" });
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
+
throw new Error(`${source} could not convert its Zod 4 schema to JSON Schema: ${message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function isJsonObject(value) {
|
|
97
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
98
|
+
}
|
|
99
|
+
function isZodSchema(value) {
|
|
100
|
+
return isJsonObject(value) && typeof value.safeParseAsync === "function";
|
|
101
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Skill } from "@plico-ai/core";
|
|
2
|
+
import type { DeclaredTool } from "../types/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* The tool that returns a skill's full text. It can only return a skill that `skills/` declared when
|
|
5
|
+
* the run started, so a name never reaches outside that folder.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createSkillModelTool(skills: Skill[]): Promise<DeclaredTool>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { SKILL_TOOL_NAME } from "@plico-ai/core";
|
|
2
|
+
import { normalizeInputSchema } from "./schema.js";
|
|
3
|
+
const source = "plico:skills";
|
|
4
|
+
/**
|
|
5
|
+
* The tool that returns a skill's full text. It can only return a skill that `skills/` declared when
|
|
6
|
+
* the run started, so a name never reaches outside that folder.
|
|
7
|
+
*/
|
|
8
|
+
export async function createSkillModelTool(skills) {
|
|
9
|
+
const byName = new Map(skills.map((skill) => [skill.name, skill]));
|
|
10
|
+
const schema = await normalizeInputSchema({
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: { name: { type: "string", minLength: 1, maxLength: 256 } },
|
|
13
|
+
required: ["name"],
|
|
14
|
+
additionalProperties: false,
|
|
15
|
+
}, source, source);
|
|
16
|
+
return {
|
|
17
|
+
name: SKILL_TOOL_NAME,
|
|
18
|
+
description: "Load the full instructions of one skill from the Skills list in your instructions, by name.",
|
|
19
|
+
capabilities: [],
|
|
20
|
+
approval: { required: false },
|
|
21
|
+
source,
|
|
22
|
+
inputSchema: schema.jsonSchema,
|
|
23
|
+
validateInput: async (input) => {
|
|
24
|
+
const result = await schema.parse(input);
|
|
25
|
+
return result.success ? undefined : result.error;
|
|
26
|
+
},
|
|
27
|
+
parseInput: schema.parse,
|
|
28
|
+
handler: async (input) => {
|
|
29
|
+
const name = input.name;
|
|
30
|
+
const skill = byName.get(name);
|
|
31
|
+
if (!skill)
|
|
32
|
+
return {
|
|
33
|
+
error: `Unknown skill: ${name}. Available skills: ${[...byName.keys()].join(", ")}.`,
|
|
34
|
+
};
|
|
35
|
+
return { name: skill.name, content: skill.body };
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RunStatus } from "@plico-ai/contracts";
|
|
2
|
+
import type { DryRunScriptStep } from "./script.js";
|
|
3
|
+
import type { EventStore } from "./store.js";
|
|
4
|
+
export interface EvalExpectation {
|
|
5
|
+
status?: RunStatus;
|
|
6
|
+
outputContains?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface EvalCase {
|
|
9
|
+
name: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
script: DryRunScriptStep[];
|
|
12
|
+
expect?: EvalExpectation;
|
|
13
|
+
}
|
|
14
|
+
export interface RunEvalsOptions {
|
|
15
|
+
eventStore?: EventStore;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ArtifactContent, ArtifactFile, ArtifactFileMetadata, ArtifactSnapshot, ArtifactSnapshotDetail, ArtifactVersion, ArtifactWorkspace, ComposedInstructions, DiagnosticSeverity, EvalCaseResult, EvalReport, EvalSummary, PlicoEvent, PlicoEventType, ReadinessDiagnostic, ReadinessReport, RunResult, RunStatus, RunSummary } from "@plico-ai/contracts";
|
|
2
|
+
export type { ArtifactFileInput } from "./artifacts.js";
|
|
3
|
+
export type { EvalCase, EvalExpectation, RunEvalsOptions } from "./evals.js";
|
|
4
|
+
export type { ModelChunk, ModelProvider, ModelRequest, ModelResponse, ModelToolCall, ProviderMessage, } from "./provider.js";
|
|
5
|
+
export type { ReadinessOptions } from "./readiness.js";
|
|
6
|
+
export type { RunProjectOptions } from "./run.js";
|
|
7
|
+
export type { DryRunScriptStep } from "./script.js";
|
|
8
|
+
export type { CreateSqliteEventStoreOptions, EventStore } from "./store.js";
|
|
9
|
+
export type { DeclaredTool, JsonSchema, ToolApprovalPolicy, ToolArtifactContext, ToolArtifactFile, ToolDefinition, ToolHandlerContext, ToolInputParseResult, ToolInputSchema, ZodInputParseResult, ZodInputSchema, } from "./tools.js";
|
|
10
|
+
export type { ArtifactContent, ArtifactFile, ArtifactFileMetadata, ArtifactSnapshot, ArtifactSnapshotDetail, ArtifactVersion, ArtifactWorkspace, ComposedInstructions, DiagnosticSeverity, EvalCaseResult, EvalReport, EvalSummary, PlicoEvent, PlicoEventType, ReadinessDiagnostic, ReadinessReport, RunResult, RunStatus, RunSummary, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { JsonValue, ModelToolCall } from "@plico-ai/contracts";
|
|
2
|
+
import type { JsonSchema } from "./tools.js";
|
|
3
|
+
export interface ProviderMessage {
|
|
4
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
5
|
+
content: string;
|
|
6
|
+
toolCallId?: string;
|
|
7
|
+
toolName?: string;
|
|
8
|
+
toolCalls?: ModelToolCall[];
|
|
9
|
+
providerMetadata?: JsonValue;
|
|
10
|
+
}
|
|
11
|
+
export type { ModelToolCall } from "@plico-ai/contracts";
|
|
12
|
+
export interface ModelRequest {
|
|
13
|
+
messages: ProviderMessage[];
|
|
14
|
+
maxSteps: number;
|
|
15
|
+
tools?: Array<{
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: JsonSchema;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export interface ModelResponse {
|
|
22
|
+
providerMetadata?: JsonValue;
|
|
23
|
+
content: string;
|
|
24
|
+
usage: {
|
|
25
|
+
inputTokens: number;
|
|
26
|
+
outputTokens: number;
|
|
27
|
+
};
|
|
28
|
+
toolCalls?: ModelToolCall[];
|
|
29
|
+
}
|
|
30
|
+
export type ModelChunk = {
|
|
31
|
+
type: "text";
|
|
32
|
+
text: string;
|
|
33
|
+
} | {
|
|
34
|
+
type: "tool.call";
|
|
35
|
+
call: ModelToolCall;
|
|
36
|
+
} | {
|
|
37
|
+
type: "finish";
|
|
38
|
+
usage: ModelResponse["usage"];
|
|
39
|
+
providerMetadata?: JsonValue;
|
|
40
|
+
};
|
|
41
|
+
export interface ModelProvider {
|
|
42
|
+
stream(request: ModelRequest, options: {
|
|
43
|
+
signal: AbortSignal;
|
|
44
|
+
}): AsyncIterable<ModelChunk>;
|
|
45
|
+
}
|