@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.
- package/deploy/local/docker-compose.quickstart-coding.yml +5 -0
- package/dist/claude-plugin/command.d.ts +4 -1
- package/dist/claude-plugin/command.js +7 -4
- package/dist/cli.js +24 -0
- package/dist/coding/transcript/recorder.js +2 -8
- package/dist/core/dispatch.js +3 -0
- package/dist/core/engine-native.d.ts +2 -0
- package/dist/core/engine-native.js +13 -0
- package/dist/core/error-code.js +1 -0
- package/dist/core/listen-address.d.ts +10 -0
- package/dist/core/listen-address.js +14 -0
- package/dist/core/power-idle.d.ts +8 -2
- package/dist/core/power-idle.js +17 -3
- package/dist/core/power.d.ts +5 -0
- package/dist/core/power.js +5 -0
- package/dist/core/retention-days.d.ts +5 -0
- package/dist/core/retention-days.js +14 -0
- package/dist/core/run-steps/config.d.ts +25 -0
- package/dist/core/run-steps/config.js +40 -0
- package/dist/core/run-steps/control-chars.d.ts +16 -0
- package/dist/core/run-steps/control-chars.js +26 -0
- package/dist/core/run-steps/detail.d.ts +32 -0
- package/dist/core/run-steps/detail.js +240 -0
- package/dist/core/run-steps/extract.d.ts +52 -0
- package/dist/core/run-steps/extract.js +467 -0
- package/dist/core/run-steps/ingress.d.ts +51 -0
- package/dist/core/run-steps/ingress.js +160 -0
- package/dist/core/run-steps/last-step.d.ts +15 -0
- package/dist/core/run-steps/last-step.js +35 -0
- package/dist/core/run-steps/native.d.ts +48 -0
- package/dist/core/run-steps/native.js +164 -0
- package/dist/core/run-steps/seen.d.ts +16 -0
- package/dist/core/run-steps/seen.js +35 -0
- package/dist/core/run-steps/summarize.d.ts +57 -0
- package/dist/core/run-steps/summarize.js +304 -0
- package/dist/core/run-steps/sweep.d.ts +21 -0
- package/dist/core/run-steps/sweep.js +41 -0
- package/dist/core/run-steps/types.d.ts +89 -0
- package/dist/core/run-steps/types.js +39 -0
- package/dist/core/run-steps/wire.d.ts +35 -0
- package/dist/core/run-steps/wire.js +47 -0
- package/dist/core/run-steps/writer.d.ts +49 -0
- package/dist/core/run-steps/writer.js +279 -0
- package/dist/core/runner.d.ts +4 -0
- package/dist/core/runner.js +15 -1
- package/dist/generated/prisma/browser.d.ts +8 -0
- package/dist/generated/prisma/client.d.ts +8 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +30 -0
- package/dist/generated/prisma/enums.d.ts +6 -0
- package/dist/generated/prisma/enums.js +5 -0
- package/dist/generated/prisma/internal/class.d.ts +11 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +103 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +19 -1
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +19 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +19 -1
- package/dist/generated/prisma/models/Agent.d.ts +101 -1
- package/dist/generated/prisma/models/CodingProxySession.d.ts +41 -1
- package/dist/generated/prisma/models/Run.d.ts +503 -1
- package/dist/generated/prisma/models/RunStep.d.ts +1453 -0
- package/dist/generated/prisma/models/RunStep.js +1 -0
- package/dist/generated/prisma/models.d.ts +1 -0
- package/dist/help-index.json +102 -14
- package/dist/mcp/auth/access.d.ts +1 -0
- package/dist/mcp/context.d.ts +3 -0
- package/dist/mcp/index.js +10 -0
- package/dist/mcp/tools/agents.js +37 -7
- package/dist/mcp/tools/power.js +2 -2
- package/dist/mcp/tools/run-access.d.ts +7 -0
- package/dist/mcp/tools/run-access.js +14 -0
- package/dist/mcp/tools/run-steps.d.ts +16 -0
- package/dist/mcp/tools/run-steps.js +85 -0
- package/dist/mcp/tools/runs.js +10 -2
- package/dist/native-worker/gateway-server.d.ts +4 -1
- package/dist/native-worker/gateway-server.js +10 -2
- package/dist/native-worker/gateway.d.ts +3 -0
- package/dist/native-worker/gateway.js +8 -0
- package/dist/native-worker/http-server.d.ts +2 -4
- package/dist/native-worker/http-server.js +2 -8
- package/dist/providers/coding-proxy/memory-ledger.js +1 -0
- package/dist/providers/coding-proxy/prisma-ledger.js +9 -3
- package/dist/providers/coding-proxy/proxy.d.ts +26 -0
- package/dist/providers/coding-proxy/proxy.js +133 -42
- package/dist/providers/coding-proxy/run-step-forwarder.d.ts +42 -0
- package/dist/providers/coding-proxy/run-step-forwarder.js +206 -0
- package/dist/providers/coding-proxy/runtime.d.ts +7 -0
- package/dist/providers/coding-proxy/runtime.js +55 -3
- package/dist/providers/coding-proxy/types.d.ts +5 -0
- package/dist/providers/engine/types.d.ts +26 -0
- package/dist/providers/executor/container.d.ts +14 -0
- package/dist/providers/executor/container.js +8 -1
- package/dist/providers/jobs/kubernetes.d.ts +8 -0
- package/dist/providers/jobs/kubernetes.js +22 -4
- package/dist/quickstart-images.json +1 -1
- package/dist/serve.js +26 -18
- package/dist/viewer/api-schema.d.ts +66 -0
- package/dist/viewer/api-schema.js +31 -0
- package/dist/viewer/openapi.js +2 -1
- package/dist/viewer/run-detail.d.ts +2 -0
- package/dist/viewer/run-detail.js +26 -1
- package/docs/claude-code-plugin.md +57 -13
- package/docs/coding-agent-setup.md +17 -0
- package/docs/coding-worker-isolation.md +16 -0
- package/docs/debugging-agent-runs.md +112 -0
- package/docs/getting-started-gke.md +28 -7
- package/docs/mcp-servers.md +9 -0
- package/docs/native-sandbox.md +6 -0
- package/docs/security-deployment.md +51 -0
- package/docs/viewer-api.md +21 -12
- package/docs/viewer-api.openapi.yml +78 -2
- package/help/admin-viewer.md +1 -0
- package/help/claude-code-run-pane.md +27 -4
- package/help/deploy-gke.md +5 -0
- package/help/errors/coding-mcp-gateway-unavailable.md +46 -0
- package/help/low-power-mode.md +19 -8
- package/help/mcp.md +2 -0
- package/help/resume-runs-locally.md +4 -0
- package/help/run-steps.md +140 -0
- package/package.json +1 -1
- package/prisma/migrations/20261110000000_run_steps/migration.sql +26 -0
- package/prisma/migrations/20261111000000_run_steps_viewer_notify/migration.sql +59 -0
- package/prisma/migrations/20261112000000_run_steps_detail/migration.sql +9 -0
- 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
|
+
}
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -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
|
package/dist/core/runner.js
CHANGED
|
@@ -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({
|
|
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:
|