@sweny-ai/core 0.1.215 → 0.1.217
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/dist/eval/grounded.d.ts +56 -0
- package/dist/eval/grounded.js +416 -0
- package/dist/eval/index.d.ts +12 -1
- package/dist/eval/index.js +17 -2
- package/dist/executor.js +6 -0
- package/dist/harness/claude-code.js +9 -0
- package/dist/harness/codex.js +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/schema.d.ts +221 -0
- package/dist/schema.js +73 -2
- package/dist/types.d.ts +67 -0
- package/package.json +1 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { OutputGroundedQuote, OutputGroundedRule, ToolAccess, ToolCall } from "../types.js";
|
|
2
|
+
/** What a node's grounding is checked against. */
|
|
3
|
+
export interface GroundingEvidence {
|
|
4
|
+
/** The evaluated attempt's tool calls, with outputs (in memory). */
|
|
5
|
+
toolCalls: ToolCall[];
|
|
6
|
+
/** The context the node's prompt received: `{ input, ...priorNodes }`. */
|
|
7
|
+
context?: Record<string, unknown>;
|
|
8
|
+
/** The evaluated node's id. Its own earlier visit (loops) is not evidence. */
|
|
9
|
+
nodeId?: string;
|
|
10
|
+
/** The author's text the prompt carried: instruction, rules, context sources. Always evidence. */
|
|
11
|
+
authored?: string[];
|
|
12
|
+
/** Access class of the node's skill tools by name. A `write` skill tool is never evidence. */
|
|
13
|
+
toolAccess?: ReadonlyMap<string, ToolAccess>;
|
|
14
|
+
}
|
|
15
|
+
export declare const BUILTIN_PATTERNS: readonly ["url", "github_ref", "issue_key"];
|
|
16
|
+
/** What `{}` scans prose for. Issue keys only when listed: prose is full of `UTF-8`-style tokens. */
|
|
17
|
+
export declare const DEFAULT_PATTERNS: readonly ["url", "github_ref"];
|
|
18
|
+
/** Same cap as `output_matches[].matches` (see value.ts). */
|
|
19
|
+
export declare const MAX_REGEX_SOURCE_LENGTH = 1000;
|
|
20
|
+
/** Offending values listed per failure group before "and N more". */
|
|
21
|
+
export declare const MAX_LISTED = 8;
|
|
22
|
+
/** Shortest quote accepted by default, after whitespace normalization. */
|
|
23
|
+
export declare const DEFAULT_MIN_QUOTE = 8;
|
|
24
|
+
/**
|
|
25
|
+
* Tools whose result is not evidence: shells, subagents, file writers, the
|
|
26
|
+
* safe-output tool (`emit_output`, EMIT_OUTPUT_TOOL in safe-outputs.ts) and
|
|
27
|
+
* tools that echo what they were given. Lower-cased; Claude Code, Codex and pi names.
|
|
28
|
+
*/
|
|
29
|
+
export declare const NOT_EVIDENCE_TOOLS: ReadonlySet<string>;
|
|
30
|
+
type Verdict = {
|
|
31
|
+
pass: boolean;
|
|
32
|
+
reasoning?: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Canonical form for comparing URLs: scheme and host lowercased and the
|
|
36
|
+
* default port dropped (by URL), any trailing slash on the path dropped.
|
|
37
|
+
* Path, query and fragment stay as written. A scheme-less GitHub or
|
|
38
|
+
* Atlassian link gets `https://`. A string that is not a URL comes back trimmed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function normalizeUrl(raw: string): string;
|
|
41
|
+
/** `needle` occurs in `hay` as a whole token: no letter, digit, `_` or `-` against a word edge. */
|
|
42
|
+
export declare function containsToken(hay: string, needle: string): boolean;
|
|
43
|
+
/** A tool's result is evidence by default only when the tool reads. */
|
|
44
|
+
export declare function isEvidenceTool(c: ToolCall, toolAccess?: ReadonlyMap<string, ToolAccess>): boolean;
|
|
45
|
+
/** A `tools` entry names this call: a bare name for sweny's own or a built-in, else `server/tool` or `mcp__server__tool`. */
|
|
46
|
+
export declare function namesCall(name: string, c: ToolCall): boolean;
|
|
47
|
+
/** The whitespace rule for a quote. `trim` is the default. */
|
|
48
|
+
export declare function normalizeQuote(s: string, mode: OutputGroundedQuote["whitespace"]): string;
|
|
49
|
+
/**
|
|
50
|
+
* Evaluate an `output_grounded` rule against the node's output and evidence.
|
|
51
|
+
* Never throws: a bad config (malformed path, invalid pattern) is a failure
|
|
52
|
+
* with a reason, not a silent pass. A configured path that matched nothing
|
|
53
|
+
* passes with a warning in the reasoning.
|
|
54
|
+
*/
|
|
55
|
+
export declare function evaluateGroundedRule(rule: OutputGroundedRule, data: unknown, evidence: GroundingEvidence): Verdict;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
// ─── Eval: output_grounded (#580) ────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// A `function` rule with two checks, both pure and deterministic (no model call):
|
|
4
|
+
//
|
|
5
|
+
// - Ids: every URL and `owner/repo#N` ref a node outputs (plus issue keys or
|
|
6
|
+
// custom patterns when listed, and every value at the configured `ids`
|
|
7
|
+
// paths) must appear in what the agent saw: the outputs of its own
|
|
8
|
+
// successful read-tool calls, the author's text its prompt carried, and (by
|
|
9
|
+
// default) the workflow input and the prior-node context.
|
|
10
|
+
// - Quotes: every value at a `quotes` path must appear verbatim, under an
|
|
11
|
+
// explicit whitespace rule, in one string of a named tool's output.
|
|
12
|
+
//
|
|
13
|
+
// Evidence lives in memory only. Tool outputs are never journaled (see
|
|
14
|
+
// NODE_RESULT_JOURNAL), so the verdict is computed once, while the run holds
|
|
15
|
+
// them, and the EvalResult is what the journal keeps. A node replayed on
|
|
16
|
+
// resume keeps its journaled verdict; a node that re-runs is re-checked
|
|
17
|
+
// against its new tool outputs.
|
|
18
|
+
import { isErrorOutput } from "./function.js";
|
|
19
|
+
export const BUILTIN_PATTERNS = ["url", "github_ref", "issue_key"];
|
|
20
|
+
/** What `{}` scans prose for. Issue keys only when listed: prose is full of `UTF-8`-style tokens. */
|
|
21
|
+
export const DEFAULT_PATTERNS = ["url", "github_ref"];
|
|
22
|
+
/** Same cap as `output_matches[].matches` (see value.ts). */
|
|
23
|
+
export const MAX_REGEX_SOURCE_LENGTH = 1000;
|
|
24
|
+
/** Offending values listed per failure group before "and N more". */
|
|
25
|
+
export const MAX_LISTED = 8;
|
|
26
|
+
/** Shortest quote accepted by default, after whitespace normalization. */
|
|
27
|
+
export const DEFAULT_MIN_QUOTE = 8;
|
|
28
|
+
/**
|
|
29
|
+
* Tools whose result is not evidence: shells, subagents, file writers, the
|
|
30
|
+
* safe-output tool (`emit_output`, EMIT_OUTPUT_TOOL in safe-outputs.ts) and
|
|
31
|
+
* tools that echo what they were given. Lower-cased; Claude Code, Codex and pi names.
|
|
32
|
+
*/
|
|
33
|
+
export const NOT_EVIDENCE_TOOLS = new Set([
|
|
34
|
+
"bash",
|
|
35
|
+
"bashoutput",
|
|
36
|
+
"killshell",
|
|
37
|
+
"shell",
|
|
38
|
+
"apply_patch",
|
|
39
|
+
"task",
|
|
40
|
+
"agent",
|
|
41
|
+
"write",
|
|
42
|
+
"edit",
|
|
43
|
+
"multiedit",
|
|
44
|
+
"notebookedit",
|
|
45
|
+
"todowrite",
|
|
46
|
+
"structuredoutput",
|
|
47
|
+
"exitplanmode",
|
|
48
|
+
"emit_output",
|
|
49
|
+
]);
|
|
50
|
+
/** A tool name that says it writes, sends or runs: its result likely echoes its input. */
|
|
51
|
+
const WRITE_VERB_RE = /(?:^|[_-])(?:create|update|save|add|post|send|delete|remove|set|write|edit|upsert|put|patch|merge|close|reopen|assign|comment|reply|publish|push|commit|run|exec|execute|echo|emit)(?:[_-]|$)/i;
|
|
52
|
+
const URL_BODY = String.raw `[^\s<>"'${"`"}()[\]{}|\\^]+`;
|
|
53
|
+
/** `http(s)://...` in any case, or a scheme-less GitHub or Atlassian link. */
|
|
54
|
+
const URL_RE = new RegExp(String.raw `https?:\/\/${URL_BODY}|(?<![\w.@/:-])(?:github\.com|[\w-]+\.atlassian\.net)\/${URL_BODY}`, "gi");
|
|
55
|
+
const KNOWN_HOST_RE = /^(?:github\.com|[\w-]+\.atlassian\.net)\//i;
|
|
56
|
+
const TRAILING_PUNCT_RE = /[.,;:!?]+$/;
|
|
57
|
+
const GITHUB_REF_RE = /(?<![\w./-])([\w.-]+)\/([\w.-]+)#(\d+)(?!\w)/g;
|
|
58
|
+
const ISSUE_KEY_RE = /(?<![\w-])([A-Z][A-Z0-9]{1,9})-\d+(?![\w-])/g;
|
|
59
|
+
/** Matched against a normalized URL: `//host/owner/repo/(issues|pull)/N` at the end. */
|
|
60
|
+
const GITHUB_URL_RE = /\/\/[^/]+\/([^/]+)\/([^/]+)\/(?:issues|pull)\/(\d+)$/;
|
|
61
|
+
const NUMBER_RE = /^-?\d+(?:\.\d+)?$/;
|
|
62
|
+
const WORD_CHAR = /[\w-]/;
|
|
63
|
+
const SEGMENT_RE = /^([A-Za-z_]\w*)(\[\*\])?$/;
|
|
64
|
+
const IDENT_RE = /^[A-Za-z_]\w*$/;
|
|
65
|
+
/** sweny's own MCP server: its tools are matched by bare name, like built-ins. */
|
|
66
|
+
const OWN_SERVER = "sweny-core";
|
|
67
|
+
// ─── URLs ────────────────────────────────────────────────────────────
|
|
68
|
+
/**
|
|
69
|
+
* Canonical form for comparing URLs: scheme and host lowercased and the
|
|
70
|
+
* default port dropped (by URL), any trailing slash on the path dropped.
|
|
71
|
+
* Path, query and fragment stay as written. A scheme-less GitHub or
|
|
72
|
+
* Atlassian link gets `https://`. A string that is not a URL comes back trimmed.
|
|
73
|
+
*/
|
|
74
|
+
export function normalizeUrl(raw) {
|
|
75
|
+
const s = raw.trim();
|
|
76
|
+
try {
|
|
77
|
+
const u = new URL(KNOWN_HOST_RE.test(s) ? `https://${s}` : s);
|
|
78
|
+
return `${u.protocol}//${u.host}${u.pathname.replace(/\/+$/, "")}${u.search}${u.hash}`;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return s;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** The URL minus its fragment. */
|
|
85
|
+
function withoutFragment(normalized) {
|
|
86
|
+
return normalized.split("#")[0];
|
|
87
|
+
}
|
|
88
|
+
/** The URL minus its last path segment (`https://h/a/b/3` -> `https://h/a/b`); none for a bare origin. */
|
|
89
|
+
function urlCollection(normalized) {
|
|
90
|
+
const base = withoutFragment(normalized);
|
|
91
|
+
const pathStart = base.indexOf("/", base.indexOf("//") + 2);
|
|
92
|
+
return pathStart === -1 ? undefined : base.slice(0, base.lastIndexOf("/"));
|
|
93
|
+
}
|
|
94
|
+
// ─── Walking values ──────────────────────────────────────────────────
|
|
95
|
+
/** `a.b`, `a[0]`, `a["not an identifier"]`. */
|
|
96
|
+
function childPath(parent, key) {
|
|
97
|
+
if (!IDENT_RE.test(String(key)))
|
|
98
|
+
return `${parent}[${JSON.stringify(key)}]`;
|
|
99
|
+
return parent === "" ? `${key}` : `${parent}.${key}`;
|
|
100
|
+
}
|
|
101
|
+
/** Every string (and, with `numbers`, number) leaf under a value of the output, with its concrete path. */
|
|
102
|
+
function leaves(value, path, numbers) {
|
|
103
|
+
if (typeof value === "string")
|
|
104
|
+
return [{ path, text: value }];
|
|
105
|
+
if (numbers && typeof value === "number")
|
|
106
|
+
return [{ path, text: String(value) }];
|
|
107
|
+
if (typeof value !== "object" || value === null)
|
|
108
|
+
return [];
|
|
109
|
+
if (Array.isArray(value))
|
|
110
|
+
return value.flatMap((v, i) => leaves(v, childPath(path, i), numbers));
|
|
111
|
+
return Object.entries(value).flatMap(([k, v]) => leaves(v, childPath(path, k), numbers));
|
|
112
|
+
}
|
|
113
|
+
/** Every string and number in an evidence value, and its object keys (never array indexes). Cycles read once. */
|
|
114
|
+
function evidenceText(value, seen = new WeakSet()) {
|
|
115
|
+
if (typeof value === "string")
|
|
116
|
+
return { values: [value], keys: [] };
|
|
117
|
+
if (typeof value === "number")
|
|
118
|
+
return { values: [String(value)], keys: [] };
|
|
119
|
+
if (typeof value !== "object" || value === null || seen.has(value))
|
|
120
|
+
return { values: [], keys: [] };
|
|
121
|
+
seen.add(value);
|
|
122
|
+
const entries = Array.isArray(value)
|
|
123
|
+
? value.map((v) => [undefined, v])
|
|
124
|
+
: Object.entries(value);
|
|
125
|
+
const out = { values: [], keys: [] };
|
|
126
|
+
for (const [k, v] of entries) {
|
|
127
|
+
const inner = evidenceText(v, seen);
|
|
128
|
+
out.values.push(...inner.values);
|
|
129
|
+
out.keys.push(...(k === undefined ? [] : [k]), ...inner.keys);
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Resolve an eval path (grammar: spec nodes#path-grammar) to concrete
|
|
135
|
+
* `{ path, value }` pairs, so a failure names `items[3].url`, not
|
|
136
|
+
* `items[*].url`. A path that does not resolve has nothing to check (the
|
|
137
|
+
* caller warns). A malformed path throws.
|
|
138
|
+
*/
|
|
139
|
+
function resolveConcrete(data, path) {
|
|
140
|
+
let current = [{ path: "", value: data }];
|
|
141
|
+
for (const part of path.replace(/^(all|any):/, "").split(".")) {
|
|
142
|
+
const m = SEGMENT_RE.exec(part);
|
|
143
|
+
if (!m)
|
|
144
|
+
throw new Error(`malformed path '${path}'`);
|
|
145
|
+
const name = m[1];
|
|
146
|
+
current = current.flatMap((c) => {
|
|
147
|
+
const obj = c.value;
|
|
148
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj) || !(name in obj))
|
|
149
|
+
return [];
|
|
150
|
+
const p = childPath(c.path, name);
|
|
151
|
+
const child = obj[name];
|
|
152
|
+
if (m[2] === undefined)
|
|
153
|
+
return [{ path: p, value: child }];
|
|
154
|
+
return Array.isArray(child) ? child.map((el, i) => ({ path: childPath(p, i), value: el })) : [];
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return current;
|
|
158
|
+
}
|
|
159
|
+
const isBuiltin = (p) => BUILTIN_PATTERNS.includes(p);
|
|
160
|
+
function compileRegex(p) {
|
|
161
|
+
if (p.length > MAX_REGEX_SOURCE_LENGTH) {
|
|
162
|
+
throw new Error(`invalid pattern: regex source exceeds ${MAX_REGEX_SOURCE_LENGTH} characters`);
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
return { name: p, re: new RegExp(p, "g") };
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
throw new Error(`invalid pattern /${p}/: ${e.message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function compilePatterns(patterns) {
|
|
172
|
+
return { builtins: patterns.filter(isBuiltin), custom: patterns.filter((p) => !isBuiltin(p)).map(compileRegex) };
|
|
173
|
+
}
|
|
174
|
+
/** Built-ins only: what runs over tool outputs (a user regex never does). */
|
|
175
|
+
const BUILTINS_ONLY = compilePatterns(BUILTIN_PATTERNS);
|
|
176
|
+
/**
|
|
177
|
+
* Find ids and URLs in one string. Every pattern reads the whole text, so an
|
|
178
|
+
* id inside a URL's path, query or fragment is a candidate of its own.
|
|
179
|
+
*/
|
|
180
|
+
function extract(text, compiled, path = "") {
|
|
181
|
+
const out = [];
|
|
182
|
+
const has = (p) => compiled.builtins.includes(p);
|
|
183
|
+
if (has("url")) {
|
|
184
|
+
for (const m of text.matchAll(URL_RE)) {
|
|
185
|
+
const value = m[0].replace(TRAILING_PUNCT_RE, "");
|
|
186
|
+
out.push({ path, value, type: "url", cls: urlCollection(normalizeUrl(value)) });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (has("github_ref")) {
|
|
190
|
+
for (const m of text.matchAll(GITHUB_REF_RE)) {
|
|
191
|
+
out.push({ path, value: m[0], type: "github_ref", cls: `gh:${m[1]}/${m[2]}`.toLowerCase() });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (has("issue_key")) {
|
|
195
|
+
for (const m of text.matchAll(ISSUE_KEY_RE))
|
|
196
|
+
out.push({ path, value: m[0], cls: `key:${m[1]}` });
|
|
197
|
+
}
|
|
198
|
+
for (const c of compiled.custom) {
|
|
199
|
+
for (const m of text.matchAll(c.re)) {
|
|
200
|
+
const value = m[1] ?? m[0];
|
|
201
|
+
if (value !== "")
|
|
202
|
+
out.push({ path, value });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
/** A whole value at an `ids` path, classified (for `stale`) when it has a recognizable kind. */
|
|
208
|
+
function wholeValue(value, path) {
|
|
209
|
+
const same = extract(value, BUILTINS_ONLY, path).find((c) => c.value === value);
|
|
210
|
+
return { ...(same ?? { path, value }), whole: true };
|
|
211
|
+
}
|
|
212
|
+
// ─── Evidence ────────────────────────────────────────────────────────
|
|
213
|
+
/** One body of evidence, indexed for lookup. Built in memory, never stored. */
|
|
214
|
+
class Corpus {
|
|
215
|
+
values = [];
|
|
216
|
+
keys = [];
|
|
217
|
+
urls = new Set();
|
|
218
|
+
bases = new Set();
|
|
219
|
+
refs = new Set();
|
|
220
|
+
add(value) {
|
|
221
|
+
const text = evidenceText(value);
|
|
222
|
+
this.keys.push(...text.keys);
|
|
223
|
+
for (const s of text.values) {
|
|
224
|
+
this.values.push(s);
|
|
225
|
+
for (const m of s.matchAll(URL_RE)) {
|
|
226
|
+
const url = normalizeUrl(m[0].replace(TRAILING_PUNCT_RE, ""));
|
|
227
|
+
this.urls.add(url);
|
|
228
|
+
this.bases.add(withoutFragment(url));
|
|
229
|
+
const gh = GITHUB_URL_RE.exec(withoutFragment(url));
|
|
230
|
+
if (gh)
|
|
231
|
+
this.refs.add(`${gh[1]}/${gh[2]}#${gh[3]}`.toLowerCase());
|
|
232
|
+
}
|
|
233
|
+
for (const m of s.matchAll(GITHUB_REF_RE))
|
|
234
|
+
this.refs.add(m[0].toLowerCase());
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
has(c) {
|
|
238
|
+
if (c.type === "url") {
|
|
239
|
+
// A fragment must have been seen; a link without one is grounded by its page.
|
|
240
|
+
const url = normalizeUrl(c.value);
|
|
241
|
+
return url.includes("#") ? this.urls.has(url) : this.bases.has(url);
|
|
242
|
+
}
|
|
243
|
+
if (c.type === "github_ref" && this.refs.has(c.value.toLowerCase()))
|
|
244
|
+
return true;
|
|
245
|
+
if (c.whole && NUMBER_RE.test(c.value))
|
|
246
|
+
return this.values.some((v) => v.trim() === c.value);
|
|
247
|
+
const texts = c.whole ? this.values : [...this.values, ...this.keys];
|
|
248
|
+
return texts.some((t) => containsToken(t, c.value));
|
|
249
|
+
}
|
|
250
|
+
/** Every id kind this corpus holds, by the built-in patterns only (undefined for a kindless URL). */
|
|
251
|
+
kinds() {
|
|
252
|
+
return new Set(this.values.flatMap((t) => extract(t, BUILTINS_ONLY)).map((c) => c.cls));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** `needle` occurs in `hay` as a whole token: no letter, digit, `_` or `-` against a word edge. */
|
|
256
|
+
export function containsToken(hay, needle) {
|
|
257
|
+
const wordStart = WORD_CHAR.test(needle.charAt(0));
|
|
258
|
+
const wordEnd = WORD_CHAR.test(needle.charAt(needle.length - 1));
|
|
259
|
+
for (let i = hay.indexOf(needle); i !== -1; i = hay.indexOf(needle, i + 1)) {
|
|
260
|
+
const clearBefore = !wordStart || !WORD_CHAR.test(hay.charAt(i - 1));
|
|
261
|
+
const clearAfter = !wordEnd || !WORD_CHAR.test(hay.charAt(i + needle.length));
|
|
262
|
+
if (clearBefore && clearAfter)
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
function didSucceed(c) {
|
|
268
|
+
if (c.status !== undefined)
|
|
269
|
+
return c.status === "success";
|
|
270
|
+
return !isErrorOutput(c.output);
|
|
271
|
+
}
|
|
272
|
+
/** sweny's own tools and built-ins have no server (sweny-core counts as none). */
|
|
273
|
+
function serverOf(c) {
|
|
274
|
+
return c.server === OWN_SERVER ? undefined : c.server;
|
|
275
|
+
}
|
|
276
|
+
/** A tool's result is evidence by default only when the tool reads. */
|
|
277
|
+
export function isEvidenceTool(c, toolAccess) {
|
|
278
|
+
if (serverOf(c) === undefined) {
|
|
279
|
+
const access = toolAccess?.get(c.tool);
|
|
280
|
+
if (access !== undefined)
|
|
281
|
+
return access === "read";
|
|
282
|
+
if (NOT_EVIDENCE_TOOLS.has(c.tool.toLowerCase()))
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return !WRITE_VERB_RE.test(c.tool);
|
|
286
|
+
}
|
|
287
|
+
/** A `tools` entry names this call: a bare name for sweny's own or a built-in, else `server/tool` or `mcp__server__tool`. */
|
|
288
|
+
export function namesCall(name, c) {
|
|
289
|
+
const server = serverOf(c);
|
|
290
|
+
if (server === undefined)
|
|
291
|
+
return name === c.tool;
|
|
292
|
+
return name === `${server}/${c.tool}` || name === `mcp__${server}__${c.tool}`;
|
|
293
|
+
}
|
|
294
|
+
/** Successful calls that count: the named ones when `names` is set, else every read tool. */
|
|
295
|
+
function evidenceCalls(evidence, names) {
|
|
296
|
+
return evidence.toolCalls.filter((c) => didSucceed(c) &&
|
|
297
|
+
(names === undefined ? isEvidenceTool(c, evidence.toolAccess) : names.some((n) => namesCall(n, c))));
|
|
298
|
+
}
|
|
299
|
+
// ─── Reasoning ───────────────────────────────────────────────────────
|
|
300
|
+
function label(path, value) {
|
|
301
|
+
const shown = value.length > 120 ? `${value.slice(0, 119)}…` : value;
|
|
302
|
+
return `${path} ${JSON.stringify(shown)}`;
|
|
303
|
+
}
|
|
304
|
+
function listed(failures) {
|
|
305
|
+
const more = failures.length - MAX_LISTED;
|
|
306
|
+
return failures.slice(0, MAX_LISTED).join("; ") + (more > 0 ? `; and ${more} more` : "");
|
|
307
|
+
}
|
|
308
|
+
/** Resolve a configured path, noting it when it matched nothing. */
|
|
309
|
+
function resolveNoting(data, path, empty) {
|
|
310
|
+
const found = resolveConcrete(data, path);
|
|
311
|
+
if (found.length === 0)
|
|
312
|
+
empty.push(path);
|
|
313
|
+
return found;
|
|
314
|
+
}
|
|
315
|
+
// ─── The two checks ──────────────────────────────────────────────────
|
|
316
|
+
function checkIds(rule, data, evidence, empty) {
|
|
317
|
+
const compiled = compilePatterns(rule.patterns ?? DEFAULT_PATTERNS);
|
|
318
|
+
const roots = rule.paths === undefined ? [{ path: "", value: data }] : rule.paths.flatMap((p) => resolveNoting(data, p, empty));
|
|
319
|
+
const scanned = roots
|
|
320
|
+
.flatMap((r) => leaves(r.value, r.path, false))
|
|
321
|
+
.flatMap((l) => extract(l.text, compiled, l.path));
|
|
322
|
+
const whole = (rule.ids ?? [])
|
|
323
|
+
.flatMap((p) => resolveNoting(data, p, empty))
|
|
324
|
+
.flatMap((r) => leaves(r.value, r.path, true))
|
|
325
|
+
.filter((l) => l.text.trim() !== "")
|
|
326
|
+
.map((l) => wholeValue(l.text.trim(), l.path));
|
|
327
|
+
const tools = new Corpus();
|
|
328
|
+
for (const c of evidenceCalls(evidence, rule.tools))
|
|
329
|
+
tools.add(c.output);
|
|
330
|
+
const useInputs = rule.inputs !== false;
|
|
331
|
+
const useContext = rule.context !== false;
|
|
332
|
+
const given = new Corpus();
|
|
333
|
+
given.add(evidence.authored);
|
|
334
|
+
for (const [k, v] of Object.entries(evidence.context ?? {})) {
|
|
335
|
+
if (k !== evidence.nodeId && (k === "input" ? useInputs : useContext))
|
|
336
|
+
given.add(v);
|
|
337
|
+
}
|
|
338
|
+
const allow = new Set(rule.allow?.map(normalizeUrl));
|
|
339
|
+
const staleKinds = rule.stale === true ? tools.kinds() : undefined;
|
|
340
|
+
const failures = [];
|
|
341
|
+
const seen = new Set();
|
|
342
|
+
for (const c of [...scanned, ...whole]) {
|
|
343
|
+
const key = JSON.stringify([c.path, c.value]);
|
|
344
|
+
if (seen.has(key) || allow.has(normalizeUrl(c.value)) || tools.has(c))
|
|
345
|
+
continue;
|
|
346
|
+
seen.add(key);
|
|
347
|
+
if (!given.has(c))
|
|
348
|
+
failures.push(label(c.path, c.value));
|
|
349
|
+
else if (c.cls !== undefined && staleKinds?.has(c.cls)) {
|
|
350
|
+
failures.push(`${label(c.path, c.value)} (stale: this node's tools returned other ids of its kind, not this one)`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (failures.length === 0)
|
|
354
|
+
return [];
|
|
355
|
+
const sources = ["tool outputs", "instruction"];
|
|
356
|
+
if (useInputs)
|
|
357
|
+
sources.push("inputs");
|
|
358
|
+
if (useContext)
|
|
359
|
+
sources.push("prior context");
|
|
360
|
+
return [`${failures.length} value(s) not grounded (evidence: ${sources.join(", ")}): ${listed(failures)}`];
|
|
361
|
+
}
|
|
362
|
+
/** The whitespace rule for a quote. `trim` is the default. */
|
|
363
|
+
export function normalizeQuote(s, mode) {
|
|
364
|
+
if (mode === "exact")
|
|
365
|
+
return s;
|
|
366
|
+
if (mode === "collapse")
|
|
367
|
+
return s.replace(/\s+/g, " ").trim();
|
|
368
|
+
return s.trim();
|
|
369
|
+
}
|
|
370
|
+
function checkQuotes(rule, data, evidence, empty) {
|
|
371
|
+
const parts = [];
|
|
372
|
+
for (const q of rule.quotes ?? []) {
|
|
373
|
+
const mode = q.whitespace ?? "trim";
|
|
374
|
+
const min = q.min_length ?? DEFAULT_MIN_QUOTE;
|
|
375
|
+
const names = q.tools ?? rule.tools;
|
|
376
|
+
// Normalizing the output under trim changes nothing a trimmed quote can match; under collapse it is the rule.
|
|
377
|
+
const hay = evidenceCalls(evidence, names).flatMap((c) => evidenceText(c.output).values.map((s) => normalizeQuote(s, mode)));
|
|
378
|
+
const failures = [];
|
|
379
|
+
for (const l of resolveNoting(data, q.path, empty).flatMap((r) => leaves(r.value, r.path, true))) {
|
|
380
|
+
const needle = normalizeQuote(l.text, mode);
|
|
381
|
+
if (!/\S/.test(needle))
|
|
382
|
+
failures.push(`${label(l.path, l.text)} (empty)`);
|
|
383
|
+
else if (needle.length < min)
|
|
384
|
+
failures.push(`${label(l.path, l.text)} (shorter than ${min} characters)`);
|
|
385
|
+
else if (!hay.some((h) => h.includes(needle)))
|
|
386
|
+
failures.push(label(l.path, l.text));
|
|
387
|
+
}
|
|
388
|
+
if (failures.length > 0) {
|
|
389
|
+
const from = names === undefined ? "any read tool's" : names.join(", ");
|
|
390
|
+
parts.push(`${failures.length} quote(s) not found verbatim in ${from} output (whitespace: ${mode}): ${listed(failures)}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return parts;
|
|
394
|
+
}
|
|
395
|
+
// ─── Entry point ─────────────────────────────────────────────────────
|
|
396
|
+
/**
|
|
397
|
+
* Evaluate an `output_grounded` rule against the node's output and evidence.
|
|
398
|
+
* Never throws: a bad config (malformed path, invalid pattern) is a failure
|
|
399
|
+
* with a reason, not a silent pass. A configured path that matched nothing
|
|
400
|
+
* passes with a warning in the reasoning.
|
|
401
|
+
*/
|
|
402
|
+
export function evaluateGroundedRule(rule, data, evidence) {
|
|
403
|
+
const empty = [];
|
|
404
|
+
let parts;
|
|
405
|
+
try {
|
|
406
|
+
parts = [...checkIds(rule, data, evidence, empty), ...checkQuotes(rule, data, evidence, empty)];
|
|
407
|
+
}
|
|
408
|
+
catch (e) {
|
|
409
|
+
return { pass: false, reasoning: `output_grounded: ${e.message}` };
|
|
410
|
+
}
|
|
411
|
+
const warning = empty.length > 0 ? [`warning: nothing at ${empty.join(", ")}`] : [];
|
|
412
|
+
const notes = [...parts, ...warning];
|
|
413
|
+
if (notes.length === 0)
|
|
414
|
+
return { pass: true };
|
|
415
|
+
return { pass: parts.length === 0, reasoning: `output_grounded: ${notes.join(" | ")}` };
|
|
416
|
+
}
|
package/dist/eval/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Claude, EvalPolicy, EvalResult, Evaluator, NodeResult, Workflow, Node } from "../types.js";
|
|
1
|
+
import type { Claude, EvalPolicy, EvalResult, Evaluator, NodeResult, ToolAccess, Workflow, Node } from "../types.js";
|
|
2
2
|
import { type ToolAliases } from "./function.js";
|
|
3
3
|
export type { ToolAliases } from "./function.js";
|
|
4
4
|
export interface EvaluateAllOptions {
|
|
@@ -20,6 +20,17 @@ export interface EvaluateAllOptions {
|
|
|
20
20
|
signal?: AbortSignal;
|
|
21
21
|
/** Per-call timeout forwarded to judge calls. */
|
|
22
22
|
timeoutMs?: number;
|
|
23
|
+
/**
|
|
24
|
+
* The context the node's prompt received (`{ input, ...priorNodes }`).
|
|
25
|
+
* Evidence for `output_grounded`; held in memory, never journaled.
|
|
26
|
+
*/
|
|
27
|
+
context?: Record<string, unknown>;
|
|
28
|
+
/** The evaluated node's id, so its own earlier visit in `context` is not evidence. */
|
|
29
|
+
nodeId?: string;
|
|
30
|
+
/** The author's text the prompt carried (instruction, rules, context sources). Evidence for `output_grounded`. */
|
|
31
|
+
authored?: string[];
|
|
32
|
+
/** Access class of the node's skill tools by name, so `output_grounded` counts only read tools. */
|
|
33
|
+
toolAccess?: ReadonlyMap<string, ToolAccess>;
|
|
23
34
|
}
|
|
24
35
|
/**
|
|
25
36
|
* Run every declared evaluator against the node's result and return one
|
package/dist/eval/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { evaluateValueRule } from "./value.js";
|
|
7
7
|
import { evaluateFunctionRule } from "./function.js";
|
|
8
8
|
import { evaluateJudge } from "./judge.js";
|
|
9
|
+
import { evaluateGroundedRule } from "./grounded.js";
|
|
9
10
|
/** Reasoning cap, matching the spec's ~500-character ceiling. */
|
|
10
11
|
const MAX_REASONING_LENGTH = 500;
|
|
11
12
|
/**
|
|
@@ -78,8 +79,22 @@ async function evaluateOne(e, result, opts) {
|
|
|
78
79
|
return { name: e.name, kind: "value", pass: verdict.pass, reasoning: capReasoning(verdict.reasoning) };
|
|
79
80
|
}
|
|
80
81
|
if (e.kind === "function") {
|
|
81
|
-
const
|
|
82
|
-
|
|
82
|
+
const rule = e.rule ?? {};
|
|
83
|
+
const verdicts = [evaluateFunctionRule(rule, result.toolCalls, opts.aliases)];
|
|
84
|
+
if (rule.output_grounded) {
|
|
85
|
+
verdicts.push(evaluateGroundedRule(rule.output_grounded, result.data, {
|
|
86
|
+
toolCalls: result.toolCalls,
|
|
87
|
+
...(opts.context ? { context: opts.context } : {}),
|
|
88
|
+
...(opts.nodeId !== undefined ? { nodeId: opts.nodeId } : {}),
|
|
89
|
+
...(opts.authored ? { authored: opts.authored } : {}),
|
|
90
|
+
...(opts.toolAccess ? { toolAccess: opts.toolAccess } : {}),
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
const failed = verdicts.filter((v) => !v.pass);
|
|
94
|
+
// A passing verdict can still carry a warning (output_grounded: a path that matched nothing).
|
|
95
|
+
const notes = (failed.length > 0 ? failed : verdicts).flatMap((v) => (v.reasoning ? [v.reasoning] : []));
|
|
96
|
+
const reasoning = notes.length > 0 ? notes.join("; ") : undefined;
|
|
97
|
+
return { name: e.name, kind: "function", pass: failed.length === 0, reasoning: capReasoning(reasoning) };
|
|
83
98
|
}
|
|
84
99
|
if (e.kind === "judge" && opts.claude) {
|
|
85
100
|
const model = resolveJudgeModel(e, opts.node, opts.workflow, opts.claude.defaultJudgeModel);
|
package/dist/executor.js
CHANGED
|
@@ -934,6 +934,12 @@ async function executeRun(workflow, input, options, results, trace, at) {
|
|
|
934
934
|
claude: judgeClaude,
|
|
935
935
|
node,
|
|
936
936
|
workflow,
|
|
937
|
+
// What the prompt was given: evidence for output_grounded (#580).
|
|
938
|
+
context,
|
|
939
|
+
nodeId: currentId,
|
|
940
|
+
// The author's own text (never a retry preamble), and which skill tools only read.
|
|
941
|
+
authored: [resolvedInstruction, effectiveRules, effectiveContext],
|
|
942
|
+
toolAccess: new Map(filteredTools.map((t) => [t.name, t.access ?? "write"])),
|
|
937
943
|
sequential: budgetOn,
|
|
938
944
|
...(signal ? { signal } : {}),
|
|
939
945
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
@@ -774,9 +774,13 @@ export class ClaudeCodeHarness {
|
|
|
774
774
|
if (am.message?.content && Array.isArray(am.message.content)) {
|
|
775
775
|
for (const block of am.message.content) {
|
|
776
776
|
if (block.type === "tool_use") {
|
|
777
|
+
const server = mcpServerOf(block.name ?? "");
|
|
777
778
|
const call = {
|
|
778
779
|
tool: stripMcpPrefix(block.name ?? ""),
|
|
779
780
|
input: block.input,
|
|
781
|
+
// External MCP tools keep their server, so output_grounded can tell
|
|
782
|
+
// linear's get_issue from another server's (#580).
|
|
783
|
+
...(server !== undefined && server !== "sweny-core" ? { server } : {}),
|
|
780
784
|
};
|
|
781
785
|
toolCalls.push(call);
|
|
782
786
|
if (typeof block.id === "string")
|
|
@@ -1058,6 +1062,11 @@ export function coreToolToSdkTool(coreTool, defaultCtx) {
|
|
|
1058
1062
|
});
|
|
1059
1063
|
}
|
|
1060
1064
|
/** Strip MCP server prefix: "mcp__server__tool" → "tool" */
|
|
1065
|
+
/** The server of an `mcp__server__tool` name, else undefined. */
|
|
1066
|
+
function mcpServerOf(name) {
|
|
1067
|
+
const parts = name.split("__");
|
|
1068
|
+
return parts.length >= 3 && parts[0] === "mcp" ? parts[1] : undefined;
|
|
1069
|
+
}
|
|
1061
1070
|
function stripMcpPrefix(name) {
|
|
1062
1071
|
const parts = name.split("__");
|
|
1063
1072
|
if (parts.length >= 3 && parts[0] === "mcp")
|
package/dist/harness/codex.js
CHANGED
|
@@ -762,7 +762,9 @@ export class CodexHarness {
|
|
|
762
762
|
: kind === "mcp_tool_call" || kind === "collab_tool_call"
|
|
763
763
|
? String(item.tool ?? "")
|
|
764
764
|
: "web_search";
|
|
765
|
-
|
|
765
|
+
const server = kind === "mcp_tool_call" && typeof item.server === "string" ? item.server : undefined;
|
|
766
|
+
// External MCP tools keep their server for output_grounded (#580).
|
|
767
|
+
call = { tool, input, ...(server !== undefined && server !== "sweny-core" ? { server } : {}) };
|
|
766
768
|
startCall(id, call);
|
|
767
769
|
const detail = kind === "command_execution" ? `shell: ${String(item.command ?? "")}` : tool;
|
|
768
770
|
opts.onProgress?.(detail.length > 80 ? detail.slice(0, 79) + "…" : detail);
|
package/dist/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* })
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ToolRunContext, ConfigField, JSONSchema, Workflow, Node, Edge, EdgeWhen, WhenExpression, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, NodeAccess, NodePermissions, NodePermissionsSpec, SafeOutputType, SafeOutputDeclaration, SafeOutputsPolicy, SafeOutputReceipt, SafeOutputPin, AuthorAssociation, } from "./types.js";
|
|
24
|
+
export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ToolRunContext, ConfigField, JSONSchema, Workflow, Node, Edge, EdgeWhen, WhenExpression, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, OutputGroundedRule, OutputGroundedQuote, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, NodeAccess, NodePermissions, NodePermissionsSpec, SafeOutputType, SafeOutputDeclaration, SafeOutputsPolicy, SafeOutputReceipt, SafeOutputPin, AuthorAssociation, } from "./types.js";
|
|
25
25
|
export { WORKFLOW_INPUT_TYPES } from "./types.js";
|
|
26
26
|
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, NODE_ACCESS, TOOL_CLASSES, SAFE_OUTPUT_TYPES, SAFE_OUTPUT_APPLIERS, AUTHOR_ASSOCIATIONS, } from "./types.js";
|
|
27
27
|
export { consoleLogger } from "./types.js";
|
package/dist/schema.d.ts
CHANGED
|
@@ -149,6 +149,27 @@ export declare const evalPolicyZ: z.ZodEnum<{
|
|
|
149
149
|
any_pass: "any_pass";
|
|
150
150
|
weighted: "weighted";
|
|
151
151
|
}>;
|
|
152
|
+
/** `output_grounded` config (#580). Mirrors `OutputGroundedRule` in types.ts. */
|
|
153
|
+
export declare const outputGroundedZ: z.ZodObject<{
|
|
154
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
155
|
+
ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
156
|
+
patterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
157
|
+
allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
158
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
159
|
+
inputs: z.ZodOptional<z.ZodBoolean>;
|
|
160
|
+
context: z.ZodOptional<z.ZodBoolean>;
|
|
161
|
+
stale: z.ZodOptional<z.ZodBoolean>;
|
|
162
|
+
quotes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
163
|
+
path: z.ZodString;
|
|
164
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
165
|
+
whitespace: z.ZodOptional<z.ZodEnum<{
|
|
166
|
+
exact: "exact";
|
|
167
|
+
trim: "trim";
|
|
168
|
+
collapse: "collapse";
|
|
169
|
+
}>>;
|
|
170
|
+
min_length: z.ZodOptional<z.ZodNumber>;
|
|
171
|
+
}, z.core.$strict>>>;
|
|
172
|
+
}, z.core.$strict>;
|
|
152
173
|
export declare const evaluatorRuleZ: z.ZodObject<{
|
|
153
174
|
any_tool_called: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
154
175
|
all_tools_called: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -160,6 +181,26 @@ export declare const evaluatorRuleZ: z.ZodObject<{
|
|
|
160
181
|
in: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
161
182
|
matches: z.ZodOptional<z.ZodString>;
|
|
162
183
|
}, z.core.$strict>>>;
|
|
184
|
+
output_grounded: z.ZodOptional<z.ZodObject<{
|
|
185
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
186
|
+
ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
187
|
+
patterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
188
|
+
allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
189
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
190
|
+
inputs: z.ZodOptional<z.ZodBoolean>;
|
|
191
|
+
context: z.ZodOptional<z.ZodBoolean>;
|
|
192
|
+
stale: z.ZodOptional<z.ZodBoolean>;
|
|
193
|
+
quotes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
194
|
+
path: z.ZodString;
|
|
195
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
196
|
+
whitespace: z.ZodOptional<z.ZodEnum<{
|
|
197
|
+
exact: "exact";
|
|
198
|
+
trim: "trim";
|
|
199
|
+
collapse: "collapse";
|
|
200
|
+
}>>;
|
|
201
|
+
min_length: z.ZodOptional<z.ZodNumber>;
|
|
202
|
+
}, z.core.$strict>>>;
|
|
203
|
+
}, z.core.$strict>>;
|
|
163
204
|
}, z.core.$strict>;
|
|
164
205
|
/**
|
|
165
206
|
* A single evaluator. The `kind` field discriminates required-fields:
|
|
@@ -185,6 +226,26 @@ export declare const evaluatorZ: z.ZodObject<{
|
|
|
185
226
|
in: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
186
227
|
matches: z.ZodOptional<z.ZodString>;
|
|
187
228
|
}, z.core.$strict>>>;
|
|
229
|
+
output_grounded: z.ZodOptional<z.ZodObject<{
|
|
230
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
231
|
+
ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
232
|
+
patterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
233
|
+
allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
234
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
235
|
+
inputs: z.ZodOptional<z.ZodBoolean>;
|
|
236
|
+
context: z.ZodOptional<z.ZodBoolean>;
|
|
237
|
+
stale: z.ZodOptional<z.ZodBoolean>;
|
|
238
|
+
quotes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
239
|
+
path: z.ZodString;
|
|
240
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
241
|
+
whitespace: z.ZodOptional<z.ZodEnum<{
|
|
242
|
+
exact: "exact";
|
|
243
|
+
trim: "trim";
|
|
244
|
+
collapse: "collapse";
|
|
245
|
+
}>>;
|
|
246
|
+
min_length: z.ZodOptional<z.ZodNumber>;
|
|
247
|
+
}, z.core.$strict>>>;
|
|
248
|
+
}, z.core.$strict>>;
|
|
188
249
|
}, z.core.$strict>>;
|
|
189
250
|
rubric: z.ZodOptional<z.ZodString>;
|
|
190
251
|
pass_when: z.ZodOptional<z.ZodString>;
|
|
@@ -467,6 +528,26 @@ export declare const nodeZ: z.ZodObject<{
|
|
|
467
528
|
in: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
468
529
|
matches: z.ZodOptional<z.ZodString>;
|
|
469
530
|
}, z.core.$strict>>>;
|
|
531
|
+
output_grounded: z.ZodOptional<z.ZodObject<{
|
|
532
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
533
|
+
ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
534
|
+
patterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
535
|
+
allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
536
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
537
|
+
inputs: z.ZodOptional<z.ZodBoolean>;
|
|
538
|
+
context: z.ZodOptional<z.ZodBoolean>;
|
|
539
|
+
stale: z.ZodOptional<z.ZodBoolean>;
|
|
540
|
+
quotes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
541
|
+
path: z.ZodString;
|
|
542
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
543
|
+
whitespace: z.ZodOptional<z.ZodEnum<{
|
|
544
|
+
exact: "exact";
|
|
545
|
+
trim: "trim";
|
|
546
|
+
collapse: "collapse";
|
|
547
|
+
}>>;
|
|
548
|
+
min_length: z.ZodOptional<z.ZodNumber>;
|
|
549
|
+
}, z.core.$strict>>>;
|
|
550
|
+
}, z.core.$strict>>;
|
|
470
551
|
}, z.core.$strict>>;
|
|
471
552
|
rubric: z.ZodOptional<z.ZodString>;
|
|
472
553
|
pass_when: z.ZodOptional<z.ZodString>;
|
|
@@ -675,6 +756,26 @@ export declare const workflowZ: z.ZodObject<{
|
|
|
675
756
|
in: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
|
|
676
757
|
matches: z.ZodOptional<z.ZodString>;
|
|
677
758
|
}, z.core.$strict>>>;
|
|
759
|
+
output_grounded: z.ZodOptional<z.ZodObject<{
|
|
760
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
761
|
+
ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
762
|
+
patterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
763
|
+
allow: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
764
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
765
|
+
inputs: z.ZodOptional<z.ZodBoolean>;
|
|
766
|
+
context: z.ZodOptional<z.ZodBoolean>;
|
|
767
|
+
stale: z.ZodOptional<z.ZodBoolean>;
|
|
768
|
+
quotes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
769
|
+
path: z.ZodString;
|
|
770
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
771
|
+
whitespace: z.ZodOptional<z.ZodEnum<{
|
|
772
|
+
exact: "exact";
|
|
773
|
+
trim: "trim";
|
|
774
|
+
collapse: "collapse";
|
|
775
|
+
}>>;
|
|
776
|
+
min_length: z.ZodOptional<z.ZodNumber>;
|
|
777
|
+
}, z.core.$strict>>>;
|
|
778
|
+
}, z.core.$strict>>;
|
|
678
779
|
}, z.core.$strict>>;
|
|
679
780
|
rubric: z.ZodOptional<z.ZodString>;
|
|
680
781
|
pass_when: z.ZodOptional<z.ZodString>;
|
|
@@ -938,6 +1039,22 @@ export declare function parseWorkflow(raw: unknown): {
|
|
|
938
1039
|
in?: unknown[] | undefined;
|
|
939
1040
|
matches?: string | undefined;
|
|
940
1041
|
}[] | undefined;
|
|
1042
|
+
output_grounded?: {
|
|
1043
|
+
paths?: string[] | undefined;
|
|
1044
|
+
ids?: string[] | undefined;
|
|
1045
|
+
patterns?: string[] | undefined;
|
|
1046
|
+
allow?: string[] | undefined;
|
|
1047
|
+
tools?: string[] | undefined;
|
|
1048
|
+
inputs?: boolean | undefined;
|
|
1049
|
+
context?: boolean | undefined;
|
|
1050
|
+
stale?: boolean | undefined;
|
|
1051
|
+
quotes?: {
|
|
1052
|
+
path: string;
|
|
1053
|
+
tools?: string[] | undefined;
|
|
1054
|
+
whitespace?: "exact" | "trim" | "collapse" | undefined;
|
|
1055
|
+
min_length?: number | undefined;
|
|
1056
|
+
}[] | undefined;
|
|
1057
|
+
} | undefined;
|
|
941
1058
|
} | undefined;
|
|
942
1059
|
rubric?: string | undefined;
|
|
943
1060
|
pass_when?: string | undefined;
|
|
@@ -1233,6 +1350,93 @@ export declare const workflowJsonSchema: {
|
|
|
1233
1350
|
};
|
|
1234
1351
|
readonly minItems: 1;
|
|
1235
1352
|
};
|
|
1353
|
+
readonly output_grounded: {
|
|
1354
|
+
readonly type: "object";
|
|
1355
|
+
readonly description: "function only. Every id and URL in the output must appear in the node's successful read-tool outputs, its instruction, the workflow input or prior nodes' context.";
|
|
1356
|
+
readonly additionalProperties: false;
|
|
1357
|
+
readonly properties: {
|
|
1358
|
+
readonly paths: {
|
|
1359
|
+
readonly type: "array";
|
|
1360
|
+
readonly items: {
|
|
1361
|
+
readonly type: "string";
|
|
1362
|
+
readonly minLength: 1;
|
|
1363
|
+
};
|
|
1364
|
+
readonly minItems: 1;
|
|
1365
|
+
};
|
|
1366
|
+
readonly ids: {
|
|
1367
|
+
readonly type: "array";
|
|
1368
|
+
readonly items: {
|
|
1369
|
+
readonly type: "string";
|
|
1370
|
+
readonly minLength: 1;
|
|
1371
|
+
};
|
|
1372
|
+
readonly minItems: 1;
|
|
1373
|
+
};
|
|
1374
|
+
readonly patterns: {
|
|
1375
|
+
readonly type: "array";
|
|
1376
|
+
readonly items: {
|
|
1377
|
+
readonly type: "string";
|
|
1378
|
+
readonly minLength: 1;
|
|
1379
|
+
};
|
|
1380
|
+
};
|
|
1381
|
+
readonly allow: {
|
|
1382
|
+
readonly type: "array";
|
|
1383
|
+
readonly items: {
|
|
1384
|
+
readonly type: "string";
|
|
1385
|
+
readonly minLength: 1;
|
|
1386
|
+
};
|
|
1387
|
+
readonly minItems: 1;
|
|
1388
|
+
};
|
|
1389
|
+
readonly tools: {
|
|
1390
|
+
readonly type: "array";
|
|
1391
|
+
readonly items: {
|
|
1392
|
+
readonly type: "string";
|
|
1393
|
+
readonly minLength: 1;
|
|
1394
|
+
};
|
|
1395
|
+
readonly minItems: 1;
|
|
1396
|
+
};
|
|
1397
|
+
readonly inputs: {
|
|
1398
|
+
readonly type: "boolean";
|
|
1399
|
+
};
|
|
1400
|
+
readonly context: {
|
|
1401
|
+
readonly type: "boolean";
|
|
1402
|
+
};
|
|
1403
|
+
readonly stale: {
|
|
1404
|
+
readonly type: "boolean";
|
|
1405
|
+
};
|
|
1406
|
+
readonly quotes: {
|
|
1407
|
+
readonly type: "array";
|
|
1408
|
+
readonly minItems: 1;
|
|
1409
|
+
readonly description: "Values that must appear verbatim in a tool's output.";
|
|
1410
|
+
readonly items: {
|
|
1411
|
+
readonly type: "object";
|
|
1412
|
+
readonly required: readonly ["path"];
|
|
1413
|
+
readonly additionalProperties: false;
|
|
1414
|
+
readonly properties: {
|
|
1415
|
+
readonly path: {
|
|
1416
|
+
readonly type: "string";
|
|
1417
|
+
readonly minLength: 1;
|
|
1418
|
+
};
|
|
1419
|
+
readonly tools: {
|
|
1420
|
+
readonly type: "array";
|
|
1421
|
+
readonly items: {
|
|
1422
|
+
readonly type: "string";
|
|
1423
|
+
readonly minLength: 1;
|
|
1424
|
+
};
|
|
1425
|
+
readonly minItems: 1;
|
|
1426
|
+
};
|
|
1427
|
+
readonly whitespace: {
|
|
1428
|
+
readonly type: "string";
|
|
1429
|
+
readonly enum: readonly ["exact", "trim", "collapse"];
|
|
1430
|
+
};
|
|
1431
|
+
readonly min_length: {
|
|
1432
|
+
readonly type: "integer";
|
|
1433
|
+
readonly minimum: 1;
|
|
1434
|
+
};
|
|
1435
|
+
};
|
|
1436
|
+
};
|
|
1437
|
+
};
|
|
1438
|
+
};
|
|
1439
|
+
};
|
|
1236
1440
|
};
|
|
1237
1441
|
};
|
|
1238
1442
|
readonly rubric: {
|
|
@@ -1286,6 +1490,23 @@ export declare const workflowJsonSchema: {
|
|
|
1286
1490
|
readonly required: readonly ["rule"];
|
|
1287
1491
|
};
|
|
1288
1492
|
};
|
|
1493
|
+
}, {
|
|
1494
|
+
readonly if: {
|
|
1495
|
+
readonly properties: {
|
|
1496
|
+
readonly kind: {
|
|
1497
|
+
readonly const: "value";
|
|
1498
|
+
};
|
|
1499
|
+
};
|
|
1500
|
+
};
|
|
1501
|
+
readonly then: {
|
|
1502
|
+
readonly properties: {
|
|
1503
|
+
readonly rule: {
|
|
1504
|
+
readonly not: {
|
|
1505
|
+
readonly required: readonly ["output_grounded"];
|
|
1506
|
+
};
|
|
1507
|
+
};
|
|
1508
|
+
};
|
|
1509
|
+
};
|
|
1289
1510
|
}];
|
|
1290
1511
|
};
|
|
1291
1512
|
readonly Permissions: {
|
package/dist/schema.js
CHANGED
|
@@ -126,6 +126,31 @@ export const evalPolicyZ = z.enum(EVAL_POLICIES);
|
|
|
126
126
|
* workflow fails at load time rather than throwing mid-run from `aggregateEval`.
|
|
127
127
|
*/
|
|
128
128
|
const SUPPORTED_EVAL_POLICIES = new Set(["all_pass"]);
|
|
129
|
+
/** `output_grounded` config (#580). Mirrors `OutputGroundedRule` in types.ts. */
|
|
130
|
+
export const outputGroundedZ = z
|
|
131
|
+
.object({
|
|
132
|
+
paths: z.array(z.string().min(1)).min(1).optional(),
|
|
133
|
+
ids: z.array(z.string().min(1)).min(1).optional(),
|
|
134
|
+
// `[]` is allowed: it turns the pattern scan off.
|
|
135
|
+
patterns: z.array(z.string().min(1)).optional(),
|
|
136
|
+
allow: z.array(z.string().min(1)).min(1).optional(),
|
|
137
|
+
tools: z.array(z.string().min(1)).min(1).optional(),
|
|
138
|
+
inputs: z.boolean().optional(),
|
|
139
|
+
context: z.boolean().optional(),
|
|
140
|
+
stale: z.boolean().optional(),
|
|
141
|
+
quotes: z
|
|
142
|
+
.array(z
|
|
143
|
+
.object({
|
|
144
|
+
path: z.string().min(1),
|
|
145
|
+
tools: z.array(z.string().min(1)).min(1).optional(),
|
|
146
|
+
whitespace: z.enum(["exact", "trim", "collapse"]).optional(),
|
|
147
|
+
min_length: z.number().int().min(1).optional(),
|
|
148
|
+
})
|
|
149
|
+
.strict())
|
|
150
|
+
.min(1)
|
|
151
|
+
.optional(),
|
|
152
|
+
})
|
|
153
|
+
.strict();
|
|
129
154
|
export const evaluatorRuleZ = z
|
|
130
155
|
.object({
|
|
131
156
|
any_tool_called: z.array(z.string().min(1)).min(1).optional(),
|
|
@@ -133,14 +158,16 @@ export const evaluatorRuleZ = z
|
|
|
133
158
|
no_tool_called: z.array(z.string().min(1)).min(1).optional(),
|
|
134
159
|
output_required: z.array(z.string().min(1)).min(1).optional(),
|
|
135
160
|
output_matches: z.array(outputMatchZ).min(1).optional(),
|
|
161
|
+
output_grounded: outputGroundedZ.optional(),
|
|
136
162
|
})
|
|
137
163
|
.strict()
|
|
138
164
|
.refine((r) => r.any_tool_called !== undefined ||
|
|
139
165
|
r.all_tools_called !== undefined ||
|
|
140
166
|
r.no_tool_called !== undefined ||
|
|
141
167
|
r.output_required !== undefined ||
|
|
142
|
-
r.output_matches !== undefined
|
|
143
|
-
|
|
168
|
+
r.output_matches !== undefined ||
|
|
169
|
+
r.output_grounded !== undefined, {
|
|
170
|
+
message: "evaluator rule must declare at least one of: any_tool_called, all_tools_called, no_tool_called, output_required, output_matches, output_grounded",
|
|
144
171
|
});
|
|
145
172
|
/**
|
|
146
173
|
* A single evaluator. The `kind` field discriminates required-fields:
|
|
@@ -175,6 +202,14 @@ export const evaluatorZ = z
|
|
|
175
202
|
path: ["rule"],
|
|
176
203
|
});
|
|
177
204
|
}
|
|
205
|
+
// output_grounded reads the tool trace; a value evaluator would skip it and pass silently.
|
|
206
|
+
if (e.kind === "value" && e.rule?.output_grounded !== undefined) {
|
|
207
|
+
ctx.addIssue({
|
|
208
|
+
code: z.ZodIssueCode.custom,
|
|
209
|
+
message: `evaluator '${e.name}' declares output_grounded on kind: value; output_grounded reads the tool trace, so declare it on kind: function`,
|
|
210
|
+
path: ["rule", "output_grounded"],
|
|
211
|
+
});
|
|
212
|
+
}
|
|
178
213
|
if (e.rubric !== undefined || e.pass_when !== undefined || e.model !== undefined) {
|
|
179
214
|
ctx.addIssue({
|
|
180
215
|
code: z.ZodIssueCode.custom,
|
|
@@ -967,6 +1002,37 @@ export const workflowJsonSchema = {
|
|
|
967
1002
|
no_tool_called: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 },
|
|
968
1003
|
output_required: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 },
|
|
969
1004
|
output_matches: { type: "array", items: { $ref: "#/$defs/OutputMatch" }, minItems: 1 },
|
|
1005
|
+
output_grounded: {
|
|
1006
|
+
type: "object",
|
|
1007
|
+
description: "function only. Every id and URL in the output must appear in the node's successful read-tool outputs, its instruction, the workflow input or prior nodes' context.",
|
|
1008
|
+
additionalProperties: false,
|
|
1009
|
+
properties: {
|
|
1010
|
+
paths: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 },
|
|
1011
|
+
ids: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 },
|
|
1012
|
+
patterns: { type: "array", items: { type: "string", minLength: 1 } },
|
|
1013
|
+
allow: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 },
|
|
1014
|
+
tools: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 },
|
|
1015
|
+
inputs: { type: "boolean" },
|
|
1016
|
+
context: { type: "boolean" },
|
|
1017
|
+
stale: { type: "boolean" },
|
|
1018
|
+
quotes: {
|
|
1019
|
+
type: "array",
|
|
1020
|
+
minItems: 1,
|
|
1021
|
+
description: "Values that must appear verbatim in a tool's output.",
|
|
1022
|
+
items: {
|
|
1023
|
+
type: "object",
|
|
1024
|
+
required: ["path"],
|
|
1025
|
+
additionalProperties: false,
|
|
1026
|
+
properties: {
|
|
1027
|
+
path: { type: "string", minLength: 1 },
|
|
1028
|
+
tools: { type: "array", items: { type: "string", minLength: 1 }, minItems: 1 },
|
|
1029
|
+
whitespace: { type: "string", enum: ["exact", "trim", "collapse"] },
|
|
1030
|
+
min_length: { type: "integer", minimum: 1 },
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
970
1036
|
},
|
|
971
1037
|
},
|
|
972
1038
|
rubric: {
|
|
@@ -1009,6 +1075,11 @@ export const workflowJsonSchema = {
|
|
|
1009
1075
|
not: { required: ["rule"] },
|
|
1010
1076
|
},
|
|
1011
1077
|
},
|
|
1078
|
+
{
|
|
1079
|
+
// output_grounded reads the tool trace: function only.
|
|
1080
|
+
if: { properties: { kind: { const: "value" } } },
|
|
1081
|
+
then: { properties: { rule: { not: { required: ["output_grounded"] } } } },
|
|
1082
|
+
},
|
|
1012
1083
|
],
|
|
1013
1084
|
},
|
|
1014
1085
|
Permissions: {
|
package/dist/types.d.ts
CHANGED
|
@@ -288,6 +288,67 @@ export interface EvaluatorRule {
|
|
|
288
288
|
output_required?: string[];
|
|
289
289
|
/** Each assertion must hold against `result.data`. (`value` rules.) */
|
|
290
290
|
output_matches?: OutputMatch[];
|
|
291
|
+
/**
|
|
292
|
+
* Every id and URL in `result.data` must appear in what the agent saw: its
|
|
293
|
+
* successful read-tool outputs, its own instruction, plus (by default) the
|
|
294
|
+
* workflow input and prior nodes' context. (`function` rules only.) See
|
|
295
|
+
* {@link OutputGroundedRule}.
|
|
296
|
+
*/
|
|
297
|
+
output_grounded?: OutputGroundedRule;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Config for the `output_grounded` check (#580). Every field is optional;
|
|
301
|
+
* `{}` scans the whole output for URLs and `owner/repo#N` refs.
|
|
302
|
+
*/
|
|
303
|
+
export interface OutputGroundedRule {
|
|
304
|
+
/** Paths into `result.data` whose strings are scanned with `patterns`. Default: the whole output. */
|
|
305
|
+
paths?: string[];
|
|
306
|
+
/** Paths whose string or number values are each one id, checked whole (e.g. `items[*].ticket_id`). */
|
|
307
|
+
ids?: string[];
|
|
308
|
+
/**
|
|
309
|
+
* What counts as an id in scanned text: built-in names `url`, `github_ref`
|
|
310
|
+
* (`owner/repo#123`), `issue_key` (`ENG-123`), or a JavaScript regex source
|
|
311
|
+
* (its first capture group is the id when it has one). Default: `url` and
|
|
312
|
+
* `github_ref`; issue keys only when listed (prose is full of `UTF-8`-style
|
|
313
|
+
* tokens), or at `ids` paths. `[]` turns the scan off.
|
|
314
|
+
*/
|
|
315
|
+
patterns?: string[];
|
|
316
|
+
/** Constants that always pass (URLs compared normalized). */
|
|
317
|
+
allow?: string[];
|
|
318
|
+
/**
|
|
319
|
+
* Only these tools' outputs count as evidence, whatever their class. A bare
|
|
320
|
+
* name matches sweny's own or a built-in tool; an MCP tool is named
|
|
321
|
+
* `server/tool` (or `mcp__server__tool`). Default: every successful read tool.
|
|
322
|
+
*/
|
|
323
|
+
tools?: string[];
|
|
324
|
+
/** The workflow input counts as evidence. Default `true`. */
|
|
325
|
+
inputs?: boolean;
|
|
326
|
+
/** Prior nodes' context (what this node's prompt was given) counts as evidence. Default `true`. */
|
|
327
|
+
context?: boolean;
|
|
328
|
+
/**
|
|
329
|
+
* Fail an id seen only in the input or prior context when this node's tool
|
|
330
|
+
* outputs returned other ids of the same kind (same issue-key prefix, same
|
|
331
|
+
* repo, same URL collection) but not this one. Default `false`.
|
|
332
|
+
*/
|
|
333
|
+
stale?: boolean;
|
|
334
|
+
/** Values that must appear verbatim in a tool's output (never in the input or context). */
|
|
335
|
+
quotes?: OutputGroundedQuote[];
|
|
336
|
+
}
|
|
337
|
+
/** One `output_grounded.quotes` entry: the values at `path` must be copied from a tool's output. */
|
|
338
|
+
export interface OutputGroundedQuote {
|
|
339
|
+
/** Path into `result.data`; every string or number value under it is one quote. */
|
|
340
|
+
path: string;
|
|
341
|
+
/** Tools whose outputs the quote must come from, named as in the rule's `tools`. Default: the rule's `tools`, else any read tool. */
|
|
342
|
+
tools?: string[];
|
|
343
|
+
/** Shortest quote accepted, counted after whitespace normalization. Default 8. */
|
|
344
|
+
min_length?: number;
|
|
345
|
+
/**
|
|
346
|
+
* How whitespace compares. `exact`: byte for byte. `trim` (default): the
|
|
347
|
+
* quote's leading and trailing whitespace is ignored. `collapse`: every run
|
|
348
|
+
* of whitespace, in the quote and the tool output, counts as one space, and
|
|
349
|
+
* the quote is trimmed.
|
|
350
|
+
*/
|
|
351
|
+
whitespace?: "exact" | "trim" | "collapse";
|
|
291
352
|
}
|
|
292
353
|
/**
|
|
293
354
|
* A single evaluator on a node.
|
|
@@ -859,6 +920,12 @@ export interface ToolCall {
|
|
|
859
920
|
* evaluators fall back to inspecting `output` for an `error` key.
|
|
860
921
|
*/
|
|
861
922
|
status?: "success" | "error";
|
|
923
|
+
/**
|
|
924
|
+
* The MCP server an external tool came from (`linear` for
|
|
925
|
+
* `mcp__linear__get_issue`). Absent for sweny's own tools, built-ins, and
|
|
926
|
+
* harnesses that do not report it. `output_grounded` matches `tools` on it.
|
|
927
|
+
*/
|
|
928
|
+
server?: string;
|
|
862
929
|
}
|
|
863
930
|
export type ExecutionEvent = {
|
|
864
931
|
type: "workflow:start";
|