@wardby/cli 0.8.0 → 0.9.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 (123) hide show
  1. package/deploy/local/docker-compose.quickstart-coding.yml +5 -0
  2. package/dist/claude-plugin/command.d.ts +4 -1
  3. package/dist/claude-plugin/command.js +7 -4
  4. package/dist/cli.js +24 -0
  5. package/dist/coding/transcript/recorder.js +2 -8
  6. package/dist/core/dispatch.js +3 -0
  7. package/dist/core/engine-native.d.ts +2 -0
  8. package/dist/core/engine-native.js +13 -0
  9. package/dist/core/error-code.js +1 -0
  10. package/dist/core/listen-address.d.ts +10 -0
  11. package/dist/core/listen-address.js +14 -0
  12. package/dist/core/power-idle.d.ts +8 -2
  13. package/dist/core/power-idle.js +17 -3
  14. package/dist/core/power.d.ts +5 -0
  15. package/dist/core/power.js +5 -0
  16. package/dist/core/retention-days.d.ts +5 -0
  17. package/dist/core/retention-days.js +14 -0
  18. package/dist/core/run-steps/config.d.ts +25 -0
  19. package/dist/core/run-steps/config.js +40 -0
  20. package/dist/core/run-steps/control-chars.d.ts +16 -0
  21. package/dist/core/run-steps/control-chars.js +26 -0
  22. package/dist/core/run-steps/detail.d.ts +32 -0
  23. package/dist/core/run-steps/detail.js +240 -0
  24. package/dist/core/run-steps/extract.d.ts +52 -0
  25. package/dist/core/run-steps/extract.js +467 -0
  26. package/dist/core/run-steps/ingress.d.ts +51 -0
  27. package/dist/core/run-steps/ingress.js +160 -0
  28. package/dist/core/run-steps/last-step.d.ts +15 -0
  29. package/dist/core/run-steps/last-step.js +35 -0
  30. package/dist/core/run-steps/native.d.ts +48 -0
  31. package/dist/core/run-steps/native.js +164 -0
  32. package/dist/core/run-steps/seen.d.ts +16 -0
  33. package/dist/core/run-steps/seen.js +35 -0
  34. package/dist/core/run-steps/summarize.d.ts +57 -0
  35. package/dist/core/run-steps/summarize.js +304 -0
  36. package/dist/core/run-steps/sweep.d.ts +21 -0
  37. package/dist/core/run-steps/sweep.js +41 -0
  38. package/dist/core/run-steps/types.d.ts +89 -0
  39. package/dist/core/run-steps/types.js +39 -0
  40. package/dist/core/run-steps/wire.d.ts +35 -0
  41. package/dist/core/run-steps/wire.js +47 -0
  42. package/dist/core/run-steps/writer.d.ts +49 -0
  43. package/dist/core/run-steps/writer.js +279 -0
  44. package/dist/core/runner.d.ts +4 -0
  45. package/dist/core/runner.js +15 -1
  46. package/dist/generated/prisma/browser.d.ts +8 -0
  47. package/dist/generated/prisma/client.d.ts +8 -0
  48. package/dist/generated/prisma/commonInputTypes.d.ts +30 -0
  49. package/dist/generated/prisma/enums.d.ts +6 -0
  50. package/dist/generated/prisma/enums.js +5 -0
  51. package/dist/generated/prisma/internal/class.d.ts +11 -0
  52. package/dist/generated/prisma/internal/class.js +4 -4
  53. package/dist/generated/prisma/internal/prismaNamespace.d.ts +103 -1
  54. package/dist/generated/prisma/internal/prismaNamespace.js +19 -1
  55. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +19 -0
  56. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +19 -1
  57. package/dist/generated/prisma/models/Agent.d.ts +101 -1
  58. package/dist/generated/prisma/models/CodingProxySession.d.ts +41 -1
  59. package/dist/generated/prisma/models/Run.d.ts +503 -1
  60. package/dist/generated/prisma/models/RunStep.d.ts +1453 -0
  61. package/dist/generated/prisma/models/RunStep.js +1 -0
  62. package/dist/generated/prisma/models.d.ts +1 -0
  63. package/dist/help-index.json +102 -14
  64. package/dist/mcp/auth/access.d.ts +1 -0
  65. package/dist/mcp/context.d.ts +3 -0
  66. package/dist/mcp/index.js +10 -0
  67. package/dist/mcp/tools/agents.js +37 -7
  68. package/dist/mcp/tools/power.js +2 -2
  69. package/dist/mcp/tools/run-access.d.ts +7 -0
  70. package/dist/mcp/tools/run-access.js +14 -0
  71. package/dist/mcp/tools/run-steps.d.ts +16 -0
  72. package/dist/mcp/tools/run-steps.js +85 -0
  73. package/dist/mcp/tools/runs.js +10 -2
  74. package/dist/native-worker/gateway-server.d.ts +4 -1
  75. package/dist/native-worker/gateway-server.js +10 -2
  76. package/dist/native-worker/gateway.d.ts +3 -0
  77. package/dist/native-worker/gateway.js +8 -0
  78. package/dist/native-worker/http-server.d.ts +2 -4
  79. package/dist/native-worker/http-server.js +2 -8
  80. package/dist/providers/coding-proxy/memory-ledger.js +1 -0
  81. package/dist/providers/coding-proxy/prisma-ledger.js +9 -3
  82. package/dist/providers/coding-proxy/proxy.d.ts +26 -0
  83. package/dist/providers/coding-proxy/proxy.js +133 -42
  84. package/dist/providers/coding-proxy/run-step-forwarder.d.ts +42 -0
  85. package/dist/providers/coding-proxy/run-step-forwarder.js +206 -0
  86. package/dist/providers/coding-proxy/runtime.d.ts +7 -0
  87. package/dist/providers/coding-proxy/runtime.js +55 -3
  88. package/dist/providers/coding-proxy/types.d.ts +5 -0
  89. package/dist/providers/engine/types.d.ts +26 -0
  90. package/dist/providers/executor/container.d.ts +14 -0
  91. package/dist/providers/executor/container.js +8 -1
  92. package/dist/providers/jobs/kubernetes.d.ts +8 -0
  93. package/dist/providers/jobs/kubernetes.js +22 -4
  94. package/dist/quickstart-images.json +1 -1
  95. package/dist/serve.js +26 -18
  96. package/dist/viewer/api-schema.d.ts +66 -0
  97. package/dist/viewer/api-schema.js +31 -0
  98. package/dist/viewer/openapi.js +2 -1
  99. package/dist/viewer/run-detail.d.ts +2 -0
  100. package/dist/viewer/run-detail.js +26 -1
  101. package/docs/claude-code-plugin.md +57 -13
  102. package/docs/coding-agent-setup.md +17 -0
  103. package/docs/coding-worker-isolation.md +16 -0
  104. package/docs/debugging-agent-runs.md +112 -0
  105. package/docs/getting-started-gke.md +28 -7
  106. package/docs/mcp-servers.md +9 -0
  107. package/docs/native-sandbox.md +6 -0
  108. package/docs/security-deployment.md +51 -0
  109. package/docs/viewer-api.md +21 -12
  110. package/docs/viewer-api.openapi.yml +78 -2
  111. package/help/admin-viewer.md +1 -0
  112. package/help/claude-code-run-pane.md +27 -4
  113. package/help/deploy-gke.md +5 -0
  114. package/help/errors/coding-mcp-gateway-unavailable.md +46 -0
  115. package/help/low-power-mode.md +19 -8
  116. package/help/mcp.md +2 -0
  117. package/help/resume-runs-locally.md +4 -0
  118. package/help/run-steps.md +140 -0
  119. package/package.json +1 -1
  120. package/prisma/migrations/20261110000000_run_steps/migration.sql +26 -0
  121. package/prisma/migrations/20261111000000_run_steps_viewer_notify/migration.sql +59 -0
  122. package/prisma/migrations/20261112000000_run_steps_detail/migration.sql +9 -0
  123. package/prisma/schema.prisma +52 -0
@@ -0,0 +1,279 @@
1
+ /**
2
+ * The one writer of RunStep rows. Every source hands it batches (RunStepSink). Before anything is
3
+ * stored, each step's text is redacted by exact value against the secret values the loader covers:
4
+ * the agent's secrets (readable and brokered) and its attached MCP servers' secrets, the same source
5
+ * the transcript recorder uses; run capabilities are masked by token shape. The native-sandbox
6
+ * gateway capability is passed by the gateway as `extraSecretValues` and redacted the same way. A batch whose run's secret values
7
+ * can't be read writes nothing (fail closed). A step whose kind is unknown, whose callId is invalid or
8
+ * would change under redaction is dropped and not counted in stepsDropped; only steps past the cap are
9
+ * counted, once per callId. One run's batches are written in arrival order, within one process only
10
+ * (step/result ordering across processes is not guaranteed), under a row lock on the run, so seq has
11
+ * no gaps or duplicates whichever process writes. Known limits: a dropped step may be counted again
12
+ * after a process restart, after its run is evicted from the remembered set, past the per-run id
13
+ * limit, or when two processes write the same run. A result is matched only to a stored callId, which
14
+ * is already clean, so a result's callId is never persisted. At the detail level (the run's
15
+ * Run.runStepsLevel, fixed at dispatch), each step's detail text is redacted the same way
16
+ * (stepDetailer) before it is cut and stored; below it, detail material in a batch is dropped
17
+ * unread. Step text, input and output lose terminal control sequences and control characters
18
+ * (plainText: exact values masked on the raw text first, then stripControlSequences, then the full
19
+ * redaction), so no reader is sent an escape sequence. Log lines
20
+ * carry the run id, a fixed event code and an error's name: never text, never an error's message.
21
+ */
22
+ import { createHash } from "node:crypto";
23
+ import { REDACTED, redactSecrets } from "../../coding/redact.js";
24
+ import { logger } from "../logger.js";
25
+ import { loadAgentSecretValues } from "../secret-guard.js";
26
+ import { MIN_REDACTABLE_SECRET_LENGTH } from "../secret-spans.js";
27
+ import { loadRunStepRetentionDays, loadRunStepsLevel, recordedRunStepsLevel } from "./config.js";
28
+ import { stripControlSequences } from "./control-chars.js";
29
+ import { joinDetail, stepDetailer } from "./detail.js";
30
+ import { stepSummarizer } from "./summarize.js";
31
+ import { MAX_RUN_STEPS, MAX_STEP_DETAIL_CHARS, STEP_CALL_ID, STEP_STATUS, STEP_TOOL_NAME, tailOf, } from "./types.js";
32
+ const stepsLog = logger.child({ module: "run-steps" });
33
+ const DAY_MS = 24 * 60 * 60 * 1000;
34
+ const TX = { maxWait: 5_000, timeout: 10_000 };
35
+ /** How long flush() waits for queued writes at shutdown. */
36
+ export const FLUSH_TIMEOUT_MS = 5_000;
37
+ /** Over-cap callIds remembered (as 32-character hashes): at most this many per run and in all. */
38
+ const DROPPED_PER_RUN = 10_000;
39
+ const DROPPED_TOTAL = 200_000;
40
+ const callKey = (callId) => createHash("sha256").update(callId).digest("hex").slice(0, 32);
41
+ function uniqueByCallId(steps) {
42
+ const seen = new Set();
43
+ return steps.filter((step) => {
44
+ if (!STEP_CALL_ID.test(step.callId) || seen.has(step.callId))
45
+ return false;
46
+ seen.add(step.callId);
47
+ return true;
48
+ });
49
+ }
50
+ /** A private-use character: stands in for a masked value while control sequences are stripped. */
51
+ const HELD = "\uE000";
52
+ /**
53
+ * Untrusted text as plain text, safely: every secret value is masked on the raw text first, whole
54
+ * and partial (StepDetailer.premask: the text's edges and every line and ellipsis boundary), then
55
+ * control sequences are stripped (stripControlSequences), and only then does the full redaction
56
+ * run. Stripping first could take a secret's first character as a malformed sequence's final byte
57
+ * (`ESC Z` + value) and leave the rest of it matching nothing; the full redaction after the strip
58
+ * still masks a value that only the strip joined (one split by colour codes). The mask is a
59
+ * placeholder no sequence pattern can take as a byte of its own (a sequence before `[REDACTED]`
60
+ * would eat its "["), swapped for the marker after the strip.
61
+ */
62
+ function plainText(raw, premask, headWasCut = false) {
63
+ if (!premask)
64
+ return stripControlSequences(raw);
65
+ const held = premask(raw.replaceAll(HELD, ""), headWasCut, HELD);
66
+ return stripControlSequences(held).replaceAll(HELD, REDACTED);
67
+ }
68
+ /**
69
+ * A result's detail suffix: its end taken from the raw output first (tailOf), so the premask's head
70
+ * check runs at the cut the detailer will see, then plainText, then the detailer.
71
+ */
72
+ function plainOutput(raw, outputCut, premask, detailer) {
73
+ const source = tailOf(raw);
74
+ const cut = outputCut || source.cut;
75
+ return detailer.output({ output: plainText(source.text, premask, cut), outputCut: cut });
76
+ }
77
+ /** `step` without its detail-only input. */
78
+ function withoutInput(step) {
79
+ if (step.kind !== "tool" || step.input === undefined)
80
+ return step;
81
+ const { input: _input, inputTruncated: _inputTruncated, ...rest } = step;
82
+ return rest;
83
+ }
84
+ /** A step's text and input as plainText. */
85
+ function plainStep(step, premask) {
86
+ if (step.kind === "said")
87
+ return { ...step, text: plainText(step.text, premask) };
88
+ const input = step.input === undefined ? {} : { input: plainText(step.input, premask) };
89
+ return { ...step, text: plainText(step.text, premask), ...input };
90
+ }
91
+ function safeToolName(tool, secretValues) {
92
+ const shown = redactSecrets(tool, secretValues);
93
+ return STEP_TOOL_NAME.test(shown) ? shown : "tool";
94
+ }
95
+ export class RunStepWriter {
96
+ deps;
97
+ queues = new Map();
98
+ /**
99
+ * Hashes of the callIds already counted in stepsDropped, so history resent past the cap is not
100
+ * counted again. Least recently used run first; bounded in total, whatever the ids' length.
101
+ */
102
+ droppedIds = new Map();
103
+ constructor(deps) {
104
+ this.deps = deps;
105
+ }
106
+ record(runId, batch, extraSecretValues = []) {
107
+ if (this.deps.level === "off")
108
+ return Promise.resolve();
109
+ if (batch.steps.length === 0 && batch.results.length === 0)
110
+ return Promise.resolve();
111
+ const previous = this.queues.get(runId) ?? Promise.resolve();
112
+ const next = previous
113
+ .then(() => this.write(runId, batch, extraSecretValues))
114
+ .catch((err) => {
115
+ (this.deps.log ?? stepsLog).warn({ runId, event: "run_steps_write_failed", errorName: err instanceof Error ? err.name : typeof err }, "could not record a run's steps; they were dropped");
116
+ });
117
+ this.queues.set(runId, next);
118
+ void next.then(() => {
119
+ if (this.queues.get(runId) === next)
120
+ this.queues.delete(runId);
121
+ });
122
+ return next;
123
+ }
124
+ /**
125
+ * Resolves when every write queued so far has settled, or after `timeoutMs`: called before the
126
+ * process disconnects from the database, so a run's last steps are not cut off. Never rejects.
127
+ */
128
+ async flush(timeoutMs = FLUSH_TIMEOUT_MS) {
129
+ const pending = [...this.queues.values()];
130
+ if (pending.length === 0)
131
+ return;
132
+ let timer;
133
+ const bound = new Promise((resolve) => {
134
+ timer = setTimeout(resolve, timeoutMs);
135
+ });
136
+ try {
137
+ await Promise.race([Promise.allSettled(pending), bound]);
138
+ }
139
+ finally {
140
+ clearTimeout(timer);
141
+ }
142
+ }
143
+ now() {
144
+ return (this.deps.now ?? (() => new Date()))();
145
+ }
146
+ async write(runId, batch, extraSecretValues) {
147
+ const { db } = this.deps;
148
+ const run = await db.run.findUnique({ where: { id: runId }, select: { agentId: true, runStepsLevel: true } });
149
+ if (!run)
150
+ return;
151
+ const level = recordedRunStepsLevel(run.runStepsLevel, this.deps.level);
152
+ if (level === "off")
153
+ return;
154
+ const detailed = level === "detail";
155
+ // Below detail a tool's input is never stored, so it is dropped here, before any redaction.
156
+ const extracted = uniqueByCallId(batch.steps.filter((step) => step.kind === "tool" || step.kind === "said")).map((step) => (detailed ? step : withoutInput(step)));
157
+ const results = batch.results.filter((r) => STEP_CALL_ID.test(r.callId) && STEP_STATUS.test(r.status));
158
+ const outputs = detailed && results.some((r) => typeof r.output === "string");
159
+ // Read fresh for every batch that stores text (steps, or outputs at detail), never cached: a
160
+ // rotated or newly attached secret must be covered. Rejects (and the batch is dropped) when the
161
+ // values can't be read: never an unredacted write.
162
+ const needsValues = extracted.length > 0 || outputs;
163
+ const own = needsValues ? await this.deps.secretValues(run.agentId) : [];
164
+ const extras = needsValues ? extraSecretValues.filter(Boolean) : [];
165
+ const rawValues = [...own, ...extras];
166
+ // Each value also as plain text, as the step text is: a value holding a control character
167
+ // (never a stripped form too short to redact safely).
168
+ const values = [
169
+ ...new Set(rawValues.flatMap((value) => {
170
+ const plain = stripControlSequences(value);
171
+ return plain.length >= MIN_REDACTABLE_SECRET_LENGTH ? [value, plain] : [value];
172
+ })),
173
+ ];
174
+ // The summary forms are built once per batch, on the first row; the detailer (whose premask
175
+ // every text goes through first) once per batch that has any secret value.
176
+ let summarize;
177
+ let detailer;
178
+ const detailOf = () => (detailer ??= stepDetailer(values));
179
+ const premask = values.length > 0 ? (raw, headWasCut, marker) => detailOf().premask(raw, headWasCut, marker) : undefined;
180
+ const steps = extracted.map((step) => plainStep(step, premask));
181
+ const at = this.now();
182
+ const expiresAt = new Date(at.getTime() + this.deps.retentionDays * DAY_MS);
183
+ // callId is stored as text: a step whose callId holds a secret value is not stored at all.
184
+ const clean = steps.filter((step) => redactSecrets(step.callId, values) === step.callId);
185
+ const rows = clean.map((step) => ({
186
+ kind: step.kind,
187
+ tool: step.kind === "tool" ? safeToolName(step.tool, values) : null,
188
+ callId: step.callId,
189
+ summary: (summarize ??= stepSummarizer(values))(step),
190
+ detail: detailed ? detailOf().step(step).slice(0, MAX_STEP_DETAIL_CHARS) : null,
191
+ at,
192
+ expiresAt,
193
+ }));
194
+ const maxSteps = this.deps.maxSteps ?? MAX_RUN_STEPS;
195
+ const counted = this.droppedIds.get(runId) ?? new Set();
196
+ if (this.droppedIds.has(runId)) {
197
+ this.droppedIds.delete(runId);
198
+ this.droppedIds.set(runId, counted);
199
+ }
200
+ // Each result's detail suffix, computed before the transaction: redaction is CPU work, and the
201
+ // transaction holds the run's row lock.
202
+ const suffixes = results.map((result) => outputs && typeof result.output === "string"
203
+ ? plainOutput(result.output, result.outputCut === true, premask, detailOf())
204
+ : undefined);
205
+ const newlyDropped = [];
206
+ await db.$transaction(async (tx) => {
207
+ await tx.$queryRaw `SELECT 1 FROM "Run" WHERE "id" = ${runId} FOR UPDATE`;
208
+ if (rows.length > 0) {
209
+ const existing = await tx.runStep.findMany({
210
+ where: { runId, callId: { in: rows.map((row) => row.callId) } },
211
+ select: { callId: true },
212
+ });
213
+ const known = new Set(existing.map((row) => row.callId));
214
+ const fresh = rows.filter((row) => !known.has(row.callId));
215
+ const last = await tx.runStep.findFirst({ where: { runId }, orderBy: { seq: "desc" }, select: { seq: true } });
216
+ const start = last?.seq ?? 0;
217
+ const kept = fresh.slice(0, Math.max(0, maxSteps - start));
218
+ if (kept.length > 0) {
219
+ await tx.runStep.createMany({ data: kept.map((row, index) => ({ ...row, runId, seq: start + index + 1 })) });
220
+ }
221
+ const over = fresh.slice(kept.length).filter((row) => !counted.has(callKey(row.callId)));
222
+ newlyDropped.push(...over.map((row) => row.callId));
223
+ const dropped = over.length;
224
+ if (dropped > 0)
225
+ await tx.run.update({ where: { id: runId }, data: { stepsDropped: { increment: dropped } } });
226
+ }
227
+ for (const [index, result] of results.entries()) {
228
+ const suffix = suffixes[index];
229
+ if (suffix === undefined) {
230
+ // The first status wins: a result resent with the history, or replayed, changes nothing.
231
+ await tx.runStep.updateMany({
232
+ where: { runId, callId: result.callId, kind: "tool", status: null },
233
+ data: { status: result.status },
234
+ });
235
+ continue;
236
+ }
237
+ const open = await tx.runStep.findFirst({
238
+ where: { runId, callId: result.callId, kind: "tool", status: null },
239
+ select: { id: true, detail: true },
240
+ });
241
+ if (!open)
242
+ continue;
243
+ await tx.runStep.update({
244
+ where: { id: open.id },
245
+ data: { status: result.status, detail: joinDetail(open.detail, suffix) },
246
+ });
247
+ }
248
+ }, TX);
249
+ if (newlyDropped.length > 0)
250
+ this.rememberDropped(runId, counted, newlyDropped);
251
+ }
252
+ rememberDropped(runId, counted, ids) {
253
+ const limits = this.deps.droppedLimits ?? { perRun: DROPPED_PER_RUN, total: DROPPED_TOTAL };
254
+ for (const id of ids)
255
+ if (counted.size < limits.perRun)
256
+ counted.add(callKey(id));
257
+ this.droppedIds.delete(runId);
258
+ this.droppedIds.set(runId, counted);
259
+ while (this.rememberedDroppedCount() > limits.total && this.droppedIds.size > 1) {
260
+ this.droppedIds.delete(this.droppedIds.keys().next().value);
261
+ }
262
+ }
263
+ /** How many dropped callIds are remembered in all (tests). */
264
+ rememberedDroppedCount() {
265
+ let total = 0;
266
+ for (const set of this.droppedIds.values())
267
+ total += set.size;
268
+ return total;
269
+ }
270
+ }
271
+ /** The process's writer: settings from the environment (an invalid value throws, naming it). */
272
+ export function buildRunStepWriter(db, cipher, env = process.env) {
273
+ return new RunStepWriter({
274
+ db,
275
+ level: loadRunStepsLevel(env),
276
+ retentionDays: loadRunStepRetentionDays(env),
277
+ secretValues: async (agentId) => (await loadAgentSecretValues(db, cipher, agentId)).flatMap((entry) => entry.values),
278
+ });
279
+ }
@@ -21,6 +21,7 @@ import { Prisma, type PrismaClient, type Run, type RunTrigger } from "#prisma";
21
21
  import type { ProviderRegistry } from "../providers/index.js";
22
22
  import type { EngineProgress, EngineResult, LoadedTool } from "../providers/engine/types.js";
23
23
  import { RunOwnershipLostError, type StepRunner } from "../providers/engine/types.js";
24
+ import type { RunStepSink } from "./run-steps/types.js";
24
25
  import { type PrivilegedHost } from "../sandbox/host-functions.js";
25
26
  import type { safeFetch } from "../sandbox/safe-fetch.js";
26
27
  import { type RunDrivability, type WorkerLauncher } from "../native-worker/gateway.js";
@@ -96,6 +97,7 @@ declare const DELEGATION_CHILD_SELECT: {
96
97
  readonly budgetUsd: true;
97
98
  readonly ownerId: true;
98
99
  readonly nativeExecutionMode: true;
100
+ readonly runSteps: true;
99
101
  readonly codingProfile: {
100
102
  readonly select: {
101
103
  readonly allowWebhookTaskOverride: true;
@@ -148,6 +150,8 @@ export type NativeRunProviders = Pick<ProviderRegistry, "llm" | "engine" | "data
148
150
  nativeSandbox?: WorkerLauncher;
149
151
  /** Overrides the sandbox's outbound network call — tests only. */
150
152
  sandboxFetch?: typeof safeFetch;
153
+ /** Records runs' steps (src/core/run-steps/writer.ts); absent = no steps are recorded. */
154
+ runSteps?: RunStepSink;
151
155
  };
152
156
  /**
153
157
  * Raised when a run's step boundary observed a cancellation (DbosExecutor's
@@ -23,6 +23,7 @@ import { RunOwnershipLostError, STEPS_CHANGED_REASON, StepReplayMismatchError, r
23
23
  import { isLlmEffort } from "../providers/llm/types.js";
24
24
  import { CODING_REPOSITORY_LINK_FILTER, extraCodingRepositoryLinks, hasCodingRepositoryList, RepositoryChoiceRefusedError, resolveCodingRepository, } from "./coding-repository.js";
25
25
  import { delegationLimitRefusal, delegationSlot, priorDelegationSlots } from "./delegation-slots.js";
26
+ import { runStepsHook } from "./run-steps/native.js";
26
27
  import { createPrivilegedHost } from "../sandbox/host-functions.js";
27
28
  import { consoleRedactionValues } from "../sandbox/secret-broker.js";
28
29
  import { runUserToolCall } from "../sandbox/user-tool.js";
@@ -73,6 +74,7 @@ import { pinNativeRunPricing } from "./run-pricing.js";
73
74
  import { RoutingLlmProvider } from "../providers/llm/routing.js";
74
75
  import { ModelUnavailableError } from "../providers/llm/catalog-types.js";
75
76
  import { trackRun } from "./in-flight-runs.js";
77
+ import { dispatchRunStepsLevel } from "./run-steps/config.js";
76
78
  import { ReviewReadLedger } from "./review-reads.js";
77
79
  import { createRepoAccessGate, requiredLevel } from "./repo-access.js";
78
80
  import { loadBudgetEvent, ownerBudgetSentence } from "./budget-events.js";
@@ -216,6 +218,7 @@ const DELEGATION_CHILD_SELECT = {
216
218
  budgetUsd: true,
217
219
  ownerId: true,
218
220
  nativeExecutionMode: true,
221
+ runSteps: true,
219
222
  codingProfile: { select: { allowWebhookTaskOverride: true, timeoutSec: true, repository: true, baseRef: true } },
220
223
  };
221
224
  /**
@@ -566,7 +569,14 @@ export async function createRun(db, agentName, trigger = "manual") {
566
569
  if (agent.nativeExecutionMode === "sandbox") {
567
570
  throw new Error(NATIVE_SANDBOX_NOT_CONFIGURED);
568
571
  }
569
- return db.run.create({ data: { agentId: agent.id, trigger, nativeExecutionMode: agent.nativeExecutionMode } });
572
+ return db.run.create({
573
+ data: {
574
+ agentId: agent.id,
575
+ trigger,
576
+ nativeExecutionMode: agent.nativeExecutionMode,
577
+ runStepsLevel: dispatchRunStepsLevel(agent),
578
+ },
579
+ });
570
580
  }
571
581
  /**
572
582
  * The trusted load of a native run: the agent, its attached tools and their
@@ -1326,6 +1336,7 @@ export function createNativeRunTools(options) {
1326
1336
  taskOverride,
1327
1337
  triggeredById: existingRun.triggeredById,
1328
1338
  nativeExecutionMode: childAgent.nativeExecutionMode,
1339
+ runStepsLevel: dispatchRunStepsLevel(childAgent),
1329
1340
  },
1330
1341
  });
1331
1342
  }
@@ -2087,6 +2098,7 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
2087
2098
  });
2088
2099
  const { runSandboxTool } = tools;
2089
2100
  const onProgress = (progress) => recordNativeRunProgress(db, runId, progress);
2101
+ const onTurn = runStepsHook(providers.runSteps, runId);
2090
2102
  const llm = loaded.pricing && providers.llm instanceof RoutingLlmProvider
2091
2103
  ? providers.llm.forRun(loaded.pricing.entry)
2092
2104
  : providers.llm;
@@ -2102,6 +2114,7 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
2102
2114
  mcpSecretValues: tools.mcpSecretValues,
2103
2115
  drivability: () => runDrivability(db, runId),
2104
2116
  launcher: providers.nativeSandbox,
2117
+ ...(providers.runSteps ? { runSteps: providers.runSteps } : {}),
2105
2118
  });
2106
2119
  return await finishNativeRun(finishContext, loaded.agent.model, engineResult);
2107
2120
  }
@@ -2116,6 +2129,7 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
2116
2129
  : {}),
2117
2130
  onText,
2118
2131
  onProgress,
2132
+ ...(onTurn ? { onTurn } : {}),
2119
2133
  step,
2120
2134
  });
2121
2135
  return await finishNativeRun(finishContext, loaded.agent.model, engineResult);
@@ -117,6 +117,14 @@ export type CodingRun = Prisma.CodingRunModel;
117
117
  * the blob store under blobKey; the unredacted transcript is never stored.
118
118
  */
119
119
  export type RunTranscript = Prisma.RunTranscriptModel;
120
+ /**
121
+ * Model RunStep
122
+ * One line of what a run did: a tool call (with its status once the result is back) or what the
123
+ * model said. Derived by trusted code from model traffic and written only by the control plane's
124
+ * RunStepWriter, after exact-value redaction against the run's secret values
125
+ * (docs/debugging-agent-runs.md#run-steps). Never tool output at the summary level.
126
+ */
127
+ export type RunStep = Prisma.RunStepModel;
120
128
  /**
121
129
  * Model CodingService
122
130
  * Coding-run services (docs/coding-services.md): the catalog a repository's
@@ -136,6 +136,14 @@ export type CodingRun = Prisma.CodingRunModel;
136
136
  * the blob store under blobKey; the unredacted transcript is never stored.
137
137
  */
138
138
  export type RunTranscript = Prisma.RunTranscriptModel;
139
+ /**
140
+ * Model RunStep
141
+ * One line of what a run did: a tool call (with its status once the result is back) or what the
142
+ * model said. Derived by trusted code from model traffic and written only by the control plane's
143
+ * RunStepWriter, after exact-value redaction against the run's secret values
144
+ * (docs/debugging-agent-runs.md#run-steps). Never tool output at the summary level.
145
+ */
146
+ export type RunStep = Prisma.RunStepModel;
139
147
  /**
140
148
  * Model CodingService
141
149
  * Coding-run services (docs/coding-services.md): the catalog a repository's
@@ -85,6 +85,12 @@ export type EnumNativeExecutionModeFilter<$PrismaModel = never> = {
85
85
  notIn?: $Enums.NativeExecutionMode[] | Prisma.ListEnumNativeExecutionModeFieldRefInput<$PrismaModel>;
86
86
  not?: Prisma.NestedEnumNativeExecutionModeFilter<$PrismaModel> | $Enums.NativeExecutionMode;
87
87
  };
88
+ export type EnumRunStepsLevelNullableFilter<$PrismaModel = never> = {
89
+ equals?: $Enums.RunStepsLevel | Prisma.EnumRunStepsLevelFieldRefInput<$PrismaModel> | null;
90
+ in?: $Enums.RunStepsLevel[] | Prisma.ListEnumRunStepsLevelFieldRefInput<$PrismaModel> | null;
91
+ notIn?: $Enums.RunStepsLevel[] | Prisma.ListEnumRunStepsLevelFieldRefInput<$PrismaModel> | null;
92
+ not?: Prisma.NestedEnumRunStepsLevelNullableFilter<$PrismaModel> | $Enums.RunStepsLevel | null;
93
+ };
88
94
  export type SortOrderInput = {
89
95
  sort: Prisma.SortOrder;
90
96
  nulls?: Prisma.NullsOrder;
@@ -204,6 +210,15 @@ export type EnumNativeExecutionModeWithAggregatesFilter<$PrismaModel = never> =
204
210
  _min?: Prisma.NestedEnumNativeExecutionModeFilter<$PrismaModel>;
205
211
  _max?: Prisma.NestedEnumNativeExecutionModeFilter<$PrismaModel>;
206
212
  };
213
+ export type EnumRunStepsLevelNullableWithAggregatesFilter<$PrismaModel = never> = {
214
+ equals?: $Enums.RunStepsLevel | Prisma.EnumRunStepsLevelFieldRefInput<$PrismaModel> | null;
215
+ in?: $Enums.RunStepsLevel[] | Prisma.ListEnumRunStepsLevelFieldRefInput<$PrismaModel> | null;
216
+ notIn?: $Enums.RunStepsLevel[] | Prisma.ListEnumRunStepsLevelFieldRefInput<$PrismaModel> | null;
217
+ not?: Prisma.NestedEnumRunStepsLevelNullableWithAggregatesFilter<$PrismaModel> | $Enums.RunStepsLevel | null;
218
+ _count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
219
+ _min?: Prisma.NestedEnumRunStepsLevelNullableFilter<$PrismaModel>;
220
+ _max?: Prisma.NestedEnumRunStepsLevelNullableFilter<$PrismaModel>;
221
+ };
207
222
  export type DecimalNullableFilter<$PrismaModel = never> = {
208
223
  equals?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel> | null;
209
224
  in?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel> | null;
@@ -721,6 +736,12 @@ export type NestedEnumNativeExecutionModeFilter<$PrismaModel = never> = {
721
736
  notIn?: $Enums.NativeExecutionMode[] | Prisma.ListEnumNativeExecutionModeFieldRefInput<$PrismaModel>;
722
737
  not?: Prisma.NestedEnumNativeExecutionModeFilter<$PrismaModel> | $Enums.NativeExecutionMode;
723
738
  };
739
+ export type NestedEnumRunStepsLevelNullableFilter<$PrismaModel = never> = {
740
+ equals?: $Enums.RunStepsLevel | Prisma.EnumRunStepsLevelFieldRefInput<$PrismaModel> | null;
741
+ in?: $Enums.RunStepsLevel[] | Prisma.ListEnumRunStepsLevelFieldRefInput<$PrismaModel> | null;
742
+ notIn?: $Enums.RunStepsLevel[] | Prisma.ListEnumRunStepsLevelFieldRefInput<$PrismaModel> | null;
743
+ not?: Prisma.NestedEnumRunStepsLevelNullableFilter<$PrismaModel> | $Enums.RunStepsLevel | null;
744
+ };
724
745
  export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
725
746
  equals?: string | Prisma.StringFieldRefInput<$PrismaModel>;
726
747
  in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>;
@@ -854,6 +875,15 @@ export type NestedEnumNativeExecutionModeWithAggregatesFilter<$PrismaModel = nev
854
875
  _min?: Prisma.NestedEnumNativeExecutionModeFilter<$PrismaModel>;
855
876
  _max?: Prisma.NestedEnumNativeExecutionModeFilter<$PrismaModel>;
856
877
  };
878
+ export type NestedEnumRunStepsLevelNullableWithAggregatesFilter<$PrismaModel = never> = {
879
+ equals?: $Enums.RunStepsLevel | Prisma.EnumRunStepsLevelFieldRefInput<$PrismaModel> | null;
880
+ in?: $Enums.RunStepsLevel[] | Prisma.ListEnumRunStepsLevelFieldRefInput<$PrismaModel> | null;
881
+ notIn?: $Enums.RunStepsLevel[] | Prisma.ListEnumRunStepsLevelFieldRefInput<$PrismaModel> | null;
882
+ not?: Prisma.NestedEnumRunStepsLevelNullableWithAggregatesFilter<$PrismaModel> | $Enums.RunStepsLevel | null;
883
+ _count?: Prisma.NestedIntNullableFilter<$PrismaModel>;
884
+ _min?: Prisma.NestedEnumRunStepsLevelNullableFilter<$PrismaModel>;
885
+ _max?: Prisma.NestedEnumRunStepsLevelNullableFilter<$PrismaModel>;
886
+ };
857
887
  export type NestedDecimalNullableFilter<$PrismaModel = never> = {
858
888
  equals?: runtime.Decimal | runtime.DecimalJsLike | number | string | Prisma.DecimalFieldRefInput<$PrismaModel> | null;
859
889
  in?: runtime.Decimal[] | runtime.DecimalJsLike[] | number[] | string[] | Prisma.ListDecimalFieldRefInput<$PrismaModel> | null;
@@ -3,6 +3,12 @@ export declare const AgentKind: {
3
3
  readonly coding: "coding";
4
4
  };
5
5
  export type AgentKind = (typeof AgentKind)[keyof typeof AgentKind];
6
+ export declare const RunStepsLevel: {
7
+ readonly off: "off";
8
+ readonly summary: "summary";
9
+ readonly detail: "detail";
10
+ };
11
+ export type RunStepsLevel = (typeof RunStepsLevel)[keyof typeof RunStepsLevel];
6
12
  export declare const NativeExecutionMode: {
7
13
  readonly control_plane: "control_plane";
8
14
  readonly sandbox: "sandbox";
@@ -11,6 +11,11 @@ export const AgentKind = {
11
11
  native: 'native',
12
12
  coding: 'coding'
13
13
  };
14
+ export const RunStepsLevel = {
15
+ off: 'off',
16
+ summary: 'summary',
17
+ detail: 'detail'
18
+ };
14
19
  export const NativeExecutionMode = {
15
20
  control_plane: 'control_plane',
16
21
  sandbox: 'sandbox'
@@ -260,6 +260,17 @@ export interface PrismaClient<in LogOpts extends Prisma.LogLevel = never, in out
260
260
  get runTranscript(): Prisma.RunTranscriptDelegate<ExtArgs, {
261
261
  omit: OmitOpts;
262
262
  }>;
263
+ /**
264
+ * `prisma.runStep`: Exposes CRUD operations for the **RunStep** model.
265
+ * Example usage:
266
+ * ```ts
267
+ * // Fetch zero or more RunSteps
268
+ * const runSteps = await prisma.runStep.findMany()
269
+ * ```
270
+ */
271
+ get runStep(): Prisma.RunStepDelegate<ExtArgs, {
272
+ omit: OmitOpts;
273
+ }>;
263
274
  /**
264
275
  * `prisma.codingService`: Exposes CRUD operations for the **CodingService** model.
265
276
  * Example usage: