@2kw/ai 6.2.0-dev.14 → 6.2.0-dev.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +2 -2
  2. package/dist/agent-config/export.d.ts +31 -0
  3. package/dist/agent-config/export.js +87 -0
  4. package/dist/agent-config/file.d.ts +20 -0
  5. package/dist/agent-config/file.js +137 -0
  6. package/dist/agent-config/plan.d.ts +33 -0
  7. package/dist/agent-config/plan.js +100 -0
  8. package/dist/agent-config/schema.d.ts +420 -0
  9. package/dist/agent-config/schema.js +222 -0
  10. package/dist/agent-config/template.d.ts +6 -0
  11. package/dist/agent-config/template.js +59 -0
  12. package/dist/agent-config/validate.d.ts +29 -0
  13. package/dist/agent-config/validate.js +123 -0
  14. package/dist/commands/agent-apply.d.ts +9 -0
  15. package/dist/commands/agent-apply.js +176 -0
  16. package/dist/commands/agent-export.d.ts +3 -0
  17. package/dist/commands/agent-export.js +100 -0
  18. package/dist/commands/agent-init.d.ts +4 -0
  19. package/dist/commands/agent-init.js +54 -0
  20. package/dist/commands/agent-run.d.ts +5 -0
  21. package/dist/commands/agent-run.js +187 -0
  22. package/dist/commands/agent-versions.js +12 -7
  23. package/dist/commands/agents.js +24 -9
  24. package/dist/commands/ai.d.ts +5 -37
  25. package/dist/commands/ai.js +29 -136
  26. package/dist/lib/agent-decide.d.ts +40 -0
  27. package/dist/lib/agent-decide.js +128 -0
  28. package/dist/lib/agent-lookup.d.ts +14 -0
  29. package/dist/lib/agent-lookup.js +70 -0
  30. package/dist/lib/agent-models.d.ts +28 -0
  31. package/dist/lib/agent-models.js +53 -0
  32. package/dist/lib/agent-run.d.ts +83 -0
  33. package/dist/lib/agent-run.js +170 -0
  34. package/dist/lib/approval-prompt.d.ts +18 -0
  35. package/dist/lib/approval-prompt.js +84 -0
  36. package/dist/lib/client.d.ts +2 -0
  37. package/dist/lib/client.js +13 -2
  38. package/dist/lib/errors.d.ts +21 -2
  39. package/dist/lib/errors.js +46 -2
  40. package/package.json +5 -2
package/README.md CHANGED
@@ -99,7 +99,7 @@ kubectl-style contexts switch between organizations and environments:
99
99
  | `prompts` | Versioned prompt management, labels, compilation, testing |
100
100
  | `extractions` | Create extractions (text, file, images), list, re-run, estimate tokens |
101
101
  | `convert` | Convert PDF, DOCX, XLSX, images, and 20+ formats to Markdown/text/HTML/JSON |
102
- | `ai` | Chat completions, Responses (agent or direct model, streaming), and model listing via the OpenAI-compatible gateway |
102
+ | `ai` | Chat completions, Responses (agent or direct model), and model listing via the OpenAI-compatible gateway |
103
103
  | `transcribe` | Transcribe audio (FLAC, MP3, MP4, OGG, WAV, WebM) |
104
104
  | `datasets` | Build datasets and dataset versions for evaluation |
105
105
  | `experiments` | Run experiments against datasets |
@@ -109,7 +109,7 @@ kubectl-style contexts switch between organizations and environments:
109
109
  | `providers` | Manage BYOK AI providers |
110
110
  | `analytics` | Usage analytics: spend, quality, providers, errors |
111
111
  | `billing` | Check subscription tier and usage limits |
112
- | `agents` | Manage agents, versions, labels, approvals, and tool catalogs |
112
+ | `agents` | Run agents and decide approvals; apply, export and scaffold agent.yaml; manage versions, labels, approvals and tool catalogs |
113
113
  | `conversations` | Create and manage conversations and their items |
114
114
  | `knowledge` | Manage knowledge bases, documents, search, and citations |
115
115
  | `files` | Upload, download, and manage files |
@@ -0,0 +1,31 @@
1
+ import type { AgentConfig } from "./validate.js";
2
+ export declare function secretEnvName(toolName: string): string;
3
+ /**
4
+ * `apply` resolves `${NAME}` in every string value and reads `$${` as a literal `${`. Stored text is
5
+ * escaped so an exported file re-applies unchanged; run before `redactSecrets` inserts real placeholders.
6
+ */
7
+ export declare function escapePlaceholders<T>(value: T): T;
8
+ /** The API returns webhook secrets in plaintext (#669); exported files carry placeholders instead. */
9
+ export declare function redactSecrets(tools: Record<string, unknown>[] | undefined): {
10
+ tools: Record<string, unknown>[] | undefined;
11
+ envNames: string[];
12
+ };
13
+ /**
14
+ * Tool entries the backend stores but never runs (planner/runtime-injected or executor-less,
15
+ * e.g. a console-stored `web_search`). agent.yaml cannot author them, so export drops them.
16
+ */
17
+ export declare function dropInertTools(tools: Record<string, unknown>[] | undefined): {
18
+ tools: Record<string, unknown>[] | undefined;
19
+ dropped: string[];
20
+ };
21
+ export declare function versionToConfig(agent: {
22
+ name: string;
23
+ description?: string | null;
24
+ }, version: Record<string, unknown>): AgentConfig;
25
+ export declare function toAgentYaml(config: AgentConfig, meta: {
26
+ agent: string;
27
+ label: string;
28
+ versionNumber: number | null;
29
+ date: string;
30
+ }): string;
31
+ //# sourceMappingURL=export.d.ts.map
@@ -0,0 +1,87 @@
1
+ import { stringify } from "yaml";
2
+ import { CliUsageError } from "../lib/errors.js";
3
+ import { isEmptyValue } from "./plan.js";
4
+ import { AGENT_SCHEMA_ID, NON_AUTHORABLE_TOOL_TYPES } from "./schema.js";
5
+ export function secretEnvName(toolName) {
6
+ return `AGENT_${toolName.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_SECRET`;
7
+ }
8
+ /**
9
+ * `apply` resolves `${NAME}` in every string value and reads `$${` as a literal `${`. Stored text is
10
+ * escaped so an exported file re-applies unchanged; run before `redactSecrets` inserts real placeholders.
11
+ */
12
+ export function escapePlaceholders(value) {
13
+ const walk = (v) => {
14
+ // A function replacer: in a replacement string `$$` would collapse to a single `$`.
15
+ if (typeof v === "string")
16
+ return v.replaceAll("${", () => "$${");
17
+ if (Array.isArray(v))
18
+ return v.map(walk);
19
+ if (v && typeof v === "object")
20
+ return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x)]));
21
+ return v;
22
+ };
23
+ return walk(value);
24
+ }
25
+ /** The API returns webhook secrets in plaintext (#669); exported files carry placeholders instead. */
26
+ export function redactSecrets(tools) {
27
+ if (!tools)
28
+ return { tools, envNames: [] };
29
+ // env name → the tool that claimed it; two tools sharing one variable would get one secret on re-apply.
30
+ const owners = new Map();
31
+ const redacted = tools.map((tool) => {
32
+ if (typeof tool.secret !== "string" || tool.secret === "")
33
+ return tool;
34
+ const toolName = String(tool.name ?? "tool");
35
+ const name = secretEnvName(toolName);
36
+ const owner = owners.get(name);
37
+ if (owner !== undefined) {
38
+ throw new CliUsageError(`Tools "${owner}" and "${toolName}" both map to ${name}; rename one tool or export with --include-secrets.`);
39
+ }
40
+ owners.set(name, toolName);
41
+ return { ...tool, secret: `\${${name}}` };
42
+ });
43
+ return { tools: redacted, envNames: [...owners.keys()] };
44
+ }
45
+ /**
46
+ * Tool entries the backend stores but never runs (planner/runtime-injected or executor-less,
47
+ * e.g. a console-stored `web_search`). agent.yaml cannot author them, so export drops them.
48
+ */
49
+ export function dropInertTools(tools) {
50
+ if (!tools)
51
+ return { tools, dropped: [] };
52
+ const dropped = [];
53
+ const kept = tools.filter((tool) => {
54
+ const type = tool.type;
55
+ if (typeof type === "string" && Object.hasOwn(NON_AUTHORABLE_TOOL_TYPES, type)) {
56
+ dropped.push(type);
57
+ return false;
58
+ }
59
+ return true;
60
+ });
61
+ return { tools: kept.length ? kept : undefined, dropped };
62
+ }
63
+ export function versionToConfig(agent, version) {
64
+ const config = { name: agent.name };
65
+ if (!isEmptyValue(agent.description))
66
+ config.description = agent.description;
67
+ // One model exports as `model`, as before lists existed; a list exports as `models` alone (model is its first entry).
68
+ const models = Array.isArray(version.models) ? version.models : [];
69
+ if (models.length > 1)
70
+ config.models = models;
71
+ else
72
+ config.model = version.model ?? models[0];
73
+ for (const field of ["instructions", "tools", "hitlPolicy", "options", "skills"]) {
74
+ if (!isEmptyValue(version[field]))
75
+ config[field] = version[field];
76
+ }
77
+ return config;
78
+ }
79
+ export function toAgentYaml(config, meta) {
80
+ const version = meta.versionNumber !== null ? ` (version ${meta.versionNumber})` : "";
81
+ const header = [
82
+ `# yaml-language-server: $schema=${AGENT_SCHEMA_ID}`,
83
+ `# exported from ${meta.agent}@${meta.label}${version} on ${meta.date}`,
84
+ ].join("\n");
85
+ return `${header}\n${stringify(config, { lineWidth: 0 })}`;
86
+ }
87
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1,20 @@
1
+ import { type AgentConfig } from "./validate.js";
2
+ export declare function parseAgentText(text: string, format: "yaml" | "json", source: string): unknown;
3
+ /**
4
+ * Resolves `${NAME}` placeholders in every string of `value` from `env`; `$${` stays a literal `${`.
5
+ * A resolved value is reported as a secret only if it was resolved inside the string value of a key
6
+ * named `secret` (at any depth), or its variable name matches SECRET|TOKEN|PASSWORD|PASSWD|PASS|API_?KEY|
7
+ * KEY|PRIVATE|CREDENTIAL|AUTH. Other resolved values (hosts, model names) are not secrets. Empty values
8
+ * are never secrets. Missing names are listed once each, in first-seen order.
9
+ */
10
+ export declare function interpolateEnv(value: unknown, env: Record<string, string | undefined>): {
11
+ value: unknown;
12
+ missing: string[];
13
+ secrets: string[];
14
+ };
15
+ export declare function maskSecrets<T>(value: T, secrets: string[]): T;
16
+ export declare function loadAgentConfig(path: string, env: Record<string, string | undefined>, readStdin?: () => string): {
17
+ config: AgentConfig;
18
+ secrets: string[];
19
+ };
20
+ //# sourceMappingURL=file.d.ts.map
@@ -0,0 +1,137 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { extname } from "node:path";
3
+ import { parse, YAMLParseError } from "yaml";
4
+ import { CliUsageError } from "../lib/errors.js";
5
+ import { validateAgentConfig } from "./validate.js";
6
+ const PLACEHOLDER = /\$\$\{|\$\{([A-Z_][A-Z0-9_]*)\}/g;
7
+ /** Variable names whose value is a secret wherever it is used. */
8
+ const SECRET_NAME = /(SECRET|TOKEN|PASSWORD|PASSWD|PASS|API_?KEY|KEY|PRIVATE|CREDENTIAL|AUTH)/;
9
+ const BOM = "\uFEFF";
10
+ export function parseAgentText(text, format, source) {
11
+ const body = text.startsWith(BOM) ? text.slice(BOM.length) : text;
12
+ if (format === "json") {
13
+ try {
14
+ return JSON.parse(body);
15
+ }
16
+ catch (err) {
17
+ throw new CliUsageError(`${source}: ${err.message}`);
18
+ }
19
+ }
20
+ try {
21
+ return parse(body);
22
+ }
23
+ catch (err) {
24
+ if (err instanceof YAMLParseError) {
25
+ const pos = err.linePos?.[0];
26
+ const where = pos ? `${pos.line}:${pos.col}` : "?";
27
+ // The first line ends with " at line L, column C:", which the prefix already says.
28
+ const first = err.message.split("\n")[0];
29
+ const at = first.indexOf(" at line ");
30
+ throw new CliUsageError(`${source}:${where}: ${at === -1 ? first : first.slice(0, at)}`);
31
+ }
32
+ // e.g. an unresolved alias throws a ReferenceError: still a problem with the file, not the CLI.
33
+ throw new CliUsageError(`${source}: ${err instanceof Error ? err.message : String(err)}`);
34
+ }
35
+ }
36
+ /**
37
+ * Resolves `${NAME}` placeholders in every string of `value` from `env`; `$${` stays a literal `${`.
38
+ * A resolved value is reported as a secret only if it was resolved inside the string value of a key
39
+ * named `secret` (at any depth), or its variable name matches SECRET|TOKEN|PASSWORD|PASSWD|PASS|API_?KEY|
40
+ * KEY|PRIVATE|CREDENTIAL|AUTH. Other resolved values (hosts, model names) are not secrets. Empty values
41
+ * are never secrets. Missing names are listed once each, in first-seen order.
42
+ */
43
+ export function interpolateEnv(value, env) {
44
+ const missing = new Set();
45
+ const secrets = new Set();
46
+ const walk = (v, key) => {
47
+ if (typeof v === "string") {
48
+ return v.replace(PLACEHOLDER, (match, name) => {
49
+ if (!name)
50
+ return "${";
51
+ const resolved = env[name];
52
+ if (resolved === undefined) {
53
+ missing.add(name);
54
+ return match;
55
+ }
56
+ if (resolved && (key === "secret" || SECRET_NAME.test(name)))
57
+ secrets.add(resolved);
58
+ return resolved;
59
+ });
60
+ }
61
+ if (Array.isArray(v))
62
+ return v.map((x) => walk(x));
63
+ if (v && typeof v === "object")
64
+ return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x, k)]));
65
+ return v;
66
+ };
67
+ const out = walk(value);
68
+ return { value: out, missing: [...missing], secrets: [...secrets] };
69
+ }
70
+ /** Replaces every occurrence of every secret in `text` with `***`; overlapping or adjacent matches become one mask. */
71
+ function maskText(text, secrets) {
72
+ const ranges = [];
73
+ for (const secret of secrets) {
74
+ for (let at = text.indexOf(secret); at !== -1; at = text.indexOf(secret, at + 1)) {
75
+ ranges.push([at, at + secret.length]);
76
+ }
77
+ }
78
+ if (!ranges.length)
79
+ return text;
80
+ ranges.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
81
+ const merged = [];
82
+ for (const [start, end] of ranges) {
83
+ const last = merged[merged.length - 1];
84
+ if (last && start <= last[1])
85
+ last[1] = Math.max(last[1], end);
86
+ else
87
+ merged.push([start, end]);
88
+ }
89
+ let out = "";
90
+ let cursor = 0;
91
+ for (const [start, end] of merged) {
92
+ out += `${text.slice(cursor, start)}***`;
93
+ cursor = end;
94
+ }
95
+ return out + text.slice(cursor);
96
+ }
97
+ export function maskSecrets(value, secrets) {
98
+ const known = [...new Set(secrets.filter((s) => s.length > 0))].sort((a, b) => b.length - a.length);
99
+ const walk = (v, key) => {
100
+ if (key === "secret" && typeof v === "string")
101
+ return "***";
102
+ if (typeof v === "string")
103
+ return maskText(v, known);
104
+ if (Array.isArray(v))
105
+ return v.map((x) => walk(x));
106
+ if (v && typeof v === "object")
107
+ return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x, k)]));
108
+ return v;
109
+ };
110
+ return walk(value);
111
+ }
112
+ function readAgentFile(path) {
113
+ try {
114
+ return readFileSync(path, "utf-8");
115
+ }
116
+ catch (err) {
117
+ const code = err.code;
118
+ throw new CliUsageError(`${path}: cannot read file (${code ?? err.message})`);
119
+ }
120
+ }
121
+ export function loadAgentConfig(path, env, readStdin = () => readFileSync(0, "utf-8")) {
122
+ const fromStdin = path === "-";
123
+ const source = fromStdin ? "stdin" : path;
124
+ const text = fromStdin ? readStdin() : readAgentFile(path);
125
+ const format = !fromStdin && extname(path).toLowerCase() === ".json" ? "json" : "yaml";
126
+ const parsed = parseAgentText(text, format, source);
127
+ const { value, missing, secrets } = interpolateEnv(parsed, env);
128
+ if (missing.length) {
129
+ throw new CliUsageError(`Missing environment variables: ${missing.join(", ")}`);
130
+ }
131
+ const issues = validateAgentConfig(value);
132
+ if (issues.length) {
133
+ throw new CliUsageError(`${source} is invalid:\n${issues.map((i) => ` ${i.pointer}: ${i.message}`).join("\n")}`);
134
+ }
135
+ return { config: value, secrets };
136
+ }
137
+ //# sourceMappingURL=file.js.map
@@ -0,0 +1,33 @@
1
+ import type { AgentConfig } from "./validate.js";
2
+ /** Version fields a file carries; any change publishes a new version. */
3
+ export declare const CONFIG_FIELDS: readonly ["model", "models", "instructions", "tools", "hitlPolicy", "options", "skills"];
4
+ /**
5
+ * The ordered model list a file or a stored version stands for (#591): `models` when non-empty, else `[model]`.
6
+ * The backend stores both (`model` = `models[0]`) and backfills `models` from `model`, so the list is what compares.
7
+ */
8
+ export declare function effectiveModels(source: {
9
+ model?: unknown;
10
+ models?: unknown;
11
+ } | undefined): string[];
12
+ export type ApplyAction = "created" | "versioned" | "updated" | "unchanged";
13
+ export interface FieldChange {
14
+ field: string;
15
+ from: unknown;
16
+ to: unknown;
17
+ }
18
+ export interface ApplyPlan {
19
+ action: ApplyAction;
20
+ changes: FieldChange[];
21
+ configChanged: boolean;
22
+ descriptionChanged: boolean;
23
+ }
24
+ export declare function isEmptyValue(v: unknown): boolean;
25
+ /** Stable JSON: object keys sorted at every depth (jsonb does not keep key order). */
26
+ export declare function canonical(v: unknown): string;
27
+ export declare function planApply(config: AgentConfig, existing?: {
28
+ agent: {
29
+ description?: string | null;
30
+ };
31
+ latest?: Record<string, unknown>;
32
+ }): ApplyPlan;
33
+ //# sourceMappingURL=plan.d.ts.map
@@ -0,0 +1,100 @@
1
+ /** Version fields a file carries; any change publishes a new version. */
2
+ export const CONFIG_FIELDS = ["model", "models", "instructions", "tools", "hitlPolicy", "options", "skills"];
3
+ /**
4
+ * The ordered model list a file or a stored version stands for (#591): `models` when non-empty, else `[model]`.
5
+ * The backend stores both (`model` = `models[0]`) and backfills `models` from `model`, so the list is what compares.
6
+ */
7
+ export function effectiveModels(source) {
8
+ if (Array.isArray(source?.models) && source.models.length > 0)
9
+ return source.models.map(String);
10
+ return typeof source?.model === "string" && source.model !== "" ? [source.model] : [];
11
+ }
12
+ export function isEmptyValue(v) {
13
+ if (v === null || v === undefined || v === "")
14
+ return true;
15
+ if (Array.isArray(v))
16
+ return v.length === 0;
17
+ if (typeof v === "object")
18
+ return Object.keys(v).length === 0;
19
+ return false;
20
+ }
21
+ /** Stable JSON: object keys sorted at every depth (jsonb does not keep key order). */
22
+ export function canonical(v) {
23
+ const sort = (x) => {
24
+ if (Array.isArray(x))
25
+ return x.map(sort);
26
+ if (x && typeof x === "object") {
27
+ return Object.fromEntries(Object.keys(x).sort().map((k) => [k, sort(x[k])]));
28
+ }
29
+ return x;
30
+ };
31
+ return JSON.stringify(sort(v)) ?? "undefined";
32
+ }
33
+ function same(a, b) {
34
+ if (isEmptyValue(a) && isEmptyValue(b))
35
+ return true;
36
+ return canonical(a) === canonical(b);
37
+ }
38
+ const shown = (v) => (isEmptyValue(v) ? undefined : v);
39
+ /**
40
+ * The backend stores every skill binding with an explicit, trimmed ref: a missing or null ref becomes "latest"
41
+ * (SkillBindings.java), so a missing or blank ref compares as "latest" and any other ref compares trimmed.
42
+ */
43
+ function withDefaultRefs(field, v) {
44
+ if (field !== "skills" || !Array.isArray(v))
45
+ return v;
46
+ return v.map((b) => {
47
+ if (!b || typeof b !== "object")
48
+ return b;
49
+ const ref = b.ref;
50
+ if (ref == null)
51
+ return { ...b, ref: "latest" };
52
+ if (typeof ref !== "string")
53
+ return b;
54
+ return { ...b, ref: ref.trim() === "" ? "latest" : ref.trim() };
55
+ });
56
+ }
57
+ export function planApply(config, existing) {
58
+ const desired = config;
59
+ const changes = [];
60
+ const descriptionFrom = existing?.agent.description;
61
+ const descriptionChanged = !same(descriptionFrom, config.description);
62
+ if (descriptionChanged)
63
+ changes.push({ field: "description", from: shown(descriptionFrom), to: shown(config.description) });
64
+ let configChanged = false;
65
+ // Order matters: the first entry is the default. A single-model change reads as `model`, as before lists existed.
66
+ const modelsFrom = effectiveModels(existing?.latest);
67
+ const modelsTo = effectiveModels(config);
68
+ if (canonical(modelsFrom) !== canonical(modelsTo)) {
69
+ configChanged = true;
70
+ if (modelsFrom.length <= 1 && modelsTo.length <= 1) {
71
+ changes.push({ field: "model", from: modelsFrom[0], to: modelsTo[0] });
72
+ }
73
+ else {
74
+ changes.push({ field: "models", from: shown(modelsFrom), to: shown(modelsTo) });
75
+ }
76
+ }
77
+ for (const field of CONFIG_FIELDS) {
78
+ if (field === "model" || field === "models")
79
+ continue;
80
+ const from = existing?.latest?.[field];
81
+ if (!same(withDefaultRefs(field, from), withDefaultRefs(field, desired[field]))) {
82
+ configChanged = true;
83
+ changes.push({ field, from: shown(from), to: shown(desired[field]) });
84
+ }
85
+ }
86
+ // An existing agent without any version always needs its first version, even if the diff is empty.
87
+ if (existing && !existing.latest)
88
+ configChanged = true;
89
+ let action;
90
+ if (!existing)
91
+ action = "created";
92
+ else if (configChanged)
93
+ action = "versioned";
94
+ else if (descriptionChanged)
95
+ action = "updated";
96
+ else
97
+ action = "unchanged";
98
+ return { action, changes, configChanged, descriptionChanged };
99
+ }
100
+ //# sourceMappingURL=plan.js.map