@tangle-network/agent-runtime 0.17.2 → 0.19.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/README.md +0 -1
- package/dist/agent.d.ts +3 -165
- package/dist/agent.js +0 -108
- package/dist/agent.js.map +1 -1
- package/dist/chunk-RZAOYKCO.js +51 -0
- package/dist/chunk-RZAOYKCO.js.map +1 -0
- package/dist/chunk-XLWPTPRP.js +52 -0
- package/dist/chunk-XLWPTPRP.js.map +1 -0
- package/dist/index.d.ts +4 -137
- package/dist/index.js +9 -45
- package/dist/index.js.map +1 -1
- package/dist/loops.d.ts +153 -0
- package/dist/loops.js +379 -0
- package/dist/loops.js.map +1 -0
- package/dist/profiles.d.ts +133 -0
- package/dist/profiles.js +249 -0
- package/dist/profiles.js.map +1 -0
- package/dist/runtime-run-4pbY3Jq5.d.ts +137 -0
- package/dist/{types-ByIhNRFk.d.ts → types-DlyPgeI0.d.ts} +1 -1
- package/dist/types-EKcAHfxI.d.ts +225 -0
- package/package.json +32 -11
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { AgentProfile } from '@tangle-network/sandbox';
|
|
2
|
+
import { O as OutputAdapter, V as Validator, A as AgentRunSpec, a as Driver } from './types-EKcAHfxI.js';
|
|
3
|
+
import './runtime-run-4pbY3Jq5.js';
|
|
4
|
+
import './types-DlyPgeI0.js';
|
|
5
|
+
import '@tangle-network/agent-eval';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @experimental
|
|
9
|
+
*
|
|
10
|
+
* `coderProfile` — opinionated preset for code-modification tasks.
|
|
11
|
+
*
|
|
12
|
+
* The agent is told to:
|
|
13
|
+
* - work on a fresh branch inside the sandbox workspace
|
|
14
|
+
* - keep the patch minimal (under `maxDiffLines`)
|
|
15
|
+
* - avoid `forbiddenPaths`
|
|
16
|
+
* - run `testCmd` and `typecheckCmd`
|
|
17
|
+
* - emit a final JSON result the output adapter parses
|
|
18
|
+
*
|
|
19
|
+
* The profile is stateless and agent-agnostic — `harness` selects the
|
|
20
|
+
* sandbox-SDK backend (`claude-code`, `codex`, `opencode/*`). For
|
|
21
|
+
* heterogeneous fanout, use `multiHarnessCoderFanout`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** @experimental */
|
|
25
|
+
interface CoderTask {
|
|
26
|
+
/** What the agent must accomplish. Free-form prose. */
|
|
27
|
+
goal: string;
|
|
28
|
+
/** Absolute path inside the sandbox where the repo lives. */
|
|
29
|
+
repoRoot: string;
|
|
30
|
+
/** Default `main`. The branch the agent diffs against. */
|
|
31
|
+
baseBranch?: string;
|
|
32
|
+
/** Default `pnpm test --run`. */
|
|
33
|
+
testCmd?: string;
|
|
34
|
+
/** Default `pnpm typecheck`. */
|
|
35
|
+
typecheckCmd?: string;
|
|
36
|
+
/** Files the agent may inspect for context. Surfaced verbatim in the prompt. */
|
|
37
|
+
contextFiles?: string[];
|
|
38
|
+
/**
|
|
39
|
+
* Paths the agent must not touch. Validator hard-fails on any match.
|
|
40
|
+
* Use glob-free literal path prefixes for unambiguous enforcement.
|
|
41
|
+
*/
|
|
42
|
+
forbiddenPaths?: string[];
|
|
43
|
+
/** Default 400. Hard cap; validator hard-fails when exceeded. */
|
|
44
|
+
maxDiffLines?: number;
|
|
45
|
+
}
|
|
46
|
+
/** @experimental */
|
|
47
|
+
interface CoderOutput {
|
|
48
|
+
/** Branch the agent wrote the patch on. */
|
|
49
|
+
branch: string;
|
|
50
|
+
/** Unified diff (`git diff <base>..HEAD`). */
|
|
51
|
+
patch: string;
|
|
52
|
+
testResult: {
|
|
53
|
+
passed: boolean;
|
|
54
|
+
output: string;
|
|
55
|
+
};
|
|
56
|
+
typecheckResult: {
|
|
57
|
+
passed: boolean;
|
|
58
|
+
output: string;
|
|
59
|
+
};
|
|
60
|
+
diffStats: {
|
|
61
|
+
filesChanged: number;
|
|
62
|
+
insertions: number;
|
|
63
|
+
deletions: number;
|
|
64
|
+
};
|
|
65
|
+
/** Optional reviewer commentary surfaced by the agent. */
|
|
66
|
+
reviewerNotes?: string;
|
|
67
|
+
}
|
|
68
|
+
/** @experimental */
|
|
69
|
+
interface CoderProfileOptions {
|
|
70
|
+
/** Sandbox-SDK backend.type. Default `'claude-code'`. */
|
|
71
|
+
harness?: string;
|
|
72
|
+
/** Default model id passed in `AgentProfile.model.default`. */
|
|
73
|
+
model?: string;
|
|
74
|
+
/** Custom system prompt replacement. Default = built-in coder preset. */
|
|
75
|
+
systemPrompt?: string;
|
|
76
|
+
/** Stable name for `AgentRunSpec.name`. Default = `coder-${harness}`. */
|
|
77
|
+
name?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build a coder preset.
|
|
81
|
+
*
|
|
82
|
+
* `validator` enforces test + typecheck + a 400-line default diff cap. For
|
|
83
|
+
* per-task `forbiddenPaths` / `maxDiffLines` enforcement, pass `task` here
|
|
84
|
+
* — the returned validator closes over its constraints. Without a task
|
|
85
|
+
* the validator falls back to the default cap and skips path enforcement.
|
|
86
|
+
*
|
|
87
|
+
* @experimental
|
|
88
|
+
*/
|
|
89
|
+
declare function coderProfile(options?: CoderProfileOptions & {
|
|
90
|
+
task?: CoderTask;
|
|
91
|
+
}): {
|
|
92
|
+
profile: AgentProfile;
|
|
93
|
+
taskToPrompt: (task: CoderTask) => string;
|
|
94
|
+
output: OutputAdapter<CoderOutput>;
|
|
95
|
+
validator: Validator<CoderOutput>;
|
|
96
|
+
agentRunSpec: AgentRunSpec<CoderTask>;
|
|
97
|
+
};
|
|
98
|
+
/** @experimental */
|
|
99
|
+
interface MultiHarnessCoderFanoutOptions {
|
|
100
|
+
/**
|
|
101
|
+
* Sandbox-SDK backend.type identifiers, one per parallel agent. Default:
|
|
102
|
+
* `['claude-code', 'codex', 'opencode/zai-coding-plan/glm-5.1']`.
|
|
103
|
+
*/
|
|
104
|
+
harnesses?: string[];
|
|
105
|
+
/** Optional per-harness model override. Indexed parallel to `harnesses`. */
|
|
106
|
+
models?: (string | undefined)[];
|
|
107
|
+
}
|
|
108
|
+
/** @experimental */
|
|
109
|
+
declare function multiHarnessCoderFanout(options?: MultiHarnessCoderFanoutOptions): {
|
|
110
|
+
agentRuns: AgentRunSpec<CoderTask>[];
|
|
111
|
+
output: OutputAdapter<CoderOutput>;
|
|
112
|
+
validator: Validator<CoderOutput>;
|
|
113
|
+
driver: Driver<CoderTask, CoderOutput, 'pick-winner' | 'fail'>;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Build a validator that closes over a specific `CoderTask`'s constraints.
|
|
117
|
+
*
|
|
118
|
+
* Checks in order:
|
|
119
|
+
* 1. Forbidden-path: any `+++` / `---` header in the patch matching a
|
|
120
|
+
* path prefix in `task.forbiddenPaths` fails hard.
|
|
121
|
+
* 2. Diff size: line count above `task.maxDiffLines` (default 400) fails
|
|
122
|
+
* hard; below cap, the score shrinks linearly.
|
|
123
|
+
* 3. Tests: `output.testResult.passed` must be `true`.
|
|
124
|
+
* 4. Typecheck: `output.typecheckResult.passed` must be `true`.
|
|
125
|
+
*
|
|
126
|
+
* Aggregate score: `0.5 * tests + 0.3 * typecheck + 0.2 * (1 - diffLines/maxDiff)`.
|
|
127
|
+
* `valid` is the conjunction of all four.
|
|
128
|
+
*
|
|
129
|
+
* @experimental
|
|
130
|
+
*/
|
|
131
|
+
declare function createCoderValidator(task: CoderTask): Validator<CoderOutput>;
|
|
132
|
+
|
|
133
|
+
export { type CoderOutput, type CoderProfileOptions, type CoderTask, type MultiHarnessCoderFanoutOptions, coderProfile, createCoderValidator, multiHarnessCoderFanout };
|
package/dist/profiles.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFanoutVoteDriver
|
|
3
|
+
} from "./chunk-XLWPTPRP.js";
|
|
4
|
+
import "./chunk-RZAOYKCO.js";
|
|
5
|
+
import "./chunk-DGUM43GV.js";
|
|
6
|
+
|
|
7
|
+
// src/profiles/coder.ts
|
|
8
|
+
var DEFAULT_MAX_DIFF_LINES = 400;
|
|
9
|
+
function coderProfile(options = {}) {
|
|
10
|
+
const harness = options.harness ?? "claude-code";
|
|
11
|
+
const name = options.name ?? `coder-${harness}`;
|
|
12
|
+
const systemPrompt = options.systemPrompt ?? DEFAULT_CODER_SYSTEM_PROMPT;
|
|
13
|
+
const profile = {
|
|
14
|
+
name,
|
|
15
|
+
description: "Code-modification agent. Minimal-diff worktree-based coder.",
|
|
16
|
+
prompt: { systemPrompt },
|
|
17
|
+
model: options.model ? { default: options.model } : void 0,
|
|
18
|
+
tools: { git: true, fs: true, shell: true, test_runner: true },
|
|
19
|
+
metadata: { backendType: harness, role: "coder" }
|
|
20
|
+
};
|
|
21
|
+
const output = { parse: parseCoderEvents };
|
|
22
|
+
const validator = options.task ? createCoderValidator(options.task) : createCoderValidator({
|
|
23
|
+
goal: "",
|
|
24
|
+
repoRoot: "",
|
|
25
|
+
forbiddenPaths: [],
|
|
26
|
+
maxDiffLines: DEFAULT_MAX_DIFF_LINES
|
|
27
|
+
});
|
|
28
|
+
const agentRunSpec = {
|
|
29
|
+
name,
|
|
30
|
+
profile,
|
|
31
|
+
taskToPrompt: formatCoderPrompt
|
|
32
|
+
};
|
|
33
|
+
return { profile, taskToPrompt: formatCoderPrompt, output, validator, agentRunSpec };
|
|
34
|
+
}
|
|
35
|
+
function multiHarnessCoderFanout(options = {}) {
|
|
36
|
+
const harnesses = options.harnesses && options.harnesses.length > 0 ? options.harnesses : ["claude-code", "codex", "opencode/zai-coding-plan/glm-5.1"];
|
|
37
|
+
const models = options.models ?? [];
|
|
38
|
+
const agentRuns = harnesses.map((harness, i) => {
|
|
39
|
+
const { agentRunSpec } = coderProfile({ harness, model: models[i] });
|
|
40
|
+
return agentRunSpec;
|
|
41
|
+
});
|
|
42
|
+
const { output, validator } = coderProfile();
|
|
43
|
+
const driver = createFanoutVoteDriver({ n: harnesses.length });
|
|
44
|
+
return { agentRuns, output, validator, driver };
|
|
45
|
+
}
|
|
46
|
+
var DEFAULT_CODER_SYSTEM_PROMPT = [
|
|
47
|
+
"You are a coder agent operating inside an isolated sandbox workspace.",
|
|
48
|
+
"Your job is to deliver a minimal, correct patch for the user-supplied goal.",
|
|
49
|
+
"",
|
|
50
|
+
"Hard rules:",
|
|
51
|
+
" 1. Work on a fresh branch off the supplied base. Do not mutate the base branch.",
|
|
52
|
+
" 2. Never touch a forbidden path. The user will list them explicitly.",
|
|
53
|
+
" 3. Keep the diff under the max-diff cap. Prefer the smallest change that ships.",
|
|
54
|
+
" 4. Run the supplied test and typecheck commands before declaring done.",
|
|
55
|
+
" 5. If either command fails, fix the cause \u2014 do not weaken the test or hide the error.",
|
|
56
|
+
"",
|
|
57
|
+
"When you finish, emit a single final structured message of the shape:",
|
|
58
|
+
" ```json",
|
|
59
|
+
' { "branch": "<branch-name>",',
|
|
60
|
+
' "patch": "<unified-diff>",',
|
|
61
|
+
' "testResult": { "passed": <bool>, "output": "<stdout/stderr>" },',
|
|
62
|
+
' "typecheckResult": { "passed": <bool>, "output": "<stdout/stderr>" },',
|
|
63
|
+
' "diffStats": { "filesChanged": <int>, "insertions": <int>, "deletions": <int> },',
|
|
64
|
+
' "reviewerNotes": "<optional commentary>" }',
|
|
65
|
+
" ```"
|
|
66
|
+
].join("\n");
|
|
67
|
+
function formatCoderPrompt(task) {
|
|
68
|
+
const base = task.baseBranch ?? "main";
|
|
69
|
+
const testCmd = task.testCmd ?? "pnpm test --run";
|
|
70
|
+
const typecheckCmd = task.typecheckCmd ?? "pnpm typecheck";
|
|
71
|
+
const maxDiff = task.maxDiffLines ?? DEFAULT_MAX_DIFF_LINES;
|
|
72
|
+
const forbidden = task.forbiddenPaths?.length ? task.forbiddenPaths.join(", ") : "(none)";
|
|
73
|
+
const context = task.contextFiles?.length ? task.contextFiles.map((f) => ` - ${f}`).join("\n") : " (none)";
|
|
74
|
+
return [
|
|
75
|
+
`Goal: ${task.goal}`,
|
|
76
|
+
`Repo: ${task.repoRoot}`,
|
|
77
|
+
`Base branch: ${base}`,
|
|
78
|
+
`Run tests with: ${testCmd}`,
|
|
79
|
+
`Run typecheck with: ${typecheckCmd}`,
|
|
80
|
+
`Forbidden paths: ${forbidden}`,
|
|
81
|
+
`Max diff lines: ${maxDiff}`,
|
|
82
|
+
"Context files:",
|
|
83
|
+
context,
|
|
84
|
+
"",
|
|
85
|
+
"Produce a minimal patch on a fresh branch. Run tests and typecheck before",
|
|
86
|
+
"returning. Emit the final JSON result block exactly as instructed."
|
|
87
|
+
].join("\n");
|
|
88
|
+
}
|
|
89
|
+
function parseCoderEvents(events) {
|
|
90
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
91
|
+
const event = events[i];
|
|
92
|
+
if (!event) continue;
|
|
93
|
+
const type = String(event.type ?? "");
|
|
94
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
95
|
+
if (type === "result" || type === "final" || type === "coder.result") {
|
|
96
|
+
const direct = coerceCoderOutput(data.result ?? data.output ?? data);
|
|
97
|
+
if (direct) return direct;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
101
|
+
const event = events[i];
|
|
102
|
+
if (!event) continue;
|
|
103
|
+
const data = isRecord(event.data) ? event.data : {};
|
|
104
|
+
const text = pickString(data.text) ?? pickString(data.delta);
|
|
105
|
+
if (!text) continue;
|
|
106
|
+
const fenced = extractFencedJson(text);
|
|
107
|
+
if (!fenced) continue;
|
|
108
|
+
const coerced = coerceCoderOutput(fenced);
|
|
109
|
+
if (coerced) return coerced;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
branch: "",
|
|
113
|
+
patch: "",
|
|
114
|
+
testResult: { passed: false, output: "" },
|
|
115
|
+
typecheckResult: { passed: false, output: "" },
|
|
116
|
+
diffStats: { filesChanged: 0, insertions: 0, deletions: 0 }
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function createCoderValidator(task) {
|
|
120
|
+
const maxDiff = task.maxDiffLines ?? DEFAULT_MAX_DIFF_LINES;
|
|
121
|
+
const forbidden = task.forbiddenPaths ?? [];
|
|
122
|
+
return {
|
|
123
|
+
async validate(output) {
|
|
124
|
+
const scores = {};
|
|
125
|
+
const notes = [];
|
|
126
|
+
let pass = true;
|
|
127
|
+
const touched = touchedPathsFromPatch(output.patch);
|
|
128
|
+
const touchedForbidden = forbidden.filter((path) => {
|
|
129
|
+
const prefix = path.endsWith("/") ? path : `${path}/`;
|
|
130
|
+
const exact = prefix.slice(0, -1);
|
|
131
|
+
return touched.some((p) => p === exact || p.startsWith(prefix));
|
|
132
|
+
});
|
|
133
|
+
if (touchedForbidden.length > 0) {
|
|
134
|
+
pass = false;
|
|
135
|
+
scores.forbiddenPath = 0;
|
|
136
|
+
notes.push(`touched forbidden paths: ${touchedForbidden.join(", ")}`);
|
|
137
|
+
} else {
|
|
138
|
+
scores.forbiddenPath = 1;
|
|
139
|
+
}
|
|
140
|
+
const diffLines = countDiffLines(output.patch);
|
|
141
|
+
if (diffLines > maxDiff) {
|
|
142
|
+
pass = false;
|
|
143
|
+
scores.diffSize = 0;
|
|
144
|
+
notes.push(`diff ${diffLines} lines exceeds cap ${maxDiff}`);
|
|
145
|
+
} else {
|
|
146
|
+
scores.diffSize = maxDiff === 0 ? 0 : Math.max(0, 1 - diffLines / maxDiff);
|
|
147
|
+
}
|
|
148
|
+
scores.tests = output.testResult.passed ? 1 : 0;
|
|
149
|
+
scores.typecheck = output.typecheckResult.passed ? 1 : 0;
|
|
150
|
+
if (!output.testResult.passed) {
|
|
151
|
+
pass = false;
|
|
152
|
+
notes.push("tests failed");
|
|
153
|
+
}
|
|
154
|
+
if (!output.typecheckResult.passed) {
|
|
155
|
+
pass = false;
|
|
156
|
+
notes.push("typecheck failed");
|
|
157
|
+
}
|
|
158
|
+
const score = 0.5 * scores.tests + 0.3 * scores.typecheck + 0.2 * scores.diffSize;
|
|
159
|
+
const verdict = {
|
|
160
|
+
valid: pass,
|
|
161
|
+
score: Number.isFinite(score) ? score : 0,
|
|
162
|
+
scores
|
|
163
|
+
};
|
|
164
|
+
if (notes.length > 0) verdict.notes = notes.join("; ");
|
|
165
|
+
return verdict;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function touchedPathsFromPatch(patch) {
|
|
170
|
+
const out = /* @__PURE__ */ new Set();
|
|
171
|
+
for (const line of patch.split(/\r?\n/)) {
|
|
172
|
+
if (line.startsWith("+++ ") || line.startsWith("--- ")) {
|
|
173
|
+
const rest = line.slice(4).trim();
|
|
174
|
+
if (rest === "/dev/null") continue;
|
|
175
|
+
const stripped = rest.startsWith("a/") || rest.startsWith("b/") ? rest.slice(2) : rest;
|
|
176
|
+
out.add(stripped);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return [...out];
|
|
180
|
+
}
|
|
181
|
+
function countDiffLines(patch) {
|
|
182
|
+
let count = 0;
|
|
183
|
+
for (const line of patch.split(/\r?\n/)) {
|
|
184
|
+
if ((line.startsWith("+") || line.startsWith("-")) && !line.startsWith("+++") && !line.startsWith("---")) {
|
|
185
|
+
count += 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return count;
|
|
189
|
+
}
|
|
190
|
+
function isRecord(value) {
|
|
191
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
192
|
+
}
|
|
193
|
+
function pickString(value) {
|
|
194
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
195
|
+
}
|
|
196
|
+
function extractFencedJson(text) {
|
|
197
|
+
const match = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
198
|
+
if (!match) return void 0;
|
|
199
|
+
const body = (match[1] ?? "").trim();
|
|
200
|
+
if (!body) return void 0;
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(body);
|
|
203
|
+
} catch {
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function coerceCoderOutput(value) {
|
|
208
|
+
if (!isRecord(value)) return void 0;
|
|
209
|
+
const branch = pickString(value.branch);
|
|
210
|
+
const patch = pickString(value.patch) ?? "";
|
|
211
|
+
if (branch === void 0) return void 0;
|
|
212
|
+
const testResult = coerceCmdResult(value.testResult);
|
|
213
|
+
const typecheckResult = coerceCmdResult(value.typecheckResult);
|
|
214
|
+
const diffStats = coerceDiffStats(value.diffStats);
|
|
215
|
+
return {
|
|
216
|
+
branch,
|
|
217
|
+
patch,
|
|
218
|
+
testResult,
|
|
219
|
+
typecheckResult,
|
|
220
|
+
diffStats,
|
|
221
|
+
reviewerNotes: pickString(value.reviewerNotes)
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function coerceCmdResult(value) {
|
|
225
|
+
if (!isRecord(value)) return { passed: false, output: "" };
|
|
226
|
+
return {
|
|
227
|
+
passed: value.passed === true,
|
|
228
|
+
output: pickString(value.output) ?? ""
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function coerceDiffStats(value) {
|
|
232
|
+
if (!isRecord(value)) return { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
233
|
+
return {
|
|
234
|
+
filesChanged: toFiniteInt(value.filesChanged),
|
|
235
|
+
insertions: toFiniteInt(value.insertions),
|
|
236
|
+
deletions: toFiniteInt(value.deletions)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function toFiniteInt(value) {
|
|
240
|
+
if (typeof value !== "number") return 0;
|
|
241
|
+
if (!Number.isFinite(value)) return 0;
|
|
242
|
+
return Math.max(0, Math.trunc(value));
|
|
243
|
+
}
|
|
244
|
+
export {
|
|
245
|
+
coderProfile,
|
|
246
|
+
createCoderValidator,
|
|
247
|
+
multiHarnessCoderFanout
|
|
248
|
+
};
|
|
249
|
+
//# sourceMappingURL=profiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/profiles/coder.ts"],"sourcesContent":["/**\n * @experimental\n *\n * `coderProfile` — opinionated preset for code-modification tasks.\n *\n * The agent is told to:\n * - work on a fresh branch inside the sandbox workspace\n * - keep the patch minimal (under `maxDiffLines`)\n * - avoid `forbiddenPaths`\n * - run `testCmd` and `typecheckCmd`\n * - emit a final JSON result the output adapter parses\n *\n * The profile is stateless and agent-agnostic — `harness` selects the\n * sandbox-SDK backend (`claude-code`, `codex`, `opencode/*`). For\n * heterogeneous fanout, use `multiHarnessCoderFanout`.\n */\n\nimport type { AgentProfile, SandboxEvent } from '@tangle-network/sandbox'\nimport { createFanoutVoteDriver } from '../loops/drivers/fanout-vote'\nimport type { AgentRunSpec, DefaultVerdict, Driver, OutputAdapter, Validator } from '../loops/types'\n\nconst DEFAULT_MAX_DIFF_LINES = 400\n\n/** @experimental */\nexport interface CoderTask {\n /** What the agent must accomplish. Free-form prose. */\n goal: string\n /** Absolute path inside the sandbox where the repo lives. */\n repoRoot: string\n /** Default `main`. The branch the agent diffs against. */\n baseBranch?: string\n /** Default `pnpm test --run`. */\n testCmd?: string\n /** Default `pnpm typecheck`. */\n typecheckCmd?: string\n /** Files the agent may inspect for context. Surfaced verbatim in the prompt. */\n contextFiles?: string[]\n /**\n * Paths the agent must not touch. Validator hard-fails on any match.\n * Use glob-free literal path prefixes for unambiguous enforcement.\n */\n forbiddenPaths?: string[]\n /** Default 400. Hard cap; validator hard-fails when exceeded. */\n maxDiffLines?: number\n}\n\n/** @experimental */\nexport interface CoderOutput {\n /** Branch the agent wrote the patch on. */\n branch: string\n /** Unified diff (`git diff <base>..HEAD`). */\n patch: string\n testResult: { passed: boolean; output: string }\n typecheckResult: { passed: boolean; output: string }\n diffStats: { filesChanged: number; insertions: number; deletions: number }\n /** Optional reviewer commentary surfaced by the agent. */\n reviewerNotes?: string\n}\n\n/** @experimental */\nexport interface CoderProfileOptions {\n /** Sandbox-SDK backend.type. Default `'claude-code'`. */\n harness?: string\n /** Default model id passed in `AgentProfile.model.default`. */\n model?: string\n /** Custom system prompt replacement. Default = built-in coder preset. */\n systemPrompt?: string\n /** Stable name for `AgentRunSpec.name`. Default = `coder-${harness}`. */\n name?: string\n}\n\n/**\n * Build a coder preset.\n *\n * `validator` enforces test + typecheck + a 400-line default diff cap. For\n * per-task `forbiddenPaths` / `maxDiffLines` enforcement, pass `task` here\n * — the returned validator closes over its constraints. Without a task\n * the validator falls back to the default cap and skips path enforcement.\n *\n * @experimental\n */\nexport function coderProfile(options: CoderProfileOptions & { task?: CoderTask } = {}): {\n profile: AgentProfile\n taskToPrompt: (task: CoderTask) => string\n output: OutputAdapter<CoderOutput>\n validator: Validator<CoderOutput>\n agentRunSpec: AgentRunSpec<CoderTask>\n} {\n const harness = options.harness ?? 'claude-code'\n const name = options.name ?? `coder-${harness}`\n const systemPrompt = options.systemPrompt ?? DEFAULT_CODER_SYSTEM_PROMPT\n const profile: AgentProfile = {\n name,\n description: 'Code-modification agent. Minimal-diff worktree-based coder.',\n prompt: { systemPrompt },\n model: options.model ? { default: options.model } : undefined,\n tools: { git: true, fs: true, shell: true, test_runner: true },\n metadata: { backendType: harness, role: 'coder' },\n }\n const output: OutputAdapter<CoderOutput> = { parse: parseCoderEvents }\n const validator: Validator<CoderOutput> = options.task\n ? createCoderValidator(options.task)\n : createCoderValidator({\n goal: '',\n repoRoot: '',\n forbiddenPaths: [],\n maxDiffLines: DEFAULT_MAX_DIFF_LINES,\n })\n const agentRunSpec: AgentRunSpec<CoderTask> = {\n name,\n profile,\n taskToPrompt: formatCoderPrompt,\n }\n return { profile, taskToPrompt: formatCoderPrompt, output, validator, agentRunSpec }\n}\n\n/** @experimental */\nexport interface MultiHarnessCoderFanoutOptions {\n /**\n * Sandbox-SDK backend.type identifiers, one per parallel agent. Default:\n * `['claude-code', 'codex', 'opencode/zai-coding-plan/glm-5.1']`.\n */\n harnesses?: string[]\n /** Optional per-harness model override. Indexed parallel to `harnesses`. */\n models?: (string | undefined)[]\n}\n\n/** @experimental */\nexport function multiHarnessCoderFanout(options: MultiHarnessCoderFanoutOptions = {}): {\n agentRuns: AgentRunSpec<CoderTask>[]\n output: OutputAdapter<CoderOutput>\n validator: Validator<CoderOutput>\n driver: Driver<CoderTask, CoderOutput, 'pick-winner' | 'fail'>\n} {\n const harnesses =\n options.harnesses && options.harnesses.length > 0\n ? options.harnesses\n : ['claude-code', 'codex', 'opencode/zai-coding-plan/glm-5.1']\n const models = options.models ?? []\n const agentRuns = harnesses.map((harness, i) => {\n const { agentRunSpec } = coderProfile({ harness, model: models[i] })\n return agentRunSpec\n })\n const { output, validator } = coderProfile()\n const driver = createFanoutVoteDriver<CoderTask, CoderOutput>({ n: harnesses.length })\n return { agentRuns, output, validator, driver }\n}\n\nconst DEFAULT_CODER_SYSTEM_PROMPT = [\n 'You are a coder agent operating inside an isolated sandbox workspace.',\n 'Your job is to deliver a minimal, correct patch for the user-supplied goal.',\n '',\n 'Hard rules:',\n ' 1. Work on a fresh branch off the supplied base. Do not mutate the base branch.',\n ' 2. Never touch a forbidden path. The user will list them explicitly.',\n ' 3. Keep the diff under the max-diff cap. Prefer the smallest change that ships.',\n ' 4. Run the supplied test and typecheck commands before declaring done.',\n ' 5. If either command fails, fix the cause — do not weaken the test or hide the error.',\n '',\n 'When you finish, emit a single final structured message of the shape:',\n ' ```json',\n ' { \"branch\": \"<branch-name>\",',\n ' \"patch\": \"<unified-diff>\",',\n ' \"testResult\": { \"passed\": <bool>, \"output\": \"<stdout/stderr>\" },',\n ' \"typecheckResult\": { \"passed\": <bool>, \"output\": \"<stdout/stderr>\" },',\n ' \"diffStats\": { \"filesChanged\": <int>, \"insertions\": <int>, \"deletions\": <int> },',\n ' \"reviewerNotes\": \"<optional commentary>\" }',\n ' ```',\n].join('\\n')\n\nfunction formatCoderPrompt(task: CoderTask): string {\n const base = task.baseBranch ?? 'main'\n const testCmd = task.testCmd ?? 'pnpm test --run'\n const typecheckCmd = task.typecheckCmd ?? 'pnpm typecheck'\n const maxDiff = task.maxDiffLines ?? DEFAULT_MAX_DIFF_LINES\n const forbidden = task.forbiddenPaths?.length ? task.forbiddenPaths.join(', ') : '(none)'\n const context = task.contextFiles?.length\n ? task.contextFiles.map((f) => ` - ${f}`).join('\\n')\n : ' (none)'\n return [\n `Goal: ${task.goal}`,\n `Repo: ${task.repoRoot}`,\n `Base branch: ${base}`,\n `Run tests with: ${testCmd}`,\n `Run typecheck with: ${typecheckCmd}`,\n `Forbidden paths: ${forbidden}`,\n `Max diff lines: ${maxDiff}`,\n 'Context files:',\n context,\n '',\n 'Produce a minimal patch on a fresh branch. Run tests and typecheck before',\n 'returning. Emit the final JSON result block exactly as instructed.',\n ].join('\\n')\n}\n\n/**\n * Walk the event stream and return the last structured `coder.result` payload.\n *\n * The agent is instructed to emit a JSON block; in practice the sandbox SDK\n * lifts the structured payload onto `data.result` of a `result` / `final`\n * event. When the event stream does not contain a structured result, the\n * adapter scans text deltas for a fenced JSON block matching the expected\n * keys. Both shapes converge on `CoderOutput`.\n */\nfunction parseCoderEvents(events: SandboxEvent[]): CoderOutput {\n for (let i = events.length - 1; i >= 0; i -= 1) {\n const event = events[i]\n if (!event) continue\n const type = String(event.type ?? '')\n const data = isRecord(event.data) ? event.data : {}\n if (type === 'result' || type === 'final' || type === 'coder.result') {\n const direct = coerceCoderOutput(data.result ?? data.output ?? data)\n if (direct) return direct\n }\n }\n // Fallback: scan text deltas in reverse for a fenced JSON block.\n for (let i = events.length - 1; i >= 0; i -= 1) {\n const event = events[i]\n if (!event) continue\n const data = isRecord(event.data) ? event.data : {}\n const text = pickString(data.text) ?? pickString(data.delta)\n if (!text) continue\n const fenced = extractFencedJson(text)\n if (!fenced) continue\n const coerced = coerceCoderOutput(fenced)\n if (coerced) return coerced\n }\n return {\n branch: '',\n patch: '',\n testResult: { passed: false, output: '' },\n typecheckResult: { passed: false, output: '' },\n diffStats: { filesChanged: 0, insertions: 0, deletions: 0 },\n }\n}\n\n/**\n * Build a validator that closes over a specific `CoderTask`'s constraints.\n *\n * Checks in order:\n * 1. Forbidden-path: any `+++` / `---` header in the patch matching a\n * path prefix in `task.forbiddenPaths` fails hard.\n * 2. Diff size: line count above `task.maxDiffLines` (default 400) fails\n * hard; below cap, the score shrinks linearly.\n * 3. Tests: `output.testResult.passed` must be `true`.\n * 4. Typecheck: `output.typecheckResult.passed` must be `true`.\n *\n * Aggregate score: `0.5 * tests + 0.3 * typecheck + 0.2 * (1 - diffLines/maxDiff)`.\n * `valid` is the conjunction of all four.\n *\n * @experimental\n */\nexport function createCoderValidator(task: CoderTask): Validator<CoderOutput> {\n const maxDiff = task.maxDiffLines ?? DEFAULT_MAX_DIFF_LINES\n const forbidden = task.forbiddenPaths ?? []\n return {\n async validate(output) {\n const scores: Record<string, number> = {}\n const notes: string[] = []\n let pass = true\n\n const touched = touchedPathsFromPatch(output.patch)\n const touchedForbidden = forbidden.filter((path) => {\n const prefix = path.endsWith('/') ? path : `${path}/`\n const exact = prefix.slice(0, -1)\n return touched.some((p) => p === exact || p.startsWith(prefix))\n })\n if (touchedForbidden.length > 0) {\n pass = false\n scores.forbiddenPath = 0\n notes.push(`touched forbidden paths: ${touchedForbidden.join(', ')}`)\n } else {\n scores.forbiddenPath = 1\n }\n\n const diffLines = countDiffLines(output.patch)\n if (diffLines > maxDiff) {\n pass = false\n scores.diffSize = 0\n notes.push(`diff ${diffLines} lines exceeds cap ${maxDiff}`)\n } else {\n scores.diffSize = maxDiff === 0 ? 0 : Math.max(0, 1 - diffLines / maxDiff)\n }\n\n scores.tests = output.testResult.passed ? 1 : 0\n scores.typecheck = output.typecheckResult.passed ? 1 : 0\n if (!output.testResult.passed) {\n pass = false\n notes.push('tests failed')\n }\n if (!output.typecheckResult.passed) {\n pass = false\n notes.push('typecheck failed')\n }\n\n const score = 0.5 * scores.tests + 0.3 * scores.typecheck + 0.2 * scores.diffSize\n const verdict: DefaultVerdict = {\n valid: pass,\n score: Number.isFinite(score) ? score : 0,\n scores,\n }\n if (notes.length > 0) verdict.notes = notes.join('; ')\n return verdict\n },\n }\n}\n\nfunction touchedPathsFromPatch(patch: string): string[] {\n const out = new Set<string>()\n for (const line of patch.split(/\\r?\\n/)) {\n if (line.startsWith('+++ ') || line.startsWith('--- ')) {\n const rest = line.slice(4).trim()\n if (rest === '/dev/null') continue\n const stripped = rest.startsWith('a/') || rest.startsWith('b/') ? rest.slice(2) : rest\n out.add(stripped)\n }\n }\n return [...out]\n}\n\nfunction countDiffLines(patch: string): number {\n let count = 0\n for (const line of patch.split(/\\r?\\n/)) {\n if (\n (line.startsWith('+') || line.startsWith('-')) &&\n !line.startsWith('+++') &&\n !line.startsWith('---')\n ) {\n count += 1\n }\n }\n return count\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\nfunction pickString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\nfunction extractFencedJson(text: string): unknown | undefined {\n const match = text.match(/```(?:json)?\\s*([\\s\\S]*?)```/i)\n if (!match) return undefined\n const body = (match[1] ?? '').trim()\n if (!body) return undefined\n try {\n return JSON.parse(body)\n } catch {\n return undefined\n }\n}\n\nfunction coerceCoderOutput(value: unknown): CoderOutput | undefined {\n if (!isRecord(value)) return undefined\n const branch = pickString(value.branch)\n const patch = pickString(value.patch) ?? ''\n if (branch === undefined) return undefined\n const testResult = coerceCmdResult(value.testResult)\n const typecheckResult = coerceCmdResult(value.typecheckResult)\n const diffStats = coerceDiffStats(value.diffStats)\n return {\n branch,\n patch,\n testResult,\n typecheckResult,\n diffStats,\n reviewerNotes: pickString(value.reviewerNotes),\n }\n}\n\nfunction coerceCmdResult(value: unknown): { passed: boolean; output: string } {\n if (!isRecord(value)) return { passed: false, output: '' }\n return {\n passed: value.passed === true,\n output: pickString(value.output) ?? '',\n }\n}\n\nfunction coerceDiffStats(value: unknown): {\n filesChanged: number\n insertions: number\n deletions: number\n} {\n if (!isRecord(value)) return { filesChanged: 0, insertions: 0, deletions: 0 }\n return {\n filesChanged: toFiniteInt(value.filesChanged),\n insertions: toFiniteInt(value.insertions),\n deletions: toFiniteInt(value.deletions),\n }\n}\n\nfunction toFiniteInt(value: unknown): number {\n if (typeof value !== 'number') return 0\n if (!Number.isFinite(value)) return 0\n return Math.max(0, Math.trunc(value))\n}\n"],"mappings":";;;;;;;AAqBA,IAAM,yBAAyB;AA4DxB,SAAS,aAAa,UAAsD,CAAC,GAMlF;AACA,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,OAAO,QAAQ,QAAQ,SAAS,OAAO;AAC7C,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,UAAwB;AAAA,IAC5B;AAAA,IACA,aAAa;AAAA,IACb,QAAQ,EAAE,aAAa;AAAA,IACvB,OAAO,QAAQ,QAAQ,EAAE,SAAS,QAAQ,MAAM,IAAI;AAAA,IACpD,OAAO,EAAE,KAAK,MAAM,IAAI,MAAM,OAAO,MAAM,aAAa,KAAK;AAAA,IAC7D,UAAU,EAAE,aAAa,SAAS,MAAM,QAAQ;AAAA,EAClD;AACA,QAAM,SAAqC,EAAE,OAAO,iBAAiB;AACrE,QAAM,YAAoC,QAAQ,OAC9C,qBAAqB,QAAQ,IAAI,IACjC,qBAAqB;AAAA,IACnB,MAAM;AAAA,IACN,UAAU;AAAA,IACV,gBAAgB,CAAC;AAAA,IACjB,cAAc;AAAA,EAChB,CAAC;AACL,QAAM,eAAwC;AAAA,IAC5C;AAAA,IACA;AAAA,IACA,cAAc;AAAA,EAChB;AACA,SAAO,EAAE,SAAS,cAAc,mBAAmB,QAAQ,WAAW,aAAa;AACrF;AAcO,SAAS,wBAAwB,UAA0C,CAAC,GAKjF;AACA,QAAM,YACJ,QAAQ,aAAa,QAAQ,UAAU,SAAS,IAC5C,QAAQ,YACR,CAAC,eAAe,SAAS,kCAAkC;AACjE,QAAM,SAAS,QAAQ,UAAU,CAAC;AAClC,QAAM,YAAY,UAAU,IAAI,CAAC,SAAS,MAAM;AAC9C,UAAM,EAAE,aAAa,IAAI,aAAa,EAAE,SAAS,OAAO,OAAO,CAAC,EAAE,CAAC;AACnE,WAAO;AAAA,EACT,CAAC;AACD,QAAM,EAAE,QAAQ,UAAU,IAAI,aAAa;AAC3C,QAAM,SAAS,uBAA+C,EAAE,GAAG,UAAU,OAAO,CAAC;AACrF,SAAO,EAAE,WAAW,QAAQ,WAAW,OAAO;AAChD;AAEA,IAAM,8BAA8B;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,IAAI;AAEX,SAAS,kBAAkB,MAAyB;AAClD,QAAM,OAAO,KAAK,cAAc;AAChC,QAAM,UAAU,KAAK,WAAW;AAChC,QAAM,eAAe,KAAK,gBAAgB;AAC1C,QAAM,UAAU,KAAK,gBAAgB;AACrC,QAAM,YAAY,KAAK,gBAAgB,SAAS,KAAK,eAAe,KAAK,IAAI,IAAI;AACjF,QAAM,UAAU,KAAK,cAAc,SAC/B,KAAK,aAAa,IAAI,CAAC,MAAM,OAAO,CAAC,EAAE,EAAE,KAAK,IAAI,IAClD;AACJ,SAAO;AAAA,IACL,SAAS,KAAK,IAAI;AAAA,IAClB,SAAS,KAAK,QAAQ;AAAA,IACtB,gBAAgB,IAAI;AAAA,IACpB,mBAAmB,OAAO;AAAA,IAC1B,uBAAuB,YAAY;AAAA,IACnC,oBAAoB,SAAS;AAAA,IAC7B,mBAAmB,OAAO;AAAA,IAC1B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAWA,SAAS,iBAAiB,QAAqC;AAC7D,WAAS,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG;AAC9C,UAAM,QAAQ,OAAO,CAAC;AACtB,QAAI,CAAC,MAAO;AACZ,UAAM,OAAO,OAAO,MAAM,QAAQ,EAAE;AACpC,UAAM,OAAO,SAAS,MAAM,IAAI,IAAI,MAAM,OAAO,CAAC;AAClD,QAAI,SAAS,YAAY,SAAS,WAAW,SAAS,gBAAgB;AACpE,YAAM,SAAS,kBAAkB,KAAK,UAAU,KAAK,UAAU,IAAI;AACnE,UAAI,OAAQ,QAAO;AAAA,IACrB;AAAA,EACF;AAEA,WAAS,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG;AAC9C,UAAM,QAAQ,OAAO,CAAC;AACtB,QAAI,CAAC,MAAO;AACZ,UAAM,OAAO,SAAS,MAAM,IAAI,IAAI,MAAM,OAAO,CAAC;AAClD,UAAM,OAAO,WAAW,KAAK,IAAI,KAAK,WAAW,KAAK,KAAK;AAC3D,QAAI,CAAC,KAAM;AACX,UAAM,SAAS,kBAAkB,IAAI;AACrC,QAAI,CAAC,OAAQ;AACb,UAAM,UAAU,kBAAkB,MAAM;AACxC,QAAI,QAAS,QAAO;AAAA,EACtB;AACA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,YAAY,EAAE,QAAQ,OAAO,QAAQ,GAAG;AAAA,IACxC,iBAAiB,EAAE,QAAQ,OAAO,QAAQ,GAAG;AAAA,IAC7C,WAAW,EAAE,cAAc,GAAG,YAAY,GAAG,WAAW,EAAE;AAAA,EAC5D;AACF;AAkBO,SAAS,qBAAqB,MAAyC;AAC5E,QAAM,UAAU,KAAK,gBAAgB;AACrC,QAAM,YAAY,KAAK,kBAAkB,CAAC;AAC1C,SAAO;AAAA,IACL,MAAM,SAAS,QAAQ;AACrB,YAAM,SAAiC,CAAC;AACxC,YAAM,QAAkB,CAAC;AACzB,UAAI,OAAO;AAEX,YAAM,UAAU,sBAAsB,OAAO,KAAK;AAClD,YAAM,mBAAmB,UAAU,OAAO,CAAC,SAAS;AAClD,cAAM,SAAS,KAAK,SAAS,GAAG,IAAI,OAAO,GAAG,IAAI;AAClD,cAAM,QAAQ,OAAO,MAAM,GAAG,EAAE;AAChC,eAAO,QAAQ,KAAK,CAAC,MAAM,MAAM,SAAS,EAAE,WAAW,MAAM,CAAC;AAAA,MAChE,CAAC;AACD,UAAI,iBAAiB,SAAS,GAAG;AAC/B,eAAO;AACP,eAAO,gBAAgB;AACvB,cAAM,KAAK,4BAA4B,iBAAiB,KAAK,IAAI,CAAC,EAAE;AAAA,MACtE,OAAO;AACL,eAAO,gBAAgB;AAAA,MACzB;AAEA,YAAM,YAAY,eAAe,OAAO,KAAK;AAC7C,UAAI,YAAY,SAAS;AACvB,eAAO;AACP,eAAO,WAAW;AAClB,cAAM,KAAK,QAAQ,SAAS,sBAAsB,OAAO,EAAE;AAAA,MAC7D,OAAO;AACL,eAAO,WAAW,YAAY,IAAI,IAAI,KAAK,IAAI,GAAG,IAAI,YAAY,OAAO;AAAA,MAC3E;AAEA,aAAO,QAAQ,OAAO,WAAW,SAAS,IAAI;AAC9C,aAAO,YAAY,OAAO,gBAAgB,SAAS,IAAI;AACvD,UAAI,CAAC,OAAO,WAAW,QAAQ;AAC7B,eAAO;AACP,cAAM,KAAK,cAAc;AAAA,MAC3B;AACA,UAAI,CAAC,OAAO,gBAAgB,QAAQ;AAClC,eAAO;AACP,cAAM,KAAK,kBAAkB;AAAA,MAC/B;AAEA,YAAM,QAAQ,MAAM,OAAO,QAAQ,MAAM,OAAO,YAAY,MAAM,OAAO;AACzE,YAAM,UAA0B;AAAA,QAC9B,OAAO;AAAA,QACP,OAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAAA,QACxC;AAAA,MACF;AACA,UAAI,MAAM,SAAS,EAAG,SAAQ,QAAQ,MAAM,KAAK,IAAI;AACrD,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEA,SAAS,sBAAsB,OAAyB;AACtD,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,QAAQ,MAAM,MAAM,OAAO,GAAG;AACvC,QAAI,KAAK,WAAW,MAAM,KAAK,KAAK,WAAW,MAAM,GAAG;AACtD,YAAM,OAAO,KAAK,MAAM,CAAC,EAAE,KAAK;AAChC,UAAI,SAAS,YAAa;AAC1B,YAAM,WAAW,KAAK,WAAW,IAAI,KAAK,KAAK,WAAW,IAAI,IAAI,KAAK,MAAM,CAAC,IAAI;AAClF,UAAI,IAAI,QAAQ;AAAA,IAClB;AAAA,EACF;AACA,SAAO,CAAC,GAAG,GAAG;AAChB;AAEA,SAAS,eAAe,OAAuB;AAC7C,MAAI,QAAQ;AACZ,aAAW,QAAQ,MAAM,MAAM,OAAO,GAAG;AACvC,SACG,KAAK,WAAW,GAAG,KAAK,KAAK,WAAW,GAAG,MAC5C,CAAC,KAAK,WAAW,KAAK,KACtB,CAAC,KAAK,WAAW,KAAK,GACtB;AACA,eAAS;AAAA,IACX;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,SAAS,OAAkD;AAClE,SAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,WAAW,OAAoC;AACtD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;AAEA,SAAS,kBAAkB,MAAmC;AAC5D,QAAM,QAAQ,KAAK,MAAM,+BAA+B;AACxD,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,QAAQ,MAAM,CAAC,KAAK,IAAI,KAAK;AACnC,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,kBAAkB,OAAyC;AAClE,MAAI,CAAC,SAAS,KAAK,EAAG,QAAO;AAC7B,QAAM,SAAS,WAAW,MAAM,MAAM;AACtC,QAAM,QAAQ,WAAW,MAAM,KAAK,KAAK;AACzC,MAAI,WAAW,OAAW,QAAO;AACjC,QAAM,aAAa,gBAAgB,MAAM,UAAU;AACnD,QAAM,kBAAkB,gBAAgB,MAAM,eAAe;AAC7D,QAAM,YAAY,gBAAgB,MAAM,SAAS;AACjD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe,WAAW,MAAM,aAAa;AAAA,EAC/C;AACF;AAEA,SAAS,gBAAgB,OAAqD;AAC5E,MAAI,CAAC,SAAS,KAAK,EAAG,QAAO,EAAE,QAAQ,OAAO,QAAQ,GAAG;AACzD,SAAO;AAAA,IACL,QAAQ,MAAM,WAAW;AAAA,IACzB,QAAQ,WAAW,MAAM,MAAM,KAAK;AAAA,EACtC;AACF;AAEA,SAAS,gBAAgB,OAIvB;AACA,MAAI,CAAC,SAAS,KAAK,EAAG,QAAO,EAAE,cAAc,GAAG,YAAY,GAAG,WAAW,EAAE;AAC5E,SAAO;AAAA,IACL,cAAc,YAAY,MAAM,YAAY;AAAA,IAC5C,YAAY,YAAY,MAAM,UAAU;AAAA,IACxC,WAAW,YAAY,MAAM,SAAS;AAAA,EACxC;AACF;AAEA,SAAS,YAAY,OAAwB;AAC3C,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC;AACtC;","names":[]}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { n as AgentTaskSpec, R as RuntimeStreamEvent } from './types-DlyPgeI0.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @stable
|
|
5
|
+
*
|
|
6
|
+
* Production-run lifecycle: record what the agent did on behalf of a customer,
|
|
7
|
+
* what it cost, and how it ended.
|
|
8
|
+
*
|
|
9
|
+
* Three concerns live in this module:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Lifecycle state machine** — `running` -> `completed | failed | cancelled`,
|
|
12
|
+
* enforced by `RuntimeRunStateError`. Completion is idempotent for the same
|
|
13
|
+
* status (a second `complete()` call is a no-op so retries / cleanup paths
|
|
14
|
+
* don't double-fire side effects). A different terminal status is a state
|
|
15
|
+
* error.
|
|
16
|
+
*
|
|
17
|
+
* 2. **Cost ledger** — every `llm_call` event the handle observes contributes
|
|
18
|
+
* `tokensIn`, `tokensOut`, `costUsd`, and bumps `llmCalls`. Wall time is
|
|
19
|
+
* measured from `startRuntimeRun()` to `complete()`. Surface via
|
|
20
|
+
* `handle.cost()` for cost-per-task dashboards.
|
|
21
|
+
*
|
|
22
|
+
* 3. **Persistence adapter** — `RuntimeRunPersistenceAdapter` is the seam
|
|
23
|
+
* consumers plug in to write a `RuntimeRunRow` to their D1 / postgres /
|
|
24
|
+
* KV store. The adapter receives a sanitized row shape; no telemetry
|
|
25
|
+
* payload bytes flow through it unless the consumer opts in via
|
|
26
|
+
* `RuntimeRunOptions.telemetryEvents`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** @stable */
|
|
30
|
+
type RuntimeRunStatus = 'running' | 'completed' | 'failed' | 'cancelled';
|
|
31
|
+
/** @stable */
|
|
32
|
+
interface RuntimeRunCost {
|
|
33
|
+
/** Cumulative input tokens across every observed `llm_call` event. */
|
|
34
|
+
tokensIn: number;
|
|
35
|
+
/** Cumulative output tokens across every observed `llm_call` event. */
|
|
36
|
+
tokensOut: number;
|
|
37
|
+
/** Sum of `costUsd` from every observed `llm_call` event. */
|
|
38
|
+
costUsd: number;
|
|
39
|
+
/** Wall time from `startRuntimeRun()` to `complete()` (or `now()` if not yet completed). */
|
|
40
|
+
wallMs: number;
|
|
41
|
+
/** Count of `llm_call` events observed during the run. */
|
|
42
|
+
llmCalls: number;
|
|
43
|
+
}
|
|
44
|
+
/** @stable */
|
|
45
|
+
interface RuntimeRunCompleteInput {
|
|
46
|
+
status: Exclude<RuntimeRunStatus, 'running'>;
|
|
47
|
+
resultSummary?: string;
|
|
48
|
+
/** Optional explicit cost override; if omitted, the accumulated ledger is used. */
|
|
49
|
+
cost?: Partial<RuntimeRunCost>;
|
|
50
|
+
/** Stable error message when `status === 'failed'`. */
|
|
51
|
+
error?: string;
|
|
52
|
+
/** Additional adapter-specific fields merged into the persisted row. */
|
|
53
|
+
metadata?: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
/** @stable */
|
|
56
|
+
interface RuntimeRunRow {
|
|
57
|
+
/** Stable runtime-side identifier. Adapters may translate to their own primary key. */
|
|
58
|
+
id: string;
|
|
59
|
+
workspaceId: string;
|
|
60
|
+
sessionId?: string;
|
|
61
|
+
agentId?: string;
|
|
62
|
+
domain?: string;
|
|
63
|
+
taskId: string;
|
|
64
|
+
scenarioId?: string;
|
|
65
|
+
status: RuntimeRunStatus;
|
|
66
|
+
resultSummary?: string;
|
|
67
|
+
error?: string;
|
|
68
|
+
cost: RuntimeRunCost;
|
|
69
|
+
startedAt: string;
|
|
70
|
+
completedAt?: string;
|
|
71
|
+
metadata?: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
/** @stable */
|
|
74
|
+
interface RuntimeRunPersistenceAdapter {
|
|
75
|
+
/**
|
|
76
|
+
* Called once when `handle.persist()` runs. Implementations write `row` to
|
|
77
|
+
* their durable store (D1, postgres, KV) and return whatever the consumer
|
|
78
|
+
* wants the caller to see (often the storage-side row id). Errors thrown
|
|
79
|
+
* here propagate out of `persist()` so the caller can decide whether to
|
|
80
|
+
* retry or log-and-continue.
|
|
81
|
+
*/
|
|
82
|
+
upsert(row: RuntimeRunRow): Promise<void> | void;
|
|
83
|
+
}
|
|
84
|
+
/** @stable */
|
|
85
|
+
interface RuntimeRunOptions {
|
|
86
|
+
workspaceId: string;
|
|
87
|
+
sessionId?: string;
|
|
88
|
+
agentId?: string;
|
|
89
|
+
taskSpec: AgentTaskSpec;
|
|
90
|
+
scenarioId?: string;
|
|
91
|
+
/** Optional persistence adapter; if omitted, `persist()` is a no-op. */
|
|
92
|
+
adapter?: RuntimeRunPersistenceAdapter;
|
|
93
|
+
/** Override the row id; default = `${taskSpec.id}:${random suffix}`. */
|
|
94
|
+
id?: string;
|
|
95
|
+
/** Override the clock; default = `Date.now()`. Useful for deterministic tests. */
|
|
96
|
+
now?: () => number;
|
|
97
|
+
}
|
|
98
|
+
/** @stable */
|
|
99
|
+
interface RuntimeRunHandle {
|
|
100
|
+
/** Stable id assigned at start. */
|
|
101
|
+
readonly id: string;
|
|
102
|
+
readonly workspaceId: string;
|
|
103
|
+
readonly sessionId: string | undefined;
|
|
104
|
+
readonly taskSpec: AgentTaskSpec;
|
|
105
|
+
readonly status: RuntimeRunStatus;
|
|
106
|
+
/**
|
|
107
|
+
* Observe a single `RuntimeStreamEvent`. The handle ignores non-cost events
|
|
108
|
+
* (text deltas, tool calls) silently so consumers can pipe the whole stream
|
|
109
|
+
* through `handle.observe`. `llm_call` events update the ledger.
|
|
110
|
+
*/
|
|
111
|
+
observe(event: RuntimeStreamEvent): void;
|
|
112
|
+
/** Snapshot of the current cost ledger. Safe to call at any time. */
|
|
113
|
+
cost(): RuntimeRunCost;
|
|
114
|
+
/**
|
|
115
|
+
* Transition to a terminal state. Idempotent for the same status; throws
|
|
116
|
+
* `RuntimeRunStateError` for a different terminal status (state machines
|
|
117
|
+
* don't time-travel).
|
|
118
|
+
*/
|
|
119
|
+
complete(input: RuntimeRunCompleteInput): void;
|
|
120
|
+
/** Build the current row without writing it. Useful for tests + dry runs. */
|
|
121
|
+
toRow(metadata?: Record<string, unknown>): RuntimeRunRow;
|
|
122
|
+
/**
|
|
123
|
+
* Persist the current row via the configured adapter. Must be called after
|
|
124
|
+
* `complete()`. Idempotent for the same terminal state (the adapter sees
|
|
125
|
+
* the same row on retry).
|
|
126
|
+
*/
|
|
127
|
+
persist(metadata?: Record<string, unknown>): Promise<void>;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* @stable
|
|
131
|
+
*
|
|
132
|
+
* Construct a runtime-run handle. The returned handle is mutable across its
|
|
133
|
+
* lifetime; consumers should not share it across requests.
|
|
134
|
+
*/
|
|
135
|
+
declare function startRuntimeRun(options: RuntimeRunOptions): RuntimeRunHandle;
|
|
136
|
+
|
|
137
|
+
export { type RuntimeRunHandle as R, type RuntimeRunPersistenceAdapter as a, type RuntimeRunRow as b, startRuntimeRun as s };
|
|
@@ -376,4 +376,4 @@ interface KnowledgeReadinessDecision {
|
|
|
376
376
|
nonBlockingGapIds: string[];
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
export type { AgentBackendInput as A, KnowledgeReadinessDecision as K, RuntimeStreamEvent as R, AgentExecutionBackend as a, AgentBackendContext as b, RunAgentTaskOptions as c, AgentTaskRunResult as d, RunAgentTaskStreamOptions as e,
|
|
379
|
+
export type { AgentBackendInput as A, KnowledgeReadinessDecision as K, RuntimeStreamEvent as R, AgentExecutionBackend as a, AgentBackendContext as b, RunAgentTaskOptions as c, AgentTaskRunResult as d, RunAgentTaskStreamOptions as e, AgentRuntimeEvent as f, AgentTaskStatus as g, RuntimeSessionStore as h, RuntimeSession as i, AgentAdapter as j, AgentKnowledgeProvider as k, AgentRuntimeEventSink as l, AgentTaskContext as m, AgentTaskSpec as n };
|