@sweny-ai/core 0.1.176 → 0.1.178
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/agent-env.d.ts +9 -0
- package/dist/agent-env.js +4 -0
- package/dist/cli/comment-output.js +4 -5
- package/dist/cli/config-file.d.ts +37 -2
- package/dist/cli/config-file.js +68 -3
- package/dist/cli/final-output.d.ts +4 -1
- package/dist/cli/final-output.js +6 -8
- package/dist/cli/main.d.ts +2 -1
- package/dist/cli/main.js +18 -10
- package/dist/cli/resume.js +1 -0
- package/dist/cli/run-history.d.ts +3 -7
- package/dist/cli/run-history.js +5 -12
- package/dist/cli/run-output.d.ts +16 -10
- package/dist/cli/run-output.js +30 -16
- package/dist/cli/ticket.js +3 -2
- package/dist/cli/try.js +2 -3
- package/dist/decider.d.ts +176 -64
- package/dist/decider.js +475 -155
- package/dist/executor.d.ts +55 -7
- package/dist/executor.js +511 -99
- package/dist/harness/acp.js +2 -2
- package/dist/harness/claude-code.js +6 -4
- package/dist/harness/codex.js +2 -2
- package/dist/harness/pi.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/journal.d.ts +34 -5
- package/dist/journal.js +42 -13
- package/dist/safe-file.d.ts +74 -0
- package/dist/safe-file.js +211 -0
- package/dist/safe-outputs.d.ts +7 -1
- package/dist/safe-outputs.js +7 -1
- package/dist/schema.d.ts +189 -241
- package/dist/schema.js +56 -36
- package/dist/skills/git-push.d.ts +99 -10
- package/dist/skills/git-push.js +413 -52
- package/dist/skills/github.d.ts +19 -15
- package/dist/skills/github.js +91 -15
- package/dist/startup-env.d.ts +30 -0
- package/dist/startup-env.js +48 -0
- package/dist/types.d.ts +86 -4
- package/dist/types.js +22 -0
- package/dist/when.d.ts +8 -1
- package/dist/when.js +8 -1
- package/dist/workflow-builder.js +3 -1
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +37 -5
- package/dist/workflows/triage.yml +40 -6
- package/package.json +1 -1
package/dist/skills/github.js
CHANGED
|
@@ -4,11 +4,6 @@
|
|
|
4
4
|
* Replaces: source-control/github.ts + issue-tracking/github-issues.ts
|
|
5
5
|
* ~800 lines → ~120 lines
|
|
6
6
|
*/
|
|
7
|
-
let branchPusher;
|
|
8
|
-
/** Register (or clear, with `undefined`) the sweny-side branch pusher. */
|
|
9
|
-
export function setBranchPusher(fn) {
|
|
10
|
-
branchPusher = fn;
|
|
11
|
-
}
|
|
12
7
|
class GitHubApiError extends Error {
|
|
13
8
|
status;
|
|
14
9
|
body;
|
|
@@ -19,8 +14,28 @@ class GitHubApiError extends Error {
|
|
|
19
14
|
this.body = body;
|
|
20
15
|
}
|
|
21
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The REST base: `ctx.githubApiUrl` (the operator's GITHUB_API_URL, for GHES)
|
|
19
|
+
* or https://api.github.com. Only a plain https URL: no credentials, query or
|
|
20
|
+
* fragment, since the token goes to it.
|
|
21
|
+
*/
|
|
22
|
+
export function githubApiBase(apiUrl) {
|
|
23
|
+
if (!apiUrl)
|
|
24
|
+
return "https://api.github.com";
|
|
25
|
+
let u;
|
|
26
|
+
try {
|
|
27
|
+
u = new URL(apiUrl);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error(`[GitHub] GITHUB_API_URL is not a URL`);
|
|
31
|
+
}
|
|
32
|
+
if (u.protocol !== "https:" || u.username || u.password || u.search || u.hash) {
|
|
33
|
+
throw new Error(`[GitHub] GITHUB_API_URL is not a plain https URL`);
|
|
34
|
+
}
|
|
35
|
+
return `${u.origin}${u.pathname.replace(/\/+$/, "")}`;
|
|
36
|
+
}
|
|
22
37
|
async function gh(path, ctx, init) {
|
|
23
|
-
const res = await fetch(
|
|
38
|
+
const res = await fetch(`${githubApiBase(ctx.githubApiUrl)}${path}`, {
|
|
24
39
|
...init,
|
|
25
40
|
headers: {
|
|
26
41
|
Authorization: `token ${ctx.config.GITHUB_TOKEN}`,
|
|
@@ -34,6 +49,49 @@ async function gh(path, ctx, init) {
|
|
|
34
49
|
throw new GitHubApiError(res.status, await res.text());
|
|
35
50
|
return res.json();
|
|
36
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Whether sweny may push the PR's head branch, from the API (#498). The agent
|
|
54
|
+
* names head and base, so head could be an existing integration branch
|
|
55
|
+
* ("release") that is neither base nor default. A head that does not exist
|
|
56
|
+
* remotely is pushed with an empty lease (it must still not exist). One that
|
|
57
|
+
* exists is pushed only when it is unprotected and already has an open PR from
|
|
58
|
+
* this repo into `base`, leased on the sha the API returned. Any API failure
|
|
59
|
+
* means no push.
|
|
60
|
+
*/
|
|
61
|
+
export async function headPushLease(repo, head, base, ctx) {
|
|
62
|
+
const branch = head.replace(/^refs\/heads\//, "");
|
|
63
|
+
const target = base.replace(/^refs\/heads\//, "");
|
|
64
|
+
const seg = branch.split("/").map(encodeURIComponent).join("/");
|
|
65
|
+
let info;
|
|
66
|
+
try {
|
|
67
|
+
info = (await gh(`/repos/${repo}/branches/${seg}`, ctx));
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err instanceof GitHubApiError && err.status === 404)
|
|
71
|
+
return { remoteSha: "" };
|
|
72
|
+
return { skip: `cannot check head ${branch} on ${repo}: ${err instanceof Error ? err.message : String(err)}` };
|
|
73
|
+
}
|
|
74
|
+
const sha = info?.commit?.sha;
|
|
75
|
+
if (typeof sha !== "string" || !/^[0-9a-f]{40}([0-9a-f]{24})?$/.test(sha)) {
|
|
76
|
+
return { skip: `cannot check head ${branch} on ${repo}: no branch sha in the API answer` };
|
|
77
|
+
}
|
|
78
|
+
if (info.protected !== false)
|
|
79
|
+
return { skip: `head ${branch} exists on ${repo} and is protected` };
|
|
80
|
+
const owner = repo.split("/")[0];
|
|
81
|
+
let open;
|
|
82
|
+
try {
|
|
83
|
+
open = await gh(`/repos/${repo}/pulls?head=${encodeURIComponent(`${owner}:${branch}`)}&base=${encodeURIComponent(target)}&state=open&per_page=1`, ctx);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
return {
|
|
87
|
+
skip: `cannot check open PRs for ${branch} on ${repo}: ${err instanceof Error ? err.message : String(err)}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(open) || open.length === 0) {
|
|
91
|
+
return { skip: `head ${branch} already exists on ${repo} with no open PR into ${target}` };
|
|
92
|
+
}
|
|
93
|
+
return { remoteSha: sha };
|
|
94
|
+
}
|
|
37
95
|
function isAlreadyExistsError(err) {
|
|
38
96
|
if (!(err instanceof GitHubApiError) || err.status !== 422)
|
|
39
97
|
return false;
|
|
@@ -187,15 +245,33 @@ export const github = {
|
|
|
187
245
|
// recommended `persist-credentials: false` checkout still ships the
|
|
188
246
|
// branch and the agent never holds a write token. Skipped unless the
|
|
189
247
|
// branch exists locally and origin is this repo; never forced.
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
248
|
+
if (ctx.pushBranch) {
|
|
249
|
+
// The default branch comes from the API, not the agent-written
|
|
250
|
+
// checkout's origin/HEAD. Unknown means no push.
|
|
251
|
+
let defaultBranch;
|
|
252
|
+
try {
|
|
253
|
+
const meta = (await gh(`/repos/${input.repo}`, ctx));
|
|
254
|
+
if (typeof meta?.default_branch === "string")
|
|
255
|
+
defaultBranch = meta.default_branch;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// The pusher refuses without it; the PR request below reports the API error.
|
|
259
|
+
}
|
|
260
|
+
const lease = await headPushLease(input.repo, input.head, input.base ?? "main", ctx);
|
|
261
|
+
const push = "skip" in lease
|
|
262
|
+
? { pushed: false, attempted: false, reason: lease.skip }
|
|
263
|
+
: await ctx.pushBranch({
|
|
264
|
+
repo: input.repo,
|
|
265
|
+
head: input.head,
|
|
266
|
+
base: input.base ?? "main",
|
|
267
|
+
...(defaultBranch ? { defaultBranch } : {}),
|
|
268
|
+
remoteSha: lease.remoteSha,
|
|
269
|
+
token: ctx.config.GITHUB_TOKEN,
|
|
270
|
+
});
|
|
271
|
+
if ("skip" in lease)
|
|
272
|
+
ctx.logger?.warn?.(` github_create_pr: not pushing: ${lease.skip}`);
|
|
273
|
+
else if (push.pushed)
|
|
274
|
+
ctx.logger?.info?.(` github_create_pr: pushed ${input.head} to ${input.repo}`);
|
|
199
275
|
else if (push.attempted)
|
|
200
276
|
ctx.logger?.warn?.(` github_create_pr: ${push.reason}; requesting the PR anyway`);
|
|
201
277
|
else
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process environment as it was before sweny read any workspace file.
|
|
3
|
+
*
|
|
4
|
+
* The workspace is agent-writable, and its `.env` (cli/config-file.ts
|
|
5
|
+
* `loadDotenv`) may set any variable the real environment left unset. The
|
|
6
|
+
* privileged git push (skills/git-push.ts) must not take its PATH, proxy or CA
|
|
7
|
+
* settings, or the server it sends the token to, from a value the workspace
|
|
8
|
+
* introduced. This module is imported by config-file.ts, so the snapshot is
|
|
9
|
+
* taken when the module graph loads, before `loadDotenv()` runs.
|
|
10
|
+
*
|
|
11
|
+
* Node only.
|
|
12
|
+
*/
|
|
13
|
+
/** The environment captured at process startup, before any workspace file was read. */
|
|
14
|
+
export declare function startupEnv(): Readonly<NodeJS.ProcessEnv>;
|
|
15
|
+
/**
|
|
16
|
+
* The temp dir from the startup env. `os.tmpdir()` reads TMPDIR at call time,
|
|
17
|
+
* which a workspace `.env` can set; this never moves after startup.
|
|
18
|
+
*/
|
|
19
|
+
export declare function startupTmpdir(): string;
|
|
20
|
+
/** Record that a workspace file (`.env`) set `key` in `process.env`. */
|
|
21
|
+
export declare function markWorkspaceEnv(key: string): void;
|
|
22
|
+
/** Test seam: forget that a workspace file set `key`. */
|
|
23
|
+
export declare function unmarkWorkspaceEnv(key: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* The value of `key` the privileged path may use. From `process.env` itself
|
|
26
|
+
* (the CLI's run env) it is always the startup value. From a caller-built env
|
|
27
|
+
* it is the caller's value, unless a workspace file introduced the key (a copy
|
|
28
|
+
* of `process.env` taken after `loadDotenv`), which yields the startup value.
|
|
29
|
+
*/
|
|
30
|
+
export declare function trustedEnvValue(env: NodeJS.ProcessEnv, key: string): string | undefined;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process environment as it was before sweny read any workspace file.
|
|
3
|
+
*
|
|
4
|
+
* The workspace is agent-writable, and its `.env` (cli/config-file.ts
|
|
5
|
+
* `loadDotenv`) may set any variable the real environment left unset. The
|
|
6
|
+
* privileged git push (skills/git-push.ts) must not take its PATH, proxy or CA
|
|
7
|
+
* settings, or the server it sends the token to, from a value the workspace
|
|
8
|
+
* introduced. This module is imported by config-file.ts, so the snapshot is
|
|
9
|
+
* taken when the module graph loads, before `loadDotenv()` runs.
|
|
10
|
+
*
|
|
11
|
+
* Node only.
|
|
12
|
+
*/
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
const SNAPSHOT = Object.freeze({ ...process.env });
|
|
15
|
+
/** os.tmpdir() as resolved from the startup env (TMPDIR, TEMP, TMP), before any workspace file was read. */
|
|
16
|
+
const TMP = tmpdir();
|
|
17
|
+
/** Variable names a workspace file wrote into `process.env` after startup. */
|
|
18
|
+
const introduced = new Set();
|
|
19
|
+
/** The environment captured at process startup, before any workspace file was read. */
|
|
20
|
+
export function startupEnv() {
|
|
21
|
+
return SNAPSHOT;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The temp dir from the startup env. `os.tmpdir()` reads TMPDIR at call time,
|
|
25
|
+
* which a workspace `.env` can set; this never moves after startup.
|
|
26
|
+
*/
|
|
27
|
+
export function startupTmpdir() {
|
|
28
|
+
return TMP;
|
|
29
|
+
}
|
|
30
|
+
/** Record that a workspace file (`.env`) set `key` in `process.env`. */
|
|
31
|
+
export function markWorkspaceEnv(key) {
|
|
32
|
+
introduced.add(key);
|
|
33
|
+
}
|
|
34
|
+
/** Test seam: forget that a workspace file set `key`. */
|
|
35
|
+
export function unmarkWorkspaceEnv(key) {
|
|
36
|
+
introduced.delete(key);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The value of `key` the privileged path may use. From `process.env` itself
|
|
40
|
+
* (the CLI's run env) it is always the startup value. From a caller-built env
|
|
41
|
+
* it is the caller's value, unless a workspace file introduced the key (a copy
|
|
42
|
+
* of `process.env` taken after `loadDotenv`), which yields the startup value.
|
|
43
|
+
*/
|
|
44
|
+
export function trustedEnvValue(env, key) {
|
|
45
|
+
if (env === process.env || introduced.has(key))
|
|
46
|
+
return SNAPSHOT[key];
|
|
47
|
+
return env[key];
|
|
48
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -12,7 +12,43 @@ export interface ToolContext {
|
|
|
12
12
|
config: Record<string, string>;
|
|
13
13
|
/** Structured logger */
|
|
14
14
|
logger: Logger;
|
|
15
|
-
|
|
15
|
+
/** The run's working directory (`ExecuteOptions.cwd`, else `process.cwd()`). */
|
|
16
|
+
cwd?: string;
|
|
17
|
+
/**
|
|
18
|
+
* A staged or dry run: no side effects. The executor refuses every
|
|
19
|
+
* non-`read` tool before its handler runs.
|
|
20
|
+
*/
|
|
21
|
+
staged?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Pushes a PR's head branch from the sweny process (#473), bound to this
|
|
24
|
+
* run's checkout. Set by the Node executor; absent in the browser build,
|
|
25
|
+
* where `github_create_pr` only calls the API.
|
|
26
|
+
*/
|
|
27
|
+
pushBranch?: BranchPusher;
|
|
28
|
+
/**
|
|
29
|
+
* The operator's GITHUB_API_URL (GHES), read through the startup env
|
|
30
|
+
* snapshot by the Node executor. Absent means https://api.github.com.
|
|
31
|
+
*/
|
|
32
|
+
githubApiUrl?: string;
|
|
33
|
+
}
|
|
34
|
+
/** The sweny-side head-branch push behind `github_create_pr` (#473). Never throws. */
|
|
35
|
+
export type BranchPusher = (opts: {
|
|
36
|
+
repo: string;
|
|
37
|
+
head: string;
|
|
38
|
+
base: string;
|
|
39
|
+
/** The repo's default branch from the GitHub API; without it nothing is pushed. */
|
|
40
|
+
defaultBranch?: string;
|
|
41
|
+
/**
|
|
42
|
+
* The head branch's sha on the remote when it was checked through the API,
|
|
43
|
+
* or "" when it did not exist. The push leases on it. Without it nothing is pushed.
|
|
44
|
+
*/
|
|
45
|
+
remoteSha?: string;
|
|
46
|
+
token?: string;
|
|
47
|
+
}) => Promise<{
|
|
48
|
+
pushed: boolean;
|
|
49
|
+
attempted: boolean;
|
|
50
|
+
reason?: string;
|
|
51
|
+
}>;
|
|
16
52
|
/**
|
|
17
53
|
* Side-effect class of a tool. `"read"` only reads. `"write"` can change
|
|
18
54
|
* something outside the run: create, update, delete, post, send, invoke.
|
|
@@ -382,6 +418,13 @@ export interface Node {
|
|
|
382
418
|
* Distinct from `requires.on_fail`, which is the pre-condition gate.
|
|
383
419
|
*/
|
|
384
420
|
on_fail?: NodeOnFail;
|
|
421
|
+
/**
|
|
422
|
+
* Who picks this node's natural-language route (#357). `agent` (default):
|
|
423
|
+
* the agent's route evaluation. `decider`: the operator's decision model is
|
|
424
|
+
* asked first and its confident answer is the route; otherwise the agent.
|
|
425
|
+
* Expressions always win over both. Safety conditions belong in expressions.
|
|
426
|
+
*/
|
|
427
|
+
route_by?: RouteBy;
|
|
385
428
|
/**
|
|
386
429
|
* What this node's agent may do (#365). `read` runs the node read-only:
|
|
387
430
|
* only `access: "read"` skill tools, no external skill MCP servers, no
|
|
@@ -519,6 +562,12 @@ export interface Edge {
|
|
|
519
562
|
* evaluated by sweny with no model call.
|
|
520
563
|
*/
|
|
521
564
|
when?: EdgeWhen;
|
|
565
|
+
/**
|
|
566
|
+
* The condition in natural language (#357). For an `{ expr }` edge it is
|
|
567
|
+
* what the next rung (decider or agent) reads when the expression cannot be
|
|
568
|
+
* evaluated because a field it needs is missing or invalid.
|
|
569
|
+
*/
|
|
570
|
+
description?: string;
|
|
522
571
|
/** Max times this edge can be followed (enables retry loops). Default: unlimited. */
|
|
523
572
|
max_iterations?: number;
|
|
524
573
|
}
|
|
@@ -533,6 +582,25 @@ export interface Edge {
|
|
|
533
582
|
*/
|
|
534
583
|
export declare const WORKFLOW_TYPES: readonly ["pr_review", "e2e_test", "content_generation", "monitor", "data_sync", "generic"];
|
|
535
584
|
export type WorkflowType = (typeof WORKFLOW_TYPES)[number];
|
|
585
|
+
/**
|
|
586
|
+
* Decision model gates (#357). Shared by the Zod schema, the JSON schema and
|
|
587
|
+
* the executor; kept here so schema-only (browser) consumers need no Node code.
|
|
588
|
+
*/
|
|
589
|
+
/** Default: accept a decider answer only at or above this confidence. */
|
|
590
|
+
export declare const DECIDER_MIN_CONFIDENCE = 0.85;
|
|
591
|
+
/** Default: ... and only when the top label leads the runner-up by at least this much. */
|
|
592
|
+
export declare const DECIDER_MIN_MARGIN = 0.2;
|
|
593
|
+
/** A workflow may lower `min_confidence` to this and no further. */
|
|
594
|
+
export declare const DECIDER_CONFIDENCE_FLOOR = 0.7;
|
|
595
|
+
/** A workflow may lower `min_margin` to this and no further. */
|
|
596
|
+
export declare const DECIDER_MARGIN_FLOOR = 0.1;
|
|
597
|
+
/** `route_by` values: who picks a node's natural-language route. */
|
|
598
|
+
export declare const ROUTE_BY: readonly ["agent", "decider"];
|
|
599
|
+
export type RouteBy = (typeof ROUTE_BY)[number];
|
|
600
|
+
/** Load error for a workflow that still declares the removed `decider.mode`. */
|
|
601
|
+
export declare const DECIDER_MODE_REMOVED: string;
|
|
602
|
+
/** Load error for a workflow that declares the provider itself. */
|
|
603
|
+
export declare const DECIDER_PROVIDER_MOVED: string;
|
|
536
604
|
/** A complete workflow definition. Pure data, fully serializable. */
|
|
537
605
|
export interface Workflow {
|
|
538
606
|
id: string;
|
|
@@ -562,8 +630,9 @@ export interface Workflow {
|
|
|
562
630
|
*/
|
|
563
631
|
budget?: Budget;
|
|
564
632
|
/**
|
|
565
|
-
* Decision model
|
|
566
|
-
*
|
|
633
|
+
* Decision model thresholds (#357) for the nodes that set `route_by:
|
|
634
|
+
* decider`. Never looser than the floors. The provider itself comes only
|
|
635
|
+
* from operator config, never from the workflow.
|
|
567
636
|
*/
|
|
568
637
|
decider?: DeciderConfig;
|
|
569
638
|
/**
|
|
@@ -765,12 +834,19 @@ export interface TraceStep {
|
|
|
765
834
|
/** 0-indexed retry attempt for this iteration. Absent when no retry fired. */
|
|
766
835
|
retryAttempt?: number;
|
|
767
836
|
}
|
|
837
|
+
/**
|
|
838
|
+
* Which rung of the route ladder chose a conditional route (#357): a `when`
|
|
839
|
+
* expression, the decision model, or the agent's route evaluation.
|
|
840
|
+
*/
|
|
841
|
+
export type RouteRung = "expr" | "decider" | "agent";
|
|
768
842
|
/** A routing decision between nodes */
|
|
769
843
|
export interface TraceEdge {
|
|
770
844
|
from: string;
|
|
771
845
|
to: string;
|
|
772
846
|
/** Why this edge was chosen (condition text or "only path") */
|
|
773
847
|
reason: string;
|
|
848
|
+
/** Who chose it. Absent for an unconditional edge (nothing to decide). */
|
|
849
|
+
rung?: RouteRung;
|
|
774
850
|
}
|
|
775
851
|
/** Full execution trace — ordered steps + edges taken */
|
|
776
852
|
export interface ExecutionTrace {
|
|
@@ -780,8 +856,14 @@ export interface ExecutionTrace {
|
|
|
780
856
|
edges: TraceEdge[];
|
|
781
857
|
/** Resolved sources keyed by field path (e.g. "nodes.gather.instruction") */
|
|
782
858
|
sources: Record<string, _ResolvedSource>;
|
|
783
|
-
/**
|
|
859
|
+
/** One record per decision-model consultation (#357). Metadata only. Absent when no decider runs. */
|
|
784
860
|
decisions?: DeciderRecord[];
|
|
861
|
+
/**
|
|
862
|
+
* Why the decider did not run for some or all of this run's `route_by:
|
|
863
|
+
* decider` nodes (#357): missing operator config, `--no-decider`, a rejected
|
|
864
|
+
* URL, an open breaker or a spent call cap. Absent when nothing turned it off.
|
|
865
|
+
*/
|
|
866
|
+
deciderOff?: string;
|
|
785
867
|
}
|
|
786
868
|
/** Result of execute() — final node results + full execution trace */
|
|
787
869
|
export interface ExecutionResult {
|
package/dist/types.js
CHANGED
|
@@ -117,6 +117,28 @@ export const WORKFLOW_TYPES = [
|
|
|
117
117
|
"data_sync",
|
|
118
118
|
"generic",
|
|
119
119
|
];
|
|
120
|
+
/**
|
|
121
|
+
* Decision model gates (#357). Shared by the Zod schema, the JSON schema and
|
|
122
|
+
* the executor; kept here so schema-only (browser) consumers need no Node code.
|
|
123
|
+
*/
|
|
124
|
+
/** Default: accept a decider answer only at or above this confidence. */
|
|
125
|
+
export const DECIDER_MIN_CONFIDENCE = 0.85;
|
|
126
|
+
/** Default: ... and only when the top label leads the runner-up by at least this much. */
|
|
127
|
+
export const DECIDER_MIN_MARGIN = 0.2;
|
|
128
|
+
/** A workflow may lower `min_confidence` to this and no further. */
|
|
129
|
+
export const DECIDER_CONFIDENCE_FLOOR = 0.7;
|
|
130
|
+
/** A workflow may lower `min_margin` to this and no further. */
|
|
131
|
+
export const DECIDER_MARGIN_FLOOR = 0.1;
|
|
132
|
+
/** `route_by` values: who picks a node's natural-language route. */
|
|
133
|
+
export const ROUTE_BY = ["agent", "decider"];
|
|
134
|
+
/** Load error for a workflow that still declares the removed `decider.mode`. */
|
|
135
|
+
export const DECIDER_MODE_REMOVED = "decider.mode was removed with shadow mode. A decider now decides the routes of nodes that set `route_by: decider`, " +
|
|
136
|
+
"using the provider from operator config (.sweny.yml `decider:` or SWENY_DECIDER_URL and SWENY_DECIDER_MODEL). " +
|
|
137
|
+
"Delete the mode line; to keep routing with the agent only, delete the whole decider block.";
|
|
138
|
+
/** Load error for a workflow that declares the provider itself. */
|
|
139
|
+
export const DECIDER_PROVIDER_MOVED = "decider.provider is not allowed in a workflow: the endpoint and key come only from operator config " +
|
|
140
|
+
"(.sweny.yml `decider: { url, model }`, or SWENY_DECIDER_URL and SWENY_DECIDER_MODEL; the key only from SWENY_DECIDER_API_KEY). " +
|
|
141
|
+
"Move it there. A workflow may only set min_confidence and min_margin here, and `route_by: decider` on nodes.";
|
|
120
142
|
export const consoleLogger = {
|
|
121
143
|
info: (msg, data) => console.log(`[info] ${msg}`, data ?? ""),
|
|
122
144
|
warn: (msg, data) => console.warn(`[warn] ${msg}`, data ?? ""),
|
package/dist/when.d.ts
CHANGED
|
@@ -95,7 +95,14 @@ export interface ExpressionResult {
|
|
|
95
95
|
* Evaluate a parsed expression against node outputs. Never throws: a missing
|
|
96
96
|
* field or a type mismatch yields `{ value: false, problem }`.
|
|
97
97
|
*/
|
|
98
|
-
export declare function evaluateExpression(ast: ExprNode, scope: ExpressionScope
|
|
98
|
+
export declare function evaluateExpression(ast: ExprNode, scope: ExpressionScope, opts?: {
|
|
99
|
+
/**
|
|
100
|
+
* `node.field` paths whose value broke its declared type or enum. Any
|
|
101
|
+
* reference to one (or below it), `exists` included, is a problem: the
|
|
102
|
+
* value is unknown, which is neither present nor absent.
|
|
103
|
+
*/
|
|
104
|
+
invalid?: ReadonlySet<string>;
|
|
105
|
+
}): ExpressionResult;
|
|
99
106
|
/**
|
|
100
107
|
* Check an expression against the workflow it routes. Returns one message per
|
|
101
108
|
* problem, empty when the expression is valid:
|
package/dist/when.js
CHANGED
|
@@ -324,8 +324,14 @@ function lookup(scope, segments) {
|
|
|
324
324
|
* Evaluate a parsed expression against node outputs. Never throws: a missing
|
|
325
325
|
* field or a type mismatch yields `{ value: false, problem }`.
|
|
326
326
|
*/
|
|
327
|
-
export function evaluateExpression(ast, scope) {
|
|
327
|
+
export function evaluateExpression(ast, scope, opts = {}) {
|
|
328
|
+
const checkInvalid = (path) => {
|
|
329
|
+
if (opts.invalid && path.length >= 2 && opts.invalid.has(`${path[0]}.${path[1]}`)) {
|
|
330
|
+
throw new Abort(`field '${path[0]}.${path[1]}' does not match its declared type`);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
328
333
|
const read = (path) => {
|
|
334
|
+
checkInvalid(path);
|
|
329
335
|
const v = lookup(scope, path);
|
|
330
336
|
if (v === undefined)
|
|
331
337
|
throw new Abort(`field '${path.join(".")}' is missing`);
|
|
@@ -345,6 +351,7 @@ export function evaluateExpression(ast, scope) {
|
|
|
345
351
|
case "path":
|
|
346
352
|
return read(node.segments);
|
|
347
353
|
case "exists": {
|
|
354
|
+
checkInvalid(node.path.segments);
|
|
348
355
|
const v = lookup(scope, node.path.segments);
|
|
349
356
|
return v !== undefined && v !== null;
|
|
350
357
|
}
|
package/dist/workflow-builder.js
CHANGED
|
@@ -56,7 +56,9 @@ export function buildSystemPrompt(skills, existingWorkflow) {
|
|
|
56
56
|
"- Use snake_case for node IDs (e.g. gather_errors, create_ticket)",
|
|
57
57
|
"- Set `entry` to the first node in the flow",
|
|
58
58
|
"- Only reference skill IDs from the list above in node `skills` arrays",
|
|
59
|
-
|
|
59
|
+
'- Prefer `when: { expr: "..." }` expressions for edge conditions: declare the deciding values as enum, number, integer or boolean properties in the source node\'s `output`, then compare them (e.g. `{ "expr": "triage.severity in [\'high\', \'critical\']" }`). Expressions route with no model call and always the same way',
|
|
60
|
+
"- Use a natural-language `when` only for a fuzzy condition no declared field can express",
|
|
61
|
+
"- Put safety conditions (anything that gates a write, a PR or an issue) in expressions, never in natural language",
|
|
60
62
|
"- Every node must be reachable from the entry node",
|
|
61
63
|
"- Return ONLY the workflow JSON object — no markdown fences, no explanation",
|
|
62
64
|
];
|