@plico-ai/cli 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 +33 -3
- package/dist/args.d.ts +25 -0
- package/dist/args.js +58 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +50 -0
- package/dist/commands/cancel.d.ts +1 -0
- package/dist/commands/cancel.js +42 -0
- package/dist/commands/eval-promote.d.ts +2 -0
- package/dist/commands/eval-promote.js +26 -0
- package/dist/commands/eval.d.ts +1 -0
- package/dist/commands/eval.js +46 -0
- package/dist/commands/events.d.ts +1 -0
- package/dist/commands/events.js +46 -0
- package/dist/commands/interrupt.d.ts +9 -0
- package/dist/commands/interrupt.js +38 -0
- package/dist/commands/live-output.d.ts +3 -0
- package/dist/commands/live-output.js +18 -0
- package/dist/commands/model-provider.d.ts +4 -0
- package/dist/commands/model-provider.js +21 -0
- package/dist/commands/readiness.d.ts +1 -0
- package/dist/commands/readiness.js +31 -0
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +152 -0
- package/dist/commands/runs.d.ts +1 -0
- package/dist/commands/runs.js +52 -0
- package/dist/commands/serve.d.ts +1 -0
- package/dist/commands/serve.js +61 -0
- package/dist/commands/usage.d.ts +1 -0
- package/dist/commands/usage.js +18 -0
- package/dist/commands/validate.d.ts +1 -0
- package/dist/commands/validate.js +38 -0
- package/dist/direct-run.d.ts +5 -0
- package/dist/direct-run.js +16 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +6 -0
- package/dist/format/eval.d.ts +2 -0
- package/dist/format/eval.js +18 -0
- package/dist/format/events.d.ts +2 -0
- package/dist/format/events.js +7 -0
- package/dist/format/readiness.d.ts +2 -0
- package/dist/format/readiness.js +18 -0
- package/dist/format/run.d.ts +2 -0
- package/dist/format/run.js +69 -0
- package/dist/format/runs.d.ts +2 -0
- package/dist/format/runs.js +17 -0
- package/dist/format/values.d.ts +2 -0
- package/dist/format/values.js +6 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/paths.d.ts +5 -0
- package/dist/paths.js +35 -0
- package/dist/quiet-sqlite-warning.d.ts +6 -0
- package/dist/quiet-sqlite-warning.js +15 -0
- package/dist/script-file.d.ts +2 -0
- package/dist/script-file.js +13 -0
- package/dist/studio/THIRD_PARTY_NOTICES.txt +2319 -0
- package/dist/studio/assets/index-CAj4ULhw.js +40 -0
- package/dist/studio/assets/index-COrtYruy.css +1 -0
- package/dist/studio/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/studio/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/studio/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/studio/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/studio/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/studio/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/studio/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/studio/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/studio/index.html +18 -0
- package/package.json +44 -8
package/NOTICE
ADDED
package/README.md
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
# @plico-ai/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI entrypoints for validating Plico projects and running the deterministic dry runtime.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
- `plico validate [path]`
|
|
8
|
+
- `plico validate --json [path]`
|
|
9
|
+
- `plico run --dry [path]`
|
|
10
|
+
- `plico run --dry --json [path]`
|
|
11
|
+
- `plico run --dry --script <file> [path]`
|
|
12
|
+
- `plico cancel <runId> [--db <path>] [--json] [path]`
|
|
13
|
+
|
|
14
|
+
`plico validate` keeps its current behavior. `plico run` is dry-run only in this release and uses the local runtime package for deterministic instruction composition, tool discovery, scripted tool execution, and approval blocking.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Durable runs
|
|
18
|
+
|
|
19
|
+
`plico run --dry --persist` resumes eligible interrupted runs in the selected database as it starts the requested run. It waits for each recovered execution to finish or reach a wait, reports recovery results on stderr, and uses the requested run's exit code. A waiting run needs a running host or a later persistent start to continue. `plico serve` adopts eligible runs at startup as well.
|
|
20
|
+
|
|
21
|
+
Set the optional host execution lease in `plico.config.ts`:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
export default {
|
|
25
|
+
schemaVersion: 1,
|
|
26
|
+
name: "my-agent",
|
|
27
|
+
execution: { leaseMs: 30_000 },
|
|
28
|
+
} as const;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The lease defaults to 30,000 ms and must be a positive integer. The host's setting applies to every run it recovers from its database. A short lease increases the chance that a stalled step runs again; side-effecting tools should use their idempotency key.
|
|
32
|
+
|
|
33
|
+
`plico cancel <runId> [path]` requests durable cancellation of a persisted run. The
|
|
34
|
+
run's worker records the terminal cancellation event; if no worker is active, the
|
|
35
|
+
next recovery pass finalizes it. `--db <path>` selects a non-default database and
|
|
36
|
+
`--json` prints the cancellation result.
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ValueOption {
|
|
2
|
+
/** Message thrown when the option is the last argument and has no value. */
|
|
3
|
+
missing: string;
|
|
4
|
+
/** Optional converter; may throw to reject a malformed value. */
|
|
5
|
+
parse?: (value: string) => unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface ArgSpec {
|
|
8
|
+
/** Boolean flags, e.g. `--json`. */
|
|
9
|
+
flags: readonly string[];
|
|
10
|
+
/** Options that consume the following argument, keyed by option name. */
|
|
11
|
+
values: Readonly<Record<string, ValueOption>>;
|
|
12
|
+
/** When false, any other `-`-prefixed argument is treated as a positional. */
|
|
13
|
+
rejectUnknown: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ParsedArgs {
|
|
16
|
+
flags: ReadonlySet<string>;
|
|
17
|
+
values: ReadonlyMap<string, unknown>;
|
|
18
|
+
positionals: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function parseArgs(args: readonly string[], spec: ArgSpec): ParsedArgs;
|
|
21
|
+
export declare function readStringOption(parsed: ParsedArgs, name: string): string | undefined;
|
|
22
|
+
export declare function readNumberOption(parsed: ParsedArgs, name: string): number | undefined;
|
|
23
|
+
export declare function lastPositional(parsed: ParsedArgs, fallback: string): string;
|
|
24
|
+
export declare function parseCursor(value: string): number;
|
|
25
|
+
export declare function parsePort(value: string): number;
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function parseArgs(args, spec) {
|
|
2
|
+
const flags = new Set();
|
|
3
|
+
const values = new Map();
|
|
4
|
+
const positionals = [];
|
|
5
|
+
let pending;
|
|
6
|
+
for (const arg of args) {
|
|
7
|
+
if (pending !== undefined) {
|
|
8
|
+
if (spec.flags.includes(arg) || Object.hasOwn(spec.values, arg)) {
|
|
9
|
+
throw new Error(spec.values[pending]?.missing ?? `Missing value after ${pending}`);
|
|
10
|
+
}
|
|
11
|
+
const option = spec.values[pending];
|
|
12
|
+
values.set(pending, option?.parse ? option.parse(arg) : arg);
|
|
13
|
+
pending = undefined;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (spec.flags.includes(arg)) {
|
|
17
|
+
flags.add(arg);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (Object.hasOwn(spec.values, arg)) {
|
|
21
|
+
pending = arg;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (spec.rejectUnknown && arg.startsWith("-")) {
|
|
25
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
26
|
+
}
|
|
27
|
+
positionals.push(arg);
|
|
28
|
+
}
|
|
29
|
+
if (pending !== undefined) {
|
|
30
|
+
throw new Error(spec.values[pending]?.missing ?? `Missing value after ${pending}`);
|
|
31
|
+
}
|
|
32
|
+
return { flags, values, positionals };
|
|
33
|
+
}
|
|
34
|
+
export function readStringOption(parsed, name) {
|
|
35
|
+
const value = parsed.values.get(name);
|
|
36
|
+
return typeof value === "string" ? value : undefined;
|
|
37
|
+
}
|
|
38
|
+
export function readNumberOption(parsed, name) {
|
|
39
|
+
const value = parsed.values.get(name);
|
|
40
|
+
return typeof value === "number" ? value : undefined;
|
|
41
|
+
}
|
|
42
|
+
export function lastPositional(parsed, fallback) {
|
|
43
|
+
return parsed.positionals.at(-1) ?? fallback;
|
|
44
|
+
}
|
|
45
|
+
export function parseCursor(value) {
|
|
46
|
+
const parsed = Number(value);
|
|
47
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
48
|
+
throw new Error(`Malformed cursor: ${value}`);
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
export function parsePort(value) {
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
55
|
+
throw new Error(`Invalid port: ${value}`);
|
|
56
|
+
}
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { cancelCommand } from "./commands/cancel.js";
|
|
4
|
+
import { evalCommand } from "./commands/eval.js";
|
|
5
|
+
import { eventsCommand } from "./commands/events.js";
|
|
6
|
+
import { readinessCommand } from "./commands/readiness.js";
|
|
7
|
+
import { runCommand } from "./commands/run.js";
|
|
8
|
+
import { runsCommand } from "./commands/runs.js";
|
|
9
|
+
import { serveCommand } from "./commands/serve.js";
|
|
10
|
+
import { usageCommand } from "./commands/usage.js";
|
|
11
|
+
import { validateCommand } from "./commands/validate.js";
|
|
12
|
+
import { isDirectRun } from "./direct-run.js";
|
|
13
|
+
import { quietSqliteWarning } from "./quiet-sqlite-warning.js";
|
|
14
|
+
function readVersion() {
|
|
15
|
+
const pkg = createRequire(import.meta.url)("../package.json");
|
|
16
|
+
return pkg.version;
|
|
17
|
+
}
|
|
18
|
+
const commands = {
|
|
19
|
+
validate: validateCommand,
|
|
20
|
+
run: runCommand,
|
|
21
|
+
cancel: cancelCommand,
|
|
22
|
+
runs: runsCommand,
|
|
23
|
+
events: eventsCommand,
|
|
24
|
+
serve: serveCommand,
|
|
25
|
+
readiness: readinessCommand,
|
|
26
|
+
eval: evalCommand,
|
|
27
|
+
};
|
|
28
|
+
export async function main(argv) {
|
|
29
|
+
const name = argv[2] ?? "help";
|
|
30
|
+
if (name === "help" || name === "--help" || name === "-h") {
|
|
31
|
+
return usageCommand();
|
|
32
|
+
}
|
|
33
|
+
if (name === "--version" || name === "-v") {
|
|
34
|
+
console.log(readVersion());
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
const command = Object.hasOwn(commands, name) ? commands[name] : undefined;
|
|
38
|
+
if (!command) {
|
|
39
|
+
console.error(`Unknown command: ${name}`);
|
|
40
|
+
usageCommand("stderr");
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
return command(argv.slice(3));
|
|
44
|
+
}
|
|
45
|
+
if (isDirectRun(import.meta.url, process.argv[1])) {
|
|
46
|
+
quietSqliteWarning();
|
|
47
|
+
main(process.argv).then((code) => {
|
|
48
|
+
process.exitCode = code;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cancelCommand(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createSqliteEngine, createSqliteEventStore } from "@plico-ai/runtime";
|
|
2
|
+
import { parseArgs, readStringOption } from "../args.js";
|
|
3
|
+
import { errorMessage } from "../errors.js";
|
|
4
|
+
import { ensureDatabaseExists, resolveDatabasePath } from "../paths.js";
|
|
5
|
+
export async function cancelCommand(args) {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = parseArgs(args, {
|
|
8
|
+
flags: ["--json"],
|
|
9
|
+
values: { "--db": { missing: "Missing database path after --db" } },
|
|
10
|
+
rejectUnknown: true,
|
|
11
|
+
});
|
|
12
|
+
const [runId, ...rest] = parsed.positionals;
|
|
13
|
+
if (!runId)
|
|
14
|
+
throw new Error("Missing run ID for cancel command");
|
|
15
|
+
const target = rest.at(-1) ?? process.cwd();
|
|
16
|
+
const databasePath = resolveDatabasePath(target, readStringOption(parsed, "--db"));
|
|
17
|
+
await ensureDatabaseExists(databasePath);
|
|
18
|
+
const eventStore = createSqliteEventStore({ databasePath });
|
|
19
|
+
if (!(await eventStore.getRun(runId))) {
|
|
20
|
+
console.error(`Unknown run ID: ${runId}`);
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const result = await createSqliteEngine({ databasePath }).cancel(runId);
|
|
24
|
+
if (parsed.flags.has("--json")) {
|
|
25
|
+
console.log(JSON.stringify({ runId, result }));
|
|
26
|
+
}
|
|
27
|
+
else if (result === "accepted") {
|
|
28
|
+
console.log(`Cancellation requested for run ${runId}.`);
|
|
29
|
+
}
|
|
30
|
+
else if (result === "ignored") {
|
|
31
|
+
console.log(`Run ${runId} is already finished or cancelled.`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.error(`Run ${runId} is not known to the execution engine.`);
|
|
35
|
+
}
|
|
36
|
+
return result === "unknown" ? 1 : 0;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.error(errorMessage(error));
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getProjectPaths } from "@plico-ai/core";
|
|
4
|
+
import { createSqliteEventStore, evalCaseFromRun } from "@plico-ai/runtime";
|
|
5
|
+
/** Writes a stored run into the project's evals folder and returns the file it created. */
|
|
6
|
+
export async function promoteRun(target, databasePath, runId, name) {
|
|
7
|
+
if (name === "." || name === ".." || /[\\/]/.test(name))
|
|
8
|
+
throw new Error(`Invalid --name "${name}". Use a plain file name without path separators.`);
|
|
9
|
+
const store = createSqliteEventStore({ databasePath, readOnly: true });
|
|
10
|
+
const run = await store.getRun(runId);
|
|
11
|
+
if (!run)
|
|
12
|
+
throw new Error(`Run not found: ${runId}`);
|
|
13
|
+
const evalCase = evalCaseFromRun(name, run.status, await store.getEvents(runId));
|
|
14
|
+
const folder = getProjectPaths(target).directories.evals;
|
|
15
|
+
const file = join(folder, `${name}.script.json`);
|
|
16
|
+
await mkdir(folder, { recursive: true });
|
|
17
|
+
try {
|
|
18
|
+
await writeFile(file, `${JSON.stringify(evalCase, null, 2)}\n`, { flag: "wx" });
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code === "EEXIST")
|
|
22
|
+
throw new Error(`${file} already exists. Pick another --name.`);
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
return file;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function evalCommand(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createSqliteEventStore, runEvals } from "@plico-ai/runtime";
|
|
2
|
+
import { lastPositional, parseArgs, readStringOption } from "../args.js";
|
|
3
|
+
import { errorMessage } from "../errors.js";
|
|
4
|
+
import { formatEvalSummary } from "../format/eval.js";
|
|
5
|
+
import { resolveDatabasePath } from "../paths.js";
|
|
6
|
+
import { promoteRun } from "./eval-promote.js";
|
|
7
|
+
export async function evalCommand(args) {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = parseArgs(args, {
|
|
10
|
+
flags: ["--json", "--allow-empty"],
|
|
11
|
+
values: {
|
|
12
|
+
"--db": { missing: "Missing database path after --db" },
|
|
13
|
+
"--from-run": { missing: "Missing run id after --from-run" },
|
|
14
|
+
"--name": { missing: "Missing case name after --name" },
|
|
15
|
+
},
|
|
16
|
+
rejectUnknown: true,
|
|
17
|
+
});
|
|
18
|
+
const target = lastPositional(parsed, process.cwd());
|
|
19
|
+
const databasePath = resolveDatabasePath(target, readStringOption(parsed, "--db"));
|
|
20
|
+
const fromRun = readStringOption(parsed, "--from-run");
|
|
21
|
+
if (fromRun !== undefined) {
|
|
22
|
+
const name = readStringOption(parsed, "--name") ?? fromRun;
|
|
23
|
+
console.log(`Wrote ${await promoteRun(target, databasePath, fromRun, name)}`);
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
const eventStore = createSqliteEventStore({ databasePath });
|
|
27
|
+
const summary = await runEvals(target, { eventStore });
|
|
28
|
+
if (parsed.flags.has("--json")) {
|
|
29
|
+
console.log(JSON.stringify(summary));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
for (const line of formatEvalSummary(target, summary)) {
|
|
33
|
+
console.log(line);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (summary.total === 0 && !parsed.flags.has("--allow-empty")) {
|
|
37
|
+
console.error("No eval cases found, so nothing was tested. Add cases under evals/ or pass --allow-empty.");
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
return summary.failed > 0 ? 1 : 0;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error(errorMessage(error));
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function eventsCommand(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createSqliteEventStore } from "@plico-ai/runtime";
|
|
2
|
+
import { parseArgs, parseCursor, readNumberOption, readStringOption } from "../args.js";
|
|
3
|
+
import { errorMessage } from "../errors.js";
|
|
4
|
+
import { formatEvents } from "../format/events.js";
|
|
5
|
+
import { ensureDatabaseExists, resolveDatabasePath } from "../paths.js";
|
|
6
|
+
export async function eventsCommand(args) {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = parseArgs(args, {
|
|
9
|
+
flags: ["--json"],
|
|
10
|
+
values: {
|
|
11
|
+
"--after": { missing: "Missing sequence after --after", parse: parseCursor },
|
|
12
|
+
"--db": { missing: "Missing database path after --db" },
|
|
13
|
+
},
|
|
14
|
+
rejectUnknown: true,
|
|
15
|
+
});
|
|
16
|
+
const [runId, ...rest] = parsed.positionals;
|
|
17
|
+
if (!runId) {
|
|
18
|
+
throw new Error("Missing run ID for events command");
|
|
19
|
+
}
|
|
20
|
+
const json = parsed.flags.has("--json");
|
|
21
|
+
const target = rest.at(-1) ?? process.cwd();
|
|
22
|
+
const afterSequence = readNumberOption(parsed, "--after");
|
|
23
|
+
const databasePath = resolveDatabasePath(target, readStringOption(parsed, "--db"));
|
|
24
|
+
await ensureDatabaseExists(databasePath);
|
|
25
|
+
const eventStore = createSqliteEventStore({ databasePath });
|
|
26
|
+
const run = await eventStore.getRun(runId);
|
|
27
|
+
if (!run) {
|
|
28
|
+
console.error(`Unknown run ID: ${runId}`);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
const events = await eventStore.getEvents(runId, afterSequence === undefined ? {} : { afterSequence });
|
|
32
|
+
if (json) {
|
|
33
|
+
console.log(JSON.stringify(events));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
for (const line of formatEvents(runId, events)) {
|
|
37
|
+
console.log(line);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error(errorMessage(error));
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Engine } from "@plico-ai/runtime";
|
|
2
|
+
/** Keep track of the requested and recovered runs until all execution settles. */
|
|
3
|
+
export declare function cancelOnInterrupt(engine: Engine, runId: string): {
|
|
4
|
+
onStarted(): void;
|
|
5
|
+
onRecoveryStarted(id: string): void;
|
|
6
|
+
onRecoverySettled(id: string): void;
|
|
7
|
+
onCompleted(): void;
|
|
8
|
+
dispose(): void;
|
|
9
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Keep track of the requested and recovered runs until all execution settles. */
|
|
2
|
+
export function cancelOnInterrupt(engine, runId) {
|
|
3
|
+
const active = new Set();
|
|
4
|
+
let interrupted = false;
|
|
5
|
+
const cancel = (id) => {
|
|
6
|
+
void engine.cancel(id).catch(() => console.error("Could not cancel the run."));
|
|
7
|
+
};
|
|
8
|
+
const track = (id) => {
|
|
9
|
+
active.add(id);
|
|
10
|
+
if (interrupted)
|
|
11
|
+
cancel(id);
|
|
12
|
+
};
|
|
13
|
+
const onSignal = () => {
|
|
14
|
+
interrupted = true;
|
|
15
|
+
for (const id of active)
|
|
16
|
+
cancel(id);
|
|
17
|
+
};
|
|
18
|
+
process.on("SIGINT", onSignal);
|
|
19
|
+
process.on("SIGTERM", onSignal);
|
|
20
|
+
return {
|
|
21
|
+
onStarted() {
|
|
22
|
+
track(runId);
|
|
23
|
+
},
|
|
24
|
+
onRecoveryStarted(id) {
|
|
25
|
+
track(id);
|
|
26
|
+
},
|
|
27
|
+
onRecoverySettled(id) {
|
|
28
|
+
active.delete(id);
|
|
29
|
+
},
|
|
30
|
+
onCompleted() {
|
|
31
|
+
active.delete(runId);
|
|
32
|
+
},
|
|
33
|
+
dispose() {
|
|
34
|
+
process.off("SIGINT", onSignal);
|
|
35
|
+
process.off("SIGTERM", onSignal);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Forward committed model text as it arrives, separating attempts on retries. */
|
|
2
|
+
export function streamModelOutput(store) {
|
|
3
|
+
const attempts = new Set();
|
|
4
|
+
return {
|
|
5
|
+
...store,
|
|
6
|
+
async appendEvent(event) {
|
|
7
|
+
await store.appendEvent(event);
|
|
8
|
+
if (event.type === "model.attempt") {
|
|
9
|
+
if (attempts.has(event.payload.turn))
|
|
10
|
+
process.stdout.write("\n[Retrying model response]\n");
|
|
11
|
+
attempts.add(event.payload.turn);
|
|
12
|
+
}
|
|
13
|
+
else if (event.type === "model.delta") {
|
|
14
|
+
process.stdout.write(event.payload.text);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProjectConfig } from "@plico-ai/core";
|
|
2
|
+
import { type ModelProvider } from "@plico-ai/runtime";
|
|
3
|
+
export declare function resolveModelProvider(model: ProjectConfig["model"], projectRoot: string): ModelProvider;
|
|
4
|
+
export declare function resolveMessage(message: string | undefined): Promise<string>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createConfiguredModelProvider } from "@plico-ai/runtime";
|
|
2
|
+
export function resolveModelProvider(model, projectRoot) {
|
|
3
|
+
if (!model)
|
|
4
|
+
throw new Error("Model configuration is missing from plico.config.ts.");
|
|
5
|
+
const provider = createConfiguredModelProvider(model, projectRoot);
|
|
6
|
+
if (!provider)
|
|
7
|
+
throw new Error(`Unsupported model provider: ${model.provider}`);
|
|
8
|
+
return provider;
|
|
9
|
+
}
|
|
10
|
+
export async function resolveMessage(message) {
|
|
11
|
+
if (message?.trim())
|
|
12
|
+
return message;
|
|
13
|
+
if (!process.stdin.isTTY) {
|
|
14
|
+
let input = "";
|
|
15
|
+
for await (const chunk of process.stdin)
|
|
16
|
+
input += String(chunk);
|
|
17
|
+
if (input.trim())
|
|
18
|
+
return input;
|
|
19
|
+
}
|
|
20
|
+
throw new Error("A user message is required. Provide --message or pipe input via stdin.");
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readinessCommand(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { checkReadiness, loadProjectEnv } from "@plico-ai/runtime";
|
|
2
|
+
import { lastPositional, parseArgs, readStringOption } from "../args.js";
|
|
3
|
+
import { errorMessage } from "../errors.js";
|
|
4
|
+
import { formatReadinessReport } from "../format/readiness.js";
|
|
5
|
+
import { findStudioPath, resolveDatabasePath } from "../paths.js";
|
|
6
|
+
export async function readinessCommand(args) {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = parseArgs(args, {
|
|
9
|
+
flags: ["--json"],
|
|
10
|
+
values: { "--db": { missing: "Missing database path after --db" } },
|
|
11
|
+
rejectUnknown: true,
|
|
12
|
+
});
|
|
13
|
+
const target = lastPositional(parsed, process.cwd());
|
|
14
|
+
loadProjectEnv(target);
|
|
15
|
+
const databasePath = resolveDatabasePath(target, readStringOption(parsed, "--db"));
|
|
16
|
+
const report = await checkReadiness(target, { databasePath, studioPath: findStudioPath() });
|
|
17
|
+
if (parsed.flags.has("--json")) {
|
|
18
|
+
console.log(JSON.stringify(report));
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
for (const line of formatReadinessReport(report)) {
|
|
22
|
+
console.log(line);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return report.summary.error > 0 ? 1 : 0;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error(errorMessage(error));
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCommand(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { ARTIFACT_TOOL_NAMES, SKILL_TOOL_NAME, validateProject } from "@plico-ai/core";
|
|
3
|
+
import { createSqliteEngine, createSqliteEventStore, loadProjectEnv, modelProviderRecoveryKey, recoverRuns, runProject, } from "@plico-ai/runtime";
|
|
4
|
+
import { lastPositional, parseArgs, readStringOption } from "../args.js";
|
|
5
|
+
import { errorMessage } from "../errors.js";
|
|
6
|
+
import { formatRunResult } from "../format/run.js";
|
|
7
|
+
import { resolveDatabasePath } from "../paths.js";
|
|
8
|
+
import { loadDryRunScript } from "../script-file.js";
|
|
9
|
+
import { cancelOnInterrupt } from "./interrupt.js";
|
|
10
|
+
import { streamModelOutput } from "./live-output.js";
|
|
11
|
+
import { resolveMessage, resolveModelProvider } from "./model-provider.js";
|
|
12
|
+
export async function runCommand(args) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = parseArgs(args, {
|
|
15
|
+
flags: ["--dry", "--json", "--persist"],
|
|
16
|
+
values: {
|
|
17
|
+
"--script": { missing: "Missing script file after --script" },
|
|
18
|
+
"--message": { missing: "Missing message text after --message" },
|
|
19
|
+
"--db": { missing: "Missing database path after --db" },
|
|
20
|
+
},
|
|
21
|
+
rejectUnknown: true,
|
|
22
|
+
});
|
|
23
|
+
const dry = parsed.flags.has("--dry");
|
|
24
|
+
const json = parsed.flags.has("--json");
|
|
25
|
+
const persist = parsed.flags.has("--persist") || !dry;
|
|
26
|
+
const target = lastPositional(parsed, process.cwd());
|
|
27
|
+
loadProjectEnv(target);
|
|
28
|
+
const scriptPath = readStringOption(parsed, "--script");
|
|
29
|
+
const dbPath = readStringOption(parsed, "--db");
|
|
30
|
+
let message = readStringOption(parsed, "--message");
|
|
31
|
+
if (!dry && scriptPath)
|
|
32
|
+
throw new Error("--script requires --dry.");
|
|
33
|
+
const script = scriptPath ? await loadDryRunScript(scriptPath) : undefined;
|
|
34
|
+
const databasePath = persist ? resolveDatabasePath(target, dbPath) : undefined;
|
|
35
|
+
const validation = databasePath ? await validateProject(target) : undefined;
|
|
36
|
+
if (!dry && (!validation?.ok || !validation.project)) {
|
|
37
|
+
throw new Error("Project is invalid. Run plico validate for details.");
|
|
38
|
+
}
|
|
39
|
+
const provider = dry
|
|
40
|
+
? message === undefined
|
|
41
|
+
? undefined
|
|
42
|
+
: createFakeProvider()
|
|
43
|
+
: resolveModelProvider(validation?.project?.config.model, target);
|
|
44
|
+
if (!dry)
|
|
45
|
+
message = await resolveMessage(message);
|
|
46
|
+
const recoveryKey = dry
|
|
47
|
+
? "plico:dry-fake:v1"
|
|
48
|
+
: validation?.project?.config.model
|
|
49
|
+
? modelProviderRecoveryKey(validation.project.config.model)
|
|
50
|
+
: undefined;
|
|
51
|
+
const eventStore = databasePath ? createSqliteEventStore({ databasePath }) : undefined;
|
|
52
|
+
const leaseMs = validation?.project?.config.execution?.leaseMs;
|
|
53
|
+
const engine = databasePath
|
|
54
|
+
? createSqliteEngine({ databasePath, ...(leaseMs === undefined ? {} : { leaseMs }) })
|
|
55
|
+
: undefined;
|
|
56
|
+
const runId = !dry ? `run-${randomUUID()}` : undefined;
|
|
57
|
+
const interruption = engine && runId ? cancelOnInterrupt(engine, runId) : undefined;
|
|
58
|
+
const recovery = engine && eventStore
|
|
59
|
+
? recoverRuns({
|
|
60
|
+
engine,
|
|
61
|
+
eventStore,
|
|
62
|
+
recoveryFilter: {
|
|
63
|
+
projectRoot: target,
|
|
64
|
+
...(recoveryKey ? { providerRecoveryKey: recoveryKey } : {}),
|
|
65
|
+
},
|
|
66
|
+
...(dry ? { provider: createFakeProvider() } : provider ? { provider } : {}),
|
|
67
|
+
...(interruption
|
|
68
|
+
? {
|
|
69
|
+
onRecoveryStarted: interruption.onRecoveryStarted,
|
|
70
|
+
onRecoverySettled: interruption.onRecoverySettled,
|
|
71
|
+
}
|
|
72
|
+
: {}),
|
|
73
|
+
}).then((runs) => {
|
|
74
|
+
for (const run of runs)
|
|
75
|
+
console.error(`Recovered run ${run.runId}: ${run.state}${run.error ? ` (${run.error})` : ""}`);
|
|
76
|
+
}, (error) => {
|
|
77
|
+
console.error(`Recovery incomplete: ${errorMessage(error)}`);
|
|
78
|
+
})
|
|
79
|
+
: Promise.resolve();
|
|
80
|
+
const outputStore = eventStore && !dry && !json ? streamModelOutput(eventStore) : eventStore;
|
|
81
|
+
const resultPromise = runProject(target, {
|
|
82
|
+
...(script ? { script } : {}),
|
|
83
|
+
...(outputStore ? { eventStore: outputStore } : {}),
|
|
84
|
+
...(engine ? { engine } : {}),
|
|
85
|
+
...(message === undefined ? {} : { message }),
|
|
86
|
+
...(provider === undefined ? {} : { provider }),
|
|
87
|
+
...(recoveryKey === undefined || provider === undefined
|
|
88
|
+
? {}
|
|
89
|
+
: { providerRecoveryKey: recoveryKey }),
|
|
90
|
+
...(runId === undefined ? {} : { runId }),
|
|
91
|
+
...(interruption === undefined ? {} : { onStarted: interruption.onStarted }),
|
|
92
|
+
});
|
|
93
|
+
const trackedResult = resultPromise.finally(() => interruption?.onCompleted());
|
|
94
|
+
void Promise.allSettled([trackedResult, recovery]).then(() => interruption?.dispose());
|
|
95
|
+
const result = await trackedResult;
|
|
96
|
+
if (json) {
|
|
97
|
+
console.log(JSON.stringify(result));
|
|
98
|
+
}
|
|
99
|
+
else if (result.status === "failed" && result.errors?.length) {
|
|
100
|
+
for (const error of result.errors) {
|
|
101
|
+
console.error(`${error.path}: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (!dry) {
|
|
105
|
+
process.stdout.write("\n");
|
|
106
|
+
if (result.status !== "completed") {
|
|
107
|
+
for (const line of formatRunResult(target, result, false))
|
|
108
|
+
console.error(line);
|
|
109
|
+
}
|
|
110
|
+
else if (persist) {
|
|
111
|
+
console.log(`Persisted run ID: ${result.runId}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
for (const line of formatRunResult(target, result, dry)) {
|
|
116
|
+
if (result.status === "completed") {
|
|
117
|
+
console.log(line);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.error(line);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (persist && result.status === "completed") {
|
|
124
|
+
console.log(`Persisted run ID: ${result.runId}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Exit 0 for completion, 2 when waiting for approval, and 1 for anything else.
|
|
128
|
+
return result.status === "completed" ? 0 : result.status === "waiting" ? 2 : 1;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.error(errorMessage(error));
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Deterministic stand-in used by `--message` until real providers are wired in. */
|
|
136
|
+
function createFakeProvider() {
|
|
137
|
+
return {
|
|
138
|
+
async *stream(request) {
|
|
139
|
+
const lastUserMessage = [...request.messages].reverse().find((m) => m.role === "user");
|
|
140
|
+
const lastToolMessage = [...request.messages].reverse().find((m) => m.role === "tool");
|
|
141
|
+
const tool = request.tools?.find(({ name }) => name !== SKILL_TOOL_NAME &&
|
|
142
|
+
!ARTIFACT_TOOL_NAMES.includes(name));
|
|
143
|
+
if (!lastToolMessage && tool) {
|
|
144
|
+
yield { type: "tool.call", call: { name: tool.name, arguments: { title: "Test ticket" } } };
|
|
145
|
+
yield { type: "finish", usage: { inputTokens: 10, outputTokens: 1 } };
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
yield { type: "text", text: `Fake provider response to: ${lastUserMessage?.content ?? ""}` };
|
|
149
|
+
yield { type: "finish", usage: { inputTokens: 10, outputTokens: 5 } };
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runsCommand(args: string[]): Promise<number>;
|