@things-factory/ai-client-base 10.1.149 → 10.1.155
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/config/config.development.js +2 -68
- package/config/config.production.js +2 -20
- package/dist-server/service/agent/agent-directory.d.ts +71 -0
- package/dist-server/service/agent/agent-directory.js +129 -0
- package/dist-server/service/agent/agent-directory.js.map +1 -0
- package/dist-server/service/agent/agent-settings-save.d.ts +38 -0
- package/dist-server/service/agent/agent-settings-save.js +153 -0
- package/dist-server/service/agent/agent-settings-save.js.map +1 -0
- package/dist-server/service/agent/agent-spec.d.ts +43 -0
- package/dist-server/service/agent/agent-spec.js +83 -0
- package/dist-server/service/agent/agent-spec.js.map +1 -0
- package/dist-server/service/agent/ai-client-for.d.ts +53 -0
- package/dist-server/service/agent/ai-client-for.js +166 -0
- package/dist-server/service/agent/ai-client-for.js.map +1 -0
- package/dist-server/service/agent/ai-usage-summary.d.ts +37 -0
- package/dist-server/service/agent/ai-usage-summary.js +108 -0
- package/dist-server/service/agent/ai-usage-summary.js.map +1 -0
- package/dist-server/service/agent/ai-usage.d.ts +35 -0
- package/dist-server/service/agent/ai-usage.js +118 -0
- package/dist-server/service/agent/ai-usage.js.map +1 -0
- package/dist-server/service/agent/index.d.ts +7 -0
- package/dist-server/service/agent/index.js +13 -0
- package/dist-server/service/agent/index.js.map +1 -0
- package/dist-server/service/agent/provider-models.d.ts +12 -0
- package/dist-server/service/agent/provider-models.js +37 -0
- package/dist-server/service/agent/provider-models.js.map +1 -0
- package/dist-server/service/capability.d.ts +21 -10
- package/dist-server/service/capability.js +20 -49
- package/dist-server/service/capability.js.map +1 -1
- package/dist-server/service/factory.d.ts +7 -12
- package/dist-server/service/factory.js +0 -45
- package/dist-server/service/factory.js.map +1 -1
- package/dist-server/service/index.d.ts +3 -0
- package/dist-server/service/index.js +6 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/providers/anthropic.d.ts +2 -0
- package/dist-server/service/providers/anthropic.js +16 -0
- package/dist-server/service/providers/anthropic.js.map +1 -1
- package/dist-server/service/providers/gemini.d.ts +13 -0
- package/dist-server/service/providers/gemini.js +52 -12
- package/dist-server/service/providers/gemini.js.map +1 -1
- package/dist-server/service/providers/openai.d.ts +6 -0
- package/dist-server/service/providers/openai.js +34 -8
- package/dist-server/service/providers/openai.js.map +1 -1
- package/dist-server/service/types.d.ts +16 -2
- package/dist-server/service/types.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -2
- package/server/service/agent/agent-directory.test.ts +206 -0
- package/server/service/agent/agent-directory.ts +198 -0
- package/server/service/agent/agent-settings-save.test.ts +151 -0
- package/server/service/agent/agent-settings-save.ts +205 -0
- package/server/service/agent/agent-spec.ts +103 -0
- package/server/service/agent/ai-client-for.test.ts +141 -0
- package/server/service/agent/ai-client-for.ts +201 -0
- package/server/service/agent/ai-usage-summary.test.ts +65 -0
- package/server/service/agent/ai-usage-summary.ts +146 -0
- package/server/service/agent/ai-usage.ts +97 -0
- package/server/service/agent/index.ts +7 -0
- package/server/service/agent/one-door.test.ts +57 -0
- package/server/service/agent/provider-models.ts +43 -0
- package/server/service/capability.test.ts +26 -55
- package/server/service/capability.ts +25 -42
- package/server/service/factory.ts +4 -44
- package/server/service/index.ts +6 -1
- package/server/service/providers/anthropic.ts +17 -0
- package/server/service/providers/gemini.ts +54 -12
- package/server/service/providers/openai-models.test.ts +30 -0
- package/server/service/providers/openai.ts +34 -8
- package/server/service/types.ts +18 -2
- package/translations/en.json +13 -0
- package/translations/es.json +13 -0
- package/translations/ja.json +13 -0
- package/translations/ko.json +13 -0
- package/translations/ms.json +13 -0
- package/translations/zh.json +13 -0
|
@@ -17,73 +17,7 @@ module.exports = {
|
|
|
17
17
|
timeoutMs: 300
|
|
18
18
|
},
|
|
19
19
|
/*
|
|
20
|
-
AI
|
|
21
|
-
|
|
22
|
-
Provider-agnostic configuration for the default AI client used by board-ai
|
|
23
|
-
and any other module that calls `getDefaultAIClient()`.
|
|
24
|
-
|
|
25
|
-
To activate, override this in your application's config (config.development.js
|
|
26
|
-
or config.production.js) with your provider/key.
|
|
27
|
-
|
|
28
|
-
Currently supported providers (registered via @things-factory/ai-client-base):
|
|
29
|
-
- 'anthropic' — Anthropic Claude (default model: claude-sonnet-4-6)
|
|
30
|
-
- 'gemini' — Google Gemini (default model: gemini-3-pro)
|
|
31
|
-
- 'openai' — OpenAI GPT (default model: gpt-4o)
|
|
32
|
-
|
|
33
|
-
Example for Anthropic Claude:
|
|
34
|
-
|
|
35
|
-
aiClient: {
|
|
36
|
-
provider: 'anthropic',
|
|
37
|
-
model: 'claude-sonnet-4-6',
|
|
38
|
-
apiKey: process.env.ANTHROPIC_API_KEY
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
Example for Google Gemini:
|
|
42
|
-
|
|
43
|
-
aiClient: {
|
|
44
|
-
provider: 'gemini',
|
|
45
|
-
model: 'gemini-3-pro',
|
|
46
|
-
apiKey: process.env.GOOGLE_API_KEY
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
Example for on-prem / proxy / Bedrock-style endpoint:
|
|
50
|
-
|
|
51
|
-
aiClient: {
|
|
52
|
-
provider: 'anthropic',
|
|
53
|
-
model: 'claude-sonnet-4-6',
|
|
54
|
-
apiKey: 'internal-key',
|
|
55
|
-
baseURL: 'https://internal-llm-gateway.example.com'
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
Fields:
|
|
59
|
-
- provider : Registered adapter name. Required.
|
|
60
|
-
- model : Model ID. Optional; falls back to adapter default.
|
|
61
|
-
- apiKey : API key. If omitted, SDK-level environment fallback applies
|
|
62
|
-
(Anthropic SDK: ANTHROPIC_API_KEY; Gemini adapter: GOOGLE_API_KEY,
|
|
63
|
-
GEMINI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY).
|
|
64
|
-
- baseURL : Optional. On-prem / proxy / compatible endpoint.
|
|
65
|
-
|
|
66
|
-
Example for OpenAI:
|
|
67
|
-
|
|
68
|
-
aiClient: {
|
|
69
|
-
provider: 'openai',
|
|
70
|
-
model: 'gpt-4o',
|
|
71
|
-
apiKey: process.env.OPENAI_API_KEY,
|
|
72
|
-
organization: process.env.OPENAI_ORGANIZATION // optional
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
Note on the legacy `openai` config key:
|
|
76
|
-
@things-factory/codelingua uses a separate `openai: { organization, apiKey }` key.
|
|
77
|
-
The two keys are intentionally independent — codelingua keeps using `openai`,
|
|
78
|
-
ai-client-base only reads `aiClient`. If you want OpenAI here too, set `aiClient`
|
|
79
|
-
explicitly with provider:'openai' (the values may duplicate codelingua's,
|
|
80
|
-
that's fine — they live in different modules).
|
|
81
|
-
|
|
82
|
-
If `aiClient` is absent, no default client is created — chat() calls will fail
|
|
83
|
-
with a clear message. Modules can still be used with an explicitly-passed AIClient
|
|
84
|
-
via setDefaultAIClient(...).
|
|
20
|
+
There is no deployment-wide AI client. Each domain sets its AI agents (provider, model, API key) in
|
|
21
|
+
Settings, and a domain with no agent has no AI - see operato-application design/ai/agent-settings.md.
|
|
85
22
|
*/
|
|
86
|
-
// aiClient: {
|
|
87
|
-
// provider: 'anthropic'
|
|
88
|
-
// }
|
|
89
23
|
}
|
|
@@ -9,25 +9,7 @@ module.exports = {
|
|
|
9
9
|
timeoutMs: 300
|
|
10
10
|
},
|
|
11
11
|
/*
|
|
12
|
-
AI
|
|
13
|
-
|
|
14
|
-
See config.development.js for the full reference and provider examples.
|
|
15
|
-
|
|
16
|
-
Override this in your application's production config with the actual
|
|
17
|
-
provider and credentials. Never commit real API keys to the source tree.
|
|
18
|
-
|
|
19
|
-
Recommended pattern — read from environment in your application config:
|
|
20
|
-
|
|
21
|
-
aiClient: {
|
|
22
|
-
provider: process.env.AI_PROVIDER || 'anthropic',
|
|
23
|
-
model: process.env.AI_MODEL,
|
|
24
|
-
apiKey: process.env.AI_API_KEY
|
|
25
|
-
|| process.env.ANTHROPIC_API_KEY
|
|
26
|
-
|| process.env.GOOGLE_API_KEY,
|
|
27
|
-
baseURL: process.env.AI_BASE_URL
|
|
28
|
-
}
|
|
12
|
+
There is no deployment-wide AI client. Each domain sets its AI agents (provider, model, API key) in
|
|
13
|
+
Settings, and a domain with no agent has no AI - see operato-application design/ai/agent-settings.md.
|
|
29
14
|
*/
|
|
30
|
-
// aiClient: {
|
|
31
|
-
// provider: 'anthropic'
|
|
32
|
-
// }
|
|
33
15
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Domain } from '@things-factory/shell';
|
|
2
|
+
import { type AiAgentSpec, type AiAgentsSetting } from './agent-spec.js';
|
|
3
|
+
/**
|
|
4
|
+
* Which agents a domain can use, read up its chain of domains (design/ai/agent-settings.md §3-3).
|
|
5
|
+
*
|
|
6
|
+
* A domain sees its own agents and every agent of the domains above it. The definition belongs to the domain that
|
|
7
|
+
* declared it; the key is the closest `AiAgent::<id>::apiKey` EnvVar, so a domain below can answer an inherited agent
|
|
8
|
+
* with a key of its own.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Where the key an agent answers with comes from, seen from the domain asking:
|
|
12
|
+
* own - this domain's key for its own agent
|
|
13
|
+
* inherited - a domain above holds the key
|
|
14
|
+
* overridden - an inherited agent answered with this domain's key
|
|
15
|
+
* missing - no key anywhere on the chain; the agent cannot answer
|
|
16
|
+
*/
|
|
17
|
+
export type AiAgentKeyState = 'own' | 'inherited' | 'overridden' | 'missing';
|
|
18
|
+
export interface AiAgentEntry {
|
|
19
|
+
spec: AiAgentSpec;
|
|
20
|
+
/** The domain that declared the agent. */
|
|
21
|
+
ownerDomainId: string;
|
|
22
|
+
inherited: boolean;
|
|
23
|
+
/** An inherited agent this domain turned off for itself. */
|
|
24
|
+
turnedOff: boolean;
|
|
25
|
+
keyState: AiAgentKeyState;
|
|
26
|
+
/** The domain whose key answers. */
|
|
27
|
+
keyDomainId?: string;
|
|
28
|
+
/** Shown only for a key of this domain (ADR-0112 decision 5). A key from above is never hinted below. */
|
|
29
|
+
keyHint?: string;
|
|
30
|
+
/** Active, not turned off here, and has a key. */
|
|
31
|
+
usable: boolean;
|
|
32
|
+
/** Server-only - never put on a GraphQL type. */
|
|
33
|
+
apiKey?: string;
|
|
34
|
+
/** For a client cache: changes whenever the key does. */
|
|
35
|
+
keyVersion?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface AiAgentDirectory {
|
|
38
|
+
domainId: string;
|
|
39
|
+
/** The domain and its ancestors, closest first, with names for the screen. */
|
|
40
|
+
chain: {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
}[];
|
|
44
|
+
/** This domain's own agents first, then each ancestor's, closest first. */
|
|
45
|
+
entries: AiAgentEntry[];
|
|
46
|
+
/** This domain's stored setting, as parsed. */
|
|
47
|
+
own: AiAgentsSetting;
|
|
48
|
+
/** The agent a request without a choice gets, and how it was decided. */
|
|
49
|
+
defaultAgentId?: string;
|
|
50
|
+
defaultFrom: 'own' | 'inherited' | 'first' | 'none';
|
|
51
|
+
}
|
|
52
|
+
export declare function readAiAgentDirectory(given: Domain | {
|
|
53
|
+
id: string;
|
|
54
|
+
}): Promise<AiAgentDirectory>;
|
|
55
|
+
/**
|
|
56
|
+
* A caller that holds only the domain's id (a background job, a webhook) still gets the domain's whole chain: the walk
|
|
57
|
+
* needs `parentId`, which an id alone does not carry.
|
|
58
|
+
*/
|
|
59
|
+
export declare function fullDomain(given: Domain | {
|
|
60
|
+
id: string;
|
|
61
|
+
}): Promise<Domain>;
|
|
62
|
+
/** Every domain below this one, any depth. */
|
|
63
|
+
export declare function readDescendantDomains(domain: Domain): Promise<Domain[]>;
|
|
64
|
+
/**
|
|
65
|
+
* For each of this domain's agents, the domains below that use it - every descendant that has not turned it off.
|
|
66
|
+
* The owner sees these before deleting, turning off or rekeying an agent (§3-3 point 8).
|
|
67
|
+
*/
|
|
68
|
+
export declare function readAgentUsersBelow(domain: Domain, agentIds: string[]): Promise<Map<string, {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
}[]>>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readAiAgentDirectory = readAiAgentDirectory;
|
|
4
|
+
exports.fullDomain = fullDomain;
|
|
5
|
+
exports.readDescendantDomains = readDescendantDomains;
|
|
6
|
+
exports.readAgentUsersBelow = readAgentUsersBelow;
|
|
7
|
+
const typeorm_1 = require("typeorm");
|
|
8
|
+
const shell_1 = require("@things-factory/shell");
|
|
9
|
+
const setting_base_1 = require("@things-factory/setting-base");
|
|
10
|
+
const agent_spec_js_1 = require("./agent-spec.js");
|
|
11
|
+
async function readAiAgentDirectory(given) {
|
|
12
|
+
const domain = await fullDomain(given);
|
|
13
|
+
const chainIds = await (0, shell_1.getDomainIdsWithAncestors)(domain);
|
|
14
|
+
const [settings, domains] = await Promise.all([
|
|
15
|
+
(0, shell_1.getRepository)(setting_base_1.Setting).find({ where: { domain: { id: (0, typeorm_1.In)(chainIds) }, name: agent_spec_js_1.AI_AGENTS_SETTING } }),
|
|
16
|
+
(0, shell_1.getRepository)(shell_1.Domain).find({ where: { id: (0, typeorm_1.In)(chainIds) } })
|
|
17
|
+
]);
|
|
18
|
+
const settingOf = new Map(settings.map(one => [one.domainId, (0, agent_spec_js_1.parseAiAgentsSetting)(one.value)]));
|
|
19
|
+
const own = settingOf.get(domain.id) ?? { agents: [] };
|
|
20
|
+
const turnedOff = new Set(own.turnedOff ?? []);
|
|
21
|
+
/* The domain's own agents whatever their state; an inherited one only while its owner keeps it on. */
|
|
22
|
+
const declared = [];
|
|
23
|
+
for (const domainId of chainIds) {
|
|
24
|
+
const setting = settingOf.get(domainId);
|
|
25
|
+
if (!setting)
|
|
26
|
+
continue;
|
|
27
|
+
for (const spec of setting.agents) {
|
|
28
|
+
if (domainId !== domain.id && !spec.active)
|
|
29
|
+
continue;
|
|
30
|
+
declared.push({ spec, ownerDomainId: domainId });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const keyNames = declared.map(one => (0, agent_spec_js_1.aiAgentKeyName)(one.spec.id));
|
|
34
|
+
const keyRows = keyNames.length
|
|
35
|
+
? await (0, shell_1.getRepository)(shell_1.EnvVar).find({ where: { domain: { id: (0, typeorm_1.In)(chainIds) }, name: (0, typeorm_1.In)(keyNames), active: true } })
|
|
36
|
+
: [];
|
|
37
|
+
const keyOf = new Map((0, shell_1.dedupByClosestDomain)(keyRows, chainIds, row => row.name).map(row => [row.name, row]));
|
|
38
|
+
const entries = declared.map(({ spec, ownerDomainId }) => {
|
|
39
|
+
const inherited = ownerDomainId !== domain.id;
|
|
40
|
+
const key = keyOf.get((0, agent_spec_js_1.aiAgentKeyName)(spec.id));
|
|
41
|
+
const keyHere = key?.domainId === domain.id;
|
|
42
|
+
const keyState = !key?.value ? 'missing' : !keyHere ? 'inherited' : inherited ? 'overridden' : 'own';
|
|
43
|
+
const off = inherited && turnedOff.has(spec.id);
|
|
44
|
+
return {
|
|
45
|
+
spec,
|
|
46
|
+
ownerDomainId,
|
|
47
|
+
inherited,
|
|
48
|
+
turnedOff: off,
|
|
49
|
+
keyState,
|
|
50
|
+
...(key?.value && { keyDomainId: key.domainId, apiKey: key.value, keyVersion: `${key.domainId}:${key.updatedAt?.valueOf() ?? ''}` }),
|
|
51
|
+
...(keyHere && key?.value && { keyHint: (0, shell_1.secretHint)(key.value) }),
|
|
52
|
+
usable: spec.active && !off && keyState !== 'missing'
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
const nameOf = new Map(domains.map(one => [one.id, one.name]));
|
|
56
|
+
const { defaultAgentId, defaultFrom } = pickDefault(chainIds, settingOf, entries);
|
|
57
|
+
return {
|
|
58
|
+
domainId: domain.id,
|
|
59
|
+
chain: chainIds.map(id => ({ id, name: nameOf.get(id) ?? id })),
|
|
60
|
+
entries,
|
|
61
|
+
own,
|
|
62
|
+
...(defaultAgentId && { defaultAgentId }),
|
|
63
|
+
defaultFrom
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A caller that holds only the domain's id (a background job, a webhook) still gets the domain's whole chain: the walk
|
|
68
|
+
* needs `parentId`, which an id alone does not carry.
|
|
69
|
+
*/
|
|
70
|
+
async function fullDomain(given) {
|
|
71
|
+
if (given.parentId !== undefined)
|
|
72
|
+
return given;
|
|
73
|
+
const found = await (0, shell_1.getRepository)(shell_1.Domain).findOne({ where: { id: given.id } });
|
|
74
|
+
if (!found)
|
|
75
|
+
throw new Error(`no domain ${given.id}`);
|
|
76
|
+
return found;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* This domain's own choice, else the closest ancestor's choice that is usable here, else the first usable agent by
|
|
80
|
+
* name. Registration order never decides it.
|
|
81
|
+
*/
|
|
82
|
+
function pickDefault(chainIds, settingOf, entries) {
|
|
83
|
+
const usable = new Map(entries.filter(one => one.usable).map(one => [one.spec.id, one]));
|
|
84
|
+
for (const [index, domainId] of chainIds.entries()) {
|
|
85
|
+
const chosen = settingOf.get(domainId)?.defaultAgentId;
|
|
86
|
+
if (chosen && usable.has(chosen))
|
|
87
|
+
return { defaultAgentId: chosen, defaultFrom: index === 0 ? 'own' : 'inherited' };
|
|
88
|
+
}
|
|
89
|
+
const [first] = [...usable.values()].sort((a, b) => a.spec.name.localeCompare(b.spec.name));
|
|
90
|
+
return first ? { defaultAgentId: first.spec.id, defaultFrom: 'first' } : { defaultFrom: 'none' };
|
|
91
|
+
}
|
|
92
|
+
/** Every domain below this one, any depth. */
|
|
93
|
+
async function readDescendantDomains(domain) {
|
|
94
|
+
const found = [];
|
|
95
|
+
const seen = new Set([domain.id]);
|
|
96
|
+
let frontier = [domain.id];
|
|
97
|
+
for (let depth = 0; frontier.length && depth < 10; depth++) {
|
|
98
|
+
const children = await (0, shell_1.getRepository)(shell_1.Domain).find({ where: { parent: { id: (0, typeorm_1.In)(frontier) } } });
|
|
99
|
+
const fresh = children.filter(one => !seen.has(one.id));
|
|
100
|
+
fresh.forEach(one => seen.add(one.id));
|
|
101
|
+
found.push(...fresh);
|
|
102
|
+
frontier = fresh.map(one => one.id);
|
|
103
|
+
}
|
|
104
|
+
return found;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* For each of this domain's agents, the domains below that use it - every descendant that has not turned it off.
|
|
108
|
+
* The owner sees these before deleting, turning off or rekeying an agent (§3-3 point 8).
|
|
109
|
+
*/
|
|
110
|
+
async function readAgentUsersBelow(domain, agentIds) {
|
|
111
|
+
const result = new Map(agentIds.map(id => [id, []]));
|
|
112
|
+
if (!agentIds.length)
|
|
113
|
+
return result;
|
|
114
|
+
const descendants = await readDescendantDomains(domain);
|
|
115
|
+
if (!descendants.length)
|
|
116
|
+
return result;
|
|
117
|
+
const settings = await (0, shell_1.getRepository)(setting_base_1.Setting).find({
|
|
118
|
+
where: { domain: { id: (0, typeorm_1.In)(descendants.map(one => one.id)) }, name: agent_spec_js_1.AI_AGENTS_SETTING }
|
|
119
|
+
});
|
|
120
|
+
const offOf = new Map(settings.map(one => [one.domainId, new Set((0, agent_spec_js_1.parseAiAgentsSetting)(one.value).turnedOff ?? [])]));
|
|
121
|
+
for (const below of descendants) {
|
|
122
|
+
for (const agentId of agentIds) {
|
|
123
|
+
if (!offOf.get(below.id)?.has(agentId))
|
|
124
|
+
result.get(agentId).push({ id: below.id, name: below.name });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=agent-directory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-directory.js","sourceRoot":"","sources":["../../../server/service/agent/agent-directory.ts"],"names":[],"mappings":"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","sourcesContent":["import { In } from 'typeorm'\n\nimport {\n dedupByClosestDomain,\n Domain,\n EnvVar,\n getDomainIdsWithAncestors,\n getRepository,\n secretHint\n} from '@things-factory/shell'\nimport { Setting } from '@things-factory/setting-base'\n\nimport {\n AI_AGENTS_SETTING,\n aiAgentKeyName,\n type AiAgentSpec,\n type AiAgentsSetting,\n parseAiAgentsSetting\n} from './agent-spec.js'\n\n/**\n * Which agents a domain can use, read up its chain of domains (design/ai/agent-settings.md §3-3).\n *\n * A domain sees its own agents and every agent of the domains above it. The definition belongs to the domain that\n * declared it; the key is the closest `AiAgent::<id>::apiKey` EnvVar, so a domain below can answer an inherited agent\n * with a key of its own.\n */\n\n/**\n * Where the key an agent answers with comes from, seen from the domain asking:\n * own - this domain's key for its own agent\n * inherited - a domain above holds the key\n * overridden - an inherited agent answered with this domain's key\n * missing - no key anywhere on the chain; the agent cannot answer\n */\nexport type AiAgentKeyState = 'own' | 'inherited' | 'overridden' | 'missing'\n\nexport interface AiAgentEntry {\n spec: AiAgentSpec\n /** The domain that declared the agent. */\n ownerDomainId: string\n inherited: boolean\n /** An inherited agent this domain turned off for itself. */\n turnedOff: boolean\n keyState: AiAgentKeyState\n /** The domain whose key answers. */\n keyDomainId?: string\n /** Shown only for a key of this domain (ADR-0112 decision 5). A key from above is never hinted below. */\n keyHint?: string\n /** Active, not turned off here, and has a key. */\n usable: boolean\n /** Server-only - never put on a GraphQL type. */\n apiKey?: string\n /** For a client cache: changes whenever the key does. */\n keyVersion?: string\n}\n\nexport interface AiAgentDirectory {\n domainId: string\n /** The domain and its ancestors, closest first, with names for the screen. */\n chain: { id: string; name: string }[]\n /** This domain's own agents first, then each ancestor's, closest first. */\n entries: AiAgentEntry[]\n /** This domain's stored setting, as parsed. */\n own: AiAgentsSetting\n /** The agent a request without a choice gets, and how it was decided. */\n defaultAgentId?: string\n defaultFrom: 'own' | 'inherited' | 'first' | 'none'\n}\n\nexport async function readAiAgentDirectory(given: Domain | { id: string }): Promise<AiAgentDirectory> {\n const domain = await fullDomain(given)\n const chainIds = await getDomainIdsWithAncestors(domain)\n const [settings, domains] = await Promise.all([\n getRepository(Setting).find({ where: { domain: { id: In(chainIds) }, name: AI_AGENTS_SETTING } }),\n getRepository(Domain).find({ where: { id: In(chainIds) } })\n ])\n\n const settingOf = new Map(settings.map(one => [one.domainId, parseAiAgentsSetting(one.value)]))\n const own = settingOf.get(domain.id) ?? { agents: [] }\n const turnedOff = new Set(own.turnedOff ?? [])\n\n /* The domain's own agents whatever their state; an inherited one only while its owner keeps it on. */\n const declared: { spec: AiAgentSpec; ownerDomainId: string }[] = []\n for (const domainId of chainIds) {\n const setting = settingOf.get(domainId)\n if (!setting) continue\n for (const spec of setting.agents) {\n if (domainId !== domain.id && !spec.active) continue\n declared.push({ spec, ownerDomainId: domainId })\n }\n }\n\n const keyNames = declared.map(one => aiAgentKeyName(one.spec.id))\n const keyRows = keyNames.length\n ? await getRepository(EnvVar).find({ where: { domain: { id: In(chainIds) }, name: In(keyNames), active: true } })\n : []\n const keyOf = new Map(dedupByClosestDomain(keyRows, chainIds, row => row.name).map(row => [row.name, row]))\n\n const entries = declared.map(({ spec, ownerDomainId }): AiAgentEntry => {\n const inherited = ownerDomainId !== domain.id\n const key = keyOf.get(aiAgentKeyName(spec.id))\n const keyHere = key?.domainId === domain.id\n const keyState: AiAgentKeyState = !key?.value ? 'missing' : !keyHere ? 'inherited' : inherited ? 'overridden' : 'own'\n const off = inherited && turnedOff.has(spec.id)\n return {\n spec,\n ownerDomainId,\n inherited,\n turnedOff: off,\n keyState,\n ...(key?.value && { keyDomainId: key.domainId, apiKey: key.value, keyVersion: `${key.domainId}:${key.updatedAt?.valueOf() ?? ''}` }),\n ...(keyHere && key?.value && { keyHint: secretHint(key.value) }),\n usable: spec.active && !off && keyState !== 'missing'\n }\n })\n\n const nameOf = new Map(domains.map(one => [one.id, one.name]))\n const { defaultAgentId, defaultFrom } = pickDefault(chainIds, settingOf, entries)\n\n return {\n domainId: domain.id,\n chain: chainIds.map(id => ({ id, name: nameOf.get(id) ?? id })),\n entries,\n own,\n ...(defaultAgentId && { defaultAgentId }),\n defaultFrom\n }\n}\n\n/**\n * A caller that holds only the domain's id (a background job, a webhook) still gets the domain's whole chain: the walk\n * needs `parentId`, which an id alone does not carry.\n */\nexport async function fullDomain(given: Domain | { id: string }): Promise<Domain> {\n if ((given as Domain).parentId !== undefined) return given as Domain\n const found = await getRepository(Domain).findOne({ where: { id: given.id } })\n if (!found) throw new Error(`no domain ${given.id}`)\n return found\n}\n\n/**\n * This domain's own choice, else the closest ancestor's choice that is usable here, else the first usable agent by\n * name. Registration order never decides it.\n */\nfunction pickDefault(\n chainIds: string[],\n settingOf: Map<string, AiAgentsSetting>,\n entries: AiAgentEntry[]\n): { defaultAgentId?: string; defaultFrom: AiAgentDirectory['defaultFrom'] } {\n const usable = new Map(entries.filter(one => one.usable).map(one => [one.spec.id, one]))\n for (const [index, domainId] of chainIds.entries()) {\n const chosen = settingOf.get(domainId)?.defaultAgentId\n if (chosen && usable.has(chosen)) return { defaultAgentId: chosen, defaultFrom: index === 0 ? 'own' : 'inherited' }\n }\n const [first] = [...usable.values()].sort((a, b) => a.spec.name.localeCompare(b.spec.name))\n return first ? { defaultAgentId: first.spec.id, defaultFrom: 'first' } : { defaultFrom: 'none' }\n}\n\n/** Every domain below this one, any depth. */\nexport async function readDescendantDomains(domain: Domain): Promise<Domain[]> {\n const found: Domain[] = []\n const seen = new Set<string>([domain.id])\n let frontier = [domain.id]\n for (let depth = 0; frontier.length && depth < 10; depth++) {\n const children = await getRepository(Domain).find({ where: { parent: { id: In(frontier) } } })\n const fresh = children.filter(one => !seen.has(one.id))\n fresh.forEach(one => seen.add(one.id))\n found.push(...fresh)\n frontier = fresh.map(one => one.id)\n }\n return found\n}\n\n/**\n * For each of this domain's agents, the domains below that use it - every descendant that has not turned it off.\n * The owner sees these before deleting, turning off or rekeying an agent (§3-3 point 8).\n */\nexport async function readAgentUsersBelow(\n domain: Domain,\n agentIds: string[]\n): Promise<Map<string, { id: string; name: string }[]>> {\n const result = new Map(agentIds.map(id => [id, [] as { id: string; name: string }[]]))\n if (!agentIds.length) return result\n const descendants = await readDescendantDomains(domain)\n if (!descendants.length) return result\n\n const settings = await getRepository(Setting).find({\n where: { domain: { id: In(descendants.map(one => one.id)) }, name: AI_AGENTS_SETTING }\n })\n const offOf = new Map(settings.map(one => [one.domainId, new Set(parseAiAgentsSetting(one.value).turnedOff ?? [])]))\n for (const below of descendants) {\n for (const agentId of agentIds) {\n if (!offOf.get(below.id)?.has(agentId)) result.get(agentId)!.push({ id: below.id, name: below.name })\n }\n }\n return result\n}\n"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { EntityManager } from 'typeorm';
|
|
2
|
+
import { Domain } from '@things-factory/shell';
|
|
3
|
+
/** One of this domain's own agents as the settings card sends it. No id means a new agent. */
|
|
4
|
+
export interface AiAgentWrite {
|
|
5
|
+
id?: string;
|
|
6
|
+
name: string;
|
|
7
|
+
provider: string;
|
|
8
|
+
model: string;
|
|
9
|
+
baseURL?: string;
|
|
10
|
+
organization?: string;
|
|
11
|
+
active: boolean;
|
|
12
|
+
/** A new key. Left out, the stored key stays (ADR-0112 decision 3). */
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
}
|
|
15
|
+
/** This domain's key for an agent it inherits - set one, or clear it to go back to the key above. */
|
|
16
|
+
export interface InheritedKeyWrite {
|
|
17
|
+
agentId: string;
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
clear?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface AiAgentsWrite {
|
|
22
|
+
/** The whole list of this domain's own agents. One that is left out is deleted. */
|
|
23
|
+
agents: AiAgentWrite[];
|
|
24
|
+
defaultAgentId?: string;
|
|
25
|
+
turnedOff?: string[];
|
|
26
|
+
inheritedKeys?: InheritedKeyWrite[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Saves a domain's agents and their keys in one transaction, touching only the request domain's rows - except that
|
|
30
|
+
* deleting an agent also removes the keys domains below had set for it (design/ai/agent-settings.md §3-2, §3-3).
|
|
31
|
+
*/
|
|
32
|
+
export declare function saveAiAgents(write: AiAgentsWrite, { tx, domain, user }: {
|
|
33
|
+
tx: EntityManager;
|
|
34
|
+
domain: Domain;
|
|
35
|
+
user?: {
|
|
36
|
+
id: string;
|
|
37
|
+
};
|
|
38
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.saveAiAgents = saveAiAgents;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const typeorm_1 = require("typeorm");
|
|
6
|
+
const shell_1 = require("@things-factory/shell");
|
|
7
|
+
const setting_base_1 = require("@things-factory/setting-base");
|
|
8
|
+
const agent_directory_js_1 = require("./agent-directory.js");
|
|
9
|
+
const agent_spec_js_1 = require("./agent-spec.js");
|
|
10
|
+
const provider_models_js_1 = require("./provider-models.js");
|
|
11
|
+
/**
|
|
12
|
+
* Saves a domain's agents and their keys in one transaction, touching only the request domain's rows - except that
|
|
13
|
+
* deleting an agent also removes the keys domains below had set for it (design/ai/agent-settings.md §3-2, §3-3).
|
|
14
|
+
*/
|
|
15
|
+
async function saveAiAgents(write, { tx, domain, user }) {
|
|
16
|
+
const directory = await (0, agent_directory_js_1.readAiAgentDirectory)(domain);
|
|
17
|
+
const storedById = new Map(directory.own.agents.map(one => [one.id, one]));
|
|
18
|
+
const inheritedIds = new Set(directory.entries.filter(one => one.inherited).map(one => one.spec.id));
|
|
19
|
+
const agents = write.agents.map(one => normalize(one, storedById));
|
|
20
|
+
refuseDuplicateNames(agents);
|
|
21
|
+
await refuseNamesTakenAround(domain, agents, directory.entries.filter(one => one.inherited).map(one => one.spec));
|
|
22
|
+
const storedKeyOf = new Map(directory.entries.filter(one => !one.inherited && one.keyState === 'own').map(one => [one.spec.id, one.apiKey]));
|
|
23
|
+
for (const [index, agent] of agents.entries()) {
|
|
24
|
+
const newKey = write.agents[index].apiKey?.trim();
|
|
25
|
+
const key = newKey || storedKeyOf.get(agent.id);
|
|
26
|
+
if (!key)
|
|
27
|
+
throw new shell_1.Refusal('ai-agent-key-required', { agent: agent.name }, `Enter the API key of "${agent.name}".`);
|
|
28
|
+
if (needsModelCheck(agent, storedById.get(agent.id), !!newKey)) {
|
|
29
|
+
const offered = await (0, provider_models_js_1.listProviderModels)({ provider: agent.provider, apiKey: key, baseURL: agent.baseURL, organization: agent.organization });
|
|
30
|
+
if (!offered.includes(agent.model)) {
|
|
31
|
+
throw new shell_1.Refusal('ai-agent-model-not-offered', { agent: agent.name, model: agent.model }, `The provider does not offer "${agent.model}" to this key.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const ownIds = new Set(agents.map(one => one.id));
|
|
36
|
+
const defaultAgentId = write.defaultAgentId && (ownIds.has(write.defaultAgentId) || inheritedIds.has(write.defaultAgentId))
|
|
37
|
+
? write.defaultAgentId
|
|
38
|
+
: undefined;
|
|
39
|
+
if (write.defaultAgentId && !defaultAgentId) {
|
|
40
|
+
throw new shell_1.Refusal('ai-agent-not-available', { agentId: write.defaultAgentId }, 'The default agent is not one this domain can use.');
|
|
41
|
+
}
|
|
42
|
+
const turnedOff = (write.turnedOff ?? []).filter(id => inheritedIds.has(id));
|
|
43
|
+
await upsertSetting(tx, domain, user, (0, agent_spec_js_1.stringifyAiAgentsSetting)({ agents, defaultAgentId, turnedOff }));
|
|
44
|
+
for (const [index, agent] of agents.entries()) {
|
|
45
|
+
const newKey = write.agents[index].apiKey?.trim();
|
|
46
|
+
if (newKey)
|
|
47
|
+
await upsertKey(tx, domain, agent, newKey);
|
|
48
|
+
}
|
|
49
|
+
for (const one of write.inheritedKeys ?? []) {
|
|
50
|
+
if (!inheritedIds.has(one.agentId)) {
|
|
51
|
+
throw new shell_1.Refusal('ai-agent-not-available', { agentId: one.agentId }, 'That agent is not inherited by this domain.');
|
|
52
|
+
}
|
|
53
|
+
if (one.clear) {
|
|
54
|
+
await tx.getRepository(shell_1.EnvVar).delete({ domain: { id: domain.id }, name: (0, agent_spec_js_1.aiAgentKeyName)(one.agentId) });
|
|
55
|
+
}
|
|
56
|
+
else if (one.apiKey?.trim()) {
|
|
57
|
+
const spec = directory.entries.find(entry => entry.spec.id === one.agentId).spec;
|
|
58
|
+
await upsertKey(tx, domain, spec, one.apiKey.trim());
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const deleted = directory.own.agents.filter(one => !ownIds.has(one.id)).map(one => (0, agent_spec_js_1.aiAgentKeyName)(one.id));
|
|
62
|
+
if (deleted.length) {
|
|
63
|
+
const below = await (0, agent_directory_js_1.readDescendantDomains)(domain);
|
|
64
|
+
await tx.getRepository(shell_1.EnvVar).delete({
|
|
65
|
+
domain: { id: (0, typeorm_1.In)([domain.id, ...below.map(one => one.id)]) },
|
|
66
|
+
name: (0, typeorm_1.In)(deleted)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function normalize(write, storedById) {
|
|
71
|
+
const name = write.name?.trim();
|
|
72
|
+
if (!name)
|
|
73
|
+
throw new shell_1.Refusal('ai-agent-name-required', {}, 'Every agent needs a name.');
|
|
74
|
+
if (!(0, agent_spec_js_1.isAiAgentProvider)(write.provider)) {
|
|
75
|
+
throw new shell_1.Refusal('ai-agent-provider-unknown', { provider: String(write.provider) }, `"${write.provider}" is not a provider an agent can use.`);
|
|
76
|
+
}
|
|
77
|
+
const model = write.model?.trim();
|
|
78
|
+
if (!model)
|
|
79
|
+
throw new shell_1.Refusal('ai-agent-model-required', { agent: name }, `Pick the model of "${name}".`);
|
|
80
|
+
if (write.id && !storedById.has(write.id)) {
|
|
81
|
+
throw new shell_1.Refusal('ai-agent-not-in-domain', { agentId: write.id }, 'That agent does not belong to this domain.');
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
id: write.id || (0, crypto_1.randomUUID)(),
|
|
85
|
+
name,
|
|
86
|
+
provider: write.provider,
|
|
87
|
+
model,
|
|
88
|
+
...(write.baseURL?.trim() && { baseURL: write.baseURL.trim() }),
|
|
89
|
+
...(write.provider === 'openai' && write.organization?.trim() && { organization: write.organization.trim() }),
|
|
90
|
+
active: write.active !== false
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/** The model is checked against the provider's list when anything that decides the list, or the model, changed. */
|
|
94
|
+
function needsModelCheck(agent, stored, newKey) {
|
|
95
|
+
if (!stored || newKey)
|
|
96
|
+
return true;
|
|
97
|
+
return stored.provider !== agent.provider || stored.model !== agent.model || stored.baseURL !== agent.baseURL || stored.organization !== agent.organization;
|
|
98
|
+
}
|
|
99
|
+
function refuseDuplicateNames(agents) {
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (const agent of agents) {
|
|
102
|
+
const key = agent.name.toLowerCase();
|
|
103
|
+
if (seen.has(key))
|
|
104
|
+
throw new shell_1.Refusal('ai-agent-name-taken', { name: agent.name, domain: '' }, `There is already an agent named "${agent.name}".`);
|
|
105
|
+
seen.add(key);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A name is one across the chain, so a picker never shows two agents of the same name: not the name of an agent
|
|
110
|
+
* inherited from above, nor of an agent a domain below declared.
|
|
111
|
+
*/
|
|
112
|
+
async function refuseNamesTakenAround(domain, agents, inherited) {
|
|
113
|
+
const taken = new Map();
|
|
114
|
+
for (const spec of inherited)
|
|
115
|
+
taken.set(spec.name.toLowerCase(), '');
|
|
116
|
+
const below = await (0, agent_directory_js_1.readDescendantDomains)(domain);
|
|
117
|
+
if (below.length) {
|
|
118
|
+
const settings = await (0, shell_1.getRepository)(setting_base_1.Setting).find({
|
|
119
|
+
where: { domain: { id: (0, typeorm_1.In)(below.map(one => one.id)) }, name: agent_spec_js_1.AI_AGENTS_SETTING }
|
|
120
|
+
});
|
|
121
|
+
const nameOf = new Map(below.map(one => [one.id, one.name]));
|
|
122
|
+
for (const setting of settings) {
|
|
123
|
+
for (const spec of (0, agent_spec_js_1.parseAiAgentsSetting)(setting.value).agents)
|
|
124
|
+
taken.set(spec.name.toLowerCase(), nameOf.get(setting.domainId) ?? '');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const agent of agents) {
|
|
128
|
+
if (taken.has(agent.name.toLowerCase())) {
|
|
129
|
+
throw new shell_1.Refusal('ai-agent-name-taken', { name: agent.name, domain: taken.get(agent.name.toLowerCase()) ?? '' }, `There is already an agent named "${agent.name}".`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function upsertSetting(tx, domain, user, value) {
|
|
134
|
+
const repository = tx.getRepository(setting_base_1.Setting);
|
|
135
|
+
const stored = await repository.findOne({ where: { domain: { id: domain.id }, name: agent_spec_js_1.AI_AGENTS_SETTING } });
|
|
136
|
+
await repository.save({
|
|
137
|
+
...(stored ?? { domain: { id: domain.id }, name: agent_spec_js_1.AI_AGENTS_SETTING, category: agent_spec_js_1.AI_AGENTS_SETTING_CATEGORY, creator: user }),
|
|
138
|
+
value,
|
|
139
|
+
updater: user
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function upsertKey(tx, domain, agent, apiKey) {
|
|
143
|
+
const repository = tx.getRepository(shell_1.EnvVar);
|
|
144
|
+
const name = (0, agent_spec_js_1.aiAgentKeyName)(agent.id);
|
|
145
|
+
const stored = await repository.findOne({ where: { domain: { id: domain.id }, name } });
|
|
146
|
+
await repository.save({
|
|
147
|
+
...(stored ?? { domain: { id: domain.id }, name }),
|
|
148
|
+
value: apiKey,
|
|
149
|
+
description: `API key of the AI agent "${agent.name}"`,
|
|
150
|
+
active: true
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=agent-settings-save.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-settings-save.js","sourceRoot":"","sources":["../../../server/service/agent/agent-settings-save.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from 'crypto'\nimport { EntityManager, In } from 'typeorm'\n\nimport { Domain, EnvVar, getRepository, Refusal } from '@things-factory/shell'\nimport { Setting } from '@things-factory/setting-base'\n\nimport { readAiAgentDirectory, readDescendantDomains } from './agent-directory.js'\nimport {\n AI_AGENTS_SETTING,\n AI_AGENTS_SETTING_CATEGORY,\n aiAgentKeyName,\n type AiAgentSpec,\n isAiAgentProvider,\n parseAiAgentsSetting,\n stringifyAiAgentsSetting\n} from './agent-spec.js'\nimport { listProviderModels } from './provider-models.js'\n\n/** One of this domain's own agents as the settings card sends it. No id means a new agent. */\nexport interface AiAgentWrite {\n id?: string\n name: string\n provider: string\n model: string\n baseURL?: string\n organization?: string\n active: boolean\n /** A new key. Left out, the stored key stays (ADR-0112 decision 3). */\n apiKey?: string\n}\n\n/** This domain's key for an agent it inherits - set one, or clear it to go back to the key above. */\nexport interface InheritedKeyWrite {\n agentId: string\n apiKey?: string\n clear?: boolean\n}\n\nexport interface AiAgentsWrite {\n /** The whole list of this domain's own agents. One that is left out is deleted. */\n agents: AiAgentWrite[]\n defaultAgentId?: string\n turnedOff?: string[]\n inheritedKeys?: InheritedKeyWrite[]\n}\n\n/**\n * Saves a domain's agents and their keys in one transaction, touching only the request domain's rows - except that\n * deleting an agent also removes the keys domains below had set for it (design/ai/agent-settings.md §3-2, §3-3).\n */\nexport async function saveAiAgents(\n write: AiAgentsWrite,\n { tx, domain, user }: { tx: EntityManager; domain: Domain; user?: { id: string } }\n): Promise<void> {\n const directory = await readAiAgentDirectory(domain)\n const storedById = new Map(directory.own.agents.map(one => [one.id, one]))\n const inheritedIds = new Set(directory.entries.filter(one => one.inherited).map(one => one.spec.id))\n\n const agents = write.agents.map(one => normalize(one, storedById))\n refuseDuplicateNames(agents)\n await refuseNamesTakenAround(domain, agents, directory.entries.filter(one => one.inherited).map(one => one.spec))\n\n const storedKeyOf = new Map(\n directory.entries.filter(one => !one.inherited && one.keyState === 'own').map(one => [one.spec.id, one.apiKey!])\n )\n for (const [index, agent] of agents.entries()) {\n const newKey = write.agents[index].apiKey?.trim()\n const key = newKey || storedKeyOf.get(agent.id)\n if (!key) throw new Refusal('ai-agent-key-required', { agent: agent.name }, `Enter the API key of \"${agent.name}\".`)\n if (needsModelCheck(agent, storedById.get(agent.id), !!newKey)) {\n const offered = await listProviderModels({ provider: agent.provider, apiKey: key, baseURL: agent.baseURL, organization: agent.organization })\n if (!offered.includes(agent.model)) {\n throw new Refusal(\n 'ai-agent-model-not-offered',\n { agent: agent.name, model: agent.model },\n `The provider does not offer \"${agent.model}\" to this key.`\n )\n }\n }\n }\n\n const ownIds = new Set(agents.map(one => one.id))\n const defaultAgentId = write.defaultAgentId && (ownIds.has(write.defaultAgentId) || inheritedIds.has(write.defaultAgentId))\n ? write.defaultAgentId\n : undefined\n if (write.defaultAgentId && !defaultAgentId) {\n throw new Refusal('ai-agent-not-available', { agentId: write.defaultAgentId }, 'The default agent is not one this domain can use.')\n }\n const turnedOff = (write.turnedOff ?? []).filter(id => inheritedIds.has(id))\n\n await upsertSetting(tx, domain, user, stringifyAiAgentsSetting({ agents, defaultAgentId, turnedOff }))\n\n for (const [index, agent] of agents.entries()) {\n const newKey = write.agents[index].apiKey?.trim()\n if (newKey) await upsertKey(tx, domain, agent, newKey)\n }\n\n for (const one of write.inheritedKeys ?? []) {\n if (!inheritedIds.has(one.agentId)) {\n throw new Refusal('ai-agent-not-available', { agentId: one.agentId }, 'That agent is not inherited by this domain.')\n }\n if (one.clear) {\n await tx.getRepository(EnvVar).delete({ domain: { id: domain.id }, name: aiAgentKeyName(one.agentId) })\n } else if (one.apiKey?.trim()) {\n const spec = directory.entries.find(entry => entry.spec.id === one.agentId)!.spec\n await upsertKey(tx, domain, spec, one.apiKey.trim())\n }\n }\n\n const deleted = directory.own.agents.filter(one => !ownIds.has(one.id)).map(one => aiAgentKeyName(one.id))\n if (deleted.length) {\n const below = await readDescendantDomains(domain)\n await tx.getRepository(EnvVar).delete({\n domain: { id: In([domain.id, ...below.map(one => one.id)]) },\n name: In(deleted)\n })\n }\n}\n\nfunction normalize(write: AiAgentWrite, storedById: Map<string, AiAgentSpec>): AiAgentSpec {\n const name = write.name?.trim()\n if (!name) throw new Refusal('ai-agent-name-required', {}, 'Every agent needs a name.')\n if (!isAiAgentProvider(write.provider)) {\n throw new Refusal('ai-agent-provider-unknown', { provider: String(write.provider) }, `\"${write.provider}\" is not a provider an agent can use.`)\n }\n const model = write.model?.trim()\n if (!model) throw new Refusal('ai-agent-model-required', { agent: name }, `Pick the model of \"${name}\".`)\n if (write.id && !storedById.has(write.id)) {\n throw new Refusal('ai-agent-not-in-domain', { agentId: write.id }, 'That agent does not belong to this domain.')\n }\n return {\n id: write.id || randomUUID(),\n name,\n provider: write.provider,\n model,\n ...(write.baseURL?.trim() && { baseURL: write.baseURL.trim() }),\n ...(write.provider === 'openai' && write.organization?.trim() && { organization: write.organization.trim() }),\n active: write.active !== false\n }\n}\n\n/** The model is checked against the provider's list when anything that decides the list, or the model, changed. */\nfunction needsModelCheck(agent: AiAgentSpec, stored: AiAgentSpec | undefined, newKey: boolean): boolean {\n if (!stored || newKey) return true\n return stored.provider !== agent.provider || stored.model !== agent.model || stored.baseURL !== agent.baseURL || stored.organization !== agent.organization\n}\n\nfunction refuseDuplicateNames(agents: AiAgentSpec[]) {\n const seen = new Set<string>()\n for (const agent of agents) {\n const key = agent.name.toLowerCase()\n if (seen.has(key)) throw new Refusal('ai-agent-name-taken', { name: agent.name, domain: '' }, `There is already an agent named \"${agent.name}\".`)\n seen.add(key)\n }\n}\n\n/**\n * A name is one across the chain, so a picker never shows two agents of the same name: not the name of an agent\n * inherited from above, nor of an agent a domain below declared.\n */\nasync function refuseNamesTakenAround(domain: Domain, agents: AiAgentSpec[], inherited: AiAgentSpec[]) {\n const taken = new Map<string, string>()\n for (const spec of inherited) taken.set(spec.name.toLowerCase(), '')\n const below = await readDescendantDomains(domain)\n if (below.length) {\n const settings = await getRepository(Setting).find({\n where: { domain: { id: In(below.map(one => one.id)) }, name: AI_AGENTS_SETTING }\n })\n const nameOf = new Map(below.map(one => [one.id, one.name]))\n for (const setting of settings) {\n for (const spec of parseAiAgentsSetting(setting.value).agents) taken.set(spec.name.toLowerCase(), nameOf.get(setting.domainId) ?? '')\n }\n }\n for (const agent of agents) {\n if (taken.has(agent.name.toLowerCase())) {\n throw new Refusal(\n 'ai-agent-name-taken',\n { name: agent.name, domain: taken.get(agent.name.toLowerCase()) ?? '' },\n `There is already an agent named \"${agent.name}\".`\n )\n }\n }\n}\n\nasync function upsertSetting(tx: EntityManager, domain: Domain, user: { id: string } | undefined, value: string) {\n const repository = tx.getRepository(Setting)\n const stored = await repository.findOne({ where: { domain: { id: domain.id }, name: AI_AGENTS_SETTING } })\n await repository.save({\n ...(stored ?? { domain: { id: domain.id }, name: AI_AGENTS_SETTING, category: AI_AGENTS_SETTING_CATEGORY, creator: user }),\n value,\n updater: user\n } as any)\n}\n\nasync function upsertKey(tx: EntityManager, domain: Domain, agent: AiAgentSpec, apiKey: string) {\n const repository = tx.getRepository(EnvVar)\n const name = aiAgentKeyName(agent.id)\n const stored = await repository.findOne({ where: { domain: { id: domain.id }, name } })\n await repository.save({\n ...(stored ?? { domain: { id: domain.id }, name }),\n value: apiKey,\n description: `API key of the AI agent \"${agent.name}\"`,\n active: true\n } as any)\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An AI agent - a provider, a model and a key that together can answer (design/ai/agent-settings.md §3).
|
|
3
|
+
*
|
|
4
|
+
* A domain keeps its agents in one Setting row (`ai-agents`) and each key in an EnvVar of its own, so the
|
|
5
|
+
* definition and the secret live where the framework already keeps domain settings and secrets (ADR-0112).
|
|
6
|
+
* This file is the shape and its parsing - no database.
|
|
7
|
+
*/
|
|
8
|
+
/** The providers an agent may name. Three, by decision (2026-10-12). */
|
|
9
|
+
export declare const AI_AGENT_PROVIDERS: readonly ["openai", "anthropic", "gemini"];
|
|
10
|
+
export type AiAgentProvider = (typeof AI_AGENT_PROVIDERS)[number];
|
|
11
|
+
/** The Setting row that holds a domain's agents. */
|
|
12
|
+
export declare const AI_AGENTS_SETTING = "ai-agents";
|
|
13
|
+
export declare const AI_AGENTS_SETTING_CATEGORY = "ai";
|
|
14
|
+
export interface AiAgentSpec {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
provider: AiAgentProvider;
|
|
18
|
+
model: string;
|
|
19
|
+
baseURL?: string;
|
|
20
|
+
/** OpenAI only. */
|
|
21
|
+
organization?: string;
|
|
22
|
+
/** Off takes the agent out of this domain and every domain below it. */
|
|
23
|
+
active: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** What the `ai-agents` Setting row holds for one domain. */
|
|
26
|
+
export interface AiAgentsSetting {
|
|
27
|
+
agents: AiAgentSpec[];
|
|
28
|
+
/** This domain's default - one of its own agents or an inherited one. */
|
|
29
|
+
defaultAgentId?: string;
|
|
30
|
+
/** Inherited agents this domain does not use. */
|
|
31
|
+
turnedOff?: string[];
|
|
32
|
+
}
|
|
33
|
+
/** The EnvVar that holds an agent's key. Named by the agent id, so renaming an agent keeps its key. */
|
|
34
|
+
export declare function aiAgentKeyName(agentId: string): string;
|
|
35
|
+
export declare const AI_AGENT_KEY_PREFIX = "AiAgent::";
|
|
36
|
+
export declare function isAiAgentProvider(value: unknown): value is AiAgentProvider;
|
|
37
|
+
/**
|
|
38
|
+
* Reads a stored `ai-agents` value. The row can also be written through the generic setting doors, so a value that
|
|
39
|
+
* does not parse reads as no agents, and an entry without an id, a name, a known provider or a model is left out -
|
|
40
|
+
* a broken row never becomes an agent that fails at the provider.
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseAiAgentsSetting(value: string | null | undefined): AiAgentsSetting;
|
|
43
|
+
export declare function stringifyAiAgentsSetting(setting: AiAgentsSetting): string;
|