@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.
Files changed (187) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +9 -3
  3. package/dist/artifacts.d.ts +10 -0
  4. package/dist/artifacts.js +106 -0
  5. package/dist/engine/context.d.ts +16 -0
  6. package/dist/engine/context.js +77 -0
  7. package/dist/engine/engine.d.ts +12 -0
  8. package/dist/engine/engine.js +235 -0
  9. package/dist/engine/errors.d.ts +12 -0
  10. package/dist/engine/errors.js +19 -0
  11. package/dist/engine/index.d.ts +7 -0
  12. package/dist/engine/index.js +5 -0
  13. package/dist/engine/journal.d.ts +65 -0
  14. package/dist/engine/journal.js +1 -0
  15. package/dist/engine/memory-journal.d.ts +2 -0
  16. package/dist/engine/memory-journal.js +125 -0
  17. package/dist/engine/sqlite-engine.d.ts +7 -0
  18. package/dist/engine/sqlite-engine.js +6 -0
  19. package/dist/engine/sqlite-journal.d.ts +5 -0
  20. package/dist/engine/sqlite-journal.js +136 -0
  21. package/dist/engine/types.d.ts +97 -0
  22. package/dist/engine/types.js +1 -0
  23. package/dist/env.d.ts +7 -0
  24. package/dist/env.js +19 -0
  25. package/dist/errors.d.ts +18 -0
  26. package/dist/errors.js +40 -0
  27. package/dist/evals/discover.d.ts +3 -0
  28. package/dist/evals/discover.js +49 -0
  29. package/dist/evals/index.d.ts +3 -0
  30. package/dist/evals/index.js +3 -0
  31. package/dist/evals/promote.d.ts +7 -0
  32. package/dist/evals/promote.js +32 -0
  33. package/dist/evals/run-evals.d.ts +2 -0
  34. package/dist/evals/run-evals.js +69 -0
  35. package/dist/events.d.ts +28 -0
  36. package/dist/events.js +104 -0
  37. package/dist/guards.d.ts +1 -0
  38. package/dist/guards.js +3 -0
  39. package/dist/index.d.ts +18 -0
  40. package/dist/index.js +17 -0
  41. package/dist/instructions.d.ts +4 -0
  42. package/dist/instructions.js +23 -0
  43. package/dist/policy/approval.d.ts +8 -0
  44. package/dist/policy/approval.js +27 -0
  45. package/dist/providers/ai-sdk.d.ts +10 -0
  46. package/dist/providers/ai-sdk.js +154 -0
  47. package/dist/providers/configured.d.ts +17 -0
  48. package/dist/providers/configured.js +60 -0
  49. package/dist/providers/openai-compatible.d.ts +8 -0
  50. package/dist/providers/openai-compatible.js +14 -0
  51. package/dist/providers/project-module.d.ts +7 -0
  52. package/dist/providers/project-module.js +29 -0
  53. package/dist/providers/registry.d.ts +20 -0
  54. package/dist/providers/registry.js +53 -0
  55. package/dist/readiness/check-readiness.d.ts +2 -0
  56. package/dist/readiness/check-readiness.js +33 -0
  57. package/dist/readiness/checks/approval-policy.d.ts +5 -0
  58. package/dist/readiness/checks/approval-policy.js +19 -0
  59. package/dist/readiness/checks/artifact-access.d.ts +3 -0
  60. package/dist/readiness/checks/artifact-access.js +5 -0
  61. package/dist/readiness/checks/artifacts.d.ts +6 -0
  62. package/dist/readiness/checks/artifacts.js +22 -0
  63. package/dist/readiness/checks/env.d.ts +3 -0
  64. package/dist/readiness/checks/env.js +10 -0
  65. package/dist/readiness/checks/evals.d.ts +2 -0
  66. package/dist/readiness/checks/evals.js +53 -0
  67. package/dist/readiness/checks/persistence.d.ts +3 -0
  68. package/dist/readiness/checks/persistence.js +61 -0
  69. package/dist/readiness/checks/project.d.ts +7 -0
  70. package/dist/readiness/checks/project.js +21 -0
  71. package/dist/readiness/checks/provider.d.ts +2 -0
  72. package/dist/readiness/checks/provider.js +20 -0
  73. package/dist/readiness/checks/server-config.d.ts +2 -0
  74. package/dist/readiness/checks/server-config.js +13 -0
  75. package/dist/readiness/checks/studio.d.ts +2 -0
  76. package/dist/readiness/checks/studio.js +16 -0
  77. package/dist/readiness/checks/thread-store.d.ts +3 -0
  78. package/dist/readiness/checks/thread-store.js +12 -0
  79. package/dist/readiness/checks/tools.d.ts +3 -0
  80. package/dist/readiness/checks/tools.js +42 -0
  81. package/dist/readiness/diagnostic.d.ts +2 -0
  82. package/dist/readiness/diagnostic.js +7 -0
  83. package/dist/readiness/index.d.ts +1 -0
  84. package/dist/readiness/index.js +1 -0
  85. package/dist/readiness/open-database.d.ts +8 -0
  86. package/dist/readiness/open-database.js +15 -0
  87. package/dist/run/artifact-tool-context.d.ts +19 -0
  88. package/dist/run/artifact-tool-context.js +74 -0
  89. package/dist/run/finalize.d.ts +16 -0
  90. package/dist/run/finalize.js +11 -0
  91. package/dist/run/index.d.ts +1 -0
  92. package/dist/run/index.js +1 -0
  93. package/dist/run/pinned-instructions.d.ts +18 -0
  94. package/dist/run/pinned-instructions.js +36 -0
  95. package/dist/run/record-tools.d.ts +8 -0
  96. package/dist/run/record-tools.js +21 -0
  97. package/dist/run/run-project.d.ts +17 -0
  98. package/dist/run/run-project.js +222 -0
  99. package/dist/run/strategies/approval.d.ts +9 -0
  100. package/dist/run/strategies/approval.js +42 -0
  101. package/dist/run/strategies/default-dry.d.ts +3 -0
  102. package/dist/run/strategies/default-dry.js +5 -0
  103. package/dist/run/strategies/provider.d.ts +6 -0
  104. package/dist/run/strategies/provider.js +193 -0
  105. package/dist/run/strategies/scripted.d.ts +5 -0
  106. package/dist/run/strategies/scripted.js +106 -0
  107. package/dist/run/tool-fingerprint.d.ts +2 -0
  108. package/dist/run/tool-fingerprint.js +26 -0
  109. package/dist/run/tool-step.d.ts +4 -0
  110. package/dist/run/tool-step.js +62 -0
  111. package/dist/run/tracing.d.ts +12 -0
  112. package/dist/run/tracing.js +92 -0
  113. package/dist/script.d.ts +4 -0
  114. package/dist/script.js +51 -0
  115. package/dist/storage/index.d.ts +1 -0
  116. package/dist/storage/index.js +1 -0
  117. package/dist/storage/metadata.d.ts +5 -0
  118. package/dist/storage/metadata.js +26 -0
  119. package/dist/storage/sqlite/append-event.d.ts +3 -0
  120. package/dist/storage/sqlite/append-event.js +108 -0
  121. package/dist/storage/sqlite/driver.d.ts +9 -0
  122. package/dist/storage/sqlite/driver.js +13 -0
  123. package/dist/storage/sqlite/index.d.ts +1 -0
  124. package/dist/storage/sqlite/index.js +1 -0
  125. package/dist/storage/sqlite/open.d.ts +3 -0
  126. package/dist/storage/sqlite/open.js +54 -0
  127. package/dist/storage/sqlite/owner-lock.d.ts +5 -0
  128. package/dist/storage/sqlite/owner-lock.js +55 -0
  129. package/dist/storage/sqlite/project-thread.d.ts +4 -0
  130. package/dist/storage/sqlite/project-thread.js +33 -0
  131. package/dist/storage/sqlite/queries.d.ts +9 -0
  132. package/dist/storage/sqlite/queries.js +134 -0
  133. package/dist/storage/sqlite/recover-interrupted.d.ts +2 -0
  134. package/dist/storage/sqlite/recover-interrupted.js +46 -0
  135. package/dist/storage/sqlite/schema.d.ts +10 -0
  136. package/dist/storage/sqlite/schema.js +205 -0
  137. package/dist/storage/sqlite/store.d.ts +2 -0
  138. package/dist/storage/sqlite/store.js +51 -0
  139. package/dist/storage/sqlite/thread-log.d.ts +4 -0
  140. package/dist/storage/sqlite/thread-log.js +9 -0
  141. package/dist/storage/sqlite/thread-title.d.ts +24 -0
  142. package/dist/storage/sqlite/thread-title.js +85 -0
  143. package/dist/storage/sqlite/threads.d.ts +47 -0
  144. package/dist/storage/sqlite/threads.js +156 -0
  145. package/dist/threads/stopwords.d.ts +1 -0
  146. package/dist/threads/stopwords.js +16 -0
  147. package/dist/threads/suggest-title.d.ts +9 -0
  148. package/dist/threads/suggest-title.js +102 -0
  149. package/dist/threads/titler.d.ts +10 -0
  150. package/dist/threads/titler.js +63 -0
  151. package/dist/threads/words.d.ts +12 -0
  152. package/dist/threads/words.js +39 -0
  153. package/dist/tools/artifact-model-tool-operations.d.ts +29 -0
  154. package/dist/tools/artifact-model-tool-operations.js +124 -0
  155. package/dist/tools/artifact-model-tools.d.ts +2 -0
  156. package/dist/tools/artifact-model-tools.js +101 -0
  157. package/dist/tools/discover.d.ts +4 -0
  158. package/dist/tools/discover.js +34 -0
  159. package/dist/tools/index.d.ts +3 -0
  160. package/dist/tools/index.js +3 -0
  161. package/dist/tools/load.d.ts +2 -0
  162. package/dist/tools/load.js +83 -0
  163. package/dist/tools/normalize.d.ts +2 -0
  164. package/dist/tools/normalize.js +48 -0
  165. package/dist/tools/schema.d.ts +7 -0
  166. package/dist/tools/schema.js +101 -0
  167. package/dist/tools/skill-model-tool.d.ts +7 -0
  168. package/dist/tools/skill-model-tool.js +38 -0
  169. package/dist/types/artifacts.d.ts +5 -0
  170. package/dist/types/artifacts.js +1 -0
  171. package/dist/types/evals.d.ts +16 -0
  172. package/dist/types/evals.js +1 -0
  173. package/dist/types/index.d.ts +10 -0
  174. package/dist/types/index.js +1 -0
  175. package/dist/types/provider.d.ts +45 -0
  176. package/dist/types/provider.js +1 -0
  177. package/dist/types/readiness.d.ts +9 -0
  178. package/dist/types/readiness.js +1 -0
  179. package/dist/types/run.d.ts +28 -0
  180. package/dist/types/run.js +1 -0
  181. package/dist/types/script.d.ts +16 -0
  182. package/dist/types/script.js +1 -0
  183. package/dist/types/store.d.ts +24 -0
  184. package/dist/types/store.js +1 -0
  185. package/dist/types/tools.d.ts +15 -0
  186. package/dist/types/tools.js +1 -0
  187. package/package.json +67 -8
@@ -0,0 +1,36 @@
1
+ import { readSkills } from "@plico-ai/core";
2
+ import { composeInstructions } from "../instructions.js";
3
+ /**
4
+ * Records the instructions and skills a run starts with. A run that resumes reuses what it
5
+ * recorded, so editing `agent.md` or a skill while it waits does not change its behaviour.
6
+ */
7
+ export async function pinInstructions(project, events) {
8
+ const recorded = events.all().find((event) => event.type === "instructions.composed");
9
+ if (recorded?.type === "instructions.composed") {
10
+ await events.append("instructions.composed", recorded.payload);
11
+ // Runs recorded before skills were pinned fall back to the skills on disk.
12
+ const skills = recorded.payload.skills ?? (await readSkills(project.root)).skills;
13
+ return { instructions: { content: recorded.payload.content }, skills };
14
+ }
15
+ const { skills } = await readSkills(project.root);
16
+ const instructions = await composeInstructions(project, skills);
17
+ await events.append("instructions.composed", {
18
+ length: instructions.content.length,
19
+ sources: instructions.sources,
20
+ content: instructions.content,
21
+ skills: skills.map(({ name, description, source, body }) => ({
22
+ name,
23
+ description,
24
+ source,
25
+ body,
26
+ })),
27
+ });
28
+ return { instructions, skills };
29
+ }
30
+ /**
31
+ * A run that already recorded its instructions and skills does not read them again, so an
32
+ * `agent.md` or skill that is invalid now must not stop it from resuming.
33
+ */
34
+ export function ignoredWhenPinned(issue) {
35
+ return issue.path === "agent.md" || issue.path === "skills" || issue.path.startsWith("skills/");
36
+ }
@@ -0,0 +1,8 @@
1
+ import type { InMemoryEventLog } from "../events.js";
2
+ import type { DeclaredTool } from "../types/index.js";
3
+ /**
4
+ * Records the tools a run discovered with a hash of the tool code. A resumed run compares both, so
5
+ * a changed handler fails the run before the tool runs. Runs recorded without a hash are compared
6
+ * on their declarations only.
7
+ */
8
+ export declare function recordTools(root: string, tools: DeclaredTool[], events: InMemoryEventLog): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { fingerprintTools } from "./tool-fingerprint.js";
2
+ /**
3
+ * Records the tools a run discovered with a hash of the tool code. A resumed run compares both, so
4
+ * a changed handler fails the run before the tool runs. Runs recorded without a hash are compared
5
+ * on their declarations only.
6
+ */
7
+ export async function recordTools(root, tools, events) {
8
+ const recorded = events.all().find((event) => event.type === "tools.discovered");
9
+ const legacy = recorded?.type === "tools.discovered" && recorded.payload.fingerprint === undefined;
10
+ await events.append("tools.discovered", {
11
+ tools: tools.map((tool) => ({
12
+ source: tool.source,
13
+ name: tool.name,
14
+ description: tool.description,
15
+ inputSchema: tool.inputSchema,
16
+ capabilities: tool.capabilities,
17
+ approval: tool.approval,
18
+ })),
19
+ ...(legacy ? {} : { fingerprint: await fingerprintTools(root) }),
20
+ });
21
+ }
@@ -0,0 +1,17 @@
1
+ import { type Engine, type RecoveredRun } from "../engine/index.js";
2
+ import type { RunProjectOptions, RunResult } from "../types/index.js";
3
+ export declare function runProject(root: string, options?: RunProjectOptions): Promise<RunResult>;
4
+ /**
5
+ * Continues runs a dead process left running. The provider and event store come from this process;
6
+ * the project path, message and script come from the journal.
7
+ */
8
+ export declare function recoverRuns(options: RunProjectOptions & {
9
+ engine: Engine;
10
+ recoveryFilter?: {
11
+ projectRoot: string;
12
+ providerRecoveryKey?: string;
13
+ providerOnly?: boolean;
14
+ };
15
+ onRecoveryStarted?: (runId: string) => void;
16
+ onRecoverySettled?: (runId: string) => void;
17
+ }): Promise<RecoveredRun[]>;
@@ -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,3 @@
1
+ import type { LoadedProject } from "@plico-ai/core";
2
+ import type { InMemoryEventLog } from "../../events.js";
3
+ export declare function runDefaultDryRun(project: LoadedProject, events: InMemoryEventLog): Promise<string>;
@@ -0,0 +1,5 @@
1
+ export async function runDefaultDryRun(project, events) {
2
+ const output = `Dry run complete for ${project.config.name}.`;
3
+ await events.append("assistant.output", { content: output });
4
+ return output;
5
+ }
@@ -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>;