@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.
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +11 -2
- 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 +77 -3
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
4
|
+
import { validateProject } from "@plico-ai/core";
|
|
5
|
+
import { createEngine, createMemoryJournal, RunCancelled, } from "../engine/index.js";
|
|
6
|
+
import { RunWaitingError, runtimeErrorPayload } from "../errors.js";
|
|
7
|
+
import { InMemoryEventLog } from "../events.js";
|
|
8
|
+
import { discoverTools } from "../tools/index.js";
|
|
9
|
+
import { finalizeRun } from "./finalize.js";
|
|
10
|
+
import { ignoredWhenPinned, pinInstructions } from "./pinned-instructions.js";
|
|
11
|
+
import { recordTools } from "./record-tools.js";
|
|
12
|
+
import { runDefaultDryRun } from "./strategies/default-dry.js";
|
|
13
|
+
import { runProviderBackedRun } from "./strategies/provider.js";
|
|
14
|
+
import { runScriptedDryRun } from "./strategies/scripted.js";
|
|
15
|
+
import { startRunSpan, storeSpanContext, tracedContext, } from "./tracing.js";
|
|
16
|
+
export async function runProject(root, options = {}) {
|
|
17
|
+
const engine = options.engine ?? createEngine({ journal: createMemoryJournal() });
|
|
18
|
+
const runId = options.runId ?? `run-${randomUUID()}`;
|
|
19
|
+
const span = startRunSpan(runId);
|
|
20
|
+
const input = {
|
|
21
|
+
root: resolve(root),
|
|
22
|
+
displayRoot: root,
|
|
23
|
+
startedAt: new Date().toISOString(),
|
|
24
|
+
traceContext: storeSpanContext(span),
|
|
25
|
+
...(options.message === undefined ? {} : { message: options.message }),
|
|
26
|
+
...(options.threadId === undefined ? {} : { threadId: options.threadId }),
|
|
27
|
+
...(options.metadata === undefined ? {} : { metadata: options.metadata }),
|
|
28
|
+
...(options.context === undefined ? {} : { context: options.context }),
|
|
29
|
+
...(options.history === undefined ? {} : { history: options.history }),
|
|
30
|
+
...(options.script === undefined ? {} : { script: options.script }),
|
|
31
|
+
// Always recorded for a script, so recovery can tell a run that may call handlers from one
|
|
32
|
+
// journaled before this flag existed.
|
|
33
|
+
...(options.script !== undefined || options.mockTools === true
|
|
34
|
+
? { mockTools: options.mockTools === true }
|
|
35
|
+
: {}),
|
|
36
|
+
...(options.providerRecoveryKey === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { providerRecoveryKey: options.providerRecoveryKey }),
|
|
39
|
+
};
|
|
40
|
+
let firstExecution = true;
|
|
41
|
+
const outcome = await engine
|
|
42
|
+
.run(runId, async (ctx, journaled) => {
|
|
43
|
+
const executionSpan = firstExecution
|
|
44
|
+
? span
|
|
45
|
+
: startRunSpan(ctx.runId, journaled.traceContext);
|
|
46
|
+
firstExecution = false;
|
|
47
|
+
try {
|
|
48
|
+
const result = await runLoop(ctx, journaled, options, executionSpan);
|
|
49
|
+
if (result.status === "failed")
|
|
50
|
+
executionSpan.setStatus({ code: SpanStatusCode.ERROR });
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
executionSpan.setStatus({ code: SpanStatusCode.ERROR });
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
executionSpan.end();
|
|
59
|
+
}
|
|
60
|
+
}, input)
|
|
61
|
+
.finally(() => span.end());
|
|
62
|
+
if (outcome.output === undefined)
|
|
63
|
+
throw new Error(`Run ${runId} stopped without a result.`);
|
|
64
|
+
return outcome.output;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Continues runs a dead process left running. The provider and event store come from this process;
|
|
68
|
+
* the project path, message and script come from the journal.
|
|
69
|
+
*/
|
|
70
|
+
export async function recoverRuns(options) {
|
|
71
|
+
const recovered = await options.engine.recover((_runId, input) => {
|
|
72
|
+
const filter = options.recoveryFilter;
|
|
73
|
+
if (filter &&
|
|
74
|
+
(!isAbsolute(input.root) ||
|
|
75
|
+
projectIdentity(input.root) !== projectIdentity(filter.projectRoot) ||
|
|
76
|
+
(filter.providerOnly === true && input.message === undefined) ||
|
|
77
|
+
(input.message !== undefined &&
|
|
78
|
+
(!input.providerRecoveryKey ||
|
|
79
|
+
!filter.providerRecoveryKey ||
|
|
80
|
+
input.providerRecoveryKey !== filter.providerRecoveryKey))))
|
|
81
|
+
return undefined;
|
|
82
|
+
const { provider: _provider, ...withoutProvider } = options;
|
|
83
|
+
return async (ctx, journaled) => {
|
|
84
|
+
const span = startRunSpan(ctx.runId, input.traceContext);
|
|
85
|
+
options.onRecoveryStarted?.(ctx.runId);
|
|
86
|
+
try {
|
|
87
|
+
const result = input.message === undefined
|
|
88
|
+
? isLegacyScript(input)
|
|
89
|
+
? await failRecovery(ctx, journaled, options, "recovery", legacyScriptMessage)
|
|
90
|
+
: await runLoop(ctx, journaled, withoutProvider, span)
|
|
91
|
+
: options.provider === undefined
|
|
92
|
+
? await failRecovery(ctx, journaled, options, "provider", "Cannot recover provider-backed run without a provider.")
|
|
93
|
+
: await runLoop(ctx, journaled, options, span);
|
|
94
|
+
if (result.status === "failed")
|
|
95
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
span.end();
|
|
104
|
+
options.onRecoverySettled?.(ctx.runId);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
return recovered.map(({ runId, state, error, output }) => ({
|
|
109
|
+
runId,
|
|
110
|
+
state: output && output.status !== "running" ? output.status : state,
|
|
111
|
+
...(error === undefined ? {} : { error }),
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
function projectIdentity(root) {
|
|
115
|
+
const absolute = resolve(root);
|
|
116
|
+
return process.platform === "win32" ? absolute.toLowerCase() : absolute;
|
|
117
|
+
}
|
|
118
|
+
const legacyScriptMessage = "This scripted run was started by an older Plico that did not record whether its tools may run, so it is not resumed. Start it again.";
|
|
119
|
+
/**
|
|
120
|
+
* A script journaled before runs recorded `mockTools`. It may have come from an API caller, so
|
|
121
|
+
* resuming it could run handlers that caller should never reach; it fails instead.
|
|
122
|
+
*/
|
|
123
|
+
function isLegacyScript(input) {
|
|
124
|
+
return input.script !== undefined && !("mockTools" in input);
|
|
125
|
+
}
|
|
126
|
+
async function failRecovery(ctx, input, options, phase, message) {
|
|
127
|
+
const events = await InMemoryEventLog.open(ctx.runId, options.eventStore);
|
|
128
|
+
const validation = await validateProject(input.root);
|
|
129
|
+
if (events.all().length === 0) {
|
|
130
|
+
await events.append("run.started", {
|
|
131
|
+
projectRoot: input.displayRoot ?? input.root,
|
|
132
|
+
projectName: validation.project?.config.name ?? "",
|
|
133
|
+
});
|
|
134
|
+
options.onStarted?.(ctx.runId);
|
|
135
|
+
}
|
|
136
|
+
events.continueAfterReplay();
|
|
137
|
+
await events.append("run.error", { phase, message });
|
|
138
|
+
await events.append("run.failed", { phase });
|
|
139
|
+
return finalizeRun({
|
|
140
|
+
runId: ctx.runId,
|
|
141
|
+
projectRoot: input.displayRoot ?? input.root,
|
|
142
|
+
projectName: validation.project?.config.name ?? "",
|
|
143
|
+
status: "failed",
|
|
144
|
+
startedAt: input.startedAt,
|
|
145
|
+
events: events.all(),
|
|
146
|
+
eventStore: options.eventStore,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
async function runLoop(ctx, input, options, span) {
|
|
150
|
+
const { runId } = ctx;
|
|
151
|
+
const { root, startedAt } = input;
|
|
152
|
+
const displayRoot = input.displayRoot ?? root;
|
|
153
|
+
const validation = await validateProject(root);
|
|
154
|
+
const events = await InMemoryEventLog.open(runId, options.eventStore);
|
|
155
|
+
const errors = events.all().some((event) => event.type === "instructions.composed")
|
|
156
|
+
? validation.errors.filter((issue) => !ignoredWhenPinned(issue))
|
|
157
|
+
: validation.errors;
|
|
158
|
+
const newlyStarted = events.all().length === 0;
|
|
159
|
+
await events.append("run.started", {
|
|
160
|
+
projectRoot: displayRoot,
|
|
161
|
+
projectName: validation.project?.config.name ?? "",
|
|
162
|
+
...(input.threadId === undefined ? {} : { threadId: input.threadId }),
|
|
163
|
+
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
|
|
164
|
+
...(input.context === undefined ? {} : { context: input.context }),
|
|
165
|
+
});
|
|
166
|
+
if (newlyStarted)
|
|
167
|
+
options.onStarted?.(runId);
|
|
168
|
+
if (errors.length > 0 || !validation.project) {
|
|
169
|
+
await events.append("run.error", { phase: "validation", issues: errors });
|
|
170
|
+
await events.append("run.failed", { phase: "validation" });
|
|
171
|
+
return finalizeRun({
|
|
172
|
+
runId,
|
|
173
|
+
projectRoot: displayRoot,
|
|
174
|
+
projectName: "",
|
|
175
|
+
status: "failed",
|
|
176
|
+
startedAt,
|
|
177
|
+
events: events.all(),
|
|
178
|
+
errors,
|
|
179
|
+
eventStore: options.eventStore,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const { project } = validation;
|
|
183
|
+
ctx = tracedContext(ctx, span, project.config.model);
|
|
184
|
+
const summary = {
|
|
185
|
+
runId,
|
|
186
|
+
projectRoot: displayRoot,
|
|
187
|
+
projectName: project.config.name,
|
|
188
|
+
startedAt,
|
|
189
|
+
eventStore: options.eventStore,
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
const { instructions, skills } = await pinInstructions(project, events);
|
|
193
|
+
const tools = await discoverTools(project);
|
|
194
|
+
await recordTools(project.root, tools, events);
|
|
195
|
+
const output = options.provider !== undefined
|
|
196
|
+
? await runProviderBackedRun(ctx, options.provider, input.message, instructions.content, tools, events, options.eventStore, project.config.model, project.config, input.history, skills)
|
|
197
|
+
: input.script === undefined
|
|
198
|
+
? await runDefaultDryRun(project, events)
|
|
199
|
+
: await runScriptedDryRun(ctx, input.script, tools, events, options.eventStore, project.config, input.mockTools === true);
|
|
200
|
+
await events.append("run.completed", {
|
|
201
|
+
...(output === undefined ? {} : { output }),
|
|
202
|
+
});
|
|
203
|
+
return await finalizeRun({ ...summary, status: "completed", output, events: events.all() });
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error instanceof RunWaitingError) {
|
|
207
|
+
return finalizeRun({
|
|
208
|
+
...summary,
|
|
209
|
+
status: "waiting",
|
|
210
|
+
output: error.output,
|
|
211
|
+
events: events.all(),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (error instanceof RunCancelled) {
|
|
215
|
+
await events.append("run.cancelled", {});
|
|
216
|
+
return finalizeRun({ ...summary, status: "cancelled", events: events.all() });
|
|
217
|
+
}
|
|
218
|
+
await events.append("run.error", runtimeErrorPayload(error));
|
|
219
|
+
await events.append("run.failed", { phase: "runtime" });
|
|
220
|
+
return finalizeRun({ ...summary, status: "failed", events: events.all() });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RunContext } from "../../engine/index.js";
|
|
2
|
+
import type { InMemoryEventLog } from "../../events.js";
|
|
3
|
+
export declare function approvalWaitName(requestId: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Holds the run on its approval wait. The first time there is no decision it stops the loop with
|
|
6
|
+
* RunWaitingError so the caller can report the run as waiting. After a decision it returns when the
|
|
7
|
+
* tool may run and fails the run for a denial or a timeout, with a different message for each.
|
|
8
|
+
*/
|
|
9
|
+
export declare function awaitApproval(ctx: RunContext, requestId: string, toolName: string, output: string | undefined, events: InMemoryEventLog, timeoutMs?: number): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { RunSuspended } from "../../engine/index.js";
|
|
2
|
+
import { RuntimeDiagnosticError, RunWaitingError } from "../../errors.js";
|
|
3
|
+
export function approvalWaitName(requestId) {
|
|
4
|
+
return `approval:${requestId}`;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Holds the run on its approval wait. The first time there is no decision it stops the loop with
|
|
8
|
+
* RunWaitingError so the caller can report the run as waiting. After a decision it returns when the
|
|
9
|
+
* tool may run and fails the run for a denial or a timeout, with a different message for each.
|
|
10
|
+
*/
|
|
11
|
+
export async function awaitApproval(ctx, requestId, toolName, output, events, timeoutMs) {
|
|
12
|
+
let outcome;
|
|
13
|
+
try {
|
|
14
|
+
outcome = await ctx.wait(approvalWaitName(requestId), {
|
|
15
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error instanceof RunSuspended)
|
|
20
|
+
throw new RunWaitingError(output);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
if (outcome.kind === "timeout") {
|
|
24
|
+
throw new RuntimeDiagnosticError({
|
|
25
|
+
phase: "approval",
|
|
26
|
+
toolName,
|
|
27
|
+
message: `Approval for ${toolName} timed out.`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
await events.append("approval.decided", {
|
|
31
|
+
requestId,
|
|
32
|
+
approved: outcome.value?.approved === true,
|
|
33
|
+
...(outcome.value?.context === undefined ? {} : { context: outcome.value.context }),
|
|
34
|
+
});
|
|
35
|
+
if (outcome.value?.approved !== true) {
|
|
36
|
+
throw new RuntimeDiagnosticError({
|
|
37
|
+
phase: "approval",
|
|
38
|
+
toolName,
|
|
39
|
+
message: `Approval for ${toolName} was denied.`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ProjectConfig } from "@plico-ai/contracts";
|
|
2
|
+
import { type Skill } from "@plico-ai/core";
|
|
3
|
+
import { type RunContext } from "../../engine/index.js";
|
|
4
|
+
import type { InMemoryEventLog } from "../../events.js";
|
|
5
|
+
import type { DeclaredTool, EventStore, ModelProvider } from "../../types/index.js";
|
|
6
|
+
export declare function runProviderBackedRun(ctx: RunContext, provider: ModelProvider, message: string | undefined, instructions: string, tools: DeclaredTool[], events: InMemoryEventLog, eventStore: EventStore | undefined, model: ProjectConfig["model"], config: ProjectConfig, history?: Array<Pick<import("../../types/index.js").ProviderMessage, "role" | "content">>, skills?: Skill[]): Promise<string>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
import { ARTIFACT_TOOL_NAMES } from "@plico-ai/core";
|
|
4
|
+
import { RunCancelled } from "../../engine/index.js";
|
|
5
|
+
import { ModelProviderError, RuntimeDiagnosticError } from "../../errors.js";
|
|
6
|
+
import { approvalTimeoutMs, decideApproval } from "../../policy/approval.js";
|
|
7
|
+
import { createArtifactModelTools } from "../../tools/artifact-model-tools.js";
|
|
8
|
+
import { createSkillModelTool } from "../../tools/skill-model-tool.js";
|
|
9
|
+
import { runToolStep } from "../tool-step.js";
|
|
10
|
+
import { nameToolStep } from "../tracing.js";
|
|
11
|
+
import { awaitApproval } from "./approval.js";
|
|
12
|
+
export async function runProviderBackedRun(ctx, provider, message, instructions, tools, events, eventStore, model, config, history = [], skills = []) {
|
|
13
|
+
const maxSteps = model?.maxSteps ?? 10;
|
|
14
|
+
if (!message) {
|
|
15
|
+
throw new RuntimeDiagnosticError({
|
|
16
|
+
phase: "provider",
|
|
17
|
+
message: "A user message is required when running with a provider. Provide --message or pipe input via stdin.",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
await events.append("input.received", { content: message });
|
|
21
|
+
const artifactTools = await createArtifactModelTools();
|
|
22
|
+
const skillTools = skills.length > 0 ? [await createSkillModelTool(skills)] : [];
|
|
23
|
+
const toolsByName = new Map([...tools, ...artifactTools, ...skillTools].map((tool) => [tool.name, tool]));
|
|
24
|
+
const artifactAccess = config.artifacts?.access ?? "read-write";
|
|
25
|
+
const providerTools = [...tools, ...artifactTools, ...skillTools]
|
|
26
|
+
.filter((tool) => isArtifactToolAllowed(tool.name, artifactAccess))
|
|
27
|
+
.map((tool) => ({
|
|
28
|
+
name: tool.name,
|
|
29
|
+
description: tool.description,
|
|
30
|
+
inputSchema: tool.inputSchema,
|
|
31
|
+
}));
|
|
32
|
+
const messages = [
|
|
33
|
+
{ role: "system", content: instructions },
|
|
34
|
+
...(history ?? []),
|
|
35
|
+
{ role: "user", content: message },
|
|
36
|
+
];
|
|
37
|
+
let output;
|
|
38
|
+
for (let turn = 0; turn < maxSteps; turn++) {
|
|
39
|
+
const modelRequest = {
|
|
40
|
+
messages,
|
|
41
|
+
maxSteps,
|
|
42
|
+
tools: providerTools,
|
|
43
|
+
};
|
|
44
|
+
await events.append("model.request", {
|
|
45
|
+
...(model ? { model: { provider: model.provider, name: model.name } } : {}),
|
|
46
|
+
messages: modelRequest.messages,
|
|
47
|
+
maxSteps: modelRequest.maxSteps,
|
|
48
|
+
tools: providerTools,
|
|
49
|
+
});
|
|
50
|
+
const modelResponse = await ctx.step(`model:${turn}`, async ({ signal, attempt }) => {
|
|
51
|
+
events.skipModelDeltas(turn);
|
|
52
|
+
if (attempt > 0)
|
|
53
|
+
await delay((model?.retryDelayMs ?? 100) * 2 ** (attempt - 1), undefined, { signal });
|
|
54
|
+
const attemptId = randomUUID();
|
|
55
|
+
await events.append("model.attempt", { turn, attemptId });
|
|
56
|
+
let content = "";
|
|
57
|
+
const toolCalls = [];
|
|
58
|
+
let providerMetadata;
|
|
59
|
+
let usage;
|
|
60
|
+
for await (const chunk of provider.stream(modelRequest, { signal })) {
|
|
61
|
+
if (chunk.type === "text") {
|
|
62
|
+
content += chunk.text;
|
|
63
|
+
await events.append("model.delta", { turn, attemptId, text: chunk.text });
|
|
64
|
+
}
|
|
65
|
+
else if (chunk.type === "tool.call")
|
|
66
|
+
toolCalls.push(chunk.call);
|
|
67
|
+
else {
|
|
68
|
+
usage = chunk.usage;
|
|
69
|
+
providerMetadata = chunk.providerMetadata;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (signal.aborted)
|
|
73
|
+
throw new RunCancelled();
|
|
74
|
+
if (!usage)
|
|
75
|
+
throw new Error("Model stream ended without a finish.");
|
|
76
|
+
return {
|
|
77
|
+
content,
|
|
78
|
+
usage,
|
|
79
|
+
attemptId,
|
|
80
|
+
...(providerMetadata === undefined ? {} : { providerMetadata }),
|
|
81
|
+
...(toolCalls.length ? { toolCalls } : {}),
|
|
82
|
+
};
|
|
83
|
+
}, {
|
|
84
|
+
retries: model?.retries ?? 2,
|
|
85
|
+
shouldRetry: (error) => error instanceof ModelProviderError && error.retryable,
|
|
86
|
+
});
|
|
87
|
+
events.skipModelDeltas(turn);
|
|
88
|
+
await events.append("model.response", {
|
|
89
|
+
attemptId: modelResponse.attemptId,
|
|
90
|
+
...(modelResponse.providerMetadata === undefined
|
|
91
|
+
? {}
|
|
92
|
+
: { providerMetadata: modelResponse.providerMetadata }),
|
|
93
|
+
content: modelResponse.content,
|
|
94
|
+
usage: modelResponse.usage,
|
|
95
|
+
toolCalls: modelResponse.toolCalls,
|
|
96
|
+
});
|
|
97
|
+
if (modelResponse.toolCalls === undefined || modelResponse.toolCalls.length === 0) {
|
|
98
|
+
const finalContent = modelResponse.content;
|
|
99
|
+
if (finalContent) {
|
|
100
|
+
await events.append("assistant.output", { content: finalContent });
|
|
101
|
+
output = finalContent;
|
|
102
|
+
}
|
|
103
|
+
return output ?? finalContent;
|
|
104
|
+
}
|
|
105
|
+
messages.push({
|
|
106
|
+
role: "assistant",
|
|
107
|
+
content: modelResponse.content,
|
|
108
|
+
toolCalls: modelResponse.toolCalls,
|
|
109
|
+
...(modelResponse.providerMetadata === undefined
|
|
110
|
+
? {}
|
|
111
|
+
: { providerMetadata: modelResponse.providerMetadata }),
|
|
112
|
+
});
|
|
113
|
+
for (const [callIndex, toolCall] of modelResponse.toolCalls.entries()) {
|
|
114
|
+
if (!isArtifactToolAllowed(toolCall.name, artifactAccess)) {
|
|
115
|
+
throw new RuntimeDiagnosticError({
|
|
116
|
+
phase: "tool.access",
|
|
117
|
+
toolName: toolCall.name,
|
|
118
|
+
message: `Artifact tool ${toolCall.name} is disabled by artifacts.access="${artifactAccess}".`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const tool = toolsByName.get(toolCall.name);
|
|
122
|
+
if (!tool) {
|
|
123
|
+
throw new RuntimeDiagnosticError({
|
|
124
|
+
phase: "tool.validation",
|
|
125
|
+
toolName: toolCall.name,
|
|
126
|
+
message: `Provider requested unknown tool: ${toolCall.name}`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
await events.append("tool.call", {
|
|
130
|
+
toolName: tool.name,
|
|
131
|
+
arguments: toolCall.arguments,
|
|
132
|
+
});
|
|
133
|
+
const parsedInput = await tool.parseInput(toolCall.arguments);
|
|
134
|
+
if (!parsedInput.success) {
|
|
135
|
+
throw new RuntimeDiagnosticError({
|
|
136
|
+
phase: "tool.validation",
|
|
137
|
+
toolName: tool.name,
|
|
138
|
+
message: `Tool ${tool.name} arguments invalid: ${parsedInput.error}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const approval = decideApproval(tool, config);
|
|
142
|
+
if (approval.required) {
|
|
143
|
+
const requestId = `${ctx.runId}:${turn}:${callIndex}`;
|
|
144
|
+
await events.append("approval.requested", {
|
|
145
|
+
requestId,
|
|
146
|
+
toolName: tool.name,
|
|
147
|
+
arguments: toolCall.arguments,
|
|
148
|
+
approval,
|
|
149
|
+
});
|
|
150
|
+
await events.append("run.waiting", {
|
|
151
|
+
requestId,
|
|
152
|
+
toolName: tool.name,
|
|
153
|
+
...(output === undefined ? {} : { output }),
|
|
154
|
+
});
|
|
155
|
+
await awaitApproval(ctx, requestId, tool.name, output, events, approvalTimeoutMs(config));
|
|
156
|
+
}
|
|
157
|
+
let result;
|
|
158
|
+
try {
|
|
159
|
+
const idempotencyKey = `${ctx.runId}:${turn}:${callIndex}`;
|
|
160
|
+
nameToolStep(ctx, `tool:${turn}:${callIndex}`, tool.name);
|
|
161
|
+
result = await runToolStep(ctx, events, eventStore, tool, parsedInput.value, `tool:${turn}:${callIndex}`, idempotencyKey);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (error instanceof RunCancelled)
|
|
165
|
+
throw error;
|
|
166
|
+
throw new RuntimeDiagnosticError({
|
|
167
|
+
phase: "runtime",
|
|
168
|
+
toolName: tool.name,
|
|
169
|
+
message: error instanceof Error ? error.message : String(error),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
messages.push({
|
|
173
|
+
role: "tool",
|
|
174
|
+
content: JSON.stringify(result),
|
|
175
|
+
toolName: tool.name,
|
|
176
|
+
...(toolCall.id === undefined ? {} : { toolCallId: toolCall.id }),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
throw new RuntimeDiagnosticError({
|
|
181
|
+
phase: "provider",
|
|
182
|
+
message: `Maximum steps reached (${maxSteps}).`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function isArtifactToolAllowed(toolName, access) {
|
|
186
|
+
if (!ARTIFACT_TOOL_NAMES.includes(toolName))
|
|
187
|
+
return true;
|
|
188
|
+
if (access === "off")
|
|
189
|
+
return false;
|
|
190
|
+
if (access === "read-only" && (toolName === "write_file" || toolName === "edit_file"))
|
|
191
|
+
return false;
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ProjectConfig } from "@plico-ai/contracts";
|
|
2
|
+
import { type RunContext } from "../../engine/index.js";
|
|
3
|
+
import type { InMemoryEventLog } from "../../events.js";
|
|
4
|
+
import type { DeclaredTool, DryRunScriptStep, EventStore } from "../../types/index.js";
|
|
5
|
+
export declare function runScriptedDryRun(ctx: RunContext, script: DryRunScriptStep[], tools: DeclaredTool[], events: InMemoryEventLog, eventStore: EventStore | undefined, config: ProjectConfig, mockTools?: boolean): Promise<string | undefined>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { RunCancelled } from "../../engine/index.js";
|
|
2
|
+
import { RuntimeDiagnosticError } from "../../errors.js";
|
|
3
|
+
import { approvalTimeoutMs, decideApproval } from "../../policy/approval.js";
|
|
4
|
+
import { runToolStep } from "../tool-step.js";
|
|
5
|
+
import { nameToolStep } from "../tracing.js";
|
|
6
|
+
import { awaitApproval } from "./approval.js";
|
|
7
|
+
export async function runScriptedDryRun(ctx, script, tools, events, eventStore, config, mockTools = false) {
|
|
8
|
+
const toolsByName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
9
|
+
let output;
|
|
10
|
+
let workspaceId;
|
|
11
|
+
const artifactPaths = new Set();
|
|
12
|
+
const artifactVersions = new Map();
|
|
13
|
+
for (const [index, step] of script.entries()) {
|
|
14
|
+
if (step.type === "assistant.output") {
|
|
15
|
+
output = step.content;
|
|
16
|
+
await events.append("assistant.output", { content: step.content });
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (step.type === "artifact.write") {
|
|
20
|
+
if (workspaceId === undefined) {
|
|
21
|
+
workspaceId = `${ctx.runId}:workspace`;
|
|
22
|
+
await events.append("artifact.workspace.created", { workspaceId });
|
|
23
|
+
}
|
|
24
|
+
for (const file of step.files) {
|
|
25
|
+
artifactPaths.add(file.path);
|
|
26
|
+
const version = (artifactVersions.get(file.path) ?? 0) + 1;
|
|
27
|
+
artifactVersions.set(file.path, version);
|
|
28
|
+
await events.append("artifact.file.written", {
|
|
29
|
+
path: file.path,
|
|
30
|
+
content: file.content,
|
|
31
|
+
mimeType: file.mimeType ?? "text/plain",
|
|
32
|
+
version,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const snapshotId = `${ctx.runId}:snapshot:${index}`;
|
|
36
|
+
await events.append("artifact.snapshot.created", {
|
|
37
|
+
snapshotId,
|
|
38
|
+
fileCount: artifactPaths.size,
|
|
39
|
+
});
|
|
40
|
+
output = `Artifact workspace ${workspaceId} created with ${step.files.length} files`;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const tool = toolsByName.get(step.toolName);
|
|
44
|
+
if (!tool) {
|
|
45
|
+
throw new RuntimeDiagnosticError({
|
|
46
|
+
phase: "tool.validation",
|
|
47
|
+
toolName: step.toolName,
|
|
48
|
+
message: `Unknown tool: ${step.toolName}`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
await events.append("tool.call", {
|
|
52
|
+
toolName: tool.name,
|
|
53
|
+
arguments: step.arguments,
|
|
54
|
+
});
|
|
55
|
+
const parsedInput = await tool.parseInput(step.arguments);
|
|
56
|
+
if (!parsedInput.success) {
|
|
57
|
+
throw new RuntimeDiagnosticError({
|
|
58
|
+
phase: "tool.validation",
|
|
59
|
+
toolName: tool.name,
|
|
60
|
+
message: `Tool ${tool.name} arguments invalid: ${parsedInput.error}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const approval = decideApproval(tool, config);
|
|
64
|
+
if (approval.required) {
|
|
65
|
+
const requestId = `${ctx.runId}:${index}`;
|
|
66
|
+
await events.append("approval.requested", {
|
|
67
|
+
requestId,
|
|
68
|
+
toolName: tool.name,
|
|
69
|
+
arguments: step.arguments,
|
|
70
|
+
approval,
|
|
71
|
+
});
|
|
72
|
+
await events.append("run.waiting", {
|
|
73
|
+
requestId,
|
|
74
|
+
toolName: tool.name,
|
|
75
|
+
...(output === undefined ? {} : { output }),
|
|
76
|
+
});
|
|
77
|
+
await awaitApproval(ctx, requestId, tool.name, output, events, approvalTimeoutMs(config));
|
|
78
|
+
}
|
|
79
|
+
if (mockTools && step.live !== true) {
|
|
80
|
+
if (!("result" in step)) {
|
|
81
|
+
throw new RuntimeDiagnosticError({
|
|
82
|
+
phase: "tool.validation",
|
|
83
|
+
toolName: tool.name,
|
|
84
|
+
message: `Tool ${tool.name} has no mocked result. Add "result" to the step, or "live": true to run the real handler.`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
await events.append("tool.result", { toolName: tool.name, result: step.result });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const idempotencyKey = `${ctx.runId}:${index}`;
|
|
91
|
+
nameToolStep(ctx, `tool:${index}`, tool.name);
|
|
92
|
+
try {
|
|
93
|
+
await runToolStep(ctx, events, eventStore, tool, parsedInput.value, `tool:${index}`, idempotencyKey);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof RunCancelled)
|
|
97
|
+
throw error;
|
|
98
|
+
throw new RuntimeDiagnosticError({
|
|
99
|
+
phase: "runtime",
|
|
100
|
+
toolName: tool.name,
|
|
101
|
+
message: error instanceof Error ? error.message : String(error),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return output;
|
|
106
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getProjectPaths } from "@plico-ai/core";
|
|
5
|
+
async function toolFiles(directory) {
|
|
6
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
7
|
+
const files = await Promise.all(entries.map(async (entry) => {
|
|
8
|
+
const path = join(directory, entry.name);
|
|
9
|
+
if (entry.isDirectory())
|
|
10
|
+
return entry.name === "node_modules" ? [] : toolFiles(path);
|
|
11
|
+
return entry.name.endsWith(".ts") ? [path] : [];
|
|
12
|
+
}));
|
|
13
|
+
return files.flat().sort();
|
|
14
|
+
}
|
|
15
|
+
/** Hashes every TypeScript file under `tools/`, tool files and the helpers they import. */
|
|
16
|
+
export async function fingerprintTools(root) {
|
|
17
|
+
const directory = getProjectPaths(root).directories.tools;
|
|
18
|
+
const hash = createHash("sha256");
|
|
19
|
+
for (const file of await toolFiles(directory)) {
|
|
20
|
+
hash.update(file.slice(directory.length).replaceAll("\\", "/"));
|
|
21
|
+
hash.update("\0");
|
|
22
|
+
hash.update(await readFile(file));
|
|
23
|
+
hash.update("\0");
|
|
24
|
+
}
|
|
25
|
+
return hash.digest("hex");
|
|
26
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RunContext } from "../engine/index.js";
|
|
2
|
+
import type { InMemoryEventLog } from "../events.js";
|
|
3
|
+
import type { DeclaredTool, EventStore } from "../types/index.js";
|
|
4
|
+
export declare function runToolStep(ctx: RunContext, events: InMemoryEventLog, eventStore: EventStore | undefined, tool: DeclaredTool, input: unknown, stepName: string, idempotencyKey: string): Promise<unknown>;
|